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.
Files changed (143) hide show
  1. figrecipe/__init__.py +74 -76
  2. figrecipe/__main__.py +12 -0
  3. figrecipe/_api/_panel.py +67 -0
  4. figrecipe/_api/_save.py +100 -4
  5. figrecipe/_cli/__init__.py +7 -0
  6. figrecipe/_cli/_compose.py +87 -0
  7. figrecipe/_cli/_convert.py +117 -0
  8. figrecipe/_cli/_crop.py +82 -0
  9. figrecipe/_cli/_edit.py +70 -0
  10. figrecipe/_cli/_extract.py +128 -0
  11. figrecipe/_cli/_fonts.py +47 -0
  12. figrecipe/_cli/_info.py +67 -0
  13. figrecipe/_cli/_main.py +58 -0
  14. figrecipe/_cli/_reproduce.py +79 -0
  15. figrecipe/_cli/_style.py +77 -0
  16. figrecipe/_cli/_validate.py +66 -0
  17. figrecipe/_cli/_version.py +50 -0
  18. figrecipe/_composition/__init__.py +32 -0
  19. figrecipe/_composition/_alignment.py +452 -0
  20. figrecipe/_composition/_compose.py +179 -0
  21. figrecipe/_composition/_import_axes.py +127 -0
  22. figrecipe/_composition/_visibility.py +125 -0
  23. figrecipe/_dev/__init__.py +2 -0
  24. figrecipe/_dev/browser/__init__.py +69 -0
  25. figrecipe/_dev/browser/_audio.py +240 -0
  26. figrecipe/_dev/browser/_caption.py +356 -0
  27. figrecipe/_dev/browser/_click_effect.py +146 -0
  28. figrecipe/_dev/browser/_cursor.py +196 -0
  29. figrecipe/_dev/browser/_highlight.py +105 -0
  30. figrecipe/_dev/browser/_narration.py +237 -0
  31. figrecipe/_dev/browser/_recorder.py +446 -0
  32. figrecipe/_dev/browser/_utils.py +178 -0
  33. figrecipe/_dev/browser/_video_trim/__init__.py +152 -0
  34. figrecipe/_dev/browser/_video_trim/_detection.py +223 -0
  35. figrecipe/_dev/browser/_video_trim/_markers.py +140 -0
  36. figrecipe/_editor/__init__.py +36 -36
  37. figrecipe/_editor/_bbox/_extract.py +155 -9
  38. figrecipe/_editor/_bbox/_extract_text.py +124 -0
  39. figrecipe/_editor/_call_overrides.py +183 -0
  40. figrecipe/_editor/_datatable_plot_handlers.py +249 -0
  41. figrecipe/_editor/_figure_layout.py +211 -0
  42. figrecipe/_editor/_flask_app.py +157 -16
  43. figrecipe/_editor/_helpers.py +17 -8
  44. figrecipe/_editor/_hitmap/_detect.py +89 -32
  45. figrecipe/_editor/_hitmap_main.py +4 -4
  46. figrecipe/_editor/_overrides.py +4 -1
  47. figrecipe/_editor/_plot_types_registry.py +190 -0
  48. figrecipe/_editor/_render_overrides.py +38 -11
  49. figrecipe/_editor/_renderer.py +46 -1
  50. figrecipe/_editor/_routes_annotation.py +114 -0
  51. figrecipe/_editor/_routes_axis.py +35 -6
  52. figrecipe/_editor/_routes_captions.py +130 -0
  53. figrecipe/_editor/_routes_composition.py +270 -0
  54. figrecipe/_editor/_routes_core.py +15 -173
  55. figrecipe/_editor/_routes_datatable.py +364 -0
  56. figrecipe/_editor/_routes_element.py +37 -19
  57. figrecipe/_editor/_routes_files.py +443 -0
  58. figrecipe/_editor/_routes_image.py +200 -0
  59. figrecipe/_editor/_routes_snapshot.py +94 -0
  60. figrecipe/_editor/_routes_style.py +28 -8
  61. figrecipe/_editor/_templates/__init__.py +40 -2
  62. figrecipe/_editor/_templates/_html.py +97 -103
  63. figrecipe/_editor/_templates/_html_components/__init__.py +13 -0
  64. figrecipe/_editor/_templates/_html_components/_composition_toolbar.py +79 -0
  65. figrecipe/_editor/_templates/_html_components/_file_browser.py +41 -0
  66. figrecipe/_editor/_templates/_html_datatable.py +92 -0
  67. figrecipe/_editor/_templates/_scripts/__init__.py +58 -0
  68. figrecipe/_editor/_templates/_scripts/_accordion.py +328 -0
  69. figrecipe/_editor/_templates/_scripts/_annotation_drag.py +504 -0
  70. figrecipe/_editor/_templates/_scripts/_api.py +1 -1
  71. figrecipe/_editor/_templates/_scripts/_canvas_context_menu.py +182 -0
  72. figrecipe/_editor/_templates/_scripts/_captions.py +231 -0
  73. figrecipe/_editor/_templates/_scripts/_composition.py +283 -0
  74. figrecipe/_editor/_templates/_scripts/_core.py +94 -37
  75. figrecipe/_editor/_templates/_scripts/_datatable/__init__.py +59 -0
  76. figrecipe/_editor/_templates/_scripts/_datatable/_cell_edit.py +97 -0
  77. figrecipe/_editor/_templates/_scripts/_datatable/_clipboard.py +164 -0
  78. figrecipe/_editor/_templates/_scripts/_datatable/_context_menu.py +221 -0
  79. figrecipe/_editor/_templates/_scripts/_datatable/_core.py +150 -0
  80. figrecipe/_editor/_templates/_scripts/_datatable/_editable.py +511 -0
  81. figrecipe/_editor/_templates/_scripts/_datatable/_import.py +161 -0
  82. figrecipe/_editor/_templates/_scripts/_datatable/_plot.py +261 -0
  83. figrecipe/_editor/_templates/_scripts/_datatable/_selection.py +438 -0
  84. figrecipe/_editor/_templates/_scripts/_datatable/_table.py +256 -0
  85. figrecipe/_editor/_templates/_scripts/_datatable/_tabs.py +354 -0
  86. figrecipe/_editor/_templates/_scripts/_element_editor.py +17 -2
  87. figrecipe/_editor/_templates/_scripts/_files.py +274 -40
  88. figrecipe/_editor/_templates/_scripts/_files_context_menu.py +240 -0
  89. figrecipe/_editor/_templates/_scripts/_hitmap.py +87 -84
  90. figrecipe/_editor/_templates/_scripts/_image_drop.py +428 -0
  91. figrecipe/_editor/_templates/_scripts/_legend_drag.py +5 -0
  92. figrecipe/_editor/_templates/_scripts/_multi_select.py +198 -0
  93. figrecipe/_editor/_templates/_scripts/_panel_drag.py +219 -48
  94. figrecipe/_editor/_templates/_scripts/_panel_drag_snapshot.py +33 -0
  95. figrecipe/_editor/_templates/_scripts/_panel_position.py +238 -54
  96. figrecipe/_editor/_templates/_scripts/_panel_resize.py +230 -0
  97. figrecipe/_editor/_templates/_scripts/_panel_snap.py +307 -0
  98. figrecipe/_editor/_templates/_scripts/_region_select.py +255 -0
  99. figrecipe/_editor/_templates/_scripts/_selection.py +8 -1
  100. figrecipe/_editor/_templates/_scripts/_sync.py +242 -0
  101. figrecipe/_editor/_templates/_scripts/_undo_redo.py +348 -0
  102. figrecipe/_editor/_templates/_scripts/_zoom.py +52 -19
  103. figrecipe/_editor/_templates/_styles/__init__.py +9 -0
  104. figrecipe/_editor/_templates/_styles/_base.py +47 -0
  105. figrecipe/_editor/_templates/_styles/_buttons.py +127 -6
  106. figrecipe/_editor/_templates/_styles/_composition.py +87 -0
  107. figrecipe/_editor/_templates/_styles/_controls.py +168 -3
  108. figrecipe/_editor/_templates/_styles/_datatable/__init__.py +40 -0
  109. figrecipe/_editor/_templates/_styles/_datatable/_editable.py +203 -0
  110. figrecipe/_editor/_templates/_styles/_datatable/_panel.py +268 -0
  111. figrecipe/_editor/_templates/_styles/_datatable/_table.py +479 -0
  112. figrecipe/_editor/_templates/_styles/_datatable/_toolbar.py +384 -0
  113. figrecipe/_editor/_templates/_styles/_datatable/_vars.py +123 -0
  114. figrecipe/_editor/_templates/_styles/_dynamic_props.py +5 -5
  115. figrecipe/_editor/_templates/_styles/_file_browser.py +466 -0
  116. figrecipe/_editor/_templates/_styles/_forms.py +98 -0
  117. figrecipe/_editor/_templates/_styles/_hitmap.py +7 -0
  118. figrecipe/_editor/_templates/_styles/_modals.py +29 -0
  119. figrecipe/_editor/_templates/_styles/_overlays.py +5 -5
  120. figrecipe/_editor/_templates/_styles/_preview.py +213 -8
  121. figrecipe/_editor/_templates/_styles/_spinner.py +117 -0
  122. figrecipe/_editor/static/audio/click.mp3 +0 -0
  123. figrecipe/_editor/static/click.mp3 +0 -0
  124. figrecipe/_editor/static/icons/favicon.ico +0 -0
  125. figrecipe/_integrations/__init__.py +17 -0
  126. figrecipe/_integrations/_scitex_stats.py +298 -0
  127. figrecipe/_params/_DECORATION_METHODS.py +2 -0
  128. figrecipe/_recorder.py +28 -3
  129. figrecipe/_reproducer/_core.py +60 -49
  130. figrecipe/_utils/__init__.py +3 -0
  131. figrecipe/_utils/_bundle.py +205 -0
  132. figrecipe/_wrappers/_axes.py +150 -2
  133. figrecipe/_wrappers/_caption_generator.py +218 -0
  134. figrecipe/_wrappers/_figure.py +26 -1
  135. figrecipe/_wrappers/_stat_annotation.py +274 -0
  136. figrecipe/styles/_style_applier.py +10 -2
  137. figrecipe/styles/presets/SCITEX.yaml +11 -4
  138. {figrecipe-0.7.4.dist-info → figrecipe-0.9.0.dist-info}/METADATA +144 -146
  139. figrecipe-0.9.0.dist-info/RECORD +277 -0
  140. figrecipe-0.9.0.dist-info/entry_points.txt +2 -0
  141. figrecipe-0.7.4.dist-info/RECORD +0 -188
  142. {figrecipe-0.7.4.dist-info → figrecipe-0.9.0.dist-info}/WHEEL +0 -0
  143. {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