diff --git a/qwen3-tts-backend/api/audiobook.py b/qwen3-tts-backend/api/audiobook.py index 7c92930..9ccd84c 100644 --- a/qwen3-tts-backend/api/audiobook.py +++ b/qwen3-tts-backend/api/audiobook.py @@ -6,6 +6,7 @@ from typing import Optional from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, status from fastapi.responses import FileResponse, StreamingResponse +from sqlalchemy import func, case from sqlalchemy.orm import Session from api.auth import get_current_user @@ -42,7 +43,7 @@ async def require_nsfw(current_user: User = Depends(get_current_user), db: Sessi return current_user -def _project_to_response(project) -> AudiobookProjectResponse: +def _project_to_response(project, segment_total: int = 0, segment_done: int = 0) -> AudiobookProjectResponse: return AudiobookProjectResponse( id=project.id, user_id=project.user_id, @@ -54,6 +55,8 @@ def _project_to_response(project) -> AudiobookProjectResponse: script_config=getattr(project, 'script_config', None), created_at=project.created_at, updated_at=project.updated_at, + segment_total=segment_total, + segment_done=segment_done, ) @@ -160,7 +163,14 @@ async def list_projects( db: Session = Depends(get_db), ): projects = crud.list_audiobook_projects(db, current_user.id, skip=skip, limit=limit) - return [_project_to_response(p) for p in projects] + project_ids = [p.id for p in projects] + counts = db.query( + AudiobookSegment.project_id, + func.count(AudiobookSegment.id).label('total'), + func.sum(case((AudiobookSegment.status == 'done', 1), else_=0)).label('done'), + ).filter(AudiobookSegment.project_id.in_(project_ids)).group_by(AudiobookSegment.project_id).all() + count_map = {r.project_id: (int(r.total), int(r.done)) for r in counts} + return [_project_to_response(p, *count_map.get(p.id, (0, 0))) for p in projects] @router.post("/projects/generate-synopsis") diff --git a/qwen3-tts-backend/schemas/audiobook.py b/qwen3-tts-backend/schemas/audiobook.py index 1785e37..14dbbaa 100644 --- a/qwen3-tts-backend/schemas/audiobook.py +++ b/qwen3-tts-backend/schemas/audiobook.py @@ -45,6 +45,8 @@ class AudiobookProjectResponse(BaseModel): script_config: Optional[Dict[str, Any]] = None created_at: datetime updated_at: datetime + segment_total: int = 0 + segment_done: int = 0 model_config = ConfigDict(from_attributes=True) diff --git a/qwen3-tts-frontend/src/lib/api/audiobook.ts b/qwen3-tts-frontend/src/lib/api/audiobook.ts index 72f034c..c932288 100644 --- a/qwen3-tts-frontend/src/lib/api/audiobook.ts +++ b/qwen3-tts-frontend/src/lib/api/audiobook.ts @@ -36,6 +36,8 @@ export interface AudiobookProject { script_config?: Record created_at: string updated_at: string + segment_total: number + segment_done: number } export interface AudiobookCharacter { diff --git a/qwen3-tts-frontend/src/pages/Audiobook.tsx b/qwen3-tts-frontend/src/pages/Audiobook.tsx index ccdc5d4..789f65d 100644 --- a/qwen3-tts-frontend/src/pages/Audiobook.tsx +++ b/qwen3-tts-frontend/src/pages/Audiobook.tsx @@ -1,7 +1,7 @@ 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, Zap, Settings2, PanelLeftClose, PanelLeftOpen, Wand2, Volume2, Bot, Flame } from 'lucide-react' +import { Book, BookOpen, Plus, Trash2, RefreshCw, Download, ChevronDown, ChevronUp, Play, Square, Pencil, Check, X, Loader2, Zap, Settings2, PanelLeftClose, PanelLeftOpen, Wand2, Volume2, Bot, Flame } from 'lucide-react' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Textarea } from '@/components/ui/textarea' @@ -1062,25 +1062,48 @@ function ProjectListSidebar({ ) : projects.length === 0 ? (
{t('noProjects')}
) : ( - projects.map(p => ( - - )) + projects.map(p => { + const isNsfw = p.source_type === 'ai_generated' && !!p.script_config?.nsfw_mode + const ProjectIcon = p.source_type === 'epub' + ? BookOpen + : isNsfw + ? Flame + : p.source_type === 'ai_generated' + ? Wand2 + : Book + const iconClass = isNsfw + ? 'h-3.5 w-3.5 shrink-0 mt-0.5 text-orange-500' + : p.source_type === 'ai_generated' + ? 'h-3.5 w-3.5 shrink-0 mt-0.5 text-violet-500' + : 'h-3.5 w-3.5 shrink-0 mt-0.5 text-muted-foreground' + const showProgress = p.segment_total > 0 && ['processing', 'generating', 'done'].includes(p.status) + return ( + + ) + }) )}