synthiaresearch 0.0.2 → 0.0.3
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/dist/client.d.ts +88 -4
- package/dist/client.js +246 -8
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export declare const DEFAULT_BASE_URL = "https://synthia-research--synthia-api-web.modal.run";
|
|
2
|
+
/** Audio input for overloaded surfaces: raw bytes or a path to an audio file. */
|
|
3
|
+
export type AudioInput = Uint8Array | string;
|
|
2
4
|
/** One traced tool invocation made by the agent while answering a probe. */
|
|
3
5
|
export interface ToolCall {
|
|
4
6
|
name: string;
|
|
@@ -44,8 +46,11 @@ export declare class ToolSandbox {
|
|
|
44
46
|
export interface TranscriptTurn {
|
|
45
47
|
role: string;
|
|
46
48
|
content: string;
|
|
49
|
+
/** Set on voiced turns (voice-enabled accounts); fetch via
|
|
50
|
+
* Rollouts.turnAudio. */
|
|
51
|
+
audio_url?: string | null;
|
|
47
52
|
}
|
|
48
|
-
export type RolloutAgent = (transcript: TranscriptTurn[], sandbox: ToolSandbox) => string | Promise<string>;
|
|
53
|
+
export type RolloutAgent = (transcript: TranscriptTurn[], sandbox: ToolSandbox) => string | AudioInput | Promise<string | AudioInput>;
|
|
49
54
|
declare class Http {
|
|
50
55
|
private baseUrl;
|
|
51
56
|
headers: Record<string, string>;
|
|
@@ -55,6 +60,8 @@ declare class Http {
|
|
|
55
60
|
constructor(baseUrl: string, headers: Record<string, string>);
|
|
56
61
|
get(path: string, params?: Record<string, string | number>): Promise<any>;
|
|
57
62
|
post(path: string, body?: unknown): Promise<any>;
|
|
63
|
+
/** Binary GET (audio artifacts). */
|
|
64
|
+
getBytes(path: string): Promise<Uint8Array>;
|
|
58
65
|
/** Un-gated request returning the raw Response (used by the handshake). */
|
|
59
66
|
raw(method: string, path: string, body?: unknown, params?: Record<string, string | number>): Promise<Response>;
|
|
60
67
|
}
|
|
@@ -119,11 +126,18 @@ export declare class GenerationJob {
|
|
|
119
126
|
export declare class Seeds {
|
|
120
127
|
#private;
|
|
121
128
|
constructor(http: Http);
|
|
122
|
-
/**
|
|
129
|
+
/**
|
|
130
|
+
* Upload seed material (documents, tool schemas, policies, traces...).
|
|
131
|
+
*
|
|
132
|
+
* Overloaded on `content`: an object is ingested as-is (text pipeline);
|
|
133
|
+
* raw bytes or a path to an audio file (.wav/.mp3/...) is uploaded and
|
|
134
|
+
* transcribed server-side (voice-enabled accounts only) — the transcript
|
|
135
|
+
* becomes the seed content.
|
|
136
|
+
*/
|
|
123
137
|
ingest(opts: {
|
|
124
138
|
kind: string;
|
|
125
139
|
source: string;
|
|
126
|
-
content: Record<string, unknown
|
|
140
|
+
content: Record<string, unknown> | AudioInput;
|
|
127
141
|
version?: string;
|
|
128
142
|
metadata?: Record<string, unknown>;
|
|
129
143
|
}): Promise<any>;
|
|
@@ -171,6 +185,9 @@ export interface PrepareResult {
|
|
|
171
185
|
reason: string;
|
|
172
186
|
successRate: number | null;
|
|
173
187
|
qualityCheckId: string | null;
|
|
188
|
+
/** Running render handles when prepare({voice: true}) pre-voiced the
|
|
189
|
+
* scenarios. */
|
|
190
|
+
voiceRenders?: VoiceRender[];
|
|
174
191
|
}
|
|
175
192
|
/**
|
|
176
193
|
* An async evaluation of finished rollouts: per rollout, the server
|
|
@@ -192,9 +209,45 @@ export declare class QualityCheck {
|
|
|
192
209
|
*/
|
|
193
210
|
rollouts(): Promise<any[]>;
|
|
194
211
|
}
|
|
212
|
+
/**
|
|
213
|
+
* An async voice render: a scenario (LLM-authored script) or a rollout
|
|
214
|
+
* transcript, voiced with ElevenLabs — N takes spliced into one mixed WAV
|
|
215
|
+
* with per-turn provenance. Requires a voice-enabled customer config.
|
|
216
|
+
*/
|
|
217
|
+
export declare class VoiceRender {
|
|
218
|
+
#private;
|
|
219
|
+
id: string;
|
|
220
|
+
status: string;
|
|
221
|
+
scenario_id: string | null;
|
|
222
|
+
rollout_id: string | null;
|
|
223
|
+
params: Record<string, unknown>;
|
|
224
|
+
duration_ms: number | null;
|
|
225
|
+
wpm: number | null;
|
|
226
|
+
provenance: any[] | null;
|
|
227
|
+
error: string | null;
|
|
228
|
+
constructor(data: any, http: Http);
|
|
229
|
+
/** Poll until the render finishes; verbose prints server telemetry
|
|
230
|
+
* (per-TTS-call latencies, take/mix progress). */
|
|
231
|
+
wait(opts?: WaitOptions): Promise<VoiceRender>;
|
|
232
|
+
/** The mixed conversation WAV. */
|
|
233
|
+
audio(): Promise<Uint8Array>;
|
|
234
|
+
/** Write the mixed conversation WAV to `path`; returns it. */
|
|
235
|
+
saveAudio(path: string): Promise<string>;
|
|
236
|
+
}
|
|
237
|
+
export interface VoiceOptions {
|
|
238
|
+
takes?: number;
|
|
239
|
+
engine?: "v2" | "v3";
|
|
240
|
+
annotate?: boolean;
|
|
241
|
+
phoneFx?: boolean;
|
|
242
|
+
roomTone?: boolean;
|
|
243
|
+
wpmRange?: [number, number];
|
|
244
|
+
voiceOverrides?: Record<string, string>;
|
|
245
|
+
}
|
|
195
246
|
/**
|
|
196
247
|
* One finished rollout: the conversation a scenario produced, plus every
|
|
197
248
|
* tool call the agent made along the way (each tagged with its turn_idx).
|
|
249
|
+
* voice_render is attached (already running) when the account's config has
|
|
250
|
+
* voice.auto — await .wait()/.saveAudio() on it if you want the WAV.
|
|
198
251
|
*/
|
|
199
252
|
export interface RolloutResult {
|
|
200
253
|
rollout_id: string;
|
|
@@ -203,6 +256,7 @@ export interface RolloutResult {
|
|
|
203
256
|
turns: number;
|
|
204
257
|
transcript: TranscriptTurn[];
|
|
205
258
|
tool_events: any[];
|
|
259
|
+
voice_render?: VoiceRender | null;
|
|
206
260
|
}
|
|
207
261
|
export interface RunOptions {
|
|
208
262
|
maxTurns?: number;
|
|
@@ -210,12 +264,21 @@ export interface RunOptions {
|
|
|
210
264
|
}
|
|
211
265
|
export declare class Rollouts {
|
|
212
266
|
#private;
|
|
213
|
-
constructor(http: Http, sessionId?: string | null);
|
|
267
|
+
constructor(http: Http, sessionId?: string | null, voiceAuto?: boolean);
|
|
214
268
|
/**
|
|
215
269
|
* A stored rollout's full captured state: status, seed, transcript,
|
|
216
270
|
* tool events, and sandbox.
|
|
217
271
|
*/
|
|
218
272
|
get(rolloutId: string): Promise<any>;
|
|
273
|
+
/**
|
|
274
|
+
* Voice a finished rollout: the transcript maps to a script
|
|
275
|
+
* deterministically (words verbatim; `annotate` may add delivery tags
|
|
276
|
+
* only), then N takes are rendered and spliced into one mixed WAV.
|
|
277
|
+
* Requires a voice-enabled customer config (throws otherwise).
|
|
278
|
+
*/
|
|
279
|
+
voice(rollout: RolloutResult | string, opts?: VoiceOptions): Promise<VoiceRender>;
|
|
280
|
+
/** One voiced turn's WAV (turns with an audio_url only). */
|
|
281
|
+
turnAudio(rolloutId: string, idx: number): Promise<Uint8Array>;
|
|
219
282
|
/**
|
|
220
283
|
* Play a dataset's scenarios against `agent` (most recent dataset when
|
|
221
284
|
* none is given).
|
|
@@ -250,6 +313,12 @@ export interface PrepareOptions {
|
|
|
250
313
|
minSuccessRate?: number;
|
|
251
314
|
maxSuccessRate?: number;
|
|
252
315
|
verbose?: boolean;
|
|
316
|
+
/** Additionally voice every scenario in the prepared dataset (an LLM
|
|
317
|
+
* authors each script, then a takes=1 render) — explicit opt-in because
|
|
318
|
+
* it spends per row; handles come back still-running on
|
|
319
|
+
* PrepareResult.voiceRenders. voice.auto accounts don't need this:
|
|
320
|
+
* their rollouts voice themselves. */
|
|
321
|
+
voice?: boolean;
|
|
253
322
|
}
|
|
254
323
|
/**
|
|
255
324
|
* Client entry point.
|
|
@@ -270,11 +339,26 @@ export declare class Synthia {
|
|
|
270
339
|
sessionName: string;
|
|
271
340
|
sessionId: string | null;
|
|
272
341
|
invocationId: string | null;
|
|
342
|
+
/** Voice mode, mirrored from the account's customer config by the
|
|
343
|
+
* session handshake: enabled unlocks the voice surfaces; auto makes
|
|
344
|
+
* rollouts voice themselves. Zero client-side flags — flipping a
|
|
345
|
+
* customer's config flips their SDK. */
|
|
346
|
+
voiceEnabled: boolean;
|
|
347
|
+
voiceAuto: boolean;
|
|
273
348
|
seeds: Seeds;
|
|
274
349
|
userModels: UserModels;
|
|
275
350
|
datasets: Datasets;
|
|
276
351
|
rollouts: Rollouts;
|
|
277
352
|
constructor(options?: SynthiaOptions);
|
|
353
|
+
/**
|
|
354
|
+
* Voice one scenario (an LLM authors the full two-sided script) or one
|
|
355
|
+
* finished rollout (deterministic transcript transform). Exactly one
|
|
356
|
+
* source id. Requires a voice-enabled customer config.
|
|
357
|
+
*/
|
|
358
|
+
voiceRender(opts: VoiceOptions & {
|
|
359
|
+
scenarioId?: string;
|
|
360
|
+
rolloutId?: string;
|
|
361
|
+
}): Promise<VoiceRender>;
|
|
278
362
|
/**
|
|
279
363
|
* Probe + generate only when needed; otherwise reuse the latest dataset.
|
|
280
364
|
*
|
package/dist/client.js
CHANGED
|
@@ -1,7 +1,41 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
3
|
import { basename, dirname, extname, resolve } from "node:path";
|
|
3
4
|
export const DEFAULT_BASE_URL = "https://synthia-research--synthia-api-web.modal.run";
|
|
4
|
-
const SDK_VERSION = "0.0.
|
|
5
|
+
const SDK_VERSION = "0.0.3"; // keep in lockstep with package.json
|
|
6
|
+
// File suffixes treated as audio where inputs are overloaded (seeds.ingest
|
|
7
|
+
// content, rollout agent replies).
|
|
8
|
+
const AUDIO_SUFFIXES = new Set([".wav", ".mp3", ".m4a", ".ogg", ".flac", ".webm"]);
|
|
9
|
+
/**
|
|
10
|
+
* base64 audio when `value` is audio — raw bytes, or a string path ending in
|
|
11
|
+
* an audio suffix — else null. The runtime half of the input overloads.
|
|
12
|
+
*/
|
|
13
|
+
function asAudioB64(value) {
|
|
14
|
+
if (value instanceof Uint8Array)
|
|
15
|
+
return Buffer.from(value).toString("base64");
|
|
16
|
+
if (typeof value === "string" && AUDIO_SUFFIXES.has(extname(value).toLowerCase())) {
|
|
17
|
+
return readFileSync(value).toString("base64");
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* 403 on a voice surface means the account's config doesn't enable voice —
|
|
23
|
+
* rethrow something actionable instead of a bare status error.
|
|
24
|
+
*/
|
|
25
|
+
function translateVoice403(e) {
|
|
26
|
+
if (e instanceof HttpError && e.status === 403) {
|
|
27
|
+
let detail = "voice is not enabled for this account";
|
|
28
|
+
try {
|
|
29
|
+
detail = JSON.parse(e.body).detail ?? detail;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
/* keep default */
|
|
33
|
+
}
|
|
34
|
+
return new Error(`${detail} — voice is enabled per customer config; ask your Synthia ` +
|
|
35
|
+
"contact to turn it on for your organization");
|
|
36
|
+
}
|
|
37
|
+
return e;
|
|
38
|
+
}
|
|
5
39
|
// Launcher argv[1] stems that don't identify a script (REPLs, runners).
|
|
6
40
|
const INTERACTIVE_STEMS = new Set(["", "node", "npx", "tsx", "ts-node"]);
|
|
7
41
|
/**
|
|
@@ -118,6 +152,14 @@ class Http {
|
|
|
118
152
|
throw new HttpError(r.status, await r.text(), "POST", path);
|
|
119
153
|
return r.json();
|
|
120
154
|
}
|
|
155
|
+
/** Binary GET (audio artifacts). */
|
|
156
|
+
async getBytes(path) {
|
|
157
|
+
await this.ready;
|
|
158
|
+
const r = await this.raw("GET", path);
|
|
159
|
+
if (!r.ok)
|
|
160
|
+
throw new HttpError(r.status, await r.text(), "GET", path);
|
|
161
|
+
return new Uint8Array(await r.arrayBuffer());
|
|
162
|
+
}
|
|
121
163
|
/** Un-gated request returning the raw Response (used by the handshake). */
|
|
122
164
|
async raw(method, path, body, params) {
|
|
123
165
|
const url = new URL(this.baseUrl + path);
|
|
@@ -291,8 +333,32 @@ export class Seeds {
|
|
|
291
333
|
constructor(http) {
|
|
292
334
|
this.#http = http;
|
|
293
335
|
}
|
|
294
|
-
/**
|
|
336
|
+
/**
|
|
337
|
+
* Upload seed material (documents, tool schemas, policies, traces...).
|
|
338
|
+
*
|
|
339
|
+
* Overloaded on `content`: an object is ingested as-is (text pipeline);
|
|
340
|
+
* raw bytes or a path to an audio file (.wav/.mp3/...) is uploaded and
|
|
341
|
+
* transcribed server-side (voice-enabled accounts only) — the transcript
|
|
342
|
+
* becomes the seed content.
|
|
343
|
+
*/
|
|
295
344
|
async ingest(opts) {
|
|
345
|
+
const audioB64 = asAudioB64(opts.content);
|
|
346
|
+
if (audioB64 !== null) {
|
|
347
|
+
const filename = typeof opts.content === "string" ? basename(opts.content) : null;
|
|
348
|
+
try {
|
|
349
|
+
return await this.#http.post("/v1/seeds", {
|
|
350
|
+
kind: opts.kind,
|
|
351
|
+
source: opts.source,
|
|
352
|
+
audio_b64: audioB64,
|
|
353
|
+
audio_filename: filename,
|
|
354
|
+
version: opts.version ?? "1",
|
|
355
|
+
metadata: opts.metadata ?? {},
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
catch (e) {
|
|
359
|
+
throw translateVoice403(e);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
296
362
|
return this.#http.post("/v1/seeds", {
|
|
297
363
|
kind: opts.kind,
|
|
298
364
|
source: opts.source,
|
|
@@ -425,12 +491,92 @@ export class QualityCheck {
|
|
|
425
491
|
return body.data;
|
|
426
492
|
}
|
|
427
493
|
}
|
|
494
|
+
/**
|
|
495
|
+
* An async voice render: a scenario (LLM-authored script) or a rollout
|
|
496
|
+
* transcript, voiced with ElevenLabs — N takes spliced into one mixed WAV
|
|
497
|
+
* with per-turn provenance. Requires a voice-enabled customer config.
|
|
498
|
+
*/
|
|
499
|
+
export class VoiceRender {
|
|
500
|
+
id;
|
|
501
|
+
status;
|
|
502
|
+
scenario_id;
|
|
503
|
+
rollout_id;
|
|
504
|
+
params;
|
|
505
|
+
duration_ms;
|
|
506
|
+
wpm;
|
|
507
|
+
provenance;
|
|
508
|
+
error;
|
|
509
|
+
#http;
|
|
510
|
+
constructor(data, http) {
|
|
511
|
+
this.id = data.id;
|
|
512
|
+
this.status = data.status;
|
|
513
|
+
this.scenario_id = data.scenario_id;
|
|
514
|
+
this.rollout_id = data.rollout_id;
|
|
515
|
+
this.params = data.params;
|
|
516
|
+
this.duration_ms = data.duration_ms;
|
|
517
|
+
this.wpm = data.wpm;
|
|
518
|
+
this.provenance = data.provenance;
|
|
519
|
+
this.error = data.error;
|
|
520
|
+
this.#http = http;
|
|
521
|
+
}
|
|
522
|
+
/** Poll until the render finishes; verbose prints server telemetry
|
|
523
|
+
* (per-TTS-call latencies, take/mix progress). */
|
|
524
|
+
async wait(opts = {}) {
|
|
525
|
+
const { pollInterval = 2, timeout = 1800, verbose = false } = opts;
|
|
526
|
+
const events = new EventStream(this.#http, `/v1/voice-renders/${this.id}/events`, verbose);
|
|
527
|
+
const deadline = Date.now() + timeout * 1000;
|
|
528
|
+
while (this.status === "running") {
|
|
529
|
+
if (Date.now() > deadline) {
|
|
530
|
+
throw new Error(`voice render ${this.id} still running after ${timeout}s`);
|
|
531
|
+
}
|
|
532
|
+
await sleep(pollInterval * 1000);
|
|
533
|
+
const data = await this.#http.get(`/v1/voice-renders/${this.id}`);
|
|
534
|
+
this.status = data.status;
|
|
535
|
+
this.params = data.params;
|
|
536
|
+
this.duration_ms = data.duration_ms;
|
|
537
|
+
this.wpm = data.wpm;
|
|
538
|
+
this.provenance = data.provenance;
|
|
539
|
+
this.error = data.error;
|
|
540
|
+
await events.pump();
|
|
541
|
+
}
|
|
542
|
+
await events.pump(); // catch events written after the final status poll
|
|
543
|
+
if (this.status !== "succeeded") {
|
|
544
|
+
throw new Error(`voice render ${this.id} failed: ${this.error}`);
|
|
545
|
+
}
|
|
546
|
+
return this;
|
|
547
|
+
}
|
|
548
|
+
/** The mixed conversation WAV. */
|
|
549
|
+
async audio() {
|
|
550
|
+
try {
|
|
551
|
+
return await this.#http.getBytes(`/v1/voice-renders/${this.id}/audio`);
|
|
552
|
+
}
|
|
553
|
+
catch (e) {
|
|
554
|
+
throw translateVoice403(e);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
/** Write the mixed conversation WAV to `path`; returns it. */
|
|
558
|
+
async saveAudio(path) {
|
|
559
|
+
writeFileSync(path, await this.audio());
|
|
560
|
+
return path;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
async function createVoiceRender(http, body) {
|
|
564
|
+
const clean = Object.fromEntries(Object.entries(body).filter(([, v]) => v !== undefined && v !== null));
|
|
565
|
+
try {
|
|
566
|
+
return new VoiceRender(await http.post("/v1/voice-renders", clean), http);
|
|
567
|
+
}
|
|
568
|
+
catch (e) {
|
|
569
|
+
throw translateVoice403(e);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
428
572
|
export class Rollouts {
|
|
429
573
|
#http;
|
|
430
574
|
#sessionId;
|
|
431
|
-
|
|
575
|
+
#voiceAuto;
|
|
576
|
+
constructor(http, sessionId = null, voiceAuto = false) {
|
|
432
577
|
this.#http = http;
|
|
433
578
|
this.#sessionId = sessionId;
|
|
579
|
+
this.#voiceAuto = voiceAuto;
|
|
434
580
|
}
|
|
435
581
|
/**
|
|
436
582
|
* A stored rollout's full captured state: status, seed, transcript,
|
|
@@ -439,6 +585,34 @@ export class Rollouts {
|
|
|
439
585
|
async get(rolloutId) {
|
|
440
586
|
return this.#http.get(`/v1/rollouts/${rolloutId}`);
|
|
441
587
|
}
|
|
588
|
+
/**
|
|
589
|
+
* Voice a finished rollout: the transcript maps to a script
|
|
590
|
+
* deterministically (words verbatim; `annotate` may add delivery tags
|
|
591
|
+
* only), then N takes are rendered and spliced into one mixed WAV.
|
|
592
|
+
* Requires a voice-enabled customer config (throws otherwise).
|
|
593
|
+
*/
|
|
594
|
+
async voice(rollout, opts = {}) {
|
|
595
|
+
const rolloutId = typeof rollout === "string" ? rollout : rollout.rollout_id;
|
|
596
|
+
return createVoiceRender(this.#http, {
|
|
597
|
+
rollout_id: rolloutId,
|
|
598
|
+
takes: opts.takes ?? 1,
|
|
599
|
+
engine: opts.engine,
|
|
600
|
+
annotate: opts.annotate ?? false,
|
|
601
|
+
phone_fx: opts.phoneFx ?? false,
|
|
602
|
+
room_tone: opts.roomTone ?? false,
|
|
603
|
+
wpm_range: opts.wpmRange,
|
|
604
|
+
voice_overrides: opts.voiceOverrides,
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
/** One voiced turn's WAV (turns with an audio_url only). */
|
|
608
|
+
async turnAudio(rolloutId, idx) {
|
|
609
|
+
try {
|
|
610
|
+
return await this.#http.getBytes(`/v1/rollouts/${rolloutId}/turns/${idx}/audio`);
|
|
611
|
+
}
|
|
612
|
+
catch (e) {
|
|
613
|
+
throw translateVoice403(e);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
442
616
|
/**
|
|
443
617
|
* Play a dataset's scenarios against `agent` (most recent dataset when
|
|
444
618
|
* none is given).
|
|
@@ -491,6 +665,21 @@ export class Rollouts {
|
|
|
491
665
|
}
|
|
492
666
|
};
|
|
493
667
|
await Promise.all(Array.from({ length: Math.min(concurrency, rows.length) }, worker));
|
|
668
|
+
if (this.#voiceAuto) {
|
|
669
|
+
// voice.auto accounts: every completed rollout gets a takes=1 mixed
|
|
670
|
+
// render, kicked here and attached still-running — audio is ready
|
|
671
|
+
// server-side whether or not anyone awaits it.
|
|
672
|
+
for (const result of results) {
|
|
673
|
+
if (result.status === "completed") {
|
|
674
|
+
try {
|
|
675
|
+
result.voice_render = await this.voice(result, { takes: 1 });
|
|
676
|
+
}
|
|
677
|
+
catch {
|
|
678
|
+
/* rendering is a bonus; results stand alone */
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
494
683
|
return results;
|
|
495
684
|
}
|
|
496
685
|
/**
|
|
@@ -516,10 +705,23 @@ export class Rollouts {
|
|
|
516
705
|
while (session.status === "running") {
|
|
517
706
|
const sandbox = ToolSandbox.fromConfig(session.sandbox);
|
|
518
707
|
const reply = await agent(session.transcript, sandbox);
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
708
|
+
const audioB64 = asAudioB64(reply);
|
|
709
|
+
const body = { tool_calls: sandbox.events };
|
|
710
|
+
if (audioB64 !== null) {
|
|
711
|
+
// The agent replied with audio — the server transcribes it
|
|
712
|
+
// (voice-enabled accounts) and the transcript drives the simulator.
|
|
713
|
+
body["reply"] = "";
|
|
714
|
+
body["audio_b64"] = audioB64;
|
|
715
|
+
}
|
|
716
|
+
else {
|
|
717
|
+
body["reply"] = reply;
|
|
718
|
+
}
|
|
719
|
+
try {
|
|
720
|
+
session = await this.#http.post(`/v1/rollouts/${session.id}/turns`, body);
|
|
721
|
+
}
|
|
722
|
+
catch (e) {
|
|
723
|
+
throw audioB64 !== null ? translateVoice403(e) : e;
|
|
724
|
+
}
|
|
523
725
|
}
|
|
524
726
|
return {
|
|
525
727
|
rollout_id: session.id,
|
|
@@ -549,6 +751,12 @@ export class Synthia {
|
|
|
549
751
|
sessionName;
|
|
550
752
|
sessionId = null;
|
|
551
753
|
invocationId = null;
|
|
754
|
+
/** Voice mode, mirrored from the account's customer config by the
|
|
755
|
+
* session handshake: enabled unlocks the voice surfaces; auto makes
|
|
756
|
+
* rollouts voice themselves. Zero client-side flags — flipping a
|
|
757
|
+
* customer's config flips their SDK. */
|
|
758
|
+
voiceEnabled = false;
|
|
759
|
+
voiceAuto = false;
|
|
552
760
|
seeds;
|
|
553
761
|
userModels;
|
|
554
762
|
datasets;
|
|
@@ -611,9 +819,29 @@ export class Synthia {
|
|
|
611
819
|
const data = (await r.json());
|
|
612
820
|
this.sessionId = data.sdk_session_id;
|
|
613
821
|
this.invocationId = data.sdk_invocation_id;
|
|
822
|
+
this.voiceEnabled = data.voice_enabled ?? false;
|
|
823
|
+
this.voiceAuto = data.voice_auto ?? false;
|
|
614
824
|
this.#http.headers["X-Synthia-Session"] = this.sessionId;
|
|
615
825
|
this.#http.headers["X-Synthia-Invocation"] = this.invocationId;
|
|
616
|
-
this.rollouts = new Rollouts(this.#http, this.sessionId);
|
|
826
|
+
this.rollouts = new Rollouts(this.#http, this.sessionId, this.voiceAuto);
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* Voice one scenario (an LLM authors the full two-sided script) or one
|
|
830
|
+
* finished rollout (deterministic transcript transform). Exactly one
|
|
831
|
+
* source id. Requires a voice-enabled customer config.
|
|
832
|
+
*/
|
|
833
|
+
async voiceRender(opts) {
|
|
834
|
+
return createVoiceRender(this.#http, {
|
|
835
|
+
scenario_id: opts.scenarioId,
|
|
836
|
+
rollout_id: opts.rolloutId,
|
|
837
|
+
takes: opts.takes ?? 1,
|
|
838
|
+
engine: opts.engine,
|
|
839
|
+
annotate: opts.annotate ?? false,
|
|
840
|
+
phone_fx: opts.phoneFx ?? false,
|
|
841
|
+
room_tone: opts.roomTone ?? false,
|
|
842
|
+
wpm_range: opts.wpmRange,
|
|
843
|
+
voice_overrides: opts.voiceOverrides,
|
|
844
|
+
});
|
|
617
845
|
}
|
|
618
846
|
/**
|
|
619
847
|
* Probe + generate only when needed; otherwise reuse the latest dataset.
|
|
@@ -634,6 +862,16 @@ export class Synthia {
|
|
|
634
862
|
* scripts/sessions never trigger regeneration here.
|
|
635
863
|
*/
|
|
636
864
|
async prepare(agent, opts = {}) {
|
|
865
|
+
const result = await this.#prepare(agent, opts);
|
|
866
|
+
if (opts.voice) {
|
|
867
|
+
result.voiceRenders = [];
|
|
868
|
+
for (const row of await result.dataset.download()) {
|
|
869
|
+
result.voiceRenders.push(await this.voiceRender({ scenarioId: row.scenario_id, takes: 1 }));
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
return result;
|
|
873
|
+
}
|
|
874
|
+
async #prepare(agent, opts) {
|
|
637
875
|
const { count = 20, maxTurns = 10, minSuccessRate = 0.6, maxSuccessRate = 0.9, verbose = false, } = opts;
|
|
638
876
|
await this.#http.ready;
|
|
639
877
|
const existing = await this.datasets.list(this.sessionId ?? undefined); // newest first
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { Synthia, ToolSandbox, Dataset, GenerationJob, ValidationRun, QualityCheck, Seeds, UserModels, Datasets, Rollouts, DEFAULT_BASE_URL, } from "./client.js";
|
|
2
|
-
export type { Agent, AgentReply, PrepareOptions, PrepareResult, RolloutAgent, RolloutResult, RunOptions, SandboxConfig, SynthiaOptions, ToolCall, ToolEvent, TranscriptTurn, UserModel, WaitOptions, } from "./client.js";
|
|
1
|
+
export { Synthia, ToolSandbox, Dataset, GenerationJob, ValidationRun, QualityCheck, Seeds, UserModels, Datasets, Rollouts, VoiceRender, DEFAULT_BASE_URL, } from "./client.js";
|
|
2
|
+
export type { Agent, AgentReply, AudioInput, PrepareOptions, PrepareResult, RolloutAgent, RolloutResult, RunOptions, SandboxConfig, SynthiaOptions, ToolCall, ToolEvent, TranscriptTurn, UserModel, VoiceOptions, WaitOptions, } from "./client.js";
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { Synthia, ToolSandbox, Dataset, GenerationJob, ValidationRun, QualityCheck, Seeds, UserModels, Datasets, Rollouts, DEFAULT_BASE_URL, } from "./client.js";
|
|
1
|
+
export { Synthia, ToolSandbox, Dataset, GenerationJob, ValidationRun, QualityCheck, Seeds, UserModels, Datasets, Rollouts, VoiceRender, DEFAULT_BASE_URL, } from "./client.js";
|