@scenar/cli 0.10.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/example-bundle/assets/index-DxmqjG-8.js +388 -0
  2. package/example-bundle/assets/style-B2sxnVVZ.css +1 -0
  3. package/example-bundle/embed.js +1 -1
  4. package/example-bundle/index.html +2 -2
  5. package/example-bundle/logo.png +0 -0
  6. package/example-bundle/pack-manifest.json +33 -9
  7. package/example-bundle/soundtrack/music.mp3 +0 -0
  8. package/example-bundle/soundtrack/sfx/click.mp3 +0 -0
  9. package/example-bundle/soundtrack/sfx/keystroke.mp3 +0 -0
  10. package/package.json +4 -4
  11. package/src/__tests__/collect-pack-shots.test.ts +8 -0
  12. package/src/__tests__/elevenlabs-provider.test.ts +187 -0
  13. package/src/__tests__/embed-snippet.test.ts +5 -0
  14. package/src/__tests__/find-authored-soundtrack.test.ts +54 -0
  15. package/src/__tests__/find-authored-title-cards.test.ts +65 -0
  16. package/src/__tests__/load-bundle.test.ts +67 -0
  17. package/src/__tests__/narrate-command.test.ts +65 -1
  18. package/src/__tests__/narration-cache.test.ts +134 -0
  19. package/src/__tests__/pack-bundle-contract.test.ts +2 -0
  20. package/src/__tests__/pack-generate-embed-entry.test.ts +95 -16
  21. package/src/__tests__/presenter-command.test.ts +366 -0
  22. package/src/__tests__/render-command.test.ts +11 -0
  23. package/src/__tests__/render-generate-entry.test.ts +127 -0
  24. package/src/__tests__/resolve-provider.test.ts +32 -3
  25. package/src/__tests__/scenario-validator.test.ts +174 -0
  26. package/src/__tests__/stage-assets.test.ts +269 -0
  27. package/src/commands/narrate.js +1 -1
  28. package/src/commands/narrate.js.map +1 -1
  29. package/src/commands/narrate.ts +1 -1
  30. package/src/commands/presenter.d.ts +3 -0
  31. package/src/commands/presenter.d.ts.map +1 -0
  32. package/src/commands/presenter.js +89 -0
  33. package/src/commands/presenter.js.map +1 -0
  34. package/src/commands/presenter.ts +107 -0
  35. package/src/commands/render.d.ts.map +1 -1
  36. package/src/commands/render.js +2 -0
  37. package/src/commands/render.js.map +1 -1
  38. package/src/commands/render.ts +6 -0
  39. package/src/embed/embed-snippet.d.ts +5 -0
  40. package/src/embed/embed-snippet.d.ts.map +1 -1
  41. package/src/embed/embed-snippet.js +6 -1
  42. package/src/embed/embed-snippet.js.map +1 -1
  43. package/src/embed/embed-snippet.ts +6 -1
  44. package/src/index.d.ts.map +1 -1
  45. package/src/index.js +2 -0
  46. package/src/index.js.map +1 -1
  47. package/src/index.ts +2 -0
  48. package/src/narrate/run-narrate.d.ts +15 -1
  49. package/src/narrate/run-narrate.d.ts.map +1 -1
  50. package/src/narrate/run-narrate.js +11 -4
  51. package/src/narrate/run-narrate.js.map +1 -1
  52. package/src/narrate/run-narrate.ts +15 -6
  53. package/src/pack/bundle-contract.d.ts +1 -0
  54. package/src/pack/bundle-contract.d.ts.map +1 -1
  55. package/src/pack/bundle-contract.js +3 -0
  56. package/src/pack/bundle-contract.js.map +1 -1
  57. package/src/pack/bundle-contract.ts +3 -0
  58. package/src/pack/collect-pack-shots.d.ts +5 -1
  59. package/src/pack/collect-pack-shots.d.ts.map +1 -1
  60. package/src/pack/collect-pack-shots.js +3 -1
  61. package/src/pack/collect-pack-shots.js.map +1 -1
  62. package/src/pack/collect-pack-shots.ts +15 -1
  63. package/src/pack/generate-embed-entry.d.ts +7 -0
  64. package/src/pack/generate-embed-entry.d.ts.map +1 -1
  65. package/src/pack/generate-embed-entry.js +102 -7
  66. package/src/pack/generate-embed-entry.js.map +1 -1
  67. package/src/pack/generate-embed-entry.ts +107 -7
  68. package/src/pack/run-pack.d.ts +1 -0
  69. package/src/pack/run-pack.d.ts.map +1 -1
  70. package/src/pack/run-pack.js +100 -9
  71. package/src/pack/run-pack.js.map +1 -1
  72. package/src/pack/run-pack.ts +133 -9
  73. package/src/presenter/heygen-client.d.ts +69 -0
  74. package/src/presenter/heygen-client.d.ts.map +1 -0
  75. package/src/presenter/heygen-client.js +154 -0
  76. package/src/presenter/heygen-client.js.map +1 -0
  77. package/src/presenter/heygen-client.ts +224 -0
  78. package/src/presenter/run-presenter.d.ts +67 -0
  79. package/src/presenter/run-presenter.d.ts.map +1 -0
  80. package/src/presenter/run-presenter.js +348 -0
  81. package/src/presenter/run-presenter.js.map +1 -0
  82. package/src/presenter/run-presenter.ts +536 -0
  83. package/src/render/generate-entry.d.ts +4 -0
  84. package/src/render/generate-entry.d.ts.map +1 -1
  85. package/src/render/generate-entry.js +87 -9
  86. package/src/render/generate-entry.js.map +1 -1
  87. package/src/render/generate-entry.ts +96 -9
  88. package/src/render/run-render.d.ts +6 -0
  89. package/src/render/run-render.d.ts.map +1 -1
  90. package/src/render/run-render.js +58 -0
  91. package/src/render/run-render.js.map +1 -1
  92. package/src/render/run-render.ts +72 -0
  93. package/src/render/stage-assets.d.ts +44 -0
  94. package/src/render/stage-assets.d.ts.map +1 -0
  95. package/src/render/stage-assets.js +91 -0
  96. package/src/render/stage-assets.js.map +1 -0
  97. package/src/render/stage-assets.ts +130 -0
  98. package/src/tts/echogarden.d.ts.map +1 -1
  99. package/src/tts/echogarden.js +4 -2
  100. package/src/tts/echogarden.js.map +1 -1
  101. package/src/tts/echogarden.ts +5 -2
  102. package/src/tts/edge-tts.d.ts.map +1 -1
  103. package/src/tts/edge-tts.js +2 -1
  104. package/src/tts/edge-tts.js.map +1 -1
  105. package/src/tts/edge-tts.ts +2 -1
  106. package/src/tts/elevenlabs.d.ts +15 -0
  107. package/src/tts/elevenlabs.d.ts.map +1 -0
  108. package/src/tts/elevenlabs.js +73 -0
  109. package/src/tts/elevenlabs.js.map +1 -0
  110. package/src/tts/elevenlabs.ts +105 -0
  111. package/src/tts/openai.d.ts.map +1 -1
  112. package/src/tts/openai.js +2 -1
  113. package/src/tts/openai.js.map +1 -1
  114. package/src/tts/openai.ts +2 -1
  115. package/src/tts/resolve-provider.d.ts +1 -1
  116. package/src/tts/resolve-provider.d.ts.map +1 -1
  117. package/src/tts/resolve-provider.js +10 -3
  118. package/src/tts/resolve-provider.js.map +1 -1
  119. package/src/tts/resolve-provider.ts +12 -4
  120. package/src/tts/types.d.ts +9 -0
  121. package/src/tts/types.d.ts.map +1 -1
  122. package/src/tts/types.ts +10 -0
  123. package/src/util/confirm.d.ts +11 -0
  124. package/src/util/confirm.d.ts.map +1 -0
  125. package/src/util/confirm.js +24 -0
  126. package/src/util/confirm.js.map +1 -0
  127. package/src/util/confirm.ts +23 -0
  128. package/src/util/load-bundle.d.ts +17 -1
  129. package/src/util/load-bundle.d.ts.map +1 -1
  130. package/src/util/load-bundle.js +19 -11
  131. package/src/util/load-bundle.js.map +1 -1
  132. package/src/util/load-bundle.ts +38 -12
  133. package/src/util/load-ts.d.ts +77 -0
  134. package/src/util/load-ts.d.ts.map +1 -1
  135. package/src/util/load-ts.js +125 -0
  136. package/src/util/load-ts.js.map +1 -1
  137. package/src/util/load-ts.ts +165 -0
  138. package/src/util/narration-cache.d.ts +13 -5
  139. package/src/util/narration-cache.d.ts.map +1 -1
  140. package/src/util/narration-cache.js +10 -8
  141. package/src/util/narration-cache.js.map +1 -1
  142. package/src/util/narration-cache.ts +22 -6
  143. package/src/util/presenter-cache.d.ts +44 -0
  144. package/src/util/presenter-cache.d.ts.map +1 -0
  145. package/src/util/presenter-cache.js +43 -0
  146. package/src/util/presenter-cache.js.map +1 -0
  147. package/src/util/presenter-cache.ts +101 -0
  148. package/src/util/scenario-assets.d.ts +47 -0
  149. package/src/util/scenario-assets.d.ts.map +1 -0
  150. package/src/util/scenario-assets.js +71 -0
  151. package/src/util/scenario-assets.js.map +1 -0
  152. package/src/util/scenario-assets.ts +111 -0
  153. package/src/util/soundtrack-assets.d.ts +34 -0
  154. package/src/util/soundtrack-assets.d.ts.map +1 -0
  155. package/src/util/soundtrack-assets.js +48 -0
  156. package/src/util/soundtrack-assets.js.map +1 -0
  157. package/src/util/soundtrack-assets.ts +69 -0
  158. package/src/validate/scenario-validator.d.ts.map +1 -1
  159. package/src/validate/scenario-validator.js +82 -0
  160. package/src/validate/scenario-validator.js.map +1 -1
  161. package/src/validate/scenario-validator.ts +110 -0
  162. package/tsconfig.tsbuildinfo +1 -1
  163. package/example-bundle/assets/index-DpJUOVBT.js +0 -388
  164. package/example-bundle/assets/style-ix_q0zsc.css +0 -1
