feat: expand intelligent investigation workspace
This commit is contained in:
+54
-17
@@ -1,7 +1,10 @@
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import { Communicate } from "edge-tts-ts";
|
||||
import { EdgeTTS } from "node-edge-tts";
|
||||
import { speechVoiceIdSchema, type SpeechVoice } from "../src/shared/tts/voices";
|
||||
import { createEdgeTtsVoiceCatalog } from "./edge-tts-voices";
|
||||
|
||||
const EDGE_TTS_PATH = "/api/tts/edge";
|
||||
const EDGE_TTS_VOICES_PATH = `${EDGE_TTS_PATH}/voices`;
|
||||
const DEFAULT_VOICE = "zh-CN-XiaoxiaoNeural";
|
||||
const MAX_REQUEST_BYTES = 8 * 1024;
|
||||
const MAX_TEXT_LENGTH = 520;
|
||||
@@ -32,6 +35,7 @@ export type EdgeTtsMiddlewareOptions = {
|
||||
synthesize?: EdgeTtsSynthesizer;
|
||||
timeoutMs?: number;
|
||||
trustProxy?: boolean;
|
||||
listVoices?: () => Promise<SpeechVoice[]>;
|
||||
};
|
||||
|
||||
function sendJson(response: ServerResponse, status: number, message: string) {
|
||||
@@ -76,14 +80,15 @@ function getClientKey(request: IncomingMessage, trustProxy: boolean) {
|
||||
}
|
||||
|
||||
export async function synthesizeEdgeSpeech(text: string, voice: string): Promise<Uint8Array> {
|
||||
const communicate = new Communicate(text, { voice });
|
||||
const communicate = new EdgeTTS({ voice, timeout: DEFAULT_TIMEOUT_MS });
|
||||
const chunks: Uint8Array[] = [];
|
||||
let byteLength = 0;
|
||||
|
||||
for await (const chunk of communicate.stream()) {
|
||||
if (chunk.type !== "audio") continue;
|
||||
chunks.push(chunk.data);
|
||||
byteLength += chunk.data.byteLength;
|
||||
for await (const chunk of communicate.streamAudio(text)) {
|
||||
if (!Buffer.isBuffer(chunk)) continue;
|
||||
chunks.push(chunk);
|
||||
byteLength += chunk.byteLength;
|
||||
if (byteLength > MAX_AUDIO_BYTES) throw new Error("语音服务返回的音频过大");
|
||||
}
|
||||
|
||||
if (!byteLength) throw new Error("语音服务返回了空音频");
|
||||
@@ -99,27 +104,32 @@ export async function synthesizeEdgeSpeech(text: string, voice: string): Promise
|
||||
|
||||
export function createEdgeTtsMiddleware(options: EdgeTtsMiddlewareOptions = {}) {
|
||||
const synthesize = options.synthesize ?? synthesizeEdgeSpeech;
|
||||
const defaultVoice = options.defaultVoice ?? process.env.EDGE_TTS_VOICE ?? DEFAULT_VOICE;
|
||||
const configuredVoice = options.defaultVoice ?? process.env.EDGE_TTS_VOICE ?? DEFAULT_VOICE;
|
||||
const defaultVoice = speechVoiceIdSchema.safeParse(configuredVoice).success ? configuredVoice : DEFAULT_VOICE;
|
||||
const maxConcurrent = options.maxConcurrent ?? DEFAULT_MAX_CONCURRENT;
|
||||
const maxRequestsPerWindow = options.maxRequestsPerWindow ?? DEFAULT_RATE_LIMIT;
|
||||
const rateWindowMs = options.rateWindowMs ?? DEFAULT_RATE_WINDOW_MS;
|
||||
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
||||
const trustProxy = options.trustProxy ?? false;
|
||||
const now = options.now ?? Date.now;
|
||||
const listVoices = createEdgeTtsVoiceCatalog(options.listVoices, now);
|
||||
const rateBuckets = new Map<string, RateBucket>();
|
||||
let activeRequests = 0;
|
||||
|
||||
return async (request: IncomingMessage, response: ServerResponse, next?: () => void) => {
|
||||
if (request.url?.split("?", 1)[0] !== EDGE_TTS_PATH) {
|
||||
const path = request.url?.split("?", 1)[0];
|
||||
const voicesRequest = path === EDGE_TTS_VOICES_PATH;
|
||||
if (path !== EDGE_TTS_PATH && !voicesRequest) {
|
||||
next?.();
|
||||
return;
|
||||
}
|
||||
if (request.method !== "POST") {
|
||||
response.setHeader("Allow", "POST");
|
||||
sendJson(response, 405, "仅支持 POST 请求");
|
||||
const method = voicesRequest ? "GET" : "POST";
|
||||
if (request.method !== method) {
|
||||
response.setHeader("Allow", method);
|
||||
sendJson(response, 405, `仅支持 ${method} 请求`);
|
||||
return;
|
||||
}
|
||||
if (!request.headers["content-type"]?.toLowerCase().startsWith("application/json")) {
|
||||
if (!voicesRequest && !request.headers["content-type"]?.toLowerCase().startsWith("application/json")) {
|
||||
sendJson(response, 415, "请求必须使用 application/json");
|
||||
return;
|
||||
}
|
||||
@@ -143,6 +153,19 @@ export function createEdgeTtsMiddleware(options: EdgeTtsMiddlewareOptions = {})
|
||||
return;
|
||||
}
|
||||
|
||||
if (voicesRequest) {
|
||||
try {
|
||||
const voices = await listVoices();
|
||||
response.statusCode = 200;
|
||||
response.setHeader("Content-Type", "application/json; charset=utf-8");
|
||||
response.setHeader("Cache-Control", "no-store");
|
||||
response.end(JSON.stringify({ voices, defaultVoice }));
|
||||
} catch {
|
||||
sendJson(response, 503, "音色列表暂时不可用,请稍后重试");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let payload: EdgeTtsPayload;
|
||||
try {
|
||||
payload = await readJson(request);
|
||||
@@ -151,10 +174,6 @@ export function createEdgeTtsMiddleware(options: EdgeTtsMiddlewareOptions = {})
|
||||
return;
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(payload, "voice")) {
|
||||
sendJson(response, 400, "语音角色只能由服务端配置");
|
||||
return;
|
||||
}
|
||||
const text = typeof payload.text === "string" ? payload.text.trim() : "";
|
||||
if (!text) {
|
||||
sendJson(response, 400, "缺少待朗读文本");
|
||||
@@ -164,6 +183,24 @@ export function createEdgeTtsMiddleware(options: EdgeTtsMiddlewareOptions = {})
|
||||
sendJson(response, 413, `单次文本不能超过 ${MAX_TEXT_LENGTH} 字`);
|
||||
return;
|
||||
}
|
||||
let voice = defaultVoice;
|
||||
if (Object.prototype.hasOwnProperty.call(payload, "voice")) {
|
||||
const selectedVoice = speechVoiceIdSchema.safeParse(payload.voice);
|
||||
if (!selectedVoice.success) {
|
||||
sendJson(response, 400, "请选择有效音色");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (!(await listVoices()).some((item) => item.id === selectedVoice.data)) {
|
||||
sendJson(response, 400, "所选音色不可用,请重新选择");
|
||||
return;
|
||||
}
|
||||
voice = selectedVoice.data;
|
||||
} catch {
|
||||
sendJson(response, 503, "无法确认音色是否可用,请稍后重试");
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (activeRequests >= maxConcurrent) {
|
||||
response.setHeader("Retry-After", "1");
|
||||
sendJson(response, 429, "语音服务繁忙,请稍后重试");
|
||||
@@ -171,7 +208,7 @@ export function createEdgeTtsMiddleware(options: EdgeTtsMiddlewareOptions = {})
|
||||
}
|
||||
|
||||
activeRequests += 1;
|
||||
const synthesisTask = Promise.resolve().then(() => synthesize(text, defaultVoice));
|
||||
const synthesisTask = Promise.resolve().then(() => synthesize(text, voice));
|
||||
void synthesisTask.finally(() => {
|
||||
activeRequests -= 1;
|
||||
}).catch(() => undefined);
|
||||
|
||||
Reference in New Issue
Block a user