feat(audiobook): change audio format from MP3 to WAV for project downloads and merging

This commit is contained in:
2026-03-10 17:56:46 +08:00
parent 006aa0c85f
commit bf7c73e57c
3 changed files with 24 additions and 14 deletions

View File

@@ -461,19 +461,19 @@ async def download_project(
if chapter is not None: if chapter is not None:
output_path = str( output_path = str(
Path(settings.OUTPUT_DIR) / "audiobook" / str(project_id) / "chapters" / f"chapter_{chapter}.mp3" Path(settings.OUTPUT_DIR) / "audiobook" / str(project_id) / "chapters" / f"chapter_{chapter}.wav"
) )
else: else:
output_path = str( output_path = str(
Path(settings.OUTPUT_DIR) / "audiobook" / str(project_id) / "full.mp3" Path(settings.OUTPUT_DIR) / "audiobook" / str(project_id) / "full.wav"
) )
if not Path(output_path).exists(): if not Path(output_path).exists():
from core.audiobook_service import merge_audio_files from core.audiobook_service import merge_audio_files
merge_audio_files(audio_paths, output_path) merge_audio_files(audio_paths, output_path)
filename = f"chapter_{chapter}.mp3" if chapter is not None else f"{project.title}.mp3" filename = f"chapter_{chapter}.wav" if chapter is not None else f"{project.title}.wav"
return FileResponse(output_path, media_type="audio/mpeg", filename=filename) return FileResponse(output_path, media_type="audio/wav", filename=filename)
@router.delete("/projects/{project_id}", status_code=status.HTTP_204_NO_CONTENT) @router.delete("/projects/{project_id}", status_code=status.HTTP_204_NO_CONTENT)

View File

@@ -542,4 +542,4 @@ def merge_audio_files(audio_paths: list[str], output_path: str) -> None:
if combined: if combined:
Path(output_path).parent.mkdir(parents=True, exist_ok=True) Path(output_path).parent.mkdir(parents=True, exist_ok=True)
combined.export(output_path, format="mp3") combined.export(output_path, format="wav")

View File

@@ -321,6 +321,8 @@ function ProjectCard({ project, onRefresh }: { project: AudiobookProject; onRefr
const [editFields, setEditFields] = useState({ name: '', description: '', instruct: '' }) const [editFields, setEditFields] = useState({ name: '', description: '', instruct: '' })
const [sequentialPlayingId, setSequentialPlayingId] = useState<number | null>(null) const [sequentialPlayingId, setSequentialPlayingId] = useState<number | null>(null)
const [turbo, setTurbo] = useState(false) const [turbo, setTurbo] = useState(false)
const [charsCollapsed, setCharsCollapsed] = useState(false)
const [chaptersCollapsed, setChaptersCollapsed] = useState(false)
const prevStatusRef = useRef(project.status) const prevStatusRef = useRef(project.status)
const autoExpandedRef = useRef(new Set<string>()) const autoExpandedRef = useRef(new Set<string>())
@@ -589,10 +591,14 @@ function ProjectCard({ project, onRefresh }: { project: AudiobookProject; onRefr
<div className="space-y-4 pt-2 border-t"> <div className="space-y-4 pt-2 border-t">
{detail && detail.characters.length > 0 && ( {detail && detail.characters.length > 0 && (
<div> <div>
<div className="text-xs font-medium text-muted-foreground mb-2"> <button
className="flex items-center gap-1 text-xs font-medium text-muted-foreground mb-2 hover:text-foreground transition-colors w-full text-left"
onClick={() => setCharsCollapsed(v => !v)}
>
{charsCollapsed ? <ChevronDown className="h-3 w-3" /> : <ChevronUp className="h-3 w-3" />}
{detail.characters.length} {detail.characters.length}
</div> </button>
<div className="space-y-1.5"> {!charsCollapsed && <div className="space-y-1.5 max-h-72 overflow-y-auto pr-1">
{detail.characters.map(char => ( {detail.characters.map(char => (
<div key={char.id} className="border rounded px-3 py-2"> <div key={char.id} className="border rounded px-3 py-2">
{editingCharId === char.id ? ( {editingCharId === char.id ? (
@@ -643,7 +649,7 @@ function ProjectCard({ project, onRefresh }: { project: AudiobookProject; onRefr
)} )}
</div> </div>
))} ))}
</div> </div>}
{status === 'characters_ready' && ( {status === 'characters_ready' && (
<Button <Button
className="w-full mt-3" className="w-full mt-3"
@@ -659,9 +665,13 @@ function ProjectCard({ project, onRefresh }: { project: AudiobookProject; onRefr
{detail && detail.chapters.length > 0 && ['ready', 'generating', 'done'].includes(status) && ( {detail && detail.chapters.length > 0 && ['ready', 'generating', 'done'].includes(status) && (
<div> <div>
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<div className="text-xs font-medium text-muted-foreground"> <button
className="flex items-center gap-1 text-xs font-medium text-muted-foreground hover:text-foreground transition-colors text-left"
onClick={() => setChaptersCollapsed(v => !v)}
>
{chaptersCollapsed ? <ChevronDown className="h-3 w-3" /> : <ChevronUp className="h-3 w-3" />}
{detail.chapters.length} {detail.chapters.length}
</div> </button>
{detail.chapters.some(c => ['pending', 'error', 'ready'].includes(c.status)) && ( {detail.chapters.some(c => ['pending', 'error', 'ready'].includes(c.status)) && (
<Button <Button
size="sm" size="sm"
@@ -673,7 +683,7 @@ function ProjectCard({ project, onRefresh }: { project: AudiobookProject; onRefr
</Button> </Button>
)} )}
</div> </div>
<div className="space-y-2"> {!chaptersCollapsed && <div className="space-y-2 max-h-96 overflow-y-auto pr-1">
{detail.chapters.map(ch => { {detail.chapters.map(ch => {
const chSegs = segments.filter(s => s.chapter_index === ch.chapter_index) const chSegs = segments.filter(s => s.chapter_index === ch.chapter_index)
const chDone = chSegs.filter(s => s.status === 'done').length const chDone = chSegs.filter(s => s.status === 'done').length
@@ -729,8 +739,8 @@ function ProjectCard({ project, onRefresh }: { project: AudiobookProject; onRefr
</div> </div>
) )
})} })}
</div> </div>}
{doneCount > 0 && ( {!chaptersCollapsed && doneCount > 0 && (
<div className="mt-2"> <div className="mt-2">
<SequentialPlayer segments={segments} projectId={project.id} onPlayingChange={setSequentialPlayingId} /> <SequentialPlayer segments={segments} projectId={project.id} onPlayingChange={setSequentialPlayingId} />
</div> </div>