From 2a446f8a6111104415648fcb245ac48858f682db Mon Sep 17 00:00:00 2001 From: itsrubberduck Date: Mon, 15 Sep 2025 22:43:07 +0200 Subject: [PATCH] USE_PIPER --- server/api/atc/say.post.ts | 130 ++++++++++++++++++++++--------------- 1 file changed, 79 insertions(+), 51 deletions(-) diff --git a/server/api/atc/say.post.ts b/server/api/atc/say.post.ts index df5f6e4..2101036 100644 --- a/server/api/atc/say.post.ts +++ b/server/api/atc/say.post.ts @@ -1,32 +1,68 @@ -// server/api/atc/say.post.ts - Simplified Version ohne FFmpeg -import { createError, readBody } from "h3"; -import { writeFile, readFile, mkdir } from "node:fs/promises"; -import { existsSync } from "node:fs"; -import { join } from "node:path"; -import { randomUUID } from "node:crypto"; -import { openai, TTS_MODEL, normalizeATC } from "../../utils/openai"; +// server/api/atc/say.post.ts +import {createError, readBody} from "h3"; +import {writeFile, mkdir} from "node:fs/promises"; +import {existsSync} from "node:fs"; +import {join} from "node:path"; +import {randomUUID} from "node:crypto"; +import {openai, TTS_MODEL, normalizeATC} from "../../utils/openai"; +import {request} from "node:http"; + +// dotenv config +import {config} from "dotenv"; + +config(); + function outDir() { - const base = process.env.ATC_OUT_DIR?.trim() || join(process.cwd(), "storage", "atc"); - return base; + return process.env.ATC_OUT_DIR?.trim() || join(process.cwd(), "storage", "atc"); } async function ensureDir(p: string) { - if (!existsSync(p)) await mkdir(p, { recursive: true }); + if (!existsSync(p)) await mkdir(p, {recursive: true}); } -// Einfache Radio-Effekt-Simulation über Gain-Reduktion -function simulateRadioQuality(level: number): { gain: number; description: string } { - switch(level) { - case 5: return { gain: 1.0, description: "crystal clear" }; - case 4: return { gain: 0.9, description: "very good" }; - case 3: return { gain: 0.8, description: "good" }; - case 2: return { gain: 0.7, description: "poor" }; - case 1: return { gain: 0.6, description: "very poor" }; - default: return { gain: 0.8, description: "standard" }; +function simulateRadioQuality(level: number) { + switch (level) { + case 5: + return {gain: 1.0, description: "crystal clear"}; + case 4: + return {gain: 0.9, description: "very good"}; + case 3: + return {gain: 0.8, description: "good"}; + case 2: + return {gain: 0.7, description: "poor"}; + case 1: + return {gain: 0.6, description: "very poor"}; + default: + return {gain: 0.8, description: "standard"}; } } +// ---- Piper HTTP helper ---- +async function piperTTS(text: string, voice: string): Promise { + return new Promise((resolve, reject) => { + const req = request( + { + hostname: "localhost", + port: 5001, + path: "/", + method: "POST", + headers: { + "Content-Type": "application/json" + } + }, + (res) => { + const data: Buffer[] = []; + res.on("data", (chunk) => data.push(chunk)); + res.on("end", () => resolve(Buffer.concat(data))); + } + ); + req.on("error", reject); + req.write(JSON.stringify({text, voice})); + req.end(); + }); +} + export default defineEventHandler(async (event) => { const body = await readBody<{ text?: string; @@ -39,45 +75,44 @@ export default defineEventHandler(async (event) => { }>(event); const raw = (body?.text || "").trim(); - if (!raw) throw createError({ statusCode: 400, statusMessage: "text required" }); + if (!raw) throw createError({statusCode: 400, statusMessage: "text required"}); const level = Math.max(1, Math.min(5, Math.floor(body?.level ?? 4))); - const voice = (body?.voice || "alloy") as string; + const voice = (body?.voice || "alloy").trim(); const speed = Math.max(0.5, Math.min(2.0, body?.speed || 1.0)); - // ATC-Normalisierung für realistischen Funkspruch const normalized = normalizeATC(raw); - if (!normalized) throw createError({ statusCode: 400, statusMessage: "normalized text empty" }); + if (!normalized) throw createError({statusCode: 400, statusMessage: "normalized text empty"}); - // Radio-Qualität simulieren const radioQuality = simulateRadioQuality(level); - - // Eindeutige ID und Pfade const id = randomUUID(); const timestamp = new Date().toISOString(); const dateFolder = timestamp.slice(0, 10); - const baseDir = join(outDir(), dateFolder); const fileWav = join(baseDir, `${id}.wav`); const fileJson = join(baseDir, `${id}.json`); try { - // OpenAI TTS direkt als WAV (keine weitere Verarbeitung) - const tts = await openai.audio.speech.create({ - model: TTS_MODEL, - voice, - format: "wav", - input: normalized, - speed - }); + let audioBuffer: Buffer; - const audioBuffer = Buffer.from(await tts.arrayBuffer()); + if (process.env.USE_PIPER?.toLowerCase() === "true") { + // --- Lokaler Piper-Server --- + audioBuffer = await piperTTS(normalized, voice); + } else { + // --- OpenAI Fallback --- + const tts = await openai.audio.speech.create({ + model: TTS_MODEL, + voice, + format: "wav", + input: normalized, + speed + }); + audioBuffer = Buffer.from(await tts.arrayBuffer()); + } - // Speichern await ensureDir(baseDir); await writeFile(fileWav, audioBuffer); - // Metadaten speichern const meta = { id, createdAt: timestamp, @@ -90,19 +125,13 @@ export default defineEventHandler(async (event) => { tag: body?.tag || null, moduleId: body?.moduleId || null, lessonId: body?.lessonId || null, - files: { - wav: fileWav, - }, - model: TTS_MODEL, - format: "audio/wav", - note: "Direct OpenAI TTS without post-processing" + files: {wav: fileWav}, + model: process.env.USE_PIPER?.toLowerCase() === "true" ? "piper-local" : TTS_MODEL, + format: "audio/wav" }; await writeFile(fileJson, JSON.stringify(meta, null, 2), "utf-8"); - // Audio als Base64 für sofortige Wiedergabe - const audioBase64 = audioBuffer.toString("base64"); - return { success: true, id, @@ -114,7 +143,7 @@ export default defineEventHandler(async (event) => { radioQuality: radioQuality.description, audio: { mime: "audio/wav", - base64: audioBase64, + base64: audioBuffer.toString("base64"), size: audioBuffer.length }, stored: { @@ -124,11 +153,10 @@ export default defineEventHandler(async (event) => { }, meta }; - - } catch (error) { + } catch (err) { throw createError({ statusCode: 500, - statusMessage: `TTS generation failed: ${error}` + statusMessage: `TTS generation failed: ${err}` }); } });