@@ -44,6 +44,66 @@ describe("validateScenario", () => {
44
44
  expect(validateScenario(s).valid).toBe(true);
45
45
  });
46
46
 
47
+ // --- Presenter (per-step opt-in, requires narration) ---
48
+
49
+ it("accepts presenter: true on a narrated step", () => {
50
+ const s = validScenario();
51
+ s.steps[0] = { ...s.steps[0]!, presenter: true } as typeof s.steps[0];
52
+ expect(validateScenario(s).valid).toBe(true);
53
+ });
54
+
55
+ it("accepts presenter: false on a step without narration", () => {
56
+ const s = validScenario();
57
+ s.steps[0] = {
58
+ view: "intro",
59
+ delayMs: 0,
60
+ presenter: false,
61
+ } as unknown as typeof s.steps[0];
62
+ expect(validateScenario(s).valid).toBe(true);
63
+ });
64
+
65
+ it("rejects presenter: true on a step without narration, naming step, field, and rule", () => {
66
+ const s = validScenario();
67
+ s.steps[0] = {
68
+ view: "intro",
69
+ delayMs: 0,
70
+ presenter: true,
71
+ } as unknown as typeof s.steps[0];
72
+
73
+ const result = validateScenario(s);
74
+ expect(result.valid).toBe(false);
75
+ expect(result.errors[0]!.path).toBe("steps[0].presenter");
76
+ expect(result.errors[0]!.reason).toBe(
77
+ "presenter requires narration_text — a presenter clip is derived from the step's narration audio.",
78
+ );
79
+ });
80
+
81
+ it("rejects presenter: true when narrationText is empty", () => {
82
+ const s = validScenario();
83
+ s.steps[0] = {
84
+ ...s.steps[0]!,
85
+ narrationText: "",
86
+ presenter: true,
87
+ } as typeof s.steps[0];
88
+
89
+ const result = validateScenario(s);
90
+ expect(result.valid).toBe(false);
91
+ expect(result.errors[0]!.path).toBe("steps[0].presenter");
92
+ });
93
+
94
+ it("rejects a non-boolean presenter", () => {
95
+ const s = validScenario();
96
+ s.steps[0] = {
97
+ ...s.steps[0]!,
98
+ presenter: "yes",
99
+ } as unknown as typeof s.steps[0];
100
+
101
+ const result = validateScenario(s);
102
+ expect(result.valid).toBe(false);
103
+ expect(result.errors[0]!.path).toBe("steps[0].presenter");
104
+ expect(result.errors[0]!.reason).toBe("presenter must be a boolean.");
105
+ });
106
+
47
107
  // --- Root-level errors ---
