From 1ba935e83ba845558c69025f261c7e50f2736114 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 1cd7694c..694d35be 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,