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,69 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Browser automation utilities for demo generation.
|
|
4
|
+
|
|
5
|
+
This module provides tools for creating visual demos of figrecipe features
|
|
6
|
+
using Playwright for browser automation.
|
|
7
|
+
|
|
8
|
+
Features:
|
|
9
|
+
- Mouse cursor visualization
|
|
10
|
+
- Click effect animations
|
|
11
|
+
- Caption overlays
|
|
12
|
+
- Element highlighting
|
|
13
|
+
- Video/GIF recording
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from ._audio import generate_tts_segments, mix_narration_with_bgm
|
|
17
|
+
from ._caption import hide_caption, show_caption, show_title_screen
|
|
18
|
+
from ._click_effect import inject_click_effect, remove_click_effect
|
|
19
|
+
from ._cursor import (
|
|
20
|
+
inject_cursor,
|
|
21
|
+
move_cursor_to,
|
|
22
|
+
move_cursor_to_element,
|
|
23
|
+
remove_cursor,
|
|
24
|
+
)
|
|
25
|
+
from ._highlight import highlight_element
|
|
26
|
+
from ._narration import (
|
|
27
|
+
add_narration_to_video,
|
|
28
|
+
estimate_caption_times,
|
|
29
|
+
extract_captions_from_script,
|
|
30
|
+
get_video_duration,
|
|
31
|
+
)
|
|
32
|
+
from ._recorder import DemoRecorder
|
|
33
|
+
from ._utils import concatenate_videos, convert_to_gif
|
|
34
|
+
from ._video_trim import (
|
|
35
|
+
detect_markers,
|
|
36
|
+
inject_end_marker,
|
|
37
|
+
inject_start_marker,
|
|
38
|
+
process_video_with_markers,
|
|
39
|
+
trim_video_by_markers,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"inject_cursor",
|
|
44
|
+
"remove_cursor",
|
|
45
|
+
"move_cursor_to",
|
|
46
|
+
"move_cursor_to_element",
|
|
47
|
+
"inject_click_effect",
|
|
48
|
+
"remove_click_effect",
|
|
49
|
+
"show_caption",
|
|
50
|
+
"hide_caption",
|
|
51
|
+
"show_title_screen",
|
|
52
|
+
"highlight_element",
|
|
53
|
+
"DemoRecorder",
|
|
54
|
+
"convert_to_gif",
|
|
55
|
+
"concatenate_videos",
|
|
56
|
+
"inject_start_marker",
|
|
57
|
+
"inject_end_marker",
|
|
58
|
+
"detect_markers",
|
|
59
|
+
"trim_video_by_markers",
|
|
60
|
+
"process_video_with_markers",
|
|
61
|
+
"generate_tts_segments",
|
|
62
|
+
"mix_narration_with_bgm",
|
|
63
|
+
"extract_captions_from_script",
|
|
64
|
+
"get_video_duration",
|
|
65
|
+
"estimate_caption_times",
|
|
66
|
+
"add_narration_to_video",
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
# EOF
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Audio processing for demo videos.
|
|
4
|
+
|
|
5
|
+
Provides TTS generation and audio mixing for demo narration.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import hashlib
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
import subprocess
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import List, Tuple
|
|
14
|
+
|
|
15
|
+
# Check for ElevenLabs API key
|
|
16
|
+
ELEVENLABS_API_KEY = os.environ.get("ELEVENLABS_API_KEY")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _sanitize_filename(text: str, max_length: int = 50) -> str:
|
|
20
|
+
"""Convert text to a safe filename prefix.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
text : str
|
|
25
|
+
Text to convert.
|
|
26
|
+
max_length : int
|
|
27
|
+
Maximum length of the result.
|
|
28
|
+
|
|
29
|
+
Returns
|
|
30
|
+
-------
|
|
31
|
+
str
|
|
32
|
+
Sanitized filename-safe string.
|
|
33
|
+
"""
|
|
34
|
+
# Remove special characters, keep alphanumeric and spaces
|
|
35
|
+
clean = re.sub(r"[^a-zA-Z0-9\s]", "", text)
|
|
36
|
+
# Replace spaces with underscores
|
|
37
|
+
clean = re.sub(r"\s+", "_", clean.strip())
|
|
38
|
+
# Truncate and lowercase
|
|
39
|
+
return clean[:max_length].lower().rstrip("_")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _get_cache_path(text: str, cache_dir: Path) -> Path:
|
|
43
|
+
"""Get cache file path using transcription-based naming.
|
|
44
|
+
|
|
45
|
+
Format: {sanitized_text}_{hash}.mp3
|
|
46
|
+
Example: enable_dark_mode_demo_a1b2c3d4.mp3
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
text : str
|
|
51
|
+
Text content for TTS.
|
|
52
|
+
cache_dir : Path
|
|
53
|
+
Cache directory.
|
|
54
|
+
|
|
55
|
+
Returns
|
|
56
|
+
-------
|
|
57
|
+
Path
|
|
58
|
+
Cache file path.
|
|
59
|
+
"""
|
|
60
|
+
# Create short hash for uniqueness
|
|
61
|
+
text_hash = hashlib.md5(text.encode()).hexdigest()[:8]
|
|
62
|
+
# Sanitize text for filename
|
|
63
|
+
sanitized = _sanitize_filename(text)
|
|
64
|
+
# Combine: readable prefix + hash for uniqueness
|
|
65
|
+
filename = f"{sanitized}_{text_hash}.mp3"
|
|
66
|
+
return cache_dir / filename
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def generate_tts_segments(
|
|
70
|
+
narrations: List[Tuple[str, str]],
|
|
71
|
+
output_dir: Path,
|
|
72
|
+
) -> List[Path]:
|
|
73
|
+
"""Generate TTS audio files for narrations.
|
|
74
|
+
|
|
75
|
+
Uses ElevenLabs if API key is available, falls back to gTTS.
|
|
76
|
+
Cache files are named using transcription text for easy identification.
|
|
77
|
+
|
|
78
|
+
Parameters
|
|
79
|
+
----------
|
|
80
|
+
narrations : List[Tuple[str, str]]
|
|
81
|
+
List of (name, text) tuples.
|
|
82
|
+
output_dir : Path
|
|
83
|
+
Output directory for audio files.
|
|
84
|
+
|
|
85
|
+
Returns
|
|
86
|
+
-------
|
|
87
|
+
List[Path]
|
|
88
|
+
List of generated audio file paths.
|
|
89
|
+
"""
|
|
90
|
+
output_dir = Path(output_dir)
|
|
91
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
92
|
+
|
|
93
|
+
audio_files = []
|
|
94
|
+
|
|
95
|
+
if ELEVENLABS_API_KEY:
|
|
96
|
+
print("Using ElevenLabs TTS (high quality)")
|
|
97
|
+
try:
|
|
98
|
+
from elevenlabs import ElevenLabs
|
|
99
|
+
|
|
100
|
+
client = ElevenLabs(api_key=ELEVENLABS_API_KEY)
|
|
101
|
+
|
|
102
|
+
for name, text in narrations:
|
|
103
|
+
cache_path = _get_cache_path(text, output_dir)
|
|
104
|
+
|
|
105
|
+
# Check cache
|
|
106
|
+
if cache_path.exists():
|
|
107
|
+
print(f" [cache] {cache_path.name}")
|
|
108
|
+
audio_files.append(cache_path)
|
|
109
|
+
continue
|
|
110
|
+
|
|
111
|
+
# Generate TTS
|
|
112
|
+
audio = client.text_to_speech.convert(
|
|
113
|
+
voice_id="21m00Tcm4TlvDq8ikWAM", # Rachel
|
|
114
|
+
text=text,
|
|
115
|
+
model_id="eleven_monolingual_v1",
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Save audio
|
|
119
|
+
with open(cache_path, "wb") as f:
|
|
120
|
+
for chunk in audio:
|
|
121
|
+
f.write(chunk)
|
|
122
|
+
|
|
123
|
+
print(f" ✓ ElevenLabs: {cache_path.name} - '{text[:40]}...'")
|
|
124
|
+
audio_files.append(cache_path)
|
|
125
|
+
|
|
126
|
+
return audio_files
|
|
127
|
+
|
|
128
|
+
except Exception as e:
|
|
129
|
+
print(f" ElevenLabs failed: {e}, falling back to gTTS")
|
|
130
|
+
|
|
131
|
+
# Fallback to gTTS
|
|
132
|
+
print("Using gTTS (fallback)")
|
|
133
|
+
try:
|
|
134
|
+
from gtts import gTTS
|
|
135
|
+
|
|
136
|
+
for name, text in narrations:
|
|
137
|
+
cache_path = _get_cache_path(text, output_dir)
|
|
138
|
+
|
|
139
|
+
# Check cache
|
|
140
|
+
if cache_path.exists():
|
|
141
|
+
print(f" [cache] {cache_path.name}")
|
|
142
|
+
audio_files.append(cache_path)
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
# Generate TTS
|
|
146
|
+
tts = gTTS(text=text, lang="en")
|
|
147
|
+
tts.save(str(cache_path))
|
|
148
|
+
|
|
149
|
+
print(f" ✓ gTTS: {cache_path.name} - '{text[:40]}...'")
|
|
150
|
+
audio_files.append(cache_path)
|
|
151
|
+
|
|
152
|
+
return audio_files
|
|
153
|
+
|
|
154
|
+
except ImportError:
|
|
155
|
+
raise RuntimeError("Neither ElevenLabs nor gTTS available")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def mix_narration_with_bgm(
|
|
159
|
+
narration_files: List[Path],
|
|
160
|
+
narration_times: List[float],
|
|
161
|
+
bgm_path: Path,
|
|
162
|
+
output_path: Path,
|
|
163
|
+
duration: float,
|
|
164
|
+
bgm_volume: float = 0.10,
|
|
165
|
+
narration_delay: float = 0.3,
|
|
166
|
+
fade_in_duration: float = 0.5,
|
|
167
|
+
fade_out_duration: float = 1.0,
|
|
168
|
+
) -> Path:
|
|
169
|
+
"""Mix narration audio with background music.
|
|
170
|
+
|
|
171
|
+
Parameters
|
|
172
|
+
----------
|
|
173
|
+
narration_files : List[Path]
|
|
174
|
+
List of narration audio files.
|
|
175
|
+
narration_times : List[float]
|
|
176
|
+
Start times for each narration.
|
|
177
|
+
bgm_path : Path
|
|
178
|
+
Path to background music file.
|
|
179
|
+
output_path : Path
|
|
180
|
+
Output path for mixed audio.
|
|
181
|
+
duration : float
|
|
182
|
+
Total duration in seconds.
|
|
183
|
+
bgm_volume : float
|
|
184
|
+
Background music volume (0.0-1.0).
|
|
185
|
+
narration_delay : float
|
|
186
|
+
Delay before narration starts.
|
|
187
|
+
fade_in_duration : float
|
|
188
|
+
BGM fade-in duration.
|
|
189
|
+
fade_out_duration : float
|
|
190
|
+
BGM fade-out duration.
|
|
191
|
+
|
|
192
|
+
Returns
|
|
193
|
+
-------
|
|
194
|
+
Path
|
|
195
|
+
Path to mixed audio file.
|
|
196
|
+
"""
|
|
197
|
+
# Build ffmpeg filter for mixing
|
|
198
|
+
inputs = ["-i", str(bgm_path)]
|
|
199
|
+
for f in narration_files:
|
|
200
|
+
inputs.extend(["-i", str(f)])
|
|
201
|
+
|
|
202
|
+
# BGM filter: loop, trim, volume, fade
|
|
203
|
+
bgm_filter = (
|
|
204
|
+
f"[0:a]aloop=loop=-1:size=2e+09,atrim=0:{duration},"
|
|
205
|
+
f"volume={bgm_volume},"
|
|
206
|
+
f"afade=t=in:st=0:d={fade_in_duration},"
|
|
207
|
+
f"afade=t=out:st={duration - fade_out_duration}:d={fade_out_duration}[bgm]"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# Narration filters with delays
|
|
211
|
+
narration_filters = []
|
|
212
|
+
mix_inputs = "[bgm]"
|
|
213
|
+
|
|
214
|
+
for i, (f, t) in enumerate(zip(narration_files, narration_times)):
|
|
215
|
+
delay_ms = int((t + narration_delay) * 1000)
|
|
216
|
+
narration_filters.append(f"[{i + 1}:a]adelay={delay_ms}|{delay_ms}[n{i}]")
|
|
217
|
+
mix_inputs += f"[n{i}]"
|
|
218
|
+
|
|
219
|
+
# Combine all filters
|
|
220
|
+
all_filters = [bgm_filter] + narration_filters
|
|
221
|
+
mix_filter = (
|
|
222
|
+
f"{mix_inputs}amix=inputs={len(narration_files) + 1}:duration=first[out]"
|
|
223
|
+
)
|
|
224
|
+
all_filters.append(mix_filter)
|
|
225
|
+
|
|
226
|
+
filter_complex = ";".join(all_filters)
|
|
227
|
+
|
|
228
|
+
# Run ffmpeg
|
|
229
|
+
cmd = (
|
|
230
|
+
["ffmpeg", "-y"]
|
|
231
|
+
+ inputs
|
|
232
|
+
+ ["-filter_complex", filter_complex, "-map", "[out]", str(output_path)]
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
subprocess.run(cmd, check=True, capture_output=True)
|
|
236
|
+
|
|
237
|
+
return output_path
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
__all__ = ["generate_tts_segments", "mix_narration_with_bgm"]
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Caption overlay for demo recordings.
|
|
4
|
+
|
|
5
|
+
Shows text captions/banners during demo recordings to explain
|
|
6
|
+
what is happening in the demo.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
# JavaScript template for showing caption
|
|
10
|
+
SHOW_CAPTION_JS = """
|
|
11
|
+
(text) => {
|
|
12
|
+
// Remove existing caption if any
|
|
13
|
+
const existing = document.getElementById('demo-caption');
|
|
14
|
+
if (existing) existing.remove();
|
|
15
|
+
|
|
16
|
+
// Add CSS if not exists
|
|
17
|
+
if (!document.getElementById('demo-caption-style')) {
|
|
18
|
+
const style = document.createElement('style');
|
|
19
|
+
style.id = 'demo-caption-style';
|
|
20
|
+
style.textContent = `
|
|
21
|
+
@keyframes demo-caption-fade-in {
|
|
22
|
+
0% { opacity: 0; transform: translateY(20px); }
|
|
23
|
+
100% { opacity: 1; transform: translateY(0); }
|
|
24
|
+
}
|
|
25
|
+
@keyframes demo-caption-fade-out {
|
|
26
|
+
0% { opacity: 1; transform: translateY(0); }
|
|
27
|
+
100% { opacity: 0; transform: translateY(-20px); }
|
|
28
|
+
}
|
|
29
|
+
`;
|
|
30
|
+
document.head.appendChild(style);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Create caption element
|
|
34
|
+
const caption = document.createElement('div');
|
|
35
|
+
caption.id = 'demo-caption';
|
|
36
|
+
caption.textContent = text;
|
|
37
|
+
caption.style.cssText = `
|
|
38
|
+
position: fixed;
|
|
39
|
+
bottom: 50px;
|
|
40
|
+
left: 50%;
|
|
41
|
+
transform: translateX(-50%);
|
|
42
|
+
background: rgba(0, 0, 0, 0.85);
|
|
43
|
+
color: white;
|
|
44
|
+
padding: 18px 40px;
|
|
45
|
+
border-radius: 10px;
|
|
46
|
+
font-size: 24px;
|
|
47
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
48
|
+
font-weight: 500;
|
|
49
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
|
50
|
+
z-index: 2147483645;
|
|
51
|
+
pointer-events: none;
|
|
52
|
+
animation: demo-caption-fade-in 0.3s ease-out forwards;
|
|
53
|
+
max-width: 80%;
|
|
54
|
+
text-align: center;
|
|
55
|
+
`;
|
|
56
|
+
document.body.appendChild(caption);
|
|
57
|
+
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
HIDE_CAPTION_JS = """
|
|
63
|
+
() => {
|
|
64
|
+
const caption = document.getElementById('demo-caption');
|
|
65
|
+
if (caption) {
|
|
66
|
+
caption.style.animation = 'demo-caption-fade-out 0.3s ease-out forwards';
|
|
67
|
+
setTimeout(() => caption.remove(), 300);
|
|
68
|
+
}
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
# JavaScript for title screen with blur overlay
|
|
74
|
+
TITLE_SCREEN_JS = """
|
|
75
|
+
async (args) => {
|
|
76
|
+
const { title, subtitle = '', timestamp = '', duration = 2000 } = args;
|
|
77
|
+
|
|
78
|
+
// Add CSS animations if not exists
|
|
79
|
+
if (!document.getElementById('demo-title-style')) {
|
|
80
|
+
const style = document.createElement('style');
|
|
81
|
+
style.id = 'demo-title-style';
|
|
82
|
+
style.textContent = `
|
|
83
|
+
@keyframes demo-title-fade-in {
|
|
84
|
+
0% { opacity: 0; }
|
|
85
|
+
100% { opacity: 1; }
|
|
86
|
+
}
|
|
87
|
+
@keyframes demo-title-fade-out {
|
|
88
|
+
0% { opacity: 1; }
|
|
89
|
+
100% { opacity: 0; }
|
|
90
|
+
}
|
|
91
|
+
@keyframes demo-title-text-in {
|
|
92
|
+
0% { opacity: 0; transform: scale(0.9); }
|
|
93
|
+
100% { opacity: 1; transform: scale(1); }
|
|
94
|
+
}
|
|
95
|
+
`;
|
|
96
|
+
document.head.appendChild(style);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Create blur overlay
|
|
100
|
+
const overlay = document.createElement('div');
|
|
101
|
+
overlay.id = 'demo-title-overlay';
|
|
102
|
+
overlay.style.cssText = `
|
|
103
|
+
position: fixed;
|
|
104
|
+
top: 0;
|
|
105
|
+
left: 0;
|
|
106
|
+
width: 100%;
|
|
107
|
+
height: 100%;
|
|
108
|
+
background: rgba(0, 0, 0, 0.7);
|
|
109
|
+
backdrop-filter: blur(8px);
|
|
110
|
+
-webkit-backdrop-filter: blur(8px);
|
|
111
|
+
z-index: 2147483646;
|
|
112
|
+
display: flex;
|
|
113
|
+
flex-direction: column;
|
|
114
|
+
align-items: center;
|
|
115
|
+
justify-content: center;
|
|
116
|
+
animation: demo-title-fade-in 0.5s ease-out forwards;
|
|
117
|
+
`;
|
|
118
|
+
|
|
119
|
+
// Create title text
|
|
120
|
+
const titleEl = document.createElement('div');
|
|
121
|
+
titleEl.style.cssText = `
|
|
122
|
+
color: white;
|
|
123
|
+
font-size: 48px;
|
|
124
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
125
|
+
font-weight: 700;
|
|
126
|
+
text-align: center;
|
|
127
|
+
animation: demo-title-text-in 0.6s ease-out 0.2s both;
|
|
128
|
+
text-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
|
129
|
+
`;
|
|
130
|
+
titleEl.textContent = title;
|
|
131
|
+
overlay.appendChild(titleEl);
|
|
132
|
+
|
|
133
|
+
// Create subtitle if provided
|
|
134
|
+
if (subtitle) {
|
|
135
|
+
const subtitleEl = document.createElement('div');
|
|
136
|
+
subtitleEl.style.cssText = `
|
|
137
|
+
color: rgba(255, 255, 255, 0.8);
|
|
138
|
+
font-size: 24px;
|
|
139
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
140
|
+
font-weight: 400;
|
|
141
|
+
margin-top: 16px;
|
|
142
|
+
text-align: center;
|
|
143
|
+
animation: demo-title-text-in 0.6s ease-out 0.4s both;
|
|
144
|
+
`;
|
|
145
|
+
subtitleEl.textContent = subtitle;
|
|
146
|
+
overlay.appendChild(subtitleEl);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Create timestamp if provided
|
|
150
|
+
if (timestamp) {
|
|
151
|
+
const timestampEl = document.createElement('div');
|
|
152
|
+
timestampEl.style.cssText = `
|
|
153
|
+
color: rgba(255, 255, 255, 0.5);
|
|
154
|
+
font-size: 14px;
|
|
155
|
+
font-family: monospace;
|
|
156
|
+
margin-top: 24px;
|
|
157
|
+
text-align: center;
|
|
158
|
+
animation: demo-title-text-in 0.6s ease-out 0.5s both;
|
|
159
|
+
`;
|
|
160
|
+
timestampEl.textContent = timestamp;
|
|
161
|
+
overlay.appendChild(timestampEl);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
document.body.appendChild(overlay);
|
|
165
|
+
|
|
166
|
+
// Wait and fade out
|
|
167
|
+
await new Promise(r => setTimeout(r, duration));
|
|
168
|
+
|
|
169
|
+
overlay.style.animation = 'demo-title-fade-out 0.5s ease-out forwards';
|
|
170
|
+
await new Promise(r => setTimeout(r, 500));
|
|
171
|
+
overlay.remove();
|
|
172
|
+
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
async def show_caption(page, text: str) -> bool:
|
|
179
|
+
"""Show caption text overlay on page.
|
|
180
|
+
|
|
181
|
+
Parameters
|
|
182
|
+
----------
|
|
183
|
+
page : playwright.async_api.Page
|
|
184
|
+
Playwright page object.
|
|
185
|
+
text : str
|
|
186
|
+
Caption text to display.
|
|
187
|
+
|
|
188
|
+
Returns
|
|
189
|
+
-------
|
|
190
|
+
bool
|
|
191
|
+
True if successful.
|
|
192
|
+
"""
|
|
193
|
+
return await page.evaluate(SHOW_CAPTION_JS, text)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
async def hide_caption(page) -> bool:
|
|
197
|
+
"""Hide caption overlay from page.
|
|
198
|
+
|
|
199
|
+
Parameters
|
|
200
|
+
----------
|
|
201
|
+
page : playwright.async_api.Page
|
|
202
|
+
Playwright page object.
|
|
203
|
+
|
|
204
|
+
Returns
|
|
205
|
+
-------
|
|
206
|
+
bool
|
|
207
|
+
True if successful.
|
|
208
|
+
"""
|
|
209
|
+
return await page.evaluate(HIDE_CAPTION_JS)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
async def show_title_screen(
|
|
213
|
+
page,
|
|
214
|
+
title: str,
|
|
215
|
+
subtitle: str = "",
|
|
216
|
+
timestamp: str = "",
|
|
217
|
+
duration_ms: int = 2000,
|
|
218
|
+
) -> bool:
|
|
219
|
+
"""Show title screen with blur overlay and fade effect.
|
|
220
|
+
|
|
221
|
+
Parameters
|
|
222
|
+
----------
|
|
223
|
+
page : playwright.async_api.Page
|
|
224
|
+
Playwright page object.
|
|
225
|
+
title : str
|
|
226
|
+
Main title text.
|
|
227
|
+
subtitle : str, optional
|
|
228
|
+
Subtitle text (default: "").
|
|
229
|
+
timestamp : str, optional
|
|
230
|
+
Timestamp to display (default: "").
|
|
231
|
+
duration_ms : int, optional
|
|
232
|
+
Duration to show title in milliseconds (default: 2000).
|
|
233
|
+
|
|
234
|
+
Returns
|
|
235
|
+
-------
|
|
236
|
+
bool
|
|
237
|
+
True if successful.
|
|
238
|
+
"""
|
|
239
|
+
args = {
|
|
240
|
+
"title": title,
|
|
241
|
+
"subtitle": subtitle,
|
|
242
|
+
"timestamp": timestamp,
|
|
243
|
+
"duration": duration_ms,
|
|
244
|
+
}
|
|
245
|
+
return await page.evaluate(TITLE_SCREEN_JS, args)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
# JavaScript for closing branding screen
|
|
249
|
+
CLOSING_SCREEN_JS = """
|
|
250
|
+
async (args) => {
|
|
251
|
+
const { duration = 2500 } = args;
|
|
252
|
+
|
|
253
|
+
// Add CSS animations
|
|
254
|
+
if (!document.getElementById('demo-closing-style')) {
|
|
255
|
+
const style = document.createElement('style');
|
|
256
|
+
style.id = 'demo-closing-style';
|
|
257
|
+
style.textContent = `
|
|
258
|
+
@keyframes demo-closing-fade-in {
|
|
259
|
+
0% { opacity: 0; }
|
|
260
|
+
100% { opacity: 1; }
|
|
261
|
+
}
|
|
262
|
+
@keyframes demo-closing-fade-out {
|
|
263
|
+
0% { opacity: 1; }
|
|
264
|
+
100% { opacity: 0; }
|
|
265
|
+
}
|
|
266
|
+
`;
|
|
267
|
+
document.head.appendChild(style);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Create overlay
|
|
271
|
+
const overlay = document.createElement('div');
|
|
272
|
+
overlay.id = 'demo-closing-overlay';
|
|
273
|
+
overlay.style.cssText = `
|
|
274
|
+
position: fixed;
|
|
275
|
+
top: 0;
|
|
276
|
+
left: 0;
|
|
277
|
+
width: 100%;
|
|
278
|
+
height: 100%;
|
|
279
|
+
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
|
280
|
+
z-index: 2147483646;
|
|
281
|
+
display: flex;
|
|
282
|
+
flex-direction: column;
|
|
283
|
+
align-items: center;
|
|
284
|
+
justify-content: center;
|
|
285
|
+
animation: demo-closing-fade-in 0.5s ease-out forwards;
|
|
286
|
+
`;
|
|
287
|
+
|
|
288
|
+
// FigRecipe title
|
|
289
|
+
const title = document.createElement('div');
|
|
290
|
+
title.style.cssText = `
|
|
291
|
+
color: white;
|
|
292
|
+
font-size: 56px;
|
|
293
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
294
|
+
font-weight: 700;
|
|
295
|
+
margin-bottom: 16px;
|
|
296
|
+
`;
|
|
297
|
+
title.textContent = 'FigRecipe';
|
|
298
|
+
overlay.appendChild(title);
|
|
299
|
+
|
|
300
|
+
// Part of SciTeX
|
|
301
|
+
const scitex = document.createElement('div');
|
|
302
|
+
scitex.style.cssText = `
|
|
303
|
+
color: rgba(255, 255, 255, 0.7);
|
|
304
|
+
font-size: 20px;
|
|
305
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
306
|
+
margin-bottom: 24px;
|
|
307
|
+
`;
|
|
308
|
+
scitex.innerHTML = 'Part of SciTeX™';
|
|
309
|
+
overlay.appendChild(scitex);
|
|
310
|
+
|
|
311
|
+
// URL
|
|
312
|
+
const url = document.createElement('div');
|
|
313
|
+
url.style.cssText = `
|
|
314
|
+
color: #4da6ff;
|
|
315
|
+
font-size: 18px;
|
|
316
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
317
|
+
`;
|
|
318
|
+
url.textContent = 'https://scitex.ai';
|
|
319
|
+
overlay.appendChild(url);
|
|
320
|
+
|
|
321
|
+
document.body.appendChild(overlay);
|
|
322
|
+
|
|
323
|
+
// Wait and fade out
|
|
324
|
+
await new Promise(r => setTimeout(r, duration));
|
|
325
|
+
|
|
326
|
+
overlay.style.animation = 'demo-closing-fade-out 0.5s ease-out forwards';
|
|
327
|
+
await new Promise(r => setTimeout(r, 500));
|
|
328
|
+
overlay.remove();
|
|
329
|
+
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
"""
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
async def show_closing_screen(page, duration_ms: int = 2500) -> bool:
|
|
336
|
+
"""Show closing branding screen with FigRecipe and SciTeX.
|
|
337
|
+
|
|
338
|
+
Parameters
|
|
339
|
+
----------
|
|
340
|
+
page : playwright.async_api.Page
|
|
341
|
+
Playwright page object.
|
|
342
|
+
duration_ms : int, optional
|
|
343
|
+
Duration to show screen in milliseconds (default: 2500).
|
|
344
|
+
|
|
345
|
+
Returns
|
|
346
|
+
-------
|
|
347
|
+
bool
|
|
348
|
+
True if successful.
|
|
349
|
+
"""
|
|
350
|
+
args = {"duration": duration_ms}
|
|
351
|
+
return await page.evaluate(CLOSING_SCREEN_JS, args)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
__all__ = ["show_caption", "hide_caption", "show_title_screen", "show_closing_screen"]
|
|
355
|
+
|
|
356
|
+
# EOF
|