@ai-matrx/media 0.15.3 → 0.15.5

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/CHANGELOG.md CHANGED
@@ -1,9 +1,15 @@
1
1
  # @ai-matrx/media
2
2
 
3
- ## 0.15.3
3
+ ## 0.15.5
4
4
 
5
5
  Automatic changed-only republish (docs/metadata drift since the last tag — see
6
- `git diff npm/media/v0.15.2..npm/media/v0.15.3 -- apps/shared/media`).
6
+ `git diff npm/media/v0.15.4..npm/media/v0.15.5 -- apps/shared/media`).
7
+ No source changes intended and no consumer action required.
8
+
9
+ ## 0.15.2
10
+
11
+ Automatic changed-only republish (docs/metadata drift since the last tag — see
12
+ `git diff npm/media/v0.15.1..npm/media/v0.15.2 -- apps/shared/media`).
7
13
  No source changes intended and no consumer action required.
8
14
 
9
15
  ## 0.15.1
@@ -34,7 +34,8 @@
34
34
  * - QuotaExceededError triggers an immediate eviction down to 70% of
35
35
  * budget and a single retry. Subsequent failures fall back to network.
36
36
  */
37
- import Dexie, { type Table } from "dexie";
37
+ import type Dexie from "dexie";
38
+ import type { Table } from "dexie";
38
39
  export declare const BLOB_CACHE_DB_NAME = "matrx-blob-cache";
39
40
  export declare const BLOB_CACHE_SCHEMA_VERSION = 1;
40
41
  export declare const DEFAULT_BUDGET_BYTES: number;
@@ -1,4 +1,3 @@
1
- import Dexie from "dexie";
2
1
  import { logger } from "../support/logger.js";
3
2
  import { extractErrorMessage } from "@ai-matrx/data/net";
4
3
  const BLOB_CACHE_DB_NAME = "matrx-blob-cache";
@@ -20,7 +19,8 @@ function openBlobCacheDb() {
20
19
  return null;
21
20
  }
