fix: transcription segment dedup, store finalisation, and panel colours

- transcriptionService: track finalizedSegmentCount so only newly-final
  segments are emitted per WS message (was re-processing full array each
  time, causing the live segment to freeze in the completed list)
- transcriptionStore: saveSegment isFinal branch now appends the passed
  text directly instead of currentSegment (currentSegment was stale
  relative to the incoming final)
- CCTranscriptionPanel: record button colour changed from var(--color-text)
  to explicit #2563eb so it is visible in dark mode; completed segment
  backgrounds changed from hardcoded #fff to var(--color-muted) so text
  is readable in both themes; keyword Add button gets same blue fix

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
kcar 2026-05-23 07:08:53 +00:00
parent 0345258247
commit 308889937c
3 changed files with 29 additions and 41 deletions

View File

@ -249,11 +249,9 @@ export const useTranscriptionStore = create<TranscriptionState>((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

View File

@ -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;

View File

@ -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)",
}}