Spaces:
Running on Zero
Running on Zero
Download avatar/facade.js from WolfDavid/japanese-learning-avatar: direct link, hf CLI and curl.
- Browser
- Download file 5.68 kB
-
https://huggingface.co/spaces/WolfDavid/japanese-learning-avatar/resolve/d6ea2800bec570988a6f20fc2dcb6041dbe6d061/avatar/facade.js
- Command line
-
hf download hf://spaces/WolfDavid/japanese-learning-avatar@d6ea2800bec570988a6f20fc2dcb6041dbe6d061/avatar/facade.js
-
curl -L -o facade.js https://huggingface.co/spaces/WolfDavid/japanese-learning-avatar/resolve/d6ea2800bec570988a6f20fc2dcb6041dbe6d061/avatar/facade.js
5.68 kB
| // avatar/facade.js | |
| // | |
| // THIS IS THE ONLY FILE IN avatar/ THAT ASSIGNS window.Avatar. | |
| // tests/test_transport_seam.py::test_only_facade_assigns_window_avatar fails if any | |
| // other module does. | |
| // | |
| // Why one builder rather than one per transport: the naive design has each transport | |
| // build its own window.Avatar, and then every later plan must remember to update both. | |
| // That seam rots silently - a method added to the inline transport and forgotten in the | |
| // fallback is invisible until the day the fallback is needed. Here the method list is | |
| // DATA (AVATAR_SURFACE), the object is built by iterating it, and an unresolved name | |
| // throws at boot with the missing name rather than being quietly absent. | |
| // | |
| // This module knows nothing about any host framework and nothing about the renderer. | |
| // It reaches the renderer only through the six-method stagePort it is handed. | |
| /** | |
| * The transport contract. The single source of truth for the method list. | |
| * Tests parse this array out of this file rather than duplicating it, so it cannot drift. | |
| * | |
| * mount (vrmUrl) -> Promise, resolves once the VRM is ready | |
| * speak ({audioUrl, timeline, expression, subtitle}) -> Promise, resolves at speech end | |
| * replay re-plays the cached directive, no network | |
| * setListening (bool) | |
| * setThinking (bool) | |
| * on (event, cb) where event is one of: | |
| * ready, speech-start, speech-end, error, asr-tier, transcript | |
| * getDebug -> Promise<debug>, async so a message-passing transport can answer | |
| * startListening push-to-talk down - body wired in plan 01-07 | |
| * stopListening push-to-talk up - body wired in plan 01-07 | |
| * dispatchTurn (text, {speed}) - body wired in plan 01-08 | |
| * requestSlower re-synthesise the last utterance slower - plan 01-08 | |
| */ | |
| export const AVATAR_SURFACE = [ | |
| 'mount', | |
| 'speak', | |
| 'replay', | |
| 'setListening', | |
| 'setThinking', | |
| 'on', | |
| 'getDebug', | |
| 'startListening', | |
| 'stopListening', | |
| 'dispatchTurn', | |
| 'requestSlower', | |
| ]; | |
| /** | |
| * Build window.Avatar out of a stagePort and a turn loop. | |
| * | |
| * @param {object} opts | |
| * @param {string} opts.transport 'inline' | 'iframe', recorded in __debug | |
| * @param {object} opts.stagePort mount, speak, replayCached, setThinking, setListening, getDebug | |
| * @param {object} opts.turnLoop the result of createTurnLoop() | |
| * @param {Function} [opts.onEmit] optional hook so a transport can forward events onward | |
| * @returns {{avatar: object, emit: Function, debug: object}} | |
| */ | |
| export function installFacade({ transport, stagePort, turnLoop, onEmit }) { | |
| if (!stagePort) throw new Error('installFacade: a stagePort is required'); | |
| if (!turnLoop) throw new Error('installFacade: a turnLoop is required'); | |
| const listeners = new Map(); | |
| const debug = { | |
| ready: false, | |
| mountCount: 0, | |
| vrmMetaTitle: '', | |
| vrmMeta: null, | |
| threeInstanceCount: 0, | |
| currentVisemes: { aa: 0, ih: 0, ou: 0, ee: 0, oh: 0 }, | |
| blinkValue: 0, | |
| breathValue: 0, | |
| clockOffset: 0, | |
| thinking: false, | |
| listening: false, | |
| lastTurnId: null, | |
| transport: transport || 'inline', | |
| }; | |
| function emit(name, data) { | |
| if (name === 'ready') debug.ready = true; | |
| const subs = listeners.get(name); | |
| if (subs) { | |
| for (const cb of [...subs]) { | |
| try { | |
| cb(data); | |
| } catch (err) { | |
| console.error(`Avatar listener for ${name} failed`, err); | |
| } | |
| } | |
| } | |
| if (typeof onEmit === 'function') { | |
| try { | |
| onEmit(name, data); | |
| } catch (err) { | |
| console.error('Avatar onEmit hook failed', err); | |
| } | |
| } | |
| } | |
| // The members the facade itself owns: the event bus, the mount counter, and the | |
| // merge that turns three independent debug sources into one readable object. | |
| const local = { | |
| on(event, cb) { | |
| if (typeof cb !== 'function') return () => {}; | |
| if (!listeners.has(event)) listeners.set(event, new Set()); | |
| listeners.get(event).add(cb); | |
| return () => listeners.get(event)?.delete(cb); | |
| }, | |
| async mount(a, b) { | |
| // The canvas belongs to the transport, so a URL is the only thing that has to | |
| // cross this boundary. Accept mount(url) and mount(canvas, url) alike. | |
| const vrmUrl = b === undefined ? a : b; | |
| debug.mountCount += 1; | |
| await stagePort.mount(vrmUrl); | |
| debug.ready = true; | |
| return local.getDebug(); | |
| }, | |
| async getDebug() { | |
| let stageDebug = null; | |
| try { | |
| stageDebug = await stagePort.getDebug(); | |
| } catch { | |
| stageDebug = null; // not mounted yet, or the far side has not answered | |
| } | |
| Object.assign(debug, turnLoop.state || {}, stageDebug || {}); | |
| return debug; | |
| }, | |
| }; | |
| // Build by ITERATING the surface list. This loop, and the throw under it, are what | |
| // make "both transports expose the same methods" a guarantee instead of a habit. | |
| const avatar = {}; | |
| const unresolved = []; | |
| for (const name of AVATAR_SURFACE) { | |
| let owner = null; | |
| if (typeof local[name] === 'function') owner = local; | |
| else if (typeof turnLoop[name] === 'function') owner = turnLoop; | |
| else if (typeof stagePort[name] === 'function') owner = stagePort; | |
| if (!owner) { | |
| unresolved.push(name); | |
| continue; | |
| } | |
| avatar[name] = (...args) => owner[name](...args); | |
| } | |
| if (unresolved.length > 0) { | |
| throw new Error( | |
| `installFacade: nothing implements ${unresolved.join(', ')} - every name in ` + | |
| 'AVATAR_SURFACE must resolve to a turn-loop method or a stagePort method' | |
| ); | |
| } | |
| avatar.__debug = debug; | |
| window.Avatar = avatar; | |
| return { avatar, emit, debug }; | |
| } | |