From 0d8756ebabc333c8c6a939ec5b13c22b6460295b Mon Sep 17 00:00:00 2001 From: bdim404 Date: Wed, 11 Mar 2026 16:37:33 +0800 Subject: [PATCH] feat: Implement generation cancellation for projects, update project status handling, and mark chapters as done upon segment completion. --- qwen3-tts-backend/api/audiobook.py | 4 +-- qwen3-tts-backend/core/audiobook_service.py | 29 ++++++++++++++++++--- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/qwen3-tts-backend/api/audiobook.py b/qwen3-tts-backend/api/audiobook.py index 996e7fb..29c4746 100644 --- a/qwen3-tts-backend/api/audiobook.py +++ b/qwen3-tts-backend/api/audiobook.py @@ -319,7 +319,7 @@ async def parse_all_chapters_endpoint( project = crud.get_audiobook_project(db, project_id, current_user.id) if not project: raise HTTPException(status_code=404, detail="Project not found") - if project.status not in ("ready", "done", "error"): + if project.status not in ("ready", "generating", "done", "error"): raise HTTPException(status_code=400, detail=f"Project must be in 'ready' state, current: {project.status}") if not current_user.llm_api_key or not current_user.llm_base_url or not current_user.llm_model: @@ -328,7 +328,7 @@ 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") + statuses = ("error",) if only_errors else ("pending", "error") async def run(): async_db = SessionLocal() diff --git a/qwen3-tts-backend/core/audiobook_service.py b/qwen3-tts-backend/core/audiobook_service.py index f7d40a2..210f6b6 100644 --- a/qwen3-tts-backend/core/audiobook_service.py +++ b/qwen3-tts-backend/core/audiobook_service.py @@ -497,11 +497,17 @@ 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) -> None: +async def generate_project(project_id: int, user: User, db: Session, chapter_index: Optional[int] = None, cancel_event: Optional[asyncio.Event] = None) -> None: project = db.query(AudiobookProject).filter(AudiobookProject.id == project_id).first() if not project: return + # Resolve cancel event: use explicit one, or fall back to global _cancel_events + if cancel_event is None: + if project_id not in _cancel_events: + _cancel_events[project_id] = asyncio.Event() + cancel_event = _cancel_events[project_id] + try: if chapter_index is None: crud.update_audiobook_project_status(db, project_id, "generating") @@ -535,6 +541,11 @@ async def generate_project(project_id: int, user: User, db: Session, chapter_ind await _bootstrap_character_voices(segments, user, backend, backend_type, db) for seg in segments: + # Check cancel event before each segment + if cancel_event and cancel_event.is_set(): + logger.info(f"Generation cancelled for project {project_id}, stopping at segment {seg.id}") + break + try: crud.update_audiobook_segment_status(db, seg.id, "generating") @@ -615,6 +626,18 @@ async def generate_project(project_id: int, user: User, db: Session, chapter_ind logger.error(f"Segment {seg.id} generation failed: {e}", exc_info=True) crud.update_audiobook_segment_status(db, seg.id, "error") + # Update chapter status to "done" if all its segments are complete + if chapter_index is not None: + ch_segs = crud.list_audiobook_segments(db, project_id, chapter_index=chapter_index) + if ch_segs and all(s.status == "done" for s in ch_segs): + from db.models import AudiobookChapter + chapter_obj = db.query(AudiobookChapter).filter( + AudiobookChapter.project_id == project_id, + AudiobookChapter.chapter_index == chapter_index, + ).first() + if chapter_obj: + crud.update_audiobook_chapter_status(db, chapter_obj.id, "done") + all_segs = crud.list_audiobook_segments(db, project_id) all_done = all(s.status == "done" for s in all_segs) if all_segs else False if all_done: @@ -645,7 +668,7 @@ 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, statuses: tuple = ("pending", "error", "ready")) -> None: +async def parse_all_chapters(project_id: int, user: User, db: Session, statuses: tuple = ("pending", "error")) -> None: """Concurrently parse chapters with matching statuses using asyncio.Semaphore.""" from core.database import SessionLocal @@ -719,7 +742,7 @@ async def generate_all_chapters(project_id: int, user: User, db: Session) -> Non task_db = SessionLocal() try: db_user = crud.get_user_by_id(task_db, user.id) - await generate_project(project_id, db_user, task_db, chapter_index=chapter.chapter_index) + await generate_project(project_id, db_user, task_db, chapter_index=chapter.chapter_index, cancel_event=cancel_ev) except Exception as e: logger.error(f"generate_all_chapters: chapter {chapter.chapter_index} failed: {e}", exc_info=True) finally: