feat: add character regeneration endpoint and integrate emotion limits for AI-generated scripts
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user