@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.
- package/LICENSE +21 -0
- package/README.md +141 -0
- package/dist/ambient.d.ts +80 -0
- package/dist/ambient.d.ts.map +1 -0
- package/dist/ambient.js +281 -0
- package/dist/ambient.js.map +1 -0
- package/dist/attachments.d.ts +149 -0
- package/dist/attachments.d.ts.map +1 -0
- package/dist/attachments.js +451 -0
- package/dist/attachments.js.map +1 -0
- package/dist/audio.d.ts +80 -0
- package/dist/audio.d.ts.map +1 -0
- package/dist/audio.js +170 -0
- package/dist/audio.js.map +1 -0
- package/dist/avatar.d.ts +111 -0
- package/dist/avatar.d.ts.map +1 -0
- package/dist/avatar.js +201 -0
- package/dist/avatar.js.map +1 -0
- package/dist/callServer.d.ts +151 -0
- package/dist/callServer.d.ts.map +1 -0
- package/dist/callServer.js +1097 -0
- package/dist/callServer.js.map +1 -0
- package/dist/callTools.d.ts +154 -0
- package/dist/callTools.d.ts.map +1 -0
- package/dist/callTools.js +255 -0
- package/dist/callTools.js.map +1 -0
- package/dist/chat.d.ts +226 -0
- package/dist/chat.d.ts.map +1 -0
- package/dist/chat.js +574 -0
- package/dist/chat.js.map +1 -0
- package/dist/config.d.ts +32 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +86 -0
- package/dist/config.js.map +1 -0
- package/dist/consult.d.ts +147 -0
- package/dist/consult.d.ts.map +1 -0
- package/dist/consult.js +388 -0
- package/dist/consult.js.map +1 -0
- package/dist/delivery.d.ts +81 -0
- package/dist/delivery.d.ts.map +1 -0
- package/dist/delivery.js +187 -0
- package/dist/delivery.js.map +1 -0
- package/dist/echoGuard.d.ts +56 -0
- package/dist/echoGuard.d.ts.map +1 -0
- package/dist/echoGuard.js +57 -0
- package/dist/echoGuard.js.map +1 -0
- package/dist/errors.d.ts +5 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +10 -0
- package/dist/errors.js.map +1 -0
- package/dist/fetch.d.ts +40 -0
- package/dist/fetch.d.ts.map +1 -0
- package/dist/fetch.js +240 -0
- package/dist/fetch.js.map +1 -0
- package/dist/gate.d.ts +80 -0
- package/dist/gate.d.ts.map +1 -0
- package/dist/gate.js +249 -0
- package/dist/gate.js.map +1 -0
- package/dist/handler.d.ts +283 -0
- package/dist/handler.d.ts.map +1 -0
- package/dist/handler.js +4 -0
- package/dist/handler.js.map +1 -0
- package/dist/hmac.d.ts +32 -0
- package/dist/hmac.d.ts.map +1 -0
- package/dist/hmac.js +108 -0
- package/dist/hmac.js.map +1 -0
- package/dist/index.d.ts +59 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +60 -0
- package/dist/index.js.map +1 -0
- package/dist/lane.d.ts +92 -0
- package/dist/lane.d.ts.map +1 -0
- package/dist/lane.js +269 -0
- package/dist/lane.js.map +1 -0
- package/dist/lipsync.d.ts +136 -0
- package/dist/lipsync.d.ts.map +1 -0
- package/dist/lipsync.js +310 -0
- package/dist/lipsync.js.map +1 -0
- package/dist/log.d.ts +18 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +20 -0
- package/dist/log.js.map +1 -0
- package/dist/media.d.ts +43 -0
- package/dist/media.d.ts.map +1 -0
- package/dist/media.js +190 -0
- package/dist/media.js.map +1 -0
- package/dist/minutes.d.ts +398 -0
- package/dist/minutes.d.ts.map +1 -0
- package/dist/minutes.js +805 -0
- package/dist/minutes.js.map +1 -0
- package/dist/outbound.d.ts +388 -0
- package/dist/outbound.d.ts.map +1 -0
- package/dist/outbound.js +933 -0
- package/dist/outbound.js.map +1 -0
- package/dist/plugins/cartesia/agent.d.ts +58 -0
- package/dist/plugins/cartesia/agent.d.ts.map +1 -0
- package/dist/plugins/cartesia/agent.js +254 -0
- package/dist/plugins/cartesia/agent.js.map +1 -0
- package/dist/plugins/cartesia/cli.d.ts +3 -0
- package/dist/plugins/cartesia/cli.d.ts.map +1 -0
- package/dist/plugins/cartesia/cli.js +10 -0
- package/dist/plugins/cartesia/cli.js.map +1 -0
- package/dist/plugins/cartesia/config.d.ts +30 -0
- package/dist/plugins/cartesia/config.d.ts.map +1 -0
- package/dist/plugins/cartesia/config.js +29 -0
- package/dist/plugins/cartesia/config.js.map +1 -0
- package/dist/plugins/cartesia/handler.d.ts +40 -0
- package/dist/plugins/cartesia/handler.d.ts.map +1 -0
- package/dist/plugins/cartesia/handler.js +142 -0
- package/dist/plugins/cartesia/handler.js.map +1 -0
- package/dist/plugins/cartesia/index.d.ts +6 -0
- package/dist/plugins/cartesia/index.d.ts.map +1 -0
- package/dist/plugins/cartesia/index.js +54 -0
- package/dist/plugins/cartesia/index.js.map +1 -0
- package/dist/plugins/deepgram/agent.d.ts +60 -0
- package/dist/plugins/deepgram/agent.d.ts.map +1 -0
- package/dist/plugins/deepgram/agent.js +302 -0
- package/dist/plugins/deepgram/agent.js.map +1 -0
- package/dist/plugins/deepgram/cli.d.ts +3 -0
- package/dist/plugins/deepgram/cli.d.ts.map +1 -0
- package/dist/plugins/deepgram/cli.js +10 -0
- package/dist/plugins/deepgram/cli.js.map +1 -0
- package/dist/plugins/deepgram/config.d.ts +39 -0
- package/dist/plugins/deepgram/config.d.ts.map +1 -0
- package/dist/plugins/deepgram/config.js +66 -0
- package/dist/plugins/deepgram/config.js.map +1 -0
- package/dist/plugins/deepgram/handler.d.ts +87 -0
- package/dist/plugins/deepgram/handler.d.ts.map +1 -0
- package/dist/plugins/deepgram/handler.js +268 -0
- package/dist/plugins/deepgram/handler.js.map +1 -0
- package/dist/plugins/deepgram/index.d.ts +11 -0
- package/dist/plugins/deepgram/index.d.ts.map +1 -0
- package/dist/plugins/deepgram/index.js +76 -0
- package/dist/plugins/deepgram/index.js.map +1 -0
- package/dist/plugins/echo/cli.d.ts +3 -0
- package/dist/plugins/echo/cli.d.ts.map +1 -0
- package/dist/plugins/echo/cli.js +10 -0
- package/dist/plugins/echo/cli.js.map +1 -0
- package/dist/plugins/echo/index.d.ts +37 -0
- package/dist/plugins/echo/index.d.ts.map +1 -0
- package/dist/plugins/echo/index.js +61 -0
- package/dist/plugins/echo/index.js.map +1 -0
- package/dist/plugins/elevenlabs/agent.d.ts +72 -0
- package/dist/plugins/elevenlabs/agent.d.ts.map +1 -0
- package/dist/plugins/elevenlabs/agent.js +305 -0
- package/dist/plugins/elevenlabs/agent.js.map +1 -0
- package/dist/plugins/elevenlabs/cli.d.ts +3 -0
- package/dist/plugins/elevenlabs/cli.d.ts.map +1 -0
- package/dist/plugins/elevenlabs/cli.js +10 -0
- package/dist/plugins/elevenlabs/cli.js.map +1 -0
- package/dist/plugins/elevenlabs/config.d.ts +27 -0
- package/dist/plugins/elevenlabs/config.d.ts.map +1 -0
- package/dist/plugins/elevenlabs/config.js +42 -0
- package/dist/plugins/elevenlabs/config.js.map +1 -0
- package/dist/plugins/elevenlabs/handler.d.ts +67 -0
- package/dist/plugins/elevenlabs/handler.d.ts.map +1 -0
- package/dist/plugins/elevenlabs/handler.js +419 -0
- package/dist/plugins/elevenlabs/handler.js.map +1 -0
- package/dist/plugins/elevenlabs/index.d.ts +11 -0
- package/dist/plugins/elevenlabs/index.d.ts.map +1 -0
- package/dist/plugins/elevenlabs/index.js +72 -0
- package/dist/plugins/elevenlabs/index.js.map +1 -0
- package/dist/plugins/livekit/cli.d.ts +3 -0
- package/dist/plugins/livekit/cli.d.ts.map +1 -0
- package/dist/plugins/livekit/cli.js +10 -0
- package/dist/plugins/livekit/cli.js.map +1 -0
- package/dist/plugins/livekit/config.d.ts +47 -0
- package/dist/plugins/livekit/config.d.ts.map +1 -0
- package/dist/plugins/livekit/config.js +46 -0
- package/dist/plugins/livekit/config.js.map +1 -0
- package/dist/plugins/livekit/handler.d.ts +59 -0
- package/dist/plugins/livekit/handler.d.ts.map +1 -0
- package/dist/plugins/livekit/handler.js +211 -0
- package/dist/plugins/livekit/handler.js.map +1 -0
- package/dist/plugins/livekit/index.d.ts +6 -0
- package/dist/plugins/livekit/index.d.ts.map +1 -0
- package/dist/plugins/livekit/index.js +68 -0
- package/dist/plugins/livekit/index.js.map +1 -0
- package/dist/plugins/livekit/room.d.ts +117 -0
- package/dist/plugins/livekit/room.d.ts.map +1 -0
- package/dist/plugins/livekit/room.js +449 -0
- package/dist/plugins/livekit/room.js.map +1 -0
- package/dist/plugins/openai/agent.d.ts +74 -0
- package/dist/plugins/openai/agent.d.ts.map +1 -0
- package/dist/plugins/openai/agent.js +264 -0
- package/dist/plugins/openai/agent.js.map +1 -0
- package/dist/plugins/openai/cli.d.ts +3 -0
- package/dist/plugins/openai/cli.d.ts.map +1 -0
- package/dist/plugins/openai/cli.js +10 -0
- package/dist/plugins/openai/cli.js.map +1 -0
- package/dist/plugins/openai/config.d.ts +29 -0
- package/dist/plugins/openai/config.d.ts.map +1 -0
- package/dist/plugins/openai/config.js +37 -0
- package/dist/plugins/openai/config.js.map +1 -0
- package/dist/plugins/openai/handler.d.ts +65 -0
- package/dist/plugins/openai/handler.d.ts.map +1 -0
- package/dist/plugins/openai/handler.js +297 -0
- package/dist/plugins/openai/handler.js.map +1 -0
- package/dist/plugins/openai/index.d.ts +6 -0
- package/dist/plugins/openai/index.d.ts.map +1 -0
- package/dist/plugins/openai/index.js +72 -0
- package/dist/plugins/openai/index.js.map +1 -0
- package/dist/plugins/openclaw/allowlist.d.ts +9 -0
- package/dist/plugins/openclaw/allowlist.d.ts.map +1 -0
- package/dist/plugins/openclaw/allowlist.js +11 -0
- package/dist/plugins/openclaw/allowlist.js.map +1 -0
- package/dist/plugins/openclaw/config.d.ts +50 -0
- package/dist/plugins/openclaw/config.d.ts.map +1 -0
- package/dist/plugins/openclaw/config.js +53 -0
- package/dist/plugins/openclaw/config.js.map +1 -0
- package/dist/plugins/openclaw/handler.d.ts +90 -0
- package/dist/plugins/openclaw/handler.d.ts.map +1 -0
- package/dist/plugins/openclaw/handler.js +146 -0
- package/dist/plugins/openclaw/handler.js.map +1 -0
- package/dist/plugins/openclaw/index.d.ts +55 -0
- package/dist/plugins/openclaw/index.d.ts.map +1 -0
- package/dist/plugins/openclaw/index.js +89 -0
- package/dist/plugins/openclaw/index.js.map +1 -0
- package/dist/plugins/openclaw/openclaw.plugin.json +74 -0
- package/dist/plugins/openclaw/package.json +14 -0
- package/dist/plugins/openclaw/realtime.d.ts +82 -0
- package/dist/plugins/openclaw/realtime.d.ts.map +1 -0
- package/dist/plugins/openclaw/realtime.js +238 -0
- package/dist/plugins/openclaw/realtime.js.map +1 -0
- package/dist/plugins/openclaw/runtime.d.ts +19 -0
- package/dist/plugins/openclaw/runtime.d.ts.map +1 -0
- package/dist/plugins/openclaw/runtime.js +160 -0
- package/dist/plugins/openclaw/runtime.js.map +1 -0
- package/dist/policy.d.ts +38 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +95 -0
- package/dist/policy.js.map +1 -0
- package/dist/protocol.d.ts +44 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +77 -0
- package/dist/protocol.js.map +1 -0
- package/dist/protocolRuntime.d.ts +19 -0
- package/dist/protocolRuntime.d.ts.map +1 -0
- package/dist/protocolRuntime.js +78 -0
- package/dist/protocolRuntime.js.map +1 -0
- package/dist/smoke.d.ts +65 -0
- package/dist/smoke.d.ts.map +1 -0
- package/dist/smoke.js +293 -0
- package/dist/smoke.js.map +1 -0
- package/dist/startup.d.ts +74 -0
- package/dist/startup.d.ts.map +1 -0
- package/dist/startup.js +112 -0
- package/dist/startup.js.map +1 -0
- package/dist/tile.d.ts +101 -0
- package/dist/tile.d.ts.map +1 -0
- package/dist/tile.js +195 -0
- package/dist/tile.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +4 -0
- package/dist/version.js.map +1 -0
- package/dist/vision.d.ts +160 -0
- package/dist/vision.d.ts.map +1 -0
- package/dist/vision.js +301 -0
- package/dist/vision.js.map +1 -0
- package/dist/visionTools.d.ts +237 -0
- package/dist/visionTools.d.ts.map +1 -0
- package/dist/visionTools.js +578 -0
- package/dist/visionTools.js.map +1 -0
- package/dist/voice.d.ts +143 -0
- package/dist/voice.d.ts.map +1 -0
- package/dist/voice.js +403 -0
- package/dist/voice.js.map +1 -0
- package/package.json +129 -0
package/dist/lipsync.js
ADDED
|
@@ -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
|
package/dist/log.js.map
ADDED
|
@@ -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"}
|
package/dist/media.d.ts
ADDED
|
@@ -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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"}
|