fix(stage-ui, stage-ui-live2d): repair broken Live2D expression pipeline and add config-driven emotion mapping#1890
Conversation
There was a problem hiding this comment.
💡 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), |
There was a problem hiding this comment.
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 }) |
There was a problem hiding this comment.
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.
7a1a0c8 to
d7a132b
Compare
The Live2D expression pipeline was broken at multiple levels, preventing
ACT token emotions (
<|ACT {"emotion":"happy"}|>) from producing anyvisible 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)hardcodedtrue → 1for every parameter, but manyexp3.json target values are negative (e.g.
-1for eyebrow depression).Sad / Awkward / Surprise expressions were literally inverted.
→
set()now reads the per-parametervaluefrom the group definitionfor
true, anddefaultValueforfalse. Number values are passedthrough directly.
expression_set LLM tool bypassing the fix
The tool converted
boolean → 1/0before callingstore.set(),bypassing the per-parameter logic above.
→ Passes the raw boolean through.
ACT token pipeline fix (packages/stage-ui)
emotion swallowed by motion handler
onSignalreturned early whenact.motionwas present, skippingthe 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 YAMLparser, 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 i18nprefixis 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 ACTinstruction guard string and replaces them.
Emotion → expression auto-detection (packages/stage-ui)
New:
emotion-expression-mappings.tsTwo tiers:
STANDARD_EXP_CONVENTION— maps Emotion enum values to theexp_01–exp_08naming convention used by most Live2D samplemodels (hiyori, mao, etc.). Works for any model regardless of ID,
as long as the expression groups follow this convention.
EMOTION_EXPRESSION_MAPPINGS— an empty per-model overridemap. 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 theloaded model.
Debug logging (packages/core-agent, stage-ui, stage-ui-live2d, stage-web)
Added a Vite
/__airi-logmiddleware and per-span logging across thefull LLM request/response chain and emotion pipeline. Enables diagnosis
without code patches.
Files changed
packages/stage-ui-live2d/src/stores/expression-store.tsset()boolean logicpackages/stage-ui-live2d/src/tools/expression-tools.tsstore.set()packages/stage-ui-live2d/src/composables/live2d/expression-controller.tspackages/stage-ui-live2d/src/components/scenes/live2d/Model.vuepackages/stage-ui/src/components/scenes/Stage.vuepackages/stage-ui/src/constants/emotion-expression-mappings.tspackages/stage-ui/src/constants/prompts/system-v2.tspackages/stage-ui/src/stores/modules/airi-card.tspackages/stage-ui/src/stores/chat/session-store.tspackages/core-agent/src/runtime/chat-orchestrator-runtime.tsapps/stage-web/vite.config.ts/__airi-logdebug middlewareHow to map a custom model
Edit
packages/stage-ui/src/constants/emotion-expression-mappings.ts:Models that follow the
exp_01–exp_08convention (hiyori, mao, etc.)work out of the box with no configuration.
Verification
pnpm -F @proj-airi/core-agent build.airi-debug.logforhasACTInstruction: trueand[Stage] Live2D expression result → success: true