{"_id":"@busyexplore/zotonic-lip-sync","_rev":"8-26cc15987765ac55e532ec6413b59040","name":"@busyexplore/zotonic-lip-sync","dist-tags":{"beta":"0.0.1-beta.2","latest":"0.0.1-beta.6"},"versions":{"0.0.1-beta.0":{"name":"@busyexplore/zotonic-lip-sync","version":"0.0.1-beta.0","_id":"@busyexplore/zotonic-lip-sync@0.0.1-beta.0","maintainers":[{"name":"busyexplore","email":"suprise.mlimi97@gmail.com"}],"dist":{"shasum":"55df8551335b0e3e39b406bd521c0ccc5801a60e","tarball":"https://registry.npmjs.org/@busyexplore/zotonic-lip-sync/-/zotonic-lip-sync-0.0.1-beta.0.tgz","fileCount":5,"integrity":"sha512-lGGmykIKcYfTwUj32R4cgwtge7vgBtR6W0uMrLWRmxTGEDwnE7cT0TF5dnQAH/VMxmRgKdItzlexqF1kOElMnQ==","signatures":[{"sig":"MEYCIQDVoS+JTdrMhHLLjnxFEPMgJjSifDHrMtpb3i0Yll3JkgIhAItswXuCvfCjBd6SNHfSZEXMUD8WTuokB5QSFSbjZRgf","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":65355},"main":"dist/zotonic-lip-sync.es.js","type":"module","module":"dist/zotonic-lip-sync.umd.js","gitHead":"b59973784165b903176c7b042507c49d663ca296","private":false,"scripts":{"dev":"set NODE_OPTIONS=--openssl-legacy-provider && vite","lint":"eslint .","build":"vite build","deploy":"npm version prerelease --preid=beta --no-git-tag-version && npm publish --tag beta","preview":"vite preview"},"_npmUser":{"name":"busyexplore","email":"suprise.mlimi97@gmail.com"},"_npmVersion":"10.1.0","description":"A React hook that turns an audio file (or URL) into lip-sync mouth-shape data — analyzed client-side, mapped to Rhubarb-style visemes, and converted to Preston Blair phoneme names. Processing state and results are also broadcast as events, which you can s","directories":{},"_nodeVersion":"20.9.0","dependencies":{"react":"^16.8 || ^17 || ^18","react-dom":"^16.8 || ^17 || ^18","@busyexplore/zotonic-command-emitter":"0.0.1-beta.8"},"_hasShrinkwrap":false,"devDependencies":{"vite":"^6.3.5","eslint":"^9.29.0","globals":"^16.2.0","@eslint/js":"^9.29.0","@types/react":"^19.1.8","@types/react-dom":"^19.1.6","@vitejs/plugin-react":"^4.6.0","eslint-plugin-react-hooks":"^5.2.0","eslint-plugin-react-refresh":"^0.4.20"},"peerDependencies":{"react":"^16.8 || ^17 || ^18","react-dom":"^16.8 || ^17 || ^18"},"_npmOperationalInternal":{"tmp":"tmp/zotonic-lip-sync_0.0.1-beta.0_1786883347697_0.44872300227324713","host":"s3://npm-registry-packages-npm-production"}},"0.0.1-beta.1":{"name":"@busyexplore/zotonic-lip-sync","version":"0.0.1-beta.1","_id":"@busyexplore/zotonic-lip-sync@0.0.1-beta.1","maintainers":[{"name":"busyexplore","email":"suprise.mlimi97@gmail.com"}],"dist":{"shasum":"a44b49934b44bb1433bc6c36d41925f66f63923a","tarball":"https://registry.npmjs.org/@busyexplore/zotonic-lip-sync/-/zotonic-lip-sync-0.0.1-beta.1.tgz","fileCount":4,"integrity":"sha512-ugfRYLGPkbmWQjhS56F3+8m5faum9FvIZ0W0n5BLpTDOr/Pg1ytmSlb5VPGaSIDprzq/WbCypQwPD6pQqLWfUw==","signatures":[{"sig":"MEQCIBrwpWWHOXzwx4+BgRyUXUAyu8nxX0sDWmKmKBlnWjk0AiAN9wL7b6Er3d4Lo+N36X+N0lV3xokfgSi5Oj50OZIGhA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":53168280},"main":"dist/zotonic-lip-sync.es.js","type":"module","gitHead":"84f6b132ff1a245b7b3b170117978a8d94a24936","private":false,"scripts":{"dev":"set NODE_OPTIONS=--openssl-legacy-provider && vite","lint":"eslint .","build":"vite build","deploy":"npm version prerelease --preid=beta --no-git-tag-version && npm publish --tag beta","preview":"vite preview"},"_npmUser":{"name":"busyexplore","email":"suprise.mlimi97@gmail.com"},"_npmVersion":"10.1.0","description":"A React hook that turns an audio file (or URL) into lip-sync mouth-shape data — analyzed client-side, mapped to Rhubarb-style visemes, and converted to Preston Blair phoneme names. Processing state and results are also broadcast as events, which you can s","directories":{},"_nodeVersion":"20.9.0","dependencies":{"react":"^16.8 || ^17 || ^18","react-dom":"^16.8 || ^17 || ^18","lip-sync-engine":"^1.0.3","@busyexplore/zotonic-command-emitter":"0.0.1-beta.8"},"_hasShrinkwrap":false,"readmeFilename":"README.md","devDependencies":{"vite":"^6.3.5","eslint":"^9.29.0","globals":"^16.2.0","@eslint/js":"^9.29.0","@types/react":"^19.1.8","@types/react-dom":"^19.1.6","@vitejs/plugin-react":"^4.6.0","eslint-plugin-react-hooks":"^5.2.0","eslint-plugin-react-refresh":"^0.4.20"},"peerDependencies":{"react":"^16.8 || ^17 || ^18","react-dom":"^16.8 || ^17 || ^18"},"_npmOperationalInternal":{"tmp":"tmp/zotonic-lip-sync_0.0.1-beta.1_1786904611727_0.6678510323676798","host":"s3://npm-registry-packages-npm-production"}},"0.0.1-beta.2":{"name":"@busyexplore/zotonic-lip-sync","version":"0.0.1-beta.2","_id":"@busyexplore/zotonic-lip-sync@0.0.1-beta.2","maintainers":[{"name":"busyexplore","email":"suprise.mlimi97@gmail.com"}],"dist":{"shasum":"fc08b01cc7e8ab30c4d7f0de5e29d4e5d1f78520","tarball":"https://registry.npmjs.org/@busyexplore/zotonic-lip-sync/-/zotonic-lip-sync-0.0.1-beta.2.tgz","fileCount":4,"integrity":"sha512-qwvBoKEQv8xJFEbMAelqz745x+zUlxWgrkmlv7J+YEqEFd9A3u2vzoT+s6eIcULFSp8Zvtv13WMLDgZclSyEpA==","signatures":[{"sig":"MEQCIHNOnpgFU/qbBKymgjjDb2p4rJhB+Bfs3Gj2EGI+SSclAiBlec+QTiCWFBjLtQQ+9hxqlYD4agcPjLwhEJEqW5cOqQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":54303},"main":"dist/zotonic-lip-sync.es.js","type":"module","gitHead":"08510497ce4372f399ae6ba4143702ff01e72376","private":false,"scripts":{"dev":"set NODE_OPTIONS=--openssl-legacy-provider && vite","lint":"eslint .","build":"vite build","deploy":"npm version prerelease --preid=beta --no-git-tag-version && npm publish --tag beta","preview":"vite preview"},"_npmUser":{"name":"busyexplore","email":"suprise.mlimi97@gmail.com"},"_npmVersion":"10.1.0","description":"A React hook that turns an audio file into lip-sync viseme data — analyzed client-side with pitch detection snapped to a C-major scale, then mapped to Preston Blair viseme names. Processing state and results are also broadcast as events, which you can sub","directories":{},"_nodeVersion":"20.9.0","dependencies":{"react":"^16.8 || ^17 || ^18","react-dom":"^16.8 || ^17 || ^18","lip-sync-engine":"^1.0.3","@busyexplore/zotonic-command-emitter":"0.0.1-beta.8"},"_hasShrinkwrap":false,"readmeFilename":"README.md","devDependencies":{"vite":"^6.3.5","eslint":"^9.29.0","globals":"^16.2.0","@eslint/js":"^9.29.0","@types/react":"^19.1.8","@types/react-dom":"^19.1.6","@vitejs/plugin-react":"^4.6.0","eslint-plugin-react-hooks":"^5.2.0","eslint-plugin-react-refresh":"^0.4.20"},"peerDependencies":{"react":"^16.8 || ^17 || ^18","react-dom":"^16.8 || ^17 || ^18"},"_npmOperationalInternal":{"tmp":"tmp/zotonic-lip-sync_0.0.1-beta.2_1786922647265_0.941125216804048","host":"s3://npm-registry-packages-npm-production"}},"0.0.1-beta.4":{"name":"@busyexplore/zotonic-lip-sync","version":"0.0.1-beta.4","_id":"@busyexplore/zotonic-lip-sync@0.0.1-beta.4","maintainers":[{"name":"busyexplore","email":"suprise.mlimi97@gmail.com"}],"dist":{"shasum":"f92a62502b0a7e52732c86de5b7eb9d9ba90920b","tarball":"https://registry.npmjs.org/@busyexplore/zotonic-lip-sync/-/zotonic-lip-sync-0.0.1-beta.4.tgz","fileCount":4,"integrity":"sha512-dOaKuECEHVD6BgAolLSYWfwwmMsK0HUfkxm+Te1iwEkpKHzMAx/PDa//NQdMf7uirf81TkvE6Z6wNjBWfbk8JA==","signatures":[{"sig":"MEUCIDD49mgY0VKhAa/79cRD8COquPQmARSE4hj0SJ7V9dPqAiEA8pF9DNjwSb7moU/iF5LGbNrRQLsSh60OgwM0FOgeLMM=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":57078},"main":"dist/zotonic-lip-sync.es.js","type":"module","gitHead":"89e47cdbca45dcd812205bd2a27143e09a960e85","private":false,"scripts":{"dev":"set NODE_OPTIONS=--openssl-legacy-provider && vite","lint":"eslint .","build":"vite build","deploy":"npm version prerelease --preid=beta --no-git-tag-version && npm publish --tag beta","preview":"vite preview"},"_npmUser":{"name":"busyexplore","email":"suprise.mlimi97@gmail.com"},"_npmVersion":"10.1.0","description":"A React hook that turns an audio file into lip-sync viseme data — analyzed client-side with pitch detection snapped to a C-major scale, then mapped to Preston Blair viseme names. Processing state and results are also broadcast as events, which you can sub","directories":{},"_nodeVersion":"20.9.0","dependencies":{"react":"^16.8 || ^17 || ^18","react-dom":"^16.8 || ^17 || ^18","lip-sync-engine":"^1.0.3","@busyexplore/zotonic-command-emitter":"0.0.1-beta.8"},"_hasShrinkwrap":false,"devDependencies":{"vite":"^6.3.5","eslint":"^9.29.0","globals":"^16.2.0","@eslint/js":"^9.29.0","@types/react":"^19.1.8","@types/react-dom":"^19.1.6","@vitejs/plugin-react":"^4.6.0","eslint-plugin-react-hooks":"^5.2.0","eslint-plugin-react-refresh":"^0.4.20"},"peerDependencies":{"react":"^16.8 || ^17 || ^18","react-dom":"^16.8 || ^17 || ^18"},"_npmOperationalInternal":{"tmp":"tmp/zotonic-lip-sync_0.0.1-beta.4_1786925023999_0.2291694576409904","host":"s3://npm-registry-packages-npm-production"}},"0.0.1-beta.5":{"name":"@busyexplore/zotonic-lip-sync","version":"0.0.1-beta.5","_id":"@busyexplore/zotonic-lip-sync@0.0.1-beta.5","maintainers":[{"name":"busyexplore","email":"suprise.mlimi97@gmail.com"}],"dist":{"shasum":"7ad172a1c103e0e02d8142dd1682549dc55c2e34","tarball":"https://registry.npmjs.org/@busyexplore/zotonic-lip-sync/-/zotonic-lip-sync-0.0.1-beta.5.tgz","fileCount":4,"integrity":"sha512-vjIFhGOpTTX2iX/mrT9HLj7+7U/UeIsq2/DniFdyE1OVK6TWY4cf/hJq4St8vCKDriQExvEO7QqtwJkAe/Errg==","signatures":[{"sig":"MEQCIG7uZg2lRAXxbPjo9UHvNUpRSqFmZXb5n5IPuhFTT5BAAiBemYXwLa7ao8uSUxmBs4XCaLW23q63/k7dk+78e9fIpg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":58211},"main":"dist/zotonic-lip-sync.es.js","type":"module","gitHead":"508eac13f9b0f11610a92e443053566739a1471a","private":false,"scripts":{"dev":"set NODE_OPTIONS=--openssl-legacy-provider && vite","lint":"eslint .","build":"vite build","deploy":"npm version prerelease --preid=beta --no-git-tag-version && npm publish --tag beta","preview":"vite preview"},"_npmUser":{"name":"busyexplore","email":"suprise.mlimi97@gmail.com"},"_npmVersion":"10.1.0","description":"A React hook that turns an audio file into lip-sync viseme data — analyzed client-side with pitch detection snapped to a C-major scale, then mapped to Preston Blair viseme names. Processing state and results are also broadcast as events, which you can sub","directories":{},"_nodeVersion":"20.9.0","dependencies":{"react":"^16.8 || ^17 || ^18","react-dom":"^16.8 || ^17 || ^18","@busyexplore/zotonic-command-emitter":"0.0.1-beta.8"},"_hasShrinkwrap":false,"devDependencies":{"vite":"^6.3.5","eslint":"^9.29.0","globals":"^16.2.0","@eslint/js":"^9.29.0","@types/react":"^19.1.8","@types/react-dom":"^19.1.6","@vitejs/plugin-react":"^4.6.0","eslint-plugin-react-hooks":"^5.2.0","eslint-plugin-react-refresh":"^0.4.20"},"peerDependencies":{"react":"^16.8 || ^17 || ^18","react-dom":"^16.8 || ^17 || ^18"},"_npmOperationalInternal":{"tmp":"tmp/zotonic-lip-sync_0.0.1-beta.5_1786966544026_0.4067634370886186","host":"s3://npm-registry-packages-npm-production"}},"0.0.1-beta.6":{"name":"@busyexplore/zotonic-lip-sync","private":false,"version":"0.0.1-beta.6","type":"module","main":"dist/zotonic-lip-sync.es.js","scripts":{"dev":"set NODE_OPTIONS=--openssl-legacy-provider && vite","build":"vite build","lint":"eslint .","preview":"vite preview","deploy":"npm version prerelease --preid=beta --no-git-tag-version && npm publish --tag beta"},"dependencies":{"@busyexplore/zotonic-audio-util":"^0.0.1-beta.2","@busyexplore/zotonic-command-emitter":"0.0.1-beta.8","react":"^16.8 || ^17 || ^18","react-dom":"^16.8 || ^17 || ^18"},"peerDependencies":{"react":"^16.8 || ^17 || ^18","react-dom":"^16.8 || ^17 || ^18"},"devDependencies":{"@eslint/js":"^9.29.0","@types/react":"^19.1.8","@types/react-dom":"^19.1.6","@vitejs/plugin-react":"^4.6.0","eslint":"^9.29.0","eslint-plugin-react-hooks":"^5.2.0","eslint-plugin-react-refresh":"^0.4.20","globals":"^16.2.0","vite":"^6.3.5"},"_id":"@busyexplore/zotonic-lip-sync@0.0.1-beta.6","gitHead":"1f45efded19be9ace3cef903be12b1234be8371f","description":"A React hook that turns an audio file into lip-sync viseme data — analyzed client-side with pitch detection snapped to a C-major scale, then mapped to Preston Blair viseme names. Processing state and results are also broadcast as events, which you can sub","_nodeVersion":"20.9.0","_npmVersion":"10.1.0","dist":{"integrity":"sha512-XOTP9rmEwTB2OuApabnepL7E2ThIhSZlezOhgimbqVKe0zrOOBT0v8FcqppHNabstqnLafSF5MRCIp/y2eP3xA==","shasum":"a262d83c077c89d4c135145e12ef04c90760ae37","tarball":"https://registry.npmjs.org/@busyexplore/zotonic-lip-sync/-/zotonic-lip-sync-0.0.1-beta.6.tgz","fileCount":4,"unpackedSize":59564,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIGjkQ8hQqdTeD49GPh8naY8sBVMjeaBSOoi+6McSH1LzAiAv9Ug5D8BQ6P+MhECQRXNxlCwch8iR8cRxPAwVaEoszA=="}]},"_npmUser":{"name":"busyexplore","email":"suprise.mlimi97@gmail.com"},"directories":{},"maintainers":[{"name":"busyexplore","email":"suprise.mlimi97@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/zotonic-lip-sync_0.0.1-beta.6_1786991277576_0.7321588909509522"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-16T12:29:07.508Z","modified":"2026-08-17T18:27:57.878Z","0.0.1-beta.0":"2026-08-16T12:29:07.844Z","0.0.1-beta.1":"2026-08-16T18:23:32.296Z","0.0.1-beta.2":"2026-08-16T23:24:07.411Z","0.0.1-beta.4":"2026-08-17T00:03:44.160Z","0.0.1-beta.5":"2026-08-17T11:35:44.172Z","0.0.1-beta.6":"2026-08-17T18:27:57.721Z"},"description":"A React hook that turns an audio file into lip-sync viseme data — analyzed client-side with pitch detection snapped to a C-major scale, then mapped to Preston Blair viseme names. Processing state and results are also broadcast as events, which you can sub","maintainers":[{"name":"busyexplore","email":"suprise.mlimi97@gmail.com"}],"readme":"# @busyexplore/zotonic-lip-sync\n\nA React hook that turns an audio file into lip-sync viseme data — analyzed client-side with pitch detection snapped to a C-major scale, then mapped to Preston Blair viseme names. Processing state and results are also broadcast as events, which you can subscribe to from anywhere in your app via the re-exported `useWatchCommandEmitter`.\n\n## Features\n\n- 🎙️ **Client-side audio analysis** — no server round trip\n- 👄 **Preston Blair viseme output** — frame-indexed cues from `LipSyncAnalyzer`\n- 📡 **Broadcasts processing events** (start, update, error, ready) so distant components can react without prop drilling\n- ⚛️ **Single hook API** — `useZotonicLipSync(fps)`\n\n## Installation\n\n```bash\nnpm install @busyexplore/zotonic-lip-sync\n```\n\n```bash\nyarn add @busyexplore/zotonic-lip-sync\n```\n\n```bash\npnpm add @busyexplore/zotonic-lip-sync\n```\n\n## Quick start\n\n```jsx\nimport { useZotonicLipSync } from \"@busyexplore/zotonic-lip-sync\";\n\nfunction LipSyncPlayer() {\n  const { loadFile } = useZotonicLipSync(30);\n\n  async function handleFileChange(e) {\n    const file = e.target.files[0];\n    if (!file) return;\n\n    const result = await loadFile(file);\n    console.log(result.visemes); // frame-indexed Preston Blair cues\n  }\n\n  return <input type=\"file\" accept=\"audio/*\" onChange={handleFileChange} />;\n}\n```\n\n## API\n\n### `useZotonicLipSync(fps = 24)`\n\nReact hook. `fps` sets the frame rate used by the analyzer (can be changed later via `setFPS`). Fires `ACTION_READY` shortly after mount. Returns:\n\n| Key | Type | Description |\n|---|---|---|\n| `loadFile` | `(file: File \\| Blob \\| string \\| Request \\| Buffer \\| ArrayBuffer \\| ArrayBufferView) => Promise<LipSyncResult \\| {}>` | Analyzes audio with `LipSyncAnalyzer`. Accepts a `File`/`Blob`, a URL string or `Request` (fetched internally), a Node `Buffer`, a raw `ArrayBuffer`, or any typed array. |\n| `setFPS` | `(fps: number) => void` | Sets the frame rate used for the *next* `loadFile` call. |\n| `MOUTH_TYPE` | `object` | `LipSyncAnalyzer.VISEME_TO_MOUTH` — a lookup table mapping each Preston Blair viseme name to itself (`{ MBP: \"MBP\", E: \"E\", AI: \"AI\", O: \"O\", U: \"U\", FV: \"FV\", L: \"L\", WQ: \"WQ\", rest: \"rest\" }`). |\n| `useWatchCommandEmitter` | `hook` | Re-exported so other components can subscribe to lip-sync events. See [Listening for events](#listening-for-events). |\n| `LIP_SYNC_TOOL` | `object` | Command/action name constants (see below). |\n\n> **Not available in this version:** `loadAudioUrl`/`loadAudioFromFile`, `getShapeAtTime`, `setLowAccuracyMode`. `loadFile` only accepts a `File`/`Blob`, and there's no built-in helper for looking up the shape at a given playback time — you'll need to search `result.visemes` yourself.\n\n#### `LipSyncResult`\n\nThis is `LipSyncAnalyzer.processAudio()`'s return value, passed through unchanged:\n\n```ts\n{\n  fps: number;         // frame rate used for analysis\n  duration: number;    // audio duration, in seconds\n  frames: number;      // audio duration, in frames (duration * fps, rounded up)\n  visemes: VisemeCue[];// [{ start, end, note, viseme, mouth }] — start/end in frame numbers\n}\n```\n\n`viseme`/`mouth` are Preston Blair names: `\"MBP\" | \"E\" | \"AI\" | \"O\" | \"U\" | \"FV\" | \"L\" | \"WQ\" | \"rest\"`.\n\n> On error, `loadFile` resolves to `{}` (empty object) rather than a `LipSyncResult` — check for a missing `visemes` key, or listen for `ACTION_ERROR`, to detect failures.\n\n### `LIP_SYNC_TOOL`\n\nCommand/action constants, exported both from the hook's return value and as a named export:\n\n```js\nexport const LIP_SYNC_TOOL = {\n  ACTION_START_PROCESSING: \"START_PROCESSING\",\n  ACTION_STOP_PROCESSING: \"STOP_PROCESSING\",\n  ACTION_UPDATE: \"update\",\n  ACTION_READY: \"ready\",\n  ACTION_ERROR: \"ERROR\",\n  LIP_SYNC_COMMAND: \"LIP_SYNC\",\n};\n```\n\n| Action | Emitted when |\n|---|---|\n| `ACTION_START_PROCESSING` | `loadFile` begins processing |\n| `ACTION_UPDATE` | A `LipSyncResult` is ready — event data is the full result object |\n| `ACTION_ERROR` | Analysis failed — event data has a `message` |\n| `ACTION_READY` | The hook has mounted and is ready to load audio |\n\n> **Note:** `ACTION_STOP_PROCESSING` is defined in `LIP_SYNC_TOOL` but is not currently emitted anywhere in `loadFile` — don't rely on it to detect completion; use `ACTION_UPDATE` or the resolved promise instead.\n\nAll events are emitted under `LIP_SYNC_COMMAND`, so you can listen for everything lip-sync related in one place.\n\n## Listening for events\n\nBecause `useWatchCommandEmitter` is re-exported from this package, any component can subscribe to lip-sync events without receiving props from the component that calls `loadFile`:\n\n```jsx\nimport { useZotonicLipSync } from \"@busyexplore/zotonic-lip-sync\";\n\nfunction LipSyncStatus() {\n  const { LIP_SYNC_TOOL, useWatchCommandEmitter } = useZotonicLipSync();\n\n  useWatchCommandEmitter((command, action, data) => {\n    if (command !== LIP_SYNC_TOOL.LIP_SYNC_COMMAND) return;\n\n    switch (action) {\n      case LIP_SYNC_TOOL.ACTION_START_PROCESSING:\n        console.log(\"Analyzing audio…\");\n        break;\n      case LIP_SYNC_TOOL.ACTION_UPDATE:\n        console.log(\"Got visemes:\", data.visemes);\n        break;\n      case LIP_SYNC_TOOL.ACTION_ERROR:\n        console.error(data.message);\n        break;\n    }\n  });\n\n  return null;\n}\n```\n\nThis is useful for surfacing a global loading spinner or toast from a component that isn't the one calling `loadFile`.\n\n> If you pass an inline arrow function as shown above, it's a new reference every render, so the listener unsubscribes/resubscribes each render — harmless, but wrap it in `useCallback` if you want to avoid the churn.\n\n## Behavior notes\n\n- **`setFPS` only affects future `loadFile` calls** — it doesn't retroactively convert an already-loaded result.\n- **`loadFile` does not throw on failure** — a failed load resolves to `{}` rather than rejecting; listen for `ACTION_ERROR` to get the failure message.\n- **Events are global, not scoped to the calling component** — every subscriber in your app receives every emitted command, so filter by `command` (and `action`) in your listener.\n- **Pitch detection is C-major-scale-snapped** — `LipSyncAnalyzer` detects pitch via autocorrelation and snaps it to the nearest note in a C-major scale before mapping to a viseme. This is a heuristic, not phonetic recognition — it will not always match true mouth shapes for actual speech content, particularly on noisy or polyphonic audio.\n\n<!-- ## License\n\nMIT -->\n\n**Happy syncing!** 👄🎵","readmeFilename":"README.md"}