From 264b5112288c2bb3630f5c14cd4daa09b4c1cd70 Mon Sep 17 00:00:00 2001 From: bdim404 Date: Wed, 11 Mar 2026 14:37:41 +0800 Subject: [PATCH] feat: Implement functionality to retry only failed audiobook chapters and refine UI for batch operations. --- qwen3-tts-backend/api/audiobook.py | 7 +- qwen3-tts-backend/core/audiobook_service.py | 6 +- qwen3-tts-frontend/src/lib/api/audiobook.ts | 5 +- qwen3-tts-frontend/src/pages/Audiobook.tsx | 100 +++++++++++++------- 4 files changed, 76 insertions(+), 42 deletions(-) diff --git a/qwen3-tts-backend/api/audiobook.py b/qwen3-tts-backend/api/audiobook.py index 08693ea..b3de797 100644 --- a/qwen3-tts-backend/api/audiobook.py +++ b/qwen3-tts-backend/api/audiobook.py @@ -270,6 +270,7 @@ async def parse_chapter( @router.post("/projects/{project_id}/parse-all") async def parse_all_chapters_endpoint( project_id: int, + only_errors: bool = False, current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): @@ -285,16 +286,18 @@ async def parse_all_chapters_endpoint( from core.audiobook_service import parse_all_chapters from core.database import SessionLocal + statuses = ("error",) if only_errors else ("pending", "error", "ready") + async def run(): async_db = SessionLocal() try: db_user = crud.get_user_by_id(async_db, current_user.id) - await parse_all_chapters(project_id, db_user, async_db) + await parse_all_chapters(project_id, db_user, async_db, statuses=statuses) finally: async_db.close() asyncio.create_task(run()) - return {"message": "Batch parsing started", "project_id": project_id} + return {"message": "Batch parsing started", "project_id": project_id, "only_errors": only_errors} @router.post("/projects/{project_id}/process-all") diff --git a/qwen3-tts-backend/core/audiobook_service.py b/qwen3-tts-backend/core/audiobook_service.py index 3afff63..aaf2aae 100644 --- a/qwen3-tts-backend/core/audiobook_service.py +++ b/qwen3-tts-backend/core/audiobook_service.py @@ -572,12 +572,12 @@ def merge_audio_files(audio_paths: list[str], output_path: str) -> None: combined.export(output_path, format="wav") -async def parse_all_chapters(project_id: int, user: User, db: Session) -> None: - """Concurrently parse all pending/error/ready chapters using asyncio.Semaphore.""" +async def parse_all_chapters(project_id: int, user: User, db: Session, statuses: tuple = ("pending", "error", "ready")) -> None: + """Concurrently parse chapters with matching statuses using asyncio.Semaphore.""" from core.database import SessionLocal chapters = crud.list_audiobook_chapters(db, project_id) - pending = [ch for ch in chapters if ch.status in ("pending", "error", "ready")] + pending = [ch for ch in chapters if ch.status in statuses] if not pending: return diff --git a/qwen3-tts-frontend/src/lib/api/audiobook.ts b/qwen3-tts-frontend/src/lib/api/audiobook.ts index f098090..d1ba729 100644 --- a/qwen3-tts-frontend/src/lib/api/audiobook.ts +++ b/qwen3-tts-frontend/src/lib/api/audiobook.ts @@ -139,8 +139,9 @@ export const audiobookApi = { return `/audiobook/projects/${projectId}/segments/${segmentId}/audio` }, - parseAllChapters: async (projectId: number): Promise => { - await apiClient.post(`/audiobook/projects/${projectId}/parse-all`) + parseAllChapters: async (projectId: number, onlyErrors?: boolean): Promise => { + const params = onlyErrors ? '?only_errors=true' : '' + await apiClient.post(`/audiobook/projects/${projectId}/parse-all${params}`) }, processAll: async (projectId: number): Promise => { diff --git a/qwen3-tts-frontend/src/pages/Audiobook.tsx b/qwen3-tts-frontend/src/pages/Audiobook.tsx index f492101..36c43dc 100644 --- a/qwen3-tts-frontend/src/pages/Audiobook.tsx +++ b/qwen3-tts-frontend/src/pages/Audiobook.tsx @@ -512,6 +512,19 @@ function ProjectCard({ project, onRefresh }: { project: AudiobookProject; onRefr } } + const handleRetryFailed = async () => { + setIsPolling(true) + try { + await audiobookApi.parseAllChapters(project.id, true) + toast.success(t('projectCard.chapters.parseAllStarted')) + onRefresh() + fetchDetail() + } catch (e: any) { + setIsPolling(false) + toast.error(formatApiError(e)) + } + } + const handleGenerateAll = async () => { if (!detail) return setLoadingAction(true) @@ -689,19 +702,26 @@ function ProjectCard({ project, onRefresh }: { project: AudiobookProject; onRefr
{(chaptersParsing > 0 || chaptersError > 0 || chaptersParsed < chaptersTotal) && (
-
- 📝 - {t('projectCard.chaptersProgress', { parsed: chaptersParsed, total: chaptersTotal })} - {chaptersParsing > 0 && ( - ({t('projectCard.chaptersParsing', { count: chaptersParsing })}) - )} - {chaptersError > 0 && ( - <> - ({t('projectCard.chaptersError', { count: chaptersError })}) - - +
+
+ 📝 + {t('projectCard.chaptersProgress', { parsed: chaptersParsed, total: chaptersTotal })} + {chaptersParsing > 0 && ( + ({t('projectCard.chaptersParsing', { count: chaptersParsing })}) + )} + {chaptersError > 0 && ( + <> + ({t('projectCard.chaptersError', { count: chaptersError })}) + + + )} +
+ {chaptersParsing > 0 && totalCount > 0 && ( + )}
@@ -709,37 +729,40 @@ function ProjectCard({ project, onRefresh }: { project: AudiobookProject; onRefr )} {totalCount > 0 && doneCount > 0 && (
-
- 🎵 - {t('projectCard.segmentsProgress', { done: doneCount, total: totalCount })} +
+
+ 🎵 + {t('projectCard.segmentsProgress', { done: doneCount, total: totalCount })} +
+ {!chaptersParsing && hasGenerating && ( + + )}
)} - {chaptersParsing > 0 && ( - - )} - {!chaptersParsing && hasGenerating && ( - + {chaptersParsing > 0 && !totalCount && ( +
+ +
)}
)}
- {!isActive && status !== 'characters_ready' && ( + {status === 'pending' && ( )} {status === 'ready' && ( @@ -763,13 +786,20 @@ function ProjectCard({ project, onRefresh }: { project: AudiobookProject; onRefr
{detail && detail.characters.length > 0 && (
- +
+ + {!isActive && status !== 'pending' && ( + + )} +
{!charsCollapsed &&
{detail.characters.map(char => (