Skip to content

fix(stage-ui, stage-ui-live2d): repair broken Live2D expression pipeline and add config-driven emotion mapping#1890

Closed
LeventureQys wants to merge 1 commit into
moeru-ai:mainfrom
LeventureQys:Emotion-Expression-Fix
Closed

fix(stage-ui, stage-ui-live2d): repair broken Live2D expression pipeline and add config-driven emotion mapping#1890
LeventureQys wants to merge 1 commit into
moeru-ai:mainfrom
LeventureQys:Emotion-Expression-Fix

Conversation

@LeventureQys

Copy link
Copy Markdown

The Live2D expression pipeline was broken at multiple levels, preventing
ACT token emotions (<|ACT {"emotion":"happy"}|>) from producing any
visible facial expression. This PR fixes the core bugs, hardcodes the
ACT token instruction to work around an i18n YAML truncation issue, and
introduces an auto-detection mechanism that works across all Live2D
sample models (hiyori, mao, etc.) without hardcoding model IDs.

Changes

Core expression fixes (packages/stage-ui-live2d)

expressionStore.set() boolean inversion
set(name, true) hardcoded true → 1 for every parameter, but many
exp3.json target values are negative (e.g. -1 for eyebrow depression).
Sad / Awkward / Surprise expressions were literally inverted.

set() now reads the per-parameter value from the group definition
for true, and defaultValue for false. Number values are passed
through directly.

expression_set LLM tool bypassing the fix
The tool converted boolean → 1/0 before calling store.set(),
bypassing the per-parameter logic above.

→ Passes the raw boolean through.

ACT token pipeline fix (packages/stage-ui)

emotion swallowed by motion handler
onSignal returned early when act.motion was present, skipping
the emotion branch in the same ACT token.

→ Removed the early return.

System prompt repair (packages/stage-ui)

i18n YAML truncation of ACT instructions
The i18n YAML pipe character escaping ({'|'}) was sliced by the YAML
parser, dropping the entire ACT token instruction block. The LLM never
received the directive to control facial expressions.

→ Hardcoded the ACT_TOKEN_INSTRUCTION in system-v2.ts. The i18n
prefix is preserved at the top of the content array so language-
specific personality instructions are retained.

Stale persisted state fix (packages/stage-ui)

Card & session prompt staleness
initialize() skipped updates when a 'default' card already existed.
Old sessions kept their pre-fix system messages forever.

initialize() now always refreshes the description.
ensureSession() detects stale system messages missing the ACT
instruction guard string and replaces them.

Emotion → expression auto-detection (packages/stage-ui)

New: emotion-expression-mappings.ts

Two tiers:

  1. STANDARD_EXP_CONVENTION — maps Emotion enum values to the
    exp_01exp_08 naming convention used by most Live2D sample
    models (hiyori, mao, etc.). Works for any model regardless of ID,
    as long as the expression groups follow this convention.

  2. EMOTION_EXPRESSION_MAPPINGS — an empty per-model override
    map. For models that deviate from the standard convention, add
    entries here by model ID.

The Stage.vue handler resolves: per-model override ?? standard convention,
then calls expressionStore.set() if the resolved group exists in the
loaded model.

Debug logging (packages/core-agent, stage-ui, stage-ui-live2d, stage-web)

Added a Vite /__airi-log middleware and per-span logging across the
full LLM request/response chain and emotion pipeline. Enables diagnosis
without code patches.

Files changed

File Change
packages/stage-ui-live2d/src/stores/expression-store.ts Rewrite set() boolean logic
packages/stage-ui-live2d/src/tools/expression-tools.ts Pass raw boolean to store.set()
packages/stage-ui-live2d/src/composables/live2d/expression-controller.ts Registration log
packages/stage-ui-live2d/src/components/scenes/live2d/Model.vue Expression init logs
packages/stage-ui/src/components/scenes/Stage.vue Wire expressionStore into emotion pipeline + logging
packages/stage-ui/src/constants/emotion-expression-mappings.ts New — standard convention + per-model
overrides
packages/stage-ui/src/constants/prompts/system-v2.ts Hardcode ACT token instruction, keep i18n prefix
packages/stage-ui/src/stores/modules/airi-card.ts Force-refresh persisted card description
packages/stage-ui/src/stores/chat/session-store.ts Replace stale system messages in old sessions
packages/core-agent/src/runtime/chat-orchestrator-runtime.ts LLM request/response debug logging
apps/stage-web/vite.config.ts /__airi-log debug middleware

How to map a custom model

Edit packages/stage-ui/src/constants/emotion-expression-mappings.ts:

export const EMOTION_EXPRESSION_MAPPINGS = {
  'my-model-id': {
    [Emotion.Happy]:    'happy_face',
    [Emotion.Surprise]: 'surprised_w',
    // ...
  },
}

Models that follow the exp_01exp_08 convention (hiyori, mao, etc.)
work out of the box with no configuration.

Verification

  1. pnpm -F @proj-airi/core-agent build
  2. Start dev server and send a message
  3. Check .airi-debug.log for hasACTInstruction: true and
    [Stage] Live2D expression result → success: true

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7a1a0c8ade

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

type: 'system-prompt',
length: typeof systemMsg.content === 'string' ? systemMsg.content.length : JSON.stringify(systemMsg.content).length,
hasACTInstruction: typeof systemMsg.content === 'string' && systemMsg.content.includes('ACT'),
content: typeof systemMsg.content === 'string' ? systemMsg.content : JSON.stringify(systemMsg.content),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Remove unguarded chat transcript debug posts

When this runtime is shipped outside the stage-web dev server, these fetch('/__airi-log') calls still run for every chat turn. The payloads include the full system prompt here, plus user-message previews and the full assistant response in adjacent new blocks, so a deployed web build will POST private conversation data to the app origin/proxies even if the endpoint just 404s; desktop builds also emit the requests with no server. Please remove this debug logging or guard it behind an explicit dev-only flag before including it in the shared core-agent runtime.

Useful? React with 👍 / 👎.

if (cards.value.has('default')) {
const card = cards.value.get('default')
if (card) {
cards.value.set('default', { ...card, description })

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve existing default card prompt

When a user already has a persisted default card, initialize() now overwrites its description every time the store initializes. That field is editable through the card settings and is part of the computed chat systemPrompt, so users who customized the default ReLU card lose their saved prompt/description on startup just to refresh the stock ACT instructions. Limit the migration to cards that still have the old stock description, or add the new instruction without replacing user-edited content.

Useful? React with 👍 / 👎.

…ine and wire emotion-to-expression via config mappings

- Fix expressionStore.set() boolean inversion (true was always mapped to 1,
  ignoring negative target values in exp3.json).
- Stop onSignal from swallowing emotion when motion is present in the same
  ACT token.
- Hardcode ACT token instructions in system-v2.ts to work around i18n YAML
  pipe-character truncation, while preserving the i18n prefix.
- Force-refresh persisted card descriptions and replace stale system messages
  in old sessions.
- Fix expression_set LLM tool passing 1/0 instead of the raw boolean,
  which bypassed the set() per-parameter logic.
- Add EMOTION_EXPRESSION_MAPPINGS config file for per-model emotion-to-
  expression-group mappings (empty by default, no hardcoded model names).
- Wire Stage.vue emotionsQueue handler to read the mapping and call
  expressionStore.set() when a model has entries.
- Add debug logging across the LLM request/response chain and emotion
  pipeline via a Vite /__airi-log middleware.
@LeventureQys LeventureQys force-pushed the Emotion-Expression-Fix branch from 7a1a0c8 to d7a132b Compare May 27, 2026 19:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant