speechtotext.vue 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. <template>
  2. <s-layout class="chat-wrap" title="语音转文字">
  3. <view class="chat-container">
  4. <view class="messages" :style="{height:messagesHeight+'px'}" id="messages">
  5. <view v-for="(message, index) in messages" :key="index" class="message" @click="playRecording(index)">
  6. <view class="bubble">
  7. <text class="duration">{{ message.duration }}" </text><text class="ss-m-l-10">
  8. <image src="@/static/icon/audioPaly.png" class="audioPaly" />
  9. </text>
  10. </view>
  11. <view class="text" v-if="message.transcription != null && message.transcription != ''">
  12. {{message.transcription}}
  13. </view>
  14. </view>
  15. </view>
  16. <view class="input-area">
  17. <button @mousedown="startRecording" @mouseup="stopRecording" @mouseleave="cancelRecording"
  18. @touchstart="startRecording" @touchend="stopRecording" @touchcancel="cancelRecording"
  19. @touchmove="handleTouchMove">
  20. 按住 说话
  21. </button>
  22. <view v-if="isRecording" class="recording-overlay">
  23. <view>{{ recordingDuration }}</view>
  24. <view>上滑至此取消</view>
  25. </view>
  26. </view>
  27. </view>
  28. </s-layout>
  29. </template>
  30. <script setup>
  31. import {
  32. ref,
  33. nextTick,
  34. onMounted
  35. } from 'vue';
  36. import sheep from '@/sheep';
  37. import VoiceApi from '@/sheep/api/system/voice';
  38. const {
  39. safeAreaInsets,
  40. safeArea
  41. } = sheep.$platform.device;
  42. const sysNavBar = sheep.$platform.navbar;
  43. const messagesHeight = (safeArea.height) - sysNavBar - 20 - 60;
  44. const messages = ref([]);
  45. const isRecording = ref(false);
  46. let startTime = ref(null);
  47. const recordingDuration = ref('');
  48. const minRecordingTime = 500; // 设置最小录音时间为500毫秒
  49. let intervalId = null;
  50. const cancelOffset = 280; // 上滑取消的距离阈值
  51. const startTouchY = ref(0);
  52. let mediaRecorder = null;
  53. let audioChunks = [];
  54. let isCancelled = ref(false);
  55. // 初始化录音功能
  56. onMounted(() => {
  57. navigator.mediaDevices.getUserMedia({
  58. audio: true
  59. })
  60. .then(stream => {
  61. mediaRecorder = new MediaRecorder(stream);
  62. mediaRecorder.ondataavailable = (event) => {
  63. audioChunks.push(event.data);
  64. };
  65. mediaRecorder.onstop = () => {
  66. if (!isCancelled.value) {
  67. sendDuration()
  68. }
  69. audioChunks = [];
  70. };
  71. })
  72. .catch(error => {
  73. console.error("Error accessing media devices.", error);
  74. });
  75. });
  76. async function sendAudioToServer(audioBlob,audioUrl,duration,messageIndex) {
  77. const formData = new FormData();
  78. formData.append('audio_file', audioBlob);
  79. try {
  80. const response = await fetch(import.meta.env.SHOPRO_BASE_URL + '/voice2text/', {
  81. method: 'POST',
  82. body: formData,
  83. });
  84. const data = await response.json();
  85. if(data.success){
  86. messages.value[messageIndex].transcription = data.transcription;
  87. }
  88. console.log('Server response:', data);
  89. } catch (error) {
  90. console.error('Error sending audio file:', error);
  91. }
  92. }
  93. // 发送录音
  94. const sendDuration = () => {
  95. const audioBlob = new Blob(audioChunks, {
  96. type: 'audio/mpeg'
  97. });
  98. const audioUrl = URL.createObjectURL(audioBlob);
  99. const duration = Math.max(Math.floor((new Date() - startTime.value) / 1000), 1);
  100. // 先添加消息,但不包含转写文本
  101. const messageIndex = messages.value.push({
  102. duration,
  103. audioUrl,
  104. transcription: '' // 初始为空
  105. }) - 1;
  106. sendAudioToServer(audioBlob,audioUrl,duration,messageIndex);
  107. nextTick(() => {
  108. let messagesElement = document.getElementById('messages');
  109. messagesElement.scrollTop = messagesElement.scrollHeight;
  110. });
  111. }
  112. // 更新录音
  113. const updateDuration = () => {
  114. const currentDuration = Math.floor((new Date() - startTime.value) / 1000);
  115. recordingDuration.value = currentDuration + 's';
  116. if (currentDuration >= 60) { // 如果到达60秒自动停止录音
  117. stopRecording(new Event('mouseup'));
  118. }
  119. };
  120. // 开始录音
  121. const startRecording = (event) => {
  122. if (!isRecording.value && (event.type === 'mousedown' || event.type === 'touchstart')) {
  123. startTime.value = new Date();
  124. isRecording.value = true;
  125. intervalId = setInterval(updateDuration, 1000); // 每秒更新一次时间
  126. recordingDuration.value = '1s'; // 开始时显示1秒
  127. // console.log('开始录音...');
  128. event.preventDefault();
  129. startTouchY.value = event.touches ? event.touches[0].clientY : 0;
  130. audioChunks = [];
  131. isCancelled.value = false; // 确保开始录音时取消标志为假
  132. mediaRecorder.start();
  133. }
  134. };
  135. // 停止录音
  136. const stopRecording = (event) => {
  137. if (isRecording.value && (event.type === 'mouseup' || event.type === 'touchend' || event.type ===
  138. 'mouseleave' || event.type === 'touchcancel')) {
  139. clearInterval(intervalId); // 停止定时器
  140. isRecording.value = false;
  141. recordingDuration.value = ''; // 清空显示的时间
  142. if (new Date() - startTime.value >= minRecordingTime) {
  143. mediaRecorder.stop();
  144. } else {
  145. console.log('录音时间太短,不保存');
  146. }
  147. }
  148. };
  149. // 取消录音
  150. const cancelRecording = () => {
  151. if (isRecording.value) {
  152. clearInterval(intervalId);
  153. console.log('录音取消');
  154. isCancelled.value = true; // 设置取消标志为真
  155. mediaRecorder.stop();
  156. resetRecording();
  157. }
  158. };
  159. // 重置录音
  160. const resetRecording = () => {
  161. isRecording.value = false;
  162. recordingDuration.value = 0;
  163. startTime.value = null;
  164. audioChunks = [];
  165. };
  166. // 处理触摸移动事件
  167. const handleTouchMove = (event) => {
  168. // 此处应添加逻辑来检测触摸位置,如果滑动到取消区域,调用 cancelRecording
  169. const currentTouchY = event.touches[0].clientY;
  170. if (startTouchY.value - currentTouchY > cancelOffset) { // 检查是否上滑超过阈值
  171. cancelRecording();
  172. }
  173. };
  174. // 播放录音
  175. const playRecording = (index) => {
  176. const message = messages.value[index];
  177. const audio = new Audio(message.audioUrl);
  178. audio.play();
  179. };
  180. </script>
  181. <style scoped>
  182. .chat-container {
  183. display: flex;
  184. flex-direction: column;
  185. overflow-y: auto;
  186. background-color: #f0f0f0;
  187. padding: 10px;
  188. }
  189. .messages {
  190. flex-grow: 1;
  191. overflow-y: auto;
  192. }
  193. .message {
  194. display: flex;
  195. justify-content: flex-end;
  196. align-items: flex-end;
  197. margin-bottom: 10px;
  198. flex-direction: column;
  199. }
  200. .text{
  201. width: 70%;
  202. background: white;
  203. border-radius: 5px;
  204. padding: 10px;
  205. margin: 5px 0 10px;
  206. box-sizing: border-box;
  207. font-size: 16px;
  208. }
  209. .bubble {
  210. width:100px;
  211. padding: 10px 20px 10px 20px;
  212. justify-content: flex-end;
  213. margin-right: 10px;
  214. background-color: rgb(131 235 96);
  215. border-radius: 5px;
  216. display: flex;
  217. align-items: right;
  218. position: relative;
  219. }
  220. .bubble::after {
  221. content: "";
  222. width: 0px;
  223. height: 0px;
  224. border-top: 5px solid transparent;
  225. border-bottom: 5px solid transparent;
  226. border-left: 5px solid #83eb60;
  227. position: absolute;
  228. top: 14px;
  229. right: -5px;
  230. }
  231. .duration {
  232. color: #000;
  233. font-size: 16px;
  234. }
  235. .audioPaly {
  236. width: 12px;
  237. height: 12px;
  238. }
  239. .input-area {
  240. position: fixed;
  241. bottom: 0;
  242. left: 0;
  243. width: 100%;
  244. background-color: rgb(245, 245, 245);
  245. display: flex;
  246. justify-content: center;
  247. padding: 10px;
  248. box-sizing: border-box;
  249. }
  250. .input-area>button {
  251. display: block;
  252. width: 100%;
  253. background-color: #FFFFFF;
  254. border: none;
  255. padding: 3px 0;
  256. border-radius: 5px;
  257. color: #333333;
  258. font-size: 16px;
  259. font-weight: bold;
  260. text-align: center;
  261. outline: none;
  262. cursor: pointer;
  263. }
  264. .input-area>.button-hover {
  265. background-color: #f0f0f0 !important;
  266. }
  267. .input-area>button:after {
  268. border: none;
  269. }
  270. .recording-overlay {
  271. position: fixed;
  272. top: 50%;
  273. left: 50%;
  274. transform: translate(-50%, -50%);
  275. background-color: rgba(0, 0, 0, 0.7);
  276. color: white;
  277. padding: 20px;
  278. border-radius: 10px;
  279. text-align: center;
  280. }
  281. .recording-overlay view {
  282. margin: 5px 0;
  283. }
  284. </style>