diff --git a/apps/rebreak-binder-mac/.gitignore b/apps/rebreak-magic-mac/.gitignore similarity index 100% rename from apps/rebreak-binder-mac/.gitignore rename to apps/rebreak-magic-mac/.gitignore diff --git a/apps/rebreak-binder-mac/README.md b/apps/rebreak-magic-mac/README.md similarity index 100% rename from apps/rebreak-binder-mac/README.md rename to apps/rebreak-magic-mac/README.md diff --git a/apps/rebreak-binder-mac/Sources/Models/DeviceState.swift b/apps/rebreak-magic-mac/Sources/Models/DeviceState.swift similarity index 100% rename from apps/rebreak-binder-mac/Sources/Models/DeviceState.swift rename to apps/rebreak-magic-mac/Sources/Models/DeviceState.swift diff --git a/apps/rebreak-binder-mac/Sources/Models/WizardModel.swift b/apps/rebreak-magic-mac/Sources/Models/WizardModel.swift similarity index 100% rename from apps/rebreak-binder-mac/Sources/Models/WizardModel.swift rename to apps/rebreak-magic-mac/Sources/Models/WizardModel.swift diff --git a/apps/rebreak-binder-mac/Sources/Models/WizardStep.swift b/apps/rebreak-magic-mac/Sources/Models/WizardStep.swift similarity index 100% rename from apps/rebreak-binder-mac/Sources/Models/WizardStep.swift rename to apps/rebreak-magic-mac/Sources/Models/WizardStep.swift diff --git a/apps/rebreak-binder-mac/Sources/RebreakBinderApp.swift b/apps/rebreak-magic-mac/Sources/RebreakMagicApp.swift similarity index 100% rename from apps/rebreak-binder-mac/Sources/RebreakBinderApp.swift rename to apps/rebreak-magic-mac/Sources/RebreakMagicApp.swift diff --git a/apps/rebreak-binder-mac/Sources/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/apps/rebreak-magic-mac/Sources/Resources/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from apps/rebreak-binder-mac/Sources/Resources/Assets.xcassets/AccentColor.colorset/Contents.json rename to apps/rebreak-magic-mac/Sources/Resources/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/apps/rebreak-binder-mac/Sources/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/apps/rebreak-magic-mac/Sources/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from apps/rebreak-binder-mac/Sources/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json rename to apps/rebreak-magic-mac/Sources/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/apps/rebreak-binder-mac/Sources/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128.png b/apps/rebreak-magic-mac/Sources/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128.png similarity index 100% rename from apps/rebreak-binder-mac/Sources/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128.png rename to apps/rebreak-magic-mac/Sources/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128.png diff --git a/apps/rebreak-binder-mac/Sources/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png b/apps/rebreak-magic-mac/Sources/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png similarity index 100% rename from apps/rebreak-binder-mac/Sources/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png rename to apps/rebreak-magic-mac/Sources/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png diff --git a/apps/rebreak-binder-mac/Sources/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16.png b/apps/rebreak-magic-mac/Sources/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16.png similarity index 100% rename from apps/rebreak-binder-mac/Sources/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16.png rename to apps/rebreak-magic-mac/Sources/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16.png diff --git a/apps/rebreak-binder-mac/Sources/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png b/apps/rebreak-magic-mac/Sources/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png similarity index 100% rename from apps/rebreak-binder-mac/Sources/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png rename to apps/rebreak-magic-mac/Sources/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png diff --git a/apps/rebreak-binder-mac/Sources/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256.png b/apps/rebreak-magic-mac/Sources/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256.png similarity index 100% rename from apps/rebreak-binder-mac/Sources/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256.png rename to apps/rebreak-magic-mac/Sources/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256.png diff --git a/apps/rebreak-binder-mac/Sources/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png b/apps/rebreak-magic-mac/Sources/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png similarity index 100% rename from apps/rebreak-binder-mac/Sources/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png rename to apps/rebreak-magic-mac/Sources/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png diff --git a/apps/rebreak-binder-mac/Sources/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32.png b/apps/rebreak-magic-mac/Sources/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32.png similarity index 100% rename from apps/rebreak-binder-mac/Sources/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32.png rename to apps/rebreak-magic-mac/Sources/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32.png diff --git a/apps/rebreak-binder-mac/Sources/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png b/apps/rebreak-magic-mac/Sources/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png similarity index 100% rename from apps/rebreak-binder-mac/Sources/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png rename to apps/rebreak-magic-mac/Sources/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png diff --git a/apps/rebreak-binder-mac/Sources/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512.png b/apps/rebreak-magic-mac/Sources/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512.png similarity index 100% rename from apps/rebreak-binder-mac/Sources/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512.png rename to apps/rebreak-magic-mac/Sources/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512.png diff --git a/apps/rebreak-binder-mac/Sources/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png b/apps/rebreak-magic-mac/Sources/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png similarity index 100% rename from apps/rebreak-binder-mac/Sources/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png rename to apps/rebreak-magic-mac/Sources/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png diff --git a/apps/rebreak-binder-mac/Sources/Resources/Assets.xcassets/Contents.json b/apps/rebreak-magic-mac/Sources/Resources/Assets.xcassets/Contents.json similarity index 100% rename from apps/rebreak-binder-mac/Sources/Resources/Assets.xcassets/Contents.json rename to apps/rebreak-magic-mac/Sources/Resources/Assets.xcassets/Contents.json diff --git a/apps/rebreak-binder-mac/Sources/Resources/Info.plist b/apps/rebreak-magic-mac/Sources/Resources/Info.plist similarity index 100% rename from apps/rebreak-binder-mac/Sources/Resources/Info.plist rename to apps/rebreak-magic-mac/Sources/Resources/Info.plist diff --git a/apps/rebreak-binder-mac/Sources/Services/DeviceDetector.swift b/apps/rebreak-magic-mac/Sources/Services/DeviceDetector.swift similarity index 100% rename from apps/rebreak-binder-mac/Sources/Services/DeviceDetector.swift rename to apps/rebreak-magic-mac/Sources/Services/DeviceDetector.swift diff --git a/apps/rebreak-binder-mac/Sources/Services/MDMClient.swift b/apps/rebreak-magic-mac/Sources/Services/MDMClient.swift similarity index 100% rename from apps/rebreak-binder-mac/Sources/Services/MDMClient.swift rename to apps/rebreak-magic-mac/Sources/Services/MDMClient.swift diff --git a/apps/rebreak-binder-mac/Sources/Services/MDMStatus.swift b/apps/rebreak-magic-mac/Sources/Services/MDMStatus.swift similarity index 100% rename from apps/rebreak-binder-mac/Sources/Services/MDMStatus.swift rename to apps/rebreak-magic-mac/Sources/Services/MDMStatus.swift diff --git a/apps/rebreak-binder-mac/Sources/Services/Paths.swift b/apps/rebreak-magic-mac/Sources/Services/Paths.swift similarity index 100% rename from apps/rebreak-binder-mac/Sources/Services/Paths.swift rename to apps/rebreak-magic-mac/Sources/Services/Paths.swift diff --git a/apps/rebreak-binder-mac/Sources/Services/ProcessRunner.swift b/apps/rebreak-magic-mac/Sources/Services/ProcessRunner.swift similarity index 100% rename from apps/rebreak-binder-mac/Sources/Services/ProcessRunner.swift rename to apps/rebreak-magic-mac/Sources/Services/ProcessRunner.swift diff --git a/apps/rebreak-binder-mac/Sources/Services/SuperviseRunner.swift b/apps/rebreak-magic-mac/Sources/Services/SuperviseRunner.swift similarity index 100% rename from apps/rebreak-binder-mac/Sources/Services/SuperviseRunner.swift rename to apps/rebreak-magic-mac/Sources/Services/SuperviseRunner.swift diff --git a/apps/rebreak-binder-mac/Sources/Views/ConfigureView.swift b/apps/rebreak-magic-mac/Sources/Views/ConfigureView.swift similarity index 100% rename from apps/rebreak-binder-mac/Sources/Views/ConfigureView.swift rename to apps/rebreak-magic-mac/Sources/Views/ConfigureView.swift diff --git a/apps/rebreak-binder-mac/Sources/Views/ContentView.swift b/apps/rebreak-magic-mac/Sources/Views/ContentView.swift similarity index 100% rename from apps/rebreak-binder-mac/Sources/Views/ContentView.swift rename to apps/rebreak-magic-mac/Sources/Views/ContentView.swift diff --git a/apps/rebreak-binder-mac/Sources/Views/DoneView.swift b/apps/rebreak-magic-mac/Sources/Views/DoneView.swift similarity index 100% rename from apps/rebreak-binder-mac/Sources/Views/DoneView.swift rename to apps/rebreak-magic-mac/Sources/Views/DoneView.swift diff --git a/apps/rebreak-binder-mac/Sources/Views/EnrollView.swift b/apps/rebreak-magic-mac/Sources/Views/EnrollView.swift similarity index 100% rename from apps/rebreak-binder-mac/Sources/Views/EnrollView.swift rename to apps/rebreak-magic-mac/Sources/Views/EnrollView.swift diff --git a/apps/rebreak-binder-mac/Sources/Views/PreflightView.swift b/apps/rebreak-magic-mac/Sources/Views/PreflightView.swift similarity index 100% rename from apps/rebreak-binder-mac/Sources/Views/PreflightView.swift rename to apps/rebreak-magic-mac/Sources/Views/PreflightView.swift diff --git a/apps/rebreak-binder-mac/Sources/Views/StepIndicator.swift b/apps/rebreak-magic-mac/Sources/Views/StepIndicator.swift similarity index 100% rename from apps/rebreak-binder-mac/Sources/Views/StepIndicator.swift rename to apps/rebreak-magic-mac/Sources/Views/StepIndicator.swift diff --git a/apps/rebreak-binder-mac/Sources/Views/SuperviseView.swift b/apps/rebreak-magic-mac/Sources/Views/SuperviseView.swift similarity index 100% rename from apps/rebreak-binder-mac/Sources/Views/SuperviseView.swift rename to apps/rebreak-magic-mac/Sources/Views/SuperviseView.swift diff --git a/apps/rebreak-binder-mac/Sources/Views/WelcomeView.swift b/apps/rebreak-magic-mac/Sources/Views/WelcomeView.swift similarity index 100% rename from apps/rebreak-binder-mac/Sources/Views/WelcomeView.swift rename to apps/rebreak-magic-mac/Sources/Views/WelcomeView.swift diff --git a/apps/rebreak-binder-mac/project.yml b/apps/rebreak-magic-mac/project.yml similarity index 100% rename from apps/rebreak-binder-mac/project.yml rename to apps/rebreak-magic-mac/project.yml diff --git a/apps/rebreak-native/.env.deploy.local.example b/apps/rebreak-native/.deploy-secrets.local.example similarity index 100% rename from apps/rebreak-native/.env.deploy.local.example rename to apps/rebreak-native/.deploy-secrets.local.example diff --git a/apps/rebreak-native/dev-ios.sh b/apps/rebreak-native/dev-ios.sh deleted file mode 100755 index bdf8e6d..0000000 --- a/apps/rebreak-native/dev-ios.sh +++ /dev/null @@ -1,67 +0,0 @@ -#!/bin/bash -# Rebreak Native: Dev-Server (Metro) + iOS Build & Run -# Pendant zu apps/rebreak/dev-ios.sh, aber für React Native + Expo statt Capacitor. -set -e - -cd "$(dirname "$0")" - -echo "🧠 Rebreak Native iOS Dev" -echo "=========================" - -# Modi: -# ./dev-ios.sh → öffnet Xcode-Workspace (Default — User baut auf iPhone) -# ./dev-ios.sh --device → physisches iPhone via USB (CLI-Build + Auto-Launch) -# ./dev-ios.sh --simulator → iOS Simulator (schnellster Test-Loop, weniger Auth/Push-Test) -MODE="${1:-xcode}" - -# Metro-Port wird NICHT mehr automatisch gekillt — falls du Metro in einem -# anderen Terminal laufen hast, würde das hier die Session zerstören. -# Falls Metro hängt: manuell `lsof -ti:8081 | xargs kill -9` ausführen. - -# Cocoapods: läuft beim ersten Run automatisch via expo run:ios. -# Falls "objectVersion 70 not supported" Fehler unter Xcode 26 → CocoaPods updaten: -# sudo gem install cocoapods --pre -# -# Podfile-Fixes werden durch Config-Plugins automatisch reinpatcht: -# - plugins/with-fmt-consteval-fix.js → FMT_USE_CONSTEVAL=0 (Xcode 16 + RN 0.79) -# - plugins/with-rebreak-protection-ios.js → NEFilter Extension Target -# → expo prebuild --clean ist daher SAFE (Plugins regenerieren die Patches). -# -# Für radikalen Cache-Reset: ./clean-ios.sh -# Bei Build-Errors aus dem Nichts: ./clean-ios.sh --build - -# 3. Run je nach Mode -case "$MODE" in - --xcode|xcode|"") - # Falls Xcode bereits mit Rebreak.xcodeproj (statt .xcworkspace) offen ist, - # schließe ihn erst. Sonst kriegst du zwei Project-Windows. - osascript -e 'tell application "Xcode" to close every window whose name contains "Rebreak.xcodeproj"' 2>/dev/null || true - - echo "🔨 Opening Xcode-Workspace..." - open -a Xcode ios/Rebreak.xcworkspace - echo "" - echo "✅ Xcode offen — In Xcode:" - echo " 1. iPhone via USB anschließen (falls nicht schon)" - echo " 2. Top-Bar: iPhone als Run-Target wählen (links neben Play-Button)" - echo " 3. Cmd+R für Build & Run auf iPhone" - echo "" - echo "ℹ️ Metro: separat starten via 'pnpm expo start --dev-client' falls noch nicht läuft" - ;; - - --device|device) - echo "📱 Building für physisches iPhone (USB)..." - echo "ℹ️ Erste Mal: Xcode wird geöffnet zum Signing-Setup." - pnpm expo run:ios --device - ;; - - --simulator|simulator) - echo "📱 Building für iOS Simulator..." - pnpm expo run:ios - ;; - - *) - echo "Unknown mode: $MODE" - echo "Usage: ./dev-ios.sh [--xcode|--device|--simulator]" - exit 1 - ;; -esac diff --git a/apps/rebreak-native/dev-iphone.sh b/apps/rebreak-native/dev-iphone.sh deleted file mode 100755 index ba47681..0000000 --- a/apps/rebreak-native/dev-iphone.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/bin/bash -# Rebreak Native — Dev auf physischem iPhone (kein Simulator). -# -# Was es macht: -# - Killt alte Metro-Instanzen auf 8081 (saubere Session) -# - Startet Metro mit --host lan damit iPhone via WiFi connecten kann -# - Druckt deine LAN-IP zum manuellen Eintragen falls Bonjour failt -# -# Auf iPhone: -# 1. Mac + iPhone müssen im SELBEN WiFi sein -# 2. App komplett killen (App-Switcher → swipe up) -# 3. App neu öffnen — dev-client sollte Metro automatisch finden -# 4. Falls nicht: dev-launcher → "Enter URL manually" → http://:8081 -# -# WICHTIG: Im Metro-Terminal NICHT `i` drücken — sonst startet Simulator! -# Nur `r` für Reload. -set -e -cd "$(dirname "$0")" - -# FamilyControls-Flag: default ON für lokale Dev-Builds, override via -# REBREAK_ENABLE_FAMILY_CONTROLS=0 ./dev-iphone.sh -# wenn man bewusst ohne FC testen will (z.B. TestFlight-Parity-Check). -# app.config.ts liest die Var um Constants.expoConfig.extra.familyControlsEnabled -# zu setzen → lib/protection.ts.FAMILY_CONTROLS_AVAILABLE → Blocker-UI. -export REBREAK_ENABLE_FAMILY_CONTROLS="${REBREAK_ENABLE_FAMILY_CONTROLS:-1}" - -echo "🧹 Killing old Metro on port 8081..." -lsof -ti:8081 | xargs kill -9 2>/dev/null || true - -echo "" -echo "📡 Mac LAN-IP für iPhone:" -ipconfig getifaddr en0 2>/dev/null || ipconfig getifaddr en1 2>/dev/null || echo " (kein WiFi/Ethernet detected)" -echo "" -echo "ℹ️ Falls dev-client Metro nicht automatisch findet:" -echo " im iPhone-Launcher → 'Enter URL manually' → http://:8081" -echo "" -echo "🚀 Starting Metro with --host lan..." -echo " (Drücke 'r' für Reload, NICHT 'i' — sonst startet Simulator!)" -echo "" - -exec pnpm expo start --host lan --clear --dev-client diff --git a/apps/rebreak-native/metro.sh b/apps/rebreak-native/metro.sh deleted file mode 100755 index 29fb8cc..0000000 --- a/apps/rebreak-native/metro.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/bash -# Rebreak Native: Metro Bundler (Kill + Clean Restart). -# Killt jede laufende Instanz auf 8081, dann frischer Start mit --clear-Cache. -# Aufruf: ./metro.sh (mit cache-clear, Default) -# ./metro.sh --keep (ohne --clear, schneller wenn keine Dependency-Changes) -set -e - -cd "$(dirname "$0")" - -echo "🚇 Metro Bundler" -echo "================" - -# 1) Existierende Metro-Instanz auf Port 8081 killen -PIDS=$(lsof -iTCP:8081 -sTCP:LISTEN -n -P 2>/dev/null | awk 'NR>1 {print $2}' | sort -u) -if [ -n "$PIDS" ]; then - echo "→ Killing existing Metro on :8081 (PIDs: $PIDS)" - echo "$PIDS" | xargs kill -9 2>/dev/null || true - sleep 1 -else - echo "→ Kein Metro auf :8081 aktiv" -fi - -# 2) Stale node-Prozesse die expo CLI gestartet haben (Belt-and-Suspenders) -pkill -f "expo start" 2>/dev/null || true -pkill -f "react-native/cli/build" 2>/dev/null || true - -# 3) Start -if [ "$1" = "--keep" ]; then - echo "→ Starte Metro (Cache behalten)" - exec npx expo start -else - echo "→ Starte Metro mit --clear (Haste-Map + Transformer-Cache reset)" - exec npx expo start --clear -fi diff --git a/backend/server/api/coach/message.post.ts b/backend/server/api/coach/message.post.ts index df6f2d9..2de84b8 100644 --- a/backend/server/api/coach/message.post.ts +++ b/backend/server/api/coach/message.post.ts @@ -305,6 +305,7 @@ import { usePrisma } from "../../utils/prisma"; import { getMemoriesForUser, markReferenced } from "../../db/lyraMemory"; import { extractAndStoreMemories } from "../../utils/lyraMemoryExtract"; import { stripMarkdown } from "../../utils/strip-markdown"; +import { detectLang } from "../../utils/detect-lang"; /** * Lyra-Plan-Beschreibung dynamisch aus PLAN_LIMITS generieren. @@ -476,17 +477,27 @@ export default defineEventHandler(async (event) => { generatePlanDetails(), ); - // Sprach-Instruktion - const LANG_INSTRUCTIONS: Record = { - de: "Antworte IMMER auf Deutsch, egal in welcher Sprache der User schreibt.", - en: "Always respond in English, regardless of what language the user writes in.", - tr: "Her zaman Türkçe yanıt ver, kullanıcı hangi dilde yazarsa yazsın.", - ar: "رد دائماً باللغة العربية، بغض النظر عن اللغة التي يكتب بها المستخدم.", - fr: "Réponds toujours en français, quelle que soit la langue dans laquelle l'utilisateur écrit.", + // Sprach-Instruktion: dynamisch nach (a) Sprache der letzten User-Message + // und (b) App-Sprach-Einstellung als Fallback. Vorher war's hartcodiert + // "antworte IMMER auf X" — wenn User mitten im Chat die Sprache wechselte, + // antwortete Lyra weiter in der App-Sprache. Jetzt: match user. + const LANG_NAMES: Record = { + de: "German", en: "English", tr: "Turkish", ar: "Arabic", + fr: "French", es: "Spanish", it: "Italian", pt: "Portuguese", + ru: "Russian", ja: "Japanese", ko: "Korean", zh: "Chinese", + he: "Hebrew", th: "Thai", }; - const langInstruction = - LANG_INSTRUCTIONS[locale ?? "de"] ?? LANG_INSTRUCTIONS.de; - systemPrompt = `${langInstruction}\n\n${systemPrompt}`; + const lastUserMsg = [...messages].reverse().find((m) => m.role === "user"); + const detectedFromUser = detectLang(lastUserMsg?.content ?? "", locale); + const appLangCode = locale ? locale.split("-")[0].toLowerCase() : null; + const appLangName = + (appLangCode && LANG_NAMES[appLangCode]) || "the user's language"; + const detectedName = + (detectedFromUser && LANG_NAMES[detectedFromUser]) || null; + const langInstruction = detectedName + ? `LANGUAGE: Reply in ${detectedName} — match the language the user just wrote in. If they switch languages, switch with them. App default fallback: ${appLangName}.` + : `LANGUAGE: Reply in ${appLangName} (the user's app language). If the user clearly writes in another language, match theirs.`; + systemPrompt = `${langInstruction}\n\n${systemPrompt}\n\n${langInstruction}`; // Plan-Kontext injizieren const PLAN_LABELS: Record = { diff --git a/backend/server/api/coach/sos-stream.get.ts b/backend/server/api/coach/sos-stream.get.ts index 2c0c16c..84543c4 100644 --- a/backend/server/api/coach/sos-stream.get.ts +++ b/backend/server/api/coach/sos-stream.get.ts @@ -23,6 +23,7 @@ import { COACH_SYSTEM_PROMPT } from "./message.post"; import { getMemoriesForUser, markReferenced } from "../../db/lyraMemory"; import { extractAndStoreMemories } from "../../utils/lyraMemoryExtract"; import { getProfile } from "../../db/profile"; +import { detectLang } from "../../utils/detect-lang"; const SOS_INSTRUCTION = `\n\nDU BEFINDEST DICH IN EINEM AKUTEN SOS-MOMENT. WICHTIGE REGELN: - Antworte als REINER TEXT, KEINE JSON-Wrapper, KEIN Markdown, KEINE Aufzählungen. @@ -89,15 +90,25 @@ export default defineEventHandler(async (event) => { const config = useRuntimeConfig(); - // System-Prompt: Coach-Basis + SOS-Streaming-Regeln - const LANG: Record = { - de: "Antworte IMMER auf Deutsch.", - en: "Always respond in English.", - tr: "Her zaman Türkçe yanıt ver.", - ar: "رد دائماً باللغة العربية.", - fr: "Réponds toujours en français.", + // Sprach-Instruktion: dynamisch nach User-Message-Sprache + App-Locale- + // Fallback. Vorher hardcoded "antworte immer auf X" — ignorierte + // Sprach-Wechsel mitten in der Session. + const LANG_NAMES: Record = { + de: "German", en: "English", tr: "Turkish", ar: "Arabic", + fr: "French", es: "Spanish", it: "Italian", pt: "Portuguese", + ru: "Russian", ja: "Japanese", ko: "Korean", zh: "Chinese", + he: "Hebrew", th: "Thai", }; - const lang = LANG[locale ?? "de"] ?? LANG.de; + const lastUserMsg = [...messages].reverse().find((m) => m.role === "user"); + const detectedFromUser = detectLang(lastUserMsg?.content ?? "", locale); + const appLangCode = locale ? locale.split("-")[0].toLowerCase() : null; + const appLangName = + (appLangCode && LANG_NAMES[appLangCode]) || "the user's language"; + const detectedName = + (detectedFromUser && LANG_NAMES[detectedFromUser]) || null; + const lang = detectedName + ? `LANGUAGE: Reply in ${detectedName} — match the language the user just wrote in. If they switch languages, switch with them. App default fallback: ${appLangName}.` + : `LANGUAGE: Reply in ${appLangName} (the user's app language). If the user clearly writes in another language, match theirs.`; // Memory-Injection: Lyra-Erinnerungen aus früheren Sessions laden let memoryBlock = ""; @@ -189,7 +200,7 @@ export default defineEventHandler(async (event) => { console.error("[sos-stream] profile load (non-fatal):", e); } - const systemPrompt = `${nicknamePrefix}${demographicsBlock}${memoryBlock}${lang}\n\n${COACH_SYSTEM_PROMPT.replace("{{PLAN_DETAILS}}", "")}${SOS_INSTRUCTION}`; + const systemPrompt = `${nicknamePrefix}${demographicsBlock}${memoryBlock}${lang}\n\n${COACH_SYSTEM_PROMPT.replace("{{PLAN_DETAILS}}", "")}${SOS_INSTRUCTION}\n\n${lang}`; // Erste Nachricht muss user sein const firstUserIdx = messages.findIndex((m) => m.role === "user"); diff --git a/backend/server/api/coach/speak-cartesia.post.ts b/backend/server/api/coach/speak-cartesia.post.ts index 442d941..359a75a 100644 --- a/backend/server/api/coach/speak-cartesia.post.ts +++ b/backend/server/api/coach/speak-cartesia.post.ts @@ -1,22 +1,28 @@ /** * POST /api/coach/speak-cartesia - * Cartesia Sonic-2 — schnellstes TTS (~75ms first-byte), native German. + * Cartesia Sonic-3 — schnellstes TTS (~75ms first-byte), 42 Sprachen multilingual. * * Returns audio/mpeg. Voice via runtimeConfig.cartesiaVoiceId * (Fallback `b9de4a89-2257-424b-94c2-db18ba68c81a` wenn unset). */ +import { detectLang } from "../../utils/detect-lang"; + const FALLBACK_VOICE_ID = "b9de4a89-2257-424b-94c2-db18ba68c81a"; export default defineEventHandler(async (event) => { await requireUser(event); const body = await readBody(event); - const { text } = body as { text: string }; + const { text, locale } = body as { text: string; locale?: string }; if (!text?.trim()) { throw createError({ statusCode: 400, message: "text fehlt" }); } + // Sprache dynamisch: Text-Script-Detection > App-Locale-Hint > null (Provider + // auto-detect). Kein "de"-Default mehr, sonst klingt arabischer Text deutsch. + const lang = detectLang(text, locale); + const config = useRuntimeConfig(); const key = (config.cartesiaApiKey as string) || process.env.CARTESIA_API_KEY || ""; @@ -40,7 +46,8 @@ export default defineEventHandler(async (event) => { "Content-Type": "application/json", }, body: JSON.stringify({ - model_id: "sonic-2", + // sonic-3 unterstützt 42 Sprachen inkl. ar/tr (sonic-2 = nur de/en). + model_id: "sonic-3", transcript: text.slice(0, 4096), voice: { mode: "id", id: voiceId }, output_format: { @@ -48,7 +55,9 @@ export default defineEventHandler(async (event) => { sample_rate: 22050, bit_rate: 64000, }, - language: "de", + // language nur setzen wenn aus Detection oder Locale ableitbar — + // sonst Sonic-3 auto-detecten lassen. + ...(lang ? { language: lang } : {}), }), }); diff --git a/backend/server/api/coach/speak-elevenlabs.post.ts b/backend/server/api/coach/speak-elevenlabs.post.ts index 1cf45a6..18d0371 100644 --- a/backend/server/api/coach/speak-elevenlabs.post.ts +++ b/backend/server/api/coach/speak-elevenlabs.post.ts @@ -6,18 +6,24 @@ * Returns audio/mpeg. Voice ist deterministisch konstant über mehrere Calls * — identisch zu Gemini-Verhalten, kein Mode-Switch wie bei gpt-4o-mini-tts. */ +import { detectLang } from "../../utils/detect-lang"; + const FALLBACK_VOICE_ID = "kdmDKE6EkgrWrrykO9Qt"; // Alexandra export default defineEventHandler(async (event) => { await requireUser(event); const body = await readBody(event); - const { text } = body as { text: string }; + const { text, locale } = body as { text: string; locale?: string }; if (!text?.trim()) { throw createError({ statusCode: 400, message: "text fehlt" }); } + // Sprache dynamisch ableiten (Text-Script > Locale-Hint > null) statt + // hardcoded "de" — sonst landen arabische Texte in deutscher Phonetik. + const lang = detectLang(text, locale); + const config = useRuntimeConfig(); // Fallback chain: runtimeConfig (Nuxt build-time) → process.env (runtime injection // via Infisical at pm2-start). Stellt sicher dass auch dann ein Key vorhanden ist @@ -66,6 +72,9 @@ export default defineEventHandler(async (event) => { // Turbo v2.5: ~50% schneller als multilingual_v2, marginal niedrigere // Quality — Trade-off lohnt sich für SOS (latency > Studio-Polish). model_id: "eleven_turbo_v2_5", + // language_code nur explizit wenn wir ihn aus Detection/Locale + // ableiten konnten — sonst ElevenLabs auto-detecten lassen. + ...(lang ? { language_code: lang } : {}), voice_settings: { stability: 0.5, similarity_boost: 0.75, diff --git a/backend/server/api/coach/speak.post.ts b/backend/server/api/coach/speak.post.ts index d2b3430..575d377 100644 --- a/backend/server/api/coach/speak.post.ts +++ b/backend/server/api/coach/speak.post.ts @@ -1,6 +1,7 @@ import type { H3Event } from "h3"; import type { VoiceConfig } from "../../utils/plan-features"; import { getPlanLimits } from "../../utils/plan-features"; +import { detectLang } from "../../utils/detect-lang"; import { getRemainingVoiceQuota, consumeVoiceQuota, @@ -42,9 +43,10 @@ export default defineEventHandler(async (event) => { } const trimmed = text.slice(0, 4096); - // i18n-Locale (z.B. "ar", "de-DE") → 2-Buchstaben-Basis-Sprachcode für die - // Provider. Ohne das sprach Lyra arabischen Text mit deutscher Stimme/Phonetik. - const lang = (locale ?? "de").split("-")[0].toLowerCase(); + // Sprache dynamisch: erst Text-Script-Detection (Arabic/Cyrillic/CJK/Turkish + // letters …), Fallback auf App-Locale aus body. Kein "de"-Default mehr — + // wenn beides leer ist, lassen wir den Provider auto-detecten. + const lang = detectLang(trimmed, locale); // ─── Load profile + plan ──────────────────────────────────────────────── const db = usePrisma(); @@ -102,7 +104,7 @@ async function speakGoogle( voiceCfg: VoiceConfig, userId: string, plan: string, - lang: string, + lang: string | null, ) { const key = (config.googleApiKey as string) || process.env.GOOGLE_API_KEY || ""; if (!key) { @@ -117,7 +119,9 @@ async function speakGoogle( de: "de-DE", en: "en-US", fr: "fr-FR", ar: "ar-XA", tr: "tr-TR", es: "es-ES", pt: "pt-PT", it: "it-IT", }; - const languageCode = GOOGLE_LANG[lang] ?? "de-DE"; + // Google verlangt zwingend languageCode — wenn weder Detection noch Locale + // greifen, neutraler en-US-Fallback statt de-DE-Bias. + const languageCode = (lang && GOOGLE_LANG[lang]) || "en-US"; const voiceName = lang === "de" ? (voiceCfg.model ?? "de-DE-Neural2-F") : undefined; const response = await fetch( @@ -171,7 +175,7 @@ async function speakCartesia( voiceCfg: VoiceConfig, userId: string, plan: string, - lang: string, + lang: string | null, ) { const key = (config.cartesiaApiKey as string) || process.env.CARTESIA_API_KEY || ""; if (!key) { @@ -201,9 +205,9 @@ async function speakCartesia( sample_rate: 22050, bit_rate: 64000, }, - // Sonic-3 unterstützt 42 Sprachen inkl. ar — language aus User-Locale - // statt hardcoded "de", sonst klingt arabischer Text deutsch-phonetisch. - language: lang, + // Sonic-3 unterstützt 42 Sprachen — wenn Detection greift, language + // explizit setzen, sonst Provider auto-detecten lassen. + ...(lang ? { language: lang } : {}), }), }); @@ -231,7 +235,7 @@ async function speakElevenLabs( userId: string, plan: string, userLyraVoiceId: string | null = null, - lang: string = "de", + lang: string | null = null, ) { const key = (config.elevenlabsApiKey as string) || process.env.ELEVENLABS_API_KEY || ""; @@ -263,9 +267,10 @@ async function speakElevenLabs( text, model_id: modelId, // Turbo v2.5 ist multilingual (32 Sprachen inkl. ar) — dieselbe Stimme - // spricht die Zielsprache. language_code explizit setzen statt nur - // Auto-Detect, damit kurze Texte zuverlässig in der User-Sprache landen. - language_code: lang, + // spricht die Zielsprache. language_code nur explizit setzen wenn wir + // ihn aus Detection/Locale ableiten konnten; sonst ElevenLabs + // auto-detecten lassen statt einen falschen Code aufzuzwingen. + ...(lang ? { language_code: lang } : {}), voice_settings: { stability: 0.5, similarity_boost: 0.75, diff --git a/backend/server/utils/detect-lang.ts b/backend/server/utils/detect-lang.ts new file mode 100644 index 0000000..6f68d1a --- /dev/null +++ b/backend/server/utils/detect-lang.ts @@ -0,0 +1,36 @@ +/** + * Detect language from text using Unicode script ranges. + * + * Non-Latin scripts are detected reliably from a single character. For Latin + * scripts (de/en/fr/tr/es/it/pt …) we fall back to the supplied locale-hint, + * since distinguishing them needs a real NLP library and the user-facing + * App-Sprache is a perfectly good signal. + * + * Returns a 2-letter ISO code, or null if neither detection nor hint apply. + */ +export function detectLang( + text: string, + localeHint?: string | null, +): string | null { + if (text) { + // Sample a window — first 300 chars is plenty; counting script hits is + // cheaper than scanning multi-KB Lyra-Antworten. + const sample = text.slice(0, 300); + if (/[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF]/.test(sample)) return "ar"; // Arabic + if (/[\u0400-\u04FF]/.test(sample)) return "ru"; // Cyrillic + if (/[\u3040-\u309F\u30A0-\u30FF]/.test(sample)) return "ja"; // Hiragana/Katakana + if (/[\uAC00-\uD7AF]/.test(sample)) return "ko"; // Hangul + if (/[\u4E00-\u9FFF]/.test(sample)) return "zh"; // CJK Unified Ideographs + if (/[\u0590-\u05FF]/.test(sample)) return "he"; // Hebrew + if (/[\u0E00-\u0E7F]/.test(sample)) return "th"; // Thai + // Turkish-specific Latin letters — strong hint without an NLP lib. + if (/[ğĞıİşŞ]/.test(sample)) return "tr"; + } + + if (localeHint) { + const base = localeHint.split("-")[0].toLowerCase(); + if (base) return base; + } + + return null; +}