argus-mask 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.
- argus_mask-0.1.0/.copier-answers.yml +21 -0
- argus_mask-0.1.0/.cursor/rules/agent-coding-packs.mdc +29 -0
- argus_mask-0.1.0/.dockerignore +15 -0
- argus_mask-0.1.0/.env.example +38 -0
- argus_mask-0.1.0/.github/workflows/ci.yml +20 -0
- argus_mask-0.1.0/.github/workflows/release.yml +163 -0
- argus_mask-0.1.0/.github/workflows/suite.yml +11 -0
- argus_mask-0.1.0/.gitignore +19 -0
- argus_mask-0.1.0/AGENTS.md +24 -0
- argus_mask-0.1.0/CHANGELOG.md +221 -0
- argus_mask-0.1.0/CLAUDE.md +89 -0
- argus_mask-0.1.0/Dockerfile +29 -0
- argus_mask-0.1.0/LICENSE +21 -0
- argus_mask-0.1.0/Makefile +33 -0
- argus_mask-0.1.0/PKG-INFO +421 -0
- argus_mask-0.1.0/README.md +361 -0
- argus_mask-0.1.0/a2f/.dockerignore +3 -0
- argus_mask-0.1.0/a2f/Dockerfile +113 -0
- argus_mask-0.1.0/a2f/README.md +139 -0
- argus_mask-0.1.0/a2f/check.py +165 -0
- argus_mask-0.1.0/a2f/server.py +557 -0
- argus_mask-0.1.0/a2f/worker/CMakeLists.txt +8 -0
- argus_mask-0.1.0/a2f/worker/main.cpp +372 -0
- argus_mask-0.1.0/compose.yaml +59 -0
- argus_mask-0.1.0/docs/synthetic-faces.md +151 -0
- argus_mask-0.1.0/docs/voices.md +108 -0
- argus_mask-0.1.0/pyproject.toml +77 -0
- argus_mask-0.1.0/schema/mask-wire.schema.json +628 -0
- argus_mask-0.1.0/scripts/README.md +140 -0
- argus_mask-0.1.0/scripts/fetch_stock_head.py +143 -0
- argus_mask-0.1.0/scripts/make_asset_catalogue.py +272 -0
- argus_mask-0.1.0/scripts/make_modifiers.py +196 -0
- argus_mask-0.1.0/scripts/stock_heads/stock-01.json +33 -0
- argus_mask-0.1.0/scripts/stock_heads/stock-01.lock.json +78 -0
- argus_mask-0.1.0/scripts/stock_heads/stock-02.json +33 -0
- argus_mask-0.1.0/scripts/stock_heads/stock-02.lock.json +78 -0
- argus_mask-0.1.0/spec.md +271 -0
- argus_mask-0.1.0/src/argus_mask/__init__.py +16 -0
- argus_mask-0.1.0/src/argus_mask/_version.py +24 -0
- argus_mask-0.1.0/src/argus_mask/arkit.py +64 -0
- argus_mask-0.1.0/src/argus_mask/audio.py +125 -0
- argus_mask-0.1.0/src/argus_mask/cli.py +506 -0
- argus_mask-0.1.0/src/argus_mask/client.py +868 -0
- argus_mask-0.1.0/src/argus_mask/config.py +139 -0
- argus_mask-0.1.0/src/argus_mask/drivers/__init__.py +61 -0
- argus_mask-0.1.0/src/argus_mask/drivers/a2f.py +196 -0
- argus_mask-0.1.0/src/argus_mask/drivers/energy.py +97 -0
- argus_mask-0.1.0/src/argus_mask/editor/__init__.py +14 -0
- argus_mask-0.1.0/src/argus_mask/editor/app.py +1000 -0
- argus_mask-0.1.0/src/argus_mask/editor/asset_catalogue.json +7724 -0
- argus_mask-0.1.0/src/argus_mask/editor/broadcast.py +281 -0
- argus_mask-0.1.0/src/argus_mask/editor/build_head.py +927 -0
- argus_mask-0.1.0/src/argus_mask/editor/builder.py +516 -0
- argus_mask-0.1.0/src/argus_mask/editor/library.py +221 -0
- argus_mask-0.1.0/src/argus_mask/editor/modifiers.json +1796 -0
- argus_mask-0.1.0/src/argus_mask/editor/packs.py +454 -0
- argus_mask-0.1.0/src/argus_mask/editor/page/broadcast.html +161 -0
- argus_mask-0.1.0/src/argus_mask/editor/page/broadcast.js +314 -0
- argus_mask-0.1.0/src/argus_mask/editor/page/editor.js +754 -0
- argus_mask-0.1.0/src/argus_mask/editor/page/faces.html +127 -0
- argus_mask-0.1.0/src/argus_mask/editor/page/faces.js +280 -0
- argus_mask-0.1.0/src/argus_mask/editor/page/index.html +187 -0
- argus_mask-0.1.0/src/argus_mask/editor/page/packs.html +89 -0
- argus_mask-0.1.0/src/argus_mask/editor/page/packs.js +199 -0
- argus_mask-0.1.0/src/argus_mask/editor/page/qr/LICENSE-qrcode-generator +27 -0
- argus_mask-0.1.0/src/argus_mask/editor/page/qr/qrcode.js +2297 -0
- argus_mask-0.1.0/src/argus_mask/editor/page/screen.js +198 -0
- argus_mask-0.1.0/src/argus_mask/editor/page/stage.js +286 -0
- argus_mask-0.1.0/src/argus_mask/editor/page/talk.json +1 -0
- argus_mask-0.1.0/src/argus_mask/editor/page/voices.html +150 -0
- argus_mask-0.1.0/src/argus_mask/editor/page/voices.js +316 -0
- argus_mask-0.1.0/src/argus_mask/editor/preset.py +151 -0
- argus_mask-0.1.0/src/argus_mask/editor/voices.py +248 -0
- argus_mask-0.1.0/src/argus_mask/expressions.py +235 -0
- argus_mask-0.1.0/src/argus_mask/faces.py +623 -0
- argus_mask-0.1.0/src/argus_mask/idle.py +246 -0
- argus_mask-0.1.0/src/argus_mask/models.py +273 -0
- argus_mask-0.1.0/src/argus_mask/performance.py +113 -0
- argus_mask-0.1.0/src/argus_mask/prosody.py +245 -0
- argus_mask-0.1.0/src/argus_mask/py.typed +0 -0
- argus_mask-0.1.0/src/argus_mask/record.py +291 -0
- argus_mask-0.1.0/src/argus_mask/server/__init__.py +1 -0
- argus_mask-0.1.0/src/argus_mask/server/app.py +688 -0
- argus_mask-0.1.0/src/argus_mask/speech.py +371 -0
- argus_mask-0.1.0/src/argus_mask/take.py +56 -0
- argus_mask-0.1.0/src/argus_mask/viewer/index.html +40 -0
- argus_mask-0.1.0/src/argus_mask/viewer/look.js +416 -0
- argus_mask-0.1.0/src/argus_mask/viewer/motion.js +354 -0
- argus_mask-0.1.0/src/argus_mask/viewer/splat.js +218 -0
- argus_mask-0.1.0/src/argus_mask/viewer/vendor/LICENSE-gsplat-flame-avatar-renderer +23 -0
- argus_mask-0.1.0/src/argus_mask/viewer/vendor/LICENSE-jszip.md +651 -0
- argus_mask-0.1.0/src/argus_mask/viewer/vendor/LICENSE-three +21 -0
- argus_mask-0.1.0/src/argus_mask/viewer/vendor/controls/OrbitControls.js +1985 -0
- argus_mask-0.1.0/src/argus_mask/viewer/vendor/environments/RoomEnvironment.js +185 -0
- argus_mask-0.1.0/src/argus_mask/viewer/vendor/gsplat-flame-avatar-renderer.esm.min.js +2 -0
- argus_mask-0.1.0/src/argus_mask/viewer/vendor/jszip.js +6 -0
- argus_mask-0.1.0/src/argus_mask/viewer/vendor/jszip.min.js +13 -0
- argus_mask-0.1.0/src/argus_mask/viewer/vendor/loaders/GLTFLoader.js +5006 -0
- argus_mask-0.1.0/src/argus_mask/viewer/vendor/math/SimplexNoise.js +470 -0
- argus_mask-0.1.0/src/argus_mask/viewer/vendor/postprocessing/BokehPass.js +218 -0
- argus_mask-0.1.0/src/argus_mask/viewer/vendor/postprocessing/EffectComposer.js +365 -0
- argus_mask-0.1.0/src/argus_mask/viewer/vendor/postprocessing/FilmPass.js +113 -0
- argus_mask-0.1.0/src/argus_mask/viewer/vendor/postprocessing/GTAOPass.js +735 -0
- argus_mask-0.1.0/src/argus_mask/viewer/vendor/postprocessing/MaskPass.js +195 -0
- argus_mask-0.1.0/src/argus_mask/viewer/vendor/postprocessing/OutputPass.js +148 -0
- argus_mask-0.1.0/src/argus_mask/viewer/vendor/postprocessing/Pass.js +191 -0
- argus_mask-0.1.0/src/argus_mask/viewer/vendor/postprocessing/RenderPass.js +193 -0
- argus_mask-0.1.0/src/argus_mask/viewer/vendor/postprocessing/SMAAPass.js +230 -0
- argus_mask-0.1.0/src/argus_mask/viewer/vendor/postprocessing/ShaderPass.js +135 -0
- argus_mask-0.1.0/src/argus_mask/viewer/vendor/postprocessing/UnrealBloomPass.js +541 -0
- argus_mask-0.1.0/src/argus_mask/viewer/vendor/shaders/BokehShader.js +151 -0
- argus_mask-0.1.0/src/argus_mask/viewer/vendor/shaders/CopyShader.js +52 -0
- argus_mask-0.1.0/src/argus_mask/viewer/vendor/shaders/FilmShader.js +73 -0
- argus_mask-0.1.0/src/argus_mask/viewer/vendor/shaders/GTAOShader.js +437 -0
- argus_mask-0.1.0/src/argus_mask/viewer/vendor/shaders/LuminosityHighPassShader.js +68 -0
- argus_mask-0.1.0/src/argus_mask/viewer/vendor/shaders/OutputShader.js +103 -0
- argus_mask-0.1.0/src/argus_mask/viewer/vendor/shaders/PoissonDenoiseShader.js +230 -0
- argus_mask-0.1.0/src/argus_mask/viewer/vendor/shaders/SMAAShader.js +489 -0
- argus_mask-0.1.0/src/argus_mask/viewer/vendor/three.core.js +61162 -0
- argus_mask-0.1.0/src/argus_mask/viewer/vendor/three.module.js +19537 -0
- argus_mask-0.1.0/src/argus_mask/viewer/vendor/utils/BufferGeometryUtils.js +1523 -0
- argus_mask-0.1.0/src/argus_mask/viewer/vendor/utils/SkeletonUtils.js +496 -0
- argus_mask-0.1.0/src/argus_mask/viewer/viewer.js +531 -0
- argus_mask-0.1.0/src/argus_mask/voice.py +548 -0
- argus_mask-0.1.0/tests/__init__.py +0 -0
- argus_mask-0.1.0/tests/helpers.py +480 -0
- argus_mask-0.1.0/tests/test_aliases.py +190 -0
- argus_mask-0.1.0/tests/test_client_cli.py +400 -0
- argus_mask-0.1.0/tests/test_editor.py +1232 -0
- argus_mask-0.1.0/tests/test_engine.py +138 -0
- argus_mask-0.1.0/tests/test_expressions.py +143 -0
- argus_mask-0.1.0/tests/test_faces.py +139 -0
- argus_mask-0.1.0/tests/test_fetch_stock_head.py +112 -0
- argus_mask-0.1.0/tests/test_packs.py +438 -0
- argus_mask-0.1.0/tests/test_push.py +195 -0
- argus_mask-0.1.0/tests/test_record.py +141 -0
- argus_mask-0.1.0/tests/test_server.py +368 -0
- argus_mask-0.1.0/tests/test_smoke.py +8 -0
- argus_mask-0.1.0/tests/test_speech.py +165 -0
- argus_mask-0.1.0/tests/test_speech_life.py +151 -0
- argus_mask-0.1.0/tests/test_splat.py +293 -0
- argus_mask-0.1.0/tests/test_voice.py +138 -0
- argus_mask-0.1.0/tests/test_voices_page.py +208 -0
- argus_mask-0.1.0/uv.lock +1091 -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: 939debb
|
|
4
|
+
_src_path: gh:smk762/argus-pkg-template
|
|
5
|
+
author: smk762
|
|
6
|
+
cli_name: argus-mask
|
|
7
|
+
description: 'A talking face for the Argus suite: audio in, facial animation frames
|
|
8
|
+
out, rendered in a browser on any screen on the LAN'
|
|
9
|
+
dockerfile: Dockerfile
|
|
10
|
+
github_owner: smk762
|
|
11
|
+
has_cli: true
|
|
12
|
+
has_server: true
|
|
13
|
+
image_name: argus-mask
|
|
14
|
+
package_name: argus_mask
|
|
15
|
+
post_test: uv run --no-sync argus-mask schema --check
|
|
16
|
+
project_name: argus-mask
|
|
17
|
+
python_min: '3.11'
|
|
18
|
+
ruff_pin: 0.15.16
|
|
19
|
+
test_extras: dev,server,cli
|
|
20
|
+
version_build_arg: false
|
|
21
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Fetch ai-code-auditor coding packs for argus-mask before changing code
|
|
3
|
+
alwaysApply: true
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<!-- ai-audit:agent-packs:start -->
|
|
7
|
+
# Agent coding packs
|
|
8
|
+
|
|
9
|
+
Do not wait for a pasted brief — fetch the audit packs for `argus-mask` yourself.
|
|
10
|
+
|
|
11
|
+
The audit API base URL is `$AI_AUDIT_PUBLIC_BASE_URL`. The real host/IP is in the local `.env`
|
|
12
|
+
(see `.env.example`, placeholder `http://<AUDIT_HOST>:8765`); read it there, don't guess.
|
|
13
|
+
|
|
14
|
+
1. **Bundle (one call):** `GET $AI_AUDIT_PUBLIC_BASE_URL/audit/packs/bundle?repo=argus-mask`
|
|
15
|
+
(world model + ranked specialist packs + token-capped `briefs_markdown`;
|
|
16
|
+
optional `&token_budget=2500&q=<keyword>&limit_specialists=5`).
|
|
17
|
+
CLI equivalent: `ai-audit packs --bundle --repo argus-mask`.
|
|
18
|
+
2. **Claim** the specialist pack you pick:
|
|
19
|
+
`POST $AI_AUDIT_PUBLIC_BASE_URL/audit/packs/<id>/status` `{"status":"claimed"}`
|
|
20
|
+
(send `X-API-Token: $AI_AUDIT_API_TOKEN`).
|
|
21
|
+
3. **Fix:** `ai-audit repair --pack-id <id>` (agent-composer repair loop; fetches the pack for you;
|
|
22
|
+
HTTP: `POST http://127.0.0.1:9150/api/repair/run` `{"pack_id":"<id>","mode":"review"}`),
|
|
23
|
+
or read `GET $AI_AUDIT_PUBLIC_BASE_URL/audit/packs/<id>` / `.../brief` and implement it yourself.
|
|
24
|
+
4. **Close:** `POST $AI_AUDIT_PUBLIC_BASE_URL/audit/validate_patch` `{"repo":"argus-mask","patch":"<diff>","pack_id":"<id>"}`
|
|
25
|
+
— a passing run marks the pack `resolved`.
|
|
26
|
+
|
|
27
|
+
World-model constraints (ownership, boundaries, scan branch `dev`) win over guesswork.
|
|
28
|
+
Full flow: ai-code-auditor `AGENTS.md`.
|
|
29
|
+
<!-- ai-audit:agent-packs:end -->
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# NOTE: .git is deliberately *not* ignored -- hatch-vcs reads the version from
|
|
2
|
+
# git history at build time. Set `version_build_arg` if you want it excluded.
|
|
3
|
+
.github
|
|
4
|
+
.venv
|
|
5
|
+
venv
|
|
6
|
+
dist
|
|
7
|
+
build
|
|
8
|
+
*.egg-info
|
|
9
|
+
**/__pycache__
|
|
10
|
+
**/*.py[cod]
|
|
11
|
+
.pytest_cache
|
|
12
|
+
.ruff_cache
|
|
13
|
+
.mypy_cache
|
|
14
|
+
.coverage
|
|
15
|
+
htmlcov
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Copy to .env and edit. Read by compose.yaml.
|
|
2
|
+
|
|
3
|
+
# Where the faces live: one directory per face, each a .glb and a manifest.json.
|
|
4
|
+
ASSETS_DIR=./assets
|
|
5
|
+
|
|
6
|
+
# energy: mouth from loudness, CPU only, crude. a2f: the Audio2Face server at A2F_URL.
|
|
7
|
+
DRIVER=energy
|
|
8
|
+
A2F_URL=
|
|
9
|
+
|
|
10
|
+
# With `docker compose --profile a2f`: DRIVER=a2f and A2F_URL=http://a2f:8110.
|
|
11
|
+
# Which character's Audio2Face model (james, mark or claire), and whether its solver runs on the GPU.
|
|
12
|
+
A2F_MODEL=james
|
|
13
|
+
A2F_GPU_SOLVER=0
|
|
14
|
+
|
|
15
|
+
# Animation frames per second. The viewer interpolates up to the screen's refresh rate.
|
|
16
|
+
FPS=30
|
|
17
|
+
|
|
18
|
+
# Host address the server is published on. 127.0.0.1 keeps it on this machine.
|
|
19
|
+
MASK_PUBLISH=127.0.0.1:8109
|
|
20
|
+
|
|
21
|
+
# Browser origins allowed to open the sockets, comma-separated. The server's own host is always
|
|
22
|
+
# allowed. Behind a TLS proxy, name the proxy's origin, e.g. https://mask.example.org
|
|
23
|
+
CORS_ORIGINS=
|
|
24
|
+
|
|
25
|
+
# 1 refuses every audio source; viewers still connect.
|
|
26
|
+
READ_ONLY=0
|
|
27
|
+
|
|
28
|
+
# The TTS stream endpoint for POST /speak (empty: /speak answers 422; /push needs none).
|
|
29
|
+
# From inside the container, a service on this host is http://host.docker.internal:<port>/...
|
|
30
|
+
# e.g. Miso TTS: http://host.docker.internal:8034/synthesise/stream, or wyoming://host:port
|
|
31
|
+
TTS_URL=
|
|
32
|
+
# The voice /speak asks for when the request names none: a stored clone's name, or a speaker number.
|
|
33
|
+
TTS_VOICE=
|
|
34
|
+
|
|
35
|
+
# ── Agent tooling (not read by this service) ──
|
|
36
|
+
# ai-code-auditor audit API base that AGENTS.md tells coding agents to use.
|
|
37
|
+
# Put the real host/IP in your local .env, never here.
|
|
38
|
+
# AI_AUDIT_PUBLIC_BASE_URL=http://<AUDIT_HOST>:8765
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [dev, main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [dev, 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,server,cli"
|
|
19
|
+
post-test: "uv run --no-sync argus-mask schema --check"
|
|
20
|
+
|
|
@@ -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_mask; print(f'argus-mask {argus_mask.__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-mask
|
|
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,19 @@
|
|
|
1
|
+
.venv/
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.pyc
|
|
4
|
+
dist/
|
|
5
|
+
build/
|
|
6
|
+
*.egg-info/
|
|
7
|
+
.pytest_cache/
|
|
8
|
+
.ruff_cache/
|
|
9
|
+
# hatch-vcs writes this at build time (version derived from git tags)
|
|
10
|
+
src/argus_mask/_version.py
|
|
11
|
+
.env
|
|
12
|
+
# Faces are large binaries with their own provenance; the stock head is added deliberately, by name.
|
|
13
|
+
/assets/
|
|
14
|
+
spec.orig.md
|
|
15
|
+
|
|
16
|
+
# Broadcast page recordings (argus-mask editor --recordings-dir)
|
|
17
|
+
/recordings/
|
|
18
|
+
# Voices page: checked clips and each voice's record and clips (argus-mask editor --voices-dir)
|
|
19
|
+
/voices/
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<!-- ai-audit:agent-packs:start -->
|
|
2
|
+
## Agent coding packs (ai-code-auditor)
|
|
3
|
+
|
|
4
|
+
Do not wait for a pasted brief — fetch the audit packs for `argus-mask` yourself.
|
|
5
|
+
|
|
6
|
+
The audit API base URL is `$AI_AUDIT_PUBLIC_BASE_URL`. The real host/IP is in the local `.env`
|
|
7
|
+
(see `.env.example`, placeholder `http://<AUDIT_HOST>:8765`); read it there, don't guess.
|
|
8
|
+
|
|
9
|
+
1. **Bundle (one call):** `GET $AI_AUDIT_PUBLIC_BASE_URL/audit/packs/bundle?repo=argus-mask`
|
|
10
|
+
(world model + ranked specialist packs + token-capped `briefs_markdown`;
|
|
11
|
+
optional `&token_budget=2500&q=<keyword>&limit_specialists=5`).
|
|
12
|
+
CLI equivalent: `ai-audit packs --bundle --repo argus-mask`.
|
|
13
|
+
2. **Claim** the specialist pack you pick:
|
|
14
|
+
`POST $AI_AUDIT_PUBLIC_BASE_URL/audit/packs/<id>/status` `{"status":"claimed"}`
|
|
15
|
+
(send `X-API-Token: $AI_AUDIT_API_TOKEN`).
|
|
16
|
+
3. **Fix:** `ai-audit repair --pack-id <id>` (agent-composer repair loop; fetches the pack for you;
|
|
17
|
+
HTTP: `POST http://127.0.0.1:9150/api/repair/run` `{"pack_id":"<id>","mode":"review"}`),
|
|
18
|
+
or read `GET $AI_AUDIT_PUBLIC_BASE_URL/audit/packs/<id>` / `.../brief` and implement it yourself.
|
|
19
|
+
4. **Close:** `POST $AI_AUDIT_PUBLIC_BASE_URL/audit/validate_patch` `{"repo":"argus-mask","patch":"<diff>","pack_id":"<id>"}`
|
|
20
|
+
— a passing run marks the pack `resolved`.
|
|
21
|
+
|
|
22
|
+
World-model constraints (ownership, boundaries, scan branch `dev`) win over guesswork.
|
|
23
|
+
Full flow: ai-code-auditor `AGENTS.md`.
|
|
24
|
+
<!-- ai-audit:agent-packs:end -->
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-11-13
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Splat heads**: a LAM Gaussian-splat avatar (a `.zip`: `skin.glb`, `offset.ply`, `animation.glb`,
|
|
15
|
+
`vertex_order.json`) is a face like any other: served (`GET /face/{id}/model`, and `/face/{id}/model/{file}` by its
|
|
16
|
+
own name), shown by the viewer and the editor's Saved faces previews, driven by mask's frames, rest sets, idle life
|
|
17
|
+
and lip budget (measured from the archive's `skin.glb`, with a jaw cap for the weak splat mouth: `?jaw=`), and
|
|
18
|
+
recorded frame-exact by `record` and the Broadcast page. The manifest's model gains `format` (`gltf`, the default,
|
|
19
|
+
or `lam-splat`); the wire contract is 1.2, and older manifests read as before. The archive is checked in memory,
|
|
20
|
+
never unpacked to disk: one folder of known members, size, count and compression-ratio caps, no path tricks or
|
|
21
|
+
links, ARKit morph targets only. `inspect` takes a `.zip` (and `--write-manifest` for it, with the splat framing).
|
|
22
|
+
The viewer draws one with Myned AI's gsplat-flame-avatar-renderer 1.4.0 (MIT) and JSZip 3.10.1 (MIT), vendored
|
|
23
|
+
with three.js r187's `OrbitControls.js`; `window.__mask.renderer` says `splat` or `mesh`. LAM's weights (CC BY-NC
|
|
24
|
+
4.0) and FLAME are non-commercial, so a LAM head is `personal`, never cleared. `display=hld` refuses a splat face
|
|
25
|
+
for now.
|
|
26
|
+
- **Personal and cleared faces and voices** (#19). Provenance gains `personal` (your own use, anything lawful in
|
|
27
|
+
private, such as a cartoon or a character LoRA; needs only a `source`) and `consented` (a real person or owned
|
|
28
|
+
character under a release: `subject` and `release_sha256`). Every provenance carries a derived `cleared`: true for
|
|
29
|
+
`stock`, `synthetic` and `consented`. `/faces`, the Saved faces page and the Broadcast page show it.
|
|
30
|
+
`inspect --write-manifest` defaults to `personal`; `stock` and `synthetic` still need a licence. The Voices page has a
|
|
31
|
+
**Personal use** source that records where the clips came from and is never cleared. The wire contract is 1.1.
|
|
32
|
+
- **The face follows the voice** (#14, #15). A new `prosody.py` reads each frame's loudness and pitch from the audio
|
|
33
|
+
and marks stressed syllables, phrase ends and phrase starts. In takes:
|
|
34
|
+
- **Nods and brows.** The head nods on stressed syllables (up to about 2.5 degrees), settles with a small tilt at
|
|
35
|
+
each phrase end (alternating sides), and the brows rise on emphasis, about once a phrase, added to the driver's own.
|
|
36
|
+
- **Blinks in pauses.** Most blinks come just as a phrase ends; none starts while a word is sounding unless the last
|
|
37
|
+
was more than 8 s ago. About 18 a minute, sometimes doubled.
|
|
38
|
+
- **Saccades.** The eyes jump and hold for 0.2-0.6 s with a slight drift. A phrase start or a pause sometimes takes
|
|
39
|
+
them away (to the side, or up, as people look away to think), and the head turns a little with them.
|
|
40
|
+
- **Micro-motion.** Slow, small changes of brow tension, and of mouth tension while not speaking.
|
|
41
|
+
|
|
42
|
+
It is all a function of the audio and the seed, so takes stay byte-identical. On five synthetic voices: 1.8 to 2.0
|
|
43
|
+
stresses a second of speech, brows raised in 7-20% of frames, and every blink starting in a pause or between words.
|
|
44
|
+
Analysis costs about 0.3% of real time.
|
|
45
|
+
- **Rest sets and idle life**: a face at rest shows a named baseline expression, `warm` by default (also `neutral`,
|
|
46
|
+
`calm`, `bright`, `attentive`, and your own in the assets' `expressions.json`), chosen per face on Saved faces and
|
|
47
|
+
served as `GET /face/{id}/rest`; a screen may override it (`?rest=`). While speaking, the set's brows, eyes and cheeks
|
|
48
|
+
hold and its mouth weights go at half. At rest the face blinks (some doubled), glances with the head following,
|
|
49
|
+
breathes, and shifts expression now and then (`?life=0` to `2`). Viewer-side only: takes are unchanged.
|
|
50
|
+
- **Lip budget**: the viewer measures how each mouth shape moves the gap between the lips, from the face's own mesh,
|
|
51
|
+
and scales the lip-closing shapes so the lips never pass through each other (1 mm allowed for pressing), with
|
|
52
|
+
`mouthShrugLower` and the rolls at 0.7. On ginger-3, lips overlapping by more than 3 mm went from 12% of frames to
|
|
53
|
+
none, and the lower lip no longer rides up over the upper. Screens, `record` and the editor's previews alike.
|
|
54
|
+
- **Smooth playback between frames** (#13): screens and the editor's `speak` preview draw a cubic through the
|
|
55
|
+
frames on either side instead of a straight line between two, so 30 fps motion has no corners (the speed jump at
|
|
56
|
+
each frame, up to 0.44 of a weight per frame on Audio2Face output, is now none). Weights use a monotone cubic, so a
|
|
57
|
+
mouth never overshoots a closure; the head turns as Catmull-Rom. The curve passes through every frame at its own
|
|
58
|
+
time: lip timing is unchanged, and `record` (which renders at the frame times) shows exactly the frames, as before.
|
|
59
|
+
- **The editor's previews speak**: the `speak` pose (now the default) loops 12 s of real Audio2Face motion, with the
|
|
60
|
+
lip budget, rest set and idle life a screen shows, instead of a sine-wave mouth; a `rest:` picker previews each set.
|
|
61
|
+
|
|
62
|
+
- **Named TTS speakers and delivery instructions**: a voice written `speaker:<name>` (`--voice`, `ARGUS_MASK_TTS_VOICE`,
|
|
63
|
+
`/speak`'s `voice`) is sent as the service's own speaker, not as a clone, for services that name their speakers, such
|
|
64
|
+
as Qwen3-TTS's CustomVoice (`speaker:Vivian`). `POST /speak` takes an optional `instruction`, how to say it, passed
|
|
65
|
+
to a service that reads one; like the text, it is never logged.
|
|
66
|
+
- **Face aliases** (#41): a stable name for a face, such as `holly` for a persona, that the agent speaking and the
|
|
67
|
+
kiosk showing it use for good while the face behind it is chosen in the editor (Saved faces → Aliases: add,
|
|
68
|
+
repoint, remove, open on a screen). Kept in the assets' `aliases.json`, written by the editor and read by the server
|
|
69
|
+
whenever it changes, so its read-only mount and no restart. Every route that names a face takes an alias; sources
|
|
70
|
+
and screens meet on the name they use; `GET /aliases` and `/health`'s `aliases` list them, `/faces` is unchanged. A
|
|
71
|
+
screen watching an alias that is repointed loads the new face on the next stream, whose header names the face
|
|
72
|
+
itself. An alias never hides a face, points only at a served face and never at another alias; renaming a face
|
|
73
|
+
carries its aliases, deleting one an alias points at is refused.
|
|
74
|
+
- **Record a broadcast from the Broadcast page**: a Record to file switch (frame size, look) for Say and Push. The
|
|
75
|
+
recorder listens on the face first, the broadcast goes, then the clip renders and muxes, as `argus-mask record` does,
|
|
76
|
+
into `ARGUS_MASK_RECORDINGS_DIR`; the page plays it, offers it to download and lists recent clips. `record` now
|
|
77
|
+
tells its caller it is waiting only once its socket is open, and muxes off the event loop.
|
|
78
|
+
- **Face voices** (#37): the editor's **Voices** page checks reference clips for a cloned voice (speech length, sample
|
|
79
|
+
rate, bandwidth, clipping, level, noise, edges, pauses, delivery, whether the transcript fits), each check with a
|
|
80
|
+
verdict and what to change (`argus_mask.voice`, pure numpy); generates synthetic candidates from the TTS service's
|
|
81
|
+
own speaker or a described voice, reading a script per delivery; makes the voice on the service (`/clone-voice`)
|
|
82
|
+
with a `voice.json` record of where it came from (generated, your own, or a consenting speaker's under a release,
|
|
83
|
+
by its sha256) in `ARGUS_MASK_VOICES_DIR`; refuses unsuitable clips and ids the service already has; tries a line
|
|
84
|
+
in the browser. `docs/voices.md`: the reference, recording your own, synthetic voices, one voice per delivery.
|
|
85
|
+
- **`say` tidies speech** (#36, on by default, `--as-is` and the Broadcast page's Tidy speech turn it off): numbers,
|
|
86
|
+
times, dates and symbols go as words (`speech.speakable`), a sentence of one to three words goes with the next, and
|
|
87
|
+
each sentence is faded in and out and brought to the same speech level. Recordings through Miso had invented
|
|
88
|
+
words for digits, one-word sentences far off in pitch, clicks at sentence ends, and 9 dB between sentences.
|
|
89
|
+
- **`POST /push` and `POST /speak`** on the server (#38), for agents: a WAV or raw PCM body (or text through the
|
|
90
|
+
configured TTS) plays into a face exactly as a source does, paced, one source per face, behind the same guards;
|
|
91
|
+
`require_screen=1` refuses when no screen can hear it. The server never sees text with `/push`.
|
|
92
|
+
- **External sound for house speakers** (#39): `?audio=external` (or `mute=1`) makes a viewer animate on the session's
|
|
93
|
+
timeline without playing anything and without a tap; `?delay=<ms>` lines the lips up with a speaker system's
|
|
94
|
+
buffer (Snapcast). `/health.screens` gains `sound: local | external`; an external screen is ready once connected.
|
|
95
|
+
The On screen dialog sets both.
|
|
96
|
+
- **`say --context`** (#40, opt-in): each sentence goes to an HTTP TTS with the one before it, text and levelled
|
|
97
|
+
audio, as Miso's `context` (voluptas#4); a service that refuses the field is asked again without it. Off by default:
|
|
98
|
+
with a cloned voice on Miso, chaining the audio made pitch drift up and loudness sink over a briefing, where
|
|
99
|
+
sentences sent alone held steady. The Broadcast page has it as an experimental checkbox.
|
|
100
|
+
- **Recording presets** (#35): 1080p, 720p and 480p, and the HLD portrait panel; an info tooltip on temperature.
|
|
101
|
+
- **Temperature on the Broadcast page**: a slider (0.1 to 1.5, Miso's range), left to the service's own value unless
|
|
102
|
+
set; it goes as the request's `temperature` (`-p temperature=…`). Over Wyoming, `-p` fields other than the voice's
|
|
103
|
+
now ride beside the text in the `synthesize` event, for a bridge to pass on, instead of being dropped.
|
|
104
|
+
- **Voices from an HTTP TTS**: for a `…/synthesise[/stream]` URL (Miso, agent-composer's TTS servers) the editor lists
|
|
105
|
+
`GET …/speakers` and `GET …/clones`, named as the Wyoming bridge names them (`0`, `clone:<id>`), so the Broadcast
|
|
106
|
+
page's voice picker works talking to Miso directly.
|
|
107
|
+
- **Broadcast page** in the editor: the server's faces with their readiness, and a form with every `say` option
|
|
108
|
+
(voice listed from a Wyoming service, pause, seed, stream, TTS URL, `-p` fields) or a WAV to `push`, run through
|
|
109
|
+
the CLI's own client with live progress, Stop, and the matching command line. `client.tts_voices()` asks a Wyoming
|
|
110
|
+
service for its voices.
|
|
111
|
+
- **Editor to screen** (#28). On screen… in the editor and on Saved faces: checks the server lists the exported
|
|
112
|
+
face (and says plainly when it does not), builds the viewer link at `ARGUS_MASK_VIEWER_URL` with display, quality,
|
|
113
|
+
kiosk and background, to open, copy or scan as a QR code, shows whether screens are watching and can be heard,
|
|
114
|
+
plays a test chime through the server's source socket, and gives the `say` command. The viewer shows **Tap to
|
|
115
|
+
start** until its sound is unlocked and a brief **Ready** after, takes `kiosk=1`, and reports its state on its
|
|
116
|
+
socket (`ViewerStatus`, in the wire schema); `/health` lists `screens` per face. The viewer's reconnect backoff
|
|
117
|
+
now grows as it was meant to.
|
|
118
|
+
- **Stock heads republished**: `stock-01` revision 4 and `stock-02`'s first lock, both built by the recipe with the wet
|
|
119
|
+
line and baked skin maps (about 10 MB each). `fetch_stock_head.py --id stock-02` fetches the second.
|
|
120
|
+
- **Recipe: wet line and skin maps** (#11, #9). A tear line along each lower lid (`wetline`, found by casting
|
|
121
|
+
rays at the eye, moving with the lid under every shape key), and normal and roughness maps baked from the skin's
|
|
122
|
+
colour map into the `.glb`. The viewer gives the wet line a fading glossy film and lays its generated pores over
|
|
123
|
+
the baked maps.
|
|
124
|
+
- **Two-pass hair** (#12): an opaque core, then the fringe blended over it. On a 10 s recording, 26% less edge
|
|
125
|
+
shimmer and 59% fewer edge pops than the cut-out; alpha to coverage alone managed 6%. The default in every profile.
|
|
126
|
+
- **Triangle budget 45k** (was 35k), while the weakest panels are not targets.
|
|
127
|
+
- **Viewer look** (`viewer/look.js`, issue #20 tier 1): a three-point light rig, image-based light and PBR Neutral
|
|
128
|
+
tone mapping (#8); soft shadows from the key light (#8); physical skin with wrap lighting for subsurface scatter,
|
|
129
|
+
sheen and a generated pore-scale detail map (#9); catch-lights from the eyes' cornea shell, which follow the
|
|
130
|
+
gaze (#11); alpha-to-coverage hair edges (#12); and post-processing (GTAO, depth of field, bloom, grain, optional
|
|
131
|
+
SMAA) in `low`, `medium` and `high` profiles with a URL switch per effect (#10). `record` renders with `high`
|
|
132
|
+
(`--quality`). The editor's previews use the same look.
|
|
133
|
+
- **Triangle budget 35k** (was 30k): a dressed head (a uniform is ~5k triangles, MakeHuman's teeth 7k) ran just over it.
|
|
134
|
+
- **Editor: compare, undo, build cache.** A Before card beside the preview shows the previous build and what
|
|
135
|
+
changed; the large view shows both side by side, posed together. Undo and redo step through the builds. Each
|
|
136
|
+
build is cached by what it was made from (the preset without id and name, the recipe, the Blender), so a repeat
|
|
137
|
+
copies in well under a second instead of running Blender: `GET /cache/{key}` serves an earlier build directly.
|
|
138
|
+
`ARGUS_MASK_BUILD_CACHE` sets how many are kept (40).
|
|
139
|
+
- **The engine.** Audio in, animation frames out: a streaming resampler to 16 kHz, a driver, a seeded idle layer
|
|
140
|
+
(blinks, gaze, head sway) and frames stamped on the audio timeline. The same audio, driver and seed give the same
|
|
141
|
+
take file.
|
|
142
|
+
- **Drivers.** `energy` moves the mouth from loudness on the CPU, for tests and hosts with no NVIDIA card. `a2f` is
|
|
143
|
+
the client of the Audio2Face server.
|
|
144
|
+
- **Faces.** A `.glb` with ARKit-named morph targets and a `manifest.json` with required provenance. A face with no
|
|
145
|
+
provenance, a `curated` face, or one whose model no longer matches its sha256 is not served.
|
|
146
|
+
- **Server** (`argus-mask serve`, port 8109): `/health`, `/faces`, `/face/{id}`, `/face/{id}/model`, a source socket
|
|
147
|
+
(one per face) and a viewer socket, both under the same origin rule as cortex's write guard.
|
|
148
|
+
- **Viewer**, bundled and served at `/`: three.js, lip sync on the page's own audio clock, the `flat` and `hld`
|
|
149
|
+
display tiers.
|
|
150
|
+
- **CLI:** `inspect` (with `--write-manifest`), `drive`, `push`, `say`, `serve`, `schema`.
|
|
151
|
+
- **The stock head.** `src/argus_mask/editor/build_head.py` (was `scripts/build_stock_head.py`) builds a head with headless Blender and MPFB from a preset (CC0
|
|
152
|
+
MakeHuman assets, adults only). The model is not in the repo: `scripts/fetch_stock_head.py` (`make head`) downloads
|
|
153
|
+
it and checks it against `scripts/stock_heads/stock-01.lock.json`.
|
|
154
|
+
- Wire schema `schema/mask-wire.schema.json`.
|
|
155
|
+
- **Face editor** (`argus-mask editor`, port 8111): a page that builds heads from presets with headless Blender and
|
|
156
|
+
MPFB, about ten seconds a build, with a posed preview. Ages 18 and up; every head wears an outfit. Skin, hair,
|
|
157
|
+
clothes and a hat are picked from thumbnails;
|
|
158
|
+
117 shape sliders (head, forehead, brow ridge, eyes, ears, cheeks, nose, mouth, chin, neck) over MakeHuman's CC0
|
|
159
|
+
modelling targets, from `modifiers.json`, generated from MPFB's catalogue. Collapsible panels, a filter, a hover
|
|
160
|
+
preview of each asset, a preview card that opens a large view, a spinner and timer while a build runs, and
|
|
161
|
+
**Live update**, which builds a second after the last change. A build is a preview (`assets/.preview/`);
|
|
162
|
+
**Export** puts it in `assets/<id>` and on `/faces`; **Download preset** saves the form and
|
|
163
|
+
**Open preset…** loads one. A **Saved faces** page reviews, remixes, renames and deletes (to `assets/.trash/`) the
|
|
164
|
+
faces the editor made, and reviews the others. It offers only assets an installed pack licenses, writes their
|
|
165
|
+
licences into the face's provenance, replaces only faces it made, and is behind the write guards. Blender, MPFB and
|
|
166
|
+
the asset packs are the user's to install; none ships.
|
|
167
|
+
- **A second stock preset**, `stock-02`: a female head, about 25, short brown hair, in the same crew uniform as
|
|
168
|
+
`stock-01` (suits02 pack, CC0). Not published yet.
|
|
169
|
+
- **`argus-mask record`**: waits for the next session on a face and renders it to a video (MP4 or WebM), offline and
|
|
170
|
+
frame-exact, in a headless browser, muxed with the session's audio; the take and WAV are written beside it. The
|
|
171
|
+
viewer gets a `?record=1` mode posed from outside. Needs the `record` extra (playwright, imageio-ffmpeg).
|
|
172
|
+
- **The Audio2Face server** (`a2f/`): NVIDIA's Audio2Face-3D SDK behind the `a2f` driver's protocol, in its own
|
|
173
|
+
Docker image. It downloads its model and builds a TensorRT engine for the host's card on first start, and holds
|
|
174
|
+
about 0.5 GB of VRAM. `docker compose --profile a2f` runs it beside mask. The image is built locally, not published.
|
|
175
|
+
|
|
176
|
+
- The viewer's audio buffer is tunable (`?buffer=<ms>`, default 250).
|
|
177
|
+
- The stock head lock points at revision 2 of `stock-01`, with the mouth fixes.
|
|
178
|
+
- **Clothes.** A preset's `assets.clothes` lists garments; the body under them is removed and they are cut with the
|
|
179
|
+
bust. `stock-01` revision 3 wears `thegreatengineer_galactic_warrior_uniform` from the MakeHuman community's
|
|
180
|
+
suits02 pack (CC0): a high-collared crew tunic, since a bust in nothing looked undressed.
|
|
181
|
+
|
|
182
|
+
- **`say` speaks Wyoming** (`--tts-url wyoming://host:port`, `-p voice=…`), the protocol of Home Assistant's TTS
|
|
183
|
+
servers and agent-composer's XTTS and Miso bridges, beside the HTTP PCM stream it already understood.
|
|
184
|
+
- `say` says it is waiting for the TTS service, and Ctrl-C stops the wait at once instead of hanging on the socket.
|
|
185
|
+
- **`say --voice`** and `ARGUS_MASK_TTS_VOICE`: the voice to ask the TTS service for, a stored clone's name or a
|
|
186
|
+
speaker number, put in the field each kind of service expects.
|
|
187
|
+
- **`say --pause`**: the text goes to the TTS service one sentence at a time, with 300 ms of silence between
|
|
188
|
+
sentences and 600 ms between paragraphs, so sentences no longer run into each other; `--pause 0` sends it whole.
|
|
189
|
+
- `say` pads the utterance with 150 ms of silence before and 300 ms after, so a waking output device does not eat the
|
|
190
|
+
first syllable and the mouth closes before the stream ends.
|
|
191
|
+
- The viewer counts audio chunks that arrived while the browser had paused its audio clock (`__mask.audioDropped`) and
|
|
192
|
+
asks for the clock back, so a clipped start can be told from a clipped reply.
|
|
193
|
+
|
|
194
|
+
### Fixed
|
|
195
|
+
|
|
196
|
+
- **A provenance says what it means** (#64). An unknown field is refused, not dropped in silence, and `licence` is
|
|
197
|
+
read as `license` (a provenance with both, disagreeing, is refused). A `source` of only whitespace is refused like a
|
|
198
|
+
missing one. A dumped record's `cleared` still reads back, and is recomputed.
|
|
199
|
+
- **A source that sends faster than real time is slowed, not cut off** (#2). The `a2f` driver lets up to 2 s of audio
|
|
200
|
+
wait on frames, then holds the source until the model catches up; a burst of a 15 s clip used to end the stream with
|
|
201
|
+
"fell more than 10s behind". A model that sends no frame for 10 s while the source is held still ends it.
|
|
202
|
+
- **The Audio2Face server reports a dead worker at once** (#4), not on the client's next message: a worker that crashed
|
|
203
|
+
during a pause in speech left the face frozen with no error.
|
|
204
|
+
- **`POST /speak` in Docker**: `compose.yaml` passes `TTS_URL` and `TTS_VOICE` from `.env` to the server as
|
|
205
|
+
`ARGUS_MASK_TTS_URL` and `ARGUS_MASK_TTS_VOICE`; without them `/speak` always answered `422`.
|
|
206
|
+
- A Broadcast recording that caught another source's session on the face is discarded, not kept as this broadcast's,
|
|
207
|
+
and a failed or stopped render no longer leaves its frames behind (#35).
|
|
208
|
+
- `say` (without `--stream`), `/speak` and the Voices page's Try buffered a TTS reply without limit: they now refuse
|
|
209
|
+
past 10 minutes of speech, saying to send less or stream (#3). The client pacing test has a wider margin (#5).
|
|
210
|
+
- `say --voice <name> -p speaker=<n>` against a Wyoming service dropped the voice name: a speaker now goes with the
|
|
211
|
+
voice (Wyoming's `{name, speaker}`), and only a name in the request stands instead of `--voice`.
|
|
212
|
+
- **`say` no longer dumps a slow TTS model's speech all at once.** It fetches the whole utterance before claiming the
|
|
213
|
+
face and plays it at the speed of speech (`--stream` keeps the old behaviour for a service faster than real time),
|
|
214
|
+
and a source's pacing clock now starts at its first chunk rather than at connect.
|
|
215
|
+
|
|
216
|
+
- **Audio no longer runs ahead of the mouth with a slow driver.** Audio2Face answers about 0.3 s behind the audio it
|
|
217
|
+
is given. The server now holds each audio chunk until frames reach its end, and ends a stream whose driver falls
|
|
218
|
+
more than 10 s behind.
|
|
219
|
+
|
|
220
|
+
[Unreleased]: https://github.com/smk762/argus-mask/compare/v0.1.0...HEAD
|
|
221
|
+
[0.1.0]: https://github.com/smk762/argus-mask/releases/tag/v0.1.0
|