48
108
 
49
109
  it("rejects null", () => {
@@ -349,4 +409,118 @@ describe("validateScenario", () => {
349
409
  expect(result.valid).toBe(false);
350
410
  expect(result.errors.length).toBeGreaterThanOrEqual(4);
351
411
  });
412
+
413
+ // --- Soundtrack ---
414
+
415
+ it("accepts a valid soundtrack, including explicit 0.0 volumes", () => {
416
+ const result = validateScenario({
417
+ steps: [{ view: "x", delayMs: 0 }],
418
+ soundtrack: {
419
+ musicSrc: "./soundtrack/music.mp3",
420
+ musicVolume: 0.25,
421
+ duckingVolume: 0,
422
+ sfx: true,
423
+ },
424
+ });
425
+ expect(result.valid).toBe(true);
426
+ });
427
+
428
+ it("accepts a soundtrack with only sfx (no music)", () => {
429
+ const result = validateScenario({
430
+ steps: [{ view: "x", delayMs: 0 }],
431
+ soundtrack: { sfx: true },
432
+ });
433
+ expect(result.valid).toBe(true);
434
+ });
435
+
436
+ it("rejects a non-object soundtrack", () => {
437
+ const result = validateScenario({
438
+ steps: [{ view: "x", delayMs: 0 }],
439
+ soundtrack: "music.mp3",
440
+ });
441
+ expect(result.errors).toContainEqual(
442
+ expect.objectContaining({ path: "soundtrack" }),
443
+ );
444
+ });
445
+
446
+ it("rejects an empty musicSrc", () => {
447
+ const result = validateScenario({
448
+ steps: [{ view: "x", delayMs: 0 }],
449
+ soundtrack: { musicSrc: "" },
450
+ });
451
+ expect(result.errors).toContainEqual(
452
+ expect.objectContaining({ path: "soundtrack.musicSrc" }),
453
+ );
454
+ });
455
+
456
+ it.each([
457
+ ["musicVolume above 1", { musicVolume: 1.2 }, "soundtrack.musicVolume"],
458
+ ["musicVolume negative", { musicVolume: -0.1 }, "soundtrack.musicVolume"],
459
+ ["musicVolume non-numeric", { musicVolume: "loud" }, "soundtrack.musicVolume"],
460
+ ["duckingVolume above 1", { duckingVolume: 2 }, "soundtrack.duckingVolume"],
461
+ ])("rejects %s", (_name, soundtrack, expectedPath) => {
462
+ const result = validateScenario({
463
+ steps: [{ view: "x", delayMs: 0 }],
464
+ soundtrack,
465
+ });
466
+ expect(result.errors).toContainEqual(
467
+ expect.objectContaining({ path: expectedPath }),
468
+ );
469
+ });
470
+
471
+ it("rejects a non-boolean sfx", () => {
472
+ const result = validateScenario({
473
+ steps: [{ view: "x", delayMs: 0 }],
474
+ soundtrack: { sfx: "yes" },
475
+ });
476
+ expect(result.errors).toContainEqual(
477
+ expect.objectContaining({ path: "soundtrack.sfx" }),
478
+ );
479
+ });
480
+
481
+ it("accepts valid title cards, one-sided or both", () => {
482
+ for (const titleCards of [
483
+ { intro: { title: "Acme", subtitle: "Fast", logoSrc: "./logo.png" } },
484
+ { outro: { title: "Try it", ctaText: "acme.dev", durationMs: 4000 } },
485
+ { intro: { title: "Acme" }, outro: { title: "Bye" } },
486
+ ]) {
487
+ const result = validateScenario({
488
+ steps: [{ view: "x", delayMs: 0 }],
489
+ titleCards,
490
+ });
491
+ expect(result.valid).toBe(true);
492
+ }
493
+ });
494
+
495
+ it("rejects a non-object titleCards", () => {
496
+ const result = validateScenario({
497
+ steps: [{ view: "x", delayMs: 0 }],
498
+ titleCards: "intro",
499
+ });
500
+ expect(result.errors).toContainEqual(
501
+ expect.objectContaining({ path: "titleCards" }),
502
+ );
503
+ });
504
+
505
+ it.each([
506
+ ["a missing title", { intro: {} }, "titleCards.intro.title"],
507
+ ["an empty title", { outro: { title: "" } }, "titleCards.outro.title"],
508
+ ["an empty subtitle", { intro: { title: "T", subtitle: "" } }, "titleCards.intro.subtitle"],
509
+ ["an empty logoSrc", { intro: { title: "T", logoSrc: "" } }, "titleCards.intro.logoSrc"],
510
+ ["an empty ctaText", { outro: { title: "T", ctaText: "" } }, "titleCards.outro.ctaText"],
511
+ ["a zero durationMs", { intro: { title: "T", durationMs: 0 } }, "titleCards.intro.durationMs"],
512
+ [
513
+ "a negative durationMs",
514
+ { intro: { title: "T", durationMs: -100 } },
515
+ "titleCards.intro.durationMs",
516
+ ],
517
+ ])("rejects a card with %s", (_name, titleCards, expectedPath) => {
518
+ const result = validateScenario({
519
+ steps: [{ view: "x", delayMs: 0 }],
520
+ titleCards,
521
+ });
522
+ expect(result.errors).toContainEqual(
523
+ expect.objectContaining({ path: expectedPath }),
524
+ );
525
+ });
352
526
  });
