123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303 |
- <template>
- <s-layout class="chat-wrap" title="语音转文字">
- <view class="chat-container">
- <view class="messages" :style="{height:messagesHeight+'px'}" id="messages">
- <view v-for="(message, index) in messages" :key="index" class="message" @click="playRecording(index)">
- <view class="bubble">
- <text class="duration">{{ message.duration }}" </text><text class="ss-m-l-10">
- <image src="@/static/icon/audioPaly.png" class="audioPaly" />
- </text>
- </view>
- <view class="text" v-if="message.transcription != null && message.transcription != ''">
- {{message.transcription}}
- </view>
- </view>
- </view>
- <view class="input-area">
- <button @mousedown="startRecording" @mouseup="stopRecording" @mouseleave="cancelRecording"
- @touchstart="startRecording" @touchend="stopRecording" @touchcancel="cancelRecording"
- @touchmove="handleTouchMove">
- 按住 说话
- </button>
- <view v-if="isRecording" class="recording-overlay">
- <view>{{ recordingDuration }}</view>
- <view>上滑至此取消</view>
- </view>
- </view>
- </view>
- </s-layout>
- </template>
- <script setup>
- import {
- ref,
- nextTick,
- onMounted
- } from 'vue';
- import sheep from '@/sheep';
- import VoiceApi from '@/sheep/api/system/voice';
- const {
- safeAreaInsets,
- safeArea
- } = sheep.$platform.device;
- const sysNavBar = sheep.$platform.navbar;
- const messagesHeight = (safeArea.height) - sysNavBar - 20 - 60;
- const messages = ref([]);
- const isRecording = ref(false);
- let startTime = ref(null);
- const recordingDuration = ref('');
- const minRecordingTime = 500; // 设置最小录音时间为500毫秒
- let intervalId = null;
- const cancelOffset = 280; // 上滑取消的距离阈值
- const startTouchY = ref(0);
- let mediaRecorder = null;
- let audioChunks = [];
- let isCancelled = ref(false);
- // 初始化录音功能
- onMounted(() => {
- navigator.mediaDevices.getUserMedia({
- audio: true
- })
- .then(stream => {
- mediaRecorder = new MediaRecorder(stream);
- mediaRecorder.ondataavailable = (event) => {
- audioChunks.push(event.data);
- };
- mediaRecorder.onstop = () => {
- if (!isCancelled.value) {
- sendDuration()
- }
- audioChunks = [];
- };
- })
- .catch(error => {
- console.error("Error accessing media devices.", error);
- });
- });
- async function sendAudioToServer(audioBlob,audioUrl,duration,messageIndex) {
- const formData = new FormData();
- formData.append('audio_file', audioBlob);
- try {
- const response = await fetch(import.meta.env.SHOPRO_BASE_URL + '/voice2text/', {
- method: 'POST',
- body: formData,
- });
- const data = await response.json();
- if(data.success){
- messages.value[messageIndex].transcription = data.transcription;
- }
- console.log('Server response:', data);
- } catch (error) {
- console.error('Error sending audio file:', error);
- }
- }
- // 发送录音
- const sendDuration = () => {
- const audioBlob = new Blob(audioChunks, {
- type: 'audio/mpeg'
- });
- const audioUrl = URL.createObjectURL(audioBlob);
- const duration = Math.max(Math.floor((new Date() - startTime.value) / 1000), 1);
- // 先添加消息,但不包含转写文本
- const messageIndex = messages.value.push({
- duration,
- audioUrl,
- transcription: '' // 初始为空
- }) - 1;
- sendAudioToServer(audioBlob,audioUrl,duration,messageIndex);
- nextTick(() => {
- let messagesElement = document.getElementById('messages');
- messagesElement.scrollTop = messagesElement.scrollHeight;
- });
- }
- // 更新录音
- const updateDuration = () => {
- const currentDuration = Math.floor((new Date() - startTime.value) / 1000);
- recordingDuration.value = currentDuration + 's';
- if (currentDuration >= 60) { // 如果到达60秒自动停止录音
- stopRecording(new Event('mouseup'));
- }
- };
- // 开始录音
- const startRecording = (event) => {
- if (!isRecording.value && (event.type === 'mousedown' || event.type === 'touchstart')) {
- startTime.value = new Date();
- isRecording.value = true;
- intervalId = setInterval(updateDuration, 1000); // 每秒更新一次时间
- recordingDuration.value = '1s'; // 开始时显示1秒
- // console.log('开始录音...');
- event.preventDefault();
- startTouchY.value = event.touches ? event.touches[0].clientY : 0;
- audioChunks = [];
- isCancelled.value = false; // 确保开始录音时取消标志为假
- mediaRecorder.start();
- }
- };
- // 停止录音
- const stopRecording = (event) => {
- if (isRecording.value && (event.type === 'mouseup' || event.type === 'touchend' || event.type ===
- 'mouseleave' || event.type === 'touchcancel')) {
- clearInterval(intervalId); // 停止定时器
- isRecording.value = false;
- recordingDuration.value = ''; // 清空显示的时间
- if (new Date() - startTime.value >= minRecordingTime) {
- mediaRecorder.stop();
- } else {
- console.log('录音时间太短,不保存');
- }
- }
- };
- // 取消录音
- const cancelRecording = () => {
- if (isRecording.value) {
- clearInterval(intervalId);
- console.log('录音取消');
- isCancelled.value = true; // 设置取消标志为真
- mediaRecorder.stop();
- resetRecording();
- }
- };
- // 重置录音
- const resetRecording = () => {
- isRecording.value = false;
- recordingDuration.value = 0;
- startTime.value = null;
- audioChunks = [];
- };
- // 处理触摸移动事件
- const handleTouchMove = (event) => {
- // 此处应添加逻辑来检测触摸位置,如果滑动到取消区域,调用 cancelRecording
- const currentTouchY = event.touches[0].clientY;
- if (startTouchY.value - currentTouchY > cancelOffset) { // 检查是否上滑超过阈值
- cancelRecording();
- }
- };
- // 播放录音
- const playRecording = (index) => {
- const message = messages.value[index];
- const audio = new Audio(message.audioUrl);
- audio.play();
- };
- </script>
- <style scoped>
- .chat-container {
- display: flex;
- flex-direction: column;
- overflow-y: auto;
- background-color: #f0f0f0;
- padding: 10px;
- }
- .messages {
- flex-grow: 1;
- overflow-y: auto;
- }
- .message {
- display: flex;
- justify-content: flex-end;
- align-items: flex-end;
- margin-bottom: 10px;
- flex-direction: column;
- }
- .text{
- width: 70%;
- background: white;
- border-radius: 5px;
- padding: 10px;
- margin: 5px 0 10px;
- box-sizing: border-box;
- font-size: 16px;
- }
- .bubble {
- width:100px;
- padding: 10px 20px 10px 20px;
- justify-content: flex-end;
- margin-right: 10px;
- background-color: rgb(131 235 96);
- border-radius: 5px;
- display: flex;
- align-items: right;
- position: relative;
- }
- .bubble::after {
- content: "";
- width: 0px;
- height: 0px;
- border-top: 5px solid transparent;
- border-bottom: 5px solid transparent;
- border-left: 5px solid #83eb60;
- position: absolute;
- top: 14px;
- right: -5px;
- }
- .duration {
- color: #000;
- font-size: 16px;
- }
- .audioPaly {
- width: 12px;
- height: 12px;
- }
- .input-area {
- position: fixed;
- bottom: 0;
- left: 0;
- width: 100%;
- background-color: rgb(245, 245, 245);
- display: flex;
- justify-content: center;
- padding: 10px;
- box-sizing: border-box;
- }
- .input-area>button {
- display: block;
- width: 100%;
- background-color: #FFFFFF;
- border: none;
- padding: 3px 0;
- border-radius: 5px;
- color: #333333;
- font-size: 16px;
- font-weight: bold;
- text-align: center;
- outline: none;
- cursor: pointer;
- }
- .input-area>.button-hover {
- background-color: #f0f0f0 !important;
- }
- .input-area>button:after {
- border: none;
- }
- .recording-overlay {
- position: fixed;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- background-color: rgba(0, 0, 0, 0.7);
- color: white;
- padding: 20px;
- border-radius: 10px;
- text-align: center;
- }
- .recording-overlay view {
- margin: 5px 0;
- }
- </style>
|