From ae4e50086d8bf7b5abde203a8a2d4843a4b7f830 Mon Sep 17 00:00:00 2001 From: AssemblyAI Date: Fri, 13 Oct 2023 15:55:44 -0400 Subject: [PATCH] Project import generated by Copybara. GitOrigin-RevId: d60ee5cfa41e1260feb0d60f9d460cd78d7ed049 --- .eslintrc.json | 16 ++++++++++++++-- README.md | 12 ++++++++---- package.json | 8 ++++---- rollup.config.js | 2 +- scripts/kitchensink.ts | 28 +++++++++++++++++----------- src/index.ts | 6 ++++-- src/services/base.ts | 4 +--- src/services/files/index.ts | 4 ++-- src/services/index.ts | 9 +++++---- src/services/lemur/index.ts | 16 ++++++++++++++-- src/services/realtime/service.ts | 16 ++++++++++++++-- src/services/transcripts/index.ts | 27 ++++++++++++++++++++++++--- src/types/services/abstractions.ts | 2 +- src/utils/errors/index.ts | 2 +- src/utils/errors/realtime.ts | 3 +-- tests/__mocks__/api.ts | 23 ++++++++++++++++++++++- tests/__mocks__/axios.ts | 4 ++-- tests/lemur.test.ts | 17 ++++++++++++----- tests/realtime.test.ts | 12 +++++++++++- tests/transcript.test.ts | 14 ++++++++------ 20 files changed, 166 insertions(+), 59 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index d658236..15217c6 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,3 +1,15 @@ { - "extends": "standard" -} \ No newline at end of file + "root": true, + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { "project": ["./tsconfig.json"] }, + "plugins": [ + "@typescript-eslint" + ], + "rules": { + }, + "ignorePatterns": ["test/**/*", "dist/**/*", "node_modules/**/*"] +} diff --git a/README.md b/README.md index e122b6b..3c3a5f3 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,6 @@ [![Discord](https://img.shields.io/discord/875120158014853141?logo=discord&label=Discord&link=https%3A%2F%2Fdiscord.com%2Fchannels%2F875120158014853141&style=social) ](https://discord.gg/5aQNZyq3) - # AssemblyAI Node.js SDK The AssemblyAI Node.js SDK provides an easy-to-use interface for interacting with the AssemblyAI API, @@ -165,13 +164,13 @@ const { response } = await client.lemur.task({ Create the real-time service. ```typescript -const service = client.realtime.createService(); +const rt = client.realtime.createService(); ``` You can also pass in the following options. ```typescript -const service = client.realtime.createService({ +const rt = client.realtime.createService({ realtimeUrl: 'wss://localhost/override', apiKey: process.env.ASSEMBLYAI_API_KEY // The API key passed to `AssemblyAI` will be used by default, sampleRate: 16_000, @@ -209,7 +208,7 @@ After configuring your events, connect to the server. await rt.connect(); ``` -Send audio data. +Send audio data via chunks. ```typescript // Pseudo code for getting audio @@ -217,6 +216,11 @@ getAudio((chunk) => { rt.sendAudio(chunk); }); ``` +Or send audio data via a stream by piping to the realtime stream. + +```typescript +audioStream.pipe(rt.stream()); +``` Close the connection when you're finished. diff --git a/package.json b/package.json index 7e8ca48..3a5f7d7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "assemblyai", - "version": "2.0.1", + "version": "2.0.2", "description": "The AssemblyAI Node.js 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.", "main": "dist/index.js", "module": "dist/index.esm.js", @@ -18,7 +18,7 @@ "scripts": { "build": "pnpm clean && pnpm rollup -c", "clean": "rimraf dist", - "lint": "tslint -p tsconfig.json", + "lint": "eslint -c .eslintrc.json 'src/**/*'", "test": "pnpm lint && pnpm test:unit", "test:unit": "jest --config jest.config.rollup.ts", "format": "prettier --write 'src/**/*.ts'", @@ -45,8 +45,9 @@ "@types/jest": "^29.5.5", "@types/node": "^20.5.7", "@types/ws": "^8.5.5", + "@typescript-eslint/eslint-plugin": "^6.7.5", "dotenv": "^16.3.1", - "eslint": "^8.43.0", + "eslint": "^8.48.0", "i": "^0.3.7", "jest": "^29.5.0", "jest-cli": "^29.5.0", @@ -63,7 +64,6 @@ "ts-jest": "^29.1.0", "ts-node": "^10.9.1", "tslib": "^2.5.3", - "tslint": "^6.1.3", "typescript": "^5.2.2" }, "dependencies": { diff --git a/rollup.config.js b/rollup.config.js index 0060484..c03b6b9 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -15,6 +15,6 @@ module.exports = [ { file: pkg.module, format: 'es' } ], plugins, - external: ['axios', 'fs/promises', 'ws'] + external: ['axios', 'fs/promises', 'stream', 'ws'] }, ] diff --git a/scripts/kitchensink.ts b/scripts/kitchensink.ts index e377413..b96d787 100644 --- a/scripts/kitchensink.ts +++ b/scripts/kitchensink.ts @@ -1,7 +1,7 @@ import { createReadStream } from 'fs' import 'dotenv/config' import AssemblyAI, { Transcript, CreateTranscriptParameters } from '../src/index'; -import { FinalTranscript, PartialTranscript, RealtimeTranscript } from '../src/types' +import { FinalTranscript, LemurBaseResponse, PartialTranscript, RealtimeTranscript } from '../src/types' const client = new AssemblyAI({ apiKey: process.env.ASSEMBLYAI_API_KEY || '', @@ -52,7 +52,7 @@ const client = new AssemblyAI({ })(); const audioUrl = 'https://storage.googleapis.com/aai-docs-samples/espn.m4a'; -const createTranscriptParams: CreateTranscriptParameters = { +const createTranscriptParams: CreateTranscriptParameters = { audio_url: audioUrl, boost_param: 'high', word_boost: ['Chicago', 'draft'], @@ -79,10 +79,10 @@ const createTranscriptParams: CreateTranscriptParameters = { (async function runLemurModels() { const transcript = await client.transcripts.create(createTranscriptParams); - await lemurSummary(transcript); - await lemurQuestionAnswer(transcript); - await lemurActionPoints(transcript); - await lemurCustomTask(transcript); + await lemurSummary(transcript).then(purgeLemurRequestData); + await lemurQuestionAnswer(transcript).then(purgeLemurRequestData); + await lemurActionPoints(transcript).then(purgeLemurRequestData); + await lemurCustomTask(transcript).then(purgeLemurRequestData); await deleteTranscript(transcript); })(); @@ -255,11 +255,8 @@ const createTranscriptParams: CreateTranscriptParameters = { })(); async function searchTranscript(transcript: Transcript) { - console.error('Search is not yet implemented'); - // const result = await client.transcripts.search(transcript.id, { - // words: ['draft', 'football'] - // }); - // console.log(result); + const result = await client.transcripts.wordSearch(transcript.id, ['draft', 'football']); + console.log(result); } async function exportAsSubtitles(transcript: Transcript) { @@ -294,6 +291,7 @@ async function lemurSummary(transcript: Transcript) { answer_format: 'bullet points' }) console.log(response.response); + return response; } async function lemurQuestionAnswer(transcript: Transcript) { @@ -316,6 +314,7 @@ async function lemurQuestionAnswer(transcript: Transcript) { max_output_size: 3000 }) console.log(response.response); + return response; } async function lemurActionPoints(transcript: Transcript) { @@ -326,6 +325,7 @@ async function lemurActionPoints(transcript: Transcript) { max_output_size: 3000 }) console.log(response.response); + return response; } async function lemurCustomTask(transcript: Transcript) { @@ -337,4 +337,10 @@ async function lemurCustomTask(transcript: Transcript) { max_output_size: 3000 }) console.log(response.response); + return response; } + +async function purgeLemurRequestData(lemurResponse: LemurBaseResponse) { + const { response } = await client.lemur.purgeRequestData(lemurResponse.request_id); + console.log(response); +}; diff --git a/src/index.ts b/src/index.ts index 38b7dc1..a5668ca 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,7 @@ -import AssemblyAI from "./services"; - +import * as services from "./services"; +import { AssemblyAI } from "./services"; export * from "./services"; export type * from "./types"; export default AssemblyAI; +class AssemblyAIExports extends AssemblyAI {} +module.exports = Object.assign(AssemblyAIExports, services); diff --git a/src/services/base.ts b/src/services/base.ts index 484ff93..0f88411 100644 --- a/src/services/base.ts +++ b/src/services/base.ts @@ -3,12 +3,10 @@ import { AxiosInstance } from "axios"; /** * Base class for services that communicate with the API. */ -abstract class BaseService { +export abstract class BaseService { /** * Create a new service. * @param params The AxiosInstance to send HTTP requests to the API. */ constructor(protected client: AxiosInstance) {} } - -export default BaseService; diff --git a/src/services/files/index.ts b/src/services/files/index.ts index b15f137..260e384 100644 --- a/src/services/files/index.ts +++ b/src/services/files/index.ts @@ -1,8 +1,8 @@ import { readFile } from "fs/promises"; -import BaseService from "@/services/base"; +import { BaseService } from "@/services/base"; import { UploadedFile } from "@/types"; -export default class FileService extends BaseService { +export class FileService extends BaseService { /** * Upload a local file to AssemblyAI. * @param path The local file to upload. diff --git a/src/services/index.ts b/src/services/index.ts index f296e81..4abcaaa 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -1,11 +1,11 @@ import { createAxiosClient } from "@/utils/axios"; import { BaseServiceParams } from "@/types"; -import LemurService from "./lemur"; +import { LemurService } from "./lemur"; import { RealtimeService, RealtimeServiceFactory } from "./realtime"; -import TranscriptService from "./transcripts"; -import FileService from "./files"; +import { TranscriptService } from "./transcripts"; +import { FileService } from "./files"; -export default class AssemblyAI { +class AssemblyAI { /** * The files service. */ @@ -41,6 +41,7 @@ export default class AssemblyAI { } export { + AssemblyAI, LemurService, RealtimeServiceFactory, RealtimeService, diff --git a/src/services/lemur/index.ts b/src/services/lemur/index.ts index 3aabea9..593ddf4 100644 --- a/src/services/lemur/index.ts +++ b/src/services/lemur/index.ts @@ -7,10 +7,11 @@ import { LemurQuestionAnswerResponse, LemurActionItemsResponse, LemurTaskResponse, + PurgeLemurRequestDataResponse, } from "@/types"; -import BaseService from "@/services/base"; +import { BaseService } from "@/services/base"; -export default class LemurService extends BaseService { +export class LemurService extends BaseService { async summary(params: LemurSummaryParameters): Promise { const { data } = await this.client.post( "/lemur/v3/generate/summary", @@ -46,4 +47,15 @@ export default class LemurService extends BaseService { ); return data; } + + /** + * Delete the data for a previously submitted LeMUR request. + * @param id ID of the LeMUR request + */ + async purgeRequestData(id: string): Promise { + const { data } = await this.client.delete( + `/lemur/v3/${id}` + ); + return data; + } } diff --git a/src/services/realtime/service.ts b/src/services/realtime/service.ts index f89d8ab..d85a7d2 100644 --- a/src/services/realtime/service.ts +++ b/src/services/realtime/service.ts @@ -14,6 +14,7 @@ import { RealtimeErrorMessages, RealtimeErrorType, } from "@/utils/errors"; +import { Writable } from "stream"; const defaultRealtimeUrl = "wss://api.assemblyai.com/v2/realtime/ws"; @@ -75,12 +76,13 @@ export class RealtimeService { ): void; on(event: "error", listener: (error: Error) => void): void; on(event: "close", listener: (code: number, reason: string) => void): void; + // eslint-disable-next-line @typescript-eslint/no-explicit-any on(event: RealtimeEvents, listener: (...args: any[]) => void) { this.listeners[event] = listener; } connect() { - return new Promise((resolve, _) => { + return new Promise((resolve) => { if (this.socket) { throw new Error("Already connected"); } @@ -160,12 +162,22 @@ export class RealtimeService { this.socket.send(JSON.stringify(payload)); } + stream(): Writable { + const stream = new Writable({ + write: (chunk: Buffer, encoding, next) => { + this.sendAudio(chunk); + next(); + }, + }); + return stream; + } + async close(waitForSessionTermination = true) { if (this.socket) { if (this.socket.readyState === WebSocket.OPEN) { const terminateSessionMessage = `{"terminate_session": true}`; if (waitForSessionTermination) { - const sessionTerminatedPromise = new Promise((resolve, _) => { + const sessionTerminatedPromise = new Promise((resolve) => { this.sessionTerminatedResolve = resolve; }); this.socket.send(terminateSessionMessage); diff --git a/src/services/transcripts/index.ts b/src/services/transcripts/index.ts index af123a7..a7c4ef0 100644 --- a/src/services/transcripts/index.ts +++ b/src/services/transcripts/index.ts @@ -1,4 +1,4 @@ -import BaseService from "@/services/base"; +import { BaseService } from "@/services/base"; import { ParagraphsResponse, SentencesResponse, @@ -12,11 +12,12 @@ import { Retrieveable, SubtitleFormat, RedactedAudioResponse, + WordSearchResponse, } from "@/types"; import { AxiosInstance } from "axios"; -import FileService from "../files"; +import { FileService } from "../files"; -export default class TranscriptService +export class TranscriptService extends BaseService implements Createable, @@ -58,6 +59,7 @@ export default class TranscriptService options?: CreateTranscriptOptions ): Promise { const startTime = Date.now(); + // eslint-disable-next-line no-constant-condition while (true) { const transcript = await this.get(transcriptId); if (transcript.status === "completed" || transcript.status === "error") { @@ -115,6 +117,25 @@ export default class TranscriptService return res.data; } + /** + * Search through the transcript for a specific set of keywords. + * You can search for individual words, numbers, or phrases containing up to five words or numbers. + * @param id The identifier of the transcript. + * @param id Keywords to search for. + * @return A promise that resolves to the sentences. + */ + async wordSearch(id: string, words: string[]): Promise { + const { data } = await this.client.get( + `/v2/transcript/${id}/word-search`, + { + params: { + words: JSON.stringify(words), + }, + } + ); + return data; + } + /** * Retrieve all sentences of a transcript. * @param id The identifier of the transcript. diff --git a/src/types/services/abstractions.ts b/src/types/services/abstractions.ts index 31dcba8..b9fd058 100644 --- a/src/types/services/abstractions.ts +++ b/src/types/services/abstractions.ts @@ -3,7 +3,7 @@ * @template T The type of the resource. * @template Parameters The type of the parameters required to create the resource. */ -interface Createable> { +interface Createable> { /** * Create a new resource. * @param params The parameters of the new resource. diff --git a/src/utils/errors/index.ts b/src/utils/errors/index.ts index 5ebd77c..7624b66 100644 --- a/src/utils/errors/index.ts +++ b/src/utils/errors/index.ts @@ -1,5 +1,5 @@ export { - default as RealtimeError, + RealtimeError, RealtimeErrorType, RealtimeErrorMessages, } from "./realtime"; diff --git a/src/utils/errors/realtime.ts b/src/utils/errors/realtime.ts index e5c2f91..3377c47 100644 --- a/src/utils/errors/realtime.ts +++ b/src/utils/errors/realtime.ts @@ -41,5 +41,4 @@ const RealtimeErrorMessages: Record = { class RealtimeError extends Error {} -export { RealtimeErrorType, RealtimeErrorMessages }; -export default RealtimeError; +export { RealtimeError, RealtimeErrorType, RealtimeErrorMessages }; diff --git a/tests/__mocks__/api.ts b/tests/__mocks__/api.ts index 25cc273..d1cf8a5 100644 --- a/tests/__mocks__/api.ts +++ b/tests/__mocks__/api.ts @@ -5,9 +5,11 @@ import type { LemurBaseParameters } from '../../src/' export const knownTranscriptIds = ['123'] +export const knownLemurRequestId = 'request_id' +export const purgeRequestId = 'deletion_request_id' const lemurResponse = { response: 'some response', - requestId: 'request_id', + requestId: knownLemurRequestId, } const withTranscriptId = ( @@ -102,4 +104,23 @@ export const get = () => ({ }, ]), ), + // word search + ...Object.fromEntries( + knownTranscriptIds.map((id) => [ + [`/v2/transcript/${id}/word-search`], + { + id: id, + total_count: 1, + matches: [{}] + }, + ]), + ), }) + +export const deleteMethod = () => ({ + '/lemur/v3/request_id': { + deleted: true, + request_id: purgeRequestId, + request_id_to_purge: knownLemurRequestId + } +}); diff --git a/tests/__mocks__/axios.ts b/tests/__mocks__/axios.ts index 8039a0c..2b7a082 100644 --- a/tests/__mocks__/axios.ts +++ b/tests/__mocks__/axios.ts @@ -1,7 +1,6 @@ import * as axiosImport from 'axios' import type { LemurBaseParameters } from '../../src' -import { get, post } from './api' -import { type } from 'os'; +import { get, post, deleteMethod } from './api' const axios = jest.createMockFromModule('axios') as jest.Mocked< typeof axiosImport.default @@ -26,6 +25,7 @@ const useKnownEndpoints = axios.post.mockImplementation(useKnownEndpoints(post)) axios.get.mockImplementation(useKnownEndpoints(get)) +axios.delete.mockImplementation(useKnownEndpoints(deleteMethod)) export const withData = (data: unknown) => ({ data }) export const isAxiosError = axiosImport.isAxiosError diff --git a/tests/lemur.test.ts b/tests/lemur.test.ts index e4e0777..b70566b 100644 --- a/tests/lemur.test.ts +++ b/tests/lemur.test.ts @@ -1,4 +1,4 @@ -import { knownTranscriptIds } from './__mocks__/api' +import { knownTranscriptIds, knownLemurRequestId, purgeRequestId } from './__mocks__/api' import AssemblyAI from '../src' const assembly = new AssemblyAI({ @@ -14,7 +14,7 @@ describe('lemur', () => { }) expect(response).toBeTruthy() - }, 15_000) + }) it('should generate an answer', async () => { const { response } = await assembly.lemur.questionAnswer({ @@ -30,7 +30,7 @@ describe('lemur', () => { expect(response).toBeTruthy() expect(response).toHaveLength(1) - }, 15_000) + }) it('should generate action items', async () => { const { response } = await assembly.lemur.actionItems({ @@ -39,7 +39,7 @@ describe('lemur', () => { }) expect(response).toBeTruthy() - }, 15_000) + }) it('should generate a task', async () => { const { response } = await assembly.lemur.task({ @@ -49,7 +49,7 @@ describe('lemur', () => { }) expect(response).toBeTruthy() - }, 15_000) + }) it('should fail to generate a summary', async () => { const promise = assembly.lemur.summary({ @@ -62,4 +62,11 @@ describe('lemur', () => { 'each transcript source id must be valid', ) }) + + it('should purge request data', async () => { + const deletionRequest = await assembly.lemur.purgeRequestData(knownLemurRequestId) + expect(deletionRequest.deleted).toBeTruthy(); + expect(deletionRequest.request_id_to_purge).toBe(knownLemurRequestId); + expect(deletionRequest.request_id).toBe(purgeRequestId); + }) }) diff --git a/tests/realtime.test.ts b/tests/realtime.test.ts index 81f27f5..e470902 100644 --- a/tests/realtime.test.ts +++ b/tests/realtime.test.ts @@ -1,9 +1,11 @@ import WS from "jest-websocket-mock"; import AssemblyAI, { RealtimeService } from '../src' -import RealtimeError, { +import { + RealtimeError, RealtimeErrorType, RealtimeErrorMessages } from '../src/utils/errors/realtime' +import stream from "stream"; const apiKey = '123'; const token = '123'; @@ -129,6 +131,14 @@ describe('realtime', () => { .toReceiveMessage(JSON.stringify({ audio_data: Buffer.from(data).toString('base64') })); }) + it('can send audio using stream', async () => { + const writeStream = new stream.PassThrough() + writeStream.pipe(rt.stream()) + writeStream.write(Buffer.alloc(5_000)) + await expect(server) + .toReceiveMessage(JSON.stringify({ audio_data: Buffer.alloc(5_000).toString('base64') })); + }) + it('can receive transcript', () => { const data = { created: '2023-09-14T03:37:11.516967', diff --git a/tests/transcript.test.ts b/tests/transcript.test.ts index 1030823..f65feb2 100644 --- a/tests/transcript.test.ts +++ b/tests/transcript.test.ts @@ -116,9 +116,7 @@ describe('failures', () => { await expect(promise).rejects.toThrow('Polling timeout') }) -}) -describe('segments', () => { it('should get paragraphs', async () => { const segment = await assembly.transcripts.paragraphs(transcriptId) @@ -132,9 +130,7 @@ describe('segments', () => { expect(segment.sentences).toBeInstanceOf(Array) expect(segment.sentences.length).toBeGreaterThan(0) }) -}) -describe('subtitles', () => { it('should get srt subtitles', async () => { const subtitle = await assembly.transcripts.subtitles(transcriptId, 'srt') @@ -146,9 +142,7 @@ describe('subtitles', () => { expect(subtitle).toBeTruthy() }) -}) -describe('redactions', () => { it('should create a redactable transcript object', async () => { const transcript = await assembly.transcripts.create( { @@ -173,4 +167,12 @@ describe('redactions', () => { expect(res.status).toBe('redacted_audio_ready') expect(res.redacted_audio_url).toBeTruthy() }) + + it('should word search', async () => { + const res = await assembly.transcripts.wordSearch(transcriptId, ['bears']) + + expect(res.id).toBe(transcriptId) + expect(res.total_count).toBe(1) + expect(res.matches).toBeInstanceOf(Array) + }) })