@elizaos/plugin-local-inference 2.0.0-beta.1 → 2.0.3-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (701) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +83 -0
  3. package/package.json +82 -15
  4. package/src/actions/generate-media.d.ts +59 -0
  5. package/src/actions/generate-media.d.ts.map +1 -0
  6. package/src/actions/generate-media.ts +647 -0
  7. package/src/actions/identify-speaker.d.ts +23 -0
  8. package/src/actions/identify-speaker.d.ts.map +1 -0
  9. package/src/actions/identify-speaker.ts +171 -0
  10. package/src/actions/transcription-control.d.ts +29 -0
  11. package/src/actions/transcription-control.d.ts.map +1 -0
  12. package/src/actions/transcription-control.test.ts +100 -0
  13. package/src/actions/transcription-control.ts +127 -0
  14. package/src/adapters/capacitor-llama/__tests__/compat-behavior.test.ts +218 -0
  15. package/src/adapters/capacitor-llama/__tests__/index.test.ts +68 -0
  16. package/src/adapters/capacitor-llama/__tests__/structured-output.test.ts +215 -0
  17. package/src/adapters/capacitor-llama/__tests__/text-streaming.test.ts +174 -0
  18. package/src/adapters/capacitor-llama/environment.ts +71 -0
  19. package/src/adapters/capacitor-llama/index.browser.ts +83 -0
  20. package/src/adapters/capacitor-llama/index.ts +807 -0
  21. package/src/adapters/capacitor-llama/loader.ts +109 -0
  22. package/src/adapters/capacitor-llama/structured-output.ts +165 -0
  23. package/src/adapters/capacitor-llama/text-streaming.ts +227 -0
  24. package/src/adapters/capacitor-llama/types.ts +374 -0
  25. package/src/backends/apple-foundation.ts +127 -0
  26. package/src/index.d.ts +8 -0
  27. package/src/index.d.ts.map +1 -0
  28. package/src/index.ts +62 -0
  29. package/src/local-inference-routes.d.ts +38 -0
  30. package/src/local-inference-routes.d.ts.map +1 -0
  31. package/src/local-inference-routes.test.ts +344 -0
  32. package/src/local-inference-routes.ts +1543 -0
  33. package/src/provider.d.ts +21 -0
  34. package/src/provider.d.ts.map +1 -0
  35. package/src/provider.ts +1082 -0
  36. package/src/routes/compat-helpers.d.ts +18 -0
  37. package/src/routes/compat-helpers.d.ts.map +1 -0
  38. package/src/routes/compat-helpers.ts +274 -0
  39. package/src/routes/family-member-route.d.ts +62 -0
  40. package/src/routes/family-member-route.d.ts.map +1 -0
  41. package/src/routes/family-member-route.ts +353 -0
  42. package/src/routes/index.d.ts +19 -0
  43. package/src/routes/index.d.ts.map +1 -0
  44. package/src/routes/index.ts +60 -0
  45. package/src/routes/live-diarization-route.d.ts +26 -0
  46. package/src/routes/live-diarization-route.d.ts.map +1 -0
  47. package/src/routes/live-diarization-route.test.ts +213 -0
  48. package/src/routes/live-diarization-route.ts +122 -0
  49. package/src/routes/local-inference-asr-route.d.ts +4 -0
  50. package/src/routes/local-inference-asr-route.d.ts.map +1 -0
  51. package/src/routes/local-inference-asr-route.test.ts +205 -0
  52. package/src/routes/local-inference-asr-route.ts +163 -0
  53. package/src/routes/local-inference-asr-transcribe.d.ts +20 -0
  54. package/src/routes/local-inference-asr-transcribe.d.ts.map +1 -0
  55. package/src/routes/local-inference-asr-transcribe.test.ts +118 -0
  56. package/src/routes/local-inference-asr-transcribe.ts +97 -0
  57. package/src/routes/local-inference-compat-routes.d.ts +16 -0
  58. package/src/routes/local-inference-compat-routes.d.ts.map +1 -0
  59. package/src/routes/local-inference-compat-routes.test.ts +485 -0
  60. package/src/routes/local-inference-compat-routes.ts +808 -0
  61. package/src/routes/local-inference-tts-route.d.ts +7 -0
  62. package/src/routes/local-inference-tts-route.d.ts.map +1 -0
  63. package/src/routes/local-inference-tts-route.test.ts +179 -0
  64. package/src/routes/local-inference-tts-route.ts +230 -0
  65. package/src/routes/transcript-audio-store.d.ts +15 -0
  66. package/src/routes/transcript-audio-store.d.ts.map +1 -0
  67. package/src/routes/transcript-audio-store.ts +27 -0
  68. package/src/routes/transcripts-routes.d.ts +36 -0
  69. package/src/routes/transcripts-routes.d.ts.map +1 -0
  70. package/src/routes/transcripts-routes.test.ts +144 -0
  71. package/src/routes/transcripts-routes.ts +159 -0
  72. package/src/routes/voice-first-run-routes.d.ts +62 -0
  73. package/src/routes/voice-first-run-routes.d.ts.map +1 -0
  74. package/src/routes/voice-first-run-routes.ts +524 -0
  75. package/src/routes/voice-models-routes.d.ts +62 -0
  76. package/src/routes/voice-models-routes.d.ts.map +1 -0
  77. package/src/routes/voice-models-routes.ts +554 -0
  78. package/src/routes/voice-profile-plugin-routes.d.ts +19 -0
  79. package/src/routes/voice-profile-plugin-routes.d.ts.map +1 -0
  80. package/src/routes/voice-profile-plugin-routes.ts +138 -0
  81. package/src/routes/voice-profiles-management-routes.d.ts +52 -0
  82. package/src/routes/voice-profiles-management-routes.d.ts.map +1 -0
  83. package/src/routes/voice-profiles-management-routes.ts +476 -0
  84. package/src/routes/voice-speaker-profile-routes.d.ts +57 -0
  85. package/src/routes/voice-speaker-profile-routes.d.ts.map +1 -0
  86. package/src/routes/voice-speaker-profile-routes.ts +199 -0
  87. package/src/runtime/aosp-llama-loader-selection.test.ts +80 -0
  88. package/src/runtime/capacitor-llama.d.ts +25 -0
  89. package/src/runtime/embedding-manager-support.d.ts +77 -0
  90. package/src/runtime/embedding-manager-support.d.ts.map +1 -0
  91. package/src/runtime/embedding-manager-support.ts +497 -0
  92. package/src/runtime/embedding-presets.d.ts +16 -0
  93. package/src/runtime/embedding-presets.d.ts.map +1 -0
  94. package/src/runtime/embedding-presets.ts +81 -0
  95. package/src/runtime/embedding-warmup-policy.d.ts +14 -0
  96. package/src/runtime/embedding-warmup-policy.d.ts.map +1 -0
  97. package/src/runtime/embedding-warmup-policy.test.ts +53 -0
  98. package/src/runtime/embedding-warmup-policy.ts +48 -0
  99. package/src/runtime/ensure-local-inference-handler.d.ts +62 -0
  100. package/src/runtime/ensure-local-inference-handler.d.ts.map +1 -0
  101. package/src/runtime/ensure-local-inference-handler.test.ts +528 -0
  102. package/src/runtime/ensure-local-inference-handler.ts +1448 -0
  103. package/src/runtime/index.d.ts +15 -0
  104. package/src/runtime/index.d.ts.map +1 -0
  105. package/src/runtime/index.ts +33 -0
  106. package/src/runtime/mobile-local-inference-gate.d.ts +31 -0
  107. package/src/runtime/mobile-local-inference-gate.d.ts.map +1 -0
  108. package/src/runtime/mobile-local-inference-gate.test.ts +69 -0
  109. package/src/runtime/mobile-local-inference-gate.ts +44 -0
  110. package/src/runtime/voice-entity-binding.d.ts +103 -0
  111. package/src/runtime/voice-entity-binding.d.ts.map +1 -0
  112. package/src/runtime/voice-entity-binding.transcript.test.ts +69 -0
  113. package/src/runtime/voice-entity-binding.ts +328 -0
  114. package/src/services/README.md +71 -0
  115. package/src/services/__tests__/backend-selector.test.ts +101 -0
  116. package/src/services/__tests__/checkpoint-manager.test.ts +376 -0
  117. package/src/services/__tests__/gpu-autotune.test.ts +400 -0
  118. package/src/services/__tests__/llm-streaming-binding.test.ts +85 -0
  119. package/src/services/__tests__/planner-grammar.test.ts +372 -0
  120. package/src/services/__tests__/runtime-target.test.ts +176 -0
  121. package/src/services/active-model-switch-rollback.test.ts +183 -0
  122. package/src/services/active-model.d.ts +282 -0
  123. package/src/services/active-model.d.ts.map +1 -0
  124. package/src/services/active-model.ts +1213 -0
  125. package/src/services/assignments.d.ts +71 -0
  126. package/src/services/assignments.d.ts.map +1 -0
  127. package/src/services/assignments.test.ts +80 -0
  128. package/src/services/assignments.ts +230 -0
  129. package/src/services/backend-selector.ts +95 -0
  130. package/src/services/backend.d.ts +346 -0
  131. package/src/services/backend.d.ts.map +1 -0
  132. package/src/services/backend.ts +612 -0
  133. package/src/services/bionic-host-loader.d.ts +46 -0
  134. package/src/services/bionic-host-loader.d.ts.map +1 -0
  135. package/src/services/bionic-host-loader.test.ts +133 -0
  136. package/src/services/bionic-host-loader.ts +180 -0
  137. package/src/services/bundled-models.d.ts +34 -0
  138. package/src/services/bundled-models.d.ts.map +1 -0
  139. package/src/services/bundled-models.ts +129 -0
  140. package/src/services/cache-bridge.d.ts +206 -0
  141. package/src/services/cache-bridge.d.ts.map +1 -0
  142. package/src/services/cache-bridge.test.ts +516 -0
  143. package/src/services/cache-bridge.ts +423 -0
  144. package/src/services/catalog.d.ts +10 -0
  145. package/src/services/catalog.d.ts.map +1 -0
  146. package/src/services/catalog.test.ts +238 -0
  147. package/src/services/catalog.ts +27 -0
  148. package/src/services/checkpoint-client.d.ts +109 -0
  149. package/src/services/checkpoint-client.d.ts.map +1 -0
  150. package/src/services/checkpoint-client.ts +258 -0
  151. package/src/services/checkpoint-manager.ts +474 -0
  152. package/src/services/cloud-fallback.d.ts +102 -0
  153. package/src/services/cloud-fallback.d.ts.map +1 -0
  154. package/src/services/cloud-fallback.ts +230 -0
  155. package/src/services/conversation-registry.d.ts +142 -0
  156. package/src/services/conversation-registry.d.ts.map +1 -0
  157. package/src/services/conversation-registry.test.ts +235 -0
  158. package/src/services/conversation-registry.ts +264 -0
  159. package/src/services/desktop-fused-ffi-backend-runtime.d.ts +95 -0
  160. package/src/services/desktop-fused-ffi-backend-runtime.d.ts.map +1 -0
  161. package/src/services/desktop-fused-ffi-backend-runtime.ts +339 -0
  162. package/src/services/device-bridge.d.ts +188 -0
  163. package/src/services/device-bridge.d.ts.map +1 -0
  164. package/src/services/device-bridge.ts +1237 -0
  165. package/src/services/device-resource-metrics.d.ts +149 -0
  166. package/src/services/device-resource-metrics.d.ts.map +1 -0
  167. package/src/services/device-resource-metrics.test.ts +98 -0
  168. package/src/services/device-resource-metrics.ts +346 -0
  169. package/src/services/device-tier.d.ts +115 -0
  170. package/src/services/device-tier.d.ts.map +1 -0
  171. package/src/services/device-tier.test.ts +371 -0
  172. package/src/services/device-tier.ts +410 -0
  173. package/src/services/downloader.d.ts +82 -0
  174. package/src/services/downloader.d.ts.map +1 -0
  175. package/src/services/downloader.test.ts +747 -0
  176. package/src/services/downloader.ts +925 -0
  177. package/src/services/engine-direct-bundle.test.ts +58 -0
  178. package/src/services/engine-streaming.test.ts +80 -0
  179. package/src/services/engine.d.ts +540 -0
  180. package/src/services/engine.d.ts.map +1 -0
  181. package/src/services/engine.ts +1909 -0
  182. package/src/services/ensure-local-artifacts.integration.test.ts +273 -0
  183. package/src/services/ensure-local-artifacts.test.ts +368 -0
  184. package/src/services/ensure-local-artifacts.ts +351 -0
  185. package/src/services/external-scanner.d.ts +17 -0
  186. package/src/services/external-scanner.d.ts.map +1 -0
  187. package/src/services/external-scanner.ts +312 -0
  188. package/src/services/ffi-llm-mock.ts +354 -0
  189. package/src/services/ffi-llm-streaming-abi.ts +442 -0
  190. package/src/services/ffi-streaming-backend.d.ts +180 -0
  191. package/src/services/ffi-streaming-backend.d.ts.map +1 -0
  192. package/src/services/ffi-streaming-backend.ts +382 -0
  193. package/src/services/ffi-streaming-runner.d.ts +122 -0
  194. package/src/services/ffi-streaming-runner.d.ts.map +1 -0
  195. package/src/services/ffi-streaming-runner.test.ts +60 -0
  196. package/src/services/ffi-streaming-runner.ts +354 -0
  197. package/src/services/ffi-unload-ordering.test.ts +162 -0
  198. package/src/services/gpu-autotune.ts +534 -0
  199. package/src/services/gpu-detect.d.ts +56 -0
  200. package/src/services/gpu-detect.d.ts.map +1 -0
  201. package/src/services/gpu-detect.ts +139 -0
  202. package/src/services/handler-registry.d.ts +72 -0
  203. package/src/services/handler-registry.d.ts.map +1 -0
  204. package/src/services/handler-registry.ts +240 -0
  205. package/src/services/hardware.d.ts +63 -0
  206. package/src/services/hardware.d.ts.map +1 -0
  207. package/src/services/hardware.test.ts +231 -0
  208. package/src/services/hardware.ts +410 -0
  209. package/src/services/hf-search.d.ts +26 -0
  210. package/src/services/hf-search.d.ts.map +1 -0
  211. package/src/services/hf-search.test.ts +69 -0
  212. package/src/services/hf-search.ts +420 -0
  213. package/src/services/image-description-runtime.d.ts +14 -0
  214. package/src/services/image-description-runtime.d.ts.map +1 -0
  215. package/src/services/image-description-runtime.test.ts +61 -0
  216. package/src/services/image-description-runtime.ts +118 -0
  217. package/src/services/imagegen/aosp-unavailable.d.ts +134 -0
  218. package/src/services/imagegen/aosp-unavailable.d.ts.map +1 -0
  219. package/src/services/imagegen/aosp-unavailable.ts +229 -0
  220. package/src/services/imagegen/backend-selector.d.ts +118 -0
  221. package/src/services/imagegen/backend-selector.d.ts.map +1 -0
  222. package/src/services/imagegen/backend-selector.ts +277 -0
  223. package/src/services/imagegen/coreml-unavailable.d.ts +105 -0
  224. package/src/services/imagegen/coreml-unavailable.d.ts.map +1 -0
  225. package/src/services/imagegen/coreml-unavailable.ts +237 -0
  226. package/src/services/imagegen/errors.d.ts +16 -0
  227. package/src/services/imagegen/errors.d.ts.map +1 -0
  228. package/src/services/imagegen/errors.ts +40 -0
  229. package/src/services/imagegen/index.d.ts +58 -0
  230. package/src/services/imagegen/index.d.ts.map +1 -0
  231. package/src/services/imagegen/index.ts +144 -0
  232. package/src/services/imagegen/mflux.d.ts +74 -0
  233. package/src/services/imagegen/mflux.d.ts.map +1 -0
  234. package/src/services/imagegen/mflux.ts +313 -0
  235. package/src/services/imagegen/sd-cpp.d.ts +180 -0
  236. package/src/services/imagegen/sd-cpp.d.ts.map +1 -0
  237. package/src/services/imagegen/sd-cpp.ts +718 -0
  238. package/src/services/imagegen/tensorrt-unavailable.d.ts +83 -0
  239. package/src/services/imagegen/tensorrt-unavailable.d.ts.map +1 -0
  240. package/src/services/imagegen/tensorrt-unavailable.ts +295 -0
  241. package/src/services/imagegen/types.d.ts +181 -0
  242. package/src/services/imagegen/types.d.ts.map +1 -0
  243. package/src/services/imagegen/types.ts +193 -0
  244. package/src/services/index.d.ts +29 -0
  245. package/src/services/index.d.ts.map +1 -0
  246. package/src/services/index.ts +211 -0
  247. package/src/services/inference-capabilities.d.ts +132 -0
  248. package/src/services/inference-capabilities.d.ts.map +1 -0
  249. package/src/services/inference-capabilities.test.ts +75 -0
  250. package/src/services/inference-capabilities.ts +204 -0
  251. package/src/services/inference-telemetry.d.ts +59 -0
  252. package/src/services/inference-telemetry.d.ts.map +1 -0
  253. package/src/services/inference-telemetry.ts +143 -0
  254. package/src/services/ios-llama-streaming.ts +248 -0
  255. package/src/services/kv-spill.d.ts +189 -0
  256. package/src/services/kv-spill.d.ts.map +1 -0
  257. package/src/services/kv-spill.test.ts +222 -0
  258. package/src/services/kv-spill.ts +356 -0
  259. package/src/services/latency-trace.d.ts +346 -0
  260. package/src/services/latency-trace.d.ts.map +1 -0
  261. package/src/services/latency-trace.test.ts +266 -0
  262. package/src/services/latency-trace.ts +844 -0
  263. package/src/services/llama-server-metrics.ts +304 -0
  264. package/src/services/llm-streaming-binding.d.ts +96 -0
  265. package/src/services/llm-streaming-binding.d.ts.map +1 -0
  266. package/src/services/llm-streaming-binding.ts +136 -0
  267. package/src/services/load-args.d.ts +82 -0
  268. package/src/services/load-args.d.ts.map +1 -0
  269. package/src/services/load-args.ts +81 -0
  270. package/src/services/manifest/eliza-1.manifest.v1.json +708 -0
  271. package/src/services/manifest/index.d.ts +4 -0
  272. package/src/services/manifest/index.d.ts.map +1 -0
  273. package/src/services/manifest/index.ts +66 -0
  274. package/src/services/manifest/manifest.test.ts +689 -0
  275. package/src/services/manifest/schema.d.ts +713 -0
  276. package/src/services/manifest/schema.d.ts.map +1 -0
  277. package/src/services/manifest/schema.ts +653 -0
  278. package/src/services/manifest/types.d.ts +30 -0
  279. package/src/services/manifest/types.d.ts.map +1 -0
  280. package/src/services/manifest/types.ts +55 -0
  281. package/src/services/manifest/validator.d.ts +66 -0
  282. package/src/services/manifest/validator.d.ts.map +1 -0
  283. package/src/services/manifest/validator.ts +567 -0
  284. package/src/services/memory-arbiter.d.ts +318 -0
  285. package/src/services/memory-arbiter.d.ts.map +1 -0
  286. package/src/services/memory-arbiter.test.ts +419 -0
  287. package/src/services/memory-arbiter.ts +925 -0
  288. package/src/services/memory-monitor.d.ts +122 -0
  289. package/src/services/memory-monitor.d.ts.map +1 -0
  290. package/src/services/memory-monitor.test.ts +208 -0
  291. package/src/services/memory-monitor.ts +297 -0
  292. package/src/services/memory-pressure.d.ts +130 -0
  293. package/src/services/memory-pressure.d.ts.map +1 -0
  294. package/src/services/memory-pressure.ts +414 -0
  295. package/src/services/mtp-doctor.d.ts +13 -0
  296. package/src/services/mtp-doctor.d.ts.map +1 -0
  297. package/src/services/mtp-doctor.ts +78 -0
  298. package/src/services/network-policy.d.ts +127 -0
  299. package/src/services/network-policy.d.ts.map +1 -0
  300. package/src/services/network-policy.ts +346 -0
  301. package/src/services/paths.d.ts +6 -0
  302. package/src/services/paths.d.ts.map +1 -0
  303. package/src/services/paths.ts +25 -0
  304. package/src/services/planner-skeleton.d.ts +124 -0
  305. package/src/services/planner-skeleton.d.ts.map +1 -0
  306. package/src/services/planner-skeleton.ts +175 -0
  307. package/src/services/providers.d.ts +38 -0
  308. package/src/services/providers.d.ts.map +1 -0
  309. package/src/services/providers.ts +507 -0
  310. package/src/services/ram-budget-cache.test.ts +163 -0
  311. package/src/services/ram-budget.d.ts +110 -0
  312. package/src/services/ram-budget.d.ts.map +1 -0
  313. package/src/services/ram-budget.ts +0 -0
  314. package/src/services/readiness.d.ts +9 -0
  315. package/src/services/readiness.d.ts.map +1 -0
  316. package/src/services/readiness.test.ts +87 -0
  317. package/src/services/readiness.ts +238 -0
  318. package/src/services/recommendation.d.ts +111 -0
  319. package/src/services/recommendation.d.ts.map +1 -0
  320. package/src/services/recommendation.ts +671 -0
  321. package/src/services/registry.d.ts +35 -0
  322. package/src/services/registry.d.ts.map +1 -0
  323. package/src/services/registry.ts +151 -0
  324. package/src/services/router-handler.d.ts +92 -0
  325. package/src/services/router-handler.d.ts.map +1 -0
  326. package/src/services/router-handler.test.ts +45 -0
  327. package/src/services/router-handler.ts +407 -0
  328. package/src/services/routing-policy.d.ts +69 -0
  329. package/src/services/routing-policy.d.ts.map +1 -0
  330. package/src/services/routing-policy.test.ts +164 -0
  331. package/src/services/routing-policy.ts +297 -0
  332. package/src/services/routing-preferences.d.ts +8 -0
  333. package/src/services/routing-preferences.d.ts.map +1 -0
  334. package/src/services/routing-preferences.ts +17 -0
  335. package/src/services/runtime-target.d.ts +98 -0
  336. package/src/services/runtime-target.d.ts.map +1 -0
  337. package/src/services/runtime-target.ts +154 -0
  338. package/src/services/service.d.ts +128 -0
  339. package/src/services/service.d.ts.map +1 -0
  340. package/src/services/service.test.ts +223 -0
  341. package/src/services/service.ts +735 -0
  342. package/src/services/session-pool.d.ts +72 -0
  343. package/src/services/session-pool.d.ts.map +1 -0
  344. package/src/services/session-pool.ts +153 -0
  345. package/src/services/structured-output/deterministic-repair.d.ts +23 -0
  346. package/src/services/structured-output/deterministic-repair.d.ts.map +1 -0
  347. package/src/services/structured-output/deterministic-repair.test.ts +169 -0
  348. package/src/services/structured-output/deterministic-repair.ts +443 -0
  349. package/src/services/structured-output/index.ts +4 -0
  350. package/src/services/structured-output.d.ts +311 -0
  351. package/src/services/structured-output.d.ts.map +1 -0
  352. package/src/services/structured-output.test.ts +483 -0
  353. package/src/services/structured-output.ts +712 -0
  354. package/src/services/system-memory.d.ts +33 -0
  355. package/src/services/system-memory.d.ts.map +1 -0
  356. package/src/services/system-memory.test.ts +47 -0
  357. package/src/services/system-memory.ts +67 -0
  358. package/src/services/transcription-priority.test.ts +211 -0
  359. package/src/services/types.d.ts +19 -0
  360. package/src/services/types.d.ts.map +1 -0
  361. package/src/services/types.ts +55 -0
  362. package/src/services/verify-on-device.d.ts +34 -0
  363. package/src/services/verify-on-device.d.ts.map +1 -0
  364. package/src/services/verify-on-device.test.ts +87 -0
  365. package/src/services/verify-on-device.ts +127 -0
  366. package/src/services/verify.d.ts +8 -0
  367. package/src/services/verify.d.ts.map +1 -0
  368. package/src/services/verify.ts +13 -0
  369. package/src/services/vision/aosp-unavailable.d.ts +115 -0
  370. package/src/services/vision/aosp-unavailable.d.ts.map +1 -0
  371. package/src/services/vision/aosp-unavailable.ts +163 -0
  372. package/src/services/vision/capacitor-llama.d.ts +99 -0
  373. package/src/services/vision/capacitor-llama.d.ts.map +1 -0
  374. package/src/services/vision/capacitor-llama.ts +255 -0
  375. package/src/services/vision/cloud-fallback.d.ts +47 -0
  376. package/src/services/vision/cloud-fallback.d.ts.map +1 -0
  377. package/src/services/vision/cloud-fallback.test.ts +243 -0
  378. package/src/services/vision/cloud-fallback.ts +268 -0
  379. package/src/services/vision/fallback-chain.test.ts +86 -0
  380. package/src/services/vision/hash.d.ts +71 -0
  381. package/src/services/vision/hash.d.ts.map +1 -0
  382. package/src/services/vision/hash.ts +157 -0
  383. package/src/services/vision/index.d.ts +95 -0
  384. package/src/services/vision/index.d.ts.map +1 -0
  385. package/src/services/vision/index.ts +251 -0
  386. package/src/services/vision/llama-server.d.ts +73 -0
  387. package/src/services/vision/llama-server.d.ts.map +1 -0
  388. package/src/services/vision/llama-server.ts +177 -0
  389. package/src/services/vision/types.d.ts +153 -0
  390. package/src/services/vision/types.d.ts.map +1 -0
  391. package/src/services/vision/types.ts +154 -0
  392. package/src/services/vision/vast-fallback.d.ts +18 -0
  393. package/src/services/vision/vast-fallback.d.ts.map +1 -0
  394. package/src/services/vision/vast-fallback.ts +127 -0
  395. package/src/services/vision-embedding-cache.d.ts +98 -0
  396. package/src/services/vision-embedding-cache.d.ts.map +1 -0
  397. package/src/services/vision-embedding-cache.ts +189 -0
  398. package/src/services/voice/VOICE_WORKBENCH.md +88 -0
  399. package/src/services/voice/__test-helpers__/fake-ffi.ts +94 -0
  400. package/src/services/voice/__test-helpers__/synthetic-speech.ts +124 -0
  401. package/src/services/voice/__tests__/checkpoint-manager.test.ts +241 -0
  402. package/src/services/voice/__tests__/checkpoint-policy.test.ts +270 -0
  403. package/src/services/voice/__tests__/eager-context-builder.test.ts +257 -0
  404. package/src/services/voice/__tests__/eliza1-eot-scorer.test.ts +288 -0
  405. package/src/services/voice/__tests__/eot-classifier.test.ts +431 -0
  406. package/src/services/voice/__tests__/optimistic-rollback.test.ts +312 -0
  407. package/src/services/voice/__tests__/prefill-client.test.ts +266 -0
  408. package/src/services/voice/__tests__/prefix-preserving-queue.test.ts +208 -0
  409. package/src/services/voice/__tests__/streaming-asr.test.ts +450 -0
  410. package/src/services/voice/__tests__/streaming-transcriber.test.ts +339 -0
  411. package/src/services/voice/__tests__/turn-detector-resolver.test.ts +195 -0
  412. package/src/services/voice/__tests__/voice-state-machine-prefill.test.ts +275 -0
  413. package/src/services/voice/__tests__/voice-state-machine.test.ts +354 -0
  414. package/src/services/voice/asr-timed.real.test.ts +141 -0
  415. package/src/services/voice/audio-frame-consumer.d.ts +212 -0
  416. package/src/services/voice/audio-frame-consumer.d.ts.map +1 -0
  417. package/src/services/voice/audio-frame-consumer.test.ts +343 -0
  418. package/src/services/voice/audio-frame-consumer.ts +491 -0
  419. package/src/services/voice/barge-in.d.ts +112 -0
  420. package/src/services/voice/barge-in.d.ts.map +1 -0
  421. package/src/services/voice/barge-in.test.ts +244 -0
  422. package/src/services/voice/barge-in.ts +336 -0
  423. package/src/services/voice/cancellation-coordinator.d.ts +127 -0
  424. package/src/services/voice/cancellation-coordinator.d.ts.map +1 -0
  425. package/src/services/voice/cancellation-coordinator.test.ts +196 -0
  426. package/src/services/voice/cancellation-coordinator.ts +269 -0
  427. package/src/services/voice/checkpoint-manager.d.ts +199 -0
  428. package/src/services/voice/checkpoint-manager.d.ts.map +1 -0
  429. package/src/services/voice/checkpoint-manager.ts +401 -0
  430. package/src/services/voice/checkpoint-policy.ts +336 -0
  431. package/src/services/voice/composite-eot-classifier.test.ts +59 -0
  432. package/src/services/voice/e2e-harness.test.ts +182 -0
  433. package/src/services/voice/e2e-harness.ts +743 -0
  434. package/src/services/voice/eager-context-builder.d.ts +170 -0
  435. package/src/services/voice/eager-context-builder.d.ts.map +1 -0
  436. package/src/services/voice/eager-context-builder.ts +262 -0
  437. package/src/services/voice/eliza1-eot-scorer.d.ts +124 -0
  438. package/src/services/voice/eliza1-eot-scorer.d.ts.map +1 -0
  439. package/src/services/voice/eliza1-eot-scorer.ts +242 -0
  440. package/src/services/voice/embedding-server.ts +200 -0
  441. package/src/services/voice/embedding.d.ts +133 -0
  442. package/src/services/voice/embedding.d.ts.map +1 -0
  443. package/src/services/voice/embedding.test.ts +131 -0
  444. package/src/services/voice/embedding.ts +243 -0
  445. package/src/services/voice/emotion-attribution.d.ts +68 -0
  446. package/src/services/voice/emotion-attribution.d.ts.map +1 -0
  447. package/src/services/voice/emotion-attribution.test.ts +129 -0
  448. package/src/services/voice/emotion-attribution.ts +361 -0
  449. package/src/services/voice/engine-bridge-cancellation.test.ts +422 -0
  450. package/src/services/voice/engine-bridge.d.ts +759 -0
  451. package/src/services/voice/engine-bridge.d.ts.map +1 -0
  452. package/src/services/voice/engine-bridge.test.ts +384 -0
  453. package/src/services/voice/engine-bridge.ts +2302 -0
  454. package/src/services/voice/eot-classifier-ggml.d.ts +179 -0
  455. package/src/services/voice/eot-classifier-ggml.d.ts.map +1 -0
  456. package/src/services/voice/eot-classifier-ggml.ts +566 -0
  457. package/src/services/voice/eot-classifier.d.ts +214 -0
  458. package/src/services/voice/eot-classifier.d.ts.map +1 -0
  459. package/src/services/voice/eot-classifier.ts +533 -0
  460. package/src/services/voice/errors.d.ts +20 -0
  461. package/src/services/voice/errors.d.ts.map +1 -0
  462. package/src/services/voice/errors.ts +32 -0
  463. package/src/services/voice/expressive-tags.d.ts +158 -0
  464. package/src/services/voice/expressive-tags.d.ts.map +1 -0
  465. package/src/services/voice/expressive-tags.ts +405 -0
  466. package/src/services/voice/ffi-bindings.d.ts +674 -0
  467. package/src/services/voice/ffi-bindings.d.ts.map +1 -0
  468. package/src/services/voice/ffi-bindings.test.ts +728 -0
  469. package/src/services/voice/ffi-bindings.ts +3225 -0
  470. package/src/services/voice/first-line-cache.d.ts +181 -0
  471. package/src/services/voice/first-line-cache.d.ts.map +1 -0
  472. package/src/services/voice/first-line-cache.ts +725 -0
  473. package/src/services/voice/fused-eot-scorer.d.ts +51 -0
  474. package/src/services/voice/fused-eot-scorer.d.ts.map +1 -0
  475. package/src/services/voice/fused-eot-scorer.ts +135 -0
  476. package/src/services/voice/index.d.ts +91 -0
  477. package/src/services/voice/index.d.ts.map +1 -0
  478. package/src/services/voice/index.ts +481 -0
  479. package/src/services/voice/kokoro/__tests__/kokoro-backend.test.ts +151 -0
  480. package/src/services/voice/kokoro/__tests__/kokoro-engine-bridge.real.test.ts +151 -0
  481. package/src/services/voice/kokoro/__tests__/kokoro-engine-bridge.test.ts +60 -0
  482. package/src/services/voice/kokoro/__tests__/kokoro-engine-discovery.test.ts +277 -0
  483. package/src/services/voice/kokoro/__tests__/kokoro-ffi-runtime.test.ts +235 -0
  484. package/src/services/voice/kokoro/__tests__/kokoro-runtime.test.ts +95 -0
  485. package/src/services/voice/kokoro/__tests__/phonemizer.test.ts +53 -0
  486. package/src/services/voice/kokoro/__tests__/runtime-selection.test.ts +231 -0
  487. package/src/services/voice/kokoro/__tests__/voices.test.ts +57 -0
  488. package/src/services/voice/kokoro/index.ts +79 -0
  489. package/src/services/voice/kokoro/kokoro-backend.d.ts +72 -0
  490. package/src/services/voice/kokoro/kokoro-backend.d.ts.map +1 -0
  491. package/src/services/voice/kokoro/kokoro-backend.ts +207 -0
  492. package/src/services/voice/kokoro/kokoro-engine-discovery.d.ts +58 -0
  493. package/src/services/voice/kokoro/kokoro-engine-discovery.d.ts.map +1 -0
  494. package/src/services/voice/kokoro/kokoro-engine-discovery.ts +177 -0
  495. package/src/services/voice/kokoro/kokoro-ffi-runtime.d.ts +75 -0
  496. package/src/services/voice/kokoro/kokoro-ffi-runtime.d.ts.map +1 -0
  497. package/src/services/voice/kokoro/kokoro-ffi-runtime.ts +233 -0
  498. package/src/services/voice/kokoro/kokoro-runtime.d.ts +100 -0
  499. package/src/services/voice/kokoro/kokoro-runtime.d.ts.map +1 -0
  500. package/src/services/voice/kokoro/kokoro-runtime.ts +170 -0
  501. package/src/services/voice/kokoro/phoneme-stream.ts +123 -0
  502. package/src/services/voice/kokoro/phonemizer.d.ts +50 -0
  503. package/src/services/voice/kokoro/phonemizer.d.ts.map +1 -0
  504. package/src/services/voice/kokoro/phonemizer.ts +344 -0
  505. package/src/services/voice/kokoro/pick-runtime.d.ts +61 -0
  506. package/src/services/voice/kokoro/pick-runtime.d.ts.map +1 -0
  507. package/src/services/voice/kokoro/pick-runtime.test.ts +91 -0
  508. package/src/services/voice/kokoro/pick-runtime.ts +130 -0
  509. package/src/services/voice/kokoro/runtime-selection.d.ts +92 -0
  510. package/src/services/voice/kokoro/runtime-selection.d.ts.map +1 -0
  511. package/src/services/voice/kokoro/runtime-selection.ts +237 -0
  512. package/src/services/voice/kokoro/types.d.ts +82 -0
  513. package/src/services/voice/kokoro/types.d.ts.map +1 -0
  514. package/src/services/voice/kokoro/types.ts +95 -0
  515. package/src/services/voice/kokoro/voice-presets.d.ts +23 -0
  516. package/src/services/voice/kokoro/voice-presets.d.ts.map +1 -0
  517. package/src/services/voice/kokoro/voice-presets.ts +129 -0
  518. package/src/services/voice/kokoro/voices.d.ts +30 -0
  519. package/src/services/voice/kokoro/voices.d.ts.map +1 -0
  520. package/src/services/voice/kokoro/voices.ts +64 -0
  521. package/src/services/voice/lifecycle.d.ts +135 -0
  522. package/src/services/voice/lifecycle.d.ts.map +1 -0
  523. package/src/services/voice/lifecycle.test.ts +315 -0
  524. package/src/services/voice/lifecycle.ts +301 -0
  525. package/src/services/voice/live-diarization-session.d.ts +96 -0
  526. package/src/services/voice/live-diarization-session.d.ts.map +1 -0
  527. package/src/services/voice/live-diarization-session.ts +289 -0
  528. package/src/services/voice/mic-source.d.ts +136 -0
  529. package/src/services/voice/mic-source.d.ts.map +1 -0
  530. package/src/services/voice/mic-source.test.ts +210 -0
  531. package/src/services/voice/mic-source.ts +503 -0
  532. package/src/services/voice/optimistic-policy.d.ts +109 -0
  533. package/src/services/voice/optimistic-policy.d.ts.map +1 -0
  534. package/src/services/voice/optimistic-policy.test.ts +101 -0
  535. package/src/services/voice/optimistic-policy.ts +192 -0
  536. package/src/services/voice/optimistic-rollback.ts +343 -0
  537. package/src/services/voice/partial-stabilizer.d.ts +73 -0
  538. package/src/services/voice/partial-stabilizer.d.ts.map +1 -0
  539. package/src/services/voice/partial-stabilizer.test.ts +68 -0
  540. package/src/services/voice/partial-stabilizer.ts +140 -0
  541. package/src/services/voice/phoneme-tokenizer.d.ts +49 -0
  542. package/src/services/voice/phoneme-tokenizer.d.ts.map +1 -0
  543. package/src/services/voice/phoneme-tokenizer.ts +158 -0
  544. package/src/services/voice/phrase-cache.d.ts +76 -0
  545. package/src/services/voice/phrase-cache.d.ts.map +1 -0
  546. package/src/services/voice/phrase-cache.test.ts +242 -0
  547. package/src/services/voice/phrase-cache.ts +186 -0
  548. package/src/services/voice/phrase-chunker.d.ts +62 -0
  549. package/src/services/voice/phrase-chunker.d.ts.map +1 -0
  550. package/src/services/voice/phrase-chunker.test.ts +239 -0
  551. package/src/services/voice/phrase-chunker.ts +281 -0
  552. package/src/services/voice/pipeline-impls.d.ts +151 -0
  553. package/src/services/voice/pipeline-impls.d.ts.map +1 -0
  554. package/src/services/voice/pipeline-impls.l6.test.ts +110 -0
  555. package/src/services/voice/pipeline-impls.test.ts +292 -0
  556. package/src/services/voice/pipeline-impls.ts +315 -0
  557. package/src/services/voice/pipeline.d.ts +216 -0
  558. package/src/services/voice/pipeline.d.ts.map +1 -0
  559. package/src/services/voice/pipeline.ts +505 -0
  560. package/src/services/voice/prefill-client.d.ts +123 -0
  561. package/src/services/voice/prefill-client.d.ts.map +1 -0
  562. package/src/services/voice/prefill-client.ts +316 -0
  563. package/src/services/voice/prefix-preserving-queue.d.ts +113 -0
  564. package/src/services/voice/prefix-preserving-queue.d.ts.map +1 -0
  565. package/src/services/voice/prefix-preserving-queue.ts +162 -0
  566. package/src/services/voice/profile-store.d.ts +248 -0
  567. package/src/services/voice/profile-store.d.ts.map +1 -0
  568. package/src/services/voice/profile-store.ts +887 -0
  569. package/src/services/voice/real-audio-decode.test.ts +148 -0
  570. package/src/services/voice/ring-buffer.d.ts +40 -0
  571. package/src/services/voice/ring-buffer.d.ts.map +1 -0
  572. package/src/services/voice/ring-buffer.test.ts +129 -0
  573. package/src/services/voice/ring-buffer.ts +123 -0
  574. package/src/services/voice/rollback-queue.d.ts +24 -0
  575. package/src/services/voice/rollback-queue.d.ts.map +1 -0
  576. package/src/services/voice/rollback-queue.ts +74 -0
  577. package/src/services/voice/samantha-preset-placeholder.d.ts +67 -0
  578. package/src/services/voice/samantha-preset-placeholder.d.ts.map +1 -0
  579. package/src/services/voice/samantha-preset-placeholder.test.ts +97 -0
  580. package/src/services/voice/samantha-preset-placeholder.ts +148 -0
  581. package/src/services/voice/samantha-preset-regenerator.d.ts +87 -0
  582. package/src/services/voice/samantha-preset-regenerator.d.ts.map +1 -0
  583. package/src/services/voice/samantha-preset-regenerator.ts +393 -0
  584. package/src/services/voice/scheduler.d.ts +146 -0
  585. package/src/services/voice/scheduler.d.ts.map +1 -0
  586. package/src/services/voice/scheduler.t2.test.ts +141 -0
  587. package/src/services/voice/scheduler.ts +927 -0
  588. package/src/services/voice/shared-resources.d.ts +190 -0
  589. package/src/services/voice/shared-resources.d.ts.map +1 -0
  590. package/src/services/voice/shared-resources.ts +320 -0
  591. package/src/services/voice/speaker/attribution-pipeline.d.ts +74 -0
  592. package/src/services/voice/speaker/attribution-pipeline.d.ts.map +1 -0
  593. package/src/services/voice/speaker/attribution-pipeline.ts +386 -0
  594. package/src/services/voice/speaker/diarizer-fused.d.ts +59 -0
  595. package/src/services/voice/speaker/diarizer-fused.d.ts.map +1 -0
  596. package/src/services/voice/speaker/diarizer-fused.real.test.ts +100 -0
  597. package/src/services/voice/speaker/diarizer-fused.ts +154 -0
  598. package/src/services/voice/speaker/diarizer.d.ts +75 -0
  599. package/src/services/voice/speaker/diarizer.d.ts.map +1 -0
  600. package/src/services/voice/speaker/diarizer.ts +218 -0
  601. package/src/services/voice/speaker/encoder-fused.d.ts +60 -0
  602. package/src/services/voice/speaker/encoder-fused.d.ts.map +1 -0
  603. package/src/services/voice/speaker/encoder-fused.real.test.ts +113 -0
  604. package/src/services/voice/speaker/encoder-fused.ts +138 -0
  605. package/src/services/voice/speaker/encoder-ggml.d.ts +33 -0
  606. package/src/services/voice/speaker/encoder-ggml.d.ts.map +1 -0
  607. package/src/services/voice/speaker/encoder-ggml.ts +79 -0
  608. package/src/services/voice/speaker/encoder.d.ts +37 -0
  609. package/src/services/voice/speaker/encoder.d.ts.map +1 -0
  610. package/src/services/voice/speaker/encoder.ts +105 -0
  611. package/src/services/voice/speaker-imprint.d.ts +83 -0
  612. package/src/services/voice/speaker-imprint.d.ts.map +1 -0
  613. package/src/services/voice/speaker-imprint.test.ts +185 -0
  614. package/src/services/voice/speaker-imprint.ts +312 -0
  615. package/src/services/voice/speaker-preset-cache.d.ts +77 -0
  616. package/src/services/voice/speaker-preset-cache.d.ts.map +1 -0
  617. package/src/services/voice/speaker-preset-cache.test.ts +154 -0
  618. package/src/services/voice/speaker-preset-cache.ts +195 -0
  619. package/src/services/voice/streaming-asr/streaming-pipeline-adapter.ts +292 -0
  620. package/src/services/voice/system-audio-sink.d.ts +73 -0
  621. package/src/services/voice/system-audio-sink.d.ts.map +1 -0
  622. package/src/services/voice/system-audio-sink.test.ts +29 -0
  623. package/src/services/voice/system-audio-sink.ts +366 -0
  624. package/src/services/voice/transcriber.d.ts +244 -0
  625. package/src/services/voice/transcriber.d.ts.map +1 -0
  626. package/src/services/voice/transcriber.test.ts +392 -0
  627. package/src/services/voice/transcriber.ts +704 -0
  628. package/src/services/voice/transcript-knowledge.d.ts +37 -0
  629. package/src/services/voice/transcript-knowledge.d.ts.map +1 -0
  630. package/src/services/voice/transcript-knowledge.test.ts +68 -0
  631. package/src/services/voice/transcript-knowledge.ts +75 -0
  632. package/src/services/voice/transcript-service.d.ts +41 -0
  633. package/src/services/voice/transcript-service.d.ts.map +1 -0
  634. package/src/services/voice/transcript-service.test.ts +137 -0
  635. package/src/services/voice/transcript-service.ts +141 -0
  636. package/src/services/voice/transcript-store.d.ts +53 -0
  637. package/src/services/voice/transcript-store.d.ts.map +1 -0
  638. package/src/services/voice/transcript-store.test.ts +153 -0
  639. package/src/services/voice/transcript-store.ts +132 -0
  640. package/src/services/voice/turn-controller.d.ts +183 -0
  641. package/src/services/voice/turn-controller.d.ts.map +1 -0
  642. package/src/services/voice/turn-controller.test.ts +575 -0
  643. package/src/services/voice/turn-controller.ts +596 -0
  644. package/src/services/voice/types.d.ts +643 -0
  645. package/src/services/voice/types.d.ts.map +1 -0
  646. package/src/services/voice/types.ts +699 -0
  647. package/src/services/voice/vad.d.ts +282 -0
  648. package/src/services/voice/vad.d.ts.map +1 -0
  649. package/src/services/voice/vad.test.ts +480 -0
  650. package/src/services/voice/vad.ts +827 -0
  651. package/src/services/voice/vad.v1-v4.test.ts +222 -0
  652. package/src/services/voice/voice-budget.d.ts +241 -0
  653. package/src/services/voice/voice-budget.d.ts.map +1 -0
  654. package/src/services/voice/voice-budget.test.ts +418 -0
  655. package/src/services/voice/voice-budget.ts +635 -0
  656. package/src/services/voice/voice-duet.test.ts +375 -0
  657. package/src/services/voice/voice-emotion-classifier.d.ts +95 -0
  658. package/src/services/voice/voice-emotion-classifier.d.ts.map +1 -0
  659. package/src/services/voice/voice-emotion-classifier.test.ts +210 -0
  660. package/src/services/voice/voice-emotion-classifier.ts +273 -0
  661. package/src/services/voice/voice-preset-format.d.ts +158 -0
  662. package/src/services/voice/voice-preset-format.d.ts.map +1 -0
  663. package/src/services/voice/voice-preset-format.ts +700 -0
  664. package/src/services/voice/voice-preset-generator.test.ts +89 -0
  665. package/src/services/voice/voice-profile-artifact.d.ts +116 -0
  666. package/src/services/voice/voice-profile-artifact.d.ts.map +1 -0
  667. package/src/services/voice/voice-profile-artifact.test.ts +138 -0
  668. package/src/services/voice/voice-profile-artifact.ts +518 -0
  669. package/src/services/voice/voice-profile-routes.d.ts +83 -0
  670. package/src/services/voice/voice-profile-routes.d.ts.map +1 -0
  671. package/src/services/voice/voice-profile-routes.test.ts +429 -0
  672. package/src/services/voice/voice-profile-routes.ts +425 -0
  673. package/src/services/voice/voice-scenario.ts +154 -0
  674. package/src/services/voice/voice-settings.d.ts +82 -0
  675. package/src/services/voice/voice-settings.d.ts.map +1 -0
  676. package/src/services/voice/voice-settings.ts +172 -0
  677. package/src/services/voice/voice-state-machine.d.ts +364 -0
  678. package/src/services/voice/voice-state-machine.d.ts.map +1 -0
  679. package/src/services/voice/voice-state-machine.ts +727 -0
  680. package/src/services/voice/voice-workbench-report.test.ts +168 -0
  681. package/src/services/voice/voice-workbench-report.ts +326 -0
  682. package/src/services/voice/voice-workbench.test.ts +158 -0
  683. package/src/services/voice/voice.test.ts +1070 -0
  684. package/src/services/voice/wake-word-ggml.d.ts +101 -0
  685. package/src/services/voice/wake-word-ggml.d.ts.map +1 -0
  686. package/src/services/voice/wake-word-ggml.ts +320 -0
  687. package/src/services/voice/wake-word.d.ts +255 -0
  688. package/src/services/voice/wake-word.d.ts.map +1 -0
  689. package/src/services/voice/wake-word.test.ts +298 -0
  690. package/src/services/voice/wake-word.ts +554 -0
  691. package/src/services/voice/wrap-with-first-line-cache.d.ts +70 -0
  692. package/src/services/voice/wrap-with-first-line-cache.d.ts.map +1 -0
  693. package/src/services/voice/wrap-with-first-line-cache.ts +267 -0
  694. package/src/services/voice-model-updater.d.ts +240 -0
  695. package/src/services/voice-model-updater.d.ts.map +1 -0
  696. package/src/services/voice-model-updater.ts +724 -0
  697. package/src/services/voice-prewarm.d.ts +3 -0
  698. package/src/services/voice-prewarm.d.ts.map +1 -0
  699. package/src/services/voice-prewarm.ts +51 -0
  700. package/dist/index.d.ts +0 -37
  701. package/dist/index.js +0 -1098