22
21
  try {
23
- const db = new Dexie(BLOB_CACHE_DB_NAME);
22
+ const { default: DexieCtor } = await import("dexie");
23
+ const db = new DexieCtor(BLOB_CACHE_DB_NAME);
24
24
  db.version(BLOB_CACHE_SCHEMA_VERSION).stores({
25
25
  // Primary key `key`; secondary indexes for purge + eviction.
26
26
  blobs: "key, userId, lastAccessedAt, bytes"
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/files/engine/cache/idb-store.ts"],"sourcesContent":["/**\n * features/files/cache/idb-store.ts\n *\n * IndexedDB tier of the 3-tier byte cache (memory LRU → IDB → network).\n *\n * Dexie wrapper for the `matrx-blob-cache` database — a SEPARATE DB from\n * `matrx-sync` so cache eviction can never threaten sync persistence.\n * Schema is intentionally simple: one `blobs` table keyed by a string\n * cache key, indexed by `userId` (for sign-out purge) and\n * `lastAccessedAt` (for LRU eviction).\n *\n * Cache key strategy (see plan §4A):\n * - Owned files (current version): `${userId}:${fileId}:current:${checksum}`\n * - Owned files (pinned version): `${userId}:${fileId}:${N}:${checksum}`\n * - URL-keyed entries (share-link bytes, etc.): `${userId}:url:${sha256(url)}`\n *\n * Signed S3 URLs (either AWS dialect — SigV4 `X-Amz-*` or SigV2\n * `AWSAccessKeyId`/`Signature`/`Expires`) are NEVER cached because they\n * expire — caching them would create stale-URL bugs.\n *\n * Failure modes:\n * - Private browsing / IDB disabled / quota exceeded: `openBlobCacheDb()`\n * rejects → all reads return null, all writes resolve as no-ops. The\n * in-memory LRU + network stay functional.\n *\n * Identity scoping:\n * - Every entry is stamped with `userId`. Sign-out calls `clearForUser`\n * so cross-user contamination is structurally impossible.\n *\n * Budget:\n * - Default 2 GB total. Eviction is size-aware-LRU: oldest entries above\n * a 2 MB floor are evicted first; tiny entries (<2 MB) only evict when\n * the cache is otherwise full of small entries.\n * - QuotaExceededError triggers an immediate eviction down to 70% of\n * budget and a single retry. Subsequent failures fall back to network.\n */\n\nimport Dexie, { type Table } from \"dexie\";\nimport { logger } from \"../support/logger\";\nimport { extractErrorMessage } from \"@ai-matrx/data/net\";\n\nexport const BLOB_CACHE_DB_NAME = \"matrx-blob-cache\";\nexport const BLOB_CACHE_SCHEMA_VERSION = 1;\nexport const DEFAULT_BUDGET_BYTES = 2 * 1024 * 1024 * 1024; // 2 GB\nconst SIZE_FLOOR_BYTES = 2 * 1024 * 1024; // 2 MB — evict bigger entries first\n\nexport type BlobCacheSource =\n | \"files-download\"\n | \"share\"\n | \"cdn\"\n | \"url\";\n\nexport interface BlobCacheEntry {\n /** Composite cache key — see header comment for the key shape. */\n key: string;\n userId: string;\n /** Null for url-keyed entries (share links / arbitrary URLs). */\n fileId: string | null;\n version: number | null;\n checksum: string | null;\n mimeType: string;\n bytes: number;\n blob: Blob;\n etag: string | null;\n fetchedAt: number;\n lastAccessedAt: number;\n source: BlobCacheSource;\n}\n\ninterface BlobCacheDb extends Dexie {\n blobs: Table<BlobCacheEntry, string>;\n}\n\nfunction hasIndexedDb(): boolean {\n if (typeof globalThis === \"undefined\") return false;\n return typeof (globalThis as { indexedDB?: unknown }).indexedDB !== \"undefined\";\n}\n\nlet dbPromise: Promise<BlobCacheDb | null> | null = null;\n\n/**\n * Open the Dexie blob-cache DB. Lazy + memoized — the server-render pass\n * never touches IDB. Returns `null` when IDB is unavailable (private\n * browsing, server-side, quota exhausted at open time); callers should\n * treat that as \"no persistent tier\" and fall through to network.\n */\nexport function openBlobCacheDb(): Promise<BlobCacheDb | null> {\n if (dbPromise) return dbPromise;\n dbPromise = (async () => {\n if (!hasIndexedDb()) {\n logger.info(\"blob-cache.unavailable\", {\n meta: { reason: \"no-indexedDB\" },\n });\n return null;\n }\n try {\n const db = new Dexie(BLOB_CACHE_DB_NAME) as BlobCacheDb;\n db.version(BLOB_CACHE_SCHEMA_VERSION).stores({\n // Primary key `key`; secondary indexes for purge + eviction.\n blobs: \"key, userId, lastAccessedAt, bytes\",\n });\n await db.open();\n logger.info(\"blob-cache.open.success\", {\n meta: { schemaVersion: BLOB_CACHE_SCHEMA_VERSION },\n });\n return db;\n } catch (err) {\n logger.warn(\"blob-cache.open.error\", {\n meta: { error: extractErrorMessage(err) },\n });\n return null;\n }\n })();\n return dbPromise;\n}\n\n// ---------------------------------------------------------------------------\n// Read / write\n// ---------------------------------------------------------------------------\n\nexport async function getEntry(key: string): Promise<BlobCacheEntry | null> {\n const db = await openBlobCacheDb();\n if (!db) return null;\n try {\n const entry = await db.blobs.get(key);\n if (!entry) return null;\n // Bump LRU position. Fire-and-forget — the read result is already\n // returned, and a missed lastAccessedAt bump just shortens this entry's\n // lifetime in the next eviction pass (acceptable).\n void db.blobs.update(key, { lastAccessedAt: Date.now() });\n return entry;\n } catch (err) {\n logger.warn(\"blob-cache.read.error\", {\n meta: { key, error: extractErrorMessage(err) },\n });\n return null;\n }\n}\n\nexport async function putEntry(entry: BlobCacheEntry): Promise<void> {\n const db = await openBlobCacheDb();\n if (!db) return;\n try {\n await db.blobs.put(entry);\n // Best-effort eviction pass. If quota is exhausted we'll catch and\n // retry the put once after an emergency eviction.\n } catch (err) {\n // Quota-exceeded: try to free space, retry once, then give up.\n if (isQuotaExceeded(err)) {\n try {\n await evictToBudget(db, entry.userId, DEFAULT_BUDGET_BYTES * 0.7);\n await db.blobs.put(entry);\n } catch (retryErr) {\n logger.warn(\"blob-cache.write.quota-exceeded\", {\n meta: { key: entry.key, error: extractErrorMessage(retryErr) },\n });\n }\n return;\n }\n logger.warn(\"blob-cache.write.error\", {\n meta: { key: entry.key, error: extractErrorMessage(err) },\n });\n }\n}\n\nexport async function deleteEntry(key: string): Promise<void> {\n const db = await openBlobCacheDb();\n if (!db) return;\n try {\n await db.blobs.delete(key);\n } catch (err) {\n logger.warn(\"blob-cache.delete.error\", {\n meta: { key, error: extractErrorMessage(err) },\n });\n }\n}\n\n/**\n * Drop every entry whose key starts with `${userId}:${fileId}:`. Use on\n * realtime version-bump / hard-delete so every cached version of a file\n * gets evicted regardless of the version segment in the key.\n */\nexport async function deleteEntriesForFile(\n userId: string,\n fileId: string,\n): Promise<void> {\n const db = await openBlobCacheDb();\n if (!db) return;\n try {\n await db.blobs.where(\"userId\").equals(userId).and((entry) => entry.fileId === fileId).delete();\n } catch (err) {\n logger.warn(\"blob-cache.delete-for-file.error\", {\n meta: { userId, fileId, error: extractErrorMessage(err) },\n });\n }\n}\n\n/**\n * Drop every entry for the signed-out user. Called from the sign-out\n * code path so the next user can never read the previous user's blobs.\n */\nexport async function clearForUser(userId: string): Promise<void> {\n const db = await openBlobCacheDb();\n if (!db) return;\n try {\n await db.blobs.where(\"userId\").equals(userId).delete();\n } catch (err) {\n logger.warn(\"blob-cache.clear-for-user.error\", {\n meta: { userId, error: extractErrorMessage(err) },\n });\n }\n}\n\n/**\n * Test/debug helper. Returns `{ entryCount, totalBytes }`. The admin\n * observability panel reads this.\n */\nexport async function getStats(): Promise<{ entryCount: number; totalBytes: number }> {\n const db = await openBlobCacheDb();\n if (!db) return { entryCount: 0, totalBytes: 0 };\n try {\n let count = 0;\n let total = 0;\n await db.blobs.each((entry) => {\n count += 1;\n total += entry.bytes;\n });\n return { entryCount: count, totalBytes: total };\n } catch {\n return { entryCount: 0, totalBytes: 0 };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Eviction\n// ---------------------------------------------------------------------------\n\nfunction isQuotaExceeded(err: unknown): boolean {\n if (!err || typeof err !== \"object\") return false;\n const e = err as { name?: string; code?: number };\n return e.name === \"QuotaExceededError\" || e.code === 22;\n}\n\n/**\n * Evict entries (size-aware-LRU) for `userId` until total bytes ≤ target.\n *\n * Strategy: entries are sorted by `(bytes >= SIZE_FLOOR_BYTES, lastAccessedAt)`\n * so large stale entries go first; small entries only get evicted once the\n * large pile is gone. Stops when the per-user total drops below `targetBytes`.\n */\nasync function evictToBudget(\n db: BlobCacheDb,\n userId: string,\n targetBytes: number,\n): Promise<void> {\n const entries = await db.blobs\n .where(\"userId\")\n .equals(userId)\n .toArray();\n const ordered = entries.sort((a, b) => {\n const aBig = a.bytes >= SIZE_FLOOR_BYTES ? 0 : 1;\n const bBig = b.bytes >= SIZE_FLOOR_BYTES ? 0 : 1;\n if (aBig !== bBig) return aBig - bBig;\n return a.lastAccessedAt - b.lastAccessedAt;\n });\n let total = ordered.reduce((sum, e) => sum + e.bytes, 0);\n const toDelete: string[] = [];\n for (const entry of ordered) {\n if (total <= targetBytes) break;\n toDelete.push(entry.key);\n total -= entry.bytes;\n }\n if (toDelete.length > 0) {\n await db.blobs.bulkDelete(toDelete);\n logger.info(\"blob-cache.evicted\", {\n meta: { userId, evicted: toDelete.length, totalAfter: total },\n });\n }\n}\n\n/**\n * Public LRU evict for a given user — exposed so the admin observability\n * panel can offer an \"Evict now\" button.\n */\nexport async function evictForUser(userId: string, targetBytes: number): Promise<void> {\n const db = await openBlobCacheDb();\n if (!db) return;\n await evictToBudget(db, userId, targetBytes);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../../src/files/engine/cache/idb-store.ts"],"sourcesContent":["/**\n * features/files/cache/idb-store.ts\n *\n * IndexedDB tier of the 3-tier byte cache (memory LRU → IDB → network).\n *\n * Dexie wrapper for the `matrx-blob-cache` database — a SEPARATE DB from\n * `matrx-sync` so cache eviction can never threaten sync persistence.\n * Schema is intentionally simple: one `blobs` table keyed by a string\n * cache key, indexed by `userId` (for sign-out purge) and\n * `lastAccessedAt` (for LRU eviction).\n *\n * Cache key strategy (see plan §4A):\n * - Owned files (current version): `${userId}:${fileId}:current:${checksum}`\n * - Owned files (pinned version): `${userId}:${fileId}:${N}:${checksum}`\n * - URL-keyed entries (share-link bytes, etc.): `${userId}:url:${sha256(url)}`\n *\n * Signed S3 URLs (either AWS dialect — SigV4 `X-Amz-*` or SigV2\n * `AWSAccessKeyId`/`Signature`/`Expires`) are NEVER cached because they\n * expire — caching them would create stale-URL bugs.\n *\n * Failure modes:\n * - Private browsing / IDB disabled / quota exceeded: `openBlobCacheDb()`\n * rejects → all reads return null, all writes resolve as no-ops. The\n * in-memory LRU + network stay functional.\n *\n * Identity scoping:\n * - Every entry is stamped with `userId`. Sign-out calls `clearForUser`\n * so cross-user contamination is structurally impossible.\n *\n * Budget:\n * - Default 2 GB total. Eviction is size-aware-LRU: oldest entries above\n * a 2 MB floor are evicted first; tiny entries (<2 MB) only evict when\n * the cache is otherwise full of small entries.\n * - QuotaExceededError triggers an immediate eviction down to 70% of\n * budget and a single retry. Subsequent failures fall back to network.\n */\n\n// Types only at import: Dexie (~28 kB gzip) loads when the blob cache first opens, so the files\n// engine in a store factory never puts it on a first screen.\nimport type Dexie from \"dexie\";\nimport type { Table } from \"dexie\";\nimport { logger } from \"../support/logger\";\nimport { extractErrorMessage } from \"@ai-matrx/data/net\";\n\nexport const BLOB_CACHE_DB_NAME = \"matrx-blob-cache\";\nexport const BLOB_CACHE_SCHEMA_VERSION = 1;\nexport const DEFAULT_BUDGET_BYTES = 2 * 1024 * 1024 * 1024; // 2 GB\nconst SIZE_FLOOR_BYTES = 2 * 1024 * 1024; // 2 MB — evict bigger entries first\n\nexport type BlobCacheSource =\n | \"files-download\"\n | \"share\"\n | \"cdn\"\n | \"url\";\n\nexport interface BlobCacheEntry {\n /** Composite cache key — see header comment for the key shape. */\n key: string;\n userId: string;\n /** Null for url-keyed entries (share links / arbitrary URLs). */\n fileId: string | null;\n version: number | null;\n checksum: string | null;\n mimeType: string;\n bytes: number;\n blob: Blob;\n etag: string | null;\n fetchedAt: number;\n lastAccessedAt: number;\n source: BlobCacheSource;\n}\n\ninterface BlobCacheDb extends Dexie {\n blobs: Table<BlobCacheEntry, string>;\n}\n\nfunction hasIndexedDb(): boolean {\n if (typeof globalThis === \"undefined\") return false;\n return typeof (globalThis as { indexedDB?: unknown }).indexedDB !== \"undefined\";\n}\n\nlet dbPromise: Promise<BlobCacheDb | null> | null = null;\n\n/**\n * Open the Dexie blob-cache DB. Lazy + memoized — the server-render pass\n * never touches IDB. Returns `null` when IDB is unavailable (private\n * browsing, server-side, quota exhausted at open time); callers should\n * treat that as \"no persistent tier\" and fall through to network.\n */\nexport function openBlobCacheDb(): Promise<BlobCacheDb | null> {\n if (dbPromise) return dbPromise;\n dbPromise = (async () => {\n if (!hasIndexedDb()) {\n logger.info(\"blob-cache.unavailable\", {\n meta: { reason: \"no-indexedDB\" },\n });\n return null;\n }\n try {\n const { default: DexieCtor } = await import(\"dexie\");\n const db = new DexieCtor(BLOB_CACHE_DB_NAME) as BlobCacheDb;\n db.version(BLOB_CACHE_SCHEMA_VERSION).stores({\n // Primary key `key`; secondary indexes for purge + eviction.\n blobs: \"key, userId, lastAccessedAt, bytes\",\n });\n await db.open();\n logger.info(\"blob-cache.open.success\", {\n meta: { schemaVersion: BLOB_CACHE_SCHEMA_VERSION },\n });\n return db;\n } catch (err) {\n logger.warn(\"blob-cache.open.error\", {\n meta: { error: extractErrorMessage(err) },\n });\n return null;\n }\n })();\n return dbPromise;\n}\n\n// ---------------------------------------------------------------------------\n// Read / write\n// ---------------------------------------------------------------------------\n\nexport async function getEntry(key: string): Promise<BlobCacheEntry | null> {\n const db = await openBlobCacheDb();\n if (!db) return null;\n try {\n const entry = await db.blobs.get(key);\n if (!entry) return null;\n // Bump LRU position. Fire-and-forget — the read result is already\n // returned, and a missed lastAccessedAt bump just shortens this entry's\n // lifetime in the next eviction pass (acceptable).\n void db.blobs.update(key, { lastAccessedAt: Date.now() });\n return entry;\n } catch (err) {\n logger.warn(\"blob-cache.read.error\", {\n meta: { key, error: extractErrorMessage(err) },\n });\n return null;\n }\n}\n\nexport async function putEntry(entry: BlobCacheEntry): Promise<void> {\n const db = await openBlobCacheDb();\n if (!db) return;\n try {\n await db.blobs.put(entry);\n // Best-effort eviction pass. If quota is exhausted we'll catch and\n // retry the put once after an emergency eviction.\n } catch (err) {\n // Quota-exceeded: try to free space, retry once, then give up.\n if (isQuotaExceeded(err)) {\n try {\n await evictToBudget(db, entry.userId, DEFAULT_BUDGET_BYTES * 0.7);\n await db.blobs.put(entry);\n } catch (retryErr) {\n logger.warn(\"blob-cache.write.quota-exceeded\", {\n meta: { key: entry.key, error: extractErrorMessage(retryErr) },\n });\n }\n return;\n }\n logger.warn(\"blob-cache.write.error\", {\n meta: { key: entry.key, error: extractErrorMessage(err) },\n });\n }\n}\n\nexport async function deleteEntry(key: string): Promise<void> {\n const db = await openBlobCacheDb();\n if (!db) return;\n try {\n await db.blobs.delete(key);\n } catch (err) {\n logger.warn(\"blob-cache.delete.error\", {\n meta: { key, error: extractErrorMessage(err) },\n });\n }\n}\n\n/**\n * Drop every entry whose key starts with `${userId}:${fileId}:`. Use on\n * realtime version-bump / hard-delete so every cached version of a file\n * gets evicted regardless of the version segment in the key.\n */\nexport async function deleteEntriesForFile(\n userId: string,\n fileId: string,\n): Promise<void> {\n const db = await openBlobCacheDb();\n if (!db) return;\n try {\n await db.blobs.where(\"userId\").equals(userId).and((entry) => entry.fileId === fileId).delete();\n } catch (err) {\n logger.warn(\"blob-cache.delete-for-file.error\", {\n meta: { userId, fileId, error: extractErrorMessage(err) },\n });\n }\n}\n\n/**\n * Drop every entry for the signed-out user. Called from the sign-out\n * code path so the next user can never read the previous user's blobs.\n */\nexport async function clearForUser(userId: string): Promise<void> {\n const db = await openBlobCacheDb();\n if (!db) return;\n try {\n await db.blobs.where(\"userId\").equals(userId).delete();\n } catch (err) {\n logger.warn(\"blob-cache.clear-for-user.error\", {\n meta: { userId, error: extractErrorMessage(err) },\n });\n }\n}\n\n/**\n * Test/debug helper. Returns `{ entryCount, totalBytes }`. The admin\n * observability panel reads this.\n */\nexport async function getStats(): Promise<{ entryCount: number; totalBytes: number }> {\n const db = await openBlobCacheDb();\n if (!db) return { entryCount: 0, totalBytes: 0 };\n try {\n let count = 0;\n let total = 0;\n await db.blobs.each((entry) => {\n count += 1;\n total += entry.bytes;\n });\n return { entryCount: count, totalBytes: total };\n } catch {\n return { entryCount: 0, totalBytes: 0 };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Eviction\n// ---------------------------------------------------------------------------\n\nfunction isQuotaExceeded(err: unknown): boolean {\n if (!err || typeof err !== \"object\") return false;\n const e = err as { name?: string; code?: number };\n return e.name === \"QuotaExceededError\" || e.code === 22;\n}\n\n/**\n * Evict entries (size-aware-LRU) for `userId` until total bytes ≤ target.\n *\n * Strategy: entries are sorted by `(bytes >= SIZE_FLOOR_BYTES, lastAccessedAt)`\n * so large stale entries go first; small entries only get evicted once the\n * large pile is gone. Stops when the per-user total drops below `targetBytes`.\n */\nasync function evictToBudget(\n db: BlobCacheDb,\n userId: string,\n targetBytes: number,\n): Promise<void> {\n const entries = await db.blobs\n .where(\"userId\")\n .equals(userId)\n .toArray();\n const ordered = entries.sort((a, b) => {\n const aBig = a.bytes >= SIZE_FLOOR_BYTES ? 0 : 1;\n const bBig = b.bytes >= SIZE_FLOOR_BYTES ? 0 : 1;\n if (aBig !== bBig) return aBig - bBig;\n return a.lastAccessedAt - b.lastAccessedAt;\n });\n let total = ordered.reduce((sum, e) => sum + e.bytes, 0);\n const toDelete: string[] = [];\n for (const entry of ordered) {\n if (total <= targetBytes) break;\n toDelete.push(entry.key);\n total -= entry.bytes;\n }\n if (toDelete.length > 0) {\n await db.blobs.bulkDelete(toDelete);\n logger.info(\"blob-cache.evicted\", {\n meta: { userId, evicted: toDelete.length, totalAfter: total },\n });\n }\n}\n\n/**\n * Public LRU evict for a given user — exposed so the admin observability\n * panel can offer an \"Evict now\" button.\n */\nexport async function evictForUser(userId: string, targetBytes: number): Promise<void> {\n const db = await openBlobCacheDb();\n if (!db) return;\n await evictToBudget(db, userId, targetBytes);\n}\n"],"mappings":"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","names":[]}
package/dist/speech.cjs CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __esm = (fn, res, err) => function __init() {
7
9
  if (err) throw err[0];
@@ -23,6 +25,14 @@ var __copyProps = (to, from, except, desc) => {
23
25
  }
24
26
  return to;
25
27
  };
28
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
29
+ // If the importer is in node compatibility mode or this is not an ESM
30
+ // file that has been converted to a CommonJS file using a Babel-
31
+ // compatible transform (i.e. "__esModule" has not been set), then set
32
+ // "default" to the CommonJS "module.exports" for node compatibility.
33
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
34
+ mod
35
+ ));
26
36
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
27
37
 
