playwright-director 0.2.0

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 (122) hide show
  1. package/README.md +860 -0
  2. package/dist/Tutorial.d.ts +108 -0
  3. package/dist/Tutorial.d.ts.map +1 -0
  4. package/dist/Tutorial.js +767 -0
  5. package/dist/Tutorial.js.map +1 -0
  6. package/dist/bin/export-transcript.d.ts +3 -0
  7. package/dist/bin/export-transcript.d.ts.map +1 -0
  8. package/dist/bin/export-transcript.js +111 -0
  9. package/dist/bin/export-transcript.js.map +1 -0
  10. package/dist/cursor.d.ts +24 -0
  11. package/dist/cursor.d.ts.map +1 -0
  12. package/dist/cursor.js +96 -0
  13. package/dist/cursor.js.map +1 -0
  14. package/dist/index.d.ts +16 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +12 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/init.d.ts +3 -0
  19. package/dist/init.d.ts.map +1 -0
  20. package/dist/init.js +183 -0
  21. package/dist/init.js.map +1 -0
  22. package/dist/merge.d.ts +26 -0
  23. package/dist/merge.d.ts.map +1 -0
  24. package/dist/merge.js +65 -0
  25. package/dist/merge.js.map +1 -0
  26. package/dist/music.d.ts +29 -0
  27. package/dist/music.d.ts.map +1 -0
  28. package/dist/music.js +107 -0
  29. package/dist/music.js.map +1 -0
  30. package/dist/overlay-html.d.ts +44 -0
  31. package/dist/overlay-html.d.ts.map +1 -0
  32. package/dist/overlay-html.js +150 -0
  33. package/dist/overlay-html.js.map +1 -0
  34. package/dist/overlay.d.ts +39 -0
  35. package/dist/overlay.d.ts.map +1 -0
  36. package/dist/overlay.js +134 -0
  37. package/dist/overlay.js.map +1 -0
  38. package/dist/postinstall.d.ts +3 -0
  39. package/dist/postinstall.d.ts.map +1 -0
  40. package/dist/postinstall.js +38 -0
  41. package/dist/postinstall.js.map +1 -0
  42. package/dist/reporter.d.ts +22 -0
  43. package/dist/reporter.d.ts.map +1 -0
  44. package/dist/reporter.js +166 -0
  45. package/dist/reporter.js.map +1 -0
  46. package/dist/site/build-site.d.ts +2 -0
  47. package/dist/site/build-site.d.ts.map +1 -0
  48. package/dist/site/build-site.js +82 -0
  49. package/dist/site/build-site.js.map +1 -0
  50. package/dist/site/embed.d.ts +48 -0
  51. package/dist/site/embed.d.ts.map +1 -0
  52. package/dist/site/embed.js +45 -0
  53. package/dist/site/embed.js.map +1 -0
  54. package/dist/site/generate-manifest.d.ts +15 -0
  55. package/dist/site/generate-manifest.d.ts.map +1 -0
  56. package/dist/site/generate-manifest.js +106 -0
  57. package/dist/site/generate-manifest.js.map +1 -0
  58. package/dist/site/scaffold.d.ts +3 -0
  59. package/dist/site/scaffold.d.ts.map +1 -0
  60. package/dist/site/scaffold.js +70 -0
  61. package/dist/site/scaffold.js.map +1 -0
  62. package/dist/site/scan-tutorials.d.ts +12 -0
  63. package/dist/site/scan-tutorials.d.ts.map +1 -0
  64. package/dist/site/scan-tutorials.js +54 -0
  65. package/dist/site/scan-tutorials.js.map +1 -0
  66. package/dist/site/types.d.ts +75 -0
  67. package/dist/site/types.d.ts.map +1 -0
  68. package/dist/site/types.js +2 -0
  69. package/dist/site/types.js.map +1 -0
  70. package/dist/skill-stamp.d.ts +21 -0
  71. package/dist/skill-stamp.d.ts.map +1 -0
  72. package/dist/skill-stamp.js +39 -0
  73. package/dist/skill-stamp.js.map +1 -0
  74. package/dist/slugify.d.ts +19 -0
  75. package/dist/slugify.d.ts.map +1 -0
  76. package/dist/slugify.js +29 -0
  77. package/dist/slugify.js.map +1 -0
  78. package/dist/stage-presets.d.ts +51 -0
  79. package/dist/stage-presets.d.ts.map +1 -0
  80. package/dist/stage-presets.js +45 -0
  81. package/dist/stage-presets.js.map +1 -0
  82. package/dist/styles.css +637 -0
  83. package/dist/timeline.d.ts +87 -0
  84. package/dist/timeline.d.ts.map +1 -0
  85. package/dist/timeline.js +108 -0
  86. package/dist/timeline.js.map +1 -0
  87. package/dist/transcript.d.ts +87 -0
  88. package/dist/transcript.d.ts.map +1 -0
  89. package/dist/transcript.js +291 -0
  90. package/dist/transcript.js.map +1 -0
  91. package/dist/tts-provider.d.ts +69 -0
  92. package/dist/tts-provider.d.ts.map +1 -0
  93. package/dist/tts-provider.js +182 -0
  94. package/dist/tts-provider.js.map +1 -0
  95. package/dist/types.d.ts +109 -0
  96. package/dist/types.d.ts.map +1 -0
  97. package/dist/types.js +2 -0
  98. package/dist/types.js.map +1 -0
  99. package/dist/voice-prerender.d.ts +41 -0
  100. package/dist/voice-prerender.d.ts.map +1 -0
  101. package/dist/voice-prerender.js +122 -0
  102. package/dist/voice-prerender.js.map +1 -0
  103. package/dist/voice.d.ts +76 -0
  104. package/dist/voice.d.ts.map +1 -0
  105. package/dist/voice.js +244 -0
  106. package/dist/voice.js.map +1 -0
  107. package/package.json +87 -0
  108. package/skills/tutorialize/SKILL.md +54 -0
  109. package/skills/tutorialize/agent.md +58 -0
  110. package/skills/tutorialize/references/api.md +489 -0
  111. package/skills/tutorialize/references/storytelling.md +193 -0
  112. package/src/styles.css +637 -0
  113. package/templates/site/public/.gitkeep +0 -0
  114. package/templates/site/public/widget.js +273 -0
  115. package/templates/site/src/components/TutorialsHome.astro +141 -0
  116. package/templates/site/src/components/VideoCard.astro +192 -0
  117. package/templates/site/src/components/VideoModal.astro +170 -0
  118. package/templates/site/src/data/.gitkeep +0 -0
  119. package/templates/site/src/layouts/Base.astro +56 -0
  120. package/templates/site/src/pages/[slug].astro +745 -0
  121. package/templates/site/src/pages/index.astro +5 -0
  122. package/templates/site/src/styles/tutorials.css +303 -0
