lyuan-tx-asr.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  1. <template>
  2. <view class="asr-container flex flex-direction " v-show="isShow" :class="isAnimation ? 'animation-slide-bottom' : 'animation-slide-bottom-hidden'">
  3. <view class="flex1" :style="'background-color: rgba(0,0,0,' + opacity + ');'"></view>
  4. <view class="bg-white shadow shadow-warp radius padding-bottom shadow-blur">
  5. <view class="cu-bar">
  6. <view class="action"><button class="cu-btn" @click="cancel">取消</button></view>
  7. <view class="content"></view>
  8. <view class="action"><button class="cu-btn bg-main" @click="ok">确定</button></view>
  9. </view>
  10. <view class="padding">
  11. <view class=" bg-gray padding border">{{ content }}</view>
  12. </view>
  13. <view class="flex align-center justify-center">
  14. <view v-if="status" class="flex1 margin-lr asr-playing"></view>
  15. <view class=" flex justify-center flex-direction align-center">
  16. <view
  17. style="border: 2px solid #007AFF;"
  18. class="cu-avatar xl round bg-white "
  19. :class="status ? 'startBtn' : ''"
  20. @touchstart="ontouchstart"
  21. @touchend="ontouchend"
  22. >
  23. <view :class="status ? 'cuIcon-stop text-blue' : 'cuIcon-playfill text-blue'"></view>
  24. </view>
  25. <view class=" text-gray padding">
  26. <text v-if="status == 0">长按开始</text>
  27. <text v-else-if="status == 1">引擎初始化</text>
  28. <text v-else-if="status == 2">语音识别启动中</text>
  29. <text v-else-if="status == 3">正在识别 {{ durationClock }}</text>
  30. </view>
  31. </view>
  32. <view v-if="status" class="flex1 margin-lr asr-playing" style=""></view>
  33. </view>
  34. </view>
  35. </view>
  36. </template>
  37. <script>
  38. import asrauthentication from './asrauthentication.js';
  39. export default {
  40. name: 'lyuan-tx-asr',
  41. props: {
  42. secretKey: { type: String },
  43. secretId: { type: String },
  44. appId: { type: Number | String },
  45. uploadFile: { type: Function },
  46. opacity: { type: Number, default: 0.4 }
  47. },
  48. data() {
  49. return {
  50. socket: null,
  51. recorder: null,
  52. status: 0,
  53. timer: null,
  54. content: '',
  55. message: '',
  56. duration: 0,
  57. durationTimer: null,
  58. isShow: false,
  59. isAnimation: false
  60. };
  61. },
  62. computed: {
  63. durationClock: function() {
  64. let minute = Math.floor(this.duration / 60);
  65. if (minute < 10) minute = '0' + minute.toString();
  66. let seconds = this.duration % 60;
  67. if (seconds < 10) seconds = '0' + seconds.toString();
  68. return minute + ':' + seconds;
  69. }
  70. },
  71. methods: {
  72. show: function() {
  73. if (!this.appId || !this.secretId || !this.secretKey) {
  74. uni.showToast({
  75. icon: 'none',
  76. title: '缺少腾讯配置参数'
  77. });
  78. return;
  79. }
  80. // #ifdef H5
  81. uni.showToast({
  82. icon: 'none',
  83. title: '暂不支持H5平台'
  84. });
  85. return;
  86. // #endif
  87. this.status = 0;
  88. this.content = '';
  89. this.isShow = true;
  90. this.isAnimation = true;
  91. },
  92. hide: function() {
  93. this.stop();
  94. this.isAnimation = false;
  95. setTimeout(() => {
  96. this.isShow = false;
  97. }, 500);
  98. },
  99. ok: function() {
  100. this.$emit('change', this.content);
  101. this.hide();
  102. },
  103. cancel: function() {
  104. this.hide();
  105. },
  106. ontouchstart(e) {
  107. console.log('touch start');
  108. if (this.timer) clearTimeout(this.timer);
  109. this.timer = setTimeout(() => {
  110. this.start();
  111. this.timer = null;
  112. }, 600);
  113. },
  114. ontouchend(e) {
  115. console.log('touch end');
  116. if (this.timer) {
  117. console.log('清除定时器');
  118. clearTimeout(this.timer);
  119. this.timer = null;
  120. }
  121. this.stop();
  122. },
  123. getUrl: function() {
  124. const timestamp = parseInt(new Date().getTime() / 1000) - 1;
  125. const params = {
  126. secretid: this.secretId,
  127. timestamp: timestamp,
  128. expired: timestamp + 60 * 60,
  129. nonce: timestamp,
  130. engine_model_type: '16k_zh',
  131. voice_id: timestamp.toString(),
  132. voice_format: 8
  133. };
  134. const url =
  135. 'asr.cloud.tencent.com/asr/v2/' +
  136. this.appId +
  137. '?' +
  138. Object.keys(params)
  139. .sort(function(a, b) {
  140. return a.localeCompare(b);
  141. })
  142. .map(key => {
  143. return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]);
  144. })
  145. .join('&');
  146. const signature = asrauthentication.signCallback(url, this.secretKey);
  147. return url + '&signature=' + signature;
  148. },
  149. startTimer: function() {
  150. this.duration = 0;
  151. this.durationTimer = setInterval(() => {
  152. this.duration += 1;
  153. }, 1000);
  154. },
  155. stopTimer: function() {
  156. if (this.durationTimer) {
  157. clearInterval(this.durationTimer);
  158. this.durationTimer = null;
  159. }
  160. },
  161. start: function() {
  162. // #ifdef APP-PLUS
  163. this.startRecorder();
  164. // #endif
  165. // #ifdef MP-WEIXIN
  166. this.startConnect();
  167. // #endif
  168. },
  169. stop: function() {
  170. // #ifdef APP-PLUS
  171. this.stopRecorder();
  172. // #endif
  173. // #ifdef MP-WEIXIN
  174. this.stopConnect();
  175. // #endif
  176. },
  177. startConnect: function() {
  178. this.status = 1;
  179. const socket = (this.socket = uni.connectSocket({
  180. url: 'wss://' + this.getUrl(),
  181. success: data => {
  182. console.log('socket connect result ', data);
  183. },
  184. fail: e => {
  185. this.loading(JSON.stringify(e));
  186. }
  187. }));
  188. socket.onOpen(() => {
  189. console.log('socket open');
  190. });
  191. socket.onMessage(({ data }) => {
  192. console.log('socket message', data);
  193. if (typeof data === 'string') data = JSON.parse(data);
  194. if (data.code == 0 && data.result) {
  195. if (data.result.voice_text_str) {
  196. console.log('识别成功:' + data.result.voice_text_str);
  197. this.content = data.result.voice_text_str;
  198. }
  199. } else if (data.code == 0) {
  200. this.status = 2;
  201. this.startRecorder();
  202. } else {
  203. this.loading(data.message);
  204. }
  205. });
  206. socket.onClose(e => {
  207. console.log('socket close');
  208. this.stopRecorder();
  209. this.socket = null;
  210. });
  211. socket.onError(e => {
  212. console.log('socket error', e);
  213. this.stopRecorder();
  214. this.socket = null;
  215. });
  216. },
  217. stopConnect: function() {
  218. if (this.socket) {
  219. this.socket.close();
  220. }
  221. },
  222. startRecorder: function() {
  223. if (this.recorder == null) {
  224. const recorder = (this.recorder = uni.getRecorderManager());
  225. recorder.onFrameRecorded(({ isLastFrame, frameBuffer }) => {
  226. if (this.socket) {
  227. this.socket.send({
  228. data: frameBuffer
  229. });
  230. if (isLastFrame) {
  231. this.socket.send({
  232. data: JSON.stringify({
  233. type: 'end'
  234. })
  235. });
  236. }
  237. }
  238. });
  239. recorder.onError(({ errMsg }) => {
  240. console.log('recorder error', errMsg);
  241. if (errMsg != "operateRecorder:fail:audio is stop, don't stop record again") {
  242. this.loading('启动失败:' + errMsg);
  243. this.stopConnect();
  244. }
  245. });
  246. recorder.onStart(() => {
  247. console.log('recorder start');
  248. //this.loading('正在识别');
  249. this.status = 3;
  250. this.startTimer();
  251. });
  252. recorder.onStop(({ tempFilePath }) => {
  253. console.log('recorder stop', tempFilePath);
  254. // #ifdef APP-PLUS
  255. if (this.uploadFile)
  256. this.uploadFile(tempFilePath)
  257. .then(res => {
  258. this.content = res;
  259. this.$emit('fileChange', { file: tempFilePath, content: res });
  260. })
  261. .catch(e => {
  262. console.log(e);
  263. });
  264. // #endif
  265. // #ifdef MP-WEIXIN
  266. this.$emit('fileChange', { file: tempFilePath, content: this.content });
  267. // #endif
  268. });
  269. recorder.onPause(e => {
  270. console.log('recorder pause');
  271. });
  272. }
  273. this.recorder.start({
  274. duration: 60 * 1000,
  275. format: 'mp3',
  276. frameSize: 1.25,
  277. sampleRate: 16000,
  278. numberOfChannels: 1
  279. });
  280. },
  281. stopRecorder: function() {
  282. this.stopTimer();
  283. if (this.status != 0) {
  284. this.status = 0;
  285. }
  286. if (this.recorder) {
  287. this.recorder.stop();
  288. }
  289. },
  290. loading(title) {
  291. uni.showToast({
  292. icon: 'none',
  293. title: title
  294. });
  295. }
  296. }
  297. };
  298. </script>
  299. <style lang="scss">
  300. .asr-container {
  301. height: calc(100vh - var(--window-top));
  302. width: 750rpx;
  303. position: fixed;
  304. bottom: 0;
  305. }
  306. .asr-playing {
  307. background: url(data:image/gif;base64,R0lGODlhDgAOAIABACWbJP///yH/C05FVFNDQVBFMi4wAwEAAAAh/wtYTVAgRGF0YVhNUDw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNS1jMDE0IDc5LjE1MTQ4MSwgMjAxMy8wMy8xMy0xMjowOToxNSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIChXaW5kb3dzKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpFNjUzM0MxOTMxMEQxMUU2OEUwRkQ0NTk5RTVERjg2OCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpFNjUzM0MxQTMxMEQxMUU2OEUwRkQ0NTk5RTVERjg2OCI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkU2NTMzQzE3MzEwRDExRTY4RTBGRDQ1OTlFNURGODY4IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkU2NTMzQzE4MzEwRDExRTY4RTBGRDQ1OTlFNURGODY4Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkECSgAAQAsAAAAAA4ADgAAAh6Mj6mrAIwcPJLJuu6zDwesfZ0ziiZZhuiZkiyrtgUAIfkECSgAAQAsAAAAAA4ADgAAAh6Mj6nL7QjAiocqGyjO0ujugR8kSmN0mmrKcez3qgUAIfkEBSgAAQAsAAAAAA4ADgAAAh2Mj6nL7Q8VAHAua3CmWgcLTtz4jWGJiuSppitQAAA7)
  308. repeat-x;
  309. height: 60rpx;
  310. background-size: contain;
  311. }
  312. .startBtn {
  313. transition: all 0.3s;
  314. cursor: pointer;
  315. &:hover {
  316. filter: contrast(1.1);
  317. }
  318. &:active {
  319. filter: contrast(0.9);
  320. }
  321. &::before,
  322. &::after {
  323. content: '';
  324. position: absolute;
  325. top: -10px;
  326. left: -10px;
  327. right: -10px;
  328. bottom: -10px;
  329. border: 2px solid #007aff;
  330. transition: all 0.5s;
  331. animation: clippath 3s infinite linear;
  332. border-radius: 10px;
  333. }
  334. &::after {
  335. animation: clippath 3s infinite -1.5s linear;
  336. }
  337. }
  338. @keyframes clippath {
  339. 0%,
  340. 100% {
  341. clip-path: inset(0 0 98% 0);
  342. }
  343. 25% {
  344. clip-path: inset(0 98% 0 0);
  345. }
  346. 50% {
  347. clip-path: inset(98% 0 0 0);
  348. }
  349. 75% {
  350. clip-path: inset(0 0 0 98%);
  351. }
  352. }
  353. </style>