feat: update emotion handling in audiobook segments and UI for multi-emotion selection

This commit is contained in:
2026-03-13 15:14:49 +08:00
parent 161e7fa76d
commit bf1532200a
4 changed files with 131 additions and 60 deletions

View File

@@ -1428,8 +1428,8 @@ function ChaptersPanel({
const [expandedChapters, setExpandedChapters] = useState<Set<number>>(new Set())
const [editingSegId, setEditingSegId] = useState<number | null>(null)
const [editText, setEditText] = useState('')
const [editEmoText, setEditEmoText] = useState('')
const [editEmoAlpha, setEditEmoAlpha] = useState(0.5)
const [editEmoSelections, setEditEmoSelections] = useState<string[]>([])
const [editEmoWeights, setEditEmoWeights] = useState<Record<string, number>>({})
const [savingSegId, setSavingSegId] = useState<number | null>(null)
const [regeneratingSegs, setRegeneratingSegs] = useState<Set<number>>(new Set())
const [audioVersions, setAudioVersions] = useState<Record<number, number>>({})
@@ -1463,8 +1463,30 @@ function ChaptersPanel({
const startEdit = (seg: AudiobookSegment) => {
setEditingSegId(seg.id)
setEditText(seg.text)
setEditEmoText(seg.emo_text || '')
setEditEmoAlpha(seg.emo_alpha ?? 0.5)
const rawEmo = seg.emo_text || ''
const alpha = seg.emo_alpha ?? 0.5
if (!rawEmo) {
setEditEmoSelections([])
setEditEmoWeights({})
return
}
const tokens = rawEmo.split('+').filter(Boolean)
const selections: string[] = []
const weights: Record<string, number> = {}
if (tokens.length === 1) {
const [name] = tokens[0].split(':')
selections.push(name.trim())
weights[name.trim()] = alpha
} else {
for (const tok of tokens) {
const [name, w] = tok.split(':')
const emo = name.trim()
selections.push(emo)
weights[emo] = w ? parseFloat(w) : parseFloat((0.5 * alpha).toFixed(2))
}
}
setEditEmoSelections(selections)
setEditEmoWeights(weights)
}
const cancelEdit = () => setEditingSegId(null)
@@ -1472,11 +1494,16 @@ function ChaptersPanel({
const saveEdit = async (segId: number) => {
setSavingSegId(segId)
try {
await onUpdateSegment(segId, {
text: editText,
emo_text: editEmoText || null,
emo_alpha: editEmoText ? editEmoAlpha : null,
})
let emo_text: string | null = null
let emo_alpha: number | null = null
if (editEmoSelections.length === 1) {
emo_text = editEmoSelections[0]
emo_alpha = editEmoWeights[editEmoSelections[0]] ?? 0.5
} else if (editEmoSelections.length > 1) {
emo_text = editEmoSelections.map(e => `${e}:${(editEmoWeights[e] ?? 0.5).toFixed(2)}`).join('+')
emo_alpha = 1.0
}
await onUpdateSegment(segId, { text: editText, emo_text, emo_alpha })
setEditingSegId(null)
} finally {
setSavingSegId(null)
@@ -1673,11 +1700,16 @@ function ChaptersPanel({
</Badge>
{!isEditing && seg.emo_text && (
<span className="text-[11px] text-muted-foreground shrink-0 flex items-center gap-0.5 flex-wrap">
{seg.emo_text.split('+').map(e => (
<span key={e} className="bg-muted rounded px-1">{e.trim()}</span>
))}
{seg.emo_alpha != null && (
<span className="opacity-60 ml-0.5">{seg.emo_alpha.toFixed(2)}</span>
{seg.emo_text.split('+').map(tok => {
const [name, w] = tok.split(':')
return (
<span key={tok} className="bg-muted rounded px-1">
{name.trim()}{w && <span className="opacity-60">:{parseFloat(w).toFixed(2)}</span>}
</span>
)
})}
{seg.emo_alpha != null && seg.emo_alpha !== 1 && (
<span className="opacity-60 ml-0.5">×{seg.emo_alpha.toFixed(2)}</span>
)}
</span>
)}
@@ -1722,19 +1754,19 @@ function ChaptersPanel({
<div className="flex items-center gap-1 flex-wrap">
<span className="text-xs text-muted-foreground shrink-0">{t('projectCard.segments.emotion')}:</span>
{EMOTION_OPTIONS.map(emo => {
const selectedEmos = editEmoText.split('+').filter(Boolean)
const isSelected = selectedEmos.includes(emo)
const isSelected = editEmoSelections.includes(emo)
return (
<button
key={emo}
type="button"
className={`px-2 py-0.5 rounded text-xs border transition-colors ${isSelected ? "bg-primary text-primary-foreground border-primary" : "bg-muted text-muted-foreground border-transparent"}`}
onClick={() => {
const current = editEmoText.split('+').filter(Boolean)
const next = isSelected
? current.filter(e => e !== emo)
: [...current, emo]
setEditEmoText(next.join('+'))
if (isSelected) {
setEditEmoSelections(prev => prev.filter(e => e !== emo))
} else {
setEditEmoSelections(prev => [...prev, emo])
setEditEmoWeights(prev => ({ ...prev, [emo]: prev[emo] ?? 0.5 }))
}
}}
>
{emo}
@@ -1742,21 +1774,21 @@ function ChaptersPanel({
)
})}
</div>
{editEmoText && (
<div className="flex items-center gap-1.5">
<span className="text-xs text-muted-foreground shrink-0">{t('projectCard.segments.intensity')}:</span>
{editEmoSelections.map(emo => (
<div key={emo} className="flex items-center gap-1.5">
<span className="text-xs text-muted-foreground w-8 shrink-0">{emo}:</span>
<input
type="range"
min={0.05}
max={0.9}
step={0.05}
value={editEmoAlpha}
onChange={e => setEditEmoAlpha(Number(e.target.value))}
value={editEmoWeights[emo] ?? 0.5}
onChange={e => setEditEmoWeights(prev => ({ ...prev, [emo]: Number(e.target.value) }))}
className="flex-1 h-1.5 accent-primary"
/>
<span className="text-xs text-muted-foreground w-8 text-right">{editEmoAlpha.toFixed(2)}</span>
<span className="text-xs text-muted-foreground w-8 text-right">{(editEmoWeights[emo] ?? 0.5).toFixed(2)}</span>
</div>
)}
))}
</div>
</div>
) : (