From b1aedd9d354e105590141fadb185de9317cae91a Mon Sep 17 00:00:00 2001 From: codesterribly Date: Thu, 11 Dec 2025 11:04:42 -0500 Subject: [PATCH 1/2] Add pronunciation test audio preview --- backend/api/pronunciation.py | 115 ++++++++++++++++++-- frontend/src/utils/translateBackendError.ts | 5 + 2 files changed, 113 insertions(+), 7 deletions(-) diff --git a/backend/api/pronunciation.py b/backend/api/pronunciation.py index 488dad82..773364f2 100644 --- a/backend/api/pronunciation.py +++ b/backend/api/pronunciation.py @@ -25,6 +25,12 @@ MessageResponse ) from services.event_broadcaster import broadcaster, EventType +from pathlib import Path + +from core.tts_engine_manager import get_tts_engine_manager +from db.repositories import SegmentRepository +from services.speaker_service import SpeakerService +from services.settings_service import SettingsService router = APIRouter(prefix="/api/pronunciation", tags=["pronunciation"]) @@ -495,7 +501,8 @@ async def generate_test_audio( ) # Get segment - from db.repositories import SegmentRepository + # Isn't this imported at the top? Is this needed? + #from db.repositories import SegmentRepository segment_repo = SegmentRepository(db) segment = segment_repo.get_by_id(segment_id) @@ -524,18 +531,112 @@ async def generate_test_audio( rules=[test_rule] ) - # Generate audio with transformed text - # This would need to call the TTS engine directly - # For now, return the transformation info + # Generate audio with transformed text using the same engine and speaker + # configuration as the target segment. + + # Segment metadata from DB + engine_name = segment.get('tts_engine') + model_name = segment.get('tts_model_name') + language = segment.get('language') or 'en' + speaker_name = segment.get('tts_speaker_name') + + if not engine_name or not model_name: + raise HTTPException( + status_code=400, + detail=f"[PRONUNCIATION_TEST_ENGINE_NOT_SET]segmentId:{segment_id}" + ) + + # Resolve speaker sample(s) based on the segment's tts_speaker_name + speaker_wav = "" + try: + speaker_service = SpeakerService(db) + if speaker_name: + speaker = speaker_service.get_speaker_by_name(speaker_name) + if not speaker or not speaker.get("samples"): + raise HTTPException( + status_code=400, + detail=f"[PRONUNCIATION_TEST_SPEAKER_NOT_FOUND]speaker:{speaker_name}" + ) + + from config import SPEAKER_SAMPLES_DIR + samples_base = Path(SPEAKER_SAMPLES_DIR) + sample_paths: List[str] = [] + + for sample in speaker["samples"]: + sample_path = samples_base / sample["filePath"] + if not sample_path.exists(): + raise HTTPException( + status_code=500, + detail=( + f"[PRONUNCIATION_TEST_SAMPLE_MISSING]" + f"speaker:{speaker_name};path:{sample_path}" + ) + ) + sample_paths.append(str(sample_path)) + + # If there is only one sample, pass a single string + speaker_wav = sample_paths[0] if len(sample_paths) == 1 else sample_paths + + except HTTPException: + raise + except Exception as e: + logger.error(f"Failed to resolve speaker for pronunciation test: {e}") + # Fall back to empty speaker reference so engines with default voices still work + speaker_wav = "" + + # Load engine parameters from settings + settings_service = SettingsService(db) + engine_parameters = settings_service.get_engine_parameters(engine_name) + + # Ensure TTS engine is ready + tts_manager = get_tts_engine_manager() + try: + await tts_manager.ensure_engine_ready(engine_name, model_name) + except Exception as e: + logger.error(f"Failed to start TTS engine for pronunciation test: {e}") + raise HTTPException( + status_code=500, + detail=( + f"[PRONUNCIATION_TEST_ENGINE_LOAD_FAILED]" + f"engine:{engine_name};model:{model_name};error:{str(e)}" + ) + ) + + # Actually generate the audio for the transformed text + try: + audio_bytes = await tts_manager.generate_with_engine( + engine_name=engine_name, + text=result.transformed_text, + language=language, + speaker_wav=speaker_wav or "", + parameters=engine_parameters, + ) + except Exception as e: + logger.error(f"Failed to generate pronunciation test audio: {e}") + raise HTTPException( + status_code=500, + detail=f"[PRONUNCIATION_TEST_AUDIO_FAILED]error:{str(e)}" + ) + + # Persist short preview file under OUTPUT_DIR/previews + from config import OUTPUT_DIR + previews_dir = Path(OUTPUT_DIR) / "previews" + previews_dir.mkdir(parents=True, exist_ok=True) + + filename = f"pron_test_{segment_id}.wav" + output_path = previews_dir / filename + with open(output_path, "wb") as f: + f.write(audio_bytes) - # TODO: Integrate with engine manager to generate actual audio + # This path is relative to OUTPUT_DIR and is used by /api/audio/{file_path} + relative_path = f"previews/{filename}" return PronunciationTestAudioResponse( original_text=segment['text'], transformed_text=result.transformed_text, rules_applied=result.rules_applied, - audio_path=None, - message="Test transformation complete (audio generation not yet implemented)" + audio_path=relative_path, + message="Test transformation complete" ) except HTTPException: diff --git a/frontend/src/utils/translateBackendError.ts b/frontend/src/utils/translateBackendError.ts index 6508430f..7ceece3a 100644 --- a/frontend/src/utils/translateBackendError.ts +++ b/frontend/src/utils/translateBackendError.ts @@ -233,8 +233,13 @@ export function translateBackendError(errorMessage: string, t: TFunction): strin PRONUNCIATION_BULK_OPERATION_FAILED: 'pronunciation.errors.bulkOperationFailed', PRONUNCIATION_RULES_EXPORT_FAILED: 'pronunciation.errors.exportFailed', PRONUNCIATION_RULES_IMPORT_FAILED: 'pronunciation.errors.importFailed', + PRONUNCIATION_TEST_ENGINE_NOT_SET: 'pronunciation.errors.testEngineNotSet', + PRONUNCIATION_TEST_SPEAKER_NOT_FOUND: 'pronunciation.errors.testSpeakerNotFound', + PRONUNCIATION_TEST_SAMPLE_MISSING: 'pronunciation.errors.testSampleMissing', + PRONUNCIATION_TEST_ENGINE_LOAD_FAILED: 'pronunciation.errors.testEngineLoadFailed', PRONUNCIATION_TEST_AUDIO_FAILED: 'pronunciation.errors.testAudioFailed', + // Settings errors SETTINGS_KEY_NOT_FOUND: 'settings.errors.keyNotFound', SETTINGS_ENGINE_NOT_FOUND: 'settings.errors.engineNotFound', From ba993ae678bf600db13619de3f30c6014e3abefa Mon Sep 17 00:00:00 2001 From: codesterribly Date: Fri, 12 Dec 2025 21:28:33 -0500 Subject: [PATCH 2/2] =?UTF-8?q?Add=20one-click=20Book=20=E2=86=92=20Audiob?= =?UTF-8?q?ook=20wizard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/hooks/useFullBookWizard.ts | 323 ++++++++++++++++++++++ frontend/src/i18n/locales/de.json | 12 + frontend/src/i18n/locales/en.json | 14 +- frontend/src/pages/FullBookWizardView.tsx | 239 ++++++++++++++++ frontend/src/pages/ImportView.tsx | 122 ++++++-- 5 files changed, 686 insertions(+), 24 deletions(-) create mode 100644 frontend/src/hooks/useFullBookWizard.ts create mode 100644 frontend/src/pages/FullBookWizardView.tsx diff --git a/frontend/src/hooks/useFullBookWizard.ts b/frontend/src/hooks/useFullBookWizard.ts new file mode 100644 index 00000000..3c243b14 --- /dev/null +++ b/frontend/src/hooks/useFullBookWizard.ts @@ -0,0 +1,323 @@ +import { useCallback, useState } from 'react' +import { useTranslation } from 'react-i18next' + +import { projectApi, ttsApi } from '../services/api' +import type { ImportExecuteResponse, MappingRules } from '../types' +import { DEFAULT_MAPPING_RULES } from '../types' +import { useAppStore } from '../store/appStore' +import { useUISettingsStore } from '../store/uiSettingsStore' +import { useTextEngineLanguages } from './useTextEngineLanguages' +import { useDefaultSpeaker } from './useSpeakersQuery' +import { useSnackbar } from './useSnackbar' +import { logger } from '../utils/logger' +import { translateBackendError } from '../utils/translateBackendError' + +export type FullBookWizardStatus = + | 'idle' + | 'importing' + | 'tts' + | 'done' + | 'error' + +export interface FullBookWizardState { + status: FullBookWizardStatus + message: string + isRunning: boolean + lastProjectId: string | null + lastProjectTitle: string | null + lastChapterCount: number +} + +/** + * Small helper to detect EPUB files, mirroring useImportQuery logic. + */ +function isEpubFile(file: File): boolean { + const name = file.name.toLowerCase() + return name.endsWith('.epub') +} + +/** + * One-click "Book → Audiobook" wizard: + * + * 1. Imports the book (Markdown or EPUB) as a new project. + * 2. Applies DEFAULT_MAPPING_RULES for chapter detection. + * 3. Uses the current default TTS engine / model / language / speaker. + * 4. Starts TTS generation for every chapter in the new project. + * + * Export still uses the existing export workflow (Export view). + */ +export function useFullBookWizard() { + const { t } = useTranslation() + const { showSnackbar } = useSnackbar() + + + const [state, setState] = useState({ + status: 'idle', + message: '', + isRunning: false, + lastProjectId: null, + lastProjectTitle: null, + lastChapterCount: 0, + }) + + // Global TTS defaults from app store + const getDefaultTtsEngine = useAppStore( + (s) => s.getDefaultTtsEngine, + ) + const getDefaultTtsModel = useAppStore( + (s) => s.getDefaultTtsModel, + ) + const getDefaultLanguage = useAppStore( + (s) => s.getDefaultLanguage, + ) + + // UI / text language for import + const uiLanguage = useUISettingsStore((s) => s.settings.uiLanguage) + const { languages: textLanguages } = useTextEngineLanguages() + + // Default speaker + const { data: defaultSpeaker } = useDefaultSpeaker() + + const resetState = useCallback(() => { + setState({ + status: 'idle', + message: '', + isRunning: false, + lastProjectId: null, + lastProjectTitle: null, + lastChapterCount: 0, + }) + }, []) + + const runWizard = useCallback( + async (file: File): Promise => { + if (!file) { + const msg = t('import.actions.noFileSelected', 'No file selected') + showSnackbar(msg, { severity: 'error' }) + throw new Error(msg) + } + + resetState() + + setState((prev) => ({ + ...prev, + status: 'importing', + isRunning: true, + message: + t('wizard.importingBook', 'Importing book and creating project...'), + })) + + try { + // 1) Import: mapping rules and language + const mappingRules: MappingRules = DEFAULT_MAPPING_RULES + + let textLanguage = uiLanguage || 'en' + if (textLanguages && textLanguages.length > 0) { + if (uiLanguage && textLanguages.includes(uiLanguage)) { + textLanguage = uiLanguage + } else { + textLanguage = textLanguages[0] + } + } + + // 2) TTS defaults (engine / model / language / speaker) + const defaultEngine = getDefaultTtsEngine() + const ttsEngine = defaultEngine || '' + const ttsModelName = + (ttsEngine && getDefaultTtsModel(ttsEngine)) || '' + const ttsLanguage = + (ttsEngine && getDefaultLanguage(ttsEngine)) || + textLanguage || + 'en' + const ttsSpeakerName = defaultSpeaker?.name ?? '' + + const mode: 'new' = 'new' + const mergeTargetId: string | null = null + const selectedChapters: string[] = [] + const renamedChapters: Record = {} + + // 3) Execute import directly (no preview step, "new" mode imports all chapters) + const importPromise = isEpubFile(file) + ? projectApi.executeEpubImport( + file, + mappingRules, + textLanguage, + mode, + mergeTargetId, + selectedChapters, + renamedChapters, + { + ttsEngine, + ttsModelName, + ttsLanguage, + ttsSpeakerName, + }, + ) + : projectApi.executeMarkdownImport( + file, + mappingRules, + textLanguage, + mode, + mergeTargetId, + selectedChapters, + renamedChapters, + { + ttsEngine, + ttsModelName, + ttsLanguage, + ttsSpeakerName, + }, + ) + + const importResult = (await importPromise) as ImportExecuteResponse + + const project = importResult.project + const chapters = project.chapters || [] + const chapterCount = chapters.length + + setState((prev) => ({ + ...prev, + lastProjectId: importResult.projectId, + lastProjectTitle: project.title, + lastChapterCount: chapterCount, + })) + + if (chapterCount === 0) { + const msg = + t( + 'wizard.importedNoChapters', + 'Import completed but no chapters were created.', + ) || + 'Import completed but no chapters were created.' + setState((prev) => ({ + ...prev, + status: 'done', + isRunning: false, + message: msg, + })) + showSnackbar(msg, { severity: 'warning' }) + return importResult + } + + // 4) Kick off TTS for every chapter + setState((prev) => ({ + ...prev, + status: 'tts', + message: + t( + 'wizard.generatingTts', + 'Starting text-to-speech jobs for all chapters...', + ) || + 'Starting text-to-speech jobs for all chapters...', + })) + + for (let index = 0; index < chapters.length; index += 1) { + const chapter = chapters[index] + const displayIndex = index + 1 + + const stepMsg = + t('wizard.generatingChapter', { + index: displayIndex, + total: chapterCount, + title: chapter.title, + }) || + `Generating audio for chapter ${displayIndex}/${chapterCount}: ${chapter.title}` + + setState((prev) => ({ + ...prev, + status: 'tts', + message: stepMsg, + })) + + try { + await ttsApi.generateChapter({ + chapterId: chapter.id, + forceRegenerate: false, + overrideSegmentSettings: false, + }) + } catch (err) { + logger.error('Failed to start TTS for chapter', { + error: err, + chapterId: chapter.id, + }) + + // Non-fatal: keep going for other chapters, but show a warning + const warnMsg = + t('wizard.chapterTtsFailed', { + defaultValue: + 'Failed to start TTS for chapter "{{title}}". See logs for details.', + title: chapter.title, + }) || + `Failed to start TTS for chapter "${chapter.title}". See logs for details.` + showSnackbar(warnMsg, { severity: 'warning' }) + } + } + + const finalMsg = + t( + 'wizard.completed', + 'Book imported and TTS jobs started for all chapters. You can monitor progress in the Jobs view and export audio when ready.', + ) || + 'Book imported and TTS jobs started for all chapters. You can monitor progress in the Jobs view and export audio when ready.' + + setState((prev) => ({ + ...prev, + status: 'done', + isRunning: false, + message: finalMsg, + })) + + showSnackbar(finalMsg, { severity: 'success' }) + + return importResult + } catch (error: any) { + logger.error('Full-book wizard failed', { error }) + + let friendlyMessage = + t( + 'wizard.unknownError', + 'An error occurred while running the Book → Audiobook wizard.', + ) || + 'An error occurred while running the Book → Audiobook wizard.' + + if (error?.response?.data?.detail) { + try { + friendlyMessage = translateBackendError( + error.response.data.detail, + t, + ) + } catch { + // Fall back to generic message + } + } else if (error instanceof Error && error.message) { + friendlyMessage = error.message + } + + setState((prev) => ({ + ...prev, + status: 'error', + isRunning: false, + message: friendlyMessage, + })) + + showSnackbar(friendlyMessage, { severity: 'error' }) + throw error + } + }, [ + t, + showSnackbar, + uiLanguage, + textLanguages, + defaultSpeaker, + getDefaultTtsEngine, + getDefaultTtsModel, + getDefaultLanguage, + resetState, + ], +) + return { + ...state, + runWizard, + resetWizard: resetState, + } +} diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json index 518f7436..ba69c57b 100644 --- a/frontend/src/i18n/locales/de.json +++ b/frontend/src/i18n/locales/de.json @@ -1109,6 +1109,9 @@ "actions": { "import": "Importieren", "importing": "Importiere...", + "wizard": "Importieren + Hörbuch erzeugen", + "wizardRunning": "Assistent läuft...", + "wizardRequiresNew": "Der Assistent unterstützt nur „Neues Projekt“. Modus wurde auf „Neu“ umgestellt.", "error": "Import fehlgeschlagen", "success": "Import abgeschlossen ({{chapters}} Kapitel, {{segments}} Segmente)", "invalidConfig": "Bitte füllen Sie alle erforderlichen Felder aus, bevor Sie importieren", @@ -1385,5 +1388,14 @@ "missingTargetId": "Zielprojekt-ID erforderlich für Merge-Modus", "unknownEngine": "Unbekannte TTS-Engine: {{engine}}" } + }, + "wizard": { + "importingBook": "Buch wird importiert und Projekt wird erstellt...", + "importedNoChapters": "Import abgeschlossen, aber es wurden keine Kapitel erstellt.", + "generatingTts": "Text-zu-Sprache-Jobs für alle Kapitel werden gestartet...", + "generatingChapter": "Audio wird für Kapitel {{index}}/{{total}} erzeugt: {{title}}", + "chapterTtsFailed": "TTS konnte für das Kapitel \"{{title}}\" nicht gestartet werden. Siehe Logs für Details.", + "completed": "Buch importiert und TTS-Jobs für alle Kapitel gestartet. Fortschritt im Jobs-Bereich prüfen und exportieren, wenn alles fertig ist.", + "unknownError": "Beim Ausführen des Buch → Hörbuch-Assistenten ist ein Fehler aufgetreten." } } diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 05417589..5273a8fe 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -1109,12 +1109,15 @@ "actions": { "import": "Import", "importing": "Importing...", + "wizard": "Import + Generate Audiobook", + "wizardRunning": "Running wizard...", "error": "Import failed", "success": "Import complete ({{chapters}} chapters, {{segments}} segments)", "invalidConfig": "Please complete all required fields before importing", "noFileSelected": "Please select a file to import", "noMergeTarget": "Please select a project to merge into", - "noChaptersSelected": "Please select at least one chapter to import" + "noChaptersSelected": "Please select at least one chapter to import", + "wizardRequiresNew": "Wizard supports only Create New Project. Switched mode to New Project." }, "preview": { "emptyTitle": "No Preview Available", @@ -1385,5 +1388,14 @@ "missingTargetId": "Target project ID is required for merge mode", "unknownEngine": "Unknown TTS engine: {{engine}}" } + }, + "wizard": { + "importingBook": "Importing book and creating project...", + "importedNoChapters": "Import completed but no chapters were created.", + "generatingTts": "Starting text-to-speech jobs for all chapters...", + "generatingChapter": "Generating audio for chapter {{index}}/{{total}}: {{title}}", + "chapterTtsFailed": "Failed to start TTS for chapter \"{{title}}\". See logs for details.", + "completed": "Book imported and TTS jobs started for all chapters. You can monitor progress in the Jobs view and export audio when ready.", + "unknownError": "An error occurred while running the Book → Audiobook wizard." } } diff --git a/frontend/src/pages/FullBookWizardView.tsx b/frontend/src/pages/FullBookWizardView.tsx new file mode 100644 index 00000000..cd611ed2 --- /dev/null +++ b/frontend/src/pages/FullBookWizardView.tsx @@ -0,0 +1,239 @@ +import React, { useState, useMemo } from 'react' +import { + Box, + Paper, + Typography, + Stack, + Button, + LinearProgress, + FormControl, + InputLabel, + Select, + MenuItem, + TextField, +} from '@mui/material' +import { Upload as UploadIcon, PlayArrow as PlayIcon } from '@mui/icons-material' +import { useTranslation } from 'react-i18next' +import { useFullBookWizard, type FullBookWizardOptions } from '@hooks/useFullBookWizard' +import { defaultMappingRules } from '@types' // adjust import to where you define defaults + +export function FullBookWizardView() { + const { t } = useTranslation() + const [file, setFile] = useState(null) + const [language, setLanguage] = useState('en') + const [outputFormat, setOutputFormat] = + useState('mp3') + const [outputQuality, setOutputQuality] = + useState('high') + + // For now: force "new" projects, you can extend later + const mode: FullBookWizardOptions['mode'] = 'new' + + const { + step, + progressText, + error, + result, + startWizard, + isRunning, + reset, + } = useFullBookWizard() + + const canStart = useMemo(() => { + return !!file && !isRunning + }, [file, isRunning]) + + const handleFileChange = (event: React.ChangeEvent) => { + const selected = event.target.files?.[0] + if (selected) { + setFile(selected) + } + } + + const handleStart = async () => { + if (!file) { + return + } + + const options: FullBookWizardOptions = { + mappingRules: defaultMappingRules, // use your existing defaults + language, + mode, + mergeTargetId: null, + selectedChapters: [], // all chapters + renamedChapters: {}, + ttsSettings: { + ttsEngine: 'local', // or pull defaults from settings + ttsModelName: 'default', // adjust to real default + language, + }, + exportFormat: outputFormat, + exportQuality: outputQuality, + } + + try { + await startWizard(file, options) + } catch { + // error state is already handled in hook + } + } + + return ( + + + {t('wizard.fullBook.title', 'Book → Audiobook Wizard')} + + + + {t( + 'wizard.fullBook.description', + 'Upload a structured Markdown or EPUB, pick your settings, and let the wizard import, generate TTS, and start exports for all chapters.' + )} + + + + + {/* File upload */} + + + {file && ( + + {t('wizard.fullBook.selectedFile', 'Selected file')}: {file.name} + + )} + + + {/* Basic settings */} + + setLanguage(e.target.value)} + size="small" + /> + + + + {t('wizard.fullBook.outputFormat', 'Output format')} + + + + + + + {t('wizard.fullBook.outputQuality', 'Quality')} + + + + + + {/* Start button */} + + + + {isRunning && ( + + {t('wizard.fullBook.running', 'Wizard is running...')} + + )} + + {step === 'done' && result && ( + + {t( + 'wizard.fullBook.done', + 'Done. TTS and export jobs were started for {{count}} chapters.', + { count: result.chapterIds.length } + )} + + )} + + {step === 'error' && error && ( + + {t('wizard.fullBook.error', 'Wizard failed')}: {error} + + )} + + + {/* Progress bar and text */} + {isRunning && ( + + + + {progressText} + + + )} + + {/* Debug summary */} + {result && ( + + + {t('wizard.fullBook.summary', 'Summary')} + + + Project: {result.projectId} + + + Chapters: {result.chapterIds.length} + + + Export jobs: {result.exportJobs.length} + + + )} + + + + ) +} + +export default FullBookWizardView diff --git a/frontend/src/pages/ImportView.tsx b/frontend/src/pages/ImportView.tsx index 52d61b29..c5177d92 100644 --- a/frontend/src/pages/ImportView.tsx +++ b/frontend/src/pages/ImportView.tsx @@ -56,11 +56,13 @@ import { useNavigationStore } from '../store/navigationStore' import { useSnackbar } from '../hooks/useSnackbar' import { useDefaultSpeaker } from '../hooks/useSpeakersQuery' import { translateBackendError } from '../utils/translateBackendError' +import { useFullBookWizard } from '../hooks/useFullBookWizard' + const ImportView = memo(() => { const { t } = useTranslation() const navigateTo = useNavigationStore((state) => state.navigateTo) - const { showSnackbar, SnackbarComponent } = useSnackbar() + const { SnackbarComponent, showSnackbar } = useSnackbar() const [importError, setImportError] = useState(null) // Check if import feature is available (requires text processing engine) @@ -119,6 +121,66 @@ const ImportView = memo(() => { // Preview and execute mutation hooks const previewMutation = usePreviewImport() const executeImport = useExecuteImport() + const fullBookWizard = useFullBookWizard() + + // Handle wizard: import + full audiobook + const handleImportAndGenerate = useCallback(async () => { + // Clear previous error + setImportError(null) + + // Wizard only supports new project imports (UX: auto-switch) + if (importMode === 'merge') { + setImportMode('new') + setMergeTargetId(null) + setExpandedSection('mode') + showSnackbar( + t( + 'import.actions.wizardRequiresNew', + 'Wizard supports only Create New Project. Switched mode to New Project.', + ), + { severity: 'info' }, + ) + // DO NOT return, continue and run wizard now + } + + // Reuse the same validations as handleImport + if (!selectedFile) { + setImportError(t('import.actions.noFileSelected')) + return + } + + try { + const result = await fullBookWizard.runWizard(selectedFile) + + // Store the imported project ID in sessionStorage for AppLayout to pick up + sessionStorage.setItem('selectedProjectId', result.project.id) + + // Optional: show a success banner after navigation (same pattern as handleImport) + sessionStorage.setItem( + 'importSuccessMessage', + t( + 'wizard.completed', + 'Book imported and TTS jobs started for all chapters. You can monitor progress in the Jobs view and export audio when ready.', + ), + ) + + // Navigate to Jobs (Monitoring) + navigateTo('monitoring') + } catch (err) { + const errorMessage = translateBackendError( + err instanceof Error ? err.message : t('import.actions.error'), + t + ) + setImportError(errorMessage) + } + }, [ + selectedFile, + importMode, + fullBookWizard, + t, + navigateTo, + showSnackbar, + ]) const handleFileSelect = useCallback((file: File | null) => { setSelectedFile(file) @@ -297,7 +359,7 @@ const ImportView = memo(() => { mergeTargetId, selectedChapters, mappingRules, - uiLanguage, + textLanguage, renamedChapters, ttsEngine, ttsModelName, @@ -352,28 +414,42 @@ const ImportView = memo(() => { - ) : ( - - ) - } - > - {executeImport.isPending - ? t('import.actions.importing') - : t('import.actions.import')} - - } - /> + + + + + } + /> {/* Split-View Content */}