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,278 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Timeline optimization for video editing.
|
|
3
|
+
|
|
4
|
+
Handles:
|
|
5
|
+
- Merging overlapping segments
|
|
6
|
+
- Snapping to word boundaries
|
|
7
|
+
- Applying padding/handles
|
|
8
|
+
- Enforcing minimum segment lengths
|
|
9
|
+
- Target duration constraints
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from typing import Any, Dict, List, Tuple
|
|
13
|
+
|
|
14
|
+
from .models import EditPlan, Segment, SegmentCategory, TranscriptResult, Word
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def optimize_timeline(
|
|
18
|
+
edit_plan: EditPlan,
|
|
19
|
+
transcript: TranscriptResult,
|
|
20
|
+
config: Dict[str, Any]
|
|
21
|
+
) -> EditPlan:
|
|
22
|
+
"""
|
|
23
|
+
Optimize edit plan timeline.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
edit_plan: Initial edit plan
|
|
27
|
+
transcript: Transcript with word timestamps
|
|
28
|
+
config: Configuration with padding, min_segment_length, etc.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Optimized EditPlan
|
|
32
|
+
"""
|
|
33
|
+
padding_ms = config.get("padding_ms", 120)
|
|
34
|
+
padding_s = padding_ms / 1000.0
|
|
35
|
+
min_segment_length = config.get("min_segment_length", 1.2)
|
|
36
|
+
target_length = config.get("target_length")
|
|
37
|
+
|
|
38
|
+
segments_to_keep = [
|
|
39
|
+
_snap_to_word_boundaries(seg, transcript.words)
|
|
40
|
+
for seg in edit_plan.segments_to_keep
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
segments_to_remove = [
|
|
44
|
+
_snap_to_word_boundaries(seg, transcript.words)
|
|
45
|
+
for seg in edit_plan.segments_to_remove
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
segments_to_keep = _merge_overlapping(segments_to_keep)
|
|
49
|
+
|
|
50
|
+
segments_to_keep = _apply_padding(
|
|
51
|
+
segments_to_keep,
|
|
52
|
+
padding_s,
|
|
53
|
+
0.0,
|
|
54
|
+
transcript.duration
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
segments_to_keep = [
|
|
58
|
+
seg for seg in segments_to_keep
|
|
59
|
+
if seg.duration >= min_segment_length
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
segments_to_remove = _calculate_remove_segments(
|
|
63
|
+
segments_to_keep,
|
|
64
|
+
edit_plan.segments_to_remove,
|
|
65
|
+
transcript.duration
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
if target_length:
|
|
69
|
+
segments_to_keep = _apply_target_length(
|
|
70
|
+
segments_to_keep,
|
|
71
|
+
segments_to_remove,
|
|
72
|
+
target_length
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
return EditPlan(
|
|
76
|
+
segments_to_keep=segments_to_keep,
|
|
77
|
+
segments_to_remove=segments_to_remove,
|
|
78
|
+
chapters=edit_plan.chapters,
|
|
79
|
+
summary=edit_plan.summary,
|
|
80
|
+
topics=edit_plan.topics
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _snap_to_word_boundaries(segment: Segment, words: List[Word]) -> Segment:
|
|
85
|
+
"""Snap segment boundaries to nearest word boundaries."""
|
|
86
|
+
if not words:
|
|
87
|
+
return segment
|
|
88
|
+
|
|
89
|
+
new_start = segment.start
|
|
90
|
+
for word in words:
|
|
91
|
+
if word.start >= segment.start:
|
|
92
|
+
new_start = word.start
|
|
93
|
+
break
|
|
94
|
+
if word.end >= segment.start:
|
|
95
|
+
new_start = word.start
|
|
96
|
+
break
|
|
97
|
+
|
|
98
|
+
new_end = segment.end
|
|
99
|
+
for word in reversed(words):
|
|
100
|
+
if word.end <= segment.end:
|
|
101
|
+
new_end = word.end
|
|
102
|
+
break
|
|
103
|
+
if word.start <= segment.end:
|
|
104
|
+
new_end = word.end
|
|
105
|
+
break
|
|
106
|
+
|
|
107
|
+
if new_end <= new_start:
|
|
108
|
+
return segment
|
|
109
|
+
|
|
110
|
+
return Segment(
|
|
111
|
+
start=new_start,
|
|
112
|
+
end=new_end,
|
|
113
|
+
category=segment.category,
|
|
114
|
+
reason=segment.reason,
|
|
115
|
+
confidence=segment.confidence,
|
|
116
|
+
text=segment.text,
|
|
117
|
+
words=segment.words
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _merge_overlapping(segments: List[Segment]) -> List[Segment]:
|
|
122
|
+
"""Merge overlapping or adjacent segments."""
|
|
123
|
+
if not segments:
|
|
124
|
+
return []
|
|
125
|
+
|
|
126
|
+
sorted_segs = sorted(segments, key=lambda s: s.start)
|
|
127
|
+
merged = [sorted_segs[0]]
|
|
128
|
+
|
|
129
|
+
for seg in sorted_segs[1:]:
|
|
130
|
+
last = merged[-1]
|
|
131
|
+
|
|
132
|
+
if seg.start <= last.end + 0.1:
|
|
133
|
+
merged[-1] = Segment(
|
|
134
|
+
start=last.start,
|
|
135
|
+
end=max(last.end, seg.end),
|
|
136
|
+
category=last.category,
|
|
137
|
+
reason=f"{last.reason}; {seg.reason}" if seg.reason else last.reason,
|
|
138
|
+
text=f"{last.text} {seg.text}".strip()
|
|
139
|
+
)
|
|
140
|
+
else:
|
|
141
|
+
merged.append(seg)
|
|
142
|
+
|
|
143
|
+
return merged
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _apply_padding(
|
|
147
|
+
segments: List[Segment],
|
|
148
|
+
padding_s: float,
|
|
149
|
+
min_time: float,
|
|
150
|
+
max_time: float
|
|
151
|
+
) -> List[Segment]:
|
|
152
|
+
"""Apply padding/handles to segment boundaries."""
|
|
153
|
+
padded = []
|
|
154
|
+
|
|
155
|
+
for seg in segments:
|
|
156
|
+
new_start = max(min_time, seg.start - padding_s)
|
|
157
|
+
new_end = min(max_time, seg.end + padding_s)
|
|
158
|
+
|
|
159
|
+
padded.append(Segment(
|
|
160
|
+
start=new_start,
|
|
161
|
+
end=new_end,
|
|
162
|
+
category=seg.category,
|
|
163
|
+
reason=seg.reason,
|
|
164
|
+
text=seg.text
|
|
165
|
+
))
|
|
166
|
+
|
|
167
|
+
return _merge_overlapping(padded)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _calculate_remove_segments(
|
|
171
|
+
keep_segments: List[Segment],
|
|
172
|
+
original_remove: List[Segment],
|
|
173
|
+
total_duration: float
|
|
174
|
+
) -> List[Segment]:
|
|
175
|
+
"""Calculate remove segments as gaps between keep segments."""
|
|
176
|
+
if not keep_segments:
|
|
177
|
+
return original_remove
|
|
178
|
+
|
|
179
|
+
remove_segments = []
|
|
180
|
+
sorted_keep = sorted(keep_segments, key=lambda s: s.start)
|
|
181
|
+
|
|
182
|
+
if sorted_keep[0].start > 0.01:
|
|
183
|
+
reason = _find_remove_reason(0, sorted_keep[0].start, original_remove)
|
|
184
|
+
category = _find_remove_category(0, sorted_keep[0].start, original_remove)
|
|
185
|
+
remove_segments.append(Segment(
|
|
186
|
+
start=0,
|
|
187
|
+
end=sorted_keep[0].start,
|
|
188
|
+
category=category,
|
|
189
|
+
reason=reason
|
|
190
|
+
))
|
|
191
|
+
|
|
192
|
+
for i in range(len(sorted_keep) - 1):
|
|
193
|
+
gap_start = sorted_keep[i].end
|
|
194
|
+
gap_end = sorted_keep[i + 1].start
|
|
195
|
+
|
|
196
|
+
if gap_end - gap_start > 0.01:
|
|
197
|
+
reason = _find_remove_reason(gap_start, gap_end, original_remove)
|
|
198
|
+
category = _find_remove_category(gap_start, gap_end, original_remove)
|
|
199
|
+
remove_segments.append(Segment(
|
|
200
|
+
start=gap_start,
|
|
201
|
+
end=gap_end,
|
|
202
|
+
category=category,
|
|
203
|
+
reason=reason
|
|
204
|
+
))
|
|
205
|
+
|
|
206
|
+
if sorted_keep[-1].end < total_duration - 0.01:
|
|
207
|
+
reason = _find_remove_reason(sorted_keep[-1].end, total_duration, original_remove)
|
|
208
|
+
category = _find_remove_category(sorted_keep[-1].end, total_duration, original_remove)
|
|
209
|
+
remove_segments.append(Segment(
|
|
210
|
+
start=sorted_keep[-1].end,
|
|
211
|
+
end=total_duration,
|
|
212
|
+
category=category,
|
|
213
|
+
reason=reason
|
|
214
|
+
))
|
|
215
|
+
|
|
216
|
+
return remove_segments
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _find_remove_reason(start: float, end: float, original_remove: List[Segment]) -> str:
|
|
220
|
+
"""Find reason for removal from original segments."""
|
|
221
|
+
for seg in original_remove:
|
|
222
|
+
if seg.start < end and seg.end > start:
|
|
223
|
+
return seg.reason
|
|
224
|
+
return "Removed"
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _find_remove_category(start: float, end: float, original_remove: List[Segment]) -> SegmentCategory:
|
|
228
|
+
"""Find category for removal from original segments."""
|
|
229
|
+
for seg in original_remove:
|
|
230
|
+
if seg.start < end and seg.end > start:
|
|
231
|
+
return seg.category
|
|
232
|
+
return SegmentCategory.FILLER
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _apply_target_length(
|
|
236
|
+
keep_segments: List[Segment],
|
|
237
|
+
remove_segments: List[Segment],
|
|
238
|
+
target_length: float
|
|
239
|
+
) -> List[Segment]:
|
|
240
|
+
"""Adjust segments to meet target length."""
|
|
241
|
+
current_duration = sum(s.duration for s in keep_segments)
|
|
242
|
+
|
|
243
|
+
if current_duration <= target_length:
|
|
244
|
+
return keep_segments
|
|
245
|
+
|
|
246
|
+
excess = current_duration - target_length
|
|
247
|
+
sorted_segs = sorted(keep_segments, key=lambda s: s.confidence)
|
|
248
|
+
|
|
249
|
+
result = []
|
|
250
|
+
removed_duration = 0.0
|
|
251
|
+
|
|
252
|
+
for seg in sorted_segs:
|
|
253
|
+
if removed_duration < excess and seg.duration < excess - removed_duration:
|
|
254
|
+
removed_duration += seg.duration
|
|
255
|
+
else:
|
|
256
|
+
result.append(seg)
|
|
257
|
+
|
|
258
|
+
return sorted(result, key=lambda s: s.start)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def get_keep_intervals(edit_plan: EditPlan) -> List[Tuple[float, float]]:
|
|
262
|
+
"""Get list of (start, end) tuples for segments to keep."""
|
|
263
|
+
intervals = [(seg.start, seg.end) for seg in edit_plan.segments_to_keep]
|
|
264
|
+
return sorted(intervals, key=lambda x: x[0])
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def get_remove_intervals(edit_plan: EditPlan) -> List[Tuple[float, float, str]]:
|
|
268
|
+
"""Get list of (start, end, reason) tuples for segments to remove."""
|
|
269
|
+
intervals = [
|
|
270
|
+
(seg.start, seg.end, seg.reason)
|
|
271
|
+
for seg in edit_plan.segments_to_remove
|
|
272
|
+
]
|
|
273
|
+
return sorted(intervals, key=lambda x: x[0])
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def calculate_final_duration(edit_plan: EditPlan) -> float:
|
|
277
|
+
"""Calculate final video duration after edits."""
|
|
278
|
+
return sum(seg.duration for seg in edit_plan.segments_to_keep)
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Audio transcription with word-level timestamps.
|
|
3
|
+
|
|
4
|
+
Supports:
|
|
5
|
+
- OpenAI Whisper API (default, requires OPENAI_API_KEY)
|
|
6
|
+
- Local faster-whisper (optional fallback)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import List, Literal
|
|
12
|
+
|
|
13
|
+
from .models import TranscriptResult, Word
|
|
14
|
+
from .utils import extract_audio, create_workdir
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def transcript(
|
|
18
|
+
input_path: str,
|
|
19
|
+
provider: Literal["openai", "local", "auto"] = "auto",
|
|
20
|
+
language: str = "en",
|
|
21
|
+
workdir: str = None
|
|
22
|
+
) -> TranscriptResult:
|
|
23
|
+
"""
|
|
24
|
+
Transcribe audio/video with word-level timestamps.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
input_path: Path to audio or video file
|
|
28
|
+
provider: Transcription provider (openai, local, auto)
|
|
29
|
+
language: Language code
|
|
30
|
+
workdir: Working directory for temp files
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
TranscriptResult with text and word timestamps
|
|
34
|
+
"""
|
|
35
|
+
if not os.path.exists(input_path):
|
|
36
|
+
raise FileNotFoundError(f"Input file not found: {input_path}")
|
|
37
|
+
|
|
38
|
+
if provider == "auto":
|
|
39
|
+
if os.environ.get("OPENAI_API_KEY"):
|
|
40
|
+
provider = "openai"
|
|
41
|
+
else:
|
|
42
|
+
provider = "local"
|
|
43
|
+
|
|
44
|
+
audio_path = input_path
|
|
45
|
+
input_ext = Path(input_path).suffix.lower()
|
|
46
|
+
|
|
47
|
+
if input_ext in [".mp4", ".mov", ".avi", ".mkv", ".webm", ".m4v"]:
|
|
48
|
+
if workdir is None:
|
|
49
|
+
work_path = create_workdir(input_path)
|
|
50
|
+
else:
|
|
51
|
+
work_path = Path(workdir)
|
|
52
|
+
work_path.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
|
|
54
|
+
audio_path = str(work_path / "audio" / "extracted.wav")
|
|
55
|
+
Path(audio_path).parent.mkdir(parents=True, exist_ok=True)
|
|
56
|
+
extract_audio(input_path, audio_path)
|
|
57
|
+
|
|
58
|
+
if provider == "openai":
|
|
59
|
+
return _transcribe_openai(audio_path, language)
|
|
60
|
+
elif provider == "local":
|
|
61
|
+
return _transcribe_local(audio_path, language)
|
|
62
|
+
else:
|
|
63
|
+
raise ValueError(f"Unknown provider: {provider}")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _transcribe_openai(audio_path: str, language: str) -> TranscriptResult:
|
|
67
|
+
"""Transcribe using OpenAI Whisper API."""
|
|
68
|
+
try:
|
|
69
|
+
from openai import OpenAI
|
|
70
|
+
except ImportError:
|
|
71
|
+
raise ImportError(
|
|
72
|
+
"OpenAI package required for transcription. "
|
|
73
|
+
"Install with: pip install openai"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
api_key = os.environ.get("OPENAI_API_KEY")
|
|
77
|
+
if not api_key:
|
|
78
|
+
raise ValueError(
|
|
79
|
+
"OPENAI_API_KEY environment variable required for OpenAI transcription"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
client = OpenAI(api_key=api_key)
|
|
83
|
+
|
|
84
|
+
file_size = os.path.getsize(audio_path)
|
|
85
|
+
if file_size > 25 * 1024 * 1024:
|
|
86
|
+
return _transcribe_openai_chunked(client, audio_path, language)
|
|
87
|
+
|
|
88
|
+
with open(audio_path, "rb") as f:
|
|
89
|
+
response = client.audio.transcriptions.create(
|
|
90
|
+
model="whisper-1",
|
|
91
|
+
file=f,
|
|
92
|
+
language=language,
|
|
93
|
+
response_format="verbose_json",
|
|
94
|
+
timestamp_granularities=["word"]
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
words = []
|
|
98
|
+
if hasattr(response, "words") and response.words:
|
|
99
|
+
for w in response.words:
|
|
100
|
+
words.append(Word(
|
|
101
|
+
text=w.word if hasattr(w, "word") else w.get("word", ""),
|
|
102
|
+
start=w.start if hasattr(w, "start") else w.get("start", 0),
|
|
103
|
+
end=w.end if hasattr(w, "end") else w.get("end", 0),
|
|
104
|
+
confidence=1.0
|
|
105
|
+
))
|
|
106
|
+
|
|
107
|
+
duration = words[-1].end if words else 0.0
|
|
108
|
+
|
|
109
|
+
return TranscriptResult(
|
|
110
|
+
text=response.text if hasattr(response, "text") else str(response),
|
|
111
|
+
words=words,
|
|
112
|
+
language=language,
|
|
113
|
+
duration=duration,
|
|
114
|
+
provider="openai"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _transcribe_openai_chunked(
|
|
119
|
+
client,
|
|
120
|
+
audio_path: str,
|
|
121
|
+
language: str,
|
|
122
|
+
chunk_duration: int = 600
|
|
123
|
+
) -> TranscriptResult:
|
|
124
|
+
"""Transcribe long audio by chunking."""
|
|
125
|
+
import subprocess
|
|
126
|
+
import tempfile
|
|
127
|
+
|
|
128
|
+
result = subprocess.run(
|
|
129
|
+
["ffprobe", "-v", "quiet", "-show_entries", "format=duration",
|
|
130
|
+
"-of", "default=noprint_wrappers=1:nokey=1", audio_path],
|
|
131
|
+
capture_output=True, text=True
|
|
132
|
+
)
|
|
133
|
+
total_duration = float(result.stdout.strip())
|
|
134
|
+
|
|
135
|
+
all_words: List[Word] = []
|
|
136
|
+
all_text_parts: List[str] = []
|
|
137
|
+
|
|
138
|
+
offset = 0.0
|
|
139
|
+
|
|
140
|
+
while offset < total_duration:
|
|
141
|
+
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp:
|
|
142
|
+
chunk_path = tmp.name
|
|
143
|
+
|
|
144
|
+
subprocess.run([
|
|
145
|
+
"ffmpeg", "-y",
|
|
146
|
+
"-ss", str(offset),
|
|
147
|
+
"-i", audio_path,
|
|
148
|
+
"-t", str(chunk_duration),
|
|
149
|
+
"-acodec", "pcm_s16le",
|
|
150
|
+
"-ar", "16000",
|
|
151
|
+
"-ac", "1",
|
|
152
|
+
chunk_path
|
|
153
|
+
], capture_output=True)
|
|
154
|
+
|
|
155
|
+
with open(chunk_path, "rb") as f:
|
|
156
|
+
response = client.audio.transcriptions.create(
|
|
157
|
+
model="whisper-1",
|
|
158
|
+
file=f,
|
|
159
|
+
language=language,
|
|
160
|
+
response_format="verbose_json",
|
|
161
|
+
timestamp_granularities=["word"]
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
if hasattr(response, "words") and response.words:
|
|
165
|
+
for w in response.words:
|
|
166
|
+
word_start = (w.start if hasattr(w, "start") else w.get("start", 0)) + offset
|
|
167
|
+
word_end = (w.end if hasattr(w, "end") else w.get("end", 0)) + offset
|
|
168
|
+
all_words.append(Word(
|
|
169
|
+
text=w.word if hasattr(w, "word") else w.get("word", ""),
|
|
170
|
+
start=word_start,
|
|
171
|
+
end=word_end,
|
|
172
|
+
confidence=1.0
|
|
173
|
+
))
|
|
174
|
+
|
|
175
|
+
all_text_parts.append(response.text if hasattr(response, "text") else str(response))
|
|
176
|
+
|
|
177
|
+
os.unlink(chunk_path)
|
|
178
|
+
offset += chunk_duration
|
|
179
|
+
|
|
180
|
+
return TranscriptResult(
|
|
181
|
+
text=" ".join(all_text_parts),
|
|
182
|
+
words=all_words,
|
|
183
|
+
language=language,
|
|
184
|
+
duration=total_duration,
|
|
185
|
+
provider="openai"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _transcribe_local(audio_path: str, language: str) -> TranscriptResult:
|
|
190
|
+
"""Transcribe using local faster-whisper."""
|
|
191
|
+
try:
|
|
192
|
+
from faster_whisper import WhisperModel
|
|
193
|
+
except ImportError:
|
|
194
|
+
raise ImportError(
|
|
195
|
+
"faster-whisper package required for local transcription. "
|
|
196
|
+
"Install with: pip install faster-whisper"
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
model_size = os.environ.get("WHISPER_MODEL", "small")
|
|
200
|
+
device = os.environ.get("WHISPER_DEVICE", "auto")
|
|
201
|
+
compute_type = os.environ.get("WHISPER_COMPUTE_TYPE", "auto")
|
|
202
|
+
|
|
203
|
+
model = WhisperModel(model_size, device=device, compute_type=compute_type)
|
|
204
|
+
|
|
205
|
+
segments, info = model.transcribe(
|
|
206
|
+
audio_path,
|
|
207
|
+
language=language,
|
|
208
|
+
word_timestamps=True
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
words: List[Word] = []
|
|
212
|
+
text_parts: List[str] = []
|
|
213
|
+
|
|
214
|
+
for segment in segments:
|
|
215
|
+
text_parts.append(segment.text)
|
|
216
|
+
if segment.words:
|
|
217
|
+
for w in segment.words:
|
|
218
|
+
words.append(Word(
|
|
219
|
+
text=w.word,
|
|
220
|
+
start=w.start,
|
|
221
|
+
end=w.end,
|
|
222
|
+
confidence=w.probability if hasattr(w, "probability") else 1.0
|
|
223
|
+
))
|
|
224
|
+
|
|
225
|
+
duration = words[-1].end if words else 0.0
|
|
226
|
+
|
|
227
|
+
return TranscriptResult(
|
|
228
|
+
text=" ".join(text_parts),
|
|
229
|
+
words=words,
|
|
230
|
+
language=info.language if hasattr(info, "language") else language,
|
|
231
|
+
duration=duration,
|
|
232
|
+
provider="local"
|
|
233
|
+
)
|