Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -221,3 +221,6 @@ docs

# HuRI client outputs
*.wav
/.huri-local/
/assets/
config/*.generated.yaml
75 changes: 75 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,81 @@ Speech-to-Text (STT) and Text-to-Speech (TTS), Retrieval-Augmented Generation (R
pip install -r requirements.txt
```

#### Local install (single machine, no Kubernetes)

`scripts/install_local.sh` is the bare-metal counterpart of the Docker images in
`deploy/` and the Helm chart in `helm/`. It probes the machine (GPU vendor, VRAM,
RAM, disk, Python), works out which parts of the pipeline actually fit, and then
installs only those: system packages, a virtualenv with the right torch build,
the model weights, Qdrant and Ollama, plus a Ray Serve config matching the plan.

```sh
scripts/install_local.sh --plan-only # what would run on this machine?
scripts/install_local.sh # show the plan, then install it
```

The plan decides per module — TTS and gesture generation need an NVIDIA GPU,
STT runs fine on CPU, and the LLM tier is picked from the VRAM (or RAM) left
over. Modules that do not fit are dropped from `HURI_MODULES`, so no Serve
deployment is created for them. Override with `--force-tts`, `--force-gesture`,
`--llm-model`, `--stt-model`, … (`--help` lists everything).

It generates:

| Path | What |
| --- | --- |
| `config/huri_local.generated.yaml` | Ray Serve config for this machine |
| `config/client_local.generated.yaml` | matching client config |
| `.huri-local/huri.env` | the same runtime env vars as a plain `.env` |
| `.huri-local/secrets.env` | LLM API key, `0600`, never in the configs |
| `.huri-local/env.sh` | venv + `huri.env` + `secrets.env` in one `source` |
| `.huri-local/start.sh` / `stop.sh` / `status.sh` | run the stack |
| `.huri-local/plan.env` | the hardware plan it acted on |

```sh
.huri-local/start.sh # services + ray head + serve deploy
source .huri-local/env.sh
python -m src.client --config config/client_local.generated.yaml
.huri-local/status.sh # what is up, locally and remotely
.huri-local/stop.sh --all
```

`source .huri-local/env.sh` gives a shell configured exactly like a Serve
replica (model paths, `HURI_MODULES`, TTS/gesture tuning, API key), so
`python -m src.launch_huri` and `python -m src.modules.rag.ingestion` work
without `serve deploy`.

TTS additionally needs a reference voice sample:
`--voice-sample voice.wav --voice-transcript "exactly what is said in it"`.

#### Using a remote LLM instead of a local one

Any endpoint that is not on localhost is treated as already running: it is not
installed, not started, and costs no local VRAM — which is what frees a small
GPU to run TTS and gesture generation.

```sh
# self-hosted vLLM over HTTPS with a private CA / self-signed cert
scripts/install_local.sh \
--llm-url https://llm.example.lan --llm-provider vllm \
--llm-model Qwen3.5-4B-GGUF --no-verify-ssl \
--embed-url https://embedding.example.lan --embed-model bge-large-en-v1.5-gguf-Q4_K_M

# hosted OpenAI-compatible API (key goes to .huri-local/secrets.env, 0600)
scripts/install_local.sh \
--llm-url https://api.example.com --llm-provider api \
--llm-model some-model --llm-api-key "$MY_KEY"

# remote Qdrant too — then nothing but HuRI itself runs locally
scripts/install_local.sh --qdrant-url https://qdrant.example.lan
```

The provider selects the wire protocol (`ollama` → `/api/chat`, `vllm`/`api` →
`/v1/chat/completions`, only `api` sends the bearer token) and is inferred from
the URL and whether a key was given. Re-point an existing install without
reinstalling anything: `scripts/install_local.sh --only config --llm-url ... --llm-model ...`
then restart (`.huri-local/stop.sh && .huri-local/start.sh`).

## Usage

#### Launch HuRI server:
Expand Down
14 changes: 14 additions & 0 deletions constraints.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,17 @@
# google-api-core / googleapis-common-protos / proto-plus / opentelemetry-proto /
# google-cloud-storage, so it satisfies the whole dependency graph.
protobuf==4.25.8

# fastapi: Ray Serve's @serve.ingress does pickle_dumps(app) to check the ASGI
# app is serializable. FastAPI 0.139.2 added a threading.Lock to its internal
# route-cache dataclass, so that check now fails with
# TypeError: cannot pickle '_thread.lock' object
# on every FastAPI app, even a bare one. No fix released yet.
# https://github.com/ray-project/ray/issues/64939
fastapi<0.139.2

# setuptools: pkg_resources was removed outright in setuptools 82.0.0
# (2026-02-08). webrtcvad==2.0.10 still does `import pkg_resources` at import
# time, so anything newer breaks STT startup with
# ModuleNotFoundError: No module named 'pkg_resources'
setuptools<82
6 changes: 6 additions & 0 deletions deploy/examples/local_nvidia_amd/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ ray:
llm_provider: "vllm"
llm_model: "Qwen3.5-4B-GGUF"
verify_ssl: false
# Widened from the 3-attempt/1s-base default (~3s total) so a
# single dropped packet on the LAN to *.huri.lan doesn't surface
# as a user-facing error — see incident 2026-09-03 19:28:50.
embed_retries: 5
llm_retries: 5
retry_base_delay: 1.5
# GPU split (manual override knob): TTS and Gesture share one NVIDIA
# GPU. num_gpus/resources are Ray *scheduling* fractions — they let
# both replicas pack onto the same device and bias the split. Audio
Expand Down
3 changes: 2 additions & 1 deletion requirements-nvidia.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ einops==0.8.2
tiktoken==0.13.0 # cosyvoice/tokenizer
pyarrow==18.1.0 # imported by cli paths via dataset utils? actually only dataset/processor — can drop
protobuf==4.25.8 # keep <5 so Ray Serve's _proto_to_dict works; 4.25.8 satisfies onnxruntime>=4.25.8 (also enforced by constraints.txt)
pydantic==2.7.0 # transitive (transformers/fastapi), but pinning avoids drift
pydantic==2.13.5 # transitive (transformers/fastapi); fastapi<0.139.2 (see constraints.txt) requires pydantic>=2.9.0
regex==2025.11.3
tqdm==4.67.3
rich==13.7.1 # cosyvoice/flow/flow_matching.py -> matcha.utils.rich_utils

# --- RAG / LLM extras ---
httpx==0.27.2
Expand Down
Loading