Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
target/
node_modules/
dist/
# Tool-owned reference checkouts, benchmark reports, and temporary receipts.
out/
assets/*.vrm
assets/*.vrma
Expand Down
46 changes: 44 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 13 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
[workspace]
resolver = "2"
members = ["crates/pocket-character", "crates/pocket-character-core"]
members = [
"crates/pocket-character",
"crates/pocket-character-core",
"crates/pocket-persona",
]
# The vendored engine keeps its own workspaces (Cargo would otherwise try to
# adopt the path-dependency crates into this one and break their inheritance).
exclude = ["vendor"]
Expand All @@ -14,11 +18,12 @@ repository = "https://github.com/pocket-stack/pocket-character"
[workspace.dependencies]
# The engine family is vendored as a git submodule (vendor/pocketjs) so the
# Rust crates and the JS framework/build pipeline stay pinned to ONE commit.
pocket3d = { path = "vendor/pocketjs/pocket3d/crates/pocket3d" }
pocket-vrm = { path = "vendor/pocketjs/pocket3d/crates/pocket-vrm" }
pocket-mod = { path = "vendor/pocketjs/pocket3d/crates/pocket-mod" }
pocket-ui-wgpu = { path = "vendor/pocketjs/pocket3d/crates/pocket-ui-wgpu" }
pocketjs-core = { path = "vendor/pocketjs/core", features = ["std"] }
pocket3d = { path = "vendor/pocketjs/engine/pocket3d/crates/pocket3d" }
pocket-vrm = { path = "vendor/pocketjs/engine/crates/pocket-vrm" }
pocket-mod = { path = "vendor/pocketjs/engine/crates/pocket-mod" }
pocket-ui-wgpu = { path = "vendor/pocketjs/engine/crates/pocket-ui-wgpu" }
pocket-widget = { path = "vendor/pocketjs/engine/crates/pocket-widget" }
pocketjs-core = { path = "vendor/pocketjs/engine/core", features = ["std"] }
pocket-character-core = { path = "crates/pocket-character-core" }

wgpu = "25"
Expand All @@ -27,6 +32,8 @@ glam = "0.33"
anyhow = "1"
log = "0.4"
env_logger = "0.11"
serde = { version = "1", features = ["derive"] }
serde_json = "1"

[profile.dev]
opt-level = 1
Expand Down
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,36 @@ cost on the Pocket architecture instead of Electron?* See
[DESIGN.md](DESIGN.md) for the architecture and the parity contract, and the
measurement section below for the answer.

## Persona parity POC

This repository also owns the Pocket-native vertical slice of
[xikhar/persona](https://github.com/xikhar/persona). The two visual acceptance
commands prepare a pinned reference checkout, validate and stage the same local
VRM/VRMA inputs, build the selected target, launch it, and drive the same
idle/speaking/lip-sync/action sequence:

```sh
bun run accept:persona
bun run accept:pocket
```

Visual acceptance includes the whole pose, not only a running window: compare
the shoulders, wrists/hands, hips, knees, and ankles/feet through idle and
speaking, and reject any persistent rest-axis twist. Press Ctrl-C to terminate
the complete target process tree. For the sequential resource comparison:

```sh
bun run bench:persona
bun run bench:persona:speaking
bun run bench:persona:controlled
```

The production commands compare stock Persona at its display-driven rate with
Pocket at its intended 30 fps / 2048 texture cap, in idle or sustained-speaking
state. See [docs/PERSONA.md](docs/PERSONA.md) for the latest upstream pin,
parity boundary, benchmark methodology, measurements, and asset-license
constraints.

## What it does

- **AvatarSample_A** (VRoid official sample) with airi's `idle_loop.vrma`
Expand Down
21 changes: 21 additions & 0 deletions crates/pocket-persona/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
name = "pocket-persona"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
description = "Persona-compatible native VRM desktop character proof of concept"

[dependencies]
pocket3d = { workspace = true }
pocket-mod = { workspace = true }
pocket-vrm = { workspace = true }
pocket-widget = { workspace = true }
wgpu = { workspace = true }
winit = { workspace = true }
glam = { workspace = true, features = ["std"] }
anyhow = { workspace = true }
log = { workspace = true }
env_logger = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
23 changes: 23 additions & 0 deletions crates/pocket-persona/guest/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { onPersonaTick, persona } from "./sdk";

console.log(
`pocket-persona: model=${persona.boot.model}`,
`actions=[${persona.boot.actions.join(", ")}]`,
);

// The native core owns continuous animation, facial motion, and physics.
// This deliberately small Pocket guest is the hot-swappable personality
// seam: product-specific reactions can be added without rebuilding Rust.
let lastHeartbeat = 0;
onPersonaTick((state, events) => {
for (const event of events) {
console.log(`pocket-persona: ${event.type}=${event.value}`);
}
if (state.t - lastHeartbeat >= 60) {
lastHeartbeat = state.t;
console.log(
`pocket-persona: t=${state.t.toFixed(0)}s animation=${state.animation}`,
`level=${state.audioLevel.toFixed(2)} fps=${state.renderFps.toFixed(1)}`,
);
}
});
38 changes: 38 additions & 0 deletions crates/pocket-persona/guest/sdk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
export interface PersonaTick {
t: number;
activity: "idle" | "listening" | "speaking";
audioLevel: number;
animation: string;
blink: number;
renderFps: number;
}

export interface PersonaEvent {
type: "animationChanged" | "voiceChanged";
value: string;
}

interface PersonaSurface {
readonly boot: {
readonly model: string;
readonly actions: readonly string[];
};
playAnimation(name: string): void;
setExpression(name: string, weight: number): void;
quit(): void;
__dispatch?: (state: PersonaTick, events: PersonaEvent[]) => void;
}

declare global {
// Mounted by the native `pocket-mod` host before this bundle is evaluated.
// eslint-disable-next-line no-var
var persona: PersonaSurface;
}

export const persona = globalThis.persona;

export function onPersonaTick(
callback: (state: PersonaTick, events: PersonaEvent[]) => void,
): void {
globalThis.persona.__dispatch = callback;
}
Loading