Skip to content

Commit 335ef19

Browse files
committed
feat: add voiceflow
1 parent 06d4282 commit 335ef19

File tree

4 files changed

+214
-76
lines changed

4 files changed

+214
-76
lines changed

Diff for: cmd/voiceflow/web/index.html

+7
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@
1818
<div class="chat-container">
1919
<div class="chat-header">
2020
<h2>VoiceFlow Chat</h2>
21+
<div class="mode-switch">
22+
<label class="switch">
23+
<input type="checkbox" id="mode-toggle">
24+
<span class="slider round"></span>
25+
</label>
26+
<span id="mode-label">persistent mode</span>
27+
</div>
2128
</div>
2229
<div class="chat-window" id="chat-window">
2330
<!-- 消息将被添加到这里 -->

Diff for: cmd/voiceflow/web/script.js

+134-74
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,103 @@
11
// script.js
22
// 获取 WebSocket URL
3-
const ws = new WebSocket(WEBSOCKET_URL);
4-
5-
ws.onopen = () => {
6-
console.log('WebSocket 连接已建立');
7-
// 在聊天窗口添加提示信息
8-
appendSystemMessage('提示:您可以长按麦克风按钮 & 长按 键盘 V 进行录音');
9-
};
10-
11-
ws.onmessage = function(event) {
12-
if (typeof event.data === 'string') {
13-
const response = JSON.parse(event.data);
14-
console.log('收到 WebSocket 响应:', response);
15-
16-
if (response.type) {
17-
// 处理带有 type 字段的消息(语音识别等)
18-
switch(response.type) {
19-
case 'audio_stored':
20-
appendAudioMessage('你', response.audio_url);
21-
break;
22-
23-
case 'recognition_complete':
24-
appendMessage('你', response.text);
25-
break;
26-
27-
case 'recognition_error':
28-
appendSystemMessage(`识别错误: ${response.error}`);
29-
break;
30-
31-
case 'tts_complete':
32-
// 移除"正在生成语音..."的系统消息
33-
const systemMessages = document.querySelectorAll('.message.system');
34-
systemMessages.forEach(msg => {
35-
if (msg.textContent === '正在生成语音...') {
36-
msg.remove();
3+
let ws;
4+
5+
// 创建新的 WebSocket 连接的函数
6+
function createWebSocket() {
7+
ws = new WebSocket(WEBSOCKET_URL);
8+
9+
ws.onopen = () => {
10+
console.log('WebSocket 连接已建立');
11+
appendSystemMessage('提示:您可以长按麦克风按钮 & 长按 键盘 V 进行录音');
12+
};
13+
14+
ws.onmessage = function(event) {
15+
if (typeof event.data === 'string') {
16+
const response = JSON.parse(event.data);
17+
console.log('收到 WebSocket 响应:', response);
18+
19+
if (response.type) {
20+
// 处理带有 type 字段的消息(语音识别等)
21+
switch(response.type) {
22+
case 'audio_stored':
23+
appendAudioMessage('你', response.audio_url);
24+
break;
25+
26+
case 'recognition_complete':
27+
appendMessage('你', response.text);
28+
if (isOneShotMode) {
29+
appendSystemMessage('单次模式:识别完成,连接将关闭');
30+
// 给用户一点时间看到结果
31+
setTimeout(() => {
32+
ws.close();
33+
}, 2000);
3734
}
38-
});
39-
40-
// 添加 AI 的文本和音频消息
41-
appendMessage('AI', response.text);
42-
appendAudioMessage('AI', response.audio_url);
43-
break;
44-
45-
default:
46-
console.log('Unknown message type:', response.type);
35+
break;
36+
37+
case 'recognition_error':
38+
appendSystemMessage(`识别错误: ${response.error}`);
39+
break;
40+
41+
case 'tts_complete':
42+
// 移除"正在生成语音..."的系统消息
43+
const systemMessages = document.querySelectorAll('.message.system');
44+
systemMessages.forEach(msg => {
45+
if (msg.textContent === '正在生成语音...') {
46+
msg.remove();
47+
}
48+
});
49+
50+
// 添加 AI 的文本和音频消息
51+
appendMessage('AI', response.text);
52+
appendAudioMessage('AI', response.audio_url);
53+
if (isOneShotMode) {
54+
appendSystemMessage('单次模式:语音合成完成,连接将关闭');
55+
setTimeout(() => {
56+
ws.close();
57+
}, 2000);
58+
}
59+
break;
60+
61+
default:
62+
console.log('Unknown message type:', response.type);
63+
}
4764
}
4865
}
49-
}
50-
};
51-
52-
ws.onerror = (error) => {
53-
console.error('WebSocket 错误:', error);
54-
};
55-
56-
// 添加重连逻辑
57-
let reconnectAttempts = 0;
58-
const maxReconnectAttempts = 5;
59-
60-
ws.onclose = (event) => {
61-
console.log('WebSocket connection closed:', event);
66+
};
67+
68+
ws.onerror = (error) => {
69+
console.error('WebSocket 错误:', error);
70+
};
71+
72+
let reconnectAttempts = 0;
73+
const maxReconnectAttempts = 5;
6274

63-
if (reconnectAttempts < maxReconnectAttempts) {
64-
reconnectAttempts++;
65-
const timeout = Math.min(1000 * Math.pow(2, reconnectAttempts), 10000);
75+
ws.onclose = (event) => {
76+
console.log('WebSocket connection closed:', event);
6677

67-
appendSystemMessage(`连接已断开,${timeout/1000}秒后尝试重新连接...`);
78+
if (isOneShotMode) {
79+
appendSystemMessage('单次模式:连接已关闭');
80+
return;
81+
}
6882

69-
setTimeout(() => {
70-
ws = new WebSocket(WEBSOCKET_URL);
71-
// 重新绑定事件处理器
72-
}, timeout);
73-
} else {
74-
appendSystemMessage('连接已断开,请刷新页面重试');
75-
}
76-
};
83+
if (reconnectAttempts < maxReconnectAttempts) {
84+
reconnectAttempts++;
85+
const timeout = Math.min(1000 * Math.pow(2, reconnectAttempts), 10000);
86+
87+
appendSystemMessage(`连接已断开,${timeout/1000}秒后尝试重新连接...`);
88+
89+
setTimeout(() => {
90+
ws = new WebSocket(WEBSOCKET_URL);
91+
// 重新绑定事件处理器
92+
}, timeout);
93+
} else {
94+
appendSystemMessage('连接已断开,请刷新页面重试');
95+
}
96+
};
97+
}
98+
99+
// 初始化第一个 WebSocket 连接
100+
createWebSocket();
77101

78102
const chatWindow = document.getElementById('chat-window');
79103
const textInput = document.getElementById('text-input');
@@ -109,16 +133,47 @@ function generateSessionId() {
109133

110134
let currentSessionId = null;
111135

136+
// 添加模式切换相关变量和函数
137+
let isOneShotMode = false;
138+
const modeToggle = document.getElementById('mode-toggle');
139+
const modeLabel = document.getElementById('mode-label');
140+
141+
modeToggle.addEventListener('change', function() {
142+
isOneShotMode = this.checked;
143+
modeLabel.textContent = isOneShotMode ? '单次模式' : '持续模式';
144+
appendSystemMessage(`已切换到${modeLabel.textContent}`);
145+
});
146+
112147
function startRecording() {
113148
if (isRecording) return;
114149

115-
// 生成新的会话 ID
150+
// 在单次模式下,强制创建新的 WebSocket 连接
151+
if (isOneShotMode) {
152+
createWebSocket();
153+
ws.onopen = () => {
154+
startRecordingProcess();
155+
};
156+
return;
157+
}
158+
159+
// 非单次模式下的原有逻辑
160+
if (!ws || ws.readyState === WebSocket.CLOSED) {
161+
createWebSocket();
162+
ws.onopen = () => {
163+
startRecordingProcess();
164+
};
165+
} else {
166+
startRecordingProcess();
167+
}
168+
}
169+
170+
function startRecordingProcess() {
116171
currentSessionId = generateSessionId();
117172

118-
// 发送开始信号
119173
ws.send(JSON.stringify({
120174
type: "audio_start",
121-
session_id: currentSessionId
175+
session_id: currentSessionId,
176+
one_shot: isOneShotMode
122177
}));
123178

124179
navigator.mediaDevices.getUserMedia({ audio: true })
@@ -134,7 +189,6 @@ function startRecording() {
134189

135190
mediaRecorder.ondataavailable = e => {
136191
if (e.data && e.data.size > 0) {
137-
// 直接发送二进制数据
138192
ws.send(e.data);
139193
}
140194
};
@@ -150,9 +204,15 @@ function startRecording() {
150204
// 发送结束信号
151205
ws.send(JSON.stringify({
152206
type: "audio_end",
153-
session_id: currentSessionId
207+
session_id: currentSessionId,
208+
one_shot: isOneShotMode
154209
}));
155210

211+
// 如果是单次模式,等待响应后关闭连接
212+
if (isOneShotMode) {
213+
appendSystemMessage('单次模式:等待响应中...');
214+
}
215+
156216
currentSessionId = null;
157217
};
158218
})
@@ -231,7 +291,7 @@ function sendTextMessage(text) {
231291
// 显示发送的消息
232292
appendMessage('你', text);
233293

234-
// ��过 WebSocket 发送文字消息
294+
// 过 WebSocket 发送文字消息
235295
ws.send(JSON.stringify({
236296
text: text,
237297
require_tts: true

Diff for: cmd/voiceflow/web/styles.css

+65
Original file line numberDiff line numberDiff line change
@@ -127,4 +127,69 @@ body {
127127

128128
.chat-window::-webkit-scrollbar-thumb:hover {
129129
background: #555;
130+
}
131+
132+
/* 添加模式切换开关的样式 */
133+
.mode-switch {
134+
display: flex;
135+
align-items: center;
136+
justify-content: center;
137+
margin-top: 5px;
138+
}
139+
140+
.switch {
141+
position: relative;
142+
display: inline-block;
143+
width: 50px;
144+
height: 24px;
145+
margin-right: 10px;
146+
}
147+
148+
.switch input {
149+
opacity: 0;
150+
width: 0;
151+
height: 0;
152+
}
153+
154+
.slider {
155+
position: absolute;
156+
cursor: pointer;
157+
top: 0;
158+
left: 0;
159+
right: 0;
160+
bottom: 0;
161+
background-color: #ccc;
162+
transition: .4s;
163+
}
164+
165+
.slider:before {
166+
position: absolute;
167+
content: "";
168+
height: 16px;
169+
width: 16px;
170+
left: 4px;
171+
bottom: 4px;
172+
background-color: white;
173+
transition: .4s;
174+
}
175+
176+
input:checked + .slider {
177+
background-color: #2196F3;
178+
}
179+
180+
input:checked + .slider:before {
181+
transform: translateX(26px);
182+
}
183+
184+
.slider.round {
185+
border-radius: 24px;
186+
}
187+
188+
.slider.round:before {
189+
border-radius: 50%;
190+
}
191+
192+
#mode-label {
193+
color: white;
194+
font-size: 14px;
130195
}

Diff for: internal/server/message/binary_handler.go

+8-2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ type BinaryMessageHandler struct {
1919
storage storage.Service
2020
audioBuffers map[string]*bytes.Buffer
2121
bufferMutex sync.RWMutex
22+
oneShot bool
2223
}
2324

2425
func NewBinaryMessageHandler(stt stt.Service, tts tts.Service, storage storage.Service) *BinaryMessageHandler {
@@ -32,7 +33,7 @@ func NewBinaryMessageHandler(stt stt.Service, tts tts.Service, storage storage.S
3233
}
3334

3435
// HandleStart 处理音频开始信号
35-
func (h *BinaryMessageHandler) HandleStart(sessionID string) error {
36+
func (h *BinaryMessageHandler) HandleStart(sessionID string, oneShot bool) error {
3637
h.bufferMutex.Lock()
3738
defer h.bufferMutex.Unlock()
3839

@@ -41,6 +42,7 @@ func (h *BinaryMessageHandler) HandleStart(sessionID string) error {
4142
}
4243

4344
h.audioBuffers[sessionID] = &bytes.Buffer{}
45+
h.oneShot = oneShot
4446
return nil
4547
}
4648

@@ -63,7 +65,7 @@ func (h *BinaryMessageHandler) HandleAudioData(sessionID string, data []byte) er
6365
}
6466

6567
// HandleEnd 处理音频结束信号
66-
func (h *BinaryMessageHandler) HandleEnd(conn *websocket.Conn, sessionID string) error {
68+
func (h *BinaryMessageHandler) HandleEnd(sessionID string, conn *websocket.Conn) error {
6769
// 1. 获取并清理音频数据
6870
audioData, err := h.getAndCleanAudioData(sessionID)
6971
if err != nil {
@@ -113,6 +115,10 @@ func (h *BinaryMessageHandler) HandleEnd(conn *websocket.Conn, sessionID string)
113115
})
114116
}()
115117

118+
if h.oneShot {
119+
defer conn.Close()
120+
}
121+
116122
return nil
117123
}
118124

0 commit comments

Comments
 (0)