chenbhao Claude Big Pickle commited on
Commit
b788554
·
1 Parent(s): 7ec132a

fix: /friend motion — interrupt hold on new action, drain queue, fix emotion mapping

Browse files

- MotionController: interrupt hold when new playAction arrives instead of
queueing, so LLM text-message actions are not blocked by stale 10s hold
- MotionController: drain action queue in hold timer callback to prevent
silent drop of queued actions
- App.tsx: remove hold=true from emotion-only messages — action plays once,
emotion timer handles duration independently
- App.tsx: remove awkward emotion→action mappings (neutral→salute,
relaxed→salute, sad→shy) that caused unnatural gestures

Co-Authored-By: Claude Big Pickle <noreply@anthropic.com>

src/components/friend/frontend/App.tsx CHANGED
@@ -14,6 +14,8 @@ import { Menu, Move, Rotate3D, EyeOff, Settings, RefreshCw, Pin } from 'lucide-r
14
  const DEFAULT_MODEL = '/friend/model1.vrm'
15
 
16
  // 情绪 → 动作映射
 
 
17
  const emotionActionMap: Record<string, string> = {
18
  think: 'scratchHead',
19
  question: 'point',
@@ -22,12 +24,12 @@ const emotionActionMap: Record<string, string> = {
22
  surprised: 'excited',
23
  angry: 'angry',
24
  awkward: 'playFingers',
25
- sad: 'shy',
26
  love: 'shy',
27
  flirty: 'shy',
28
  greeting: 'greeting',
29
- relaxed: 'salute',
30
- neutral: 'salute',
 
31
  }
32
 
33
  const btnStyle: React.CSSProperties = {
@@ -122,7 +124,10 @@ export default function App() {
122
  if (action) sceneRef.current.playAction(action)
123
  } else {
124
  sceneRef.current.setEmotionWithReset(msg.emotion, msg.emotionDuration ?? 10000, msg.emotionIntensity)
125
- if (action) sceneRef.current.playAction(action, true)
 
 
 
126
  }
127
  }
128
  }, [])
 
14
  const DEFAULT_MODEL = '/friend/model1.vrm'
15
 
16
  // 情绪 → 动作映射
17
+ // 只有有明确肢体动作关联的情绪才映射;neutral/relaxed 不绑动作,
18
+ // 避免角色在无明确意图时做出违和姿势。
19
  const emotionActionMap: Record<string, string> = {
20
  think: 'scratchHead',
21
  question: 'point',
 
24
  surprised: 'excited',
25
  angry: 'angry',
26
  awkward: 'playFingers',
 
27
  love: 'shy',
28
  flirty: 'shy',
29
  greeting: 'greeting',
30
+ sad: '',
31
+ relaxed: '',
32
+ neutral: '',
33
  }
34
 
35
  const btnStyle: React.CSSProperties = {
 
124
  if (action) sceneRef.current.playAction(action)
125
  } else {
126
  sceneRef.current.setEmotionWithReset(msg.emotion, msg.emotionDuration ?? 10000, msg.emotionIntensity)
127
+ // No hold — action plays once, emotion timer handles duration.
128
+ // Hold would lock _actionPlaying for 10s, blocking text-message actions
129
+ // when friend_emotion tool fires before broadcastResponse.
130
+ if (action) sceneRef.current.playAction(action)
131
  }
132
  }
133
  }, [])
src/components/friend/frontend/motion-controller.ts CHANGED
@@ -187,9 +187,16 @@ export class MotionController {
187
  const preset = actionPresets[name]
188
  if (!preset) { console.warn('[Motion] unknown action:', name); return }
189
 
190
- // If already playing an action, queue instead of dropping
191
- if (this._actionPlaying) {
192
- // Avoid duplicate consecutive queued items
 
 
 
 
 
 
 
193
  const last = this.actionQueue[this.actionQueue.length - 1]
194
  if (!last || last.name !== name) {
195
  this.actionQueue.push({ name, hold })
@@ -247,6 +254,15 @@ export class MotionController {
247
  if (hold) {
248
  this.holdTimer = setTimeout(() => {
249
  if (gen !== this._actionGeneration) return
 
 
 
 
 
 
 
 
 
250
  this._actionPlaying = false
251
  this.startIdle()
252
  }, 10000)
 
187
  const preset = actionPresets[name]
188
  if (!preset) { console.warn('[Motion] unknown action:', name); return }
189
 
190
+ // If currently holding (pose frozen, not actively animating), interrupt hold
191
+ // and play the new action immediately. This prevents LLM text-message actions
192
+ // from being blocked by a stale 10s hold timer.
193
+ if (this._actionPlaying && this.holdTimer !== null) {
194
+ this.clearTimers()
195
+ this._actionPlaying = false
196
+ // fall through to play new action
197
+ } else if (this._actionPlaying) {
198
+ // Normal queue: only when an action is actively animating (e.g. two rapid
199
+ // LLM tool calls). Hold does NOT queue — it interrupts.
200
  const last = this.actionQueue[this.actionQueue.length - 1]
201
  if (!last || last.name !== name) {
202
  this.actionQueue.push({ name, hold })
 
254
  if (hold) {
255
  this.holdTimer = setTimeout(() => {
256
  if (gen !== this._actionGeneration) return
257
+ this.clearTimers()
258
+ // Drain any actions queued during hold before going idle —
259
+ // prevents the queue leak where LLM actions are silently dropped.
260
+ const queued = this.actionQueue.shift()
261
+ if (queued) {
262
+ this._actionPlaying = false
263
+ this.playAction(queued.name, queued.hold)
264
+ return
265
+ }
266
  this._actionPlaying = false
267
  this.startIdle()
268
  }, 10000)