@@ -0,0 +1,3225 @@
1
+ /**
2
+ * Node/Bun FFI binding to `libelizainference.{dylib,so,dll}`.
3
+ *
4
+ * The fused omnivoice + llama.cpp build (see
5
+ * `packages/app-core/scripts/omnivoice-fuse/`) produces ONE shared
6
+ * library that exports both `llama_*` and `omnivoice_*` symbols plus
7
+ * the C ABI declared in `scripts/omnivoice-fuse/ffi.h`. This module is
8
+ * the JS-side proxy for that ABI — it loads the library, binds every
9
+ * `eliza_inference_*` symbol declared in `ffi.h`, and exposes a typed
10
+ * handle (`ElizaInferenceFfi`) the voice lifecycle calls into.
11
+ *
12
+ * Runtime: production runs under Bun (Electrobun shell, Capacitor
13
+ * bridge), so the loader uses `bun:ffi`. Tests that need to actually
14
+ * load a `.dylib` against a stub library spawn a `bun` subprocess —
15
+ * see `ffi-bindings.test.ts`. Calling this loader from a non-Bun
16
+ * runtime (e.g. plain Node) throws `VoiceLifecycleError({code:
17
+ * "missing-ffi"})` with a diagnostic explaining why.
18
+ *
19
+ * No defensive try/catch on the success path. Any dlopen failure,
20
+ * symbol-resolution failure, or ABI mismatch is a structured throw
21
+ * (AGENTS.md §3 + §9). The caller — `voice/lifecycle.ts` and
22
+ * `voice/engine-bridge.ts` — surfaces it as a `VoiceLifecycleError` to
23
+ * the UI.
24
+ */
25
+
26
+ import { VoiceLifecycleError } from "./lifecycle";
27
+
28
+ /**
29
+ * ABI version the JS binding was authored against. Must match the value
30
+ * `eliza_inference_abi_version()` returns at runtime — a mismatch is a
31
+ * hard error (AGENTS.md §3, §9: no silent compatibility shims).
32
+ *
33
+ * Bump in lockstep with `ELIZA_INFERENCE_ABI_VERSION` in
34
+ * `scripts/omnivoice-fuse/ffi.h` whenever the C surface changes shape.
35
+ *
36
+ * v4: the FFI bridge resolves `speaker_preset_id` against the bundle's
37
+ * `cache/voice-preset-<id>.bin` (ELZ2 v2) and applies the
38
+ * `(instruct, ref_audio_tokens, ref_T, ref_text)` triple to
39
+ * `ov_tts_params` before calling `ov_synthesize`. Adds the
40
+ * `eliza_inference_encode_reference` entrypoint that the freeze CLI
41
+ * uses to pre-encode reference WAVs into the preset file. A v3 caller
42
+ * remains source-compatible: every v3 entry point keeps its v3 shape.
43
+ *
44
+ * v5: the FFI bridge gains the native openWakeWord surface
45
+ * (`eliza_inference_wakeword_supported/open/score/reset/close`). It
46
+ * replaces the previous `onnxruntime-node`-backed wake-word path —
47
+ * the JS binding routes wake-word detection exclusively through this
48
+ * ABI with no ONNX fallback (AGENTS.md §3, §8). v4 callers that
49
+ * never touched the wake-word entries are source-compatible.
50
+ *
51
+ * v6: the FFI bridge gains the native speaker-encoder + diarizer
52
+ * surfaces (`eliza_inference_speaker_supported/open/embed/free/close`
53
+ * and `eliza_inference_diariz_supported/open/segment/close`). These
54
+ * fuse the remaining standalone `libvoice_classifier` voice
55
+ * classifiers into the one `libelizainference` handle so the whole
56
+ * voice pipeline runs through a single native lib. v5 callers that
57
+ * never touched the speaker/diarizer entries are source-compatible.
58
+ *
59
+ * v9: the last text-adjacent modalities move onto the fused handle. Three
60
+ * additive surfaces + probes: text embeddings (`embed` / `embedSupported`),
61
+ * mmproj vision describe (`describeImage` / `visionSupported`), and the
62
+ * tokenizer (`tokenize` / `detokenize` / `tokenizeSupported`). With these,
63
+ * libllama is fully retired: text, embeddings, vision, and tokenization all
64
+ * run through the fused handle. A pre-v9 library lacks these symbols, so the
65
+ * probes report unsupported and the fused runtime refuses (there is no
66
+ * libllama fallback). v8 callers that never touched the new entries remain
67
+ * source-compatible (the new probes simply return false on a v8 lib).
68
+ *
69
+ * v10: Kokoro-82M TTS folded in-process. The fused handle gains
70
+ * `eliza_inference_kokoro_supported/load/synthesize/sample_rate` so the
71
+ * mobile Kokoro path synthesizes through the same dlopen()-ed
72
+ * libelizainference as OmniVoice instead of POSTing to the local-TCP
73
+ * `llama-server /v1/audio/speech` route (forbidden on iOS / Google Play).
74
+ * The four symbols are additive — a v9 library lacks them, so the
75
+ * `kokoroSupported()` probe reports false and the Kokoro FFI runtime
76
+ * refuses (no TCP fallback on mobile). A v9 library is still accepted at
77
+ * degraded capability: its voice/ASR/VAD/LLM/text surface is unchanged and
78
+ * Kokoro just probes unsupported on it.
79
+ */
80
+ export const ELIZA_INFERENCE_ABI_VERSION = 12 as const;
81
+
82
+ /** One transcribed word with playback-synced timing (ms from utterance start). */
83
+ export interface AsrWordTiming {
84
+ text: string;
85
+ startMs: number;
86
+ endMs: number;
87
+ }
88
+
89
+ /**
90
+ * Recover per-word `{ text, startMs, endMs }` from a v12 timed-ASR result.
91
+ *
92
+ * The native `eliza_inference_asr_transcribe_timed` sizes the `startMs`/`endMs`
93
+ * arrays by splitting the transcript on ASCII whitespace — `std::isspace` in the
94
+ * C locale matches EXACTLY ` \t\n\v\f\r`. We must mirror that split byte-for-byte
95
+ * to recover the word strings: a broader Unicode `\s` split collapses NBSP /
96
+ * ideographic space (U+00A0, U+3000, …) that the native byte split keeps, which
97
+ * would make `tokens` shorter than `count` and silently zip each word's text
98
+ * against a DIFFERENT word's timing — a desync `validateAsrWordTimings` cannot
99
+ * see (it never compares text to count). `count` only falls below the true word
100
+ * count when the caller's `maxWords` cap is hit, in which case the trailing
101
+ * (untimed) words are dropped by `Math.min`.
102
+ */
103
+ export function recoverAsrWords(
104
+ text: string,
105
+ count: number,
106
+ startMs: Int32Array,
107
+ endMs: Int32Array,
108
+ ): AsrWordTiming[] {
109
+ const tokens = text.split(/[ \t\n\v\f\r]+/).filter(Boolean);
110
+ const n = Math.min(count, tokens.length);
111
+ const words: AsrWordTiming[] = [];
112
+ for (let i = 0; i < n; i++) {
113
+ words.push({
114
+ text: tokens[i] as string,
115
+ startMs: startMs[i] ?? 0,
116
+ endMs: endMs[i] ?? 0,
117
+ });
118
+ }
119
+ return words;
120
+ }
121
+
122
+ /**
123
+ * Pooling strategies for `embed`. Mirror `enum llama_pooling_type` and the
124
+ * `ELIZA_POOLING_*` constants in `eliza-inference-ffi.h`.
125
+ */
126
+ export const ELIZA_POOLING_MEAN = 1;
127
+ export const ELIZA_POOLING_CLS = 2;
128
+ export const ELIZA_POOLING_LAST = 3;
129
+
130
+ /** Status codes mirrored from `ffi.h`. Negative = failure. */
131
+ export const ELIZA_OK = 0;
132
+ export const ELIZA_ERR_NOT_IMPLEMENTED = -1;
133
+ export const ELIZA_ERR_INVALID_ARG = -2;
134
+ export const ELIZA_ERR_BUNDLE_INVALID = -3;
135
+ export const ELIZA_ERR_FFI_FAULT = -4;
136
+ export const ELIZA_ERR_OOM = -5;
137
+ export const ELIZA_ERR_ABI_MISMATCH = -6;
138
+ export const ELIZA_ERR_CANCELLED = -7;
139
+
140
+ /**
141
+ * WeSpeaker ResNet34-LM embedding dimension. The native
142
+ * `eliza_inference_speaker_embed` writes exactly this many L2-normalized
143
+ * fp32 values into the caller-owned output buffer. Mirrors the C-side
144
+ * `VOICE_SPEAKER_EMBEDDING_DIM` and `SPEAKER_GGML_EMBEDDING_DIM`.
145
+ */
146
+ const SPEAKER_EMBEDDING_DIM = 256;
147
+
148
+ /**
149
+ * Upper bound on the per-window diarizer label count. pyannote-3 emits 293
150
+ * int8 frame labels per 5 s window; the caller passes a generous capacity and
151
+ * the library reports the real count back via `*io_n_labels`.
152
+ */
153
+ const DIARIZ_LABELS_CAPACITY = 2048;
154
+
155
+ /**
156
+ * Region names the lifecycle hands to `mmap_acquire` / `mmap_evict`.
157
+ * Mirrors the set the C stub validates in `ffi-stub.c::valid_region`.
158
+ */
159
+ export type ElizaInferenceRegion =
160
+ | "tts"
161
+ | "asr"
162
+ | "text"
163
+ | "mtp"
164
+ | "vad"
165
+ | "wakeword";
166
+
167
+ /**
168
+ * Opaque pointer to the C-side `EliInferenceContext`. Numeric on Bun
169
+ * (FFI returns the raw pointer as `bigint`); never inspected on the JS
170
+ * side beyond passing it back through the binding.
171
+ */
172
+ export type ElizaInferenceContextHandle = bigint;
173
+
174
+ /** Opaque pointer to a native Silero VAD session. */
175
+ export type NativeVadHandle = bigint;
176
+
177
+ /** Opaque pointer to a native openWakeWord session. */
178
+ export type NativeWakeWordHandle = bigint;
179
+
180
+ /** Opaque pointer to a native WeSpeaker speaker-encoder session. */
181
+ export type NativeSpeakerHandle = bigint;
182
+
183
+ /** Opaque pointer to a native pyannote diarizer session. */
184
+ export type NativeDiarizHandle = bigint;
185
+
186
+ /** Opaque pointer to a streaming-LLM session. */
187
+ export type LlmStreamHandle = bigint;
188
+
189
+ /**
190
+ * Per-session config handed to `llmStreamOpen`. Mirrors
191
+ * `eliza_llm_stream_config_t` in
192
+ * `native/llama.cpp/tools/omnivoice/include/eliza-inference-ffi.h` (ABI v8).
193
+ */
194
+ export interface LlmStreamConfig {
195
+ maxTokens: number;
196
+ temperature: number;
197
+ topP: number;
198
+ topK: number;
199
+ repeatPenalty: number;
200
+ /** Pinned slot id; -1 disables pinning. */
201
+ slotId: number;
202
+ /** Optional prompt cache key used to derive a slot when `slotId === -1`. */
203
+ promptCacheKey: string | null;
204
+ /** MTP drafter bounds; `0` for either disables speculative decoding. */
205
+ draftMin: number;
206
+ draftMax: number;
207
+ /** Absolute MTP drafter GGUF path; null disables drafter-backed MTP. */
208
+ draftModelPath: string | null;
209
+ /**
210
+ * GBNF grammar source. When set the native session installs a grammar
211
+ * sampler FIRST in the chain so every sampled token is constrained — this
212
+ * is how the structured-reply envelope is forced on the in-process FFI
213
+ * path. `null`/empty disables grammar constraint.
214
+ */
215
+ gbnfGrammar?: string | null;
216
+ /** Qwen3-style thinking-tag suppression passthrough (v1 no-op). */
217
+ disableThinking?: boolean;
218
+ /**
219
+ * Per-load GPU offload (ABI v8). Number of model layers to place on GPU.
220
+ * `undefined`/-1 selects the runtime default (all layers); 0 forces CPU.
221
+ * The model is loaded once per ctx, so the FIRST session's value wins.
222
+ */
223
+ gpuLayers?: number;
224
+ /**
225
+ * KV-cache K quant type name (ABI v8), e.g. "f16", "q8_0", "qjl1_256".
226
+ * `undefined`/null leaves the f16 default. Mapped to `ggml_type` by the
227
+ * fused lib's `eliza_llm_stream_config_t.cache_type_k`.
228
+ */
229
+ cacheTypeK?: string | null;
230
+ /** KV-cache V quant type name (ABI v8); see `cacheTypeK`. */
231
+ cacheTypeV?: string | null;
232
+ }
233
+
234
+ /**
235
+ * One step of streaming LLM output. `tokens` is the batch of accepted text
236
+ * model token ids the runtime committed this step (>= 1; > 1 only when the
237
+ * MTP drafter is active and the verifier accepted multiple drafts).
238
+ * `text` is the detokenized text for those tokens. `done` is `true` only
239
+ * on the final step (EOS reached). `drafterDrafted` and `drafterAccepted`
240
+ * are populated when the drafter is active.
241
+ */
242
+ export interface LlmStreamStep {
243
+ tokens: number[];
244
+ text: string;
245
+ done: boolean;
246
+ drafterDrafted: number;
247
+ drafterAccepted: number;
248
+ }
249
+
250
+ /**
251
+ * One streaming-TTS chunk delivered to the `onChunk` callback passed to
252
+ * `ttsSynthesizeStream`. `pcm` is a *view* over the library's buffer —
253
+ * valid only for the duration of the callback; copy it before
254
+ * returning. `isFinal` marks the zero-length tail chunk that closes the
255
+ * utterance. The callback returning `true` requests cancellation at the
256
+ * next kernel boundary.
257
+ */
258
+ export interface TtsStreamChunk {
259
+ pcm: Float32Array;
260
+ isFinal: boolean;
261
+ }
262
+
263
+ /**
264
+ * A native MTP speculative-step event from
265
+ * `eliza_inference_set_verifier_callback`. Token-index domain is the
266
+ * generated-output stream (token 0 = first generated token), matching
267
+ * `RejectedTokenRange`. `rejectedFrom`/`rejectedTo` are -1 when nothing
268
+ * was rejected this step.
269
+ */
270
+ export interface NativeVerifierEvent {
271
+ acceptedTokenIds: number[];
272
+ rejectedFrom: number;
273
+ rejectedTo: number;
274
+ correctedTokenIds: number[];
275
+ }
276
+
277
+ /**
278
+ * Typed handle returned by `loadElizaInferenceFfi`. Each method maps
279
+ * 1:1 to a symbol declared in `ffi.h`. Methods that allocate a context
280
+ * return the opaque pointer; methods that consume one take it as the
281
+ * first argument. Failures throw `VoiceLifecycleError` with the
282
+ * structured code derived from the C return value.
283
+ */
284
+ export interface ElizaInferenceFfi {
285
+ /** Library path the binding was loaded from (for diagnostics). */
286
+ readonly libraryPath: string;
287
+ /** ABI version reported by the loaded library. */
288
+ readonly libraryAbiVersion: string;
289
+ /** Create a fresh context anchored at `bundleDir`. */
290
+ create(bundleDir: string): ElizaInferenceContextHandle;
291
+ /** Destroy a previously-created context. Idempotent on already-freed handles. */
292
+ destroy(ctx: ElizaInferenceContextHandle): void;
293
+ /** Map / re-page weights for a region. */
294
+ mmapAcquire(
295
+ ctx: ElizaInferenceContextHandle,
296
+ region: ElizaInferenceRegion,
297
+ ): void;
298
+ /**
299
+ * Release or evict a voice-only region after the lifecycle leaves
300
+ * voice-on. Implementations may madvise mapped pages or unload the
301
+ * ASR/TTS runtime state entirely; callers must treat the region as
302
+ * unavailable until the next `mmapAcquire`.
303
+ */
304
+ mmapEvict(
305
+ ctx: ElizaInferenceContextHandle,
306
+ region: ElizaInferenceRegion,
307
+ ): void;
308
+ /**
309
+ * Synchronous TTS forward. Caller provides the output buffer; library
310
+ * fills up to its capacity and returns the number of samples written.
311
+ */
312
+ ttsSynthesize(args: {
313
+ ctx: ElizaInferenceContextHandle;
314
+ text: string;
315
+ speakerPresetId: string | null;
316
+ out: Float32Array;
317
+ }): number;
318
+ /**
319
+ * Synchronous ASR forward. Returns the decoded transcript as a UTF-8
320
+ * string (allocated by the JS side, sized to fit the library's max
321
+ * write).
322
+ */
323
+ asrTranscribe(args: {
324
+ ctx: ElizaInferenceContextHandle;
325
+ pcm: Float32Array;
326
+ sampleRateHz: number;
327
+ maxTextBytes?: number;
328
+ }): string;
329
+
330
+ /* ---- ASR word timestamps (ABI v12) --------------------------- */
331
+
332
+ /** True when this build can emit per-word ASR timestamps (v12+). v11 and
333
+ * older report false — callers fall back to the text-only `asrTranscribe`. */
334
+ timedAsrSupported(): boolean;
335
+ /** Transcribe like `asrTranscribe` AND return per-word `[startMs,endMs)`
336
+ * spans (duration-proportional, char-weighted, monotonic — the honest
337
+ * single-model signal; see the v12 ABI changelog). The word texts come from
338
+ * a whitespace split of the transcript, zipped with the native timing. */
339
+ asrTranscribeTimed(args: {
340
+ ctx: ElizaInferenceContextHandle;
341
+ pcm: Float32Array;
342
+ sampleRateHz: number;
343
+ maxTextBytes?: number;
344
+ maxWords?: number;
345
+ }): { text: string; words: AsrWordTiming[] };
346
+
347
+ /* ---- Streaming TTS + verifier callback (ABI v2) --------------- */
348
+
349
+ /**
350
+ * True when this build implements streaming TTS (false for the stub /
351
+ * a TTS-disabled build). Callers pick the streaming path vs the batch
352
+ * `ttsSynthesize` off this flag — no probe-and-catch.
353
+ */
354
+ ttsStreamSupported(): boolean;
355
+ /**
356
+ * Chunked synthesis. `onChunk` is invoked for each decoded PCM segment
357
+ * as it arrives, then once more with `isFinal: true` (zero-length
358
+ * tail). Returning `true` from `onChunk` requests cancellation; the
359
+ * call then resolves with `cancelled: true` after the final-chunk
360
+ * callback. Any negative library return is a thrown `VoiceLifecycleError`.
361
+ */
362
+ ttsSynthesizeStream(args: {
363
+ ctx: ElizaInferenceContextHandle;
364
+ text: string;
365
+ speakerPresetId: string | null;
366
+ onChunk: (chunk: TtsStreamChunk) => boolean | undefined;
367
+ }): { cancelled: boolean };
368
+ /**
369
+ * Hard-cancel any in-flight TTS forward pass on `ctx` (started on
370
+ * another thread by `ttsSynthesize` / `ttsSynthesizeStream`). The
371
+ * in-flight call returns `ELIZA_ERR_CANCELLED` at the next kernel
372
+ * boundary. Cancelling nothing is not an error.
373
+ */
374
+ cancelTts(ctx: ElizaInferenceContextHandle): void;
375
+ /**
376
+ * Register (or, with `cb: null`, clear) the native MTP verifier
377
+ * callback. The runtime fires `cb` for every speculative accept/reject
378
+ * step from the in-process drafter↔target loop. The returned
379
+ * `JSCallbackHandle` MUST be kept alive for as long as the callback is
380
+ * registered and `.close()`d when it's cleared (or on dispose) — Bun's
381
+ * `JSCallback` is GC'd otherwise and the native side dereferences a
382
+ * dead pointer.
383
+ */
384
+ setVerifierCallback(
385
+ ctx: ElizaInferenceContextHandle,
386
+ cb: ((event: NativeVerifierEvent) => void) | null,
387
+ ): { close(): void };
388
+
389
+ /* ---- OmniVoice reference encode (ABI v4) ---------------------- */
390
+
391
+ /**
392
+ * True when this build exports the OmniVoice reference-encode symbols
393
+ * (`eliza_inference_encode_reference`). The freeze CLI uses this to
394
+ * pre-encode same reference audio into the persisted voice preset;
395
+ * the runtime synthesis path never calls it (it reads pre-encoded
396
+ * tokens from the preset file).
397
+ */
398
+ encodeReferenceSupported?(): boolean;
399
+ /**
400
+ * Run the encode-only half of the TTS pipeline (HuBERT semantic + RVQ
401
+ * codec) on a 24 kHz mono fp32 PCM buffer and return the resulting
402
+ * reference-audio-token tensor `[K=8, ref_T]` as `Int32Array`
403
+ * row-major (`tokens[k*ref_T + t]`). The library allocates and the
404
+ * binding takes care of freeing the native buffer via
405
+ * `eliza_inference_free_tokens` before this returns.
406
+ *
407
+ * The TTS region must have been acquired (`mmapAcquire("tts")`)
408
+ * before the call. `sampleRateHz` must be 24000; the entrypoint does
409
+ * NOT resample, by design — the freeze artifact must be deterministic.
410
+ */
411
+ encodeReference?(args: {
412
+ ctx: ElizaInferenceContextHandle;
413
+ pcm: Float32Array;
414
+ sampleRateHz: number;
415
+ }): { K: number; refT: number; tokens: Int32Array };
416
+
417
+ /* ---- Native VAD (ABI v3) -------------------------------------- */
418
+
419
+ /** True when this build exports and enables the native Silero VAD backend. */
420
+ vadSupported?(): boolean;
421
+ /** Open a native VAD session. The ABI-compatible sample rate is 16 kHz. */
422
+ vadOpen?(args: {
423
+ ctx: ElizaInferenceContextHandle;
424
+ sampleRateHz: number;
425
+ }): NativeVadHandle;
426
+ /** Process one 512-sample fp32 mono window and return P(speech). */
427
+ vadProcess?(args: { vad: NativeVadHandle; pcm: Float32Array }): number;
428
+ /** Clear native VAD recurrent state at utterance boundaries. */
429
+ vadReset?(vad: NativeVadHandle): void;
430
+ /** Close + free a native VAD session. Idempotent on already-closed handles. */
431
+ vadClose?(vad: NativeVadHandle): void;
432
+
433
+ /* ---- Native wake-word (ABI v5) -------------------------------- */
434
+
435
+ /**
436
+ * True when this build exports and enables the native openWakeWord
437
+ * backend. The JS binding routes wake-word detection exclusively
438
+ * through this surface; when this returns false, the wake-word path
439
+ * throws a structured "runtime not ready" error — no ONNX fallback
440
+ * (AGENTS.md §3, §8).
441
+ */
442
+ wakewordSupported?(): boolean;
443
+ /**
444
+ * Open a native wake-word session. `sampleRateHz` must be 16000;
445
+ * `headName` selects the classifier head inside the bundle's combined
446
+ * wake-word GGUF (e.g. "hey-eliza").
447
+ */
448
+ wakewordOpen?(args: {
449
+ ctx: ElizaInferenceContextHandle;
450
+ sampleRateHz: number;
451
+ headName: string;
452
+ }): NativeWakeWordHandle;
453
+ /**
454
+ * Score one 1280-sample (80 ms @ 16 kHz) fp32 mono frame and return
455
+ * the latest P(wake) in [0, 1]. Early calls before enough context
456
+ * accumulates return 0.
457
+ */
458
+ wakewordScore?(args: {
459
+ wake: NativeWakeWordHandle;
460
+ pcm: Float32Array;
461
+ }): number;
462
+ /** Clear all streaming state (audio tail, mel ring, embedding ring). */
463
+ wakewordReset?(wake: NativeWakeWordHandle): void;
464
+ /** Close + free a native wake-word session. Idempotent on already-closed handles. */
465
+ wakewordClose?(wake: NativeWakeWordHandle): void;
466
+
467
+ /* ---- Native speaker encoder (ABI v6) -------------------------- */
468
+
469
+ /** True when this build exports and enables the native WeSpeaker encoder. */
470
+ speakerSupported?(): boolean;
471
+ /**
472
+ * Open a native speaker-encoder session. `ggufPath` may be null to
473
+ * resolve the bundle's `speaker/` dir, or an absolute path to a
474
+ * WeSpeaker GGUF.
475
+ */
476
+ speakerOpen?(args: {
477
+ ctx: ElizaInferenceContextHandle;
478
+ ggufPath: string | null;
479
+ }): NativeSpeakerHandle;
480
+ /**
481
+ * Embed `pcm` (16 kHz mono fp32) into a 256-d L2-normalized speaker
482
+ * embedding. Returns a freshly-allocated `Float32Array` of length 256.
483
+ */
484
+ speakerEmbed?(args: {
485
+ speaker: NativeSpeakerHandle;
486
+ pcm: Float32Array;
487
+ }): Float32Array;
488
+ /** Close + free a native speaker-encoder session. Idempotent on already-closed handles. */
489
+ speakerClose?(speaker: NativeSpeakerHandle): void;
490
+
491
+ /* ---- Native diarizer (ABI v6) --------------------------------- */
492
+
493
+ /** True when this build exports and enables the native pyannote diarizer. */
494
+ diarizSupported?(): boolean;
495
+ /**
496
+ * Open a native diarizer session. `ggufPath` may be null to resolve the
497
+ * bundle's `diariz/` dir, or an absolute path to a pyannote GGUF.
498
+ */
499
+ diarizOpen?(args: {
500
+ ctx: ElizaInferenceContextHandle;
501
+ ggufPath: string | null;
502
+ }): NativeDiarizHandle;
503
+ /**
504
+ * Segment one 80000-sample (5 s @ 16 kHz) mono fp32 window into a
505
+ * per-frame powerset-label sequence. Returns the `Int8Array` of frame
506
+ * labels (293 for pyannote-segmentation-3.0), each in `[0, 7)`.
507
+ */
508
+ diarizSegment?(args: {
509
+ diariz: NativeDiarizHandle;
510
+ pcm: Float32Array;
511
+ }): Int8Array;
512
+ /** Close + free a native diarizer session. Idempotent on already-closed handles. */
513
+ diarizClose?(diariz: NativeDiarizHandle): void;
514
+
515
+ /* ---- Streaming ASR (ABI v2) ----------------------------------- */
516
+
517
+ /**
518
+ * True when this build has a working streaming ASR decoder (false for
519
+ * the stub / an ASR-disabled build). Callers pick the fused streaming
520
+ * path vs the fused batch interim adapter off this flag — they do not
521
+ * have to open a session and catch `ELIZA_ERR_NOT_IMPLEMENTED`.
522
+ */
523
+ asrStreamSupported(): boolean;
524
+ /** Open a streaming ASR session. The handle is closed via `asrStreamClose`. */
525
+ asrStreamOpen(args: {
526
+ ctx: ElizaInferenceContextHandle;
527
+ sampleRateHz: number;
528
+ }): bigint;
529
+ /** Feed one PCM frame at the session's sample rate. */
530
+ asrStreamFeed(args: { stream: bigint; pcm: Float32Array }): void;
531
+ /** Read the current running partial transcript (and token ids when available). */
532
+ asrStreamPartial(args: {
533
+ stream: bigint;
534
+ maxTextBytes?: number;
535
+ maxTokens?: number;
536
+ }): { partial: string; tokens?: number[] };
537
+ /** Force-finalize: drain buffered audio, run a final decode, return the final transcript. */
538
+ asrStreamFinish(args: {
539
+ stream: bigint;
540
+ maxTextBytes?: number;
541
+ maxTokens?: number;
542
+ }): { partial: string; tokens?: number[] };
543
+ /** Close + free a streaming ASR session. Idempotent on already-closed handles. */
544
+ asrStreamClose(stream: bigint): void;
545
+
546
+ /* ---- Streaming LLM (additive on top of ABI v3) ---------------- */
547
+
548
+ /**
549
+ * True when this build exports the streaming LLM symbols
550
+ * (`eliza_inference_llm_stream_*`). Transitional builds may load
551
+ * without them; the runner uses this to pick between the FFI streaming
552
+ * path.
553
+ */
554
+ llmStreamSupported?(): boolean;
555
+ /**
556
+ * True when this build wires same-file / separate-drafter MTP
557
+ * speculative decoding into the streaming-LLM text path (ABI v8). A v7
558
+ * library returns `false` here (the symbol is absent), so the fused TEXT
559
+ * path can refuse to route through it without a speculative-decode
560
+ * regression. Anti-regression guard — see ABI v8 changelog.
561
+ */
562
+ llmMtpSupported?(): boolean;
563
+ /**
564
+ * True when this build maps + applies KV-cache quant types in the
565
+ * streaming-LLM text path (ABI v8). A v7 library returns `false` (symbol
566
+ * absent); the fused TEXT path refuses it to avoid a silent fallback to
567
+ * f16 KV when a quantized cache was requested.
568
+ */
569
+ llmKvQuantSupported?(): boolean;
570
+ /**
571
+ * Open a streaming-LLM session against `ctx`. Failure throws
572
+ * `VoiceLifecycleError`. Close exactly once via `llmStreamClose`.
573
+ */
574
+ llmStreamOpen?(args: {
575
+ ctx: ElizaInferenceContextHandle;
576
+ config: LlmStreamConfig;
577
+ }): LlmStreamHandle;
578
+ /** Feed a batch of pre-tokenized prompt tokens before the first `next`. */
579
+ llmStreamPrefill?(args: {
580
+ stream: LlmStreamHandle;
581
+ tokens: Int32Array;
582
+ }): void;
583
+ /**
584
+ * Pull the next streaming step. Returns `null` when the runtime declined
585
+ * to emit tokens this call (rare — drafter rejected everything and the
586
+ * verifier had nothing to commit); poll again. `step.done === true` is
587
+ * the final step.
588
+ */
589
+ llmStreamNext?(args: {
590
+ stream: LlmStreamHandle;
591
+ maxTokensPerStep?: number;
592
+ maxTextBytes?: number;
593
+ }): LlmStreamStep;
594
+ /** Cancel in-flight generation; the next `_next` returns CANCELLED. */
595
+ llmStreamCancel?(stream: LlmStreamHandle): void;
596
+ /** Persist the session's slot KV state to disk. */
597
+ llmStreamSaveSlot?(args: { stream: LlmStreamHandle; filename: string }): void;
598
+ /** Restore a previously-saved slot KV file. Call before the first prefill/next. */
599
+ llmStreamRestoreSlot?(args: {
600
+ stream: LlmStreamHandle;
601
+ filename: string;
602
+ }): void;
603
+ /** Close + free a streaming-LLM session. Idempotent on already-closed handles. */
604
+ llmStreamClose?(stream: LlmStreamHandle): void;
605
+
606
+ /* ---- Text embeddings (ABI v9) -------------------------------- */
607
+
608
+ /**
609
+ * True when this build wires the fused text-embedding path
610
+ * (`eliza_inference_embed`). A v8 library returns false (symbol absent),
611
+ * so the default TEXT_EMBEDDING handler keeps the node-llama-cpp /
612
+ * libllama path.
613
+ */
614
+ embedSupported?(): boolean;
615
+ /**
616
+ * Compute a pooled, L2-normalized sentence embedding for `text` over the
617
+ * bundle's text model. `pooling` selects the strategy (default MEAN — the
618
+ * gte-small convention). Returns a `Float32Array` of length `n_embd`.
619
+ */
620
+ embed?(args: {
621
+ ctx: ElizaInferenceContextHandle;
622
+ text: string;
623
+ pooling?: number;
624
+ }): Float32Array;
625
+
626
+ /* ---- mmproj vision describe (ABI v9) ------------------------- */
627
+
628
+ /**
629
+ * True when this build was compiled with vision (`-DELIZA_ENABLE_VISION`)
630
+ * and exports `eliza_inference_describe_image`. A v8 / vision-off library
631
+ * returns false, so the IMAGE_DESCRIPTION handler keeps the libllama mtmd
632
+ * path.
633
+ */
634
+ visionSupported?(): boolean;
635
+ /**
636
+ * Describe `imageBytes` (raw PNG/JPEG/WebP) through the text model's
637
+ * mmproj projector at `mmprojPath`. `prompt` defaults to a generic
638
+ * caption request. Returns the description text.
639
+ */
640
+ describeImage?(args: {
641
+ ctx: ElizaInferenceContextHandle;
642
+ imageBytes: Uint8Array;
643
+ mmprojPath: string;
644
+ prompt?: string;
645
+ maxTextBytes?: number;
646
+ }): string;
647
+
648
+ /* ---- Tokenizer (ABI v9) -------------------------------------- */
649
+
650
+ /**
651
+ * True when this build exposes the tokenizer over the loaded text vocab
652
+ * (`eliza_inference_tokenize`). A pre-v9 library returns false, so the
653
+ * desktop fused runtime refuses (libllama is retired — no tokenizer sidecar).
654
+ */
655
+ tokenizeSupported?(): boolean;
656
+ /**
657
+ * Tokenize `text` against the loaded text model's vocab. `addSpecial`
658
+ * (default true) adds BOS/EOS; `parseSpecial` (default false) renders
659
+ * special tokens from the input. Returns the token ids as an `Int32Array`.
660
+ */
661
+ tokenize?(args: {
662
+ ctx: ElizaInferenceContextHandle;
663
+ text: string;
664
+ addSpecial?: boolean;
665
+ parseSpecial?: boolean;
666
+ }): Int32Array;
667
+ /**
668
+ * Detokenize `tokens` back to text against the loaded text model's vocab.
669
+ * `removeSpecial` (default false) strips BOS/EOS; `unparseSpecial`
670
+ * (default false) renders special tokens.
671
+ */
672
+ detokenize?(args: {
673
+ ctx: ElizaInferenceContextHandle;
674
+ tokens: Int32Array;
675
+ removeSpecial?: boolean;
676
+ unparseSpecial?: boolean;
677
+ maxTextBytes?: number;
678
+ }): string;
679
+
680
+ /* ---- End-of-turn scoring (ABI v11) -------------------------- */
681
+
682
+ /**
683
+ * True when this build wires the fused end-of-turn scorer
684
+ * (`eliza_inference_llm_eot_score`). A v10 library returns false (symbol
685
+ * absent), so the composite EOT classifier uses the heuristic-only signal.
686
+ */
687
+ eotSupported?(): boolean;
688
+ /**
689
+ * Single causal forward pass over `tokens` (a tokenized partial transcript)
690
+ * returning the next-token softmax probability of `targetTokenId` (the
691
+ * end-of-turn marker, e.g. `<|im_end|>`), plus the argmax next token and its
692
+ * probability. Runs on a dedicated scoring context over the loaded text
693
+ * model; KV is cleared per call so scores are independent.
694
+ */
695
+ eotScore?(args: {
696
+ ctx: ElizaInferenceContextHandle;
697
+ tokens: Int32Array;
698
+ targetTokenId: number;
699
+ }): { targetProb: number; topToken: number; topProb: number };
700
+
701
+ /* ---- Kokoro TTS (ABI v10) ----------------------------------- */
702
+
703
+ /**
704
+ * True when this build linked Eliza-1's in-process Kokoro engine
705
+ * (`eliza_inference_kokoro_*`). A v9 library returns false (symbols
706
+ * absent), so the Kokoro FFI runtime refuses rather than falling back to
707
+ * the local-TCP `llama-server` route (forbidden on iOS / Google Play).
708
+ */
709
+ kokoroSupported?(): boolean;
710
+ /**
711
+ * Load the Kokoro GGUF at `ggufPath` and the voice preset `.bin` at
712
+ * `voiceBinPath` (raw fp32 ref_s, `styleDim` inner dim — 256 for v1.0)
713
+ * into `ctx`. Replaces any previously-loaded Kokoro model on the ctx.
714
+ * Throws `VoiceLifecycleError` on a negative return with the C diagnostic.
715
+ */
716
+ kokoroLoad?(args: {
717
+ ctx: ElizaInferenceContextHandle;
718
+ ggufPath: string;
719
+ voiceBinPath: string;
720
+ styleDim?: number;
721
+ }): void;
722
+ /**
723
+ * Synthesize `text` through the loaded Kokoro model+voice at the model's
724
+ * native rate (24 kHz for v1.0). `speed` scales predicted durations
725
+ * (default 1.0). Allocates an output buffer of `maxSamples` fp32 samples,
726
+ * reads back the count the library wrote, and returns that slice.
727
+ */
728
+ kokoroSynthesize?(args: {
729
+ ctx: ElizaInferenceContextHandle;
730
+ text: string;
731
+ speed?: number;
732
+ maxSamples: number;
733
+ }): Float32Array;
734
+ /** The loaded Kokoro model's audio sample rate (24000 for v1.0). */
735
+ kokoroSampleRate?(ctx: ElizaInferenceContextHandle): number;
736
+
737
+ /** Best-effort dispose for the binding itself (closes the dlopen handle). */
738
+ close(): void;
739
+ }
740
+
741
+ /* ---------------------------------------------------------------- */
742
+ /* Loader */
743
+ /* ---------------------------------------------------------------- */
744
+
745
+ /** Runtime detector: returns true when running under Bun. */
746
+ function isBunRuntime(): boolean {
747
+ return typeof (globalThis as { Bun?: unknown }).Bun !== "undefined";
748
+ }
749
+
750
+ /**
751
+ * Load `libelizainference` at `dylibPath` and bind every symbol
752
+ * declared in `ffi.h`. The returned handle's methods delegate directly
753
+ * to the library; they throw `VoiceLifecycleError` on any negative
754
+ * return value or runtime fault.
755
+ *
756
+ * Throws synchronously (no Promise) when:
757
+ * - the JS runtime is not Bun (no FFI primitive available),
758
+ * - `dlopen` cannot find or open the library,
759
+ * - the library's reported ABI version does not match
760
+ * `ELIZA_INFERENCE_ABI_VERSION`.
761
+ */
762
+ export function loadElizaInferenceFfi(dylibPath: string): ElizaInferenceFfi {
763
+ if (!isBunRuntime()) {
764
+ throw new VoiceLifecycleError(
765
+ "kernel-missing",
766
+ `[ffi-bindings] Cannot load libelizainference: current runtime is not Bun. ` +
767
+ `The fused omnivoice FFI uses bun:ffi (production runs under Bun via Electrobun + Capacitor). ` +
768
+ `process.versions=${JSON.stringify(process.versions)}`,
769
+ );
770
+ }
771
+ if (!dylibPath || dylibPath.length === 0) {
772
+ throw new VoiceLifecycleError(
773
+ "kernel-missing",
774
+ "[ffi-bindings] loadElizaInferenceFfi: dylibPath is required",
775
+ );
776
+ }
777
+ return bindWithBunFfi(dylibPath);
778
+ }
779
+
780
+ /* ---------------------------------------------------------------- */
781
+ /* Bun:ffi binding */
782
+ /* ---------------------------------------------------------------- */
783
+
784
+ interface BunFfiSymbols {
785
+ eliza_inference_abi_version: () => unknown;
786
+ eliza_inference_create: (bundleDir: unknown, outErr: unknown) => unknown;
787
+ eliza_inference_destroy: (ctx: bigint) => void;
788
+ eliza_inference_mmap_acquire: (
789
+ ctx: bigint,
790
+ region: unknown,
791
+ outErr: unknown,
792
+ ) => number;
793
+ eliza_inference_mmap_evict: (
794
+ ctx: bigint,
795
+ region: unknown,
796
+ outErr: unknown,
797
+ ) => number;
798
+ eliza_inference_tts_synthesize: (
799
+ ctx: bigint,
800
+ text: unknown,
801
+ textLen: bigint | number,
802
+ speaker: unknown,
803
+ outPcm: unknown,
804
+ maxSamples: bigint | number,
805
+ outErr: unknown,
806
+ ) => number;
807
+ eliza_inference_asr_transcribe: (
808
+ ctx: bigint,
809
+ pcm: unknown,
810
+ nSamples: bigint | number,
811
+ sampleRateHz: number,
812
+ outText: unknown,
813
+ maxTextBytes: bigint | number,
814
+ outErr: unknown,
815
+ ) => number;
816
+ eliza_inference_asr_timestamps_supported?: () => number;
817
+ eliza_inference_asr_transcribe_timed?: (
818
+ ctx: bigint,
819
+ pcm: unknown,
820
+ nSamples: bigint | number,
821
+ sampleRateHz: number,
822
+ outText: unknown,
823
+ maxTextBytes: bigint | number,
824
+ outWordStartMs: unknown,
825
+ outWordEndMs: unknown,
826
+ ioNWords: unknown,
827
+ outErr: unknown,
828
+ ) => number;
829
+ eliza_inference_tts_stream_supported: () => number;
830
+ eliza_inference_tts_synthesize_stream: (
831
+ ctx: bigint,
832
+ text: unknown,
833
+ textLen: bigint | number,
834
+ speaker: unknown,
835
+ onChunk: unknown,
836
+ userData: bigint | number,
837
+ outErr: unknown,
838
+ ) => number;
839
+ eliza_inference_cancel_tts: (ctx: bigint, outErr: unknown) => number;
840
+ eliza_inference_set_verifier_callback: (
841
+ ctx: bigint,
842
+ cb: unknown,
843
+ userData: bigint | number,
844
+ outErr: unknown,
845
+ ) => number;
846
+ eliza_inference_encode_reference?: (
847
+ ctx: bigint,
848
+ pcm: unknown,
849
+ nSamples: bigint | number,
850
+ sampleRateHz: number,
851
+ outK: unknown,
852
+ outRefT: unknown,
853
+ outTokens: unknown,
854
+ outErr: unknown,
855
+ ) => number;
856
+ eliza_inference_free_tokens?: (tokens: bigint | number) => void;
857
+ eliza_inference_vad_supported?: () => number;
858
+ eliza_inference_vad_open?: (
859
+ ctx: bigint,
860
+ sampleRateHz: number,
861
+ outErr: unknown,
862
+ ) => unknown;
863
+ eliza_inference_vad_process?: (
864
+ vad: bigint,
865
+ pcm: unknown,
866
+ nSamples: bigint | number,
867
+ outProbability: unknown,
868
+ outErr: unknown,
869
+ ) => number;
870
+ eliza_inference_vad_reset?: (vad: bigint, outErr: unknown) => number;
871
+ eliza_inference_vad_close?: (vad: bigint) => void;
872
+ eliza_inference_wakeword_supported?: () => number;
873
+ eliza_inference_wakeword_open?: (
874
+ ctx: bigint,
875
+ sampleRateHz: number,
876
+ headName: unknown,
877
+ outErr: unknown,
878
+ ) => unknown;
879
+ eliza_inference_wakeword_score?: (
880
+ wake: bigint,
881
+ pcm: unknown,
882
+ nSamples: bigint | number,
883
+ outProbability: unknown,
884
+ outErr: unknown,
885
+ ) => number;
886
+ eliza_inference_wakeword_reset?: (wake: bigint, outErr: unknown) => number;
887
+ eliza_inference_wakeword_close?: (wake: bigint) => void;
888
+ eliza_inference_speaker_supported?: () => number;
889
+ eliza_inference_speaker_open?: (
890
+ ctx: bigint,
891
+ ggufPath: unknown,
892
+ outErr: unknown,
893
+ ) => unknown;
894
+ eliza_inference_speaker_embed?: (
895
+ speaker: bigint,
896
+ pcm: unknown,
897
+ nSamples: bigint | number,
898
+ outEmbedding: unknown,
899
+ outErr: unknown,
900
+ ) => number;
901
+ eliza_inference_speaker_close?: (speaker: bigint) => void;
902
+ eliza_inference_diariz_supported?: () => number;
903
+ eliza_inference_diariz_open?: (
904
+ ctx: bigint,
905
+ ggufPath: unknown,
906
+ outErr: unknown,
907
+ ) => unknown;
908
+ eliza_inference_diariz_segment?: (
909
+ diariz: bigint,
910
+ pcm: unknown,
911
+ nSamples: bigint | number,
912
+ outLabels: unknown,
913
+ ioNLabels: unknown,
914
+ outErr: unknown,
915
+ ) => number;
916
+ eliza_inference_diariz_close?: (diariz: bigint) => void;
917
+ eliza_inference_asr_stream_supported: () => number;
918
+ eliza_inference_asr_stream_open: (
919
+ ctx: bigint,
920
+ sampleRateHz: number,
921
+ outErr: unknown,
922
+ ) => unknown;
923
+ eliza_inference_asr_stream_feed: (
924
+ stream: bigint,
925
+ pcm: unknown,
926
+ nSamples: bigint | number,
927
+ outErr: unknown,
928
+ ) => number;
929
+ eliza_inference_asr_stream_partial: (
930
+ stream: bigint,
931
+ outText: unknown,
932
+ maxTextBytes: bigint | number,
933
+ outTokens: unknown,
934
+ ioNTokens: unknown,
935
+ outErr: unknown,
936
+ ) => number;
937
+ eliza_inference_asr_stream_finish: (
938
+ stream: bigint,
939
+ outText: unknown,
940
+ maxTextBytes: bigint | number,
941
+ outTokens: unknown,
942
+ ioNTokens: unknown,
943
+ outErr: unknown,
944
+ ) => number;
945
+ eliza_inference_asr_stream_close: (stream: bigint) => void;
946
+ eliza_inference_free_string: (str: bigint | number) => void;
947
+ // Streaming LLM (additive). Optional — transitional builds may omit.
948
+ // ABI v8 capability probes — absent on v7 (treated as unsupported).
949
+ eliza_inference_llm_mtp_supported?: () => number;
950
+ eliza_inference_llm_kv_quant_supported?: () => number;
951
+ eliza_inference_llm_stream_open?: (
952
+ ctx: bigint,
953
+ cfg: unknown,
954
+ outErr: unknown,
955
+ ) => unknown;
956
+ eliza_inference_llm_stream_prefill?: (
957
+ stream: bigint,
958
+ tokens: unknown,
959
+ nTokens: bigint | number,
960
+ outErr: unknown,
961
+ ) => number;
962
+ eliza_inference_llm_stream_next?: (
963
+ stream: bigint,
964
+ tokensOut: unknown,
965
+ tokensCapacity: bigint | number,
966
+ numTokensOut: unknown,
967
+ textOut: unknown,
968
+ textCapacity: bigint | number,
969
+ drafterDraftedOut: unknown,
970
+ drafterAcceptedOut: unknown,
971
+ outErr: unknown,
972
+ ) => number;
973
+ eliza_inference_llm_stream_cancel?: (stream: bigint) => number;
974
+ eliza_inference_llm_stream_save_slot?: (
975
+ stream: bigint,
976
+ filename: unknown,
977
+ outErr: unknown,
978
+ ) => number;
979
+ eliza_inference_llm_stream_restore_slot?: (
980
+ stream: bigint,
981
+ filename: unknown,
982
+ outErr: unknown,
983
+ ) => number;
984
+ eliza_inference_llm_stream_close?: (stream: bigint) => void;
985
+ // Text embeddings (ABI v9). Optional — absent on v8 builds.
986
+ eliza_inference_embed_supported?: () => number;
987
+ eliza_inference_embed?: (
988
+ ctx: bigint,
989
+ text: unknown,
990
+ textLen: bigint | number,
991
+ pooling: number,
992
+ outEmbedding: unknown,
993
+ outCapacity: bigint | number,
994
+ outDim: unknown,
995
+ outErr: unknown,
996
+ ) => number;
997
+ // mmproj vision describe (ABI v9). Optional — absent on v8 / vision-off builds.
998
+ eliza_inference_vision_supported?: () => number;
999
+ eliza_inference_describe_image?: (
1000
+ ctx: bigint,
1001
+ imageBytes: unknown,
1002
+ nBytes: bigint | number,
1003
+ mmprojPath: unknown,
1004
+ prompt: unknown,
1005
+ outText: unknown,
1006
+ maxTextBytes: bigint | number,
1007
+ outErr: unknown,
1008
+ ) => number;
1009
+ // Tokenizer (ABI v9). Optional — absent on v8 builds.
1010
+ eliza_inference_tokenize_supported?: () => number;
1011
+ eliza_inference_tokenize?: (
1012
+ ctx: bigint,
1013
+ text: unknown,
1014
+ textLen: bigint | number,
1015
+ addSpecial: number,
1016
+ parseSpecial: number,
1017
+ outTokens: unknown,
1018
+ outN: unknown,
1019
+ outErr: unknown,
1020
+ ) => number;
1021
+ eliza_inference_detokenize?: (
1022
+ ctx: bigint,
1023
+ tokens: unknown,
1024
+ nTokens: bigint | number,
1025
+ removeSpecial: number,
1026
+ unparseSpecial: number,
1027
+ outText: unknown,
1028
+ maxTextBytes: bigint | number,
1029
+ outErr: unknown,
1030
+ ) => number;
1031
+ // End-of-turn scoring (ABI v11). Optional — absent on v10 builds (the probe
1032
+ // then reports unsupported and the composite EOT classifier uses the
1033
+ // heuristic-only signal).
1034
+ eliza_inference_llm_eot_supported?: () => number;
1035
+ eliza_inference_llm_eot_score?: (
1036
+ ctx: bigint,
1037
+ tokenIds: unknown,
1038
+ numTokens: bigint | number,
1039
+ targetTokenId: number,
1040
+ outTargetProb: unknown,
1041
+ outTopToken: unknown,
1042
+ outTopProb: unknown,
1043
+ outErr: unknown,
1044
+ ) => number;
1045
+ // Kokoro TTS (ABI v10). Optional — absent on v9 builds (the probe then
1046
+ // reports unsupported and the Kokoro FFI runtime refuses).
1047
+ eliza_inference_kokoro_supported?: () => number;
1048
+ eliza_inference_kokoro_load?: (
1049
+ ctx: bigint,
1050
+ ggufPath: unknown,
1051
+ voiceBinPath: unknown,
1052
+ styleDim: number,
1053
+ outErr: unknown,
1054
+ ) => number;
1055
+ eliza_inference_kokoro_synthesize?: (
1056
+ ctx: bigint,
1057
+ text: unknown,
1058
+ textLen: bigint | number,
1059
+ speed: number,
1060
+ outPcm: unknown,
1061
+ maxSamples: bigint | number,
1062
+ outErr: unknown,
1063
+ ) => number;
1064
+ eliza_inference_kokoro_sample_rate?: (ctx: bigint) => number;
1065
+ }
1066
+
1067
+ interface BunFfiLib {
1068
+ symbols: BunFfiSymbols;
1069
+ close(): void;
1070
+ }
1071
+
1072
+ interface BunFfiJSCallback {
1073
+ readonly ptr: bigint | number;
1074
+ close(): void;
1075
+ }
1076
+
1077
+ interface BunFfiModule {
1078
+ dlopen(path: string, defs: Record<string, unknown>): BunFfiLib;
1079
+ FFIType: Record<string, number>;
1080
+ ptr(value: ArrayBufferView): unknown;
1081
+ CString: new (ptr: unknown) => { toString(): string };
1082
+ read: {
1083
+ ptr(buf: unknown, offset?: number): bigint;
1084
+ i32(buf: unknown, offset?: number): number;
1085
+ u64(buf: unknown, offset?: number): bigint;
1086
+ };
1087
+ toArrayBuffer(
1088
+ ptr: bigint | number,
1089
+ byteOffset?: number,
1090
+ byteLength?: number,
1091
+ ): ArrayBuffer;
1092
+ JSCallback: new (
1093
+ fn: (...args: never[]) => unknown,
1094
+ def: { args: number[]; returns: number },
1095
+ ) => BunFfiJSCallback;
1096
+ }
1097
+
1098
+ /**
1099
+ * Resolve `bun:ffi` synchronously via the Bun-injected `require`.
1100
+ * Bun exposes a CJS `require` even from ESM modules, and `bun:ffi` is
1101
+ * a built-in importable that way. Doing this dynamically (rather than a
1102
+ * static `import "bun:ffi"`) keeps the module loadable under plain Node
1103
+ * for the parts of the test suite that don't need the FFI.
1104
+ */
1105
+ function loadBunFfiModule(): BunFfiModule {
1106
+ const req: ((id: string) => unknown) | undefined = (
1107
+ globalThis as { Bun?: { __require?: (id: string) => unknown } }
1108
+ ).Bun?.__require;
1109
+ if (typeof req === "function") {
1110
+ return req("bun:ffi") as BunFfiModule;
1111
+ }
1112
+ // Fallback to `module.createRequire` on the current file when running
1113
+ // under Bun via an ESM entry without `Bun.__require`. This is rare —
1114
+ // current Bun exposes `Bun.__require` — but we keep the path explicit
1115
+ // so the failure mode is `MODULE_NOT_FOUND` (a real error), not a
1116
+ // silent fall-through.
1117
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
1118
+ const mod = require("node:module") as {
1119
+ createRequire: (filename: string) => (id: string) => unknown;
1120
+ };
1121
+ const r = mod.createRequire(import.meta.url);
1122
+ return r("bun:ffi") as BunFfiModule;
1123
+ }
1124
+
1125
+ function bindWithBunFfi(dylibPath: string): ElizaInferenceFfi {
1126
+ let ffi: BunFfiModule;
1127
+ try {
1128
+ ffi = loadBunFfiModule();
1129
+ } catch (err) {
1130
+ throw new VoiceLifecycleError(
1131
+ "kernel-missing",
1132
+ `[ffi-bindings] Cannot load bun:ffi while opening ${dylibPath}: ${formatFfiError(err)}`,
1133
+ );
1134
+ }
1135
+ const T = ffi.FFIType;
1136
+
1137
+ // All `char *` arguments are typed as T.ptr — Bun's `T.cstring` is a
1138
+ // RETURN-only type for "library hands back a NUL-terminated string".
1139
+ // For inputs we encode UTF-8 to a NUL-terminated Buffer on the JS
1140
+ // side and pass `ffi.ptr(buffer)`.
1141
+ let lib: BunFfiLib | null = null;
1142
+ let nativeVadSymbolsAvailable = true;
1143
+ const nativeVadDefs = {
1144
+ // Native Silero VAD (ABI v3). These are additive; some transitional
1145
+ // builds may report ABI v3 before carrying the VAD symbols, so bind
1146
+ // them opportunistically and advertise unsupported if absent.
1147
+ eliza_inference_vad_supported: { args: [], returns: T.i32 },
1148
+ eliza_inference_vad_open: {
1149
+ args: [T.ptr, T.i32, T.ptr],
1150
+ returns: T.ptr,
1151
+ },
1152
+ eliza_inference_vad_process: {
1153
+ args: [T.usize, T.ptr, T.usize, T.ptr, T.ptr],
1154
+ returns: T.i32,
1155
+ },
1156
+ eliza_inference_vad_reset: { args: [T.usize, T.ptr], returns: T.i32 },
1157
+ eliza_inference_vad_close: { args: [T.usize], returns: T.void },
1158
+ };
1159
+ // Native openWakeWord (ABI v5). Additive; transitional builds may report
1160
+ // v5 before the wake-word symbols ship, so bind opportunistically and
1161
+ // advertise unsupported when absent. The wake-word path throws a
1162
+ // structured "runtime not ready" error in that case (no ONNX fallback).
1163
+ let wakewordSymbolsAvailable = true;
1164
+ const wakewordDefs = {
1165
+ eliza_inference_wakeword_supported: { args: [], returns: T.i32 },
1166
+ eliza_inference_wakeword_open: {
1167
+ // ctx, sample_rate_hz, head_name (cstr), out_error
1168
+ args: [T.ptr, T.i32, T.ptr, T.ptr],
1169
+ returns: T.ptr,
1170
+ },
1171
+ eliza_inference_wakeword_score: {
1172
+ // wake (usize), pcm (ptr), n_samples (usize), out_prob (ptr),
1173
+ // out_error (ptr)
1174
+ args: [T.usize, T.ptr, T.usize, T.ptr, T.ptr],
1175
+ returns: T.i32,
1176
+ },
1177
+ eliza_inference_wakeword_reset: {
1178
+ args: [T.usize, T.ptr],
1179
+ returns: T.i32,
1180
+ },
1181
+ eliza_inference_wakeword_close: {
1182
+ args: [T.usize],
1183
+ returns: T.void,
1184
+ },
1185
+ };
1186
+ // Native voice classifiers (ABI v6): WeSpeaker speaker encoder + pyannote
1187
+ // diarizer, fused into the one libelizainference handle. Additive;
1188
+ // transitional builds may report v6 before the classifier symbols ship, so
1189
+ // bind opportunistically and advertise unsupported when absent (the
1190
+ // fused encoder/diarizer classes throw a structured error in that case —
1191
+ // no standalone libvoice_classifier fallback).
1192
+ let speakerSymbolsAvailable = true;
1193
+ const speakerDefs = {
1194
+ eliza_inference_speaker_supported: { args: [], returns: T.i32 },
1195
+ eliza_inference_speaker_open: {
1196
+ // ctx, gguf_path (cstr or NULL), out_error
1197
+ args: [T.ptr, T.ptr, T.ptr],
1198
+ returns: T.ptr,
1199
+ },
1200
+ eliza_inference_speaker_embed: {
1201
+ // speaker (usize), pcm (ptr), n_samples (usize), out_embedding (ptr),
1202
+ // out_error (ptr)
1203
+ args: [T.usize, T.ptr, T.usize, T.ptr, T.ptr],
1204
+ returns: T.i32,
1205
+ },
1206
+ eliza_inference_speaker_close: {
1207
+ args: [T.usize],
1208
+ returns: T.void,
1209
+ },
1210
+ };
1211
+ let diarizSymbolsAvailable = true;
1212
+ const diarizDefs = {
1213
+ eliza_inference_diariz_supported: { args: [], returns: T.i32 },
1214
+ eliza_inference_diariz_open: {
1215
+ // ctx, gguf_path (cstr or NULL), out_error
1216
+ args: [T.ptr, T.ptr, T.ptr],
1217
+ returns: T.ptr,
1218
+ },
1219
+ eliza_inference_diariz_segment: {
1220
+ // diariz (usize), pcm (ptr), n_samples (usize), out_labels (ptr),
1221
+ // io_n_labels (ptr), out_error (ptr)
1222
+ args: [T.usize, T.ptr, T.usize, T.ptr, T.ptr, T.ptr],
1223
+ returns: T.i32,
1224
+ },
1225
+ eliza_inference_diariz_close: {
1226
+ args: [T.usize],
1227
+ returns: T.void,
1228
+ },
1229
+ };
1230
+ // Streaming LLM (additive on top of v3). Bound opportunistically — when
1231
+ // absent the runner reports native streaming as unsupported.
1232
+ let llmStreamSymbolsAvailable = true;
1233
+ // ABI v8 streaming-LLM capability probes. Bound as their own family so a
1234
+ // v7 library (which has the `llm_stream_*` symbols but not these probes)
1235
+ // still binds `llmStreamDefs` while reporting MTP / KV-quant unsupported.
1236
+ let llmCapabilitySymbolsAvailable = true;
1237
+ const llmCapabilityDefs = {
1238
+ eliza_inference_llm_mtp_supported: { args: [], returns: T.i32 },
1239
+ eliza_inference_llm_kv_quant_supported: { args: [], returns: T.i32 },
1240
+ };
1241
+ const llmStreamDefs = {
1242
+ eliza_inference_llm_stream_open: {
1243
+ // ctx (ptr), cfg (ptr to eliza_llm_stream_config_t), out_error (ptr)
1244
+ args: [T.ptr, T.ptr, T.ptr],
1245
+ returns: T.ptr,
1246
+ },
1247
+ eliza_inference_llm_stream_prefill: {
1248
+ args: [T.usize, T.ptr, T.usize, T.ptr],
1249
+ returns: T.i32,
1250
+ },
1251
+ eliza_inference_llm_stream_next: {
1252
+ // stream, tokens_out, tokens_cap, num_tokens_out, text_out,
1253
+ // text_cap, drafter_drafted_out, drafter_accepted_out, out_error
1254
+ args: [
1255
+ T.usize,
1256
+ T.ptr,
1257
+ T.usize,
1258
+ T.ptr,
1259
+ T.ptr,
1260
+ T.usize,
1261
+ T.ptr,
1262
+ T.ptr,
1263
+ T.ptr,
1264
+ ],
1265
+ returns: T.i32,
1266
+ },
1267
+ eliza_inference_llm_stream_cancel: {
1268
+ args: [T.usize],
1269
+ returns: T.i32,
1270
+ },
1271
+ eliza_inference_llm_stream_save_slot: {
1272
+ args: [T.usize, T.ptr, T.ptr],
1273
+ returns: T.i32,
1274
+ },
1275
+ eliza_inference_llm_stream_restore_slot: {
1276
+ args: [T.usize, T.ptr, T.ptr],
1277
+ returns: T.i32,
1278
+ },
1279
+ eliza_inference_llm_stream_close: {
1280
+ args: [T.usize],
1281
+ returns: T.void,
1282
+ },
1283
+ };
1284
+ const referenceEncodeDefs = {
1285
+ // OmniVoice reference encode (ABI v4) — optional for transitional
1286
+ // fused libraries. Default TTS/ASR must still load when reference-clone
1287
+ // freezing is unavailable; encodeReferenceSupported() exposes that state.
1288
+ eliza_inference_encode_reference: {
1289
+ // ctx, pcm, n_samples, sample_rate_hz, out_K, out_ref_T, out_tokens (int**), out_error
1290
+ args: [T.ptr, T.ptr, T.usize, T.i32, T.ptr, T.ptr, T.ptr, T.ptr],
1291
+ returns: T.i32,
1292
+ },
1293
+ eliza_inference_free_tokens: { args: [T.usize], returns: T.void },
1294
+ };
1295
+ let referenceEncodeSymbolsAvailable = true;
1296
+ // Text-adjacent modalities (ABI v9): embeddings, mmproj vision describe, and
1297
+ // the tokenizer over the loaded text vocab. They ship together in a v9
1298
+ // build; bound and gated as one block layered on top of the v8 surface so
1299
+ // the cascade peels them when a v8 library is loaded. `free_tokens` is
1300
+ // re-listed here (a v9 build that lacks reference-encode still needs it for
1301
+ // `tokenize`'s buffer); identical defs merge harmlessly.
1302
+ let textModalitiesSymbolsAvailable = true;
1303
+ const textModalitiesDefs = {
1304
+ eliza_inference_embed_supported: { args: [], returns: T.i32 },
1305
+ eliza_inference_embed: {
1306
+ // ctx, text, text_len, pooling, out_embedding, out_capacity, out_dim, out_error
1307
+ args: [T.ptr, T.ptr, T.usize, T.i32, T.ptr, T.usize, T.ptr, T.ptr],
1308
+ returns: T.i32,
1309
+ },
1310
+ eliza_inference_vision_supported: { args: [], returns: T.i32 },
1311
+ eliza_inference_describe_image: {
1312
+ // ctx, image_bytes, n_bytes, mmproj_path, prompt, out_text, max_text_bytes, out_error
1313
+ args: [T.ptr, T.ptr, T.usize, T.ptr, T.ptr, T.ptr, T.usize, T.ptr],
1314
+ returns: T.i32,
1315
+ },
1316
+ eliza_inference_tokenize_supported: { args: [], returns: T.i32 },
1317
+ eliza_inference_tokenize: {
1318
+ // ctx, text, text_len, add_special, parse_special, out_tokens (int**), out_n, out_error
1319
+ args: [T.ptr, T.ptr, T.usize, T.i32, T.i32, T.ptr, T.ptr, T.ptr],
1320
+ returns: T.i32,
1321
+ },
1322
+ eliza_inference_detokenize: {
1323
+ // ctx, tokens, n_tokens, remove_special, unparse_special, out_text, max_text_bytes, out_error
1324
+ args: [T.ptr, T.ptr, T.usize, T.i32, T.i32, T.ptr, T.usize, T.ptr],
1325
+ returns: T.i32,
1326
+ },
1327
+ eliza_inference_free_tokens: { args: [T.usize], returns: T.void },
1328
+ };
1329
+ // Kokoro TTS (ABI v10): the in-process Kokoro engine folded into the fused
1330
+ // handle so the mobile path stops POSTing to the local-TCP llama-server
1331
+ // route. Bound as its own family layered on top of the v9 surface; the
1332
+ // cascade peels it when a v9 library is loaded. `kokoroSupported()` reports
1333
+ // false in that case and the Kokoro FFI runtime refuses (no TCP fallback).
1334
+ let kokoroSymbolsAvailable = true;
1335
+ const kokoroDefs = {
1336
+ eliza_inference_kokoro_supported: { args: [], returns: T.i32 },
1337
+ eliza_inference_kokoro_load: {
1338
+ // ctx, gguf_path, voice_bin_path, style_dim, out_error
1339
+ args: [T.ptr, T.ptr, T.ptr, T.i32, T.ptr],
1340
+ returns: T.i32,
1341
+ },
1342
+ eliza_inference_kokoro_synthesize: {
1343
+ // ctx, text, text_len, speed, out_pcm, max_samples, out_error
1344
+ args: [T.ptr, T.ptr, T.usize, T.f32, T.ptr, T.usize, T.ptr],
1345
+ returns: T.i32,
1346
+ },
1347
+ eliza_inference_kokoro_sample_rate: { args: [T.ptr], returns: T.i32 },
1348
+ };
1349
+ // End-of-turn scoring (ABI v11): a single causal forward pass over a
1350
+ // pre-tokenized partial transcript returns P(end-of-turn token). Layered on
1351
+ // top of the v10 surface; the cascade peels it when a v10 library is loaded
1352
+ // (the `eotSupported()` probe then reports false and the composite EOT
1353
+ // classifier falls back to the heuristic-only signal).
1354
+ let eotSymbolsAvailable = true;
1355
+ const eotDefs = {
1356
+ eliza_inference_llm_eot_supported: { args: [], returns: T.i32 },
1357
+ eliza_inference_llm_eot_score: {
1358
+ // ctx, token_ids, num_tokens, target_token_id,
1359
+ // out_target_prob, out_top_token, out_top_prob, out_error
1360
+ args: [T.ptr, T.ptr, T.usize, T.i32, T.ptr, T.ptr, T.ptr, T.ptr],
1361
+ returns: T.i32,
1362
+ },
1363
+ };
1364
+ // ABI v12 — fused ASR word timestamps.
1365
+ let timedAsrSymbolsAvailable = true;
1366
+ const timedAsrDefs = {
1367
+ eliza_inference_asr_timestamps_supported: { args: [], returns: T.i32 },
1368
+ eliza_inference_asr_transcribe_timed: {
1369
+ // ctx, pcm, n_samples, sr, out_text, max_text_bytes,
1370
+ // out_word_start_ms, out_word_end_ms, io_n_words, out_error
1371
+ args: [
1372
+ T.ptr,
1373
+ T.ptr,
1374
+ T.usize,
1375
+ T.i32,
1376
+ T.ptr,
1377
+ T.usize,
1378
+ T.ptr,
1379
+ T.ptr,
1380
+ T.ptr,
1381
+ T.ptr,
1382
+ ],
1383
+ returns: T.i32,
1384
+ },
1385
+ };
1386
+ const coreDefs = {
1387
+ eliza_inference_abi_version: { args: [], returns: T.cstring },
1388
+ eliza_inference_create: {
1389
+ args: [T.ptr, T.ptr],
1390
+ returns: T.ptr,
1391
+ },
1392
+ eliza_inference_destroy: { args: [T.ptr], returns: T.void },
1393
+ eliza_inference_mmap_acquire: {
1394
+ args: [T.ptr, T.ptr, T.ptr],
1395
+ returns: T.i32,
1396
+ },
1397
+ eliza_inference_mmap_evict: {
1398
+ args: [T.ptr, T.ptr, T.ptr],
1399
+ returns: T.i32,
1400
+ },
1401
+ eliza_inference_tts_synthesize: {
1402
+ args: [T.ptr, T.ptr, T.usize, T.ptr, T.ptr, T.usize, T.ptr],
1403
+ returns: T.i32,
1404
+ },
1405
+ eliza_inference_asr_transcribe: {
1406
+ args: [T.ptr, T.ptr, T.usize, T.i32, T.ptr, T.usize, T.ptr],
1407
+ returns: T.i32,
1408
+ },
1409
+ // Streaming TTS + native verifier callback (ABI v2). The
1410
+ // function-pointer args are passed as raw pointer values
1411
+ // (`JSCallback.ptr`, or 0n to clear) so this binding owns the
1412
+ // JSCallback lifetime explicitly — see `ttsSynthesizeStream` /
1413
+ // `setVerifierCallback` below.
1414
+ eliza_inference_tts_stream_supported: { args: [], returns: T.i32 },
1415
+ eliza_inference_tts_synthesize_stream: {
1416
+ // ctx, text, text_len, speaker, on_chunk (fn ptr), user_data, out_error
1417
+ args: [T.ptr, T.ptr, T.usize, T.ptr, T.usize, T.usize, T.ptr],
1418
+ returns: T.i32,
1419
+ },
1420
+ eliza_inference_cancel_tts: { args: [T.ptr, T.ptr], returns: T.i32 },
1421
+ eliza_inference_set_verifier_callback: {
1422
+ // ctx, cb (fn ptr — 0 to clear), user_data, out_error
1423
+ args: [T.ptr, T.usize, T.usize, T.ptr],
1424
+ returns: T.i32,
1425
+ },
1426
+ // Streaming ASR (ABI v2).
1427
+ eliza_inference_asr_stream_supported: { args: [], returns: T.i32 },
1428
+ eliza_inference_asr_stream_open: {
1429
+ args: [T.ptr, T.i32, T.ptr],
1430
+ returns: T.ptr,
1431
+ },
1432
+ eliza_inference_asr_stream_feed: {
1433
+ // stream handle is a raw C pointer → pass as usize.
1434
+ args: [T.usize, T.ptr, T.usize, T.ptr],
1435
+ returns: T.i32,
1436
+ },
1437
+ eliza_inference_asr_stream_partial: {
1438
+ args: [T.usize, T.ptr, T.usize, T.ptr, T.ptr, T.ptr],
1439
+ returns: T.i32,
1440
+ },
1441
+ eliza_inference_asr_stream_finish: {
1442
+ args: [T.usize, T.ptr, T.usize, T.ptr, T.ptr, T.ptr],
1443
+ returns: T.i32,
1444
+ },
1445
+ eliza_inference_asr_stream_close: { args: [T.usize], returns: T.void },
1446
+ // Bun 1.3.x accepts raw pointer values passed back into C as
1447
+ // `usize`, while `ptr` is for JS-owned ArrayBuffer pointers.
1448
+ eliza_inference_free_string: { args: [T.usize], returns: T.void },
1449
+ };
1450
+ // Try the maximal additive symbol set first, then progressively drop
1451
+ // optional families. Each fallback flips a sentinel so `*Supported()` probes
1452
+ // report false instead of making an unavailable native call.
1453
+ // The v6 voice-classifier families (speaker encoder + diarizer) ship
1454
+ // together in the fused build, so they are bound and gated as one
1455
+ // `classifiers` block layered on top of the v5 wake-word family. The
1456
+ // cascade peels them in priority order: full v6 → v6-without-classifiers
1457
+ // (a real v5 build) → progressively smaller. Each rung flips a sentinel so
1458
+ // `*Supported()` reports false instead of calling an unbound symbol.
1459
+ const classifierDefs = { ...speakerDefs, ...diarizDefs };
1460
+ const attempts = [
1461
+ {
1462
+ // Full v12 surface (v11 + the in-process ASR word-timestamp decoder).
1463
+ defs: {
1464
+ ...coreDefs,
1465
+ ...referenceEncodeDefs,
1466
+ ...nativeVadDefs,
1467
+ ...wakewordDefs,
1468
+ ...classifierDefs,
1469
+ ...llmStreamDefs,
1470
+ ...llmCapabilityDefs,
1471
+ ...textModalitiesDefs,
1472
+ ...kokoroDefs,
1473
+ ...eotDefs,
1474
+ ...timedAsrDefs,
1475
+ },
1476
+ referenceEncode: true,
1477
+ nativeVad: true,
1478
+ wakeword: true,
1479
+ classifiers: true,
1480
+ llmStream: true,
1481
+ llmCapability: true,
1482
+ textModalities: true,
1483
+ kokoro: true,
1484
+ eot: true,
1485
+ timedAsr: true,
1486
+ },
1487
+ {
1488
+ // Full v11 surface (v10 + the in-process end-of-turn scorer); a v11
1489
+ // build lacks the v12 timed-ASR symbols.
1490
+ defs: {
1491
+ ...coreDefs,
1492
+ ...referenceEncodeDefs,
1493
+ ...nativeVadDefs,
1494
+ ...wakewordDefs,
1495
+ ...classifierDefs,
1496
+ ...llmStreamDefs,
1497
+ ...llmCapabilityDefs,
1498
+ ...textModalitiesDefs,
1499
+ ...kokoroDefs,
1500
+ ...eotDefs,
1501
+ },
1502
+ referenceEncode: true,
1503
+ nativeVad: true,
1504
+ wakeword: true,
1505
+ classifiers: true,
1506
+ llmStream: true,
1507
+ llmCapability: true,
1508
+ textModalities: true,
1509
+ kokoro: true,
1510
+ eot: true,
1511
+ timedAsr: false,
1512
+ },
1513
+ {
1514
+ // Full v10 surface (v9 + the in-process Kokoro block).
1515
+ defs: {
1516
+ ...coreDefs,
1517
+ ...referenceEncodeDefs,
1518
+ ...nativeVadDefs,
1519
+ ...wakewordDefs,
1520
+ ...classifierDefs,
1521
+ ...llmStreamDefs,
1522
+ ...llmCapabilityDefs,
1523
+ ...textModalitiesDefs,
1524
+ ...kokoroDefs,
1525
+ },
1526
+ referenceEncode: true,
1527
+ nativeVad: true,
1528
+ wakeword: true,
1529
+ classifiers: true,
1530
+ llmStream: true,
1531
+ llmCapability: true,
1532
+ textModalities: true,
1533
+ kokoro: true,
1534
+ },
1535
+ {
1536
+ // Full v9 surface (no v10 Kokoro block).
1537
+ defs: {
1538
+ ...coreDefs,
1539
+ ...referenceEncodeDefs,
1540
+ ...nativeVadDefs,
1541
+ ...wakewordDefs,
1542
+ ...classifierDefs,
1543
+ ...llmStreamDefs,
1544
+ ...llmCapabilityDefs,
1545
+ ...textModalitiesDefs,
1546
+ },
1547
+ referenceEncode: true,
1548
+ nativeVad: true,
1549
+ wakeword: true,
1550
+ classifiers: true,
1551
+ llmStream: true,
1552
+ llmCapability: true,
1553
+ textModalities: true,
1554
+ },
1555
+ {
1556
+ // Full v8 surface (no v9 text-modality block).
1557
+ defs: {
1558
+ ...coreDefs,
1559
+ ...referenceEncodeDefs,
1560
+ ...nativeVadDefs,
1561
+ ...wakewordDefs,
1562
+ ...classifierDefs,
1563
+ ...llmStreamDefs,
1564
+ ...llmCapabilityDefs,
1565
+ },
1566
+ referenceEncode: true,
1567
+ nativeVad: true,
1568
+ wakeword: true,
1569
+ classifiers: true,
1570
+ llmStream: true,
1571
+ llmCapability: true,
1572
+ textModalities: false,
1573
+ },
1574
+ {
1575
+ defs: {
1576
+ ...coreDefs,
1577
+ ...nativeVadDefs,
1578
+ ...wakewordDefs,
1579
+ ...classifierDefs,
1580
+ ...llmStreamDefs,
1581
+ ...llmCapabilityDefs,
1582
+ },
1583
+ referenceEncode: false,
1584
+ nativeVad: true,
1585
+ wakeword: true,
1586
+ classifiers: true,
1587
+ llmStream: true,
1588
+ llmCapability: true,
1589
+ textModalities: false,
1590
+ },
1591
+ {
1592
+ defs: {
1593
+ ...coreDefs,
1594
+ ...referenceEncodeDefs,
1595
+ ...nativeVadDefs,
1596
+ ...wakewordDefs,
1597
+ ...llmStreamDefs,
1598
+ },
1599
+ referenceEncode: true,
1600
+ nativeVad: true,
1601
+ wakeword: true,
1602
+ classifiers: false,
1603
+ llmStream: true,
1604
+ llmCapability: false,
1605
+ },
1606
+ {
1607
+ defs: {
1608
+ ...coreDefs,
1609
+ ...nativeVadDefs,
1610
+ ...wakewordDefs,
1611
+ ...llmStreamDefs,
1612
+ },
1613
+ referenceEncode: false,
1614
+ nativeVad: true,
1615
+ wakeword: true,
1616
+ classifiers: false,
1617
+ llmStream: true,
1618
+ llmCapability: false,
1619
+ },
1620
+ {
1621
+ defs: {
1622
+ ...coreDefs,
1623
+ ...referenceEncodeDefs,
1624
+ ...nativeVadDefs,
1625
+ ...llmStreamDefs,
1626
+ },
1627
+ referenceEncode: true,
1628
+ nativeVad: true,
1629
+ wakeword: false,
1630
+ classifiers: false,
1631
+ llmStream: true,
1632
+ llmCapability: false,
1633
+ },
1634
+ {
1635
+ defs: { ...coreDefs, ...nativeVadDefs, ...llmStreamDefs },
1636
+ referenceEncode: false,
1637
+ nativeVad: true,
1638
+ wakeword: false,
1639
+ classifiers: false,
1640
+ llmStream: true,
1641
+ llmCapability: false,
1642
+ },
1643
+ {
1644
+ defs: { ...coreDefs, ...referenceEncodeDefs, ...nativeVadDefs },
1645
+ referenceEncode: true,
1646
+ nativeVad: true,
1647
+ wakeword: false,
1648
+ classifiers: false,
1649
+ llmStream: false,
1650
+ llmCapability: false,
1651
+ },
1652
+ {
1653
+ defs: { ...coreDefs, ...nativeVadDefs },
1654
+ referenceEncode: false,
1655
+ nativeVad: true,
1656
+ wakeword: false,
1657
+ classifiers: false,
1658
+ llmStream: false,
1659
+ llmCapability: false,
1660
+ },
1661
+ {
1662
+ defs: { ...coreDefs, ...referenceEncodeDefs },
1663
+ referenceEncode: true,
1664
+ nativeVad: false,
1665
+ wakeword: false,
1666
+ classifiers: false,
1667
+ llmStream: false,
1668
+ llmCapability: false,
1669
+ },
1670
+ {
1671
+ defs: coreDefs,
1672
+ referenceEncode: false,
1673
+ nativeVad: false,
1674
+ wakeword: false,
1675
+ classifiers: false,
1676
+ llmStream: false,
1677
+ llmCapability: false,
1678
+ },
1679
+ ];
1680
+ let lastOpenError: unknown = null;
1681
+ for (const attempt of attempts) {
1682
+ try {
1683
+ lib = ffi.dlopen(dylibPath, attempt.defs);
1684
+ referenceEncodeSymbolsAvailable = attempt.referenceEncode;
1685
+ nativeVadSymbolsAvailable = attempt.nativeVad;
1686
+ wakewordSymbolsAvailable = attempt.wakeword;
1687
+ speakerSymbolsAvailable = attempt.classifiers;
1688
+ diarizSymbolsAvailable = attempt.classifiers;
1689
+ llmStreamSymbolsAvailable = attempt.llmStream;
1690
+ llmCapabilitySymbolsAvailable = attempt.llmCapability ?? false;
1691
+ textModalitiesSymbolsAvailable =
1692
+ (attempt as { textModalities?: boolean }).textModalities ?? false;
1693
+ kokoroSymbolsAvailable =
1694
+ (attempt as { kokoro?: boolean }).kokoro ?? false;
1695
+ eotSymbolsAvailable = (attempt as { eot?: boolean }).eot ?? false;
1696
+ timedAsrSymbolsAvailable =
1697
+ (attempt as { timedAsr?: boolean }).timedAsr ?? false;
1698
+ break;
1699
+ } catch (err) {
1700
+ lastOpenError = err;
1701
+ }
1702
+ }
1703
+ if (lib === null) {
1704
+ throw new VoiceLifecycleError(
1705
+ "kernel-missing",
1706
+ `[ffi-bindings] Failed to open libelizainference at ${dylibPath}: ${formatFfiError(lastOpenError)}`,
1707
+ );
1708
+ }
1709
+ const loadedLib = lib;
1710
+
1711
+ // ABI version check. v4 is the current full surface; v3 is accepted only
1712
+ // when the optional reference-encode symbols are absent so default TTS/ASR
1713
+ // can still run while sample-to-profile freezing stays explicitly disabled.
1714
+ const reported = readCString(
1715
+ loadedLib.symbols.eliza_inference_abi_version(),
1716
+ ffi,
1717
+ );
1718
+ // v8 is the current full surface (v8 = streaming-LLM text parity: same-file
1719
+ // MTP speculative decoding + KV-cache quant + per-load GPU layers, probed
1720
+ // via `eliza_inference_llm_{mtp,kv_quant}_supported()`). A v7 library has
1721
+ // the identical voice/ASR/VAD symbol surface but lacks those LLM
1722
+ // optimizations, so it is still accepted for voice — the new capability
1723
+ // probes report unsupported, and the fused TEXT path refuses to route
1724
+ // through it (the anti-regression guard). Older fused builds may still be
1725
+ // useful at degraded capability:
1726
+ // - v7: real Silero VAD; LLM-text optimizations absent (probed).
1727
+ // - v6: same symbols as v7; VAD may be a stub (probed at runtime).
1728
+ // - v5: no speaker/diarizer classifiers — JS reports them unsupported.
1729
+ // - v4: additionally no wake-word — JS reports wake-word unsupported.
1730
+ // - v3: additionally no reference-encode — accepted only when the
1731
+ // optional reference-encode symbols are absent from the binding.
1732
+ // v10 (current) accepts the full surface. A v9 library has the identical
1733
+ // voice/ASR/VAD/LLM/text surface but lacks the v10 Kokoro symbols
1734
+ // (`eliza_inference_kokoro_*`), so it is accepted only when those symbols
1735
+ // are absent — the `kokoroSupported()` probe then reports false and the
1736
+ // Kokoro FFI runtime refuses (no TCP fallback on mobile). A v8 library
1737
+ // additionally lacks the v9 text-modality symbols (embeddings, vision,
1738
+ // tokenizer), accepted only when those are absent too.
1739
+ const abiOk =
1740
+ reported === String(ELIZA_INFERENCE_ABI_VERSION) ||
1741
+ (reported === "11" && !timedAsrSymbolsAvailable) ||
1742
+ (reported === "10" && !eotSymbolsAvailable && !timedAsrSymbolsAvailable) ||
1743
+ (reported === "9" && !kokoroSymbolsAvailable && !eotSymbolsAvailable) ||
1744
+ (reported === "8" &&
1745
+ !kokoroSymbolsAvailable &&
1746
+ !textModalitiesSymbolsAvailable) ||
1747
+ reported === "7" ||
1748
+ reported === "6" ||
1749
+ (reported === "5" && !speakerSymbolsAvailable && !diarizSymbolsAvailable) ||
1750
+ (reported === "4" &&
1751
+ !wakewordSymbolsAvailable &&
1752
+ !speakerSymbolsAvailable &&
1753
+ !diarizSymbolsAvailable) ||
1754
+ (reported === "3" &&
1755
+ !wakewordSymbolsAvailable &&
1756
+ !speakerSymbolsAvailable &&
1757
+ !diarizSymbolsAvailable &&
1758
+ !referenceEncodeSymbolsAvailable);
1759
+ if (!abiOk) {
1760
+ loadedLib.close();
1761
+ throw new VoiceLifecycleError(
1762
+ "kernel-missing",
1763
+ `[ffi-bindings] ABI mismatch: binding expected v${ELIZA_INFERENCE_ABI_VERSION}, ` +
1764
+ `library at ${dylibPath} reports v${reported}. The fused build was produced ` +
1765
+ `against a different ffi.h — rebuild against the current header.`,
1766
+ );
1767
+ }
1768
+
1769
+ /**
1770
+ * Read `*outErrPtr` (a `char**` that the library populated with a
1771
+ * heap-allocated NUL-terminated string), free the underlying buffer
1772
+ * via `eliza_inference_free_string`, and return the JS string. When
1773
+ * the library left `*outErrPtr` as NULL, returns null.
1774
+ */
1775
+ function takeError(outErrPtrBuf: BigUint64Array): string | null {
1776
+ const ptrValue = outErrPtrBuf[0];
1777
+ if (ptrValue === undefined || ptrValue === 0n) return null;
1778
+ const ptrNumber = Number(ptrValue);
1779
+ if (!Number.isSafeInteger(ptrNumber)) {
1780
+ throw new VoiceLifecycleError(
1781
+ "kernel-missing",
1782
+ `[ffi-bindings] C diagnostic pointer ${ptrValue.toString()} exceeds JS safe integer range`,
1783
+ );
1784
+ }
1785
+ const cstr = new ffi.CString(ptrNumber);
1786
+ const message = cstr.toString();
1787
+ loadedLib.symbols.eliza_inference_free_string(ptrValue);
1788
+ return message;
1789
+ }
1790
+
1791
+ function makeOutErr(): { buf: BigUint64Array; ptr: unknown } {
1792
+ const buf = new BigUint64Array(1);
1793
+ return { buf, ptr: ffi.ptr(buf) };
1794
+ }
1795
+
1796
+ /**
1797
+ * Encode a JS string to a NUL-terminated UTF-8 buffer and return a
1798
+ * `T.ptr`-compatible pointer suitable for `const char *` arguments.
1799
+ * Returns null when the input is null — the C ABI accepts NULL for
1800
+ * optional arguments like `speaker_preset_id`.
1801
+ */
1802
+ function cstr(value: string | null): {
1803
+ ptr: unknown;
1804
+ bytes: number;
1805
+ buffer: Buffer | null;
1806
+ } {
1807
+ if (value === null) return { ptr: null, bytes: 0, buffer: null };
1808
+ const bytes = Buffer.from(value, "utf8");
1809
+ const buf = Buffer.alloc(bytes.byteLength + 1);
1810
+ bytes.copy(buf);
1811
+ return { ptr: ffi.ptr(buf), bytes: bytes.byteLength, buffer: buf };
1812
+ }
1813
+
1814
+ function failureCode(rc: number): VoiceLifecycleError["code"] {
1815
+ if (rc === ELIZA_ERR_OOM) return "ram-pressure";
1816
+ if (rc === ELIZA_ERR_FFI_FAULT) return "mmap-fail";
1817
+ if (rc === ELIZA_ERR_NOT_IMPLEMENTED) return "kernel-missing";
1818
+ if (rc === ELIZA_ERR_ABI_MISMATCH) return "kernel-missing";
1819
+ if (rc === ELIZA_ERR_BUNDLE_INVALID) return "kernel-missing";
1820
+ return "kernel-missing";
1821
+ }
1822
+
1823
+ function isNullPointer(value: unknown): boolean {
1824
+ return value === null || value === undefined || value === 0n || value === 0;
1825
+ }
1826
+
1827
+ return {
1828
+ libraryPath: dylibPath,
1829
+ libraryAbiVersion: reported,
1830
+
1831
+ create(bundleDir: string): ElizaInferenceContextHandle {
1832
+ const err = makeOutErr();
1833
+ const bundleArg = cstr(bundleDir);
1834
+ const handle = loadedLib.symbols.eliza_inference_create(
1835
+ bundleArg.ptr,
1836
+ err.ptr,
1837
+ );
1838
+ if (isNullPointer(handle)) {
1839
+ const message =
1840
+ takeError(err.buf) ??
1841
+ "[ffi-bindings] eliza_inference_create returned NULL with no diagnostic";
1842
+ throw new VoiceLifecycleError("kernel-missing", message);
1843
+ }
1844
+ return handle as ElizaInferenceContextHandle;
1845
+ },
1846
+
1847
+ destroy(ctx: ElizaInferenceContextHandle): void {
1848
+ loadedLib.symbols.eliza_inference_destroy(ctx);
1849
+ },
1850
+
1851
+ mmapAcquire(ctx, region) {
1852
+ const err = makeOutErr();
1853
+ const regionArg = cstr(region);
1854
+ const rc = loadedLib.symbols.eliza_inference_mmap_acquire(
1855
+ ctx,
1856
+ regionArg.ptr,
1857
+ err.ptr,
1858
+ );
1859
+ if (rc !== ELIZA_OK) {
1860
+ const message =
1861
+ takeError(err.buf) ??
1862
+ `[ffi-bindings] eliza_inference_mmap_acquire(${region}) rc=${rc}`;
1863
+ throw new VoiceLifecycleError(failureCode(rc), message);
1864
+ }
1865
+ },
1866
+
1867
+ mmapEvict(ctx, region) {
1868
+ const err = makeOutErr();
1869
+ const regionArg = cstr(region);
1870
+ const rc = loadedLib.symbols.eliza_inference_mmap_evict(
1871
+ ctx,
1872
+ regionArg.ptr,
1873
+ err.ptr,
1874
+ );
1875
+ if (rc !== ELIZA_OK) {
1876
+ const message =
1877
+ takeError(err.buf) ??
1878
+ `[ffi-bindings] eliza_inference_mmap_evict(${region}) rc=${rc}`;
1879
+ throw new VoiceLifecycleError(failureCode(rc), message);
1880
+ }
1881
+ },
1882
+
1883
+ ttsSynthesize({ ctx, text, speakerPresetId, out }) {
1884
+ const err = makeOutErr();
1885
+ const textArg = cstr(text);
1886
+ const speakerArg = cstr(speakerPresetId);
1887
+ const rc = loadedLib.symbols.eliza_inference_tts_synthesize(
1888
+ ctx,
1889
+ textArg.ptr,
1890
+ BigInt(textArg.bytes),
1891
+ speakerArg.ptr,
1892
+ ffi.ptr(out),
1893
+ BigInt(out.length),
1894
+ err.ptr,
1895
+ );
1896
+ if (rc < 0) {
1897
+ const message =
1898
+ takeError(err.buf) ??
1899
+ `[ffi-bindings] eliza_inference_tts_synthesize rc=${rc}`;
1900
+ throw new VoiceLifecycleError(failureCode(rc), message);
1901
+ }
1902
+ return rc;
1903
+ },
1904
+
1905
+ asrTranscribe({ ctx, pcm, sampleRateHz, maxTextBytes }) {
1906
+ const err = makeOutErr();
1907
+ const cap = maxTextBytes ?? 4096;
1908
+ const outText = new Uint8Array(cap);
1909
+ const rc = loadedLib.symbols.eliza_inference_asr_transcribe(
1910
+ ctx,
1911
+ ffi.ptr(pcm),
1912
+ BigInt(pcm.length),
1913
+ sampleRateHz,
1914
+ ffi.ptr(outText),
1915
+ BigInt(cap),
1916
+ err.ptr,
1917
+ );
1918
+ if (rc < 0) {
1919
+ const message =
1920
+ takeError(err.buf) ??
1921
+ `[ffi-bindings] eliza_inference_asr_transcribe rc=${rc}`;
1922
+ throw new VoiceLifecycleError(failureCode(rc), message);
1923
+ }
1924
+ const nul = outText.indexOf(0, 0);
1925
+ const len = nul >= 0 ? nul : rc;
1926
+ return Buffer.from(outText.buffer, outText.byteOffset, len).toString(
1927
+ "utf8",
1928
+ );
1929
+ },
1930
+
1931
+ timedAsrSupported(): boolean {
1932
+ const probe = loadedLib.symbols.eliza_inference_asr_timestamps_supported;
1933
+ return (
1934
+ timedAsrSymbolsAvailable && typeof probe === "function" && probe() === 1
1935
+ );
1936
+ },
1937
+
1938
+ asrTranscribeTimed({ ctx, pcm, sampleRateHz, maxTextBytes, maxWords }) {
1939
+ const fn = loadedLib.symbols.eliza_inference_asr_transcribe_timed;
1940
+ if (!timedAsrSymbolsAvailable || typeof fn !== "function") {
1941
+ throw new VoiceLifecycleError(
1942
+ "kernel-missing",
1943
+ "[ffi-bindings] eliza_inference_asr_transcribe_timed is not exported by this build (pre-v12)",
1944
+ );
1945
+ }
1946
+ const err = makeOutErr();
1947
+ const cap = maxTextBytes ?? 4096;
1948
+ const wordCap = maxWords ?? 1024;
1949
+ const outText = new Uint8Array(cap);
1950
+ const startMs = new Int32Array(wordCap);
1951
+ const endMs = new Int32Array(wordCap);
1952
+ const nWords = new BigUint64Array(1);
1953
+ nWords[0] = BigInt(wordCap);
1954
+ const rc = fn(
1955
+ ctx,
1956
+ ffi.ptr(pcm),
1957
+ BigInt(pcm.length),
1958
+ sampleRateHz,
1959
+ ffi.ptr(outText),
1960
+ BigInt(cap),
1961
+ ffi.ptr(startMs),
1962
+ ffi.ptr(endMs),
1963
+ ffi.ptr(nWords),
1964
+ err.ptr,
1965
+ );
1966
+ if (rc < 0) {
1967
+ const message =
1968
+ takeError(err.buf) ??
1969
+ `[ffi-bindings] eliza_inference_asr_transcribe_timed rc=${rc}`;
1970
+ throw new VoiceLifecycleError(failureCode(rc), message);
1971
+ }
1972
+ const nul = outText.indexOf(0, 0);
1973
+ const len = nul >= 0 ? nul : rc;
1974
+ const text = Buffer.from(
1975
+ outText.buffer,
1976
+ outText.byteOffset,
1977
+ len,
1978
+ ).toString("utf8");
1979
+ const words = recoverAsrWords(text, Number(nWords[0]), startMs, endMs);
1980
+ return { text, words };
1981
+ },
1982
+
1983
+ /* ---- Streaming TTS + verifier callback (ABI v2) ------------ */
1984
+
1985
+ ttsStreamSupported(): boolean {
1986
+ return loadedLib.symbols.eliza_inference_tts_stream_supported() === 1;
1987
+ },
1988
+
1989
+ ttsSynthesizeStream({ ctx, text, speakerPresetId, onChunk }) {
1990
+ const err = makeOutErr();
1991
+ const textArg = cstr(text);
1992
+ const speakerArg = cstr(speakerPresetId);
1993
+ // (pcm: ptr, n_samples: usize, is_final: i32, user_data: ptr) -> i32
1994
+ const cb = new ffi.JSCallback(
1995
+ ((pcmPtr: bigint, nSamples: bigint, isFinal: number) => {
1996
+ const n = Number(nSamples);
1997
+ // Bun delivers the C pointer as a bigint; copy the floats out
1998
+ // before returning — the buffer is the library's, valid only
1999
+ // for this call.
2000
+ const pcm =
2001
+ n > 0 && pcmPtr !== 0n
2002
+ ? new Float32Array(ffi.toArrayBuffer(pcmPtr, 0, n * 4).slice(0))
2003
+ : new Float32Array(0);
2004
+ const requestCancel = onChunk({ pcm, isFinal: isFinal !== 0 });
2005
+ return requestCancel === true ? 1 : 0;
2006
+ }) as unknown as (...args: never[]) => unknown,
2007
+ {
2008
+ args: [T.ptr, T.usize, T.i32, T.ptr],
2009
+ returns: T.i32,
2010
+ },
2011
+ );
2012
+ try {
2013
+ const rc = loadedLib.symbols.eliza_inference_tts_synthesize_stream(
2014
+ ctx,
2015
+ textArg.ptr,
2016
+ BigInt(textArg.bytes),
2017
+ speakerArg.ptr,
2018
+ BigInt(cb.ptr),
2019
+ 0n,
2020
+ err.ptr,
2021
+ );
2022
+ if (rc === ELIZA_ERR_CANCELLED) return { cancelled: true };
2023
+ if (rc < 0) {
2024
+ const message =
2025
+ takeError(err.buf) ??
2026
+ `[ffi-bindings] eliza_inference_tts_synthesize_stream rc=${rc}`;
2027
+ throw new VoiceLifecycleError(failureCode(rc), message);
2028
+ }
2029
+ return { cancelled: false };
2030
+ } finally {
2031
+ cb.close();
2032
+ }
2033
+ },
2034
+
2035
+ cancelTts(ctx) {
2036
+ const err = makeOutErr();
2037
+ const rc = loadedLib.symbols.eliza_inference_cancel_tts(ctx, err.ptr);
2038
+ if (rc !== ELIZA_OK) {
2039
+ const message =
2040
+ takeError(err.buf) ??
2041
+ `[ffi-bindings] eliza_inference_cancel_tts rc=${rc}`;
2042
+ throw new VoiceLifecycleError(failureCode(rc), message);
2043
+ }
2044
+ },
2045
+
2046
+ encodeReferenceSupported(): boolean {
2047
+ return (
2048
+ typeof loadedLib.symbols.eliza_inference_encode_reference === "function"
2049
+ );
2050
+ },
2051
+
2052
+ encodeReference({ ctx, pcm, sampleRateHz }) {
2053
+ if (
2054
+ typeof loadedLib.symbols.eliza_inference_encode_reference !==
2055
+ "function" ||
2056
+ typeof loadedLib.symbols.eliza_inference_free_tokens !== "function"
2057
+ ) {
2058
+ throw new VoiceLifecycleError(
2059
+ "kernel-missing",
2060
+ "[ffi-bindings] eliza_inference_encode_reference is not exported by this build",
2061
+ );
2062
+ }
2063
+ if (sampleRateHz !== 24000) {
2064
+ throw new VoiceLifecycleError(
2065
+ "kernel-missing",
2066
+ `[ffi-bindings] encodeReference: sampleRateHz must be 24000 (got ${sampleRateHz})`,
2067
+ );
2068
+ }
2069
+ const err = makeOutErr();
2070
+ // out_K and out_ref_T are int*, out_tokens is int** — give the library
2071
+ // a slot to write into, then read back.
2072
+ const outK = new Int32Array(1);
2073
+ const outRefT = new Int32Array(1);
2074
+ const outTokensPtr = new BigUint64Array(1);
2075
+ const rc = loadedLib.symbols.eliza_inference_encode_reference(
2076
+ ctx,
2077
+ ffi.ptr(pcm),
2078
+ BigInt(pcm.length),
2079
+ sampleRateHz,
2080
+ ffi.ptr(outK),
2081
+ ffi.ptr(outRefT),
2082
+ ffi.ptr(outTokensPtr),
2083
+ err.ptr,
2084
+ );
2085
+ if (rc !== ELIZA_OK) {
2086
+ const message =
2087
+ takeError(err.buf) ??
2088
+ `[ffi-bindings] eliza_inference_encode_reference rc=${rc}`;
2089
+ throw new VoiceLifecycleError(failureCode(rc), message);
2090
+ }
2091
+ const K = outK[0];
2092
+ const refT = outRefT[0];
2093
+ const tokensRaw = outTokensPtr[0];
2094
+ if (K <= 0 || refT <= 0 || tokensRaw === 0n) {
2095
+ throw new VoiceLifecycleError(
2096
+ "kernel-missing",
2097
+ `[ffi-bindings] encodeReference returned empty result (K=${K}, refT=${refT})`,
2098
+ );
2099
+ }
2100
+ const tokenCount = K * refT;
2101
+ try {
2102
+ // Copy out of the library's malloc'ed buffer so we can free it
2103
+ // before returning. Each int32 is 4 bytes.
2104
+ const tokenBytes = tokenCount * 4;
2105
+ const tokensPtr =
2106
+ typeof tokensRaw === "bigint" ? Number(tokensRaw) : tokensRaw;
2107
+ const nativeView = ffi.toArrayBuffer(tokensPtr, 0, tokenBytes);
2108
+ const bytes = new Uint8Array(nativeView);
2109
+ if (bytes.byteLength < tokenBytes) {
2110
+ throw new VoiceLifecycleError(
2111
+ "kernel-missing",
2112
+ `[ffi-bindings] encodeReference returned an unreadable token buffer (K=${K}, refT=${refT}, got=${bytes.byteLength}, expected=${tokenBytes}, ctor=${nativeView.constructor.name})`,
2113
+ );
2114
+ }
2115
+ const copied = bytes.slice(0, tokenBytes);
2116
+ const tokens = new Int32Array(copied.buffer);
2117
+ return { K, refT, tokens };
2118
+ } finally {
2119
+ loadedLib.symbols.eliza_inference_free_tokens(tokensRaw);
2120
+ }
2121
+ },
2122
+
2123
+ setVerifierCallback(ctx, cbFn) {
2124
+ const err = makeOutErr();
2125
+ if (cbFn === null) {
2126
+ const rc = loadedLib.symbols.eliza_inference_set_verifier_callback(
2127
+ ctx,
2128
+ 0n,
2129
+ 0n,
2130
+ err.ptr,
2131
+ );
2132
+ if (rc !== ELIZA_OK) {
2133
+ const message =
2134
+ takeError(err.buf) ??
2135
+ `[ffi-bindings] eliza_inference_set_verifier_callback(clear) rc=${rc}`;
2136
+ throw new VoiceLifecycleError(failureCode(rc), message);
2137
+ }
2138
+ return { close: () => {} };
2139
+ }
2140
+ // (ev: ptr to EliVerifierEvent, user_data: ptr) -> void
2141
+ const cb = new ffi.JSCallback(
2142
+ ((evPtr: bigint) => {
2143
+ cbFn(readVerifierEvent(evPtr, ffi));
2144
+ }) as unknown as (...args: never[]) => unknown,
2145
+ { args: [T.ptr, T.ptr], returns: T.void },
2146
+ );
2147
+ const rc = loadedLib.symbols.eliza_inference_set_verifier_callback(
2148
+ ctx,
2149
+ BigInt(cb.ptr),
2150
+ 0n,
2151
+ err.ptr,
2152
+ );
2153
+ if (rc !== ELIZA_OK) {
2154
+ cb.close();
2155
+ const message =
2156
+ takeError(err.buf) ??
2157
+ `[ffi-bindings] eliza_inference_set_verifier_callback rc=${rc}`;
2158
+ throw new VoiceLifecycleError(failureCode(rc), message);
2159
+ }
2160
+ return {
2161
+ close: () => {
2162
+ // Clear the native registration FIRST, then free the
2163
+ // JSCallback — order matters so the native side never
2164
+ // dereferences a closed callback.
2165
+ const clearErr = makeOutErr();
2166
+ loadedLib.symbols.eliza_inference_set_verifier_callback(
2167
+ ctx,
2168
+ 0n,
2169
+ 0n,
2170
+ clearErr.ptr,
2171
+ );
2172
+ takeError(clearErr.buf);
2173
+ cb.close();
2174
+ },
2175
+ };
2176
+ },
2177
+
2178
+ /* ---- Native VAD (ABI v3) ----------------------------------- */
2179
+
2180
+ vadSupported(): boolean {
2181
+ if (
2182
+ !nativeVadSymbolsAvailable ||
2183
+ typeof loadedLib.symbols.eliza_inference_vad_supported !== "function"
2184
+ ) {
2185
+ return false;
2186
+ }
2187
+ return loadedLib.symbols.eliza_inference_vad_supported() === 1;
2188
+ },
2189
+
2190
+ vadOpen({ ctx, sampleRateHz }) {
2191
+ const open = loadedLib.symbols.eliza_inference_vad_open;
2192
+ if (!nativeVadSymbolsAvailable || typeof open !== "function") {
2193
+ throw new VoiceLifecycleError(
2194
+ "kernel-missing",
2195
+ "[ffi-bindings] eliza_inference_vad_open is not exported by this libelizainference build",
2196
+ );
2197
+ }
2198
+ const err = makeOutErr();
2199
+ const handle = open(ctx, sampleRateHz, err.ptr);
2200
+ if (isNullPointer(handle)) {
2201
+ const message =
2202
+ takeError(err.buf) ??
2203
+ "[ffi-bindings] eliza_inference_vad_open returned NULL with no diagnostic";
2204
+ throw new VoiceLifecycleError("kernel-missing", message);
2205
+ }
2206
+ return handle as NativeVadHandle;
2207
+ },
2208
+
2209
+ vadProcess({ vad, pcm }) {
2210
+ const process = loadedLib.symbols.eliza_inference_vad_process;
2211
+ if (!nativeVadSymbolsAvailable || typeof process !== "function") {
2212
+ throw new VoiceLifecycleError(
2213
+ "kernel-missing",
2214
+ "[ffi-bindings] eliza_inference_vad_process is not exported by this libelizainference build",
2215
+ );
2216
+ }
2217
+ const err = makeOutErr();
2218
+ const outProbability = new Float32Array(1);
2219
+ const rc = process(
2220
+ vad,
2221
+ ffi.ptr(pcm),
2222
+ BigInt(pcm.length),
2223
+ ffi.ptr(outProbability),
2224
+ err.ptr,
2225
+ );
2226
+ if (rc !== ELIZA_OK) {
2227
+ const message =
2228
+ takeError(err.buf) ??
2229
+ `[ffi-bindings] eliza_inference_vad_process rc=${rc}`;
2230
+ throw new VoiceLifecycleError(failureCode(rc), message);
2231
+ }
2232
+ return outProbability[0] ?? 0;
2233
+ },
2234
+
2235
+ vadReset(vad) {
2236
+ const reset = loadedLib.symbols.eliza_inference_vad_reset;
2237
+ if (!nativeVadSymbolsAvailable || typeof reset !== "function") {
2238
+ throw new VoiceLifecycleError(
2239
+ "kernel-missing",
2240
+ "[ffi-bindings] eliza_inference_vad_reset is not exported by this libelizainference build",
2241
+ );
2242
+ }
2243
+ const err = makeOutErr();
2244
+ const rc = reset(vad, err.ptr);
2245
+ if (rc !== ELIZA_OK) {
2246
+ const message =
2247
+ takeError(err.buf) ??
2248
+ `[ffi-bindings] eliza_inference_vad_reset rc=${rc}`;
2249
+ throw new VoiceLifecycleError(failureCode(rc), message);
2250
+ }
2251
+ },
2252
+
2253
+ vadClose(vad) {
2254
+ loadedLib.symbols.eliza_inference_vad_close?.(vad);
2255
+ },
2256
+
2257
+ /* ---- Native wake-word (ABI v5) ----------------------------- */
2258
+
2259
+ wakewordSupported(): boolean {
2260
+ if (
2261
+ !wakewordSymbolsAvailable ||
2262
+ typeof loadedLib.symbols.eliza_inference_wakeword_supported !==
2263
+ "function"
2264
+ ) {
2265
+ return false;
2266
+ }
2267
+ return loadedLib.symbols.eliza_inference_wakeword_supported() === 1;
2268
+ },
2269
+
2270
+ wakewordOpen({ ctx, sampleRateHz, headName }) {
2271
+ const open = loadedLib.symbols.eliza_inference_wakeword_open;
2272
+ if (!wakewordSymbolsAvailable || typeof open !== "function") {
2273
+ throw new VoiceLifecycleError(
2274
+ "kernel-missing",
2275
+ "[ffi-bindings] eliza_inference_wakeword_open is not exported by this libelizainference build (wake-word GGUF runtime not present)",
2276
+ );
2277
+ }
2278
+ const err = makeOutErr();
2279
+ const headArg = cstr(headName);
2280
+ const handle = open(ctx, sampleRateHz, headArg.ptr, err.ptr);
2281
+ if (isNullPointer(handle)) {
2282
+ const message =
2283
+ takeError(err.buf) ??
2284
+ "[ffi-bindings] eliza_inference_wakeword_open returned NULL with no diagnostic";
2285
+ throw new VoiceLifecycleError("kernel-missing", message);
2286
+ }
2287
+ return handle as NativeWakeWordHandle;
2288
+ },
2289
+
2290
+ wakewordScore({ wake, pcm }) {
2291
+ const score = loadedLib.symbols.eliza_inference_wakeword_score;
2292
+ if (!wakewordSymbolsAvailable || typeof score !== "function") {
2293
+ throw new VoiceLifecycleError(
2294
+ "kernel-missing",
2295
+ "[ffi-bindings] eliza_inference_wakeword_score is not exported by this libelizainference build",
2296
+ );
2297
+ }
2298
+ const err = makeOutErr();
2299
+ const outProbability = new Float32Array(1);
2300
+ const rc = score(
2301
+ wake,
2302
+ ffi.ptr(pcm),
2303
+ BigInt(pcm.length),
2304
+ ffi.ptr(outProbability),
2305
+ err.ptr,
2306
+ );
2307
+ if (rc !== ELIZA_OK) {
2308
+ const message =
2309
+ takeError(err.buf) ??
2310
+ `[ffi-bindings] eliza_inference_wakeword_score rc=${rc}`;
2311
+ throw new VoiceLifecycleError(failureCode(rc), message);
2312
+ }
2313
+ return outProbability[0] ?? 0;
2314
+ },
2315
+
2316
+ wakewordReset(wake) {
2317
+ const reset = loadedLib.symbols.eliza_inference_wakeword_reset;
2318
+ if (!wakewordSymbolsAvailable || typeof reset !== "function") {
2319
+ throw new VoiceLifecycleError(
2320
+ "kernel-missing",
2321
+ "[ffi-bindings] eliza_inference_wakeword_reset is not exported by this libelizainference build",
2322
+ );
2323
+ }
2324
+ const err = makeOutErr();
2325
+ const rc = reset(wake, err.ptr);
2326
+ if (rc !== ELIZA_OK) {
2327
+ const message =
2328
+ takeError(err.buf) ??
2329
+ `[ffi-bindings] eliza_inference_wakeword_reset rc=${rc}`;
2330
+ throw new VoiceLifecycleError(failureCode(rc), message);
2331
+ }
2332
+ },
2333
+
2334
+ wakewordClose(wake) {
2335
+ loadedLib.symbols.eliza_inference_wakeword_close?.(wake);
2336
+ },
2337
+
2338
+ /* ---- Native speaker encoder (ABI v6) ----------------------- */
2339
+
2340
+ speakerSupported(): boolean {
2341
+ if (
2342
+ !speakerSymbolsAvailable ||
2343
+ typeof loadedLib.symbols.eliza_inference_speaker_supported !==
2344
+ "function"
2345
+ ) {
2346
+ return false;
2347
+ }
2348
+ return loadedLib.symbols.eliza_inference_speaker_supported() === 1;
2349
+ },
2350
+
2351
+ speakerOpen({ ctx, ggufPath }) {
2352
+ const open = loadedLib.symbols.eliza_inference_speaker_open;
2353
+ if (!speakerSymbolsAvailable || typeof open !== "function") {
2354
+ throw new VoiceLifecycleError(
2355
+ "kernel-missing",
2356
+ "[ffi-bindings] eliza_inference_speaker_open is not exported by this libelizainference build",
2357
+ );
2358
+ }
2359
+ const err = makeOutErr();
2360
+ const ggufArg = cstr(ggufPath);
2361
+ const handle = open(ctx, ggufArg.ptr, err.ptr);
2362
+ if (isNullPointer(handle)) {
2363
+ const message =
2364
+ takeError(err.buf) ??
2365
+ "[ffi-bindings] eliza_inference_speaker_open returned NULL with no diagnostic";
2366
+ throw new VoiceLifecycleError("kernel-missing", message);
2367
+ }
2368
+ return handle as NativeSpeakerHandle;
2369
+ },
2370
+
2371
+ speakerEmbed({ speaker, pcm }) {
2372
+ const embed = loadedLib.symbols.eliza_inference_speaker_embed;
2373
+ if (!speakerSymbolsAvailable || typeof embed !== "function") {
2374
+ throw new VoiceLifecycleError(
2375
+ "kernel-missing",
2376
+ "[ffi-bindings] eliza_inference_speaker_embed is not exported by this libelizainference build",
2377
+ );
2378
+ }
2379
+ const err = makeOutErr();
2380
+ const outEmbedding = new Float32Array(SPEAKER_EMBEDDING_DIM);
2381
+ const rc = embed(
2382
+ speaker,
2383
+ ffi.ptr(pcm),
2384
+ BigInt(pcm.length),
2385
+ ffi.ptr(outEmbedding),
2386
+ err.ptr,
2387
+ );
2388
+ if (rc !== ELIZA_OK) {
2389
+ const message =
2390
+ takeError(err.buf) ??
2391
+ `[ffi-bindings] eliza_inference_speaker_embed rc=${rc}`;
2392
+ throw new VoiceLifecycleError(failureCode(rc), message);
2393
+ }
2394
+ return outEmbedding;
2395
+ },
2396
+
2397
+ speakerClose(speaker) {
2398
+ loadedLib.symbols.eliza_inference_speaker_close?.(speaker);
2399
+ },
2400
+
2401
+ /* ---- Native diarizer (ABI v6) ------------------------------ */
2402
+
2403
+ diarizSupported(): boolean {
2404
+ if (
2405
+ !diarizSymbolsAvailable ||
2406
+ typeof loadedLib.symbols.eliza_inference_diariz_supported !== "function"
2407
+ ) {
2408
+ return false;
2409
+ }
2410
+ return loadedLib.symbols.eliza_inference_diariz_supported() === 1;
2411
+ },
2412
+
2413
+ diarizOpen({ ctx, ggufPath }) {
2414
+ const open = loadedLib.symbols.eliza_inference_diariz_open;
2415
+ if (!diarizSymbolsAvailable || typeof open !== "function") {
2416
+ throw new VoiceLifecycleError(
2417
+ "kernel-missing",
2418
+ "[ffi-bindings] eliza_inference_diariz_open is not exported by this libelizainference build",
2419
+ );
2420
+ }
2421
+ const err = makeOutErr();
2422
+ const ggufArg = cstr(ggufPath);
2423
+ const handle = open(ctx, ggufArg.ptr, err.ptr);
2424
+ if (isNullPointer(handle)) {
2425
+ const message =
2426
+ takeError(err.buf) ??
2427
+ "[ffi-bindings] eliza_inference_diariz_open returned NULL with no diagnostic";
2428
+ throw new VoiceLifecycleError("kernel-missing", message);
2429
+ }
2430
+ return handle as NativeDiarizHandle;
2431
+ },
2432
+
2433
+ diarizSegment({ diariz, pcm }) {
2434
+ const segment = loadedLib.symbols.eliza_inference_diariz_segment;
2435
+ if (!diarizSymbolsAvailable || typeof segment !== "function") {
2436
+ throw new VoiceLifecycleError(
2437
+ "kernel-missing",
2438
+ "[ffi-bindings] eliza_inference_diariz_segment is not exported by this libelizainference build",
2439
+ );
2440
+ }
2441
+ const err = makeOutErr();
2442
+ // The library writes `frames_per_window` (293 for pyannote-3) int8
2443
+ // labels. Pass a generous capacity and read back the actual count
2444
+ // the library writes into `*io_n_labels`.
2445
+ const outLabels = new Int8Array(DIARIZ_LABELS_CAPACITY);
2446
+ const ioNLabels = new BigUint64Array([BigInt(outLabels.length)]);
2447
+ const rc = segment(
2448
+ diariz,
2449
+ ffi.ptr(pcm),
2450
+ BigInt(pcm.length),
2451
+ ffi.ptr(outLabels),
2452
+ ffi.ptr(ioNLabels),
2453
+ err.ptr,
2454
+ );
2455
+ if (rc !== ELIZA_OK) {
2456
+ const message =
2457
+ takeError(err.buf) ??
2458
+ `[ffi-bindings] eliza_inference_diariz_segment rc=${rc}`;
2459
+ throw new VoiceLifecycleError(failureCode(rc), message);
2460
+ }
2461
+ const nFrames = Number(ioNLabels[0] ?? 0n);
2462
+ return outLabels.slice(0, Math.min(nFrames, outLabels.length));
2463
+ },
2464
+
2465
+ diarizClose(diariz) {
2466
+ loadedLib.symbols.eliza_inference_diariz_close?.(diariz);
2467
+ },
2468
+
2469
+ /* ---- Streaming ASR (ABI v2) -------------------------------- */
2470
+
2471
+ asrStreamSupported(): boolean {
2472
+ return loadedLib.symbols.eliza_inference_asr_stream_supported() === 1;
2473
+ },
2474
+
2475
+ asrStreamOpen({ ctx, sampleRateHz }) {
2476
+ const err = makeOutErr();
2477
+ const handle = loadedLib.symbols.eliza_inference_asr_stream_open(
2478
+ ctx,
2479
+ sampleRateHz,
2480
+ err.ptr,
2481
+ );
2482
+ if (isNullPointer(handle)) {
2483
+ const message =
2484
+ takeError(err.buf) ??
2485
+ "[ffi-bindings] eliza_inference_asr_stream_open returned NULL with no diagnostic";
2486
+ throw new VoiceLifecycleError("kernel-missing", message);
2487
+ }
2488
+ return handle as bigint;
2489
+ },
2490
+
2491
+ asrStreamFeed({ stream, pcm }) {
2492
+ const err = makeOutErr();
2493
+ const rc = loadedLib.symbols.eliza_inference_asr_stream_feed(
2494
+ stream,
2495
+ ffi.ptr(pcm),
2496
+ BigInt(pcm.length),
2497
+ err.ptr,
2498
+ );
2499
+ if (rc < 0) {
2500
+ const message =
2501
+ takeError(err.buf) ??
2502
+ `[ffi-bindings] eliza_inference_asr_stream_feed rc=${rc}`;
2503
+ throw new VoiceLifecycleError(failureCode(rc), message);
2504
+ }
2505
+ },
2506
+
2507
+ asrStreamPartial(args) {
2508
+ return readAsrStreamResult(
2509
+ "partial",
2510
+ loadedLib.symbols.eliza_inference_asr_stream_partial,
2511
+ args,
2512
+ );
2513
+ },
2514
+
2515
+ asrStreamFinish(args) {
2516
+ return readAsrStreamResult(
2517
+ "finish",
2518
+ loadedLib.symbols.eliza_inference_asr_stream_finish,
2519
+ args,
2520
+ );
2521
+ },
2522
+
2523
+ asrStreamClose(stream) {
2524
+ loadedLib.symbols.eliza_inference_asr_stream_close(stream);
2525
+ },
2526
+
2527
+ /* ---- Streaming LLM (additive on top of v3) ----------------- */
2528
+
2529
+ llmStreamSupported(): boolean {
2530
+ // Symbols are bound at dlopen — if the fallback path stripped them
2531
+ // out, the runtime never advertises support.
2532
+ return (
2533
+ llmStreamSymbolsAvailable &&
2534
+ typeof loadedLib.symbols.eliza_inference_llm_stream_open === "function"
2535
+ );
2536
+ },
2537
+
2538
+ llmMtpSupported(): boolean {
2539
+ // ABI v8 capability probe. Absent (or the whole probe family
2540
+ // unbound) on a v7 library → unsupported, so the fused text path
2541
+ // refuses to route MTP through it.
2542
+ const probe = loadedLib.symbols.eliza_inference_llm_mtp_supported;
2543
+ return (
2544
+ llmCapabilitySymbolsAvailable &&
2545
+ typeof probe === "function" &&
2546
+ probe() === 1
2547
+ );
2548
+ },
2549
+
2550
+ llmKvQuantSupported(): boolean {
2551
+ const probe = loadedLib.symbols.eliza_inference_llm_kv_quant_supported;
2552
+ return (
2553
+ llmCapabilitySymbolsAvailable &&
2554
+ typeof probe === "function" &&
2555
+ probe() === 1
2556
+ );
2557
+ },
2558
+
2559
+ llmStreamOpen({ ctx, config }) {
2560
+ const open = loadedLib.symbols.eliza_inference_llm_stream_open;
2561
+ if (!llmStreamSymbolsAvailable || typeof open !== "function") {
2562
+ throw new VoiceLifecycleError(
2563
+ "kernel-missing",
2564
+ "[ffi-bindings] eliza_inference_llm_stream_open is not exported by this build",
2565
+ );
2566
+ }
2567
+ const err = makeOutErr();
2568
+ // Marshal the config struct into a Buffer. Layout matches
2569
+ // `eliza_llm_stream_config_t` in `eliza-inference-ffi.h`
2570
+ // (8-byte aligned, ABI v8):
2571
+ // off 0 : i32 max_tokens
2572
+ // off 4 : f32 temperature
2573
+ // off 8 : f32 top_p
2574
+ // off 12 : i32 top_k
2575
+ // off 16 : f32 repeat_penalty
2576
+ // off 20 : i32 slot_id
2577
+ // off 24 : ptr prompt_cache_key
2578
+ // off 32 : i32 draft_min
2579
+ // off 36 : i32 draft_max
2580
+ // off 40 : ptr mtp_drafter_path
2581
+ // off 48 : ptr gbnf_grammar
2582
+ // off 56 : i32 disable_thinking
2583
+ // off 60 : i32 n_gpu_layers (ABI v8 — fills old tail pad)
2584
+ // off 64 : ptr cache_type_k (ABI v8)
2585
+ // off 72 : ptr cache_type_v (ABI v8)
2586
+ // sizeof = 80
2587
+ const buf = Buffer.alloc(80);
2588
+ buf.writeInt32LE(config.maxTokens, 0);
2589
+ buf.writeFloatLE(config.temperature, 4);
2590
+ buf.writeFloatLE(config.topP, 8);
2591
+ buf.writeInt32LE(config.topK, 12);
2592
+ buf.writeFloatLE(config.repeatPenalty, 16);
2593
+ buf.writeInt32LE(config.slotId, 20);
2594
+ const keyArg = cstr(config.promptCacheKey);
2595
+ const drafterArg = cstr(config.draftModelPath);
2596
+ const grammarArg = cstr(
2597
+ config.gbnfGrammar && config.gbnfGrammar.length > 0
2598
+ ? config.gbnfGrammar
2599
+ : null,
2600
+ );
2601
+ const cacheKArg = cstr(
2602
+ config.cacheTypeK && config.cacheTypeK.length > 0
2603
+ ? config.cacheTypeK
2604
+ : null,
2605
+ );
2606
+ const cacheVArg = cstr(
2607
+ config.cacheTypeV && config.cacheTypeV.length > 0
2608
+ ? config.cacheTypeV
2609
+ : null,
2610
+ );
2611
+ buf.writeBigUInt64LE(toPtrBigInt(keyArg.ptr), 24);
2612
+ buf.writeInt32LE(config.draftMin, 32);
2613
+ buf.writeInt32LE(config.draftMax, 36);
2614
+ buf.writeBigUInt64LE(toPtrBigInt(drafterArg.ptr), 40);
2615
+ buf.writeBigUInt64LE(toPtrBigInt(grammarArg.ptr), 48);
2616
+ buf.writeInt32LE(config.disableThinking ? 1 : 0, 56);
2617
+ // -1 = runtime default (all layers); 0 = CPU. `undefined` -> -1.
2618
+ buf.writeInt32LE(
2619
+ config.gpuLayers === undefined ? -1 : config.gpuLayers,
2620
+ 60,
2621
+ );
2622
+ buf.writeBigUInt64LE(toPtrBigInt(cacheKArg.ptr), 64);
2623
+ buf.writeBigUInt64LE(toPtrBigInt(cacheVArg.ptr), 72);
2624
+ const handle = open(ctx, ffi.ptr(buf), err.ptr);
2625
+ if (isNullPointer(handle)) {
2626
+ const message =
2627
+ takeError(err.buf) ??
2628
+ "[ffi-bindings] eliza_inference_llm_stream_open returned NULL with no diagnostic";
2629
+ throw new VoiceLifecycleError("kernel-missing", message);
2630
+ }
2631
+ return handle as LlmStreamHandle;
2632
+ },
2633
+
2634
+ llmStreamPrefill({ stream, tokens }) {
2635
+ const prefill = loadedLib.symbols.eliza_inference_llm_stream_prefill;
2636
+ if (!llmStreamSymbolsAvailable || typeof prefill !== "function") {
2637
+ throw new VoiceLifecycleError(
2638
+ "kernel-missing",
2639
+ "[ffi-bindings] eliza_inference_llm_stream_prefill is not exported by this build",
2640
+ );
2641
+ }
2642
+ const err = makeOutErr();
2643
+ const rc = prefill(
2644
+ stream,
2645
+ ffi.ptr(tokens),
2646
+ BigInt(tokens.length),
2647
+ err.ptr,
2648
+ );
2649
+ if (rc !== ELIZA_OK) {
2650
+ const message =
2651
+ takeError(err.buf) ??
2652
+ `[ffi-bindings] eliza_inference_llm_stream_prefill rc=${rc}`;
2653
+ throw new VoiceLifecycleError(failureCode(rc), message);
2654
+ }
2655
+ },
2656
+
2657
+ llmStreamNext({ stream, maxTokensPerStep, maxTextBytes }) {
2658
+ const next = loadedLib.symbols.eliza_inference_llm_stream_next;
2659
+ if (!llmStreamSymbolsAvailable || typeof next !== "function") {
2660
+ throw new VoiceLifecycleError(
2661
+ "kernel-missing",
2662
+ "[ffi-bindings] eliza_inference_llm_stream_next is not exported by this build",
2663
+ );
2664
+ }
2665
+ const err = makeOutErr();
2666
+ const tokenCap = maxTokensPerStep ?? 32;
2667
+ const textCap = maxTextBytes ?? 1024;
2668
+ const tokensOut = new Int32Array(tokenCap);
2669
+ const numTokensOut = new BigUint64Array(1);
2670
+ const textOut = new Uint8Array(textCap);
2671
+ const drafterDrafted = new Int32Array(1);
2672
+ const drafterAccepted = new Int32Array(1);
2673
+ const rc = next(
2674
+ stream,
2675
+ ffi.ptr(tokensOut),
2676
+ BigInt(tokenCap),
2677
+ ffi.ptr(numTokensOut),
2678
+ ffi.ptr(textOut),
2679
+ BigInt(textCap),
2680
+ ffi.ptr(drafterDrafted),
2681
+ ffi.ptr(drafterAccepted),
2682
+ err.ptr,
2683
+ );
2684
+ if (rc < 0) {
2685
+ const message =
2686
+ takeError(err.buf) ??
2687
+ `[ffi-bindings] eliza_inference_llm_stream_next rc=${rc}`;
2688
+ throw new VoiceLifecycleError(failureCode(rc), message);
2689
+ }
2690
+ const n = Number(numTokensOut[0] ?? 0n);
2691
+ const tokens = Array.from(tokensOut.subarray(0, Math.min(n, tokenCap)));
2692
+ const nul = textOut.indexOf(0, 0);
2693
+ const len = nul >= 0 ? nul : textCap;
2694
+ const text = Buffer.from(
2695
+ textOut.buffer,
2696
+ textOut.byteOffset,
2697
+ len,
2698
+ ).toString("utf8");
2699
+ return {
2700
+ tokens,
2701
+ text,
2702
+ done: rc === 1,
2703
+ drafterDrafted: drafterDrafted[0] ?? 0,
2704
+ drafterAccepted: drafterAccepted[0] ?? 0,
2705
+ };
2706
+ },
2707
+
2708
+ llmStreamCancel(stream) {
2709
+ const cancel = loadedLib.symbols.eliza_inference_llm_stream_cancel;
2710
+ if (!llmStreamSymbolsAvailable || typeof cancel !== "function") {
2711
+ // Cancel is best-effort — a build without the symbol just means
2712
+ // the runtime cannot interrupt mid-step. The next `_next` call
2713
+ // will still finish normally; the caller drops the result.
2714
+ return;
2715
+ }
2716
+ cancel(stream);
2717
+ },
2718
+
2719
+ llmStreamSaveSlot({ stream, filename }) {
2720
+ const save = loadedLib.symbols.eliza_inference_llm_stream_save_slot;
2721
+ if (!llmStreamSymbolsAvailable || typeof save !== "function") {
2722
+ throw new VoiceLifecycleError(
2723
+ "kernel-missing",
2724
+ "[ffi-bindings] eliza_inference_llm_stream_save_slot is not exported by this build",
2725
+ );
2726
+ }
2727
+ const err = makeOutErr();
2728
+ const fnameArg = cstr(filename);
2729
+ const rc = save(stream, fnameArg.ptr, err.ptr);
2730
+ if (rc !== ELIZA_OK) {
2731
+ const message =
2732
+ takeError(err.buf) ??
2733
+ `[ffi-bindings] eliza_inference_llm_stream_save_slot rc=${rc}`;
2734
+ throw new VoiceLifecycleError(failureCode(rc), message);
2735
+ }
2736
+ },
2737
+
2738
+ llmStreamRestoreSlot({ stream, filename }) {
2739
+ const restore = loadedLib.symbols.eliza_inference_llm_stream_restore_slot;
2740
+ if (!llmStreamSymbolsAvailable || typeof restore !== "function") {
2741
+ throw new VoiceLifecycleError(
2742
+ "kernel-missing",
2743
+ "[ffi-bindings] eliza_inference_llm_stream_restore_slot is not exported by this build",
2744
+ );
2745
+ }
2746
+ const err = makeOutErr();
2747
+ const fnameArg = cstr(filename);
2748
+ const rc = restore(stream, fnameArg.ptr, err.ptr);
2749
+ if (rc !== ELIZA_OK) {
2750
+ const message =
2751
+ takeError(err.buf) ??
2752
+ `[ffi-bindings] eliza_inference_llm_stream_restore_slot rc=${rc}`;
2753
+ throw new VoiceLifecycleError(failureCode(rc), message);
2754
+ }
2755
+ },
2756
+
2757
+ llmStreamClose(stream) {
2758
+ loadedLib.symbols.eliza_inference_llm_stream_close?.(stream);
2759
+ },
2760
+
2761
+ /* ---- Text embeddings (ABI v9) ------------------------------ */
2762
+
2763
+ embedSupported(): boolean {
2764
+ const probe = loadedLib.symbols.eliza_inference_embed_supported;
2765
+ return (
2766
+ textModalitiesSymbolsAvailable &&
2767
+ typeof probe === "function" &&
2768
+ probe() === 1
2769
+ );
2770
+ },
2771
+
2772
+ embed({ ctx, text, pooling }) {
2773
+ const embed = loadedLib.symbols.eliza_inference_embed;
2774
+ if (!textModalitiesSymbolsAvailable || typeof embed !== "function") {
2775
+ throw new VoiceLifecycleError(
2776
+ "kernel-missing",
2777
+ "[ffi-bindings] eliza_inference_embed is not exported by this build",
2778
+ );
2779
+ }
2780
+ const err = makeOutErr();
2781
+ const textArg = cstr(text);
2782
+ // The C side caps the write at n_embd. Hand it a generous buffer (the
2783
+ // largest dedicated-embedding dim we ship is 1024; 4096 covers any
2784
+ // decoder-as-embedder n_embd) and read back *out_dim for the real
2785
+ // length.
2786
+ const cap = 4096;
2787
+ const outEmbedding = new Float32Array(cap);
2788
+ const outDim = new Int32Array(1);
2789
+ const rc = embed(
2790
+ ctx,
2791
+ textArg.ptr,
2792
+ BigInt(textArg.bytes),
2793
+ pooling ?? ELIZA_POOLING_MEAN,
2794
+ ffi.ptr(outEmbedding),
2795
+ BigInt(cap),
2796
+ ffi.ptr(outDim),
2797
+ err.ptr,
2798
+ );
2799
+ if (rc !== ELIZA_OK) {
2800
+ const message =
2801
+ takeError(err.buf) ?? `[ffi-bindings] eliza_inference_embed rc=${rc}`;
2802
+ throw new VoiceLifecycleError(failureCode(rc), message);
2803
+ }
2804
+ const dim = outDim[0] ?? 0;
2805
+ if (dim <= 0 || dim > cap) {
2806
+ throw new VoiceLifecycleError(
2807
+ "kernel-missing",
2808
+ `[ffi-bindings] eliza_inference_embed returned out-of-range n_embd=${dim}`,
2809
+ );
2810
+ }
2811
+ return outEmbedding.slice(0, dim);
2812
+ },
2813
+
2814
+ /* ---- mmproj vision describe (ABI v9) ----------------------- */
2815
+
2816
+ visionSupported(): boolean {
2817
+ const probe = loadedLib.symbols.eliza_inference_vision_supported;
2818
+ return (
2819
+ textModalitiesSymbolsAvailable &&
2820
+ typeof probe === "function" &&
2821
+ probe() === 1
2822
+ );
2823
+ },
2824
+
2825
+ describeImage({ ctx, imageBytes, mmprojPath, prompt, maxTextBytes }) {
2826
+ const describe = loadedLib.symbols.eliza_inference_describe_image;
2827
+ if (!textModalitiesSymbolsAvailable || typeof describe !== "function") {
2828
+ throw new VoiceLifecycleError(
2829
+ "kernel-missing",
2830
+ "[ffi-bindings] eliza_inference_describe_image is not exported by this build",
2831
+ );
2832
+ }
2833
+ const err = makeOutErr();
2834
+ const cap = maxTextBytes ?? 4096;
2835
+ const outText = new Uint8Array(cap);
2836
+ const mmprojArg = cstr(mmprojPath);
2837
+ const promptArg = cstr(prompt ?? null);
2838
+ const rc = describe(
2839
+ ctx,
2840
+ ffi.ptr(imageBytes),
2841
+ BigInt(imageBytes.length),
2842
+ mmprojArg.ptr,
2843
+ promptArg.ptr,
2844
+ ffi.ptr(outText),
2845
+ BigInt(cap),
2846
+ err.ptr,
2847
+ );
2848
+ if (rc < 0) {
2849
+ const message =
2850
+ takeError(err.buf) ??
2851
+ `[ffi-bindings] eliza_inference_describe_image rc=${rc}`;
2852
+ throw new VoiceLifecycleError(failureCode(rc), message);
2853
+ }
2854
+ const nul = outText.indexOf(0, 0);
2855
+ const len = nul >= 0 ? nul : rc;
2856
+ return Buffer.from(outText.buffer, outText.byteOffset, len).toString(
2857
+ "utf8",
2858
+ );
2859
+ },
2860
+
2861
+ /* ---- Tokenizer (ABI v9) ------------------------------------ */
2862
+
2863
+ tokenizeSupported(): boolean {
2864
+ const probe = loadedLib.symbols.eliza_inference_tokenize_supported;
2865
+ return (
2866
+ textModalitiesSymbolsAvailable &&
2867
+ typeof probe === "function" &&
2868
+ probe() === 1
2869
+ );
2870
+ },
2871
+
2872
+ tokenize({ ctx, text, addSpecial, parseSpecial }) {
2873
+ const tokenize = loadedLib.symbols.eliza_inference_tokenize;
2874
+ const freeTokens = loadedLib.symbols.eliza_inference_free_tokens;
2875
+ if (
2876
+ !textModalitiesSymbolsAvailable ||
2877
+ typeof tokenize !== "function" ||
2878
+ typeof freeTokens !== "function"
2879
+ ) {
2880
+ throw new VoiceLifecycleError(
2881
+ "kernel-missing",
2882
+ "[ffi-bindings] eliza_inference_tokenize is not exported by this build",
2883
+ );
2884
+ }
2885
+ const err = makeOutErr();
2886
+ const textArg = cstr(text);
2887
+ // out_tokens is int** — give the library a slot to write the malloc'ed
2888
+ // pointer into, plus a size_t out for the count.
2889
+ const outTokensPtr = new BigUint64Array(1);
2890
+ const outN = new BigUint64Array(1);
2891
+ const rc = tokenize(
2892
+ ctx,
2893
+ textArg.ptr,
2894
+ BigInt(textArg.bytes),
2895
+ addSpecial === false ? 0 : 1,
2896
+ parseSpecial === true ? 1 : 0,
2897
+ ffi.ptr(outTokensPtr),
2898
+ ffi.ptr(outN),
2899
+ err.ptr,
2900
+ );
2901
+ if (rc !== ELIZA_OK) {
2902
+ const message =
2903
+ takeError(err.buf) ??
2904
+ `[ffi-bindings] eliza_inference_tokenize rc=${rc}`;
2905
+ throw new VoiceLifecycleError(failureCode(rc), message);
2906
+ }
2907
+ const n = Number(outN[0] ?? 0n);
2908
+ const tokensRaw = outTokensPtr[0] ?? 0n;
2909
+ if (n === 0) {
2910
+ // Empty token sequence — the library still returns a non-NULL
2911
+ // 1-byte buffer to free.
2912
+ if (tokensRaw !== 0n) freeTokens(tokensRaw);
2913
+ return new Int32Array(0);
2914
+ }
2915
+ try {
2916
+ const tokenBytes = n * 4;
2917
+ const tokensPtr =
2918
+ typeof tokensRaw === "bigint" ? Number(tokensRaw) : tokensRaw;
2919
+ const view = ffi.toArrayBuffer(tokensPtr, 0, tokenBytes);
2920
+ // Copy out of the library's malloc'ed buffer before freeing.
2921
+ return new Int32Array(new Uint8Array(view).slice(0, tokenBytes).buffer);
2922
+ } finally {
2923
+ freeTokens(tokensRaw);
2924
+ }
2925
+ },
2926
+
2927
+ detokenize({ ctx, tokens, removeSpecial, unparseSpecial, maxTextBytes }) {
2928
+ const detokenize = loadedLib.symbols.eliza_inference_detokenize;
2929
+ if (!textModalitiesSymbolsAvailable || typeof detokenize !== "function") {
2930
+ throw new VoiceLifecycleError(
2931
+ "kernel-missing",
2932
+ "[ffi-bindings] eliza_inference_detokenize is not exported by this build",
2933
+ );
2934
+ }
2935
+ const err = makeOutErr();
2936
+ const cap = maxTextBytes ?? 4096;
2937
+ const outText = new Uint8Array(cap);
2938
+ const rc = detokenize(
2939
+ ctx,
2940
+ ffi.ptr(tokens),
2941
+ BigInt(tokens.length),
2942
+ removeSpecial === true ? 1 : 0,
2943
+ unparseSpecial === true ? 1 : 0,
2944
+ ffi.ptr(outText),
2945
+ BigInt(cap),
2946
+ err.ptr,
2947
+ );
2948
+ if (rc < 0) {
2949
+ const message =
2950
+ takeError(err.buf) ??
2951
+ `[ffi-bindings] eliza_inference_detokenize rc=${rc}`;
2952
+ throw new VoiceLifecycleError(failureCode(rc), message);
2953
+ }
2954
+ const nul = outText.indexOf(0, 0);
2955
+ const len = nul >= 0 ? nul : rc;
2956
+ return Buffer.from(outText.buffer, outText.byteOffset, len).toString(
2957
+ "utf8",
2958
+ );
2959
+ },
2960
+
2961
+ /* ---- End-of-turn scoring (ABI v11) ------------------------- */
2962
+
2963
+ eotSupported(): boolean {
2964
+ const probe = loadedLib.symbols.eliza_inference_llm_eot_supported;
2965
+ return (
2966
+ eotSymbolsAvailable && typeof probe === "function" && probe() === 1
2967
+ );
2968
+ },
2969
+
2970
+ eotScore({ ctx, tokens, targetTokenId }) {
2971
+ const score = loadedLib.symbols.eliza_inference_llm_eot_score;
2972
+ if (!eotSymbolsAvailable || typeof score !== "function") {
2973
+ throw new VoiceLifecycleError(
2974
+ "kernel-missing",
2975
+ "[ffi-bindings] eliza_inference_llm_eot_score is not exported by this build (pre-v11)",
2976
+ );
2977
+ }
2978
+ if (tokens.length === 0) {
2979
+ throw new VoiceLifecycleError(
2980
+ "kernel-missing",
2981
+ "[ffi-bindings] eliza_inference_llm_eot_score requires a non-empty token sequence",
2982
+ );
2983
+ }
2984
+ const err = makeOutErr();
2985
+ const outTargetProb = new Float32Array(1);
2986
+ const outTopToken = new Int32Array(1);
2987
+ const outTopProb = new Float32Array(1);
2988
+ const rc = score(
2989
+ ctx,
2990
+ ffi.ptr(tokens),
2991
+ BigInt(tokens.length),
2992
+ targetTokenId,
2993
+ ffi.ptr(outTargetProb),
2994
+ ffi.ptr(outTopToken),
2995
+ ffi.ptr(outTopProb),
2996
+ err.ptr,
2997
+ );
2998
+ if (rc !== ELIZA_OK) {
2999
+ const message =
3000
+ takeError(err.buf) ??
3001
+ `[ffi-bindings] eliza_inference_llm_eot_score rc=${rc}`;
3002
+ throw new VoiceLifecycleError(failureCode(rc), message);
3003
+ }
3004
+ return {
3005
+ targetProb: outTargetProb[0] ?? 0,
3006
+ topToken: outTopToken[0] ?? -1,
3007
+ topProb: outTopProb[0] ?? 0,
3008
+ };
3009
+ },
3010
+
3011
+ /* ---- Kokoro TTS (ABI v10) ---------------------------------- */
3012
+
3013
+ kokoroSupported(): boolean {
3014
+ const probe = loadedLib.symbols.eliza_inference_kokoro_supported;
3015
+ return (
3016
+ kokoroSymbolsAvailable && typeof probe === "function" && probe() === 1
3017
+ );
3018
+ },
3019
+
3020
+ kokoroLoad({ ctx, ggufPath, voiceBinPath, styleDim }) {
3021
+ const load = loadedLib.symbols.eliza_inference_kokoro_load;
3022
+ if (!kokoroSymbolsAvailable || typeof load !== "function") {
3023
+ throw new VoiceLifecycleError(
3024
+ "kernel-missing",
3025
+ "[ffi-bindings] eliza_inference_kokoro_load is not exported by this build (pre-v10; Eliza-1 Kokoro engine not linked)",
3026
+ );
3027
+ }
3028
+ const err = makeOutErr();
3029
+ const ggufArg = cstr(ggufPath);
3030
+ const voiceArg = cstr(voiceBinPath);
3031
+ const rc = load(ctx, ggufArg.ptr, voiceArg.ptr, styleDim ?? 256, err.ptr);
3032
+ if (rc !== ELIZA_OK) {
3033
+ const message =
3034
+ takeError(err.buf) ??
3035
+ `[ffi-bindings] eliza_inference_kokoro_load rc=${rc}`;
3036
+ throw new VoiceLifecycleError(failureCode(rc), message);
3037
+ }
3038
+ },
3039
+
3040
+ kokoroSynthesize({ ctx, text, speed, maxSamples }) {
3041
+ const synth = loadedLib.symbols.eliza_inference_kokoro_synthesize;
3042
+ if (!kokoroSymbolsAvailable || typeof synth !== "function") {
3043
+ throw new VoiceLifecycleError(
3044
+ "kernel-missing",
3045
+ "[ffi-bindings] eliza_inference_kokoro_synthesize is not exported by this build",
3046
+ );
3047
+ }
3048
+ const err = makeOutErr();
3049
+ const textArg = cstr(text);
3050
+ const outPcm = new Float32Array(maxSamples);
3051
+ const rc = synth(
3052
+ ctx,
3053
+ textArg.ptr,
3054
+ BigInt(textArg.bytes),
3055
+ speed ?? 1.0,
3056
+ ffi.ptr(outPcm),
3057
+ BigInt(maxSamples),
3058
+ err.ptr,
3059
+ );
3060
+ if (rc < 0) {
3061
+ const message =
3062
+ takeError(err.buf) ??
3063
+ `[ffi-bindings] eliza_inference_kokoro_synthesize rc=${rc}`;
3064
+ throw new VoiceLifecycleError(failureCode(rc), message);
3065
+ }
3066
+ return outPcm.slice(0, Math.min(rc, maxSamples));
3067
+ },
3068
+
3069
+ kokoroSampleRate(ctx): number {
3070
+ const rate = loadedLib.symbols.eliza_inference_kokoro_sample_rate;
3071
+ if (!kokoroSymbolsAvailable || typeof rate !== "function") {
3072
+ throw new VoiceLifecycleError(
3073
+ "kernel-missing",
3074
+ "[ffi-bindings] eliza_inference_kokoro_sample_rate is not exported by this build",
3075
+ );
3076
+ }
3077
+ const rc = rate(ctx);
3078
+ if (rc < 0) {
3079
+ throw new VoiceLifecycleError(
3080
+ failureCode(rc),
3081
+ `[ffi-bindings] eliza_inference_kokoro_sample_rate rc=${rc} (no Kokoro model loaded on this ctx)`,
3082
+ );
3083
+ }
3084
+ return rc;
3085
+ },
3086
+
3087
+ close(): void {
3088
+ loadedLib.close();
3089
+ },
3090
+ };
3091
+
3092
+ /**
3093
+ * Convert a Bun-FFI pointer value (`unknown` per the lazy types) to the
3094
+ * bigint the marshalled config struct stores in its `const char *`
3095
+ * slots. NULL inputs translate to `0n`. Used by `llmStreamOpen` to
3096
+ * inline the cstr pointers into the config buffer.
3097
+ */
3098
+ function toPtrBigInt(value: unknown): bigint {
3099
+ if (value === null || value === undefined) return 0n;
3100
+ if (typeof value === "bigint") return value;
3101
+ if (typeof value === "number") return BigInt(value);
3102
+ // Bun returns its internal pointer object that coerces to bigint.
3103
+ return BigInt(value as unknown as number);
3104
+ }
3105
+
3106
+ /**
3107
+ * Shared body for `asr_stream_partial` / `asr_stream_finish` — both
3108
+ * have the same 6-arg shape (`stream, out_text, max_text_bytes,
3109
+ * out_tokens, io_n_tokens, out_error`). Token ids are read only when
3110
+ * the caller asks for them (`maxTokens > 0`); otherwise the
3111
+ * out_tokens / io_n_tokens pointers are NULL.
3112
+ */
3113
+ function readAsrStreamResult(
3114
+ label: string,
3115
+ fn: (
3116
+ stream: bigint,
3117
+ outText: unknown,
3118
+ maxTextBytes: bigint | number,
3119
+ outTokens: unknown,
3120
+ ioNTokens: unknown,
3121
+ outErr: unknown,
3122
+ ) => number,
3123
+ args: { stream: bigint; maxTextBytes?: number; maxTokens?: number },
3124
+ ): { partial: string; tokens?: number[] } {
3125
+ const err = makeOutErr();
3126
+ const textCap = args.maxTextBytes ?? 4096;
3127
+ const outText = new Uint8Array(textCap);
3128
+ const wantTokens = (args.maxTokens ?? 0) > 0;
3129
+ const tokenCap = wantTokens ? (args.maxTokens as number) : 0;
3130
+ const outTokens = wantTokens ? new Int32Array(tokenCap) : null;
3131
+ const ioNTokens = wantTokens
3132
+ ? new BigUint64Array([BigInt(tokenCap)])
3133
+ : null;
3134
+ const rc = fn(
3135
+ args.stream,
3136
+ ffi.ptr(outText),
3137
+ BigInt(textCap),
3138
+ outTokens ? ffi.ptr(outTokens) : null,
3139
+ ioNTokens ? ffi.ptr(ioNTokens) : null,
3140
+ err.ptr,
3141
+ );
3142
+ if (rc < 0) {
3143
+ const message =
3144
+ takeError(err.buf) ??
3145
+ `[ffi-bindings] eliza_inference_asr_stream_${label} rc=${rc}`;
3146
+ throw new VoiceLifecycleError(failureCode(rc), message);
3147
+ }
3148
+ const nul = outText.indexOf(0, 0);
3149
+ const len = nul >= 0 ? nul : rc;
3150
+ const partial = Buffer.from(
3151
+ outText.buffer,
3152
+ outText.byteOffset,
3153
+ len,
3154
+ ).toString("utf8");
3155
+ if (wantTokens && outTokens && ioNTokens) {
3156
+ const n = Number(ioNTokens[0] ?? 0n);
3157
+ const tokens = Array.from(outTokens.subarray(0, Math.min(n, tokenCap)));
3158
+ return { partial, tokens };
3159
+ }
3160
+ return { partial };
3161
+ }
3162
+ }
3163
+
3164
+ function formatFfiError(err: unknown): string {
3165
+ if (err instanceof Error) {
3166
+ return err.message;
3167
+ }
3168
+ return String(err);
3169
+ }
3170
+
3171
+ /**
3172
+ * Read an `EliVerifierEvent` (see `ffi.h`) from a C struct pointer.
3173
+ * Layout on 64-bit (8-byte aligned, default packing):
3174
+ * off 0 : const int* accepted_token_ids (8)
3175
+ * off 8 : size_t n_accepted (8)
3176
+ * off 16 : int rejected_from (4)
3177
+ * off 20 : int rejected_to (4)
3178
+ * off 24 : const int* corrected_token_ids (8)
3179
+ * off 32 : size_t n_corrected (8)
3180
+ */
3181
+ function readVerifierEvent(
3182
+ evPtr: bigint,
3183
+ ffi: BunFfiModule,
3184
+ ): NativeVerifierEvent {
3185
+ const acceptedPtr = ffi.read.ptr(evPtr, 0);
3186
+ const nAccepted = Number(ffi.read.u64(evPtr, 8));
3187
+ const rejectedFrom = ffi.read.i32(evPtr, 16);
3188
+ const rejectedTo = ffi.read.i32(evPtr, 20);
3189
+ const correctedPtr = ffi.read.ptr(evPtr, 24);
3190
+ const nCorrected = Number(ffi.read.u64(evPtr, 32));
3191
+ return {
3192
+ acceptedTokenIds: readInt32Array(acceptedPtr, nAccepted, ffi),
3193
+ rejectedFrom,
3194
+ rejectedTo,
3195
+ correctedTokenIds: readInt32Array(correctedPtr, nCorrected, ffi),
3196
+ };
3197
+ }
3198
+
3199
+ function readInt32Array(
3200
+ ptr: bigint,
3201
+ count: number,
3202
+ ffi: BunFfiModule,
3203
+ ): number[] {
3204
+ if (ptr === 0n || count <= 0) return [];
3205
+ // Copy out — the array is the library's, valid only for the callback.
3206
+ const view = new Int32Array(ffi.toArrayBuffer(ptr, 0, count * 4).slice(0));
3207
+ return Array.from(view);
3208
+ }
3209
+
3210
+ /**
3211
+ * Decode a `T.cstring` return value (Bun returns these as either a
3212
+ * lazy string-like object with `toString()` or a JS string depending
3213
+ * on version). Wrap so the caller never has to branch.
3214
+ */
3215
+ function readCString(value: unknown, ffi: BunFfiModule): string {
3216
+ if (typeof value === "string") return value;
3217
+ if (value === null || value === undefined) return "";
3218
+ if (typeof value === "object" && value !== null && "toString" in value) {
3219
+ return (value as { toString(): string }).toString();
3220
+ }
3221
+ if (typeof value === "number" || typeof value === "bigint") {
3222
+ return new ffi.CString(value).toString();
3223
+ }
3224
+ return String(value);
3225
+ }