| --- |
| title: Minimal Conversation JS Static |
| emoji: 🎙️ |
| colorFrom: indigo |
| colorTo: purple |
| sdk: static |
| pinned: false |
| hf_oauth: true |
| short_description: Static port of the minimal JS voice chat (Reachy Mini). |
| tags: |
| - reachy_mini |
| - reachy_mini_js_app |
| --- |
| |
| # Minimal Conversation (JS) — static build |
|
|
| A **`sdk: static`** port of [`tfrere/minimal-conversation`](https://huggingface.co/spaces/tfrere/minimal-conversation) |
| (originally a `sdk: docker` Space). Same app, no container, no nginx, no |
| entrypoint script — just the Vite production build served as files. |
|
|
| This is an **investigation** Space: does the conversation app run as a |
| pure static Space with **no server-side proxy**? Conclusion: **yes.** |
| Three things the Docker variant used a server for turn out to be |
| unnecessary on a static Space: |
|
|
| 1. **HF OAuth client ID.** The Docker variant patched |
| `__OAUTH_CLIENT_ID__` into `index.html` from a container env var at |
| start-up (`docker-entrypoint.d/10-inject-hf-vars.sh`). On a static |
| Space, HF injects `window.huggingface.variables` (incl. |
| `OAUTH_CLIENT_ID`) into the `<head>` automatically when |
| `hf_oauth: true` is set — the SDK host shell reads it directly. |
|
|
| 2. **OpenAI ephemeral-key minting.** `src/ephemeral-key.ts` POSTs the |
| visitor's HF token to the Pollen website |
| (`…pollen-robotics-reachy-mini.hf.space/api/openai/ephemeral`) |
| straight from the browser. That endpoint's CORS is origin-reflecting, |
| so this origin is accepted with no allow-list change. The master |
| `OPENAI_API_KEY` stays in the website Space's secrets. |
|
|
| 3. **OpenAI Realtime SDP call.** The Docker variant routed this through |
| an nginx `/openai/` reverse-proxy because OpenAI exposes no CORS for |
| personal `sk-` keys. But this app authenticates with short-lived |
| `ek_…` ephemeral keys, and OpenAI's `POST /v1/realtime/calls` returns |
| `Access-Control-Allow-Origin: *` for those — so the browser calls |
| `https://api.openai.com/v1/realtime/calls` directly. `src/openai-realtime.ts` |
| was changed from the `/openai/...` same-origin path to the direct URL |
| (overridable via `VITE_OPENAI_REALTIME_URL`). |
|
|
| So there is **no backend**. The browser talks to: Reachy Mini central |
| (robot signaling, via the SDK), the Pollen website (ephemeral key mint), |
| and OpenAI Realtime (SDP handshake + WebRTC voice tunnel). |
|
|
| > **Must be public.** A *private* static Space can't be loaded in the |
| > `huggingface.co` Spaces iframe wrapper: its `hf_jwt` auth cookie is |
| > SameSite-blocked in the cross-origin iframe, so assets come back as |
| > `401 text/plain` and the module is refused. Public has no such cookie |
| > requirement. |
| |
| ## How it's built |
| |
| ```bash |
| npm install |
| npm run build # → dist/ (this Space is the contents of dist/) |
| ``` |
| |
| The source lives in the upstream `tfrere/minimal-conversation` repo |
| (plus the one-line `openai-realtime.ts` change above). This Space holds |
| only the built artifacts + this README. To update, rebuild and |
| re-upload `dist/`. |
| |
| ## Architecture (unchanged from upstream) |
| |
| ``` |
| ┌────────┐ robot mic ┌────────┐ input track ┌───────────┐ |
| │ Reachy │ ────────────► │ Browser│ ──────────► │ OpenAI │ |
| │ Mini │ │ (this │ │ Realtime │ |
| │ daemon │ ◄──────────── │ app) │ ◄────────── │ API │ |
| └────────┘ robot speaker └────────┘ output track └───────────┘ |
| ``` |
| |
| ## License |
| |
| MIT for the glue code (same as upstream). Underlying SDKs keep their own |
| licenses. |
| |