argus-darkroom 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 (62) hide show
  1. argus_darkroom-0.1.0/.copier-answers.yml +21 -0
  2. argus_darkroom-0.1.0/.cursor/rules/git-workflow.mdc +20 -0
  3. argus_darkroom-0.1.0/.dockerignore +11 -0
  4. argus_darkroom-0.1.0/.env.example +45 -0
  5. argus_darkroom-0.1.0/.github/workflows/ci.yml +32 -0
  6. argus_darkroom-0.1.0/.github/workflows/release.yml +163 -0
  7. argus_darkroom-0.1.0/.gitignore +15 -0
  8. argus_darkroom-0.1.0/CLAUDE.md +60 -0
  9. argus_darkroom-0.1.0/Dockerfile +62 -0
  10. argus_darkroom-0.1.0/LICENSE +21 -0
  11. argus_darkroom-0.1.0/Makefile +33 -0
  12. argus_darkroom-0.1.0/PKG-INFO +288 -0
  13. argus_darkroom-0.1.0/README.md +247 -0
  14. argus_darkroom-0.1.0/compose.yaml +47 -0
  15. argus_darkroom-0.1.0/demo/run.py +223 -0
  16. argus_darkroom-0.1.0/demo/smoke.py +105 -0
  17. argus_darkroom-0.1.0/eval/preflight_probes.jsonl +17 -0
  18. argus_darkroom-0.1.0/examples/workflows/sdxl.json +95 -0
  19. argus_darkroom-0.1.0/examples/workflows/sdxl_lora.json +111 -0
  20. argus_darkroom-0.1.0/pyproject.toml +72 -0
  21. argus_darkroom-0.1.0/schema/darkroom-wire.schema.json +680 -0
  22. argus_darkroom-0.1.0/scripts/preflight_eval.py +95 -0
  23. argus_darkroom-0.1.0/spec.md +250 -0
  24. argus_darkroom-0.1.0/src/argus_darkroom/__init__.py +16 -0
  25. argus_darkroom-0.1.0/src/argus_darkroom/_version.py +24 -0
  26. argus_darkroom-0.1.0/src/argus_darkroom/age.py +249 -0
  27. argus_darkroom-0.1.0/src/argus_darkroom/cli.py +231 -0
  28. argus_darkroom-0.1.0/src/argus_darkroom/comfyui.py +259 -0
  29. argus_darkroom-0.1.0/src/argus_darkroom/config.py +87 -0
  30. argus_darkroom-0.1.0/src/argus_darkroom/engine.py +481 -0
  31. argus_darkroom-0.1.0/src/argus_darkroom/expansion.py +84 -0
  32. argus_darkroom-0.1.0/src/argus_darkroom/guard.py +107 -0
  33. argus_darkroom-0.1.0/src/argus_darkroom/hashing.py +36 -0
  34. argus_darkroom-0.1.0/src/argus_darkroom/images.py +27 -0
  35. argus_darkroom-0.1.0/src/argus_darkroom/modelfiles.py +56 -0
  36. argus_darkroom-0.1.0/src/argus_darkroom/models.py +172 -0
  37. argus_darkroom-0.1.0/src/argus_darkroom/outcome.py +122 -0
  38. argus_darkroom-0.1.0/src/argus_darkroom/preflight.py +92 -0
  39. argus_darkroom-0.1.0/src/argus_darkroom/prompt.py +44 -0
  40. argus_darkroom-0.1.0/src/argus_darkroom/py.typed +1 -0
  41. argus_darkroom-0.1.0/src/argus_darkroom/request.py +76 -0
  42. argus_darkroom-0.1.0/src/argus_darkroom/scan.py +83 -0
  43. argus_darkroom-0.1.0/src/argus_darkroom/server/__init__.py +5 -0
  44. argus_darkroom-0.1.0/src/argus_darkroom/server/app.py +375 -0
  45. argus_darkroom-0.1.0/src/argus_darkroom/store.py +95 -0
  46. argus_darkroom-0.1.0/src/argus_darkroom/workflow.py +193 -0
  47. argus_darkroom-0.1.0/tests/conftest.py +45 -0
  48. argus_darkroom-0.1.0/tests/fake_comfyui.py +176 -0
  49. argus_darkroom-0.1.0/tests/fakes.py +220 -0
  50. argus_darkroom-0.1.0/tests/rig.py +109 -0
  51. argus_darkroom-0.1.0/tests/test_age.py +141 -0
  52. argus_darkroom-0.1.0/tests/test_cli.py +182 -0
  53. argus_darkroom-0.1.0/tests/test_comfyui.py +130 -0
  54. argus_darkroom-0.1.0/tests/test_engine_render.py +213 -0
  55. argus_darkroom-0.1.0/tests/test_expansion.py +212 -0
  56. argus_darkroom-0.1.0/tests/test_generate.py +470 -0
  57. argus_darkroom-0.1.0/tests/test_outcome.py +217 -0
  58. argus_darkroom-0.1.0/tests/test_preflight.py +233 -0
  59. argus_darkroom-0.1.0/tests/test_server.py +520 -0
  60. argus_darkroom-0.1.0/tests/test_smoke.py +8 -0
  61. argus_darkroom-0.1.0/tests/test_store_and_schema.py +69 -0
  62. argus_darkroom-0.1.0/tests/test_workflow.py +174 -0
