diff --git a/.changeset/hot-taxis-judge.md b/.changeset/hot-taxis-judge.md new file mode 100644 index 000000000..f3dc1928e --- /dev/null +++ b/.changeset/hot-taxis-judge.md @@ -0,0 +1,5 @@ +--- +'@livekit/agents': patch +--- + +Add utility to play local audio file to livekit diff --git a/REUSE.toml b/REUSE.toml index d1b1789cb..8897121a7 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -36,3 +36,9 @@ SPDX-License-Identifier = "CC-BY-NC-SA-4.0" path = ["**/.gitattributes", "**.wav", "**/__snapshots__/**"] SPDX-FileCopyrightText = "2024 LiveKit, Inc." SPDX-License-Identifier = "Apache-2.0" + +# audio resources +[[annotations]] +path = ["agents/resources/*.ogg", "agents/resources/NOTICE"] +SPDX-FileCopyrightText = "2024 LiveKit, Inc." +SPDX-License-Identifier = "Apache-2.0" diff --git a/agents/package.json b/agents/package.json index 9aafc0424..03e9b3e4a 100644 --- a/agents/package.json +++ b/agents/package.json @@ -34,11 +34,14 @@ "api:update": "api-extractor run --local --typescript-compiler-folder ../node_modules/typescript --verbose" }, "devDependencies": { - "@livekit/rtc-node": "^0.13.12", + "@ffmpeg-installer/ffmpeg": "^1.1.0", + "@livekit/rtc-node": "^0.13.13", "@microsoft/api-extractor": "^7.35.0", + "@types/fluent-ffmpeg": "^2.1.28", "@types/json-schema": "^7.0.15", "@types/node": "^22.5.5", "@types/ws": "^8.5.10", + "fluent-ffmpeg": "^2.1.3", "tsup": "^8.4.0", "typescript": "^5.0.0" }, @@ -50,8 +53,8 @@ "commander": "^12.0.0", "heap-js": "^2.6.0", "json-schema": "^0.4.0", - "openai": "^4.91.1", "livekit-server-sdk": "^2.13.3", + "openai": "^4.91.1", "pidusage": "^4.0.1", "pino": "^8.19.0", "pino-pretty": "^11.0.0", diff --git a/agents/resources/NOTICE b/agents/resources/NOTICE new file mode 100644 index 000000000..80e5b76ef --- /dev/null +++ b/agents/resources/NOTICE @@ -0,0 +1,2 @@ +keyboard-typing.ogg by Anton -- https://freesound.org/s/137/ -- License: Attribution 4.0 +keyboard-typing2.opg by Anton -- https://freesound.org/s/137/ -- License: Attribution 4.0 diff --git a/agents/resources/keyboard-typing.ogg b/agents/resources/keyboard-typing.ogg new file mode 100644 index 000000000..a0ea4bb32 Binary files /dev/null and b/agents/resources/keyboard-typing.ogg differ diff --git a/agents/resources/keyboard-typing2.ogg b/agents/resources/keyboard-typing2.ogg new file mode 100644 index 000000000..a0b69969d Binary files /dev/null and b/agents/resources/keyboard-typing2.ogg differ diff --git a/agents/resources/office-ambience.ogg b/agents/resources/office-ambience.ogg new file mode 100644 index 000000000..bbb444705 Binary files /dev/null and b/agents/resources/office-ambience.ogg differ diff --git a/agents/src/audio.ts b/agents/src/audio.ts index 6c816db7c..3f15b06ac 100644 --- a/agents/src/audio.ts +++ b/agents/src/audio.ts @@ -1,10 +1,27 @@ // SPDX-FileCopyrightText: 2024 LiveKit, Inc. // // SPDX-License-Identifier: Apache-2.0 +import ffmpegInstaller from '@ffmpeg-installer/ffmpeg'; import { AudioFrame } from '@livekit/rtc-node'; +import ffmpeg from 'fluent-ffmpeg'; +import type { ReadableStream } from 'node:stream/web'; import { log } from './log.js'; +import { createStreamChannel } from './stream/stream_channel.js'; import type { AudioBuffer } from './utils.js'; +ffmpeg.setFfmpegPath(ffmpegInstaller.path); + +export interface AudioDecodeOptions { + sampleRate?: number; + numChannels?: number; + /** + * Audio format hint (e.g., 'mp3', 'ogg', 'wav', 'opus') + * If not provided, FFmpeg will auto-detect + */ + format?: string; + abortSignal?: AbortSignal; +} + export function calculateAudioDurationSeconds(frame: AudioBuffer) { // TODO(AJS-102): use frame.durationMs once available in rtc-node return Array.isArray(frame) @@ -72,3 +89,117 @@ export class AudioByteStream { return frames; } } + +/** + * Decode an audio file into AudioFrame instances + * + * @param filePath - Path to the audio file + * @param options - Decoding options + * @returns AsyncGenerator that yields AudioFrame objects + * + * @example + * ```typescript + * for await (const frame of audioFramesFromFile('audio.ogg', { sampleRate: 48000 })) { + * console.log('Frame:', frame.samplesPerChannel, 'samples'); + * } + * ``` + */ +export function audioFramesFromFile( + filePath: string, + options: AudioDecodeOptions = {}, +): ReadableStream { + const sampleRate = options.sampleRate ?? 48000; + const numChannels = options.numChannels ?? 1; + + const audioStream = new AudioByteStream(sampleRate, numChannels); + const channel = createStreamChannel(); + const logger = log(); + + // TODO (Brian): decode WAV using a custom decoder instead of FFmpeg + const command = ffmpeg(filePath) + .inputOptions([ + '-probesize', + '32', + '-analyzeduration', + '0', + '-fflags', + '+nobuffer+flush_packets', + '-flags', + 'low_delay', + ]) + .format('s16le') // signed 16-bit little-endian PCM to be consistent cross-platform + .audioChannels(numChannels) + .audioFrequency(sampleRate); + + let commandRunning = true; + + const onClose = () => { + logger.debug('Audio file playback aborted'); + + channel.close(); + if (commandRunning) { + commandRunning = false; + command.kill('SIGKILL'); + } + }; + + const outputStream = command.pipe(); + options.abortSignal?.addEventListener('abort', onClose, { once: true }); + + outputStream.on('data', (chunk: Buffer) => { + const arrayBuffer = chunk.buffer.slice( + chunk.byteOffset, + chunk.byteOffset + chunk.byteLength, + ) as ArrayBuffer; + + const frames = audioStream.write(arrayBuffer); + for (const frame of frames) { + channel.write(frame); + } + }); + + outputStream.on('end', () => { + const frames = audioStream.flush(); + for (const frame of frames) { + channel.write(frame); + } + commandRunning = false; + channel.close(); + }); + + outputStream.on('error', (err: Error) => { + logger.error(err); + commandRunning = false; + onClose(); + }); + + return channel.stream(); +} + +/** + * Loop audio frames from a file indefinitely + * + * @param filePath - Path to the audio file + * @param options - Decoding options + * @returns AsyncGenerator that yields AudioFrame objects in an infinite loop + */ +export async function* loopAudioFramesFromFile( + filePath: string, + options: AudioDecodeOptions = {}, +): AsyncGenerator { + const frames: AudioFrame[] = []; + const logger = log(); + + for await (const frame of audioFramesFromFile(filePath, options)) { + frames.push(frame); + yield frame; + } + + while (!options.abortSignal?.aborted) { + for (const frame of frames) { + yield frame; + } + } + + logger.debug('Audio file playback loop finished'); +} diff --git a/agents/src/stream/stream_channel.test.ts b/agents/src/stream/stream_channel.test.ts index 2914d47e5..7acad4e93 100644 --- a/agents/src/stream/stream_channel.test.ts +++ b/agents/src/stream/stream_channel.test.ts @@ -126,4 +126,41 @@ describe('StreamChannel', () => { const nextResult = await reader.read(); expect(nextResult.done).toBe(true); }); + + it('should gracefully handle close while read is pending', async () => { + const channel = createStreamChannel(); + const reader = channel.stream().getReader(); + + const readPromise = reader.read(); + + await channel.close(); + + const result = await readPromise; + expect(result.done).toBe(true); + expect(result.value).toBeUndefined(); + }); + + it('should complete all pending reads when closed', async () => { + const channel = createStreamChannel(); + const reader = channel.stream().getReader(); + + const read1 = reader.read(); + const read2 = reader.read(); + const read3 = reader.read(); + + await channel.write(42); + await channel.write(43); + await channel.close(); + + const result1 = await read1; + expect(result1.done).toBe(false); + expect(result1.value).toBe(42); + + const result2 = await read2; + expect(result2.done).toBe(false); + expect(result2.value).toBe(43); + + const result3 = await read3; + expect(result3.done).toBe(true); + }); }); diff --git a/examples/src/play_local_audio_file.ts b/examples/src/play_local_audio_file.ts new file mode 100644 index 000000000..447f5ec6c --- /dev/null +++ b/examples/src/play_local_audio_file.ts @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: 2025 LiveKit, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +import { + type JobContext, + WorkerOptions, + cli, + defineAgent, + log, + loopAudioFramesFromFile, +} from '@livekit/agents'; +import { AudioSource, LocalAudioTrack, TrackPublishOptions, TrackSource } from '@livekit/rtc-node'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +export default defineAgent({ + entry: async (ctx: JobContext) => { + const logger = log(); + + await ctx.connect(); + + logger.info('Playing audio file to LiveKit track...'); + + const audioSource = new AudioSource(48000, 1); + + const track = LocalAudioTrack.createAudioTrack('background_audio', audioSource); + + const publication = await ctx.room.localParticipant!.publishTrack( + track, + new TrackPublishOptions({ + source: TrackSource.SOURCE_MICROPHONE, + }), + ); + + await publication.waitForSubscription(); + + logger.info(`Audio track published: ${publication?.sid}`); + + const currentDir = dirname(fileURLToPath(import.meta.url)); + const resourcesPath = join(currentDir, '../../agents/resources'); + const audioFile = join(resourcesPath, 'office-ambience.ogg'); + + logger.info(`Playing: ${audioFile}`); + + const abortController = new AbortController(); + + ctx.addShutdownCallback(async () => { + abortController.abort(); + }); + + let frameCount = 0; + for await (const frame of loopAudioFramesFromFile(audioFile, { + sampleRate: 48000, + numChannels: 1, + abortSignal: abortController.signal, + })) { + await audioSource.captureFrame(frame); + frameCount++; + + if (frameCount % 100 === 0) { + logger.info(`Played ${frameCount} frames (${(frameCount * 0.1).toFixed(1)}s)`); + } + } + }, +}); + +cli.runApp(new WorkerOptions({ agent: fileURLToPath(import.meta.url) })); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7b9be53ec..33169599f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -143,12 +143,18 @@ importers: specifier: ^3.24.6 version: 3.24.6(zod@3.23.8) devDependencies: + '@ffmpeg-installer/ffmpeg': + specifier: ^1.1.0 + version: 1.1.0 '@livekit/rtc-node': - specifier: ^0.13.12 + specifier: ^0.13.13 version: 0.13.13 '@microsoft/api-extractor': specifier: ^7.35.0 version: 7.43.7(@types/node@22.15.30) + '@types/fluent-ffmpeg': + specifier: ^2.1.28 + version: 2.1.28 '@types/json-schema': specifier: ^7.0.15 version: 7.0.15 @@ -158,6 +164,9 @@ importers: '@types/ws': specifier: ^8.5.10 version: 8.5.10 + fluent-ffmpeg: + specifier: ^2.1.3 + version: 2.1.3 tsup: specifier: ^8.4.0 version: 8.4.0(@microsoft/api-extractor@7.43.7(@types/node@22.15.30))(postcss@8.4.38)(tsx@4.20.4)(typescript@5.4.5) @@ -1253,6 +1262,49 @@ packages: resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@ffmpeg-installer/darwin-arm64@4.1.5': + resolution: {integrity: sha512-hYqTiP63mXz7wSQfuqfFwfLOfwwFChUedeCVKkBtl/cliaTM7/ePI9bVzfZ2c+dWu3TqCwLDRWNSJ5pqZl8otA==} + cpu: [arm64] + os: [darwin] + + '@ffmpeg-installer/darwin-x64@4.1.0': + resolution: {integrity: sha512-Z4EyG3cIFjdhlY8wI9aLUXuH8nVt7E9SlMVZtWvSPnm2sm37/yC2CwjUzyCQbJbySnef1tQwGG2Sx+uWhd9IAw==} + cpu: [x64] + os: [darwin] + + '@ffmpeg-installer/ffmpeg@1.1.0': + resolution: {integrity: sha512-Uq4rmwkdGxIa9A6Bd/VqqYbT7zqh1GrT5/rFwCwKM70b42W5gIjWeVETq6SdcL0zXqDtY081Ws/iJWhr1+xvQg==} + + '@ffmpeg-installer/linux-arm64@4.1.4': + resolution: {integrity: sha512-dljEqAOD0oIM6O6DxBW9US/FkvqvQwgJ2lGHOwHDDwu/pX8+V0YsDL1xqHbj1DMX/+nP9rxw7G7gcUvGspSoKg==} + cpu: [arm64] + os: [linux] + + '@ffmpeg-installer/linux-arm@4.1.3': + resolution: {integrity: sha512-NDf5V6l8AfzZ8WzUGZ5mV8O/xMzRag2ETR6+TlGIsMHp81agx51cqpPItXPib/nAZYmo55Bl2L6/WOMI3A5YRg==} + cpu: [arm] + os: [linux] + + '@ffmpeg-installer/linux-ia32@4.1.0': + resolution: {integrity: sha512-0LWyFQnPf+Ij9GQGD034hS6A90URNu9HCtQ5cTqo5MxOEc7Rd8gLXrJvn++UmxhU0J5RyRE9KRYstdCVUjkNOQ==} + cpu: [ia32] + os: [linux] + + '@ffmpeg-installer/linux-x64@4.1.0': + resolution: {integrity: sha512-Y5BWhGLU/WpQjOArNIgXD3z5mxxdV8c41C+U15nsE5yF8tVcdCGet5zPs5Zy3Ta6bU7haGpIzryutqCGQA/W8A==} + cpu: [x64] + os: [linux] + + '@ffmpeg-installer/win32-ia32@4.1.0': + resolution: {integrity: sha512-FV2D7RlaZv/lrtdhaQ4oETwoFUsUjlUiasiZLDxhEUPdNDWcH1OU9K1xTvqz+OXLdsmYelUDuBS/zkMOTtlUAw==} + cpu: [ia32] + os: [win32] + + '@ffmpeg-installer/win32-x64@4.1.0': + resolution: {integrity: sha512-Drt5u2vzDnIONf4ZEkKtFlbvwj6rI3kxw1Ck9fpudmtgaZIHD4ucsWB2lCZBXRxJgXR+2IMSti+4rtM4C4rXgg==} + cpu: [x64] + os: [win32] + '@google/genai@1.13.0': resolution: {integrity: sha512-BxilXzE8cJ0zt5/lXk6KwuBcIT9P2Lbi2WXhwWMbxf1RNeC68/8DmYQqMrzQP333CieRMdbDXs0eNCphLoScWg==} engines: {node: '>=20.0.0'} @@ -1316,78 +1368,92 @@ packages: resolution: {integrity: sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.0': resolution: {integrity: sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.0': resolution: {integrity: sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.0': resolution: {integrity: sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.0': resolution: {integrity: sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.0': resolution: {integrity: sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.0': resolution: {integrity: sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-linux-arm64@0.34.3': resolution: {integrity: sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-linux-arm@0.34.3': resolution: {integrity: sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-linux-ppc64@0.34.3': resolution: {integrity: sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-linux-s390x@0.34.3': resolution: {integrity: sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-linux-x64@0.34.3': resolution: {integrity: sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.3': resolution: {integrity: sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-linuxmusl-x64@0.34.3': resolution: {integrity: sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-wasm32@0.34.3': resolution: {integrity: sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==} @@ -1512,12 +1578,14 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@livekit/rtc-node-linux-x64-gnu@0.13.13': resolution: {integrity: sha512-B/SgbeBRobpA5LqmDEoBJHpRXePpoF4RO4F0zJf9BdkDhOR0j77p6hD0ZiOuPTRoBzUqukpsTszp+lZnHoNmiA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@livekit/rtc-node-win32-x64-msvc@0.13.13': resolution: {integrity: sha512-ygVYV4eHczs3QdaW/p0ADhhm7InUDhFaCYk8OzzIn056ZibZPXzvPizCThZqs8VsDniA01MraZF3qhZZb8IyRg==} @@ -1658,101 +1726,121 @@ packages: resolution: {integrity: sha512-3reX2fUHqN7sffBNqmEyMQVj/CKhIHZd4y631duy0hZqI8Qoqf6lTtmAKvJFYa6bhU95B1D0WgzHkmTg33In0A==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-gnueabihf@4.40.0': resolution: {integrity: sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.17.2': resolution: {integrity: sha512-uSqpsp91mheRgw96xtyAGP9FW5ChctTFEoXP0r5FAzj/3ZRv3Uxjtc7taRQSaQM/q85KEKjKsZuiZM3GyUivRg==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm-musleabihf@4.40.0': resolution: {integrity: sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.17.2': resolution: {integrity: sha512-EMMPHkiCRtE8Wdk3Qhtciq6BndLtstqZIroHiiGzB3C5LDJmIZcSzVtLRbwuXuUft1Cnv+9fxuDtDxz3k3EW2A==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-gnu@4.40.0': resolution: {integrity: sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.17.2': resolution: {integrity: sha512-NMPylUUZ1i0z/xJUIx6VUhISZDRT+uTWpBcjdv0/zkp7b/bQDF+NfnfdzuTiB1G6HTodgoFa93hp0O1xl+/UbA==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-musl@4.40.0': resolution: {integrity: sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loongarch64-gnu@4.40.0': resolution: {integrity: sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-powerpc64le-gnu@4.17.2': resolution: {integrity: sha512-T19My13y8uYXPw/L/k0JYaX1fJKFT/PWdXiHr8mTbXWxjVF1t+8Xl31DgBBvEKclw+1b00Chg0hxE2O7bTG7GQ==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-powerpc64le-gnu@4.40.0': resolution: {integrity: sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.17.2': resolution: {integrity: sha512-BOaNfthf3X3fOWAB+IJ9kxTgPmMqPPH5f5k2DcCsRrBIbWnaJCgX2ll77dV1TdSy9SaXTR5iDXRL8n7AnoP5cg==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.40.0': resolution: {integrity: sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.40.0': resolution: {integrity: sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.17.2': resolution: {integrity: sha512-W0UP/x7bnn3xN2eYMql2T/+wpASLE5SjObXILTMPUBDB/Fg/FxC+gX4nvCfPBCbNhz51C+HcqQp2qQ4u25ok6g==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-s390x-gnu@4.40.0': resolution: {integrity: sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.17.2': resolution: {integrity: sha512-Hy7pLwByUOuyaFC6mAr7m+oMC+V7qyifzs/nW2OJfC8H4hbCzOX07Ov0VFk/zP3kBsELWNFi7rJtgbKYsav9QQ==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.40.0': resolution: {integrity: sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.17.2': resolution: {integrity: sha512-h1+yTWeYbRdAyJ/jMiVw0l6fOOm/0D1vNLui9iPuqgRGnXA0u21gAqOyB5iHjlM9MMfNOm9RHCQ7zLIzT0x11Q==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-linux-x64-musl@4.40.0': resolution: {integrity: sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-win32-arm64-msvc@4.17.2': resolution: {integrity: sha512-tmdtXMfKAjy5+IQsVtDiCfqbynAQE/TQRpWdVataHmhMb9DCoJxp9vLcCBjEQWMiUYxO1QprH/HbY9ragCEFLA==} @@ -1853,6 +1941,9 @@ packages: '@types/estree@1.0.7': resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} + '@types/fluent-ffmpeg@2.1.28': + resolution: {integrity: sha512-5ovxsDwBcPfJ+eYs1I/ZpcYCnkce7pvH9AHSvrZllAp1ZPpTRDZAFjF3TRFbukxSgIYTTNYePbS0rKUmaxVbXw==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -2120,6 +2211,9 @@ packages: ast-v8-to-istanbul@0.3.3: resolution: {integrity: sha512-MuXMrSLVVoA6sYN/6Hke18vMzrT4TZNbZIj/hvh0fnYFpO+/kFXcLIaiPwXXWaQUPg4yJD8fj+lfJ7/1EBconw==} + async@0.2.10: + resolution: {integrity: sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ==} + asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} @@ -2787,6 +2881,11 @@ packages: flatted@3.3.1: resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} + fluent-ffmpeg@2.1.3: + resolution: {integrity: sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q==} + engines: {node: '>=18'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} @@ -4466,6 +4565,10 @@ packages: resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==} engines: {node: '>= 0.4'} + which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -5055,6 +5158,41 @@ snapshots: '@eslint/js@8.57.0': {} + '@ffmpeg-installer/darwin-arm64@4.1.5': + optional: true + + '@ffmpeg-installer/darwin-x64@4.1.0': + optional: true + + '@ffmpeg-installer/ffmpeg@1.1.0': + optionalDependencies: + '@ffmpeg-installer/darwin-arm64': 4.1.5 + '@ffmpeg-installer/darwin-x64': 4.1.0 + '@ffmpeg-installer/linux-arm': 4.1.3 + '@ffmpeg-installer/linux-arm64': 4.1.4 + '@ffmpeg-installer/linux-ia32': 4.1.0 + '@ffmpeg-installer/linux-x64': 4.1.0 + '@ffmpeg-installer/win32-ia32': 4.1.0 + '@ffmpeg-installer/win32-x64': 4.1.0 + + '@ffmpeg-installer/linux-arm64@4.1.4': + optional: true + + '@ffmpeg-installer/linux-arm@4.1.3': + optional: true + + '@ffmpeg-installer/linux-ia32@4.1.0': + optional: true + + '@ffmpeg-installer/linux-x64@4.1.0': + optional: true + + '@ffmpeg-installer/win32-ia32@4.1.0': + optional: true + + '@ffmpeg-installer/win32-x64@4.1.0': + optional: true + '@google/genai@1.13.0': dependencies: google-auth-library: 9.15.1 @@ -5591,6 +5729,10 @@ snapshots: '@types/estree@1.0.7': {} + '@types/fluent-ffmpeg@2.1.28': + dependencies: + '@types/node': 22.15.30 + '@types/json-schema@7.0.15': {} '@types/json5@0.0.29': {} @@ -5940,6 +6082,8 @@ snapshots: estree-walker: 3.0.3 js-tokens: 9.0.1 + async@0.2.10: {} + asynckit@0.4.0: {} atomic-sleep@1.0.0: {} @@ -6774,6 +6918,11 @@ snapshots: flatted@3.3.1: {} + fluent-ffmpeg@2.1.3: + dependencies: + async: 0.2.10 + which: 1.3.1 + for-each@0.3.3: dependencies: is-callable: 1.2.7 @@ -8589,6 +8738,10 @@ snapshots: gopd: 1.0.1 has-tostringtag: 1.0.2 + which@1.3.1: + dependencies: + isexe: 2.0.0 + which@2.0.2: dependencies: isexe: 2.0.0