diff --git a/speakr/src/api/voiceprint.py b/speakr/src/api/voiceprint.py index ea4b2ff..ad4e6c9 100644 --- a/speakr/src/api/voiceprint.py +++ b/speakr/src/api/voiceprint.py @@ -3,9 +3,10 @@ import os import tempfile -import mimetypes -import subprocess -from pathlib import Path +import mimetypes +import subprocess +import uuid +from pathlib import Path from datetime import datetime from flask import Blueprint, request, jsonify, send_file, current_app @@ -95,7 +96,42 @@ def convert_to_wav(input_path): raise Exception(f"音频转换错误: {str(e)}") -@voiceprint_bp.route('/api/voiceprints', methods=['GET']) +def _safe_audio_extension(uploaded_file): + filename = secure_filename(uploaded_file.filename or "") + suffix = Path(filename).suffix.lower() + if suffix: + return suffix + + content_type = (uploaded_file.content_type or "").lower() + if "webm" in content_type: + return ".webm" + if "wav" in content_type or "wave" in content_type: + return ".wav" + if "mpeg" in content_type or "mp3" in content_type: + return ".mp3" + if "ogg" in content_type: + return ".ogg" + if "mp4" in content_type or "m4a" in content_type: + return ".m4a" + return ".webm" + + +def _make_temp_audio_path(upload_dir, uploaded_file): + ext = _safe_audio_extension(uploaded_file) + filename = secure_filename( + f"temp_{datetime.now().strftime('%Y%m%d%H%M%S%f')}_{uuid.uuid4().hex}{ext}" + ) + return os.path.join(upload_dir, filename) + + +def _make_final_voiceprint_path(upload_dir, name): + filename = secure_filename( + f"{name}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}_{uuid.uuid4().hex[:8]}.wav" + ) + return os.path.join(upload_dir, filename) + + +@voiceprint_bp.route('/api/voiceprints', methods=['GET']) @login_required def get_voiceprints(): """获取声纹列表""" @@ -141,8 +177,7 @@ def create_voiceprint(): os.makedirs(voiceprint_dir, exist_ok=True) # 保存原始音频文件到临时位置 - temp_filename = secure_filename(f"temp_{datetime.now().strftime('%Y%m%d%H%M%S')}_{file.filename}") - temp_filepath = os.path.join(voiceprint_dir, temp_filename) + temp_filepath = _make_temp_audio_path(voiceprint_dir, file) file.save(temp_filepath) original_file_path = temp_filepath @@ -150,8 +185,7 @@ def create_voiceprint(): converted_file_path = convert_to_wav(temp_filepath) # 保存转换后的文件 - final_filename = secure_filename(f"{name}_{datetime.now().strftime('%Y%m%d%H%M%S')}.wav") - final_filepath = os.path.join(voiceprint_dir, final_filename) + final_filepath = _make_final_voiceprint_path(voiceprint_dir, name) # 如果转换后的文件不是原始文件,则移动 if converted_file_path != temp_filepath: @@ -242,8 +276,7 @@ def update_voiceprint(voiceprint_id): os.makedirs(voiceprint_dir, exist_ok=True) # 保存原始音频文件到临时位置 - temp_filename = secure_filename(f"temp_{datetime.now().strftime('%Y%m%d%H%M%S')}_{file.filename}") - temp_filepath = os.path.join(voiceprint_dir, temp_filename) + temp_filepath = _make_temp_audio_path(voiceprint_dir, file) file.save(temp_filepath) original_file_path = temp_filepath @@ -251,8 +284,7 @@ def update_voiceprint(voiceprint_id): converted_file_path = convert_to_wav(temp_filepath) # 保存转换后的文件 - final_filename = secure_filename(f"{name}_{datetime.now().strftime('%Y%m%d%H%M%S')}.wav") - final_filepath = os.path.join(voiceprint_dir, final_filename) + final_filepath = _make_final_voiceprint_path(voiceprint_dir, name) # 如果转换后的文件不是原始文件,则移动 if converted_file_path != temp_filepath: diff --git a/speakr/templates/account.html b/speakr/templates/account.html index 939377e..83c492e 100644 --- a/speakr/templates/account.html +++ b/speakr/templates/account.html @@ -1109,7 +1109,7 @@ style="background-color: #fff;border-radius: 4px !important;"> -->
@@ -1791,10 +1791,11 @@ let voices = []; // 存储声纹数据 let editIndex = null; let deleteIndex = null; - let mediaRecorder; - let audioChunks = []; - let isRecording = false; - let modetypes = "add" + let mediaRecorder; + let audioChunks = []; + let isRecording = false; + let isSavingVoice = false; + let modetypes = "add" let modechooseIndex = 0 // 打开添加/编辑弹框 async function openModal(mode, index) { @@ -1851,8 +1852,11 @@ async function synchronize() { } // 保存声纹信息 - async function saveVoice() { - const name = document.getElementById('voiceName').value.trim(); + async function saveVoice() { + if (isSavingVoice) { + return; + } + const name = document.getElementById('voiceName').value.trim(); // const desc // = document.getElementById('voiceDesc').value.trim(); const fileInput = document.getElementById('voiceFile').files[0]; @@ -1879,9 +1883,17 @@ console.log("audioBlob:", audioBlob); // 使用 FormData 发送音频文件 - const formData = new FormData(); - formData.append("name", name); - formData.append("voice_data", audioBlob); + const formData = new FormData(); + formData.append("name", name); + const uploadFileName = fileInput && fileInput.name ? fileInput.name : `voiceprint_${Date.now()}.webm`; + formData.append("voice_data", audioBlob, uploadFileName); + isSavingVoice = true; + const saveBtn = document.getElementById('saveVoiceBtn'); + if (saveBtn) { + saveBtn.disabled = true; + saveBtn.dataset.originalText = saveBtn.textContent; + saveBtn.textContent = '保存中...'; + } if (modetypes == 'add') { try { @@ -1901,9 +1913,15 @@ console.log("audioBlob:", audioBlob); } catch (err) { // console.error("上传出错:", err); // alert("上传失败:" + err.message); - showMessage(`${err.message}`, 'error'); - - } + showMessage(`${err.message}`, 'error'); + + } finally { + isSavingVoice = false; + if (saveBtn) { + saveBtn.disabled = false; + saveBtn.textContent = saveBtn.dataset.originalText || '保存'; + } + } } else { try { let v = voices[modechooseIndex] @@ -1926,8 +1944,14 @@ console.log("audioBlob:", audioBlob); } catch (err) { // console.error("上传出错:", err); // alert("上传失败:" + err.message); - showMessage(`${err.message}`, 'error'); - } + showMessage(`${err.message}`, 'error'); + } finally { + isSavingVoice = false; + if (saveBtn) { + saveBtn.disabled = false; + saveBtn.textContent = saveBtn.dataset.originalText || '保存'; + } + } } } function showMessage(msg, type, duration = 3000) {