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,277 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LLM-based content analysis for video editing.
|
|
3
|
+
|
|
4
|
+
Uses LLM to:
|
|
5
|
+
- Understand video content
|
|
6
|
+
- Identify tangent segments
|
|
7
|
+
- Generate chapter markers
|
|
8
|
+
- Create edit plans
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
from typing import Any, Dict, List
|
|
14
|
+
|
|
15
|
+
from .models import (
|
|
16
|
+
Chapter,
|
|
17
|
+
EditPlan,
|
|
18
|
+
Segment,
|
|
19
|
+
SegmentCategory,
|
|
20
|
+
TranscriptResult,
|
|
21
|
+
)
|
|
22
|
+
from .heuristics import detect_fillers, detect_repetitions, detect_silence
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
ANALYSIS_SYSTEM_PROMPT = """You are an expert video editor AI assistant. Your task is to analyze video transcripts and create precise edit plans.
|
|
26
|
+
|
|
27
|
+
You will receive:
|
|
28
|
+
1. A transcript with word-level timestamps
|
|
29
|
+
2. Configuration for what to remove (fillers, tangents, repetitions)
|
|
30
|
+
3. Optional target duration
|
|
31
|
+
|
|
32
|
+
Your job is to identify segments to KEEP and segments to REMOVE, with precise timestamps.
|
|
33
|
+
|
|
34
|
+
Rules:
|
|
35
|
+
- Never cut in the middle of a word - use word boundaries
|
|
36
|
+
- Minimum segment length should be respected
|
|
37
|
+
- Add padding around cuts for smooth transitions
|
|
38
|
+
- Be conservative with tangent detection - only remove clearly off-topic content
|
|
39
|
+
- For fillers, only remove standalone filler words, not fillers that are part of natural speech flow
|
|
40
|
+
- For repetitions, identify when speaker restarts a sentence or repeats themselves
|
|
41
|
+
|
|
42
|
+
Output must be valid JSON matching the specified schema."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
ANALYSIS_USER_PROMPT = """Analyze this transcript and create an edit plan.
|
|
46
|
+
|
|
47
|
+
TRANSCRIPT:
|
|
48
|
+
{transcript}
|
|
49
|
+
|
|
50
|
+
WORD TIMESTAMPS:
|
|
51
|
+
{word_timestamps}
|
|
52
|
+
|
|
53
|
+
CONFIGURATION:
|
|
54
|
+
- Remove fillers: {remove_fillers}
|
|
55
|
+
- Filler words to detect: {filler_words}
|
|
56
|
+
- Remove repetitions: {remove_repetitions}
|
|
57
|
+
- Remove tangents: {remove_tangents}
|
|
58
|
+
- Remove long silences: {remove_silence}
|
|
59
|
+
- Silence threshold: {silence_threshold_ms}ms
|
|
60
|
+
- Minimum segment length: {min_segment_length}s
|
|
61
|
+
- Padding around cuts: {padding_ms}ms
|
|
62
|
+
{target_length_instruction}
|
|
63
|
+
|
|
64
|
+
Analyze the content and return a JSON object with this exact structure:
|
|
65
|
+
{{
|
|
66
|
+
"summary": "Brief summary of what the video is about",
|
|
67
|
+
"topics": ["main topic 1", "main topic 2"],
|
|
68
|
+
"segments_to_keep": [
|
|
69
|
+
{{"start": 0.0, "end": 10.5, "reason": "Introduction", "text": "transcript text..."}}
|
|
70
|
+
],
|
|
71
|
+
"segments_to_remove": [
|
|
72
|
+
{{"start": 10.5, "end": 11.2, "category": "filler", "reason": "um", "text": "um"}}
|
|
73
|
+
],
|
|
74
|
+
"chapters": [
|
|
75
|
+
{{"start": 0.0, "title": "Introduction", "description": "..."}}
|
|
76
|
+
]
|
|
77
|
+
}}
|
|
78
|
+
|
|
79
|
+
Categories for segments_to_remove: "filler", "tangent", "repeat", "silence"
|
|
80
|
+
|
|
81
|
+
Be precise with timestamps. Ensure segments don't overlap and cover the entire duration."""
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def analyze_content(
|
|
85
|
+
transcript: TranscriptResult,
|
|
86
|
+
config: Dict[str, Any],
|
|
87
|
+
model: str = None
|
|
88
|
+
) -> EditPlan:
|
|
89
|
+
"""
|
|
90
|
+
Analyze transcript content using LLM.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
transcript: Transcript with word timestamps
|
|
94
|
+
config: Edit configuration
|
|
95
|
+
model: LLM model to use (default: gpt-4o-mini)
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
EditPlan with segments to keep/remove
|
|
99
|
+
"""
|
|
100
|
+
try:
|
|
101
|
+
from openai import OpenAI
|
|
102
|
+
except ImportError:
|
|
103
|
+
raise ImportError(
|
|
104
|
+
"OpenAI package required for content analysis. "
|
|
105
|
+
"Install with: pip install openai"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
api_key = os.environ.get("OPENAI_API_KEY")
|
|
109
|
+
if not api_key:
|
|
110
|
+
raise ValueError("OPENAI_API_KEY environment variable required")
|
|
111
|
+
|
|
112
|
+
client = OpenAI(api_key=api_key)
|
|
113
|
+
|
|
114
|
+
if model is None:
|
|
115
|
+
model = os.environ.get("OPENAI_MODEL", "gpt-4o-mini")
|
|
116
|
+
|
|
117
|
+
word_ts_lines = []
|
|
118
|
+
for w in transcript.words[:500]:
|
|
119
|
+
word_ts_lines.append(f"[{w.start:.2f}-{w.end:.2f}] {w.text}")
|
|
120
|
+
|
|
121
|
+
target_instruction = ""
|
|
122
|
+
if config.get("target_length"):
|
|
123
|
+
target_instruction = f"- Target output length: {config['target_length']}s (prioritize removing lower-value content to meet this)"
|
|
124
|
+
|
|
125
|
+
user_prompt = ANALYSIS_USER_PROMPT.format(
|
|
126
|
+
transcript=transcript.text[:8000],
|
|
127
|
+
word_timestamps="\n".join(word_ts_lines),
|
|
128
|
+
remove_fillers=config.get("remove_fillers", True),
|
|
129
|
+
filler_words=", ".join(config.get("filler_words", ["um", "uh"])),
|
|
130
|
+
remove_repetitions=config.get("remove_repetitions", True),
|
|
131
|
+
remove_tangents=config.get("remove_tangents", False),
|
|
132
|
+
remove_silence=config.get("remove_silence", True),
|
|
133
|
+
silence_threshold_ms=config.get("silence_threshold_ms", 700),
|
|
134
|
+
min_segment_length=config.get("min_segment_length", 1.2),
|
|
135
|
+
padding_ms=config.get("padding_ms", 120),
|
|
136
|
+
target_length_instruction=target_instruction
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
response = client.chat.completions.create(
|
|
140
|
+
model=model,
|
|
141
|
+
messages=[
|
|
142
|
+
{"role": "system", "content": ANALYSIS_SYSTEM_PROMPT},
|
|
143
|
+
{"role": "user", "content": user_prompt}
|
|
144
|
+
],
|
|
145
|
+
temperature=0.1,
|
|
146
|
+
response_format={"type": "json_object"}
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
content = response.choices[0].message.content
|
|
150
|
+
try:
|
|
151
|
+
data = json.loads(content)
|
|
152
|
+
except json.JSONDecodeError as e:
|
|
153
|
+
raise RuntimeError(f"Failed to parse LLM response as JSON: {e}\nResponse: {content[:500]}")
|
|
154
|
+
|
|
155
|
+
return _parse_edit_plan(data)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _parse_edit_plan(data: Dict[str, Any]) -> EditPlan:
|
|
159
|
+
"""Parse LLM response into EditPlan."""
|
|
160
|
+
segments_to_keep = []
|
|
161
|
+
segments_to_remove = []
|
|
162
|
+
chapters = []
|
|
163
|
+
|
|
164
|
+
for seg_data in data.get("segments_to_keep", []):
|
|
165
|
+
segments_to_keep.append(Segment(
|
|
166
|
+
start=float(seg_data.get("start", 0)),
|
|
167
|
+
end=float(seg_data.get("end", 0)),
|
|
168
|
+
category=SegmentCategory.KEEP,
|
|
169
|
+
reason=seg_data.get("reason", ""),
|
|
170
|
+
text=seg_data.get("text", "")
|
|
171
|
+
))
|
|
172
|
+
|
|
173
|
+
for seg_data in data.get("segments_to_remove", []):
|
|
174
|
+
category_str = seg_data.get("category", "filler").lower()
|
|
175
|
+
try:
|
|
176
|
+
category = SegmentCategory(category_str)
|
|
177
|
+
except ValueError:
|
|
178
|
+
category = SegmentCategory.FILLER
|
|
179
|
+
|
|
180
|
+
segments_to_remove.append(Segment(
|
|
181
|
+
start=float(seg_data.get("start", 0)),
|
|
182
|
+
end=float(seg_data.get("end", 0)),
|
|
183
|
+
category=category,
|
|
184
|
+
reason=seg_data.get("reason", ""),
|
|
185
|
+
text=seg_data.get("text", "")
|
|
186
|
+
))
|
|
187
|
+
|
|
188
|
+
for ch_data in data.get("chapters", []):
|
|
189
|
+
chapters.append(Chapter(
|
|
190
|
+
start=float(ch_data.get("start", 0)),
|
|
191
|
+
title=ch_data.get("title", ""),
|
|
192
|
+
description=ch_data.get("description", "")
|
|
193
|
+
))
|
|
194
|
+
|
|
195
|
+
return EditPlan(
|
|
196
|
+
segments_to_keep=segments_to_keep,
|
|
197
|
+
segments_to_remove=segments_to_remove,
|
|
198
|
+
chapters=chapters,
|
|
199
|
+
summary=data.get("summary", ""),
|
|
200
|
+
topics=data.get("topics", [])
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def create_simple_edit_plan(
|
|
205
|
+
transcript: TranscriptResult,
|
|
206
|
+
config: Dict[str, Any]
|
|
207
|
+
) -> EditPlan:
|
|
208
|
+
"""
|
|
209
|
+
Create edit plan using simple pattern matching (no LLM).
|
|
210
|
+
|
|
211
|
+
This is faster but less accurate than LLM-based analysis.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
transcript: Transcript with word timestamps
|
|
215
|
+
config: Edit configuration
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
EditPlan with segments to keep/remove
|
|
219
|
+
"""
|
|
220
|
+
segments_to_remove = []
|
|
221
|
+
|
|
222
|
+
if config.get("remove_fillers", True):
|
|
223
|
+
filler_words = config.get("filler_words", ["um", "uh"])
|
|
224
|
+
segments_to_remove.extend(detect_fillers(transcript, filler_words))
|
|
225
|
+
|
|
226
|
+
if config.get("remove_repetitions", True):
|
|
227
|
+
segments_to_remove.extend(detect_repetitions(transcript))
|
|
228
|
+
|
|
229
|
+
if config.get("remove_silence", True):
|
|
230
|
+
threshold = config.get("silence_threshold_ms", 700)
|
|
231
|
+
segments_to_remove.extend(detect_silence(transcript, threshold))
|
|
232
|
+
|
|
233
|
+
segments_to_remove.sort(key=lambda s: s.start)
|
|
234
|
+
|
|
235
|
+
segments_to_keep = _invert_segments(
|
|
236
|
+
segments_to_remove,
|
|
237
|
+
0.0,
|
|
238
|
+
transcript.duration
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
return EditPlan(
|
|
242
|
+
segments_to_keep=segments_to_keep,
|
|
243
|
+
segments_to_remove=segments_to_remove,
|
|
244
|
+
summary="",
|
|
245
|
+
topics=[]
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _invert_segments(
|
|
250
|
+
remove_segments: List[Segment],
|
|
251
|
+
start: float,
|
|
252
|
+
end: float
|
|
253
|
+
) -> List[Segment]:
|
|
254
|
+
"""Convert remove segments to keep segments."""
|
|
255
|
+
if not remove_segments:
|
|
256
|
+
return [Segment(start=start, end=end, category=SegmentCategory.KEEP)]
|
|
257
|
+
|
|
258
|
+
keep_segments = []
|
|
259
|
+
current = start
|
|
260
|
+
|
|
261
|
+
for seg in sorted(remove_segments, key=lambda s: s.start):
|
|
262
|
+
if seg.start > current:
|
|
263
|
+
keep_segments.append(Segment(
|
|
264
|
+
start=current,
|
|
265
|
+
end=seg.start,
|
|
266
|
+
category=SegmentCategory.KEEP
|
|
267
|
+
))
|
|
268
|
+
current = max(current, seg.end)
|
|
269
|
+
|
|
270
|
+
if current < end:
|
|
271
|
+
keep_segments.append(Segment(
|
|
272
|
+
start=current,
|
|
273
|
+
end=end,
|
|
274
|
+
category=SegmentCategory.KEEP
|
|
275
|
+
))
|
|
276
|
+
|
|
277
|
+
return keep_segments
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Data models for AI Video Editor.
|
|
3
|
+
|
|
4
|
+
Defines data structures for:
|
|
5
|
+
- Video metadata and probe results
|
|
6
|
+
- Transcripts with word-level timestamps
|
|
7
|
+
- Edit plans and segments
|
|
8
|
+
- Final edit results
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Dict, List, Optional, Union
|
|
15
|
+
import json
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SegmentCategory(str, Enum):
|
|
19
|
+
"""Category of segment to remove."""
|
|
20
|
+
FILLER = "filler"
|
|
21
|
+
TANGENT = "tangent"
|
|
22
|
+
REPEAT = "repeat"
|
|
23
|
+
NOISE = "noise"
|
|
24
|
+
SILENCE = "silence"
|
|
25
|
+
KEEP = "keep"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class Word:
|
|
30
|
+
"""A single word with timing information."""
|
|
31
|
+
text: str
|
|
32
|
+
start: float # seconds
|
|
33
|
+
end: float # seconds
|
|
34
|
+
confidence: float = 1.0
|
|
35
|
+
|
|
36
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
37
|
+
return {
|
|
38
|
+
"text": self.text,
|
|
39
|
+
"start": self.start,
|
|
40
|
+
"end": self.end,
|
|
41
|
+
"confidence": self.confidence
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def from_dict(cls, data: Dict[str, Any]) -> "Word":
|
|
46
|
+
return cls(
|
|
47
|
+
text=data["text"],
|
|
48
|
+
start=data["start"],
|
|
49
|
+
end=data["end"],
|
|
50
|
+
confidence=data.get("confidence", 1.0)
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class Segment:
|
|
56
|
+
"""A segment of video with timing and metadata."""
|
|
57
|
+
start: float # seconds
|
|
58
|
+
end: float # seconds
|
|
59
|
+
category: SegmentCategory = SegmentCategory.KEEP
|
|
60
|
+
reason: str = ""
|
|
61
|
+
confidence: float = 1.0
|
|
62
|
+
text: str = ""
|
|
63
|
+
words: List[Word] = field(default_factory=list)
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def duration(self) -> float:
|
|
67
|
+
return self.end - self.start
|
|
68
|
+
|
|
69
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
70
|
+
return {
|
|
71
|
+
"start": self.start,
|
|
72
|
+
"end": self.end,
|
|
73
|
+
"category": self.category.value,
|
|
74
|
+
"reason": self.reason,
|
|
75
|
+
"confidence": self.confidence,
|
|
76
|
+
"text": self.text,
|
|
77
|
+
"words": [w.to_dict() for w in self.words]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def from_dict(cls, data: Dict[str, Any]) -> "Segment":
|
|
82
|
+
return cls(
|
|
83
|
+
start=data["start"],
|
|
84
|
+
end=data["end"],
|
|
85
|
+
category=SegmentCategory(data.get("category", "keep")),
|
|
86
|
+
reason=data.get("reason", ""),
|
|
87
|
+
confidence=data.get("confidence", 1.0),
|
|
88
|
+
text=data.get("text", ""),
|
|
89
|
+
words=[Word.from_dict(w) for w in data.get("words", [])]
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass
|
|
94
|
+
class Chapter:
|
|
95
|
+
"""A chapter marker for the video."""
|
|
96
|
+
start: float # seconds
|
|
97
|
+
title: str
|
|
98
|
+
description: str = ""
|
|
99
|
+
|
|
100
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
101
|
+
return {
|
|
102
|
+
"start": self.start,
|
|
103
|
+
"title": self.title,
|
|
104
|
+
"description": self.description
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@classmethod
|
|
108
|
+
def from_dict(cls, data: Dict[str, Any]) -> "Chapter":
|
|
109
|
+
return cls(
|
|
110
|
+
start=data["start"],
|
|
111
|
+
title=data["title"],
|
|
112
|
+
description=data.get("description", "")
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass
|
|
117
|
+
class EditPlan:
|
|
118
|
+
"""Complete edit plan for a video."""
|
|
119
|
+
segments_to_keep: List[Segment] = field(default_factory=list)
|
|
120
|
+
segments_to_remove: List[Segment] = field(default_factory=list)
|
|
121
|
+
chapters: List[Chapter] = field(default_factory=list)
|
|
122
|
+
summary: str = ""
|
|
123
|
+
topics: List[str] = field(default_factory=list)
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def total_keep_duration(self) -> float:
|
|
127
|
+
return sum(s.duration for s in self.segments_to_keep)
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def total_remove_duration(self) -> float:
|
|
131
|
+
return sum(s.duration for s in self.segments_to_remove)
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def removal_stats(self) -> Dict[str, float]:
|
|
135
|
+
"""Get duration removed by category."""
|
|
136
|
+
stats: Dict[str, float] = {}
|
|
137
|
+
for seg in self.segments_to_remove:
|
|
138
|
+
cat = seg.category.value
|
|
139
|
+
stats[cat] = stats.get(cat, 0) + seg.duration
|
|
140
|
+
return stats
|
|
141
|
+
|
|
142
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
143
|
+
return {
|
|
144
|
+
"segments_to_keep": [s.to_dict() for s in self.segments_to_keep],
|
|
145
|
+
"segments_to_remove": [s.to_dict() for s in self.segments_to_remove],
|
|
146
|
+
"chapters": [c.to_dict() for c in self.chapters],
|
|
147
|
+
"summary": self.summary,
|
|
148
|
+
"topics": self.topics,
|
|
149
|
+
"stats": {
|
|
150
|
+
"total_keep_duration": self.total_keep_duration,
|
|
151
|
+
"total_remove_duration": self.total_remove_duration,
|
|
152
|
+
"removal_by_category": self.removal_stats
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
@classmethod
|
|
157
|
+
def from_dict(cls, data: Dict[str, Any]) -> "EditPlan":
|
|
158
|
+
return cls(
|
|
159
|
+
segments_to_keep=[Segment.from_dict(s) for s in data.get("segments_to_keep", [])],
|
|
160
|
+
segments_to_remove=[Segment.from_dict(s) for s in data.get("segments_to_remove", [])],
|
|
161
|
+
chapters=[Chapter.from_dict(c) for c in data.get("chapters", [])],
|
|
162
|
+
summary=data.get("summary", ""),
|
|
163
|
+
topics=data.get("topics", [])
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
def to_json(self, path: Optional[Union[str, Path]] = None) -> str:
|
|
167
|
+
"""Export plan to JSON."""
|
|
168
|
+
json_str = json.dumps(self.to_dict(), indent=2)
|
|
169
|
+
if path:
|
|
170
|
+
Path(path).write_text(json_str)
|
|
171
|
+
return json_str
|
|
172
|
+
|
|
173
|
+
@classmethod
|
|
174
|
+
def from_json(cls, json_str_or_path: Union[str, Path]) -> "EditPlan":
|
|
175
|
+
"""Load plan from JSON string or file."""
|
|
176
|
+
path = Path(json_str_or_path)
|
|
177
|
+
if path.exists():
|
|
178
|
+
data = json.loads(path.read_text())
|
|
179
|
+
else:
|
|
180
|
+
data = json.loads(str(json_str_or_path))
|
|
181
|
+
return cls.from_dict(data)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@dataclass
|
|
185
|
+
class VideoProbeResult:
|
|
186
|
+
"""Result of probing a video file."""
|
|
187
|
+
path: str
|
|
188
|
+
duration: float # seconds
|
|
189
|
+
width: int
|
|
190
|
+
height: int
|
|
191
|
+
fps: float
|
|
192
|
+
codec: str
|
|
193
|
+
audio_codec: str = ""
|
|
194
|
+
audio_channels: int = 0
|
|
195
|
+
audio_sample_rate: int = 0
|
|
196
|
+
bitrate: int = 0
|
|
197
|
+
file_size: int = 0
|
|
198
|
+
format_name: str = ""
|
|
199
|
+
|
|
200
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
201
|
+
return {
|
|
202
|
+
"path": self.path,
|
|
203
|
+
"duration": self.duration,
|
|
204
|
+
"width": self.width,
|
|
205
|
+
"height": self.height,
|
|
206
|
+
"fps": self.fps,
|
|
207
|
+
"codec": self.codec,
|
|
208
|
+
"audio_codec": self.audio_codec,
|
|
209
|
+
"audio_channels": self.audio_channels,
|
|
210
|
+
"audio_sample_rate": self.audio_sample_rate,
|
|
211
|
+
"bitrate": self.bitrate,
|
|
212
|
+
"file_size": self.file_size,
|
|
213
|
+
"format_name": self.format_name
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
@classmethod
|
|
217
|
+
def from_dict(cls, data: Dict[str, Any]) -> "VideoProbeResult":
|
|
218
|
+
return cls(**data)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@dataclass
|
|
222
|
+
class TranscriptResult:
|
|
223
|
+
"""Result of transcribing audio."""
|
|
224
|
+
text: str
|
|
225
|
+
words: List[Word] = field(default_factory=list)
|
|
226
|
+
language: str = "en"
|
|
227
|
+
duration: float = 0.0
|
|
228
|
+
provider: str = "openai"
|
|
229
|
+
|
|
230
|
+
def to_srt(self, path: Optional[Union[str, Path]] = None) -> str:
|
|
231
|
+
"""Export transcript to SRT format."""
|
|
232
|
+
lines = []
|
|
233
|
+
segments = []
|
|
234
|
+
current_segment: List[Word] = []
|
|
235
|
+
segment_start = 0.0
|
|
236
|
+
|
|
237
|
+
for word in self.words:
|
|
238
|
+
if not current_segment:
|
|
239
|
+
segment_start = word.start
|
|
240
|
+
current_segment.append(word)
|
|
241
|
+
elif len(current_segment) >= 10 or (word.end - segment_start) > 5.0:
|
|
242
|
+
segments.append((segment_start, current_segment[-1].end, current_segment))
|
|
243
|
+
current_segment = [word]
|
|
244
|
+
segment_start = word.start
|
|
245
|
+
else:
|
|
246
|
+
current_segment.append(word)
|
|
247
|
+
|
|
248
|
+
if current_segment:
|
|
249
|
+
segments.append((segment_start, current_segment[-1].end, current_segment))
|
|
250
|
+
|
|
251
|
+
for i, (start, end, words) in enumerate(segments, 1):
|
|
252
|
+
text = " ".join(w.text for w in words)
|
|
253
|
+
start_ts = _format_srt_time(start)
|
|
254
|
+
end_ts = _format_srt_time(end)
|
|
255
|
+
lines.append(f"{i}")
|
|
256
|
+
lines.append(f"{start_ts} --> {end_ts}")
|
|
257
|
+
lines.append(text)
|
|
258
|
+
lines.append("")
|
|
259
|
+
|
|
260
|
+
srt_content = "\n".join(lines)
|
|
261
|
+
if path:
|
|
262
|
+
Path(path).write_text(srt_content)
|
|
263
|
+
return srt_content
|
|
264
|
+
|
|
265
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
266
|
+
return {
|
|
267
|
+
"text": self.text,
|
|
268
|
+
"words": [w.to_dict() for w in self.words],
|
|
269
|
+
"language": self.language,
|
|
270
|
+
"duration": self.duration,
|
|
271
|
+
"provider": self.provider
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
@classmethod
|
|
275
|
+
def from_dict(cls, data: Dict[str, Any]) -> "TranscriptResult":
|
|
276
|
+
return cls(
|
|
277
|
+
text=data["text"],
|
|
278
|
+
words=[Word.from_dict(w) for w in data.get("words", [])],
|
|
279
|
+
language=data.get("language", "en"),
|
|
280
|
+
duration=data.get("duration", 0.0),
|
|
281
|
+
provider=data.get("provider", "openai")
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
@dataclass
|
|
286
|
+
class VideoEditResult:
|
|
287
|
+
"""Result of editing a video."""
|
|
288
|
+
output_path: str
|
|
289
|
+
report_path: str
|
|
290
|
+
transcript_path: str
|
|
291
|
+
srt_path: str
|
|
292
|
+
edl_path: str
|
|
293
|
+
|
|
294
|
+
original_duration: float
|
|
295
|
+
final_duration: float
|
|
296
|
+
time_saved: float
|
|
297
|
+
|
|
298
|
+
edit_plan: EditPlan
|
|
299
|
+
probe: VideoProbeResult
|
|
300
|
+
transcript: TranscriptResult
|
|
301
|
+
|
|
302
|
+
workdir: str = ""
|
|
303
|
+
config_snapshot: Dict[str, Any] = field(default_factory=dict)
|
|
304
|
+
|
|
305
|
+
@property
|
|
306
|
+
def compression_ratio(self) -> float:
|
|
307
|
+
if self.original_duration == 0:
|
|
308
|
+
return 0
|
|
309
|
+
return self.final_duration / self.original_duration
|
|
310
|
+
|
|
311
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
312
|
+
return {
|
|
313
|
+
"output_path": self.output_path,
|
|
314
|
+
"report_path": self.report_path,
|
|
315
|
+
"transcript_path": self.transcript_path,
|
|
316
|
+
"srt_path": self.srt_path,
|
|
317
|
+
"edl_path": self.edl_path,
|
|
318
|
+
"original_duration": self.original_duration,
|
|
319
|
+
"final_duration": self.final_duration,
|
|
320
|
+
"time_saved": self.time_saved,
|
|
321
|
+
"compression_ratio": self.compression_ratio,
|
|
322
|
+
"edit_plan": self.edit_plan.to_dict(),
|
|
323
|
+
"probe": self.probe.to_dict(),
|
|
324
|
+
"transcript": self.transcript.to_dict(),
|
|
325
|
+
"workdir": self.workdir,
|
|
326
|
+
"config_snapshot": self.config_snapshot
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
def to_json(self, path: Optional[Union[str, Path]] = None) -> str:
|
|
330
|
+
"""Export result to JSON."""
|
|
331
|
+
json_str = json.dumps(self.to_dict(), indent=2, default=str)
|
|
332
|
+
if path:
|
|
333
|
+
Path(path).write_text(json_str)
|
|
334
|
+
return json_str
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _format_srt_time(seconds: float) -> str:
|
|
338
|
+
"""Format seconds as SRT timestamp (HH:MM:SS,mmm)."""
|
|
339
|
+
hours = int(seconds // 3600)
|
|
340
|
+
minutes = int((seconds % 3600) // 60)
|
|
341
|
+
secs = int(seconds % 60)
|
|
342
|
+
millis = int((seconds % 1) * 1000)
|
|
343
|
+
return f"{hours:02d}:{minutes:02d}:{secs:02d},{millis:03d}"
|