From fa00b36e4953b741ac2258e2e019d83e71307782 Mon Sep 17 00:00:00 2001 From: Alvaro Parker Date: Tue, 6 Oct 2026 18:27:09 -0300 Subject: [PATCH] build(client): use locked uv environment in launcher and Docker --- .dockerignore | 5 ++ README.md | 7 +- docs/README.md | 1 + docs/client/architecture.md | 13 +++- docs/client/context-injection.md | 3 +- docs/client/development.md | 77 +++++++++++++++++++++ docs/client/reasoning.md | 2 +- docs/client/troubleshooting.md | 9 +-- docs/client/usage.md | 15 ++-- s2s-client/CHANGELOG.md | 6 +- s2s-client/Dockerfile | 18 +++-- s2s-client/README.md | 13 +++- s2s-client/requirements.txt | 4 -- s2s-client/scripts/test_docker_image.sh | 53 ++++++++++++++ s2s-frontend/config/launcher.integration.ts | 28 ++++++-- start_ui.sh | 29 +++----- 16 files changed, 230 insertions(+), 53 deletions(-) create mode 100644 docs/client/development.md delete mode 100644 s2s-client/requirements.txt create mode 100644 s2s-client/scripts/test_docker_image.sh diff --git a/.dockerignore b/.dockerignore index 33e4966..6826e5c 100644 --- a/.dockerignore +++ b/.dockerignore @@ -17,3 +17,8 @@ **/uploads/ **/sessions/ debug_files/ +tmp/ +**/.mypy_cache/ +**/.pytest_cache/ +**/.ruff_cache/ +**/*.egg-info/ diff --git a/README.md b/README.md index a149127..9046a04 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ documentation, source identifiers, comments, and internal instructions use Engli Start with [client setup and usage](docs/client/usage.md), then read the [architecture](docs/client/architecture.md) and [documentation index](docs/README.md). The speech backend needs its own installed model and runtime dependencies; -`s2s-client/requirements.txt` installs only the UI server dependencies. +The uv project in `s2s-client/` installs only the UI server dependencies. ## Run the browser client @@ -25,13 +25,13 @@ S2S_PROXY_TARGET=ws://localhost:8765/v1/realtime ./start_ui.sh ``` Open and allow microphone access. The launcher creates -`.venv`, installs missing Python requirements and locked frontend dependencies, +`s2s-client/.venv`, syncs locked Python and frontend dependencies, and loads the root `.env` if present. Vite listens on `UI_PORT` (default `8420`) and forwards HTTP/WebSocket backend routes to FastAPI on `127.0.0.1:8421`. Both servers reload during development; stopping the launcher stops both. Leave `SPEECH_TO_SPEECH_URL` unset to use the same-origin speech proxy. -The launcher requires Python 3 with venv support, Node.js 22.18+ on the 22.x +The launcher requires uv 0.12.5 (which provisions Python 3.12), Node.js 22.18+ on the 22.x line or Node.js 24+, npm, Bash 4.4+, `setsid`, and `curl`. For staging hostnames, set `S2S_DEV_ALLOWED_HOSTS` and forward the Vite HMR WebSocket through the existing HTTPS reverse proxy. See [development setup](s2s-frontend/README.md). @@ -52,6 +52,7 @@ proxy deployment, supply a backend address reachable from the container instead. ## Project references - [Change history](s2s-client/CHANGELOG.md) +- [Backend development and compatibility](docs/client/development.md) - [Realtime protocol and pipeline](docs/speech/realtime-api.md) - [Context injection](docs/client/context-injection.md) - [Tool calling](docs/client/tool-calling.md) diff --git a/docs/README.md b/docs/README.md index 2a6cbdb..c667713 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,6 +8,7 @@ do not define the current implementation. - [Usage and configuration](client/usage.md) - [Architecture and source map](client/architecture.md) +- [Backend development and compatibility](client/development.md) - [Reasoning modes](client/reasoning.md) - [Context injection](client/context-injection.md) - [Tool calling](client/tool-calling.md) diff --git a/docs/client/architecture.md b/docs/client/architecture.md index a3b1141..a385020 100644 --- a/docs/client/architecture.md +++ b/docs/client/architecture.md @@ -42,7 +42,14 @@ audio. It is not an unconditional byte-for-byte tunnel. | Source | Responsibility | |---|---| -| [server.py](../../s2s-client/server.py) | Configuration, documentation serving, WebSocket relay, context loading, ApiPilot, reasoning, search and queue routes | +| [server.py](../../s2s-client/server.py) | Compatibility entry point for existing `server:app` commands | +| [main.py](../../s2s-client/s2s_client/main.py) | FastAPI assembly, documentation serving and redirects | +| [api](../../s2s-client/s2s_client/api) | HTTP route registration, handlers and compatible response validation | +| [models](../../s2s-client/s2s_client/models) | Pydantic request and response contracts | +| [core](../../s2s-client/s2s_client/core) | Existing environment parsing, logging, URL helpers and diagnostics | +| [providers](../../s2s-client/s2s_client/providers) | Caller context and ApiPilot HTTP integrations | +| [services](../../s2s-client/s2s_client/services) | Context construction, reasoning and tool-context rendering | +| [realtime](../../s2s-client/s2s_client/realtime) | WebSocket relay, events, markers and audio handling | | [start_ui.sh](../../start_ui.sh) | Dependency setup, ports, readiness, reload processes, and shutdown | | [vite.config.ts](../../s2s-frontend/vite.config.ts) | Public development endpoint, HMR, allowed hosts, and backend routing | | [App.tsx](../../s2s-frontend/src/App.tsx) | React layout, voice controls, dialogs, profile and reports sidebar | @@ -75,6 +82,10 @@ source text, and directories without `index.html` are not browsable pages. FastAPI serves backend and documentation routes only. The frontend source and assets live in `s2s-frontend` and are served by Vite. +The uv project and its Python tools are scoped to `s2s-client/`. Response models +describe service-owned structure and retain loose upstream values. See +[development and compatibility](development.md) before tightening a contract. + ## Audio path 1. The browser captures microphone audio with echo cancellation, noise diff --git a/docs/client/context-injection.md b/docs/client/context-injection.md index 2a5105b..8a6e62e 100644 --- a/docs/client/context-injection.md +++ b/docs/client/context-injection.md @@ -2,7 +2,8 @@ Context reaches the assistant through browser-built instructions, server-patched realtime session instructions, and HTTP reasoning context. The implementation is -in [server.py](../../s2s-client/server.py), +in the server's [context services](../../s2s-client/s2s_client/services/context.py), +[realtime proxy](../../s2s-client/s2s_client/realtime/proxy.py), [context.ts](../../s2s-frontend/src/lib/context.ts), and the [WebSocket client](../../s2s-frontend/src/realtime/client.ts). diff --git a/docs/client/development.md b/docs/client/development.md new file mode 100644 index 0000000..0120b4e --- /dev/null +++ b/docs/client/development.md @@ -0,0 +1,77 @@ +# Backend development and compatibility + +The FastAPI project lives entirely in `s2s-client/`. It uses uv 0.12.5, Python +3.12, and a committed `uv.lock`. The frontend and speech pipeline keep their +own tooling. Runtime dependency versions match the environment used to capture +the pre-migration server; framework upgrades require a separate compatibility +review. + +## Commands + +From the repository root: + +```bash +make -C s2s-client install # uv sync --locked, including development tools +make -C s2s-client format # Ruff safe fixes and formatting +make -C s2s-client check # lock freshness, format, lint, mypy, pytest +make -C s2s-client test-launcher # Bash supervisor integration fixtures +make -C s2s-client test-image # Docker build and isolated runtime smoke test +``` + +Run `uv lock` inside `s2s-client/` when intentionally changing dependencies. +Do not install server packages into a shared root environment. The launcher +syncs `s2s-client/.venv` and retains root `.env` loading, port defaults, +readiness checks, and process-group shutdown. Offline starts require the +locked packages and Python runtime to have been installed or cached already. + +Optional hooks use `make -C s2s-client hooks-install`; the configuration stays +in `s2s-client/.pre-commit-config.yaml`. `make -C s2s-client hooks-run` runs the +same hooks without installing them. Hooks and Python quality checks apply +only to the client service. The server entry points are `s2s_client.main:app` +and the retained `server:app` shim; use `--app-dir s2s-client` from the root. + +## Schema boundaries + +`models/requests.py` retains the existing search, reasoning, and ApiPilot +request models, including their defaults, coercion, and ignored extra fields. +The queue beacon still accepts malformed JSON and non-object bodies as before; +its Pydantic view is applied only after the original object check, and +`queueId` remains unconstrained. + +`models/responses.py` validates structure constructed by this server. Fields +owned by external services remain `Any` where the old parser imposed no type +constraint. Object models allow extra fields. Caller profiles can be arbitrary +JSON; load-balancer fields remain optional; execution results and error details +remain open. Existing provider parsing, authentication precedence, argument +filtering, environment parsing, and WebSocket frames are unchanged. + +HTTP handlers call `validated_json_response`: Pydantic validates the payload, +then `JSONResponse` serializes the original object. Returning a dumped model +would insert defaults or change values/key order, so do not replace this helper +with model serialization without a contract review. Route `response_model` +declarations publish the new response schemas in OpenAPI. Existing request +schemas and validation errors remain unchanged. HTTP exceptions retain their +existing error shapes; there is no new common error envelope. + +## Compatibility evidence + +`tests/fixtures/legacy-contracts.json` was captured from the original monolithic +server at commit `3d42df5`, before refactoring. Tests compare 86 HTTP scenarios, +including exact response text, statuses, selected headers, and outgoing mock +HTTP requests. They also compare request OpenAPI definitions and representative +realtime frames. Additional tests cover response validation and permissive +upstream values; the original local HTTP/WebSocket tests remain in the suite. + +The captures deliberately preserve unknown fields, omitted fields, unusual JSON +values, queue beacon edge cases, permissive tool arguments, and known failures. +Examples include reasoning's existing uninitialized local variables in certain +configuration combinations, API-key-only tool precheck behavior, the catalog +cache's existing key, and relay termination after a suppressed invalid +`session.update` error. Fix those in separately reviewed behavior changes; +do not regenerate golden captures to make a refactor pass. + +These checks use local fixtures and do not claim exhaustive coverage of all +provider responses, live speech models, or production configuration. No deployed +services or real credentials are needed. The Docker check verifies the actual +runtime image, API configuration, documentation, OpenAPI, and the backend's +existing root 404; it removes its temporary container and image afterward. diff --git a/docs/client/reasoning.md b/docs/client/reasoning.md index c336734..2e0bbed 100644 --- a/docs/client/reasoning.md +++ b/docs/client/reasoning.md @@ -99,7 +99,7 @@ realtime backend instead. |---|---| | Change the base agent prompt | `S2S_PRIMARY_PROMPT` or browser settings when unpinned | | Change browser context assembly | `buildInstructions()` in `s2s-frontend/src/lib/context.ts` | -| Change HTTP provider messages | `reasoning()` in `server.py` | +| Change HTTP provider messages | `reasoning()` in `s2s-client/s2s_client/services/reasoning.py` | | Change external-answer rendering | `VoiceController.reason()` and `RealtimeClient.renderExternalAnswer()` | | Change Node-RED processing | `s2s-reasoning-engine/flows.json` | | Change native model/tool generation | `speech_to_speech/LLM` and realtime handlers | diff --git a/docs/client/troubleshooting.md b/docs/client/troubleshooting.md index 39ba6fc..cf6434e 100644 --- a/docs/client/troubleshooting.md +++ b/docs/client/troubleshooting.md @@ -35,9 +35,10 @@ launcher. If the page loads but edits do not appear, check the browser's `/__vite_hmr` WebSocket and the reverse proxy's upgrade handling. Speech uses the separate `/v1/realtime` WebSocket on the same public origin. -For a startup dependency error, check Node/npm on the process manager's PATH, -Python venv support, and `setsid`/`curl`. The first start installs dependencies; -later starts reuse them until frontend manifests change. Changes to root +For a startup dependency error, check uv 0.12.5 and Node/npm on the process +manager's PATH, plus `setsid`/`curl`. uv manages Python 3.12 in +`s2s-client/.venv` and checks the committed lock on every start. Frontend +dependencies are reused until their manifests change. Changes to root `.env` or launcher settings require a restart. ## The assistant promises a tool but nothing happens @@ -126,7 +127,7 @@ From the repository root, run the frontend checks and local backend fixtures: ```bash npm --prefix s2s-frontend run check npm --prefix s2s-frontend run build -.venv/bin/python -m unittest discover -s s2s-client/tests -v +make -C s2s-client check ``` These checks cover frontend contracts and local session/queue/WebSocket diff --git a/docs/client/usage.md b/docs/client/usage.md index 6cafee3..933a29d 100644 --- a/docs/client/usage.md +++ b/docs/client/usage.md @@ -12,8 +12,8 @@ From the repository root: S2S_PROXY_TARGET=ws://localhost:8765/v1/realtime ./start_ui.sh ``` -The launcher loads the root `.env`, creates `.venv` if needed, installs the -client requirements when the dependency check fails, and installs the locked +The launcher loads the root `.env`, syncs `s2s-client/.venv` from the committed +`s2s-client/uv.lock`, and installs the locked frontend dependencies when needed. Vite serves the React frontend on port 8420 and proxies the backend routes to Uvicorn on `127.0.0.1:8421`. `UI_PORT` overrides the public port; `S2S_BACKEND_PORT` overrides the internal @@ -21,7 +21,7 @@ port. Both servers reload source edits, and stopping the launcher stops both. The speech pipeline remains a separate service. Because `.env` is sourced by the script, values in that file can replace variables supplied on the command line. -Use Node.js 22.18+ on the 22.x line or Node.js 24+, npm, Python 3 with venv, +Use Node.js 22.18+ on the 22.x line or Node.js 24+, npm, uv 0.12.5, Bash 4.4+, `setsid`, and `curl`. Remote staging access also needs the exact hostname in `S2S_DEV_ALLOWED_HOSTS`, HTTPS, and WebSocket forwarding for speech and Vite HMR. See [frontend development setup](../../s2s-frontend/README.md). @@ -29,15 +29,18 @@ and Vite HMR. See [frontend development setup](../../s2s-frontend/README.md). To install and start the backend manually: ```bash -python3 -m venv .venv -.venv/bin/python -m pip install -r s2s-client/requirements.txt +uv sync --project s2s-client --locked S2S_PROXY_TARGET=ws://localhost:8765/v1/realtime \ - .venv/bin/python -m uvicorn --app-dir s2s-client server:app \ + uv run --project s2s-client --locked uvicorn --app-dir s2s-client s2s_client.main:app \ --host 127.0.0.1 --port 8421 --reload --reload-dir s2s-client ``` Then, in a second terminal, run `cd s2s-frontend`, `npm ci`, and `npm run dev`. +uv provisions Python 3.12 when needed. The Python environment belongs only to +`s2s-client`; an existing root `.venv` for speech dependencies is left alone. +See [development and compatibility](development.md) for checks and schema rules. + Open , tap the orb/microphone control, grant microphone access, and speak. Microphone access requires HTTPS or a local browser origin; use HTTPS when accessing the UI from another machine. diff --git a/s2s-client/CHANGELOG.md b/s2s-client/CHANGELOG.md index 8d574dd..f9b1abd 100644 --- a/s2s-client/CHANGELOG.md +++ b/s2s-client/CHANGELOG.md @@ -10,8 +10,12 @@ the implementation at the recorded date; current operating instructions live in ### Changed - Added a uv-managed Python 3.12 environment and Ruff, mypy, and pytest quality - checks. Split the FastAPI service into configuration, route, provider, + checks, all contained in `s2s-client/`. Startup and Docker use the committed + lockfile. Split the FastAPI service into configuration, route, provider, context/reasoning, and realtime modules while retaining `server:app` startup. +- Added Pydantic response contracts with permissive upstream fields and original + payload serialization. Captured HTTP, request-schema, and realtime regression + fixtures preserve existing API behavior, including known failures. - Removed the legacy browser site in favor of the React application in `s2s-frontend`. FastAPI now serves only backend and documentation routes. - Retired daily talk-time accounting and the associated Hugging Face account diff --git a/s2s-client/Dockerfile b/s2s-client/Dockerfile index 5e7729c..4d43a41 100644 --- a/s2s-client/Dockerfile +++ b/s2s-client/Dockerfile @@ -2,16 +2,24 @@ # Serve backend APIs, the WebSocket proxy, and shared project documentation. # Run the React/Vite frontend separately; this image contains no browser UI. # HF Spaces (sdk: docker) routes traffic to $PORT, default 7860. -FROM python:3.11-slim +FROM python:3.12.14-slim + +COPY --from=ghcr.io/astral-sh/uv:0.12.5 /uv /uvx /bin/ + +ENV UV_LINK_MODE=copy \ + UV_PYTHON_DOWNLOADS=never \ + PATH="/app/s2s-client/.venv/bin:$PATH" WORKDIR /app/s2s-client -COPY s2s-client/requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt +COPY s2s-client/pyproject.toml s2s-client/uv.lock ./ +RUN uv sync --locked --no-dev --no-install-project -COPY s2s-client/ . +COPY s2s-client/README.md s2s-client/server.py ./ +COPY s2s-client/s2s_client/ ./s2s_client/ COPY docs/ /app/docs/ +RUN uv sync --locked --no-dev EXPOSE 7860 -CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "7860"] +CMD ["uvicorn", "s2s_client.main:app", "--host", "0.0.0.0", "--port", "7860"] diff --git a/s2s-client/README.md b/s2s-client/README.md index 142e979..2d7cf86 100644 --- a/s2s-client/README.md +++ b/s2s-client/README.md @@ -31,17 +31,24 @@ Build from the repository root so the container includes the documentation: docker build -f s2s-client/Dockerfile -t s2s-client . ``` -Run the backend regression tests from the repository root after installing -`requirements.txt` in `.venv`: +The Python project is contained in this directory: `pyproject.toml`, `uv.lock`, +`.python-version`, `.venv`, quality tools, and tests. Use uv 0.12.5 and Python +3.12 (uv can provision Python). From the repository root: ```bash -.venv/bin/python -m unittest discover -s s2s-client/tests -v +make -C s2s-client install +make -C s2s-client check ``` Tests isolate environment configuration, use HTTP fixtures for allocation and caller authentication, and start a loopback WebSocket service to verify audio frames, keepalive, and teardown. No deployed services or database are needed. +See [backend development and compatibility](../docs/client/development.md) for +formatting, typing, schema policy, optional Git hooks, and deployment checks. +`uvicorn server:app --app-dir s2s-client` remains a supported entry point; +the implementation now lives in `s2s_client/`. + FastAPI's API explorer remains at `/docs`. Project documents are served as files below `/docs/client/`, `/docs/speech/`, `/docs/adr/`, `/docs/examples/`, and `/docs/proposals/`. The historical `/project/` and `/public/docs.html` URLs diff --git a/s2s-client/requirements.txt b/s2s-client/requirements.txt deleted file mode 100644 index 9cb3e8e..0000000 --- a/s2s-client/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -fastapi==0.115.6 -uvicorn[standard]==0.34.0 -httpx==0.28.1 -python-dotenv==1.2.2 diff --git a/s2s-client/scripts/test_docker_image.sh b/s2s-client/scripts/test_docker_image.sh new file mode 100644 index 0000000..ec0e598 --- /dev/null +++ b/s2s-client/scripts/test_docker_image.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# Build the actual runtime and check HTTP behavior without external services. +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +IMAGE="s2s-client-smoke:$(date +%s)-$$" +container_id="" +cleanup() { + if [ -n "$container_id" ]; then + docker rm --force "$container_id" >/dev/null + fi + docker image rm "$IMAGE" >/dev/null 2>&1 || true +} +trap cleanup EXIT + +docker build --file "$ROOT_DIR/s2s-client/Dockerfile" --tag "$IMAGE" "$ROOT_DIR" +container_id="$(docker run --detach --publish 127.0.0.1::7860 "$IMAGE")" +port="$(docker port "$container_id" 7860/tcp)" +ready=false +for ((attempt = 0; attempt < 60; attempt++)); do + if curl --noproxy '*' --silent --fail --max-time 1 "http://$port/api/config" >/dev/null; then + ready=true + break + fi + sleep 0.5 +done +if [ "$ready" != true ]; then + docker logs "$container_id" + exit 1 +fi +docker exec -i "$container_id" python - <<'PY' +import json +import urllib.error +import urllib.request + +base = "http://127.0.0.1:7860" +with urllib.request.urlopen(base + "/api/config") as response: + assert json.load(response) == { + "search": False, "lb": False, "allowDirect": True, "s2sUrl": "", + "primaryPrompt": "", "reasoningType": "PROXY", "injectProfile": False, + "debugPrompt": False, "startSessionMode": "", "theme": "", "apipilot": False, + } +for path in ["/docs", "/docs/README.md", "/openapi.json"]: + with urllib.request.urlopen(base + path) as response: + assert response.status == 200 +try: + urllib.request.urlopen(base + "/") +except urllib.error.HTTPError as error: + assert error.code == 404 +else: + raise AssertionError("The backend root must remain 404") +print("Docker runtime smoke checks passed") +PY diff --git a/s2s-frontend/config/launcher.integration.ts b/s2s-frontend/config/launcher.integration.ts index cce8d11..314e1b1 100644 --- a/s2s-frontend/config/launcher.integration.ts +++ b/s2s-frontend/config/launcher.integration.ts @@ -66,14 +66,19 @@ async function launcher(context: TestContext, env: Record = {}) const root = await mkdtemp(join(tmpdir(), "s2s-launcher-")); const frontend = join(root, "s2s-frontend"); const modules = join(frontend, "node_modules"); - await mkdir(join(root, ".venv/bin"), { recursive: true }); - await mkdir(join(root, "s2s-client")); + await mkdir(join(root, "s2s-client/.venv/bin"), { recursive: true }); + await mkdir(join(root, "bin")); await mkdir(join(modules, "vite/bin"), { recursive: true }); await copyFile(new URL("../../start_ui.sh", import.meta.url), join(root, "start_ui.sh")); - await writeFile(join(root, "s2s-client/requirements.txt"), ""); - await writeFile(join(root, ".venv/bin/activate"), 'export PATH="$S2S_TEST_ROOT/.venv/bin:$PATH"\n'); + await writeFile(join(root, "s2s-client/pyproject.toml"), ""); + await writeFile(join(root, "s2s-client/uv.lock"), ""); await writeFile( - join(root, ".venv/bin/python"), + join(root, "bin/uv"), + '#!/bin/bash\nprintf "%s\\n" "$@" > "$S2S_TEST_ROOT/uv-args"\nprintf "%s" "$UV_PROJECT_ENVIRONMENT" > "$S2S_TEST_ROOT/uv-environment"\nexit "${S2S_TEST_UV_EXIT:-0}"\n', + { mode: 0o755 }, + ); + await writeFile( + join(root, "s2s-client/.venv/bin/python"), '#!/bin/bash\nif [ "$1" = "-c" ]; then exit 0; fi\nexec node "$S2S_TEST_ROOT/child.mjs" backend "$@"\n', { mode: 0o755 }, ); @@ -94,6 +99,7 @@ async function launcher(context: TestContext, env: Record = {}) cwd: tmpdir(), env: { ...process.env, + PATH: `${join(root, "bin")}:${process.env.PATH ?? ""}`, UI_PORT: String(publicPort), UI_HOST: "0.0.0.0", NODE_ENV: "production", @@ -138,6 +144,11 @@ for (const signal of ["SIGINT", "SIGTERM"] as const) { nodeEnv: "development", }); assert.equal(run.output().includes("Installing"), false); + assert.equal( + await readFile(join(run.root, "uv-args"), "utf8"), + `sync\n--project\n${run.root}/s2s-client\n--locked\n`, + ); + assert.equal(await readFile(join(run.root, "uv-environment"), "utf8"), `${run.root}/s2s-client/.venv`); run.child.kill(signal); assert.equal(await run.exited, signal === "SIGINT" ? 130 : 143); for (const role of ["backend", "frontend"]) { @@ -168,6 +179,13 @@ await test("does not start Vite when the backend fails readiness", { timeout: 10 assert.match(run.output(), /FastAPI exited before it was ready/); }); +await test("does not start either server when uv sync fails", { timeout: 10_000 }, async (context) => { + const run = await launcher(context, { S2S_TEST_UV_EXIT: "23" }); + assert.equal(await run.exited, 23, run.output()); + await assert.rejects(readFile(join(run.root, "backend.json"))); + await assert.rejects(readFile(join(run.root, "frontend.json"))); +}); + await test("stops Vite if the backend exits after startup", { timeout: 10_000 }, async (context) => { const run = await launcher(context); await waitForFile(join(run.root, "frontend-worker-ready")); diff --git a/start_ui.sh b/start_ui.sh index 7bc4076..2554da4 100755 --- a/start_ui.sh +++ b/start_ui.sh @@ -2,8 +2,8 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -VENV_DIR="$ROOT_DIR/.venv" -REQ_FILE="$ROOT_DIR/s2s-client/requirements.txt" +CLIENT_DIR="$ROOT_DIR/s2s-client" +VENV_DIR="$CLIENT_DIR/.venv" FRONTEND_DIR="$ROOT_DIR/s2s-frontend" if [ -f "$ROOT_DIR/.env" ]; then set -a @@ -20,7 +20,7 @@ if (( BASH_VERSINFO[0] < 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] < 4) )) exit 1 fi -for required_command in python3 node npm setsid curl; do +for required_command in uv node npm setsid curl; do if ! command -v "$required_command" >/dev/null 2>&1; then echo "Error: $required_command was not found." exit 1 @@ -46,22 +46,13 @@ if [ "$UI_PORT" = "$S2S_BACKEND_PORT" ]; then exit 1 fi -if [ ! -d "$VENV_DIR" ]; then - echo "Creating the virtual environment in .venv..." - python3 -m venv "$VENV_DIR" -fi - -source "$VENV_DIR/bin/activate" - -if [ ! -f "$REQ_FILE" ]; then - echo "Error: requirements file was not found: $REQ_FILE" +if [ ! -f "$CLIENT_DIR/pyproject.toml" ] || [ ! -f "$CLIENT_DIR/uv.lock" ]; then + echo "Error: s2s-client/pyproject.toml and s2s-client/uv.lock are required." exit 1 fi -if ! python -c "import fastapi, uvicorn, httpx, dotenv, websockets" >/dev/null 2>&1; then - echo "Installing dependencies from s2s-client/requirements.txt..." - python -m pip install -r "$REQ_FILE" -fi +# uv manages only the client environment; an existing root speech venv is separate. +UV_PROJECT_ENVIRONMENT="$VENV_DIR" uv sync --project "$CLIENT_DIR" --locked # Keep restarts offline when the installed frontend manifests have not changed. if [ ! -f "$FRONTEND_DIR/node_modules/vite/bin/vite.js" ] || \ @@ -118,10 +109,10 @@ echo " Press Ctrl+C in this terminal to stop both servers." echo "----------------------------------------------------" cd "$ROOT_DIR" -setsid "$VENV_DIR/bin/python" -m uvicorn server:app \ - --app-dir "$ROOT_DIR/s2s-client" \ +setsid "$VENV_DIR/bin/python" -m uvicorn s2s_client.main:app \ + --app-dir "$CLIENT_DIR" \ --host 127.0.0.1 --port "$S2S_BACKEND_PORT" \ - --reload --reload-dir "$ROOT_DIR/s2s-client" & + --reload --reload-dir "$CLIENT_DIR" & backend_pid=$! child_pids+=("$backend_pid")