mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-01 06:06:05 +08:00
USE_PIPER
This commit is contained in:
@@ -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<Buffer> {
|
||||
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}`
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user