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.
Files changed (269) hide show
  1. figrecipe/__init__.py +161 -1030
  2. figrecipe/__main__.py +12 -0
  3. figrecipe/_api/__init__.py +48 -0
  4. figrecipe/_api/_extract.py +108 -0
  5. figrecipe/_api/_notebook.py +61 -0
  6. figrecipe/_api/_panel.py +113 -0
  7. figrecipe/_api/_save.py +287 -0
  8. figrecipe/_api/_seaborn_proxy.py +34 -0
  9. figrecipe/_api/_style_manager.py +153 -0
  10. figrecipe/_api/_subplots.py +333 -0
  11. figrecipe/_api/_validate.py +82 -0
  12. figrecipe/_cli/__init__.py +7 -0
  13. figrecipe/_cli/_compose.py +87 -0
  14. figrecipe/_cli/_convert.py +117 -0
  15. figrecipe/_cli/_crop.py +82 -0
  16. figrecipe/_cli/_edit.py +70 -0
  17. figrecipe/_cli/_extract.py +128 -0
  18. figrecipe/_cli/_fonts.py +47 -0
  19. figrecipe/_cli/_info.py +67 -0
  20. figrecipe/_cli/_main.py +58 -0
  21. figrecipe/_cli/_reproduce.py +79 -0
  22. figrecipe/_cli/_style.py +77 -0
  23. figrecipe/_cli/_validate.py +66 -0
  24. figrecipe/_cli/_version.py +50 -0
  25. figrecipe/_composition/__init__.py +32 -0
  26. figrecipe/_composition/_alignment.py +452 -0
  27. figrecipe/_composition/_compose.py +179 -0
  28. figrecipe/_composition/_import_axes.py +127 -0
  29. figrecipe/_composition/_visibility.py +125 -0
  30. figrecipe/_dev/__init__.py +4 -93
  31. figrecipe/_dev/_plotters.py +76 -0
  32. figrecipe/_dev/_run_demos.py +56 -0
  33. figrecipe/_dev/browser/__init__.py +69 -0
  34. figrecipe/_dev/browser/_audio.py +240 -0
  35. figrecipe/_dev/browser/_caption.py +356 -0
  36. figrecipe/_dev/browser/_click_effect.py +146 -0
  37. figrecipe/_dev/browser/_cursor.py +196 -0
  38. figrecipe/_dev/browser/_highlight.py +105 -0
  39. figrecipe/_dev/browser/_narration.py +237 -0
  40. figrecipe/_dev/browser/_recorder.py +446 -0
  41. figrecipe/_dev/browser/_utils.py +178 -0
  42. figrecipe/_dev/browser/_video_trim/__init__.py +152 -0
  43. figrecipe/_dev/browser/_video_trim/_detection.py +223 -0
  44. figrecipe/_dev/browser/_video_trim/_markers.py +140 -0
  45. figrecipe/_dev/demo_plotters/__init__.py +35 -166
  46. figrecipe/_dev/demo_plotters/_categories.py +81 -0
  47. figrecipe/_dev/demo_plotters/_figure_creators.py +119 -0
  48. figrecipe/_dev/demo_plotters/_helpers.py +31 -0
  49. figrecipe/_dev/demo_plotters/_registry.py +50 -0
  50. figrecipe/_dev/demo_plotters/bar_categorical/__init__.py +4 -0
  51. figrecipe/_dev/demo_plotters/contour_surface/__init__.py +4 -0
  52. figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
  53. figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
  54. figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
  55. figrecipe/_dev/demo_plotters/{plot_plot.py → line_curve/plot_plot.py} +3 -2
  56. figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
  57. figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
  58. figrecipe/_dev/demo_plotters/{plot_pie.py → special/plot_pie.py} +5 -1
  59. figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
  60. figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
  61. figrecipe/_editor/__init__.py +61 -13
  62. figrecipe/_editor/_bbox/__init__.py +43 -0
  63. figrecipe/_editor/_bbox/_collections.py +177 -0
  64. figrecipe/_editor/_bbox/_elements.py +159 -0
  65. figrecipe/_editor/_bbox/_extract.py +402 -0
  66. figrecipe/_editor/_bbox/_extract_axes.py +370 -0
  67. figrecipe/_editor/_bbox/_extract_text.py +466 -0
  68. figrecipe/_editor/_bbox/_lines.py +173 -0
  69. figrecipe/_editor/_bbox/_transforms.py +146 -0
  70. figrecipe/_editor/_call_overrides.py +183 -0
  71. figrecipe/_editor/_datatable_plot_handlers.py +249 -0
  72. figrecipe/_editor/_figure_layout.py +211 -0
  73. figrecipe/_editor/_flask_app.py +200 -1030
  74. figrecipe/_editor/_helpers.py +251 -0
  75. figrecipe/_editor/_hitmap/__init__.py +76 -0
  76. figrecipe/_editor/_hitmap/_artists/__init__.py +21 -0
  77. figrecipe/_editor/_hitmap/_artists/_collections.py +345 -0
  78. figrecipe/_editor/_hitmap/_artists/_images.py +68 -0
  79. figrecipe/_editor/_hitmap/_artists/_lines.py +107 -0
  80. figrecipe/_editor/_hitmap/_artists/_patches.py +163 -0
  81. figrecipe/_editor/_hitmap/_artists/_text.py +190 -0
  82. figrecipe/_editor/_hitmap/_colors.py +181 -0
  83. figrecipe/_editor/_hitmap/_detect.py +194 -0
  84. figrecipe/_editor/_hitmap/_restore.py +154 -0
  85. figrecipe/_editor/_hitmap_main.py +182 -0
  86. figrecipe/_editor/_overrides.py +4 -1
  87. figrecipe/_editor/_plot_types_registry.py +190 -0
  88. figrecipe/_editor/_preferences.py +135 -0
  89. figrecipe/_editor/_render_overrides.py +507 -0
  90. figrecipe/_editor/_renderer.py +81 -186
  91. figrecipe/_editor/_routes_annotation.py +114 -0
  92. figrecipe/_editor/_routes_axis.py +482 -0
  93. figrecipe/_editor/_routes_captions.py +130 -0
  94. figrecipe/_editor/_routes_composition.py +270 -0
  95. figrecipe/_editor/_routes_core.py +126 -0
  96. figrecipe/_editor/_routes_datatable.py +364 -0
  97. figrecipe/_editor/_routes_element.py +335 -0
  98. figrecipe/_editor/_routes_files.py +443 -0
  99. figrecipe/_editor/_routes_image.py +200 -0
  100. figrecipe/_editor/_routes_snapshot.py +94 -0
  101. figrecipe/_editor/_routes_style.py +243 -0
  102. figrecipe/_editor/_templates/__init__.py +116 -1
  103. figrecipe/_editor/_templates/_html.py +154 -64
  104. figrecipe/_editor/_templates/_html_components/__init__.py +13 -0
  105. figrecipe/_editor/_templates/_html_components/_composition_toolbar.py +79 -0
  106. figrecipe/_editor/_templates/_html_components/_file_browser.py +41 -0
  107. figrecipe/_editor/_templates/_html_datatable.py +92 -0
  108. figrecipe/_editor/_templates/_scripts/__init__.py +178 -0
  109. figrecipe/_editor/_templates/_scripts/_accordion.py +328 -0
  110. figrecipe/_editor/_templates/_scripts/_annotation_drag.py +504 -0
  111. figrecipe/_editor/_templates/_scripts/_api.py +228 -0
  112. figrecipe/_editor/_templates/_scripts/_canvas_context_menu.py +182 -0
  113. figrecipe/_editor/_templates/_scripts/_captions.py +231 -0
  114. figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
  115. figrecipe/_editor/_templates/_scripts/_composition.py +283 -0
  116. figrecipe/_editor/_templates/_scripts/_core.py +493 -0
  117. figrecipe/_editor/_templates/_scripts/_datatable/__init__.py +59 -0
  118. figrecipe/_editor/_templates/_scripts/_datatable/_cell_edit.py +97 -0
  119. figrecipe/_editor/_templates/_scripts/_datatable/_clipboard.py +164 -0
  120. figrecipe/_editor/_templates/_scripts/_datatable/_context_menu.py +221 -0
  121. figrecipe/_editor/_templates/_scripts/_datatable/_core.py +150 -0
  122. figrecipe/_editor/_templates/_scripts/_datatable/_editable.py +511 -0
  123. figrecipe/_editor/_templates/_scripts/_datatable/_import.py +161 -0
  124. figrecipe/_editor/_templates/_scripts/_datatable/_plot.py +261 -0
  125. figrecipe/_editor/_templates/_scripts/_datatable/_selection.py +438 -0
  126. figrecipe/_editor/_templates/_scripts/_datatable/_table.py +256 -0
  127. figrecipe/_editor/_templates/_scripts/_datatable/_tabs.py +354 -0
  128. figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
  129. figrecipe/_editor/_templates/_scripts/_element_editor.py +325 -0
  130. figrecipe/_editor/_templates/_scripts/_files.py +429 -0
  131. figrecipe/_editor/_templates/_scripts/_files_context_menu.py +240 -0
  132. figrecipe/_editor/_templates/_scripts/_hitmap.py +512 -0
  133. figrecipe/_editor/_templates/_scripts/_image_drop.py +428 -0
  134. figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
  135. figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
  136. figrecipe/_editor/_templates/_scripts/_legend_drag.py +270 -0
  137. figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
  138. figrecipe/_editor/_templates/_scripts/_multi_select.py +198 -0
  139. figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
  140. figrecipe/_editor/_templates/_scripts/_panel_drag.py +505 -0
  141. figrecipe/_editor/_templates/_scripts/_panel_drag_snapshot.py +33 -0
  142. figrecipe/_editor/_templates/_scripts/_panel_position.py +463 -0
  143. figrecipe/_editor/_templates/_scripts/_panel_resize.py +230 -0
  144. figrecipe/_editor/_templates/_scripts/_panel_snap.py +307 -0
  145. figrecipe/_editor/_templates/_scripts/_region_select.py +255 -0
  146. figrecipe/_editor/_templates/_scripts/_selection.py +244 -0
  147. figrecipe/_editor/_templates/_scripts/_sync.py +242 -0
  148. figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
  149. figrecipe/_editor/_templates/_scripts/_undo_redo.py +348 -0
  150. figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
  151. figrecipe/_editor/_templates/_scripts/_zoom.py +212 -0
  152. figrecipe/_editor/_templates/_styles/__init__.py +78 -0
  153. figrecipe/_editor/_templates/_styles/_base.py +111 -0
  154. figrecipe/_editor/_templates/_styles/_buttons.py +327 -0
  155. figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
  156. figrecipe/_editor/_templates/_styles/_composition.py +87 -0
  157. figrecipe/_editor/_templates/_styles/_controls.py +430 -0
  158. figrecipe/_editor/_templates/_styles/_datatable/__init__.py +40 -0
  159. figrecipe/_editor/_templates/_styles/_datatable/_editable.py +203 -0
  160. figrecipe/_editor/_templates/_styles/_datatable/_panel.py +268 -0
  161. figrecipe/_editor/_templates/_styles/_datatable/_table.py +479 -0
  162. figrecipe/_editor/_templates/_styles/_datatable/_toolbar.py +384 -0
  163. figrecipe/_editor/_templates/_styles/_datatable/_vars.py +123 -0
  164. figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
  165. figrecipe/_editor/_templates/_styles/_file_browser.py +466 -0
  166. figrecipe/_editor/_templates/_styles/_forms.py +224 -0
  167. figrecipe/_editor/_templates/_styles/_hitmap.py +191 -0
  168. figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
  169. figrecipe/_editor/_templates/_styles/_labels.py +118 -0
  170. figrecipe/_editor/_templates/_styles/_modals.py +127 -0
  171. figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
  172. figrecipe/_editor/_templates/_styles/_preview.py +430 -0
  173. figrecipe/_editor/_templates/_styles/_selection.py +73 -0
  174. figrecipe/_editor/_templates/_styles/_spinner.py +117 -0
  175. figrecipe/_editor/static/audio/click.mp3 +0 -0
  176. figrecipe/_editor/static/click.mp3 +0 -0
  177. figrecipe/_editor/static/icons/favicon.ico +0 -0
  178. figrecipe/_integrations/__init__.py +17 -0
  179. figrecipe/_integrations/_scitex_stats.py +298 -0
  180. figrecipe/_params/_DECORATION_METHODS.py +8 -0
  181. figrecipe/_recorder.py +63 -109
  182. figrecipe/_recorder_utils.py +124 -0
  183. figrecipe/_reproducer/__init__.py +18 -0
  184. figrecipe/_reproducer/_core.py +509 -0
  185. figrecipe/_reproducer/_custom_plots.py +279 -0
  186. figrecipe/_reproducer/_seaborn.py +100 -0
  187. figrecipe/_reproducer/_violin.py +186 -0
  188. figrecipe/_signatures/_kwargs.py +273 -0
  189. figrecipe/_signatures/_loader.py +21 -423
  190. figrecipe/_signatures/_parsing.py +147 -0
  191. figrecipe/_utils/__init__.py +3 -0
  192. figrecipe/_utils/_bundle.py +205 -0
  193. figrecipe/_wrappers/_axes.py +252 -895
  194. figrecipe/_wrappers/_axes_helpers.py +136 -0
  195. figrecipe/_wrappers/_axes_plots.py +418 -0
  196. figrecipe/_wrappers/_axes_seaborn.py +157 -0
  197. figrecipe/_wrappers/_caption_generator.py +218 -0
  198. figrecipe/_wrappers/_figure.py +188 -1
  199. figrecipe/_wrappers/_panel_labels.py +127 -0
  200. figrecipe/_wrappers/_plot_helpers.py +143 -0
  201. figrecipe/_wrappers/_stat_annotation.py +274 -0
  202. figrecipe/_wrappers/_violin_helpers.py +180 -0
  203. figrecipe/styles/__init__.py +8 -6
  204. figrecipe/styles/_dotdict.py +72 -0
  205. figrecipe/styles/_finalize.py +134 -0
  206. figrecipe/styles/_fonts.py +77 -0
  207. figrecipe/styles/_kwargs_converter.py +178 -0
  208. figrecipe/styles/_plot_styles.py +209 -0
  209. figrecipe/styles/_style_applier.py +42 -480
  210. figrecipe/styles/_style_loader.py +16 -192
  211. figrecipe/styles/_themes.py +151 -0
  212. figrecipe/styles/presets/MATPLOTLIB.yaml +2 -1
  213. figrecipe/styles/presets/SCITEX.yaml +40 -28
  214. figrecipe-0.9.0.dist-info/METADATA +427 -0
  215. figrecipe-0.9.0.dist-info/RECORD +277 -0
  216. figrecipe-0.9.0.dist-info/entry_points.txt +2 -0
  217. figrecipe/_editor/_bbox.py +0 -978
  218. figrecipe/_editor/_hitmap.py +0 -937
  219. figrecipe/_editor/_templates/_scripts.py +0 -2778
  220. figrecipe/_editor/_templates/_styles.py +0 -1326
  221. figrecipe/_reproducer.py +0 -975
  222. figrecipe-0.6.0.dist-info/METADATA +0 -394
  223. figrecipe-0.6.0.dist-info/RECORD +0 -90
  224. /figrecipe/_dev/demo_plotters/{plot_bar.py → bar_categorical/plot_bar.py} +0 -0
  225. /figrecipe/_dev/demo_plotters/{plot_barh.py → bar_categorical/plot_barh.py} +0 -0
  226. /figrecipe/_dev/demo_plotters/{plot_contour.py → contour_surface/plot_contour.py} +0 -0
  227. /figrecipe/_dev/demo_plotters/{plot_contourf.py → contour_surface/plot_contourf.py} +0 -0
  228. /figrecipe/_dev/demo_plotters/{plot_tricontour.py → contour_surface/plot_tricontour.py} +0 -0
  229. /figrecipe/_dev/demo_plotters/{plot_tricontourf.py → contour_surface/plot_tricontourf.py} +0 -0
  230. /figrecipe/_dev/demo_plotters/{plot_tripcolor.py → contour_surface/plot_tripcolor.py} +0 -0
  231. /figrecipe/_dev/demo_plotters/{plot_triplot.py → contour_surface/plot_triplot.py} +0 -0
  232. /figrecipe/_dev/demo_plotters/{plot_boxplot.py → distribution/plot_boxplot.py} +0 -0
  233. /figrecipe/_dev/demo_plotters/{plot_ecdf.py → distribution/plot_ecdf.py} +0 -0
  234. /figrecipe/_dev/demo_plotters/{plot_hist.py → distribution/plot_hist.py} +0 -0
  235. /figrecipe/_dev/demo_plotters/{plot_hist2d.py → distribution/plot_hist2d.py} +0 -0
  236. /figrecipe/_dev/demo_plotters/{plot_violinplot.py → distribution/plot_violinplot.py} +0 -0
  237. /figrecipe/_dev/demo_plotters/{plot_hexbin.py → image_matrix/plot_hexbin.py} +0 -0
  238. /figrecipe/_dev/demo_plotters/{plot_imshow.py → image_matrix/plot_imshow.py} +0 -0
  239. /figrecipe/_dev/demo_plotters/{plot_matshow.py → image_matrix/plot_matshow.py} +0 -0
  240. /figrecipe/_dev/demo_plotters/{plot_pcolor.py → image_matrix/plot_pcolor.py} +0 -0
  241. /figrecipe/_dev/demo_plotters/{plot_pcolormesh.py → image_matrix/plot_pcolormesh.py} +0 -0
  242. /figrecipe/_dev/demo_plotters/{plot_spy.py → image_matrix/plot_spy.py} +0 -0
  243. /figrecipe/_dev/demo_plotters/{plot_errorbar.py → line_curve/plot_errorbar.py} +0 -0
  244. /figrecipe/_dev/demo_plotters/{plot_fill.py → line_curve/plot_fill.py} +0 -0
  245. /figrecipe/_dev/demo_plotters/{plot_fill_between.py → line_curve/plot_fill_between.py} +0 -0
  246. /figrecipe/_dev/demo_plotters/{plot_fill_betweenx.py → line_curve/plot_fill_betweenx.py} +0 -0
  247. /figrecipe/_dev/demo_plotters/{plot_stackplot.py → line_curve/plot_stackplot.py} +0 -0
  248. /figrecipe/_dev/demo_plotters/{plot_stairs.py → line_curve/plot_stairs.py} +0 -0
  249. /figrecipe/_dev/demo_plotters/{plot_step.py → line_curve/plot_step.py} +0 -0
  250. /figrecipe/_dev/demo_plotters/{plot_scatter.py → scatter_points/plot_scatter.py} +0 -0
  251. /figrecipe/_dev/demo_plotters/{plot_eventplot.py → special/plot_eventplot.py} +0 -0
  252. /figrecipe/_dev/demo_plotters/{plot_loglog.py → special/plot_loglog.py} +0 -0
  253. /figrecipe/_dev/demo_plotters/{plot_semilogx.py → special/plot_semilogx.py} +0 -0
  254. /figrecipe/_dev/demo_plotters/{plot_semilogy.py → special/plot_semilogy.py} +0 -0
  255. /figrecipe/_dev/demo_plotters/{plot_stem.py → special/plot_stem.py} +0 -0
  256. /figrecipe/_dev/demo_plotters/{plot_acorr.py → spectral_signal/plot_acorr.py} +0 -0
  257. /figrecipe/_dev/demo_plotters/{plot_angle_spectrum.py → spectral_signal/plot_angle_spectrum.py} +0 -0
  258. /figrecipe/_dev/demo_plotters/{plot_cohere.py → spectral_signal/plot_cohere.py} +0 -0
  259. /figrecipe/_dev/demo_plotters/{plot_csd.py → spectral_signal/plot_csd.py} +0 -0
  260. /figrecipe/_dev/demo_plotters/{plot_magnitude_spectrum.py → spectral_signal/plot_magnitude_spectrum.py} +0 -0
  261. /figrecipe/_dev/demo_plotters/{plot_phase_spectrum.py → spectral_signal/plot_phase_spectrum.py} +0 -0
  262. /figrecipe/_dev/demo_plotters/{plot_psd.py → spectral_signal/plot_psd.py} +0 -0
  263. /figrecipe/_dev/demo_plotters/{plot_specgram.py → spectral_signal/plot_specgram.py} +0 -0
  264. /figrecipe/_dev/demo_plotters/{plot_xcorr.py → spectral_signal/plot_xcorr.py} +0 -0
  265. /figrecipe/_dev/demo_plotters/{plot_barbs.py → vector_flow/plot_barbs.py} +0 -0
  266. /figrecipe/_dev/demo_plotters/{plot_quiver.py → vector_flow/plot_quiver.py} +0 -0
  267. /figrecipe/_dev/demo_plotters/{plot_streamplot.py → vector_flow/plot_streamplot.py} +0 -0
  268. {figrecipe-0.6.0.dist-info → figrecipe-0.9.0.dist-info}/WHEEL +0 -0
  269. {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