@@ -0,0 +1,269 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { mkdtemp, mkdir, readFile, writeFile, rm, access } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join, dirname } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { stageRenderAssets } from "../render/stage-assets.js";
7
+ import {
8
+ SFX_DEST_PATHS,
9
+ isRemoteUrl,
10
+ resolveMusicAsset,
11
+ resolveSfxAssetPaths,
12
+ } from "../util/soundtrack-assets.js";
13
+ import { resolveLogoAsset } from "../util/scenario-assets.js";
14
+
15
+ let scenarioDir: string;
16
+ let publicDir: string;
17
+
18
+ beforeEach(async () => {
19
+ scenarioDir = await mkdtemp(join(tmpdir(), "scenar-stage-"));
20
+ publicDir = join(scenarioDir, ".scenar-render", "public");
21
+ });
22
+
23
+ afterEach(async () => {
24
+ await rm(scenarioDir, { recursive: true, force: true });
25
+ });
26
+
27
+ async function exists(path: string): Promise<boolean> {
28
+ return access(path).then(
29
+ () => true,
30
+ () => false,
31
+ );
32
+ }
33
+
34
+ describe("stageRenderAssets", () => {
35
+ it("stages narration clips flat at the public root (srcs are ./step-N.mp3)", async () => {
36
+ await mkdir(join(scenarioDir, "narration"), { recursive: true });
37
+ await writeFile(join(scenarioDir, "narration", "step-0.mp3"), "audio-0");
38
+ await writeFile(join(scenarioDir, "narration", "step-2.mp3"), "audio-2");
39
+ await writeFile(join(scenarioDir, "narration", "manifest.json"), "{}");
40
+
41
+ const staged = await stageRenderAssets({
42
+ scenarioDir,
43
+ publicDir,
44
+ hasNarration: true,
45
+ });
46
+
47
+ expect(staged).toBe(2);
48
+ expect(await readFile(join(publicDir, "step-0.mp3"), "utf-8")).toBe("audio-0");
49
+ expect(await readFile(join(publicDir, "step-2.mp3"), "utf-8")).toBe("audio-2");
50
+ // The manifest itself is imported by the entry, never served.
51
+ expect(await exists(join(publicDir, "manifest.json"))).toBe(false);
52
+ });
53
+
54
+ it("stages presenter clips flat at the public root beside narration (srcs are ./step-N.mp4)", async () => {
55
+ await mkdir(join(scenarioDir, "narration"), { recursive: true });
56
+ await writeFile(join(scenarioDir, "narration", "step-0.mp3"), "audio-0");
57
+ await writeFile(join(scenarioDir, "narration", "manifest.json"), "{}");
58
+ await mkdir(join(scenarioDir, "presenter"), { recursive: true });
59
+ await writeFile(join(scenarioDir, "presenter", "step-0.mp4"), "clip-0");
60
+ await writeFile(join(scenarioDir, "presenter", "manifest.json"), "{}");
61
+ await writeFile(join(scenarioDir, "presenter", ".presenter-cache.json"), "{}");
62
+
63
+ const staged = await stageRenderAssets({
64
+ scenarioDir,
65
+ publicDir,
66
+ hasNarration: true,
67
+ hasPresenter: true,
68
+ });
69
+
70
+ // step-0.mp3 + step-0.mp4 — same basename, different extensions, no collision.
71
+ expect(staged).toBe(2);
72
+ expect(await readFile(join(publicDir, "step-0.mp3"), "utf-8")).toBe("audio-0");
73
+ expect(await readFile(join(publicDir, "step-0.mp4"), "utf-8")).toBe("clip-0");
74
+ // Manifests are imported by the entry; the cache never leaves the scenario.
75
+ expect(await exists(join(publicDir, "manifest.json"))).toBe(false);
76
+ expect(await exists(join(publicDir, ".presenter-cache.json"))).toBe(false);
77
+ });
78
+
79
+ it("stages no presenter clips when the scenario has none", async () => {
80
+ const staged = await stageRenderAssets({
81
+ scenarioDir,
82
+ publicDir,
83
+ hasNarration: false,
84
+ hasPresenter: false,
85
+ });
86
+ expect(staged).toBe(0);
87
+ });
88
+
89
+ it("stages the music asset at its scenario-relative path", async () => {
90
+ await mkdir(join(scenarioDir, "soundtrack"), { recursive: true });
91
+ await writeFile(join(scenarioDir, "soundtrack", "music.mp3"), "music-bytes");
92
+
93
+ const staged = await stageRenderAssets({
94
+ scenarioDir,
95
+ publicDir,
96
+ hasNarration: false,
97
+ soundtrack: { musicSrc: "./soundtrack/music.mp3" },
98
+ });
99
+
100
+ expect(staged).toBe(1);
101
+ expect(await readFile(join(publicDir, "soundtrack", "music.mp3"), "utf-8")).toBe(
102
+ "music-bytes",
103
+ );
104
+ });
105
+
106
+ it("stages the built-in SFX set at the shared destinations", async () => {
107
+ const clickSource = join(scenarioDir, "fake-click.mp3");
108
+ const keystrokeSource = join(scenarioDir, "fake-keystroke.mp3");
109
+ await writeFile(clickSource, "click-bytes");
110
+ await writeFile(keystrokeSource, "keystroke-bytes");
111
+
112
+ const staged = await stageRenderAssets({
113
+ scenarioDir,
114
+ publicDir,
115
+ hasNarration: false,
116
+ soundtrack: { sfx: true },
117
+ sfxPaths: { click: clickSource, keystroke: keystrokeSource },
118
+ });
119
+
120
+ expect(staged).toBe(2);
121
+ expect(await readFile(join(publicDir, SFX_DEST_PATHS.click), "utf-8")).toBe("click-bytes");
122
+ expect(await readFile(join(publicDir, SFX_DEST_PATHS.keystroke), "utf-8")).toBe(
123
+ "keystroke-bytes",
124
+ );
125
+ });
126
+
127
+ it("stages title-card logos at their scenario-relative paths", async () => {
128
+ await writeFile(join(scenarioDir, "logo.png"), "logo-bytes");
129
+
130
+ const staged = await stageRenderAssets({
131
+ scenarioDir,
132
+ publicDir,
133
+ hasNarration: false,
134
+ titleCards: { intro: { title: "Acme", logoSrc: "./logo.png" } },
135
+ });
136
+
137
+ expect(staged).toBe(1);
138
+ expect(await readFile(join(publicDir, "logo.png"), "utf-8")).toBe("logo-bytes");
139
+ });
140
+
141
+ it("stages a logo shared by intro and outro exactly once", async () => {
142
+ await writeFile(join(scenarioDir, "logo.png"), "logo-bytes");
143
+
144
+ const staged = await stageRenderAssets({
145
+ scenarioDir,
146
+ publicDir,
147
+ hasNarration: false,
148
+ titleCards: {
149
+ intro: { title: "Acme", logoSrc: "./logo.png" },
150
+ outro: { title: "Bye", logoSrc: "./logo.png" },
151
+ },
152
+ });
153
+
154
+ expect(staged).toBe(1);
155
+ });
156
+
157
+ it("stages nothing (and reports zero) for a silent, card-less scenario", async () => {
158
+ const staged = await stageRenderAssets({
159
+ scenarioDir,
160
+ publicDir,
161
+ hasNarration: false,
162
+ });
163
+ expect(staged).toBe(0);
164
+ expect(await exists(publicDir)).toBe(false);
165
+ });
166
+
167
+ it("skips remote music URLs — they are referenced, not copied", async () => {
168
+ const staged = await stageRenderAssets({
169
+ scenarioDir,
170
+ publicDir,
171
+ hasNarration: false,
172
+ soundtrack: { musicSrc: "https://cdn.example.com/music.mp3" },
173
+ });
174
+ expect(staged).toBe(0);
175
+ });
176
+
177
+ it("fails loudly when the music file is missing (a silent render otherwise)", async () => {
178
+ await expect(
179
+ stageRenderAssets({
180
+ scenarioDir,
181
+ publicDir,
182
+ hasNarration: false,
183
+ soundtrack: { musicSrc: "./soundtrack/missing.mp3" },
184
+ }),
185
+ ).rejects.toThrow(/not found/);
186
+ });
187
+
188
+ it("fails loudly when a card's logo file is missing (a broken card otherwise)", async () => {
189
+ await expect(
190
+ stageRenderAssets({
191
+ scenarioDir,
192
+ publicDir,
193
+ hasNarration: false,
194
+ titleCards: { outro: { title: "Bye", logoSrc: "./missing.png" } },
195
+ }),
196
+ ).rejects.toThrow(/not found/);
197
+ });
198
+ });
199
+
200
+ describe("resolveMusicAsset", () => {
201
+ it("rejects paths escaping the scenario directory", async () => {
202
+ await expect(resolveMusicAsset(scenarioDir, "../outside.mp3")).rejects.toThrow(
203
+ /outside the scenario directory/,
204
+ );
205
+ });
206
+
207
+ it("returns null for remote URLs", async () => {
208
+ expect(await resolveMusicAsset(scenarioDir, "https://cdn.example.com/a.mp3")).toBeNull();
209
+ });
210
+ });
211
+
212
+ describe("resolveLogoAsset", () => {
213
+ it("rejects paths escaping the scenario directory", async () => {
214
+ await expect(
215
+ resolveLogoAsset(scenarioDir, "../outside.png", "titleCards.intro.logoSrc"),
216
+ ).rejects.toThrow(/outside the scenario directory/);
217
+ });
218
+
219
+ it("returns null for remote URLs", async () => {
220
+ expect(
221
+ await resolveLogoAsset(
222
+ scenarioDir,
223
+ "https://cdn.example.com/logo.png",
224
+ "titleCards.intro.logoSrc",
225
+ ),
226
+ ).toBeNull();
227
+ });
228
+
229
+ it("rejects extensions the deploy contract cannot serve — svg above all", async () => {
230
+ await writeFile(join(scenarioDir, "logo.svg"), "<svg/>");
231
+ await expect(
232
+ resolveLogoAsset(scenarioDir, "./logo.svg", "titleCards.intro.logoSrc"),
233
+ ).rejects.toThrow(/not a supported image format/);
234
+ });
235
+
236
+ it("accepts every allowlisted raster format", async () => {
237
+ for (const ext of ["png", "jpg", "jpeg", "gif", "webp", "avif"]) {
238
+ await writeFile(join(scenarioDir, `logo.${ext}`), "bytes");
239
+ const asset = await resolveLogoAsset(
240
+ scenarioDir,
241
+ `./logo.${ext}`,
242
+ "titleCards.intro.logoSrc",
243
+ );
244
+ expect(asset?.destRelPath).toBe(`logo.${ext}`);
245
+ }
246
+ });
247
+ });
248
+
249
+ describe("isRemoteUrl", () => {
250
+ it.each([
251
+ ["https://cdn.example.com/a.mp3", true],
252
+ ["http://x/a.mp3", true],
253
+ ["./soundtrack/music.mp3", false],
254
+ ["soundtrack/music.mp3", false],
255
+ ])("%s -> %s", (src, expected) => {
256
+ expect(isRemoteUrl(src)).toBe(expected);
257
+ });
258
+ });
259
+
260
+ describe("resolveSfxAssetPaths", () => {
261
+ it("resolves the real assets from a workspace context", async () => {
262
+ // This test file lives inside the repo, where @scenar/react is
263
+ // installed — the same resolution a consumer project gets.
264
+ const fromDir = dirname(fileURLToPath(import.meta.url));
265
+ const paths = resolveSfxAssetPaths(fromDir);
266
+ expect(await exists(paths.click)).toBe(true);
267
+ expect(await exists(paths.keystroke)).toBe(true);
268
+ });
269
+ });
@@ -6,7 +6,7 @@ export function registerNarrateCommand(program) {
6
6
  "Accepts a YAML file, a TypeScript steps file, or a directory\n" +
7
7
  "containing scenario subdirectories (each with a steps.ts).")
