laughing-man 1.2.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 (45) hide show
  1. laughing_man-1.2.0/.envrc +6 -0
  2. laughing_man-1.2.0/.github/workflows/ci.yml +46 -0
  3. laughing_man-1.2.0/.github/workflows/pypi-publish.yml +33 -0
  4. laughing_man-1.2.0/.github/workflows/release.yml +142 -0
  5. laughing_man-1.2.0/.gitignore +19 -0
  6. laughing_man-1.2.0/.python-version +1 -0
  7. laughing_man-1.2.0/AGENTS.md +78 -0
  8. laughing_man-1.2.0/CHANGELOG.md +30 -0
  9. laughing_man-1.2.0/PKG-INFO +11 -0
  10. laughing_man-1.2.0/README.md +49 -0
  11. laughing_man-1.2.0/docs/ARCHITECTURE.md +107 -0
  12. laughing_man-1.2.0/docs/proposals/face-detection-multi-angle-proposal.md +256 -0
  13. laughing_man-1.2.0/pyproject.toml +77 -0
  14. laughing_man-1.2.0/scripts/magenta_to_alpha.py +342 -0
  15. laughing_man-1.2.0/src/laughing_man/__init__.py +16 -0
  16. laughing_man-1.2.0/src/laughing_man/__version__.py +3 -0
  17. laughing_man-1.2.0/src/laughing_man/assets/__init__.py +1 -0
  18. laughing_man-1.2.0/src/laughing_man/assets/limg.png +0 -0
  19. laughing_man-1.2.0/src/laughing_man/assets/ltext.png +0 -0
  20. laughing_man-1.2.0/src/laughing_man/bootstrap.py +28 -0
  21. laughing_man-1.2.0/src/laughing_man/box_tracking.py +160 -0
  22. laughing_man-1.2.0/src/laughing_man/camera.py +77 -0
  23. laughing_man-1.2.0/src/laughing_man/cascade.py +174 -0
  24. laughing_man-1.2.0/src/laughing_man/cli.py +117 -0
  25. laughing_man-1.2.0/src/laughing_man/cli_options.py +277 -0
  26. laughing_man-1.2.0/src/laughing_man/constants.py +66 -0
  27. laughing_man-1.2.0/src/laughing_man/deps.py +21 -0
  28. laughing_man-1.2.0/src/laughing_man/detection.py +93 -0
  29. laughing_man-1.2.0/src/laughing_man/logging_setup.py +20 -0
  30. laughing_man-1.2.0/src/laughing_man/model.py +111 -0
  31. laughing_man-1.2.0/src/laughing_man/overlay.py +247 -0
  32. laughing_man-1.2.0/src/laughing_man/postprocess.py +358 -0
  33. laughing_man-1.2.0/src/laughing_man/privacy.py +63 -0
  34. laughing_man-1.2.0/src/laughing_man/protocols.py +44 -0
  35. laughing_man-1.2.0/src/laughing_man/roi.py +324 -0
  36. laughing_man-1.2.0/src/laughing_man/run.py +399 -0
  37. laughing_man-1.2.0/src/laughing_man/tuning.py +205 -0
  38. laughing_man-1.2.0/src/laughing_man/yunet_face.py +108 -0
  39. laughing_man-1.2.0/tests/test_box_tracking.py +30 -0
  40. laughing_man-1.2.0/tests/test_cascade.py +36 -0
  41. laughing_man-1.2.0/tests/test_cascaded_source.py +38 -0
  42. laughing_man-1.2.0/tests/test_cli.py +26 -0
  43. laughing_man-1.2.0/tests/test_postprocess.py +39 -0
  44. laughing_man-1.2.0/tests/test_version.py +33 -0
  45. laughing_man-1.2.0/uv.lock +1703 -0
