@scribeberry/sdk 0.2.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/dist/index.mjs ADDED
@@ -0,0 +1,791 @@
1
+ // src/lib/errors.ts
2
+ var ScribeberryError = class extends Error {
3
+ constructor(message, code, status) {
4
+ super(message);
5
+ this.code = code;
6
+ this.status = status;
7
+ this.name = "ScribeberryError";
8
+ }
9
+ };
10
+ var AuthenticationError = class extends ScribeberryError {
11
+ constructor(message = "Invalid or expired API key") {
12
+ super(message, "AUTHENTICATION_ERROR", 401);
13
+ this.name = "AuthenticationError";
14
+ }
15
+ };
16
+ var RateLimitError = class extends ScribeberryError {
17
+ constructor(message = "Rate limit exceeded") {
18
+ super(message, "RATE_LIMIT_EXCEEDED", 429);
19
+ this.name = "RateLimitError";
20
+ }
21
+ };
22
+
23
+ // src/lib/runtime.ts
24
+ var isBrowser = () => typeof window !== "undefined" && typeof window.navigator !== "undefined";
25
+
26
+ // src/client.ts
27
+ var DEFAULT_BASE_URL = "https://api.scribeberry.com";
28
+ var DEFAULT_TIMEOUT = 3e4;
29
+ var TOKEN_REFRESH_BUFFER_MS = 6e4;
30
+ var HttpClient = class {
31
+ constructor(config) {
32
+ /** Current realtime token (fetched via callback or static). */
33
+ this.currentToken = null;
34
+ /** When the current token expires. */
35
+ this.tokenExpiresAt = null;
36
+ /** Active refresh timer. */
37
+ this.refreshTimer = null;
38
+ /** Listeners notified on token refresh. */
39
+ this.tokenRefreshListeners = /* @__PURE__ */ new Set();
40
+ if (!config.apiKey && !config.getRealtimeToken) {
41
+ throw new ScribeberryError(
42
+ "Either apiKey or getRealtimeToken is required",
43
+ "MISSING_AUTH"
44
+ );
45
+ }
46
+ this.getRealtimeToken = config.getRealtimeToken;
47
+ this.hasTokenProvider = !!config.getRealtimeToken;
48
+ if (config.apiKey) {
49
+ const validPrefixes = ["sk_test_", "sk_live_", "sb_rt_"];
50
+ if (!validPrefixes.some((p) => config.apiKey.startsWith(p))) {
51
+ throw new ScribeberryError(
52
+ "API key must start with sk_test_, sk_live_, or sb_rt_",
53
+ "INVALID_API_KEY_FORMAT"
54
+ );
55
+ }
56
+ }
57
+ this.isTemporaryToken = !!config.getRealtimeToken || !!config.apiKey?.startsWith("sb_rt_");
58
+ if (isBrowser() && config.apiKey && !config.apiKey.startsWith("sb_rt_") && !config.getRealtimeToken) {
59
+ console.warn(
60
+ "[Scribeberry] WARNING: Using a permanent API key in the browser is a security risk.\nUse getRealtimeToken callback or sb.realtime.createToken() on your server.\nSee: https://scribeberry.com/docs/realtime#browser-usage"
61
+ );
62
+ }
63
+ this.apiKey = config.apiKey;
64
+ this.baseUrl = (config.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
65
+ this.timeout = config.timeout || DEFAULT_TIMEOUT;
66
+ if (config.apiKey?.startsWith("sb_rt_")) {
67
+ this.currentToken = config.apiKey;
68
+ }
69
+ }
70
+ /**
71
+ * The WebSocket URL derived from the base URL.
72
+ */
73
+ get wsUrl() {
74
+ return this.baseUrl.replace("https://", "wss://").replace("http://", "ws://");
75
+ }
76
+ /**
77
+ * Get the current realtime token for WebSocket connections.
78
+ * If a getRealtimeToken callback is configured, fetches/refreshes as needed.
79
+ * Otherwise returns the static apiKey.
80
+ */
81
+ async getWsToken() {
82
+ if (this.getRealtimeToken) {
83
+ if (this.currentToken && this.tokenExpiresAt && Date.now() < this.tokenExpiresAt - TOKEN_REFRESH_BUFFER_MS) {
84
+ return this.currentToken;
85
+ }
86
+ return this.refreshToken();
87
+ }
88
+ if (!this.apiKey) {
89
+ throw new ScribeberryError(
90
+ "No API key or token available",
91
+ "MISSING_AUTH"
92
+ );
93
+ }
94
+ return this.apiKey;
95
+ }
96
+ /**
97
+ * Fetch a fresh token from the callback and schedule auto-refresh.
98
+ */
99
+ async refreshToken() {
100
+ if (!this.getRealtimeToken) {
101
+ throw new ScribeberryError(
102
+ "No token provider configured",
103
+ "MISSING_AUTH"
104
+ );
105
+ }
106
+ try {
107
+ const result = await this.getRealtimeToken();
108
+ if (!result.token || !result.expiresAt) {
109
+ throw new ScribeberryError(
110
+ "getRealtimeToken must return { token, expiresAt }",
111
+ "INVALID_TOKEN_RESPONSE"
112
+ );
113
+ }
114
+ this.currentToken = result.token;
115
+ this.tokenExpiresAt = new Date(result.expiresAt).getTime();
116
+ this.scheduleRefresh();
117
+ return result.token;
118
+ } catch (err) {
119
+ if (err instanceof ScribeberryError) throw err;
120
+ throw new ScribeberryError(
121
+ `Failed to fetch realtime token: ${err instanceof Error ? err.message : String(err)}`,
122
+ "TOKEN_FETCH_FAILED"
123
+ );
124
+ }
125
+ }
126
+ /**
127
+ * Register a listener that's called when the token is refreshed.
128
+ * Used by RealtimeTranscriptionSession to reconnect with the new token.
129
+ */
130
+ onTokenRefresh(listener) {
131
+ this.tokenRefreshListeners.add(listener);
132
+ }
133
+ /**
134
+ * Remove a token refresh listener.
135
+ */
136
+ offTokenRefresh(listener) {
137
+ this.tokenRefreshListeners.delete(listener);
138
+ }
139
+ /**
140
+ * Schedule an automatic token refresh before the current token expires.
141
+ */
142
+ scheduleRefresh() {
143
+ if (this.refreshTimer) {
144
+ clearTimeout(this.refreshTimer);
145
+ }
146
+ if (!this.tokenExpiresAt || !this.getRealtimeToken) return;
147
+ const msUntilExpiry = this.tokenExpiresAt - Date.now();
148
+ const refreshIn = Math.max(msUntilExpiry - TOKEN_REFRESH_BUFFER_MS, 5e3);
149
+ this.refreshTimer = setTimeout(async () => {
150
+ try {
151
+ const token = await this.refreshToken();
152
+ for (const listener of this.tokenRefreshListeners) {
153
+ try {
154
+ listener(token);
155
+ } catch {
156
+ }
157
+ }
158
+ } catch {
159
+ }
160
+ }, refreshIn);
161
+ }
162
+ /**
163
+ * Make an authenticated HTTP request to the Scribeberry API.
164
+ */
165
+ async request(method, path, options) {
166
+ if (this.isTemporaryToken && !this.apiKey?.startsWith("sk_")) {
167
+ throw new ScribeberryError(
168
+ "Temporary tokens (sb_rt_*) can only be used for realtime WebSocket connections. Use a full API key (sk_test_*/sk_live_*) for REST API calls.",
169
+ "INVALID_TOKEN_SCOPE"
170
+ );
171
+ }
172
+ const url = new URL(`${this.baseUrl}/api/v1${path}`);
173
+ if (options?.params) {
174
+ Object.entries(options.params).forEach(([key, value]) => {
175
+ if (value !== void 0) {
176
+ url.searchParams.set(key, String(value));
177
+ }
178
+ });
179
+ }
180
+ const controller = new AbortController();
181
+ const timeoutId = setTimeout(
182
+ () => controller.abort(),
183
+ options?.timeout || this.timeout
184
+ );
185
+ try {
186
+ const fetchFn = await this.getFetch();
187
+ const response = await fetchFn(url.toString(), {
188
+ method,
189
+ headers: {
190
+ "Authorization": `Bearer ${this.apiKey}`,
191
+ "Content-Type": "application/json",
192
+ "User-Agent": "scribeberry-sdk/0.2.0"
193
+ },
194
+ body: options?.body ? JSON.stringify(options.body) : void 0,
195
+ signal: controller.signal
196
+ });
197
+ if (!response.ok) {
198
+ await this.handleError(response);
199
+ }
200
+ if (response.status === 204 || response.headers.get("content-length") === "0") {
201
+ return {};
202
+ }
203
+ return await response.json();
204
+ } catch (error) {
205
+ if (error instanceof ScribeberryError) throw error;
206
+ if (error instanceof DOMException && error.name === "AbortError") {
207
+ throw new ScribeberryError("Request timed out", "TIMEOUT", 408);
208
+ }
209
+ throw new ScribeberryError(
210
+ `Network error: ${error instanceof Error ? error.message : "Unknown"}`,
211
+ "NETWORK_ERROR"
212
+ );
213
+ } finally {
214
+ clearTimeout(timeoutId);
215
+ }
216
+ }
217
+ async getFetch() {
218
+ if (typeof globalThis.fetch !== "undefined") {
219
+ return globalThis.fetch;
220
+ }
221
+ try {
222
+ const { default: crossFetch } = await import("cross-fetch");
223
+ return crossFetch;
224
+ } catch {
225
+ throw new ScribeberryError(
226
+ 'fetch is not available. Upgrade to Node.js >= 18 or install "cross-fetch".',
227
+ "FETCH_UNAVAILABLE"
228
+ );
229
+ }
230
+ }
231
+ async handleError(response) {
232
+ let body;
233
+ try {
234
+ body = await response.json();
235
+ } catch {
236
+ body = { message: response.statusText };
237
+ }
238
+ const message = body.message || body.error || response.statusText;
239
+ switch (response.status) {
240
+ case 401:
241
+ throw new AuthenticationError(message);
242
+ case 429:
243
+ throw new RateLimitError(message);
244
+ default:
245
+ throw new ScribeberryError(
246
+ message,
247
+ body.code || `HTTP_${response.status}`,
248
+ response.status
249
+ );
250
+ }
251
+ }
252
+ };
253
+
254
+ // src/resources/templates.ts
255
+ var Templates = class {
256
+ constructor(http) {
257
+ this.http = http;
258
+ }
259
+ /**
260
+ * List all templates (paginated).
261
+ */
262
+ async list(options) {
263
+ return this.http.request("GET", "/templates", {
264
+ params: {
265
+ page: options?.page,
266
+ pageSize: options?.pageSize,
267
+ sortBy: options?.sortBy,
268
+ sortOrder: options?.sortOrder
269
+ }
270
+ });
271
+ }
272
+ /**
273
+ * Get a template by ID.
274
+ */
275
+ async get(templateId) {
276
+ return this.http.request("GET", `/templates/${templateId}`);
277
+ }
278
+ /**
279
+ * Create a new template.
280
+ */
281
+ async create(options) {
282
+ return this.http.request("POST", "/templates", { body: options });
283
+ }
284
+ /**
285
+ * Delete a template.
286
+ */
287
+ async delete(templateId) {
288
+ return this.http.request("DELETE", `/templates/${templateId}`);
289
+ }
290
+ };
291
+
292
+ // src/resources/notes.ts
293
+ var Notes = class {
294
+ constructor(http) {
295
+ this.http = http;
296
+ }
297
+ /**
298
+ * Generate a note from conversation text or audio.
299
+ *
300
+ * @param options - Note generation options.
301
+ * @returns The generated note with optional transcript data.
302
+ */
303
+ async generate(options) {
304
+ const body = {
305
+ template_id: options.templateId
306
+ };
307
+ if (options.conversationText) {
308
+ body.conversation_text = options.conversationText;
309
+ }
310
+ if (options.audioUrl) {
311
+ body.audioUrl = options.audioUrl;
312
+ }
313
+ if (options.sourceLanguage) {
314
+ body.sourceLanguage = options.sourceLanguage;
315
+ }
316
+ if (options.transcriptionQuality) {
317
+ body.transcriptionQuality = options.transcriptionQuality;
318
+ }
319
+ if (options.context) {
320
+ body.context = options.context;
321
+ }
322
+ const response = await this.http.request(
323
+ "POST",
324
+ "/notes",
325
+ { body, timeout: 12e4 }
326
+ // Notes may take longer (transcription + LLM)
327
+ );
328
+ return {
329
+ note: {
330
+ markdown: response.note?.markdown || response.markdown || "",
331
+ text: response.note?.text || response.text || "",
332
+ structured: response.note?.structured || response.structured || {}
333
+ },
334
+ transcript: response.transcript ? {
335
+ text: response.transcript.text,
336
+ confidence: response.transcript.confidence,
337
+ duration: response.transcript.duration,
338
+ sourceLanguage: response.transcript.sourceLanguage
339
+ } : void 0,
340
+ template: {
341
+ id: response.template?.id || options.templateId,
342
+ name: response.template?.name || ""
343
+ }
344
+ };
345
+ }
346
+ };
347
+
348
+ // src/lib/ws-client.ts
349
+ async function createWebSocket(url, protocols) {
350
+ if (typeof WebSocket !== "undefined") {
351
+ return new WebSocket(url, protocols);
352
+ }
353
+ try {
354
+ const { default: WS } = await import("ws");
355
+ return new WS(url, protocols);
356
+ } catch {
357
+ throw new Error(
358
+ 'WebSocket is not available. Install the "ws" package: npm install ws'
359
+ );
360
+ }
361
+ }
362
+
363
+ // src/realtime/session.ts
364
+ var RealtimeTranscriptionSession = class {
365
+ constructor(http, config) {
366
+ this.http = http;
367
+ this.config = config;
368
+ this.ws = null;
369
+ this._sessionId = null;
370
+ this._state = "idle";
371
+ this.segments = [];
372
+ this.durationSeconds = 0;
373
+ this.stopResolver = null;
374
+ // Event handlers
375
+ this.handlers = /* @__PURE__ */ new Map();
376
+ }
377
+ /** Current session state. */
378
+ get state() {
379
+ return this._state;
380
+ }
381
+ /** Session ID (available after `'started'` event). */
382
+ get sessionId() {
383
+ return this._sessionId;
384
+ }
385
+ /**
386
+ * Get the full accumulated transcript text at any point during the session.
387
+ *
388
+ * @returns Concatenated text from all confirmed segments.
389
+ */
390
+ getTranscript() {
391
+ return this.segments.map((s) => s.text).join(" ");
392
+ }
393
+ /**
394
+ * Get all confirmed transcript segments.
395
+ *
396
+ * @returns A copy of the segments array.
397
+ */
398
+ getSegments() {
399
+ return [...this.segments];
400
+ }
401
+ /**
402
+ * Connect to the realtime transcription server.
403
+ *
404
+ * Called automatically by `sb.realtime.transcribe()`. You don't usually
405
+ * need to call this directly.
406
+ *
407
+ * @throws {ScribeberryError} If the session has already been started
408
+ */
409
+ async connect() {
410
+ if (this._state !== "idle") {
411
+ throw new ScribeberryError(
412
+ "Session already started",
413
+ "SESSION_STATE_ERROR"
414
+ );
415
+ }
416
+ this._state = "connecting";
417
+ return new Promise(async (resolve, reject) => {
418
+ try {
419
+ const token = await this.http.getWsToken();
420
+ const wsUrl = `${this.http.wsUrl}/ws/realtime?token=${encodeURIComponent(token)}`;
421
+ this.ws = await createWebSocket(wsUrl);
422
+ } catch (err) {
423
+ this._state = "idle";
424
+ const error = new ScribeberryError(
425
+ `Failed to create WebSocket: ${err instanceof Error ? err.message : String(err)}`,
426
+ "CONNECTION_ERROR"
427
+ );
428
+ reject(error);
429
+ return;
430
+ }
431
+ const connectTimeout = setTimeout(() => {
432
+ if (this._state === "connecting") {
433
+ const error = new ScribeberryError(
434
+ "Connect timeout \u2014 no session:started received",
435
+ "CONNECT_TIMEOUT"
436
+ );
437
+ reject(error);
438
+ this.emit("error", error);
439
+ this.cleanup();
440
+ }
441
+ }, 1e4);
442
+ this.ws.onopen = () => {
443
+ this.send({
444
+ type: "start",
445
+ config: {
446
+ language: this.config.language,
447
+ enableDiarization: this.config.enableDiarization,
448
+ templateId: this.config.templateId
449
+ }
450
+ });
451
+ };
452
+ this.ws.onmessage = (event) => {
453
+ const raw = typeof event.data === "string" ? event.data : String(event.data);
454
+ let data;
455
+ try {
456
+ data = JSON.parse(raw);
457
+ } catch {
458
+ return;
459
+ }
460
+ if (data.type === "session:started") {
461
+ clearTimeout(connectTimeout);
462
+ }
463
+ this.handleMessage(data, resolve);
464
+ };
465
+ this.ws.onerror = (event) => {
466
+ clearTimeout(connectTimeout);
467
+ const error = new ScribeberryError(
468
+ "WebSocket connection error",
469
+ "CONNECTION_ERROR"
470
+ );
471
+ if (this._state === "connecting") {
472
+ reject(error);
473
+ }
474
+ this.emit("error", error);
475
+ };
476
+ this.ws.onclose = (event) => {
477
+ clearTimeout(connectTimeout);
478
+ if (this._state === "connecting") {
479
+ reject(
480
+ new ScribeberryError(
481
+ `Connection closed: ${event?.code || "unknown"}`,
482
+ "CONNECTION_CLOSED"
483
+ )
484
+ );
485
+ }
486
+ if (this._state !== "stopped") {
487
+ this._state = "stopped";
488
+ }
489
+ };
490
+ });
491
+ }
492
+ /**
493
+ * Send an audio chunk to the server for transcription.
494
+ *
495
+ * **Required format:** PCM 16-bit signed little-endian, 16kHz, mono.
496
+ *
497
+ * @param data - Raw audio data
498
+ * @throws {ScribeberryError} If the session is not active
499
+ */
500
+ sendAudio(data) {
501
+ if (this._state !== "active") {
502
+ throw new ScribeberryError(
503
+ 'Session is not active. Wait for the "started" event before sending audio.',
504
+ "SESSION_STATE_ERROR"
505
+ );
506
+ }
507
+ this.ws?.send(data);
508
+ }
509
+ /**
510
+ * Stream audio from an async iterable source (e.g., microphone stream).
511
+ *
512
+ * @param stream - Async iterable of audio chunks.
513
+ */
514
+ async sendStream(stream) {
515
+ for await (const chunk of stream) {
516
+ if (this._state !== "active") break;
517
+ this.sendAudio(chunk);
518
+ }
519
+ }
520
+ /**
521
+ * Pause audio streaming. The WebSocket connection stays alive.
522
+ */
523
+ pause() {
524
+ this.send({ type: "pause" });
525
+ }
526
+ /**
527
+ * Resume audio streaming after a pause.
528
+ */
529
+ resume() {
530
+ this.send({ type: "resume" });
531
+ }
532
+ /**
533
+ * Request finalization of the current audio buffer.
534
+ * Forces the server to emit any pending partial results as final.
535
+ */
536
+ finalize() {
537
+ this.send({ type: "finalize" });
538
+ }
539
+ /**
540
+ * Stop the session gracefully.
541
+ *
542
+ * Waits for the server to confirm the stop and (if configured) for
543
+ * note generation to complete.
544
+ *
545
+ * @returns Final transcript, segments, duration, and optional note.
546
+ */
547
+ async stop() {
548
+ if (this._state === "stopped") {
549
+ return this.buildResult();
550
+ }
551
+ this._state = "stopping";
552
+ return new Promise((resolve) => {
553
+ this.stopResolver = resolve;
554
+ this.send({ type: "stop" });
555
+ setTimeout(() => {
556
+ if (this.stopResolver) {
557
+ this.stopResolver(this.buildResult());
558
+ this.stopResolver = null;
559
+ this.cleanup();
560
+ }
561
+ }, 3e4);
562
+ });
563
+ }
564
+ /**
565
+ * Register an event handler.
566
+ *
567
+ * @param event - Event name
568
+ * @param handler - Event handler function
569
+ * @returns `this` for chaining
570
+ *
571
+ * @example
572
+ * ```typescript
573
+ * session
574
+ * .on('partial', (text) => console.log('Interim:', text))
575
+ * .on('final', (segment) => console.log('Confirmed:', segment.text))
576
+ * .on('error', (err) => console.error(err));
577
+ * ```
578
+ */
579
+ on(event, handler) {
580
+ if (!this.handlers.has(event)) {
581
+ this.handlers.set(event, /* @__PURE__ */ new Set());
582
+ }
583
+ this.handlers.get(event).add(handler);
584
+ return this;
585
+ }
586
+ /**
587
+ * Remove an event handler.
588
+ *
589
+ * @param event - Event name
590
+ * @param handler - Handler to remove
591
+ * @returns `this` for chaining
592
+ */
593
+ off(event, handler) {
594
+ this.handlers.get(event)?.delete(handler);
595
+ return this;
596
+ }
597
+ // --- Internal ---
598
+ emit(event, ...args) {
599
+ this.handlers.get(event)?.forEach((handler) => {
600
+ try {
601
+ handler(...args);
602
+ } catch {
603
+ }
604
+ });
605
+ }
606
+ send(message) {
607
+ if (this.ws?.readyState === 1) {
608
+ this.ws.send(JSON.stringify(message));
609
+ }
610
+ }
611
+ handleMessage(data, connectResolve) {
612
+ switch (data.type) {
613
+ case "session:started":
614
+ this._sessionId = data.sessionId;
615
+ this._state = "active";
616
+ this.emit("started", data.sessionId);
617
+ connectResolve?.();
618
+ break;
619
+ case "transcript:partial":
620
+ this.emit("partial", data.text, data.speaker);
621
+ break;
622
+ case "transcript:final": {
623
+ const segment = {
624
+ text: data.text,
625
+ speaker: data.speaker,
626
+ startMs: data.startMs,
627
+ endMs: data.endMs
628
+ };
629
+ this.segments.push(segment);
630
+ this.emit("final", segment);
631
+ break;
632
+ }
633
+ case "endpoint":
634
+ this.emit("endpoint");
635
+ break;
636
+ case "transcript:full":
637
+ this.durationSeconds = (data.durationMs || 0) / 1e3;
638
+ break;
639
+ case "note:generating":
640
+ break;
641
+ case "note:complete":
642
+ this.note = data.note;
643
+ this.emit("note", data.note);
644
+ this.resolveStop();
645
+ break;
646
+ case "session:stopped":
647
+ this.durationSeconds = data.durationSeconds || this.durationSeconds;
648
+ if (!this.config.templateId) {
649
+ this.resolveStop();
650
+ }
651
+ break;
652
+ case "error":
653
+ this.emit(
654
+ "error",
655
+ new ScribeberryError(
656
+ data.message || "Unknown error",
657
+ data.code || "REALTIME_ERROR"
658
+ )
659
+ );
660
+ break;
661
+ }
662
+ }
663
+ resolveStop() {
664
+ this._state = "stopped";
665
+ const result = this.buildResult();
666
+ this.emit("stopped", result);
667
+ if (this.stopResolver) {
668
+ this.stopResolver(result);
669
+ this.stopResolver = null;
670
+ }
671
+ this.cleanup();
672
+ }
673
+ buildResult() {
674
+ return {
675
+ transcript: this.getTranscript(),
676
+ segments: this.getSegments(),
677
+ durationSeconds: this.durationSeconds,
678
+ note: this.note
679
+ };
680
+ }
681
+ cleanup() {
682
+ try {
683
+ this.ws?.close();
684
+ } catch {
685
+ }
686
+ this.ws = null;
687
+ }
688
+ };
689
+
690
+ // src/realtime/index.ts
691
+ var Realtime = class {
692
+ constructor(http) {
693
+ this.http = http;
694
+ }
695
+ /**
696
+ * Create a temporary realtime token for browser-side WebSocket access.
697
+ *
698
+ * **Server-side only.** Call this from your backend, then pass the token
699
+ * to your frontend for use with `new Scribeberry({ apiKey: token })`.
700
+ *
701
+ * @param options - Token creation options.
702
+ * @returns A temporary token and the WebSocket URL.
703
+ *
704
+ * @example
705
+ * ```typescript
706
+ * // On your server
707
+ * const sb = new Scribeberry({ apiKey: 'sk_live_...' });
708
+ * const { token, wsUrl, expiresAt } = await sb.realtime.createToken({
709
+ * expiresInSeconds: 3600,
710
+ * });
711
+ *
712
+ * // Return token to the browser client
713
+ * res.json({ token, wsUrl, expiresAt });
714
+ * ```
715
+ *
716
+ * @throws {ScribeberryError} If called in a browser environment
717
+ * @throws {ScribeberryError} If called with a temporary token (sb_rt_*)
718
+ */
719
+ async createToken(options = {}) {
720
+ if (isBrowser()) {
721
+ throw new ScribeberryError(
722
+ "createToken() is only available in Node.js server environments. Call this from your backend and pass the resulting token to the browser.",
723
+ "SERVER_ONLY"
724
+ );
725
+ }
726
+ if (this.http.isTemporaryToken) {
727
+ throw new ScribeberryError(
728
+ "Cannot create a token using a temporary token. Use a full API key (sk_test_*/sk_live_*).",
729
+ "INVALID_TOKEN_SCOPE"
730
+ );
731
+ }
732
+ return this.http.request("POST", "/realtime/tokens", {
733
+ body: {
734
+ expiresInSeconds: options.expiresInSeconds
735
+ }
736
+ });
737
+ }
738
+ /**
739
+ * Start a new realtime transcription session.
740
+ *
741
+ * The session connects via WebSocket and streams transcription results
742
+ * back as audio is sent. Optionally generates a note when stopped.
743
+ *
744
+ * **Audio format:** PCM 16-bit signed little-endian, 16kHz, mono.
745
+ *
746
+ * @param config - Session configuration.
747
+ * @returns A realtime transcription session.
748
+ *
749
+ * @example
750
+ * ```typescript
751
+ * const session = sb.realtime.transcribe({
752
+ * language: 'en-US',
753
+ * enableDiarization: true,
754
+ * templateId: 'template-id',
755
+ * });
756
+ *
757
+ * session.on('partial', (text) => console.log('Partial:', text));
758
+ * session.on('final', (segment) => console.log('Final:', segment.text));
759
+ * session.on('stopped', (result) => console.log('Transcript:', result.transcript));
760
+ *
761
+ * // Stream audio chunks from your microphone
762
+ * session.sendAudio(audioChunk);
763
+ *
764
+ * // When done
765
+ * const result = await session.stop();
766
+ * ```
767
+ */
768
+ transcribe(config = {}) {
769
+ const session = new RealtimeTranscriptionSession(this.http, config);
770
+ session.connect().catch(() => {
771
+ });
772
+ return session;
773
+ }
774
+ };
775
+
776
+ // src/index.ts
777
+ var Scribeberry = class {
778
+ constructor(config) {
779
+ this.http = new HttpClient(config);
780
+ this.templates = new Templates(this.http);
781
+ this.notes = new Notes(this.http);
782
+ this.realtime = new Realtime(this.http);
783
+ }
784
+ };
785
+ export {
786
+ AuthenticationError,
787
+ RateLimitError,
788
+ RealtimeTranscriptionSession,
789
+ Scribeberry,
790
+ ScribeberryError
791
+ };