> ## Documentation Index
> Fetch the complete documentation index at: https://docs.moorcheh.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Voice server

> HTTP API for moorcheh-edge voice serve on edge hardware (default port 8766).

## Overview

The **voice server** is started with [`moorcheh-edge voice serve`](/cli/voice/serve). It runs on **Linux edge hardware** (for example Arduino UNO Q) and exposes mic, speaker, and RAG endpoints over HTTP.

It is **not** part of the Moorcheh Edge Docker container (`:8080`). RAG calls are proxied to Moorcheh Edge on the same device (default `http://127.0.0.1:8080`).

|                   |                                         |
| ----------------- | --------------------------------------- |
| **Default URL**   | `http://<device-ip>:8766`               |
| **Start command** | `moorcheh-edge voice serve --port 8766` |
| **Platform**      | Linux with ALSA (not Windows/macOS)     |

<Note>
  Run [`moorcheh-edge voice setup`](/cli/voice/setup) once before starting the server.
</Note>

***

## GET /health

Check that the voice server is running.

<RequestExample>
  ```bash theme={null}
  curl -X GET "http://192.168.1.50:8766/health"
  ```
</RequestExample>

<ResponseExample>
  ```json 200 - OK theme={null}
  {
    "status": "ok",
    "service": "moorcheh-edge-voice"
  }
  ```
</ResponseExample>

***

## POST /listen

Record from the device mic and return transcribed text.

<ParamField body="seconds" type="number">
  Fixed recording length in seconds. When set, disables silence detection.
</ParamField>

<ParamField body="until_silence" type="boolean" default="true">
  When `true` (and `seconds` is omitted), stop recording after a pause in speech.
</ParamField>

<ParamField body="max_seconds" type="number" default="30">
  Maximum recording length when using silence detection (3–60).
</ParamField>

<RequestExample>
  ```bash theme={null}
  curl -X POST "http://192.168.1.50:8766/listen" \
    -H "Content-Type: application/json" \
    -d '{"until_silence": true, "max_seconds": 30}'
  ```
</RequestExample>

<ResponseExample>
  ```json 200 - OK theme={null}
  {
    "heard": "Do you have oat milk?"
  }
  ```
</ResponseExample>

***

## POST /speak

Synthesize and play text on the device speaker.

<ParamField body="text" type="string" required>
  Text to speak.
</ParamField>

<RequestExample>
  ```bash theme={null}
  curl -X POST "http://192.168.1.50:8766/speak" \
    -H "Content-Type: application/json" \
    -d '{"text": "Welcome to The Brew Corner."}'
  ```
</RequestExample>

<ResponseExample>
  ```json 200 - OK theme={null}
  {
    "spoke": true
  }
  ```
</ResponseExample>

***

## POST /ask/stream

Stream a RAG answer as **Server-Sent Events**. Embeds the query on the **edge device** when `query_vector` is omitted; otherwise uses the vector you supply.

Proxies [`POST /answer/stream`](/api-references/answer-stream) on Moorcheh Edge. When `speak: true`, **streams answer tokens to the remote UI immediately** and starts **TTS after the full answer** is ready (one Piper pass on the device speaker).

<ParamField body="query" type="string" required>
  Question text.
</ParamField>

<ParamField body="query_vector" type="array">
  Optional precomputed embedding. When omitted, the server embeds locally with BGE-small-en-v1.5 (384-dim).
</ParamField>

<ParamField body="top_k" type="number" default="5">
  Passages to retrieve for context.
</ParamField>

<ParamField body="kiosk_mode" type="boolean" default="true">
  When `true`, filters passages below `threshold`.
</ParamField>

<ParamField body="threshold" type="number" default="0.25">
  Minimum search score when `kiosk_mode` is `true`.
</ParamField>

<ParamField body="header_prompt" type="string">
  Optional system instruction for RAG.
</ParamField>

<ParamField body="footer_prompt" type="string">
  Optional instruction before the question.
</ParamField>

<ParamField body="chat_history" type="array">
  Prior turns: `[{"role": "user"|"assistant", "content": "..."}]`.
</ParamField>

<ParamField body="speak" type="boolean" default="false">
  When `true`, speak the **full answer** on the device after streaming completes.
</ParamField>

