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,222 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility functions for AI Video Editor.
|
|
3
|
+
|
|
4
|
+
Provides:
|
|
5
|
+
- FFmpeg detection and command building
|
|
6
|
+
- File hashing for reproducibility
|
|
7
|
+
- Working directory management
|
|
8
|
+
- Time formatting utilities
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
import os
|
|
13
|
+
import shutil
|
|
14
|
+
import subprocess
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
17
|
+
import json
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def check_ffmpeg() -> Tuple[bool, str]:
|
|
21
|
+
"""Check if ffmpeg is available."""
|
|
22
|
+
try:
|
|
23
|
+
result = subprocess.run(
|
|
24
|
+
["ffmpeg", "-version"],
|
|
25
|
+
capture_output=True,
|
|
26
|
+
text=True,
|
|
27
|
+
timeout=10
|
|
28
|
+
)
|
|
29
|
+
if result.returncode == 0:
|
|
30
|
+
version_line = result.stdout.split("\n")[0]
|
|
31
|
+
return True, version_line
|
|
32
|
+
return False, "ffmpeg returned non-zero exit code"
|
|
33
|
+
except FileNotFoundError:
|
|
34
|
+
return False, "ffmpeg not found. Install with: brew install ffmpeg (macOS) or apt install ffmpeg (Linux)"
|
|
35
|
+
except subprocess.TimeoutExpired:
|
|
36
|
+
return False, "ffmpeg check timed out"
|
|
37
|
+
except Exception as e:
|
|
38
|
+
return False, f"Error checking ffmpeg: {e}"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def check_ffprobe() -> Tuple[bool, str]:
|
|
42
|
+
"""Check if ffprobe is available."""
|
|
43
|
+
try:
|
|
44
|
+
result = subprocess.run(
|
|
45
|
+
["ffprobe", "-version"],
|
|
46
|
+
capture_output=True,
|
|
47
|
+
text=True,
|
|
48
|
+
timeout=10
|
|
49
|
+
)
|
|
50
|
+
if result.returncode == 0:
|
|
51
|
+
version_line = result.stdout.split("\n")[0]
|
|
52
|
+
return True, version_line
|
|
53
|
+
return False, "ffprobe returned non-zero exit code"
|
|
54
|
+
except FileNotFoundError:
|
|
55
|
+
return False, "ffprobe not found. Install ffmpeg which includes ffprobe."
|
|
56
|
+
except subprocess.TimeoutExpired:
|
|
57
|
+
return False, "ffprobe check timed out"
|
|
58
|
+
except Exception as e:
|
|
59
|
+
return False, f"Error checking ffprobe: {e}"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def ensure_ffmpeg():
|
|
63
|
+
"""Raise error if ffmpeg is not available."""
|
|
64
|
+
available, msg = check_ffmpeg()
|
|
65
|
+
if not available:
|
|
66
|
+
raise RuntimeError(f"FFmpeg is required but not available: {msg}")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def file_hash(path: str, algorithm: str = "sha256") -> str:
|
|
70
|
+
"""Compute hash of a file."""
|
|
71
|
+
h = hashlib.new(algorithm)
|
|
72
|
+
with open(path, "rb") as f:
|
|
73
|
+
for chunk in iter(lambda: f.read(8192), b""):
|
|
74
|
+
h.update(chunk)
|
|
75
|
+
return h.hexdigest()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def create_workdir(input_path: str, base_dir: Optional[str] = None) -> Path:
|
|
79
|
+
"""Create a working directory for video processing."""
|
|
80
|
+
if base_dir is None:
|
|
81
|
+
base_dir = os.path.join(os.getcwd(), ".praison", "video")
|
|
82
|
+
|
|
83
|
+
input_hash = file_hash(input_path)[:12]
|
|
84
|
+
input_name = Path(input_path).stem
|
|
85
|
+
workdir = Path(base_dir) / f"{input_name}_{input_hash}"
|
|
86
|
+
|
|
87
|
+
workdir.mkdir(parents=True, exist_ok=True)
|
|
88
|
+
(workdir / "audio").mkdir(exist_ok=True)
|
|
89
|
+
(workdir / "segments").mkdir(exist_ok=True)
|
|
90
|
+
(workdir / "output").mkdir(exist_ok=True)
|
|
91
|
+
|
|
92
|
+
return workdir
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def format_duration(seconds: float) -> str:
|
|
96
|
+
"""Format duration in human-readable format."""
|
|
97
|
+
hours = int(seconds // 3600)
|
|
98
|
+
minutes = int((seconds % 3600) // 60)
|
|
99
|
+
secs = int(seconds % 60)
|
|
100
|
+
|
|
101
|
+
parts = []
|
|
102
|
+
if hours > 0:
|
|
103
|
+
parts.append(f"{hours}h")
|
|
104
|
+
if minutes > 0 or hours > 0:
|
|
105
|
+
parts.append(f"{minutes}m")
|
|
106
|
+
parts.append(f"{secs}s")
|
|
107
|
+
|
|
108
|
+
return " ".join(parts)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def parse_duration(duration_str: str) -> float:
|
|
112
|
+
"""Parse duration string to seconds."""
|
|
113
|
+
import re
|
|
114
|
+
|
|
115
|
+
total = 0.0
|
|
116
|
+
|
|
117
|
+
h_match = re.search(r"(\d+)h", duration_str)
|
|
118
|
+
if h_match:
|
|
119
|
+
total += int(h_match.group(1)) * 3600
|
|
120
|
+
|
|
121
|
+
m_match = re.search(r"(\d+)m", duration_str)
|
|
122
|
+
if m_match:
|
|
123
|
+
total += int(m_match.group(1)) * 60
|
|
124
|
+
|
|
125
|
+
s_match = re.search(r"(\d+)s", duration_str)
|
|
126
|
+
if s_match:
|
|
127
|
+
total += int(s_match.group(1))
|
|
128
|
+
|
|
129
|
+
if total == 0:
|
|
130
|
+
try:
|
|
131
|
+
total = float(duration_str)
|
|
132
|
+
except ValueError:
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
return total
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def extract_audio(video_path: str, output_path: str, sample_rate: int = 16000, mono: bool = True) -> str:
|
|
139
|
+
"""Extract audio from video file."""
|
|
140
|
+
ensure_ffmpeg()
|
|
141
|
+
|
|
142
|
+
cmd = [
|
|
143
|
+
"ffmpeg", "-y",
|
|
144
|
+
"-i", video_path,
|
|
145
|
+
"-vn",
|
|
146
|
+
"-acodec", "pcm_s16le",
|
|
147
|
+
"-ar", str(sample_rate),
|
|
148
|
+
]
|
|
149
|
+
|
|
150
|
+
if mono:
|
|
151
|
+
cmd.extend(["-ac", "1"])
|
|
152
|
+
|
|
153
|
+
cmd.append(output_path)
|
|
154
|
+
|
|
155
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
156
|
+
if result.returncode != 0:
|
|
157
|
+
raise RuntimeError(f"Failed to extract audio: {result.stderr}")
|
|
158
|
+
|
|
159
|
+
return output_path
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def generate_edl(
|
|
163
|
+
segments_to_keep: List[Tuple[float, float]],
|
|
164
|
+
segments_to_remove: List[Tuple[float, float, str]],
|
|
165
|
+
output_path: str,
|
|
166
|
+
fps: float = 30.0
|
|
167
|
+
) -> str:
|
|
168
|
+
"""Generate Edit Decision List (EDL) file."""
|
|
169
|
+
lines = [
|
|
170
|
+
"TITLE: AI Video Editor EDL",
|
|
171
|
+
"FCM: NON-DROP FRAME",
|
|
172
|
+
""
|
|
173
|
+
]
|
|
174
|
+
|
|
175
|
+
for i, (start, end) in enumerate(segments_to_keep, 1):
|
|
176
|
+
start_tc = _seconds_to_timecode(start, fps)
|
|
177
|
+
end_tc = _seconds_to_timecode(end, fps)
|
|
178
|
+
lines.append(f"{i:03d} AX V C {start_tc} {end_tc} {start_tc} {end_tc}")
|
|
179
|
+
|
|
180
|
+
lines.append("")
|
|
181
|
+
lines.append("* REMOVED SEGMENTS:")
|
|
182
|
+
|
|
183
|
+
for start, end, reason in segments_to_remove:
|
|
184
|
+
start_tc = _seconds_to_timecode(start, fps)
|
|
185
|
+
end_tc = _seconds_to_timecode(end, fps)
|
|
186
|
+
lines.append(f"* {start_tc} - {end_tc}: {reason}")
|
|
187
|
+
|
|
188
|
+
content = "\n".join(lines)
|
|
189
|
+
Path(output_path).write_text(content)
|
|
190
|
+
return output_path
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _seconds_to_timecode(seconds: float, fps: float = 30.0) -> str:
|
|
194
|
+
"""Convert seconds to SMPTE timecode."""
|
|
195
|
+
frames = int((seconds % 1) * fps)
|
|
196
|
+
total_secs = int(seconds)
|
|
197
|
+
hours = total_secs // 3600
|
|
198
|
+
minutes = (total_secs % 3600) // 60
|
|
199
|
+
secs = total_secs % 60
|
|
200
|
+
return f"{hours:02d}:{minutes:02d}:{secs:02d}:{frames:02d}"
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def save_config_snapshot(config: Dict[str, Any], workdir: Path) -> str:
|
|
204
|
+
"""Save configuration snapshot for reproducibility."""
|
|
205
|
+
config_path = workdir / "config_snapshot.json"
|
|
206
|
+
with open(config_path, "w") as f:
|
|
207
|
+
json.dump(config, f, indent=2, default=str)
|
|
208
|
+
return str(config_path)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def cleanup_workdir(workdir: Path, keep_output: bool = True):
|
|
212
|
+
"""Clean up working directory."""
|
|
213
|
+
if not workdir.exists():
|
|
214
|
+
return
|
|
215
|
+
|
|
216
|
+
for item in workdir.iterdir():
|
|
217
|
+
if keep_output and item.name == "output":
|
|
218
|
+
continue
|
|
219
|
+
if item.is_dir():
|
|
220
|
+
shutil.rmtree(item)
|
|
221
|
+
else:
|
|
222
|
+
item.unlink()
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for heuristic-based content analysis.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
# Add src to path for imports
|
|
9
|
+
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TestFillerDetection:
|
|
15
|
+
"""Test filler word detection."""
|
|
16
|
+
|
|
17
|
+
def test_detect_fillers(self):
|
|
18
|
+
from ai_video_editor.models import TranscriptResult, Word
|
|
19
|
+
from ai_video_editor.heuristics import detect_fillers
|
|
20
|
+
|
|
21
|
+
transcript = TranscriptResult(
|
|
22
|
+
text="So um I think uh this is like really important",
|
|
23
|
+
words=[
|
|
24
|
+
Word(text="So", start=0.0, end=0.2),
|
|
25
|
+
Word(text="um", start=0.3, end=0.5),
|
|
26
|
+
Word(text="I", start=0.6, end=0.7),
|
|
27
|
+
Word(text="think", start=0.7, end=1.0),
|
|
28
|
+
Word(text="uh", start=1.1, end=1.3),
|
|
29
|
+
Word(text="this", start=1.4, end=1.6),
|
|
30
|
+
Word(text="is", start=1.6, end=1.8),
|
|
31
|
+
Word(text="like", start=1.9, end=2.1),
|
|
32
|
+
Word(text="really", start=2.2, end=2.5),
|
|
33
|
+
Word(text="important", start=2.5, end=3.0),
|
|
34
|
+
],
|
|
35
|
+
duration=3.0
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
filler_words = ["um", "uh", "like"]
|
|
39
|
+
fillers = detect_fillers(transcript, filler_words)
|
|
40
|
+
|
|
41
|
+
assert len(fillers) == 3
|
|
42
|
+
assert fillers[0].text == "um"
|
|
43
|
+
assert fillers[1].text == "uh"
|
|
44
|
+
assert fillers[2].text == "like"
|
|
45
|
+
|
|
46
|
+
def test_detect_fillers_case_insensitive(self):
|
|
47
|
+
from ai_video_editor.models import TranscriptResult, Word
|
|
48
|
+
from ai_video_editor.heuristics import detect_fillers
|
|
49
|
+
|
|
50
|
+
transcript = TranscriptResult(
|
|
51
|
+
text="UM this is UH important",
|
|
52
|
+
words=[
|
|
53
|
+
Word(text="UM", start=0.0, end=0.3),
|
|
54
|
+
Word(text="this", start=0.4, end=0.6),
|
|
55
|
+
Word(text="is", start=0.6, end=0.8),
|
|
56
|
+
Word(text="UH", start=0.9, end=1.1),
|
|
57
|
+
Word(text="important", start=1.2, end=1.6),
|
|
58
|
+
],
|
|
59
|
+
duration=1.6
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
fillers = detect_fillers(transcript, ["um", "uh"])
|
|
63
|
+
assert len(fillers) == 2
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class TestRepetitionDetection:
|
|
67
|
+
"""Test repetition detection."""
|
|
68
|
+
|
|
69
|
+
def test_detect_repetitions(self):
|
|
70
|
+
from ai_video_editor.models import TranscriptResult, Word
|
|
71
|
+
from ai_video_editor.heuristics import detect_repetitions
|
|
72
|
+
|
|
73
|
+
transcript = TranscriptResult(
|
|
74
|
+
text="I think I think this is good",
|
|
75
|
+
words=[
|
|
76
|
+
Word(text="I", start=0.0, end=0.1),
|
|
77
|
+
Word(text="think", start=0.1, end=0.4),
|
|
78
|
+
Word(text="I", start=0.5, end=0.6),
|
|
79
|
+
Word(text="think", start=0.6, end=0.9),
|
|
80
|
+
Word(text="this", start=1.0, end=1.2),
|
|
81
|
+
Word(text="is", start=1.2, end=1.4),
|
|
82
|
+
Word(text="good", start=1.4, end=1.7),
|
|
83
|
+
],
|
|
84
|
+
duration=1.7
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
repetitions = detect_repetitions(transcript, window_size=5, min_repeat_words=2)
|
|
88
|
+
assert len(repetitions) >= 1
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class TestSilenceDetection:
|
|
92
|
+
"""Test silence detection."""
|
|
93
|
+
|
|
94
|
+
def test_detect_silence(self):
|
|
95
|
+
from ai_video_editor.models import TranscriptResult, Word
|
|
96
|
+
from ai_video_editor.heuristics import detect_silence
|
|
97
|
+
|
|
98
|
+
transcript = TranscriptResult(
|
|
99
|
+
text="Hello world",
|
|
100
|
+
words=[
|
|
101
|
+
Word(text="Hello", start=0.0, end=0.5),
|
|
102
|
+
Word(text="world", start=2.0, end=2.5),
|
|
103
|
+
],
|
|
104
|
+
duration=2.5
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
silences = detect_silence(transcript, threshold_ms=1000)
|
|
108
|
+
assert len(silences) == 1
|
|
109
|
+
assert silences[0].start == 0.5
|
|
110
|
+
assert silences[0].end == 2.0
|
|
111
|
+
|
|
112
|
+
def test_no_silence_below_threshold(self):
|
|
113
|
+
from ai_video_editor.models import TranscriptResult, Word
|
|
114
|
+
from ai_video_editor.heuristics import detect_silence
|
|
115
|
+
|
|
116
|
+
transcript = TranscriptResult(
|
|
117
|
+
text="Hello world",
|
|
118
|
+
words=[
|
|
119
|
+
Word(text="Hello", start=0.0, end=0.5),
|
|
120
|
+
Word(text="world", start=0.8, end=1.3),
|
|
121
|
+
],
|
|
122
|
+
duration=1.3
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
silences = detect_silence(transcript, threshold_ms=700)
|
|
126
|
+
assert len(silences) == 0
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
if __name__ == "__main__":
|
|
130
|
+
pytest.main([__file__, "-v"])
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for AI Video Editor models.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
# Add src to path for imports
|
|
10
|
+
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestWord:
|
|
16
|
+
"""Test Word model."""
|
|
17
|
+
|
|
18
|
+
def test_word_serialization(self):
|
|
19
|
+
from ai_video_editor.models import Word
|
|
20
|
+
|
|
21
|
+
word = Word(text="hello", start=1.0, end=1.5, confidence=0.95)
|
|
22
|
+
data = word.to_dict()
|
|
23
|
+
|
|
24
|
+
assert data["text"] == "hello"
|
|
25
|
+
assert data["start"] == 1.0
|
|
26
|
+
assert data["end"] == 1.5
|
|
27
|
+
assert data["confidence"] == 0.95
|
|
28
|
+
|
|
29
|
+
word2 = Word.from_dict(data)
|
|
30
|
+
assert word2.text == word.text
|
|
31
|
+
assert word2.start == word.start
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class TestSegment:
|
|
35
|
+
"""Test Segment model."""
|
|
36
|
+
|
|
37
|
+
def test_segment_duration(self):
|
|
38
|
+
from ai_video_editor.models import Segment, SegmentCategory
|
|
39
|
+
|
|
40
|
+
seg = Segment(start=10.0, end=15.5, category=SegmentCategory.KEEP)
|
|
41
|
+
assert seg.duration == 5.5
|
|
42
|
+
|
|
43
|
+
def test_segment_serialization(self):
|
|
44
|
+
from ai_video_editor.models import Segment, SegmentCategory
|
|
45
|
+
|
|
46
|
+
seg = Segment(
|
|
47
|
+
start=0.0,
|
|
48
|
+
end=10.0,
|
|
49
|
+
category=SegmentCategory.FILLER,
|
|
50
|
+
reason="um",
|
|
51
|
+
text="um"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
data = seg.to_dict()
|
|
55
|
+
assert data["category"] == "filler"
|
|
56
|
+
|
|
57
|
+
seg2 = Segment.from_dict(data)
|
|
58
|
+
assert seg2.category == SegmentCategory.FILLER
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class TestEditPlan:
|
|
62
|
+
"""Test EditPlan model."""
|
|
63
|
+
|
|
64
|
+
def test_edit_plan_stats(self):
|
|
65
|
+
from ai_video_editor.models import EditPlan, Segment, SegmentCategory
|
|
66
|
+
|
|
67
|
+
plan = EditPlan(
|
|
68
|
+
segments_to_keep=[
|
|
69
|
+
Segment(start=0, end=10, category=SegmentCategory.KEEP),
|
|
70
|
+
Segment(start=15, end=25, category=SegmentCategory.KEEP),
|
|
71
|
+
],
|
|
72
|
+
segments_to_remove=[
|
|
73
|
+
Segment(start=10, end=12, category=SegmentCategory.FILLER),
|
|
74
|
+
Segment(start=12, end=15, category=SegmentCategory.SILENCE),
|
|
75
|
+
]
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
assert plan.total_keep_duration == 20.0
|
|
79
|
+
assert plan.total_remove_duration == 5.0
|
|
80
|
+
|
|
81
|
+
stats = plan.removal_stats
|
|
82
|
+
assert stats["filler"] == 2.0
|
|
83
|
+
assert stats["silence"] == 3.0
|
|
84
|
+
|
|
85
|
+
def test_edit_plan_json(self, tmp_path):
|
|
86
|
+
from ai_video_editor.models import EditPlan, Segment, SegmentCategory
|
|
87
|
+
|
|
88
|
+
plan = EditPlan(
|
|
89
|
+
segments_to_keep=[
|
|
90
|
+
Segment(start=0, end=10, category=SegmentCategory.KEEP, reason="intro"),
|
|
91
|
+
],
|
|
92
|
+
summary="Test video",
|
|
93
|
+
topics=["testing"]
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
json_path = tmp_path / "plan.json"
|
|
97
|
+
plan.to_json(json_path)
|
|
98
|
+
|
|
99
|
+
plan2 = EditPlan.from_json(json_path)
|
|
100
|
+
assert plan2.summary == "Test video"
|
|
101
|
+
assert len(plan2.segments_to_keep) == 1
|
|
102
|
+
assert plan2.topics == ["testing"]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class TestTranscriptResult:
|
|
106
|
+
"""Test TranscriptResult model."""
|
|
107
|
+
|
|
108
|
+
def test_transcript_to_srt(self, tmp_path):
|
|
109
|
+
from ai_video_editor.models import TranscriptResult, Word
|
|
110
|
+
|
|
111
|
+
transcript = TranscriptResult(
|
|
112
|
+
text="Hello world this is a test",
|
|
113
|
+
words=[
|
|
114
|
+
Word(text="Hello", start=0.0, end=0.5),
|
|
115
|
+
Word(text="world", start=0.5, end=1.0),
|
|
116
|
+
Word(text="this", start=1.0, end=1.3),
|
|
117
|
+
Word(text="is", start=1.3, end=1.5),
|
|
118
|
+
Word(text="a", start=1.5, end=1.6),
|
|
119
|
+
Word(text="test", start=1.6, end=2.0),
|
|
120
|
+
],
|
|
121
|
+
duration=2.0
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
srt_path = tmp_path / "captions.srt"
|
|
125
|
+
srt_content = transcript.to_srt(srt_path)
|
|
126
|
+
|
|
127
|
+
assert srt_path.exists()
|
|
128
|
+
assert "Hello world this is a test" in srt_content
|
|
129
|
+
assert "-->" in srt_content
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class TestPresets:
|
|
133
|
+
"""Test preset configurations."""
|
|
134
|
+
|
|
135
|
+
def test_presets_exist(self):
|
|
136
|
+
from ai_video_editor.config import PRESETS
|
|
137
|
+
|
|
138
|
+
assert "podcast" in PRESETS
|
|
139
|
+
assert "meeting" in PRESETS
|
|
140
|
+
assert "course" in PRESETS
|
|
141
|
+
assert "clean" in PRESETS
|
|
142
|
+
|
|
143
|
+
def test_podcast_preset(self):
|
|
144
|
+
from ai_video_editor.config import PRESETS
|
|
145
|
+
|
|
146
|
+
podcast = PRESETS["podcast"]
|
|
147
|
+
assert podcast["remove_fillers"] is True
|
|
148
|
+
assert "um" in podcast["filler_words"]
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
if __name__ == "__main__":
|
|
152
|
+
pytest.main([__file__, "-v"])
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for timeline optimization.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
# Add src to path for imports
|
|
9
|
+
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TestTimelineOptimization:
|
|
15
|
+
"""Test timeline optimization functions."""
|
|
16
|
+
|
|
17
|
+
def test_merge_overlapping_segments(self):
|
|
18
|
+
from ai_video_editor.models import Segment, SegmentCategory
|
|
19
|
+
from ai_video_editor.timeline import _merge_overlapping
|
|
20
|
+
|
|
21
|
+
segments = [
|
|
22
|
+
Segment(start=0, end=10, category=SegmentCategory.KEEP),
|
|
23
|
+
Segment(start=8, end=15, category=SegmentCategory.KEEP),
|
|
24
|
+
Segment(start=20, end=25, category=SegmentCategory.KEEP),
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
merged = _merge_overlapping(segments)
|
|
28
|
+
|
|
29
|
+
assert len(merged) == 2
|
|
30
|
+
assert merged[0].start == 0
|
|
31
|
+
assert merged[0].end == 15
|
|
32
|
+
assert merged[1].start == 20
|
|
33
|
+
|
|
34
|
+
def test_get_keep_intervals(self):
|
|
35
|
+
from ai_video_editor.models import EditPlan, Segment, SegmentCategory
|
|
36
|
+
from ai_video_editor.timeline import get_keep_intervals
|
|
37
|
+
|
|
38
|
+
plan = EditPlan(
|
|
39
|
+
segments_to_keep=[
|
|
40
|
+
Segment(start=10, end=20, category=SegmentCategory.KEEP),
|
|
41
|
+
Segment(start=0, end=5, category=SegmentCategory.KEEP),
|
|
42
|
+
]
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
intervals = get_keep_intervals(plan)
|
|
46
|
+
|
|
47
|
+
assert intervals[0] == (0, 5)
|
|
48
|
+
assert intervals[1] == (10, 20)
|
|
49
|
+
|
|
50
|
+
def test_calculate_final_duration(self):
|
|
51
|
+
from ai_video_editor.models import EditPlan, Segment, SegmentCategory
|
|
52
|
+
from ai_video_editor.timeline import calculate_final_duration
|
|
53
|
+
|
|
54
|
+
plan = EditPlan(
|
|
55
|
+
segments_to_keep=[
|
|
56
|
+
Segment(start=0, end=10, category=SegmentCategory.KEEP),
|
|
57
|
+
Segment(start=20, end=35, category=SegmentCategory.KEEP),
|
|
58
|
+
]
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
duration = calculate_final_duration(plan)
|
|
62
|
+
assert duration == 25.0
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class TestUtils:
|
|
66
|
+
"""Test utility functions."""
|
|
67
|
+
|
|
68
|
+
def test_format_duration(self):
|
|
69
|
+
from ai_video_editor.utils import format_duration
|
|
70
|
+
|
|
71
|
+
assert format_duration(30) == "30s"
|
|
72
|
+
assert format_duration(90) == "1m 30s"
|
|
73
|
+
assert format_duration(3661) == "1h 1m 1s"
|
|
74
|
+
|
|
75
|
+
def test_parse_duration(self):
|
|
76
|
+
from ai_video_editor.utils import parse_duration
|
|
77
|
+
|
|
78
|
+
assert parse_duration("30s") == 30.0
|
|
79
|
+
assert parse_duration("5m") == 300.0
|
|
80
|
+
assert parse_duration("1h30m") == 5400.0
|
|
81
|
+
assert parse_duration("1h30m45s") == 5445.0
|
|
82
|
+
assert parse_duration("90") == 90.0
|
|
83
|
+
|
|
84
|
+
def test_file_hash(self, tmp_path):
|
|
85
|
+
from ai_video_editor.utils import file_hash
|
|
86
|
+
|
|
87
|
+
test_file = tmp_path / "test.txt"
|
|
88
|
+
test_file.write_text("hello world")
|
|
89
|
+
|
|
90
|
+
hash1 = file_hash(str(test_file))
|
|
91
|
+
hash2 = file_hash(str(test_file))
|
|
92
|
+
|
|
93
|
+
assert hash1 == hash2
|
|
94
|
+
assert len(hash1) == 64
|
|
95
|
+
|
|
96
|
+
def test_check_ffmpeg(self):
|
|
97
|
+
from ai_video_editor.utils import check_ffmpeg
|
|
98
|
+
|
|
99
|
+
available, msg = check_ffmpeg()
|
|
100
|
+
assert isinstance(available, bool)
|
|
101
|
+
assert isinstance(msg, str)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
if __name__ == "__main__":
|
|
105
|
+
pytest.main([__file__, "-v"])
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
name: AI Video Editor Workflow
|
|
2
|
+
description: |
|
|
3
|
+
End-to-end AI video editing pipeline.
|
|
4
|
+
Uses praisonai_tools.video CLI for video processing.
|
|
5
|
+
|
|
6
|
+
Tier 1: Shell orchestration via shell_tool
|
|
7
|
+
Tier 2/3: Native tools via praisonai_tools.video module
|
|
8
|
+
|
|
9
|
+
process: sequential
|
|
10
|
+
|
|
11
|
+
variables:
|
|
12
|
+
input: ""
|
|
13
|
+
output: ""
|
|
14
|
+
preset: "podcast"
|
|
15
|
+
verbose: "true"
|
|
16
|
+
|
|
17
|
+
agents:
|
|
18
|
+
video_editor:
|
|
19
|
+
name: VideoEditor
|
|
20
|
+
role: Video Editor
|
|
21
|
+
goal: Edit videos using AI analysis
|
|
22
|
+
instructions: |
|
|
23
|
+
You are an AI video editor. Use the praisonai_tools.video CLI to process videos.
|
|
24
|
+
|
|
25
|
+
Available commands:
|
|
26
|
+
- python -m praisonai_tools.video edit <input> --output <output> --preset <preset> -v
|
|
27
|
+
- python -m praisonai_tools.video probe <input>
|
|
28
|
+
- python -m praisonai_tools.video transcribe <input> --output transcript.srt
|
|
29
|
+
|
|
30
|
+
Presets: podcast, meeting, course, clean
|
|
31
|
+
tools: [shell_tool]
|
|
32
|
+
|
|
33
|
+
tasks:
|
|
34
|
+
- name: probe_video
|
|
35
|
+
agent: video_editor
|
|
36
|
+
description: |
|
|
37
|
+
Probe the input video to get metadata.
|
|
38
|
+
Run: python -m praisonai_tools.video probe "{input}"
|
|
39
|
+
expected_output: Video metadata (duration, resolution, fps, codec)
|
|
40
|
+
|
|
41
|
+
- name: edit_video
|
|
42
|
+
agent: video_editor
|
|
43
|
+
description: |
|
|
44
|
+
Edit the video using AI analysis.
|
|
45
|
+
Run: python -m praisonai_tools.video edit "{input}" --preset {preset} --output "{output}" -v
|
|
46
|
+
expected_output: |
|
|
47
|
+
Edited video with:
|
|
48
|
+
- Filler words removed
|
|
49
|
+
- Repetitions removed
|
|
50
|
+
- Silences trimmed
|
|
51
|
+
- Transcript and captions generated
|