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.
- laughing_man-1.2.0/.envrc +6 -0
- laughing_man-1.2.0/.github/workflows/ci.yml +46 -0
- laughing_man-1.2.0/.github/workflows/pypi-publish.yml +33 -0
- laughing_man-1.2.0/.github/workflows/release.yml +142 -0
- laughing_man-1.2.0/.gitignore +19 -0
- laughing_man-1.2.0/.python-version +1 -0
- laughing_man-1.2.0/AGENTS.md +78 -0
- laughing_man-1.2.0/CHANGELOG.md +30 -0
- laughing_man-1.2.0/PKG-INFO +11 -0
- laughing_man-1.2.0/README.md +49 -0
- laughing_man-1.2.0/docs/ARCHITECTURE.md +107 -0
- laughing_man-1.2.0/docs/proposals/face-detection-multi-angle-proposal.md +256 -0
- laughing_man-1.2.0/pyproject.toml +77 -0
- laughing_man-1.2.0/scripts/magenta_to_alpha.py +342 -0
- laughing_man-1.2.0/src/laughing_man/__init__.py +16 -0
- laughing_man-1.2.0/src/laughing_man/__version__.py +3 -0
- laughing_man-1.2.0/src/laughing_man/assets/__init__.py +1 -0
- laughing_man-1.2.0/src/laughing_man/assets/limg.png +0 -0
- laughing_man-1.2.0/src/laughing_man/assets/ltext.png +0 -0
- laughing_man-1.2.0/src/laughing_man/bootstrap.py +28 -0
- laughing_man-1.2.0/src/laughing_man/box_tracking.py +160 -0
- laughing_man-1.2.0/src/laughing_man/camera.py +77 -0
- laughing_man-1.2.0/src/laughing_man/cascade.py +174 -0
- laughing_man-1.2.0/src/laughing_man/cli.py +117 -0
- laughing_man-1.2.0/src/laughing_man/cli_options.py +277 -0
- laughing_man-1.2.0/src/laughing_man/constants.py +66 -0
- laughing_man-1.2.0/src/laughing_man/deps.py +21 -0
- laughing_man-1.2.0/src/laughing_man/detection.py +93 -0
- laughing_man-1.2.0/src/laughing_man/logging_setup.py +20 -0
- laughing_man-1.2.0/src/laughing_man/model.py +111 -0
- laughing_man-1.2.0/src/laughing_man/overlay.py +247 -0
- laughing_man-1.2.0/src/laughing_man/postprocess.py +358 -0
- laughing_man-1.2.0/src/laughing_man/privacy.py +63 -0
- laughing_man-1.2.0/src/laughing_man/protocols.py +44 -0
- laughing_man-1.2.0/src/laughing_man/roi.py +324 -0
- laughing_man-1.2.0/src/laughing_man/run.py +399 -0
- laughing_man-1.2.0/src/laughing_man/tuning.py +205 -0
- laughing_man-1.2.0/src/laughing_man/yunet_face.py +108 -0
- laughing_man-1.2.0/tests/test_box_tracking.py +30 -0
- laughing_man-1.2.0/tests/test_cascade.py +36 -0
- laughing_man-1.2.0/tests/test_cascaded_source.py +38 -0
- laughing_man-1.2.0/tests/test_cli.py +26 -0
- laughing_man-1.2.0/tests/test_postprocess.py +39 -0
- laughing_man-1.2.0/tests/test_version.py +33 -0
- laughing_man-1.2.0/uv.lock +1703 -0
|
@@ -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.
|