Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ serde_json = "1.0"
toml = "0.8"
rmp-serde = "1"

deadpool = "0.10"
cached = { version = "0.56.0", features = ["async"] }

anyhow = "1.0"
Expand Down
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,26 @@ Configure WiFi and server
**Chat:** press the `K0` button once or multiple times until the status bar shows "Ready". You can now speak and it will show "Listening ...". The device answers after it decides that you have done speaking.

**Config:** press `RST`. While it is restarting, press and hold `K0` to enter the configuration mode. Then [open the configuration UI](https://echokit.dev/setup/) to connect to the device via BT.

## 🤖 AI-Powered Development with Claude Code

EchoKit Server includes specialized **SKILLs** for [Claude Code](https://claude.ai/code) to automate configuration and environment setup.

### 1. Install SKILLs
Link the project's local skills to your Claude Code environment:

```bash
mkdir -p ~/.claude/skills/
ln -s $(pwd)/.claude/skills/echokit-config-generator ~/.claude/skills/
```

### 2. Available Slash Commands
Once installed, you can use the following commands within Claude Code:

* **/setup-echokit**: Interactive five-phase guide to generate a complete `config.toml`. It handles assistant definitions, platform selection (OpenAI, Groq, etc.), and server launch.
* **/sync-seekdb**: (If using SeekDB) Automatically generates `config.toml` tailored for SeekDB/OceanBase vector storage and verifies connectivity.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't see you changed the SKILL.md. Did you miss something?


### 3. Benefits
* **Zero-Config Setup**: Automatically detects available API endpoints and generates the correct TOML structure.
* **Context-Aware**: The generator can create sophisticated system prompts based on your specific use case.
* **Validation**: Built-in health checks ensure your API keys and server address are correct before finishing.
99 changes: 35 additions & 64 deletions src/services/ws/stable/tts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,84 +122,55 @@ impl TTSSession {
}
}

pub struct TTSSessionPool {
pub config: crate::config::TTSConfig,
pub workers: usize,
pub pool: tokio::sync::mpsc::UnboundedReceiver<tokio::sync::oneshot::Sender<TTSRequest>>,
pub tx: tokio::sync::mpsc::UnboundedSender<tokio::sync::oneshot::Sender<TTSRequest>>,
pub struct TTSManager {
config: crate::config::TTSConfig,
}

impl TTSSessionPool {
pub fn new(config: crate::config::TTSConfig, workers: usize) -> Self {
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
TTSSessionPool {
config,
workers,
pool: rx,
tx,
}
}
impl deadpool::managed::Manager for TTSManager {
type Type = TTSSession;
type Error = anyhow::Error;

pub async fn create_session(&self) -> anyhow::Result<TTSSession> {
async fn create(&self) -> Result<TTSSession, anyhow::Error> {
TTSSession::new_from_config(&self.config).await
}

pub async fn run_session(
id: u128,
mut session: TTSSession,
tx: tokio::sync::mpsc::UnboundedSender<tokio::sync::oneshot::Sender<TTSRequest>>,
) -> anyhow::Result<()> {
log::info!("{} starting TTS session worker", id);
loop {
let (resp_tx, resp_rx) = tokio::sync::oneshot::channel();
tx.send(resp_tx)
.map_err(|e| anyhow::anyhow!("send session request error: {}", e))?;

let (text, tts_resp_tx) = resp_rx
.await
.map_err(|e| anyhow::anyhow!("receive session request error: {}", e))?;

log::info!("{} processing TTS request: {}", id, text);

if let Err(e) = session.synthesize(&text, &tts_resp_tx).await {
log::error!("{} TTS synthesis error: {}", id, e);
}
}
async fn recycle(
&self,
_obj: &mut TTSSession,
_metrics: &deadpool::managed::Metrics,
) -> deadpool::managed::RecycleResult<anyhow::Error> {
Ok(())
}
}

pub struct TTSSessionPool {
pool: deadpool::managed::Pool<TTSManager>,
}

async fn get_req_tx(&mut self) -> anyhow::Result<tokio::sync::oneshot::Sender<TTSRequest>> {
let req_tx = self
.pool
.recv()
.await
.ok_or_else(|| anyhow::anyhow!("no available tts session"))?;
Ok(req_tx)
impl TTSSessionPool {
pub fn new(config: crate::config::TTSConfig, workers: usize) -> Self {
let manager = TTSManager { config };
let pool = deadpool::managed::Pool::builder(manager)
.max_size(workers)
.build()
.expect("Failed to create TTS session pool");
TTSSessionPool { pool }
}

pub async fn run_loop(&mut self, mut rx: TTSRequestRx) -> anyhow::Result<()> {
let mut sucess_workers = 0;
for i in 0..self.workers {
match self.create_session().await {
Ok(session) => {
tokio::spawn(Self::run_session(i as u128, session, self.tx.clone()));
sucess_workers += 1;
while let Some((text, tts_resp_tx)) = rx.recv().await {
match self.pool.get().await {
Ok(mut session) => {
tokio::spawn(async move {
log::info!("Processing TTS request: {}", text);
if let Err(e) = session.synthesize(&text, &tts_resp_tx).await {
log::error!("TTS synthesis error: {}", e);
}
});
}
Err(e) => {
log::error!("create tts session[{i}] error: {}", e);
continue;
log::error!("Failed to get TTS session from pool: {}", e);
}
};
}

if sucess_workers == 0 {
return Err(anyhow::anyhow!("no available tts session worker"));
}

while let Some(tts_req) = rx.recv().await {
let req_tx = self.get_req_tx().await?;

if let Err(e) = req_tx.send(tts_req) {
log::error!("send tts request to session error: {}", e.0);
}
}
Ok(())
Expand Down