@komaa/standin-sdk 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 (269) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +141 -0
  3. package/dist/ambient.d.ts +80 -0
  4. package/dist/ambient.d.ts.map +1 -0
  5. package/dist/ambient.js +281 -0
  6. package/dist/ambient.js.map +1 -0
  7. package/dist/attachments.d.ts +149 -0
  8. package/dist/attachments.d.ts.map +1 -0
  9. package/dist/attachments.js +451 -0
  10. package/dist/attachments.js.map +1 -0
  11. package/dist/audio.d.ts +80 -0
  12. package/dist/audio.d.ts.map +1 -0
  13. package/dist/audio.js +170 -0
  14. package/dist/audio.js.map +1 -0
  15. package/dist/avatar.d.ts +111 -0
  16. package/dist/avatar.d.ts.map +1 -0
  17. package/dist/avatar.js +201 -0
  18. package/dist/avatar.js.map +1 -0
  19. package/dist/callServer.d.ts +151 -0
  20. package/dist/callServer.d.ts.map +1 -0
  21. package/dist/callServer.js +1097 -0
  22. package/dist/callServer.js.map +1 -0
  23. package/dist/callTools.d.ts +154 -0
  24. package/dist/callTools.d.ts.map +1 -0
  25. package/dist/callTools.js +255 -0
  26. package/dist/callTools.js.map +1 -0
  27. package/dist/chat.d.ts +226 -0
  28. package/dist/chat.d.ts.map +1 -0
  29. package/dist/chat.js +574 -0
  30. package/dist/chat.js.map +1 -0
  31. package/dist/config.d.ts +32 -0
  32. package/dist/config.d.ts.map +1 -0
  33. package/dist/config.js +86 -0
  34. package/dist/config.js.map +1 -0
  35. package/dist/consult.d.ts +147 -0
  36. package/dist/consult.d.ts.map +1 -0
  37. package/dist/consult.js +388 -0
  38. package/dist/consult.js.map +1 -0
  39. package/dist/delivery.d.ts +81 -0
  40. package/dist/delivery.d.ts.map +1 -0
  41. package/dist/delivery.js +187 -0
  42. package/dist/delivery.js.map +1 -0
  43. package/dist/echoGuard.d.ts +56 -0
  44. package/dist/echoGuard.d.ts.map +1 -0
  45. package/dist/echoGuard.js +57 -0
  46. package/dist/echoGuard.js.map +1 -0
  47. package/dist/errors.d.ts +5 -0
  48. package/dist/errors.d.ts.map +1 -0
  49. package/dist/errors.js +10 -0
  50. package/dist/errors.js.map +1 -0
  51. package/dist/fetch.d.ts +40 -0
  52. package/dist/fetch.d.ts.map +1 -0
  53. package/dist/fetch.js +240 -0
  54. package/dist/fetch.js.map +1 -0
  55. package/dist/gate.d.ts +80 -0
  56. package/dist/gate.d.ts.map +1 -0
  57. package/dist/gate.js +249 -0
  58. package/dist/gate.js.map +1 -0
  59. package/dist/handler.d.ts +283 -0
  60. package/dist/handler.d.ts.map +1 -0
  61. package/dist/handler.js +4 -0
  62. package/dist/handler.js.map +1 -0
  63. package/dist/hmac.d.ts +32 -0
  64. package/dist/hmac.d.ts.map +1 -0
  65. package/dist/hmac.js +108 -0
  66. package/dist/hmac.js.map +1 -0
  67. package/dist/index.d.ts +59 -0
  68. package/dist/index.d.ts.map +1 -0
  69. package/dist/index.js +60 -0
  70. package/dist/index.js.map +1 -0
  71. package/dist/lane.d.ts +92 -0
  72. package/dist/lane.d.ts.map +1 -0
  73. package/dist/lane.js +269 -0
  74. package/dist/lane.js.map +1 -0
  75. package/dist/lipsync.d.ts +136 -0
  76. package/dist/lipsync.d.ts.map +1 -0
  77. package/dist/lipsync.js +310 -0
  78. package/dist/lipsync.js.map +1 -0
  79. package/dist/log.d.ts +18 -0
  80. package/dist/log.d.ts.map +1 -0
  81. package/dist/log.js +20 -0
  82. package/dist/log.js.map +1 -0
  83. package/dist/media.d.ts +43 -0
  84. package/dist/media.d.ts.map +1 -0
  85. package/dist/media.js +190 -0
  86. package/dist/media.js.map +1 -0
  87. package/dist/minutes.d.ts +398 -0
  88. package/dist/minutes.d.ts.map +1 -0
  89. package/dist/minutes.js +805 -0
  90. package/dist/minutes.js.map +1 -0
  91. package/dist/outbound.d.ts +388 -0
  92. package/dist/outbound.d.ts.map +1 -0
  93. package/dist/outbound.js +933 -0
  94. package/dist/outbound.js.map +1 -0
  95. package/dist/plugins/cartesia/agent.d.ts +58 -0
  96. package/dist/plugins/cartesia/agent.d.ts.map +1 -0
  97. package/dist/plugins/cartesia/agent.js +254 -0
  98. package/dist/plugins/cartesia/agent.js.map +1 -0
  99. package/dist/plugins/cartesia/cli.d.ts +3 -0
  100. package/dist/plugins/cartesia/cli.d.ts.map +1 -0
  101. package/dist/plugins/cartesia/cli.js +10 -0
  102. package/dist/plugins/cartesia/cli.js.map +1 -0
  103. package/dist/plugins/cartesia/config.d.ts +30 -0
  104. package/dist/plugins/cartesia/config.d.ts.map +1 -0
  105. package/dist/plugins/cartesia/config.js +29 -0
  106. package/dist/plugins/cartesia/config.js.map +1 -0
  107. package/dist/plugins/cartesia/handler.d.ts +40 -0
  108. package/dist/plugins/cartesia/handler.d.ts.map +1 -0
  109. package/dist/plugins/cartesia/handler.js +142 -0
  110. package/dist/plugins/cartesia/handler.js.map +1 -0
  111. package/dist/plugins/cartesia/index.d.ts +6 -0
  112. package/dist/plugins/cartesia/index.d.ts.map +1 -0
  113. package/dist/plugins/cartesia/index.js +54 -0
  114. package/dist/plugins/cartesia/index.js.map +1 -0
  115. package/dist/plugins/deepgram/agent.d.ts +60 -0
  116. package/dist/plugins/deepgram/agent.d.ts.map +1 -0
  117. package/dist/plugins/deepgram/agent.js +302 -0
  118. package/dist/plugins/deepgram/agent.js.map +1 -0
  119. package/dist/plugins/deepgram/cli.d.ts +3 -0
  120. package/dist/plugins/deepgram/cli.d.ts.map +1 -0
  121. package/dist/plugins/deepgram/cli.js +10 -0
  122. package/dist/plugins/deepgram/cli.js.map +1 -0
  123. package/dist/plugins/deepgram/config.d.ts +39 -0
  124. package/dist/plugins/deepgram/config.d.ts.map +1 -0
  125. package/dist/plugins/deepgram/config.js +66 -0
  126. package/dist/plugins/deepgram/config.js.map +1 -0
  127. package/dist/plugins/deepgram/handler.d.ts +87 -0
  128. package/dist/plugins/deepgram/handler.d.ts.map +1 -0
  129. package/dist/plugins/deepgram/handler.js +268 -0
  130. package/dist/plugins/deepgram/handler.js.map +1 -0
  131. package/dist/plugins/deepgram/index.d.ts +11 -0
  132. package/dist/plugins/deepgram/index.d.ts.map +1 -0
  133. package/dist/plugins/deepgram/index.js +76 -0
  134. package/dist/plugins/deepgram/index.js.map +1 -0
  135. package/dist/plugins/echo/cli.d.ts +3 -0
  136. package/dist/plugins/echo/cli.d.ts.map +1 -0
  137. package/dist/plugins/echo/cli.js +10 -0
  138. package/dist/plugins/echo/cli.js.map +1 -0
  139. package/dist/plugins/echo/index.d.ts +37 -0
  140. package/dist/plugins/echo/index.d.ts.map +1 -0
  141. package/dist/plugins/echo/index.js +61 -0
  142. package/dist/plugins/echo/index.js.map +1 -0
  143. package/dist/plugins/elevenlabs/agent.d.ts +72 -0
  144. package/dist/plugins/elevenlabs/agent.d.ts.map +1 -0
  145. package/dist/plugins/elevenlabs/agent.js +305 -0
  146. package/dist/plugins/elevenlabs/agent.js.map +1 -0
  147. package/dist/plugins/elevenlabs/cli.d.ts +3 -0
  148. package/dist/plugins/elevenlabs/cli.d.ts.map +1 -0
  149. package/dist/plugins/elevenlabs/cli.js +10 -0
  150. package/dist/plugins/elevenlabs/cli.js.map +1 -0
  151. package/dist/plugins/elevenlabs/config.d.ts +27 -0
  152. package/dist/plugins/elevenlabs/config.d.ts.map +1 -0
  153. package/dist/plugins/elevenlabs/config.js +42 -0
  154. package/dist/plugins/elevenlabs/config.js.map +1 -0
  155. package/dist/plugins/elevenlabs/handler.d.ts +67 -0
  156. package/dist/plugins/elevenlabs/handler.d.ts.map +1 -0
  157. package/dist/plugins/elevenlabs/handler.js +419 -0
  158. package/dist/plugins/elevenlabs/handler.js.map +1 -0
  159. package/dist/plugins/elevenlabs/index.d.ts +11 -0
  160. package/dist/plugins/elevenlabs/index.d.ts.map +1 -0
  161. package/dist/plugins/elevenlabs/index.js +72 -0
  162. package/dist/plugins/elevenlabs/index.js.map +1 -0
  163. package/dist/plugins/livekit/cli.d.ts +3 -0
  164. package/dist/plugins/livekit/cli.d.ts.map +1 -0
  165. package/dist/plugins/livekit/cli.js +10 -0
  166. package/dist/plugins/livekit/cli.js.map +1 -0
  167. package/dist/plugins/livekit/config.d.ts +47 -0
  168. package/dist/plugins/livekit/config.d.ts.map +1 -0
  169. package/dist/plugins/livekit/config.js +46 -0
  170. package/dist/plugins/livekit/config.js.map +1 -0
  171. package/dist/plugins/livekit/handler.d.ts +59 -0
  172. package/dist/plugins/livekit/handler.d.ts.map +1 -0
  173. package/dist/plugins/livekit/handler.js +211 -0
  174. package/dist/plugins/livekit/handler.js.map +1 -0
  175. package/dist/plugins/livekit/index.d.ts +6 -0
  176. package/dist/plugins/livekit/index.d.ts.map +1 -0
  177. package/dist/plugins/livekit/index.js +68 -0
  178. package/dist/plugins/livekit/index.js.map +1 -0
  179. package/dist/plugins/livekit/room.d.ts +117 -0
  180. package/dist/plugins/livekit/room.d.ts.map +1 -0
  181. package/dist/plugins/livekit/room.js +449 -0
  182. package/dist/plugins/livekit/room.js.map +1 -0
  183. package/dist/plugins/openai/agent.d.ts +74 -0
  184. package/dist/plugins/openai/agent.d.ts.map +1 -0
  185. package/dist/plugins/openai/agent.js +264 -0
  186. package/dist/plugins/openai/agent.js.map +1 -0
  187. package/dist/plugins/openai/cli.d.ts +3 -0
  188. package/dist/plugins/openai/cli.d.ts.map +1 -0
  189. package/dist/plugins/openai/cli.js +10 -0
  190. package/dist/plugins/openai/cli.js.map +1 -0
  191. package/dist/plugins/openai/config.d.ts +29 -0
  192. package/dist/plugins/openai/config.d.ts.map +1 -0
  193. package/dist/plugins/openai/config.js +37 -0
  194. package/dist/plugins/openai/config.js.map +1 -0
  195. package/dist/plugins/openai/handler.d.ts +65 -0
  196. package/dist/plugins/openai/handler.d.ts.map +1 -0
  197. package/dist/plugins/openai/handler.js +297 -0
  198. package/dist/plugins/openai/handler.js.map +1 -0
  199. package/dist/plugins/openai/index.d.ts +6 -0
  200. package/dist/plugins/openai/index.d.ts.map +1 -0
  201. package/dist/plugins/openai/index.js +72 -0
  202. package/dist/plugins/openai/index.js.map +1 -0
  203. package/dist/plugins/openclaw/allowlist.d.ts +9 -0
  204. package/dist/plugins/openclaw/allowlist.d.ts.map +1 -0
  205. package/dist/plugins/openclaw/allowlist.js +11 -0
  206. package/dist/plugins/openclaw/allowlist.js.map +1 -0
  207. package/dist/plugins/openclaw/config.d.ts +50 -0
  208. package/dist/plugins/openclaw/config.d.ts.map +1 -0
  209. package/dist/plugins/openclaw/config.js +53 -0
  210. package/dist/plugins/openclaw/config.js.map +1 -0
  211. package/dist/plugins/openclaw/handler.d.ts +90 -0
  212. package/dist/plugins/openclaw/handler.d.ts.map +1 -0
  213. package/dist/plugins/openclaw/handler.js +146 -0
  214. package/dist/plugins/openclaw/handler.js.map +1 -0
  215. package/dist/plugins/openclaw/index.d.ts +55 -0
  216. package/dist/plugins/openclaw/index.d.ts.map +1 -0
  217. package/dist/plugins/openclaw/index.js +89 -0
  218. package/dist/plugins/openclaw/index.js.map +1 -0
  219. package/dist/plugins/openclaw/openclaw.plugin.json +74 -0
  220. package/dist/plugins/openclaw/package.json +14 -0
  221. package/dist/plugins/openclaw/realtime.d.ts +82 -0
  222. package/dist/plugins/openclaw/realtime.d.ts.map +1 -0
  223. package/dist/plugins/openclaw/realtime.js +238 -0
  224. package/dist/plugins/openclaw/realtime.js.map +1 -0
  225. package/dist/plugins/openclaw/runtime.d.ts +19 -0
  226. package/dist/plugins/openclaw/runtime.d.ts.map +1 -0
  227. package/dist/plugins/openclaw/runtime.js +160 -0
  228. package/dist/plugins/openclaw/runtime.js.map +1 -0
  229. package/dist/policy.d.ts +38 -0
  230. package/dist/policy.d.ts.map +1 -0
  231. package/dist/policy.js +95 -0
  232. package/dist/policy.js.map +1 -0
  233. package/dist/protocol.d.ts +44 -0
  234. package/dist/protocol.d.ts.map +1 -0
  235. package/dist/protocol.js +77 -0
  236. package/dist/protocol.js.map +1 -0
  237. package/dist/protocolRuntime.d.ts +19 -0
  238. package/dist/protocolRuntime.d.ts.map +1 -0
  239. package/dist/protocolRuntime.js +78 -0
  240. package/dist/protocolRuntime.js.map +1 -0
  241. package/dist/smoke.d.ts +65 -0
  242. package/dist/smoke.d.ts.map +1 -0
  243. package/dist/smoke.js +293 -0
  244. package/dist/smoke.js.map +1 -0
  245. package/dist/startup.d.ts +74 -0
  246. package/dist/startup.d.ts.map +1 -0
  247. package/dist/startup.js +112 -0
  248. package/dist/startup.js.map +1 -0
  249. package/dist/tile.d.ts +101 -0
  250. package/dist/tile.d.ts.map +1 -0
  251. package/dist/tile.js +195 -0
  252. package/dist/tile.js.map +1 -0
  253. package/dist/version.d.ts +2 -0
  254. package/dist/version.d.ts.map +1 -0
  255. package/dist/version.js +4 -0
  256. package/dist/version.js.map +1 -0
  257. package/dist/vision.d.ts +160 -0
  258. package/dist/vision.d.ts.map +1 -0
  259. package/dist/vision.js +301 -0
  260. package/dist/vision.js.map +1 -0
  261. package/dist/visionTools.d.ts +237 -0
  262. package/dist/visionTools.d.ts.map +1 -0
  263. package/dist/visionTools.js +578 -0
  264. package/dist/visionTools.js.map +1 -0
  265. package/dist/voice.d.ts +143 -0
  266. package/dist/voice.d.ts.map +1 -0
  267. package/dist/voice.js +403 -0
  268. package/dist/voice.js.map +1 -0
  269. package/package.json +129 -0
