diff --git a/qwen3-tts-frontend/src/components/AudioPlayer.tsx b/qwen3-tts-frontend/src/components/AudioPlayer.tsx index 55b3fab..facd353 100644 --- a/qwen3-tts-frontend/src/components/AudioPlayer.tsx +++ b/qwen3-tts-frontend/src/components/AudioPlayer.tsx @@ -1,5 +1,6 @@ import { useRef, useState, useEffect, useCallback, memo } from 'react' import { useTranslation } from 'react-i18next' +import { useTheme } from '@/contexts/ThemeContext' import WaveformPlayer from '@arraypress/waveform-player' import '@arraypress/waveform-player/dist/waveform-player.css' import { Button } from '@/components/ui/button' @@ -15,6 +16,7 @@ interface AudioPlayerProps { const AudioPlayer = memo(({ audioUrl, jobId }: AudioPlayerProps) => { const { t } = useTranslation('common') + const { theme } = useTheme() const [blobUrl, setBlobUrl] = useState('') const [isLoading, setIsLoading] = useState(false) const [loadError, setLoadError] = useState(null) @@ -71,6 +73,9 @@ const AudioPlayer = memo(({ audioUrl, jobId }: AudioPlayerProps) => { useEffect(() => { if (!containerRef.current || !blobUrl) return + const waveformColor = theme === 'dark' ? '#4b5563' : '#d1d5db' + const progressColor = theme === 'dark' ? '#a78bfa' : '#7c3aed' + const player = new WaveformPlayer(containerRef.current, { url: blobUrl, waveformStyle: 'mirror', @@ -78,6 +83,8 @@ const AudioPlayer = memo(({ audioUrl, jobId }: AudioPlayerProps) => { barWidth: 3, barSpacing: 1, samples: 200, + waveformColor, + progressColor, showTime: true, showPlaybackSpeed: false, autoplay: false, @@ -103,7 +110,7 @@ const AudioPlayer = memo(({ audioUrl, jobId }: AudioPlayerProps) => { playerInstanceRef.current = null } } - }, [blobUrl]) + }, [blobUrl, theme]) const handleDownload = useCallback(() => { const link = document.createElement('a') diff --git a/qwen3-tts-frontend/src/locales/en-US/audiobook.json b/qwen3-tts-frontend/src/locales/en-US/audiobook.json new file mode 100644 index 0000000..59fb5d4 --- /dev/null +++ b/qwen3-tts-frontend/src/locales/en-US/audiobook.json @@ -0,0 +1,115 @@ +{ + "title": "Audiobook Generation", + "llmConfig": "LLM Config", + "newProject": "New Project", + "loading": "Loading...", + "noProjects": "No audiobook projects yet", + "noProjectsHint": "Click \"New Project\" to get started", + + "status": { + "pending": "Pending", + "analyzing": "Analyzing", + "characters_ready": "Awaiting Character Review", + "parsing": "Parsing Chapters", + "ready": "Ready", + "generating": "Generating", + "done": "Done", + "error": "Error" + }, + + "stepHints": { + "pending": "Step 1: Click \"Analyze\" — the LLM will automatically extract the character list", + "analyzing": "Step 1: LLM is extracting characters, please wait...", + "characters_ready": "Step 2: Review character info, then click \"Confirm Characters · Identify Chapters\"", + "ready": "Step 3: Parse chapters one by one (LLM); parsed chapters can generate audio immediately", + "generating": "Step 4: Synthesizing audio — completed segments can be played immediately" + }, + + "llmConfigPanel": { + "title": "LLM Configuration", + "current": "Current: {{baseUrl}} / {{model}} / {{keyStatus}}", + "hasKey": "API key set", + "noKey": "No API key", + "notSet": "Not set", + "saving": "Saving...", + "save": "Save Config", + "savedSuccess": "LLM config saved", + "incompleteError": "Please fill in all LLM configuration fields" + }, + + "createPanel": { + "title": "New Audiobook Project", + "titlePlaceholder": "Title", + "pasteText": "Paste Text", + "uploadEpub": "Upload EPUB", + "textPlaceholder": "Paste novel text here...", + "creating": "Creating...", + "create": "Create Project", + "createdSuccess": "Project created", + "titleRequired": "Please enter a title", + "textRequired": "Please enter text content", + "epubRequired": "Please select an EPUB file" + }, + + "projectCard": { + "analyze": "Analyze", + "reanalyze": "Re-analyze", + "reanalyzeConfirm": "Re-analyzing will clear all character and chapter data. Continue?", + "analyzeStarted": "Analysis started", + "generateAll": "Generate Full Book", + "downloadAll": "Download Full Book", + "deleteConfirm": "Delete project \"{{title}}\" and all its audio?", + "deleteSuccess": "Project deleted", + "allDoneToast": "\"{{title}}\" — all audio generation complete!", + "segmentsProgress": "{{done}} / {{total}} segments done", + + "characters": { + "title": "Characters ({{count}})", + "namePlaceholder": "Character name", + "genderPlaceholder": "Gender (not set)", + "genderMale": "Male", + "genderFemale": "Female", + "genderUnknown": "Unknown", + "instructPlaceholder": "Voice description (for TTS)", + "descPlaceholder": "Character description", + "voiceDesign": "Voice #{{id}}", + "noVoice": "Unassigned", + "savedSuccess": "Character saved" + }, + + "confirm": { + "button": "Confirm Characters · Identify Chapters", + "loading": "Identifying...", + "chaptersRecognized": "Chapters identified" + }, + + "chapters": { + "title": "Chapters ({{count}} total)", + "processAll": "Process All", + "defaultTitle": "Chapter {{index}}", + "parse": "Parse Chapter", + "parsing": "Parsing", + "parseStarted": "Parsing \"{{title}}\" started", + "parseStartedDefault": "Chapter parsing started", + "reparse": "Re-parse", + "generate": "Generate Chapter", + "generateStarted": "Chapter {{index}} generation started", + "generateAllStarted": "Full book generation started", + "processAllStarted": "All tasks triggered", + "doneBadge": "{{count}} segments done", + "segmentProgress": "{{done}}/{{total}} segments" + }, + + "segments": { + "errorBadge": "Error", + "unknownCharacter": "?" + }, + + "sequential": { + "play": "Play In Order ({{count}} segments)", + "stop": "Stop", + "progress": "{{current}} / {{total}}", + "loading": "Loading..." + } + } +} diff --git a/qwen3-tts-frontend/src/locales/en-US/index.ts b/qwen3-tts-frontend/src/locales/en-US/index.ts index e35b997..b5fc98d 100644 --- a/qwen3-tts-frontend/src/locales/en-US/index.ts +++ b/qwen3-tts-frontend/src/locales/en-US/index.ts @@ -9,6 +9,7 @@ import user from './user.json' import errors from './errors.json' import constants from './constants.json' import onboarding from './onboarding.json' +import audiobook from './audiobook.json' export default { common, @@ -22,4 +23,5 @@ export default { errors, constants, onboarding, + audiobook, } diff --git a/qwen3-tts-frontend/src/locales/ja-JP/audiobook.json b/qwen3-tts-frontend/src/locales/ja-JP/audiobook.json new file mode 100644 index 0000000..87799de --- /dev/null +++ b/qwen3-tts-frontend/src/locales/ja-JP/audiobook.json @@ -0,0 +1,115 @@ +{ + "title": "オーディオブック生成", + "llmConfig": "LLM 設定", + "newProject": "新規プロジェクト", + "loading": "読み込み中...", + "noProjects": "オーディオブックプロジェクトがありません", + "noProjectsHint": "「新規プロジェクト」をクリックして作成を開始", + + "status": { + "pending": "未分析", + "analyzing": "分析中", + "characters_ready": "キャラクター確認待ち", + "parsing": "章を解析中", + "ready": "生成待ち", + "generating": "生成中", + "done": "完了", + "error": "エラー" + }, + + "stepHints": { + "pending": "ステップ 1:「分析」をクリック — LLM がキャラクターリストを自動抽出します", + "analyzing": "ステップ 1:LLM がキャラクターを抽出中です。少々お待ちください...", + "characters_ready": "ステップ 2:キャラクター情報を確認し、「キャラクター確認 · 章を識別」をクリック", + "ready": "ステップ 3:章ごとに解析(LLM)— 解析済みの章はすぐに音声生成できます", + "generating": "ステップ 4:音声合成中 — 完成したセグメントはすぐに再生できます" + }, + + "llmConfigPanel": { + "title": "LLM 設定", + "current": "現在:{{baseUrl}} / {{model}} / {{keyStatus}}", + "hasKey": "APIキー設定済み", + "noKey": "APIキー未設定", + "notSet": "未設定", + "saving": "保存中...", + "save": "設定を保存", + "savedSuccess": "LLM 設定を保存しました", + "incompleteError": "LLM 設定をすべて入力してください" + }, + + "createPanel": { + "title": "新規オーディオブックプロジェクト", + "titlePlaceholder": "タイトル", + "pasteText": "テキストを貼り付け", + "uploadEpub": "EPUB アップロード", + "textPlaceholder": "小説のテキストを貼り付け...", + "creating": "作成中...", + "create": "プロジェクト作成", + "createdSuccess": "プロジェクトを作成しました", + "titleRequired": "タイトルを入力してください", + "textRequired": "テキスト内容を入力してください", + "epubRequired": "EPUB ファイルを選択してください" + }, + + "projectCard": { + "analyze": "分析", + "reanalyze": "再分析", + "reanalyzeConfirm": "再分析するとすべてのキャラクターと章のデータが削除されます。続けますか?", + "analyzeStarted": "分析を開始しました", + "generateAll": "全冊生成", + "downloadAll": "全冊ダウンロード", + "deleteConfirm": "プロジェクト「{{title}}」とすべての音声を削除しますか?", + "deleteSuccess": "プロジェクトを削除しました", + "allDoneToast": "「{{title}}」の音声生成がすべて完了しました!", + "segmentsProgress": "{{done}} / {{total}} セグメント完了", + + "characters": { + "title": "キャラクター({{count}} 人)", + "namePlaceholder": "キャラクター名", + "genderPlaceholder": "性別(未設定)", + "genderMale": "男性", + "genderFemale": "女性", + "genderUnknown": "不明", + "instructPlaceholder": "音声説明(TTS 用)", + "descPlaceholder": "キャラクター説明", + "voiceDesign": "音声 #{{id}}", + "noVoice": "未割り当て", + "savedSuccess": "キャラクターを保存しました" + }, + + "confirm": { + "button": "キャラクター確認 · 章を識別", + "loading": "識別中...", + "chaptersRecognized": "章を識別しました" + }, + + "chapters": { + "title": "章一覧(全 {{count}} 章)", + "processAll": "すべて処理", + "defaultTitle": "第 {{index}} 章", + "parse": "この章を解析", + "parsing": "解析中", + "parseStarted": "「{{title}}」の解析を開始しました", + "parseStartedDefault": "章の解析を開始しました", + "reparse": "再解析", + "generate": "この章を生成", + "generateStarted": "第 {{index}} 章の生成を開始しました", + "generateAllStarted": "全冊生成を開始しました", + "processAllStarted": "すべてのタスクを開始しました", + "doneBadge": "{{count}} セグメント完了", + "segmentProgress": "{{done}}/{{total}} セグメント" + }, + + "segments": { + "errorBadge": "エラー", + "unknownCharacter": "?" + }, + + "sequential": { + "play": "順番に再生({{count}} セグメント)", + "stop": "停止", + "progress": "{{current}} / {{total}}", + "loading": "読み込み中..." + } + } +} diff --git a/qwen3-tts-frontend/src/locales/ja-JP/index.ts b/qwen3-tts-frontend/src/locales/ja-JP/index.ts index e35b997..b5fc98d 100644 --- a/qwen3-tts-frontend/src/locales/ja-JP/index.ts +++ b/qwen3-tts-frontend/src/locales/ja-JP/index.ts @@ -9,6 +9,7 @@ import user from './user.json' import errors from './errors.json' import constants from './constants.json' import onboarding from './onboarding.json' +import audiobook from './audiobook.json' export default { common, @@ -22,4 +23,5 @@ export default { errors, constants, onboarding, + audiobook, } diff --git a/qwen3-tts-frontend/src/locales/ko-KR/audiobook.json b/qwen3-tts-frontend/src/locales/ko-KR/audiobook.json new file mode 100644 index 0000000..4609154 --- /dev/null +++ b/qwen3-tts-frontend/src/locales/ko-KR/audiobook.json @@ -0,0 +1,115 @@ +{ + "title": "오디오북 생성", + "llmConfig": "LLM 설정", + "newProject": "새 프로젝트", + "loading": "로딩 중...", + "noProjects": "오디오북 프로젝트가 없습니다", + "noProjectsHint": "「새 프로젝트」를 클릭하여 시작하세요", + + "status": { + "pending": "분석 대기", + "analyzing": "분석 중", + "characters_ready": "캐릭터 확인 대기", + "parsing": "챕터 파싱 중", + "ready": "생성 대기", + "generating": "생성 중", + "done": "완료", + "error": "오류" + }, + + "stepHints": { + "pending": "1단계: 「분석」을 클릭하면 LLM이 캐릭터 목록을 자동으로 추출합니다", + "analyzing": "1단계: LLM이 캐릭터를 추출 중입니다. 잠시 기다려 주세요...", + "characters_ready": "2단계: 캐릭터 정보를 확인한 후 「캐릭터 확인 · 챕터 식별」을 클릭하세요", + "ready": "3단계: 챕터별로 대본을 파싱합니다(LLM). 파싱된 챕터는 즉시 음성 생성이 가능합니다", + "generating": "4단계: 음성 합성 중 — 완료된 세그먼트는 즉시 재생할 수 있습니다" + }, + + "llmConfigPanel": { + "title": "LLM 설정", + "current": "현재: {{baseUrl}} / {{model}} / {{keyStatus}}", + "hasKey": "API 키 설정됨", + "noKey": "API 키 없음", + "notSet": "미설정", + "saving": "저장 중...", + "save": "설정 저장", + "savedSuccess": "LLM 설정이 저장되었습니다", + "incompleteError": "LLM 설정을 모두 입력해 주세요" + }, + + "createPanel": { + "title": "새 오디오북 프로젝트", + "titlePlaceholder": "제목", + "pasteText": "텍스트 붙여넣기", + "uploadEpub": "EPUB 업로드", + "textPlaceholder": "소설 텍스트를 붙여넣으세요...", + "creating": "생성 중...", + "create": "프로젝트 생성", + "createdSuccess": "프로젝트가 생성되었습니다", + "titleRequired": "제목을 입력해 주세요", + "textRequired": "텍스트 내용을 입력해 주세요", + "epubRequired": "EPUB 파일을 선택해 주세요" + }, + + "projectCard": { + "analyze": "분석", + "reanalyze": "재분석", + "reanalyzeConfirm": "재분석하면 모든 캐릭터와 챕터 데이터가 삭제됩니다. 계속하시겠습니까?", + "analyzeStarted": "분석이 시작되었습니다", + "generateAll": "전체 책 생성", + "downloadAll": "전체 책 다운로드", + "deleteConfirm": "프로젝트 「{{title}}」와 모든 음성을 삭제하시겠습니까?", + "deleteSuccess": "프로젝트가 삭제되었습니다", + "allDoneToast": "「{{title}}」 음성 생성이 모두 완료되었습니다!", + "segmentsProgress": "{{done}} / {{total}} 세그먼트 완료", + + "characters": { + "title": "캐릭터 목록 ({{count}}명)", + "namePlaceholder": "캐릭터 이름", + "genderPlaceholder": "성별 (미설정)", + "genderMale": "남성", + "genderFemale": "여성", + "genderUnknown": "알 수 없음", + "instructPlaceholder": "음성 설명 (TTS용)", + "descPlaceholder": "캐릭터 설명", + "voiceDesign": "음성 #{{id}}", + "noVoice": "미할당", + "savedSuccess": "캐릭터가 저장되었습니다" + }, + + "confirm": { + "button": "캐릭터 확인 · 챕터 식별", + "loading": "식별 중...", + "chaptersRecognized": "챕터가 식별되었습니다" + }, + + "chapters": { + "title": "챕터 목록 (총 {{count}}챕터)", + "processAll": "전체 처리", + "defaultTitle": "제 {{index}} 장", + "parse": "이 챕터 파싱", + "parsing": "파싱 중", + "parseStarted": "「{{title}}」 파싱이 시작되었습니다", + "parseStartedDefault": "챕터 파싱이 시작되었습니다", + "reparse": "재파싱", + "generate": "이 챕터 생성", + "generateStarted": "제 {{index}} 장 생성이 시작되었습니다", + "generateAllStarted": "전체 책 생성이 시작되었습니다", + "processAllStarted": "모든 작업이 시작되었습니다", + "doneBadge": "{{count}}개 세그먼트 완료", + "segmentProgress": "{{done}}/{{total}} 세그먼트" + }, + + "segments": { + "errorBadge": "오류", + "unknownCharacter": "?" + }, + + "sequential": { + "play": "순차 재생 ({{count}}개 세그먼트)", + "stop": "정지", + "progress": "{{current}} / {{total}}", + "loading": "로딩 중..." + } + } +} diff --git a/qwen3-tts-frontend/src/locales/ko-KR/index.ts b/qwen3-tts-frontend/src/locales/ko-KR/index.ts index e35b997..b5fc98d 100644 --- a/qwen3-tts-frontend/src/locales/ko-KR/index.ts +++ b/qwen3-tts-frontend/src/locales/ko-KR/index.ts @@ -9,6 +9,7 @@ import user from './user.json' import errors from './errors.json' import constants from './constants.json' import onboarding from './onboarding.json' +import audiobook from './audiobook.json' export default { common, @@ -22,4 +23,5 @@ export default { errors, constants, onboarding, + audiobook, } diff --git a/qwen3-tts-frontend/src/locales/zh-CN/audiobook.json b/qwen3-tts-frontend/src/locales/zh-CN/audiobook.json new file mode 100644 index 0000000..cc0b990 --- /dev/null +++ b/qwen3-tts-frontend/src/locales/zh-CN/audiobook.json @@ -0,0 +1,115 @@ +{ + "title": "有声书生成", + "llmConfig": "LLM 配置", + "newProject": "新建项目", + "loading": "加载中...", + "noProjects": "暂无有声书项目", + "noProjectsHint": "点击「新建项目」开始创建", + + "status": { + "pending": "待分析", + "analyzing": "分析中", + "characters_ready": "角色待确认", + "parsing": "解析章节", + "ready": "待生成", + "generating": "生成中", + "done": "已完成", + "error": "出错" + }, + + "stepHints": { + "pending": "第 1 步:点击「分析」,LLM 将自动提取角色列表", + "analyzing": "第 1 步:LLM 正在提取角色,请稍候...", + "characters_ready": "第 2 步:确认角色信息,可编辑后点击「确认角色 · 识别章节」", + "ready": "第 3 步:逐章解析剧本(LLM),解析完的章节可立即生成音频", + "generating": "第 4 步:正在合成音频,已完成片段可立即播放" + }, + + "llmConfigPanel": { + "title": "LLM 配置", + "current": "当前:{{baseUrl}} / {{model}} / {{keyStatus}}", + "hasKey": "已配置密钥", + "noKey": "未配置密钥", + "notSet": "未设置", + "saving": "保存中...", + "save": "保存配置", + "savedSuccess": "LLM 配置已保存", + "incompleteError": "请填写完整的 LLM 配置" + }, + + "createPanel": { + "title": "新建有声书项目", + "titlePlaceholder": "书名", + "pasteText": "粘贴文本", + "uploadEpub": "上传 epub", + "textPlaceholder": "粘贴小说文本...", + "creating": "创建中...", + "create": "创建项目", + "createdSuccess": "项目已创建", + "titleRequired": "请输入书名", + "textRequired": "请输入文本内容", + "epubRequired": "请选择 epub 文件" + }, + + "projectCard": { + "analyze": "分析", + "reanalyze": "重新分析", + "reanalyzeConfirm": "重新分析将清除所有角色和章节数据,确定继续?", + "analyzeStarted": "分析已开始", + "generateAll": "生成全书", + "downloadAll": "下载全书", + "deleteConfirm": "确认删除项目「{{title}}」及所有音频?", + "deleteSuccess": "项目已删除", + "allDoneToast": "「{{title}}」音频全部生成完成!", + "segmentsProgress": "{{done}} / {{total}} 片段完成", + + "characters": { + "title": "角色列表({{count}} 个)", + "namePlaceholder": "角色名", + "genderPlaceholder": "性别(未设置)", + "genderMale": "男", + "genderFemale": "女", + "genderUnknown": "未知", + "instructPlaceholder": "音色描述(用于 TTS)", + "descPlaceholder": "角色描述", + "voiceDesign": "音色 #{{id}}", + "noVoice": "未分配", + "savedSuccess": "角色已保存" + }, + + "confirm": { + "button": "确认角色 · 识别章节", + "loading": "识别中...", + "chaptersRecognized": "章节已识别" + }, + + "chapters": { + "title": "章节列表(共 {{count}} 章)", + "processAll": "一键全部处理", + "defaultTitle": "第 {{index}} 章", + "parse": "解析此章", + "parsing": "解析中", + "parseStarted": "「{{title}}」解析已开始", + "parseStartedDefault": "章节解析已开始", + "reparse": "重新解析", + "generate": "生成此章", + "generateStarted": "第 {{index}} 章生成已开始", + "generateAllStarted": "全书生成已开始", + "processAllStarted": "全部任务已触发", + "doneBadge": "已完成 {{count}} 段", + "segmentProgress": "{{done}}/{{total}} 段" + }, + + "segments": { + "errorBadge": "出错", + "unknownCharacter": "?" + }, + + "sequential": { + "play": "顺序播放({{count}} 段)", + "stop": "停止", + "progress": "第 {{current}} / {{total}} 段", + "loading": "加载中..." + } + } +} diff --git a/qwen3-tts-frontend/src/locales/zh-CN/index.ts b/qwen3-tts-frontend/src/locales/zh-CN/index.ts index e35b997..b5fc98d 100644 --- a/qwen3-tts-frontend/src/locales/zh-CN/index.ts +++ b/qwen3-tts-frontend/src/locales/zh-CN/index.ts @@ -9,6 +9,7 @@ import user from './user.json' import errors from './errors.json' import constants from './constants.json' import onboarding from './onboarding.json' +import audiobook from './audiobook.json' export default { common, @@ -22,4 +23,5 @@ export default { errors, constants, onboarding, + audiobook, } diff --git a/qwen3-tts-frontend/src/locales/zh-TW/audiobook.json b/qwen3-tts-frontend/src/locales/zh-TW/audiobook.json new file mode 100644 index 0000000..fe28992 --- /dev/null +++ b/qwen3-tts-frontend/src/locales/zh-TW/audiobook.json @@ -0,0 +1,115 @@ +{ + "title": "有聲書生成", + "llmConfig": "LLM 配置", + "newProject": "新建專案", + "loading": "載入中...", + "noProjects": "暫無有聲書專案", + "noProjectsHint": "點擊「新建專案」開始建立", + + "status": { + "pending": "待分析", + "analyzing": "分析中", + "characters_ready": "角色待確認", + "parsing": "解析章節", + "ready": "待生成", + "generating": "生成中", + "done": "已完成", + "error": "出錯" + }, + + "stepHints": { + "pending": "第 1 步:點擊「分析」,LLM 將自動提取角色列表", + "analyzing": "第 1 步:LLM 正在提取角色,請稍候...", + "characters_ready": "第 2 步:確認角色資訊,可編輯後點擊「確認角色 · 識別章節」", + "ready": "第 3 步:逐章解析劇本(LLM),解析完的章節可立即生成音訊", + "generating": "第 4 步:正在合成音訊,已完成片段可立即播放" + }, + + "llmConfigPanel": { + "title": "LLM 配置", + "current": "當前:{{baseUrl}} / {{model}} / {{keyStatus}}", + "hasKey": "已配置金鑰", + "noKey": "未配置金鑰", + "notSet": "未設置", + "saving": "儲存中...", + "save": "儲存配置", + "savedSuccess": "LLM 配置已儲存", + "incompleteError": "請填寫完整的 LLM 配置" + }, + + "createPanel": { + "title": "新建有聲書專案", + "titlePlaceholder": "書名", + "pasteText": "貼上文字", + "uploadEpub": "上傳 epub", + "textPlaceholder": "貼上小說文字...", + "creating": "建立中...", + "create": "建立專案", + "createdSuccess": "專案已建立", + "titleRequired": "請輸入書名", + "textRequired": "請輸入文字內容", + "epubRequired": "請選擇 epub 檔案" + }, + + "projectCard": { + "analyze": "分析", + "reanalyze": "重新分析", + "reanalyzeConfirm": "重新分析將清除所有角色和章節資料,確定繼續?", + "analyzeStarted": "分析已開始", + "generateAll": "生成全書", + "downloadAll": "下載全書", + "deleteConfirm": "確認刪除專案「{{title}}」及所有音訊?", + "deleteSuccess": "專案已刪除", + "allDoneToast": "「{{title}}」音訊全部生成完成!", + "segmentsProgress": "{{done}} / {{total}} 片段完成", + + "characters": { + "title": "角色列表({{count}} 個)", + "namePlaceholder": "角色名", + "genderPlaceholder": "性別(未設置)", + "genderMale": "男", + "genderFemale": "女", + "genderUnknown": "未知", + "instructPlaceholder": "音色描述(用於 TTS)", + "descPlaceholder": "角色描述", + "voiceDesign": "音色 #{{id}}", + "noVoice": "未分配", + "savedSuccess": "角色已儲存" + }, + + "confirm": { + "button": "確認角色 · 識別章節", + "loading": "識別中...", + "chaptersRecognized": "章節已識別" + }, + + "chapters": { + "title": "章節列表(共 {{count}} 章)", + "processAll": "一鍵全部處理", + "defaultTitle": "第 {{index}} 章", + "parse": "解析此章", + "parsing": "解析中", + "parseStarted": "「{{title}}」解析已開始", + "parseStartedDefault": "章節解析已開始", + "reparse": "重新解析", + "generate": "生成此章", + "generateStarted": "第 {{index}} 章生成已開始", + "generateAllStarted": "全書生成已開始", + "processAllStarted": "全部任務已觸發", + "doneBadge": "已完成 {{count}} 段", + "segmentProgress": "{{done}}/{{total}} 段" + }, + + "segments": { + "errorBadge": "出錯", + "unknownCharacter": "?" + }, + + "sequential": { + "play": "順序播放({{count}} 段)", + "stop": "停止", + "progress": "第 {{current}} / {{total}} 段", + "loading": "載入中..." + } + } +} diff --git a/qwen3-tts-frontend/src/locales/zh-TW/index.ts b/qwen3-tts-frontend/src/locales/zh-TW/index.ts index e35b997..b5fc98d 100644 --- a/qwen3-tts-frontend/src/locales/zh-TW/index.ts +++ b/qwen3-tts-frontend/src/locales/zh-TW/index.ts @@ -9,6 +9,7 @@ import user from './user.json' import errors from './errors.json' import constants from './constants.json' import onboarding from './onboarding.json' +import audiobook from './audiobook.json' export default { common, @@ -22,4 +23,5 @@ export default { errors, constants, onboarding, + audiobook, } diff --git a/qwen3-tts-frontend/src/pages/Audiobook.tsx b/qwen3-tts-frontend/src/pages/Audiobook.tsx index 800f2a2..d25ab50 100644 --- a/qwen3-tts-frontend/src/pages/Audiobook.tsx +++ b/qwen3-tts-frontend/src/pages/Audiobook.tsx @@ -1,4 +1,5 @@ import { useState, useEffect, useCallback, useRef } from 'react' +import { useTranslation } from 'react-i18next' import { toast } from 'sonner' import { Book, Plus, Trash2, RefreshCw, Download, ChevronDown, ChevronUp, Play, Square, Pencil, Check, X, Loader2 } from 'lucide-react' import { Button } from '@/components/ui/button' @@ -27,17 +28,6 @@ function LazyAudioPlayer({ audioUrl, jobId }: { audioUrl: string; jobId: number return
{visible && }
} -const STATUS_LABELS: Record = { - pending: '待分析', - analyzing: '分析中', - characters_ready: '角色待确认', - parsing: '解析章节', - ready: '待生成', - generating: '生成中', - done: '已完成', - error: '出错', -} - const STATUS_COLORS: Record = { pending: 'secondary', analyzing: 'default', @@ -49,13 +39,7 @@ const STATUS_COLORS: Record = { error: 'destructive', } -const STEP_HINTS: Record = { - pending: '第 1 步:点击「分析」,LLM 将自动提取角色列表', - analyzing: '第 1 步:LLM 正在提取角色,请稍候...', - characters_ready: '第 2 步:确认角色信息,可编辑后点击「确认角色 · 识别章节」', - ready: '第 3 步:逐章解析剧本(LLM),解析完的章节可立即生成音频', - generating: '第 4 步:正在合成音频,已完成片段可立即播放', -} +const STEP_HINT_STATUSES = ['pending', 'analyzing', 'characters_ready', 'ready', 'generating'] function SequentialPlayer({ segments, @@ -66,6 +50,7 @@ function SequentialPlayer({ projectId: number onPlayingChange: (segmentId: number | null) => void }) { + const { t } = useTranslation('audiobook') const [displayIndex, setDisplayIndex] = useState(null) const [isLoading, setIsLoading] = useState(false) const audioRef = useRef(new Audio()) @@ -140,15 +125,17 @@ function SequentialPlayer({ {displayIndex !== null ? ( <> - {isLoading ? '加载中...' : `第 ${displayIndex + 1} / ${doneSegments.length} 段`} + {isLoading + ? t('projectCard.sequential.loading') + : t('projectCard.sequential.progress', { current: displayIndex + 1, total: doneSegments.length })} ) : ( )} @@ -228,6 +215,7 @@ function LogStream({ projectId, chapterId, active }: { projectId: number; chapte } function LLMConfigPanel({ onSaved }: { onSaved?: () => void }) { + const { t } = useTranslation('audiobook') const [baseUrl, setBaseUrl] = useState('') const [apiKey, setApiKey] = useState('') const [model, setModel] = useState('') @@ -240,13 +228,13 @@ function LLMConfigPanel({ onSaved }: { onSaved?: () => void }) { const handleSave = async () => { if (!baseUrl || !apiKey || !model) { - toast.error('请填写完整的 LLM 配置') + toast.error(t('llmConfigPanel.incompleteError')) return } setLoading(true) try { await audiobookApi.setLLMConfig({ base_url: baseUrl, api_key: apiKey, model }) - toast.success('LLM 配置已保存') + toast.success(t('llmConfigPanel.savedSuccess')) setApiKey('') const updated = await audiobookApi.getLLMConfig() setExisting(updated) @@ -260,21 +248,28 @@ function LLMConfigPanel({ onSaved }: { onSaved?: () => void }) { return (
-
LLM 配置
+
{t('llmConfigPanel.title')}
{existing && (
- 当前: {existing.base_url || '未设置'} / {existing.model || '未设置'} / {existing.has_key ? '已配置密钥' : '未配置密钥'} + {t('llmConfigPanel.current', { + baseUrl: existing.base_url || t('llmConfigPanel.notSet'), + model: existing.model || t('llmConfigPanel.notSet'), + keyStatus: existing.has_key ? t('llmConfigPanel.hasKey') : t('llmConfigPanel.noKey'), + })}
)} setBaseUrl(e.target.value)} /> setApiKey(e.target.value)} /> setModel(e.target.value)} /> - +
) } function CreateProjectPanel({ onCreated }: { onCreated: () => void }) { + const { t } = useTranslation('audiobook') const [title, setTitle] = useState('') const [sourceType, setSourceType] = useState<'text' | 'epub'>('text') const [text, setText] = useState('') @@ -282,9 +277,9 @@ function CreateProjectPanel({ onCreated }: { onCreated: () => void }) { const [loading, setLoading] = useState(false) const handleCreate = async () => { - if (!title) { toast.error('请输入书名'); return } - if (sourceType === 'text' && !text) { toast.error('请输入文本内容'); return } - if (sourceType === 'epub' && !epubFile) { toast.error('请选择 epub 文件'); return } + if (!title) { toast.error(t('createPanel.titleRequired')); return } + if (sourceType === 'text' && !text) { toast.error(t('createPanel.textRequired')); return } + if (sourceType === 'epub' && !epubFile) { toast.error(t('createPanel.epubRequired')); return } setLoading(true) try { if (sourceType === 'text') { @@ -292,7 +287,7 @@ function CreateProjectPanel({ onCreated }: { onCreated: () => void }) { } else { await audiobookApi.uploadEpub(title, epubFile!) } - toast.success('项目已创建') + toast.success(t('createPanel.createdSuccess')) setTitle(''); setText(''); setEpubFile(null) onCreated() } catch (e: any) { @@ -304,14 +299,18 @@ function CreateProjectPanel({ onCreated }: { onCreated: () => void }) { return (
-
新建有声书项目
- setTitle(e.target.value)} /> +
{t('createPanel.title')}
+ setTitle(e.target.value)} />
- - + +
{sourceType === 'text' && ( -