Skip to content

Commit c2cc5cf

Browse files
authored
Merge pull request #13 from second-state/0.2
v0.2
2 parents 4ba5d7c + 6c393a7 commit c2cc5cf

File tree

11 files changed

+580
-159
lines changed

11 files changed

+580
-159
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "echokit_server"
3-
version = "0.1.0"
3+
version = "0.2.0"
44
edition = "2021"
55

66
[dependencies]

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/ai/mod.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,46 @@ impl ChatSession {
566566
}
567567
}
568568

569+
pub fn create_from_config(
570+
config: &crate::config::AIConfig,
571+
tools: ToolSet<McpToolAdapter>,
572+
) -> Self {
573+
match config {
574+
crate::config::AIConfig::Stable { llm, .. } => {
575+
let mut session = ChatSession::new(
576+
llm.llm_chat_url.clone(),
577+
llm.api_key.clone().unwrap_or_default(),
578+
llm.model.clone(),
579+
None,
580+
llm.history,
581+
tools,
582+
);
583+
584+
session.system_prompts = llm.sys_prompts.clone();
585+
session.messages = llm.dynamic_prompts.clone();
586+
587+
session
588+
}
589+
crate::config::AIConfig::GeminiAndTTS { gemini, .. }
590+
| crate::config::AIConfig::Gemini { gemini } => {
591+
let mut session = ChatSession::new(
592+
String::new(),
593+
gemini.api_key.clone(),
594+
gemini
595+
.model
596+
.clone()
597+
.unwrap_or("models/gemini-2.0-flash-live-001".to_string()),
598+
None,
599+
20,
600+
tools,
601+
);
602+
603+
session.system_prompts = gemini.sys_prompts.clone();
604+
session
605+
}
606+
}
607+
}
608+
569609
pub fn add_user_message(&mut self, message: String) {
570610
self.messages.push_back(llm::Content {
571611
role: llm::Role::User,

src/config.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,10 +143,19 @@ pub struct Config {
143143

144144
pub hello_wav: Option<String>,
145145

146+
#[serde(default)]
147+
pub record: RecordConfig,
148+
146149
#[serde(flatten)]
147150
pub config: AIConfig,
148151
}
149152

153+
#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
154+
pub struct RecordConfig {
155+
#[serde(default)]
156+
pub callback_url: Option<String>,
157+
}
158+
150159
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
151160
#[serde(untagged)]
152161
pub enum AIConfig {

src/main.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,20 @@ async fn routes(
8989

9090
let mut router = Router::new()
9191
// .route("/", get(handler))
92-
.route("/ws/{id}", any(services::ws::ws_handler))
93-
.nest("/record", services::file::new_file_service("./record"))
92+
.route("/ws/{id}", any(services::mixed_handler))
93+
.route("/v1/chat/{id}", any(services::ws::ws_handler))
94+
.route("/v1/record/{id}", any(services::ws_record::ws_handler))
95+
.nest("/downloads", services::file::new_file_service("./record"))
9496
.layer(axum::Extension(Arc::new(services::ws::WsSetting::new(
9597
hello_wav,
9698
config.config,
9799
tool_set,
98-
))));
100+
))))
101+
.layer(axum::Extension(Arc::new(
102+
services::ws_record::WsRecordSetting {
103+
record_callback_url: config.record.callback_url,
104+
},
105+
)));
99106

100107
if let Some(real_config) = real_config {
101108
log::info!(

0 commit comments

Comments
 (0)