zrb-face 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.
@@ -0,0 +1,7 @@
1
+ __pycache__/
2
+ app/node_modules/
3
+ app/dist/
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ *.vrm
@@ -0,0 +1,279 @@
1
+ Metadata-Version: 2.5
2
+ Name: zrb-face
3
+ Version: 0.1.0
4
+ Summary: A 3D character for zrb in a small, see-through, always-on-top window
5
+ Project-URL: Homepage, https://github.com/state-alchemists/zrb-face
6
+ Requires-Python: >=3.10
7
+ Provides-Extra: zrb
8
+ Requires-Dist: zrb; extra == 'zrb'
9
+ Description-Content-Type: text/markdown
10
+
11
+ # zrb-face
12
+
13
+ A companion for [zrb](https://github.com/state-alchemists/zrb): a 3D character in a small, see-through, always-on-top window. She idles naturally (breathing, shifting her weight, glancing around, stretching), shows what zrb is doing and how it feels, moves her mouth with zrb's voice, dances while you play music, gestures with your own animations, and can start a conversation herself.
14
+
15
+ ## Quick start
16
+
17
+ ```sh
18
+ pipx install zrb-face # or: pip install zrb-face, next to zrb; from a checkout: pip install -e .
19
+
20
+ # once, e.g. in ~/.zshrc: load the zrb side into every zrb chat
21
+ export ZRB_INIT_SCRIPTS="$ZRB_INIT_SCRIPTS:$(zrb-face init-path)"
22
+
23
+ mkdir -p ~/.config/zrb-face/models && cp my.vrm ~/.config/zrb-face/models/ # a model: see Changing the model
24
+ zrb-face my # (re)start the server, open a window with my.vrm
25
+ zrb chat
26
+ ```
27
+
28
+ The window is a small desktop app; download it from the [releases](https://github.com/state-alchemists/zrb-face/releases) or build it (see [The desktop app](#the-desktop-app)). Without it, `zrb-face` opens the page in your browser instead.
29
+
30
+ `zrb-face` opens the Windows build from WSL, the installed app on macOS, or Electron from `app/` in a source checkout (`ZRB_FACE_APP` overrides the program). It restarts the server each time, so the server always runs the current code with your current settings; open windows reload by themselves. The server keeps running across chats; its output goes to `zrb-face.log` in the temp folder, and `pkill -f "[z]rb_face/server.py"` stops it. The order doesn't matter: the window waits for the server, reloads itself when the server restarts, and zrb skips sending while either is missing. If a model can't be loaded, the window says so in red and tries again every few seconds.
31
+
32
+ ## How it fits together
33
+
34
+ ```
35
+ zrb chat ── POST /event ──▶ zrb-face server ── page, models, animations, live events ──▶ window (face.html)
36
+ + zrb_init.py (port 7777) in the app or a browser
37
+ ```
38
+
39
+ | File | Role |
40
+ | --- | --- |
41
+ | `zrb_face/zrb_init.py` | The zrb side, loaded through `ZRB_INIT_SCRIPTS`: zrb's speech (moving the mouth, silent for "nothing to say"), the face hooks, the `FaceAction` tool, and the optional triggers that let her speak first. |
42
+ | `zrb_face/client.py` | Sends events to the server: state and emotion from zrb's hooks, mouth openness from the loudness of the speech audio. Skips sending when the server is down. |
43
+ | `zrb_face/server.py` | The server (Python standard library only): serves the page, the models and the animations from their folders, and relays every event to the open windows over Server-Sent Events. |
44
+ | `zrb_face/face.html` | Everything visible, with three.js and three-vrm: loads the model, plays the animations, blinks, lip sync, eye contact, smiles, emotions, the camera. |
45
+ | `zrb_face/__main__.py` | The `zrb-face` command. |
46
+ | `zrb_face/anim/` | The animations that ship with the package. |
47
+ | `app/` | The desktop window: an Electron shell (`main.js`) that shows the page see-through and always on top. |
48
+ | `make_idle.py` | Generates the idle and dance animations in `zrb_face/anim/`. |
49
+ | `test_face.py` | The self-check. |
50
+
51
+ zrb runs in WSL on Windows, but a window that stays on top has to be a Windows program. Windows can reach a server in WSL (not the other way around in WSL's default NAT mode), so the server runs next to zrb and the window connects to it.
52
+
53
+ ## Running by hand
54
+
55
+ What `zrb-face` does, per platform:
56
+
57
+ ```sh
58
+ zrb-face serve & # the server
59
+ /mnt/c/Users/$USER/AppData/Local/zrb-face/zrb-face.exe rem </dev/null &>/dev/null &! # Windows, from WSL
60
+ open -n -a zrb-face --args rem # macOS, installed app
61
+ ~/zrb-face/app/node_modules/.bin/electron ~/zrb-face/app rem # Linux, or any OS from source
62
+ ```
63
+
64
+ The window takes a model name (`rem` or `rem.vrm`; without one, the first model alphabetically) and optionally another server URL (`http://localhost:7779`). Start the Windows build detached as above, or zsh stops it ("suspended (tty input)"). Instead of the app you can open `http://localhost:7777/?model=rem` in a browser: the same page, but not see-through or always on top.
65
+
66
+ ## Using the window
67
+
68
+ - **Move**: drag it. **Close**: Alt+F4 on Windows, Cmd+Q on macOS.
69
+ - **Camera angle**: click the window, then use the keys in [Keyboard](#keyboard-click-the-window-first). The angle is remembered.
70
+
71
+ ## What she shows
72
+
73
+ | Glow | State | Sent by |
74
+ | --- | --- | --- |
75
+ | cyan | idle | `Stop`, `StopFailure`, `SessionStart` |
76
+ | violet (head tilts) | thinking | `UserPromptSubmit`, `PostToolUse`, `PostToolUseFailure` |
77
+ | green | running a tool | `PreToolUse` |
78
+ | red | needs attention | `Notification`, `PermissionRequest` |
79
+ | bright cyan | speaking | mouth events |
80
+
81
+ - **Voice**: the mouth follows the loudness of the speech, in 40 ms steps, and she nods slightly while speaking. It needs a voice zrb plays itself (piper, openai, gemini); `say` and `termux` play the audio themselves, so zrb never sees it. The mouth starts slightly ahead of the voice, because zrb reads audio ahead of playing it; fixing that exactly needs a frame observer in zrb's `PcmUtterance._fill`.
82
+ - **Face**: she blinks every few seconds, her eyes follow you (the camera), and she smiles softly every 25–50 seconds and after finishing a task.
83
+ - **Body**: each state loops its own animations, falling back to the idles; see [Animations](#animations). On `SessionStart` she plays the `greeting` action if there is one.
84
+ - **Emotion**: with zrb's emotion on (`ZRB_LLM_EMOTION_ENABLED=true`), zrb opens each reply with an emoji, and she shows it for 6 s with the matching VRM expression (`happy`, `sad`, `angry`, `surprised`, `relaxed`), and plays the action of the same name if there is one (`actions/happy.vrma`). The user's own emotion isn't shown: her reply answers it.
85
+ - **Gestures**: with animations in `actions/`, zrb gets a `FaceAction` tool and plays them itself, alongside its words (wave, nod, bow, …).
86
+ - **Music**: when something plays music (a browser, Spotify, …) she dances with the `dance/` animations while idle; anything zrb does takes over, and she goes back to dancing after. The server watches the system's media status: Windows' media controls (also from WSL) or `playerctl` on Linux. Any playing media counts, a talk video too. On macOS, or to override it, use `zrb-face music on` / `off`; `ZRB_FACE_MUSIC=off` stops the watching.
87
+ - **Talking first** (off by default): with `ZRB_FACE_TALK_FIRST=idle` she speaks up after you've been quiet for a while during waking hours (8:00–22:00); with `camera`, when you sit back down after being away. She never interrupts a running turn or her own speech, waits a cooldown between such turns, and replies `…` (shown, not spoken) when there's nothing worth saying.
88
+
89
+ The mouth, the blink and the smile need the model's `aa`, `blink` and `relaxed` expressions. A model without them still nods while speaking.
90
+
91
+ ## Wiring it into zrb
92
+
93
+ `zrb_face/zrb_init.py` is all zrb needs; load it through `ZRB_INIT_SCRIPTS` as in [Quick start](#quick-start) (`zrb-face init-path` prints where it is). It owns zrb's speech (`enable_speech`), so other init scripts should only choose the voice (`CFG.DEFAULT_LLM_SPEECH_BACKEND`, `CFG.DEFAULT_LLM_SPEECH_VOICE`) and not call `enable_speech` themselves; whichever script calls it last would win.
94
+
95
+ ## Configuration
96
+
97
+ ### Environment variables
98
+
99
+ The zrb-side names follow zrb's `CFG.ENV_PREFIX` (`ZRB` by default). The server and the `zrb-face` command run without zrb, so their names are fixed.
100
+
101
+ | Variable | Default | Read by | What it does |
102
+ | --- | --- | --- | --- |
103
+ | `ZRB_FACE_TALK_FIRST` | empty (never) | `zrb_init.py` | What may start a conversation: `idle`, `camera`, or `idle,camera`. |
104
+ | `ZRB_FACE_IDLE_MINUTES` | `45` | `zrb_init.py` | Quiet minutes before `idle` speaks. |
105
+ | `ZRB_FACE_COOLDOWN` | `1200` | `zrb_init.py` | Seconds between conversations she starts herself. |
106
+ | `ZRB_FACE_CAMERA_INTERVAL` | `20` | `zrb_init.py` | Seconds between camera looks; each one lights the camera. |
107
+ | `ZRB_FACE_CAMERA_PRESENT_BYTES` | `25000` | `zrb_init.py` | Photo size above which someone counts as present; tune it for your camera and room. |
108
+ | `ZRB_FACE_MOUTH_GAIN` | `4` | `client.py` | How wide the mouth opens for a given loudness: raise it if the mouth barely opens, lower it if it stays wide open. |
109
+ | `ZRB_FACE_URL` | `http://127.0.0.1:7777/event` | `client.py` | Where zrb sends events. |
110
+ | `ZRB_FACE_HOST` | `127.0.0.1` | server | The address the server listens on. |
111
+ | `ZRB_FACE_PORT` | `7777` | server, `zrb-face` | The server's port (change `ZRB_FACE_URL` to match). |
112
+ | `ZRB_FACE_HOME` | `~/.config/zrb-face` | server, `zrb_init.py` | Your folder: models in `models/`, animations in `anim/`. |
113
+ | `ZRB_FACE_MODELS` | empty | server | More model folders, searched before `<home>/models`; separated by `:` (`;` on Windows). `~` is expanded. |
114
+ | `ZRB_FACE_ANIMS` | empty | server, `zrb_init.py` | More animation folders, searched before `<home>/anim` and the shipped ones. |
115
+ | `ZRB_FACE_MUSIC` | `auto` | server | `auto` dances while media plays; `off` only on `zrb-face music on`. |
116
+ | `ZRB_FACE_APP` | per platform | `zrb-face` | The program to open as the window, instead of the Windows build, the macOS app or Electron from source. |
117
+
118
+ ### zrb settings it depends on
119
+
120
+ | Setting | Why it matters |
121
+ | --- | --- |
122
+ | `ZRB_INIT_SCRIPTS` | Must include `$(zrb-face init-path)`; that's what turns the face on. |
123
+ | `ZRB_LLM_EMOTION_ENABLED` | `true` makes zrb tag replies with an emotion, which she shows. Off by default. Needs a zrb with the `Emotion` hook. |
124
+ | `ZRB_LLM_SPEECH_BACKEND` / `ZRB_LLM_SPEECH_VOICE` (or `CFG.DEFAULT_LLM_SPEECH_BACKEND` / `_VOICE` in an init script) | The voice. For the mouth to move it must be one zrb plays itself (piper, openai, gemini), not `say` or `termux`. |
125
+ | `ZRB_LLM_SPEECH_PLAYER` | Leave it at `auto` (the default), with zrb's voice extra installed (`zrb[voice]`, for sounddevice): then zrb plays the audio itself and the mouth can follow it. `command` hands the audio to a player program, hidden from zrb. |
126
+ | `ZRB_HOOKS_ENABLED` | Must stay on (the default): the states and speech ride on zrb's hooks. |
127
+ | zrb's camera settings (`ZRB_LLM_CAMERA_*`) | Used by `ZRB_FACE_TALK_FIRST=camera` to take its photos. |
128
+
129
+ ### Command line and URL
130
+
131
+ | Where | Option | Example | What it does |
132
+ | --- | --- | --- | --- |
133
+ | `zrb-face` | model name | `zrb-face rem` | (Re)starts the server and opens a window with that model; without a name, the first model alphabetically. |
134
+ | `zrb-face serve` | | | Runs the server in the foreground. |
135
+ | `zrb-face action <name>` | | `zrb-face action wave` | Plays `actions/<name>` once; handy for trying a new clip. |
136
+ | `zrb-face music on\|off` | | `zrb-face music on` | Dance while idle, or stop. |
137
+ | `zrb-face dirs` | | | Lists the model and animation folders in search order, and what they hold. |
138
+ | `zrb-face init-path` | | | Prints the path of `zrb_init.py`, for `ZRB_INIT_SCRIPTS`. |
139
+ | `zrb-face.exe`, the macOS app, `electron app` | model name | `zrb-face.exe rem` or `rem.vrm` | The model to show (letters, digits, `-`, `_`; `.vrm` optional). |
140
+ | same | server URL | `zrb-face.exe http://localhost:7779` | Another server than `http://localhost:7777`. |
141
+ | the page | `?model=<name>` | `http://localhost:7777/?model=rem` | The model, in a browser. |
142
+ | the page | `?app` | (set by the app) | See-through background, dragging moves the window, no status text. |
143
+
144
+ ### Keyboard (click the window first)
145
+
146
+ | Key | What it does |
147
+ | --- | --- |
148
+ | ← → | Orbit around her. |
149
+ | ↑ ↓ | Look from higher or lower. |
150
+ | `+` / `-` | Zoom in or out (0.5× to 4×). |
151
+ | `0` | Reset the camera. |
152
+
153
+ The camera angle is saved in the window's storage and comes back next time.
154
+
155
+ ### Files and folders
156
+
157
+ Models and animations are looked up in folders, in this order; the first file with a given name wins, so your own files override the shipped ones. `zrb-face dirs` shows the list.
158
+
159
+ | Kind | Folders |
160
+ | --- | --- |
161
+ | models | `$ZRB_FACE_MODELS`, `~/.config/zrb-face/models` |
162
+ | animations | `$ZRB_FACE_ANIMS`, `~/.config/zrb-face/anim`, the package's `anim/` |
163
+
164
+ | Path | Convention |
165
+ | --- | --- |
166
+ | `<models folder>/<name>.vrm` | A model, opened by `<name>`. Only files directly in the folder are served. |
167
+ | `<anim folder>/<pool>/<name>.vrma` | An animation; the pool (a subfolder) says when it plays. See [Animations](#animations). |
168
+
169
+ ### In the code
170
+
171
+ Constants with no setting of their own; edit the file to change them.
172
+
173
+ | File | Constant | Value | What it does |
174
+ | --- | --- | --- | --- |
175
+ | `face.html` | `FRAMING` | `'body'` | `'body'` frames her head to toe (with room for the reach of every looping clip), `'face'` head and shoulders. |
176
+ | `face.html` | `LOOK` | cyan, violet, green, red, bright cyan | The glow colour per state: idle, thinking, tool, attention, speaking. |
177
+ | `face.html` | blinking | every 2–6 s, 0.12 s long | How often and how long she blinks. |
178
+ | `face.html` | smile | every 25–50 s for 3 s at 0.5; 2 s after a task | The soft `relaxed` smile. |
179
+ | `face.html` | thinking tilt / speaking nod | 0.12 / 0.04 rad | How far the head tilts while thinking and nods with the voice. |
180
+ | `face.html` | crossfade | 0.6 s (actions 0.3 s) | Blend time between animations. |
181
+ | `client.py` | `MOUTH_STEP` | 0.04 s | Audio per mouth value. |
182
+ | `zrb_init.py` | `AWAKE_HOURS` | 8:00–21:59 | When she may speak first. |
183
+ | `zrb_init.py` | idle check / away time | every 60 s / 5 min | How often `idle` checks; how long you must be away before `camera` greets you back. |
184
+ | `zrb_init.py` | `NOTHING_TO_SAY` | `…` | The reply she gives, unspoken, when nothing is worth saying. |
185
+ | `app/main.js` | window | 440×640, bottom right | Size and place of the window (320×420 suits `'face'` framing). |
186
+ | `app/main.js` | on top | `screen-saver` level | Stays above full-screen windows too. |
187
+ | `face.html` | emotion | 0.6 for 6 s | How strong and how long she shows an emotion. |
188
+ | `make_idle.py` | `CLIPS`, `DANCES` | 8 idles, 4 dances | The animations as eased keyframes of poses; run the script to regenerate `zrb_face/anim/idle/` and `dance/`. |
189
+ | `make_idle.py` | `FPS` | 15 | Keyframes per second in the generated clips. |
190
+
191
+ ## Changing the model
192
+
193
+ Put any VRM file (VRM 0.x or 1.0) in `~/.config/zrb-face/models/` (or a folder in `ZRB_FACE_MODELS`) as `<name>.vrm` and open it by name: `zrb-face <name>`. Without a name, the window shows the first model alphabetically. No model ships with the package. Model files are kept out of git (`*.vrm` is ignored). Nothing in the code depends on the model: the camera fits it, and the pose and idle come from the animations, which three-vrm retargets onto any VRM, as VRoid Hub's playground does.
194
+
195
+ Where to get models:
196
+
197
+ - **[VRoid Studio](https://vroid.com/en/studio)**: free, for Windows and macOS. Make your own and export it as VRM. You own it, and it has every expression.
198
+ - **[VRoid Hub](https://hub.vroid.com/en)**: thousands of models. Only the ones marked downloadable can be downloaded, and each has its own conditions (avatar use, commercial use, redistribution, changes). Read them before you change or share the model.
199
+ - **[BOOTH](https://booth.pm/en/search/VRM)**: search for `VRM` and filter for free items. The terms are on each item's page, often only in Japanese.
200
+ - **[Open Source Avatars](https://github.com/toxsam/open-source-avatars)**: free VRM avatars with direct download links. The license is set per collection (CC0, CC-BY, …), so check it.
201
+ - **[OpenGameArt: VRoid Studio CC0 models](https://opengameart.org/content/vroid-studio-cc0-models)**: public domain sample models from early VRoid Studio.
202
+
203
+ Fan models of existing characters (Cortana, Kusanagi, and others) are fine on your own desktop, but don't commit or share them.
204
+
205
+ ## Animations
206
+
207
+ Every `<pool>/<name>.vrma` (VRM Animation) in the animation folders loads when the page starts. The pool, the subfolder, says when it plays:
208
+
209
+ | Pool | Plays |
210
+ | --- | --- |
211
+ | `idle/` | Loops while idle, and for every state with no pool of its own. |
212
+ | `thinking/`, `tool/`, `attention/`, `speaking/` | Loops in that state. None ship; add your own. |
213
+ | `dance/` | Loops instead of `idle/` while music plays. |
214
+ | `actions/` | Once, on `{"type": "action", "value": "<name>"}`, then back to looping. zrb plays them through its `FaceAction` tool; `greeting` plays when a chat starts, and `happy`, `sad`, … with that emotion. |
215
+
216
+ Any other pool loads too, and plays once something asks for it.
217
+
218
+ To add your own, drop the file in, e.g. `~/.config/zrb-face/anim/actions/wave.vrma`, and restart with `zrb-face` (and zrb, so the `FaceAction` tool knows the new name). A file named like a shipped one replaces it: `~/.config/zrb-face/anim/idle/rest.vrma`. Try an action with `zrb-face action wave`.
219
+
220
+ Within a pool, clips play in random order with crossfades, picked by group: the first part of the name (`stretch-side` belongs to `stretch`). A random group, then a random clip in it, so the stretches together come up as often as any single other idle. On top of whatever plays: the head tilts while thinking, nods while speaking, the eyes follow you, and the face shows zrb's emotion.
221
+
222
+ The idles and dances are ours (CC0), generated by `python3 make_idle.py`. The dances are 8-beat loops at 120 BPM: `bounce` (down on every beat, arms pumping), `sway` (side to side every two beats), `step-touch` (stepping side to side, hands up on the touch) and `hands-up` (arms high, waving). They don't follow the song's tempo yet. The idles:
223
+
224
+ | Clip | What she does |
225
+ | --- | --- |
226
+ | `rest` | Relaxed, a slow weight shift. |
227
+ | `hands` | Hands clasped in front, swaying; the free foot twists on its toes. |
228
+ | `weight` | Shifts from hip to hip with the free knee bent, taking a small step each time. |
229
+ | `look` | Glances left and right. |
230
+ | `stretch` | The yawn-stretch: hands rise past the face, then up and out into a Y, shoulders shrugged, back arched, up on the toes, eyes closed, yawning; then a slow release, shoulders dropping. |
231
+ | `stretch-side` | One arm arcs over her head, the other hand slides down the thigh, the hips shift the other way; each side. |
232
+ | `stretch-neck` | Ear to each shoulder, then chin down; shoulders dropped, eyes closed. |
233
+ | `stretch-twist` | Turns from the hips up, the head leading, the arms swinging loosely, the back heel lifting. |
234
+
235
+ Every clip also has breathing, uneven head drift, knees that flex a little, and fingers that relax and tighten. The hands are loosely curled, the arms hang a little away from the body, and the weight shifts move the hips over the standing leg. The motion follows VRoid's own VRMA clips (every joint moving, fingers included), common VTuber idle loops (breathing, sway, head pitch, roll and yaw), and how people actually stretch (the whole-body yawn-stretch; a side bend with the hips shifting the other way).
236
+
237
+ To change or add idles, edit `CLIPS` in `make_idle.py` (eased keyframes of poses) and run it again. Its docstring lists which way each joint turns. Poses can also move the hips (`hipsMove`) and set the `blink` and `aa` expressions; the page takes the larger of the clip's value and its own blinking and lip sync.
238
+
239
+ More free VRMA:
240
+
241
+ - **VRoid's official 7-motion pack** (show full body, greeting, peace sign, shoot, spin, model pose, squat), free on [BOOTH](https://vroid.com/en/news/6HozzBIV0KkcKf9dc1fZGW). Free to use, commercial use with the credit "Animation credits to pixiv Inc.'s VRoid Project", but its terms forbid redistributing the files, so keep them out of git. E.g. `cp VRMA_02.vrma ~/.config/zrb-face/anim/actions/greeting.vrma`.
242
+ - **Dances**: motion capture through bvh2vrma, e.g. the [AIST++](https://google.github.io/aistplusplus_dataset/) dance dataset (CC BY 4.0, credit needed) or the [CMU](http://mocap.cs.cmu.edu/) library (free). Mixamo's dances can be converted too, but its terms forbid sharing the files, so keep them in your own folder.
243
+ - **Your own**: [bvh2vrma](https://vrm-c.github.io/bvh2vrma/) (VRM Consortium, MIT) converts BVH motion capture to VRMA, and VRoid Studio exports poses and animations as VRMA.
244
+
245
+ ## The desktop app
246
+
247
+ Each [release](https://github.com/state-alchemists/zrb-face/releases) has a zip per platform:
248
+
249
+ - **Windows** (`zrb-face-win32-x64.zip`): unzip into `%LOCALAPPDATA%\zrb-face\`, so `zrb-face` finds `zrb-face.exe` there from WSL.
250
+ - **macOS** (`zrb-face-darwin-arm64.zip` for Apple silicon, `-x64` for Intel): move `zrb-face.app` to `/Applications`. The app isn't signed, so clear macOS's block once with `xattr -dr com.apple.quarantine /Applications/zrb-face.app`.
251
+ - **Linux** (`zrb-face-linux-x64.zip`): unzip anywhere and set `ZRB_FACE_APP` to its `zrb-face` program. On Wayland (GNOME), a window can't keep itself on top, so pin it by hand.
252
+
253
+ To build it yourself:
254
+
255
+ ```sh
256
+ cd app
257
+ npm install
258
+ npm start # run from source
259
+ npm run pack:win # or pack:mac / pack:linux -> dist/
260
+ cp -r dist/zrb-face-win32-x64 /mnt/c/Users/$USER/AppData/Local/zrb-face
261
+ ```
262
+
263
+ If GitHub downloads are slow, prefix the build with `ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/`, then check the zip in `~/.cache/electron` against GitHub's `SHASUMS256.txt`.
264
+
265
+ ## Test
266
+
267
+ ```sh
268
+ python3 test_face.py # with a Python that has zrb installed
269
+ ```
270
+
271
+ ## Releasing
272
+
273
+ Bump the version in `pyproject.toml` and `app/package.json`, then push a tag: `git tag 0.1.0 && git push --tags`. `.github/workflows/release.yml` publishes the package to PyPI and attaches the app zips to the GitHub release. PyPI publishing is trusted publishing, so first add this repository, the workflow `release.yml` and the environment `pypi` as a trusted publisher of `zrb-face` on pypi.org.
274
+
275
+ ## Credits
276
+
277
+ `zrb_face/anim/` and `make_idle.py`: ours, CC0.
278
+
279
+ The models used during development, not included: *ram* and *rem made in mexico* (Re:Zero fan models) by pon3mx on [VRoid Hub](https://hub.vroid.com/en); search for them there. Ram: use, changes, commercial use and redistribution allowed, no credit needed. Rem: the same but no commercial use. Both have mouth, blink and emotion expressions. They are fan models of the Re:Zero characters, so keep them on your own machine.
@@ -0,0 +1,269 @@
1
+ # zrb-face
2
+
3
+ A companion for [zrb](https://github.com/state-alchemists/zrb): a 3D character in a small, see-through, always-on-top window. She idles naturally (breathing, shifting her weight, glancing around, stretching), shows what zrb is doing and how it feels, moves her mouth with zrb's voice, dances while you play music, gestures with your own animations, and can start a conversation herself.
4
+
5
+ ## Quick start
6
+
7
+ ```sh
8
+ pipx install zrb-face # or: pip install zrb-face, next to zrb; from a checkout: pip install -e .
9
+
10
+ # once, e.g. in ~/.zshrc: load the zrb side into every zrb chat
11
+ export ZRB_INIT_SCRIPTS="$ZRB_INIT_SCRIPTS:$(zrb-face init-path)"
12
+
13
+ mkdir -p ~/.config/zrb-face/models && cp my.vrm ~/.config/zrb-face/models/ # a model: see Changing the model
14
+ zrb-face my # (re)start the server, open a window with my.vrm
15
+ zrb chat
16
+ ```
17
+
18
+ The window is a small desktop app; download it from the [releases](https://github.com/state-alchemists/zrb-face/releases) or build it (see [The desktop app](#the-desktop-app)). Without it, `zrb-face` opens the page in your browser instead.
19
+
20
+ `zrb-face` opens the Windows build from WSL, the installed app on macOS, or Electron from `app/` in a source checkout (`ZRB_FACE_APP` overrides the program). It restarts the server each time, so the server always runs the current code with your current settings; open windows reload by themselves. The server keeps running across chats; its output goes to `zrb-face.log` in the temp folder, and `pkill -f "[z]rb_face/server.py"` stops it. The order doesn't matter: the window waits for the server, reloads itself when the server restarts, and zrb skips sending while either is missing. If a model can't be loaded, the window says so in red and tries again every few seconds.
21
+
22
+ ## How it fits together
23
+
24
+ ```
25
+ zrb chat ── POST /event ──▶ zrb-face server ── page, models, animations, live events ──▶ window (face.html)
26
+ + zrb_init.py (port 7777) in the app or a browser
27
+ ```
28
+
29
+ | File | Role |
30
+ | --- | --- |
31
+ | `zrb_face/zrb_init.py` | The zrb side, loaded through `ZRB_INIT_SCRIPTS`: zrb's speech (moving the mouth, silent for "nothing to say"), the face hooks, the `FaceAction` tool, and the optional triggers that let her speak first. |
32
+ | `zrb_face/client.py` | Sends events to the server: state and emotion from zrb's hooks, mouth openness from the loudness of the speech audio. Skips sending when the server is down. |
33
+ | `zrb_face/server.py` | The server (Python standard library only): serves the page, the models and the animations from their folders, and relays every event to the open windows over Server-Sent Events. |
34
+ | `zrb_face/face.html` | Everything visible, with three.js and three-vrm: loads the model, plays the animations, blinks, lip sync, eye contact, smiles, emotions, the camera. |
35
+ | `zrb_face/__main__.py` | The `zrb-face` command. |
36
+ | `zrb_face/anim/` | The animations that ship with the package. |
37
+ | `app/` | The desktop window: an Electron shell (`main.js`) that shows the page see-through and always on top. |
38
+ | `make_idle.py` | Generates the idle and dance animations in `zrb_face/anim/`. |
39
+ | `test_face.py` | The self-check. |
40
+
41
+ zrb runs in WSL on Windows, but a window that stays on top has to be a Windows program. Windows can reach a server in WSL (not the other way around in WSL's default NAT mode), so the server runs next to zrb and the window connects to it.
42
+
43
+ ## Running by hand
44
+
45
+ What `zrb-face` does, per platform:
46
+
47
+ ```sh
48
+ zrb-face serve & # the server
49
+ /mnt/c/Users/$USER/AppData/Local/zrb-face/zrb-face.exe rem </dev/null &>/dev/null &! # Windows, from WSL
50
+ open -n -a zrb-face --args rem # macOS, installed app
51
+ ~/zrb-face/app/node_modules/.bin/electron ~/zrb-face/app rem # Linux, or any OS from source
52
+ ```
53
+
54
+ The window takes a model name (`rem` or `rem.vrm`; without one, the first model alphabetically) and optionally another server URL (`http://localhost:7779`). Start the Windows build detached as above, or zsh stops it ("suspended (tty input)"). Instead of the app you can open `http://localhost:7777/?model=rem` in a browser: the same page, but not see-through or always on top.
55
+
56
+ ## Using the window
57
+
58
+ - **Move**: drag it. **Close**: Alt+F4 on Windows, Cmd+Q on macOS.
59
+ - **Camera angle**: click the window, then use the keys in [Keyboard](#keyboard-click-the-window-first). The angle is remembered.
60
+
61
+ ## What she shows
62
+
63
+ | Glow | State | Sent by |
64
+ | --- | --- | --- |
65
+ | cyan | idle | `Stop`, `StopFailure`, `SessionStart` |
66
+ | violet (head tilts) | thinking | `UserPromptSubmit`, `PostToolUse`, `PostToolUseFailure` |
67
+ | green | running a tool | `PreToolUse` |
68
+ | red | needs attention | `Notification`, `PermissionRequest` |
69
+ | bright cyan | speaking | mouth events |
70
+
71
+ - **Voice**: the mouth follows the loudness of the speech, in 40 ms steps, and she nods slightly while speaking. It needs a voice zrb plays itself (piper, openai, gemini); `say` and `termux` play the audio themselves, so zrb never sees it. The mouth starts slightly ahead of the voice, because zrb reads audio ahead of playing it; fixing that exactly needs a frame observer in zrb's `PcmUtterance._fill`.
72
+ - **Face**: she blinks every few seconds, her eyes follow you (the camera), and she smiles softly every 25–50 seconds and after finishing a task.
73
+ - **Body**: each state loops its own animations, falling back to the idles; see [Animations](#animations). On `SessionStart` she plays the `greeting` action if there is one.
74
+ - **Emotion**: with zrb's emotion on (`ZRB_LLM_EMOTION_ENABLED=true`), zrb opens each reply with an emoji, and she shows it for 6 s with the matching VRM expression (`happy`, `sad`, `angry`, `surprised`, `relaxed`), and plays the action of the same name if there is one (`actions/happy.vrma`). The user's own emotion isn't shown: her reply answers it.
75
+ - **Gestures**: with animations in `actions/`, zrb gets a `FaceAction` tool and plays them itself, alongside its words (wave, nod, bow, …).
76
+ - **Music**: when something plays music (a browser, Spotify, …) she dances with the `dance/` animations while idle; anything zrb does takes over, and she goes back to dancing after. The server watches the system's media status: Windows' media controls (also from WSL) or `playerctl` on Linux. Any playing media counts, a talk video too. On macOS, or to override it, use `zrb-face music on` / `off`; `ZRB_FACE_MUSIC=off` stops the watching.
77
+ - **Talking first** (off by default): with `ZRB_FACE_TALK_FIRST=idle` she speaks up after you've been quiet for a while during waking hours (8:00–22:00); with `camera`, when you sit back down after being away. She never interrupts a running turn or her own speech, waits a cooldown between such turns, and replies `…` (shown, not spoken) when there's nothing worth saying.
78
+
79
+ The mouth, the blink and the smile need the model's `aa`, `blink` and `relaxed` expressions. A model without them still nods while speaking.
80
+
81
+ ## Wiring it into zrb
82
+
83
+ `zrb_face/zrb_init.py` is all zrb needs; load it through `ZRB_INIT_SCRIPTS` as in [Quick start](#quick-start) (`zrb-face init-path` prints where it is). It owns zrb's speech (`enable_speech`), so other init scripts should only choose the voice (`CFG.DEFAULT_LLM_SPEECH_BACKEND`, `CFG.DEFAULT_LLM_SPEECH_VOICE`) and not call `enable_speech` themselves; whichever script calls it last would win.
84
+
85
+ ## Configuration
86
+
87
+ ### Environment variables
88
+
89
+ The zrb-side names follow zrb's `CFG.ENV_PREFIX` (`ZRB` by default). The server and the `zrb-face` command run without zrb, so their names are fixed.
90
+
91
+ | Variable | Default | Read by | What it does |
92
+ | --- | --- | --- | --- |
93
+ | `ZRB_FACE_TALK_FIRST` | empty (never) | `zrb_init.py` | What may start a conversation: `idle`, `camera`, or `idle,camera`. |
94
+ | `ZRB_FACE_IDLE_MINUTES` | `45` | `zrb_init.py` | Quiet minutes before `idle` speaks. |
95
+ | `ZRB_FACE_COOLDOWN` | `1200` | `zrb_init.py` | Seconds between conversations she starts herself. |
96
+ | `ZRB_FACE_CAMERA_INTERVAL` | `20` | `zrb_init.py` | Seconds between camera looks; each one lights the camera. |
97
+ | `ZRB_FACE_CAMERA_PRESENT_BYTES` | `25000` | `zrb_init.py` | Photo size above which someone counts as present; tune it for your camera and room. |
98
+ | `ZRB_FACE_MOUTH_GAIN` | `4` | `client.py` | How wide the mouth opens for a given loudness: raise it if the mouth barely opens, lower it if it stays wide open. |
99
+ | `ZRB_FACE_URL` | `http://127.0.0.1:7777/event` | `client.py` | Where zrb sends events. |
100
+ | `ZRB_FACE_HOST` | `127.0.0.1` | server | The address the server listens on. |
101
+ | `ZRB_FACE_PORT` | `7777` | server, `zrb-face` | The server's port (change `ZRB_FACE_URL` to match). |
102
+ | `ZRB_FACE_HOME` | `~/.config/zrb-face` | server, `zrb_init.py` | Your folder: models in `models/`, animations in `anim/`. |
103
+ | `ZRB_FACE_MODELS` | empty | server | More model folders, searched before `<home>/models`; separated by `:` (`;` on Windows). `~` is expanded. |
104
+ | `ZRB_FACE_ANIMS` | empty | server, `zrb_init.py` | More animation folders, searched before `<home>/anim` and the shipped ones. |
105
+ | `ZRB_FACE_MUSIC` | `auto` | server | `auto` dances while media plays; `off` only on `zrb-face music on`. |
106
+ | `ZRB_FACE_APP` | per platform | `zrb-face` | The program to open as the window, instead of the Windows build, the macOS app or Electron from source. |
107
+
108
+ ### zrb settings it depends on
109
+
110
+ | Setting | Why it matters |
111
+ | --- | --- |
112
+ | `ZRB_INIT_SCRIPTS` | Must include `$(zrb-face init-path)`; that's what turns the face on. |
113
+ | `ZRB_LLM_EMOTION_ENABLED` | `true` makes zrb tag replies with an emotion, which she shows. Off by default. Needs a zrb with the `Emotion` hook. |
114
+ | `ZRB_LLM_SPEECH_BACKEND` / `ZRB_LLM_SPEECH_VOICE` (or `CFG.DEFAULT_LLM_SPEECH_BACKEND` / `_VOICE` in an init script) | The voice. For the mouth to move it must be one zrb plays itself (piper, openai, gemini), not `say` or `termux`. |
115
+ | `ZRB_LLM_SPEECH_PLAYER` | Leave it at `auto` (the default), with zrb's voice extra installed (`zrb[voice]`, for sounddevice): then zrb plays the audio itself and the mouth can follow it. `command` hands the audio to a player program, hidden from zrb. |
116
+ | `ZRB_HOOKS_ENABLED` | Must stay on (the default): the states and speech ride on zrb's hooks. |
117
+ | zrb's camera settings (`ZRB_LLM_CAMERA_*`) | Used by `ZRB_FACE_TALK_FIRST=camera` to take its photos. |
118
+
119
+ ### Command line and URL
120
+
121
+ | Where | Option | Example | What it does |
122
+ | --- | --- | --- | --- |
123
+ | `zrb-face` | model name | `zrb-face rem` | (Re)starts the server and opens a window with that model; without a name, the first model alphabetically. |
124
+ | `zrb-face serve` | | | Runs the server in the foreground. |
125
+ | `zrb-face action <name>` | | `zrb-face action wave` | Plays `actions/<name>` once; handy for trying a new clip. |
126
+ | `zrb-face music on\|off` | | `zrb-face music on` | Dance while idle, or stop. |
127
+ | `zrb-face dirs` | | | Lists the model and animation folders in search order, and what they hold. |
128
+ | `zrb-face init-path` | | | Prints the path of `zrb_init.py`, for `ZRB_INIT_SCRIPTS`. |
129
+ | `zrb-face.exe`, the macOS app, `electron app` | model name | `zrb-face.exe rem` or `rem.vrm` | The model to show (letters, digits, `-`, `_`; `.vrm` optional). |
130
+ | same | server URL | `zrb-face.exe http://localhost:7779` | Another server than `http://localhost:7777`. |
131
+ | the page | `?model=<name>` | `http://localhost:7777/?model=rem` | The model, in a browser. |
132
+ | the page | `?app` | (set by the app) | See-through background, dragging moves the window, no status text. |
133
+
134
+ ### Keyboard (click the window first)
135
+
136
+ | Key | What it does |
137
+ | --- | --- |
138
+ | ← → | Orbit around her. |
139
+ | ↑ ↓ | Look from higher or lower. |
140
+ | `+` / `-` | Zoom in or out (0.5× to 4×). |
141
+ | `0` | Reset the camera. |
142
+
143
+ The camera angle is saved in the window's storage and comes back next time.
144
+
145
+ ### Files and folders
146
+
147
+ Models and animations are looked up in folders, in this order; the first file with a given name wins, so your own files override the shipped ones. `zrb-face dirs` shows the list.
148
+
149
+ | Kind | Folders |
150
+ | --- | --- |
151
+ | models | `$ZRB_FACE_MODELS`, `~/.config/zrb-face/models` |
152
+ | animations | `$ZRB_FACE_ANIMS`, `~/.config/zrb-face/anim`, the package's `anim/` |
153
+
154
+ | Path | Convention |
155
+ | --- | --- |
156
+ | `<models folder>/<name>.vrm` | A model, opened by `<name>`. Only files directly in the folder are served. |
157
+ | `<anim folder>/<pool>/<name>.vrma` | An animation; the pool (a subfolder) says when it plays. See [Animations](#animations). |
158
+
159
+ ### In the code
160
+
161
+ Constants with no setting of their own; edit the file to change them.
162
+
163
+ | File | Constant | Value | What it does |
164
+ | --- | --- | --- | --- |
165
+ | `face.html` | `FRAMING` | `'body'` | `'body'` frames her head to toe (with room for the reach of every looping clip), `'face'` head and shoulders. |
166
+ | `face.html` | `LOOK` | cyan, violet, green, red, bright cyan | The glow colour per state: idle, thinking, tool, attention, speaking. |
167
+ | `face.html` | blinking | every 2–6 s, 0.12 s long | How often and how long she blinks. |
168
+ | `face.html` | smile | every 25–50 s for 3 s at 0.5; 2 s after a task | The soft `relaxed` smile. |
169
+ | `face.html` | thinking tilt / speaking nod | 0.12 / 0.04 rad | How far the head tilts while thinking and nods with the voice. |
170
+ | `face.html` | crossfade | 0.6 s (actions 0.3 s) | Blend time between animations. |
171
+ | `client.py` | `MOUTH_STEP` | 0.04 s | Audio per mouth value. |
172
+ | `zrb_init.py` | `AWAKE_HOURS` | 8:00–21:59 | When she may speak first. |
173
+ | `zrb_init.py` | idle check / away time | every 60 s / 5 min | How often `idle` checks; how long you must be away before `camera` greets you back. |
174
+ | `zrb_init.py` | `NOTHING_TO_SAY` | `…` | The reply she gives, unspoken, when nothing is worth saying. |
175
+ | `app/main.js` | window | 440×640, bottom right | Size and place of the window (320×420 suits `'face'` framing). |
176
+ | `app/main.js` | on top | `screen-saver` level | Stays above full-screen windows too. |
177
+ | `face.html` | emotion | 0.6 for 6 s | How strong and how long she shows an emotion. |
178
+ | `make_idle.py` | `CLIPS`, `DANCES` | 8 idles, 4 dances | The animations as eased keyframes of poses; run the script to regenerate `zrb_face/anim/idle/` and `dance/`. |
179
+ | `make_idle.py` | `FPS` | 15 | Keyframes per second in the generated clips. |
180
+
181
+ ## Changing the model
182
+
183
+ Put any VRM file (VRM 0.x or 1.0) in `~/.config/zrb-face/models/` (or a folder in `ZRB_FACE_MODELS`) as `<name>.vrm` and open it by name: `zrb-face <name>`. Without a name, the window shows the first model alphabetically. No model ships with the package. Model files are kept out of git (`*.vrm` is ignored). Nothing in the code depends on the model: the camera fits it, and the pose and idle come from the animations, which three-vrm retargets onto any VRM, as VRoid Hub's playground does.
184
+
185
+ Where to get models:
186
+
187
+ - **[VRoid Studio](https://vroid.com/en/studio)**: free, for Windows and macOS. Make your own and export it as VRM. You own it, and it has every expression.
188
+ - **[VRoid Hub](https://hub.vroid.com/en)**: thousands of models. Only the ones marked downloadable can be downloaded, and each has its own conditions (avatar use, commercial use, redistribution, changes). Read them before you change or share the model.
189
+ - **[BOOTH](https://booth.pm/en/search/VRM)**: search for `VRM` and filter for free items. The terms are on each item's page, often only in Japanese.
190
+ - **[Open Source Avatars](https://github.com/toxsam/open-source-avatars)**: free VRM avatars with direct download links. The license is set per collection (CC0, CC-BY, …), so check it.
191
+ - **[OpenGameArt: VRoid Studio CC0 models](https://opengameart.org/content/vroid-studio-cc0-models)**: public domain sample models from early VRoid Studio.
192
+
193
+ Fan models of existing characters (Cortana, Kusanagi, and others) are fine on your own desktop, but don't commit or share them.
194
+
195
+ ## Animations
196
+
197
+ Every `<pool>/<name>.vrma` (VRM Animation) in the animation folders loads when the page starts. The pool, the subfolder, says when it plays:
198
+
199
+ | Pool | Plays |
200
+ | --- | --- |
201
+ | `idle/` | Loops while idle, and for every state with no pool of its own. |
202
+ | `thinking/`, `tool/`, `attention/`, `speaking/` | Loops in that state. None ship; add your own. |
203
+ | `dance/` | Loops instead of `idle/` while music plays. |
204
+ | `actions/` | Once, on `{"type": "action", "value": "<name>"}`, then back to looping. zrb plays them through its `FaceAction` tool; `greeting` plays when a chat starts, and `happy`, `sad`, … with that emotion. |
205
+
206
+ Any other pool loads too, and plays once something asks for it.
207
+
208
+ To add your own, drop the file in, e.g. `~/.config/zrb-face/anim/actions/wave.vrma`, and restart with `zrb-face` (and zrb, so the `FaceAction` tool knows the new name). A file named like a shipped one replaces it: `~/.config/zrb-face/anim/idle/rest.vrma`. Try an action with `zrb-face action wave`.
209
+
210
+ Within a pool, clips play in random order with crossfades, picked by group: the first part of the name (`stretch-side` belongs to `stretch`). A random group, then a random clip in it, so the stretches together come up as often as any single other idle. On top of whatever plays: the head tilts while thinking, nods while speaking, the eyes follow you, and the face shows zrb's emotion.
211
+
212
+ The idles and dances are ours (CC0), generated by `python3 make_idle.py`. The dances are 8-beat loops at 120 BPM: `bounce` (down on every beat, arms pumping), `sway` (side to side every two beats), `step-touch` (stepping side to side, hands up on the touch) and `hands-up` (arms high, waving). They don't follow the song's tempo yet. The idles:
213
+
214
+ | Clip | What she does |
215
+ | --- | --- |
216
+ | `rest` | Relaxed, a slow weight shift. |
217
+ | `hands` | Hands clasped in front, swaying; the free foot twists on its toes. |
218
+ | `weight` | Shifts from hip to hip with the free knee bent, taking a small step each time. |
219
+ | `look` | Glances left and right. |
220
+ | `stretch` | The yawn-stretch: hands rise past the face, then up and out into a Y, shoulders shrugged, back arched, up on the toes, eyes closed, yawning; then a slow release, shoulders dropping. |
221
+ | `stretch-side` | One arm arcs over her head, the other hand slides down the thigh, the hips shift the other way; each side. |
222
+ | `stretch-neck` | Ear to each shoulder, then chin down; shoulders dropped, eyes closed. |
223
+ | `stretch-twist` | Turns from the hips up, the head leading, the arms swinging loosely, the back heel lifting. |
224
+
225
+ Every clip also has breathing, uneven head drift, knees that flex a little, and fingers that relax and tighten. The hands are loosely curled, the arms hang a little away from the body, and the weight shifts move the hips over the standing leg. The motion follows VRoid's own VRMA clips (every joint moving, fingers included), common VTuber idle loops (breathing, sway, head pitch, roll and yaw), and how people actually stretch (the whole-body yawn-stretch; a side bend with the hips shifting the other way).
226
+
227
+ To change or add idles, edit `CLIPS` in `make_idle.py` (eased keyframes of poses) and run it again. Its docstring lists which way each joint turns. Poses can also move the hips (`hipsMove`) and set the `blink` and `aa` expressions; the page takes the larger of the clip's value and its own blinking and lip sync.
228
+
229
+ More free VRMA:
230
+
231
+ - **VRoid's official 7-motion pack** (show full body, greeting, peace sign, shoot, spin, model pose, squat), free on [BOOTH](https://vroid.com/en/news/6HozzBIV0KkcKf9dc1fZGW). Free to use, commercial use with the credit "Animation credits to pixiv Inc.'s VRoid Project", but its terms forbid redistributing the files, so keep them out of git. E.g. `cp VRMA_02.vrma ~/.config/zrb-face/anim/actions/greeting.vrma`.
232
+ - **Dances**: motion capture through bvh2vrma, e.g. the [AIST++](https://google.github.io/aistplusplus_dataset/) dance dataset (CC BY 4.0, credit needed) or the [CMU](http://mocap.cs.cmu.edu/) library (free). Mixamo's dances can be converted too, but its terms forbid sharing the files, so keep them in your own folder.
233
+ - **Your own**: [bvh2vrma](https://vrm-c.github.io/bvh2vrma/) (VRM Consortium, MIT) converts BVH motion capture to VRMA, and VRoid Studio exports poses and animations as VRMA.
234
+
235
+ ## The desktop app
236
+
237
+ Each [release](https://github.com/state-alchemists/zrb-face/releases) has a zip per platform:
238
+
239
+ - **Windows** (`zrb-face-win32-x64.zip`): unzip into `%LOCALAPPDATA%\zrb-face\`, so `zrb-face` finds `zrb-face.exe` there from WSL.
240
+ - **macOS** (`zrb-face-darwin-arm64.zip` for Apple silicon, `-x64` for Intel): move `zrb-face.app` to `/Applications`. The app isn't signed, so clear macOS's block once with `xattr -dr com.apple.quarantine /Applications/zrb-face.app`.
241
+ - **Linux** (`zrb-face-linux-x64.zip`): unzip anywhere and set `ZRB_FACE_APP` to its `zrb-face` program. On Wayland (GNOME), a window can't keep itself on top, so pin it by hand.
242
+
243
+ To build it yourself:
244
+
245
+ ```sh
246
+ cd app
247
+ npm install
248
+ npm start # run from source
249
+ npm run pack:win # or pack:mac / pack:linux -> dist/
250
+ cp -r dist/zrb-face-win32-x64 /mnt/c/Users/$USER/AppData/Local/zrb-face
251
+ ```
252
+
253
+ If GitHub downloads are slow, prefix the build with `ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/`, then check the zip in `~/.cache/electron` against GitHub's `SHASUMS256.txt`.
254
+
255
+ ## Test
256
+
257
+ ```sh
258
+ python3 test_face.py # with a Python that has zrb installed
259
+ ```
260
+
261
+ ## Releasing
262
+
263
+ Bump the version in `pyproject.toml` and `app/package.json`, then push a tag: `git tag 0.1.0 && git push --tags`. `.github/workflows/release.yml` publishes the package to PyPI and attaches the app zips to the GitHub release. PyPI publishing is trusted publishing, so first add this repository, the workflow `release.yml` and the environment `pypi` as a trusted publisher of `zrb-face` on pypi.org.
264
+
265
+ ## Credits
266
+
267
+ `zrb_face/anim/` and `make_idle.py`: ours, CC0.
268
+
269
+ The models used during development, not included: *ram* and *rem made in mexico* (Re:Zero fan models) by pon3mx on [VRoid Hub](https://hub.vroid.com/en); search for them there. Ram: use, changes, commercial use and redistribution allowed, no credit needed. Rem: the same but no commercial use. Both have mouth, blink and emotion expressions. They are fan models of the Re:Zero characters, so keep them on your own machine.
@@ -0,0 +1,26 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "zrb-face"
7
+ version = "0.1.0"
8
+ description = "A 3D character for zrb in a small, see-through, always-on-top window"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ dependencies = [] # the server and the CLI are stdlib only; zrb_init.py runs inside zrb
12
+
13
+ [project.optional-dependencies]
14
+ zrb = ["zrb"]
15
+
16
+ [project.scripts]
17
+ zrb-face = "zrb_face.__main__:main"
18
+
19
+ [project.urls]
20
+ Homepage = "https://github.com/state-alchemists/zrb-face"
21
+
22
+ [tool.hatch.build.targets.wheel]
23
+ packages = ["zrb_face"]
24
+
25
+ [tool.hatch.build.targets.sdist]
26
+ include = ["zrb_face", "README.md"]
@@ -0,0 +1 @@
1
+ """zrb-face: a 3D character for zrb in a small, see-through, always-on-top window."""