@@ -0,0 +1,310 @@
1
+ // Copyright (c) 2026 Komaa DigiTech
2
+ // SPDX-License-Identifier: MIT
3
+ /**
4
+ * Lip-sync: the viseme timeline that makes the avatar's mouth match the words.
5
+ *
6
+ * A realtime speech-to-speech model streams voice and hands back no phoneme
7
+ * timings, so {@link speechMarks} has nothing to carry and the mouth never moves
8
+ * on the default path. This module closes that gap the only honest way open to a
9
+ * worker: walk the spoken text, turn each character into a mouth shape, and
10
+ * spread those shapes over the duration of the audio the worker ACTUALLY SENT
11
+ * for that turn.
12
+ *
13
+ * Spreading over sent audio is the whole reason the estimate is worth sending.
14
+ * The shapes are approximate either way, but the timeline is pinned to a length
15
+ * the worker measured, byte by byte, as the frames went out: the mouth opens
16
+ * when the voice starts and closes when it stops, on a long sentence and a short
17
+ * one alike. {@link TurnLipSync} is that counter, and it is the piece to reach
18
+ * for first.
19
+ *
20
+ * A duration GUESSED from text length or a words-per-minute rate is a different
21
+ * proposition, and it is still worse than sending nothing. Its error compounds
22
+ * sentence after sentence, so the mouth drifts further from the voice the longer
23
+ * the call runs, and a mouth moving against the voice reads as broken in a way a
24
+ * still mouth never does. Measured beats still; guessed loses to still.
25
+ *
26
+ * Better again, when the speech provider hands back per-character timings, is
27
+ * {@link visemesFromAlignment}: same table, real times, no estimate at all.
28
+ *
29
+ * The table covers Latin and Arabic in one map. Without the Arabic rows an
30
+ * Arabic reply produces no tokens, carries no timeline, and the mouth simply
31
+ * does not move for half the people who will be on these calls.
32
+ *
33
+ * Timing anchor: `tMs` counts from the START of that turn's audio, never from
34
+ * the moment the message reaches the service. Where the duration is known before
35
+ * playback (a text-to-speech path) send the timeline ahead of the first audio
36
+ * frame. On a realtime path it is known only once the turn ends, so the marks
37
+ * necessarily go out after the last chunk was handed over, and that is correct
38
+ * only while the service still holds the turn's audio buffered for playout.
39
+ *
40
+ * Identical in shape to the Python SDK's `standin.lipsync`.
41
+ */
42
+ import { BYTES_PER_SAMPLE } from "./audio.js";
43
+ import { SAMPLE_RATE_HZ } from "./protocol.js";
44
+ /**
45
+ * A closed mouth, and what the space between two words becomes. Every other
46
+ * unmapped character is skipped instead: punching silence into "3.5%" would
47
+ * close the mouth in the middle of a spoken number.
48
+ */
49
+ export const SILENCE_VISEME = 0;
50
+ /**
51
+ * A viseme id no character can carry, so the first token of a turn always counts
52
+ * as a change. Starting the run collapser at 0 instead would swallow a leading
53
+ * silence mark, which is what anchors the mouth shut before the first vowel.
54
+ */
55
+ const NO_VISEME = -1;
56
+ // Mouth shape to every character that wears it, in the avatar lane's 0 to 21
57
+ // numbering. All 26 Latin letters and all 28 Arabic letters are here on purpose:
58
+ // one common letter left out thins the timeline unevenly and the mouth stalls on
59
+ // that syllable. The Arabic rows carry the eight variant forms too.
60
+ const SHAPES = [
61
+ [2, "a"],
62
+ [2, "اأإآىة"], // alef, with hamza above and below, madda, maqsura, teh marbuta
63
+ [2, "َ"], // fatha
64
+ [4, "e"],
65
+ [6, "iy"],
66
+ [6, "يئ"], // yeh, yeh with hamza
67
+ [6, "ِ"], // kasra
68
+ [7, "uw"],
69
+ [7, "و"], // waw
70
+ [7, "ُ"], // damma
71
+ [8, "o"],
72
+ [12, "h"],
73
+ [12, "هحعءؤ"], // heh, hah, ain, hamza, waw with hamza
74
+ [13, "r"],
75
+ [13, "ر"], // reh
76
+ [14, "l"],
77
+ [14, "ل"], // lam
78
+ [15, "szx"],
79
+ [15, "سصز"], // seen, sad, zain
80
+ [16, "j"],
81
+ [16, "شج"], // sheen, jeem
82
+ [18, "fv"],
83
+ [18, "ف"], // feh
84
+ [19, "tdn"],
85
+ [19, "تدنطضثذظ"], // teh, dal, noon, tah, dad, theh, thal, zah
86
+ [20, "kgcq"],
87
+ [20, "كقغخ"], // kaf, qaf, ghain, khah
88
+ [21, "mbp"],
89
+ [21, "مب"], // meem, beh
90
+ ];
91
+ function buildTable() {
92
+ // No prototype, so a lookup of "constructor" or "__proto__" answers undefined
93
+ // the way every other unmapped character does. A plain object literal would
94
+ // hand those two an inherited value instead, and the Python twin returns None
95
+ // for both.
96
+ const table = Object.create(null);
97
+ for (const [visemeId, chars] of SHAPES) {
98
+ for (const ch of chars)
99
+ table[ch] = visemeId;
100
+ }
101
+ return table;
102
+ }
103
+ /**
104
+ * Character to mouth shape, read-only. Sukun, shadda, tanween, the tatweel and
105
+ * every presentation form are deliberately absent: a stretch mark and a doubling
106
+ * mark carry no mouth shape of their own, and mapping them would insert phantom
107
+ * mouth changes into an otherwise correct timeline.
108
+ */
109
+ export const CHAR_VISEMES = Object.freeze(buildTable());
110
+ /**
111
+ * The mouth shape a character wears, or undefined when it has none.
112
+ *
113
+ * The lookup is on the raw character after lowercasing, with no Unicode
114
+ * normalization at all: adding NFKC here would change which characters map and
115
+ * the two SDKs would disagree on the same string. Digits, punctuation, the
116
+ * tatweel and the non-vowel diacritics come back undefined, and every caller
117
+ * skips them rather than holding the mouth closed over them.
118
+ */
119
+ export function visemeForChar(ch) {
120
+ return CHAR_VISEMES[ch.toLowerCase()];
121
+ }
122
+ /** One character's token: silence for a space, the map for anything else. */
123
+ function tokenFor(ch) {
124
+ return ch === " " ? SILENCE_VISEME : visemeForChar(ch);
125
+ }
126
+ /**
127
+ * Collapse runs, then make the times strictly increasing.
128
+ *
129
+ * Two rules, both about what a renderer can actually use. A run of one shape
130
+ * ("mmm") is one mouth position, so only a CHANGE earns a mark, timed at the
131
+ * first character of the run; a mark per character would multiply the payload
132
+ * for an identical rendering. And when a long sentence is spread over a very
133
+ * short buffer the step falls under half a millisecond and neighbouring marks
134
+ * round onto the same one: the later shape wins, because a shape held for zero
135
+ * milliseconds is not renderable, and because {@link speechMarks} sorts by
136
+ * (tMs, visemeId) and would otherwise pick a different winner than the one the
137
+ * walk ended on.
138
+ */
139
+ function timeline(marks) {
140
+ const kept = [];
141
+ let previous = NO_VISEME;
142
+ for (const mark of marks) {
143
+ if (mark.visemeId === previous)
144
+ continue;
145
+ previous = mark.visemeId;
146
+ const last = kept[kept.length - 1];
147
+ if (last !== undefined && mark.tMs <= last.tMs) {
148
+ kept[kept.length - 1] = { tMs: last.tMs, visemeId: mark.visemeId };
149
+ continue;
150
+ }
151
+ kept.push(mark);
152
+ }
153
+ return kept;
154
+ }
155
+ /**
156
+ * Spread `text` over `durationMs` as a viseme timeline.
157
+ *
158
+ * Pass the duration of the audio you actually sent for the turn, which
159
+ * {@link TurnLipSync} counts for you. Anything else is a guess, and a guessed
160
+ * timeline is worse than no timeline at all.
161
+ *
162
+ * `text` is lowercased, its whitespace runs collapsed, and trimmed. Anything
163
+ * that is not a positive finite duration returns no marks rather than being
164
+ * divided by, because an infinite or not-a-number timestamp desynchronises the
165
+ * mouth for the rest of the utterance and reaches the wire as a null.
166
+ *
167
+ * Returns `{tMs, visemeId}` marks, strictly increasing in time, ready for
168
+ * {@link speechMarks}. Empty when there is nothing to say: no text, no duration,
169
+ * or nothing in the text that has a mouth shape, which is the right answer for
170
+ * "3.5%" or an emoji on its own.
171
+ *
172
+ * ```ts
173
+ * const marks = estimateVisemes(finalTranscript, lipsync.durationMs);
174
+ * if (marks.length > 0) await session.sendSpeechMarks(marks);
175
+ * ```
176
+ */
177
+ export function estimateVisemes(text, durationMs) {
178
+ const normalized = (text ?? "").toLowerCase().replace(/\s+/g, " ").trim();
179
+ if (normalized === "" || !Number.isFinite(durationMs) || durationMs <= 0)
180
+ return [];
181
+ // for..of walks whole characters, so an astral one (an emoji, a rare sign) is
182
+ // skipped once instead of being read as two broken halves.
183
+ const tokens = [];
184
+ for (const ch of normalized) {
185
+ const token = tokenFor(ch);
186
+ if (token !== undefined)
187
+ tokens.push(token);
188
+ }
189
+ if (!tokens.some((token) => token !== SILENCE_VISEME))
190
+ return [];
191
+ const step = durationMs / tokens.length;
192
+ return timeline(tokens.map((visemeId, i) => ({ tMs: Math.round(i * step), visemeId })));
193
+ }
194
+ /**
195
+ * Build the timeline from per-character timings the speech provider gave you.
196
+ *
197
+ * Real times are strictly better than an estimate and cost nothing when the
198
+ * provider already returns them, so prefer this whenever a synthesis call can
199
+ * hand back an alignment. Core takes the two plain arrays: `characters` as the
200
+ * provider spoke them, and `startTimesSeconds` counting from the start of the
201
+ * utterance. Normalising a vendor's field names is the speech plugin's job.
202
+ *
203
+ * Returns marks, or an empty array when the alignment holds no mouth shape at
204
+ * all (all spaces, all punctuation). Fall back to {@link estimateVisemes} on an
205
+ * EMPTY result rather than on a missing alignment: a provider that returns
206
+ * timings for punctuation only has an alignment and still needs the estimate.
207
+ *
208
+ * Ragged arrays are tolerated: the walk stops at the shorter of the two.
209
+ * Providers do return mismatched lengths, and throwing there would lose the turn
210
+ * over a cosmetic hint.
211
+ */
212
+ export function visemesFromAlignment(characters, startTimesSeconds) {
213
+ const count = Math.min(characters.length, startTimesSeconds.length);
214
+ const marks = [];
215
+ let spoke = false;
216
+ for (let i = 0; i < count; i += 1) {
217
+ const seconds = startTimesSeconds[i];
218
+ // A time that is not a finite number is provider sloppiness of the same
219
+ // class as a ragged array, so it costs its own mark and nothing else.
220
+ if (!Number.isFinite(seconds))
221
+ continue;
222
+ const token = tokenFor(characters[i]);
223
+ if (token === undefined)
224
+ continue;
225
+ spoke = spoke || token !== SILENCE_VISEME;
226
+ marks.push({
227
+ tMs: Math.max(0, Math.round(seconds * 1000)),
228
+ visemeId: token,
229
+ });
230
+ }
231
+ return spoke ? timeline(marks) : [];
232
+ }
233
+ /**
234
+ * Counts the audio one turn actually sent, then times the mouth to it.
235
+ *
236
+ * Feed it every buffer you hand to the call, and ask it for the timeline when
237
+ * that turn's text is final. It resets itself, so the next turn starts from
238
+ * zero:
239
+ *
240
+ * ```ts
241
+ * const lipsync = new TurnLipSync();
242
+ *
243
+ * // the audio sink
244
+ * await session.sendAudio(chunk);
245
+ * lipsync.audioSent(chunk);
246
+ *
247
+ * // the final transcript only
248
+ * if (isFinal) {
249
+ * const marks = lipsync.finish(text);
250
+ * if (marks.length > 0) await session.sendSpeechMarks(marks);
251
+ * }
252
+ *
253
+ * // playback cancelled
254
+ * lipsync.cancel();
255
+ * ```
256
+ *
257
+ * Emit once per turn, on the final transcript. A partial would send an
258
+ * ever-lengthening timeline several times over and the avatar would restart the
259
+ * mouth mid-sentence.
260
+ *
261
+ * {@link cancel} is not optional. On a barge-in the service drops audio the
262
+ * caller never heard, and a counter that keeps those milliseconds spreads the
263
+ * next turn's text over its own audio plus the discarded audio: the mouth runs
264
+ * long for the whole of that turn and every turn after it.
265
+ */
266
+ export class TurnLipSync {
267
+ #sampleRateHz;
268
+ #durationMs = 0;
269
+ constructor(options = {}) {
270
+ this.#sampleRateHz = options.sampleRateHz ?? SAMPLE_RATE_HZ;
271
+ }
272
+ /** Milliseconds of audio sent for the turn in progress. Starts at 0. */
273
+ get durationMs() {
274
+ return this.#durationMs;
275
+ }
276
+ /** Add one PCM16 mono buffer that has gone out to the call. */
277
+ audioSent(pcm) {
278
+ this.audioSentMs((pcm.length / BYTES_PER_SAMPLE / this.#sampleRateHz) * 1000);
279
+ }
280
+ /**
281
+ * Add a duration directly, for a sink that hands over encoded audio.
282
+ *
283
+ * Rounded per chunk rather than kept as a running float, so both SDKs
284
+ * accumulate the same integer for the same stream of chunks. A chunk that
285
+ * measures as nothing, or as no number at all, is ignored rather than taking
286
+ * the turn's count with it.
287
+ */
288
+ audioSentMs(ms) {
289
+ if (!Number.isFinite(ms) || ms <= 0)
290
+ return;
291
+ this.#durationMs += Math.round(ms);
292
+ }
293
+ /** Drop the count on a barge-in or a playback cancel, emitting nothing. */
294
+ cancel() {
295
+ this.#durationMs = 0;
296
+ }
297
+ /**
298
+ * Return the turn's timeline and reset the counter.
299
+ *
300
+ * Empty when no audio was sent or the text carries no mouth shape, and a
301
+ * caller sends nothing in that case. The reset happens either way: the next
302
+ * turn must not inherit these milliseconds.
303
+ */
304
+ finish(text) {
305
+ const marks = estimateVisemes(text, this.#durationMs);
306
+ this.#durationMs = 0;
307
+ return marks;
308
+ }
309
+ }
310
+ //# sourceMappingURL=lipsync.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lipsync.js","sourceRoot":"","sources":["../src/lipsync.ts"],"names":[],"mappings":"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"}
package/dist/log.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The SDK's log sink.
3
+ *
4
+ * Console by default, because a library that silently swallows "this worker
5
+ * takes no Microsoft Teams calls" is a library people debug for an hour. Replace it with
6
+ * `setLogger()` to route into your own framework's logger - the Python SDK does
7
+ * the same thing through the stdlib `logging` tree.
8
+ */
9
+ export interface Logger {
10
+ debug(message: string): void;
11
+ info(message: string): void;
12
+ warn(message: string): void;
13
+ error(message: string): void;
14
+ }
15
+ /** Route SDK logs into your own logger. */
16
+ export declare function setLogger(next: Logger): void;
17
+ export declare const logger: Logger;
18
+ //# sourceMappingURL=log.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log.d.ts","sourceRoot":"","sources":["../src/log.ts"],"names":[],"mappings":"AAGA;;;;;;;GAOG;AACH,MAAM,WAAW,MAAM;IACrB,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAWD,2CAA2C;AAC3C,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAE5C;AAED,eAAO,MAAM,MAAM,EAAE,MAKpB,CAAC"}
package/dist/log.js ADDED
@@ -0,0 +1,20 @@
1
+ // Copyright (c) 2026 Komaa DigiTech
2
+ // SPDX-License-Identifier: MIT
3
+ const consoleLogger = {
4
+ debug: (m) => console.debug(m),
5
+ info: (m) => console.info(m),
6
+ warn: (m) => console.warn(m),
7
+ error: (m) => console.error(m),
8
+ };
9
+ let active = consoleLogger;
10
+ /** Route SDK logs into your own logger. */
11
+ export function setLogger(next) {
12
+ active = next;
13
+ }
14
+ export const logger = {
15
+ debug: (m) => active.debug(m),
16
+ info: (m) => active.info(m),
17
+ warn: (m) => active.warn(m),
18
+ error: (m) => active.error(m),
19
+ };
20
+ //# sourceMappingURL=log.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log.js","sourceRoot":"","sources":["../src/log.ts"],"names":[],"mappings":"AAAA,oCAAoC;AACpC,+BAA+B;AAiB/B,MAAM,aAAa,GAAW;IAC5B,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;IAC9B,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IAC5B,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IAC5B,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;CAC/B,CAAC;AAEF,IAAI,MAAM,GAAW,aAAa,CAAC;AAEnC,2CAA2C;AAC3C,MAAM,UAAU,SAAS,CAAC,IAAY;IACpC,MAAM,GAAG,IAAI,CAAC;AAChB,CAAC;AAED,MAAM,CAAC,MAAM,MAAM,GAAW;IAC5B,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IAC7B,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;IAC3B,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;IAC3B,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;CAC9B,CAAC"}
@@ -0,0 +1,43 @@
1
+ import { type OutboundImage } from "./chat.js";
2
+ /** Where a local reference may be read from. Nothing until an operator says so. */
3
+ export declare const MEDIA_ROOTS_ENV = "STANDIN_MEDIA_ROOTS";
4
+ /** A reply with its markers taken out, and what they pointed at. */
5
+ export interface AgentMedia {
6
+ /** What is safe to post and to say out loud. */
7
+ readonly text: string;
8
+ /** What the markers referred to, in the order they were written. */
9
+ readonly refs: string[];
10
+ }
11
+ /**
12
+ * Take the markers out of a reply and return them separately.
13
+ *
14
+ * The text that comes back is what to post AND what to say. Both, always: the
15
+ * whole point is that nobody sees or hears the marker.
16
+ */
17
+ export declare function parseMedia(reply: string): AgentMedia;
18
+ /**
19
+ * Directories a local reference may be read from.
20
+ *
21
+ * Empty by default, which makes local references unavailable until somebody
22
+ * opts in. That is the right default for a path chosen by a model.
23
+ */
24
+ export declare function mediaRoots(roots?: readonly string[]): string[];
25
+ /** Options for {@link loadMedia}. */
26
+ export interface LoadMediaOptions {
27
+ roots?: readonly string[];
28
+ maxBytes?: number;
29
+ name?: string;
30
+ }
31
+ /**
32
+ * Turn one reference into a picture ready to send.
33
+ *
34
+ * Throws with something worth reading, because the caller is on a path where
35
+ * the alternative is a dropped answer.
36
+ *
37
+ * A URL goes through the SDK's own guard, so a reference pointed at a private
38
+ * address is refused rather than fetched. A path is read only from a named root.
39
+ * Everything else is refused by name: `file://` handed to a URL fetcher is the
40
+ * usual way around a path guard.
41
+ */
42
+ export declare function loadMedia(ref: string, options?: LoadMediaOptions): Promise<OutboundImage>;
43
+ //# sourceMappingURL=media.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media.d.ts","sourceRoot":"","sources":["../src/media.ts"],"names":[],"mappings":"AA6BA,OAAO,EAIL,KAAK,aAAa,EACnB,MAAM,WAAW,CAAC;AAInB,mFAAmF;AACnF,eAAO,MAAM,eAAe,wBAAwB,CAAC;AAkBrD,oEAAoE;AACpE,MAAM,WAAW,UAAU;IACzB,gDAAgD;IAChD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,oEAAoE;IACpE,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC;CACzB;AAgBD;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,CAepD;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,CAe9D;AAyBD,qCAAqC;AACrC,MAAM,WAAW,gBAAgB;IAC/B,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,SAAS,CAC7B,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,aAAa,CAAC,CA6DxB"}
package/dist/media.js ADDED
@@ -0,0 +1,190 @@
1
+ // Copyright (c) 2026 Komaa DigiTech
2
+ // SPDX-License-Identifier: MIT
3
+ /**
4
+ * `MEDIA:` markers, the convention an agent uses to send a picture.
5
+ *
6
+ * Some agent frameworks let a reply name a file by writing a line like
7
+ * `MEDIA:/tmp/chart.png` and expect the channel to attach it. A channel that
8
+ * does not understand the convention posts that line as prose, and a caller
9
+ * reads a temporary file path in their chat. On a call it is worse:
10
+ * text-to-speech reads the path out, character by character.
11
+ *
12
+ * So the marker is an instruction to the channel, not something anyone should
13
+ * see. {@link parseMedia} takes it out of the text and hands back what it
14
+ * referred to. {@link loadMedia} turns one reference into bytes, through the
15
+ * same guards everything else in this SDK uses.
16
+ *
17
+ * A local path is read only from a directory an operator named. Default: none.
18
+ * An agent can be talked into writing `MEDIA:/etc/passwd`, and the answer to
19
+ * that has to be a refusal rather than a file read followed by an upload into
20
+ * somebody's chat.
21
+ *
22
+ * Identical in shape to the Python SDK's `standin.media`.
23
+ */
24
+ import { realpathSync, statSync, readFileSync } from "node:fs";
25
+ import { homedir } from "node:os";
26
+ import { delimiter, resolve, sep, basename } from "node:path";
27
+ import { OUTBOUND_IMAGE_MAX_BYTES, outboundImage, sniffImageType, } from "./chat.js";
28
+ import { fetchPublicImage } from "./fetch.js";
29
+ import { logger } from "./log.js";
30
+ /** Where a local reference may be read from. Nothing until an operator says so. */
31
+ export const MEDIA_ROOTS_ENV = "STANDIN_MEDIA_ROOTS";
32
+ /**
33
+ * The marker, as the convention defines it: case-insensitive, one per line, and
34
+ * anchored to the END of the line so a path with spaces survives intact.
35
+ *
36
+ * Used only through `replace`, which resets `lastIndex`. A shared global regex
37
+ * driven with `test` or `exec` carries state between calls and skips matches.
38
+ */
39
+ const MARKER = /\bMEDIA:\s*`?([^\n]+?)`?\s*$/gim;
40
+ /** How long a reference has to look like one before its line is removed. */
41
+ const PATH_STARTS = ["http://", "https://", "/", "./", "../", "~/"];
42
+ const MEDIA_SUFFIXES = [".png", ".jpg", ".jpeg", ".gif", ".webp"];
43
+ /** A URL reference has the same budget as the picture it becomes. */
44
+ const FETCH_TIMEOUT_MS = 10_000;
45
+ /**
46
+ * Whether this is plausibly a file or a URL.
47
+ *
48
+ * A deliberate narrowing. Stripping every line that merely begins with `MEDIA:`
49
+ * eats a sentence like "MEDIA: we should talk to them" out of the answer, and
50
+ * the person who wrote it never learns why.
51
+ */
52
+ function looksLikeRef(ref) {
53
+ const lowered = ref.toLowerCase();
54
+ if (PATH_STARTS.some((start) => lowered.startsWith(start)))
55
+ return true;
56
+ if (/^[a-z]:[\\/]/.test(lowered))
57
+ return true; // a Windows drive
58
+ return MEDIA_SUFFIXES.some((suffix) => lowered.endsWith(suffix));
59
+ }
60
+ /**
61
+ * Take the markers out of a reply and return them separately.
62
+ *
63
+ * The text that comes back is what to post AND what to say. Both, always: the
64
+ * whole point is that nobody sees or hears the marker.
65
+ */
66
+ export function parseMedia(reply) {
67
+ const refs = [];
68
+ let text = (reply ?? "").replace(MARKER, (whole, captured) => {
69
+ const ref = captured.trim().replace(/^`|`$/g, "").trim();
70
+ if (ref === "" || !looksLikeRef(ref)) {
71
+ // Not a reference, so it was prose. Leave it alone.
72
+ return whole;
73
+ }
74
+ refs.push(ref);
75
+ return "";
76
+ });
77
+ // Removing a line leaves the blank line it sat on. Three or more become two,
78
+ // which is a paragraph break; two are left alone, because they already are.
79
+ text = text.replace(/\n{3,}/g, "\n\n").trim();
80
+ return { text, refs };
81
+ }
82
+ /**
83
+ * Directories a local reference may be read from.
84
+ *
85
+ * Empty by default, which makes local references unavailable until somebody
86
+ * opts in. That is the right default for a path chosen by a model.
87
+ */
88
+ export function mediaRoots(roots) {
89
+ const parts = roots ??
90
+ (process.env[MEDIA_ROOTS_ENV] ?? "")
91
+ .split(delimiter)
92
+ .filter((p) => p.trim() !== "");
93
+ const out = [];
94
+ for (const part of parts) {
95
+ try {
96
+ out.push(realpathSync(expand(part)));
97
+ }
98
+ catch {
99
+ // A root that does not exist cannot contain anything.
100
+ }
101
+ }
102
+ return out;
103
+ }
104
+ function expand(path) {
105
+ return path.startsWith("~/") ? resolve(homedir(), path.slice(2)) : path;
106
+ }
107
+ /**
108
+ * Whether a real path sits under one of these real roots.
109
+ *
110
+ * Both sides are resolved through symlinks first, and the comparison is
111
+ * separator-terminated: without that, `/tmp/rootevil` passes for `/tmp/root`.
112
+ */
113
+ function inside(path, roots) {
114
+ let real;
115
+ try {
116
+ real = realpathSync(path);
117
+ }
118
+ catch {
119
+ return false;
120
+ }
121
+ return roots.some((root) => {
122
+ const prefix = root.replace(new RegExp(`${sep}+$`), "") + sep;
123
+ return real === root || real.startsWith(prefix);
124
+ });
125
+ }
126
+ /**
127
+ * Turn one reference into a picture ready to send.
128
+ *
129
+ * Throws with something worth reading, because the caller is on a path where
130
+ * the alternative is a dropped answer.
131
+ *
132
+ * A URL goes through the SDK's own guard, so a reference pointed at a private
133
+ * address is refused rather than fetched. A path is read only from a named root.
134
+ * Everything else is refused by name: `file://` handed to a URL fetcher is the
135
+ * usual way around a path guard.
136
+ */
137
+ export async function loadMedia(ref, options = {}) {
138
+ const target = (ref ?? "").trim();
139
+ if (target === "")
140
+ throw new Error("there was nothing to send");
141
+ const maxBytes = options.maxBytes ?? OUTBOUND_IMAGE_MAX_BYTES;
142
+ const lowered = target.toLowerCase();
143
+ let data;
144
+ let declared = "";
145
+ let suggested;
146
+ if (lowered.startsWith("http://") || lowered.startsWith("https://")) {
147
+ const fetched = await fetchPublicImage(target, maxBytes, FETCH_TIMEOUT_MS);
148
+ data = fetched.bytes;
149
+ declared = fetched.mime;
150
+ suggested = options.name ?? basename(target.split("?")[0]);
151
+ }
152
+ else if (lowered.includes("://") || lowered.startsWith("data:")) {
153
+ throw new Error(`${lowered.split(":")[0]} references are not allowed here`);
154
+ }
155
+ else {
156
+ const allowed = mediaRoots(options.roots);
157
+ if (allowed.length === 0) {
158
+ throw new Error(`sending a local file is off until a directory is named in ${MEDIA_ROOTS_ENV}`);
159
+ }
160
+ const path = expand(target);
161
+ if (!inside(path, allowed)) {
162
+ throw new Error("that file is outside the directories this worker may read");
163
+ }
164
+ let size;
165
+ try {
166
+ // Checked before it is read: the point of a cap is not to load it.
167
+ size = statSync(path).size;
168
+ }
169
+ catch {
170
+ throw new Error("no such file");
171
+ }
172
+ if (size > maxBytes)
173
+ throw new Error(`that file is ${size} bytes, over the ${maxBytes} limit`);
174
+ data = readFileSync(path);
175
+ suggested = options.name ?? basename(path);
176
+ }
177
+ // Re-checked here whatever anything upstream reported. A content type from a
178
+ // response header or a file extension is a claim; the bytes are the fact.
179
+ const actual = sniffImageType(data);
180
+ if (actual === undefined)
181
+ throw new Error("that file is not a picture this can send");
182
+ if (declared !== "" &&
183
+ declared !== "application/octet-stream" &&
184
+ declared !== actual) {
185
+ throw new Error(`that was served as ${declared} but the bytes are ${actual}`);
186
+ }
187
+ logger.info(`standin: attaching ${suggested || "a picture"} from a MEDIA marker`);
188
+ return outboundImage(data, actual, suggested);
189
+ }
190
+ //# sourceMappingURL=media.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media.js","sourceRoot":"","sources":["../src/media.ts"],"names":[],"mappings":"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"}