mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
* feat(sse): add EdgeTTS audio-tts provider (#6668) Registers Microsoft Edge "Read Aloud" as a new no-API-key AUDIO_SPEECH_PROVIDERS entry — the first WebSocket-transport TTS provider in the registry. Reverse- engineered/unofficial endpoint, same class of integration already accepted for other "-web" style providers (chatgpt-web.ts, copilot-web.ts). - open-sse/executors/edgeTts.ts: pure Sec-MS-GEC token construction (SHA-256 over a public trusted-client-token + rounded Windows file-time ticks, ported from rany2/edge-tts drm.py), WS message framing (speech.config/ssml), binary-chunk demuxing, SSML building/escaping, and the WS synth call itself (injectable WebSocket ctor for tests, lazy `import("ws")` in production so it never enters esbuild's top-level CJS bundle graph). Per-client-IP sliding-window throttle (SlidingWindowLimiter) since there's no per-user key — one abusive deployment could otherwise get the shared trusted token rate-limited for everyone. - open-sse/utils/publicCreds.ts: embeds the trusted-client-token via resolvePublicCred() (Hard Rule #11) — it's a constant hardcoded in every Edge build and every open-source edge-tts port, not a per-user secret. - Extracted open-sse/utils/audioResponse.ts (shared response helpers) and open-sse/executors/awsPollyTts.ts (AWS Polly handler) out of open-sse/handlers/audioSpeech.ts to stay under its frozen file-size ratchet baseline while making room for the new branch — no behavior change to either extracted piece. - src/app/api/v1/audio/speech/route.ts: thread the caller's IP through to the handler for the new throttle. Tests: tests/unit/edgetts-provider.test.ts (23 cases) — Sec-MS-GEC determinism and cross-check against a hand-derived reference vector, message framing, binary demux, SSML escaping/injection-safety, registry lookup, publicCreds shape, and the error path via an injected fake WebSocket (upstream failure -> sanitized 502, no stack/path leak; Hard Rule #12), plus the per-IP rate limit. No live upstream is required or used — the reverse-engineered protocol can't be validated against real credentials, but every pure/testable seam is covered per the TDD path in the bug/feature validation gate. * test(mutation): register edgetts-provider.test.ts in stryker tap.testFiles (#6668) The new provider's unit test covers a mutated module, so the strict mutation-test-coverage gate requires it in stryker.conf.json's tap.testFiles. Single-line addition (kept the file's existing formatting).
66 lines
1.9 KiB
TypeScript
66 lines
1.9 KiB
TypeScript
/**
|
|
* Shared audio/speech HTTP response helpers.
|
|
*
|
|
* Extracted from `open-sse/handlers/audioSpeech.ts` so that both the handler
|
|
* and any provider-specific adapter modules extracted alongside it (e.g.
|
|
* `open-sse/executors/awsPollyTts.ts`) can share the same response-shaping
|
|
* logic without importing from the (frozen, file-size-ratcheted) handler
|
|
* itself — which would create a circular import.
|
|
*/
|
|
import { CORS_HEADERS } from "./cors.ts";
|
|
|
|
/**
|
|
* Pull a human-readable error message out of a parsed upstream JSON error body.
|
|
*/
|
|
function extractUpstreamErrorMessage(parsed) {
|
|
const detail = parsed?.detail;
|
|
const candidates = [
|
|
parsed?.err_msg,
|
|
parsed?.error?.message,
|
|
typeof parsed?.error === "string" ? parsed.error : null,
|
|
parsed?.message,
|
|
typeof detail === "string" ? detail : detail?.message,
|
|
];
|
|
|
|
const raw = candidates.find(Boolean);
|
|
return raw ? String(raw) : null;
|
|
}
|
|
|
|
/**
|
|
* Return a CORS error response from an upstream fetch failure.
|
|
*/
|
|
export function upstreamErrorResponse(res: Response, errText: string): Response {
|
|
// Always return JSON so the client can detect 401/credential errors reliably
|
|
let errorMessage: string;
|
|
try {
|
|
const parsed = JSON.parse(errText);
|
|
errorMessage =
|
|
extractUpstreamErrorMessage(parsed) || errText || `Upstream error (${res.status})`;
|
|
} catch {
|
|
errorMessage = errText || `Upstream error (${res.status})`;
|
|
}
|
|
|
|
return Response.json(
|
|
{ error: { message: errorMessage, code: res.status } },
|
|
{
|
|
status: res.status,
|
|
headers: { ...CORS_HEADERS },
|
|
}
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Return a CORS audio stream response.
|
|
*/
|
|
export function audioStreamResponse(res: Response, defaultContentType = "audio/mpeg"): Response {
|
|
const contentType = res.headers.get("content-type") || defaultContentType;
|
|
return new Response(res.body, {
|
|
status: 200,
|
|
headers: {
|
|
...CORS_HEADERS,
|
|
"Content-Type": contentType,
|
|
"Transfer-Encoding": "chunked",
|
|
},
|
|
});
|
|
}
|