immersive-studio 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. immersive_studio-0.1.0/PKG-INFO +413 -0
  2. immersive_studio-0.1.0/README.md +371 -0
  3. immersive_studio-0.1.0/pyproject.toml +78 -0
  4. immersive_studio-0.1.0/setup.cfg +4 -0
  5. immersive_studio-0.1.0/src/immersive_studio/__init__.py +54 -0
  6. immersive_studio-0.1.0/src/immersive_studio.egg-info/PKG-INFO +413 -0
  7. immersive_studio-0.1.0/src/immersive_studio.egg-info/SOURCES.txt +68 -0
  8. immersive_studio-0.1.0/src/immersive_studio.egg-info/dependency_links.txt +1 -0
  9. immersive_studio-0.1.0/src/immersive_studio.egg-info/entry_points.txt +2 -0
  10. immersive_studio-0.1.0/src/immersive_studio.egg-info/requires.txt +24 -0
  11. immersive_studio-0.1.0/src/immersive_studio.egg-info/top_level.txt +2 -0
  12. immersive_studio-0.1.0/src/studio_worker/__init__.py +3 -0
  13. immersive_studio-0.1.0/src/studio_worker/__main__.py +4 -0
  14. immersive_studio-0.1.0/src/studio_worker/api.py +461 -0
  15. immersive_studio-0.1.0/src/studio_worker/attribution.py +93 -0
  16. immersive_studio-0.1.0/src/studio_worker/billing_config.py +111 -0
  17. immersive_studio-0.1.0/src/studio_worker/billing_routes.py +137 -0
  18. immersive_studio-0.1.0/src/studio_worker/blender/export_mesh.py +355 -0
  19. immersive_studio-0.1.0/src/studio_worker/cli.py +338 -0
  20. immersive_studio-0.1.0/src/studio_worker/comfy_client.py +200 -0
  21. immersive_studio-0.1.0/src/studio_worker/comfy_workflows/sd15_albedo_v1.api.json +59 -0
  22. immersive_studio-0.1.0/src/studio_worker/comfy_workflows/sdxl_albedo_v1.api.json +59 -0
  23. immersive_studio-0.1.0/src/studio_worker/data/studio-asset-spec-v0.1.schema.json +106 -0
  24. immersive_studio-0.1.0/src/studio_worker/job_artifacts.py +112 -0
  25. immersive_studio-0.1.0/src/studio_worker/job_runner.py +192 -0
  26. immersive_studio-0.1.0/src/studio_worker/jobs_store.py +165 -0
  27. immersive_studio-0.1.0/src/studio_worker/json_extract.py +39 -0
  28. immersive_studio-0.1.0/src/studio_worker/manifest.py +40 -0
  29. immersive_studio-0.1.0/src/studio_worker/mesh_export.py +140 -0
  30. immersive_studio-0.1.0/src/studio_worker/mock_spec.py +75 -0
  31. immersive_studio-0.1.0/src/studio_worker/moderation.py +43 -0
  32. immersive_studio-0.1.0/src/studio_worker/ollama_client.py +44 -0
  33. immersive_studio-0.1.0/src/studio_worker/pack_writer.py +114 -0
  34. immersive_studio-0.1.0/src/studio_worker/paths.py +86 -0
  35. immersive_studio-0.1.0/src/studio_worker/pbr_keys.py +62 -0
  36. immersive_studio-0.1.0/src/studio_worker/prompts.py +62 -0
  37. immersive_studio-0.1.0/src/studio_worker/queue_executor.py +37 -0
  38. immersive_studio-0.1.0/src/studio_worker/queue_postgres.py +447 -0
  39. immersive_studio-0.1.0/src/studio_worker/queue_redis.py +418 -0
  40. immersive_studio-0.1.0/src/studio_worker/queue_redis_streams.py +481 -0
  41. immersive_studio-0.1.0/src/studio_worker/queue_sqlite.py +426 -0
  42. immersive_studio-0.1.0/src/studio_worker/queue_sqs_postgres.py +218 -0
  43. immersive_studio-0.1.0/src/studio_worker/quotas.py +82 -0
  44. immersive_studio-0.1.0/src/studio_worker/scale_config.py +166 -0
  45. immersive_studio-0.1.0/src/studio_worker/spec_generate.py +61 -0
  46. immersive_studio-0.1.0/src/studio_worker/sqlite_queue.py +53 -0
  47. immersive_studio-0.1.0/src/studio_worker/stripe_billing.py +314 -0
  48. immersive_studio-0.1.0/src/studio_worker/tenant_context.py +84 -0
  49. immersive_studio-0.1.0/src/studio_worker/tenants_db.py +70 -0
  50. immersive_studio-0.1.0/src/studio_worker/tenants_postgres.py +396 -0
  51. immersive_studio-0.1.0/src/studio_worker/tenants_sqlite.py +414 -0
  52. immersive_studio-0.1.0/src/studio_worker/texture_pipeline.py +174 -0
  53. immersive_studio-0.1.0/src/studio_worker/tiers.py +60 -0
  54. immersive_studio-0.1.0/src/studio_worker/validate.py +121 -0
  55. immersive_studio-0.1.0/src/studio_worker/workflow_template.py +53 -0
  56. immersive_studio-0.1.0/src/studio_worker/zip_pack.py +23 -0
  57. immersive_studio-0.1.0/tests/test_comfy_client_mock.py +64 -0
  58. immersive_studio-0.1.0/tests/test_gpu_comfy_placeholder.py +47 -0
  59. immersive_studio-0.1.0/tests/test_immersive_studio_sdk.py +28 -0
  60. immersive_studio-0.1.0/tests/test_mesh_export.py +80 -0
  61. immersive_studio-0.1.0/tests/test_moderation.py +14 -0
  62. immersive_studio-0.1.0/tests/test_pbr_keys.py +53 -0
  63. immersive_studio-0.1.0/tests/test_queue_api_idempotency.py +59 -0
  64. immersive_studio-0.1.0/tests/test_queue_redis.py +78 -0
  65. immersive_studio-0.1.0/tests/test_queue_redis_streams.py +85 -0
  66. immersive_studio-0.1.0/tests/test_sqlite_queue.py +102 -0
  67. immersive_studio-0.1.0/tests/test_stripe_billing.py +202 -0
  68. immersive_studio-0.1.0/tests/test_tenant_auth.py +101 -0
  69. immersive_studio-0.1.0/tests/test_validate.py +22 -0
  70. immersive_studio-0.1.0/tests/test_zip_pack.py +18 -0
