Fix: TalkingHead2.animate crash - null morph target access in animate()

#5
by bep40 - opened
Files changed (4) hide show
  1. Dockerfile +6 -1
  2. index.html +4 -2
  3. patch_talkinghead.py +95 -0
  4. src/vt_patcher.js +30 -0
Dockerfile CHANGED
@@ -6,6 +6,11 @@ WORKDIR /app
6
  COPY package.json ./
7
  RUN bun install
8
 
 
 
 
 
 
9
  # Copy remaining source (public, src, index.ts, index.html)
10
  COPY public ./public
11
  COPY src ./src
@@ -13,4 +18,4 @@ COPY index.ts index.html .
13
 
14
  ENV PORT=7860
15
 
16
- CMD ["bun", "run", "index.ts"]
 
6
  COPY package.json ./
7
  RUN bun install
8
 
9
+ # ── PATCH TalkingHead animate() to fix "Cannot read properties of undefined (reading 'value')" ──
10
+ # This prevents crashes when this.mtAvatar has null/undefined entries.
11
+ COPY patch_talkinghead.py ./
12
+ RUN python3 patch_talkinghead.py
13
+
14
  # Copy remaining source (public, src, index.ts, index.html)
15
  COPY public ./public
16
  COPY src ./src
 
18
 
19
  ENV PORT=7860
20
 
21
+ CMD ["bun", "run", "index.ts"]
index.html CHANGED
@@ -23,7 +23,7 @@
23
  <button id="settings-btn" class="icon-btn" aria-label="Settings" title="Settings">
24
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
25
  <circle cx="12" cy="12" r="3"></circle>
26
- <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06-.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06-.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
27
  </svg>
28
  </button>
29
  </header>
