| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275 |
- <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>
|