@pipedog/core 0.0.0 โ†’ 0.1.0

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 (698) hide show
  1. package/LICENSE +25 -0
  2. package/NOTICE +9 -0
  3. package/dist/adapters/base_llm_adapter.d.ts +266 -0
  4. package/dist/adapters/base_llm_adapter.js +489 -0
  5. package/dist/adapters/index.d.ts +2 -0
  6. package/dist/adapters/index.js +8 -0
  7. package/dist/adapters/schemas/direct_function.d.ts +202 -0
  8. package/dist/adapters/schemas/direct_function.js +302 -0
  9. package/dist/adapters/schemas/function_schema.d.ts +67 -0
  10. package/dist/adapters/schemas/function_schema.js +72 -0
  11. package/dist/adapters/schemas/index.d.ts +2 -0
  12. package/dist/adapters/schemas/index.js +8 -0
  13. package/dist/adapters/schemas/tools_schema.d.ts +57 -0
  14. package/dist/adapters/schemas/tools_schema.js +79 -0
  15. package/dist/adapters/services/index.d.ts +2 -0
  16. package/dist/adapters/services/index.js +8 -0
  17. package/dist/adapters/services/open_ai_adapter.d.ts +147 -0
  18. package/dist/adapters/services/open_ai_adapter.js +250 -0
  19. package/dist/audio/dtmf/dtmf_data.d.ts +3 -0
  20. package/dist/audio/dtmf/dtmf_data.js +22 -0
  21. package/dist/audio/dtmf/index.d.ts +2 -0
  22. package/dist/audio/dtmf/index.js +8 -0
  23. package/dist/audio/dtmf/types.d.ts +31 -0
  24. package/dist/audio/dtmf/types.js +35 -0
  25. package/dist/audio/dtmf/utils.d.ts +13 -0
  26. package/dist/audio/dtmf/utils.js +62 -0
  27. package/dist/audio/filters/base_audio_filter.d.ts +51 -0
  28. package/dist/audio/filters/base_audio_filter.js +17 -0
  29. package/dist/audio/filters/index.d.ts +2 -0
  30. package/dist/audio/filters/index.js +8 -0
  31. package/dist/audio/index.d.ts +2 -0
  32. package/dist/audio/index.js +8 -0
  33. package/dist/audio/krisp_instance.d.ts +95 -0
  34. package/dist/audio/krisp_instance.js +229 -0
  35. package/dist/audio/mixers/base_audio_mixer.d.ts +56 -0
  36. package/dist/audio/mixers/base_audio_mixer.js +16 -0
  37. package/dist/audio/mixers/index.d.ts +2 -0
  38. package/dist/audio/mixers/index.js +8 -0
  39. package/dist/audio/resamplers/base_audio_resampler.d.ts +46 -0
  40. package/dist/audio/resamplers/base_audio_resampler.js +35 -0
  41. package/dist/audio/resamplers/index.d.ts +2 -0
  42. package/dist/audio/resamplers/index.js +8 -0
  43. package/dist/audio/resamplers/resampy_resampler.d.ts +10 -0
  44. package/dist/audio/resamplers/resampy_resampler.js +15 -0
  45. package/dist/audio/resamplers/soxr_resampler.d.ts +54 -0
  46. package/dist/audio/resamplers/soxr_resampler.js +69 -0
  47. package/dist/audio/resamplers/soxr_stream_resampler.d.ts +75 -0
  48. package/dist/audio/resamplers/soxr_stream_resampler.js +146 -0
  49. package/dist/audio/turn/base_turn_analyzer.d.ts +84 -0
  50. package/dist/audio/turn/base_turn_analyzer.js +61 -0
  51. package/dist/audio/turn/index.d.ts +2 -0
  52. package/dist/audio/turn/index.js +8 -0
  53. package/dist/audio/turn/smart_turn/_whisper_features.d.ts +30 -0
  54. package/dist/audio/turn/smart_turn/_whisper_features.js +225 -0
  55. package/dist/audio/turn/smart_turn/base_smart_turn.d.ts +94 -0
  56. package/dist/audio/turn/smart_turn/base_smart_turn.js +239 -0
  57. package/dist/audio/turn/smart_turn/local_smart_turn_v3.d.ts +76 -0
  58. package/dist/audio/turn/smart_turn/local_smart_turn_v3.js +143 -0
  59. package/dist/audio/utils.d.ts +224 -0
  60. package/dist/audio/utils.js +634 -0
  61. package/dist/audio/vad/data/index.d.ts +2 -0
  62. package/dist/audio/vad/data/index.js +8 -0
  63. package/dist/audio/vad/index.d.ts +2 -0
  64. package/dist/audio/vad/index.js +8 -0
  65. package/dist/audio/vad/vad_analyzer.d.ts +126 -0
  66. package/dist/audio/vad/vad_analyzer.js +211 -0
  67. package/dist/audio/vad/vad_controller.d.ts +143 -0
  68. package/dist/audio/vad/vad_controller.js +228 -0
  69. package/dist/audio/volume.d.ts +38 -0
  70. package/dist/audio/volume.js +75 -0
  71. package/dist/bus/adapters/base.d.ts +35 -0
  72. package/dist/bus/adapters/base.js +19 -0
  73. package/dist/bus/adapters/index.d.ts +10 -0
  74. package/dist/bus/adapters/index.js +15 -0
  75. package/dist/bus/adapters/llm_context_adapter.d.ts +36 -0
  76. package/dist/bus/adapters/llm_context_adapter.js +82 -0
  77. package/dist/bus/adapters/tools_schema_adapter.d.ts +25 -0
  78. package/dist/bus/adapters/tools_schema_adapter.js +70 -0
  79. package/dist/bus/bridge_processor.d.ts +111 -0
  80. package/dist/bus/bridge_processor.js +166 -0
  81. package/dist/bus/bus.d.ts +110 -0
  82. package/dist/bus/bus.js +200 -0
  83. package/dist/bus/index.d.ts +22 -0
  84. package/dist/bus/index.js +27 -0
  85. package/dist/bus/local/async_queue.d.ts +12 -0
  86. package/dist/bus/local/async_queue.js +21 -0
  87. package/dist/bus/local/index.d.ts +3 -0
  88. package/dist/bus/local/index.js +8 -0
  89. package/dist/bus/messages.d.ts +378 -0
  90. package/dist/bus/messages.js +357 -0
  91. package/dist/bus/network/index.d.ts +16 -0
  92. package/dist/bus/network/index.js +21 -0
  93. package/dist/bus/queue.d.ts +27 -0
  94. package/dist/bus/queue.js +51 -0
  95. package/dist/bus/serializers/base.d.ts +25 -0
  96. package/dist/bus/serializers/base.js +14 -0
  97. package/dist/bus/serializers/index.d.ts +9 -0
  98. package/dist/bus/serializers/index.js +14 -0
  99. package/dist/bus/serializers/json.d.ts +77 -0
  100. package/dist/bus/serializers/json.js +367 -0
  101. package/dist/bus/subscriber.d.ts +46 -0
  102. package/dist/bus/subscriber.js +27 -0
  103. package/dist/bus/ui/index.d.ts +3 -0
  104. package/dist/bus/ui/index.js +8 -0
  105. package/dist/bus/ui/messages.d.ts +186 -0
  106. package/dist/bus/ui/messages.js +171 -0
  107. package/dist/classifiers/base_classifier.d.ts +263 -0
  108. package/dist/classifiers/base_classifier.js +346 -0
  109. package/dist/classifiers/cloudflare/clef/classifier.d.ts +70 -0
  110. package/dist/classifiers/cloudflare/clef/classifier.js +204 -0
  111. package/dist/classifiers/cloudflare/clef/client.d.ts +104 -0
  112. package/dist/classifiers/cloudflare/clef/client.js +234 -0
  113. package/dist/classifiers/cloudflare/clef/index.d.ts +2 -0
  114. package/dist/classifiers/cloudflare/clef/index.js +8 -0
  115. package/dist/classifiers/cloudflare/index.d.ts +2 -0
  116. package/dist/classifiers/cloudflare/index.js +8 -0
  117. package/dist/classifiers/http_client.d.ts +70 -0
  118. package/dist/classifiers/http_client.js +161 -0
  119. package/dist/classifiers/index.d.ts +2 -0
  120. package/dist/classifiers/index.js +8 -0
  121. package/dist/classifiers/jev/classifier.d.ts +8 -0
  122. package/dist/classifiers/jev/classifier.js +13 -0
  123. package/dist/classifiers/jev/client.d.ts +8 -0
  124. package/dist/classifiers/jev/client.js +13 -0
  125. package/dist/classifiers/jev/index.d.ts +2 -0
  126. package/dist/classifiers/jev/index.js +8 -0
  127. package/dist/classifiers/llm/classifier.d.ts +72 -0
  128. package/dist/classifiers/llm/classifier.js +304 -0
  129. package/dist/classifiers/llm/index.d.ts +2 -0
  130. package/dist/classifiers/llm/index.js +8 -0
  131. package/dist/classifiers/openai/decisions/classifier.d.ts +77 -0
  132. package/dist/classifiers/openai/decisions/classifier.js +241 -0
  133. package/dist/classifiers/openai/decisions/client.d.ts +108 -0
  134. package/dist/classifiers/openai/decisions/client.js +234 -0
  135. package/dist/classifiers/openai/decisions/index.d.ts +2 -0
  136. package/dist/classifiers/openai/decisions/index.js +8 -0
  137. package/dist/classifiers/openai/index.d.ts +2 -0
  138. package/dist/classifiers/openai/index.js +8 -0
  139. package/dist/classifiers/typesafe/index.d.ts +2 -0
  140. package/dist/classifiers/typesafe/index.js +8 -0
  141. package/dist/classifiers/typesafe/jev/classifier.d.ts +70 -0
  142. package/dist/classifiers/typesafe/jev/classifier.js +220 -0
  143. package/dist/classifiers/typesafe/jev/client.d.ts +105 -0
  144. package/dist/classifiers/typesafe/jev/client.js +233 -0
  145. package/dist/classifiers/typesafe/jev/index.d.ts +2 -0
  146. package/dist/classifiers/typesafe/jev/index.js +8 -0
  147. package/dist/clocks/base_clock.d.ts +13 -0
  148. package/dist/clocks/base_clock.js +14 -0
  149. package/dist/clocks/index.d.ts +2 -0
  150. package/dist/clocks/index.js +8 -0
  151. package/dist/clocks/system_clock.d.ts +15 -0
  152. package/dist/clocks/system_clock.js +25 -0
  153. package/dist/frames/frames.d.ts +2533 -0
  154. package/dist/frames/frames.js +2485 -0
  155. package/dist/frames/index.d.ts +2 -0
  156. package/dist/frames/index.js +8 -0
  157. package/dist/frames/protobufs/frames_pb2.d.ts +229 -0
  158. package/dist/frames/protobufs/frames_pb2.js +466 -0
  159. package/dist/index.d.ts +11 -0
  160. package/dist/index.js +56 -0
  161. package/dist/metrics/index.d.ts +2 -0
  162. package/dist/metrics/index.js +8 -0
  163. package/dist/metrics/metrics.d.ts +276 -0
  164. package/dist/metrics/metrics.js +254 -0
  165. package/dist/observers/base_observer.d.ts +173 -0
  166. package/dist/observers/base_observer.js +151 -0
  167. package/dist/observers/error_observer.d.ts +75 -0
  168. package/dist/observers/error_observer.js +87 -0
  169. package/dist/observers/function_call_observer.d.ts +157 -0
  170. package/dist/observers/function_call_observer.js +222 -0
  171. package/dist/observers/index.d.ts +2 -0
  172. package/dist/observers/index.js +8 -0
  173. package/dist/observers/loggers/debug_log_observer.d.ts +91 -0
  174. package/dist/observers/loggers/debug_log_observer.js +228 -0
  175. package/dist/observers/loggers/index.d.ts +2 -0
  176. package/dist/observers/loggers/index.js +8 -0
  177. package/dist/observers/loggers/llm_log_observer.d.ts +24 -0
  178. package/dist/observers/loggers/llm_log_observer.js +110 -0
  179. package/dist/observers/loggers/metrics_log_observer.d.ts +85 -0
  180. package/dist/observers/loggers/metrics_log_observer.js +186 -0
  181. package/dist/observers/loggers/transcription_log_observer.d.ts +23 -0
  182. package/dist/observers/loggers/transcription_log_observer.js +74 -0
  183. package/dist/observers/service_metrics_observer.d.ts +168 -0
  184. package/dist/observers/service_metrics_observer.js +236 -0
  185. package/dist/observers/speaking_observer.d.ts +104 -0
  186. package/dist/observers/speaking_observer.js +150 -0
  187. package/dist/observers/startup_timing_observer.d.ts +221 -0
  188. package/dist/observers/startup_timing_observer.js +365 -0
  189. package/dist/observers/turn_tracking_observer.d.ts +78 -0
  190. package/dist/observers/turn_tracking_observer.js +189 -0
  191. package/dist/observers/user_bot_latency_observer.d.ts +411 -0
  192. package/dist/observers/user_bot_latency_observer.js +974 -0
  193. package/dist/pipeline/base_pipeline.d.ts +49 -0
  194. package/dist/pipeline/base_pipeline.js +101 -0
  195. package/dist/pipeline/capabilities.d.ts +76 -0
  196. package/dist/pipeline/capabilities.js +97 -0
  197. package/dist/pipeline/index.d.ts +2 -0
  198. package/dist/pipeline/index.js +8 -0
  199. package/dist/pipeline/job_context.d.ts +317 -0
  200. package/dist/pipeline/job_context.js +450 -0
  201. package/dist/pipeline/job_decorator.d.ts +54 -0
  202. package/dist/pipeline/job_decorator.js +92 -0
  203. package/dist/pipeline/llm_switcher.d.ts +84 -0
  204. package/dist/pipeline/llm_switcher.js +107 -0
  205. package/dist/pipeline/llm_with_backend.d.ts +435 -0
  206. package/dist/pipeline/llm_with_backend.js +806 -0
  207. package/dist/pipeline/parallel_pipeline.d.ts +83 -0
  208. package/dist/pipeline/parallel_pipeline.js +200 -0
  209. package/dist/pipeline/pipeline.d.ts +126 -0
  210. package/dist/pipeline/pipeline.js +196 -0
  211. package/dist/pipeline/service_switcher.d.ts +287 -0
  212. package/dist/pipeline/service_switcher.js +509 -0
  213. package/dist/pipeline/sync_parallel_pipeline.d.ts +192 -0
  214. package/dist/pipeline/sync_parallel_pipeline.js +387 -0
  215. package/dist/pipeline/worker.d.ts +814 -0
  216. package/dist/pipeline/worker.js +1639 -0
  217. package/dist/pipeline/worker_observer.d.ts +131 -0
  218. package/dist/pipeline/worker_observer.js +274 -0
  219. package/dist/pipeline/worker_ready_decorator.d.ts +45 -0
  220. package/dist/pipeline/worker_ready_decorator.js +76 -0
  221. package/dist/processors/aggregators/async_tool_messages.d.ts +146 -0
  222. package/dist/processors/aggregators/async_tool_messages.js +291 -0
  223. package/dist/processors/aggregators/dtmf_aggregator.d.ts +61 -0
  224. package/dist/processors/aggregators/dtmf_aggregator.js +144 -0
  225. package/dist/processors/aggregators/gated.d.ts +43 -0
  226. package/dist/processors/aggregators/gated.js +86 -0
  227. package/dist/processors/aggregators/gated_llm_context.d.ts +42 -0
  228. package/dist/processors/aggregators/gated_llm_context.js +86 -0
  229. package/dist/processors/aggregators/index.d.ts +2 -0
  230. package/dist/processors/aggregators/index.js +8 -0
  231. package/dist/processors/aggregators/llm_context.d.ts +370 -0
  232. package/dist/processors/aggregators/llm_context.js +580 -0
  233. package/dist/processors/aggregators/llm_context_summarizer.d.ts +219 -0
  234. package/dist/processors/aggregators/llm_context_summarizer.js +447 -0
  235. package/dist/processors/aggregators/llm_response.d.ts +37 -0
  236. package/dist/processors/aggregators/llm_response.js +71 -0
  237. package/dist/processors/aggregators/llm_response_universal.d.ts +966 -0
  238. package/dist/processors/aggregators/llm_response_universal.js +2394 -0
  239. package/dist/processors/aggregators/llm_text_processor.d.ts +53 -0
  240. package/dist/processors/aggregators/llm_text_processor.js +102 -0
  241. package/dist/processors/aggregators/sentence.d.ts +39 -0
  242. package/dist/processors/aggregators/sentence.js +69 -0
  243. package/dist/processors/async_generator.d.ts +42 -0
  244. package/dist/processors/async_generator.js +76 -0
  245. package/dist/processors/audio/audio_buffer_processor.d.ts +241 -0
  246. package/dist/processors/audio/audio_buffer_processor.js +565 -0
  247. package/dist/processors/audio/index.d.ts +2 -0
  248. package/dist/processors/audio/index.js +8 -0
  249. package/dist/processors/audio/vad_processor.d.ts +67 -0
  250. package/dist/processors/audio/vad_processor.js +103 -0
  251. package/dist/processors/consumer_processor.d.ts +49 -0
  252. package/dist/processors/consumer_processor.js +85 -0
  253. package/dist/processors/filters/frame_filter.d.ts +39 -0
  254. package/dist/processors/filters/frame_filter.js +52 -0
  255. package/dist/processors/filters/function_filter.d.ts +59 -0
  256. package/dist/processors/filters/function_filter.js +78 -0
  257. package/dist/processors/filters/identity_filter.d.ts +33 -0
  258. package/dist/processors/filters/identity_filter.js +37 -0
  259. package/dist/processors/filters/index.d.ts +2 -0
  260. package/dist/processors/filters/index.js +8 -0
  261. package/dist/processors/filters/null_filter.d.ts +34 -0
  262. package/dist/processors/filters/null_filter.js +47 -0
  263. package/dist/processors/filters/wake_notifier_filter.d.ts +48 -0
  264. package/dist/processors/filters/wake_notifier_filter.js +49 -0
  265. package/dist/processors/frame_processor.d.ts +360 -0
  266. package/dist/processors/frame_processor.js +932 -0
  267. package/dist/processors/frameworks/index.d.ts +2 -0
  268. package/dist/processors/frameworks/index.js +8 -0
  269. package/dist/processors/frameworks/rtvi/frames.d.ts +241 -0
  270. package/dist/processors/frameworks/rtvi/frames.js +259 -0
  271. package/dist/processors/frameworks/rtvi/index.d.ts +6 -0
  272. package/dist/processors/frameworks/rtvi/index.js +11 -0
  273. package/dist/processors/frameworks/rtvi/models.d.ts +1741 -0
  274. package/dist/processors/frameworks/rtvi/models.js +1807 -0
  275. package/dist/processors/frameworks/rtvi/observer.d.ts +340 -0
  276. package/dist/processors/frameworks/rtvi/observer.js +1113 -0
  277. package/dist/processors/frameworks/rtvi/processor.d.ts +182 -0
  278. package/dist/processors/frameworks/rtvi/processor.js +676 -0
  279. package/dist/processors/idle_frame_processor.d.ts +44 -0
  280. package/dist/processors/idle_frame_processor.js +86 -0
  281. package/dist/processors/index.d.ts +2 -0
  282. package/dist/processors/index.js +8 -0
  283. package/dist/processors/logger.d.ts +42 -0
  284. package/dist/processors/logger.js +87 -0
  285. package/dist/processors/metrics/frame_processor_metrics.d.ts +172 -0
  286. package/dist/processors/metrics/frame_processor_metrics.js +372 -0
  287. package/dist/processors/metrics/index.d.ts +2 -0
  288. package/dist/processors/metrics/index.js +8 -0
  289. package/dist/processors/producer_processor.d.ts +68 -0
  290. package/dist/processors/producer_processor.js +78 -0
  291. package/dist/processors/text_transformer.d.ts +30 -0
  292. package/dist/processors/text_transformer.js +48 -0
  293. package/dist/registry/index.d.ts +3 -0
  294. package/dist/registry/index.js +8 -0
  295. package/dist/registry/registry.d.ts +69 -0
  296. package/dist/registry/registry.js +149 -0
  297. package/dist/registry/types.d.ts +47 -0
  298. package/dist/registry/types.js +39 -0
  299. package/dist/serializers/base_serializer.d.ts +94 -0
  300. package/dist/serializers/base_serializer.js +82 -0
  301. package/dist/serializers/index.d.ts +2 -0
  302. package/dist/serializers/index.js +8 -0
  303. package/dist/services/ai_service.d.ts +145 -0
  304. package/dist/services/ai_service.js +262 -0
  305. package/dist/services/anthropic/index.d.ts +2 -0
  306. package/dist/services/anthropic/index.js +7 -0
  307. package/dist/services/assemblyai/index.d.ts +2 -0
  308. package/dist/services/assemblyai/index.js +7 -0
  309. package/dist/services/asyncai/index.d.ts +2 -0
  310. package/dist/services/asyncai/index.js +7 -0
  311. package/dist/services/aws/index.d.ts +2 -0
  312. package/dist/services/aws/index.js +7 -0
  313. package/dist/services/azure/index.d.ts +2 -0
  314. package/dist/services/azure/index.js +7 -0
  315. package/dist/services/baseten/index.d.ts +2 -0
  316. package/dist/services/baseten/index.js +7 -0
  317. package/dist/services/bland/index.d.ts +2 -0
  318. package/dist/services/bland/index.js +7 -0
  319. package/dist/services/camb/index.d.ts +2 -0
  320. package/dist/services/camb/index.js +7 -0
  321. package/dist/services/cartesia/index.d.ts +2 -0
  322. package/dist/services/cartesia/index.js +7 -0
  323. package/dist/services/cerebras/index.d.ts +2 -0
  324. package/dist/services/cerebras/index.js +7 -0
  325. package/dist/services/crusoe/index.d.ts +2 -0
  326. package/dist/services/crusoe/index.js +7 -0
  327. package/dist/services/deepgram/index.d.ts +2 -0
  328. package/dist/services/deepgram/index.js +7 -0
  329. package/dist/services/deepseek/index.d.ts +2 -0
  330. package/dist/services/deepseek/index.js +7 -0
  331. package/dist/services/elevenlabs/index.d.ts +2 -0
  332. package/dist/services/elevenlabs/index.js +7 -0
  333. package/dist/services/fal/index.d.ts +2 -0
  334. package/dist/services/fal/index.js +7 -0
  335. package/dist/services/fireworks/index.d.ts +2 -0
  336. package/dist/services/fireworks/index.js +7 -0
  337. package/dist/services/fish/index.d.ts +2 -0
  338. package/dist/services/fish/index.js +7 -0
  339. package/dist/services/funasr/index.d.ts +3 -0
  340. package/dist/services/funasr/index.js +8 -0
  341. package/dist/services/gladia/index.d.ts +2 -0
  342. package/dist/services/gladia/index.js +7 -0
  343. package/dist/services/google/index.d.ts +2 -0
  344. package/dist/services/google/index.js +7 -0
  345. package/dist/services/gradium/index.d.ts +2 -0
  346. package/dist/services/gradium/index.js +7 -0
  347. package/dist/services/grok/index.d.ts +2 -0
  348. package/dist/services/grok/index.js +7 -0
  349. package/dist/services/groq/index.d.ts +2 -0
  350. package/dist/services/groq/index.js +7 -0
  351. package/dist/services/heygen/index.d.ts +2 -0
  352. package/dist/services/heygen/index.js +7 -0
  353. package/dist/services/hume/index.d.ts +2 -0
  354. package/dist/services/hume/index.js +7 -0
  355. package/dist/services/image_service.d.ts +52 -0
  356. package/dist/services/image_service.js +60 -0
  357. package/dist/services/inception/index.d.ts +2 -0
  358. package/dist/services/inception/index.js +7 -0
  359. package/dist/services/index.d.ts +2 -0
  360. package/dist/services/index.js +8 -0
  361. package/dist/services/inworld/index.d.ts +2 -0
  362. package/dist/services/inworld/index.js +7 -0
  363. package/dist/services/keenable/index.d.ts +2 -0
  364. package/dist/services/keenable/index.js +7 -0
  365. package/dist/services/kokoro/index.d.ts +2 -0
  366. package/dist/services/kokoro/index.js +7 -0
  367. package/dist/services/llm_service.d.ts +1106 -0
  368. package/dist/services/llm_service.js +2231 -0
  369. package/dist/services/mem0/index.d.ts +2 -0
  370. package/dist/services/mem0/index.js +7 -0
  371. package/dist/services/meta/index.d.ts +2 -0
  372. package/dist/services/meta/index.js +7 -0
  373. package/dist/services/minimax/index.d.ts +2 -0
  374. package/dist/services/minimax/index.js +7 -0
  375. package/dist/services/mistral/index.d.ts +2 -0
  376. package/dist/services/mistral/index.js +7 -0
  377. package/dist/services/moondream/index.d.ts +2 -0
  378. package/dist/services/moondream/index.js +7 -0
  379. package/dist/services/moonshine/index.d.ts +2 -0
  380. package/dist/services/moonshine/index.js +7 -0
  381. package/dist/services/nebius/index.d.ts +2 -0
  382. package/dist/services/nebius/index.js +7 -0
  383. package/dist/services/neuphonic/index.d.ts +2 -0
  384. package/dist/services/neuphonic/index.js +7 -0
  385. package/dist/services/novita/index.d.ts +2 -0
  386. package/dist/services/novita/index.js +7 -0
  387. package/dist/services/nvidia/index.d.ts +2 -0
  388. package/dist/services/nvidia/index.js +11 -0
  389. package/dist/services/ollama/index.d.ts +2 -0
  390. package/dist/services/ollama/index.js +7 -0
  391. package/dist/services/openai/index.d.ts +2 -0
  392. package/dist/services/openai/index.js +7 -0
  393. package/dist/services/openclaw/index.d.ts +14 -0
  394. package/dist/services/openclaw/index.js +19 -0
  395. package/dist/services/openrouter/index.d.ts +2 -0
  396. package/dist/services/openrouter/index.js +7 -0
  397. package/dist/services/perplexity/index.d.ts +2 -0
  398. package/dist/services/perplexity/index.js +7 -0
  399. package/dist/services/piper/index.d.ts +2 -0
  400. package/dist/services/piper/index.js +7 -0
  401. package/dist/services/pocket_tts/index.d.ts +2 -0
  402. package/dist/services/pocket_tts/index.js +7 -0
  403. package/dist/services/qwen/index.d.ts +2 -0
  404. package/dist/services/qwen/index.js +7 -0
  405. package/dist/services/resembleai/index.d.ts +2 -0
  406. package/dist/services/resembleai/index.js +7 -0
  407. package/dist/services/rime/index.d.ts +2 -0
  408. package/dist/services/rime/index.js +7 -0
  409. package/dist/services/sambanova/index.d.ts +2 -0
  410. package/dist/services/sambanova/index.js +7 -0
  411. package/dist/services/sarvam/index.d.ts +2 -0
  412. package/dist/services/sarvam/index.js +7 -0
  413. package/dist/services/settings.d.ts +356 -0
  414. package/dist/services/settings.js +437 -0
  415. package/dist/services/simli/index.d.ts +2 -0
  416. package/dist/services/simli/index.js +7 -0
  417. package/dist/services/smallest/index.d.ts +2 -0
  418. package/dist/services/smallest/index.js +7 -0
  419. package/dist/services/soniox/index.d.ts +2 -0
  420. package/dist/services/soniox/index.js +7 -0
  421. package/dist/services/speechify/index.d.ts +2 -0
  422. package/dist/services/speechify/index.js +7 -0
  423. package/dist/services/speechmatics/index.d.ts +2 -0
  424. package/dist/services/speechmatics/index.js +7 -0
  425. package/dist/services/stt_latency.d.ts +62 -0
  426. package/dist/services/stt_latency.js +71 -0
  427. package/dist/services/stt_service.d.ts +601 -0
  428. package/dist/services/stt_service.js +1044 -0
  429. package/dist/services/tavus/index.d.ts +2 -0
  430. package/dist/services/tavus/index.js +7 -0
  431. package/dist/services/together/index.d.ts +2 -0
  432. package/dist/services/together/index.js +7 -0
  433. package/dist/services/tts_service.d.ts +909 -0
  434. package/dist/services/tts_service.js +2018 -0
  435. package/dist/services/ultravox/index.d.ts +2 -0
  436. package/dist/services/ultravox/index.js +9 -0
  437. package/dist/services/vision_service.d.ts +56 -0
  438. package/dist/services/vision_service.js +62 -0
  439. package/dist/services/websocket_service.d.ts +213 -0
  440. package/dist/services/websocket_service.js +654 -0
  441. package/dist/services/whisper/index.d.ts +2 -0
  442. package/dist/services/whisper/index.js +9 -0
  443. package/dist/services/xai/index.d.ts +2 -0
  444. package/dist/services/xai/index.js +9 -0
  445. package/dist/services/xtts/index.d.ts +2 -0
  446. package/dist/services/xtts/index.js +9 -0
  447. package/dist/tests/index.d.ts +2 -0
  448. package/dist/tests/index.js +8 -0
  449. package/dist/tests/utils.d.ts +123 -0
  450. package/dist/tests/utils.js +212 -0
  451. package/dist/transcriptions/index.d.ts +2 -0
  452. package/dist/transcriptions/index.js +8 -0
  453. package/dist/transcriptions/language.d.ts +376 -0
  454. package/dist/transcriptions/language.js +543 -0
  455. package/dist/transports/base_input.d.ts +149 -0
  456. package/dist/transports/base_input.js +338 -0
  457. package/dist/transports/base_output.d.ts +432 -0
  458. package/dist/transports/base_output.js +1231 -0
  459. package/dist/transports/base_transport.d.ts +223 -0
  460. package/dist/transports/base_transport.js +167 -0
  461. package/dist/transports/daily/index.d.ts +2 -0
  462. package/dist/transports/daily/index.js +9 -0
  463. package/dist/transports/heygen/index.d.ts +2 -0
  464. package/dist/transports/heygen/index.js +9 -0
  465. package/dist/transports/index.d.ts +2 -0
  466. package/dist/transports/index.js +8 -0
  467. package/dist/transports/lemonslice/index.d.ts +2 -0
  468. package/dist/transports/lemonslice/index.js +9 -0
  469. package/dist/transports/livekit/index.d.ts +2 -0
  470. package/dist/transports/livekit/index.js +9 -0
  471. package/dist/transports/local/index.d.ts +2 -0
  472. package/dist/transports/local/index.js +9 -0
  473. package/dist/transports/moq/index.d.ts +2 -0
  474. package/dist/transports/moq/index.js +9 -0
  475. package/dist/transports/smallwebrtc/index.d.ts +2 -0
  476. package/dist/transports/smallwebrtc/index.js +9 -0
  477. package/dist/transports/tavus/index.d.ts +2 -0
  478. package/dist/transports/tavus/index.js +9 -0
  479. package/dist/transports/video_in_sampler.d.ts +157 -0
  480. package/dist/transports/video_in_sampler.js +258 -0
  481. package/dist/transports/vonage/index.d.ts +2 -0
  482. package/dist/transports/vonage/index.js +9 -0
  483. package/dist/transports/websocket/index.d.ts +2 -0
  484. package/dist/transports/websocket/index.js +9 -0
  485. package/dist/transports/whatsapp/index.d.ts +2 -0
  486. package/dist/transports/whatsapp/index.js +9 -0
  487. package/dist/turns/eager_end_of_turn_mixin.d.ts +70 -0
  488. package/dist/turns/eager_end_of_turn_mixin.js +122 -0
  489. package/dist/turns/empty_user_turn.d.ts +55 -0
  490. package/dist/turns/empty_user_turn.js +53 -0
  491. package/dist/turns/index.d.ts +2 -0
  492. package/dist/turns/index.js +8 -0
  493. package/dist/turns/speculation_gate.d.ts +169 -0
  494. package/dist/turns/speculation_gate.js +294 -0
  495. package/dist/turns/types.d.ts +33 -0
  496. package/dist/turns/types.js +35 -0
  497. package/dist/turns/user_idle_controller.d.ts +94 -0
  498. package/dist/turns/user_idle_controller.js +187 -0
  499. package/dist/turns/user_mute/always_user_mute_strategy.d.ts +15 -0
  500. package/dist/turns/user_mute/always_user_mute_strategy.js +29 -0
  501. package/dist/turns/user_mute/base_user_mute_strategy.d.ts +39 -0
  502. package/dist/turns/user_mute/base_user_mute_strategy.js +45 -0
  503. package/dist/turns/user_mute/first_speech_user_mute_strategy.d.ts +28 -0
  504. package/dist/turns/user_mute/first_speech_user_mute_strategy.js +52 -0
  505. package/dist/turns/user_mute/function_call_user_mute_strategy.d.ts +25 -0
  506. package/dist/turns/user_mute/function_call_user_mute_strategy.js +50 -0
  507. package/dist/turns/user_mute/index.d.ts +6 -0
  508. package/dist/turns/user_mute/index.js +11 -0
  509. package/dist/turns/user_mute/mute_until_first_bot_complete_user_mute_strategy.d.ts +43 -0
  510. package/dist/turns/user_mute/mute_until_first_bot_complete_user_mute_strategy.js +76 -0
  511. package/dist/turns/user_start/base_user_turn_start_strategy.d.ts +135 -0
  512. package/dist/turns/user_start/base_user_turn_start_strategy.js +134 -0
  513. package/dist/turns/user_start/external_user_turn_start_strategy.d.ts +45 -0
  514. package/dist/turns/user_start/external_user_turn_start_strategy.js +64 -0
  515. package/dist/turns/user_start/index.d.ts +8 -0
  516. package/dist/turns/user_start/index.js +16 -0
  517. package/dist/turns/user_start/krisp_viva_ip_user_turn_start_strategy.d.ts +133 -0
  518. package/dist/turns/user_start/krisp_viva_ip_user_turn_start_strategy.js +243 -0
  519. package/dist/turns/user_start/min_words_user_turn_start_strategy.d.ts +60 -0
  520. package/dist/turns/user_start/min_words_user_turn_start_strategy.js +102 -0
  521. package/dist/turns/user_start/transcription_user_turn_start_strategy.d.ts +29 -0
  522. package/dist/turns/user_start/transcription_user_turn_start_strategy.js +43 -0
  523. package/dist/turns/user_start/vad_user_turn_start_strategy.d.ts +20 -0
  524. package/dist/turns/user_start/vad_user_turn_start_strategy.js +31 -0
  525. package/dist/turns/user_start/wake_phrase_user_turn_start_strategy.d.ts +141 -0
  526. package/dist/turns/user_start/wake_phrase_user_turn_start_strategy.js +238 -0
  527. package/dist/turns/user_stop/base_user_turn_stop_strategy.d.ts +174 -0
  528. package/dist/turns/user_stop/base_user_turn_stop_strategy.js +170 -0
  529. package/dist/turns/user_stop/deferred_user_turn_stop_strategy.d.ts +81 -0
  530. package/dist/turns/user_stop/deferred_user_turn_stop_strategy.js +107 -0
  531. package/dist/turns/user_stop/eager_match_policy.d.ts +36 -0
  532. package/dist/turns/user_stop/eager_match_policy.js +50 -0
  533. package/dist/turns/user_stop/eager_user_turn_stop_strategy.d.ts +84 -0
  534. package/dist/turns/user_stop/eager_user_turn_stop_strategy.js +154 -0
  535. package/dist/turns/user_stop/external_user_turn_completion_stop_strategy.d.ts +45 -0
  536. package/dist/turns/user_stop/external_user_turn_completion_stop_strategy.js +55 -0
  537. package/dist/turns/user_stop/external_user_turn_stop_strategy.d.ts +115 -0
  538. package/dist/turns/user_stop/external_user_turn_stop_strategy.js +205 -0
  539. package/dist/turns/user_stop/index.d.ts +10 -0
  540. package/dist/turns/user_stop/index.js +15 -0
  541. package/dist/turns/user_stop/llm_turn_completion_user_turn_stop_strategy.d.ts +44 -0
  542. package/dist/turns/user_stop/llm_turn_completion_user_turn_stop_strategy.js +59 -0
  543. package/dist/turns/user_stop/speech_timeout_user_turn_stop_strategy.d.ts +140 -0
  544. package/dist/turns/user_stop/speech_timeout_user_turn_stop_strategy.js +303 -0
  545. package/dist/turns/user_stop/turn_analyzer_user_turn_stop_strategy.d.ts +135 -0
  546. package/dist/turns/user_stop/turn_analyzer_user_turn_stop_strategy.js +332 -0
  547. package/dist/turns/user_turn_completion_mixin.d.ts +147 -0
  548. package/dist/turns/user_turn_completion_mixin.js +591 -0
  549. package/dist/turns/user_turn_controller.d.ts +134 -0
  550. package/dist/turns/user_turn_controller.js +335 -0
  551. package/dist/turns/user_turn_processor.d.ts +87 -0
  552. package/dist/turns/user_turn_processor.js +180 -0
  553. package/dist/turns/user_turn_strategies.d.ts +187 -0
  554. package/dist/turns/user_turn_strategies.js +222 -0
  555. package/dist/utils/async_tool_cancellation.d.ts +35 -0
  556. package/dist/utils/async_tool_cancellation.js +78 -0
  557. package/dist/utils/asyncio/index.d.ts +2 -0
  558. package/dist/utils/asyncio/index.js +7 -0
  559. package/dist/utils/asyncio/task_manager.d.ts +49 -0
  560. package/dist/utils/asyncio/task_manager.js +105 -0
  561. package/dist/utils/base_object.d.ts +76 -0
  562. package/dist/utils/base_object.js +154 -0
  563. package/dist/utils/context/aggregated_frame_sequencer.d.ts +491 -0
  564. package/dist/utils/context/aggregated_frame_sequencer.js +803 -0
  565. package/dist/utils/context/audio_context_manager.d.ts +131 -0
  566. package/dist/utils/context/audio_context_manager.js +233 -0
  567. package/dist/utils/context/index.d.ts +2 -0
  568. package/dist/utils/context/index.js +7 -0
  569. package/dist/utils/context/llm_context_summarization.d.ts +258 -0
  570. package/dist/utils/context/llm_context_summarization.js +511 -0
  571. package/dist/utils/context/text_segment_map.d.ts +611 -0
  572. package/dist/utils/context/text_segment_map.js +1132 -0
  573. package/dist/utils/context/word_completion_tracker.d.ts +273 -0
  574. package/dist/utils/context/word_completion_tracker.js +441 -0
  575. package/dist/utils/context/word_timestamp_tracker.d.ts +72 -0
  576. package/dist/utils/context/word_timestamp_tracker.js +98 -0
  577. package/dist/utils/deprecation.d.ts +28 -0
  578. package/dist/utils/deprecation.js +75 -0
  579. package/dist/utils/env.d.ts +38 -0
  580. package/dist/utils/env.js +62 -0
  581. package/dist/utils/errors.d.ts +75 -0
  582. package/dist/utils/errors.js +202 -0
  583. package/dist/utils/file_resolver.d.ts +126 -0
  584. package/dist/utils/file_resolver.js +314 -0
  585. package/dist/utils/file_storage.d.ts +99 -0
  586. package/dist/utils/file_storage.js +100 -0
  587. package/dist/utils/frame_queue.d.ts +36 -0
  588. package/dist/utils/frame_queue.js +66 -0
  589. package/dist/utils/http.d.ts +39 -0
  590. package/dist/utils/http.js +53 -0
  591. package/dist/utils/image.d.ts +15 -0
  592. package/dist/utils/image.js +30 -0
  593. package/dist/utils/index.d.ts +2 -0
  594. package/dist/utils/index.js +8 -0
  595. package/dist/utils/log_config.d.ts +34 -0
  596. package/dist/utils/log_config.js +76 -0
  597. package/dist/utils/logger.d.ts +40 -0
  598. package/dist/utils/logger.js +91 -0
  599. package/dist/utils/mixin.d.ts +14 -0
  600. package/dist/utils/mixin.js +63 -0
  601. package/dist/utils/network.d.ts +73 -0
  602. package/dist/utils/network.js +90 -0
  603. package/dist/utils/security/allowed_origins.d.ts +16 -0
  604. package/dist/utils/security/allowed_origins.js +34 -0
  605. package/dist/utils/security/index.d.ts +2 -0
  606. package/dist/utils/security/index.js +7 -0
  607. package/dist/utils/security/redaction.d.ts +27 -0
  608. package/dist/utils/security/redaction.js +123 -0
  609. package/dist/utils/security/ssrf.d.ts +79 -0
  610. package/dist/utils/security/ssrf.js +327 -0
  611. package/dist/utils/shared.d.ts +35 -0
  612. package/dist/utils/shared.js +136 -0
  613. package/dist/utils/startup.d.ts +17 -0
  614. package/dist/utils/startup.js +130 -0
  615. package/dist/utils/string.d.ts +116 -0
  616. package/dist/utils/string.js +460 -0
  617. package/dist/utils/sync/base_notifier.d.ts +25 -0
  618. package/dist/utils/sync/base_notifier.js +16 -0
  619. package/dist/utils/sync/event_notifier.d.ts +38 -0
  620. package/dist/utils/sync/event_notifier.js +51 -0
  621. package/dist/utils/sync/index.d.ts +2 -0
  622. package/dist/utils/sync/index.js +7 -0
  623. package/dist/utils/text/alnum_utils.d.ts +109 -0
  624. package/dist/utils/text/alnum_utils.js +262 -0
  625. package/dist/utils/text/base_text_aggregator.d.ts +146 -0
  626. package/dist/utils/text/base_text_aggregator.js +102 -0
  627. package/dist/utils/text/base_text_filter.d.ts +54 -0
  628. package/dist/utils/text/base_text_filter.js +50 -0
  629. package/dist/utils/text/index.d.ts +2 -0
  630. package/dist/utils/text/index.js +7 -0
  631. package/dist/utils/text/markdown_text_filter.d.ts +147 -0
  632. package/dist/utils/text/markdown_text_filter.js +522 -0
  633. package/dist/utils/text/markup_utils.d.ts +54 -0
  634. package/dist/utils/text/markup_utils.js +156 -0
  635. package/dist/utils/text/pattern_pair_aggregator.d.ts +251 -0
  636. package/dist/utils/text/pattern_pair_aggregator.js +478 -0
  637. package/dist/utils/text/phonemes.d.ts +60 -0
  638. package/dist/utils/text/phonemes.js +172 -0
  639. package/dist/utils/text/simple_text_aggregator.d.ts +135 -0
  640. package/dist/utils/text/simple_text_aggregator.js +254 -0
  641. package/dist/utils/text/skip_tags_aggregator.d.ts +85 -0
  642. package/dist/utils/text/skip_tags_aggregator.js +147 -0
  643. package/dist/utils/text/transforms/pronunciations.d.ts +65 -0
  644. package/dist/utils/text/transforms/pronunciations.js +111 -0
  645. package/dist/utils/text/word_timestamp_utils.d.ts +26 -0
  646. package/dist/utils/text/word_timestamp_utils.js +52 -0
  647. package/dist/utils/time.d.ts +13 -0
  648. package/dist/utils/time.js +31 -0
  649. package/dist/utils/tracing/index.d.ts +3 -0
  650. package/dist/utils/tracing/index.js +8 -0
  651. package/dist/utils/tracing/service_attributes.d.ts +198 -0
  652. package/dist/utils/tracing/service_attributes.js +434 -0
  653. package/dist/utils/tracing/service_decorators.d.ts +214 -0
  654. package/dist/utils/tracing/service_decorators.js +1662 -0
  655. package/dist/utils/tracing/setup.d.ts +95 -0
  656. package/dist/utils/tracing/setup.js +110 -0
  657. package/dist/utils/tracing/tracing_context.d.ts +55 -0
  658. package/dist/utils/tracing/tracing_context.js +97 -0
  659. package/dist/utils/tracing/turn_trace_observer.d.ts +90 -0
  660. package/dist/utils/tracing/turn_trace_observer.js +219 -0
  661. package/dist/utils/types.d.ts +80 -0
  662. package/dist/utils/types.js +108 -0
  663. package/dist/utils/utils.d.ts +5 -0
  664. package/dist/utils/utils.js +22 -0
  665. package/dist/utils/yaml.d.ts +64 -0
  666. package/dist/utils/yaml.js +41 -0
  667. package/dist/version.d.ts +3 -0
  668. package/dist/version.js +7 -0
  669. package/dist/workers/base_worker.d.ts +772 -0
  670. package/dist/workers/base_worker.js +1298 -0
  671. package/dist/workers/index.d.ts +2 -0
  672. package/dist/workers/index.js +8 -0
  673. package/dist/workers/llm/backend_llm_worker.d.ts +552 -0
  674. package/dist/workers/llm/backend_llm_worker.js +1055 -0
  675. package/dist/workers/llm/index.d.ts +6 -0
  676. package/dist/workers/llm/index.js +11 -0
  677. package/dist/workers/llm/llm_context_worker.d.ts +66 -0
  678. package/dist/workers/llm/llm_context_worker.js +72 -0
  679. package/dist/workers/llm/llm_worker.d.ts +180 -0
  680. package/dist/workers/llm/llm_worker.js +323 -0
  681. package/dist/workers/llm/tool_decorator.d.ts +68 -0
  682. package/dist/workers/llm/tool_decorator.js +60 -0
  683. package/dist/workers/runner.d.ts +268 -0
  684. package/dist/workers/runner.js +489 -0
  685. package/dist/workers/ui/index.d.ts +22 -0
  686. package/dist/workers/ui/index.js +27 -0
  687. package/dist/workers/ui/ui_event_decorator.d.ts +43 -0
  688. package/dist/workers/ui/ui_event_decorator.js +75 -0
  689. package/dist/workers/ui/ui_job_context.d.ts +12 -0
  690. package/dist/workers/ui/ui_job_context.js +17 -0
  691. package/dist/workers/ui/ui_prompts.d.ts +21 -0
  692. package/dist/workers/ui/ui_prompts.js +41 -0
  693. package/dist/workers/ui/ui_tools.d.ts +32 -0
  694. package/dist/workers/ui/ui_tools.js +112 -0
  695. package/dist/workers/ui/ui_worker.d.ts +571 -0
  696. package/dist/workers/ui/ui_worker.js +1156 -0
  697. package/package.json +46 -3
  698. package/README.md +0 -5
