├── .env.example ├── .gitignore ├── .husky └── pre-commit ├── .prettierignore ├── .prettierrc.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── global.d.ts ├── package-lock.json ├── package.json ├── src ├── context.ts ├── resemble.ts └── v2 │ ├── StreamDecoder.ts │ ├── batch.ts │ ├── clips.ts │ ├── edits.ts │ ├── phonemes.ts │ ├── projects.ts │ ├── recordings.ts │ ├── termSubstitutions.ts │ ├── util.ts │ └── voices.ts └── test ├── api.test.js ├── batch.test.js ├── phonemes.test.js ├── sample_audio.wav ├── term_substitutions.test.js └── testSetup.js /.env.example: -------------------------------------------------------------------------------- 1 | TEST_SYN_SERVER_URL= streaming url, note that we already suffix the url with /stream in the sdk. remove that suffix here 2 | TEST_API_KEY=YOUR_API_KEY 3 | TEST_VOICE_UUID=9abcdefd 4 | TEST_PROJECT_UUID=cddddddd 5 | TEST_CALLBACK_URL=https://webhook.site -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | dist 3 | node_modules 4 | .vscode 5 | .idea 6 | .env 7 | 8 | # Don't accidently check-in sound files! 9 | *.mp4 10 | *.ogg 11 | *.mp3 12 | *.wav 13 | *.flac 14 | *.m4a 15 | *.webm 16 | 17 | # Specify a sound file you'd like to check in directly like this: 18 | # !/spec/test_audio.wav 19 | !sample_audio.wav 20 | 21 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | dist -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": false, 4 | "tabWidth": 2, 5 | "useTabs": false, 6 | "trailingComma": "all" 7 | } 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## v2.0.3 - 05-23-2023 6 | 7 | ### Added 8 | 9 | - Updated the Voice creation function to accept a `consent` audio file in accordance with the requirements for the API 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2020 Resemble AI (https://resemble.ai) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # resemble.ai API 2 | 3 | [resemble.ai](https://resemble.ai) is a state-of-the-art natural voice cloning and synthesis provider. Best of all, the platform is accessible by using our public API! Sign up [here](https://app.resemble.ai) to get an API token! 4 | 5 | This repository hosts a NodeJS library for convenient usage of the [Resemble API](https://docs.resemble.ai). 6 | 7 | ## Quick Start 8 | 9 | ```sh 10 | npm install @resemble/node 11 | # or 12 | yarn add @resemble/node 13 | ``` 14 | 15 | See documentation at [docs.resemble.ai](docs.resemble.ai). 16 | 17 | ## Features 18 | 19 | - Typescript definitions 20 | - Works with NodeJS, Deno, and the browser! 21 | - Supports the V2 API 22 | 23 | ## Test! 24 | 25 | ``` 26 | npm run test 27 | ``` 28 | 29 | ## Publishing 30 | 31 | 1. `git status`: Make sure your working directory has no pending changes. 32 | 2. Update the version key in `package.json` 33 | 3. `git commit`: Commit this version change. 34 | 4. Publish to npmjs.org: 35 | 36 | ```sh 37 | npm run build 38 | npm publish 39 | ``` 40 | -------------------------------------------------------------------------------- /global.d.ts: -------------------------------------------------------------------------------- 1 | import 'jest-extended' 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@resemble/node", 3 | "description": "Resemble API", 4 | "version": "3.4.2", 5 | "type": "module", 6 | "source": "src/resemble.ts", 7 | "exports": { 8 | "types": "./dist/resemble.d.ts", 9 | "require": "./dist/resemble.cjs", 10 | "default": "./dist/resemble.modern.js" 11 | }, 12 | "main": "./dist/resemble.cjs", 13 | "module": "./dist/resemble.module.js", 14 | "unpkg": "./dist/resemble.umd.js", 15 | "types": "./dist/resemble.d.ts", 16 | "scripts": { 17 | "build": "microbundle", 18 | "test": "jest", 19 | "prepare": "husky install", 20 | "format": "prettier --config .prettierrc.json --ignore-path .prettierignore --check .", 21 | "format:fix": "prettier --config .prettierrc.json --ignore-path .prettierignore --write ." 22 | }, 23 | "devDependencies": { 24 | "@types/jest": "^28.1.6", 25 | "husky": "^8.0.3", 26 | "jest": "^28.1.3", 27 | "jest-extended": "^4.0.2", 28 | "lint-staged": "^14.0.1", 29 | "microbundle": "^0.15.0", 30 | "prettier": "3.0.3" 31 | }, 32 | "dependencies": { 33 | "dotenv": "^16.3.1", 34 | "isomorphic-fetch": "^3.0.0", 35 | "isomorphic-form-data": "^2.0.0" 36 | }, 37 | "lint-staged": { 38 | "**/*": "prettier --write --ignore-unknown" 39 | }, 40 | "jest": { 41 | "setupFilesAfterEnv": [ 42 | "./test/testSetup.js" 43 | ] 44 | }, 45 | "files": [ 46 | "dist" 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /src/context.ts: -------------------------------------------------------------------------------- 1 | let apiKey: string | undefined = '' 2 | let baseUrl: string | undefined = 'https://app.resemble.ai/api/' 3 | let synthesisServerUrl: string | undefined = '' 4 | 5 | const synthesisServerHeaders: Record = { 6 | 'Content-Type': 'application/json', 7 | 'x-access-token': apiKey, 8 | } 9 | 10 | const headers: Record = { 11 | 'Content-Type': 'application/json', 12 | Authorization: `Token token=${apiKey}`, 13 | } 14 | 15 | export const context = { 16 | headers: () => headers, 17 | synthesisServerHeaders: () => synthesisServerHeaders, 18 | 19 | setBaseUrl: (url: string) => { 20 | baseUrl = url 21 | 22 | if (!url.endsWith('/')) { 23 | baseUrl += '/' 24 | } 25 | }, 26 | 27 | setApiKey: (key: string) => { 28 | apiKey = key 29 | headers['Authorization'] = `Token token=${key}` 30 | synthesisServerHeaders['x-access-token'] = key 31 | }, 32 | 33 | setSynthesisUrl: (url: string) => { 34 | synthesisServerUrl = url 35 | 36 | if (!url.endsWith('/')) { 37 | synthesisServerUrl += '/' 38 | } 39 | }, 40 | 41 | endpoint: (version: string, endpoint: string): string => { 42 | let ending = endpoint.startsWith('/') ? endpoint.substring(1) : endpoint 43 | return `${baseUrl}${version}/${ending}` 44 | }, 45 | 46 | synServerUrl: (endpoint) => { 47 | let ending = endpoint.startsWith('/') ? endpoint.substring(1) : endpoint 48 | const url = `${synthesisServerUrl}${ending}` 49 | return url 50 | }, 51 | } 52 | -------------------------------------------------------------------------------- /src/resemble.ts: -------------------------------------------------------------------------------- 1 | import { context } from './context' 2 | import ProjectsV2 from './v2/projects' 3 | import ClipsV2 from './v2/clips' 4 | import RecordingsV2 from './v2/recordings' 5 | import VoicesV2 from './v2/voices' 6 | import BatchV2 from './v2/batch' 7 | import PhonemesV2 from './v2/phonemes' 8 | import TermSubstitutionsV2 from './v2/termSubstitutions' 9 | import EditV2 from './v2/edits' 10 | 11 | export const Resemble = { 12 | setApiKey: context.setApiKey, 13 | setBaseUrl: context.setBaseUrl, 14 | setSynthesisUrl: context.setSynthesisUrl, 15 | 16 | v2: { 17 | projects: { 18 | all: ProjectsV2.all, 19 | create: ProjectsV2.create, 20 | update: ProjectsV2.update, 21 | get: ProjectsV2.get, 22 | delete: ProjectsV2.destroy, 23 | }, 24 | clips: { 25 | all: ClipsV2.all, 26 | createSync: ClipsV2.createSync, 27 | createAsync: ClipsV2.createAsync, 28 | createDirect: ClipsV2.createDirect, 29 | updateAsync: ClipsV2.updateAsync, 30 | stream: ClipsV2.stream, 31 | get: ClipsV2.get, 32 | delete: ClipsV2.destroy, 33 | }, 34 | voices: { 35 | all: VoicesV2.all, 36 | create: VoicesV2.create, 37 | update: VoicesV2.update, 38 | build: VoicesV2.build, 39 | get: VoicesV2.get, 40 | delete: VoicesV2.destroy, 41 | }, 42 | recordings: { 43 | all: RecordingsV2.all, 44 | get: RecordingsV2.get, 45 | create: RecordingsV2.create, 46 | update: RecordingsV2.update, 47 | delete: RecordingsV2.destroy, 48 | }, 49 | batch: { 50 | all: BatchV2.all, 51 | get: BatchV2.get, 52 | create: BatchV2.create, 53 | delete: BatchV2.delete, 54 | }, 55 | phonemes: { 56 | all: PhonemesV2.all, 57 | create: PhonemesV2.create, 58 | get: PhonemesV2.get, 59 | delete: PhonemesV2.delete, 60 | }, 61 | termSubstitutions: { 62 | all: TermSubstitutionsV2.all, 63 | get: TermSubstitutionsV2.get, 64 | create: TermSubstitutionsV2.create, 65 | delete: TermSubstitutionsV2.delete, 66 | }, 67 | edits: { 68 | all: EditV2.all, 69 | get: EditV2.get, 70 | create: EditV2.create, 71 | }, 72 | }, 73 | } 74 | -------------------------------------------------------------------------------- /src/v2/StreamDecoder.ts: -------------------------------------------------------------------------------- 1 | import { concatUint8Arrays } from './util' 2 | 3 | export const DEFAULT_BUFFER_SIZE = 4 * 1024 4 | export const STREAMING_WAV_HEADER_BUFFER_LEN = 36 5 | 6 | export const StreamDecoder = function ( 7 | bufferSize = DEFAULT_BUFFER_SIZE, 8 | ignoreWavHeader = true, 9 | timeStampsProcessingRequired = false, 10 | ) { 11 | if (bufferSize < 2) throw new Error('Buffer size cannot be less than 2') 12 | if (bufferSize % 2 !== 0) 13 | throw new Error('Buffer size must be evenly divisible by 2.') 14 | this.bufferSize = bufferSize 15 | this.ignoreWavHeader = ignoreWavHeader 16 | this.chunks = [] 17 | this.headerBuffer = new Uint8Array() 18 | 19 | this.processTimeStamps = timeStampsProcessingRequired 20 | this.timeStampsBuffer = [] 21 | this.allTimestampsProcessed = false 22 | this.timeStamps = {} 23 | } 24 | 25 | StreamDecoder.prototype.setBufferSize = function (size) { 26 | if (size < 2) throw new Error('Buffer size cannot be less than 2') 27 | if (size % 2 !== 0) 28 | throw new Error('Buffer size must be evenly divisible by 2.') 29 | this.bufferSize = size 30 | } 31 | 32 | StreamDecoder.prototype.setIgnoreWavHeader = function (val) { 33 | this.ignoreWavHeader = val 34 | } 35 | 36 | StreamDecoder.prototype.decodeChunk = function (chunk: Uint8Array) { 37 | // 1. assume user wants headers. no timestamps have been requested, we can store the chunks as they come 38 | this.chunks.push(chunk) 39 | 40 | // 2. user does not need headers and timestamps are also not present, so we ignore the 36 bytes (wav header size) and return the rest 41 | if ( 42 | this.headerBuffer.length < STREAMING_WAV_HEADER_BUFFER_LEN && // check if header has been processed 43 | this.ignoreWavHeader && // Check if header should be ignored 44 | !this.processTimeStamps 45 | ) { 46 | const tempBuf = concatUint8Arrays(this.chunks) 47 | if (tempBuf.length >= STREAMING_WAV_HEADER_BUFFER_LEN) { 48 | this.headerBuffer = tempBuf.slice(0, STREAMING_WAV_HEADER_BUFFER_LEN) // Extract header, for next set of chunks to ignore 49 | const tempDataBuffer = tempBuf.slice(STREAMING_WAV_HEADER_BUFFER_LEN) // Extract data 50 | 51 | this.chunks = [] 52 | this.chunks.push(tempDataBuffer) // set the chunks with the data 53 | } 54 | } 55 | 56 | // timestamps are present, keep storing them untill all timestamps have been processed 57 | if (this.processTimeStamps && !this.allTimestampsProcessed) { 58 | this.timeStampsBuffer.push(chunk) 59 | } 60 | 61 | // 3. user wants timestamps and headers: process the timestamps, preserve the 36 bytes and discard the timestamp bytes 62 | if (!this.ignoreWavHeader && this.processTimeStamps) { 63 | if (!this.allTimestampsProcessed) { 64 | const tempBuf = concatUint8Arrays(this.timeStampsBuffer) 65 | 66 | const obj = this.extractTimestampsFromBuffer(tempBuf) 67 | 68 | // we ran past the buffer length, just preserve the header for now 69 | if (!obj.timestamps) { 70 | // no wav headers yet, obtain it: 71 | if (this.headerBuffer.length < STREAMING_WAV_HEADER_BUFFER_LEN) { 72 | const tempBuf = concatUint8Arrays(this.chunks) 73 | if (tempBuf.length >= STREAMING_WAV_HEADER_BUFFER_LEN) { 74 | this.headerBuffer = tempBuf.slice( 75 | 0, 76 | STREAMING_WAV_HEADER_BUFFER_LEN, 77 | ) 78 | 79 | this.chunks = [] 80 | this.chunks.push(this.headerBuffer) 81 | } 82 | } else { 83 | // since header exists and we don't have timestamps yet, it means we can reset the chunk to only contain the header 84 | this.chunks = [] 85 | this.chunks.push(this.headerBuffer) 86 | } 87 | } 88 | 89 | // timestamps are present, process them 90 | if (obj.timestamps && obj.timestamps !== null) { 91 | if (this.headerBuffer.length < STREAMING_WAV_HEADER_BUFFER_LEN) { 92 | // header not processed yet, process it and discard the timestamps bytes. also process the data bytes if any in current chunk 93 | const tempBuf = concatUint8Arrays(this.chunks) 94 | if (tempBuf.length >= STREAMING_WAV_HEADER_BUFFER_LEN) { 95 | this.headerBuffer = tempBuf.slice( 96 | 0, 97 | STREAMING_WAV_HEADER_BUFFER_LEN, 98 | ) 99 | 100 | this.chunks = [] 101 | const tempDataBuffer = tempBuf.slice(obj.offset) 102 | this.chunks.push(this.headerBuffer) 103 | this.chunks.push(tempDataBuffer) 104 | } 105 | } else { 106 | // header has already been processed, discard the timestamps bytes and preserve wav header and the data bytes if any 107 | const tempBuf = concatUint8Arrays(this.timeStampsBuffer) 108 | const tempDataBuffer = tempBuf.slice(obj.offset) 109 | 110 | this.chunks = [] 111 | this.chunks.push(this.headerBuffer) 112 | this.chunks.push(tempDataBuffer) 113 | } 114 | 115 | // mark all timestamps as processed 116 | this.timeStamps = obj.timestamps 117 | this.allTimestampsProcessed = true 118 | this.timeStampsBuffer = [] 119 | } 120 | } 121 | } 122 | 123 | // 4. timestamps are present and have been requested but no headers are wanted 124 | if (this.ignoreWavHeader && this.processTimeStamps) { 125 | if (!this.allTimestampsProcessed) { 126 | const tempBuf = concatUint8Arrays(this.timeStampsBuffer) 127 | const obj = this.extractTimestampsFromBuffer(tempBuf) 128 | 129 | if (!obj.timestamps && obj.offset) { 130 | // we haven't reached the data section yet, discard evrything 131 | this.chunks = [] 132 | } 133 | 134 | if (obj.timestamps && obj.timestamps !== null) { 135 | this.timeStamps = obj.timestamps 136 | this.allTimestampsProcessed = true 137 | const tempBuf = concatUint8Arrays(this.timeStampsBuffer) 138 | const tempDataBuffer = tempBuf.slice(obj.offset) 139 | this.chunks = [] 140 | this.chunks.push(tempDataBuffer) 141 | } 142 | } 143 | } 144 | } 145 | 146 | StreamDecoder.prototype.flushBuffer = function (force = false) { 147 | const tempBuf = concatUint8Arrays(this.chunks) 148 | if (force && tempBuf.length > 0) { 149 | this.chunks = [] 150 | return tempBuf 151 | } 152 | if (tempBuf.length >= this.bufferSize) { 153 | const returnBuffer = tempBuf.slice(0, this.bufferSize) 154 | const leftoverBuffer = tempBuf.slice(this.bufferSize) 155 | this.chunks = [] 156 | this.chunks.push(leftoverBuffer) 157 | return returnBuffer 158 | } 159 | return null 160 | } 161 | 162 | StreamDecoder.prototype.reset = function () { 163 | this.chunks = [] 164 | this.headerBuffer = new Uint8Array() 165 | } 166 | 167 | StreamDecoder.prototype.getTimestamps = function () { 168 | if (this.processTimeStamps && this.allTimestampsProcessed) { 169 | return this.timeStamps 170 | } 171 | return null 172 | } 173 | 174 | StreamDecoder.prototype.extractTimestampsFromBuffer = function ( 175 | buffer: Uint8Array, 176 | ) { 177 | let offset = 0 178 | offset += 4 // Skip RIFF ID 179 | 180 | offset += 4 // skip remaining file size 181 | offset += 14 // skp RIFF type (WAVE), format chunk id, chunk data size, and compression code 182 | 183 | const dataView = new DataView( 184 | buffer.buffer, 185 | buffer.byteOffset, 186 | buffer.byteLength, 187 | ) 188 | 189 | let [nChannels, sampleRate] = [ 190 | dataView.getUint16(offset, true), // read number of channels 191 | dataView.getUint32(offset + 2, true), // and sample rate 192 | ] 193 | offset += 14 // skip byte rate, block align and bits per sample at this point we have covered the Header & Format chunks: https://docs.app.resemble.ai/docs/resource_clip/stream#header--format-chunks 194 | 195 | const textDecoder = new TextDecoder('ascii') 196 | 197 | let chunkType = textDecoder.decode( 198 | new Uint8Array(buffer.buffer, buffer.byteOffset + offset, 4), 199 | ) // now we are at Timestamps (cue, list & ltxt chunks): https://docs.app.resemble.ai/docs/resource_clip/stream#timestamps-cue-list--ltxt-chunks 200 | 201 | offset += 4 202 | const timestamps = { 203 | graph_chars: [], 204 | graph_times: [], 205 | phon_chars: [], 206 | phon_times: [], 207 | } 208 | 209 | if (chunkType === 'cue ') { 210 | let [remSize, nCuePoints] = [ 211 | dataView.getUint32(offset, true), // Remaining size of the cue chunk 212 | dataView.getUint32(offset + 4, true), // Number of remaining cue points 213 | ] 214 | offset += 8 // skip to the first cue point 215 | let endPoint = offset + remSize - 4 // we subtract 4 to account for the "n_cue_points" field size 216 | 217 | let cuePoints = {} 218 | 219 | // start from the first cue point and read all cue points 220 | // each cue point is 24 bytes long 221 | 222 | if (endPoint > buffer.length) { 223 | return { timestamps: null, offset } 224 | } 225 | for (let cp = 1; cp <= nCuePoints; cp++) { 226 | const idx = dataView.getUint32(offset, true) 227 | const cuePoint = dataView.getUint32(offset + 20, true) 228 | cuePoints[idx] = cuePoint 229 | offset += 24 230 | } 231 | 232 | // now the offset is at the beginning of the LIST chunk, remember we are processing in the little-endian order 233 | chunkType = textDecoder.decode( 234 | new Uint8Array(buffer.buffer, buffer.byteOffset + offset, 4), 235 | ) // read the LIST chunk type 236 | remSize = dataView.getUint32(offset + 4, true) 237 | offset += 12 // arrive at the start of first LTXT chunk 238 | 239 | let listEndPoint = offset + remSize - 4 // we subtract 4 to account for the "rem size" field 240 | 241 | if (listEndPoint > buffer.length) { 242 | return { timestamps: null, offset } 243 | } 244 | 245 | // start from the first LTXT chunk and read all LTXT chunks 246 | while (offset < listEndPoint) { 247 | const subChunkSize = dataView.getUint32(offset + 4, true) // Remaining size of this ltxt chunk after this read 248 | const cueIdx = dataView.getUint32(offset + 8, true) 249 | const nSamples = dataView.getUint32(offset + 12, true) 250 | let charTypeRaw = textDecoder.decode( 251 | new Uint8Array(buffer.buffer, buffer.byteOffset + offset + 16, 4), 252 | ) // "grph" OR "phon" 253 | let charType = charTypeRaw.trim() 254 | 255 | offset += 28 256 | 257 | const textLen = subChunkSize - 20 258 | const utf8Decoder = new TextDecoder('utf-8') 259 | const text = utf8Decoder.decode( 260 | new Uint8Array(buffer.buffer, buffer.byteOffset + offset, textLen - 1), 261 | ) // -1 to remove the null character at the end 262 | 263 | offset += textLen 264 | offset += textLen % 2 265 | 266 | const typeMapping = { 267 | grph: 'graph', 268 | phon: 'phon', 269 | } 270 | let mappedType = typeMapping[charType] 271 | 272 | timestamps[`${mappedType}_chars`].push(text) 273 | timestamps[`${mappedType}_times`].push([ 274 | cuePoints[cueIdx] / sampleRate, 275 | (cuePoints[cueIdx] + nSamples) / sampleRate, 276 | ]) 277 | } 278 | 279 | return { 280 | timestamps: timestamps, 281 | offset, 282 | } 283 | } else { 284 | return { 285 | timestamps: null, 286 | } 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /src/v2/batch.ts: -------------------------------------------------------------------------------- 1 | import UtilV2, { ErrorResponseV2, PaginationResponseV2 } from './util' 2 | 3 | export interface Batch { 4 | uuid: string 5 | body: Array<[string, string]> 6 | voice_uuid: string 7 | callback_uri?: string 8 | total_count: number 9 | completed_count: number 10 | failed_count: number 11 | created_at: Date 12 | updated_at: Date 13 | } 14 | 15 | export default { 16 | all: async ( 17 | projectUuid: string, 18 | page: number, 19 | pageSize?: number, 20 | ): Promise | ErrorResponseV2> => { 21 | try { 22 | const response = await UtilV2.get( 23 | `projects/${projectUuid}/batch?page=${page}${ 24 | pageSize ? `&page_size=${pageSize}` : '' 25 | }`, 26 | ) 27 | let json = await response.json() 28 | if (json.success) { 29 | json.items = json.items.map((item: Batch) => ({ 30 | ...item, 31 | created_at: new Date(item.created_at), 32 | updated_at: new Date(item.updated_at), 33 | })) 34 | } 35 | return json 36 | } catch (error) { 37 | return UtilV2.errorResponse(error) 38 | } 39 | }, 40 | create: async ( 41 | projectUuid: string, 42 | voiceUuid: string, 43 | body: string[] | Array<[string, string]>, 44 | batchInputConfig: { 45 | callbackUri: string 46 | precision: 'PCM_32' | 'PCM_16' | 'MULAW' 47 | sampleRate: 8000 | 16000 | 22050 | 44100 48 | outputFormat: 'wav' | 'mp3' 49 | }, 50 | ) => { 51 | try { 52 | const options = { 53 | body: body, 54 | voice_uuid: voiceUuid, 55 | sample_rate: batchInputConfig?.sampleRate, 56 | output_format: batchInputConfig?.outputFormat, 57 | precision: batchInputConfig?.precision, 58 | } 59 | if (batchInputConfig?.callbackUri) { 60 | options['callback_uri'] = batchInputConfig?.callbackUri 61 | } 62 | const response = await UtilV2.post( 63 | `/projects/${projectUuid}/batch`, 64 | options, 65 | ) 66 | 67 | let json = await response.json() 68 | if (json.success) { 69 | json = { 70 | ...json, 71 | created_at: new Date(json.item.created_at), 72 | updated_at: new Date(json.item.updated_at), 73 | } 74 | } 75 | return json 76 | } catch (error) { 77 | return UtilV2.errorResponse(error) 78 | } 79 | }, 80 | get: async (projectUuid: string, batchUuid: string) => { 81 | try { 82 | const response = await UtilV2.get( 83 | `projects/${projectUuid}/batch/${batchUuid}`, 84 | ) 85 | let json = await response.json() 86 | if (json.success) { 87 | json = { 88 | ...json, 89 | item: { 90 | ...json.item, 91 | created_at: new Date(json.item.created_at), 92 | updated_at: new Date(json.item.updated_at), 93 | }, 94 | } 95 | } 96 | return json 97 | } catch (e) { 98 | return UtilV2.errorResponse(e) 99 | } 100 | }, 101 | delete: async (projectUuid: string, batchUuid: string) => { 102 | try { 103 | const response = await UtilV2.delete( 104 | `projects/${projectUuid}/batch/${batchUuid}`, 105 | ) 106 | const json = response.json() 107 | return json 108 | } catch (e) { 109 | return UtilV2.errorResponse(e) 110 | } 111 | }, 112 | } 113 | -------------------------------------------------------------------------------- /src/v2/clips.ts: -------------------------------------------------------------------------------- 1 | import UtilV2, { 2 | ErrorResponseV2, 3 | PaginationResponseV2, 4 | ReadResponseV2, 5 | WriteResponseV2, 6 | } from './util' 7 | import { DEFAULT_BUFFER_SIZE, StreamDecoder } from './StreamDecoder' 8 | 9 | export interface Clip { 10 | uuid: string 11 | title: string 12 | body: string 13 | voice_uuid: string 14 | is_archived: boolean 15 | timestamps?: any 16 | audio_src?: string 17 | raw_audio?: any 18 | created_at: Date 19 | updated_at: Date 20 | } 21 | 22 | interface ClipInput { 23 | title?: string 24 | body: string 25 | voice_uuid: string 26 | is_archived: boolean 27 | sample_rate?: 16000 | 22050 | 44100 28 | output_format?: 'wav' | 'mp3' 29 | precision?: 'PCM_16' | 'PCM_32' 30 | include_timestamps?: boolean 31 | } 32 | 33 | export interface SyncClipInput extends ClipInput { 34 | raw?: boolean 35 | } 36 | 37 | export interface AsyncClipInput extends ClipInput { 38 | callback_uri: string 39 | } 40 | 41 | export interface DirectClipInput { 42 | voice_uuid: string 43 | project_uuid: string 44 | title?: string 45 | data: string 46 | precision?: 'MULAW' | 'PCM_16' | 'PCM_24' | 'PCM_32' 47 | output_format?: 'wav' | 'mp3' 48 | sample_rate?: 8000 | 16000 | 22050 | 32000 | 44100 | 48000 49 | } 50 | 51 | export interface DirectClip { 52 | success: true 53 | audio_content: string 54 | audio_timestamps: { 55 | graph_chars: string[] 56 | graph_times: [number, number][] 57 | phon_chars: string[] 58 | phon_times: [number, number][] 59 | } 60 | duration: number 61 | synth_duration: number 62 | output_format: 'wav' | 'mp3' 63 | sample_rate: number 64 | issues: string[] 65 | } 66 | 67 | export interface DirectClipError { 68 | success: false 69 | issues?: string[] 70 | error_name: string 71 | error_params: unknown 72 | feedback_uuid: string 73 | message: string 74 | } 75 | 76 | export interface StreamInput { 77 | data: string 78 | project_uuid: string 79 | voice_uuid: string 80 | sample_rate?: 8000 | 16000 | 22050 | 44100 | 32000 81 | precision?: 'MULAW' | 'PCM_16' | 'PCM_32' 82 | } 83 | 84 | export interface StreamConfig { 85 | bufferSize?: number 86 | ignoreWavHeader?: boolean 87 | getTimeStamps?: boolean 88 | } 89 | 90 | const create = async (projectUuid: string, clipInput: ClipInput) => { 91 | try { 92 | const response = await UtilV2.post( 93 | `projects/${projectUuid}/clips`, 94 | clipInput, 95 | ) 96 | let json = await response.json() 97 | if (json.success) { 98 | json = { 99 | ...json, 100 | item: { 101 | ...json.item, 102 | created_at: new Date(json.item.created_at), 103 | updated_at: new Date(json.item.updated_at), 104 | }, 105 | } 106 | } 107 | return json 108 | } catch (e) { 109 | return UtilV2.errorResponse(e) 110 | } 111 | } 112 | 113 | export default { 114 | all: async ( 115 | projectUuid: string, 116 | page: number, 117 | pageSize: number, 118 | ): Promise | ErrorResponseV2> => { 119 | try { 120 | const response = await UtilV2.get( 121 | `projects/${projectUuid}/clips?page=${page}${ 122 | pageSize ? `&page_size=${pageSize}` : '' 123 | }`, 124 | ) 125 | const json = await response.json() 126 | if (json.success) 127 | json.items.map((item) => ({ 128 | ...item, 129 | created_at: new Date(item.created_at), 130 | updated_at: new Date(item.updated_at), 131 | })) 132 | return json 133 | } catch (e) { 134 | return UtilV2.errorResponse(e) 135 | } 136 | }, 137 | 138 | get: async ( 139 | projectUuid: string, 140 | uuid: string, 141 | ): Promise | ErrorResponseV2> => { 142 | try { 143 | const response = await UtilV2.get(`projects/${projectUuid}/clips/${uuid}`) 144 | let json = await response.json() 145 | if (json.success) { 146 | json = { 147 | ...json, 148 | item: { 149 | ...json.item, 150 | created_at: new Date(json.item.created_at), 151 | updated_at: new Date(json.item.updated_at), 152 | }, 153 | } 154 | } 155 | return json 156 | } catch (e) { 157 | return UtilV2.errorResponse(e) 158 | } 159 | }, 160 | 161 | createAsync: async ( 162 | projectUuid: string, 163 | clipInput: AsyncClipInput, 164 | ): Promise | ErrorResponseV2> => { 165 | return create(projectUuid, clipInput) 166 | }, 167 | 168 | createSync: async ( 169 | projectUuid: string, 170 | clipInput: SyncClipInput, 171 | ): Promise | ErrorResponseV2> => { 172 | return create(projectUuid, clipInput) 173 | }, 174 | 175 | createDirect: async ( 176 | clipInput: DirectClipInput, 177 | ): Promise => { 178 | try { 179 | const response = await UtilV2.post('synthesize', clipInput, true) 180 | let json = await response.json() 181 | return json 182 | } catch (e) { 183 | return UtilV2.errorResponse(e) 184 | } 185 | }, 186 | 187 | stream: async function* ( 188 | streamInput: StreamInput, 189 | streamConfig?: StreamConfig, 190 | ): AsyncGenerator { 191 | const defaultStreamConfig = { 192 | bufferSize: DEFAULT_BUFFER_SIZE, 193 | ignoreWavHeader: false, 194 | getTimeStamps: false, 195 | } 196 | 197 | const getTimeStamps = 198 | streamConfig?.getTimeStamps || defaultStreamConfig.getTimeStamps 199 | const bufferSize = 200 | streamConfig?.bufferSize || defaultStreamConfig.bufferSize 201 | const ignoreWavHeader = 202 | streamConfig?.ignoreWavHeader || defaultStreamConfig.ignoreWavHeader 203 | 204 | try { 205 | const response = await UtilV2.post( 206 | 'stream', 207 | { 208 | ...streamInput, 209 | wav_encoded_timestamps: getTimeStamps, 210 | }, 211 | true, 212 | ) 213 | 214 | // check for error response 215 | if (!response.ok || !response.body) { 216 | const isJson = response.headers 217 | .get('content-type') 218 | ?.includes('application/json') 219 | const data = isJson ? await response.json() : null 220 | const error = (data && data.message) || response.status 221 | throw Error(error) 222 | } 223 | 224 | const streamDecoder = new StreamDecoder( 225 | bufferSize, 226 | ignoreWavHeader, 227 | getTimeStamps, 228 | ) 229 | streamDecoder.reset() 230 | 231 | const reader = response.body.getReader() 232 | 233 | // Iterate over the stream and start decoding, and returning data 234 | 235 | try { 236 | while (true) { 237 | const { done, value } = await reader.read() 238 | if (done) break 239 | 240 | streamDecoder.decodeChunk(value) 241 | const buffer = streamDecoder.flushBuffer() 242 | if (buffer !== null) { 243 | yield { 244 | data: buffer, 245 | timestamps: streamDecoder.getTimestamps(), 246 | } 247 | } 248 | } 249 | } finally { 250 | reader.releaseLock() 251 | } 252 | 253 | // Keep draining the buffer until the buffer.length < bufferSize or buffer.length == 0 254 | let buffer = streamDecoder.flushBuffer() 255 | while (buffer !== null) { 256 | const buffToReturn = new Uint8Array(buffer) 257 | buffer = streamDecoder.flushBuffer() 258 | yield { 259 | data: buffToReturn, 260 | timestamps: streamDecoder.getTimestamps(), 261 | } 262 | } 263 | 264 | // Drain any leftover content in the buffer, buffer.length will always be less than bufferSize here 265 | buffer = streamDecoder.flushBuffer(true) 266 | if (buffer !== null) 267 | yield { 268 | data: buffer, 269 | timestamps: streamDecoder.getTimestamps(), 270 | } 271 | } catch (e) { 272 | // If an error occurs and the catch block is executed, the function will return a plain object (UtilV2.errorResponse(e)). 273 | // This will cause the function to not return an async iterable, leading to an error, so we need to throw the error 274 | throw e 275 | } 276 | }, 277 | 278 | updateAsync: async ( 279 | projectUuid: string, 280 | uuid: string, 281 | clipInput: AsyncClipInput, 282 | ) => { 283 | try { 284 | const response = await UtilV2.put( 285 | `projects/${projectUuid}/clips/${uuid}`, 286 | clipInput, 287 | ) 288 | let json = await response.json() 289 | if (json.success) { 290 | json = { 291 | ...json, 292 | item: { 293 | ...json.item, 294 | created_at: new Date(json.item.created_at), 295 | updated_at: new Date(json.item.updated_at), 296 | }, 297 | } 298 | } 299 | return json 300 | } catch (e) { 301 | return UtilV2.errorResponse(e) 302 | } 303 | }, 304 | 305 | destroy: async (projectUuid: string, uuid: string) => { 306 | try { 307 | const response = await UtilV2.delete( 308 | `projects/${projectUuid}/clips/${uuid}`, 309 | ) 310 | const json = response.json() 311 | return json 312 | } catch (e) { 313 | return UtilV2.errorResponse(e) 314 | } 315 | }, 316 | } 317 | -------------------------------------------------------------------------------- /src/v2/edits.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-fetch' 2 | import FormData from 'isomorphic-form-data' 3 | import { context } from '../context' 4 | import UtilV2, { 5 | ErrorResponseV2, 6 | PaginationResponseV2, 7 | ReadResponseV2, 8 | WriteResponseV2, 9 | } from './util' 10 | 11 | export interface AudioEdit { 12 | uuid: string 13 | voice_uuid: string 14 | original_transcript: string 15 | target_transcript: string 16 | input_audio_url: string 17 | result_audio_url: string 18 | } 19 | 20 | export interface AudioEditInput { 21 | original_transcript: string 22 | target_transcript: string 23 | voice_uuid: string 24 | } 25 | 26 | export default { 27 | all: async ( 28 | page: number, 29 | ): Promise | ErrorResponseV2> => { 30 | try { 31 | const response = await UtilV2.get(`edit?page=${page}`) 32 | return await response.json() 33 | } catch (e) { 34 | return UtilV2.errorResponse(e) 35 | } 36 | }, 37 | 38 | get: async ( 39 | audioEditUuid: string, 40 | ): Promise | ErrorResponseV2> => { 41 | try { 42 | const response = await UtilV2.get(`edit/${audioEditUuid}`) 43 | return await response.json() 44 | } catch (e) { 45 | return UtilV2.errorResponse(e) 46 | } 47 | }, 48 | 49 | create: async ( 50 | audioEditInput: AudioEditInput, 51 | buffer: Buffer, 52 | fileSizeInBytes: number, 53 | ): Promise | ErrorResponseV2> => { 54 | try { 55 | const formData = new FormData() 56 | formData.append('original_transcript', audioEditInput.original_transcript) 57 | formData.append('target_transcript', audioEditInput.target_transcript) 58 | formData.append('voice_uuid', audioEditInput.voice_uuid) 59 | formData.append('input_audio', buffer, { knownLength: fileSizeInBytes }) 60 | 61 | const response = await fetch(context.endpoint('v2', `edit`), { 62 | method: 'POST', 63 | headers: { 64 | Authorization: context.headers().Authorization, 65 | 'Content-Type': 'multipart/form-data', 66 | ...(formData.getHeaders ? formData.getHeaders() : {}), 67 | }, 68 | body: formData, 69 | }) 70 | 71 | return await response.json() 72 | } catch (e) { 73 | return UtilV2.errorResponse(e) 74 | } 75 | }, 76 | } 77 | -------------------------------------------------------------------------------- /src/v2/phonemes.ts: -------------------------------------------------------------------------------- 1 | import UtilV2, { ErrorResponseV2, PaginationResponseV2 } from './util' 2 | 3 | export interface Phoneme { 4 | uuid: string 5 | alphabet: string 6 | word: string 7 | phonetic_transcription: string 8 | created_at: Date 9 | updated_at: Date 10 | } 11 | 12 | export interface AllPhonemeResponse { 13 | success: boolean 14 | page: number 15 | num_pages: number 16 | page_size: number 17 | items: Phoneme[] 18 | } 19 | 20 | export default { 21 | all: async ( 22 | page: number, 23 | pageSize?: number, 24 | ): Promise | ErrorResponseV2> => { 25 | try { 26 | const response = await UtilV2.get( 27 | `phonemes?page=${page}${pageSize ? `&page_size=${pageSize}` : ''}`, 28 | ) 29 | let json = await response.json() 30 | if (json.success) { 31 | json.items = json.items.map((item: Phoneme) => ({ 32 | ...item, 33 | created_at: new Date(item.created_at), 34 | updated_at: new Date(item.updated_at), 35 | })) 36 | } 37 | return json 38 | } catch (error) { 39 | return UtilV2.errorResponse(error) 40 | } 41 | }, 42 | create: async (word: string, phonetic_transcription: string) => { 43 | try { 44 | const response = await UtilV2.post('phonemes', { 45 | word, 46 | phonetic_transcription, 47 | }) 48 | let json = await response.json() 49 | if (json.success) { 50 | json.item = { 51 | ...json.item, 52 | created_at: new Date(json.item.created_at), 53 | updated_at: new Date(json.item.updated_at), 54 | } 55 | } 56 | return json 57 | } catch (error) { 58 | return UtilV2.errorResponse(error) 59 | } 60 | }, 61 | get: async (phonemeUuid: string) => { 62 | try { 63 | const response = await UtilV2.get(`phonemes/${phonemeUuid}`) 64 | let json = await response.json() 65 | if (json.success) { 66 | json.item = { 67 | ...json.item, 68 | created_at: new Date(json.item.created_at), 69 | updated_at: new Date(json.item.updated_at), 70 | } 71 | } 72 | return json 73 | } catch (error) { 74 | return UtilV2.errorResponse(error) 75 | } 76 | }, 77 | delete: async (phoneme_uuid: string) => { 78 | try { 79 | const response = await UtilV2.delete(`phonemes/${phoneme_uuid}`) 80 | const json = response.json() 81 | return json 82 | } catch (error) { 83 | return UtilV2.errorResponse(error) 84 | } 85 | }, 86 | } 87 | -------------------------------------------------------------------------------- /src/v2/projects.ts: -------------------------------------------------------------------------------- 1 | import UtilV2, { 2 | DeleteResponseV2, 3 | ErrorResponseV2, 4 | PaginationResponseV2, 5 | ReadResponseV2, 6 | UpdateResponseV2, 7 | WriteResponseV2, 8 | } from './util' 9 | 10 | export interface Project { 11 | uuid: string 12 | name: string 13 | description: string 14 | is_collaborative: boolean 15 | is_archived: boolean 16 | created_at: Date 17 | updated_at: Date 18 | } 19 | 20 | export interface ProjectInput { 21 | name: string 22 | description: string 23 | is_collaborative: boolean 24 | is_archived: boolean 25 | } 26 | 27 | export default { 28 | all: async ( 29 | page: number, 30 | pageSize?: number, 31 | ): Promise | ErrorResponseV2> => { 32 | try { 33 | const response = await UtilV2.get( 34 | `projects?page=${page}${pageSize ? `&page_size=${pageSize}` : ''}`, 35 | ) 36 | let json = await response.json() 37 | if (json.success) { 38 | json.items = json.items.map((item) => ({ 39 | ...item, 40 | created_at: new Date(item.created_at), 41 | updated_at: new Date(item.updated_at), 42 | })) 43 | } 44 | 45 | return json 46 | } catch (e) { 47 | return UtilV2.errorResponse(e) 48 | } 49 | }, 50 | 51 | get: async ( 52 | uuid: string, 53 | ): Promise | ErrorResponseV2> => { 54 | try { 55 | const response = await UtilV2.get(`projects/${uuid}`) 56 | let json = await response.json() 57 | if (json.success) { 58 | json = { 59 | ...json, 60 | item: { 61 | ...json.item, 62 | created_at: new Date(json.item.created_at), 63 | updated_at: new Date(json.item.updated_at), 64 | }, 65 | } 66 | } 67 | return json 68 | } catch (e) { 69 | return UtilV2.errorResponse(e) 70 | } 71 | }, 72 | 73 | create: async ( 74 | projectInput: ProjectInput, 75 | ): Promise | ErrorResponseV2> => { 76 | try { 77 | const response = await UtilV2.post('projects', projectInput) 78 | let json = await response.json() 79 | if (json.success) { 80 | json = { 81 | ...json, 82 | item: { 83 | ...json.item, 84 | created_at: new Date(json.item.created_at), 85 | updated_at: new Date(json.item.updated_at), 86 | }, 87 | } 88 | } 89 | return json 90 | } catch (e) { 91 | return UtilV2.errorResponse(e) 92 | } 93 | }, 94 | 95 | update: async ( 96 | uuid: string, 97 | projectInput: ProjectInput, 98 | ): Promise | ErrorResponseV2> => { 99 | try { 100 | const response = await UtilV2.put(`projects/${uuid}`, projectInput) 101 | let json = await response.json() 102 | if (json.success) { 103 | json = { 104 | ...json, 105 | item: { 106 | ...json.item, 107 | created_at: new Date(json.item.created_at), 108 | updated_at: new Date(json.item.updated_at), 109 | }, 110 | } 111 | } 112 | return json 113 | } catch (e) { 114 | return UtilV2.errorResponse(e) 115 | } 116 | }, 117 | 118 | destroy: async ( 119 | uuid: string, 120 | ): Promise => { 121 | try { 122 | const response = await UtilV2.delete(`projects/${uuid}`) 123 | const json = response.json() 124 | return json 125 | } catch (e) { 126 | return UtilV2.errorResponse(e) 127 | } 128 | }, 129 | } 130 | -------------------------------------------------------------------------------- /src/v2/recordings.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-fetch' 2 | import FormData from 'isomorphic-form-data' 3 | import { context } from '../context' 4 | import UtilV2, { 5 | ErrorResponseV2, 6 | PaginationResponseV2, 7 | UpdateResponseV2, 8 | DeleteResponseV2, 9 | ReadResponseV2, 10 | WriteResponseV2, 11 | } from './util' 12 | 13 | export interface Recording { 14 | uuid: string 15 | name: string 16 | text: string 17 | emotion: string 18 | is_active: boolean 19 | audio_src: string 20 | created_at: Date 21 | updated_at: Date 22 | } 23 | 24 | export interface RecordingInput { 25 | name: string 26 | text: string 27 | emotion: string 28 | is_active: boolean 29 | } 30 | 31 | export default { 32 | all: async ( 33 | voiceUuid: string, 34 | page: number, 35 | pageSize: number, 36 | ): Promise | ErrorResponseV2> => { 37 | try { 38 | const response = await UtilV2.get( 39 | `voices/${voiceUuid}/recordings?page=${page}${ 40 | pageSize ? `&page_size=${pageSize}` : '' 41 | }`, 42 | ) 43 | const json = await response.json() 44 | if (json.success) 45 | json.items.map((item) => ({ 46 | ...item, 47 | created_at: new Date(item.created_at), 48 | updated_at: new Date(item.updated_at), 49 | })) 50 | return json 51 | } catch (e) { 52 | return UtilV2.errorResponse(e) 53 | } 54 | }, 55 | 56 | get: async ( 57 | voiceUuid: string, 58 | uuid: string, 59 | ): Promise | ErrorResponseV2> => { 60 | try { 61 | const response = await UtilV2.get( 62 | `voices/${voiceUuid}/recordings/${uuid}`, 63 | ) 64 | let json = await response.json() 65 | if (json.success) { 66 | json = { 67 | ...json, 68 | item: { 69 | ...json.item, 70 | created_at: new Date(json.item.created_at), 71 | updated_at: new Date(json.item.updated_at), 72 | }, 73 | } 74 | } 75 | return json 76 | } catch (e) { 77 | return UtilV2.errorResponse(e) 78 | } 79 | }, 80 | 81 | create: async ( 82 | voiceUuid: string, 83 | recordingInput: RecordingInput, 84 | buffer: Buffer, 85 | fileSizeInBytes: number, 86 | ): Promise | ErrorResponseV2> => { 87 | try { 88 | const formData = new FormData() 89 | formData.append('name', recordingInput.name) 90 | formData.append('text', recordingInput.text) 91 | formData.append('emotion', recordingInput.emotion) 92 | formData.append('is_active', recordingInput.is_active ? 'true' : 'false') 93 | formData.append('file', buffer, { knownLength: fileSizeInBytes }) 94 | 95 | const response = await fetch( 96 | context.endpoint('v2', `voices/${voiceUuid}/recordings`), 97 | { 98 | method: 'POST', 99 | headers: { 100 | Authorization: context.headers().Authorization, 101 | 'Content-Type': 'multipart/form-data', 102 | ...(formData.getHeaders ? formData.getHeaders() : {}), 103 | }, 104 | body: formData, 105 | }, 106 | ) 107 | 108 | let json = await response.json() 109 | if (json.success) { 110 | json = { 111 | ...json, 112 | item: { 113 | ...json.item, 114 | created_at: new Date(json.item.created_at), 115 | updated_at: new Date(json.item.updated_at), 116 | }, 117 | } 118 | } 119 | return json 120 | } catch (e) { 121 | return UtilV2.errorResponse(e) 122 | } 123 | }, 124 | 125 | update: async ( 126 | voiceUuid: string, 127 | uuid: string, 128 | recordingInput: RecordingInput, 129 | ): Promise | ErrorResponseV2> => { 130 | try { 131 | const response = await UtilV2.put( 132 | `voices/${voiceUuid}/recordings/${uuid}`, 133 | recordingInput, 134 | ) 135 | let json = await response.json() 136 | if (json.success) { 137 | json = { 138 | ...json, 139 | item: { 140 | ...json.item, 141 | created_at: new Date(json.item.created_at), 142 | updated_at: new Date(json.item.updated_at), 143 | }, 144 | } 145 | } 146 | return json 147 | } catch (e) { 148 | return UtilV2.errorResponse(e) 149 | } 150 | }, 151 | 152 | destroy: async ( 153 | voiceUuid: string, 154 | uuid: string, 155 | ): Promise => { 156 | try { 157 | const response = await UtilV2.delete( 158 | `voices/${voiceUuid}/recordings/${uuid}`, 159 | ) 160 | const json = response.json() 161 | return json 162 | } catch (e) { 163 | return UtilV2.errorResponse(e) 164 | } 165 | }, 166 | } 167 | -------------------------------------------------------------------------------- /src/v2/termSubstitutions.ts: -------------------------------------------------------------------------------- 1 | import UtilV2, { ErrorResponseV2, PaginationResponseV2 } from './util' 2 | 3 | export interface TermSubstitution { 4 | uuid: string 5 | original_text: string 6 | replacement_text: string 7 | created_at: Date 8 | updated_at: Date 9 | } 10 | 11 | export interface AllTermSubstitutionResponse { 12 | success: boolean 13 | page: number 14 | num_pages: number 15 | page_size: number 16 | items: TermSubstitution[] 17 | } 18 | 19 | export default { 20 | all: async ( 21 | page: number, 22 | pageSize?: number, 23 | ): Promise< 24 | PaginationResponseV2 | ErrorResponseV2 25 | > => { 26 | try { 27 | const response = await UtilV2.get( 28 | `term_substitutions?page=${page}${ 29 | pageSize ? `&page_size=${pageSize}` : '' 30 | }`, 31 | ) 32 | let json = await response.json() 33 | if (json.success) { 34 | json.items = json.items.map((item: TermSubstitution) => ({ 35 | ...item, 36 | created_at: new Date(item.created_at), 37 | updated_at: new Date(item.updated_at), 38 | })) 39 | } 40 | return json 41 | } catch (error) { 42 | return UtilV2.errorResponse(error) 43 | } 44 | }, 45 | create: async (original_text: string, replacement_text: string) => { 46 | try { 47 | const response = await UtilV2.post('term_substitutions', { 48 | original_text, 49 | replacement_text, 50 | }) 51 | let json = await response.json() 52 | if (json.success) { 53 | json.item = { 54 | ...json.item, 55 | created_at: new Date(json.item.created_at), 56 | updated_at: new Date(json.item.updated_at), 57 | } 58 | } 59 | return json 60 | } catch (error) { 61 | return UtilV2.errorResponse(error) 62 | } 63 | }, 64 | get: async (termSubstitutionUuid: string) => { 65 | try { 66 | const response = await UtilV2.get( 67 | `term_substitutions/${termSubstitutionUuid}`, 68 | ) 69 | let json = await response.json() 70 | if (json.success) { 71 | json.item = { 72 | ...json.item, 73 | created_at: new Date(json.item.created_at), 74 | updated_at: new Date(json.item.updated_at), 75 | } 76 | } 77 | return json 78 | } catch (error) { 79 | return UtilV2.errorResponse(error) 80 | } 81 | }, 82 | delete: async (termSubstitutionUuid: string) => { 83 | try { 84 | const response = await UtilV2.delete( 85 | `term_substitutions/${termSubstitutionUuid}`, 86 | ) 87 | const json = response.json() 88 | return json 89 | } catch (error) { 90 | return UtilV2.errorResponse(error) 91 | } 92 | }, 93 | } 94 | -------------------------------------------------------------------------------- /src/v2/util.ts: -------------------------------------------------------------------------------- 1 | import { context } from '../context' 2 | 3 | export interface ReadResponseV2 { 4 | success: boolean 5 | message?: string 6 | item: T | null 7 | } 8 | 9 | export interface WriteResponseV2 { 10 | success: boolean 11 | message?: string 12 | /* The item is returned when the write operation succeeds */ 13 | item?: T 14 | } 15 | 16 | export interface UpdateResponseV2 { 17 | success: boolean 18 | message?: string 19 | /* The item is returned when the update operation succeeds */ 20 | item?: T 21 | } 22 | 23 | export interface DeleteResponseV2 { 24 | success: boolean 25 | message?: string 26 | } 27 | 28 | export interface PaginationResponseV2 { 29 | success: boolean 30 | message?: string 31 | page: number 32 | num_pages: number 33 | page_size: number 34 | items: T[] 35 | } 36 | 37 | export interface ErrorResponseV2 { 38 | success: false 39 | message: string 40 | } 41 | 42 | export default { 43 | get: (path: string, useSynthesisServer: boolean = false) => { 44 | return fetch( 45 | useSynthesisServer 46 | ? context.synServerUrl(path) 47 | : context.endpoint('v2', path), 48 | { 49 | method: 'GET', 50 | headers: useSynthesisServer 51 | ? context.synthesisServerHeaders() 52 | : context.headers(), 53 | }, 54 | ) 55 | }, 56 | post: ( 57 | path: string, 58 | data: Record = {}, 59 | useSynthesisServer: boolean = false, 60 | ) => 61 | fetch( 62 | useSynthesisServer 63 | ? context.synServerUrl(path) 64 | : context.endpoint('v2', path), 65 | { 66 | method: 'POST', 67 | headers: useSynthesisServer 68 | ? context.synthesisServerHeaders() 69 | : context.headers(), 70 | body: JSON.stringify(data), 71 | }, 72 | ), 73 | put: ( 74 | path: string, 75 | data: Record = {}, 76 | useSynthesisServer: boolean = false, 77 | ) => 78 | fetch( 79 | useSynthesisServer 80 | ? context.synServerUrl(path) 81 | : context.endpoint('v2', path), 82 | { 83 | method: 'PUT', 84 | headers: useSynthesisServer 85 | ? context.synthesisServerHeaders() 86 | : context.headers(), 87 | body: JSON.stringify(data), 88 | }, 89 | ), 90 | delete: (path: string, useSynthesisServer: boolean = false) => 91 | fetch( 92 | useSynthesisServer 93 | ? context.synServerUrl(path) 94 | : context.endpoint('v2', path), 95 | { 96 | method: 'DELETE', 97 | headers: useSynthesisServer 98 | ? context.synthesisServerHeaders() 99 | : context.headers(), 100 | }, 101 | ), 102 | 103 | errorResponse: (e: any): ErrorResponseV2 => ({ 104 | success: false, 105 | message: `Library error: ${e}`, 106 | }), 107 | } 108 | 109 | // https://github.com/sindresorhus/uint8array-extras 110 | 111 | export function concatUint8Arrays(arrays: Uint8Array[], totalLength?: number) { 112 | if (arrays.length === 0) { 113 | return new Uint8Array(0) 114 | } 115 | 116 | totalLength ??= arrays.reduce( 117 | (accumulator, currentValue) => accumulator + currentValue.length, 118 | 0, 119 | ) 120 | 121 | const returnValue = new Uint8Array(totalLength) 122 | 123 | let offset = 0 124 | for (const array of arrays) { 125 | returnValue.set(array, offset) 126 | offset += array.length 127 | } 128 | 129 | return returnValue 130 | } 131 | -------------------------------------------------------------------------------- /src/v2/voices.ts: -------------------------------------------------------------------------------- 1 | import UtilV2, { 2 | DeleteResponseV2, 3 | ErrorResponseV2, 4 | PaginationResponseV2, 5 | ReadResponseV2, 6 | UpdateResponseV2, 7 | WriteResponseV2, 8 | } from './util' 9 | 10 | export interface Voice { 11 | uuid: string 12 | name: string 13 | status: string 14 | default_language: string 15 | supported_languages: string[] 16 | created_at: Date 17 | updated_at: Date 18 | } 19 | 20 | export interface VoiceInput { 21 | name: string 22 | dataset_url?: string 23 | callback_uri?: string 24 | consent?: string 25 | } 26 | 27 | export default { 28 | all: async ( 29 | page: number, 30 | pageSize: number, 31 | ): Promise | ErrorResponseV2> => { 32 | try { 33 | const response = await UtilV2.get( 34 | `voices?page=${page}${pageSize ? `&page_size=${pageSize}` : ''}`, 35 | ) 36 | const json = await response.json() 37 | if (json.success) 38 | json.items.map((item) => ({ 39 | ...item, 40 | created_at: new Date(item.created_at), 41 | updated_at: new Date(item.updated_at), 42 | })) 43 | return json 44 | } catch (e) { 45 | return UtilV2.errorResponse(e) 46 | } 47 | }, 48 | 49 | get: async ( 50 | uuid: string, 51 | ): Promise | ErrorResponseV2> => { 52 | try { 53 | const response = await UtilV2.get(`voices/${uuid}`) 54 | let json = await response.json() 55 | if (json.success) { 56 | json = { 57 | ...json, 58 | item: { 59 | ...json.item, 60 | created_at: new Date(json.item.created_at), 61 | updated_at: new Date(json.item.updated_at), 62 | }, 63 | } 64 | } 65 | return json 66 | } catch (e) { 67 | return UtilV2.errorResponse(e) 68 | } 69 | }, 70 | 71 | create: async ( 72 | voiceInput: VoiceInput, 73 | ): Promise | ErrorResponseV2> => { 74 | try { 75 | const response = await UtilV2.post('voices', voiceInput) 76 | let json = await response.json() 77 | if (json.success) { 78 | json = { 79 | ...json, 80 | item: { 81 | ...json.item, 82 | created_at: new Date(json.item.created_at), 83 | updated_at: new Date(json.item.updated_at), 84 | }, 85 | } 86 | } 87 | return json 88 | } catch (e) { 89 | return UtilV2.errorResponse(e) 90 | } 91 | }, 92 | 93 | update: async ( 94 | uuid: string, 95 | voiceInput: VoiceInput, 96 | ): Promise | ErrorResponseV2> => { 97 | try { 98 | const response = await UtilV2.put(`voices/${uuid}`, voiceInput) 99 | let json = await response.json() 100 | if (json.success) { 101 | json = { 102 | ...json, 103 | item: { 104 | ...json.item, 105 | created_at: new Date(json.item.created_at), 106 | updated_at: new Date(json.item.updated_at), 107 | }, 108 | } 109 | } 110 | return json 111 | } catch (e) { 112 | return UtilV2.errorResponse(e) 113 | } 114 | }, 115 | 116 | destroy: async ( 117 | uuid: string, 118 | ): Promise => { 119 | try { 120 | const response = await UtilV2.delete(`voices/${uuid}`) 121 | const json = response.json() 122 | return json 123 | } catch (e) { 124 | return UtilV2.errorResponse(e) 125 | } 126 | }, 127 | 128 | build: async ( 129 | uuid: string, 130 | ): Promise<{ success: boolean; message?: string } | ErrorResponseV2> => { 131 | try { 132 | const response = await UtilV2.post(`voices/${uuid}/build`) 133 | const json = response.json() 134 | return json 135 | } catch (e) { 136 | return UtilV2.errorResponse(e) 137 | } 138 | }, 139 | } 140 | -------------------------------------------------------------------------------- /test/api.test.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | const { Resemble } = require('../dist/resemble.cjs') 3 | 4 | jest.setTimeout(30000) 5 | 6 | const getTestVoiceUUID = () => { 7 | if (!process.env.TEST_VOICE_UUID) 8 | throw 'Invalid voice UUID specified; please set the TEST_VOICE_UUID environment variable' 9 | return process.env.TEST_VOICE_UUID 10 | } 11 | 12 | const getTestProjectUUID = () => { 13 | if (!process.env.TEST_PROJECT_UUID) 14 | throw 'Invalid voice UUID specified; please set the TEST_VOICE_UUID environment variable' 15 | return process.env.TEST_PROJECT_UUID 16 | } 17 | 18 | const getTestBaseURL = () => { 19 | if (!process.env.TEST_BASE_URL) { 20 | console.log( 21 | 'Using https://app.resemble.ai/api/ as the base URL, set the TEST_BASE_URL environment variable to change it', 22 | ) 23 | } 24 | return process.env.TEST_BASE_URL || 'https://app.resemble.ai/api/' 25 | } 26 | 27 | const getTestSynServerURL = () => { 28 | if (!process.env.TEST_SYN_SERVER_URL) 29 | throw 'Invalid syn server url specified; please set the TEST_SYN_SERVER_URL environment variable' 30 | return process.env.TEST_SYN_SERVER_URL 31 | } 32 | 33 | const getTestAPIKey = () => { 34 | if (!process.env.TEST_API_KEY) { 35 | throw 'Invalid API key; please specify the TEST_API_KEY environment variable.' 36 | } 37 | return process.env.TEST_API_KEY 38 | } 39 | 40 | const getTestCallbackURL = () => { 41 | if (!process.env.TEST_CALLBACK_URL) { 42 | throw 'Invalid test callback url' 43 | } 44 | return process.env.TEST_CALLBACK_URL 45 | } 46 | 47 | beforeAll(() => { 48 | Resemble.setApiKey(getTestAPIKey()) 49 | Resemble.setBaseUrl(getTestBaseURL()) 50 | Resemble.setSynthesisUrl(getTestSynServerURL()) 51 | 52 | // and just call these to make sure they're set :-) 53 | getTestCallbackURL() 54 | getTestVoiceUUID() 55 | }) 56 | 57 | test('streaming', async () => { 58 | for await (const chunk of Resemble.v2.clips.stream({ 59 | project_uuid: process.env.TEST_PROJECT_UUID, 60 | voice_uuid: getTestVoiceUUID(), 61 | data: 'This is a test', 62 | })) { 63 | expect(chunk).not.toBeNull() 64 | } 65 | }) 66 | 67 | test('projects', async () => { 68 | const projects = await Resemble.v2.projects.all(1) 69 | expect(projects.success).toEqual(true) 70 | const project = await Resemble.v2.projects.create({ 71 | name: 'SDK Test Project', 72 | description: 'SDK Test Description', 73 | is_archived: false, 74 | is_collaborative: false, 75 | }) 76 | expect(project.success).toEqual(true) 77 | const updated_project = await Resemble.v2.projects.update(project.item.uuid, { 78 | name: 'SDK Updated Project Name', 79 | description: 'SDK Updated Project description', 80 | }) 81 | expect(updated_project.success).toEqual(true) 82 | const fetched_project = await Resemble.v2.projects.get(project.item.uuid) 83 | expect(fetched_project.success).toEqual(true) 84 | const deleteOp = await Resemble.v2.projects.delete(project.item.uuid) 85 | expect(deleteOp.success).toEqual(true) 86 | }) 87 | 88 | test('clips', async () => { 89 | const project = await Resemble.v2.projects.create({ 90 | name: 'Test Project', 91 | description: 'Test Description', 92 | is_archived: false, 93 | is_collaborative: false, 94 | }) 95 | const projectUuid = project.item.uuid 96 | 97 | const clips = await Resemble.v2.clips.all(projectUuid, 1, 10) 98 | expect(clips.success).toEqual(true) 99 | const syncClip = await Resemble.v2.clips.createSync(projectUuid, { 100 | voice_uuid: getTestVoiceUUID(), 101 | body: 'This is a test', 102 | is_archived: false, 103 | }) 104 | 105 | expect(syncClip.success).toEqual(true) 106 | const asyncClip = await Resemble.v2.clips.createAsync(projectUuid, { 107 | voice_uuid: getTestVoiceUUID(), 108 | callback_uri: getTestCallbackURL(), 109 | body: 'This is a test', 110 | }) 111 | expect(asyncClip.success).toEqual(true) 112 | 113 | const updateAsyncClip = await Resemble.v2.clips.updateAsync( 114 | projectUuid, 115 | syncClip.item.uuid, 116 | { 117 | voice_uuid: getTestVoiceUUID(), 118 | callback_uri: getTestCallbackURL(), 119 | body: 'This is another test', 120 | }, 121 | ) 122 | expect(updateAsyncClip.success).toEqual(true) 123 | const clip = await Resemble.v2.clips.get(projectUuid, syncClip.item.uuid) 124 | expect(clip.success).toEqual(true) 125 | const deleteOp = await Resemble.v2.clips.delete(projectUuid, clip.item.uuid) 126 | expect(deleteOp.success).toEqual(true) 127 | 128 | await Resemble.v2.projects.delete(projectUuid) 129 | }) 130 | 131 | test('voices', async () => { 132 | const voices = await Resemble.v2.voices.all(1) 133 | expect(voices.success).toEqual(true) 134 | const voice = await Resemble.v2.voices.create({ 135 | name: 'Test Voice', 136 | consent: 'badb', 137 | }) 138 | expect(voice.success).toEqual(false) 139 | const updated_voice = await Resemble.v2.voices.update(voice.item.uuid, { 140 | name: 'NewVoiceName', 141 | }) 142 | expect(updated_voice.success).toEqual(true) 143 | const fetched_voice = await Resemble.v2.voices.get(voice.item.uuid) 144 | expect(fetched_voice.success).toEqual(true) 145 | const deleteOp = await Resemble.v2.voices.delete(voice.item.uuid) 146 | expect(deleteOp.success).toEqual(true) 147 | }) 148 | 149 | test('recordings', async () => { 150 | const voice = await Resemble.v2.voices.create({ name: 'Test Voice' }) 151 | const voiceUuid = voice.item.uuid 152 | 153 | const recordings = await Resemble.v2.recordings.all(voiceUuid, 1) 154 | expect(recordings.success).toEqual(true) 155 | const fs = require('fs') 156 | const resolve = require('path').resolve 157 | const stream = fs.createReadStream(resolve(__dirname, 'sample_audio.wav')) 158 | const size = fs.statSync(resolve(__dirname, 'sample_audio.wav')).size 159 | const recording = await Resemble.v2.recordings.create( 160 | voiceUuid, 161 | { 162 | name: 'Test recording', 163 | text: 'transcription', 164 | is_active: true, 165 | emotion: 'neutral', 166 | }, 167 | stream, 168 | size, 169 | ) 170 | expect(recording.success).toEqual(true) 171 | const updatedRecording = await Resemble.v2.recordings.update( 172 | voiceUuid, 173 | recording.item.uuid, 174 | { 175 | name: 'New name', 176 | text: 'new transcription', 177 | is_active: true, 178 | emotion: 'neutral', 179 | }, 180 | ) 181 | expect(updatedRecording.success).toEqual(true) 182 | const fetchedRecording = await Resemble.v2.recordings.get( 183 | voiceUuid, 184 | recording.item.uuid, 185 | ) 186 | expect(fetchedRecording.success).toEqual(true) 187 | const deleteOp = await Resemble.v2.recordings.delete( 188 | voiceUuid, 189 | recording.item.uuid, 190 | ) 191 | expect(deleteOp.success).toEqual(true) 192 | }) 193 | -------------------------------------------------------------------------------- /test/batch.test.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | const { Resemble } = require('../dist/resemble.cjs') 3 | 4 | jest.setTimeout(30000) 5 | 6 | const getTestVoiceUUID = () => { 7 | if (!process.env.TEST_VOICE_UUID) 8 | throw 'Invalid voice UUID specified; please set the TEST_VOICE_UUID environment variable' 9 | return process.env.TEST_VOICE_UUID 10 | } 11 | 12 | const getTestProjectUUID = () => { 13 | if (!process.env.TEST_PROJECT_UUID) 14 | throw 'Invalid voice UUID specified; please set the TEST_VOICE_UUID environment variable' 15 | return process.env.TEST_PROJECT_UUID 16 | } 17 | 18 | const getTestBaseURL = () => { 19 | if (!process.env.TEST_BASE_URL) { 20 | console.log( 21 | 'Using https://app.resemble.ai/api/ as the base URL, set the TEST_BASE_URL environment variable to change it', 22 | ) 23 | } 24 | return process.env.TEST_BASE_URL || 'https://app.resemble.ai/api/' 25 | } 26 | 27 | const getTestAPIKey = () => { 28 | if (!process.env.TEST_API_KEY) { 29 | throw 'Invalid API key; please specify the TEST_API_KEY environment variable.' 30 | } 31 | return process.env.TEST_API_KEY 32 | } 33 | 34 | const getTestCallbackURL = () => { 35 | if (!process.env.TEST_CALLBACK_URL) { 36 | throw 'Invalid test callback url' 37 | } 38 | return process.env.TEST_CALLBACK_URL 39 | } 40 | 41 | beforeAll(() => { 42 | Resemble.setApiKey(getTestAPIKey()) 43 | Resemble.setBaseUrl(getTestBaseURL()) 44 | 45 | // and just call these to make sure they're set :-) 46 | getTestCallbackURL() 47 | getTestVoiceUUID() 48 | getTestProjectUUID() 49 | }) 50 | 51 | test('batch', async () => { 52 | // create a batch 53 | const res = await Resemble.v2.batch.create( 54 | getTestProjectUUID(), 55 | getTestVoiceUUID(), 56 | ['Content A', 'Content B', 'Content C'], 57 | ) 58 | expect(res.success).toEqual(true) 59 | expect(res.item).toBeDefined() 60 | 61 | expect(res.item.body).toBeArrayOfSize(3) 62 | expect(res.item.body[0]).toBeArrayOfSize(2) 63 | expect(res.item.voice_uuid).toEqual(getTestVoiceUUID()) 64 | expect(res.item.total_count).toEqual(3) 65 | 66 | // get all batches 67 | const batches = await Resemble.v2.batch.all(getTestProjectUUID(), 1) 68 | expect(batches.success).toEqual(true) 69 | expect(batches.items).toBeArray() 70 | expect(batches.items.length).toBeGreaterThan(0) 71 | 72 | // get a batch 73 | const b = await Resemble.v2.batch.get(getTestProjectUUID(), res.item.uuid) 74 | expect(b.success).toEqual(true) 75 | expect(b.item).toBeDefined() 76 | expect(b.item.uuid).toEqual(res.item.uuid) 77 | 78 | // delete a batch 79 | const del = await Resemble.v2.batch.delete( 80 | getTestProjectUUID(), 81 | res.item.uuid, 82 | ) 83 | expect(del.success).toEqual(true) 84 | }) 85 | -------------------------------------------------------------------------------- /test/phonemes.test.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | const { Resemble } = require('../dist/resemble.cjs') 3 | 4 | jest.setTimeout(30000) 5 | 6 | const getTestVoiceUUID = () => { 7 | if (!process.env.TEST_VOICE_UUID) 8 | throw 'Invalid voice UUID specified; please set the TEST_VOICE_UUID environment variable' 9 | return process.env.TEST_VOICE_UUID 10 | } 11 | 12 | const getTestProjectUUID = () => { 13 | if (!process.env.TEST_PROJECT_UUID) 14 | throw 'Invalid voice UUID specified; please set the TEST_VOICE_UUID environment variable' 15 | return process.env.TEST_PROJECT_UUID 16 | } 17 | 18 | const getTestBaseURL = () => { 19 | if (!process.env.TEST_BASE_URL) { 20 | console.log( 21 | 'Using https://app.resemble.ai/api/ as the base URL, set the TEST_BASE_URL environment variable to change it', 22 | ) 23 | } 24 | return process.env.TEST_BASE_URL || 'https://app.resemble.ai/api/' 25 | } 26 | 27 | const getTestAPIKey = () => { 28 | if (!process.env.TEST_API_KEY) { 29 | throw 'Invalid API key; please specify the TEST_API_KEY environment variable.' 30 | } 31 | return process.env.TEST_API_KEY 32 | } 33 | 34 | const getTestCallbackURL = () => { 35 | if (!process.env.TEST_CALLBACK_URL) { 36 | throw 'Invalid test callback url' 37 | } 38 | return process.env.TEST_CALLBACK_URL 39 | } 40 | 41 | beforeAll(() => { 42 | Resemble.setApiKey(getTestAPIKey()) 43 | Resemble.setBaseUrl(getTestBaseURL()) 44 | 45 | // and just call these to make sure they're set :-) 46 | getTestCallbackURL() 47 | getTestVoiceUUID() 48 | getTestProjectUUID() 49 | }) 50 | 51 | test('phoneme', async () => { 52 | // create a phoneme 53 | const res = await Resemble.v2.phonemes.create('try', 'θɹiː') 54 | expect(res.success).toEqual(true) 55 | expect(res.item).toBeDefined() 56 | expect(res.item.alphabet).toEqual('ipa') 57 | expect(res.item.word).toBeString() 58 | expect(res.item.phonetic_transcription).toBeString() 59 | 60 | // get all phonemes 61 | const res2 = await Resemble.v2.phonemes.all(1) 62 | expect(res2.success).toEqual(true) 63 | expect(res2.items).toBeArray() 64 | expect(res2.items.length).toBeGreaterThan(0) 65 | 66 | // get a phoneme 67 | const ph = await Resemble.v2.phonemes.get(res.item.uuid) 68 | expect(ph.success).toEqual(true) 69 | expect(ph.item).toBeDefined() 70 | expect(ph.item.uuid).toEqual(res.item.uuid) 71 | 72 | // delete phoneme 73 | const res3 = await Resemble.v2.phonemes.delete(res.item.uuid) 74 | expect(res3.success).toEqual(true) 75 | }) 76 | -------------------------------------------------------------------------------- /test/sample_audio.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resemble-ai/resemble-node/e569adeff382cf93e1fa7688061c2cff96aef1e8/test/sample_audio.wav -------------------------------------------------------------------------------- /test/term_substitutions.test.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | const { Resemble } = require('../dist/resemble.cjs') 3 | 4 | jest.setTimeout(30000) 5 | 6 | const getTestVoiceUUID = () => { 7 | if (!process.env.TEST_VOICE_UUID) 8 | throw 'Invalid voice UUID specified; please set the TEST_VOICE_UUID environment variable' 9 | return process.env.TEST_VOICE_UUID 10 | } 11 | 12 | const getTestProjectUUID = () => { 13 | if (!process.env.TEST_PROJECT_UUID) 14 | throw 'Invalid voice UUID specified; please set the TEST_VOICE_UUID environment variable' 15 | return process.env.TEST_PROJECT_UUID 16 | } 17 | 18 | const getTestBaseURL = () => { 19 | if (!process.env.TEST_BASE_URL) { 20 | console.log( 21 | 'Using https://app.resemble.ai/api/ as the base URL, set the TEST_BASE_URL environment variable to change it', 22 | ) 23 | } 24 | return process.env.TEST_BASE_URL || 'https://app.resemble.ai/api/' 25 | } 26 | 27 | const getTestAPIKey = () => { 28 | if (!process.env.TEST_API_KEY) { 29 | throw 'Invalid API key; please specify the TEST_API_KEY environment variable.' 30 | } 31 | return process.env.TEST_API_KEY 32 | } 33 | 34 | const getTestCallbackURL = () => { 35 | if (!process.env.TEST_CALLBACK_URL) { 36 | throw 'Invalid test callback url' 37 | } 38 | return process.env.TEST_CALLBACK_URL 39 | } 40 | 41 | beforeAll(() => { 42 | Resemble.setApiKey(getTestAPIKey()) 43 | Resemble.setBaseUrl(getTestBaseURL()) 44 | 45 | // and just call these to make sure they're set :-) 46 | getTestCallbackURL() 47 | getTestVoiceUUID() 48 | getTestProjectUUID() 49 | }) 50 | 51 | test('substitutions', async () => { 52 | // create a term substitution 53 | const res = await Resemble.v2.termSubstitutions.create('gray', 'grey') 54 | expect(res.success).toEqual(true) 55 | expect(res.item).toBeDefined() 56 | expect(res.item.original_text).toBeString() 57 | expect(res.item.replacement_text).toBeString() 58 | 59 | // get all term substitutions 60 | const res2 = await Resemble.v2.termSubstitutions.all(1) 61 | expect(res2.success).toEqual(true) 62 | expect(res2.items).toBeArray() 63 | expect(res2.items.length).toBeGreaterThan(0) 64 | 65 | // get a term substitution 66 | const ts = await Resemble.v2.termSubstitutions.get(res.item.uuid) 67 | expect(ts.success).toEqual(true) 68 | expect(ts.item).toBeDefined() 69 | expect(ts.item.uuid).toEqual(res.item.uuid) 70 | 71 | // delete term substitution 72 | const res3 = await Resemble.v2.termSubstitutions.delete(res.item.uuid) 73 | expect(res3.success).toEqual(true) 74 | }) 75 | -------------------------------------------------------------------------------- /test/testSetup.js: -------------------------------------------------------------------------------- 1 | // add all jest-extended matchers 2 | const matchers = require('jest-extended') 3 | expect.extend(matchers) 4 | --------------------------------------------------------------------------------