diff --git a/__pycache__/server.cpython-310.pyc b/__pycache__/server.cpython-310.pyc new file mode 100644 index 0000000..f0f53b4 Binary files /dev/null and b/__pycache__/server.cpython-310.pyc differ diff --git a/cert.pem b/cert.pem new file mode 100644 index 0000000..078c40a --- /dev/null +++ b/cert.pem @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIFCTCCAvGgAwIBAgIUBWu1dbsZGPwTcg/pzECc8otDEr4wDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDQyMTEwMjgyMVoXDTI3MDQy +MTEwMjgyMVowFDESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEA79zUZ4lGsVwv/1bJHq6xkKeszWDrC4qeHuiNOLX/7MCK +zk/GEcbRbTp1TYZg0+g+ixEmpaXa3jxhaYCVwMpinLfgpfL6FNmNPtxocXdNYm7K +s0+czmiaaBiutNluXC0az8QYt/BR00FwHOFuj3wX0olrUMWLhGELtRO921+9NF1W +GDYpnOo1smOHyIXuF/XboRQt2BlWEg6NKgXWUjqSDfzBan/aESlSFg0pLHzsYC2O +61hRn47LhWKhZ7tdSyLrSEVhnlXApjVDOsd7ZHUbY7/r3/tJ+DJXUTIAarpUnupO +SOZh1NtQPpg9wa2KPeWlF1yNEDkvLlER3kqB/nOqGhxh7u5VGXR9R9ZoFUsHsuLP +ru5d+UCWakBROSKc0K0vidGiZKqaiIfyTgpnvov+7nyL8y6QatQJ8bqCrPF91otn +WjWfr5Xr+iNyWnF/SP9Hoem693+wwL+7StmyfcS/1wChiqNFgceWbMK+0dGiqE9O +zoXQUuTmR3VCZ1pJaSZPqa4icmoYlAO99leqNE/SYvUk3LGs2pelDVIcfPSXgd1R +sbqt66EKMwwE2faQcMeNqNsFQXJ0bnJGKH7nZKevn/pkdG/F9G/KtyIbglqU7hM0 +7vIVKBbuu23fUfAFjvoTForjD/MGYoZguLbzccfGi9Dmd/Ge4es3ej25wnlrPO0C +AwEAAaNTMFEwHQYDVR0OBBYEFI861NylCN4wI9WNQD3+5U4YGAmyMB8GA1UdIwQY +MBaAFI861NylCN4wI9WNQD3+5U4YGAmyMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI +hvcNAQELBQADggIBADMWyaukkWDtyukXsRoaD3xikLSEUPaCU9QdQ1S9WRqOtAI4 +oqgngSEIYgyRyfkOMTY8LoQgjNXW+je0poVE4yPrdW6CA0VZrr4uWY/HvFn9+JL3 +5GhNiH8JjeJBnsVw3DA9fG+B0BhYmRxQqei9HDU8QSE0J9eaQUrNftXRoFOOQg3k +8rXj6BTZaIitsw/YHNSdnvDECqAxPam0BwqQXx/U0IadZ3AZvdJBf0uad0yAFkFU +7fJStheEEbjva14P4Tuthoh53uSyiTsZm1OBgJkauaXNhmjKijb9J+AfYYEV1lHL +R1TPm3p3KsZSjYLH8tkjO8ns+o81AmMGMzIrpM0qrlO4uSPjtz80a/eFe6LPIIgr +FKs0ZOWhuP7eA/o/TxPqQXoTHFDuAhxg37NfeveAtEGDW1yAcadkLyswDfCm/XUV +JJXbyySOaCw6sVOmbbl5LzVt+EJryM9YwCUQ15sBFwg6DXxxfDdNtLObjgaI+iL2 +5Zqs6DmXYt/YMhChIPDIZN047pIbxRLJjLwmcmynLlQLlwsbL3ljqN3aB6zp66sT +mBKdlHnSHZW+ExRR1eG3wW3i8GzfIt6t8Rd/YfV90edoQtsqEa5G62rkh+C6hDvm +HREqe6efjDZd0yppNtYjGrWH1LG9Caw1NgBH4XvO//rHN12GkxCiLRp24URU +-----END CERTIFICATE----- diff --git a/key.pem b/key.pem new file mode 100644 index 0000000..b5738d8 --- /dev/null +++ b/key.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +pvgmLr3zMMOd7hLpj+sXBsd3L6YLOw== +-----END PRIVATE KEY----- diff --git a/logs/server.log b/logs/server.log new file mode 100644 index 0000000..4dc08ec Binary files /dev/null and b/logs/server.log differ diff --git a/main.py b/main.py index 647173d..d8731e5 100644 --- a/main.py +++ b/main.py @@ -39,4 +39,11 @@ async def index(): if __name__ == "__main__": PORT = int(os.getenv("PORT", "19019")) - uvicorn.run(app, host="0.0.0.0", port=PORT) \ No newline at end of file + SSL_KEY = os.getenv("SSL_KEY", "key.pem") + SSL_CERT = os.getenv("SSL_CERT", "cert.pem") + + # 检查是否有 SSL 证书 + if os.path.exists(SSL_KEY) and os.path.exists(SSL_CERT): + uvicorn.run(app, host="0.0.0.0", port=PORT, ssl_keyfile=SSL_KEY, ssl_certfile=SSL_CERT) + else: + uvicorn.run(app, host="0.0.0.0", port=PORT) \ No newline at end of file diff --git a/server.py b/server.py index 4bd9fb2..f9d2a0b 100644 --- a/server.py +++ b/server.py @@ -11,7 +11,6 @@ from datetime import datetime import aiohttp from fastapi import FastAPI, UploadFile, File, HTTPException, Form from fastapi.middleware.cors import CORSMiddleware -from fastapi.staticfiles import StaticFiles from pydantic import BaseModel # 配置 @@ -58,7 +57,7 @@ async def root(): return {"status": "ok", "service": "voice-chat-web"} -@app.get("/api/status", response_model=StatusResponse) +@app.get("/status", response_model=StatusResponse) async def get_status(): """检查服务状态""" try: @@ -81,7 +80,7 @@ async def get_status(): ) -@app.post("/api/voice/chat", response_model=VoiceResponse) +@app.post("/voice/chat", response_model=VoiceResponse) async def voice_chat( audio: UploadFile = File(..., description="音频文件"), conversation_id: Optional[str] = Form(None, description="对话ID") @@ -131,7 +130,7 @@ async def voice_chat( raise HTTPException(status_code=500, detail=str(e)) -@app.delete("/api/conversation/{conversation_id}") +@app.delete("/conversation/{conversation_id}") async def delete_conversation(conversation_id: str): """删除对话""" try: @@ -146,10 +145,6 @@ async def delete_conversation(conversation_id: str): raise HTTPException(status_code=500, detail=str(e)) -# 静态文件(前端页面) -app.mount("/static", StaticFiles(directory="static"), name="static") - - if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=PORT) \ No newline at end of file diff --git a/static/index.html b/static/index.html index fd38b9c..9fc7290 100644 --- a/static/index.html +++ b/static/index.html @@ -304,6 +304,9 @@ let audioChunks = []; let conversationId = null; let audioContext = null; + let audioStream = null; + let scriptProcessor = null; + let recordedBuffers = []; // 元素 const recordBtn = document.getElementById('recordBtn'); @@ -333,10 +336,58 @@ } } + // 创建 WAV 文件 + function createWavFile(audioBuffer, sampleRate = 16000) { + const numChannels = 1; + const bitsPerSample = 16; + const bytesPerSample = bitsPerSample / 8; + const blockAlign = numChannels * bytesPerSample; + const byteRate = sampleRate * blockAlign; + const dataSize = audioBuffer.length * bytesPerSample; + const headerSize = 44; + const totalSize = headerSize + dataSize; + + const buffer = new ArrayBuffer(totalSize); + const view = new DataView(buffer); + + // WAV header + writeString(view, 0, 'RIFF'); + view.setUint32(4, totalSize - 8, true); + writeString(view, 8, 'WAVE'); + writeString(view, 12, 'fmt '); + view.setUint32(16, 16, true); // fmt chunk size + view.setUint16(20, 1, true); // audio format (PCM) + view.setUint16(22, numChannels, true); + view.setUint32(24, sampleRate, true); + view.setUint32(28, byteRate, true); + view.setUint16(32, blockAlign, true); + view.setUint16(34, bitsPerSample, true); + writeString(view, 36, 'data'); + view.setUint32(40, dataSize, true); + + // 写入音频数据 + floatTo16BitPCM(view, 44, audioBuffer); + + return new Blob([buffer], { type: 'audio/wav' }); + } + + function writeString(view, offset, string) { + for (let i = 0; i < string.length; i++) { + view.setUint8(offset + i, string.charCodeAt(i)); + } + } + + function floatTo16BitPCM(view, offset, input) { + for (let i = 0; i < input.length; i++, offset += 2) { + const s = Math.max(-1, Math.min(1, input[i])); + view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true); + } + } + // 初始化录音 async function initAudio() { try { - const stream = await navigator.mediaDevices.getUserMedia({ + audioStream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, @@ -344,22 +395,21 @@ } }); - audioContext = new (window.AudioContext || window.webkitAudioContext)(); - - // 创建 MediaRecorder - mediaRecorder = new MediaRecorder(stream, { - mimeType: 'audio/webm' + audioContext = new (window.AudioContext || window.webkitAudioContext)({ + sampleRate: 16000 }); - mediaRecorder.ondataavailable = (e) => { - audioChunks.push(e.data); + const source = audioContext.createMediaStreamSource(audioStream); + scriptProcessor = audioContext.createScriptProcessor(4096, 1, 1); + + scriptProcessor.onaudioprocess = (e) => { + if (isRecording) { + recordedBuffers.push(e.inputBuffer.getChannelData(0).slice()); + } }; - mediaRecorder.onstop = async () => { - const audioBlob = new Blob(audioChunks, { type: 'audio/webm' }); - audioChunks = []; - await sendAudio(audioBlob); - }; + source.connect(scriptProcessor); + scriptProcessor.connect(audioContext.destination); return true; } catch (e) { @@ -371,13 +421,12 @@ // 开始录音 async function startRecording() { - if (!mediaRecorder) { + if (!audioContext) { const success = await initAudio(); if (!success) return; } - audioChunks = []; - mediaRecorder.start(); + recordedBuffers = []; isRecording = true; recordBtn.classList.add('recording'); @@ -390,16 +439,29 @@ // 停止录音 function stopRecording() { - if (mediaRecorder && isRecording) { - mediaRecorder.stop(); + if (isRecording) { isRecording = false; + // 合并所有缓冲区 + const totalLength = recordedBuffers.reduce((acc, buf) => acc + buf.length, 0); + const mergedBuffer = new Float32Array(totalLength); + let offset = 0; + for (const buf of recordedBuffers) { + mergedBuffer.set(buf, offset); + offset += buf.length; + } + + // 创建 WAV 文件 + const wavBlob = createWavFile(mergedBuffer, 16000); + recordBtn.classList.remove('recording'); recordBtn.querySelector('.icon').textContent = '🎤'; recordBtn.querySelector('.text').textContent = '点击录音'; recordStatus.textContent = '处理中...'; recordStatus.classList.remove('recording'); waveform.style.display = 'none'; + + sendAudio(wavBlob); } } @@ -409,7 +471,7 @@ showLoading(); const formData = new FormData(); - formData.append('audio', audioBlob, 'recording.webm'); + formData.append('audio', audioBlob, 'recording.wav'); if (conversationId) { formData.append('conversation_id', conversationId); }