mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-05 06:42:12 +03:00
refactor(sse): declare the ArrayBuffer backing on media byte producers (#8665)
Six declarations spell a byte buffer as bare `Buffer` / `Uint8Array`. Without its type argument that widens to `ArrayBufferLike`, which also admits `SharedArrayBuffer` — so the value is rejected at every Web API boundary it is actually passed to: `BodyInit` for `new Response(...)` and `BlobPart` for `new Blob([...])`. Every one of them is already ArrayBuffer-backed at runtime. `hexToBytes()` allocates with `new Uint8Array(len)`; `synthesizeGtts()` and `pcmToWav()` return `Buffer.concat(...)`; `fetchRemoteImage()` returns `Buffer.from(await response.arrayBuffer())`; `readPageResponseBody()` returns `Buffer.from(body)`, which copies. The declarations were simply less specific than the values, so this states what the code already guarantees. Same fix #8533 applied to the multipart and gRPC-web bodies. Fixes 5 of the 208 `tsc -p open-sse/tsconfig.json` diagnostics with no new ones: 3 in audioSpeech.ts (MiniMax hex, gTTS, Vertex Gemini TTS), 1 in imageGeneration.ts (Topaz Blob upload) and 1 in browserBackedChat.ts. Refs #8484
This commit is contained in:
@@ -198,7 +198,7 @@ async function synthesizeGttsChunk(
|
||||
export async function synthesizeGtts(
|
||||
input: GttsSynthInput,
|
||||
fetchImpl: FetchLike = fetch
|
||||
): Promise<Buffer> {
|
||||
): Promise<Buffer<ArrayBuffer>> {
|
||||
const lang = normalizeGttsLang(input.lang);
|
||||
const tld = (typeof input.tld === "string" && input.tld.trim()) || DEFAULT_TLD;
|
||||
const chunks = chunkGttsText(input.text);
|
||||
|
||||
@@ -131,7 +131,12 @@ async function vertexError(res: Response): Promise<VertexHttpError> {
|
||||
}
|
||||
|
||||
/** Wrap raw little-endian 16-bit PCM mono samples in a minimal WAV container. */
|
||||
export function pcmToWav(pcm: Buffer, sampleRate = 24000, channels = 1, bitsPerSample = 16): Buffer {
|
||||
export function pcmToWav(
|
||||
pcm: Buffer,
|
||||
sampleRate = 24000,
|
||||
channels = 1,
|
||||
bitsPerSample = 16
|
||||
): Buffer<ArrayBuffer> {
|
||||
const blockAlign = (channels * bitsPerSample) / 8;
|
||||
const byteRate = sampleRate * blockAlign;
|
||||
const header = Buffer.alloc(44);
|
||||
@@ -190,7 +195,7 @@ function extractText(data: unknown): string {
|
||||
export async function vertexGenerateSpeech(
|
||||
credentials: VertexMediaCredentials,
|
||||
options: { model: string; input: string; voice?: string }
|
||||
): Promise<{ audio: Buffer; contentType: string }> {
|
||||
): Promise<{ audio: Buffer<ArrayBuffer>; contentType: string }> {
|
||||
const auth = await resolveVertexAuth(credentials);
|
||||
const { url, headers } = buildModelRequest(auth, options.model, "generateContent");
|
||||
const payload = {
|
||||
|
||||
@@ -618,7 +618,7 @@ async function handleXiaomiMimoSpeech(providerConfig, body, modelId, token, cred
|
||||
* `base_resp.status_code` (0 = success).
|
||||
* Port of decolua/9router#1043 by toanalien <toanalien@gmail.com>.
|
||||
*/
|
||||
function hexToBytes(audioHex) {
|
||||
function hexToBytes(audioHex): Uint8Array<ArrayBuffer> {
|
||||
const clean = typeof audioHex === "string" ? audioHex.trim() : "";
|
||||
if (!clean) throw new Error("MiniMax TTS returned no audio");
|
||||
if (clean.length % 2 !== 0 || !/^[0-9a-f]+$/i.test(clean)) {
|
||||
@@ -684,7 +684,7 @@ async function handleMinimaxSpeech(providerConfig, body, modelId, token) {
|
||||
}
|
||||
|
||||
const audioField = (data.data as Record<string, unknown> | undefined)?.audio;
|
||||
let bytes: Uint8Array;
|
||||
let bytes: Uint8Array<ArrayBuffer>;
|
||||
try {
|
||||
bytes = hexToBytes(audioField);
|
||||
} catch (err) {
|
||||
|
||||
@@ -502,7 +502,7 @@ export function __resetBrowserPoolMetricsForTest(): void {
|
||||
|
||||
export async function readPageResponseBody(
|
||||
response: import("playwright").Response
|
||||
): Promise<{ status: number; headers: Record<string, string>; body: Buffer }> {
|
||||
): Promise<{ status: number; headers: Record<string, string>; body: Buffer<ArrayBuffer> }> {
|
||||
const headers: Record<string, string> = {};
|
||||
for (const [name, value] of Object.entries(response.headers())) {
|
||||
headers[name] = value;
|
||||
|
||||
@@ -39,7 +39,7 @@ export interface RemoteImageFetchOptions {
|
||||
}
|
||||
|
||||
export interface RemoteImageFetchResult {
|
||||
buffer: Buffer;
|
||||
buffer: Buffer<ArrayBuffer>;
|
||||
contentType: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
80
tests/unit/media-body-arraybuffer-backing.test.ts
Normal file
80
tests/unit/media-body-arraybuffer-backing.test.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { pcmToWav } from "../../open-sse/executors/vertexMedia.ts";
|
||||
import { synthesizeGtts } from "../../open-sse/executors/gtts.ts";
|
||||
import { readPageResponseBody } from "../../open-sse/services/browserPool.ts";
|
||||
import { fetchRemoteImage } from "@/shared/network/remoteImageFetch";
|
||||
|
||||
/**
|
||||
* Each producer below is handed straight to a Web API that only accepts an
|
||||
* `ArrayBuffer`-backed view — `new Response(body)` (BodyInit) or
|
||||
* `new Blob([part])` (BlobPart). A `Buffer` / `Uint8Array` written without its
|
||||
* type argument widens to `ArrayBufferLike`, which also admits
|
||||
* `SharedArrayBuffer` and is therefore rejected at that boundary.
|
||||
*
|
||||
* The existing tests for these functions assert their *contents* — the RIFF
|
||||
* header, the concatenated chunks, the decoded base64. None assert the backing,
|
||||
* which is the single property the narrowed return types exist to guarantee and
|
||||
* the one a plausible refactor to a shared or pooled allocation would break.
|
||||
*
|
||||
* So each case asserts both: the backing itself, and that the value really is
|
||||
* accepted by the Web API it is passed to in production.
|
||||
*/
|
||||
|
||||
test("pcmToWav returns an ArrayBuffer-backed WAV that new Response accepts", () => {
|
||||
const wav = pcmToWav(Buffer.from([0x01, 0x02, 0x03, 0x04]), 24000);
|
||||
|
||||
assert.ok(wav.buffer instanceof ArrayBuffer, "WAV buffer must not be SharedArrayBuffer-backed");
|
||||
// open-sse/handlers/audioSpeech.ts hands this to `new Response(audio, …)`.
|
||||
assert.doesNotThrow(() => new Response(wav));
|
||||
});
|
||||
|
||||
test("synthesizeGtts returns an ArrayBuffer-backed buffer that new Response accepts", async () => {
|
||||
// Same batchexecute envelope shape gtts-provider.test.ts builds; "aGk=" is
|
||||
// base64 for "hi".
|
||||
const inner = JSON.stringify(["aGk=", null, null, null, null, null, []]);
|
||||
const outer = JSON.stringify([["wrb.fr", "jQ1olc", inner, null, null, null, "generic"]]);
|
||||
const fetchImpl = async () =>
|
||||
new Response(`)]}'\n\n${outer.length}\n${outer}\n`, { status: 200 });
|
||||
|
||||
const audio = await synthesizeGtts({ text: "hi", lang: "en" }, fetchImpl);
|
||||
|
||||
assert.ok(audio.buffer instanceof ArrayBuffer, "gTTS audio must not be SharedArrayBuffer-backed");
|
||||
assert.doesNotThrow(() => new Response(audio));
|
||||
});
|
||||
|
||||
test("fetchRemoteImage returns an ArrayBuffer-backed buffer that new Blob accepts", async () => {
|
||||
const result = await fetchRemoteImage("https://cdn.example.com/image.png", {
|
||||
fetchImpl: async () =>
|
||||
new Response(new Uint8Array([1, 2, 3]), {
|
||||
status: 200,
|
||||
headers: { "content-type": "image/png" },
|
||||
}),
|
||||
guard: "public-only",
|
||||
lookup: async () => [{ address: "203.0.113.5", family: 4 }],
|
||||
});
|
||||
|
||||
assert.ok(result.buffer.buffer instanceof ArrayBuffer, "image bytes must be ArrayBuffer-backed");
|
||||
// open-sse/handlers/imageGeneration.ts wraps this in `new Blob([…])` for the
|
||||
// Topaz multipart upload.
|
||||
assert.doesNotThrow(() => new Blob([result.buffer], { type: "image/png" }));
|
||||
});
|
||||
|
||||
test("readPageResponseBody returns an ArrayBuffer-backed body", async () => {
|
||||
const fakeResponse = {
|
||||
status: () => 200,
|
||||
headers: () => ({ "content-type": "application/json" }),
|
||||
body: async () => Buffer.from('{"ok":true}'),
|
||||
};
|
||||
|
||||
const captured = await readPageResponseBody(
|
||||
fakeResponse as unknown as Parameters<typeof readPageResponseBody>[0]
|
||||
);
|
||||
|
||||
assert.equal(captured.status, 200);
|
||||
assert.equal(captured.body.toString(), '{"ok":true}');
|
||||
// browserBackedChat.ts assigns this into a `Buffer.alloc(0)`-seeded local
|
||||
// that it later returns as the response body.
|
||||
assert.ok(captured.body.buffer instanceof ArrayBuffer);
|
||||
});
|
||||
Reference in New Issue
Block a user