Spaces:
Paused
Paused
Fix: TalkingHead2.animate crash - null morph target access in animate()
#5
by bep40 - opened
- Dockerfile +6 -1
- index.html +4 -2
- patch_talkinghead.py +95 -0
- 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
|
| 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 |
+
})();
|