Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bcb0fbb384 | ||
|
|
0dced68876 |
Binary file not shown.
Binary file not shown.
@@ -130,6 +130,47 @@ async def voice_chat(
|
|||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/voice/text", response_model=VoiceResponse)
|
||||||
|
async def text_chat(
|
||||||
|
text: str = Form(..., description="文本消息"),
|
||||||
|
conversation_id: Optional[str] = Form(None, description="对话ID")
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
文字聊天接口
|
||||||
|
转发到模型服务
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
form = aiohttp.FormData()
|
||||||
|
form.add_field('text', text)
|
||||||
|
if conversation_id:
|
||||||
|
form.add_field('conversation_id', conversation_id)
|
||||||
|
|
||||||
|
async with session.post(
|
||||||
|
f"{MODEL_SERVICE_URL}/api/voice/text",
|
||||||
|
data=form,
|
||||||
|
timeout=aiohttp.ClientTimeout(total=120)
|
||||||
|
) as resp:
|
||||||
|
if resp.status != 200:
|
||||||
|
error_text = await resp.text()
|
||||||
|
logger.error(f"Model service error: {error_text}")
|
||||||
|
raise HTTPException(status_code=resp.status, detail=error_text)
|
||||||
|
|
||||||
|
data = await resp.json()
|
||||||
|
return VoiceResponse(
|
||||||
|
reply=data["reply"],
|
||||||
|
conversation_id=data["conversation_id"],
|
||||||
|
timestamp=data.get("timestamp", datetime.now().isoformat())
|
||||||
|
)
|
||||||
|
|
||||||
|
except aiohttp.ClientError as e:
|
||||||
|
logger.error(f"Connection error: {e}")
|
||||||
|
raise HTTPException(status_code=503, detail="模型服务连接失败")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Text chat error: {e}", exc_info=True)
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@app.delete("/conversation/{conversation_id}")
|
@app.delete("/conversation/{conversation_id}")
|
||||||
async def delete_conversation(conversation_id: str):
|
async def delete_conversation(conversation_id: str):
|
||||||
"""删除对话"""
|
"""删除对话"""
|
||||||
|
|||||||
+180
-7
@@ -127,6 +127,50 @@
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-section {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-input-wrapper {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px 15px;
|
||||||
|
border: 2px solid #eee;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 15px;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-input:focus {
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-text-btn {
|
||||||
|
padding: 12px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
font-size: 15px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-text-btn:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-text-btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
.waveform {
|
.waveform {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -196,6 +240,40 @@
|
|||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.audio-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 15px;
|
||||||
|
border-radius: 20px;
|
||||||
|
border: none;
|
||||||
|
background: rgba(255,255,255,0.2);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-btn:hover {
|
||||||
|
background: rgba(255,255,255,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-btn.playing {
|
||||||
|
background: rgba(255,255,255,0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.duration {
|
||||||
|
color: rgba(255,255,255,0.8);
|
||||||
|
}
|
||||||
|
|
||||||
.loading {
|
.loading {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -285,6 +363,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="text-section">
|
||||||
|
<div class="text-input-wrapper">
|
||||||
|
<input type="text" id="textInput" placeholder="输入文字消息..." class="text-input">
|
||||||
|
<button id="sendTextBtn" class="send-text-btn">发送</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="chat-section" id="chatSection">
|
<div class="chat-section" id="chatSection">
|
||||||
<div class="hint">开始你的第一次语音对话吧!</div>
|
<div class="hint">开始你的第一次语音对话吧!</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -316,6 +401,46 @@
|
|||||||
const clearBtn = document.getElementById('clearBtn');
|
const clearBtn = document.getElementById('clearBtn');
|
||||||
const statusDot = document.getElementById('statusDot');
|
const statusDot = document.getElementById('statusDot');
|
||||||
const statusText = document.getElementById('statusText');
|
const statusText = document.getElementById('statusText');
|
||||||
|
const textInput = document.getElementById('textInput');
|
||||||
|
const sendTextBtn = document.getElementById('sendTextBtn');
|
||||||
|
|
||||||
|
// 发送文字消息
|
||||||
|
async function sendText(text) {
|
||||||
|
if (!text.trim()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
showLoading();
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('text', text);
|
||||||
|
if (conversationId) {
|
||||||
|
formData.append('conversation_id', conversationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const resp = await fetch(`${API_URL}/voice/text`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!resp.ok) {
|
||||||
|
const error = await resp.text();
|
||||||
|
throw new Error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await resp.json();
|
||||||
|
conversationId = data.conversation_id;
|
||||||
|
|
||||||
|
// 显示消息
|
||||||
|
addMessage('user', text);
|
||||||
|
addMessage('assistant', data.reply);
|
||||||
|
|
||||||
|
textInput.value = '';
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error('发送失败:', e);
|
||||||
|
showError('发送失败: ' + e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 检查服务状态
|
// 检查服务状态
|
||||||
async function checkStatus() {
|
async function checkStatus() {
|
||||||
@@ -470,6 +595,9 @@
|
|||||||
try {
|
try {
|
||||||
showLoading();
|
showLoading();
|
||||||
|
|
||||||
|
// 计算音频时长
|
||||||
|
const duration = Math.round(recordedBuffers.reduce((acc, buf) => acc + buf.length, 0) / 16000);
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('audio', audioBlob, 'recording.wav');
|
formData.append('audio', audioBlob, 'recording.wav');
|
||||||
if (conversationId) {
|
if (conversationId) {
|
||||||
@@ -489,8 +617,8 @@
|
|||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
conversationId = data.conversation_id;
|
conversationId = data.conversation_id;
|
||||||
|
|
||||||
// 显示消息
|
// 显示消息(带音频播放)
|
||||||
addMessage('user', '🎵 语音消息');
|
addMessage('user', audioBlob, duration);
|
||||||
addMessage('assistant', data.reply);
|
addMessage('assistant', data.reply);
|
||||||
|
|
||||||
recordStatus.textContent = '点击按钮开始录音';
|
recordStatus.textContent = '点击按钮开始录音';
|
||||||
@@ -503,7 +631,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 添加消息
|
// 添加消息
|
||||||
function addMessage(role, content) {
|
function addMessage(role, content, audioDuration = null) {
|
||||||
// 移除提示
|
// 移除提示
|
||||||
const hint = chatSection.querySelector('.hint');
|
const hint = chatSection.querySelector('.hint');
|
||||||
if (hint) hint.remove();
|
if (hint) hint.remove();
|
||||||
@@ -514,16 +642,50 @@
|
|||||||
|
|
||||||
const msg = document.createElement('div');
|
const msg = document.createElement('div');
|
||||||
msg.className = `message ${role}`;
|
msg.className = `message ${role}`;
|
||||||
msg.innerHTML = `
|
|
||||||
<div class="role">${role === 'user' ? '我' : 'AI'}</div>
|
// 用户消息可能是音频
|
||||||
<div class="content">${content}</div>
|
if (role === 'user' && content instanceof Blob) {
|
||||||
`;
|
const audioUrl = URL.createObjectURL(content);
|
||||||
|
const durationText = audioDuration ? `${audioDuration}s` : '';
|
||||||
|
msg.innerHTML = `
|
||||||
|
<div class="role">我</div>
|
||||||
|
<div class="content audio-content">
|
||||||
|
<button class="play-btn" onclick="playAudio('${audioUrl}', this)">
|
||||||
|
<span class="play-icon">▶️</span>
|
||||||
|
<span class="duration">${durationText}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
msg.innerHTML = `
|
||||||
|
<div class="role">${role === 'user' ? '我' : 'AI'}</div>
|
||||||
|
<div class="content">${content}</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
chatSection.appendChild(msg);
|
chatSection.appendChild(msg);
|
||||||
|
|
||||||
// 滚动到底部
|
// 滚动到底部
|
||||||
chatSection.scrollTop = chatSection.scrollHeight;
|
chatSection.scrollTop = chatSection.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 播放音频
|
||||||
|
function playAudio(audioUrl, btn) {
|
||||||
|
const audio = new Audio(audioUrl);
|
||||||
|
const icon = btn.querySelector('.play-icon');
|
||||||
|
|
||||||
|
audio.onplay = () => {
|
||||||
|
icon.textContent = '🔊';
|
||||||
|
btn.classList.add('playing');
|
||||||
|
};
|
||||||
|
|
||||||
|
audio.onended = () => {
|
||||||
|
icon.textContent = '▶️';
|
||||||
|
btn.classList.remove('playing');
|
||||||
|
};
|
||||||
|
|
||||||
|
audio.play();
|
||||||
|
}
|
||||||
|
|
||||||
// 显示加载
|
// 显示加载
|
||||||
function showLoading() {
|
function showLoading() {
|
||||||
const hint = chatSection.querySelector('.hint');
|
const hint = chatSection.querySelector('.hint');
|
||||||
@@ -579,6 +741,17 @@
|
|||||||
|
|
||||||
clearBtn.addEventListener('click', clearChat);
|
clearBtn.addEventListener('click', clearChat);
|
||||||
|
|
||||||
|
// 文字输入事件
|
||||||
|
sendTextBtn.addEventListener('click', () => {
|
||||||
|
sendText(textInput.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
textInput.addEventListener('keypress', (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
sendText(textInput.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 初始化
|
// 初始化
|
||||||
checkStatus();
|
checkStatus();
|
||||||
setInterval(checkStatus, 10000); // 每10秒检查状态
|
setInterval(checkStatus, 10000); // 每10秒检查状态
|
||||||
|
|||||||
Reference in New Issue
Block a user