@@ -0,0 +1,413 @@
1
+ Metadata-Version: 2.4
2
+ Name: immersive-studio
3
+ Version: 0.1.0
4
+ Summary: Immersive Studio CLI and SDK: spec generation, ComfyUI textures, Unity packs, and HTTP worker API.
5
+ Author: Immersive Labs
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/immersive-labs/immersive.labs
8
+ Project-URL: Documentation, https://github.com/immersive-labs/immersive.labs/tree/main/apps/studio-worker
9
+ Project-URL: Repository, https://github.com/immersive-labs/immersive.labs
10
+ Project-URL: Issues, https://github.com/immersive-labs/immersive.labs/issues
11
+ Keywords: cli,game-dev,unity,comfyui,3d,procedural
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Games/Entertainment
20
+ Classifier: Topic :: Multimedia :: Graphics :: 3D Modeling
21
+ Requires-Python: >=3.11
22
+ Description-Content-Type: text/markdown
23
+ Requires-Dist: fastapi>=0.115
24
+ Requires-Dist: httpx>=0.27
25
+ Requires-Dist: jsonschema>=4.21
26
+ Requires-Dist: stripe>=10.0
27
+ Requires-Dist: uvicorn[standard]>=0.32
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest>=8.0; extra == "dev"
30
+ Requires-Dist: respx>=0.21.0; extra == "dev"
31
+ Requires-Dist: fakeredis>=2.23; extra == "dev"
32
+ Provides-Extra: postgres
33
+ Requires-Dist: psycopg[binary]>=3.2; extra == "postgres"
34
+ Provides-Extra: redis
35
+ Requires-Dist: redis>=5.0; extra == "redis"
36
+ Provides-Extra: s3
37
+ Requires-Dist: boto3>=1.34; extra == "s3"
38
+ Provides-Extra: scale
39
+ Requires-Dist: psycopg[binary]>=3.2; extra == "scale"
40
+ Requires-Dist: boto3>=1.34; extra == "scale"
41
+ Requires-Dist: redis>=5.0; extra == "scale"
42
+
43
+ # Immersive Studio (CLI + SDK)
44
+
45
+ Publishable **PyPI** package **`immersive-studio`**: the **`immersive-studio`** terminal command, importable Python SDK **`immersive_studio`**, and the same **FastAPI** worker used by the **Video Game Generation Studio** (`/studio` in `@immersive/web`). Features include Ollama/mock spec generation, **ComfyUI** texture passes, optional **Blender** placeholder `.glb`, **Unity** pack layout (`manifest.json`, `spec.json`, `pack.zip`), and the **HTTP API**.
46
+
47
+ ## Install (PyPI — like any other CLI)
48
+
49
+ After the package is published on PyPI as **`immersive-studio`** (maintainers: see **Publishing** below), end users can install globally with [**pipx**](https://pipx.pypa.io/) so the executable lands on `PATH` without touching system Python:
50
+
51
+ ```bash
52
+ pipx install immersive-studio
53
+ immersive-studio doctor
54
+ ```
55
+
56
+ Or into the active virtual environment:
57
+
58
+ ```bash
59
+ pip install immersive-studio
60
+ python -m studio_worker.cli doctor
61
+ ```
62
+
63
+ **Optional extras:** `pip install immersive-studio[postgres]`, `[redis]`, `[s3]`, or `[scale]` for production-style backends (see Environment table below).
64
+
65
+ > **Note:** The distribution was previously named `immersive-studio-worker`. Use **`immersive-studio`** going forward.
66
+
67
+ ## Python SDK
68
+
69
+ ```python
70
+ from immersive_studio import __version__, run_studio_job
71
+
72
+ result = run_studio_job(
73
+ user_prompt="wooden barrel",
74
+ category="prop",
75
+ style_preset="toon_bold",
76
+ use_mock=True,
77
+ generate_textures=False,
78
+ unity_urp_hint="6000.0.x LTS (pin when smoke-tested)",
79
+ export_mesh=False,
80
+ )
81
+ print(result["job_id"], result["zip_path"])
82
+ ```
83
+
84
+ Public symbols: `__version__`, `run_studio_job`, `validate_asset_spec_file`, `generate_asset_spec_with_metadata`, `comfy_base_url`, `comfy_reachability`. Advanced integrations can import from the `studio_worker` package directly.
85
+
86
+ ## Publishing to PyPI
87
+
88
+ I cannot create or “register” a PyPI project on your behalf (that requires you to sign in at [pypi.org](https://pypi.org)). Use an API token as below to upload from CI or your laptop.
89
+
90
+ ### GitHub Actions publish (this repo)
91
+
92
+ The workflow **`.github/workflows/publish-immersive-studio.yml`** uploads with **`twine`** only. It **does not** use OIDC or `pypa/gh-action-pypi-publish`, so you will not see **`invalid-publisher`** from that path.
93
+
94
+ 1. Create a PyPI **API token** with **upload** scope for **`immersive-studio`** ([PyPI → API tokens](https://pypi.org/manage/account/token/)).
95
+ 2. In GitHub: **Settings → Secrets and variables → Actions** → **Repository secrets** → **New repository secret**.
96
+ - **Name:** `PYPI_API_TOKEN` (exact spelling, all caps).
97
+ - **Value:** the token string (often starts with `pypi-`).
98
+ Do **not** put it under *Dependabot* or *Codespaces* only; it must be a **repository** secret for this repo.
99
+ 3. Run **Actions → “PyPI publish (immersive-studio package)” → Run workflow** on **`main`**, or publish a **GitHub Release** to trigger it. Use a **new** run after changing secrets (re-running a very old failed job can still use old workflow YAML).
100
+
101
+ If the job fails immediately with **“PYPI_API_TOKEN is missing or empty”**, GitHub did not inject the secret (wrong name, empty value, or workflow running from a fork without secrets).
102
+
103
+ ### Optional: PyPI trusted publishing (OIDC)
104
+
105
+ OIDC is **not** used by the publish workflow above. To use trusted publishing instead, upload from your machine with `twine` or configure a **separate** workflow; PyPI’s form needs your **owner/repo**, workflow filename **`publish-immersive-studio.yml`**, and an **empty** environment name unless you add a matching GitHub Environment.
106
+
107
+ ### Manual upload (API token)
108
+
109
+ From `apps/studio-worker` with [build](https://pypi.org/project/build/) and [twine](https://twine.readthedocs.io/):
110
+
111
+ ```bash
112
+ pip install build twine
113
+ python -m build
114
+ twine upload dist/*
115
+ ```
116
+
117
+ Use a [PyPI API token](https://pypi.org/manage/account/token/) with upload scope; the first upload can create the project if the name is free.
118
+
119
+ If the project name **`immersive-studio`** is already taken on PyPI, change `name` in `pyproject.toml` to an available name (for example `immersive-labs-studio`) and update install docs accordingly.
120
+
121
+ When you change **`packages/studio-types/schema/studio-asset-spec-v0.1.schema.json`**, copy the file to **`src/studio_worker/data/studio-asset-spec-v0.1.schema.json`** before releasing so the wheel matches the canonical schema.
122
+
123
+ ## Setup (monorepo / contributors)
124
+
125
+ From repository root:
126
+
127
+ ```bash
128
+ cd apps/studio-worker
129
+ python -m venv .venv
130
+ source .venv/bin/activate # Windows: .venv\Scripts\activate
131
+ pip install -e .
132
+ ```
133
+
134
+ **Invoking the CLI:** After `pip install -e .`, the `immersive-studio` command lives next to your Python interpreter (for example `.venv\Scripts\immersive-studio.exe`, or `%APPDATA%\Python\Python3xx\Scripts\immersive-studio.exe` when pip used a **user** install). If your shell says `command not found`, either add that `Scripts` folder to your **PATH**, or call the module directly (works from any directory once the package is installed):
135
+
136
+ ```bash
137
+ python -m studio_worker.cli doctor
138
+ ```
139
+
140
+ From this app directory you can also use the repo wrappers (no PATH change):
141
+
142
+ ```bash
143
+ ./scripts/immersive-studio.sh doctor # Git Bash / MSYS
144
+ scripts\immersive-studio.cmd doctor # cmd.exe / PowerShell
145
+ ```
146
+
147
+ Ensure [Ollama](https://ollama.com/) is running locally if you use LLM generation (default `http://127.0.0.1:11434`). Pull a JSON-capable model, for example:
148
+
149
+ ```bash
150
+ ollama pull llama3.2
151
+ ```
152
+
153
+ For **texture generation**, run [ComfyUI](https://github.com/comfyanonymous/ComfyUI) (default `http://127.0.0.1:8188`) and install a checkpoint that matches `STUDIO_COMFY_CHECKPOINT`. See [`comfy/README.md`](./comfy/README.md).
154
+
155
+ After installing ComfyUI and (for mesh export) [Blender](https://www.blender.org/), verify the worker can see both:
156
+
157
+ ```bash
158
+ python -m studio_worker.cli doctor
159
+ ```
160
+
161
+ Exit code `0` means ComfyUI answered at `STUDIO_COMFY_URL` (default `http://127.0.0.1:8188`) and Blender was resolved (PATH, `STUDIO_BLENDER_BIN`, or a standard Windows/macOS install path). Use `python -m studio_worker.cli doctor --comfy-url http://host:8188` to probe a different URL once.
162
+
163
+ To launch ComfyUI from a git checkout once dependencies are installed, set **`COMFYUI_ROOT`** and run [`scripts/start-comfyui-dev.sh`](./scripts/start-comfyui-dev.sh) or [`scripts/start-comfyui-dev.ps1`](./scripts/start-comfyui-dev.ps1) (listens on `127.0.0.1:8188`).
164
+
165
+ ## Docker (VM / production-shaped)
166
+
167
+ The image copies **`packages/studio-types/schema`** and **`apps/studio-worker`** into a layout that matches `STUDIO_REPO_ROOT` (defaults to **`/repo`** in the image).
168
+
169
+ **Build** (from **monorepo root**):
170
+
171
+ ```bash
172
+ docker build -f apps/studio-worker/Dockerfile -t immersive-studio-worker:local .
173
+ ```
174
+
175
+ **Run** (set CORS to every **browser origin** that loads `/studio` — comma-separated, no trailing slashes):
176
+
177
+ ```bash
178
+ docker run -d --name studio-worker --restart unless-stopped \
179
+ -p 8787:8787 \
180
+ -e STUDIO_CORS_ORIGINS='https://YOUR-APP.vercel.app,https://www.yourdomain.com,https://yourdomain.com' \
181
+ -v studio-output:/repo/apps/studio-worker/output \
182
+ immersive-studio-worker:local
183
+ ```
184
+
185
+ If you only use the default Vercel hostname, a single origin is enough: `https://YOUR-APP.vercel.app`. Trailing slashes in the env var are ignored.
186
+
187
+ On a cloud VM behind **Cloudflare Tunnel**, bind Docker to localhost only: **`-p 127.0.0.1:8787:8787`** (see [deploy-gcp-free-vm.md](../../docs/studio/deploy-gcp-free-vm.md)).
188
+
189
+ **Compose** (dev smoke test; overrides `STUDIO_CORS_ORIGINS` as needed):
190
+
191
+ ```bash
192
+ docker compose -f apps/studio-worker/docker-compose.yml up --build
193
+ ```
194
+
195
+ **Free GCP VM + HTTPS + Vercel:** [docs/studio/deploy-gcp-free-vm.md](../../docs/studio/deploy-gcp-free-vm.md) (includes **`scripts/studio-cloudflare-tunnel/`** for `gcloud` + **Cloudflare Tunnel**).
196
+
197
+ ## CLI
198
+
199
+ In the examples below, `immersive-studio` is shorthand for **`python -m studio_worker.cli`** (or the `.exe` on your PATH after you add Python’s `Scripts` folder — see **Setup**).
200
+
201
+ ```bash
202
+ # Generate spec via Ollama (default model from env STUDIO_OLLAMA_MODEL or llama3.2)
203
+ immersive-studio generate-spec --prompt "wooden crate with iron bands" --style-preset toon_bold --category prop
204
+
205
+ # Validated mock spec without Ollama (CI / offline)
206
+ immersive-studio generate-spec --mock --prompt "neon barrel" --style-preset anime_stylized --category prop
207
+
208
+ # Validate a JSON file against schema + studio rules
209
+ immersive-studio validate-spec --file ./spec.json
210
+
211
+ # Write pack folder: manifest.json, spec.json, UnityImportNotes.md, Models/ + Textures/ layout
212
+ immersive-studio pack --spec ./spec.json --output ./out/MyPack
213
+
214
+ # Full persisted job under output/jobs/ (spec + pack + zip + index); optional ComfyUI textures
215
+ immersive-studio run-job --mock --prompt "wooden barrel" --textures
216
+
217
+ # Optional: ComfyUI on a non-default URL for this run only
218
+ immersive-studio run-job --mock --prompt "wooden barrel" --textures --comfy-url http://127.0.0.1:8188
219
+
220
+ # Same, plus headless Blender placeholder `.glb` under Models/ (requires Blender on PATH or STUDIO_BLENDER_BIN)
221
+ immersive-studio run-job --mock --prompt "crate" --export-mesh
222
+
223
+ # Check ComfyUI + Blender before a full job
224
+ immersive-studio doctor
225
+
226
+ # HTTP API for the web UI (CORS allows Vite dev server)
227
+ immersive-studio serve --host 127.0.0.1 --port 8787
228
+
229
+ # Poll SQLite-backed job queue (run multiple terminals or processes for parallel workers)
230
+ immersive-studio queue-worker --worker-id worker-a
231
+ immersive-studio queue-worker --once --worker-id one-shot
232
+
233
+ # Multi-tenant / indie SaaS (after STUDIO_API_AUTH_REQUIRED=1 on the server)
234
+ immersive-studio tenants create --name "Pixel Friends" --tier indie
235
+ immersive-studio tenants issue-key --tenant-id <uuid-from-create> --label "lead-dev-laptop"
236
+ immersive-studio tenants list
237
+ immersive-studio tenants set-tier --tenant-id <uuid> --tier team
238
+ ```
239
+
240
+ ## HTTP API (selected)
241
+
242
+ | Method | Path | Purpose |
243
+ |--------|------|---------|
244
+ | GET | `/api/studio/health` | Worker liveness (`auth_required` flag for clients) |
245
+ | GET | `/api/studio/metrics` | Queue counts by status + `jobs_indexed` (tenant-scoped when auth on) |
246
+ | GET | `/api/studio/comfy-status` | ComfyUI `/system_stats` probe (no API key) |
247
+ | GET | `/api/studio/usage` | Tier + monthly credits used/cap (requires key when auth on) |
248
+ | POST | `/api/studio/generate-spec` | Prompt → validated spec |
249
+ | POST | `/api/studio/pack` | Ad-hoc pack (scoped under `output/packs/…` when auth on) |
250
+ | POST | `/api/studio/jobs/run` | Full job → `output/jobs/job_*/` + `pack.zip` + index |
251
+ | POST | `/api/studio/queue/jobs` | Enqueue async job (SQLite or Postgres); optional `idempotency_key` dedupes per tenant; response `deduplicated` |
252
+ | GET | `/api/studio/queue/jobs` | Recent queue rows (tenant-scoped when auth on) |
253
+ | GET | `/api/studio/queue/jobs/{queue_id}` | Single queue row |
254
+ | GET | `/api/studio/jobs` | List persisted jobs + `jobs_root` path (tenant-scoped when auth on) |
255
+ | GET | `/api/studio/jobs/{job_id}/download` | Download `pack.zip` (local file) or **302** to a remote URL when `STUDIO_JOB_ARTIFACTS` uploads zips |
256
+ | GET | `/api/studio/paths` | Debug paths + active backends (`queue_backend`, `redis_queue_engine` when Redis, `tenants_backend`, …) |
257
+ | GET | `/api/studio/billing/status` | Stripe readiness + linked customer (requires key when auth on) |
258
+ | POST | `/api/studio/billing/checkout-session` | Returns Stripe Checkout URL for `{ "tier": "indie" \| "team" }` |
259
+ | POST | `/api/studio/billing/portal-session` | Returns Stripe Customer Portal URL (manage/cancel) |
260
+ | POST | `/api/studio/billing/webhook` | Stripe webhooks (signing secret); **no API key** |
261
+
262
+ Remote clients send **`Authorization: Bearer <api_key>`** or **`X-API-Key`**. The `/studio` UI stores an optional key in `localStorage` and uses it for fetch + zip download.
263
+
264
+ ## Environment
265
+
266
+ | Variable | Default | Purpose |
267
+ |----------|---------|---------|
268
+ | `STUDIO_OLLAMA_URL` | `http://127.0.0.1:11434` | Ollama base URL |
269
+ | `STUDIO_OLLAMA_MODEL` | `llama3.2` | Chat model name |
270
+ | `STUDIO_REPO_ROOT` | — | When set, writable job/queue data uses `apps/studio-worker/output` under this monorepo root (Docker / local dev). |
271
+ | `STUDIO_WORKER_DATA_DIR` | — | Override writable root for jobs, `queue.sqlite`, and `tenants.sqlite` (default: `~/.immersive-studio/worker` when `STUDIO_REPO_ROOT` is unset). |
272
+ | `STUDIO_COMFY_URL` | `http://127.0.0.1:8188` | ComfyUI server |
273
+ | `STUDIO_COMFY_PROFILE` | `sd15` | `sd15` or `sdxl` (selects workflow + latent size) |
274
+ | `STUDIO_COMFY_CHECKPOINT` | profile-specific | Checkpoint **file name** as shown in ComfyUI |
275
+ | `STUDIO_TEXTURE_MAX_IMAGES` | `32` | Cap variant×slot ComfyUI renders per job (albedo/normal/orm) |
276
+ | `STUDIO_JOBS_MAX_COUNT` | `200` | Index cap; oldest job folders deleted when trimming |
277
+ | `STUDIO_JOBS_MAX_TOTAL_BYTES` | `5368709120` | Target max bytes under `output/jobs/` before pruning |
278
+ | `STUDIO_QUOTAS_DISABLED` | — | Set to `1` to skip quota enforcement |
279
+ | `STUDIO_PROMPT_BLOCKLIST` | — | Comma-separated substrings to block in prompts |
280
+ | `STUDIO_MODERATION_DISABLED` | — | Set to `1` to skip moderation |
281
+ | `STUDIO_API_AUTH_REQUIRED` | off | Set to `1` to require API keys for studio routes (indie SaaS mode) |
282
+ | `STUDIO_CORS_ORIGINS` | Vite dev origins | Comma-separated browser origins, or `*` for any (use carefully in production) |
283
+ | `STRIPE_SECRET_KEY` | — | Secret API key (Dashboard → Developers) — required for Checkout / Portal |
284
+ | `STRIPE_WEBHOOK_SECRET` | — | Webhook signing secret for `POST /api/studio/billing/webhook` |
285
+ | `STRIPE_API_VERSION` | account default | Optional; pin Stripe API version string if you need stability |
286
+ | `STUDIO_STRIPE_PRICE_INDIE` | — | Stripe **Price** id for Indie recurring plan |
287
+ | `STUDIO_STRIPE_PRICE_TEAM` | — | Stripe **Price** id for Small team recurring plan |
288
+ | `STUDIO_STRIPE_PRICE_MAP` | — | Optional `price_abc:indie,price_def:team` overrides / extra prices |
289
+ | `STUDIO_BILLING_SUCCESS_URL` | `/studio?billing=success` | Checkout success redirect (optional: add `{CHECKOUT_SESSION_ID}` per Stripe docs) |
290
+ | `STUDIO_BILLING_CANCEL_URL` | `/studio?billing=cancel` | Checkout cancel redirect |
291
+ | `STUDIO_BILLING_PORTAL_RETURN_URL` | `/studio` | Customer Portal return URL |
292
+ | `STUDIO_BLENDER_BIN` | — | Path to `blender` executable if not on `PATH` |
293
+ | `STUDIO_BLENDER_TIMEOUT_S` | `180` | Max seconds for headless Blender mesh export |
294
+ | `STUDIO_EXPORT_MESH_DEFAULT` | off | Set to `1` / `true` to run mesh export on every `run-job` when the client does not pass `export_mesh` |
295
+ | `DATABASE_URL` | — | Postgres connection string (e.g. Vercel Postgres, Neon). Required when using Postgres backends below. |
296
+ | `STUDIO_QUEUE_BACKEND` | `sqlite` | `postgres` (shared rows in Postgres), `redis` (Redis broker + hashes), or `sqs` (SQS long-poll + Postgres rows — needs `DATABASE_URL`). |
297
+ | `STUDIO_REDIS_URL` / `REDIS_URL` | — | Broker URL when `STUDIO_QUEUE_BACKEND=redis`. |
298
+ | `STUDIO_REDIS_QUEUE_ENGINE` | `zset` | With Redis queue: `zset` (sorted-set dispatch) or `streams` (`XADD` / `XREADGROUP` / `XACK`). |
299
+ | `STUDIO_REDIS_STREAMS_PREFIX` | `{STUDIO_REDIS_QUEUE_PREFIX}:sx` | Key namespace for Streams mode (isolated from zset keys). |
300
+ | `STUDIO_REDIS_STREAM_GROUP` | `studio_workers` | Redis consumer group name for Streams mode. |
301
+ | `STUDIO_REDIS_STREAM_BLOCK_MS` | `5000` | `XREADGROUP` block timeout (ms). |
302
+ | `STUDIO_REDIS_STREAM_MAXLEN` | `50000` | Approximate `MAXLEN` on the dispatch stream (`~`). |
303
+ | `STUDIO_REDIS_QUEUE_PREFIX` | `studio:q` | Key namespace for the Redis queue. |
304
+ | `STUDIO_REDIS_QUEUE_MAX_TIMELINE` | `5000` | Cap on timeline index entries (oldest trimmed). |
305
+ | `STUDIO_SQS_QUEUE_URL` | — | Standard or FIFO SQS queue URL when `STUDIO_QUEUE_BACKEND=sqs` (Postgres holds job metadata). |
306
+ | `STUDIO_SQS_WAIT_SECONDS` | `20` | SQS `ReceiveMessage` long-poll wait (max 20). |
307
+ | `STUDIO_SQS_VISIBILITY_TIMEOUT` | `900` | Visibility timeout for in-flight SQS messages (seconds). |
308
+ | `STUDIO_SQS_ENDPOINT_URL` | — | Optional (e.g. LocalStack). |
309
+ | `AWS_REGION` / `AWS_DEFAULT_REGION` | `us-east-1` | SQS client region when using `sqs` backend. |
310
+ | `STUDIO_TENANTS_BACKEND` | `sqlite` | Set `postgres` with `DATABASE_URL` for shared tenants / API keys / usage. |
311
+ | `STUDIO_JOB_ARTIFACTS` | `local` | `s3` (S3-compatible, incl. R2 via endpoint), or `vercel_blob` to upload `pack.zip` after each job. |
312
+ | `STUDIO_S3_BUCKET` | — | Bucket name when `STUDIO_JOB_ARTIFACTS=s3` (or `r2`). |
313
+ | `STUDIO_S3_REGION` | `auto` | AWS region; use `auto` for Cloudflare R2. |
314
+ | `STUDIO_S3_ENDPOINT_URL` | — | Custom endpoint URL (set for R2 / MinIO). |
315
+ | `STUDIO_S3_KEY_PREFIX` | `immersive-studio/packs` | Prefix for object keys (`{prefix}/{folder}/pack.zip`). |
316
+ | `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` | — | Standard boto3 credentials for S3/R2 (or instance role). |
317
+ | `BLOB_READ_WRITE_TOKEN` | — | Vercel Blob store read-write token when `STUDIO_JOB_ARTIFACTS=vercel_blob`. |
318
+
319
+ **Optional pip extras:** `pip install -e ".[postgres]"` (`psycopg`), `pip install -e ".[redis]"` (`redis` client), `pip install -e ".[s3]"` (`boto3`), or `pip install -e ".[scale]"` (Postgres + Redis + S3 deps together).
320
+
321
+ ## Stripe Billing (subscriptions → tiers)
322
+
323
+ Operations (monitoring, trial emails, optional notify URL): [docs/studio/stripe-monitoring-and-emails.md](../../docs/studio/stripe-monitoring-and-emails.md).
324
+
325
+ Python dependency: **`stripe`** (declared in `pyproject.toml`). Flow:
326
+
327
+ 1. **Stripe Dashboard:** create **Products** with recurring **Prices** (e.g. monthly). Copy each **Price id** (`price_…`) into `STUDIO_STRIPE_PRICE_INDIE` / `STUDIO_STRIPE_PRICE_TEAM`, *or* set **`metadata.tier`** on the Price to `indie` / `team` / `free` (overrides id mapping).
328
+ 2. **Enable Customer portal** (Billing → Customer portal) so `POST /api/studio/billing/portal-session` works.
329
+ 3. **Webhooks:** add endpoint `https://<your-host>/api/studio/billing/webhook` and subscribe at minimum to:
330
+ - `checkout.session.completed`
331
+ - `customer.subscription.created`
332
+ - `customer.subscription.updated`
333
+ - `customer.subscription.deleted`
334
+ Paste the webhook **signing secret** into `STRIPE_WEBHOOK_SECRET`.
335
+ 4. **Runtime env:** `STRIPE_SECRET_KEY`, `STUDIO_API_AUTH_REQUIRED=1`, tenant + API key as today.
336
+ 5. **Indie group:** `tenants create` → `tenants issue-key` → open `/studio`, paste key → **Subscribe** (Checkout). After payment, webhooks link `stripe_customer_id`, set `stripe_subscription_id`, and call **`set_tenant_tier`** from the subscription’s price/metadata.
337
+
338
+ **Tier mapping rules:** For each subscription item’s Price, the worker uses (1) `metadata.tier` if valid, else (2) `STUDIO_STRIPE_PRICE_MAP` / env price ids, else (3) stays conservative (`free` if unknown). **Canceled / unpaid / incomplete_expired** subscriptions downgrade the tenant to **`free`**. **`past_due`** keeps the paid tier until Stripe resolves or cancels (adjust in `stripe_billing.py` if you prefer stricter cutoffs).
339
+
340
+ **Local testing:** [Stripe CLI](https://stripe.com/docs/stripe-cli) `stripe listen --forward-to localhost:8787/api/studio/billing/webhook` and use the printed **`whsec_…`** as `STRIPE_WEBHOOK_SECRET`.
341
+
342
+ **Manual override:** `immersive-studio tenants set-tier` still works for support / migrations; the next subscription webhook may overwrite tier until billing state matches.
343
+
344
+ ## Indie / multi-tenant tiers (SaaS mode)
345
+
346
+ When **`STUDIO_API_AUTH_REQUIRED=1`**, each **workspace** (small studio / customer group) is a **tenant** with:
347
+
348
+ - **API keys** (hashed in SQLite — only shown once at `tenants issue-key`)
349
+ - **Monthly credits** (UTC calendar month), consumed by:
350
+ - `generate-spec`: **1** credit
351
+ - `jobs/run` or **queue enqueue** (sync cost up front): **2** credits, or **6** with **textures**
352
+ - **Concurrent job cap** (SQLite counter per tenant while `run_studio_job` is in flight)
353
+ - **Texture jobs** blocked on **Free** tier
354
+
355
+ Tiers are defined in `src/studio_worker/tiers.py` (`free`, `indie`, `team`). **Stripe webhooks** update `tier_id` from the active subscription; **`tenants set-tier`** remains a manual admin override.
356
+
357
+ **Per-tenant isolation:** job index entries, queue rows, and ad-hoc pack paths are tagged with `tenant_id` so one API key cannot list or download another workspace’s jobs.
358
+
359
+ Default **without** `STUDIO_API_AUTH_REQUIRED`: single-machine dev — unlimited dev tier, no keys (same as before).
360
+
361
+ ## Blender placeholder mesh
362
+
363
+ Full pipeline reference (env, CI, Unity): [docs/studio/essentials.md](../../docs/studio/essentials.md).
364
+
365
+ Exports procedural placeholder geometry as `.glb` from a spec (placeholder until full kitbash lands):
366
+
367
+ ```bash
368
+ blender --background --python path/to/site-packages/studio_worker/blender/export_mesh.py -- --spec path/to/spec.json --output path/to/out.glb
369
+ ```
370
+
371
+ Use `pack/spec.json` as the `--spec` argument when generating from a worker pack. Requires Blender 4.x on your `PATH`, or set **`STUDIO_BLENDER_BIN`**.
372
+
373
+ **Integrated path:** `immersive-studio run-job … --export-mesh`, **`POST /api/studio/jobs/run`** / queue with **`export_mesh: true`**, or the **Export placeholder mesh** checkbox on `/studio`. Successful runs place **`Models/<asset_id>/<asset_id>.glb`** and set **`manifest.toolchain.mesh_pipeline`** (`blender:export_mesh.py+ok` or `+error`). Failures are non-fatal: the job still completes with textures/spec; check **`mesh_logs`** in the API response or job metadata.
374
+
375
+ **Profiles:** `studio_worker/blender/export_mesh.py` picks a coarse shape from **`category`**: `prop` — stacked boxes with deterministic jitter; `environment_piece` — wide platform; `character_base` — two-block silhouette; `material_library` — small swatch cube; other categories fall back to a beveled cube scaled by **`target_height_m`**. PBR material names come from **`studio_worker/pbr_keys.py`** (same ordering as ComfyUI filenames and the Unity importer). The exported glTF material names are **`{variant_id}_{slot_id}`** per part so Unity can match them to **`{base}_Lit`** materials from textures.
376
+
377
+ ## Unity import
378
+
379
+ Add the UPM package under [`../../packages/studio-unity`](../../packages/studio-unity) to a Unity 2022.3+ **URP** project, then **Immersive Labs → Import Studio Pack...** and select the folder that contains `manifest.json`.
380
+
381
+ ## Layout
382
+
383
+ | Path | Role |
384
+ |------|------|
385
+ | `src/studio_worker/` | CLI, API, validation, jobs, ComfyUI client, texture pipeline, Stripe billing |
386
+ | `src/studio_worker/billing_config.py` | Env mapping: Stripe Price id → entitlement tier |
387
+ | `src/studio_worker/stripe_billing.py` | Webhook handlers, Checkout / Portal session builders |
388
+ | `src/studio_worker/billing_routes.py` | FastAPI routes under `/api/studio/billing/*` |
389
+ | `studio_worker/comfy_workflows/*.api.json` | ComfyUI `/prompt` graphs (SD1.5 + SDXL albedo v1) |
390
+ | `studio_worker/blender/export_mesh.py` | Headless export stub |
391
+ | `jobs/`, `queue.sqlite`, `tenants.sqlite`, `packs/…` | Writable state (default **`~/.immersive-studio/worker`** when `STUDIO_REPO_ROOT` is unset; otherwise **`apps/studio-worker/output/`** — gitignored in the monorepo) |
392
+
393
+ JSON Schema (runtime): `studio_worker/data/studio-asset-spec-v0.1.schema.json` (copy kept in sync with `packages/studio-types/schema/` in the monorepo).
394
+
395
+ ### GPU / Comfy CI smoke test
396
+
397
+ Opt-in pytest marker `gpu_comfy` runs a minimal SD1.5 txt2img graph against live ComfyUI when:
398
+
399
+ - `STUDIO_RUN_GPU_COMFY_TESTS=1`
400
+ - `STUDIO_COMFY_URL` — base URL of ComfyUI
401
+ - `STUDIO_COMFY_CHECKPOINT` — checkpoint **filename** as shown in ComfyUI (SD1.5-style graph)
402
+
403
+ Optional: `STUDIO_COMFY_STEPS` (default `4`), `STUDIO_COMFY_TIMEOUT_S` (default `420`). Workflow: `.github/workflows/studio-worker-gpu-comfy.yml`.
404
+
405
+ **GitHub Actions — I can’t set these for you** (they live in your repo/org settings). In GitHub: **Settings → Secrets and variables → Actions → New repository secret**, create:
406
+
407
+ | Secret | Example value | Purpose |
408
+ |--------|----------------|---------|
409
+ | `STUDIO_RUN_GPU_COMFY_TESTS` | `1` | Enables the live pytest (otherwise it skips) |
410
+ | `STUDIO_COMFY_URL` | `http://127.0.0.1:8188` | Base URL of ComfyUI **as reachable from the workflow runner** |
411
+ | `STUDIO_COMFY_CHECKPOINT` | `your_model.safetensors` | Exact checkpoint name in ComfyUI’s loader |
412
+
413
+ `workflow_dispatch` can override the URL via the **comfy_url** input when non-empty. **GitHub-hosted runners** cannot see your LAN GPU; point `runs-on` at a **self-hosted** runner (or a URL the runner can reach) or run the test locally with the same env vars.