Ctrl K
ComunidadGuías…

PORTING.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Architecture & extension notes

The sidecar ships complete and runnable: `agent.py` (orchestrator),
`audiosocket.py` (wire codec), `stt.py`, `llm.py`, `tts.py`, and `tools.py`.
This doc explains the seams so you can swap providers or add tools.

## Module contracts

`agent.py` drives the providers through these interfaces. Any implementation
that honours them drops straight in.

### `stt.StreamingSTT`
```python
StreamingSTT(provider, model, language, api_key=None,
             elevenlabs_api_key=None, min_silence_ms=None, sixtydb_api_key=None)
  async def start(self) -> None
  async def feed(self, pcm320: bytes) -> None      # one 320-byte slin frame in
  async def stream(self) -> AsyncIterator[dict]    # yields {'text': ...} finals
  def drain_pending(self) -> list[dict]            # non-blocking backlog drain
  property voice_active -> bool                    # sustained talk-spurt (barge-in)
  async def finalize_text(self) -> str             # flush utterance in progress at hangup
  async def close(self) -> None
```
Not a true streaming API. It VAD-gates with `webrtcvad`, buffers an utterance,
and transcribes on end-of-speech (silence or a wall-clock frame gap, so far-end
DTX doesn't strand the last utterance). Whisper hallucinations on near-silence
are filtered. Included providers: OpenAI Whisper, ElevenLabs Scribe and 60db.
60db uses the same buffered WAV upload and returns final transcripts, not live
partial results. Configure `providers.sixtydb.api_key` and select
`stt_provider: sixtydb` on the persona.

### `llm.LLM`
```python
LLM(provider, model, temperature, system_prompt,
    tool_specs=None, tools_enabled=None, api_key=None)
  async def start(self) -> None
  async def add_user(self, text: str) -> None
  async def add_tool_result(self, tool_use_id: str, result_obj) -> None
  async def stream_reply(self) -> AsyncIterator[dict]   # {'kind':'text'|'tool'|'end', ...}
  async def close(self) -> None
```
Anthropic Claude, streamed, with `tool_use`. To use OpenAI or another provider,
reimplement this class keeping the `stream_reply()` event shape, and nothing else
changes.

### `tts.StreamingTTS`
```python
StreamingTTS(provider, voice_id, model, api_key=None,
             voices_dir=None, default_voice="en_US-amy-medium")
  async def start(self) -> None
  def synthesise(self, text: str) -> AsyncIterator[bytes]  # yields 320-byte slin frames
  async def close(self) -> None
```
Piper (local ONNX) + ElevenLabs (cloud), resampled to 8 kHz slin. Piper voices
are cached process-wide (load is 2.5–5.5 s) and synthesis is serialized behind a
lock, because espeak-ng's phonemizer isn't thread-safe. The voices directory comes from
config (`providers.piper.voices_dir`) or `AI_AGENT_VOICES_DIR`; no hardcoded paths.

## Adding a tool

1. Add a spec to `TOOL_SPECS` in `tools.py` (name → description + parameters).
2. List the name in a persona's `tools_enabled`.
3. Handle it in your `tools.webhook_url` endpoint and return a JSON result.

When the model calls the tool, `agent.py` POSTs
`{"session", "tool", "args", "caller"}` to your webhook and feeds the JSON
response back to the model as the tool result. All tool *behaviour* is yours,
the sidecar only advertises the schema and relays the call.

## Piper voices

```bash
./download_voices.sh en_US-amy-medium        # into ./voices (or $AI_AGENT_VOICES_DIR)
```
Browse voices at https://huggingface.co/rhasspy/piper-voices.