let streamMuteNode;
let committedTranscription = "";
let partialTranscription = "";
+let lastCommittedTurn = -1;
const startButton = document.getElementById("btnStart");
const stopButton = document.getElementById("btnStop");
return;
}
- transcriptionElement.textContent = partial ? `${committed} ${partial}`.trim() : committed;
+ transcriptionElement.textContent = partial
+ ? `${committed} ${partial}`.trim()
+ : committed;
}
function cleanupCaptureResources(closeTranscriptionSocket = true) {
audioContext.close();
}
- if (closeTranscriptionSocket && transcriptionSocket && transcriptionSocket.readyState === WebSocket.OPEN) {
+ if (
+ closeTranscriptionSocket && transcriptionSocket &&
+ transcriptionSocket.readyState === WebSocket.OPEN
+ ) {
transcriptionSocket.close();
}
}
if (typeof payload.error === "string") {
- return {
- kind: "error",
- text: payload.error
- };
+ return { kind: "error", text: payload.error };
}
- const typeValue = typeof payload.type === "string" ? payload.type.toLowerCase() : "";
- const finalByType =
- typeValue.includes("termination") ||
- typeValue.includes("final") ||
- typeValue.includes("completed");
-
- const candidates = [
- payload.text,
- payload.transcript,
- payload.delta,
- payload.partial,
- payload.final,
- payload.turn?.text,
- payload.turn?.transcript,
- payload.turn?.final,
- payload.result?.text,
- payload.result?.transcript
- ];
-
- const transcriptText = candidates.find((value) => typeof value === "string" && value.trim());
- if (!transcriptText) {
+ if (payload.type !== "Turn") {
+ return null;
+ }
+
+ const transcriptText = typeof payload.transcript === "string"
+ ? payload.transcript
+ : "";
+ if (!transcriptText.trim()) {
return null;
}
return {
- kind: finalByType || payload.is_final === true || payload.final === true ? "final" : "partial",
- text: transcriptText
+ kind: payload.end_of_turn === true ? "final" : "partial",
+ text: transcriptText,
+ turnOrder: typeof payload.turn_order === "number"
+ ? payload.turn_order
+ : undefined,
};
}
-function downsampleToTargetSampleRate(float32Samples, inputSampleRate, targetSampleRate) {
+function downsampleToTargetSampleRate(
+ float32Samples,
+ inputSampleRate,
+ targetSampleRate,
+) {
if (targetSampleRate >= inputSampleRate) {
return float32Samples;
}
let total = 0;
let count = 0;
- for (let i = inputIndex; i < nextInputIndex && i < float32Samples.length; i += 1) {
+ for (
+ let i = inputIndex;
+ i < nextInputIndex && i < float32Samples.length;
+ i += 1
+ ) {
total += float32Samples[i];
count += 1;
}
} catch {
return;
}
+ console.log(parsed);
+ console.log(JSON.stringify(parsed, null, 2));
const transcriptEvent = extractTranscriptFromRealtimeEvent(parsed);
+ console.log(transcriptEvent);
if (!transcriptEvent) {
return;
}
}
if (transcriptEvent.kind === "final") {
- appendCommittedTranscription(transcriptEvent.text);
+ if (transcriptEvent.turnOrder > lastCommittedTurn) {
+ lastCommittedTurn = transcriptEvent.turnOrder;
+ appendCommittedTranscription(transcriptEvent.text);
+ }
} else {
updatePartialTranscription(transcriptEvent.text);
}
try {
committedTranscription = "";
partialTranscription = "";
+ lastCommittedTurn = -1;
renderTranscription();
- microphoneStream = await navigator.mediaDevices.getUserMedia({ audio: true });
+ microphoneStream = await navigator.mediaDevices.getUserMedia({
+ audio: true,
+ });
systemStream = await navigator.mediaDevices.getDisplayMedia({
video: true,
- audio: true
+ audio: true,
});
console.log(systemStream.getTracks());
streamMuteNode.connect(audioContext.destination);
streamProcessorNode.onaudioprocess = (audioProcessEvent) => {
- if (!transcriptionSocket || transcriptionSocket.readyState !== WebSocket.OPEN) {
+ if (
+ !transcriptionSocket ||
+ transcriptionSocket.readyState !== WebSocket.OPEN
+ ) {
return;
}
const downsampled = downsampleToTargetSampleRate(
channelSamples,
audioContext.sampleRate,
- TARGET_SAMPLE_RATE
+ TARGET_SAMPLE_RATE,
);
const pcm16Buffer = float32ToPcm16Buffer(downsampled);
transcriptionSocket.send(pcm16Buffer);
"audio/webm;codecs=opus",
"audio/webm",
"audio/ogg;codecs=opus",
- "audio/ogg"
+ "audio/ogg",
];
- const selectedMimeType = preferredMimeTypes.find((type) => MediaRecorder.isTypeSupported(type));
+ const selectedMimeType = preferredMimeTypes.find((type) =>
+ MediaRecorder.isTypeSupported(type)
+ );
console.log("selected mime type:", selectedMimeType);
mediaRecorder = selectedMimeType
? new MediaRecorder(mixedAudioStream, { mimeType: selectedMimeType })
});
mediaRecorder.addEventListener("stop", () => {
- const audioBlob = new Blob(audioChunks, { type: mediaRecorder.mimeType || "audio/webm" });
- console.log(audioBlob)
+ const audioBlob = new Blob(audioChunks, {
+ type: mediaRecorder.mimeType || "audio/webm",
+ });
+ console.log(audioBlob);
if (recordedAudioUrl) {
URL.revokeObjectURL(recordedAudioUrl);
return;
}
- if (transcriptionSocket && transcriptionSocket.readyState === WebSocket.OPEN) {
+ if (
+ transcriptionSocket && transcriptionSocket.readyState === WebSocket.OPEN
+ ) {
transcriptionSocket.send(JSON.stringify({ type: "Terminate" }));
}