feat(audiobook): refactor background tasks to use asyncio for project analysis and generation
This commit is contained in:
@@ -1,8 +1,9 @@
|
|||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, UploadFile, File, Form, status
|
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, status
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
@@ -152,7 +153,6 @@ async def get_project(
|
|||||||
@router.post("/projects/{project_id}/analyze")
|
@router.post("/projects/{project_id}/analyze")
|
||||||
async def analyze_project(
|
async def analyze_project(
|
||||||
project_id: int,
|
project_id: int,
|
||||||
background_tasks: BackgroundTasks,
|
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
@@ -176,14 +176,13 @@ async def analyze_project(
|
|||||||
finally:
|
finally:
|
||||||
async_db.close()
|
async_db.close()
|
||||||
|
|
||||||
background_tasks.add_task(run_analysis)
|
asyncio.create_task(run_analysis())
|
||||||
return {"message": "Analysis started", "project_id": project_id}
|
return {"message": "Analysis started", "project_id": project_id}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/projects/{project_id}/confirm")
|
@router.post("/projects/{project_id}/confirm")
|
||||||
async def confirm_characters(
|
async def confirm_characters(
|
||||||
project_id: int,
|
project_id: int,
|
||||||
background_tasks: BackgroundTasks,
|
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
@@ -207,7 +206,7 @@ async def confirm_characters(
|
|||||||
finally:
|
finally:
|
||||||
async_db.close()
|
async_db.close()
|
||||||
|
|
||||||
background_tasks.add_task(run_parsing)
|
asyncio.create_task(run_parsing())
|
||||||
return {"message": "Chapter parsing started", "project_id": project_id}
|
return {"message": "Chapter parsing started", "project_id": project_id}
|
||||||
|
|
||||||
|
|
||||||
@@ -259,7 +258,6 @@ async def update_character(
|
|||||||
@router.post("/projects/{project_id}/generate")
|
@router.post("/projects/{project_id}/generate")
|
||||||
async def generate_project(
|
async def generate_project(
|
||||||
project_id: int,
|
project_id: int,
|
||||||
background_tasks: BackgroundTasks,
|
|
||||||
data: AudiobookGenerateRequest = AudiobookGenerateRequest(),
|
data: AudiobookGenerateRequest = AudiobookGenerateRequest(),
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
@@ -285,7 +283,7 @@ async def generate_project(
|
|||||||
finally:
|
finally:
|
||||||
async_db.close()
|
async_db.close()
|
||||||
|
|
||||||
background_tasks.add_task(run_generation)
|
asyncio.create_task(run_generation())
|
||||||
msg = f"Generation started for chapter {chapter_index}" if chapter_index is not None else "Generation started"
|
msg = f"Generation started for chapter {chapter_index}" if chapter_index is not None else "Generation started"
|
||||||
return {"message": msg, "project_id": project_id, "chapter_index": chapter_index}
|
return {"message": msg, "project_id": project_id, "chapter_index": chapter_index}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { Book, Plus, Trash2, RefreshCw, Download, ChevronDown, ChevronUp, Play, Square, Pencil, Check, X } from 'lucide-react'
|
import { Book, Plus, Trash2, RefreshCw, Download, ChevronDown, ChevronUp, Play, Square, Pencil, Check, X, Loader2 } from 'lucide-react'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
@@ -227,7 +227,13 @@ function CreateProjectPanel({ onCreated }: { onCreated: () => void }) {
|
|||||||
<Textarea placeholder="粘贴小说文本..." rows={6} value={text} onChange={e => setText(e.target.value)} />
|
<Textarea placeholder="粘贴小说文本..." rows={6} value={text} onChange={e => setText(e.target.value)} />
|
||||||
)}
|
)}
|
||||||
{sourceType === 'epub' && (
|
{sourceType === 'epub' && (
|
||||||
<Input type="file" accept=".epub" onChange={e => setEpubFile(e.target.files?.[0] || null)} />
|
<Input type="file" accept=".epub" onChange={e => {
|
||||||
|
const file = e.target.files?.[0] || null
|
||||||
|
setEpubFile(file)
|
||||||
|
if (file && !title) {
|
||||||
|
setTitle(file.name.replace(/\.epub$/i, ''))
|
||||||
|
}
|
||||||
|
}} />
|
||||||
)}
|
)}
|
||||||
<Button size="sm" onClick={handleCreate} disabled={loading}>{loading ? '创建中...' : '创建项目'}</Button>
|
<Button size="sm" onClick={handleCreate} disabled={loading}>{loading ? '创建中...' : '创建项目'}</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -553,7 +559,10 @@ function ProjectCard({ project, onRefresh }: { project: AudiobookProject; onRefr
|
|||||||
<span className="text-xs text-muted-foreground mx-2 flex-1">{chDone}/{chTotal} 段</span>
|
<span className="text-xs text-muted-foreground mx-2 flex-1">{chDone}/{chTotal} 段</span>
|
||||||
<div className="flex gap-1 shrink-0">
|
<div className="flex gap-1 shrink-0">
|
||||||
{chGenerating ? (
|
{chGenerating ? (
|
||||||
<Badge variant="secondary" className="text-xs">生成中...</Badge>
|
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||||
|
<Loader2 className="h-3 w-3 animate-spin" />
|
||||||
|
<span>生成中</span>
|
||||||
|
</div>
|
||||||
) : chAllDone ? (
|
) : chAllDone ? (
|
||||||
<>
|
<>
|
||||||
<Badge variant="outline" className="text-xs">已完成</Badge>
|
<Badge variant="outline" className="text-xs">已完成</Badge>
|
||||||
@@ -571,7 +580,10 @@ function ProjectCard({ project, onRefresh }: { project: AudiobookProject; onRefr
|
|||||||
disabled={loadingAction}
|
disabled={loadingAction}
|
||||||
onClick={() => handleGenerate(chIdx)}
|
onClick={() => handleGenerate(chIdx)}
|
||||||
>
|
>
|
||||||
生成此章
|
{loadingAction
|
||||||
|
? <Loader2 className="h-3 w-3 animate-spin" />
|
||||||
|
: '生成此章'
|
||||||
|
}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -604,14 +616,18 @@ function ProjectCard({ project, onRefresh }: { project: AudiobookProject; onRefr
|
|||||||
<div className="flex items-start gap-2 text-xs">
|
<div className="flex items-start gap-2 text-xs">
|
||||||
<Badge variant="outline" className="shrink-0 text-xs mt-0.5">{seg.character_name || '?'}</Badge>
|
<Badge variant="outline" className="shrink-0 text-xs mt-0.5">{seg.character_name || '?'}</Badge>
|
||||||
<span className="text-muted-foreground flex-1 min-w-0 break-words leading-relaxed">{seg.text}</span>
|
<span className="text-muted-foreground flex-1 min-w-0 break-words leading-relaxed">{seg.text}</span>
|
||||||
{seg.status !== 'done' && (
|
{seg.status === 'generating' ? (
|
||||||
|
<div className="flex items-center gap-1 text-xs text-muted-foreground shrink-0 mt-0.5">
|
||||||
|
<Loader2 className="h-3 w-3 animate-spin" />
|
||||||
|
</div>
|
||||||
|
) : seg.status !== 'done' ? (
|
||||||
<Badge
|
<Badge
|
||||||
variant={seg.status === 'error' ? 'destructive' : 'secondary'}
|
variant={seg.status === 'error' ? 'destructive' : 'secondary'}
|
||||||
className="shrink-0 text-xs mt-0.5"
|
className="shrink-0 text-xs mt-0.5"
|
||||||
>
|
>
|
||||||
{seg.status === 'generating' ? '生成中' : seg.status === 'error' ? '出错' : '待生成'}
|
{seg.status === 'error' ? '出错' : '待生成'}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
{seg.status === 'done' && (
|
{seg.status === 'done' && (
|
||||||
<AudioPlayer
|
<AudioPlayer
|
||||||
|
|||||||
Reference in New Issue
Block a user