diff --git a/qwen3-tts-backend/api/audiobook.py b/qwen3-tts-backend/api/audiobook.py index 6dd13d6..59a9517 100644 --- a/qwen3-tts-backend/api/audiobook.py +++ b/qwen3-tts-backend/api/audiobook.py @@ -457,12 +457,13 @@ async def generate_project( from core.database import SessionLocal chapter_index = data.chapter_index + force = data.force async def run_generation(): async_db = SessionLocal() try: db_user = crud.get_user_by_id(async_db, current_user.id) - await _generate(project_id, db_user, async_db, chapter_index=chapter_index) + await _generate(project_id, db_user, async_db, chapter_index=chapter_index, force=force) finally: async_db.close() diff --git a/qwen3-tts-backend/core/audiobook_service.py b/qwen3-tts-backend/core/audiobook_service.py index b493767..0ec3044 100644 --- a/qwen3-tts-backend/core/audiobook_service.py +++ b/qwen3-tts-backend/core/audiobook_service.py @@ -536,7 +536,7 @@ async def _bootstrap_character_voices(segments, user, backend, backend_type: str logger.error(f"Failed to bootstrap voice for design_id={design.id}: {e}", exc_info=True) -async def generate_project(project_id: int, user: User, db: Session, chapter_index: Optional[int] = None, cancel_event: Optional[asyncio.Event] = None) -> None: +async def generate_project(project_id: int, user: User, db: Session, chapter_index: Optional[int] = None, cancel_event: Optional[asyncio.Event] = None, force: bool = False) -> None: project = db.query(AudiobookProject).filter(AudiobookProject.id == project_id).first() if not project: return @@ -551,6 +551,11 @@ async def generate_project(project_id: int, user: User, db: Session, chapter_ind crud.update_audiobook_project_status(db, project_id, "generating") segments = crud.list_audiobook_segments(db, project_id, chapter_index=chapter_index) + if force: + for s in segments: + if s.status == "done": + crud.update_audiobook_segment_status(db, s.id, "pending") + segments = crud.list_audiobook_segments(db, project_id, chapter_index=chapter_index) pending_segments = [s for s in segments if s.status in ("pending", "error")] if not pending_segments: if chapter_index is None: diff --git a/qwen3-tts-backend/schemas/audiobook.py b/qwen3-tts-backend/schemas/audiobook.py index 4dda842..e938fcc 100644 --- a/qwen3-tts-backend/schemas/audiobook.py +++ b/qwen3-tts-backend/schemas/audiobook.py @@ -58,6 +58,7 @@ class AudiobookAnalyzeRequest(BaseModel): class AudiobookGenerateRequest(BaseModel): chapter_index: Optional[int] = None + force: bool = False class AudiobookCharacterUpdate(BaseModel): diff --git a/qwen3-tts-frontend/src/lib/api/audiobook.ts b/qwen3-tts-frontend/src/lib/api/audiobook.ts index ba91cb0..aa86747 100644 --- a/qwen3-tts-frontend/src/lib/api/audiobook.ts +++ b/qwen3-tts-frontend/src/lib/api/audiobook.ts @@ -118,9 +118,10 @@ export const audiobookApi = { await apiClient.post(`/audiobook/projects/${projectId}/chapters/${chapterId}/parse`) }, - generate: async (id: number, chapterIndex?: number): Promise => { + generate: async (id: number, chapterIndex?: number, force?: boolean): Promise => { await apiClient.post(`/audiobook/projects/${id}/generate`, { chapter_index: chapterIndex ?? null, + force: force ?? false, }) }, diff --git a/qwen3-tts-frontend/src/pages/Audiobook.tsx b/qwen3-tts-frontend/src/pages/Audiobook.tsx index a2765d4..cc2079c 100644 --- a/qwen3-tts-frontend/src/pages/Audiobook.tsx +++ b/qwen3-tts-frontend/src/pages/Audiobook.tsx @@ -737,7 +737,7 @@ function ChaptersPanel({ generatingChapterIndices: Set sequentialPlayingId: number | null onParseChapter: (chapterId: number, title?: string) => void - onGenerate: (chapterIndex?: number) => void + onGenerate: (chapterIndex?: number, force?: boolean) => void onParseAll: () => void onGenerateAll: () => void onProcessAll: () => void @@ -921,6 +921,12 @@ function ChaptersPanel({ {ch.status === 'ready' && chAllDone && ( <> {t('projectCard.chapters.doneBadge', { count: chDone })} + @@ -1179,7 +1185,7 @@ export default function Audiobook() { } }, [segments, generatingChapterIndices]) - const shouldPoll = isPolling || ['analyzing', 'generating'].includes(status) || hasParsingChapter || generatingChapterIndices.size > 0 + const shouldPoll = isPolling || ['analyzing', 'generating'].includes(status) || hasParsingChapter || generatingChapterIndices.size > 0 || segments.some(s => s.status === 'generating') useEffect(() => { if (!shouldPoll || !selectedProjectId) return @@ -1235,7 +1241,7 @@ export default function Audiobook() { } } - const handleGenerate = async (chapterIndex?: number) => { + const handleGenerate = async (chapterIndex?: number, force?: boolean) => { if (!selectedProject) return setLoadingAction(true) if (chapterIndex !== undefined) { @@ -1244,7 +1250,7 @@ export default function Audiobook() { setIsPolling(true) } try { - await audiobookApi.generate(selectedProject.id, chapterIndex) + await audiobookApi.generate(selectedProject.id, chapterIndex, force) toast.success(chapterIndex !== undefined ? t('projectCard.chapters.generateStarted', { index: chapterIndex + 1 }) : t('projectCard.chapters.generateAllStarted')) @@ -1365,7 +1371,6 @@ export default function Audiobook() { if (!selectedProject) return try { await audiobookApi.regenerateSegment(selectedProject.id, segmentId) - setIsPolling(true) fetchSegments() } catch (e: any) { toast.error(formatApiError(e))