Fix remaining German comment

This commit is contained in:
Remi
2025-09-20 09:46:34 +02:00
parent 103fdd6bbc
commit 440aef2f62
9 changed files with 63 additions and 63 deletions

View File

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

View File

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

View File

@@ -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(() => {});

View File

@@ -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 = {

View File

@@ -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 NormalizerTTS 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 normalizerTTS 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 (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 N2N4"
notes?: string; // e.g. "TWY N closed between N2N4"
}) {
// 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");
}
/** PilotATC (rückwärtskompatibler Name, aber mit robustem Rahmen) */
/** PilotATC (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<string,string> = {
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."
— PilotATC:
— PilotATC:
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 NormalizerTTS.
After receiving the LLM output, call `speakATC(llmText)` to trigger normalizerTTS.
*/

View File

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

View File

@@ -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<typeof OpenAI>[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<LLMDecisio
}
}
// Sofortige Erkennung ohne LLM für häufige Cases
// Instant detection without the LLM for common cases
if (pilotText.includes('radio check') || pilotText.includes('signal test') ||
(pilotText.includes('read') && (pilotText.includes('check') || pilotText.includes('you')))) {
return finalize({
@@ -479,17 +479,17 @@ export async function routeDecision(input: LLMDecisionInput): Promise<LLMDecisio
const optimizedInput = optimizeInputForLLM(input)
// Prüfe ob nächste States ATC-Responses brauchen
// Check whether the next states require ATC responses
const atcCandidates = input.candidates.filter(c =>
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<LLMDecisio
})
}
// Pilot readback oder acknowledgment → keine ATC response nötig
// Pilot readback or acknowledgment → no ATC response required
if (pilotText.includes('wilco') || pilotText.includes('roger') ||
pilotText.includes('cleared') || pilotText.includes('copied')) {
fallbackInfo.selected = 'acknowledge'

View File

@@ -58,7 +58,7 @@ export function getServerRuntimeConfig(): ServerRuntimeConfig {
const openaiKey = String(runtimeConfig.openaiKey || '').trim()
if (!openaiKey && !warnedMissingOpenAIKey) {
console.warn('[OpenSquawk] OPENAI_API_KEY fehlt. Einige KI-Funktionen stehen ohne Schlüssel nicht zur Verfügung.')
console.warn('[OpenSquawk] OPENAI_API_KEY is missing. Some AI features are unavailable without a key.')
warnedMissingOpenAIKey = true
}

View File

@@ -12,16 +12,16 @@ export interface PasswordValidationResult {
export function validatePasswordStrength(password: string): PasswordValidationResult {
const trimmed = password.trim()
if (trimmed.length < 10) {
return { valid: false, message: 'Passwort muss mindestens 10 Zeichen lang sein.' }
return { valid: false, message: 'Password must be at least 10 characters long.' }
}
if (/\s/.test(trimmed)) {
return { valid: false, message: 'Passwort darf keine Leerzeichen enthalten.' }
return { valid: false, message: 'Password cannot contain spaces.' }
}
if (!/[A-Za-zÄÖÜäöüß]/.test(trimmed) || !/[0-9]/.test(trimmed)) {
return { valid: false, message: 'Bitte Buchstaben und Zahlen kombinieren.' }
return { valid: false, message: 'Please use both letters and numbers.' }
}
if (!/[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/.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 }
}