@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
package/src/export.ts
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import { execFileSync, spawnSync } from 'node:child_process';
|
|
2
|
-
import { existsSync, mkdirSync } from 'node:fs';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
|
|
5
|
-
export interface ExportOptions {
|
|
6
|
-
demoName: string;
|
|
7
|
-
argoDir: string;
|
|
8
|
-
outputDir: string;
|
|
9
|
-
preset?: string;
|
|
10
|
-
crf?: number;
|
|
11
|
-
fps?: number;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Check whether ffmpeg is available on the system PATH.
|
|
16
|
-
* Returns true if found, throws with install instructions otherwise.
|
|
17
|
-
*/
|
|
18
|
-
export function checkFfmpeg(): boolean {
|
|
19
|
-
try {
|
|
20
|
-
execFileSync('ffmpeg', ['-version'], { stdio: 'pipe' });
|
|
21
|
-
return true;
|
|
22
|
-
} catch {
|
|
23
|
-
throw new Error(
|
|
24
|
-
'ffmpeg is not installed. Install it with:\n' +
|
|
25
|
-
' macOS: brew install ffmpeg\n' +
|
|
26
|
-
' Linux: apt install ffmpeg\n' +
|
|
27
|
-
' Windows: choco install ffmpeg',
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Export a demo to MP4 by combining the screen recording with aligned narration audio.
|
|
34
|
-
*/
|
|
35
|
-
export async function exportVideo(options: ExportOptions): Promise<string> {
|
|
36
|
-
const {
|
|
37
|
-
demoName,
|
|
38
|
-
argoDir,
|
|
39
|
-
outputDir,
|
|
40
|
-
preset = 'slow',
|
|
41
|
-
crf = 16,
|
|
42
|
-
fps,
|
|
43
|
-
} = options;
|
|
44
|
-
|
|
45
|
-
checkFfmpeg();
|
|
46
|
-
|
|
47
|
-
const demoDir = join(argoDir, demoName);
|
|
48
|
-
const videoPath = join(demoDir, 'video.webm');
|
|
49
|
-
const audioPath = join(demoDir, 'narration-aligned.wav');
|
|
50
|
-
|
|
51
|
-
if (!existsSync(videoPath)) {
|
|
52
|
-
throw new Error(`Missing video.webm at ${videoPath}`);
|
|
53
|
-
}
|
|
54
|
-
if (!existsSync(audioPath)) {
|
|
55
|
-
throw new Error(`Missing narration-aligned.wav at ${audioPath}`);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (!existsSync(outputDir)) {
|
|
59
|
-
mkdirSync(outputDir, { recursive: true });
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const outputPath = join(outputDir, `${demoName}.mp4`);
|
|
63
|
-
|
|
64
|
-
const args: string[] = [
|
|
65
|
-
'-i', videoPath,
|
|
66
|
-
'-i', audioPath,
|
|
67
|
-
'-c:v', 'libx264',
|
|
68
|
-
'-preset', preset,
|
|
69
|
-
'-crf', String(crf),
|
|
70
|
-
'-c:a', 'aac',
|
|
71
|
-
'-b:a', '192k',
|
|
72
|
-
];
|
|
73
|
-
|
|
74
|
-
if (fps !== undefined) {
|
|
75
|
-
args.push('-r', String(fps));
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
args.push('-shortest', '-y', outputPath);
|
|
79
|
-
|
|
80
|
-
const result = spawnSync('ffmpeg', args, { stdio: 'inherit' });
|
|
81
|
-
|
|
82
|
-
if (result.error) {
|
|
83
|
-
throw new Error(`Failed to launch ffmpeg: ${result.error.message}`);
|
|
84
|
-
}
|
|
85
|
-
if (result.signal) {
|
|
86
|
-
throw new Error(`ffmpeg was killed by signal ${result.signal}`);
|
|
87
|
-
}
|
|
88
|
-
if (result.status !== 0) {
|
|
89
|
-
throw new Error(`ffmpeg failed with exit code ${result.status}`);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return outputPath;
|
|
93
|
-
}
|
package/src/fixtures.ts
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { test as base, expect } from '@playwright/test';
|
|
2
|
-
import type { Page } from '@playwright/test';
|
|
3
|
-
import { NarrationTimeline } from './narration.js';
|
|
4
|
-
|
|
5
|
-
type TimelineFactory = (title: string) => NarrationTimeline;
|
|
6
|
-
|
|
7
|
-
const defaultFactory: TimelineFactory = () => new NarrationTimeline();
|
|
8
|
-
|
|
9
|
-
export function createNarrationFixture(factory: TimelineFactory = defaultFactory) {
|
|
10
|
-
return async (
|
|
11
|
-
_context: Record<string, unknown>,
|
|
12
|
-
use: (timeline: NarrationTimeline) => Promise<void>,
|
|
13
|
-
testInfo: { title: string },
|
|
14
|
-
) => {
|
|
15
|
-
const timeline = factory(testInfo.title);
|
|
16
|
-
timeline.start();
|
|
17
|
-
try {
|
|
18
|
-
await use(timeline);
|
|
19
|
-
} finally {
|
|
20
|
-
await timeline.flush(`narration-${testInfo.title}.json`);
|
|
21
|
-
}
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export async function demoType(
|
|
26
|
-
page: Page,
|
|
27
|
-
selector: string,
|
|
28
|
-
text: string,
|
|
29
|
-
delay = 60,
|
|
30
|
-
): Promise<void> {
|
|
31
|
-
await page.locator(selector).pressSequentially(text, { delay });
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export const test = base.extend<{ narration: NarrationTimeline }>({
|
|
35
|
-
narration: async ({}, use, testInfo) => {
|
|
36
|
-
const timeline = new NarrationTimeline();
|
|
37
|
-
timeline.start();
|
|
38
|
-
try {
|
|
39
|
-
await use(timeline);
|
|
40
|
-
} finally {
|
|
41
|
-
const argoDir = process.env.ARGO_OUTPUT_DIR;
|
|
42
|
-
const outputPath = argoDir
|
|
43
|
-
? `${argoDir}/.timing.json`
|
|
44
|
-
: `narration-${testInfo.title}.json`;
|
|
45
|
-
await timeline.flush(outputPath);
|
|
46
|
-
}
|
|
47
|
-
},
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
export { expect };
|
package/src/index.ts
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
// Argo — Playwright demo recording with AI voiceover
|
|
2
|
-
|
|
3
|
-
// Config
|
|
4
|
-
export {
|
|
5
|
-
defineConfig,
|
|
6
|
-
loadConfig,
|
|
7
|
-
demosProject,
|
|
8
|
-
type ArgoConfig,
|
|
9
|
-
type UserConfig,
|
|
10
|
-
type TTSConfig,
|
|
11
|
-
type TTSEngine,
|
|
12
|
-
type VideoConfig,
|
|
13
|
-
type ExportConfig,
|
|
14
|
-
} from './config.js';
|
|
15
|
-
|
|
16
|
-
// Fixtures
|
|
17
|
-
export { test, expect, demoType } from './fixtures.js';
|
|
18
|
-
|
|
19
|
-
// Narration
|
|
20
|
-
export { NarrationTimeline } from './narration.js';
|
|
21
|
-
|
|
22
|
-
// Captions
|
|
23
|
-
export { showCaption, hideCaption, withCaption } from './captions.js';
|
|
24
|
-
|
|
25
|
-
// Overlays
|
|
26
|
-
export {
|
|
27
|
-
showOverlay,
|
|
28
|
-
hideOverlay,
|
|
29
|
-
withOverlay,
|
|
30
|
-
type OverlayCue,
|
|
31
|
-
type OverlayManifestEntry,
|
|
32
|
-
type Zone,
|
|
33
|
-
type TemplateType,
|
|
34
|
-
type MotionPreset,
|
|
35
|
-
} from './overlays/index.js';
|
|
36
|
-
|
|
37
|
-
// TTS
|
|
38
|
-
export { type TTSEngineOptions } from './tts/engine.js';
|
|
39
|
-
|
|
40
|
-
// Init
|
|
41
|
-
export { init } from './init.js';
|
package/src/narration.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
-
import { dirname } from 'node:path';
|
|
3
|
-
|
|
4
|
-
export class NarrationTimeline {
|
|
5
|
-
private timings: Map<string, number> = new Map();
|
|
6
|
-
private startTime: number | null = null;
|
|
7
|
-
|
|
8
|
-
start(): void {
|
|
9
|
-
this.startTime = Date.now();
|
|
10
|
-
this.timings = new Map();
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
mark(scene: string): void {
|
|
14
|
-
if (this.startTime === null) {
|
|
15
|
-
throw new Error('Cannot mark before start() has been called');
|
|
16
|
-
}
|
|
17
|
-
if (this.timings.has(scene)) {
|
|
18
|
-
throw new Error(`Duplicate scene name: "${scene}"`);
|
|
19
|
-
}
|
|
20
|
-
this.timings.set(scene, Date.now() - this.startTime);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
getTimings(): Record<string, number> {
|
|
24
|
-
return Object.fromEntries(this.timings);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
async flush(outputPath: string): Promise<void> {
|
|
28
|
-
await mkdir(dirname(outputPath), { recursive: true });
|
|
29
|
-
await writeFile(outputPath, JSON.stringify(this.getTimings(), null, 2), 'utf-8');
|
|
30
|
-
}
|
|
31
|
-
}
|
package/src/overlays/index.ts
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import type { Page } from '@playwright/test';
|
|
2
|
-
import type { OverlayCue, Zone } from './types.js';
|
|
3
|
-
import { injectIntoZone, removeZone, ZONE_ID_PREFIX } from './zones.js';
|
|
4
|
-
import { renderTemplate } from './templates.js';
|
|
5
|
-
import { getMotionCSS, getMotionStyles } from './motion.js';
|
|
6
|
-
|
|
7
|
-
export type { OverlayCue, OverlayManifestEntry, Zone, TemplateType, MotionPreset } from './types.js';
|
|
8
|
-
export { renderTemplate } from './templates.js';
|
|
9
|
-
|
|
10
|
-
export async function showOverlay(
|
|
11
|
-
page: Page,
|
|
12
|
-
_scene: string,
|
|
13
|
-
cue: OverlayCue,
|
|
14
|
-
durationMs: number,
|
|
15
|
-
): Promise<void> {
|
|
16
|
-
const zone: Zone = cue.placement ?? 'bottom-center';
|
|
17
|
-
const motion = cue.motion ?? 'none';
|
|
18
|
-
const { contentHtml, styles } = renderTemplate(cue);
|
|
19
|
-
const zoneId = ZONE_ID_PREFIX + zone;
|
|
20
|
-
const motionCSS = getMotionCSS(motion, zoneId);
|
|
21
|
-
const motionStyles = getMotionStyles(motion, zoneId);
|
|
22
|
-
|
|
23
|
-
await injectIntoZone(page, zone, contentHtml, { ...styles, ...motionStyles }, motionCSS);
|
|
24
|
-
await page.waitForTimeout(durationMs);
|
|
25
|
-
await removeZone(page, zone);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export async function hideOverlay(
|
|
29
|
-
page: Page,
|
|
30
|
-
zone: Zone = 'bottom-center',
|
|
31
|
-
): Promise<void> {
|
|
32
|
-
await removeZone(page, zone);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export async function withOverlay(
|
|
36
|
-
page: Page,
|
|
37
|
-
_scene: string,
|
|
38
|
-
cue: OverlayCue,
|
|
39
|
-
action: () => Promise<void>,
|
|
40
|
-
): Promise<void> {
|
|
41
|
-
const zone: Zone = cue.placement ?? 'bottom-center';
|
|
42
|
-
const motion = cue.motion ?? 'none';
|
|
43
|
-
const { contentHtml, styles } = renderTemplate(cue);
|
|
44
|
-
const zoneId = ZONE_ID_PREFIX + zone;
|
|
45
|
-
const motionCSS = getMotionCSS(motion, zoneId);
|
|
46
|
-
const motionStyles = getMotionStyles(motion, zoneId);
|
|
47
|
-
|
|
48
|
-
await injectIntoZone(page, zone, contentHtml, { ...styles, ...motionStyles }, motionCSS);
|
|
49
|
-
try {
|
|
50
|
-
await action();
|
|
51
|
-
} finally {
|
|
52
|
-
await removeZone(page, zone);
|
|
53
|
-
}
|
|
54
|
-
}
|
package/src/overlays/manifest.ts
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises';
|
|
2
|
-
import { existsSync } from 'node:fs';
|
|
3
|
-
import type { OverlayManifestEntry } from './types.js';
|
|
4
|
-
import { isValidTemplateType, isValidZone, isValidMotion } from './types.js';
|
|
5
|
-
|
|
6
|
-
export async function loadOverlayManifest(
|
|
7
|
-
manifestPath: string,
|
|
8
|
-
): Promise<OverlayManifestEntry[] | null> {
|
|
9
|
-
if (!existsSync(manifestPath)) return null;
|
|
10
|
-
|
|
11
|
-
const raw = await readFile(manifestPath, 'utf-8');
|
|
12
|
-
|
|
13
|
-
let parsed: unknown;
|
|
14
|
-
try {
|
|
15
|
-
parsed = JSON.parse(raw);
|
|
16
|
-
} catch (err) {
|
|
17
|
-
throw new Error(
|
|
18
|
-
`Failed to parse overlay manifest ${manifestPath}: ${(err as Error).message}`,
|
|
19
|
-
);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
if (!Array.isArray(parsed)) {
|
|
23
|
-
throw new Error(`Overlay manifest ${manifestPath} must contain a JSON array`);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const entries: OverlayManifestEntry[] = [];
|
|
27
|
-
|
|
28
|
-
for (let i = 0; i < parsed.length; i++) {
|
|
29
|
-
const entry = parsed[i];
|
|
30
|
-
const prefix = `Overlay manifest entry ${i}`;
|
|
31
|
-
|
|
32
|
-
if (!entry.scene || typeof entry.scene !== 'string') {
|
|
33
|
-
throw new Error(`${prefix}: missing required field "scene"`);
|
|
34
|
-
}
|
|
35
|
-
if (!entry.type || typeof entry.type !== 'string') {
|
|
36
|
-
throw new Error(`${prefix}: missing required field "type"`);
|
|
37
|
-
}
|
|
38
|
-
if (!isValidTemplateType(entry.type)) {
|
|
39
|
-
throw new Error(`${prefix}: unknown overlay type "${entry.type}"`);
|
|
40
|
-
}
|
|
41
|
-
if (entry.placement && !isValidZone(entry.placement)) {
|
|
42
|
-
throw new Error(`${prefix}: unknown placement "${entry.placement}"`);
|
|
43
|
-
}
|
|
44
|
-
if (entry.motion && !isValidMotion(entry.motion)) {
|
|
45
|
-
throw new Error(`${prefix}: unknown motion "${entry.motion}"`);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
entries.push(entry as OverlayManifestEntry);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return entries;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function hasImageAssets(entries: OverlayManifestEntry[]): boolean {
|
|
55
|
-
return entries.some((e) => e.type === 'image-card');
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function resolveAssetURLs(
|
|
59
|
-
entries: OverlayManifestEntry[],
|
|
60
|
-
assetBaseURL: string,
|
|
61
|
-
): OverlayManifestEntry[] {
|
|
62
|
-
return entries.map((e) => {
|
|
63
|
-
if (e.type === 'image-card' && e.src && !e.src.startsWith('http')) {
|
|
64
|
-
return { ...e, src: `${assetBaseURL}/${e.src.replace(/^assets\//, '')}` };
|
|
65
|
-
}
|
|
66
|
-
return e;
|
|
67
|
-
});
|
|
68
|
-
}
|
package/src/overlays/motion.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import type { MotionPreset } from './types.js';
|
|
2
|
-
|
|
3
|
-
export function getMotionCSS(motion: MotionPreset, elementId: string): string {
|
|
4
|
-
const animName = `argo-${motion}-${elementId}`;
|
|
5
|
-
switch (motion) {
|
|
6
|
-
case 'fade-in':
|
|
7
|
-
return `@keyframes ${animName} { from { opacity: 0; } to { opacity: 1; } }`;
|
|
8
|
-
case 'slide-in':
|
|
9
|
-
return `@keyframes ${animName} { from { opacity: 0; transform: translateX(-20px); } to { opacity: 1; transform: translateX(0); } }`;
|
|
10
|
-
case 'none':
|
|
11
|
-
default:
|
|
12
|
-
return '';
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function getMotionStyles(motion: MotionPreset, elementId: string): Record<string, string> {
|
|
17
|
-
const animName = `argo-${motion}-${elementId}`;
|
|
18
|
-
switch (motion) {
|
|
19
|
-
case 'fade-in':
|
|
20
|
-
return { animation: `${animName} 300ms ease-out forwards` };
|
|
21
|
-
case 'slide-in':
|
|
22
|
-
return { animation: `${animName} 400ms ease-out forwards` };
|
|
23
|
-
case 'none':
|
|
24
|
-
default:
|
|
25
|
-
return {};
|
|
26
|
-
}
|
|
27
|
-
}
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import type { OverlayCue } from './types.js';
|
|
2
|
-
|
|
3
|
-
export interface TemplateResult {
|
|
4
|
-
contentHtml: string;
|
|
5
|
-
styles: Record<string, string>;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
function escapeHtml(str: string): string {
|
|
9
|
-
return str
|
|
10
|
-
.replace(/&/g, '&')
|
|
11
|
-
.replace(/</g, '<')
|
|
12
|
-
.replace(/>/g, '>')
|
|
13
|
-
.replace(/"/g, '"');
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function lowerThird(text: string): TemplateResult {
|
|
17
|
-
return {
|
|
18
|
-
contentHtml: `<span>${escapeHtml(text)}</span>`,
|
|
19
|
-
styles: {
|
|
20
|
-
background: 'rgba(0, 0, 0, 0.85)',
|
|
21
|
-
color: '#fff',
|
|
22
|
-
padding: '16px 32px',
|
|
23
|
-
borderRadius: '12px',
|
|
24
|
-
fontSize: '28px',
|
|
25
|
-
fontWeight: '500',
|
|
26
|
-
textAlign: 'center',
|
|
27
|
-
maxWidth: '80vw',
|
|
28
|
-
letterSpacing: '0.01em',
|
|
29
|
-
lineHeight: '1.4',
|
|
30
|
-
boxShadow: '0 4px 24px rgba(0, 0, 0, 0.3)',
|
|
31
|
-
},
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function headlineCard(title: string, kicker?: string, body?: string): TemplateResult {
|
|
36
|
-
const parts: string[] = [];
|
|
37
|
-
if (kicker) {
|
|
38
|
-
parts.push(
|
|
39
|
-
`<div style="font-size:12px;font-weight:700;letter-spacing:0.08em;text-transform:uppercase;color:rgba(255,255,255,0.7);margin-bottom:8px">${escapeHtml(kicker)}</div>`,
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
parts.push(
|
|
43
|
-
`<div style="font-size:26px;font-weight:700;line-height:1.25;color:#fff">${escapeHtml(title)}</div>`,
|
|
44
|
-
);
|
|
45
|
-
if (body) {
|
|
46
|
-
parts.push(
|
|
47
|
-
`<div style="font-size:16px;line-height:1.5;color:rgba(255,255,255,0.85);margin-top:8px">${escapeHtml(body)}</div>`,
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
return {
|
|
51
|
-
contentHtml: parts.join(''),
|
|
52
|
-
styles: {
|
|
53
|
-
background: 'rgba(0, 0, 0, 0.7)',
|
|
54
|
-
backdropFilter: 'blur(16px)',
|
|
55
|
-
WebkitBackdropFilter: 'blur(16px)',
|
|
56
|
-
padding: '24px 28px',
|
|
57
|
-
borderRadius: '16px',
|
|
58
|
-
maxWidth: '420px',
|
|
59
|
-
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.4)',
|
|
60
|
-
},
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function callout(text: string): TemplateResult {
|
|
65
|
-
return {
|
|
66
|
-
contentHtml: `<span>${escapeHtml(text)}</span>`,
|
|
67
|
-
styles: {
|
|
68
|
-
background: 'rgba(0, 0, 0, 0.8)',
|
|
69
|
-
color: '#fff',
|
|
70
|
-
padding: '10px 18px',
|
|
71
|
-
borderRadius: '20px',
|
|
72
|
-
fontSize: '16px',
|
|
73
|
-
fontWeight: '500',
|
|
74
|
-
lineHeight: '1.3',
|
|
75
|
-
maxWidth: '300px',
|
|
76
|
-
boxShadow: '0 2px 12px rgba(0, 0, 0, 0.3)',
|
|
77
|
-
},
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function imageCard(src: string, title?: string, body?: string): TemplateResult {
|
|
82
|
-
const parts: string[] = [];
|
|
83
|
-
parts.push(
|
|
84
|
-
`<img src="${escapeHtml(src)}" style="max-width:100%;border-radius:8px;display:block" />`,
|
|
85
|
-
);
|
|
86
|
-
if (title) {
|
|
87
|
-
parts.push(
|
|
88
|
-
`<div style="font-size:18px;font-weight:600;color:#fff;margin-top:12px">${escapeHtml(title)}</div>`,
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
if (body) {
|
|
92
|
-
parts.push(
|
|
93
|
-
`<div style="font-size:14px;color:rgba(255,255,255,0.8);margin-top:4px;line-height:1.4">${escapeHtml(body)}</div>`,
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
return {
|
|
97
|
-
contentHtml: parts.join(''),
|
|
98
|
-
styles: {
|
|
99
|
-
background: 'rgba(0, 0, 0, 0.75)',
|
|
100
|
-
backdropFilter: 'blur(12px)',
|
|
101
|
-
WebkitBackdropFilter: 'blur(12px)',
|
|
102
|
-
padding: '16px',
|
|
103
|
-
borderRadius: '14px',
|
|
104
|
-
maxWidth: '360px',
|
|
105
|
-
boxShadow: '0 6px 24px rgba(0, 0, 0, 0.4)',
|
|
106
|
-
},
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export function renderTemplate(cue: OverlayCue): TemplateResult {
|
|
111
|
-
switch (cue.type) {
|
|
112
|
-
case 'lower-third':
|
|
113
|
-
return lowerThird(cue.text);
|
|
114
|
-
case 'headline-card':
|
|
115
|
-
return headlineCard(cue.title, cue.kicker, cue.body);
|
|
116
|
-
case 'callout':
|
|
117
|
-
return callout(cue.text);
|
|
118
|
-
case 'image-card':
|
|
119
|
-
return imageCard(cue.src, cue.title, cue.body);
|
|
120
|
-
}
|
|
121
|
-
}
|
package/src/overlays/types.ts
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
export const ZONES = [
|
|
2
|
-
'bottom-center',
|
|
3
|
-
'top-left',
|
|
4
|
-
'top-right',
|
|
5
|
-
'bottom-left',
|
|
6
|
-
'bottom-right',
|
|
7
|
-
'center',
|
|
8
|
-
] as const;
|
|
9
|
-
|
|
10
|
-
export type Zone = (typeof ZONES)[number];
|
|
11
|
-
|
|
12
|
-
export const TEMPLATE_TYPES = [
|
|
13
|
-
'lower-third',
|
|
14
|
-
'headline-card',
|
|
15
|
-
'callout',
|
|
16
|
-
'image-card',
|
|
17
|
-
] as const;
|
|
18
|
-
|
|
19
|
-
export type TemplateType = (typeof TEMPLATE_TYPES)[number];
|
|
20
|
-
|
|
21
|
-
export const MOTIONS = ['none', 'fade-in', 'slide-in'] as const;
|
|
22
|
-
|
|
23
|
-
export type MotionPreset = (typeof MOTIONS)[number];
|
|
24
|
-
|
|
25
|
-
export interface LowerThirdCue {
|
|
26
|
-
type: 'lower-third';
|
|
27
|
-
text: string;
|
|
28
|
-
placement?: Zone;
|
|
29
|
-
motion?: MotionPreset;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface HeadlineCardCue {
|
|
33
|
-
type: 'headline-card';
|
|
34
|
-
title: string;
|
|
35
|
-
kicker?: string;
|
|
36
|
-
body?: string;
|
|
37
|
-
placement?: Zone;
|
|
38
|
-
motion?: MotionPreset;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface CalloutCue {
|
|
42
|
-
type: 'callout';
|
|
43
|
-
text: string;
|
|
44
|
-
placement?: Zone;
|
|
45
|
-
motion?: MotionPreset;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export interface ImageCardCue {
|
|
49
|
-
type: 'image-card';
|
|
50
|
-
src: string;
|
|
51
|
-
title?: string;
|
|
52
|
-
body?: string;
|
|
53
|
-
placement?: Zone;
|
|
54
|
-
motion?: MotionPreset;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export type OverlayCue = LowerThirdCue | HeadlineCardCue | CalloutCue | ImageCardCue;
|
|
58
|
-
|
|
59
|
-
export type OverlayManifestEntry = OverlayCue & {
|
|
60
|
-
scene: string;
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
export function isValidZone(value: string): value is Zone {
|
|
64
|
-
return (ZONES as readonly string[]).includes(value);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function isValidTemplateType(value: string): value is TemplateType {
|
|
68
|
-
return (TEMPLATE_TYPES as readonly string[]).includes(value);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function isValidMotion(value: string): value is MotionPreset {
|
|
72
|
-
return (MOTIONS as readonly string[]).includes(value);
|
|
73
|
-
}
|
package/src/overlays/zones.ts
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import type { Page } from '@playwright/test';
|
|
2
|
-
import type { Zone } from './types.js';
|
|
3
|
-
|
|
4
|
-
export const ZONE_ID_PREFIX = 'argo-overlay-';
|
|
5
|
-
|
|
6
|
-
const ZONE_POSITIONS: Record<Zone, Record<string, string>> = {
|
|
7
|
-
'bottom-center': {
|
|
8
|
-
position: 'fixed', bottom: '60px', left: '50%', transform: 'translateX(-50%)',
|
|
9
|
-
},
|
|
10
|
-
'top-left': {
|
|
11
|
-
position: 'fixed', top: '40px', left: '40px',
|
|
12
|
-
},
|
|
13
|
-
'top-right': {
|
|
14
|
-
position: 'fixed', top: '40px', right: '40px',
|
|
15
|
-
},
|
|
16
|
-
'bottom-left': {
|
|
17
|
-
position: 'fixed', bottom: '60px', left: '40px',
|
|
18
|
-
},
|
|
19
|
-
'bottom-right': {
|
|
20
|
-
position: 'fixed', bottom: '60px', right: '40px',
|
|
21
|
-
},
|
|
22
|
-
'center': {
|
|
23
|
-
position: 'fixed', top: '50%', left: '50%', transform: 'translate(-50%, -50%)',
|
|
24
|
-
},
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Inject content into a zone. Replaces any existing overlay in that zone.
|
|
29
|
-
* Security: contentHtml is generated by template renderers which escape all
|
|
30
|
-
* text fields via escapeHtml(). Content originates from author-controlled
|
|
31
|
-
* manifest files, not end-user input.
|
|
32
|
-
*/
|
|
33
|
-
export async function injectIntoZone(
|
|
34
|
-
page: Page,
|
|
35
|
-
zone: Zone,
|
|
36
|
-
contentHtml: string,
|
|
37
|
-
containerStyles: Record<string, string>,
|
|
38
|
-
animationCSS?: string,
|
|
39
|
-
): Promise<void> {
|
|
40
|
-
const zoneId = ZONE_ID_PREFIX + zone;
|
|
41
|
-
const positionStyles = ZONE_POSITIONS[zone];
|
|
42
|
-
const baseStyles: Record<string, string> = {
|
|
43
|
-
...positionStyles,
|
|
44
|
-
zIndex: '999999',
|
|
45
|
-
pointerEvents: 'none',
|
|
46
|
-
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
47
|
-
...containerStyles,
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
await page.evaluate(([id, html, styles, css]) => {
|
|
51
|
-
const existing = document.getElementById(id);
|
|
52
|
-
if (existing) existing.remove();
|
|
53
|
-
|
|
54
|
-
if (css) {
|
|
55
|
-
const styleId = id + '-style';
|
|
56
|
-
let styleEl = document.getElementById(styleId) as HTMLStyleElement | null;
|
|
57
|
-
if (!styleEl) {
|
|
58
|
-
styleEl = document.createElement('style');
|
|
59
|
-
styleEl.id = styleId;
|
|
60
|
-
document.head.appendChild(styleEl);
|
|
61
|
-
}
|
|
62
|
-
styleEl.textContent = css;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const container = document.createElement('div');
|
|
66
|
-
container.id = id;
|
|
67
|
-
container.innerHTML = html;
|
|
68
|
-
Object.assign(container.style, styles);
|
|
69
|
-
document.body.appendChild(container);
|
|
70
|
-
}, [zoneId, contentHtml, baseStyles, animationCSS ?? ''] as const);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Remove the overlay in the specified zone, if any.
|
|
75
|
-
*/
|
|
76
|
-
export async function removeZone(page: Page, zone: Zone): Promise<void> {
|
|
77
|
-
const zoneId = ZONE_ID_PREFIX + zone;
|
|
78
|
-
await page.evaluate((id) => {
|
|
79
|
-
document.getElementById(id)?.remove();
|
|
80
|
-
document.getElementById(id + '-style')?.remove();
|
|
81
|
-
}, zoneId);
|
|
82
|
-
}
|