Download shared/realtime-voice-protocol.ts from taqarub/node-api: direct link, hf CLI and curl.
- Browser
- Download file 6.28 kB
-
https://huggingface.co/spaces/taqarub/node-api/resolve/main/shared/realtime-voice-protocol.ts
- Command line
-
hf download hf://spaces/taqarub/node-api/shared/realtime-voice-protocol.ts
-
curl -L -o realtime-voice-protocol.ts https://huggingface.co/spaces/taqarub/node-api/resolve/main/shared/realtime-voice-protocol.ts
6.28 kB
| export interface BufferedAudioFrame { | |
| audioSequence: number; | |
| audio: string; | |
| } | |
| /** | |
| * Bounded replay buffer. Frames remain here until the server acknowledges | |
| * them, which makes reconnect replay deterministic and idempotent. | |
| */ | |
| export class AudioReplayBuffer { | |
| private frames: BufferedAudioFrame[] = []; | |
| constructor(private readonly capacity: number) { | |
| if (!Number.isInteger(capacity) || capacity < 1) { | |
| throw new Error("AudioReplayBuffer capacity must be a positive integer"); | |
| } | |
| } | |
| push(frame: BufferedAudioFrame): BufferedAudioFrame | null { | |
| if ( | |
| this.frames.some( | |
| (existing) => existing.audioSequence === frame.audioSequence, | |
| ) | |
| ) { | |
| return null; | |
| } | |
| this.frames.push(frame); | |
| this.frames.sort((a, b) => a.audioSequence - b.audioSequence); | |
| return this.frames.length > this.capacity ? this.frames.shift()! : null; | |
| } | |
| acknowledge(audioSequence: number): void { | |
| this.frames = this.frames.filter( | |
| (frame) => frame.audioSequence > audioSequence, | |
| ); | |
| } | |
| unacknowledged(): BufferedAudioFrame[] { | |
| return [...this.frames]; | |
| } | |
| firstSequence(): number | null { | |
| return this.frames[0]?.audioSequence ?? null; | |
| } | |
| clear(): void { | |
| this.frames = []; | |
| } | |
| get size(): number { | |
| return this.frames.length; | |
| } | |
| } | |
| /** | |
| * Turns potentially duplicated or reordered frames back into one contiguous | |
| * stream. WebSocket normally preserves order, but reconnect replay can overlap | |
| * with frames already accepted by the server. | |
| */ | |
| export class OrderedAudioFrames { | |
| private pending = new Map<number, BufferedAudioFrame>(); | |
| constructor( | |
| private nextSequence: number, | |
| private readonly capacity = 600, | |
| ) {} | |
| receive(frame: BufferedAudioFrame): { | |
| duplicate: boolean; | |
| ready: BufferedAudioFrame[]; | |
| } { | |
| if ( | |
| frame.audioSequence < this.nextSequence || | |
| this.pending.has(frame.audioSequence) | |
| ) { | |
| return { duplicate: true, ready: [] }; | |
| } | |
| this.pending.set(frame.audioSequence, frame); | |
| if (this.pending.size > this.capacity) { | |
| const highest = Math.max(...this.pending.keys()); | |
| this.pending.delete(highest); | |
| } | |
| const ready: BufferedAudioFrame[] = []; | |
| while (this.pending.has(this.nextSequence)) { | |
| ready.push(this.pending.get(this.nextSequence)!); | |
| this.pending.delete(this.nextSequence); | |
| this.nextSequence++; | |
| } | |
| return { duplicate: false, ready }; | |
| } | |
| expectedSequence(): number { | |
| return this.nextSequence; | |
| } | |
| } | |
| export interface RealtimeTranscriptEvent { | |
| sessionId: string; | |
| utteranceId: string; | |
| utteranceIndex: number; | |
| revision: number; | |
| text: string; | |
| } | |
| export interface TranscriptUpdate { | |
| accepted: boolean; | |
| confirmedText: string; | |
| interimText: string; | |
| appendedFinals: string[]; | |
| } | |
| /** | |
| * Applies recognizer revisions while keeping exactly one replaceable interim | |
| * hypothesis. Finals are buffered until every earlier utterance is present. | |
| */ | |
| export class TranscriptAccumulator { | |
| private latestRevision = new Map<string, number>(); | |
| private finalized = new Set<string>(); | |
| private pendingFinals = new Map<number, RealtimeTranscriptEvent>(); | |
| private nextFinalIndex = 0; | |
| private activeInterim: RealtimeTranscriptEvent | null = null; | |
| private confirmedParts: string[] = []; | |
| private sessionId: string | null = null; | |
| reset(sessionId?: string): void { | |
| this.latestRevision.clear(); | |
| this.finalized.clear(); | |
| this.pendingFinals.clear(); | |
| this.nextFinalIndex = 0; | |
| this.activeInterim = null; | |
| this.confirmedParts = []; | |
| this.sessionId = sessionId ?? null; | |
| } | |
| /** Start a replacement audio session without discarding confirmed text. */ | |
| continueSession(sessionId?: string): void { | |
| const confirmedParts = [...this.confirmedParts]; | |
| this.reset(sessionId); | |
| this.confirmedParts = confirmedParts; | |
| } | |
| applyInterim(event: RealtimeTranscriptEvent): TranscriptUpdate { | |
| if (!this.acceptSession(event.sessionId)) return this.snapshot(false); | |
| if (this.finalized.has(event.utteranceId)) return this.snapshot(false); | |
| const previousRevision = this.latestRevision.get(event.utteranceId) ?? -1; | |
| if (event.revision <= previousRevision) return this.snapshot(false); | |
| if (event.utteranceIndex < this.nextFinalIndex) return this.snapshot(false); | |
| this.latestRevision.set(event.utteranceId, event.revision); | |
| this.activeInterim = event; | |
| return this.snapshot(true); | |
| } | |
| applyFinal(event: RealtimeTranscriptEvent): TranscriptUpdate { | |
| if (!this.acceptSession(event.sessionId)) return this.snapshot(false); | |
| if (this.finalized.has(event.utteranceId)) return this.snapshot(false); | |
| const previousRevision = this.latestRevision.get(event.utteranceId) ?? -1; | |
| if (event.revision < previousRevision) return this.snapshot(false); | |
| if (event.utteranceIndex < this.nextFinalIndex) return this.snapshot(false); | |
| const pending = this.pendingFinals.get(event.utteranceIndex); | |
| if (pending && pending.revision >= event.revision) { | |
| return this.snapshot(false); | |
| } | |
| this.latestRevision.set(event.utteranceId, event.revision); | |
| this.pendingFinals.set(event.utteranceIndex, event); | |
| const appendedFinals: string[] = []; | |
| while (this.pendingFinals.has(this.nextFinalIndex)) { | |
| const next = this.pendingFinals.get(this.nextFinalIndex)!; | |
| this.pendingFinals.delete(this.nextFinalIndex); | |
| this.finalized.add(next.utteranceId); | |
| const text = next.text.replace(/\s+/g, " ").trim(); | |
| if (text) { | |
| this.confirmedParts.push(text); | |
| appendedFinals.push(text); | |
| } | |
| if (this.activeInterim?.utteranceId === next.utteranceId) { | |
| this.activeInterim = null; | |
| } | |
| this.nextFinalIndex++; | |
| } | |
| return this.snapshot(true, appendedFinals); | |
| } | |
| private acceptSession(sessionId: string): boolean { | |
| if (this.sessionId === null) { | |
| this.sessionId = sessionId; | |
| return true; | |
| } | |
| return this.sessionId === sessionId; | |
| } | |
| private snapshot( | |
| accepted: boolean, | |
| appendedFinals: string[] = [], | |
| ): TranscriptUpdate { | |
| return { | |
| accepted, | |
| confirmedText: this.confirmedParts.join(" "), | |
| interimText: this.activeInterim?.text.trim() ?? "", | |
| appendedFinals, | |
| }; | |
| } | |
| } | |