|
@@ -1,285 +0,0 @@
|
|
|
-<template>
|
|
|
|
|
- <div>
|
|
|
|
|
- <button @click="toggleRecording" :class="['recordButton', { recording: isRecording }]">
|
|
|
|
|
- {{ isRecording ? 'Stop Recording' : 'Start Recording' }}
|
|
|
|
|
- </button>
|
|
|
|
|
- <label>
|
|
|
|
|
- Language:
|
|
|
|
|
- <input v-model="lang" type="text" value="auto" />
|
|
|
|
|
- </label>
|
|
|
|
|
- <label> <input type="checkbox" v-model="speakerVerification" /> Speaker Verification </label>
|
|
|
|
|
- <hr />
|
|
|
|
|
- Transcription result will be displayed below:
|
|
|
|
|
- <p id="transcriptionResult">{{ transcriptionResult }}</p>
|
|
|
|
|
- </div>
|
|
|
|
|
-</template>
|
|
|
|
|
-
|
|
|
|
|
-<script>
|
|
|
|
|
-import { defineComponent, ref, onMounted, onBeforeUnmount } from 'vue';
|
|
|
|
|
-
|
|
|
|
|
-export default defineComponent({
|
|
|
|
|
- name: 'AudioTranscription',
|
|
|
|
|
- setup() {
|
|
|
|
|
- const lang = ref('auto');
|
|
|
|
|
- const speakerVerification = ref(false);
|
|
|
|
|
- const isRecording = ref(false);
|
|
|
|
|
- const transcriptionResult = ref('');
|
|
|
|
|
- const ws = ref(null);
|
|
|
|
|
- const record = ref(null);
|
|
|
|
|
- const timeInte = ref(null);
|
|
|
|
|
-
|
|
|
|
|
- const speakerVerificationParam = computed(() => (speakerVerification.value ? 1 : 0));
|
|
|
|
|
-
|
|
|
|
|
- // WebSocket initialization
|
|
|
|
|
- const initWebSocket = () => {
|
|
|
|
|
- const queryParams = [];
|
|
|
|
|
- if (lang.value) queryParams.push(`lang=${lang.value}`);
|
|
|
|
|
- if (speakerVerification.value) queryParams.push('sv=1');
|
|
|
|
|
- const queryString = queryParams.length > 0 ? `?${queryParams.join('&')}` : '';
|
|
|
|
|
-
|
|
|
|
|
- const init_send_message_data = {
|
|
|
|
|
- ext: '.wav',
|
|
|
|
|
- meeting_id: 'f282094a415c482e9e72a3c6a94a30d1'
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- ws.value = new WebSocket(`ws://192.168.20.72:9601/api/asr/realtime`);
|
|
|
|
|
- ws.value.binaryType = 'arraybuffer';
|
|
|
|
|
-
|
|
|
|
|
- ws.value.onopen = () => {
|
|
|
|
|
- console.log('WebSocket connection established');
|
|
|
|
|
- ws.value.send(JSON.stringify(init_send_message_data));
|
|
|
|
|
- record.value.start();
|
|
|
|
|
- timeInte.value = setInterval(() => {
|
|
|
|
|
- if (ws.value.readyState === 1) {
|
|
|
|
|
- const audioBlob = record.value.getBlob();
|
|
|
|
|
- console.log('Blob size: ', audioBlob.size);
|
|
|
|
|
-
|
|
|
|
|
- const reader = new FileReader();
|
|
|
|
|
- reader.onloadend = () => {
|
|
|
|
|
- console.log('Blob content: ', new Uint8Array(reader.result));
|
|
|
|
|
- ws.value.send(audioBlob);
|
|
|
|
|
- console.log('Sending audio data');
|
|
|
|
|
- record.value.clear();
|
|
|
|
|
- };
|
|
|
|
|
- reader.readAsArrayBuffer(audioBlob);
|
|
|
|
|
- }
|
|
|
|
|
- }, 500);
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- ws.value.onmessage = (evt) => {
|
|
|
|
|
- console.log('Received message: ' + evt.data);
|
|
|
|
|
- try {
|
|
|
|
|
- const resJson = JSON.parse(evt.data);
|
|
|
|
|
- if (resJson.code === 0) {
|
|
|
|
|
- transcriptionResult.value += `\n${resJson.data || 'No speech recognized'}`;
|
|
|
|
|
- }
|
|
|
|
|
- } catch (e) {
|
|
|
|
|
- console.error('Failed to parse response data', e);
|
|
|
|
|
- transcriptionResult.value += `\n${evt.data}`;
|
|
|
|
|
- }
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- ws.value.onclose = () => {
|
|
|
|
|
- console.log('WebSocket connection closed');
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- ws.value.onerror = (error) => {
|
|
|
|
|
- console.error('WebSocket error: ' + error);
|
|
|
|
|
- };
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- const startRecording = () => {
|
|
|
|
|
- console.log('Start Recording');
|
|
|
|
|
- if (!navigator.getUserMedia) {
|
|
|
|
|
- alert('Your browser does not support audio input');
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- navigator.getUserMedia(
|
|
|
|
|
- { audio: true },
|
|
|
|
|
- (mediaStream) => {
|
|
|
|
|
- initRecorder(mediaStream);
|
|
|
|
|
- initWebSocket();
|
|
|
|
|
- },
|
|
|
|
|
- (error) => {
|
|
|
|
|
- console.error('Error accessing microphone:', error);
|
|
|
|
|
- }
|
|
|
|
|
- );
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- const stopRecording = () => {
|
|
|
|
|
- console.log('Stop Recording');
|
|
|
|
|
- if (ws.value) {
|
|
|
|
|
- ws.value.close();
|
|
|
|
|
- }
|
|
|
|
|
- if (record.value) {
|
|
|
|
|
- record.value.stop();
|
|
|
|
|
- clearInterval(timeInte.value);
|
|
|
|
|
- }
|
|
|
|
|
- isRecording.value = false;
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- const toggleRecording = () => {
|
|
|
|
|
- if (isRecording.value) {
|
|
|
|
|
- stopRecording();
|
|
|
|
|
- } else {
|
|
|
|
|
- startRecording();
|
|
|
|
|
- isRecording.value = true;
|
|
|
|
|
- }
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- const initRecorder = (stream) => {
|
|
|
|
|
- record.value = new Recorder(stream);
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- onMounted(() => {
|
|
|
|
|
- if (!navigator.getUserMedia) {
|
|
|
|
|
- alert('Your browser does not support audio input');
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- onBeforeUnmount(() => {
|
|
|
|
|
- if (ws.value) {
|
|
|
|
|
- ws.value.close();
|
|
|
|
|
- }
|
|
|
|
|
- if (record.value) {
|
|
|
|
|
- record.value.stop();
|
|
|
|
|
- }
|
|
|
|
|
- if (timeInte.value) {
|
|
|
|
|
- clearInterval(timeInte.value);
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- // Recorder logic
|
|
|
|
|
- class Recorder {
|
|
|
|
|
- constructor(stream) {
|
|
|
|
|
- this.sampleBits = 16;
|
|
|
|
|
- this.inputSampleRate = 48000;
|
|
|
|
|
- this.outputSampleRate = 16000;
|
|
|
|
|
- this.channelCount = 1;
|
|
|
|
|
- this.context = new AudioContext();
|
|
|
|
|
- this.audioInput = this.context.createMediaStreamSource(stream);
|
|
|
|
|
- this.recorder = this.context.createScriptProcessor(4096, this.channelCount, this.channelCount);
|
|
|
|
|
- this.audioData = {
|
|
|
|
|
- size: 0,
|
|
|
|
|
- buffer: [],
|
|
|
|
|
- inputSampleRate: this.inputSampleRate,
|
|
|
|
|
- inputSampleBits: this.sampleBits,
|
|
|
|
|
- clear() {
|
|
|
|
|
- this.buffer = [];
|
|
|
|
|
- this.size = 0;
|
|
|
|
|
- },
|
|
|
|
|
- input(data) {
|
|
|
|
|
- this.buffer.push(new Float32Array(data));
|
|
|
|
|
- this.size += data.length;
|
|
|
|
|
- },
|
|
|
|
|
- encodePCM() {
|
|
|
|
|
- const bytes = new Float32Array(this.size);
|
|
|
|
|
- let offset = 0;
|
|
|
|
|
-
|
|
|
|
|
- // 合并所有数据到一个字节数组
|
|
|
|
|
- for (let i = 0; i < this.buffer.length; i++) {
|
|
|
|
|
- bytes.set(this.buffer[i], offset);
|
|
|
|
|
- offset += this.buffer[i].length;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 计算最终数据需要的字节长度
|
|
|
|
|
- const dataLength = bytes.length * (this.sampleBits / 8);
|
|
|
|
|
- const buffer = new ArrayBuffer(dataLength);
|
|
|
|
|
- const data = new DataView(buffer);
|
|
|
|
|
-
|
|
|
|
|
- // 使用新的offset递增逻辑,避免超出边界
|
|
|
|
|
- offset = 0;
|
|
|
|
|
- for (let i = 0; i < bytes.length; i++, offset += 2) {
|
|
|
|
|
- if (offset + 2 > data.byteLength) {
|
|
|
|
|
- console.error('Offset exceeds buffer length');
|
|
|
|
|
- break;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const s = Math.max(-1, Math.min(1, bytes[i])); // 限制值的范围
|
|
|
|
|
- data.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true); // 写入16位数据
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 返回Blob数据
|
|
|
|
|
- return new Blob([data], { type: 'audio/pcm' });
|
|
|
|
|
- }
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- this.start = () => {
|
|
|
|
|
- this.audioInput.connect(this.recorder);
|
|
|
|
|
- this.recorder.connect(this.context.destination);
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- this.stop = () => {
|
|
|
|
|
- this.recorder.disconnect();
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- this.getBlob = () => {
|
|
|
|
|
- return this.audioData.encodePCM();
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- this.clear = () => {
|
|
|
|
|
- this.audioData.clear();
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- this.recorder.onaudioprocess = (e) => {
|
|
|
|
|
- const resampledData = this.downsampleBuffer(
|
|
|
|
|
- e.inputBuffer.getChannelData(0),
|
|
|
|
|
- this.inputSampleRate,
|
|
|
|
|
- this.outputSampleRate
|
|
|
|
|
- );
|
|
|
|
|
- this.audioData.input(resampledData);
|
|
|
|
|
- };
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- downsampleBuffer(buffer, inputSampleRate, outputSampleRate) {
|
|
|
|
|
- if (outputSampleRate === inputSampleRate) {
|
|
|
|
|
- return buffer;
|
|
|
|
|
- }
|
|
|
|
|
- const sampleRateRatio = inputSampleRate / outputSampleRate;
|
|
|
|
|
- const newLength = Math.round(buffer.length / sampleRateRatio);
|
|
|
|
|
- const result = new Float32Array(newLength);
|
|
|
|
|
- let offsetResult = 0;
|
|
|
|
|
- let offsetBuffer = 0;
|
|
|
|
|
- while (offsetResult < result.length) {
|
|
|
|
|
- const nextOffsetBuffer = Math.round((offsetResult + 1) * sampleRateRatio);
|
|
|
|
|
- let accum = 0,
|
|
|
|
|
- count = 0;
|
|
|
|
|
- for (let i = offsetBuffer; i < nextOffsetBuffer && i < buffer.length; i++) {
|
|
|
|
|
- accum += buffer[i];
|
|
|
|
|
- count++;
|
|
|
|
|
- }
|
|
|
|
|
- result[offsetResult] = accum / count;
|
|
|
|
|
- offsetResult++;
|
|
|
|
|
- offsetBuffer = nextOffsetBuffer;
|
|
|
|
|
- }
|
|
|
|
|
- return result;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return {
|
|
|
|
|
- lang,
|
|
|
|
|
- speakerVerification,
|
|
|
|
|
- transcriptionResult,
|
|
|
|
|
- isRecording,
|
|
|
|
|
- toggleRecording
|
|
|
|
|
- };
|
|
|
|
|
- }
|
|
|
|
|
-});
|
|
|
|
|
-</script>
|
|
|
|
|
-
|
|
|
|
|
-<style scoped>
|
|
|
|
|
-.recordButton.recording {
|
|
|
|
|
- background-color: red;
|
|
|
|
|
- color: white;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-#transcriptionResult {
|
|
|
|
|
- white-space: pre-wrap;
|
|
|
|
|
- background-color: #f5f5f5;
|
|
|
|
|
- padding: 10px;
|
|
|
|
|
- border: 1px solid #ddd;
|
|
|
|
|
- border-radius: 5px;
|
|
|
|
|
- font-family: monospace;
|
|
|
|
|
-}
|
|
|
|
|
-</style>
|
|
|