feat(audiobook): change audio format from MP3 to WAV for project downloads and merging
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user