<ParamField body="holding_enabled" type="boolean" default="true">
  When `speak` is `true`, play the cached kiosk holding welcome audio in parallel with RAG. Pre-generate with `moorcheh-edge voice cache-holding` - see [Retail Kiosk: kiosk audio cache](/examples/retail-kiosk#kiosk-audio-cache).
</ParamField>

<ParamField body="thinking_enabled" type="boolean" default="true">
  When `speak` is `true`, loop the ambient thinking WAV until answer tokens arrive (after holding). Pre-generate with `moorcheh-edge voice cache-thinking` - see [Retail Kiosk: kiosk audio cache](/examples/retail-kiosk#kiosk-audio-cache). Disable globally with `MOORCHEH_THINKING_SOUND=0`.
</ParamField>

<RequestExample>
  ```bash theme={null}
  curl -N -X POST "http://192.168.1.50:8766/ask/stream" \
    -H "Content-Type: application/json" \
    -d '{
      "query": "Do you have oat milk?",
      "top_k": 2,
      "kiosk_mode": true,
      "threshold": 0.3,
      "speak": true
    }'
  ```
</RequestExample>

### SSE events

Inherits Moorcheh Edge events from [Answer stream](/api-references/answer-stream):

| Event   | Description                            |
| ------- | -------------------------------------- |
| `meta`  | Model, sources, and context count      |
| `token` | Answer text delta (`{"delta": "..."}`) |
| `done`  | Full answer and metadata               |
| `error` | LLM or upstream failure                |

Additional events when **`speak: true`**:

| Event      | Description                                                              |
| ---------- | ------------------------------------------------------------------------ |
| `holding`  | Kiosk welcome audio started (`{"text": "...", "playing": true}`)         |
| `thinking` | Ambient thinking loop started (`{"text": "Thinking…", "playing": true}`) |
| `sentence` | Full answer text sent before TTS starts (`{"text": "..."}`)              |

TTS continues in the background after the HTTP stream closes; the connection does not wait for playback to finish.

***

## POST /ask and POST /ask/voice

Full voice loop: record from mic (unless `query` is provided), embed locally, call **`POST /answer`** on Moorcheh Edge, and speak the reply on the device.

`/ask/voice` is an alias for `/ask`.

<ParamField body="query" type="string">
  When set, skip mic capture and use this text as the question.
</ParamField>

<ParamField body="seconds" type="number">
  Fixed recording length when `query` is omitted.
</ParamField>

<ParamField body="until_silence" type="boolean" default="true">
  Stop recording after a pause when `seconds` is omitted.
</ParamField>

<ParamField body="max_seconds" type="number" default="30">
  Maximum recording length for silence detection.
</ParamField>

<ParamField body="top_k" type="number" default="5">
  Passages to retrieve for context.
</ParamField>

<ParamField body="kiosk_mode" type="boolean" default="true">
  Filter low-scoring passages when `true`.
</ParamField>

<ParamField body="threshold" type="number" default="0.25">
  Minimum score when `kiosk_mode` is `true`.
</ParamField>

<ParamField body="header_prompt" type="string">
  Optional system instruction.
</ParamField>

<ParamField body="footer_prompt" type="string">
  Optional instruction before the question.
</ParamField>

<ParamField body="chat_history" type="array">
  Prior conversation turns.
</ParamField>

<ParamField body="speak" type="boolean" default="true">
  When `true`, play the answer on the device speaker.
</ParamField>

<RequestExample>
  ```bash theme={null}
  curl -X POST "http://192.168.1.50:8766/ask" \
    -H "Content-Type: application/json" \
    -d '{
      "query": "What are your hours?",
      "top_k": 2,
      "kiosk_mode": true,
      "threshold": 0.3,
      "speak": true
    }'
  ```
</RequestExample>

<ResponseExample>
  ```json 200 - OK theme={null}
  {
    "heard": "What are your hours?",
    "query": "What are your hours?",
    "answer": "We are open Monday through Friday, 7am to 6pm.",
    "model": "qwen2.5:0.5b-instruct",
    "context_count": 1,
    "spoke": true
  }
  ```
</ResponseExample>

***

## POST /catalog/document

Chunk a catalog document, **embed on the device**, and upload vectors to Moorcheh Edge on the same board. Used by the [retail kiosk](/examples/retail-kiosk) Admin when saving documents (`MOORCHEH_VOICE_PROXY_URL`).

<ParamField body="doc_id" type="string" required>
  Stable document id (used in chunk ids and `[meta]` headers).
</ParamField>

<ParamField body="category" type="string" required>
  Document category (e.g. `menu`, `policy`).
</ParamField>

<ParamField body="title" type="string" required>
  Display title for the document.
</ParamField>

<ParamField body="text" type="string" required>
  Full document body to chunk and embed. Alias: `body`.
</ParamField>

<ParamField body="tags" type="array | string">
  Tags as a JSON array or comma-separated string.
</ParamField>

<ParamField body="orphan_chunk_ids" type="array">
  Edge item ids to delete before upload (replaced chunks after an edit). Alias: `orphan_ids`.
</ParamField>

<RequestExample>
  ```bash theme={null}
  curl -X POST "http://192.168.1.50:8766/catalog/document" \
    -H "Content-Type: application/json" \
    -d '{
      "doc_id": "hours",
      "category": "store-info",
      "title": "Store hours",
      "tags": ["hours"],
      "text": "We are open Monday through Friday, 7am to 6pm."
    }'
  ```
</RequestExample>

<ResponseExample>
  ```json 200 - OK theme={null}
  {
    "doc_id": "hours",
    "chunks_uploaded": 1,
    "chunks_deleted": 0,
    "chunk_ids": ["hours:0"],
    "chunks": [
      {
        "chunk_id": "hours:0",
        "doc_id": "hours",
        "chunk_index": 0,
        "category": "store-info",
        "title": "Store hours",
        "tags": "hours",
        "text": "[meta]\ndoc_id: hours\ncategory: store-info\ntitle: Store hours\ntags: hours\nchunk: 0\n[/meta]\n\nWe are open Monday through Friday, 7am to 6pm."
      }
    ]
  }
  ```
</ResponseExample>

***

## Errors

| Condition                        | Status | Body                             |
| -------------------------------- | ------ | -------------------------------- |
| Invalid JSON                     | `400`  | `{"error": "invalid JSON body"}` |
| Missing `query` on `/ask/stream` | `400`  | `{"error": "query is required"}` |
| Unknown route                    | `404`  | `{"error": "not found"}`         |
| Voice runtime / audio failure    | `503`  | `{"error": "..."}`               |
| Unexpected server error          | `500`  | `{"error": "..."}`               |

***

## Related

* [`moorcheh-edge voice serve`](/cli/voice/serve)
* [Voice CLI overview](/cli/voice/introduction)
* [Answer stream](/api-references/answer-stream)
* [Retail Kiosk example](/examples/retail-kiosk)
