From b194fc3130bc9c8005d38f70f85df74ac7153d3b Mon Sep 17 00:00:00 2001 From: harshana Date: Tue, 7 Apr 2026 00:35:44 +0200 Subject: [PATCH 1/3] feat: add German language support (de-DE) following new i18n system Refactored the initial German language support to align with the newly introduced i18n system. Changes: - Added de-DE.json with all translation keys. - Registered de-DE in supportedLocales. - Localized fallback strings in the generation pipeline. - Updated UI components to dynamically use supportedLocales. - Added Azure TTS German voices. - Made prompts generic for multiple languages. Closes #215 --- app/api/generate/scene-content/route.ts | 3 +- app/page.tsx | 19 +- components/generation/generation-toolbar.tsx | 23 +- lib/audio/constants.ts | 12 + lib/audio/tts-providers.ts | 9 +- lib/generation/outline-generator.ts | 21 +- lib/generation/prompt-formatters.ts | 24 +- .../requirements-to-outlines/system.md | 4 +- .../requirements-to-outlines/user.md | 2 +- lib/generation/scene-generator.ts | 79 +- lib/i18n/locales.ts | 1 + lib/i18n/locales/de-DE.json | 882 ++++++++++++++++++ lib/server/classroom-generation.ts | 6 +- lib/types/generation.ts | 6 +- 14 files changed, 1044 insertions(+), 47 deletions(-) create mode 100644 lib/i18n/locales/de-DE.json diff --git a/app/api/generate/scene-content/route.ts b/app/api/generate/scene-content/route.ts index cf4d9f341..5cbc08212 100644 --- a/app/api/generate/scene-content/route.ts +++ b/app/api/generate/scene-content/route.ts @@ -13,6 +13,7 @@ import { generateSceneContent, buildVisionUserContent, } from '@/lib/generation/generation-pipeline'; +import type { Locale } from '@/lib/i18n/types'; import type { AgentInfo } from '@/lib/generation/generation-pipeline'; import type { SceneOutline, PdfImage, ImageMapping } from '@/lib/types/generation'; import { createLogger } from '@/lib/logger'; @@ -69,7 +70,7 @@ export async function POST(req: NextRequest) { // Ensure outline has language from stageInfo (fallback for older outlines) const outline: SceneOutline = { ...rawOutline, - language: rawOutline.language || (stageInfo?.language as 'zh-CN' | 'en-US') || 'zh-CN', + language: rawOutline.language || (stageInfo?.language as Locale) || 'zh-CN', }; // ── Model resolution from request headers ── diff --git a/app/page.tsx b/app/page.tsx index 1799ea97e..906f158e9 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -32,6 +32,8 @@ import { useTheme } from '@/lib/hooks/use-theme'; import { nanoid } from 'nanoid'; import { storePdfBlob } from '@/lib/utils/image-storage'; import type { UserRequirements } from '@/lib/types/generation'; +import type { Locale } from '@/lib/i18n/types'; +import { supportedLocales } from '@/lib/i18n/locales'; import { useSettingsStore } from '@/lib/store/settings'; import { useUserProfileStore, AVATAR_OPTIONS } from '@/lib/store/user-profile'; import { @@ -58,7 +60,7 @@ const RECENT_OPEN_STORAGE_KEY = 'recentClassroomsOpen'; interface FormState { pdfFile: File | null; requirement: string; - language: 'zh-CN' | 'en-US'; + language: Locale; webSearch: boolean; } @@ -98,14 +100,21 @@ function HomePage() { } try { const savedWebSearch = localStorage.getItem(WEB_SEARCH_STORAGE_KEY); - const savedLanguage = localStorage.getItem(LANGUAGE_STORAGE_KEY); + const savedLanguage = localStorage.getItem(LANGUAGE_STORAGE_KEY) as Locale | null; const updates: Partial = {}; if (savedWebSearch === 'true') updates.webSearch = true; - if (savedLanguage === 'zh-CN' || savedLanguage === 'en-US') { + + const isSupported = (lang: string | null): lang is Locale => + !!lang && supportedLocales.some((l) => l.code === lang); + + if (isSupported(savedLanguage)) { updates.language = savedLanguage; } else { - const detected = navigator.language?.startsWith('zh') ? 'zh-CN' : 'en-US'; - updates.language = detected; + const browserLang = navigator.language; + const matched = + supportedLocales.find((l) => browserLang.startsWith(l.code.split('-')[0]))?.code || + (browserLang.startsWith('zh') ? 'zh-CN' : 'en-US'); + updates.language = matched as Locale; } if (Object.keys(updates).length > 0) { setForm((prev) => ({ ...prev, ...updates })); diff --git a/components/generation/generation-toolbar.tsx b/components/generation/generation-toolbar.tsx index 27301bbd8..feed8f682 100644 --- a/components/generation/generation-toolbar.tsx +++ b/components/generation/generation-toolbar.tsx @@ -22,14 +22,17 @@ import type { ProviderId } from '@/lib/ai/providers'; import type { SettingsSection } from '@/lib/types/settings'; import { MediaPopover } from '@/components/generation/media-popover'; +import type { Locale } from '@/lib/i18n/types'; +import { supportedLocales } from '@/lib/i18n/locales'; + // ─── Constants ─────────────────────────────────────────────── const MAX_PDF_SIZE_MB = 50; const MAX_PDF_SIZE_BYTES = MAX_PDF_SIZE_MB * 1024 * 1024; // ─── Types ─────────────────────────────────────────────────── export interface GenerationToolbarProps { - language: 'zh-CN' | 'en-US'; - onLanguageChange: (lang: 'zh-CN' | 'en-US') => void; + language: Locale; + onLanguageChange: (lang: Locale) => void; webSearch: boolean; onWebSearchChange: (v: boolean) => void; onSettingsOpen: (section?: SettingsSection) => void; @@ -64,6 +67,15 @@ export function GenerationToolbar({ const fileInputRef = useRef(null); const [isDragging, setIsDragging] = useState(false); + // Cycle language among supported locales + const cycleLanguage = () => { + const currentIndex = supportedLocales.findIndex((l) => l.code === language); + const nextIndex = (currentIndex + 1) % supportedLocales.length; + onLanguageChange(supportedLocales[nextIndex].code); + }; + + const currentLocaleInfo = supportedLocales.find((l) => l.code === language) || supportedLocales[0]; + // Check if the selected web search provider has a valid config (API key or server-configured) const webSearchProvider = WEB_SEARCH_PROVIDERS[webSearchProviderId]; const webSearchConfig = webSearchProvidersConfig[webSearchProviderId]; @@ -360,12 +372,9 @@ export function GenerationToolbar({ {/* ── Language pill ── */} - {t('toolbar.languageHint')} diff --git a/lib/audio/constants.ts b/lib/audio/constants.ts index 423f5b82c..50ca903b8 100644 --- a/lib/audio/constants.ts +++ b/lib/audio/constants.ts @@ -204,6 +204,18 @@ export const TTS_PROVIDERS: Record = { gender: 'female', }, { id: 'en-US-GuyNeural', name: 'Guy', language: 'en-US', gender: 'male' }, + { + id: 'de-DE-KatjaNeural', + name: 'Katja (女)', + language: 'de-DE', + gender: 'female', + }, + { + id: 'de-DE-ConradNeural', + name: 'Conrad (男)', + language: 'de-DE', + gender: 'male', + }, ], supportedFormats: ['mp3', 'wav', 'ogg'], speedRange: { min: 0.5, max: 2.0, default: 1.0 }, diff --git a/lib/audio/tts-providers.ts b/lib/audio/tts-providers.ts index 67f0e7cc0..434940a45 100644 --- a/lib/audio/tts-providers.ts +++ b/lib/audio/tts-providers.ts @@ -212,11 +212,16 @@ async function generateAzureTTS( ): Promise { const baseUrl = config.baseUrl || TTS_PROVIDERS['azure-tts'].defaultBaseUrl; + // Extract language from voice ID (e.g., "zh-CN-XiaoxiaoNeural" -> "zh-CN") + // Fallback to "zh-CN" if no match + const langMatch = config.voice.match(/^[a-z]{2}-[A-Z]{2}/); + const lang = langMatch ? langMatch[0] : 'zh-CN'; + // Build SSML const rate = config.speed ? `${((config.speed - 1) * 100).toFixed(0)}%` : '0%'; const ssml = ` - - + + ${escapeXml(text)} diff --git a/lib/generation/outline-generator.ts b/lib/generation/outline-generator.ts index 4849bcefe..25689cbc7 100644 --- a/lib/generation/outline-generator.ts +++ b/lib/generation/outline-generator.ts @@ -39,8 +39,10 @@ export async function generateSceneOutlinesFromRequirements( }, ): Promise> { // Build available images description for the prompt - let availableImagesText = - requirements.language === 'zh-CN' ? '无可用图片' : 'No images available'; + let availableImagesText = 'No images available'; + if (requirements.language === 'zh-CN') availableImagesText = '无可用图片'; + if (requirements.language === 'ja-JP') availableImagesText = '利用可能な画像はありません'; + if (requirements.language === 'de-DE') availableImagesText = 'Keine Bilder verfügbar'; let visionImages: Array<{ id: string; src: string }> | undefined; if (pdfImages && pdfImages.length > 0) { @@ -103,12 +105,23 @@ export async function generateSceneOutlinesFromRequirements( ? pdfText.substring(0, MAX_PDF_CONTENT_CHARS) : requirements.language === 'zh-CN' ? '无' - : 'None', + : requirements.language === 'ja-JP' + ? 'なし' + : requirements.language === 'de-DE' + ? 'Keine' + : 'None', availableImages: availableImagesText, userProfile: userProfileText, mediaGenerationPolicy, researchContext: - options?.researchContext || (requirements.language === 'zh-CN' ? '无' : 'None'), + options?.researchContext || + (requirements.language === 'zh-CN' + ? '无' + : requirements.language === 'ja-JP' + ? 'なし' + : requirements.language === 'de-DE' + ? 'Keine' + : 'None'), // Server-side generation populates this via options; client-side populates via formatTeacherPersonaForPrompt teacherContext: options?.teacherContext || '', }); diff --git a/lib/generation/prompt-formatters.ts b/lib/generation/prompt-formatters.ts index 4486ba09c..bc31a0cd3 100644 --- a/lib/generation/prompt-formatters.ts +++ b/lib/generation/prompt-formatters.ts @@ -79,12 +79,16 @@ export function formatImageDescription(img: PdfImage, language: string): string let dimInfo = ''; if (img.width && img.height) { const ratio = (img.width / img.height).toFixed(2); - dimInfo = ` | 尺寸: ${img.width}×${img.height} (宽高比${ratio})`; + if (language === 'zh-CN') dimInfo = ` | 尺寸: ${img.width}×${img.height} (宽高比${ratio})`; + else if (language === 'ja-JP') dimInfo = ` | サイズ: ${img.width}×${img.height} (アスペクト比${ratio})`; + else if (language === 'de-DE') dimInfo = ` | Größe: ${img.width}×${img.height} (Seitenverhältnis${ratio})`; + else dimInfo = ` | size: ${img.width}×${img.height} (ratio ${ratio})`; } const desc = img.description ? ` | ${img.description}` : ''; - return language === 'zh-CN' - ? `- **${img.id}**: 来自PDF第${img.pageNumber}页${dimInfo}${desc}` - : `- **${img.id}**: from PDF page ${img.pageNumber}${dimInfo}${desc}`; + if (language === 'zh-CN') return `- **${img.id}**: 来自PDF第${img.pageNumber}页${dimInfo}${desc}`; + if (language === 'ja-JP') return `- **${img.id}**: PDFの${img.pageNumber}ページ目から${dimInfo}${desc}`; + if (language === 'de-DE') return `- **${img.id}**: von PDF-Seite ${img.pageNumber}${dimInfo}${desc}`; + return `- **${img.id}**: from PDF page ${img.pageNumber}${dimInfo}${desc}`; } /** @@ -95,11 +99,15 @@ export function formatImagePlaceholder(img: PdfImage, language: string): string let dimInfo = ''; if (img.width && img.height) { const ratio = (img.width / img.height).toFixed(2); - dimInfo = ` | 尺寸: ${img.width}×${img.height} (宽高比${ratio})`; + if (language === 'zh-CN') dimInfo = ` | 尺寸: ${img.width}×${img.height} (宽高比${ratio})`; + else if (language === 'ja-JP') dimInfo = ` | サイズ: ${img.width}×${img.height} (アスペクト比${ratio})`; + else if (language === 'de-DE') dimInfo = ` | Größe: ${img.width}×${img.height} (Seitenverhältnis${ratio})`; + else dimInfo = ` | size: ${img.width}×${img.height} (ratio ${ratio})`; } - return language === 'zh-CN' - ? `- **${img.id}**: PDF第${img.pageNumber}页的图片${dimInfo} [参见附图]` - : `- **${img.id}**: image from PDF page ${img.pageNumber}${dimInfo} [see attached]`; + if (language === 'zh-CN') return `- **${img.id}**: PDF第${img.pageNumber}页的图片${dimInfo} [参见附图]`; + if (language === 'ja-JP') return `- **${img.id}**: PDFの${img.pageNumber}ページ目の画像${dimInfo} [添付画像参照]`; + if (language === 'de-DE') return `- **${img.id}**: Bild von PDF-Seite ${img.pageNumber}${dimInfo} [siehe Anhang]`; + return `- **${img.id}**: image from PDF page ${img.pageNumber}${dimInfo} [see attached]`; } /** diff --git a/lib/generation/prompts/templates/requirements-to-outlines/system.md b/lib/generation/prompts/templates/requirements-to-outlines/system.md index 72f5bc0ec..abc5c4baa 100644 --- a/lib/generation/prompts/templates/requirements-to-outlines/system.md +++ b/lib/generation/prompts/templates/requirements-to-outlines/system.md @@ -94,7 +94,7 @@ When a slide scene needs an image or video but no suitable PDF image exists, mar - **Image IDs**: use `"gen_img_1"`, `"gen_img_2"`, etc. — IDs are **globally unique across the entire course**, NOT reset per scene - **Video IDs**: use `"gen_vid_1"`, `"gen_vid_2"`, etc. — same global numbering rule - The prompt should describe the desired media clearly and specifically -- **Language in images**: If the image contains text, labels, or annotations, the prompt MUST explicitly specify that all text in the image should be in the course language (e.g., "all labels in Chinese" for zh-CN courses, "all labels in English" for en-US courses). For purely visual images without text, language does not matter. +- **Language in images**: If the image contains text, labels, or annotations, the prompt MUST explicitly specify that all text in the image should be in the course language (e.g., "all labels in German" for de-DE courses). For purely visual images without text, language does not matter. - Only request media generation when it genuinely enhances the content — not every slide needs an image or video - Video generation is slow (1-2 minutes each), so only request videos when motion genuinely enhances understanding - If a suitable PDF image exists, prefer using `suggestedImageIds` instead @@ -280,7 +280,7 @@ You must output a JSON array where each element is a scene outline object: "projectDescription": "Brief description of what students will build/accomplish", "targetSkills": ["Skill 1", "Skill 2", "Skill 3"], "issueCount": 3, - "language": "zh-CN" + "language": "de-DE" } ``` diff --git a/lib/generation/prompts/templates/requirements-to-outlines/user.md b/lib/generation/prompts/templates/requirements-to-outlines/user.md index 65d0a4921..526d32d33 100644 --- a/lib/generation/prompts/templates/requirements-to-outlines/user.md +++ b/lib/generation/prompts/templates/requirements-to-outlines/user.md @@ -14,7 +14,7 @@ Please generate scene outlines based on the following course requirements. **Required language**: {{language}} -(If language is zh-CN, all content must be in Chinese; if en-US, all content must be in English) +(Strictly generate all content in the required language: zh-CN -> Chinese, en-US -> English, ja-JP -> Japanese, de-DE -> German, etc.) --- diff --git a/lib/generation/scene-generator.ts b/lib/generation/scene-generator.ts index ff81a840f..eb3ff8990 100644 --- a/lib/generation/scene-generator.ts +++ b/lib/generation/scene-generator.ts @@ -1035,13 +1035,27 @@ export async function generateSceneActions( /** * Generate default PBL Actions (fallback) */ -function generateDefaultPBLActions(_outline: SceneOutline): Action[] { +function generateDefaultPBLActions(outline: SceneOutline): Action[] { + const lang = outline.language || 'zh-CN'; + let title = 'PBL Project Intro'; + let text = 'Let\'s start a Project-Based Learning activity. Choose your role, check the issue board, and collaborate to complete the project.'; + if (lang === 'zh-CN') { + title = 'PBL 项目介绍'; + text = '现在让我们开始一个项目式学习活动。请选择你的角色,查看任务看板,开始协作完成项目。'; + } else if (lang === 'ja-JP') { + title = 'PBLプロジェクト紹介'; + text = 'プロジェクトベース学習活動を開始しましょう。役割を選択し、課題ボードを確認して、協力してプロジェクトを完了させてください。'; + } else if (lang === 'de-DE') { + title = 'PBL Projekt-Einführung'; + text = 'Beginnen wir mit einer projektbasierten Lernaktivität. Wählen Sie Ihre Rolle, prüfen Sie das Aufgabenboard und arbeiten Sie zusammen, um das Projekt abzuschließen.'; + } + return [ { id: `action_${nanoid(8)}`, type: 'speech', - title: 'PBL 项目介绍', - text: '现在让我们开始一个项目式学习活动。请选择你的角色,查看任务看板,开始协作完成项目。', + title, + text, }, ]; } @@ -1143,26 +1157,39 @@ function processActions(actions: Action[], elements: PPTElement[], agents?: Agen */ function generateDefaultSlideActions(outline: SceneOutline, elements: PPTElement[]): Action[] { const actions: Action[] = []; + const lang = outline.language || 'zh-CN'; // Add spotlight for text elements const textElements = elements.filter((el) => el.type === 'text'); if (textElements.length > 0) { + let spotlightTitle = 'Focus'; + if (lang === 'zh-CN') spotlightTitle = '聚焦重点'; + else if (lang === 'ja-JP') spotlightTitle = '重要ポイント'; + else if (lang === 'de-DE') spotlightTitle = 'Fokus'; + actions.push({ id: `action_${nanoid(8)}`, type: 'spotlight', - title: '聚焦重点', + title: spotlightTitle, elementId: textElements[0].id, }); } // Add opening speech based on key points + let speechTitle = 'Scene Explanation'; + if (lang === 'zh-CN') speechTitle = '场景讲解'; + else if (lang === 'ja-JP') speechTitle = 'シーン解説'; + else if (lang === 'de-DE') speechTitle = 'Szenenerklärung'; + + const separator = lang === 'zh-CN' || lang === 'ja-JP' ? '。' : '. '; const speechText = outline.keyPoints?.length - ? outline.keyPoints.join('。') + '。' + ? outline.keyPoints.join(separator) + (lang === 'zh-CN' || lang === 'ja-JP' ? '。' : '.') : outline.description || outline.title; + actions.push({ id: `action_${nanoid(8)}`, type: 'speech', - title: '场景讲解', + title: speechTitle, text: speechText, }); @@ -1172,13 +1199,27 @@ function generateDefaultSlideActions(outline: SceneOutline, elements: PPTElement /** * Generate default quiz Actions (fallback) */ -function generateDefaultQuizActions(_outline: SceneOutline): Action[] { +function generateDefaultQuizActions(outline: SceneOutline): Action[] { + const lang = outline.language || 'zh-CN'; + let title = 'Quiz Intro'; + let text = 'Now let\'s take a short quiz to test what we\'ve learned.'; + if (lang === 'zh-CN') { + title = '测验引导'; + text = '现在让我们来做一个小测验,检验一下学习成果。'; + } else if (lang === 'ja-JP') { + title = 'クイズ案内'; + text = '学んだことをテストするために、短いクイズに答えましょう。'; + } else if (lang === 'de-DE') { + title = 'Quiz-Einführung'; + text = 'Machen wir nun ein kurzes Quiz, um das Gelernte zu testen.'; + } + return [ { id: `action_${nanoid(8)}`, type: 'speech', - title: '测验引导', - text: '现在让我们来做一个小测验,检验一下学习成果。', + title, + text, }, ]; } @@ -1186,13 +1227,27 @@ function generateDefaultQuizActions(_outline: SceneOutline): Action[] { /** * Generate default interactive Actions (fallback) */ -function generateDefaultInteractiveActions(_outline: SceneOutline): Action[] { +function generateDefaultInteractiveActions(outline: SceneOutline): Action[] { + const lang = outline.language || 'zh-CN'; + let title = 'Interactive Intro'; + let text = 'Now let\'s explore this concept through interactive visualization. Try interacting with the elements on the page to see how they change.'; + if (lang === 'zh-CN') { + title = '交互引导'; + text = '现在让我们通过交互式可视化来探索这个概念。请尝试操作页面中的元素,观察变化。'; + } else if (lang === 'ja-JP') { + title = 'インタラクティブ案内'; + text = 'インタラクティブな視覚化を通じて、この概念を探求しましょう。ページ上の要素を操作して、どのように変化するか確認してください。'; + } else if (lang === 'de-DE') { + title = 'Interaktive Einführung'; + text = 'Erforschen wir nun dieses Konzept durch eine interaktive Visualisierung. Versuchen Sie, mit den Elementen auf der Seite zu interagieren, um zu sehen, wie sie sich verändern.'; + } + return [ { id: `action_${nanoid(8)}`, type: 'speech', - title: '交互引导', - text: '现在让我们通过交互式可视化来探索这个概念。请尝试操作页面中的元素,观察变化。', + title, + text, }, ]; } diff --git a/lib/i18n/locales.ts b/lib/i18n/locales.ts index 8e3d69794..a87613e0a 100644 --- a/lib/i18n/locales.ts +++ b/lib/i18n/locales.ts @@ -17,4 +17,5 @@ export const supportedLocales = [ { code: 'zh-CN', label: '简体中文', shortLabel: 'CN' }, { code: 'en-US', label: 'English', shortLabel: 'EN' }, { code: 'ja-JP', label: '日本語', shortLabel: 'JA' }, + { code: 'de-DE', label: 'Deutsch', shortLabel: 'DE' }, ] as const satisfies readonly LocaleEntry[]; diff --git a/lib/i18n/locales/de-DE.json b/lib/i18n/locales/de-DE.json new file mode 100644 index 000000000..65e6410aa --- /dev/null +++ b/lib/i18n/locales/de-DE.json @@ -0,0 +1,882 @@ +{ + "common": { + "you": "Du", + "confirm": "Bestätigen", + "cancel": "Abbrechen", + "loading": "Laden...", + "images": "Bilder" + }, + "home": { + "slogan": "Generatives Lernen im interaktiven Klassenzimmer mit mehreren Agenten", + "greeting": "Hallo, ", + "greetingWithName": "Hallo, {{name}}" + }, + "toolbar": { + "languageHint": "Der Kurs wird in dieser Sprache erstellt", + "pdfParser": "Parser", + "pdfUpload": "PDF hochladen", + "uploadFiles": "Dateien hochladen", + "removePdf": "Datei entfernen", + "webSearchOn": "Aktiviert", + "webSearchOff": "Klicken zum Aktivieren", + "webSearchDesc": "Vor der Erstellung im Web nach aktuellen Informationen suchen", + "webSearchProvider": "Suchmaschine", + "webSearchNoProvider": "Suchmaschinen-API-Key in den Einstellungen konfigurieren", + "selectProvider": "Anbieter auswählen", + "configureProvider": "Modell einrichten", + "configureProviderHint": "Konfigurieren Sie mindestens einen Modellanbieter, um Kurse zu erstellen", + "enterClassroom": "Klassenzimmer betreten", + "advancedSettings": "Erweiterte Einstellungen", + "ttsTitle": "Text-zu-Sprache", + "ttsHint": "Wählen Sie eine Stimme für den KI-Lehrer", + "ttsPreview": "Vorschau", + "ttsPreviewing": "Wiedergabe..." + }, + "export": { + "pptx": "PPTX exportieren", + "resourcePack": "Ressourcenpaket exportieren", + "resourcePackDesc": "PPTX + interaktive Seiten", + "exporting": "Exportieren...", + "exportSuccess": "Export erfolgreich", + "exportFailed": "Export fehlgeschlagen" + }, + "chat": { + "lecture": "Vortrag", + "noConversations": "Keine Unterhaltungen", + "startConversation": "Geben Sie unten eine Nachricht ein, um den Chat zu beginnen", + "noMessages": "Noch keine Nachrichten", + "ended": "beendet", + "unknown": "Unbekannt", + "stopDiscussion": "Diskussion beenden", + "endQA": "Fragen & Antworten beenden", + "tabs": { + "lecture": "Notizen", + "chat": "Chat" + }, + "lectureNotes": { + "empty": "Notizen erscheinen hier nach der Wiedergabe des Vortrags", + "emptyHint": "Drücken Sie Play, um den Vortrag zu starten", + "pageLabel": "Seite {{n}}", + "currentPage": "Aktuell" + }, + "badge": { + "qa": "F&A", + "discussion": "DISK", + "lecture": "VOR" + } + }, + "actions": { + "names": { + "spotlight": "Spotlight", + "laser": "Laserpointer", + "wb_open": "Whiteboard öffnen", + "wb_draw_text": "Whiteboard-Text", + "wb_draw_shape": "Whiteboard-Form", + "wb_draw_chart": "Whiteboard-Diagramm", + "wb_draw_latex": "Whiteboard-Formel", + "wb_draw_table": "Whiteboard-Tabelle", + "wb_draw_line": "Whiteboard-Linie", + "wb_clear": "Whiteboard löschen", + "wb_delete": "Element löschen", + "wb_close": "Whiteboard schließen", + "discussion": "Diskussion" + }, + "status": { + "inputStreaming": "Warten", + "inputAvailable": "Ausführen", + "outputAvailable": "Abgeschlossen", + "outputError": "Fehler", + "outputDenied": "Abgelehnt", + "running": "Ausführen", + "result": "Abgeschlossen", + "error": "Fehler" + } + }, + "agentBar": { + "readyToLearn": "Bereit, gemeinsam zu lernen?", + "expandedTitle": "Konfiguration der Klassenzimmer-Rollen", + "configTooltip": "Klicken, um Klassenzimmer-Rollen zu konfigurieren", + "voiceLabel": "Stimme", + "voiceLoading": "Laden...", + "voiceAutoAssign": "Stimmen werden automatisch zugewiesen" + }, + "proactiveCard": { + "discussion": "Diskussion", + "join": "Beitreten", + "skip": "Überspringen", + "pause": "Pause", + "resume": "Fortsetzen" + }, + "voice": { + "startListening": "Spracheingabe", + "stopListening": "Aufnahme stoppen" + }, + "stage": { + "currentScene": "Aktuelle Szene", + "generating": "Generieren...", + "paused": "Pausiert", + "generationFailed": "Generierung fehlgeschlagen", + "confirmSwitchTitle": "Szene wechseln", + "confirmSwitchMessage": "Ein Thema ist gerade in Bearbeitung. Das Wechseln der Szene beendet das aktuelle Thema. Sind Sie sicher?", + "generatingNextPage": "Szene wird generiert, bitte warten...", + "fullscreen": "Vollbild", + "exitFullscreen": "Vollbild beenden" + }, + "whiteboard": { + "title": "Interaktives Whiteboard", + "open": "Whiteboard öffnen", + "clear": "Whiteboard löschen", + "minimize": "Whiteboard minimieren", + "ready": "Whiteboard ist bereit", + "readyHint": "Elemente erscheinen hier, wenn sie von der KI hinzugefügt werden", + "clearSuccess": "Whiteboard erfolgreich gelöscht", + "clearError": "Fehler beim Löschen des Whiteboards: ", + "resetView": "Ansicht zurücksetzen", + "restoreError": "Fehler beim Wiederherstellen des Whiteboards: ", + "history": "Verlauf", + "restore": "Wiederherstellen", + "noHistory": "Noch kein Verlauf", + "restored": "Whiteboard wiederhergestellt", + "elementCount": "{{count}} Elemente" + }, + "quiz": { + "title": "Quiz", + "subtitle": "Testen Sie Ihr Wissen", + "questionsCount": "Fragen", + "totalPrefix": "", + "pointsSuffix": "Pkt", + "startQuiz": "Quiz starten", + "multipleChoiceHint": "(Mehrfachauswahl — wählen Sie alle richtigen Antworten aus)", + "inputPlaceholder": "Geben Sie hier Ihre Antwort ein...", + "charCount": "Zeichen", + "yourAnswer": "Ihre Antwort:", + "notAnswered": "Nicht beantwortet", + "aiComment": "KI-Feedback", + "singleChoice": "Einzelwahl", + "multipleChoice": "Mehrfachwahl", + "shortAnswer": "Kurzantwort", + "analysis": "Analyse: ", + "excellent": "Ausgezeichnet!", + "keepGoing": "Weiter so!", + "needsReview": "Überprüfung erforderlich", + "correct": "richtig", + "incorrect": "falsch", + "answering": "In Bearbeitung", + "submitAnswers": "Antworten absenden", + "aiGrading": "KI korrigiert...", + "aiGradingWait": "Bitte warten, Ihre Antworten werden analysiert", + "quizReport": "Quiz-Bericht", + "retry": "Wiederholen" + }, + "roundtable": { + "teacher": "LEHRER", + "you": "DU", + "inputPlaceholder": "Geben Sie Ihre Nachricht ein...", + "listening": "Zuhören...", + "processing": "Verarbeitung...", + "noSpeechDetected": "Keine Sprache erkannt, bitte versuchen Sie es erneut", + "discussionEnded": "Diskussion beendet", + "qaEnded": "Fragen & Antworten beendet", + "thinking": "Nachdenken", + "yourTurn": "Sie sind dran", + "stopDiscussion": "Diskussion beenden", + "pauseDiscussion": "Pause", + "resumeDiscussion": "Fortsetzen", + "autoPlay": "Autoplay", + "autoPlayOff": "Autoplay stoppen", + "speed": "Geschwindigkeit", + "voiceInput": "Spracheingabe", + "voiceInputDisabled": "Spracheingabe deaktiviert", + "textInput": "Texteingabe", + "stopRecording": "Aufnahme stoppen", + "startRecording": "Aufnahme starten" + }, + "pbl": { + "legacyFormat": "Diese PBL-Szene verwendet ein altes Format. Bitte generieren Sie den Kurs neu.", + "emptyProject": "PBL-Projekt wurde noch nicht generiert. Bitte über die Kursgenerierung erstellen.", + "roleSelection": { + "title": "Wählen Sie Ihre Rolle", + "description": "Wählen Sie eine Rolle aus, um mit der Zusammenarbeit am Projekt zu beginnen" + }, + "workspace": { + "restart": "Neustart", + "confirmRestart": "Gesamten Fortschritt zurücksetzen?", + "confirm": "Bestätigen", + "cancel": "Abbrechen" + }, + "issueboard": { + "title": "Aufgabenboard", + "noIssues": "Noch keine Aufgaben", + "statusDone": "Erledigt", + "statusActive": "Aktiv", + "statusPending": "Ausstehend" + }, + "chat": { + "title": "Projektdiskussion", + "currentIssue": "Aktuelle Aufgabe", + "mentionHint": "Verwenden Sie @question zum Fragen, @judge zum Einreichen zur Überprüfung", + "placeholder": "Geben Sie eine Nachricht ein...", + "send": "Senden", + "welcomeMessage": "Hallo! Ich bin Ihr Fragen-Agent für diese Aufgabe: \"{{title}}\"\n\nUm Ihre Arbeit zu unterstützen, habe ich einige Fragen für Sie vorbereitet:\n\n{{questions}}\n\nFühlen Sie sich frei, mich jederzeit mit @question zu fragen, wenn Sie Hilfe oder Klärung benötigen!", + "issueCompleteMessage": "Aufgabe \"{{completed}}\" abgeschlossen! Weiter zur nächsten Aufgabe: \"{{next}}\"", + "allCompleteMessage": "🎉 Alle Aufgaben abgeschlossen! Tolle Arbeit am Projekt!" + }, + "guide": { + "howItWorks": "Wie es funktioniert", + "help": "Hilfe", + "title": "Hilfe", + "step1": { + "title": "Schritt 1: Wählen Sie eine Rolle", + "desc": "Nachdem das Projekt generiert wurde, wählen Sie eine Rolle aus der Liste aus (Nicht-Systemrollen sind mit \ud83d\udfe2 markiert)" + }, + "step2": { + "title": "Schritt 2: Aufgaben abschließen", + "desc": "Jede Aufgabe repräsentiert ein Lernziel:", + "s1": { + "title": "Aktuelle Aufgabe anzeigen", + "desc": "Überprüfen Sie den Titel, die Beschreibung und den Verantwortlichen der Aufgabe" + }, + "s2": { + "title": "Anleitung erhalten", + "example": "@question Wo soll ich anfangen?\n@question Wie implementiere ich diese Funktion?", + "desc": "Der Fragen-Agent stellt leitende Fragen und Hinweise zur Verfügung (keine direkten Antworten)" + }, + "s3": { + "title": "Reichen Sie Ihre Arbeit ein", + "example": "@judge Ich bin fertig, bitte überprüfen Sie meine Notizen", + "desc": "Der Judge-Agent bewertet Ihre Arbeit und gibt Feedback:", + "complete": "Wechselt automatisch zur nächsten Aufgabe", + "revision": "Verbessern basierend auf Feedback" + } + }, + "step3": { + "title": "Schritt 3: Schließen Sie das Projekt ab", + "desc": "Wenn alle Aufgaben erledigt sind, zeigt das System \u201e\ud83c\udf89 Projekt abgeschlossen!\u201c an" + } + } + }, + "share": { + "notReady": "Verfügbar nach Abschluss der Generierung" + }, + "classroom": { + "recentClassrooms": "Zuletzt", + "today": "Heute", + "yesterday": "Gestern", + "daysAgo": "vor {{n}} Tagen", + "slides": "Folien", + "nameCopied": "Name kopiert", + "deleteConfirmTitle": "Löschen", + "delete": "Löschen", + "rename": "Umbenennen", + "renamePlaceholder": "Klassenzimmernamen eingeben", + "renameFailed": "Umbenennen fehlgeschlagen" + }, + "upload": { + "pdfSizeLimit": "Unterstützt PDF-Dateien bis zu 50 MB", + "generateFailed": "Klassenzimmer konnte nicht erstellt werden, bitte versuchen Sie es erneut", + "requirementPlaceholder": "Sagen Sie mir, was Sie lernen möchten, z. B.\n\u201eBring mir Python von Grund auf in 30 Minuten bei\u201c\n\u201eErkläre mir die Fourier-Transformation am Whiteboard\u201c\n\u201eWie spielt man das Brettspiel Avalon\u201c", + "requirementRequired": "Bitte geben Sie die Kursanforderungen ein", + "fileTooLarge": "Datei zu groß. Bitte wählen Sie eine PDF-Datei kleiner als 50 MB" + }, + "generation": { + "analyzingPdf": "PDF-Dokument analysieren", + "analyzingPdfDesc": "Dokumentstruktur und -inhalt extrahieren...", + "pdfLoadFailed": "PDF-Datei konnte nicht geladen werden, bitte versuchen Sie es erneut", + "pdfParseFailed": "PDF-Parsing fehlgeschlagen", + "streamNotReadable": "Generationsstream konnte nicht gelesen werden", + "generatingOutlines": "Kursübersicht entwerfen", + "generatingOutlinesDesc": "Lernpfad strukturieren...", + "generatingSlideContent": "Seiteninhalt generieren", + "generatingSlideContentDesc": "Folien, Quiz und interaktive Inhalte erstellen...", + "generatingActions": "Unterrichtsaktionen generieren", + "generatingActionsDesc": "Erzählung, Spotlights und Interaktionen orchestrieren...", + "generationComplete": "Generierung abgeschlossen!", + "generationFailed": "Generierung fehlgeschlagen", + "generatingCourse": "Kurs wird generiert", + "openingClassroom": "Klassenzimmer wird geöffnet...", + "outlineReady": "Kursübersicht generiert", + "generatingFirstPage": "Erste Seite wird generiert...", + "firstPageReady": "Erste Seite bereit! Klassenzimmer wird geöffnet...", + "speechFailed": "Sprachgenerierung fehlgeschlagen", + "retryScene": "Wiederholen", + "retryingScene": "Wird neu generiert...", + "backToHome": "Zurück zur Startseite", + "sessionNotFound": "Sitzung nicht gefunden", + "sessionNotFoundDesc": "Bitte füllen Sie die Kursanforderungen aus, um den Generierungsprozess zu starten.", + "goBackAndRetry": "Zurückgehen und erneut versuchen", + "classroomReady": "Ihre personalisierte KI-Lernumgebung wurde erfolgreich erstellt.", + "aiWorking": "KI-Agenten arbeiten...", + "textTruncated": "Dokumenttext ist lang, die ersten {{n}} Zeichen werden für die Generierung verwendet", + "imageTruncated": "{{total}} Bilder gefunden, das Limit von {{max}} Bildern wurde überschritten. Zusätzliche Bilder verwenden nur Textbeschreibungen", + "agentGeneration": "Klassenzimmer-Rollen generieren", + "agentGenerationDesc": "Rollen basierend auf dem Kursinhalt generieren...", + "agentRevealTitle": "Ihre Klassenzimmer-Rollen", + "viewAgents": "Rollen anzeigen", + "continue": "Weiter", + "outlineRetrying": "Problem bei der Übersichtserstellung, Wiederholung...", + "outlineEmptyResponse": "Modell gab keine gültigen Übersichten zurück. Bitte überprüfen Sie die Modellkonfiguration und versuchen Sie es erneut", + "outlineGenerateFailed": "Übersichtserstellung fehlgeschlagen, bitte versuchen Sie es später erneut", + "webSearching": "Websuche", + "webSearchingDesc": "Websuche nach aktuellen Informationen", + "webSearchFailed": "Websuche fehlgeschlagen" + }, + "settings": { + "title": "Einstellungen", + "description": "Anwendungseinstellungen konfigurieren", + "language": "Sprache", + "languageDesc": "Sprache der Benutzeroberfläche auswählen", + "theme": "Thema", + "themeDesc": "Themenmodus auswählen (Hell/Dunkel/System)", + "themeOptions": { + "light": "Hell", + "dark": "Dunkel", + "system": "System" + }, + "apiKey": "API-Schlüssel", + "apiKeyDesc": "Konfigurieren Sie Ihren API-Schlüssel", + "apiBaseUrl": "API-Endpunkt-URL", + "apiBaseUrlDesc": "Konfigurieren Sie Ihre API-Endpunkt-URL", + "apiKeyRequired": "API-Schlüssel darf nicht leer sein", + "model": "Modellkonfiguration", + "modelDesc": "KI-Modelle konfigurieren", + "modelPlaceholder": "Modellnamen eingeben oder auswählen", + "ttsModel": "TTS-Modell", + "ttsModelDesc": "TTS-Modelle konfigurieren", + "ttsModelPlaceholder": "TTS-Modellnamen eingeben oder auswählen", + "ttsModelOptions": { + "openaiTts": "OpenAI TTS", + "azureTts": "Azure TTS" + }, + "availableModels": "Verfügbare Modelle", + "modelSelectedViaVoice": "Modell wird durch die Stimmenauswahl bestimmt", + "testConnection": "Verbindung testen", + "testConnectionDesc": "Testen, ob die aktuelle API-Konfiguration verfügbar ist", + "testing": "Testen...", + "agentSettings": "Agenten-Einstellungen", + "agentSettingsDesc": "Wählen Sie die Agenten aus, die an der Unterhaltung teilnehmen sollen. Wählen Sie 1 für den Einzelagenten-Modus, wählen Sie mehrere für den kollaborativen Multi-Agenten-Modus.", + "agentMode": "Agenten-Modus", + "agentModePreset": "Voreinstellung", + "agentModeAuto": "Automatisch generieren", + "agentModeAutoDesc": "Die KI wird automatisch passende Rollen generieren", + "autoAgentCount": "Anzahl der Agenten", + "autoAgentCountDesc": "Anzahl der automatisch zu generierenden Agenten (einschließlich Lehrer)", + "atLeastOneAgent": "Bitte wählen Sie mindestens einen Agenten aus", + "singleAgentMode": "Einzelagenten-Modus", + "directAnswer": "Direkte Antwort", + "multiAgentMode": "Multi-Agenten-Modus", + "agentsCollaborating": "Kollaborative Diskussion", + "agentsCollaboratingCount": "{{count}} Agenten für kollaborative Diskussion ausgewählt", + "maxTurns": "Maximale Diskussionsrunden", + "maxTurnsDesc": "Die maximale Anzahl von Diskussionsrunden zwischen Agenten (jeder Agent schließt Aktionen ab und eine Antwort zählt als eine Runde)", + "priority": "Priorität", + "actions": "Aktionen", + "actionCount": "{{count}} Aktionen", + "selectedAgent": "Ausgewählter Agent", + "selectedAgents": "Ausgewählte Agenten", + "required": "Erforderlich", + "agentNames": { + "default-1": "KI-Lehrer", + "default-2": "KI-Assistent", + "default-3": "Klassenclown", + "default-4": "Neugieriger Geist", + "default-5": "Notizenschreiber", + "default-6": "Tiefer Denker" + }, + "agentRoles": { + "teacher": "Lehrer", + "assistant": "Assistent", + "student": "Schüler" + }, + "agentDescriptions": { + "default-1": "Hauptlehrer mit klaren und strukturierten Erklärungen", + "default-2": "Unterstützt das Lernen und hilft, wichtige Punkte zu klären", + "default-3": "Bringt Humor und Energie in das Klassenzimmer", + "default-4": "Immer neugierig, liebt es zu fragen, warum und wie", + "default-5": "Aufmerksames Aufzeichnen und Organisieren von Unterrichtsnotizen", + "default-6": "Denkt tief nach und erforscht das Wesen der Themen" + }, + "close": "Schließen", + "save": "Speichern", + "providers": "LLM", + "addProviderDescription": "Fügen Sie benutzerdefinierte Modellanbieter hinzu, um verfügbare KI-Modelle zu erweitern", + "providerNames": { + "openai": "OpenAI", + "anthropic": "Claude", + "google": "Gemini", + "deepseek": "DeepSeek", + "qwen": "Qwen", + "kimi": "Kimi", + "minimax": "MiniMax", + "glm": "GLM", + "siliconflow": "SiliconFlow" + }, + "providerTypes": { + "openai": "OpenAI-Protokoll", + "anthropic": "Claude-Protokoll", + "google": "Gemini-Protokoll" + }, + "modelCount": "Modelle", + "modelSingular": "Modell", + "defaultModel": "Standardmodell", + "webSearch": "Websuche", + "mcp": "MCP", + "knowledgeBase": "Wissensdatenbank", + "documentParser": "Dokumentenparser", + "conversationSettings": "Unterhaltung", + "keyboardShortcuts": "Tastenkombinationen", + "generalSettings": "Allgemein", + "systemSettings": "System", + "addProvider": "Hinzufügen", + "importFromClipboard": "Aus Zwischenablage importieren", + "apiSecret": "API-Schlüssel", + "apiHost": "Basis-URL", + "requestUrl": "Anforderungs-URL", + "models": "Modelle", + "addModel": "Neu", + "reset": "Zurücksetzen", + "fetch": "Abrufen", + "connectionSuccess": "Verbindung erfolgreich", + "connectionFailed": "Verbindung fehlgeschlagen", + "capabilities": { + "vision": "Vision", + "tools": "Tools", + "streaming": "Streaming" + }, + "contextWindow": "Kontext", + "contextShort": "ctx", + "outputWindow": "Ausgabe", + "addProviderButton": "Hinzufügen", + "addProviderDialog": "Modellanbieter hinzufügen", + "providerName": "Name", + "providerNamePlaceholder": "z.B. Mein OpenAI-Proxy", + "providerNameRequired": "Bitte Anbieternamen eingeben", + "providerApiMode": "API-Modus", + "apiModeOpenAI": "OpenAI-Protokoll", + "apiModeAnthropic": "Claude-Protokoll", + "apiModeGoogle": "Gemini-Protokoll", + "defaultBaseUrl": "Standard-Basis-URL", + "providerIcon": "Anbieter-Icon-URL", + "requiresApiKey": "Benötigt API-Schlüssel", + "deleteProvider": "Anbieter löschen", + "deleteProviderConfirm": "Sind Sie sicher, dass Sie diesen Anbieter löschen möchten?", + "cannotDeleteBuiltIn": "Integrierter Anbieter kann nicht gelöscht werden", + "resetToDefault": "Auf Standard zurücksetzen", + "resetToDefaultDescription": "Modellliste auf Standardkonfiguration zurücksetzen (API-Schlüssel und Basis-URL bleiben erhalten)", + "resetConfirmDescription": "Dadurch werden alle benutzerdefinierten Modelle entfernt und die integrierte Standardmodellliste wiederhergestellt. API-Schlüssel und Basis-URL bleiben erhalten.", + "confirmReset": "Zurücksetzen bestätigen", + "resetSuccess": "Erfolgreich auf Standardkonfiguration zurückgesetzt", + "saveSuccess": "Einstellungen gespeichert", + "saveFailed": "Speichern fehlgeschlagen, bitte versuchen Sie es erneut", + "cannotDeleteBuiltInModel": "Integriertes Modell kann nicht gelöscht werden", + "cannotEditBuiltInModel": "Integriertes Modell kann nicht bearbeitet werden", + "modelIdRequired": "Bitte Modell-ID eingeben", + "noModelsAvailable": "Keine Modelle für den Test verfügbar", + "providerMetadata": "Anbieter-Metadaten", + "editModel": "Modell bearbeiten", + "editModelDescription": "Modellkonfiguration und -fähigkeiten bearbeiten", + "addNewModel": "Neues Modell", + "addNewModelDescription": "Neue Modellkonfiguration hinzufügen", + "modelId": "Modell-ID", + "modelIdPlaceholder": "z.B. gpt-4o", + "modelName": "Anzeigename", + "modelNamePlaceholder": "Optional", + "modelCapabilities": "Fähigkeiten", + "advancedSettings": "Erweiterte Einstellungen", + "contextWindowLabel": "Kontextfenster", + "contextWindowPlaceholder": "z.B. 128000", + "outputWindowLabel": "Max. Ausgabe-Tokens", + "outputWindowPlaceholder": "z.B. 4096", + "testModel": "Modell testen", + "deleteModel": "Löschen", + "cancelEdit": "Abbrechen", + "saveModel": "Speichern", + "modelsManagementDescription": "Verwalten Sie die Modelle für diesen Anbieter. Um das aktive Modell auszuwählen, gehen Sie zu \u201eAllgemein\u201c.", + "howToUse": "Bedienungsanleitung", + "step1ConfigureProvider": "Gehen Sie zu \u201eModellanbieter\u201c, wählen Sie einen Anbieter aus oder fügen Sie einen hinzu, und konfigurieren Sie die Verbindungseinstellungen (API-Schlüssel, Basis-URL usw.)", + "step2SelectModel": "Wählen Sie das gewünschte Modell unter \u201eAktives Modell\u201c unten aus", + "step3StartUsing": "Nach dem Speichern verwendet das System Ihr ausgewähltes Modell", + "activeModel": "Aktives Modell", + "activeModelDescription": "Wählen Sie das Modell für KI-Unterhaltungen und Inhaltserstellung aus", + "selectModel": "Modell auswählen", + "searchModels": "Modelle suchen", + "noModelsFound": "Keine passenden Modelle gefunden", + "noConfiguredProviders": "Keine konfigurierten Anbieter", + "configureProvidersFirst": "Bitte konfigurieren Sie die Anbieter-Verbindungseinstellungen unter \u201eModellanbieter\u201c links", + "currentlyUsing": "Aktuell verwendet", + "ttsSettings": "Text-zu-Sprache", + "asrSettings": "Spracherkennung", + "audioSettings": "Audio-Einstellungen", + "ttsSection": "Text-zu-Sprache (TTS)", + "asrSection": "Automatische Spracherkennung (ASR)", + "ttsDescription": "TTS (Text-to-Speech) - Text in Sprache umwandeln", + "asrDescription": "ASR (Automatic Speech Recognition) - Sprache in Text umwandeln", + "enableTTS": "Text-zu-Sprache aktivieren", + "ttsEnabledDescription": "Wenn aktiviert, wird bei der Kurserstellung Sprachaudio generiert", + "ttsVoiceConfigHint": "Die Stimme pro Agent kann in der „Klassenzimmer-Rollen-Konfiguration“ auf der Startseite konfiguriert werden", + "enableASR": "Spracherkennung aktivieren", + "asrEnabledDescription": "Wenn aktiviert, können Schüler das Mikrofon für die Spracheingabe verwenden", + "ttsProvider": "TTS-Anbieter", + "ttsLanguageFilter": "Sprachfilter", + "allLanguages": "Alle Sprachen", + "ttsVoice": "Stimme", + "ttsSpeed": "Geschwindigkeit", + "ttsBaseUrl": "Basis-URL", + "ttsApiKey": "API-Schlüssel", + "doubaoAppId": "App ID", + "doubaoAccessKey": "Access Key", + "asrProvider": "ASR-Anbieter", + "asrLanguage": "Erkennungssprache", + "asrBaseUrl": "Basis-URL", + "asrApiKey": "API-Schlüssel", + "enterApiKey": "API-Schlüssel eingeben", + "enterCustomBaseUrl": "Benutzerdefinierte Basis-URL eingeben", + "browserNativeNote": "Browser-native ASR erfordert keine Konfiguration und ist völlig kostenlos", + "providerOpenAITTS": "OpenAI TTS (gpt-4o-mini-tts)", + "providerAzureTTS": "Azure TTS", + "providerGLMTTS": "GLM TTS", + "providerQwenTTS": "Qwen TTS (Alibaba Cloud Bailian)", + "providerDoubaoTTS": "Doubao TTS 2.0 (Volcengine)", + "providerElevenLabsTTS": "ElevenLabs TTS", + "providerMiniMaxTTS": "MiniMax TTS", + "providerBrowserNativeTTS": "Browser-native TTS", + "providerOpenAIWhisper": "OpenAI ASR (gpt-4o-mini-transcribe)", + "providerBrowserNative": "Browser-native ASR", + "providerQwenASR": "Qwen ASR (Alibaba Cloud Bailian)", + "providerUnpdf": "unpdf (Integriert)", + "providerMinerU": "MinerU", + "browserNativeTTSNote": "Browser-native TTS erfordert keine Konfiguration und ist völlig kostenlos, wobei systemeigene Stimmen verwendet werden", + "testTTS": "TTS testen", + "testASR": "ASR testen", + "testSuccess": "Test erfolgreich", + "testFailed": "Test fehlgeschlagen", + "ttsTestText": "TTS-Testtext", + "ttsTestSuccess": "TTS-Test erfolgreich, Audio wurde abgespielt", + "ttsTestFailed": "TTS-Test fehlgeschlagen", + "asrTestSuccess": "Spracherkennung erfolgreich", + "asrTestFailed": "Spracherkennung fehlgeschlagen", + "asrResult": "Erkennungsergebnis", + "asrNotSupported": "Browser unterstützt die Spracherkennungs-API nicht", + "browserTTSNotSupported": "Browser unterstützt die Sprachsynthese-API nicht", + "browserTTSNoVoices": "Der aktuelle Browser hat keine verfügbaren TTS-Stimmen", + "microphoneAccessDenied": "Mikrofonzugriff verweigert", + "microphoneAccessFailed": "Mikrofonzugriff fehlgeschlagen", + "asrResultPlaceholder": "Erkennungsergebnis wird nach der Aufnahme angezeigt", + "useThisProvider": "Diesen Anbieter verwenden", + "fetchVoices": "Stimmenliste abrufen", + "fetchingVoices": "Abrufen...", + "voicesFetched": "Stimmen abgerufen", + "fetchVoicesFailed": "Stimmen konnten nicht abgerufen werden", + "voiceApiKeyRequired": "API-Schlüssel erforderlich", + "voiceBaseUrlRequired": "Basis-URL erforderlich", + "ttsTestTextPlaceholder": "Umzuwandelnden Text eingeben", + "ttsTestTextDefault": "Hallo, dies ist ein Test-Sprachausgabe.", + "startRecording": "Aufnahme starten", + "stopRecording": "Aufnahme stoppen", + "recording": "Aufnahme...", + "transcribing": "Transkribieren...", + "transcriptionResult": "Transkriptionsergebnis", + "noTranscriptionResult": "Kein Transkriptionsergebnis", + "baseUrlOptional": "Basis-URL (optional)", + "defaultValue": "Standard", + "voiceMarin": "Empfohlen - Beste Qualität", + "voiceCedar": "Empfohlen - Beste Qualität", + "voiceAlloy": "Neutral, ausgewogen", + "voiceAsh": "Stetig, professionell", + "voiceBallad": "Elegant, lyrisch", + "voiceCoral": "Warm, freundlich", + "voiceEcho": "Männlich, klar", + "voiceFable": "Erzählend, lebendig", + "voiceNova": "Weiblich, hell", + "voiceOnyx": "Männlich, tief", + "voiceSage": "Weise, gefasst", + "voiceShimmer": "Weiblich, sanft", + "voiceVerse": "Natürlich, geschmeidig", + "glmVoiceTongtong": "Standardstimme", + "glmVoiceChuichui": "Chuichui Stimme", + "glmVoiceXiaochen": "Xiaochen Stimme", + "glmVoiceJam": "Jam Stimme", + "glmVoiceKazi": "Kazi Stimme", + "glmVoiceDouji": "Douji Stimme", + "glmVoiceLuodo": "Luodo Stimme", + "qwenVoiceCherry": "Sonnig, warm und natürlich", + "qwenVoiceSerena": "Sanft und weich", + "qwenVoiceEthan": "Energetisch und lebendig", + "qwenVoiceChelsie": "Anime virtuelle Freundin", + "qwenVoiceMomo": "Verspielt und fröhlich", + "qwenVoiceVivian": "Niedlich und frech", + "qwenVoiceMoon": "Cool und attraktiv", + "qwenVoiceMaia": "Intellektuell und sanft", + "qwenVoiceKai": "Ein Wellness-Erlebnis für Ihre Ohren", + "qwenVoiceNofish": "Designer, der keine retroflexen Laute aussprechen kann", + "qwenVoiceBella": "Kleine Loli, die nicht betrunken wird", + "qwenVoiceJennifer": "Marken-Qualität, filmreife amerikanische Frauenstimme", + "qwenVoiceRyan": "Schnelllebig, dramatische Darbietung", + "qwenVoiceKaterina": "Reife Dame mit einprägsamem Rhythmus", + "qwenVoiceAiden": "Amerikanischer Junge, der das Kochen beherrscht", + "qwenVoiceEldricSage": "Stetiger und weiser Älterer", + "qwenVoiceMia": "Sanft wie Quellwasser, brav wie Schnee", + "qwenVoiceMochi": "Schlauer kleiner Erwachsener mit kindlicher Unschuld", + "qwenVoiceBellona": "Laute Stimme, klare Aussprache, lebendige Charaktere", + "qwenVoiceVincent": "Einzigartige heisere Stimme, die Geschichten von Krieg und Ehre erzählt", + "qwenVoiceBunny": "Super süße Loli", + "qwenVoiceNeil": "Professioneller Nachrichtensprecher", + "qwenVoiceElias": "Professioneller Instruktor", + "qwenVoiceArthur": "Einfache Stimme, geprägt von Jahren und trockenem Tabak", + "qwenVoiceNini": "Weiche und klebrige Stimme wie Klebreiskuchen", + "qwenVoiceEbona": "Ihr Flüstern ist wie ein rostiger Schlüssel", + "qwenVoiceSeren": "Sanfte und beruhigende Stimme, die beim Einschlafen hilft", + "qwenVoicePip": "Frech, aber voller kindlicher Unschuld", + "qwenVoiceStella": "Süße, verwirrte Mädchenstimme, die beim Schreien gerecht wird", + "qwenVoiceBodega": "Begeisterter spanischer Onkel", + "qwenVoiceSonrisa": "Begeisterte lateinamerikanische Dame", + "qwenVoiceAlek": "Kälte der Kriegernation, warm unter dem Wollmantel", + "qwenVoiceDolce": "Lazy italienischer Onkel", + "qwenVoiceSohee": "Sanfte, fröhliche koreanische Unnie", + "qwenVoiceOnoAnna": "Schelmische Jugendfreundin", + "qwenVoiceLenn": "Rationaler deutscher Jugendlicher, der Anzug trägt und Post-Punk hört", + "qwenVoiceEmilien": "Romantischer französischer großer Bruder", + "qwenVoiceAndre": "Magnetische, natürliche und ruhige Männerstimme", + "qwenVoiceRadioGol": "Fußballpoet Rádio Gol!", + "qwenVoiceJada": "Lebhafte Shanghai-Dame", + "qwenVoiceDylan": "Peking-Junge", + "qwenVoiceLi": "Geduldige Yogalehrerin", + "qwenVoiceMarcus": "Breites Gesicht, kurze Worte, festes Herz - alter Shaanxi-Geschmack", + "qwenVoiceRoy": "Humorvoller und direkter taiwanesischer Junge", + "qwenVoicePeter": "Tianjin Cross-Talk-Profi-Unterstützer", + "qwenVoiceSunny": "Süßes Sichuan-Mädchen", + "qwenVoiceEric": "Chengdu-Gentleman", + "qwenVoiceRocky": "Humorvoller Typ aus Hongkong", + "qwenVoiceKiki": "Süßes Mädchen aus Hongkong", + "lang_auto": "Automatisch erkennen", + "lang_zh": "Chinesisch", + "lang_yue": "Kantonesisch", + "lang_en": "Englisch", + "lang_ja": "Japanisch", + "lang_ko": "Koreanisch", + "lang_es": "Spanisch", + "lang_fr": "Französisch", + "lang_de": "Deutsch", + "lang_ru": "Russisch", + "lang_ar": "Arabisch", + "lang_pt": "Portugiesisch", + "lang_it": "Italienisch", + "lang_af": "Afrikaans", + "lang_hy": "Armenisch", + "lang_az": "Aserbaidschanisch", + "lang_be": "Belarussisch", + "lang_bs": "Bosnisch", + "lang_bg": "Bulgarisch", + "lang_ca": "Katalanisch", + "lang_hr": "Kroatisch", + "lang_cs": "Tschechisch", + "lang_da": "Dänisch", + "lang_nl": "Niederländisch", + "lang_et": "Estnisch", + "lang_fi": "Finnisch", + "lang_gl": "Galicisch", + "lang_el": "Griechisch", + "lang_he": "Hebräisch", + "lang_hi": "Hindi", + "lang_hu": "Ungarisch", + "lang_is": "Isländisch", + "lang_id": "Indonesisch", + "lang_kn": "Kannada", + "lang_kk": "Kasachisch", + "lang_lv": "Lettisch", + "lang_lt": "Litauisch", + "lang_mk": "Mazedonisch", + "lang_ms": "Malaiisch", + "lang_mr": "Marathi", + "lang_mi": "Maori", + "lang_ne": "Nepalesisch", + "lang_no": "Norwegisch", + "lang_fa": "Persisch", + "lang_pl": "Polnisch", + "lang_ro": "Rumänisch", + "lang_sr": "Serbisch", + "lang_sk": "Slowakisch", + "lang_sl": "Slowenisch", + "lang_sw": "Suaheli", + "lang_sv": "Schwedisch", + "lang_tl": "Tagalog", + "lang_fil": "Filipino", + "lang_ta": "Tamil", + "lang_th": "Thailändisch", + "lang_tr": "Türkisch", + "lang_uk": "Ukrainisch", + "lang_ur": "Urdu", + "lang_vi": "Vietnamesisch", + "lang_cy": "Walisisch", + "lang_zh-CN": "Chinesisch (Vereinfacht, China)", + "lang_zh-TW": "Chinesisch (Traditionell, Taiwan)", + "lang_zh-HK": "Kantonesisch (Hongkong)", + "lang_yue-Hant-HK": "Kantonesisch (Traditionell)", + "lang_en-US": "Englisch (USA)", + "lang_en-GB": "Englisch (Großbritannien)", + "lang_en-AU": "Englisch (Australien)", + "lang_en-CA": "Englisch (Kanada)", + "lang_en-IN": "Englisch (Indien)", + "lang_en-NZ": "Englisch (Neuseeland)", + "lang_en-ZA": "Englisch (Südafrika)", + "lang_de-DE": "Deutsch (Deutschland)", + "lang_fr-FR": "Französisch (Frankreich)", + "lang_es-ES": "Spanisch (Spanien)", + "lang_es-MX": "Spanisch (Mexiko)", + "lang_es-AR": "Spanisch (Argentinien)", + "lang_es-CO": "Spanisch (Kolumbien)", + "lang_it-IT": "Italienisch (Italien)", + "lang_pt-BR": "Portugiesisch (Brasilien)", + "lang_pt-PT": "Portugiesisch (Portugal)", + "lang_ru-RU": "Russisch (Russland)", + "lang_ja-JP": "Japanisch (Japan)", + "lang_ko-KR": "Koreanisch (Südkorea)", + "lang_nl-NL": "Niederländisch (Niederlande)", + "lang_pl-PL": "Polnisch (Polen)", + "lang_cs-CZ": "Tschechisch (Tschechien)", + "lang_da-DK": "Dänisch (Dänemark)", + "lang_fi-FI": "Finnisch (Finnland)", + "lang_sv-SE": "Schwedisch (Schweden)", + "lang_no-NO": "Norwegisch (Norwegen)", + "lang_tr-TR": "Türkisch (Türkei)", + "lang_el-GR": "Griechisch (Griechenland)", + "lang_hu-HU": "Ungarisch (Ungarn)", + "lang_ro-RO": "Rumänisch (Rumänien)", + "lang_sk-SK": "Slowakisch (Slowakei)", + "lang_bg-BG": "Bulgarisch (Bulgarien)", + "lang_hr-HR": "Kroatisch (Kroatien)", + "lang_ca-ES": "Katalanisch (Spanien)", + "lang_ar-SA": "Arabisch (Saudi-Arabien)", + "lang_ar-EG": "Arabisch (Ägypten)", + "lang_he-IL": "Hebräisch (Israel)", + "lang_hi-IN": "Hindi (Indien)", + "lang_th-TH": "Thailändisch (Thailand)", + "lang_vi-VN": "Vietnamesisch (Vietnam)", + "lang_id-ID": "Indonesisch (Indonesien)", + "lang_ms-MY": "Malaiisch (Malaysia)", + "lang_fil-PH": "Filipino (Philippinen)", + "lang_af-ZA": "Afrikaans (Südafrika)", + "lang_uk-UA": "Ukrainisch (Ukraine)", + "pdfSettings": "PDF-Parsing", + "pdfParsingSettings": "PDF-Parsing-Einstellungen", + "pdfDescription": "Wählen Sie eine PDF-Parsing-Engine mit Unterstützung für Textextraktion, Bildverarbeitung und Tabellenerkennung", + "pdfProvider": "PDF-Parser", + "pdfFeatures": "Unterstützte Funktionen", + "pdfApiKey": "API-Schlüssel", + "pdfBaseUrl": "Basis-URL", + "mineruDescription": "MinerU ist ein kommerzieller PDF-Parsing-Dienst, der erweiterte Funktionen wie Tabellenextraktion, Formelerkennung und Layoutanalyse unterstützt.", + "mineruApiKeyRequired": "Sie müssen vor der Verwendung einen API-Schlüssel auf der MinerU-Website beantragen.", + "mineruWarning": "Warnung", + "mineruCostWarning": "MinerU ist ein kommerzieller Dienst und es können Gebühren anfallen. Bitte informieren Sie sich auf der MinerU-Website über die Preise.", + "enterMinerUApiKey": "MinerU API-Schlüssel eingeben", + "mineruLocalDescription": "MinerU unterstützt die lokale Bereitstellung mit erweitertem PDF-Parsing. Erfordert die vorherige Bereitstellung des MinerU-Dienstes.", + "mineruServerAddress": "Lokale MinerU-Serveradresse (z.B. http://localhost:8080)", + "mineruApiKeyOptional": "Nur erforderlich, wenn auf dem Server die Authentifizierung aktiviert ist", + "optionalApiKey": "Optionaler API-Schlüssel", + "featureText": "Textextraktion", + "featureImages": "Bildextraktion", + "featureTables": "Tabellenextraktion", + "featureFormulas": "Formelerkennung", + "featureLayoutAnalysis": "Layoutanalyse", + "featureMetadata": "Metadaten", + "enableImageGeneration": "KI-Bildgenerierung aktivieren", + "imageGenerationDisabledHint": "Wenn aktiviert, werden während der Kurserstellung automatisch Bilder generiert", + "imageSettings": "Bildgenerierung", + "imageSection": "Text zu Bild", + "imageProvider": "Bildgenerierungsanbieter", + "imageModel": "Bildgenerierungsmodell", + "providerSeedream": "Seedream (ByteDance)", + "providerQwenImage": "Qwen Image (Alibaba)", + "providerNanoBanana": "Nano Banana (Gemini)", + "providerMiniMaxImage": "MiniMax Image", + "providerGrokImage": "Grok Image (xAI)", + "testImageGeneration": "Bildgenerierung testen", + "testImageConnectivity": "Verbindung testen", + "imageConnectivitySuccess": "Bilddienst erfolgreich verbunden", + "imageConnectivityFailed": "Verbindung zum Bilddienst fehlgeschlagen", + "imageTestSuccess": "Bildgenerierungstest erfolgreich", + "imageTestFailed": "Bildgenerierungstest fehlgeschlagen", + "imageTestPromptPlaceholder": "Bildbeschreibung zum Testen eingeben", + "imageTestPromptDefault": "Eine süße Katze, die auf einem Schreibtisch sitzt", + "imageGenerating": "Bild wird generiert...", + "imageGenerationFailed": "Bildgenerierung fehlgeschlagen", + "enableVideoGeneration": "KI-Videogenerierung aktivieren", + "videoGenerationDisabledHint": "Wenn aktiviert, werden während der Kurserstellung automatisch Videos generiert", + "videoSettings": "Videogenerierung", + "videoSection": "Text zu Video", + "videoProvider": "Videogenerierungsanbieter", + "videoModel": "Videogenerierungsmodell", + "providerSeedance": "Seedance (ByteDance)", + "providerKling": "Kling (Kuaishou)", + "providerVeo": "Veo (Google)", + "providerSora": "Sora (OpenAI)", + "providerMiniMaxVideo": "MiniMax Video", + "providerGrokVideo": "Grok Video (xAI)", + "testVideoGeneration": "Videogenerierung testen", + "testVideoConnectivity": "Verbindung testen", + "videoConnectivitySuccess": "Videodienst erfolgreich verbunden", + "videoConnectivityFailed": "Verbindung zum Videodienst fehlgeschlagen", + "testingConnection": "Testen...", + "videoTestSuccess": "Videogenerierungstest erfolgreich", + "videoTestFailed": "Videogenerierungstest fehlgeschlagen", + "videoTestPromptDefault": "Eine süße Katze, die auf einem Schreibtisch läuft", + "videoGenerating": "Video wird generiert (ca. 1-2 Min.)...", + "videoGenerationWarning": "Die Videogenerierung dauert normalerweise 1-2 Minuten, bitte haben Sie Geduld", + "mediaRetry": "Wiederholen", + "mediaContentSensitive": "Entschuldigung, dieser Inhalt hat eine Sicherheitsüberprüfung ausgelöst.", + "mediaGenerationDisabled": "Generierung in den Einstellungen deaktiviert", + "singleAgent": "Einzelagent", + "multiAgent": "Multi-Agent", + "selectAgents": "Agenten auswählen", + "noVisionWarning": "Das aktuelle Modell unterstützt keine Vision-Funktionen. Bilder können weiterhin in Folien platziert werden, aber das Modell kann den Bildinhalt nicht verstehen, um Auswahl und Layout zu optimieren.", + "serverConfigured": "Server", + "serverConfiguredNotice": "Der Administrator hat einen API-Schlüssel für diesen Anbieter auf dem Server konfiguriert. Sie können ihn direkt verwenden oder Ihren eigenen Schlüssel eingeben, um ihn zu überschreiben.", + "optionalOverride": "Optional — leer lassen, um die Serverkonfiguration zu verwenden", + "setupNeeded": "Einrichtung erforderlich", + "modelNotConfigured": "Bitte wählen Sie ein Modell aus, um zu beginnen", + "dangerZone": "Gefahrenzone", + "clearCache": "Lokalen Cache leeren", + "clearCacheDescription": "Löschen Sie alle lokal gespeicherten Daten, einschließlich Klassenzimmeraufzeichnungen, Chatverlauf, Audio-Cache und App-Einstellungen. Diese Aktion kann nicht rückgängig gemacht werden.", + "clearCacheConfirmTitle": "Sind Sie sicher, dass Sie den gesamten Cache leeren möchten?", + "clearCacheConfirmDescription": "Dadurch werden alle folgenden Daten dauerhaft gelöscht und können nicht wiederhergestellt werden:", + "clearCacheConfirmItems": "Klassenzimmer & Szenen, Chatverlauf, Audio- & Bild-Cache, App-Einstellungen & Präferenzen", + "clearCacheConfirmInput": "Geben Sie „DELETE“ ein, um fortzufahren", + "clearCacheConfirmPhrase": "DELETE", + "clearCacheButton": "Alle Daten dauerhaft löschen", + "clearCacheSuccess": "Cache geleert, die Seite wird in Kürze aktualisiert", + "clearCacheFailed": "Cache konnte nicht geleert werden, bitte versuchen Sie es erneut", + "webSearchSettings": "Websuche", + "webSearchApiKey": "Tavily API-Schlüssel", + "webSearchApiKeyPlaceholder": "Geben Sie Ihren Tavily API-Schlüssel ein", + "webSearchApiKeyPlaceholderServer": "Server-Schlüssel konfiguriert, optional überschreiben", + "webSearchApiKeyHint": "Holen Sie sich einen API-Schlüssel von tavily.com für die Websuche", + "webSearchBaseUrl": "Basis-URL", + "webSearchServerConfigured": "Serverseitiger Tavily API-Schlüssel ist konfiguriert", + "optional": "Optional" + }, + "profile": { + "title": "Profil", + "defaultNickname": "Schüler", + "chooseAvatar": "Avatar auswählen", + "uploadAvatar": "Hochladen", + "bioPlaceholder": "Erzählen Sie uns etwas über sich — der KI-Lehrer wird den Unterricht für Sie personalisieren...", + "avatarHint": "Ihr Avatar wird in Diskussionsrunden und Chats im Klassenzimmer angezeigt", + "fileTooLarge": "Bild zu groß — bitte wählen Sie eines unter 5 MB", + "invalidFileType": "Bitte wählen Sie eine Bilddatei aus", + "editTooltip": "Klicken, um das Profil zu bearbeiten" + }, + "media": { + "imageCapability": "Bildgenerierung", + "imageHint": "Bilder in Folien generieren", + "videoCapability": "Videogenerierung", + "videoHint": "Videos in Folien generieren", + "ttsCapability": "Text-zu-Sprache", + "ttsHint": "KI-Lehrer spricht laut", + "asrCapability": "Spracherkennung", + "asrHint": "Spracheingabe für Diskussionen", + "provider": "Anbieter", + "model": "Modell", + "voice": "Stimme", + "speed": "Geschwindigkeit", + "language": "Sprache" + } +} diff --git a/lib/server/classroom-generation.ts b/lib/server/classroom-generation.ts index 049cc9f30..5efd26cc4 100644 --- a/lib/server/classroom-generation.ts +++ b/lib/server/classroom-generation.ts @@ -98,8 +98,10 @@ function createInMemoryStore(stage: Stage): StageStore { }; } -function normalizeLanguage(language?: string): 'zh-CN' | 'en-US' { - return language === 'en-US' ? 'en-US' : 'zh-CN'; +function normalizeLanguage(language?: string): Locale { + const isSupported = (lang: string | undefined): lang is Locale => + !!lang && supportedLocales.some((l) => l.code === lang); + return isSupported(language) ? language : 'zh-CN'; } function stripCodeFences(text: string): string { diff --git a/lib/types/generation.ts b/lib/types/generation.ts index c1e6eb7a7..09d4b2af1 100644 --- a/lib/types/generation.ts +++ b/lib/types/generation.ts @@ -64,7 +64,7 @@ export interface UploadedDocument { */ export interface UserRequirements { requirement: string; // Single free-form text for all user input - language: 'zh-CN' | 'en-US'; // Course language - critical for generation + language: 'zh-CN' | 'en-US' | 'ja-JP' | 'de-DE'; // Course language - critical for generation userNickname?: string; // Student nickname for personalization userBio?: string; // Student background for personalization webSearch?: boolean; // Enable web search for richer context @@ -100,7 +100,7 @@ export interface SceneOutline { teachingObjective?: string; estimatedDuration?: number; // seconds order: number; - language?: 'zh-CN' | 'en-US'; // Generation language (inherited from requirements) + language?: 'zh-CN' | 'en-US' | 'ja-JP' | 'de-DE'; // Generation language (inherited from requirements) // Suggested image IDs (from PDF-extracted images) suggestedImageIds?: string[]; // e.g., ["img_1", "img_3"] // AI-generated media requests (when PDF images are insufficient) @@ -124,7 +124,7 @@ export interface SceneOutline { projectDescription: string; targetSkills: string[]; issueCount?: number; - language: 'zh-CN' | 'en-US'; + language: 'zh-CN' | 'en-US' | 'ja-JP' | 'de-DE'; }; } From f372c21b3e5b0939aabac6b2302ebb6240b36521 Mon Sep 17 00:00:00 2001 From: harshana Date: Tue, 7 Apr 2026 00:47:29 +0200 Subject: [PATCH 2/3] style: fix formatting with prettier --- components/generation/generation-toolbar.tsx | 3 ++- lib/generation/prompt-formatters.ts | 27 +++++++++++++------- lib/generation/scene-generator.ts | 20 ++++++++++----- 3 files changed, 33 insertions(+), 17 deletions(-) diff --git a/components/generation/generation-toolbar.tsx b/components/generation/generation-toolbar.tsx index feed8f682..19e6099f8 100644 --- a/components/generation/generation-toolbar.tsx +++ b/components/generation/generation-toolbar.tsx @@ -74,7 +74,8 @@ export function GenerationToolbar({ onLanguageChange(supportedLocales[nextIndex].code); }; - const currentLocaleInfo = supportedLocales.find((l) => l.code === language) || supportedLocales[0]; + const currentLocaleInfo = + supportedLocales.find((l) => l.code === language) || supportedLocales[0]; // Check if the selected web search provider has a valid config (API key or server-configured) const webSearchProvider = WEB_SEARCH_PROVIDERS[webSearchProviderId]; diff --git a/lib/generation/prompt-formatters.ts b/lib/generation/prompt-formatters.ts index bc31a0cd3..d32561539 100644 --- a/lib/generation/prompt-formatters.ts +++ b/lib/generation/prompt-formatters.ts @@ -80,14 +80,18 @@ export function formatImageDescription(img: PdfImage, language: string): string if (img.width && img.height) { const ratio = (img.width / img.height).toFixed(2); if (language === 'zh-CN') dimInfo = ` | 尺寸: ${img.width}×${img.height} (宽高比${ratio})`; - else if (language === 'ja-JP') dimInfo = ` | サイズ: ${img.width}×${img.height} (アスペクト比${ratio})`; - else if (language === 'de-DE') dimInfo = ` | Größe: ${img.width}×${img.height} (Seitenverhältnis${ratio})`; + else if (language === 'ja-JP') + dimInfo = ` | サイズ: ${img.width}×${img.height} (アスペクト比${ratio})`; + else if (language === 'de-DE') + dimInfo = ` | Größe: ${img.width}×${img.height} (Seitenverhältnis${ratio})`; else dimInfo = ` | size: ${img.width}×${img.height} (ratio ${ratio})`; } const desc = img.description ? ` | ${img.description}` : ''; if (language === 'zh-CN') return `- **${img.id}**: 来自PDF第${img.pageNumber}页${dimInfo}${desc}`; - if (language === 'ja-JP') return `- **${img.id}**: PDFの${img.pageNumber}ページ目から${dimInfo}${desc}`; - if (language === 'de-DE') return `- **${img.id}**: von PDF-Seite ${img.pageNumber}${dimInfo}${desc}`; + if (language === 'ja-JP') + return `- **${img.id}**: PDFの${img.pageNumber}ページ目から${dimInfo}${desc}`; + if (language === 'de-DE') + return `- **${img.id}**: von PDF-Seite ${img.pageNumber}${dimInfo}${desc}`; return `- **${img.id}**: from PDF page ${img.pageNumber}${dimInfo}${desc}`; } @@ -100,13 +104,18 @@ export function formatImagePlaceholder(img: PdfImage, language: string): string if (img.width && img.height) { const ratio = (img.width / img.height).toFixed(2); if (language === 'zh-CN') dimInfo = ` | 尺寸: ${img.width}×${img.height} (宽高比${ratio})`; - else if (language === 'ja-JP') dimInfo = ` | サイズ: ${img.width}×${img.height} (アスペクト比${ratio})`; - else if (language === 'de-DE') dimInfo = ` | Größe: ${img.width}×${img.height} (Seitenverhältnis${ratio})`; + else if (language === 'ja-JP') + dimInfo = ` | サイズ: ${img.width}×${img.height} (アスペクト比${ratio})`; + else if (language === 'de-DE') + dimInfo = ` | Größe: ${img.width}×${img.height} (Seitenverhältnis${ratio})`; else dimInfo = ` | size: ${img.width}×${img.height} (ratio ${ratio})`; } - if (language === 'zh-CN') return `- **${img.id}**: PDF第${img.pageNumber}页的图片${dimInfo} [参见附图]`; - if (language === 'ja-JP') return `- **${img.id}**: PDFの${img.pageNumber}ページ目の画像${dimInfo} [添付画像参照]`; - if (language === 'de-DE') return `- **${img.id}**: Bild von PDF-Seite ${img.pageNumber}${dimInfo} [siehe Anhang]`; + if (language === 'zh-CN') + return `- **${img.id}**: PDF第${img.pageNumber}页的图片${dimInfo} [参见附图]`; + if (language === 'ja-JP') + return `- **${img.id}**: PDFの${img.pageNumber}ページ目の画像${dimInfo} [添付画像参照]`; + if (language === 'de-DE') + return `- **${img.id}**: Bild von PDF-Seite ${img.pageNumber}${dimInfo} [siehe Anhang]`; return `- **${img.id}**: image from PDF page ${img.pageNumber}${dimInfo} [see attached]`; } diff --git a/lib/generation/scene-generator.ts b/lib/generation/scene-generator.ts index eb3ff8990..1646cf7a6 100644 --- a/lib/generation/scene-generator.ts +++ b/lib/generation/scene-generator.ts @@ -1038,16 +1038,19 @@ export async function generateSceneActions( function generateDefaultPBLActions(outline: SceneOutline): Action[] { const lang = outline.language || 'zh-CN'; let title = 'PBL Project Intro'; - let text = 'Let\'s start a Project-Based Learning activity. Choose your role, check the issue board, and collaborate to complete the project.'; + let text = + "Let's start a Project-Based Learning activity. Choose your role, check the issue board, and collaborate to complete the project."; if (lang === 'zh-CN') { title = 'PBL 项目介绍'; text = '现在让我们开始一个项目式学习活动。请选择你的角色,查看任务看板,开始协作完成项目。'; } else if (lang === 'ja-JP') { title = 'PBLプロジェクト紹介'; - text = 'プロジェクトベース学習活動を開始しましょう。役割を選択し、課題ボードを確認して、協力してプロジェクトを完了させてください。'; + text = + 'プロジェクトベース学習活動を開始しましょう。役割を選択し、課題ボードを確認して、協力してプロジェクトを完了させてください。'; } else if (lang === 'de-DE') { title = 'PBL Projekt-Einführung'; - text = 'Beginnen wir mit einer projektbasierten Lernaktivität. Wählen Sie Ihre Rolle, prüfen Sie das Aufgabenboard und arbeiten Sie zusammen, um das Projekt abzuschließen.'; + text = + 'Beginnen wir mit einer projektbasierten Lernaktivität. Wählen Sie Ihre Rolle, prüfen Sie das Aufgabenboard und arbeiten Sie zusammen, um das Projekt abzuschließen.'; } return [ @@ -1202,7 +1205,7 @@ function generateDefaultSlideActions(outline: SceneOutline, elements: PPTElement function generateDefaultQuizActions(outline: SceneOutline): Action[] { const lang = outline.language || 'zh-CN'; let title = 'Quiz Intro'; - let text = 'Now let\'s take a short quiz to test what we\'ve learned.'; + let text = "Now let's take a short quiz to test what we've learned."; if (lang === 'zh-CN') { title = '测验引导'; text = '现在让我们来做一个小测验,检验一下学习成果。'; @@ -1230,16 +1233,19 @@ function generateDefaultQuizActions(outline: SceneOutline): Action[] { function generateDefaultInteractiveActions(outline: SceneOutline): Action[] { const lang = outline.language || 'zh-CN'; let title = 'Interactive Intro'; - let text = 'Now let\'s explore this concept through interactive visualization. Try interacting with the elements on the page to see how they change.'; + let text = + "Now let's explore this concept through interactive visualization. Try interacting with the elements on the page to see how they change."; if (lang === 'zh-CN') { title = '交互引导'; text = '现在让我们通过交互式可视化来探索这个概念。请尝试操作页面中的元素,观察变化。'; } else if (lang === 'ja-JP') { title = 'インタラクティブ案内'; - text = 'インタラクティブな視覚化を通じて、この概念を探求しましょう。ページ上の要素を操作して、どのように変化するか確認してください。'; + text = + 'インタラクティブな視覚化を通じて、この概念を探求しましょう。ページ上の要素を操作して、どのように変化するか確認してください。'; } else if (lang === 'de-DE') { title = 'Interaktive Einführung'; - text = 'Erforschen wir nun dieses Konzept durch eine interaktive Visualisierung. Versuchen Sie, mit den Elementen auf der Seite zu interagieren, um zu sehen, wie sie sich verändern.'; + text = + 'Erforschen wir nun dieses Konzept durch eine interaktive Visualisierung. Versuchen Sie, mit den Elementen auf der Seite zu interagieren, um zu sehen, wie sie sich verändern.'; } return [ From bfc7360350eeba6e611ebb8c13c5b1e50c0446e9 Mon Sep 17 00:00:00 2001 From: harshana Date: Tue, 7 Apr 2026 00:52:54 +0200 Subject: [PATCH 3/3] fix: resolve type errors and lint warnings in CI --- components/generation/media-popover.tsx | 134 +----------------------- components/roundtable/index.tsx | 3 + lib/generation/scene-generator.ts | 3 +- lib/server/classroom-generation.ts | 2 + 4 files changed, 11 insertions(+), 131 deletions(-) diff --git a/components/generation/media-popover.tsx b/components/generation/media-popover.tsx index a09a32432..13b4c0467 100644 --- a/components/generation/media-popover.tsx +++ b/components/generation/media-popover.tsx @@ -9,8 +9,6 @@ import { Mic, SlidersHorizontal, ChevronRight, - Play, - Loader2, } from 'lucide-react'; import { toast } from 'sonner'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; @@ -24,7 +22,6 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; -import { Slider } from '@/components/ui/slider'; import { Switch } from '@/components/ui/switch'; import { cn } from '@/lib/utils'; import { useI18n } from '@/lib/hooks/use-i18n'; @@ -32,10 +29,10 @@ import { useSettingsStore } from '@/lib/store/settings'; import { useTTSPreview } from '@/lib/audio/use-tts-preview'; import { IMAGE_PROVIDERS } from '@/lib/media/image-providers'; import { VIDEO_PROVIDERS } from '@/lib/media/video-providers'; -import { TTS_PROVIDERS, getTTSVoices } from '@/lib/audio/constants'; +import { TTS_PROVIDERS } from '@/lib/audio/constants'; import { ASR_PROVIDERS, getASRSupportedLanguages } from '@/lib/audio/constants'; import type { ImageProviderId, VideoProviderId } from '@/lib/media/types'; -import type { TTSProviderId, ASRProviderId } from '@/lib/audio/types'; +import type { ASRProviderId } from '@/lib/audio/types'; import type { SettingsSection } from '@/lib/types/settings'; interface MediaPopoverProps { @@ -81,35 +78,11 @@ const TABS: Array<{ id: TabId; icon: LucideIcon; label: string }> = [ { id: 'asr', icon: Mic, label: 'ASR' }, ]; -/** Localized TTS provider name (mirrors audio-settings.tsx) */ -function getTTSProviderName(providerId: TTSProviderId, t: (key: string) => string): string { - const names: Record = { - 'openai-tts': t('settings.providerOpenAITTS'), - 'azure-tts': t('settings.providerAzureTTS'), - 'glm-tts': t('settings.providerGLMTTS'), - 'qwen-tts': t('settings.providerQwenTTS'), - 'doubao-tts': t('settings.providerDoubaoTTS'), - 'elevenlabs-tts': t('settings.providerElevenLabsTTS'), - 'minimax-tts': t('settings.providerMiniMaxTTS'), - 'browser-native-tts': t('settings.providerBrowserNativeTTS'), - }; - return names[providerId] || providerId; -} - -/** Extract the English name from voice name format "ChineseName (English)" */ -function getVoiceDisplayName(name: string, lang: string): string { - if (lang === 'en-US') { - const match = name.match(/\(([^)]+)\)/); - return match ? match[1] : name; - } - return name; -} - export function MediaPopover({ onSettingsOpen }: MediaPopoverProps) { - const { t, locale } = useI18n(); + const { t } = useI18n(); const [open, setOpen] = useState(false); const [activeTab, setActiveTab] = useState('image'); - const { previewing, startPreview, stopPreview } = useTTSPreview(); + const { stopPreview } = useTTSPreview(); // ─── Store ─── const imageGenerationEnabled = useSettingsStore((s) => s.imageGenerationEnabled); @@ -133,14 +106,6 @@ export function MediaPopover({ onSettingsOpen }: MediaPopoverProps) { const setVideoProvider = useSettingsStore((s) => s.setVideoProvider); const setVideoModelId = useSettingsStore((s) => s.setVideoModelId); - const ttsProviderId = useSettingsStore((s) => s.ttsProviderId); - const ttsVoice = useSettingsStore((s) => s.ttsVoice); - const ttsSpeed = useSettingsStore((s) => s.ttsSpeed); - const ttsProvidersConfig = useSettingsStore((s) => s.ttsProvidersConfig); - const setTTSProvider = useSettingsStore((s) => s.setTTSProvider); - const setTTSVoice = useSettingsStore((s) => s.setTTSVoice); - const setTTSSpeed = useSettingsStore((s) => s.setTTSSpeed); - const asrProviderId = useSettingsStore((s) => s.asrProviderId); const asrLanguage = useSettingsStore((s) => s.asrLanguage); const asrProvidersConfig = useSettingsStore((s) => s.asrProvidersConfig); @@ -167,18 +132,6 @@ export function MediaPopover({ onSettingsOpen }: MediaPopoverProps) { needsKey: boolean, ) => !needsKey || !!configs[id]?.apiKey || !!configs[id]?.isServerConfigured; - const ttsSpeedRange = TTS_PROVIDERS[ttsProviderId]?.speedRange; - - // ─── Dynamic browser voices ─── - const [browserVoices, setBrowserVoices] = useState([]); - useEffect(() => { - if (typeof window === 'undefined' || !window.speechSynthesis) return; - const load = () => setBrowserVoices(window.speechSynthesis.getVoices()); - load(); - window.speechSynthesis.addEventListener('voiceschanged', load); - return () => window.speechSynthesis.removeEventListener('voiceschanged', load); - }, []); - // ─── Grouped select data (only available providers) ─── const imageGroups = useMemo( () => @@ -214,85 +167,6 @@ export function MediaPopover({ onSettingsOpen }: MediaPopoverProps) { [videoProvidersConfig], ); - // TTS: grouped by provider, voices as items (matching Image/Video pattern) - // Browser-native voices are split into sub-groups by language. - const ttsGroups = useMemo(() => { - const groups: SelectGroupData[] = []; - - for (const p of Object.values(TTS_PROVIDERS)) { - if (p.requiresApiKey && !cfgOk(ttsProvidersConfig, p.id, p.requiresApiKey)) continue; - - const providerName = getTTSProviderName(p.id, t); - - // For browser-native-tts, split voices by language - if (p.id === 'browser-native-tts' && browserVoices.length > 0) { - const byLang = new Map(); - for (const v of browserVoices) { - const langKey = v.lang.split('-')[0]; // "zh-CN" → "zh" - if (!byLang.has(langKey)) byLang.set(langKey, []); - byLang.get(langKey)!.push(v); - } - for (const [langKey, voices] of byLang) { - const langLabel = LANG_LABELS[langKey] || langKey; - groups.push({ - groupId: p.id, - groupName: `${providerName} · ${langLabel}`, - groupIcon: p.icon, - available: true, - items: voices.map((v) => ({ id: v.voiceURI, name: v.name })), - }); - } - continue; - } - - groups.push({ - groupId: p.id, - groupName: providerName, - groupIcon: p.icon, - available: true, - items: getTTSVoices(p.id).map((v) => ({ - id: v.id, - name: getVoiceDisplayName(v.name, locale), - })), - }); - } - - return groups; - }, [ttsProvidersConfig, locale, browserVoices, t]); - - // TTS preview - const handlePreview = useCallback(async () => { - if (previewing) { - stopPreview(); - return; - } - try { - const providerConfig = ttsProvidersConfig[ttsProviderId]; - await startPreview({ - text: t('settings.ttsTestTextDefault'), - providerId: ttsProviderId, - modelId: providerConfig?.modelId, - voice: ttsVoice, - speed: ttsSpeed, - apiKey: providerConfig?.apiKey, - baseUrl: providerConfig?.baseUrl, - }); - } catch (error) { - const message = - error instanceof Error && error.message ? error.message : t('settings.ttsTestFailed'); - toast.error(message); - } - }, [ - previewing, - startPreview, - stopPreview, - t, - ttsProviderId, - ttsProvidersConfig, - ttsSpeed, - ttsVoice, - ]); - // ASR: only available providers const asrGroups = useMemo( () => diff --git a/components/roundtable/index.tsx b/components/roundtable/index.tsx index 78f3f17a5..b5e52cf45 100644 --- a/components/roundtable/index.tsx +++ b/components/roundtable/index.tsx @@ -462,6 +462,9 @@ export function Roundtable({ isVoiceOpen, isRecording, isProcessing, + cancelRecording, + handleToggleInput, + handleToggleVoice, ]); const isPresentationInteractionActive = isInputOpen || isVoiceOpen || isRecording || isProcessing; diff --git a/lib/generation/scene-generator.ts b/lib/generation/scene-generator.ts index 1646cf7a6..3e2b61cea 100644 --- a/lib/generation/scene-generator.ts +++ b/lib/generation/scene-generator.ts @@ -19,6 +19,7 @@ import type { ImageMapping, } from '@/lib/types/generation'; import type { LanguageModel } from 'ai'; +import type { Locale } from '@/lib/i18n/types'; import type { StageStore } from '@/lib/api/stage-api'; import { createStageAPI } from '@/lib/api/stage-api'; import { generatePBLContent } from '@/lib/pbl/generate-pbl'; @@ -735,7 +736,7 @@ function normalizeQuizAnswer(question: Record): string[] | unde async function generateInteractiveContent( outline: SceneOutline, aiCall: AICallFn, - language: 'zh-CN' | 'en-US' = 'zh-CN', + language: Locale = 'zh-CN', ): Promise { const config = outline.interactiveConfig!; diff --git a/lib/server/classroom-generation.ts b/lib/server/classroom-generation.ts index 5efd26cc4..42fce2130 100644 --- a/lib/server/classroom-generation.ts +++ b/lib/server/classroom-generation.ts @@ -28,6 +28,8 @@ import { generateTTSForClassroom, } from '@/lib/server/classroom-media-generation'; import type { UserRequirements } from '@/lib/types/generation'; +import type { Locale } from '@/lib/i18n/types'; +import { supportedLocales } from '@/lib/i18n/locales'; import type { Scene, Stage } from '@/lib/types/stage'; import { AGENT_COLOR_PALETTE, AGENT_DEFAULT_AVATARS } from '@/lib/constants/agent-defaults';