From 8d699e3bf632f544d6d30a032daacdb80eb3985c Mon Sep 17 00:00:00 2001 From: Ivan Corsetti Date: Mon, 27 Jul 2026 08:31:51 +0200 Subject: [PATCH] fix(export): disambiguate export filenames with generation id (#956) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Export filenames were derived from only the first 30 characters of the generation text. Generations with similar wording (a common workflow when iterating on the same line) produced identical filenames, so exports collided on disk — the browser appended " (1)"/" (2)" and users ended up opening audio that didn't match the expected filename. Append the first 8 chars of the generation id to the .wav and .voicebox.zip export filenames, in both the backend Content-Disposition headers and the frontend save-file hooks. Co-authored-by: Claude Opus 4.8 --- app/src/lib/hooks/useHistory.ts | 12 ++++++++---- backend/routes/history.py | 8 ++++++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/app/src/lib/hooks/useHistory.ts b/app/src/lib/hooks/useHistory.ts index e6f4aa7a..51983339 100644 --- a/app/src/lib/hooks/useHistory.ts +++ b/app/src/lib/hooks/useHistory.ts @@ -47,12 +47,14 @@ export function useExportGeneration() { mutationFn: async ({ generationId, text }: { generationId: string; text: string }) => { const blob = await apiClient.exportGeneration(generationId); - // Create safe filename from text + // Create safe filename from text. Append a short id so exports of + // similarly-worded generations don't collide on the same filename + // (the first 30 chars are frequently identical). const safeText = text .substring(0, 30) .replace(/[^a-z0-9]/gi, '-') .toLowerCase(); - const filename = `generation-${safeText}.voicebox.zip`; + const filename = `generation-${safeText}-${generationId.substring(0, 8)}.voicebox.zip`; await platform.filesystem.saveFile(filename, blob, [ { @@ -73,12 +75,14 @@ export function useExportGenerationAudio() { mutationFn: async ({ generationId, text }: { generationId: string; text: string }) => { const blob = await apiClient.exportGenerationAudio(generationId); - // Create safe filename from text + // Create safe filename from text. Append a short id so exports of + // similarly-worded generations don't collide on the same filename + // (the first 30 chars are frequently identical). const safeText = text .substring(0, 30) .replace(/[^a-z0-9]/gi, '-') .toLowerCase(); - const filename = `${safeText}.wav`; + const filename = `${safeText}-${generationId.substring(0, 8)}.wav`; await platform.filesystem.saveFile(filename, blob, [ { diff --git a/backend/routes/history.py b/backend/routes/history.py index d5aa0533..32b7a542 100644 --- a/backend/routes/history.py +++ b/backend/routes/history.py @@ -151,7 +151,9 @@ async def export_generation( safe_text = "".join(c for c in generation.text[:30] if c.isalnum() or c in (" ", "-", "_")).strip() if not safe_text: safe_text = "generation" - filename = f"generation-{safe_text}.voicebox.zip" + # Append a short id so exports of similarly-worded generations don't collide + # on the same filename (the first 30 chars are frequently identical). + filename = f"generation-{safe_text}-{generation_id[:8]}.voicebox.zip" return StreamingResponse( io.BytesIO(zip_bytes), @@ -180,7 +182,9 @@ async def export_generation_audio( safe_text = "".join(c for c in generation.text[:30] if c.isalnum() or c in (" ", "-", "_")).strip() if not safe_text: safe_text = "generation" - filename = f"{safe_text}.wav" + # Append a short id so exports of similarly-worded generations don't collide + # on the same filename (the first 30 chars are frequently identical). + filename = f"{safe_text}-{generation_id[:8]}.wav" return FileResponse( audio_path,