diff --git a/src/stores/transcriptionStore.ts b/src/stores/transcriptionStore.ts index 469ce0e..bf310d8 100644 --- a/src/stores/transcriptionStore.ts +++ b/src/stores/transcriptionStore.ts @@ -249,11 +249,9 @@ export const useTranscriptionStore = create((set, get) => ({ const { completedSegments, currentSegment, activeSession, wordCount } = get(); if (isFinal) { - // Final segment - move current to completed, clear current - const newCompleted = [...completedSegments]; - if (currentSegment && currentSegment.text.trim()) { - newCompleted.push({ ...currentSegment, isFinal: true }); - } + // Final segment — append the finalized text directly (not currentSegment, which + // may lag behind or duplicate when WhisperLive re-sends the full segments array). + const newCompleted = [...completedSegments, { text, isFinal: true, ...metadata }]; const newWordCount = newCompleted.reduce( (sum, seg) => sum + seg.text.trim().split(/\s+/).filter(Boolean).length, 0 diff --git a/src/utils/tldraw/cc-base/cc-transcription/transcriptionService.tsx b/src/utils/tldraw/cc-base/cc-transcription/transcriptionService.tsx index 7e27112..a46d78e 100644 --- a/src/utils/tldraw/cc-base/cc-transcription/transcriptionService.tsx +++ b/src/utils/tldraw/cc-base/cc-transcription/transcriptionService.tsx @@ -14,6 +14,7 @@ export class TranscriptionService { private mediaStreamSource: MediaStreamAudioSourceNode | null = null; private workletNode: AudioWorkletNode | null = null; private selectedDeviceId: string = ''; + private finalizedSegmentCount: number = 0; private onTranscriptionUpdate: ((text: string, isFinal: boolean, metadata: { start: number, end: number }) => void) | null = null; constructor(deviceId: string = '') { @@ -99,39 +100,27 @@ export class TranscriptionService { return; } - if (this.onTranscriptionUpdate && data.segments) { - // Get the last segment which is the current one being updated - const lastSegment = data.segments[data.segments.length - 1]; - - // Process completed segments - let lastCompletedText = ''; - for (let i = 0; i < data.segments.length - 1; i++) { - const segment = data.segments[i]; - // Only send update if this segment is different from the last one - if (segment.text.trim() !== lastCompletedText.trim()) { - this.onTranscriptionUpdate( - segment.text, - segment.completed ?? true, // Server marks completed segments - { - start: parseFloat(segment.start), - end: parseFloat(segment.end) - } - ); - lastCompletedText = segment.text; - } + if (this.onTranscriptionUpdate && data.segments && data.segments.length > 0) { + const segments = data.segments; + const lastIdx = segments.length - 1; + + // Only emit segments we have not finalized yet — avoids re-processing the + // full array on every message (which caused the "stuck last segment" bug). + for (let i = this.finalizedSegmentCount; i < lastIdx; i++) { + const seg = segments[i]; + this.onTranscriptionUpdate(seg.text, true, { + start: parseFloat(seg.start), + end: parseFloat(seg.end), + }); + this.finalizedSegmentCount = i + 1; } - // Update the current (incomplete) segment only if it's different from the last completed one - if (lastSegment && lastSegment.text.trim() !== lastCompletedText.trim()) { - this.onTranscriptionUpdate( - lastSegment.text, - lastSegment.completed ?? false, // Last segment is typically incomplete unless marked otherwise - { - start: parseFloat(lastSegment.start), - end: parseFloat(lastSegment.end) - } - ); - } + // Always update the live (last) segment + const lastSeg = segments[lastIdx]; + this.onTranscriptionUpdate(lastSeg.text, lastSeg.completed ?? false, { + start: parseFloat(lastSeg.start), + end: parseFloat(lastSeg.end), + }); } }; } catch (error) { @@ -190,6 +179,7 @@ export class TranscriptionService { } private cleanup() { + this.finalizedSegmentCount = 0; if (this.workletNode) { this.workletNode.disconnect(); this.workletNode = null; diff --git a/src/utils/tldraw/ui-overrides/components/shared/CCTranscriptionPanel.tsx b/src/utils/tldraw/ui-overrides/components/shared/CCTranscriptionPanel.tsx index c846cc0..6cd8ef2 100644 --- a/src/utils/tldraw/ui-overrides/components/shared/CCTranscriptionPanel.tsx +++ b/src/utils/tldraw/ui-overrides/components/shared/CCTranscriptionPanel.tsx @@ -365,7 +365,7 @@ export const CCTranscriptionPanel: React.FC = () => { fontSize: "14px", fontWeight: 600, color: "#fff", - backgroundColor: isRecording ? "#ef4444" : "var(--color-text)", + backgroundColor: isRecording ? "#ef4444" : "#2563eb", transition: "background-color 200ms ease", }} > @@ -538,7 +538,7 @@ export const CCTranscriptionPanel: React.FC = () => { key={"completed-" + i} style={{ padding: "8px 10px", - backgroundColor: "#fff", + backgroundColor: "var(--color-muted)", borderRadius: "4px", border: "1px solid var(--color-divider)", fontSize: "13px", @@ -614,7 +614,7 @@ export const CCTranscriptionPanel: React.FC = () => { key={session.id} style={{ padding: "10px", - backgroundColor: "#fff", + backgroundColor: "var(--color-muted)", borderRadius: "4px", border: "1px solid var(--color-divider)", }} @@ -694,7 +694,7 @@ export const CCTranscriptionPanel: React.FC = () => { padding: "6px 10px", border: "none", borderRadius: "4px", - backgroundColor: newKeyword.trim() ? "var(--color-text)" : "var(--color-divider)", + backgroundColor: newKeyword.trim() ? "#2563eb" : "var(--color-divider)", color: "#fff", cursor: newKeyword.trim() ? "pointer" : "not-allowed", fontSize: "13px", @@ -722,7 +722,7 @@ export const CCTranscriptionPanel: React.FC = () => { alignItems: "center", justifyContent: "space-between", padding: "6px 10px", - backgroundColor: "#fff", + backgroundColor: "var(--color-muted)", borderRadius: "4px", border: "1px solid var(--color-divider)", }}