Agent-oriented guide for AI coding assistants working in this repository.
hart-web is a local HTTP API and browser UI for
HART text-to-image generation.
Stack: Nuxt 3 / Nitro (TypeScript), running inside a Docker container
that inherits a hart-cli:thor GPU image.
pages/
index.vue # Browser UI
server/
api/
generate.post.ts # POST /api/generate — spawns hart-txt2img
middleware/
00-req-logger.ts # Per-request ID + timing logs
routes/
_debug.get.ts # GET /_debug — liveness probe
api.get.ts # GET /api — 404 placeholder
files/
[name].ts # GET|HEAD /files/:name — serve output PNGs
utils/
mutex.ts # Single-slot non-queuing lock
tests/
_utils.ts # In-process h3 test-server helper
files.test.ts
generate.test.ts
nuxt.config.ts
vitest.config.ts
docker-compose.yml
Dockerfile
npm install # install dependencies
npm run dev # dev server with hot-reload (http://localhost:3011)
npm test # run the full Vitest suite
npm run build # production build → .output/
npm run start # serve the production build- Uses Vitest with a
nodeenvironment. - Tests spin up lightweight in-process h3 servers — no Docker, GPU, or real CLI binary needed.
node:child_processspawn is mocked ingenerate.test.ts; do not remove or alter those mocks.- All test files live in
tests/and match**/*.test.ts. passWithNoTests: false— a test file with no test cases is a failure.
server/utils/mutex.ts exposes a module-level boolean lock (tryAcquire).
Only one /api/generate request may be in flight at any time; a second
concurrent request receives 429 busy immediately with no queuing.
- Do not introduce async queuing or waitlist logic without understanding that the lock is intentionally non-queuing.
- Do not move the mutex state into a request-scoped variable.
generate.post.ts spawns hart-txt2img using node:child_process spawn
with an array of arguments (no shell expansion). Prompt text is passed as
a CLI argument; never build a shell string with user input interpolated into it.
/files/:name enforces the pattern ^[0-9]{5,}-[0-9]+-[0-9a-f]{8}\.png$ (case-insensitive flag i).
Do not relax this regex without considering path-traversal implications.
Copy .env.example to .env. Note that some defaults differ by context:
.env.examplesetsOUT_DIR=/data/hart/outputs/hart_samples.docker-compose.ymlpassesOUT_DIR=/workspace/outputs/hart_samples.nuxt.config.tsfalls back toTIMEOUT_SECS=300if nothing is set, while.env.exampleand Docker set600.
Key variables:
| Variable | Default | Notes |
|---|---|---|
MODEL_PATH |
/workspace/models/hart-0.7b-1024px/llm |
HART LLM path |
TEXT_MODEL_PATH |
/workspace/models/Qwen2-VL-1.5B-Instruct |
Text encoder path |
OUT_DIR |
context-dependent | Output dir; see note above |
MAX_PROMPT_LEN |
600 |
Prompt character limit |
TIMEOUT_SECS |
context-dependent | 600 in env/Docker, 300 code fallback |
For local dev without a real model, the tests mock the spawn call so the model paths are irrelevant.
- All server code is TypeScript.
- Input validation happens before I/O or subprocess execution; preserve that order when adding request fields.
queueSizeexists in runtime config, but the current implementation is a single-slot mutex and effectively supports only one in-flight generation.- Do not add runtime dependencies without a clear justification; this is an intentionally thin stack.
The production container is built on top of hart-cli:thor and is attached to
two bridge networks: hart_pub (regular bridge, carries the published port) and
hart_internal (internal: true, no external routing). Outbound internet is
gated at the application layer via HuggingFace and W&B offline environment
variables (e.g. HF_HUB_OFFLINE=1), not purely by the network topology. Do not
write code that assumes outbound network access from within the container.