Compare commits

...

10 Commits

Author SHA1 Message Date
33d3bf33f7 Фикс эмодзи
All checks were successful
Validating / publish (push) Successful in 53s
2025-02-11 18:30:24 +03:00
afabcb0afa Линтер 2025-01-31 22:14:59 +03:00
9add816188 Новый пре-коммит 2025-01-31 22:01:46 +03:00
1fe5aa8dd1 Апдейт #2 2025-01-29 04:32:59 +03:00
ece1b2e927 Апдейт 2025-01-29 00:24:44 +03:00
10f2a552e3 Фикс билда 2024-12-30 23:52:56 +03:00
57cb1a1d38 Комит для запуска CI/CD 2024-12-30 23:51:04 +03:00
1f9e6c8c4e Добавлен билд для проверки состояния 2024-12-30 23:49:59 +03:00
e5293cbf05 Маленькие правки 2024-12-30 23:48:18 +03:00
e16d7a440e Update README.md 2024-12-23 18:06:51 +03:00
5 changed files with 122 additions and 48 deletions

View 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

View File

@ -1,4 +1,4 @@
# Telegram Stickers Generator Git # Telegram Stickers Generator
## Description ## Description

117
main.py
View File

@ -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,
vcodec='libvpx-vp9',
pix_fmt='yuva420p',
an=True,
extra_options=args,
)
stream.run(overwrite_output=True)
if bitrate: file_size = output_file.stat().st_size
args['video_bitrate'] = f'{bitrate}k'
output = ffmpeg.output(stream, filename=file_output, **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)
if file_size / 1024 > max_size:
print(stream.compile_line())
logger.info( logger.info(
f'File size is too high [{int(file_size / 1024)} KB], reducing to {bitrate}k...' f'Average bitrate: {(int(ffmpeg.probe(output_file)['format']['bit_rate']) / 1024):.2f} KB/s'
) )
logger.error(
return compress(input_file, output_file, sticker_width, bitrate) 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__':

View File

@ -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"

View File

@ -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)