8
8
  .argument("<file-or-dir>", "path to scenario file (.yaml/.ts) or directory")
9
- .option("--tts <provider>", "TTS provider: echogarden, edge-tts, or openai", "echogarden")
9
+ .option("--tts <provider>", "TTS provider: echogarden, edge-tts, openai, or elevenlabs", "echogarden")
10
10
  .option("--out <dir>", "output directory for audio files")
11
11
  .option("--voice <voice>", "voice name (provider-specific)")
12
12
  .option("--base-url <path>", "URL path prefix for src fields in manifest")
@@ -1 +1 @@
1
- {"version":3,"file":"narrate.js","sourceRoot":"","sources":["../../../src/commands/narrate.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"narrate.js","sourceRoot":"","sources":["../../../src/commands/narrate.ts"],"names":[],"mappings":"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"}
@@ -17,7 +17,7 @@ export function registerNarrateCommand(program: Command): void {
17
17
  "containing scenario subdirectories (each with a steps.ts).",
18
18
  )
19
19
  .argument("<file-or-dir>", "path to scenario file (.yaml/.ts) or directory")
20
- .option("--tts <provider>", "TTS provider: echogarden, edge-tts, or openai", "echogarden")
20
+ .option("--tts <provider>", "TTS provider: echogarden, edge-tts, openai, or elevenlabs", "echogarden")
21
21
  .option("--out <dir>", "output directory for audio files")
22
22
  .option("--voice <voice>", "voice name (provider-specific)")
23
23
  .option("--base-url <path>", "URL path prefix for src fields in manifest")
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ export declare function registerPresenterCommand(program: Command): void;
3
+ //# sourceMappingURL=presenter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"presenter.d.ts","sourceRoot":"","sources":["../../../src/commands/presenter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAiBpC,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAyF/D"}
@@ -0,0 +1,89 @@
1
+ import { confirm } from "../util/confirm.js";
2
+ import { runPresenter } from "../presenter/run-presenter.js";
3
+ const ENGINES = new Set(["iii", "iv"]);
4
+ const RESOLUTIONS = new Set(["720p", "1080p"]);
5
+ export function registerPresenterCommand(program) {
6
+ program
7
+ .command("presenter")
8
+ .description("Generate presenter avatar clips from narration audio (HeyGen).\n\n" +
9
+ "For each step marked `presenter: true`, uploads the step's\n" +
10
+ "narration audio, generates a lip-synced avatar clip, and writes\n" +
11
+ "presenter/manifest.json beside the narration output. Run\n" +
12
+ "'scenar narrate' first — clips are derived from narration audio.\n\n" +
13
+ "This command calls a paid API: the batch estimate is printed and\n" +
14
+ "confirmed before any generation (skip the prompt with --yes).\n" +
15
+ "Requires HEYGEN_API_KEY.")
16
+ .argument("<file-or-dir>", "path to scenario file (.yaml/.ts) or directory")
17
+ .option("--avatar <id>", "HeyGen avatar id (fallback: HEYGEN_AVATAR_ID env)")
18
+ .option("--engine <engine>", "generation engine: iii or iv", "iii")
19
+ .option("--resolution <tier>", "output resolution: 720p or 1080p", "720p")
20
+ .option("--out <dir>", "output directory for clips")
21
+ .option("--base-url <path>", "URL path prefix for src fields in manifest")
22
+ .option("--yes", "skip the cost confirmation (CI / scripts)")
23
+ .action(async (fileOrDir, options) => {
24
+ try {
25
+ if (!ENGINES.has(options.engine)) {
26
+ throw new Error(`Unknown engine '${options.engine}'. Use iii or iv.`);
27
+ }
28
+ if (!RESOLUTIONS.has(options.resolution)) {
29
+ throw new Error(`Unknown resolution '${options.resolution}'. Use 720p or 1080p.`);
30
+ }
31
+ const result = await runPresenter({
32
+ target: fileOrDir,
33
+ avatar: options.avatar,
34
+ engine: options.engine,
35
+ resolution: options.resolution,
36
+ out: options.out,
37
+ baseUrl: options.baseUrl,
38
+ yes: options.yes,
39
+ confirmImpl: confirm,
40
+ onLog: (message) => process.stderr.write(`${message}\n`),
41
+ });
42
+ if (result.aborted) {
43
+ process.exitCode = 1;
44
+ return;
45
+ }
46
+ if (result.mode === "file") {
47
+ const only = result.scenarios[0];
48
+ if (only?.skipped) {
49
+ process.stderr.write("\x1b[33m!\x1b[0m No steps opt into the presenter (`presenter: true`). Nothing to generate.\n");
50
+ }
51
+ if (result.totalFailed > 0)
52
+ process.exitCode = 1;
53
+ return;
54
+ }
55
+ // Directory mode: print the aggregate summary.
56
+ if (result.scenarios.length === 0) {
57
+ process.stderr.write("\x1b[33m!\x1b[0m No scenario directories with steps.ts found.\n");
58
+ return;
59
+ }
60
+ const withPresenter = result.scenarios.filter((s) => !s.skipped).length;
61
+ process.stderr.write("\n");
62
+ process.stderr.write(`Scenarios with presenter steps: ${withPresenter}\n`);
63
+ process.stderr.write(`Clips generated: ${result.totalGenerated}\n`);
64
+ process.stderr.write(`Clips cached: ${result.totalCached}\n`);
65
+ process.stderr.write(`Scenarios skipped: ${result.totalSkipped}\n`);
66
+ if (result.totalFailed > 0 || result.errors.length > 0) {
67
+ if (result.errors.length > 0) {
68
+ process.stderr.write(`\n\x1b[31m${result.errors.length} scenario(s) failed:\x1b[0m\n`);
69
+ for (const { id, message } of result.errors) {
70
+ process.stderr.write(` - ${id}: ${message}\n`);
71
+ }
72
+ }
73
+ if (result.totalFailed > 0) {
74
+ process.stderr.write(`\n\x1b[31m${result.totalFailed} clip(s) failed to generate.\x1b[0m\n`);
75
+ }
76
+ process.exitCode = 1;
77
+ }
78
+ else {
79
+ process.stderr.write("\nDone\n");
80
+ }
81
+ }
82
+ catch (error) {
83
+ const msg = error instanceof Error ? error.message : String(error);
84
+ process.stderr.write(`\x1b[31mError:\x1b[0m ${msg}\n`);
85
+ process.exitCode = 1;
86
+ }
87
+ });
88
+ }
89
+ //# sourceMappingURL=presenter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"presenter.js","sourceRoot":"","sources":["../../../src/commands/presenter.ts"],"names":[],"mappings":"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"}