figrecipe 0.6.0__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 +161 -1030
- figrecipe/__main__.py +12 -0
- figrecipe/_api/__init__.py +48 -0
- figrecipe/_api/_extract.py +108 -0
- figrecipe/_api/_notebook.py +61 -0
- figrecipe/_api/_panel.py +113 -0
- figrecipe/_api/_save.py +287 -0
- figrecipe/_api/_seaborn_proxy.py +34 -0
- figrecipe/_api/_style_manager.py +153 -0
- figrecipe/_api/_subplots.py +333 -0
- figrecipe/_api/_validate.py +82 -0
- 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 +4 -93
- figrecipe/_dev/_plotters.py +76 -0
- figrecipe/_dev/_run_demos.py +56 -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/_dev/demo_plotters/__init__.py +35 -166
- figrecipe/_dev/demo_plotters/_categories.py +81 -0
- figrecipe/_dev/demo_plotters/_figure_creators.py +119 -0
- figrecipe/_dev/demo_plotters/_helpers.py +31 -0
- figrecipe/_dev/demo_plotters/_registry.py +50 -0
- figrecipe/_dev/demo_plotters/bar_categorical/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/contour_surface/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/{plot_plot.py → line_curve/plot_plot.py} +3 -2
- figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/{plot_pie.py → special/plot_pie.py} +5 -1
- figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
- figrecipe/_editor/__init__.py +61 -13
- figrecipe/_editor/_bbox/__init__.py +43 -0
- figrecipe/_editor/_bbox/_collections.py +177 -0
- figrecipe/_editor/_bbox/_elements.py +159 -0
- figrecipe/_editor/_bbox/_extract.py +402 -0
- figrecipe/_editor/_bbox/_extract_axes.py +370 -0
- figrecipe/_editor/_bbox/_extract_text.py +466 -0
- figrecipe/_editor/_bbox/_lines.py +173 -0
- figrecipe/_editor/_bbox/_transforms.py +146 -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 +200 -1030
- figrecipe/_editor/_helpers.py +251 -0
- figrecipe/_editor/_hitmap/__init__.py +76 -0
- figrecipe/_editor/_hitmap/_artists/__init__.py +21 -0
- figrecipe/_editor/_hitmap/_artists/_collections.py +345 -0
- figrecipe/_editor/_hitmap/_artists/_images.py +68 -0
- figrecipe/_editor/_hitmap/_artists/_lines.py +107 -0
- figrecipe/_editor/_hitmap/_artists/_patches.py +163 -0
- figrecipe/_editor/_hitmap/_artists/_text.py +190 -0
- figrecipe/_editor/_hitmap/_colors.py +181 -0
- figrecipe/_editor/_hitmap/_detect.py +194 -0
- figrecipe/_editor/_hitmap/_restore.py +154 -0
- figrecipe/_editor/_hitmap_main.py +182 -0
- figrecipe/_editor/_overrides.py +4 -1
- figrecipe/_editor/_plot_types_registry.py +190 -0
- figrecipe/_editor/_preferences.py +135 -0
- figrecipe/_editor/_render_overrides.py +507 -0
- figrecipe/_editor/_renderer.py +81 -186
- figrecipe/_editor/_routes_annotation.py +114 -0
- figrecipe/_editor/_routes_axis.py +482 -0
- figrecipe/_editor/_routes_captions.py +130 -0
- figrecipe/_editor/_routes_composition.py +270 -0
- figrecipe/_editor/_routes_core.py +126 -0
- figrecipe/_editor/_routes_datatable.py +364 -0
- figrecipe/_editor/_routes_element.py +335 -0
- 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 +243 -0
- figrecipe/_editor/_templates/__init__.py +116 -1
- figrecipe/_editor/_templates/_html.py +154 -64
- 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 +178 -0
- figrecipe/_editor/_templates/_scripts/_accordion.py +328 -0
- figrecipe/_editor/_templates/_scripts/_annotation_drag.py +504 -0
- figrecipe/_editor/_templates/_scripts/_api.py +228 -0
- figrecipe/_editor/_templates/_scripts/_canvas_context_menu.py +182 -0
- figrecipe/_editor/_templates/_scripts/_captions.py +231 -0
- figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
- figrecipe/_editor/_templates/_scripts/_composition.py +283 -0
- figrecipe/_editor/_templates/_scripts/_core.py +493 -0
- 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/_debug_snapshot.py +186 -0
- figrecipe/_editor/_templates/_scripts/_element_editor.py +325 -0
- figrecipe/_editor/_templates/_scripts/_files.py +429 -0
- figrecipe/_editor/_templates/_scripts/_files_context_menu.py +240 -0
- figrecipe/_editor/_templates/_scripts/_hitmap.py +512 -0
- figrecipe/_editor/_templates/_scripts/_image_drop.py +428 -0
- figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
- figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
- figrecipe/_editor/_templates/_scripts/_legend_drag.py +270 -0
- figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
- figrecipe/_editor/_templates/_scripts/_multi_select.py +198 -0
- figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
- figrecipe/_editor/_templates/_scripts/_panel_drag.py +505 -0
- figrecipe/_editor/_templates/_scripts/_panel_drag_snapshot.py +33 -0
- figrecipe/_editor/_templates/_scripts/_panel_position.py +463 -0
- 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 +244 -0
- figrecipe/_editor/_templates/_scripts/_sync.py +242 -0
- figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
- figrecipe/_editor/_templates/_scripts/_undo_redo.py +348 -0
- figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
- figrecipe/_editor/_templates/_scripts/_zoom.py +212 -0
- figrecipe/_editor/_templates/_styles/__init__.py +78 -0
- figrecipe/_editor/_templates/_styles/_base.py +111 -0
- figrecipe/_editor/_templates/_styles/_buttons.py +327 -0
- figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
- figrecipe/_editor/_templates/_styles/_composition.py +87 -0
- figrecipe/_editor/_templates/_styles/_controls.py +430 -0
- 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 +144 -0
- figrecipe/_editor/_templates/_styles/_file_browser.py +466 -0
- figrecipe/_editor/_templates/_styles/_forms.py +224 -0
- figrecipe/_editor/_templates/_styles/_hitmap.py +191 -0
- figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
- figrecipe/_editor/_templates/_styles/_labels.py +118 -0
- figrecipe/_editor/_templates/_styles/_modals.py +127 -0
- figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
- figrecipe/_editor/_templates/_styles/_preview.py +430 -0
- figrecipe/_editor/_templates/_styles/_selection.py +73 -0
- 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 +8 -0
- figrecipe/_recorder.py +63 -109
- figrecipe/_recorder_utils.py +124 -0
- figrecipe/_reproducer/__init__.py +18 -0
- figrecipe/_reproducer/_core.py +509 -0
- figrecipe/_reproducer/_custom_plots.py +279 -0
- figrecipe/_reproducer/_seaborn.py +100 -0
- figrecipe/_reproducer/_violin.py +186 -0
- figrecipe/_signatures/_kwargs.py +273 -0
- figrecipe/_signatures/_loader.py +21 -423
- figrecipe/_signatures/_parsing.py +147 -0
- figrecipe/_utils/__init__.py +3 -0
- figrecipe/_utils/_bundle.py +205 -0
- figrecipe/_wrappers/_axes.py +252 -895
- figrecipe/_wrappers/_axes_helpers.py +136 -0
- figrecipe/_wrappers/_axes_plots.py +418 -0
- figrecipe/_wrappers/_axes_seaborn.py +157 -0
- figrecipe/_wrappers/_caption_generator.py +218 -0
- figrecipe/_wrappers/_figure.py +188 -1
- figrecipe/_wrappers/_panel_labels.py +127 -0
- figrecipe/_wrappers/_plot_helpers.py +143 -0
- figrecipe/_wrappers/_stat_annotation.py +274 -0
- figrecipe/_wrappers/_violin_helpers.py +180 -0
- figrecipe/styles/__init__.py +8 -6
- figrecipe/styles/_dotdict.py +72 -0
- figrecipe/styles/_finalize.py +134 -0
- figrecipe/styles/_fonts.py +77 -0
- figrecipe/styles/_kwargs_converter.py +178 -0
- figrecipe/styles/_plot_styles.py +209 -0
- figrecipe/styles/_style_applier.py +42 -480
- figrecipe/styles/_style_loader.py +16 -192
- figrecipe/styles/_themes.py +151 -0
- figrecipe/styles/presets/MATPLOTLIB.yaml +2 -1
- figrecipe/styles/presets/SCITEX.yaml +40 -28
- figrecipe-0.9.0.dist-info/METADATA +427 -0
- figrecipe-0.9.0.dist-info/RECORD +277 -0
- figrecipe-0.9.0.dist-info/entry_points.txt +2 -0
- figrecipe/_editor/_bbox.py +0 -978
- figrecipe/_editor/_hitmap.py +0 -937
- figrecipe/_editor/_templates/_scripts.py +0 -2778
- figrecipe/_editor/_templates/_styles.py +0 -1326
- figrecipe/_reproducer.py +0 -975
- figrecipe-0.6.0.dist-info/METADATA +0 -394
- figrecipe-0.6.0.dist-info/RECORD +0 -90
- /figrecipe/_dev/demo_plotters/{plot_bar.py → bar_categorical/plot_bar.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_barh.py → bar_categorical/plot_barh.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_contour.py → contour_surface/plot_contour.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_contourf.py → contour_surface/plot_contourf.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_tricontour.py → contour_surface/plot_tricontour.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_tricontourf.py → contour_surface/plot_tricontourf.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_tripcolor.py → contour_surface/plot_tripcolor.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_triplot.py → contour_surface/plot_triplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_boxplot.py → distribution/plot_boxplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_ecdf.py → distribution/plot_ecdf.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_hist.py → distribution/plot_hist.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_hist2d.py → distribution/plot_hist2d.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_violinplot.py → distribution/plot_violinplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_hexbin.py → image_matrix/plot_hexbin.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_imshow.py → image_matrix/plot_imshow.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_matshow.py → image_matrix/plot_matshow.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_pcolor.py → image_matrix/plot_pcolor.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_pcolormesh.py → image_matrix/plot_pcolormesh.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_spy.py → image_matrix/plot_spy.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_errorbar.py → line_curve/plot_errorbar.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_fill.py → line_curve/plot_fill.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_fill_between.py → line_curve/plot_fill_between.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_fill_betweenx.py → line_curve/plot_fill_betweenx.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_stackplot.py → line_curve/plot_stackplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_stairs.py → line_curve/plot_stairs.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_step.py → line_curve/plot_step.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_scatter.py → scatter_points/plot_scatter.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_eventplot.py → special/plot_eventplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_loglog.py → special/plot_loglog.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_semilogx.py → special/plot_semilogx.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_semilogy.py → special/plot_semilogy.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_stem.py → special/plot_stem.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_acorr.py → spectral_signal/plot_acorr.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_angle_spectrum.py → spectral_signal/plot_angle_spectrum.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_cohere.py → spectral_signal/plot_cohere.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_csd.py → spectral_signal/plot_csd.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_magnitude_spectrum.py → spectral_signal/plot_magnitude_spectrum.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_phase_spectrum.py → spectral_signal/plot_phase_spectrum.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_psd.py → spectral_signal/plot_psd.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_specgram.py → spectral_signal/plot_specgram.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_xcorr.py → spectral_signal/plot_xcorr.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_barbs.py → vector_flow/plot_barbs.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_quiver.py → vector_flow/plot_quiver.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_streamplot.py → vector_flow/plot_streamplot.py} +0 -0
- {figrecipe-0.6.0.dist-info → figrecipe-0.9.0.dist-info}/WHEEL +0 -0
- {figrecipe-0.6.0.dist-info → figrecipe-0.9.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Click effect visualization for demo recordings.
|
|
4
|
+
|
|
5
|
+
Shows ripple/pulse animation at click locations to make
|
|
6
|
+
mouse clicks visible in recordings.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
# JavaScript to inject click effect handler
|
|
10
|
+
CLICK_EFFECT_JS = """
|
|
11
|
+
() => {
|
|
12
|
+
// Add CSS animation if not exists
|
|
13
|
+
if (!document.getElementById('demo-click-style')) {
|
|
14
|
+
const style = document.createElement('style');
|
|
15
|
+
style.id = 'demo-click-style';
|
|
16
|
+
style.textContent = `
|
|
17
|
+
@keyframes demo-click-ripple {
|
|
18
|
+
0% {
|
|
19
|
+
transform: translate(-50%, -50%) scale(0);
|
|
20
|
+
opacity: 1;
|
|
21
|
+
}
|
|
22
|
+
100% {
|
|
23
|
+
transform: translate(-50%, -50%) scale(1);
|
|
24
|
+
opacity: 0;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
.demo-click-ripple {
|
|
28
|
+
position: fixed;
|
|
29
|
+
width: 60px;
|
|
30
|
+
height: 60px;
|
|
31
|
+
border: 4px solid #FF4444;
|
|
32
|
+
border-radius: 50%;
|
|
33
|
+
pointer-events: none;
|
|
34
|
+
z-index: 2147483646;
|
|
35
|
+
animation: demo-click-ripple 0.6s ease-out forwards;
|
|
36
|
+
}
|
|
37
|
+
`;
|
|
38
|
+
document.head.appendChild(style);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Initialize click sound audio element
|
|
42
|
+
if (!window._demoClickAudio) {
|
|
43
|
+
window._demoClickAudio = new Audio('/static/audio/click.mp3');
|
|
44
|
+
window._demoClickAudio.volume = 0.15;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Function to play click sound
|
|
48
|
+
window._playClickSound = () => {
|
|
49
|
+
if (window._demoClickAudio) {
|
|
50
|
+
window._demoClickAudio.currentTime = 0;
|
|
51
|
+
window._demoClickAudio.play().catch(() => {});
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Remove existing handler if any
|
|
56
|
+
if (window._demoClickHandler) {
|
|
57
|
+
document.removeEventListener('mousedown', window._demoClickHandler);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Add click handler
|
|
61
|
+
window._demoClickHandler = (e) => {
|
|
62
|
+
// Play click sound
|
|
63
|
+
if (window._playClickSound) window._playClickSound();
|
|
64
|
+
|
|
65
|
+
const ripple = document.createElement('div');
|
|
66
|
+
ripple.className = 'demo-click-ripple';
|
|
67
|
+
ripple.style.left = e.clientX + 'px';
|
|
68
|
+
ripple.style.top = e.clientY + 'px';
|
|
69
|
+
document.body.appendChild(ripple);
|
|
70
|
+
|
|
71
|
+
// Also pulse the cursor if it exists
|
|
72
|
+
const cursor = document.getElementById('demo-cursor');
|
|
73
|
+
if (cursor) {
|
|
74
|
+
cursor.style.transform = 'translate(-50%, -50%) scale(1.3)';
|
|
75
|
+
setTimeout(() => {
|
|
76
|
+
cursor.style.transform = 'translate(-50%, -50%) scale(1)';
|
|
77
|
+
}, 150);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Remove ripple after animation
|
|
81
|
+
setTimeout(() => ripple.remove(), 600);
|
|
82
|
+
};
|
|
83
|
+
document.addEventListener('mousedown', window._demoClickHandler);
|
|
84
|
+
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
REMOVE_CLICK_EFFECT_JS = """
|
|
90
|
+
() => {
|
|
91
|
+
// Remove handler
|
|
92
|
+
if (window._demoClickHandler) {
|
|
93
|
+
document.removeEventListener('mousedown', window._demoClickHandler);
|
|
94
|
+
delete window._demoClickHandler;
|
|
95
|
+
}
|
|
96
|
+
// Remove audio element
|
|
97
|
+
if (window._demoClickAudio) {
|
|
98
|
+
window._demoClickAudio.pause();
|
|
99
|
+
delete window._demoClickAudio;
|
|
100
|
+
}
|
|
101
|
+
delete window._playClickSound;
|
|
102
|
+
// Remove style
|
|
103
|
+
const style = document.getElementById('demo-click-style');
|
|
104
|
+
if (style) style.remove();
|
|
105
|
+
// Remove any remaining ripples
|
|
106
|
+
document.querySelectorAll('.demo-click-ripple').forEach(el => el.remove());
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
async def inject_click_effect(page) -> bool:
|
|
113
|
+
"""Inject click ripple effect handler into page.
|
|
114
|
+
|
|
115
|
+
Parameters
|
|
116
|
+
----------
|
|
117
|
+
page : playwright.async_api.Page
|
|
118
|
+
Playwright page object.
|
|
119
|
+
|
|
120
|
+
Returns
|
|
121
|
+
-------
|
|
122
|
+
bool
|
|
123
|
+
True if successful.
|
|
124
|
+
"""
|
|
125
|
+
return await page.evaluate(CLICK_EFFECT_JS)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
async def remove_click_effect(page) -> bool:
|
|
129
|
+
"""Remove click effect handler from page.
|
|
130
|
+
|
|
131
|
+
Parameters
|
|
132
|
+
----------
|
|
133
|
+
page : playwright.async_api.Page
|
|
134
|
+
Playwright page object.
|
|
135
|
+
|
|
136
|
+
Returns
|
|
137
|
+
-------
|
|
138
|
+
bool
|
|
139
|
+
True if successful.
|
|
140
|
+
"""
|
|
141
|
+
return await page.evaluate(REMOVE_CLICK_EFFECT_JS)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
__all__ = ["inject_click_effect", "remove_click_effect"]
|
|
145
|
+
|
|
146
|
+
# EOF
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Mouse cursor visualization for demo recordings.
|
|
4
|
+
|
|
5
|
+
Injects a visible cursor element that follows mouse movements,
|
|
6
|
+
making demos more intuitive when the system cursor isn't captured.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
# JavaScript to inject cursor visualization
|
|
10
|
+
CURSOR_JS = """
|
|
11
|
+
() => {
|
|
12
|
+
// Remove existing cursor if any
|
|
13
|
+
const existing = document.getElementById('demo-cursor');
|
|
14
|
+
if (existing) existing.remove();
|
|
15
|
+
|
|
16
|
+
// Create cursor element
|
|
17
|
+
const cursor = document.createElement('div');
|
|
18
|
+
cursor.id = 'demo-cursor';
|
|
19
|
+
cursor.style.cssText = `
|
|
20
|
+
position: fixed;
|
|
21
|
+
width: 24px;
|
|
22
|
+
height: 24px;
|
|
23
|
+
border: 3px solid #FF4444;
|
|
24
|
+
border-radius: 50%;
|
|
25
|
+
pointer-events: none;
|
|
26
|
+
z-index: 2147483647;
|
|
27
|
+
box-shadow: 0 0 10px rgba(255, 68, 68, 0.5);
|
|
28
|
+
transform: translate(-50%, -50%);
|
|
29
|
+
transition: transform 0.05s ease-out;
|
|
30
|
+
`;
|
|
31
|
+
document.body.appendChild(cursor);
|
|
32
|
+
|
|
33
|
+
// Create inner dot
|
|
34
|
+
const dot = document.createElement('div');
|
|
35
|
+
dot.style.cssText = `
|
|
36
|
+
position: absolute;
|
|
37
|
+
top: 50%;
|
|
38
|
+
left: 50%;
|
|
39
|
+
width: 6px;
|
|
40
|
+
height: 6px;
|
|
41
|
+
background: #FF4444;
|
|
42
|
+
border-radius: 50%;
|
|
43
|
+
transform: translate(-50%, -50%);
|
|
44
|
+
`;
|
|
45
|
+
cursor.appendChild(dot);
|
|
46
|
+
|
|
47
|
+
// Track mouse movement
|
|
48
|
+
window._demoCursorHandler = (e) => {
|
|
49
|
+
cursor.style.left = e.clientX + 'px';
|
|
50
|
+
cursor.style.top = e.clientY + 'px';
|
|
51
|
+
};
|
|
52
|
+
document.addEventListener('mousemove', window._demoCursorHandler);
|
|
53
|
+
|
|
54
|
+
// Initial position at center
|
|
55
|
+
cursor.style.left = window.innerWidth / 2 + 'px';
|
|
56
|
+
cursor.style.top = window.innerHeight / 2 + 'px';
|
|
57
|
+
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
REMOVE_CURSOR_JS = """
|
|
63
|
+
() => {
|
|
64
|
+
const cursor = document.getElementById('demo-cursor');
|
|
65
|
+
if (cursor) cursor.remove();
|
|
66
|
+
if (window._demoCursorHandler) {
|
|
67
|
+
document.removeEventListener('mousemove', window._demoCursorHandler);
|
|
68
|
+
delete window._demoCursorHandler;
|
|
69
|
+
}
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
# JavaScript to animate cursor to a position
|
|
75
|
+
MOVE_CURSOR_JS = """
|
|
76
|
+
async (args) => {
|
|
77
|
+
const { x, y, duration = 500 } = args;
|
|
78
|
+
const cursor = document.getElementById('demo-cursor');
|
|
79
|
+
if (!cursor) return false;
|
|
80
|
+
|
|
81
|
+
// Get current position
|
|
82
|
+
const startX = parseFloat(cursor.style.left) || window.innerWidth / 2;
|
|
83
|
+
const startY = parseFloat(cursor.style.top) || window.innerHeight / 2;
|
|
84
|
+
|
|
85
|
+
// Animate to target
|
|
86
|
+
const startTime = performance.now();
|
|
87
|
+
|
|
88
|
+
return new Promise((resolve) => {
|
|
89
|
+
function animate(currentTime) {
|
|
90
|
+
const elapsed = currentTime - startTime;
|
|
91
|
+
const progress = Math.min(elapsed / duration, 1);
|
|
92
|
+
|
|
93
|
+
// Ease-out cubic for natural motion
|
|
94
|
+
const eased = 1 - Math.pow(1 - progress, 3);
|
|
95
|
+
|
|
96
|
+
const currentX = startX + (x - startX) * eased;
|
|
97
|
+
const currentY = startY + (y - startY) * eased;
|
|
98
|
+
|
|
99
|
+
cursor.style.left = currentX + 'px';
|
|
100
|
+
cursor.style.top = currentY + 'px';
|
|
101
|
+
|
|
102
|
+
if (progress < 1) {
|
|
103
|
+
requestAnimationFrame(animate);
|
|
104
|
+
} else {
|
|
105
|
+
resolve(true);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
requestAnimationFrame(animate);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
async def inject_cursor(page) -> bool:
|
|
115
|
+
"""Inject visible cursor element into page.
|
|
116
|
+
|
|
117
|
+
Parameters
|
|
118
|
+
----------
|
|
119
|
+
page : playwright.async_api.Page
|
|
120
|
+
Playwright page object.
|
|
121
|
+
|
|
122
|
+
Returns
|
|
123
|
+
-------
|
|
124
|
+
bool
|
|
125
|
+
True if successful.
|
|
126
|
+
"""
|
|
127
|
+
return await page.evaluate(CURSOR_JS)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
async def remove_cursor(page) -> bool:
|
|
131
|
+
"""Remove cursor visualization from page.
|
|
132
|
+
|
|
133
|
+
Parameters
|
|
134
|
+
----------
|
|
135
|
+
page : playwright.async_api.Page
|
|
136
|
+
Playwright page object.
|
|
137
|
+
|
|
138
|
+
Returns
|
|
139
|
+
-------
|
|
140
|
+
bool
|
|
141
|
+
True if successful.
|
|
142
|
+
"""
|
|
143
|
+
return await page.evaluate(REMOVE_CURSOR_JS)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
async def move_cursor_to(page, x: float, y: float, duration_ms: int = 500) -> bool:
|
|
147
|
+
"""Animate cursor to a specific position.
|
|
148
|
+
|
|
149
|
+
Parameters
|
|
150
|
+
----------
|
|
151
|
+
page : playwright.async_api.Page
|
|
152
|
+
Playwright page object.
|
|
153
|
+
x : float
|
|
154
|
+
Target X coordinate.
|
|
155
|
+
y : float
|
|
156
|
+
Target Y coordinate.
|
|
157
|
+
duration_ms : int, optional
|
|
158
|
+
Animation duration in milliseconds (default: 500).
|
|
159
|
+
|
|
160
|
+
Returns
|
|
161
|
+
-------
|
|
162
|
+
bool
|
|
163
|
+
True if successful.
|
|
164
|
+
"""
|
|
165
|
+
args = {"x": x, "y": y, "duration": duration_ms}
|
|
166
|
+
return await page.evaluate(MOVE_CURSOR_JS, args)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
async def move_cursor_to_element(page, locator, duration_ms: int = 500) -> bool:
|
|
170
|
+
"""Animate cursor to an element's center.
|
|
171
|
+
|
|
172
|
+
Parameters
|
|
173
|
+
----------
|
|
174
|
+
page : playwright.async_api.Page
|
|
175
|
+
Playwright page object.
|
|
176
|
+
locator : playwright.async_api.Locator
|
|
177
|
+
Playwright locator for target element.
|
|
178
|
+
duration_ms : int, optional
|
|
179
|
+
Animation duration in milliseconds (default: 500).
|
|
180
|
+
|
|
181
|
+
Returns
|
|
182
|
+
-------
|
|
183
|
+
bool
|
|
184
|
+
True if successful.
|
|
185
|
+
"""
|
|
186
|
+
box = await locator.bounding_box()
|
|
187
|
+
if not box:
|
|
188
|
+
return False
|
|
189
|
+
x = box["x"] + box["width"] / 2
|
|
190
|
+
y = box["y"] + box["height"] / 2
|
|
191
|
+
return await move_cursor_to(page, x, y, duration_ms)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
__all__ = ["inject_cursor", "remove_cursor", "move_cursor_to", "move_cursor_to_element"]
|
|
195
|
+
|
|
196
|
+
# EOF
|
|
@@ -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
|