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,343 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI for AI Video Editor.
|
|
3
|
+
|
|
4
|
+
Provides command-line interface for:
|
|
5
|
+
- edit: Edit video with AI analysis
|
|
6
|
+
- probe: Extract video metadata
|
|
7
|
+
- transcript: Generate transcript with timestamps
|
|
8
|
+
- doctor: Check dependencies
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def main():
|
|
19
|
+
"""Main CLI entry point."""
|
|
20
|
+
parser = argparse.ArgumentParser(
|
|
21
|
+
prog="ai_video_editor",
|
|
22
|
+
description="AI-powered video editor - removes fillers, repetitions, tangents, and silences"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
subparsers = parser.add_subparsers(dest="command", help="Commands")
|
|
26
|
+
|
|
27
|
+
# Edit command
|
|
28
|
+
edit_parser = subparsers.add_parser("edit", help="Edit video with AI analysis")
|
|
29
|
+
edit_parser.add_argument("input", help="Input video file")
|
|
30
|
+
edit_parser.add_argument("-o", "--output", help="Output video file")
|
|
31
|
+
edit_parser.add_argument("-p", "--preset", default="podcast",
|
|
32
|
+
choices=["podcast", "meeting", "course", "clean"],
|
|
33
|
+
help="Edit preset (default: podcast)")
|
|
34
|
+
edit_parser.add_argument("--remove-fillers", action="store_true",
|
|
35
|
+
help="Remove filler words")
|
|
36
|
+
edit_parser.add_argument("--remove-repetitions", action="store_true",
|
|
37
|
+
help="Remove repeated phrases")
|
|
38
|
+
edit_parser.add_argument("--remove-tangents", action="store_true",
|
|
39
|
+
help="Remove off-topic content")
|
|
40
|
+
edit_parser.add_argument("--remove-silence", action="store_true",
|
|
41
|
+
help="Remove long silences")
|
|
42
|
+
edit_parser.add_argument("--auto-crop", default="off",
|
|
43
|
+
choices=["off", "center", "face"],
|
|
44
|
+
help="Cropping mode (default: off)")
|
|
45
|
+
edit_parser.add_argument("--target-length", help="Target duration (e.g., 6m, 90s)")
|
|
46
|
+
edit_parser.add_argument("--captions", default="srt",
|
|
47
|
+
choices=["off", "srt", "burn"],
|
|
48
|
+
help="Caption mode (default: srt)")
|
|
49
|
+
edit_parser.add_argument("--provider", default="auto",
|
|
50
|
+
choices=["openai", "local", "auto"],
|
|
51
|
+
help="Transcription provider (default: auto)")
|
|
52
|
+
edit_parser.add_argument("--no-llm", action="store_true",
|
|
53
|
+
help="Use simple pattern matching instead of LLM")
|
|
54
|
+
edit_parser.add_argument("--force", action="store_true",
|
|
55
|
+
help="Overwrite output if exists")
|
|
56
|
+
edit_parser.add_argument("--json-report", help="Save JSON report to path")
|
|
57
|
+
edit_parser.add_argument("-v", "--verbose", action="store_true",
|
|
58
|
+
help="Enable verbose output")
|
|
59
|
+
|
|
60
|
+
# Probe command
|
|
61
|
+
probe_parser = subparsers.add_parser("probe", help="Extract video metadata")
|
|
62
|
+
probe_parser.add_argument("input", help="Input video file")
|
|
63
|
+
probe_parser.add_argument("--json", action="store_true",
|
|
64
|
+
help="Output as JSON")
|
|
65
|
+
|
|
66
|
+
# Transcript command
|
|
67
|
+
transcript_parser = subparsers.add_parser("transcript", help="Generate transcript")
|
|
68
|
+
transcript_parser.add_argument("input", help="Input video/audio file")
|
|
69
|
+
transcript_parser.add_argument("-o", "--output", help="Output file path")
|
|
70
|
+
transcript_parser.add_argument("--format", default="srt",
|
|
71
|
+
choices=["srt", "txt", "json"],
|
|
72
|
+
help="Output format (default: srt)")
|
|
73
|
+
transcript_parser.add_argument("--provider", default="auto",
|
|
74
|
+
choices=["openai", "local", "auto"],
|
|
75
|
+
help="Transcription provider (default: auto)")
|
|
76
|
+
transcript_parser.add_argument("--language", default="en",
|
|
77
|
+
help="Language code (default: en)")
|
|
78
|
+
|
|
79
|
+
# Doctor command
|
|
80
|
+
doctor_parser = subparsers.add_parser("doctor", help="Check dependencies")
|
|
81
|
+
|
|
82
|
+
# Help command
|
|
83
|
+
help_parser = subparsers.add_parser("help", help="Show help")
|
|
84
|
+
|
|
85
|
+
args = parser.parse_args()
|
|
86
|
+
|
|
87
|
+
if args.command is None or args.command == "help":
|
|
88
|
+
_print_help()
|
|
89
|
+
return 0
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
if args.command == "edit":
|
|
93
|
+
return cmd_edit(args)
|
|
94
|
+
elif args.command == "probe":
|
|
95
|
+
return cmd_probe(args)
|
|
96
|
+
elif args.command == "transcript":
|
|
97
|
+
return cmd_transcript(args)
|
|
98
|
+
elif args.command == "doctor":
|
|
99
|
+
return cmd_doctor(args)
|
|
100
|
+
else:
|
|
101
|
+
parser.print_help()
|
|
102
|
+
return 1
|
|
103
|
+
except KeyboardInterrupt:
|
|
104
|
+
print("\nOperation cancelled.")
|
|
105
|
+
return 130
|
|
106
|
+
except Exception as e:
|
|
107
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
108
|
+
if os.environ.get("DEBUG"):
|
|
109
|
+
import traceback
|
|
110
|
+
traceback.print_exc()
|
|
111
|
+
return 1
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _print_help():
|
|
115
|
+
"""Print help message."""
|
|
116
|
+
print("""
|
|
117
|
+
AI Video Editor - Self-contained recipe for PraisonAI
|
|
118
|
+
|
|
119
|
+
Usage:
|
|
120
|
+
python -m ai_video_editor.cli <command> [options]
|
|
121
|
+
|
|
122
|
+
Commands:
|
|
123
|
+
edit <input> Edit video with AI analysis
|
|
124
|
+
probe <input> Extract video metadata
|
|
125
|
+
transcript <input> Generate transcript with timestamps
|
|
126
|
+
doctor Check dependencies
|
|
127
|
+
help Show this help
|
|
128
|
+
|
|
129
|
+
Edit Options:
|
|
130
|
+
--output, -o PATH Output video path
|
|
131
|
+
--preset PRESET Edit preset (podcast, meeting, course, clean)
|
|
132
|
+
--remove-fillers Remove filler words (um, uh, like, etc.)
|
|
133
|
+
--remove-repetitions Remove repeated phrases
|
|
134
|
+
--remove-tangents Remove off-topic content
|
|
135
|
+
--auto-crop MODE Crop mode (off, center, face)
|
|
136
|
+
--target-length TIME Target duration (e.g., 6m, 90s)
|
|
137
|
+
--captions MODE Caption mode (off, srt, burn)
|
|
138
|
+
--provider PROVIDER Transcription provider (openai, local, auto)
|
|
139
|
+
--no-llm Use simple pattern matching instead of LLM
|
|
140
|
+
--force Overwrite output if exists
|
|
141
|
+
--json-report PATH Save JSON report to path
|
|
142
|
+
--verbose, -v Enable verbose output
|
|
143
|
+
|
|
144
|
+
Presets:
|
|
145
|
+
podcast Remove fillers, repetitions, long silences
|
|
146
|
+
meeting Remove fillers, repetitions, tangents, silences
|
|
147
|
+
course Remove fillers, repetitions, short silences
|
|
148
|
+
clean Aggressive removal of all detected issues
|
|
149
|
+
|
|
150
|
+
Examples:
|
|
151
|
+
python -m ai_video_editor.cli edit input.mp4 --preset podcast --output out.mp4
|
|
152
|
+
python -m ai_video_editor.cli edit input.mp4 --remove-fillers --remove-tangents
|
|
153
|
+
python -m ai_video_editor.cli probe input.mp4
|
|
154
|
+
python -m ai_video_editor.cli transcript input.mp4 --output transcript.srt
|
|
155
|
+
python -m ai_video_editor.cli doctor
|
|
156
|
+
|
|
157
|
+
Environment Variables:
|
|
158
|
+
OPENAI_API_KEY Required for transcription and LLM analysis
|
|
159
|
+
WHISPER_MODEL Local whisper model size (default: small)
|
|
160
|
+
DEBUG Enable debug output
|
|
161
|
+
""")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def cmd_edit(args):
|
|
165
|
+
"""Handle edit command."""
|
|
166
|
+
from .pipeline import edit
|
|
167
|
+
|
|
168
|
+
# Determine overrides from flags
|
|
169
|
+
remove_fillers = True if args.remove_fillers else None
|
|
170
|
+
remove_repetitions = True if args.remove_repetitions else None
|
|
171
|
+
remove_tangents = True if args.remove_tangents else None
|
|
172
|
+
remove_silence = True if args.remove_silence else None
|
|
173
|
+
|
|
174
|
+
result = edit(
|
|
175
|
+
input_path=args.input,
|
|
176
|
+
output_path=args.output,
|
|
177
|
+
preset=args.preset,
|
|
178
|
+
remove_fillers=remove_fillers,
|
|
179
|
+
remove_repetitions=remove_repetitions,
|
|
180
|
+
remove_tangents=remove_tangents,
|
|
181
|
+
remove_silence=remove_silence,
|
|
182
|
+
auto_crop=args.auto_crop,
|
|
183
|
+
target_length=args.target_length,
|
|
184
|
+
captions=args.captions,
|
|
185
|
+
provider=args.provider,
|
|
186
|
+
use_llm=not args.no_llm,
|
|
187
|
+
force=args.force,
|
|
188
|
+
verbose=args.verbose
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Print summary
|
|
192
|
+
print(f"\n✓ Edit complete!")
|
|
193
|
+
print(f" Output: {result.output_path}")
|
|
194
|
+
print(f" Original: {_format_duration(result.original_duration)}")
|
|
195
|
+
print(f" Final: {_format_duration(result.final_duration)}")
|
|
196
|
+
print(f" Saved: {_format_duration(result.time_saved)} ({result.time_saved/result.original_duration*100:.1f}%)")
|
|
197
|
+
print(f"\nArtifacts:")
|
|
198
|
+
print(f" Transcript: {result.transcript_path}")
|
|
199
|
+
print(f" Captions: {result.srt_path}")
|
|
200
|
+
print(f" Edit Plan: {result.report_path}")
|
|
201
|
+
print(f" EDL: {result.edl_path}")
|
|
202
|
+
|
|
203
|
+
if args.json_report:
|
|
204
|
+
result.to_json(args.json_report)
|
|
205
|
+
print(f" JSON Report: {args.json_report}")
|
|
206
|
+
|
|
207
|
+
return 0
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def cmd_probe(args):
|
|
211
|
+
"""Handle probe command."""
|
|
212
|
+
from .pipeline import probe
|
|
213
|
+
|
|
214
|
+
result = probe(args.input)
|
|
215
|
+
|
|
216
|
+
if args.json:
|
|
217
|
+
print(json.dumps(result.to_dict(), indent=2))
|
|
218
|
+
else:
|
|
219
|
+
print(f"File: {result.path}")
|
|
220
|
+
print(f"Duration: {_format_duration(result.duration)}")
|
|
221
|
+
print(f"Resolution: {result.width}x{result.height}")
|
|
222
|
+
print(f"FPS: {result.fps}")
|
|
223
|
+
print(f"Video Codec: {result.codec}")
|
|
224
|
+
if result.audio_codec:
|
|
225
|
+
print(f"Audio Codec: {result.audio_codec}")
|
|
226
|
+
print(f"Audio Channels: {result.audio_channels}")
|
|
227
|
+
print(f"Sample Rate: {result.audio_sample_rate} Hz")
|
|
228
|
+
print(f"File Size: {result.file_size / 1024 / 1024:.2f} MB")
|
|
229
|
+
|
|
230
|
+
return 0
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def cmd_transcript(args):
|
|
234
|
+
"""Handle transcript command."""
|
|
235
|
+
from .pipeline import transcript
|
|
236
|
+
|
|
237
|
+
result = transcript(
|
|
238
|
+
input_path=args.input,
|
|
239
|
+
provider=args.provider,
|
|
240
|
+
language=args.language
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
output_path = args.output
|
|
244
|
+
if output_path is None:
|
|
245
|
+
stem = Path(args.input).stem
|
|
246
|
+
if args.format == "srt":
|
|
247
|
+
output_path = f"{stem}.srt"
|
|
248
|
+
elif args.format == "txt":
|
|
249
|
+
output_path = f"{stem}.txt"
|
|
250
|
+
else:
|
|
251
|
+
output_path = f"{stem}_transcript.json"
|
|
252
|
+
|
|
253
|
+
if args.format == "srt":
|
|
254
|
+
result.to_srt(output_path)
|
|
255
|
+
print(f"✓ SRT saved to: {output_path}")
|
|
256
|
+
elif args.format == "txt":
|
|
257
|
+
Path(output_path).write_text(result.text)
|
|
258
|
+
print(f"✓ Transcript saved to: {output_path}")
|
|
259
|
+
else:
|
|
260
|
+
Path(output_path).write_text(json.dumps(result.to_dict(), indent=2))
|
|
261
|
+
print(f"✓ JSON saved to: {output_path}")
|
|
262
|
+
|
|
263
|
+
print(f" Words: {len(result.words)}")
|
|
264
|
+
print(f" Duration: {_format_duration(result.duration)}")
|
|
265
|
+
print(f" Provider: {result.provider}")
|
|
266
|
+
|
|
267
|
+
return 0
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def cmd_doctor(args):
|
|
271
|
+
"""Handle doctor command."""
|
|
272
|
+
from .utils import check_ffmpeg, check_ffprobe
|
|
273
|
+
|
|
274
|
+
print("AI Video Editor - Dependency Check\n")
|
|
275
|
+
|
|
276
|
+
all_ok = True
|
|
277
|
+
|
|
278
|
+
# Check FFmpeg
|
|
279
|
+
ffmpeg_ok, ffmpeg_msg = check_ffmpeg()
|
|
280
|
+
if ffmpeg_ok:
|
|
281
|
+
print(f"✓ FFmpeg: {ffmpeg_msg}")
|
|
282
|
+
else:
|
|
283
|
+
print(f"✗ FFmpeg: {ffmpeg_msg}")
|
|
284
|
+
all_ok = False
|
|
285
|
+
|
|
286
|
+
# Check FFprobe
|
|
287
|
+
ffprobe_ok, ffprobe_msg = check_ffprobe()
|
|
288
|
+
if ffprobe_ok:
|
|
289
|
+
print(f"✓ FFprobe: {ffprobe_msg}")
|
|
290
|
+
else:
|
|
291
|
+
print(f"✗ FFprobe: {ffprobe_msg}")
|
|
292
|
+
all_ok = False
|
|
293
|
+
|
|
294
|
+
# Check OpenAI API key
|
|
295
|
+
api_key = os.environ.get("OPENAI_API_KEY")
|
|
296
|
+
if api_key:
|
|
297
|
+
print(f"✓ OPENAI_API_KEY: Set ({len(api_key)} chars)")
|
|
298
|
+
else:
|
|
299
|
+
print("✗ OPENAI_API_KEY: Not set")
|
|
300
|
+
all_ok = False
|
|
301
|
+
|
|
302
|
+
# Check OpenAI package
|
|
303
|
+
try:
|
|
304
|
+
import openai
|
|
305
|
+
print(f"✓ openai package: {openai.__version__}")
|
|
306
|
+
except ImportError:
|
|
307
|
+
print("✗ openai package: Not installed (pip install openai)")
|
|
308
|
+
all_ok = False
|
|
309
|
+
|
|
310
|
+
# Check faster-whisper (optional)
|
|
311
|
+
try:
|
|
312
|
+
import faster_whisper
|
|
313
|
+
print(f"✓ faster-whisper: Available (optional)")
|
|
314
|
+
except ImportError:
|
|
315
|
+
print("○ faster-whisper: Not installed (optional, for local transcription)")
|
|
316
|
+
|
|
317
|
+
print()
|
|
318
|
+
if all_ok:
|
|
319
|
+
print("All required dependencies are available!")
|
|
320
|
+
return 0
|
|
321
|
+
else:
|
|
322
|
+
print("Some dependencies are missing. Please install them to use all features.")
|
|
323
|
+
return 1
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _format_duration(seconds: float) -> str:
|
|
327
|
+
"""Format duration in human-readable format."""
|
|
328
|
+
hours = int(seconds // 3600)
|
|
329
|
+
minutes = int((seconds % 3600) // 60)
|
|
330
|
+
secs = int(seconds % 60)
|
|
331
|
+
|
|
332
|
+
parts = []
|
|
333
|
+
if hours > 0:
|
|
334
|
+
parts.append(f"{hours}h")
|
|
335
|
+
if minutes > 0 or hours > 0:
|
|
336
|
+
parts.append(f"{minutes}m")
|
|
337
|
+
parts.append(f"{secs}s")
|
|
338
|
+
|
|
339
|
+
return " ".join(parts)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
if __name__ == "__main__":
|
|
343
|
+
sys.exit(main())
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration and presets for AI Video Editor.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# Preset configurations
|
|
10
|
+
PRESETS = {
|
|
11
|
+
"podcast": {
|
|
12
|
+
"remove_fillers": True,
|
|
13
|
+
"remove_repetitions": True,
|
|
14
|
+
"remove_tangents": False,
|
|
15
|
+
"remove_silence": True,
|
|
16
|
+
"silence_threshold_ms": 700,
|
|
17
|
+
"min_segment_length": 1.2,
|
|
18
|
+
"padding_ms": 120,
|
|
19
|
+
"filler_words": ["um", "uh", "like", "you know", "sort of", "kind of", "basically", "actually", "literally", "right"],
|
|
20
|
+
},
|
|
21
|
+
"meeting": {
|
|
22
|
+
"remove_fillers": True,
|
|
23
|
+
"remove_repetitions": True,
|
|
24
|
+
"remove_tangents": True,
|
|
25
|
+
"remove_silence": True,
|
|
26
|
+
"silence_threshold_ms": 1000,
|
|
27
|
+
"min_segment_length": 1.5,
|
|
28
|
+
"padding_ms": 150,
|
|
29
|
+
"filler_words": ["um", "uh", "like", "you know"],
|
|
30
|
+
},
|
|
31
|
+
"course": {
|
|
32
|
+
"remove_fillers": True,
|
|
33
|
+
"remove_repetitions": True,
|
|
34
|
+
"remove_tangents": False,
|
|
35
|
+
"remove_silence": True,
|
|
36
|
+
"silence_threshold_ms": 500,
|
|
37
|
+
"min_segment_length": 1.0,
|
|
38
|
+
"padding_ms": 100,
|
|
39
|
+
"filler_words": ["um", "uh"],
|
|
40
|
+
},
|
|
41
|
+
"clean": {
|
|
42
|
+
"remove_fillers": True,
|
|
43
|
+
"remove_repetitions": True,
|
|
44
|
+
"remove_tangents": True,
|
|
45
|
+
"remove_silence": True,
|
|
46
|
+
"silence_threshold_ms": 600,
|
|
47
|
+
"min_segment_length": 1.0,
|
|
48
|
+
"padding_ms": 100,
|
|
49
|
+
"filler_words": ["um", "uh", "like", "you know", "sort of", "kind of"],
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class EditConfig:
|
|
56
|
+
"""Configuration for video editing."""
|
|
57
|
+
preset: str = "podcast"
|
|
58
|
+
remove_fillers: bool = True
|
|
59
|
+
remove_repetitions: bool = True
|
|
60
|
+
remove_tangents: bool = False
|
|
61
|
+
remove_silence: bool = True
|
|
62
|
+
silence_threshold_ms: int = 700
|
|
63
|
+
min_segment_length: float = 1.2
|
|
64
|
+
padding_ms: int = 120
|
|
65
|
+
filler_words: List[str] = field(default_factory=lambda: ["um", "uh", "like", "you know"])
|
|
66
|
+
target_length: Optional[float] = None # seconds
|
|
67
|
+
auto_crop: str = "off" # off, center, face
|
|
68
|
+
captions: str = "srt" # off, srt, burn
|
|
69
|
+
provider: str = "auto" # openai, local, auto
|
|
70
|
+
use_llm: bool = True
|
|
71
|
+
verbose: bool = False
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def from_preset(cls, preset: str, **overrides) -> "EditConfig":
|
|
75
|
+
"""Create config from preset with optional overrides."""
|
|
76
|
+
if preset not in PRESETS:
|
|
77
|
+
raise ValueError(f"Unknown preset: {preset}. Available: {list(PRESETS.keys())}")
|
|
78
|
+
|
|
79
|
+
config_dict = PRESETS[preset].copy()
|
|
80
|
+
config_dict["preset"] = preset
|
|
81
|
+
config_dict.update(overrides)
|
|
82
|
+
|
|
83
|
+
return cls(**{k: v for k, v in config_dict.items() if k in cls.__dataclass_fields__})
|
|
84
|
+
|
|
85
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
86
|
+
return {
|
|
87
|
+
"preset": self.preset,
|
|
88
|
+
"remove_fillers": self.remove_fillers,
|
|
89
|
+
"remove_repetitions": self.remove_repetitions,
|
|
90
|
+
"remove_tangents": self.remove_tangents,
|
|
91
|
+
"remove_silence": self.remove_silence,
|
|
92
|
+
"silence_threshold_ms": self.silence_threshold_ms,
|
|
93
|
+
"min_segment_length": self.min_segment_length,
|
|
94
|
+
"padding_ms": self.padding_ms,
|
|
95
|
+
"filler_words": self.filler_words,
|
|
96
|
+
"target_length": self.target_length,
|
|
97
|
+
"auto_crop": self.auto_crop,
|
|
98
|
+
"captions": self.captions,
|
|
99
|
+
"provider": self.provider,
|
|
100
|
+
"use_llm": self.use_llm,
|
|
101
|
+
"verbose": self.verbose,
|
|
102
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Video probe functionality using ffprobe.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import subprocess
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from .models import VideoProbeResult
|
|
11
|
+
from .utils import check_ffprobe
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def probe(input_path: str) -> VideoProbeResult:
|
|
15
|
+
"""
|
|
16
|
+
Probe a video file to extract metadata.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
input_path: Path to video file
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
VideoProbeResult with video metadata
|
|
23
|
+
"""
|
|
24
|
+
if not os.path.exists(input_path):
|
|
25
|
+
raise FileNotFoundError(f"Video file not found: {input_path}")
|
|
26
|
+
|
|
27
|
+
available, msg = check_ffprobe()
|
|
28
|
+
if not available:
|
|
29
|
+
raise RuntimeError(f"ffprobe is required: {msg}")
|
|
30
|
+
|
|
31
|
+
cmd = [
|
|
32
|
+
"ffprobe",
|
|
33
|
+
"-v", "quiet",
|
|
34
|
+
"-print_format", "json",
|
|
35
|
+
"-show_format",
|
|
36
|
+
"-show_streams",
|
|
37
|
+
input_path
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
|
42
|
+
except subprocess.TimeoutExpired:
|
|
43
|
+
raise RuntimeError("ffprobe timed out")
|
|
44
|
+
|
|
45
|
+
if result.returncode != 0:
|
|
46
|
+
raise RuntimeError(f"ffprobe failed: {result.stderr}")
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
data = json.loads(result.stdout)
|
|
50
|
+
except json.JSONDecodeError as e:
|
|
51
|
+
raise RuntimeError(f"Failed to parse ffprobe output: {e}")
|
|
52
|
+
|
|
53
|
+
video_stream = None
|
|
54
|
+
audio_stream = None
|
|
55
|
+
|
|
56
|
+
for stream in data.get("streams", []):
|
|
57
|
+
if stream.get("codec_type") == "video" and video_stream is None:
|
|
58
|
+
video_stream = stream
|
|
59
|
+
elif stream.get("codec_type") == "audio" and audio_stream is None:
|
|
60
|
+
audio_stream = stream
|
|
61
|
+
|
|
62
|
+
if video_stream is None:
|
|
63
|
+
raise RuntimeError("No video stream found in file")
|
|
64
|
+
|
|
65
|
+
format_info = data.get("format", {})
|
|
66
|
+
|
|
67
|
+
fps = 30.0
|
|
68
|
+
if "r_frame_rate" in video_stream:
|
|
69
|
+
fps_parts = video_stream["r_frame_rate"].split("/")
|
|
70
|
+
if len(fps_parts) == 2 and int(fps_parts[1]) != 0:
|
|
71
|
+
fps = int(fps_parts[0]) / int(fps_parts[1])
|
|
72
|
+
elif len(fps_parts) == 1:
|
|
73
|
+
fps = float(fps_parts[0])
|
|
74
|
+
elif "avg_frame_rate" in video_stream:
|
|
75
|
+
fps_parts = video_stream["avg_frame_rate"].split("/")
|
|
76
|
+
if len(fps_parts) == 2 and int(fps_parts[1]) != 0:
|
|
77
|
+
fps = int(fps_parts[0]) / int(fps_parts[1])
|
|
78
|
+
|
|
79
|
+
return VideoProbeResult(
|
|
80
|
+
path=str(Path(input_path).absolute()),
|
|
81
|
+
duration=float(format_info.get("duration", 0)),
|
|
82
|
+
width=int(video_stream.get("width", 0)),
|
|
83
|
+
height=int(video_stream.get("height", 0)),
|
|
84
|
+
fps=round(fps, 3),
|
|
85
|
+
codec=video_stream.get("codec_name", "unknown"),
|
|
86
|
+
audio_codec=audio_stream.get("codec_name", "") if audio_stream else "",
|
|
87
|
+
audio_channels=int(audio_stream.get("channels", 0)) if audio_stream else 0,
|
|
88
|
+
audio_sample_rate=int(audio_stream.get("sample_rate", 0)) if audio_stream else 0,
|
|
89
|
+
bitrate=int(format_info.get("bit_rate", 0)),
|
|
90
|
+
file_size=int(format_info.get("size", 0)),
|
|
91
|
+
format_name=format_info.get("format_name", "")
|
|
92
|
+
)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Heuristic-based content analysis (non-LLM).
|
|
3
|
+
|
|
4
|
+
Provides fast detection of:
|
|
5
|
+
- Filler words
|
|
6
|
+
- Repetitions
|
|
7
|
+
- Long silences
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from typing import List
|
|
11
|
+
|
|
12
|
+
from .models import Segment, SegmentCategory, TranscriptResult
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def detect_fillers(
|
|
16
|
+
transcript: TranscriptResult,
|
|
17
|
+
filler_words: List[str]
|
|
18
|
+
) -> List[Segment]:
|
|
19
|
+
"""
|
|
20
|
+
Detect filler words in transcript using simple pattern matching.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
transcript: Transcript with word timestamps
|
|
24
|
+
filler_words: List of filler words to detect
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
List of segments containing fillers
|
|
28
|
+
"""
|
|
29
|
+
filler_segments = []
|
|
30
|
+
filler_set = set(w.lower().strip() for w in filler_words)
|
|
31
|
+
|
|
32
|
+
for word in transcript.words:
|
|
33
|
+
word_text = word.text.lower().strip().strip(".,!?")
|
|
34
|
+
if word_text in filler_set:
|
|
35
|
+
filler_segments.append(Segment(
|
|
36
|
+
start=word.start,
|
|
37
|
+
end=word.end,
|
|
38
|
+
category=SegmentCategory.FILLER,
|
|
39
|
+
reason=f"Filler word: {word.text}",
|
|
40
|
+
text=word.text
|
|
41
|
+
))
|
|
42
|
+
|
|
43
|
+
return filler_segments
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def detect_repetitions(
|
|
47
|
+
transcript: TranscriptResult,
|
|
48
|
+
window_size: int = 5,
|
|
49
|
+
min_repeat_words: int = 3
|
|
50
|
+
) -> List[Segment]:
|
|
51
|
+
"""
|
|
52
|
+
Detect repeated phrases in transcript.
|
|
53
|
+
|
|
54
|
+
Looks for patterns where speaker restarts or repeats themselves.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
transcript: Transcript with word timestamps
|
|
58
|
+
window_size: Number of words to look ahead for repetition
|
|
59
|
+
min_repeat_words: Minimum words to consider a repetition
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
List of segments containing repetitions
|
|
63
|
+
"""
|
|
64
|
+
repetition_segments = []
|
|
65
|
+
words = transcript.words
|
|
66
|
+
|
|
67
|
+
i = 0
|
|
68
|
+
while i < len(words) - min_repeat_words:
|
|
69
|
+
current_phrase = [w.text.lower().strip(".,!?") for w in words[i:i+min_repeat_words]]
|
|
70
|
+
|
|
71
|
+
for j in range(i + 1, min(i + window_size + 1, len(words) - min_repeat_words + 1)):
|
|
72
|
+
next_phrase = [w.text.lower().strip(".,!?") for w in words[j:j+min_repeat_words]]
|
|
73
|
+
|
|
74
|
+
if current_phrase == next_phrase:
|
|
75
|
+
repetition_segments.append(Segment(
|
|
76
|
+
start=words[i].start,
|
|
77
|
+
end=words[j-1].end if j > i else words[i+min_repeat_words-1].end,
|
|
78
|
+
category=SegmentCategory.REPEAT,
|
|
79
|
+
reason=f"Repeated phrase: {' '.join(current_phrase)}",
|
|
80
|
+
text=" ".join(w.text for w in words[i:j])
|
|
81
|
+
))
|
|
82
|
+
i = j + min_repeat_words - 1
|
|
83
|
+
break
|
|
84
|
+
|
|
85
|
+
i += 1
|
|
86
|
+
|
|
87
|
+
return repetition_segments
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def detect_silence(
|
|
91
|
+
transcript: TranscriptResult,
|
|
92
|
+
threshold_ms: float = 700
|
|
93
|
+
) -> List[Segment]:
|
|
94
|
+
"""
|
|
95
|
+
Detect long silences between words.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
transcript: Transcript with word timestamps
|
|
99
|
+
threshold_ms: Minimum silence duration in milliseconds
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
List of segments containing silences
|
|
103
|
+
"""
|
|
104
|
+
silence_segments = []
|
|
105
|
+
threshold_s = threshold_ms / 1000.0
|
|
106
|
+
|
|
107
|
+
words = transcript.words
|
|
108
|
+
for i in range(len(words) - 1):
|
|
109
|
+
gap = words[i + 1].start - words[i].end
|
|
110
|
+
if gap >= threshold_s:
|
|
111
|
+
silence_segments.append(Segment(
|
|
112
|
+
start=words[i].end,
|
|
113
|
+
end=words[i + 1].start,
|
|
114
|
+
category=SegmentCategory.SILENCE,
|
|
115
|
+
reason=f"Silence: {gap:.2f}s",
|
|
116
|
+
text=""
|
|
117
|
+
))
|
|
118
|
+
|
|
119
|
+
return silence_segments
|