Spaces:
Runtime error
Runtime error
| import {TYPING_ANIMATION_DELAY_MS} from './StreamingInterface'; | |
| import {getURLParams} from './URLParams'; | |
| import audioBuffertoWav from 'audiobuffer-to-wav'; | |
| import './StreamingInterface.css'; | |
| type StartEndTime = { | |
| start: number; | |
| end: number; | |
| }; | |
| type StartEndTimeWithAudio = StartEndTime & { | |
| float32Audio: Float32Array; | |
| }; | |
| type Text = { | |
| time: number; | |
| chars: number; | |
| }; | |
| type DebugTimings = { | |
| receivedAudio: StartEndTime[]; | |
| playedAudio: StartEndTimeWithAudio[]; | |
| receivedText: Text[]; | |
| renderedText: StartEndTime[]; | |
| sentAudio: StartEndTimeWithAudio[]; | |
| startRenderTextTime: number | null; | |
| startRecordingTime: number | null; | |
| receivedAudioSampleRate: number | null; | |
| }; | |
| function getInitialTimings(): DebugTimings { | |
| return { | |
| receivedAudio: [], | |
| playedAudio: [], | |
| receivedText: [], | |
| renderedText: [], | |
| sentAudio: [], | |
| startRenderTextTime: null, | |
| startRecordingTime: null, | |
| receivedAudioSampleRate: null, | |
| }; | |
| } | |
| function downloadAudioBuffer(audioBuffer: AudioBuffer, fileName: string): void { | |
| const wav = audioBuffertoWav(audioBuffer); | |
| const wavBlob = new Blob([new DataView(wav)], { | |
| type: 'audio/wav', | |
| }); | |
| const url = URL.createObjectURL(wavBlob); | |
| const anchor = document.createElement('a'); | |
| anchor.href = url; | |
| anchor.target = '_blank'; | |
| anchor.download = fileName; | |
| anchor.click(); | |
| } | |
| // Uncomment for debugging without download | |
| // function playAudioBuffer(audioBuffer: AudioBuffer): void { | |
| // const audioContext = new AudioContext(); | |
| // const source = audioContext.createBufferSource(); | |
| // source.buffer = audioBuffer; | |
| // source.connect(audioContext.destination); | |
| // source.start(); | |
| // } | |
| // Accumulate timings and audio / text translation samples for debugging and exporting | |
| class DebugTimingsManager { | |
| timings: DebugTimings = getInitialTimings(); | |
| start(): void { | |
| this.timings = getInitialTimings(); | |
| this.timings.startRecordingTime = new Date().getTime(); | |
| } | |
| sentAudio(event: AudioProcessingEvent): void { | |
| const end = new Date().getTime(); | |
| const start = end - event.inputBuffer.duration * 1000; | |
| // Copy or else buffer seems to be re-used | |
| const float32Audio = new Float32Array(event.inputBuffer.getChannelData(0)); | |
| this.timings.sentAudio.push({ | |
| start, | |
| end, | |
| float32Audio, | |
| }); | |
| } | |
| receivedText(text: string): void { | |
| this.timings.receivedText.push({ | |
| time: new Date().getTime(), | |
| chars: text.length, | |
| }); | |
| } | |
| startRenderText(): void { | |
| if (this.timings.startRenderTextTime == null) { | |
| this.timings.startRenderTextTime = new Date().getTime(); | |
| } | |
| } | |
| endRenderText(): void { | |
| if (this.timings.startRenderTextTime == null) { | |
| console.warn( | |
| 'Wrong timings of start / end rendering text. startRenderText is null', | |
| ); | |
| return; | |
| } | |
| this.timings.renderedText.push({ | |
| start: this.timings.startRenderTextTime as number, | |
| end: new Date().getTime(), | |
| }); | |
| this.timings.startRenderTextTime = null; | |
| } | |
| receivedAudio(duration: number): void { | |
| const start = new Date().getTime(); | |
| this.timings.receivedAudio.push({ | |
| start, | |
| end: start + duration * 1000, | |
| }); | |
| } | |
| playedAudio(start: number, end: number, buffer: AudioBuffer | null): void { | |
| if (buffer != null) { | |
| if (this.timings.receivedAudioSampleRate == null) { | |
| this.timings.receivedAudioSampleRate = buffer.sampleRate; | |
| } | |
| if (this.timings.receivedAudioSampleRate != buffer.sampleRate) { | |
| console.error( | |
| 'Sample rates of received audio are unequal, will fail to reconstruct debug audio', | |
| this.timings.receivedAudioSampleRate, | |
| buffer.sampleRate, | |
| ); | |
| } | |
| } | |
| this.timings.playedAudio.push({ | |
| start, | |
| end, | |
| float32Audio: | |
| buffer == null | |
| ? new Float32Array() | |
| : new Float32Array(buffer.getChannelData(0)), | |
| }); | |
| } | |
| getChartData() { | |
| const columns = [ | |
| {type: 'string', id: 'Series'}, | |
| {type: 'date', id: 'Start'}, | |
| {type: 'date', id: 'End'}, | |
| ]; | |
| return [ | |
| columns, | |
| ...this.timings.sentAudio.map((sentAudio) => [ | |
| 'Sent Audio', | |
| new Date(sentAudio.start), | |
| new Date(sentAudio.end), | |
| ]), | |
| ...this.timings.receivedAudio.map((receivedAudio) => [ | |
| 'Received Audio', | |
| new Date(receivedAudio.start), | |
| new Date(receivedAudio.end), | |
| ]), | |
| ...this.timings.playedAudio.map((playedAudio) => [ | |
| 'Played Audio', | |
| new Date(playedAudio.start), | |
| new Date(playedAudio.end), | |
| ]), | |
| // Best estimate duration by multiplying length with animation duration for each letter | |
| ...this.timings.receivedText.map((receivedText) => [ | |
| 'Received Text', | |
| new Date(receivedText.time), | |
| new Date( | |
| receivedText.time + receivedText.chars * TYPING_ANIMATION_DELAY_MS, | |
| ), | |
| ]), | |
| ...this.timings.renderedText.map((renderedText) => [ | |
| 'Rendered Text', | |
| new Date(renderedText.start), | |
| new Date(renderedText.end), | |
| ]), | |
| ]; | |
| } | |
| downloadInputAudio() { | |
| const audioContext = new AudioContext(); | |
| const totalLength = this.timings.sentAudio.reduce((acc, cur) => { | |
| return acc + cur?.float32Audio?.length ?? 0; | |
| }, 0); | |
| if (totalLength === 0) { | |
| return; | |
| } | |
| const incomingArrayBuffer = audioContext.createBuffer( | |
| 1, // 1 channel | |
| totalLength, | |
| audioContext.sampleRate, | |
| ); | |
| const buffer = incomingArrayBuffer.getChannelData(0); | |
| let i = 0; | |
| this.timings.sentAudio.forEach((sentAudio) => { | |
| sentAudio.float32Audio.forEach((bytes) => { | |
| buffer[i++] = bytes; | |
| }); | |
| }); | |
| // Play for debugging | |
| // playAudioBuffer(incomingArrayBuffer); | |
| downloadAudioBuffer(incomingArrayBuffer, `input_audio.wav`); | |
| } | |
| downloadOutputAudio() { | |
| const playedAudio = this.timings.playedAudio; | |
| const sampleRate = this.timings.receivedAudioSampleRate; | |
| if ( | |
| playedAudio.length === 0 || | |
| this.timings.startRecordingTime == null || | |
| sampleRate == null | |
| ) { | |
| return null; | |
| } | |
| let previousEndTime = this.timings.startRecordingTime; | |
| const audioArray: number[] = []; | |
| playedAudio.forEach((audio) => { | |
| const delta = (audio.start - previousEndTime) / 1000; | |
| for (let i = 0; i < delta * sampleRate; i++) { | |
| audioArray.push(0.0); | |
| } | |
| audio.float32Audio.forEach((bytes) => audioArray.push(bytes)); | |
| previousEndTime = audio.end; | |
| }); | |
| const audioContext = new AudioContext(); | |
| const incomingArrayBuffer = audioContext.createBuffer( | |
| 1, // 1 channel | |
| audioArray.length, | |
| sampleRate, | |
| ); | |
| incomingArrayBuffer.copyToChannel( | |
| new Float32Array(audioArray), | |
| 0, // first channel | |
| ); | |
| // Play for debugging | |
| // playAudioBuffer(incomingArrayBuffer); | |
| downloadAudioBuffer(incomingArrayBuffer, 'output_audio.wav'); | |
| } | |
| } | |
| const debugSingleton = new DebugTimingsManager(); | |
| export default function debug(): DebugTimingsManager | null { | |
| const debugParam = getURLParams().debug; | |
| return debugParam ? debugSingleton : null; | |
| } | |