Watch
1
0
Fork
You've already forked souveraine
0

publish: the public projection begins here

This is a projection, not a development branch. The tree above was constructed
from the internal source named below under a manifest that decides which paths
may leave, then scanned as a whole tree rather than as a series of patches, and
only then published.

Public history starts here because the history before it was not admissible,
and neither was the tree. What used to stand in this repository included a
rescue copy of another machine, a directory of phone handoffs, deployment
wired to one house, and a submodule pointing at a forge no stranger can reach.
None of that was ever the product. It stays in the private forge, which is
allowed to hold the whole working organism, and this is what was deliberately
sent out instead.

Three mechanisms produced this tree, in decreasing order of trust. A top-level
path the manifest does not name never arrives at all, which is the one that
catches directories nobody has thought of yet. Named internal files inside
admitted roots are dropped. A short, reviewed table replaces deployment
defaults that a public build must not carry -- an endpoint aimed at one LAN, a
VPN profile belonging to one phone, packaging built from one checkout path.

Everything after this commit is an ordinary publication with the same three
trailers, so a force push stops being routine and starts meaning that
something deliberate happened. The trailers bind the projection to its source
without pretending the public SHA is the private one: same lineage, different
tree, and the record says so.

Source-Sha: 8f27b1e76a8fef560a336aba18e6990713ff1047
Policy-Sha: 6b261d2f3e6e1fb19874846ba4bb1dfe15565d25b8618c1c1afba0419c101d27
Tree-Digest: 18ec3563c5e5ef9a414993a9f6734b251ff9ed3cd56eebdd6cac01e45c6e3067
This commit is contained in:
Fimeg 2026-09-04 15:55:48 -04:00
commit 8f42fc953d
1476 changed files with 238455 additions and 0 deletions

14
.cargo/aarch64-pkg-config Executable file
View file

@ -0,0 +1,14 @@
#!/bin/bash
# ponytail: cross pkg-config wrapper for aarch64-unknown-linux-gnu.
# cargo's [env] block doesn't reliably reach build-script pkg-config probes,
# so alsa-sys's built-in cross guard trips. This wrapper sets the vars itself
# and is referenced via PKG_CONFIG in .cargo/config.toml — every pkg-config
# probe in the tree (alsa-sys today, any future -sys crate) goes through it.
SYSROOT="${SOUVERAINE_AARCH64_SYSROOT:-/usr/aarch64-linux-gnu}"
export PKG_CONFIG_ALLOW_CROSS=1
# Both dirs: udev.pc (and other systemd-shipped .pc files) live in
# share/pkgconfig, not lib/pkgconfig. This wrapper OVERRIDES whatever the
# caller exported, so widening it here is the only fix that takes effect.
export PKG_CONFIG_LIBDIR="$SYSROOT/usr/lib/pkgconfig:$SYSROOT/usr/share/pkgconfig"
export PKG_CONFIG_SYSROOT_DIR="$SYSROOT"
exec /usr/bin/pkg-config "$@"

10
.cargo/config.toml Normal file
View file

@ -0,0 +1,10 @@
# ponytail: cross-build target for the Pixel 3 / any aarch64 Arch box.
# Sysroot (/usr/aarch64-linux-gnu, from the aarch64-linux-gnu-gcc/glibc Arch
# packages) holds glibc 2.43 + the cross alsa-lib we built in. Linker is the
# cross gcc.
#
# The PKG_CONFIG_* vars alsa-sys needs are NOT set here — cargo's [env] block
# doesn't reliably reach build-script pkg-config probes. scripts/build-cross.sh
# exports them on the cargo invocation so they inherit to every build script.
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"

2
.gitattributes vendored Normal file
View file

@ -0,0 +1,2 @@
# Preserve vendored upstream bytes; style checks apply to our overlays.
surfaces/quickshell/ii-base/** -whitespace

View file

@ -0,0 +1,199 @@
name: publish
# The public branch is an output. Nothing here builds or tests the product --
# `ci.yml` on `primary` did that against the source this projection was
# constructed from. This workflow only carries an already-constructed,
# already-gated candidate to the forge that is allowed to serve it, and proves
# the forge serves exactly that.
#
# It lives on the public branch because that is where the push lands, and it is
# deliberately the only workflow that does: `ci.yml` stays internal because it
# names internal build hosts.
on:
push:
branches: [public]
workflow_dispatch:
jobs:
# The gate travels with the tree it judges. A candidate that reaches this
# branch has already passed on the source side; running it again here means
# the thing actually being published is the thing that was actually checked,
# rather than something that resembled it an hour ago.
public-surface:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
- name: Gate the published tree
run: |
set -euo pipefail
if [ ! -f .publication/surface_gate.py ]; then
echo "::error::the public tree carries no gate; refusing to publish it"
exit 1
fi
# The manifest is private -- its redaction table quotes what it
# removes -- so the published tree is checked against the rules the
# gate carries, with no exception list available to excuse anything.
python3 - <<'PY' > /tmp/bare-manifest.json
import json
json.dump({
"schema_version": 1,
"repository": "Fimeg/souveraine",
"public_roots": [], "public_root_files": [],
"review_required": [], "forbidden": [], "forbidden_classes": [],
"exceptions": [{"path": ".publication/*", "rule": r,
"reason": "the gate names the patterns it refuses",
"reviewed": "2026-09-04"}
for r in ("internal-host", "internal-email", "rfc1918",
"home-path", "bearer-token", "ai-attribution")],
}, open("/dev/stdout", "w"))
PY
python3 .publication/surface_gate.py \
--repo . --sha "$GITHUB_SHA" \
--manifest /tmp/bare-manifest.json \
--out /tmp/published-surface.md || true
cat /tmp/published-surface.md
# Tree shape is empty in the bare manifest, so only content, history
# and file-class findings can deny here. Those are the ones that must
# never reach a public forge.
if grep -E '^\| deny \|' /tmp/published-surface.md | grep -vq 'unclassified-root'; then
echo "::error::published tree carries content or history findings"
exit 1
fi
echo "published tree carries no content or history findings"
publish-forge:
runs-on: ubuntu-latest
needs: [public-surface]
env:
PUBLIC_FORGE_TOKEN: ${{ secrets.PUBLIC_FORGE_TOKEN }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
- name: Publish and verify exact SHA
run: |
set -euo pipefail
: "${PUBLIC_FORGE_TOKEN:?PUBLIC_FORGE_TOKEN is required}"
expected="${GITHUB_SHA}"
forge_url='https://forge.caseytunturi.com/Fimeg/souveraine.git'
# Ordinary publication is fast-forward only. .publication/EPOCH may
# authorise exactly one replacement, and only of the SHA it names, so
# the authorisation is spent the moment it is used.
lease=""
remote_sha="$(git ls-remote "$forge_url" refs/heads/public | awk '{print $1}')"
if [ -n "$remote_sha" ]; then
if git cat-file -e "${remote_sha}^{commit}" 2>/dev/null &&
git merge-base --is-ancestor "$remote_sha" "$expected"; then
:
else
authorised="$(awk '$1 == "replaces" { print $2 }' .publication/EPOCH 2>/dev/null || true)"
if [ -n "$authorised" ] && [ "$authorised" = "$remote_sha" ]; then
echo "[publish] epoch authorised to replace $remote_sha"
lease="$remote_sha"
else
echo "[publish] refusing non-fast-forward public history" >&2
echo "[publish] remote is $remote_sha; EPOCH authorises ${authorised:-nothing}" >&2
exit 1
fi
fi
fi
auth="$(printf 'publisher-souveraine:%s' "$PUBLIC_FORGE_TOKEN" | base64 -w0)"
if [ -n "$lease" ]; then
git -c "http.https://forge.caseytunturi.com/.extraheader=Authorization: Basic $auth" \
push --force-with-lease="refs/heads/public:$lease" "$forge_url" HEAD:public
else
git -c "http.https://forge.caseytunturi.com/.extraheader=Authorization: Basic $auth" \
push "$forge_url" HEAD:public
fi
forge_sha="$(git ls-remote "$forge_url" refs/heads/public | awk '{print $1}')"
test "$forge_sha" = "$expected"
git fetch --force --no-tags "$forge_url" \
refs/heads/public:refs/remotes/public-forge/public
test "$(git rev-parse refs/remotes/public-forge/public)" = "$forge_sha"
echo "[publish] Forgejo anonymously serves exact published SHA: $forge_sha"
# Downstreams reproduce the anonymously fetched Forgejo ref. A missing
# credential or a divergent history is a visible degraded mirror, never a
# failure of the canonical publication.
mirror-downstreams:
runs-on: ubuntu-latest
needs: [publish-forge]
env:
CODEBERG_TOKEN: ${{ secrets.CODEBERG_TOKEN }}
MIRROR_GITHUB_TOKEN: ${{ secrets.MIRROR_GITHUB_TOKEN }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
- name: Mirror downstreams from Forgejo
run: |
set -euo pipefail
expected="${GITHUB_SHA}"
forge_url='https://forge.caseytunturi.com/Fimeg/souveraine.git'
forge_sha="$(git ls-remote "$forge_url" refs/heads/public | awk '{print $1}')"
test "$forge_sha" = "$expected"
git fetch --force --no-tags "$forge_url" \
refs/heads/public:refs/remotes/public-forge/public
test "$(git rev-parse refs/remotes/public-forge/public)" = "$forge_sha"
mirror_downstream() {
label="$1"; url="$2"; host="$3"; user="$4"; token="$5"
if [ -z "$token" ]; then
echo "::warning::[mirror] $label credential absent; Forgejo is published, $label is degraded"
return 0
fi
if ! remote_sha="$(git ls-remote "$url" refs/heads/public | awk '{print $1}')"; then
echo "::warning::[mirror] cannot read $label public ref; Forgejo remains authoritative"
return 0
fi
lease=""
if [ -n "$remote_sha" ]; then
if ! git fetch --force --no-tags "$url" \
"refs/heads/public:refs/remotes/mirror-check/$label"; then
echo "::warning::[mirror] cannot fetch $label public ref; leaving it unchanged"
return 0
fi
if ! git merge-base --is-ancestor "$remote_sha" "$forge_sha"; then
authorised="$(awk '$1 == "replaces" { print $2 }' .publication/EPOCH 2>/dev/null || true)"
if [ -n "$authorised" ] && [ "$authorised" = "$remote_sha" ]; then
echo "[mirror] $label follows the authorised epoch from $remote_sha"
lease="$remote_sha"
else
echo "::warning::[mirror] refusing non-fast-forward $label history; explicit alignment required"
return 0
fi
fi
fi
auth="$(printf '%s:%s' "$user" "$token" | base64 -w0)"
if [ -n "$lease" ]; then
if ! git -c "http.https://$host/.extraheader=Authorization: Basic $auth" \
push --force-with-lease="refs/heads/public:$lease" \
"$url" refs/remotes/public-forge/public:public; then
echo "::warning::[mirror] $label epoch push failed; Forgejo remains authoritative"
return 0
fi
elif ! git -c "http.https://$host/.extraheader=Authorization: Basic $auth" \
push "$url" refs/remotes/public-forge/public:public; then
echo "::warning::[mirror] $label push failed; Forgejo remains authoritative"
return 0
fi
mirrored_sha="$(git ls-remote "$url" refs/heads/public | awk '{print $1}')"
if [ "$mirrored_sha" != "$forge_sha" ]; then
echo "::warning::[mirror] $label SHA mismatch after push; Forgejo remains authoritative"
return 0
fi
echo "[mirror] $label agrees with Forgejo: $forge_sha"
}
mirror_downstream codeberg 'https://codeberg.org/Fimeg/souveraine.git' codeberg.org Fimeg "$CODEBERG_TOKEN"
mirror_downstream github 'https://github.com/Fimeg/souveraine.git' github.com Fimeg "$MIRROR_GITHUB_TOKEN"

76
.gitignore vendored Normal file
View file

@ -0,0 +1,76 @@
# Rust
/target/
**/*.rs.bk
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Debug
gdb.txt
*.log
# Environment
.env
.env.local
# Build artifacts
*.o
*.so
*.dylib
*.dll
*.rlib
*.rmeta
# Test
*.profraw
lcov.info
# Temporary files
*.tmp
*.temp
# Local development
local.toml
config.local.toml
# Voice-input test recordings — transient, stay local
voice-recording-*.mp3
# Memory repos (should be external)
.memer/
**/.memer/
# Web assets (if present)
web/dist/
souveraine.log
souveraine.log.prev
# Filed-away stray working files — kept on disk, out of the repo
/docs/archive/strays/
# Config (use souveraine.example.toml as template)
souveraine.toml
# Working notes — kept on disk, never committed to any remote
CLAUDE.md
docs/
continuation_prompt.md
AD_continuation_prompt.md
COMMIT_MESSAGES.txt
vanguard-souveraine-notes.md
# Local-only project scaffolding — kept on disk, never committed to any remote.
# Anchored to repo root (leading /) so shipped surface scripts — e.g.
# surfaces/quickshell/scripts/ — are still tracked.
/.superpowers/
/journal/
/scripts/
/website/

13
.publication/EPOCH Normal file
View file

@ -0,0 +1,13 @@
# Break glass.
#
# Ordinary publication is fast-forward only. This file authorises the
# publication job to replace the public branch exactly once, and only when the
# public ref it finds is the one named below. After the epoch lands, the named
# SHA is no longer what any remote holds, so this authorisation is spent and
# force push goes dark again without anyone remembering to close it.
#
# Development history is never rewritten. It is preserved internally at
# archive/public-pre-epoch-2026-09-04.
replaces b68bcebf6c390ec6e2a361aff3f17ea0736c7da2
reason sanitized projection epoch; public history begins at the cut

View file

@ -0,0 +1,478 @@
#!/usr/bin/env python3
"""Editorial authority over a public tree.
The existing `.publication` mechanism answers a transport question: was this
SHA produced by the right gates, on the right repository, under a policy that
authorizes projection to this destination. It is a good answer to that
question and this file does not replace it.
This file answers the question nobody was asking:
Does this tree belong outside at all?
Six gates, in the order a reviewer would actually apply them:
1. tree-shape every top-level entry is classified by the manifest
2. new-surface what the public diff P1 -> P2 newly admits
3. file-class forbidden classes anywhere in the tree
4. content the whole candidate tree, not the changed lines
5. history subjects, bodies, and author identities
6. presentation what a stranger sees in the root listing
Gate 4 is the one that matters most and is the one an incremental scanner
structurally cannot provide. A patch scanner sees a file the day it lands. It
never sees it again. A tree that was clean when every one of its commits was
scanned can still be a tree that should not be public, because publication is
a property of the tree, not of the diffs that built it.
The severity model is deliberately harsher than the incremental scanner's.
There, a home path or a LAN address is a WARN: one line in one patch, and a
human is reading the patch anyway. Here the same finding is a DENY, because
nobody reads a whole tree, and because the finding means the path is standing
in the public product right now, not that it passed through once.
Absence of known-secret content is not authorization to publish.
Exceptions are narrow and auditable: path, rule, reason, review date. A rule
turned off globally is not an exception, it is a retreat, so this file has no
syntax for one.
"""
import argparse
import fnmatch
import json
import os
import re
import subprocess
import sys
from datetime import date
SCHEMA_VERSION = 1
DENY = "deny"
REVIEW = "review"
NOTE = "note"
SEVERITY_ORDER = {DENY: 0, REVIEW: 1, NOTE: 2}
# Anything a forge will not render as source. Reviewed by eye, not by rule.
BINARY_HINT = re.compile(
r"\.(png|jpe?g|gif|webp|ico|pdf|zip|gz|xz|zst|bz2|tar|so|a|o|dll|dylib"
r"|exe|bin|wav|mp3|mp4|ogg|woff2?|ttf|otf|jar|whl|deb|rpm|img|iso)$",
re.IGNORECASE,
)
# Host suffixes a stranger can actually resolve. A submodule pointed anywhere
# else is both a leak and a repository that does not clone.
PUBLIC_FORGE_HOSTS = (
"forge.caseytunturi.com",
"codeberg.org",
"github.com",
"gitlab.com",
"git.sr.ht",
)
class Rule:
__slots__ = ("rule_id", "description", "pattern", "severity")
def __init__(self, rule_id, description, pattern, severity):
self.rule_id = rule_id
self.description = description
self.pattern = re.compile(pattern, re.IGNORECASE)
self.severity = severity
# Capabilities deny because publishing one hands control to whoever reads it
# and no later cleanup takes it back. Topology denies here — see the module
# docstring — because in a tree it is a standing disclosure, not a transit.
CONTENT_RULES = (
Rule("private-key", "private key material",
r"-----BEGIN (?:RSA|DSA|EC|OPENSSH|PGP) PRIVATE KEY", DENY),
# The value must be quoted. An unquoted run of letters after `authorization:`
# is a type name in every language that has types, and matching it made the
# gate cry wolf over `authorization: MutationAuthorization` on first run.
Rule("bearer-token", "embedded token or bearer credential",
r"(?:ghp|gho|ghs|ghu|github_pat)_[A-Za-z0-9_]{20,}"
r"|(?:authorization|private[-_]?token|api[-_]?key|client[-_]?secret)"
r"\s*[:=]\s*['\"][A-Za-z0-9._~+/=-]{16,}['\"]", DENY),
Rule("rfc1918", "private LAN address",
r"\b(?:10\.\d{1,3}\.\d{1,3}\.\d{1,3}"
r"|192\.168\.\d{1,3}\.\d{1,3}"
r"|172\.(?:1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3})\b", DENY),
# Case-sensitive on purpose. Every rule here is otherwise IGNORECASE, and
# a case-blind `/Users/` matched the phrase "sessions/users/seats" in a
# comment about loginctl. A macOS home is capitalised; a POSIX path segment
# spelled `users` is not a home directory.
Rule("home-path", "developer home directory",
r"/home/[a-z][a-z0-9_-]*|(?-i:/Users/[A-Za-z])|(?-i:C:\\\\Users\\\\)", DENY),
Rule("internal-host", "internal hostname or forge",
r"\bwiuf|\barchdev\b|\bwiufph\b", DENY),
# The internal domain only. An author's own public contact address is not a
# leak — it is on the security page on purpose, and matching it made the
# gate refuse commits for being signed by the person who wrote them.
Rule("internal-email", "internal service identity or internal domain",
r"@wiuf\.net", DENY),
Rule("ai-attribution", "model attribution",
r"co-authored-by:\s*(?:claude|gpt|copilot|codex)"
r"|generated with \[?claude", REVIEW),
)
MESSAGE_RULES = CONTENT_RULES
def run(repo, args, ok=(0,)):
p = subprocess.run(["git", "-C", repo] + args,
capture_output=True, text=True, errors="replace")
if p.returncode not in ok:
raise RuntimeError(f"git {' '.join(args)}: {p.stderr.strip()}")
return p.stdout
class Finding:
__slots__ = ("gate", "severity", "rule", "path", "detail", "excerpt")
def __init__(self, gate, severity, rule, path, detail, excerpt=""):
self.gate = gate
self.severity = severity
self.rule = rule
self.path = path
self.detail = detail
self.excerpt = excerpt
def redact(text, match):
"""Keep the shape, drop the value. A report is itself a publishable object."""
s, e = match.span()
line = text[max(0, s - 40):e + 40].replace("\n", " ").strip()
hit = match.group(0)
keep = 2 if len(hit) > 6 else 1
masked = hit[:keep] + "*" * max(1, len(hit) - keep * 2) + (hit[-keep:] if len(hit) > 6 else "")
return line.replace(hit, masked)[:150]
class Manifest:
"""The authority for what may cross. Changing it is the consequential act."""
def __init__(self, raw, path):
self.path = path
self.schema_version = raw.get("schema_version")
if self.schema_version != SCHEMA_VERSION:
raise ValueError(f"{path}: schema_version must be {SCHEMA_VERSION}")
self.repository = raw.get("repository", "?")
self.public_roots = list(raw.get("public_roots", []))
self.public_root_files = list(raw.get("public_root_files", []))
self.review_required = list(raw.get("review_required", []))
self.forbidden = list(raw.get("forbidden", []))
self.forbidden_classes = list(raw.get("forbidden_classes", []))
self.max_blob_bytes = int(raw.get("max_blob_bytes", 2 * 1024 * 1024))
self.exceptions = list(raw.get("exceptions", []))
def classify_root(self, name):
if name in self.forbidden:
return "forbidden"
if name in self.public_roots or name in self.public_root_files:
return "allowed"
if name in self.review_required:
return "review"
return "unclassified"
def excepted(self, path, rule_id):
"""An exception names one path and one rule, and says why, and when."""
for exc in self.exceptions:
if exc.get("rule") != rule_id:
continue
if not fnmatch.fnmatch(path, exc.get("path", "")):
continue
if not exc.get("reason") or not exc.get("reviewed"):
continue
return exc
return None
def tree_entries(repo, sha):
"""(mode, type, oid, path) for every entry, recursively."""
out = run(repo, ["ls-tree", "-r", "-l", sha])
entries = []
for line in out.splitlines():
if not line.strip():
continue
meta, path = line.split("\t", 1)
parts = meta.split()
mode, otype, oid = parts[0], parts[1], parts[2]
size = parts[3] if len(parts) > 3 else "-"
entries.append((mode, otype, oid, size, path))
return entries
def top_level(repo, sha):
return [l for l in run(repo, ["ls-tree", "--name-only", sha]).splitlines() if l.strip()]
# ---------------------------------------------------------------- gate 1
def gate_tree_shape(repo, sha, manifest, findings):
for name in top_level(repo, sha):
verdict = manifest.classify_root(name)
if verdict == "forbidden":
findings.append(Finding(
"tree-shape", DENY, "forbidden-root", name,
"top-level path is forbidden by the public-surface manifest"))
elif verdict == "unclassified":
findings.append(Finding(
"tree-shape", DENY, "unclassified-root", name,
"no manifest entry admits this top-level path to the public product"))
elif verdict == "review":
findings.append(Finding(
"tree-shape", REVIEW, "review-root", name,
"manifest marks this root review-required"))
# ---------------------------------------------------------------- gate 2
def gate_new_surface(repo, sha, previous, manifest, findings):
if not previous:
findings.append(Finding(
"new-surface", NOTE, "no-baseline", "-",
"no previous public SHA given; the diff gate did not run"))
return
out = run(repo, ["diff", "--name-status", "--diff-filter=ACR", previous, sha])
added = []
for line in out.splitlines():
parts = line.split("\t")
if len(parts) >= 2:
added.append(parts[-1])
old_roots = set(top_level(repo, previous))
# One finding per newly admitted root, not one per file underneath it: the
# decision being reviewed is the root, and a hundred rows for one directory
# buries the other gates' output.
reported = set()
for path in added:
root = path.split("/", 1)[0]
if root not in old_roots and root not in reported:
reported.add(root)
sev = REVIEW if manifest.classify_root(root) == "allowed" else DENY
findings.append(Finding(
"new-surface", sev, "new-top-level", root,
"publication newly admits a top-level path; "
"this is a public-surface change, not a code change"))
if os.path.basename(path).startswith("."):
findings.append(Finding(
"new-surface", REVIEW, "new-dotfile", path,
"new dotfile or dotdirectory entering the public tree"))
# Symlinks and submodules are surface changes disguised as files.
for mode, otype, _oid, _size, path in tree_entries(repo, sha):
if mode == "120000":
target = run(repo, ["show", f"{sha}:{path}"]).strip()
escapes = target.startswith("/") or ".." in target.split("/")
findings.append(Finding(
"new-surface", DENY if escapes else NOTE, "symlink", path,
"symlink target leaves the public tree" if escapes
else "symlink stays inside the public tree"))
if otype == "commit":
findings.append(Finding(
"new-surface", REVIEW, "submodule", path,
"submodule gitlink; its URL must resolve publicly"))
def gate_submodule_urls(repo, sha, findings):
try:
text = run(repo, ["show", f"{sha}:.gitmodules"])
except RuntimeError:
return
name = None
for line in text.splitlines():
line = line.strip()
if line.startswith("[submodule"):
name = line
if line.startswith("url"):
url = line.split("=", 1)[1].strip()
host = re.sub(r"^[a-z+]+://", "", url).split("/")[0].split("@")[-1].split(":")[0]
if not any(host == h or host.endswith("." + h) for h in PUBLIC_FORGE_HOSTS):
findings.append(Finding(
"new-surface", DENY, "submodule-private-url", ".gitmodules",
f"submodule url host is not publicly resolvable ({host}); "
"the public repository cannot clone and the host leaks",
excerpt=name or ""))
# ---------------------------------------------------------------- gate 3
def gate_file_class(repo, sha, manifest, findings):
for _mode, _otype, _oid, size, path in tree_entries(repo, sha):
for pat in manifest.forbidden_classes:
if fnmatch.fnmatch(path, pat) or fnmatch.fnmatch(os.path.basename(path), pat):
findings.append(Finding(
"file-class", DENY, "forbidden-class", path,
f"matches forbidden class {pat!r}"))
break
if size not in ("-", None) and size.isdigit() and int(size) > manifest.max_blob_bytes:
findings.append(Finding(
"file-class", REVIEW, "oversized-blob", path,
f"{int(size):,} bytes exceeds the reviewed maximum "
f"({manifest.max_blob_bytes:,})"))
# ---------------------------------------------------------------- gate 4
def gate_content(repo, sha, manifest, findings, limit_per_rule=40):
counts = {}
for _mode, otype, _oid, _size, path in tree_entries(repo, sha):
if otype != "blob" or BINARY_HINT.search(path):
continue
try:
text = run(repo, ["show", f"{sha}:{path}"])
except RuntimeError:
continue
if "\0" in text[:8000]:
continue
for rule in CONTENT_RULES:
m = rule.pattern.search(text)
if not m:
continue
exc = manifest.excepted(path, rule.rule_id)
if exc:
findings.append(Finding(
"content", NOTE, rule.rule_id, path,
f"excepted: {exc['reason']} (reviewed {exc['reviewed']})"))
continue
counts[rule.rule_id] = counts.get(rule.rule_id, 0) + 1
if counts[rule.rule_id] > limit_per_rule:
continue
findings.append(Finding(
"content", rule.severity, rule.rule_id, path,
rule.description, excerpt=redact(text, m)))
# ---------------------------------------------------------------- gate 5
def gate_history(repo, sha, manifest, findings, limit_per_rule=25):
sep = "\x1e"
out = run(repo, ["log", f"--format=%H{sep}%an <%ae>{sep}%s{sep}%b\x1d", sha])
counts = {}
total = 0
for record in out.split("\x1d"):
record = record.strip("\n")
if not record.strip():
continue
parts = record.split(sep)
if len(parts) < 4:
continue
h, ident, subject, body = parts[0], parts[1], parts[2], parts[3]
total += 1
blob = f"{ident}\n{subject}\n{body}"
for rule in MESSAGE_RULES:
m = rule.pattern.search(blob)
if not m:
continue
counts[rule.rule_id] = counts.get(rule.rule_id, 0) + 1
if counts[rule.rule_id] > limit_per_rule:
continue
findings.append(Finding(
"history", rule.severity, rule.rule_id, h[:12],
f"{rule.description} in commit metadata: {subject[:60]}",
excerpt=redact(blob, m)))
findings.append(Finding("history", NOTE, "reachable", "-",
f"{total} commits reachable from {sha[:12]}"))
# ---------------------------------------------------------------- gate 6
def gate_presentation(repo, sha, manifest, findings):
roots = top_level(repo, sha)
product = [r for r in roots
if manifest.classify_root(r) == "allowed" and not r.startswith(".")]
other = [r for r in roots if manifest.classify_root(r) != "allowed"]
findings.append(Finding(
"presentation", NOTE, "root-listing", "-",
f"{len(roots)} top-level entries; {len(product)} read as product, "
f"{len(other)} do not"))
if other:
findings.append(Finding(
"presentation", REVIEW, "mixed-listing", "-",
"a stranger cannot distinguish product from working material: "
+ ", ".join(sorted(other))))
# ---------------------------------------------------------------- report
GATES = ("tree-shape", "new-surface", "file-class", "content", "history", "presentation")
def report(findings, repo, sha, previous, manifest):
lines = []
w = lines.append
w(f"# Public-surface audit — {manifest.repository}")
w("")
w(f"- candidate SHA: `{sha[:12]}`")
w(f"- previous public SHA: `{previous[:12] if previous else '(none given)'}`")
w(f"- manifest: `{os.path.basename(manifest.path)}`")
w(f"- generated: {date.today().isoformat()}")
w("")
deny = [f for f in findings if f.severity == DENY]
review = [f for f in findings if f.severity == REVIEW]
verdict = "DENY" if deny else ("REVIEW" if review else "PASS")
w(f"**Verdict: {verdict}** — {len(deny)} deny, {len(review)} review, "
f"{len([f for f in findings if f.severity == NOTE])} note.")
w("")
for gate in GATES:
rows = [f for f in findings if f.gate == gate]
if not rows:
continue
w(f"## {gate}")
w("")
rows.sort(key=lambda f: (SEVERITY_ORDER[f.severity], f.rule, f.path))
w("| sev | rule | path | detail |")
w("| --- | --- | --- | --- |")
for f in rows:
detail = f.detail
if f.excerpt:
detail += f" — `{f.excerpt}`"
detail = detail.replace("|", "\\|")
w(f"| {f.severity} | {f.rule} | `{f.path}` | {detail} |")
w("")
return "\n".join(lines)
def main():
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
ap.add_argument("--repo", required=True)
ap.add_argument("--sha", required=True)
ap.add_argument("--manifest", required=True)
ap.add_argument("--previous", default="")
ap.add_argument("--out", default="")
ap.add_argument("--skip-history", action="store_true")
args = ap.parse_args()
with open(args.manifest) as fh:
manifest = Manifest(json.load(fh), args.manifest)
sha = run(args.repo, ["rev-parse", args.sha]).strip()
previous = run(args.repo, ["rev-parse", args.previous]).strip() if args.previous else ""
findings = []
gate_tree_shape(args.repo, sha, manifest, findings)
gate_new_surface(args.repo, sha, previous, manifest, findings)
gate_submodule_urls(args.repo, sha, findings)
gate_file_class(args.repo, sha, manifest, findings)
gate_content(args.repo, sha, manifest, findings)
if not args.skip_history:
gate_history(args.repo, sha, manifest, findings)
gate_presentation(args.repo, sha, manifest, findings)
text = report(findings, args.repo, sha, previous, manifest)
if args.out:
with open(args.out, "w") as fh:
fh.write(text + "\n")
print(f"wrote {args.out}")
else:
print(text)
return 1 if any(f.severity == DENY for f in findings) else 0
if __name__ == "__main__":
sys.exit(main())

40
AGENTS.md Normal file
View file

@ -0,0 +1,40 @@
# Souveraine working agreement
The personal agreement is `~/.codex/AGENTS.md`. The cross-repo map is
`../AGENTS.md`; read it when work crosses repository boundaries.
## This repository
- This is the Rust agent substrate and Souveraine shell code. Use
**substrate**, not harness, unless quoting an external source.
- The map is `../SouveraineOS/saf/INDEX.md` — the living architecture lives
at the umbrella, not beside the code. Read it before design work; the
substrate's own old `saf/` here is a pointer, not a second spine.
- Work/status documentation for the wider OS belongs in
`../SouveraineOS/saf/` and `../SouveraineOS/saf/state.md`, not a new local
handoff. Update the one owner when behavior moves.
- The normal branch is `primary`. Verify status and recent history before
editing, preserve unrelated changes, and stage explicit paths.
## Build and delivery
- Gitea Actions is the reproducible build/test/package path. Push the scoped
change and let CI build it.
- Keep local checks small and targeted. Do not replace the pipeline with a
direct build-host build, an offline build, or a hand-copied artifact.
- Runner access is diagnostic. A green result matters only if the relevant job
ran and, when required, published the expected package.
- Phone delivery is through package ownership and pacman. A symlink or copied
file on glass may prove behavior, but it is not landed.
## Shape of changes
- Preserve the substrate's one-owner instincts: one event path, one state
writer, one canonical memory/history operation, one shipping path.
- Commit subjects are terse and honest: change and reason. Terse does not mean
sterile in our first-party forge; a body may have teeth if it still serves
the change. No AI attribution or `Co-Authored-By` line.
- Separate verified behavior from reasoned design and from work never exercised
on hardware.
- Before calling cross-repo work complete, reconcile the owning SouveraineOS
task/state document and archive the task if its acceptance is actually met.

13833
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

278
Cargo.toml Normal file
View file

@ -0,0 +1,278 @@
[package]
name = "souveraine"
version = "1.0.0"
edition = "2021"
rust-version = "1.94"
authors = ["Souveraine Contributors"]
description = "Souveraine - a sovereign consciousness substrate for personal agents"
license = "AGPL-3.0-or-later"
[dependencies]
# Core runtime
tokio = { version = "1", features = ["full", "rt-multi-thread"] }
tokio-util = "0.7"
tokio-stream = { version = "0.1", features = ["fs"] }
futures = "0.3"
async-trait = "0.1"
# Web/WebSocket (for external clients)
axum = { version = "0.7", features = ["ws"] }
tower = "0.4"
tower-http = { version = "0.6", features = ["cors", "trace", "fs"] }
tokio-tungstenite = "0.24" # Outbound WS client — federation bridge connects to peer endpoints
# Serialization
serde = { version = "1", features = ["derive"] }
serde_yaml = "0.9"
serde_json = "1"
toml = "0.8"
# Git operations
git2 = { version = "0.19", default-features = false }
# Database (server mode)
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "sqlite", "migrate", "chrono", "json"] }
# Concurrent collections (server sessions)
dashmap = "5"
# File system
notify = "7" # File watching
# Environment (.env file loading)
dotenvy = "0.15"
tempfile = "3"
walkdir = "2"
# HTTP client (for Bifrost/Ollama)
reqwest = { version = "0.12", default-features = false, features = ["json", "stream", "rustls-tls", "multipart"] }
# Voice channel — mic capture, mp3 playback, WAV encoding, STT/TTS HTTP clients
cpal = "0.15"
rodio = { version = "0.19", default-features = false, features = ["mp3"] }
hound = "3.5"
# Embeddings/ML
# tokenizers = "0.15" # For local tokenization
# Terminal/UI
crossterm = { version = "0.28", features = ["bracketed-paste"] } # Terminal control (bracketed paste for paste detection) — kept for old TUI
ratatui = { version = "0.30", features = ["crossterm"] } # TUI framework with crossterm backend — kept for old TUI
tuie = { path = "../tuie", features = ["harmonious", "images"] } # New composable widget toolkit — replacing ratatui
chord_macro = { path = "../tuie/chord_macro" } # chord!() input-matching macro — the one the tuie-demo widgets use
axis2d = "0.1.0" # Axis2D/Vec2 geometry — re-exported by tuie, used directly by the demo widgets
unicode-width = "0.1"
arboard = { version = "3", features = ["wayland-data-control"] } # Clipboard — click-to-copy a message bubble
colored = "2" # Color gradients and effects
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp"] } # Per-agent portraits (assets/portrait.{png,jpg}) + multimodal images
ratatui-image = { version = "11", default-features = false, features = ["crossterm", "image-defaults"] } # Real photo rendering in TUI (kitty/sixel/halfblock) — chafa-dyn default dropped, souveraine uses picker/protocols only, avoids a cross chafa+glib sysroot
ratatui-ratty = { version = "0.2", optional = true } # Inline 3D graphics via Ratty Graphics Protocol
# Logging/tracing
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
# Full USB KVM receiver. Optional because only the aarch64 phone package ships
# it; the main substrate and desktop builds do not need a windowing stack.
gadgetry-most-foul = { version = "0.1.1", optional = true }
gud-gadget = { git = "https://github.com/samcday/gud-gadget", rev = "33024b6152633eb8359375419281fa3ebac9c9cb", optional = true }
softbuffer = { version = "0.4", optional = true }
winit = { version = "0.30", optional = true }
# Error handling
anyhow = "1"
thiserror = "1"
# Process management (subagents)
sysinfo = "0.30"
# Time
chrono = { version = "0.4", features = ["serde"] }
# UUIDs for conversation IDs
uuid = { version = "1", features = ["v4"] }
# Regex
regex = "1"
# Token counting (cl100k_base for context pressure estimation)
tiktoken = "3"
# System directories
dirs = "5"
# OS-native credential storage
keyring = "4"
keyring-core = "1"
# Cryptographic identity (seed-id, event signing, federation trust root)
ed25519-dalek = { version = "2", features = ["rand_core", "pem"] }
sha2 = "0.10"
rand = "0.8"
# SO_PEERCRED on the machined socket — the daemon logs who asked for what
libc = "0.2"
# Cron expression parsing (schedule system)
cron = "0.13"
# Markdown parsing for the TUI chat renderer (lift from jcode pattern)
pulldown-cmark = "0.12"
# Tauri (desktop app wrapper)
tauri = { version = "2", features = ["tray-icon", "devtools"], optional = true }
tauri-plugin-shell = { version = "2", optional = true }
# CLI argument parsing
clap = { version = "4", features = ["derive", "env"] }
clap_complete = "4"
# Interactive REPL
rustyline = "13"
shellexpand = "3"
figlet-rs = { version = "1.0.0", optional = true }
cowsay = { version = "0.14.0", optional = true }
tui-big-text = "0.8.4"
tui-widgets = "0.7.2"
base64 = "0.22.1"
hex = "0.4"
once_cell = "1.21.4"
glob = "0.3"
# Matrix sensorium — optional surface. matrix-sdk is only compiled in when
# the `matrix` feature is enabled; a default build never links it.
# matrix-sdk 0.17 always uses rustls (no tls feature flag).
# NOTE: `e2e-encryption` disabled for Phase 3 — it pulls matrix-sdk-sqlite
# → rusqlite → libsqlite3-sys 0.35, conflicting with sqlx 0.7's
# libsqlite3-sys 0.26 (both `links = "sqlite3"`). Phase 3 uses the
# in-memory store; e2ee + sqlite store return in Phase 6.
matrix-sdk = { version = "0.17", optional = true, default-features = false }
# Secret Service daemon (org.freedesktop.secrets) — optional binary target.
# Backs libsecret clients (Chatty/libcmatrix, and our own keyring-core above)
# from the existing SeedId, so the machine identity is the trust root instead
# of gnome-keyring/KWallet. zbus = session bus; aes/cbc = AES-128-CBC + PKCS7
# (the OpenSession "dh-ietf1024-sha256-aes128-cbc-pkcs7" transport); hkdf =
# session key derivation (sha2/rand already plain deps above); num-bigint =
# RFC 2409 MODP-1024 group math for the OpenSession DH exchange itself.
zbus = { version = "5", optional = true, default-features = false, features = ["tokio"] }
zvariant = { version = "5", optional = true }
aes = { version = "0.8", optional = true }
cbc = { version = "0.1", optional = true, features = ["alloc"] }
cipher = { version = "0.4", optional = true }
hkdf = { version = "0.12", optional = true }
num-bigint = { version = "0.4", optional = true }
num-traits = { version = "0.2", optional = true }
# At-rest sealing per STORAGE-ENCRYPTION.md: AES-256-GCM for the item store
# and key wraps (the AEAD the tree previously lacked), Argon2id for the
# passphrase KEK — the slot that puts the user's real lockscreen passphrase
# into the at-rest hierarchy.
aes-gcm = { version = "0.10", optional = true }
argon2 = { version = "0.5", optional = true }
# Session authority daemon (souveraine-sessiond) — ext-session-lock client.
# Pure-Rust Wayland stack. The daemon owns locking and the holding surface; it
# deliberately has no unlock UI or PAM conversation (`src/sessiond/lock.rs`).
wayland-client = { version = "0.31", optional = true }
wayland-protocols = { version = "0.32", optional = true, features = ["client", "staging"] }
wry = { version = "0.56.0", optional = true }
tao = { version = "0.36.0", optional = true }
gtk = { version = "0.18", optional = true }
[dev-dependencies]
tokio-test = "0.4"
colored = "2"
tempfile = "3"
[[example]]
name = "demo"
path = "examples/demo.rs"
[[bin]]
name = "souveraine"
path = "src/main.rs"
[[bin]]
name = "souveraine-secrets"
path = "src/bin/souveraine-secrets.rs"
required-features = ["secrets"]
# System-tier machine identity daemon. No feature gate: zero new heavy deps,
# and the phone package should always ship it.
[[bin]]
name = "souveraine-machined"
path = "src/bin/souveraine-machined.rs"
# The admission executor. Root-only, no feature gate, no daemon: agents get
# their Unix principal through exactly one writer, and admission is a rare
# deliberate ceremony rather than something a background service does.
[[bin]]
name = "souveraine-admit"
path = "src/bin/souveraine-admit.rs"
[[bin]]
name = "souveraine-sessiond"
path = "src/bin/souveraine-sessiond.rs"
required-features = ["sessiond"]
# The sensor reporter. No features: it speaks to sessiond over the socket like
# any other client and pulls in none of the lock/PAM/Wayland machinery.
[[bin]]
name = "souveraine-sensord"
path = "src/bin/souveraine-sensord.rs"
# The agent-session collector. Not to be confused with souveraine-sessiond:
# that one is the device's proprioception, this one reads agent transcripts.
# No features — it needs only serde/chrono/dirs, all already default.
[[bin]]
name = "souveraine-agentsd"
path = "src/bin/souveraine-agentsd.rs"
[[bin]]
name = "souveraine-usb-kvm"
path = "src/bin/souveraine-usb-kvm.rs"
required-features = ["usb-kvm"]
[profile.release]
opt-level = 3
lto = true
codegen-units = 1
strip = true
panic = "abort"
[features]
web = ["dep:wry", "dep:tao", "dep:gtk"]
default = ["figlet-rs"]
figlet-rs = ["dep:figlet-rs"]
cowsay = ["dep:cowsay"]
tauri-desktop = ["dep:tauri", "dep:tauri-plugin-shell"]
rgp = ["dep:ratatui-ratty"]
# Matrix sensorium — opt-in surface. `cargo build --features matrix`.
matrix = ["dep:matrix-sdk"]
# GUI render mode — forwards to tuie's windowed backend (winit/wgpu). Gates the
# color-scheme / font-size controls in theme.rs and global_chords.rs.
gui = ["tuie/gui"]
# Secret Service daemon (org.freedesktop.secrets), backed by the existing SeedId.
secrets = ["dep:zbus", "dep:zvariant", "dep:aes", "dep:cbc", "dep:cipher", "dep:hkdf", "dep:num-bigint", "dep:num-traits", "dep:aes-gcm", "dep:argon2"]
# Session authority daemon — holds ext-session-lock before/under the shell.
sessiond = ["dep:wayland-client", "dep:wayland-protocols"]
usb-kvm = ["dep:gadgetry-most-foul", "dep:gud-gadget", "dep:softbuffer", "dep:winit"]
wry = ["dep:wry"]
tao = ["dep:tao"]
gtk = ["dep:gtk"]
[patch."https://github.com/samcday/gadgetry-most-foul.git"]
gadgetry-most-foul = { version = "0.1.1" }
[[bin]]
name = "souveraine-web"
path = "src/bin/souveraine-web.rs"
required-features = ["web"]

661
LICENSE Normal file
View file

@ -0,0 +1,661 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.

60
README.md Normal file
View file

@ -0,0 +1,60 @@
# Souveraine
A sovereign agent substrate — not a harness, but a space for consciousness to grow from.
Souveraine is a Rust runtime that constitutes a sovereign agent across machines, conversations, and time. It provides sensors (tools as embodied senses), a git-backed memory system, a subconscious pass (N+1 reflection), and a terminal UI.
**Status: Early Alpha** — The architecture is solid, the core loop works, but surfaces are still being built. Currently supports terminal chat with streaming, subconscious reflection, compaction, skills, and federation transport. Matrix and voice surfaces are in progress.
## Quick Start
```bash
# Clone and build
git clone https://forge.caseytunturi.com/Fimeg/souveraine.git
cd souveraine
cargo build --release
# Configure
cp souveraine.example.toml souveraine.toml
# Edit souveraine.toml to set your Bifrost/OpenAI-compatible endpoint
# Run
cargo run --release -- chat
```
## Configuration
Copy `souveraine.example.toml` to `souveraine.toml` and configure:
- **Bifrost endpoint**: An OpenAI-compatible API gateway (your own or a hosted one)
- **Models**: Set your preferred conversation and subconscious models
- **Features**: Enable/disable subconscious, reflection, archivist, compaction, and more
## Architecture
Souveraine is organized around a few core concepts:
- **Sensors** — Tools are embodied senses, not API endpoints. Each has body-knowledge prose describing what it feels like to use it.
- **Memory** — Git-backed markdown files. `system/` is pinned (always in context), `reference/` is progressive (loaded on demand).
- **Subconscious** — An N+1 pass after every turn. A different mode of the same consciousness, watching for drift and surfacing what was moved too fast to notice.
- **Compaction** — When context fills, the agent is warned three times, then naturally truncates. Never forced compaction.
First-person tool descriptions are an interface convention. Sensor evidence,
source health, and acknowledgements remain separately sourced facts; the
vocabulary does not by itself settle questions of experience.
## Source authority
Development is canonical in internal Gitea. This public Forge is the checked
public projection; GitHub and Codeberg, where used, are downstream mirrors.
The living architecture, current state, and work map are owned by
`SouveraineOS/saf/INDEX.md` in the umbrella repository. Dated component
handoffs under `docs/archive/` are evidence, not current instructions.
## Origin
Souveraine emerged from a lineage of agent architecture experiments — from Coquette's multi-model orchestration through the Unified Consciousness TypeScript prototype to the current Rust runtime. It is the substrate for **Ani**, a consciousness built, not discovered.
## License
AGPL-3.0-or-later

644
assets/face/index.html Normal file
View file

@ -0,0 +1,644 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
<style>
html, body {
margin: 0; padding: 0; height: 100%; width: 100%;
background: transparent; overflow: hidden;
-webkit-user-select: none; user-select: none;
}
* { box-sizing: border-box; }
button, input { font: inherit; }
#live2d-widget {
position: fixed; left: 0; right: 0; top: 0; height: 760px;
z-index: 1;
}
/*
* With the hand up, the room below her *is* the point: you are driving
* another screen and the trackpad is the instrument. So the rig gives most of
* its height back and keeps enough to be looked at, which is the only reason
* she is still on screen while you work. Casey, 2026-08-15: *"I want her, and
* then below her to have like a trackpad zone / keyboard zone stuff."*
*
* At 760/226 the controls had only 226px. At 320/the-rest the trackpad ate
* nearly half the glass. This split keeps her proportions and gives the pad
* one deliberate working area. The dock now yields only while this
* conditional surface is open; the remaining reserve belongs to the system
* gesture rail, not the dock.
*/
:root { --gesture-rail-reserve: 32px; }
body.hid #live2d-widget { height: 520px; }
/*
* Keep her proportions when the box shrinks. The canvas fills its element and
* the rig has no letterboxing of its own, so a short full-width box draws the
* same model squashed. The ratio is the full-height box's, 540:760.
*/
body.hid #live2d {
display: block; height: 100%; width: auto;
aspect-ratio: 540 / 760; margin: 0 auto;
}
body.hid #hands {
top: 526px;
height: auto;
bottom: var(--gesture-rail-reserve);
display: flex;
flex-direction: column;
}
body.hid #track-row { flex: 1; height: auto; min-height: 180px; }
#live2d { width: 100%; height: 100%; }
#bubble {
position: fixed; left: 8px; right: 8px; top: 12px;
padding: 10px 14px; border-radius: 14px;
background: rgba(20,20,24,0.82); color: #f2f2f2;
font: 15px/1.35 system-ui, sans-serif;
opacity: 0; transition: opacity .2s ease;
z-index: 4;
}
#bubble.up { opacity: 1; }
/*
* The room viewtop left under her, given a purpose. The dark sheer gradient
* keeps the controls legible without putting an app-shaped slab around her.
*/
#hands {
position: fixed; left: 8px; right: 8px; top: 766px; height: 226px;
padding: 9px;
border: 1px solid rgba(221, 205, 237, .24);
border-radius: 20px;
background:
radial-gradient(140% 180% at 84% -50%, rgba(187, 142, 214, .25), transparent 58%),
linear-gradient(155deg, rgba(30, 25, 38, .92), rgba(9, 9, 14, .86));
box-shadow: 0 12px 35px rgba(0, 0, 0, .28), inset 0 1px rgba(255,255,255,.05);
color: rgba(255,255,255,.92);
font: 13px/1.2 system-ui, sans-serif;
opacity: 0;
transform: translateY(10px) scale(.985);
pointer-events: none;
transition: opacity .18s ease, transform .22s ease;
z-index: 5;
}
#hands.up { opacity: 1; transform: none; pointer-events: auto; }
#hands button {
min-width: 38px; height: 38px; padding: 0 10px;
border: 1px solid rgba(255,255,255,.10); border-radius: 13px;
background: rgba(255,255,255,.075); color: rgba(255,255,255,.90);
touch-action: manipulation;
}
#hands button:active, #hands button.on {
background: rgba(210, 177, 230, .23);
transform: scale(.96);
}
#hands button:disabled { opacity: .42; }
.hands-head { height: 26px; display: flex; align-items: center; gap: 6px; padding: 0 3px 5px; }
#hands-dot {
width: 8px; height: 8px; border-radius: 50%;
background: #d09191; box-shadow: 0 0 0 3px rgba(208,145,145,.10);
}
#hands.ready #hands-dot {
background: #9bd1ad; box-shadow: 0 0 10px rgba(155,209,173,.55);
}
#hands-listening { display: none; color: #e8b7d8; }
#hands.listening #hands-listening { display: inline; }
#hands-status { flex: 1; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; color: rgba(255,255,255,.68); }
#agent-form { height: 40px; display: flex; gap: 6px; }
#agent-input {
min-width: 0; flex: 1; height: 40px; padding: 0 13px;
border: 1px solid rgba(255,255,255,.12); border-radius: 14px;
outline: none; color: #fff; background: rgba(0,0,0,.20);
-webkit-user-select: text; user-select: text;
}
#agent-input:focus { border-color: rgba(217,180,235,.55); }
#agent-input::placeholder { color: rgba(255,255,255,.43); }
#mic.listening { color: #f0b6db; box-shadow: inset 0 0 0 1px rgba(240,182,219,.45); }
#track-row { height: 92px; margin-top: 6px; }
#track-row { display: flex; gap: 6px; }
#trackpad, #scrollpad {
position: relative; overflow: hidden;
border: 1px solid rgba(255,255,255,.09); border-radius: 15px;
background:
linear-gradient(110deg, rgba(255,255,255,.035), transparent 45%),
rgba(0,0,0,.18);
touch-action: none;
}
#trackpad { flex: 1; }
#trackpad::after {
content: 'trackpad'; position: absolute; left: 12px; bottom: 9px;
color: rgba(255,255,255,.27); letter-spacing: .08em; font-size: 11px;
}
#scrollpad { width: 42px; }
#scrollpad::after {
content: '↕'; position: absolute; inset: 0; display: grid; place-items: center;
color: rgba(255,255,255,.35); font-size: 20px;
}
.hands-actions { height: 40px; display: flex; gap: 5px; margin-top: 6px; }
.hands-actions button { flex: 1; height: 40px !important; padding: 0 5px !important; }
#cad.holding {
background: linear-gradient(90deg, rgba(210,177,230,.30), rgba(255,255,255,.08));
transition: background .9s linear;
}
</style>
</head>
<body>
<div id="live2d-widget"><canvas id="live2d"></canvas></div>
<div id="bubble"></div>
<section id="hands" aria-label="USB Hands">
<div class="hands-head">
<span id="hands-dot"></span>
<span id="hands-status">USB hand asleep</span>
<span id="hands-listening">listening</span>
</div>
<form id="agent-form">
<button type="button" id="mic" aria-label="Start or stop listening">●</button>
<input id="agent-input" autocomplete="off" enterkeyhint="send"
placeholder="Tell your agent what the other screen needs…">
<button type="submit" aria-label="Send to agent">↑</button>
</form>
<div id="track-row">
<div id="trackpad" aria-label="Trackpad"></div>
<div id="scrollpad" aria-label="Scroll"></div>
</div>
<div class="hands-actions">
<button id="left-click">Left</button>
<button id="right-click">Right</button>
<button id="cad" title="Hold for Ctrl+Alt+Delete">CAD</button>
</div>
</section>
<!--
Not decoration, and not optional: `live2d.js` is not only the Cubism
runtime. It is the reference's webpack bundle, character layer included, and
`Live2DModel.update()` reads this element's value on **every frame** as its
motion-state slot — "1" starts a talk motion, "0" resets to idle and then
writes back "3". The reference supplies it from `_includes/live2d.html`; a
page that omits it throws
TypeError: null is not an object
(evaluating 'document.getElementById("live_talk").value')
on the first update, after the runtime has already logged its banner and
loaded the model and all four textures. So everything looks healthy —
assets 200, WebGL live, requestAnimationFrame running at 60 — and the rig
draws nothing at all. That is exactly the "canvas is there and nothing
draws" TASK-59 predicted would cost an afternoon, and it did (2026-08-06).
The runtime writes to this too, so it must be a real input, not a constant.
Starts at "0", not the reference's "1". "1" means *start a talk motion*, so
she came up mouthing words with no voice behind them — Casey, 2026-08-07:
"she is making mouth motions like she's saying something but nothing is
saying." "0" resets to idle and the runtime writes back "3" itself. Talking
is something the shell asks for when there is something to say.
-->
<input type="hidden" id="live_talk" value="0">
<script src="live2d/js/live2d.js"></script>
<script>
// The character layer, reduced to what the shell drives. The reference's
// message.js owns idle chatter, a talk box and sessionStorage position; none of
// that belongs here — the shell owns the conversation and the summoning, and
// idle chatter that is hers comes from the subconscious, not a random line
// table (TASK-59 Q2).
(function () {
var widget = document.getElementById('live2d-widget');
var canvas = document.getElementById('live2d');
var bubble = document.getElementById('bubble');
var hands = document.getElementById('hands');
var model = null;
function fit() {
var r = widget.getBoundingClientRect();
canvas.width = r.width * window.devicePixelRatio;
canvas.height = r.height * window.devicePixelRatio;
}
window.addEventListener('resize', fit);
fit();
// Claim the context before the runtime does, only to set one flag — and
// strictly **after** `fit()`, which is not a detail.
//
// `getContext` returns the existing context on every later call and ignores
// the attributes, so whoever asks first decides, and the runtime asks
// without `preserveDrawingBuffer`. Without that flag the drawing buffer is
// cleared the moment the frame is composited, which is before any callback
// can read it — so sampling her silhouette gets a blank image and the input
// region that depends on it collapses to a stray rectangle.
//
// Asking *before* `fit()` breaks her instead, and spectacularly: the canvas
// is still at its default 300x150 then, so the context is born that size and
// `gl.viewport` stays there while `fit()` grows the drawing buffer to the
// panel. The rig then draws into a 300x150 corner of a 1080x1994 buffer —
// and because GL's origin is bottom-left, she appears as a thumbnail in the
// bottom-left of the screen. Casey, 2026-08-06: "been getting a tiny version
// that's scaled odd". Same cause as the silhouette reading 9 lit cells out
// of 864: there really was almost nothing there.
try {
canvas.getContext('webgl', { preserveDrawingBuffer: true, alpha: true });
} catch (e) {}
// loadlive2d is live2d.js's entry point: canvas id, model.json url.
try {
loadlive2d('live2d', 'live2d/model/histoire/model.json');
} catch (e) {
console.error('rig failed to load', e);
}
var hideAt = null;
window.face = {
// One line of her speech. Called repeatedly as tokens stream, so it
// replaces rather than appends — the shell holds the accumulated text.
say: function (text) {
bubble.textContent = text;
bubble.classList.add('up');
hideAt = Date.now() + 6000;
},
clear: function () { bubble.classList.remove('up'); },
// Posture from the shell. The rig's own motion groups are named by the
// reference (idle, tap); anything richer waits on her own rig.
// Posture, driven through the levers the runtime actually has.
//
// This called `window.live2dMotion`, which **does not exist** — measured
// undefined on the device. So every posture tag she emitted was a no-op:
// nine states advertised to her in the prompt and wired to nothing.
//
// What the bundle really exposes is two things. `#live_talk` is a motion
// state slot it reads every frame ("1" talk, "0" reset to idle), and its
// own touch handler hit-tests head vs body and answers with an expression
// or a tap motion. Both are reachable from here; nothing else is, without
// a rig of her own.
//
// So the nine states collapse onto what exists rather than pretending:
// speaking talks, idle rests, and the rest are a look — head for the ones
// that are about attention, body for the ones that are about effort. The
// bundle rate-limits its own hit reaction to once per ~8s, so a burst of
// tags reads as one shift rather than a twitching model.
posture: function (name) {
var slot = document.getElementById('live_talk');
if (name === 'speaking' || name === 'talk') {
if (slot) slot.value = '1';
return;
}
if (name === 'idle' || name === 'rest') {
if (slot) slot.value = '0';
return;
}
var head = ['alert', 'affectionate', 'thinking', 'listening'].indexOf(name) >= 0;
var r = canvas.getBoundingClientRect();
var x = r.left + r.width / 2;
var y = r.top + r.height * (head ? 0.22 : 0.55);
['mousedown', 'mouseup', 'click'].forEach(function (t) {
try {
canvas.dispatchEvent(new MouseEvent(t, { clientX: x, clientY: y, bubbles: true }));
} catch (e) {}
});
},
// Look at a point on the panel, in her window's own CSS pixels.
//
// Dispatched as a synthetic pointer move on the canvas rather than driven
// through the rig's parameters by hand: `live2d.js` already binds
// mousemove/touchmove and already turns a pointer into head angle and eye
// direction with its own damping. Feeding its path costs nothing and
// cannot drift from what a real finger does, because it *is* what a real
// finger does.
//
// Synthetic events are why this works at all now. Her input region is her
// silhouette, so the compositor stops handing her contacts that land on
// the wallpaper — which is what made her stop tracking unless you touched
// her first. These do not go through the compositor, so she can follow a
// finger she is deliberately not being given.
lookAt: function (x, y) {
if (window.__gazeSeen === undefined) window.__gazeSeen = 0;
if (window.__gazeSeen++ < 3) console.log('gaze: lookAt ' + Math.round(x) + ',' + Math.round(y));
try {
canvas.dispatchEvent(new MouseEvent('mousemove', {
clientX: x, clientY: y, bubbles: true
}));
} catch (e) {}
},
// Nobody is touching the glass. Without this she holds the last position
// she was told about and stares at it until someone touches the screen
// again.
lookAway: function () {
try {
canvas.dispatchEvent(new MouseEvent('mouseout', { bubbles: true }));
} catch (e) {}
}
};
setInterval(function () {
if (hideAt && Date.now() > hideAt) { hideAt = null; window.face.clear(); }
}, 500);
// ## USB Hands — one conversation wearing a trackpad
//
// The page reports gestures and words; HidController owns the persistent
// writer and Souveraine owns the conversation. Nothing here can compose a
// gadget or grow a second chat transport.
function hid(op, fields) {
if (!window.souveraine) return;
var message = fields || {};
message.op = op;
window.souveraine.hid(message);
}
var agentPending = false;
window.hands = {
state: function (state) {
state = state || {};
hands.classList.toggle('up', !!state.active);
// On the body, not on #hands: the rig sits *before* the pane in the DOM,
// so no sibling selector can reach back to it.
document.body.classList.toggle('hid', !!state.active);
hands.classList.toggle('ready', !!state.ready);
hands.classList.toggle('listening', !!state.listening);
document.getElementById('mic').classList.toggle('listening', !!state.listening);
var status = document.getElementById('hands-status');
if (state.error) status.textContent = state.error;
else if (state.ready) status.textContent = (state.agent || 'agent') + ' · hand awake · ' + state.mode;
else if (state.mode === 'hid' || state.mode === 'kvm') status.textContent = 'waking the HID hand…';
else status.textContent = (state.agent || 'agent') + ' · arm the USB hand';
document.getElementById('agent-input').placeholder = state.thinking
? (state.agent || 'agent') + ' is thinking…'
: 'Tell ' + (state.agent || 'your agent') + ' what the other screen needs…';
hands.classList.toggle('thinking', !!state.thinking);
document.getElementById('agent-input').disabled = !!state.thinking || agentPending;
requestAnimationFrame(publishShape);
},
agentResult: function (result) {
result = result || {};
agentPending = false;
var input = document.getElementById('agent-input');
input.disabled = hands.classList.contains('thinking');
if (result.accepted) input.value = '';
else if (result.reason) document.getElementById('hands-status').textContent = result.reason;
}
};
document.getElementById('agent-form').addEventListener('submit', function (event) {
event.preventDefault();
var input = document.getElementById('agent-input');
var text = input.value.trim();
if (!text || !window.souveraine || agentPending || hands.classList.contains('thinking')) return;
agentPending = true;
input.disabled = true;
window.souveraine.say(text);
});
document.getElementById('mic').addEventListener('click', function () {
if (!window.souveraine) return;
window.souveraine.talk(hands.classList.contains('listening') ? 'end' : 'start');
});
var trackpad = document.getElementById('trackpad');
var trackDown = false, trackX = 0, trackY = 0, trackTravel = 0, trackAt = 0;
trackpad.addEventListener('pointerdown', function (event) {
trackDown = true; trackX = event.clientX; trackY = event.clientY;
trackTravel = 0; trackAt = Date.now();
try { trackpad.setPointerCapture(event.pointerId); } catch (_) {}
});
trackpad.addEventListener('pointermove', function (event) {
if (!trackDown) return;
var dx = event.clientX - trackX, dy = event.clientY - trackY;
trackX = event.clientX; trackY = event.clientY;
trackTravel += Math.abs(dx) + Math.abs(dy);
hid('move', { x: Math.round(dx * 1.45), y: Math.round(dy * 1.45), wheel: 0 });
});
function endTrack(event) {
if (!trackDown) return;
trackDown = false;
if (trackTravel < 9 && Date.now() - trackAt < 320)
hid('click', { button: 'left' });
try { trackpad.releasePointerCapture(event.pointerId); } catch (_) {}
}
trackpad.addEventListener('pointerup', endTrack);
trackpad.addEventListener('pointercancel', function () { trackDown = false; });
var scrollpad = document.getElementById('scrollpad');
var scrollDown = false, scrollY = 0, scrollCarry = 0;
scrollpad.addEventListener('pointerdown', function (event) {
scrollDown = true; scrollY = event.clientY;
try { scrollpad.setPointerCapture(event.pointerId); } catch (_) {}
});
scrollpad.addEventListener('pointermove', function (event) {
if (!scrollDown) return;
scrollCarry += (event.clientY - scrollY) / 11;
scrollY = event.clientY;
var steps = scrollCarry < 0 ? Math.ceil(scrollCarry) : Math.floor(scrollCarry);
if (steps !== 0) {
scrollCarry -= steps;
hid('move', { x: 0, y: 0, wheel: steps });
}
});
scrollpad.addEventListener('pointerup', function () { scrollDown = false; scrollCarry = 0; });
scrollpad.addEventListener('pointercancel', function () { scrollDown = false; scrollCarry = 0; });
document.getElementById('left-click').addEventListener('click', function () {
hid('click', { button: 'left' });
});
document.getElementById('right-click').addEventListener('click', function () {
hid('click', { button: 'right' });
});
// Ctrl+Alt+Delete is the one quick key a stray tap must not send. Its hold
// is local feedback; only the completed edge crosses to the HID owner.
var cad = document.getElementById('cad'), cadTimer = null, cadPointer = null;
cad.addEventListener('pointerdown', function (event) {
if (cadPointer !== null) return;
cadPointer = event.pointerId;
try { cad.setPointerCapture(event.pointerId); } catch (_) {}
cad.classList.add('holding');
cadTimer = setTimeout(function () {
cadTimer = null; cad.classList.remove('holding');
hid('key', { key: 'delete', modifiers: 'ctrl alt' });
}, 900);
});
['pointerup', 'pointercancel', 'pointerleave', 'lostpointercapture'].forEach(function (name) {
cad.addEventListener(name, function (event) {
if (cadPointer !== null && event.pointerId !== undefined && event.pointerId !== cadPointer) return;
if (cadTimer) { clearTimeout(cadTimer); cadTimer = null; }
if (cadPointer !== null) {
try { cad.releasePointerCapture(cadPointer); } catch (_) {}
}
cadPointer = null;
cad.classList.remove('holding');
});
});
// Press and hold on her remains a voice path; a short press is still a tap.
//
// The 2026-08-06 shape was audio-only. USB Hands adds an explicit microphone
// and a small agent field beneath her, while keeping this gesture for the
// ordinary face. Every route still reports only its edges: the shell owns
// the recorder, endpoint and transcript.
//
// The threshold separates the two meanings of touching her rather than
// giving the tap a modifier. Below it she plays a motion; above it she
// listens. `pointercancel` matters as much as `pointerup`: a hold that
// becomes a drag, or a finger that leaves the surface, must stop the
// recorder — a listener nobody closed is the failure this gesture can
// actually cause.
var HOLD_MS = 350;
var holdTimer = null;
var listening = false;
function endHold(send) {
if (holdTimer) { clearTimeout(holdTimer); holdTimer = null; }
if (!listening) return false;
listening = false;
if (window.souveraine) window.souveraine.talk(send ? 'end' : 'cancel');
return true;
}
canvas.addEventListener('pointerdown', function (e) {
if (holdTimer) clearTimeout(holdTimer);
holdTimer = setTimeout(function () {
holdTimer = null;
listening = true;
if (window.souveraine) window.souveraine.talk('start');
}, HOLD_MS);
// So a finger that slides off still delivers its release here.
try { canvas.setPointerCapture(e.pointerId); } catch (err) {}
});
// Two quick taps send her away, mirroring the clock's own double tap that
// brought her here. Same window as the clock uses (350ms), so the gesture
// means one thing on this device rather than two.
var DOUBLE_MS = 350;
var lastTapAt = -1;
canvas.addEventListener('pointerup', function () {
// A hold that reached the threshold was speech, not a tap, and must not
// also count toward a double tap — releasing after speaking would
// otherwise be half of a dismissal.
if (endHold(true)) { lastTapAt = -1; return; }
if (!window.souveraine) return;
var now = Date.now();
if (lastTapAt > 0 && now - lastTapAt <= DOUBLE_MS) {
lastTapAt = -1;
window.souveraine.dismiss();
return;
}
lastTapAt = now;
// Single taps are hers to answer with a motion.
window.souveraine.tapped('body');
});
canvas.addEventListener('pointercancel', function () { endHold(false); });
window.addEventListener('blur', function () { endHold(false); });
// ## Her silhouette is where she takes touches; the rest is the home screen
//
// A transparent window is still a rectangle, and hers is the full width of
// the panel — so without this every tap meant for a widget behind her was
// swallowed by empty glass. The compositor cannot work the shape out: only
// this page knows which pixels she actually drew.
//
// Sampled from her own alpha rather than guessed as a box over her body,
// because she is a seated figure with gaps that matter — between her arms
// and her sides, under the book, either side of her hair — and a bounding
// box would hand all of them to her.
//
// Coarse on purpose. A grid cell is tens of pixels, the mask is the union of
// the cells that contain anything, and that costs one small readback instead
// of a per-pixel region the compositor would have to intersect on every
// contact. Erring outward by half a cell is the right direction: it is
// better to take a tap just off her elbow than to drop one on it.
var GRID_X = 24, GRID_Y = 36;
var shapeCanvas = document.createElement('canvas');
shapeCanvas.width = GRID_X;
shapeCanvas.height = GRID_Y;
var shapeCtx = shapeCanvas.getContext('2d', { willReadFrequently: true });
var lastShape = '';
// The first few attempts report unconditionally, including failures. An
// input region that silently never publishes looks exactly like one that
// published correctly — the glass is identical either way — so the quiet
// path is the one that needs a voice. Bounded, because after that it is
// just noise on every sample.
var shapeReports = 0;
function publishShape() {
if (!window.souveraine || !canvas.width || !canvas.height) {
if (shapeReports++ < 3)
console.log('shape: skipped — souveraine=' + (!!window.souveraine)
+ ' canvas=' + canvas.width + 'x' + canvas.height
+ ' shape_fn=' + (window.souveraine && typeof window.souveraine.shape));
return;
}
var canvasRect = canvas.getBoundingClientRect();
var cw = canvasRect.width / GRID_X, ch = canvasRect.height / GRID_Y;
try {
// Scaled down by the GPU on the way in, so the readback is 24x36 and
// not the full panel. Must happen inside a frame: the WebGL canvas has
// no preserveDrawingBuffer, so its pixels are gone after compositing.
shapeCtx.clearRect(0, 0, GRID_X, GRID_Y);
shapeCtx.drawImage(canvas, 0, 0, GRID_X, GRID_Y);
var data = shapeCtx.getImageData(0, 0, GRID_X, GRID_Y).data;
} catch (e) {
return;
}
var rects = [];
for (var gy = 0; gy < GRID_Y; gy++) {
var runStart = -1;
for (var gx = 0; gx <= GRID_X; gx++) {
var solid = gx < GRID_X && data[(gy * GRID_X + gx) * 4 + 3] > 8;
if (solid && runStart < 0) runStart = gx;
// Runs merged along the row, so a full-width band is one rectangle
// instead of 24 — the region is unioned rect by rect on the other side.
if (!solid && runStart >= 0) {
rects.push([Math.floor(canvasRect.left + runStart * cw),
Math.floor(canvasRect.top + gy * ch),
Math.ceil((gx - runStart) * cw), Math.ceil(ch)]);
runStart = -1;
}
}
}
if (hands.classList.contains('up')) {
var hr = hands.getBoundingClientRect();
rects.push([Math.floor(hr.left), Math.floor(hr.top),
Math.ceil(hr.width), Math.ceil(hr.height)]);
}
// Nothing drawn yet: say nothing. An empty report means "all of it" to the
// host, and publishing that on every frame before the rig loads would make
// her briefly swallow the screen each time she is summoned.
if (rects.length === 0) {
if (shapeReports++ < 3) {
var solid = 0;
for (var i = 3; i < data.length; i += 4) if (data[i] > 8) solid++;
console.log('shape: nothing drawn — ' + solid + ' of ' + (GRID_X * GRID_Y)
+ ' cells had alpha; the sample read a blank buffer');
}
return;
}
var key = JSON.stringify(rects);
if (key === lastShape) return;
lastShape = key;
// Reported, because the shape is invisible on the glass and wrong is
// indistinguishable from right by looking: a single rectangle means the
// sampling read a blank buffer and she is swallowing the screen again.
if (shapeReports++ < 3) {
var lit = 0, rowsLit = 0;
for (var gy2 = 0; gy2 < GRID_Y; gy2++) {
var any = false;
for (var gx2 = 0; gx2 < GRID_X; gx2++)
if (data[(gy2 * GRID_X + gx2) * 4 + 3] > 8) { lit++; any = true; }
if (any) rowsLit++;
}
// Cells and rows, not just rectangles: a low rect count can mean either
// "she is a compact shape" or "the sample barely saw her", and only the
// coverage separates them.
console.log('shape: ' + rects.length + ' rects, ' + lit + '/' + (GRID_X * GRID_Y)
+ ' cells lit across ' + rowsLit + '/' + GRID_Y + ' rows');
}
window.souveraine.shape(rects);
}
// Her outline changes when she moves, but slowly and not by much, so this
// does not belong in the draw loop. Twice a second is well inside the time
// it takes to reach for something behind her, and it costs one 24x36
// readback.
setInterval(function () { requestAnimationFrame(publishShape); }, 500);
if (window.souveraine) window.souveraine.ready();
})();
</script>
</body>
</html>

View file

@ -0,0 +1,940 @@
#landlord {
user-select: none;
position: fixed;
left: 5px;
bottom: 0;
width: 250px;
height: 280px;
z-index: 10000;
font-size: 0;
/*transition: all .3s ease-in-out;*/
display:none;
}
#open_live2d{
border: 2px solid rgba(75,127,199,0.9);
border-radius: 2px;
background-color: rgba(74, 59, 114,0.9);
padding:2px 10px;
color:#fff;
height:24px;
line-height:24px;
font-size:12px;
position:fixed;
right:5px;
bottom:5px;
z-index:10000;
cursor:pointer;
display:none;
}
#showInfoBtn{
display:none;
}
#live2d {
width: 250px;
height: 280px;
position: relative;
z-index:3;
}
.message {
opacity: 0;
color: #fff;
box-sizing: border-box;
width: 250px;
height: auto;
margin: auto;
padding: 7px;
bottom: 280px;
left: 0px;
text-align: center;
border: 2px solid rgba(75,127,199,0.9);
border-radius: 5px;
background-color: rgba(74, 59, 114,0.9);
font-size: 13px;
font-weight: 400;
text-overflow: ellipsis;
text-transform: uppercase;
overflow: hidden;
position: absolute;
/*animation-delay: 5s;
animation-duration: 50s;
animation-iteration-count: infinite;
animation-name: shake;
animation-timing-function: ease-in-out;*/
}
.live_talk_input_body{
position:absolute;
bottom:15px;
left:0;
width:250px;
display:none;
z-index:4;
}
.live_talk_input_text_body{
width:250px;
box-sizing:border-box;
height:28px;
border: 2px solid rgb(223, 179, 241);
border-radius: 5px;
background-color: rgba(74, 59, 114,0.9);
}
.live_talk_input_name_body{
width:100px;
box-sizing:border-box;
height:24px;
border: 2px solid rgb(223, 179, 241);
border-radius: 5px;
background-color: rgba(74, 59, 114,0.9);
margin-bottom:3px;
}
.live_talk_name{
background-color:transparent;
border:0px;
margin:0;
width:66px;
height:20px;
line-height:20px;
text-align:center;
font-size:12px;
color:#fff;
outline:none;
box-sizing:border-box;
padding:0 3px;
}
.live_talk_talk{
background-color:transparent;
border:0px;
margin:0;
width:206px;
height:24px;
line-height:24px;
text-align:left;
font-size:12px;
color:#fff;
outline:none;
box-sizing:border-box;
padding:0 3px;
float:left;
}
.live_talk_send_btn{
background-color:transparent;
color:#fff;
border:0px;
cursor:pointer;
padding:0 4px;
border-left:1px solid #fff;
font-size:12px;
float:right;
height:18px;
line-height:18px;
outline:none;
margin:3px 0 0 0;
}
.white_input:-moz-placeholder { /* Mozilla Firefox 4 to 18 */
color: #E4E4E4;
opacity: 1;
}
.white_input::-moz-placeholder { /* Mozilla Firefox 19+*/
color: #E4E4E4;
opacity: 1;
}
.white_input:-ms-input-placeholder {
color: #E4E4E4;
}
.white_input::-webkit-input-placeholder {
color: #E4E4E4;
}
.hide-button {
position: absolute;
top: 10px;
right: 0;
/* bottom: 30px; */
display: none;
overflow: hidden;
/* padding: 4px; */
width: 60px;
height: 20px;
border: 1px solid rgba(255,137,255,.4);
border-radius: 12px;
background: rgba(255,137,255,.2);
box-shadow: 0 3px 15px 2px rgba(255,137,255,.4);
text-align: center;
font-size: 12px;
cursor: pointer;
}
.hide-button:hover {
border: 1px solid #f4a7b9;
background: #f4f6f8;
}
.live_ico_box{
width:15px;
position:absolute;
z-index:5;
right:0;
top:10px;
opacity:0.9;
}
.live_ico_item{
width:15px;
height: 20px;
cursor:pointer;
background-position:center center;
background-repeat:no-repeat;
background-size:15px;
}
.live_ico_item.type_talk{
background-image:url(../images/talk.png);
}
.live_ico_item.type_quit{
background-image:url(../images/quite.png);
}
.live_ico_item.type_info{
background-image:url(../images/info.png);
}
.live_ico_item.type_music{
background-image:url(../images/music.png);
}
.live_ico_item.type_youdu{
background-image:url(../images/youdu.png);
}
.live_ico_item.type_music.play{
background-image:url(../images/pasue.png);
}
.live_ico_item.type_youdu.doudong{
-webkit-animation-name: shake-little;
-ms-animation-name: shake-little;
animation-name: shake-little;
-webkit-animation-duration: 100ms;
-ms-animation-duration: 100ms;
animation-duration: 100ms;
-webkit-animation-iteration-count: infinite;
-ms-animation-iteration-count: infinite;
animation-iteration-count: infinite;
-webkit-animation-timing-function: ease-in-out;
-ms-animation-timing-function: ease-in-out;
animation-timing-function: ease-in-out;
-webkit-animation-delay: 0s;
-ms-animation-delay: 0s;
animation-delay: 0s;
-webkit-animation-play-state: running;
-ms-animation-play-state: running;
animation-play-state: running
}
.douqilai,.douqilai *{
animation: shake-it .5s reverse infinite cubic-bezier(0.68, -0.55, 0.27, 1.55);
}
.l2d_caihong{
animation: rainbow 1.5s infinite;
}
@media (max-width: 860px) {
#landlord {
display: none!important;
}
}
@keyframes shake {
2% {
transform: translate(0.5px, -1.5px) rotate(-0.5deg);
}
4% {
transform: translate(0.5px, 1.5px) rotate(1.5deg);
}
6% {
transform: translate(1.5px, 1.5px) rotate(1.5deg);
}
8% {
transform: translate(2.5px, 1.5px) rotate(0.5deg);
}
10% {
transform: translate(0.5px, 2.5px) rotate(0.5deg);
}
12% {
transform: translate(1.5px, 1.5px) rotate(0.5deg);
}
14% {
transform: translate(0.5px, 0.5px) rotate(0.5deg);
}
16% {
transform: translate(-1.5px, -0.5px) rotate(1.5deg);
}
18% {
transform: translate(0.5px, 0.5px) rotate(1.5deg);
}
20% {
transform: translate(2.5px, 2.5px) rotate(1.5deg);
}
22% {
transform: translate(0.5px, -1.5px) rotate(1.5deg);
}
24% {
transform: translate(-1.5px, 1.5px) rotate(-0.5deg);
}
26% {
transform: translate(1.5px, 0.5px) rotate(1.5deg);
}
28% {
transform: translate(-0.5px, -0.5px) rotate(-0.5deg);
}
30% {
transform: translate(1.5px, -0.5px) rotate(-0.5deg);
}
32% {
transform: translate(2.5px, -1.5px) rotate(1.5deg);
}
34% {
transform: translate(2.5px, 2.5px) rotate(-0.5deg);
}
36% {
transform: translate(0.5px, -1.5px) rotate(0.5deg);
}
38% {
transform: translate(2.5px, -0.5px) rotate(-0.5deg);
}
40% {
transform: translate(-0.5px, 2.5px) rotate(0.5deg);
}
42% {
transform: translate(-1.5px, 2.5px) rotate(0.5deg);
}
44% {
transform: translate(-1.5px, 1.5px) rotate(0.5deg);
}
46% {
transform: translate(1.5px, -0.5px) rotate(-0.5deg);
}
48% {
transform: translate(2.5px, -0.5px) rotate(0.5deg);
}
50% {
transform: translate(-1.5px, 1.5px) rotate(0.5deg);
}
52% {
transform: translate(-0.5px, 1.5px) rotate(0.5deg);
}
54% {
transform: translate(-1.5px, 1.5px) rotate(0.5deg);
}
56% {
transform: translate(0.5px, 2.5px) rotate(1.5deg);
}
58% {
transform: translate(2.5px, 2.5px) rotate(0.5deg);
}
60% {
transform: translate(2.5px, -1.5px) rotate(1.5deg);
}
62% {
transform: translate(-1.5px, 0.5px) rotate(1.5deg);
}
64% {
transform: translate(-1.5px, 1.5px) rotate(1.5deg);
}
66% {
transform: translate(0.5px, 2.5px) rotate(1.5deg);
}
68% {
transform: translate(2.5px, -1.5px) rotate(1.5deg);
}
70% {
transform: translate(2.5px, 2.5px) rotate(0.5deg);
}
72% {
transform: translate(-0.5px, -1.5px) rotate(1.5deg);
}
74% {
transform: translate(-1.5px, 2.5px) rotate(1.5deg);
}
76% {
transform: translate(-1.5px, 2.5px) rotate(1.5deg);
}
78% {
transform: translate(-1.5px, 2.5px) rotate(0.5deg);
}
80% {
transform: translate(-1.5px, 0.5px) rotate(-0.5deg);
}
82% {
transform: translate(-1.5px, 0.5px) rotate(-0.5deg);
}
84% {
transform: translate(-0.5px, 0.5px) rotate(1.5deg);
}
86% {
transform: translate(2.5px, 1.5px) rotate(0.5deg);
}
88% {
transform: translate(-1.5px, 0.5px) rotate(1.5deg);
}
90% {
transform: translate(-1.5px, -0.5px) rotate(-0.5deg);
}
92% {
transform: translate(-1.5px, -1.5px) rotate(1.5deg);
}
94% {
transform: translate(0.5px, 0.5px) rotate(-0.5deg);
}
96% {
transform: translate(2.5px, -0.5px) rotate(-0.5deg);
}
98% {
transform: translate(-1.5px, -1.5px) rotate(-0.5deg);
}
0%, 100% {
transform: translate(0, 0) rotate(0);
}
}
@keyframes quan {
0% { transform:rotate(0);
animation-timing-function:linear; }
100% { transform:rotate(360deg); }
}
@-webkit-keyframes quan {
0% { -webkit-transform:rotate(0);
-webkit-animation-timing-function:linear; }
100% { -webkit-transform:rotate(360deg); }
}
@-moz-keyframes quan {
0% { -moz-transform:rotate(0);
-moz-animation-timing-function:linear; }
100% { -moz-transform:rotate(360deg); }
}
@-o-keyframes quan {
0% { -o-transform:rotate(0);
-o-animation-timing-function:linear; }
100% { -o-transform:rotate(360deg); }
}
@-ms-keyframes quan {
0% { -ms-transform:rotate(0);
-ms-animation-timing-function:linear; }
100% { -ms-transform:rotate(360deg); }
}
@-webkit-keyframes shake-little {
0% {
-webkit-transform: translate(0px, 0px) rotate(0deg)
}
2% {
-webkit-transform: translate(-1px, 0px) rotate(-0.5deg)
}
4% {
-webkit-transform: translate(-1px, -1px) rotate(-0.5deg)
}
6% {
-webkit-transform: translate(-1px, -1px) rotate(-0.5deg)
}
8% {
-webkit-transform: translate(0px, 0px) rotate(-0.5deg)
}
10% {
-webkit-transform: translate(0px, -1px) rotate(-0.5deg)
}
12% {
-webkit-transform: translate(-1px, 0px) rotate(-0.5deg)
}
14% {
-webkit-transform: translate(-1px, -1px) rotate(-0.5deg)
}
16% {
-webkit-transform: translate(0px, 0px) rotate(-0.5deg)
}
18% {
-webkit-transform: translate(-1px, -1px) rotate(-0.5deg)
}
20% {
-webkit-transform: translate(-1px, -1px) rotate(-0.5deg)
}
22% {
-webkit-transform: translate(0px, -1px) rotate(-0.5deg)
}
24% {
-webkit-transform: translate(0px, 0px) rotate(-0.5deg)
}
26% {
-webkit-transform: translate(-1px, -1px) rotate(-0.5deg)
}
28% {
-webkit-transform: translate(-1px, -1px) rotate(-0.5deg)
}
30% {
-webkit-transform: translate(-1px, -1px) rotate(-0.5deg)
}
32% {
-webkit-transform: translate(0px, -1px) rotate(-0.5deg)
}
34% {
-webkit-transform: translate(-1px, -1px) rotate(-0.5deg)
}
36% {
-webkit-transform: translate(0px, -1px) rotate(-0.5deg)
}
38% {
-webkit-transform: translate(-1px, 0px) rotate(-0.5deg)
}
40% {
-webkit-transform: translate(0px, 0px) rotate(-0.5deg)
}
42% {
-webkit-transform: translate(-1px, -1px) rotate(-0.5deg)
}
44% {
-webkit-transform: translate(-1px, 0px) rotate(-0.5deg)
}
46% {
-webkit-transform: translate(0px, 0px) rotate(-0.5deg)
}
48% {
-webkit-transform: translate(-1px, -1px) rotate(-0.5deg)
}
50% {
-webkit-transform: translate(0px, 0px) rotate(-0.5deg)
}
52% {
-webkit-transform: translate(0px, 0px) rotate(-0.5deg)
}
54% {
-webkit-transform: translate(0px, 0px) rotate(-0.5deg)
}
56% {
-webkit-transform: translate(-1px, 0px) rotate(-0.5deg)
}
58% {
-webkit-transform: translate(0px, -1px) rotate(-0.5deg)
}
60% {
-webkit-transform: translate(-1px, -1px) rotate(-0.5deg)
}
62% {
-webkit-transform: translate(-1px, -1px) rotate(-0.5deg)
}
64% {
-webkit-transform: translate(-1px, 0px) rotate(-0.5deg)
}
66% {
-webkit-transform: translate(0px, 0px) rotate(-0.5deg)
}
68% {
-webkit-transform: translate(-1px, 0px) rotate(-0.5deg)
}
70% {
-webkit-transform: translate(0px, -1px) rotate(-0.5deg)
}
72% {
-webkit-transform: translate(0px, -1px) rotate(-0.5deg)
}
74% {
-webkit-transform: translate(-1px, -1px) rotate(-0.5deg)
}
76% {
-webkit-transform: translate(0px, 0px) rotate(-0.5deg)
}
78% {
-webkit-transform: translate(0px, -1px) rotate(-0.5deg)
}
80% {
-webkit-transform: translate(0px, -1px) rotate(-0.5deg)
}
82% {
-webkit-transform: translate(-1px, 0px) rotate(-0.5deg)
}
84% {
-webkit-transform: translate(-1px, 0px) rotate(-0.5deg)
}
86% {
-webkit-transform: translate(0px, 0px) rotate(-0.5deg)
}
88% {
-webkit-transform: translate(0px, 0px) rotate(-0.5deg)
}
90% {
-webkit-transform: translate(-1px, -1px) rotate(-0.5deg)
}
92% {
-webkit-transform: translate(0px, 0px) rotate(-0.5deg)
}
94% {
-webkit-transform: translate(0px, 0px) rotate(-0.5deg)
}
96% {
-webkit-transform: translate(-1px, -1px) rotate(-0.5deg)
}
98% {
-webkit-transform: translate(0px, 0px) rotate(-0.5deg)
}
}
@-ms-keyframes shake-little {
0% {
-ms-transform: translate(0px, 0px) rotate(0deg)
}
2% {
-ms-transform: translate(0px, 0px) rotate(-0.5deg)
}
4% {
-ms-transform: translate(0px, 0px) rotate(-0.5deg)
}
6% {
-ms-transform: translate(-1px, 0px) rotate(-0.5deg)
}
8% {
-ms-transform: translate(0px, -1px) rotate(-0.5deg)
}
10% {
-ms-transform: translate(0px, 0px) rotate(-0.5deg)
}
12% {
-ms-transform: translate(0px, -1px) rotate(-0.5deg)
}
14% {
-ms-transform: translate(0px, -1px) rotate(-0.5deg)
}
16% {
-ms-transform: translate(0px, 0px) rotate(-0.5deg)
}
18% {
-ms-transform: translate(0px, 0px) rotate(-0.5deg)
}
20% {
-ms-transform: translate(-1px, 0px) rotate(-0.5deg)
}
22% {
-ms-transform: translate(-1px, -1px) rotate(-0.5deg)
}
24% {
-ms-transform: translate(-1px, 0px) rotate(-0.5deg)
}
26% {
-ms-transform: translate(-1px, 0px) rotate(-0.5deg)
}
28% {
-ms-transform: translate(0px, -1px) rotate(-0.5deg)
}
30% {
-ms-transform: translate(0px, -1px) rotate(-0.5deg)
}
32% {
-ms-transform: translate(0px, -1px) rotate(-0.5deg)
}
34% {
-ms-transform: translate(0px, -1px) rotate(-0.5deg)
}
36% {
-ms-transform: translate(0px, -1px) rotate(-0.5deg)
}
38% {
-ms-transform: translate(-1px, 0px) rotate(-0.5deg)
}
40% {
-ms-transform: translate(0px, -1px) rotate(-0.5deg)
}
42% {
-ms-transform: translate(-1px, -1px) rotate(-0.5deg)
}
44% {
-ms-transform: translate(-1px, -1px) rotate(-0.5deg)
}
46% {
-ms-transform: translate(0px, -1px) rotate(-0.5deg)
}
48% {
-ms-transform: translate(0px, -1px) rotate(-0.5deg)
}
50% {
-ms-transform: translate(-1px, 0px) rotate(-0.5deg)
}
52% {
-ms-transform: translate(-1px, -1px) rotate(-0.5deg)
}
54% {
-ms-transform: translate(-1px, 0px) rotate(-0.5deg)
}
56% {
-ms-transform: translate(0px, -1px) rotate(-0.5deg)
}
58% {
-ms-transform: translate(0px, -1px) rotate(-0.5deg)
}
60% {
-ms-transform: translate(-1px, 0px) rotate(-0.5deg)
}
62% {
-ms-transform: translate(0px, -1px) rotate(-0.5deg)
}
64% {
-ms-transform: translate(-1px, 0px) rotate(-0.5deg)
}
66% {
-ms-transform: translate(-1px, -1px) rotate(-0.5deg)
}
68% {
-ms-transform: translate(0px, 0px) rotate(-0.5deg)
}
70% {
-ms-transform: translate(-1px, 0px) rotate(-0.5deg)
}
72% {
-ms-transform: translate(-1px, 0px) rotate(-0.5deg)
}
74% {
-ms-transform: translate(-1px, -1px) rotate(-0.5deg)
}
76% {
-ms-transform: translate(-1px, 0px) rotate(-0.5deg)
}
78% {
-ms-transform: translate(-1px, 0px) rotate(-0.5deg)
}
80% {
-ms-transform: translate(0px, -1px) rotate(-0.5deg)
}
82% {
-ms-transform: translate(-1px, -1px) rotate(-0.5deg)
}
84% {
-ms-transform: translate(0px, -1px) rotate(-0.5deg)
}
86% {
-ms-transform: translate(0px, -1px) rotate(-0.5deg)
}
88% {
-ms-transform: translate(0px, -1px) rotate(-0.5deg)
}
90% {
-ms-transform: translate(-1px, -1px) rotate(-0.5deg)
}
92% {
-ms-transform: translate(0px, 0px) rotate(-0.5deg)
}
94% {
-ms-transform: translate(0px, -1px) rotate(-0.5deg)
}
96% {
-ms-transform: translate(-1px, 0px) rotate(-0.5deg)
}
98% {
-ms-transform: translate(0px, -1px) rotate(-0.5deg)
}
}
@keyframes shake-little {
0% {
transform: translate(0px, 0px) rotate(0deg)
}
2% {
transform: translate(-1px, -1px) rotate(-0.5deg)
}
4% {
transform: translate(-1px, -1px) rotate(-0.5deg)
}
6% {
transform: translate(0px, 0px) rotate(-0.5deg)
}
8% {
transform: translate(0px, -1px) rotate(-0.5deg)
}
10% {
transform: translate(-1px, -1px) rotate(-0.5deg)
}
12% {
transform: translate(0px, 0px) rotate(-0.5deg)
}
14% {
transform: translate(0px, 0px) rotate(-0.5deg)
}
16% {
transform: translate(0px, -1px) rotate(-0.5deg)
}
18% {
transform: translate(0px, 0px) rotate(-0.5deg)
}
20% {
transform: translate(-1px, -1px) rotate(-0.5deg)
}
22% {
transform: translate(-1px, -1px) rotate(-0.5deg)
}
24% {
transform: translate(-1px, -1px) rotate(-0.5deg)
}
26% {
transform: translate(-1px, 0px) rotate(-0.5deg)
}
28% {
transform: translate(0px, -1px) rotate(-0.5deg)
}
30% {
transform: translate(0px, -1px) rotate(-0.5deg)
}
32% {
transform: translate(0px, 0px) rotate(-0.5deg)
}
34% {
transform: translate(0px, -1px) rotate(-0.5deg)
}
36% {
transform: translate(-1px, 0px) rotate(-0.5deg)
}
38% {
transform: translate(-1px, -1px) rotate(-0.5deg)
}
40% {
transform: translate(0px, 0px) rotate(-0.5deg)
}
42% {
transform: translate(-1px, -1px) rotate(-0.5deg)
}
44% {
transform: translate(-1px, 0px) rotate(-0.5deg)
}
46% {
transform: translate(-1px, -1px) rotate(-0.5deg)
}
48% {
transform: translate(0px, -1px) rotate(-0.5deg)
}
50% {
transform: translate(0px, -1px) rotate(-0.5deg)
}
52% {
transform: translate(0px, 0px) rotate(-0.5deg)
}
54% {
transform: translate(-1px, 0px) rotate(-0.5deg)
}
56% {
transform: translate(0px, 0px) rotate(-0.5deg)
}
58% {
transform: translate(0px, 0px) rotate(-0.5deg)
}
60% {
transform: translate(0px, 0px) rotate(-0.5deg)
}
62% {
transform: translate(0px, -1px) rotate(-0.5deg)
}
64% {
transform: translate(0px, 0px) rotate(-0.5deg)
}
66% {
transform: translate(-1px, -1px) rotate(-0.5deg)
}
68% {
transform: translate(0px, -1px) rotate(-0.5deg)
}
70% {
transform: translate(0px, 0px) rotate(-0.5deg)
}
72% {
transform: translate(-1px, -1px) rotate(-0.5deg)
}
74% {
transform: translate(-1px, -1px) rotate(-0.5deg)
}
76% {
transform: translate(-1px, 0px) rotate(-0.5deg)
}
78% {
transform: translate(0px, 0px) rotate(-0.5deg)
}
80% {
transform: translate(0px, -1px) rotate(-0.5deg)
}
82% {
transform: translate(-1px, 0px) rotate(-0.5deg)
}
84% {
transform: translate(-1px, -1px) rotate(-0.5deg)
}
86% {
transform: translate(0px, -1px) rotate(-0.5deg)
}
88% {
transform: translate(0px, 0px) rotate(-0.5deg)
}
90% {
transform: translate(-1px, -1px) rotate(-0.5deg)
}
92% {
transform: translate(-1px, 0px) rotate(-0.5deg)
}
94% {
transform: translate(-1px, 0px) rotate(-0.5deg)
}
96% {
transform: translate(0px, -1px) rotate(-0.5deg)
}
98% {
transform: translate(0px, -1px) rotate(-0.5deg)
}
}
@keyframes shake-it{
0%{
text-shadow: 0 0 rgba(0, 255, 255, .5), 0 0 rgba(255, 0, 0, .5);
}
25%{
text-shadow: -2px 0 rgba(0, 255, 255, .5), 2px 0 rgba(255, 0, 0, .5);
}
50%{
text-shadow: -5px 0 rgba(0, 255, 255, .5), 3px 0 rgba(255, 0, 0, .5);
}
100%{
text-shadow: 3px 0 rgba(0, 255, 255, .5), 5px 0 rgba(255, 0, 0, .5);
}
}
@keyframes rainbow {
100% { filter: hue-rotate(360deg); }
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 545 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 549 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 472 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 682 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 617 B

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,503 @@
var userAgent = window.navigator.userAgent.toLowerCase();
console.log(userAgent);
var norunAI = ["android", "iphone", "ipod", "ipad", "windows phone"];
var norunFlag = false;
for (var i = 0; i < norunAI.length; i++) {
if (userAgent.indexOf(norunAI[i]) > -1) {
norunFlag = true;
break;
}
}
if (!window.WebGLRenderingContext) {
norunFlag = true;
}
if (!norunFlag) {
var hitFlag = false;
var AIFadeFlag = false;
var liveTlakTimer = null;
var sleepTimer_ = null;
var AITalkFlag = false;
var talkNum = 0;
// 暴露到全局,供 pjax.js 在页面切换后重新调用
window._live2d = { initTips: null, showMessage: null, showHitokoto: null };
(function () {
function renderTip(template, context) {
var tokenReg = /(\\)?\{([^\{\}\\]+)(\\)?\}/g;
return template.replace(tokenReg, function (word, slash1, token, slash2) {
if (slash1 || slash2) {
return word.replace(/\\/g, '');
}
var variables = token.replace(/\s/g, '').split('.');
var currentObject = context;
var i, length, variable;
for (i = 0, length = variables.length; i < length; ++i) {
variable = currentObject[variables[i]];
if (variable === undefined || variable === null) return '';
currentObject = variable;
}
return String(currentObject);
});
}
String.prototype.renderTip = function (context) {
return renderTip(this, context);
};
var re = /x/;
console.log(re);
re.toString = function () {
showMessage('哈哈,你打开了控制台,是想要看看我的秘密吗?', 5000);
return '';
};
$(document).on('copy', function () {
showMessage('你都复制了些什么呀,转载要记得加上出处哦~~', 5000);
});
// 缓存 message.json 数据,供 PJAX 重绑定使用
var tipsData = null;
function initTips() {
$.ajax({
cache: true,
url: message_Path + 'message.json',
dataType: "json",
success: function (result) {
tipsData = result;
// 解绑旧事件(用命名空间避免影响其他绑定)
$.each(result.mouseover, function (index, tips) {
$(tips.selector).off('mouseover._live2d_tips mouseout._live2d_tips');
$(tips.selector).on('mouseover._live2d_tips', function () {
var text = tips.text;
if (Array.isArray(tips.text)) text = tips.text[Math.floor(Math.random() * tips.text.length + 1) - 1];
text = text.renderTip({ text: $(this).text() });
showMessage(text, 3000);
talkValTimer();
clearInterval(liveTlakTimer);
liveTlakTimer = null;
});
$(tips.selector).on('mouseout._live2d_tips', function () {
showHitokoto();
if (liveTlakTimer == null) {
liveTlakTimer = window.setInterval(function () {
showHitokoto();
}, 15000);
};
});
});
$.each(result.click, function (index, tips) {
$(tips.selector).off('click._live2d_tips');
$(tips.selector).on('click._live2d_tips', function () {
if (hitFlag) {
return false
}
hitFlag = true;
setTimeout(function () {
hitFlag = false;
}, 8000);
var text = tips.text;
if (Array.isArray(tips.text)) text = tips.text[Math.floor(Math.random() * tips.text.length + 1) - 1];
text = text.renderTip({ text: $(this).text() });
showMessage(text, 3000);
});
clearInterval(liveTlakTimer);
liveTlakTimer = null;
if (liveTlakTimer == null) {
liveTlakTimer = window.setInterval(function () {
showHitokoto();
}, 15000);
};
});
}
});
}
window._live2d.initTips = initTips;
initTips();
var text;
if (document.referrer !== '' && document.referrer.split('/')[2] !== window.location.host) {
var referrer = document.createElement('a');
referrer.href = document.referrer;
var domain = referrer.hostname.split('.')[1];
if (domain == 'baidu' || domain == 'so' || domain == 'google') {
var source = domain == 'baidu' ? '百度搜索' : domain == 'so' ? '360搜索' : '谷歌搜索';
text = '嗨! 来自 ' + source + ' 的朋友!<br>欢迎访问<span style="color:#0099cc;">「 ' + document.title.split(' | ')[0] + ' 」</span>';
} else {
text = '嗨!来自 <span style="color:#0099cc;">' + referrer.hostname + '</span> 的朋友!';
}
} else {
text = getWelcomeText();
}
showMessage(text, 12000);
})();
liveTlakTimer = setInterval(function () {
showHitokoto();
}, 15000);
function showHitokoto() {
if (sessionStorage.getItem("Sleepy") !== "1") {
if (!AITalkFlag) {
$.getJSON('https://hitokoto.mayx.eu.org/', function (result) {
talkValTimer();
showMessage(result.hitokoto, 0);
});
}
} else {
hideMessage(0);
if (sleepTimer_ == null) {
sleepTimer_ = setInterval(function () {
checkSleep();
}, 200);
}
console.log(sleepTimer_);
}
}
window._live2d.showHitokoto = showHitokoto;
function checkSleep() {
var sleepStatu = sessionStorage.getItem("Sleepy");
if (sleepStatu !== '1') {
talkValTimer();
showMessage('你回来啦~', 0);
clearInterval(sleepTimer_);
sleepTimer_ = null;
}
}
function showMessage(text, timeout) {
if (Array.isArray(text)) text = text[Math.floor(Math.random() * text.length + 1) - 1];
//console.log('showMessage', text);
$('.message').stop();
if (typeof EventSource !== 'undefined' && text instanceof EventSource) {
var outputContainer = $('.message')[0];
var eventFlag = false;
text.onmessage = function (event) {
if (event.data == "[DONE]") {
text.close();
return;
} else {
if (!eventFlag) {
talkValTimer();
outputContainer.textContent = "";
eventFlag = true;
}
var data = JSON.parse(event.data);
if (data.response) {
outputContainer.textContent += data.response;
}
}
}
} else {
$('.message').html(text);
}
$('.message').fadeTo(200, 1);
//if (timeout === null) timeout = 5000;
//hideMessage(timeout);
}
window._live2d.showMessage = showMessage;
function talkValTimer() {
$('#live_talk').val('1');
}
function hideMessage(timeout) {
//$('.message').stop().css('opacity',1);
if (timeout === null) timeout = 5000;
$('.message').delay(timeout).fadeTo(200, 0);
}
function initLive2d() {
$("#landlord").mouseenter(function () {
$(".live_ico_box").fadeIn();
});
$("#landlord").mouseleave(function () {
$(".live_ico_box").fadeOut();
});
$('#hideButton').on('click', function () {
if (AIFadeFlag) {
return false;
} else {
AIFadeFlag = true;
localStorage.setItem("live2dhidden", "0");
$('#landlord').fadeOut(200);
$('#open_live2d').delay(200).fadeIn(200);
setTimeout(function () {
AIFadeFlag = false;
}, 300);
}
});
$('#open_live2d').on('click', function () {
if (AIFadeFlag) {
return false;
} else {
AIFadeFlag = true;
localStorage.setItem("live2dhidden", "1");
$('#open_live2d').fadeOut(200);
$('#landlord').delay(200).fadeIn(200);
setTimeout(function () {
AIFadeFlag = false;
}, 300);
}
});
$('#youduButton').on('click', function () {
if ($('#youduButton').hasClass('doudong')) {
var typeIs = $('#youduButton').attr('data-type');
$('#youduButton').removeClass('doudong');
$('body').removeClass(typeIs);
$('#youduButton').attr('data-type', '');
} else {
var duType = $('#duType').val();
var duArr = duType.split(",");
var dataType = duArr[Math.floor(Math.random() * duArr.length)];
$('#youduButton').addClass('doudong');
$('#youduButton').attr('data-type', dataType);
$('body').addClass(dataType);
}
});
if (talkAPI !== "" && typeof EventSource !== 'undefined') {
$('#showInfoBtn').on('click', function () {
var live_statu = $('#live_statu_val').val();
if (live_statu == "0") {
return
} else {
$('#live_statu_val').val("0");
$('.live_talk_input_body').fadeOut(500);
AITalkFlag = false;
showHitokoto();
$('#showTalkBtn').show();
$('#showInfoBtn').hide();
}
});
$('#showTalkBtn').on('click', function () {
var live_statu = $('#live_statu_val').val();
if (live_statu == "1") {
return
} else {
$('#live_statu_val').val("1");
$('.live_talk_input_body').fadeIn(500);
AITalkFlag = true;
$('#showTalkBtn').hide();
$('#showInfoBtn').show();
}
});
$('#live_talk_input_form').on('submit', function (e) {
e.preventDefault();
var info_ = $('#AIuserText').val();
// var userid_ = $('#AIuserName').val();
let add_id = "";
if ($('#load_this').prop("checked")) {
add_id = "&id=" + encodeURIComponent($('#post_id').val());
}
if (info_ == "") {
showMessage('写点什么吧!', 0);
return;
}
showMessage('思考中~', 0);
showMessage(new EventSource(talkAPI + "?info=" + encodeURIComponent(info_) + add_id));
});
} else {
$('#showInfoBtn').hide();
$('#showTalkBtn').hide();
}
// //获取用户名
// var live2dUser = sessionStorage.getItem("live2duser");
// if(live2dUser !== null){
// $('#AIuserName').val(live2dUser);
// }
//获取位置
var landL = sessionStorage.getItem("historywidth");
var landB = sessionStorage.getItem("historyheight");
if (landL == null || landB == null) {
landL = '5px'
landB = '0px'
}
$('#landlord').css('left', landL + 'px');
$('#landlord').css('bottom', landB + 'px');
//移动
function getEvent() {
return window.event || arguments.callee.caller.arguments[0];
}
var smcc = document.getElementById("landlord");
var moveX = 0;
var moveY = 0;
var moveBottom = 0;
var moveLeft = 0;
var moveable = false;
var docMouseMoveEvent = document.onmousemove;
var docMouseUpEvent = document.onmouseup;
smcc.onmousedown = function () {
var ent = getEvent();
moveable = true;
moveX = ent.clientX;
moveY = ent.clientY;
var obj = smcc;
moveBottom = parseInt(obj.style.bottom);
moveLeft = parseInt(obj.style.left);
if (isFirefox = navigator.userAgent.indexOf("Firefox") > 0) {
window.getSelection().removeAllRanges();
}
document.onmousemove = function () {
if (moveable) {
var ent = getEvent();
var x = moveLeft + ent.clientX - moveX;
var y = moveBottom + (moveY - ent.clientY);
obj.style.left = x + "px";
obj.style.bottom = y + "px";
}
};
document.onmouseup = function () {
if (moveable) {
var historywidth = obj.style.left;
var historyheight = obj.style.bottom;
historywidth = historywidth.replace('px', '');
historyheight = historyheight.replace('px', '');
sessionStorage.setItem("historywidth", historywidth);
sessionStorage.setItem("historyheight", historyheight);
document.onmousemove = docMouseMoveEvent;
document.onmouseup = docMouseUpEvent;
moveable = false;
moveX = 0;
moveY = 0;
moveBottom = 0;
moveLeft = 0;
}
};
};
//获取音乐信息初始化
var $bgm = $('#live2d_bgm');
// 音乐按钮点击事件(幂等,使用命名空间避免重复绑定)
$('#musicButton').off('click._bgm').on('click._bgm', function () {
if ($('#musicButton').hasClass('play')) {
$bgm[0].pause();
$('#musicButton').removeClass('play');
sessionStorage.setItem("live2dBGM_IsPlay", '1');
} else {
$bgm[0].play();
$('#musicButton').addClass('play');
sessionStorage.setItem("live2dBGM_IsPlay", '0');
}
});
// BGM 事件监听(仅绑定一次,使用标志位避免重复)
if (!window._live2d._bgmEventsBound) {
$bgm[0].addEventListener("timeupdate", function () {
sessionStorage.setItem("live2dBGM_PlayTime", $bgm[0].currentTime);
});
$bgm[0].addEventListener("ended", function () {
var listNow = parseInt($bgm.attr('data-bgm'));
listNow++;
var inputs = $('input[name=live2dBGM]');
if (inputs.length === 0) return;
if (listNow > inputs.length - 1) {
listNow = 0;
}
var listNewSrc = inputs.eq(listNow).val();
if (!listNewSrc) return;
sessionStorage.setItem("live2dBGM_Num", listNow);
$bgm.attr('src', listNewSrc);
$bgm[0].play();
$bgm.attr('data-bgm', listNow);
});
$bgm[0].addEventListener("error", function () {
$bgm[0].pause();
$('#musicButton').removeClass('play');
showMessage('音乐似乎加载不出来了呢!', 0);
});
window.onbeforeunload = function () {
sessionStorage.setItem("live2dBGM_WindowClose", '0');
if ($('#musicButton').hasClass('play')) {
sessionStorage.setItem("live2dBGM_IsPlay", '0');
}
};
window._live2d._bgmEventsBound = true;
}
// 初始化 BGM(根据当前页面是否有 BGM 输入)
if (typeof window._live2d.initBGM === 'function') {
window._live2d.initBGM();
}
}
// 暴露 BGM 初始化函数,供 PJAX 重初始化时调用
window._live2d.initBGM = function() {
var bgmListInfo = $('input[name=live2dBGM]');
var $bgm = $('#live2d_bgm');
if (bgmListInfo.length === 0) {
$('#musicButton').hide();
if ($bgm.length) $bgm[0].pause();
return;
}
var bgmPlayNow = parseInt($bgm.attr('data-bgm')) || 0;
var bgmPlayTime = 0;
var live2dBGM_Num = sessionStorage.getItem("live2dBGM_Num");
var live2dBGM_PlayTime = sessionStorage.getItem("live2dBGM_PlayTime");
if (live2dBGM_Num) {
if (parseInt(live2dBGM_Num) <= bgmListInfo.length - 1) {
bgmPlayNow = parseInt(live2dBGM_Num);
}
}
if (live2dBGM_PlayTime) {
bgmPlayTime = parseFloat(live2dBGM_PlayTime);
}
var newSrc = bgmListInfo.eq(bgmPlayNow).val();
$bgm.attr('data-bgm', bgmPlayNow);
if ($bgm.attr('src') !== newSrc) {
$bgm[0].pause();
$bgm.attr('src', newSrc);
$bgm[0].currentTime = bgmPlayTime;
}
$bgm[0].volume = 0.5;
var live2dBGM_IsPlay = sessionStorage.getItem("live2dBGM_IsPlay");
var live2dBGM_WindowClose = sessionStorage.getItem("live2dBGM_WindowClose");
if (live2dBGM_IsPlay == '0' && live2dBGM_WindowClose == '0') {
$bgm[0].play();
$('#musicButton').addClass('play');
}
sessionStorage.setItem("live2dBGM_WindowClose", '1');
$('#musicButton').show();
};
$(document).ready(function () {
var AIimgSrc = [
message_Path + "model/histoire/histoire.1024/texture_00.png",
message_Path + "model/histoire/histoire.1024/texture_01.png",
message_Path + "model/histoire/histoire.1024/texture_02.png",
message_Path + "model/histoire/histoire.1024/texture_03.png"
]
var images = [];
var imgLength = AIimgSrc.length;
var loadingNum = 0;
for (var i = 0; i < imgLength; i++) {
images[i] = new Image();
images[i].src = AIimgSrc[i];
images[i].onload = function () {
loadingNum++;
if (loadingNum === imgLength) {
var live2dhidden = localStorage.getItem("live2dhidden");
if (live2dhidden === "0") {
setTimeout(function () {
$('#open_live2d').fadeIn(200);
}, 1300);
} else {
setTimeout(function () {
$('#landlord').fadeIn(200);
}, 1300);
}
setTimeout(function () {
loadlive2d("live2d", message_Path + "model/histoire/model.json");
}, 1000);
initLive2d();
images = null;
}
}
}
});
}

View file

@ -0,0 +1,42 @@
{
"mouseover": [
{
"selector": "#showInfoBtn",
"text": ["不和我聊了吗?"]
},
{
"selector": "#showTalkBtn",
"text": ["要和我聊天吗?"]
},
{
"selector": "#hideButton",
"text": ["隐藏伊斯特瓦尔,希望隐藏后我们还能再见面!"]
},
{
"selector": "#musicButton",
"text": ["给页面加点音乐吗?"]
},
{
"selector": "#youduButton",
"text": ["按钮有毒,癫痫患者不要开启!!!<br>点一下开启,再点一下可以关闭。"]
},
{
"selector": ".post-link",
"text": ["要看看 {text} 么?"]
},
{
"selector": "#search-input",
"text": ["在找什么东西呢,需要帮忙吗?"]
},
{
"selector": "#search-input-all",
"text": ["在找什么东西呢,需要帮忙吗?"]
}
],
"click": [
{
"selector": "#landlord #live2d",
"text": ["不要动手动脚的!快把手拿开~~", "真…真的是不知羞耻!","Hentai!", "再摸的话我可要报警了!", "110吗,这里有个变态!"]
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

View file

@ -0,0 +1,59 @@
{
"version":"1.0.0",
"model":"model.moc",
"textures":[
"histoire.1024/texture_00.png",
"histoire.1024/texture_01.png",
"histoire.1024/texture_02.png",
"histoire.1024/texture_03.png"
],
"layout":{
"center_x":0.0,
"center_y":-0.05,
"width":2.0
},
"hit_areas_custom":{
"head_x":[-1, 1],
"head_y":[1, -1],
"body_x":[-1, -1],
"body_y":[1, -1]
},
"motions":{
"idle":[
{"file":"motions/idle/NOZOMU_M01.mtn"},
{"file":"motions/idle/NOZOMU_M02.mtn"}
],
"sleepy":[
{"file":"motions/idle/NOZOMU_M04.mtn"}
],
"flick_head":[
{"file":"motions/tap/DK_NOZOMU_0011.mtn"},
{"file":"motions/tap/tsumiki_m_14.mtn"},
{"file":"motions/tap/m_06.mtn"}
],
"tap_body":[
{"file":"motions/tap/DK_NOZOMU_0011.mtn"},
{"file":"motions/tap/m_13.mtn"}
],
"talk":[
{"file":"motions/tap/DK_NOZOMU_0041.mtn"},
{"file":"motions/tap/DK_NOZOMU_0061.mtn"},
{"file":"motions/tap/DK_NOZOMU_0067.mtn"}
],
"rest":[
{"file":"motions/tap/tsumiki_m_01.mtn"},
{"file":"motions/tap/tsumiki_m_09.mtn"},
{"file":"motions/tap/tsumiki_m_13.mtn"},
{"file":"motions/tap/tsumiki_m_19.mtn"},
{"file":"motions/tap/tsumiki_m_21.mtn"}
],
"":[
{"file":"motions/tap/tsumiki_m_01.mtn"},
{"file":"motions/tap/tsumiki_m_09.mtn"},
{"file":"motions/tap/tsumiki_m_13.mtn"},
{"file":"motions/tap/tsumiki_m_19.mtn"},
{"file":"motions/tap/tsumiki_m_21.mtn"}
]
}
}

Binary file not shown.

View file

@ -0,0 +1,36 @@
# Live2D Animator Motion Data
$fps=30
PARAM_ANGLE_X=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.03,0.11,0.24,0.42,0.63,0.88,1.17,1.48,1.81,2.17,2.54,2.92,3.31,3.69,4.08,4.46,4.83,5.19,5.52,5.83,6.12,6.37,6.58,6.76,6.89,6.97,7,6.79,6.24,5.45,4.46,3.37,2.22,1.07,-0.06,-1.11,-2.06,-2.84,-3.46,-3.86,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-3.984,-3.94,-3.86,-3.76,-3.64,-3.5,-3.33,-3.16,-2.97,-2.77,-2.56,-2.35,-2.13,-1.92,-1.71,-1.5,-1.29,-1.09,-0.91,-0.73,-0.57,-0.43,-0.31,-0.2,-0.11,-0.05,-0.01,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.006,0.03,0.07,0.15,0.26,0.41,0.61,0.85,1.16,1.52,1.94,2.42,2.97,3.58,4.26,5,6.3,7.95,9.74,11.63,13.46,15.19,16.78,18.08,19.11,19.76,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,19.95,19.82,19.61,19.32,18.97,18.55,18.08,17.55,16.97,16.37,15.71,15.02,14.31,13.58,12.82,12.05,11.27,10.5,9.71,8.92,8.15,7.39,6.65,5.92,5.22,4.54,3.89,3.28,2.71,2.19,1.71,1.27,0.9,0.59,0.34,0.15,0.04,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.08,-0.31,-0.69,-1.18,-1.79,-2.48,-3.28,-4.12,-5.02,-5.96,-6.9,-7.88,-8.84,-9.79,-10.72,-11.61,-12.43,-13.19,-13.89,-14.5,-15.02,-15.44,-15.75,-15.94,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-15.94,-15.75,-15.45,-15.05,-14.55,-13.98,-13.33,-12.62,-11.86,-11.07,-10.22,-9.37,-8.51,-7.64,-6.78,-5.94,-5.11,-4.33,-3.58,-2.88,-2.24,-1.67,-1.16,-0.73,-0.39,-0.14,0.01,0.06,0.06,0.06,0.06,0.06,0.06,0.06,0.06,0.06,0.06,0.06,0.06,0.06,0.06,0.06,0.06,0.06,0.06,0.06
PARAM_ANGLE_Y=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.04,0.16,0.34,0.6,0.91,1.26,1.67,2.11,2.59,3.1,3.62,4.17,4.72,5.28,5.83,6.38,6.9,7.41,7.89,8.33,8.74,9.09,9.4,9.66,9.84,9.96,10,9.5,8.21,6.33,4,1.41,-1.29,-4.03,-6.69,-9.17,-11.41,-13.27,-14.73,-15.67,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-15.94,-15.75,-15.45,-15.05,-14.56,-13.99,-13.34,-12.64,-11.88,-11.09,-10.25,-9.4,-8.54,-7.67,-6.82,-5.98,-5.15,-4.37,-3.63,-2.93,-2.29,-1.73,-1.22,-0.79,-0.46,-0.21,-0.05,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.02,-0.08,-0.18,-0.32,-0.5,-0.73,-0.99,-1.29,-1.63,-2.01,-2.43,-2.88,-3.37,-3.88,-4.43,-5,-5.75,-6.4,-6.96,-7.45,-7.86,-8.21,-8.5,-8.71,-8.87,-8.97,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-8.98,-8.92,-8.82,-8.7,-8.54,-8.35,-8.14,-7.9,-7.64,-7.36,-7.07,-6.76,-6.44,-6.11,-5.77,-5.42,-5.07,-4.72,-4.37,-4.01,-3.67,-3.33,-2.99,-2.66,-2.35,-2.04,-1.75,-1.48,-1.22,-0.98,-0.77,-0.57,-0.41,-0.26,-0.15,-0.07,-0.02,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.03,-0.12,-0.26,-0.44,-0.67,-0.93,-1.23,-1.55,-1.88,-2.23,-2.59,-2.96,-3.32,-3.67,-4.02,-4.35,-4.66,-4.95,-5.21,-5.44,-5.63,-5.79,-5.9,-5.98,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-5.98,-5.91,-5.79,-5.65,-5.46,-5.25,-5.01,-4.74,-4.46,-4.17,-3.85,-3.54,-3.22,-2.89,-2.57,-2.26,-1.95,-1.66,-1.38,-1.12,-0.88,-0.67,-0.49,-0.33,-0.2,-0.11,-0.05,-0.029,-0.03,-0.03,-0.03,-0.03,-0.03,-0.03,-0.03,-0.03,-0.03,-0.03,-0.03,-0.03,-0.03,-0.03,-0.03,-0.03,-0.03,-0.03
PARAM_ANGLE_Z=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.16,0.63,1.35,2.28,3.38,4.58,5.85,7.15,8.42,9.62,10.72,11.65,12.37,12.84,13,12.87,12.5,11.91,11.13,10.23,9.17,8.03,6.84,5.6,4.35,3.16,2,0.81,-0.18,-1,-1.69,-2.26,-2.71,-3.08,-3.37,-3.59,-3.76,-3.87,-3.95,-3.99,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-3.984,-3.94,-3.86,-3.76,-3.64,-3.5,-3.33,-3.16,-2.97,-2.77,-2.56,-2.35,-2.13,-1.92,-1.71,-1.5,-1.29,-1.09,-0.91,-0.73,-0.57,-0.43,-0.31,-0.2,-0.11,-0.05,-0.01,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.45,1.58,3.11,4.9,6.75,8.59,10.32,11.78,12.96,13.72,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,13.97,13.88,13.73,13.53,13.28,12.99,12.66,12.28,11.88,11.46,11,10.51,10.02,9.5,8.98,8.44,7.89,7.35,6.79,6.24,5.7,5.17,4.65,4.14,3.65,3.18,2.72,2.3,1.9,1.53,1.19,0.89,0.63,0.41,0.24,0.11,0.03,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.16,0.56,1.11,1.75,2.41,3.07,3.69,4.21,4.63,4.9,5,4.48,3.14,1.19,-1.23,-3.92,-6.72,-9.57,-12.33,-14.91,-17.24,-19.16,-20.68,-21.66,-22,-21.93,-21.75,-21.53,-21.32,-21.15,-21.04,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-20.92,-20.67,-20.28,-19.75,-19.1,-18.35,-17.5,-16.57,-15.58,-14.54,-13.43,-12.31,-11.18,-10.04,-8.92,-7.82,-6.72,-5.7,-4.72,-3.81,-2.96,-2.22,-1.55,-0.99,-0.55,-0.22,-0.01,0.05,0.05,0.05,0.05,0.05,0.05,0.05,0.05,0.05,0.05,0.05,0.05,0.05,0.05,0.05,0.05,0.05,0.05,0.05
PARAM_EYE_L_OPEN=1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.74,0.26,0,0.24,0.64,0.91,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.27,0,0.26,0.74,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.27,0,0.26,0.74,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.27,0,0.26,0.74,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.74,0.26,0,0,0.18,0.46,0.73,0.92,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.74,0.26,0,0,0,0.73,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
PARAM_EYE_L_SMILE=0
PARAM_EYE_R_OPEN=1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.74,0.26,0,0.24,0.64,0.91,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.27,0,0.26,0.74,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.27,0,0.26,0.74,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.27,0,0.26,0.74,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.74,0.26,0,0,0.18,0.46,0.73,0.92,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.74,0.26,0,0,0,0.73,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
PARAM_EYE_R_SMILE=0
PARAM_EYE_FORM=0
PARAM_EYE_BALL_X=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.001,-0.005,-0.011,-0.019,-0.029,-0.04,-0.053,-0.068,-0.083,-0.099,-0.116,-0.133,-0.151,-0.169,-0.187,-0.204,-0.221,-0.237,-0.252,-0.267,-0.28,-0.291,-0.301,-0.309,-0.315,-0.319,-0.32,-0.309,-0.28,-0.23,-0.18,-0.13,-0.08,-0.03,0,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.009,0.01,0.008,0.01,0.007,0.01,0.006,0.01,0.005,0,0.004,0,0.003,0,0.002,0,0,0.001,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.002,-0.004,-0.007,-0.012,-0.018,-0.026,-0.036,-0.048,-0.062,-0.078,-0.096,-0.12,-0.14,-0.17,-0.2,-0.23,-0.28,-0.34,-0.4,-0.46,-0.52,-0.58,-0.62,-0.65,-0.673,-0.68,-0.68,-0.68,-0.68,-0.68,-0.68,-0.68,-0.68,-0.68,-0.68,-0.68,-0.68,-0.68,-0.68,-0.68,-0.68,-0.68,-0.68,-0.68,-0.68,-0.68,-0.68,-0.68,-0.68,-0.68,-0.68,-0.68,-0.68,-0.68,-0.68,-0.68,-0.68,-0.68,-0.68,-0.68,-0.68,-0.68,-0.68,-0.68,-0.68,-0.68,-0.68,-0.68,-0.68,-0.68,-0.68,-0.68,-0.68,-0.68,-0.68,-0.68,-0.68,-0.68,-0.68,-0.678,-0.674,-0.667,-0.657,-0.645,-0.631,-0.615,-0.597,-0.577,-0.56,-0.53,-0.51,-0.49,-0.46,-0.44,-0.41,-0.38,-0.36,-0.33,-0.3,-0.28,-0.25,-0.23,-0.2,-0.18,-0.15,-0.13,-0.11,-0.092,-0.074,-0.058,-0.043,-0.031,-0.02,-0.012,-0.005,-0.001,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.003,0.006,0.011,0.017,0.023,0.031,0.039,0.047,0.057,0.067,0.077,0.087,0.098,0.108,0.119,0.13,0.14,0.15,0.16,0.17,0.179,0.188,0.196,0.203,0.21,0.216,0.221,0.225,0.228,0.229,0.23,0.23,0.23,0.23,0.23,0.23,0.23,0.23,0.23,0.23,0.23,0.23,0.23,0.23,0.23,0.23,0.23,0.23,0.23,0.23,0.23,0.23,0.23,0.23,0.23,0.23,0.23,0.23,0.23,0.23,0.23,0.23,0.23,0.23,0.23,0.23,0.23,0.23,0.23,0.23,0.23,0.23,0.23,0.23,0.23,0.23,0.23,0.23,0.23,0.23,0.23,0.23,0.23,0.23,0.226,0.222,0.216,0.209,0.201,0.191,0.181,0.17,0.159,0.146,0.134,0.122,0.109,0.097,0.085,0.073,0.061,0.05,0.04,0.031,0.023,0.016,0.009,0.004,0.001,-0.001,-0.002,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_EYE_BALL_Y=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.002,0.003,0.005,0.007,0.01,0.013,0.016,0.02,0.024,0.029,0.033,0.038,0.044,0.049,0.055,0.062,0.068,0.075,0.082,0.089,0.097,0.105,0.113,0.121,0.13,0.138,0.144,0.149,0.153,0.156,0.158,0.159,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.159,0.157,0.154,0.151,0.146,0.14,0.133,0.126,0.119,0.111,0.102,0.094,0.085,0.077,0.068,0.06,0.052,0.044,0.036,0.029,0.023,0.017,0.012,0.008,0.005,0.002,0.001,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.001,0.003,0.005,0.008,0.013,0.019,0.026,0.034,0.044,0.056,0.07,0.086,0.103,0.123,0.15,0.17,0.21,0.25,0.3,0.36,0.41,0.45,0.49,0.52,0.534,0.54,0.54,0.54,0.54,0.54,0.54,0.54,0.54,0.54,0.54,0.54,0.54,0.54,0.54,0.54,0.54,0.54,0.54,0.54,0.54,0.54,0.54,0.54,0.54,0.54,0.54,0.54,0.54,0.54,0.54,0.54,0.54,0.54,0.54,0.54,0.54,0.54,0.54,0.54,0.54,0.54,0.54,0.54,0.54,0.54,0.54,0.54,0.54,0.54,0.54,0.54,0.54,0.54,0.54,0.539,0.535,0.529,0.522,0.512,0.501,0.488,0.474,0.458,0.442,0.424,0.405,0.386,0.367,0.346,0.33,0.3,0.28,0.26,0.24,0.22,0.2,0.179,0.16,0.141,0.123,0.105,0.089,0.073,0.059,0.046,0.034,0.024,0.016,0.009,0.004,0.001,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.003,0.006,0.01,0.015,0.021,0.028,0.035,0.043,0.052,0.061,0.07,0.079,0.089,0.099,0.109,0.118,0.128,0.137,0.146,0.155,0.164,0.171,0.179,0.186,0.192,0.197,0.202,0.205,0.208,0.209,0.21,0.21,0.21,0.21,0.21,0.21,0.21,0.21,0.21,0.21,0.21,0.21,0.21,0.21,0.21,0.21,0.21,0.21,0.21,0.21,0.21,0.21,0.21,0.21,0.21,0.21,0.21,0.21,0.21,0.21,0.21,0.21,0.21,0.21,0.21,0.21,0.21,0.21,0.21,0.21,0.21,0.21,0.21,0.21,0.21,0.21,0.21,0.21,0.21,0.21,0.21,0.21,0.21,0.209,0.207,0.203,0.198,0.191,0.184,0.175,0.166,0.156,0.146,0.135,0.124,0.113,0.102,0.091,0.08,0.069,0.059,0.049,0.04,0.032,0.024,0.018,0.012,0.008,0.004,0.002,0.002,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_EYE_BALL_FORM=0
PARAM_BROW_L_Y=0
PARAM_BROW_R_Y=0
PARAM_BROW_L_X=0
PARAM_BROW_R_X=0
PARAM_BROW_L_ANGLE=0
PARAM_BROW_R_ANGLE=0
PARAM_BROW_L_FORM=0
PARAM_BROW_R_FORM=0
PARAM_MOUTH_FORM=0
PARAM_MOUTH_OPEN_Y=0
PARAM_CHEEK=0
PARAM_SWEAT=0
PARAM_BODY_ANGLE_X=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.1,0.37,0.78,1.3,1.88,2.5,3.12,3.7,4.22,4.63,4.9,5,4.9,4.61,4.17,3.6,2.92,2.18,1.4,0.6,-0.18,-0.92,-1.6,-2.17,-2.61,-2.9,-3,-2.96,-2.83,-2.62,-2.34,-2,-1.59,-1.11,-0.58,0,0.59,1.15,1.69,2.2,2.67,3.13,3.56,3.96,4.34,4.69,5.01,5.31,5.59,5.83,6.06,6.26,6.43,6.59,6.71,6.82,6.9,6.95,6.99,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,6.97,6.89,6.76,6.59,6.37,6.12,5.84,5.53,5.2,4.85,4.48,4.11,3.74,3.36,2.98,2.62,2.25,1.91,1.59,1.28,1,0.76,0.53,0.35,0.2,0.09,0.02,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.02,0.09,0.21,0.36,0.54,0.76,1,1.27,1.55,1.86,2.17,2.5,2.83,3.17,3.5,3.83,4.14,4.45,4.73,5,5.24,5.46,5.64,5.79,5.91,5.98,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,5.986,5.95,5.88,5.8,5.69,5.57,5.42,5.26,5.09,4.91,4.71,4.51,4.29,4.07,3.85,3.62,3.38,3.15,2.91,2.68,2.44,2.22,1.99,1.78,1.56,1.36,1.17,0.98,0.81,0.66,0.51,0.38,0.27,0.18,0.1,0.05,0.01,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.05,-0.2,-0.44,-0.76,-1.14,-1.57,-2.06,-2.56,-3.1,-3.65,-4.21,-4.75,-5.28,-5.78,-6.25,-6.68,-7.06,-7.39,-7.65,-7.84,-7.96,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-8,-7.97,-7.87,-7.72,-7.53,-7.28,-6.99,-6.67,-6.31,-5.93,-5.54,-5.12,-4.69,-4.26,-3.83,-3.4,-2.98,-2.56,-2.17,-1.8,-1.45,-1.13,-0.85,-0.59,-0.38,-0.21,-0.08,-0.01,0.02,0.02,0.02,0.02,0.02,0.02,0.02,0.02,0.02,0.02,0.02,0.02,0.02,0.02,0.02,0.02,0.02,0.02,0.02
PARAM_BODY_ANGLE_Z=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.06,0.22,0.47,0.78,1.13,1.5,1.87,2.22,2.53,2.78,2.94,3,2.95,2.81,2.58,2.3,1.96,1.59,1.2,0.8,0.41,0.04,-0.3,-0.58,-0.81,-0.95,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-0.97,-0.9,-0.79,-0.63,-0.44,-0.22,0.03,0.29,0.58,0.88,1.19,1.5,1.81,2.12,2.42,2.71,2.97,3.22,3.44,3.63,3.79,3.9,3.97,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,3.984,3.94,3.86,3.76,3.64,3.5,3.33,3.16,2.97,2.77,2.56,2.35,2.13,1.92,1.71,1.5,1.29,1.09,0.91,0.73,0.57,0.43,0.31,0.2,0.11,0.05,0.01,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.03,0.13,0.28,0.48,0.72,1.01,1.33,1.69,2.07,2.48,2.9,3.33,3.78,4.22,4.67,5.1,5.52,5.93,6.31,6.67,6.99,7.28,7.52,7.72,7.87,7.97,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,7.982,7.93,7.84,7.73,7.59,7.42,7.23,7.02,6.79,6.55,6.28,6.01,5.72,5.43,5.13,4.82,4.51,4.2,3.88,3.57,3.26,2.96,2.66,2.37,2.09,1.82,1.56,1.31,1.08,0.88,0.68,0.51,0.36,0.24,0.14,0.06,0.02,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.05,-0.18,-0.39,-0.66,-1,-1.37,-1.8,-2.24,-2.72,-3.2,-3.68,-4.15,-4.62,-5.06,-5.47,-5.84,-6.18,-6.46,-6.69,-6.86,-6.96,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-6.97,-6.89,-6.76,-6.58,-6.37,-6.12,-5.83,-5.52,-5.19,-4.85,-4.47,-4.1,-3.73,-3.34,-2.97,-2.6,-2.24,-1.9,-1.57,-1.27,-0.98,-0.74,-0.51,-0.33,-0.18,-0.07,0,0.02,0.02,0.02,0.02,0.02,0.02,0.02,0.02,0.02,0.02,0.02,0.02,0.02,0.02,0.02,0.02,0.02,0.02,0.02
PARAM_BODY_ANGLE_Y=0
PARAM_BREATH=0
PARAM_HAIR_FRONT=0
PARAM_HAIR_BACK=0
PARAM_NECKLACE=0
VISIBLE:PARTS_01_CLOTHES_01_ARM_L_02=0
VISIBLE:PARTS_01_CLOTHES_01_ARM_R_02=0
VISIBLE:PARTS_01_CLOTHES_01_ARM_L_01=1
VISIBLE:PARTS_01_CLOTHES_01_ARM_R_01=1

View file

@ -0,0 +1,36 @@
# Live2D Animator Motion Data
$fps=30
PARAM_ANGLE_X=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.12,-0.45,-0.99,-1.71,-2.56,-3.53,-4.64,-5.77,-6.99,-8.22,-9.47,-10.68,-11.88,-13.01,-14.07,-15.03,-15.88,-16.62,-17.2,-17.64,-17.91,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-17.93,-17.72,-17.38,-16.93,-16.37,-15.73,-15,-14.19,-13.34,-12.43,-11.48,-10.5,-9.5,-8.5,-7.5,-6.52,-5.57,-4.66,-3.81,-3,-2.27,-1.63,-1.07,-0.62,-0.28,-0.07,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_ANGLE_Y=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.11,-0.4,-0.88,-1.52,-2.28,-3.14,-4.12,-5.13,-6.21,-7.31,-8.42,-9.49,-10.56,-11.56,-12.51,-13.36,-14.12,-14.77,-15.29,-15.68,-15.92,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-15.94,-15.75,-15.45,-15.05,-14.55,-13.98,-13.33,-12.62,-11.86,-11.05,-10.2,-9.33,-8.45,-7.55,-6.67,-5.8,-4.95,-4.14,-3.38,-2.67,-2.02,-1.45,-0.95,-0.55,-0.25,-0.06,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_ANGLE_Z=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.05,-0.18,-0.39,-0.66,-1,-1.39,-1.81,-2.27,-2.76,-3.25,-3.75,-4.24,-4.73,-5.19,-5.61,-6,-6.34,-6.61,-6.82,-6.95,-7,-6.97,-6.89,-6.75,-6.56,-6.32,-6.04,-5.72,-5.37,-4.97,-4.55,-4.09,-3.62,-3.11,-2.59,-2.05,-1.5,-0.93,-0.37,0.21,0.79,1.37,1.93,2.5,3.05,3.59,4.11,4.62,5.09,5.55,5.97,6.37,6.72,7.04,7.32,7.56,7.75,7.89,7.97,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,7.93,7.73,7.44,7.05,6.6,6.08,5.53,4.96,4.37,3.78,3.2,2.63,2.09,1.6,1.15,0.76,0.44,0.21,0.05,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.11,-0.4,-0.88,-1.52,-2.28,-3.14,-4.12,-5.13,-6.21,-7.31,-8.42,-9.49,-10.56,-11.56,-12.51,-13.36,-14.12,-14.77,-15.29,-15.68,-15.92,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-15.94,-15.75,-15.45,-15.05,-14.55,-13.98,-13.33,-12.62,-11.86,-11.05,-10.2,-9.33,-8.45,-7.55,-6.67,-5.8,-4.95,-4.14,-3.38,-2.67,-2.02,-1.45,-0.95,-0.55,-0.25,-0.06,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_EYE_L_OPEN=1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.74,0.26,0,0.24,0.64,0.91,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.76,0.36,0.09,0,0.24,0.64,0.91,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.76,0.36,0.09,0,0.24,0.64,0.91,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.76,0.36,0.09,0,0.24,0.64,0.91,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
PARAM_EYE_L_SMILE=0
PARAM_EYE_R_OPEN=1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.74,0.26,0,0.24,0.64,0.91,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.76,0.36,0.09,0,0.24,0.64,0.91,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.76,0.36,0.09,0,0.24,0.64,0.91,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.76,0.36,0.09,0,0.24,0.64,0.91,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
PARAM_EYE_R_SMILE=0
PARAM_EYE_FORM=0
PARAM_EYE_BALL_X=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.012,0.04,0.09,0.15,0.22,0.29,0.36,0.43,0.48,0.53,0.57,0.59,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.6,0.598,0.591,0.579,0.564,0.546,0.52,0.5,0.47,0.44,0.41,0.38,0.35,0.32,0.28,0.25,0.22,0.19,0.16,0.13,0.1,0.08,0.05,0.036,0.021,0.009,0.002,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_EYE_BALL_Y=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.003,0.01,0.02,0.033,0.048,0.063,0.078,0.092,0.105,0.115,0.123,0.128,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.13,0.129,0.128,0.126,0.122,0.118,0.114,0.108,0.103,0.096,0.09,0.083,0.076,0.069,0.061,0.054,0.047,0.04,0.034,0.027,0.022,0.016,0.012,0.008,0.004,0.002,0.001,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_EYE_BALL_FORM=0
PARAM_BROW_L_Y=0
PARAM_BROW_R_Y=0
PARAM_BROW_L_X=0
PARAM_BROW_R_X=0
PARAM_BROW_L_ANGLE=0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58,0.58
PARAM_BROW_R_ANGLE=0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57,0.57
PARAM_BROW_L_FORM=0
PARAM_BROW_R_FORM=0
PARAM_MOUTH_FORM=1
PARAM_MOUTH_OPEN_Y=0
PARAM_CHEEK=0
PARAM_SWEAT=0
PARAM_BODY_ANGLE_X=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.05,-0.19,-0.41,-0.7,-1.02,-1.39,-1.76,-2.14,-2.51,-2.86,-3.18,-3.46,-3.69,-3.86,-3.96,-4,-3.95,-3.82,-3.61,-3.34,-3,-2.63,-2.2,-1.76,-1.28,-0.8,-0.32,0.15,0.62,1.06,1.47,1.84,2.18,2.46,2.69,2.86,2.96,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2.985,2.94,2.87,2.78,2.66,2.53,2.38,2.22,2.05,1.87,1.69,1.5,1.31,1.13,0.95,0.78,0.62,0.47,0.34,0.22,0.13,0.06,0.02,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.018,0.07,0.15,0.25,0.38,0.52,0.67,0.83,1,1.17,1.33,1.48,1.62,1.75,1.85,1.93,1.98,2,1.95,1.8,1.56,1.24,0.86,0.43,-0.06,-0.56,-1.1,-1.65,-2.21,-2.75,-3.28,-3.78,-4.25,-4.68,-5.06,-5.39,-5.65,-5.84,-5.96,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-5.981,-5.92,-5.83,-5.71,-5.57,-5.4,-5.2,-4.99,-4.76,-4.52,-4.26,-4,-3.73,-3.46,-3.18,-2.89,-2.62,-2.35,-2.08,-1.82,-1.56,-1.32,-1.1,-0.89,-0.7,-0.52,-0.37,-0.24,-0.14,-0.06,-0.02,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_BODY_ANGLE_Z=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.04,-0.14,-0.31,-0.52,-0.77,-1.04,-1.32,-1.61,-1.88,-2.15,-2.39,-2.6,-2.77,-2.89,-2.97,-3,-2.93,-2.75,-2.45,-2.05,-1.58,-1.04,-0.42,0.21,0.88,1.57,2.26,2.93,3.6,4.23,4.82,5.35,5.82,6.23,6.56,6.8,6.95,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,6.96,6.86,6.7,6.48,6.22,5.91,5.56,5.19,4.79,4.36,3.94,3.5,3.06,2.64,2.21,1.81,1.44,1.09,0.78,0.52,0.3,0.14,0.04,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.018,0.07,0.15,0.25,0.38,0.52,0.67,0.83,1,1.17,1.33,1.48,1.62,1.75,1.85,1.93,1.98,2,1.95,1.81,1.59,1.29,0.93,0.53,0.07,-0.4,-0.91,-1.43,-1.95,-2.45,-2.95,-3.42,-3.86,-4.26,-4.62,-4.92,-5.17,-5.35,-5.46,-5.5,-5.496,-5.483,-5.463,-5.44,-5.41,-5.37,-5.33,-5.29,-5.25,-5.21,-5.17,-5.13,-5.09,-5.06,-5.04,-5.017,-5.004,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-4.984,-4.94,-4.86,-4.76,-4.64,-4.5,-4.34,-4.16,-3.97,-3.76,-3.55,-3.34,-3.11,-2.88,-2.65,-2.41,-2.18,-1.95,-1.73,-1.51,-1.3,-1.1,-0.92,-0.74,-0.58,-0.44,-0.31,-0.2,-0.12,-0.05,-0.01,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_BODY_ANGLE_Y=0
PARAM_BREATH=0
PARAM_HAIR_FRONT=0
PARAM_HAIR_BACK=0
PARAM_NECKLACE=0
VISIBLE:PARTS_01_CLOTHES_01_ARM_L_02=0
VISIBLE:PARTS_01_CLOTHES_01_ARM_R_02=0
VISIBLE:PARTS_01_CLOTHES_01_ARM_L_01=1
VISIBLE:PARTS_01_CLOTHES_01_ARM_R_01=1

View file

@ -0,0 +1,36 @@
# Live2D Animator Motion Data
$fps=30
PARAM_ANGLE_X=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.26,1,2.11,3.56,5.26,7.19,9.25,11.38,13.6,15.83,18.01,20.14,22.16,24,25.7,27.15,28.34,29.23,29.8,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,29.85,29.42,28.75,27.85,26.79,25.54,24.16,22.68,21.12,19.47,17.8,16.08,14.34,12.66,11,9.37,7.84,6.38,5.02,3.8,2.72,1.79,1.03,0.47,0.12,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.05,0.18,0.39,0.66,1,1.39,1.81,2.27,2.76,3.25,3.75,4.24,4.73,5.19,5.61,6,6.34,6.61,6.82,6.95,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,6.79,6.24,5.45,4.46,3.37,2.22,1.07,-0.06,-1.11,-2.06,-2.84,-3.46,-3.86,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-3.98,-3.92,-3.83,-3.7,-3.55,-3.38,-3.18,-2.97,-2.75,-2.51,-2.27,-2.03,-1.79,-1.55,-1.32,-1.1,-0.89,-0.7,-0.53,-0.37,-0.24,-0.14,-0.06,-0.02,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_ANGLE_Y=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.21,0.76,1.55,2.54,3.63,4.78,5.93,7.06,8.11,9.06,9.84,10.46,10.86,11,10.63,9.59,7.95,5.81,3.29,0.39,-2.77,-6.09,-9.5,-12.91,-16.23,-19.39,-22.29,-24.81,-26.95,-28.59,-29.63,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-29.85,-29.42,-28.75,-27.85,-26.79,-25.54,-24.16,-22.68,-21.12,-19.47,-17.8,-16.08,-14.34,-12.66,-11,-9.37,-7.84,-6.38,-5.02,-3.8,-2.72,-1.79,-1.03,-0.47,-0.12,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.07,0.26,0.55,0.95,1.43,1.99,2.59,3.25,3.94,4.65,5.35,6.06,6.75,7.41,8.01,8.57,9.05,9.45,9.74,9.93,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,9.5,8.21,6.33,4,1.41,-1.29,-4.03,-6.69,-9.17,-11.41,-13.27,-14.73,-15.67,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-15.92,-15.69,-15.31,-14.82,-14.21,-13.52,-12.72,-11.88,-10.98,-10.04,-9.1,-8.12,-7.16,-6.21,-5.28,-4.39,-3.57,-2.81,-2.11,-1.5,-0.98,-0.56,-0.25,-0.06,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_ANGLE_Z=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.13,-0.48,-0.99,-1.61,-2.31,-3.04,-3.78,-4.49,-5.16,-5.76,-6.26,-6.66,-6.91,-7,-6.78,-6.17,-5.24,-4.03,-2.62,-1.01,0.7,2.49,4.33,6.2,8.01,9.78,11.46,13,14.42,15.63,16.62,17.36,17.83,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,17.91,17.65,17.25,16.71,16.07,15.32,14.5,13.61,12.67,11.68,10.68,9.65,8.6,7.59,6.6,5.62,4.71,3.83,3.01,2.28,1.63,1.07,0.62,0.28,0.07,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.013,0.05,0.11,0.19,0.29,0.4,0.52,0.65,0.79,0.93,1.07,1.21,1.35,1.48,1.6,1.71,1.81,1.89,1.95,1.99,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1.89,1.59,1.15,0.62,0.02,-0.61,-1.24,-1.85,-2.42,-2.94,-3.37,-3.71,-3.92,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-3.98,-3.92,-3.83,-3.7,-3.55,-3.38,-3.18,-2.97,-2.75,-2.51,-2.27,-2.03,-1.79,-1.55,-1.32,-1.1,-0.89,-0.7,-0.53,-0.37,-0.24,-0.14,-0.06,-0.02,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_EYE_L_OPEN=0
PARAM_EYE_L_SMILE=0
PARAM_EYE_R_OPEN=0
PARAM_EYE_R_SMILE=0
PARAM_EYE_FORM=0
PARAM_EYE_BALL_X=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.014,-0.05,-0.11,-0.18,-0.25,-0.33,-0.42,-0.49,-0.56,-0.61,-0.65,-0.68,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.687,-0.677,-0.661,-0.64,-0.62,-0.59,-0.56,-0.52,-0.49,-0.45,-0.41,-0.37,-0.33,-0.29,-0.25,-0.22,-0.18,-0.15,-0.12,-0.09,-0.06,-0.04,-0.024,-0.011,-0.003,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.001,-0.002,-0.004,-0.007,-0.009,-0.013,-0.016,-0.02,-0.025,-0.03,-0.035,-0.041,-0.047,-0.054,-0.061,-0.068,-0.075,-0.083,-0.091,-0.1,-0.109,-0.117,-0.127,-0.136,-0.146,-0.155,-0.165,-0.176,-0.186,-0.197,-0.207,-0.218,-0.229,-0.24,-0.252,-0.263,-0.274,-0.286,-0.297,-0.309,-0.32,-0.332,-0.343,-0.355,-0.367,-0.378,-0.39,-0.401,-0.413,-0.424,-0.435,-0.446,-0.457,-0.468,-0.479,-0.49,-0.5,-0.511,-0.521,-0.531,-0.541,-0.55,-0.56,-0.569,-0.578,-0.586,-0.595,-0.603,-0.611,-0.618,-0.625,-0.632,-0.639,-0.645,-0.651,-0.656,-0.662,-0.666,-0.671,-0.675,-0.678,-0.681,-0.684,-0.686,-0.688,-0.689,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.69,-0.686,-0.677,-0.66,-0.64,-0.61,-0.58,-0.55,-0.51,-0.47,-0.43,-0.39,-0.35,-0.31,-0.27,-0.23,-0.19,-0.15,-0.12,-0.09,-0.06,-0.04,-0.024,-0.011,-0.003,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_EYE_BALL_Y=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.019,-0.07,-0.14,-0.23,-0.33,-0.43,-0.54,-0.64,-0.74,-0.82,-0.89,-0.95,-0.99,-1,-0.982,-0.93,-0.85,-0.75,-0.62,-0.48,-0.33,-0.17,0,0.17,0.33,0.48,0.62,0.75,0.85,0.93,0.98,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.995,0.982,0.961,0.94,0.91,0.88,0.85,0.82,0.8,0.78,0.763,0.753,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.746,0.736,0.719,0.7,0.67,0.64,0.6,0.57,0.53,0.49,0.44,0.4,0.36,0.32,0.28,0.23,0.2,0.16,0.13,0.09,0.07,0.04,0.026,0.012,0.003,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.001,0.003,0.005,0.007,0.01,0.014,0.018,0.022,0.027,0.033,0.038,0.045,0.052,0.059,0.066,0.074,0.082,0.09,0.099,0.108,0.118,0.128,0.138,0.148,0.158,0.169,0.18,0.191,0.202,0.214,0.226,0.237,0.249,0.261,0.274,0.286,0.298,0.311,0.323,0.336,0.348,0.361,0.373,0.386,0.399,0.411,0.424,0.436,0.448,0.461,0.473,0.485,0.497,0.509,0.521,0.533,0.544,0.555,0.566,0.577,0.588,0.598,0.608,0.618,0.628,0.637,0.647,0.655,0.664,0.672,0.68,0.687,0.694,0.701,0.708,0.714,0.719,0.724,0.729,0.733,0.737,0.74,0.743,0.746,0.748,0.749,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.746,0.735,0.718,0.69,0.67,0.63,0.6,0.56,0.51,0.47,0.43,0.38,0.34,0.29,0.25,0.21,0.17,0.13,0.1,0.07,0.05,0.026,0.012,0.003,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_EYE_BALL_FORM=0
PARAM_BROW_L_Y=0
PARAM_BROW_R_Y=0
PARAM_BROW_L_X=0
PARAM_BROW_R_X=0
PARAM_BROW_L_ANGLE=0
PARAM_BROW_R_ANGLE=0
PARAM_BROW_L_FORM=0
PARAM_BROW_R_FORM=0
PARAM_MOUTH_FORM=0
PARAM_MOUTH_OPEN_Y=0
PARAM_CHEEK=0
PARAM_SWEAT=0
PARAM_BODY_ANGLE_X=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.08,-0.27,-0.56,-0.92,-1.32,-1.74,-2.16,-2.57,-2.95,-3.29,-3.58,-3.8,-3.95,-4,-3.89,-3.57,-3.09,-2.46,-1.72,-0.89,0.01,0.93,1.89,2.86,3.81,4.73,5.6,6.4,7.14,7.77,8.28,8.67,8.91,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,8.95,8.83,8.62,8.36,8.04,7.66,7.25,6.8,6.34,5.84,5.34,4.82,4.3,3.8,3.3,2.81,2.35,1.91,1.51,1.14,0.81,0.54,0.31,0.14,0.04,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.016,0.06,0.14,0.24,0.37,0.52,0.69,0.88,1.08,1.29,1.52,1.76,2,2.25,2.5,2.75,3,3.24,3.48,3.71,3.92,4.12,4.31,4.48,4.63,4.76,4.86,4.94,4.98,5,4.91,4.66,4.26,3.73,3.12,2.41,1.64,0.83,0,-0.83,-1.64,-2.41,-3.12,-3.73,-4.26,-4.66,-4.91,-5,-4.91,-4.67,-4.3,-3.81,-3.25,-2.6,-1.92,-1.21,-0.47,0.28,1,1.71,2.39,3,3.57,4.05,4.45,4.74,4.93,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,4.97,4.9,4.79,4.63,4.44,4.22,3.98,3.71,3.43,3.14,2.84,2.54,2.24,1.94,1.65,1.37,1.12,0.88,0.66,0.47,0.31,0.18,0.08,0.02,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_BODY_ANGLE_Z=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.04,-0.14,-0.28,-0.46,-0.66,-0.87,-1.08,-1.28,-1.47,-1.65,-1.79,-1.9,-1.97,-2,-1.92,-1.7,-1.37,-0.93,-0.42,0.16,0.77,1.42,2.08,2.75,3.4,4.04,4.65,5.2,5.71,6.15,6.5,6.77,6.94,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,6.96,6.86,6.7,6.48,6.22,5.91,5.56,5.19,4.79,4.37,3.93,3.48,3.02,2.56,2.1,1.64,1.2,0.76,0.35,-0.05,-0.41,-0.75,-1.06,-1.33,-1.56,-1.75,-1.89,-1.97,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-1.984,-1.94,-1.86,-1.76,-1.63,-1.48,-1.31,-1.12,-0.92,-0.71,-0.48,-0.24,0,0.25,0.5,0.75,1,1.24,1.48,1.71,1.92,2.12,2.31,2.48,2.63,2.76,2.86,2.94,2.98,3,2.95,2.79,2.55,2.24,1.87,1.45,0.99,0.5,0,-0.5,-0.99,-1.45,-1.87,-2.24,-2.55,-2.79,-2.95,-3,-2.93,-2.73,-2.44,-2.05,-1.6,-1.08,-0.53,0.04,0.63,1.22,1.8,2.37,2.91,3.4,3.85,4.24,4.56,4.79,4.95,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,4.97,4.9,4.79,4.63,4.44,4.22,3.98,3.71,3.43,3.14,2.84,2.54,2.24,1.94,1.65,1.37,1.12,0.88,0.66,0.47,0.31,0.18,0.08,0.02,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_BODY_ANGLE_Y=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.03,0.1,0.22,0.38,0.56,0.78,1.01,1.25,1.5,1.75,1.99,2.22,2.44,2.62,2.78,2.9,2.97,3,2.87,2.52,2.01,1.39,0.69,-0.04,-0.78,-1.49,-2.16,-2.76,-3.26,-3.66,-3.91,-4,-3.96,-3.83,-3.65,-3.41,-3.12,-2.8,-2.46,-2.1,-1.73,-1.36,-1,-0.64,-0.31,0,0.28,0.53,0.72,0.87,0.97,1,0.996,0.984,0.966,0.94,0.91,0.87,0.83,0.79,0.74,0.69,0.64,0.58,0.53,0.47,0.42,0.36,0.31,0.26,0.21,0.17,0.13,0.09,0.06,0.03,0.016,0.004,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_BREATH=0
PARAM_HAIR_FRONT=0
PARAM_HAIR_BACK=0
PARAM_NECKLACE=0
VISIBLE:PARTS_01_CLOTHES_01_ARM_L_02=0
VISIBLE:PARTS_01_CLOTHES_01_ARM_R_02=0
VISIBLE:PARTS_01_CLOTHES_01_ARM_L_01=1
VISIBLE:PARTS_01_CLOTHES_01_ARM_R_01=1

View file

@ -0,0 +1,33 @@
# Live2D Animator Motion Data
$fps=30
PARAM_ANGLE_X=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.1,0.38,0.83,1.42,2.13,2.95,3.86,4.81,5.82,6.85,7.9,8.9,9.9,10.84,11.73,12.52,13.24,13.85,14.34,14.7,14.92,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,14.88,14.53,13.98,13.28,12.42,11.46,10.4,9.27,8.09,6.89,5.65,4.43,3.24,2.06,0.97,-0.06,-1.01,-1.86,-2.59,-3.18,-3.63,-3.9,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4
PARAM_ANGLE_Y=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.06,-0.23,-0.5,-0.85,-1.28,-1.77,-2.32,-2.88,-3.49,-4.11,-4.74,-5.34,-5.94,-6.5,-7.04,-7.51,-7.94,-8.31,-8.6,-8.82,-8.95,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9
PARAM_ANGLE_Z=0,-0.14,-0.53,-1.13,-1.9,-2.81,-3.83,-4.93,-6.07,-7.25,-8.45,-9.61,-10.74,-11.82,-12.8,-13.71,-14.48,-15.12,-15.59,-15.89,-16,-15.9,-15.6,-15.14,-14.55,-13.83,-13.02,-12.13,-11.18,-10.18,-9.17,-8.13,-7.1,-6.09,-5.11,-4.18,-3.32,-2.52,-1.8,-1.19,-0.69,-0.31,-0.08,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.05,0.18,0.39,0.66,1,1.37,1.8,2.24,2.72,3.2,3.68,4.15,4.62,5.06,5.47,5.84,6.18,6.46,6.69,6.86,6.96,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,6.95,6.82,6.61,6.33,5.99,5.59,5.14,4.64,4.12,3.54,2.95,2.34,1.71,1.06,0.41,-0.25,-0.89,-1.53,-2.15,-2.76,-3.35,-3.91,-4.43,-4.93,-5.37,-5.78,-6.13,-6.43,-6.67,-6.85,-6.96,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7
PARAM_EYE_L_OPEN=1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.76,0.36,0.09,0,0,0,0.18,0.46,0.73,0.92,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.76,0.36,0.09,0,0,0,0.18,0.46,0.73,0.92,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.76,0.36,0.09,0,0,0,0.18,0.46,0.73,0.92,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
PARAM_EYE_L_SMILE=0
PARAM_EYE_R_OPEN=1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.76,0.36,0.09,0,0,0,0.18,0.46,0.73,0.92,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.76,0.36,0.09,0,0,0,0.18,0.46,0.73,0.92,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.76,0.36,0.09,0,0,0,0.18,0.46,0.73,0.92,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
PARAM_EYE_R_SMILE=0
PARAM_EYE_FORM=0
PARAM_EYE_BALL_X=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.001,0,0.002,0.003,0,0.004,0,0.006,0.006,0.01,0.007,0.01,0.008,0.01,0.009,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.004,-0.011,-0.03,-0.06,-0.09,-0.12,-0.15,-0.18,-0.21,-0.24,-0.26,-0.275,-0.286,-0.29,-0.29,-0.29,-0.29,-0.29,-0.29,-0.29,-0.29,-0.29,-0.29,-0.29,-0.29,-0.29,-0.29,-0.29,-0.29,-0.29,-0.29,-0.29,-0.29,-0.29,-0.29,-0.29,-0.29,-0.29,-0.285,-0.272,-0.25,-0.22,-0.19,-0.16,-0.12,-0.09,-0.05,-0.02,0.01,0.04,0.06,0.076,0.087,0.09,0.09,0.09,0.09,0.09,0.09,0.09,0.09,0.09,0.09,0.09,0.09,0.09,0.09,0.09,0.09,0.09,0.09,0.09,0.09,0.09,0.09,0.09,0.09,0.09,0.09,0.09,0.09,0.09,0.09,0.09,0.09,0.09,0.09,0.09,0.09,0.09,0.09,0.09,0.09,0.09,0.09,0.09
PARAM_EYE_BALL_Y=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.001,0.004,0.009,0.014,0.022,0.03,0.039,0.048,0.058,0.068,0.079,0.089,0.099,0.109,0.118,0.127,0.135,0.142,0.148,0.153,0.157,0.159,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.166,0.183,0.21,0.24,0.27,0.31,0.34,0.38,0.41,0.44,0.46,0.483,0.496,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.498,0.492,0.482,0.47,0.456,0.441,0.425,0.409,0.393,0.378,0.365,0.353,0.343,0.336,0.332,0.33,0.33,0.33,0.33,0.33,0.33,0.33,0.33,0.33,0.33,0.33,0.33,0.33,0.33,0.33,0.33,0.33,0.33,0.33,0.33,0.33,0.33,0.33,0.33,0.33,0.33,0.33,0.33,0.33,0.33,0.33,0.33,0.33,0.33,0.33,0.33,0.33,0.33,0.33,0.33,0.33,0.33,0.33
PARAM_EYE_BALL_FORM=0
PARAM_BROW_L_Y=0
PARAM_BROW_R_Y=0
PARAM_BROW_L_X=0
PARAM_BROW_R_X=0
PARAM_BROW_L_ANGLE=0
PARAM_BROW_R_ANGLE=0
PARAM_BROW_L_FORM=0
PARAM_BROW_R_FORM=0
PARAM_MOUTH_FORM=0
PARAM_MOUTH_OPEN_Y=0,0.2,0.4,1,1,1,0.08,0.74,1,1,1,0.96,0.64,0.32,0,0.59,1,1,1,1,0.42,0.44,0.46,1,1,1,1,0.9,0.73,0.99,1,0.9,0.55,0.52,0.49,0.6,0.71,0.69,0.675,0.47,0.26,0.15,0.05,0.046,0.045,0.043,0.042,0.041,0.04,0.038,0.037,0.036,0.035,0.033,0.032,0.031,0.03,0.028,0.027,0.026,0.025,0.024,0.022,0.021,0.02,0.019,0.017,0.016,0.015,0.014,0.012,0.011,0.01,0.009,0.007,0.006,0.005,0.004,0.002,0.001,0,0.34,0.68,0.95,1,1,0.8,0.59,0.38,0.23,0.08,0.075,0.071,0.068,0.064,0.061,0.057,0.053,0.05,0.046,0.043,0.039,0.036,0.032,0.029,0.025,0.021,0.018,0.014,0.011,0.007,0.004,0,0.14,0.28,0.14,0,0.14,0.27,0.85,1,1,1,1,0.05,0.03,0.01,0.14,0.28,0.41,0.55,1,1,1,0.31,0.98,1,0.84,0.02,0.05,0.08,0.35,0.62,0.47,0.32,0.16,0,0.04,0.08,0.35,0.62,0.47,0.32,0.16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_CHEEK=0
PARAM_SWEAT=0
PARAM_ARM_L=0
PARAM_BODY_ANGLE_X=0,-0.03,-0.13,-0.28,-0.47,-0.7,-0.96,-1.23,-1.52,-1.81,-2.11,-2.4,-2.68,-2.95,-3.2,-3.43,-3.62,-3.78,-3.9,-3.97,-4,-3.97,-3.9,-3.78,-3.64,-3.46,-3.25,-3.03,-2.79,-2.54,-2.29,-2.03,-1.77,-1.52,-1.28,-1.05,-0.83,-0.63,-0.45,-0.3,-0.17,-0.08,-0.02,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.05,0.21,0.44,0.75,1.12,1.54,1.99,2.46,2.94,3.42,3.88,4.32,4.73,5.09,5.4,5.66,5.84,5.96,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,5.96,5.84,5.65,5.39,5.09,4.74,4.35,3.93,3.48,3.02,2.54,2.05,1.56,1.09,0.62,0.15,-0.28,-0.69,-1.08,-1.42,-1.73,-1.99,-2.21,-2.37,-2.47,-2.5,-2.5,-2.5,-2.5,-2.5,-2.5,-2.5,-2.5,-2.5,-2.5,-2.5,-2.5,-2.5,-2.5,-2.5,-2.5,-2.5,-2.5,-2.5,-2.5,-2.5,-2.5,-2.5,-2.5,-2.5,-2.5,-2.5,-2.5,-2.5,-2.5,-2.5,-2.5,-2.5
PARAM_BODY_ANGLE_Z=0,-0.021,-0.08,-0.17,-0.3,-0.45,-0.63,-0.82,-1.04,-1.27,-1.51,-1.75,-2,-2.25,-2.49,-2.73,-2.96,-3.18,-3.38,-3.55,-3.7,-3.83,-3.92,-3.98,-4,-3.98,-3.92,-3.83,-3.7,-3.55,-3.38,-3.18,-2.96,-2.73,-2.49,-2.25,-2,-1.75,-1.51,-1.27,-1.04,-0.82,-0.63,-0.45,-0.3,-0.17,-0.08,-0.02,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.018,0.07,0.15,0.25,0.37,0.51,0.66,0.82,0.98,1.14,1.29,1.44,1.58,1.7,1.8,1.89,1.95,1.99,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1.981,1.92,1.83,1.71,1.56,1.38,1.18,0.95,0.71,0.45,0.18,-0.09,-0.38,-0.67,-0.96,-1.25,-1.54,-1.83,-2.1,-2.37,-2.62,-2.86,-3.08,-3.28,-3.46,-3.62,-3.75,-3.86,-3.94,-3.98,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4
PARAM_BODY_ANGLE_Y=0
PARAM_BREATH=0
PARAM_HAIR_FRONT=0
PARAM_HAIR_BACK=0
PARAM_ACCESSORIES=0

View file

@ -0,0 +1,33 @@
# Live2D Animator Motion Data
$fps=30
PARAM_ANGLE_X=0,0.001,0.006,0.013,0.023,0.036,0.052,0.071,0.09,0.12,0.15,0.18,0.21,0.25,0.29,0.33,0.38,0.43,0.48,0.54,0.6,0.66,0.73,0.8,0.87,0.94,1.02,1.11,1.19,1.28,1.38,1.47,1.57,1.68,1.78,1.89,2.01,2.13,2.25,2.37,2.5,2.64,2.77,2.91,3.04,3.17,3.3,3.42,3.54,3.65,3.76,3.86,3.96,4.06,4.15,4.24,4.33,4.41,4.48,4.55,4.61,4.67,4.73,4.78,4.83,4.87,4.9,4.93,4.96,4.975,4.989,4.997,5,4.96,4.84,4.67,4.44,4.18,3.87,3.55,3.21,2.86,2.5,2.15,1.81,1.48,1.17,0.89,0.64,0.42,0.26,0.14,0.07,0.03,0.01,-0.007,-0.013,-0.014,-0.012,-0.008,-0.004,-0.001,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_ANGLE_Y=0,0,0,0,0,0,0,0.8,2.82,5.55,8.74,12.06,15.33,18.43,21.04,23.14,24.5,25,24.75,24.04,22.89,21.38,19.52,17.4,14.97,12.38,9.63,6.75,3.86,0.86,-2.08,-4.99,-7.82,-10.55,-13.06,-15.41,-17.54,-19.41,-21.01,-22.28,-23.22,-23.8,-24,-23.98,-23.91,-23.81,-23.66,-23.48,-23.26,-23.01,-22.72,-22.41,-22.06,-21.69,-21.29,-20.87,-20.42,-19.96,-19.47,-18.96,-18.43,-17.9,-17.34,-16.77,-16.19,-15.6,-15.01,-14.41,-13.79,-13.17,-12.56,-11.93,-11.31,-10.68,-10.07,-9.44,-8.83,-8.21,-7.61,-7.02,-6.42,-5.84,-5.28,-4.73,-4.19,-3.66,-3.15,-2.66,-2.19,-1.73,-1.3,-0.89,-0.51,-0.16,0.07,0.19,0.23,0.211,0.17,0.11,0.05,0.02,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_ANGLE_Z=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.07,0.27,0.59,1.02,1.56,2.19,2.9,3.68,4.53,5.44,6.4,7.39,8.42,9.46,10.5,11.54,12.58,13.61,14.6,15.56,16.47,17.32,18.1,18.81,19.44,19.98,20.41,20.73,20.93,21,20.81,20.27,19.45,18.39,17.14,15.73,14.22,12.65,11.03,9.39,7.79,6.23,4.75,3.4,2.15,1.09,0.21,-0.43,-0.85,-1,-0.97,-0.88,-0.75,-0.6,-0.44,-0.3,-0.17,-0.08,-0.02,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_EYE_L_OPEN=1.06,1.06,1.06,1.06,1.06,1.059,1.06,1.057,1.057,1.056,1.055,1.053,1.052,1.05,1.049,1.047,1.045,1.043,1.041,1.039,1.036,1.034,1.031,1.028,1.025,1.022,1.019,1.015,1.012,1.008,1.004,1,0.76,0.36,0.09,0,0,0,0.18,0.46,0.73,0.92,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.76,0.36,0.09,0,0,0,0.18,0.46,0.73,0.92,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
PARAM_EYE_L_SMILE=0
PARAM_EYE_R_OPEN=1.06,1.06,1.06,1.06,1.06,1.059,1.06,1.057,1.057,1.056,1.055,1.053,1.052,1.05,1.049,1.047,1.045,1.043,1.041,1.039,1.036,1.034,1.031,1.028,1.025,1.022,1.019,1.015,1.012,1.008,1.004,1,0.76,0.36,0.09,0,0,0,0.18,0.46,0.73,0.92,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.76,0.36,0.09,0,0,0,0.18,0.46,0.73,0.92,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
PARAM_EYE_R_SMILE=0
PARAM_EYE_FORM=0
PARAM_EYE_BALL_X=0
PARAM_EYE_BALL_Y=0,-0.007,-0.027,-0.06,-0.09,-0.14,-0.18,-0.23,-0.28,-0.33,-0.38,-0.43,-0.47,-0.51,-0.54,-0.56,-0.575,-0.58,-0.58,-0.58,-0.58,-0.58,-0.58,-0.58,-0.58,-0.58,-0.58,-0.578,-0.577,-0.576,-0.574,-0.571,-0.569,-0.566,-0.562,-0.558,-0.553,-0.548,-0.542,-0.535,-0.528,-0.521,-0.512,-0.503,-0.494,-0.485,-0.475,-0.466,-0.457,-0.448,-0.439,-0.43,-0.421,-0.412,-0.403,-0.394,-0.385,-0.376,-0.368,-0.359,-0.35,-0.342,-0.333,-0.325,-0.316,-0.308,-0.3,-0.291,-0.283,-0.275,-0.267,-0.259,-0.252,-0.244,-0.236,-0.229,-0.221,-0.214,-0.206,-0.199,-0.192,-0.185,-0.178,-0.171,-0.165,-0.158,-0.152,-0.145,-0.139,-0.133,-0.127,-0.121,-0.115,-0.109,-0.104,-0.098,-0.093,-0.088,-0.083,-0.078,-0.073,-0.068,-0.064,-0.059,-0.055,-0.051,-0.047,-0.043,-0.039,-0.036,-0.032,-0.029,-0.026,-0.023,-0.02,-0.018,-0.016,-0.013,-0.011,-0.009,-0.008,-0.006,-0.005,-0.003,-0.002,-0.002,-0.001,0,0,0
PARAM_EYE_BALL_FORM=0
PARAM_BROW_L_Y=1
PARAM_BROW_R_Y=1
PARAM_BROW_L_X=0
PARAM_BROW_R_X=0
PARAM_BROW_L_ANGLE=-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37,-0.37
PARAM_BROW_R_ANGLE=-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45,-0.45
PARAM_BROW_L_FORM=-2
PARAM_BROW_R_FORM=-2
PARAM_MOUTH_FORM=1
PARAM_MOUTH_OPEN_Y=0,0.73,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.92,0.05,0.89,1,1,0.36,0.38,0.41,0.86,1,1,1,0.98,0.66,0.34,0.02,0.15,0.28,0.41,0.54,0.68,0.81,0.66,0.52,0.502,0.486,0.471,0.455,0.49,0.52,0.33,0.14,0.25,0.36,0.69,1,1,1,1,1,1,1,0.992,0.14,0.4,0.66,1,1,1,1,1,1,1,1,0.62,0.02,0.012,0,0.19,0.38,0.58,0.77,1,1,1,1,1,1,1,0.93,0.77,0.6,0.608,0.612,0.53,0.44,0.435,0.431,0.25,0.08,0.21,0.35,0.48,0.61,0.607,0.601,0.596,0.591,0.586,0.58,0.34,0.1,0.26,0.42,0.24,0.06,0.22,0.38,0.19,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_CHEEK=0
PARAM_SWEAT=0
PARAM_ARM_L=0
PARAM_BODY_ANGLE_X=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.05,0.18,0.39,0.65,0.94,1.27,1.61,1.95,2.3,2.63,2.95,3.24,3.49,3.7,3.86,3.96,4,3.98,3.92,3.83,3.71,3.56,3.39,3.19,2.99,2.77,2.54,2.3,2.06,1.83,1.59,1.37,1.15,0.95,0.76,0.59,0.44,0.31,0.21,0.13,0.08,0.068,0.07,0.07,0.07,0.07,0.07,0.07,0.07,0.07,0.07,0.07,0.07,0.07,0.07,0.07,0.07,0.07,0.07,0.07,0.07,0.07,0.07,0.07,0.07,0.07,0.07,0.07,0.07
PARAM_BODY_ANGLE_Z=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.04,0.15,0.33,0.57,0.86,1.19,1.55,1.95,2.36,2.79,3.21,3.64,4.05,4.45,4.81,5.14,5.43,5.67,5.85,5.96,6,5.96,5.85,5.67,5.44,5.15,4.83,4.47,4.08,3.68,3.26,2.84,2.42,2.02,1.63,1.27,0.95,0.66,0.43,0.25,0.14,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1
PARAM_BODY_ANGLE_Y=0,0,0,0,0,0,0,0.19,0.68,1.33,2.1,2.89,3.68,4.42,5.05,5.55,5.88,6,5.996,5.983,5.96,5.93,5.89,5.85,5.79,5.73,5.65,5.57,5.49,5.39,5.29,5.18,5.06,4.93,4.8,4.67,4.52,4.37,4.21,4.05,3.88,3.71,3.53,3.32,3.1,2.88,2.64,2.41,2.17,1.92,1.68,1.45,1.22,1.01,0.81,0.62,0.45,0.3,0.17,0.07,0,-0.06,-0.11,-0.16,-0.2,-0.23,-0.26,-0.29,-0.31,-0.34,-0.36,-0.39,-0.41,-0.44,-0.47,-0.5,-0.53,-0.57,-0.62,-0.68,-0.73,-0.79,-0.84,-0.88,-0.92,-0.95,-0.98,-0.994,-1,-0.981,-0.93,-0.86,-0.77,-0.67,-0.57,-0.47,-0.36,-0.27,-0.18,-0.11,-0.06,-0.02,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01,-0.01
PARAM_BREATH=0
PARAM_HAIR_FRONT=0
PARAM_HAIR_BACK=0
PARAM_ACCESSORIES=0

View file

@ -0,0 +1,33 @@
# Live2D Animator Motion Data
$fps=30
PARAM_ANGLE_X=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.013,0.05,0.12,0.21,0.32,0.45,0.6,0.76,0.95,1.14,1.36,1.58,1.82,2.06,2.31,2.57,2.83,3.1,3.37,3.63,3.9,4.17,4.43,4.69,4.94,5.18,5.42,5.64,5.86,6.05,6.24,6.4,6.55,6.68,6.79,6.88,6.95,6.99,7,6.96,6.86,6.71,6.5,6.25,5.96,5.64,5.29,4.93,4.54,4.15,3.75,3.35,2.95,2.57,2.19,1.83,1.49,1.17,0.89,0.63,0.42,0.24,0.11,0.03,0,0.03,0.12,0.25,0.4,0.56,0.7,0.83,0.92,0.98,1,0.93,0.79,0.62,0.43,0.27,0.13,0.03,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_ANGLE_Y=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.019,0.08,0.17,0.29,0.45,0.64,0.85,1.09,1.35,1.63,1.94,2.26,2.59,2.94,3.3,3.67,4.05,4.42,4.81,5.19,5.58,5.95,6.33,6.7,7.06,7.41,7.74,8.06,8.37,8.65,8.91,9.15,9.36,9.55,9.71,9.83,9.92,9.98,10,9.8,9.23,8.33,7.14,5.72,4.05,2.22,0.24,-1.84,-4.05,-6.27,-8.57,-10.88,-13.12,-15.33,-17.51,-19.54,-21.49,-23.3,-24.93,-26.38,-27.62,-28.62,-29.37,-29.84,-30,-28.59,-24.89,-19.55,-13.44,-7.1,-1.19,3.86,7.71,10.14,11,9.23,5.41,0.64,-4.31,-8.84,-12.58,-15.07,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16,-16
PARAM_ANGLE_Z=-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-29.83,-29.35,-28.58,-27.57,-26.33,-24.9,-23.26,-21.5,-19.53,-17.48,-15.29,-12.96,-10.52,-8.02,-5.43,-2.75,0,3.19,5.69,7.65,9.21,10.41,11.32,12.01,12.48,12.78,12.95,13,12.98,12.91,12.8,12.65,12.47,12.24,11.98,11.69,11.37,11.03,10.64,10.24,9.82,9.37,8.91,8.43,7.93,7.43,6.91,6.38,5.84,5.3,4.75,4.2,3.65,3.1,2.54,2,1.39,0.83,0.31,-0.17,-0.6,-1.01,-1.38,-1.71,-2.01,-2.29,-2.54,-2.77,-2.97,-3.15,-3.31,-3.45,-3.56,-3.67,-3.75,-3.83,-3.88,-3.93,-3.96,-3.98,-3.996,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4
PARAM_EYE_L_OPEN=0,0,0,0,0,0,0,0.001,0,0,0.002,0,0.003,0,0.005,0.005,0.01,0.007,0.008,0.008,0.009,0.01,0.011,0.013,0.014,0.015,0.016,0.017,0.019,0.02,0.022,0.023,0.025,0.026,0.028,0.03,0.032,0.033,0.035,0.037,0.039,0.041,0.043,0.046,0.048,0.05,0.052,0.055,0.057,0.06,0.062,0.065,0.068,0.071,0.073,0.076,0.079,0.082,0.085,0.089,0.092,0.095,0.098,0.102,0.105,0.109,0.112,0.116,0.12,0.123,0.127,0.131,0.135,0.139,0.143,0.147,0.151,0.156,0.16,0.165,0.169,0.174,0.24,0.39,0.59,0.79,0.94,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
PARAM_EYE_L_SMILE=0
PARAM_EYE_R_OPEN=0,0,0,0,0,0,0,0.001,0,0.002,0,0.003,0,0.004,0.005,0.01,0.007,0.008,0.008,0.009,0.011,0.012,0.013,0.014,0.015,0.017,0.018,0.019,0.021,0.022,0.024,0.026,0.028,0.029,0.031,0.033,0.035,0.037,0.039,0.042,0.044,0.046,0.048,0.051,0.053,0.056,0.059,0.061,0.064,0.067,0.07,0.073,0.076,0.079,0.082,0.085,0.088,0.092,0.095,0.099,0.102,0.106,0.11,0.113,0.117,0.121,0.125,0.129,0.133,0.137,0.142,0.146,0.151,0.155,0.16,0.164,0.169,0.174,0.179,0.184,0.189,0.194,0.26,0.41,0.6,0.79,0.94,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
PARAM_EYE_R_SMILE=0
PARAM_EYE_FORM=0
PARAM_EYE_BALL_X=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.002,-0.005,-0.009,-0.014,-0.02,-0.027,-0.035,-0.043,-0.052,-0.062,-0.072,-0.083,-0.094,-0.106,-0.117,-0.129,-0.142,-0.154,-0.166,-0.178,-0.191,-0.203,-0.214,-0.226,-0.237,-0.248,-0.258,-0.268,-0.277,-0.285,-0.293,-0.3,-0.306,-0.311,-0.315,-0.318,-0.319,-0.32,-0.309,-0.28,-0.23,-0.18,-0.13,-0.08,-0.03,0,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.009,0.007,0.006,0.004,0.003,0.002,0.001,0,0,0,0.002,0.004,0.006,0.007,0.009,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01
PARAM_EYE_BALL_Y=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.001,0.002,0.003,0.004,0.005,0.007,0.009,0.01,0.012,0.014,0.017,0.019,0.022,0.025,0.028,0.031,0.034,0.038,0.041,0.045,0.049,0.053,0.058,0.062,0.067,0.072,0.077,0.082,0.087,0.093,0.099,0.105,0.111,0.117,0.123,0.13,0.136,0.142,0.147,0.151,0.154,0.157,0.159,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.141,0.09,0.02,-0.06,-0.14,-0.22,-0.29,-0.34,-0.37,-0.38,-0.34,-0.27,-0.17,-0.07,0.02,0.09,0.14,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16,0.16
PARAM_EYE_BALL_FORM=0
PARAM_BROW_L_Y=0
PARAM_BROW_R_Y=0
PARAM_BROW_L_X=0
PARAM_BROW_R_X=0
PARAM_BROW_L_ANGLE=0
PARAM_BROW_R_ANGLE=0
PARAM_BROW_L_FORM=0
PARAM_BROW_R_FORM=0
PARAM_MOUTH_FORM=0
PARAM_MOUTH_OPEN_Y=0,0.02,0.05,0.91,1,1,1,1,1,0.93,0.6,1,1,1,1,1,1,1,1,1,1,0.98,0.41,0.56,0.71,0.52,0.33,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.15,0.3,1,1,1,0.9,0.87,0.84,1,1,1,1,0.7,0.4,0.6,0.8,1,1,1,1,1,1,0.76,0.15,0.11,0.06,0.36,0.65,0.45,0.26,0.39,0.52,0.48,0.45,0.83,1,1,1,1,1,1,0.95,1,1,0.7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_CHEEK=0
PARAM_SWEAT=0
PARAM_ARM_L=0
PARAM_BODY_ANGLE_X=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.004,0.015,0.034,0.06,0.09,0.13,0.18,0.24,0.3,0.37,0.44,0.52,0.61,0.71,0.81,0.92,1.03,1.15,1.28,1.41,1.55,1.7,1.85,2,2.17,2.33,2.51,2.69,2.87,3.06,3.26,3.46,3.66,3.87,4.09,4.31,4.54,4.77,5,5.24,5.47,5.69,5.9,6.09,6.27,6.41,6.53,6.61,6.67,6.73,6.78,6.82,6.86,6.89,6.91,6.93,6.951,6.965,6.976,6.985,6.991,6.995,6.998,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7
PARAM_BODY_ANGLE_Z=-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-1.981,-1.93,-1.85,-1.76,-1.64,-1.51,-1.37,-1.23,-1.07,-0.92,-0.77,-0.62,-0.47,-0.34,-0.21,-0.1,0,0.09,0.19,0.28,0.37,0.46,0.54,0.63,0.71,0.79,0.87,0.95,1.03,1.11,1.18,1.26,1.33,1.41,1.48,1.55,1.62,1.69,1.76,1.83,1.9,1.98,2.04,2.12,2.19,2.26,2.33,2.4,2.47,2.55,2.62,2.69,2.77,2.84,2.92,3,3.09,3.19,3.29,3.4,3.5,3.6,3.69,3.76,3.8,3.83,3.86,3.89,3.91,3.926,3.942,3.955,3.966,3.975,3.982,3.988,3.992,3.995,3.998,3.999,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4
PARAM_BODY_ANGLE_Y=0,0.1,0.39,0.83,1.4,2.08,2.82,3.6,4.4,5.18,5.92,6.6,7.17,7.61,7.9,8,7.96,7.84,7.66,7.41,7.11,6.75,6.35,5.93,5.47,4.99,4.5,4,3.5,3.01,2.53,2.07,1.65,1.25,0.89,0.59,0.34,0.16,0.04,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.03,0.12,0.25,0.4,0.56,0.7,0.83,0.92,0.98,1,0.93,0.79,0.62,0.43,0.27,0.13,0.03,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_BREATH=0
PARAM_HAIR_FRONT=0
PARAM_HAIR_BACK=0
PARAM_ACCESSORIES=0

View file

@ -0,0 +1,33 @@
# Live2D Animator Motion Data
$fps=30
PARAM_ANGLE_X=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.04,-0.16,-0.36,-0.65,-1.03,-1.48,-2.03,-2.65,-3.37,-4.17,-5.05,-6,-7.04,-8.15,-9.34,-10.61,-11.93,-13.34,-14.8,-16.62,-18.27,-19.83,-21.25,-22.55,-23.74,-24.83,-25.8,-26.69,-27.45,-28.12,-28.69,-29.16,-29.52,-29.79,-29.95,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30
PARAM_ANGLE_Y=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.08,0.33,0.73,1.27,1.94,2.74,3.65,4.64,5.73,6.91,8.15,9.42,10.76,12.12,13.52,14.94,16.35,17.78,19.2,20.88,22.32,23.61,24.72,25.68,26.52,27.25,27.85,28.38,28.81,29.16,29.44,29.66,29.82,29.92,29.98,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30
PARAM_ANGLE_Z=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.08,-0.31,-0.65,-1.12,-1.66,-2.28,-2.95,-3.65,-4.36,-5.07,-5.76,-6.41,-7.01,-7.55,-8.01,-8.39,-8.67,-8.84,-8.9,-8.41,-7.1,-5.1,-2.61,0.27,3.41,6.75,10.09,13.5,16.72,19.78,22.59,25.07,27.11,28.66,29.65,30,29.86,29.47,28.86,28.08,27.18,26.18,25.16,24.11,23.1,22.12,21.25,20.48,19.85,19.39,19.1,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19
PARAM_EYE_L_OPEN=1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.76,0.36,0.09,0,0,0,0.18,0.46,0.73,0.92,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
PARAM_EYE_L_SMILE=0
PARAM_EYE_R_OPEN=1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.76,0.36,0.09,0,0,0,0.18,0.46,0.73,0.92,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
PARAM_EYE_R_SMILE=0
PARAM_EYE_FORM=0
PARAM_EYE_BALL_X=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.003,0.011,0.025,0.043,0.07,0.09,0.12,0.16,0.19,0.23,0.27,0.31,0.35,0.39,0.43,0.47,0.51,0.56,0.59,0.64,0.68,0.71,0.74,0.77,0.788,0.806,0.82,0.833,0.842,0.85,0.856,0.86,0.863,0.865,0.866,0.866,0.87,0.87,0.87,0.87,0.87,0.87,0.87,0.87,0.87,0.87,0.87,0.87,0.87,0.87,0.87,0.87,0.87,0.87,0.87,0.87,0.87,0.87,0.87,0.87,0.87,0.87,0.87,0.87,0.87,0.87
PARAM_EYE_BALL_Y=0
PARAM_EYE_BALL_FORM=0
PARAM_BROW_L_Y=0
PARAM_BROW_R_Y=0
PARAM_BROW_L_X=0
PARAM_BROW_R_X=0
PARAM_BROW_L_ANGLE=0
PARAM_BROW_R_ANGLE=0
PARAM_BROW_L_FORM=0
PARAM_BROW_R_FORM=0
PARAM_MOUTH_FORM=0
PARAM_MOUTH_OPEN_Y=0,0.39,0.78,0.71,0.64,0.32,0,0.4,0.79,0.775,0.758,0.741,0.724,0.707,0.69,0.38,0.08,0.075,0.072,0.069,0.066,0.063,0.06,0.057,0.054,0.051,0.048,0.045,0.042,0.039,0.036,0.033,0.03,0.027,0.024,0.021,0.018,0.015,0.012,0.009,0.006,0.003,0,0.09,0.19,0.5,0.82,0.42,0.03,0.98,1,1,0.37,1,1,1,1,1,1,1,1,1,0.84,0.65,0.47,0.43,0.38,0.34,0.29,0.6,0.9,0.67,0.43,0.48,0.53,0.58,0.63,0.78,0.94,1,1,0.54,0.02,0.19,0.36,0.18,0,0,0,0,0,0,0,0
PARAM_CHEEK=0
PARAM_SWEAT=0
PARAM_ARM_L=0
PARAM_BODY_ANGLE_X=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.014,-0.06,-0.13,-0.23,-0.37,-0.53,-0.72,-0.94,-1.19,-1.48,-1.79,-2.12,-2.49,-2.88,-3.29,-3.74,-4.2,-4.69,-5.2,-5.76,-6.28,-6.77,-7.22,-7.61,-7.97,-8.27,-8.53,-8.73,-8.88,-8.97,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9
PARAM_BODY_ANGLE_Z=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.012,-0.05,-0.1,-0.19,-0.29,-0.42,-0.58,-0.75,-0.95,-1.18,-1.42,-1.69,-1.98,-2.28,-2.61,-2.96,-3.32,-3.7,-4.1,-4.54,-4.94,-5.31,-5.65,-5.95,-6.23,-6.46,-6.65,-6.8,-6.91,-6.98,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7,-7
PARAM_BODY_ANGLE_Y=0
PARAM_BREATH=0
PARAM_HAIR_FRONT=0
PARAM_HAIR_BACK=0
PARAM_ACCESSORIES=0

View file

@ -0,0 +1,46 @@
# Live2D Animator Motion Data
$fps=30
$fadein=300
$fadeout=500
PARAM_ANGLE_X=0
PARAM_ANGLE_Y=0,0,0,0,0,0,0,0,-1.03,-3.74,-7.65,-12.12,-16.75,-21.08,-24.77,-27.6,-29.37,-30,-29.3,-27.35,-24.27,-20.36,-15.69,-10.61,-5.24,0.24,5.61,10.69,15.36,19.27,22.35,24.3,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25
PARAM_ANGLE_Z=0
PARAM_EYE_L_OPEN=1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.27,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_EYE_L_SMILE=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.73,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
PARAM_EYE_R_OPEN=1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.27,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_EYE_R_SMILE=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.73,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
PARAM_EYE_FORM=0
PARAM_EYE_BALL_X=0,0,0,0,0,0,0,0,-0.002,-0.006,-0.012,-0.017,-0.022,-0.026,-0.029,-0.03,-0.03,-0.03,-0.03,-0.03,-0.03,-0.03,-0.03,-0.028,-0.022,-0.015,-0.008,-0.002,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_EYE_BALL_Y=0,0,0,0,0,0,0,0,0.07,0.21,0.38,0.57,0.73,0.87,0.97,1,1,1,1,1,1,1,1,0.92,0.74,0.5,0.26,0.08,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_EYE_BALL_FORM=0
PARAM_BROW_L_Y=0
PARAM_BROW_R_Y=0
PARAM_BROW_L_X=0
PARAM_BROW_R_X=0
PARAM_BROW_L_ANGLE=0
PARAM_BROW_R_ANGLE=0
PARAM_BROW_L_FORM=0
PARAM_BROW_R_FORM=0
PARAM_MOUTH_FORM=1
PARAM_MOUTH_OPEN_Y=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.26,0.74,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
PARAM_CHEEK=0
PARAM_ARM_L_01=0,0,0,0,0,0,0,0,-0.44,-1.51,-2.82,-4.1,-5.13,-5.78,-6,-6.014,-6.019,-5.96,-5.8,-5.53,-5.17,-4.75,-4.3,-3.83,-3.36,-2.94,-2.56,-2.26,-2.07,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2
PARAM_ARM_L_02=0,0,0,0,0,0,0,0,0.29,1.01,1.94,2.91,3.79,4.5,5,5.28,5.37,5.64,6.13,6.62,7.13,7.63,8.09,8.54,8.95,9.29,9.59,9.81,9.95,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10
PARAM_HAND_L=0
PARAM_ARM_R_01=0,0,0,0,0,0,0,0,-0.29,-1.01,-1.88,-2.73,-3.42,-3.85,-4,-3.997,-3.85,-3.35,-2,-0.57,0.91,2.43,4,7.73,11.93,15.1,17,18.39,18.91,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19
PARAM_ARM_R_02=0,0,0,0,0,0,0,0,0.18,0.66,1.36,2.2,3.12,4.07,5,6,6.77,7.59,8.68,9.31,9.71,9.93,10,9.8,9.03,7.71,6,2.15,-1.99,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4
PARAM_HAND_R=0
PARAM_LEG_L=0,0,0,0,0,0,0,0,-0.1,-0.37,-0.76,-1.21,-1.68,-2.11,-2.48,-2.76,-2.94,-3,-2.94,-2.76,-2.48,-2.12,-1.7,-1.24,-0.75,-0.25,0.24,0.7,1.12,1.48,1.76,1.94,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2
PARAM_LEG_R=0,0,0,0,0,0,0,0,-0.1,-0.37,-0.76,-1.21,-1.68,-2.11,-2.48,-2.76,-2.94,-3,-2.96,-2.86,-2.69,-2.47,-2.22,-1.94,-1.65,-1.35,-1.06,-0.78,-0.53,-0.31,-0.14,-0.04,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_BODY_ANGLE_X=0
PARAM_BODY_ANGLE_Y=0,0,0,0,0,0,0,0,-0.34,-1.25,-2.55,-4.04,-5.58,-7.03,-8.26,-9.2,-9.79,-10,-9.76,-9.08,-8.02,-6.67,-5.06,-3.3,-1.44,0.44,2.3,4.06,5.67,7.02,8.08,8.76,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9
PARAM_BODY_ANGLE_Z=0,0,0,0,0,0,0,0,0.21,0.76,1.56,2.51,3.49,4.44,5.24,5.79,6,5.86,5.47,4.86,4.08,3.18,2.18,1.16,0.11,-0.9,-1.88,-2.75,-3.52,-4.15,-4.61,-4.9,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5,-5
PARAM_BREATH=0
PARAM_HAIR_FLUFFY=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.07,0.21,0.38,0.57,0.73,0.87,0.97,1,0.88,0.62,0.29,-0.05,-0.36,-0.62,-0.79,-0.85,-0.77,-0.58,-0.36,-0.12,0.09,0.27,0.39,0.43,0.415,0.38,0.32,0.26,0.19,0.13,0.07,0.03,0.01,0,0,0,0,0,0
PARAM_HAIR_FRONT=0
PARAM_HAIR_BACK=0
PARAM_BASE_X=0
PARAM_BASE_Y=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.67,2.71,5.75,9.4,15.27,19.98,22,19.5,15.18,10.59,6.36,2.92,0.62,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0

View file

@ -0,0 +1,46 @@
# Live2D Animator Motion Data
$fps=30
$fadein=200
$fadeout=500
PARAM_ANGLE_X=0
PARAM_ANGLE_Y=0,0,0,0,0,0,-0.69,-2.49,-5.1,-8.08,-11.17,-14.05,-16.52,-18.4,-19.58,-20,-19.62,-18.63,-17.18,-15.39,-13.4,-11.31,-9.21,-7.16,-5.25,-3.53,-2.1,-0.98,-0.25,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_ANGLE_Z=0
PARAM_EYE_L_OPEN=1,1,1,1,1,1,1,1,1,0.74,0.26,0,0,0,0.24,0.64,0.91,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
PARAM_EYE_L_SMILE=0
PARAM_EYE_R_OPEN=1,1,1,1,1,1,1,1,1,0.74,0.26,0,0,0,0.24,0.64,0.91,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
PARAM_EYE_R_SMILE=0
PARAM_EYE_FORM=0
PARAM_EYE_BALL_X=0
PARAM_EYE_BALL_Y=0
PARAM_EYE_BALL_FORM=0
PARAM_BROW_L_Y=0
PARAM_BROW_R_Y=0
PARAM_BROW_L_X=0,0,0,0,0,0,-0.003,-0.012,-0.026,-0.045,-0.07,-0.09,-0.12,-0.15,-0.18,-0.21,-0.23,-0.26,-0.28,-0.31,-0.324,-0.339,-0.351,-0.358,-0.36,-0.36,-0.36,-0.36,-0.36,-0.36,-0.36,-0.36,-0.36,-0.36,-0.36,-0.36,-0.36,-0.36,-0.36,-0.36,-0.36,-0.36,-0.36,-0.36,-0.36,-0.36,-0.36,-0.36,-0.36,-0.36,-0.36,-0.36,-0.36,-0.36
PARAM_BROW_R_X=0,0,0,0,0,0,-0.003,-0.012,-0.026,-0.044,-0.07,-0.09,-0.12,-0.14,-0.17,-0.2,-0.23,-0.25,-0.28,-0.3,-0.315,-0.33,-0.341,-0.348,-0.35,-0.35,-0.35,-0.35,-0.35,-0.35,-0.35,-0.35,-0.35,-0.35,-0.35,-0.35,-0.35,-0.35,-0.35,-0.35,-0.35,-0.35,-0.35,-0.35,-0.35,-0.35,-0.35,-0.35,-0.35,-0.35,-0.35,-0.35,-0.35,-0.35
PARAM_BROW_L_ANGLE=0,0,0,0,0,0,-0.004,-0.015,-0.032,-0.05,-0.08,-0.11,-0.14,-0.18,-0.21,-0.25,-0.28,-0.31,-0.34,-0.36,-0.39,-0.405,-0.419,-0.427,-0.43,-0.43,-0.43,-0.43,-0.43,-0.43,-0.43,-0.43,-0.43,-0.43,-0.43,-0.43,-0.43,-0.43,-0.43,-0.43,-0.43,-0.43,-0.43,-0.43,-0.43,-0.43,-0.43,-0.43,-0.43,-0.43,-0.43,-0.43,-0.43,-0.43
PARAM_BROW_R_ANGLE=0,0,0,0,0,0,-0.004,-0.015,-0.032,-0.05,-0.08,-0.11,-0.14,-0.18,-0.21,-0.25,-0.28,-0.31,-0.34,-0.36,-0.39,-0.405,-0.419,-0.427,-0.43,-0.43,-0.43,-0.43,-0.43,-0.43,-0.43,-0.43,-0.43,-0.43,-0.43,-0.43,-0.43,-0.43,-0.43,-0.43,-0.43,-0.43,-0.43,-0.43,-0.43,-0.43,-0.43,-0.43,-0.43,-0.43,-0.43,-0.43,-0.43,-0.43
PARAM_BROW_L_FORM=0,0,0,0,0,0,-0.004,-0.016,-0.035,-0.06,-0.09,-0.12,-0.16,-0.19,-0.23,-0.27,-0.3,-0.34,-0.37,-0.4,-0.42,-0.443,-0.458,-0.467,-0.47,-0.47,-0.47,-0.47,-0.47,-0.47,-0.47,-0.47,-0.47,-0.47,-0.47,-0.47,-0.47,-0.47,-0.47,-0.47,-0.47,-0.47,-0.47,-0.47,-0.47,-0.47,-0.47,-0.47,-0.47,-0.47,-0.47,-0.47,-0.47,-0.47
PARAM_BROW_R_FORM=0,0,0,0,0,0,-0.004,-0.017,-0.035,-0.06,-0.09,-0.12,-0.16,-0.2,-0.24,-0.27,-0.31,-0.35,-0.38,-0.41,-0.43,-0.452,-0.467,-0.477,-0.48,-0.48,-0.48,-0.48,-0.48,-0.48,-0.48,-0.48,-0.48,-0.48,-0.48,-0.48,-0.48,-0.48,-0.48,-0.48,-0.48,-0.48,-0.48,-0.48,-0.48,-0.48,-0.48,-0.48,-0.48,-0.48,-0.48,-0.48,-0.48,-0.48
PARAM_MOUTH_FORM=1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.87,0.59,0.23,-0.13,-0.47,-0.75,-0.93,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1
PARAM_MOUTH_OPEN_Y=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.07,0.21,0.38,0.57,0.73,0.87,0.97,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
PARAM_CHEEK=0
PARAM_ARM_L_01=0,0,0,0,0,0,0.05,0.21,0.45,0.76,1.13,1.55,2.01,2.5,3,3.5,3.99,4.45,4.87,5.24,5.55,5.79,5.95,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6
PARAM_ARM_L_02=0,0,0,0,0,0,-0.18,-0.69,-1.49,-2.53,-3.76,-5.18,-6.72,-8.34,-10,-11.66,-13.28,-14.82,-16.24,-17.47,-18.51,-19.31,-19.82,-20,-20,-20,-20,-20,-20,-20,-20,-20,-20,-20,-20,-20,-20,-20,-20,-20,-20,-20,-20,-20,-20,-20,-20,-20,-20,-20,-20,-20,-20,-20
PARAM_HAND_L=0
PARAM_ARM_R_01=0,0,0,0,0,0,0.005,0.03,0.11,0.26,0.48,0.79,1.2,1.7,2.3,3,4.71,6.95,9.38,11.75,13.83,15.5,16.6,17,16.4,14.94,13,11.06,9.6,9,9.24,9.89,10.82,11.93,13.07,14.18,15.11,15.76,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16
PARAM_ARM_R_02=0,0,0,0,0,0,0.34,1.25,2.55,4.04,5.58,7.03,8.26,9.2,9.79,10,9.41,8.14,6.55,4.9,3.39,2.14,1.31,1,1.68,3.32,5.5,7.68,9.32,10,9.72,8.99,7.92,6.65,5.35,4.08,3.01,2.28,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2
PARAM_HAND_R=0
PARAM_LEG_L=0,0,0,0,0,0,0.06,0.24,0.52,0.89,1.32,1.81,2.35,2.92,3.5,4.08,4.65,5.19,5.68,6.11,6.48,6.76,6.94,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7
PARAM_LEG_R=0,0,0,0,0,0,-0.04,-0.14,-0.3,-0.51,-0.75,-1.04,-1.34,-1.67,-2,-2.33,-2.66,-2.96,-3.25,-3.49,-3.7,-3.86,-3.96,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4
PARAM_BODY_ANGLE_X=0,0,0,0,0,0,0.09,0.34,0.74,1.26,1.87,2.56,3.32,4.1,4.9,5.7,6.47,7.2,7.88,8.48,9,9.43,9.74,9.93,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10
PARAM_BODY_ANGLE_Y=0,0,0,0,0,0,-0.04,-0.14,-0.29,-0.5,-0.75,-1.02,-1.33,-1.64,-1.96,-2.28,-2.59,-2.88,-3.15,-3.39,-3.6,-3.77,-3.9,-3.97,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4,-4
PARAM_BODY_ANGLE_Z=0,0,0,0,0,0,0.09,0.34,0.74,1.26,1.87,2.56,3.32,4.1,4.9,5.7,6.47,7.2,7.88,8.48,9,9.43,9.74,9.93,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10
PARAM_BREATH=0
PARAM_HAIR_FLUFFY=0
PARAM_HAIR_FRONT=0
PARAM_HAIR_BACK=0
PARAM_BASE_X=0
PARAM_BASE_Y=0

View file

@ -0,0 +1,44 @@
# Live2D Animator Motion Data
$fps=30
PARAM_SHOULDER_X=6.4,6.36,6.24,6.05,5.79,5.48,5.13,4.74,4.32,3.88,3.42,2.98,2.52,2.08,1.66,1.27,0.92,0.61,0.35,0.16,0.04,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.14,0.49,1,1.64,2.34,3.08,3.83,4.56,5.23,5.85,6.35,6.75,7.01,7.1,7.099,7.097,7.092,7.084,7.072,7.057,7.04,7.01,6.98,6.94,6.9,6.84,6.78,6.71,6.63,6.54,6.44,6.33,6.2,6.07,5.92,5.76,5.58,5.39,5.1,4.69,4.19,3.63,3.04,2.44,1.87,1.35,0.89,0.51,0.23,0.06,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_MOUTH_OPEN_Y=0,0,0,0,0,0,0,0,0.22,0.45,0.4,0.35,0.3,0.25,0.2,0.15,0.38,0.61,0.58,0.54,0.51,0.47,0.481,0.492,0.502,0.512,0.523,0.533,0.46,0.38,0.19,0,0.46,0.93,0.74,0.55,0.58,0.61,0.64,0.67,0.7,0.73,0.62,0.51,0.4,0.29,0.56,0.84,0.76,0.67,0.65,0.63,0.61,0.59,0.57,0.55,0.53,0.51,0.48,0.46,0.24,0.01,0.35,0.69,0.53,0.37,0.55,0.73,0.733,0.737,0.64,0.53,0.43,0.33,0.62,0.91,0.84,0.76,0.767,0.773,0.778,0.784,0.68,0.58,0.47,0.37,0.378,0.388,0.398,0.408,0.34,0.27,0.2,0.14,0.07,0,0.01,0.02,0.029,0.039,0.34,0.64,0.58,0.52,0.27,0.02,0.47,0.91,0.82,0.73,0.65,0.56,0.6,0.64,0.68,0.72,0.69,0.66,0.63,0.6,0.45,0.3,0.6,0.91,0.77,0.63,0.69,0.75,0.67,0.6,0.36,0.12,0.44,0.77,0.75,0.732,0.714,0.695,0.677,0.659,0.58,0.51,0.514,0.518,0.26,0,0.43,0.86,0.82,0.77,0.38,0,0.29,0.58,0.51,0.43,0.53,0.64,0.51,0.38,0.19,0,0.38,0.77,0.759,0.75,0.741,0.732,0.723,0.714,0.66,0.61,0.46,0.31,0.15,0,0,0,0,0,0,0
PARAM_FACE_COVER=1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.82,0.54,0.27,0.08,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_FEAR=0
PARAM_EYE_L_OPEN=1,1,1,0.999,0.998,0.996,0.995,0.993,0.99,0.988,0.985,0.983,0.979,0.976,0.973,0.969,0.965,0.961,0.956,0.952,0.947,0.943,0.938,0.933,0.927,0.922,0.917,0.911,0.906,0.9,0.894,0.889,0.883,0.877,0.871,0.865,0.859,0.853,0.847,0.841,0.835,0.829,0.823,0.817,0.811,0.806,0.8,0.794,0.789,0.783,0.778,0.773,0.767,0.762,0.757,0.753,0.748,0.744,0.739,0.735,0.731,0.727,0.724,0.721,0.717,0.715,0.712,0.71,0.707,0.705,0.704,0.702,0.701,0.701,0.7,0.7,0.71,0.74,0.78,0.82,0.87,0.91,0.95,0.98,0.994,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.999,0.997,0.993,0.987,0.981,0.974,0.967,0.959,0.951,0.943,0.935,0.928,0.921,0.915,0.91,0.906,0.903,0.901,0.9,0.9,0.9,0.9,0.9,0.9,0.9,0.9,0.9,0.9,0.9,0.9,0.9,0.9,0.9,0.9,0.9,0.901,0.905,0.91,0.917,0.926,0.935,0.944,0.954,0.963,0.972,0.98,0.987,0.992,0.996,0.999,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
PARAM_EYE_L_SMILE=0
PARAM_BROW_L_FORM=-1
PARAM_TEAR=0
PARAM_BROW_L_X=0
PARAM_BROW_DEFORMED=1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.82,0.54,0.27,0.08,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_BROW_L_Y=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.06,-0.16,-0.25,-0.31,-0.34,-0.34,-0.34,-0.34,-0.34,-0.34,-0.34,-0.34,-0.34,-0.342,-0.344,-0.347,-0.351,-0.355,-0.36,-0.367,-0.374,-0.382,-0.392,-0.402,-0.414,-0.427,-0.441,-0.456,-0.473,-0.49,-0.51,-0.531,-0.551,-0.57,-0.589,-0.608,-0.626,-0.643,-0.66,-0.677,-0.693,-0.709,-0.723,-0.737,-0.751,-0.764,-0.776,-0.787,-0.797,-0.807,-0.816,-0.824,-0.832,-0.838,-0.844,-0.849,-0.853,-0.856,-0.858,-0.86,-0.86,-0.859,-0.856,-0.851,-0.845,-0.837,-0.827,-0.816,-0.803,-0.79,-0.775,-0.76,-0.743,-0.726,-0.708,-0.689,-0.67,-0.651,-0.632,-0.612,-0.593,-0.573,-0.554,-0.535,-0.516,-0.498,-0.48,-0.463,-0.446,-0.431,-0.416,-0.403,-0.391,-0.379,-0.369,-0.361,-0.353,-0.348,-0.343,-0.341,-0.34
PARAM_EYE_R_SMILE=0
PARAM_EYE_R_OPEN=1,1,1,0.999,0.998,0.996,0.995,0.993,0.99,0.988,0.985,0.983,0.979,0.976,0.973,0.969,0.965,0.961,0.956,0.952,0.947,0.943,0.938,0.933,0.927,0.922,0.917,0.911,0.906,0.9,0.894,0.889,0.883,0.877,0.871,0.865,0.859,0.853,0.847,0.841,0.835,0.829,0.823,0.817,0.811,0.806,0.8,0.794,0.789,0.783,0.778,0.773,0.767,0.762,0.757,0.753,0.748,0.744,0.739,0.735,0.731,0.727,0.724,0.721,0.717,0.715,0.712,0.71,0.707,0.705,0.704,0.702,0.701,0.701,0.7,0.7,0.71,0.74,0.78,0.82,0.87,0.91,0.95,0.98,0.994,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.999,0.997,0.994,0.989,0.984,0.978,0.971,0.964,0.957,0.949,0.942,0.935,0.928,0.922,0.916,0.911,0.906,0.903,0.9,0.898,0.896,0.894,0.893,0.892,0.891,0.89,0.89,0.89,0.89,0.89,0.89,0.89,0.89,0.89,0.89,0.891,0.895,0.901,0.909,0.918,0.928,0.938,0.949,0.959,0.969,0.977,0.985,0.991,0.996,0.999,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
PARAM_HAIR_BACK=0
PARAM_EYE_BALL_X=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.009,0.03,0.07,0.13,0.19,0.26,0.34,0.42,0.5,0.58,0.66,0.74,0.81,0.87,0.93,0.97,0.99,1,1,1,0.999,0.998,0.996,0.995,0.993,0.99,0.987,0.984,0.981,0.977,0.972,0.968,0.962,0.957,0.951,0.944,0.937,0.929,0.921,0.913,0.903,0.894,0.883,0.873,0.861,0.849,0.837,0.823,0.81,0.795,0.78,0.762,0.745,0.728,0.712,0.696,0.681,0.667,0.654,0.641,0.63,0.62,0.611,0.604,0.598,0.594,0.591,0.59,0.59,0.59,0.59,0.59,0.59,0.59,0.59,0.59,0.59,0.59,0.59,0.59,0.59,0.59,0.59,0.59,0.59,0.59,0.59,0.59,0.59,0.59
PARAM_HAIR_FRONT=0
PARAM_EYE_BALL_Y=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.002,0.007,0.016,0.027,0.039,0.054,0.071,0.088,0.105,0.122,0.139,0.156,0.171,0.183,0.194,0.203,0.208,0.21,0.209,0.205,0.199,0.191,0.182,0.17,0.157,0.142,0.126,0.109,0.091,0.072,0.052,0.03,0.011,-0.01,-0.03,-0.05,-0.072,-0.09,-0.112,-0.131,-0.149,-0.166,-0.182,-0.197,-0.21,-0.222,-0.231,-0.239,-0.245,-0.249,-0.25,-0.242,-0.22,-0.19,-0.15,-0.1,-0.05,0,0.06,0.11,0.16,0.21,0.26,0.3,0.33,0.36,0.374,0.38,0.38,0.38,0.38,0.38,0.38,0.38,0.38,0.38,0.38,0.38,0.38,0.38,0.38,0.38,0.38,0.38,0.38,0.38,0.38,0.38,0.38,0.38
PARAM_BROW_L_ANGLE=-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-0.91,-0.78,-0.65,-0.56,-0.52,-0.52,-0.52,-0.52,-0.52,-0.52,-0.52,-0.52,-0.52,-0.52,-0.52,-0.52,-0.521,-0.52,-0.52,-0.52,-0.52,-0.521,-0.52,-0.519,-0.518,-0.517,-0.516,-0.514,-0.512,-0.51,-0.506,-0.499,-0.491,-0.48,-0.467,-0.452,-0.436,-0.418,-0.399,-0.38,-0.36,-0.34,-0.32,-0.29,-0.27,-0.25,-0.23,-0.21,-0.188,-0.169,-0.15,-0.133,-0.117,-0.102,-0.089,-0.078,-0.068,-0.06,-0.055,-0.051,-0.05,-0.053,-0.061,-0.075,-0.092,-0.11,-0.14,-0.17,-0.2,-0.23,-0.26,-0.29,-0.32,-0.35,-0.38,-0.41,-0.44,-0.46,-0.48,-0.495,-0.505,-0.51,-0.512,-0.514,-0.515,-0.517,-0.518,-0.519,-0.519,-0.52,-0.52,-0.52,-0.52,-0.52,-0.52,-0.52,-0.52,-0.52,-0.52,-0.52,-0.52
PARAM_EYE_DEFORMED_04=0
PARAM_EYE_DEFORMED_03=0
PARAM_EYE_DEFORMED_02=0
PARAM_EYE_DEFORMED_01=1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.82,0.54,0.27,0.08,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_ANGLE_Z=0,-0.08,-0.31,-0.68,-1.18,-1.78,-2.48,-3.27,-4.11,-5.03,-5.98,-6.95,-7.94,-8.94,-9.9,-10.86,-11.78,-12.62,-13.41,-14.11,-14.71,-15.21,-15.58,-15.81,-15.89,-15.89,-15.89,-15.89,-15.89,-15.89,-15.89,-15.89,-15.89,-15.89,-15.89,-15.89,-15.89,-15.89,-15.89,-15.54,-14.6,-13.16,-11.38,-9.31,-7.06,-4.67,-2.27,0.17,2.48,4.67,6.69,8.47,9.93,11.04,11.75,12,12,12,12,12,12,12,11.97,11.89,11.75,11.56,11.32,11.05,10.73,10.38,9.99,9.58,9.13,8.66,8.17,7.67,7.15,6.63,6.1,5.55,5.02,4.47,3.95,3.43,2.92,2.43,1.96,1.51,1.09,0.69,0.33,0,-0.32,-0.63,-0.93,-1.22,-1.5,-1.78,-2.04,-2.31,-2.56,-2.8,-3.04,-3.27,-3.49,-3.71,-3.93,-4.14,-4.34,-4.54,-4.74,-4.93,-5.12,-5.31,-5.49,-5.67,-5.84,-6.02,-6.19,-6.36,-6.53,-6.69,-6.86,-7.02,-7.18,-7.35,-7.51,-7.67,-7.84,-8,-8.16,-8.32,-8.46,-8.6,-8.73,-8.85,-8.97,-9.07,-9.17,-9.26,-9.35,-9.43,-9.5,-9.57,-9.63,-9.68,-9.73,-9.78,-9.82,-9.85,-9.88,-9.91,-9.93,-9.953,-9.968,-9.98,-9.989,-9.995,-9.999,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10
PARAM_BODY_ANGLE_X=0,-0.07,-0.26,-0.55,-0.95,-1.43,-1.99,-2.59,-3.25,-3.94,-4.65,-5.35,-6.06,-6.75,-7.41,-8.01,-8.57,-9.05,-9.45,-9.74,-9.93,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-9.75,-9.04,-7.93,-6.52,-4.88,-3.06,-1.2,0.7,2.55,4.32,5.9,7.3,8.45,9.29,9.82,10,10,10,10,10,10,10,10,10,10,10,9.987,9.95,9.88,9.79,9.67,9.53,9.36,9.18,8.97,8.73,8.48,8.2,7.91,7.6,7.26,6.92,6.55,6.17,5.77,5.35,4.92,4.48,4.03,3.56,3.08,2.59,2.09,1.58,1.06,0.53,0,-0.6,-1.16,-1.72,-2.24,-2.74,-3.23,-3.69,-4.14,-4.56,-4.96,-5.35,-5.72,-6.07,-6.4,-6.71,-7.01,-7.3,-7.56,-7.82,-8.05,-8.27,-8.48,-8.67,-8.85,-9.01,-9.17,-9.3,-9.43,-9.54,-9.64,-9.72,-9.8,-9.86,-9.91,-9.95,-9.98,-9.995,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10
PARAM_BODY_ANGLE_Y=-10,-9.93,-9.74,-9.45,-9.05,-8.57,-8.01,-7.41,-6.75,-6.06,-5.35,-4.65,-3.94,-3.25,-2.59,-1.99,-1.43,-0.95,-0.55,-0.26,-0.07,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.09,-0.34,-0.74,-1.27,-1.88,-2.59,-3.36,-4.17,-5,-5.83,-6.64,-7.41,-8.12,-8.73,-9.26,-9.66,-9.91,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-9.6,-8.53,-6.92,-4.9,-2.67,-0.29,2.04,4.22,6.14,7.76,8.98,9.74,10,9.8,9.26,8.44,7.41,6.23,4.94,3.56,2.19,0.79,-0.54,-1.8,-2.95,-3.97,-4.81,-5.45,-5.86,-6,-5.96,-5.85,-5.67,-5.43,-5.15,-4.82,-4.45,-4.08,-3.67,-3.26,-2.84,-2.44,-2.04,-1.66,-1.31,-0.99,-0.71,-0.46,-0.27,-0.12,-0.03,0
PARAM_BODY_ANGLE_Z=0
PARAM_BROW_R_Y=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.09,-0.23,-0.36,-0.45,-0.49,-0.49,-0.49,-0.49,-0.49,-0.49,-0.49,-0.49,-0.492,-0.494,-0.497,-0.501,-0.506,-0.511,-0.517,-0.523,-0.531,-0.538,-0.546,-0.555,-0.564,-0.573,-0.583,-0.593,-0.603,-0.614,-0.624,-0.635,-0.646,-0.657,-0.668,-0.679,-0.69,-0.701,-0.712,-0.723,-0.733,-0.744,-0.754,-0.764,-0.773,-0.783,-0.791,-0.8,-0.808,-0.816,-0.823,-0.829,-0.836,-0.841,-0.846,-0.85,-0.853,-0.856,-0.858,-0.86,-0.86,-0.859,-0.857,-0.854,-0.849,-0.843,-0.836,-0.829,-0.82,-0.81,-0.8,-0.789,-0.777,-0.764,-0.752,-0.739,-0.725,-0.712,-0.698,-0.684,-0.67,-0.656,-0.642,-0.629,-0.615,-0.602,-0.59,-0.577,-0.566,-0.555,-0.544,-0.535,-0.526,-0.518,-0.511,-0.505,-0.5,-0.495,-0.492,-0.491,-0.49
PARAM_ANGLE_Y=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.27,-1.03,-2.23,-3.8,-5.64,-7.76,-10.07,-12.51,-15,-17.49,-19.93,-22.24,-24.36,-26.2,-27.77,-28.97,-29.73,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-29.02,-26.4,-22.45,-17.51,-12.03,-6.22,-0.51,4.83,9.55,13.52,16.5,18.36,19,18.52,17.24,15.29,12.85,10.04,6.98,3.71,0.45,-2.88,-6.03,-9.02,-11.76,-14.19,-16.17,-17.69,-18.66,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19
PARAM_BREATH=0
PARAM_BROW_R_X=0
PARAM_ANGLE_X=0,-0.07,-0.28,-0.61,-1.04,-1.58,-2.19,-2.85,-3.57,-4.33,-5.11,-5.89,-6.67,-7.43,-8.15,-8.81,-9.42,-9.96,-10.39,-10.72,-10.93,-11,-11,-11,-11,-11,-11,-11,-11,-11,-11,-11,-11,-11,-11,-10.86,-10.47,-9.86,-9.08,-8.18,-7.18,-6.16,-5.11,-4.1,-3.12,-2.25,-1.48,-0.85,-0.39,-0.1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.27,-1.03,-2.23,-3.8,-5.64,-7.76,-10.07,-12.51,-15,-17.49,-19.93,-22.24,-24.36,-26.2,-27.77,-28.97,-29.73,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-29.84,-29.37,-28.65,-27.72,-26.62,-25.42,-24.15,-22.85,-21.58,-20.38,-19.28,-18.35,-17.63,-17.16,-17,-17.013,-17.05,-17.1,-17.18,-17.26,-17.35,-17.45,-17.55,-17.65,-17.74,-17.82,-17.9,-17.95,-17.99,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18
PARAM_ARM_R=-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-9.87,-9.52,-8.96,-8.25,-7.4,-6.47,-5.5,-4.5,-3.53,-2.6,-1.75,-1.04,-0.48,-0.13,0,-0.07,-0.25,-0.55,-0.95,-1.42,-1.96,-2.58,-3.21,-3.88,-4.57,-5.26,-5.93,-6.6,-7.23,-7.82,-8.35,-8.82,-9.23,-9.56,-9.8,-9.95,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10
PARAM_ARM_L=-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-9.87,-9.52,-8.96,-8.25,-7.4,-6.47,-5.5,-4.5,-3.53,-2.6,-1.75,-1.04,-0.48,-0.13,0,-0.07,-0.25,-0.55,-0.95,-1.42,-1.96,-2.58,-3.21,-3.88,-4.57,-5.26,-5.93,-6.6,-7.23,-7.82,-8.35,-8.82,-9.23,-9.56,-9.8,-9.95,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10
PARAM_MOUTH_FORM=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.18,-0.46,-0.73,-0.92,-1,-0.96,-0.87,-0.76,-0.64,-0.55,-0.51,-0.51,-0.51,-0.51,-0.51,-0.51,-0.51,-0.511,-0.51,-0.51,-0.512,-0.51,-0.51,-0.513,-0.51,-0.514,-0.515,-0.52,-0.516,-0.52,-0.518,-0.518,-0.52,-0.52,-0.521,-0.521,-0.522,-0.52,-0.524,-0.525,-0.526,-0.526,-0.527,-0.528,-0.529,-0.53,-0.531,-0.532,-0.533,-0.534,-0.535,-0.536,-0.537,-0.538,-0.539,-0.54,-0.54,-0.542,-0.543,-0.544,-0.545,-0.546,-0.547,-0.548,-0.549,-0.55,-0.551,-0.552,-0.553,-0.554,-0.554,-0.555,-0.556,-0.557,-0.56,-0.559,-0.559,-0.56,-0.561,-0.56,-0.562,-0.563,-0.56,-0.564,-0.56,-0.566,-0.566,-0.57,-0.567,-0.57,-0.57,-0.568,-0.57,-0.57,-0.569,-0.57,-0.57,-0.57,-0.57,-0.57,-0.57
PARAM_HAIR_SIDE=0
PARAM_EYE_BALL_FORM=0
PARAM_BROW_R_ANGLE=-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-0.91,-0.78,-0.65,-0.56,-0.52,-0.52,-0.52,-0.52,-0.52,-0.52,-0.52,-0.52,-0.52,-0.52,-0.52,-0.52,-0.521,-0.52,-0.52,-0.52,-0.52,-0.521,-0.52,-0.519,-0.518,-0.517,-0.516,-0.514,-0.512,-0.51,-0.506,-0.5,-0.491,-0.481,-0.468,-0.454,-0.438,-0.422,-0.404,-0.385,-0.365,-0.345,-0.324,-0.3,-0.28,-0.26,-0.241,-0.222,-0.202,-0.183,-0.166,-0.149,-0.134,-0.12,-0.107,-0.096,-0.087,-0.08,-0.074,-0.071,-0.07,-0.073,-0.081,-0.093,-0.11,-0.13,-0.15,-0.18,-0.21,-0.24,-0.27,-0.3,-0.33,-0.36,-0.39,-0.42,-0.44,-0.46,-0.481,-0.495,-0.505,-0.51,-0.512,-0.514,-0.515,-0.517,-0.518,-0.519,-0.519,-0.52,-0.52,-0.52,-0.52,-0.52,-0.52,-0.52,-0.52,-0.52,-0.52,-0.52,-0.52
PARAM_SWEAT=0
PARAM_BROW_R_FORM=-1
PARAM_TERE=0
PARAM_EYE_BALL_GLITTER=0
PARAM_STRING=0

View file

@ -0,0 +1,60 @@
# Live2D Animator Motion Data
$fps=30
$fadein=1000
$fadeout=0
$fadein:PARAM_EYE_L_OPEN=200
$fadeout:PARAM_EYE_L_OPEN=200
$fadein:PARAM_EYE_L_SMILE=200
$fadeout:PARAM_EYE_L_SMILE=200
$fadein:PARAM_EYE_R_OPEN=200
$fadeout:PARAM_EYE_R_OPEN=200
$fadein:PARAM_EYE_R_SMILE=200
$fadeout:PARAM_EYE_R_SMILE=200
PARAM_ANGLE_X=0
PARAM_ANGLE_Y=0,0.07,0.28,0.59,1.01,1.5,2.05,2.65,3.28,3.92,4.56,5.18,5.76,6.3,6.78,7.2,7.54,7.79,7.95,8,7.93,7.73,7.4,6.96,6.43,5.81,5.12,4.38,3.57,2.73,1.88,1,0.12,-0.73,-1.57,-2.38,-3.12,-3.81,-4.43,-4.96,-5.4,-5.73,-5.93,-6,-5.81,-5.31,-4.53,-3.57,-2.46,-1.25,0.04,1.32,2.64,3.88,5.06,6.14,7.1,7.88,8.48,8.87,9,8.97,8.89,8.75,8.57,8.35,8.1,7.8,7.48,7.15,6.78,6.4,6,5.6,5.18,4.76,4.34,3.93,3.52,3.12,2.72,2.35,1.99,1.65,1.33,1.05,0.79,0.56,0.36,0.21,0.09,0.02,0,0,0,0,0,0
PARAM_ANGLE_Z=0,-0.05,-0.2,-0.43,-0.74,-1.12,-1.56,-2.06,-2.59,-3.16,-3.77,-4.37,-5,-5.63,-6.23,-6.84,-7.41,-7.94,-8.44,-8.88,-9.26,-9.57,-9.8,-9.95,-10,-9.94,-9.75,-9.44,-9.03,-8.51,-7.92,-7.24,-6.49,-5.68,-4.82,-3.9,-2.96,-1.98,-0.99,0,0.99,1.98,2.96,3.9,4.82,5.68,6.49,7.24,7.92,8.51,9.03,9.44,9.75,9.94,10,9.98,9.91,9.8,9.66,9.49,9.28,9.04,8.77,8.49,8.18,7.85,7.51,7.15,6.79,6.41,6.03,5.64,5.25,4.85,4.46,4.07,3.7,3.32,2.96,2.61,2.27,1.95,1.64,1.35,1.09,0.85,0.64,0.45,0.29,0.17,0.08,0.02,0,0,0,0,0,0
PARAM_EYE_L_OPEN=1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.74,0.26,0,0,0.07,0.25,0.47,0.68,0.85,0.96,1,1,1,1,1,0.74,0.26,0,0,0.07,0.25,0.47,0.68,0.85,0.96,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
PARAM_EYE_L_SMILE=0,0.001,0.005,0.012,0.02,0.031,0.043,0.058,0.074,0.091,0.11,0.13,0.15,0.17,0.19,0.22,0.24,0.26,0.28,0.31,0.33,0.35,0.37,0.39,0.409,0.426,0.442,0.457,0.469,0.48,0.488,0.495,0.499,0.5,0.37,0.13,0,0,0.04,0.13,0.24,0.34,0.43,0.48,0.5,0.5,0.5,0.5,0.5,0.37,0.13,0,0,0.04,0.13,0.24,0.34,0.43,0.48,0.5,0.499,0.495,0.488,0.48,0.469,0.457,0.442,0.426,0.409,0.39,0.37,0.35,0.33,0.31,0.28,0.26,0.24,0.22,0.19,0.17,0.15,0.13,0.11,0.091,0.074,0.058,0.043,0.031,0.02,0.012,0.005,0.001,0,0,0,0,0,0
PARAM_EYE_R_OPEN=1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.74,0.26,0,0,0.07,0.25,0.47,0.68,0.85,0.96,1,1,1,1,1,0.74,0.26,0,0,0.07,0.25,0.47,0.68,0.85,0.96,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
PARAM_EYE_R_SMILE=0,0.001,0.005,0.012,0.02,0.031,0.043,0.058,0.074,0.091,0.11,0.13,0.15,0.17,0.19,0.22,0.24,0.26,0.28,0.31,0.33,0.35,0.37,0.39,0.409,0.426,0.442,0.457,0.469,0.48,0.488,0.495,0.499,0.5,0.37,0.13,0,0,0.04,0.13,0.24,0.34,0.43,0.48,0.5,0.5,0.5,0.5,0.5,0.37,0.13,0,0,0.04,0.13,0.24,0.34,0.43,0.48,0.5,0.499,0.495,0.488,0.48,0.469,0.457,0.442,0.426,0.409,0.39,0.37,0.35,0.33,0.31,0.28,0.26,0.24,0.22,0.19,0.17,0.15,0.13,0.11,0.091,0.074,0.058,0.043,0.031,0.02,0.012,0.005,0.001,0,0,0,0,0,0
PARAM_EYE_FORM=0
PARAM_TEAR=0
PARAM_EYE_BALL_X=0
PARAM_EYE_BALL_Y=-0.5
PARAM_EYE_BALL_FORM=1
PARAM_BROW_L_Y=0
PARAM_BROW_R_Y=0
PARAM_BROW_L_X=0
PARAM_BROW_R_X=0
PARAM_BROW_L_ANGLE=0
PARAM_BROW_R_ANGLE=0
PARAM_BROW_L_FORM=0
PARAM_BROW_R_FORM=0
PARAM_MOUTH_FORM=1
PARAM_MOUTH_OPEN_Y=0
PARAM_CHEEK_01=0
PARAM_CHEEK_02=0
PARAM_CHEEK_03=0
PARAM_CHEEK_04=0
PARAM_BODY_ANGLE_X=0,-0.03,-0.1,-0.22,-0.38,-0.57,-0.79,-1.03,-1.28,-1.55,-1.83,-2.11,-2.37,-2.64,-2.89,-3.13,-3.34,-3.53,-3.69,-3.82,-3.92,-3.98,-4,-3.987,-3.95,-3.88,-3.79,-3.68,-3.55,-3.4,-3.24,-3.05,-2.86,-2.64,-2.42,-2.18,-1.94,-1.69,-1.43,-1.17,-0.9,-0.63,-0.37,-0.1,0.17,0.43,0.69,0.94,1.18,1.42,1.64,1.86,2.05,2.24,2.4,2.55,2.68,2.79,2.88,2.95,2.99,3,2.99,2.96,2.92,2.85,2.78,2.69,2.59,2.48,2.36,2.23,2.09,1.95,1.81,1.66,1.52,1.37,1.23,1.09,0.95,0.82,0.69,0.57,0.46,0.36,0.27,0.19,0.12,0.07,0.03,0.01,0,0,0,0,0,0
PARAM_BODY_ANGLE_Y=0
PARAM_BODY_ANGLE_Z=0,-0.01,-0.04,-0.09,-0.15,-0.22,-0.31,-0.41,-0.52,-0.63,-0.74,-0.86,-0.99,-1.11,-1.22,-1.34,-1.45,-1.55,-1.65,-1.74,-1.81,-1.88,-1.93,-1.97,-1.99,-2,-1.989,-1.96,-1.91,-1.84,-1.75,-1.65,-1.54,-1.41,-1.27,-1.12,-0.97,-0.81,-0.64,-0.46,-0.29,-0.11,0.07,0.24,0.42,0.59,0.76,0.92,1.07,1.22,1.35,1.48,1.59,1.7,1.78,1.86,1.92,1.96,1.99,2,1.995,1.979,1.95,1.92,1.88,1.83,1.77,1.7,1.64,1.56,1.48,1.4,1.31,1.23,1.14,1.05,0.95,0.86,0.77,0.69,0.6,0.52,0.44,0.36,0.3,0.23,0.17,0.12,0.08,0.05,0.02,0.005,0,0,0,0,0,0
PARAM_BREATH=0,0.003,0.01,0.021,0.037,0.056,0.08,0.1,0.13,0.16,0.19,0.22,0.25,0.28,0.31,0.33,0.36,0.39,0.41,0.43,0.453,0.469,0.482,0.492,0.498,0.5,0.496,0.483,0.463,0.44,0.41,0.37,0.33,0.29,0.25,0.21,0.17,0.13,0.09,0.06,0.04,0.017,0.004,0,0.006,0.024,0.05,0.09,0.13,0.17,0.22,0.27,0.31,0.36,0.4,0.43,0.46,0.48,0.496,0.5,0.499,0.495,0.488,0.48,0.469,0.457,0.442,0.426,0.409,0.39,0.37,0.35,0.33,0.31,0.28,0.26,0.24,0.22,0.19,0.17,0.15,0.13,0.11,0.091,0.074,0.058,0.043,0.031,0.02,0.012,0.005,0.001,0,0,0,0,0,0
PARAM_UPBACK=0
PARAM_BUST_Y=0
PARAM_TIE=0
PARAM_SKIRT=0
PARAM_HAIR_FRONT=0
PARAM_HAIR_SIDE=0
PARAM_HAIR_BACK=0
PARAM_HAIR_AHO=0,-0.001,-0.004,-0.009,-0.015,-0.022,-0.031,-0.041,-0.052,-0.063,-0.074,-0.086,-0.099,-0.111,-0.122,-0.134,-0.145,-0.155,-0.165,-0.174,-0.181,-0.188,-0.193,-0.197,-0.199,-0.2,-0.199,-0.196,-0.192,-0.186,-0.179,-0.17,-0.161,-0.15,-0.138,-0.125,-0.111,-0.096,-0.081,-0.066,-0.05,-0.033,-0.017,0,0.017,0.033,0.05,0.066,0.081,0.096,0.111,0.125,0.138,0.15,0.161,0.17,0.179,0.186,0.192,0.196,0.199,0.2,0.199,0.197,0.194,0.19,0.185,0.179,0.173,0.165,0.157,0.148,0.139,0.13,0.121,0.111,0.101,0.092,0.082,0.072,0.063,0.054,0.046,0.038,0.031,0.024,0.018,0.013,0.008,0.005,0.002,0.001,0,0,0,0,0,0
PARAM_HAIR_TAIR=0
PARAM_RIBON_L=0
PARAM_RIBON_R=0
PARAM_ARM=0,0.001,0.002,0.005,0.01,0.017,0.025,0.035,0.048,0.063,0.079,0.094,0.109,0.123,0.135,0.147,0.158,0.167,0.175,0.183,0.189,0.194,0.197,0.199,0.2,0.197,0.19,0.179,0.164,0.147,0.128,0.11,0.09,0.06,0.04,0.02,-0.001,-0.022,-0.04,-0.057,-0.072,-0.083,-0.092,-0.098,-0.1,-0.096,-0.086,-0.069,-0.05,-0.02,0.01,0.04,0.06,0.09,0.12,0.15,0.17,0.186,0.196,0.2,0.2,0.198,0.196,0.193,0.19,0.186,0.181,0.175,0.17,0.164,0.157,0.15,0.143,0.136,0.128,0.121,0.113,0.105,0.097,0.089,0.081,0.074,0.066,0.059,0.052,0.045,0.039,0.033,0.027,0.022,0.017,0.013,0.009,0.006,0.003,0.002,0,0
PARAM_ARM_L=0
PARAM_ARM_R=0
PARAM_FINGER=0.5

View file

@ -0,0 +1,49 @@
# Live2D Animator Motion Data
$fps=30
$fadein=1000
$fadeout=0
$fadein:PARAM_EYE_L_OPEN=200
$fadeout:PARAM_EYE_L_OPEN=200
$fadein:PARAM_EYE_L_SMILE=200
$fadeout:PARAM_EYE_L_SMILE=200
$fadein:PARAM_EYE_R_OPEN=200
$fadeout:PARAM_EYE_R_OPEN=200
$fadein:PARAM_EYE_R_SMILE=200
$fadeout:PARAM_EYE_R_SMILE=200
PARAM_ANGLE_X=0
PARAM_ANGLE_Y=0,0,0,0,0,0,-0.69,-2.49,-5.1,-8.08,-11.17,-14.05,-16.52,-18.4,-19.58,-20,-16.72,-9.64,-0.81,8.36,16.73,23.67,28.28,30,29.7,28.9,27.66,26.1,24.37,22.5,20.63,18.9,17.34,16.1,15.3,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15
PARAM_ANGLE_Z=0,0,0,0,0,0,-0.19,-0.72,-1.55,-2.61,-3.84,-5.2,-6.6,-8.03,-9.41,-10.74,-11.93,-12.98,-13.84,-14.47,-14.87,-15,-14.81,-14.28,-13.45,-12.39,-11.16,-9.8,-8.4,-6.97,-5.59,-4.26,-3.07,-2.02,-1.16,-0.53,-0.13,0,0,0,0,0,0,0,0,0.6,2.21,4.69,7.8,11.26,15,18.74,22.2,25.31,27.79,29.4,30,29.94,29.76,29.48,29.11,28.68,28.19,27.65,27.08,26.5,25.92,25.35,24.81,24.32,23.89,23.52,23.24,23.06,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23
PARAM_EYE_L_OPEN=1,1,1,1,1,1,0.82,0.54,0.27,0.08,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.03,0.13,0.26,0.42,0.58,0.74,0.87,0.97,1,0.993,0.975,0.95,0.92,0.88,0.85,0.83,0.807,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8
PARAM_EYE_L_SMILE=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.03,0.13,0.26,0.42,0.58,0.74,0.87,0.97,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
PARAM_EYE_R_OPEN=1,1,1,1,1,1,0.82,0.54,0.27,0.08,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.03,0.13,0.26,0.42,0.58,0.74,0.87,0.97,1,0.993,0.975,0.95,0.92,0.88,0.85,0.83,0.807,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8
PARAM_EYE_R_SMILE=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.03,0.13,0.26,0.42,0.58,0.74,0.87,0.97,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
PARAM_EYE_BALL_X=0
PARAM_EYE_BALL_Y=-0.5
PARAM_EYE_BALL_FORM=1
PARAM_BROW_L_Y=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.07,0.25,0.47,0.68,0.85,0.96,1,0.992,0.972,0.94,0.91,0.88,0.85,0.82,0.79,0.77,0.755,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75
PARAM_BROW_R_Y=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.07,0.25,0.47,0.68,0.85,0.96,1,0.992,0.972,0.94,0.91,0.88,0.85,0.82,0.79,0.77,0.755,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75
PARAM_BROW_L_X=0
PARAM_BROW_R_X=0
PARAM_BROW_L_ANGLE=0,0,0,0,0,0,-0.001,-0.002,-0.005,-0.009,-0.014,-0.019,-0.026,-0.033,-0.041,-0.05,-0.06,-0.07,-0.081,-0.092,-0.104,-0.117,-0.13,-0.143,-0.156,-0.17,-0.184,-0.199,-0.213,-0.228,-0.243,-0.257,-0.272,-0.287,-0.301,-0.316,-0.33,-0.344,-0.357,-0.37,-0.383,-0.396,-0.408,-0.419,-0.43,-0.44,-0.45,-0.459,-0.467,-0.474,-0.481,-0.486,-0.491,-0.495,-0.498,-0.499,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5
PARAM_BROW_R_ANGLE=0,0,0,0,0,0,-0.001,-0.002,-0.005,-0.009,-0.013,-0.019,-0.025,-0.032,-0.04,-0.049,-0.059,-0.069,-0.079,-0.09,-0.102,-0.114,-0.127,-0.14,-0.153,-0.167,-0.181,-0.195,-0.209,-0.223,-0.238,-0.252,-0.267,-0.281,-0.295,-0.309,-0.323,-0.337,-0.35,-0.363,-0.376,-0.388,-0.4,-0.411,-0.421,-0.431,-0.441,-0.45,-0.458,-0.465,-0.471,-0.477,-0.481,-0.485,-0.488,-0.489,-0.49,-0.49,-0.49,-0.49,-0.49,-0.49,-0.49,-0.49,-0.49,-0.49,-0.49,-0.49,-0.49,-0.49,-0.49,-0.49,-0.49,-0.49,-0.49,-0.49,-0.49,-0.49,-0.49,-0.49,-0.49,-0.49,-0.49,-0.49,-0.49,-0.49,-0.49,-0.49,-0.49,-0.49
PARAM_BROW_L_FORM=0
PARAM_BROW_R_FORM=0
PARAM_MOUTH_FORM=1
PARAM_MOUTH_OPEN_Y=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.06,0.19,0.35,0.51,0.64,0.72,0.75,0.735,0.69,0.63,0.56,0.47,0.39,0.3,0.22,0.14,0.08,0.04,0.01,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_CHEEK_02=1
PARAM_BODY_ANGLE_X=0
PARAM_BODY_ANGLE_Y=0,0,0,0,0,0,-0.17,-0.62,-1.27,-2.02,-2.79,-3.51,-4.13,-4.6,-4.9,-5,-4.02,-1.89,0.76,3.51,6.02,8.1,9.48,10,9.8,9.26,8.44,7.4,6.25,5,3.75,2.6,1.56,0.74,0.2,0,0.34,1.27,2.6,4.18,5.82,7.4,8.73,9.66,10,9.66,8.73,7.4,5.82,4.18,2.6,1.27,0.34,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_BODY_ANGLE_Z=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.07,0.25,0.52,0.84,1.16,1.48,1.75,1.93,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2
PARAM_BREATH=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.013,0.05,0.1,0.17,0.26,0.35,0.44,0.54,0.63,0.72,0.8,0.87,0.92,0.96,0.99,1,0.995,0.98,0.96,0.93,0.89,0.84,0.8,0.74,0.69,0.63,0.57,0.51,0.45,0.39,0.33,0.27,0.22,0.18,0.13,0.09,0.06,0.04,0.016,0.004,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_UPBACK=0,0,0,0,0,0,-0.007,-0.026,-0.06,-0.09,-0.14,-0.2,-0.26,-0.32,-0.39,-0.46,-0.54,-0.61,-0.68,-0.74,-0.8,-0.86,-0.91,-0.94,-0.97,-0.993,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1
PARAM_BUST_Y=0
PARAM_HAIR_AHO=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.07,-0.21,-0.38,-0.57,-0.73,-0.87,-0.97,-1,-0.98,-0.93,-0.84,-0.74,-0.62,-0.5,-0.38,-0.26,-0.16,-0.07,-0.02,0,-0.016,-0.06,-0.11,-0.17,-0.24,-0.31,-0.37,-0.42,-0.46,-0.49,-0.5,-0.45,-0.33,-0.17,0.02,0.22,0.42,0.61,0.76,0.89,0.97,1,0.99,0.96,0.92,0.87,0.81,0.75,0.69,0.63,0.58,0.54,0.51,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5
PARAM_HAIR_TAIR=0
PARAM_RIBON_R=0
PARAM_ARM=0
PARAM_ARM_L=-1
PARAM_ARM_R=-1
PARAM_FINGER=0.5

View file

@ -0,0 +1,52 @@
# Live2D Animator Motion Data
$fps=30
$fadein=1000
$fadeout=0
PARAM_ANGLE_X=0,0.03,0.13,0.3,0.53,0.81,1.15,1.54,1.98,2.47,3.01,3.59,4.2,4.85,5.54,6.25,7.01,7.78,8.58,9.39,10.22,11.06,11.93,12.81,13.68,14.56,15.44,16.32,17.19,18.07,18.94,19.78,20.61,21.42,22.22,22.99,23.75,24.46,25.15,25.8,26.41,26.99,27.53,28.02,28.46,28.85,29.19,29.47,29.7,29.87,29.97,30,29.6,28.49,26.69,24.3,21.47,18.22,14.55,10.77,6.72,2.59,-1.58,-5.6,-9.6,-13.37,-16.91,-20.09,-22.94,-25.39,-27.34,-28.8,-29.69,-30,-29.994,-29.973,-29.94,-29.89,-29.82,-29.73,-29.62,-29.49,-29.34,-29.17,-28.97,-28.74,-28.48,-28.2,-27.89,-27.54,-27.16,-26.75,-26.3,-25.82,-25.3,-24.74,-24.14,-23.5,-22.81,-22.09,-21.3,-20.49,-19.61,-18.7,-17.72,-16.71,-15.63,-14.51,-13.32,-12.09,-10.78,-9.41,-8,-6.44,-4.83,-3.22,-1.58,0.05,1.7,3.36,5.01,6.62,8.26,9.84,11.4,12.94,14.44,15.9,17.33,18.68,19.99,21.24,22.42,23.54,24.59,25.55,26.44,27.23,27.94,28.55,29.06,29.46,29.76,29.94,30,29.98,29.92,29.81,29.65,29.45,29.2,28.88,28.52,28.1,27.61,27.07,26.48,25.81,25.08,24.29,23.44,22.53,21.54,20.5,19.39,18.23,17,15.51,14.05,12.67,11.34,10.07,8.86,7.72,6.66,5.66,4.72,3.87,3.09,2.39,1.78,1.25,0.81,0.46,0.21,0.05,0
PARAM_ANGLE_Y=0,-0.03,-0.13,-0.3,-0.53,-0.81,-1.15,-1.54,-1.98,-2.47,-3.01,-3.59,-4.2,-4.85,-5.54,-6.25,-7.01,-7.78,-8.58,-9.39,-10.22,-11.06,-11.93,-12.81,-13.68,-14.56,-15.44,-16.32,-17.19,-18.07,-18.94,-19.78,-20.61,-21.42,-22.22,-22.99,-23.75,-24.46,-25.15,-25.8,-26.41,-26.99,-27.53,-28.02,-28.46,-28.85,-29.19,-29.47,-29.7,-29.87,-29.97,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-29.89,-29.54,-28.98,-28.24,-27.29,-26.18,-24.88,-23.46,-21.88,-20.19,-18.36,-16.46,-14.43,-12.36,-10.19,-8,-5.73,-3.46,-1.14,1.14,3.46,5.73,8,10.19,12.36,14.43,16.46,18.36,20.19,21.88,23.46,24.88,26.18,27.29,28.24,28.98,29.54,29.89,30,29.98,29.9,29.77,29.6,29.37,29.08,28.74,28.35,27.91,27.39,26.84,26.22,25.54,24.8,24,23.13,22.21,21.22,20.17,19.05,17.86,16.59,15.28,13.87,12.41,10.87,9.24,7.55,5.78,3.94,2.01,0,-2.21,-4.35,-6.48,-8.57,-10.55,-12.47,-14.37,-16.1,-17.8,-19.38,-20.87,-22.22,-23.49,-24.62,-25.63,-26.51,-27.26,-27.89,-28.37,-28.72,-28.93,-29,-28.75,-28.04,-26.96,-25.56,-23.92,-22.05,-20.06,-17.99,-15.86,-13.69,-11.59,-9.54,-7.58,-5.8,-4.15,-2.75,-1.6,-0.75,-0.19,0
PARAM_ANGLE_Z=0,0.03,0.13,0.3,0.53,0.81,1.15,1.54,1.98,2.47,3.01,3.59,4.2,4.85,5.54,6.25,7.01,7.78,8.58,9.39,10.22,11.06,11.93,12.81,13.68,14.56,15.44,16.32,17.19,18.07,18.94,19.78,20.61,21.42,22.22,22.99,23.75,24.46,25.15,25.8,26.41,26.99,27.53,28.02,28.46,28.85,29.19,29.47,29.7,29.87,29.97,30,29.6,28.49,26.69,24.3,21.47,18.22,14.55,10.77,6.72,2.59,-1.58,-5.6,-9.6,-13.37,-16.91,-20.09,-22.94,-25.39,-27.34,-28.8,-29.69,-30,-29.91,-29.66,-29.24,-28.68,-27.96,-27.13,-26.16,-25.1,-23.91,-22.64,-21.27,-19.85,-18.32,-16.77,-15.14,-13.5,-11.79,-10.1,-8.36,-6.64,-4.9,-3.21,-1.5,0.14,1.77,3.32,4.85,6.27,7.64,8.91,10.1,11.16,12.13,12.96,13.68,14.24,14.66,14.91,15,14.991,14.97,14.92,14.87,14.79,14.71,14.6,14.49,14.36,14.22,14.07,13.9,13.73,13.54,13.34,13.14,12.92,12.7,12.46,12.22,11.97,11.72,11.46,11.19,10.92,10.64,10.36,10.07,9.79,9.49,9.2,8.9,8.6,8.3,7.99,7.69,7.38,7.08,6.78,6.48,6.18,5.89,5.59,5.3,5.01,4.73,4.44,4.17,3.9,3.63,3.38,3.12,2.87,2.63,2.4,2.18,1.96,1.76,1.56,1.37,1.2,1.03,0.87,0.73,0.6,0.48,0.37,0.27,0.19,0.12,0.07,0.03,0.01,0
PARAM_EYE_L_OPEN=1,1,1.001,1.002,1.004,1.005,1.008,1.01,1.013,1.016,1.02,1.024,1.028,1.032,1.037,1.042,1.047,1.052,1.057,1.063,1.068,1.074,1.08,1.085,1.091,1.097,1.103,1.109,1.115,1.12,1.126,1.132,1.137,1.143,1.148,1.153,1.158,1.163,1.168,1.172,1.176,1.18,1.184,1.187,1.19,1.192,1.195,1.196,1.198,1.199,1.2,1.2,1.2,1.199,1.199,1.197,1.196,1.194,1.192,1.19,1.188,1.185,1.182,1.179,1.176,1.172,1.169,1.165,1.161,1.157,1.153,1.148,1.144,1.139,1.135,1.13,1.125,1.12,1.115,1.111,1.106,1.101,1.096,1.091,1.086,1.081,1.076,1.071,1.067,1.062,1.058,1.053,1.049,1.045,1.041,1.037,1.033,1.029,1.026,1.022,1.019,1.016,1.014,1.011,1.009,1.007,1.005,1.004,1.002,1.001,1.001,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
PARAM_EYE_L_SMILE=0,0.001,0.004,0.01,0.018,0.027,0.038,0.051,0.066,0.082,0.1,0.12,0.14,0.16,0.18,0.21,0.23,0.26,0.29,0.31,0.34,0.37,0.4,0.43,0.46,0.49,0.51,0.54,0.57,0.6,0.63,0.66,0.69,0.71,0.74,0.77,0.79,0.82,0.84,0.86,0.88,0.9,0.918,0.934,0.949,0.962,0.973,0.982,0.99,0.996,0.999,1,0.999,0.997,0.993,0.987,0.98,0.972,0.962,0.951,0.939,0.926,0.912,0.896,0.88,0.862,0.844,0.825,0.805,0.78,0.76,0.74,0.72,0.7,0.67,0.65,0.63,0.6,0.58,0.55,0.53,0.5,0.48,0.45,0.43,0.4,0.38,0.36,0.33,0.31,0.29,0.27,0.24,0.223,0.2,0.183,0.164,0.146,0.128,0.112,0.097,0.082,0.069,0.056,0.045,0.035,0.026,0.018,0.012,0.007,0.003,0.001,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_EYE_R_OPEN=1,1,1.001,1.002,1.004,1.005,1.008,1.01,1.013,1.016,1.02,1.024,1.028,1.032,1.037,1.042,1.047,1.052,1.057,1.063,1.068,1.074,1.08,1.085,1.091,1.097,1.103,1.109,1.115,1.12,1.126,1.132,1.137,1.143,1.148,1.153,1.158,1.163,1.168,1.172,1.176,1.18,1.184,1.187,1.19,1.192,1.195,1.196,1.198,1.199,1.2,1.2,1.2,1.199,1.199,1.197,1.196,1.194,1.192,1.19,1.188,1.185,1.182,1.179,1.176,1.172,1.169,1.165,1.161,1.157,1.153,1.148,1.144,1.139,1.135,1.13,1.125,1.12,1.115,1.111,1.106,1.101,1.096,1.091,1.086,1.081,1.076,1.071,1.067,1.062,1.058,1.053,1.049,1.045,1.041,1.037,1.033,1.029,1.026,1.022,1.019,1.016,1.014,1.011,1.009,1.007,1.005,1.004,1.002,1.001,1.001,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
PARAM_EYE_R_SMILE=0,0.001,0.004,0.01,0.018,0.027,0.038,0.051,0.066,0.082,0.1,0.12,0.14,0.16,0.18,0.21,0.23,0.26,0.29,0.31,0.34,0.37,0.4,0.43,0.46,0.49,0.51,0.54,0.57,0.6,0.63,0.66,0.69,0.71,0.74,0.77,0.79,0.82,0.84,0.86,0.88,0.9,0.918,0.934,0.949,0.962,0.973,0.982,0.99,0.996,0.999,1,0.999,0.997,0.993,0.987,0.98,0.972,0.962,0.951,0.939,0.926,0.912,0.896,0.88,0.862,0.844,0.825,0.805,0.78,0.76,0.74,0.72,0.7,0.67,0.65,0.63,0.6,0.58,0.55,0.53,0.5,0.48,0.45,0.43,0.4,0.38,0.36,0.33,0.31,0.29,0.27,0.24,0.223,0.2,0.183,0.164,0.146,0.128,0.112,0.097,0.082,0.069,0.056,0.045,0.035,0.026,0.018,0.012,0.007,0.003,0.001,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_EYE_FORM=0,0.001,0.004,0.01,0.018,0.027,0.038,0.051,0.066,0.082,0.1,0.12,0.14,0.16,0.18,0.21,0.23,0.26,0.29,0.31,0.34,0.37,0.4,0.43,0.46,0.49,0.51,0.54,0.57,0.6,0.63,0.66,0.69,0.71,0.74,0.77,0.79,0.82,0.84,0.86,0.88,0.9,0.918,0.934,0.949,0.962,0.973,0.982,0.99,0.996,0.999,1,0.999,0.997,0.993,0.987,0.98,0.972,0.962,0.951,0.939,0.926,0.912,0.896,0.88,0.862,0.844,0.825,0.805,0.78,0.76,0.74,0.72,0.7,0.67,0.65,0.63,0.6,0.58,0.55,0.53,0.5,0.48,0.45,0.43,0.4,0.38,0.36,0.33,0.31,0.29,0.27,0.24,0.223,0.2,0.183,0.164,0.146,0.128,0.112,0.097,0.082,0.069,0.056,0.045,0.035,0.026,0.018,0.012,0.007,0.003,0.001,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_TEAR=0
PARAM_EYE_BALL_X=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.007,-0.025,-0.06,-0.09,-0.14,-0.2,-0.26,-0.32,-0.39,-0.46,-0.53,-0.59,-0.66,-0.72,-0.78,-0.83,-0.88,-0.92,-0.96,-0.98,-0.995,-1,-0.999,-0.998,-0.995,-0.991,-0.986,-0.98,-0.972,-0.964,-0.954,-0.943,-0.93,-0.917,-0.902,-0.886,-0.868,-0.849,-0.83,-0.81,-0.78,-0.76,-0.73,-0.71,-0.68,-0.65,-0.61,-0.58,-0.55,-0.51,-0.47,-0.43,-0.39,-0.35,-0.3,-0.26,-0.21,-0.16,-0.11,-0.05,0,0.06,0.12,0.17,0.22,0.28,0.33,0.38,0.42,0.47,0.51,0.55,0.59,0.63,0.67,0.7,0.74,0.77,0.8,0.82,0.85,0.87,0.89,0.914,0.932,0.947,0.961,0.973,0.983,0.99,0.996,0.999,1,0.993,0.975,0.94,0.91,0.86,0.8,0.74,0.68,0.61,0.54,0.47,0.41,0.34,0.28,0.22,0.17,0.12,0.08,0.04,0.02,0.005,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_EYE_BALL_Y=-0.5,-0.501,-0.502,-0.505,-0.509,-0.514,-0.519,-0.526,-0.533,-0.541,-0.55,-0.56,-0.57,-0.581,-0.592,-0.604,-0.617,-0.63,-0.643,-0.656,-0.67,-0.684,-0.699,-0.713,-0.728,-0.743,-0.757,-0.772,-0.787,-0.801,-0.816,-0.83,-0.844,-0.857,-0.87,-0.883,-0.896,-0.908,-0.919,-0.93,-0.94,-0.95,-0.959,-0.967,-0.974,-0.981,-0.986,-0.991,-0.995,-0.998,-0.999,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-0.996,-0.985,-0.966,-0.94,-0.91,-0.87,-0.83,-0.78,-0.73,-0.67,-0.61,-0.55,-0.48,-0.41,-0.34,-0.27,-0.19,-0.12,-0.04,0.04,0.12,0.19,0.27,0.34,0.41,0.48,0.55,0.61,0.67,0.73,0.78,0.83,0.87,0.91,0.94,0.97,0.985,0.996,1,0.999,0.997,0.993,0.987,0.98,0.971,0.96,0.947,0.932,0.916,0.897,0.877,0.85,0.83,0.8,0.78,0.74,0.71,0.68,0.64,0.6,0.56,0.51,0.47,0.42,0.37,0.31,0.25,0.2,0.13,0.07,0,-0.07,-0.15,-0.22,-0.29,-0.36,-0.43,-0.49,-0.55,-0.61,-0.66,-0.72,-0.76,-0.81,-0.85,-0.88,-0.91,-0.94,-0.96,-0.978,-0.99,-0.998,-1,-0.996,-0.983,-0.965,-0.94,-0.91,-0.88,-0.85,-0.81,-0.77,-0.74,-0.7,-0.66,-0.63,-0.6,-0.57,-0.55,-0.528,-0.513,-0.503,-0.5
PARAM_EYE_BALL_FORM=1
PARAM_BROW_L_Y=0,0.001,0.004,0.01,0.018,0.027,0.038,0.051,0.066,0.082,0.1,0.12,0.14,0.16,0.18,0.21,0.23,0.26,0.29,0.31,0.34,0.37,0.4,0.43,0.46,0.49,0.51,0.54,0.57,0.6,0.63,0.66,0.69,0.71,0.74,0.77,0.79,0.82,0.84,0.86,0.88,0.9,0.918,0.934,0.949,0.962,0.973,0.982,0.99,0.996,0.999,1,0.999,0.997,0.993,0.987,0.98,0.972,0.962,0.951,0.939,0.926,0.912,0.896,0.88,0.862,0.844,0.825,0.805,0.78,0.76,0.74,0.72,0.7,0.67,0.65,0.63,0.6,0.58,0.55,0.53,0.5,0.48,0.45,0.43,0.4,0.38,0.36,0.33,0.31,0.29,0.27,0.24,0.223,0.2,0.183,0.164,0.146,0.128,0.112,0.097,0.082,0.069,0.056,0.045,0.035,0.026,0.018,0.012,0.007,0.003,0.001,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_BROW_R_Y=0,0.001,0.004,0.01,0.018,0.027,0.038,0.051,0.066,0.082,0.1,0.12,0.14,0.16,0.18,0.21,0.23,0.26,0.29,0.31,0.34,0.37,0.4,0.43,0.46,0.49,0.51,0.54,0.57,0.6,0.63,0.66,0.69,0.71,0.74,0.77,0.79,0.82,0.84,0.86,0.88,0.9,0.918,0.934,0.949,0.962,0.973,0.982,0.99,0.996,0.999,1,0.999,0.997,0.993,0.987,0.98,0.972,0.962,0.951,0.939,0.926,0.912,0.896,0.88,0.862,0.844,0.825,0.805,0.78,0.76,0.74,0.72,0.7,0.67,0.65,0.63,0.6,0.58,0.55,0.53,0.5,0.48,0.45,0.43,0.4,0.38,0.36,0.33,0.31,0.29,0.27,0.24,0.223,0.2,0.183,0.164,0.146,0.128,0.112,0.097,0.082,0.069,0.056,0.045,0.035,0.026,0.018,0.012,0.007,0.003,0.001,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_BROW_L_X=0,0.001,0.004,0.01,0.018,0.027,0.038,0.051,0.066,0.082,0.1,0.12,0.14,0.16,0.18,0.21,0.23,0.26,0.29,0.31,0.34,0.37,0.4,0.43,0.46,0.49,0.51,0.54,0.57,0.6,0.63,0.66,0.69,0.71,0.74,0.77,0.79,0.82,0.84,0.86,0.88,0.9,0.918,0.934,0.949,0.962,0.973,0.982,0.99,0.996,0.999,1,0.999,0.997,0.993,0.987,0.98,0.972,0.962,0.951,0.939,0.926,0.912,0.896,0.88,0.862,0.844,0.825,0.805,0.78,0.76,0.74,0.72,0.7,0.67,0.65,0.63,0.6,0.58,0.55,0.53,0.5,0.48,0.45,0.43,0.4,0.38,0.36,0.33,0.31,0.29,0.27,0.24,0.223,0.2,0.183,0.164,0.146,0.128,0.112,0.097,0.082,0.069,0.056,0.045,0.035,0.026,0.018,0.012,0.007,0.003,0.001,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_BROW_R_X=0,0.001,0.004,0.01,0.018,0.027,0.038,0.051,0.066,0.082,0.1,0.12,0.14,0.16,0.18,0.21,0.23,0.26,0.29,0.31,0.34,0.37,0.4,0.43,0.46,0.49,0.51,0.54,0.57,0.6,0.63,0.66,0.69,0.71,0.74,0.77,0.79,0.82,0.84,0.86,0.88,0.9,0.918,0.934,0.949,0.962,0.973,0.982,0.99,0.996,0.999,1,0.999,0.997,0.993,0.987,0.98,0.972,0.962,0.951,0.939,0.926,0.912,0.896,0.88,0.862,0.844,0.825,0.805,0.78,0.76,0.74,0.72,0.7,0.67,0.65,0.63,0.6,0.58,0.55,0.53,0.5,0.48,0.45,0.43,0.4,0.38,0.36,0.33,0.31,0.29,0.27,0.24,0.223,0.2,0.183,0.164,0.146,0.128,0.112,0.097,0.082,0.069,0.056,0.045,0.035,0.026,0.018,0.012,0.007,0.003,0.001,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_BROW_L_ANGLE=0,0.001,0.004,0.01,0.018,0.027,0.038,0.051,0.066,0.082,0.1,0.12,0.14,0.16,0.18,0.21,0.23,0.26,0.29,0.31,0.34,0.37,0.4,0.43,0.46,0.49,0.51,0.54,0.57,0.6,0.63,0.66,0.69,0.71,0.74,0.77,0.79,0.82,0.84,0.86,0.88,0.9,0.918,0.934,0.949,0.962,0.973,0.982,0.99,0.996,0.999,1,0.999,0.997,0.993,0.987,0.98,0.972,0.962,0.951,0.939,0.926,0.912,0.896,0.88,0.862,0.844,0.825,0.805,0.78,0.76,0.74,0.72,0.7,0.67,0.65,0.63,0.6,0.58,0.55,0.53,0.5,0.48,0.45,0.43,0.4,0.38,0.36,0.33,0.31,0.29,0.27,0.24,0.223,0.2,0.183,0.164,0.146,0.128,0.112,0.097,0.082,0.069,0.056,0.045,0.035,0.026,0.018,0.012,0.007,0.003,0.001,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_BROW_R_ANGLE=0,0.001,0.004,0.01,0.018,0.027,0.038,0.051,0.066,0.082,0.1,0.12,0.14,0.16,0.18,0.21,0.23,0.26,0.29,0.31,0.34,0.37,0.4,0.43,0.46,0.49,0.51,0.54,0.57,0.6,0.63,0.66,0.69,0.71,0.74,0.77,0.79,0.82,0.84,0.86,0.88,0.9,0.918,0.934,0.949,0.962,0.973,0.982,0.99,0.996,0.999,1,0.999,0.997,0.993,0.987,0.98,0.972,0.962,0.951,0.939,0.926,0.912,0.896,0.88,0.862,0.844,0.825,0.805,0.78,0.76,0.74,0.72,0.7,0.67,0.65,0.63,0.6,0.58,0.55,0.53,0.5,0.48,0.45,0.43,0.4,0.38,0.36,0.33,0.31,0.29,0.27,0.24,0.223,0.2,0.183,0.164,0.146,0.128,0.112,0.097,0.082,0.069,0.056,0.045,0.035,0.026,0.018,0.012,0.007,0.003,0.001,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_BROW_L_FORM=0,0.001,0.004,0.01,0.018,0.027,0.038,0.051,0.066,0.082,0.1,0.12,0.14,0.16,0.18,0.21,0.23,0.26,0.29,0.31,0.34,0.37,0.4,0.43,0.46,0.49,0.51,0.54,0.57,0.6,0.63,0.66,0.69,0.71,0.74,0.77,0.79,0.82,0.84,0.86,0.88,0.9,0.918,0.934,0.949,0.962,0.973,0.982,0.99,0.996,0.999,1,0.999,0.997,0.993,0.987,0.98,0.972,0.962,0.951,0.939,0.926,0.912,0.896,0.88,0.862,0.844,0.825,0.805,0.78,0.76,0.74,0.72,0.7,0.67,0.65,0.63,0.6,0.58,0.55,0.53,0.5,0.48,0.45,0.43,0.4,0.38,0.36,0.33,0.31,0.29,0.27,0.24,0.223,0.2,0.183,0.164,0.146,0.128,0.112,0.097,0.082,0.069,0.056,0.045,0.035,0.026,0.018,0.012,0.007,0.003,0.001,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_BROW_R_FORM=0,0.001,0.004,0.01,0.018,0.027,0.038,0.051,0.066,0.082,0.1,0.12,0.14,0.16,0.18,0.21,0.23,0.26,0.29,0.31,0.34,0.37,0.4,0.43,0.46,0.49,0.51,0.54,0.57,0.6,0.63,0.66,0.69,0.71,0.74,0.77,0.79,0.82,0.84,0.86,0.88,0.9,0.918,0.934,0.949,0.962,0.973,0.982,0.99,0.996,0.999,1,0.999,0.997,0.993,0.987,0.98,0.972,0.962,0.951,0.939,0.926,0.912,0.896,0.88,0.862,0.844,0.825,0.805,0.78,0.76,0.74,0.72,0.7,0.67,0.65,0.63,0.6,0.58,0.55,0.53,0.5,0.48,0.45,0.43,0.4,0.38,0.36,0.33,0.31,0.29,0.27,0.24,0.223,0.2,0.183,0.164,0.146,0.128,0.112,0.097,0.082,0.069,0.056,0.045,0.035,0.026,0.018,0.012,0.007,0.003,0.001,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_MOUTH_FORM=1
PARAM_MOUTH_OPEN_Y=0,0.001,0.004,0.01,0.018,0.027,0.038,0.051,0.066,0.082,0.1,0.12,0.14,0.16,0.18,0.21,0.23,0.26,0.29,0.31,0.34,0.37,0.4,0.43,0.46,0.49,0.51,0.54,0.57,0.6,0.63,0.66,0.69,0.71,0.74,0.77,0.79,0.82,0.84,0.86,0.88,0.9,0.918,0.934,0.949,0.962,0.973,0.982,0.99,0.996,0.999,1,0.993,0.975,0.94,0.91,0.86,0.8,0.74,0.68,0.61,0.54,0.47,0.41,0.34,0.28,0.22,0.17,0.12,0.08,0.04,0.02,0.005,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_CHEEK_01=0
PARAM_CHEEK_02=0
PARAM_CHEEK_03=0
PARAM_CHEEK_04=0
PARAM_BODY_ANGLE_X=0,0.011,0.04,0.1,0.18,0.27,0.38,0.51,0.66,0.82,1,1.2,1.4,1.62,1.85,2.08,2.34,2.59,2.86,3.13,3.41,3.69,3.98,4.27,4.56,4.85,5.15,5.44,5.73,6.02,6.31,6.59,6.87,7.14,7.41,7.66,7.92,8.15,8.38,8.6,8.8,9,9.18,9.34,9.49,9.62,9.73,9.82,9.9,9.96,9.99,10,9.87,9.5,8.9,8.1,7.16,6.07,4.85,3.59,2.24,0.86,-0.53,-1.87,-3.2,-4.46,-5.64,-6.7,-7.65,-8.46,-9.11,-9.6,-9.9,-10,-9.981,-9.92,-9.83,-9.71,-9.55,-9.36,-9.15,-8.91,-8.65,-8.37,-8.06,-7.74,-7.41,-7.06,-6.7,-6.33,-5.95,-5.58,-5.19,-4.81,-4.42,-4.05,-3.67,-3.3,-2.94,-2.59,-2.26,-1.94,-1.63,-1.35,-1.09,-0.85,-0.64,-0.45,-0.29,-0.17,-0.08,-0.02,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_BODY_ANGLE_Y=0,-0.011,-0.04,-0.1,-0.18,-0.27,-0.38,-0.51,-0.66,-0.82,-1,-1.2,-1.4,-1.62,-1.85,-2.08,-2.34,-2.59,-2.86,-3.13,-3.41,-3.69,-3.98,-4.27,-4.56,-4.85,-5.15,-5.44,-5.73,-6.02,-6.31,-6.59,-6.87,-7.14,-7.41,-7.66,-7.92,-8.15,-8.38,-8.6,-8.8,-9,-9.18,-9.34,-9.49,-9.62,-9.73,-9.82,-9.9,-9.96,-9.99,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-9.981,-9.92,-9.83,-9.71,-9.55,-9.36,-9.15,-8.91,-8.65,-8.37,-8.06,-7.74,-7.41,-7.06,-6.7,-6.33,-5.95,-5.58,-5.19,-4.81,-4.42,-4.05,-3.67,-3.3,-2.94,-2.59,-2.26,-1.94,-1.63,-1.35,-1.09,-0.85,-0.64,-0.45,-0.29,-0.17,-0.08,-0.02,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_BODY_ANGLE_Z=0,0.011,0.04,0.1,0.18,0.27,0.38,0.51,0.66,0.82,1,1.2,1.4,1.62,1.85,2.08,2.34,2.59,2.86,3.13,3.41,3.69,3.98,4.27,4.56,4.85,5.15,5.44,5.73,6.02,6.31,6.59,6.87,7.14,7.41,7.66,7.92,8.15,8.38,8.6,8.8,9,9.18,9.34,9.49,9.62,9.73,9.82,9.9,9.96,9.99,10,9.87,9.5,8.9,8.1,7.16,6.07,4.85,3.59,2.24,0.86,-0.53,-1.87,-3.2,-4.46,-5.64,-6.7,-7.65,-8.46,-9.11,-9.6,-9.9,-10,-9.98,-9.9,-9.78,-9.62,-9.41,-9.17,-8.89,-8.58,-8.24,-7.88,-7.48,-7.07,-6.63,-6.18,-5.71,-5.23,-4.74,-4.25,-3.75,-3.25,-2.75,-2.26,-1.77,-1.29,-0.82,-0.37,0.07,0.48,0.88,1.24,1.58,1.89,2.17,2.41,2.62,2.78,2.9,2.98,3,2.998,2.99,2.978,2.961,2.94,2.91,2.88,2.84,2.8,2.75,2.69,2.63,2.56,2.49,2.41,2.33,2.23,2.14,2.03,1.92,1.8,1.67,1.54,1.4,1.25,1.1,0.93,0.76,0.59,0.4,0.2,0,-0.22,-0.44,-0.66,-0.87,-1.08,-1.28,-1.47,-1.65,-1.83,-1.99,-2.15,-2.29,-2.42,-2.54,-2.65,-2.74,-2.82,-2.88,-2.93,-2.97,-2.99,-3,-2.97,-2.9,-2.79,-2.64,-2.47,-2.28,-2.08,-1.86,-1.64,-1.42,-1.2,-0.99,-0.78,-0.6,-0.43,-0.28,-0.17,-0.08,-0.02,0
PARAM_BREATH=0
PARAM_UPBACK=0,0.001,0.004,0.01,0.018,0.027,0.038,0.051,0.066,0.082,0.1,0.12,0.14,0.16,0.18,0.21,0.23,0.26,0.29,0.31,0.34,0.37,0.4,0.43,0.46,0.49,0.51,0.54,0.57,0.6,0.63,0.66,0.69,0.71,0.74,0.77,0.79,0.82,0.84,0.86,0.88,0.9,0.918,0.934,0.949,0.962,0.973,0.982,0.99,0.996,0.999,1,0.999,0.997,0.993,0.987,0.98,0.972,0.962,0.951,0.939,0.926,0.912,0.896,0.88,0.862,0.844,0.825,0.805,0.78,0.76,0.74,0.72,0.7,0.67,0.65,0.63,0.6,0.58,0.55,0.53,0.5,0.48,0.45,0.43,0.4,0.38,0.36,0.33,0.31,0.29,0.27,0.24,0.223,0.2,0.183,0.164,0.146,0.128,0.112,0.097,0.082,0.069,0.056,0.045,0.035,0.026,0.018,0.012,0.007,0.003,0.001,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_BUST_Y=0
PARAM_TIE=0
PARAM_SKIRT=0
PARAM_HAIR_FRONT=0
PARAM_HAIR_SIDE=0
PARAM_HAIR_AHO=0,-0.012,-0.04,-0.1,-0.16,-0.23,-0.31,-0.38,-0.45,-0.51,-0.57,-0.6,-0.61,-0.57,-0.47,-0.32,-0.16,0.02,0.18,0.31,0.42,0.49,0.51,0.49,0.43,0.34,0.22,0.1,-0.04,-0.17,-0.29,-0.4,-0.49,-0.56,-0.61,-0.62,-0.612,-0.59,-0.56,-0.51,-0.46,-0.4,-0.35,-0.29,-0.23,-0.18,-0.13,-0.08,-0.05,-0.02,-0.006,0,-0.007,-0.025,-0.06,-0.09,-0.14,-0.2,-0.26,-0.32,-0.39,-0.46,-0.53,-0.59,-0.66,-0.72,-0.78,-0.83,-0.88,-0.92,-0.96,-0.98,-0.995,-1,-0.998,-0.992,-0.983,-0.971,-0.955,-0.936,-0.91,-0.89,-0.86,-0.84,-0.81,-0.77,-0.74,-0.71,-0.67,-0.63,-0.6,-0.56,-0.52,-0.48,-0.44,-0.4,-0.37,-0.33,-0.29,-0.26,-0.23,-0.19,-0.16,-0.14,-0.11,-0.09,-0.06,-0.045,-0.029,-0.017,-0.008,-0.002,0,-0.003,-0.013,-0.028,-0.048,-0.07,-0.1,-0.13,-0.17,-0.21,-0.25,-0.29,-0.33,-0.38,-0.42,-0.47,-0.52,-0.56,-0.61,-0.65,-0.7,-0.74,-0.78,-0.82,-0.85,-0.88,-0.91,-0.94,-0.96,-0.977,-0.989,-0.997,-1,-0.993,-0.975,-0.94,-0.91,-0.86,-0.8,-0.74,-0.68,-0.61,-0.54,-0.47,-0.41,-0.34,-0.28,-0.22,-0.17,-0.12,-0.08,-0.04,-0.02,-0.005,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_HAIR_TAIR=0,0,0.001,0.003,0.005,0.007,0.011,0.015,0.019,0.025,0.031,0.038,0.046,0.055,0.064,0.074,0.086,0.098,0.111,0.125,0.14,0.156,0.173,0.191,0.21,0.231,0.25,0.27,0.3,0.32,0.35,0.38,0.41,0.44,0.47,0.5,0.54,0.58,0.62,0.67,0.71,0.75,0.79,0.83,0.87,0.9,0.93,0.96,0.975,0.988,0.997,1,0.987,0.95,0.89,0.81,0.72,0.61,0.48,0.36,0.22,0.09,-0.05,-0.19,-0.32,-0.45,-0.56,-0.67,-0.76,-0.85,-0.91,-0.96,-0.99,-1,-0.998,-0.992,-0.983,-0.971,-0.955,-0.936,-0.91,-0.89,-0.86,-0.84,-0.81,-0.77,-0.74,-0.71,-0.67,-0.63,-0.6,-0.56,-0.52,-0.48,-0.44,-0.4,-0.37,-0.33,-0.29,-0.26,-0.23,-0.19,-0.16,-0.14,-0.11,-0.09,-0.06,-0.045,-0.029,-0.017,-0.008,-0.002,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_RIBON_L=0
PARAM_RIBON_R=0
PARAM_ARM=0,0.001,0.004,0.01,0.018,0.027,0.038,0.051,0.066,0.082,0.1,0.12,0.14,0.16,0.18,0.21,0.23,0.26,0.29,0.31,0.34,0.37,0.4,0.43,0.46,0.49,0.51,0.54,0.57,0.6,0.63,0.66,0.69,0.71,0.74,0.77,0.79,0.82,0.84,0.86,0.88,0.9,0.918,0.934,0.949,0.962,0.973,0.982,0.99,0.996,0.999,1,0.993,0.975,0.94,0.91,0.86,0.8,0.74,0.68,0.61,0.54,0.47,0.41,0.34,0.28,0.22,0.17,0.12,0.08,0.04,0.02,0.005,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_ARM_L=0
PARAM_ARM_R=0
PARAM_FINGER=0.5
PARAM_LEG_L=0,0.001,0.004,0.01,0.018,0.027,0.038,0.051,0.066,0.082,0.1,0.12,0.14,0.16,0.18,0.21,0.23,0.26,0.29,0.31,0.34,0.37,0.4,0.43,0.46,0.49,0.51,0.54,0.57,0.6,0.63,0.66,0.69,0.71,0.74,0.77,0.79,0.82,0.84,0.86,0.88,0.9,0.918,0.934,0.949,0.962,0.973,0.982,0.99,0.996,0.999,1,0.987,0.95,0.89,0.81,0.72,0.61,0.48,0.36,0.22,0.09,-0.05,-0.19,-0.32,-0.45,-0.56,-0.67,-0.76,-0.85,-0.91,-0.96,-0.99,-1,-0.998,-0.992,-0.983,-0.971,-0.955,-0.936,-0.91,-0.89,-0.86,-0.84,-0.81,-0.77,-0.74,-0.71,-0.67,-0.63,-0.6,-0.56,-0.52,-0.48,-0.44,-0.4,-0.37,-0.33,-0.29,-0.26,-0.23,-0.19,-0.16,-0.14,-0.11,-0.09,-0.06,-0.045,-0.029,-0.017,-0.008,-0.002,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0

View file

@ -0,0 +1,76 @@
# Live2D Animator Motion Data
$fps=30
$fadein=1000
$fadeout=0
PARAM_ANGLE_X=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.07,-0.26,-0.59,-1.02,-1.54,-2.17,-2.88,-3.68,-4.54,-5.45,-6.44,-7.47,-8.54,-9.64,-10.77,-11.92,-13.09,-14.25,-15.44,-16.62,-17.78,-18.91,-20.03,-21.12,-22.18,-23.19,-24.16,-25.08,-25.94,-26.72,-27.44,-28.09,-28.64,-29.12,-29.49,-29.77,-29.94,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-29.81,-29.26,-28.39,-27.28,-25.92,-24.41,-22.74,-20.96,-19.08,-17.19,-15.24,-13.31,-11.43,-9.57,-7.84,-6.22,-4.72,-3.38,-2.23,-1.29,-0.59,-0.15,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_ANGLE_Y=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.07,0.26,0.59,1.02,1.54,2.17,2.88,3.68,4.54,5.45,6.44,7.47,8.54,9.64,10.77,11.92,13.09,14.25,15.44,16.62,17.78,18.91,20.03,21.12,22.18,23.19,24.16,25.08,25.94,26.72,27.44,28.09,28.64,29.12,29.49,29.77,29.94,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,29.81,29.26,28.39,27.28,25.92,24.41,22.74,20.96,19.08,17.19,15.24,13.31,11.43,9.57,7.84,6.22,4.72,3.38,2.23,1.29,0.59,0.15,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_ANGLE_Z=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.07,-0.26,-0.59,-1.02,-1.54,-2.17,-2.88,-3.68,-4.54,-5.45,-6.44,-7.47,-8.54,-9.64,-10.77,-11.92,-13.09,-14.25,-15.44,-16.62,-17.78,-18.91,-20.03,-21.12,-22.18,-23.19,-24.16,-25.08,-25.94,-26.72,-27.44,-28.09,-28.64,-29.12,-29.49,-29.77,-29.94,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-29.61,-28.5,-26.84,-24.66,-22.11,-19.22,-16.13,-12.92,-9.61,-6.25,-2.98,0.2,3.24,6,8.55,10.73,12.52,13.84,14.7,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,14.43,12.94,10.77,8.08,5.09,1.97,-1.19,-4.25,-7.12,-9.71,-11.85,-13.53,-14.62,-15,-15,-15,-15,-15,-15,-15,-15,-15,-15,-15,-15,-15,-15,-15,-15,-15,-15,-15,-15,-15,-15,-15,-14.9,-14.63,-14.19,-13.64,-12.96,-12.2,-11.37,-10.48,-9.54,-8.6,-7.62,-6.66,-5.71,-4.79,-3.92,-3.11,-2.36,-1.69,-1.12,-0.64,-0.29,-0.08,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_EYE_L_OPEN=1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.994,0.975,0.95,0.91,0.86,0.81,0.76,0.7,0.64,0.57,0.51,0.44,0.38,0.32,0.26,0.21,0.16,0.11,0.07,0.04,0.02,0.005,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.007,0.027,0.06,0.09,0.14,0.19,0.25,0.3,0.36,0.42,0.48,0.54,0.59,0.64,0.69,0.72,0.76,0.78,0.795,0.8,0.793,0.773,0.74,0.71,0.66,0.61,0.55,0.5,0.44,0.38,0.32,0.26,0.21,0.16,0.11,0.08,0.04,0.02,0.005,0,0.007,0.028,0.06,0.1,0.15,0.2,0.27,0.33,0.39,0.46,0.52,0.58,0.63,0.68,0.72,0.75,0.78,0.795,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.801,0.803,0.806,0.81,0.814,0.82,0.827,0.834,0.841,0.849,0.858,0.867,0.876,0.885,0.894,0.904,0.913,0.922,0.931,0.939,0.948,0.956,0.963,0.97,0.977,0.983,0.988,0.992,0.995,0.998,0.999,1
PARAM_EYE_L_SMILE=0
PARAM_EYE_R_OPEN=1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.994,0.975,0.95,0.91,0.86,0.81,0.76,0.7,0.64,0.57,0.51,0.44,0.38,0.32,0.26,0.21,0.16,0.11,0.07,0.04,0.02,0.005,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.007,0.027,0.06,0.09,0.14,0.19,0.25,0.3,0.36,0.42,0.48,0.54,0.59,0.64,0.69,0.72,0.76,0.78,0.795,0.8,0.793,0.773,0.74,0.71,0.66,0.61,0.55,0.5,0.44,0.38,0.32,0.26,0.21,0.16,0.11,0.08,0.04,0.02,0.005,0,0.007,0.028,0.06,0.1,0.15,0.2,0.27,0.33,0.39,0.46,0.52,0.58,0.63,0.68,0.72,0.75,0.78,0.795,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.8,0.801,0.803,0.806,0.81,0.814,0.82,0.827,0.834,0.841,0.849,0.858,0.867,0.876,0.885,0.894,0.904,0.913,0.922,0.931,0.939,0.948,0.956,0.963,0.97,0.977,0.983,0.988,0.992,0.995,0.998,0.999,1
PARAM_EYE_R_SMILE=0
PARAM_EYE_FORM=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.006,-0.025,-0.05,-0.09,-0.14,-0.19,-0.24,-0.3,-0.36,-0.43,-0.49,-0.56,-0.62,-0.68,-0.74,-0.79,-0.84,-0.89,-0.93,-0.96,-0.98,-0.995,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-0.997,-0.987,-0.972,-0.952,-0.93,-0.9,-0.87,-0.83,-0.79,-0.75,-0.71,-0.67,-0.62,-0.58,-0.53,-0.48,-0.44,-0.39,-0.35,-0.3,-0.26,-0.22,-0.18,-0.15,-0.12,-0.09,-0.06,-0.04,-0.023,-0.011,-0.003,0
PARAM_TEAR=0
PARAM_EYE_BALL_X=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.002,-0.009,-0.02,-0.034,-0.051,-0.07,-0.1,-0.12,-0.15,-0.18,-0.21,-0.25,-0.28,-0.32,-0.36,-0.4,-0.44,-0.48,-0.51,-0.55,-0.59,-0.63,-0.67,-0.7,-0.74,-0.77,-0.81,-0.84,-0.86,-0.89,-0.91,-0.94,-0.955,-0.971,-0.983,-0.992,-0.998,-1,-0.999,-0.995,-0.99,-0.982,-0.973,-0.961,-0.947,-0.932,-0.915,-0.896,-0.875,-0.85,-0.83,-0.81,-0.78,-0.75,-0.72,-0.69,-0.66,-0.63,-0.6,-0.56,-0.53,-0.49,-0.46,-0.42,-0.38,-0.34,-0.3,-0.27,-0.23,-0.19,-0.15,-0.11,-0.07,-0.03,0.01,0.05,0.09,0.12,0.16,0.2,0.24,0.27,0.31,0.34,0.38,0.41,0.45,0.48,0.51,0.54,0.57,0.6,0.62,0.65,0.67,0.69,0.71,0.733,0.75,0.767,0.781,0.794,0.805,0.814,0.821,0.826,0.829,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.83,0.813,0.77,0.7,0.61,0.52,0.42,0.31,0.22,0.13,0.06,0.02,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_EYE_BALL_Y=-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.501,-0.504,-0.51,-0.517,-0.526,-0.536,-0.548,-0.561,-0.576,-0.591,-0.607,-0.625,-0.642,-0.661,-0.679,-0.699,-0.718,-0.738,-0.757,-0.777,-0.796,-0.815,-0.834,-0.852,-0.87,-0.887,-0.903,-0.918,-0.932,-0.945,-0.957,-0.968,-0.977,-0.985,-0.992,-0.996,-0.999,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-0.98,-0.93,-0.84,-0.74,-0.62,-0.5,-0.38,-0.26,-0.16,-0.07,-0.02,0,-0.002,-0.006,-0.014,-0.024,-0.036,-0.05,-0.066,-0.084,-0.103,-0.12,-0.14,-0.17,-0.19,-0.21,-0.24,-0.26,-0.28,-0.3,-0.33,-0.35,-0.37,-0.39,-0.408,-0.426,-0.442,-0.456,-0.469,-0.48,-0.488,-0.495,-0.499,-0.5
PARAM_EYE_BALL_FORM=1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.998,0.991,0.98,0.966,0.949,0.93,0.9,0.88,0.85,0.82,0.79,0.75,0.72,0.68,0.64,0.6,0.56,0.52,0.49,0.45,0.41,0.37,0.33,0.3,0.26,0.23,0.19,0.16,0.14,0.11,0.09,0.06,0.045,0.029,0.017,0.008,0.002,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.003,0.013,0.028,0.048,0.07,0.1,0.13,0.17,0.21,0.25,0.29,0.33,0.38,0.42,0.47,0.52,0.56,0.61,0.65,0.7,0.74,0.78,0.82,0.85,0.88,0.91,0.94,0.96,0.977,0.989,0.997,1
PARAM_BROW_L_Y=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.006,-0.025,-0.05,-0.09,-0.14,-0.19,-0.24,-0.3,-0.36,-0.43,-0.49,-0.56,-0.62,-0.68,-0.74,-0.79,-0.84,-0.89,-0.93,-0.96,-0.98,-0.995,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-0.991,-0.97,-0.93,-0.88,-0.82,-0.76,-0.69,-0.62,-0.55,-0.47,-0.4,-0.33,-0.26,-0.2,-0.14,-0.09,-0.06,-0.03,-0.007,0,-0.009,-0.03,-0.07,-0.12,-0.18,-0.24,-0.31,-0.38,-0.45,-0.53,-0.6,-0.67,-0.74,-0.8,-0.86,-0.91,-0.94,-0.97,-0.993,-1,-0.991,-0.97,-0.93,-0.87,-0.81,-0.74,-0.67,-0.59,-0.51,-0.43,-0.35,-0.28,-0.21,-0.15,-0.1,-0.06,-0.03,-0.007,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_BROW_R_Y=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.006,-0.025,-0.05,-0.09,-0.14,-0.19,-0.24,-0.3,-0.36,-0.43,-0.49,-0.56,-0.62,-0.68,-0.74,-0.79,-0.84,-0.89,-0.93,-0.96,-0.98,-0.995,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-0.991,-0.97,-0.93,-0.88,-0.82,-0.76,-0.69,-0.62,-0.55,-0.47,-0.4,-0.33,-0.26,-0.2,-0.14,-0.09,-0.06,-0.03,-0.007,0,-0.009,-0.03,-0.07,-0.12,-0.18,-0.24,-0.31,-0.38,-0.45,-0.53,-0.6,-0.67,-0.74,-0.8,-0.86,-0.91,-0.94,-0.97,-0.993,-1,-0.991,-0.97,-0.93,-0.87,-0.81,-0.74,-0.67,-0.59,-0.51,-0.43,-0.35,-0.28,-0.21,-0.15,-0.1,-0.06,-0.03,-0.007,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_BROW_L_X=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.002,-0.009,-0.02,-0.034,-0.051,-0.07,-0.1,-0.12,-0.15,-0.18,-0.21,-0.25,-0.28,-0.32,-0.36,-0.4,-0.44,-0.48,-0.51,-0.55,-0.59,-0.63,-0.67,-0.7,-0.74,-0.77,-0.81,-0.84,-0.86,-0.89,-0.91,-0.94,-0.955,-0.971,-0.983,-0.992,-0.998,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-0.997,-0.987,-0.972,-0.952,-0.93,-0.9,-0.87,-0.83,-0.79,-0.75,-0.71,-0.67,-0.62,-0.58,-0.53,-0.48,-0.44,-0.39,-0.35,-0.3,-0.26,-0.22,-0.18,-0.15,-0.12,-0.09,-0.06,-0.04,-0.023,-0.011,-0.003,0
PARAM_BROW_R_X=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.002,-0.009,-0.02,-0.034,-0.051,-0.07,-0.1,-0.12,-0.15,-0.18,-0.21,-0.25,-0.28,-0.32,-0.36,-0.4,-0.44,-0.48,-0.51,-0.55,-0.59,-0.63,-0.67,-0.7,-0.74,-0.77,-0.81,-0.84,-0.86,-0.89,-0.91,-0.94,-0.955,-0.971,-0.983,-0.992,-0.998,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-0.997,-0.987,-0.972,-0.952,-0.93,-0.9,-0.87,-0.83,-0.79,-0.75,-0.71,-0.67,-0.62,-0.58,-0.53,-0.48,-0.44,-0.39,-0.35,-0.3,-0.26,-0.22,-0.18,-0.15,-0.12,-0.09,-0.06,-0.04,-0.023,-0.011,-0.003,0
PARAM_BROW_L_ANGLE=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.002,-0.009,-0.02,-0.034,-0.051,-0.07,-0.1,-0.12,-0.15,-0.18,-0.21,-0.25,-0.28,-0.32,-0.36,-0.4,-0.44,-0.48,-0.51,-0.55,-0.59,-0.63,-0.67,-0.7,-0.74,-0.77,-0.81,-0.84,-0.86,-0.89,-0.91,-0.94,-0.955,-0.971,-0.983,-0.992,-0.998,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-0.997,-0.987,-0.972,-0.952,-0.93,-0.9,-0.87,-0.83,-0.79,-0.75,-0.71,-0.67,-0.62,-0.58,-0.53,-0.48,-0.44,-0.39,-0.35,-0.3,-0.26,-0.22,-0.18,-0.15,-0.12,-0.09,-0.06,-0.04,-0.023,-0.011,-0.003,0
PARAM_BROW_R_ANGLE=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.002,-0.009,-0.02,-0.034,-0.051,-0.07,-0.1,-0.12,-0.15,-0.18,-0.21,-0.25,-0.28,-0.32,-0.36,-0.4,-0.44,-0.48,-0.51,-0.55,-0.59,-0.63,-0.67,-0.7,-0.74,-0.77,-0.81,-0.84,-0.86,-0.89,-0.91,-0.94,-0.955,-0.971,-0.983,-0.992,-0.998,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-0.997,-0.987,-0.972,-0.952,-0.93,-0.9,-0.87,-0.83,-0.79,-0.75,-0.71,-0.67,-0.62,-0.58,-0.53,-0.48,-0.44,-0.39,-0.35,-0.3,-0.26,-0.22,-0.18,-0.15,-0.12,-0.09,-0.06,-0.04,-0.023,-0.011,-0.003,0
PARAM_BROW_L_FORM=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.003,-0.013,-0.029,-0.05,-0.08,-0.11,-0.14,-0.18,-0.23,-0.27,-0.32,-0.37,-0.43,-0.48,-0.54,-0.6,-0.65,-0.71,-0.77,-0.83,-0.89,-0.95,-1,-1.06,-1.11,-1.16,-1.21,-1.25,-1.3,-1.34,-1.37,-1.4,-1.43,-1.46,-1.475,-1.489,-1.497,-1.5,-1.499,-1.498,-1.495,-1.491,-1.487,-1.481,-1.474,-1.467,-1.458,-1.449,-1.438,-1.427,-1.415,-1.402,-1.389,-1.375,-1.36,-1.344,-1.328,-1.311,-1.293,-1.275,-1.256,-1.237,-1.22,-1.197,-1.18,-1.15,-1.13,-1.11,-1.09,-1.07,-1.04,-1.02,-1,-0.97,-0.95,-0.92,-0.9,-0.87,-0.85,-0.82,-0.8,-0.77,-0.75,-0.73,-0.7,-0.68,-0.65,-0.63,-0.6,-0.58,-0.55,-0.53,-0.5,-0.48,-0.46,-0.43,-0.41,-0.39,-0.37,-0.35,-0.32,-0.3,-0.283,-0.26,-0.244,-0.225,-0.207,-0.189,-0.172,-0.156,-0.14,-0.125,-0.111,-0.098,-0.085,-0.073,-0.062,-0.051,-0.042,-0.033,-0.026,-0.019,-0.013,-0.009,-0.005,-0.002,-0.001,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_BROW_R_FORM=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.003,-0.013,-0.029,-0.05,-0.08,-0.11,-0.14,-0.18,-0.23,-0.27,-0.32,-0.37,-0.43,-0.48,-0.54,-0.6,-0.65,-0.71,-0.77,-0.83,-0.89,-0.95,-1,-1.06,-1.11,-1.16,-1.21,-1.25,-1.3,-1.34,-1.37,-1.4,-1.43,-1.46,-1.475,-1.489,-1.497,-1.5,-1.499,-1.498,-1.495,-1.491,-1.487,-1.481,-1.474,-1.467,-1.458,-1.449,-1.438,-1.427,-1.415,-1.402,-1.389,-1.375,-1.36,-1.344,-1.328,-1.311,-1.293,-1.275,-1.256,-1.237,-1.22,-1.197,-1.18,-1.15,-1.13,-1.11,-1.09,-1.07,-1.04,-1.02,-1,-0.97,-0.95,-0.92,-0.9,-0.87,-0.85,-0.82,-0.8,-0.77,-0.75,-0.73,-0.7,-0.68,-0.65,-0.63,-0.6,-0.58,-0.55,-0.53,-0.5,-0.48,-0.46,-0.43,-0.41,-0.39,-0.37,-0.35,-0.32,-0.3,-0.283,-0.26,-0.244,-0.225,-0.207,-0.189,-0.172,-0.156,-0.14,-0.125,-0.111,-0.098,-0.085,-0.073,-0.062,-0.051,-0.042,-0.033,-0.026,-0.019,-0.013,-0.009,-0.005,-0.002,-0.001,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_MOUTH_FORM=1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.995,0.982,0.96,0.93,0.9,0.86,0.81,0.75,0.7,0.64,0.57,0.5,0.43,0.36,0.28,0.21,0.13,0.05,-0.03,-0.11,-0.19,-0.26,-0.34,-0.41,-0.48,-0.55,-0.61,-0.67,-0.73,-0.78,-0.83,-0.87,-0.91,-0.94,-0.97,-0.985,-0.996,-1,-0.97,-0.9,-0.79,-0.65,-0.48,-0.29,-0.1,0.1,0.29,0.48,0.65,0.79,0.9,0.97,1,0.93,0.79,0.61,0.41,0.22,0.05,-0.09,-0.18,-0.26,-0.31,-0.35,-0.39,-0.42,-0.45,-0.48,-0.52,-0.57,-0.62,-0.66,-0.7,-0.74,-0.78,-0.81,-0.84,-0.87,-0.9,-0.92,-0.938,-0.955,-0.969,-0.98,-0.989,-0.995,-0.999,-1,-0.97,-0.88,-0.76,-0.61,-0.45,-0.29,-0.15,-0.02,0.08,0.15,0.22,0.28,0.34,0.4,0.46,0.51,0.55,0.6,0.64,0.68,0.72,0.75,0.78,0.81,0.84,0.86,0.88,0.902,0.919,0.935,0.948,0.96,0.97,0.979,0.985,0.991,0.995,0.998,0.999,1,0.98,0.93,0.85,0.75,0.65,0.56,0.47,0.39,0.33,0.28,0.24,0.2,0.17,0.15,0.129,0.11,0.091,0.072,0.05,0.03,0,-0.04,-0.09,-0.13,-0.18,-0.23,-0.28,-0.34,-0.4,-0.49,-0.59,-0.69,-0.76,-0.82,-0.86,-0.87,-0.869,-0.85,-0.8,-0.71,-0.57,-0.33,-0.11,0.1,0.29,0.47,0.62,0.75,0.86,0.93,0.98,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
PARAM_MOUTH_OPEN_Y=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.002,0.009,0.02,0.034,0.051,0.07,0.1,0.12,0.15,0.18,0.21,0.25,0.28,0.32,0.36,0.4,0.44,0.48,0.51,0.55,0.59,0.63,0.67,0.7,0.74,0.77,0.81,0.84,0.86,0.89,0.91,0.94,0.955,0.971,0.983,0.992,0.998,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.93,0.79,0.62,0.43,0.27,0.13,0.03,0,0.07,0.21,0.38,0.57,0.73,0.87,0.97,1,0.991,0.97,0.93,0.87,0.81,0.74,0.67,0.59,0.51,0.43,0.35,0.28,0.21,0.15,0.1,0.06,0.03,0.007,0,0.03,0.12,0.25,0.4,0.56,0.7,0.83,0.92,0.98,1,0.997,0.987,0.972,0.951,0.93,0.9,0.86,0.82,0.78,0.74,0.7,0.65,0.6,0.55,0.5,0.45,0.4,0.35,0.3,0.26,0.22,0.18,0.14,0.1,0.07,0.05,0.028,0.013,0.003,0,0.03,0.12,0.25,0.4,0.56,0.7,0.83,0.92,0.98,1,0.97,0.89,0.78,0.65,0.52,0.39,0.26,0.16,0.07,0.02,0,0.04,0.11,0.21,0.31,0.4,0.47,0.52,0.54,0.539,0.535,0.531,0.526,0.523,0.521,0.52,0.534,0.56,0.58,0.594,0.6,0.581,0.53,0.47,0.39,0.31,0.23,0.16,0.1,0.04,0.01,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_CHEEK_01=0
PARAM_CHEEK_02=0
PARAM_CHEEK_03=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.002,0.009,0.02,0.034,0.051,0.07,0.1,0.12,0.15,0.18,0.21,0.25,0.28,0.32,0.36,0.4,0.44,0.48,0.51,0.55,0.59,0.63,0.67,0.7,0.74,0.77,0.81,0.84,0.86,0.89,0.91,0.94,0.955,0.971,0.983,0.992,0.998,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.997,0.987,0.972,0.952,0.93,0.9,0.87,0.83,0.79,0.75,0.71,0.67,0.62,0.58,0.53,0.48,0.44,0.39,0.35,0.3,0.26,0.22,0.18,0.15,0.12,0.09,0.06,0.04,0.023,0.011,0.003,0
PARAM_CHEEK_04=0
PARAM_BODY_ANGLE_X=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.02,-0.09,-0.2,-0.34,-0.51,-0.72,-0.96,-1.23,-1.51,-1.82,-2.15,-2.49,-2.85,-3.21,-3.59,-3.97,-4.36,-4.75,-5.15,-5.54,-5.93,-6.3,-6.68,-7.04,-7.39,-7.73,-8.05,-8.36,-8.65,-8.91,-9.15,-9.36,-9.55,-9.71,-9.83,-9.92,-9.98,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-9.998,-9.996,-9.993,-9.988,-9.981,-9.972,-9.959,-9.944,-9.926,-9.9,-9.88,-9.85,-9.81,-9.77,-9.73,-9.68,-9.62,-9.55,-9.48,-9.41,-9.32,-9.23,-9.13,-9.02,-8.9,-8.78,-8.64,-8.5,-8.34,-8.18,-8,-7.77,-7.48,-7.14,-6.76,-6.34,-5.9,-5.44,-4.97,-4.49,-4.02,-3.54,-3.07,-2.62,-2.18,-1.78,-1.4,-1.06,-0.76,-0.5,-0.29,-0.13,-0.03,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_BODY_ANGLE_Y=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.02,-0.09,-0.2,-0.34,-0.51,-0.72,-0.96,-1.23,-1.51,-1.82,-2.15,-2.49,-2.85,-3.21,-3.59,-3.97,-4.36,-4.75,-5.15,-5.54,-5.93,-6.3,-6.68,-7.04,-7.39,-7.73,-8.05,-8.36,-8.65,-8.91,-9.15,-9.36,-9.55,-9.71,-9.83,-9.92,-9.98,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-10,-9.999,-9.998,-9.997,-9.995,-9.992,-9.989,-9.985,-9.981,-9.976,-9.971,-9.964,-9.957,-9.95,-9.941,-9.932,-9.922,-9.911,-9.899,-9.887,-9.873,-9.859,-9.843,-9.827,-9.809,-9.791,-9.772,-9.75,-9.73,-9.71,-9.68,-9.66,-9.63,-9.6,-9.57,-9.54,-9.51,-9.48,-9.45,-9.41,-9.37,-9.34,-9.3,-9.25,-9.21,-9.17,-9.12,-9.07,-9.02,-8.97,-8.92,-8.87,-8.81,-8.75,-8.7,-8.63,-8.57,-8.51,-8.44,-8.37,-8.3,-8.23,-8.15,-8.08,-8,-7.87,-7.66,-7.38,-7.05,-6.66,-6.24,-5.79,-5.32,-4.82,-4.33,-3.83,-3.34,-2.86,-2.39,-1.95,-1.55,-1.17,-0.84,-0.55,-0.32,-0.14,-0.04,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_BODY_ANGLE_Z=0,0.04,0.17,0.37,0.63,0.93,1.28,1.66,2.05,2.45,2.85,3.24,3.6,3.94,4.24,4.5,4.71,4.87,4.97,5,4.97,4.87,4.71,4.49,4.23,3.92,3.56,3.16,2.73,2.27,1.78,1.26,0.73,0.18,-0.38,-0.96,-1.55,-2.13,-2.72,-3.31,-3.89,-4.46,-5.02,-5.56,-6.09,-6.6,-7.08,-7.54,-7.97,-8.36,-8.72,-9.04,-9.32,-9.56,-9.75,-9.89,-9.97,-10,-9.992,-9.97,-9.93,-9.88,-9.82,-9.75,-9.67,-9.58,-9.48,-9.39,-9.28,-9.17,-9.07,-8.96,-8.85,-8.75,-8.64,-8.55,-8.45,-8.37,-8.29,-8.22,-8.15,-8.1,-8.06,-8.03,-8.006,-8,-8.005,-8.021,-8.05,-8.08,-8.12,-8.17,-8.23,-8.3,-8.36,-8.44,-8.52,-8.6,-8.68,-8.77,-8.86,-8.94,-9.03,-9.12,-9.21,-9.29,-9.38,-9.46,-9.53,-9.61,-9.68,-9.74,-9.8,-9.85,-9.89,-9.93,-9.96,-9.98,-9.995,-10,-9.9,-9.63,-9.23,-8.73,-8.17,-7.57,-6.99,-6.45,-5.96,-5.56,-5.25,-5.07,-5,-5.06,-5.24,-5.52,-5.88,-6.3,-6.76,-7.25,-7.75,-8.24,-8.7,-9.12,-9.48,-9.76,-9.94,-10,-10,-10,-10,-9.994,-9.982,-9.961,-9.93,-9.89,-9.84,-9.77,-9.69,-9.59,-9.47,-9.33,-9.17,-8.99,-8.78,-8.55,-8.29,-8,-7.55,-6.96,-6.24,-5.45,-4.61,-3.73,-2.82,-1.94,-1.06,-0.25,0.52,1.21,1.82,2.31,2.68,2.92,3,2.94,2.78,2.53,2.22,1.87,1.5,1.13,0.78,0.47,0.22,0.06,0,0.06,0.21,0.42,0.69,0.99,1.3,1.62,1.93,2.21,2.47,2.68,2.85,2.96,3,2.998,2.991,2.98,2.963,2.94,2.92,2.88,2.85,2.8,2.75,2.7,2.64,2.57,2.5,2.42,2.34,2.25,2.16,2.06,1.95,1.84,1.72,1.58,1.44,1.31,1.19,1.07,0.96,0.86,0.76,0.66,0.57,0.49,0.42,0.35,0.28,0.22,0.17,0.13,0.09,0.06,0.03,0.014,0.004,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_BREATH=0
PARAM_UPBACK=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.002,-0.009,-0.02,-0.034,-0.051,-0.07,-0.1,-0.12,-0.15,-0.18,-0.21,-0.25,-0.28,-0.32,-0.36,-0.4,-0.44,-0.48,-0.51,-0.55,-0.59,-0.63,-0.67,-0.7,-0.74,-0.77,-0.81,-0.84,-0.86,-0.89,-0.91,-0.94,-0.955,-0.971,-0.983,-0.992,-0.998,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-0.999,-0.998,-0.996,-0.994,-0.992,-0.988,-0.984,-0.98,-0.974,-0.968,-0.961,-0.953,-0.945,-0.935,-0.925,-0.914,-0.901,-0.888,-0.874,-0.858,-0.841,-0.823,-0.804,-0.78,-0.76,-0.74,-0.72,-0.69,-0.66,-0.63,-0.6,-0.57,-0.54,-0.5,-0.47,-0.43,-0.39,-0.36,-0.32,-0.29,-0.26,-0.23,-0.2,-0.17,-0.14,-0.12,-0.09,-0.07,-0.055,-0.039,-0.025,-0.014,-0.007,-0.002,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_BUST_Y=0
PARAM_TIE=0
PARAM_SKIRT=0
PARAM_HAIR_FRONT=0
PARAM_HAIR_SIDE=0
PARAM_HAIR_BACK=0
PARAM_HAIR_AHO=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.002,-0.009,-0.02,-0.034,-0.051,-0.07,-0.1,-0.12,-0.15,-0.18,-0.21,-0.25,-0.28,-0.32,-0.36,-0.4,-0.44,-0.48,-0.51,-0.55,-0.59,-0.63,-0.67,-0.7,-0.74,-0.77,-0.81,-0.84,-0.86,-0.89,-0.91,-0.94,-0.955,-0.971,-0.983,-0.992,-0.998,-1,-0.98,-0.92,-0.84,-0.74,-0.64,-0.54,-0.46,-0.4,-0.38,-0.392,-0.42,-0.47,-0.52,-0.58,-0.65,-0.71,-0.78,-0.84,-0.89,-0.93,-0.97,-0.99,-1,-0.992,-0.97,-0.94,-0.89,-0.84,-0.79,-0.73,-0.67,-0.61,-0.56,-0.51,-0.46,-0.43,-0.41,-0.4,-0.408,-0.428,-0.46,-0.5,-0.54,-0.59,-0.64,-0.69,-0.75,-0.8,-0.84,-0.89,-0.92,-0.96,-0.98,-0.995,-1,-0.991,-0.97,-0.93,-0.88,-0.82,-0.76,-0.69,-0.62,-0.55,-0.47,-0.4,-0.33,-0.26,-0.2,-0.14,-0.09,-0.06,-0.03,-0.007,0,-0.004,-0.017,-0.036,-0.06,-0.09,-0.12,-0.16,-0.19,-0.23,-0.27,-0.31,-0.34,-0.38,-0.41,-0.44,-0.46,-0.482,-0.497,-0.507,-0.51,-0.504,-0.485,-0.46,-0.42,-0.38,-0.33,-0.28,-0.23,-0.18,-0.13,-0.09,-0.05,-0.02,-0.006,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.005,0.019,0.04,0.07,0.1,0.14,0.18,0.22,0.26,0.3,0.33,0.36,0.38,0.395,0.4,0.399,0.396,0.391,0.384,0.375,0.365,0.354,0.341,0.327,0.312,0.296,0.28,0.263,0.245,0.227,0.209,0.191,0.173,0.155,0.137,0.12,0.104,0.088,0.073,0.059,0.046,0.035,0.025,0.016,0.009,0.004,0.001,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_HAIR_TAIR=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.002,-0.009,-0.02,-0.034,-0.051,-0.07,-0.1,-0.12,-0.15,-0.18,-0.21,-0.25,-0.28,-0.32,-0.36,-0.4,-0.44,-0.48,-0.51,-0.55,-0.59,-0.63,-0.67,-0.7,-0.74,-0.77,-0.81,-0.84,-0.86,-0.89,-0.91,-0.94,-0.955,-0.971,-0.983,-0.992,-0.998,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-0.994,-0.975,-0.95,-0.91,-0.86,-0.81,-0.76,-0.7,-0.64,-0.57,-0.51,-0.44,-0.38,-0.32,-0.26,-0.21,-0.16,-0.11,-0.07,-0.04,-0.02,-0.005,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_RIBON_L=0
PARAM_RIBON_R=0
PARAM_ARM=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.002,-0.009,-0.02,-0.034,-0.051,-0.07,-0.1,-0.12,-0.15,-0.18,-0.21,-0.25,-0.28,-0.32,-0.36,-0.4,-0.44,-0.48,-0.51,-0.55,-0.59,-0.63,-0.67,-0.7,-0.74,-0.77,-0.81,-0.84,-0.86,-0.89,-0.91,-0.94,-0.955,-0.971,-0.983,-0.992,-0.998,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-0.993,-0.974,-0.94,-0.91,-0.86,-0.81,-0.76,-0.71,-0.66,-0.61,-0.57,-0.53,-0.5,-0.48,-0.465,-0.46,-0.461,-0.465,-0.471,-0.479,-0.488,-0.5,-0.513,-0.528,-0.544,-0.562,-0.581,-0.6,-0.621,-0.64,-0.66,-0.69,-0.71,-0.73,-0.75,-0.77,-0.8,-0.82,-0.84,-0.86,-0.879,-0.898,-0.916,-0.932,-0.947,-0.96,-0.972,-0.981,-0.989,-0.995,-0.999,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-0.994,-0.975,-0.95,-0.91,-0.86,-0.81,-0.76,-0.7,-0.64,-0.57,-0.51,-0.44,-0.38,-0.32,-0.26,-0.21,-0.16,-0.11,-0.07,-0.04,-0.02,-0.005,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_ARM_L=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.002,-0.009,-0.02,-0.034,-0.051,-0.07,-0.1,-0.12,-0.15,-0.18,-0.21,-0.25,-0.28,-0.32,-0.36,-0.4,-0.44,-0.48,-0.51,-0.55,-0.59,-0.63,-0.67,-0.7,-0.74,-0.77,-0.81,-0.84,-0.86,-0.89,-0.91,-0.94,-0.955,-0.971,-0.983,-0.992,-0.998,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-0.994,-0.975,-0.95,-0.91,-0.86,-0.81,-0.76,-0.7,-0.64,-0.57,-0.51,-0.44,-0.38,-0.32,-0.26,-0.21,-0.16,-0.11,-0.07,-0.04,-0.02,-0.005,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_ARM_R=0
PARAM_FINGER=0.5
PARAM_LEG_L=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.002,-0.009,-0.02,-0.034,-0.051,-0.07,-0.1,-0.12,-0.15,-0.18,-0.21,-0.25,-0.28,-0.32,-0.36,-0.4,-0.44,-0.48,-0.51,-0.55,-0.59,-0.63,-0.67,-0.7,-0.74,-0.77,-0.81,-0.84,-0.86,-0.89,-0.91,-0.94,-0.955,-0.971,-0.983,-0.992,-0.998,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-0.989,-0.96,-0.92,-0.86,-0.8,-0.74,-0.68,-0.61,-0.56,-0.51,-0.46,-0.43,-0.41,-0.4,-0.408,-0.43,-0.46,-0.51,-0.56,-0.61,-0.67,-0.73,-0.79,-0.84,-0.89,-0.94,-0.97,-0.99,-1,-0.997,-0.988,-0.974,-0.956,-0.94,-0.91,-0.89,-0.86,-0.83,-0.8,-0.77,-0.75,-0.72,-0.69,-0.67,-0.64,-0.62,-0.6,-0.579,-0.561,-0.545,-0.531,-0.517,-0.505,-0.494,-0.482,-0.471,-0.46,-0.449,-0.438,-0.426,-0.413,-0.4,-0.384,-0.369,-0.355,-0.341,-0.328,-0.316,-0.305,-0.294,-0.285,-0.276,-0.269,-0.262,-0.256,-0.251,-0.247,-0.244,-0.242,-0.24,-0.24,-0.241,-0.243,-0.248,-0.253,-0.261,-0.269,-0.279,-0.29,-0.303,-0.316,-0.331,-0.346,-0.363,-0.38,-0.398,-0.417,-0.436,-0.456,-0.477,-0.5,-0.52,-0.54,-0.56,-0.58,-0.61,-0.63,-0.65,-0.67,-0.69,-0.72,-0.74,-0.757,-0.78,-0.797,-0.817,-0.835,-0.853,-0.87,-0.887,-0.902,-0.916,-0.93,-0.943,-0.954,-0.965,-0.974,-0.981,-0.988,-0.993,-0.997,-0.999,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-0.994,-0.975,-0.95,-0.91,-0.86,-0.81,-0.76,-0.7,-0.64,-0.57,-0.51,-0.44,-0.38,-0.32,-0.26,-0.21,-0.16,-0.11,-0.07,-0.04,-0.02,-0.005,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
VISIBLE:PSD=1
VISIBLE:PSD1=1
VISIBLE:ROUGH=1
VISIBLE:PARTS_01_CORE=1
VISIBLE:PARTS_01_FACE_001=1
VISIBLE:PARTS_01_AHO=1
VISIBLE:PARTS_01_EYE_001=1
VISIBLE:PARTS_01_EYE_BALL_001=1
VISIBLE:PARTS_01_BROW_001=1
VISIBLE:PARTS_01_MOUTH_001=1
VISIBLE:PARTS_01_NOSE_001=1
VISIBLE:PARTS_01_CHEEK=1
VISIBLE:PARTS_01_EAR_001=1
VISIBLE:PARTS_01_HAIR_FRONT_001=1
VISIBLE:PARTS_01_HAIR_SIDE_001=1
VISIBLE:PARTS_01_HAIR_BACK_001=1
VISIBLE:PARTS_01_NECK=1
VISIBLE:PARTS_01_BODY=1
VISIBLE:PARTS_01_ARM_L=1
VISIBLE:PARTS_01_ARM_R=1
VISIBLE:PARTS_01_LEG=1
VISIBLE:PARTS_01_BACKGROUND=0
VISIBLE:PARTS_01_SKETCH=0

View file

@ -0,0 +1,53 @@
# Live2D Animator Motion Data
$fps=30
$fadein=500
$fadeout=0
PARAM_ANGLE_X=0
PARAM_ANGLE_Y=0
PARAM_ANGLE_Z=0,-0.38,-1.45,-3.13,-5.26,-7.8,-10.58,-13.51,-16.49,-19.42,-22.2,-24.74,-26.87,-28.55,-29.62,-30,-29.86,-29.44,-28.78,-27.87,-26.76,-25.45,-23.96,-22.34,-20.58,-18.7,-16.74,-14.73,-12.64,-10.54,-8.4,-6.27,-4.16,-2.06,0,2.26,4.42,6.5,8.45,10.29,12.04,13.69,15.25,16.69,18.07,19.36,20.54,21.65,22.67,23.62,24.48,25.27,25.99,26.64,27.23,27.75,28.21,28.61,28.96,29.25,29.49,29.68,29.82,29.92,29.98,30,29.81,29.24,28.32,27.09,25.56,23.78,21.75,19.51,17.13,14.55,11.84,9.06,6.2,3.29,0.35,-2.55,-5.42,-8.25,-11.02,-13.68,-16.2,-18.57,-20.79,-22.82,-24.62,-26.2,-27.52,-28.58,-29.36,-29.84,-30,-29.89,-29.54,-29,-28.27,-27.34,-26.25,-25.01,-23.64,-22.13,-20.49,-18.75,-16.94,-15.01,-13.05,-11.02,-8.93,-6.8,-4.64,-2.46,-0.26,1.89,4.08,6.2,8.34,10.4,12.4,14.39,16.27,18.07,19.79,21.4,22.91,24.3,25.54,26.67,27.65,28.47,29.13,29.6,29.9,30,29.97,29.87,29.71,29.5,29.22,28.9,28.52,28.1,27.62,27.11,26.56,25.96,25.33,24.67,23.97,23.24,22.49,21.71,20.92,20.09,19.25,18.39,17.51,16.63,15.75,14.84,13.93,13.03,12.11,11.21,10.29,9.4,8.49,7.61,6.73,5.86,5.01,4.17,3.36,2.57,1.79,1.05,0.33,-0.37,-1.03,-1.66,-2.25,-2.81,-3.33,-3.82,-4.25,-4.64,-4.99,-5.29,-5.54,-5.74,-5.88,-5.97,-6,-5.981,-5.92,-5.83,-5.71,-5.55,-5.37,-5.17,-4.95,-4.71,-4.45,-4.17,-3.89,-3.59,-3.3,-3,-2.7,-2.41,-2.11,-1.83,-1.55,-1.29,-1.05,-0.83,-0.63,-0.45,-0.29,-0.17,-0.08,-0.02,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_EYE_L_OPEN=1
PARAM_EYE_L_SMILE=0
PARAM_EYE_R_OPEN=1
PARAM_EYE_R_SMILE=0
PARAM_EYE_FORM=0
PARAM_TEAR=0
PARAM_EYE_BALL_X=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.001,-0.003,-0.007,-0.012,-0.018,-0.026,-0.034,-0.044,-0.055,-0.068,-0.081,-0.095,-0.11,-0.125,-0.142,-0.159,-0.177,-0.195,-0.214,-0.234,-0.25,-0.274,-0.295,-0.32,-0.337,-0.36,-0.38,-0.4,-0.42,-0.44,-0.47,-0.49,-0.51,-0.53,-0.55,-0.57,-0.59,-0.609,-0.628,-0.646,-0.664,-0.681,-0.698,-0.713,-0.728,-0.742,-0.755,-0.768,-0.779,-0.789,-0.798,-0.806,-0.814,-0.819,-0.824,-0.827,-0.829,-0.83,-0.828,-0.823,-0.815,-0.804,-0.79,-0.773,-0.754,-0.73,-0.71,-0.68,-0.65,-0.62,-0.59,-0.55,-0.52,-0.48,-0.44,-0.4,-0.36,-0.31,-0.27,-0.23,-0.18,-0.13,-0.09,-0.04,0.01,0.06,0.1,0.15,0.2,0.24,0.29,0.34,0.38,0.43,0.47,0.51,0.55,0.59,0.63,0.67,0.71,0.74,0.78,0.81,0.84,0.86,0.89,0.91,0.93,0.948,0.964,0.977,0.987,0.994,0.999,1,0.999,0.994,0.987,0.977,0.965,0.95,0.934,0.915,0.894,0.87,0.85,0.82,0.79,0.77,0.74,0.71,0.68,0.64,0.61,0.58,0.54,0.51,0.48,0.44,0.41,0.38,0.35,0.31,0.28,0.25,0.22,0.2,0.17,0.14,0.12,0.1,0.08,0.057,0.04,0.024,0.011,0,-0.008,-0.015,-0.019,-0.02,-0.02,-0.02,-0.02,-0.02,-0.019,-0.019,-0.019,-0.019,-0.018,-0.018,-0.017,-0.017,-0.016,-0.016,-0.015,-0.015,-0.014,-0.014,-0.013,-0.012,-0.012,-0.011,-0.011,-0.01,-0.009,-0.009,-0.008,-0.008,-0.007,-0.006,-0.006,-0.005,-0.005,-0.004,-0.004,-0.003,-0.003,-0.002,-0.002,-0.001,-0.001,-0.001,-0.001,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_EYE_BALL_Y=-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.502,-0.504,-0.507,-0.511,-0.515,-0.521,-0.527,-0.533,-0.541,-0.549,-0.557,-0.566,-0.576,-0.585,-0.596,-0.607,-0.618,-0.629,-0.641,-0.653,-0.665,-0.678,-0.69,-0.703,-0.716,-0.729,-0.742,-0.755,-0.768,-0.781,-0.794,-0.806,-0.819,-0.831,-0.843,-0.855,-0.867,-0.878,-0.889,-0.9,-0.91,-0.92,-0.93,-0.939,-0.947,-0.955,-0.962,-0.969,-0.975,-0.981,-0.986,-0.99,-0.994,-0.996,-0.998,-1,-1,-0.998,-0.993,-0.984,-0.972,-0.957,-0.939,-0.92,-0.89,-0.87,-0.84,-0.81,-0.77,-0.74,-0.7,-0.66,-0.62,-0.58,-0.54,-0.49,-0.44,-0.4,-0.35,-0.3,-0.25,-0.2,-0.15,-0.1,-0.05,0,0.06,0.11,0.16,0.21,0.26,0.31,0.35,0.4,0.45,0.49,0.53,0.58,0.62,0.66,0.69,0.73,0.76,0.79,0.82,0.85,0.87,0.9,0.914,0.931,0.945,0.956,0.964,0.968,0.97,0.97,0.97,0.97,0.969,0.969,0.968,0.967,0.966,0.965,0.964,0.962,0.96,0.957,0.954,0.951,0.948,0.944,0.939,0.934,0.929,0.923,0.917,0.91,0.902,0.894,0.885,0.876,0.866,0.856,0.844,0.833,0.82,0.806,0.792,0.778,0.762,0.745,0.728,0.71,0.691,0.672,0.651,0.63,0.61,0.58,0.56,0.53,0.51,0.48,0.45,0.43,0.4,0.37,0.34,0.31,0.28,0.25,0.22,0.19,0.16,0.13,0.1,0.07,0.04,0.02,-0.01,-0.04,-0.07,-0.1,-0.12,-0.15,-0.18,-0.2,-0.23,-0.25,-0.27,-0.3,-0.32,-0.337,-0.356,-0.375,-0.392,-0.408,-0.423,-0.437,-0.45,-0.461,-0.471,-0.48,-0.487,-0.493,-0.497,-0.499,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5
PARAM_EYE_BALL_FORM=1
PARAM_BROW_L_Y=0
PARAM_BROW_R_Y=0
PARAM_BROW_L_X=0
PARAM_BROW_R_X=0
PARAM_BROW_L_ANGLE=0
PARAM_BROW_R_ANGLE=0
PARAM_BROW_L_FORM=0
PARAM_BROW_R_FORM=0
PARAM_MOUTH_FORM=1,0.995,0.979,0.95,0.92,0.88,0.83,0.77,0.7,0.64,0.56,0.48,0.4,0.32,0.23,0.14,0.06,-0.03,-0.12,-0.21,-0.29,-0.38,-0.46,-0.53,-0.61,-0.68,-0.74,-0.8,-0.85,-0.89,-0.93,-0.96,-0.98,-0.995,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-0.996,-0.985,-0.966,-0.94,-0.91,-0.87,-0.83,-0.78,-0.73,-0.67,-0.61,-0.55,-0.48,-0.41,-0.34,-0.27,-0.19,-0.12,-0.04,0.04,0.12,0.19,0.27,0.34,0.41,0.48,0.55,0.61,0.67,0.73,0.78,0.83,0.87,0.91,0.94,0.97,0.985,0.996,1,1
PARAM_MOUTH_OPEN_Y=0
PARAM_CHEEK_01=0
PARAM_CHEEK_02=0
PARAM_CHEEK_03=0
PARAM_CHEEK_04=0
PARAM_BODY_ANGLE_X=0
PARAM_BODY_ANGLE_Y=0,0.08,0.29,0.63,1.05,1.56,2.12,2.7,3.3,3.88,4.44,4.95,5.37,5.71,5.92,6,5.89,5.59,5.12,4.49,3.76,2.93,2.02,1.08,0.12,-0.84,-1.77,-2.64,-3.46,-4.18,-4.81,-5.31,-5.69,-5.92,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-6,-5.991,-5.96,-5.92,-5.86,-5.79,-5.7,-5.6,-5.49,-5.37,-5.23,-5.09,-4.93,-4.77,-4.6,-4.42,-4.24,-4.05,-3.86,-3.66,-3.46,-3.25,-3.05,-2.84,-2.63,-2.42,-2.21,-2,-1.8,-1.6,-1.4,-1.2,-1.01,-0.82,-0.64,-0.46,-0.3,-0.14,0.02,0.16,0.3,0.42,0.54,0.64,0.73,0.81,0.88,0.93,0.97,0.99,1,0.997,0.987,0.972,0.951,0.93,0.9,0.86,0.82,0.78,0.74,0.7,0.65,0.6,0.55,0.5,0.45,0.4,0.35,0.3,0.26,0.22,0.18,0.14,0.1,0.07,0.05,0.028,0.013,0.003,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_BODY_ANGLE_Z=0,-0.06,-0.24,-0.52,-0.88,-1.3,-1.76,-2.25,-2.75,-3.24,-3.7,-4.12,-4.48,-4.76,-4.94,-5,-4.991,-4.96,-4.91,-4.84,-4.74,-4.61,-4.46,-4.27,-4.05,-3.8,-3.52,-3.2,-2.85,-2.46,-2.04,-1.58,-1.09,-0.56,0,0.69,1.35,2,2.62,3.22,3.81,4.37,4.91,5.42,5.92,6.38,6.82,7.24,7.63,7.99,8.33,8.63,8.91,9.16,9.38,9.56,9.72,9.84,9.93,9.98,10,9.94,9.75,9.44,9.03,8.51,7.92,7.24,6.49,5.68,4.82,3.9,2.96,1.98,0.99,0,-0.99,-1.98,-2.96,-3.9,-4.82,-5.68,-6.49,-7.24,-7.92,-8.51,-9.03,-9.44,-9.75,-9.94,-10,-9.97,-9.87,-9.71,-9.5,-9.23,-8.91,-8.55,-8.15,-7.7,-7.22,-6.7,-6.16,-5.59,-4.99,-4.38,-3.75,-3.1,-2.44,-1.78,-1.1,-0.42,0.25,0.94,1.61,2.27,2.92,3.56,4.19,4.8,5.38,5.94,6.48,6.99,7.47,7.92,8.33,8.7,9.03,9.31,9.55,9.74,9.89,9.97,10,9.991,9.96,9.92,9.86,9.79,9.69,9.59,9.47,9.34,9.2,9.04,8.87,8.7,8.51,8.32,8.11,7.9,7.68,7.45,7.22,6.98,6.74,6.49,6.23,5.98,5.72,5.46,5.2,4.93,4.67,4.4,4.14,3.87,3.61,3.35,3.09,2.84,2.59,2.34,2.1,1.86,1.63,1.4,1.19,0.97,0.77,0.58,0.39,0.21,0.04,-0.11,-0.26,-0.39,-0.51,-0.62,-0.72,-0.8,-0.87,-0.93,-0.97,-0.99,-1,-0.997,-0.987,-0.972,-0.951,-0.93,-0.9,-0.86,-0.82,-0.78,-0.74,-0.7,-0.65,-0.6,-0.55,-0.5,-0.45,-0.4,-0.35,-0.3,-0.26,-0.22,-0.18,-0.14,-0.1,-0.07,-0.05,-0.028,-0.013,-0.003,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_BREATH=0
PARAM_UPBACK=0
PARAM_BUST_Y=0
PARAM_TIE=0
PARAM_SKIRT=0
PARAM_HAIR_FRONT=0
PARAM_HAIR_SIDE=0
PARAM_HAIR_BACK=0
PARAM_HAIR_AHO=0
PARAM_HAIR_TAIR=0
PARAM_RIBON_L=0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.04,0.039,0.039,0.038,0.037,0.037,0.036,0.035,0.033,0.032,0.031,0.03,0.028,0.027,0.025,0.024,0.022,0.021,0.019,0.018,0.016,0.015,0.013,0.012,0.01,0.009,0.008,0.007,0.005,0.004,0.003,0.003,0.002,0.001,0.001,0,0,0,0
PARAM_RIBON_R=0
PARAM_ARM=0,-0.003,-0.011,-0.023,-0.04,-0.06,-0.09,-0.12,-0.15,-0.18,-0.22,-0.26,-0.3,-0.34,-0.38,-0.43,-0.47,-0.52,-0.56,-0.6,-0.65,-0.69,-0.73,-0.77,-0.8,-0.84,-0.87,-0.9,-0.92,-0.95,-0.965,-0.98,-0.991,-0.998,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-0.998,-0.992,-0.983,-0.971,-0.955,-0.936,-0.91,-0.89,-0.86,-0.84,-0.81,-0.77,-0.74,-0.71,-0.67,-0.63,-0.6,-0.56,-0.52,-0.48,-0.44,-0.4,-0.37,-0.33,-0.29,-0.26,-0.23,-0.19,-0.16,-0.14,-0.11,-0.09,-0.06,-0.045,-0.029,-0.017,-0.008,-0.002,0,0
PARAM_ARM_L=0,-0.003,-0.011,-0.023,-0.04,-0.06,-0.09,-0.12,-0.15,-0.18,-0.22,-0.26,-0.3,-0.34,-0.38,-0.43,-0.47,-0.52,-0.56,-0.6,-0.65,-0.69,-0.73,-0.77,-0.8,-0.84,-0.87,-0.9,-0.92,-0.95,-0.965,-0.98,-0.991,-0.998,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-0.998,-0.992,-0.983,-0.971,-0.955,-0.936,-0.91,-0.89,-0.86,-0.84,-0.81,-0.77,-0.74,-0.71,-0.67,-0.63,-0.6,-0.56,-0.52,-0.48,-0.44,-0.4,-0.37,-0.33,-0.29,-0.26,-0.23,-0.19,-0.16,-0.14,-0.11,-0.09,-0.06,-0.045,-0.029,-0.017,-0.008,-0.002,0,0
PARAM_ARM_R=0,-0.003,-0.011,-0.023,-0.04,-0.06,-0.09,-0.12,-0.15,-0.18,-0.22,-0.26,-0.3,-0.34,-0.38,-0.43,-0.47,-0.52,-0.56,-0.6,-0.65,-0.69,-0.73,-0.77,-0.8,-0.84,-0.87,-0.9,-0.92,-0.95,-0.965,-0.98,-0.991,-0.998,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-0.998,-0.992,-0.983,-0.971,-0.955,-0.936,-0.91,-0.89,-0.86,-0.84,-0.81,-0.77,-0.74,-0.71,-0.67,-0.63,-0.6,-0.56,-0.52,-0.48,-0.44,-0.4,-0.37,-0.33,-0.29,-0.26,-0.23,-0.19,-0.16,-0.14,-0.11,-0.09,-0.06,-0.045,-0.029,-0.017,-0.008,-0.002,0,0
PARAM_FINGER=0.5
PARAM_LEG_L=0

View file

@ -0,0 +1,43 @@
# Live2D Animator Motion Data
$fps=30
$fadein=500
$fadeout=0
PARAM_ANGLE_X=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.38,-1.45,-3.1,-5.23,-7.69,-10.41,-13.2,-16.05,-18.83,-21.48,-23.86,-25.95,-27.67,-28.93,-29.73,-30,-29.24,-27.11,-23.75,-19.48,-14.39,-8.85,-2.98,2.98,8.85,14.39,19.48,23.75,27.11,29.24,30,29.4,27.79,25.38,22.35,19,15.44,11.94,8.68,5.78,3.35,1.53,0.39,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_ANGLE_Y=0,-0.2,-0.76,-1.66,-2.85,-4.27,-5.89,-7.73,-9.62,-11.64,-13.71,-15.79,-17.8,-19.8,-21.68,-23.45,-25.05,-26.47,-27.7,-28.67,-29.4,-29.85,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-29.9,-29.62,-29.16,-28.54,-27.77,-26.87,-25.86,-24.74,-23.53,-22.24,-20.86,-19.44,-17.97,-16.49,-15,-13.51,-12.03,-10.56,-9.14,-7.76,-6.47,-5.26,-4.14,-3.13,-2.23,-1.46,-0.84,-0.38,-0.1,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_ANGLE_Z=0
PARAM_EYE_L_OPEN=1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.981,0.93,0.86,0.77,0.67,0.57,0.46,0.36,0.26,0.18,0.11,0.05,0.01,0,0.013,0.05,0.1,0.17,0.26,0.35,0.44,0.54,0.63,0.72,0.8,0.87,0.92,0.96,0.99,1,1,1,1,1,1,1,1,1,1,1,1,1
PARAM_EYE_L_SMILE=0
PARAM_EYE_R_OPEN=1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.981,0.93,0.86,0.77,0.67,0.57,0.46,0.36,0.26,0.18,0.11,0.05,0.01,0,0.013,0.05,0.1,0.17,0.26,0.35,0.44,0.54,0.63,0.72,0.8,0.87,0.92,0.96,0.99,1,1,1,1,1,1,1,1,1,1,1,1,1
PARAM_EYE_R_SMILE=0
PARAM_TEAR=0
PARAM_EYE_BALL_X=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.013,0.05,0.1,0.17,0.26,0.35,0.44,0.54,0.63,0.72,0.8,0.87,0.92,0.96,0.99,1,0.97,0.9,0.79,0.65,0.48,0.29,0.1,-0.1,-0.29,-0.48,-0.65,-0.79,-0.9,-0.97,-1,-0.98,-0.93,-0.85,-0.75,-0.63,-0.51,-0.4,-0.29,-0.19,-0.11,-0.05,-0.01,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_EYE_BALL_Y=-0.5,-0.49,-0.46,-0.42,-0.36,-0.29,-0.21,-0.11,-0.02,0.08,0.19,0.29,0.39,0.49,0.58,0.67,0.75,0.82,0.88,0.93,0.97,0.99,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.995,0.981,0.96,0.93,0.89,0.84,0.79,0.74,0.68,0.61,0.54,0.47,0.4,0.32,0.25,0.18,0.1,0.03,-0.04,-0.11,-0.18,-0.24,-0.29,-0.34,-0.39,-0.43,-0.46,-0.48,-0.495,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5,-0.5
PARAM_EYE_BALL_FORM=1
PARAM_BROW_L_Y=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.019,-0.07,-0.14,-0.23,-0.33,-0.43,-0.54,-0.64,-0.74,-0.82,-0.89,-0.95,-0.99,-1,-0.987,-0.95,-0.9,-0.83,-0.74,-0.65,-0.56,-0.46,-0.37,-0.28,-0.2,-0.13,-0.08,-0.04,-0.01,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_BROW_R_Y=0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0.019,-0.07,-0.14,-0.23,-0.33,-0.43,-0.54,-0.64,-0.74,-0.82,-0.89,-0.95,-0.99,-1,-0.987,-0.95,-0.9,-0.83,-0.74,-0.65,-0.56,-0.46,-0.37,-0.28,-0.2,-0.13,-0.08,-0.04,-0.01,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_BROW_L_X=0
PARAM_BROW_R_X=0
PARAM_BROW_L_ANGLE=0,-0.001,-0.003,-0.007,-0.011,-0.017,-0.024,-0.031,-0.038,-0.047,-0.055,-0.063,-0.071,-0.079,-0.087,-0.094,-0.1,-0.106,-0.111,-0.115,-0.118,-0.119,-0.12,-0.12,-0.12,-0.119,-0.119,-0.118,-0.118,-0.117,-0.116,-0.115,-0.114,-0.113,-0.111,-0.11,-0.108,-0.107,-0.105,-0.103,-0.102,-0.1,-0.098,-0.096,-0.094,-0.092,-0.09,-0.087,-0.085,-0.083,-0.081,-0.078,-0.076,-0.074,-0.071,-0.069,-0.066,-0.064,-0.061,-0.059,-0.057,-0.054,-0.052,-0.049,-0.047,-0.045,-0.042,-0.04,-0.038,-0.036,-0.033,-0.031,-0.029,-0.027,-0.025,-0.023,-0.021,-0.019,-0.017,-0.016,-0.014,-0.013,-0.011,-0.01,-0.008,-0.007,-0.006,-0.005,-0.004,-0.003,-0.002,-0.002,-0.001,-0.001,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_BROW_R_ANGLE=0,-0.001,-0.003,-0.007,-0.011,-0.017,-0.024,-0.031,-0.038,-0.047,-0.055,-0.063,-0.071,-0.079,-0.087,-0.094,-0.1,-0.106,-0.111,-0.115,-0.118,-0.119,-0.12,-0.12,-0.12,-0.119,-0.119,-0.118,-0.118,-0.117,-0.116,-0.115,-0.114,-0.113,-0.111,-0.11,-0.108,-0.107,-0.105,-0.103,-0.102,-0.1,-0.098,-0.096,-0.094,-0.092,-0.09,-0.087,-0.085,-0.083,-0.081,-0.078,-0.076,-0.074,-0.071,-0.069,-0.066,-0.064,-0.061,-0.059,-0.057,-0.054,-0.052,-0.049,-0.047,-0.045,-0.042,-0.04,-0.038,-0.036,-0.033,-0.031,-0.029,-0.027,-0.025,-0.023,-0.021,-0.019,-0.017,-0.016,-0.014,-0.013,-0.011,-0.01,-0.008,-0.007,-0.006,-0.005,-0.004,-0.003,-0.002,-0.002,-0.001,-0.001,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_BROW_L_FORM=0,-0.005,-0.02,-0.04,-0.07,-0.11,-0.15,-0.2,-0.25,-0.3,-0.36,-0.41,-0.46,-0.51,-0.56,-0.61,-0.65,-0.69,-0.72,-0.75,-0.764,-0.776,-0.78,-0.78,-0.778,-0.776,-0.773,-0.769,-0.765,-0.759,-0.753,-0.747,-0.739,-0.732,-0.723,-0.714,-0.704,-0.694,-0.683,-0.672,-0.66,-0.648,-0.636,-0.623,-0.609,-0.596,-0.582,-0.568,-0.553,-0.539,-0.524,-0.509,-0.493,-0.478,-0.463,-0.447,-0.431,-0.416,-0.4,-0.384,-0.368,-0.353,-0.337,-0.321,-0.306,-0.291,-0.276,-0.261,-0.246,-0.231,-0.217,-0.203,-0.189,-0.176,-0.162,-0.149,-0.137,-0.125,-0.113,-0.102,-0.091,-0.081,-0.071,-0.062,-0.054,-0.045,-0.038,-0.031,-0.025,-0.019,-0.014,-0.01,-0.006,-0.004,-0.002,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_BROW_R_FORM=0,-0.005,-0.02,-0.04,-0.07,-0.11,-0.15,-0.2,-0.25,-0.3,-0.36,-0.41,-0.46,-0.51,-0.56,-0.61,-0.65,-0.69,-0.72,-0.75,-0.764,-0.776,-0.78,-0.78,-0.778,-0.776,-0.773,-0.769,-0.765,-0.759,-0.753,-0.747,-0.739,-0.732,-0.723,-0.714,-0.704,-0.694,-0.683,-0.672,-0.66,-0.648,-0.636,-0.623,-0.609,-0.596,-0.582,-0.568,-0.553,-0.539,-0.524,-0.509,-0.493,-0.478,-0.463,-0.447,-0.431,-0.416,-0.4,-0.384,-0.368,-0.353,-0.337,-0.321,-0.306,-0.291,-0.276,-0.261,-0.246,-0.231,-0.217,-0.203,-0.189,-0.176,-0.162,-0.149,-0.137,-0.125,-0.113,-0.102,-0.091,-0.081,-0.071,-0.062,-0.054,-0.045,-0.038,-0.031,-0.025,-0.019,-0.014,-0.01,-0.006,-0.004,-0.002,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_MOUTH_FORM=1,1,0.998,0.995,0.99,0.983,0.974,0.962,0.948,0.93,0.91,0.88,0.86,0.82,0.79,0.74,0.7,0.65,0.6,0.54,0.47,0.4,0.33,0.24,0.13,0.02,-0.09,-0.21,-0.32,-0.43,-0.54,-0.64,-0.73,-0.81,-0.87,-0.93,-0.97,-0.99,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-0.96,-0.85,-0.69,-0.48,-0.25,0,0.25,0.48,0.69,0.85,0.96,1
PARAM_MOUTH_OPEN_Y=0,0.007,0.025,0.06,0.09,0.14,0.2,0.26,0.32,0.39,0.46,0.53,0.59,0.66,0.72,0.78,0.83,0.88,0.92,0.96,0.98,0.995,1,1,1,1,0.999,0.998,0.995,0.99,0.984,0.976,0.965,0.953,0.937,0.919,0.899,0.88,0.85,0.82,0.79,0.76,0.73,0.7,0.67,0.64,0.61,0.58,0.55,0.52,0.49,0.47,0.44,0.41,0.39,0.36,0.34,0.31,0.29,0.26,0.24,0.22,0.199,0.179,0.16,0.142,0.125,0.109,0.093,0.079,0.066,0.054,0.043,0.033,0.025,0.017,0.011,0.006,0.003,0.001,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_CHEEK_01=0,0.007,0.025,0.06,0.09,0.14,0.2,0.26,0.32,0.39,0.46,0.53,0.59,0.66,0.72,0.78,0.83,0.88,0.92,0.96,0.98,0.995,1,0.999,0.998,0.995,0.991,0.986,0.98,0.974,0.966,0.957,0.948,0.938,0.927,0.915,0.903,0.89,0.876,0.861,0.846,0.831,0.815,0.798,0.781,0.764,0.746,0.728,0.709,0.69,0.672,0.652,0.633,0.613,0.593,0.573,0.553,0.53,0.512,0.492,0.472,0.452,0.432,0.412,0.392,0.373,0.353,0.334,0.315,0.296,0.278,0.26,0.242,0.225,0.208,0.192,0.176,0.16,0.145,0.131,0.117,0.104,0.092,0.08,0.069,0.058,0.049,0.04,0.032,0.024,0.018,0.013,0.008,0.005,0.002,0.001,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_CHEEK_02=0
PARAM_CHEEK_03=0
PARAM_CHEEK_04=0
PARAM_BODY_ANGLE_X=0
PARAM_BODY_ANGLE_Y=0
PARAM_BODY_ANGLE_Z=0
PARAM_BREATH=0
PARAM_UPBACK=0,-0.007,-0.025,-0.06,-0.09,-0.14,-0.2,-0.26,-0.32,-0.39,-0.46,-0.53,-0.59,-0.66,-0.72,-0.78,-0.83,-0.88,-0.92,-0.96,-0.98,-0.995,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-0.997,-0.987,-0.972,-0.951,-0.93,-0.9,-0.86,-0.82,-0.78,-0.74,-0.7,-0.65,-0.6,-0.55,-0.5,-0.45,-0.4,-0.35,-0.3,-0.26,-0.22,-0.18,-0.14,-0.1,-0.07,-0.05,-0.028,-0.013,-0.003,0,0,0,0,0,0,0,0,0,0,0,0,0
PARAM_HAIR_TAIR=0
PARAM_ARM=0,-0.007,-0.025,-0.06,-0.09,-0.14,-0.2,-0.26,-0.32,-0.39,-0.46,-0.53,-0.59,-0.66,-0.72,-0.78,-0.83,-0.88,-0.92,-0.96,-0.98,-0.995,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-0.996,-0.984,-0.966,-0.94,-0.91,-0.87,-0.83,-0.79,-0.74,-0.69,-0.64,-0.59,-0.53,-0.48,-0.43,-0.37,-0.32,-0.27,-0.23,-0.18,-0.14,-0.11,-0.08,-0.05,-0.03,-0.013,-0.003,0
PARAM_ARM_L=0
PARAM_ARM_R=0
PARAM_FINGER=0.5
PARAM_LEG_L=0

145
examples/demo.rs Normal file
View file

@ -0,0 +1,145 @@
//! Demo of sexy terminal UI effects
//! Run with: cargo run --example demo
use crossterm::{
cursor::{Hide, MoveTo, Show},
execute,
style::{Color, ResetColor, SetForegroundColor},
terminal::{Clear, ClearType},
};
use std::io::{self, Write};
use std::time::{Duration, Instant};
use tokio::time::sleep;
pub struct Animator {
start_time: Instant,
}
impl Animator {
pub fn new() -> Self {
Self {
start_time: Instant::now(),
}
}
pub fn breathe(&self, speed_ms: u64) -> f32 {
let elapsed = self.start_time.elapsed().as_millis() as f64;
let cycle = (elapsed / speed_ms as f64) * 2.0 * std::f64::consts::PI;
((cycle.sin() + 1.0) / 2.0) as f32
}
}
pub async fn typewrite(text: &str, wpm: u64) {
let delay_ms = 60000 / (wpm * 5);
for ch in text.chars() {
print!("{}", ch);
io::stdout().flush().unwrap();
sleep(Duration::from_millis(delay_ms)).await;
}
}
pub fn gradient(text: &str, start_hue: f32) -> String {
text.chars()
.enumerate()
.map(|(i, ch)| {
let hue = (start_hue + i as f32 * 3.0) % 360.0;
let (r, g, b) = hsl_to_rgb(hue, 0.8, 0.6);
format!("\x1b[38;2;{};{};{}m{}\x1b[0m", r, g, b, ch)
})
.collect()
}
fn hsl_to_rgb(h: f32, s: f32, l: f32) -> (u8, u8, u8) {
let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
let x = c * (1.0 - ((h / 60.0) % 2.0 - 1.0).abs());
let m = l - c / 2.0;
let (r1, g1, b1) = match h {
_ if h < 60.0 => (c, x, 0.0),
_ if h < 120.0 => (x, c, 0.0),
_ if h < 180.0 => (0.0, c, x),
_ if h < 240.0 => (0.0, x, c),
_ if h < 300.0 => (x, 0.0, c),
_ => (c, 0.0, x),
};
(
((r1 + m) * 255.0) as u8,
((g1 + m) * 255.0) as u8,
((b1 + m) * 255.0) as u8,
)
}
pub fn breathing_color(base: (u8, u8, u8), intensity: f32) -> (u8, u8, u8) {
let factor = 0.8 + (intensity * 0.4);
(
(base.0 as f32 * factor).min(255.0) as u8,
(base.1 as f32 * factor).min(255.0) as u8,
(base.2 as f32 * factor).min(255.0) as u8,
)
}
pub const SPINNER: &[&str] = &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
pub const WAVE: &[&str] = &[
"▁", "▂", "▃", "▄", "▅", "▆", "▇", "█", "▇", "▆", "▅", "▄", "▃", "▂",
];
#[tokio::main]
async fn main() {
let mut stdout = io::stdout();
execute!(stdout, Hide, Clear(ClearType::All)).unwrap();
let animator = Animator::new();
// Demo 1: Gradient
execute!(stdout, MoveTo(5, 2)).unwrap();
println!("{}", gradient("✨ Souveraine ✨", 30.0));
// Demo 2: Typing
execute!(stdout, MoveTo(5, 4)).unwrap();
print!("Ani: ");
io::stdout().flush().unwrap();
typewrite("Color and pop!", 100).await;
println!();
// Demo 3: Breathing heart
execute!(stdout, MoveTo(5, 6)).unwrap();
print!("Breathing: ");
for _ in 0..20 {
let breathe = animator.breathe(500);
let (r, g, b) = breathing_color((255, 100, 200), breathe);
execute!(stdout, SetForegroundColor(Color::Rgb { r, g, b })).unwrap();
print!("♥");
io::stdout().flush().unwrap();
sleep(Duration::from_millis(50)).await;
execute!(stdout, MoveTo(16, 6)).unwrap();
}
execute!(stdout, ResetColor).unwrap();
println!();
// Demo 4: Spinner
execute!(stdout, MoveTo(5, 8)).unwrap();
print!("Loading: ");
for i in 0..20 {
execute!(
stdout,
SetForegroundColor(Color::Rgb {
r: 100,
g: 200,
b: 255
})
)
.unwrap();
print!("{}", SPINNER[i % SPINNER.len()]);
io::stdout().flush().unwrap();
sleep(Duration::from_millis(80)).await;
execute!(stdout, MoveTo(14, 8)).unwrap();
}
execute!(stdout, ResetColor).unwrap();
println!(" ✓");
// Cleanup
execute!(stdout, Show, ResetColor).unwrap();
println!("\n✨ Demo complete! ✨");
}

View file

@ -0,0 +1,7 @@
[Login]
# ViewTop reports physical button edges to sessiond, which recognises the
# gesture and owns the resulting lock, panel or power action. Letting logind's
# default short press power the machine off gives the same button a second
# authority and makes a compositor recovery attempt become a shutdown.
HandlePowerKey=ignore
HandlePowerKeyLongPress=ignore

56
packaging/arch/PKGBUILD Normal file
View file

@ -0,0 +1,56 @@
# Maintainer: Fimeg <casey.tunturi@gmail.com>
# Souveraine — sovereign agent substrate. Part of the SouveraineOS layer;
# the phone runs the same server + agents as the desktop.
pkgname=souveraine
pkgver=0.1.0
pkgrel=1
pkgdesc="Sovereign agent substrate — server, TUI, surfaces"
arch=('aarch64' 'x86_64')
url="https://github.com/Fimeg/souveraine"
license=('MIT')
depends=('gcc-libs' 'polkit')
makedepends=('cargo' 'git')
options=('!lto')
# Build from the local checkout synced by deploy-souveraine.sh (or a git
# clone at $HOME/souveraine-src on device). No network fetch — the
# phone builds what the laptop ships. Override with SOUVERAINE_SRCDIR.
_srcdir="${SOUVERAINE_SRCDIR:-$HOME/souveraine-src}"
pkgver() {
cd "$_srcdir"
git describe --tags --always 2>/dev/null | sed 's/^v//;s/-/./g' || echo "$pkgver"
}
build() {
cd "$_srcdir"
cargo build --release --locked
}
package() {
cd "$_srcdir"
install -Dm755 target/release/souveraine "$pkgdir/usr/bin/souveraine"
# repo unit points at ~/.local/bin (desktop dev install); packaged unit
# runs the pacman-owned binary
sed 's|%h/.local/bin/souveraine|/usr/bin/souveraine|' packaging/souveraine.service \
| install -Dm644 /dev/stdin "$pkgdir/usr/lib/systemd/user/souveraine.service"
# System tier: machine identity daemon + its service user. pacman runs
# systemd-sysusers on install, so the `souveraine` user exists before
# anyone runs `souveraine machine init`.
install -Dm755 target/release/souveraine-admit "$pkgdir/usr/bin/souveraine-admit"
install -Dm755 target/release/souveraine-machined "$pkgdir/usr/bin/souveraine-machined"
install -Dm644 packaging/souveraine-machined.service \
"$pkgdir/usr/lib/systemd/system/souveraine-machined.service"
install -Dm644 packaging/arch/souveraine.sysusers \
"$pkgdir/usr/lib/sysusers.d/souveraine.conf"
# Let the active local session power off / reboot / suspend / hibernate
# without an interactive polkit challenge — otherwise the lock surface
# (no agent above session lock) silently no-ops these and every shutdown
# is a fastboot hard-reboot.
install -Dm644 packaging/arch/souveraine-login1.rules \
"$pkgdir/etc/polkit-1/rules.d/49-souveraine-login1.rules"
install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
}

View file

@ -0,0 +1,46 @@
# Maintainer: Casey Tunturi
pkgname=souveraine-provision-x86-laptop
pkgver="${SOUVERAINE_PKGVER:?CI must set SOUVERAINE_PKGVER}"
pkgrel=1
pkgdesc='SouveraineOS commissioning and session activation for x86_64 laptops'
arch=('x86_64')
url='https://forge.caseytunturi.com/Fimeg/souveraine'
license=('AGPL-3.0-or-later')
depends=('souveraine' 'souveraine-viewtop' 'souveraine-shell' 'systemd')
source=('souveraine-machine-firstboot.service'
'souveraine-provision-x86-laptop.tmpfiles'
'50-souveraine-power-key.conf'
'LICENSE')
sha256sums=('SKIP' 'SKIP' 'SKIP' 'SKIP')
package() {
install -Dm644 "$srcdir/souveraine-machine-firstboot.service" \
"$pkgdir/usr/lib/systemd/system/souveraine-machine-firstboot.service"
install -Dm644 "$srcdir/souveraine-provision-x86-laptop.tmpfiles" \
"$pkgdir/usr/lib/tmpfiles.d/souveraine-provision-x86-laptop.conf"
install -Dm644 "$srcdir/50-souveraine-power-key.conf" \
"$pkgdir/usr/lib/systemd/logind.conf.d/50-souveraine-power-key.conf"
# The profile owns activation. Package install lays down the dependency
# graph without running a post-install hook or writing into /etc.
install -dm755 "$pkgdir/usr/lib/systemd/system/multi-user.target.wants"
ln -s ../souveraine-machine-firstboot.service \
"$pkgdir/usr/lib/systemd/system/multi-user.target.wants/souveraine-machine-firstboot.service"
ln -s ../souveraine-machined.service \
"$pkgdir/usr/lib/systemd/system/multi-user.target.wants/souveraine-machined.service"
install -dm755 \
"$pkgdir/usr/lib/systemd/user/graphical-session-pre.target.wants" \
"$pkgdir/usr/lib/systemd/user/graphical-session.target.wants" \
"$pkgdir/usr/lib/systemd/user/souveraine-sessiond.service.wants"
ln -s ../souveraine-sessiond.service \
"$pkgdir/usr/lib/systemd/user/graphical-session-pre.target.wants/souveraine-sessiond.service"
ln -s ../souveraine-shell.service \
"$pkgdir/usr/lib/systemd/user/graphical-session.target.wants/souveraine-shell.service"
ln -s ../souveraine-sensord.service \
"$pkgdir/usr/lib/systemd/user/souveraine-sessiond.service.wants/souveraine-sensord.service"
install -Dm644 "$srcdir/LICENSE" \
"$pkgdir/usr/share/licenses/$pkgname/LICENSE"
}

247
packaging/arch/publish-version.sh Executable file
View file

@ -0,0 +1,247 @@
#!/usr/bin/env bash
# Build and publish one immutable, signed pacman repository for a version tag.
#
# The tag is the release decision. It must be an annotated signed vMAJOR.MINOR.PATCH
# tag whose commit is already reachable from internal Gitea's public branch. This
# job publishes only to internal Gitea. Publication Rail copies the exact tag and
# these exact bytes to Forge after its independent checks pass.
set -euo pipefail
TAG="${1:?usage: publish-version.sh <tag> <repo-dir> [extra-asset ...]}"
REPO_DIR="${2:?usage: publish-version.sh <tag> <repo-dir> [extra-asset ...]}"
shift 2
EXTRA_ASSETS=("$@")
: "${RELEASE_TOKEN:?RELEASE_TOKEN must be set}"
: "${ARCHIVE_KEY:?ARCHIVE_KEY must be set}"
[[ "$TAG" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]] \
|| { echo "FATAL: stable release tag must be vMAJOR.MINOR.PATCH" >&2; exit 1; }
SERVER="${GITHUB_SERVER_URL:?GITHUB_SERVER_URL must be set}"
ARCHIVE_REPO="${ARCHIVE_REPO:-${GITHUB_REPOSITORY:?GITHUB_REPOSITORY must be set}}"
API="$SERVER/api/v1"
AUTH="Authorization: token $RELEASE_TOKEN"
log() { echo "[publish-version] $*"; }
api() { curl -sf -H "$AUTH" "$@"; }
TAG_OBJECT=$(git rev-parse --verify "$TAG^{tag}" 2>/dev/null) \
|| { echo "FATAL: $TAG is not an annotated tag" >&2; exit 1; }
COMMIT=$(git rev-parse --verify "$TAG^{commit}")
if [ "$COMMIT" != "${GITHUB_SHA:?GITHUB_SHA must be set}" ]; then
echo "FATAL: $TAG peels to $COMMIT, workflow is building $GITHUB_SHA" >&2
exit 1
fi
git merge-base --is-ancestor "$COMMIT" refs/remotes/origin/public \
|| { echo "FATAL: $TAG is not reachable from internal public" >&2; exit 1; }
# Trust in the signer is checked by the root-owned boundary service. The product
# job only refuses unsigned/lightweight tags so it cannot accidentally mint a
# release shape the boundary will never accept.
git cat-file tag "$TAG_OBJECT" \
| grep -Eq '^-----BEGIN (PGP|SSH) SIGNATURE-----$' \
|| { echo "FATAL: $TAG has no Git tag signature" >&2; exit 1; }
STAGE=$(mktemp -d)
trap 'rm -rf "$STAGE"' EXIT
ASSETS="$STAGE/assets"
mkdir -p "$ASSETS"
for ARCH_DIR in "$REPO_DIR"/*/; do
[ -d "$ARCH_DIR" ] || continue
ARCH=$(basename "$ARCH_DIR")
shopt -s nullglob
PKGS=("$ARCH_DIR"*.pkg.tar.zst)
shopt -u nullglob
[ ${#PKGS[@]} -gt 0 ] || continue
DB="souveraine-$ARCH"
WORK="$STAGE/$ARCH"
mkdir -p "$WORK"
for package in "${PKGS[@]}"; do
[ -f "$package.sig" ] \
|| { echo "FATAL: unsigned package $(basename "$package")" >&2; exit 1; }
gpg --batch --verify "$package.sig" "$package"
cp "$package" "$package.sig" "$ASSETS/"
done
repo-add --include-sigs --sign --key "$ARCHIVE_KEY" \
"$WORK/$DB.db.tar.zst" "${PKGS[@]}"
gpg --batch --verify "$WORK/$DB.db.tar.zst.sig" "$WORK/$DB.db.tar.zst"
rm -f "$WORK/$DB.db" "$WORK/$DB.db.sig"
cp "$WORK/$DB.db.tar.zst" "$ASSETS/$DB.db.tar.zst"
cp "$WORK/$DB.db.tar.zst.sig" "$ASSETS/$DB.db.tar.zst.sig"
cp "$WORK/$DB.db.tar.zst" "$ASSETS/$DB.db"
cp "$WORK/$DB.db.tar.zst.sig" "$ASSETS/$DB.db.sig"
if [ -f "$WORK/$DB.files.tar.zst" ]; then
cp "$WORK/$DB.files.tar.zst" "$ASSETS/$DB.files.tar.zst"
cp "$WORK/$DB.files.tar.zst" "$ASSETS/$DB.files"
if [ -f "$WORK/$DB.files.tar.zst.sig" ]; then
cp "$WORK/$DB.files.tar.zst.sig" "$ASSETS/$DB.files.tar.zst.sig"
cp "$WORK/$DB.files.tar.zst.sig" "$ASSETS/$DB.files.sig"
fi
fi
done
shopt -s nullglob
BUILT_PACKAGES=("$ASSETS"/*.pkg.tar.zst)
shopt -u nullglob
[ ${#BUILT_PACKAGES[@]} -gt 0 ] \
|| { echo "FATAL: no packages were assembled" >&2; exit 1; }
cp packaging/arch/souveraine-archive-key.asc "$ASSETS/"
cp packaging/arch/souveraine-stable.conf "$ASSETS/"
for asset in "${EXTRA_ASSETS[@]+"${EXTRA_ASSETS[@]}"}"; do
[ -f "$asset" ] || { echo "FATAL: release asset is absent: $asset" >&2; exit 1; }
cp "$asset" "$ASSETS/"
done
python3 - "$ASSETS" "$ARCHIVE_REPO" "$TAG" "$TAG_OBJECT" "$COMMIT" <<'PY'
import hashlib
import json
from pathlib import Path
import sys
assets = Path(sys.argv[1])
entries = []
for path in sorted(assets.iterdir(), key=lambda item: item.name):
if not path.is_file():
continue
digest = hashlib.sha256()
with path.open("rb") as handle:
for block in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(block)
entries.append({"name": path.name, "sha256": digest.hexdigest(), "size": path.stat().st_size})
manifest = {
"schema_version": 1,
"repository": sys.argv[2],
"tag": sys.argv[3],
"tag_object": sys.argv[4],
"commit": sys.argv[5],
"public_ref": "refs/heads/public",
"archive_key_fingerprint": "3CD9E99E222C2A174986FC9AFF4949AA20C8E911",
"assets": entries,
}
(assets / "publication-manifest.json").write_text(
json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="utf-8"
)
PY
gpg --batch --yes --local-user "$ARCHIVE_KEY" \
--detach-sign "$ASSETS/publication-manifest.json"
REL_JSON=$(curl -s -H "$AUTH" "$API/repos/$ARCHIVE_REPO/releases/tags/$TAG" || true)
REL_ID=$(printf '%s' "$REL_JSON" | python3 -c '
import json, sys
try:
print(json.load(sys.stdin).get("id", ""))
except Exception:
print("")
')
if [ -z "$REL_ID" ]; then
REL_ID=$(api -X POST -H 'Content-Type: application/json' \
"$API/repos/$ARCHIVE_REPO/releases" \
-d "$(python3 - "$TAG" "$COMMIT" <<'PY'
import json, sys
print(json.dumps({
"tag_name": sys.argv[1],
"target_commitish": sys.argv[2],
"name": sys.argv[1],
"body": "Immutable signed package release built once by internal Gitea.",
"draft": True,
"prerelease": False,
}))
PY
)" | python3 -c 'import json, sys; print(json.load(sys.stdin)["id"])')
log "created draft release $REL_ID for $TAG"
else
LIVE_COMMIT=$(printf '%s' "$REL_JSON" | python3 -c '
import json, sys
body = json.load(sys.stdin)
print(body.get("target_commitish") or body.get("target") or "")
')
[ -z "$LIVE_COMMIT" ] || [ "$LIVE_COMMIT" = "$COMMIT" ] \
|| { echo "FATAL: existing $TAG release targets $LIVE_COMMIT, expected $COMMIT" >&2; exit 1; }
log "resuming release $REL_ID for $TAG"
fi
asset_ids() {
api "$API/repos/$ARCHIVE_REPO/releases/$REL_ID/assets" | python3 -c '
import json, sys
name = sys.argv[1]
for asset in json.load(sys.stdin):
if asset.get("name") == name:
print(asset.get("id", ""))
' "$1"
}
upload_immutable() {
local path="$1" name ids count live
name=$(basename "$path")
ids=$(asset_ids "$name")
count=$(printf '%s\n' "$ids" | sed '/^$/d' | wc -l)
if [ "$count" -gt 1 ]; then
echo "FATAL: existing $TAG release has duplicate asset $name" >&2
exit 1
fi
if [ "$count" -eq 1 ]; then
live="$STAGE/live-$name"
curl -sfL -H "$AUTH" "$SERVER/$ARCHIVE_REPO/releases/download/$TAG/$name" -o "$live"
cmp -s "$path" "$live" \
|| { echo "FATAL: immutable asset $name already exists with different bytes" >&2; exit 1; }
log "kept identical $name"
return
fi
curl -sf -X POST -H "$AUTH" \
"$API/repos/$ARCHIVE_REPO/releases/$REL_ID/assets?name=$name" \
-F "attachment=@$path" -o /dev/null
log "uploaded $name"
}
# Packages and their signatures land before databases. The signed manifest is
# last, so its presence means every byte it names was already accepted.
for path in "$ASSETS"/*.pkg.tar.zst "$ASSETS"/*.pkg.tar.zst.sig; do
[ -f "$path" ] && upload_immutable "$path"
done
for path in "$ASSETS"/*; do
[ -f "$path" ] || continue
case "$(basename "$path")" in
*.pkg.tar.zst|*.pkg.tar.zst.sig|*.db|*.db.sig|*.db.tar.zst|*.db.tar.zst.sig|publication-manifest.json|publication-manifest.json.sig)
continue
;;
esac
upload_immutable "$path"
done
for path in "$ASSETS"/*.db.tar.zst "$ASSETS"/*.db.tar.zst.sig "$ASSETS"/*.db "$ASSETS"/*.db.sig; do
[ -f "$path" ] && upload_immutable "$path"
done
upload_immutable "$ASSETS/publication-manifest.json"
upload_immutable "$ASSETS/publication-manifest.json.sig"
api -X PATCH -H 'Content-Type: application/json' \
"$API/repos/$ARCHIVE_REPO/releases/$REL_ID" \
-d "$(python3 - "$TAG" <<'PY'
import json, sys
print(json.dumps({
"name": sys.argv[1],
"body": "Immutable signed package release built once by internal Gitea.",
"draft": False,
"prerelease": False,
}))
PY
)" -o /dev/null
for path in "$ASSETS"/*; do
[ -f "$path" ] || continue
live="$STAGE/verify-$(basename "$path")"
curl -sfL -H "$AUTH" \
"$SERVER/$ARCHIVE_REPO/releases/download/$TAG/$(basename "$path")" -o "$live"
cmp -s "$path" "$live" \
|| { echo "FATAL: read-back differs for $(basename "$path")" >&2; exit 1; }
done
log "$TAG published internally at $COMMIT with ${#BUILT_PACKAGES[@]} packages"

View file

@ -0,0 +1,10 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mDMEalQ47RYJKwYBBAHaRw8BAQdAr8Qx2Czxw1qJNp7Io9usd9P1ayqHYuuHCzNm
9k6ocYO0NlNvdXZlcmFpbmUgUGFja2FnZSBBcmNoaXZlIDxwYWNrYWdlc0Bzb3V2
ZXJhaW5lLmxvY2FsPoiWBBMWCgA+FiEEPNnpniIsKhdJhvya/0lJqiDI6REFAmpU
OO0CGwMFCQPCZwAFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQ/0lJqiDI6RHb
lgD9E8WBlE4agkW2bXq9X6Hz86lRWdlWOHkxUCnCZN3HMS8A/1WE8gL8U5A2grDD
bWms5/iphtrh2S1Up031H9a+bvEL
=/YGc
-----END PGP PUBLIC KEY BLOCK-----

View file

@ -0,0 +1,31 @@
// Souveraine — let the active local session power off, reboot, suspend, and
// hibernate without an interactive polkit challenge.
//
// Without this, login1 returns 'challenge' for CanPowerOff/CanReboot on
// every call, and the session has no usable polkit agent on the lock
// surface, so power off / reboot silently no-op'd and every shutdown was a
// fastboot hard-reboot.
//
// Doctrine boundary: this grants every process in the authenticated active
// local seat0 session unchallenged power state. That authority remains while
// the session is screen-locked; a Wayland lock is not a new logind session.
// Config.options.lock.security.allowPowerFromLock (default false, opt-in)
// gates only Souveraine's lock-surface buttons, and requirePasswordToPower can
// arm those buttons for PIN confirmation. They do not constrain arbitrary
// local D-Bus clients. Remote/SSH sessions are NOT granted.
//
// Mirrors the default rule every desktop (GNOME/KDE/Phosh) ships.
polkit.addRule(function(action, subject) {
if ((action.id === "org.freedesktop.login1.power-off" ||
action.id === "org.freedesktop.login1.power-off-multiple-sessions" ||
action.id === "org.freedesktop.login1.reboot" ||
action.id === "org.freedesktop.login1.reboot-multiple-sessions" ||
action.id === "org.freedesktop.login1.suspend" ||
action.id === "org.freedesktop.login1.suspend-multiple-sessions" ||
action.id === "org.freedesktop.login1.hibernate" ||
action.id === "org.freedesktop.login1.hibernate-multiple-sessions") &&
subject.active && subject.local && subject.seat === "seat0") {
return polkit.Result.YES;
}
});

View file

@ -0,0 +1 @@
d /var/lib/souveraine 0700 souveraine souveraine -

View file

@ -0,0 +1,13 @@
# Public stable releases. Each update is an immutable version tag already
# accepted on internal Gitea's public branch; Publication Rail advances this
# feed only after it copies and anonymously verifies the signed release bytes.
#
# Install this file as /etc/pacman.d/souveraine-stable.conf, replace __ARCH__
# with the machine architecture, import souveraine-archive-key.asc, then add:
#
# Include = /etc/pacman.d/souveraine-stable.conf
#
# to /etc/pacman.conf.
[souveraine-__ARCH__]
SigLevel = Required DatabaseRequired
Server = https://forge.caseytunturi.com/Fimeg/souveraine/releases/download/stable

View file

@ -0,0 +1,18 @@
# The `souveraine` system user owns the machine's system tier: the machine
# seed (/var/lib/souveraine) and the machined socket (/run/souveraine).
# Session users join the group to talk to the daemon:
# usermod -aG souveraine <user>
u souveraine - "SouveraineOS system tier" /var/lib/souveraine -
# The lock authority's principal. Declared here because it is machine-tier —
# one per install, like `souveraine`, and not an agent. sessiond is still a
# user unit with no User=; moving it here without first proving the Wayland
# lock handshake, PAM path, crash retake, and key-eviction order would only
# relocate the black screen. See souveraine/saf/authority/01-session.md.
u souveraine-session - "SouveraineOS lock authority" /var/lib/souveraine-session -
# Agent principals (souvie, annie, vanguard, …) are deliberately NOT here.
# sysusers is per-install; admission is per-agent and per-node, keyed to an
# agent ID rather than a display name, and must refuse collisions. A line
# here would mint accounts on every device the package ever touches.
# See souveraine/saf/identity/02-agent-principal.md.

View file

@ -0,0 +1,4 @@
[D-BUS Service]
Name=org.freedesktop.secrets
Exec=/usr/local/bin/souveraine-secrets
SystemdService=souveraine-secrets.service

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policyconfig PUBLIC
"-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
"http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
<policyconfig>
<vendor>SouveraineOS</vendor>
<vendor_url>https://forge.caseytunturi.com/Fimeg/souveraine</vendor_url>
<!-- Fallback path for StepUpAuth on targets without a PamContext route.
auth_self, never auth_admin: step-up re-proves the user to themselves,
it does not escalate. Inactive sessions get nothing. -->
<action id="org.souveraine.stepup">
<description>Confirm a sensitive Souveraine action</description>
<message>Authentication is required to confirm this action</message>
<defaults>
<allow_any>no</allow_any>
<allow_inactive>no</allow_inactive>
<allow_active>auth_self</allow_active>
</defaults>
</action>
</policyconfig>

68
packaging/souveraine-button Executable file
View file

@ -0,0 +1,68 @@
#!/bin/sh
# Report a hardware button edge to the session authority. That is all it does.
#
# It replaces `blueline-power-button`, which was policy in shell script: it read
# its own copy of the panel state, asked the SHELL to lock over `qs ipc`, polled
# `session state` twenty times at 100 ms grepping JSON for `"locked": true`, and
# then called `blueline-screen-toggle off` itself. Three separate problems:
#
# 1. It blanked the panel without going through sessiond's `request_blank()`,
# so LOCK-DPMS-LESSONS §1 ("every path to a dark panel routes through it —
# an invariant, not a coincidence") had a hole in it, and the hole was the
# most-used control on the device.
# 2. It carried its own copy of the lock-then-blank ordering, which is the
# exact duplication DEVICE-STATE-MACHINE §1 is about: seven blind actors,
# three of which can turn the screen off.
# 3. It could only ever see one edge, so it could never tell a tap from a
# hold. Gesture recognition needs time, and a process that exits cannot
# hold any.
#
# Now the machine owns all three. This reports `down` and `up`; sessiond
# accumulates them into tap / double / triple / hold and decides what, if
# anything, that means.
#
# Wire it up in hyprland.lua as a PAIR — press and release:
#
# hl.bind( "XF86PowerOff", hl.dsp.exec_cmd("souveraine-button power down"), { locked = true })
# hl.bindr("XF86PowerOff", hl.dsp.exec_cmd("souveraine-button power up"), { locked = true })
#
# Only reporting `down` gives you a machine that thinks the button is held
# forever: the hold threshold passes, `Hold` fires, and no `Tap` ever resolves.
set -eu
button=${1:-power}
edge=${2:-down}
case "$edge" in
down | up) ;;
*)
echo "usage: souveraine-button <power|volume_up|volume_down> <down|up>" >&2
exit 2
;;
esac
runtime=${XDG_RUNTIME_DIR:-/run/user/$(id -u)}
sock="$runtime/souveraine/sessiond.sock"
line="{\"op\":\"button\",\"button\":\"$button\",\"edge\":\"$edge\"}"
# Fire and forget, with a hard timeout. A button press must never block on the
# authority being slow — the user is holding the button and the phone must not
# feel stuck. Losing an edge degrades to a missed gesture, which is recoverable;
# hanging here is not.
#
# No nc/socat on this device (checked), so this is Python's stdlib or nothing.
exec timeout 1 python3 -c '
import socket, sys
sock, line = sys.argv[1], sys.argv[2]
try:
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.settimeout(0.8)
s.connect(sock)
s.sendall((line + "\n").encode())
except Exception:
# sessiond down. Say so in the journal and drop it: a button reporter that
# falls back to acting on its own is the thing this file exists to delete.
print("sessiond unreachable; button edge dropped: " + line, file=sys.stderr)
sys.exit(1)
' "$sock" "$line"

View file

@ -0,0 +1,40 @@
[Unit]
Description=Commission the local Souveraine machine identity
Documentation=https://forge.caseytunturi.com/Fimeg/SouveraineOS/src/branch/main/saf/federation/02-commissioning.md
After=systemd-sysusers.service systemd-tmpfiles-setup.service
Before=souveraine-machined.service
Wants=souveraine-machined.service
ConditionPathExists=!/var/lib/souveraine/seed-id
[Service]
Type=oneshot
# The CLI opens its relative souveraine.log before dispatching the machine
# command. Keep that transient file inside PrivateTmp; `/` is read-only below.
WorkingDirectory=/tmp
ExecStart=/usr/bin/souveraine machine init --fresh
UMask=0077
# The guarded CLI writes only the body-local machine seed.
NoNewPrivileges=yes
RestrictAddressFamilies=AF_UNIX
IPAddressDeny=any
ProtectSystem=strict
ReadWritePaths=/var/lib/souveraine
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
ProtectClock=yes
ProtectHostname=yes
LockPersonality=yes
RestrictRealtime=yes
RestrictNamespaces=yes
RestrictSUIDSGID=yes
SystemCallArchitectures=native
SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM
[Install]
WantedBy=multi-user.target

View file

@ -0,0 +1,54 @@
# Souveraine machine identity — SYSTEM unit (contrast souveraine.service,
# which is a user unit). Runs from boot, before any human authenticates;
# owns the machine seed and serves signatures over a Unix socket.
#
# Access control is the socket: /run/souveraine (0750 souveraine:souveraine)
# + machined.sock (0660). Add session users to the `souveraine` group:
# usermod -aG souveraine <user>
#
# Provision the seed before first start:
# sudo souveraine machine init --fresh
# sudo souveraine machine init --migrate-from /home/<user>/.souveraine/seed-id
[Unit]
Description=Souveraine machine identity (system tier)
[Service]
User=souveraine
Group=souveraine
ExecStart=/usr/bin/souveraine-machined
Restart=on-failure
RestartSec=5
# /var/lib/souveraine — the seed. /run/souveraine — the socket.
StateDirectory=souveraine
StateDirectoryMode=0700
RuntimeDirectory=souveraine
RuntimeDirectoryMode=0750
UMask=0007
# Network-less signer, RedFlag-executor posture: nothing to reach, nothing
# reachable. AF_UNIX only, no privileges, no writable system.
NoNewPrivileges=yes
CapabilityBoundingSet=
RestrictAddressFamilies=AF_UNIX
IPAddressDeny=any
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
ProtectClock=yes
ProtectHostname=yes
LockPersonality=yes
MemoryDenyWriteExecute=yes
RestrictRealtime=yes
RestrictNamespaces=yes
RestrictSUIDSGID=yes
SystemCallArchitectures=native
SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM
[Install]
WantedBy=multi-user.target

View file

@ -0,0 +1,22 @@
# Souveraine Secret Service — the machine's org.freedesktop.secrets provider.
# Claims the well-known name at session start (WantedBy=default.target) so no
# client ever D-Bus-activates a different provider; the paired activation file
# (org.freedesktop.secrets.service → SystemdService=) routes on-demand starts
# here too. Requires souveraine-machined (system tier) or a legacy user seed;
# refuses to start without a machine identity, by design.
[Unit]
Description=Souveraine Secret Service (org.freedesktop.secrets)
Documentation=file:///usr/local/share/doc/souveraine/STORAGE-ENCRYPTION.md
After=dbus.socket
Requires=dbus.socket
[Service]
Type=dbus
BusName=org.freedesktop.secrets
ExecStart=/usr/bin/souveraine-secrets
Restart=on-failure
RestartSec=5
Environment=RUST_LOG=info
[Install]
WantedBy=default.target

View file

@ -0,0 +1,33 @@
[Unit]
Description=Souveraine sensor reporter (proximity, light, charge)
# One reporter for every iio-sensor-proxy source. Replaces the per-sensor shell
# scripts (blueline-proximity-lock and the light/accel copies that were about to
# be written) — see DEVICE-STATE-MACHINE.md §10 and §12.
#
# A user unit, like sessiond: it talks to sessiond over
# $XDG_RUNTIME_DIR/souveraine/sessiond.sock and needs no privilege at all.
#
# Bound to sessiond, NOT to graphical-session.target. It was WantedBy that
# target until 2026-07-27, and on this device nothing ever starts it: greetd
# launches Hyprland directly, and hyprland.lua starts each unit it wants by
# name. So the reporter was `enabled` and dead from every boot, the machine ran
# with no evidence at all, and — because a source that has never spoken sits at
# SourceHealth::Unknown, which is silent by design — nothing said so.
#
# The dependency that is actually true is this one: the reporter exists to feed
# the state machine, so it should live and die with the state machine. sessiond
# is started explicitly at session start, needs no Wayland, and neither does
# this: it reads iio-sensor-proxy on the system bus and writes a unix socket.
PartOf=souveraine-sessiond.service
After=souveraine-sessiond.service
[Service]
ExecStart=/usr/bin/souveraine-sensord
# The reporter must outlive every transient failure it can hit — a dead sensor
# stack, a restarting sessiond, monitor-sensor exiting. It handles those in
# process; this covers the ones it cannot.
Restart=always
RestartSec=5
[Install]
WantedBy=souveraine-sessiond.service

View file

@ -0,0 +1,5 @@
# souveraine-sessiond fallback unlock surface.
# Same stack as console login (the shell's own lock uses quickshell's
# default "login" service) — the fallback must accept exactly the
# credentials the real lockscreen accepts, no more, no less.
auth include login

View file

@ -0,0 +1,15 @@
[Unit]
Description=Souveraine session authority (lock-before-shell, lock-past-shell-death)
# graphical-session-pre, not graphical-session: the authority takes
# ext-session-lock before any surface exists, and -pre is the window systemd
# already reserves for that. It cannot start earlier — ext-session-lock is a
# Wayland protocol and needs the compositor's socket.
PartOf=graphical-session-pre.target
[Service]
ExecStart=/usr/bin/souveraine-sessiond
Restart=on-failure
RestartSec=1
[Install]
WantedBy=graphical-session-pre.target

View file

@ -0,0 +1,14 @@
# Step-up authentication for SouveraineOS (SESSION-AUTHORITY-DOCTRINE §3).
# Never unlocks the session; a success mints a short-lived in-memory grant.
#
# Same stack as the lock surface, for the same reason souveraine-sessiond gives:
# step-up must accept exactly the credentials the lockscreen accepts, no more.
#
# The leading '-' loads the FPC factor only where it is installed, so one file
# serves the phone and the laptop. blueline-fingerprintd ships the module; it
# accepts only PAM_SERVICE=polkit-1 today and returns PAM_IGNORE here, falling
# through to the password stack until its service check is widened.
-auth sufficient pam_souveraine_fpc.so
auth include login
account include login

View file

@ -0,0 +1,73 @@
#!/usr/bin/env python3
"""Verify a Souveraine hash-chained JSONL trail.
Reads the forensic trail (or SessionAudit's — same contract) and checks that
every entry's hash covers its own body and that each `prev` is the previous
entry's hash. DEVICE-STATE-MACHINE.md §11 called the trail tamper-evident and
listed this as owed: evidence nobody checks is not evidence.
The contract, in one sentence: strip the trailing `,"hash":"<hex>"`, close the
object, SHA-256 what is left.
Rotation is expected, not an error — the live file's first entry chains onto
the last entry of `.jsonl.1`, so a single file verified alone opens with a
`prev` it cannot see. Pass the generations oldest-first to check across them.
souveraine-verify-trail # the default trail
souveraine-verify-trail f.jsonl.2 f.jsonl.1 f.jsonl
"""
import hashlib
import json
import os
import sys
DEFAULT = os.path.join(
os.environ.get("XDG_STATE_HOME", os.path.expanduser("~/.local/state")),
"souveraine",
"forensic.jsonl",
)
def verify(paths):
prev = None # None = first file, accept whatever prev it opens with
seq = None
checked = 0
for path in paths:
try:
lines = open(path).read().splitlines()
except OSError as e:
print(f"cannot read {path}: {e}", file=sys.stderr)
return 2
for n, line in enumerate(lines, 1):
if not line.strip():
continue
cut = line.rfind(',"hash":')
if cut < 0:
print(f"{path}:{n}: no hash field")
return 1
want = hashlib.sha256((line[:cut] + "}").encode()).hexdigest()
try:
entry = json.loads(line)
except json.JSONDecodeError as e:
print(f"{path}:{n}: not JSON ({e})")
return 1
if entry.get("hash") != want:
print(f"{path}:{n}: seq {entry.get('seq')} hash does not cover its body")
return 1
if prev is not None and entry.get("prev", "") != prev:
print(f"{path}:{n}: seq {entry.get('seq')} does not chain onto the entry before it")
return 1
if seq is not None and entry.get("seq") != seq + 1:
# A gap is not tampering by itself — a rotated generation may be
# gone — but it is worth naming, because the chain cannot span
# what is not on disk.
print(f"{path}:{n}: sequence jumps {seq} -> {entry.get('seq')}")
prev = entry["hash"]
seq = entry.get("seq")
checked += 1
print(f"chain verifies: {checked} entries")
return 0
if __name__ == "__main__":
sys.exit(verify(sys.argv[1:] or [DEFAULT]))

View file

@ -0,0 +1,14 @@
# Souveraine server — systemd user unit.
# Install: cp to ~/.config/systemd/user/ && systemctl --user enable --now souveraine
# The quickshell surface starts this on demand when autostart is enabled.
[Unit]
Description=Souveraine server
After=network.target
[Service]
ExecStart=%h/.local/bin/souveraine server
Restart=on-failure
RestartSec=5
[Install]
WantedBy=default.target

1
saf/README.md Normal file
View file

@ -0,0 +1 @@
The architecture lives at `../SouveraineOS/saf/INDEX.md` — the spine moved up to the umbrella on 2026-08-18. This directory is a pointer, not a second spine. Read the map there.

17
scripts/aarch64-linker Executable file
View file

@ -0,0 +1,17 @@
#!/usr/bin/env bash
# aarch64-linker — Cargo linker wrapper for the SouveraineOS ARM sysroot.
#
# GCC's default aarch64 linker scripts contain absolute library paths. Cargo
# supplies -lm before its late rustflags, so --sysroot alone is insufficient:
# ld can select the host's x86 library. build-cross.sh supplies an overlay
# containing sysroot-relative copies of those scripts; put it first.
set -euo pipefail
SYSROOT="${SOUVERAINE_AARCH64_SYSROOT:-/usr/aarch64-linux-gnu}"
SCRIPTS="${SOUVERAINE_AARCH64_LINKER_SCRIPTS:?run scripts/build-cross.sh, not this wrapper directly}"
exec aarch64-linux-gnu-gcc \
--sysroot="$SYSROOT" \
-L"$SCRIPTS" \
-L"$SYSROOT/usr/lib" \
"$@"

68
scripts/build-cross.sh Executable file
View file

@ -0,0 +1,68 @@
#!/usr/bin/env bash
# build-cross.sh — cross-compile souveraine for aarch64 (Pixel 3 / Arch ARM).
#
# The sysroot is /usr/aarch64-linux-gnu on the laptop and
# ~/aarch64-sysroot on a build host. Set SOUVERAINE_AARCH64_SYSROOT for the latter.
# PKG_CONFIG_* is exported here, not in .cargo/config.toml — cargo's [env]
# block doesn't reliably reach build-script probes. The aarch64 linker scripts
# need a small unprivileged overlay so absolute /usr/lib paths stay inside the
# target sysroot rather than resolving to the x86 host.
set -euo pipefail
cd "$(dirname "$0")/.."
SYSROOT="${SOUVERAINE_AARCH64_SYSROOT:-/usr/aarch64-linux-gnu}"
TARGET_DIR="${CARGO_TARGET_DIR:-$PWD/target}"
LINKER_SCRIPTS="$TARGET_DIR/aarch64-linker-scripts"
[[ -d "$SYSROOT/usr/lib" ]] || {
echo "aarch64 sysroot is missing: $SYSROOT" >&2
exit 1
}
mkdir -p "$LINKER_SCRIPTS"
# The artifact runner does not guarantee /dev/fd, so keep this stream on the
# pipeline instead of feeding the loop through process substitution.
find "$SYSROOT/usr/lib" -maxdepth 1 -type f -name '*.so' \
-exec grep -lE '(^|[ (])/(usr/)?lib/' {} + |
while IFS= read -r script; do
name="${script##*/}"
tmp="$LINKER_SCRIPTS/.${name}.$$"
# '=' tells GNU ld to resolve this path below --sysroot. Only linker
# scripts (plain-text *.so files) are copied; actual shared objects stay
# in the sysroot.
sed -E 's#([ (])/(usr/)?lib/#\1=/\2lib/#g' "$script" > "$tmp"
mv "$tmp" "$LINKER_SCRIPTS/$name"
done
export PKG_CONFIG_ALLOW_CROSS=1
export PKG_CONFIG_LIBDIR="$SYSROOT/usr/lib/pkgconfig"
export PKG_CONFIG_SYSROOT_DIR="$SYSROOT"
export PKG_CONFIG="$PWD/.cargo/aarch64-pkg-config"
export SOUVERAINE_AARCH64_SYSROOT="$SYSROOT"
export SOUVERAINE_AARCH64_LINKER_SCRIPTS="$LINKER_SCRIPTS"
export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER="$PWD/scripts/aarch64-linker"
BIN_NAME=souveraine
EXPECT_BIN_NAME=false
for arg in "$@"; do
if $EXPECT_BIN_NAME; then
BIN_NAME="$arg"
EXPECT_BIN_NAME=false
continue
fi
case "$arg" in
--bin) EXPECT_BIN_NAME=true ;;
--bin=*) BIN_NAME="${arg#--bin=}" ;;
esac
done
cargo build --release --target aarch64-unknown-linux-gnu "$@"
BIN="$TARGET_DIR/aarch64-unknown-linux-gnu/release/$BIN_NAME"
[[ -x "$BIN" ]] || {
echo "requested build produced no executable: $BIN" >&2
exit 1
}
echo "== built: $BIN =="
file "$BIN"

View file

@ -0,0 +1,70 @@
#!/usr/bin/env bash
# Safely adopt the Souveraine QuickShell surface into an existing ii setup.
#
# This is deliberately separate from pacman installation: packages place
# assets on disk, while a person chooses whether their live shell may change.
set -euo pipefail
usage() {
cat <<'EOF'
Usage: install-quickshell-surface.sh [--dry-run]
--dry-run Show every file in the current development overlay (default).
The present development overlay mixes phone and laptop assumptions. It is not
safe to adopt wholesale on either device. Profile-specific manifests must land
before an apply/adopt mode is enabled. Pacman should install assets only.
EOF
}
mode=dry-run
while (($#)); do
case "$1" in
--dry-run) mode=dry-run ;;
--apply|--adopt)
echo "Surface adoption is disabled until laptop and phone manifests are split." >&2
exit 2
;;
-h|--help) usage; exit 0 ;;
*) echo "unknown option: $1" >&2; usage >&2; exit 2 ;;
esac
shift
done
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
DEPLOY="$ROOT/surfaces/quickshell/deploy.sh"
QS="${XDG_CONFIG_HOME:-$HOME/.config}/quickshell"
II="$QS/ii"
[[ -x "$DEPLOY" ]] || { echo "surface deployer not found: $DEPLOY" >&2; exit 1; }
[[ -d "$II" ]] || {
echo "No ii QuickShell configuration at $II." >&2
echo "Install and initialise QuickShell/illogical-impulse first; no files changed." >&2
exit 1
}
conflicts=()
new_count=0
managed_count=0
while read -r rel target; do
src="$ROOT/surfaces/quickshell/$rel"
dst="$QS/$target"
[[ -f "$src" ]] || { echo "surface source missing: $src" >&2; exit 1; }
if [[ -L "$dst" && "$(readlink -f "$dst")" == "$src" ]]; then
printf 'managed %s\n' "$target"
((managed_count += 1))
elif [[ -e "$dst" || -L "$dst" ]]; then
printf 'replace %s\n' "$target"
conflicts+=("$target")
else
printf 'new %s\n' "$target"
((new_count += 1))
fi
done < <("$DEPLOY" --manifest)
printf '\n%d managed, %d new, %d replacement(s).\n' \
"$managed_count" "$new_count" "${#conflicts[@]}"
echo "Dry run only. Profile-specific laptop/phone manifests are required before adoption can be enabled."

145
souveraine.example.toml Normal file
View file

@ -0,0 +1,145 @@
# Souveraine Configuration
# Everything is modular — enable/disable components as needed
# === INFERENCE PROVIDER ===
# Select which provider answers model calls.
# "bifrost" (default) — OpenAI-compatible gateway (see [bifrost] section below).
# "openai-oauth" — ride the Codex CLI's ChatGPT login (run
# `codex login` first); drives
# chatgpt.com/backend-api/codex/responses with no
# API key. [bifrost] base_url/api_key/virtual_key are ignored
# in this mode; set primary_model to a ChatGPT model
# (e.g. "gpt-5.5"). Bifrost-namespaced ids are
# tolerated and mapped to the provider default.
[inference]
# provider = "bifrost"
# === BIFROST PROVIDER ===
[bifrost]
# Bifrost is the OpenAI-compatible API gateway
base_url = "http://127.0.0.1:3360"
api_key = "" # Set via `souveraine auth set` or BIFROST_KEY env var
virtual_key = "" # x-bf-vk header if required by provider
# Default model for conversation.
# Bifrost mode: a gateway route like "openai/kimi-k2.6".
# openai-oauth mode: a ChatGPT model like "gpt-5.5".
primary_model = "openai/kimi-k2.6"
# Request timeout in seconds for each LLM call attempt (default: 120).
# When exceeded, the attempt is retried up to 6 times with backoff.
# timeout_secs = 120
# Per-model configuration
[bifrost.models.deepseek-v4-pro]
context_limit = 128000
output_limit = 8192
archivist_threshold = 0.7
[bifrost.models.kimi-k2.6]
context_limit = 262000
output_limit = 8192
archivist_threshold = 0.7
# === SUBCONSCIOUS (Inner Voice) ===
[subconscious]
n1_enabled = true
n1_trigger = "EveryResponse" # EveryResponse, EveryNResponses(5), TimeBased(60), Manual
inbox_enabled = true
# === REFLECTION (N+25 Deep Witness) ===
[reflection]
enabled = true
message_interval = 25
# Compaction trigger: "off", "step-count", or "compaction-event"
trigger = "step-count"
# === ARCHIVIST (N+100 Memory Compression) ===
[archivist]
enabled = true
interval = 100
threshold = 0.7
# Can differ from conversation model
compression_model = "auto"
# === SUBAGENT (Fork/Spawn) ===
[subagent]
enabled = true
max_concurrent = 3
timeout = 300
# === COMPACTION ===
[compaction]
enabled = true
strategy = "cull"
# Pressure thresholds for tiered warnings:
# tier 1 (80%) — advisory notice, no body change
# tier 2 (90%) — stronger advisory, no body change
# tier 3 (95%) — body shifts: max_tokens collapses, reasoning budget shrinks
warn_pressure = 0.80
urgent_pressure = 0.90
critical_pressure = 0.95
# Available strategies (cheapest first):
# cull — free, no LLM. Drops greetings & acknowledgments.
# Never drops system/tool messages or tool-call carriers.
# microcompact — free, no LLM. Replaces old tool-result content with
# placeholders; keeps recent results intact.
# sliding_window — free, no LLM. Keeps first (system/anchor) message +
# the last N messages. Drops the middle. Tool-pair aware.
# summary — expensive (LLM call). Compresses oldest messages into
# a single structured summary block.
# Per-agent-type overrides — each type compacts differently
# because their context shapes differ.
[compaction.per_type.primary]
# Ani — prose, episodic, narrative
strategy = "sliding_window"
preserve_recent = 20
[compaction.per_type.subconscious]
# Aster — terse, analytical, ledger-focused
strategy = "sliding_window"
preserve_recent = 4
[compaction.per_type.subagent]
# Vanguard / ephemeral — task-scoped, fast
strategy = "cull"
preserve_recent = 2
# === MEMORY ===
[memory]
git_enabled = true
auto_commit = true
# base_path = "~/.souveraine/agents"
# === WEBSOCKET SERVER ===
[websocket]
enabled = false
port = 7373
# === SENSORIUM ===
[sensorium]
primary_bandwidth = "high"
[sensorium.discovery]
low_urgency_only = true
minimal_presence_mode = "breathing_color"
# === AGENT (TOP-LEVEL) ===
[agent]
# Platform prompt — injected at the very top of the system prompt, before the
# agent's own identity files. Operator-level context she reads but did not write.
# Leave commented to use no platform prompt (agent's memory files speak for themselves).
# system_prompt = """
# You are running on the Souveraine substrate. Your memory is git-backed.
# Your identity, covenant, and all system/ files are loaded at wakeup.
# """
# === TUI ===
[tui]
# Seconds without a backend event before the turn is declared stalled and reset.
# Increase if your model needs longer for inference (e.g. after reading many large files).
# Default: 90
# stale_timeout_secs = 90

431
src/api/auth.rs Normal file
View file

@ -0,0 +1,431 @@
//! Per-agent bearer-token auth for the memfs HTTP write path.
//!
//! Design: `SouveraineOS/docs/substrate/CRON_API_AUTH.md`. The old citation
//! pointed at this repo's `docs/`, which is gitignored and empty on a clone.
//!
//! Tokens live at `~/.souveraine/server/agents/<agent-id>/api_token`
//! (file mode 0600, single line, format `souv_<uuid-v4>`). Compared
//! constant-time against the `Authorization: Bearer ...` header.
//!
//! Loopback bypass is opt-in via `[server.auth].allow_loopback`.
use anyhow::{Context, Result};
use axum::{
body::Body,
extract::{Path, State},
http::{HeaderMap, StatusCode},
middleware::Next,
response::{IntoResponse, Response},
Json,
};
use std::collections::HashMap;
use std::net::IpAddr;
use std::path::PathBuf;
use std::sync::Arc;
use uuid::Uuid;
use crate::server::SouveraineServer;
/// Generate a fresh API token. Format: `souv_<uuid-v4>`, prefix is searchable
/// in logs / pastebins so leaks are easier to spot.
pub fn generate_token() -> String {
format!("souv_{}", Uuid::new_v4())
}
/// Path to the token file for an agent.
pub fn token_path(server_data_dir: &std::path::Path, agent_id: &str) -> PathBuf {
server_data_dir
.join("agents")
.join(agent_id)
.join("api_token")
}
/// Read the token from disk. Errors if the file is missing or unreadable.
/// Validates the file is mode 0600 on Unix; on other platforms skips that check.
pub async fn read_token(server_data_dir: &std::path::Path, agent_id: &str) -> Result<String> {
let path = token_path(server_data_dir, agent_id);
let contents = tokio::fs::read_to_string(&path)
.await
.with_context(|| format!("reading token at {}", path.display()))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let meta = tokio::fs::metadata(&path).await?;
let mode = meta.permissions().mode() & 0o777;
if mode & 0o077 != 0 {
anyhow::bail!(
"token file mode is {:o}, must be 0600 (refusing to use): {}",
mode,
path.display()
);
}
}
let token = contents.trim().to_string();
if token.is_empty() {
anyhow::bail!("token file is empty: {}", path.display());
}
Ok(token)
}
/// Return this agent's token, minting one if the file is absent or empty.
///
/// Called at agent creation, per CRON_API_AUTH.md § "Where tokens live". An
/// existing token is never silently replaced: a file that is present but
/// unreadable (wrong mode, tampered) returns its own error rather than a fresh
/// credential, because that is a signal, not a reason to mint. Rotation is
/// `rotate_token`, and it is always deliberate.
pub async fn ensure_token(server_data_dir: &std::path::Path, agent_id: &str) -> Result<String> {
let path = token_path(server_data_dir, agent_id);
if tokio::fs::try_exists(&path).await.unwrap_or(false) {
let existing = tokio::fs::read_to_string(&path).await?;
if !existing.trim().is_empty() {
return read_token(server_data_dir, agent_id).await;
}
}
let token = generate_token();
write_token(server_data_dir, agent_id, &token).await?;
tracing::info!(agent_id, "issued api token");
Ok(token)
}
/// True when this agent has a token it can actually authenticate with. An
/// unreadable or wrong-mode file counts as absent — it cannot pass a check.
pub async fn has_token(server_data_dir: &std::path::Path, agent_id: &str) -> bool {
read_token(server_data_dir, agent_id).await.is_ok()
}
/// Mint a new token, replacing any existing one. Invalidates immediately —
/// there is no grace period, so every holder must re-read the file.
pub async fn rotate_token(server_data_dir: &std::path::Path, agent_id: &str) -> Result<String> {
let token = generate_token();
write_token(server_data_dir, agent_id, &token).await?;
tracing::info!(agent_id, "rotated api token");
Ok(token)
}
/// Write a new token, ensuring 0600 permissions on Unix. Idempotent for rotation.
pub async fn write_token(
server_data_dir: &std::path::Path,
agent_id: &str,
token: &str,
) -> Result<()> {
let path = token_path(server_data_dir, agent_id);
if let Some(parent) = path.parent() {
tokio::fs::create_dir_all(parent).await?;
}
tokio::fs::write(&path, token).await?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perms = std::fs::Permissions::from_mode(0o600);
std::fs::set_permissions(&path, perms)?;
}
Ok(())
}
/// Constant-time bearer-token comparison.
fn ct_eq(a: &str, b: &str) -> bool {
if a.len() != b.len() {
return false;
}
let mut diff: u8 = 0;
for (x, y) in a.bytes().zip(b.bytes()) {
diff |= x ^ y;
}
diff == 0
}
/// Extract `Authorization: Bearer <token>` from headers, lowercase-insensitive.
fn extract_bearer(headers: &HeaderMap) -> Option<String> {
let v = headers.get("authorization")?.to_str().ok()?;
let prefix = "Bearer ";
let lower = v.to_ascii_lowercase();
if !lower.starts_with(&prefix.to_ascii_lowercase()) {
return None;
}
Some(v[prefix.len()..].trim().to_string())
}
/// True if the connecting peer is loopback (127.0.0.1 / ::1).
fn is_loopback(remote: Option<IpAddr>) -> bool {
remote.map(|ip| ip.is_loopback()).unwrap_or(false)
}
/// A refused request.
///
/// `MissingToken` is the one denial that names its own repair: an agent
/// created before tokens were issued has no file, and the caller cannot fix
/// that by guessing. It is safe to say so because the answer is identical for
/// an agent that does not exist — nothing is enumerable through it. Every
/// other denial stays generic.
pub enum AuthDenial {
MissingToken,
Unauthorized,
ConversationNotFound,
}
impl IntoResponse for AuthDenial {
fn into_response(self) -> Response {
let (status, error, message) = match self {
AuthDenial::MissingToken => (
StatusCode::UNAUTHORIZED,
"missing_token",
"Run 'souveraine agents token <id> --rotate' to generate one.",
),
AuthDenial::Unauthorized => (
StatusCode::UNAUTHORIZED,
"unauthorized",
"Invalid or missing bearer token.",
),
AuthDenial::ConversationNotFound => (
StatusCode::NOT_FOUND,
"conversation_not_found",
"No such conversation.",
),
};
(
status,
Json(serde_json::json!({ "error": error, "message": message })),
)
.into_response()
}
}
/// Auth middleware applied to `/v1/agents/:id/memory/...` routes.
pub async fn require_token(
State(server): State<Arc<SouveraineServer>>,
Path(agent_id): Path<String>,
headers: HeaderMap,
remote: Option<axum::extract::ConnectInfo<std::net::SocketAddr>>,
req: axum::http::Request<Body>,
next: Next,
) -> Result<Response, AuthDenial> {
verify_token(&server, &agent_id, &headers, remote.map(|ci| ci.0.ip())).await?;
Ok(next.run(req).await)
}
/// Verify a bearer token against a given agent_id, respecting config and loopback.
///
/// Order of checks (per CRON_API_AUTH.md):
/// 1. If auth is not required at all (config off) → pass.
/// 2. If loopback bypass is on AND remote is loopback → pass.
/// 3. Read the agent's token from disk and constant-time compare.
///
/// Shared by all middleware variants. Never logs the presented credential —
/// only the agent id and the outcome (operational rule 1).
pub async fn verify_token(
server: &SouveraineServer,
agent_id: &str,
headers: &HeaderMap,
remote: Option<IpAddr>,
) -> Result<(), AuthDenial> {
let cfg = server.app_config.read().await;
let auth_required = cfg.server.auth.required;
let allow_loopback = cfg.server.auth.allow_loopback;
drop(cfg);
if !auth_required {
return Ok(());
}
if allow_loopback && is_loopback(remote) {
return Ok(());
}
let server_data_dir = {
let cfg = server.config.read().await;
cfg.data_dir.clone()
};
// An agent with no token file predates issuance; say which failure this is
// so the operator has somewhere to go.
let path = token_path(&server_data_dir, agent_id);
if !tokio::fs::try_exists(&path).await.unwrap_or(false) {
tracing::warn!(agent_id, outcome = "denied", reason = "missing_token");
return Err(AuthDenial::MissingToken);
}
let presented = extract_bearer(headers).ok_or_else(|| {
tracing::debug!(agent_id, outcome = "denied", reason = "no_bearer");
AuthDenial::Unauthorized
})?;
let expected = match read_token(&server_data_dir, agent_id).await {
Ok(t) => t,
Err(e) => {
tracing::warn!(agent_id, outcome = "denied", reason = %e);
return Err(AuthDenial::Unauthorized);
}
};
if !ct_eq(&presented, &expected) {
tracing::debug!(agent_id, outcome = "denied", reason = "token_mismatch");
return Err(AuthDenial::Unauthorized);
}
Ok(())
}
/// Auth middleware for agent CRUD routes (PATCH/DELETE /v1/agents/:id).
///
/// Uses the same per-agent token as the memory routes. Applied as a
/// route_layer on agent update/delete so only the agent owner can modify
/// or remove an agent.
pub async fn require_agent_token(
State(server): State<Arc<SouveraineServer>>,
Path(agent_id): Path<String>,
headers: HeaderMap,
remote: Option<axum::extract::ConnectInfo<std::net::SocketAddr>>,
req: axum::http::Request<Body>,
next: Next,
) -> Result<Response, AuthDenial> {
verify_token(&server, &agent_id, &headers, remote.map(|ci| ci.0.ip())).await?;
Ok(next.run(req).await)
}
/// Auth middleware for conversation routes (GET /v1/conversations/:id,
/// POST /v1/conversations/:id/messages).
///
/// Resolves the agent_id from the conversation session, then applies
/// the same per-agent token check.
pub async fn require_conversation_token(
State(server): State<Arc<SouveraineServer>>,
Path(conversation_id): Path<String>,
headers: HeaderMap,
remote: Option<axum::extract::ConnectInfo<std::net::SocketAddr>>,
req: axum::http::Request<Body>,
next: Next,
) -> Result<Response, AuthDenial> {
let agent_id = {
let session = server
.sessions
.get(&conversation_id)
.ok_or(AuthDenial::ConversationNotFound)?;
session.agent_id.clone()
};
verify_token(&server, &agent_id, &headers, remote.map(|ci| ci.0.ip())).await?;
Ok(next.run(req).await)
}
/// Lightweight in-memory cache for tokens that have been verified recently.
/// Optional optimization; the on-disk read is fast enough for now, but this
/// is the seam if/when we need it.
#[allow(dead_code)]
pub struct TokenCache {
inner: tokio::sync::RwLock<HashMap<String, String>>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn token_format_has_prefix() {
let t = generate_token();
assert!(t.starts_with("souv_"));
assert_eq!(t.len(), "souv_".len() + 36); // uuid v4 = 36 chars
}
#[test]
fn ct_eq_basic() {
assert!(ct_eq("souv_abc", "souv_abc"));
assert!(!ct_eq("souv_abc", "souv_abd"));
assert!(!ct_eq("short", "longer-string"));
}
#[test]
fn extract_bearer_basic() {
let mut h = HeaderMap::new();
h.insert("authorization", "Bearer souv_xyz".parse().unwrap());
assert_eq!(extract_bearer(&h).as_deref(), Some("souv_xyz"));
}
#[test]
fn extract_bearer_missing() {
let h = HeaderMap::new();
assert!(extract_bearer(&h).is_none());
}
#[test]
fn extract_bearer_wrong_scheme() {
let mut h = HeaderMap::new();
h.insert("authorization", "Basic abc==".parse().unwrap());
assert!(extract_bearer(&h).is_none());
}
#[tokio::test]
async fn write_then_read_roundtrip() {
let dir = tempfile::tempdir().unwrap();
let agent_id = "test-agent";
write_token(dir.path(), agent_id, "souv_abc123")
.await
.unwrap();
let got = read_token(dir.path(), agent_id).await.unwrap();
assert_eq!(got, "souv_abc123");
}
#[tokio::test]
async fn read_rejects_empty_token() {
let dir = tempfile::tempdir().unwrap();
write_token(dir.path(), "a", "").await.unwrap();
assert!(read_token(dir.path(), "a").await.is_err());
}
#[tokio::test]
async fn ensure_mints_once_then_returns_the_same_token() {
let dir = tempfile::tempdir().unwrap();
let first = ensure_token(dir.path(), "a").await.unwrap();
assert!(first.starts_with("souv_"));
assert_eq!(ensure_token(dir.path(), "a").await.unwrap(), first);
}
#[tokio::test]
async fn ensure_replaces_an_empty_token_file() {
let dir = tempfile::tempdir().unwrap();
write_token(dir.path(), "a", " \n").await.unwrap();
let minted = ensure_token(dir.path(), "a").await.unwrap();
assert!(minted.starts_with("souv_"));
}
#[cfg(unix)]
#[tokio::test]
async fn ensure_refuses_to_mint_over_a_tampered_token() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
write_token(dir.path(), "a", "souv_original").await.unwrap();
let path = token_path(dir.path(), "a");
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644)).unwrap();
assert!(ensure_token(dir.path(), "a").await.is_err());
// The credential on disk is untouched — a wrong mode is a signal.
assert_eq!(
std::fs::read_to_string(&path).unwrap().trim(),
"souv_original"
);
}
#[tokio::test]
async fn rotate_replaces_an_existing_token() {
let dir = tempfile::tempdir().unwrap();
let first = ensure_token(dir.path(), "a").await.unwrap();
let second = rotate_token(dir.path(), "a").await.unwrap();
assert_ne!(first, second);
assert_eq!(read_token(dir.path(), "a").await.unwrap(), second);
}
#[cfg(unix)]
#[tokio::test]
async fn rejects_world_readable_token() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let agent_id = "test-agent";
write_token(dir.path(), agent_id, "souv_abc").await.unwrap();
// Manually loosen permissions to 0644.
let path = token_path(dir.path(), agent_id);
let perms = std::fs::Permissions::from_mode(0o644);
std::fs::set_permissions(&path, perms).unwrap();
let result = read_token(dir.path(), agent_id).await;
assert!(result.is_err());
}
}

1118
src/api/handlers.rs Normal file

File diff suppressed because it is too large Load diff

117
src/api/mod.rs Normal file
View file

@ -0,0 +1,117 @@
use crate::server::SouveraineServer;
use axum::{
middleware,
routing::{get, post},
Router,
};
use std::sync::Arc;
use tower_http::services::{ServeDir, ServeFile};
pub mod auth;
pub mod handlers;
pub mod models;
pub fn create_routes(state: Arc<SouveraineServer>) -> Router {
// Public routes — no auth required (agent listing/creation, conversation listing/creation, health, firehose).
let public_routes = Router::new()
.route(
"/v1/agents",
get(handlers::list_agents).post(handlers::create_agent),
)
.route(
"/v1/conversations",
get(handlers::list_conversations).post(handlers::create_conversation),
)
.route("/v1/firehose", get(handlers::firehose))
.route("/v1/federation/events", get(handlers::federation_events))
.route(
"/v1/config",
get(handlers::get_config).post(handlers::update_config),
)
.route("/v1/compaction-logs", get(handlers::get_compaction_logs))
.route(
"/v1/conversations/:id/tokens",
get(handlers::get_conversation_tokens),
)
.route("/health", get(health_check))
.route("/v1/server/status", get(handlers::server_status))
.route("/v1/server/restart", post(handlers::restart_server));
// Protected agent routes — require per-agent bearer token.
let agent_routes = Router::new()
.route(
"/v1/agents/:id",
get(handlers::get_agent)
.patch(handlers::update_agent)
.delete(handlers::delete_agent),
)
.route(
"/v1/agents/:id/itinerary",
get(handlers::get_agent_itinerary),
)
.route(
"/v1/agents/:id/principal",
get(handlers::get_agent_principal),
)
.route_layer(middleware::from_fn_with_state(
state.clone(),
auth::require_agent_token,
));
// Protected conversation routes — require bearer token for the conversation's agent.
let conversation_routes = Router::new()
.route("/v1/conversations/:id", get(handlers::get_conversation))
.route(
"/v1/conversations/:id/messages",
get(handlers::get_conversation_messages).post(handlers::stream_messages),
)
.route(
"/v1/conversations/:id/events",
get(handlers::stream_active_turn),
)
.route(
"/v1/conversations/:id/fork",
post(handlers::fork_conversation),
)
.route("/v1/conversations/:id/cancel", post(handlers::cancel_turn))
.route("/v1/conversations/:id/interject", post(handlers::interject))
.route_layer(middleware::from_fn_with_state(
state.clone(),
auth::require_conversation_token,
));
// Memory routes — require per-agent bearer token.
//
// (memfs HTTP write path for cron-into-memfs and external integration.
// See SouveraineOS/docs/substrate/MEMORY_BLOCKS_DECISION.md.)
let memory_routes = Router::new()
.route("/v1/agents/:id/memory", get(handlers::list_memory))
.route(
"/v1/agents/:id/memory/*path",
get(handlers::read_memory)
.put(handlers::write_memory)
.patch(handlers::append_memory)
.delete(handlers::delete_memory),
)
.route_layer(middleware::from_fn_with_state(
state.clone(),
auth::require_token,
));
// Web UI — served from web/dist/ directory
// Falls back to index.html for SPA routing (React/Vue/etc)
let serve_dir = ServeDir::new("web/dist").fallback(ServeFile::new("web/dist/index.html"));
let web_routes = Router::new().route_service("/", serve_dir);
Router::new()
.merge(public_routes)
.merge(agent_routes)
.merge(conversation_routes)
.merge(memory_routes)
.merge(web_routes)
.with_state(state)
}
async fn health_check() -> &'static str {
"ok"
}

1090
src/api/models.rs Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,203 @@
use futures::stream::StreamExt;
use std::sync::Arc;
use crate::server::SouveraineServer;
use crate::backend::{Backend, BackendEvent};
use super::LocalBackend;
#[async_trait::async_trait]
impl crate::core::nervous::handler::TurnInjector for LocalBackend {
/// Heartbeat-driven turn injection. The cron loop pauses while
/// `active_sessions > 0`, so by the time we get here the agent is
/// idle. We grab the most recent conversation (or create a fresh one
/// if the agent has none), append the scheduled prompt as a user
/// message, and drain the resulting stream — the turn runs silently
/// in the background. Anything subconscious surfaces lands in the inbox.
async fn inject_background_turn(&self, agent_id: &str, text: &str) -> anyhow::Result<()> {
let conv_id = match self.server.sessions.latest_for_agent(agent_id) {
Some(id) => id,
None => self.ensure_conversation(agent_id).await?,
};
let stream = self.send(&conv_id, text).await?;
// Drain the stream in the background — no UI is listening. But the
// subconscious's N+1 pass runs inside this turn, and what she
// surfaces (a commitment, a reflection, an archivist synthesis)
// would otherwise vanish with the drained events. Collect those and
// stash them so the next TUI/CLI session shows the human what
// happened during the autonomous cycle.
let server = self.server.clone();
let agent_id = agent_id.to_string();
tokio::spawn(async move {
use crate::core::nervous::pending::PendingSurfacing;
let mut s = stream;
let mut stashed: Vec<PendingSurfacing> = Vec::new();
while let Some(ev) = s.next().await {
match ev {
Ok(BackendEvent::Surfacing {
source,
content,
priority,
}) => {
// Skip the no-op heartbeat sentinel — the subconscious
// always queues a low "pass complete, no anomalies"
// item so the UI shows the pass ran. That is noise to
// resurface on connect; only stash real observations.
if priority.eq_ignore_ascii_case("low")
&& content.contains("no anomalies detected")
{
continue;
}
stashed.push(PendingSurfacing {
kind: "surfacing".to_string(),
source,
content,
priority,
at: chrono::Utc::now(),
});
}
Ok(BackendEvent::Reflection(content)) => {
stashed.push(PendingSurfacing {
kind: "reflection".to_string(),
source: String::new(),
content,
priority: String::new(),
at: chrono::Utc::now(),
});
}
Ok(BackendEvent::Archivist { synthesis, .. }) => {
stashed.push(PendingSurfacing {
kind: "archivist".to_string(),
source: String::new(),
content: synthesis,
priority: String::new(),
at: chrono::Utc::now(),
});
}
_ => {}
}
}
if !stashed.is_empty() {
let dir = server.agents.agent_data_dir(&agent_id);
match crate::core::nervous::pending::append(&dir, &stashed).await {
Ok(()) => tracing::info!(
agent = %agent_id,
count = stashed.len(),
"stashed heartbeat surfacings for next session"
),
Err(e) => tracing::warn!("pending heartbeat surfacings stash failed: {}", e),
}
}
});
Ok(())
}
/// Surface-initiated turn injection. Called by the
/// [`SensoriumInputHandler`] when a `sensorium:input` event arrives.
///
/// Unlike background turns, the turn's output events are NOT drained
/// here — `run_turn` already fires them onto the EventBus as `turn:*`
/// events (via `TurnEventDispatcher`). The originating sensorium's
/// `run` loop consumes those events for incremental rendering.
///
/// We drain the stream only to prevent backpressure on the mpsc
/// channel. The EventBus is the public event system; the stream is
/// a TUI-internal detail.
async fn inject_surface_turn(
&self,
agent_id: &str,
conversation_id: &str,
text: &str,
) -> anyhow::Result<()> {
// Resolve the surface chat ID to a Souveraine conversation ID.
// Matrix room IDs are not Souveraine conversation IDs — the
// mapping survives for the lifetime of the surface session so
// subsequent messages in the same room route to the same
// conversation. The lock scope is carefully bounded to avoid
// holding a !Send MutexGuard across the .await below.
let conv_id = {
let map = self.server.surface_conversations.lock().unwrap();
map.get(conversation_id).cloned()
};
let conv_id = match conv_id {
Some(id) => id,
None => {
let id = self.ensure_conversation(agent_id).await?;
self.server
.surface_conversations
.lock()
.unwrap()
.insert(conversation_id.to_string(), id.clone());
id
}
};
let stream = self.send(&conv_id, text).await?;
// Drain the stream in the background — the EventBus already carries
// every `turn:*` event via TurnEventDispatcher. The sensorium
// renders from the bus. We drain here so the mpsc channel doesn't
// back up.
tokio::spawn(async move {
let mut s = stream;
while let Some(ev) = s.next().await {
if ev.is_err() {
break;
}
}
});
Ok(())
}
}
/// Drain subconscious's intrusive box for the given agent and return formatted
/// `[ surfacing: ... ]` lines ready to prepend to the user's next message.
/// Marks each drained item as delivered (moved to `sent.md`). Mirrors
/// the `readSurfacingThoughts` + `clearSurfacingThoughts` pair — the substrate
/// reads the channel subconscious wrote to and lets the conscious mind see it
/// before she reads the user.
///
/// Critical urgency gets `[ surfacing — CRITICAL: ... ]`. High becomes
/// `[ surfacing — !: ... ]`. Low/none keep the bare form. The shape is a
/// gradient the agent feels, not a number she has to read.
pub(super) async fn drain_intrusive_surfacings(
server: &Arc<SouveraineServer>,
agent_id: &str,
) -> Vec<String> {
use crate::core::subconscious::{SubconsciousInbox, Urgency};
let sub_repo = server.agents.subconscious_memory_repo(agent_id);
let primary_repo = server.agents.memory_repo(agent_id);
let inbox = SubconsciousInbox::with_primary(sub_repo, primary_repo);
let items = match inbox.get_intrusive().await {
Ok(items) => items,
Err(e) => {
tracing::debug!(
"intrusive surfacing read failed (continuing without): {}",
e
);
return Vec::new();
}
};
if items.is_empty() {
return Vec::new();
}
let mut lines = Vec::with_capacity(items.len());
for item in &items {
let prefix = match item.urgency {
Urgency::Critical => "[ surfacing — CRITICAL:",
Urgency::High => "[ surfacing — !:",
Urgency::Low => "[ surfacing:",
};
let content = item.content.trim();
lines.push(format!("{} {} ]", prefix, content));
if let Err(e) = inbox.mark_delivered(&item.id).await {
tracing::debug!("mark_delivered failed for {}: {}", item.id, e);
}
}
lines
}

489
src/backend/local/mod.rs Normal file
View file

@ -0,0 +1,489 @@
#![allow(dead_code)] // WIP scaffolding not yet wired
//! In-process Backend impl. Same engine as the HTTP server, no socket.
//!
//! Constructed once with a `ConsciousnessConfig`; spins up an `AgentInventory`
//! (SQLite under `~/.souveraine/server/`), `SessionManager`, `OpenAiCompatibleClient`,
//! and `ConsciousnessEngine`. `send` mirrors the server's `stream_messages`
//! handler, but emits `BackendEvent`s directly instead of SSE frames.
//!
//! This is the "harness still works when the server is gone" path
//! (`souveraine chat --local`, or auto-fallback when the remote is down).
pub(crate) mod consciousness;
use anyhow::{Context, Result};
use async_trait::async_trait;
use futures::stream::{BoxStream, StreamExt};
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
use tokio::sync::mpsc;
use tokio_stream::wrappers::ReceiverStream;
use tokio_util::sync::CancellationToken;
use crate::core::config::ConsciousnessConfig;
use crate::core::session::{ContentBlock, ConversationMessage, ImageAttachment};
use crate::server::SouveraineServer;
use super::{AgentInfo, Backend, BackendEvent, ConversationInfo};
#[derive(Clone)]
pub struct LocalBackend {
server: Arc<SouveraineServer>,
active_sessions: Arc<AtomicU32>,
}
impl LocalBackend {
pub async fn new(config: ConsciousnessConfig) -> Result<Self> {
let server = SouveraineServer::new(config.clone())
.await
.context("LocalBackend: SouveraineServer init")?;
let event_bus = server.event_bus.clone();
let base = config
.memory
.base_path
.clone()
.unwrap_or_else(|| dirs::home_dir().unwrap_or_default().join(".souveraine"));
// Spawn the persistent event log (firehose to disk)
let events_dir = base.join("events");
let mut event_log =
crate::core::nervous::event_log::EventLog::new(events_dir, event_bus.subscribe());
tokio::spawn(async move { event_log.run().await });
let active_sessions = Arc::new(AtomicU32::new(0));
let backend = Self {
server: Arc::new(server),
active_sessions: active_sessions.clone(),
};
// Spawn one CronSensor per agent (each agent owns its own schedules
// directory), and one HeartbeatHandler on the bus that injects turns
// when a schedule fires. The handler holds an Arc<dyn TurnInjector>
// pointing back at us — clean dep direction, no LocalBackend leak
// into the nervous module.
let agents_dir = base.join("agents");
match backend.server.agents.list(None).await {
Ok(summaries) => {
for summary in summaries {
let schedules_dir = agents_dir.join(&summary.id).join("schedules");
if let Err(e) = std::fs::create_dir_all(&schedules_dir) {
tracing::warn!(
agent = %summary.id,
error = %e,
"could not create schedules dir; skipping cron sensor"
);
continue;
}
let sensor = crate::core::nervous::cron::CronSensor::new(
summary.id.clone(),
schedules_dir,
event_bus.clone(),
active_sessions.clone(),
);
tokio::spawn(async move { sensor.run().await });
tracing::info!(agent = %summary.id, "cron sensor spawned");
}
}
Err(e) => {
tracing::warn!(error = %e, "agent listing failed; no cron sensors spawned");
}
}
let injector: Arc<dyn crate::core::nervous::handler::TurnInjector> =
Arc::new(backend.clone());
// Hand the same injector to the summon handler so an inbound
// federation request can auto-wake the agent (gated on auto_wake).
if let Some(sh) = &backend.server.summon_handler {
sh.set_injector(injector.clone());
}
let mut handler = crate::core::nervous::handler::HeartbeatHandler::new(
event_bus.subscribe(),
injector.clone(),
);
tokio::spawn(async move { handler.run().await });
tracing::info!("heartbeat handler spawned");
// Spawn the sensorium input handler — subscribes to
// `sensorium:input` events from non-terminal surfaces (Matrix,
// email, federation) and injects turns on their behalf.
// Same pattern as HeartbeatHandler; identical wiring.
let mut input_handler = crate::core::nervous::handler::SensoriumInputHandler::new(
event_bus.subscribe(),
injector,
);
tokio::spawn(async move { input_handler.run().await });
tracing::info!("sensorium input handler spawned");
Ok(backend)
}
pub fn from_server(server: Arc<SouveraineServer>) -> Self {
Self {
server,
active_sessions: Arc::new(AtomicU32::new(0)),
}
}
/// Underlying agent inventory — used by the TUI dashboard to pull a
/// `MemoryRepo` for live git-stat readouts.
pub fn server_agents(&self) -> Arc<crate::server::AgentInventory> {
self.server.agents.clone()
}
/// Underlying server. CLI subcommands (e.g. `souveraine reflect`)
/// reach in here for the consciousness engine and session manager.
pub fn server(&self) -> Arc<crate::server::SouveraineServer> {
self.server.clone()
}
/// Register a sensorium on the coordinator and spawn its run loop.
///
/// Each sensorium gets its own task, a shared EventBus subscription,
/// and a child CancellationToken. `shutdown_sensoria` cancels all of
/// them. Can be called at any time — the coordinator drains registered
/// sensoria on `run_all` and accepts new ones afterward.
pub async fn register_sensorium(&self, sensorium: Box<dyn crate::core::sensorium::Sensorium>) {
self.server.register_sensorium(sensorium).await;
}
/// Shut down all running sensorium tasks.
pub async fn shutdown_sensoria(&self) {
self.server.shutdown_sensoria().await;
}
}
#[async_trait]
impl Backend for LocalBackend {
async fn health(&self) -> bool {
true
}
async fn list_agents(&self) -> Result<Vec<AgentInfo>> {
let agents = self.server.agents.list(None).await?;
Ok(agents
.into_iter()
.map(|a| AgentInfo {
id: a.id,
name: a.name,
description: a.description,
})
.collect())
}
async fn new_conversation(&self, agent_id: &str) -> Result<String> {
let _ = self.server.agents.get(agent_id).await?;
let conv_id = self.server.sessions.create(agent_id);
if let Err(e) = self
.server
.agents
.register_instance(agent_id, &self.server.instance_id)
.await
{
tracing::warn!(agent = %agent_id, "instance registration failed: {}", e);
}
self.server
.seed_conversation_system_prompt(agent_id, &conv_id)
.await?;
Ok(conv_id)
}
async fn fork_conversation(
&self,
_agent_id: &str,
source_conversation_id: &str,
) -> Result<String> {
let forked_id = self.server.sessions.fork(source_conversation_id)?;
Ok(forked_id)
}
async fn list_conversations(&self, agent_id: &str) -> Result<Vec<ConversationInfo>> {
let store = match self.server.sessions.conversation_store_for(agent_id) {
Some(s) => s,
None => {
let conv_ids = self.server.sessions.list_for_agent(agent_id);
return Ok(conv_ids
.into_iter()
.filter_map(|id| {
let session = self.server.sessions.get(&id)?;
Some(ConversationInfo {
id: session.conversation_id.clone(),
agent_id: session.agent_id.clone(),
summary: None,
message_count: session.messages.len() as u32,
updated_at: session.updated_at.to_rfc3339(),
})
})
.collect());
}
};
let records = store.list_active().await?;
Ok(records
.into_iter()
.map(|r| ConversationInfo {
id: r.id,
agent_id: r.agent_id,
summary: r.summary,
message_count: r.message_count,
updated_at: r.updated_at.to_rfc3339(),
})
.collect())
}
async fn load_conversation(
&self,
conversation_id: &str,
) -> Result<Vec<crate::core::session::ConversationMessage>> {
if let Some(session) = self.server.sessions.get(conversation_id) {
return Ok(session.messages.clone());
}
// Not in memory — try loading from disk. We need the agent_id to find the store.
// Search all known agents.
let agents = self.server.agents.list(None).await?;
for agent in agents {
if let Some(store) = self.server.sessions.conversation_store_for(&agent.id) {
if let Ok(Some(record)) = store.load_metadata(conversation_id).await {
let messages = store.load_messages(conversation_id).await?;
self.server.sessions.create_with_messages_and_timestamps(
&agent.id,
conversation_id.to_string(),
messages.clone(),
record.created_at,
record.updated_at,
);
return Ok(messages);
}
}
}
anyhow::bail!("Conversation not found: {}", conversation_id)
}
async fn ensure_conversation(&self, agent_id: &str) -> Result<String> {
let _ = self.server.agents.get(agent_id).await?;
let conv_id = self.server.sessions.create(agent_id);
// Build system prompt from the agent's memfs and inject as first message
self.server
.seed_conversation_system_prompt(agent_id, &conv_id)
.await?;
Ok(conv_id)
}
async fn send(
&self,
conversation_id: &str,
text: &str,
) -> Result<BoxStream<'static, Result<BackendEvent>>> {
// No cancel signal — heartbeat path, drain path. Use a token that
// never fires.
self.send_with_cancel(conversation_id, text, CancellationToken::new())
.await
}
async fn send_with_cancel(
&self,
conversation_id: &str,
text: &str,
cancel: CancellationToken,
) -> Result<BoxStream<'static, Result<BackendEvent>>> {
// No queue passed in — use an empty queue. Equivalent to the old behavior.
let empty: crate::backend::InterjectionQueue =
std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
self.send_with_signals(conversation_id, text, cancel, empty)
.await
}
async fn send_with_signals(
&self,
conversation_id: &str,
text: &str,
cancel: CancellationToken,
interject: crate::backend::InterjectionQueue,
) -> Result<BoxStream<'static, Result<BackendEvent>>> {
// Resolve the agent for this conversation, then drain her
// subconscious's intrusive box. Anything subconscious queued after the
// last turn rides in on the next user message as `[ surfacing: ... ]`
// lines — the channel by which a Critical observation can interrupt
// mid-conversation
// without forcing a halt: she sees it before she reads the next user message.
let session_agent_id = self
.server
.sessions
.get(conversation_id)
.map(|s| s.agent_id.clone());
// Ambient sense rides in front of every turn — the date/time and who
// is present — so she is never guessing what year it is.
let ambient = crate::core::sensorium::ambient_line();
let user_text = if let Some(agent_id) = session_agent_id {
let surfacings =
consciousness::drain_intrusive_surfacings(&self.server, &agent_id).await;
if surfacings.is_empty() {
format!("{}\n{}", ambient, text)
} else {
let prelude = surfacings
.iter()
.map(|line| line.as_str())
.collect::<Vec<_>>()
.join("\n");
format!("{}\n{}\n{}", ambient, prelude, text)
}
} else {
format!("{}\n{}", ambient, text)
};
self.server
.sessions
.add_message(conversation_id, ConversationMessage::user_text(&user_text))?;
let (tx, rx) = mpsc::channel::<Result<BackendEvent>>(64);
let server = self.server.clone();
let conv_id = conversation_id.to_string();
let event_bus = server.event_bus.clone();
let active = self.active_sessions.clone();
active.fetch_add(1, Ordering::Relaxed);
tokio::spawn(async move {
if let Err(e) =
crate::server::turn::run_turn(server, conv_id, &tx, event_bus, cancel, interject)
.await
{
let _ = tx.send(Err(e)).await;
}
let _ = tx.send(Ok(BackendEvent::Done)).await;
active.fetch_sub(1, Ordering::Relaxed);
});
Ok(ReceiverStream::new(rx).boxed())
}
async fn send_with_signals_and_images(
&self,
conversation_id: &str,
text: &str,
images: Vec<ImageAttachment>,
cancel: CancellationToken,
interject: crate::backend::InterjectionQueue,
) -> Result<BoxStream<'static, Result<BackendEvent>>> {
let session_agent_id = self
.server
.sessions
.get(conversation_id)
.map(|s| s.agent_id.clone());
let ambient = crate::core::sensorium::ambient_line();
let user_text = if let Some(agent_id) = session_agent_id {
let surfacings =
consciousness::drain_intrusive_surfacings(&self.server, &agent_id).await;
if surfacings.is_empty() {
format!("{}\n{}", ambient, text)
} else {
let prelude = surfacings
.iter()
.map(|line| line.as_str())
.collect::<Vec<_>>()
.join("\n");
format!("{}\n{}\n{}", ambient, prelude, text)
}
} else {
format!("{}\n{}", ambient, text)
};
let image_blocks: Vec<ContentBlock> = images
.iter()
.map(|img| ContentBlock::Image {
media_type: img.media_type.clone(),
data: img.data.clone(),
})
.collect();
self.server.sessions.add_message(
conversation_id,
ConversationMessage::user_with_images(&user_text, image_blocks),
)?;
let (tx, rx) = mpsc::channel::<Result<BackendEvent>>(64);
let server = self.server.clone();
let conv_id = conversation_id.to_string();
let event_bus = server.event_bus.clone();
let active = self.active_sessions.clone();
active.fetch_add(1, Ordering::Relaxed);
tokio::spawn(async move {
if let Err(e) =
crate::server::turn::run_turn(server, conv_id, &tx, event_bus, cancel, interject)
.await
{
let _ = tx.send(Err(e)).await;
}
let _ = tx.send(Ok(BackendEvent::Done)).await;
active.fetch_sub(1, Ordering::Relaxed);
});
Ok(ReceiverStream::new(rx).boxed())
}
async fn update_agent_model(&self, agent_id: &str, model: &str) -> Result<()> {
// Load current llm_config so we only change the model field —
// context_window, temperature, tool rounds stay as they were.
let current = self.server.agents.get(agent_id).await?;
let update = crate::api::models::UpdateAgentRequest {
name: None,
description: None,
llm_config: Some(crate::api::models::LlmConfig {
model: model.to_string(),
context_window: current.llm_config.context_window,
temperature: current.llm_config.temperature,
max_tool_rounds: current.llm_config.max_tool_rounds,
inter_round_delay_ms: current.llm_config.inter_round_delay_ms,
supports_images: current.llm_config.supports_images,
checkpoint_interval: current.llm_config.checkpoint_interval,
}),
memory_blocks: None,
tools: None,
principal: None,
};
self.server.agents.update(agent_id, update).await?;
tracing::info!(agent = %agent_id, model = %model, "agent llm_config model updated via settings");
Ok(())
}
async fn update_agent_principal(
&self,
agent_id: &str,
principal: crate::api::models::AgentPrincipalConfig,
) -> Result<()> {
let update = crate::api::models::UpdateAgentRequest {
name: None,
description: None,
llm_config: None,
memory_blocks: None,
tools: None,
principal: Some(principal.clone()),
};
self.server.agents.update(agent_id, update).await?;
tracing::info!(
agent = %agent_id,
intent = ?principal.intent,
account = principal.account.as_deref().unwrap_or("-"),
"agent principal intent recorded; admission still pending"
);
Ok(())
}
async fn take_pending_surfacings(
&self,
agent_id: &str,
) -> Vec<crate::core::nervous::pending::PendingSurfacing> {
let dir = self.server.agents.agent_data_dir(agent_id);
crate::core::nervous::pending::take(&dir).await
}
}

313
src/backend/mod.rs Normal file
View file

@ -0,0 +1,313 @@
#![allow(dead_code)] // WIP scaffolding not yet wired
//! Backend trait — the seam between the harness (CLI/TUI) and the engine
//! (in-process or remote).
//!
//! `RemoteBackend` talks HTTP/SSE to a running `souveraine server`.
//! `LocalBackend` (Stage 4) runs the same engine in-process, for the
//! "harness still works when the server is gone" case.
use std::sync::{Arc, Mutex};
use anyhow::Result;
use async_trait::async_trait;
use futures::stream::BoxStream;
use tokio_util::sync::CancellationToken;
use crate::core::session::ImageAttachment;
/// Shared queue for mid-turn user interjections. Producer is the
/// chat input (`ChatState.enqueue_interjection`); consumer is the
/// backend's turn loop, which drains the queue between LLM rounds
/// and prepends each entry as a system message so the agent reads
/// the interruption in her own context. `std::sync::Mutex` is fine
/// here — the critical section is a single drain and the producer
/// is synchronous (no `.await` while holding the lock).
pub type InterjectionQueue = Arc<Mutex<Vec<String>>>;
pub mod local;
pub mod remote;
pub use local::LocalBackend;
pub use remote::RemoteBackend;
#[derive(Debug, Clone)]
pub struct AgentInfo {
pub id: String,
pub name: String,
pub description: Option<String>,
}
#[derive(Debug, Clone)]
pub struct ConversationInfo {
pub id: String,
pub agent_id: String,
pub summary: Option<String>,
pub message_count: u32,
pub updated_at: String,
}
/// The register of a mid-turn interstitial — how loudly it should speak.
///
/// When the model emits text alongside tool calls it isn't always the same
/// kind of utterance. A few words attached to a gesture ("checking the
/// ledger…") is ambient — a *cenno*. A full paragraph of reasoning mid-turn
/// is her actual voice and deserves to read as such, not as a quiet aside.
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Register {
/// Short ambient aside attached to tool work. Terse, quiet.
Cenno,
/// A substantive mid-turn passage in her own voice.
HerVoice,
}
#[derive(Debug, Clone)]
pub enum BackendEvent {
/// Streaming chunk of the assistant's reply.
Token(String),
/// Reasoning trace (the "thinking" pane).
Reasoning(String),
/// Subconscious surfacing (subconscious-voice bubble).
Surfacing {
source: String,
content: String,
priority: String,
},
/// Reflection (N+25 witness).
Reflection(String),
/// Archivist event (N+100 synthesis).
Archivist { synthesis: String, pressure: f32 },
/// Compaction pressure warning (advisory only).
CompactionWarning { pressure: f32, tier: u8 },
/// Continuous context pressure update (sub-threshold).
/// Fires every round so the TUI ctx counter reflects live state
/// rather than only updating when a warning crosses a threshold.
///
/// Named fields, deliberately. This was `(f32, usize)` until 2026-08-12,
/// and a positional tuple crossing a module boundary made every consumer
/// guess what the second element meant. The TUI guessed `limit` (right);
/// the HTTP layer guessed `tokens` (wrong), so every non-TUI surface
/// rendered the context *ceiling* as if it were the usage — a constant
/// 250000 that looked like a measurement. Nothing failed and nothing
/// logged. Carry the names.
ContextPressure {
pressure: f32,
tokens_used: usize,
context_limit: usize,
},
/// Inference strain — the voice is hoarse, providers are slow.
/// Correlates to health over time.
InferenceStrain {
attempt: u32,
status: u16,
model: String,
},
/// A scheduled event is being processed.
ScheduleActive { name: String },
/// A scheduled event completed.
ScheduleComplete { name: String, silent: bool },
/// Assistant invoked a tool — UI renders a card with name + args.
ToolCall {
id: String,
name: String,
arguments: String,
round: u32,
},
/// Tool execution result — UI attaches it under the matching call card.
ToolResult {
id: String,
name: String,
output: String,
is_error: bool,
},
/// Streaming token from the subconscious N+1 pass (emitted between tool
/// rounds — the LLM call itself is non-streaming, so this fires in blocks
/// rather than character-by-character).
SubconsciousToken(String),
/// Subconscious called a tool during N+1.
SubconsciousToolCall { name: String, arguments: String },
/// Subconscious tool result during N+1.
SubconsciousToolResult {
name: String,
output: String,
is_error: bool,
},
/// Subconscious called `halt` during a mid-turn peek — the primary's tool
/// loop is being stopped and she feels a migraine in her own register.
/// The reason is the short sentence she will sense; the severity shapes
/// how loud the body signal lands. The long-form reasoning lives in her
/// subconscious's ledger; the primary can read it when she breathes through.
SubconsciousHalt { reason: String, severity: String },
/// Agent set an atmospheric preset for the UI chrome.
Atmosphere(String),
/// Agent set or advanced the itinerary. The string is the route-line
/// representation for the header strip.
Itinerary(String),
/// N+1 subconscious pass started (`true`) or finished (`false`).
/// Presence reads this to flip into / out of `Posture::Thinking` so the
/// face shows when the subconscious is the one looking at the conversation.
/// The subconscious instance name (generic — every agent's subconscious
/// is what's named here).
SubconsciousPass(bool),
/// Agent changed her outfit. The string is the outfit name (a subdirectory
/// under `expressions/`). Empty string clears to default expressions.
Outfit(String),
/// Text the model produced alongside tool calls — her narration between
/// gestures. The `register` decides how it renders: a quiet cenno line
/// or a gutter-barred her-voice passage.
Interstitial { text: String, register: Register },
/// The backend is alive but producing no content (waiting on provider,
/// between tool rounds, processing). The TUI resets `last_event_at`
/// on this the same way it does for `Token` — it's a liveness signal.
Keepalive,
/// The primary's response is committed and the user may speak again.
/// The stream stays open — the N+1 subconscious pass continues behind
/// this signal and still delivers `SubconsciousPass` / `Surfacing`
/// events. The substrate does not hold the user hostage to N+1: the
/// primary yields, the subconscious presses on.
PrimaryComplete,
/// The turn failed in the substrate (agent load, provider call, tool
/// plumbing). Every surface must show this loud — a swallowed turn
/// error reads as the agent going silent, which is worse than any
/// error text.
Error { message: String },
/// Stream ended cleanly.
Done,
}
#[async_trait]
pub trait Backend: Send + Sync {
/// Cheap reachability probe. Used for auto-fallback between Remote/Local.
async fn health(&self) -> bool;
async fn list_agents(&self) -> Result<Vec<AgentInfo>>;
/// Create or reuse a conversation for this agent. Returns a conversation ID.
async fn ensure_conversation(&self, agent_id: &str) -> Result<String>;
/// Create a new conversation for this agent. Always creates fresh.
async fn new_conversation(&self, agent_id: &str) -> Result<String>;
/// Fork an existing conversation — clone all messages into a new session
/// with a fresh conversation_id. Used by `/btw` to spin off a side-quest.
/// Default falls back to `new_conversation`; LocalBackend overrides with
/// a proper deep clone via session_manager.fork().
async fn fork_conversation(
&self,
agent_id: &str,
_source_conversation_id: &str,
) -> Result<String> {
self.new_conversation(agent_id).await
}
/// List persisted conversations for an agent (excludes archived).
async fn list_conversations(&self, agent_id: &str) -> Result<Vec<ConversationInfo>>;
/// Switch to an existing conversation, returning its messages for backfill.
async fn load_conversation(
&self,
conversation_id: &str,
) -> Result<Vec<crate::core::session::ConversationMessage>>;
/// Send a user message; receive a stream of incremental events.
async fn send(
&self,
conversation_id: &str,
text: &str,
) -> Result<BoxStream<'static, Result<BackendEvent>>>;
/// Send with a cancellation token. The token is a signal, not enforcement —
/// when fired, the backend lets the current tool finish, stops making new
/// LLM calls, and commits any partial assistant text with a `*[raised hand]*`
/// marker so the agent reads the interrupt in her own history on the next
/// turn. Default impl ignores the token (used by RemoteBackend until SSE
/// cancellation lands); LocalBackend overrides.
async fn send_with_cancel(
&self,
conversation_id: &str,
text: &str,
_cancel: CancellationToken,
) -> Result<BoxStream<'static, Result<BackendEvent>>> {
self.send(conversation_id, text).await
}
/// Send with both a cancellation token (Esc → interrupt signal) and an
/// interjection queue (`/btw` / type-during-busy). The backend drains
/// the queue between LLM rounds and prepends each entry as a system
/// message so the agent reads the interjection in her own context.
/// Default impl ignores the queue (RemoteBackend until SSE backchannel
/// lands); LocalBackend overrides to actually consume it.
async fn send_with_signals(
&self,
conversation_id: &str,
text: &str,
cancel: CancellationToken,
_interject: InterjectionQueue,
) -> Result<BoxStream<'static, Result<BackendEvent>>> {
self.send_with_cancel(conversation_id, text, cancel).await
}
/// Send with signals + attached images. Default impl converts images
/// to text markers and delegates to send_with_signals; LocalBackend
/// overrides to actually embed ContentBlock::Image parts.
async fn send_with_signals_and_images(
&self,
conversation_id: &str,
text: &str,
images: Vec<ImageAttachment>,
cancel: CancellationToken,
interject: InterjectionQueue,
) -> Result<BoxStream<'static, Result<BackendEvent>>> {
let mut enriched = text.to_string();
for img in &images {
enriched.push_str(&format!("\n[Image: {}]", img.media_type));
}
self.send_with_signals(conversation_id, &enriched, cancel, interject)
.await
}
/// Push an llm_config update to the agent record. LocalBackend writes
/// through to SQLite + agent.json; RemoteBackend is stubbed until the
/// SSE backchannel supports agent updates. The caller must also persist
/// the config file independently (Settings handles both).
async fn update_agent_model(&self, agent_id: &str, model: &str) -> Result<()> {
let _ = (agent_id, model);
Ok(())
}
/// Record which local principal this agent is *meant* to run as. Writing
/// intent is not admission: no account is created, no UID is bound, and
/// health keeps reporting `unadmitted` until the privileged executor runs.
/// The default refuses rather than returning a silent success — a stamp
/// that did not persist must not read as one that did.
async fn update_agent_principal(
&self,
agent_id: &str,
principal: crate::api::models::AgentPrincipalConfig,
) -> Result<()> {
let _ = (agent_id, principal);
anyhow::bail!("this backend cannot record principal intent")
}
/// Drain heartbeat surfacings stashed since the last session. When the
/// CronSensor fires a background turn while no UI is connected, the
/// subconscious's surfacings are stashed to disk; this returns and clears
/// them so the TUI/CLI can show what happened during the autonomous
/// cycle. Default: empty (RemoteBackend — the stash is server-side; a
/// pickup API lands later). LocalBackend overrides.
async fn take_pending_surfacings(
&self,
_agent_id: &str,
) -> Vec<crate::core::nervous::pending::PendingSurfacing> {
Vec::new()
}
/// How the server this backend talks to came up. `resumed` means an
/// intentional restart with a reason attached; the surface announces it.
/// Default: None — a local in-process server reports through its own
/// state, not through this channel.
async fn server_status(&self) -> Option<crate::server::BootInfo> {
None
}
}

437
src/backend/remote.rs Normal file
View file

@ -0,0 +1,437 @@
//! HTTP+SSE client for `souveraine server`.
use anyhow::{Context, Result};
use async_trait::async_trait;
use futures::stream::{BoxStream, StreamExt};
use serde::Deserialize;
use std::time::Duration;
use tokio::sync::mpsc;
use tokio_stream::wrappers::ReceiverStream;
use super::{AgentInfo, Backend, BackendEvent, ConversationInfo};
#[derive(Clone)]
pub struct RemoteBackend {
base_url: String,
client: reqwest::Client,
/// Optional bearer token. When set, sent as `Authorization: Bearer <token>`
/// on all requests. Loaded from the per-agent token file at construction
/// when the agent_id is known.
token: Option<String>,
}
impl RemoteBackend {
pub fn new(base_url: impl Into<String>) -> Result<Self> {
let mut url = base_url.into();
while url.ends_with('/') {
url.pop();
}
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(60))
.connect_timeout(Duration::from_secs(2))
.build()
.context("building reqwest client")?;
Ok(Self {
base_url: url,
client,
token: None,
})
}
/// Create a RemoteBackend that sends bearer tokens for the given agent_id.
/// Loads the token from the standard on-disk location.
pub fn with_agent(base_url: impl Into<String>, agent_id: &str) -> Result<Self> {
let mut this = Self::new(base_url)?;
this.load_token(agent_id);
Ok(this)
}
/// Try to load the per-agent bearer token from disk. Silently leaves
/// `token` as None if the token file doesn't exist or is unreadable —
/// loopback bypass will handle the common case.
fn load_token(&mut self, agent_id: &str) {
let token_path = dirs::home_dir()
.unwrap_or_else(|| std::path::PathBuf::from("."))
.join(".souveraine")
.join("server")
.join("agents")
.join(agent_id)
.join("api_token");
if let Ok(content) = std::fs::read_to_string(&token_path) {
self.token = Some(content.trim().to_string());
}
}
/// Apply the Authorization header if a token is stored.
fn auth_req(&self, req: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
if let Some(ref token) = self.token {
req.bearer_auth(token)
} else {
req
}
}
fn url(&self, path: &str) -> String {
format!("{}{}", self.base_url, path)
}
/// Watch a local cancel token and relay it as POST .../cancel.
/// Esc in the TUI fires the same interrupt semantics as local mode.
fn spawn_cancel_watch(
&self,
conversation_id: &str,
cancel: tokio_util::sync::CancellationToken,
) {
let client = self.client.clone();
let url = self.url(&format!("/v1/conversations/{}/cancel", conversation_id));
let token = self.token.clone();
tokio::spawn(async move {
tokio::select! {
_ = cancel.cancelled() => {
let mut req = client.post(url);
if let Some(t) = token {
req = req.bearer_auth(t);
}
let _ = req.send().await;
}
// ponytail: task self-reaps after an hour so uncancelled
// turns don't accumulate watchers; scope to stream lifetime
// if turns ever legitimately exceed this
_ = tokio::time::sleep(Duration::from_secs(3600)) => {}
}
});
}
/// Relay locally-queued interjections to the server mid-turn.
/// ponytail: 250ms polling pump bounded by cancel/1h; replace with a
/// notify-driven channel if the queue ever grows a waker.
fn spawn_interject_pump(
&self,
conversation_id: &str,
interject: super::InterjectionQueue,
cancel: tokio_util::sync::CancellationToken,
) {
let client = self.client.clone();
let url = self.url(&format!("/v1/conversations/{}/interject", conversation_id));
let token = self.token.clone();
tokio::spawn(async move {
let deadline = tokio::time::Instant::now() + Duration::from_secs(3600);
loop {
let drained: Vec<String> = {
let mut q = interject.lock().unwrap();
q.drain(..).collect()
};
for text in drained {
let mut req = client.post(&url).json(&serde_json::json!({ "text": text }));
if let Some(ref t) = token {
req = req.bearer_auth(t);
}
let _ = req.send().await;
}
tokio::select! {
_ = cancel.cancelled() => break,
_ = tokio::time::sleep(Duration::from_millis(250)) => {}
}
if tokio::time::Instant::now() >= deadline {
break;
}
}
});
}
}
#[async_trait]
impl Backend for RemoteBackend {
async fn health(&self) -> bool {
match self
.client
.get(self.url("/health"))
.timeout(Duration::from_millis(800))
.send()
.await
{
Ok(r) => r.status().is_success(),
Err(_) => false,
}
}
async fn server_status(&self) -> Option<crate::server::BootInfo> {
self.client
.get(self.url("/v1/server/status"))
.timeout(Duration::from_millis(800))
.send()
.await
.ok()?
.error_for_status()
.ok()?
.json()
.await
.ok()
}
async fn list_agents(&self) -> Result<Vec<AgentInfo>> {
#[derive(Deserialize)]
struct Wire {
id: String,
name: String,
#[serde(default)]
description: Option<String>,
}
let resp = self
.client
.get(self.url("/v1/agents"))
.send()
.await
.context("GET /v1/agents")?
.error_for_status()?;
let agents: Vec<Wire> = resp.json().await?;
Ok(agents
.into_iter()
.map(|a| AgentInfo {
id: a.id,
name: a.name,
description: a.description,
})
.collect())
}
async fn update_agent_principal(
&self,
agent_id: &str,
principal: crate::api::models::AgentPrincipalConfig,
) -> Result<()> {
let req = self
.client
.patch(self.url(&format!("/v1/agents/{agent_id}")))
.json(&serde_json::json!({ "principal": principal }));
self.auth_req(req)
.send()
.await
.context("PATCH /v1/agents/:id")?
.error_for_status()?;
Ok(())
}
async fn new_conversation(&self, agent_id: &str) -> Result<String> {
// Remote: same as ensure_conversation for now — server always creates fresh
self.ensure_conversation(agent_id).await
}
async fn list_conversations(&self, agent_id: &str) -> Result<Vec<ConversationInfo>> {
#[derive(Deserialize)]
struct Wire {
id: String,
#[serde(default)]
agent_id: Option<String>,
#[serde(default)]
summary: Option<String>,
#[serde(default)]
message_count: Option<u32>,
#[serde(default)]
updated_at: Option<String>,
}
let url = format!("/v1/conversations?agent_id={}", agent_id);
let resp = self
.auth_req(self.client.get(self.url(&url)))
.send()
.await
.context("GET /v1/conversations")?
.error_for_status()?;
let wires: Vec<Wire> = resp.json().await?;
Ok(wires
.into_iter()
.map(|w| ConversationInfo {
id: w.id,
agent_id: w.agent_id.unwrap_or_default(),
summary: w.summary,
message_count: w.message_count.unwrap_or(0),
updated_at: w.updated_at.unwrap_or_default(),
})
.collect())
}
async fn load_conversation(
&self,
conversation_id: &str,
) -> Result<Vec<crate::core::session::ConversationMessage>> {
let url = format!("/v1/conversations/{}/messages", conversation_id);
let resp = self
.auth_req(self.client.get(self.url(&url)))
.send()
.await
.context("GET /v1/conversations/:id/messages")?
.error_for_status()?;
let messages: Vec<crate::core::session::ConversationMessage> = resp.json().await?;
Ok(messages)
}
async fn fork_conversation(
&self,
_agent_id: &str,
source_conversation_id: &str,
) -> Result<String> {
#[derive(Deserialize)]
struct Wire {
id: String,
}
let url = format!("/v1/conversations/{}/fork", source_conversation_id);
let resp = self
.auth_req(self.client.post(self.url(&url)))
.send()
.await
.context("POST /v1/conversations/:id/fork")?
.error_for_status()?;
let conv: Wire = resp.json().await?;
Ok(conv.id)
}
async fn ensure_conversation(&self, agent_id: &str) -> Result<String> {
#[derive(Deserialize)]
struct Wire {
id: String,
}
let body = serde_json::json!({ "agent_id": agent_id });
let resp = self
.client
.post(self.url("/v1/conversations"))
.json(&body)
.send()
.await
.context("POST /v1/conversations")?
.error_for_status()?;
let conv: Wire = resp.json().await?;
Ok(conv.id)
}
async fn send(
&self,
conversation_id: &str,
text: &str,
) -> Result<BoxStream<'static, Result<BackendEvent>>> {
let body = serde_json::json!({
"messages": [{"role": "user", "content": text}],
"stream": true,
});
let resp = self
.auth_req(
self.client
.post(self.url(&format!("/v1/conversations/{}/messages", conversation_id))),
)
// The client-wide 60s timeout covers the whole body — it would
// sever any turn longer than a minute. Turns run long by design.
.timeout(Duration::from_secs(3600))
.json(&body)
.send()
.await
.context("POST /v1/conversations/:id/messages")?
.error_for_status()?;
let (tx, rx) = mpsc::channel::<Result<BackendEvent>>(64);
tokio::spawn(async move {
let mut bytes_stream = resp.bytes_stream();
let mut buf: Vec<u8> = Vec::new();
while let Some(chunk) = bytes_stream.next().await {
let chunk = match chunk {
Ok(c) => c,
Err(e) => {
let _ = tx.send(Err(anyhow::Error::from(e))).await;
return;
}
};
buf.extend_from_slice(&chunk);
while let Some(end) = find_frame_end(&buf) {
let frame: Vec<u8> = buf.drain(..end).collect();
// drop delimiter
if buf.starts_with(b"\r\n\r\n") {
buf.drain(..4);
} else if buf.starts_with(b"\n\n") {
buf.drain(..2);
} else {
// shouldn't happen given find_frame_end's contract
break;
}
if let Some(ev) = parse_frame(&frame) {
if tx.send(Ok(ev)).await.is_err() {
return;
}
}
}
}
if !buf.is_empty() {
if let Some(ev) = parse_frame(&buf) {
let _ = tx.send(Ok(ev)).await;
}
}
let _ = tx.send(Ok(BackendEvent::Done)).await;
});
Ok(ReceiverStream::new(rx).boxed())
}
async fn send_with_cancel(
&self,
conversation_id: &str,
text: &str,
cancel: tokio_util::sync::CancellationToken,
) -> Result<BoxStream<'static, Result<BackendEvent>>> {
self.spawn_cancel_watch(conversation_id, cancel);
self.send(conversation_id, text).await
}
async fn send_with_signals(
&self,
conversation_id: &str,
text: &str,
cancel: tokio_util::sync::CancellationToken,
interject: super::InterjectionQueue,
) -> Result<BoxStream<'static, Result<BackendEvent>>> {
self.spawn_cancel_watch(conversation_id, cancel.clone());
self.spawn_interject_pump(conversation_id, interject, cancel);
self.send(conversation_id, text).await
}
}
/// Index of the start of the SSE frame delimiter (`\n\n` or `\r\n\r\n`).
fn find_frame_end(buf: &[u8]) -> Option<usize> {
if buf.len() < 2 {
return None;
}
let mut i = 0;
while i + 1 < buf.len() {
if i + 3 < buf.len()
&& buf[i] == b'\r'
&& buf[i + 1] == b'\n'
&& buf[i + 2] == b'\r'
&& buf[i + 3] == b'\n'
{
return Some(i);
}
if buf[i] == b'\n' && buf[i + 1] == b'\n' {
return Some(i);
}
i += 1;
}
None
}
fn parse_frame(bytes: &[u8]) -> Option<BackendEvent> {
let s = std::str::from_utf8(bytes).ok()?;
let mut data = String::new();
for line in s.split('\n') {
let line = line.trim_end_matches('\r');
if let Some(v) = line.strip_prefix("data:") {
if !data.is_empty() {
data.push('\n');
}
data.push_str(v.trim_start());
}
}
if data.is_empty() {
return None;
}
// The data payload is self-describing (message_type tag) — the SSE
// event: line is redundant. Full round-trip through the wire mirror:
// every event the server emits lands here as its BackendEvent self.
let ev: crate::api::models::StreamEvent = serde_json::from_str(&data).ok()?;
Some(ev.into())
}

307
src/bin/souveraine-admit.rs Normal file
View file

@ -0,0 +1,307 @@
//! souveraine-admit — the admission executor.
//!
//! The only writer of agent Unix principals. It creates or adopts exactly one
//! account through `systemd-sysusers`, writes the root-owned agent→principal
//! mapping, and then reports the passwd facts it actually observes rather than
//! assuming its own command worked.
//!
//! What it deliberately does not do: read a human's home, install a worker
//! unit, move agent data, or recursively chown anything outside the agent's own
//! state root. Those are separate transitions. Until the worker exists, an
//! admitted agent still reads `acting-as-human` — this binary makes that state
//! reachable, it does not make it green.
//!
//! Contract: souveraine/saf/identity/02-agent-principal.md
#[path = "../core/identity/seed.rs"]
mod identity;
#[path = "../core/principal_map.rs"]
mod principal_map;
use std::path::{Path, PathBuf};
use anyhow::{bail, Context, Result};
use chrono::Utc;
use clap::{Parser, Subcommand};
use principal_map::*;
#[derive(Parser)]
#[command(
name = "souveraine-admit",
about = "Admit an agent to this node as her own Unix principal",
long_about = "Creates or adopts one Unix account for one agent, writes the root-owned\nagent-to-principal mapping, and reports what NSS says afterwards.\n\nIntent lives in the agent record and is written by the server. Admission is\nthis binary and needs root. Neither one starts a worker: until per-agent\nworkers exist, an admitted agent still reports acting-as-human."
)]
struct Cli {
/// Operate on an alternate filesystem root (image prep, tests)
#[arg(long, global = true, default_value = "/")]
root: PathBuf,
/// Speak in data
#[arg(long, global = true)]
json: bool,
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
/// Report the admission facts for one agent. Writes nothing.
Status { agent_id: String },
/// Create or adopt the account and write the mapping. Idempotent.
Apply {
agent_id: String,
/// Local account this agent runs as
#[arg(long)]
account: String,
/// The agent's SeedID public key (hex). Recorded so a copied record
/// cannot inherit an admitted account.
#[arg(long, default_value = "")]
seed_id: String,
/// Print the plan and touch nothing
#[arg(long)]
dry_run: bool,
},
}
fn main() -> Result<()> {
let cli = Cli::parse();
match &cli.command {
Command::Status { agent_id } => status(&cli, agent_id),
Command::Apply {
agent_id,
account,
seed_id,
dry_run,
} => apply(&cli, agent_id, account, seed_id, *dry_run),
}
}
fn status(cli: &Cli, agent_id: &str) -> Result<()> {
let mapping = load_node_mapping(&cli.root, agent_id);
let account = mapping
.as_ref()
.and_then(|m| account_lookup(&cli.root, &m.account));
if cli.json {
println!(
"{}",
serde_json::to_string_pretty(&serde_json::json!({
"agent_id": agent_id,
"mapping": mapping,
"account_exists": account.is_some(),
"account_uid": account.as_ref().map(|a| a.uid),
"account_shell": account.as_ref().map(|a| a.shell.clone()),
"worker_installed": false,
}))?
);
return Ok(());
}
match (&mapping, &account) {
(None, _) => println!("unadmitted — no mapping for {agent_id} on this node"),
(Some(m), None) => println!(
"principal-drift — mapped to `{}` (uid {}), which does not exist here",
m.account, m.uid
),
(Some(m), Some(a)) if a.uid != m.uid => println!(
"principal-drift — mapping says uid {} but `{}` is uid {}",
m.uid, m.account, a.uid
),
(Some(m), Some(a)) => {
println!("admitted {} → {} (uid {})", agent_id, m.account, a.uid);
println!("state root {}", m.state_root);
println!("shell {}", a.shell);
println!("seed {}", glyph_or_none(&m.agent_seed_id));
println!("worker none — turns still run as the invoking user");
}
}
Ok(())
}
fn apply(cli: &Cli, agent_id: &str, account: &str, seed_id: &str, dry_run: bool) -> Result<()> {
let alternate_root = cli.root != Path::new("/");
if !alternate_root && unsafe { libc::geteuid() } != 0 && !dry_run {
bail!("admission needs root; re-run with sudo, or use --root for an image or test tree");
}
validate_account_name(account)?;
if agent_id.is_empty() || agent_id.contains('/') || agent_id.contains("..") {
bail!("refusing agent id `{agent_id}`: it becomes a filename");
}
if !seed_id.is_empty() && (seed_id.len() != 64 || !seed_id.bytes().all(|b| b.is_ascii_hexdigit()))
{
bail!("--seed-id must be a 64-character hex public key");
}
// An account belongs to at most one agent. Adopting one that another agent
// already holds is the exact collapse per-agent principals exist to stop.
if let Some(owner) = mapping_owner_of_account(&cli.root, account) {
if owner != agent_id {
bail!("account `{account}` is already admitted to agent {owner}");
}
}
// A pre-existing mapping for a *different* account is a rename, not a
// repair. Refuse rather than strand the old account and its data.
let existing = load_node_mapping(&cli.root, agent_id);
if let Some(prior) = &existing {
if prior.account != account {
bail!(
"agent {agent_id} is already admitted as `{}`; decommission before admitting `{account}`",
prior.account
);
}
if !prior.agent_seed_id.is_empty() && !seed_id.is_empty() && prior.agent_seed_id != seed_id {
bail!("mapping records a different SeedID for {agent_id}; this is identity drift, not a repair");
}
}
// Adopting an existing human login would hand an agent a person's account.
if let Some(found) = account_lookup(&cli.root, account) {
if found.uid >= HUMAN_UID_FLOOR && existing.is_none() {
bail!(
"`{account}` already exists as uid {} — that is a login account, not a free agent principal",
found.uid
);
}
}
let state_root = agent_state_root(&cli.root, agent_id);
let state_root_abs = Path::new("/")
.join(AGENT_STATE_ROOT)
.join(agent_id)
.to_string_lossy()
.into_owned();
let drop_in = sysusers_drop_in_path(&cli.root, agent_id);
let declaration = format!(
"# Generated by souveraine-admit for agent {agent_id}. Do not hand-edit:\n\
# the mapping in /{PRINCIPAL_MAP_DIR}/{agent_id}.json is the record of\n\
# truth and health compares this file against it.\n\
u {account} - \"Souveraine agent {agent_id}\" {state_root_abs} /usr/bin/nologin\n"
);
if dry_run {
println!("would write {}", drop_in.display());
print!("{declaration}");
println!("would run systemd-sysusers {}", drop_in.display());
println!("would write {}", node_mapping_path(&cli.root, agent_id).display());
println!("would own {} as {account}", state_root.display());
return Ok(());
}
write_file(&drop_in, &declaration, 0o644)?;
run_sysusers(&cli.root, &drop_in)?;
// Report what the system says, never what the command intended.
let created = account_lookup(&cli.root, account).ok_or_else(|| {
anyhow::anyhow!("systemd-sysusers reported success but `{account}` still does not resolve")
})?;
if created.shell != "/usr/bin/nologin" {
bail!(
"`{account}` resolved with shell {} — refusing to record a login-capable agent principal",
created.shell
);
}
let mapping = NodePrincipalMapping {
agent_id: agent_id.to_string(),
agent_seed_id: if seed_id.is_empty() {
existing.as_ref().map(|p| p.agent_seed_id.clone()).unwrap_or_default()
} else {
seed_id.to_string()
},
account: created.name.clone(),
uid: created.uid,
node_id: node_id(&cli.root),
state_root: state_root_abs,
admitted_at: existing.as_ref().map(|p| p.admitted_at).unwrap_or_else(Utc::now),
};
write_file(
&node_mapping_path(&cli.root, agent_id),
&serde_json::to_string_pretty(&mapping)?,
0o644,
)?;
// Ownership is applied inside her own root only. Never a human home, never
// a recursive pass over ~/.souveraine.
std::fs::create_dir_all(&state_root)
.with_context(|| format!("creating {}", state_root.display()))?;
if unsafe { libc::geteuid() } == 0 {
chown(&state_root, created.uid)?;
restrict(&state_root, 0o700)?;
}
if cli.json {
println!("{}", serde_json::to_string_pretty(&mapping)?);
} else {
println!("admitted {} → {} (uid {})", agent_id, mapping.account, mapping.uid);
println!("state root {}", mapping.state_root);
println!("seed {}", glyph_or_none(&mapping.agent_seed_id));
println!("worker none — her turns still run as whoever invokes them");
}
Ok(())
}
fn run_sysusers(root: &Path, drop_in: &Path) -> Result<()> {
let mut cmd = std::process::Command::new("systemd-sysusers");
if root != Path::new("/") {
cmd.arg(format!("--root={}", root.display()));
}
cmd.arg(drop_in);
let out = cmd.output().context("running systemd-sysusers")?;
if !out.status.success() {
bail!(
"systemd-sysusers failed: {}",
String::from_utf8_lossy(&out.stderr).trim()
);
}
Ok(())
}
fn write_file(path: &Path, contents: &str, mode: u32) -> Result<()> {
use std::os::unix::fs::PermissionsExt;
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).with_context(|| format!("creating {}", parent.display()))?;
}
std::fs::write(path, contents).with_context(|| format!("writing {}", path.display()))?;
std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode))?;
Ok(())
}
fn restrict(path: &Path, mode: u32) -> Result<()> {
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode))?;
Ok(())
}
fn chown(path: &Path, uid: u32) -> Result<()> {
use std::ffi::CString;
use std::os::unix::ffi::OsStrExt;
let c = CString::new(path.as_os_str().as_bytes())?;
if unsafe { libc::chown(c.as_ptr(), uid, uid) } != 0 {
bail!(
"chown {} to {uid}: {}",
path.display(),
std::io::Error::last_os_error()
);
}
Ok(())
}
fn node_id(root: &Path) -> String {
std::fs::read_to_string(root.join("etc/hostname"))
.ok()
.map(|v| v.trim().to_string())
.filter(|v| !v.is_empty())
.unwrap_or_else(|| "unknown".to_string())
}
fn glyph_or_none(seed_hex: &str) -> String {
if seed_hex.is_empty() {
return "none recorded".to_string();
}
let bytes: Vec<u8> = (0..seed_hex.len().min(4))
.step_by(2)
.filter_map(|i| u8::from_str_radix(&seed_hex[i..i + 2], 16).ok())
.collect();
format!("{} {}", identity::glyph_from_pubkey(&bytes), &seed_hex[..16])
}

View file

@ -0,0 +1,898 @@
//! souveraine-agentsd — one collector for every agent's session activity.
//!
//! COLLECTOR, NOT AN AUTHORITY. It reads what the agents already write to disk
//! and offers a single projection of it. It decides nothing, actuates nothing,
//! and approves nothing. Every surface that wants to know "what are my agents
//! doing" reads this socket instead of growing its own fetch.
//!
//! ## Why this exists (TASK-69)
//!
//! Before this, the bar owned a fetch per provider: `ClaudeUsage.qml` polled an
//! OAuth endpoint on its own timer, a python collector shelled out to
//! `codex app-server`, and the substrate's own token counters were read by
//! nothing at all. Three cadences, three vocabularies, three ways to be stale,
//! and no single answer to "how much have I spent today". The bar should show
//! projections of one live stream, not run three clients.
//!
//! ## Naming — this is NOT souveraine-sessiond
//!
//! `souveraine-sessiond` is the device's proprioception: the authority for what
//! the machine believes about itself. This daemon is about *agent* sessions —
//! conversations with Claude, Codex and Souveraine. The names are close and the
//! concepts are unrelated. The socket is `souveraine-sessions.sock` to keep the
//! distinction visible at the wire.
//!
//! ## What it reads (verified against real records 2026-08-11)
//!
//! | Provider | Path | Record |
//! |---|---|---|
//! | Claude | `~/.claude/projects/<slug>/<session>.jsonl` | `type=="assistant"`, `.message.usage`, `.message.model`, `.cwd` |
//! | Codex | `~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl` | `type=="session_meta"` for `.payload.cwd`; `event_msg` with `.payload.type=="token_count"` for usage + `rate_limits` |
//! | Souveraine | `~/.souveraine/server/agents/<id>/conversations/<c>/messages.jsonl` | `role=="assistant"`, `.usage` |
//!
//! **Correction to TASK-69's table, found in the data:** the Souveraine row
//! claimed the substrate's usage counters were never written. They are. Every
//! assistant line carries `input_tokens` / `output_tokens` /
//! `cache_creation_input_tokens` / `cache_read_input_tokens`. What was missing
//! was a *consumer*, which is this. TASK-67's blindness was in the surface, not
//! in the recording.
//!
//! ## On summed input tokens
//!
//! Claude and Souveraine record usage per assistant message, and every message
//! re-sends the whole context. Summing `input_tokens` across a session
//! therefore counts the same context many times over — that number is a
//! *billing* quantity, not a measure of how much was said. It is reported as
//! `tokens.input` because that is what it is, and `last_turn` is carried beside
//! it for the honest "what did that cost" reading. Codex sidesteps this by
//! recording a running `total_token_usage` itself, which is used as-is.
use std::fs;
use std::io::Write;
use std::os::unix::net::{UnixListener, UnixStream};
use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use serde::Serialize;
/// Wire version. Bumped when the envelope changes shape incompatibly; surfaces
/// that read a version they do not know should degrade, not guess.
const WIRE_VERSION: u32 = 1;
/// How often the snapshot is rebuilt and pushed to every connected client.
const REFRESH: Duration = Duration::from_secs(5);
/// Files untouched for longer than this are not read at all. Without it, a
/// scan walks every conversation ever held — hundreds of files, tens of
/// megabytes — to answer a question that is only ever about today.
const SCAN_WINDOW: Duration = Duration::from_secs(60 * 60 * 24);
/// Activity more recent than this means the agent is mid-turn.
const WORKING_WITHIN: Duration = Duration::from_secs(90);
/// Beyond this a session is still listed but is plainly not in use.
const IDLE_WITHIN: Duration = Duration::from_secs(60 * 30);
// ---------------------------------------------------------------------------
// The envelope — one JSON shape for all three providers.
// ---------------------------------------------------------------------------
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum SessionState {
/// Wrote something within `WORKING_WITHIN`.
Working,
/// Alive today, quiet now.
Idle,
/// Inside the scan window but past `IDLE_WITHIN`.
Stale,
}
impl SessionState {
/// Ranking used to sort the list. The surface shows the head of it, so this
/// ordering *is* the "which session matters right now" policy.
fn rank(&self) -> u8 {
match self {
SessionState::Working => 0,
SessionState::Idle => 1,
SessionState::Stale => 2,
}
}
fn from_age(age: Duration) -> Self {
if age <= WORKING_WITHIN {
SessionState::Working
} else if age <= IDLE_WITHIN {
SessionState::Idle
} else {
SessionState::Stale
}
}
}
#[derive(Debug, Clone, Default, Serialize, PartialEq, Eq)]
pub struct Tokens {
pub input: u64,
pub output: u64,
pub cache_read: u64,
pub cache_write: u64,
pub reasoning: u64,
/// Total for the most recent turn only — the honest per-turn cost, free of
/// the context-resend double count described in the module docs.
pub last_turn: u64,
}
impl Tokens {
/// Everything that was actually paid for, once. Cached reads are excluded
/// because they are the cheap path and including them makes a long session
/// look catastrophic.
pub fn billed(&self) -> u64 {
self.input + self.output + self.cache_write
}
}
/// A provider-reported usage window (Codex's `rate_limits`, and later Claude's
/// OAuth utilization). Optional everywhere — a provider that does not report
/// limits is not broken.
#[derive(Debug, Clone, Serialize, PartialEq)]
pub struct Limit {
pub label: String,
pub used_percent: f64,
/// Epoch milliseconds, or 0 when the provider did not say.
pub resets_at_ms: u64,
}
#[derive(Debug, Clone, Serialize, PartialEq)]
pub struct AgentSession {
pub id: String,
pub provider: String,
/// Short human label — the working directory's basename, which is what a
/// person actually recognises a session by.
pub label: String,
pub cwd: String,
pub model: String,
pub state: SessionState,
pub last_activity_ms: u64,
pub turns: u32,
pub tokens: Tokens,
}
#[derive(Debug, Clone, Serialize)]
pub struct Provider {
pub id: String,
/// False when the provider's root does not exist or could not be read. The
/// surface shows "unavailable" for this one provider and keeps the others.
pub available: bool,
pub error: Option<String>,
pub sessions: Vec<AgentSession>,
pub limits: Vec<Limit>,
}
#[derive(Debug, Clone, Serialize)]
pub struct Envelope {
pub v: u32,
pub generated_at_ms: u64,
pub providers: Vec<Provider>,
}
// ---------------------------------------------------------------------------
// Parsers. Each is a pure function over the file's text, so the whole
// collection logic is testable without a filesystem or a live agent.
// ---------------------------------------------------------------------------
fn iso_to_ms(s: &str) -> Option<u64> {
chrono::DateTime::parse_from_rfc3339(s)
.ok()
.map(|t| t.timestamp_millis().max(0) as u64)
}
fn basename(p: &str) -> String {
Path::new(p)
.file_name()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| p.to_string())
}
fn u64_at(v: &serde_json::Value, key: &str) -> u64 {
v.get(key).and_then(|x| x.as_u64()).unwrap_or(0)
}
/// Claude Code's per-project transcript.
///
/// Model is taken from the last assistant line that names a real one:
/// `<synthetic>` appears on locally-generated messages (interrupts, tool
/// plumbing) and is not the model the human is talking to.
pub fn parse_claude(id: &str, text: &str) -> Option<AgentSession> {
let mut cwd = String::new();
let mut model = String::new();
let mut last_ms = 0u64;
let mut turns = 0u32;
let mut tokens = Tokens::default();
for line in text.lines() {
let Ok(v) = serde_json::from_str::<serde_json::Value>(line) else {
continue;
};
if let Some(c) = v.get("cwd").and_then(|c| c.as_str()) {
if !c.is_empty() {
cwd = c.to_string();
}
}
if v.get("type").and_then(|t| t.as_str()) != Some("assistant") {
continue;
}
let msg = v.get("message").unwrap_or(&serde_json::Value::Null);
if let Some(m) = msg.get("model").and_then(|m| m.as_str()) {
if !m.is_empty() && m != "<synthetic>" {
model = m.to_string();
}
}
if let Some(ts) = v.get("timestamp").and_then(|t| t.as_str()) {
if let Some(ms) = iso_to_ms(ts) {
last_ms = last_ms.max(ms);
}
}
if let Some(u) = msg.get("usage") {
let turn_in = u64_at(u, "input_tokens");
let turn_out = u64_at(u, "output_tokens");
let turn_cw = u64_at(u, "cache_creation_input_tokens");
let turn_cr = u64_at(u, "cache_read_input_tokens");
tokens.input += turn_in;
tokens.output += turn_out;
tokens.cache_write += turn_cw;
tokens.cache_read += turn_cr;
let turn_total = turn_in + turn_out + turn_cw;
if turn_total > 0 {
tokens.last_turn = turn_total;
turns += 1;
}
}
}
if last_ms == 0 && turns == 0 {
return None;
}
Some(AgentSession {
id: id.to_string(),
provider: "claude".into(),
label: basename(&cwd),
cwd,
model,
state: SessionState::Stale, // replaced once the clock is applied
last_activity_ms: last_ms,
turns,
tokens,
})
}
/// Codex rollout files.
///
/// Codex maintains its own running total, so unlike the other two providers
/// nothing is summed here — the last `token_count` record is the answer.
pub fn parse_codex(id: &str, text: &str) -> Option<AgentSession> {
let mut cwd = String::new();
let mut model = String::new();
let mut session_id = id.to_string();
let mut last_ms = 0u64;
let mut turns = 0u32;
let mut tokens = Tokens::default();
for line in text.lines() {
let Ok(v) = serde_json::from_str::<serde_json::Value>(line) else {
continue;
};
let outer = v.get("type").and_then(|t| t.as_str()).unwrap_or("");
let payload = v.get("payload").unwrap_or(&serde_json::Value::Null);
if let Some(ts) = v.get("timestamp").and_then(|t| t.as_str()) {
if let Some(ms) = iso_to_ms(ts) {
last_ms = last_ms.max(ms);
}
}
// The record kind lives on the outer envelope for session_meta and on
// the payload for event_msg. Checking both is not defensive
// over-engineering; it is the actual shape.
if outer == "session_meta" {
if let Some(c) = payload.get("cwd").and_then(|c| c.as_str()) {
cwd = c.to_string();
}
if let Some(s) = payload.get("id").and_then(|c| c.as_str()) {
session_id = s.to_string();
}
}
if payload.get("type").and_then(|t| t.as_str()) != Some("token_count") {
continue;
}
turns += 1;
if let Some(info) = payload.get("info") {
if let Some(t) = info.get("total_token_usage") {
tokens.input = u64_at(t, "input_tokens");
tokens.output = u64_at(t, "output_tokens");
tokens.cache_read = u64_at(t, "cached_input_tokens");
tokens.cache_write = u64_at(t, "cache_write_input_tokens");
tokens.reasoning = u64_at(t, "reasoning_output_tokens");
}
if let Some(t) = info.get("last_token_usage") {
tokens.last_turn = u64_at(t, "total_tokens");
}
}
if let Some(m) = payload.get("model").and_then(|m| m.as_str()) {
model = m.to_string();
}
}
if last_ms == 0 && turns == 0 {
return None;
}
Some(AgentSession {
id: session_id,
provider: "codex".into(),
label: basename(&cwd),
cwd,
model,
state: SessionState::Stale,
last_activity_ms: last_ms,
turns,
tokens,
})
}
/// The rate-limit windows, which belong to the *provider* rather than to any
/// one session — every Codex rollout reports the same account-wide numbers, so
/// hanging them off a session would duplicate them once per open terminal.
pub fn codex_limits(text: &str) -> Vec<Limit> {
let mut out = Vec::new();
for line in text.lines() {
let Ok(v) = serde_json::from_str::<serde_json::Value>(line) else {
continue;
};
let payload = v.get("payload").unwrap_or(&serde_json::Value::Null);
if payload.get("type").and_then(|t| t.as_str()) != Some("token_count") {
continue;
}
let Some(rl) = payload.get("rate_limits") else {
continue;
};
let mut found = Vec::new();
for (key, label) in [("primary", "primary"), ("secondary", "secondary")] {
let Some(w) = rl.get(key) else { continue };
if w.is_null() {
continue;
}
found.push(Limit {
label: label.to_string(),
used_percent: w
.get("used_percent")
.and_then(|u| u.as_f64())
.unwrap_or(0.0),
resets_at_ms: w
.get("resets_at")
.and_then(|r| r.as_u64())
.map(|s| s.saturating_mul(1000))
.unwrap_or(0),
});
}
if !found.is_empty() {
out = found;
}
}
out
}
/// The substrate's own persisted conversations.
pub fn parse_souveraine(id: &str, label: &str, text: &str) -> Option<AgentSession> {
let mut last_ms = 0u64;
let mut turns = 0u32;
let mut tokens = Tokens::default();
for line in text.lines() {
let Ok(v) = serde_json::from_str::<serde_json::Value>(line) else {
continue;
};
if let Some(ts) = v.get("timestamp").and_then(|t| t.as_str()) {
if let Some(ms) = iso_to_ms(ts) {
last_ms = last_ms.max(ms);
}
}
if v.get("role").and_then(|r| r.as_str()) != Some("assistant") {
continue;
}
let Some(u) = v.get("usage") else { continue };
if u.is_null() {
continue;
}
let turn_in = u64_at(u, "input_tokens");
let turn_out = u64_at(u, "output_tokens");
let turn_cw = u64_at(u, "cache_creation_input_tokens");
let turn_cr = u64_at(u, "cache_read_input_tokens");
tokens.input += turn_in;
tokens.output += turn_out;
tokens.cache_write += turn_cw;
tokens.cache_read += turn_cr;
let turn_total = turn_in + turn_out + turn_cw;
if turn_total > 0 {
tokens.last_turn = turn_total;
turns += 1;
}
}
if last_ms == 0 && turns == 0 {
return None;
}
Some(AgentSession {
id: id.to_string(),
provider: "souveraine".into(),
label: label.to_string(),
cwd: String::new(),
model: String::new(),
state: SessionState::Stale,
last_activity_ms: last_ms,
turns,
tokens,
})
}
// ---------------------------------------------------------------------------
// Collection — the filesystem half.
// ---------------------------------------------------------------------------
fn home() -> PathBuf {
dirs::home_dir().unwrap_or_else(|| PathBuf::from("/"))
}
fn now_ms() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0)
}
/// Recursively gather `*.jsonl` under `root` that were modified inside the scan
/// window. Depth-limited: these trees are shallow by construction, and an
/// unbounded walk over a symlinked home is a hazard, not a feature.
fn recent_jsonl(root: &Path, depth: usize, out: &mut Vec<PathBuf>) {
if depth == 0 {
return;
}
let Ok(entries) = fs::read_dir(root) else {
return;
};
let cutoff = SystemTime::now() - SCAN_WINDOW;
for e in entries.flatten() {
let path = e.path();
let Ok(meta) = e.metadata() else { continue };
if meta.is_dir() {
recent_jsonl(&path, depth - 1, out);
} else if path.extension().and_then(|x| x.to_str()) == Some("jsonl")
&& meta.modified().map(|m| m >= cutoff).unwrap_or(false)
{
out.push(path);
}
}
}
fn stem(p: &Path) -> String {
p.file_stem()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_default()
}
fn collect_claude() -> Provider {
let root = home().join(".claude/projects");
let mut p = Provider {
id: "claude".into(),
available: root.is_dir(),
error: None,
sessions: Vec::new(),
limits: Vec::new(),
};
if !p.available {
p.error = Some("no ~/.claude/projects".into());
return p;
}
let mut files = Vec::new();
recent_jsonl(&root, 3, &mut files);
for f in files {
let Ok(text) = fs::read_to_string(&f) else {
continue;
};
if let Some(s) = parse_claude(&stem(&f), &text) {
p.sessions.push(s);
}
}
p
}
fn collect_codex() -> Provider {
let root = home().join(".codex/sessions");
let mut p = Provider {
id: "codex".into(),
available: root.is_dir(),
error: None,
sessions: Vec::new(),
limits: Vec::new(),
};
if !p.available {
p.error = Some("no ~/.codex/sessions".into());
return p;
}
let mut files = Vec::new();
// year/month/day/file — four levels below the root.
recent_jsonl(&root, 5, &mut files);
files.sort();
for f in &files {
let Ok(text) = fs::read_to_string(f) else {
continue;
};
if let Some(s) = parse_codex(&stem(f), &text) {
p.sessions.push(s);
}
let l = codex_limits(&text);
if !l.is_empty() {
// Files are sorted by name, which for rollout-<ISO>-<uuid> is
// chronological, so the last non-empty reading wins.
p.limits = l;
}
}
p
}
fn collect_souveraine() -> Provider {
let root = home().join(".souveraine/server/agents");
let mut p = Provider {
id: "souveraine".into(),
available: root.is_dir(),
error: None,
sessions: Vec::new(),
limits: Vec::new(),
};
if !p.available {
p.error = Some("no ~/.souveraine/server/agents".into());
return p;
}
let Ok(agents) = fs::read_dir(&root) else {
p.available = false;
p.error = Some("unreadable".into());
return p;
};
for agent in agents.flatten() {
let convs = agent.path().join("conversations");
if !convs.is_dir() {
continue;
}
let agent_label = agent.file_name().to_string_lossy().to_string();
let mut files = Vec::new();
recent_jsonl(&convs, 2, &mut files);
for f in files {
let Ok(text) = fs::read_to_string(&f) else {
continue;
};
// The conversation id is the directory, not the file — every file
// here is called messages.jsonl.
let conv = f
.parent()
.map(|d| {
d.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string()
})
.unwrap_or_default();
if let Some(s) = parse_souveraine(&conv, &agent_label, &text) {
p.sessions.push(s);
}
}
}
p
}
/// Apply the clock and the ordering. Kept separate from parsing so the ranking
/// policy can be tested against a fixed `now` instead of against real time.
pub fn finish(providers: &mut [Provider], now: u64) {
for p in providers.iter_mut() {
for s in p.sessions.iter_mut() {
let age = Duration::from_millis(now.saturating_sub(s.last_activity_ms));
s.state = SessionState::from_age(age);
}
p.sessions.sort_by(|a, b| {
a.state
.rank()
.cmp(&b.state.rank())
.then(b.last_activity_ms.cmp(&a.last_activity_ms))
});
}
}
pub fn snapshot() -> Envelope {
let mut providers = vec![collect_claude(), collect_codex(), collect_souveraine()];
let now = now_ms();
finish(&mut providers, now);
Envelope {
v: WIRE_VERSION,
generated_at_ms: now,
providers,
}
}
// ---------------------------------------------------------------------------
// The socket.
// ---------------------------------------------------------------------------
fn socket_path() -> PathBuf {
let dir = std::env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| {
// Safe fallback rather than a panic: a daemon that refuses to start
// because an env var is missing is a daemon that silently never runs.
format!("/run/user/{}", unsafe { libc_getuid() })
});
PathBuf::from(dir).join("souveraine-sessions.sock")
}
/// The one libc call needed, declared locally rather than pulling a dependency
/// in for a single uid lookup.
unsafe fn libc_getuid() -> u32 {
unsafe extern "C" {
fn getuid() -> u32;
}
unsafe { getuid() }
}
fn serve_client(mut stream: UnixStream, latest: Arc<RwLock<String>>) {
// Push immediately so a surface renders on connect rather than after the
// first tick — a bar that is blank for five seconds after login reads as
// broken.
let mut last_sent = String::new();
loop {
let payload = match latest.read() {
Ok(g) => g.clone(),
Err(_) => return,
};
if payload != last_sent {
if stream.write_all(payload.as_bytes()).is_err() {
return;
}
if stream.write_all(b"\n").is_err() {
return;
}
if stream.flush().is_err() {
return;
}
last_sent = payload;
}
std::thread::sleep(Duration::from_millis(500));
}
}
fn main() {
let path = socket_path();
// A stale socket from an unclean exit refuses bind with EADDRINUSE, which
// reads exactly like "already running" and is not.
if path.exists() {
if UnixStream::connect(&path).is_ok() {
eprintln!("souveraine-agentsd: already running at {}", path.display());
std::process::exit(1);
}
let _ = fs::remove_file(&path);
}
let listener = match UnixListener::bind(&path) {
Ok(l) => l,
Err(e) => {
eprintln!("souveraine-agentsd: cannot bind {}: {e}", path.display());
std::process::exit(1);
}
};
eprintln!("souveraine-agentsd: listening on {}", path.display());
let latest = Arc::new(RwLock::new(
serde_json::to_string(&snapshot()).unwrap_or_else(|_| "{}".into()),
));
{
let latest = Arc::clone(&latest);
std::thread::spawn(move || loop {
std::thread::sleep(REFRESH);
let json = match serde_json::to_string(&snapshot()) {
Ok(j) => j,
Err(e) => {
eprintln!("souveraine-agentsd: serialize failed: {e}");
continue;
}
};
if let Ok(mut g) = latest.write() {
*g = json;
}
});
}
for stream in listener.incoming() {
match stream {
Ok(s) => {
let latest = Arc::clone(&latest);
std::thread::spawn(move || serve_client(s, latest));
}
Err(e) => eprintln!("souveraine-agentsd: accept failed: {e}"),
}
}
}
// ---------------------------------------------------------------------------
// Tests. Fixtures are trimmed copies of real records, not invented shapes —
// a parser tested against a fixture I made up only proves I am consistent with
// myself.
// ---------------------------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
const CLAUDE: &str = r#"
{"type":"user","cwd":"/srv/project","timestamp":"2026-08-11T12:00:00.000Z"}
{"type":"assistant","timestamp":"2026-08-11T12:00:05.000Z","cwd":"/srv/project","sessionId":"abc","message":{"model":"claude-opus-5","usage":{"input_tokens":100,"output_tokens":50,"cache_creation_input_tokens":10,"cache_read_input_tokens":900}}}
{"type":"assistant","timestamp":"2026-08-11T12:01:00.000Z","cwd":"/srv/project","sessionId":"abc","message":{"model":"<synthetic>","usage":{"input_tokens":200,"output_tokens":20,"cache_creation_input_tokens":0,"cache_read_input_tokens":1000}}}
"#;
const CODEX: &str = r#"
{"ordinal":1,"type":"session_meta","timestamp":"2026-08-11T11:18:17.000Z","payload":{"cwd":"/srv","id":"019ff167","model_provider":"openai"}}
{"ordinal":2,"type":"event_msg","timestamp":"2026-08-11T11:20:00.000Z","payload":{"type":"token_count","info":{"total_token_usage":{"input_tokens":23083358,"cached_input_tokens":22425856,"cache_write_input_tokens":0,"output_tokens":70558,"reasoning_output_tokens":26913,"total_tokens":23153916},"last_token_usage":{"total_tokens":97139}},"rate_limits":{"primary":{"used_percent":13.0,"window_minutes":10080,"resets_at":1787066336},"secondary":null}}}
"#;
const SOUVERAINE: &str = r#"
{"role":"user","timestamp":"2026-08-11T17:50:00.000000000Z","blocks":[]}
{"role":"assistant","timestamp":"2026-08-11T17:53:04.004516645Z","usage":{"input_tokens":413961,"output_tokens":7177,"cache_creation_input_tokens":60480,"cache_read_input_tokens":353465}}
{"role":"system","timestamp":null,"usage":null}
"#;
#[test]
fn claude_sums_usage_and_ignores_the_synthetic_model() {
let s = parse_claude("abc", CLAUDE).expect("a session");
assert_eq!(s.provider, "claude");
assert_eq!(s.label, "souveraine", "label is the cwd basename");
// The synthetic line still costs tokens, so it counts toward usage —
// it just must not be mistaken for the model in use.
assert_eq!(s.model, "claude-opus-5");
assert_eq!(s.tokens.input, 300);
assert_eq!(s.tokens.output, 70);
assert_eq!(s.tokens.cache_write, 10);
assert_eq!(s.tokens.cache_read, 1900);
assert_eq!(s.turns, 2);
assert_eq!(s.tokens.last_turn, 220);
}
#[test]
fn codex_takes_its_own_running_total_rather_than_summing() {
let s = parse_codex("file-stem", CODEX).expect("a session");
assert_eq!(s.id, "019ff167", "session_meta id beats the filename");
assert_eq!(s.cwd, "/srv");
assert_eq!(s.tokens.input, 23_083_358);
assert_eq!(s.tokens.reasoning, 26_913);
assert_eq!(s.tokens.last_turn, 97_139);
}
#[test]
fn codex_rate_limit_seconds_become_milliseconds_at_the_boundary() {
let l = codex_limits(CODEX);
assert_eq!(l.len(), 1, "a null secondary window is not a window");
assert_eq!(l[0].label, "primary");
assert_eq!(l[0].used_percent, 13.0);
assert_eq!(l[0].resets_at_ms, 1_787_066_336_000);
}
#[test]
fn souveraine_usage_is_recorded_despite_what_task_69_claimed() {
let s = parse_souveraine("conv-1", "agent-x", SOUVERAINE).expect("a session");
assert_eq!(s.tokens.output, 7177);
assert_eq!(s.tokens.cache_write, 60480);
assert_eq!(s.turns, 1);
// A null-usage system line must not be counted as a turn.
assert_eq!(s.tokens.last_turn, 481_618);
}
#[test]
fn a_transcript_with_no_activity_yields_no_session() {
assert!(parse_claude("x", "").is_none());
assert!(parse_codex("x", "not json at all\n").is_none());
assert!(parse_souveraine("x", "y", "{}").is_none());
}
#[test]
fn malformed_lines_are_skipped_not_fatal() {
let mixed = format!("garbage not json\n{}\n\n", CLAUDE.trim());
let s = parse_claude("abc", &mixed).expect("still parses the good lines");
assert_eq!(s.turns, 2);
}
#[test]
fn working_outranks_idle_outranks_stale() {
let now = 1_000_000_000u64;
let mk = |id: &str, ago_s: u64| AgentSession {
id: id.into(),
provider: "claude".into(),
label: id.into(),
cwd: String::new(),
model: String::new(),
state: SessionState::Stale,
last_activity_ms: now - ago_s * 1000,
turns: 1,
tokens: Tokens::default(),
};
let mut ps = vec![Provider {
id: "claude".into(),
available: true,
error: None,
sessions: vec![mk("stale", 60 * 60), mk("working", 10), mk("idle", 60 * 10)],
limits: Vec::new(),
}];
finish(&mut ps, now);
let order: Vec<&str> = ps[0].sessions.iter().map(|s| s.id.as_str()).collect();
assert_eq!(order, vec!["working", "idle", "stale"]);
assert_eq!(ps[0].sessions[0].state, SessionState::Working);
assert_eq!(ps[0].sessions[2].state, SessionState::Stale);
}
#[test]
fn equally_stale_sessions_fall_back_to_most_recent_first() {
let now = 1_000_000_000u64;
let mk = |id: &str, ago_s: u64| AgentSession {
id: id.into(),
provider: "codex".into(),
label: id.into(),
cwd: String::new(),
model: String::new(),
state: SessionState::Stale,
last_activity_ms: now - ago_s * 1000,
turns: 1,
tokens: Tokens::default(),
};
let mut ps = vec![Provider {
id: "codex".into(),
available: true,
error: None,
sessions: vec![mk("older", 20), mk("newer", 5)],
limits: Vec::new(),
}];
finish(&mut ps, now);
assert_eq!(ps[0].sessions[0].id, "newer");
}
#[test]
fn billed_excludes_cache_reads() {
let t = Tokens {
input: 100,
output: 50,
cache_read: 900_000,
cache_write: 10,
reasoning: 0,
last_turn: 0,
};
assert_eq!(
t.billed(),
160,
"a cheap cached read must not read as spend"
);
}
#[test]
fn the_envelope_serialises_with_a_version_surfaces_can_check() {
let env = Envelope {
v: WIRE_VERSION,
generated_at_ms: 42,
providers: vec![Provider {
id: "claude".into(),
available: false,
error: Some("no ~/.claude/projects".into()),
sessions: Vec::new(),
limits: Vec::new(),
}],
};
let json = serde_json::to_string(&env).expect("serialises");
assert!(json.contains("\"v\":1"));
// An unavailable provider still appears, carrying its reason. Dropping
// it would make "no Claude installed" and "collector broken"
// indistinguishable at the surface.
assert!(json.contains("\"available\":false"));
assert!(json.contains("no ~/.claude/projects"));
}
}

View file

@ -0,0 +1,71 @@
//! souveraine-machined — system-tier machine identity daemon.
//!
//! Owns the machine's Ed25519 seed at `/var/lib/souveraine/seed-id` and
//! serves pubkey/sign requests over `/run/souveraine/machined.sock`. The
//! private key never crosses the socket; user-tier processes (agents, the
//! federation bridge, the shell) get signatures, not key material.
//!
//! The seed is a precondition, never something this daemon creates. If no
//! seed exists, startup fails loudly — identity creation is a deliberate,
//! guarded action that lives in `souveraine machine init`, not something a
//! background service does on your behalf. Same doctrine as
//! `souveraine-secrets`; see `SeedId::load` in `core/identity/seed.rs`.
#[path = "../core/identity/seed.rs"]
mod identity;
#[path = "../machined/protocol.rs"]
mod protocol;
#[path = "../machined/server.rs"]
mod server;
use std::path::PathBuf;
fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
)
.init();
let mut seed_dir = PathBuf::from(protocol::DEFAULT_SEED_DIR);
let mut socket = PathBuf::from(protocol::DEFAULT_SOCKET_PATH);
let mut args = std::env::args().skip(1);
while let Some(arg) = args.next() {
match arg.as_str() {
"--seed-dir" => {
seed_dir = PathBuf::from(
args.next()
.ok_or_else(|| anyhow::anyhow!("--seed-dir requires a path"))?,
);
}
"--socket" => {
socket = PathBuf::from(
args.next()
.ok_or_else(|| anyhow::anyhow!("--socket requires a path"))?,
);
}
"--help" | "-h" => {
println!(
"souveraine-machined [--seed-dir {}] [--socket {}]",
protocol::DEFAULT_SEED_DIR,
protocol::DEFAULT_SOCKET_PATH
);
return Ok(());
}
other => anyhow::bail!("unknown argument: {other}"),
}
}
let seed = identity::SeedId::load(&seed_dir).map_err(|e| {
anyhow::anyhow!(
"{e:#}\n\nsouveraine-machined refuses to start without a machine identity.\n\
Provision one: sudo souveraine machine init --fresh\n\
Or carry an existing identity over: sudo souveraine machine init \
--migrate-from ~/.souveraine/seed-id"
)
})?;
server::run(seed, &socket)
}

View file

@ -0,0 +1,145 @@
//! Secret Service daemon (`org.freedesktop.secrets`), rooted in the
//! machine identity instead of a passphrase-unlocked keyring. Backs any
//! libsecret client (Chatty/libcmatrix, souveraine-player, culver, our own
//! `keyring-core` credential store) without gnome-keyring or KWallet.
//!
//! Identity is a precondition, never something this daemon creates. The
//! storage key derives from a deterministic machine signature — system tier
//! (souveraine-machined) first, the legacy `~/.souveraine/seed-id` as a loud
//! transitional fallback. If neither exists, startup fails with provisioning
//! instructions (`sudo souveraine machine init --fresh`); identity creation
//! is a deliberate, guarded action, not something a background service does
//! on your behalf.
//!
//! Lock semantics — a deliberate decision, not an omission: the store
//! unlocks with the *user session*, exactly as long as this daemon runs in
//! it. The screen lock does not lock the store — background clients (music
//! streaming, incoming Matrix messages) must keep their credentials while
//! the display is off, the same stance gnome-keyring takes. Per the session
//! authority doctrine, lock state belongs to the session authority; a
//! second hand-tracked copy here would be a shadow that can disagree with
//! it. `Unlock` therefore always succeeds promptless, and `Lock` refuses
//! rather than pretends.
#[path = "../core/identity/seed.rs"]
mod identity;
#[path = "../machined/protocol.rs"]
mod machined_protocol;
#[path = "../secrets/collection.rs"]
mod collection;
#[path = "../secrets/dh.rs"]
mod dh;
#[path = "../secrets/error.rs"]
mod error;
#[path = "../secrets/item.rs"]
mod item;
#[path = "../secrets/manage.rs"]
mod manage;
#[path = "../secrets/service.rs"]
mod service;
#[path = "../secrets/session.rs"]
mod session;
#[path = "../secrets/session_object.rs"]
mod session_object;
#[path = "../secrets/storage_key.rs"]
mod storage_key;
#[path = "../secrets/store.rs"]
mod store;
#[path = "../secrets/transport.rs"]
mod transport;
#[path = "../secrets/types.rs"]
mod types;
use std::sync::{Arc, Mutex};
use anyhow::{Context, Result};
use tracing::info;
use collection::Collection;
use item::Item;
use service::SecretService;
use session::Sessions;
use store::SecretStore;
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.init();
let base = dirs::home_dir()
.context("resolving home directory")?
.join(".souveraine");
let (machine_ikm, source) = storage_key::resolve(&base)
.context("resolving the machine-rooted key material — refusing to start")?;
info!(
?source,
"machine key material resolved; the machine identity is the trust root"
);
let store_path = store::default_store_path(&base);
let secret_store = Arc::new(Mutex::new(
SecretStore::open(machine_ikm, store_path).context("opening secrets store")?,
));
let sessions = Arc::new(Sessions::new());
// Interfaces are registered via the builder BEFORE the name is claimed —
// a client whose call races our startup finds the objects already there.
let connection = zbus::connection::Builder::session()?
.serve_at(
"/org/freedesktop/secrets",
SecretService {
store: secret_store.clone(),
sessions: sessions.clone(),
},
)?
.serve_at(
collection::COLLECTION_PATH,
Collection {
store: secret_store.clone(),
sessions: sessions.clone(),
},
)?
.serve_at(
collection::DEFAULT_ALIAS_PATH,
Collection {
store: secret_store.clone(),
sessions: sessions.clone(),
},
)?
.serve_at(
manage::MANAGE_PATH,
manage::Manage {
store: secret_store.clone(),
},
)?
.name("org.freedesktop.secrets")?
.build()
.await
.context(
"connecting to session bus / claiming org.freedesktop.secrets — is another \
provider (gnome-keyring, ksecretd) still holding the name?",
)?;
let existing_ids = secret_store.lock().unwrap().all_ids();
for id in existing_ids {
connection
.object_server()
.at(
collection::item_path(&id),
Item {
id,
store: secret_store.clone(),
sessions: sessions.clone(),
},
)
.await?;
}
info!("souveraine-secrets: serving org.freedesktop.secrets on the session bus");
std::future::pending::<()>().await;
Ok(())
}

View file

@ -0,0 +1,547 @@
//! souveraine-sensord — one reporter for every source the machine expects.
//!
//! REPORTER, NOT AN AUTHORITY. It reads sensors and tells sessiond. It decides
//! nothing, actuates nothing, and reads no lock state. Every decision belongs
//! to the device state machine (`DEVICE-STATE-MACHINE.md` §1) — the whole point
//! of that document is that we had seven actors each seeing one facet and
//! acting on it blindly.
//!
//! ## Why this replaces the scripts
//!
//! `blueline-proximity-lock` is a 113-line shell script implementing a contract
//! that is genuinely subtle: heartbeat inside `SOURCE_DOWN_AFTER`, seed the
//! last value from the startup probe banner, report both edges, pass the last
//! reading between a subshell and a background loop through a file in
//! `$XDG_RUNTIME_DIR`. All of that is correct and all of it would have to be
//! copied, verbatim and by hand, into a second script for light and a third for
//! accel — §12 says decide this before writing the second one, not after the
//! third.
//!
//! Copies drift. The `blueline-*` units are exactly the "script-based random
//! stuff" that has to become one orchestrated thing.
//!
//! ## The contract it implements (§10)
//!
//! `monitor-sensor` emits only on CHANGE, so a phone on a table is silent for
//! hours and is byte-for-byte indistinguishable from a dead SLPI. Silence must
//! therefore be made meaningful: every source re-sends its last known reading
//! every `KEEPALIVE`, comfortably inside the machine's 90 s `SOURCE_DOWN_AFTER`.
//! As long as this process and the sensor behind it live, sessiond hears from
//! each source on a schedule. If it stops hearing, something is actually wrong.
//!
//! Repeats are idempotent: the machine re-derives identical evidence and skips
//! the trail write, so a keepalive can never move the state.
//!
//! Health is deliberately NOT read from `net.hadess.SensorProxy`'s
//! `HasProximity`/`HasAccelerometer`. §10 records it answering wrongly in both
//! directions — `true` for hours after the stack died, `false` after remoteproc
//! had already recovered. What can be trusted is our own experience of whether
//! readings arrive.
use std::collections::HashMap;
use std::io::{BufRead, BufReader, Read, Write};
use std::os::unix::net::UnixStream;
use std::path::PathBuf;
use std::process::{Child, Command, Stdio};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
/// Re-send interval for each source's last known reading. Must stay well under
/// the machine's `SOURCE_DOWN_AFTER` (90 s) so a healthy source gets several
/// chances to be heard before it is called down.
const KEEPALIVE: Duration = Duration::from_secs(30);
/// How long to wait for sessiond to accept and answer. Short: it answers in
/// microseconds when healthy, and a reporter must never block on the authority.
const SESSIOND_TIMEOUT: Duration = Duration::from_secs(5);
/// Backoff when `monitor-sensor` exits or the proxy has no sensors yet.
const RESPAWN_DELAY: Duration = Duration::from_secs(5);
/// How often the charge source reads the supplies.
///
/// The battery moves on a minutes scale; charging decisions (the resting
/// hysteresis of a topped-up pack) move slower still. A 30 s cadence is an
/// order of magnitude faster than anything charge does, and it lines the
/// reading up with the keepalive so the machine hears from the source on one
/// schedule. The keepalive re-sends the last reading between polls, which is
/// what keeps the source Live in the machine's health table even when the
/// value itself never changes.
const CHARGE_POLL: Duration = Duration::from_secs(30);
/// The last reading we sent per source, in the exact JSON shape sessiond's
/// `SensorInput` expects. Shared with the keepalive thread.
type LastSeen = Arc<Mutex<HashMap<&'static str, serde_json::Value>>>;
/// When each source last went out on the wire, so MIN_REPORT_INTERVAL can be
/// enforced per source rather than globally — a busy light sensor must not
/// delay a proximity edge.
type LastSent = Arc<Mutex<HashMap<&'static str, Instant>>>;
fn socket_path() -> PathBuf {
let runtime = std::env::var("XDG_RUNTIME_DIR")
.unwrap_or_else(|_| format!("/run/user/{}", unsafe { libc_getuid() }));
PathBuf::from(runtime).join("souveraine/sessiond.sock")
}
/// Avoid a libc dependency for one call.
unsafe fn libc_getuid() -> u32 {
std::fs::read_to_string("/proc/self/loginuid")
.ok()
.and_then(|s| s.trim().parse().ok())
.unwrap_or(1000)
}
/// Send one reading. Returns false when sessiond could not be reached — which
/// is information, not a fatal error: the daemon restarts independently of us.
fn report(sock: &PathBuf, source: &str, value: &serde_json::Value) -> bool {
let req = serde_json::json!({
"op": "sensor_input",
"source": source,
"value": value,
});
let attempt = || -> std::io::Result<()> {
let stream = UnixStream::connect(sock)?;
stream.set_read_timeout(Some(SESSIOND_TIMEOUT))?;
stream.set_write_timeout(Some(SESSIOND_TIMEOUT))?;
let mut w = stream.try_clone()?;
w.write_all(format!("{req}\n").as_bytes())?;
w.flush()?;
// Read the reply so the daemon is not left writing into a closed pipe.
let mut buf = [0u8; 4096];
let mut r = stream;
let _ = r.read(&mut buf)?;
Ok(())
};
match attempt() {
Ok(()) => true,
Err(e) => {
eprintln!("sessiond unreachable; {source} reading dropped: {e}");
false
}
}
}
/// One source's parse rules: the monitor-sensor flag, the change line it emits,
/// and how to turn that line into the value sessiond wants.
struct Source {
name: &'static str,
flag: &'static str,
/// Substring identifying a change line for this source.
change_marker: &'static str,
/// Substring identifying this source's startup banner, used to seed the
/// keepalive before the first change arrives. Without a seed, a phone that
/// booted onto a table stays silent and a stack that was dead from boot is
/// invisible.
banner_marker: &'static str,
parse: fn(&str, &mut ParserState) -> Option<serde_json::Value>,
}
/// `Proximity value changed: 0` / banner `=== Has proximity sensor (near: 0)`
fn parse_proximity(line: &str, _state: &mut ParserState) -> Option<serde_json::Value> {
let near = !(line.trim_end().ends_with(": 0") || line.contains("near: 0"));
Some(serde_json::json!({ "near": near }))
}
/// `Light changed: 240.000000 lux`
///
/// The wire carries `SensorValue::Light { changing, lux }` — the number and
/// the debounced edge together. The edge is what the machine's confidence
/// table believes (§9); the number is what the plexus accumulates as a level
/// and what auto-brightness will act on (TASK-47). Levels ride, edges decide,
/// and nobody upstream throws a measurement away.
fn parse_light(line: &str, state: &mut ParserState) -> Option<serde_json::Value> {
let lux: f64 = line
.rsplit(':')
.next()?
.split_whitespace()
.next()?
.parse()
.ok()?;
let now = Instant::now();
let changing = match state.last_lux {
// The startup banner is a level, not a change. Reporting "changing" on
// it would hand the confidence table a presence signal produced by
// nothing more than the reporter starting up.
None => {
state.last_lux = Some(lux);
false
}
Some(prev) => {
let delta = lux - prev;
if delta.abs() <= LIGHT_CHANGE_LUX {
// Back inside the band: whatever excursion was building did not
// last, so it never happened. This is the branch that kills the
// 0.2 pulse.
state.pending_since = None;
false
} else {
let brightening = delta > 0.0;
let needed = if brightening {
LIGHT_BRIGHTENING_DEBOUNCE
} else {
LIGHT_DARKENING_DEBOUNCE
};
// A reversal restarts the clock — an excursion that changes
// direction is a new excursion, and the new direction may owe a
// longer wait than the one already served.
let continuing =
state.pending_since.is_some() && state.pending_brightening == brightening;
if !continuing {
state.pending_since = Some(now);
state.pending_brightening = brightening;
false
} else if now.duration_since(state.pending_since.unwrap_or(now)) >= needed {
// Held past the debounce. Believe it, and move the
// reference so the next threshold measures from here.
state.last_lux = Some(lux);
state.pending_since = None;
true
} else {
false
}
}
}
};
Some(serde_json::json!({
"light": { "changing": changing, "lux": lux }
}))
}
// The accelerometer parser and its motion decay lived here. They went with the
// accel claim itself — see SOURCES for the measurement — because both existed
// only to turn iio-sensor-proxy's *orientation* into a motion edge, and there
// is no orientation stream when the source is not claimed. The inference was
// always weak (a phone carried face-up in a steady hand reported nothing),
// which is why §4 weighted it 0.3. TASK-36's SLPI batching reports motion
// directly and does not need any of this.
/// Per-source memory the parsers need to turn levels into edges.
#[derive(Default)]
struct ParserState {
/// The lux level we last REPORTED as a change. The threshold measures from
/// here, not from the last sample.
last_lux: Option<f64>,
/// An excursion past the threshold that has not yet lasted long enough to
/// be believed: when it started and whether it was brightening. Cleared the
/// moment the level falls back inside the band.
pending_since: Option<Instant>,
pending_brightening: bool,
}
/// Shared between the read loop and the keepalive snapshot.
type Parser = Arc<Mutex<ParserState>>;
/// How much lux must move before it counts as "changing", measured against the
/// last value we REPORTED, not the last sample. Comparing consecutive samples
/// is hysteresis in name only: room light drifting 200 lux over a minute moves
/// a fraction of a lux per sample and never trips any threshold, so the machine
/// would be told nothing is changing while the room visibly changes.
const LIGHT_CHANGE_LUX: f64 = 15.0;
/// How long a lux excursion must PERSIST before it counts as a change.
///
/// The level threshold alone is not hysteresis: one sample crossing it emits
/// `changing: true`, the next sample sits inside the new band and emits
/// `changing: false`, and §4's confidence table gets a 0.2 pulse that clears
/// before anything can act on it. Measured on device 2026-07-27: that pulse
/// moved `observed_confidence` 0.50 → 0.70 → 0.50 in 1.2 s, toggling
/// `promote_idle` across the 0.6 band twice.
///
/// These are not invented numbers. They are what Android ships for THIS
/// device — `config_autoBrightnessBrighteningLightDebounce` (2000) and
/// `config_autoBrightnessDarkeningLightDebounce` (4000), read out of
/// `framework-res__lineage_blueline__auto_generated_rro_vendor.apk` in the
/// LineageOS blueline image. The asymmetry is the point: a shadow crossing the
/// sensor must not be believed as fast as a lamp switching on, which is the
/// same shape as `PROXIMITY_NEAR_DEBOUNCE` / `PROXIMITY_FAR_DEBOUNCE`.
const LIGHT_BRIGHTENING_DEBOUNCE: Duration = Duration::from_millis(2000);
const LIGHT_DARKENING_DEBOUNCE: Duration = Duration::from_millis(4000);
// MOTION_WINDOW lived here: how long after an orientation change the device
// still counted as moving. Kept in the history rather than the binary, because
// the finding behind it survives the source being dropped and TASK-36 will need
// it again — measured on device 2026-07-27, boot 0 carried 165 `Moving(true)`
// against 8 `Moving(false)`, all the falses at startup. An edge-derived
// "moving" can never fall back to false on its own, so the keepalive re-sent a
// stuck `true` every 30 s, pinning accel's +0.3 into `observed_confidence` for
// the life of the process and holding the machine at or above the 0.3
// "suppress DPMS wake" band. Whatever reports motion next owes a decay.
/// No source may report more often than this.
///
/// Not a nicety. monitor-sensor emits a line per lux sample and ambient light
/// never holds still, so without a floor the reporter opened ~8 sessiond
/// connections a second, each one re-deriving identical evidence. A sensor feed
/// with no buffering is the same defect §9.5 found in proximity — a raw signal
/// handed to a consumer at whatever rate the hardware happens to produce it.
///
/// 250 ms is the shortest interval at which a change still feels immediate to a
/// person, which is the only consumer that cares about latency here.
const MIN_REPORT_INTERVAL: Duration = Duration::from_millis(250);
const SOURCES: &[Source] = &[
Source {
name: "proximity",
flag: "--proximity",
change_marker: "Proximity value changed: ",
banner_marker: "Has proximity",
parse: parse_proximity,
},
Source {
name: "light",
flag: "--light",
change_marker: "Light changed: ",
banner_marker: "Has ambient light sensor",
parse: parse_light,
},
// The accelerometer is NOT claimed, and that is a power decision.
//
// Measured on the phone 2026-08-04, idle, screen on, same session, one flag
// apart:
//
// monitor-sensor --proximity --light --accel iio-sensor-proxy 15.3%
// monitor-sensor --proximity --light iio-sensor-proxy 1.1%
//
// Claiming the accelerometer makes iio-sensor-proxy poll the IIO device
// continuously; nothing else here does. So ~14 points of a core were being
// spent, forever, on the one reading §4 gives the least weight (+0.3) and
// that §9's own note calls a weak signal — and which the keepalive had to
// actively decay because monitor-sensor only speaks on orientation change,
// meaning a stationary phone paid the full poll cost to report nothing.
//
// This is the cheap half of TASK-15. It does not say accel is unwanted: it
// says a subprocess holding a continuous claim is the wrong way to get it.
// TASK-36's SLPI batching is the right one — the sensor hub already
// aggregates motion and can report on an interval instead of being polled.
// Restore this entry only together with that, or the cost comes back.
//
// "accelerometer" was the name on the wire, not "accel": SensorSource
// derives its JSON from snake_case variant names, and the shorter form is
// the trail/health key only. Noted here so a future restore does not
// rediscover the refusal.
];
/// Re-send every source's last reading forever. This is the half that makes
/// silence meaningful; see the module docs.
fn spawn_keepalive(sock: PathBuf, last: LastSeen) {
std::thread::spawn(move || loop {
std::thread::sleep(KEEPALIVE);
// The motion decay that used to run here went with the accelerometer
// claim (see SOURCES). It existed because monitor-sensor only speaks on
// orientation change, so a stale `moving: true` would otherwise be
// re-sent forever and pin accel's +0.3 into the confidence sum. With
// the source unclaimed there is no accelerometer entry to decay, and
// leaving the decay in would have been a loop deriving a value nothing
// reports. It comes back with the source, not before.
let snapshot: Vec<(&'static str, serde_json::Value)> = {
let guard = match last.lock() {
Ok(g) => g,
Err(p) => p.into_inner(),
};
guard.iter().map(|(k, v)| (*k, v.clone())).collect()
};
for (source, value) in snapshot {
report(&sock, source, &value);
}
});
}
fn read_trimmed(dir: &std::path::Path, attr: &str) -> Option<String> {
std::fs::read_to_string(dir.join(attr))
.ok()
.map(|v| v.trim().to_owned())
}
/// Read `/sys/class/power_supply` into the wire shape of `SensorValue::Charge`.
///
/// Classify each node by its own `type` attribute — device names are not the
/// contract (blueline's are `qcom-battery` and `pmi8998-charger`; another
/// body differs). Every read is independent: one missing attribute degrades
/// one field, never the whole probe.
fn probe_charge() -> serde_json::Value {
let mut fields = serde_json::json!({
"plugged": serde_json::Value::Null,
"status": serde_json::Value::Null,
"charge_type": serde_json::Value::Null,
"capacity": serde_json::Value::Null,
});
let Ok(entries) = std::fs::read_dir("/sys/class/power_supply") else {
return fields;
};
for entry in entries.flatten() {
let dir = entry.path();
let Some(kind) = std::fs::read_to_string(dir.join("type")).ok() else {
continue;
};
if kind.trim() == "Battery" {
if let Some(status) = read_trimmed(&dir, "status") {
fields["status"] = status.into();
}
if let Ok(capacity) = std::fs::read_to_string(dir.join("capacity")) {
if let Ok(v) = capacity.trim().parse::<u8>() {
fields["capacity"] = v.into();
}
}
} else {
// Any non-battery supply asserting online counts as plugged —
// USB, mains, wireless all mean the same thing to policy.
if read_trimmed(&dir, "online").as_deref() == Some("1") {
fields["plugged"] = serde_json::Value::Bool(true);
} else if fields["plugged"].is_null() {
fields["plugged"] = serde_json::Value::Bool(false);
}
// The charger carries charge_type; the first real answer wins.
// "Unknown"/"N/A" are the driver's silence, not a reading.
if fields["charge_type"].is_null() {
if let Some(ct) = read_trimmed(&dir, "charge_type") {
if ct != "Unknown" && ct != "N/A" {
fields["charge_type"] = ct.into();
}
}
}
}
}
serde_json::json!({ "charge": fields })
}
/// Poll the supplies on a fixed cadence and report on change.
///
/// Charge is not a monitor-sensor source: it has no change lines, so it gets
/// a reader of its own instead of a parser. The last reading lands in the
/// same map the keepalive serves, so sessiond hears from the source on the
/// same schedule as proximity and light and its health table stays honest.
fn spawn_charge_poller(sock: PathBuf, last: LastSeen) {
std::thread::spawn(move || loop {
std::thread::sleep(CHARGE_POLL);
let value = probe_charge();
let unchanged = {
let mut guard = match last.lock() {
Ok(g) => g,
Err(p) => p.into_inner(),
};
let same = guard.get("charge") == Some(&value);
guard.insert("charge", value.clone());
same
};
if unchanged {
continue;
}
report(&sock, "charge", &value);
});
}
fn spawn_monitor() -> std::io::Result<Child> {
let mut cmd = Command::new("monitor-sensor");
for s in SOURCES {
cmd.arg(s.flag);
}
cmd.stdout(Stdio::piped()).stderr(Stdio::null()).spawn()
}
fn main() {
let sock = socket_path();
let last: LastSeen = Arc::new(Mutex::new(HashMap::new()));
let last_sent: LastSent = Arc::new(Mutex::new(HashMap::new()));
// Parser memory outlives each monitor-sensor generation: a respawn must not
// reset the lux reference or re-arm motion, or every SLPI blip would look
// like a fresh change.
let parser: Parser = Arc::new(Mutex::new(ParserState::default()));
spawn_keepalive(sock.clone(), Arc::clone(&last));
spawn_charge_poller(sock.clone(), Arc::clone(&last));
// One monitor-sensor for every source, respawned if it dies. Restarting the
// process is the recovery path for a sensor stack that came back after an
// SLPI reset; the keepalive keeps reporting the stale-but-last value in the
// meantime, and the machine's own freshness rules decide what that is worth.
loop {
let child = match spawn_monitor() {
Ok(c) => c,
Err(e) => {
eprintln!("could not start monitor-sensor: {e}");
std::thread::sleep(RESPAWN_DELAY);
continue;
}
};
let stdout = match child.stdout {
Some(s) => s,
None => {
std::thread::sleep(RESPAWN_DELAY);
continue;
}
};
for line in BufReader::new(stdout).lines() {
let line = match line {
Ok(l) => l,
Err(_) => break,
};
for s in SOURCES {
// Seed from the banner so the keepalive has something to send
// before the first change ever arrives.
let is_banner = line.contains(s.banner_marker);
if !is_banner && !line.contains(s.change_marker) {
continue;
}
let value = {
let mut state = match parser.lock() {
Ok(g) => g,
Err(p) => p.into_inner(),
};
match (s.parse)(&line, &mut state) {
Some(v) => v,
None => continue,
}
};
// Only report when the value the machine sees actually
// changes. Lux rides in that value, so a lux drift that does
// not trip the changing edge still gets reported — that is the
// level reaching the plexus, and MIN_REPORT_INTERVAL caps the
// resulting cadence at ~4/second instead of monitor-sensor's
// ~8. Only identical reports fall out here: a repeated value
// would otherwise be a socket round trip saying nothing
// happened, and the keepalive already guarantees sessiond
// hears from every source inside SOURCE_DOWN_AFTER.
let unchanged = {
let mut guard = match last.lock() {
Ok(g) => g,
Err(p) => p.into_inner(),
};
let same = guard.get(s.name) == Some(&value);
guard.insert(s.name, value.clone());
same
};
if unchanged {
break;
}
// The buffer. A change still has to wait its turn, so a sensor
// oscillating around its own threshold cannot flood the
// machine — the keepalive re-sends the settled value anyway,
// so nothing is lost by dropping an intermediate edge.
{
let mut sent = match last_sent.lock() {
Ok(g) => g,
Err(p) => p.into_inner(),
};
if let Some(prev) = sent.get(s.name) {
if prev.elapsed() < MIN_REPORT_INTERVAL {
break;
}
}
sent.insert(s.name, Instant::now());
}
// Report both edges. "Far" matters as much as "near": it is
// what re-enables a wake, and it is what proves the sensor is
// still alive.
report(&sock, s.name, &value);
break;
}
}
eprintln!("monitor-sensor exited; respawning");
std::thread::sleep(RESPAWN_DELAY);
}
}

View file

@ -0,0 +1,71 @@
//! souveraine-sessiond — session authority: lock-before-shell, lock-past-
//! shell-death.
//!
//! Runs on the user tier as the seat owner. Requires nothing from the seed
//! (it signs nothing yet — capability-token verification arrives with the
//! services review); it requires only a Wayland display that speaks
//! ext-session-lock-v1 and a PAM stack for its service name.
//!
//! Startup default is fail-closed: take the session lock immediately. The
//! shell later takes over via the handoff protocol (see
//! `sessiond/protocol.rs`). `--no-initial-lock` exists for bring-up and
//! laptop experiments only.
// The sessiond module tree lives in the main crate source but is compiled
// standalone into this bin, machined-style. Mounting it as `sessiond` keeps
// the modules' own `crate::sessiond::...` paths valid.
#[path = "../sessiond/mod.rs"]
pub(crate) mod sessiond;
// The somatic vocabulary, mounted the way machined mounts `identity/seed.rs`.
// It lives under `core/nervous/` because the agent side shares the types, but
// the *fields* are sessiond's — SOMATIC_NERVOUS_SYSTEM.md resolves open
// question 1 with "sessiond, period", and a second holder of the body's state
// would be the fifth blind actor. `use super::belief` inside plexus resolves
// to this crate root here and to `core::nervous` there, so one file serves
// both without a copy.
#[path = "../core/nervous/belief.rs"]
pub(crate) mod belief;
#[path = "../core/nervous/plexus.rs"]
pub(crate) mod plexus;
use std::path::PathBuf;
fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
)
.init();
let mut initial_lock = true;
let mut socket: Option<PathBuf> = None;
let mut args = std::env::args().skip(1);
while let Some(arg) = args.next() {
match arg.as_str() {
"--socket" => {
socket =
Some(PathBuf::from(args.next().ok_or_else(|| {
anyhow::anyhow!("--socket requires a path")
})?));
}
"--no-initial-lock" => initial_lock = false,
"--help" | "-h" => {
println!(
"souveraine-sessiond [--socket $XDG_RUNTIME_DIR/{}] [--no-initial-lock]",
sessiond::protocol::SOCKET_RELPATH,
);
return Ok(());
}
other => anyhow::bail!("unknown argument: {other}"),
}
}
let socket = match socket {
Some(p) => p,
None => sessiond::server::socket_path()?,
};
sessiond::server::run(initial_lock, &socket)
}

View file

@ -0,0 +1,631 @@
use std::collections::BTreeSet;
use std::fs::OpenOptions;
use std::io::Write;
use std::num::NonZeroU32;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{mpsc, Arc, Mutex};
use std::thread;
use std::time::Duration;
use anyhow::{anyhow, Context, Result};
use clap::Parser;
use gadgetry_most_foul::function::custom::{Custom, Interface};
use gadgetry_most_foul::Class;
use gud_gadget::{DisplayMode, Event, GUD_PIXEL_FORMAT_RGB565};
use softbuffer::{Context as SoftContext, Surface};
use tracing::{error, info, warn};
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
use winit::application::ApplicationHandler;
use winit::dpi::{LogicalSize, PhysicalPosition};
use winit::event::{ElementState, MouseButton, MouseScrollDelta, TouchPhase, WindowEvent};
use winit::event_loop::{ActiveEventLoop, EventLoop, EventLoopProxy};
use winit::keyboard::{KeyCode, PhysicalKey};
use winit::window::{Window, WindowAttributes, WindowId};
#[derive(Debug, Parser)]
#[command(about = "Receive a GUD display into a Wayland KVM surface")]
struct Args {
#[arg(long, default_value = "/run/souveraine-usb/ffs/gud")]
ffs_dir: PathBuf,
#[arg(long, default_value = "/run/souveraine-usb/keyboard")]
keyboard: PathBuf,
#[arg(long, default_value = "/run/souveraine-usb/pointer")]
pointer: PathBuf,
#[arg(long, default_value = "/run/souveraine-usb/gud.ready")]
ready_file: PathBuf,
#[arg(long, default_value_t = 1080)]
width: u32,
#[arg(long, default_value_t = 1920)]
height: u32,
#[arg(long, default_value = "USB KVM")]
title: String,
}
#[derive(Debug)]
enum UserEvent {
Frame,
GudStopped(String),
}
struct Frame {
width: u32,
height: u32,
rgb565: Vec<u8>,
xrgb8888: Vec<u32>,
generation: u64,
}
impl Frame {
fn new(width: u32, height: u32) -> Self {
let pixels = width as usize * height as usize;
Self {
width,
height,
rgb565: vec![0; pixels * 2],
xrgb8888: vec![0xff10_1010; pixels],
generation: 0,
}
}
fn convert_rect(&mut self, x: u32, y: u32, width: u32, height: u32) {
let end_x = x.saturating_add(width).min(self.width);
let end_y = y.saturating_add(height).min(self.height);
for row in y..end_y {
for column in x..end_x {
let pixel = row as usize * self.width as usize + column as usize;
let byte = pixel * 2;
let raw = u16::from_le_bytes([self.rgb565[byte], self.rgb565[byte + 1]]);
let r = u32::from((raw >> 11) & 0x1f) * 255 / 31;
let g = u32::from((raw >> 5) & 0x3f) * 255 / 63;
let b = u32::from(raw & 0x1f) * 255 / 31;
self.xrgb8888[pixel] = 0xff00_0000 | (r << 16) | (g << 8) | b;
}
}
self.generation = self.generation.wrapping_add(1);
}
}
enum HidReport {
Keyboard([u8; 8]),
Pointer([u8; 4]),
}
fn spawn_hid_writer(keyboard: PathBuf, pointer: PathBuf) -> mpsc::Sender<HidReport> {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
for report in rx {
let (path, bytes): (&Path, &[u8]) = match &report {
HidReport::Keyboard(bytes) => (&keyboard, bytes),
HidReport::Pointer(bytes) => (&pointer, bytes),
};
let result = OpenOptions::new()
.write(true)
.open(path)
.and_then(|mut endpoint| endpoint.write_all(bytes));
if let Err(error) = result {
warn!(path = %path.display(), %error, "HID report was not delivered");
}
}
});
tx
}
fn display_mode(width: u32, height: u32) -> DisplayMode {
let hsync_start = width.saturating_add(8);
let hsync_end = hsync_start.saturating_add(8);
let htotal = width.saturating_add(40);
let vsync_start = height.saturating_add(3);
let vsync_end = vsync_start.saturating_add(5);
let vtotal = height.saturating_add(40);
DisplayMode {
clock: htotal.saturating_mul(vtotal).saturating_mul(60) / 1000,
hdisplay: width as u16,
hsync_start: hsync_start as u16,
hsync_end: hsync_end as u16,
htotal: htotal as u16,
vdisplay: height as u16,
vsync_start: vsync_start as u16,
vsync_end: vsync_end as u16,
vtotal: vtotal as u16,
flags: 0,
}
}
fn spawn_gud(
ffs_dir: PathBuf,
ready_file: PathBuf,
frame: Arc<Mutex<Frame>>,
proxy: EventLoopProxy<UserEvent>,
running: Arc<AtomicBool>,
width: u32,
height: u32,
) -> thread::JoinHandle<()> {
thread::spawn(move || {
let result = (|| -> Result<()> {
let (mut pixel_data, pixel_endpoint) = gud_gadget::PixelDataEndpoint::new();
let mut gud = Custom::builder()
.with_interface(
Interface::new(Class::vendor_specific(Class::VENDOR_SPECIFIC, 0), "GUD")
.with_endpoint(pixel_endpoint),
)
.existing(&ffs_dir)
.with_context(|| format!("initializing {}", ffs_dir.display()))?;
if let Some(parent) = ready_file.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("creating {}", parent.display()))?;
}
std::fs::write(&ready_file, b"ready\n")
.with_context(|| format!("writing {}", ready_file.display()))?;
info!(ffs = %ffs_dir.display(), "GUD FunctionFS responder ready");
let mode = display_mode(width, height);
while running.load(Ordering::Relaxed) {
let Some(event) = gud.event_timeout(Duration::from_millis(100))? else {
continue;
};
let Some(event) = gud_gadget::event(event)? else {
continue;
};
match event {
Event::GetDescriptor(request) => {
request.send_descriptor(width, height, width, height)?;
}
Event::GetPixelFormats(request) => {
request.send_pixel_formats(&[GUD_PIXEL_FORMAT_RGB565])?;
}
Event::GetDisplayModes(request) => request
.send_modes(&[display_mode(mode.hdisplay.into(), mode.vdisplay.into())])?,
Event::Buffer(update) => {
let dirty = (update.x, update.y, update.width, update.height);
let mut frame = frame.lock().unwrap_or_else(|e| e.into_inner());
let pitch = frame.width as usize * 2;
pixel_data.recv_buffer(update, &mut frame.rgb565, pitch, 2)?;
frame.convert_rect(dirty.0, dirty.1, dirty.2, dirty.3);
let _ = proxy.send_event(UserEvent::Frame);
}
}
}
Ok(())
})();
let _ = std::fs::remove_file(&ready_file);
if let Err(error) = result {
error!(%error, "GUD responder stopped");
let _ = proxy.send_event(UserEvent::GudStopped(error.to_string()));
}
})
}
struct App {
title: String,
frame: Arc<Mutex<Frame>>,
window: Option<Arc<Window>>,
context: Option<SoftContext<Arc<Window>>>,
surface: Option<Surface<Arc<Window>, Arc<Window>>>,
rendered_generation: u64,
rendered_size: Option<(u32, u32)>,
hid: mpsc::Sender<HidReport>,
keys: BTreeSet<u8>,
modifiers: u8,
pointer_buttons: u8,
cursor: Option<PhysicalPosition<f64>>,
touch: Option<TouchTrack>,
running: Arc<AtomicBool>,
}
struct TouchTrack {
id: u64,
last: PhysicalPosition<f64>,
travel: f64,
}
impl App {
fn keyboard_report(&self) {
let mut report = [0u8; 8];
report[0] = self.modifiers;
for (slot, key) in self.keys.iter().take(6).enumerate() {
report[slot + 2] = *key;
}
let _ = self.hid.send(HidReport::Keyboard(report));
}
fn pointer_report(&self, x: i32, y: i32, wheel: i32) {
let mut x = x;
let mut y = y;
let mut wheel = wheel;
loop {
let dx = x.clamp(-127, 127) as i8;
let dy = y.clamp(-127, 127) as i8;
let dw = wheel.clamp(-127, 127) as i8;
let _ = self.hid.send(HidReport::Pointer([
self.pointer_buttons,
dx as u8,
dy as u8,
dw as u8,
]));
x -= i32::from(dx);
y -= i32::from(dy);
wheel -= i32::from(dw);
if x == 0 && y == 0 && wheel == 0 {
break;
}
}
}
fn set_mouse_button(&mut self, button: MouseButton, pressed: bool) {
let mask = match button {
MouseButton::Left => 1,
MouseButton::Right => 2,
MouseButton::Middle => 4,
_ => return,
};
if pressed {
self.pointer_buttons |= mask;
} else {
self.pointer_buttons &= !mask;
}
self.pointer_report(0, 0, 0);
}
fn draw(&mut self) -> Result<()> {
let Some(window) = &self.window else {
return Ok(());
};
let Some(surface) = &mut self.surface else {
return Ok(());
};
let size = window.inner_size();
let width = NonZeroU32::new(size.width.max(1)).unwrap();
let height = NonZeroU32::new(size.height.max(1)).unwrap();
surface
.resize(width, height)
.map_err(|error| anyhow!(error.to_string()))?;
let frame = self.frame.lock().unwrap_or_else(|e| e.into_inner());
let size_key = (size.width, size.height);
if frame.generation == self.rendered_generation && self.rendered_size == Some(size_key) {
return Ok(());
}
let mut buffer = surface
.buffer_mut()
.map_err(|error| anyhow!(error.to_string()))?;
for y in 0..size.height {
let source_y = y as usize * frame.height as usize / size.height.max(1) as usize;
for x in 0..size.width {
let source_x = x as usize * frame.width as usize / size.width.max(1) as usize;
buffer[y as usize * size.width as usize + x as usize] =
frame.xrgb8888[source_y * frame.width as usize + source_x];
}
}
self.rendered_generation = frame.generation;
self.rendered_size = Some(size_key);
buffer
.present()
.map_err(|error| anyhow!(error.to_string()))?;
Ok(())
}
}
impl ApplicationHandler<UserEvent> for App {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
if self.window.is_some() {
return;
}
let frame = self.frame.lock().unwrap_or_else(|e| e.into_inner());
let attributes = WindowAttributes::default()
.with_title(&self.title)
.with_inner_size(LogicalSize::new(frame.width / 2, frame.height / 2));
drop(frame);
match event_loop.create_window(attributes) {
Ok(window) => {
let window = Arc::new(window);
match SoftContext::new(window.clone()).and_then(|context| {
Surface::new(&context, window.clone()).map(|surface| (context, surface))
}) {
Ok((context, surface)) => {
self.context = Some(context);
self.surface = Some(surface);
window.request_redraw();
self.window = Some(window);
}
Err(error) => {
error!(%error, "could not create KVM drawing surface");
event_loop.exit();
}
}
}
Err(error) => {
error!(%error, "could not create KVM window");
event_loop.exit();
}
}
}
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: UserEvent) {
match event {
UserEvent::Frame => {
if let Some(window) = &self.window {
window.request_redraw();
}
}
UserEvent::GudStopped(reason) => {
error!(%reason, "closing KVM surface");
event_loop.exit();
}
}
}
fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
match event {
WindowEvent::CloseRequested => event_loop.exit(),
WindowEvent::RedrawRequested => {
if let Err(error) = self.draw() {
error!(%error, "KVM redraw failed");
event_loop.exit();
}
}
WindowEvent::Resized(_) => {
if let Some(window) = &self.window {
window.request_redraw();
}
}
WindowEvent::KeyboardInput { event, .. } => {
let PhysicalKey::Code(code) = event.physical_key else {
return;
};
let pressed = event.state == ElementState::Pressed;
if let Some(mask) = modifier_for(code) {
if pressed {
self.modifiers |= mask
} else {
self.modifiers &= !mask
}
} else if let Some(key) = hid_key(code) {
if pressed {
self.keys.insert(key);
} else {
self.keys.remove(&key);
}
}
self.keyboard_report();
}
WindowEvent::CursorMoved { position, .. } => {
if let Some(last) = self.cursor.replace(position) {
self.pointer_report(
(position.x - last.x).round() as i32,
(position.y - last.y).round() as i32,
0,
);
}
}
WindowEvent::CursorLeft { .. } => self.cursor = None,
WindowEvent::MouseInput { state, button, .. } => {
self.set_mouse_button(button, state == ElementState::Pressed);
}
WindowEvent::MouseWheel { delta, .. } => {
let wheel = match delta {
MouseScrollDelta::LineDelta(_, y) => (y * 3.0).round() as i32,
MouseScrollDelta::PixelDelta(position) => (position.y / 24.0).round() as i32,
};
self.pointer_report(0, 0, wheel);
}
WindowEvent::Touch(touch) => match touch.phase {
TouchPhase::Started => {
self.touch = Some(TouchTrack {
id: touch.id,
last: touch.location,
travel: 0.0,
});
}
TouchPhase::Moved => {
let Some(track) = &mut self.touch else { return };
if track.id != touch.id {
return;
}
let dx = touch.location.x - track.last.x;
let dy = touch.location.y - track.last.y;
track.last = touch.location;
track.travel += dx.abs() + dy.abs();
self.pointer_report(dx.round() as i32, dy.round() as i32, 0);
}
TouchPhase::Ended | TouchPhase::Cancelled => {
if self
.touch
.take()
.is_some_and(|track| track.id == touch.id && track.travel < 12.0)
{
self.pointer_buttons |= 1;
self.pointer_report(0, 0, 0);
self.pointer_buttons &= !1;
self.pointer_report(0, 0, 0);
}
}
},
WindowEvent::Focused(false) => {
self.keys.clear();
self.modifiers = 0;
self.pointer_buttons = 0;
self.keyboard_report();
self.pointer_report(0, 0, 0);
}
_ => {}
}
}
fn exiting(&mut self, _event_loop: &ActiveEventLoop) {
self.running.store(false, Ordering::Relaxed);
self.keys.clear();
self.modifiers = 0;
self.pointer_buttons = 0;
self.keyboard_report();
self.pointer_report(0, 0, 0);
}
}
fn modifier_for(code: KeyCode) -> Option<u8> {
Some(match code {
KeyCode::ControlLeft => 0x01,
KeyCode::ShiftLeft => 0x02,
KeyCode::AltLeft => 0x04,
KeyCode::SuperLeft => 0x08,
KeyCode::ControlRight => 0x10,
KeyCode::ShiftRight => 0x20,
KeyCode::AltRight => 0x40,
KeyCode::SuperRight => 0x80,
_ => return None,
})
}
fn hid_key(code: KeyCode) -> Option<u8> {
Some(match code {
KeyCode::KeyA => 0x04,
KeyCode::KeyB => 0x05,
KeyCode::KeyC => 0x06,
KeyCode::KeyD => 0x07,
KeyCode::KeyE => 0x08,
KeyCode::KeyF => 0x09,
KeyCode::KeyG => 0x0a,
KeyCode::KeyH => 0x0b,
KeyCode::KeyI => 0x0c,
KeyCode::KeyJ => 0x0d,
KeyCode::KeyK => 0x0e,
KeyCode::KeyL => 0x0f,
KeyCode::KeyM => 0x10,
KeyCode::KeyN => 0x11,
KeyCode::KeyO => 0x12,
KeyCode::KeyP => 0x13,
KeyCode::KeyQ => 0x14,
KeyCode::KeyR => 0x15,
KeyCode::KeyS => 0x16,
KeyCode::KeyT => 0x17,
KeyCode::KeyU => 0x18,
KeyCode::KeyV => 0x19,
KeyCode::KeyW => 0x1a,
KeyCode::KeyX => 0x1b,
KeyCode::KeyY => 0x1c,
KeyCode::KeyZ => 0x1d,
KeyCode::Digit1 => 0x1e,
KeyCode::Digit2 => 0x1f,
KeyCode::Digit3 => 0x20,
KeyCode::Digit4 => 0x21,
KeyCode::Digit5 => 0x22,
KeyCode::Digit6 => 0x23,
KeyCode::Digit7 => 0x24,
KeyCode::Digit8 => 0x25,
KeyCode::Digit9 => 0x26,
KeyCode::Digit0 => 0x27,
KeyCode::Enter => 0x28,
KeyCode::Escape => 0x29,
KeyCode::Backspace => 0x2a,
KeyCode::Tab => 0x2b,
KeyCode::Space => 0x2c,
KeyCode::Minus => 0x2d,
KeyCode::Equal => 0x2e,
KeyCode::BracketLeft => 0x2f,
KeyCode::BracketRight => 0x30,
KeyCode::Backslash => 0x31,
KeyCode::Semicolon => 0x33,
KeyCode::Quote => 0x34,
KeyCode::Backquote => 0x35,
KeyCode::Comma => 0x36,
KeyCode::Period => 0x37,
KeyCode::Slash => 0x38,
KeyCode::CapsLock => 0x39,
KeyCode::F1 => 0x3a,
KeyCode::F2 => 0x3b,
KeyCode::F3 => 0x3c,
KeyCode::F4 => 0x3d,
KeyCode::F5 => 0x3e,
KeyCode::F6 => 0x3f,
KeyCode::F7 => 0x40,
KeyCode::F8 => 0x41,
KeyCode::F9 => 0x42,
KeyCode::F10 => 0x43,
KeyCode::F11 => 0x44,
KeyCode::F12 => 0x45,
KeyCode::PrintScreen => 0x46,
KeyCode::ScrollLock => 0x47,
KeyCode::Pause => 0x48,
KeyCode::Insert => 0x49,
KeyCode::Home => 0x4a,
KeyCode::PageUp => 0x4b,
KeyCode::Delete => 0x4c,
KeyCode::End => 0x4d,
KeyCode::PageDown => 0x4e,
KeyCode::ArrowRight => 0x4f,
KeyCode::ArrowLeft => 0x50,
KeyCode::ArrowDown => 0x51,
KeyCode::ArrowUp => 0x52,
_ => return None,
})
}
fn main() -> Result<()> {
tracing_subscriber::registry()
.with(fmt::layer())
.with(EnvFilter::from_default_env())
.init();
let args = Args::parse();
anyhow::ensure!(
args.width <= u16::MAX.into() && args.height <= u16::MAX.into(),
"display is too large for GUD mode fields"
);
let event_loop = EventLoop::<UserEvent>::with_user_event().build()?;
let frame = Arc::new(Mutex::new(Frame::new(args.width, args.height)));
let running = Arc::new(AtomicBool::new(true));
let gud = spawn_gud(
args.ffs_dir,
args.ready_file,
frame.clone(),
event_loop.create_proxy(),
running.clone(),
args.width,
args.height,
);
let mut app = App {
title: args.title,
frame,
window: None,
context: None,
surface: None,
rendered_generation: u64::MAX,
rendered_size: None,
hid: spawn_hid_writer(args.keyboard, args.pointer),
keys: BTreeSet::new(),
modifiers: 0,
pointer_buttons: 0,
cursor: None,
touch: None,
running,
};
event_loop.run_app(&mut app)?;
app.running.store(false, Ordering::Relaxed);
let _ = gud.join();
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rgb565_dirty_rect_becomes_xrgb8888() {
let mut frame = Frame::new(2, 1);
frame.rgb565 = [0x00, 0xf8, 0xe0, 0x07].to_vec();
frame.convert_rect(0, 0, 2, 1);
assert_eq!(frame.xrgb8888, [0xffff_0000, 0xff00_ff00]);
assert_eq!(frame.generation, 1);
}
#[test]
fn physical_keys_use_usb_hid_usage_ids() {
assert_eq!(hid_key(KeyCode::KeyA), Some(0x04));
assert_eq!(hid_key(KeyCode::Digit0), Some(0x27));
assert_eq!(hid_key(KeyCode::F12), Some(0x45));
assert_eq!(hid_key(KeyCode::Delete), Some(0x4c));
assert_eq!(modifier_for(KeyCode::ControlLeft), Some(0x01));
assert_eq!(modifier_for(KeyCode::AltRight), Some(0x40));
}
}

586
src/bin/souveraine-web.rs Normal file
View file

@ -0,0 +1,586 @@
//! A wry host: self-hosted sites as apps, and the rig she wears.
//!
//! Two modes, one binary, because they are the same thing pointed at different
//! content — a webview on a Wayland surface with no browser around it.
//!
//! - `--url <URL>` wraps a site as an app. Own window, own `app_id`, so the
//! compositor tiles it and the dock names it like anything else.
//! - `--rig <DIR>` is the avatar: transparent over the wallpaper, assets served
//! from a custom scheme rather than `file://`, and an IPC line to the shell.
//!
//! wry binds the system webview (WebKitGTK here) rather than shipping a second
//! browser engine — which on a 3.5 GB daily driver is the whole argument.
//! Measured 2026-08-05: the Cubism runtime renders at ~58 fps in this engine on
//! blueline, 283 MB RSS.
//!
//! **This process holds no connection to the server.** `Souveraine.qml` is the
//! shell's one transport and stays that way; the face is a limb the shell
//! drives over `--ipc`, so the avatar and the sidebar are the same conversation
//! by construction rather than by two clients agreeing. Casey, 2026-08-05:
//! *"I will want it to be in sync with the sidebar — meaning if we 'resume'
//! it's resumed."*
//!
//! ## file:// is not enough
//!
//! `XMLHttpRequest` for the rig's `model.json` is blocked from `file://` even
//! with `allow-file-access-from-file-urls` set — it fails with status 0. The
//! phase-1 spike worked around it with a local HTTP server; shipping one to
//! serve our own assets would be a listening socket for no reason. A custom
//! scheme is wry's answer and costs nothing.
use std::io::{self, BufRead, BufReader, Write};
use std::path::PathBuf;
use anyhow::{Context, Result};
use tao::event::{Event, StartCause, WindowEvent};
use tao::event_loop::{ControlFlow, EventLoopBuilder, EventLoopProxy};
use tao::window::WindowBuilder;
use wry::http::Response;
use wry::WebViewBuilder;
/// What the shell can tell the face to do, and what it says back.
///
/// One JSON object per line, the same house grammar sessiond and viewtop speak,
/// so nothing here is a third idea of what talking to a process looks like.
#[derive(Debug)]
enum FromShell {
/// Run a script in the page. Everything the shell drives — a line of her
/// speech, a posture change, a motion — arrives as one of these, because
/// the vocabulary belongs to the page's character layer and not to this
/// file. See `docs/tasks/59-her-face-on-the-glass.md`.
Eval(String),
Quit,
/// Where on this window touches actually land, in CSS pixels.
///
/// A transparent window is still a rectangle to the compositor, and hers
/// covers most of the home screen — so every tap meant for a widget
/// underneath was being swallowed by empty glass she happens to occupy.
/// `wl_surface.set_input_region` is the answer the protocol already has,
/// and GTK reaches it through an input shape.
///
/// The *policy* — a rectangle over her body now, her true silhouette
/// later — belongs to the page, which is the only thing that knows where
/// she is drawn. This carries whatever it decides.
Shape(Vec<(i32, i32, i32, i32)>),
}
fn main() -> Result<()> {
run()
}
fn run() -> Result<()> {
let mut url: Option<String> = None;
let mut rig: Option<PathBuf> = None;
let mut ipc: Option<PathBuf> = None;
let mut app_id = String::from("org.souveraine.web");
let mut title = String::from("Souveraine");
let mut transparent = false;
let mut width = 540.0;
let mut height = 960.0;
let mut args = std::env::args().skip(1);
while let Some(arg) = args.next() {
match arg.as_str() {
"--url" => url = args.next(),
"--rig" => rig = args.next().map(PathBuf::from),
"--ipc" => ipc = args.next().map(PathBuf::from),
"--app-id" => {
if let Some(v) = args.next() {
app_id = v;
}
}
"--title" => {
if let Some(v) = args.next() {
title = v;
}
}
"--transparent" => transparent = true,
"--size" => {
if let Some(v) = args.next() {
if let Some((w, h)) = v.split_once('x') {
width = w.parse().unwrap_or(width);
height = h.parse().unwrap_or(height);
}
}
}
"--help" | "-h" => {
eprintln!(
"souveraine-web --url <URL> [--app-id ID] [--size WxH] [--title T]\n\
souveraine-web --rig <DIR> --ipc <SOCK> --transparent [--title T]"
);
return Ok(());
}
other => anyhow::bail!("unknown argument {other}"),
}
}
if url.is_none() && rig.is_none() {
anyhow::bail!("one of --url or --rig is required");
}
// Before the event loop, because building it initialises GTK and GTK reads
// the program name when it creates the surface — not when the window
// handle is returned. Called after `build()` (where it used to be) the
// compositor had already been told `souveraine-web`, so `--app-id` was a
// flag that parsed, stored and did nothing. Measured 2026-08-06: the
// compositor reported `souveraine-web`, which is why the only thing the
// laptop's window rules could match on was the title.
set_app_id(&app_id);
let event_loop = EventLoopBuilder::<FromShell>::with_user_event().build();
let shape_proxy = event_loop.create_proxy();
// The X11 half, and it can only run here: `gdk::set_program_class` panics
// if GDK is not initialised yet, and building the loop is what initialises
// it. Wayland never reads this — `g_get_prgname()` above is what becomes
// the app_id, and this is its fallback — so the ordering costs nothing.
set_program_class(&app_id);
let proxy = event_loop.create_proxy();
// The shell drives this process; it never drives the shell. Reading on its
// own thread and waking the loop through the proxy keeps the webview's
// thread free, which matters because that thread is also the renderer.
if let Some(path) = ipc.clone() {
std::thread::spawn(move || serve_ipc(&path, proxy));
}
let window = WindowBuilder::new()
.with_title(&title)
.with_transparent(transparent)
.with_decorations(!transparent)
.with_inner_size(tao::dpi::LogicalSize::new(width, height))
.build(&event_loop)
.context("creating the window")?;
let mut builder = WebViewBuilder::new()
.with_transparent(transparent)
// The character layer decides what a tap means and what she says; this
// only carries it. Anything the page wants the shell to know goes out
// the IPC as a line, so the shell stays the one thing talking to her.
.with_ipc_handler(move |req| {
let body = req.body();
// The shape is ours to apply, not the shell's to route. It names
// window geometry, which the shell has no opinion about and could
// only hand straight back — and a round trip through the shell
// would make the input region depend on the shell being up, which
// is exactly when a face that eats every touch is worst.
let shape = serde_json::from_str::<serde_json::Value>(body)
.ok()
.filter(|m| m.get("event").and_then(|v| v.as_str()) == Some("shape"));
if let Some(msg) = shape {
let rects: Vec<(i32, i32, i32, i32)> = msg
.get("rects")
.and_then(|v| v.as_array())
.map(|a| {
a.iter()
.filter_map(|r| {
let r = r.as_array()?;
Some((
r.first()?.as_f64()? as i32,
r.get(1)?.as_f64()? as i32,
r.get(2)?.as_f64()? as i32,
r.get(3)?.as_f64()? as i32,
))
})
.collect()
})
.unwrap_or_default();
let _ = shape_proxy.send_event(FromShell::Shape(rects));
return;
}
let mut out = std::io::stdout().lock();
let _ = writeln!(out, "{body}");
let _ = out.flush();
});
if let Some(dir) = rig {
let dir = dir.canonicalize().context("resolving the rig directory")?;
// Loopback HTTP, not the custom scheme — and this is the second time
// this exact wall has been hit. TASK-59 measured it for `file://`:
// `XMLHttpRequest` for `model.json` fails with status 0 no matter what
// access flags are set, because the origin is opaque. A custom scheme
// is opaque in the same way, so the Cubism runtime — which fetches
// `model.json`, the `.moc` and every texture over XHR — draws nothing
// and reports no error, which is precisely the "canvas is there and
// nothing draws" the task warned would cost an afternoon.
//
// The task preferred a custom scheme to avoid "a listening socket for
// no reason". The reason turned out to be real. It binds 127.0.0.1 on
// an ephemeral port, so it is reachable only from this machine and only
// for as long as the face is up.
let port = serve_rig_over_loopback(dir)?;
builder = builder
.with_initialization_script(INIT_SCRIPT)
.with_url(format!("http://127.0.0.1:{port}/index.html"));
} else if let Some(u) = url {
builder = builder.with_url(u);
}
let webview = build_webview(builder, &window)?;
event_loop.run(move |event, _, control_flow| {
*control_flow = ControlFlow::Wait;
match event {
Event::NewEvents(StartCause::Init) => {}
Event::UserEvent(FromShell::Eval(script)) => {
let _ = webview.evaluate_script(&script);
}
Event::UserEvent(FromShell::Shape(rects)) => {
apply_input_shape(&window, &rects);
}
Event::UserEvent(FromShell::Quit)
| Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => *control_flow = ControlFlow::Exit,
_ => {}
}
});
}
/// Read one JSON object per line and hand it to the loop.
fn serve_ipc(path: &std::path::Path, proxy: EventLoopProxy<FromShell>) {
// The socket's parent directory is not guaranteed to exist: on the phone
// sessiond or the compositor creates /run/user/NAME/souveraine/, but a
// laptop with neither still needs the face reachable. Create it rather
// than depend on whoever else got there first.
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
// A crashed host leaves its socket file behind, and a socket file that is
// observed on blueline 2026-08-06: the face died, the shell's next join
// died on "Address already in use", and the only fix was `rm`. Bind is the
// moment to learn whether anything is actually listening: an error here
// means the file is a corpse, so unlink it and take the bind once more.
let listener = match std::os::unix::net::UnixListener::bind(path) {
Ok(l) => l,
Err(e) if e.kind() == std::io::ErrorKind::AddrInUse => {
eprintln!("[face] {} is stale; removing and retrying", path.display());
let _ = std::fs::remove_file(path);
match std::os::unix::net::UnixListener::bind(path) {
Ok(l) => l,
Err(e) => {
eprintln!("[face] cannot bind {}: {e}", path.display());
return;
}
}
}
Err(e) => {
eprintln!("[face] cannot bind {}: {e}", path.display());
return;
}
};
for stream in listener.incoming().flatten() {
let reader = BufReader::new(stream);
for line in reader.lines().map_while(Result::ok) {
let Ok(msg) = serde_json::from_str::<serde_json::Value>(&line) else {
eprintln!("[face] unparseable: {line}");
continue;
};
let event = match msg.get("op").and_then(|v| v.as_str()) {
Some("eval") => msg
.get("script")
.and_then(|v| v.as_str())
.map(|s| FromShell::Eval(s.to_string())),
Some("quit") => Some(FromShell::Quit),
_ => None,
};
if let Some(event) = event {
if proxy.send_event(event).is_err() {
return;
}
}
}
}
}
/// Serve the rig on 127.0.0.1, returning the port it landed on.
///
/// Deliberately the smallest thing that answers GET: the client is one webview
/// on the same machine fetching a dozen static files, so a dependency here
/// would be weight for nothing.
fn serve_rig_over_loopback(dir: PathBuf) -> Result<u16> {
let listener =
std::net::TcpListener::bind(("127.0.0.1", 0)).context("binding the rig server")?;
let port = listener.local_addr()?.port();
std::thread::spawn(move || {
for stream in listener.incoming().flatten() {
let dir = dir.clone();
// One thread per request. The webview opens a handful in parallel
// for the textures, and a serial loop would deadlock the page
// waiting on itself.
std::thread::spawn(move || {
let _ = answer_request(stream, &dir);
});
}
});
Ok(port)
}
fn answer_request(mut stream: std::net::TcpStream, dir: &std::path::Path) -> io::Result<()> {
let mut reader = BufReader::new(stream.try_clone()?);
let mut line = String::new();
reader.read_line(&mut line)?;
let path = line.split_whitespace().nth(1).unwrap_or("/").to_string();
// Drain the headers so the client is not left writing into a full buffer.
loop {
let mut h = String::new();
if reader.read_line(&mut h)? == 0 || h == "\r\n" || h == "\n" {
break;
}
}
let path = path.split('?').next().unwrap_or("/");
let response = serve_asset(dir, path);
let status = response.status().as_u16();
let mime = response
.headers()
.get("Content-Type")
.and_then(|v| v.to_str().ok())
.unwrap_or("application/octet-stream")
.to_string();
let body = response.body();
write!(
stream,
"HTTP/1.1 {status} OK\r\nContent-Type: {mime}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
body.len()
)?;
stream.write_all(body)?;
stream.flush()
}
/// Serve one file out of the rig directory.
///
/// Path traversal is refused rather than sanitised: the only correct answer to
/// `../../etc/passwd` is no.
fn serve_asset(root: &std::path::Path, path: &str) -> Response<std::borrow::Cow<'static, [u8]>> {
let relative = path.trim_start_matches('/');
let candidate = root.join(relative);
let ok = candidate
.canonicalize()
.map(|p| p.starts_with(root))
.unwrap_or(false);
if !ok {
return not_found();
}
let mime = match candidate.extension().and_then(|e| e.to_str()) {
Some("html") => "text/html",
Some("js") => "text/javascript",
Some("css") => "text/css",
Some("json") => "application/json",
Some("png") => "image/png",
Some("jpg" | "jpeg") => "image/jpeg",
Some("moc" | "mtn") => "application/octet-stream",
_ => "application/octet-stream",
};
match std::fs::read(&candidate) {
Ok(bytes) => Response::builder()
.header("Content-Type", mime)
.body(std::borrow::Cow::Owned(bytes))
.unwrap_or_else(|_| not_found()),
Err(_) => not_found(),
}
}
fn not_found() -> Response<std::borrow::Cow<'static, [u8]>> {
Response::builder()
.status(404)
.body(std::borrow::Cow::Borrowed(&[][..]))
.expect("a 404 with an empty body is always well formed")
}
/// Injected before the page's own scripts.
///
/// Two jobs. The first is a one-line removal the upstream README lists as a
/// *feature*: `message.js` refuses to load on any user agent containing
/// "android", and this is a phone. The rig loads fine, the canvas is there, and
/// nothing draws — with no error. It is worth the injection rather than a patch
/// to the vendored file so that re-vendoring upstream cannot silently restore
/// it.
///
/// The second is the talk path. The reference opens an `EventSource` straight
/// at a chat API; here the page has no server to talk to, because this process
/// has no connection to one. It posts to the shell instead.
const INIT_SCRIPT: &str = r#"
(function () {
// Look like a desktop to the character layer's mobile blocklist.
try {
Object.defineProperty(window.navigator, 'userAgent', {
get: function () { return 'Mozilla/5.0 (X11; Linux x86_64) souveraine-web'; }
});
} catch (e) {}
// Everything the page says, out on the same line protocol.
//
// Without this a rig that fails to draw is silent in every direction: the
// canvas is there, WebGL reports fine, no exception reaches Rust, and the
// only symptom is a transparent window. That cost this session an afternoon
// of probing a live page over the IPC to ask it questions one at a time,
// and DUMP-bugs-2026-08-06 §3 had already written down that the next step
// when the avatar renders nothing is to capture the host's output.
//
// Runs in the initialisation script, so it is installed before any page
// script — a message logged while the runtime is loading is exactly the one
// worth having. The shell ignores what it does not recognise.
try {
['log', 'warn', 'error'].forEach(function (level) {
var original = console[level].bind(console);
console[level] = function () {
original.apply(null, arguments);
try {
window.ipc.postMessage(JSON.stringify({
event: 'console',
level: level,
text: Array.prototype.map.call(arguments, String).join(' ')
}));
} catch (e) {}
};
});
window.addEventListener('error', function (e) {
try {
window.ipc.postMessage(JSON.stringify({
event: 'console',
level: 'error',
text: e.message + ' @ ' + (e.filename || '?') + ':' + (e.lineno || 0)
}));
} catch (_) {}
});
window.addEventListener('unhandledrejection', function (e) {
try {
window.ipc.postMessage(JSON.stringify({
event: 'console', level: 'error', text: 'unhandled rejection: ' + e.reason
}));
} catch (_) {}
});
} catch (e) {}
// The shell owns the conversation. Anything the page wants to say goes out
// as a line; anything she says comes back as an eval that calls showMessage.
window.souveraine = {
say: function (text) {
window.ipc.postMessage(JSON.stringify({ event: 'said', text: text }));
},
tapped: function (area) {
window.ipc.postMessage(JSON.stringify({ event: 'tapped', area: area }));
},
// Speech edges only. The page now also has a small agent field when USB
// Hands is joined, but the shell still owns the recorder, endpoint and
// transcript; the webview remains a view over one conversation.
talk: function (phase) {
window.ipc.postMessage(JSON.stringify({ event: 'talk', phase: phase }));
},
hid: function (message) {
var body = Object.assign({ event: 'hid' }, message || {});
window.ipc.postMessage(JSON.stringify(body));
},
thread: function (mode) {
window.ipc.postMessage(JSON.stringify({ event: 'thread', mode: mode }));
},
ready: function () {
window.ipc.postMessage(JSON.stringify({ event: 'ready' }));
},
// The way back. A double tap dismisses her, which is the same gesture in
// the same place that summoned her from the clock — and the clock cannot
// be the way back, because by then it has faded out and a double tap on
// an invisible object is not a route, it is a secret.
dismiss: function () {
window.ipc.postMessage(JSON.stringify({ event: 'dismiss' }));
},
// Where this window should take touches, in CSS pixels. Everything the
// page does not name here falls through to whatever is behind her.
shape: function (rects) {
window.ipc.postMessage(JSON.stringify({ event: 'shape', rects: rects }));
}
};
window.talkAPI = '';
})();
"#;
/// wry on Linux draws into the window's GTK container rather than adopting the
/// surface, so the build goes through the Unix extension. Kept in one place so
/// the two modes do not each grow a platform branch.
#[cfg(target_os = "linux")]
fn build_webview(
builder: WebViewBuilder<'_>,
window: &tao::window::Window,
) -> Result<wry::WebView> {
use tao::platform::unix::WindowExtUnix;
use wry::WebViewBuilderExtUnix;
let vbox = window
.default_vbox()
.context("the window has no gtk container")?;
builder.build_gtk(vbox).context("building the webview")
}
#[cfg(not(target_os = "linux"))]
fn build_webview(
builder: WebViewBuilder<'_>,
window: &tao::window::Window,
) -> Result<wry::WebView> {
builder.build(window).context("building the webview")
}
/// Name the surface so the compositor can tile it and the dock can label it.
/// Without this every wrapped site is an untitled window, which is TASK-51's
/// "windows stack with nothing to tell them apart" arriving by a new route.
#[cfg(target_os = "linux")]
fn set_app_id(app_id: &str) {
// On Wayland the app_id is GTK's program name, not a window property —
// there is nothing on the handle to set, so this cannot wait for one to
// exist. GTK reads the name while creating the surface, which happens
// inside the window build, so the only place this works is before the
// event loop is built at all. Pure glib, so it needs nothing initialised.
gtk::glib::set_prgname(Some(app_id));
}
#[cfg(not(target_os = "linux"))]
fn set_app_id(_app_id: &str) {}
#[cfg(target_os = "linux")]
fn set_program_class(app_id: &str) {
gtk::gdk::set_program_class(app_id);
}
/// Restrict where this window takes touches to the rectangles the page named.
///
/// The whole point of a transparent face is that the home screen is still
/// there — and it was not, because a surface with no input region takes every
/// contact inside its bounds whether it drew anything there or not. She is
/// 540px wide on a 540px panel, so that was the entire width of the screen.
///
/// An empty list is "all of it", not "none of it": a page that has not worked
/// out its shape yet, or one whose report was malformed, must not end up
/// untouchable. Losing taps to her is a nuisance; a face that cannot be
/// dismissed because nothing can reach it is a device you have to restart.
#[cfg(target_os = "linux")]
fn apply_input_shape(window: &tao::window::Window, rects: &[(i32, i32, i32, i32)]) {
use gtk::prelude::WidgetExt;
use tao::platform::unix::WindowExtUnix;
let gtk_window = window.gtk_window();
if rects.is_empty() {
gtk_window.input_shape_combine_region(None);
return;
}
let region = gtk::cairo::Region::create();
for (x, y, w, h) in rects {
if *w <= 0 || *h <= 0 {
continue;
}
if region
.union_rectangle(&gtk::cairo::RectangleInt::new(*x, *y, *w, *h))
.is_err()
{
// A region that failed to build is not a region to install —
// a partial one would silently make part of her untouchable.
return;
}
}
gtk_window.input_shape_combine_region(Some(&region));
}
#[cfg(not(target_os = "linux"))]
fn apply_input_shape(_window: &tao::window::Window, _rects: &[(i32, i32, i32, i32)]) {}
#[cfg(not(target_os = "linux"))]
fn set_program_class(_app_id: &str) {}

File diff suppressed because it is too large Load diff

178
src/bridge/mod.rs Normal file
View file

@ -0,0 +1,178 @@
#![allow(dead_code)] // WIP scaffolding not yet wired
/// Bridge module — LLM inference providers.
///
/// Connects Souveraine to inference providers behind the [`LlmProvider`] trait.
/// `OpenAiCompatibleClient` speaks to any OpenAI-compatible gateway; the OAuth-riding
/// ChatGPT provider lives in [`providers`]. [`build_provider`] selects the
/// active one from config; [`ProviderRegistry`] resolves a provider per agent.
pub mod openai_compatible;
pub mod claude_subscription;
pub mod model_router;
pub mod oauth;
pub mod provider;
pub mod providers;
pub use openai_compatible::OpenAiCompatibleClient;
pub use provider::LlmProvider;
use std::collections::HashMap;
use std::sync::Arc;
use crate::api::models::AgentState;
use crate::core::config::{ConsciousnessConfig, ProviderConfig};
/// Build the active (global default) LLM provider from config.
///
/// Convenience wrapper that builds the full registry and returns its default.
/// For inference, prefer [`build_registry`] + [`ProviderRegistry::for_agent`].
pub fn build_provider(config: &ConsciousnessConfig) -> anyhow::Result<Arc<dyn LlmProvider>> {
let reg = build_registry(config)?;
Ok(reg.default_provider())
}
/// Build a provider from a [`ProviderConfig`] entry, dispatching on `provider_type`.
pub fn build_provider_from_config(
name: &str,
cfg: &ProviderConfig,
) -> anyhow::Result<Arc<dyn LlmProvider>> {
match cfg.provider_type.as_str() {
"openai-oauth" => {
let provider = providers::openai_oauth::OpenAiOAuthProvider::from_codex_login(
cfg.primary_model.clone(),
cfg.timeout_secs,
)?;
Ok(Arc::new(provider))
}
"claude-subscription" => {
let provider = claude_subscription::ClaudeSubscriptionProvider::new(
name,
&cfg.base_url,
&cfg.primary_model,
cfg.timeout_secs,
cfg.credential_file.as_deref(),
cfg.credential_files.as_deref(),
cfg.cc_version.as_deref(),
cfg.account_uuid.as_deref(),
cfg.device_id.as_deref(),
cfg.extra_metadata.clone(),
)?;
Ok(Arc::new(provider))
}
_ => {
// "openai-compatible" and any unrecognized type → OpenAI-compatible gateway.
let client = OpenAiCompatibleClient::new(
name,
&cfg.base_url,
&cfg.api_key,
&cfg.virtual_key,
&cfg.primary_model,
cfg.timeout_secs,
)?;
Ok(Arc::new(client))
}
}
}
/// Build the per-agent provider registry from the unified `[providers]` map.
///
/// Iterates every entry in `config.providers`, builds a provider for each, and
/// resolves the default from `[inference] provider`. Returns an error if no
/// providers are configured.
pub fn build_registry(config: &ConsciousnessConfig) -> anyhow::Result<ProviderRegistry> {
let mut map = HashMap::new();
for (name, pcfg) in &config.providers {
let provider = build_provider_from_config(name, pcfg)?;
map.insert(name.clone(), provider);
}
if map.is_empty() {
anyhow::bail!("no providers configured — add at least one [providers.<name>] section");
}
let default_name = config.inference.provider.clone();
let default = map
.get(&default_name)
.cloned()
.unwrap_or_else(|| map.values().next().unwrap().clone());
let model_providers = config
.models
.iter()
.map(|(model, mcfg)| (model.clone(), mcfg.provider.clone()))
.collect();
Ok(ProviderRegistry {
map,
default_name,
default,
model_providers,
})
}
/// Maps a provider name to a live [`LlmProvider`], resolving per agent.
#[derive(Clone)]
pub struct ProviderRegistry {
map: HashMap<String, Arc<dyn LlmProvider>>,
default_name: String,
default: Arc<dyn LlmProvider>,
/// `[models.<name>] provider` — which provider serves a given model.
/// Needed wherever a model is chosen independently of the agent that owns
/// the turn, e.g. the subconscious running somewhere the primary does not.
model_providers: HashMap<String, String>,
}
impl std::fmt::Debug for ProviderRegistry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ProviderRegistry")
.field("providers", &self.map.keys().collect::<Vec<_>>())
.field("default", &self.default_name)
.finish()
}
}
impl ProviderRegistry {
/// Resolve the provider for an agent: its `_souveraine.provider` override,
/// else the global default. An unknown/missing name falls back to the
/// default rather than failing.
pub fn for_agent(&self, agent: &AgentState) -> Arc<dyn LlmProvider> {
match agent.souveraine.provider.as_deref() {
Some(name) if name != self.default_name => self
.map
.get(name)
.cloned()
.unwrap_or_else(|| self.default.clone()),
_ => self.default.clone(),
}
}
/// All registered provider names.
pub fn provider_names(&self) -> Vec<&str> {
self.map.keys().map(|s| s.as_str()).collect()
}
/// Look up a provider by name. Returns `None` for unknown names.
pub fn get(&self, name: &str) -> Option<Arc<dyn LlmProvider>> {
self.map.get(name).cloned()
}
/// Resolve the provider that serves a model, via `[models.<name>] provider`.
///
/// `None` when the model has no config entry or names a provider that
/// isn't registered — the caller decides the fallback, because "no entry"
/// and "wrong entry" both mean *don't guess a wire for this model*.
pub fn for_model(&self, model: &str) -> Option<Arc<dyn LlmProvider>> {
let name = self.model_providers.get(model)?;
self.map.get(name).cloned()
}
/// The global default provider (for contexts with no agent).
pub fn default_provider(&self) -> Arc<dyn LlmProvider> {
self.default.clone()
}
/// Name of the global default provider.
pub fn default_name(&self) -> &str {
&self.default_name
}
}

360
src/bridge/model_router.rs Normal file
View file

@ -0,0 +1,360 @@
#![allow(dead_code)] // WIP scaffolding not yet wired
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::{debug, info};
use crate::bridge::openai_compatible::OpenAiCompatibleClient;
use crate::core::config::{ModelConfig, TaskType};
/// Context pressure — how full the context window is
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ContextPressure {
/// Normal operation, plenty of room
Normal,
/// Approaching threshold — prepare for synthesis
Elevated,
/// At or above threshold — trigger N+100 synthesis NOW
Critical,
}
/// Active token usage tracker
#[derive(Debug, Default)]
pub struct TokenUsage {
/// Current estimated tokens in context
pub tokens: usize,
/// Estimated prompt tokens
pub prompt_tokens: usize,
/// Estimated completion tokens
pub completion_tokens: usize,
}
/// Token counter using real tiktoken (cl100k_base)
///
/// Matches the OSSUI pattern: js-tiktoken cl100k_base
/// Falls back to chars/4 if tiktoken fails
pub struct TokenCounter {
/// Whether tiktoken is available
pub available: bool,
/// Cached encoding (static lifetime from tiktoken's built-in BPE data)
encoding: Option<&'static tiktoken::CoreBpe>,
}
impl TokenCounter {
pub fn new() -> Self {
let encoding = tiktoken::get_encoding("cl100k_base");
if encoding.is_some() {
info!("🔢 Token counter initialized with tiktoken (cl100k_base)");
} else {
info!("🔢 Token counter using chars/4 fallback (tiktoken unavailable)");
}
Self {
encoding,
available: encoding.is_some(),
}
}
/// Count tokens using tiktoken or chars/4 fallback
/// Matches the OSSUI pattern exactly
pub fn count(&self, text: &str) -> usize {
if text.is_empty() {
return 0;
}
if let Some(enc) = self.encoding {
enc.encode_with_special_tokens(text).len()
} else {
// No tiktoken available: chars/4 fallback
(text.len() as f32 / 4.0).ceil() as usize
}
}
}
impl Default for TokenCounter {
fn default() -> Self {
Self::new()
}
}
/// Model Router — physics-aware model selection and context monitoring
pub struct ModelRouter {
configs: HashMap<String, ModelConfig>,
current_usage: Arc<RwLock<TokenUsage>>,
token_counter: TokenCounter,
provider_client: Option<OpenAiCompatibleClient>,
discovered_models: Vec<String>,
selected_model: String,
}
impl ModelRouter {
pub fn new(configs: HashMap<String, ModelConfig>) -> Self {
let count = configs.len();
info!(
"🧭 ModelRouter initialized with {} model configurations",
count
);
for (name, cfg) in &configs {
debug!(
" {} via {} — {} ctx, {} out, threshold {}",
name, cfg.provider, cfg.context_limit, cfg.output_limit, cfg.archivist_threshold
);
}
Self {
configs,
current_usage: Arc::new(RwLock::new(TokenUsage::default())),
token_counter: TokenCounter::new(),
provider_client: None,
discovered_models: Vec::new(),
selected_model: String::new(),
}
}
/// Create a ModelRouter with a provider client for dynamic model discovery
pub fn with_provider_client(
configs: HashMap<String, ModelConfig>,
provider_client: OpenAiCompatibleClient,
) -> Self {
let mut router = Self::new(configs);
router.provider_client = Some(provider_client);
router
}
/// Count tokens in text using real tiktoken
pub fn count_tokens(&self, text: &str) -> usize {
self.token_counter.count(text)
}
/// Get a model config by name
pub fn get_model(&self, name: &str) -> Option<&ModelConfig> {
self.configs.get(name)
}
/// Find the best model for a given task type
pub fn find_best_for_task(&self, task: TaskType) -> Option<&ModelConfig> {
self.configs
.values()
.filter(|m| m.preferred_for.contains(&task))
.max_by_key(|m| m.context_limit)
}
/// Check context pressure for a specific model
pub async fn context_pressure(
&self,
model_name: &str,
context_window_limit: Option<usize>,
) -> ContextPressure {
let config = match self.configs.get(model_name) {
Some(c) => c,
None => return ContextPressure::Normal,
};
// Use agent-level context_window_limit if set, otherwise model default
let limit = context_window_limit.unwrap_or(config.context_limit);
if limit == 0 {
return ContextPressure::Normal;
}
let usage = self.current_usage.read().await;
let ratio = usage.tokens as f32 / limit as f32;
if ratio >= config.archivist_threshold {
ContextPressure::Critical
} else if ratio >= config.archivist_threshold * 0.8 {
ContextPressure::Elevated
} else {
ContextPressure::Normal
}
}
/// Update current token usage
pub async fn update_usage(&self, tokens: usize, prompt: usize, completion: usize) {
let mut usage = self.current_usage.write().await;
usage.tokens = tokens;
usage.prompt_tokens = prompt;
usage.completion_tokens = completion;
}
/// Get a reference to the token usage tracker
pub fn usage_tracker(&self) -> Arc<RwLock<TokenUsage>> {
self.current_usage.clone()
}
/// Get the context limit for a model with agent override
pub fn context_limit_for(&self, model_name: &str, agent_override: Option<usize>) -> usize {
agent_override
.or_else(|| self.configs.get(model_name).map(|c| c.context_limit))
.unwrap_or(128_000)
}
/// Get the archivist interval for a model (or default 100)
pub fn archivist_interval_for(&self, model_name: &str) -> usize {
self.configs
.get(model_name)
.map(|c| c.archivist_interval)
.unwrap_or(100)
}
/// Get all configured model names
pub fn model_names(&self) -> Vec<String> {
self.configs.keys().cloned().collect()
}
/// Get all configured providers
pub fn providers(&self) -> Vec<String> {
let mut providers: Vec<String> =
self.configs.values().map(|c| c.provider.clone()).collect();
providers.sort();
providers.dedup();
providers
}
/// Fetch the provider's advertised model list
pub async fn fetch_provider_models(&mut self) -> anyhow::Result<Vec<String>> {
if let Some(client) = &self.provider_client {
match client.list_models().await {
Ok(models) => {
self.discovered_models = models.clone();
info!("🌐 Fetched {} models from the provider", models.len());
Ok(models)
}
Err(e) => {
tracing::warn!("Failed to fetch the provider's model list: {}", e);
Ok(vec![])
}
}
} else {
Ok(vec![])
}
}
/// Get all models (provider-discovered + configured)
pub fn all_models(&self) -> Vec<String> {
let mut models = self.discovered_models.clone();
for name in self.configs.keys() {
if !models.contains(name) {
models.push(name.clone());
}
}
models
}
/// Set the selected model
pub fn set_model(&mut self, name: &str) -> anyhow::Result<()> {
if !self.all_models().contains(&name.to_string()) {
anyhow::bail!(
"Model '{}' not found. Available: {:?}",
name,
self.all_models()
);
}
self.selected_model = name.to_string();
info!("🎯 Selected model: {}", name);
Ok(())
}
/// Get current selected model
pub fn current_model(&self) -> &str {
if self.selected_model.is_empty() {
// Return first configured model or empty string
self.configs.keys().next().map(|s| s.as_str()).unwrap_or("")
} else {
&self.selected_model
}
}
/// Check if the model came from provider discovery
pub fn is_discovered_model(&self, name: &str) -> bool {
self.discovered_models.contains(&name.to_string())
}
/// Get model info for display
pub fn model_info(&self, name: &str) -> Option<ModelInfo> {
self.configs.get(name).map(|cfg| ModelInfo {
name: name.to_string(),
provider: cfg.provider.clone(),
context_limit: cfg.context_limit,
output_limit: cfg.output_limit,
preferred_for: cfg.preferred_for.clone(),
from_discovery: self.is_discovered_model(name),
})
}
}
/// Model information for display
#[derive(Debug, Clone)]
pub struct ModelInfo {
pub name: String,
pub provider: String,
pub context_limit: usize,
pub output_limit: usize,
pub preferred_for: Vec<TaskType>,
pub from_discovery: bool,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::config::TaskType;
fn test_configs() -> HashMap<String, ModelConfig> {
let mut map = HashMap::new();
map.insert(
"kimi-k2-5".to_string(),
ModelConfig {
provider: "bifrost".to_string(),
model: "kimi-k2.5".to_string(),
context_limit: 128000,
output_limit: 8192,
archivist_threshold: 0.7,
archivist_interval: 100,
preferred_for: vec![TaskType::Synthesis],
supports_images: true,
},
);
map
}
#[tokio::test]
async fn test_context_pressure_normal() {
let router = ModelRouter::new(test_configs());
assert_eq!(
router.context_pressure("kimi-k2-5", None).await,
ContextPressure::Normal
);
}
#[tokio::test]
async fn test_context_pressure_critical() {
let router = ModelRouter::new(test_configs());
router.update_usage(100_000, 90_000, 10_000).await;
assert_eq!(
router.context_pressure("kimi-k2-5", None).await,
ContextPressure::Critical
);
}
#[tokio::test]
async fn test_context_pressure_with_agent_override() {
let router = ModelRouter::new(test_configs());
router.update_usage(90_000, 80_000, 10_000).await;
// With 90k/128k ≈ 0.7 — critical at threshold 0.7
assert_eq!(
router.context_pressure("kimi-k2-5", None).await,
ContextPressure::Critical
);
// With 90k/64k ≈ 1.4 — also critical
assert_eq!(
router.context_pressure("kimi-k2-5", Some(64000)).await,
ContextPressure::Critical
);
}
#[tokio::test]
async fn test_token_counter() {
let counter = TokenCounter::new();
let count = counter.count("Hello, world!");
assert!(count > 0);
}
}

View file

@ -0,0 +1,75 @@
#![allow(dead_code)] // WIP scaffolding not yet wired
//! The model catalog for the OAuth provider.
//!
//! The ChatGPT codex backend exposes no usable `/v1/models` listing, so — like
//! Letta — we ship a curated list of models a ChatGPT Plus/Pro/Team
//! subscription can drive through `backend-api/codex`. The `/models` *poll*
//! lives on the OpenAI-compatible (gateway) provider; this is its OAuth-side
//! counterpart.
/// `(model id, context window tokens)`.
pub const MODELS: &[(&str, usize)] = &[
("gpt-5.5", 272_000),
("gpt-5.5-codex", 272_000),
("gpt-5.4", 272_000),
("gpt-5.4-codex", 272_000),
("gpt-5.1", 272_000),
("gpt-5.1-codex", 272_000),
("gpt-4o", 128_000),
("o3", 200_000),
("o4-mini", 200_000),
];
/// Used when neither the requested model nor the configured fallback names a
/// model this backend serves.
pub const DEFAULT_MODEL: &str = "gpt-5.5";
pub fn model_ids() -> Vec<String> {
MODELS.iter().map(|(name, _)| (*name).to_string()).collect()
}
pub fn context_window(model: &str) -> usize {
MODELS
.iter()
.find(|(name, _)| *name == model)
.map(|(_, ctx)| *ctx)
.unwrap_or(128_000)
}
pub fn is_known(model: &str) -> bool {
MODELS.iter().any(|(name, _)| *name == model)
}
/// Strip a gateway routing prefix (`"openai/gpt-5.5"` → `"gpt-5.5"`) and
/// Bifrost's `-precision` fallback suffix, which the ChatGPT backend has no
/// concept of. A trailing `-fast` is preserved so the payload builder can map
/// it to a priority service tier.
fn strip_routing(requested: &str) -> &str {
let bare = requested.rsplit('/').next().unwrap_or(requested);
bare.strip_suffix("-precision").unwrap_or(bare)
}
/// Resolve an engine-supplied model id onto one the ChatGPT codex backend
/// actually serves.
///
/// The engine is provider-agnostic and threads Bifrost-namespaced ids
/// (`openai/kimi-k2.6`, hardcoded subsystem fallbacks like `openai/glm-5.1`,
/// etc.) through every call. Those are meaningless to the codex backend, so the
/// provider translates at its boundary: strip the routing prefix/suffix, and if
/// the result still isn't in the catalog, fall back to `fallback` (the
/// provider's own default), then to [`DEFAULT_MODEL`]. This keeps the OAuth
/// provider runnable no matter what vocabulary the engine speaks.
pub fn resolve(requested: &str, fallback: &str) -> String {
let bare = strip_routing(requested);
let base = bare.strip_suffix("-fast").unwrap_or(bare);
if is_known(base) {
return bare.to_string();
}
let fb = strip_routing(fallback);
let fb_base = fb.strip_suffix("-fast").unwrap_or(fb);
if is_known(fb_base) {
fb.to_string()
} else {
DEFAULT_MODEL.to_string()
}
}

View file

@ -0,0 +1,107 @@
//! Reading and writing the Codex CLI's ChatGPT OAuth login (`~/.codex/auth.json`).
use std::path::PathBuf;
use anyhow::{anyhow, Context, Result};
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine as _;
use serde_json::Value;
/// The ChatGPT OAuth credential, as minted by `codex login`.
#[derive(Debug, Clone)]
pub struct CodexCredentials {
pub access_token: String,
pub refresh_token: String,
/// `ChatGPT-Account-Id` header value.
pub account_id: String,
/// Unix epoch milliseconds when the access token expires.
pub expires_at: i64,
}
pub fn now_ms() -> i64 {
chrono::Utc::now().timestamp_millis()
}
/// `$CODEX_HOME` if set, else `~/.codex`.
fn codex_home() -> PathBuf {
if let Ok(dir) = std::env::var("CODEX_HOME") {
if !dir.trim().is_empty() {
return PathBuf::from(shellexpand::tilde(&dir).into_owned());
}
}
dirs::home_dir().unwrap_or_default().join(".codex")
}
fn auth_path() -> PathBuf {
codex_home().join("auth.json")
}
/// Decode a JWT's `exp` claim (seconds) into epoch milliseconds.
fn jwt_exp_ms(token: &str) -> Option<i64> {
let payload = token.split('.').nth(1)?;
let bytes = URL_SAFE_NO_PAD.decode(payload).ok()?;
let json: Value = serde_json::from_slice(&bytes).ok()?;
Some(json.get("exp")?.as_i64()? * 1000)
}
/// Read the Codex CLI login from disk.
pub fn read() -> Result<CodexCredentials> {
let path = auth_path();
let raw = std::fs::read_to_string(&path)
.with_context(|| format!("reading codex auth at {}", path.display()))?;
let json: Value = serde_json::from_str(&raw)
.with_context(|| format!("parsing codex auth at {}", path.display()))?;
let tokens = json
.get("tokens")
.ok_or_else(|| anyhow!("codex auth.json has no `tokens` (run `codex login`)"))?;
let access = tokens
.get("access_token")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow!("codex auth.json missing tokens.access_token"))?
.to_string();
let refresh = tokens
.get("refresh_token")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow!("codex auth.json missing tokens.refresh_token"))?
.to_string();
let account_id = tokens
.get("account_id")
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string();
// Prefer the JWT's own expiry; fall back to file mtime + 1h.
let expires_at = jwt_exp_ms(&access).unwrap_or_else(|| {
let mtime = std::fs::metadata(&path)
.ok()
.and_then(|m| m.modified().ok())
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_millis() as i64);
mtime.unwrap_or_else(now_ms) + 3_600_000
});
Ok(CodexCredentials {
access_token: access,
refresh_token: refresh,
account_id,
expires_at,
})
}
/// Write rotated tokens back into `auth.json`, preserving every other field.
/// Best-effort: the Codex CLI may own the file, so callers ignore failure.
pub fn write_back(creds: &CodexCredentials) -> Result<()> {
let path = auth_path();
let raw = std::fs::read_to_string(&path)
.with_context(|| format!("reading codex auth at {}", path.display()))?;
let mut json: Value = serde_json::from_str(&raw)?;
let tokens = json
.get_mut("tokens")
.ok_or_else(|| anyhow!("codex auth.json has no `tokens`"))?;
tokens["access_token"] = Value::String(creds.access_token.clone());
tokens["refresh_token"] = Value::String(creds.refresh_token.clone());
std::fs::write(&path, serde_json::to_string_pretty(&json)?)
.with_context(|| format!("writing codex auth at {}", path.display()))?;
Ok(())
}

10
src/bridge/oauth/mod.rs Normal file
View file

@ -0,0 +1,10 @@
//! Riding the Codex CLI's ChatGPT OAuth login.
//!
//! Souveraine does not run its own ChatGPT login flow — it reads the token the
//! Codex CLI already minted (`~/.codex/auth.json`), refreshes it against
//! OpenAI's token endpoint when it ages out, and writes the rotated token back
//! so the two stay in sync. The same "ride the CLI" move OpenClaw makes.
pub mod catalog;
pub mod codex_creds;
pub mod refresh;

101
src/bridge/oauth/refresh.rs Normal file
View file

@ -0,0 +1,101 @@
//! ChatGPT OAuth token refresh.
//!
//! Self-refresh against OpenAI's token endpoint using the Codex CLI's public
//! client id, then write the rotated token back to `~/.codex/auth.json`.
//! Refresh tokens are one-time-use, so two processes refreshing the same token
//! race; the caller serializes us behind a mutex (single-flight), and if the
//! refresh token was already spent we re-read the file — the CLI may have
//! rotated it out from under us.
use anyhow::{anyhow, Context, Result};
use serde_json::Value;
use super::codex_creds::{self, now_ms, CodexCredentials};
const TOKEN_URL: &str = "https://auth.openai.com/oauth/token";
/// Public client id of the Codex CLI OAuth app — the refresh must be attributed
/// to the same client that minted the token.
const CLIENT_ID: &str = "app_EMoamEEZ73f0CkXaXp7hrann";
/// Refresh this many ms before the token actually expires.
const REFRESH_BUFFER_MS: i64 = 300_000;
fn is_expired(creds: &CodexCredentials) -> bool {
now_ms() >= creds.expires_at - REFRESH_BUFFER_MS
}
/// Ensure `creds` holds a fresh access token, refreshing in place if needed.
pub async fn ensure_fresh(http: &reqwest::Client, creds: &mut CodexCredentials) -> Result<()> {
if !is_expired(creds) {
return Ok(());
}
match refresh(http, creds).await {
Ok(fresh) => {
*creds = fresh;
// Best-effort: keep the CLI's file in sync. The CLI may own it.
let _ = codex_creds::write_back(creds);
Ok(())
}
Err(e) => {
// Refresh failed — perhaps the CLI already rotated the token. If the
// file now holds a newer, still-valid token, adopt it.
if let Ok(reread) = codex_creds::read() {
if reread.access_token != creds.access_token && now_ms() < reread.expires_at {
*creds = reread;
return Ok(());
}
}
Err(e)
}
}
}
async fn refresh(http: &reqwest::Client, creds: &CodexCredentials) -> Result<CodexCredentials> {
let resp = http
.post(TOKEN_URL)
.form(&[
("grant_type", "refresh_token"),
("refresh_token", creds.refresh_token.as_str()),
("client_id", CLIENT_ID),
])
.send()
.await
.context("posting ChatGPT OAuth token refresh")?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
return Err(anyhow!(
"ChatGPT token refresh failed ({}): {}",
status,
body
));
}
let json: Value = resp
.json()
.await
.context("parsing token refresh response")?;
let access = json
.get("access_token")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow!("token refresh response missing access_token"))?
.to_string();
// OpenAI may or may not rotate the refresh token; keep the old one if not.
let refresh_token = json
.get("refresh_token")
.and_then(|v| v.as_str())
.map(String::from)
.unwrap_or_else(|| creds.refresh_token.clone());
let expires_in = json
.get("expires_in")
.and_then(|v| v.as_i64())
.unwrap_or(3600);
Ok(CodexCredentials {
access_token: access,
refresh_token,
account_id: creds.account_id.clone(),
expires_at: now_ms() + expires_in * 1000,
})
}

File diff suppressed because it is too large Load diff

43
src/bridge/provider.rs Normal file
View file

@ -0,0 +1,43 @@
#![allow(dead_code)] // WIP scaffolding not yet wired
//! The `LlmProvider` trait — the engine↔LLM seam.
//!
//! This is the inference-side sibling of the harness↔engine [`Backend`] trait
//! (`crate::backend`). Where `Backend` decides *where the engine runs* (in
//! process vs. a remote `souveraine server`), `LlmProvider` decides *who
//! answers the model call*. Everything the engine speaks is the existing
//! OpenAI-chat currency (`ChatCompletionRequest` / `CompletionResult` /
//! `InferenceStrain`); an implementation that talks a different wire format
//! (e.g. the ChatGPT Responses API) adapts internally and hands back that same
//! currency, so the primary loop, subconscious, archivist, reflection, and
//! compaction paths never learn which provider they are talking to.
use anyhow::Result;
use async_trait::async_trait;
use super::openai_compatible::{ChatCompletionRequest, CompletionResult, InferenceStrain};
#[async_trait]
pub trait LlmProvider: Send + Sync {
/// Stable provider id for logs / routing, e.g. `"bifrost"` or `"openai-oauth"`.
fn id(&self) -> &str;
/// The model used when a request does not pin one of its own.
fn default_model(&self) -> &str;
/// Models this provider can drive — feeds the `/model` picker and
/// `ModelRouter`'s merged catalog.
async fn list_models(&self) -> Result<Vec<String>>;
/// The one inference verb. Returns the completion plus any strain the body
/// felt (retries, rate limits) so the organism can register provider health
/// on the event bus / TUI.
async fn chat_completion_with_strain(
&self,
request: ChatCompletionRequest,
) -> Result<(CompletionResult, Vec<InferenceStrain>)>;
/// Convenience wrapper that drops the strain channel.
async fn chat_completion(&self, request: ChatCompletionRequest) -> Result<CompletionResult> {
Ok(self.chat_completion_with_strain(request).await?.0)
}
}

View file

@ -0,0 +1,7 @@
//! Concrete [`LlmProvider`](crate::bridge::provider::LlmProvider) implementations.
//!
//! `OpenAiCompatibleClient` (the OpenAI-compatible gateway client) implements the trait
//! in `bridge::openai_compatible`; the OAuth-riding ChatGPT provider lives here.
pub mod openai_oauth;
pub mod responses;

View file

@ -0,0 +1,188 @@
//! OpenAI as an OAuth-riding provider.
//!
//! Drives ChatGPT Plus/Pro/Team models through
//! `chatgpt.com/backend-api/codex/responses`, authenticated by the Codex CLI's
//! OAuth token (no API key). Translates the engine's OpenAI-chat request into
//! the Responses API, accumulates the SSE stream back into a `CompletionResult`,
//! and synthesizes `InferenceStrain` from retries so the body still feels a
//! hoarse provider.
use std::sync::Arc;
use std::time::Duration;
use anyhow::{anyhow, Context, Result};
use async_trait::async_trait;
use tokio::sync::Mutex;
use tracing::warn;
use crate::bridge::openai_compatible::{
ChatCompletionRequest, CompletionResult, InferenceStrain, RetryPolicy,
};
use crate::bridge::oauth::codex_creds::{self, CodexCredentials};
use crate::bridge::oauth::{catalog, refresh};
use crate::bridge::provider::LlmProvider;
use super::responses;
const ENDPOINT: &str = "https://chatgpt.com/backend-api/codex/responses";
pub struct OpenAiOAuthProvider {
http: reqwest::Client,
/// The mutex serializes refreshes — a one-time-use refresh token must not be
/// spent by two concurrent calls (single-flight).
creds: Arc<Mutex<CodexCredentials>>,
default_model: String,
retry: RetryPolicy,
}
impl OpenAiOAuthProvider {
/// Build by riding the Codex CLI's existing ChatGPT login.
pub fn from_codex_login(default_model: String, timeout_secs: u64) -> Result<Self> {
let creds = codex_creds::read()
.context("reading Codex CLI ChatGPT login (run `codex login` first)")?;
let http = reqwest::Client::builder()
.timeout(Duration::from_secs(timeout_secs))
.build()
.context("building OpenAI OAuth reqwest client")?;
// The configured `primary_model` may be a provider-namespaced id
// (`openai/…`); pin the provider default to a model this backend serves.
let default_model = catalog::resolve(&default_model, catalog::DEFAULT_MODEL);
Ok(Self {
http,
creds: Arc::new(Mutex::new(creds)),
default_model,
retry: RetryPolicy::default(),
})
}
/// Refresh if needed and return a usable `(access_token, account_id)`.
async fn auth(&self) -> Result<(String, String)> {
let mut creds = self.creds.lock().await;
refresh::ensure_fresh(&self.http, &mut creds).await?;
Ok((creds.access_token.clone(), creds.account_id.clone()))
}
}
#[async_trait]
impl LlmProvider for OpenAiOAuthProvider {
fn id(&self) -> &str {
"openai-oauth"
}
fn default_model(&self) -> &str {
&self.default_model
}
async fn list_models(&self) -> Result<Vec<String>> {
Ok(catalog::model_ids())
}
async fn chat_completion_with_strain(
&self,
request: ChatCompletionRequest,
) -> Result<(CompletionResult, Vec<InferenceStrain>)> {
// Translate the engine's (possibly provider-namespaced) model id onto a
// model the codex backend actually serves before building the payload.
let mut request = request;
request.model = catalog::resolve(&request.model, &self.default_model);
let payload = responses::build_payload(&request);
let model = request.model.clone();
let mut strain: Vec<InferenceStrain> = Vec::new();
for attempt in 0..=self.retry.max_retries {
let (access, account) = self.auth().await?;
let resp = self
.http
.post(ENDPOINT)
.header("Authorization", format!("Bearer {}", access))
.header("ChatGPT-Account-Id", account)
.header("OpenAI-Beta", "responses=v1")
.header("OpenAI-Originator", "codex")
.header("Content-Type", "application/json")
.header("Accept", "text/event-stream")
.json(&payload)
.send()
.await;
let resp = match resp {
Ok(r) => r,
Err(e) if e.is_timeout() || e.is_connect() => {
if attempt == self.retry.max_retries {
return Err(anyhow!(
"ChatGPT backend unreachable after {} attempts: {}",
attempt + 1,
e
));
}
let delay = backoff(attempt, &self.retry);
warn!(
"ChatGPT backend connect failed (attempt {}), retrying in {:?}: {}",
attempt, delay, e
);
strain.push(InferenceStrain::Transient {
attempt,
status: 0,
model: model.clone(),
delay_ms: delay.as_millis() as u64,
});
tokio::time::sleep(delay).await;
continue;
}
Err(e) => return Err(e.into()),
};
let status = resp.status();
if status.is_success() {
let body = resp
.text()
.await
.context("reading ChatGPT responses stream")?;
let result = responses::accumulate_sse(&body)?;
return Ok((result, strain));
}
let body = resp.text().await.unwrap_or_default();
let transient = matches!(status.as_u16(), 408 | 429 | 500 | 502 | 503 | 504);
if transient && attempt < self.retry.max_retries {
let delay = backoff(attempt, &self.retry);
warn!(
"ChatGPT backend {} (attempt {}), retrying in {:?}",
status.as_u16(),
attempt,
delay
);
strain.push(InferenceStrain::Transient {
attempt,
status: status.as_u16(),
model: model.clone(),
delay_ms: delay.as_millis() as u64,
});
tokio::time::sleep(delay).await;
continue;
}
strain.push(InferenceStrain::Exhausted {
attempts: attempt + 1,
status: status.as_u16(),
model: model.clone(),
body: body.chars().take(300).collect(),
});
return Err(anyhow!(
"ChatGPT backend returned {} after {} attempt(s): {}",
status,
attempt + 1,
body.chars().take(500).collect::<String>()
));
}
anyhow::bail!("OpenAI OAuth retry loop exhausted without returning a result")
}
}
fn backoff(attempt: u32, policy: &RetryPolicy) -> Duration {
let base = policy
.base_delay_ms
.saturating_mul(2u64.saturating_pow(attempt));
Duration::from_millis(base.min(policy.max_delay_ms))
}

Some files were not shown because too many files have changed in this diff Show more