agent-recipes 0.0.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agent_recipes/__init__.py +27 -0
- agent_recipes/recipe_runtime/__init__.py +28 -0
- agent_recipes/recipe_runtime/core.py +385 -0
- agent_recipes/templates/ai-ab-hook-tester/recipe.yaml +45 -0
- agent_recipes/templates/ai-ab-hook-tester/tools.py +169 -0
- agent_recipes/templates/ai-angle-generator/recipe.yaml +49 -0
- agent_recipes/templates/ai-angle-generator/tools.py +182 -0
- agent_recipes/templates/ai-api-doc-generator/README.md +59 -0
- agent_recipes/templates/ai-api-doc-generator/TEMPLATE.yaml +29 -0
- agent_recipes/templates/ai-api-tester/README.md +60 -0
- agent_recipes/templates/ai-api-tester/TEMPLATE.yaml +29 -0
- agent_recipes/templates/ai-audio-enhancer/README.md +59 -0
- agent_recipes/templates/ai-audio-enhancer/TEMPLATE.yaml +28 -0
- agent_recipes/templates/ai-audio-normalizer/README.md +13 -0
- agent_recipes/templates/ai-audio-normalizer/TEMPLATE.yaml +44 -0
- agent_recipes/templates/ai-audio-splitter/README.md +14 -0
- agent_recipes/templates/ai-audio-splitter/TEMPLATE.yaml +47 -0
- agent_recipes/templates/ai-background-music-generator/README.md +59 -0
- agent_recipes/templates/ai-background-music-generator/TEMPLATE.yaml +28 -0
- agent_recipes/templates/ai-background-remover/README.md +60 -0
- agent_recipes/templates/ai-background-remover/TEMPLATE.yaml +27 -0
- agent_recipes/templates/ai-barcode-scanner/README.md +60 -0
- agent_recipes/templates/ai-barcode-scanner/TEMPLATE.yaml +26 -0
- agent_recipes/templates/ai-blog-generator/README.md +59 -0
- agent_recipes/templates/ai-blog-generator/TEMPLATE.yaml +28 -0
- agent_recipes/templates/ai-brief-generator/recipe.yaml +52 -0
- agent_recipes/templates/ai-brief-generator/tools.py +231 -0
- agent_recipes/templates/ai-broll-builder/recipe.yaml +47 -0
- agent_recipes/templates/ai-broll-builder/tools.py +204 -0
- agent_recipes/templates/ai-calendar-scheduler/README.md +60 -0
- agent_recipes/templates/ai-calendar-scheduler/TEMPLATE.yaml +29 -0
- agent_recipes/templates/ai-changelog-generator/README.md +14 -0
- agent_recipes/templates/ai-changelog-generator/TEMPLATE.yaml +46 -0
- agent_recipes/templates/ai-chart-generator/README.md +61 -0
- agent_recipes/templates/ai-chart-generator/TEMPLATE.yaml +32 -0
- agent_recipes/templates/ai-code-documenter/README.md +12 -0
- agent_recipes/templates/ai-code-documenter/TEMPLATE.yaml +37 -0
- agent_recipes/templates/ai-code-refactorer/README.md +59 -0
- agent_recipes/templates/ai-code-refactorer/TEMPLATE.yaml +28 -0
- agent_recipes/templates/ai-code-reviewer/README.md +59 -0
- agent_recipes/templates/ai-code-reviewer/TEMPLATE.yaml +31 -0
- agent_recipes/templates/ai-color-palette-extractor/README.md +60 -0
- agent_recipes/templates/ai-color-palette-extractor/TEMPLATE.yaml +27 -0
- agent_recipes/templates/ai-comment-miner/recipe.yaml +40 -0
- agent_recipes/templates/ai-comment-miner/tools.py +141 -0
- agent_recipes/templates/ai-commit-message-generator/README.md +59 -0
- agent_recipes/templates/ai-commit-message-generator/TEMPLATE.yaml +31 -0
- agent_recipes/templates/ai-content-calendar/recipe.yaml +43 -0
- agent_recipes/templates/ai-content-calendar/tools.py +170 -0
- agent_recipes/templates/ai-context-enricher/recipe.yaml +48 -0
- agent_recipes/templates/ai-context-enricher/tools.py +258 -0
- agent_recipes/templates/ai-contract-analyzer/README.md +60 -0
- agent_recipes/templates/ai-contract-analyzer/TEMPLATE.yaml +34 -0
- agent_recipes/templates/ai-csv-cleaner/README.md +13 -0
- agent_recipes/templates/ai-csv-cleaner/TEMPLATE.yaml +45 -0
- agent_recipes/templates/ai-cta-generator/recipe.yaml +54 -0
- agent_recipes/templates/ai-cta-generator/tools.py +174 -0
- agent_recipes/templates/ai-daily-news-show/recipe.yaml +103 -0
- agent_recipes/templates/ai-daily-news-show/tools.py +308 -0
- agent_recipes/templates/ai-data-anonymizer/README.md +60 -0
- agent_recipes/templates/ai-data-anonymizer/TEMPLATE.yaml +31 -0
- agent_recipes/templates/ai-data-profiler/README.md +14 -0
- agent_recipes/templates/ai-data-profiler/TEMPLATE.yaml +42 -0
- agent_recipes/templates/ai-dependency-auditor/README.md +12 -0
- agent_recipes/templates/ai-dependency-auditor/TEMPLATE.yaml +37 -0
- agent_recipes/templates/ai-doc-translator/README.md +12 -0
- agent_recipes/templates/ai-doc-translator/TEMPLATE.yaml +41 -0
- agent_recipes/templates/ai-duplicate-finder/README.md +59 -0
- agent_recipes/templates/ai-duplicate-finder/TEMPLATE.yaml +28 -0
- agent_recipes/templates/ai-ebook-converter/README.md +60 -0
- agent_recipes/templates/ai-ebook-converter/TEMPLATE.yaml +27 -0
- agent_recipes/templates/ai-email-parser/README.md +59 -0
- agent_recipes/templates/ai-email-parser/TEMPLATE.yaml +29 -0
- agent_recipes/templates/ai-etl-pipeline/README.md +60 -0
- agent_recipes/templates/ai-etl-pipeline/TEMPLATE.yaml +30 -0
- agent_recipes/templates/ai-excel-formula-generator/README.md +59 -0
- agent_recipes/templates/ai-excel-formula-generator/TEMPLATE.yaml +28 -0
- agent_recipes/templates/ai-face-blur/README.md +60 -0
- agent_recipes/templates/ai-face-blur/TEMPLATE.yaml +28 -0
- agent_recipes/templates/ai-fact-checker/recipe.yaml +52 -0
- agent_recipes/templates/ai-fact-checker/tools.py +279 -0
- agent_recipes/templates/ai-faq-generator/README.md +59 -0
- agent_recipes/templates/ai-faq-generator/TEMPLATE.yaml +28 -0
- agent_recipes/templates/ai-file-organizer/README.md +59 -0
- agent_recipes/templates/ai-file-organizer/TEMPLATE.yaml +29 -0
- agent_recipes/templates/ai-folder-packager/README.md +15 -0
- agent_recipes/templates/ai-folder-packager/TEMPLATE.yaml +48 -0
- agent_recipes/templates/ai-form-filler/README.md +60 -0
- agent_recipes/templates/ai-form-filler/TEMPLATE.yaml +30 -0
- agent_recipes/templates/ai-hashtag-optimizer/recipe.yaml +45 -0
- agent_recipes/templates/ai-hashtag-optimizer/tools.py +134 -0
- agent_recipes/templates/ai-hook-generator/recipe.yaml +50 -0
- agent_recipes/templates/ai-hook-generator/tools.py +177 -0
- agent_recipes/templates/ai-image-captioner/README.md +59 -0
- agent_recipes/templates/ai-image-captioner/TEMPLATE.yaml +28 -0
- agent_recipes/templates/ai-image-cataloger/README.md +13 -0
- agent_recipes/templates/ai-image-cataloger/TEMPLATE.yaml +39 -0
- agent_recipes/templates/ai-image-optimizer/README.md +13 -0
- agent_recipes/templates/ai-image-optimizer/TEMPLATE.yaml +43 -0
- agent_recipes/templates/ai-image-resizer/README.md +12 -0
- agent_recipes/templates/ai-image-resizer/TEMPLATE.yaml +39 -0
- agent_recipes/templates/ai-image-tagger/README.md +59 -0
- agent_recipes/templates/ai-image-tagger/TEMPLATE.yaml +28 -0
- agent_recipes/templates/ai-image-upscaler/README.md +60 -0
- agent_recipes/templates/ai-image-upscaler/TEMPLATE.yaml +27 -0
- agent_recipes/templates/ai-invoice-processor/README.md +60 -0
- agent_recipes/templates/ai-invoice-processor/TEMPLATE.yaml +34 -0
- agent_recipes/templates/ai-json-to-csv/README.md +12 -0
- agent_recipes/templates/ai-json-to-csv/TEMPLATE.yaml +36 -0
- agent_recipes/templates/ai-log-analyzer/README.md +59 -0
- agent_recipes/templates/ai-log-analyzer/TEMPLATE.yaml +28 -0
- agent_recipes/templates/ai-markdown-to-pdf/README.md +12 -0
- agent_recipes/templates/ai-markdown-to-pdf/TEMPLATE.yaml +40 -0
- agent_recipes/templates/ai-meeting-summarizer/README.md +59 -0
- agent_recipes/templates/ai-meeting-summarizer/TEMPLATE.yaml +32 -0
- agent_recipes/templates/ai-meta-tag-generator/README.md +59 -0
- agent_recipes/templates/ai-meta-tag-generator/TEMPLATE.yaml +28 -0
- agent_recipes/templates/ai-news-capture-pack/recipe.yaml +42 -0
- agent_recipes/templates/ai-news-capture-pack/tools.py +150 -0
- agent_recipes/templates/ai-news-crawler/recipe.yaml +99 -0
- agent_recipes/templates/ai-news-crawler/tools.py +417 -0
- agent_recipes/templates/ai-news-deduper/recipe.yaml +47 -0
- agent_recipes/templates/ai-news-deduper/tools.py +235 -0
- agent_recipes/templates/ai-newsletter-generator/README.md +59 -0
- agent_recipes/templates/ai-newsletter-generator/TEMPLATE.yaml +28 -0
- agent_recipes/templates/ai-note-summarizer/README.md +59 -0
- agent_recipes/templates/ai-note-summarizer/TEMPLATE.yaml +28 -0
- agent_recipes/templates/ai-pdf-summarizer/README.md +12 -0
- agent_recipes/templates/ai-pdf-summarizer/TEMPLATE.yaml +40 -0
- agent_recipes/templates/ai-pdf-to-markdown/README.md +19 -0
- agent_recipes/templates/ai-pdf-to-markdown/TEMPLATE.yaml +63 -0
- agent_recipes/templates/ai-performance-analyzer/recipe.yaml +45 -0
- agent_recipes/templates/ai-performance-analyzer/tools.py +159 -0
- agent_recipes/templates/ai-podcast-cleaner/README.md +117 -0
- agent_recipes/templates/ai-podcast-cleaner/TEMPLATE.yaml +117 -0
- agent_recipes/templates/ai-podcast-cleaner/agents.yaml +59 -0
- agent_recipes/templates/ai-podcast-cleaner/workflow.yaml +77 -0
- agent_recipes/templates/ai-podcast-transcriber/README.md +59 -0
- agent_recipes/templates/ai-podcast-transcriber/TEMPLATE.yaml +32 -0
- agent_recipes/templates/ai-post-copy-generator/recipe.yaml +41 -0
- agent_recipes/templates/ai-post-copy-generator/tools.py +105 -0
- agent_recipes/templates/ai-product-description-generator/README.md +59 -0
- agent_recipes/templates/ai-product-description-generator/TEMPLATE.yaml +28 -0
- agent_recipes/templates/ai-publisher-pack/recipe.yaml +44 -0
- agent_recipes/templates/ai-publisher-pack/tools.py +252 -0
- agent_recipes/templates/ai-qr-code-generator/README.md +60 -0
- agent_recipes/templates/ai-qr-code-generator/TEMPLATE.yaml +26 -0
- agent_recipes/templates/ai-regex-generator/README.md +59 -0
- agent_recipes/templates/ai-regex-generator/TEMPLATE.yaml +28 -0
- agent_recipes/templates/ai-repo-readme/README.md +13 -0
- agent_recipes/templates/ai-repo-readme/TEMPLATE.yaml +42 -0
- agent_recipes/templates/ai-report-generator/README.md +61 -0
- agent_recipes/templates/ai-report-generator/TEMPLATE.yaml +32 -0
- agent_recipes/templates/ai-resume-parser/README.md +60 -0
- agent_recipes/templates/ai-resume-parser/TEMPLATE.yaml +33 -0
- agent_recipes/templates/ai-rss-aggregator/README.md +60 -0
- agent_recipes/templates/ai-rss-aggregator/TEMPLATE.yaml +30 -0
- agent_recipes/templates/ai-schema-generator/README.md +12 -0
- agent_recipes/templates/ai-schema-generator/TEMPLATE.yaml +34 -0
- agent_recipes/templates/ai-screen-recorder/recipe.yaml +43 -0
- agent_recipes/templates/ai-screen-recorder/tools.py +184 -0
- agent_recipes/templates/ai-screenshot-capture/recipe.yaml +45 -0
- agent_recipes/templates/ai-screenshot-capture/tools.py +231 -0
- agent_recipes/templates/ai-screenshot-ocr/README.md +12 -0
- agent_recipes/templates/ai-screenshot-ocr/TEMPLATE.yaml +37 -0
- agent_recipes/templates/ai-script-writer/recipe.yaml +58 -0
- agent_recipes/templates/ai-script-writer/tools.py +297 -0
- agent_recipes/templates/ai-sentiment-analyzer/README.md +59 -0
- agent_recipes/templates/ai-sentiment-analyzer/TEMPLATE.yaml +28 -0
- agent_recipes/templates/ai-seo-optimizer/README.md +59 -0
- agent_recipes/templates/ai-seo-optimizer/TEMPLATE.yaml +28 -0
- agent_recipes/templates/ai-signal-ranker/recipe.yaml +54 -0
- agent_recipes/templates/ai-signal-ranker/tools.py +256 -0
- agent_recipes/templates/ai-sitemap-generator/README.md +59 -0
- agent_recipes/templates/ai-sitemap-generator/TEMPLATE.yaml +26 -0
- agent_recipes/templates/ai-sitemap-scraper/README.md +13 -0
- agent_recipes/templates/ai-sitemap-scraper/TEMPLATE.yaml +41 -0
- agent_recipes/templates/ai-slide-generator/README.md +60 -0
- agent_recipes/templates/ai-slide-generator/TEMPLATE.yaml +29 -0
- agent_recipes/templates/ai-slide-to-notes/README.md +12 -0
- agent_recipes/templates/ai-slide-to-notes/TEMPLATE.yaml +37 -0
- agent_recipes/templates/ai-social-media-generator/README.md +59 -0
- agent_recipes/templates/ai-social-media-generator/TEMPLATE.yaml +28 -0
- agent_recipes/templates/ai-sql-generator/README.md +59 -0
- agent_recipes/templates/ai-sql-generator/TEMPLATE.yaml +28 -0
- agent_recipes/templates/ai-subtitle-generator/README.md +59 -0
- agent_recipes/templates/ai-subtitle-generator/TEMPLATE.yaml +31 -0
- agent_recipes/templates/ai-test-generator/README.md +59 -0
- agent_recipes/templates/ai-test-generator/TEMPLATE.yaml +28 -0
- agent_recipes/templates/ai-translation-batch/README.md +59 -0
- agent_recipes/templates/ai-translation-batch/TEMPLATE.yaml +28 -0
- agent_recipes/templates/ai-url-to-markdown/README.md +14 -0
- agent_recipes/templates/ai-url-to-markdown/TEMPLATE.yaml +44 -0
- agent_recipes/templates/ai-video-chapter-generator/README.md +59 -0
- agent_recipes/templates/ai-video-chapter-generator/TEMPLATE.yaml +32 -0
- agent_recipes/templates/ai-video-compressor/README.md +59 -0
- agent_recipes/templates/ai-video-compressor/TEMPLATE.yaml +28 -0
- agent_recipes/templates/ai-video-editor/README.md +254 -0
- agent_recipes/templates/ai-video-editor/TEMPLATE.yaml +139 -0
- agent_recipes/templates/ai-video-editor/agents.yaml +36 -0
- agent_recipes/templates/ai-video-editor/requirements.txt +8 -0
- agent_recipes/templates/ai-video-editor/scripts/run.sh +10 -0
- agent_recipes/templates/ai-video-editor/src/ai_video_editor/__init__.py +45 -0
- agent_recipes/templates/ai-video-editor/src/ai_video_editor/__main__.py +8 -0
- agent_recipes/templates/ai-video-editor/src/ai_video_editor/__pycache__/__init__.cpython-312.pyc +0 -0
- agent_recipes/templates/ai-video-editor/src/ai_video_editor/__pycache__/cli.cpython-312.pyc +0 -0
- agent_recipes/templates/ai-video-editor/src/ai_video_editor/__pycache__/config.cpython-312.pyc +0 -0
- agent_recipes/templates/ai-video-editor/src/ai_video_editor/__pycache__/ffmpeg_probe.cpython-312.pyc +0 -0
- agent_recipes/templates/ai-video-editor/src/ai_video_editor/__pycache__/heuristics.cpython-312.pyc +0 -0
- agent_recipes/templates/ai-video-editor/src/ai_video_editor/__pycache__/llm_plan.cpython-312.pyc +0 -0
- agent_recipes/templates/ai-video-editor/src/ai_video_editor/__pycache__/models.cpython-312.pyc +0 -0
- agent_recipes/templates/ai-video-editor/src/ai_video_editor/__pycache__/pipeline.cpython-312.pyc +0 -0
- agent_recipes/templates/ai-video-editor/src/ai_video_editor/__pycache__/render.cpython-312.pyc +0 -0
- agent_recipes/templates/ai-video-editor/src/ai_video_editor/__pycache__/timeline.cpython-312.pyc +0 -0
- agent_recipes/templates/ai-video-editor/src/ai_video_editor/__pycache__/transcribe.cpython-312.pyc +0 -0
- agent_recipes/templates/ai-video-editor/src/ai_video_editor/__pycache__/utils.cpython-312.pyc +0 -0
- agent_recipes/templates/ai-video-editor/src/ai_video_editor/cli.py +343 -0
- agent_recipes/templates/ai-video-editor/src/ai_video_editor/config.py +102 -0
- agent_recipes/templates/ai-video-editor/src/ai_video_editor/ffmpeg_probe.py +92 -0
- agent_recipes/templates/ai-video-editor/src/ai_video_editor/heuristics.py +119 -0
- agent_recipes/templates/ai-video-editor/src/ai_video_editor/llm_plan.py +277 -0
- agent_recipes/templates/ai-video-editor/src/ai_video_editor/models.py +343 -0
- agent_recipes/templates/ai-video-editor/src/ai_video_editor/pipeline.py +287 -0
- agent_recipes/templates/ai-video-editor/src/ai_video_editor/render.py +274 -0
- agent_recipes/templates/ai-video-editor/src/ai_video_editor/timeline.py +278 -0
- agent_recipes/templates/ai-video-editor/src/ai_video_editor/transcribe.py +233 -0
- agent_recipes/templates/ai-video-editor/src/ai_video_editor/utils.py +222 -0
- agent_recipes/templates/ai-video-editor/src/input.mov +0 -0
- agent_recipes/templates/ai-video-editor/src/out.mp4 +0 -0
- agent_recipes/templates/ai-video-editor/tests/test_heuristics.py +130 -0
- agent_recipes/templates/ai-video-editor/tests/test_models.py +152 -0
- agent_recipes/templates/ai-video-editor/tests/test_timeline.py +105 -0
- agent_recipes/templates/ai-video-editor/workflow.yaml +51 -0
- agent_recipes/templates/ai-video-highlight-extractor/README.md +60 -0
- agent_recipes/templates/ai-video-highlight-extractor/TEMPLATE.yaml +33 -0
- agent_recipes/templates/ai-video-merger/recipe.yaml +40 -0
- agent_recipes/templates/ai-video-merger/tools.py +172 -0
- agent_recipes/templates/ai-video-thumbnails/README.md +16 -0
- agent_recipes/templates/ai-video-thumbnails/TEMPLATE.yaml +53 -0
- agent_recipes/templates/ai-video-to-gif/README.md +14 -0
- agent_recipes/templates/ai-video-to-gif/TEMPLATE.yaml +64 -0
- agent_recipes/templates/ai-voice-cloner/README.md +59 -0
- agent_recipes/templates/ai-voice-cloner/TEMPLATE.yaml +31 -0
- agent_recipes/templates/ai-voiceover-generator/recipe.yaml +41 -0
- agent_recipes/templates/ai-voiceover-generator/tools.py +194 -0
- agent_recipes/templates/ai-watermark-adder/README.md +59 -0
- agent_recipes/templates/ai-watermark-adder/TEMPLATE.yaml +26 -0
- agent_recipes/templates/ai-watermark-remover/README.md +60 -0
- agent_recipes/templates/ai-watermark-remover/TEMPLATE.yaml +32 -0
- agent_recipes/templates/data-transformer/README.md +75 -0
- agent_recipes/templates/data-transformer/TEMPLATE.yaml +63 -0
- agent_recipes/templates/data-transformer/agents.yaml +70 -0
- agent_recipes/templates/data-transformer/workflow.yaml +92 -0
- agent_recipes/templates/shorts-generator/README.md +61 -0
- agent_recipes/templates/shorts-generator/TEMPLATE.yaml +65 -0
- agent_recipes/templates/shorts-generator/agents.yaml +66 -0
- agent_recipes/templates/shorts-generator/workflow.yaml +86 -0
- agent_recipes/templates/transcript-generator/README.md +103 -0
- agent_recipes/templates/transcript-generator/TEMPLATE.yaml +57 -0
- agent_recipes/templates/transcript-generator/agents.yaml +62 -0
- agent_recipes/templates/transcript-generator/workflow.yaml +82 -0
- agent_recipes/templates/video-editor/README.md +70 -0
- agent_recipes/templates/video-editor/TEMPLATE.yaml +55 -0
- agent_recipes/templates/video-editor/agents.yaml +68 -0
- agent_recipes/templates/video-editor/workflow.yaml +92 -0
- agent_recipes-0.0.5.dist-info/METADATA +145 -0
- agent_recipes-0.0.5.dist-info/RECORD +269 -0
- agent_recipes-0.0.5.dist-info/WHEEL +5 -0
- agent_recipes-0.0.5.dist-info/top_level.txt +1 -0
- /236/326/177nE/243/231/214/232/265/322m/201/253/353/022C/372/321/266/b/225^=/272/017t/262/3337/310@/315wb/341pB/277z/216/330/314/004/265B/213/375/236/203/026/373/307/354z41/347#/374q/262/22589/032/276 /277/244Vh/322/017/004/224/215/004/367/377/375/335/n +0 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Main pipeline orchestrator for AI Video Editor.
|
|
3
|
+
|
|
4
|
+
Provides the high-level edit(), probe(), and transcript() functions.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
from .config import PRESETS
|
|
12
|
+
from .models import TranscriptResult, VideoEditResult, VideoProbeResult
|
|
13
|
+
from .ffmpeg_probe import probe as probe_video
|
|
14
|
+
from .transcribe import transcript as transcribe_video
|
|
15
|
+
from .llm_plan import analyze_content, create_simple_edit_plan
|
|
16
|
+
from .timeline import optimize_timeline, calculate_final_duration
|
|
17
|
+
from .render import render as render_video
|
|
18
|
+
from .utils import (
|
|
19
|
+
check_ffmpeg,
|
|
20
|
+
create_workdir,
|
|
21
|
+
file_hash,
|
|
22
|
+
format_duration,
|
|
23
|
+
generate_edl,
|
|
24
|
+
parse_duration,
|
|
25
|
+
save_config_snapshot,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def probe(input_path: str) -> VideoProbeResult:
|
|
30
|
+
"""
|
|
31
|
+
Probe a video file to extract metadata.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
input_path: Path to video file
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
VideoProbeResult with video metadata
|
|
38
|
+
"""
|
|
39
|
+
return probe_video(input_path)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def transcript(
|
|
43
|
+
input_path: str,
|
|
44
|
+
provider: str = "auto",
|
|
45
|
+
language: str = "en",
|
|
46
|
+
workdir: str = None
|
|
47
|
+
) -> TranscriptResult:
|
|
48
|
+
"""
|
|
49
|
+
Transcribe audio/video with word-level timestamps.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
input_path: Path to audio or video file
|
|
53
|
+
provider: Transcription provider (openai, local, auto)
|
|
54
|
+
language: Language code
|
|
55
|
+
workdir: Working directory for temp files
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
TranscriptResult with text and word timestamps
|
|
59
|
+
"""
|
|
60
|
+
return transcribe_video(input_path, provider=provider, language=language, workdir=workdir)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def edit(
|
|
64
|
+
input_path: str,
|
|
65
|
+
output_path: Optional[str] = None,
|
|
66
|
+
preset: str = "podcast",
|
|
67
|
+
workdir: Optional[str] = None,
|
|
68
|
+
remove_fillers: Optional[bool] = None,
|
|
69
|
+
remove_repetitions: Optional[bool] = None,
|
|
70
|
+
remove_tangents: Optional[bool] = None,
|
|
71
|
+
remove_silence: Optional[bool] = None,
|
|
72
|
+
auto_crop: str = "off",
|
|
73
|
+
target_length: Optional[str] = None,
|
|
74
|
+
captions: str = "srt",
|
|
75
|
+
provider: str = "auto",
|
|
76
|
+
use_llm: bool = True,
|
|
77
|
+
force: bool = False,
|
|
78
|
+
verbose: bool = False
|
|
79
|
+
) -> VideoEditResult:
|
|
80
|
+
"""
|
|
81
|
+
Edit a video using AI-powered analysis.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
input_path: Path to input video file
|
|
85
|
+
output_path: Path for output video (default: input_edited.mp4)
|
|
86
|
+
preset: Edit preset (podcast, meeting, course, clean)
|
|
87
|
+
workdir: Working directory for temp files
|
|
88
|
+
remove_fillers: Remove filler words (overrides preset)
|
|
89
|
+
remove_repetitions: Remove repeated phrases (overrides preset)
|
|
90
|
+
remove_tangents: Remove off-topic content (overrides preset)
|
|
91
|
+
remove_silence: Remove long silences (overrides preset)
|
|
92
|
+
auto_crop: Cropping mode (off, center, face)
|
|
93
|
+
target_length: Target output duration (e.g., "6m", "90s")
|
|
94
|
+
captions: Caption mode (off, srt, burn)
|
|
95
|
+
provider: Transcription provider (openai, local, auto)
|
|
96
|
+
use_llm: Use LLM for content analysis (False = simple pattern matching)
|
|
97
|
+
force: Overwrite output if exists
|
|
98
|
+
verbose: Print progress messages
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
VideoEditResult with paths to output files and metadata
|
|
102
|
+
"""
|
|
103
|
+
def _log(message: str):
|
|
104
|
+
if verbose:
|
|
105
|
+
print(f"[AI Video Editor] {message}")
|
|
106
|
+
|
|
107
|
+
# Validate input
|
|
108
|
+
if not os.path.exists(input_path):
|
|
109
|
+
raise FileNotFoundError(f"Input video not found: {input_path}")
|
|
110
|
+
|
|
111
|
+
# Check ffmpeg
|
|
112
|
+
available, msg = check_ffmpeg()
|
|
113
|
+
if not available:
|
|
114
|
+
raise RuntimeError(f"FFmpeg is required: {msg}")
|
|
115
|
+
|
|
116
|
+
_log(f"Starting edit of: {input_path}")
|
|
117
|
+
|
|
118
|
+
# Set up paths
|
|
119
|
+
input_path = str(Path(input_path).absolute())
|
|
120
|
+
|
|
121
|
+
if output_path is None:
|
|
122
|
+
input_stem = Path(input_path).stem
|
|
123
|
+
output_path = str(Path(input_path).parent / f"{input_stem}_edited.mp4")
|
|
124
|
+
|
|
125
|
+
output_path = str(Path(output_path).absolute())
|
|
126
|
+
|
|
127
|
+
# Create working directory
|
|
128
|
+
if workdir is None:
|
|
129
|
+
work_path = create_workdir(input_path)
|
|
130
|
+
else:
|
|
131
|
+
work_path = Path(workdir)
|
|
132
|
+
work_path.mkdir(parents=True, exist_ok=True)
|
|
133
|
+
|
|
134
|
+
_log(f"Working directory: {work_path}")
|
|
135
|
+
|
|
136
|
+
# Build configuration from preset + overrides
|
|
137
|
+
config = _build_config(
|
|
138
|
+
preset=preset,
|
|
139
|
+
remove_fillers=remove_fillers,
|
|
140
|
+
remove_repetitions=remove_repetitions,
|
|
141
|
+
remove_tangents=remove_tangents,
|
|
142
|
+
remove_silence=remove_silence,
|
|
143
|
+
target_length=target_length
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Save config snapshot
|
|
147
|
+
config_snapshot = {
|
|
148
|
+
"input_path": input_path,
|
|
149
|
+
"input_hash": file_hash(input_path),
|
|
150
|
+
"preset": preset,
|
|
151
|
+
"config": config,
|
|
152
|
+
"auto_crop": auto_crop,
|
|
153
|
+
"captions": captions,
|
|
154
|
+
"provider": provider,
|
|
155
|
+
"use_llm": use_llm
|
|
156
|
+
}
|
|
157
|
+
save_config_snapshot(config_snapshot, work_path)
|
|
158
|
+
|
|
159
|
+
# Step 1: Probe video
|
|
160
|
+
_log("Probing video...")
|
|
161
|
+
video_probe = probe_video(input_path)
|
|
162
|
+
_log(f"Duration: {format_duration(video_probe.duration)}, "
|
|
163
|
+
f"Resolution: {video_probe.width}x{video_probe.height}")
|
|
164
|
+
|
|
165
|
+
# Step 2: Transcribe
|
|
166
|
+
_log("Transcribing audio...")
|
|
167
|
+
transcript_result = transcribe_video(
|
|
168
|
+
input_path,
|
|
169
|
+
provider=provider,
|
|
170
|
+
workdir=str(work_path)
|
|
171
|
+
)
|
|
172
|
+
_log(f"Transcribed {len(transcript_result.words)} words")
|
|
173
|
+
|
|
174
|
+
# Save transcript
|
|
175
|
+
transcript_path = work_path / "output" / "transcript.txt"
|
|
176
|
+
transcript_path.parent.mkdir(exist_ok=True)
|
|
177
|
+
transcript_path.write_text(transcript_result.text)
|
|
178
|
+
|
|
179
|
+
srt_path = work_path / "output" / "captions.srt"
|
|
180
|
+
transcript_result.to_srt(srt_path)
|
|
181
|
+
|
|
182
|
+
# Step 3: Analyze content and create edit plan
|
|
183
|
+
_log("Analyzing content...")
|
|
184
|
+
if use_llm:
|
|
185
|
+
edit_plan = analyze_content(transcript_result, config)
|
|
186
|
+
else:
|
|
187
|
+
edit_plan = create_simple_edit_plan(transcript_result, config)
|
|
188
|
+
|
|
189
|
+
_log(f"Found {len(edit_plan.segments_to_remove)} segments to remove")
|
|
190
|
+
|
|
191
|
+
# Step 4: Optimize timeline
|
|
192
|
+
_log("Optimizing timeline...")
|
|
193
|
+
edit_plan = optimize_timeline(edit_plan, transcript_result, config)
|
|
194
|
+
|
|
195
|
+
# Step 5: Render
|
|
196
|
+
_log("Rendering video...")
|
|
197
|
+
render_video(
|
|
198
|
+
input_path=input_path,
|
|
199
|
+
output_path=output_path,
|
|
200
|
+
edit_plan=edit_plan,
|
|
201
|
+
probe=video_probe,
|
|
202
|
+
workdir=work_path,
|
|
203
|
+
crop_mode=auto_crop,
|
|
204
|
+
normalize_audio=True,
|
|
205
|
+
burn_captions=(captions == "burn"),
|
|
206
|
+
srt_path=str(srt_path) if captions == "burn" else None,
|
|
207
|
+
force=force
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# Step 6: Generate reports
|
|
211
|
+
_log("Generating reports...")
|
|
212
|
+
|
|
213
|
+
# EDL file
|
|
214
|
+
edl_path = work_path / "output" / "edit_decision_list.edl"
|
|
215
|
+
generate_edl(
|
|
216
|
+
segments_to_keep=[(s.start, s.end) for s in edit_plan.segments_to_keep],
|
|
217
|
+
segments_to_remove=[(s.start, s.end, s.reason) for s in edit_plan.segments_to_remove],
|
|
218
|
+
output_path=str(edl_path),
|
|
219
|
+
fps=video_probe.fps
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# Edit plan JSON
|
|
223
|
+
plan_path = work_path / "output" / "edit_plan.json"
|
|
224
|
+
edit_plan.to_json(plan_path)
|
|
225
|
+
|
|
226
|
+
# Calculate final duration
|
|
227
|
+
final_duration = calculate_final_duration(edit_plan)
|
|
228
|
+
time_saved = video_probe.duration - final_duration
|
|
229
|
+
|
|
230
|
+
# Build result
|
|
231
|
+
result = VideoEditResult(
|
|
232
|
+
output_path=output_path,
|
|
233
|
+
report_path=str(plan_path),
|
|
234
|
+
transcript_path=str(transcript_path),
|
|
235
|
+
srt_path=str(srt_path),
|
|
236
|
+
edl_path=str(edl_path),
|
|
237
|
+
original_duration=video_probe.duration,
|
|
238
|
+
final_duration=final_duration,
|
|
239
|
+
time_saved=time_saved,
|
|
240
|
+
edit_plan=edit_plan,
|
|
241
|
+
probe=video_probe,
|
|
242
|
+
transcript=transcript_result,
|
|
243
|
+
workdir=str(work_path),
|
|
244
|
+
config_snapshot=config_snapshot
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
# Save full report
|
|
248
|
+
report_path = work_path / "output" / "report.json"
|
|
249
|
+
result.to_json(report_path)
|
|
250
|
+
|
|
251
|
+
_log("Edit complete!")
|
|
252
|
+
_log(f"Original: {format_duration(video_probe.duration)}")
|
|
253
|
+
_log(f"Final: {format_duration(final_duration)}")
|
|
254
|
+
_log(f"Saved: {format_duration(time_saved)} ({(time_saved/video_probe.duration)*100:.1f}%)")
|
|
255
|
+
_log(f"Output: {output_path}")
|
|
256
|
+
|
|
257
|
+
return result
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _build_config(
|
|
261
|
+
preset: str,
|
|
262
|
+
remove_fillers: Optional[bool],
|
|
263
|
+
remove_repetitions: Optional[bool],
|
|
264
|
+
remove_tangents: Optional[bool],
|
|
265
|
+
remove_silence: Optional[bool],
|
|
266
|
+
target_length: Optional[str]
|
|
267
|
+
) -> dict:
|
|
268
|
+
"""Build configuration from preset and overrides."""
|
|
269
|
+
|
|
270
|
+
if preset not in PRESETS:
|
|
271
|
+
raise ValueError(f"Unknown preset: {preset}. Available: {list(PRESETS.keys())}")
|
|
272
|
+
|
|
273
|
+
config = PRESETS[preset].copy()
|
|
274
|
+
|
|
275
|
+
if remove_fillers is not None:
|
|
276
|
+
config["remove_fillers"] = remove_fillers
|
|
277
|
+
if remove_repetitions is not None:
|
|
278
|
+
config["remove_repetitions"] = remove_repetitions
|
|
279
|
+
if remove_tangents is not None:
|
|
280
|
+
config["remove_tangents"] = remove_tangents
|
|
281
|
+
if remove_silence is not None:
|
|
282
|
+
config["remove_silence"] = remove_silence
|
|
283
|
+
|
|
284
|
+
if target_length:
|
|
285
|
+
config["target_length"] = parse_duration(target_length)
|
|
286
|
+
|
|
287
|
+
return config
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Video rendering using FFmpeg.
|
|
3
|
+
|
|
4
|
+
Handles:
|
|
5
|
+
- Segment concatenation
|
|
6
|
+
- Cropping and scaling
|
|
7
|
+
- Audio normalization
|
|
8
|
+
- Caption burning
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import subprocess
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import List, Literal, Tuple
|
|
15
|
+
|
|
16
|
+
from .models import EditPlan, VideoProbeResult
|
|
17
|
+
from .timeline import get_keep_intervals
|
|
18
|
+
from .utils import ensure_ffmpeg
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def render(
|
|
22
|
+
input_path: str,
|
|
23
|
+
output_path: str,
|
|
24
|
+
edit_plan: EditPlan,
|
|
25
|
+
probe: VideoProbeResult,
|
|
26
|
+
workdir: Path,
|
|
27
|
+
crop_mode: Literal["off", "center", "face"] = "off",
|
|
28
|
+
normalize_audio: bool = False,
|
|
29
|
+
burn_captions: bool = False,
|
|
30
|
+
srt_path: str = None,
|
|
31
|
+
force: bool = False
|
|
32
|
+
) -> str:
|
|
33
|
+
"""
|
|
34
|
+
Render edited video.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
input_path: Path to input video
|
|
38
|
+
output_path: Path for output video
|
|
39
|
+
edit_plan: Edit plan with segments to keep
|
|
40
|
+
probe: Video probe result
|
|
41
|
+
workdir: Working directory
|
|
42
|
+
crop_mode: Cropping mode (off, center, face)
|
|
43
|
+
normalize_audio: Normalize audio loudness
|
|
44
|
+
burn_captions: Burn captions into video
|
|
45
|
+
srt_path: Path to SRT file for captions
|
|
46
|
+
force: Overwrite output if exists
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Path to rendered video
|
|
50
|
+
"""
|
|
51
|
+
ensure_ffmpeg()
|
|
52
|
+
|
|
53
|
+
if os.path.exists(output_path) and not force:
|
|
54
|
+
raise FileExistsError(
|
|
55
|
+
f"Output file already exists: {output_path}. "
|
|
56
|
+
"Use force=True to overwrite."
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
intervals = get_keep_intervals(edit_plan)
|
|
60
|
+
|
|
61
|
+
if not intervals:
|
|
62
|
+
raise ValueError("No segments to keep in edit plan")
|
|
63
|
+
|
|
64
|
+
Path(output_path).parent.mkdir(parents=True, exist_ok=True)
|
|
65
|
+
|
|
66
|
+
if len(intervals) <= 10:
|
|
67
|
+
return _render_filter_complex(
|
|
68
|
+
input_path, output_path, intervals, probe,
|
|
69
|
+
crop_mode, normalize_audio, burn_captions, srt_path
|
|
70
|
+
)
|
|
71
|
+
else:
|
|
72
|
+
return _render_concat_demuxer(
|
|
73
|
+
input_path, output_path, intervals, probe, workdir,
|
|
74
|
+
crop_mode, normalize_audio, burn_captions, srt_path
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _render_filter_complex(
|
|
79
|
+
input_path: str,
|
|
80
|
+
output_path: str,
|
|
81
|
+
intervals: List[Tuple[float, float]],
|
|
82
|
+
probe: VideoProbeResult,
|
|
83
|
+
crop_mode: str,
|
|
84
|
+
normalize_audio: bool,
|
|
85
|
+
burn_captions: bool,
|
|
86
|
+
srt_path: str
|
|
87
|
+
) -> str:
|
|
88
|
+
"""Render using filter_complex (efficient for few segments)."""
|
|
89
|
+
|
|
90
|
+
filter_parts = []
|
|
91
|
+
n = len(intervals)
|
|
92
|
+
|
|
93
|
+
for i, (start, end) in enumerate(intervals):
|
|
94
|
+
duration = end - start
|
|
95
|
+
filter_parts.append(
|
|
96
|
+
f"[0:v]trim=start={start}:duration={duration},setpts=PTS-STARTPTS[v{i}]"
|
|
97
|
+
)
|
|
98
|
+
filter_parts.append(
|
|
99
|
+
f"[0:a]atrim=start={start}:duration={duration},asetpts=PTS-STARTPTS[a{i}]"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
video_inputs = "".join(f"[v{i}]" for i in range(n))
|
|
103
|
+
filter_parts.append(f"{video_inputs}concat=n={n}:v=1:a=0[vconcat]")
|
|
104
|
+
|
|
105
|
+
audio_inputs = "".join(f"[a{i}]" for i in range(n))
|
|
106
|
+
filter_parts.append(f"{audio_inputs}concat=n={n}:v=0:a=1[aconcat]")
|
|
107
|
+
|
|
108
|
+
video_out = "[vconcat]"
|
|
109
|
+
if crop_mode == "center":
|
|
110
|
+
target_ratio = 9 / 16
|
|
111
|
+
current_ratio = probe.width / probe.height
|
|
112
|
+
|
|
113
|
+
if current_ratio > target_ratio:
|
|
114
|
+
new_width = int(probe.height * target_ratio)
|
|
115
|
+
crop_x = (probe.width - new_width) // 2
|
|
116
|
+
filter_parts.append(f"[vconcat]crop={new_width}:{probe.height}:{crop_x}:0[vcrop]")
|
|
117
|
+
video_out = "[vcrop]"
|
|
118
|
+
|
|
119
|
+
audio_out = "[aconcat]"
|
|
120
|
+
if normalize_audio:
|
|
121
|
+
filter_parts.append("[aconcat]loudnorm=I=-16:TP=-1.5:LRA=11[anorm]")
|
|
122
|
+
audio_out = "[anorm]"
|
|
123
|
+
|
|
124
|
+
filter_complex = ";".join(filter_parts)
|
|
125
|
+
|
|
126
|
+
cmd = [
|
|
127
|
+
"ffmpeg", "-y",
|
|
128
|
+
"-i", input_path,
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
if burn_captions and srt_path and os.path.exists(srt_path):
|
|
132
|
+
filter_complex += f";{video_out}subtitles='{srt_path}'[vfinal]"
|
|
133
|
+
video_out = "[vfinal]"
|
|
134
|
+
|
|
135
|
+
cmd.extend([
|
|
136
|
+
"-filter_complex", filter_complex,
|
|
137
|
+
"-map", video_out,
|
|
138
|
+
"-map", audio_out,
|
|
139
|
+
"-c:v", "libx264",
|
|
140
|
+
"-preset", "medium",
|
|
141
|
+
"-crf", "23",
|
|
142
|
+
"-c:a", "aac",
|
|
143
|
+
"-b:a", "192k",
|
|
144
|
+
output_path
|
|
145
|
+
])
|
|
146
|
+
|
|
147
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
148
|
+
|
|
149
|
+
if result.returncode != 0:
|
|
150
|
+
raise RuntimeError(f"FFmpeg render failed: {result.stderr}")
|
|
151
|
+
|
|
152
|
+
return output_path
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _render_concat_demuxer(
|
|
156
|
+
input_path: str,
|
|
157
|
+
output_path: str,
|
|
158
|
+
intervals: List[Tuple[float, float]],
|
|
159
|
+
probe: VideoProbeResult,
|
|
160
|
+
workdir: Path,
|
|
161
|
+
crop_mode: str,
|
|
162
|
+
normalize_audio: bool,
|
|
163
|
+
burn_captions: bool,
|
|
164
|
+
srt_path: str
|
|
165
|
+
) -> str:
|
|
166
|
+
"""Render using concat demuxer (efficient for many segments)."""
|
|
167
|
+
|
|
168
|
+
segments_dir = workdir / "segments"
|
|
169
|
+
segments_dir.mkdir(exist_ok=True)
|
|
170
|
+
|
|
171
|
+
segment_files = []
|
|
172
|
+
|
|
173
|
+
for i, (start, end) in enumerate(intervals):
|
|
174
|
+
seg_path = segments_dir / f"seg_{i:04d}.mp4"
|
|
175
|
+
duration = end - start
|
|
176
|
+
|
|
177
|
+
cmd = [
|
|
178
|
+
"ffmpeg", "-y",
|
|
179
|
+
"-ss", str(start),
|
|
180
|
+
"-i", input_path,
|
|
181
|
+
"-t", str(duration),
|
|
182
|
+
"-c", "copy",
|
|
183
|
+
"-avoid_negative_ts", "make_zero",
|
|
184
|
+
str(seg_path)
|
|
185
|
+
]
|
|
186
|
+
|
|
187
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
188
|
+
if result.returncode != 0:
|
|
189
|
+
raise RuntimeError(f"Failed to extract segment {i}: {result.stderr}")
|
|
190
|
+
|
|
191
|
+
segment_files.append(seg_path)
|
|
192
|
+
|
|
193
|
+
concat_file = workdir / "concat.txt"
|
|
194
|
+
with open(concat_file, "w") as f:
|
|
195
|
+
for seg_path in segment_files:
|
|
196
|
+
f.write(f"file '{seg_path}'\n")
|
|
197
|
+
|
|
198
|
+
temp_output = workdir / "temp_concat.mp4"
|
|
199
|
+
|
|
200
|
+
cmd = [
|
|
201
|
+
"ffmpeg", "-y",
|
|
202
|
+
"-f", "concat",
|
|
203
|
+
"-safe", "0",
|
|
204
|
+
"-i", str(concat_file),
|
|
205
|
+
"-c", "copy",
|
|
206
|
+
str(temp_output)
|
|
207
|
+
]
|
|
208
|
+
|
|
209
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
210
|
+
if result.returncode != 0:
|
|
211
|
+
raise RuntimeError(f"Failed to concatenate segments: {result.stderr}")
|
|
212
|
+
|
|
213
|
+
if crop_mode != "off" or normalize_audio or burn_captions:
|
|
214
|
+
return _apply_post_processing(
|
|
215
|
+
str(temp_output), output_path, probe,
|
|
216
|
+
crop_mode, normalize_audio, burn_captions, srt_path
|
|
217
|
+
)
|
|
218
|
+
else:
|
|
219
|
+
import shutil
|
|
220
|
+
shutil.move(str(temp_output), output_path)
|
|
221
|
+
return output_path
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _apply_post_processing(
|
|
225
|
+
input_path: str,
|
|
226
|
+
output_path: str,
|
|
227
|
+
probe: VideoProbeResult,
|
|
228
|
+
crop_mode: str,
|
|
229
|
+
normalize_audio: bool,
|
|
230
|
+
burn_captions: bool,
|
|
231
|
+
srt_path: str
|
|
232
|
+
) -> str:
|
|
233
|
+
"""Apply post-processing filters."""
|
|
234
|
+
|
|
235
|
+
filters_v = []
|
|
236
|
+
filters_a = []
|
|
237
|
+
|
|
238
|
+
if crop_mode == "center":
|
|
239
|
+
target_ratio = 9 / 16
|
|
240
|
+
current_ratio = probe.width / probe.height
|
|
241
|
+
|
|
242
|
+
if current_ratio > target_ratio:
|
|
243
|
+
new_width = int(probe.height * target_ratio)
|
|
244
|
+
crop_x = (probe.width - new_width) // 2
|
|
245
|
+
filters_v.append(f"crop={new_width}:{probe.height}:{crop_x}:0")
|
|
246
|
+
|
|
247
|
+
if burn_captions and srt_path and os.path.exists(srt_path):
|
|
248
|
+
filters_v.append(f"subtitles='{srt_path}'")
|
|
249
|
+
|
|
250
|
+
if normalize_audio:
|
|
251
|
+
filters_a.append("loudnorm=I=-16:TP=-1.5:LRA=11")
|
|
252
|
+
|
|
253
|
+
cmd = ["ffmpeg", "-y", "-i", input_path]
|
|
254
|
+
|
|
255
|
+
if filters_v:
|
|
256
|
+
cmd.extend(["-vf", ",".join(filters_v)])
|
|
257
|
+
|
|
258
|
+
if filters_a:
|
|
259
|
+
cmd.extend(["-af", ",".join(filters_a)])
|
|
260
|
+
|
|
261
|
+
cmd.extend([
|
|
262
|
+
"-c:v", "libx264",
|
|
263
|
+
"-preset", "medium",
|
|
264
|
+
"-crf", "23",
|
|
265
|
+
"-c:a", "aac",
|
|
266
|
+
"-b:a", "192k",
|
|
267
|
+
output_path
|
|
268
|
+
])
|
|
269
|
+
|
|
270
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
271
|
+
if result.returncode != 0:
|
|
272
|
+
raise RuntimeError(f"Post-processing failed: {result.stderr}")
|
|
273
|
+
|
|
274
|
+
return output_path
|