diff --git a/app/pages/atc.vue b/app/pages/atc.vue deleted file mode 100644 index 8245fa2..0000000 --- a/app/pages/atc.vue +++ /dev/null @@ -1,109 +0,0 @@ - - - ATC Demo (ohne PTT) - - - - - Verständigungslevel: - Stufe {{ radioLevel }} - - - - 1 = sehr schlecht (Rauschen/Dropouts) · 4 = gut (Default) · 5 = klar - - - - - ATC erzeugen & abspielen (Radio, Stufe {{ radioLevel }}) - - - - Status: {{ status }} - - Server-Level bestätigt: {{ returnedLevel }} - - - ATC Text: {{ atcText }} - - - - - - Clean (TTS): - - Play Clean - - - Radio (Funk-Effekt): - - Play Radio - - - - - - - diff --git a/app/pages/ptt.vue b/app/pages/ptt.vue deleted file mode 100644 index ee5f4b5..0000000 --- a/app/pages/ptt.vue +++ /dev/null @@ -1,116 +0,0 @@ - - - ATC Push-to-Talk - - - - {{ recording ? 'Recording…' : 'Push-to-Talk' }} - - - - Status: {{ status }} - Pilot: {{ pilotText }} - ATC: {{ replyText }} - - - - - - - - diff --git a/app/pages/say.vue b/app/pages/say.vue deleted file mode 100644 index 2e8356c..0000000 --- a/app/pages/say.vue +++ /dev/null @@ -1,287 +0,0 @@ - - - - ATC Generator - Zurücksetzen - - - - - - - - - - - - 1 = stark verrauscht · 4 = gut (Default) · 5 = klar - - - - - - - - - - - - - - mdi-play Erzeugen & Abspielen - - - mdi-download OGG herunterladen - - - mdi-content-copy Meta kopieren - - - - - - - - - - - - - MIME: audio/ogg; codecs=opus · 16 kHz mono - - - - - - - - - - - - - - - - - - - History - mdi-delete leeren - - - - - DatumLevelVoiceTextPlay - - - - - {{ new Date(h.createdAt).toLocaleString() }} - {{ h.level }} - {{ h.voice }} - {{ h.text }} - - mdi-play - - - - - - - - {{ snack.msg }} - - - - - - - diff --git a/composables/radioTts.ts b/composables/radioTts.ts deleted file mode 100644 index 2bc6d65..0000000 --- a/composables/radioTts.ts +++ /dev/null @@ -1,142 +0,0 @@ -// audioRadioTTS.ts -/** AUDIO: English TTS with radio ambience (noise + bandlimit + PTT click) **/ -declare const cfg: - | { value?: { tts?: boolean } } - | undefined; // optional global (Nuxt/Pinia o.ä.) - -let _ctx: AudioContext | null = null; -let _masterGain: GainNode; -let _noiseGain: GainNode; -let _noiseFilterBand: BiquadFilterNode; -let _noiseFilterHP: BiquadFilterNode; -let _noiseFilterLP: BiquadFilterNode; -let _compressor: DynamicsCompressorNode; - -function ensureAudioGraph() { - if (_ctx) return; - _ctx = new (window.AudioContext || (window as any).webkitAudioContext)(); - - _masterGain = _ctx.createGain(); - _masterGain.gain.value = 0.9; - - // Background "radio" noise - const noiseBuffer = _ctx.createBuffer(1, _ctx.sampleRate * 2, _ctx.sampleRate); - const data = noiseBuffer.getChannelData(0); - for (let i = 0; i < data.length; i++) { - const w = Math.random() * 2 - 1; - data[i] = (data[i - 1] || 0) * 0.97 + w * 0.03; // cheap pink-ish - } - const noise = _ctx.createBufferSource(); - noise.buffer = noiseBuffer; - noise.loop = true; - - _noiseFilterHP = _ctx.createBiquadFilter(); - _noiseFilterHP.type = "highpass"; - _noiseFilterHP.frequency.value = 300; - - _noiseFilterBand = _ctx.createBiquadFilter(); - _noiseFilterBand.type = "bandpass"; - _noiseFilterBand.frequency.value = 1600; - _noiseFilterBand.Q.value = 0.6; - - _noiseFilterLP = _ctx.createBiquadFilter(); - _noiseFilterLP.type = "lowpass"; - _noiseFilterLP.frequency.value = 3200; - - _compressor = _ctx.createDynamicsCompressor(); - _compressor.threshold.value = -24; - _compressor.knee.value = 20; - _compressor.ratio.value = 6; - _compressor.attack.value = 0.003; - _compressor.release.value = 0.1; - - _noiseGain = _ctx.createGain(); - _noiseGain.gain.value = 0.07; - - noise - .connect(_noiseFilterHP) - .connect(_noiseFilterBand) - .connect(_noiseFilterLP) - .connect(_noiseGain) - .connect(_compressor) - .connect(_masterGain) - .connect(_ctx.destination); - - noise.start(); -} - -function pttClick(durationMs = 25, gain = 0.5) { - if (!_ctx) return; - const osc = _ctx.createOscillator(); - const g = _ctx.createGain(); - osc.type = "square"; - osc.frequency.value = 1200 + Math.random() * 600; - g.gain.value = gain; - osc.connect(g).connect(_masterGain); - osc.start(); - const now = _ctx.currentTime; - g.gain.setValueAtTime(gain, now); - g.gain.exponentialRampToValueAtTime(0.001, now + durationMs / 1000); - osc.stop(now + durationMs / 1000 + 0.02); -} - -/** Helpers */ -function isTtsEnabled(): boolean { - // Wenn cfg fehlt → default: true (nicht crashen, trotzdem sprechen) - try { - // @ts-ignore – cfg evtl. global - const val = (cfg as any)?.value?.tts; - return val ?? true; - } catch { - return true; - } -} - -/** Main speak with radio ambience */ -export default function speak(text: string) { - if (typeof window === "undefined") return; - if (!("speechSynthesis" in window)) return; - if (!isTtsEnabled()) return; - - ensureAudioGraph(); - - const u = new SpeechSynthesisUtterance(text); - u.lang = "en-US"; // or "en-GB" - u.rate = 1.0; - u.pitch = 1.0; - - u.onstart = () => { - _ctx?.resume(); - pttClick(18, 0.35); - if (_noiseGain) { - const now = _ctx!.currentTime; - _noiseGain.gain.cancelScheduledValues(now); - _noiseGain.gain.setValueAtTime(_noiseGain.gain.value, now); - _noiseGain.gain.linearRampToValueAtTime(0.1, now + 0.05); - } - }; - - u.onend = () => { - pttClick(22, 0.28); - if (_noiseGain) { - const now = _ctx!.currentTime; - _noiseGain.gain.cancelScheduledValues(now); - _noiseGain.gain.setValueAtTime(_noiseGain.gain.value, now); - _noiseGain.gain.linearRampToValueAtTime(0.07, now + 0.2); - } - }; - - setTimeout(() => { - window.speechSynthesis.cancel(); - window.speechSynthesis.speak(u); - }, 60); -} - - -export function createRadioTTS(isEnabled: () => boolean = () => true) { - return (text: string) => { - if (!isEnabled()) return; - speak(text); - }; -} -// usage: const speakRadio = createRadioTTS(() => cfg?.value?.tts ?? true); diff --git a/public/docs/communication.json b/public/docs/communication.json index 78fa0c5..296dfcf 100644 --- a/public/docs/communication.json +++ b/public/docs/communication.json @@ -1,198 +1,185 @@ -{ - "meta": { - "filename": "vatsim_communication_template.json", - "source": ":contentReference[oaicite:0]{index=0}" +[ + { + "frequency": "Delivery", + "action": "Flight Plan", + "pilot": "Delivery, good day, ${callsign} at stand ${gate}, with information ${atis}, requesting clearance to ${destination}", + "atc": "${callsign}, ${airport} Delivery, good day, clearance is to ${destination}, ${departureRoute} departure, runway ${runway}, squawk ${squawk}", + "pilotResponse": "Cleared to ${destination}, ${departureRoute} departure, runway ${runway}, squawk ${squawk}, ${callsign}" + "atcResponse": "${callsign}, readback is correct, report ready for startup" }, - "columns": ["frequency", "action", "pilot", "atc", "pilotResponse"], - "rows": [ - { - "frequency": "Delivery", - "action": "Flight Plan", - "pilot": "Delivery, good day, ${callsign} at stand ${gate}, with information ${atis}, requesting clearance to ${destination}", - "atc": "${callsign}, ${airport} Delivery, good day, clearance is to ${destination}, ${departureRoute} departure, runway ${runway}, squawk ${squawk}", - "pilotResponse": "Cleared to ${destination}, ${departureRoute} departure, runway ${runway}, squawk ${squawk}, ${callsign}" - }, - { - "frequency": "Delivery", - "action": "Flight Plan – ATC Reply", - "pilot": "", - "atc": "${callsign}, readback is correct, report ready for startup", - "pilotResponse": "Wilco, ${callsign}" - }, - { - "frequency": "Startup", - "action": "Clearance", - "pilot": "${callsign} is ready for startup.", - "atc": "${callsign}, roger, startup is approved, for the pushback contact ground on ${groundFreq}", - "pilotResponse": "Startup approved and contact ground on ${groundFreq} for the pushback, ${callsign}, bye bye" - }, - { - "frequency": "Ground", - "action": "Pushback", - "pilot": "Ground, good day, ${callsign} at stand ${gate}, requesting pushback", - "atc": "${callsign}, ${airport} Ground, good day, pushback is approved.", - "pilotResponse": "Pushback approved, ${callsign}" - }, - { - "frequency": "Taxi", - "action": "Taxiing", - "pilot": "${callsign}, request taxi", - "atc": "${callsign}, taxi to ${holdingPointOrRunway} via ${taxiRoute}", - "pilotResponse": "Taxi to ${holdingPointOrRunway} via ${taxiRoute}, ${callsign}" - }, - { - "frequency": "Ground", - "action": "Give way to airplane", - "pilot": "", - "atc": "${callsign}, give way to the ${otherAirline} ${otherType} from the ${direction}", - "pilotResponse": "Give way to the ${otherAirline} ${otherType} from the ${direction}, ${callsign}" - }, - { - "frequency": "Tower", - "action": "Handoff to Tower", - "pilot": "", - "atc": "${callsign}, at ${holdingPointOrRunway} hold short and contact Tower on ${towerFreq}, bye bye", - "pilotResponse": "At ${holdingPointOrRunway} hold short and contact Tower on ${towerFreq}, ${callsign}" - }, - { - "frequency": "Tower", - "action": "Lineup and Wait", - "pilot": "${airport} Tower, good day, ${callsign} at ${holdingPoint}, ready for departure", - "atc": "${callsign}, ${airport} Tower, good day, line up and wait runway ${runway}", - "pilotResponse": "Line up and wait runway ${runway}, ${callsign}" - }, - { - "frequency": "Tower", - "action": "Takeoff Clearance", - "pilot": "", - "atc": "${callsign}, wind ${wind}, runway ${runway}, cleared for takeoff.", - "pilotResponse": "Cleared for takeoff runway ${runway}, ${callsign}" - }, - { - "frequency": "Departure", - "action": "Handoff to Departure", - "pilot": "", - "atc": "${callsign}, contact ${airport} Departure on ${depFreq}, bye bye", - "pilotResponse": "Contact ${airport} Departure on ${depFreq}, ${callsign}, bye bye" - }, - { - "frequency": "Departure", - "action": "Contacting Departure", - "pilot": "${airport} Departure, good day, ${callsign}, passing ${altitude}, ${depRouteOrHeading}", - "atc": "${callsign}, ${airport} Departure, identified, climb ${flightLevel}", - "pilotResponse": "Climb ${flightLevel}, ${callsign}" - }, - { - "frequency": "Enroute", - "action": "Direct Route", - "pilot": "", - "atc": "${callsign}, direct to ${vorFix}", - "pilotResponse": "Direct ${vorFix}, ${callsign}" - }, - { - "frequency": "Enroute", - "action": "Handoff to Nearest Center", - "pilot": "", - "atc": "${callsign}, contact ${nearestCenter} on ${centerFreq}, bye bye", - "pilotResponse": "Contact ${nearestCenter} on ${centerFreq}, ${callsign}, bye bye" - }, - { - "frequency": "Center", - "action": "Contacting Center", - "pilot": "${nearestCenter}, good day, ${callsign}, passing ${altitude}, inbound ${vorFixOrHeading}", - "atc": "${callsign}, ${nearestCenter}, radar contact, climb ${flightLevel}", - "pilotResponse": "Climb ${flightLevel}, ${callsign}" - }, - { - "frequency": "Descent", - "action": "Request Descent", - "pilot": "${callsign}, request descent", - "atc": "${callsign}, descend to ${flightLevel}", - "pilotResponse": "Descending to ${flightLevel}, ${callsign}" - }, - { - "frequency": "Center", - "action": "Handoff to another center", - "pilot": "", - "atc": "${callsign}, contact ${nextCenter} on ${nextCenterFreq}, bye bye", - "pilotResponse": "Contact ${nextCenter} on ${nextCenterFreq}, ${callsign}, bye bye" - }, - { - "frequency": "Center", - "action": "Contacting another center and getting an approach", - "pilot": "${nextCenter}, good day, ${callsign}, passing ${altitude} for ${flightLevel}, inbound ${vorFixOrHeading}", - "atc": "${callsign}, good day, radar contact, ${star}, expect ${approachType} runway ${runway}, descend ${flightLevel}", - "pilotResponse": "${star}, ${approachType} runway ${runway} and descend ${flightLevel}, ${callsign}" - }, - { - "frequency": "Approach", - "action": "Handoff to Approach", - "pilot": "", - "atc": "${callsign}, contact ${arrivalAirport} Approach on ${approachFreq}, bye bye", - "pilotResponse": "Contact ${arrivalAirport} Approach on ${approachFreq}, ${callsign}, bye bye" - }, - { - "frequency": "Approach", - "action": "Contacting Approach", - "pilot": "${arrivalAirport} Approach, good day, ${callsign}, ${flightLevel}, ${star}", - "atc": "${callsign}, ${arrivalAirport} Approach, good day, continue approach", - "pilotResponse": "Continue approach, ${callsign}" - }, - { - "frequency": "Approach", - "action": "Vectoring", - "pilot": "", - "atc": "${callsign}, descend ${flightLevel} and after ${vorFix} fly heading ${heading}", - "pilotResponse": "Descend ${flightLevel} and after ${vorFix} fly heading ${heading}, ${callsign}" - }, - { - "frequency": "Approach", - "action": "Vectoring 2", - "pilot": "", - "atc": "${callsign}, fly heading ${heading}, descend ${flightLevel}, QNH ${qnh}", - "pilotResponse": "Fly heading ${heading}, descend ${flightLevel} on QNH ${qnh}, ${callsign}" - }, - { - "frequency": "Approach", - "action": "Vectoring 3", - "pilot": "", - "atc": "${callsign}, turn left heading ${heading}, speed ${speed}", - "pilotResponse": "Left heading ${heading} and speed ${speed}, ${callsign}" - }, - { - "frequency": "Approach", - "action": "Cleared Approach", - "pilot": "", - "atc": "${callsign}, turn left heading ${heading}, cleared ${approachType} runway ${runway}", - "pilotResponse": "Turn left heading ${heading}, cleared ${approachType} runway ${runway}, ${callsign}" - }, - { - "frequency": "Tower", - "action": "Handoff to Tower", - "pilot": "", - "atc": "${callsign}, contact ${arrivalAirport} Tower on ${towerFreq}, bye bye", - "pilotResponse": "Contact ${arrivalAirport} Tower on ${towerFreq}, ${callsign}, bye bye" - }, - { - "frequency": "Tower", - "action": "Contacting Tower", - "pilot": "${arrivalAirport} Tower, ${callsign}, ${approachType} runway ${runway}", - "atc": "${callsign}, good day, ${sequenceInfo}", - "pilotResponse": "" - }, - { - "frequency": "Tower", - "action": "Landing Clearance", - "pilot": "", - "atc": "${callsign}, wind ${wind}, runway ${runway}, cleared to land", - "pilotResponse": "Cleared to land runway ${runway}, ${callsign}" - }, - { - "frequency": "Ground", - "action": "Taxiing (Arrival)", - "pilot": "${callsign}, ${arrivalAirport} Ground, taxi to stand ${stand} via ${taxiRoute}", - "atc": "Taxi to stand ${stand} via ${taxiRoute}, ${callsign}", - "pilotResponse": "" - } - ] -} + { + "frequency": "Startup", + "action": "Clearance", + "pilot": "${callsign} is ready for startup.", + "atc": "${callsign}, roger, startup is approved, for the pushback contact ground on ${groundFreq}", + "pilotResponse": "Startup approved and contact ground on ${groundFreq} for the pushback, ${callsign}, bye bye" + }, + { + "frequency": "Ground", + "action": "Pushback", + "pilot": "Ground, good day, ${callsign} at stand ${gate}, requesting pushback", + "atc": "${callsign}, ${airport} Ground, good day, pushback is approved.", + "pilotResponse": "Pushback approved, ${callsign}" + }, + { + "frequency": "Taxi", + "action": "Taxiing", + "pilot": "${callsign}, request taxi", + "atc": "${callsign}, taxi to ${holdingPointOrRunway} via ${taxiRoute}", + "pilotResponse": "Taxi to ${holdingPointOrRunway} via ${taxiRoute}, ${callsign}" + }, + { + "frequency": "Ground", + "action": "Give way to airplane", + "pilot": "", + "atc": "${callsign}, give way to the ${otherAirline} ${otherType} from the ${direction}", + "pilotResponse": "Give way to the ${otherAirline} ${otherType} from the ${direction}, ${callsign}" + }, + { + "frequency": "Tower", + "action": "Handoff to Tower", + "pilot": "", + "atc": "${callsign}, at ${holdingPointOrRunway} hold short and contact Tower on ${towerFreq}, bye bye", + "pilotResponse": "At ${holdingPointOrRunway} hold short and contact Tower on ${towerFreq}, ${callsign}" + }, + { + "frequency": "Tower", + "action": "Lineup and Wait", + "pilot": "${airport} Tower, good day, ${callsign} at ${holdingPoint}, ready for departure", + "atc": "${callsign}, ${airport} Tower, good day, line up and wait runway ${runway}", + "pilotResponse": "Line up and wait runway ${runway}, ${callsign}" + }, + { + "frequency": "Tower", + "action": "Takeoff Clearance", + "pilot": "", + "atc": "${callsign}, wind ${wind}, runway ${runway}, cleared for takeoff.", + "pilotResponse": "Cleared for takeoff runway ${runway}, ${callsign}" + }, + { + "frequency": "Departure", + "action": "Handoff to Departure", + "pilot": "", + "atc": "${callsign}, contact ${airport} Departure on ${depFreq}, bye bye", + "pilotResponse": "Contact ${airport} Departure on ${depFreq}, ${callsign}, bye bye" + }, + { + "frequency": "Departure", + "action": "Contacting Departure", + "pilot": "${airport} Departure, good day, ${callsign}, passing ${altitude}, ${depRouteOrHeading}", + "atc": "${callsign}, ${airport} Departure, identified, climb ${flightLevel}", + "pilotResponse": "Climb ${flightLevel}, ${callsign}" + }, + { + "frequency": "Enroute", + "action": "Direct Route", + "pilot": "", + "atc": "${callsign}, direct to ${vorFix}", + "pilotResponse": "Direct ${vorFix}, ${callsign}" + }, + { + "frequency": "Enroute", + "action": "Handoff to Nearest Center", + "pilot": "", + "atc": "${callsign}, contact ${nearestCenter} on ${centerFreq}, bye bye", + "pilotResponse": "Contact ${nearestCenter} on ${centerFreq}, ${callsign}, bye bye" + }, + { + "frequency": "Center", + "action": "Contacting Center", + "pilot": "${nearestCenter}, good day, ${callsign}, passing ${altitude}, inbound ${vorFixOrHeading}", + "atc": "${callsign}, ${nearestCenter}, radar contact, climb ${flightLevel}", + "pilotResponse": "Climb ${flightLevel}, ${callsign}" + }, + { + "frequency": "Descent", + "action": "Request Descent", + "pilot": "${callsign}, request descent", + "atc": "${callsign}, descend to ${flightLevel}", + "pilotResponse": "Descending to ${flightLevel}, ${callsign}" + }, + { + "frequency": "Center", + "action": "Handoff to another center", + "pilot": "", + "atc": "${callsign}, contact ${nextCenter} on ${nextCenterFreq}, bye bye", + "pilotResponse": "Contact ${nextCenter} on ${nextCenterFreq}, ${callsign}, bye bye" + }, + { + "frequency": "Center", + "action": "Contacting another center and getting an approach", + "pilot": "${nextCenter}, good day, ${callsign}, passing ${altitude} for ${flightLevel}, inbound ${vorFixOrHeading}", + "atc": "${callsign}, good day, radar contact, ${star}, expect ${approachType} runway ${runway}, descend ${flightLevel}", + "pilotResponse": "${star}, ${approachType} runway ${runway} and descend ${flightLevel}, ${callsign}" + }, + { + "frequency": "Approach", + "action": "Handoff to Approach", + "pilot": "", + "atc": "${callsign}, contact ${arrivalAirport} Approach on ${approachFreq}, bye bye", + "pilotResponse": "Contact ${arrivalAirport} Approach on ${approachFreq}, ${callsign}, bye bye" + }, + { + "frequency": "Approach", + "action": "Contacting Approach", + "pilot": "${arrivalAirport} Approach, good day, ${callsign}, ${flightLevel}, ${star}", + "atc": "${callsign}, ${arrivalAirport} Approach, good day, continue approach", + "pilotResponse": "Continue approach, ${callsign}" + }, + { + "frequency": "Approach", + "action": "Vectoring", + "pilot": "", + "atc": "${callsign}, descend ${flightLevel} and after ${vorFix} fly heading ${heading}", + "pilotResponse": "Descend ${flightLevel} and after ${vorFix} fly heading ${heading}, ${callsign}" + }, + { + "frequency": "Approach", + "action": "Vectoring 2", + "pilot": "", + "atc": "${callsign}, fly heading ${heading}, descend ${flightLevel}, QNH ${qnh}", + "pilotResponse": "Fly heading ${heading}, descend ${flightLevel} on QNH ${qnh}, ${callsign}" + }, + { + "frequency": "Approach", + "action": "Vectoring 3", + "pilot": "", + "atc": "${callsign}, turn left heading ${heading}, speed ${speed}", + "pilotResponse": "Left heading ${heading} and speed ${speed}, ${callsign}" + }, + { + "frequency": "Approach", + "action": "Cleared Approach", + "pilot": "", + "atc": "${callsign}, turn left heading ${heading}, cleared ${approachType} runway ${runway}", + "pilotResponse": "Turn left heading ${heading}, cleared ${approachType} runway ${runway}, ${callsign}" + }, + { + "frequency": "Tower", + "action": "Handoff to Tower", + "pilot": "", + "atc": "${callsign}, contact ${arrivalAirport} Tower on ${towerFreq}, bye bye", + "pilotResponse": "Contact ${arrivalAirport} Tower on ${towerFreq}, ${callsign}, bye bye" + }, + { + "frequency": "Tower", + "action": "Contacting Tower", + "pilot": "${arrivalAirport} Tower, ${callsign}, ${approachType} runway ${runway}", + "atc": "${callsign}, good day, ${sequenceInfo}", + "pilotResponse": "" + }, + { + "frequency": "Tower", + "action": "Landing Clearance", + "pilot": "", + "atc": "${callsign}, wind ${wind}, runway ${runway}, cleared to land", + "pilotResponse": "Cleared to land runway ${runway}, ${callsign}" + }, + { + "frequency": "Ground", + "action": "Taxiing (Arrival)", + "pilot": "${callsign}, ${arrivalAirport} Ground, taxi to stand ${stand} via ${taxiRoute}", + "atc": "Taxi to stand ${stand} via ${taxiRoute}, ${callsign}", + "pilotResponse": "" + } +] diff --git a/server/api/atc/say.stream.get.ts b/server/api/atc/say.stream.get.ts deleted file mode 100644 index 72bd710..0000000 --- a/server/api/atc/say.stream.get.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { getQuery, createError } from "h3"; -import { spawn } from "node:child_process"; -import { openai, TTS_MODEL, normalizeATC } from "../../utils/openai"; - -function buildRadioFilter(level: number) { - const L = Math.max(1, Math.min(5, Math.floor(level || 4))); - const band = (hp: number, lp: number, gain = 6) => - `highpass=f=${hp},lowpass=f=${lp},compand=attacks=0.02:decays=0.25:points=-80/-900|-70/-20|0/-10|20/-8:gain=${gain}`; - const dropout = (period: number, dur: number) => - `volume=enable='lt(mod(t\\,${period})\\,${dur})':volume=0`; - const tail = `aecho=0.6:0.7:8:0.08,acompressor=threshold=0.6:ratio=6:attack=20:release=200`; - - // helpers (ACHTUNG: weights **gequotet** oder mit | getrennt) - const mixCrush = (bits: number, mix = 0.25, inLabel = "pre", outLabel = "mix1") => [ - `[${inLabel}]asplit=2[clean][toCrush]`, - `[toCrush]acrusher=bits=${bits}:mix=1[crushed]`, - `[clean][crushed]amix=inputs=2:weights='1 ${mix}':duration=shortest[${outLabel}]`, - ]; - - const mixNoise = (amp: number, inLabel = "mix1", outLabel = "mix2") => [ - `anoisesrc=color=white:amplitude=${amp}[ns]`, - `[${inLabel}][ns]amix=inputs=2:weights='1 0.25':duration=shortest[${outLabel}]`, - ]; - - const dropAndTail = (inLabel: string, period?: number, dur?: number) => { - const chain: string[] = []; - if (period && dur) chain.push(`[${inLabel}]${dropout(period, dur)}[drop]`); - const src = period && dur ? "drop" : inLabel; - chain.push(`[${src}]${tail}[out]`); - return chain; - }; - - if (L === 5) - return [ - `[0:a]${band(300,3400)},volume=1.2[a]`, - `anoisesrc=color=white:amplitude=0.02[ns]`, - `[a][ns]amix=inputs=2:weights='1 0.25':duration=shortest[mix]`, - `[mix]${tail}[out]`, - ].join(";"); - - if (L === 4) - return [ - `[0:a]${band(320,3300)},volume=1.15[pre]`, - ...mixNoise(0.03, "pre", "mix"), - `[mix]${tail}[out]`, - ].join(";"); - - if (L === 3) - return [ - `[0:a]${band(350,3200)},volume=1.1[pre]`, - ...mixCrush(12, 0.22, "pre", "mix1"), - ...mixNoise(0.05, "mix1", "mix2"), - ...dropAndTail("mix2", 6, 0.06), - ].join(";"); - - if (L === 2) - return [ - `[0:a]${band(400,3000)},volume=1.05[pre]`, - ...mixCrush(10, 0.32, "pre", "mix1"), - `anoisesrc=color=white:amplitude=0.08[ns]`, - `[mix1][ns]amix=inputs=2:weights='1 0.6':duration=shortest[mix2]`, - ...dropAndTail("mix2", 4.5, 0.12), - ].join(";"); - - // L === 1 - return [ - `[0:a]${band(500,2600,5)},volume=1.0[pre]`, - ...mixCrush(8, 0.45, "pre", "mix1"), - `anoisesrc=color=white:amplitude=0.12[ns]`, - `[mix1][ns]amix=inputs=2:weights='1 0.8':duration=shortest[mix2]`, - ...dropAndTail("mix2", 3.5, 0.2), - ].join(";"); -} - -// super-simpler Fallback ohne Mix/Noise (falls Filter scheitert) -const SIMPLE_FILTER = `[0:a]highpass=f=350,lowpass=f=3000,acompressor=threshold=0.6:ratio=6:attack=20:release=200[out]`; - -export default defineEventHandler(async (event) => { - const q = getQuery(event); - const raw = String(q.text || "").trim(); - if (!raw) throw createError({ statusCode: 400, statusMessage: "text required" }); - - const level = Math.max(1, Math.min(5, parseInt(String(q.level ?? "4"), 10) || 4)); - const voice = String(q.voice || "alloy"); - const normalized = normalizeATC(raw) || raw; - - // 1) TTS → WAV - const tts = await openai.audio.speech.create({ - model: TTS_MODEL, - voice, - format: "wav", - input: normalized, - }); - const wav = Buffer.from(await tts.arrayBuffer()); - if (wav.byteLength < 100) throw createError({ statusCode: 500, statusMessage: "TTS empty" }); - - async function runFfmpeg(filter: string) { - return new Promise((resolve) => { - const ff = spawn("ffmpeg", [ - "-hide_banner", "-loglevel", "error", - "-f", "wav", "-i", "pipe:0", - "-filter_complex", filter, - "-map", "[out]", - "-ac", "1", "-ar", "16000", - "-c:a", "libopus", "-b:a", "12k", "-application", "voip", - "-f", "ogg", "pipe:1", - ], { stdio: ["pipe", "pipe", "pipe"] }); - - const res = event.node.res; - let started = false; - let ffErr = ""; - - ff.stderr.on("data", d => { ffErr += d.toString(); }); - - ff.stdout.once("data", (chunk: Buffer) => { - if (!started) { - started = true; - res.statusCode = 200; - res.setHeader("Content-Type", "audio/ogg"); - res.setHeader("Cache-Control", "no-store"); - res.setHeader("Accept-Ranges", "none"); - (res as any).flushHeaders?.(); - } - res.write(chunk); - ff.stdout.pipe(res); - }); - - ff.stdin.on("error", () => {}); - ff.stdin.write(wav); - ff.stdin.end(); - - ff.on("close", (code) => { - if (!started) { - // Fehlerpfad → JSON-Fehler zurück - if (!res.headersSent) { - res.statusCode = 500; - res.setHeader("Content-Type", "application/json"); - } - res.end(JSON.stringify({ error: true, message: ffErr || `ffmpeg exit ${code}` })); - } else { - if (!res.writableEnded) res.end(); - } - resolve(); - }); - }); - } - - // erst komplexer Filter; wenn der fehlschlägt → SIMPLE_FILTER - await runFfmpeg(buildRadioFilter(level)); - if (!event.node.res.headersSent) { - // zweiter Versuch - await runFfmpeg(SIMPLE_FILTER); - } -}); diff --git a/server/api/ptt/reply.post.ts b/server/api/ptt/reply.post.ts deleted file mode 100644 index 2cb6270..0000000 --- a/server/api/ptt/reply.post.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { readMultipartFormData, createError } from "h3"; -import { openai, LLM_MODEL, TTS_MODEL, atcReplyPrompt } from "../../utils/openai"; -import { applyRadioEffect } from "../../utils/radio"; -import { writeFile, rm, readFile } from "node:fs/promises"; -import { randomUUID } from "node:crypto"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { toFile } from "openai/uploads"; - -export default defineEventHandler(async (event) => { - const parts = await readMultipartFormData(event); - if (!parts) throw createError({ statusCode: 400, statusMessage: "No form-data" }); - - // Default ohne Text-Input: immer TTS - const audio = parts.find(p => p.type && p.data); - if (!audio) throw createError({ statusCode: 400, statusMessage: "No audio file" }); - - const file = await toFile( - audio.data, - audio.filename || "ptt.webm", - { type: audio.type || "audio/webm" } - ); - - // 1) Transkription (Whisper) - const tr = await openai.audio.transcriptions.create({ model: "whisper-1", file }); - const pilotText = (tr.text || "").trim(); - if (!pilotText) throw createError({ statusCode: 400, statusMessage: "Empty transcription" }); - - // 2) ATC-Antwort (LLM) - const resp = await openai.responses.create({ - model: LLM_MODEL, - input: atcReplyPrompt(pilotText), - }); - const replyText = (resp.output_text || "").trim(); - if (!replyText) throw createError({ statusCode: 500, statusMessage: "LLM empty" }); - - // 3) TTS + Funk-Effekt - const clean = join(tmpdir(), `tts-${randomUUID()}.wav`); - const radio = join(tmpdir(), `radio-${randomUUID()}.wav`); - - const tts = await openai.audio.speech.create({ - model: TTS_MODEL, - voice: "alloy", - format: "wav", - input: replyText, - }); - await writeFile(clean, Buffer.from(await tts.arrayBuffer())); - await applyRadioEffect(clean, radio); - - const data = await readFile(radio); - const b64 = Buffer.from(data).toString("base64"); - rm(clean).catch(() => {}); - rm(radio).catch(() => {}); - - return { pilotText, replyText, audio: { mime: "audio/wav", base64: b64 } }; -}); diff --git a/server/api/ptt/transcribe.post.ts b/server/api/ptt/transcribe.post.ts deleted file mode 100644 index c712cac..0000000 --- a/server/api/ptt/transcribe.post.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { readMultipartFormData, createError } from "h3"; -import { openai } from "../../utils/openai"; -import { toFile } from "openai/uploads"; // ⟵ NEU - -export default defineEventHandler(async (event) => { - const parts = await readMultipartFormData(event); - if (!parts) throw createError({ statusCode: 400, statusMessage: "No form-data" }); - - const audio = parts.find(p => p.type && p.data); - if (!audio) throw createError({ statusCode: 400, statusMessage: "No audio file" }); - - const file = await toFile( - audio.data, // Buffer - audio.filename || "ptt.webm", // ⟵ mit Endung - { type: audio.type || "audio/webm" } // ⟵ korrekter MIME - ); - - const tr = await openai.audio.transcriptions.create({ - model: "whisper-1", - file - }); - - return { text: tr.text }; -}); diff --git a/server/routes/health.get.ts b/server/routes/health.get.ts deleted file mode 100644 index b1410eb..0000000 --- a/server/routes/health.get.ts +++ /dev/null @@ -1 +0,0 @@ -export default defineEventHandler(() => ({status:'ok'}));