Skip to content

Commit c0c6867

Browse files
committed
feat: new record server and page
1 parent ff738db commit c0c6867

File tree

7 files changed

+178
-24
lines changed

7 files changed

+178
-24
lines changed

resources/index.html

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
<!-- Add VAD-Web dependency -->
1414
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/ort.js"></script>
1515
<script src="https://cdn.jsdelivr.net/npm/@ricky0123/[email protected]/dist/bundle.min.js"></script>
16+
17+
<script src="https://openfpcdn.io/fingerprintjs/v5/iife.min.js"></script>
1618
<style>
1719
.recording-pulse {
1820
animation: pulse 1s ease-in-out infinite alternate;
@@ -117,6 +119,11 @@ <h1 class="card-title text-2xl justify-center mb-6">Voice Chat</h1>
117119
</div>
118120

119121
<script>
122+
FingerprintJS.load().then(fp => fp.get()).then(result => {
123+
window.visitorId = result.visitorId;
124+
console.log("FingerprintJS Visitor ID:", window.visitorId);
125+
});
126+
120127
class AudioRecorder {
121128
constructor() {
122129
this.isRecording = false;
@@ -176,6 +183,11 @@ <h1 class="card-title text-2xl justify-center mb-6">Voice Chat</h1>
176183
this.recordingMode = e.target.checked;
177184
const modeText = this.recordingMode ? 'Recording mode' : 'Chat mode';
178185
this.addServerMessage(`🔄 Switched to ${modeText}`);
186+
187+
// Stop listening when switching modes
188+
if (this.isVadActive) {
189+
this.toggleRecording();
190+
}
179191
});
180192

181193
// Add spacebar shortcut
@@ -234,27 +246,33 @@ <h1 class="card-title text-2xl justify-center mb-6">Voice Chat</h1>
234246
if (this.isConnected) {
235247
this.disconnectWebSocket();
236248
} else {
237-
this.connectWebSocket();
249+
// Pass recording mode state to determine whether to add record parameter
250+
this.connectWebSocket(this.recordingMode);
238251
}
239252
}
240253

