From 440aef2f62fb41e183a7c4da9a95d685f597be0b Mon Sep 17 00:00:00 2001 From: Remi <73385395+itsrubberduck@users.noreply.github.com> Date: Sat, 20 Sep 2025 09:46:34 +0200 Subject: [PATCH] Fix remaining German comment --- app/assets/css/opensquawk-glass.css | 28 +++++++++++----------- app/pages/pm.vue | 2 +- server/api/atc/ptt.post.ts | 14 +++++------ server/api/atc/say.post.ts | 12 +++++----- server/utils/normalize.ts | 36 ++++++++++++++--------------- server/utils/notifications.ts | 4 ++-- server/utils/openai.ts | 20 ++++++++-------- server/utils/runtimeConfig.ts | 2 +- server/utils/validation.ts | 8 +++---- 9 files changed, 63 insertions(+), 63 deletions(-) diff --git a/app/assets/css/opensquawk-glass.css b/app/assets/css/opensquawk-glass.css index 9915b1e..76db6dd 100644 --- a/app/assets/css/opensquawk-glass.css +++ b/app/assets/css/opensquawk-glass.css @@ -1,6 +1,6 @@ /* === iOS-Style Glassmorphism Add-on === */ :root { - /* feinere Glas-Parameter */ + /* refined glass parameters */ --glass-bg: color-mix(in srgb, var(--text) 7%, transparent); --glass-border: color-mix(in srgb, var(--text) 16%, transparent); --glass-highlight: rgba(255, 255, 255, .10); @@ -11,7 +11,7 @@ --glass-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140' viewBox='0 0 140 140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncA type='table' tableValues='0 0 .03 0'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); } -/* generische Glasfläche */ +/* generic glass surface */ .glass, .panel, .card, @@ -33,7 +33,7 @@ -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat)); } -/* App Bar (transluzent, sticky) */ +/* App bar (translucent, sticky) */ .hud { background: linear-gradient(180deg, color-mix(in srgb, var(--bg) 55%, transparent), @@ -43,12 +43,12 @@ border-radius: 0 0 14px 14px; } -/* Panels etwas luftiger */ +/* Panels with extra breathing room */ .panel { padding: 14px; } -/* Buttons – iOS-Glanz & Lift */ +/* Buttons – iOS shine and lift */ .btn { border-radius: calc(var(--glass-radius) - 6px); transition: transform .15s ease, box-shadow .15s ease, background .2s ease; @@ -92,7 +92,7 @@ } -/* Chips – feiner Rand, leicht leuchtend */ +/* Chips – subtle border, gentle glow */ .chip { background: linear-gradient(180deg, color-mix(in srgb, var(--text) 7%, transparent), @@ -100,7 +100,7 @@ box-shadow: inset 0 1px 0 rgba(255, 255, 255, .08); } -/* Karten/Kacheln – zarter Hover */ +/* Cards/tiles – subtle hover */ .card, .tile, .lesson { transition: transform .18s ease, box-shadow .18s ease, border-color .18s ease; } @@ -110,7 +110,7 @@ border-color: color-mix(in srgb, var(--accent) 28%, transparent); } -/* Fortschrittsbalken – Glasfüllung */ +/* Progress bar – glass fill */ .line { background: linear-gradient(180deg, color-mix(in srgb, var(--text) 8%, transparent), @@ -125,7 +125,7 @@ filter: saturate(130%); } -/* Textareas / Inputs im Glas */ +/* Textareas / inputs in glass */ .input :deep(textarea), :deep(.v-field__input) { background: linear-gradient(180deg, @@ -141,14 +141,14 @@ display: none; } -/* Dialog runder + stärkerer Blur */ +/* Dialog – rounder with stronger blur */ .dialog { border-radius: 22px; backdrop-filter: blur(22px) saturate(150%); -webkit-backdrop-filter: blur(22px) saturate(150%); } -/* iOS-Range (Slider) – Track & Thumb */ +/* iOS-style range (slider) – track & thumb */ .chip.inline input[type="range"] { height: 24px; -webkit-appearance: none; @@ -194,7 +194,7 @@ box-shadow: 0 2px 6px rgba(0, 0, 0, .35), inset 0 1px 0 rgba(255, 255, 255, .85); } -/* Snackbar – glasiger Toast */ +/* Snackbar – glassy toast */ :deep(.v-snackbar) { border: 1px solid var(--glass-border); backdrop-filter: blur(16px) saturate(140%); @@ -204,14 +204,14 @@ color-mix(in srgb, var(--bg2) 60%, transparent)); } -/* Fokus-Ring – iOS-Style */ +/* Focus ring – iOS style */ *:focus-visible { outline: 2px solid color-mix(in srgb, var(--accent) 60%, transparent); outline-offset: 2px; border-radius: 12px; } -/* Weniger Transparenz, falls gewünscht */ +/* Reduced transparency if requested */ @media (prefers-reduced-transparency: reduce) { .glass, .panel, diff --git a/app/pages/pm.vue b/app/pages/pm.vue index 8ab2748..96a2968 100644 --- a/app/pages/pm.vue +++ b/app/pages/pm.vue @@ -2011,7 +2011,7 @@ const swapFrequencies = () => { } const formatTime = (date: Date): string => { - return date.toLocaleTimeString('de-DE', { + return date.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', diff --git a/server/api/atc/ptt.post.ts b/server/api/atc/ptt.post.ts index 9936952..ed6c58f 100644 --- a/server/api/atc/ptt.post.ts +++ b/server/api/atc/ptt.post.ts @@ -77,11 +77,11 @@ function decodeAudioPayload(encoded: string): Buffer { return buffer; } -// Audio zu WAV konvertieren für bessere Whisper-Kompatibilität +// Convert audio to WAV for better Whisper compatibility async function convertToWav(inputPath: string, outputPath: string) { await sh("ffmpeg", [ "-y", "-i", inputPath, - "-ar", "16000", // 16kHz für Whisper + "-ar", "16000", // 16 kHz for Whisper "-ac", "1", // Mono "-f", "wav", outputPath @@ -104,11 +104,11 @@ export default defineEventHandler(async (event) => { const tmpAudioWav = join(tmpdir(), `ptt-wav-${id}.wav`); try { - // 1. Audio aus Base64 dekodieren und speichern + // 1. Decode audio from base64 and save const audioBuffer = decodeAudioPayload(body.audio); await writeFile(tmpAudioInput, audioBuffer); - // 2. Zu WAV konvertieren falls nötig (nur wenn FFmpeg verfügbar) + // 2. Convert to WAV if needed (only when FFmpeg is available) let audioFileForWhisper = tmpAudioInput; if (format !== 'wav') { try { @@ -119,7 +119,7 @@ export default defineEventHandler(async (event) => { } } - // 3. OpenAI Whisper für Transkription + // 3. OpenAI Whisper for transcription const openai = getOpenAIClient(); const transcription = await openai.audio.transcriptions.create({ file: createReadStream(audioFileForWhisper), @@ -143,7 +143,7 @@ export default defineEventHandler(async (event) => { let decision: PTTResponse['decision']; if (shouldAutoDecide) { - // 4. Direkt LLM Decision aufrufen mit transkribiertem Text + // 4. Call the LLM decision directly with the transcribed text const decisionInput = { ...body.context, pilot_utterance: transcribedText @@ -191,7 +191,7 @@ export default defineEventHandler(async (event) => { return result; } catch (error: any) { - // Cleanup bei Fehler + // Cleanup on error await rm(tmpAudioInput).catch(() => {}); await rm(tmpAudioWav).catch(() => {}); diff --git a/server/api/atc/say.post.ts b/server/api/atc/say.post.ts index 0cc04a3..6c8a3d6 100644 --- a/server/api/atc/say.post.ts +++ b/server/api/atc/say.post.ts @@ -161,7 +161,7 @@ export default defineEventHandler(async (event) => { let actualMime = mime; if (useSpeaches) { - // Speaches (bevorzugt klein: MP3, alternativ FLAC/WAV/PCM) + // Speaches (prefer compact: MP3, otherwise FLAC/WAV/PCM) const baseUrl = runtimeConfig.speachesBaseUrl || ""; const model = runtimeConfig.speechModelId || "speaches-ai/piper-en_US-ryan-low"; if (!baseUrl) { @@ -169,16 +169,16 @@ export default defineEventHandler(async (event) => { } audioBuffer = await speachesTTS(normalized, voice, model, fmt, baseUrl); modelUsed = model; - // Server liefert korrektes Format gemäß response_format + // Server returns the correct format according to response_format actualMime = fmtToMime(fmt); } else if (usePiper) { - // Lokaler Piper + // Local Piper audioBuffer = await piperTTS(normalized, voice, runtimeConfig.piperPort); modelUsed = "piper-local"; - // Piper liefert WAV + // Piper returns WAV actualMime = "audio/wav"; } else { - // OpenAI (Fallback) + // OpenAI (fallback) const tts = await normalize.audio.speech.create({ model: TTS_MODEL, voice, @@ -191,7 +191,7 @@ export default defineEventHandler(async (event) => { actualMime = "audio/wav"; } - // Optional speichern + // Optional persistence // await ensureDir(baseDir); // await writeFile(fileOut, audioBuffer); const meta = { diff --git a/server/utils/normalize.ts b/server/utils/normalize.ts index d04a05e..0705eb2 100644 --- a/server/utils/normalize.ts +++ b/server/utils/normalize.ts @@ -16,10 +16,10 @@ export const LLM_MODEL = llmModel; export const TTS_MODEL = ttsModel; /* ========================= - LLM PROMPTS (überarbeitet) + LLM PROMPTS (refined) ========================= - Ziel: LLM liefert kompakte, maschinenfreundliche ICAO-Zeile, die unser Normalizer→TTS perfekt erweitert. - WICHTIG: Zahlen/Marker exakt im unten definierten Output-Format, keine ausgeschriebenen Wörter. + Goal: the LLM returns a compact, machine-friendly ICAO line that our normalizer → TTS can expand perfectly. + IMPORTANT: Use the exact output format defined below; do not spell out numbers. */ export const ATC_OUTPUT_SPEC = ` @@ -43,9 +43,9 @@ OUTPUT RULES (STRICT): - Use standard order for the phase (e.g., taxi: destination RWY first, then route, then hold short). `.trim(); -/** System-Prompt: legt Rolle/Regeln fest */ +/** System prompt: defines the role and rules */ export function atcSystemPrompt(opts?: { - regionHint?: "EUR" | "US" | "INTL"; // nur als Soft-Hinweis, default INTL + regionHint?: "EUR" | "US" | "INTL"; // soft hint only, defaults to INTL }) { const region = opts?.regionHint ?? "INTL"; return [ @@ -56,7 +56,7 @@ export function atcSystemPrompt(opts?: { ].join("\n\n"); } -/** Seed-ATC ohne Pilot-Input (rückwärtskompatible Signatur, aber reicherer Prompt) */ +/** Seed ATC without pilot input (backward-compatible signature, but richer prompt) */ export function atcSeedPrompt(s: { airport: string; // e.g., "EDDF" aircraft: string; // e.g., "A320" @@ -66,11 +66,11 @@ export function atcSeedPrompt(s: { sid?: string; // e.g., "MARUN 7F" squawk?: string; // "4723" freq?: string; // "121.800" - runway?: string; // "25R" (optional: falls bekannt) + runway?: string; // "25R" (optional if known) phase?: "clearance" | "taxi" | "lineup" | "departure" | "handoff" | "approach" | "landing"; - notes?: string; // z.B. "TWY N closed between N2–N4" + notes?: string; // e.g. "TWY N closed between N2–N4" }) { - // Default-Phase: clearance + // Default phase: clearance const phase = s.phase || "clearance"; const ctx = [ `Airport ${s.airport}`, @@ -106,12 +106,12 @@ export function atcSeedPrompt(s: { ].join("\n"); } -/** Pilot→ATC (rückwärtskompatibler Name, aber mit robustem Rahmen) */ +/** Pilot → ATC (same legacy name, but with a sturdier framework) */ export function atcReplyPrompt(userText: string, state?: { airport?: string; runway?: string; sid?: string; dep?: string; lastSquawk?: string; lastFreq?: string; lastQNH?: string; phase?: "clearance" | "taxi" | "lineup" | "departure" | "handoff" | "approach" | "landing"; - constraints?: string; // z.B. "TWY N closed", "no intersection deps on 25C" + constraints?: string; // e.g. "TWY N closed", "no intersection deps on 25C" }) { const ctx = [ state?.airport ? `Airport ${state.airport}` : null, @@ -137,10 +137,10 @@ export function atcReplyPrompt(userText: string, state?: { } /* ========================= - Normalizer → TTS (wie zuvor) + Normalizer → TTS (unchanged) ========================= */ -// Airline-Telephony (erweiterbar) +// Airline telephony (extensible) export const CALLSIGN_MAP: Record = { DLH: "Lufthansa", EWG: "Eurowings", @@ -185,16 +185,16 @@ export async function speakATC(text: string, filePath = "atc.mp3") { } /* ========================= - Beispiele + Examples ========================= -— Seed (Clearance): +— Seed (clearance): const sys = atcSystemPrompt(); const usr = atcSeedPrompt({ airport: "EDDF", aircraft: "A320", type: "IFR", stand: "V155", dep: "EHAM", sid: "MARUN 7F", runway: "25R", freq: "121.800" }); -// → LLM antwortet z.B.: +// → The LLM might respond: // "DLH359, cleared to EHAM via MARUN 7F, initial 5000 ft, squawk 4723. QNH 1013." — Taxi: @@ -205,12 +205,12 @@ const usrTaxi = atcSeedPrompt({ }); // → "DLH359, taxi to RWY 25R via A3 A N2, hold short." -— Pilot→ATC: +— Pilot → ATC: const usrReply = atcReplyPrompt( "DLH359 ready for departure RWY 25R", { airport: "EDDF", runway: "25R", phase: "lineup", lastFreq: "121.800" } ); // → "DLH359, line up and wait RWY 25R." -Nach dem LLM-Output: `speakATC(llmText)` ruft Normalizer→TTS. +After receiving the LLM output, call `speakATC(llmText)` to trigger normalizer → TTS. */ diff --git a/server/utils/notifications.ts b/server/utils/notifications.ts index 7496725..6296274 100644 --- a/server/utils/notifications.ts +++ b/server/utils/notifications.ts @@ -97,7 +97,7 @@ export async function sendMail(options: MailOptions) { const success = await sendViaSmtp(payload) if (!success) { - console.info(`[mail:fallback] ${options.subject}\nEmpfänger: ${options.to}\n${options.text}`) + console.info(`[mail:fallback] ${options.subject}\nRecipient: ${options.to}\n${options.text}`) } return success } @@ -169,7 +169,7 @@ export async function sendAdminNotification(notification: string | AdminNotifica const success = await sendMail(mailOptions) if (!success) { - console.info(`[notify:fallback] ${mailOptions.subject}\nEmpfänger: ${to}\n${mailOptions.text}`) + console.info(`[notify:fallback] ${mailOptions.subject}\nRecipient: ${to}\n${mailOptions.text}`) } return success } diff --git a/server/utils/openai.ts b/server/utils/openai.ts index 23c5d2d..deabea1 100644 --- a/server/utils/openai.ts +++ b/server/utils/openai.ts @@ -10,7 +10,7 @@ function ensureOpenAI(): OpenAI { if (!openaiClient) { const { openaiKey, openaiProject, llmModel } = getServerRuntimeConfig() if (!openaiKey) { - throw new Error('OPENAI_API_KEY fehlt. Bitte den Schlüssel setzen, bevor KI-Funktionen genutzt werden.') + throw new Error('OPENAI_API_KEY is missing. Please set the key before using AI features.') } const clientOptions: ConstructorParameters[0] = { apiKey: openaiKey } if (openaiProject) { @@ -246,9 +246,9 @@ function extractTemplateVariables(text?: string): string[] { return matches.map(match => match.slice(1, -1)) // Remove { } } -// Optimierte aber ausreichende Eingabe für gute Entscheidungen +// Optimized yet sufficient input for reliable decisions function optimizeInputForLLM(input: LLMDecisionInput) { - // Sammle alle verfügbaren Variablen aus dem Decision Tree + // Collect all available variables from the decision tree const availableVariables = [ 'callsign', 'dest', 'dep', 'runway', 'squawk', 'sid', 'transition', 'initial_altitude_ft', 'climb_altitude_ft', 'cruise_flight_level', @@ -308,7 +308,7 @@ function optimizeInputForLLM(input: LLMDecisionInput) { current_role: input.state.role, state_summary: stateSummary, candidates: candidates, - available_variables: availableVariables, // Alle verfügbaren Variablen + available_variables: availableVariables, // All available variables candidate_variables: Array.from(candidateVars), // Variablen die Candidates verwenden pilot_utterance: input.pilot_utterance, decision_hints: { @@ -318,7 +318,7 @@ function optimizeInputForLLM(input: LLMDecisionInput) { has_interrupt_candidate: input.candidates.some(c => c.id.startsWith('INT_')), readback_check_state: Boolean(readbackKeys.length) }, - // Nur aktueller Context ohne Werte (für Token-Sparen) + // Current context only without values (to save tokens) context: { callsign: input.variables.callsign, current_unit: input.flags.current_unit, @@ -459,7 +459,7 @@ export async function routeDecision(input: LLMDecisionInput): Promise c.state.role === 'atc' || c.state.say_tpl || c.id.startsWith('INT_') ) - // Wenn keine ATC-States verfügbar, einfache Transition ohne Response + // If no ATC states are available, perform a simple transition without a response if (atcCandidates.length === 0 && input.candidates.length > 0) { return finalize({ next_state: input.candidates[0].id }) } - // Kompakter aber informativer Prompt - mit Variable-Info für intelligente Responses + // Compact yet informative prompt — includes variable info for intelligent responses const system = [ 'You are an ATC state router. Return strict JSON.', 'Keys: next_state, controller_say_tpl (optional), off_schema (optional), intent (optional).', @@ -593,7 +593,7 @@ export async function routeDecision(input: LLMDecisionInput): Promise/?]/.test(trimmed)) { - return { valid: false, message: 'Mindestens ein Sonderzeichen erhöht die Sicherheit.' } + return { valid: false, message: 'Include at least one special character for better security.' } } return { valid: true } }