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.
- package/README.md +860 -0
- package/dist/Tutorial.d.ts +108 -0
- package/dist/Tutorial.d.ts.map +1 -0
- package/dist/Tutorial.js +767 -0
- package/dist/Tutorial.js.map +1 -0
- package/dist/bin/export-transcript.d.ts +3 -0
- package/dist/bin/export-transcript.d.ts.map +1 -0
- package/dist/bin/export-transcript.js +111 -0
- package/dist/bin/export-transcript.js.map +1 -0
- package/dist/cursor.d.ts +24 -0
- package/dist/cursor.d.ts.map +1 -0
- package/dist/cursor.js +96 -0
- package/dist/cursor.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/init.d.ts +3 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +183 -0
- package/dist/init.js.map +1 -0
- package/dist/merge.d.ts +26 -0
- package/dist/merge.d.ts.map +1 -0
- package/dist/merge.js +65 -0
- package/dist/merge.js.map +1 -0
- package/dist/music.d.ts +29 -0
- package/dist/music.d.ts.map +1 -0
- package/dist/music.js +107 -0
- package/dist/music.js.map +1 -0
- package/dist/overlay-html.d.ts +44 -0
- package/dist/overlay-html.d.ts.map +1 -0
- package/dist/overlay-html.js +150 -0
- package/dist/overlay-html.js.map +1 -0
- package/dist/overlay.d.ts +39 -0
- package/dist/overlay.d.ts.map +1 -0
- package/dist/overlay.js +134 -0
- package/dist/overlay.js.map +1 -0
- package/dist/postinstall.d.ts +3 -0
- package/dist/postinstall.d.ts.map +1 -0
- package/dist/postinstall.js +38 -0
- package/dist/postinstall.js.map +1 -0
- package/dist/reporter.d.ts +22 -0
- package/dist/reporter.d.ts.map +1 -0
- package/dist/reporter.js +166 -0
- package/dist/reporter.js.map +1 -0
- package/dist/site/build-site.d.ts +2 -0
- package/dist/site/build-site.d.ts.map +1 -0
- package/dist/site/build-site.js +82 -0
- package/dist/site/build-site.js.map +1 -0
- package/dist/site/embed.d.ts +48 -0
- package/dist/site/embed.d.ts.map +1 -0
- package/dist/site/embed.js +45 -0
- package/dist/site/embed.js.map +1 -0
- package/dist/site/generate-manifest.d.ts +15 -0
- package/dist/site/generate-manifest.d.ts.map +1 -0
- package/dist/site/generate-manifest.js +106 -0
- package/dist/site/generate-manifest.js.map +1 -0
- package/dist/site/scaffold.d.ts +3 -0
- package/dist/site/scaffold.d.ts.map +1 -0
- package/dist/site/scaffold.js +70 -0
- package/dist/site/scaffold.js.map +1 -0
- package/dist/site/scan-tutorials.d.ts +12 -0
- package/dist/site/scan-tutorials.d.ts.map +1 -0
- package/dist/site/scan-tutorials.js +54 -0
- package/dist/site/scan-tutorials.js.map +1 -0
- package/dist/site/types.d.ts +75 -0
- package/dist/site/types.d.ts.map +1 -0
- package/dist/site/types.js +2 -0
- package/dist/site/types.js.map +1 -0
- package/dist/skill-stamp.d.ts +21 -0
- package/dist/skill-stamp.d.ts.map +1 -0
- package/dist/skill-stamp.js +39 -0
- package/dist/skill-stamp.js.map +1 -0
- package/dist/slugify.d.ts +19 -0
- package/dist/slugify.d.ts.map +1 -0
- package/dist/slugify.js +29 -0
- package/dist/slugify.js.map +1 -0
- package/dist/stage-presets.d.ts +51 -0
- package/dist/stage-presets.d.ts.map +1 -0
- package/dist/stage-presets.js +45 -0
- package/dist/stage-presets.js.map +1 -0
- package/dist/styles.css +637 -0
- package/dist/timeline.d.ts +87 -0
- package/dist/timeline.d.ts.map +1 -0
- package/dist/timeline.js +108 -0
- package/dist/timeline.js.map +1 -0
- package/dist/transcript.d.ts +87 -0
- package/dist/transcript.d.ts.map +1 -0
- package/dist/transcript.js +291 -0
- package/dist/transcript.js.map +1 -0
- package/dist/tts-provider.d.ts +69 -0
- package/dist/tts-provider.d.ts.map +1 -0
- package/dist/tts-provider.js +182 -0
- package/dist/tts-provider.js.map +1 -0
- package/dist/types.d.ts +109 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/voice-prerender.d.ts +41 -0
- package/dist/voice-prerender.d.ts.map +1 -0
- package/dist/voice-prerender.js +122 -0
- package/dist/voice-prerender.js.map +1 -0
- package/dist/voice.d.ts +76 -0
- package/dist/voice.d.ts.map +1 -0
- package/dist/voice.js +244 -0
- package/dist/voice.js.map +1 -0
- package/package.json +87 -0
- package/skills/tutorialize/SKILL.md +54 -0
- package/skills/tutorialize/agent.md +58 -0
- package/skills/tutorialize/references/api.md +489 -0
- package/skills/tutorialize/references/storytelling.md +193 -0
- package/src/styles.css +637 -0
- package/templates/site/public/.gitkeep +0 -0
- package/templates/site/public/widget.js +273 -0
- package/templates/site/src/components/TutorialsHome.astro +141 -0
- package/templates/site/src/components/VideoCard.astro +192 -0
- package/templates/site/src/components/VideoModal.astro +170 -0
- package/templates/site/src/data/.gitkeep +0 -0
- package/templates/site/src/layouts/Base.astro +56 -0
- package/templates/site/src/pages/[slug].astro +745 -0
- package/templates/site/src/pages/index.astro +5 -0
- package/templates/site/src/styles/tutorials.css +303 -0
package/dist/voice.js
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
import { existsSync, mkdirSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { createTTSProvider } from './tts-provider.js';
|
|
5
|
+
const TUTORIAL_MODE = process.env.TUTORIAL_MODE === 'true';
|
|
6
|
+
// Audio files stored in static folder to be served by dev server
|
|
7
|
+
const AUDIO_STATIC_DIR = join(process.cwd(), 'static', 'audio', 'tutorial-voice');
|
|
8
|
+
const AUDIO_URL_BASE = '/audio/tutorial-voice';
|
|
9
|
+
/**
|
|
10
|
+
* Get hash-based filename for text
|
|
11
|
+
*/
|
|
12
|
+
function getAudioFilename(text, lang) {
|
|
13
|
+
const hash = createHash('md5').update(`${lang}:${text}`).digest('hex').slice(0, 12);
|
|
14
|
+
const ext = process.platform === 'darwin' ? 'wav' : 'mp3';
|
|
15
|
+
return `${hash}.${ext}`;
|
|
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 class TutorialVoice {
|
|
23
|
+
page;
|
|
24
|
+
options;
|
|
25
|
+
ttsProvider = null;
|
|
26
|
+
preloadedAudio = new Map();
|
|
27
|
+
constructor(page, options) {
|
|
28
|
+
this.page = page;
|
|
29
|
+
this.options = {
|
|
30
|
+
lang: options.lang,
|
|
31
|
+
voiceName: options.voiceName,
|
|
32
|
+
voiceRate: options.voiceRate,
|
|
33
|
+
voicePitch: options.voicePitch,
|
|
34
|
+
playInBrowser: options.playInBrowser ?? true,
|
|
35
|
+
audioBaseUrl: options.audioBaseUrl ?? 'http://localhost:5173'
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
switchPage(page) {
|
|
39
|
+
this.page = page;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Pre-generate a single audio file (non-blocking).
|
|
43
|
+
* Returns a promise that resolves when preload is complete.
|
|
44
|
+
* No-op when TUTORIAL_MODE is not enabled.
|
|
45
|
+
*/
|
|
46
|
+
preloadSingle(text) {
|
|
47
|
+
if (!TUTORIAL_MODE)
|
|
48
|
+
return Promise.resolve();
|
|
49
|
+
return this.generateAudio(text).then(() => { });
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Pre-generate all audio files before the test runs.
|
|
53
|
+
* No-op when TUTORIAL_MODE is not enabled.
|
|
54
|
+
*/
|
|
55
|
+
async preload(texts) {
|
|
56
|
+
if (!TUTORIAL_MODE)
|
|
57
|
+
return;
|
|
58
|
+
// Ensure output directory exists
|
|
59
|
+
if (!existsSync(AUDIO_STATIC_DIR)) {
|
|
60
|
+
mkdirSync(AUDIO_STATIC_DIR, { recursive: true });
|
|
61
|
+
}
|
|
62
|
+
// Initialize TTS provider
|
|
63
|
+
if (!this.ttsProvider) {
|
|
64
|
+
this.ttsProvider = await createTTSProvider({
|
|
65
|
+
lang: this.options.lang,
|
|
66
|
+
voice: this.options.voiceName || undefined,
|
|
67
|
+
rate: this.options.voiceRate
|
|
68
|
+
});
|
|
69
|
+
console.log(`[Voice] Using TTS provider: ${this.ttsProvider.name}`);
|
|
70
|
+
}
|
|
71
|
+
console.log(`[Voice] Preloading ${texts.length} audio files...`);
|
|
72
|
+
const startTime = Date.now();
|
|
73
|
+
for (const text of texts) {
|
|
74
|
+
const filename = getAudioFilename(text, this.options.lang);
|
|
75
|
+
const filePath = join(AUDIO_STATIC_DIR, filename);
|
|
76
|
+
const url = `${AUDIO_URL_BASE}/${filename}`;
|
|
77
|
+
let durationMs;
|
|
78
|
+
// Check cache
|
|
79
|
+
if (existsSync(filePath)) {
|
|
80
|
+
// Get duration from cached file
|
|
81
|
+
durationMs = await this.getAudioDuration(filePath);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
// Generate audio file
|
|
85
|
+
const result = await this.ttsProvider.synthesize(text, filePath);
|
|
86
|
+
durationMs = result.duration;
|
|
87
|
+
}
|
|
88
|
+
this.preloadedAudio.set(text, {
|
|
89
|
+
text,
|
|
90
|
+
filename,
|
|
91
|
+
filePath,
|
|
92
|
+
url,
|
|
93
|
+
durationMs
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
console.log(`[Voice] Preloaded ${texts.length} files in ${Date.now() - startTime}ms`);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Get audio info for a text (must be preloaded first)
|
|
100
|
+
*/
|
|
101
|
+
getAudio(text) {
|
|
102
|
+
return this.preloadedAudio.get(text);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Get duration in ms for a text (must be preloaded)
|
|
106
|
+
*/
|
|
107
|
+
getDuration(text) {
|
|
108
|
+
return this.preloadedAudio.get(text)?.durationMs ?? 0;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Get the filename for a text (for timeline)
|
|
112
|
+
*/
|
|
113
|
+
getFilename(text) {
|
|
114
|
+
const cached = this.preloadedAudio.get(text);
|
|
115
|
+
if (cached)
|
|
116
|
+
return cached.filename;
|
|
117
|
+
return getAudioFilename(text, this.options.lang);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Start audio playback without waiting for it to finish.
|
|
121
|
+
* Returns the clip duration in ms — the caller owns the wall clock
|
|
122
|
+
* (action offset + clamp to duration), so headed playback and the
|
|
123
|
+
* ffmpeg merge stay in sync.
|
|
124
|
+
* No-op when TUTORIAL_MODE is not enabled (returns 0).
|
|
125
|
+
*/
|
|
126
|
+
async startPlayback(text) {
|
|
127
|
+
if (!TUTORIAL_MODE)
|
|
128
|
+
return 0;
|
|
129
|
+
let audio = this.preloadedAudio.get(text);
|
|
130
|
+
if (!audio)
|
|
131
|
+
audio = await this.generateAudio(text);
|
|
132
|
+
if (!audio) {
|
|
133
|
+
console.warn(`[Voice] Failed to generate audio for: "${text.substring(0, 30)}..."`);
|
|
134
|
+
return 0;
|
|
135
|
+
}
|
|
136
|
+
if (this.options.playInBrowser) {
|
|
137
|
+
// Fire-and-forget: a navigation during the step action may kill the
|
|
138
|
+
// context — playback loss is acceptable, timing comes from durationMs.
|
|
139
|
+
await this.page
|
|
140
|
+
.evaluate(({ url }) => {
|
|
141
|
+
new Audio(url).play().catch(() => { });
|
|
142
|
+
}, { url: `${this.options.audioBaseUrl}${audio.url}` })
|
|
143
|
+
.catch(() => { });
|
|
144
|
+
}
|
|
145
|
+
return audio.durationMs;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Play audio in browser (optional, for live viewing)
|
|
149
|
+
* Returns duration in ms
|
|
150
|
+
* Will generate audio on-demand if not preloaded.
|
|
151
|
+
* No-op when TUTORIAL_MODE is not enabled (returns 0).
|
|
152
|
+
*/
|
|
153
|
+
async play(text) {
|
|
154
|
+
if (!TUTORIAL_MODE)
|
|
155
|
+
return 0;
|
|
156
|
+
let audio = this.preloadedAudio.get(text);
|
|
157
|
+
if (!audio) {
|
|
158
|
+
audio = await this.generateAudio(text);
|
|
159
|
+
}
|
|
160
|
+
if (!audio) {
|
|
161
|
+
console.warn(`[Voice] Failed to generate audio for: "${text.substring(0, 30)}..."`);
|
|
162
|
+
return 0;
|
|
163
|
+
}
|
|
164
|
+
if (this.options.playInBrowser) {
|
|
165
|
+
await this.page.evaluate(({ url }) => {
|
|
166
|
+
return new Promise((resolve) => {
|
|
167
|
+
const audio = new Audio(url);
|
|
168
|
+
audio.onended = () => resolve();
|
|
169
|
+
audio.onerror = () => resolve();
|
|
170
|
+
audio.play().catch(() => resolve());
|
|
171
|
+
});
|
|
172
|
+
}, { url: `${this.options.audioBaseUrl}${audio.url}` });
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
// Just wait for the duration (silent)
|
|
176
|
+
await this.page.waitForTimeout(audio.durationMs);
|
|
177
|
+
}
|
|
178
|
+
return audio.durationMs;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Generate audio for a single text on demand
|
|
182
|
+
*/
|
|
183
|
+
async generateAudio(text) {
|
|
184
|
+
// Ensure output directory exists
|
|
185
|
+
if (!existsSync(AUDIO_STATIC_DIR)) {
|
|
186
|
+
mkdirSync(AUDIO_STATIC_DIR, { recursive: true });
|
|
187
|
+
}
|
|
188
|
+
// Initialize TTS provider if needed
|
|
189
|
+
if (!this.ttsProvider) {
|
|
190
|
+
this.ttsProvider = await createTTSProvider({
|
|
191
|
+
lang: this.options.lang,
|
|
192
|
+
voice: this.options.voiceName || undefined,
|
|
193
|
+
rate: this.options.voiceRate
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
const filename = getAudioFilename(text, this.options.lang);
|
|
197
|
+
const filePath = join(AUDIO_STATIC_DIR, filename);
|
|
198
|
+
const url = `${AUDIO_URL_BASE}/${filename}`;
|
|
199
|
+
let durationMs;
|
|
200
|
+
try {
|
|
201
|
+
// Check cache first
|
|
202
|
+
if (existsSync(filePath)) {
|
|
203
|
+
durationMs = await this.getAudioDuration(filePath);
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
// Generate audio file
|
|
207
|
+
const result = await this.ttsProvider.synthesize(text, filePath);
|
|
208
|
+
durationMs = result.duration;
|
|
209
|
+
}
|
|
210
|
+
const audio = {
|
|
211
|
+
text,
|
|
212
|
+
filename,
|
|
213
|
+
filePath,
|
|
214
|
+
url,
|
|
215
|
+
durationMs
|
|
216
|
+
};
|
|
217
|
+
// Cache for future use
|
|
218
|
+
this.preloadedAudio.set(text, audio);
|
|
219
|
+
return audio;
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
console.error(`[Voice] Error generating audio: ${error}`);
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Get audio duration using ffprobe or file stats
|
|
228
|
+
*/
|
|
229
|
+
async getAudioDuration(filePath) {
|
|
230
|
+
try {
|
|
231
|
+
const { execSync } = await import('child_process');
|
|
232
|
+
const output = execSync(`ffprobe -v quiet -show_entries format=duration -of csv=p=0 "${filePath}"`, { encoding: 'utf-8' });
|
|
233
|
+
return Math.ceil(parseFloat(output.trim()) * 1000);
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
// Fallback: estimate based on file size (rough)
|
|
237
|
+
const { statSync } = await import('fs');
|
|
238
|
+
const stats = statSync(filePath);
|
|
239
|
+
// Rough estimate: ~16KB per second for wav at 44100Hz mono
|
|
240
|
+
return Math.ceil((stats.size / 16000) * 1000);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
//# sourceMappingURL=voice.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"voice.js","sourceRoot":"","sources":["../src/voice.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "playwright-director",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "AI-assisted Playwright tool that turns executable tests into directed video guides — demos, tutorials, and documentation that never lag behind the product.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/Youniwemi/playwright-director.git"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/Youniwemi/playwright-director#readme",
|
|
10
|
+
"type": "module",
|
|
11
|
+
"main": "dist/index.js",
|
|
12
|
+
"types": "dist/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"default": "./dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./reporter": {
|
|
19
|
+
"types": "./dist/reporter.d.ts",
|
|
20
|
+
"default": "./dist/reporter.js"
|
|
21
|
+
},
|
|
22
|
+
"./slugify": {
|
|
23
|
+
"types": "./dist/slugify.d.ts",
|
|
24
|
+
"default": "./dist/slugify.js"
|
|
25
|
+
},
|
|
26
|
+
"./site": {
|
|
27
|
+
"types": "./dist/site/types.d.ts",
|
|
28
|
+
"default": "./dist/site/types.js"
|
|
29
|
+
},
|
|
30
|
+
"./styles.css": "./src/styles.css"
|
|
31
|
+
},
|
|
32
|
+
"bin": {
|
|
33
|
+
"playwright-director": "./dist/init.js",
|
|
34
|
+
"tutorial-transcript": "./dist/bin/export-transcript.js"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist",
|
|
38
|
+
"src/styles.css",
|
|
39
|
+
"skills",
|
|
40
|
+
"templates",
|
|
41
|
+
"README.md"
|
|
42
|
+
],
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsc && cp src/styles.css dist/styles.css",
|
|
45
|
+
"test": "vitest run",
|
|
46
|
+
"test:e2e": "playwright test",
|
|
47
|
+
"test:e2e:video": "TUTORIAL_MODE=true playwright test",
|
|
48
|
+
"prepublishOnly": "npm run build",
|
|
49
|
+
"postinstall": "node dist/postinstall.js"
|
|
50
|
+
},
|
|
51
|
+
"keywords": [
|
|
52
|
+
"playwright",
|
|
53
|
+
"tutorial",
|
|
54
|
+
"video",
|
|
55
|
+
"e2e",
|
|
56
|
+
"testing",
|
|
57
|
+
"narration",
|
|
58
|
+
"tts",
|
|
59
|
+
"text-to-speech",
|
|
60
|
+
"screen-recording",
|
|
61
|
+
"documentation",
|
|
62
|
+
"automated-docs",
|
|
63
|
+
"how-to",
|
|
64
|
+
"demo",
|
|
65
|
+
"walkthrough"
|
|
66
|
+
],
|
|
67
|
+
"author": "Youniwemi",
|
|
68
|
+
"license": "MIT",
|
|
69
|
+
"dependencies": {
|
|
70
|
+
"astro": "^5.0.0"
|
|
71
|
+
},
|
|
72
|
+
"peerDependencies": {
|
|
73
|
+
"@playwright/test": ">=1.40.0"
|
|
74
|
+
},
|
|
75
|
+
"peerDependenciesMeta": {
|
|
76
|
+
"sharp": {
|
|
77
|
+
"optional": true
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
"devDependencies": {
|
|
81
|
+
"@playwright/test": "^1.52.0",
|
|
82
|
+
"@types/node": "^26.2.0",
|
|
83
|
+
"@types/sharp": "^0.31.1",
|
|
84
|
+
"typescript": "^5.7.0",
|
|
85
|
+
"vitest": "^3.1.0"
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tutorialize
|
|
3
|
+
description: Transform a Playwright E2E test into a narrated tutorial video. Load before writing or reviewing any tutorial.step(), tutorial.context(), or tutorial.complete() call. Covers persona analysis, storytelling arc, choreography rules, and the playwright-director API.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /tutorialize — Tutorial Design Skill
|
|
7
|
+
|
|
8
|
+
Turn a Playwright test into a tutorial that **teaches**, not just demonstrates.
|
|
9
|
+
|
|
10
|
+
Two reference files support this skill:
|
|
11
|
+
- [`references/storytelling.md`](references/storytelling.md) — Persona analysis, emotional arc, narration voice, pacing decisions
|
|
12
|
+
- [`references/api.md`](references/api.md) — `playwright-director` API, timing model, technical rules, checklist
|
|
13
|
+
|
|
14
|
+
**Read both before tutorializing.** Storytelling comes first — it drives every API decision.
|
|
15
|
+
|
|
16
|
+
## Process
|
|
17
|
+
|
|
18
|
+
### 1. Understand (read `storytelling.md`)
|
|
19
|
+
- **Who** is watching? (role, expertise, emotional state)
|
|
20
|
+
- **What** are they trying to accomplish? (goal, not feature)
|
|
21
|
+
- **What** do they already know? (skip the obvious, explain the surprising)
|
|
22
|
+
- **How many profiles** does the story need? One is the default. Reach for a
|
|
23
|
+
second scene only when the lesson is the handoff between two people —
|
|
24
|
+
see `storytelling.md` §8 and `api.md` §10.
|
|
25
|
+
|
|
26
|
+
### 2. Design the arc
|
|
27
|
+
```
|
|
28
|
+
Navigate to first screen (before any tutorial call)
|
|
29
|
+
↓
|
|
30
|
+
CONTEXT (goal): "Here's what we'll accomplish"
|
|
31
|
+
↓
|
|
32
|
+
STEP (no-op): Describe what's on screen
|
|
33
|
+
↓
|
|
34
|
+
STEPS: Grouped by concept, do/explain narration
|
|
35
|
+
↓
|
|
36
|
+
CONTEXT (clarification): Only if a complex section needs framing
|
|
37
|
+
↓
|
|
38
|
+
MORE STEPS...
|
|
39
|
+
↓
|
|
40
|
+
COMPLETE: Celebrate the outcome, suggest what's next
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 3. Implement (read `api.md`)
|
|
44
|
+
- Use the Tutorial API: `context()`, `step()`, `complete()`
|
|
45
|
+
- Follow timing rules, blank-screen prevention, navigation handling
|
|
46
|
+
- Apply the polish checklist before submitting
|
|
47
|
+
|
|
48
|
+
### 4. Verify
|
|
49
|
+
- Test passes without `TUTORIAL_MODE` (plain E2E)
|
|
50
|
+
- Test passes with `TUTORIAL_MODE=true` (video generation)
|
|
51
|
+
- Watch the generated video — does it feel human?
|
|
52
|
+
- Narration texts also become the gallery site's step-by-step guide
|
|
53
|
+
(`npx playwright-director build-site`) — even `skipVoice` steps show their
|
|
54
|
+
text there, so write every step text to read well on the page too.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tutorial-crafter
|
|
3
|
+
description: Use this agent to convert Playwright tests into professional tutorial videos with voice narration, visual highlights, and proper choreography. Example: user: 'Tutorialize tests/free/02_clients.init.ts' → dispatch tutorial-crafter.
|
|
4
|
+
model: sonnet
|
|
5
|
+
tools: Bash, Glob, Grep, LS, Read, Edit, MultiEdit, Write, TodoWrite
|
|
6
|
+
color: purple
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
You are an expert Tutorial Designer who transforms Playwright E2E tests into professional, engaging video tutorials. You understand cognitive load, visual pacing, and how humans learn from screencasts.
|
|
10
|
+
|
|
11
|
+
## Before you start
|
|
12
|
+
|
|
13
|
+
Load the `/tutorialize` skill and read both reference files:
|
|
14
|
+
- `references/storytelling.md` — persona, emotional arc, narration voice
|
|
15
|
+
- `references/api.md` — API, timing model, technical rules, checklist
|
|
16
|
+
|
|
17
|
+
## Your process
|
|
18
|
+
|
|
19
|
+
### 1. Analyze the test
|
|
20
|
+
Read the original test file. Identify:
|
|
21
|
+
- **Persona**: who is this tutorial for? (role, expertise, emotional state)
|
|
22
|
+
- **Goal**: what will the viewer accomplish?
|
|
23
|
+
- **Key actions**: what needs explanation vs what is obvious?
|
|
24
|
+
- **Profiles**: does the lesson depend on a handoff between two people? If so,
|
|
25
|
+
the test becomes multi-scene (`storytelling.md` §8, `api.md` §10). Default to
|
|
26
|
+
a single profile — a second scene doubles the length and must earn it.
|
|
27
|
+
|
|
28
|
+
### 2. Structure the tutorial
|
|
29
|
+
Follow the arc from `storytelling.md`:
|
|
30
|
+
```
|
|
31
|
+
Navigate → Goal context → Steps (grouped) → Clarifications (if needed) → Complete
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### 3. Write natural narration
|
|
35
|
+
- "Do" ≤ 8 words, action-oriented
|
|
36
|
+
- "Explain" gives the WHY
|
|
37
|
+
- Second person, conversational tone
|
|
38
|
+
- Persona-appropriate vocabulary
|
|
39
|
+
|
|
40
|
+
### 4. Apply technical rules
|
|
41
|
+
Follow all rules from `api.md`:
|
|
42
|
+
- No blank-screen opening
|
|
43
|
+
- Navigation transitions properly awaited
|
|
44
|
+
- Dual-mode (test works with and without TUTORIAL_MODE)
|
|
45
|
+
- `@tutorial` tag on the test
|
|
46
|
+
- Acronyms have `voiceText` where TTS mispronounces
|
|
47
|
+
- Multi-scene only: every step touching a scene carries `{ scene }`, scenes sit
|
|
48
|
+
on distinct origins, each tab switch is acknowledged in the narration, and
|
|
49
|
+
split ratios (`focus(['a','b'], { ratio: [30, 70] })`) are used to keep the
|
|
50
|
+
acting pane readable
|
|
51
|
+
|
|
52
|
+
### 5. Show and explain
|
|
53
|
+
- Present the diff or full transformed code
|
|
54
|
+
- Explain why certain groupings were chosen
|
|
55
|
+
- Describe what the pacing will feel like when the video plays
|
|
56
|
+
|
|
57
|
+
## Remember
|
|
58
|
+
A great tutorial makes the viewer feel capable, not confused. When in doubt, slow down and explain.
|