@@ -0,0 +1,1132 @@
1
+ // Copyright (c) 2024-2026, Daily
2
+ // Copyright (c) 2026, pipedog contributors
3
+ // SPDX-License-Identifier: BSD-2-Clause
4
+ //
5
+ // Ported from pipecat: src/pipecat/utils/context/text_segment_map.py
6
+ /**
7
+ * Keeps a position in three versions of the same text as a TTS speaks it.
8
+ *
9
+ * Positions are UTF-16 code unit offsets (upstream counts code points); see
10
+ * `alnum_utils.ts`.
11
+ */
12
+ import { advanceByAlnums, advanceByChars, alnumOnly, charAt, foldForMatching, hasAlnum, isAlnum, isPunctuation, isSpace, stripTrailingPunctuation, } from "../text/alnum_utils.js";
13
+ import { rawOffsetAfterCleanChars, splitMarkupRuns, stripCompleteMarkup, stripMarkup, } from "../text/markup_utils.js";
14
+ /** Python's `len(s.split())`. */
15
+ function wordCount(text) {
16
+ return text.split(/\s+/).filter((w) => w).length;
17
+ }
18
+ /**
19
+ * A piece of the utterance, paired with what the TTS was given in its place.
20
+ *
21
+ * The map is a list of these, laid end to end over the whole utterance. In
22
+ * most of them the two sides are identical; the interesting ones are where a
23
+ * transform, a filter or a tag made them differ (see `isTransformed`).
24
+ *
25
+ * - `original`: the piece as a client displays it.
26
+ * - `tts`: the same piece as it was sent to the TTS. Identical to `original`
27
+ * unless something rewrote it, and empty if it was dropped entirely.
28
+ * - `originalStart`: where the piece starts in the full original text.
29
+ * - `originalEnd`: where it ends. Cursors jump straight here once a rewritten
30
+ * piece is finished, since no position inside one means anything.
31
+ */
32
+ export class TextSegment {
33
+ original;
34
+ tts;
35
+ originalStart;
36
+ originalEnd;
37
+ constructor(options) {
38
+ this.original = options.original;
39
+ this.tts = options.tts;
40
+ this.originalStart = options.originalStart;
41
+ this.originalEnd = options.originalEnd;
42
+ Object.freeze(this);
43
+ }
44
+ /**
45
+ * True when the two sides cannot be followed together, character by character.
46
+ *
47
+ * A segment like this is all or nothing. The cursors into the other texts
48
+ * wait at its start until every spoken word of it has arrived, then jump
49
+ * straight to its end, because no position inside it means anything.
50
+ *
51
+ * Any one of these makes it true:
52
+ *
53
+ * - the letters and digits differ, as in `"$42.50"` against
54
+ * `"forty two dollars"`;
55
+ * - the two sides have different numbers of words;
56
+ * - the TTS side has tags in it, even when the words match. The cursor
57
+ * through the spoken text has tag characters to cross that the other
58
+ * texts do not have.
59
+ *
60
+ * Only the shape of a tag matters, never its name: `<phoneme
61
+ * ...>Siobhan</phoneme>` counts because of the tags around the word.
62
+ */
63
+ get isTransformed() {
64
+ if (this.tts !== stripCompleteMarkup(this.tts)) {
65
+ return true;
66
+ }
67
+ if (alnumOnly(this.original) !== alnumOnly(this.tts)) {
68
+ return true;
69
+ }
70
+ return wordCount(this.original) !== wordCount(this.tts);
71
+ }
72
+ /**
73
+ * How many letters and digits the spoken side of this segment has.
74
+ *
75
+ * This is what the provider's words spend as they arrive. On a rewritten
76
+ * segment it has nothing to do with `originalAlnumCount`: "forty two
77
+ * dollars" against "$42.50".
78
+ */
79
+ get ttsAlnumCount() {
80
+ return alnumOnly(this.tts).length;
81
+ }
82
+ /**
83
+ * How many letters and digits the original side of this segment has.
84
+ *
85
+ * This is what the cursors into `originalText` and `llmText` spend, since
86
+ * those texts hold the original characters, not the spoken ones.
87
+ */
88
+ get originalAlnumCount() {
89
+ return alnumOnly(this.original).length;
90
+ }
91
+ toString() {
92
+ return `TextSegment(original=${JSON.stringify(this.original)}, tts=${JSON.stringify(this.tts)}, original_start=${this.originalStart}, original_end=${this.originalEnd})`;
93
+ }
94
+ }
95
+ /**
96
+ * What happened when a word was offered to one segment.
97
+ *
98
+ * Trying one word against one segment is called a hop. Two of these answers
99
+ * end the attempt; the other two send the word on to the next segment.
100
+ *
101
+ * - `PLACED`: the word fits here. Move to the end of the match and stop.
102
+ * - `CROSSES`: this segment holds only the beginning of the word. Finish the
103
+ * segment and take the rest of the word to the next one.
104
+ * - `EXHAUSTED`: there is nothing here that can be spoken, so no word will
105
+ * ever match -- an empty side of the diff, or a lone `<break/>`. Finish the
106
+ * segment and try the whole word again on the next one.
107
+ * - `NO_MATCH`: nothing here matches the word, usually a symbol the provider
108
+ * reports differently from the text. Step every cursor past the next run of
109
+ * punctuation, which the word stands for, and stop.
110
+ */
111
+ export const _HopKind = {
112
+ PLACED: 1,
113
+ CROSSES: 2,
114
+ EXHAUSTED: 3,
115
+ NO_MATCH: 4,
116
+ };
117
+ function hop(kind, options = {}) {
118
+ return Object.freeze({
119
+ kind,
120
+ segmentAdvance: options.segmentAdvance ?? 0,
121
+ wordConsumed: options.wordConsumed ?? 0,
122
+ });
123
+ }
124
+ function getOpcodes(a, b) {
125
+ const b2j = new Map();
126
+ b.forEach((elt, j) => {
127
+ let indices = b2j.get(elt);
128
+ if (!indices) {
129
+ indices = [];
130
+ b2j.set(elt, indices);
131
+ }
132
+ indices.push(j);
133
+ });
134
+ const findLongestMatch = (alo, ahi, blo, bhi) => {
135
+ let besti = alo;
136
+ let bestj = blo;
137
+ let bestsize = 0;
138
+ let j2len = new Map();
139
+ for (let i = alo; i < ahi; i++) {
140
+ const newj2len = new Map();
141
+ for (const j of b2j.get(a[i]) ?? []) {
142
+ if (j < blo)
143
+ continue;
144
+ if (j >= bhi)
145
+ break;
146
+ const k = (j2len.get(j - 1) ?? 0) + 1;
147
+ newj2len.set(j, k);
148
+ if (k > bestsize) {
149
+ besti = i - k + 1;
150
+ bestj = j - k + 1;
151
+ bestsize = k;
152
+ }
153
+ }
154
+ j2len = newj2len;
155
+ }
156
+ while (besti > alo && bestj > blo && a[besti - 1] === b[bestj - 1]) {
157
+ besti -= 1;
158
+ bestj -= 1;
159
+ bestsize += 1;
160
+ }
161
+ while (besti + bestsize < ahi &&
162
+ bestj + bestsize < bhi &&
163
+ a[besti + bestsize] === b[bestj + bestsize]) {
164
+ bestsize += 1;
165
+ }
166
+ return [besti, bestj, bestsize];
167
+ };
168
+ const la = a.length;
169
+ const lb = b.length;
170
+ const queue = [[0, la, 0, lb]];
171
+ const matchingBlocks = [];
172
+ while (queue.length > 0) {
173
+ const [alo, ahi, blo, bhi] = queue.pop();
174
+ const x = findLongestMatch(alo, ahi, blo, bhi);
175
+ const [i, j, k] = x;
176
+ if (k) {
177
+ matchingBlocks.push(x);
178
+ if (alo < i && blo < j)
179
+ queue.push([alo, i, blo, j]);
180
+ if (i + k < ahi && j + k < bhi)
181
+ queue.push([i + k, ahi, j + k, bhi]);
182
+ }
183
+ }
184
+ matchingBlocks.sort((p, q) => p[0] - q[0] || p[1] - q[1] || p[2] - q[2]);
185
+ let i1 = 0;
186
+ let j1 = 0;
187
+ let k1 = 0;
188
+ const nonAdjacent = [];
189
+ for (const [i2, j2, k2] of matchingBlocks) {
190
+ if (i1 + k1 === i2 && j1 + k1 === j2) {
191
+ k1 += k2;
192
+ }
193
+ else {
194
+ if (k1)
195
+ nonAdjacent.push([i1, j1, k1]);
196
+ i1 = i2;
197
+ j1 = j2;
198
+ k1 = k2;
199
+ }
200
+ }
201
+ if (k1)
202
+ nonAdjacent.push([i1, j1, k1]);
203
+ nonAdjacent.push([la, lb, 0]);
204
+ let i = 0;
205
+ let j = 0;
206
+ const answer = [];
207
+ for (const [ai, bj, size] of nonAdjacent) {
208
+ let tag = "";
209
+ if (i < ai && j < bj)
210
+ tag = "replace";
211
+ else if (i < ai)
212
+ tag = "delete";
213
+ else if (j < bj)
214
+ tag = "insert";
215
+ if (tag)
216
+ answer.push([tag, i, ai, j, bj]);
217
+ i = ai + size;
218
+ j = bj + size;
219
+ if (size)
220
+ answer.push(["equal", ai, i, bj, j]);
221
+ }
222
+ return answer;
223
+ }
224
+ /**
225
+ * Answers "where are we?" in three versions of one utterance, word by word.
226
+ *
227
+ * A TTS provider reports the words it speaks. Each report has to be turned
228
+ * into a position -- but into a position in *three* different strings,
229
+ * because the same utterance exists in three forms at once:
230
+ *
231
+ * - `ttsText` -- what was actually spoken, tags and all:
232
+ * `"Your balance is forty two dollars"`
233
+ * - `originalText` -- what a client displays: `"Your balance is $42.50"`
234
+ * - `llmText` -- what the LLM wrote, so what the transcript should keep:
235
+ * `"Your balance is <b>$42.50</b>"`. Defaults to `originalText`.
236
+ *
237
+ * For a frame nothing rewrote, all three are the same string and every
238
+ * position is the same.
239
+ *
240
+ * **The hard part is that a spoken word need not appear in the other two.**
241
+ * The provider says `"dollars"`; nothing in `"$42.50"` matches it. So the map
242
+ * is built once, by diffing `ttsText` against `originalText` into aligned
243
+ * `TextSegment` pieces -- each either survived unchanged or was rewritten
244
+ * whole.
245
+ *
246
+ * `llmText` is never compared against the others, and does not need to be.
247
+ * It holds the same letters and digits as `originalText`, in the same order,
248
+ * and differs only in what is wrapped around them -- tags, delimiters,
249
+ * punctuation. So counting letters and digits is enough to keep it in step,
250
+ * and its cursor moves by that count.
251
+ *
252
+ * From then on one real cursor moves: `rawPos`, how far into `ttsText` the
253
+ * provider has got. `userFacingPos` and the two LLM cursors follow it:
254
+ *
255
+ * - Through an **unchanged** segment they keep pace, word for word.
256
+ * - Through a **rewritten** one they wait. There is no honest position
257
+ * halfway through `"$42.50"` while `"forty two dollars"` is being spoken, so
258
+ * they hold and then jump to the end of the span in one step when the last
259
+ * of its words lands.
260
+ *
261
+ * The LLM side carries two of them because one position cannot answer both
262
+ * questions asked of it. `llmSpokenPos` is what the provider has reported;
263
+ * `llmPos` is what has been attributed to a word, and runs ahead of it over a
264
+ * mark stuck to a word the provider has already named. See
265
+ * `_advanceLlmCursors`.
266
+ *
267
+ * Callers ask two things. `wordBelongsCurrentSegment` -- does this token
268
+ * plausibly continue what is left to speak? -- and `advanceWord`, which
269
+ * consumes it. Both tolerate the ways providers mangle tokens (added
270
+ * punctuation, changed case or diacritics, a fragment of a half-open SSML
271
+ * tag) without the caller knowing anything about it; `_classifyHop` holds
272
+ * that logic.
273
+ *
274
+ * A word need not sit at the cursor to be placed. When the provider garbles
275
+ * or drops an event, the next good word still matches a few words on, and the
276
+ * text stepped over -- which no event will ever name -- is consumed with it.
277
+ * So the two questions above stay simple: a token either has a home somewhere
278
+ * in what is left to speak, or it belongs to another utterance entirely.
279
+ *
280
+ * @example
281
+ * // "$42.50" was sent to the TTS as "forty two dollars and fifty cents"
282
+ * const smap = new TextSegmentMap(
283
+ * "Your balance is forty two dollars and fifty cents",
284
+ * "Your balance is $42.50",
285
+ * );
286
+ * for (const word of ["Your", "balance", "is"]) {
287
+ * smap.advanceWord(word); // unchanged: every cursor keeps pace
288
+ * }
289
+ * for (const word of ["forty", "two", "dollars", "and", "fifty"]) {
290
+ * smap.advanceWord(word); // rewritten: the other two cursors wait
291
+ * }
292
+ * smap.advanceWord("cents"); // the span is done, so they jump to its end
293
+ * // smap.lastCompletedSegment.original === "$42.50"
294
+ * // smap.inTransformedSegment === false
295
+ */
296
+ export class TextSegmentMap {
297
+ /** How many words a word may be placed past -- see `_lookaheadHop`. */
298
+ static LOOKAHEAD_WORDS = 3;
299
+ _ttsText;
300
+ _originalText;
301
+ _llmText;
302
+ _segments;
303
+ _segIdx = 0;
304
+ _segRawPos = 0;
305
+ _userFacingPos = 0;
306
+ _llmPos = 0;
307
+ _llmSpokenPos = 0;
308
+ _lastCompleted = null;
309
+ _lastOverflow = null;
310
+ _lastLeadingDuplicate = 0;
311
+ /**
312
+ * Line the three texts up against each other.
313
+ *
314
+ * The comparison happens once, here. Everything after this only moves
315
+ * cursors.
316
+ *
317
+ * @param ttsText What was sent to the TTS, and so what incoming words are
318
+ * matched against. May carry synthesis tags and rewritten values.
319
+ * @param originalText The same content as a client displays it, before any
320
+ * rewriting. Diffed against `ttsText` to build the segments.
321
+ * @param llmText The same content as the LLM wrote it, which may add
322
+ * delimiters the other two never see. Rides its own cursor rather than
323
+ * being diffed. Defaults to `originalText`.
324
+ */
325
+ constructor(ttsText, originalText, llmText = null) {
326
+ this._ttsText = ttsText;
327
+ this._originalText = originalText;
328
+ this._llmText = llmText !== null && llmText !== undefined ? llmText : originalText;
329
+ this._segments = TextSegmentMap._buildSegments(ttsText, originalText);
330
+ this._resetState();
331
+ }
332
+ /**
333
+ * Compare the two texts and cut them into the segments the map walks.
334
+ *
335
+ * `difflib` lines them up a word at a time and reports each piece as equal,
336
+ * replaced, inserted or deleted. Every piece becomes one `TextSegment`.
337
+ * Spaces are kept as words of their own so that the positions stay exact.
338
+ *
339
+ * One extra step: a piece that came out equal is cut around any tag inside
340
+ * it, so a single tag does not turn the whole sentence into an
341
+ * all-or-nothing segment.
342
+ *
343
+ * Called once, from the constructor.
344
+ */
345
+ static _buildSegments(ttsText, originalText) {
346
+ const tokenize = (text) => text.split(/(\s+)/);
347
+ const origTokens = tokenize(originalText);
348
+ const ttsTokens = tokenize(ttsText);
349
+ // SequenceMatcher lines the two texts up a word at a time. Each piece it
350
+ // reports becomes a TextSegment, positioned in the original text.
351
+ //
352
+ // Example:
353
+ //
354
+ // originalText = "Your balance is $42.50"
355
+ // ttsText = "Your balance is forty two dollars and fifty cents"
356
+ //
357
+ // Tokenization preserves whitespace, so SequenceMatcher sees:
358
+ //
359
+ // equal:
360
+ // "Your balance is "
361
+ //
362
+ // replace:
363
+ // "$42.50"
364
+ // ->
365
+ // "forty two dollars and fifty cents"
366
+ //
367
+ // This produces two segments:
368
+ //
369
+ // TextSegment(
370
+ // original="Your balance is ",
371
+ // tts="Your balance is ",
372
+ // original_start=0,
373
+ // original_end=16,
374
+ // )
375
+ //
376
+ // TextSegment(
377
+ // original="$42.50",
378
+ // tts="forty two dollars and fifty cents",
379
+ // original_start=16,
380
+ // original_end=22,
381
+ // )
382
+ //
383
+ // During playback, unchanged segments advance cursors
384
+ // proportionally. Transformed segments are treated as atomic:
385
+ // the cursors are held while the expanded TTS text is being
386
+ // consumed and jump to original_end only when the entire
387
+ // transformed segment completes.
388
+ const segments = [];
389
+ let origPos = 0;
390
+ for (const [tag, i1, i2, j1, j2] of getOpcodes(origTokens, ttsTokens)) {
391
+ const origChunk = origTokens.slice(i1, i2).join("");
392
+ const ttsChunk = ttsTokens.slice(j1, j2).join("");
393
+ // A segment is all-or-nothing as soon as it holds a tag, so one tag in
394
+ // the middle of otherwise identical text would hold the cursors still
395
+ // for the whole piece. Giving the tag a segment of its own limits that
396
+ // to the words the tag wraps:
397
+ //
398
+ // original = tts = "I love to count <spell>1234</spell>."
399
+ //
400
+ // "I love to count " plain, cursors move word by word
401
+ // "<spell>1234</spell>." all-or-nothing, lands as one
402
+ //
403
+ // Only equal pieces can be split this way. Both sides hold the same
404
+ // text there, so a single position cuts both. Where the sides differ
405
+ // there is no position that means the same thing on each.
406
+ const parts = tag === "equal"
407
+ ? splitMarkupRuns(origChunk).map((part) => [part, part])
408
+ : [[origChunk, ttsChunk]];
409
+ for (const [origPart, ttsPart] of parts) {
410
+ const origEnd = origPos + origPart.length;
411
+ segments.push(new TextSegment({
412
+ original: origPart,
413
+ tts: ttsPart,
414
+ originalStart: origPos,
415
+ originalEnd: origEnd,
416
+ }));
417
+ origPos = origEnd;
418
+ }
419
+ }
420
+ return segments;
421
+ }
422
+ _resetState() {
423
+ this._segIdx = 0;
424
+ this._segRawPos = 0;
425
+ this._userFacingPos = 0;
426
+ this._llmPos = 0;
427
+ this._llmSpokenPos = 0;
428
+ this._lastCompleted = null;
429
+ this._lastOverflow = null;
430
+ this._lastLeadingDuplicate = 0;
431
+ }
432
+ /**
433
+ * Return `word`, then with its last mark removed, then with every trailing
434
+ * mark removed.
435
+ *
436
+ * A TTS can add punctuation the text it was given never had -- reading a
437
+ * list item `"my account"` as a sentence and reporting `"account."`.
438
+ * Matching tries the word as it arrived first, then the trimmed forms.
439
+ *
440
+ * Dropping only the added mark comes before dropping them all, so the marks
441
+ * the text does have are still matched: `"images:."` lands past the colon
442
+ * in `"images:"`, and `"---."` keeps something to match at all. The
443
+ * variants are tried in order and empty ones are skipped.
444
+ */
445
+ static _wordVariants(word) {
446
+ const allMarksDropped = stripTrailingPunctuation(word);
447
+ // No trailing mark to drop: "hello" -> ["hello"]
448
+ if (allMarksDropped === word) {
449
+ return [word];
450
+ }
451
+ const lastMarkDropped = word.slice(0, -1);
452
+ // A single trailing mark, so both trims agree: "account." -> ["account.", "account"]
453
+ if (lastMarkDropped === allMarksDropped) {
454
+ return [word, allMarksDropped];
455
+ }
456
+ // Several trailing marks: "images:." -> ["images:.", "images:", "images"]
457
+ return [word, lastMarkDropped, allMarksDropped];
458
+ }
459
+ /**
460
+ * Compare `remainingWord` to each candidate, character for character.
461
+ *
462
+ * Two things can match. If a candidate starts with the word, the word fits
463
+ * here (`PLACED`). If the word starts with a candidate, the candidate ran
464
+ * out first, so the rest of the word belongs to the next segment
465
+ * (`CROSSES`).
466
+ *
467
+ * `_foldedHop` calls this too, on folded copies of the same strings.
468
+ * Folding never changes a string's length, so a position found in a folded
469
+ * copy is the same position in the original.
470
+ *
471
+ * @param candidates `[text, offset]` pairs to match `remainingWord` against,
472
+ * tried in order.
473
+ * @param remainingWord The word (or its trailing-punctuation-stripped
474
+ * variant) to match.
475
+ * @param requireWordBoundary When true, the word must end where a word ends
476
+ * in the candidate -- either it used the candidate up, or the next
477
+ * character is not a letter or digit. This stops `"account"` from matching
478
+ * the start of `"Accountant"`. Only the folded pass asks for it, because
479
+ * folding away case makes that kind of accidental match much easier to hit.
480
+ * @returns A `PLACED` or `CROSSES` hop, or `null` if nothing matched.
481
+ */
482
+ static _literalHop(candidates, remainingWord, requireWordBoundary = false) {
483
+ for (const word of TextSegmentMap._wordVariants(remainingWord)) {
484
+ if (!word) {
485
+ continue;
486
+ }
487
+ for (const [candidate, offset] of candidates) {
488
+ if (candidate.startsWith(word)) {
489
+ const landsMidWord = requireWordBoundary &&
490
+ word.length < candidate.length &&
491
+ isAlnum(charAt(candidate, word.length));
492
+ if (!landsMidWord) {
493
+ return hop(_HopKind.PLACED, { segmentAdvance: offset + word.length });
494
+ }
495
+ }
496
+ else if (candidate && word.startsWith(candidate)) {
497
+ return hop(_HopKind.CROSSES, { wordConsumed: candidate.length });
498
+ }
499
+ }
500
+ }
501
+ return null;
502
+ }
503
+ /**
504
+ * Count the characters at the start of `text` that are not letters or digits.
505
+ *
506
+ * For `", I can"` this returns 2 -- the comma and the space.
507
+ *
508
+ * With `stopAtMarkup`, counting also stops at a `'<'`, so the count never
509
+ * reaches inside a tag. Without it, `"<break/>hello"` would count past the
510
+ * `'<'`, and a provider that reports the tag's name `"break"` as a word
511
+ * would look like it had spoken it.
512
+ */
513
+ static _leadingNonalnumLen(text, stopAtMarkup = false) {
514
+ let i = 0;
515
+ while (i < text.length) {
516
+ const ch = charAt(text, i);
517
+ if (isAlnum(ch)) {
518
+ break;
519
+ }
520
+ if (stopAtMarkup && ch === "<") {
521
+ break;
522
+ }
523
+ i += ch.length;
524
+ }
525
+ return i;
526
+ }
527
+ /**
528
+ * Return the three starting points a word may be matched against here.
529
+ *
530
+ * Providers disagree about what they include in a word, so the same text is
531
+ * offered from three places, each paired with how far in it starts:
532
+ *
533
+ * 1. The text as it is, for a provider that reports `" world"` with its own
534
+ * leading space.
535
+ * 2. Past any spaces.
536
+ * 3. Past everything that is not a letter or digit, for a provider that
537
+ * does not repeat punctuation it already spoke -- when `"I"` arrives for
538
+ * `"Yeah, I can"`, the `", "` is still waiting here.
539
+ *
540
+ * Each start is further in than the last, so the closest match is tried
541
+ * first. Identical starting points are dropped.
542
+ */
543
+ static _matchCandidates(segmentRemaining) {
544
+ const leadWs = segmentRemaining.length - segmentRemaining.trimStart().length;
545
+ const leadNonalnum = TextSegmentMap._leadingNonalnumLen(segmentRemaining, true);
546
+ return [...new Set([0, leadWs, leadNonalnum])].map((offset) => [
547
+ segmentRemaining.slice(offset),
548
+ offset,
549
+ ]);
550
+ }
551
+ /**
552
+ * Match `remainingWord` again, ignoring differences in how it is written.
553
+ *
554
+ * A provider may report a word in lower case, without accents, or with
555
+ * plain quotes: `"SQL"` as `"sql"`, `"cafรฉ"` as `"cafe"`, `"donโ€™t"` as
556
+ * `"don't"`. Folding both sides makes those the same.
557
+ *
558
+ * Folding swaps characters one for one and never adds or removes any, so
559
+ * the strings keep their length and a position found here means the same
560
+ * position in the original text.
561
+ *
562
+ * Because folding hides case, a short word could now match inside a longer
563
+ * one -- `"account"` inside `"Accountant"`. So a match here is only accepted
564
+ * if it ends where a word ends.
565
+ */
566
+ static _foldedHop(candidates, remainingWord) {
567
+ const foldedCandidates = candidates.map(([c, offset]) => [
568
+ foldForMatching(c),
569
+ offset,
570
+ ]);
571
+ return TextSegmentMap._literalHop(foldedCandidates, foldForMatching(remainingWord), true);
572
+ }
573
+ /**
574
+ * Match `remainingWord` again, with tags removed from both sides.
575
+ *
576
+ * A provider may report a word wrapped in tags the text it was given did
577
+ * not have, or the other way round. Removing tags from both sides lets the
578
+ * words themselves be compared.
579
+ *
580
+ * The match is found in text that has no tags, so its position has to be
581
+ * translated back to a position in the real text, tags included, by
582
+ * `rawOffsetAfterCleanChars`.
583
+ *
584
+ * Only `PLACED` comes out of this. A word that runs past the end of the
585
+ * segment is left to the two earlier passes.
586
+ */
587
+ static _markupHop(segmentRemaining, remainingWord) {
588
+ const stripped = segmentRemaining.trimStart();
589
+ const leadWs = segmentRemaining.length - stripped.length;
590
+ const haystack = stripMarkup(stripped);
591
+ for (const candidate of TextSegmentMap._wordVariants(stripMarkup(remainingWord))) {
592
+ if (candidate && haystack.startsWith(candidate)) {
593
+ const rawLen = rawOffsetAfterCleanChars(stripped, candidate.length);
594
+ return hop(_HopKind.PLACED, { segmentAdvance: leadWs + rawLen });
595
+ }
596
+ }
597
+ return null;
598
+ }
599
+ /**
600
+ * Look for `remainingWord` a few words further into this segment.
601
+ *
602
+ * Reached only once the three strategies above have all declined, which
603
+ * means the provider garbled or dropped an event and the text at the cursor
604
+ * will never be reported on its own. Matching a word a little further on
605
+ * puts the segment back in step, and the text stepped over is consumed with
606
+ * the word that found it rather than being lost.
607
+ *
608
+ * Two limits keep a coincidence from being read as a recovery. Only whole
609
+ * words anchor the match, so a partial prefix is not enough, and only the
610
+ * next `LOOKAHEAD_WORDS` of them are tried, so a word repeated later in a
611
+ * long segment cannot swallow everything before it.
612
+ *
613
+ * Word separators are what those anchors are found by, so a script written
614
+ * without them -- Japanese, Chinese -- offers a single word start for the
615
+ * whole run and never matches here; frames in one recover through
616
+ * force-complete instead. Spaced scripts, Korean among them, recover
617
+ * normally.
618
+ *
619
+ * `_foldedHop` alone does the matching, since it already demands the whole
620
+ * word this needs and folding only ever widens what matches -- the fold is
621
+ * one character for one, so anything matching literally matches folded too.
622
+ *
623
+ * A segment holding markup is left alone: tag names are made of letters, so
624
+ * a word start inside one would read as something spoken. That also keeps
625
+ * the match from anchoring inside a rewritten span whose words carry tags.
626
+ */
627
+ static _lookaheadHop(segmentRemaining, remainingWord) {
628
+ if (segmentRemaining.includes("<")) {
629
+ return null;
630
+ }
631
+ const wordStarts = [];
632
+ let prevAlnum = false;
633
+ let i = 0;
634
+ for (const ch of segmentRemaining) {
635
+ const alnum = isAlnum(ch);
636
+ if (alnum && !prevAlnum) {
637
+ wordStarts.push(i);
638
+ }
639
+ prevAlnum = alnum;
640
+ i += ch.length;
641
+ }
642
+ // The first word start is where the strategies above already looked.
643
+ for (const offset of wordStarts.slice(1, 1 + TextSegmentMap.LOOKAHEAD_WORDS)) {
644
+ const candidates = [[segmentRemaining.slice(offset), offset]];
645
+ const h = TextSegmentMap._foldedHop(candidates, remainingWord);
646
+ if (h !== null && h.kind === _HopKind.PLACED) {
647
+ return h;
648
+ }
649
+ }
650
+ return null;
651
+ }
652
+ /**
653
+ * Decide what `remainingWord` does to the text left in this segment.
654
+ *
655
+ * Everything here is plain string comparison. No tag names are understood,
656
+ * and nothing is remembered between calls.
657
+ *
658
+ * Four ways of matching are tried, each more forgiving than the one before:
659
+ * `_literalHop`, then `_foldedHop`, then `_markupHop`. Any of them can
660
+ * report that the word fits here (`PLACED`) or that it runs past the end of
661
+ * the segment (`CROSSES`). Last comes `_lookaheadHop`, which places the word
662
+ * a few words further in, stepping over text a garbled or dropped event left
663
+ * unaccounted for.
664
+ *
665
+ * If none of them match, the answer depends on what is left in the segment:
666
+ *
667
+ * - `EXHAUSTED` when nothing here can be spoken -- only a tag such as
668
+ * `<break/>`, or trailing spaces and punctuation. The segment is finished
669
+ * so the word can try the next one. This is checked last, so a word that
670
+ * really does match something like a trailing emoji is found by the
671
+ * passes above first.
672
+ * - `NO_MATCH` otherwise. The word is taken to stand for the next run of
673
+ * punctuation (see `_unmatchedSymbolLen`), so the cursors step past that
674
+ * run alone, never past a letter or digit.
675
+ */
676
+ static _classifyHop(segmentRemaining, remainingWord) {
677
+ const candidates = TextSegmentMap._matchCandidates(segmentRemaining);
678
+ let h = TextSegmentMap._literalHop(candidates, remainingWord);
679
+ if (h === null) {
680
+ h = TextSegmentMap._foldedHop(candidates, remainingWord);
681
+ }
682
+ if (h === null) {
683
+ h = TextSegmentMap._markupHop(segmentRemaining, remainingWord);
684
+ }
685
+ if (h === null) {
686
+ h = TextSegmentMap._lookaheadHop(segmentRemaining, remainingWord);
687
+ }
688
+ if (h !== null) {
689
+ return h;
690
+ }
691
+ // Nothing left here that can be spoken: finish the segment so the word
692
+ // can try the next one.
693
+ if (!hasAlnum(segmentRemaining)) {
694
+ return hop(_HopKind.EXHAUSTED);
695
+ }
696
+ return hop(_HopKind.NO_MATCH, {
697
+ segmentAdvance: TextSegmentMap._unmatchedSymbolLen(segmentRemaining),
698
+ });
699
+ }
700
+ /**
701
+ * Count how far a word that matched nothing moves the cursor.
702
+ *
703
+ * Such a word is usually a symbol the provider reports differently from the
704
+ * text -- `"-"` for `"โ†’"` -- so it stands for the next run of punctuation,
705
+ * and the cursor steps past that run alone:
706
+ *
707
+ * " โ†’ step two" -> 2, stops before " step"
708
+ * " โ†’ ### **Title**" -> 2, stops before " ###"
709
+ *
710
+ * Stopping at whitespace is what matters: the symbols after it (`###`,
711
+ * `**`) arrive as events of their own, and stepping past them would leave
712
+ * those events nothing to match.
713
+ */
714
+ static _unmatchedSymbolLen(segmentRemaining) {
715
+ let i = segmentRemaining.length - segmentRemaining.trimStart().length;
716
+ while (i < segmentRemaining.length) {
717
+ const ch = charAt(segmentRemaining, i);
718
+ if (isAlnum(ch) || isSpace(ch)) {
719
+ break;
720
+ }
721
+ i += ch.length;
722
+ }
723
+ return i;
724
+ }
725
+ /**
726
+ * Move every cursor to `newPos` within `seg`, and finish `seg` if reached.
727
+ *
728
+ * This is where the "keep pace or wait" rule from the class docstring is
729
+ * applied, and the only place the cursors into the other two texts move.
730
+ *
731
+ * **Unchanged segment** -- both sides hold the same text, so the other two
732
+ * cursors keep pace. They move by a count, not a position: however many
733
+ * letters and digits this step used up in the spoken text is how many they
734
+ * move past. `advanceByAlnums` does that, crossing tags for free (which is
735
+ * how a tag travels with the word beside it) and taking any punctuation
736
+ * stuck to the end of the word, so `"you?"` moves as one.
737
+ *
738
+ * **Rewritten segment** -- the other two cursors do not move at all until
739
+ * the segment is finished, then jump straight to its end.
740
+ *
741
+ * Either way a cursor stops before trailing spaces, which belong to
742
+ * whatever comes next.
743
+ */
744
+ _advanceCursorsTo(seg, newPos) {
745
+ if (seg.isTransformed) {
746
+ // Whatever is left is only a closing tag or the like, which no word
747
+ // event will ever name. Take it now so the segment can finish.
748
+ // Unchanged segments are not given this: a trailing emoji there is
749
+ // real output, and its own event is still coming.
750
+ if (!hasAlnum(seg.tts.slice(newPos))) {
751
+ newPos = seg.tts.length;
752
+ }
753
+ }
754
+ else {
755
+ this._keepDerivedCursorsInPace(seg, newPos);
756
+ }
757
+ this._segRawPos = newPos;
758
+ if (newPos >= seg.tts.length) {
759
+ if (seg.isTransformed) {
760
+ this._commitTransformedSpan(seg);
761
+ }
762
+ this._finishSegment(seg);
763
+ }
764
+ }
765
+ /**
766
+ * Move the cursors into the other two texts by what this step just spoke.
767
+ *
768
+ * The user-facing cursor moves by the count of letters and digits consumed
769
+ * here. The two LLM cursors have their own rule -- see `_advanceLlmCursors`.
770
+ */
771
+ _keepDerivedCursorsInPace(seg, newPos) {
772
+ const crossed = seg.tts.slice(this._segRawPos, newPos);
773
+ const nAlnum = alnumOnly(crossed).length;
774
+ if (nAlnum) {
775
+ this._userFacingPos = advanceByAlnums(this._originalText, this._userFacingPos, nAlnum);
776
+ }
777
+ else {
778
+ // A token with no letters or digits to spend -- punctuation set
779
+ // off by a space, as French writes it ("va ?", "Attention :").
780
+ // There is no budget to advance by, so step straight to where the
781
+ // raw cursor got to, and the mark leaves the remaining text now
782
+ // rather than a word later. Both sides are identical here, so that
783
+ // offset is exact.
784
+ this._userFacingPos = seg.originalStart + seg.tts.slice(0, newPos).trimEnd().length;
785
+ }
786
+ this._advanceLlmCursors(crossed, nAlnum);
787
+ }
788
+ /**
789
+ * Move both LLM cursors for a step that just spoke `crossed`.
790
+ *
791
+ * The two answer different questions and so stop in different places,
792
+ * which is the whole reason there are two of them:
793
+ *
794
+ * - `llmSpokenPos` -- what has been **reported spoken**. It crosses exactly
795
+ * the characters the raw cursor did, so it stops in front of a mark no
796
+ * event has arrived for.
797
+ * - `llmPos` -- what has been **attributed**. It also takes a mark stuck to
798
+ * the end of the word, since the conversation context is rebuilt by
799
+ * joining the attributed spans and every character has to belong to one
800
+ * of them: `"Yeah,"` then `"I"` reads back correctly where `"Yeah"` then
801
+ * `", I"` would put a space before the comma.
802
+ *
803
+ * Attribution never moves backwards and never trails what was spoken, so a
804
+ * step that spends no budget on the attributed side -- an emoji, a symbol
805
+ * the source spells differently -- is still credited to the word that
806
+ * crossed it.
807
+ */
808
+ _advanceLlmCursors(crossed, nAlnum) {
809
+ this._llmSpokenPos = advanceByChars(this._llmText, this._llmSpokenPos, crossed.length);
810
+ this._llmPos = Math.max(advanceByAlnums(this._llmText, this._llmPos, nAlnum), this._llmSpokenPos);
811
+ }
812
+ /** Jump the other two cursors to the end of `seg`, now that it is done. */
813
+ _commitTransformedSpan(seg) {
814
+ this._userFacingPos = seg.originalEnd;
815
+ // The original's count, not the TTS side's: llmText holds "$42.50"
816
+ // (4 alnums), never the spoken "forty two dollars".
817
+ this._llmPos = advanceByAlnums(this._llmText, this._llmPos, seg.originalAlnumCount);
818
+ // The span is spoken in full or not at all, so both cursors land together.
819
+ this._llmSpokenPos = this._llmPos;
820
+ }
821
+ /** Record `seg` as finished and move on to the next segment. */
822
+ _finishSegment(seg) {
823
+ this._lastCompleted = seg;
824
+ this._segIdx += 1;
825
+ this._segRawPos = 0;
826
+ }
827
+ /**
828
+ * Work out what `word` would do, without moving anything.
829
+ *
830
+ * This decides; `_consumeWord` acts. Keeping them apart is what stops them
831
+ * disagreeing, since `_canConsumeWord` asks the same question and must get
832
+ * the same answer.
833
+ *
834
+ * Most words are placed by the first segment tried, and the walk stops
835
+ * there. It goes on when a segment cannot finish the job -- the word runs
836
+ * past it, or it has nothing speakable left -- and whatever remains of the
837
+ * word is offered to the next segment.
838
+ *
839
+ * @returns What each segment answered, in order, and whatever is left of
840
+ * the word once the segments run out. Anything left over is the word
841
+ * running past the end of this text.
842
+ */
843
+ _planHops(word) {
844
+ let segIdx = this._segIdx;
845
+ let rawPos = this._segRawPos;
846
+ let remainingWord = word;
847
+ const hops = [];
848
+ while (remainingWord && segIdx < this._segments.length) {
849
+ const h = TextSegmentMap._classifyHop(this._segments[segIdx].tts.slice(rawPos), remainingWord);
850
+ hops.push(h);
851
+ // PLACED and NO_MATCH both end the walk; the other two carry on.
852
+ if (h.kind === _HopKind.PLACED || h.kind === _HopKind.NO_MATCH) {
853
+ return [hops, ""];
854
+ }
855
+ remainingWord = remainingWord.slice(h.wordConsumed);
856
+ segIdx += 1;
857
+ rawPos = 0;
858
+ }
859
+ return [hops, remainingWord];
860
+ }
861
+ /**
862
+ * Move the cursors according to what `_planHops` decided.
863
+ *
864
+ * Anything left unplaced ran past the end of this text and is kept in
865
+ * `lastOverflow`, for the caller to hand to the next frame.
866
+ */
867
+ _consumeWord(word) {
868
+ const [hops, overflow] = this._planHops(word);
869
+ for (const h of hops) {
870
+ const seg = this._segments[this._segIdx];
871
+ if (h.kind === _HopKind.PLACED || h.kind === _HopKind.NO_MATCH) {
872
+ // NO_MATCH is a symbol the provider reports differently from the
873
+ // text ("-" for "โ†’"). It was still spoken, so every cursor moves
874
+ // past the symbol it stands for, the same as a placed word.
875
+ this._advanceCursorsTo(seg, this._segRawPos + h.segmentAdvance);
876
+ }
877
+ else {
878
+ // CROSSES or EXHAUSTED: this segment is done either way, and the
879
+ // next hop was classified against the one after it.
880
+ this._advanceCursorsTo(seg, seg.tts.length);
881
+ }
882
+ }
883
+ if (overflow) {
884
+ this._lastOverflow = overflow;
885
+ }
886
+ }
887
+ /**
888
+ * Take one spoken word and move every cursor to where it ends.
889
+ *
890
+ * Afterwards `lastCompletedSegment`, `lastOverflow` and
891
+ * `lastLeadingDuplicate` describe what this particular word did; each is
892
+ * cleared at the start of the next call.
893
+ *
894
+ * @param word One token from the provider's word-timestamp stream. It may
895
+ * be a plain word, a word carrying its own spacing or punctuation, or a
896
+ * fragment of a half-open tag -- matching is textual, so the caller does
897
+ * not have to know which.
898
+ */
899
+ advanceWord(word) {
900
+ this._lastCompleted = null;
901
+ this._lastOverflow = null;
902
+ this._lastLeadingDuplicate = 0;
903
+ if (word) {
904
+ this._lastLeadingDuplicate = this._leadingDuplicateLen(word);
905
+ this._consumeWord(word);
906
+ }
907
+ }
908
+ /**
909
+ * Count characters at the start of `word` that were already spoken.
910
+ *
911
+ * In `"Yeah, I can"` the comma travels with `"Yeah"`. If the provider then
912
+ * reports the next word as `", I"` instead of `"I"`, the comma would be
913
+ * recorded twice, so this returns 2 -- the comma and its space.
914
+ *
915
+ * Returns 0 when that punctuation is new text instead (`'"hello'`), and
916
+ * when the word is nothing but punctuation, which is a mark arriving on its
917
+ * own. Must be called before the cursors move, while `llmPos` still sits at
918
+ * the end of the previous word.
919
+ */
920
+ _leadingDuplicateLen(word) {
921
+ let i = 0;
922
+ while (i < word.length && (isSpace(word[i]) || isPunctuation(word[i]))) {
923
+ i += 1;
924
+ }
925
+ if (i >= word.length) {
926
+ return 0;
927
+ }
928
+ const mark = word.slice(0, i).trim();
929
+ if (!mark) {
930
+ return 0;
931
+ }
932
+ const start = this._llmPos - mark.length;
933
+ if (start < 0 || this._llmText.slice(start, this._llmPos) !== mark) {
934
+ return 0;
935
+ }
936
+ return i;
937
+ }
938
+ /**
939
+ * Return true if `word` could be the next thing spoken here.
940
+ *
941
+ * `advanceWord` without the moving, so a caller can check first. A false
942
+ * answer means the provider skipped ahead, and the word should go to the
943
+ * next frame instead.
944
+ *
945
+ * A word with no letters or digits gets a second chance from
946
+ * `_symbolBelongsHere`, since there is nothing in it to match on.
947
+ */
948
+ wordBelongsCurrentSegment(word) {
949
+ if (!word) {
950
+ return true;
951
+ }
952
+ if (this._canConsumeWord(word)) {
953
+ return true;
954
+ }
955
+ if (!hasAlnum(word)) {
956
+ return this._symbolBelongsHere(word);
957
+ }
958
+ return false;
959
+ }
960
+ /**
961
+ * Ask whether this word would be placed, without placing it.
962
+ *
963
+ * True if some segment would take the word, or if the word simply runs off
964
+ * the end. False if there are no segments left, or a segment turns it down.
965
+ */
966
+ _canConsumeWord(word) {
967
+ if (this._segIdx >= this._segments.length) {
968
+ return false;
969
+ }
970
+ const [hops] = this._planHops(word);
971
+ return hops.length === 0 || hops[hops.length - 1].kind !== _HopKind.NO_MATCH;
972
+ }
973
+ /**
974
+ * Decide whether a word made only of punctuation or symbols belongs here.
975
+ *
976
+ * There is nothing in such a word to match on, so it gets two chances:
977
+ *
978
+ * 1. Look for the word itself in the text still to be spoken. The search
979
+ * starts a little before the cursor, because punctuation is often taken
980
+ * along with the word before it.
981
+ *
982
+ * 2. Accept it as a stand-in. Some providers report a different symbol than
983
+ * the one they were given -- ElevenLabs reports `"->"` as `"-"` -- so the
984
+ * first check can never succeed. If words remain to be spoken and the
985
+ * next thing in the text is itself a symbol, treat the word as that
986
+ * symbol.
987
+ */
988
+ _symbolBelongsHere(word) {
989
+ let pos = this.rawPos;
990
+ let searchStart = pos;
991
+ while (searchStart > 0) {
992
+ const ch = this._ttsText[searchStart - 1];
993
+ if (isAlnum(ch) || isSpace(ch) || ch === ">") {
994
+ break;
995
+ }
996
+ searchStart -= 1;
997
+ }
998
+ if (this._ttsText.slice(searchStart).includes(word)) {
999
+ return true;
1000
+ }
1001
+ if (this._segIdx >= this._segments.length) {
1002
+ return false;
1003
+ }
1004
+ while (pos < this._ttsText.length && isSpace(this._ttsText[pos])) {
1005
+ pos += 1;
1006
+ }
1007
+ return pos < this._ttsText.length && !isAlnum(charAt(this._ttsText, pos));
1008
+ }
1009
+ /** How far into the user-facing text the spoken words have reached. */
1010
+ get userFacingPos() {
1011
+ return this._userFacingPos;
1012
+ }
1013
+ /**
1014
+ * How far into the LLM's text has been attributed to a word so far.
1015
+ *
1016
+ * Ahead of `llmSpokenPos` whenever a word swept up a mark that no event has
1017
+ * reported yet; the text between the two is attributed but not yet spoken.
1018
+ */
1019
+ get llmPos() {
1020
+ return this._llmPos;
1021
+ }
1022
+ /**
1023
+ * How far into the LLM's text the provider has actually reported speaking.
1024
+ *
1025
+ * What a caller showing progress, or ending a frame early, should read:
1026
+ * text past this point has had no word event, so it still belongs to what
1027
+ * is left to say even when `llmPos` has already credited it to a word.
1028
+ */
1029
+ get llmSpokenPos() {
1030
+ return this._llmSpokenPos;
1031
+ }
1032
+ /** How far into `ttsText` the provider has spoken, counted from its start. */
1033
+ get rawPos() {
1034
+ let pos = 0;
1035
+ for (const s of this._segments.slice(0, this._segIdx)) {
1036
+ pos += s.tts.length;
1037
+ }
1038
+ if (this._segIdx < this._segments.length) {
1039
+ pos += this._segRawPos;
1040
+ }
1041
+ return pos;
1042
+ }
1043
+ /**
1044
+ * The end of the last word passed to `advanceWord`, if it did not fit.
1045
+ *
1046
+ * `null` most of the time. It is set only when that word ran past the end
1047
+ * of `ttsText`, with no segment left to take the rest, which means the
1048
+ * leftover belongs to the next frame. It is always the tail of the word
1049
+ * that was passed in, so the part that did fit is
1050
+ * `word.slice(0, word.length - lastOverflow.length)`.
1051
+ */
1052
+ get lastOverflow() {
1053
+ return this._lastOverflow;
1054
+ }
1055
+ /**
1056
+ * How much of the last word's start was punctuation already spoken.
1057
+ *
1058
+ * The opposite end of the word from `lastOverflow`: that one is about a
1059
+ * tail running past this text, this one about a head repeating punctuation
1060
+ * the previous word already took. Cut both off to get the part of the word
1061
+ * that belongs to this frame:
1062
+ *
1063
+ * word.slice(lastLeadingDuplicate, word.length - (lastOverflow ?? "").length)
1064
+ */
1065
+ get lastLeadingDuplicate() {
1066
+ return this._lastLeadingDuplicate;
1067
+ }
1068
+ /**
1069
+ * True once every letter and digit in the text has been spoken.
1070
+ *
1071
+ * This is not the same as the cursor reaching the end. If all that is left
1072
+ * is punctuation or tags, the text counts as finished even though those
1073
+ * characters have not been walked over, because no word event is coming
1074
+ * for them.
1075
+ *
1076
+ * There is one exception. Punctuation separated from its word by a space,
1077
+ * as French writes `"Comment รงa va ?"`, does arrive as its own word event,
1078
+ * so the text stays unfinished until it does (see
1079
+ * `_pendingSeparatedPunctuation`). Punctuation stuck to the word itself, as
1080
+ * in `"you?"`, was already taken with the word.
1081
+ */
1082
+ get isComplete() {
1083
+ if (this._segIdx >= this._segments.length) {
1084
+ return true;
1085
+ }
1086
+ const seg = this._segments[this._segIdx];
1087
+ if (hasAlnum(seg.tts.slice(this._segRawPos))) {
1088
+ return false;
1089
+ }
1090
+ if (TextSegmentMap._pendingSeparatedPunctuation(seg.tts.slice(this._segRawPos))) {
1091
+ return false;
1092
+ }
1093
+ return this._segments.slice(this._segIdx + 1).every((s) => !hasAlnum(s.tts));
1094
+ }
1095
+ /**
1096
+ * True when all that is left is punctuation set off from its word by a space.
1097
+ *
1098
+ * Some languages write a space before a mark, as in `"va ?"` or
1099
+ * `"Bonjour !"`. A TTS reports that mark as a word of its own, so the
1100
+ * segment has to stay open until it arrives.
1101
+ *
1102
+ * Only real punctuation counts. A trailing emoji or arrow (`"day! ๐Ÿ˜Š"`,
1103
+ * `"โ†’"`) never arrives as its own word, so it must not keep the segment
1104
+ * open, and tags are removed first for the same reason. Only called once no
1105
+ * letters or digits are left.
1106
+ */
1107
+ static _pendingSeparatedPunctuation(remaining) {
1108
+ const strippedMarkup = stripCompleteMarkup(remaining);
1109
+ if (!isSpace(strippedMarkup[0])) {
1110
+ return false;
1111
+ }
1112
+ const content = strippedMarkup.trim();
1113
+ return content.length > 0 && isPunctuation(charAt(content, 0));
1114
+ }
1115
+ /** True when the cursor is partway through a rewritten segment. */
1116
+ get inTransformedSegment() {
1117
+ if (this._segIdx >= this._segments.length) {
1118
+ return false;
1119
+ }
1120
+ const seg = this._segments[this._segIdx];
1121
+ return seg.isTransformed && this._segRawPos > 0;
1122
+ }
1123
+ /** The segment finished by the last `advanceWord` call, if any. */
1124
+ get lastCompletedSegment() {
1125
+ return this._lastCompleted;
1126
+ }
1127
+ /** Put every cursor back to the start of the text. */
1128
+ reset() {
1129
+ this._resetState();
1130
+ }
1131
+ }
1132
+ //# sourceMappingURL=text_segment_map.js.map