qa-video 1.0.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 ADDED
@@ -0,0 +1,112 @@
1
+ # QA Video Generator
2
+
3
+ Generate flashcard-style videos from YAML Q&A files with offline neural TTS narration. Designed for interview preparation — provide your questions and answers, get a YouTube-ready MP4.
4
+
5
+ ## How it works
6
+
7
+ 1. **Parse** YAML file with questions and answers
8
+ 2. **Synthesize** speech for each Q&A using [Kokoro TTS](https://github.com/hexgrad/kokoro) (offline, neural, 82M params)
9
+ 3. **Render** styled slides as 1920x1080 PNGs using Skia canvas
10
+ 4. **Assemble** video with FFmpeg — H.264 High Profile, AAC audio, YouTube-optimized
11
+
12
+ Each card shows the question (with voiceover), pauses, then shows the answer (with voiceover), then moves to the next card.
13
+
14
+ ## Prerequisites
15
+
16
+ - **Node.js** 18+
17
+ - **pnpm** (or npm/yarn)
18
+ - **FFmpeg** — `brew install ffmpeg`
19
+ - **espeak-ng** — `brew install espeak-ng` (required by Kokoro TTS phonemizer)
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ pnpm install
25
+ pnpm build
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ ### Single file
31
+
32
+ ```bash
33
+ node dist/index.js generate -i qa/core-concepts.yaml
34
+ ```
35
+
36
+ ### All files in a directory
37
+
38
+ ```bash
39
+ node dist/index.js batch -d qa/
40
+ ```
41
+
42
+ ### Clear cached artifacts
43
+
44
+ ```bash
45
+ node dist/index.js clear # clear all caches
46
+ node dist/index.js clear -i qa/test.yaml # clear cache for one file
47
+ node dist/index.js clear -d qa/ # clear caches for all files in dir
48
+ ```
49
+
50
+ ### Options
51
+
52
+ | Option | Default | Description |
53
+ |--------|---------|-------------|
54
+ | `--voice <name>` | `af_heart` | Kokoro TTS voice |
55
+ | `--question-delay <sec>` | `2` | Silence after question speech |
56
+ | `--answer-delay <sec>` | `3` | Silence after answer speech |
57
+ | `--card-gap <sec>` | `1` | Gap between cards |
58
+ | `--font-size <px>` | `52` | Slide text font size |
59
+ | `--force` | `false` | Regenerate all artifacts, ignore cache |
60
+
61
+ ## YAML Format
62
+
63
+ ```yaml
64
+ config:
65
+ questionDelay: 2
66
+ answerDelay: 3
67
+ questions:
68
+ - question: What is DevOps?
69
+ answer: DevOps is a set of practices that combine software development and IT operations.
70
+ - question: What is Docker?
71
+ answer: Docker is a platform for containerizing applications.
72
+ ```
73
+
74
+ Config values in the YAML override defaults but CLI flags take priority.
75
+
76
+ ## Caching
77
+
78
+ Artifacts (WAV audio, PNG slides, MP4 clips) are cached in `output/.tmp/` with SHA-based filenames. If the pipeline is interrupted, re-running reuses all previously generated artifacts. Only changed questions get regenerated. Use `--force` to bypass the cache or `clear` command to remove it.
79
+
80
+ ## Output
81
+
82
+ - **Format:** MP4 (H.264 High Profile + AAC)
83
+ - **Resolution:** 1920x1080 @ 30fps
84
+ - **Audio:** 384kbps stereo, 48kHz
85
+ - **Optimized:** `-movflags +faststart`, `-tune stillimage`
86
+
87
+ Videos are saved to `output/<filename>.mp4`.
88
+
89
+ ## Architecture
90
+
91
+ ```
92
+ src/
93
+ ├── index.ts # CLI entry point (commander)
94
+ ├── types.ts # Shared types & defaults
95
+ ├── parser.ts # YAML parser
96
+ ├── tts.ts # Kokoro TTS synthesis
97
+ ├── renderer.ts # Slide rendering (@napi-rs/canvas)
98
+ ├── assembler.ts # FFmpeg video assembly
99
+ ├── pipeline.ts # 4-stage orchestration
100
+ └── cache.ts # SHA-based artifact caching
101
+ ```
102
+
103
+ ## Tech Stack
104
+
105
+ - **TTS:** [kokoro-js](https://www.npmjs.com/package/kokoro-js) — offline neural TTS, Apache 2.0
106
+ - **Canvas:** [@napi-rs/canvas](https://www.npmjs.com/package/@napi-rs/canvas) — Skia-based, zero system deps
107
+ - **Video:** [fluent-ffmpeg](https://www.npmjs.com/package/fluent-ffmpeg) + system FFmpeg
108
+ - **CLI:** [commander](https://www.npmjs.com/package/commander)
109
+
110
+ ## License
111
+
112
+ MIT
@@ -0,0 +1,4 @@
1
+ import { Segment } from './types.js';
2
+ export declare function createSegmentClip(segment: Segment, outputPath: string): Promise<void>;
3
+ export declare function createSilentClip(imagePath: string, durationSec: number, outputPath: string): Promise<void>;
4
+ export declare function concatenateClips(clipPaths: string[], outputPath: string, tempDir: string): Promise<void>;
@@ -0,0 +1,80 @@
1
+ import { join } from 'path';
2
+ import { writeFile } from 'fs/promises';
3
+ import { execFile } from 'child_process';
4
+ import { promisify } from 'util';
5
+ import { getFfmpegPath } from './ffmpeg-paths.js';
6
+ const execFileAsync = promisify(execFile);
7
+ async function getFfmpeg() {
8
+ const ffmpeg = await import('fluent-ffmpeg');
9
+ return ffmpeg.default;
10
+ }
11
+ export async function createSegmentClip(segment, outputPath) {
12
+ const FfmpegCommand = await getFfmpeg();
13
+ return new Promise((resolve, reject) => {
14
+ FfmpegCommand()
15
+ .input(segment.imagePath)
16
+ .inputOptions(['-loop', '1'])
17
+ .input(segment.audioPath)
18
+ .outputOptions([
19
+ '-c:v', 'libx264',
20
+ '-tune', 'stillimage',
21
+ '-c:a', 'aac',
22
+ '-b:a', '384k',
23
+ '-ar', '48000',
24
+ '-ac', '2',
25
+ '-pix_fmt', 'yuv420p',
26
+ '-shortest',
27
+ '-t', String(segment.totalDuration),
28
+ '-r', '30',
29
+ ])
30
+ .output(outputPath)
31
+ .on('end', () => resolve())
32
+ .on('error', (err) => reject(err))
33
+ .run();
34
+ });
35
+ }
36
+ export async function createSilentClip(imagePath, durationSec, outputPath) {
37
+ await execFileAsync(getFfmpegPath(), [
38
+ '-y',
39
+ '-loop', '1', '-i', imagePath,
40
+ '-f', 'lavfi', '-i', 'anullsrc=r=48000:cl=stereo',
41
+ '-c:v', 'libx264', '-tune', 'stillimage',
42
+ '-c:a', 'aac', '-b:a', '384k', '-ar', '48000', '-ac', '2',
43
+ '-pix_fmt', 'yuv420p',
44
+ '-t', String(durationSec),
45
+ '-r', '30',
46
+ outputPath,
47
+ ]);
48
+ }
49
+ export async function concatenateClips(clipPaths, outputPath, tempDir) {
50
+ const FfmpegCommand = await getFfmpeg();
51
+ // Write concat list file
52
+ const concatListPath = join(tempDir, 'concat_list.txt');
53
+ const concatContent = clipPaths.map(p => `file '${p}'`).join('\n');
54
+ await writeFile(concatListPath, concatContent, 'utf-8');
55
+ return new Promise((resolve, reject) => {
56
+ FfmpegCommand()
57
+ .input(concatListPath)
58
+ .inputOptions(['-f', 'concat', '-safe', '0'])
59
+ .outputOptions([
60
+ '-c:v', 'libx264',
61
+ '-crf', '18',
62
+ '-preset', 'medium',
63
+ '-profile:v', 'high',
64
+ '-level', '4.0',
65
+ '-tune', 'stillimage',
66
+ '-c:a', 'aac',
67
+ '-b:a', '384k',
68
+ '-ar', '48000',
69
+ '-ac', '2',
70
+ '-pix_fmt', 'yuv420p',
71
+ '-movflags', '+faststart',
72
+ '-r', '30',
73
+ ])
74
+ .output(outputPath)
75
+ .on('end', () => resolve())
76
+ .on('error', (err) => reject(err))
77
+ .run();
78
+ });
79
+ }
80
+ //# sourceMappingURL=assembler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assembler.js","sourceRoot":"","sources":["../src/assembler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ /** Short SHA-8 hash of content for cache keys */
2
+ export declare function sha(content: string): string;
3
+ /** Build a cache-keyed filename: <prefix>_<sha>.<ext> */
4
+ export declare function cachedPath(tempDir: string, prefix: string, hashContent: string, ext: string): string;
5
+ /** Returns true if the file exists and force=false (i.e., cached and valid) */
6
+ export declare function isCached(filePath: string, force: boolean): boolean;
package/dist/cache.js ADDED
@@ -0,0 +1,16 @@
1
+ import { createHash } from 'crypto';
2
+ import { existsSync } from 'fs';
3
+ import { join } from 'path';
4
+ /** Short SHA-8 hash of content for cache keys */
5
+ export function sha(content) {
6
+ return createHash('sha256').update(content).digest('hex').substring(0, 8);
7
+ }
8
+ /** Build a cache-keyed filename: <prefix>_<sha>.<ext> */
9
+ export function cachedPath(tempDir, prefix, hashContent, ext) {
10
+ return join(tempDir, `${prefix}_${sha(hashContent)}.${ext}`);
11
+ }
12
+ /** Returns true if the file exists and force=false (i.e., cached and valid) */
13
+ export function isCached(filePath, force) {
14
+ return !force && existsSync(filePath);
15
+ }
16
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AACpC,OAAO,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAChC,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,iDAAiD;AACjD,MAAM,UAAU,GAAG,CAAC,OAAe;IACjC,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AAC5E,CAAC;AAED,yDAAyD;AACzD,MAAM,UAAU,UAAU,CAAC,OAAe,EAAE,MAAc,EAAE,WAAmB,EAAE,GAAW;IAC1F,OAAO,IAAI,CAAC,OAAO,EAAE,GAAG,MAAM,IAAI,GAAG,CAAC,WAAW,CAAC,IAAI,GAAG,EAAE,CAAC,CAAC;AAC/D,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,QAAQ,CAAC,QAAgB,EAAE,KAAc;IACvD,OAAO,CAAC,KAAK,IAAI,UAAU,CAAC,QAAQ,CAAC,CAAC;AACxC,CAAC"}
@@ -0,0 +1,3 @@
1
+ export declare function ensureDeps(): Promise<void>;
2
+ export declare function getFfmpegPath(): string;
3
+ export declare function getFfprobePath(): string;
@@ -0,0 +1,59 @@
1
+ import { execFileSync } from 'child_process';
2
+ function which(bin) {
3
+ try {
4
+ return execFileSync('which', [bin], { encoding: 'utf-8' }).trim();
5
+ }
6
+ catch {
7
+ return null;
8
+ }
9
+ }
10
+ async function resolveFfmpegPath() {
11
+ try {
12
+ const mod = await import('ffmpeg-static');
13
+ if (mod.default)
14
+ return mod.default;
15
+ }
16
+ catch { }
17
+ return process.env.FFMPEG_PATH || which('ffmpeg');
18
+ }
19
+ async function resolveFfprobePath() {
20
+ try {
21
+ const mod = await import('@ffprobe-installer/ffprobe');
22
+ if (mod.path)
23
+ return mod.path;
24
+ }
25
+ catch { }
26
+ return process.env.FFPROBE_PATH || which('ffprobe');
27
+ }
28
+ let _ffmpegPath = null;
29
+ let _ffprobePath = null;
30
+ export async function ensureDeps() {
31
+ _ffmpegPath = await resolveFfmpegPath();
32
+ _ffprobePath = await resolveFfprobePath();
33
+ if (!_ffmpegPath) {
34
+ console.error('Error: ffmpeg not found.\n' +
35
+ ' Install: brew install ffmpeg (macOS) / sudo apt install ffmpeg (Linux)\n' +
36
+ ' Or set FFMPEG_PATH environment variable.');
37
+ process.exit(1);
38
+ }
39
+ if (!_ffprobePath) {
40
+ console.error('Error: ffprobe not found.\n' +
41
+ ' Install ffmpeg (includes ffprobe):\n' +
42
+ ' brew install ffmpeg (macOS) / sudo apt install ffmpeg (Linux)\n' +
43
+ ' Or set FFPROBE_PATH environment variable.');
44
+ process.exit(1);
45
+ }
46
+ process.env.FFMPEG_PATH = _ffmpegPath;
47
+ process.env.FFPROBE_PATH = _ffprobePath;
48
+ }
49
+ export function getFfmpegPath() {
50
+ if (!_ffmpegPath)
51
+ throw new Error('Call ensureDeps() first');
52
+ return _ffmpegPath;
53
+ }
54
+ export function getFfprobePath() {
55
+ if (!_ffprobePath)
56
+ throw new Error('Call ensureDeps() first');
57
+ return _ffprobePath;
58
+ }
59
+ //# sourceMappingURL=ffmpeg-paths.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ffmpeg-paths.js","sourceRoot":"","sources":["../src/ffmpeg-paths.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAE7C,SAAS,KAAK,CAAC,GAAW;IACxB,IAAI,CAAC;QACH,OAAO,YAAY,CAAC,OAAO,EAAE,CAAC,GAAG,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACpE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,KAAK,UAAU,iBAAiB;IAC9B,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,eAAe,CAAC,CAAC;QAC1C,IAAI,GAAG,CAAC,OAAO;YAAE,OAAO,GAAG,CAAC,OAAO,CAAC;IACtC,CAAC;IAAC,MAAM,CAAC,CAAA,CAAC;IACV,OAAO,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,KAAK,CAAC,QAAQ,CAAC,CAAC;AACpD,CAAC;AAED,KAAK,UAAU,kBAAkB;IAC/B,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,4BAA4B,CAAC,CAAC;QACvD,IAAI,GAAG,CAAC,IAAI;YAAE,OAAO,GAAG,CAAC,IAAI,CAAC;IAChC,CAAC;IAAC,MAAM,CAAC,CAAA,CAAC;IACV,OAAO,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,KAAK,CAAC,SAAS,CAAC,CAAC;AACtD,CAAC;AAED,IAAI,WAAW,GAAkB,IAAI,CAAC;AACtC,IAAI,YAAY,GAAkB,IAAI,CAAC;AAEvC,MAAM,CAAC,KAAK,UAAU,UAAU;IAC9B,WAAW,GAAG,MAAM,iBAAiB,EAAE,CAAC;IACxC,YAAY,GAAG,MAAM,kBAAkB,EAAE,CAAC;IAE1C,IAAI,CAAC,WAAW,EAAE,CAAC;QACjB,OAAO,CAAC,KAAK,CACX,4BAA4B;YAC5B,4EAA4E;YAC5E,4CAA4C,CAC7C,CAAC;QACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,IAAI,CAAC,YAAY,EAAE,CAAC;QAClB,OAAO,CAAC,KAAK,CACX,6BAA6B;YAC7B,wCAAwC;YACxC,qEAAqE;YACrE,6CAA6C,CAC9C,CAAC;QACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,WAAW,GAAG,WAAW,CAAC;IACtC,OAAO,CAAC,GAAG,CAAC,YAAY,GAAG,YAAY,CAAC;AAC1C,CAAC;AAED,MAAM,UAAU,aAAa;IAC3B,IAAI,CAAC,WAAW;QAAE,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;IAC7D,OAAO,WAAW,CAAC;AACrB,CAAC;AAED,MAAM,UAAU,cAAc;IAC5B,IAAI,CAAC,YAAY;QAAE,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;IAC9D,OAAO,YAAY,CAAC;AACtB,CAAC"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,212 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { resolve, basename, dirname, join } from 'path';
4
+ import { existsSync, mkdirSync, readdirSync } from 'fs';
5
+ import { rm } from 'fs/promises';
6
+ import { createRequire } from 'module';
7
+ import { runPipeline } from './pipeline.js';
8
+ import { DEFAULT_CONFIG } from './types.js';
9
+ import { ensureDeps } from './ffmpeg-paths.js';
10
+ const require = createRequire(import.meta.url);
11
+ const pkg = require('../package.json');
12
+ const program = new Command();
13
+ program
14
+ .name('qa-video')
15
+ .description('Generate flashcard videos from YAML Q&A files with TTS narration')
16
+ .version(pkg.version);
17
+ function buildConfig(inputPath, opts) {
18
+ const inputName = basename(inputPath, '.yaml').replace(/\.yml$/, '');
19
+ const outputPath = opts.output
20
+ ? resolve(opts.output)
21
+ : join(dirname(inputPath), '..', 'output', `${inputName}.mp4`);
22
+ const outputDir = dirname(outputPath);
23
+ mkdirSync(outputDir, { recursive: true });
24
+ const tempDir = opts.tempDir
25
+ ? resolve(opts.tempDir)
26
+ : join(dirname(outputPath), '.tmp', inputName);
27
+ mkdirSync(tempDir, { recursive: true });
28
+ return {
29
+ inputPath,
30
+ outputPath,
31
+ tempDir,
32
+ voice: opts.voice,
33
+ questionDelay: parseFloat(opts.questionDelay),
34
+ answerDelay: parseFloat(opts.answerDelay),
35
+ cardGap: parseFloat(opts.cardGap),
36
+ fontSize: parseInt(opts.fontSize),
37
+ backgroundColor: DEFAULT_CONFIG.backgroundColor,
38
+ questionColor: DEFAULT_CONFIG.questionColor,
39
+ answerColor: DEFAULT_CONFIG.answerColor,
40
+ textColor: DEFAULT_CONFIG.textColor,
41
+ width: DEFAULT_CONFIG.width,
42
+ height: DEFAULT_CONFIG.height,
43
+ force: opts.force ?? false,
44
+ };
45
+ }
46
+ const sharedOptions = (cmd) => cmd
47
+ .option('--voice <name>', `TTS voice name`, DEFAULT_CONFIG.voice)
48
+ .option('--question-delay <seconds>', 'Pause after question speech', String(DEFAULT_CONFIG.questionDelay))
49
+ .option('--answer-delay <seconds>', 'Pause after answer speech', String(DEFAULT_CONFIG.answerDelay))
50
+ .option('--card-gap <seconds>', 'Gap between cards', String(DEFAULT_CONFIG.cardGap))
51
+ .option('--font-size <px>', 'Font size for slide text', String(DEFAULT_CONFIG.fontSize))
52
+ .option('--temp-dir <path>', 'Temporary directory for intermediate files', '')
53
+ .option('--force', 'Regenerate all artifacts, ignoring cache', false);
54
+ // Single file
55
+ sharedOptions(program
56
+ .command('generate')
57
+ .description('Generate a video from a single YAML Q&A file')
58
+ .requiredOption('-i, --input <path>', 'Path to YAML file')
59
+ .option('-o, --output <path>', 'Output video file path')).action(async (opts) => {
60
+ try {
61
+ const inputPath = resolve(opts.input);
62
+ if (!existsSync(inputPath)) {
63
+ console.error(`Error: Input file not found: ${inputPath}`);
64
+ process.exit(1);
65
+ }
66
+ const config = buildConfig(inputPath, opts);
67
+ console.log(`\n╔══════════════════════════════════╗`);
68
+ console.log(`║ QA Video Generator ║`);
69
+ console.log(`╚══════════════════════════════════╝`);
70
+ console.log(`Input: ${config.inputPath}`);
71
+ console.log(`Output: ${config.outputPath}`);
72
+ await runPipeline(config);
73
+ }
74
+ catch (err) {
75
+ console.error(`\nError: ${err.message}`);
76
+ if (process.env.DEBUG)
77
+ console.error(err.stack);
78
+ process.exit(1);
79
+ }
80
+ });
81
+ // Batch: all files in a directory
82
+ sharedOptions(program
83
+ .command('batch')
84
+ .description('Generate videos for all YAML files in a directory')
85
+ .requiredOption('-d, --dir <path>', 'Directory containing YAML files')
86
+ .option('-o, --output-dir <path>', 'Output directory for videos')).action(async (opts) => {
87
+ try {
88
+ const dirPath = resolve(opts.dir);
89
+ if (!existsSync(dirPath)) {
90
+ console.error(`Error: Directory not found: ${dirPath}`);
91
+ process.exit(1);
92
+ }
93
+ const yamlFiles = readdirSync(dirPath)
94
+ .filter(f => f.endsWith('.yaml') || f.endsWith('.yml'))
95
+ .sort();
96
+ if (yamlFiles.length === 0) {
97
+ console.error(`Error: No YAML files found in: ${dirPath}`);
98
+ process.exit(1);
99
+ }
100
+ console.log(`\n╔══════════════════════════════════╗`);
101
+ console.log(`║ QA Video Generator — Batch ║`);
102
+ console.log(`╚══════════════════════════════════╝`);
103
+ console.log(`Directory: ${dirPath}`);
104
+ console.log(`Files: ${yamlFiles.length}`);
105
+ console.log(`Files: ${yamlFiles.join(', ')}`);
106
+ const batchStart = Date.now();
107
+ const results = [];
108
+ for (let fi = 0; fi < yamlFiles.length; fi++) {
109
+ const file = yamlFiles[fi];
110
+ const inputPath = join(dirPath, file);
111
+ console.log(`\n${'═'.repeat(60)}`);
112
+ console.log(` FILE ${fi + 1}/${yamlFiles.length}: ${file}`);
113
+ console.log(`${'═'.repeat(60)}`);
114
+ const fileStart = Date.now();
115
+ try {
116
+ const fileOpts = {
117
+ ...opts,
118
+ output: opts.outputDir
119
+ ? join(resolve(opts.outputDir), basename(file, '.yaml').replace(/\.yml$/, '') + '.mp4')
120
+ : undefined,
121
+ };
122
+ const config = buildConfig(inputPath, fileOpts);
123
+ console.log(`Output: ${config.outputPath}`);
124
+ await runPipeline(config);
125
+ const timeStr = ((Date.now() - fileStart) / 1000).toFixed(1) + 's';
126
+ results.push({ file, status: 'OK', time: timeStr });
127
+ }
128
+ catch (err) {
129
+ const timeStr = ((Date.now() - fileStart) / 1000).toFixed(1) + 's';
130
+ results.push({ file, status: `FAILED: ${err.message}`, time: timeStr });
131
+ console.error(` Error processing ${file}: ${err.message}`);
132
+ }
133
+ }
134
+ // Batch summary
135
+ const totalTime = ((Date.now() - batchStart) / 1000).toFixed(1);
136
+ const ok = results.filter(r => r.status === 'OK').length;
137
+ const failed = results.length - ok;
138
+ console.log(`\n${'═'.repeat(60)}`);
139
+ console.log(` BATCH SUMMARY`);
140
+ console.log(`${'═'.repeat(60)}`);
141
+ for (const r of results) {
142
+ const icon = r.status === 'OK' ? 'OK' : 'FAIL';
143
+ console.log(` [${icon}] ${r.file} (${r.time})`);
144
+ }
145
+ console.log(`\n Total: ${ok} succeeded, ${failed} failed, ${totalTime}s elapsed`);
146
+ if (failed > 0)
147
+ process.exit(1);
148
+ }
149
+ catch (err) {
150
+ console.error(`\nError: ${err.message}`);
151
+ if (process.env.DEBUG)
152
+ console.error(err.stack);
153
+ process.exit(1);
154
+ }
155
+ });
156
+ // Clear cached artifacts
157
+ program
158
+ .command('clear')
159
+ .description('Remove cached artifacts (temp files)')
160
+ .option('-d, --dir <path>', 'YAML directory (clears all caches for that dir)')
161
+ .option('-i, --input <path>', 'Single YAML file (clears cache for that file)')
162
+ .option('--output-dir <path>', 'Output directory containing .tmp folder')
163
+ .action(async (opts) => {
164
+ try {
165
+ const targets = [];
166
+ if (opts.input) {
167
+ const inputPath = resolve(opts.input);
168
+ const inputName = basename(inputPath, '.yaml').replace(/\.yml$/, '');
169
+ const tmpDir = opts.outputDir
170
+ ? join(resolve(opts.outputDir), '.tmp', inputName)
171
+ : join(dirname(inputPath), '..', 'output', '.tmp', inputName);
172
+ targets.push(tmpDir);
173
+ }
174
+ else if (opts.dir) {
175
+ const dirPath = resolve(opts.dir);
176
+ const yamlFiles = readdirSync(dirPath).filter(f => f.endsWith('.yaml') || f.endsWith('.yml'));
177
+ for (const file of yamlFiles) {
178
+ const inputName = basename(file, '.yaml').replace(/\.yml$/, '');
179
+ const tmpDir = opts.outputDir
180
+ ? join(resolve(opts.outputDir), '.tmp', inputName)
181
+ : join(dirname(join(dirPath, file)), '..', 'output', '.tmp', inputName);
182
+ targets.push(tmpDir);
183
+ }
184
+ }
185
+ else {
186
+ // Default: clear all .tmp under output/
187
+ const defaultTmp = join(process.cwd(), 'output', '.tmp');
188
+ targets.push(defaultTmp);
189
+ }
190
+ let cleared = 0;
191
+ for (const dir of targets) {
192
+ if (existsSync(dir)) {
193
+ await rm(dir, { recursive: true, force: true });
194
+ console.log(` Removed: ${dir}`);
195
+ cleared++;
196
+ }
197
+ }
198
+ if (cleared === 0) {
199
+ console.log(' No cached artifacts found.');
200
+ }
201
+ else {
202
+ console.log(`\n Cleared ${cleared} cache(s).`);
203
+ }
204
+ }
205
+ catch (err) {
206
+ console.error(`Error: ${err.message}`);
207
+ process.exit(1);
208
+ }
209
+ });
210
+ await ensureDeps();
211
+ program.parse();
212
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import { YamlInput } from './types.js';
2
+ export declare function parseYamlFile(filePath: string): Promise<YamlInput>;
package/dist/parser.js ADDED
@@ -0,0 +1,32 @@
1
+ import { readFile } from 'fs/promises';
2
+ import { parse } from 'yaml';
3
+ export async function parseYamlFile(filePath) {
4
+ const content = await readFile(filePath, 'utf-8');
5
+ const data = parse(content);
6
+ if (!data || typeof data !== 'object') {
7
+ throw new Error(`Invalid YAML file: ${filePath}`);
8
+ }
9
+ // Support both "questions" and "cards" array keys
10
+ const questions = data.questions || data.cards || [];
11
+ if (!Array.isArray(questions) || questions.length === 0) {
12
+ throw new Error(`No questions found in: ${filePath}`);
13
+ }
14
+ // Normalize card fields: support both q/a and question/answer
15
+ const normalizedQuestions = questions.map((card, i) => {
16
+ const question = card.question || card.q;
17
+ const answer = card.answer || card.a;
18
+ if (!question || !answer) {
19
+ throw new Error(`Card ${i + 1} is missing question or answer`);
20
+ }
21
+ return {
22
+ question: String(question).trim(),
23
+ answer: String(answer).trim(),
24
+ };
25
+ });
26
+ const config = data.config || {};
27
+ return {
28
+ config,
29
+ questions: normalizedQuestions,
30
+ };
31
+ }
32
+ //# sourceMappingURL=parser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parser.js","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,KAAK,EAAE,MAAM,MAAM,CAAC;AAG7B,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,QAAgB;IAClD,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAClD,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC;IAE5B,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QACtC,MAAM,IAAI,KAAK,CAAC,sBAAsB,QAAQ,EAAE,CAAC,CAAC;IACpD,CAAC;IAED,kDAAkD;IAClD,MAAM,SAAS,GAAe,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;IAEjE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxD,MAAM,IAAI,KAAK,CAAC,0BAA0B,QAAQ,EAAE,CAAC,CAAC;IACxD,CAAC;IAED,8DAA8D;IAC9D,MAAM,mBAAmB,GAAe,SAAS,CAAC,GAAG,CAAC,CAAC,IAAS,EAAE,CAAS,EAAE,EAAE;QAC7E,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,CAAC,CAAC;QACzC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,CAAC,CAAC;QAErC,IAAI,CAAC,QAAQ,IAAI,CAAC,MAAM,EAAE,CAAC;YACzB,MAAM,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,gCAAgC,CAAC,CAAC;QACjE,CAAC;QAED,OAAO;YACL,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC,IAAI,EAAE;YACjC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE;SAC9B,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,MAAM,MAAM,GAAe,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC;IAE7C,OAAO;QACL,MAAM;QACN,SAAS,EAAE,mBAAmB;KAC/B,CAAC;AACJ,CAAC"}
@@ -0,0 +1,2 @@
1
+ import { PipelineConfig } from './types.js';
2
+ export declare function runPipeline(config: PipelineConfig): Promise<void>;
@@ -0,0 +1,205 @@
1
+ import { mkdirSync, existsSync } from 'fs';
2
+ import { parseYamlFile } from './parser.js';
3
+ import { initTTS, synthesize, getAudioDuration } from './tts.js';
4
+ import { renderSlide } from './renderer.js';
5
+ import { createSegmentClip, createSilentClip, concatenateClips } from './assembler.js';
6
+ import { DEFAULT_CONFIG } from './types.js';
7
+ import { sha, cachedPath, isCached } from './cache.js';
8
+ function elapsed(start) {
9
+ return ((Date.now() - start) / 1000).toFixed(1);
10
+ }
11
+ function formatDuration(seconds) {
12
+ const m = Math.floor(seconds / 60);
13
+ const s = Math.floor(seconds % 60);
14
+ return m > 0 ? `${m}m ${s}s` : `${s}s`;
15
+ }
16
+ export async function runPipeline(config) {
17
+ const pipelineStart = Date.now();
18
+ const { force } = config;
19
+ if (!existsSync(config.tempDir)) {
20
+ mkdirSync(config.tempDir, { recursive: true });
21
+ }
22
+ // ── Stage 1: Parse YAML ──
23
+ const stage1Start = Date.now();
24
+ console.log('\n── Stage 1/4: Parsing YAML ──');
25
+ const yamlData = await parseYamlFile(config.inputPath);
26
+ const cards = yamlData.questions;
27
+ const yamlConfig = yamlData.config;
28
+ if (yamlConfig.questionDelay !== undefined && config.questionDelay === DEFAULT_CONFIG.questionDelay) {
29
+ config.questionDelay = yamlConfig.questionDelay;
30
+ }
31
+ if (yamlConfig.answerDelay !== undefined && config.answerDelay === DEFAULT_CONFIG.answerDelay) {
32
+ config.answerDelay = yamlConfig.answerDelay;
33
+ }
34
+ if (yamlConfig.voice && config.voice === DEFAULT_CONFIG.voice) {
35
+ config.voice = yamlConfig.voice;
36
+ }
37
+ console.log(` Cards: ${cards.length}`);
38
+ console.log(` Voice: ${config.voice}`);
39
+ console.log(` Delays: question=${config.questionDelay}s, answer=${config.answerDelay}s, gap=${config.cardGap}s`);
40
+ if (force)
41
+ console.log(` Force: regenerating all artifacts`);
42
+ console.log(` Stage 1 done (${elapsed(stage1Start)}s)`);
43
+ // ── Stage 2: Synthesize speech ──
44
+ const stage2Start = Date.now();
45
+ console.log('\n── Stage 2/4: Synthesizing speech ──');
46
+ let ttsInitialized = false;
47
+ const segments = [];
48
+ let totalAudioDuration = 0;
49
+ let cachedAudioCount = 0;
50
+ for (let i = 0; i < cards.length; i++) {
51
+ const card = cards[i];
52
+ const progress = `[${i + 1}/${cards.length}]`;
53
+ // Question audio — SHA keyed on text + voice
54
+ const qAudioHash = `audio:${card.question}:${config.voice}`;
55
+ const qAudioPath = cachedPath(config.tempDir, `q_${i}`, qAudioHash, 'wav');
56
+ if (isCached(qAudioPath, force)) {
57
+ const qDuration = await getAudioDuration(qAudioPath);
58
+ totalAudioDuration += qDuration;
59
+ console.log(` ${progress} Q: "${card.question.substring(0, 50)}..." (cached, ${qDuration.toFixed(1)}s)`);
60
+ cachedAudioCount++;
61
+ segments.push({
62
+ type: 'question', text: card.question, cardIndex: i, totalCards: cards.length,
63
+ audioPath: qAudioPath, imagePath: '', audioDuration: qDuration,
64
+ totalDuration: qDuration + config.questionDelay,
65
+ });
66
+ }
67
+ else {
68
+ if (!ttsInitialized) {
69
+ await initTTS(config.voice);
70
+ ttsInitialized = true;
71
+ }
72
+ const qStart = Date.now();
73
+ process.stdout.write(` ${progress} Q: "${card.question.substring(0, 50)}..." `);
74
+ await synthesize(card.question, qAudioPath, config.voice);
75
+ const qDuration = await getAudioDuration(qAudioPath);
76
+ totalAudioDuration += qDuration;
77
+ console.log(`(${qDuration.toFixed(1)}s audio, ${elapsed(qStart)}s)`);
78
+ segments.push({
79
+ type: 'question', text: card.question, cardIndex: i, totalCards: cards.length,
80
+ audioPath: qAudioPath, imagePath: '', audioDuration: qDuration,
81
+ totalDuration: qDuration + config.questionDelay,
82
+ });
83
+ }
84
+ // Answer audio
85
+ const aAudioHash = `audio:${card.answer}:${config.voice}`;
86
+ const aAudioPath = cachedPath(config.tempDir, `a_${i}`, aAudioHash, 'wav');
87
+ if (isCached(aAudioPath, force)) {
88
+ const aDuration = await getAudioDuration(aAudioPath);
89
+ totalAudioDuration += aDuration;
90
+ console.log(` ${progress} A: "${card.answer.substring(0, 50)}..." (cached, ${aDuration.toFixed(1)}s)`);
91
+ cachedAudioCount++;
92
+ segments.push({
93
+ type: 'answer', text: card.answer, cardIndex: i, totalCards: cards.length,
94
+ audioPath: aAudioPath, imagePath: '', audioDuration: aDuration,
95
+ totalDuration: aDuration + config.answerDelay,
96
+ });
97
+ }
98
+ else {
99
+ if (!ttsInitialized) {
100
+ await initTTS(config.voice);
101
+ ttsInitialized = true;
102
+ }
103
+ const aStart = Date.now();
104
+ process.stdout.write(` ${progress} A: "${card.answer.substring(0, 50)}..." `);
105
+ await synthesize(card.answer, aAudioPath, config.voice);
106
+ const aDuration = await getAudioDuration(aAudioPath);
107
+ totalAudioDuration += aDuration;
108
+ console.log(`(${aDuration.toFixed(1)}s audio, ${elapsed(aStart)}s)`);
109
+ segments.push({
110
+ type: 'answer', text: card.answer, cardIndex: i, totalCards: cards.length,
111
+ audioPath: aAudioPath, imagePath: '', audioDuration: aDuration,
112
+ totalDuration: aDuration + config.answerDelay,
113
+ });
114
+ }
115
+ }
116
+ console.log(` Total audio: ${formatDuration(totalAudioDuration)} (${cachedAudioCount}/${segments.length} cached)`);
117
+ console.log(` Stage 2 done (${elapsed(stage2Start)}s)`);
118
+ // ── Stage 3: Render slides ──
119
+ const stage3Start = Date.now();
120
+ console.log('\n── Stage 3/4: Rendering slides ──');
121
+ let cachedSlideCount = 0;
122
+ for (let i = 0; i < segments.length; i++) {
123
+ const seg = segments[i];
124
+ const slideHash = `slide:${seg.text}:${seg.type}:${seg.cardIndex}:${seg.totalCards}:${config.fontSize}:${config.questionColor}:${config.answerColor}:${config.textColor}`;
125
+ const imagePath = cachedPath(config.tempDir, `slide_${i}`, slideHash, 'png');
126
+ seg.imagePath = imagePath;
127
+ if (isCached(imagePath, force)) {
128
+ cachedSlideCount++;
129
+ }
130
+ else {
131
+ await renderSlide(imagePath, {
132
+ text: seg.text, type: seg.type, cardIndex: seg.cardIndex,
133
+ totalCards: seg.totalCards, config,
134
+ });
135
+ }
136
+ }
137
+ // Gap slide
138
+ const gapSlideHash = `slide:gap:${config.backgroundColor}:${config.fontSize}:${cards.length}`;
139
+ const gapSlidePath = cachedPath(config.tempDir, 'slide_gap', gapSlideHash, 'png');
140
+ if (!isCached(gapSlidePath, force)) {
141
+ await renderSlide(gapSlidePath, {
142
+ text: '', type: 'question', cardIndex: 0, totalCards: cards.length,
143
+ config: { ...config, questionColor: config.backgroundColor },
144
+ });
145
+ }
146
+ else {
147
+ cachedSlideCount++;
148
+ }
149
+ console.log(` Rendered ${segments.length + 1} slides (${cachedSlideCount} cached)`);
150
+ console.log(` Stage 3 done (${elapsed(stage3Start)}s)`);
151
+ // ── Stage 4: Assemble video ──
152
+ const stage4Start = Date.now();
153
+ console.log('\n── Stage 4/4: Assembling video ──');
154
+ const clipPaths = [];
155
+ const totalClips = segments.length + (cards.length - 1);
156
+ let cachedClipCount = 0;
157
+ for (let i = 0; i < segments.length; i++) {
158
+ const seg = segments[i];
159
+ // Clip hash: based on audio hash + slide hash + timing
160
+ const clipHash = `clip:${sha(`audio:${seg.text}:${config.voice}`)}:${sha(`slide:${seg.text}:${seg.type}:${seg.cardIndex}:${seg.totalCards}:${config.fontSize}:${config.questionColor}:${config.answerColor}:${config.textColor}`)}:${seg.totalDuration}`;
161
+ const clipPath = cachedPath(config.tempDir, `clip_${i}`, clipHash, 'mp4');
162
+ const clipNum = clipPaths.length + 1;
163
+ if (isCached(clipPath, force)) {
164
+ console.log(` [${clipNum}/${totalClips}] ${seg.type} card ${seg.cardIndex + 1} (${seg.totalDuration.toFixed(1)}s) (cached)`);
165
+ cachedClipCount++;
166
+ }
167
+ else {
168
+ process.stdout.write(` [${clipNum}/${totalClips}] ${seg.type} card ${seg.cardIndex + 1} (${seg.totalDuration.toFixed(1)}s)... `);
169
+ const clipStart = Date.now();
170
+ await createSegmentClip(seg, clipPath);
171
+ console.log(`done (${elapsed(clipStart)}s)`);
172
+ }
173
+ clipPaths.push(clipPath);
174
+ if (seg.type === 'answer' && seg.cardIndex < seg.totalCards - 1 && config.cardGap > 0) {
175
+ const gapHash = `gap:${sha(gapSlideHash)}:${config.cardGap}`;
176
+ const gapClipPath = cachedPath(config.tempDir, `gap_${i}`, gapHash, 'mp4');
177
+ const gapNum = clipPaths.length + 1;
178
+ if (isCached(gapClipPath, force)) {
179
+ console.log(` [${gapNum}/${totalClips}] gap (${config.cardGap}s) (cached)`);
180
+ cachedClipCount++;
181
+ }
182
+ else {
183
+ process.stdout.write(` [${gapNum}/${totalClips}] gap (${config.cardGap}s)... `);
184
+ const gapStart = Date.now();
185
+ await createSilentClip(gapSlidePath, config.cardGap, gapClipPath);
186
+ console.log(`done (${elapsed(gapStart)}s)`);
187
+ }
188
+ clipPaths.push(gapClipPath);
189
+ }
190
+ }
191
+ process.stdout.write(` Concatenating ${clipPaths.length} clips... `);
192
+ const concatStart = Date.now();
193
+ await concatenateClips(clipPaths, config.outputPath, config.tempDir);
194
+ console.log(`done (${elapsed(concatStart)}s)`);
195
+ console.log(` Clips: ${cachedClipCount}/${totalClips} cached`);
196
+ console.log(` Stage 4 done (${elapsed(stage4Start)}s)`);
197
+ // ── Summary ──
198
+ const estimatedVideoDuration = segments.reduce((sum, s) => sum + s.totalDuration, 0) + (cards.length - 1) * config.cardGap;
199
+ console.log(`\n══ Complete ══`);
200
+ console.log(` Output: ${config.outputPath}`);
201
+ console.log(` Duration: ~${formatDuration(estimatedVideoDuration)}`);
202
+ console.log(` Cards: ${cards.length}`);
203
+ console.log(` Time: ${elapsed(pipelineStart)}s`);
204
+ }
205
+ //# sourceMappingURL=pipeline.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pipeline.js","sourceRoot":"","sources":["../src/pipeline.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import { PipelineConfig } from './types.js';
2
+ interface SlideOptions {
3
+ text: string;
4
+ type: 'question' | 'answer';
5
+ cardIndex: number;
6
+ totalCards: number;
7
+ config: PipelineConfig;
8
+ }
9
+ export declare function renderSlide(outputPath: string, options: SlideOptions): Promise<void>;
10
+ export {};
@@ -0,0 +1,89 @@
1
+ import { createCanvas } from '@napi-rs/canvas';
2
+ import { writeFile } from 'fs/promises';
3
+ function wrapText(ctx, text, maxWidth) {
4
+ const lines = [];
5
+ const paragraphs = text.split('\n');
6
+ for (const paragraph of paragraphs) {
7
+ if (paragraph.trim() === '') {
8
+ lines.push('');
9
+ continue;
10
+ }
11
+ const words = paragraph.split(/\s+/);
12
+ let currentLine = '';
13
+ for (const word of words) {
14
+ const testLine = currentLine ? `${currentLine} ${word}` : word;
15
+ const metrics = ctx.measureText(testLine);
16
+ if (metrics.width > maxWidth && currentLine) {
17
+ lines.push(currentLine);
18
+ currentLine = word;
19
+ }
20
+ else {
21
+ currentLine = testLine;
22
+ }
23
+ }
24
+ if (currentLine) {
25
+ lines.push(currentLine);
26
+ }
27
+ }
28
+ return lines;
29
+ }
30
+ export async function renderSlide(outputPath, options) {
31
+ const { text, type, cardIndex, totalCards, config } = options;
32
+ const { width, height, fontSize, textColor } = config;
33
+ const bgColor = type === 'question' ? config.questionColor : config.answerColor;
34
+ const canvas = createCanvas(width, height);
35
+ const ctx = canvas.getContext('2d');
36
+ // Background
37
+ ctx.fillStyle = bgColor;
38
+ ctx.fillRect(0, 0, width, height);
39
+ // Header bar
40
+ const headerHeight = 80;
41
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.3)';
42
+ ctx.fillRect(0, 0, width, headerHeight);
43
+ // Header text
44
+ ctx.fillStyle = textColor;
45
+ ctx.font = `bold 28px "Arial", "Helvetica", sans-serif`;
46
+ ctx.textAlign = 'left';
47
+ ctx.fillText(`${type === 'question' ? 'QUESTION' : 'ANSWER'} ${cardIndex + 1} of ${totalCards}`, 40, 50);
48
+ // Type badge
49
+ const badgeText = type === 'question' ? 'Q' : 'A';
50
+ const badgeColor = type === 'question' ? '#e94560' : '#0cca4a';
51
+ const badgeSize = 60;
52
+ const badgeX = width - 100;
53
+ const badgeY = 10;
54
+ ctx.fillStyle = badgeColor;
55
+ ctx.beginPath();
56
+ ctx.roundRect(badgeX, badgeY, badgeSize, badgeSize, 12);
57
+ ctx.fill();
58
+ ctx.fillStyle = '#ffffff';
59
+ ctx.font = `bold 36px "Arial", "Helvetica", sans-serif`;
60
+ ctx.textAlign = 'center';
61
+ ctx.fillText(badgeText, badgeX + badgeSize / 2, badgeY + 43);
62
+ // Main text
63
+ ctx.font = `${fontSize}px "Arial", "Helvetica", sans-serif`;
64
+ ctx.textAlign = 'center';
65
+ ctx.fillStyle = textColor;
66
+ const maxTextWidth = width - 200;
67
+ const lines = wrapText(ctx, text, maxTextWidth);
68
+ const lineHeight = fontSize * 1.4;
69
+ const totalTextHeight = lines.length * lineHeight;
70
+ // Center vertically (below header)
71
+ const contentAreaTop = headerHeight;
72
+ const contentAreaHeight = height - contentAreaTop;
73
+ let startY = contentAreaTop + (contentAreaHeight - totalTextHeight) / 2 + fontSize;
74
+ // Clamp so text doesn't go above header
75
+ if (startY < contentAreaTop + fontSize + 20) {
76
+ startY = contentAreaTop + fontSize + 20;
77
+ }
78
+ for (const line of lines) {
79
+ ctx.fillText(line, width / 2, startY);
80
+ startY += lineHeight;
81
+ }
82
+ // Footer
83
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.15)';
84
+ ctx.fillRect(0, height - 4, width, 4);
85
+ // Save PNG
86
+ const buffer = canvas.toBuffer('image/png');
87
+ await writeFile(outputPath, buffer);
88
+ }
89
+ //# sourceMappingURL=renderer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"renderer.js","sourceRoot":"","sources":["../src/renderer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAe,MAAM,iBAAiB,CAAC;AAC5D,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAWxC,SAAS,QAAQ,CACf,GAAQ,EACR,IAAY,EACZ,QAAgB;IAEhB,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAEpC,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,IAAI,SAAS,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;YAC5B,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACf,SAAS;QACX,CAAC;QAED,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QACrC,IAAI,WAAW,GAAG,EAAE,CAAC;QAErB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,MAAM,QAAQ,GAAG,WAAW,CAAC,CAAC,CAAC,GAAG,WAAW,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;YAC/D,MAAM,OAAO,GAAG,GAAG,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;YAE1C,IAAI,OAAO,CAAC,KAAK,GAAG,QAAQ,IAAI,WAAW,EAAE,CAAC;gBAC5C,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;gBACxB,WAAW,GAAG,IAAI,CAAC;YACrB,CAAC;iBAAM,CAAC;gBACN,WAAW,GAAG,QAAQ,CAAC;YACzB,CAAC;QACH,CAAC;QAED,IAAI,WAAW,EAAE,CAAC;YAChB,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QAC1B,CAAC;IACH,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,UAAkB,EAClB,OAAqB;IAErB,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC;IAC9D,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,MAAM,CAAC;IAEtD,MAAM,OAAO,GAAG,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC;IAEhF,MAAM,MAAM,GAAG,YAAY,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;IAC3C,MAAM,GAAG,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;IAEpC,aAAa;IACb,GAAG,CAAC,SAAS,GAAG,OAAO,CAAC;IACxB,GAAG,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;IAElC,aAAa;IACb,MAAM,YAAY,GAAG,EAAE,CAAC;IACxB,GAAG,CAAC,SAAS,GAAG,oBAAoB,CAAC;IACrC,GAAG,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,YAAY,CAAC,CAAC;IAExC,cAAc;IACd,GAAG,CAAC,SAAS,GAAG,SAAS,CAAC;IAC1B,GAAG,CAAC,IAAI,GAAG,4CAA4C,CAAC;IACxD,GAAG,CAAC,SAAS,GAAG,MAAM,CAAC;IACvB,GAAG,CAAC,QAAQ,CACV,GAAG,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,QAAQ,IAAI,SAAS,GAAG,CAAC,OAAO,UAAU,EAAE,EAClF,EAAE,EACF,EAAE,CACH,CAAC;IAEF,aAAa;IACb,MAAM,SAAS,GAAG,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC;IAClD,MAAM,UAAU,GAAG,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC;IAC/D,MAAM,SAAS,GAAG,EAAE,CAAC;IACrB,MAAM,MAAM,GAAG,KAAK,GAAG,GAAG,CAAC;IAC3B,MAAM,MAAM,GAAG,EAAE,CAAC;IAElB,GAAG,CAAC,SAAS,GAAG,UAAU,CAAC;IAC3B,GAAG,CAAC,SAAS,EAAE,CAAC;IAChB,GAAG,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,EAAE,CAAC,CAAC;IACxD,GAAG,CAAC,IAAI,EAAE,CAAC;IAEX,GAAG,CAAC,SAAS,GAAG,SAAS,CAAC;IAC1B,GAAG,CAAC,IAAI,GAAG,4CAA4C,CAAC;IACxD,GAAG,CAAC,SAAS,GAAG,QAAQ,CAAC;IACzB,GAAG,CAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,CAAC,EAAE,MAAM,GAAG,EAAE,CAAC,CAAC;IAE7D,YAAY;IACZ,GAAG,CAAC,IAAI,GAAG,GAAG,QAAQ,qCAAqC,CAAC;IAC5D,GAAG,CAAC,SAAS,GAAG,QAAQ,CAAC;IACzB,GAAG,CAAC,SAAS,GAAG,SAAS,CAAC;IAE1B,MAAM,YAAY,GAAG,KAAK,GAAG,GAAG,CAAC;IACjC,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,EAAE,IAAI,EAAE,YAAY,CAAC,CAAC;IAChD,MAAM,UAAU,GAAG,QAAQ,GAAG,GAAG,CAAC;IAClC,MAAM,eAAe,GAAG,KAAK,CAAC,MAAM,GAAG,UAAU,CAAC;IAElD,mCAAmC;IACnC,MAAM,cAAc,GAAG,YAAY,CAAC;IACpC,MAAM,iBAAiB,GAAG,MAAM,GAAG,cAAc,CAAC;IAClD,IAAI,MAAM,GAAG,cAAc,GAAG,CAAC,iBAAiB,GAAG,eAAe,CAAC,GAAG,CAAC,GAAG,QAAQ,CAAC;IAEnF,wCAAwC;IACxC,IAAI,MAAM,GAAG,cAAc,GAAG,QAAQ,GAAG,EAAE,EAAE,CAAC;QAC5C,MAAM,GAAG,cAAc,GAAG,QAAQ,GAAG,EAAE,CAAC;IAC1C,CAAC;IAED,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,KAAK,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC;QACtC,MAAM,IAAI,UAAU,CAAC;IACvB,CAAC;IAED,SAAS;IACT,GAAG,CAAC,SAAS,GAAG,2BAA2B,CAAC;IAC5C,GAAG,CAAC,QAAQ,CAAC,CAAC,EAAE,MAAM,GAAG,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;IAEtC,WAAW;IACX,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;IAC5C,MAAM,SAAS,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;AACtC,CAAC"}
package/dist/tts.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export declare function initTTS(voice?: string): Promise<void>;
2
+ export declare function synthesize(text: string, outputPath: string, voice: string): Promise<void>;
3
+ export declare function getAudioDuration(filePath: string): Promise<number>;
package/dist/tts.js ADDED
@@ -0,0 +1,33 @@
1
+ let ttsInstance = null;
2
+ export async function initTTS(voice) {
3
+ const { KokoroTTS } = await import('kokoro-js');
4
+ console.log('Loading Kokoro TTS model (first run downloads ~80MB model)...');
5
+ ttsInstance = await KokoroTTS.from_pretrained('onnx-community/Kokoro-82M-v1.0-ONNX', { dtype: 'q8' });
6
+ console.log('TTS model loaded successfully.');
7
+ }
8
+ export async function synthesize(text, outputPath, voice) {
9
+ if (!ttsInstance) {
10
+ throw new Error('TTS not initialized. Call initTTS() first.');
11
+ }
12
+ const audio = await ttsInstance.generate(text, { voice });
13
+ audio.save(outputPath);
14
+ }
15
+ export async function getAudioDuration(filePath) {
16
+ const ffmpeg = await import('fluent-ffmpeg');
17
+ const FfmpegCommand = ffmpeg.default;
18
+ return new Promise((resolve, reject) => {
19
+ FfmpegCommand.ffprobe(filePath, (err, metadata) => {
20
+ if (err) {
21
+ reject(new Error(`Failed to probe audio file: ${err.message}`));
22
+ return;
23
+ }
24
+ const duration = metadata?.format?.duration;
25
+ if (typeof duration !== 'number' || isNaN(duration)) {
26
+ reject(new Error(`Could not determine duration for: ${filePath}`));
27
+ return;
28
+ }
29
+ resolve(duration);
30
+ });
31
+ });
32
+ }
33
+ //# sourceMappingURL=tts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tts.js","sourceRoot":"","sources":["../src/tts.ts"],"names":[],"mappings":"AAAA,IAAI,WAAW,GAAQ,IAAI,CAAC;AAE5B,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,KAAc;IAC1C,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC,CAAC;IAChD,OAAO,CAAC,GAAG,CAAC,+DAA+D,CAAC,CAAC;IAC7E,WAAW,GAAG,MAAM,SAAS,CAAC,eAAe,CAC3C,qCAAqC,EACrC,EAAE,KAAK,EAAE,IAAI,EAAE,CAChB,CAAC;IACF,OAAO,CAAC,GAAG,CAAC,gCAAgC,CAAC,CAAC;AAChD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,IAAY,EACZ,UAAkB,EAClB,KAAa;IAEb,IAAI,CAAC,WAAW,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CAAC,4CAA4C,CAAC,CAAC;IAChE,CAAC;IAED,MAAM,KAAK,GAAG,MAAM,WAAW,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;IAC1D,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;AACzB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,QAAgB;IACrD,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,eAAe,CAAC,CAAC;IAC7C,MAAM,aAAa,GAAG,MAAM,CAAC,OAAO,CAAC;IAErC,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,aAAa,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,GAAiB,EAAE,QAAa,EAAE,EAAE;YACnE,IAAI,GAAG,EAAE,CAAC;gBACR,MAAM,CAAC,IAAI,KAAK,CAAC,+BAA+B,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;gBAChE,OAAO;YACT,CAAC;YACD,MAAM,QAAQ,GAAG,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC;YAC5C,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC;gBACpD,MAAM,CAAC,IAAI,KAAK,CAAC,qCAAqC,QAAQ,EAAE,CAAC,CAAC,CAAC;gBACnE,OAAO;YACT,CAAC;YACD,OAAO,CAAC,QAAQ,CAAC,CAAC;QACpB,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,47 @@
1
+ export interface YamlConfig {
2
+ questionDelay?: number;
3
+ answerDelay?: number;
4
+ cardGap?: number;
5
+ voice?: string;
6
+ fontSize?: number;
7
+ backgroundColor?: string;
8
+ questionColor?: string;
9
+ answerColor?: string;
10
+ textColor?: string;
11
+ }
12
+ export interface YamlCard {
13
+ question: string;
14
+ answer: string;
15
+ }
16
+ export interface YamlInput {
17
+ config: YamlConfig;
18
+ questions: YamlCard[];
19
+ }
20
+ export interface Segment {
21
+ type: 'question' | 'answer';
22
+ text: string;
23
+ cardIndex: number;
24
+ totalCards: number;
25
+ audioPath: string;
26
+ imagePath: string;
27
+ audioDuration: number;
28
+ totalDuration: number;
29
+ }
30
+ export interface PipelineConfig {
31
+ inputPath: string;
32
+ outputPath: string;
33
+ voice: string;
34
+ tempDir: string;
35
+ questionDelay: number;
36
+ answerDelay: number;
37
+ cardGap: number;
38
+ fontSize: number;
39
+ backgroundColor: string;
40
+ questionColor: string;
41
+ answerColor: string;
42
+ textColor: string;
43
+ width: number;
44
+ height: number;
45
+ force: boolean;
46
+ }
47
+ export declare const DEFAULT_CONFIG: Omit<PipelineConfig, 'inputPath' | 'outputPath' | 'tempDir'>;
package/dist/types.js ADDED
@@ -0,0 +1,15 @@
1
+ export const DEFAULT_CONFIG = {
2
+ voice: 'af_heart',
3
+ questionDelay: 2,
4
+ answerDelay: 3,
5
+ cardGap: 1,
6
+ fontSize: 52,
7
+ backgroundColor: '#1a1a2e',
8
+ questionColor: '#16213e',
9
+ answerColor: '#0f3460',
10
+ textColor: '#ffffff',
11
+ width: 1920,
12
+ height: 1080,
13
+ force: false,
14
+ };
15
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAmDA,MAAM,CAAC,MAAM,cAAc,GAAiE;IAC1F,KAAK,EAAE,UAAU;IACjB,aAAa,EAAE,CAAC;IAChB,WAAW,EAAE,CAAC;IACd,OAAO,EAAE,CAAC;IACV,QAAQ,EAAE,EAAE;IACZ,eAAe,EAAE,SAAS;IAC1B,aAAa,EAAE,SAAS;IACxB,WAAW,EAAE,SAAS;IACtB,SAAS,EAAE,SAAS;IACpB,KAAK,EAAE,IAAI;IACX,MAAM,EAAE,IAAI;IACZ,KAAK,EAAE,KAAK;CACb,CAAC"}
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "qa-video",
3
+ "version": "1.0.0",
4
+ "description": "Generate flashcard videos from YAML Q&A files with TTS narration",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "qa-video": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md"
13
+ ],
14
+ "engines": {
15
+ "node": ">=18"
16
+ },
17
+ "scripts": {
18
+ "build": "tsc",
19
+ "prepublishOnly": "tsc",
20
+ "start": "node dist/index.js",
21
+ "dev": "tsx src/index.ts"
22
+ },
23
+ "license": "MIT",
24
+ "dependencies": {
25
+ "@ffprobe-installer/ffprobe": "^2.1.2",
26
+ "@napi-rs/canvas": "^0.1.92",
27
+ "commander": "^14.0.3",
28
+ "ffmpeg-static": "^5.3.0",
29
+ "fluent-ffmpeg": "^2.1.3",
30
+ "fs-extra": "^11.3.3",
31
+ "kokoro-js": "^1.2.1",
32
+ "yaml": "^2.8.2"
33
+ },
34
+ "devDependencies": {
35
+ "@types/fluent-ffmpeg": "^2.1.28",
36
+ "@types/fs-extra": "^11.0.4",
37
+ "@types/node": "^25.2.3",
38
+ "tsx": "^4.21.0",
39
+ "typescript": "^5.9.3"
40
+ }
41
+ }