28
38
  // src/speech/configure.ts
@@ -645,7 +655,8 @@ function toNativeVoice(voice) {
645
655
  return { id: voice.id };
646
656
  }
647
657
  async function buildConnectedSocket(token, options) {
648
- const client = new import_client.Cartesia({ token });
658
+ const { Cartesia: CartesiaClient } = await import("@cartesia/cartesia-js/client");
659
+ const client = new CartesiaClient({ token });
649
660
  const native = await client.tts.websocket({
650
661
  headers: { "Cartesia-Version": CARTESIA_API_VERSION }
651
662
  });
@@ -712,11 +723,10 @@ async function reportCartesiaConnectFailure(error) {
712
723
  { cause: error }
713
724
  );
714
725
  }
715
- var import_client, ProviderSessionError, CartesiaAudioSource, DEFAULTS, CartesiaV4Socket, BrokerMintFailure;
726
+ var ProviderSessionError, CartesiaAudioSource, DEFAULTS, CartesiaV4Socket, BrokerMintFailure;
716
727
  var init_cartesia_socket = __esm({
717
728
  "src/speech/cartesia-socket.ts"() {
718
729
  "use strict";
719
- import_client = require("@cartesia/cartesia-js/client");
720
730
  init_tts_config();
721
731
  init_configure();
722
732
  ProviderSessionError = class extends Error {