diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a1dd9c..92975a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## [4.4.2] + +### Changed + +- Caching is disabled for all HTTP request made by the SDK +- Change how the WebSocket libraries are imported for better compatibility across frameworks and runtimes. + The library no longer relies on a internal `#ws` import, and instead compiles the imports into the dist bundles. + Browser builds will use the native `WebSocket`, other builds will use the `ws` package. + ## [4.4.1] - 2024-04-16 ### Changed diff --git a/package.json b/package.json index d1820b7..18b5bc2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "assemblyai", - "version": "4.4.1", + "version": "4.4.2", "description": "The AssemblyAI JavaScript SDK provides an easy-to-use interface for interacting with the AssemblyAI API, which supports async and real-time transcription, as well as the latest LeMUR models.", "engines": { "node": ">=18" @@ -17,7 +17,7 @@ "default": "./dist/deno.mjs" }, "workerd": "./dist/index.mjs", - "browser": "./dist/index.mjs", + "browser": "./dist/browser.mjs", "node": { "types": "./dist/index.d.ts", "import": "./dist/node.mjs", @@ -40,14 +40,10 @@ "node": "./src/polyfills/streams/node.ts", "default": "./src/polyfills/streams/index.ts" }, - "#ws": { - "types": "./src/polyfills/ws/index.d.ts", - "browser": "./src/polyfills/ws/browser.mjs", - "default": { - "types": "./src/polyfills/ws/index.d.ts", - "import": "./src/polyfills/ws/index.mjs", - "require": "./src/polyfills/ws/index.cjs" - } + "#websocket": { + "browser": "./src/polyfills/websocket/browser.ts", + "node": "./src/polyfills/websocket/default.ts", + "default": "./src/polyfills/websocket/default.ts" } }, "type": "commonjs", @@ -70,7 +66,7 @@ "clean": "rimraf dist/* && rimraf temp/* && rimraf temp-docs/*", "lint": "eslint -c .eslintrc.json '{src,tests}/**/*.{js,ts}' && publint && tsc --noEmit -p tsconfig.json", "test": "pnpm run test:unit && pnpm run test:integration", - "test:unit": "jest --config jest.unit.config.js", + "test:unit": "jest --config jest.unit.config.js --testTimeout 1000", "test:integration": "jest --config jest.integration.config.js --testTimeout 360000", "format": "prettier '**/*' --write", "generate:types": "tsx ./scripts/generate-types.ts && prettier 'src/types/*.generated.ts' --write", diff --git a/rollup.config.js b/rollup.config.js index 403fb8d..1b5c89d 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -21,7 +21,7 @@ module.exports = [ // so we compile to es2015 for maximum compatibility. ts({ compilerOptions: { target: "ES2015" } }), ], - external: ["#ws"], + external: ["ws"], output: [ { file: `./dist/index.mjs`, @@ -38,7 +38,7 @@ module.exports = [ { input: "src/index.ts", plugins: [ts({ compilerOptions: { customConditions: ["node"] } })], - external: ["fs", "stream", "stream/web", "#ws"], + external: ["fs", "stream", "stream/web", "ws"], output: [ { file: `./dist/node.mjs`, @@ -55,7 +55,7 @@ module.exports = [ { input: "src/index.ts", plugins: [ts({ compilerOptions: { customConditions: ["deno"] } })], - external: ["#ws"], + external: ["ws"], output: [ { file: `./dist/deno.mjs`, @@ -67,7 +67,7 @@ module.exports = [ { input: "src/index.ts", plugins: [ts({ compilerOptions: { customConditions: ["bun"] } })], - external: ["#ws"], + external: ["ws"], output: [ { file: `./dist/bun.mjs`, @@ -76,6 +76,18 @@ module.exports = [ }, ], }, + // Browser ESM build + { + input: "src/index.ts", + plugins: [ts({ compilerOptions: { customConditions: ["browser"] } })], + output: [ + { + file: `./dist/browser.mjs`, + format: "es", + exports: "named", + }, + ], + }, // Browser UMD build to reference directly in the browser. { ...umdConfig, diff --git a/src/polyfills/websocket/browser.ts b/src/polyfills/websocket/browser.ts new file mode 100644 index 0000000..d1193dc --- /dev/null +++ b/src/polyfills/websocket/browser.ts @@ -0,0 +1,18 @@ +import { PolyfillWebSocketFactory, PolyfillWebSocket } from "."; +export { PolyfillWebSocket } from "."; + +const PolyfillWebSocket = + WebSocket ?? global?.WebSocket ?? window?.WebSocket ?? self?.WebSocket; + +export const factory: PolyfillWebSocketFactory = ( + url: string, + params?: unknown, +) => { + if (params) { + return new PolyfillWebSocket( + url, + params as string | string[], + ) as unknown as PolyfillWebSocket; + } + return new PolyfillWebSocket(url) as unknown as PolyfillWebSocket; +}; diff --git a/src/polyfills/websocket/default.ts b/src/polyfills/websocket/default.ts new file mode 100644 index 0000000..82260fc --- /dev/null +++ b/src/polyfills/websocket/default.ts @@ -0,0 +1,8 @@ +import ws from "ws"; +import { PolyfillWebSocket, PolyfillWebSocketFactory } from "."; +export { PolyfillWebSocket } from "."; + +export const factory: PolyfillWebSocketFactory = ( + url: string, + params?: unknown, +) => new ws(url, params as ws.ClientOptions) as unknown as PolyfillWebSocket; diff --git a/src/polyfills/websocket/index.ts b/src/polyfills/websocket/index.ts new file mode 100644 index 0000000..611583f --- /dev/null +++ b/src/polyfills/websocket/index.ts @@ -0,0 +1,41 @@ +import ws, { Event, ErrorEvent, CloseEvent, MessageEvent } from "ws"; + +export type PolyfillWebSocket = { + OPEN: typeof ws.OPEN; + binaryType: string; + onopen: ((event: Event) => void) | null; + onerror: ((event: ErrorEvent) => void) | null; + onclose: ((event: CloseEvent) => void) | null; + onmessage: ((event: MessageEvent) => void) | null; + readonly readyState: + | typeof ws.CONNECTING + | typeof ws.OPEN + | typeof ws.CLOSING + | typeof ws.CLOSED; + removeAllListeners?: () => void; + send( + data: + | string + | number + | Buffer + | DataView + | ArrayBufferView + | Uint8Array + | ArrayBuffer + | SharedArrayBuffer + | readonly unknown[] + | readonly number[] + | { valueOf(): ArrayBuffer } + | { valueOf(): SharedArrayBuffer } + | { valueOf(): Uint8Array } + | { valueOf(): readonly number[] } + | { valueOf(): string } + | { [Symbol.toPrimitive](hint: string): string }, + ): unknown; + close(): unknown; +}; + +export type PolyfillWebSocketFactory = ( + url: string, + params?: unknown, +) => PolyfillWebSocket; diff --git a/src/polyfills/ws/browser.mjs b/src/polyfills/ws/browser.mjs deleted file mode 100644 index eb1754f..0000000 --- a/src/polyfills/ws/browser.mjs +++ /dev/null @@ -1,15 +0,0 @@ -var ws = null; - -if (typeof WebSocket !== "undefined") { - ws = WebSocket; -} else if (typeof MozWebSocket !== "undefined") { - ws = MozWebSocket; -} else if (typeof global !== "undefined") { - ws = global.WebSocket || global.MozWebSocket; -} else if (typeof window !== "undefined") { - ws = window.WebSocket || window.MozWebSocket; -} else if (typeof self !== "undefined") { - ws = self.WebSocket || self.MozWebSocket; -} - -export default ws; diff --git a/src/polyfills/ws/index.cjs b/src/polyfills/ws/index.cjs deleted file mode 100644 index 104c2b1..0000000 --- a/src/polyfills/ws/index.cjs +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("ws"); diff --git a/src/polyfills/ws/index.d.ts b/src/polyfills/ws/index.d.ts deleted file mode 100644 index 76226dc..0000000 --- a/src/polyfills/ws/index.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -import ws from "ws"; -export default ws; diff --git a/src/polyfills/ws/index.mjs b/src/polyfills/ws/index.mjs deleted file mode 100644 index 76226dc..0000000 --- a/src/polyfills/ws/index.mjs +++ /dev/null @@ -1,2 +0,0 @@ -import ws from "ws"; -export default ws; diff --git a/src/services/base.ts b/src/services/base.ts index 2f50688..c091068 100644 --- a/src/services/base.ts +++ b/src/services/base.ts @@ -21,6 +21,7 @@ export abstract class BaseService { "Content-Type": "application/json", ...init.headers, }; + init.cache = "no-store"; if (!input.startsWith("http")) input = this.params.baseUrl + input; const response = await fetch(input, init); diff --git a/src/services/files/index.ts b/src/services/files/index.ts index 7c561df..dd5be8f 100644 --- a/src/services/files/index.ts +++ b/src/services/files/index.ts @@ -10,8 +10,13 @@ export class FileService extends BaseService { */ async upload(input: FileUploadParams): Promise { let fileData: FileUploadData; - if (typeof input === "string") fileData = await readFile(input); - else fileData = input; + if (typeof input === "string") { + if (input.startsWith("data:")) { + fileData = dataUrlToBlob(input); + } else { + fileData = await readFile(input); + } + } else fileData = input; const data = await this.fetchJson("/v2/upload", { method: "POST", @@ -24,3 +29,15 @@ export class FileService extends BaseService { return data.upload_url; } } + +function dataUrlToBlob(dataUrl: string) { + const arr = dataUrl.split(","); + const mime = arr[0].match(/:(.*?);/)![1]; + const bstr = atob(arr[1]); + let n = bstr.length; + const u8arr = new Uint8Array(n); + while (n--) { + u8arr[n] = bstr.charCodeAt(n); + } + return new Blob([u8arr], { type: mime }); +} diff --git a/src/services/realtime/service.ts b/src/services/realtime/service.ts index 9227aa5..2bcfdfc 100644 --- a/src/services/realtime/service.ts +++ b/src/services/realtime/service.ts @@ -1,5 +1,8 @@ import { WritableStream } from "#streams"; -import WebSocket from "#ws"; +import { + PolyfillWebSocket, + factory as polyfillWebSocketFactory, +} from "#websocket"; import { ErrorEvent, MessageEvent, CloseEvent } from "ws"; import { RealtimeEvents, @@ -52,7 +55,7 @@ export class RealtimeTranscriber { private endUtteranceSilenceThreshold?: number; private disablePartialTranscripts?: boolean; - private socket?: WebSocket; + private socket?: PolyfillWebSocket; private listeners: RealtimeListeners = {}; private sessionTerminatedResolve?: () => void; @@ -136,15 +139,15 @@ export class RealtimeTranscriber { const url = this.connectionUrl(); if (this.token) { - this.socket = new WebSocket(url.toString()); + this.socket = polyfillWebSocketFactory(url.toString()); } else { - this.socket = new WebSocket(url.toString(), { + this.socket = polyfillWebSocketFactory(url.toString(), { headers: { Authorization: this.apiKey }, }); } - this.socket.binaryType = "arraybuffer"; + this.socket!.binaryType = "arraybuffer"; - this.socket.onopen = () => { + this.socket!.onopen = () => { if ( this.endUtteranceSilenceThreshold === undefined || this.endUtteranceSilenceThreshold === null @@ -156,7 +159,7 @@ export class RealtimeTranscriber { ); }; - this.socket.onclose = ({ code, reason }: CloseEvent) => { + this.socket!.onclose = ({ code, reason }: CloseEvent) => { if (!reason) { if (code in RealtimeErrorType) { reason = RealtimeErrorMessages[code as RealtimeErrorType]; @@ -165,12 +168,12 @@ export class RealtimeTranscriber { this.listeners.close?.(code, reason); }; - this.socket.onerror = (event: ErrorEvent) => { + this.socket!.onerror = (event: ErrorEvent) => { if (event.error) this.listeners.error?.(event.error as Error); else this.listeners.error?.(new Error(event.message)); }; - this.socket.onmessage = ({ data }: MessageEvent) => { + this.socket!.onmessage = ({ data }: MessageEvent) => { const message = JSON.parse(data.toString()) as RealtimeMessage; if ("error" in message) { this.listeners.error?.(new RealtimeError(message.error)); @@ -242,7 +245,7 @@ export class RealtimeTranscriber { } private send(data: BufferLike) { - if (!this.socket || this.socket.readyState !== WebSocket.OPEN) { + if (!this.socket || this.socket.readyState !== this.socket.OPEN) { throw new Error("Socket is not open for communication"); } this.socket.send(data); @@ -250,7 +253,7 @@ export class RealtimeTranscriber { async close(waitForSessionTermination = true) { if (this.socket) { - if (this.socket.readyState === WebSocket.OPEN) { + if (this.socket.readyState === this.socket.OPEN) { if (waitForSessionTermination) { const sessionTerminatedPromise = new Promise((resolve) => { this.sessionTerminatedResolve = resolve; @@ -261,7 +264,7 @@ export class RealtimeTranscriber { this.socket.send(terminateSessionMessage); } } - if ("removeAllListeners" in this.socket) this.socket.removeAllListeners(); + if (this.socket?.removeAllListeners) this.socket.removeAllListeners(); this.socket.close(); } diff --git a/src/services/transcripts/index.ts b/src/services/transcripts/index.ts index 79a8139..e986b73 100644 --- a/src/services/transcripts/index.ts +++ b/src/services/transcripts/index.ts @@ -60,8 +60,12 @@ export class TranscriptService extends BaseService { // audio is local path, upload local file audioUrl = await this.files.upload(path); } else { - // audio is not a local path, assume it's a URL - audioUrl = audio; + if (audio.startsWith("data:")) { + audioUrl = await this.files.upload(audio); + } else { + // audio is not a local path, and not a data-URI, assume it's a normal URL + audioUrl = audio; + } } } else { // audio is of uploadable type diff --git a/src/types/openapi.generated.ts b/src/types/openapi.generated.ts index 150d15e..3789141 100644 --- a/src/types/openapi.generated.ts +++ b/src/types/openapi.generated.ts @@ -789,7 +789,7 @@ export type LemurQuestionAnswerResponse = LemurBaseResponse & { * ```js * { * "transcript_ids": [ - * "64nygnr62k-405c-4ae8-8a6b-d90b40ff3cce" + * "47b95ba5-8889-44d8-bc80-5de38306e582" * ], * "context": "This is an interview about wildfires.", * "final_model": "default", @@ -901,7 +901,6 @@ export type ListTranscriptParams = { }; /** - * Details of the transcript page. * Details of the transcript page. Transcripts are sorted from newest to oldest. The previous URL always points to a page with older transcripts. * @example * ```js @@ -924,12 +923,10 @@ export type PageDetails = { */ limit: number; /** - * The URL to the next page of transcripts * The URL to the next page of transcripts. The next URL always points to a page with newer transcripts. */ next_url: string | null; /** - * The URL to the previous page of transcripts * The URL to the next page of transcripts. The previous URL always points to a page with older transcripts. */ prev_url: string | null; @@ -1234,7 +1231,7 @@ export type SentencesResponse = { export type Sentiment = "POSITIVE" | "NEUTRAL" | "NEGATIVE"; /** - * The result of the sentiment analysis model + * The result of the Sentiment Analysis model * @example * ```js * { @@ -2239,7 +2236,7 @@ export type Transcript = { auto_highlights: boolean; /** * An array of results for the Key Phrases model, if it is enabled. - * See {@link https://www.assemblyai.com/docs/models/key-phrases | Key phrases } for more information. + * See {@link https://www.assemblyai.com/docs/models/key-phrases | Key Phrases } for more information. */ auto_highlights_result?: AutoHighlightsResult | null; /** @@ -2361,7 +2358,7 @@ export type Transcript = { sentiment_analysis?: boolean | null; /** * An array of results for the Sentiment Analysis model, if it is enabled. - * See {@link https://www.assemblyai.com/docs/models/sentiment-analysis | Sentiment analysis } for more information. + * See {@link https://www.assemblyai.com/docs/models/sentiment-analysis | Sentiment Analysis } for more information. */ sentiment_analysis_results?: SentimentAnalysisResult[] | null; /** @@ -2438,7 +2435,7 @@ export type Transcript = { */ webhook_status_code?: number | null; /** - * The URL to which we send webhooks upon trancription completion + * The URL to which we send webhooks upon transcription completion */ webhook_url?: string | null; /** @@ -2497,19 +2494,100 @@ export type TranscriptLanguageCode = | "it" | "pt" | "nl" - | "hi" - | "ja" + | "af" + | "sq" + | "am" + | "ar" + | "hy" + | "as" + | "az" + | "ba" + | "eu" + | "be" + | "bn" + | "bs" + | "br" + | "bg" + | "my" + | "ca" | "zh" + | "hr" + | "cs" + | "da" + | "et" + | "fo" | "fi" + | "gl" + | "ka" + | "el" + | "gu" + | "ht" + | "ha" + | "haw" + | "he" + | "hi" + | "hu" + | "is" + | "id" + | "ja" + | "jw" + | "kn" + | "kk" + | "km" | "ko" + | "lo" + | "la" + | "lv" + | "ln" + | "lt" + | "lb" + | "mk" + | "mg" + | "ms" + | "ml" + | "mt" + | "mi" + | "mr" + | "mn" + | "ne" + | "no" + | "nn" + | "oc" + | "pa" + | "ps" + | "fa" | "pl" + | "ro" | "ru" + | "sa" + | "sr" + | "sn" + | "sd" + | "si" + | "sk" + | "sl" + | "so" + | "su" + | "sw" + | "sv" + | "tl" + | "tg" + | "ta" + | "tt" + | "te" + | "th" + | "bo" | "tr" + | "tk" | "uk" - | "vi"; + | "ur" + | "uz" + | "vi" + | "cy" + | "yi" + | "yo"; /** - * A list of transcripts * A list of transcripts. Transcripts are sorted from newest to oldest. The previous URL always points to a page with older transcripts. * @example * ```js @@ -2649,7 +2727,7 @@ export type TranscriptOptionalParams = { */ auto_chapters?: boolean; /** - * Whether Key Phrases is enabled, either true or false + * Enable Key Phrases, either true or false */ auto_highlights?: boolean; /** @@ -2661,7 +2739,7 @@ export type TranscriptOptionalParams = { */ content_safety?: boolean; /** - * The confidence threshold for content moderation. Values must be between 25 and 100. + * The confidence threshold for the Content Moderation model. Values must be between 25 and 100. */ content_safety_confidence?: number; /** @@ -2669,7 +2747,7 @@ export type TranscriptOptionalParams = { */ custom_spelling?: TranscriptCustomSpelling[]; /** - * Whether custom topics is enabled, either true or false + * Enable custom topics, either true or false */ custom_topics?: boolean; /** @@ -2702,7 +2780,7 @@ export type TranscriptOptionalParams = { */ language_code?: LiteralUnion | null; /** - * Whether {@link https://www.assemblyai.com/docs/models/speech-recognition#automatic-language-detection | Automatic language detection } was enabled in the transcription request, either true or false. + * Enable {@link https://www.assemblyai.com/docs/models/speech-recognition#automatic-language-detection | Automatic language detection }, either true or false. */ language_detection?: boolean; /** @@ -2770,7 +2848,7 @@ export type TranscriptOptionalParams = { */ summary_type?: SummaryType; /** - * The list of custom topics provided, if custom topics is enabled + * The list of custom topics */ topics?: string[]; /** @@ -2784,7 +2862,7 @@ export type TranscriptOptionalParams = { */ webhook_auth_header_value?: string | null; /** - * The URL to which AssemblyAI send webhooks upon trancription completion + * The URL to which AssemblyAI send webhooks upon transcription completion */ webhook_url?: string; /** diff --git a/src/utils/path.ts b/src/utils/path.ts index 46bd11d..632a47c 100644 --- a/src/utils/path.ts +++ b/src/utils/path.ts @@ -1,6 +1,7 @@ export function getPath(path: string) { if (path.startsWith("http")) return null; if (path.startsWith("https")) return null; + if (path.startsWith("data:")) return null; if (path.startsWith("file://")) return path.substring(7); if (path.startsWith("file:")) return path.substring(5); return path; diff --git a/tests/integration/file.test.ts b/tests/integration/file.test.ts index 3f0471c..ab77993 100644 --- a/tests/integration/file.test.ts +++ b/tests/integration/file.test.ts @@ -4,7 +4,7 @@ import path from "path"; import "dotenv/config"; import { AssemblyAI } from "../../src"; -const testDir = process.env["TESTDATA_DIR"] ?? "."; +const testDir = process.env["TESTDATA_DIR"] ?? "tests/static"; const client = new AssemblyAI({ apiKey: process.env.ASSEMBLYAI_API_KEY!, @@ -16,6 +16,15 @@ describe("files", () => { expect(uploadUrl).toBeTruthy(); }); + it("should upload a file from data URI", async () => { + const dataUri = `data:audio/wav;base64,${await readFile( + path.join(testDir, "gore.wav"), + "base64", + )}`; + const uploadUrl = await client.files.upload(dataUri); + expect(uploadUrl).toBeTruthy(); + }); + it("should upload a file from stream", async () => { const stream = createReadStream(path.join(testDir, "gore.wav")); const uploadUrl = await client.files.upload(stream); diff --git a/tests/integration/realtime.test.ts b/tests/integration/realtime.test.ts index efb36d3..2d89e18 100644 --- a/tests/integration/realtime.test.ts +++ b/tests/integration/realtime.test.ts @@ -8,7 +8,7 @@ import { PartialTranscript, } from "../../src"; -const testDir = process.env["TESTDATA_DIR"] ?? "."; +const testDir = process.env["TESTDATA_DIR"] ?? "tests/static"; const client = new AssemblyAI({ apiKey: process.env.ASSEMBLYAI_API_KEY!, }); diff --git a/tests/integration/transcript.test.ts b/tests/integration/transcript.test.ts index 065c0ac..31bb55a 100644 --- a/tests/integration/transcript.test.ts +++ b/tests/integration/transcript.test.ts @@ -1,8 +1,9 @@ import path from "path"; import "dotenv/config"; import { AssemblyAI } from "../../src"; +import { readFile } from "fs/promises"; -const testDir = process.env["TESTDATA_DIR"] ?? "."; +const testDir = process.env["TESTDATA_DIR"] ?? "tests/static"; const remoteAudioUrl = "https://storage.googleapis.com/aai-web-samples/espn-bears.m4a"; const badRemoteAudioURL = @@ -33,6 +34,18 @@ describe("transcript", () => { expect(["processing", "queued"]).toContain(transcript.status); }); + it("submit should create the transcript object from data URI", async () => { + const dataUri = `data:audio/wav;base64,${await readFile( + path.join(testDir, "gore.wav"), + "base64", + )}`; + const transcript = await client.transcripts.submit({ + audio: dataUri, + }); + + expect(["processing", "queued"]).toContain(transcript.status); + }); + it("should get the transcript object", async () => { const transcript = await client.transcripts.get(knownTranscriptId); diff --git a/tests/unit/file.test.ts b/tests/unit/file.test.ts index f8ba0ad..5640c8d 100644 --- a/tests/unit/file.test.ts +++ b/tests/unit/file.test.ts @@ -6,7 +6,7 @@ import { createClient, requestMatches } from "./utils"; fetchMock.enableMocks(); -const testDir = process.env["TESTDATA_DIR"] ?? "."; +const testDir = process.env["TESTDATA_DIR"] ?? "tests/static"; const assembly = createClient(); @@ -27,6 +27,19 @@ describe("files", () => { expect(uploadUrl).toBeTruthy(); }); + it("should upload a file from data URI", async () => { + fetchMock.doMockIf( + requestMatches({ method: "POST", url: "/v2/upload" }), + JSON.stringify({ upload_url: "https://example.com" }), + ); + const dataUri = `data:audio/wav;base64,${await readFile( + path.join(testDir, "gore.wav"), + "base64", + )}`; + const uploadUrl = await assembly.files.upload(dataUri); + expect(uploadUrl).toBeTruthy(); + }); + it("should upload a file from stream", async () => { fetchMock.doMockIf( requestMatches({ method: "POST", url: "/v2/upload" }), diff --git a/tests/unit/transcript.test.ts b/tests/unit/transcript.test.ts index 9a5b105..572b619 100644 --- a/tests/unit/transcript.test.ts +++ b/tests/unit/transcript.test.ts @@ -6,10 +6,11 @@ import { defaultBaseUrl, requestMatches, } from "./utils"; +import { readFile } from "fs/promises"; fetchMock.enableMocks(); -const testDir = process.env["TESTDATA_DIR"] ?? "."; +const testDir = process.env["TESTDATA_DIR"] ?? "tests/static"; const assembly = createClient(); @@ -111,6 +112,26 @@ describe("transcript", () => { expect(["processing", "queued"]).toContain(transcript.status); }); + it("submit should create the transcript object from data URI", async () => { + fetchMock.doMockOnceIf( + requestMatches({ url: "/v2/upload", method: "POST" }), + JSON.stringify({ upload_url: "https://example.com" }), + ); + fetchMock.doMockOnceIf( + requestMatches({ url: "/v2/transcript", method: "POST" }), + JSON.stringify({ id: transcriptId, status: "queued" }), + ); + const dataUri = `data:audio/wav;base64,${await readFile( + path.join(testDir, "gore.wav"), + "base64", + )}`; + const transcript = await assembly.transcripts.submit({ + audio: dataUri, + }); + + expect(["processing", "queued"]).toContain(transcript.status); + }); + it("should get the transcript object", async () => { fetchMock.doMockOnceIf( requestMatches({ url: `/v2/transcript/${transcriptId}`, method: "GET" }), @@ -245,6 +266,7 @@ describe("transcript", () => { expect(fetch).toHaveBeenLastCalledWith( `${defaultBaseUrl}/v2/transcript/${transcriptId}`, { + cache: "no-store", headers: { Authorization: defaultApiKey, "Content-Type": "application/json", @@ -265,6 +287,7 @@ describe("transcript", () => { expect(transcript).toStrictEqual(errorResponse); expect(fetch).toHaveBeenLastCalledWith(`${defaultBaseUrl}/v2/transcript`, { body: JSON.stringify({ audio_url: badRemoteAudioURL }), + cache: "no-store", headers: { Authorization: defaultApiKey, "Content-Type": "application/json", @@ -288,6 +311,7 @@ describe("transcript", () => { expect(transcript).toStrictEqual(errorResponse); expect(fetch).toHaveBeenLastCalledWith(`${defaultBaseUrl}/v2/transcript`, { body: JSON.stringify({ audio_url: badRemoteAudioURL }), + cache: "no-store", headers: { Authorization: defaultApiKey, "Content-Type": "application/json", @@ -310,7 +334,7 @@ describe("transcript", () => { await expect(promise).rejects.toThrow("Polling timeout"); fetchMock.resetMocks(); - }); + }, 5000); it("create should fail to poll", async () => { fetchMock.mockResponse(JSON.stringify({ status: "queued" })); @@ -326,7 +350,7 @@ describe("transcript", () => { await expect(promise).rejects.toThrow("Polling timeout"); fetchMock.resetMocks(); - }); + }, 5000); it("should get paragraphs", async () => { fetchMock.doMockOnceIf(