evOo-local/packages/testkit/src/fake-kokoro.ts
cynarAI 8ae3764b05
Speak German through a local Piper server, since Kokoro has no German voices (#182)
Kokoro-FastAPI ships English, Spanish, French, Hindi, Italian, Japanese,
Portuguese and Chinese voices only. The German presets pointed at voice
ids no Kokoro server has, so German users either got English voices or
picked a voice that failed to speak.

The local voice now also lists the German voices whose Piper model is
installed (PIPER_BASE_URL) and speaks them through Piper's /synthesize
as WAV. Thorsten, the clearest model, is the German default. Mobile
plays WAV as well as MP3, and the E2E voice stub stands in for Piper.


Claude-Session: https://claude.ai/code/session_01KWEAjTw26eRf3ZXTXfRjmD

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-09 11:55:51 +02:00

47 lines
1.8 KiB
TypeScript

import { createServer } from "node:http";
import type { AddressInfo } from "node:net";
/**
* Stands in for the local Kokoro and Piper speech servers so onboarding's voice step never
* depends on whatever happens to listen on the default ports of the machine running the suite.
*/
export async function startFakeKokoro(): Promise<{ baseUrl: string; close: () => Promise<void> }> {
const server = createServer((req, res) => {
if (req.method === "GET" && req.url === "/v1/audio/voices") {
res.writeHead(200, { "content-type": "application/json" });
// An empty list makes the adapter fall back to its curated presets.
res.end(JSON.stringify({ voices: [] }));
return;
}
if (req.method === "POST" && req.url === "/v1/audio/speech") {
req.resume();
req.on("end", () => {
// An MPEG frame header is enough for callers that only check the content type.
res.writeHead(200, { "content-type": "audio/mpeg" });
res.end(Buffer.from([0xff, 0xfb, 0x90, 0x64, 0, 0, 0, 0]));
});
return;
}
if (req.method === "GET" && req.url === "/voices") {
res.writeHead(200, { "content-type": "application/json" });
res.end(JSON.stringify({ "de_DE-thorsten-high": {}, "de_DE-eva_k-x_low": {} }));
return;
}
if (req.method === "POST" && req.url === "/synthesize") {
req.resume();
req.on("end", () => {
res.writeHead(200, { "content-type": "audio/wav" });
res.end(Buffer.from("RIFF\0\0\0\0WAVE"));
});
return;
}
res.writeHead(404).end();
});
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
const { port } = server.address() as AddressInfo;
return {
baseUrl: `http://127.0.0.1:${port}`,
close: () => new Promise((resolve) => server.close(() => resolve())),
};
}