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:
itsrubberduck
2025-09-16 16:14:12 +02:00
parent d69c01e97a
commit e155434b57
15 changed files with 8 additions and 1495 deletions

View File

@@ -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}`
});
}
});

View File

@@ -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}`
});
}
});

View File

@@ -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,

View File

@@ -1,269 +0,0 @@
// server/utils/atcPhrases.ts
export interface ATCPhrase {
id: string;
moduleId: string;
lessonId: string;
type: 'instruction' | 'clearance' | 'information' | 'request';
template: string;
variables?: Record<string, string[]>;
context?: {
airport?: string;
runway?: string;
frequency?: string;
callsign?: string;
};
}
export const ATC_PHRASES: ATCPhrase[] = [
// ICAO Alphabet Module
{
id: 'icao_alpha_drill',
moduleId: 'icao',
lessonId: 'alpha',
type: 'instruction',
template: 'Spell your callsign using phonetic alphabet from {start} to {end}',
variables: {
start: ['Alpha', 'Bravo', 'Charlie'],
end: ['Lima', 'Mike', 'November']
}
},
{
id: 'icao_numbers_drill',
moduleId: 'icao',
lessonId: 'numbers',
type: 'instruction',
template: 'Read back transponder code {squawk}',
variables: {
squawk: ['1234', '4567', '7321', '2156', '6543']
}
},
{
id: 'icao_callsign_spell',
moduleId: 'icao',
lessonId: 'callsign-icao',
type: 'instruction',
template: '{callsign}, spell your callsign',
variables: {
callsign: ['DLH123', 'BAW456', 'AFR789', 'KLM321', 'RYR654']
}
},
// Basics Module
{
id: 'ground_checkin',
moduleId: 'basics',
lessonId: 'checkin',
type: 'clearance',
template: '{callsign}, {ground_station}, stand {stand} available, taxi when ready',
variables: {
callsign: ['DLH123', 'BAW456', 'AFR789'],
ground_station: ['Frankfurt Ground', 'Munich Ground', 'Berlin Ground'],
stand: ['A12', 'B24', 'C15', 'V155', 'G23']
},
context: {
airport: 'EDDF'
}
},
{
id: 'basic_readback',
moduleId: 'basics',
lessonId: 'readback',
type: 'instruction',
template: '{callsign}, contact Tower on {frequency}',
variables: {
callsign: ['DLH123', 'BAW456', 'AFR789'],
frequency: ['118.500', '119.900', '121.700', '124.850']
}
},
// Ground Operations
{
id: 'taxi_clearance_simple',
moduleId: 'ground',
lessonId: 'taxi1',
type: 'clearance',
template: '{callsign}, taxi to runway {runway} via {taxiway}, hold short runway {runway}',
variables: {
callsign: ['DLH123', 'BAW456', 'AFR789', 'EZY234'],
runway: ['25R', '25L', '07R', '07L', '18', '36'],
taxiway: ['A A5 B2', 'C C3 A', 'A A7 N N4', 'B B1 A3']
}
},
{
id: 'taxi_clearance_complex',
moduleId: 'ground',
lessonId: 'taxi1',
type: 'clearance',
template: '{callsign}, taxi to runway {runway} via {route}, hold short runway {hold_runway}',
variables: {
callsign: ['DLH359', 'BAW12A', 'AFR567'],
runway: ['25R', '25L', '07R'],
route: ['A A3 B B1', 'C C5 A A7', 'M M1 A A5 B'],
hold_runway: ['25R', '25L', '07R', '18']
}
},
{
id: 'handoff_tower',
moduleId: 'ground',
lessonId: 'handoff',
type: 'instruction',
template: '{callsign}, contact Tower on {frequency}',
variables: {
callsign: ['DLH123', 'BAW456', 'AFR789'],
frequency: ['118.500', '119.900', '121.700', '124.850', '132.025']
}
},
// Departure Operations
{
id: 'lineup_wait',
moduleId: 'departure',
lessonId: 'lineup',
type: 'clearance',
template: '{callsign}, line up and wait runway {runway}',
variables: {
callsign: ['DLH123', 'BAW456', 'AFR789'],
runway: ['25R', '25L', '07R', '07L', '18', '36']
}
},
{
id: 'takeoff_clearance',
moduleId: 'departure',
lessonId: 'lineup',
type: 'clearance',
template: '{callsign}, runway {runway}, cleared for takeoff',
variables: {
callsign: ['DLH123', 'BAW456', 'AFR789'],
runway: ['25R', '25L', '07R', '07L']
}
},
{
id: 'departure_instructions',
moduleId: 'departure',
lessonId: 'lineup',
type: 'instruction',
template: '{callsign}, after takeoff turn {direction} heading {heading}, contact Departure on {frequency}',
variables: {
callsign: ['DLH123', 'BAW456', 'AFR789'],
direction: ['left', 'right'],
heading: ['090', '180', '270', '360', '045', '135', '225', '315'],
frequency: ['121.200', '125.750', '127.275', '135.725']
}
},
// Arrival Operations
{
id: 'landing_clearance',
moduleId: 'arrival',
lessonId: 'vacate',
type: 'clearance',
template: '{callsign}, runway {runway}, cleared to land',
variables: {
callsign: ['DLH123', 'BAW456', 'AFR789'],
runway: ['25R', '25L', '07R', '07L']
}
},
{
id: 'vacate_instruction',
moduleId: 'arrival',
lessonId: 'vacate',
type: 'instruction',
template: '{callsign}, vacate runway {runway} via {taxiway}, contact Ground on {frequency}',
variables: {
callsign: ['DLH123', 'BAW456', 'AFR789'],
runway: ['25R', '25L', '07R', '07L'],
taxiway: ['A6', 'A7', 'B3', 'C4', 'N2'],
frequency: ['121.800', '121.900', '129.725']
}
},
// VATSIM Operations
{
id: 'ifr_clearance',
moduleId: 'vatsim',
lessonId: 'checkin',
type: 'clearance',
template: '{callsign}, cleared to {destination} via {sid}, initial climb {altitude}, squawk {squawk}',
variables: {
callsign: ['DLH359', 'BAW12A', 'AFR567'],
destination: ['EHAM', 'EGLL', 'LFPG', 'LEMD', 'LIRF'],
sid: ['MARUN7F', 'BIBTI7F', 'CHA7F', 'SOBRA7F'],
altitude: ['5000 feet', '6000 feet', 'FL070', 'FL080'],
squawk: ['4723', '1234', '5647', '7321']
}
},
{
id: 'startup_clearance',
moduleId: 'vatsim',
lessonId: 'checkin',
type: 'clearance',
template: '{callsign}, startup approved, {atis_info} current, expect runway {runway}',
variables: {
callsign: ['DLH359', 'BAW12A', 'AFR567'],
atis_info: ['information Alpha', 'information Bravo', 'information Charlie'],
runway: ['25R', '25L', '07R', '07L']
}
},
// Emergency/Special Situations
{
id: 'traffic_info',
moduleId: 'ground',
lessonId: 'taxi1',
type: 'information',
template: '{callsign}, traffic {direction}, {aircraft_type} {distance}',
variables: {
callsign: ['DLH123', 'BAW456', 'AFR789'],
direction: ['ahead', 'behind', 'left', 'right', 'crossing'],
aircraft_type: ['A320', 'B737', 'A380', 'B777'],
distance: ['100 meters', '200 meters', '500 meters']
}
},
{
id: 'hold_position',
moduleId: 'ground',
lessonId: 'taxi1',
type: 'instruction',
template: '{callsign}, hold position, traffic crossing',
variables: {
callsign: ['DLH123', 'BAW456', 'AFR789']
}
}
];
// Hilfsfunktionen für Template-Verarbeitung
export function generateATCPhrase(phraseId: string, customVariables?: Record<string, string>): string {
const phrase = ATC_PHRASES.find(p => p.id === phraseId);
if (!phrase) {
throw new Error(`ATC phrase with id "${phraseId}" not found`);
}
let result = phrase.template;
const variables = phrase.variables || {};
// Ersetze Variablen im Template
for (const [key, values] of Object.entries(variables)) {
const placeholder = `{${key}}`;
if (result.includes(placeholder)) {
// Verwende custom value oder wähle zufällig
const value = customVariables?.[key] || values[Math.floor(Math.random() * values.length)];
result = result.replace(new RegExp(`\\{${key}\\}`, 'g'), value);
}
}
return result;
}
export function getPhrasesForLesson(moduleId: string, lessonId: string): ATCPhrase[] {
return ATC_PHRASES.filter(p => p.moduleId === moduleId && p.lessonId === lessonId);
}
export function getRandomPhraseForLesson(moduleId: string, lessonId: string): string {
const phrases = getPhrasesForLesson(moduleId, lessonId);
if (phrases.length === 0) {
throw new Error(`No phrases found for module "${moduleId}", lesson "${lessonId}"`);
}
const randomPhrase = phrases[Math.floor(Math.random() * phrases.length)];
return generateATCPhrase(randomPhrase.id);
}

View File

@@ -5,7 +5,7 @@ import fs from "node:fs";
dotenv.config();
export const openaiOld = new OpenAI({
export const normalize = new OpenAI({
apiKey: process.env.OPENAI_API_KEY!,
project: process.env.OPENAI_PROJECT, // optional
});
@@ -251,7 +251,7 @@ export function normalizeATC(
// TTS Wrapper (mp3)
export async function speakATC(text: string, filePath = "atc.mp3") {
const input = normalizeATC(text);
const resp = await (openaiOld as any).audio.speech.create({
const resp = await (normalize as any).audio.speech.create({
model: TTS_MODEL,
voice: "alloy",
input,