@vibeframe/cli 0.53.0 → 0.54.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/dist/commands/_shared/scene-html-emit.d.ts +35 -0
- package/dist/commands/_shared/scene-html-emit.d.ts.map +1 -1
- package/dist/commands/_shared/scene-html-emit.js +85 -16
- package/dist/commands/_shared/scene-html-emit.js.map +1 -1
- package/dist/commands/_shared/scene-html-emit.test.js +194 -1
- package/dist/commands/_shared/scene-html-emit.test.js.map +1 -1
- package/dist/commands/_shared/scene-project.d.ts +1 -1
- package/dist/commands/_shared/scene-project.d.ts.map +1 -1
- package/dist/commands/_shared/tts-resolve.d.ts +75 -0
- package/dist/commands/_shared/tts-resolve.d.ts.map +1 -0
- package/dist/commands/_shared/tts-resolve.js +89 -0
- package/dist/commands/_shared/tts-resolve.js.map +1 -0
- package/dist/commands/_shared/tts-resolve.test.d.ts +6 -0
- package/dist/commands/_shared/tts-resolve.test.d.ts.map +1 -0
- package/dist/commands/_shared/tts-resolve.test.js +147 -0
- package/dist/commands/_shared/tts-resolve.test.js.map +1 -0
- package/dist/commands/scene.d.ts +27 -1
- package/dist/commands/scene.d.ts.map +1 -1
- package/dist/commands/scene.js +111 -13
- package/dist/commands/scene.js.map +1 -1
- package/dist/commands/scene.test.js +87 -1
- package/dist/commands/scene.test.js.map +1 -1
- package/dist/utils/provider-resolver.d.ts.map +1 -1
- package/dist/utils/provider-resolver.js +6 -2
- package/dist/utils/provider-resolver.js.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module _shared/tts-resolve
|
|
3
|
+
*
|
|
4
|
+
* One-stop factory for picking a TTS provider at runtime. Used by every
|
|
5
|
+
* scene-authoring path that turns text into narration audio (currently
|
|
6
|
+
* `executeSceneAdd`; `script-to-video --format scenes` follows in C5+).
|
|
7
|
+
*
|
|
8
|
+
* Routes between:
|
|
9
|
+
* - **ElevenLabs** — cloud, API-key gated, paid (~$0.02/scene). Default
|
|
10
|
+
* when `ELEVENLABS_API_KEY` is set.
|
|
11
|
+
* - **Kokoro** — local Apache 2.0 model, free, ~330MB first-call download.
|
|
12
|
+
* The fallback for key-less users.
|
|
13
|
+
*
|
|
14
|
+
* The router exposes a uniform `TtsCallable` shape so call sites don't have
|
|
15
|
+
* to branch on provider id. Both providers already return the same
|
|
16
|
+
* `{ success, audioBuffer, error?, characterCount? }` result.
|
|
17
|
+
*/
|
|
18
|
+
import { ElevenLabsProvider, KokoroProvider } from "@vibeframe/ai-providers";
|
|
19
|
+
import { hasApiKey } from "../../utils/api-key.js";
|
|
20
|
+
import { getApiKey } from "../../utils/api-key.js";
|
|
21
|
+
/**
|
|
22
|
+
* Resolve a TTS provider preference into a callable + metadata.
|
|
23
|
+
*
|
|
24
|
+
* Resolution policy:
|
|
25
|
+
* - `"elevenlabs"`: requires `ELEVENLABS_API_KEY`. Throws an `ApiKeyError`-style
|
|
26
|
+
* error via {@link getApiKey} (consumed by `requireApiKey`/`exitWithError`).
|
|
27
|
+
* - `"kokoro"`: always available (local). No key check.
|
|
28
|
+
* - `"auto"` (or `undefined`): if `ELEVENLABS_API_KEY` is set, picks
|
|
29
|
+
* ElevenLabs; otherwise falls back to Kokoro.
|
|
30
|
+
*/
|
|
31
|
+
export async function resolveTtsProvider(preferred = "auto") {
|
|
32
|
+
const choice = preferred === "auto"
|
|
33
|
+
? (hasApiKey("ELEVENLABS_API_KEY") ? "elevenlabs" : "kokoro")
|
|
34
|
+
: preferred;
|
|
35
|
+
if (choice === "elevenlabs") {
|
|
36
|
+
return buildElevenLabs();
|
|
37
|
+
}
|
|
38
|
+
return buildKokoro();
|
|
39
|
+
}
|
|
40
|
+
async function buildElevenLabs() {
|
|
41
|
+
const key = await getApiKey("ELEVENLABS_API_KEY", "ElevenLabs");
|
|
42
|
+
if (!key) {
|
|
43
|
+
throw new TtsKeyMissingError("elevenlabs");
|
|
44
|
+
}
|
|
45
|
+
const provider = new ElevenLabsProvider();
|
|
46
|
+
await provider.initialize({ apiKey: key });
|
|
47
|
+
const call = async (text, opts) => provider.textToSpeech(text, {
|
|
48
|
+
voiceId: opts?.voice,
|
|
49
|
+
speed: opts?.speed,
|
|
50
|
+
});
|
|
51
|
+
return { provider: "elevenlabs", audioExtension: "mp3", call };
|
|
52
|
+
}
|
|
53
|
+
async function buildKokoro() {
|
|
54
|
+
const provider = new KokoroProvider();
|
|
55
|
+
// No-op initialize — Kokoro doesn't take a config but we keep parity with
|
|
56
|
+
// the AIProvider lifecycle for future audit checks.
|
|
57
|
+
await provider.initialize({});
|
|
58
|
+
const call = async (text, opts) => provider.textToSpeech(text, {
|
|
59
|
+
voice: opts?.voice,
|
|
60
|
+
speed: opts?.speed,
|
|
61
|
+
onProgress: opts?.onProgress,
|
|
62
|
+
});
|
|
63
|
+
return { provider: "kokoro", audioExtension: "wav", call };
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Thrown when the caller asked for a specific provider whose key/runtime is
|
|
67
|
+
* unavailable. Carries the provider id so the caller can format a clean
|
|
68
|
+
* `usageError`/`exitWithError`.
|
|
69
|
+
*/
|
|
70
|
+
export class TtsKeyMissingError extends Error {
|
|
71
|
+
provider;
|
|
72
|
+
constructor(provider) {
|
|
73
|
+
super(provider === "elevenlabs"
|
|
74
|
+
? "ElevenLabs API key required (ELEVENLABS_API_KEY). Run 'vibe setup', set ELEVENLABS_API_KEY in .env, or pass --tts kokoro for local synthesis."
|
|
75
|
+
: `Provider ${provider} is unavailable.`);
|
|
76
|
+
this.provider = provider;
|
|
77
|
+
this.name = "TtsKeyMissingError";
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** Validate a free-form `--tts <value>` against {@link TtsProviderName}. */
|
|
81
|
+
export function parseTtsProviderName(value) {
|
|
82
|
+
if (!value)
|
|
83
|
+
return "auto";
|
|
84
|
+
if (value === "auto" || value === "elevenlabs" || value === "kokoro") {
|
|
85
|
+
return value;
|
|
86
|
+
}
|
|
87
|
+
throw new Error(`Invalid --tts: ${value}. Valid: auto, elevenlabs, kokoro.`);
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=tts-resolve.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tts-resolve.js","sourceRoot":"","sources":["../../../src/commands/_shared/tts-resolve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAE7E,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AA0CnD;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,YAA6B,MAAM;IAEnC,MAAM,MAAM,GAAG,SAAS,KAAK,MAAM;QACjC,CAAC,CAAC,CAAC,SAAS,CAAC,oBAAoB,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,QAAQ,CAAC;QAC7D,CAAC,CAAC,SAAS,CAAC;IAEd,IAAI,MAAM,KAAK,YAAY,EAAE,CAAC;QAC5B,OAAO,eAAe,EAAE,CAAC;IAC3B,CAAC;IACD,OAAO,WAAW,EAAE,CAAC;AACvB,CAAC;AAED,KAAK,UAAU,eAAe;IAC5B,MAAM,GAAG,GAAG,MAAM,SAAS,CAAC,oBAAoB,EAAE,YAAY,CAAC,CAAC;IAChE,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,IAAI,kBAAkB,CAAC,YAAY,CAAC,CAAC;IAC7C,CAAC;IACD,MAAM,QAAQ,GAAG,IAAI,kBAAkB,EAAE,CAAC;IAC1C,MAAM,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;IAC3C,MAAM,IAAI,GAAgB,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,CAC7C,QAAQ,CAAC,YAAY,CAAC,IAAI,EAAE;QAC1B,OAAO,EAAE,IAAI,EAAE,KAAK;QACpB,KAAK,EAAE,IAAI,EAAE,KAAK;KACnB,CAAC,CAAC;IACL,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,cAAc,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;AACjE,CAAC;AAED,KAAK,UAAU,WAAW;IACxB,MAAM,QAAQ,GAAG,IAAI,cAAc,EAAE,CAAC;IACtC,0EAA0E;IAC1E,oDAAoD;IACpD,MAAM,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;IAC9B,MAAM,IAAI,GAAgB,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,CAC7C,QAAQ,CAAC,YAAY,CAAC,IAAI,EAAE;QAC1B,KAAK,EAAE,IAAI,EAAE,KAAK;QAClB,KAAK,EAAE,IAAI,EAAE,KAAK;QAClB,UAAU,EAAE,IAAI,EAAE,UAAU;KAC7B,CAAC,CAAC;IACL,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,cAAc,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;AAC7D,CAAC;AAED;;;;GAIG;AACH,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IACf;IAA5B,YAA4B,QAA6B;QACvD,KAAK,CACH,QAAQ,KAAK,YAAY;YACvB,CAAC,CAAC,+IAA+I;YACjJ,CAAC,CAAC,YAAY,QAAQ,kBAAkB,CAC3C,CAAC;QALwB,aAAQ,GAAR,QAAQ,CAAqB;QAMvD,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;IACnC,CAAC;CACF;AAED,4EAA4E;AAC5E,MAAM,UAAU,oBAAoB,CAAC,KAAyB;IAC5D,IAAI,CAAC,KAAK;QAAE,OAAO,MAAM,CAAC;IAC1B,IAAI,KAAK,KAAK,MAAM,IAAI,KAAK,KAAK,YAAY,IAAI,KAAK,KAAK,QAAQ,EAAE,CAAC;QACrE,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,IAAI,KAAK,CACb,kBAAkB,KAAK,oCAAoC,CAC5D,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tts-resolve.test.d.ts","sourceRoot":"","sources":["../../../src/commands/_shared/tts-resolve.test.ts"],"names":[],"mappings":"AAAA;;;GAGG"}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the TTS router. The actual provider classes are mocked at
|
|
3
|
+
* the module level so we can drive resolution outcomes deterministically.
|
|
4
|
+
*/
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
// vi.mock factories are hoisted above imports — so any references must come
|
|
7
|
+
// from vi.hoisted (which is also hoisted) rather than module-scope variables.
|
|
8
|
+
const mocks = vi.hoisted(() => {
|
|
9
|
+
return {
|
|
10
|
+
elevenLabsTextToSpeech: vi.fn(),
|
|
11
|
+
elevenLabsInitialize: vi.fn(),
|
|
12
|
+
kokoroTextToSpeech: vi.fn(),
|
|
13
|
+
kokoroInitialize: vi.fn(),
|
|
14
|
+
hasApiKey: vi.fn(),
|
|
15
|
+
getApiKey: vi.fn(),
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
vi.mock("@vibeframe/ai-providers", () => ({
|
|
19
|
+
ElevenLabsProvider: class {
|
|
20
|
+
initialize = mocks.elevenLabsInitialize;
|
|
21
|
+
textToSpeech = mocks.elevenLabsTextToSpeech;
|
|
22
|
+
},
|
|
23
|
+
KokoroProvider: class {
|
|
24
|
+
initialize = mocks.kokoroInitialize;
|
|
25
|
+
textToSpeech = mocks.kokoroTextToSpeech;
|
|
26
|
+
},
|
|
27
|
+
}));
|
|
28
|
+
vi.mock("../../utils/api-key.js", () => ({
|
|
29
|
+
hasApiKey: (...args) => mocks.hasApiKey(...args),
|
|
30
|
+
getApiKey: (...args) => mocks.getApiKey(...args),
|
|
31
|
+
}));
|
|
32
|
+
const { elevenLabsTextToSpeech, elevenLabsInitialize, kokoroTextToSpeech, kokoroInitialize, hasApiKey, getApiKey, } = mocks;
|
|
33
|
+
import { parseTtsProviderName, resolveTtsProvider, TtsKeyMissingError, } from "./tts-resolve.js";
|
|
34
|
+
describe("parseTtsProviderName", () => {
|
|
35
|
+
it("defaults to auto when undefined or empty", () => {
|
|
36
|
+
expect(parseTtsProviderName(undefined)).toBe("auto");
|
|
37
|
+
expect(parseTtsProviderName("")).toBe("auto");
|
|
38
|
+
});
|
|
39
|
+
it("accepts the three valid values", () => {
|
|
40
|
+
expect(parseTtsProviderName("auto")).toBe("auto");
|
|
41
|
+
expect(parseTtsProviderName("elevenlabs")).toBe("elevenlabs");
|
|
42
|
+
expect(parseTtsProviderName("kokoro")).toBe("kokoro");
|
|
43
|
+
});
|
|
44
|
+
it("throws for unknown providers", () => {
|
|
45
|
+
expect(() => parseTtsProviderName("openai")).toThrow(/Invalid --tts/);
|
|
46
|
+
expect(() => parseTtsProviderName("KOKORO")).toThrow(/Invalid --tts/);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
describe("resolveTtsProvider", () => {
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
elevenLabsTextToSpeech.mockReset();
|
|
52
|
+
elevenLabsInitialize.mockReset();
|
|
53
|
+
kokoroTextToSpeech.mockReset();
|
|
54
|
+
kokoroInitialize.mockReset();
|
|
55
|
+
hasApiKey.mockReset();
|
|
56
|
+
getApiKey.mockReset();
|
|
57
|
+
});
|
|
58
|
+
afterEach(() => {
|
|
59
|
+
vi.restoreAllMocks();
|
|
60
|
+
});
|
|
61
|
+
describe("auto", () => {
|
|
62
|
+
it("picks ElevenLabs when ELEVENLABS_API_KEY is set", async () => {
|
|
63
|
+
hasApiKey.mockReturnValue(true);
|
|
64
|
+
getApiKey.mockResolvedValue("sk-test");
|
|
65
|
+
const r = await resolveTtsProvider("auto");
|
|
66
|
+
expect(r.provider).toBe("elevenlabs");
|
|
67
|
+
expect(r.audioExtension).toBe("mp3");
|
|
68
|
+
expect(hasApiKey).toHaveBeenCalledWith("ELEVENLABS_API_KEY");
|
|
69
|
+
expect(elevenLabsInitialize).toHaveBeenCalledWith({ apiKey: "sk-test" });
|
|
70
|
+
});
|
|
71
|
+
it("falls back to Kokoro when ELEVENLABS_API_KEY is unset", async () => {
|
|
72
|
+
hasApiKey.mockReturnValue(false);
|
|
73
|
+
const r = await resolveTtsProvider("auto");
|
|
74
|
+
expect(r.provider).toBe("kokoro");
|
|
75
|
+
expect(r.audioExtension).toBe("wav");
|
|
76
|
+
expect(kokoroInitialize).toHaveBeenCalled();
|
|
77
|
+
expect(getApiKey).not.toHaveBeenCalled();
|
|
78
|
+
});
|
|
79
|
+
it("treats undefined preferred the same as auto", async () => {
|
|
80
|
+
hasApiKey.mockReturnValue(false);
|
|
81
|
+
const r = await resolveTtsProvider();
|
|
82
|
+
expect(r.provider).toBe("kokoro");
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
describe("explicit elevenlabs", () => {
|
|
86
|
+
it("uses ElevenLabs when key is present", async () => {
|
|
87
|
+
getApiKey.mockResolvedValue("sk-real");
|
|
88
|
+
const r = await resolveTtsProvider("elevenlabs");
|
|
89
|
+
expect(r.provider).toBe("elevenlabs");
|
|
90
|
+
expect(elevenLabsInitialize).toHaveBeenCalledWith({ apiKey: "sk-real" });
|
|
91
|
+
});
|
|
92
|
+
it("throws TtsKeyMissingError when key is absent", async () => {
|
|
93
|
+
getApiKey.mockResolvedValue(undefined);
|
|
94
|
+
await expect(resolveTtsProvider("elevenlabs")).rejects.toBeInstanceOf(TtsKeyMissingError);
|
|
95
|
+
expect(kokoroInitialize).not.toHaveBeenCalled();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
describe("explicit kokoro", () => {
|
|
99
|
+
it("uses Kokoro without checking any key", async () => {
|
|
100
|
+
const r = await resolveTtsProvider("kokoro");
|
|
101
|
+
expect(r.provider).toBe("kokoro");
|
|
102
|
+
expect(r.audioExtension).toBe("wav");
|
|
103
|
+
expect(kokoroInitialize).toHaveBeenCalled();
|
|
104
|
+
expect(getApiKey).not.toHaveBeenCalled();
|
|
105
|
+
expect(hasApiKey).not.toHaveBeenCalled();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
describe("call dispatch", () => {
|
|
109
|
+
it("forwards voice + speed to ElevenLabs as voiceId/speed", async () => {
|
|
110
|
+
hasApiKey.mockReturnValue(true);
|
|
111
|
+
getApiKey.mockResolvedValue("sk-test");
|
|
112
|
+
elevenLabsTextToSpeech.mockResolvedValue({ success: true, audioBuffer: Buffer.from("x") });
|
|
113
|
+
const r = await resolveTtsProvider("elevenlabs");
|
|
114
|
+
await r.call("Hello.", { voice: "rachel", speed: 1.1 });
|
|
115
|
+
expect(elevenLabsTextToSpeech).toHaveBeenCalledWith("Hello.", {
|
|
116
|
+
voiceId: "rachel",
|
|
117
|
+
speed: 1.1,
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
it("forwards voice + speed + onProgress to Kokoro", async () => {
|
|
121
|
+
kokoroTextToSpeech.mockResolvedValue({ success: true, audioBuffer: Buffer.from("y") });
|
|
122
|
+
const r = await resolveTtsProvider("kokoro");
|
|
123
|
+
const onProgress = vi.fn();
|
|
124
|
+
await r.call("Hello.", { voice: "af_heart", speed: 1.0, onProgress });
|
|
125
|
+
expect(kokoroTextToSpeech).toHaveBeenCalledWith("Hello.", {
|
|
126
|
+
voice: "af_heart",
|
|
127
|
+
speed: 1.0,
|
|
128
|
+
onProgress,
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
describe("TtsKeyMissingError", () => {
|
|
134
|
+
it("provides actionable elevenlabs message", () => {
|
|
135
|
+
const err = new TtsKeyMissingError("elevenlabs");
|
|
136
|
+
expect(err.message).toMatch(/ELEVENLABS_API_KEY/);
|
|
137
|
+
expect(err.message).toMatch(/--tts kokoro/);
|
|
138
|
+
expect(err.provider).toBe("elevenlabs");
|
|
139
|
+
});
|
|
140
|
+
it("is identifiable via instanceof", () => {
|
|
141
|
+
const err = new TtsKeyMissingError("elevenlabs");
|
|
142
|
+
expect(err).toBeInstanceOf(TtsKeyMissingError);
|
|
143
|
+
expect(err).toBeInstanceOf(Error);
|
|
144
|
+
expect(err.name).toBe("TtsKeyMissingError");
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
//# sourceMappingURL=tts-resolve.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tts-resolve.test.js","sourceRoot":"","sources":["../../../src/commands/_shared/tts-resolve.test.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAEzE,4EAA4E;AAC5E,8EAA8E;AAC9E,MAAM,KAAK,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE;IAC5B,OAAO;QACL,sBAAsB,EAAE,EAAE,CAAC,EAAE,EAAE;QAC/B,oBAAoB,EAAE,EAAE,CAAC,EAAE,EAAE;QAC7B,kBAAkB,EAAE,EAAE,CAAC,EAAE,EAAE;QAC3B,gBAAgB,EAAE,EAAE,CAAC,EAAE,EAAE;QACzB,SAAS,EAAE,EAAE,CAAC,EAAE,EAAE;QAClB,SAAS,EAAE,EAAE,CAAC,EAAE,EAAE;KACnB,CAAC;AACJ,CAAC,CAAC,CAAC;AAEH,EAAE,CAAC,IAAI,CAAC,yBAAyB,EAAE,GAAG,EAAE,CAAC,CAAC;IACxC,kBAAkB,EAAE;QAClB,UAAU,GAAG,KAAK,CAAC,oBAAoB,CAAC;QACxC,YAAY,GAAG,KAAK,CAAC,sBAAsB,CAAC;KAC7C;IACD,cAAc,EAAE;QACd,UAAU,GAAG,KAAK,CAAC,gBAAgB,CAAC;QACpC,YAAY,GAAG,KAAK,CAAC,kBAAkB,CAAC;KACzC;CACF,CAAC,CAAC,CAAC;AAEJ,EAAE,CAAC,IAAI,CAAC,wBAAwB,EAAE,GAAG,EAAE,CAAC,CAAC;IACvC,SAAS,EAAE,CAAC,GAAG,IAAe,EAAE,EAAE,CAAC,KAAK,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC;IAC3D,SAAS,EAAE,CAAC,GAAG,IAAe,EAAE,EAAE,CAAC,KAAK,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC;CAC5D,CAAC,CAAC,CAAC;AAEJ,MAAM,EACJ,sBAAsB,EACtB,oBAAoB,EACpB,kBAAkB,EAClB,gBAAgB,EAChB,SAAS,EACT,SAAS,GACV,GAAG,KAAK,CAAC;AAEV,OAAO,EACL,oBAAoB,EACpB,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,kBAAkB,CAAC;AAE1B,QAAQ,CAAC,sBAAsB,EAAE,GAAG,EAAE;IACpC,EAAE,CAAC,0CAA0C,EAAE,GAAG,EAAE;QAClD,MAAM,CAAC,oBAAoB,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACrD,MAAM,CAAC,oBAAoB,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAChD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gCAAgC,EAAE,GAAG,EAAE;QACxC,MAAM,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAClD,MAAM,CAAC,oBAAoB,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QAC9D,MAAM,CAAC,oBAAoB,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,8BAA8B,EAAE,GAAG,EAAE;QACtC,MAAM,CAAC,GAAG,EAAE,CAAC,oBAAoB,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;QACtE,MAAM,CAAC,GAAG,EAAE,CAAC,oBAAoB,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;IACxE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,oBAAoB,EAAE,GAAG,EAAE;IAClC,UAAU,CAAC,GAAG,EAAE;QACd,sBAAsB,CAAC,SAAS,EAAE,CAAC;QACnC,oBAAoB,CAAC,SAAS,EAAE,CAAC;QACjC,kBAAkB,CAAC,SAAS,EAAE,CAAC;QAC/B,gBAAgB,CAAC,SAAS,EAAE,CAAC;QAC7B,SAAS,CAAC,SAAS,EAAE,CAAC;QACtB,SAAS,CAAC,SAAS,EAAE,CAAC;IACxB,CAAC,CAAC,CAAC;IAEH,SAAS,CAAC,GAAG,EAAE;QACb,EAAE,CAAC,eAAe,EAAE,CAAC;IACvB,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,MAAM,EAAE,GAAG,EAAE;QACpB,EAAE,CAAC,iDAAiD,EAAE,KAAK,IAAI,EAAE;YAC/D,SAAS,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;YAChC,SAAS,CAAC,iBAAiB,CAAC,SAAS,CAAC,CAAC;YAEvC,MAAM,CAAC,GAAG,MAAM,kBAAkB,CAAC,MAAM,CAAC,CAAC;YAE3C,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;YACtC,MAAM,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACrC,MAAM,CAAC,SAAS,CAAC,CAAC,oBAAoB,CAAC,oBAAoB,CAAC,CAAC;YAC7D,MAAM,CAAC,oBAAoB,CAAC,CAAC,oBAAoB,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;QAC3E,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,uDAAuD,EAAE,KAAK,IAAI,EAAE;YACrE,SAAS,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;YAEjC,MAAM,CAAC,GAAG,MAAM,kBAAkB,CAAC,MAAM,CAAC,CAAC;YAE3C,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YAClC,MAAM,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACrC,MAAM,CAAC,gBAAgB,CAAC,CAAC,gBAAgB,EAAE,CAAC;YAC5C,MAAM,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;QAC3C,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,6CAA6C,EAAE,KAAK,IAAI,EAAE;YAC3D,SAAS,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;YAEjC,MAAM,CAAC,GAAG,MAAM,kBAAkB,EAAE,CAAC;YAErC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACpC,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,qBAAqB,EAAE,GAAG,EAAE;QACnC,EAAE,CAAC,qCAAqC,EAAE,KAAK,IAAI,EAAE;YACnD,SAAS,CAAC,iBAAiB,CAAC,SAAS,CAAC,CAAC;YAEvC,MAAM,CAAC,GAAG,MAAM,kBAAkB,CAAC,YAAY,CAAC,CAAC;YAEjD,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;YACtC,MAAM,CAAC,oBAAoB,CAAC,CAAC,oBAAoB,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;QAC3E,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,8CAA8C,EAAE,KAAK,IAAI,EAAE;YAC5D,SAAS,CAAC,iBAAiB,CAAC,SAAS,CAAC,CAAC;YAEvC,MAAM,MAAM,CAAC,kBAAkB,CAAC,YAAY,CAAC,CAAC,CAAC,OAAO,CAAC,cAAc,CACnE,kBAAkB,CACnB,CAAC;YACF,MAAM,CAAC,gBAAgB,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;QAClD,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,iBAAiB,EAAE,GAAG,EAAE;QAC/B,EAAE,CAAC,sCAAsC,EAAE,KAAK,IAAI,EAAE;YACpD,MAAM,CAAC,GAAG,MAAM,kBAAkB,CAAC,QAAQ,CAAC,CAAC;YAE7C,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YAClC,MAAM,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACrC,MAAM,CAAC,gBAAgB,CAAC,CAAC,gBAAgB,EAAE,CAAC;YAC5C,MAAM,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;YACzC,MAAM,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;QAC3C,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;QAC7B,EAAE,CAAC,uDAAuD,EAAE,KAAK,IAAI,EAAE;YACrE,SAAS,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;YAChC,SAAS,CAAC,iBAAiB,CAAC,SAAS,CAAC,CAAC;YACvC,sBAAsB,CAAC,iBAAiB,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YAE3F,MAAM,CAAC,GAAG,MAAM,kBAAkB,CAAC,YAAY,CAAC,CAAC;YACjD,MAAM,CAAC,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;YAExD,MAAM,CAAC,sBAAsB,CAAC,CAAC,oBAAoB,CAAC,QAAQ,EAAE;gBAC5D,OAAO,EAAE,QAAQ;gBACjB,KAAK,EAAE,GAAG;aACX,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,+CAA+C,EAAE,KAAK,IAAI,EAAE;YAC7D,kBAAkB,CAAC,iBAAiB,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YAEvF,MAAM,CAAC,GAAG,MAAM,kBAAkB,CAAC,QAAQ,CAAC,CAAC;YAC7C,MAAM,UAAU,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC;YAC3B,MAAM,CAAC,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,GAAG,EAAE,UAAU,EAAE,CAAC,CAAC;YAEtE,MAAM,CAAC,kBAAkB,CAAC,CAAC,oBAAoB,CAAC,QAAQ,EAAE;gBACxD,KAAK,EAAE,UAAU;gBACjB,KAAK,EAAE,GAAG;gBACV,UAAU;aACX,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,oBAAoB,EAAE,GAAG,EAAE;IAClC,EAAE,CAAC,wCAAwC,EAAE,GAAG,EAAE;QAChD,MAAM,GAAG,GAAG,IAAI,kBAAkB,CAAC,YAAY,CAAC,CAAC;QACjD,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,oBAAoB,CAAC,CAAC;QAClD,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;QAC5C,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;IAC1C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gCAAgC,EAAE,GAAG,EAAE;QACxC,MAAM,GAAG,GAAG,IAAI,kBAAkB,CAAC,YAAY,CAAC,CAAC;QACjD,MAAM,CAAC,GAAG,CAAC,CAAC,cAAc,CAAC,kBAAkB,CAAC,CAAC;QAC/C,MAAM,CAAC,GAAG,CAAC,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;QAClC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC;IAC9C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
package/dist/commands/scene.d.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* - render [C4, this commit] — render scene project to MP4/WebM/MOV
|
|
15
15
|
*/
|
|
16
16
|
import { Command } from "commander";
|
|
17
|
+
import { type TtsProviderName } from "./_shared/tts-resolve.js";
|
|
17
18
|
import { type ScenePreset } from "./_shared/scene-html-emit.js";
|
|
18
19
|
export declare const sceneCommand: Command;
|
|
19
20
|
export interface SceneAddOptions {
|
|
@@ -21,6 +22,14 @@ export interface SceneAddOptions {
|
|
|
21
22
|
preset: ScenePreset;
|
|
22
23
|
/** Narration text. If the value is an existing file path, its content is used. */
|
|
23
24
|
narration?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Path to an existing narration audio file (.wav/.mp3). When provided,
|
|
27
|
+
* skips TTS entirely and copies the file into the project's `assets/`.
|
|
28
|
+
* Compatible with `npx hyperframes tts`, `say`, or any other external
|
|
29
|
+
* synthesis tool. The transcript is still generated unless
|
|
30
|
+
* `skipTranscribe` is set.
|
|
31
|
+
*/
|
|
32
|
+
narrationFile?: string;
|
|
24
33
|
/** Explicit duration in seconds. Overrides narration-derived duration. */
|
|
25
34
|
duration?: number;
|
|
26
35
|
/** Image generation prompt. */
|
|
@@ -33,12 +42,21 @@ export interface SceneAddOptions {
|
|
|
33
42
|
insertInto?: string;
|
|
34
43
|
/** "gemini" | "openai". */
|
|
35
44
|
imageProvider?: string;
|
|
36
|
-
/** ElevenLabs
|
|
45
|
+
/** TTS provider preference. Defaults to `"auto"` (ElevenLabs if key set, else Kokoro). */
|
|
46
|
+
tts?: TtsProviderName;
|
|
47
|
+
/** Voice id (ElevenLabs name/id, or Kokoro id like `af_heart`). */
|
|
37
48
|
voice?: string;
|
|
38
49
|
/** When true, skip TTS even if narration is provided. */
|
|
39
50
|
skipAudio?: boolean;
|
|
40
51
|
/** When true, skip image generation even if visuals is provided. */
|
|
41
52
|
skipImage?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* When true, skip the Whisper word-level transcribe step that would
|
|
55
|
+
* otherwise emit `assets/transcript-<id>.json`.
|
|
56
|
+
*/
|
|
57
|
+
skipTranscribe?: boolean;
|
|
58
|
+
/** BCP-47 language hint forwarded to Whisper (e.g. `"en"`, `"ko"`). */
|
|
59
|
+
transcribeLanguage?: string;
|
|
42
60
|
/** Overwrite existing compositions/scene-<id>.html. */
|
|
43
61
|
force?: boolean;
|
|
44
62
|
/** Progress sink (CLI spinner / agent stream). */
|
|
@@ -54,6 +72,14 @@ export interface SceneAddResult {
|
|
|
54
72
|
rootPath: string;
|
|
55
73
|
audioPath?: string;
|
|
56
74
|
imagePath?: string;
|
|
75
|
+
/**
|
|
76
|
+
* Project-relative path to `assets/transcript-<id>.json` when Whisper
|
|
77
|
+
* word-level transcribe ran. `undefined` when skipped or when the audio
|
|
78
|
+
* source produced no narration text to transcribe.
|
|
79
|
+
*/
|
|
80
|
+
transcriptPath?: string;
|
|
81
|
+
/** Number of word entries emitted into the transcript JSON. */
|
|
82
|
+
transcriptWordCount?: number;
|
|
57
83
|
error?: string;
|
|
58
84
|
}
|
|
59
85
|
export declare function executeSceneAdd(opts: SceneAddOptions): Promise<SceneAddResult>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scene.d.ts","sourceRoot":"","sources":["../../src/commands/scene.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"scene.d.ts","sourceRoot":"","sources":["../../src/commands/scene.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAYpC,OAAO,EAIL,KAAK,eAAe,EACrB,MAAM,0BAA0B,CAAC;AAOlC,OAAO,EAOL,KAAK,WAAW,EACjB,MAAM,8BAA8B,CAAC;AA8CtC,eAAO,MAAM,YAAY,SAkByC,CAAC;AAoMnE,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,WAAW,CAAC;IACpB,kFAAkF;IAClF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,+BAA+B;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0CAA0C;IAC1C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iEAAiE;IACjE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,2BAA2B;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,0FAA0F;IAC1F,GAAG,CAAC,EAAE,eAAe,CAAC;IACtB,mEAAmE;IACnE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yDAAyD;IACzD,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,oEAAoE;IACpE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;OAGG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,uEAAuE;IACvE,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,uDAAuD;IACvD,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,kDAAkD;IAClD,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,WAAW,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,+DAA+D;IAC/D,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAwCD,wBAAsB,eAAe,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC,CAuPpF"}
|
package/dist/commands/scene.js
CHANGED
|
@@ -15,12 +15,13 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { Command } from "commander";
|
|
17
17
|
import { basename, resolve, relative, dirname } from "node:path";
|
|
18
|
-
import { mkdir, readFile, writeFile, access } from "node:fs/promises";
|
|
18
|
+
import { mkdir, readFile, writeFile, access, copyFile } from "node:fs/promises";
|
|
19
19
|
import { existsSync } from "node:fs";
|
|
20
20
|
import chalk from "chalk";
|
|
21
21
|
import ora from "ora";
|
|
22
22
|
import { parse as yamlParse } from "yaml";
|
|
23
|
-
import {
|
|
23
|
+
import { GeminiProvider, OpenAIImageProvider, WhisperProvider, } from "@vibeframe/ai-providers";
|
|
24
|
+
import { resolveTtsProvider, TtsKeyMissingError, parseTtsProviderName, } from "./_shared/tts-resolve.js";
|
|
24
25
|
import { scaffoldSceneProject, aspectToDims, } from "./_shared/scene-project.js";
|
|
25
26
|
import { emitSceneHtml, insertClipIntoRoot, nextSceneStart, readRootDims, slugifySceneName, SCENE_PRESETS, } from "./_shared/scene-html-emit.js";
|
|
26
27
|
import { runProjectLint, rootExists, } from "./_shared/scene-lint.js";
|
|
@@ -136,6 +137,7 @@ sceneCommand
|
|
|
136
137
|
.argument("<name>", "Scene name (slugified into the composition id)")
|
|
137
138
|
.option("--style <preset>", `Style preset: ${SCENE_PRESETS.join(", ")}`, "simple")
|
|
138
139
|
.option("--narration <text>", "Narration text (or path to a .txt file). Drives TTS + scene duration.")
|
|
140
|
+
.option("--narration-file <path>", "Existing narration audio file (.wav/.mp3). Skips TTS — useful with hyperframes tts, Mac say, or other external tools.")
|
|
139
141
|
.option("-d, --duration <sec>", "Explicit scene duration in seconds (overrides narration audio)")
|
|
140
142
|
.option("--visuals <prompt>", "Image prompt — generates assets/scene-<id>.png via the configured image provider")
|
|
141
143
|
.option("--headline <text>", "Visible headline (defaults to the humanised scene name)")
|
|
@@ -143,9 +145,12 @@ sceneCommand
|
|
|
143
145
|
.option("--insert-into <path>", "Root composition file to update", "index.html")
|
|
144
146
|
.option("--project <dir>", "Project directory", ".")
|
|
145
147
|
.option("--image-provider <name>", "Image provider: gemini, openai", "gemini")
|
|
146
|
-
.option("--
|
|
148
|
+
.option("--tts <provider>", "TTS provider: auto, elevenlabs, kokoro (default auto — picks ElevenLabs when key set, else Kokoro local)", "auto")
|
|
149
|
+
.option("--voice <id>", "Voice id (ElevenLabs name/id, or Kokoro id like af_heart, am_michael)")
|
|
147
150
|
.option("--no-audio", "Skip TTS even when --narration is provided (useful for tests/agent dry runs)")
|
|
148
151
|
.option("--no-image", "Skip image generation even when --visuals is provided")
|
|
152
|
+
.option("--no-transcribe", "Skip Whisper word-level transcribe step (no transcript-<id>.json emitted)")
|
|
153
|
+
.option("--transcribe-language <code>", "BCP-47 language code passed to Whisper (e.g. en, ko)")
|
|
149
154
|
.option("--force", "Overwrite an existing compositions/scene-<id>.html")
|
|
150
155
|
.option("--dry-run", "Preview parameters without writing files or calling APIs")
|
|
151
156
|
.action(async (name, options) => {
|
|
@@ -153,6 +158,13 @@ sceneCommand
|
|
|
153
158
|
options.style = validatePreset(options.style);
|
|
154
159
|
if (options.duration !== undefined)
|
|
155
160
|
options.duration = validateDuration(options.duration);
|
|
161
|
+
let tts;
|
|
162
|
+
try {
|
|
163
|
+
tts = parseTtsProviderName(options.tts);
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
exitWithError(usageError(error instanceof Error ? error.message : String(error)));
|
|
167
|
+
}
|
|
156
168
|
if (options.dryRun) {
|
|
157
169
|
const id = slugifySceneName(name);
|
|
158
170
|
outputResult({
|
|
@@ -170,6 +182,7 @@ sceneCommand
|
|
|
170
182
|
project: options.project,
|
|
171
183
|
insertInto: options.insertInto,
|
|
172
184
|
imageProvider: options.imageProvider,
|
|
185
|
+
tts,
|
|
173
186
|
audio: options.audio, // commander sets `audio: false` when --no-audio is passed
|
|
174
187
|
image: options.image,
|
|
175
188
|
},
|
|
@@ -182,6 +195,7 @@ sceneCommand
|
|
|
182
195
|
name,
|
|
183
196
|
preset: options.style,
|
|
184
197
|
narration: options.narration,
|
|
198
|
+
narrationFile: options.narrationFile,
|
|
185
199
|
duration: options.duration,
|
|
186
200
|
visuals: options.visuals,
|
|
187
201
|
headline: options.headline,
|
|
@@ -189,9 +203,12 @@ sceneCommand
|
|
|
189
203
|
projectDir: options.project,
|
|
190
204
|
insertInto: options.insertInto,
|
|
191
205
|
imageProvider: options.imageProvider,
|
|
206
|
+
tts,
|
|
192
207
|
voice: options.voice,
|
|
193
208
|
skipAudio: options.audio === false,
|
|
194
209
|
skipImage: options.image === false,
|
|
210
|
+
skipTranscribe: options.transcribe === false,
|
|
211
|
+
transcribeLanguage: options.transcribeLanguage,
|
|
195
212
|
force: !!options.force,
|
|
196
213
|
onProgress: (msg) => {
|
|
197
214
|
if (spinner)
|
|
@@ -218,6 +235,8 @@ sceneCommand
|
|
|
218
235
|
console.log(chalk.green(" +"), result.audioPath);
|
|
219
236
|
if (result.imagePath)
|
|
220
237
|
console.log(chalk.green(" +"), result.imagePath);
|
|
238
|
+
if (result.transcriptPath)
|
|
239
|
+
console.log(chalk.green(" +"), result.transcriptPath);
|
|
221
240
|
console.log(chalk.yellow(" ~"), result.rootPath, chalk.dim("(updated)"));
|
|
222
241
|
console.log();
|
|
223
242
|
console.log(chalk.bold.cyan("Composition"));
|
|
@@ -325,19 +344,53 @@ export async function executeSceneAdd(opts) {
|
|
|
325
344
|
let audioRelPath;
|
|
326
345
|
let audioAbsPath;
|
|
327
346
|
let narrationDuration;
|
|
328
|
-
if (
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
347
|
+
if (opts.narrationFile && !opts.skipAudio) {
|
|
348
|
+
// External wav/mp3: skip TTS, copy file into the project's assets/ dir.
|
|
349
|
+
const sourceAbs = resolve(opts.narrationFile);
|
|
350
|
+
if (!(await pathExists(sourceAbs))) {
|
|
351
|
+
return errResult(`Narration file not found: ${sourceAbs}`);
|
|
352
|
+
}
|
|
353
|
+
const ext = (sourceAbs.match(/\.([a-z0-9]+)$/i)?.[1] ?? "wav").toLowerCase();
|
|
354
|
+
if (ext !== "wav" && ext !== "mp3") {
|
|
355
|
+
return errResult(`Unsupported narration file extension: .${ext}. Use .wav or .mp3.`);
|
|
356
|
+
}
|
|
357
|
+
audioRelPath = `assets/narration-${id}.${ext}`;
|
|
358
|
+
audioAbsPath = resolve(projectDir, audioRelPath);
|
|
359
|
+
await mkdir(dirname(audioAbsPath), { recursive: true });
|
|
360
|
+
await copyFile(sourceAbs, audioAbsPath);
|
|
361
|
+
try {
|
|
362
|
+
narrationDuration = await getAudioDuration(audioAbsPath);
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
narrationDuration = undefined;
|
|
332
366
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
367
|
+
}
|
|
368
|
+
else if (narrationText && !opts.skipAudio) {
|
|
369
|
+
let resolution;
|
|
370
|
+
try {
|
|
371
|
+
resolution = await resolveTtsProvider(opts.tts ?? "auto");
|
|
372
|
+
}
|
|
373
|
+
catch (error) {
|
|
374
|
+
if (error instanceof TtsKeyMissingError) {
|
|
375
|
+
return errResult(error.message);
|
|
376
|
+
}
|
|
377
|
+
throw error;
|
|
378
|
+
}
|
|
379
|
+
opts.onProgress?.(resolution.provider === "kokoro"
|
|
380
|
+
? "Generating narration with Kokoro (local — first run downloads ~330MB)..."
|
|
381
|
+
: "Generating narration with ElevenLabs...");
|
|
382
|
+
const tts = await resolution.call(narrationText, {
|
|
383
|
+
voice: opts.voice,
|
|
384
|
+
onProgress: (event) => {
|
|
385
|
+
if (event.status === "progress" && typeof event.progress === "number") {
|
|
386
|
+
opts.onProgress?.(`Kokoro model: ${event.file ?? ""} ${Math.round(event.progress)}%`);
|
|
387
|
+
}
|
|
388
|
+
},
|
|
389
|
+
});
|
|
337
390
|
if (!tts.success || !tts.audioBuffer) {
|
|
338
|
-
return errResult(
|
|
391
|
+
return errResult(`${resolution.provider} TTS failed: ${tts.error ?? "unknown error"}`);
|
|
339
392
|
}
|
|
340
|
-
audioRelPath = `assets/narration-${id}.
|
|
393
|
+
audioRelPath = `assets/narration-${id}.${resolution.audioExtension}`;
|
|
341
394
|
audioAbsPath = resolve(projectDir, audioRelPath);
|
|
342
395
|
await mkdir(dirname(audioAbsPath), { recursive: true });
|
|
343
396
|
await writeFile(audioAbsPath, tts.audioBuffer);
|
|
@@ -348,6 +401,47 @@ export async function executeSceneAdd(opts) {
|
|
|
348
401
|
narrationDuration = undefined;
|
|
349
402
|
}
|
|
350
403
|
}
|
|
404
|
+
// -- Whisper word-level transcribe --------------------------------------
|
|
405
|
+
// Auto-runs whenever we have audio + an OpenAI key. The transcript JSON
|
|
406
|
+
// mirrors the Hyperframes shape so a future emitSceneHtml (C5) can drive
|
|
407
|
+
// GSAP word-sync from it. Failure is non-fatal — narration still plays,
|
|
408
|
+
// we just lose word-level animation timing.
|
|
409
|
+
let transcriptRelPath;
|
|
410
|
+
let transcriptWordCount;
|
|
411
|
+
let transcriptWords;
|
|
412
|
+
if (audioAbsPath && !opts.skipTranscribe) {
|
|
413
|
+
const whisperKey = await getApiKey("OPENAI_API_KEY", "OpenAI");
|
|
414
|
+
if (!whisperKey) {
|
|
415
|
+
opts.onProgress?.("Skipping transcribe (OPENAI_API_KEY not set — narration plays but word-sync unavailable)");
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
opts.onProgress?.("Transcribing narration (Whisper word-level)...");
|
|
419
|
+
try {
|
|
420
|
+
const whisper = new WhisperProvider();
|
|
421
|
+
await whisper.initialize({ apiKey: whisperKey });
|
|
422
|
+
const audioBytes = await readFile(audioAbsPath);
|
|
423
|
+
const audioBlob = new Blob([new Uint8Array(audioBytes)]);
|
|
424
|
+
const transcript = await whisper.transcribe(audioBlob, undefined, {
|
|
425
|
+
granularity: "word",
|
|
426
|
+
language: opts.transcribeLanguage,
|
|
427
|
+
});
|
|
428
|
+
if (transcript.status === "completed" && transcript.words?.length) {
|
|
429
|
+
transcriptRelPath = `assets/transcript-${id}.json`;
|
|
430
|
+
const transcriptAbs = resolve(projectDir, transcriptRelPath);
|
|
431
|
+
await writeFile(transcriptAbs, JSON.stringify(transcript.words, null, 2), "utf-8");
|
|
432
|
+
transcriptWordCount = transcript.words.length;
|
|
433
|
+
transcriptWords = transcript.words.map((w) => ({ text: w.text, start: w.start, end: w.end }));
|
|
434
|
+
}
|
|
435
|
+
else if (transcript.status === "failed") {
|
|
436
|
+
opts.onProgress?.(`Transcribe failed: ${transcript.error ?? "unknown error"}`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
catch (error) {
|
|
440
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
441
|
+
opts.onProgress?.(`Transcribe failed: ${msg}`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
351
445
|
// -- Asset generation: backdrop image -----------------------------------
|
|
352
446
|
let imageRelPath;
|
|
353
447
|
let imageAbsPath;
|
|
@@ -424,6 +518,7 @@ export async function executeSceneAdd(opts) {
|
|
|
424
518
|
kicker: opts.kicker,
|
|
425
519
|
imagePath: imageRelPath,
|
|
426
520
|
audioPath: audioRelPath,
|
|
521
|
+
transcript: transcriptWords,
|
|
427
522
|
});
|
|
428
523
|
await mkdir(dirname(scenePath), { recursive: true });
|
|
429
524
|
await writeFile(scenePath, sceneHtml, "utf-8");
|
|
@@ -432,6 +527,7 @@ export async function executeSceneAdd(opts) {
|
|
|
432
527
|
const start = nextSceneStart(rootHtmlBefore);
|
|
433
528
|
const updated = insertClipIntoRoot(rootHtmlBefore, { id, start, duration });
|
|
434
529
|
await writeFile(rootPath, updated, "utf-8");
|
|
530
|
+
const transcriptAbsPath = transcriptRelPath ? resolve(projectDir, transcriptRelPath) : undefined;
|
|
435
531
|
return {
|
|
436
532
|
success: true,
|
|
437
533
|
id,
|
|
@@ -442,6 +538,8 @@ export async function executeSceneAdd(opts) {
|
|
|
442
538
|
rootPath: relative(process.cwd(), rootPath) || rootPath,
|
|
443
539
|
audioPath: audioAbsPath ? (relative(process.cwd(), audioAbsPath) || audioAbsPath) : undefined,
|
|
444
540
|
imagePath: imageAbsPath ? (relative(process.cwd(), imageAbsPath) || imageAbsPath) : undefined,
|
|
541
|
+
transcriptPath: transcriptAbsPath ? (relative(process.cwd(), transcriptAbsPath) || transcriptAbsPath) : undefined,
|
|
542
|
+
transcriptWordCount,
|
|
445
543
|
};
|
|
446
544
|
}
|
|
447
545
|
// ---------------------------------------------------------------------------
|