From dd5e1999244338072132e8963b69d17ff61190b6 Mon Sep 17 00:00:00 2001 From: Chad Simon Date: Sun, 28 Jun 2026 10:24:53 -0400 Subject: [PATCH] feat(progress): live transcription progress + auto-refresh on the note page The detail page never polled, so it spun on "this may take a few minutes" until a manual reload even though transcription finishes in seconds. Now it polls while processing and updates itself, shows an elapsed timer + an optimistic progress bar, and accurate copy. Persist clip duration before the transcription call so the UI can show recording length while it works. Deepgram batch gives no true %, so the bar is an elapsed-time ramp, not a false percentage. Claude-Session: https://claude.ai/code/session_01BzkXTG6PfbUdFoif8kBeyw --- backend/apps/voice_notes/tasks.py | 3 ++ frontend/src/pages/NoteDetailPage.tsx | 56 +++++++++++++++++++++++---- 2 files changed, 51 insertions(+), 8 deletions(-) diff --git a/backend/apps/voice_notes/tasks.py b/backend/apps/voice_notes/tasks.py index ba9ec17..97a3163 100644 --- a/backend/apps/voice_notes/tasks.py +++ b/backend/apps/voice_notes/tasks.py @@ -26,6 +26,9 @@ def transcribe_voice_note_task(self, note_id: int, language: str = 'auto') -> No duration = AudioProcessingService.get_audio_duration(note.audio_file) if duration: note.duration = timedelta(seconds=duration) + # Persist duration before the (slow) transcription call so the UI can + # show the clip length and a meaningful estimate while it processes. + note.save(update_fields=['duration']) result = transcription_service.transcribe_audio(note.audio_file, language) diff --git a/frontend/src/pages/NoteDetailPage.tsx b/frontend/src/pages/NoteDetailPage.tsx index d3ff20e..a9a91ba 100644 --- a/frontend/src/pages/NoteDetailPage.tsx +++ b/frontend/src/pages/NoteDetailPage.tsx @@ -133,12 +133,26 @@ const NoteDetailPage: React.FC = () => { () => voiceNotesAPI.get(noteId), { enabled: !!noteId, + // Poll while transcription is in flight so the page updates itself; stop + // once it reaches a terminal state. + refetchInterval: (data) => (data?.data?.status === 'processing' ? 2500 : false), onError: () => { toast.error('Failed to load voice note'); }, } ); + // Elapsed-time ticker for the processing indicator. + const isProcessing = noteData?.data?.status === 'processing'; + const [elapsed, setElapsed] = useState(0); + useEffect(() => { + if (!isProcessing) return; + const start = Date.now(); + setElapsed(0); + const timer = setInterval(() => setElapsed((Date.now() - start) / 1000), 500); + return () => clearInterval(timer); + }, [isProcessing]); + const { data: foldersData } = useQuery(['folders'], () => foldersAPI.list()); const folders = foldersData?.data || []; @@ -349,14 +363,40 @@ const NoteDetailPage: React.FC = () => { )} - {note.status === 'processing' && ( -
- - - Transcribing your audio... This may take a few minutes. - -
- )} + {note.status === 'processing' && (() => { + // Optimistic progress: ramps smoothly toward 90% and completes when + // the poll reports done. Deepgram's batch API gives no true %, so this + // conveys activity + elapsed time without claiming a false figure. + const progress = Math.min(90, 90 * (1 - Math.exp(-elapsed / 8))); + const clip = note.duration ? formatTimestamp(parseDurationToSeconds(note.duration)) : null; + return ( +
+
+ Transcribing your audio… + + {formatTimestamp(elapsed)} elapsed + +
+
+
+
+

+ Transcribing speech and identifying speakers{clip ? ` in your ${clip} recording` : ''}. + Short clips finish in a few seconds; longer recordings take a bit more. +

+
+ ); + })()} {note.status === 'failed' && (