@@ -167,6 +167,8 @@
167
  .vaistudio-loadmore-btn:hover { background:#e2e8f0;border-color:#0077b6 }
168
  </style>
169
 
 
 
170
  <script type="module" src="./src/app.js"></script>
171
  </body>
172
- </html>
 
23
  <button id="settings-btn" class="icon-btn" aria-label="Settings" title="Settings">
24
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
25
  <circle cx="12" cy="12" r="3"></circle>
26
+ <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0 -.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
27
  </svg>
28
  </button>
29
  </header>
 
167
  .vaistudio-loadmore-btn:hover { background:#e2e8f0;border-color:#0077b6 }
168
  </style>
169
 
170
+ <!-- VT_PATCHER: Fix TalkingHead animate() crashes BEFORE app.js loads -->
171
+ <script src="./src/vt_patcher.js"></script>
172
  <script type="module" src="./src/app.js"></script>
173
  </body>
174
+ </html>
patch_talkinghead.py ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Patch @met4citizen/talkinghead/dist/talkinghead.mjs to fix:
4
+ "Cannot read properties of undefined (reading 'value')"
5
+
6
+ Root cause: this.mtRandomized contains morph keys not in this.mtAvatar for
7
+ the loaded GLB model. Line 2619: j = this.mtAvatar[i]; if (!j.needsUpdate) crashes.
8
+ """
9
+ import os, sys, re
10
+
11
+ PKG_DIR = os.path.join(os.getcwd(), 'node_modules', '@met4citizen', 'talkinghead')
12
+
13
+ dist_files = ['dist/talkinghead.mjs', 'dist/talkinghead.js']
14
+ target_path = None
15
+ for name in dist_files:
16
+ p = os.path.join(PKG_DIR, name)
17
+ if os.path.exists(p):
18
+ target_path = p
19
+ break
20
+
21
+ if not target_path:
22
+ print('ERROR: talkinghead file not found in ' + PKG_DIR)
23
+ sys.exit(1)
24
+
25
+ print('Patching: ' + target_path)
26
+ with open(target_path, 'r') as f:
27
+ code = f.read()
28
+
29
+ if '__VT_PATCHED__' in code:
30
+ print('Already patched, skipping.')
31
+ sys.exit(0)
32
+
33
+ # Fix 1: Null check for mtRandomized loop (line ~2619)
34
+ old1 = "j = this.mtAvatar[i];\n if ( !j.needsUpdate ) {"
35
+ new1 = "j = this.mtAvatar[i];\n if ( !j ) continue;\n __VT_PATCHED__=1;\n if ( !j.needsUpdate ) {"
36
+ if old1 in code:
37
+ code = code.replace(old1, new1)
38
+ print('OK Fix1: added null check for mtRandomized loop')
39
+ else:
40
+ code = re.sub(
41
+ r'(j = this\.mtAvatar\[i\];)\s*\n\s*if\s*\(\s*!j\.needsUpdate',
42
+ r'\1\n if ( !j ) continue;\n __VT_PATCHED__=1;\n if ( !j.needsUpdate',
43
+ code
44
+ )
45
+
46
+ # Fix 2: Guard direct .value accesses on specific keys
47
+ for key in ['bodyRotateY', 'eyeLookInLeft', 'eyeLookOutLeft', 'eyesLookDown',
48
+ 'browDownLeft', 'browDownRight', 'eyeBlinkLeft', 'eyeBlinkRight',
49
+ 'eyesLookUp', 'bodyRotateX', 'bodyRotateZ', 'headRotateX',
50
+ 'headRotateY', 'headRotateZ']:
51
+ full_access = 'this.mtAvatar[' + repr(key) + '].value'
52
+ safe_access = "(this.mtAvatar[" + repr(key) + "] || {}).value || 0"
53
+ code = code.replace('this.mtAvatar[' + repr(key) + '].value', safe_access)
54
+
55
+ if '(this.mtAvatar[' + repr('bodyRotateY') + '] || {}).value' in code:
56
+ print('OK Fix2: guarded direct .value accesses on known keys')
57
+
58
+ # Fix 3: Wrap animate method in try/catch as safety net
59
+ match = re.search(
60
+ r'prototype\.animate\s*=\s*function\s*\(\s*t\s*\)\s*\{',
61
+ code
62
+ )
63
+ if match:
64
+ brace_open = code.index('{', match.end() - 1)
65
+ depth = 1
66
+ pos = brace_open + 1
67
+ while depth > 0 and pos < len(code):
68
+ ch = code[pos]
69
+ if ch == '{': depth += 1
70
+ elif ch == '}': depth -= 1
71
+ pos += 1
72
+ brace_close = pos - 1
73
+ indent = ' '
74
+ body = code[brace_open + 1:brace_close]
75
+ if '__VT_CATCH__' not in body:
76
+ new_body = (
77
+ indent + '__VT_CATCH__=1;\n'
78
+ + indent + 'try {\n'
79
+ + body + '\n'
80
+ + indent + '} catch (e) {\n'
81
+ + indent + indent + 'if (!(e && e.message && e.message.includes("Cannot read properties of undefined"))) {\n'
82
+ + indent + indent + indent + 'console.warn("[patch_talkinghead] animate:", e?.message || e);\n'
83
+ + indent + indent + indent + 'throw e;\n'
84
+ + indent + indent + '}\n'
85
+ + indent + '}\n'
86
+ )
87
+ code = code[:brace_open + 1] + new_body + code[brace_close + 1:]
88
+ print('OK Fix3: wrapped animate() in try/catch')
89
+ else:
90
+ print('WARN Fix3: could not wrap animate()')
91
+
92
+ with open(target_path, 'w') as f:
93
+ f.write(code)
94
+
95
+ print('Done. Wrote ' + str(len(code)) + ' bytes')
src/vt_patcher.js ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Runtime guard: override TalkingHead animate to catch null morph target crashes
2
+ // This is a safety net in addition to the Dockerfile build-time patch.
3
+
4
+ (function() {
5
+ // Wait for TalkingHead to be defined
6
+ function checkAndPatch() {
7
+ if (typeof TalkingHead !== 'undefined' && TalkingHead.prototype && TalkingHead.prototype.animate) {
8
+ const origAnimate = TalkingHead.prototype.animate;
9
+ if (!TalkingHead.prototype.__VT_PATCHED__) {
10
+ TalkingHead.prototype.animate = function(t) {
11
+ try {
12
+ return origAnimate.call(this, t);
13
+ } catch (e) {
14
+ if (e && e.message && e.message.includes('Cannot read properties of undefined')) {
15
+ console.warn('[Avatar] TalkingHead animate skipped error:', e.message);
16
+ } else {
17
+ throw e;
18
+ }
19
+ }
20
+ };
21
+ TalkingHead.prototype.__VT_PATCHED__ = true;
22
+ }
23
+ }
24
+ }
25
+
26
+ // Check immediately, then after each raf
27
+ checkAndPatch();
28
+ const pollInterval = setInterval(checkAndPatch, 100);
29
+ setTimeout(() => clearInterval(pollInterval), 5000);
30
+ })();