feat: Implement functionality to retry only failed audiobook chapters and refine UI for batch operations.
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -139,8 +139,9 @@ export const audiobookApi = {
|
||||
return `/audiobook/projects/${projectId}/segments/${segmentId}/audio`
|
||||
},
|
||||
|
||||
parseAllChapters: async (projectId: number): Promise<void> => {
|
||||
await apiClient.post(`/audiobook/projects/${projectId}/parse-all`)
|
||||
parseAllChapters: async (projectId: number, onlyErrors?: boolean): Promise<void> => {
|
||||
const params = onlyErrors ? '?only_errors=true' : ''
|
||||
await apiClient.post(`/audiobook/projects/${projectId}/parse-all${params}`)
|
||||
},
|
||||
|
||||
processAll: async (projectId: number): Promise<void> => {
|
||||
|
||||
@@ -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,7 +702,8 @@ function ProjectCard({ project, onRefresh }: { project: AudiobookProject; onRefr
|
||||
<div className="space-y-2">
|
||||
{(chaptersParsing > 0 || chaptersError > 0 || chaptersParsed < chaptersTotal) && (
|
||||
<div className="space-y-1">
|
||||
<div className="text-xs text-muted-foreground flex items-center gap-1 flex-wrap">
|
||||
<div className="text-xs text-muted-foreground flex items-center justify-between">
|
||||
<div className="flex items-center gap-1 flex-wrap">
|
||||
<span>📝</span>
|
||||
<span>{t('projectCard.chaptersProgress', { parsed: chaptersParsed, total: chaptersTotal })}</span>
|
||||
{chaptersParsing > 0 && (
|
||||
@@ -698,48 +712,57 @@ function ProjectCard({ project, onRefresh }: { project: AudiobookProject; onRefr
|
||||
{chaptersError > 0 && (
|
||||
<>
|
||||
<span className="text-destructive">({t('projectCard.chaptersError', { count: chaptersError })})</span>
|
||||
<Button size="sm" variant="outline" className="h-5 text-[10px] px-1.5 text-destructive border-destructive/40" onClick={handleParseAll}>
|
||||
<Button size="sm" variant="outline" className="h-5 text-[10px] px-1.5 text-destructive border-destructive/40" onClick={handleRetryFailed}>
|
||||
{t('projectCard.retryFailed')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{chaptersParsing > 0 && totalCount > 0 && (
|
||||
<Button size="sm" variant="ghost" className="h-5 text-[10px] px-1.5 text-destructive" onClick={handleCancelBatch}>
|
||||
{t('projectCard.cancelParsing')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<Progress value={chapterProgress} />
|
||||
</div>
|
||||
)}
|
||||
{totalCount > 0 && doneCount > 0 && (
|
||||
<div className="space-y-1">
|
||||
<div className="text-xs text-muted-foreground flex items-center gap-1">
|
||||
<div className="text-xs text-muted-foreground flex items-center justify-between">
|
||||
<div className="flex items-center gap-1">
|
||||
<span>🎵</span>
|
||||
<span>{t('projectCard.segmentsProgress', { done: doneCount, total: totalCount })}</span>
|
||||
</div>
|
||||
{!chaptersParsing && hasGenerating && (
|
||||
<Button size="sm" variant="ghost" className="h-5 text-[10px] px-1.5 text-destructive" onClick={handleCancelBatch}>
|
||||
{t('projectCard.cancelGenerating')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<Progress value={progress} />
|
||||
</div>
|
||||
)}
|
||||
{chaptersParsing > 0 && (
|
||||
<Button size="sm" variant="outline" className="h-6 text-xs px-2 text-destructive border-destructive/40" onClick={handleCancelBatch}>
|
||||
{chaptersParsing > 0 && !totalCount && (
|
||||
<div className="flex justify-end">
|
||||
<Button size="sm" variant="ghost" className="h-5 text-[10px] px-1.5 text-destructive" onClick={handleCancelBatch}>
|
||||
{t('projectCard.cancelParsing')}
|
||||
</Button>
|
||||
)}
|
||||
{!chaptersParsing && hasGenerating && (
|
||||
<Button size="sm" variant="outline" className="h-6 text-xs px-2 text-destructive border-destructive/40" onClick={handleCancelBatch}>
|
||||
{t('projectCard.cancelGenerating')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between gap-2 pt-1 border-t">
|
||||
<div className="flex items-center gap-1 flex-wrap">
|
||||
{!isActive && status !== 'characters_ready' && (
|
||||
{status === 'pending' && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant={status === 'pending' ? 'default' : 'outline'}
|
||||
className="h-7 text-xs px-2"
|
||||
onClick={handleAnalyze}
|
||||
disabled={loadingAction}
|
||||
>
|
||||
{status === 'pending' ? t('projectCard.analyze') : t('projectCard.reanalyze')}
|
||||
{t('projectCard.analyze')}
|
||||
</Button>
|
||||
)}
|
||||
{status === 'ready' && (
|
||||
@@ -763,13 +786,20 @@ function ProjectCard({ project, onRefresh }: { project: AudiobookProject; onRefr
|
||||
<div className="space-y-3 pt-2 border-t">
|
||||
{detail && detail.characters.length > 0 && (
|
||||
<div className="rounded-lg border border-blue-500/20 bg-blue-500/5 px-3 py-2">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<button
|
||||
className="flex items-center gap-1 text-xs font-medium text-blue-400/80 mb-2 hover:text-blue-300 transition-colors w-full text-left"
|
||||
className="flex items-center gap-1 text-xs font-medium text-blue-400/80 hover:text-blue-300 transition-colors text-left"
|
||||
onClick={() => setCharsCollapsed(v => !v)}
|
||||
>
|
||||
{charsCollapsed ? <ChevronDown className="h-3 w-3" /> : <ChevronUp className="h-3 w-3" />}
|
||||
{t('projectCard.characters.title', { count: detail.characters.length })}
|
||||
</button>
|
||||
{!isActive && status !== 'pending' && (
|
||||
<Button size="sm" variant="ghost" className="h-6 text-xs px-2 text-muted-foreground" onClick={handleAnalyze} disabled={loadingAction}>
|
||||
{t('projectCard.reanalyze')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{!charsCollapsed && <div className={`space-y-1.5 pr-1 ${editingCharId ? '' : 'max-h-72 overflow-y-auto'}`}>
|
||||
{detail.characters.map(char => (
|
||||
<div key={char.id} className="border rounded px-3 py-2">
|
||||
|
||||
Reference in New Issue
Block a user