@ai-matrx/media 0.15.4 → 0.15.6
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 +8 -2
- package/dist/files/engine/cache/idb-store.d.ts +2 -1
- package/dist/files/engine/cache/idb-store.js +2 -2
- package/dist/files/engine/cache/idb-store.js.map +1 -1
- package/dist/speech.cjs +13 -3
- package/dist/speech.cjs.map +1 -1
- package/dist/speech.js +2 -2
- package/dist/speech.js.map +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
# @ai-matrx/media
|
|
2
2
|
|
|
3
|
-
## 0.15.
|
|
3
|
+
## 0.15.6
|
|
4
4
|
|
|
5
5
|
Automatic changed-only republish (docs/metadata drift since the last tag — see
|
|
6
|
-
`git diff npm/media/v0.15.
|
|
6
|
+
`git diff npm/media/v0.15.5..npm/media/v0.15.6 -- 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
|
|
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
|
|
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
|
|
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
|
|
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 {
|