@argo-video/cli 0.1.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/.claude/settings.local.json +34 -0
- package/DESIGN.md +261 -0
- package/README.md +192 -0
- package/bin/argo.js +2 -0
- package/docs/enhancement-proposal.md +262 -0
- package/docs/superpowers/plans/2026-03-12-argo.md +208 -0
- package/docs/superpowers/plans/2026-03-12-editorial-overlay-system.md +1560 -0
- package/docs/superpowers/plans/2026-03-13-npm-rename-skill-showcase.md +499 -0
- package/docs/superpowers/specs/2026-03-13-npm-rename-skill-showcase-design.md +109 -0
- package/package.json +38 -0
- package/skills/argo-demo-creator.md +355 -0
- package/src/asset-server.ts +81 -0
- package/src/captions.ts +36 -0
- package/src/cli.ts +97 -0
- package/src/config.ts +125 -0
- package/src/export.ts +93 -0
- package/src/fixtures.ts +50 -0
- package/src/index.ts +41 -0
- package/src/init.ts +114 -0
- package/src/narration.ts +31 -0
- package/src/overlays/index.ts +54 -0
- package/src/overlays/manifest.ts +68 -0
- package/src/overlays/motion.ts +27 -0
- package/src/overlays/templates.ts +121 -0
- package/src/overlays/types.ts +73 -0
- package/src/overlays/zones.ts +82 -0
- package/src/pipeline.ts +120 -0
- package/src/record.ts +123 -0
- package/src/tts/align.ts +75 -0
- package/src/tts/cache.ts +65 -0
- package/src/tts/engine.ts +147 -0
- package/src/tts/generate.ts +83 -0
- package/src/tts/kokoro.ts +51 -0
- package/tests/asset-server.test.ts +67 -0
- package/tests/captions.test.ts +76 -0
- package/tests/cli.test.ts +131 -0
- package/tests/config.test.ts +150 -0
- package/tests/e2e/fake-server.ts +45 -0
- package/tests/e2e/record.e2e.test.ts +131 -0
- package/tests/export.test.ts +155 -0
- package/tests/fixtures.test.ts +74 -0
- package/tests/init.test.ts +77 -0
- package/tests/narration.test.ts +120 -0
- package/tests/overlays/index.test.ts +73 -0
- package/tests/overlays/manifest.test.ts +120 -0
- package/tests/overlays/motion.test.ts +34 -0
- package/tests/overlays/templates.test.ts +69 -0
- package/tests/overlays/types.test.ts +36 -0
- package/tests/overlays/zones.test.ts +49 -0
- package/tests/pipeline.test.ts +177 -0
- package/tests/record.test.ts +87 -0
- package/tests/tts/align.test.ts +118 -0
- package/tests/tts/cache.test.ts +110 -0
- package/tests/tts/engine.test.ts +204 -0
- package/tests/tts/generate.test.ts +177 -0
- package/tests/tts/kokoro.test.ts +25 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
createWavBuffer,
|
|
4
|
+
parseWavHeader,
|
|
5
|
+
createMockTTSEngine,
|
|
6
|
+
type TTSEngine,
|
|
7
|
+
type TTSEngineOptions,
|
|
8
|
+
} from '../../src/tts/engine.js';
|
|
9
|
+
|
|
10
|
+
describe('createWavBuffer', () => {
|
|
11
|
+
it('produces a buffer starting with RIFF....WAVE', () => {
|
|
12
|
+
const samples = new Float32Array(100);
|
|
13
|
+
const buf = createWavBuffer(samples);
|
|
14
|
+
expect(buf.toString('ascii', 0, 4)).toBe('RIFF');
|
|
15
|
+
expect(buf.toString('ascii', 8, 12)).toBe('WAVE');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('writes correct fmt chunk for 32-bit float', () => {
|
|
19
|
+
const samples = new Float32Array(10);
|
|
20
|
+
const buf = createWavBuffer(samples, 24000);
|
|
21
|
+
|
|
22
|
+
// fmt chunk starts at byte 12
|
|
23
|
+
expect(buf.toString('ascii', 12, 16)).toBe('fmt ');
|
|
24
|
+
const fmtSize = buf.readUInt32LE(16);
|
|
25
|
+
expect(fmtSize).toBe(16);
|
|
26
|
+
|
|
27
|
+
const audioFormat = buf.readUInt16LE(20);
|
|
28
|
+
expect(audioFormat).toBe(3); // IEEE float
|
|
29
|
+
|
|
30
|
+
const numChannels = buf.readUInt16LE(22);
|
|
31
|
+
expect(numChannels).toBe(1); // mono
|
|
32
|
+
|
|
33
|
+
const sampleRate = buf.readUInt32LE(24);
|
|
34
|
+
expect(sampleRate).toBe(24000);
|
|
35
|
+
|
|
36
|
+
const bitsPerSample = buf.readUInt16LE(34);
|
|
37
|
+
expect(bitsPerSample).toBe(32);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('has correct RIFF chunk size', () => {
|
|
41
|
+
const samples = new Float32Array(50);
|
|
42
|
+
const buf = createWavBuffer(samples);
|
|
43
|
+
const riffSize = buf.readUInt32LE(4);
|
|
44
|
+
expect(riffSize).toBe(buf.length - 8);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('has correct data chunk size', () => {
|
|
48
|
+
const samples = new Float32Array(50);
|
|
49
|
+
const buf = createWavBuffer(samples);
|
|
50
|
+
// data chunk id at byte 36
|
|
51
|
+
expect(buf.toString('ascii', 36, 40)).toBe('data');
|
|
52
|
+
const dataSize = buf.readUInt32LE(40);
|
|
53
|
+
expect(dataSize).toBe(50 * 4); // 50 samples * 4 bytes each
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('total buffer length = 44 + sample data', () => {
|
|
57
|
+
const samples = new Float32Array(100);
|
|
58
|
+
const buf = createWavBuffer(samples);
|
|
59
|
+
expect(buf.length).toBe(44 + 100 * 4);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('preserves sample values', () => {
|
|
63
|
+
const samples = new Float32Array([0.5, -0.5, 1.0, -1.0, 0.0]);
|
|
64
|
+
const buf = createWavBuffer(samples);
|
|
65
|
+
for (let i = 0; i < samples.length; i++) {
|
|
66
|
+
const val = buf.readFloatLE(44 + i * 4);
|
|
67
|
+
expect(val).toBeCloseTo(samples[i], 5);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('respects custom sample rate', () => {
|
|
72
|
+
const samples = new Float32Array(10);
|
|
73
|
+
const buf = createWavBuffer(samples, 44100);
|
|
74
|
+
const sampleRate = buf.readUInt32LE(24);
|
|
75
|
+
expect(sampleRate).toBe(44100);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('uses default sample rate of 24000', () => {
|
|
79
|
+
const samples = new Float32Array(10);
|
|
80
|
+
const buf = createWavBuffer(samples);
|
|
81
|
+
expect(buf.readUInt32LE(24)).toBe(24000);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('parseWavHeader', () => {
|
|
86
|
+
it('round-trips with createWavBuffer', () => {
|
|
87
|
+
const samples = new Float32Array(200);
|
|
88
|
+
const buf = createWavBuffer(samples, 16000);
|
|
89
|
+
const header = parseWavHeader(buf);
|
|
90
|
+
|
|
91
|
+
expect(header.sampleRate).toBe(16000);
|
|
92
|
+
expect(header.numChannels).toBe(1);
|
|
93
|
+
expect(header.bitsPerSample).toBe(32);
|
|
94
|
+
expect(header.audioFormat).toBe(3);
|
|
95
|
+
expect(header.dataSize).toBe(200 * 4);
|
|
96
|
+
expect(header.dataOffset).toBe(44);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('computes durationMs correctly', () => {
|
|
100
|
+
const sampleRate = 24000;
|
|
101
|
+
const numSamples = sampleRate; // 1 second worth
|
|
102
|
+
const samples = new Float32Array(numSamples);
|
|
103
|
+
const buf = createWavBuffer(samples, sampleRate);
|
|
104
|
+
const header = parseWavHeader(buf);
|
|
105
|
+
expect(header.durationMs).toBeCloseTo(1000, 0);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('throws on buffer smaller than 44 bytes', () => {
|
|
109
|
+
const small = Buffer.alloc(20);
|
|
110
|
+
expect(() => parseWavHeader(small)).toThrow();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('throws on non-WAV buffer', () => {
|
|
114
|
+
const notWav = Buffer.alloc(100, 0);
|
|
115
|
+
notWav.write('NOTRIFF', 0, 'ascii');
|
|
116
|
+
expect(() => parseWavHeader(notWav)).toThrow();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('throws when WAVE marker is missing', () => {
|
|
120
|
+
const buf = Buffer.alloc(100, 0);
|
|
121
|
+
buf.write('RIFF', 0, 'ascii');
|
|
122
|
+
buf.write('NOOO', 8, 'ascii');
|
|
123
|
+
expect(() => parseWavHeader(buf)).toThrow();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('finds data chunk even with extra chunks', () => {
|
|
127
|
+
// Build a WAV with an extra chunk between fmt and data
|
|
128
|
+
const samples = new Float32Array(10);
|
|
129
|
+
const normal = createWavBuffer(samples);
|
|
130
|
+
|
|
131
|
+
// Insert a dummy chunk after fmt (at byte 36)
|
|
132
|
+
const dummyChunkId = Buffer.from('LIST', 'ascii');
|
|
133
|
+
const dummyChunkSize = Buffer.alloc(4);
|
|
134
|
+
dummyChunkSize.writeUInt32LE(8, 0);
|
|
135
|
+
const dummyChunkData = Buffer.alloc(8, 0xAB);
|
|
136
|
+
|
|
137
|
+
const before = normal.subarray(0, 36); // RIFF header + fmt chunk
|
|
138
|
+
const dataChunk = normal.subarray(36); // data chunk
|
|
139
|
+
|
|
140
|
+
// Fix RIFF size
|
|
141
|
+
const extended = Buffer.concat([before, dummyChunkId, dummyChunkSize, dummyChunkData, dataChunk]);
|
|
142
|
+
extended.writeUInt32LE(extended.length - 8, 4);
|
|
143
|
+
|
|
144
|
+
const header = parseWavHeader(extended);
|
|
145
|
+
expect(header.dataSize).toBe(10 * 4);
|
|
146
|
+
expect(header.dataOffset).toBe(44 + 16); // shifted by dummy chunk
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('createMockTTSEngine', () => {
|
|
151
|
+
it('implements TTSEngine interface', async () => {
|
|
152
|
+
const engine = createMockTTSEngine();
|
|
153
|
+
// Should have generate method
|
|
154
|
+
expect(typeof engine.generate).toBe('function');
|
|
155
|
+
const result = await engine.generate('hello', {});
|
|
156
|
+
expect(Buffer.isBuffer(result)).toBe(true);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('returns a valid WAV buffer', async () => {
|
|
160
|
+
const engine = createMockTTSEngine(500);
|
|
161
|
+
const buf = await engine.generate('test', {});
|
|
162
|
+
const header = parseWavHeader(buf);
|
|
163
|
+
expect(header.sampleRate).toBe(24000);
|
|
164
|
+
expect(header.numChannels).toBe(1);
|
|
165
|
+
expect(header.audioFormat).toBe(3);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('records calls for assertions', async () => {
|
|
169
|
+
const engine = createMockTTSEngine();
|
|
170
|
+
expect(engine.calls).toEqual([]);
|
|
171
|
+
|
|
172
|
+
await engine.generate('first', { voice: 'alloy' });
|
|
173
|
+
await engine.generate('second', { speed: 1.5 });
|
|
174
|
+
|
|
175
|
+
expect(engine.calls).toHaveLength(2);
|
|
176
|
+
expect(engine.calls[0]).toEqual({ text: 'first', options: { voice: 'alloy' } });
|
|
177
|
+
expect(engine.calls[1]).toEqual({ text: 'second', options: { speed: 1.5 } });
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('respects durationMs parameter', async () => {
|
|
181
|
+
const engine = createMockTTSEngine(1000);
|
|
182
|
+
const buf = await engine.generate('test', {});
|
|
183
|
+
const header = parseWavHeader(buf);
|
|
184
|
+
// 1000ms at 24000 Hz = 24000 samples
|
|
185
|
+
expect(header.durationMs).toBeCloseTo(1000, -1);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('defaults to 500ms duration', async () => {
|
|
189
|
+
const engine = createMockTTSEngine();
|
|
190
|
+
const buf = await engine.generate('test', {});
|
|
191
|
+
const header = parseWavHeader(buf);
|
|
192
|
+
expect(header.durationMs).toBeCloseTo(500, -1);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('produces silent audio (all zeros)', async () => {
|
|
196
|
+
const engine = createMockTTSEngine(100);
|
|
197
|
+
const buf = await engine.generate('test', {});
|
|
198
|
+
const header = parseWavHeader(buf);
|
|
199
|
+
for (let i = 0; i < header.dataSize; i += 4) {
|
|
200
|
+
const sample = buf.readFloatLE(header.dataOffset + i);
|
|
201
|
+
expect(sample).toBe(0);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
});
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import { createMockTTSEngine } from '../../src/tts/engine.js';
|
|
6
|
+
import { generateClips } from '../../src/tts/generate.js';
|
|
7
|
+
|
|
8
|
+
function makeTmpDir(): string {
|
|
9
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'argo-generate-test-'));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe('generateClips', () => {
|
|
13
|
+
let tmpDir: string;
|
|
14
|
+
let manifestPath: string;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
tmpDir = makeTmpDir();
|
|
18
|
+
manifestPath = path.join(tmpDir, 'manifest.json');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const demoName = 'test-demo';
|
|
26
|
+
|
|
27
|
+
function writeManifest(entries: unknown[]) {
|
|
28
|
+
fs.writeFileSync(manifestPath, JSON.stringify(entries));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
it('generates clips for all entries', async () => {
|
|
32
|
+
const entries = [
|
|
33
|
+
{ scene: 'intro', text: 'Hello world' },
|
|
34
|
+
{ scene: 'outro', text: 'Goodbye world' },
|
|
35
|
+
];
|
|
36
|
+
writeManifest(entries);
|
|
37
|
+
const engine = createMockTTSEngine();
|
|
38
|
+
|
|
39
|
+
const results = await generateClips({
|
|
40
|
+
manifestPath,
|
|
41
|
+
demoName,
|
|
42
|
+
engine,
|
|
43
|
+
projectRoot: tmpDir,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
expect(results).toHaveLength(2);
|
|
47
|
+
expect(engine.calls).toHaveLength(2);
|
|
48
|
+
expect(results[0].scene).toBe('intro');
|
|
49
|
+
expect(results[1].scene).toBe('outro');
|
|
50
|
+
// Files should exist on disk
|
|
51
|
+
for (const r of results) {
|
|
52
|
+
expect(fs.existsSync(r.clipPath)).toBe(true);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('uses cached clips on second run', async () => {
|
|
57
|
+
const entries = [
|
|
58
|
+
{ scene: 'intro', text: 'Hello world' },
|
|
59
|
+
];
|
|
60
|
+
writeManifest(entries);
|
|
61
|
+
const engine = createMockTTSEngine();
|
|
62
|
+
|
|
63
|
+
// First run — generates
|
|
64
|
+
await generateClips({ manifestPath, demoName, engine, projectRoot: tmpDir });
|
|
65
|
+
expect(engine.calls).toHaveLength(1);
|
|
66
|
+
|
|
67
|
+
// Reset calls
|
|
68
|
+
engine.calls.length = 0;
|
|
69
|
+
|
|
70
|
+
// Second run — should use cache
|
|
71
|
+
const results = await generateClips({ manifestPath, demoName, engine, projectRoot: tmpDir });
|
|
72
|
+
expect(engine.calls).toHaveLength(0);
|
|
73
|
+
expect(results).toHaveLength(1);
|
|
74
|
+
expect(fs.existsSync(results[0].clipPath)).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('regenerates only changed entries', async () => {
|
|
78
|
+
const entries = [
|
|
79
|
+
{ scene: 'intro', text: 'Hello world' },
|
|
80
|
+
{ scene: 'outro', text: 'Goodbye world' },
|
|
81
|
+
];
|
|
82
|
+
writeManifest(entries);
|
|
83
|
+
const engine = createMockTTSEngine();
|
|
84
|
+
|
|
85
|
+
// First run
|
|
86
|
+
await generateClips({ manifestPath, demoName, engine, projectRoot: tmpDir });
|
|
87
|
+
expect(engine.calls).toHaveLength(2);
|
|
88
|
+
|
|
89
|
+
// Change one entry's text
|
|
90
|
+
engine.calls.length = 0;
|
|
91
|
+
const updatedEntries = [
|
|
92
|
+
{ scene: 'intro', text: 'Hello world' }, // unchanged
|
|
93
|
+
{ scene: 'outro', text: 'See you later' }, // changed
|
|
94
|
+
];
|
|
95
|
+
writeManifest(updatedEntries);
|
|
96
|
+
|
|
97
|
+
const results = await generateClips({ manifestPath, demoName, engine, projectRoot: tmpDir });
|
|
98
|
+
expect(engine.calls).toHaveLength(1);
|
|
99
|
+
expect(engine.calls[0].text).toBe('See you later');
|
|
100
|
+
expect(results).toHaveLength(2);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('throws on missing manifest file', async () => {
|
|
104
|
+
const engine = createMockTTSEngine();
|
|
105
|
+
await expect(
|
|
106
|
+
generateClips({
|
|
107
|
+
manifestPath: path.join(tmpDir, 'nonexistent.json'),
|
|
108
|
+
demoName,
|
|
109
|
+
engine,
|
|
110
|
+
projectRoot: tmpDir,
|
|
111
|
+
}),
|
|
112
|
+
).rejects.toThrow('Manifest file not found');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('throws on invalid JSON', async () => {
|
|
116
|
+
fs.writeFileSync(manifestPath, '{ not valid json }}}');
|
|
117
|
+
const engine = createMockTTSEngine();
|
|
118
|
+
await expect(
|
|
119
|
+
generateClips({ manifestPath, demoName, engine, projectRoot: tmpDir }),
|
|
120
|
+
).rejects.toThrow('Failed to parse manifest');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('throws on missing required field scene', async () => {
|
|
124
|
+
writeManifest([{ text: 'Hello world' }]);
|
|
125
|
+
const engine = createMockTTSEngine();
|
|
126
|
+
await expect(
|
|
127
|
+
generateClips({ manifestPath, demoName, engine, projectRoot: tmpDir }),
|
|
128
|
+
).rejects.toThrow('missing required field');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('throws on missing required field text', async () => {
|
|
132
|
+
writeManifest([{ scene: 'intro' }]);
|
|
133
|
+
const engine = createMockTTSEngine();
|
|
134
|
+
await expect(
|
|
135
|
+
generateClips({ manifestPath, demoName, engine, projectRoot: tmpDir }),
|
|
136
|
+
).rejects.toThrow('missing required field');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('applies defaults for voice and speed when entry does not specify them', async () => {
|
|
140
|
+
const entries = [
|
|
141
|
+
{ scene: 'intro', text: 'Hello world' },
|
|
142
|
+
];
|
|
143
|
+
writeManifest(entries);
|
|
144
|
+
const engine = createMockTTSEngine();
|
|
145
|
+
|
|
146
|
+
await generateClips({
|
|
147
|
+
manifestPath,
|
|
148
|
+
demoName,
|
|
149
|
+
engine,
|
|
150
|
+
projectRoot: tmpDir,
|
|
151
|
+
defaults: { voice: 'alloy', speed: 1.2 },
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
expect(engine.calls).toHaveLength(1);
|
|
155
|
+
expect(engine.calls[0].options.voice).toBe('alloy');
|
|
156
|
+
expect(engine.calls[0].options.speed).toBe(1.2);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('entry-level voice/speed overrides defaults', async () => {
|
|
160
|
+
const entries = [
|
|
161
|
+
{ scene: 'intro', text: 'Hello', voice: 'nova', speed: 0.8 },
|
|
162
|
+
];
|
|
163
|
+
writeManifest(entries);
|
|
164
|
+
const engine = createMockTTSEngine();
|
|
165
|
+
|
|
166
|
+
await generateClips({
|
|
167
|
+
manifestPath,
|
|
168
|
+
demoName,
|
|
169
|
+
engine,
|
|
170
|
+
projectRoot: tmpDir,
|
|
171
|
+
defaults: { voice: 'alloy', speed: 1.2 },
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
expect(engine.calls[0].options.voice).toBe('nova');
|
|
175
|
+
expect(engine.calls[0].options.speed).toBe(0.8);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { KokoroEngine } from '../../src/tts/kokoro.js';
|
|
3
|
+
import { parseWavHeader } from '../../src/tts/engine.js';
|
|
4
|
+
|
|
5
|
+
const describeIntegration = process.env.CI ? describe.skip : describe;
|
|
6
|
+
|
|
7
|
+
describeIntegration('KokoroEngine (integration)', () => {
|
|
8
|
+
const engine = new KokoroEngine();
|
|
9
|
+
|
|
10
|
+
it('generates valid WAV from text', async () => {
|
|
11
|
+
const wav = await engine.generate('Hello world', { voice: 'af_heart' });
|
|
12
|
+
expect(wav).toBeInstanceOf(Buffer);
|
|
13
|
+
expect(wav.length).toBeGreaterThan(44);
|
|
14
|
+
|
|
15
|
+
const header = parseWavHeader(wav);
|
|
16
|
+
expect(header.sampleRate).toBeGreaterThan(0);
|
|
17
|
+
expect(header.numChannels).toBe(1);
|
|
18
|
+
expect(header.durationMs).toBeGreaterThan(0);
|
|
19
|
+
}, 120_000);
|
|
20
|
+
|
|
21
|
+
it('throws on empty text', async () => {
|
|
22
|
+
await expect(engine.generate('', {})).rejects.toThrow('TTS text must not be empty');
|
|
23
|
+
await expect(engine.generate(' ', {})).rejects.toThrow('TTS text must not be empty');
|
|
24
|
+
});
|
|
25
|
+
}, 180_000);
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"rootDir": "src",
|
|
8
|
+
"declaration": true,
|
|
9
|
+
"declarationMap": true,
|
|
10
|
+
"sourceMap": true,
|
|
11
|
+
"strict": true,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"forceConsistentCasingInFileNames": true,
|
|
15
|
+
"resolveJsonModule": true
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*.ts"],
|
|
18
|
+
"exclude": ["node_modules", "dist", "tests"]
|
|
19
|
+
}
|