figrecipe 0.7.4__py3-none-any.whl → 0.9.0__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.
- figrecipe/__init__.py +74 -76
- figrecipe/__main__.py +12 -0
- figrecipe/_api/_panel.py +67 -0
- figrecipe/_api/_save.py +100 -4
- figrecipe/_cli/__init__.py +7 -0
- figrecipe/_cli/_compose.py +87 -0
- figrecipe/_cli/_convert.py +117 -0
- figrecipe/_cli/_crop.py +82 -0
- figrecipe/_cli/_edit.py +70 -0
- figrecipe/_cli/_extract.py +128 -0
- figrecipe/_cli/_fonts.py +47 -0
- figrecipe/_cli/_info.py +67 -0
- figrecipe/_cli/_main.py +58 -0
- figrecipe/_cli/_reproduce.py +79 -0
- figrecipe/_cli/_style.py +77 -0
- figrecipe/_cli/_validate.py +66 -0
- figrecipe/_cli/_version.py +50 -0
- figrecipe/_composition/__init__.py +32 -0
- figrecipe/_composition/_alignment.py +452 -0
- figrecipe/_composition/_compose.py +179 -0
- figrecipe/_composition/_import_axes.py +127 -0
- figrecipe/_composition/_visibility.py +125 -0
- figrecipe/_dev/__init__.py +2 -0
- figrecipe/_dev/browser/__init__.py +69 -0
- figrecipe/_dev/browser/_audio.py +240 -0
- figrecipe/_dev/browser/_caption.py +356 -0
- figrecipe/_dev/browser/_click_effect.py +146 -0
- figrecipe/_dev/browser/_cursor.py +196 -0
- figrecipe/_dev/browser/_highlight.py +105 -0
- figrecipe/_dev/browser/_narration.py +237 -0
- figrecipe/_dev/browser/_recorder.py +446 -0
- figrecipe/_dev/browser/_utils.py +178 -0
- figrecipe/_dev/browser/_video_trim/__init__.py +152 -0
- figrecipe/_dev/browser/_video_trim/_detection.py +223 -0
- figrecipe/_dev/browser/_video_trim/_markers.py +140 -0
- figrecipe/_editor/__init__.py +36 -36
- figrecipe/_editor/_bbox/_extract.py +155 -9
- figrecipe/_editor/_bbox/_extract_text.py +124 -0
- figrecipe/_editor/_call_overrides.py +183 -0
- figrecipe/_editor/_datatable_plot_handlers.py +249 -0
- figrecipe/_editor/_figure_layout.py +211 -0
- figrecipe/_editor/_flask_app.py +157 -16
- figrecipe/_editor/_helpers.py +17 -8
- figrecipe/_editor/_hitmap/_detect.py +89 -32
- figrecipe/_editor/_hitmap_main.py +4 -4
- figrecipe/_editor/_overrides.py +4 -1
- figrecipe/_editor/_plot_types_registry.py +190 -0
- figrecipe/_editor/_render_overrides.py +38 -11
- figrecipe/_editor/_renderer.py +46 -1
- figrecipe/_editor/_routes_annotation.py +114 -0
- figrecipe/_editor/_routes_axis.py +35 -6
- figrecipe/_editor/_routes_captions.py +130 -0
- figrecipe/_editor/_routes_composition.py +270 -0
- figrecipe/_editor/_routes_core.py +15 -173
- figrecipe/_editor/_routes_datatable.py +364 -0
- figrecipe/_editor/_routes_element.py +37 -19
- figrecipe/_editor/_routes_files.py +443 -0
- figrecipe/_editor/_routes_image.py +200 -0
- figrecipe/_editor/_routes_snapshot.py +94 -0
- figrecipe/_editor/_routes_style.py +28 -8
- figrecipe/_editor/_templates/__init__.py +40 -2
- figrecipe/_editor/_templates/_html.py +97 -103
- figrecipe/_editor/_templates/_html_components/__init__.py +13 -0
- figrecipe/_editor/_templates/_html_components/_composition_toolbar.py +79 -0
- figrecipe/_editor/_templates/_html_components/_file_browser.py +41 -0
- figrecipe/_editor/_templates/_html_datatable.py +92 -0
- figrecipe/_editor/_templates/_scripts/__init__.py +58 -0
- figrecipe/_editor/_templates/_scripts/_accordion.py +328 -0
- figrecipe/_editor/_templates/_scripts/_annotation_drag.py +504 -0
- figrecipe/_editor/_templates/_scripts/_api.py +1 -1
- figrecipe/_editor/_templates/_scripts/_canvas_context_menu.py +182 -0
- figrecipe/_editor/_templates/_scripts/_captions.py +231 -0
- figrecipe/_editor/_templates/_scripts/_composition.py +283 -0
- figrecipe/_editor/_templates/_scripts/_core.py +94 -37
- figrecipe/_editor/_templates/_scripts/_datatable/__init__.py +59 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_cell_edit.py +97 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_clipboard.py +164 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_context_menu.py +221 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_core.py +150 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_editable.py +511 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_import.py +161 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_plot.py +261 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_selection.py +438 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_table.py +256 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_tabs.py +354 -0
- figrecipe/_editor/_templates/_scripts/_element_editor.py +17 -2
- figrecipe/_editor/_templates/_scripts/_files.py +274 -40
- figrecipe/_editor/_templates/_scripts/_files_context_menu.py +240 -0
- figrecipe/_editor/_templates/_scripts/_hitmap.py +87 -84
- figrecipe/_editor/_templates/_scripts/_image_drop.py +428 -0
- figrecipe/_editor/_templates/_scripts/_legend_drag.py +5 -0
- figrecipe/_editor/_templates/_scripts/_multi_select.py +198 -0
- figrecipe/_editor/_templates/_scripts/_panel_drag.py +219 -48
- figrecipe/_editor/_templates/_scripts/_panel_drag_snapshot.py +33 -0
- figrecipe/_editor/_templates/_scripts/_panel_position.py +238 -54
- figrecipe/_editor/_templates/_scripts/_panel_resize.py +230 -0
- figrecipe/_editor/_templates/_scripts/_panel_snap.py +307 -0
- figrecipe/_editor/_templates/_scripts/_region_select.py +255 -0
- figrecipe/_editor/_templates/_scripts/_selection.py +8 -1
- figrecipe/_editor/_templates/_scripts/_sync.py +242 -0
- figrecipe/_editor/_templates/_scripts/_undo_redo.py +348 -0
- figrecipe/_editor/_templates/_scripts/_zoom.py +52 -19
- figrecipe/_editor/_templates/_styles/__init__.py +9 -0
- figrecipe/_editor/_templates/_styles/_base.py +47 -0
- figrecipe/_editor/_templates/_styles/_buttons.py +127 -6
- figrecipe/_editor/_templates/_styles/_composition.py +87 -0
- figrecipe/_editor/_templates/_styles/_controls.py +168 -3
- figrecipe/_editor/_templates/_styles/_datatable/__init__.py +40 -0
- figrecipe/_editor/_templates/_styles/_datatable/_editable.py +203 -0
- figrecipe/_editor/_templates/_styles/_datatable/_panel.py +268 -0
- figrecipe/_editor/_templates/_styles/_datatable/_table.py +479 -0
- figrecipe/_editor/_templates/_styles/_datatable/_toolbar.py +384 -0
- figrecipe/_editor/_templates/_styles/_datatable/_vars.py +123 -0
- figrecipe/_editor/_templates/_styles/_dynamic_props.py +5 -5
- figrecipe/_editor/_templates/_styles/_file_browser.py +466 -0
- figrecipe/_editor/_templates/_styles/_forms.py +98 -0
- figrecipe/_editor/_templates/_styles/_hitmap.py +7 -0
- figrecipe/_editor/_templates/_styles/_modals.py +29 -0
- figrecipe/_editor/_templates/_styles/_overlays.py +5 -5
- figrecipe/_editor/_templates/_styles/_preview.py +213 -8
- figrecipe/_editor/_templates/_styles/_spinner.py +117 -0
- figrecipe/_editor/static/audio/click.mp3 +0 -0
- figrecipe/_editor/static/click.mp3 +0 -0
- figrecipe/_editor/static/icons/favicon.ico +0 -0
- figrecipe/_integrations/__init__.py +17 -0
- figrecipe/_integrations/_scitex_stats.py +298 -0
- figrecipe/_params/_DECORATION_METHODS.py +2 -0
- figrecipe/_recorder.py +28 -3
- figrecipe/_reproducer/_core.py +60 -49
- figrecipe/_utils/__init__.py +3 -0
- figrecipe/_utils/_bundle.py +205 -0
- figrecipe/_wrappers/_axes.py +150 -2
- figrecipe/_wrappers/_caption_generator.py +218 -0
- figrecipe/_wrappers/_figure.py +26 -1
- figrecipe/_wrappers/_stat_annotation.py +274 -0
- figrecipe/styles/_style_applier.py +10 -2
- figrecipe/styles/presets/SCITEX.yaml +11 -4
- {figrecipe-0.7.4.dist-info → figrecipe-0.9.0.dist-info}/METADATA +144 -146
- figrecipe-0.9.0.dist-info/RECORD +277 -0
- figrecipe-0.9.0.dist-info/entry_points.txt +2 -0
- figrecipe-0.7.4.dist-info/RECORD +0 -188
- {figrecipe-0.7.4.dist-info → figrecipe-0.9.0.dist-info}/WHEEL +0 -0
- {figrecipe-0.7.4.dist-info → figrecipe-0.9.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Element highlighting for demo recordings.
|
|
4
|
+
|
|
5
|
+
Highlights elements with colored overlays to draw attention
|
|
6
|
+
during demo recordings. Based on scitex browser debugging patterns.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
# JavaScript for highlighting element
|
|
10
|
+
HIGHLIGHT_JS = """
|
|
11
|
+
async (args) => {
|
|
12
|
+
const { selector, duration = 1000, color = '#FF4444' } = args;
|
|
13
|
+
|
|
14
|
+
// Find element
|
|
15
|
+
const element = document.querySelector(selector);
|
|
16
|
+
if (!element) {
|
|
17
|
+
console.warn('Element not found:', selector);
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Scroll into view
|
|
22
|
+
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
23
|
+
|
|
24
|
+
// Wait for scroll
|
|
25
|
+
await new Promise(r => setTimeout(r, 300));
|
|
26
|
+
|
|
27
|
+
// Get position after scroll
|
|
28
|
+
const rect = element.getBoundingClientRect();
|
|
29
|
+
|
|
30
|
+
// Create highlight overlay
|
|
31
|
+
const overlay = document.createElement('div');
|
|
32
|
+
overlay.className = 'demo-highlight';
|
|
33
|
+
overlay.style.cssText = `
|
|
34
|
+
position: fixed;
|
|
35
|
+
top: ${rect.top - 4}px;
|
|
36
|
+
left: ${rect.left - 4}px;
|
|
37
|
+
width: ${rect.width + 8}px;
|
|
38
|
+
height: ${rect.height + 8}px;
|
|
39
|
+
border: 4px solid ${color};
|
|
40
|
+
border-radius: 4px;
|
|
41
|
+
background: ${color}20;
|
|
42
|
+
pointer-events: none;
|
|
43
|
+
z-index: 2147483644;
|
|
44
|
+
box-shadow: 0 0 20px ${color}80;
|
|
45
|
+
animation: demo-highlight-pulse 0.5s ease-in-out infinite alternate;
|
|
46
|
+
`;
|
|
47
|
+
|
|
48
|
+
// Add animation style if not exists
|
|
49
|
+
if (!document.getElementById('demo-highlight-style')) {
|
|
50
|
+
const style = document.createElement('style');
|
|
51
|
+
style.id = 'demo-highlight-style';
|
|
52
|
+
style.textContent = `
|
|
53
|
+
@keyframes demo-highlight-pulse {
|
|
54
|
+
0% { box-shadow: 0 0 10px ${color}40; }
|
|
55
|
+
100% { box-shadow: 0 0 25px ${color}80; }
|
|
56
|
+
}
|
|
57
|
+
`;
|
|
58
|
+
document.head.appendChild(style);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
document.body.appendChild(overlay);
|
|
62
|
+
|
|
63
|
+
// Remove after duration
|
|
64
|
+
setTimeout(() => {
|
|
65
|
+
overlay.style.transition = 'opacity 0.3s';
|
|
66
|
+
overlay.style.opacity = '0';
|
|
67
|
+
setTimeout(() => overlay.remove(), 300);
|
|
68
|
+
}, duration);
|
|
69
|
+
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
async def highlight_element(
|
|
76
|
+
page,
|
|
77
|
+
selector: str,
|
|
78
|
+
duration_ms: int = 1000,
|
|
79
|
+
color: str = "#FF4444",
|
|
80
|
+
) -> bool:
|
|
81
|
+
"""Highlight an element with colored overlay.
|
|
82
|
+
|
|
83
|
+
Parameters
|
|
84
|
+
----------
|
|
85
|
+
page : playwright.async_api.Page
|
|
86
|
+
Playwright page object.
|
|
87
|
+
selector : str
|
|
88
|
+
CSS selector for element to highlight.
|
|
89
|
+
duration_ms : int, optional
|
|
90
|
+
Duration to show highlight in milliseconds (default: 1000).
|
|
91
|
+
color : str, optional
|
|
92
|
+
Highlight color in hex format (default: "#FF4444").
|
|
93
|
+
|
|
94
|
+
Returns
|
|
95
|
+
-------
|
|
96
|
+
bool
|
|
97
|
+
True if element was found and highlighted.
|
|
98
|
+
"""
|
|
99
|
+
args = {"selector": selector, "duration": duration_ms, "color": color}
|
|
100
|
+
return await page.evaluate(HIGHLIGHT_JS, args)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
__all__ = ["highlight_element"]
|
|
104
|
+
|
|
105
|
+
# EOF
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Narration processing for demo videos.
|
|
4
|
+
|
|
5
|
+
Provides utilities to extract captions from demo scripts,
|
|
6
|
+
estimate timing, and add TTS narration with BGM.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
import subprocess
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Dict, List, Tuple
|
|
13
|
+
|
|
14
|
+
from ._audio import generate_tts_segments, mix_narration_with_bgm
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def extract_captions_from_script(script_path: Path) -> List[str]:
|
|
18
|
+
"""Extract caption texts from a demo script.
|
|
19
|
+
|
|
20
|
+
Parameters
|
|
21
|
+
----------
|
|
22
|
+
script_path : Path
|
|
23
|
+
Path to demo Python script.
|
|
24
|
+
|
|
25
|
+
Returns
|
|
26
|
+
-------
|
|
27
|
+
List[str]
|
|
28
|
+
List of caption texts in order.
|
|
29
|
+
"""
|
|
30
|
+
content = script_path.read_text()
|
|
31
|
+
pattern = r'await\s+self\.caption\s*\(\s*["\']([^"\']+)["\']'
|
|
32
|
+
return re.findall(pattern, content)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_video_duration(video_path: Path) -> float:
|
|
36
|
+
"""Get video duration in seconds.
|
|
37
|
+
|
|
38
|
+
Parameters
|
|
39
|
+
----------
|
|
40
|
+
video_path : Path
|
|
41
|
+
Path to video file.
|
|
42
|
+
|
|
43
|
+
Returns
|
|
44
|
+
-------
|
|
45
|
+
float
|
|
46
|
+
Duration in seconds.
|
|
47
|
+
"""
|
|
48
|
+
result = subprocess.run(
|
|
49
|
+
[
|
|
50
|
+
"ffprobe",
|
|
51
|
+
"-v",
|
|
52
|
+
"error",
|
|
53
|
+
"-show_entries",
|
|
54
|
+
"format=duration",
|
|
55
|
+
"-of",
|
|
56
|
+
"default=noprint_wrappers=1:nokey=1",
|
|
57
|
+
str(video_path),
|
|
58
|
+
],
|
|
59
|
+
capture_output=True,
|
|
60
|
+
text=True,
|
|
61
|
+
)
|
|
62
|
+
return float(result.stdout.strip())
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def estimate_caption_times(
|
|
66
|
+
captions: List[str],
|
|
67
|
+
video_duration: float,
|
|
68
|
+
title_duration: float = 2.5,
|
|
69
|
+
closing_duration: float = 2.5,
|
|
70
|
+
) -> List[float]:
|
|
71
|
+
"""Estimate caption start times based on video duration.
|
|
72
|
+
|
|
73
|
+
Assumes captions are evenly distributed in the content portion
|
|
74
|
+
(between title and closing screens).
|
|
75
|
+
|
|
76
|
+
Parameters
|
|
77
|
+
----------
|
|
78
|
+
captions : List[str]
|
|
79
|
+
List of caption texts.
|
|
80
|
+
video_duration : float
|
|
81
|
+
Total video duration.
|
|
82
|
+
title_duration : float
|
|
83
|
+
Title screen duration at start.
|
|
84
|
+
closing_duration : float
|
|
85
|
+
Closing screen duration at end.
|
|
86
|
+
|
|
87
|
+
Returns
|
|
88
|
+
-------
|
|
89
|
+
List[float]
|
|
90
|
+
Estimated start times for each caption.
|
|
91
|
+
"""
|
|
92
|
+
content_duration = video_duration - title_duration - closing_duration
|
|
93
|
+
if len(captions) == 0:
|
|
94
|
+
return []
|
|
95
|
+
if len(captions) == 1:
|
|
96
|
+
return [title_duration + content_duration / 2]
|
|
97
|
+
interval = content_duration / (len(captions) + 1)
|
|
98
|
+
return [title_duration + interval * (i + 1) for i in range(len(captions))]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def add_narration_to_video(
|
|
102
|
+
video_path: Path,
|
|
103
|
+
captions: List[str],
|
|
104
|
+
output_path: Path,
|
|
105
|
+
bgm_path: Path,
|
|
106
|
+
tts_cache_dir: Path,
|
|
107
|
+
title_text: str = "",
|
|
108
|
+
bgm_volume: float = 0.08,
|
|
109
|
+
narration_delay: float = 0.2,
|
|
110
|
+
fade_in_duration: float = 0.5,
|
|
111
|
+
fade_out_duration: float = 2.0,
|
|
112
|
+
verbose: bool = True,
|
|
113
|
+
) -> Tuple[bool, Dict]:
|
|
114
|
+
"""Add TTS narration and BGM to a video.
|
|
115
|
+
|
|
116
|
+
Parameters
|
|
117
|
+
----------
|
|
118
|
+
video_path : Path
|
|
119
|
+
Input video file.
|
|
120
|
+
captions : List[str]
|
|
121
|
+
List of caption texts.
|
|
122
|
+
output_path : Path
|
|
123
|
+
Output video file path.
|
|
124
|
+
bgm_path : Path
|
|
125
|
+
Path to BGM audio file.
|
|
126
|
+
tts_cache_dir : Path
|
|
127
|
+
Directory for TTS cache.
|
|
128
|
+
title_text : str, optional
|
|
129
|
+
Title narration text (spoken at start).
|
|
130
|
+
bgm_volume : float, optional
|
|
131
|
+
BGM volume level (0.0-1.0).
|
|
132
|
+
narration_delay : float, optional
|
|
133
|
+
Delay before each narration.
|
|
134
|
+
fade_in_duration : float, optional
|
|
135
|
+
BGM fade-in duration.
|
|
136
|
+
fade_out_duration : float, optional
|
|
137
|
+
BGM fade-out duration.
|
|
138
|
+
verbose : bool, optional
|
|
139
|
+
Print progress messages.
|
|
140
|
+
|
|
141
|
+
Returns
|
|
142
|
+
-------
|
|
143
|
+
Tuple[bool, Dict]
|
|
144
|
+
(success, info_dict)
|
|
145
|
+
"""
|
|
146
|
+
try:
|
|
147
|
+
duration = get_video_duration(video_path)
|
|
148
|
+
if verbose:
|
|
149
|
+
print(f" Video duration: {duration:.2f}s")
|
|
150
|
+
|
|
151
|
+
# Build narrations list
|
|
152
|
+
narrations = []
|
|
153
|
+
if title_text:
|
|
154
|
+
narrations.append(("title", title_text))
|
|
155
|
+
for i, caption in enumerate(captions):
|
|
156
|
+
narrations.append((f"caption_{i}", caption))
|
|
157
|
+
|
|
158
|
+
if verbose:
|
|
159
|
+
print(f" {len(narrations)} narration segments")
|
|
160
|
+
|
|
161
|
+
# Estimate timing
|
|
162
|
+
caption_times = estimate_caption_times(captions, duration)
|
|
163
|
+
narration_times = [1.0] + caption_times if title_text else caption_times
|
|
164
|
+
|
|
165
|
+
# Generate TTS
|
|
166
|
+
tts_cache_dir.mkdir(parents=True, exist_ok=True)
|
|
167
|
+
if verbose:
|
|
168
|
+
print(" Generating TTS...")
|
|
169
|
+
narration_files = generate_tts_segments(narrations, tts_cache_dir)
|
|
170
|
+
|
|
171
|
+
# Mix audio
|
|
172
|
+
mixed_audio = Path(f"/tmp/narration_mixed_{video_path.stem}.mp3")
|
|
173
|
+
if verbose:
|
|
174
|
+
print(" Mixing audio...")
|
|
175
|
+
mix_narration_with_bgm(
|
|
176
|
+
narration_files=narration_files,
|
|
177
|
+
narration_times=narration_times,
|
|
178
|
+
bgm_path=bgm_path,
|
|
179
|
+
output_path=mixed_audio,
|
|
180
|
+
duration=duration,
|
|
181
|
+
bgm_volume=bgm_volume,
|
|
182
|
+
narration_delay=narration_delay,
|
|
183
|
+
fade_in_duration=fade_in_duration,
|
|
184
|
+
fade_out_duration=fade_out_duration,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# Create final video
|
|
188
|
+
if verbose:
|
|
189
|
+
print(f" Creating: {output_path.name}")
|
|
190
|
+
|
|
191
|
+
result = subprocess.run(
|
|
192
|
+
[
|
|
193
|
+
"ffmpeg",
|
|
194
|
+
"-y",
|
|
195
|
+
"-i",
|
|
196
|
+
str(video_path),
|
|
197
|
+
"-i",
|
|
198
|
+
str(mixed_audio),
|
|
199
|
+
"-c:v",
|
|
200
|
+
"copy",
|
|
201
|
+
"-c:a",
|
|
202
|
+
"aac",
|
|
203
|
+
"-b:a",
|
|
204
|
+
"192k",
|
|
205
|
+
"-shortest",
|
|
206
|
+
str(output_path),
|
|
207
|
+
],
|
|
208
|
+
capture_output=True,
|
|
209
|
+
text=True,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
# Cleanup
|
|
213
|
+
mixed_audio.unlink(missing_ok=True)
|
|
214
|
+
|
|
215
|
+
if result.returncode != 0:
|
|
216
|
+
return False, {"error": result.stderr[:200]}
|
|
217
|
+
|
|
218
|
+
return True, {
|
|
219
|
+
"duration": duration,
|
|
220
|
+
"captions": len(captions),
|
|
221
|
+
"output": str(output_path),
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
except Exception as e:
|
|
225
|
+
import traceback
|
|
226
|
+
|
|
227
|
+
return False, {"error": str(e), "traceback": traceback.format_exc()}
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
__all__ = [
|
|
231
|
+
"extract_captions_from_script",
|
|
232
|
+
"get_video_duration",
|
|
233
|
+
"estimate_caption_times",
|
|
234
|
+
"add_narration_to_video",
|
|
235
|
+
]
|
|
236
|
+
|
|
237
|
+
# EOF
|