SoundRecorder.vue 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. <script>
  2. import {connectASR_WebSocket, getASR_Token} from '@/api/diagnosis';
  3. import {generateUUID} from '@/utils';
  4. let websocket;
  5. let dispose = () => {};
  6. let onAudioConnect = () => {};
  7. let onAudioCleanup = () => {};
  8. let onSocketCleanup = () => {};
  9. export default {
  10. name: 'SoundRecorder',
  11. props: {
  12. enabled: {
  13. type: Boolean,
  14. default: false,
  15. },
  16. },
  17. data() {
  18. return {
  19. taskId: '',
  20. recording: false,
  21. loading: false,
  22. proof: {token: '', appKey: ''},
  23. };
  24. },
  25. beforeMount() {
  26. dispose = (event) => {
  27. switch (event.type) {
  28. case 'open':
  29. return this.startTranscription(this.taskId);
  30. case 'close': {
  31. websocket = null;
  32. this.recording = false;
  33. this.loading = false;
  34. break;
  35. }
  36. case 'message':
  37. return this.analysis(JSON.parse(event.data));
  38. case 'audioprocess':
  39. return this.send(event.inputBuffer.getChannelData(0));
  40. }
  41. console.log(event, 'log:');
  42. };
  43. },
  44. beforeDestroy() {
  45. this.stop();
  46. this.disconnect();
  47. },
  48. watch: {
  49. enabled(value) {
  50. if (!value) this.stop();
  51. },
  52. },
  53. methods: {
  54. handle() {
  55. if (this.loading) return;
  56. try { if (this.recording) this.stop(); else this.start(); } catch (e) {
  57. this.$message.warning(`出错了,请重试`);
  58. this.disconnect();
  59. console.log('[sr-ars]', e);
  60. }
  61. },
  62. async connect() {
  63. if (!this.proof || !this.proof.token) this.proof = await getASR_Token();
  64. websocket = await connectASR_WebSocket(this.proof);
  65. websocket.addEventListener('open', dispose);
  66. websocket.addEventListener('close', dispose);
  67. websocket.addEventListener('message', dispose);
  68. websocket.addEventListener('error', dispose);
  69. onSocketCleanup = () => {
  70. websocket.removeEventListener('open', dispose);
  71. websocket.removeEventListener('close', dispose);
  72. websocket.removeEventListener('message', dispose);
  73. websocket.removeEventListener('error', dispose);
  74. onSocketCleanup = () => {};
  75. };
  76. },
  77. async disconnect() {
  78. this.loading = false;
  79. this.recording = false;
  80. onSocketCleanup();
  81. },
  82. async start() {
  83. if (onAudioCleanup) onAudioCleanup();
  84. this.loading = true;
  85. this.taskId = generateUUID();
  86. const stream = await navigator.mediaDevices.getUserMedia({audio: true});
  87. const context = new window.AudioContext({sampleRate: 16000});
  88. const input = context.createMediaStreamSource(stream);
  89. const processor = context.createScriptProcessor(2048, 1, 1);
  90. processor.addEventListener('audioprocess', dispose);
  91. onAudioCleanup = () => {
  92. this.loading = false;
  93. processor.removeEventListener('audioprocess', dispose);
  94. processor.disconnect();
  95. input.disconnect();
  96. stream.getTracks().forEach(track => track.stop());
  97. context.close();
  98. onAudioCleanup = () => {};
  99. };
  100. onAudioConnect = () => {
  101. this.loading = false;
  102. input.connect(processor);
  103. processor.connect(context.destination);
  104. onAudioConnect = () => {};
  105. };
  106. if (!websocket || websocket.readyState !== WebSocket.OPEN) await this.connect();
  107. else if (this.recording) onAudioConnect();
  108. },
  109. async stop() {
  110. if (!websocket || websocket.readyState !== WebSocket.OPEN) return;
  111. websocket.send(JSON.stringify({
  112. header: {
  113. appkey: this.proof.appKey,
  114. namespace: 'SpeechTranscriber',
  115. name: 'StopTranscription',
  116. task_id: this.taskId,
  117. message_id: generateUUID(),
  118. },
  119. }));
  120. },
  121. startTranscription(taskId = generateUUID()) {
  122. if (!websocket || websocket.readyState !== WebSocket.OPEN) return;
  123. websocket.send(JSON.stringify({
  124. header: {
  125. appkey: this.proof.appKey,
  126. namespace: 'SpeechTranscriber',
  127. name: 'StartTranscription',
  128. task_id: taskId,
  129. message_id: generateUUID(),
  130. },
  131. payload: {
  132. 'format': 'pcm',
  133. 'sample_rate': 16000,
  134. 'enable_intermediate_result': true,
  135. 'enable_punctuation_prediction': true,
  136. 'enable_inverse_text_normalization': true,
  137. },
  138. }));
  139. },
  140. send(data) {
  141. if (!websocket || websocket.readyState !== WebSocket.OPEN) return;
  142. const inputData16 = new Int16Array(data.length);
  143. for (let i = 0; i < data.length; ++i) {
  144. inputData16[i] = Math.max(-1, Math.min(1, data[i])) * 0x7FFF; // PCM 16-bit
  145. }
  146. websocket.send(inputData16.buffer);
  147. console.log('[sr-ars] 发送音频数据块');
  148. },
  149. analysis(message) {
  150. switch (message.header.name) {
  151. case 'TranscriptionStarted': {
  152. this.recording = true;
  153. onAudioConnect();
  154. break;
  155. }
  156. case 'TranscriptionCompleted': {
  157. this.recording = false;
  158. onAudioCleanup();
  159. break;
  160. }
  161. case 'SentenceBegin': {
  162. const {index, time} = message.payload;
  163. console.log(`[sr-ars]:message: 一句话开始:[${index}(${time} - )]`);
  164. this.$emit('begin');
  165. break;
  166. }
  167. case 'SentenceEnd': {
  168. const {index, time, result, begin_time} = message.payload;
  169. console.log(`[sr-ars]:message: 一句话结束:${result} [${index}(${begin_time} - ${time})]`);
  170. this.$emit('finish', result);
  171. break;
  172. }
  173. case 'TranscriptionResultChanged': {
  174. const {index, time, result} = message.payload;
  175. console.log(`[sr-ars]:message: 识别结果变化:${result} [${index}(${time})]`);
  176. this.$emit('change', result);
  177. break;
  178. }
  179. default: {
  180. console.log('[sr-ars]:message', message);
  181. }
  182. }
  183. },
  184. },
  185. };
  186. </script>
  187. <template>
  188. <div style="z-index: 11;">
  189. <slot>
  190. <div class="sr-wrapper" :class="{recording, loading}" @click="handle()">
  191. <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
  192. <circle v-if="loading" cx="12" cy="12" r="5" fill="none" stroke="white" stroke-width="1"></circle>
  193. <rect v-if="!loading" x="9" y="6" :width="recording? 10 : 6" height="10" :rx="recording ? 5 : 3"
  194. fill="white"/>
  195. <path v-if="!loading"
  196. 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"
  197. stroke="white" stroke-width="1.5" stroke-linecap="round"/>
  198. </svg>
  199. </div>
  200. </slot>
  201. </div>
  202. </template>
  203. <style scoped lang="scss">
  204. .sr-wrapper {
  205. position: relative;
  206. width: 24px;
  207. height: 24px;
  208. background: #FF3359;
  209. border-radius: 50%;
  210. transform: scale(1.5);
  211. cursor: pointer;
  212. box-shadow: 0 4px 50px #FF3359;
  213. &.recording {
  214. svg {
  215. rect {
  216. transform: translate(-2px, 1px);
  217. animation: recording 2s infinite 0.1s;
  218. }
  219. path {
  220. transform: scale(0.7);
  221. opacity: 0;
  222. }
  223. }
  224. }
  225. &.loading {
  226. cursor: not-allowed;
  227. svg {
  228. animation: loading-rotate 2s linear infinite;
  229. }
  230. }
  231. svg {
  232. rect {
  233. stroke-width: 0;
  234. stroke: white;
  235. transition: all 0.2s ease;
  236. transform-origin: center;
  237. }
  238. circle {
  239. stroke-linecap: round;
  240. animation: loading-dash 1.5s ease-in-out infinite;
  241. }
  242. path {
  243. transition: all 0.2s ease;
  244. transform-origin: center;
  245. }
  246. }
  247. @keyframes recording {
  248. 50% {
  249. stroke-width: 2;
  250. }
  251. }
  252. }
  253. </style>