@argo-video/cli 0.1.0 → 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/LICENSE +21 -0
- package/README.md +2 -2
- package/dist/asset-server.d.ts +7 -0
- package/dist/asset-server.d.ts.map +1 -0
- package/dist/asset-server.js +69 -0
- package/dist/asset-server.js.map +1 -0
- package/dist/captions.d.ts +17 -0
- package/dist/captions.d.ts.map +1 -0
- package/dist/captions.js +23 -0
- package/dist/captions.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +87 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +49 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +76 -0
- package/dist/config.js.map +1 -0
- package/dist/export.d.ts +19 -0
- package/dist/export.d.ts.map +1 -0
- package/dist/export.js +66 -0
- package/dist/export.js.map +1 -0
- package/dist/fixtures.d.ts +13 -0
- package/dist/fixtures.d.ts.map +1 -0
- package/dist/fixtures.js +49 -0
- package/dist/fixtures.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/init.d.ts +2 -0
- package/dist/init.d.ts.map +1 -0
- package/{src/init.ts → dist/init.js} +39 -54
- package/dist/init.js.map +1 -0
- package/dist/narration.d.ts +32 -0
- package/dist/narration.d.ts.map +1 -0
- package/dist/narration.js +86 -0
- package/dist/narration.js.map +1 -0
- package/dist/overlays/index.d.ts +13 -0
- package/dist/overlays/index.d.ts.map +1 -0
- package/dist/overlays/index.js +45 -0
- package/dist/overlays/index.js.map +1 -0
- package/dist/overlays/manifest.d.ts +5 -0
- package/dist/overlays/manifest.d.ts.map +1 -0
- package/dist/overlays/manifest.js +52 -0
- package/dist/overlays/manifest.js.map +1 -0
- package/dist/overlays/motion.d.ts +4 -0
- package/dist/overlays/motion.d.ts.map +1 -0
- package/dist/overlays/motion.js +25 -0
- package/dist/overlays/motion.js.map +1 -0
- package/dist/overlays/templates.d.ts +8 -0
- package/dist/overlays/templates.d.ts.map +1 -0
- package/dist/overlays/templates.js +102 -0
- package/dist/overlays/templates.js.map +1 -0
- package/dist/overlays/types.d.ts +46 -0
- package/dist/overlays/types.d.ts.map +1 -0
- package/dist/overlays/types.js +25 -0
- package/dist/overlays/types.js.map +1 -0
- package/dist/overlays/zones.d.ts +23 -0
- package/dist/overlays/zones.d.ts.map +1 -0
- package/dist/overlays/zones.js +117 -0
- package/dist/overlays/zones.js.map +1 -0
- package/dist/pipeline.d.ts +3 -0
- package/dist/pipeline.d.ts.map +1 -0
- package/dist/pipeline.js +109 -0
- package/dist/pipeline.js.map +1 -0
- package/dist/record.d.ts +15 -0
- package/dist/record.d.ts.map +1 -0
- package/dist/record.js +110 -0
- package/dist/record.js.map +1 -0
- package/dist/tts/align.d.ts +26 -0
- package/dist/tts/align.d.ts.map +1 -0
- package/dist/tts/align.js +53 -0
- package/dist/tts/align.js.map +1 -0
- package/dist/tts/cache.d.ts +31 -0
- package/dist/tts/cache.d.ts.map +1 -0
- package/dist/tts/cache.js +51 -0
- package/dist/tts/cache.js.map +1 -0
- package/dist/tts/engine.d.ts +41 -0
- package/dist/tts/engine.d.ts.map +1 -0
- package/dist/tts/engine.js +108 -0
- package/dist/tts/engine.js.map +1 -0
- package/dist/tts/generate.d.ts +21 -0
- package/dist/tts/generate.d.ts.map +1 -0
- package/dist/tts/generate.js +61 -0
- package/dist/tts/generate.js.map +1 -0
- package/dist/tts/kokoro.d.ts +30 -0
- package/dist/tts/kokoro.d.ts.map +1 -0
- package/dist/tts/kokoro.js +66 -0
- package/dist/tts/kokoro.js.map +1 -0
- package/package.json +13 -1
- package/.claude/settings.local.json +0 -34
- package/DESIGN.md +0 -261
- package/docs/enhancement-proposal.md +0 -262
- package/docs/superpowers/plans/2026-03-12-argo.md +0 -208
- package/docs/superpowers/plans/2026-03-12-editorial-overlay-system.md +0 -1560
- package/docs/superpowers/plans/2026-03-13-npm-rename-skill-showcase.md +0 -499
- package/docs/superpowers/specs/2026-03-13-npm-rename-skill-showcase-design.md +0 -109
- package/skills/argo-demo-creator.md +0 -355
- package/src/asset-server.ts +0 -81
- package/src/captions.ts +0 -36
- package/src/cli.ts +0 -97
- package/src/config.ts +0 -125
- package/src/export.ts +0 -93
- package/src/fixtures.ts +0 -50
- package/src/index.ts +0 -41
- package/src/narration.ts +0 -31
- package/src/overlays/index.ts +0 -54
- package/src/overlays/manifest.ts +0 -68
- package/src/overlays/motion.ts +0 -27
- package/src/overlays/templates.ts +0 -121
- package/src/overlays/types.ts +0 -73
- package/src/overlays/zones.ts +0 -82
- package/src/pipeline.ts +0 -120
- package/src/record.ts +0 -123
- package/src/tts/align.ts +0 -75
- package/src/tts/cache.ts +0 -65
- package/src/tts/engine.ts +0 -147
- package/src/tts/generate.ts +0 -83
- package/src/tts/kokoro.ts +0 -51
- package/tests/asset-server.test.ts +0 -67
- package/tests/captions.test.ts +0 -76
- package/tests/cli.test.ts +0 -131
- package/tests/config.test.ts +0 -150
- package/tests/e2e/fake-server.ts +0 -45
- package/tests/e2e/record.e2e.test.ts +0 -131
- package/tests/export.test.ts +0 -155
- package/tests/fixtures.test.ts +0 -74
- package/tests/init.test.ts +0 -77
- package/tests/narration.test.ts +0 -120
- package/tests/overlays/index.test.ts +0 -73
- package/tests/overlays/manifest.test.ts +0 -120
- package/tests/overlays/motion.test.ts +0 -34
- package/tests/overlays/templates.test.ts +0 -69
- package/tests/overlays/types.test.ts +0 -36
- package/tests/overlays/zones.test.ts +0 -49
- package/tests/pipeline.test.ts +0 -177
- package/tests/record.test.ts +0 -87
- package/tests/tts/align.test.ts +0 -118
- package/tests/tts/cache.test.ts +0 -110
- package/tests/tts/engine.test.ts +0 -204
- package/tests/tts/generate.test.ts +0 -177
- package/tests/tts/kokoro.test.ts +0 -25
- package/tsconfig.json +0 -19
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { renderTemplate } from '../../src/overlays/templates.js';
|
|
3
|
-
|
|
4
|
-
describe('renderTemplate', () => {
|
|
5
|
-
describe('lower-third', () => {
|
|
6
|
-
it('renders text in a styled span', () => {
|
|
7
|
-
const result = renderTemplate({ type: 'lower-third', text: 'Hello world' });
|
|
8
|
-
expect(result.contentHtml).toContain('Hello world');
|
|
9
|
-
expect(result.styles.background).toBeDefined();
|
|
10
|
-
expect(result.styles.borderRadius).toBeDefined();
|
|
11
|
-
});
|
|
12
|
-
it('includes maxWidth for readability', () => {
|
|
13
|
-
const result = renderTemplate({ type: 'lower-third', text: 'Test' });
|
|
14
|
-
expect(result.styles.maxWidth).toBeDefined();
|
|
15
|
-
});
|
|
16
|
-
it('escapes HTML in text', () => {
|
|
17
|
-
const result = renderTemplate({ type: 'lower-third', text: '<script>alert("xss")</script>' });
|
|
18
|
-
expect(result.contentHtml).not.toContain('<script>');
|
|
19
|
-
expect(result.contentHtml).toContain('<script>');
|
|
20
|
-
});
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
describe('headline-card', () => {
|
|
24
|
-
it('renders title', () => {
|
|
25
|
-
const result = renderTemplate({ type: 'headline-card', title: 'Big Title' });
|
|
26
|
-
expect(result.contentHtml).toContain('Big Title');
|
|
27
|
-
});
|
|
28
|
-
it('renders kicker when provided', () => {
|
|
29
|
-
const result = renderTemplate({ type: 'headline-card', title: 'Title', kicker: 'LABEL' });
|
|
30
|
-
expect(result.contentHtml).toContain('LABEL');
|
|
31
|
-
});
|
|
32
|
-
it('renders body when provided', () => {
|
|
33
|
-
const result = renderTemplate({ type: 'headline-card', title: 'Title', body: 'Details here' });
|
|
34
|
-
expect(result.contentHtml).toContain('Details here');
|
|
35
|
-
});
|
|
36
|
-
it('omits kicker element when not provided', () => {
|
|
37
|
-
const result = renderTemplate({ type: 'headline-card', title: 'Title' });
|
|
38
|
-
expect(result.contentHtml).not.toContain('uppercase');
|
|
39
|
-
});
|
|
40
|
-
it('has backdrop blur style', () => {
|
|
41
|
-
const result = renderTemplate({ type: 'headline-card', title: 'T' });
|
|
42
|
-
expect(result.styles.backdropFilter).toContain('blur');
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
describe('callout', () => {
|
|
47
|
-
it('renders text in a compact bubble', () => {
|
|
48
|
-
const result = renderTemplate({ type: 'callout', text: 'Note this' });
|
|
49
|
-
expect(result.contentHtml).toContain('Note this');
|
|
50
|
-
expect(result.styles.borderRadius).toBeDefined();
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
describe('image-card', () => {
|
|
55
|
-
it('renders img tag with src', () => {
|
|
56
|
-
const result = renderTemplate({ type: 'image-card', src: 'http://localhost:9999/diagram.png' });
|
|
57
|
-
expect(result.contentHtml).toContain('<img');
|
|
58
|
-
expect(result.contentHtml).toContain('http://localhost:9999/diagram.png');
|
|
59
|
-
});
|
|
60
|
-
it('renders title when provided', () => {
|
|
61
|
-
const result = renderTemplate({ type: 'image-card', src: 'http://x/img.png', title: 'Architecture' });
|
|
62
|
-
expect(result.contentHtml).toContain('Architecture');
|
|
63
|
-
});
|
|
64
|
-
it('renders body when provided', () => {
|
|
65
|
-
const result = renderTemplate({ type: 'image-card', src: 'http://x/img.png', body: 'Description' });
|
|
66
|
-
expect(result.contentHtml).toContain('Description');
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
});
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { isValidZone, isValidTemplateType, isValidMotion } from '../../src/overlays/types.js';
|
|
3
|
-
|
|
4
|
-
describe('isValidZone', () => {
|
|
5
|
-
it('accepts all defined zones', () => {
|
|
6
|
-
for (const z of ['bottom-center', 'top-left', 'top-right', 'bottom-left', 'bottom-right', 'center']) {
|
|
7
|
-
expect(isValidZone(z)).toBe(true);
|
|
8
|
-
}
|
|
9
|
-
});
|
|
10
|
-
it('rejects unknown zones', () => {
|
|
11
|
-
expect(isValidZone('middle')).toBe(false);
|
|
12
|
-
expect(isValidZone('')).toBe(false);
|
|
13
|
-
});
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
describe('isValidTemplateType', () => {
|
|
17
|
-
it('accepts all defined types', () => {
|
|
18
|
-
for (const t of ['lower-third', 'headline-card', 'callout', 'image-card']) {
|
|
19
|
-
expect(isValidTemplateType(t)).toBe(true);
|
|
20
|
-
}
|
|
21
|
-
});
|
|
22
|
-
it('rejects unknown types', () => {
|
|
23
|
-
expect(isValidTemplateType('banner')).toBe(false);
|
|
24
|
-
});
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
describe('isValidMotion', () => {
|
|
28
|
-
it('accepts defined motions and none', () => {
|
|
29
|
-
expect(isValidMotion('fade-in')).toBe(true);
|
|
30
|
-
expect(isValidMotion('slide-in')).toBe(true);
|
|
31
|
-
expect(isValidMotion('none')).toBe(true);
|
|
32
|
-
});
|
|
33
|
-
it('rejects unknown motions', () => {
|
|
34
|
-
expect(isValidMotion('bounce')).toBe(false);
|
|
35
|
-
});
|
|
36
|
-
});
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import { injectIntoZone, removeZone, ZONE_ID_PREFIX } from '../../src/overlays/zones.js';
|
|
3
|
-
import type { Page } from '@playwright/test';
|
|
4
|
-
|
|
5
|
-
function createMockPage() {
|
|
6
|
-
return {
|
|
7
|
-
evaluate: vi.fn(),
|
|
8
|
-
waitForTimeout: vi.fn(),
|
|
9
|
-
} as unknown as Page;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
describe('ZONE_ID_PREFIX', () => {
|
|
13
|
-
it('is argo-overlay-', () => {
|
|
14
|
-
expect(ZONE_ID_PREFIX).toBe('argo-overlay-');
|
|
15
|
-
});
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
describe('injectIntoZone', () => {
|
|
19
|
-
let page: Page;
|
|
20
|
-
|
|
21
|
-
beforeEach(() => {
|
|
22
|
-
page = createMockPage();
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it('calls page.evaluate with zone ID', async () => {
|
|
26
|
-
await injectIntoZone(page, 'top-left', '<div>Hello</div>', { color: 'red' });
|
|
27
|
-
expect(page.evaluate).toHaveBeenCalledTimes(1);
|
|
28
|
-
const [fn, args] = (page.evaluate as any).mock.calls[0];
|
|
29
|
-
expect(typeof fn).toBe('function');
|
|
30
|
-
expect(args[0]).toBe('argo-overlay-top-left');
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('uses bottom-center zone ID correctly', async () => {
|
|
34
|
-
await injectIntoZone(page, 'bottom-center', '<span>text</span>', {});
|
|
35
|
-
const [, args] = (page.evaluate as any).mock.calls[0];
|
|
36
|
-
expect(args[0]).toBe('argo-overlay-bottom-center');
|
|
37
|
-
});
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
describe('removeZone', () => {
|
|
41
|
-
it('calls page.evaluate to remove element by zone ID', async () => {
|
|
42
|
-
const page = createMockPage();
|
|
43
|
-
await removeZone(page, 'top-left');
|
|
44
|
-
expect(page.evaluate).toHaveBeenCalledTimes(1);
|
|
45
|
-
const [fn, arg] = (page.evaluate as any).mock.calls[0];
|
|
46
|
-
expect(typeof fn).toBe('function');
|
|
47
|
-
expect(arg).toBe('argo-overlay-top-left');
|
|
48
|
-
});
|
|
49
|
-
});
|
package/tests/pipeline.test.ts
DELETED
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import { mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
|
|
5
|
-
vi.mock('../src/tts/generate.js', () => ({
|
|
6
|
-
generateClips: vi.fn(),
|
|
7
|
-
}));
|
|
8
|
-
|
|
9
|
-
vi.mock('../src/record.js', () => ({
|
|
10
|
-
record: vi.fn(),
|
|
11
|
-
}));
|
|
12
|
-
|
|
13
|
-
vi.mock('../src/export.js', () => ({
|
|
14
|
-
checkFfmpeg: vi.fn(),
|
|
15
|
-
exportVideo: vi.fn(),
|
|
16
|
-
}));
|
|
17
|
-
|
|
18
|
-
// Mock execFileSync (used by getVideoDurationMs for ffprobe)
|
|
19
|
-
vi.mock('node:child_process', () => ({
|
|
20
|
-
execFileSync: vi.fn().mockReturnValue('16.240\n'),
|
|
21
|
-
}));
|
|
22
|
-
|
|
23
|
-
import { execFileSync } from 'node:child_process';
|
|
24
|
-
import { generateClips } from '../src/tts/generate.js';
|
|
25
|
-
import { record } from '../src/record.js';
|
|
26
|
-
import { checkFfmpeg, exportVideo } from '../src/export.js';
|
|
27
|
-
import { runPipeline } from '../src/pipeline.js';
|
|
28
|
-
import { createWavBuffer } from '../src/tts/engine.js';
|
|
29
|
-
import type { ArgoConfig } from '../src/config.js';
|
|
30
|
-
|
|
31
|
-
const mockedExecFileSync = vi.mocked(execFileSync);
|
|
32
|
-
const mockedGenerateClips = vi.mocked(generateClips);
|
|
33
|
-
const mockedRecord = vi.mocked(record);
|
|
34
|
-
const mockedCheckFfmpeg = vi.mocked(checkFfmpeg);
|
|
35
|
-
const mockedExportVideo = vi.mocked(exportVideo);
|
|
36
|
-
|
|
37
|
-
const DEMO_NAME = 'test-demo';
|
|
38
|
-
const ARGO_DIR = join('.argo', DEMO_NAME);
|
|
39
|
-
const TIMING_PATH = join(ARGO_DIR, '.timing.json');
|
|
40
|
-
const VIDEO_PATH = join(ARGO_DIR, 'video.webm');
|
|
41
|
-
|
|
42
|
-
const mockEngine = { generate: vi.fn().mockResolvedValue(Buffer.from('fake')) };
|
|
43
|
-
|
|
44
|
-
const defaultConfig: Pick<ArgoConfig, 'baseURL' | 'demosDir' | 'outputDir' | 'tts' | 'video' | 'export'> = {
|
|
45
|
-
baseURL: 'http://localhost:3000',
|
|
46
|
-
demosDir: 'demos',
|
|
47
|
-
outputDir: 'videos',
|
|
48
|
-
tts: { defaultVoice: 'af_heart', defaultSpeed: 1.0, engine: mockEngine },
|
|
49
|
-
video: { width: 1920, height: 1080, fps: 30 },
|
|
50
|
-
export: { preset: 'slow', crf: 16 },
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
function setupFixtures() {
|
|
54
|
-
mkdirSync(join(ARGO_DIR, 'clips'), { recursive: true });
|
|
55
|
-
// Write a minimal timing file
|
|
56
|
-
writeFileSync(TIMING_PATH, JSON.stringify({ intro: 1000, done: 5000 }));
|
|
57
|
-
// Write a minimal video placeholder (ffprobe is mocked)
|
|
58
|
-
writeFileSync(VIDEO_PATH, Buffer.from('fake-video'));
|
|
59
|
-
// Write clip WAV files
|
|
60
|
-
const silence = createWavBuffer(new Float32Array(24000), 24000); // 1s silence
|
|
61
|
-
writeFileSync(join(ARGO_DIR, 'clips', 'intro.wav'), silence);
|
|
62
|
-
writeFileSync(join(ARGO_DIR, 'clips', 'done.wav'), silence);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
beforeEach(() => {
|
|
66
|
-
vi.resetAllMocks();
|
|
67
|
-
rmSync('.argo', { recursive: true, force: true });
|
|
68
|
-
setupFixtures();
|
|
69
|
-
|
|
70
|
-
mockedExecFileSync.mockReturnValue('16.240\n');
|
|
71
|
-
mockedCheckFfmpeg.mockReturnValue(true);
|
|
72
|
-
mockedGenerateClips.mockResolvedValue([
|
|
73
|
-
{ scene: 'intro', clipPath: join(ARGO_DIR, 'clips', 'intro.wav') },
|
|
74
|
-
{ scene: 'done', clipPath: join(ARGO_DIR, 'clips', 'done.wav') },
|
|
75
|
-
]);
|
|
76
|
-
mockedRecord.mockResolvedValue({ videoPath: VIDEO_PATH, timingPath: TIMING_PATH });
|
|
77
|
-
mockedExportVideo.mockResolvedValue(`videos/${DEMO_NAME}.mp4`);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
describe('runPipeline', () => {
|
|
81
|
-
it('calls checkFfmpeg before other steps', async () => {
|
|
82
|
-
const callOrder: string[] = [];
|
|
83
|
-
mockedCheckFfmpeg.mockImplementation(() => { callOrder.push('checkFfmpeg'); return true; });
|
|
84
|
-
mockedGenerateClips.mockImplementation(async () => {
|
|
85
|
-
callOrder.push('generateClips');
|
|
86
|
-
return [
|
|
87
|
-
{ scene: 'intro', clipPath: join(ARGO_DIR, 'clips', 'intro.wav') },
|
|
88
|
-
{ scene: 'done', clipPath: join(ARGO_DIR, 'clips', 'done.wav') },
|
|
89
|
-
];
|
|
90
|
-
});
|
|
91
|
-
mockedRecord.mockImplementation(async () => { callOrder.push('record'); return { videoPath: VIDEO_PATH, timingPath: TIMING_PATH }; });
|
|
92
|
-
mockedExportVideo.mockImplementation(async () => { callOrder.push('exportVideo'); return 'out.mp4'; });
|
|
93
|
-
|
|
94
|
-
await runPipeline(DEMO_NAME, defaultConfig);
|
|
95
|
-
expect(callOrder[0]).toBe('checkFfmpeg');
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('calls steps in order: generateClips → record → exportVideo', async () => {
|
|
99
|
-
const callOrder: string[] = [];
|
|
100
|
-
mockedCheckFfmpeg.mockImplementation(() => { callOrder.push('checkFfmpeg'); return true; });
|
|
101
|
-
mockedGenerateClips.mockImplementation(async () => {
|
|
102
|
-
callOrder.push('generateClips');
|
|
103
|
-
return [
|
|
104
|
-
{ scene: 'intro', clipPath: join(ARGO_DIR, 'clips', 'intro.wav') },
|
|
105
|
-
{ scene: 'done', clipPath: join(ARGO_DIR, 'clips', 'done.wav') },
|
|
106
|
-
];
|
|
107
|
-
});
|
|
108
|
-
mockedRecord.mockImplementation(async () => { callOrder.push('record'); return { videoPath: VIDEO_PATH, timingPath: TIMING_PATH }; });
|
|
109
|
-
mockedExportVideo.mockImplementation(async () => { callOrder.push('exportVideo'); return 'out.mp4'; });
|
|
110
|
-
|
|
111
|
-
await runPipeline(DEMO_NAME, defaultConfig);
|
|
112
|
-
expect(callOrder).toEqual(['checkFfmpeg', 'generateClips', 'record', 'exportVideo']);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it('returns the output path from exportVideo', async () => {
|
|
116
|
-
const result = await runPipeline(DEMO_NAME, defaultConfig);
|
|
117
|
-
expect(result).toBe(`videos/${DEMO_NAME}.mp4`);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it('passes correct options to generateClips', async () => {
|
|
121
|
-
await runPipeline(DEMO_NAME, defaultConfig);
|
|
122
|
-
expect(mockedGenerateClips).toHaveBeenCalledWith({
|
|
123
|
-
manifestPath: `demos/${DEMO_NAME}.voiceover.json`,
|
|
124
|
-
demoName: DEMO_NAME,
|
|
125
|
-
engine: mockEngine,
|
|
126
|
-
projectRoot: '.',
|
|
127
|
-
defaults: { voice: 'af_heart', speed: 1.0 },
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it('passes correct options to record', async () => {
|
|
132
|
-
await runPipeline(DEMO_NAME, defaultConfig);
|
|
133
|
-
expect(mockedRecord).toHaveBeenCalledWith(DEMO_NAME, {
|
|
134
|
-
demosDir: 'demos',
|
|
135
|
-
baseURL: 'http://localhost:3000',
|
|
136
|
-
video: { width: 1920, height: 1080 },
|
|
137
|
-
});
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it('passes correct options to exportVideo', async () => {
|
|
141
|
-
await runPipeline(DEMO_NAME, defaultConfig);
|
|
142
|
-
expect(mockedExportVideo).toHaveBeenCalledWith({
|
|
143
|
-
demoName: DEMO_NAME,
|
|
144
|
-
argoDir: '.argo',
|
|
145
|
-
outputDir: 'videos',
|
|
146
|
-
preset: 'slow',
|
|
147
|
-
crf: 16,
|
|
148
|
-
fps: 30,
|
|
149
|
-
});
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it('writes narration-aligned.wav to .argo/<demo>/', async () => {
|
|
153
|
-
const { existsSync } = await import('node:fs');
|
|
154
|
-
await runPipeline(DEMO_NAME, defaultConfig);
|
|
155
|
-
expect(existsSync(join(ARGO_DIR, 'narration-aligned.wav'))).toBe(true);
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it('propagates error from checkFfmpeg', async () => {
|
|
159
|
-
mockedCheckFfmpeg.mockImplementation(() => { throw new Error('ffmpeg not found'); });
|
|
160
|
-
await expect(runPipeline(DEMO_NAME, defaultConfig)).rejects.toThrow('ffmpeg not found');
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it('propagates error from generateClips', async () => {
|
|
164
|
-
mockedGenerateClips.mockRejectedValue(new Error('TTS failed'));
|
|
165
|
-
await expect(runPipeline(DEMO_NAME, defaultConfig)).rejects.toThrow('TTS failed');
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
it('propagates error from record', async () => {
|
|
169
|
-
mockedRecord.mockRejectedValue(new Error('Recording failed'));
|
|
170
|
-
await expect(runPipeline(DEMO_NAME, defaultConfig)).rejects.toThrow('Recording failed');
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it('propagates error from exportVideo', async () => {
|
|
174
|
-
mockedExportVideo.mockRejectedValue(new Error('Export failed'));
|
|
175
|
-
await expect(runPipeline(DEMO_NAME, defaultConfig)).rejects.toThrow('Export failed');
|
|
176
|
-
});
|
|
177
|
-
});
|
package/tests/record.test.ts
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { mkdtemp, rm } from 'node:fs/promises';
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
-
import { join, resolve } from 'node:path';
|
|
5
|
-
import { tmpdir } from 'node:os';
|
|
6
|
-
|
|
7
|
-
const { execFileMock } = vi.hoisted(() => ({
|
|
8
|
-
execFileMock: vi.fn(),
|
|
9
|
-
}));
|
|
10
|
-
|
|
11
|
-
vi.mock('node:child_process', () => ({
|
|
12
|
-
execFile: execFileMock,
|
|
13
|
-
}));
|
|
14
|
-
|
|
15
|
-
import { record } from '../src/record.js';
|
|
16
|
-
|
|
17
|
-
const originalCwd = process.cwd();
|
|
18
|
-
|
|
19
|
-
describe('record', () => {
|
|
20
|
-
let tempDir: string;
|
|
21
|
-
|
|
22
|
-
beforeEach(async () => {
|
|
23
|
-
execFileMock.mockReset();
|
|
24
|
-
tempDir = await mkdtemp(join(tmpdir(), 'argo-record-'));
|
|
25
|
-
process.chdir(tempDir);
|
|
26
|
-
mkdirSync('custom-demos', { recursive: true });
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
afterEach(async () => {
|
|
30
|
-
process.chdir(originalCwd);
|
|
31
|
-
await rm(tempDir, { recursive: true, force: true });
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('generates a Playwright config from record options and copies artifacts', async () => {
|
|
35
|
-
execFileMock.mockImplementation((_cmd, args, options, callback) => {
|
|
36
|
-
const testResultsDir = resolve(tempDir, 'test-results');
|
|
37
|
-
const argoOutputDir = options.env.ARGO_OUTPUT_DIR as string;
|
|
38
|
-
|
|
39
|
-
mkdirSync(join(testResultsDir, 'demo-run'), { recursive: true });
|
|
40
|
-
writeFileSync(join(testResultsDir, 'demo-run', 'video.webm'), 'video');
|
|
41
|
-
mkdirSync(resolve(tempDir, argoOutputDir), { recursive: true });
|
|
42
|
-
writeFileSync(resolve(tempDir, argoOutputDir, '.timing.json'), '{}');
|
|
43
|
-
|
|
44
|
-
callback(null, '', '');
|
|
45
|
-
return {} as never;
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
const result = await record('demo', {
|
|
49
|
-
demosDir: 'custom-demos',
|
|
50
|
-
baseURL: 'http://localhost:4321',
|
|
51
|
-
video: { width: 1280, height: 720 },
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
const configPath = join(tempDir, '.argo', 'demo', 'playwright.record.config.mjs');
|
|
55
|
-
const config = readFileSync(configPath, 'utf-8');
|
|
56
|
-
|
|
57
|
-
expect(config).toContain(`testDir: ${JSON.stringify(resolve('custom-demos'))}`);
|
|
58
|
-
expect(config).toContain(`baseURL: ${JSON.stringify('http://localhost:4321')}`);
|
|
59
|
-
expect(config).toContain('viewport: { width: 1280, height: 720 }');
|
|
60
|
-
expect(config).toContain('size: { width: 1280, height: 720 }');
|
|
61
|
-
expect(existsSync(join(tempDir, '.argo', 'demo', 'video.webm'))).toBe(true);
|
|
62
|
-
expect(result).toEqual({
|
|
63
|
-
videoPath: join('.argo', 'demo', 'video.webm'),
|
|
64
|
-
timingPath: join('.argo', 'demo', '.timing.json'),
|
|
65
|
-
});
|
|
66
|
-
expect(execFileMock).toHaveBeenCalledWith(
|
|
67
|
-
'npx',
|
|
68
|
-
[
|
|
69
|
-
'playwright',
|
|
70
|
-
'test',
|
|
71
|
-
'--config',
|
|
72
|
-
join('.argo', 'demo', 'playwright.record.config.mjs'),
|
|
73
|
-
'--grep',
|
|
74
|
-
'demo',
|
|
75
|
-
'--project',
|
|
76
|
-
'demos',
|
|
77
|
-
],
|
|
78
|
-
expect.objectContaining({
|
|
79
|
-
env: expect.objectContaining({
|
|
80
|
-
ARGO_OUTPUT_DIR: join('.argo', 'demo'),
|
|
81
|
-
BASE_URL: 'http://localhost:4321',
|
|
82
|
-
}),
|
|
83
|
-
}),
|
|
84
|
-
expect.any(Function),
|
|
85
|
-
);
|
|
86
|
-
});
|
|
87
|
-
});
|
package/tests/tts/align.test.ts
DELETED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
alignClips,
|
|
4
|
-
type SceneTiming,
|
|
5
|
-
type ClipInfo,
|
|
6
|
-
type Placement,
|
|
7
|
-
type AlignResult,
|
|
8
|
-
} from '../../src/tts/align.js';
|
|
9
|
-
|
|
10
|
-
function makeClip(scene: string, durationMs: number, fillValue = 0): ClipInfo {
|
|
11
|
-
const sampleRate = 24_000;
|
|
12
|
-
const numSamples = Math.round((durationMs / 1000) * sampleRate);
|
|
13
|
-
const samples = new Float32Array(numSamples);
|
|
14
|
-
if (fillValue !== 0) samples.fill(fillValue);
|
|
15
|
-
return { scene, durationMs, samples };
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
describe('alignClips', () => {
|
|
19
|
-
it('places clips at their scene timestamps (no overlap)', () => {
|
|
20
|
-
const timing: SceneTiming = { intro: 0, middle: 5000, end: 10000 };
|
|
21
|
-
const clips: ClipInfo[] = [
|
|
22
|
-
makeClip('intro', 2000),
|
|
23
|
-
makeClip('middle', 2000),
|
|
24
|
-
makeClip('end', 2000),
|
|
25
|
-
];
|
|
26
|
-
const result = alignClips(timing, clips, 15000);
|
|
27
|
-
expect(result.placements).toEqual([
|
|
28
|
-
{ scene: 'intro', startMs: 0, endMs: 2000 },
|
|
29
|
-
{ scene: 'middle', startMs: 5000, endMs: 7000 },
|
|
30
|
-
{ scene: 'end', startMs: 10000, endMs: 12000 },
|
|
31
|
-
]);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('prevents overlap with 100ms gap', () => {
|
|
35
|
-
const timing: SceneTiming = { a: 0, b: 1000 };
|
|
36
|
-
// Clip 'a' is 2000ms, so it extends past b's timestamp of 1000ms
|
|
37
|
-
const clips: ClipInfo[] = [
|
|
38
|
-
makeClip('a', 2000),
|
|
39
|
-
makeClip('b', 500),
|
|
40
|
-
];
|
|
41
|
-
const result = alignClips(timing, clips, 5000);
|
|
42
|
-
// 'b' should be pushed to 2000 + 100 = 2100
|
|
43
|
-
expect(result.placements).toEqual([
|
|
44
|
-
{ scene: 'a', startMs: 0, endMs: 2000 },
|
|
45
|
-
{ scene: 'b', startMs: 2100, endMs: 2600 },
|
|
46
|
-
]);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('output buffer has correct total duration', () => {
|
|
50
|
-
const timing: SceneTiming = { x: 0 };
|
|
51
|
-
const clips: ClipInfo[] = [makeClip('x', 500)];
|
|
52
|
-
const sampleRate = 24_000;
|
|
53
|
-
const totalDurationMs = 10000;
|
|
54
|
-
const result = alignClips(timing, clips, totalDurationMs, sampleRate);
|
|
55
|
-
const expectedSamples = Math.round((totalDurationMs / 1000) * sampleRate);
|
|
56
|
-
expect(result.samples.length).toBe(expectedSamples);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('mixes samples at correct positions', () => {
|
|
60
|
-
const timing: SceneTiming = { a: 1000 };
|
|
61
|
-
const fillValue = 0.5;
|
|
62
|
-
const clips: ClipInfo[] = [makeClip('a', 100, fillValue)];
|
|
63
|
-
const sampleRate = 24_000;
|
|
64
|
-
const result = alignClips(timing, clips, 3000, sampleRate);
|
|
65
|
-
|
|
66
|
-
const startSample = Math.round((1000 / 1000) * sampleRate);
|
|
67
|
-
const clipSamples = Math.round((100 / 1000) * sampleRate);
|
|
68
|
-
|
|
69
|
-
// Before clip: silence
|
|
70
|
-
expect(result.samples[0]).toBe(0);
|
|
71
|
-
// At clip start
|
|
72
|
-
expect(result.samples[startSample]).toBe(fillValue);
|
|
73
|
-
// At clip end - 1
|
|
74
|
-
expect(result.samples[startSample + clipSamples - 1]).toBe(fillValue);
|
|
75
|
-
// After clip: silence
|
|
76
|
-
expect(result.samples[startSample + clipSamples]).toBe(0);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('handles empty clips array', () => {
|
|
80
|
-
const timing: SceneTiming = { a: 0 };
|
|
81
|
-
const result = alignClips(timing, [], 5000);
|
|
82
|
-
expect(result.placements).toEqual([]);
|
|
83
|
-
expect(result.samples.length).toBe(Math.round((5000 / 1000) * 24_000));
|
|
84
|
-
// All silence
|
|
85
|
-
expect(result.samples.every((s) => s === 0)).toBe(true);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('orders by timestamp, not input order', () => {
|
|
89
|
-
const timing: SceneTiming = { first: 0, second: 3000, third: 6000 };
|
|
90
|
-
// Provide clips in reverse order
|
|
91
|
-
const clips: ClipInfo[] = [
|
|
92
|
-
makeClip('third', 1000),
|
|
93
|
-
makeClip('first', 1000),
|
|
94
|
-
makeClip('second', 1000),
|
|
95
|
-
];
|
|
96
|
-
const result = alignClips(timing, clips, 10000);
|
|
97
|
-
expect(result.placements.map((p) => p.scene)).toEqual([
|
|
98
|
-
'first',
|
|
99
|
-
'second',
|
|
100
|
-
'third',
|
|
101
|
-
]);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it('skips clips whose scene is not in timing data', () => {
|
|
105
|
-
const timing: SceneTiming = { a: 0, b: 3000 };
|
|
106
|
-
const clips: ClipInfo[] = [
|
|
107
|
-
makeClip('a', 1000),
|
|
108
|
-
makeClip('unknown', 1000),
|
|
109
|
-
makeClip('b', 1000),
|
|
110
|
-
];
|
|
111
|
-
const result = alignClips(timing, clips, 6000);
|
|
112
|
-
expect(result.placements).toEqual([
|
|
113
|
-
{ scene: 'a', startMs: 0, endMs: 1000 },
|
|
114
|
-
{ scene: 'b', startMs: 3000, endMs: 4000 },
|
|
115
|
-
]);
|
|
116
|
-
expect(result.placements.some((p) => p.scene === 'unknown')).toBe(false);
|
|
117
|
-
});
|
|
118
|
-
});
|
package/tests/tts/cache.test.ts
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
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 crypto from 'node:crypto';
|
|
6
|
-
import { createWavBuffer } from '../../src/tts/engine.js';
|
|
7
|
-
import { ClipCache, type ManifestEntry } from '../../src/tts/cache.js';
|
|
8
|
-
|
|
9
|
-
function makeTmpDir(): string {
|
|
10
|
-
return fs.mkdtempSync(path.join(os.tmpdir(), 'argo-cache-test-'));
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function makeHash(entry: ManifestEntry): string {
|
|
14
|
-
const { scene, text, voice, speed } = entry;
|
|
15
|
-
return crypto.createHash('sha256').update(JSON.stringify({ scene, text, voice, speed })).digest('hex');
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
describe('ClipCache', () => {
|
|
19
|
-
let tmpDir: string;
|
|
20
|
-
let cache: ClipCache;
|
|
21
|
-
|
|
22
|
-
beforeEach(() => {
|
|
23
|
-
tmpDir = makeTmpDir();
|
|
24
|
-
cache = new ClipCache(tmpDir);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
afterEach(() => {
|
|
28
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
const entry: ManifestEntry = { scene: 'intro', text: 'Hello world' };
|
|
32
|
-
const demoName = 'demo1';
|
|
33
|
-
|
|
34
|
-
function makeWav(): Buffer {
|
|
35
|
-
return createWavBuffer(new Float32Array([0.1, 0.2, 0.3]));
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
describe('isCached', () => {
|
|
39
|
-
it('returns false when not cached', () => {
|
|
40
|
-
expect(cache.isCached(demoName, entry)).toBe(false);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('returns true after caching', () => {
|
|
44
|
-
cache.cacheClip(demoName, entry, makeWav());
|
|
45
|
-
expect(cache.isCached(demoName, entry)).toBe(true);
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
describe('getCachedClip', () => {
|
|
50
|
-
it('returns null when not cached', () => {
|
|
51
|
-
expect(cache.getCachedClip(demoName, entry)).toBeNull();
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('returns buffer when cached', () => {
|
|
55
|
-
const wav = makeWav();
|
|
56
|
-
cache.cacheClip(demoName, entry, wav);
|
|
57
|
-
const result = cache.getCachedClip(demoName, entry);
|
|
58
|
-
expect(result).not.toBeNull();
|
|
59
|
-
expect(Buffer.compare(result!, wav)).toBe(0);
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
describe('cacheClip', () => {
|
|
64
|
-
it('creates directory structure and writes file named by hash', () => {
|
|
65
|
-
const wav = makeWav();
|
|
66
|
-
cache.cacheClip(demoName, entry, wav);
|
|
67
|
-
|
|
68
|
-
const hash = makeHash(entry);
|
|
69
|
-
const expectedPath = path.join(tmpDir, '.argo', demoName, 'clips', `${hash}.wav`);
|
|
70
|
-
expect(fs.existsSync(expectedPath)).toBe(true);
|
|
71
|
-
|
|
72
|
-
const contents = fs.readFileSync(expectedPath);
|
|
73
|
-
expect(Buffer.compare(contents, wav)).toBe(0);
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
describe('getClipPath', () => {
|
|
78
|
-
it('returns correct path', () => {
|
|
79
|
-
const hash = makeHash(entry);
|
|
80
|
-
const expected = path.join(tmpDir, '.argo', demoName, 'clips', `${hash}.wav`);
|
|
81
|
-
expect(cache.getClipPath(demoName, entry)).toBe(expected);
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
describe('invalidation', () => {
|
|
86
|
-
it('different text produces different hash', () => {
|
|
87
|
-
const entry2: ManifestEntry = { scene: 'intro', text: 'Goodbye world' };
|
|
88
|
-
expect(cache.getClipPath(demoName, entry)).not.toBe(cache.getClipPath(demoName, entry2));
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('different voice produces different hash', () => {
|
|
92
|
-
const entry2: ManifestEntry = { scene: 'intro', text: 'Hello world', voice: 'alloy' };
|
|
93
|
-
expect(cache.getClipPath(demoName, entry)).not.toBe(cache.getClipPath(demoName, entry2));
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('different speed produces different hash', () => {
|
|
97
|
-
const entry2: ManifestEntry = { scene: 'intro', text: 'Hello world', speed: 1.5 };
|
|
98
|
-
expect(cache.getClipPath(demoName, entry)).not.toBe(cache.getClipPath(demoName, entry2));
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
describe('independent per demo name', () => {
|
|
103
|
-
it('caching under one demo does not affect another', () => {
|
|
104
|
-
const wav = makeWav();
|
|
105
|
-
cache.cacheClip('demoA', entry, wav);
|
|
106
|
-
expect(cache.isCached('demoA', entry)).toBe(true);
|
|
107
|
-
expect(cache.isCached('demoB', entry)).toBe(false);
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
});
|