feat: add segment tracking to audiobook projects and update UI to display progress
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -36,6 +36,8 @@ export interface AudiobookProject {
|
||||
script_config?: Record<string, unknown>
|
||||
created_at: string
|
||||
updated_at: string
|
||||
segment_total: number
|
||||
segment_done: number
|
||||
}
|
||||
|
||||
export interface AudiobookCharacter {
|
||||
|
||||
@@ -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 ? (
|
||||
<div className="px-3 py-4 text-xs text-muted-foreground">{t('noProjects')}</div>
|
||||
) : (
|
||||
projects.map(p => (
|
||||
<button
|
||||
key={p.id}
|
||||
onClick={() => onSelect(p.id)}
|
||||
className={`w-full text-left rounded-lg border p-3 flex flex-col gap-1.5 transition-colors hover:bg-muted/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 ${
|
||||
selectedId === p.id
|
||||
? 'bg-background border-border shadow-sm'
|
||||
: 'bg-background/50 border-border/40'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-2">
|
||||
<Book className="h-3.5 w-3.5 shrink-0 mt-0.5 text-muted-foreground" />
|
||||
<span className="text-sm font-medium leading-snug break-words min-w-0 flex-1">{p.title}</span>
|
||||
</div>
|
||||
<Badge variant={(STATUS_COLORS[p.status] || 'secondary') as any} className="text-[10px] h-4 px-1 self-start ml-5">
|
||||
{t(`status.${p.status}`, { defaultValue: p.status })}
|
||||
</Badge>
|
||||
</button>
|
||||
))
|
||||
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 (
|
||||
<button
|
||||
key={p.id}
|
||||
onClick={() => onSelect(p.id)}
|
||||
className={`w-full text-left rounded-lg border p-3 flex flex-col gap-1.5 transition-colors hover:bg-muted/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 ${
|
||||
selectedId === p.id
|
||||
? 'bg-background border-border shadow-sm'
|
||||
: 'bg-background/50 border-border/40'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-2">
|
||||
<ProjectIcon className={iconClass} />
|
||||
<span className="text-sm font-medium leading-snug break-words min-w-0 flex-1">{p.title}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 ml-5">
|
||||
<Badge variant={(STATUS_COLORS[p.status] || 'secondary') as any} className="text-[10px] h-4 px-1">
|
||||
{t(`status.${p.status}`, { defaultValue: p.status })}
|
||||
</Badge>
|
||||
{showProgress && (
|
||||
<span className="text-[10px] text-muted-foreground tabular-nums">
|
||||
{p.segment_done}/{p.segment_total}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user