Back to projects

Python · avatar motion for VTube Studio

Gaming AI Companion “Kaguya”

  • Co-authored
  • AI

A local-first AI companion that watches gameplay, speaks and chats on Discord with memory and a Live2D avatar, using local models by default, with an optional cloud fallback. Built mainly by keithku321; I contributed the avatar’s motion and Discord picture reading.

BLINK · GAZE · SWAY30 Hz
Poster drawn from the repository

By the numbers

646motion engine lines, all from my commits
  • single motion loop30 Hz
  • feelings held as mood points16
  • test functions for the motion and install work55
  • test functions for picture reading35
§ 01

Overview

A private Windows companion project, built mainly by keithku321, that watches a game on screen, talks about it, chats on Discord with memory and appears as an animated Live2D character, using local models by default, with an optional cloud fallback. My part is the motion layer that makes the character blink, look around and gesture as it talks, picture reading in Discord chats through a local vision model, and an install path for newer Python versions. The stack listed here is the whole project’s; I worked in Python against the VTube Studio API. The repository is private, so this page carries prose and counts only.

§ 02

In motion

BLINKGAZESWAYMOODBREATH30 Hz
Blink, gaze, sway, mood and breath each add a small offset; one mixer sums them and one 30 Hz loop sends the result out.
§ 03

My contributions

  • A motion engine for VTube Studio, 646 of 646 lines mine: seeded blink, gaze and sway, and a face that eases towards any of 16 feelings, each held as a valence–arousal–dominance point, with a 0.45 s time constant, all sent in one 30 Hz loop.
  • Speech gestures planned from a clip’s loudness: small accents on loud onsets, at most one per 0.6 s, plus up to three larger gestures by clip length, tried with a made-up voice clip.
  • A text-driven mouth, so lines nobody speaks aloud still move the mouth and head, using a repeatable mouth built from the text; verified live in the terminal chat, while the Discord side is tested with fakes only.
  • Mouse glances: every 25 to 70 s the eyes go to the cursor for 3.5 to 7.5 s and give way to speech gestures, checked on a 14-second clip by reading the engine’s values back.
  • Picture reading in Discord chats through a local Ollama vision model, with captions treated as untrusted data so nothing from a picture reaches memory or the cloud. It adds 35 test functions, run against fakes; the real vision model on real pictures is still unchecked.
  • An install path for Python 3.13 and 3.14 that leaves the 3.11 and 3.12 setup unchanged; a full run on the newer versions is unverified.
  • 55 test functions for the motion and install work remain in the project, counted from the source rather than from a test run.
§ 04

How it works

  1. The companion’s reply

    keithku321’s system decides what to say, which feeling to show and whether the line is spoken.

  2. Mouth envelope

    A spoken clip uses keithku321’s voice-following mouth; a line nobody speaks uses my text-built mouth on the same path.

  3. Speech planner

    Places small accents on loud onsets and up to three larger gestures by clip length.

  4. Motion layers

    Seeded blink, gaze, sway and breath, the eased mood and the mouse glances each produce small offsets.

  5. Mixer and loop

    One mixer adds the layers together, and one loop is the only writer, so mouth and motion cannot overwrite each other.

  6. VTube Studio

    Receives the values and moves the Live2D character on screen.

§ 05

Engineering notes

Dispatch № 1

A character that stood still between lines

The problem
Hotkeys could only switch faces and props on and off, and the avatar model has no blink, gaze or sway of its own. Between lines the character stood motionless.
The decision
I added independent seeded controllers whose outputs one mixer adds together, held each feeling as a point the face eases towards, and had one 30 Hz loop write everything, mouth included. The repository records these ideas as drawn from a public Apache-2.0 emotion SDK and re-implemented with no code copied.
The result
Read back from the live model, the head turned between -4.5 and 2.2 units and blinks closed the eye; an angry face took the mouth form to -0.54 and a happy one to +0.82. These are measured values, not a judgement of how the motion looks.

Dispatch № 2

Lines nobody speaks aloud

The problem
The mouth only followed real audio, so text replies left the character silent and still.
The decision
With no sound to measure, I built a mouth from the text: one opening per kana or Chinese character, about one per three letters of a Latin word, and rests at punctuation, seeded from the text so a line always gives the same mouth. It enters the same path as real audio, so head gestures are planned as normal, and the Discord side first checks whether a reply is really spoken.
The result
Verified live in the terminal chat: the mouth peaked at 0.82 for about 5.6 s with a 2-degree nod, then closed. The Discord side was written against fakes and has not been seen live.

Dispatch № 3

A glance that yields to a gesture

The problem
The first mouse glance fired only when the mouse had moved and the character was silent, so it never happened during speech or with a still mouse.
The decision
Fourteen minutes after the first version I dropped both conditions and ran it on a timer of 25 to 70 s. A glance moves the head, so only a planned speech gesture can now block one or fade it out, within 0.25 s.
The result
On a 14 s clip with a still mouse, read back from the engine, glances fell at 0 to 2.3 s, 5 to 6.8 s and 9.3 to 11.8 s, and gestures ran at 2.3 to 3.1, 7.0 to 8.4 and 11.7 to 13.0 s. No glance ran during a gesture, apart from a 0.1 s overlap as the last one faded out.
§ 06

Development timeline

No recorded video for this project.

  1. 19 Oct 2026

    Discord picture reading merged

    Milestone
  2. 17 Oct 2026

    Mouse glances made to give way to speech gestures

    Milestone
  3. 17 Oct 2026

    Text-driven mouth for lines nobody speaks aloud

    Milestone
  4. 17 Oct 2026

    Motion engine added

    Milestone
  5. 16 Oct 2026

    My first commit: install path for Python 3.13 and 3.14

    Milestone
  6. 9 Oct 2026

    keithku321 starts the project

    Milestone