241-
connectWebSocket() {
254+
connectWebSocket(addRecordParam = false) {
242255
let url = this.wsUrlInput.value.trim();
243256
if (!url) {
244257
this.addServerMessage('Please enter WebSocket URL');
245258
return;
246259
}
247260
url = url.endsWith('/') ? url : url + '/';
248-
let uuid = crypto.randomUUID();
249-
url = url + uuid;
261+
let id = window.visitorId || crypto.randomUUID();
262+
url = url + id;
263+
264+
// If need to add recording parameter
265+
if (addRecordParam) {
266+
url = url + '?record=true';
267+
}
250268

251269
try {
252270
this.websocket = new WebSocket(url);
253271
this.websocket.binaryType = 'arraybuffer'; // Set to receive binary data
254272

255273
this.websocket.onopen = () => {
256274
this.isConnected = true;
257-
this.updateConnectionStatus('connected', 'Connected: ' + uuid);
275+
this.updateConnectionStatus('connected', 'Connected: ' + id);
258276
this.connectBtn.textContent = 'Disconnect';
259277
this.connectBtn.classList.remove('btn-primary');
260278
this.connectBtn.classList.add('btn-error');
@@ -336,16 +354,12 @@ <h1 class="card-title text-2xl justify-center mb-6">Voice Chat</h1>
336354
// Handle string events
337355
switch (data) {
338356
case 'HelloStart':
339-
this.addServerMessage(`👋 Hello started`, debugInfo);
340357
break;
341358
case 'HelloEnd':
342-
this.addServerMessage(`👋 Hello ended`, debugInfo);
343359
break;
344360
case 'BGStart':
345-
this.addServerMessage(`🎵 Background music started`, debugInfo);
346361
break;
347362
case 'BGEnd':
348-
this.addServerMessage(`🎵 Background music ended`, debugInfo);
349363
break;
350364
case 'EndAudio':
351365
// Handle complete audio data
@@ -475,8 +489,32 @@ <h1 class="card-title text-2xl justify-center mb-6">Voice Chat</h1>
475489
this.myvad.pause();
476490
this.isVadActive = false;
477491
this.addServerMessage('⏹️ VAD stopped');
492+
493+
// Disconnect from server when stopping VAD listening
494+
if (this.isConnected) {
495+
this.disconnectWebSocket();
496+
this.addServerMessage('🔌 Server connection closed');
497+
}
478498
} else {
479499
try {
500+
// In recording mode, reconnect to server with record=true parameter
501+
if (this.recordingMode) {
502+
if (this.isConnected) {
503+
this.addServerMessage('🔄 Recording mode: Reconnecting to server...');
504+
this.disconnectWebSocket();
505+
// Wait for disconnection to complete
506+
await new Promise(resolve => setTimeout(resolve, 100));
507+
}
508+
this.connectWebSocket(true); // Add record=true parameter
509+
// Wait for connection to establish
510+
await new Promise(resolve => setTimeout(resolve, 1000));
511+
} else {
512+
// In chat mode, ensure connection is established
513+
this.connectWebSocket(false);
514+
// Wait for connection to establish
515+
await new Promise(resolve => setTimeout(resolve, 1000));
516+
}
517+
480518
await this.myvad.start();
481519
this.isVadActive = true;
482520
const modeText = this.recordingMode ? 'Recording mode' : 'Chat mode';
@@ -797,4 +835,4 @@ <h1 class="card-title text-2xl justify-center mb-6">Voice Chat</h1>
797835
</script>
798836
</body>
799837

800-
</html>
838+
</html>

resources/index_zh.html

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
<!-- 添加 VAD-Web 依赖 -->
1414
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/ort.js"></script>
1515
<script src="https://cdn.jsdelivr.net/npm/@ricky0123/[email protected]/dist/bundle.min.js"></script>
16+
17+
<script src="https://openfpcdn.io/fingerprintjs/v5/iife.min.js"></script>
1618
<style>
1719
.recording-pulse {
1820
animation: pulse 1s ease-in-out infinite alternate;
@@ -117,6 +119,11 @@ <h1 class="card-title text-2xl justify-center mb-6">语音聊天</h1>
117119
</div>
118120

119121
<script>
122+
FingerprintJS.load().then(fp => fp.get()).then(result => {
123+
window.visitorId = result.visitorId;
124+
console.log("FingerprintJS Visitor ID:", window.visitorId);
125+
});
126+
120127
class AudioRecorder {
121128
constructor() {
122129
this.isRecording = false;
@@ -176,6 +183,11 @@ <h1 class="card-title text-2xl justify-center mb-6">语音聊天</h1>
176183
this.recordingMode = e.target.checked;
177184
const modeText = this.recordingMode ? '录制模式' : '对话模式';
178185
this.addServerMessage(`🔄 切换到${modeText}`);
186+
187+
// 切换模式时,如果正在监听则停止
188+
if (this.isVadActive) {
189+
this.toggleRecording();
190+
}
179191
});
180192

181193
// 添加空格键快捷键
@@ -234,27 +246,33 @@ <h1 class="card-title text-2xl justify-center mb-6">语音聊天</h1>
234246
if (this.isConnected) {
235247
this.disconnectWebSocket();
236248
} else {
237-
this.connectWebSocket();
249+
// 根据录制模式状态决定是否添加 record 参数
250+
this.connectWebSocket(this.recordingMode);
238251
}
239252
}
240253

241-
connectWebSocket() {
254+
connectWebSocket(addRecordParam = false) {
242255
let url = this.wsUrlInput.value.trim();
243256
if (!url) {
244257
this.addServerMessage('请输入 WebSocket 地址');
245258
return;
246259
}
247260
url = url.endsWith('/') ? url : url + '/';
248-
let uuid = crypto.randomUUID();
249-
url = url + uuid;
261+
let id = window.visitorId || crypto.randomUUID();
262+
url = url + id;
263+
264+
// 如果需要添加录制参数
265+
if (addRecordParam) {
266+
url = url + '?record=true';
267+
}
250268

251269
try {
252270
this.websocket = new WebSocket(url);
253271
this.websocket.binaryType = 'arraybuffer'; // 设置为接收二进制数据
254272

255273
this.websocket.onopen = () => {
256274
this.isConnected = true;
257-
this.updateConnectionStatus('connected', '已连接: ' + uuid);
275+
this.updateConnectionStatus('connected', '已连接: ' + id);
258276
this.connectBtn.textContent = '断开';
259277
this.connectBtn.classList.remove('btn-primary');
260278
this.connectBtn.classList.add('btn-error');
@@ -336,16 +354,12 @@ <h1 class="card-title text-2xl justify-center mb-6">语音聊天</h1>
336354
// 处理字符串事件
337355
switch (data) {
338356
case 'HelloStart':
339-
this.addServerMessage(`👋 开始问候`, debugInfo);
340357
break;
341358
case 'HelloEnd':
342-
this.addServerMessage(`👋 问候结束`, debugInfo);
343359
break;
344360
case 'BGStart':
345-
this.addServerMessage(`🎵 开始背景音乐`, debugInfo);
346361
break;
347362
case 'BGEnd':
348-
this.addServerMessage(`🎵 背景音乐结束`, debugInfo);
349363
break;
350364
case 'EndAudio':
351365
// 处理完整的音频数据
@@ -475,8 +489,32 @@ <h1 class="card-title text-2xl justify-center mb-6">语音聊天</h1>
475489
this.myvad.pause();
476490
this.isVadActive = false;
477491
this.addServerMessage('⏹️ VAD 已停止');
492+
493+
// 关闭 VAD 监听时同时断开服务器连接
494+
if (this.isConnected) {
495+
this.disconnectWebSocket();
496+
this.addServerMessage('🔌 已断开服务器连接');
497+
}
478498
} else {
479499
try {
500+
// 如果是录制模式,重新连接服务器并添加 record=true 参数
501+
if (this.recordingMode) {
502+
if (this.isConnected) {
503+
this.addServerMessage('🔄 录制模式:重新连接服务器...');
504+
this.disconnectWebSocket();
505+
// 等待断开完成
506+
await new Promise(resolve => setTimeout(resolve, 100));
507+
}
508+
this.connectWebSocket(true); // 添加 record=true 参数
509+
// 等待连接建立
510+
await new Promise(resolve => setTimeout(resolve, 1000));
511+
} else {
512+
// 对话模式下确保连接已建立
513+
this.connectWebSocket(false);
514+
// 等待连接建立
515+
await new Promise(resolve => setTimeout(resolve, 1000));
516+
}
517+
480518
await this.myvad.start();
481519
this.isVadActive = true;
482520
const modeText = this.recordingMode ? '录制模式' : '对话模式';
@@ -797,4 +835,4 @@ <h1 class="card-title text-2xl justify-center mb-6">语音聊天</h1>
797835
</script>
798836
</body>
799837

800-
</html>
838+
</html>

src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ async fn routes(
8989

9090
let mut router = Router::new()
9191
// .route("/", get(handler))
92-
.route("/ws/{id}", any(services::ws::ws_handler))
92+
.route("/ws/{id}", any(services::mixed_handler))
9393
.route("/v1/chat/{id}", any(services::ws::ws_handler))
9494
.route("/v1/record/{id}", any(services::ws_record::ws_handler))
9595
.nest("/downloads", services::file::new_file_service("./record"))

src/services/mod.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,45 @@
1+
use std::sync::Arc;
2+
3+
use axum::{
4+
extract::{Path, Query, WebSocketUpgrade},
5+
response::{IntoResponse, Response},
6+
Extension,
7+
};
8+
19
pub mod file;
210
pub mod realtime_ws;
311
pub mod ws;
412
pub mod ws_record;
13+
14+
#[derive(Debug, serde::Deserialize)]
15+
pub struct ConnectQueryParams {
16+
#[serde(default)]
17+
reconnect: bool,
18+
#[serde(default)]
19+
record: bool,
20+
}
21+
22+
pub async fn mixed_handler(
23+
Extension(pool): Extension<Arc<ws::WsSetting>>,
24+
Extension(record_setting): Extension<Arc<ws_record::WsRecordSetting>>,
25+
ws: WebSocketUpgrade,
26+
Path(id): Path<String>,
27+
Query(params): Query<ConnectQueryParams>,
28+
) -> Response {
29+
if params.record {
30+
ws_record::ws_handler(Extension(record_setting), ws, Path(id))
31+
.await
32+
.into_response()
33+
} else {
34+
ws::ws_handler(
35+
Extension(pool),
36+
ws,
37+
Path(id),
38+
Query(ws::ConnectQueryParams {
39+
reconnect: params.reconnect,
40+
}),
41+
)
42+
.await
43+
.into_response()
44+
}
45+
}

src/services/ws.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ impl WsSetting {
7777
#[derive(Debug, serde::Deserialize)]
7878
pub struct ConnectQueryParams {
7979
#[serde(default)]
80-
reconnect: bool,
80+
pub reconnect: bool,
8181
}
8282

8383
pub async fn ws_handler(

src/services/ws_record.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use axum::{
55
ws::{Message, WebSocket},
66
Path, WebSocketUpgrade,
77
},
8-
response::IntoResponse,
8+
response::{IntoResponse, Response},
99
Extension,
1010
};
1111
use bytes::Bytes;
@@ -43,6 +43,13 @@ pub async fn ws_handler(
4343
) -> impl IntoResponse {
4444
let request_id = uuid::Uuid::new_v4().as_u128();
4545
log::info!("[Record] {id}:{request_id:x} connected.");
46+
if let Err(e) = std::fs::create_dir_all(format!("./record/{}", id)) {
47+
log::error!("[Record] {} create dir failed: {}", id, e);
48+
return Response::builder()
49+
.status(500)
50+
.body("Internal Server Error".into())
51+
.unwrap();
52+
}
4653

4754
ws.on_upgrade(move |socket| async move {
4855
match handle_socket(socket, &id).await {
@@ -104,6 +111,8 @@ async fn handle_socket(mut socket: WebSocket, id: &str) -> anyhow::Result<String
104111
)
105112
.await?;
106113

114+
wav_file.write_wav_header().await?;
115+
107116
while let Ok(Some(Ok(message))) =
108117
tokio::time::timeout(std::time::Duration::from_secs(60), socket.recv()).await
109118
{

0 commit comments

Comments
 (0)