Compare commits
10 Commits
e53da43eac
...
33d3bf33f7
| Author | SHA1 | Date | |
|---|---|---|---|
| 33d3bf33f7 | |||
| afabcb0afa | |||
| 9add816188 | |||
| 1fe5aa8dd1 | |||
| ece1b2e927 | |||
| 10f2a552e3 | |||
| 57cb1a1d38 | |||
| 1f9e6c8c4e | |||
| e5293cbf05 | |||
| e16d7a440e |
44
.gitea/workflows/latest.yaml
Normal file
44
.gitea/workflows/latest.yaml
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
name: Validating
|
||||||
|
run-name: ${{ github.actor }} is runs ci pipeline
|
||||||
|
on: push
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
RUNNER_TOOL_CACHE: /toolcache
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@v5
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets._GITHUB_TOKEN }}
|
||||||
|
enable-cache: true
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
run: uv python install
|
||||||
|
|
||||||
|
- name: Cache uv
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ${{ github.workspace }}/.cache/uv
|
||||||
|
key: uv-cache-${{ runner.os }}
|
||||||
|
restore-keys: uv-cache-${{ runner.os }}
|
||||||
|
|
||||||
|
- name: Install the project
|
||||||
|
run: uv sync --no-install-project --cache-dir ${{ github.workspace }}/.cache/uv
|
||||||
|
|
||||||
|
- name: Cache pre-commit
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/.cache/pre-commit
|
||||||
|
key: pre-commit-cache-${{ runner.os }}-${{ hashFiles('.pre-commit-config.yaml') }}
|
||||||
|
restore-keys: pre-commit-cache-${{ runner.os }}-
|
||||||
|
|
||||||
|
- name: Linter & Formatter
|
||||||
|
run: uv run pre-commit run --all-files
|
||||||
@ -1,4 +1,4 @@
|
|||||||
# Telegram Stickers Generator Git
|
# Telegram Stickers Generator
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
|
|||||||
121
main.py
121
main.py
@ -1,8 +1,9 @@
|
|||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from time import time
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import ffmpeg
|
import ffmpeg
|
||||||
from ffmpeg.nodes import FilterableStream
|
|
||||||
from rich.prompt import Prompt
|
from rich.prompt import Prompt
|
||||||
|
|
||||||
from utils.log import configure_logging
|
from utils.log import configure_logging
|
||||||
@ -11,63 +12,66 @@ PATH = Path(__file__).parent
|
|||||||
INPUT_DIR = PATH / 'input'
|
INPUT_DIR = PATH / 'input'
|
||||||
OUTPUT_DIR = PATH / 'output'
|
OUTPUT_DIR = PATH / 'output'
|
||||||
|
|
||||||
OUTPUT_ARGS = {'vcodec': 'libvpx-vp9', 'pix_fmt': 'yuva420p', 'loglevel': 'error', 'y': None}
|
OUTPUT_ARGS: dict[str, Any] = {
|
||||||
|
'row-mt': 1,
|
||||||
|
'loglevel': 'warning',
|
||||||
|
'max-intra-rate': 100,
|
||||||
|
'quality': 'default',
|
||||||
|
'auto-alt-ref': 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def compress(input_file: Path, output_file: Path, sticker_width: int, bitrate: int | None = None):
|
def compress(input_file: Path, output_file: Path, sticker_width: int, max_size: int):
|
||||||
file_input = ffmpeg.input(input_file)
|
file_input = ffmpeg.input(input_file)
|
||||||
file_output = OUTPUT_DIR / input_file.name.replace(input_file.suffix, '.webm')
|
|
||||||
|
|
||||||
if not isinstance(file_input, FilterableStream):
|
args = OUTPUT_ARGS.copy()
|
||||||
logger.error(f'Failed to process file: {input_file}')
|
|
||||||
raise Exception('Failed to process file')
|
|
||||||
|
|
||||||
stream = ffmpeg.filter(file_input, 'fps', fps=25, round='up')
|
stream = file_input.fps(fps=25, round='up')
|
||||||
stream = ffmpeg.filter(
|
stream = stream.scale(w=sticker_width, h=-1, force_original_aspect_ratio='decrease')
|
||||||
stream, 'scale', sticker_width, -1, force_original_aspect_ratio='decrease'
|
stream = stream.crop(
|
||||||
)
|
out_w=f'min(min(iw,ih),{sticker_width})', out_h=f'min(min(iw,ih),{sticker_width})'
|
||||||
stream = ffmpeg.filter(
|
|
||||||
stream, 'pad', sticker_width, sticker_width, '(ow-iw)/2', '(oh-ih)/2', color='0x00000000'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if input_file.suffix == '.gif':
|
if input_file.suffix in ('.gif', '.mp4'):
|
||||||
duration = float(ffmpeg.probe(input_file)['streams'][0]['duration'])
|
duration = float(ffmpeg.probe(input_file)['streams'][0]['duration'])
|
||||||
|
max_bitrate = int(max_size * 8 / duration) * 0.95
|
||||||
speed_factor = 1
|
speed_factor = 1
|
||||||
|
|
||||||
if duration > 3:
|
if duration > 3:
|
||||||
speed_factor = duration / 3
|
speed_factor = duration / 3
|
||||||
|
|
||||||
stream = ffmpeg.filter(stream, 'setpts', f'PTS/{speed_factor}')
|
stream = stream.setpts(expr=f'PTS*{speed_factor}')
|
||||||
|
args = args | {
|
||||||
|
'minrate': f'{(max_bitrate * .2):.2f}k',
|
||||||
|
'maxrate': f'{max_bitrate:.2f}k',
|
||||||
|
'b:v': f'{(max_bitrate * .65):.2f}k',
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
args = args | {'crf': 0}
|
||||||
|
|
||||||
args = OUTPUT_ARGS
|
stream = stream.output(
|
||||||
|
filename=output_file,
|
||||||
if bitrate:
|
vcodec='libvpx-vp9',
|
||||||
args['video_bitrate'] = f'{bitrate}k'
|
pix_fmt='yuva420p',
|
||||||
|
an=True,
|
||||||
output = ffmpeg.output(stream, filename=file_output, **args)
|
extra_options=args,
|
||||||
ffmpeg.run(output)
|
|
||||||
|
|
||||||
file_size = file_output.stat().st_size
|
|
||||||
|
|
||||||
if file_size / 1024 > 256:
|
|
||||||
if not bitrate:
|
|
||||||
bitrate = int(int(ffmpeg.probe(output_file)['format']['bit_rate']) / 1000)
|
|
||||||
|
|
||||||
bitrate_offset = 100
|
|
||||||
|
|
||||||
if file_size / 256 * 1024 > 2:
|
|
||||||
bitrate_offset = 100 * (file_size / (256 * 1024))
|
|
||||||
|
|
||||||
bitrate = int(bitrate - bitrate_offset)
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
f'File size is too high [{int(file_size / 1024)} KB], reducing to {bitrate}k...'
|
|
||||||
)
|
)
|
||||||
|
stream.run(overwrite_output=True)
|
||||||
|
|
||||||
return compress(input_file, output_file, sticker_width, bitrate)
|
file_size = output_file.stat().st_size
|
||||||
|
|
||||||
|
if file_size / 1024 > max_size:
|
||||||
|
print(stream.compile_line())
|
||||||
|
logger.info(
|
||||||
|
f'Average bitrate: {(int(ffmpeg.probe(output_file)['format']['bit_rate']) / 1024):.2f} KB/s'
|
||||||
|
)
|
||||||
|
logger.error(
|
||||||
|
f'File size ({file_size / 1024:.2f} KB) exceeds the maximum size ({max_size} KB).'
|
||||||
|
)
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
@ -80,27 +84,50 @@ def main():
|
|||||||
for file in OUTPUT_DIR.iterdir():
|
for file in OUTPUT_DIR.iterdir():
|
||||||
file.unlink()
|
file.unlink()
|
||||||
|
|
||||||
sticker_type = Prompt.ask('Do you want to create stickers or emoji?', choices=['s', 'e'])
|
sticker_type = Prompt.ask(
|
||||||
|
'Do you want to create stickers or emoji or icon?', choices=['s', 'e', 'i']
|
||||||
|
)
|
||||||
|
|
||||||
match sticker_type:
|
match sticker_type:
|
||||||
case 's':
|
case 's':
|
||||||
STICKER_WIDTH = 512
|
width = 512
|
||||||
|
size = 256
|
||||||
case 'e':
|
case 'e':
|
||||||
STICKER_WIDTH = 100
|
width = 100
|
||||||
|
size = 64
|
||||||
|
case 'i':
|
||||||
|
width = 100
|
||||||
|
size = 32
|
||||||
case _:
|
case _:
|
||||||
STICKER_WIDTH = 512
|
width = 512
|
||||||
|
size = 256
|
||||||
|
|
||||||
for file in INPUT_DIR.iterdir():
|
started_at = time()
|
||||||
logger.info(f'Processing {file.name}...')
|
files = list(INPUT_DIR.iterdir())
|
||||||
|
|
||||||
if file.suffix not in ('.png', '.jpg', '.webp', '.gif'):
|
html = '<html><head><style>body { background-color: gray; }</style></head><body>\n'
|
||||||
|
|
||||||
|
for i, file in enumerate(files):
|
||||||
|
logger.info(f'Processing [{i+1}/{len(list(files))}] {file.name}...')
|
||||||
|
|
||||||
|
if file.suffix not in ('.png', '.jpg', '.webp', '.gif', '.mp4'):
|
||||||
logger.warning(f'Unsupported file type: {file.suffix}')
|
logger.warning(f'Unsupported file type: {file.suffix}')
|
||||||
continue
|
continue
|
||||||
|
|
||||||
file_path = INPUT_DIR / file.name
|
file_path = INPUT_DIR / file.name
|
||||||
file_output = OUTPUT_DIR / file.name.replace(file.suffix, '.webm')
|
file_output = OUTPUT_DIR / file.name.replace(file.suffix, '.webm')
|
||||||
|
|
||||||
compress(file_path, file_output, STICKER_WIDTH)
|
compress(file_path.relative_to(PATH), file_output.relative_to(PATH), width, size)
|
||||||
|
|
||||||
|
html += f'<video width="250" height="250" src="{file.name.replace(file.suffix, '.webm')}" autoplay loop></video>\n'
|
||||||
|
|
||||||
|
logger.info(f'Finished in {round(time() - started_at, 2)} seconds')
|
||||||
|
|
||||||
|
html += '</body>\n'
|
||||||
|
html += '</html>\n'
|
||||||
|
|
||||||
|
with open(OUTPUT_DIR / 'index.html', 'w', encoding='utf-8') as f:
|
||||||
|
f.write(html)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "telegramstickersgenerator"
|
name = "telegramstickersgenerator"
|
||||||
version = "1.0.0"
|
version = "1.0.2"
|
||||||
description = "A simple tool to convert any images and/or gifs to webm extension so that they can be used to create animated sticker/emoji packs in Telegram."
|
description = "A simple tool to convert any images and/or gifs to webm extension so that they can be used to create animated sticker/emoji packs in Telegram."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ffmpeg-python==0.2.0",
|
|
||||||
"rich==13.9.4",
|
"rich==13.9.4",
|
||||||
|
"typed-ffmpeg==2.6.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
@ -20,6 +20,7 @@ _git = "git add ."
|
|||||||
_lint = "pre-commit run --all-files"
|
_lint = "pre-commit run --all-files"
|
||||||
|
|
||||||
lint = ["_git", "_lint"]
|
lint = ["_git", "_lint"]
|
||||||
|
run = "uv run main.py"
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
target-version = "py313"
|
target-version = "py313"
|
||||||
|
|||||||
@ -13,3 +13,5 @@ def configure_logging():
|
|||||||
datefmt=DATE_FORMAT,
|
datefmt=DATE_FORMAT,
|
||||||
handlers=[RichHandler(show_time=False, rich_tracebacks=True)],
|
handlers=[RichHandler(show_time=False, rich_tracebacks=True)],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logging.getLogger('ffmpeg').setLevel(logging.ERROR)
|
||||||
|
|||||||
Reference in New Issue
Block a user