Рефакторинг в питон код
This commit is contained in:
25
.gitignore
vendored
25
.gitignore
vendored
@ -1,2 +1,23 @@
|
||||
input/*
|
||||
output/*
|
||||
# Python-generated files
|
||||
__pycache__/
|
||||
*.py[oc]
|
||||
build/
|
||||
dist/
|
||||
wheels/
|
||||
*.egg-info
|
||||
|
||||
# Virtual environments
|
||||
.venv
|
||||
|
||||
# UV
|
||||
uv.lock
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
|
||||
# Ruff
|
||||
.ruff_cache
|
||||
|
||||
# Project
|
||||
input
|
||||
output
|
||||
|
||||
35
.pre-commit-config.yaml
Normal file
35
.pre-commit-config.yaml
Normal file
@ -0,0 +1,35 @@
|
||||
repos:
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.19.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py313-plus]
|
||||
|
||||
- repo: https://github.com/crate-ci/typos
|
||||
rev: v1.28.1
|
||||
hooks:
|
||||
- id: typos
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.8.1
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [ --fix ]
|
||||
- id: ruff-format
|
||||
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v5.0.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: check-docstring-first
|
||||
- id: check-added-large-files
|
||||
- id: check-yaml
|
||||
- id: debug-statements
|
||||
- id: check-merge-conflict
|
||||
- id: double-quote-string-fixer
|
||||
- id: end-of-file-fixer
|
||||
|
||||
- repo: meta
|
||||
hooks:
|
||||
- id: check-hooks-apply
|
||||
- id: check-useless-excludes
|
||||
108
main.py
Normal file
108
main.py
Normal file
@ -0,0 +1,108 @@
|
||||
from logging import getLogger
|
||||
from pathlib import Path
|
||||
|
||||
import ffmpeg
|
||||
from ffmpeg.nodes import FilterableStream
|
||||
from rich.prompt import Prompt
|
||||
|
||||
from utils.log import configure_logging
|
||||
|
||||
PATH = Path(__file__).parent
|
||||
INPUT_DIR = PATH / 'input'
|
||||
OUTPUT_DIR = PATH / 'output'
|
||||
|
||||
OUTPUT_ARGS = {'vcodec': 'libvpx-vp9', 'pix_fmt': 'yuva420p', 'loglevel': 'error', 'y': None}
|
||||
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
def compress(input_file: Path, output_file: Path, sticker_width: int, bitrate: int | None = None):
|
||||
file_input = ffmpeg.input(input_file)
|
||||
file_output = OUTPUT_DIR / input_file.name.replace(input_file.suffix, '.webm')
|
||||
|
||||
if not isinstance(file_input, FilterableStream):
|
||||
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 = ffmpeg.filter(
|
||||
stream, 'scale', sticker_width, -1, force_original_aspect_ratio='decrease'
|
||||
)
|
||||
stream = ffmpeg.filter(
|
||||
stream, 'pad', sticker_width, sticker_width, '(ow-iw)/2', '(oh-ih)/2', color='0x00000000'
|
||||
)
|
||||
|
||||
if input_file.suffix == '.gif':
|
||||
duration = float(ffmpeg.probe(input_file)['streams'][0]['duration'])
|
||||
speed_factor = 1
|
||||
|
||||
if duration > 3:
|
||||
speed_factor = duration / 3
|
||||
|
||||
stream = ffmpeg.filter(stream, 'setpts', f'PTS/{speed_factor}')
|
||||
|
||||
args = OUTPUT_ARGS
|
||||
|
||||
if bitrate:
|
||||
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)
|
||||
|
||||
logger.info(
|
||||
f'File size is too high [{int(file_size / 1024)} KB], reducing to {bitrate}k...'
|
||||
)
|
||||
|
||||
return compress(input_file, output_file, sticker_width, bitrate)
|
||||
|
||||
|
||||
def main():
|
||||
if not INPUT_DIR.exists():
|
||||
INPUT_DIR.mkdir(parents=True)
|
||||
|
||||
if not OUTPUT_DIR.exists():
|
||||
OUTPUT_DIR.mkdir(parents=True)
|
||||
|
||||
for file in OUTPUT_DIR.iterdir():
|
||||
file.unlink()
|
||||
|
||||
sticker_type = Prompt.ask('Do you want to create stickers or emoji?', choices=['s', 'e'])
|
||||
|
||||
match sticker_type:
|
||||
case 's':
|
||||
STICKER_WIDTH = 512
|
||||
case 'e':
|
||||
STICKER_WIDTH = 100
|
||||
case _:
|
||||
STICKER_WIDTH = 512
|
||||
|
||||
for file in INPUT_DIR.iterdir():
|
||||
logger.info(f'Processing {file.name}...')
|
||||
|
||||
if file.suffix not in ('.png', '.jpg', '.webp', '.gif'):
|
||||
logger.warning(f'Unsupported file type: {file.suffix}')
|
||||
continue
|
||||
|
||||
file_path = INPUT_DIR / file.name
|
||||
file_output = OUTPUT_DIR / file.name.replace(file.suffix, '.webm')
|
||||
|
||||
compress(file_path, file_output, STICKER_WIDTH)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
configure_logging()
|
||||
main()
|
||||
34
pyproject.toml
Normal file
34
pyproject.toml
Normal file
@ -0,0 +1,34 @@
|
||||
[project]
|
||||
name = "telegramstickersgenerator-git"
|
||||
version = "1.0.0"
|
||||
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"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"ffmpeg-python==0.2.0",
|
||||
"rich==13.9.4",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"poethepoet==0.31.1",
|
||||
"pre-commit==4.0.1",
|
||||
]
|
||||
|
||||
[tool.poe.tasks]
|
||||
_git = "git add ."
|
||||
_lint = "pre-commit run --all-files"
|
||||
|
||||
lint = ["_git", "_lint"]
|
||||
|
||||
[tool.ruff]
|
||||
target-version = "py313"
|
||||
line-length = 100
|
||||
|
||||
[tool.ruff.lint]
|
||||
extend-select = ["F", "UP", "B", "SIM", "I"]
|
||||
|
||||
[tool.ruff.format]
|
||||
quote-style = "single"
|
||||
indent-style = "space"
|
||||
docstring-code-format = true
|
||||
172
script.ps1
172
script.ps1
@ -1,172 +0,0 @@
|
||||
# Variables
|
||||
$ffmpegPath = (Get-Command ffmpeg -ErrorAction SilentlyContinue).Path
|
||||
$ffprobePath = (Get-Command ffprobe -ErrorAction SilentlyContinue).Path
|
||||
$inputDir = "./input"
|
||||
$outputDir = "./output"
|
||||
|
||||
# Check if ffmpeg and ffprobe executables exist
|
||||
if (-not $ffmpegPath -or -not $ffprobePath) {
|
||||
Write-Host "ffmpeg or ffprobe is not installed on this system. Exiting..."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Ask user if he wants to do stickers or emoji
|
||||
$choice = Read-Host "Do you want to create stickers or emoji? (s/e)"
|
||||
if ($choice -eq "s") {
|
||||
$stickerWidth = 512
|
||||
} elseif ($choice -eq "e") {
|
||||
$stickerWidth = 100
|
||||
} else {
|
||||
Write-Host "Invalid choice. Exiting..."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check if the output directory exists, if not, create it
|
||||
if (-not (Test-Path $outputDir)) {
|
||||
New-Item -Path $outputDir -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
|
||||
# Functions
|
||||
# Function to get the bitrate of a video file using ffprobe
|
||||
function Get-Bitrate {
|
||||
param (
|
||||
[string]$filePath
|
||||
)
|
||||
|
||||
# Get bitrate from ffprobe
|
||||
$bitrateOutput = & $ffprobePath -v error -select_streams v:0 -show_entries stream=bit_rate -of default=noprint_wrappers=1:nokey=1 $filePath
|
||||
|
||||
if ([string]::IsNullOrEmpty($bitrateOutput) -or $bitrateOutput -eq "N/A") {
|
||||
# If direct bitrate is not available, calculate bitrate based on file size and duration
|
||||
$fileSize = (Get-Item $filePath).Length
|
||||
$durationOutput = & $ffprobePath -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 $filePath
|
||||
|
||||
if ($fileSize -gt 0 -and $durationOutput -ne "N/A" -and $durationOutput -ne "") {
|
||||
$duration = [float]$durationOutput
|
||||
$bitrate = [math]::Round(($fileSize * 8) / $duration / 1000) # in kbps
|
||||
return $bitrate
|
||||
} else {
|
||||
Write-Host "Unable to determine bitrate for $filePath."
|
||||
return 1000
|
||||
}
|
||||
}
|
||||
|
||||
return [int]$bitrateOutput
|
||||
}
|
||||
|
||||
# Function to get the duration of a video file using ffprobe
|
||||
function Get-Duration {
|
||||
param (
|
||||
[string]$filePath
|
||||
)
|
||||
|
||||
$durationOutput = & $ffprobePath -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 $filePath
|
||||
|
||||
if ($durationOutput -ne "N/A" -and $durationOutput -ne "") {
|
||||
return [float]$durationOutput
|
||||
} else {
|
||||
Write-Host "Unable to determine duration for $filePath."
|
||||
return $null
|
||||
}
|
||||
}
|
||||
|
||||
# Function to compress the video file until it is below the specified size
|
||||
function Compress-File {
|
||||
param (
|
||||
[string]$inputFilePath,
|
||||
[string]$outputFilePath,
|
||||
[int]$initialBitrate,
|
||||
[int]$maxSizeKB
|
||||
)
|
||||
|
||||
$bitrate = $initialBitrate
|
||||
$originalDuration = Get-Duration -filePath $file.FullName
|
||||
$speedFactor = $originalDuration / 3
|
||||
$outputFilePathTemp = "$outputFilePath.webm"
|
||||
|
||||
if ($speedFactor -le 1) {
|
||||
$speedFactor = 1
|
||||
}
|
||||
|
||||
do {
|
||||
Write-Host Trying bitrate: ${bitrate}k
|
||||
|
||||
# Compress with the current bitrate
|
||||
& $ffmpegPath -loglevel quiet -i $inputFilePath -t 3 -r 25 -filter:v "setpts=PTS/$speedFactor,scale=-1:$stickerWidth" -c:v libvpx-vp9 -b:v ${bitrate}k $outputFilePathTemp -y
|
||||
|
||||
# Check the size of the compressed file
|
||||
$fileSize = (Get-Item $outputFilePathTemp).Length
|
||||
$fileSizeKB = [math]::Round($fileSize / 1KB)
|
||||
|
||||
if ($fileSizeKB -le $maxSizeKB) {
|
||||
# Rename the temporary file to the final output file name
|
||||
if (Test-Path $outputFilePath) { Remove-Item -Path $outputFilePath -Force }
|
||||
Rename-Item -Path $outputFilePathTemp -NewName (Split-Path $outputFilePath -Leaf)
|
||||
return
|
||||
}
|
||||
|
||||
$bitrateOffset = 100
|
||||
if (($fileSizeKB / $maxSizeKB) -gt 2) {
|
||||
$bitrateOffset = 100 * ($fileSizeKB / $maxSizeKB)
|
||||
}
|
||||
|
||||
# Reduce the bitrate
|
||||
$bitrate = [math]::Max([int]($bitrate - $bitrateOffset), 100) # Ensuring bitrate doesn't go below 100k
|
||||
|
||||
} while ($bitrate -gt 100)
|
||||
|
||||
# If bitrate falls too low, retain the last generated file
|
||||
if (Test-Path $outputFilePathTemp) {
|
||||
if (Test-Path $outputFilePath) { Remove-Item -Path $outputFilePath -Force }
|
||||
Rename-Item -Path $outputFilePathTemp -NewName (Split-Path $outputFilePath -Leaf)
|
||||
}
|
||||
}
|
||||
|
||||
# Process each file in the input directory
|
||||
foreach ($file in Get-ChildItem -Path $inputDir -File) {
|
||||
$outputFile = "$outputDir/$($file.BaseName).webm"
|
||||
|
||||
# Check if the file is a PNG or GIF
|
||||
if ($file.Extension -notin ".png", ".gif") {
|
||||
continue
|
||||
}
|
||||
|
||||
Write-Host "Processing $($file.FullName)..."
|
||||
|
||||
# Convert the file to WebM format
|
||||
if ($file.Extension -eq ".png") {
|
||||
& $ffmpegPath -loglevel quiet -i $file.FullName -r 25 -vf "scale=-1:$stickerWidth" -c:v libvpx-vp9 -pix_fmt rgba $outputFile -y
|
||||
}
|
||||
|
||||
if ($file.Extension -eq ".gif") {
|
||||
# Get the original duration of the GIF
|
||||
$originalDuration = Get-Duration -filePath $file.FullName
|
||||
|
||||
if ($originalDuration -gt 0) {
|
||||
$speedFactor = $originalDuration / 3
|
||||
|
||||
if ($speedFactor -le 1) {
|
||||
$speedFactor = 1
|
||||
}
|
||||
|
||||
# Convert the file to WebM format
|
||||
& $ffmpegPath -loglevel quiet -i $file.FullName -t 3 -r 25 -filter:v "setpts=PTS/$speedFactor,scale=-1:$stickerWidth" -c:v libvpx-vp9 $outputFile -y
|
||||
} else {
|
||||
Write-Host "Invalid duration for GIF $($file.FullName). Skipping..."
|
||||
continue
|
||||
}
|
||||
|
||||
# Check if the file size exceeds 256 KB (262144 bytes)
|
||||
$fileSize = (Get-Item $outputFile).Length
|
||||
|
||||
if ($fileSize -gt 255 * 1024) {
|
||||
Write-Host "File $($outputFile) exceeds 256 KB, compressing..."
|
||||
|
||||
# Get the initial bitrate of the WebM file
|
||||
$initialBitrate = Get-Bitrate -filePath $outputFile
|
||||
|
||||
# Compress the file until it is below 255 KB
|
||||
Compress-File -inputFilePath $file.FullName -outputFilePath $outputFile -initialBitrate $initialBitrate -maxSizeKB 255
|
||||
}
|
||||
}
|
||||
}
|
||||
0
utils/__init__.py
Normal file
0
utils/__init__.py
Normal file
15
utils/log.py
Normal file
15
utils/log.py
Normal file
@ -0,0 +1,15 @@
|
||||
import logging
|
||||
|
||||
from rich.logging import RichHandler
|
||||
|
||||
|
||||
def configure_logging():
|
||||
DATE_FORMAT = '[%d.%m %H:%M:%S]'
|
||||
LOGGER_FORMAT = '%(asctime)s %(message)s'
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format=LOGGER_FORMAT,
|
||||
datefmt=DATE_FORMAT,
|
||||
handlers=[RichHandler(show_time=False, rich_tracebacks=True)],
|
||||
)
|
||||
Reference in New Issue
Block a user