@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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stage-assets.js","sourceRoot":"","sources":["../../../src/render/stage-assets.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,130 @@
1
+ import { copyFile, mkdir, readdir } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import type { AuthoredSoundtrack, AuthoredTitleCards } from "../util/load-ts.js";
4
+ import {
5
+ SFX_DEST_PATHS,
6
+ type SfxAssetPaths,
7
+ resolveMusicAsset,
8
+ } from "../util/soundtrack-assets.js";
9
+ import { resolveLogoAsset } from "../util/scenario-assets.js";
10
+
11
+ export interface StageRenderAssetsInput {
12
+ /** Absolute path to the scenario directory. */
13
+ readonly scenarioDir: string;
14
+ /** Absolute path of the Remotion public dir to stage into (created). */
15
+ readonly publicDir: string;
16
+ /** Whether the scenario has a narration manifest (+ clips to stage). */
17
+ readonly hasNarration: boolean;
18
+ /** Whether the scenario has a presenter manifest (+ clips to stage). */
19
+ readonly hasPresenter?: boolean;
20
+ /** The scenario's authored soundtrack, if any. */
21
+ readonly soundtrack?: AuthoredSoundtrack;
22
+ /**
23
+ * Absolute locations of the built-in SFX assets — required when
24
+ * `soundtrack.sfx` is enabled (the caller resolves them from
25
+ * `@scenar/react` via `resolveSfxAssetPaths`).
26
+ */
27
+ readonly sfxPaths?: SfxAssetPaths;
28
+ /** The scenario's authored title cards, if any (logo assets to stage). */
29
+ readonly titleCards?: AuthoredTitleCards;
30
+ }
31
+
32
+ /**
33
+ * Stage every local asset a render needs into a Remotion public dir, so
34
+ * `staticFile()` resolution inside `ScenarioComposition` finds real files.
35
+ *
36
+ * The layout mirrors how each src is written:
37
+ * - Narration clips: manifest srcs are manifest-relative (`./step-N.mp3`),
38
+ * so the clips stage flat at the public root.
39
+ * - Presenter clips: same convention (`./step-N.mp4`), staged flat at the
40
+ * public root beside the narration clips — the extensions never collide.
41
+ * - Music: `musicSrc` is scenario-relative (`./soundtrack/music.mp3`), so
42
+ * the file stages at that same relative path. Remote URLs stage nothing.
43
+ * - Built-in SFX: staged at {@link SFX_DEST_PATHS}, the composition's
44
+ * default `sfxSrcs`.
45
+ * - Title-card logos: `logoSrc` is scenario-relative like music, staged
46
+ * at its relative path (intro and outro may share one file — it stages
47
+ * once). Remote URLs stage nothing.
48
+ *
49
+ * Returns the number of files staged. Zero means the render needs no
50
+ * local assets and the caller can skip the public dir entirely.
51
+ */
52
+ export async function stageRenderAssets(input: StageRenderAssetsInput): Promise<number> {
53
+ let staged = 0;
54
+
55
+ if (input.hasNarration) {
56
+ staged += await stageClipDirectory(input.scenarioDir, "narration", ".mp3", input.publicDir);
57
+ }
58
+
59
+ if (input.hasPresenter) {
60
+ staged += await stageClipDirectory(input.scenarioDir, "presenter", ".mp4", input.publicDir);
61
+ }
62
+
63
+ const musicSrc = input.soundtrack?.musicSrc;
64
+ if (musicSrc) {
65
+ const music = await resolveMusicAsset(input.scenarioDir, musicSrc);
66
+ if (music) {
67
+ const dest = join(input.publicDir, music.destRelPath);
68
+ await mkdir(dirname(dest), { recursive: true });
69
+ await copyFile(music.sourcePath, dest);
70
+ staged += 1;
71
+ }
72
+ }
73
+
74
+ if (input.soundtrack?.sfx && input.sfxPaths) {
75
+ for (const sound of ["click", "keystroke"] as const) {
76
+ const dest = join(input.publicDir, SFX_DEST_PATHS[sound]);
77
+ await mkdir(dirname(dest), { recursive: true });
78
+ await copyFile(input.sfxPaths[sound], dest);
79
+ staged += 1;
80
+ }
81
+ }
82
+
83
+ const stagedLogos = new Set<string>();
84
+ for (const side of ["intro", "outro"] as const) {
85
+ const logoSrc = input.titleCards?.[side]?.logoSrc;
86
+ if (!logoSrc) continue;
87
+ const logo = await resolveLogoAsset(
88
+ input.scenarioDir,
89
+ logoSrc,
90
+ `titleCards.${side}.logoSrc`,
91
+ );
92
+ if (!logo || stagedLogos.has(logo.destRelPath)) continue;
93
+ const dest = join(input.publicDir, logo.destRelPath);
94
+ await mkdir(dirname(dest), { recursive: true });
95
+ await copyFile(logo.sourcePath, dest);
96
+ stagedLogos.add(logo.destRelPath);
97
+ staged += 1;
98
+ }
99
+
100
+ return staged;
101
+ }
102
+
103
+ /**
104
+ * Copy every clip of one track (narration mp3s, presenter mp4s) flat into
105
+ * the public root — manifest srcs are manifest-relative (`./step-N.<ext>`),
106
+ * which `staticFile()` resolves at the root.
107
+ */
108
+ async function stageClipDirectory(
109
+ scenarioDir: string,
110
+ trackDir: string,
111
+ extension: string,
112
+ publicDir: string,
113
+ ): Promise<number> {
114
+ const sourceDir = join(scenarioDir, trackDir);
115
+ let entries;
116
+ try {
117
+ entries = await readdir(sourceDir, { withFileTypes: true });
118
+ } catch {
119
+ return 0;
120
+ }
121
+
122
+ let staged = 0;
123
+ await mkdir(publicDir, { recursive: true });
124
+ for (const entry of entries) {
125
+ if (!entry.isFile() || !entry.name.toLowerCase().endsWith(extension)) continue;
126
+ await copyFile(join(sourceDir, entry.name), join(publicDir, entry.name));
127
+ staged += 1;
128
+ }
129
+ return staged;
130
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"echogarden.d.ts","sourceRoot":"","sources":["../../../src/tts/echogarden.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAyB,MAAM,YAAY,CAAC;AAErE;;;GAGG;AACH,wBAAsB,qBAAqB,IAAI,OAAO,CAAC,OAAO,CAAC,CAO9D;AAED;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,IAAI,WAAW,CAmCtD"}
1
+ {"version":3,"file":"echogarden.d.ts","sourceRoot":"","sources":["../../../src/tts/echogarden.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAyB,MAAM,YAAY,CAAC;AAIrE;;;GAGG;AACH,wBAAsB,qBAAqB,IAAI,OAAO,CAAC,OAAO,CAAC,CAO9D;AAED;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,IAAI,WAAW,CAoCtD"}
@@ -1,3 +1,4 @@
1
+ const ENGINE = "vits";
1
2
  /**
2
3
  * Checks whether the `echogarden` package is available at runtime.
3
4
  * Returns `true` if it can be imported, `false` otherwise.
@@ -24,6 +25,7 @@ export async function isEchogardenAvailable() {
24
25
  export function createEchogardenProvider() {
25
26
  return {
26
27
  name: "echogarden",
28
+ fingerprint: `echogarden/${ENGINE}`,
27
29
  async synthesize(text, options) {
28
30
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
29
31
  let echogarden;
@@ -33,10 +35,10 @@ export function createEchogardenProvider() {
33
35
  catch {
34
36
  throw new Error("Echogarden is not installed. Install it with:\n\n" +
35
37
  " pnpm add echogarden\n\n" +
36
- "Or use a different TTS provider: --tts edge-tts, --tts openai");
38
+ "Or use a different TTS provider: --tts edge-tts, --tts openai, --tts elevenlabs");
37
39
  }
38
40
  const result = await echogarden.synthesize(text, {
39
- engine: "vits",
41
+ engine: ENGINE,
40
42
  voice: options.voice,
41
43
  });
42
44
  const rawAudio = result.audio;
@@ -1 +1 @@
1
- {"version":3,"file":"echogarden.js","sourceRoot":"","sources":["../../../src/tts/echogarden.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB;IACzC,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,YAAY,CAAC,CAAC;QAC3B,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,wBAAwB;IACtC,OAAO;QACL,IAAI,EAAE,YAAY;QAElB,KAAK,CAAC,UAAU,CAAC,IAAY,EAAE,OAAmB;YAChD,8DAA8D;YAC9D,IAAI,UAAe,CAAC;YACpB,IAAI,CAAC;gBACH,UAAU,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,CAAC;YAC1C,CAAC;YAAC,MAAM,CAAC;gBACP,MAAM,IAAI,KAAK,CACb,mDAAmD;oBACnD,2BAA2B;oBAC3B,+DAA+D,CAChE,CAAC;YACJ,CAAC;YAED,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,UAAU,CAAC,IAAI,EAAE;gBAC/C,MAAM,EAAE,MAAM;gBACd,KAAK,EAAE,OAAO,CAAC,KAAK;aACrB,CAAC,CAAC;YAEH,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC;YAC9B,MAAM,UAAU,GAAW,QAAQ,CAAC,UAAU,IAAI,KAAK,CAAC;YACxD,MAAM,OAAO,GAAW,QAAQ,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,IAAI,CAAC,CAAC;YACjE,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,OAAO,GAAG,UAAU,CAAC,GAAG,IAAI,CAAC,CAAC;YAE7D,MAAM,QAAQ,GAAe,MAAM,UAAU,CAAC,mBAAmB,CAAC,QAAQ,CAAC,CAAC;YAE5E,OAAO;gBACL,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC;gBAC5B,UAAU;aACX,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"echogarden.js","sourceRoot":"","sources":["../../../src/tts/echogarden.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,GAAG,MAAM,CAAC;AAEtB;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB;IACzC,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,YAAY,CAAC,CAAC;QAC3B,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,wBAAwB;IACtC,OAAO;QACL,IAAI,EAAE,YAAY;QAClB,WAAW,EAAE,cAAc,MAAM,EAAE;QAEnC,KAAK,CAAC,UAAU,CAAC,IAAY,EAAE,OAAmB;YAChD,8DAA8D;YAC9D,IAAI,UAAe,CAAC;YACpB,IAAI,CAAC;gBACH,UAAU,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,CAAC;YAC1C,CAAC;YAAC,MAAM,CAAC;gBACP,MAAM,IAAI,KAAK,CACb,mDAAmD;oBACnD,2BAA2B;oBAC3B,iFAAiF,CAClF,CAAC;YACJ,CAAC;YAED,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,UAAU,CAAC,IAAI,EAAE;gBAC/C,MAAM,EAAE,MAAM;gBACd,KAAK,EAAE,OAAO,CAAC,KAAK;aACrB,CAAC,CAAC;YAEH,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC;YAC9B,MAAM,UAAU,GAAW,QAAQ,CAAC,UAAU,IAAI,KAAK,CAAC;YACxD,MAAM,OAAO,GAAW,QAAQ,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,IAAI,CAAC,CAAC;YACjE,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,OAAO,GAAG,UAAU,CAAC,GAAG,IAAI,CAAC,CAAC;YAE7D,MAAM,QAAQ,GAAe,MAAM,UAAU,CAAC,mBAAmB,CAAC,QAAQ,CAAC,CAAC;YAE5E,OAAO;gBACL,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC;gBAC5B,UAAU;aACX,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -1,5 +1,7 @@
1
1
  import type { TtsProvider, TtsOptions, TtsResult } from "./types.js";
2
2
 
3
+ const ENGINE = "vits";
4
+
3
5
  /**
4
6
  * Checks whether the `echogarden` package is available at runtime.
5
7
  * Returns `true` if it can be imported, `false` otherwise.
@@ -26,6 +28,7 @@ export async function isEchogardenAvailable(): Promise<boolean> {
26
28
  export function createEchogardenProvider(): TtsProvider {
27
29
  return {
28
30
  name: "echogarden",
31
+ fingerprint: `echogarden/${ENGINE}`,
29
32
 
30
33
  async synthesize(text: string, options: TtsOptions): Promise<TtsResult> {
31
34
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -36,12 +39,12 @@ export function createEchogardenProvider(): TtsProvider {
36
39
  throw new Error(
37
40
  "Echogarden is not installed. Install it with:\n\n" +
38
41
  " pnpm add echogarden\n\n" +
39
- "Or use a different TTS provider: --tts edge-tts, --tts openai",
42
+ "Or use a different TTS provider: --tts edge-tts, --tts openai, --tts elevenlabs",
40
43
  );
41
44
  }
42
45
 
43
46
  const result = await echogarden.synthesize(text, {
44
- engine: "vits",
47
+ engine: ENGINE,
45
48
  voice: options.voice,
46
49
  });
47
50
 
@@ -1 +1 @@
1
- {"version":3,"file":"edge-tts.d.ts","sourceRoot":"","sources":["../../../src/tts/edge-tts.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAyB,MAAM,YAAY,CAAC;AAUrE;;;GAGG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,OAAO,CAAC,CAO3D;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,qBAAqB,IAAI,WAAW,CAsCnD"}
1
+ {"version":3,"file":"edge-tts.d.ts","sourceRoot":"","sources":["../../../src/tts/edge-tts.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAyB,MAAM,YAAY,CAAC;AAUrE;;;GAGG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,OAAO,CAAC,CAO3D;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,qBAAqB,IAAI,WAAW,CAuCnD"}
@@ -39,6 +39,7 @@ export async function isEdgeTtsAvailable() {
39
39
  export function createEdgeTtsProvider() {
40
40
  return {
41
41
  name: "edge-tts",
42
+ fingerprint: `edge-tts/${DEFAULT_VOICE}`,
42
43
  async synthesize(text, options) {
43
44
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
44
45
  let IsomorphicEdgeTTS;
@@ -49,7 +50,7 @@ export function createEdgeTtsProvider() {
49
50
  catch {
50
51
  throw new Error("edge-tts-universal is not installed. Install it with:\n\n" +
51
52
  " pnpm add edge-tts-universal\n\n" +
52
- "Or use a different TTS provider: --tts echogarden, --tts openai");
53
+ "Or use a different TTS provider: --tts echogarden, --tts openai, --tts elevenlabs");
53
54
  }
54
55
  const voice = options.voice ?? DEFAULT_VOICE;
55
56
  const tts = new IsomorphicEdgeTTS(text, voice);
@@ -1 +1 @@
1
- {"version":3,"file":"edge-tts.js","sourceRoot":"","sources":["../../../src/tts/edge-tts.ts"],"names":[],"mappings":"AAEA,MAAM,aAAa,GAAG,gCAAgC,CAAC;AAEvD;;;GAGG;AACH,MAAM,qBAAqB,GAAG,EAAE,CAAC;AAEjC;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB;IACtC,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,oBAAoB,CAAC,CAAC;QACnC,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,qBAAqB;IACnC,OAAO;QACL,IAAI,EAAE,UAAU;QAEhB,KAAK,CAAC,UAAU,CAAC,IAAY,EAAE,OAAmB;YAChD,8DAA8D;YAC9D,IAAI,iBAAsB,CAAC;YAC3B,IAAI,CAAC;gBACH,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,oBAAoB,CAAC,CAAC;gBAC/C,iBAAiB,GAAG,GAAG,CAAC,iBAAiB,CAAC;YAC5C,CAAC;YAAC,MAAM,CAAC;gBACP,MAAM,IAAI,KAAK,CACb,2DAA2D;oBACzD,mCAAmC;oBACnC,iEAAiE,CACpE,CAAC;YACJ,CAAC;YAED,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,aAAa,CAAC;YAC7C,MAAM,GAAG,GAAG,IAAI,iBAAiB,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;YAC/C,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,UAAU,EAAE,CAAC;YAEtC,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,MAAM,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC;YAElE,IAAI,UAAU,GAAG,CAAC,CAAC;YACnB,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC/B,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;gBACzD,kDAAkD;gBAClD,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,MAAM,CAAC,CAAC;YACjE,CAAC;YAED,IAAI,UAAU,KAAK,CAAC,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC/C,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,WAAW,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,qBAAqB,CAAC,CAAC;YAC3E,CAAC;YAED,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,CAAC;QAC5C,CAAC;KACF,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"edge-tts.js","sourceRoot":"","sources":["../../../src/tts/edge-tts.ts"],"names":[],"mappings":"AAEA,MAAM,aAAa,GAAG,gCAAgC,CAAC;AAEvD;;;GAGG;AACH,MAAM,qBAAqB,GAAG,EAAE,CAAC;AAEjC;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB;IACtC,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,oBAAoB,CAAC,CAAC;QACnC,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,qBAAqB;IACnC,OAAO;QACL,IAAI,EAAE,UAAU;QAChB,WAAW,EAAE,YAAY,aAAa,EAAE;QAExC,KAAK,CAAC,UAAU,CAAC,IAAY,EAAE,OAAmB;YAChD,8DAA8D;YAC9D,IAAI,iBAAsB,CAAC;YAC3B,IAAI,CAAC;gBACH,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,oBAAoB,CAAC,CAAC;gBAC/C,iBAAiB,GAAG,GAAG,CAAC,iBAAiB,CAAC;YAC5C,CAAC;YAAC,MAAM,CAAC;gBACP,MAAM,IAAI,KAAK,CACb,2DAA2D;oBACzD,mCAAmC;oBACnC,mFAAmF,CACtF,CAAC;YACJ,CAAC;YAED,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,aAAa,CAAC;YAC7C,MAAM,GAAG,GAAG,IAAI,iBAAiB,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;YAC/C,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,UAAU,EAAE,CAAC;YAEtC,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,MAAM,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC;YAElE,IAAI,UAAU,GAAG,CAAC,CAAC;YACnB,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC/B,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;gBACzD,kDAAkD;gBAClD,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,MAAM,CAAC,CAAC;YACjE,CAAC;YAED,IAAI,UAAU,KAAK,CAAC,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC/C,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,WAAW,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,qBAAqB,CAAC,CAAC;YAC3E,CAAC;YAED,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,CAAC;QAC5C,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -43,6 +43,7 @@ export async function isEdgeTtsAvailable(): Promise<boolean> {
43
43
  export function createEdgeTtsProvider(): TtsProvider {
44
44
  return {
45
45
  name: "edge-tts",
46
+ fingerprint: `edge-tts/${DEFAULT_VOICE}`,
46
47
 
47
48
  async synthesize(text: string, options: TtsOptions): Promise<TtsResult> {
48
49
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -54,7 +55,7 @@ export function createEdgeTtsProvider(): TtsProvider {
54
55
  throw new Error(
55
56
  "edge-tts-universal is not installed. Install it with:\n\n" +
56
57
  " pnpm add edge-tts-universal\n\n" +
57
- "Or use a different TTS provider: --tts echogarden, --tts openai",
58
+ "Or use a different TTS provider: --tts echogarden, --tts openai, --tts elevenlabs",
58
59
  );
59
60
  }
60
61
 
@@ -0,0 +1,15 @@
1
+ import type { TtsProvider } from "./types.js";
2
+ /**
3
+ * TTS provider backed by ElevenLabs' text-to-speech API.
4
+ * Requires the `ELEVENLABS_API_KEY` environment variable.
5
+ *
6
+ * Uses the `with-timestamps` endpoint variant so the audio duration comes
7
+ * from the API's character alignment data (exact) rather than a bitrate
8
+ * estimate. Duration drives step timing — `atPercent` interactions are
9
+ * measured against it — so precision here keeps narration and motion in sync.
10
+ *
11
+ * Model defaults to Multilingual v2 (~$0.10 per finished minute) and can be
12
+ * overridden with the `ELEVENLABS_MODEL_ID` environment variable.
13
+ */
14
+ export declare function createElevenLabsProvider(): TtsProvider;
15
+ //# sourceMappingURL=elevenlabs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"elevenlabs.d.ts","sourceRoot":"","sources":["../../../src/tts/elevenlabs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAyB,MAAM,YAAY,CAAC;AA6BrE;;;;;;;;;;;GAWG;AACH,wBAAgB,wBAAwB,IAAI,WAAW,CAsDtD"}
@@ -0,0 +1,73 @@
1
+ const ELEVENLABS_TTS_URL = "https://api.elevenlabs.io/v1/text-to-speech";
2
+ const DEFAULT_MODEL = "eleven_multilingual_v2";
3
+ /** Rachel — an ElevenLabs stock voice available to every account. */
4
+ const DEFAULT_VOICE_ID = "21m00Tcm4TlvDq8ikWAM";
5
+ /**
6
+ * Requested output format: MP3, 44.1 kHz, 128 kbps. Available on the
7
+ * ElevenLabs free tier (192 kbps requires Creator+). The bitrate doubles
8
+ * as the fallback duration estimate when alignment data is unavailable.
9
+ */
10
+ const OUTPUT_FORMAT = "mp3_44100_128";
11
+ const OUTPUT_BITRATE_KBPS = 128;
12
+ /**
13
+ * TTS provider backed by ElevenLabs' text-to-speech API.
14
+ * Requires the `ELEVENLABS_API_KEY` environment variable.
15
+ *
16
+ * Uses the `with-timestamps` endpoint variant so the audio duration comes
17
+ * from the API's character alignment data (exact) rather than a bitrate
18
+ * estimate. Duration drives step timing — `atPercent` interactions are
19
+ * measured against it — so precision here keeps narration and motion in sync.
20
+ *
21
+ * Model defaults to Multilingual v2 (~$0.10 per finished minute) and can be
22
+ * overridden with the `ELEVENLABS_MODEL_ID` environment variable.
23
+ */
24
+ export function createElevenLabsProvider() {
25
+ const model = process.env["ELEVENLABS_MODEL_ID"] ?? DEFAULT_MODEL;
26
+ return {
27
+ name: "elevenlabs",
28
+ fingerprint: `elevenlabs/${model}/${DEFAULT_VOICE_ID}`,
29
+ async synthesize(text, options) {
30
+ const apiKey = process.env["ELEVENLABS_API_KEY"];
31
+ if (!apiKey) {
32
+ throw new Error("ELEVENLABS_API_KEY environment variable is not set.\n\n" +
33
+ "Set it before running narrate:\n" +
34
+ " export ELEVENLABS_API_KEY=...\n\n" +
35
+ "Or use a different TTS provider: --tts openai");
36
+ }
37
+ const voiceId = options.voice ?? DEFAULT_VOICE_ID;
38
+ const url = `${ELEVENLABS_TTS_URL}/${voiceId}/with-timestamps` +
39
+ `?output_format=${OUTPUT_FORMAT}`;
40
+ const response = await fetch(url, {
41
+ method: "POST",
42
+ headers: {
43
+ "xi-api-key": apiKey,
44
+ "Content-Type": "application/json",
45
+ },
46
+ body: JSON.stringify({
47
+ text,
48
+ model_id: model,
49
+ }),
50
+ });
51
+ if (!response.ok) {
52
+ const body = await response.text();
53
+ throw new Error(`ElevenLabs TTS API error (${response.status}): ${body}`);
54
+ }
55
+ const payload = (await response.json());
56
+ const audio = Buffer.from(payload.audio_base64, "base64");
57
+ const endTimes = payload.alignment?.character_end_times_seconds;
58
+ const durationMs = endTimes && endTimes.length > 0
59
+ ? Math.ceil(endTimes[endTimes.length - 1] * 1000)
60
+ : estimateMp3DurationMs(audio);
61
+ return { audio, durationMs };
62
+ },
63
+ };
64
+ }
65
+ /**
66
+ * Rough MP3 duration estimate from file size and the requested bitrate.
67
+ * Only used as a fallback when the API returns no alignment data.
68
+ */
69
+ function estimateMp3DurationMs(mp3) {
70
+ const bytes = mp3.byteLength;
71
+ return Math.round((bytes * 8) / OUTPUT_BITRATE_KBPS);
72
+ }
73
+ //# sourceMappingURL=elevenlabs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"elevenlabs.js","sourceRoot":"","sources":["../../../src/tts/elevenlabs.ts"],"names":[],"mappings":"AAEA,MAAM,kBAAkB,GAAG,6CAA6C,CAAC;AACzE,MAAM,aAAa,GAAG,wBAAwB,CAAC;AAE/C,qEAAqE;AACrE,MAAM,gBAAgB,GAAG,sBAAsB,CAAC;AAEhD;;;;GAIG;AACH,MAAM,aAAa,GAAG,eAAe,CAAC;AACtC,MAAM,mBAAmB,GAAG,GAAG,CAAC;AAehC;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,wBAAwB;IACtC,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,qBAAqB,CAAC,IAAI,aAAa,CAAC;IAElE,OAAO;QACL,IAAI,EAAE,YAAY;QAClB,WAAW,EAAE,cAAc,KAAK,IAAI,gBAAgB,EAAE;QAEtD,KAAK,CAAC,UAAU,CAAC,IAAY,EAAE,OAAmB;YAChD,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC;YACjD,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,MAAM,IAAI,KAAK,CACb,yDAAyD;oBACzD,kCAAkC;oBAClC,qCAAqC;oBACrC,+CAA+C,CAChD,CAAC;YACJ,CAAC;YAED,MAAM,OAAO,GAAG,OAAO,CAAC,KAAK,IAAI,gBAAgB,CAAC;YAClD,MAAM,GAAG,GACP,GAAG,kBAAkB,IAAI,OAAO,kBAAkB;gBAClD,kBAAkB,aAAa,EAAE,CAAC;YAEpC,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;gBAChC,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE;oBACP,YAAY,EAAE,MAAM;oBACpB,cAAc,EAAE,kBAAkB;iBACnC;gBACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;oBACnB,IAAI;oBACJ,QAAQ,EAAE,KAAK;iBAChB,CAAC;aACH,CAAC,CAAC;YAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;gBACjB,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;gBACnC,MAAM,IAAI,KAAK,CACb,6BAA6B,QAAQ,CAAC,MAAM,MAAM,IAAI,EAAE,CACzD,CAAC;YACJ,CAAC;YAED,MAAM,OAAO,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAuB,CAAC;YAC9D,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAC;YAE1D,MAAM,QAAQ,GAAG,OAAO,CAAC,SAAS,EAAE,2BAA2B,CAAC;YAChE,MAAM,UAAU,GACd,QAAQ,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC;gBAC7B,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAE,GAAG,IAAI,CAAC;gBAClD,CAAC,CAAC,qBAAqB,CAAC,KAAK,CAAC,CAAC;YAEnC,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC;QAC/B,CAAC;KACF,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,SAAS,qBAAqB,CAAC,GAAW;IACxC,MAAM,KAAK,GAAG,GAAG,CAAC,UAAU,CAAC;IAC7B,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,mBAAmB,CAAC,CAAC;AACvD,CAAC"}
@@ -0,0 +1,105 @@
1
+ import type { TtsProvider, TtsOptions, TtsResult } from "./types.js";
2
+
3
+ const ELEVENLABS_TTS_URL = "https://api.elevenlabs.io/v1/text-to-speech";
4
+ const DEFAULT_MODEL = "eleven_multilingual_v2";
5
+
6
+ /** Rachel — an ElevenLabs stock voice available to every account. */
7
+ const DEFAULT_VOICE_ID = "21m00Tcm4TlvDq8ikWAM";
8
+
9
+ /**
10
+ * Requested output format: MP3, 44.1 kHz, 128 kbps. Available on the
11
+ * ElevenLabs free tier (192 kbps requires Creator+). The bitrate doubles
12
+ * as the fallback duration estimate when alignment data is unavailable.
13
+ */
14
+ const OUTPUT_FORMAT = "mp3_44100_128";
15
+ const OUTPUT_BITRATE_KBPS = 128;
16
+
17
+ /**
18
+ * Response shape of the `with-timestamps` endpoint variant: audio arrives
19
+ * base64-encoded alongside character-level timing alignment.
20
+ */
21
+ interface TimestampsResponse {
22
+ audio_base64: string;
23
+ alignment: {
24
+ characters: string[];
25
+ character_start_times_seconds: number[];
26
+ character_end_times_seconds: number[];
27
+ } | null;
28
+ }
29
+
30
+ /**
31
+ * TTS provider backed by ElevenLabs' text-to-speech API.
32
+ * Requires the `ELEVENLABS_API_KEY` environment variable.
33
+ *
34
+ * Uses the `with-timestamps` endpoint variant so the audio duration comes
35
+ * from the API's character alignment data (exact) rather than a bitrate
36
+ * estimate. Duration drives step timing — `atPercent` interactions are
37
+ * measured against it — so precision here keeps narration and motion in sync.
38
+ *
39
+ * Model defaults to Multilingual v2 (~$0.10 per finished minute) and can be
40
+ * overridden with the `ELEVENLABS_MODEL_ID` environment variable.
41
+ */
42
+ export function createElevenLabsProvider(): TtsProvider {
43
+ const model = process.env["ELEVENLABS_MODEL_ID"] ?? DEFAULT_MODEL;
44
+
45
+ return {
46
+ name: "elevenlabs",
47
+ fingerprint: `elevenlabs/${model}/${DEFAULT_VOICE_ID}`,
48
+
49
+ async synthesize(text: string, options: TtsOptions): Promise<TtsResult> {
50
+ const apiKey = process.env["ELEVENLABS_API_KEY"];
51
+ if (!apiKey) {
52
+ throw new Error(
53
+ "ELEVENLABS_API_KEY environment variable is not set.\n\n" +
54
+ "Set it before running narrate:\n" +
55
+ " export ELEVENLABS_API_KEY=...\n\n" +
56
+ "Or use a different TTS provider: --tts openai",
57
+ );
58
+ }
59
+
60
+ const voiceId = options.voice ?? DEFAULT_VOICE_ID;
61
+ const url =
62
+ `${ELEVENLABS_TTS_URL}/${voiceId}/with-timestamps` +
63
+ `?output_format=${OUTPUT_FORMAT}`;
64
+
65
+ const response = await fetch(url, {
66
+ method: "POST",
67
+ headers: {
68
+ "xi-api-key": apiKey,
69
+ "Content-Type": "application/json",
70
+ },
71
+ body: JSON.stringify({
72
+ text,
73
+ model_id: model,
74
+ }),
75
+ });
76
+
77
+ if (!response.ok) {
78
+ const body = await response.text();
79
+ throw new Error(
80
+ `ElevenLabs TTS API error (${response.status}): ${body}`,
81
+ );
82
+ }
83
+
84
+ const payload = (await response.json()) as TimestampsResponse;
85
+ const audio = Buffer.from(payload.audio_base64, "base64");
86
+
87
+ const endTimes = payload.alignment?.character_end_times_seconds;
88
+ const durationMs =
89
+ endTimes && endTimes.length > 0
90
+ ? Math.ceil(endTimes[endTimes.length - 1]! * 1000)
91
+ : estimateMp3DurationMs(audio);
92
+
93
+ return { audio, durationMs };
94
+ },
95
+ };
96
+ }
97
+
98
+ /**
99
+ * Rough MP3 duration estimate from file size and the requested bitrate.
100
+ * Only used as a fallback when the API returns no alignment data.
101
+ */
102
+ function estimateMp3DurationMs(mp3: Buffer): number {
103
+ const bytes = mp3.byteLength;
104
+ return Math.round((bytes * 8) / OUTPUT_BITRATE_KBPS);
105
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"openai.d.ts","sourceRoot":"","sources":["../../../src/tts/openai.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAyB,MAAM,YAAY,CAAC;AAMrE;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,WAAW,CA8ClD"}
1
+ {"version":3,"file":"openai.d.ts","sourceRoot":"","sources":["../../../src/tts/openai.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAyB,MAAM,YAAY,CAAC;AAMrE;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,WAAW,CA+ClD"}
package/src/tts/openai.js CHANGED
@@ -8,13 +8,14 @@ const DEFAULT_VOICE = "alloy";
8
8
  export function createOpenAIProvider() {
9
9
  return {
10
10
  name: "openai",
11
+ fingerprint: `openai/${DEFAULT_MODEL}/${DEFAULT_VOICE}`,
11
12
  async synthesize(text, options) {
12
13
  const apiKey = process.env["OPENAI_API_KEY"];
13
14
  if (!apiKey) {
14
15
  throw new Error("OPENAI_API_KEY environment variable is not set.\n\n" +
15
16
  "Set it before running narrate:\n" +
16
17
  " export OPENAI_API_KEY=sk-...\n\n" +
17
- "Or use a different TTS provider: --tts echogarden");
18
+ "Or use a different TTS provider: --tts echogarden, --tts elevenlabs");
18
19
  }
19
20
  const voice = options.voice ?? DEFAULT_VOICE;
20
21
  const response = await fetch(OPENAI_TTS_URL, {
@@ -1 +1 @@
1
- {"version":3,"file":"openai.js","sourceRoot":"","sources":["../../../src/tts/openai.ts"],"names":[],"mappings":"AAEA,MAAM,cAAc,GAAG,wCAAwC,CAAC;AAChE,MAAM,aAAa,GAAG,OAAO,CAAC;AAC9B,MAAM,aAAa,GAAG,OAAO,CAAC;AAE9B;;;GAGG;AACH,MAAM,UAAU,oBAAoB;IAClC,OAAO;QACL,IAAI,EAAE,QAAQ;QAEd,KAAK,CAAC,UAAU,CAAC,IAAY,EAAE,OAAmB;YAChD,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;YAC7C,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,MAAM,IAAI,KAAK,CACb,qDAAqD;oBACrD,kCAAkC;oBAClC,oCAAoC;oBACpC,mDAAmD,CACpD,CAAC;YACJ,CAAC;YAED,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,aAAa,CAAC;YAE7C,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,cAAc,EAAE;gBAC3C,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE;oBACP,eAAe,EAAE,UAAU,MAAM,EAAE;oBACnC,cAAc,EAAE,kBAAkB;iBACnC;gBACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;oBACnB,KAAK,EAAE,aAAa;oBACpB,KAAK,EAAE,IAAI;oBACX,KAAK;oBACL,eAAe,EAAE,KAAK;iBACvB,CAAC;aACH,CAAC,CAAC;YAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;gBACjB,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;gBACnC,MAAM,IAAI,KAAK,CACb,yBAAyB,QAAQ,CAAC,MAAM,MAAM,IAAI,EAAE,CACrD,CAAC;YACJ,CAAC;YAED,MAAM,WAAW,GAAG,MAAM,QAAQ,CAAC,WAAW,EAAE,CAAC;YACjD,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;YAEvC,MAAM,UAAU,GAAG,qBAAqB,CAAC,KAAK,CAAC,CAAC;YAEhD,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC;QAC/B,CAAC;KACF,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,SAAS,qBAAqB,CAAC,GAAW;IACxC,MAAM,oBAAoB,GAAG,EAAE,CAAC;IAChC,MAAM,KAAK,GAAG,GAAG,CAAC,UAAU,CAAC;IAC7B,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,oBAAoB,CAAC,CAAC;AACxD,CAAC"}
1
+ {"version":3,"file":"openai.js","sourceRoot":"","sources":["../../../src/tts/openai.ts"],"names":[],"mappings":"AAEA,MAAM,cAAc,GAAG,wCAAwC,CAAC;AAChE,MAAM,aAAa,GAAG,OAAO,CAAC;AAC9B,MAAM,aAAa,GAAG,OAAO,CAAC;AAE9B;;;GAGG;AACH,MAAM,UAAU,oBAAoB;IAClC,OAAO;QACL,IAAI,EAAE,QAAQ;QACd,WAAW,EAAE,UAAU,aAAa,IAAI,aAAa,EAAE;QAEvD,KAAK,CAAC,UAAU,CAAC,IAAY,EAAE,OAAmB;YAChD,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;YAC7C,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,MAAM,IAAI,KAAK,CACb,qDAAqD;oBACrD,kCAAkC;oBAClC,oCAAoC;oBACpC,qEAAqE,CACtE,CAAC;YACJ,CAAC;YAED,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,aAAa,CAAC;YAE7C,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,cAAc,EAAE;gBAC3C,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE;oBACP,eAAe,EAAE,UAAU,MAAM,EAAE;oBACnC,cAAc,EAAE,kBAAkB;iBACnC;gBACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;oBACnB,KAAK,EAAE,aAAa;oBACpB,KAAK,EAAE,IAAI;oBACX,KAAK;oBACL,eAAe,EAAE,KAAK;iBACvB,CAAC;aACH,CAAC,CAAC;YAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;gBACjB,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;gBACnC,MAAM,IAAI,KAAK,CACb,yBAAyB,QAAQ,CAAC,MAAM,MAAM,IAAI,EAAE,CACrD,CAAC;YACJ,CAAC;YAED,MAAM,WAAW,GAAG,MAAM,QAAQ,CAAC,WAAW,EAAE,CAAC;YACjD,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;YAEvC,MAAM,UAAU,GAAG,qBAAqB,CAAC,KAAK,CAAC,CAAC;YAEhD,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC;QAC/B,CAAC;KACF,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,SAAS,qBAAqB,CAAC,GAAW;IACxC,MAAM,oBAAoB,GAAG,EAAE,CAAC;IAChC,MAAM,KAAK,GAAG,GAAG,CAAC,UAAU,CAAC;IAC7B,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,oBAAoB,CAAC,CAAC;AACxD,CAAC"}
package/src/tts/openai.ts CHANGED
@@ -11,6 +11,7 @@ const DEFAULT_VOICE = "alloy";
11
11
  export function createOpenAIProvider(): TtsProvider {
12
12
  return {
13
13
  name: "openai",
14
+ fingerprint: `openai/${DEFAULT_MODEL}/${DEFAULT_VOICE}`,
14
15
 
15
16
  async synthesize(text: string, options: TtsOptions): Promise<TtsResult> {
16
17
  const apiKey = process.env["OPENAI_API_KEY"];
@@ -19,7 +20,7 @@ export function createOpenAIProvider(): TtsProvider {
19
20
  "OPENAI_API_KEY environment variable is not set.\n\n" +
20
21
  "Set it before running narrate:\n" +
21
22
  " export OPENAI_API_KEY=sk-...\n\n" +
22
- "Or use a different TTS provider: --tts echogarden",
23
+ "Or use a different TTS provider: --tts echogarden, --tts elevenlabs",
23
24
  );
24
25
  }
25
26
 
@@ -1,5 +1,5 @@
1
1
  import type { TtsProvider } from "./types.js";
2
- export type TtsProviderName = "echogarden" | "edge-tts" | "openai";
2
+ export type TtsProviderName = "echogarden" | "edge-tts" | "openai" | "elevenlabs";
3
3
  /**
4
4
  * Resolve a TTS provider by name.
5
5
  *
@@ -1 +1 @@
1
- {"version":3,"file":"resolve-provider.d.ts","sourceRoot":"","sources":["../../../src/tts/resolve-provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAK9C,MAAM,MAAM,eAAe,GAAG,YAAY,GAAG,UAAU,GAAG,QAAQ,CAAC;AAQnE;;;;;;GAMG;AACH,wBAAsB,eAAe,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAwChF"}
1
+ {"version":3,"file":"resolve-provider.d.ts","sourceRoot":"","sources":["../../../src/tts/resolve-provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAM9C,MAAM,MAAM,eAAe,GAAG,YAAY,GAAG,UAAU,GAAG,QAAQ,GAAG,YAAY,CAAC;AASlF;;;;;;GAMG;AACH,wBAAsB,eAAe,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CA8ChF"}
@@ -1,10 +1,12 @@
1
1
  import { createEchogardenProvider, isEchogardenAvailable } from "./echogarden.js";
2
2
  import { createEdgeTtsProvider, isEdgeTtsAvailable } from "./edge-tts.js";
3
+ import { createElevenLabsProvider } from "./elevenlabs.js";
3
4
  import { createOpenAIProvider } from "./openai.js";
4
5
  const KNOWN_PROVIDERS = new Set([
5
6
  "echogarden",
6
7
  "edge-tts",
7
8
  "openai",
9
+ "elevenlabs",
8
10
  ]);
9
11
  /**
10
12
  * Resolve a TTS provider by name.
@@ -21,6 +23,9 @@ export async function resolveProvider(providerName) {
21
23
  if (providerName === "openai") {
22
24
  return createOpenAIProvider();
23
25
  }
26
+ if (providerName === "elevenlabs") {
27
+ return createElevenLabsProvider();
28
+ }
24
29
  if (providerName === "edge-tts") {
25
30
  const available = await isEdgeTtsAvailable();
26
31
  if (!available) {
@@ -28,7 +33,8 @@ export async function resolveProvider(providerName) {
28
33
  " pnpm add edge-tts-universal\n\n" +
29
34
  "Or use a different provider:\n\n" +
30
35
  " scenar narrate <file> --tts echogarden (offline, pnpm add echogarden)\n" +
31
- " scenar narrate <file> --tts openai (requires OPENAI_API_KEY)");
36
+ " scenar narrate <file> --tts openai (requires OPENAI_API_KEY)\n" +
37
+ " scenar narrate <file> --tts elevenlabs (requires ELEVENLABS_API_KEY)");
32
38
  }
33
39
  return createEdgeTtsProvider();
34
40
  }
@@ -39,8 +45,9 @@ export async function resolveProvider(providerName) {
39
45
  "Or use free online TTS (requires network):\n\n" +
40
46
  " scenar narrate <file> --tts edge-tts\n" +
41
47
  " pnpm add edge-tts-universal\n\n" +
42
- "Or use OpenAI TTS (requires API key):\n\n" +
43
- " scenar narrate <file> --tts openai");
48
+ "Or use an API-key TTS provider:\n\n" +
49
+ " scenar narrate <file> --tts openai (requires OPENAI_API_KEY)\n" +
50
+ " scenar narrate <file> --tts elevenlabs (requires ELEVENLABS_API_KEY)");
44
51
  }
45
52
  return createEchogardenProvider();
46
53
  }
@@ -1 +1 @@
1
- {"version":3,"file":"resolve-provider.js","sourceRoot":"","sources":["../../../src/tts/resolve-provider.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,wBAAwB,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAClF,OAAO,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAC1E,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAInD,MAAM,eAAe,GAAwB,IAAI,GAAG,CAAkB;IACpE,YAAY;IACZ,UAAU;IACV,QAAQ;CACT,CAAC,CAAC;AAEH;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,YAAoB;IACxD,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,YAAY,CAAC,EAAE,CAAC;QACvC,MAAM,IAAI,KAAK,CACb,0BAA0B,YAAY,KAAK;YAC3C,wBAAwB,CAAC,GAAG,eAAe,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAC3D,CAAC;IACJ,CAAC;IAED,IAAI,YAAY,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO,oBAAoB,EAAE,CAAC;IAChC,CAAC;IAED,IAAI,YAAY,KAAK,UAAU,EAAE,CAAC;QAChC,MAAM,SAAS,GAAG,MAAM,kBAAkB,EAAE,CAAC;QAC7C,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CACb,kEAAkE;gBAClE,mCAAmC;gBACnC,kCAAkC;gBAClC,6EAA6E;gBAC7E,uEAAuE,CACxE,CAAC;QACJ,CAAC;QACD,OAAO,qBAAqB,EAAE,CAAC;IACjC,CAAC;IAED,MAAM,SAAS,GAAG,MAAM,qBAAqB,EAAE,CAAC;IAChD,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CACb,2DAA2D;YAC3D,2BAA2B;YAC3B,gDAAgD;YAChD,0CAA0C;YAC1C,mCAAmC;YACnC,2CAA2C;YAC3C,sCAAsC,CACvC,CAAC;IACJ,CAAC;IAED,OAAO,wBAAwB,EAAE,CAAC;AACpC,CAAC"}
1
+ {"version":3,"file":"resolve-provider.js","sourceRoot":"","sources":["../../../src/tts/resolve-provider.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,wBAAwB,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAClF,OAAO,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAC1E,OAAO,EAAE,wBAAwB,EAAE,MAAM,iBAAiB,CAAC;AAC3D,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAInD,MAAM,eAAe,GAAwB,IAAI,GAAG,CAAkB;IACpE,YAAY;IACZ,UAAU;IACV,QAAQ;IACR,YAAY;CACb,CAAC,CAAC;AAEH;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,YAAoB;IACxD,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,YAAY,CAAC,EAAE,CAAC;QACvC,MAAM,IAAI,KAAK,CACb,0BAA0B,YAAY,KAAK;YAC3C,wBAAwB,CAAC,GAAG,eAAe,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAC3D,CAAC;IACJ,CAAC;IAED,IAAI,YAAY,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO,oBAAoB,EAAE,CAAC;IAChC,CAAC;IAED,IAAI,YAAY,KAAK,YAAY,EAAE,CAAC;QAClC,OAAO,wBAAwB,EAAE,CAAC;IACpC,CAAC;IAED,IAAI,YAAY,KAAK,UAAU,EAAE,CAAC;QAChC,MAAM,SAAS,GAAG,MAAM,kBAAkB,EAAE,CAAC;QAC7C,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CACb,kEAAkE;gBAClE,mCAAmC;gBACnC,kCAAkC;gBAClC,6EAA6E;gBAC7E,yEAAyE;gBACzE,2EAA2E,CAC5E,CAAC;QACJ,CAAC;QACD,OAAO,qBAAqB,EAAE,CAAC;IACjC,CAAC;IAED,MAAM,SAAS,GAAG,MAAM,qBAAqB,EAAE,CAAC;IAChD,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CACb,2DAA2D;YAC3D,2BAA2B;YAC3B,gDAAgD;YAChD,0CAA0C;YAC1C,mCAAmC;YACnC,qCAAqC;YACrC,yEAAyE;YACzE,2EAA2E,CAC5E,CAAC;IACJ,CAAC;IAED,OAAO,wBAAwB,EAAE,CAAC;AACpC,CAAC"}
@@ -1,14 +1,16 @@
1
1
  import type { TtsProvider } from "./types.js";
2
2
  import { createEchogardenProvider, isEchogardenAvailable } from "./echogarden.js";
3
3
  import { createEdgeTtsProvider, isEdgeTtsAvailable } from "./edge-tts.js";
4
+ import { createElevenLabsProvider } from "./elevenlabs.js";
4
5
  import { createOpenAIProvider } from "./openai.js";
5
6
 
6
- export type TtsProviderName = "echogarden" | "edge-tts" | "openai";
7
+ export type TtsProviderName = "echogarden" | "edge-tts" | "openai" | "elevenlabs";
7
8
 
8
9
  const KNOWN_PROVIDERS: ReadonlySet<string> = new Set<TtsProviderName>([
9
10
  "echogarden",
10
11
  "edge-tts",
11
12
  "openai",
13
+ "elevenlabs",
12
14
  ]);
13
15
 
14
16
  /**
@@ -30,6 +32,10 @@ export async function resolveProvider(providerName: string): Promise<TtsProvider
30
32
  return createOpenAIProvider();
31
33
  }
32
34
 
35
+ if (providerName === "elevenlabs") {
36
+ return createElevenLabsProvider();
37
+ }
38
+
33
39
  if (providerName === "edge-tts") {
34
40
  const available = await isEdgeTtsAvailable();
35
41
  if (!available) {
@@ -38,7 +44,8 @@ export async function resolveProvider(providerName: string): Promise<TtsProvider
38
44
  " pnpm add edge-tts-universal\n\n" +
39
45
  "Or use a different provider:\n\n" +
40
46
  " scenar narrate <file> --tts echogarden (offline, pnpm add echogarden)\n" +
41
- " scenar narrate <file> --tts openai (requires OPENAI_API_KEY)",
47
+ " scenar narrate <file> --tts openai (requires OPENAI_API_KEY)\n" +
48
+ " scenar narrate <file> --tts elevenlabs (requires ELEVENLABS_API_KEY)",
42
49
  );
43
50
  }
44
51
  return createEdgeTtsProvider();
@@ -52,8 +59,9 @@ export async function resolveProvider(providerName: string): Promise<TtsProvider
52
59
  "Or use free online TTS (requires network):\n\n" +
53
60
  " scenar narrate <file> --tts edge-tts\n" +
54
61
  " pnpm add edge-tts-universal\n\n" +
55
- "Or use OpenAI TTS (requires API key):\n\n" +
56
- " scenar narrate <file> --tts openai",
62
+ "Or use an API-key TTS provider:\n\n" +
63
+ " scenar narrate <file> --tts openai (requires OPENAI_API_KEY)\n" +
64
+ " scenar narrate <file> --tts elevenlabs (requires ELEVENLABS_API_KEY)",
57
65
  );
58
66
  }
59
67