|
|
@@ -0,0 +1,275 @@
|
|
|
+<script>
|
|
|
+import {connectASR_WebSocket, getASR_Token} from '@/api/diagnosis';
|
|
|
+import {generateUUID} from '@/utils';
|
|
|
+
|
|
|
+let websocket;
|
|
|
+let dispose = () => {};
|
|
|
+
|
|
|
+let onAudioConnect = () => {};
|
|
|
+let onAudioCleanup = () => {};
|
|
|
+let onSocketCleanup = () => {};
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: 'SoundRecorder',
|
|
|
+ props: {
|
|
|
+ enabled: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ taskId: '',
|
|
|
+
|
|
|
+ recording: false,
|
|
|
+ loading: false,
|
|
|
+ proof: {token: '', appKey: ''},
|
|
|
+ };
|
|
|
+ },
|
|
|
+ beforeMount() {
|
|
|
+ dispose = (event) => {
|
|
|
+ switch (event.type) {
|
|
|
+ case 'open':
|
|
|
+ return this.startTranscription(this.taskId);
|
|
|
+ case 'close': {
|
|
|
+ websocket = null;
|
|
|
+ this.recording = false;
|
|
|
+ this.loading = false;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ case 'message':
|
|
|
+ return this.analysis(JSON.parse(event.data));
|
|
|
+ case 'audioprocess':
|
|
|
+ return this.send(event.inputBuffer.getChannelData(0));
|
|
|
+ }
|
|
|
+ console.log(event, 'log:');
|
|
|
+ };
|
|
|
+ },
|
|
|
+ beforeDestroy() {
|
|
|
+ this.stop();
|
|
|
+ this.disconnect();
|
|
|
+ },
|
|
|
+ watch: {
|
|
|
+ enabled(value) {
|
|
|
+ if (!value) this.stop();
|
|
|
+ },
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ handle() {
|
|
|
+ if (this.loading) return;
|
|
|
+ try { if (this.recording) this.stop(); else this.start(); } catch (e) {
|
|
|
+ this.$message.warning(`出错了,请重试`);
|
|
|
+ this.disconnect();
|
|
|
+ console.log('[sr-ars]', e);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async connect() {
|
|
|
+ if (!this.proof || !this.proof.token) this.proof = await getASR_Token();
|
|
|
+ websocket = await connectASR_WebSocket(this.proof);
|
|
|
+ websocket.addEventListener('open', dispose);
|
|
|
+ websocket.addEventListener('close', dispose);
|
|
|
+ websocket.addEventListener('message', dispose);
|
|
|
+ websocket.addEventListener('error', dispose);
|
|
|
+ onSocketCleanup = () => {
|
|
|
+ websocket.removeEventListener('open', dispose);
|
|
|
+ websocket.removeEventListener('close', dispose);
|
|
|
+ websocket.removeEventListener('message', dispose);
|
|
|
+ websocket.removeEventListener('error', dispose);
|
|
|
+ onSocketCleanup = () => {};
|
|
|
+ };
|
|
|
+ },
|
|
|
+ async disconnect() {
|
|
|
+ this.loading = false;
|
|
|
+ this.recording = false;
|
|
|
+ onSocketCleanup();
|
|
|
+ },
|
|
|
+ async start() {
|
|
|
+ if (onAudioCleanup) onAudioCleanup();
|
|
|
+
|
|
|
+ this.loading = true;
|
|
|
+ this.taskId = generateUUID();
|
|
|
+
|
|
|
+ const stream = await navigator.mediaDevices.getUserMedia({audio: true});
|
|
|
+ const context = new window.AudioContext({sampleRate: 16000});
|
|
|
+ const input = context.createMediaStreamSource(stream);
|
|
|
+ const processor = context.createScriptProcessor(2048, 1, 1);
|
|
|
+ processor.addEventListener('audioprocess', dispose);
|
|
|
+
|
|
|
+ onAudioCleanup = () => {
|
|
|
+ this.loading = false;
|
|
|
+ processor.removeEventListener('audioprocess', dispose);
|
|
|
+ processor.disconnect();
|
|
|
+ input.disconnect();
|
|
|
+ stream.getTracks().forEach(track => track.stop());
|
|
|
+ context.close();
|
|
|
+ onAudioCleanup = () => {};
|
|
|
+ };
|
|
|
+
|
|
|
+ onAudioConnect = () => {
|
|
|
+ this.loading = false;
|
|
|
+ input.connect(processor);
|
|
|
+ processor.connect(context.destination);
|
|
|
+ onAudioConnect = () => {};
|
|
|
+ };
|
|
|
+
|
|
|
+ if (!websocket || websocket.readyState !== WebSocket.OPEN) await this.connect();
|
|
|
+ else if (this.recording) onAudioConnect();
|
|
|
+ },
|
|
|
+ async stop() {
|
|
|
+ if (!websocket || websocket.readyState !== WebSocket.OPEN) return;
|
|
|
+ websocket.send(JSON.stringify({
|
|
|
+ header: {
|
|
|
+ appkey: this.proof.appKey,
|
|
|
+ namespace: 'SpeechTranscriber',
|
|
|
+ name: 'StopTranscription',
|
|
|
+ task_id: this.taskId,
|
|
|
+ message_id: generateUUID(),
|
|
|
+ },
|
|
|
+ }));
|
|
|
+ },
|
|
|
+
|
|
|
+ startTranscription(taskId = generateUUID()) {
|
|
|
+ if (!websocket || websocket.readyState !== WebSocket.OPEN) return;
|
|
|
+ websocket.send(JSON.stringify({
|
|
|
+ header: {
|
|
|
+ appkey: this.proof.appKey,
|
|
|
+ namespace: 'SpeechTranscriber',
|
|
|
+ name: 'StartTranscription',
|
|
|
+ task_id: taskId,
|
|
|
+ message_id: generateUUID(),
|
|
|
+ },
|
|
|
+ payload: {
|
|
|
+ 'format': 'pcm',
|
|
|
+ 'sample_rate': 16000,
|
|
|
+ 'enable_intermediate_result': true,
|
|
|
+ 'enable_punctuation_prediction': true,
|
|
|
+ 'enable_inverse_text_normalization': true,
|
|
|
+ },
|
|
|
+ }));
|
|
|
+ },
|
|
|
+ send(data) {
|
|
|
+ if (!websocket || websocket.readyState !== WebSocket.OPEN) return;
|
|
|
+ const inputData16 = new Int16Array(data.length);
|
|
|
+ for (let i = 0; i < data.length; ++i) {
|
|
|
+ inputData16[i] = Math.max(-1, Math.min(1, data[i])) * 0x7FFF; // PCM 16-bit
|
|
|
+ }
|
|
|
+ websocket.send(inputData16.buffer);
|
|
|
+ console.log('[sr-ars] 发送音频数据块');
|
|
|
+ },
|
|
|
+ analysis(message) {
|
|
|
+ switch (message.header.name) {
|
|
|
+ case 'TranscriptionStarted': {
|
|
|
+ this.recording = true;
|
|
|
+ onAudioConnect();
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ case 'TranscriptionCompleted': {
|
|
|
+ this.recording = false;
|
|
|
+ onAudioCleanup();
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ case 'SentenceBegin': {
|
|
|
+ const {index, time} = message.payload;
|
|
|
+ console.log(`[sr-ars]:message: 一句话开始:[${index}(${time} - )]`);
|
|
|
+ this.$emit('begin');
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ case 'SentenceEnd': {
|
|
|
+ const {index, time, result, begin_time} = message.payload;
|
|
|
+ console.log(`[sr-ars]:message: 一句话结束:${result} [${index}(${begin_time} - ${time})]`);
|
|
|
+ this.$emit('finish', result);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ case 'TranscriptionResultChanged': {
|
|
|
+ const {index, time, result} = message.payload;
|
|
|
+ console.log(`[sr-ars]:message: 识别结果变化:${result} [${index}(${time})]`);
|
|
|
+ this.$emit('change', result);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ default: {
|
|
|
+ console.log('[sr-ars]:message', message);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ },
|
|
|
+};
|
|
|
+</script>
|
|
|
+
|
|
|
+<template>
|
|
|
+ <div style="z-index: 11;">
|
|
|
+ <slot>
|
|
|
+ <div class="sr-wrapper" :class="{recording, loading}" @click="handle()">
|
|
|
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
|
+
|
|
|
+ <circle v-if="loading" cx="12" cy="12" r="5" fill="none" stroke="white" stroke-width="1"></circle>
|
|
|
+ <rect v-if="!loading" x="9" y="6" :width="recording? 10 : 6" height="10" :rx="recording ? 5 : 3"
|
|
|
+ fill="white"/>
|
|
|
+ <path v-if="!loading"
|
|
|
+ d="M6.75 12.75V13C6.75 15.8995 9.1005 18.25 12 18.25C14.8995 18.25 17.25 15.8995 17.25 13V12.75"
|
|
|
+ stroke="white" stroke-width="1.5" stroke-linecap="round"/>
|
|
|
+ </svg>
|
|
|
+ </div>
|
|
|
+ </slot>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+.sr-wrapper {
|
|
|
+ position: relative;
|
|
|
+ width: 24px;
|
|
|
+ height: 24px;
|
|
|
+ background: #FF3359;
|
|
|
+ border-radius: 50%;
|
|
|
+ transform: scale(1.5);
|
|
|
+ cursor: pointer;
|
|
|
+ box-shadow: 0 4px 50px #FF3359;
|
|
|
+
|
|
|
+ &.recording {
|
|
|
+ svg {
|
|
|
+ rect {
|
|
|
+ transform: translate(-2px, 1px);
|
|
|
+ animation: recording 2s infinite 0.1s;
|
|
|
+ }
|
|
|
+
|
|
|
+ path {
|
|
|
+ transform: scale(0.7);
|
|
|
+ opacity: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ &.loading {
|
|
|
+ cursor: not-allowed;
|
|
|
+
|
|
|
+ svg {
|
|
|
+ animation: loading-rotate 2s linear infinite;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ svg {
|
|
|
+ rect {
|
|
|
+ stroke-width: 0;
|
|
|
+ stroke: white;
|
|
|
+ transition: all 0.2s ease;
|
|
|
+ transform-origin: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ circle {
|
|
|
+ stroke-linecap: round;
|
|
|
+ animation: loading-dash 1.5s ease-in-out infinite;
|
|
|
+ }
|
|
|
+
|
|
|
+ path {
|
|
|
+ transition: all 0.2s ease;
|
|
|
+ transform-origin: center;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @keyframes recording {
|
|
|
+ 50% {
|
|
|
+ stroke-width: 2;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|