@opengeni/sdk 0.15.0 → 0.20.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/README.md +33 -6
- package/dist/index.d.ts +442 -29
- package/dist/index.js +379 -40
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +222 -23
- package/src/index.ts +51 -0
- package/src/transcription.ts +496 -0
- package/src/types.ts +330 -23
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework- and transport-agnostic speech-to-text capability contract.
|
|
3
|
+
*
|
|
4
|
+
* Audio transport, microphone access, credentials, and provider SDKs belong to
|
|
5
|
+
* host-supplied adapters. This module deliberately contains no browser globals
|
|
6
|
+
* and no provider implementation.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type TranscriptionCredentialMode = "managed" | "byok";
|
|
10
|
+
|
|
11
|
+
export type WorkspaceTranscriptionTarget = {
|
|
12
|
+
provider: string;
|
|
13
|
+
model: string | null;
|
|
14
|
+
credentialMode: TranscriptionCredentialMode;
|
|
15
|
+
/** Workspace-scoped connection reference. This is never a secret. */
|
|
16
|
+
credentialConnectionId: string | null;
|
|
17
|
+
region: string | null;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type WorkspaceTranscriptionPolicy = {
|
|
21
|
+
enabled: boolean;
|
|
22
|
+
/** Exact admin-accepted policy identity; required whenever enabled. */
|
|
23
|
+
acceptanceId: string | null;
|
|
24
|
+
primary: WorkspaceTranscriptionTarget | null;
|
|
25
|
+
/** Explicit language preference. Required when automatic detection is not accepted. */
|
|
26
|
+
language: string | null;
|
|
27
|
+
/** Whether the accepted adapter may automatically detect the spoken language. */
|
|
28
|
+
autoDetectLanguage: boolean;
|
|
29
|
+
/** Whether the accepted adapter may identify distinct speakers. */
|
|
30
|
+
diarization: {
|
|
31
|
+
enabled: boolean;
|
|
32
|
+
maxSpeakers: number | null;
|
|
33
|
+
};
|
|
34
|
+
retention: {
|
|
35
|
+
mode: "none" | "provider-policy";
|
|
36
|
+
maxDays: number | null;
|
|
37
|
+
};
|
|
38
|
+
privacy: {
|
|
39
|
+
allowProviderLogging: boolean;
|
|
40
|
+
allowProviderTraining: boolean;
|
|
41
|
+
};
|
|
42
|
+
fallback: {
|
|
43
|
+
mode: "disabled" | "explicit";
|
|
44
|
+
targets: WorkspaceTranscriptionTarget[];
|
|
45
|
+
};
|
|
46
|
+
cost: {
|
|
47
|
+
currency: "USD";
|
|
48
|
+
maxPerHour: number | null;
|
|
49
|
+
maxPerMonth: number | null;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const DEFAULT_WORKSPACE_TRANSCRIPTION_POLICY: WorkspaceTranscriptionPolicy = {
|
|
54
|
+
enabled: false,
|
|
55
|
+
acceptanceId: null,
|
|
56
|
+
primary: null,
|
|
57
|
+
language: null,
|
|
58
|
+
autoDetectLanguage: false,
|
|
59
|
+
diarization: { enabled: false, maxSpeakers: null },
|
|
60
|
+
retention: { mode: "none", maxDays: null },
|
|
61
|
+
privacy: { allowProviderLogging: false, allowProviderTraining: false },
|
|
62
|
+
fallback: { mode: "disabled", targets: [] },
|
|
63
|
+
cost: { currency: "USD", maxPerHour: null, maxPerMonth: null },
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export type TranscriptionAdapterDescriptor = {
|
|
67
|
+
provider: string;
|
|
68
|
+
model: string | null;
|
|
69
|
+
credentialMode: TranscriptionCredentialMode;
|
|
70
|
+
region: string | null;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export type TranscriptionTargetSelection =
|
|
74
|
+
| { kind: "primary" }
|
|
75
|
+
| { kind: "fallback"; index: number };
|
|
76
|
+
|
|
77
|
+
export type TranscriptionPolicyBlockReason =
|
|
78
|
+
| "disabled"
|
|
79
|
+
| "unaccepted"
|
|
80
|
+
| "target_missing"
|
|
81
|
+
| "fallback_disabled"
|
|
82
|
+
| "fallback_unaccepted"
|
|
83
|
+
| "provider_mismatch"
|
|
84
|
+
| "model_mismatch"
|
|
85
|
+
| "credential_mode_mismatch"
|
|
86
|
+
| "region_mismatch";
|
|
87
|
+
|
|
88
|
+
export type TranscriptionAuthorization =
|
|
89
|
+
| {
|
|
90
|
+
authorized: true;
|
|
91
|
+
acceptanceId: string;
|
|
92
|
+
target: WorkspaceTranscriptionTarget;
|
|
93
|
+
selection: TranscriptionTargetSelection;
|
|
94
|
+
}
|
|
95
|
+
| { authorized: false; reason: TranscriptionPolicyBlockReason };
|
|
96
|
+
|
|
97
|
+
export type TranscriptionLifecycleStatus =
|
|
98
|
+
| "idle"
|
|
99
|
+
| "requesting-permission"
|
|
100
|
+
| "listening"
|
|
101
|
+
| "reconnecting"
|
|
102
|
+
| "cancelling"
|
|
103
|
+
| "closed"
|
|
104
|
+
| "error";
|
|
105
|
+
|
|
106
|
+
export type TranscriptionErrorCode =
|
|
107
|
+
| "permission_denied"
|
|
108
|
+
| "not_supported"
|
|
109
|
+
| "network"
|
|
110
|
+
| "provider"
|
|
111
|
+
| "policy_blocked"
|
|
112
|
+
| "timeout"
|
|
113
|
+
| "cancelled"
|
|
114
|
+
| "unknown";
|
|
115
|
+
|
|
116
|
+
export type TranscriptionTimeSpan = {
|
|
117
|
+
startMilliseconds: number;
|
|
118
|
+
endMilliseconds: number;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export type TranscriptionSpeaker = {
|
|
122
|
+
/** Provider-neutral identity stable within the local transcription session. */
|
|
123
|
+
id: string;
|
|
124
|
+
label?: string | undefined;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export type TranscriptionWord = {
|
|
128
|
+
text: string;
|
|
129
|
+
span: TranscriptionTimeSpan;
|
|
130
|
+
confidence?: number | undefined;
|
|
131
|
+
speaker?: TranscriptionSpeaker | undefined;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/** Optional result detail; adapters omit fields their provider cannot supply. */
|
|
135
|
+
export type TranscriptionResultMetadata = {
|
|
136
|
+
detectedLanguage?: string | undefined;
|
|
137
|
+
span?: TranscriptionTimeSpan | undefined;
|
|
138
|
+
confidence?: number | undefined;
|
|
139
|
+
speaker?: TranscriptionSpeaker | undefined;
|
|
140
|
+
words?: TranscriptionWord[] | undefined;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
export type TranscriptionDiagnostic = {
|
|
144
|
+
operation: "start" | "session" | "cancel" | "close";
|
|
145
|
+
code: TranscriptionErrorCode;
|
|
146
|
+
/** Diagnostic-only detail. React sanitizes and bounds this before forwarding it. */
|
|
147
|
+
detail: string;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
type TranscriptionEventBase = {
|
|
151
|
+
/** Stable across reconnects and explicitly accepted fallback attempts. */
|
|
152
|
+
localSessionId: string;
|
|
153
|
+
/** Adapter-monotonic across the entire local session, including replay. */
|
|
154
|
+
sequence: number;
|
|
155
|
+
occurredAt: string;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export type TranscriptionEvent =
|
|
159
|
+
| (TranscriptionEventBase & { type: "permission.requested" })
|
|
160
|
+
| (TranscriptionEventBase & {
|
|
161
|
+
type: "session.opened";
|
|
162
|
+
providerSessionId: string;
|
|
163
|
+
})
|
|
164
|
+
| (TranscriptionEventBase & {
|
|
165
|
+
type: "transcript.partial";
|
|
166
|
+
segmentId: string;
|
|
167
|
+
text: string;
|
|
168
|
+
metadata?: TranscriptionResultMetadata | undefined;
|
|
169
|
+
})
|
|
170
|
+
| (TranscriptionEventBase & {
|
|
171
|
+
type: "transcript.final";
|
|
172
|
+
segmentId: string;
|
|
173
|
+
text: string;
|
|
174
|
+
/** Stable provider/coordinator acceptance identity used for dedupe. */
|
|
175
|
+
providerAcceptanceId: string;
|
|
176
|
+
metadata?: TranscriptionResultMetadata | undefined;
|
|
177
|
+
})
|
|
178
|
+
| (TranscriptionEventBase & {
|
|
179
|
+
type: "usage";
|
|
180
|
+
audioMilliseconds: number;
|
|
181
|
+
costUsd: number | null;
|
|
182
|
+
})
|
|
183
|
+
| (TranscriptionEventBase & {
|
|
184
|
+
type: "session.reconnecting";
|
|
185
|
+
attempt: number;
|
|
186
|
+
reason: string;
|
|
187
|
+
})
|
|
188
|
+
| (TranscriptionEventBase & {
|
|
189
|
+
type: "session.error";
|
|
190
|
+
code: TranscriptionErrorCode;
|
|
191
|
+
recoverable: boolean;
|
|
192
|
+
})
|
|
193
|
+
| (TranscriptionEventBase & {
|
|
194
|
+
type: "session.closed";
|
|
195
|
+
reason: "completed" | "cancelled" | "error" | "replaced";
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
export type TranscriptionSessionRequest = {
|
|
199
|
+
localSessionId: string;
|
|
200
|
+
policyAcceptanceId: string;
|
|
201
|
+
selection: TranscriptionTargetSelection;
|
|
202
|
+
target: WorkspaceTranscriptionTarget;
|
|
203
|
+
language: string | null;
|
|
204
|
+
autoDetectLanguage: boolean;
|
|
205
|
+
diarization: WorkspaceTranscriptionPolicy["diarization"];
|
|
206
|
+
retention: WorkspaceTranscriptionPolicy["retention"];
|
|
207
|
+
privacy: WorkspaceTranscriptionPolicy["privacy"];
|
|
208
|
+
cost: WorkspaceTranscriptionPolicy["cost"];
|
|
209
|
+
/** A replacement/reconnect adapter must emit events above this floor. */
|
|
210
|
+
sequenceFloor: number;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
export type TranscriptionEventListener = (event: TranscriptionEvent) => void;
|
|
214
|
+
|
|
215
|
+
export type TranscriptionAdapterStartContext = {
|
|
216
|
+
/** Aborted on local cancellation, policy replacement, timeout, or unmount. */
|
|
217
|
+
signal: AbortSignal;
|
|
218
|
+
/** Non-UI observability seam; callers receive only bounded, redacted detail. */
|
|
219
|
+
reportDiagnostic: (diagnostic: TranscriptionDiagnostic) => void;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
export type TranscriptionSession = {
|
|
223
|
+
readonly localSessionId: string;
|
|
224
|
+
cancel(reason?: string): Promise<void>;
|
|
225
|
+
close(): Promise<void>;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
export type TranscriptionAdapter = {
|
|
229
|
+
readonly descriptor: TranscriptionAdapterDescriptor;
|
|
230
|
+
start(
|
|
231
|
+
request: TranscriptionSessionRequest,
|
|
232
|
+
listener: TranscriptionEventListener,
|
|
233
|
+
context: TranscriptionAdapterStartContext,
|
|
234
|
+
): Promise<TranscriptionSession>;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
/** Invalid or absent settings always resolve to the fail-closed default. */
|
|
238
|
+
export function resolveWorkspaceTranscriptionPolicy(
|
|
239
|
+
settings: unknown,
|
|
240
|
+
): WorkspaceTranscriptionPolicy {
|
|
241
|
+
if (!isRecord(settings)) return cloneDefaultPolicy();
|
|
242
|
+
const candidate = settings.transcription;
|
|
243
|
+
if (!isWorkspaceTranscriptionPolicy(candidate)) return cloneDefaultPolicy();
|
|
244
|
+
return {
|
|
245
|
+
...candidate,
|
|
246
|
+
primary: candidate.primary ? normalizeTarget(candidate.primary) : null,
|
|
247
|
+
language: candidate.language?.trim() ?? null,
|
|
248
|
+
diarization: { ...candidate.diarization },
|
|
249
|
+
retention: { ...candidate.retention },
|
|
250
|
+
privacy: { ...candidate.privacy },
|
|
251
|
+
fallback: {
|
|
252
|
+
mode: candidate.fallback.mode,
|
|
253
|
+
targets: candidate.fallback.targets.map(normalizeTarget),
|
|
254
|
+
},
|
|
255
|
+
cost: { ...candidate.cost },
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Speech authorization is intentionally independent from turn model policy.
|
|
261
|
+
* Every selected adapter must match one exact admin-accepted target.
|
|
262
|
+
*/
|
|
263
|
+
export function authorizeTranscriptionAdapter(
|
|
264
|
+
policy: WorkspaceTranscriptionPolicy,
|
|
265
|
+
descriptor: TranscriptionAdapterDescriptor,
|
|
266
|
+
selection: TranscriptionTargetSelection = { kind: "primary" },
|
|
267
|
+
): TranscriptionAuthorization {
|
|
268
|
+
if (!isWorkspaceTranscriptionPolicy(policy)) {
|
|
269
|
+
return { authorized: false, reason: "unaccepted" };
|
|
270
|
+
}
|
|
271
|
+
if (!policy.enabled) return { authorized: false, reason: "disabled" };
|
|
272
|
+
if (!policy.acceptanceId) return { authorized: false, reason: "unaccepted" };
|
|
273
|
+
let target: WorkspaceTranscriptionTarget | null | undefined;
|
|
274
|
+
if (selection.kind === "primary") {
|
|
275
|
+
target = policy.primary;
|
|
276
|
+
} else {
|
|
277
|
+
if (policy.fallback.mode !== "explicit") {
|
|
278
|
+
return { authorized: false, reason: "fallback_disabled" };
|
|
279
|
+
}
|
|
280
|
+
target = policy.fallback.targets[selection.index];
|
|
281
|
+
if (!target) return { authorized: false, reason: "fallback_unaccepted" };
|
|
282
|
+
}
|
|
283
|
+
if (!target) return { authorized: false, reason: "target_missing" };
|
|
284
|
+
const acceptedTarget = normalizeTarget(target);
|
|
285
|
+
if (acceptedTarget.provider !== descriptor.provider) {
|
|
286
|
+
return { authorized: false, reason: "provider_mismatch" };
|
|
287
|
+
}
|
|
288
|
+
if (acceptedTarget.model !== descriptor.model) {
|
|
289
|
+
return { authorized: false, reason: "model_mismatch" };
|
|
290
|
+
}
|
|
291
|
+
if (acceptedTarget.credentialMode !== descriptor.credentialMode) {
|
|
292
|
+
return { authorized: false, reason: "credential_mode_mismatch" };
|
|
293
|
+
}
|
|
294
|
+
if (acceptedTarget.region !== descriptor.region) {
|
|
295
|
+
return { authorized: false, reason: "region_mismatch" };
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
authorized: true,
|
|
299
|
+
acceptanceId: policy.acceptanceId,
|
|
300
|
+
target: acceptedTarget,
|
|
301
|
+
selection,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function createTranscriptionSessionRequest(input: {
|
|
306
|
+
policy: WorkspaceTranscriptionPolicy;
|
|
307
|
+
adapter: TranscriptionAdapter;
|
|
308
|
+
localSessionId: string;
|
|
309
|
+
selection?: TranscriptionTargetSelection | undefined;
|
|
310
|
+
sequenceFloor?: number | undefined;
|
|
311
|
+
}): TranscriptionSessionRequest | null {
|
|
312
|
+
const sequenceFloor = input.sequenceFloor ?? 0;
|
|
313
|
+
if (!Number.isSafeInteger(sequenceFloor) || sequenceFloor < 0) return null;
|
|
314
|
+
const authorization = authorizeTranscriptionAdapter(
|
|
315
|
+
input.policy,
|
|
316
|
+
input.adapter.descriptor,
|
|
317
|
+
input.selection,
|
|
318
|
+
);
|
|
319
|
+
if (!authorization.authorized) return null;
|
|
320
|
+
return {
|
|
321
|
+
localSessionId: input.localSessionId,
|
|
322
|
+
policyAcceptanceId: authorization.acceptanceId,
|
|
323
|
+
selection: authorization.selection,
|
|
324
|
+
target: { ...authorization.target },
|
|
325
|
+
language: input.policy.language?.trim() ?? null,
|
|
326
|
+
autoDetectLanguage: input.policy.autoDetectLanguage,
|
|
327
|
+
diarization: { ...input.policy.diarization },
|
|
328
|
+
retention: { ...input.policy.retention },
|
|
329
|
+
privacy: { ...input.policy.privacy },
|
|
330
|
+
cost: { ...input.policy.cost },
|
|
331
|
+
sequenceFloor,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function cloneDefaultPolicy(): WorkspaceTranscriptionPolicy {
|
|
336
|
+
return {
|
|
337
|
+
...DEFAULT_WORKSPACE_TRANSCRIPTION_POLICY,
|
|
338
|
+
diarization: { ...DEFAULT_WORKSPACE_TRANSCRIPTION_POLICY.diarization },
|
|
339
|
+
retention: { ...DEFAULT_WORKSPACE_TRANSCRIPTION_POLICY.retention },
|
|
340
|
+
privacy: { ...DEFAULT_WORKSPACE_TRANSCRIPTION_POLICY.privacy },
|
|
341
|
+
fallback: { mode: "disabled", targets: [] },
|
|
342
|
+
cost: { ...DEFAULT_WORKSPACE_TRANSCRIPTION_POLICY.cost },
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function isWorkspaceTranscriptionPolicy(value: unknown): value is WorkspaceTranscriptionPolicy {
|
|
347
|
+
if (!isRecord(value) || typeof value.enabled !== "boolean") return false;
|
|
348
|
+
if (
|
|
349
|
+
!hasOnlyKeys(value, [
|
|
350
|
+
"enabled",
|
|
351
|
+
"acceptanceId",
|
|
352
|
+
"primary",
|
|
353
|
+
"language",
|
|
354
|
+
"autoDetectLanguage",
|
|
355
|
+
"diarization",
|
|
356
|
+
"retention",
|
|
357
|
+
"privacy",
|
|
358
|
+
"fallback",
|
|
359
|
+
"cost",
|
|
360
|
+
])
|
|
361
|
+
) {
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
if (!(value.acceptanceId === null || isUuid(value.acceptanceId))) return false;
|
|
365
|
+
if (!(value.primary === null || isTarget(value.primary))) return false;
|
|
366
|
+
if (!(value.language === null || isBoundedString(value.language, 64))) return false;
|
|
367
|
+
if (typeof value.autoDetectLanguage !== "boolean") return false;
|
|
368
|
+
if (
|
|
369
|
+
!isRecord(value.diarization) ||
|
|
370
|
+
!hasOnlyKeys(value.diarization, ["enabled", "maxSpeakers"]) ||
|
|
371
|
+
typeof value.diarization.enabled !== "boolean" ||
|
|
372
|
+
!(
|
|
373
|
+
value.diarization.maxSpeakers === null ||
|
|
374
|
+
(isBoundedInteger(value.diarization.maxSpeakers, 100) && value.diarization.maxSpeakers >= 2)
|
|
375
|
+
)
|
|
376
|
+
) {
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
if (!value.diarization.enabled && value.diarization.maxSpeakers !== null) return false;
|
|
380
|
+
if (!isRecord(value.retention) || !hasOnlyKeys(value.retention, ["mode", "maxDays"])) {
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
if (value.retention.mode !== "none" && value.retention.mode !== "provider-policy") return false;
|
|
384
|
+
if (!(value.retention.maxDays === null || isBoundedInteger(value.retention.maxDays, 3650))) {
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
if (
|
|
388
|
+
!isRecord(value.privacy) ||
|
|
389
|
+
!hasOnlyKeys(value.privacy, ["allowProviderLogging", "allowProviderTraining"]) ||
|
|
390
|
+
typeof value.privacy.allowProviderLogging !== "boolean" ||
|
|
391
|
+
typeof value.privacy.allowProviderTraining !== "boolean"
|
|
392
|
+
) {
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
if (!isRecord(value.fallback) || !hasOnlyKeys(value.fallback, ["mode", "targets"])) {
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
if (value.fallback.mode !== "disabled" && value.fallback.mode !== "explicit") return false;
|
|
399
|
+
if (
|
|
400
|
+
!Array.isArray(value.fallback.targets) ||
|
|
401
|
+
value.fallback.targets.length > 8 ||
|
|
402
|
+
!value.fallback.targets.every(isTarget)
|
|
403
|
+
) {
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
if (value.fallback.mode === "disabled" && value.fallback.targets.length !== 0) return false;
|
|
407
|
+
if (value.fallback.mode === "explicit" && value.fallback.targets.length === 0) return false;
|
|
408
|
+
if (
|
|
409
|
+
!isRecord(value.cost) ||
|
|
410
|
+
!hasOnlyKeys(value.cost, ["currency", "maxPerHour", "maxPerMonth"]) ||
|
|
411
|
+
value.cost.currency !== "USD"
|
|
412
|
+
) {
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
if (!isNullableBoundedNumber(value.cost.maxPerHour, 10_000)) return false;
|
|
416
|
+
if (!isNullableBoundedNumber(value.cost.maxPerMonth, 1_000_000)) return false;
|
|
417
|
+
if (value.enabled && (!value.acceptanceId || !value.primary)) return false;
|
|
418
|
+
if (value.enabled && !value.autoDetectLanguage && value.language === null) return false;
|
|
419
|
+
if (value.autoDetectLanguage && value.language !== null) return false;
|
|
420
|
+
const targets = [value.primary, ...value.fallback.targets].filter(
|
|
421
|
+
(target): target is WorkspaceTranscriptionTarget => target !== null,
|
|
422
|
+
);
|
|
423
|
+
if (new Set(targets.map(targetKey)).size !== targets.length) return false;
|
|
424
|
+
return true;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function targetKey(target: WorkspaceTranscriptionTarget): string {
|
|
428
|
+
return [
|
|
429
|
+
target.provider.trim(),
|
|
430
|
+
target.model?.trim() ?? "",
|
|
431
|
+
target.credentialMode,
|
|
432
|
+
target.credentialConnectionId ?? "",
|
|
433
|
+
target.region?.trim() ?? "",
|
|
434
|
+
].join("\u0000");
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function isTarget(value: unknown): value is WorkspaceTranscriptionTarget {
|
|
438
|
+
if (!isRecord(value)) return false;
|
|
439
|
+
if (
|
|
440
|
+
!hasOnlyKeys(value, ["provider", "model", "credentialMode", "credentialConnectionId", "region"])
|
|
441
|
+
) {
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
if (!isBoundedString(value.provider, 128)) return false;
|
|
445
|
+
if (!(value.model === null || isBoundedString(value.model, 256))) return false;
|
|
446
|
+
if (value.credentialMode !== "managed" && value.credentialMode !== "byok") return false;
|
|
447
|
+
if (value.provider.trim() === "azure-speech" && value.credentialMode !== "byok") return false;
|
|
448
|
+
if (!(value.credentialConnectionId === null || isUuid(value.credentialConnectionId))) {
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
if (!(value.region === null || isBoundedString(value.region, 128))) return false;
|
|
452
|
+
if (value.credentialMode === "byok" && value.credentialConnectionId === null) return false;
|
|
453
|
+
if (value.credentialMode === "managed" && value.credentialConnectionId !== null) return false;
|
|
454
|
+
return true;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function normalizeTarget(target: WorkspaceTranscriptionTarget): WorkspaceTranscriptionTarget {
|
|
458
|
+
return {
|
|
459
|
+
provider: target.provider.trim(),
|
|
460
|
+
model: target.model?.trim() ?? null,
|
|
461
|
+
credentialMode: target.credentialMode,
|
|
462
|
+
credentialConnectionId: target.credentialConnectionId,
|
|
463
|
+
region: target.region?.trim() ?? null,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
468
|
+
return typeof value === "object" && value !== null;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function hasOnlyKeys(value: Record<string, unknown>, keys: readonly string[]): boolean {
|
|
472
|
+
const accepted = new Set(keys);
|
|
473
|
+
return Object.keys(value).every((key) => accepted.has(key));
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function isBoundedString(value: unknown, maximum: number): value is string {
|
|
477
|
+
return typeof value === "string" && value.trim().length > 0 && value.length <= maximum;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function isUuid(value: unknown): value is string {
|
|
481
|
+
return (
|
|
482
|
+
typeof value === "string" &&
|
|
483
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value)
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function isBoundedInteger(value: unknown, maximum: number): value is number {
|
|
488
|
+
return Number.isInteger(value) && (value as number) >= 0 && (value as number) <= maximum;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function isNullableBoundedNumber(value: unknown, maximum: number): boolean {
|
|
492
|
+
return (
|
|
493
|
+
value === null ||
|
|
494
|
+
(typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= maximum)
|
|
495
|
+
);
|
|
496
|
+
}
|