demo-composer 0.1.1
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 +262 -0
- package/dist/bin/demo-composer.d.ts +2 -0
- package/dist/bin/demo-composer.js +47 -0
- package/dist/bin/demo-composer.js.map +1 -0
- package/dist/composer.d.ts +17 -0
- package/dist/composer.js +511 -0
- package/dist/composer.js.map +1 -0
- package/dist/configuration.d.ts +89 -0
- package/dist/configuration.js +104 -0
- package/dist/configuration.js.map +1 -0
- package/dist/cucumber-config.d.ts +4 -0
- package/dist/cucumber-config.js +17 -0
- package/dist/cucumber-config.js.map +1 -0
- package/dist/effects.d.ts +6 -0
- package/dist/effects.js +248 -0
- package/dist/effects.js.map +1 -0
- package/dist/hooks.d.ts +1 -0
- package/dist/hooks.js +57 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/narration.d.ts +17 -0
- package/dist/narration.js +109 -0
- package/dist/narration.js.map +1 -0
- package/dist/overlay.d.ts +2 -0
- package/dist/overlay.js +16 -0
- package/dist/overlay.js.map +1 -0
- package/dist/runner.d.ts +6 -0
- package/dist/runner.js +144 -0
- package/dist/runner.js.map +1 -0
- package/dist/templates.d.ts +20 -0
- package/dist/templates.js +140 -0
- package/dist/templates.js.map +1 -0
- package/dist/tts.d.ts +25 -0
- package/dist/tts.js +106 -0
- package/dist/tts.js.map +1 -0
- package/dist/world.d.ts +50 -0
- package/dist/world.js +245 -0
- package/dist/world.js.map +1 -0
- package/package.json +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# demo-composer
|
|
2
|
+
|
|
3
|
+
Generate narrated demo videos from Cucumber BDD tests using Playwright and Gemini AI.
|
|
4
|
+
|
|
5
|
+
demo-composer records your Cucumber scenarios running against a live application, generates AI narration with text-to-speech, and composes polished demo videos — complete with title cards, subtitles, sidebar overlays, cursor effects, and optional background music.
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
- **Node.js** >= 18
|
|
10
|
+
- **FFmpeg** with the `sidechaincompress` filter (required for background music ducking)
|
|
11
|
+
```sh
|
|
12
|
+
brew install ffmpeg
|
|
13
|
+
```
|
|
14
|
+
- **Google Cloud credentials** configured for Vertex AI (Gemini)
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
npm install demo-composer
|
|
20
|
+
npx demo-composer
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Project Conventions
|
|
24
|
+
|
|
25
|
+
demo-composer follows **convention over configuration**. Place your files according to these conventions and everything works out of the box:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
project-root/
|
|
29
|
+
├── demo.json # Optional: override convention defaults
|
|
30
|
+
├── demo-music.mp3 # Optional: background music (auto-detected)
|
|
31
|
+
├── features/
|
|
32
|
+
│ ├── login/
|
|
33
|
+
│ │ ├── login.feature # Gherkin feature files
|
|
34
|
+
│ │ └── login.steps.ts # Step definitions
|
|
35
|
+
│ └── checkout/
|
|
36
|
+
│ ├── checkout.feature
|
|
37
|
+
│ └── checkout.steps.ts
|
|
38
|
+
├── support/
|
|
39
|
+
│ └── world.ts # Optional: extend DemoWorld for project-specific helpers
|
|
40
|
+
└── demos/ # Output directory (auto-created)
|
|
41
|
+
└── login/
|
|
42
|
+
└── login.mp4
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Feature Files
|
|
46
|
+
|
|
47
|
+
Place Gherkin `.feature` files in `features/**/*.feature`. These are the same files you use for testing — no separate demo features needed.
|
|
48
|
+
|
|
49
|
+
### Step Definitions
|
|
50
|
+
|
|
51
|
+
Place step definition files as `features/**/*.steps.ts`. Use the `DemoWorld` API in your steps.
|
|
52
|
+
|
|
53
|
+
### Support Files
|
|
54
|
+
|
|
55
|
+
If you need to extend `DemoWorld` (e.g., to add project-specific helpers or override the World constructor), place a `support/world.ts` file. It will be auto-imported.
|
|
56
|
+
|
|
57
|
+
## Writing Step Definitions
|
|
58
|
+
|
|
59
|
+
Import `DemoWorld` and use its methods for visual demo actions:
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
import { Given, When, Then } from '@cucumber/cucumber';
|
|
63
|
+
import type { DemoWorld } from 'demo-composer';
|
|
64
|
+
|
|
65
|
+
When('the user clicks the sign in button', async function (this: DemoWorld) {
|
|
66
|
+
const button = this.page.getByRole('button', { name: 'Sign In' });
|
|
67
|
+
await this.demoClick(button);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
When('the user enters their email', async function (this: DemoWorld) {
|
|
71
|
+
const input = this.page.getByLabel('Email');
|
|
72
|
+
await this.demoType(input, 'user@example.com');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
Then('the user should see their profile', async function (this: DemoWorld) {
|
|
76
|
+
const profile = this.page.getByTestId('profile');
|
|
77
|
+
await this.demoVisible(profile);
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### DemoWorld API
|
|
82
|
+
|
|
83
|
+
| Method | Description |
|
|
84
|
+
|--------|-------------|
|
|
85
|
+
| `demoClick(locator, page?)` | Zoom to element, glide cursor, click, zoom back |
|
|
86
|
+
| `demoType(locator, text, page?)` | Zoom to element, click, type with visible keystrokes, zoom back |
|
|
87
|
+
| `demoVisible(locator)` | Highlight element with pulsing glow, zoom in, then reset |
|
|
88
|
+
| `this.page` | The Playwright `Page` instance |
|
|
89
|
+
| `this.context` | The Playwright `BrowserContext` (with video recording) |
|
|
90
|
+
| `this.appBaseUrl` | The application URL (from `APP_BASE_URL` env var) |
|
|
91
|
+
|
|
92
|
+
When recording is disabled (i.e., running as a normal test), `demoClick` performs a plain click, `demoType` clears and types, and `demoVisible` is a no-op. This lets you **reuse the same step definitions for both tests and demo videos** with no code changes.
|
|
93
|
+
|
|
94
|
+
### Using DemoWorld for Tests
|
|
95
|
+
|
|
96
|
+
`DemoWorld` works in lightweight mode by default — no video recording, no visual effects. Just re-export it as your test World:
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
// support/world.ts
|
|
100
|
+
export { DemoWorld } from 'demo-composer';
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
// support/hooks.ts
|
|
105
|
+
import { Before, After, setWorldConstructor, AfterAll } from '@cucumber/cucumber';
|
|
106
|
+
import { DemoWorld } from './world.ts';
|
|
107
|
+
|
|
108
|
+
setWorldConstructor(DemoWorld);
|
|
109
|
+
|
|
110
|
+
Before(async function (this: DemoWorld) {
|
|
111
|
+
await this.init();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
After(async function (this: DemoWorld) {
|
|
115
|
+
await this.cleanup();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
AfterAll(async function () {
|
|
119
|
+
await DemoWorld.closeBrowser();
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
When you run `npx demo-composer`, the demo hooks automatically enable recording mode, and the same step definitions produce polished demo videos.
|
|
124
|
+
|
|
125
|
+
### Extending DemoWorld
|
|
126
|
+
|
|
127
|
+
For project-specific helpers, extend `DemoWorld` in `support/world.ts`:
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
import { DemoWorld } from 'demo-composer';
|
|
131
|
+
import { setWorldConstructor, type IWorldOptions } from '@cucumber/cucumber';
|
|
132
|
+
|
|
133
|
+
class MyWorld extends DemoWorld {
|
|
134
|
+
constructor(options: IWorldOptions) {
|
|
135
|
+
super(options);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
override async init() {
|
|
139
|
+
await super.init();
|
|
140
|
+
// Project-specific initialization
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
setWorldConstructor(MyWorld);
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Custom Templates
|
|
148
|
+
|
|
149
|
+
Override the visual design of title cards, subtitles, and overlays by passing `TemplateOverrides` to the programmatic API. To reference the default implementations when writing overrides, import them from `demo-composer/templates`:
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
import { titleCardHtml } from 'demo-composer/templates';
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Environment Variables
|
|
156
|
+
|
|
157
|
+
### Required
|
|
158
|
+
|
|
159
|
+
| Variable | Description |
|
|
160
|
+
|----------|-------------|
|
|
161
|
+
| `APP_BASE_URL` | URL of the running application to record |
|
|
162
|
+
| `GEMINI_API_KEY` | Google Gemini API key (or use Application Default Credentials) |
|
|
163
|
+
|
|
164
|
+
### Optional
|
|
165
|
+
|
|
166
|
+
| Variable | Description | Default |
|
|
167
|
+
|----------|-------------|---------|
|
|
168
|
+
| `GOOGLE_CLOUD_PROJECT` | Google Cloud project ID | — |
|
|
169
|
+
| `GOOGLE_CLOUD_REGION` | Google Cloud region | `us-central1` |
|
|
170
|
+
|
|
171
|
+
## Configuration: `demo.json`
|
|
172
|
+
|
|
173
|
+
Place an optional `demo.json` at the project root to override convention defaults. All fields are optional:
|
|
174
|
+
|
|
175
|
+
```json
|
|
176
|
+
{
|
|
177
|
+
"width": 1920,
|
|
178
|
+
"height": 1080,
|
|
179
|
+
"outputDir": "demos",
|
|
180
|
+
"featuresGlob": "features/**/*.feature",
|
|
181
|
+
"musicFile": "demo-music.mp3",
|
|
182
|
+
"musicVolume": 0.15,
|
|
183
|
+
"musicFadeSeconds": 2,
|
|
184
|
+
"stepDelayMs": 500,
|
|
185
|
+
"stepSettleMs": 500,
|
|
186
|
+
"scenarioEndPauseMs": 2000,
|
|
187
|
+
"ttsModel": "gemini-2.5-pro-tts",
|
|
188
|
+
"ttsVoice": "Kore",
|
|
189
|
+
"ttsStylePreamble": "Read the following in a calm, steady, professional tone...",
|
|
190
|
+
"introLabel": "Demo"
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Field Reference
|
|
195
|
+
|
|
196
|
+
| Field | Type | Default | Description |
|
|
197
|
+
|-------|------|---------|-------------|
|
|
198
|
+
| `width` | number | `1920` | Video width in pixels |
|
|
199
|
+
| `height` | number | `1080` | Video height in pixels |
|
|
200
|
+
| `outputDir` | string | `"demos"` | Output directory (relative to project root) |
|
|
201
|
+
| `featuresGlob` | string | `"features/**/*.feature"` | Glob pattern for feature files |
|
|
202
|
+
| `musicFile` | string | `"demo-music.mp3"` if present | Path to background music file |
|
|
203
|
+
| `musicVolume` | number | `0.15` | Background music volume (0–1) |
|
|
204
|
+
| `musicFadeSeconds` | number | `2` | Fade in/out duration for music |
|
|
205
|
+
| `stepDelayMs` | number | `500` | Delay before each step (milliseconds) |
|
|
206
|
+
| `stepSettleMs` | number | `500` | Settle time after each step |
|
|
207
|
+
| `scenarioEndPauseMs` | number | `2000` | Pause at end of each scenario |
|
|
208
|
+
| `ttsModel` | string | `"gemini-2.5-pro-tts"` | Gemini TTS model |
|
|
209
|
+
| `ttsVoice` | string | `"Kore"` | TTS voice name |
|
|
210
|
+
| `ttsStylePreamble` | string | *(calm professional tone)* | Preamble text prepended to every TTS request |
|
|
211
|
+
| `introLabel` | string | `"Demo"` | Label shown on the intro title card |
|
|
212
|
+
|
|
213
|
+
### Config Resolution Order
|
|
214
|
+
|
|
215
|
+
```
|
|
216
|
+
Built-in conventions → demo.json overrides → Environment variables (secrets/runtime only)
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Background Music
|
|
220
|
+
|
|
221
|
+
If a file named `demo-music.mp3` exists at the project root, it is automatically used as background music. The music:
|
|
222
|
+
|
|
223
|
+
- Loops for the full video duration
|
|
224
|
+
- Fades in at the start and out at the end
|
|
225
|
+
- Automatically ducks (reduces volume) when narration is playing
|
|
226
|
+
|
|
227
|
+
To use a different file or disable music, set the `musicFile` field in `demo.json`.
|
|
228
|
+
|
|
229
|
+
## CLI
|
|
230
|
+
|
|
231
|
+
```sh
|
|
232
|
+
npx demo-composer [options]
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
| Option | Description |
|
|
236
|
+
|--------|-------------|
|
|
237
|
+
| `--config <path>` | Path to a custom Cucumber config file |
|
|
238
|
+
| `--cwd <path>` | Working directory (default: current directory) |
|
|
239
|
+
| `-h, --help` | Show help |
|
|
240
|
+
|
|
241
|
+
### Custom Cucumber Config
|
|
242
|
+
|
|
243
|
+
If you provide `--config`, you are responsible for importing demo-composer hooks:
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
import type { IConfiguration } from '@cucumber/cucumber';
|
|
247
|
+
|
|
248
|
+
export default {
|
|
249
|
+
failFast: true,
|
|
250
|
+
format: ['summary'],
|
|
251
|
+
paths: ['features/**/*.feature'],
|
|
252
|
+
import: ['demo-composer/hooks', 'support/**/*.ts', 'features/**/*.steps.ts'],
|
|
253
|
+
} satisfies Partial<IConfiguration>;
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## How It Works
|
|
257
|
+
|
|
258
|
+
1. **Discover** feature files via glob pattern
|
|
259
|
+
2. **Generate narration** text using Gemini AI from the Gherkin scenarios
|
|
260
|
+
3. **Synthesize speech** for all narration clips using Gemini TTS
|
|
261
|
+
4. **Record** scenarios running in Playwright with video recording, cursor effects, and dynamic step delays synchronized to narration audio durations
|
|
262
|
+
5. **Compose** final videos with title cards, subtitle overlays, sidebar progress panels, picture-in-picture popups, and background music with sidechain ducking
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { run } from '../runner.js';
|
|
3
|
+
const args = process.argv.slice(2);
|
|
4
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
5
|
+
console.log(`
|
|
6
|
+
demo-composer — Generate narrated demo videos from Cucumber BDD tests
|
|
7
|
+
|
|
8
|
+
Usage: demo-composer [options]
|
|
9
|
+
|
|
10
|
+
Options:
|
|
11
|
+
--config <path> Path to a custom Cucumber config file
|
|
12
|
+
--cwd <path> Working directory (default: current directory)
|
|
13
|
+
-h, --help Show this help message
|
|
14
|
+
|
|
15
|
+
Environment variables (required):
|
|
16
|
+
GEMINI_API_KEY Google Gemini API key
|
|
17
|
+
APP_BASE_URL URL of the running application
|
|
18
|
+
|
|
19
|
+
Environment variables (optional):
|
|
20
|
+
GOOGLE_CLOUD_PROJECT Google Cloud project ID
|
|
21
|
+
GOOGLE_CLOUD_REGION Google Cloud region (default: us-central1)
|
|
22
|
+
|
|
23
|
+
Configuration:
|
|
24
|
+
Place a demo.json file at the project root to override convention defaults.
|
|
25
|
+
See the README for the full schema.
|
|
26
|
+
`);
|
|
27
|
+
process.exit(0);
|
|
28
|
+
}
|
|
29
|
+
let cwd;
|
|
30
|
+
let cucumberConfig;
|
|
31
|
+
for (let i = 0; i < args.length; i++) {
|
|
32
|
+
if (args[i] === '--config' && args[i + 1]) {
|
|
33
|
+
cucumberConfig = args[++i];
|
|
34
|
+
}
|
|
35
|
+
else if (args[i] === '--cwd' && args[i + 1]) {
|
|
36
|
+
cwd = args[++i];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (!process.env.APP_BASE_URL) {
|
|
40
|
+
console.error('Error: APP_BASE_URL environment variable is required.');
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
run({ cwd, cucumberConfig }).catch((err) => {
|
|
44
|
+
console.error('Demo generation failed:', err);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
});
|
|
47
|
+
//# sourceMappingURL=demo-composer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"demo-composer.js","sourceRoot":"","sources":["../../src/bin/demo-composer.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAEnC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAEnC,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;IACjD,OAAO,CAAC,GAAG,CAAC;;;;;;;;;;;;;;;;;;;;;CAqBf,CAAC,CAAC;IACC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACpB,CAAC;AAED,IAAI,GAAuB,CAAC;AAC5B,IAAI,cAAkC,CAAC;AAEvC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;IACnC,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,UAAU,IAAI,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;QACxC,cAAc,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;IAC/B,CAAC;SAAM,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,OAAO,IAAI,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;QAC5C,GAAG,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;IACpB,CAAC;AACL,CAAC;AAED,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,YAAY,EAAE,CAAC;IAC5B,OAAO,CAAC,KAAK,CAAC,uDAAuD,CAAC,CAAC;IACvE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACpB,CAAC;AAED,GAAG,CAAC,EAAE,GAAG,EAAE,cAAc,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACvC,OAAO,CAAC,KAAK,CAAC,yBAAyB,EAAE,GAAG,CAAC,CAAC;IAC9C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACpB,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ScenarioRecording } from './world.js';
|
|
2
|
+
import type { AudioClip } from './tts.js';
|
|
3
|
+
import type { ScenarioNarration } from './narration.js';
|
|
4
|
+
import { type TemplateOverrides } from './templates.js';
|
|
5
|
+
interface ScenarioCompositionInput {
|
|
6
|
+
recording: ScenarioRecording;
|
|
7
|
+
narration: ScenarioNarration;
|
|
8
|
+
titleAudio: AudioClip;
|
|
9
|
+
stepAudios: AudioClip[];
|
|
10
|
+
}
|
|
11
|
+
export interface MusicConfig {
|
|
12
|
+
filePath: string;
|
|
13
|
+
volume: number;
|
|
14
|
+
fadeSeconds: number;
|
|
15
|
+
}
|
|
16
|
+
export declare function composeFeatureVideo(inputs: ScenarioCompositionInput[], featureName: string, featureDescription: string, introAudio: AudioClip, conclusionAudio: AudioClip, outputDir: string, width: number, height: number, music?: MusicConfig, templates?: TemplateOverrides): Promise<string>;
|
|
17
|
+
export {};
|