Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .github/workflows/python-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ jobs:
run: |
npm ci
npx playwright install --with-deps chromium
- name: Type-check browser tests
run: npm run check:e2e
- name: Test browser workflows
run: npm run test:e2e
- name: Upload browser-test diagnostics
Expand Down
12 changes: 11 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,4 +126,14 @@ pytest parallax_maker/test_filename.py::test_function_name
- GPU recommended for reasonable performance
- Memory usage scales with image size and model complexity
- TailwindCSS compilation required only when modifying styles
- SD3 Medium requires latest diffusers from GitHub
- SD3 Medium requires latest diffusers from GitHub

## Svelte 5 migration (in progress)

The Dash UI is being replaced by a Svelte 5 frontend. Dash is frozen as the
behavioral reference; do not add new Dash adapters. Read, in order:
`docs/SVELTE_5_MIGRATION_HANDOFF.md` (background and plan),
`docs/svelte-migration/ARCHITECTURE.md` (target design and API contract), and
`docs/svelte-migration/PARITY.md` (per-callback checklist and progress).
Browser scenarios in `e2e/` are frontend-neutral and run through a `UiDriver`
(`e2e/drivers/`); keep new scenarios free of framework-specific selectors.
373 changes: 373 additions & 0 deletions docs/SVELTE_5_MIGRATION_HANDOFF.md

Large diffs are not rendered by default.

151 changes: 151 additions & 0 deletions docs/svelte-migration/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# Svelte migration: target architecture and API contract

Status: design for PR 1–2. Plan and background are in
[`../SVELTE_5_MIGRATION_HANDOFF.md`](../SVELTE_5_MIGRATION_HANDOFF.md);
progress is tracked in [`PARITY.md`](PARITY.md).

## Decisions (2026-09-24)

- **Dash is frozen as the reference UI.** It is not rewired to new services.
New behavior is characterized on Dash with shared e2e scenarios, extracted into
framework-neutral services, exposed through the API, implemented in Svelte, and
the same scenarios must pass on both UIs.
- **Look and feel stays as-is for now.** A redesign is planned later, so the
Svelte UI keeps colors, spacing and typography in CSS custom properties /
Tailwind theme tokens and keeps layout in a few shell components.
- **One process during the migration.** The existing Dash Flask server also
serves the API (`/api/v1/`) and the built Svelte app (`/next/`). Both UIs share
the `AppState` cache and `appstate-*` directories, so a project can be opened in
either. The supported execution model is a single process.
- **Project ID = `appstate-*` directory name**, so existing projects, the e2e
oracle (`/__e2e__/state`) and artifacts endpoints work unchanged.

## Backend layout

```
parallax_maker/
runtime.py # composition root: Runtime dataclass (factories, services, registry)
api/
__init__.py # create_api_blueprint(runtime) -> flask.Blueprint
schemas.py # pydantic v2 request/response models (single source of truth)
projects.py # project/asset/threshold/slice routes
jobs.py # JobManager: single worker thread, progress, results
errors.py # domain exception -> HTTP mapping
assets.py # logical asset IDs -> files inside the project dir (containment checked)
server.py # create_server(runtime): Dash app + API blueprint + /next static; main()
static/next/ # built Svelte app (generated by `npm run build:frontend`; packaged)
frontend/ # Svelte 5 + TS + Vite source
```

`Runtime` owns: depth/segmentation/inpainting/upscaler factories, the three
existing services built from those factories, the `ProjectRegistry`
(per-project `threading.RLock`, revision counter, active job ID) and the
`JobManager`. `runtime.create_runtime()` builds production dependencies;
`e2e_support.fakes.create_fake_runtime()` builds fakes. `install_fakes()` keeps
patching the frozen Dash module *and* the e2e server passes a fake runtime to
`create_server`, so both UIs use the same fakes.

Entry points: `parallax-maker` switches to `server:main` (Dash at `/`, Svelte at
`/next/`) once PR 2 lands; at cutover Svelte moves to `/` and Dash is removed.

## HTTP contract (v1)

All JSON is camelCase. Errors: `{"error": {"code": str, "message": str}}`.

| Situation | Status | code |
| --- | --- | --- |
| Malformed body / invalid parameter | 400 | `invalid_request` |
| Unknown project/slice/job/asset | 404 | `not_found` |
| `WorkflowNotReady` / service validation errors | 409 | `not_ready` |
| Project busy (another mutation or job running) | 409 | `busy` |
| Stale `baseRevision` | 409 | `stale_revision` |
| Provider/model failure | 502 | `provider_error` |
| Unexpected | 500 | `internal` (details logged server-side only) |