@@ -0,0 +1,6 @@
1
+ # Create virtual environment and install deps (uv.lock) on first use
2
+ if [ ! -d .venv ]; then
3
+ uv venv && uv sync
4
+ fi
5
+
6
+ source .venv/bin/activate
@@ -0,0 +1,46 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [master, main]
6
+ pull_request:
7
+ branches: [master, main]
8
+
9
+ jobs:
10
+ lint-and-test:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ with:
15
+ fetch-depth: 0
16
+
17
+ - name: Install uv
18
+ uses: astral-sh/setup-uv@v5
19
+ with:
20
+ version: "latest"
21
+
22
+ - name: Python version
23
+ run: uv python install 3.12
24
+
25
+ - name: Sync dependencies
26
+ run: uv sync --group dev --python 3.12
27
+
28
+ - name: Commitizen changelog (dry-run)
29
+ run: uv run cz changelog --dry-run
30
+
31
+ - name: Commitizen commit messages (pull requests)
32
+ if: github.event_name == 'pull_request'
33
+ run: |
34
+ uv run cz check --rev-range "${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}"
35
+
36
+ - name: Ruff check
37
+ run: uv run ruff check src tests scripts
38
+
39
+ - name: Ruff format
40
+ run: uv run ruff format --check src tests scripts
41
+
42
+ - name: ty
43
+ run: uv run ty check
44
+
45
+ - name: Pytest
46
+ run: uv run pytest
@@ -0,0 +1,33 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ permissions:
8
+ contents: read
9
+ id-token: write
10
+
11
+ jobs:
12
+ publish:
13
+ name: Publish package
14
+ runs-on: ubuntu-latest
15
+ environment:
16
+ name: pypi
17
+ url: https://pypi.org/p/laughing-man
18
+ steps:
19
+ - name: Checkout release tag
20
+ uses: actions/checkout@v4
21
+ with:
22
+ ref: ${{ github.event.release.tag_name }}
23
+
24
+ - name: Install uv
25
+ uses: astral-sh/setup-uv@v5
26
+ with:
27
+ version: "latest"
28
+
29
+ - name: Build sdist and wheel
30
+ run: uv build
31
+
32
+ - name: Publish to PyPI
33
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,142 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ branches: [master, main]
6
+
7
+ permissions:
8
+ contents: write
9
+ id-token: write
10
+
11
+ jobs:
12
+ release:
13
+ runs-on: ubuntu-latest
14
+ outputs:
15
+ bumped: ${{ steps.bump.outputs.bumped }}
16
+ version: ${{ steps.release_meta.outputs.version }}
17
+ if: |
18
+ github.event_name == 'push'
19
+ && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main')
20
+ && github.event.head_commit != null
21
+ && !contains(github.event.head_commit.message, '[skip ci]')
22
+ steps:
23
+ - name: Checkout
24
+ uses: actions/checkout@v4
25
+ with:
26
+ fetch-depth: 0
27
+
28
+ # Private half of the repo Deploy key (Settings → Deploy keys). Must allow write access.
29
+ # Add repository secret RELEASE_DEPLOY_KEY with the full PEM (BEGIN/END lines included).
30
+ - name: SSH agent (deploy key)
31
+ uses: webfactory/ssh-agent@v0.9.0
32
+ with:
33
+ ssh-private-key: ${{ secrets.RELEASE_DEPLOY_KEY }}
34
+
35
+ - name: Trust GitHub SSH host key
36
+ run: mkdir -p ~/.ssh && ssh-keyscan -t ed25519,rsa,ecdsa github.com >> ~/.ssh/known_hosts
37
+
38
+ - name: Use SSH for git push
39
+ run: git remote set-url origin "git@github.com:${GITHUB_REPOSITORY}.git"
40
+
41
+ - name: Install uv
42
+ uses: astral-sh/setup-uv@v5
43
+ with:
44
+ version: "latest"
45
+
46
+ - name: Configure git
47
+ run: |
48
+ git config user.name "github-actions[bot]"
49
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
50
+
51
+ - name: Sync dependencies (incl. commitizen)
52
+ run: uv sync --group dev --python 3.12
53
+
54
+ - name: Bump version, changelog, and tag
55
+ id: bump
56
+ run: |
57
+ set +e
58
+ uv run cz bump --yes --changelog --no-verify
59
+ code=$?
60
+ set -e
61
+ if [ "$code" -eq 0 ]; then
62
+ echo "bumped=true" >> "$GITHUB_OUTPUT"
63
+ elif [ "$code" -eq 3 ] || [ "$code" -eq 21 ]; then
64
+ echo "bumped=false" >> "$GITHUB_OUTPUT"
65
+ echo "No release: no new commits (exit 3), or commits are not bump-eligible types like feat/fix (exit 21, e.g. docs/ci/chore only). Skipping build and push."
66
+ else
67
+ exit "$code"
68
+ fi
69
+
70
+ - name: Prepare release notes
71
+ id: release_meta
72
+ if: steps.bump.outputs.bumped == 'true'
73
+ run: |
74
+ set -euo pipefail
75
+ VERSION=$(uv run python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
76
+ echo "version=$VERSION" >> "$GITHUB_OUTPUT"
77
+ # Commitizen range PREV..NEW = commits between tags (matches tag_format v$version).
78
+ PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || true)
79
+ if [ -n "$PREV_TAG" ]; then
80
+ PREV_VER="${PREV_TAG#v}"
81
+ uv run cz changelog "${PREV_VER}..${VERSION}" --dry-run > release_notes.md
82
+ else
83
+ uv run cz changelog "${VERSION}" --dry-run > release_notes.md
84
+ fi
85
+
86
+ - name: Build sdist and wheel
87
+ if: steps.bump.outputs.bumped == 'true'
88
+ run: uv build
89
+
90
+ # Only when cz bump exited 0: a release commit and vX.Y.Z tag exist locally.
91
+ # Exit 3/21 never create a tag, so this step is skipped — no tags pushed.
92
+ - name: Push release commit and tag
93
+ if: steps.bump.outputs.bumped == 'true'
94
+ run: |
95
+ git push origin "HEAD:${{ github.ref_name }}"
96
+ git push origin --tags
97
+
98
+ - name: Create GitHub release
99
+ if: steps.bump.outputs.bumped == 'true'
100
+ uses: softprops/action-gh-release@v2
101
+ with:
102
+ tag_name: v${{ steps.release_meta.outputs.version }}
103
+ body_path: release_notes.md
104
+ files: |
105
+ dist/*
106
+
107
+ - name: Upload build artifacts
108
+ if: steps.bump.outputs.bumped == 'true'
109
+ uses: actions/upload-artifact@v4
110
+ with:
111
+ name: dist
112
+ path: dist/
113
+
114
+ # Releases created with GITHUB_TOKEN do not trigger other workflows' `on: release`
115
+ # handlers, so PyPI publish must run in this workflow after the GitHub Release exists.
116
+ publish-pypi:
117
+ name: Publish to PyPI
118
+ needs: release
119
+ if: needs.release.outputs.bumped == 'true'
120
+ runs-on: ubuntu-latest
121
+ environment:
122
+ name: pypi
123
+ url: https://pypi.org/p/laughing-man
124
+ permissions:
125
+ contents: read
126
+ id-token: write
127
+ steps:
128
+ - name: Checkout release tag
129
+ uses: actions/checkout@v4
130
+ with:
131
+ ref: v${{ needs.release.outputs.version }}
132
+
133
+ - name: Install uv
134
+ uses: astral-sh/setup-uv@v5
135
+ with:
136
+ version: "latest"
137
+
138
+ - name: Build sdist and wheel
139
+ run: uv build
140
+
141
+ - name: Publish to PyPI
142
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,19 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ .pytest_cache/
6
+ .mypy_cache/
7
+ .ruff_cache/
8
+ .env
9
+ *.egg-info/
10
+ dist/
11
+ build/
12
+
13
+ # Local scratch / fixture captures at repo root (avoid accidental commits)
14
+ /*.png
15
+ /*.jpg
16
+ /*.jpeg
17
+ /*.gif
18
+ /*.webp
19
+ /*.bmp
@@ -0,0 +1 @@
1
+ 3.13
@@ -0,0 +1,78 @@
1
+ # Agent and contributor notes
2
+
3
+ ## Cursor cloud and headless environments
4
+
5
+ This is a **Python CLI** app (webcam face overlay) managed with **uv** — no Docker, databases, or external services required for development.
6
+
7
+ ### Quick reference
8
+
9
+ | Action | Command |
10
+ | --- | --- |
11
+ | Install / sync deps | `uv sync --group dev` |
12
+ | Lint (Ruff) | `uv run ruff check src tests scripts` |
13
+ | Typecheck (ty) | `uv run ty check` |
14
+ | Run tests | `uv run pytest` (add `-v` for verbose) |
15
+ | CLI help | `uv run laughing-man --help` |
16
+ | Run the app | `uv run laughing-man` (needs a webcam) |
17
+
18
+ ### Environment caveats
19
+
20
+ - **Webcam at runtime.** The main command opens a camera (often `/dev/video0` on Linux). In a **headless cloud VM** without a camera device, run **unit tests** and **import checks** only; the live overlay loop fails when opening the camera.
21
+ - **Model download.** BlazeFace (`.tflite`) and YuNet (`.onnx`) may be fetched to `~/.cache/laughing-man/` on first use — **network access** may be required once.
22
+ - **`tool.uv.link-mode = "copy"`** in `pyproject.toml` avoids broken OpenCV wheels when the uv cache and `.venv` sit on different filesystems. Do not change this without a good reason.
23
+ - **Python version.** `.python-version` pins **3.13** for local/CI convenience; `requires-python` in `pyproject.toml` remains **>=3.10** for compatibility.
24
+
25
+ ## Conventional commits
26
+
27
+ Use [Conventional Commits](https://www.conventionalcommits.org/) for commit messages so history stays readable and tooling (changelog generators, semantic versioning) can work if adopted later.
28
+
29
+ **Format:** `type(scope): short description`
30
+
31
+ Common **types:** `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `ci`, `chore`.
32
+
33
+ **Guidelines:**
34
+
35
+ - Keep the subject line in imperative mood (e.g. “add ruff config”, not “added”).
36
+ - One logical change per commit when practical (config vs. code fixes vs. CI).
37
+ - Use a body for non-obvious rationale or breaking changes.
38
+
39
+ **Examples:**
40
+
41
+ - `feat(cli): add virtual camera option`
42
+ - `fix(overlay): correct alpha compositing on custom images`
43
+ - `chore(dev): add ruff and ty for lint and type checking`
44
+
45
+ ## Planning work and commits
46
+
47
+ When planning a change set, **sketch the commits in advance**: what each commit will contain and its conventional message. That reduces the risk of **partial-file staging** (`git add -p` or staging only some hunks) mixing unrelated edits in the same file, which is easy to get wrong and produces confusing history.
48
+
49
+ **Practical habits:**
50
+
51
+ - **Order work** so one logical concern is finished before another touches the same files, when you can.
52
+ - If one file must hold multiple concerns, **finish and commit the first concern entirely** (or split into a separate file in a first commit), then apply the second change set.
53
+ - Use **interactive staging** only when you are deliberately splitting a file; double-check `git diff --staged` before committing.
54
+
55
+ ## Linting and type checking
56
+
57
+ This project uses **Ruff** for linting/formatting and **ty** (Astral) for static type checking. **ty** is chosen for speed, alignment with Ruff/uv in the Astral ecosystem, and solid diagnostics; it is configured in `pyproject.toml` under `[tool.ty]`.
58
+
59
+ From the repo root (with dev dependencies installed, e.g. `uv sync --group dev`):
60
+
61
+ ```bash
62
+ uv run ruff check src tests scripts
63
+ uv run ruff format --check src tests scripts
64
+ uv run ty check
65
+ ```
66
+
67
+ Auto-fix import/order issues and apply formatting:
68
+
69
+ ```bash
70
+ uv run ruff check --fix src tests scripts
71
+ uv run ruff format src tests scripts
72
+ ```
73
+
74
+ Run tests:
75
+
76
+ ```bash
77
+ uv run pytest
78
+ ```
@@ -0,0 +1,30 @@
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
+ ## v1.2.0 (2026-04-12)
9
+
10
+ ### Feat
11
+
12
+ - **cli**: add --version and centralize __version__
13
+
14
+ ### Fix
15
+
16
+ - **test**: avoid tomllib for Python 3.10 ty compatibility
17
+
18
+ ## v1.1.0 (2026-04-12)
19
+
20
+ ### Feat
21
+
22
+ - **release**: set version 1.0.0 and run commitizen in CI
23
+ - **cli**: add postprocess command for offline image/video overlay
24
+ - custom overlay image, scale, and chroma-key helper
25
+ - **overlay**: terminal lambda tuning, roi horizontal smoothing, defaults
26
+
27
+ ### Fix
28
+
29
+ - **ci**: skip release when commits are not bump-eligible (e.g. docs/ci)
30
+ - satisfy ruff and ty checks across source and scripts
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.4
2
+ Name: laughing-man
3
+ Version: 1.2.0
4
+ Summary: Webcam Laughing Man face overlay (MediaPipe + OpenCV + Pillow)
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: loguru>=0.7.3
7
+ Requires-Dist: mediapipe>=0.10.33
8
+ Requires-Dist: opencv-python>=4.13.0.92
9
+ Requires-Dist: pillow>=12.2.0
10
+ Requires-Dist: pyvirtualcam>=0.15.0
11
+ Requires-Dist: typer>=0.24.1
@@ -0,0 +1,49 @@
1
+ # laughing-man
2
+
3
+ Webcam **Laughing Man** face overlay (Ghost in the Shell style) using MediaPipe BlazeFace, OpenCV, and Pillow.
4
+
5
+ ## Setup
6
+
7
+ Install dependencies (e.g. with [uv](https://github.com/astral-sh/uv)):
8
+
9
+ ```bash
10
+ uv sync
11
+ ```
12
+
13
+ Run the CLI:
14
+
15
+ ```bash
16
+ uv run laughing-man
17
+ ```
18
+
19
+ ## Virtual webcam (Discord, OBS, browsers)
20
+
21
+ Use `--virtual-cam` to expose the composited video as a camera other apps can select.
22
+
23
+ ### Linux: v4l2loopback
24
+
25
+ The virtual device is provided by the **v4l2loopback** kernel module. Load it before starting the app (often required once per boot):
26
+
27
+ ```bash
28
+ sudo modprobe v4l2loopback
29
+ ```
30
+
31
+ You can create a named device on a fixed number (example):
32
+
33
+ ```bash
34
+ sudo modprobe v4l2loopback devices=1 video_nr=10 card_label="LaughingMan"
35
+ ```
36
+
37
+ Then point the CLI at that device if needed:
38
+
39
+ ```bash
40
+ laughing-man --virtual-cam --v4l2-device /dev/video10
41
+ ```
42
+
43
+ Use `--no-preview` to stream only to the virtual camera (no OpenCV window); stop with Ctrl+C.
44
+
45
+ Package names vary by distro (e.g. `v4l2loopback-dkms` on Debian/Ubuntu).
46
+
47
+ ### Other platforms
48
+
49
+ On Windows and macOS, [pyvirtualcam](https://github.com/letmaik/pyvirtualcam) may use OBS Virtual Camera or other backends when available.
@@ -0,0 +1,107 @@
1
+ # Laughing Man — software architecture
2
+
3
+ This document describes the high-level structure of the **laughing-man** project: a Python CLI that reads a webcam, detects a face, composites a “Laughing Man” style overlay, optionally blurs the frame when no face is visible, and can mirror the result to a **virtual camera** (e.g. v4l2loopback on Linux) or an OpenCV preview window.
4
+
5
+ ## Technology stack
6
+
7
+ | Layer | Role |
8
+ |--------|------|
9
+ | **Typer** | CLI (`main` command, options, validation). |
10
+ | **OpenCV** | Webcam capture, BGR frames, optional YuNet detector, preview window, compositing helpers. |
11
+ | **MediaPipe Tasks** | BlazeFace short/full-range TFLite models in **VIDEO** mode. |
12
+ | **Pillow (PIL)** | Overlay artwork, alpha compositing, pre-rotated overlay cache. |
13
+ | **pyvirtualcam** | Virtual webcam output (BGR), including Linux v4l2loopback. |
14
+ | **loguru** | Logging; configured from the CLI `--debug` flag. |
15
+
16
+ Bundled default artwork lives under `src/laughing_man/assets/` (`limg.png`, `ltext.png`). BlazeFace and YuNet model files are resolved and optionally downloaded under the XDG cache directory (see `model.py`).
17
+
18
+ ## Repository layout
19
+
20
+ | Path | Purpose |
21
+ |------|---------|
22
+ | `src/laughing_man/` | Application package — all runtime code. |
23
+ | `src/laughing_man/assets/` | Default PNG overlay layers shipped with the wheel. |
24
+ | `docs/` | Design notes and proposals (e.g. `proposals/`). |
25
+ | `scripts/` | Standalone utilities (e.g. image prep), not part of the installed CLI. |
26
+ | `pyproject.toml` | Project metadata, dependencies, console script `laughing-man`. |
27
+
28
+ ## Control flow (runtime)
29
+
30
+ 1. **Entry** — `laughing_man:app` (see `[project.scripts]` in `pyproject.toml`) loads `laughing_man/__init__.py`, which calls `bootstrap.apply_runtime_env()` once (TensorFlow/OpenCV/Qt log noise, Linux Qt hints), then exposes the Typer `app` from `cli.py`.
31
+ 2. **CLI** — `cli.main()` configures logging (`logging_setup.configure_logging`) and calls `run.run_overlay(...)` with parsed options.
32
+ 3. **Orchestration** — `run.run_overlay()` opens the webcam (`camera.open_webcam`), loads overlay images (`overlay.load_overlay_images`), ensures detector models exist (`model.ensure_*`), builds a **FaceBoxSource** (BlazeFace or YuNet, optionally wrapped for cascade), starts a **background thread** to prefill the rotated overlay cache, then runs the **capture loop**: read frame → `face_source.face_box(...)` → `roi.smooth_and_draw(...)` → optional `pyvirtualcam` send and/or OpenCV preview.
33
+
34
+ ```mermaid
35
+ flowchart LR
36
+ subgraph entry [Entry]
37
+ Init["__init__.py\nbootstrap + app"]
38
+ CLI["cli.py\nTyper"]
39
+ end
40
+ subgraph core [Pipeline]
41
+ Run["run.py\nrun_overlay"]
42
+ Cam["camera.py"]
43
+ Det["FaceBoxSource\ndetection / cascade"]
44
+ ROI["roi.py\nsmooth_and_draw"]
45
+ VC["pyvirtualcam\noptional"]
46
+ Prev["OpenCV preview\noptional"]
47
+ end
48
+ Init --> CLI
49
+ CLI --> Run
50
+ Run --> Cam
51
+ Run --> Det
52
+ Run --> ROI
53
+ ROI --> VC
54
+ ROI --> Prev
55
+ ```
56
+
57
+ ## Module map (`src/laughing_man/`)
58
+
59
+ Use this table to find responsibilities and jump to the right file.
60
+
61
+ | Module | Responsibility |
62
+ |--------|----------------|
63
+ | **`__init__.py`** | Applies `bootstrap` before other imports; exports Typer `app`. |
64
+ | **`bootstrap.py`** | Process environment defaults (`TF_CPP_MIN_LOG_LEVEL`, Qt on Linux, etc.). |
65
+ | **`cli.py`** | Typer application: flags for detector, ROI tuning, virtual camera, overlay image, `--debug`. |
66
+ | **`run.py`** | Main orchestration: webcam, model setup, overlay cache prefill thread, capture loop, virtual camera lifecycle. |
67
+ | **`constants.py`** | Tunables: camera index, detection thresholds, ROI/overlay factors, key codes for tuning, default lambdas. |
68
+ | **`protocols.py`** | Structural typing: `FaceBoxSource` (raw `(x,y,w,h)` per frame), `PrivacyEffect` (full-frame effect when privacy engages). |
69
+ | **`deps.py`** | `PipelineDeps` — injectable implementations (e.g. privacy backend) for a run. |
70
+ | **`camera.py`** | `open_webcam` and user-facing error messages when capture fails. |
71
+ | **`model.py`** | BlazeFace / YuNet path resolution, cache dir, optional HTTP download. |
72
+ | **`detection.py`** | BlazeFace: `FaceDetectorOptions`, `mediapipe_detect_face`, `BlazeFaceFaceBoxSource`. |
73
+ | **`yunet_face.py`** | OpenCV `FaceDetectorYN` (YuNet): `create_yunet_detector`, `YuNetFaceBoxSource`. |
74
+ | **`cascade.py`** | `CascadedFaceBoxSource` — expands the previous smoothed ROI and runs the inner detector on a crop first (YuNet-oriented); crop/box math helpers. |
75
+ | **`roi.py`** | `RoiState`, temporal smoothing (**EMA**, **Kalman**, **Kalman + optical flow**), compositing the square overlay onto the face ROI, privacy debounce and `PrivacyEffect` application. |
76
+ | **`box_tracking.py`** | `BoxKalman` (OpenCV Kalman on center/size) and optical-flow helper used by Kalman-flow ROI mode. |
77
+ | **`overlay.py`** | Load bundled or custom overlay images; `build_rotated_overlay_frame`; square resize for the cache. |
78
+ | **`privacy.py`** | `GaussianBlurPrivacy` — full-frame blur blended by strength. |
79
+ | **`tuning.py`** | Interactive adjustment of `roi_lambda` / `size_lambda` via OpenCV keys or stdin (when TTY available). |
80
+ | **`logging_setup.py`** | Single stderr sink for loguru; level from `--debug`. |
81
+
82
+ **Face detection is intentionally pluggable:** anything implementing `FaceBoxSource` can supply `(x, y, w, h)` or `None`. BlazeFace uses MediaPipe VIDEO timestamps; YuNet ignores timestamps; cascade wraps another `FaceBoxSource` and uses shared `RoiState` from `smooth_and_draw`.
83
+
84
+ ## Data flow (one frame)
85
+
86
+ 1. **Input** — BGR `numpy` array from `VideoCapture.read()`.
87
+ 2. **Detection** — Selected backend returns the largest face box above minimum size (see `constants.MIN_FACE_SIZE`), or `None`.
88
+ 3. **Stabilization** — `roi.smooth_and_draw` updates `RoiState` using the chosen motion model, applies optional EMA on center/size, and composites the **pre-sized** RGB overlay and mask onto the face region.
89
+ 4. **Privacy** — After a streak of no-face frames, the pipeline increases blur via `PipelineDeps.privacy` (`GaussianBlurPrivacy` by default).
90
+ 5. **Output** — The same BGR frame is sent to the virtual camera (if enabled) and/or shown in a named OpenCV window; the rotating overlay angle advances each frame for the stock two-layer artwork.
91
+
92
+ ## External dependencies (conceptual)
93
+
94
+ - **Webcam** — System video device (index from `constants.CAMERA_INDEX`, typically `0`).
95
+ - **Virtual camera** — OS-specific: on Linux, **v4l2loopback** provides a `/dev/video*` sink that `pyvirtualcam` writes to; see `README.md` for `modprobe` examples.
96
+ - **Models** — BlazeFace TFLite (short vs full range) and YuNet ONNX URLs/paths are centralized in `constants.py` / `model.py`; overrides via environment variables where documented in code.
97
+
98
+ ## Related documentation
99
+
100
+ - **`README.md`** — Install, run, virtual webcam setup.
101
+ - **`docs/proposals/`** — Exploratory design write-ups (not necessarily implemented).
102
+
103
+ ## Extension points (for contributors)
104
+
105
+ - **New detector** — Implement `FaceBoxSource` in a new module and wire it in `run.py` similarly to `BlazeFaceFaceBoxSource` / `YuNetFaceBoxSource`.
106
+ - **New privacy effect** — Implement `PrivacyEffect` and pass it in `PipelineDeps` (today `run.py` constructs `GaussianBlurPrivacy()` directly; wiring could be generalized if needed).
107
+ - **ROI behavior** — `roi.smooth_and_draw` and `RoiState` encapsulate motion models; `box_tracking.py` holds Kalman/optical-flow primitives.