feat: add character regeneration endpoint and integrate emotion limits for AI-generated scripts

This commit is contained in:
2026-03-13 13:58:01 +08:00
parent 0a12f204ba
commit 16947d6b8b
4 changed files with 128 additions and 16 deletions

View File

@@ -162,6 +162,10 @@ export const audiobookApi = {
await apiClient.post(`/audiobook/projects/${id}/analyze`, { turbo: options?.turbo ?? false })
},
regenerateCharacters: async (id: number): Promise<void> => {
await apiClient.post(`/audiobook/projects/${id}/regenerate-characters`)
},
updateCharacter: async (
projectId: number,
charId: number,

View File

@@ -1397,6 +1397,7 @@ function ChaptersPanel({
onParseAll,
onGenerateAll,
onProcessAll,
isBackgroundGenerating,
onContinueScript,
onDownload,
onSequentialPlayingChange,
@@ -1416,6 +1417,7 @@ function ChaptersPanel({
onParseAll: () => void
onGenerateAll: () => void
onProcessAll: () => void
isBackgroundGenerating: boolean
onContinueScript?: () => void
onDownload: (chapterIndex?: number) => void
onSequentialPlayingChange: (id: number | null) => void
@@ -1520,7 +1522,7 @@ function ChaptersPanel({
}, [segments, detail])
const isAIMode = project.source_type === 'ai_generated'
const hasChapters = detail && detail.chapters.length > 0 && ['ready', 'generating', 'done'].includes(status)
const hasChapters = detail && detail.chapters.length > 0 && ['analyzing', 'ready', 'generating', 'done'].includes(status)
return (
<div className="flex-1 flex flex-col bg-emerald-500/5 overflow-hidden">
@@ -1530,7 +1532,7 @@ function ChaptersPanel({
</span>
{hasChapters && (
<div className="flex items-center gap-1 flex-wrap">
{detail!.chapters.some(c => ['pending', 'error', 'ready'].includes(c.status)) && (
{detail!.chapters.some(c => ['pending', 'error', 'ready'].includes(c.status)) && !isBackgroundGenerating && (
<Button size="xs" variant="outline" disabled={loadingAction} onClick={onParseAll}>
{isAIMode ? <Bot className="h-3 w-3 mr-1" /> : <Wand2 className="h-3 w-3 mr-1" />}
{t(isAIMode ? 'projectCard.chapters.parseAllAI' : 'projectCard.chapters.parseAll')}
@@ -1588,10 +1590,16 @@ function ChaptersPanel({
<span className="text-xs font-medium flex-1 truncate">{chTitle}</span>
<span className="shrink-0 flex items-center gap-1.5" onClick={e => e.stopPropagation()}>
{ch.status === 'pending' && (
<Button size="xs" variant="outline" onClick={() => onParseChapter(ch.id, ch.title)}>
{isAIMode ? <Bot className="h-3 w-3 mr-1" /> : <Wand2 className="h-3 w-3 mr-1" />}
{t(isAIMode ? 'projectCard.chapters.parseAI' : 'projectCard.chapters.parse')}
</Button>
isBackgroundGenerating ? (
<span className="flex items-center gap-1 text-[11px] text-muted-foreground">
<Loader2 className="h-3 w-3 animate-spin" />
</span>
) : (
<Button size="xs" variant="outline" onClick={() => onParseChapter(ch.id, ch.title)}>
{isAIMode ? <Bot className="h-3 w-3 mr-1" /> : <Wand2 className="h-3 w-3 mr-1" />}
{t(isAIMode ? 'projectCard.chapters.parseAI' : 'projectCard.chapters.parse')}
</Button>
)
)}
{ch.status === 'parsing' && (
<span className="flex items-center gap-1 text-[11px] text-muted-foreground">
@@ -1865,13 +1873,17 @@ export default function Audiobook() {
}, [status, selectedProject, t])
const hasParsingChapter = detail?.chapters.some(c => c.status === 'parsing') ?? false
const isAiProject = selectedProject?.source_type === 'ai_generated'
const hasPendingChapters = detail?.chapters.some(c => c.status === 'pending') ?? false
const isBackgroundGenerating = isAiProject && (status === 'analyzing' || (status === 'ready' && hasPendingChapters))
useEffect(() => {
if (!isPolling) return
if (['analyzing', 'generating'].includes(status)) return
if (hasParsingChapter) return
if (isAiProject && hasPendingChapters) return
if (!segments.some(s => s.status === 'generating')) setIsPolling(false)
}, [isPolling, status, segments, hasParsingChapter])
}, [isPolling, status, segments, hasParsingChapter, isAiProject, hasPendingChapters])
useEffect(() => {
if (generatingChapterIndices.size === 0) return
@@ -1891,7 +1903,7 @@ export default function Audiobook() {
}
}, [segments, generatingChapterIndices])
const shouldPoll = isPolling || ['analyzing', 'generating'].includes(status) || hasParsingChapter || generatingChapterIndices.size > 0 || segments.some(s => s.status === 'generating')
const shouldPoll = isPolling || ['analyzing', 'generating'].includes(status) || hasParsingChapter || (isAiProject && hasPendingChapters) || generatingChapterIndices.size > 0 || segments.some(s => s.status === 'generating')
useEffect(() => {
if (!shouldPoll || !selectedProjectId) return
@@ -1908,7 +1920,11 @@ export default function Audiobook() {
setLoadingAction(true)
setIsPolling(true)
try {
await audiobookApi.analyze(selectedProject.id, { turbo: true })
if (selectedProject.source_type === 'ai_generated') {
await audiobookApi.regenerateCharacters(selectedProject.id)
} else {
await audiobookApi.analyze(selectedProject.id, { turbo: true })
}
toast.success(t('projectCard.analyzeStarted'))
fetchProjects()
} catch (e: any) {
@@ -1924,6 +1940,7 @@ export default function Audiobook() {
setLoadingAction(true)
try {
await audiobookApi.confirmCharacters(selectedProject.id)
if (selectedProject.source_type === 'ai_generated') setIsPolling(true)
toast.success(t('projectCard.confirm.chaptersRecognized'))
fetchProjects()
fetchDetail()
@@ -2253,17 +2270,22 @@ export default function Audiobook() {
</div>
)}
{chaptersTotal > 0 && ['ready', 'generating', 'done'].includes(status) && (chaptersParsing > 0 || chaptersError > 0 || (totalCount > 0 && doneCount > 0)) && (
{chaptersTotal > 0 && (isBackgroundGenerating || ['ready', 'generating', 'done'].includes(status)) && (isBackgroundGenerating || chaptersParsing > 0 || chaptersError > 0 || (totalCount > 0 && doneCount > 0)) && (
<div className="shrink-0 mx-4 mt-2 space-y-2">
{(chaptersParsing > 0 || chaptersError > 0 || chaptersParsed < chaptersTotal) && (
{(isBackgroundGenerating || chaptersParsing > 0 || chaptersError > 0 || chaptersParsed < chaptersTotal) && (
<div className="space-y-1">
<div className="text-xs text-muted-foreground flex items-center justify-between">
<div className="flex items-center gap-1 flex-wrap">
<span>📝</span>
{isBackgroundGenerating && chaptersParsing === 0 && !chaptersError
? <Loader2 className="h-3 w-3 animate-spin" />
: <span>📝</span>}
<span>{t('projectCard.chaptersProgress', { parsed: chaptersParsed, total: chaptersTotal })}</span>
{chaptersParsing > 0 && (
<span className="text-primary">({t('projectCard.chaptersParsing', { count: chaptersParsing })})</span>
)}
{isBackgroundGenerating && hasPendingChapters && chaptersParsing > 0 && (
<span className="text-muted-foreground">({detail!.chapters.filter(c => c.status === 'pending').length} )</span>
)}
{chaptersError > 0 && (
<>
<span className="text-destructive">({t('projectCard.chaptersError', { count: chaptersError })})</span>
@@ -2273,7 +2295,7 @@ export default function Audiobook() {
</>
)}
</div>
{chaptersParsing > 0 && totalCount > 0 && (
{(isBackgroundGenerating || chaptersParsing > 0) && (
<Button size="xs" variant="ghost" className="text-destructive" onClick={handleCancelBatch}>
{t('projectCard.cancelParsing')}
</Button>
@@ -2333,6 +2355,7 @@ export default function Audiobook() {
onParseAll={handleParseAll}
onGenerateAll={handleGenerateAll}
onProcessAll={handleProcessAll}
isBackgroundGenerating={isBackgroundGenerating}
onContinueScript={selectedProject.source_type === 'ai_generated' ? () => setShowContinueScript(true) : undefined}
onDownload={handleDownload}
onSequentialPlayingChange={setSequentialPlayingId}