`WorkflowUnchanged` is **not** an error: the response is `200` with the current
project view and `"changed": false`.

### Project view (public state projection)

```ts
type ProjectView = {
id: string; // appstate-* name
revision: number; // bumps on every mutation; used for stale-response rejection
image: { width: number; height: number } | null;
assets: { input: AssetRef | null; depth: AssetRef | null };
depthModel: string;
numSlices: number;
thresholds: number[]; // numSlices + 1 boundaries
slices: SliceView[];
selectedSlice: number | null;
busy: { jobId: string; kind: JobKind } | null;
// later PRs add: segmentation, inpainting, camera, export settings
};
type SliceView = {
index: number; depth: number; version: number;
canUndo: boolean; canRedo: boolean;
positivePrompt: string; negativePrompt: string;
image: AssetRef; // raw RGBA slice
thumbnail: AssetRef; // checkerboard composite for display
};
type AssetRef = { url: string }; // /api/v1/projects/{id}/assets/{assetId}?rev={revision}
```

Never serialize `AppState`, PIL/NumPy objects, model objects or credentials.

### Endpoints for the first vertical slice (PR 2)

| Method & path | Body | Result |
| --- | --- | --- |
| `POST /api/v1/projects` | multipart `image` | `201 ProjectView` (upload; validates via `WorkflowService.upload_image`) |
| `POST /api/v1/projects/restore` | multipart `state` (legacy `appstate.json`) | `200 ProjectView`; requires the server-side directory, as today |
| `GET /api/v1/projects/{id}` | – | `ProjectView` |
| `POST /api/v1/projects/{id}/depth` | `{model}` | `202 {job}` |
| `PUT /api/v1/projects/{id}/slice-count` | `{numSlices}` | `ProjectView` |
| `PUT /api/v1/projects/{id}/thresholds` | `{values, baseRevision}` | `ProjectView` |
| `POST /api/v1/projects/{id}/slices` | `{}` | `202 {job}` |
| `GET /api/v1/jobs/{jobId}` | – | `{id, kind, status: queued/running/succeeded/failed, progress: 0..1, error?, project?: ProjectView}` |
| `GET /api/v1/projects/{id}/assets/{assetId}` | – | image bytes, `Cache-Control: no-cache`, strong ETag |
| `GET /api/v1/logs?after={seq}` | – | `{entries: [{seq, level, message}], next}` (replaces the Dash log pane) |

Asset IDs are logical (`input`, `depth`, `slice-{i}`, `slice-{i}-thumb`, later
`mask-{i}`, `candidate-{gen}-{k}`), resolved server-side; raw paths are never
accepted. `/next/` must not reuse the legacy unrestricted `/{SRV_DIR}/<path>` route.

### Concurrency

- Every mutating request takes the project lock; if a job holds it the request
fails fast with `409 busy` (the UI disables controls while busy).
- `JobManager` runs jobs on one worker thread; the progress reporter updates the
job record. The client polls `GET /jobs/{id}` (~250 ms) until terminal.
- Each response carries `revision`; the client ignores views older than the one
it holds. Mutations that depend on what the user saw send `baseRevision`.

## Frontend layout

```
frontend/
src/lib/api/ # fetch client, generated types (from pydantic JSON Schema), job polling
src/lib/state/ # project.svelte.ts ($state runes), ui.svelte.ts (tabs, tools, zoom)
src/lib/components/ # one folder per workflow: upload/, depth/, slices/, segmentation/, canvas/, inpainting/, export/, config/
src/lib/geometry.ts # screen <-> image coordinate transforms (unit tested)
src/App.svelte
```

- Svelte 5 runes only (`$state`, `$derived`, `$effect` sparingly, `$props`).
No reproduction of Dash trigger stores or callback cycles.
- Backend state (anything persisted) comes only from `ProjectView`; Svelte owns
transient UI state (active tab, tool, zoom/pan, in-progress strokes).
- Stable test hooks: `data-testid` on every control the e2e `SvelteDriver` uses.
- Tailwind v4 via `@tailwindcss/vite`; theme tokens in `src/app.css`.

## Tooling and CI additions

- `package.json` scripts: `build:frontend`, `check:frontend` (svelte-check),
`test:frontend` (vitest), `gen:api-types` (pydantic schema → TS). CI runs all
four, fails if generated types are stale, and runs Playwright for both the
`dash` and `svelte` projects.
- Python package data includes `parallax_maker/static/next/**`; a CI step builds
a wheel and checks that the Svelte `index.html` and assets are inside it.
- Pin versions compatible with each other (verify svelte-check against the
chosen TypeScript major) and commit `package-lock.json`.
Loading
Loading