mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-01 14:16:05 +08:00
refactor: update OpenAI TTS integration and cleanup imports
- index.vue: comment out cockpit simulator image - learn.vue: remove unused imports (useRadioTTS, learnModules) - atc/say.post.ts & utils/normalize.ts: rename openaiOld → normalize, adjust TTS calls, skip ensureDir/writeFile - communicationsEngine.ts: fix atcDecisionTree import path
This commit is contained in:
@@ -1,110 +0,0 @@
|
||||
// server/api/atc/audio/[...path].get.ts
|
||||
import { createError } from "h3";
|
||||
import { readFile, stat } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { existsSync } from "node:fs";
|
||||
|
||||
function outDir() {
|
||||
const base = process.env.ATC_OUT_DIR?.trim() || join(process.cwd(), "storage", "atc");
|
||||
return base;
|
||||
}
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const path = getRouterParam(event, 'path');
|
||||
|
||||
if (!path || typeof path !== 'string') {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Invalid path"
|
||||
});
|
||||
}
|
||||
|
||||
// Sicherheitscheck: verhindere Directory Traversal
|
||||
if (path.includes('..') || path.includes('/./') || path.startsWith('/')) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Invalid path"
|
||||
});
|
||||
}
|
||||
|
||||
const filePath = join(outDir(), path);
|
||||
|
||||
if (!existsSync(filePath)) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: "Audio file not found"
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const stats = await stat(filePath);
|
||||
|
||||
if (!stats.isFile()) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Path is not a file"
|
||||
});
|
||||
}
|
||||
|
||||
// Nur Audio-Dateien servieren
|
||||
const allowedExtensions = ['.wav', '.mp3', '.ogg'];
|
||||
const hasValidExtension = allowedExtensions.some(ext => filePath.toLowerCase().endsWith(ext));
|
||||
|
||||
if (!hasValidExtension) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Not an audio file"
|
||||
});
|
||||
}
|
||||
|
||||
const fileBuffer = await readFile(filePath);
|
||||
|
||||
// MIME Type basierend auf Dateiendung
|
||||
let mimeType = 'audio/ogg';
|
||||
if (filePath.endsWith('.wav')) {
|
||||
mimeType = 'audio/wav';
|
||||
} else if (filePath.endsWith('.mp3')) {
|
||||
mimeType = 'audio/mpeg';
|
||||
} else if (filePath.endsWith('.ogg')) {
|
||||
mimeType = 'audio/ogg; codecs=opus';
|
||||
}
|
||||
|
||||
// HTTP Headers für Audio-Streaming
|
||||
setHeader(event, 'Content-Type', mimeType);
|
||||
setHeader(event, 'Content-Length', stats.size.toString());
|
||||
setHeader(event, 'Accept-Ranges', 'bytes');
|
||||
setHeader(event, 'Cache-Control', 'public, max-age=3600'); // 1 Stunde Cache
|
||||
setHeader(event, 'Access-Control-Allow-Origin', '*');
|
||||
|
||||
// Range-Request Support für Audio-Seeking
|
||||
const range = getHeader(event, 'range');
|
||||
if (range) {
|
||||
const parts = range.replace(/bytes=/, "").split("-");
|
||||
const start = parseInt(parts[0], 10);
|
||||
const end = parts[1] ? parseInt(parts[1], 10) : stats.size - 1;
|
||||
|
||||
if (start >= stats.size || end >= stats.size) {
|
||||
setResponseStatus(event, 416); // Range Not Satisfiable
|
||||
setHeader(event, 'Content-Range', `bytes */${stats.size}`);
|
||||
return '';
|
||||
}
|
||||
|
||||
const chunkSize = (end - start) + 1;
|
||||
const chunk = fileBuffer.slice(start, end + 1);
|
||||
|
||||
setResponseStatus(event, 206); // Partial Content
|
||||
setHeader(event, 'Content-Range', `bytes ${start}-${end}/${stats.size}`);
|
||||
setHeader(event, 'Content-Length', chunkSize.toString());
|
||||
|
||||
return chunk;
|
||||
}
|
||||
|
||||
return fileBuffer;
|
||||
|
||||
} catch (error) {
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: `Failed to serve audio file: ${error}`
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,96 +0,0 @@
|
||||
// server/api/atc/generate.post.ts
|
||||
import { createError, readBody } from "h3";
|
||||
import { generateATCPhrase, getRandomPhraseForLesson, getPhrasesForLesson } from "../../utils/atcPhrases";
|
||||
import { normalizeATC } from "../../utils/openaiOld";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody<{
|
||||
moduleId: string;
|
||||
lessonId: string;
|
||||
phraseId?: string;
|
||||
customVariables?: Record<string, string>;
|
||||
type?: 'instruction' | 'clearance' | 'information' | 'request';
|
||||
count?: number;
|
||||
}>(event);
|
||||
|
||||
const { moduleId, lessonId, phraseId, customVariables, type, count = 1 } = body;
|
||||
|
||||
if (!moduleId || !lessonId) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "moduleId and lessonId are required"
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
let phrases: string[] = [];
|
||||
|
||||
if (phraseId) {
|
||||
// Spezifische Phrase generieren
|
||||
const phrase = generateATCPhrase(phraseId, customVariables);
|
||||
phrases.push(phrase);
|
||||
} else {
|
||||
// Zufällige Phrasen für das Modul/Lektion generieren
|
||||
const availablePhrases = getPhrasesForLesson(moduleId, lessonId);
|
||||
|
||||
if (availablePhrases.length === 0) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: `No phrases found for module "${moduleId}", lesson "${lessonId}"`
|
||||
});
|
||||
}
|
||||
|
||||
// Filter nach Typ wenn angegeben
|
||||
const filteredPhrases = type
|
||||
? availablePhrases.filter(p => p.type === type)
|
||||
: availablePhrases;
|
||||
|
||||
if (filteredPhrases.length === 0) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: `No phrases of type "${type}" found for module "${moduleId}", lesson "${lessonId}"`
|
||||
});
|
||||
}
|
||||
|
||||
// Generiere die gewünschte Anzahl von Phrasen
|
||||
for (let i = 0; i < Math.min(count, 10); i++) { // Max 10 Phrasen pro Request
|
||||
const randomPhrase = filteredPhrases[Math.floor(Math.random() * filteredPhrases.length)];
|
||||
const generated = generateATCPhrase(randomPhrase.id, customVariables);
|
||||
phrases.push(generated);
|
||||
}
|
||||
}
|
||||
|
||||
// Normalisiere alle Phrasen für TTS
|
||||
const normalizedPhrases = phrases.map(phrase => ({
|
||||
original: phrase,
|
||||
normalized: normalizeATC(phrase),
|
||||
length: phrase.length
|
||||
}));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
moduleId,
|
||||
lessonId,
|
||||
type: type || 'any',
|
||||
count: phrases.length,
|
||||
phrases: normalizedPhrases,
|
||||
availableTypes: getPhrasesForLesson(moduleId, lessonId)
|
||||
.map(p => p.type)
|
||||
.filter((type, index, arr) => arr.indexOf(type) === index), // Unique types
|
||||
meta: {
|
||||
totalAvailablePhrases: getPhrasesForLesson(moduleId, lessonId).length,
|
||||
generatedAt: new Date().toISOString()
|
||||
}
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
if (error.statusCode) {
|
||||
throw error; // Re-throw HTTP errors
|
||||
}
|
||||
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: `Phrase generation failed: ${error}`
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -4,7 +4,7 @@ import {writeFile, mkdir} from "node:fs/promises";
|
||||
import {existsSync} from "node:fs";
|
||||
import {join} from "node:path";
|
||||
import {randomUUID} from "node:crypto";
|
||||
import {openaiOld, TTS_MODEL, normalizeATC} from "../../utils/openaiOld";
|
||||
import {normalize, TTS_MODEL, normalizeATC} from "../../utils/normalize";
|
||||
import {request} from "node:http";
|
||||
|
||||
// dotenv config
|
||||
@@ -100,7 +100,7 @@ export default defineEventHandler(async (event) => {
|
||||
audioBuffer = await piperTTS(normalized, voice);
|
||||
} else {
|
||||
// --- OpenAI Fallback ---
|
||||
const tts = await openaiOld.audio.speech.create({
|
||||
const tts = await normalize.audio.speech.create({
|
||||
model: TTS_MODEL,
|
||||
voice,
|
||||
format: "wav",
|
||||
@@ -110,7 +110,7 @@ export default defineEventHandler(async (event) => {
|
||||
audioBuffer = Buffer.from(await tts.arrayBuffer());
|
||||
}
|
||||
|
||||
await ensureDir(baseDir);
|
||||
// await ensureDir(baseDir);
|
||||
// await writeFile(fileWav, audioBuffer);
|
||||
|
||||
const meta = {
|
||||
@@ -130,7 +130,7 @@ export default defineEventHandler(async (event) => {
|
||||
format: "audio/wav"
|
||||
};
|
||||
|
||||
await writeFile(fileJson, JSON.stringify(meta, null, 2), "utf-8");
|
||||
// await writeFile(fileJson, JSON.stringify(meta, null, 2), "utf-8");
|
||||
|
||||
return {
|
||||
success: true,
|
||||
|
||||
Reference in New Issue
Block a user