diff --git a/.changeset/nvidia-plugin-initial.md b/.changeset/nvidia-plugin-initial.md new file mode 100644 index 000000000..2155c4d11 --- /dev/null +++ b/.changeset/nvidia-plugin-initial.md @@ -0,0 +1,5 @@ +--- +"@livekit/agents-plugin-nvidia": patch +--- + +Add NVIDIA Riva plugin with STT and TTS support diff --git a/examples/package.json b/examples/package.json index 531ef544e..b8b15b6b4 100644 --- a/examples/package.json +++ b/examples/package.json @@ -34,6 +34,7 @@ "@livekit/agents-plugin-inworld": "workspace:*", "@livekit/agents-plugin-livekit": "workspace:*", "@livekit/agents-plugin-neuphonic": "workspace:*", + "@livekit/agents-plugin-nvidia": "workspace:*", "@livekit/agents-plugin-openai": "workspace:*", "@livekit/agents-plugin-resemble": "workspace:*", "@livekit/agents-plugin-silero": "workspace:*", diff --git a/examples/src/nvidia_agent.ts b/examples/src/nvidia_agent.ts new file mode 100644 index 000000000..15d7c9e7c --- /dev/null +++ b/examples/src/nvidia_agent.ts @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: 2025 LiveKit, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +import { + type JobContext, + type JobProcess, + WorkerOptions, + cli, + defineAgent, + metrics, + voice, +} from '@livekit/agents'; +import * as livekit from '@livekit/agents-plugin-livekit'; +import * as nvidia from '@livekit/agents-plugin-nvidia'; +import * as openai from '@livekit/agents-plugin-openai'; +import * as silero from '@livekit/agents-plugin-silero'; +import { fileURLToPath } from 'node:url'; + +export default defineAgent({ + prewarm: async (proc: JobProcess) => { + proc.userData.vad = await silero.VAD.load(); + }, + entry: async (ctx: JobContext) => { + const agent = new voice.Agent({ + instructions: + "You are a helpful assistant using NVIDIA Riva for speech recognition and synthesis. You can hear the user's message and respond to it.", + }); + + const vad = ctx.proc.userData.vad! as silero.VAD; + + const session = new voice.AgentSession({ + vad, + stt: new nvidia.STT({ + model: 'parakeet-1.1b-en-US-asr-streaming-silero-vad-sortformer', + languageCode: 'en-US', + }), + tts: new nvidia.TTS({ + voice: 'Magpie-Multilingual.EN-US.Leo', + languageCode: 'en-US', + }), + llm: new openai.LLM(), + turnDetection: new livekit.turnDetector.MultilingualModel(), + }); + + const usageCollector = new metrics.UsageCollector(); + + session.on(voice.AgentSessionEventTypes.MetricsCollected, (ev) => { + metrics.logMetrics(ev.metrics); + usageCollector.collect(ev.metrics); + }); + + await session.start({ + agent, + room: ctx.room, + }); + + session.say('Hello, how can I help you today?'); + }, +}); + +cli.runApp(new WorkerOptions({ agent: fileURLToPath(import.meta.url) })); diff --git a/plugins/nvidia/README.md b/plugins/nvidia/README.md new file mode 100644 index 000000000..2515664ee --- /dev/null +++ b/plugins/nvidia/README.md @@ -0,0 +1,75 @@ + +# NVIDIA Riva Plugin for LiveKit Agents + +This plugin provides NVIDIA Riva Speech-to-Text (STT) and Text-to-Speech (TTS) capabilities for LiveKit Agents. + +## Installation + +```bash +npm install @livekit/agents-plugin-nvidia +# or +pnpm add @livekit/agents-plugin-nvidia +``` + +## Configuration + +Set your NVIDIA API key as an environment variable: + +```bash +export NVIDIA_API_KEY=your_api_key_here +``` + +## Usage + +### Speech-to-Text (STT) + +```typescript +import * as nvidia from '@livekit/agents-plugin-nvidia'; + +const stt = new nvidia.STT({ + model: 'parakeet-1.1b-en-US-asr-streaming-silero-vad-sortformer', + languageCode: 'en-US', +}); +``` + +### Text-to-Speech (TTS) + +```typescript +import * as nvidia from '@livekit/agents-plugin-nvidia'; + +const tts = new nvidia.TTS({ + voice: 'Magpie-Multilingual.EN-US.Leo', + languageCode: 'en-US', +}); +``` + +## Options + +### STT Options + +- `model`: The ASR model to use (default: `'parakeet-1.1b-en-US-asr-streaming-silero-vad-sortformer'`) +- `functionId`: NVIDIA function ID (default: `'1598d209-5e27-4d3c-8079-4751568b1081'`) +- `punctuate`: Enable automatic punctuation (default: `true`) +- `languageCode`: Language code (default: `'en-US'`) +- `sampleRate`: Audio sample rate (default: `16000`) +- `server`: NVIDIA Riva server address (default: `'grpc.nvcf.nvidia.com:443'`) +- `useSsl`: Use SSL for connection (default: `true`) +- `apiKey`: NVIDIA API key (defaults to `NVIDIA_API_KEY` environment variable) + +### TTS Options + +- `voice`: Voice name (default: `'Magpie-Multilingual.EN-US.Leo'`) +- `functionId`: NVIDIA function ID (default: `'877104f7-e885-42b9-8de8-f6e4c6303969'`) +- `languageCode`: Language code (default: `'en-US'`) +- `sampleRate`: Audio sample rate (default: `16000`) +- `server`: NVIDIA Riva server address (default: `'grpc.nvcf.nvidia.com:443'`) +- `useSsl`: Use SSL for connection (default: `true`) +- `apiKey`: NVIDIA API key (defaults to `NVIDIA_API_KEY` environment variable) + +## License + +Apache-2.0 diff --git a/plugins/nvidia/api-extractor.json b/plugins/nvidia/api-extractor.json new file mode 100644 index 000000000..1f75e0708 --- /dev/null +++ b/plugins/nvidia/api-extractor.json @@ -0,0 +1,20 @@ +/** + * Config file for API Extractor. For more info, please visit: https://api-extractor.com + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + + /** + * Optionally specifies another JSON config file that this file extends from. This provides a way for + * standard settings to be shared across multiple projects. + * + * If the path starts with "./" or "../", the path is resolved relative to the folder of the file that contains + * the "extends" field. Otherwise, the first path segment is interpreted as an NPM package name, and will be + * resolved using NodeJS require(). + * + * SUPPORTED TOKENS: none + * DEFAULT VALUE: "" + */ + "extends": "../../api-extractor-shared.json", + "mainEntryPointFilePath": "./dist/index.d.ts" +} diff --git a/plugins/nvidia/package.json b/plugins/nvidia/package.json new file mode 100644 index 000000000..0c416251f --- /dev/null +++ b/plugins/nvidia/package.json @@ -0,0 +1,52 @@ +{ + "name": "@livekit/agents-plugin-nvidia", + "version": "1.0.0", + "description": "NVIDIA Riva plugin for LiveKit Agents for Node.js", + "main": "dist/index.js", + "require": "dist/index.cjs", + "types": "dist/index.d.ts", + "exports": { + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } + }, + "author": "LiveKit", + "type": "module", + "repository": "git@github.com:livekit/agents-js.git", + "license": "Apache-2.0", + "files": [ + "dist", + "src", + "README.md" + ], + "scripts": { + "build": "tsup --onSuccess \"pnpm build:types\"", + "build:types": "tsc --declaration --emitDeclarationOnly && node ../../scripts/copyDeclarationOutput.js", + "clean": "rm -rf dist", + "clean:build": "pnpm clean && pnpm build", + "lint": "eslint -f unix \"src/**/*.{ts,js}\"", + "api:check": "api-extractor run --typescript-compiler-folder ../../node_modules/typescript", + "api:update": "api-extractor run --local --typescript-compiler-folder ../../node_modules/typescript --verbose" + }, + "devDependencies": { + "@livekit/agents": "workspace:*", + "@livekit/agents-plugins-test": "workspace:*", + "@livekit/rtc-node": "^0.13.22", + "@microsoft/api-extractor": "^7.35.0", + "tsup": "^8.3.5", + "typescript": "^5.0.0" + }, + "dependencies": { + "@grpc/grpc-js": "^1.12.6", + "@grpc/proto-loader": "^0.7.15" + }, + "peerDependencies": { + "@livekit/agents": "workspace:*", + "@livekit/rtc-node": "^0.13.22" + } +} diff --git a/plugins/nvidia/src/index.ts b/plugins/nvidia/src/index.ts new file mode 100644 index 000000000..94913b212 --- /dev/null +++ b/plugins/nvidia/src/index.ts @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2025 LiveKit, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +import { Plugin } from '@livekit/agents'; + +export * from './stt.js'; +export * from './tts.js'; +export * from './models.js'; + +class NVIDIAPlugin extends Plugin { + constructor() { + super({ + title: 'nvidia', + version: '0.1.0', + package: '@livekit/agents-plugin-nvidia', + }); + } +} + +Plugin.registerPlugin(new NVIDIAPlugin()); diff --git a/plugins/nvidia/src/models.ts b/plugins/nvidia/src/models.ts new file mode 100644 index 000000000..5519e2c07 --- /dev/null +++ b/plugins/nvidia/src/models.ts @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2025 LiveKit, Inc. +// +// SPDX-License-Identifier: Apache-2.0 + +export type STTModels = + | 'parakeet-1.1b-en-US-asr-streaming-silero-vad-sortformer' + | 'parakeet-ctc-1.1b-asr' + | 'parakeet-ctc-0.6b-asr' + | string; + +export type STTLanguages = + | 'en-US' + | 'es-US' + | 'de-DE' + | 'fr-FR' + | 'it-IT' + | 'pt-BR' + | 'zh-CN' + | 'ja-JP' + | 'ko-KR' + | string; + +export type TTSVoices = + | 'Magpie-Multilingual.EN-US.Leo' + | 'Magpie-Multilingual.EN-US.Luna' + | 'Magpie-Multilingual.EN-US.Aria' + | 'Magpie-Multilingual.EN-US.Asteria' + | string; + +export type TTSLanguages = + | 'en-US' + | 'es-US' + | 'de-DE' + | 'fr-FR' + | 'it-IT' + | 'pt-BR' + | 'zh-CN' + | 'ja-JP' + | 'ko-KR' + | string; diff --git a/plugins/nvidia/src/stt.ts b/plugins/nvidia/src/stt.ts new file mode 100644 index 000000000..32291c092 --- /dev/null +++ b/plugins/nvidia/src/stt.ts @@ -0,0 +1,346 @@ +// SPDX-FileCopyrightText: 2025 LiveKit, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +import * as grpc from '@grpc/grpc-js'; +import { + type APIConnectOptions, + type AudioBuffer, + AudioByteStream, + log, + stt, +} from '@livekit/agents'; +import type { AudioFrame } from '@livekit/rtc-node'; +import type { STTLanguages, STTModels } from './models.js'; + +const DEFAULT_SERVER = 'grpc.nvcf.nvidia.com:443'; +const DEFAULT_FUNCTION_ID = '1598d209-5e27-4d3c-8079-4751568b1081'; +const DEFAULT_MODEL = 'parakeet-1.1b-en-US-asr-streaming-silero-vad-sortformer'; + +export interface STTOptions { + apiKey?: string; + model: STTModels; + functionId: string; + punctuate: boolean; + languageCode: STTLanguages | string; + sampleRate: number; + server: string; + useSsl: boolean; +} + +const defaultSTTOptions: STTOptions = { + apiKey: process.env.NVIDIA_API_KEY, + model: DEFAULT_MODEL, + functionId: DEFAULT_FUNCTION_ID, + punctuate: true, + languageCode: 'en-US', + sampleRate: 16000, + server: DEFAULT_SERVER, + useSsl: true, +}; + +interface RecognitionConfig { + encoding: number; + sampleRateHertz: number; + languageCode: string; + maxAlternatives: number; + enableAutomaticPunctuation: boolean; + audioChannelCount: number; + enableWordTimeOffsets: boolean; + model: string; +} + +interface StreamingRecognitionConfig { + config: RecognitionConfig; + interimResults: boolean; +} + +interface WordInfo { + startTime: number; + endTime: number; + word: string; + confidence: number; +} + +interface SpeechRecognitionAlternative { + transcript: string; + confidence: number; + words: WordInfo[]; +} + +interface StreamingRecognitionResult { + alternatives: SpeechRecognitionAlternative[]; + isFinal: boolean; + stability: number; + channelTag: number; + audioProcessed: number; +} + +interface StreamingRecognizeResponse { + results: StreamingRecognitionResult[]; +} + +export class STT extends stt.STT { + #opts: STTOptions; + #logger = log(); + label = 'nvidia.STT'; + + constructor(opts: Partial = {}) { + super({ + streaming: true, + interimResults: opts.punctuate ?? defaultSTTOptions.punctuate, + }); + + this.#opts = { ...defaultSTTOptions, ...opts }; + + if (this.#opts.useSsl && !this.#opts.apiKey) { + throw new Error( + 'NVIDIA API key is required when using SSL. Either pass apiKey parameter, set NVIDIA_API_KEY environment variable, or disable SSL and use a locally hosted Riva NIM service.', + ); + } + + this.#logger.info( + { model: this.#opts.model, server: this.#opts.server }, + 'Initializing NVIDIA STT', + ); + } + + async _recognize(_: AudioBuffer): Promise { + throw new Error('Recognize is not supported on NVIDIA STT, use stream() instead'); + } + + updateOptions(opts: Partial) { + this.#opts = { ...this.#opts, ...opts }; + } + + stream(options?: { connOptions?: APIConnectOptions }): SpeechStream { + return new SpeechStream(this, this.#opts, options?.connOptions); + } +} + +export class SpeechStream extends stt.SpeechStream { + #opts: STTOptions; + #logger = log(); + #speaking = false; + #requestId = ''; + #grpcClient: grpc.Client | null = null; + #call: grpc.ClientDuplexStream | null = null; + label = 'nvidia.SpeechStream'; + + constructor(stt: STT, opts: STTOptions, connOptions?: APIConnectOptions) { + super(stt, opts.sampleRate, connOptions); + this.#opts = opts; + } + + protected async run() { + const credentials = this.#opts.useSsl + ? grpc.credentials.createSsl() + : grpc.credentials.createInsecure(); + + const metadata = new grpc.Metadata(); + if (this.#opts.apiKey) { + metadata.set('authorization', `Bearer ${this.#opts.apiKey}`); + } + metadata.set('function-id', this.#opts.functionId); + + const serviceDef = this.createServiceDefinition(); + const ServiceClient = grpc.makeGenericClientConstructor(serviceDef, 'RivaSpeechRecognition'); + + this.#grpcClient = new ServiceClient(this.#opts.server, credentials); + + const streamingConfig: StreamingRecognitionConfig = { + config: { + encoding: 1, // LINEAR_PCM + sampleRateHertz: this.#opts.sampleRate, + languageCode: this.#opts.languageCode, + maxAlternatives: 1, + enableAutomaticPunctuation: this.#opts.punctuate, + audioChannelCount: 1, + enableWordTimeOffsets: true, + model: this.#opts.model, + }, + interimResults: true, + }; + + try { + await this.runRecognition(streamingConfig, metadata); + } finally { + if (this.#grpcClient) { + this.#grpcClient.close(); + this.#grpcClient = null; + } + } + } + + private createServiceDefinition(): grpc.ServiceDefinition { + return { + StreamingRecognize: { + path: '/nvidia.riva.asr.RivaSpeechRecognition/StreamingRecognize', + requestStream: true, + responseStream: true, + requestSerialize: (value: unknown) => Buffer.from(JSON.stringify(value)), + requestDeserialize: (value: Buffer) => JSON.parse(value.toString()), + responseSerialize: (value: unknown) => Buffer.from(JSON.stringify(value)), + responseDeserialize: (value: Buffer) => JSON.parse(value.toString()), + }, + }; + } + + private async runRecognition( + streamingConfig: StreamingRecognitionConfig, + metadata: grpc.Metadata, + ): Promise { + return new Promise((resolve, reject) => { + if (!this.#grpcClient) { + reject(new Error('gRPC client not initialized')); + return; + } + + this.#call = ( + this.#grpcClient as grpc.Client & { + StreamingRecognize: ( + metadata: grpc.Metadata, + ) => grpc.ClientDuplexStream; + } + ).StreamingRecognize(metadata); + + if (!this.#call) { + reject(new Error('Failed to create streaming call')); + return; + } + + // Send initial config + this.#call.write({ streamingConfig }); + + // Handle responses + this.#call.on('data', (response: StreamingRecognizeResponse) => { + this.handleResponse(response); + }); + + this.#call.on('error', (error: Error) => { + if (!this.closed) { + this.#logger.error({ error }, 'NVIDIA STT stream error'); + } + reject(error); + }); + + this.#call.on('end', () => { + resolve(); + }); + + // Send audio data + this.sendAudioData() + .then(() => { + if (this.#call) { + this.#call.end(); + } + }) + .catch(reject); + }); + } + + private async sendAudioData(): Promise { + const samples100Ms = Math.floor(this.#opts.sampleRate / 10); + const stream = new AudioByteStream(this.#opts.sampleRate, 1, samples100Ms); + + for await (const data of this.input) { + if (this.closed || !this.#call) break; + + if (data === SpeechStream.FLUSH_SENTINEL) { + const frames = stream.flush(); + for (const frame of frames) { + this.sendFrame(frame); + } + continue; + } + + const frames = stream.write(data.data.buffer as ArrayBuffer); + for (const frame of frames) { + this.sendFrame(frame); + } + } + + // Flush remaining audio + const remainingFrames = stream.flush(); + for (const frame of remainingFrames) { + this.sendFrame(frame); + } + } + + private sendFrame(frame: AudioFrame): void { + if (this.#call && !this.closed) { + this.#call.write({ audioContent: Buffer.from(frame.data.buffer).toString('base64') }); + } + } + + private handleResponse(response: StreamingRecognizeResponse): void { + if (!response.results || response.results.length === 0) { + return; + } + + for (const result of response.results) { + if (!result.alternatives || result.alternatives.length === 0) { + continue; + } + + const alternative = result.alternatives[0]; + if (!alternative) { + continue; + } + const transcript = alternative.transcript || ''; + + if (!transcript.trim()) { + continue; + } + + this.#requestId = `nvidia-${Date.now()}`; + + if (!this.#speaking && transcript.trim()) { + this.#speaking = true; + this.queue.put({ type: stt.SpeechEventType.START_OF_SPEECH }); + } + + const speechData = this.convertToSpeechData(alternative); + + if (result.isFinal) { + this.queue.put({ + type: stt.SpeechEventType.FINAL_TRANSCRIPT, + requestId: this.#requestId, + alternatives: [speechData], + }); + + if (this.#speaking) { + this.queue.put({ type: stt.SpeechEventType.END_OF_SPEECH }); + this.#speaking = false; + } + } else { + this.queue.put({ + type: stt.SpeechEventType.INTERIM_TRANSCRIPT, + requestId: this.#requestId, + alternatives: [speechData], + }); + } + } + } + + private convertToSpeechData(alternative: SpeechRecognitionAlternative): stt.SpeechData { + const transcript = alternative.transcript || ''; + const confidence = alternative.confidence || 0.0; + const words = alternative.words || []; + + let startTime = 0.0; + let endTime = 0.0; + + if (words.length > 0) { + startTime = (words[0]?.startTime || 0) / 1000.0; + endTime = (words[words.length - 1]?.endTime || 0) / 1000.0; + } + + return { + language: this.#opts.languageCode, + startTime, + endTime, + confidence, + text: transcript, + }; + } +} diff --git a/plugins/nvidia/src/tts.ts b/plugins/nvidia/src/tts.ts new file mode 100644 index 000000000..ed3558e87 --- /dev/null +++ b/plugins/nvidia/src/tts.ts @@ -0,0 +1,348 @@ +// SPDX-FileCopyrightText: 2025 LiveKit, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +import * as grpc from '@grpc/grpc-js'; +import { + type APIConnectOptions, + AudioByteStream, + log, + shortuuid, + tokenize, + tts, +} from '@livekit/agents'; +import type { TTSLanguages, TTSVoices } from './models.js'; + +const DEFAULT_SERVER = 'grpc.nvcf.nvidia.com:443'; +const DEFAULT_FUNCTION_ID = '877104f7-e885-42b9-8de8-f6e4c6303969'; +const DEFAULT_VOICE = 'Magpie-Multilingual.EN-US.Leo'; +const NUM_CHANNELS = 1; +const BUFFERED_WORDS_COUNT = 8; + +export interface TTSOptions { + apiKey?: string; + voice: TTSVoices | string; + functionId: string; + languageCode: TTSLanguages | string; + sampleRate: number; + server: string; + useSsl: boolean; +} + +const defaultTTSOptions: TTSOptions = { + apiKey: process.env.NVIDIA_API_KEY, + voice: DEFAULT_VOICE, + functionId: DEFAULT_FUNCTION_ID, + languageCode: 'en-US', + sampleRate: 16000, + server: DEFAULT_SERVER, + useSsl: true, +}; + +interface SynthesizeSpeechRequest { + text: string; + languageCode: string; + encoding: number; + sampleRateHz: number; + voiceName: string; +} + +interface SynthesizeSpeechResponse { + audio: string; // base64 encoded audio +} + +export class TTS extends tts.TTS { + #opts: TTSOptions; + #logger = log(); + label = 'nvidia.TTS'; + + constructor(opts: Partial = {}) { + super(opts.sampleRate || defaultTTSOptions.sampleRate, NUM_CHANNELS, { + streaming: true, + }); + + this.#opts = { ...defaultTTSOptions, ...opts }; + + if (this.#opts.useSsl && !this.#opts.apiKey) { + throw new Error( + 'NVIDIA API key is required when using SSL. Either pass apiKey parameter, set NVIDIA_API_KEY environment variable, or disable SSL and use a locally hosted Riva NIM service.', + ); + } + + this.#logger.info( + { voice: this.#opts.voice, server: this.#opts.server }, + 'Initializing NVIDIA TTS', + ); + } + + updateOptions(opts: Partial) { + this.#opts = { ...this.#opts, ...opts }; + } + + synthesize(text: string, connOptions?: APIConnectOptions): ChunkedStream { + return new ChunkedStream(this, text, this.#opts, connOptions); + } + + stream(): SynthesizeStream { + return new SynthesizeStream(this, this.#opts); + } +} + +export class ChunkedStream extends tts.ChunkedStream { + #opts: TTSOptions; + #text: string; + #logger = log(); + label = 'nvidia.ChunkedStream'; + + constructor(tts: TTS, text: string, opts: TTSOptions, connOptions?: APIConnectOptions) { + super(text, tts, connOptions); + this.#text = text; + this.#opts = opts; + } + + protected async run() { + const requestId = shortuuid(); + const bstream = new AudioByteStream(this.#opts.sampleRate, NUM_CHANNELS); + + const credentials = this.#opts.useSsl + ? grpc.credentials.createSsl() + : grpc.credentials.createInsecure(); + + const metadata = new grpc.Metadata(); + if (this.#opts.apiKey) { + metadata.set('authorization', `Bearer ${this.#opts.apiKey}`); + } + metadata.set('function-id', this.#opts.functionId); + + const serviceDef = this.createServiceDefinition(); + const ServiceClient = grpc.makeGenericClientConstructor(serviceDef, 'RivaSpeechSynthesis'); + const client = new ServiceClient(this.#opts.server, credentials); + + try { + const request: SynthesizeSpeechRequest = { + text: this.#text, + languageCode: this.#opts.languageCode, + encoding: 1, // LINEAR_PCM + sampleRateHz: this.#opts.sampleRate, + voiceName: this.#opts.voice, + }; + + await new Promise((resolve, reject) => { + const call = ( + client as unknown as { + SynthesizeOnline: ( + request: SynthesizeSpeechRequest, + metadata: grpc.Metadata, + ) => grpc.ClientReadableStream; + } + ).SynthesizeOnline(request, metadata); + + call.on('data', (response: SynthesizeSpeechResponse) => { + if (response.audio) { + const audioBuffer = Buffer.from(response.audio, 'base64'); + const arrayBuffer = audioBuffer.buffer.slice( + audioBuffer.byteOffset, + audioBuffer.byteOffset + audioBuffer.byteLength, + ); + for (const frame of bstream.write(arrayBuffer)) { + this.queue.put({ + requestId, + frame, + final: false, + segmentId: requestId, + }); + } + } + }); + + call.on('error', (error: Error) => { + this.#logger.error({ error }, 'NVIDIA TTS synthesis error'); + reject(error); + }); + + call.on('end', () => { + // Flush remaining audio + for (const frame of bstream.flush()) { + this.queue.put({ + requestId, + frame, + final: false, + segmentId: requestId, + }); + } + resolve(); + }); + }); + } finally { + client.close(); + } + } + + private createServiceDefinition(): grpc.ServiceDefinition { + return { + SynthesizeOnline: { + path: '/nvidia.riva.tts.RivaSpeechSynthesis/SynthesizeOnline', + requestStream: false, + responseStream: true, + requestSerialize: (value: unknown) => Buffer.from(JSON.stringify(value)), + requestDeserialize: (value: Buffer) => JSON.parse(value.toString()), + responseSerialize: (value: unknown) => Buffer.from(JSON.stringify(value)), + responseDeserialize: (value: Buffer) => JSON.parse(value.toString()), + }, + }; + } +} + +export class SynthesizeStream extends tts.SynthesizeStream { + #opts: TTSOptions; + #logger = log(); + #tokenizer = new tokenize.basic.SentenceTokenizer({ + minSentenceLength: BUFFERED_WORDS_COUNT, + }).stream(); + #grpcClient: grpc.Client | null = null; + label = 'nvidia.SynthesizeStream'; + + constructor(tts: TTS, opts: TTSOptions) { + super(tts); + this.#opts = opts; + } + + updateOptions(opts: Partial) { + this.#opts = { ...this.#opts, ...opts }; + } + + protected async run() { + const requestId = shortuuid(); + const bstream = new AudioByteStream(this.#opts.sampleRate, NUM_CHANNELS); + + const credentials = this.#opts.useSsl + ? grpc.credentials.createSsl() + : grpc.credentials.createInsecure(); + + const metadata = new grpc.Metadata(); + if (this.#opts.apiKey) { + metadata.set('authorization', `Bearer ${this.#opts.apiKey}`); + } + metadata.set('function-id', this.#opts.functionId); + + const serviceDef = this.createServiceDefinition(); + const ServiceClient = grpc.makeGenericClientConstructor(serviceDef, 'RivaSpeechSynthesis'); + this.#grpcClient = new ServiceClient(this.#opts.server, credentials); + + const inputTask = async () => { + for await (const data of this.input) { + if (data === SynthesizeStream.FLUSH_SENTINEL) { + this.#tokenizer.flush(); + continue; + } + this.#tokenizer.pushText(data); + } + this.#tokenizer.endInput(); + this.#tokenizer.close(); + }; + + const synthesizeTask = async () => { + for await (const event of this.#tokenizer) { + if (this.closed || this.abortController.signal.aborted) break; + + const text = event.token; + if (!text.trim()) continue; + + await this.synthesizeText(text, requestId, bstream, metadata); + } + + // Flush remaining audio + for (const frame of bstream.flush()) { + if (!this.queue.closed) { + this.queue.put({ + requestId, + frame, + final: false, + segmentId: requestId, + }); + } + } + + if (!this.queue.closed) { + this.queue.put(SynthesizeStream.END_OF_STREAM); + } + }; + + try { + await Promise.all([inputTask(), synthesizeTask()]); + } finally { + if (this.#grpcClient) { + this.#grpcClient.close(); + this.#grpcClient = null; + } + } + } + + private async synthesizeText( + text: string, + requestId: string, + bstream: AudioByteStream, + metadata: grpc.Metadata, + ): Promise { + if (!this.#grpcClient) return; + + const request: SynthesizeSpeechRequest = { + text, + languageCode: this.#opts.languageCode, + encoding: 1, // LINEAR_PCM + sampleRateHz: this.#opts.sampleRate, + voiceName: this.#opts.voice, + }; + + return new Promise((resolve, reject) => { + const call = ( + this.#grpcClient as unknown as { + SynthesizeOnline: ( + request: SynthesizeSpeechRequest, + metadata: grpc.Metadata, + ) => grpc.ClientReadableStream; + } + ).SynthesizeOnline(request, metadata); + + call.on('data', (response: SynthesizeSpeechResponse) => { + if (response.audio && !this.queue.closed) { + const audioBuffer = Buffer.from(response.audio, 'base64'); + const arrayBuffer = audioBuffer.buffer.slice( + audioBuffer.byteOffset, + audioBuffer.byteOffset + audioBuffer.byteLength, + ); + for (const frame of bstream.write(arrayBuffer)) { + this.queue.put({ + requestId, + frame, + final: false, + segmentId: requestId, + }); + } + } + }); + + call.on('error', (error: Error) => { + this.#logger.error({ error }, 'NVIDIA TTS synthesis error'); + reject(error); + }); + + call.on('end', () => { + resolve(); + }); + }); + } + + private createServiceDefinition(): grpc.ServiceDefinition { + return { + SynthesizeOnline: { + path: '/nvidia.riva.tts.RivaSpeechSynthesis/SynthesizeOnline', + requestStream: false, + responseStream: true, + requestSerialize: (value: unknown) => Buffer.from(JSON.stringify(value)), + requestDeserialize: (value: Buffer) => JSON.parse(value.toString()), + responseSerialize: (value: unknown) => Buffer.from(JSON.stringify(value)), + responseDeserialize: (value: Buffer) => JSON.parse(value.toString()), + }, + }; + } +} diff --git a/plugins/nvidia/tsconfig.json b/plugins/nvidia/tsconfig.json new file mode 100644 index 000000000..fbbc25bac --- /dev/null +++ b/plugins/nvidia/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.json", + "include": ["./src"], + "compilerOptions": { + "rootDir": "./src", + "declarationDir": "./dist", + "outDir": "./dist" + }, + "typedocOptions": { + "name": "plugins/agents-plugin-nvidia", + "entryPointStrategy": "resolve", + "readme": "none", + "entryPoints": ["src/index.ts"] + } +} diff --git a/plugins/nvidia/tsup.config.ts b/plugins/nvidia/tsup.config.ts new file mode 100644 index 000000000..8ca20961f --- /dev/null +++ b/plugins/nvidia/tsup.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'tsup'; + +import defaults from '../../tsup.config'; + +export default defineConfig({ + ...defaults, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1dce72646..9d72eab84 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -48,7 +48,7 @@ importers: version: 8.10.0(eslint@8.57.0) eslint-config-standard: specifier: ^17.1.0 - version: 17.1.0(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint-plugin-n@16.6.2(eslint@8.57.0))(eslint-plugin-promise@6.1.1(eslint@8.57.0))(eslint@8.57.0) + version: 17.1.0(eslint-plugin-import@2.29.1)(eslint-plugin-n@16.6.2(eslint@8.57.0))(eslint-plugin-promise@6.1.1(eslint@8.57.0))(eslint@8.57.0) eslint-config-turbo: specifier: ^1.12.2 version: 1.13.3(eslint@8.57.0) @@ -251,6 +251,9 @@ importers: '@livekit/agents-plugin-neuphonic': specifier: workspace:* version: link:../plugins/neuphonic + '@livekit/agents-plugin-nvidia': + specifier: workspace:* + version: link:../plugins/nvidia '@livekit/agents-plugin-openai': specifier: workspace:* version: link:../plugins/openai @@ -644,6 +647,34 @@ importers: specifier: ^5.0.0 version: 5.4.5 + plugins/nvidia: + dependencies: + '@grpc/grpc-js': + specifier: ^1.12.6 + version: 1.14.3 + '@grpc/proto-loader': + specifier: ^0.7.15 + version: 0.7.15 + devDependencies: + '@livekit/agents': + specifier: workspace:* + version: link:../../agents + '@livekit/agents-plugins-test': + specifier: workspace:* + version: link:../test + '@livekit/rtc-node': + specifier: ^0.13.22 + version: 0.13.22 + '@microsoft/api-extractor': + specifier: ^7.35.0 + version: 7.43.7(@types/node@22.19.1) + tsup: + specifier: ^8.3.5 + version: 8.4.0(@microsoft/api-extractor@7.43.7(@types/node@22.19.1))(postcss@8.5.6)(tsx@4.20.4)(typescript@5.9.3) + typescript: + specifier: ^5.0.0 + version: 5.9.3 + plugins/openai: dependencies: '@livekit/mutex': @@ -1646,6 +1677,20 @@ packages: '@modelcontextprotocol/sdk': optional: true + '@grpc/grpc-js@1.14.3': + resolution: {integrity: sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==} + engines: {node: '>=12.10.0'} + + '@grpc/proto-loader@0.7.15': + resolution: {integrity: sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==} + engines: {node: '>=6'} + hasBin: true + + '@grpc/proto-loader@0.8.0': + resolution: {integrity: sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==} + engines: {node: '>=6'} + hasBin: true + '@huggingface/hub@2.4.1': resolution: {integrity: sha512-g/EJG091aIdP1whpSjhqBOL25/m60NKXhYGz3wqp7hLX57r4Fx7QVFfXRbtxI0ZMQjLQV3GYrPtldz38mvOr+w==} engines: {node: '>=18'} @@ -1844,6 +1889,9 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@js-sdsl/ordered-map@4.4.2': + resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} + '@livekit/changesets-changelog-github@0.0.4': resolution: {integrity: sha512-MXaiLYwgkYciZb8G2wkVtZ1pJJzZmVx5cM30Q+ClslrIYyAqQhRbPmZDM79/5CGxb1MTemR/tfOM25tgJgAK0g==} @@ -3001,6 +3049,10 @@ packages: cjs-module-lexer@1.4.3: resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} @@ -3276,6 +3328,10 @@ packages: engines: {node: '>=18'} hasBin: true + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + escape-string-regexp@1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} @@ -3638,6 +3694,10 @@ packages: resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==} engines: {node: '>=18'} + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + get-func-name@2.0.2: resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} @@ -4110,6 +4170,9 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + lodash.get@4.4.2: resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} deprecated: This package is deprecated. Use the optional chaining (?.) operator instead. @@ -4598,6 +4661,10 @@ packages: resolution: {integrity: sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==} engines: {node: '>=12.0.0'} + protobufjs@7.5.4: + resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} + engines: {node: '>=12.0.0'} + pump@3.0.0: resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} @@ -4651,6 +4718,10 @@ packages: resolution: {integrity: sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==} engines: {node: '>= 0.4'} + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + require-in-the-middle@7.5.2: resolution: {integrity: sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==} engines: {node: '>=8.6.0'} @@ -5464,6 +5535,10 @@ packages: utf-8-validate: optional: true + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} @@ -5471,6 +5546,14 @@ packages: resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} engines: {node: '>=18'} + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -6119,6 +6202,25 @@ snapshots: - supports-color - utf-8-validate + '@grpc/grpc-js@1.14.3': + dependencies: + '@grpc/proto-loader': 0.8.0 + '@js-sdsl/ordered-map': 4.4.2 + + '@grpc/proto-loader@0.7.15': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.2.3 + protobufjs: 7.4.0 + yargs: 17.7.2 + + '@grpc/proto-loader@0.8.0': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.2.3 + protobufjs: 7.5.4 + yargs: 17.7.2 + '@huggingface/hub@2.4.1': dependencies: '@huggingface/tasks': 0.19.36 @@ -6282,6 +6384,8 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@js-sdsl/ordered-map@4.4.2': {} + '@livekit/changesets-changelog-github@0.0.4': dependencies: '@changesets/get-github-info': 0.5.2 @@ -7520,6 +7624,12 @@ snapshots: cjs-module-lexer@1.4.3: {} + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + color-convert@1.9.3: dependencies: color-name: 1.1.3 @@ -7908,6 +8018,8 @@ snapshots: '@esbuild/win32-ia32': 0.27.2 '@esbuild/win32-x64': 0.27.2 + escalade@3.2.0: {} + escape-string-regexp@1.0.5: {} escape-string-regexp@4.0.0: {} @@ -7939,7 +8051,7 @@ snapshots: dependencies: eslint: 8.57.0 - eslint-config-standard@17.1.0(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint-plugin-n@16.6.2(eslint@8.57.0))(eslint-plugin-promise@6.1.1(eslint@8.57.0))(eslint@8.57.0): + eslint-config-standard@17.1.0(eslint-plugin-import@2.29.1)(eslint-plugin-n@16.6.2(eslint@8.57.0))(eslint-plugin-promise@6.1.1(eslint@8.57.0))(eslint@8.57.0): dependencies: eslint: 8.57.0 eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) @@ -7964,7 +8076,7 @@ snapshots: debug: 4.3.4 enhanced-resolve: 5.16.1 eslint: 8.57.0 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.7.5 @@ -7976,7 +8088,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0): + eslint-module-utils@2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): dependencies: debug: 3.2.7 optionalDependencies: @@ -8004,7 +8116,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) hasown: 2.0.2 is-core-module: 2.13.1 is-glob: 4.0.3 @@ -8355,6 +8467,8 @@ snapshots: transitivePeerDependencies: - supports-color + get-caller-file@2.0.5: {} + get-func-name@2.0.2: {} get-intrinsic@1.2.4: @@ -8837,6 +8951,8 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash.camelcase@4.3.0: {} + lodash.get@4.4.2: {} lodash.isequal@4.5.0: {} @@ -9318,6 +9434,21 @@ snapshots: '@types/node': 22.19.1 long: 5.2.3 + protobufjs@7.5.4: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 22.19.1 + long: 5.2.3 + pump@3.0.0: dependencies: end-of-stream: 1.4.4 @@ -9375,6 +9506,8 @@ snapshots: es-errors: 1.3.0 set-function-name: 2.0.2 + require-directory@2.1.1: {} + require-in-the-middle@7.5.2: dependencies: debug: 4.4.1 @@ -10354,10 +10487,24 @@ snapshots: ws@8.18.3: {} + y18n@5.0.8: {} + yallist@4.0.0: {} yallist@5.0.0: {} + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + yocto-queue@0.1.0: {} yocto-queue@1.0.0: {} diff --git a/turbo.json b/turbo.json index aa13412ea..00391762e 100644 --- a/turbo.json +++ b/turbo.json @@ -51,7 +51,8 @@ "RIME_API_KEY", "LK_OPENAI_DEBUG", "OVHCLOUD_API_KEY", - "INWORLD_API_KEY" + "INWORLD_API_KEY", + "NVIDIA_API_KEY" ], "pipeline": { "build": {