e261f4a346
Reduce median scene-load latency from ~30-80s to ~17-25s by switching image generation to Runware FLUX.2 [klein] 9B KV and moving per-beat TTS synthesis off the scene response into a new lazy /api/beat-audio endpoint with hard timeout + abort support.
- feat(image): migrate to Runware FLUX.2 [klein] 9B KV — task-array API, $0.001/image, sub-second inference.
- feat(tts): split /api/scene into directScene + image + voicedesign-provisioning; lazily synth per beat via /api/beat-audio with 15s hard timeout + AbortSignal threaded to MiMo so timed-out calls don't keep burning sockets/quota; client fans out per-beat fetches on scene-id change with abort + identity-check finally to prevent cross-scene beat-id collisions.
- refactor(tts): slim BeatAudioRequest to { beat, voice } — ~800KB per-beat upload dropped to ~160KB by sending only the speaker's voice instead of the full session.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
75 lines
1.8 KiB
TypeScript
75 lines
1.8 KiB
TypeScript
import type { ProviderConfig } from "@yume/types";
|
|
import { fetchWithRetry } from "./fetchWithRetry";
|
|
|
|
// Runware uses its own task-array protocol (not OpenAI-compatible).
|
|
// POST <baseUrl> with [{ taskType: "imageInference", ... }]; errors come
|
|
// back as a 200 with `errors[]`, so we have to inspect the body either way.
|
|
type RunwareImageResult = {
|
|
imageBase64Data?: string;
|
|
};
|
|
type RunwareError = {
|
|
code?: string;
|
|
message?: string;
|
|
parameter?: string;
|
|
};
|
|
type RunwareResponse = {
|
|
data?: RunwareImageResult[];
|
|
errors?: RunwareError[];
|
|
};
|
|
|
|
export async function generateImage(
|
|
config: ProviderConfig,
|
|
prompt: string,
|
|
): Promise<string> {
|
|
const url = config.baseUrl.replace(/\/$/, "");
|
|
|
|
const body = [
|
|
{
|
|
taskType: "imageInference",
|
|
taskUUID: crypto.randomUUID(),
|
|
model: config.model,
|
|
positivePrompt: prompt,
|
|
width: 1792,
|
|
height: 1024,
|
|
steps: 4,
|
|
CFGScale: 3.5,
|
|
numberResults: 1,
|
|
outputType: "base64Data",
|
|
outputFormat: "PNG",
|
|
},
|
|
];
|
|
|
|
const res = await fetchWithRetry(url, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: `Bearer ${config.apiKey}`,
|
|
},
|
|
body: JSON.stringify(body),
|
|
});
|
|
|
|
const text = await res.text();
|
|
let json: RunwareResponse;
|
|
try {
|
|
json = JSON.parse(text) as RunwareResponse;
|
|
} catch {
|
|
throw new Error(`Image API error ${res.status}: ${text.slice(0, 500)}`);
|
|
}
|
|
|
|
if (json.errors?.length) {
|
|
const e = json.errors[0]!;
|
|
throw new Error(
|
|
`Runware error [${e.code ?? "unknown"}]: ${e.message ?? "no message"}` +
|
|
(e.parameter ? ` (parameter: ${e.parameter})` : ""),
|
|
);
|
|
}
|
|
|
|
const b64 = json.data?.[0]?.imageBase64Data;
|
|
if (!b64) {
|
|
throw new Error(
|
|
`No image in Runware response: ${text.slice(0, 300)}`,
|
|
);
|
|
}
|
|
return b64;
|
|
}
|