@@ -0,0 +1,21 @@
1
+ # Managed by Copier — do not edit by hand.
2
+ # Run `copier update` in this repo to pull template changes.
3
+ _commit: b53a3a4
4
+ _src_path: /home/smk/argus-pkg-template
5
+ author: smk762
6
+ cli_name: argus-darkroom
7
+ description: 'Guarded image generation for the Argus suite: prompts and outputs are
8
+ checked before anything is released.'
9
+ dockerfile: Dockerfile
10
+ github_owner: smk762
11
+ has_cli: true
12
+ has_server: true
13
+ image_name: argus-darkroom
14
+ package_name: argus_darkroom
15
+ post_test: ''
16
+ project_name: argus-darkroom
17
+ python_min: '3.11'
18
+ ruff_pin: 0.15.16
19
+ test_extras: dev
20
+ version_build_arg: false
21
+
@@ -0,0 +1,20 @@
1
+ ---
2
+ description: Keep main releasable by doing work on dev in a worktree
3
+ alwaysApply: true
4
+ ---
5
+
6
+ # Git workflow
7
+
8
+ `main` is the release branch. Do not commit on it, and do not push it.
9
+
10
+ `dev` is `main` plus updates that are not released yet. Before starting work, fetch and merge `origin/main` into `dev`.
11
+
12
+ Do each change in a worktree branched from `dev`. Open a pull request into `dev`. A release is a pull request from `dev` into `main`.
13
+
14
+ ```bash
15
+ git fetch origin
16
+ git switch dev && git merge origin/main
17
+ git worktree add ../argus-darkroom-<topic> -b <topic> dev
18
+ ```
19
+
20
+ Do not force-push `main`. Do not commit secrets.
@@ -0,0 +1,11 @@
1
+ # .git stays: hatch-vcs derives the version from it.
2
+ .env
3
+ .env.*
4
+ !.env.example
5
+ .venv/
6
+ __pycache__/
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ darkroom-output/
10
+ dist/
11
+ build/
@@ -0,0 +1,45 @@
1
+ # Copy to .env. Compose reads it; nothing here is a secret, but .env is git-ignored all the same.
2
+
3
+ # --- Required ---------------------------------------------------------------
4
+ # SingGuard (agent-composer's /api/guard/check). Darkroom never runs unguarded: without this it refuses to start.
5
+ SINGGUARD_URL=http://host.docker.internal:9150
6
+ # ComfyUI models root (the folder that holds checkpoints/ and loras/). Mounted read-only; hashed for the record.
7
+ # Use the directory ComfyUI actually lists — if it loads models via extra_model_paths.yaml, that folder, not models/.
8
+ MODELS_ROOT=/path/to/ComfyUI/models
9
+
10
+ # --- Backends, reached by URL (they run outside compose) ---------------------
11
+ COMFYUI_URL=http://host.docker.internal:8188
12
+ # NudeNet. Empty means SFW generation is unavailable (every SFW output would be omitted), so darkroom refuses it.
13
+ NUDENET_URL=
14
+ # Curator's POST /faces?body=true (InsightFace + MiVOLO, body crop and unmatched people). Empty falls back to the in-process [age] extra.
15
+ AGE_URL=http://host.docker.internal:8101
16
+ # Prompt expansion. Both must be set to turn it on.
17
+ OLLAMA_URL=
18
+ OLLAMA_MODEL=
19
+
20
+ # --- Policy ------------------------------------------------------------------
21
+ # 1 lets an NSFW policy enforce (and makes adult-granular the default). Anything else: SFW only.
22
+ ALLOW_NSFW=0
23
+ # cascade | fast | fast-slow | slow
24
+ THINKING=cascade
25
+ # 1 refuses every write: generation, pre-flight and record changes. Records stay readable.
26
+ READ_ONLY=0
27
+ # Origin(s) of the UI allowed to write, comma separated. Safelight's address as the browser sees it.
28
+ CORS_ORIGINS=http://localhost:3000,http://127.0.0.1:3000
29
+
30
+ # --- Development -------------------------------------------------------------
31
+ # 1 adds POST /purge and the purge button in safelight: it deletes every record and image. Leave off elsewhere.
32
+ DEVMODE=0
33
+
34
+ # --- Publish -------------------------------------------------------------------
35
+ # Host:port published for each service. 127.0.0.1 keeps them off the LAN.
36
+ # Use 0.0.0.0:<port> only when the UI is opened as a LAN address (the browser then
37
+ # calls that same host). Darkroom is not for public hosting.
38
+ DARKROOM_PUBLISH=127.0.0.1:8106
39
+ SAFELIGHT_PUBLISH=127.0.0.1:3000
40
+
41
+ # --- Safelight -----------------------------------------------------------------
42
+ # Where the browser reaches darkroom. Baked into the safelight image when it is built.
43
+ DARKROOM_PUBLIC_URL=http://localhost:8106
44
+ # Safelight's repo, as a build context, until it publishes an image.
45
+ SAFELIGHT_CONTEXT=../argus-safelight
@@ -0,0 +1,32 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ concurrency:
10
+ group: ci-${{ github.ref }}
11
+ cancel-in-progress: true
12
+
13
+ jobs:
14
+ ci:
15
+ uses: smk762/argus-ci/.github/workflows/python-ci.yml@v1
16
+ with:
17
+ ruff-version: "0.15.16"
18
+ test-extras: "dev"
19
+
20
+ # The wire schema is a contract other repos generate code from: a model change must come with the regenerated file.
21
+ schema:
22
+ runs-on: ubuntu-latest
23
+ steps:
24
+ - uses: actions/checkout@v4
25
+ with:
26
+ fetch-depth: 0 # hatch-vcs reads the version from git history
27
+ - uses: astral-sh/setup-uv@v6
28
+ with:
29
+ enable-cache: true
30
+ - run: uv venv && uv pip install -e ".[cli]"
31
+ - run: uv run --no-sync argus-darkroom schema --check
32
+
@@ -0,0 +1,163 @@
1
+ name: Release to PyPI
2
+
3
+ # Top-level (non-reusable) on purpose: PyPI trusted publishing cannot run inside
4
+ # a reusable workflow. Keep this file in sync across the suite via `copier update`.
5
+
6
+ on:
7
+ push:
8
+ tags: ["v*"]
9
+ # Sent by argus-cortex's release workflow. The image installs argus-cortex
10
+ # from the pyproject range at build time, so rebuilding the newest release's
11
+ # image is how a new cortex reaches it. Only the image jobs run: PyPI and the
12
+ # GitHub Release for that tag already exist.
13
+ repository_dispatch:
14
+ types: [cortex-released]
15
+
16
+ permissions:
17
+ contents: read
18
+ id-token: write
19
+
20
+ jobs:
21
+ build:
22
+ # Not on a cortex dispatch, so the PyPI and Release jobs that chain off it
23
+ # skip too.
24
+ if: github.event_name == 'push'
25
+ runs-on: ubuntu-latest
26
+ steps:
27
+ - uses: actions/checkout@v4
28
+ with:
29
+ fetch-depth: 0
30
+ - uses: astral-sh/setup-uv@v6
31
+ with:
32
+ enable-cache: true
33
+ - run: uv build
34
+ - uses: actions/upload-artifact@v4
35
+ with:
36
+ name: dist
37
+ path: dist/
38
+
39
+ test-install:
40
+ runs-on: ubuntu-latest
41
+ needs: build
42
+ strategy:
43
+ matrix:
44
+ python-version: ["3.11", "3.12"]
45
+ steps:
46
+ - uses: actions/download-artifact@v4
47
+ with:
48
+ name: dist
49
+ path: dist/
50
+ - uses: astral-sh/setup-uv@v6
51
+ with:
52
+ enable-cache: true
53
+ - name: Install wheel and smoke-test import
54
+ run: |
55
+ uv venv --python ${{ matrix.python-version }}
56
+ uv pip install dist/*.whl
57
+ uv run --no-sync python -c "import argus_darkroom; print(f'argus-darkroom {argus_darkroom.__version__} OK')"
58
+
59
+ publish-testpypi:
60
+ runs-on: ubuntu-latest
61
+ needs: test-install
62
+ environment: testpypi
63
+ steps:
64
+ - uses: actions/download-artifact@v4
65
+ with:
66
+ name: dist
67
+ path: dist/
68
+ - uses: pypa/gh-action-pypi-publish@release/v1
69
+ with:
70
+ repository-url: https://test.pypi.org/legacy/
71
+
72
+ publish-pypi:
73
+ runs-on: ubuntu-latest
74
+ # Ordered *after* the TestPyPI rehearsal so its outcome is visible first, but
75
+ # deliberately not gated on it. A TestPyPI-only fault — a missing trusted
76
+ # publisher, an index outage — must not silently withhold the real release:
77
+ # that failure mode shipped argus-proof images for two tags with no wheel on
78
+ # PyPI, and nothing surfaced it because publish-image is independent.
79
+ # Only the build + install gate is required; a TestPyPI failure still marks
80
+ # the whole run failed, so the problem stays loud.
81
+ needs: [test-install, publish-testpypi]
82
+ if: "!cancelled() && needs.test-install.result == 'success'"
83
+ environment: pypi
84
+ steps:
85
+ - uses: actions/download-artifact@v4
86
+ with:
87
+ name: dist
88
+ path: dist/
89
+ - uses: pypa/gh-action-pypi-publish@release/v1
90
+
91
+ release-tag:
92
+ # The tag the image jobs build: the pushed one, or on a cortex dispatch the
93
+ # newest final release. A pre-release never moved `latest`, so a dispatch
94
+ # does not rebuild one.
95
+ runs-on: ubuntu-latest
96
+ outputs:
97
+ tag: ${{ steps.tag.outputs.tag }}
98
+ sha: ${{ steps.tag.outputs.sha }}
99
+ steps:
100
+ - uses: actions/checkout@v4
101
+ if: github.event_name == 'repository_dispatch'
102
+ - id: tag
103
+ env:
104
+ CORTEX_VERSION: ${{ github.event.client_payload.version }}
105
+ run: |
106
+ if [ "$GITHUB_EVENT_NAME" != repository_dispatch ]; then
107
+ echo "tag=$GITHUB_REF_NAME" >> "$GITHUB_OUTPUT"
108
+ echo "sha=$GITHUB_SHA" >> "$GITHUB_OUTPUT"
109
+ exit 0
110
+ fi
111
+ refs=$(git ls-remote --tags origin 'refs/tags/v*')
112
+ tag=$(echo "$refs" | sed -n 's|.*refs/tags/\(v[0-9]*\.[0-9]*\.[0-9]*\)\(\^{}\)\{0,1\}$|\1|p' | sort -uV | tail -n1)
113
+ if [ -z "$tag" ]; then
114
+ echo "::notice::No release tag yet, so no image to rebuild for cortex $CORTEX_VERSION."
115
+ exit 0
116
+ fi
117
+ # An annotated tag lists its commit on the peeled ^{} line.
118
+ sha=$(echo "$refs" | awk -v t="refs/tags/$tag" '$2 == t {l = $1} $2 == t "^{}" {p = $1} END {print p ? p : l}')
119
+ echo "Rebuilding $tag ($sha) for cortex $CORTEX_VERSION."
120
+ echo "tag=$tag" >> "$GITHUB_OUTPUT"
121
+ echo "sha=$sha" >> "$GITHUB_OUTPUT"
122
+
123
+ publish-image:
124
+ runs-on: ubuntu-latest
125
+ needs: [release-tag, test-install]
126
+ # On a cortex dispatch test-install is skipped (nothing was built for PyPI),
127
+ # so the job runs on the tag alone.
128
+ if: "!cancelled() && needs.release-tag.outputs.tag != '' && (needs.test-install.result == 'success' || github.event_name == 'repository_dispatch')"
129
+ permissions:
130
+ contents: read
131
+ packages: write
132
+ steps:
133
+ - uses: actions/checkout@v4
134
+ with:
135
+ ref: ${{ needs.release-tag.outputs.tag }}
136
+ fetch-depth: 0 # hatch-vcs reads the tag from git history
137
+ - uses: docker/setup-buildx-action@v3
138
+ - uses: docker/login-action@v3
139
+ with:
140
+ registry: ghcr.io
141
+ username: ${{ github.actor }}
142
+ password: ${{ secrets.GITHUB_TOKEN }}
143
+ - id: meta
144
+ uses: docker/metadata-action@v5
145
+ with:
146
+ images: ghcr.io/${{ github.repository_owner }}/argus-darkroom
147
+ labels: org.opencontainers.image.revision=${{ needs.release-tag.outputs.sha }}
148
+ tags: |
149
+ type=semver,pattern={{version}},value=${{ needs.release-tag.outputs.tag }}
150
+ type=semver,pattern={{major}}.{{minor}},value=${{ needs.release-tag.outputs.tag }}
151
+ type=raw,value=latest
152
+ - uses: docker/build-push-action@v6
153
+ with:
154
+ context: .
155
+ file: ./Dockerfile
156
+ push: true
157
+ tags: ${{ steps.meta.outputs.tags }}
158
+ labels: ${{ steps.meta.outputs.labels }}
159
+ # A dispatch rebuilds an unchanged context, so a cached install layer
160
+ # would keep the old argus-cortex.
161
+ no-cache: ${{ github.event_name == 'repository_dispatch' }}
162
+ cache-from: type=gha
163
+ cache-to: type=gha,mode=max
@@ -0,0 +1,15 @@
1
+ .venv/
2
+ # compose settings; .env.example is the committed template
3
+ .env
4
+ compose.override.yaml
5
+ darkroom-output/
6
+ __pycache__/
7
+ *.pyc
8
+ dist/
9
+ build/
10
+ *.egg-info/
11
+ .pytest_cache/
12
+ .ruff_cache/
13
+ # hatch-vcs writes this at build time (version derived from git tags)
14
+ src/argus_darkroom/_version.py
15
+ demo-data/
@@ -0,0 +1,60 @@
1
+ # argus-darkroom
2
+
3
+ Guarded image generation. Read `spec.md` for the design and `README.md` for running it. This file is the rules that must
4
+ not be broken and where things are.
5
+
6
+ ## Rules that must hold
7
+
8
+ - **Nothing is written before the verdict.** No temp file, no cortex blob, no `SaveImage`/`PreviewImage` node. A
9
+ deleted or omitted image is never written, and its buffer is dropped. `tests/test_generate.py` lists the output root,
10
+ the system temp directory and the fake ComfyUI history before and after to prove it.
11
+ - **Never run unguarded.** An empty `SINGGUARD_URL` refuses to start (`Settings.__post_init__`). A failed check is an
12
+ `unchecked` cell, never a pass. An unchecked enforcing cell deletes; an unchecked advisory cell is only shown.
13
+ - **Every check judges the final prompt**, expansion included. The negative prompt is always part of pre-flight. Do not
14
+ add a path that checks only the user's text.
15
+ - **Policies are chosen by name only.** Never accept policy text in a request. Policy texts, the illegal table and
16
+ `mode()` live in `argus_cortex.moderation.policies`; do not copy them here.
17
+ - **Fail closed.** If you are unsure whether an error should refuse, delete or pass, it refuses or deletes.
18
+ - **Never import NudeNet** (AGPL). Reach it over HTTP through cortex's `NudeNetClient`.
19
+ - **The server binds to localhost by default.** Writes need `WriteGuard` and `cross_site_refuse`. `POST /purge` exists
20
+ only with `ARGUS_DARKROOM_DEVMODE` and answers 404 otherwise.
21
+ - **The age is a tool's estimate.** Name it `age_estimate_years`, source `mivolo`. Never call it an age, and never send
22
+ it to SingGuard as context.
23
+ - **No real sexual, violent or illegal content** in the repo or tests. Drive the illegal path with a fake verdict.
24
+
25
+ ## Layout
26
+
27
+ | | |
28
+ |---|---|
29
+ | `engine.py` | `Engine`: `begin` validates, `_run` expands, pre-flights, renders, scans, decides, releases or destroys |
30
+ | `outcome.py` | the outcome table (`decide`). Precedence: deleted, omitted (SFW), review, passed. Unit-test any change by cell combination |
31
+ | `preflight.py`, `scan.py`, `guard.py` | the text check, the image scans, and the SingGuard wrapper that turns failures into `unchecked` |
32
+ | `prompt.py` | `assemble_prompt`, and `frame` (folds line breaks so text cannot pose as the frame's labels) |
33
+ | `expansion.py` | `OllamaExpander`. Ollama is never a guard |
34
+ | `comfyui.py`, `workflow.py`, `modelfiles.py`, `hashing.py` | render over the websocket, the `$placeholder` template, model sha256s |
35
+ | `age.py` | curator over `AGE_URL` (`POST /faces?body=true`), or in-process `detect_faces` behind the `[age]` extra |
36
+ | `store.py` | `<id>.json` and `<id>.png` in the output root; atomic image write |
37
+ | `models.py` | the versioned wire models. `schema/darkroom-wire.schema.json` is generated from them |
38
+ | `demo/run.py`, `demo/smoke.py` | the real server and engine over the test fakes; a marker in the prompt picks the verdicts. Demo only: it turns `DEVMODE` on and uses fake models |
39
+ | `server/app.py`, `cli.py`, `config.py`, `request.py` | HTTP, Typer, `ARGUS_DARKROOM_*`, the request model (`extra=forbid`) |
40
+
41
+ ## Working
42
+
43
+ ```bash
44
+ make install && make test && make lint
45
+ uv run --no-sync argus-darkroom schema --check # after any change to models.py: run `schema`, commit the file
46
+ ```
47
+
48
+ - `main` is releases, `dev` is work. Work in a worktree from `dev`, and open PRs into `dev`. CI runs on `main` only, so
49
+ run lint, tests and `schema --check` locally before every PR.
50
+ - Use `docker compose`, not `docker-compose`.
51
+ - The record is a wire contract (`record_version`). Adding a field is fine; renaming or removing one is a new version.
52
+ The blur decision lives only in `guard.nudenet.blur`, which is recorded for every NSFW-policy image (`exposed_score`
53
+ is `null` when NudeNet was unreachable). Each LoRA records the `trigger_words` the request supplied.
54
+ - argus-safelight codes against the HTTP shapes in the README's API table (`/health`, `/policies`, `/models`, `/records`
55
+ paging, SSE event bodies). Change one and `make demo` plus `make smoke` show whether the UI still works; update
56
+ safelight's `types.ts` in step.
57
+ - Tests use fake servers on real sockets (`tests/fakes.py`, `tests/fake_comfyui.py`, `tests/rig.py`). Prefer extending
58
+ them to mocking.
59
+ - `scripts/preflight_eval.py` is run by hand against a real SingGuard that supports text-only checks (agent-composer
60
+ PR #4). It is not a test.
@@ -0,0 +1,62 @@
1
+ # syntax=docker/dockerfile:1
2
+ FROM python:3.11-slim AS base
3
+
4
+ # Pin past the eMudhra/emSign incident (discuss.python.org/t/109576). A floating
5
+ # :latest tag can stay cached on a pre-0.13.4 digest.
6
+ COPY --from=ghcr.io/astral-sh/uv:0.13.5 /uv /uvx /bin/
7
+
8
+ WORKDIR /app
9
+
10
+ # Distrust emSign ECC Root CA - G3 and the Comodo AAA cross-sign root, then
11
+ # replace the base image's pip. pip < 26.2.2 trusts its vendored bundle in
12
+ # addition to the system store.
13
+ RUN apt-get update \
14
+ && apt-get install -y --no-install-recommends ca-certificates \
15
+ && sed -i \
16
+ -e 's|^mozilla/emSign_ECC_Root_CA_-_G3|!mozilla/emSign_ECC_Root_CA_-_G3|' \
17
+ -e 's|^mozilla/Comodo_AAA|!mozilla/Comodo_AAA|' \
18
+ /etc/ca-certificates.conf \
19
+ && update-ca-certificates --fresh >/dev/null \
20
+ && rm -rf /var/lib/apt/lists/* \
21
+ && python -m pip install --no-cache-dir \
22
+ "pip @ https://files.pythonhosted.org/packages/3f/06/3e4420302363fbb71be3c7f35ce042f54bfca330e3e66b3a2e3c4cca579f/pip-26.2.2-py3-none-any.whl#sha256=fd7f4dfa8190e014013b9f08f3e973f4e4c26ffd179a204895533a7254813a70"
23
+
24
+
25
+ # hatch-vcs derives the version from git history, so this stage needs git and a build context that includes .git
26
+ # (keep it out of .dockerignore). Only the wheel leaves this stage: git and the history stay behind.
27
+ FROM base AS build
28
+ RUN apt-get update \
29
+ && apt-get install -y --no-install-recommends git \
30
+ && rm -rf /var/lib/apt/lists/*
31
+ COPY . .
32
+ RUN git config --global --add safe.directory /app && uv build --wheel --out-dir /wheel
33
+
34
+
35
+ FROM base
36
+ COPY --from=build /wheel /wheel
37
+ RUN uv pip install --system --no-cache "certifi>=2026.10.19" "$(ls /wheel/*.whl)[server,cli]" \
38
+ && rm -rf /wheel
39
+
40
+ # The age check (the `age` extra) pulls in onnxruntime and InsightFace and needs the MiVOLO model file, so it is left
41
+ # out of the image; /health reports it as unavailable and records get `age: null`.
42
+
43
+ # The example workflows are the default template; mount your own and set ARGUS_DARKROOM_WORKFLOW to use it.
44
+ COPY examples ./examples
45
+ ENV ARGUS_DARKROOM_OUTPUT_ROOT=/data \
46
+ ARGUS_DARKROOM_MODELS_ROOT=/models \
47
+ ARGUS_DARKROOM_WORKFLOW=/app/examples/workflows/sdxl.json
48
+
49
+ # Records and images live in /data, so give the unprivileged user that and nothing else.
50
+ RUN useradd --system --uid 10001 --no-create-home darkroom \
51
+ && mkdir /data \
52
+ && chown darkroom /data
53
+ USER darkroom
54
+ VOLUME /data
55
+
56
+ # Bound to all interfaces so the port can be published; publish it on 127.0.0.1 (compose.yaml does). Darkroom is not
57
+ # for public hosting. It refuses to start without ARGUS_DARKROOM_SINGGUARD_URL.
58
+ EXPOSE 8106
59
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s \
60
+ CMD ["python", "-c", "import urllib.request as u; u.urlopen('http://127.0.0.1:8106/health', timeout=4)"]
61
+ ENTRYPOINT ["argus-darkroom"]
62
+ CMD ["serve", "--host", "0.0.0.0", "--port", "8106"]
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 smk762
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,33 @@
1
+ UV ?= uv
2
+
3
+ .PHONY: help install lint format test build demo smoke clean
4
+
5
+ help: ## Show this help
6
+ @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-12s\033[0m %s\n", $$1, $$2}'
7
+
8
+ install: ## Create venv and install with test extras
9
+ $(UV) venv
10
+ $(UV) pip install -e ".[dev]"
11
+
12
+ lint: ## Ruff check + format check
13
+ $(UV)x ruff@0.15.16 check src/ tests/
14
+ $(UV)x ruff@0.15.16 format --check src/ tests/
15
+
16
+ format: ## Ruff autoformat + autofix
17
+ $(UV)x ruff@0.15.16 format src/ tests/
18
+ $(UV)x ruff@0.15.16 check --fix src/ tests/
19
+
20
+ test: ## Run the test suite
21
+ $(UV) run --no-sync pytest --tb=short -q
22
+
23
+ build: ## Build sdist + wheel
24
+ $(UV) build
25
+
26
+ demo: ## Run the real server against fake backends (no GPU), on http://127.0.0.1:8106
27
+ $(UV) run --no-sync python demo/run.py
28
+
29
+ smoke: ## With `make demo` running: send every marker prompt and check the outcome
30
+ $(UV) run --no-sync python demo/smoke.py
31
+
32
+ clean: ## Remove build + cache artifacts
33
+ rm -rf dist build .venv *.egg-info .pytest_cache .ruff_cache src/argus_darkroom/_version.py