@@ -0,0 +1,182 @@
1
+ import { exec } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import { existsSync, mkdirSync } from 'fs';
4
+ import { join } from 'path';
5
+ const execAsync = promisify(exec);
6
+ /** Get audio duration using ffprobe */
7
+ async function getAudioDuration(filePath) {
8
+ try {
9
+ const { stdout } = await execAsync(`ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${filePath}"`);
10
+ return Math.ceil(parseFloat(stdout.trim()) * 1000);
11
+ }
12
+ catch {
13
+ // Fallback: estimate based on text length (80ms per character at normal rate)
14
+ return 3000;
15
+ }
16
+ }
17
+ /**
18
+ * Command-based TTS provider. Defaults to macOS `say`, can be overridden
19
+ * via TUTORIAL_TTS_CMD env var.
20
+ * Supports placeholders: {lang}, {text}, {output}.
21
+ * If {text}/{output} are present they are substituted in-place; otherwise
22
+ * the legacy "-o <output> <text>" suffix is appended automatically.
23
+ * Examples:
24
+ * TUTORIAL_TTS_CMD='qsay --my-voice jadou -l {lang} {text} -o {output}'
25
+ * TUTORIAL_TTS_CMD='say -v Thomas' (legacy: appends -o <out> "<text>")
26
+ */
27
+ export class CommandTTSProvider {
28
+ name;
29
+ command;
30
+ options;
31
+ static VOICES = {
32
+ fr: 'Thomas',
33
+ en: 'Samantha',
34
+ ar: 'Maged'
35
+ };
36
+ static LANG_NAMES = {
37
+ fr: 'french',
38
+ en: 'english',
39
+ ar: 'arabic'
40
+ };
41
+ static _logged = false;
42
+ constructor(options) {
43
+ this.options = options;
44
+ const ttsCmd = process.env.TUTORIAL_TTS_CMD;
45
+ if (!CommandTTSProvider._logged) {
46
+ console.log(`[TTS] TUTORIAL_TTS_CMD=${ttsCmd ?? '(not set)'}`);
47
+ CommandTTSProvider._logged = true;
48
+ }
49
+ if (ttsCmd) {
50
+ const langName = CommandTTSProvider.LANG_NAMES[options.lang] || options.lang;
51
+ this.command = ttsCmd.replace('{lang}', langName);
52
+ this.name = `tts:${this.command.split(/\s/)[0]}`;
53
+ }
54
+ else {
55
+ const voice = options.voice || CommandTTSProvider.VOICES[options.lang] || 'Samantha';
56
+ const rate = Math.round((options.rate || 1) * 175);
57
+ this.command = `say -v "${voice}" -r ${rate} --data-format=LEI16@44100`;
58
+ this.name = 'macos-say';
59
+ }
60
+ }
61
+ async isAvailable() {
62
+ const binary = this.command.split(/\s/)[0];
63
+ try {
64
+ await execAsync(`which ${binary}`);
65
+ return true;
66
+ }
67
+ catch {
68
+ return false;
69
+ }
70
+ }
71
+ async synthesize(text, outputPath) {
72
+ const escapedText = text.replace(/"/g, '\\"');
73
+ const hasPlaceholders = this.command.includes('{text}') || this.command.includes('{output}');
74
+ const cmd = hasPlaceholders
75
+ ? this.command.replace('{text}', `"${escapedText}"`).replace('{output}', `"${outputPath}"`)
76
+ : `${this.command} -o "${outputPath}" "${escapedText}"`;
77
+ console.log(`[TTS] Running: ${cmd}`);
78
+ await execAsync(cmd);
79
+ const duration = await getAudioDuration(outputPath);
80
+ return { filePath: outputPath, duration };
81
+ }
82
+ }
83
+ /** Edge TTS provider using Microsoft's free TTS API */
84
+ export class EdgeTTSProvider {
85
+ name = 'edge-tts';
86
+ options;
87
+ static VOICES = {
88
+ fr: 'fr-FR-HenriNeural',
89
+ en: 'en-US-GuyNeural',
90
+ ar: 'ar-SA-HamedNeural'
91
+ };
92
+ constructor(options) {
93
+ this.options = options;
94
+ }
95
+ async isAvailable() {
96
+ try {
97
+ await execAsync('which edge-tts');
98
+ return true;
99
+ }
100
+ catch {
101
+ return false;
102
+ }
103
+ }
104
+ async synthesize(text, outputPath) {
105
+ const voice = this.options.voice || EdgeTTSProvider.VOICES[this.options.lang] || 'en-US-GuyNeural';
106
+ const rate = this.options.rate || 1;
107
+ const ratePercent = rate >= 1 ? `+${Math.round((rate - 1) * 100)}%` : `-${Math.round((1 - rate) * 100)}%`;
108
+ await execAsync(`edge-tts --voice "${voice}" --rate="${ratePercent}" --text "${text.replace(/"/g, '\\"')}" --write-media "${outputPath}"`);
109
+ const duration = await getAudioDuration(outputPath);
110
+ return { filePath: outputPath, duration };
111
+ }
112
+ }
113
+ /** Auto-detect and create the best available TTS provider */
114
+ export async function createTTSProvider(options) {
115
+ // Command provider: custom TUTORIAL_TTS_CMD or macOS say
116
+ const cmdProvider = new CommandTTSProvider(options);
117
+ if (await cmdProvider.isAvailable()) {
118
+ return cmdProvider;
119
+ }
120
+ // Fallback to edge-tts
121
+ const edgeProvider = new EdgeTTSProvider(options);
122
+ if (await edgeProvider.isAvailable()) {
123
+ return edgeProvider;
124
+ }
125
+ throw new Error('No TTS provider available. Install edge-tts: pip install edge-tts');
126
+ }
127
+ /** Manages TTS audio generation and tracking for tutorials */
128
+ export class TTSManager {
129
+ provider = null;
130
+ options;
131
+ outputDir;
132
+ segments = [];
133
+ recordingStartTime = 0;
134
+ constructor(options, outputDir) {
135
+ this.options = options;
136
+ this.outputDir = outputDir;
137
+ }
138
+ async initialize() {
139
+ if (!existsSync(this.outputDir)) {
140
+ mkdirSync(this.outputDir, { recursive: true });
141
+ }
142
+ this.provider = await createTTSProvider(this.options);
143
+ console.log(`[TTS] Using provider: ${this.provider.name}`);
144
+ }
145
+ startRecording() {
146
+ this.recordingStartTime = Date.now();
147
+ this.segments = [];
148
+ }
149
+ async speak(text) {
150
+ if (!this.provider) {
151
+ await this.initialize();
152
+ }
153
+ const timestamp = Date.now() - this.recordingStartTime;
154
+ const filename = `voice_${timestamp}.mp3`;
155
+ const filePath = join(this.outputDir, filename);
156
+ const result = await this.provider.synthesize(text, filePath);
157
+ this.segments.push({
158
+ filePath: result.filePath,
159
+ timestamp,
160
+ duration: result.duration,
161
+ text
162
+ });
163
+ return result.duration;
164
+ }
165
+ getSegments() {
166
+ return [...this.segments];
167
+ }
168
+ /** Generate a manifest file for post-processing */
169
+ async writeManifest(videoPath) {
170
+ const manifestPath = join(this.outputDir, 'audio-manifest.json');
171
+ const manifest = {
172
+ videoPath,
173
+ recordingStart: this.recordingStartTime,
174
+ segments: this.segments,
175
+ provider: this.provider?.name
176
+ };
177
+ const { writeFileSync } = await import('fs');
178
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
179
+ return manifestPath;
180
+ }
181
+ }
182
+ //# sourceMappingURL=tts-provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tts-provider.js","sourceRoot":"","sources":["../src/tts-provider.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,109 @@
1
+ export interface TutorialOptions {
2
+ /** Tutorial title shown in overlay */
3
+ title: string;
4
+ /** Test name for output files (default: auto-generated) */
5
+ testName?: string;
6
+ /** Raw test title as passed to `test(...)` — the i18n key. Used by the reporter
7
+ * to match this tutorial to its Playwright testCase without re-deriving. */
8
+ testTitle?: string;
9
+ /** Test file path (e.g., "free/01_company.init.ts") */
10
+ testFile?: string;
11
+ /** Playwright project name (e.g., "setup-free-init") */
12
+ projectName?: string;
13
+ /** Language for UI text and TTS (default: 'en') */
14
+ lang?: string;
15
+ /** Translation function — pass your i18n `t()`. Falls back to identity. */
16
+ translate?: (key: string) => string;
17
+ /** Base URL for serving audio files (default: 'http://localhost:5173') */
18
+ audioBaseUrl?: string;
19
+ /** Delay between steps in ms (default: 500) */
20
+ stepDelay?: number;
21
+ /** Duration of highlight animation in ms (default: 800) */
22
+ highlightDuration?: number;
23
+ /** Mouse movement speed - steps for smooth movement (default: 25) */
24
+ mouseSteps?: number;
25
+ /** Pause after mouse reaches target in ms (default: 300) */
26
+ pauseBeforeClick?: number;
27
+ /** Enable voice narration using Web Speech API */
28
+ enableVoice?: boolean;
29
+ /** Voice name to use (e.g., 'Google français', 'Microsoft David') */
30
+ voiceName?: string;
31
+ /** Voice rate (0.1 to 10, default: 0.9) */
32
+ voiceRate?: number;
33
+ /** Voice pitch (0 to 2, default: 1) */
34
+ voicePitch?: number;
35
+ /** Background music URL (relative to public folder or absolute URL) */
36
+ backgroundMusic?: string;
37
+ /** Background music volume (0 to 1, default: 0.15) */
38
+ musicVolume?: number;
39
+ /** Voice narration volume multiplier (default: 2.5) */
40
+ voiceVolume?: number;
41
+ /** Custom CSS to inject (overrides default styles) */
42
+ customStyles?: string;
43
+ /** Play audio in browser during test for live viewing (default: true) */
44
+ playAudioInBrowser?: boolean;
45
+ /** Feature tag (e.g., 'payroll') — sourced from @feature:* test tag */
46
+ feature?: string;
47
+ /** Named scenes, each an iframe of its own origin, shown as browser-like tabs.
48
+ * Distinct origins are what let two user profiles stay logged in at once. */
49
+ scenes?: Record<string, SceneOptions>;
50
+ /** Scene(s) active when the stage is mounted (default: the first one) */
51
+ focus?: SceneFocus;
52
+ /** Scene switch animation */
53
+ sceneTransition?: {
54
+ duration?: number;
55
+ };
56
+ /** Overlay position: TL (top-left), TR (top-right, default), BL (bottom-left), BR (bottom-right) */
57
+ overlayPosition?: OverlayPosition;
58
+ /** Variant suffix appended to testName — `<testName>-<variant>` — so a second
59
+ * recording of the same tutorial (video, timeline, transcript, screenshots)
60
+ * never overwrites the first. Defaults to process.env.TUTORIAL_VARIANT.
61
+ * The variant `'mobile'` also switches to a compact overlay and, with 2+
62
+ * scenes, a permanent split stage (all scenes visible, ratios ignored). */
63
+ variant?: string;
64
+ }
65
+ export interface SceneOptions {
66
+ /** Label shown on the scene's tab — keep it a person, not a URL */
67
+ label: string;
68
+ /** Origin that relative `goto` paths resolve against */
69
+ baseUrl?: string;
70
+ }
71
+ /** One scene fills the stage; two share it side by side.
72
+ * In an array, the first scene is the one acting — it carries the cursor. */
73
+ export type SceneFocus = string | string[];
74
+ export interface FocusOptions {
75
+ /** Flex ratio for each active scene, e.g. [30, 70]. Must match the number of scenes. */
76
+ ratio?: number[];
77
+ }
78
+ export interface StepOptions {
79
+ /** Optional description shown below step title */
80
+ description?: string;
81
+ /** Skip mouse movement for this step */
82
+ skipMouse?: boolean;
83
+ /** Custom delay after this step */
84
+ delay?: number;
85
+ /** Custom text for voice narration (if different from title/description) */
86
+ voiceText?: string;
87
+ /** Skip voice for this step */
88
+ skipVoice?: boolean;
89
+ /** Short action narration (defaults to title) */
90
+ do?: string;
91
+ /** Explanation that plays during action (two-phase timing) */
92
+ explain?: string;
93
+ /** Scene(s) this step plays on. The stage switches before the action runs,
94
+ * because a hidden scene cannot be interacted with. */
95
+ scene?: SceneFocus;
96
+ /** Override overlay position for this step */
97
+ overlayPosition?: OverlayPosition;
98
+ }
99
+ export type OverlayPosition = 'TL' | 'TR' | 'BL' | 'BR';
100
+ export type ContextStyle = 'goal' | 'clarification' | 'attention';
101
+ export interface ContextOptions {
102
+ /** Description shown below title */
103
+ text?: string;
104
+ /** Custom voice text (defaults to title + text) */
105
+ voiceText?: string;
106
+ /** Context style: goal (🎯), clarification (💡), attention (⚠️) */
107
+ style?: ContextStyle;
108
+ }
109
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Voice pre-rendering — generate the narration clips OUTSIDE of any test run,
3
+ * where TTS synthesis is slow and serial. Texts are collected from the
4
+ * artifacts that record them verbatim:
5
+ *
6
+ * - `tutorials/transcripts/*.md` — one per tutorial, survives every run;
7
+ * - `tutorials/output/*_timeline.json` — fresh but wiped by Playwright runs.
8
+ */
9
+ export interface NarrationText {
10
+ lang: string;
11
+ text: string;
12
+ }
13
+ export interface VoiceSources {
14
+ timelineDir?: string;
15
+ transcriptDir?: string;
16
+ }
17
+ /** Same naming scheme as the runtime voice cache (voice.ts). */
18
+ export declare function audioFilename(lang: string, text: string): string;
19
+ /** All known narration texts, deduplicated. */
20
+ export declare function collectVoices(sources?: VoiceSources): NarrationText[];
21
+ export interface RegenOptions extends VoiceSources {
22
+ lang?: string;
23
+ /** Parallel TTS syntheses (default 2) */
24
+ workers?: number;
25
+ /** Re-synthesize cached clips too (after a voice settings change). Default: missing only. */
26
+ force?: boolean;
27
+ }
28
+ /**
29
+ * Pre-render the voice cache without running any test: collect every known
30
+ * narration text, synthesize the missing clips (all of them with `force`) with
31
+ * the CURRENT TTS settings, in parallel. The audio dir may have been deleted
32
+ * entirely — it is recreated. Videos are not remixed; a `TUTORIAL_MODE=true`
33
+ * run picks the clips up from cache.
34
+ */
35
+ export declare function regenVoices(audioDir: string, options?: RegenOptions): Promise<{
36
+ total: number;
37
+ cached: number;
38
+ done: number;
39
+ failed: number;
40
+ }>;
41
+ //# sourceMappingURL=voice-prerender.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"voice-prerender.d.ts","sourceRoot":"","sources":["../src/voice-prerender.ts"],"names":[],"mappings":"AAOA;;;;;;;GAOG;AACH,MAAM,WAAW,aAAa;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,YAAY;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,gEAAgE;AAChE,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAIhE;AAoCD,+CAA+C;AAC/C,wBAAgB,aAAa,CAAC,OAAO,GAAE,YAAiB,GAAG,aAAa,EAAE,CAiBzE;AAUD,MAAM,WAAW,YAAa,SAAQ,YAAY;IACjD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,yCAAyC;IACzC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6FAA6F;IAC7F,KAAK,CAAC,EAAE,OAAO,CAAC;CAChB;AAED;;;;;;GAMG;AACH,wBAAsB,WAAW,CAChC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,YAAiB,GACxB,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CA4C1E"}
@@ -0,0 +1,122 @@
1
+ import { createHash } from 'crypto';
2
+ import { existsSync, readFileSync, rmSync, mkdirSync, readdirSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { createTTSProvider } from './tts-provider.js';
5
+ import { parseTranscript } from './transcript.js';
6
+ /** Same naming scheme as the runtime voice cache (voice.ts). */
7
+ export function audioFilename(lang, text) {
8
+ const hash = createHash('md5').update(`${lang}:${text}`).digest('hex').slice(0, 12);
9
+ const ext = process.platform === 'darwin' ? 'wav' : 'mp3';
10
+ return `${hash}.${ext}`;
11
+ }
12
+ function collectFromTimelines(timelineDir) {
13
+ if (!existsSync(timelineDir))
14
+ return [];
15
+ const entries = [];
16
+ for (const file of readdirSync(timelineDir).filter((f) => f.endsWith('_timeline.json'))) {
17
+ try {
18
+ const data = JSON.parse(readFileSync(join(timelineDir, file), 'utf-8'));
19
+ for (const step of data.steps) {
20
+ if (step.audioFile && step.text?.trim())
21
+ entries.push({ lang: data.lang, text: step.text });
22
+ }
23
+ }
24
+ catch {
25
+ console.warn(`[Voice] Skipping unreadable timeline: ${file}`);
26
+ }
27
+ }
28
+ return entries;
29
+ }
30
+ function collectFromTranscripts(transcriptDir) {
31
+ if (!existsSync(transcriptDir))
32
+ return [];
33
+ const entries = [];
34
+ for (const file of readdirSync(transcriptDir).filter((f) => f.endsWith('.md'))) {
35
+ try {
36
+ const markdown = readFileSync(join(transcriptDir, file), 'utf-8');
37
+ const lang = markdown.match(/^- \*\*Language:\*\* (\S+)/m)?.[1];
38
+ if (!lang)
39
+ continue;
40
+ for (const entry of parseTranscript(markdown)) {
41
+ if (entry.text.trim())
42
+ entries.push({ lang, text: entry.text });
43
+ }
44
+ }
45
+ catch {
46
+ console.warn(`[Voice] Skipping unreadable transcript: ${file}`);
47
+ }
48
+ }
49
+ return entries;
50
+ }
51
+ /** All known narration texts, deduplicated. */
52
+ export function collectVoices(sources = {}) {
53
+ const timelineDir = sources.timelineDir ?? process.env.TUTORIAL_OUTPUT_DIR ?? join(process.cwd(), 'tutorials', 'output');
54
+ const transcriptDir = sources.transcriptDir ??
55
+ process.env.TUTORIAL_TRANSCRIPT_DIR ??
56
+ join(process.cwd(), 'tutorials', 'transcripts');
57
+ const seen = new Set();
58
+ const all = [];
59
+ for (const entry of [...collectFromTranscripts(transcriptDir), ...collectFromTimelines(timelineDir)]) {
60
+ const key = `${entry.lang}:${entry.text}`;
61
+ if (seen.has(key))
62
+ continue;
63
+ seen.add(key);
64
+ all.push(entry);
65
+ }
66
+ return all;
67
+ }
68
+ async function pool(items, limit, fn) {
69
+ const queue = [...items];
70
+ const workers = Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, async () => {
71
+ while (queue.length)
72
+ await fn(queue.shift());
73
+ });
74
+ await Promise.all(workers);
75
+ }
76
+ /**
77
+ * Pre-render the voice cache without running any test: collect every known
78
+ * narration text, synthesize the missing clips (all of them with `force`) with
79
+ * the CURRENT TTS settings, in parallel. The audio dir may have been deleted
80
+ * entirely — it is recreated. Videos are not remixed; a `TUTORIAL_MODE=true`
81
+ * run picks the clips up from cache.
82
+ */
83
+ export async function regenVoices(audioDir, options = {}) {
84
+ const all = collectVoices(options).filter((e) => !options.lang || e.lang === options.lang);
85
+ const todo = options.force
86
+ ? all
87
+ : all.filter((e) => !existsSync(join(audioDir, audioFilename(e.lang, e.text))));
88
+ const result = { total: all.length, cached: all.length - todo.length, done: 0, failed: 0 };
89
+ console.log(`[Voice] ${all.length} narration(s) known, ${todo.length} to synthesize` +
90
+ (options.force ? ' (force)' : `, ${result.cached} already cached`));
91
+ if (todo.length === 0)
92
+ return result;
93
+ mkdirSync(audioDir, { recursive: true });
94
+ // One provider per language — the provider resolves voice/rate from the env.
95
+ const providers = new Map();
96
+ for (const lang of new Set(todo.map((e) => e.lang))) {
97
+ const provider = await createTTSProvider({
98
+ lang,
99
+ voice: process.env.TUTORIAL_VOICE_NAME || undefined,
100
+ rate: 1.0
101
+ });
102
+ console.log(`[Voice] ${lang}: TTS provider ${provider.name}`);
103
+ providers.set(lang, provider);
104
+ }
105
+ await pool(todo, options.workers ?? 2, async ({ lang, text }) => {
106
+ const filePath = join(audioDir, audioFilename(lang, text));
107
+ try {
108
+ // Remove first: a failed synthesis must not leave a stale clip
109
+ // pretending to be regenerated.
110
+ rmSync(filePath, { force: true });
111
+ await providers.get(lang).synthesize(text, filePath);
112
+ result.done++;
113
+ console.log(` ✔ [${result.done}/${todo.length}] ${lang} ${text.slice(0, 60)}${text.length > 60 ? '…' : ''}`);
114
+ }
115
+ catch (error) {
116
+ result.failed++;
117
+ console.error(` ✖ ${lang} ${text.slice(0, 40)}… ${error}`);
118
+ }
119
+ });
120
+ return result;
121
+ }
122
+ //# sourceMappingURL=voice-prerender.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"voice-prerender.js","sourceRoot":"","sources":["../src/voice-prerender.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,76 @@
1
+ import type { Page } from '@playwright/test';
2
+ export interface VoiceOptions {
3
+ lang: string;
4
+ voiceName: string;
5
+ voiceRate: number;
6
+ voicePitch: number;
7
+ playInBrowser?: boolean;
8
+ audioBaseUrl?: string;
9
+ }
10
+ export interface PreloadedAudio {
11
+ text: string;
12
+ filename: string;
13
+ filePath: string;
14
+ url: string;
15
+ durationMs: number;
16
+ }
17
+ /**
18
+ * TTS voice generation for tutorials.
19
+ * Pre-generates audio files and tracks their durations.
20
+ * Optionally plays audio in browser during test.
21
+ */
22
+ export declare class TutorialVoice {
23
+ private page;
24
+ private options;
25
+ private ttsProvider;
26
+ private preloadedAudio;
27
+ constructor(page: Page, options: VoiceOptions);
28
+ switchPage(page: Page): void;
29
+ /**
30
+ * Pre-generate a single audio file (non-blocking).
31
+ * Returns a promise that resolves when preload is complete.
32
+ * No-op when TUTORIAL_MODE is not enabled.
33
+ */
34
+ preloadSingle(text: string): Promise<void>;
35
+ /**
36
+ * Pre-generate all audio files before the test runs.
37
+ * No-op when TUTORIAL_MODE is not enabled.
38
+ */
39
+ preload(texts: string[]): Promise<void>;
40
+ /**
41
+ * Get audio info for a text (must be preloaded first)
42
+ */
43
+ getAudio(text: string): PreloadedAudio | undefined;
44
+ /**
45
+ * Get duration in ms for a text (must be preloaded)
46
+ */
47
+ getDuration(text: string): number;
48
+ /**
49
+ * Get the filename for a text (for timeline)
50
+ */
51
+ getFilename(text: string): string;
52
+ /**
53
+ * Start audio playback without waiting for it to finish.
54
+ * Returns the clip duration in ms — the caller owns the wall clock
55
+ * (action offset + clamp to duration), so headed playback and the
56
+ * ffmpeg merge stay in sync.
57
+ * No-op when TUTORIAL_MODE is not enabled (returns 0).
58
+ */
59
+ startPlayback(text: string): Promise<number>;
60
+ /**
61
+ * Play audio in browser (optional, for live viewing)
62
+ * Returns duration in ms
63
+ * Will generate audio on-demand if not preloaded.
64
+ * No-op when TUTORIAL_MODE is not enabled (returns 0).
65
+ */
66
+ play(text: string): Promise<number>;
67
+ /**
68
+ * Generate audio for a single text on demand
69
+ */
70
+ private generateAudio;
71
+ /**
72
+ * Get audio duration using ffprobe or file stats
73
+ */
74
+ private getAudioDuration;
75
+ }
76
+ //# sourceMappingURL=voice.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"voice.d.ts","sourceRoot":"","sources":["../src/voice.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAQ7C,MAAM,WAAW,YAAY;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB;AAeD,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;CACnB;AAED;;;;GAIG;AACH,qBAAa,aAAa;IACzB,OAAO,CAAC,IAAI,CAAO;IACnB,OAAO,CAAC,OAAO,CAAyB;IACxC,OAAO,CAAC,WAAW,CAA4B;IAC/C,OAAO,CAAC,cAAc,CAA0C;gBAEpD,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,YAAY;IAY7C,UAAU,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI;IAI5B;;;;OAIG;IACH,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAK1C;;;OAGG;IACG,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAiD7C;;OAEG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS;IAIlD;;OAEG;IACH,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAIjC;;OAEG;IACH,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAMjC;;;;;;OAMG;IACG,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAyBlD;;;;;OAKG;IACG,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAiCzC;;OAEG;YACW,aAAa;IAgD3B;;OAEG;YACW,gBAAgB;CAiB9B"}