Kuma Voice is a watch-only voice assistant with a local-first Studio. The Watch streams microphone audio to a FastAPI backend, which keeps provider credentials off the device and streams a spoken answer back. The Next.js Studio handles accounts, onboarding, Watch pairing, and provider setup. Optional tools add web search, Notion notes, and Google Calendar access.
This repository does not include a public backend or any credentials. Run your own server and point the Watch app at it.
watchos/KumaVoice.xcodeproj — watch-only SwiftUI appbackend/app — FastAPI server and provider integrationsbackend/tests — tests that use fakes and do not consume API creditsweb — landing page and authenticated Kuma Studiocompose.yaml — local Postgres, MinIO, API, and web stackPython 3.12 and uv are only required when running the backend outside Docker. OpenRouter powers the older REST fallback. Exa, Notion, and Google Calendar are optional.
cp .env.example .env
docker compose up --build
Then open:
Postgres listens on port 5432, MinIO's S3 endpoint on 9000, and FastAPI on
8000. MinIO is provisioned for future conversation media but is not used by
the first onboarding flow yet. The Compose credentials are deliberately simple
local defaults; change them before sharing the environment.
The first-run flow is:
Docker persists Postgres, MinIO data, and the locally generated encryption key in named volumes. To run only the backend without Docker:
cd backend
cp .env.example .env
uv sync
uv run uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
The liveness check is at
http://127.0.0.1:8000/health, and local API
documentation is at http://127.0.0.1:8000/docs.
watchos/KumaVoice.xcodeproj in Xcode.KumaVoice target and choose your development team under
Signing & Capabilities.localhost:3001/studio.The checked-in configuration uses http://127.0.0.1:8000. For a physical Watch
or hosted server, create a private local configuration:
cp watchos/Local.xcconfig.example watchos/Local.xcconfig
Edit watchos/Local.xcconfig with a backend URL reachable by the Watch. Leave
KUMA_API_TOKEN empty to use the Studio pairing flow. For the simulator this
can be your Mac's local address; a physical Watch needs a LAN address or HTTPS
host. This file is ignored by Git. Local HTTP is allowed for development;
remote deployments must use HTTPS.
The Compose stack sets KUMA_REQUIRE_DEVICE_AUTH=true, so each paired Watch
receives its own revocable credential. For the older self-hosted mode,
KUMA_API_TOKEN can still be used as a shared backend gate. If neither device
authentication nor a shared token is enabled, the API accepts requests without
authentication and must remain local-only.
Authentication protects every /api/v1 HTTP and WebSocket endpoint. /health
remains public but returns only {"status":"ok"}. Email/password Studio auth is
an intentionally small local implementation; add rate limiting, email
verification, password recovery, audit events, and managed key rotation before
exposing it as a production service.
For a hosted setup, provide an explicit KUMA_ENCRYPTION_KEY from a secret
manager rather than relying on the local key file, enable secure cookies, and
serve the three surfaces as:
kumavoice.com — public Next.js pagesstudio.kumavoice.com — Next.js Studioapi.kumavoice.com — FastAPI and realtime WebSocket APISet NEXT_PUBLIC_STUDIO_ORIGIN=https://studio.kumavoice.com in Next.js,
KUMA_API_INTERNAL_URL=https://api.kumavoice.com on the Next.js server, and
KUMA_STUDIO_URL=https://studio.kumavoice.com in FastAPI. The Studio already
supports host-based routing for the dedicated subdomain.
The included fly.toml deliberately contains no app name or personal region.
Choose your own globally unique name:
KUMA_FLY_APP=your-unique-app-name
fly auth login
fly apps create "$KUMA_FLY_APP"
fly secrets set -a "$KUMA_FLY_APP" OPENAI_API_KEY=... KUMA_API_TOKEN=...
fly deploy -a "$KUMA_FLY_APP"
Set KUMA_BACKEND_URL in watchos/Local.xcconfig to your assigned HTTPS URL,
then rebuild the Watch app. Use openssl rand -hex 32 to generate a suitable
token and keep it in a password manager.
Provider secrets belong only in backend/.env locally or in your host's secret
store. The full list and safe placeholders are in backend/.env.example.
Set EXA_API_KEY to let the realtime assistant search the web.
NOTION_API_KEY and, optionally, NOTION_NOTES_PAGE_ID.GOOGLE_CALENDAR_CLIENT_ID and GOOGLE_CALENDAR_CLIENT_SECRET locally.uv run python -m scripts.google_calendar_auth from backend and save
the resulting GOOGLE_CALENDAR_REFRESH_TOKEN.GOOGLE_CALENDAR_TIMEZONE to an IANA zone such as UTC.Use a dedicated test calendar and least-privilege Notion pages when evaluating the prototype.
Microphone audio and conversation content are sent to the providers configured on your backend. Web searches, notes, and calendar requests may also be sent to their respective services. Review those providers' retention settings before using personal or sensitive data.
Never commit backend/.env or watchos/Local.xcconfig. If a credential is ever
exposed, rotate it rather than merely deleting it from Git.
GET /health — minimal liveness responsePOST /studio/v1/auth/signup — create a local Studio accountPOST /studio/v1/auth/signin — start an HTTP-only cookie sessionGET /studio/v1/bootstrap — load account state plus a complete profile or nullPUT /studio/v1/profile — atomically create or replace all required onboarding answersPOST /studio/v1/pairings/approve — bind a Watch code to the accountPOST /studio/v1/providers/openai — encrypt and store an OpenAI keyPOST /device/v1/authorizations — request a Watch pairing codePOST /device/v1/token — poll for the paired device credentialWS /api/v1/realtime — realtime PCM voice conversationPOST /api/v1/transcriptions — audio file to textPOST /api/v1/responses — messages to assistant textPOST /api/v1/speech — text to MP3 audioPOST /api/v1/conversations — start an in-memory conversationPOST /api/v1/conversations/{id}/turns — run a complete voice turnDELETE /api/v1/conversations/{id} — end a conversationConversations are kept only in memory and disappear when the backend restarts. Realtime audio is 24 kHz mono, 16-bit PCM.
cd backend
uv run pytest
10 commits
Python
34.5%
TypeScript
23.1%
CSS
22.0%
Swift
20.1%
Kuma Voice is a watch-only voice assistant with a local-first Studio. The Watch streams microphone audio to a FastAPI backend, which keeps provider credentials off the device and streams a spoken answer back. The Next.js Studio handles accounts, onboarding, Watch pairing, and provider setup. Optional tools add web search, Notion notes, and Google Calendar access.
This repository does not include a public backend or any credentials. Run your own server and point the Watch app at it.
watchos/KumaVoice.xcodeproj — watch-only SwiftUI appbackend/app — FastAPI server and provider integrationsbackend/tests — tests that use fakes and do not consume API creditsweb — landing page and authenticated Kuma Studiocompose.yaml — local Postgres, MinIO, API, and web stackPython 3.12 and uv are only required when running the backend outside Docker. OpenRouter powers the older REST fallback. Exa, Notion, and Google Calendar are optional.
cp .env.example .env
docker compose up --build
Then open:
Postgres listens on port 5432, MinIO's S3 endpoint on 9000, and FastAPI on
8000. MinIO is provisioned for future conversation media but is not used by
the first onboarding flow yet. The Compose credentials are deliberately simple
local defaults; change them before sharing the environment.
The first-run flow is:
Docker persists Postgres, MinIO data, and the locally generated encryption key in named volumes. To run only the backend without Docker:
cd backend
cp .env.example .env
uv sync
uv run uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
The liveness check is at
http://127.0.0.1:8000/health, and local API
documentation is at http://127.0.0.1:8000/docs.
watchos/KumaVoice.xcodeproj in Xcode.KumaVoice target and choose your development team under
Signing & Capabilities.localhost:3001/studio.The checked-in configuration uses http://127.0.0.1:8000. For a physical Watch
or hosted server, create a private local configuration:
cp watchos/Local.xcconfig.example watchos/Local.xcconfig
Edit watchos/Local.xcconfig with a backend URL reachable by the Watch. Leave
KUMA_API_TOKEN empty to use the Studio pairing flow. For the simulator this
can be your Mac's local address; a physical Watch needs a LAN address or HTTPS
host. This file is ignored by Git. Local HTTP is allowed for development;
remote deployments must use HTTPS.
The Compose stack sets KUMA_REQUIRE_DEVICE_AUTH=true, so each paired Watch
receives its own revocable credential. For the older self-hosted mode,
KUMA_API_TOKEN can still be used as a shared backend gate. If neither device
authentication nor a shared token is enabled, the API accepts requests without
authentication and must remain local-only.
Authentication protects every /api/v1 HTTP and WebSocket endpoint. /health
remains public but returns only {"status":"ok"}. Email/password Studio auth is
an intentionally small local implementation; add rate limiting, email
verification, password recovery, audit events, and managed key rotation before
exposing it as a production service.
For a hosted setup, provide an explicit KUMA_ENCRYPTION_KEY from a secret
manager rather than relying on the local key file, enable secure cookies, and
serve the three surfaces as:
kumavoice.com — public Next.js pagesstudio.kumavoice.com — Next.js Studioapi.kumavoice.com — FastAPI and realtime WebSocket APISet NEXT_PUBLIC_STUDIO_ORIGIN=https://studio.kumavoice.com in Next.js,
KUMA_API_INTERNAL_URL=https://api.kumavoice.com on the Next.js server, and
KUMA_STUDIO_URL=https://studio.kumavoice.com in FastAPI. The Studio already
supports host-based routing for the dedicated subdomain.
The included fly.toml deliberately contains no app name or personal region.
Choose your own globally unique name:
KUMA_FLY_APP=your-unique-app-name
fly auth login
fly apps create "$KUMA_FLY_APP"
fly secrets set -a "$KUMA_FLY_APP" OPENAI_API_KEY=... KUMA_API_TOKEN=...
fly deploy -a "$KUMA_FLY_APP"
Set KUMA_BACKEND_URL in watchos/Local.xcconfig to your assigned HTTPS URL,
then rebuild the Watch app. Use openssl rand -hex 32 to generate a suitable
token and keep it in a password manager.
Provider secrets belong only in backend/.env locally or in your host's secret
store. The full list and safe placeholders are in backend/.env.example.
Set EXA_API_KEY to let the realtime assistant search the web.
NOTION_API_KEY and, optionally, NOTION_NOTES_PAGE_ID.GOOGLE_CALENDAR_CLIENT_ID and GOOGLE_CALENDAR_CLIENT_SECRET locally.uv run python -m scripts.google_calendar_auth from backend and save
the resulting GOOGLE_CALENDAR_REFRESH_TOKEN.GOOGLE_CALENDAR_TIMEZONE to an IANA zone such as UTC.Use a dedicated test calendar and least-privilege Notion pages when evaluating the prototype.
Microphone audio and conversation content are sent to the providers configured on your backend. Web searches, notes, and calendar requests may also be sent to their respective services. Review those providers' retention settings before using personal or sensitive data.
Never commit backend/.env or watchos/Local.xcconfig. If a credential is ever
exposed, rotate it rather than merely deleting it from Git.
GET /health — minimal liveness responsePOST /studio/v1/auth/signup — create a local Studio accountPOST /studio/v1/auth/signin — start an HTTP-only cookie sessionGET /studio/v1/bootstrap — load account state plus a complete profile or nullPUT /studio/v1/profile — atomically create or replace all required onboarding answersPOST /studio/v1/pairings/approve — bind a Watch code to the accountPOST /studio/v1/providers/openai — encrypt and store an OpenAI keyPOST /device/v1/authorizations — request a Watch pairing codePOST /device/v1/token — poll for the paired device credentialWS /api/v1/realtime — realtime PCM voice conversationPOST /api/v1/transcriptions — audio file to textPOST /api/v1/responses — messages to assistant textPOST /api/v1/speech — text to MP3 audioPOST /api/v1/conversations — start an in-memory conversationPOST /api/v1/conversations/{id}/turns — run a complete voice turnDELETE /api/v1/conversations/{id} — end a conversationConversations are kept only in memory and disappear when the backend restarts. Realtime audio is 24 kHz mono, 16-bit PCM.
cd backend
uv run pytest
10 commits
Python
34.5%
TypeScript
23.1%
CSS
22.0%
Swift
20.1%