├── CLAUDE.md ├── .gitignore ├── bin └── audiotee ├── .prettierrc.json ├── src ├── index.ts ├── types.ts └── audiotee.ts ├── tsup.config.ts ├── examples └── assemblyai-universal-streaming │ ├── package.json │ ├── README.md │ ├── index.ts │ └── package-lock.json ├── tsconfig.json ├── package.json ├── README.md └── .cursorrules /CLAUDE.md: -------------------------------------------------------------------------------- 1 | .cursorrules -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | -------------------------------------------------------------------------------- /bin/audiotee: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makeusabrew/audioteejs/HEAD/bin/audiotee -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "semi": false, 4 | "singleQuote": true, 5 | "printWidth": 120 6 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { AudioTee } from './audiotee.js' 2 | export type { AudioTeeOptions, AudioChunk, LogLevel, MessageData, LogMessage, AudioTeeEvents } from './types.js' 3 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts'], 5 | format: ['esm'], 6 | dts: true, 7 | sourcemap: true, 8 | clean: true, 9 | minify: false, 10 | target: 'es2022', 11 | }) 12 | -------------------------------------------------------------------------------- /examples/assemblyai-universal-streaming/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "assemblyai-universal-streaming", 3 | "version": "0.0.0", 4 | "description": "AudioTee to AssemblyAI Universal Streaming example", 5 | "author": "", 6 | "type": "module", 7 | "main": "index.ts", 8 | "scripts": { 9 | "start": "tsx index.ts", 10 | "dev": "tsx watch index.ts" 11 | }, 12 | "dependencies": { 13 | "assemblyai": "^4.14.0", 14 | "tsx": "^4.19.2" 15 | }, 16 | "devDependencies": { 17 | "@types/node": "^22.10.2", 18 | "typescript": "^5.7.3" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "outDir": "dist", 7 | "rootDir": "src", 8 | "declaration": true, 9 | "declarationMap": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "skipLibCheck": true, 15 | "forceConsistentCasingInFileNames": true 16 | }, 17 | "include": ["src/**/*"], 18 | "exclude": ["node_modules", "dist", "**/*.test.ts"] 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "audiotee", 3 | "version": "0.0.7", 4 | "type": "module", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "license": "MIT", 8 | "exports": { 9 | ".": { 10 | "types": "./dist/index.d.ts", 11 | "import": "./dist/index.js", 12 | "require": "./dist/index.js" 13 | } 14 | }, 15 | "files": [ 16 | "dist/", 17 | "bin/" 18 | ], 19 | "scripts": { 20 | "build": "tsup", 21 | "dev": "tsup --watch", 22 | "test": "vitest", 23 | "prepublishOnly": "npm run build" 24 | }, 25 | "engines": { 26 | "node": ">=20" 27 | }, 28 | "devDependencies": { 29 | "@types/node": "^24.1.0", 30 | "tsup": "^8.5.0", 31 | "typescript": "^5.8.3" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface AudioTeeOptions { 2 | sampleRate?: number 3 | chunkDurationMs?: number 4 | mute?: boolean 5 | includeProcesses?: number[] 6 | excludeProcesses?: number[] 7 | binaryPath?: string 8 | } 9 | 10 | export interface AudioChunk { 11 | data: Buffer 12 | } 13 | 14 | export type MessageType = 'metadata' | 'stream_start' | 'stream_stop' | 'info' | 'error' | 'debug' 15 | 16 | export type LogLevel = 'info' | 'debug' 17 | 18 | export interface MessageData { 19 | message: string 20 | context?: Record 21 | } 22 | 23 | export interface LogMessage { 24 | timestamp: string 25 | message_type: MessageType 26 | data: MessageData 27 | } 28 | 29 | export interface AudioTeeEvents { 30 | data: (chunk: AudioChunk) => void 31 | start: () => void 32 | stop: () => void 33 | error: (error: Error) => void 34 | log: (level: LogLevel, message: MessageData) => void 35 | } 36 | -------------------------------------------------------------------------------- /examples/assemblyai-universal-streaming/README.md: -------------------------------------------------------------------------------- 1 | # AudioTee → AssemblyAI Universal Streaming Example 2 | 3 | This example demonstrates how to stream macOS system audio captured by AudioTee directly to AssemblyAI's Universal Streaming API for real-time speech transcription. 4 | 5 | ## What this does 6 | 7 | 1. **Captures system audio** using AudioTee (whatever's playing on your Mac) 8 | 2. **Streams it in real-time** to AssemblyAI's Universal Streaming API 9 | 3. **Displays live transcriptions** with partial results updating in real-time 10 | 4. **Shows final formatted transcripts** when AssemblyAI detects end-of-turn 11 | 12 | It is no more than a quick proof of concept. It is **not** production ready. 13 | 14 | ## Setup 15 | 16 | 1. **Install dependencies**: 17 | 18 | ```bash 19 | npm install 20 | ``` 21 | 22 | 2. **Get your AssemblyAI API key**: 23 | 24 | - Sign up at [AssemblyAI](https://www.assemblyai.com/) 25 | - Get your API key from the dashboard 26 | 27 | 3. **Set your API key**: 28 | 29 | ```bash 30 | export ASSEMBLYAI_API_KEY="your-api-key-here" 31 | ``` 32 | 33 | ## Usage 34 | 35 | ```bash 36 | npm start 37 | ``` 38 | 39 | Or for development with auto-restart: 40 | 41 | ```bash 42 | npm run dev 43 | ``` 44 | 45 | ## What you'll see 46 | 47 | ``` 48 | Starting AudioTee -> AssemblyAI Universal Streaming example 49 | Press Ctrl+C to stop 50 | 51 | Connecting to AssemblyAI... 52 | Starting system audio capture... 53 | 54 | AssemblyAI session opened with ID: abc123... 55 | AudioTee started capturing system audio 56 | Listening for system audio... 57 | 58 | hello this is a test of the system # <- partial transcript (updates in real-time) 59 | Hello, this is a test of the system. # <- final formatted transcript 60 | ``` 61 | 62 | ## Configuration 63 | 64 | The example is configured with optimal settings for system audio transcription: 65 | 66 | - **16kHz sample rate**: Balances quality and processing speed 67 | - **50ms chunks**: AssemblyAI's recommended chunk size for minimal latency 68 | - **Format turns enabled**: Gets punctuated, properly formatted final transcripts 69 | - **PCM S16LE encoding**: 16-bit audio format 70 | 71 | You can modify these settings in `index.ts`: 72 | 73 | ```typescript 74 | const audioTee = new AudioTee({ 75 | sampleRate: 16000, // Audio sample rate 76 | chunkDuration: 0.05, // 50ms chunks 77 | }) 78 | 79 | const transcriber = client.streaming.transcriber({ 80 | sampleRate: 16000, 81 | formatTurns: true, // Enable formatted final transcripts 82 | encoding: 'pcm_s16le', 83 | }) 84 | ``` 85 | 86 | ## Requirements 87 | 88 | - macOS 14.2+ 89 | - Node.js 18+ 90 | - AssemblyAI API key 91 | - System audio recording permissions (prompted on first run) 92 | 93 | ## Troubleshooting 94 | 95 | - **No audio detected**: Ensure something is playing audio on your Mac and that AudioTee has permission to access system audio 96 | -------------------------------------------------------------------------------- /examples/assemblyai-universal-streaming/index.ts: -------------------------------------------------------------------------------- 1 | import { AudioChunk, AudioTee, LogLevel, MessageData } from '../../dist/index.js' 2 | import { AssemblyAI, TurnEvent } from 'assemblyai' 3 | 4 | // Replace with your actual AssemblyAI API key 5 | const API_KEY = process.env.ASSEMBLYAI_API_KEY 6 | 7 | const SAMPLE_RATE = 16000 8 | 9 | async function run() { 10 | if (!API_KEY) { 11 | console.error('Please set your AssemblyAI API key in the ASSEMBLYAI_API_KEY environment variable') 12 | process.exit(1) 13 | } 14 | 15 | console.log('Starting AudioTee -> AssemblyAI Universal Streaming example') 16 | console.log('Press Ctrl+C to stop') 17 | 18 | // Initialize AssemblyAI client 19 | const client = new AssemblyAI({ 20 | apiKey: API_KEY, 21 | }) 22 | 23 | // Create streaming transcriber with optimized settings for system audio 24 | const transcriber = client.streaming.transcriber({ 25 | sampleRate: SAMPLE_RATE, 26 | formatTurns: true, 27 | encoding: 'pcm_s16le', 28 | }) 29 | 30 | // Initialize AudioTee with matching sample rate and optimized chunk duration 31 | const audioTee = new AudioTee({ 32 | sampleRate: SAMPLE_RATE, // Match AssemblyAI's expected sample rate 33 | chunkDurationMs: 50, // 50ms chunks as recommended by AssemblyAI 34 | }) 35 | 36 | // Set up AssemblyAI transcriber event handlers 37 | transcriber.on('open', ({ id }: { id: string }) => { 38 | console.log(`AssemblyAI session opened with ID: ${id}`) 39 | console.log('Listening for system audio...\n') 40 | }) 41 | 42 | transcriber.on('error', (error: Error) => { 43 | console.error('AssemblyAI Error:', error) 44 | }) 45 | 46 | transcriber.on('close', (code: number, reason: string) => { 47 | console.log(`AssemblyAI session closed: ${code} - ${reason}`) 48 | }) 49 | 50 | transcriber.on('turn', (turn: TurnEvent) => { 51 | if (!turn.transcript || turn.transcript.trim() === '') { 52 | return 53 | } 54 | 55 | // Show partial transcripts in real-time 56 | if (!turn.end_of_turn || !turn.turn_is_formatted) { 57 | // Clear line and show partial transcript 58 | process.stdout.write(`\r${turn.transcript}`) 59 | } else { 60 | // Show final formatted transcript 61 | process.stdout.write(`\r${' '.repeat(100)}\r`) // Clear line 62 | console.log(`${turn.transcript}`) 63 | } 64 | }) 65 | 66 | audioTee.on('start', () => { 67 | console.log('AudioTee: Audio capture started') 68 | }) 69 | 70 | audioTee.on('stop', () => { 71 | console.log('AudioTee: Audio capture stopped') 72 | }) 73 | 74 | audioTee.on('error', (error: Error) => { 75 | console.error('AudioTee Error:', error) 76 | }) 77 | 78 | audioTee.on('log', (level: LogLevel, message: MessageData) => { 79 | // Log different levels with appropriate formatting 80 | if (level === 'debug') { 81 | console.debug(`AudioTee [DEBUG]: ${message.message}`) 82 | } else if (level === 'info') { 83 | console.info(`AudioTee [INFO]: ${message.message}`) 84 | } 85 | }) 86 | 87 | // Pipe AudioTee data to AssemblyAI 88 | audioTee.on('data', (chunk: AudioChunk) => { 89 | // Send audio data to AssemblyAI for transcription 90 | transcriber.sendAudio(chunk.data) 91 | }) 92 | 93 | // Handle graceful shutdown 94 | let isShuttingDown = false 95 | 96 | const shutdown = async () => { 97 | if (isShuttingDown) return 98 | isShuttingDown = true 99 | 100 | console.log('Shutting down...') 101 | 102 | try { 103 | // Stop AudioTee first 104 | await audioTee.stop() 105 | 106 | // Close AssemblyAI transcriber 107 | await transcriber.close() 108 | 109 | console.log('Cleanup complete') 110 | } catch (error) { 111 | console.error('Error during shutdown:', error) 112 | } 113 | 114 | process.exit(0) 115 | } 116 | 117 | process.on('SIGINT', shutdown) 118 | process.on('SIGTERM', shutdown) 119 | 120 | try { 121 | // Connect to AssemblyAI 122 | console.log('Connecting to AssemblyAI...') 123 | await transcriber.connect() 124 | 125 | // Start AudioTee 126 | console.log('Starting system audio capture...') 127 | await audioTee.start() 128 | } catch (error) { 129 | console.error('Failed to start:', error) 130 | await shutdown() 131 | } 132 | } 133 | 134 | // Start the example 135 | run().catch(console.error) 136 | -------------------------------------------------------------------------------- /src/audiotee.ts: -------------------------------------------------------------------------------- 1 | import { spawn, ChildProcess } from 'child_process' 2 | import { EventEmitter } from 'events' 3 | import path, { join } from 'path' 4 | import type { AudioTeeOptions, LogMessage, AudioTeeEvents } from './types.js' 5 | import { fileURLToPath } from 'url' 6 | 7 | const __dirname = path.dirname(fileURLToPath(import.meta.url)) 8 | 9 | // FIXME: not emitting start, stop, or any events really 10 | export class AudioTee { 11 | private events = new EventEmitter() 12 | private process: ChildProcess | null = null 13 | private isRunning = false 14 | private options: AudioTeeOptions 15 | 16 | constructor(options: AudioTeeOptions = {}) { 17 | this.options = options 18 | } 19 | 20 | on(event: K, listener: AudioTeeEvents[K]): this { 21 | this.events.on(event, listener) 22 | return this 23 | } 24 | 25 | once(event: K, listener: AudioTeeEvents[K]): this { 26 | this.events.once(event, listener) 27 | return this 28 | } 29 | 30 | off(event: K, listener: AudioTeeEvents[K]): this { 31 | this.events.off(event, listener) 32 | return this 33 | } 34 | 35 | removeAllListeners(event?: K): this { 36 | this.events.removeAllListeners(event) 37 | return this 38 | } 39 | 40 | private emit(event: K, ...args: Parameters): boolean { 41 | return this.events.emit(event, ...args) 42 | } 43 | 44 | private buildArguments(): string[] { 45 | const args: string[] = [] 46 | 47 | if (this.options.sampleRate !== undefined) { 48 | args.push('--sample-rate', this.options.sampleRate.toString()) 49 | } 50 | 51 | if (this.options.chunkDurationMs !== undefined) { 52 | // the underlying audiotee binary still expects the chunk duration in seconds 53 | args.push('--chunk-duration', (this.options.chunkDurationMs / 1000).toString()) 54 | } 55 | 56 | if (this.options.mute) { 57 | args.push('--mute') 58 | } 59 | 60 | if (this.options.includeProcesses && this.options.includeProcesses.length > 0) { 61 | args.push('--include-processes', ...this.options.includeProcesses.map((p) => p.toString())) 62 | } 63 | 64 | if (this.options.excludeProcesses && this.options.excludeProcesses.length > 0) { 65 | args.push('--exclude-processes', ...this.options.excludeProcesses.map((p) => p.toString())) 66 | } 67 | 68 | return args 69 | } 70 | 71 | private handleStderr(data: Buffer): void { 72 | const text = data.toString('utf8') 73 | const lines = text.split('\n').filter((line) => line.trim()) 74 | 75 | for (const line of lines) { 76 | try { 77 | const logMessage: LogMessage = JSON.parse(line) 78 | 79 | // Only emit log events for debug and info types 80 | if (logMessage.message_type === 'debug' || logMessage.message_type === 'info') { 81 | this.emit('log', logMessage.message_type, logMessage.data) 82 | } 83 | 84 | // Handle specific message types 85 | if (logMessage.message_type === 'stream_start') { 86 | this.emit('start') 87 | } else if (logMessage.message_type === 'stream_stop') { 88 | this.emit('stop') 89 | } else if (logMessage.message_type === 'error') { 90 | this.emit('error', new Error(logMessage.data.message)) 91 | } 92 | } catch (parseError) { 93 | console.error('Error parsing log message:', parseError) 94 | // TODO: handle this 95 | } 96 | } 97 | } 98 | 99 | start(): Promise { 100 | return new Promise((resolve, reject) => { 101 | if (this.isRunning) { 102 | reject(new Error('AudioTee is already running')) 103 | return 104 | } 105 | 106 | // Check platform at runtime 107 | if (process.platform !== 'darwin') { 108 | reject(new Error(`AudioTee currently only supports macOS (darwin). Current platform: ${process.platform}`)) 109 | return 110 | } 111 | 112 | const binaryPath = this.options.binaryPath ?? join(__dirname, '..', 'bin', 'audiotee') 113 | const args = this.buildArguments() 114 | 115 | this.process = spawn(binaryPath, args) 116 | 117 | this.process.on('error', (error) => { 118 | this.isRunning = false 119 | this.emit('error', error) 120 | reject(error) 121 | }) 122 | 123 | this.process.on('exit', (code, signal) => { 124 | this.isRunning = false 125 | if (code !== 0 && code !== null) { 126 | const error = new Error(`AudioTee process exited with code ${code}`) 127 | this.emit('error', error) 128 | } 129 | }) 130 | 131 | this.process.stdout?.on('data', (data: Buffer) => { 132 | this.emit('data', { data: data }) 133 | }) 134 | 135 | this.process.stderr?.on('data', (data: Buffer) => { 136 | this.handleStderr(data) 137 | }) 138 | 139 | this.isRunning = true 140 | resolve() 141 | }) 142 | } 143 | 144 | stop(): Promise { 145 | return new Promise((resolve) => { 146 | if (!this.isRunning || !this.process) { 147 | resolve() 148 | return 149 | } 150 | 151 | // Force kill after 5 seconds if process doesn't respond 152 | const timeout = setTimeout(() => { 153 | if (this.process && this.isRunning) { 154 | this.process.kill('SIGKILL') 155 | } 156 | }, 5000) 157 | 158 | this.process.once('exit', () => { 159 | clearTimeout(timeout) 160 | this.isRunning = false 161 | this.process = null 162 | resolve() 163 | }) 164 | 165 | this.process.kill('SIGTERM') 166 | }) 167 | } 168 | 169 | isActive(): boolean { 170 | return this.isRunning 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AudioTee.js 2 | 3 | AudioTee.js captures your Mac's system audio output - whatever's playing through your speakers or headphones - and emits it as PCM encoded chunks at regular intervals. It's a tiny Node.js wrapper around the underlying [AudioTee](https://github.com/makeusabrew/audiotee) swift binary, which is [bundled](./bin) in this repository and distributed with the package published to [npm](https://www.npmjs.com/package/audiotee). 4 | 5 | ## About 6 | 7 | [AudioTee](https://github.com/makeusabrew/audiotee) is a standalone swift binary which uses the [Core Audio taps](https://developer.apple.com/documentation/coreaudio/capturing-system-audio-with-core-audio-taps) API introduced in macOS 14.2 to 'tap' whatever's playing through your speakers and write it to `stdout`. AudioTee.js spawns that binary as a child process and relays stdout as `data` events. 8 | 9 | ## Basic usage 10 | 11 | ```ts 12 | import { AudioTee, AudioChunk } from 'audiotee' 13 | 14 | const audiotee = new AudioTee({ sampleRate: 16000 }) 15 | 16 | audiotee.on('data', (chunk: AudioChunk) => { 17 | // chunk.data contains a raw PCM chunk of captured system audio 18 | }) 19 | 20 | await audiotee.start() 21 | // ... later 22 | await audiotee.stop() 23 | ``` 24 | 25 | Unless otherwise specified, AudioTee will capture system audio from all running processes. 26 | 27 | ## Installation 28 | 29 | `npm install audiotee` 30 | 31 | Installation will download a prebuilt universal macOS binary which runs on both Apple and Intel chips, and weighs less than 600Kb. 32 | 33 | ## Options 34 | 35 | The `AudioTee` constructor accepts an optional options object: 36 | 37 | ```ts 38 | interface AudioTeeOptions { 39 | sampleRate?: number // Target sample rate (Hz), default: device default 40 | chunkDurationMs?: number // Duration of each audio chunk in milliseconds, defaults to 200 41 | mute?: boolean // Mute system audio whilst capturing, default: false 42 | includeProcesses?: number[] // Only capture audio from these process IDs 43 | excludeProcesses?: number[] // Exclude audio from these process IDs 44 | } 45 | ``` 46 | 47 | ## Events 48 | 49 | AudioTee uses an EventEmitter interface to stream audio data and system events: 50 | 51 | ```ts 52 | // Audio data events 53 | audiotee.on('data', (chunk: { data: Buffer }) => { 54 | // Raw PCM audio data - mono channel, 32-bit float or 16-bit int depending on conversion 55 | }) 56 | 57 | // Lifecycle events 58 | audiotee.on('start', () => { 59 | // Audio capture has started 60 | }) 61 | 62 | audiotee.on('stop', () => { 63 | // Audio capture has stopped 64 | }) 65 | 66 | // Error handling 67 | audiotee.on('error', (error: Error) => { 68 | // Process errors, permission issues, etc. 69 | }) 70 | 71 | // Logging 72 | audiotee.on('log', (level: LogLevel, message: MessageData) => { 73 | // System logs from the AudioTee binary 74 | // LogLevel: 'info' | 'debug' 75 | // MessageData includes message string and optional context object 76 | }) 77 | ``` 78 | 79 | ### Event details 80 | 81 | - **`data`**: Emitted for each audio chunk. The `data` property contains raw PCM audio bytes 82 | - **`start`**: Emitted when audio capture begins successfully 83 | - **`stop`**: Emitted when audio capture ends 84 | - **`error`**: Emitted for process errors, permission failures, or system issues 85 | - **`log`**: Emitted for debug and info messages from the underlying AudioTee binary 86 | 87 | **Note:** Versions prior to 0.0.5 only emit the `data` event. All other events (`start`, `stop`, `error`, `log`) were fixed in version 0.0.5. 88 | 89 | ## Requirements 90 | 91 | - macOS >= 14.2 92 | 93 | ## API stability 94 | 95 | During the `0.x.x` release, the API is unstable and subject to change without notice. 96 | 97 | ## Best practices 98 | 99 | - Always specify a sample rate. Tell AudioTee what you want, rather than having to parse the 100 | `metadata` message to see what you got from the output device 101 | - Specifying _any_ sample rate automatically switches encoding to use 16-bit signed integers, which is half the byte size and bandwidth compared to the 32-bit float the source stream was probably using 102 | - You'll probably need to specify a different `chunkDuration` depending on your use case. For example, some ASRs are quite particular about the exact length of each chunk they expect to process. 103 | 104 | ## Permissions 105 | 106 | There is no provision in the underlying AudioTee library to pre-emptively check the state of the required `NSAudioCaptureUsageDescription` permission. You _should_ be prompted to grant it the first time AudioTee.js tries to record anything, but at least some popular terminal emulators like iTerm and those built in to VSCode/Cursor don't. They will instead happily start recording total silence. 107 | 108 | You can work around this either by using the built in macOS terminal emulator, or by granting system audio recording permission manually. Open Settings > Privacy & Security > Screen & System Audio Recording and scroll down to the **System Audio Recording Only** section (**not** the top 'Screen & System Audio Recording' section) and add the terminal application you're using. 109 | 110 | ## Code signing 111 | 112 | The AudioTee binary included in this package is ad-hoc signed (unsigned with a developer certificate). This is fine for most use cases because: 113 | 114 | ### For Electron applications 115 | 116 | When the AudioTee binary is bundled inside an Electron app, it **inherits the code signature** from the parent application. You don't need to sign it separately: 117 | 118 | 1. **Automatic inclusion**: The binary at `node_modules/audiotee/bin/audiotee` gets bundled with your app 119 | 2. **Parent signature inheritance**: When you sign your Electron app (with properly configured `electron-builder` or `electron-forge`), all binaries within the app bundle automatically inherit that signature 120 | 3. **Entitlements**: Ensure your app's entitlements are correct. 121 | 122 | ### For standalone usage 123 | 124 | If you're running AudioTee.js outside of a signed parent application (e.g., directly via Node.js in Terminal): 125 | 126 | - **Development**: The ad-hoc signed binary works fine 127 | - **First run**: macOS may show a Gatekeeper warning that can be bypassed via System Settings 128 | - **Production**: Consider signing the binary with your Developer ID if distributing as a standalone tool 129 | 130 | ## License 131 | 132 | ### The MIT License 133 | 134 | Copyright (C) 2025 Nick Payne. 135 | -------------------------------------------------------------------------------- /.cursorrules: -------------------------------------------------------------------------------- 1 | # AudioTee NPM Wrapper Development Guide 2 | 3 | ## Project Overview 4 | 5 | We're creating a TypeScript npm package that wraps the AudioTee Swift binary to provide a clean Node.js interface for capturing macOS system audio. AudioTee is a command-line tool that captures system audio and outputs raw PCM data to stdout, with logs going to stderr. 6 | 7 | ## What We're Building 8 | 9 | A TypeScript npm package that: 10 | 11 | - Wraps the AudioTee Swift binary (600KB universal binary for macOS) 12 | - Provides an EventEmitter-style API for real-time audio capture 13 | - Handles binary execution, argument parsing, and stream processing 14 | - Offers strong TypeScript types for all audio data and options 15 | 16 | ## Current Project Structure 17 | 18 | ``` 19 | audiotee-node/ 20 | ├── src/ 21 | │ ├── index.ts # Main entry point (exports) 22 | │ ├── types.ts # TypeScript type definitions 23 | │ └── audiotee.ts # Main wrapper class (TO BE CREATED) 24 | ├── bin/ 25 | │ └── audiotee # Swift universal binary (600KB) 26 | ├── dist/ # Generated TypeScript output 27 | ├── package.json # ESM-first, Node 18+, tsup bundler 28 | ├── tsconfig.json # Modern config with NodeNext resolution 29 | ├── tsup.config.ts # Modern bundler config 30 | └── .gitignore 31 | ``` 32 | 33 | ## Key Technical Decisions Made 34 | 35 | - **ESM-first package** (`"type": "module"` in package.json) 36 | - **Composition over inheritance** for EventEmitter functionality 37 | - **Universal binary bundling** (600KB is acceptable for npm distribution) 38 | - **tsup for bundling** (faster than tsc, better DX than rollup) 39 | - **Node 20+ minimum** (using modern features) 40 | 41 | ## AudioTee Binary Behavior 42 | 43 | The Swift binary we're wrapping: 44 | 45 | - Captures macOS system audio using Core Audio taps API 46 | - Outputs **raw PCM audio data to stdout** in configurable chunks 47 | - Sends **all logs/metadata to stderr** (clean separation) 48 | - Supports sample rate conversion, process filtering, chunk duration control 49 | - Requires macOS 14.2+ and audio recording permissions 50 | 51 | ### Command Line Interface 52 | 53 | ```bash 54 | # Basic usage 55 | ./audiotee > output.pcm 56 | 57 | # With options 58 | ./audiotee --sample-rate 16000 --chunk-duration 0.1 --mute 59 | 60 | # Process filtering 61 | ./audiotee --include-processes 1234 5678 62 | ./audiotee --exclude-processes 9012 63 | ``` 64 | 65 | ### Audio Output Format 66 | 67 | - **Format**: Raw PCM audio data 68 | - **Channels**: Always mono (1 channel) 69 | - **Sample rate**: Device default (usually 48kHz) or converted 70 | - **Bit depth**: 32-bit float (default) or 16-bit (when converted) 71 | - **Chunk timing**: 200ms default, configurable 0.0-5.0s 72 | 73 | ## Implementation Task: AudioTee Wrapper Class 74 | 75 | ### File: `src/audiotee.ts` 76 | 77 | Create a class that: 78 | 79 | 1. **Manages the binary process lifecycle** 80 | 81 | - Spawns the AudioTee binary with proper arguments 82 | - Handles process startup, shutdown, and error conditions 83 | - Builds command-line arguments from options object 84 | 85 | 2. **Provides EventEmitter interface via composition** 86 | 87 | - `private events = new EventEmitter()` 88 | - Expose `on()`, `once()`, `off()`, `removeAllListeners()` methods 89 | - Private `emit()` method for internal use 90 | 91 | 3. **Processes binary streams** 92 | 93 | - Parse stdout as raw PCM audio chunks 94 | - Parse stderr for logs and error messages 95 | - Emit structured events with proper TypeScript types 96 | 97 | 4. **Handles macOS-specific concerns** 98 | - Permission detection and user guidance 99 | - Process lifecycle management 100 | - Binary path resolution 101 | 102 | ### Expected Event Interface 103 | 104 | ```typescript 105 | interface AudioTeeEvents { 106 | data: (chunk: AudioChunk) => void // Raw PCM audio data 107 | start: () => void // Recording started 108 | stop: () => void // Recording stopped 109 | error: (error: Error) => void // Process/system errors 110 | log: (message: string, level: LogLevel) => void // Binary logs 111 | 'permission-required': () => void // macOS permission prompt 112 | } 113 | ``` 114 | 115 | ### AudioChunk Structure 116 | 117 | Each audio data event should provide: 118 | 119 | ```typescript 120 | interface AudioChunk { 121 | data: Buffer // Raw PCM audio bytes 122 | // open for expansion 123 | } 124 | ``` 125 | 126 | ### Log message structure 127 | 128 | All messages received via stderr **should** be a JSON payload as follows: 129 | 130 | ```typescript 131 | interface LogMessage { 132 | timestamp: Date 133 | message_type: 'metadata' | 'stream_start' | 'stream_stop' | 'info' | 'error' | 'debug' 134 | data: { 135 | message: string 136 | } 137 | } 138 | ``` 139 | 140 | ### Implementation Hints 141 | 142 | 1. **Binary path resolution:** 143 | 144 | ```typescript 145 | const binaryPath = join(__dirname, '..', 'bin', 'audiotee') 146 | ``` 147 | 148 | 2. **Argument building:** 149 | Map options object to command-line flags systematically 150 | 151 | 3. **Stream processing:** 152 | 153 | - stdout = raw PCM audio data → emit as `AudioChunk` 154 | - stderr = logs/errors → parse and emit as `log` events 155 | - Detect permission issues in stderr content 156 | 157 | 4. **Process management:** 158 | 159 | - Use `spawn()` not `exec()` for streaming data 160 | - Handle SIGTERM for clean shutdown 161 | - Track running state to prevent double-start 162 | 163 | 5. **Error handling:** 164 | - Process exit codes 165 | - Permission denied scenarios 166 | - Binary not found or corrupted 167 | 168 | ## Usage Example (Target API) 169 | 170 | ```typescript 171 | import { AudioTee } from 'audiotee' 172 | 173 | const audiotee = new AudioTee({ 174 | sampleRate: 16000, 175 | chunkDurationMs: 100, 176 | mute: true, 177 | }) 178 | 179 | audiotee.on('data', (chunk) => { 180 | // Process raw PCM audio 181 | console.log(`${chunk.data.length} bytes at ${chunk.sampleRate}Hz`) 182 | }) 183 | 184 | audiotee.on('log', (message) => {}) 185 | 186 | audiotee.start() 187 | // Audio chunks will flow via 'data' events 188 | audiotee.stop() 189 | ``` 190 | 191 | ## Development Workflow 192 | 193 | 1. **Implement the wrapper class** in `src/audiotee.ts` 194 | 2. **Update exports** in `src/index.ts` 195 | 3. **Test locally** with `npm run build && node dist/index.js` 196 | 4. **Verify binary execution** and stream processing 197 | 5. **Handle edge cases** (permissions, process crashes, etc.) 198 | 199 | ## Testing Strategy 200 | 201 | - **Unit tests**: Mock child_process for argument building, event emission 202 | - **Integration tests**: Use actual binary with test audio scenarios 203 | - **Permission tests**: Verify behavior when audio permissions missing 204 | - **Error tests**: Binary missing, invalid arguments, process crashes 205 | 206 | ## Distribution Notes 207 | 208 | - Binary ships with npm package (included in `files` array) 209 | - Package size: ~1MB total (600KB binary + ~400KB TypeScript/deps) 210 | - Platform restriction: macOS-only (enforce in package.json) 211 | - Node version: 18+ required for modern features 212 | 213 | This wrapper transforms a command-line audio tool into a modern Node.js streaming interface, handling all the complexity of process management, stream parsing, and platform-specific concerns. 214 | -------------------------------------------------------------------------------- /examples/assemblyai-universal-streaming/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "assemblyai-universal-streaming", 3 | "version": "0.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "assemblyai-universal-streaming", 9 | "version": "0.0.0", 10 | "dependencies": { 11 | "assemblyai": "^4.14.0", 12 | "tsx": "^4.19.2" 13 | }, 14 | "devDependencies": { 15 | "@types/node": "^22.10.2", 16 | "typescript": "^5.7.3" 17 | } 18 | }, 19 | "node_modules/@esbuild/aix-ppc64": { 20 | "version": "0.25.8", 21 | "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", 22 | "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==", 23 | "cpu": [ 24 | "ppc64" 25 | ], 26 | "license": "MIT", 27 | "optional": true, 28 | "os": [ 29 | "aix" 30 | ], 31 | "engines": { 32 | "node": ">=18" 33 | } 34 | }, 35 | "node_modules/@esbuild/android-arm": { 36 | "version": "0.25.8", 37 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz", 38 | "integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==", 39 | "cpu": [ 40 | "arm" 41 | ], 42 | "license": "MIT", 43 | "optional": true, 44 | "os": [ 45 | "android" 46 | ], 47 | "engines": { 48 | "node": ">=18" 49 | } 50 | }, 51 | "node_modules/@esbuild/android-arm64": { 52 | "version": "0.25.8", 53 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz", 54 | "integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==", 55 | "cpu": [ 56 | "arm64" 57 | ], 58 | "license": "MIT", 59 | "optional": true, 60 | "os": [ 61 | "android" 62 | ], 63 | "engines": { 64 | "node": ">=18" 65 | } 66 | }, 67 | "node_modules/@esbuild/android-x64": { 68 | "version": "0.25.8", 69 | "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz", 70 | "integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==", 71 | "cpu": [ 72 | "x64" 73 | ], 74 | "license": "MIT", 75 | "optional": true, 76 | "os": [ 77 | "android" 78 | ], 79 | "engines": { 80 | "node": ">=18" 81 | } 82 | }, 83 | "node_modules/@esbuild/darwin-arm64": { 84 | "version": "0.25.8", 85 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz", 86 | "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==", 87 | "cpu": [ 88 | "arm64" 89 | ], 90 | "license": "MIT", 91 | "optional": true, 92 | "os": [ 93 | "darwin" 94 | ], 95 | "engines": { 96 | "node": ">=18" 97 | } 98 | }, 99 | "node_modules/@esbuild/darwin-x64": { 100 | "version": "0.25.8", 101 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz", 102 | "integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==", 103 | "cpu": [ 104 | "x64" 105 | ], 106 | "license": "MIT", 107 | "optional": true, 108 | "os": [ 109 | "darwin" 110 | ], 111 | "engines": { 112 | "node": ">=18" 113 | } 114 | }, 115 | "node_modules/@esbuild/freebsd-arm64": { 116 | "version": "0.25.8", 117 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz", 118 | "integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==", 119 | "cpu": [ 120 | "arm64" 121 | ], 122 | "license": "MIT", 123 | "optional": true, 124 | "os": [ 125 | "freebsd" 126 | ], 127 | "engines": { 128 | "node": ">=18" 129 | } 130 | }, 131 | "node_modules/@esbuild/freebsd-x64": { 132 | "version": "0.25.8", 133 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz", 134 | "integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==", 135 | "cpu": [ 136 | "x64" 137 | ], 138 | "license": "MIT", 139 | "optional": true, 140 | "os": [ 141 | "freebsd" 142 | ], 143 | "engines": { 144 | "node": ">=18" 145 | } 146 | }, 147 | "node_modules/@esbuild/linux-arm": { 148 | "version": "0.25.8", 149 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz", 150 | "integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==", 151 | "cpu": [ 152 | "arm" 153 | ], 154 | "license": "MIT", 155 | "optional": true, 156 | "os": [ 157 | "linux" 158 | ], 159 | "engines": { 160 | "node": ">=18" 161 | } 162 | }, 163 | "node_modules/@esbuild/linux-arm64": { 164 | "version": "0.25.8", 165 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz", 166 | "integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==", 167 | "cpu": [ 168 | "arm64" 169 | ], 170 | "license": "MIT", 171 | "optional": true, 172 | "os": [ 173 | "linux" 174 | ], 175 | "engines": { 176 | "node": ">=18" 177 | } 178 | }, 179 | "node_modules/@esbuild/linux-ia32": { 180 | "version": "0.25.8", 181 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz", 182 | "integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==", 183 | "cpu": [ 184 | "ia32" 185 | ], 186 | "license": "MIT", 187 | "optional": true, 188 | "os": [ 189 | "linux" 190 | ], 191 | "engines": { 192 | "node": ">=18" 193 | } 194 | }, 195 | "node_modules/@esbuild/linux-loong64": { 196 | "version": "0.25.8", 197 | "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz", 198 | "integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==", 199 | "cpu": [ 200 | "loong64" 201 | ], 202 | "license": "MIT", 203 | "optional": true, 204 | "os": [ 205 | "linux" 206 | ], 207 | "engines": { 208 | "node": ">=18" 209 | } 210 | }, 211 | "node_modules/@esbuild/linux-mips64el": { 212 | "version": "0.25.8", 213 | "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz", 214 | "integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==", 215 | "cpu": [ 216 | "mips64el" 217 | ], 218 | "license": "MIT", 219 | "optional": true, 220 | "os": [ 221 | "linux" 222 | ], 223 | "engines": { 224 | "node": ">=18" 225 | } 226 | }, 227 | "node_modules/@esbuild/linux-ppc64": { 228 | "version": "0.25.8", 229 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz", 230 | "integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==", 231 | "cpu": [ 232 | "ppc64" 233 | ], 234 | "license": "MIT", 235 | "optional": true, 236 | "os": [ 237 | "linux" 238 | ], 239 | "engines": { 240 | "node": ">=18" 241 | } 242 | }, 243 | "node_modules/@esbuild/linux-riscv64": { 244 | "version": "0.25.8", 245 | "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz", 246 | "integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==", 247 | "cpu": [ 248 | "riscv64" 249 | ], 250 | "license": "MIT", 251 | "optional": true, 252 | "os": [ 253 | "linux" 254 | ], 255 | "engines": { 256 | "node": ">=18" 257 | } 258 | }, 259 | "node_modules/@esbuild/linux-s390x": { 260 | "version": "0.25.8", 261 | "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz", 262 | "integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==", 263 | "cpu": [ 264 | "s390x" 265 | ], 266 | "license": "MIT", 267 | "optional": true, 268 | "os": [ 269 | "linux" 270 | ], 271 | "engines": { 272 | "node": ">=18" 273 | } 274 | }, 275 | "node_modules/@esbuild/linux-x64": { 276 | "version": "0.25.8", 277 | "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz", 278 | "integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==", 279 | "cpu": [ 280 | "x64" 281 | ], 282 | "license": "MIT", 283 | "optional": true, 284 | "os": [ 285 | "linux" 286 | ], 287 | "engines": { 288 | "node": ">=18" 289 | } 290 | }, 291 | "node_modules/@esbuild/netbsd-arm64": { 292 | "version": "0.25.8", 293 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz", 294 | "integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==", 295 | "cpu": [ 296 | "arm64" 297 | ], 298 | "license": "MIT", 299 | "optional": true, 300 | "os": [ 301 | "netbsd" 302 | ], 303 | "engines": { 304 | "node": ">=18" 305 | } 306 | }, 307 | "node_modules/@esbuild/netbsd-x64": { 308 | "version": "0.25.8", 309 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz", 310 | "integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==", 311 | "cpu": [ 312 | "x64" 313 | ], 314 | "license": "MIT", 315 | "optional": true, 316 | "os": [ 317 | "netbsd" 318 | ], 319 | "engines": { 320 | "node": ">=18" 321 | } 322 | }, 323 | "node_modules/@esbuild/openbsd-arm64": { 324 | "version": "0.25.8", 325 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz", 326 | "integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==", 327 | "cpu": [ 328 | "arm64" 329 | ], 330 | "license": "MIT", 331 | "optional": true, 332 | "os": [ 333 | "openbsd" 334 | ], 335 | "engines": { 336 | "node": ">=18" 337 | } 338 | }, 339 | "node_modules/@esbuild/openbsd-x64": { 340 | "version": "0.25.8", 341 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz", 342 | "integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==", 343 | "cpu": [ 344 | "x64" 345 | ], 346 | "license": "MIT", 347 | "optional": true, 348 | "os": [ 349 | "openbsd" 350 | ], 351 | "engines": { 352 | "node": ">=18" 353 | } 354 | }, 355 | "node_modules/@esbuild/openharmony-arm64": { 356 | "version": "0.25.8", 357 | "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz", 358 | "integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==", 359 | "cpu": [ 360 | "arm64" 361 | ], 362 | "license": "MIT", 363 | "optional": true, 364 | "os": [ 365 | "openharmony" 366 | ], 367 | "engines": { 368 | "node": ">=18" 369 | } 370 | }, 371 | "node_modules/@esbuild/sunos-x64": { 372 | "version": "0.25.8", 373 | "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz", 374 | "integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==", 375 | "cpu": [ 376 | "x64" 377 | ], 378 | "license": "MIT", 379 | "optional": true, 380 | "os": [ 381 | "sunos" 382 | ], 383 | "engines": { 384 | "node": ">=18" 385 | } 386 | }, 387 | "node_modules/@esbuild/win32-arm64": { 388 | "version": "0.25.8", 389 | "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz", 390 | "integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==", 391 | "cpu": [ 392 | "arm64" 393 | ], 394 | "license": "MIT", 395 | "optional": true, 396 | "os": [ 397 | "win32" 398 | ], 399 | "engines": { 400 | "node": ">=18" 401 | } 402 | }, 403 | "node_modules/@esbuild/win32-ia32": { 404 | "version": "0.25.8", 405 | "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz", 406 | "integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==", 407 | "cpu": [ 408 | "ia32" 409 | ], 410 | "license": "MIT", 411 | "optional": true, 412 | "os": [ 413 | "win32" 414 | ], 415 | "engines": { 416 | "node": ">=18" 417 | } 418 | }, 419 | "node_modules/@esbuild/win32-x64": { 420 | "version": "0.25.8", 421 | "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz", 422 | "integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==", 423 | "cpu": [ 424 | "x64" 425 | ], 426 | "license": "MIT", 427 | "optional": true, 428 | "os": [ 429 | "win32" 430 | ], 431 | "engines": { 432 | "node": ">=18" 433 | } 434 | }, 435 | "node_modules/@types/node": { 436 | "version": "22.16.5", 437 | "resolved": "https://registry.npmjs.org/@types/node/-/node-22.16.5.tgz", 438 | "integrity": "sha512-bJFoMATwIGaxxx8VJPeM8TonI8t579oRvgAuT8zFugJsJZgzqv0Fu8Mhp68iecjzG7cnN3mO2dJQ5uUM2EFrgQ==", 439 | "dev": true, 440 | "license": "MIT", 441 | "dependencies": { 442 | "undici-types": "~6.21.0" 443 | } 444 | }, 445 | "node_modules/assemblyai": { 446 | "version": "4.14.0", 447 | "resolved": "https://registry.npmjs.org/assemblyai/-/assemblyai-4.14.0.tgz", 448 | "integrity": "sha512-+j/ov8z+RF7pY1Crl6yO3dMHsriRT7jQzBE75xF2rFXqaYVjzKc6DPTWB7nLLBHXWbA72Cayk3pg0hl15eQ6fg==", 449 | "license": "MIT", 450 | "dependencies": { 451 | "ws": "^8.18.0" 452 | }, 453 | "engines": { 454 | "node": ">=18" 455 | } 456 | }, 457 | "node_modules/esbuild": { 458 | "version": "0.25.8", 459 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", 460 | "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==", 461 | "hasInstallScript": true, 462 | "license": "MIT", 463 | "bin": { 464 | "esbuild": "bin/esbuild" 465 | }, 466 | "engines": { 467 | "node": ">=18" 468 | }, 469 | "optionalDependencies": { 470 | "@esbuild/aix-ppc64": "0.25.8", 471 | "@esbuild/android-arm": "0.25.8", 472 | "@esbuild/android-arm64": "0.25.8", 473 | "@esbuild/android-x64": "0.25.8", 474 | "@esbuild/darwin-arm64": "0.25.8", 475 | "@esbuild/darwin-x64": "0.25.8", 476 | "@esbuild/freebsd-arm64": "0.25.8", 477 | "@esbuild/freebsd-x64": "0.25.8", 478 | "@esbuild/linux-arm": "0.25.8", 479 | "@esbuild/linux-arm64": "0.25.8", 480 | "@esbuild/linux-ia32": "0.25.8", 481 | "@esbuild/linux-loong64": "0.25.8", 482 | "@esbuild/linux-mips64el": "0.25.8", 483 | "@esbuild/linux-ppc64": "0.25.8", 484 | "@esbuild/linux-riscv64": "0.25.8", 485 | "@esbuild/linux-s390x": "0.25.8", 486 | "@esbuild/linux-x64": "0.25.8", 487 | "@esbuild/netbsd-arm64": "0.25.8", 488 | "@esbuild/netbsd-x64": "0.25.8", 489 | "@esbuild/openbsd-arm64": "0.25.8", 490 | "@esbuild/openbsd-x64": "0.25.8", 491 | "@esbuild/openharmony-arm64": "0.25.8", 492 | "@esbuild/sunos-x64": "0.25.8", 493 | "@esbuild/win32-arm64": "0.25.8", 494 | "@esbuild/win32-ia32": "0.25.8", 495 | "@esbuild/win32-x64": "0.25.8" 496 | } 497 | }, 498 | "node_modules/fsevents": { 499 | "version": "2.3.3", 500 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 501 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 502 | "hasInstallScript": true, 503 | "license": "MIT", 504 | "optional": true, 505 | "os": [ 506 | "darwin" 507 | ], 508 | "engines": { 509 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 510 | } 511 | }, 512 | "node_modules/get-tsconfig": { 513 | "version": "4.10.1", 514 | "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", 515 | "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", 516 | "license": "MIT", 517 | "dependencies": { 518 | "resolve-pkg-maps": "^1.0.0" 519 | }, 520 | "funding": { 521 | "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" 522 | } 523 | }, 524 | "node_modules/resolve-pkg-maps": { 525 | "version": "1.0.0", 526 | "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", 527 | "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", 528 | "license": "MIT", 529 | "funding": { 530 | "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" 531 | } 532 | }, 533 | "node_modules/tsx": { 534 | "version": "4.20.3", 535 | "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.3.tgz", 536 | "integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==", 537 | "license": "MIT", 538 | "dependencies": { 539 | "esbuild": "~0.25.0", 540 | "get-tsconfig": "^4.7.5" 541 | }, 542 | "bin": { 543 | "tsx": "dist/cli.mjs" 544 | }, 545 | "engines": { 546 | "node": ">=18.0.0" 547 | }, 548 | "optionalDependencies": { 549 | "fsevents": "~2.3.3" 550 | } 551 | }, 552 | "node_modules/typescript": { 553 | "version": "5.8.3", 554 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", 555 | "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", 556 | "dev": true, 557 | "license": "Apache-2.0", 558 | "bin": { 559 | "tsc": "bin/tsc", 560 | "tsserver": "bin/tsserver" 561 | }, 562 | "engines": { 563 | "node": ">=14.17" 564 | } 565 | }, 566 | "node_modules/undici-types": { 567 | "version": "6.21.0", 568 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", 569 | "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", 570 | "dev": true, 571 | "license": "MIT" 572 | }, 573 | "node_modules/ws": { 574 | "version": "8.18.3", 575 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", 576 | "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", 577 | "license": "MIT", 578 | "engines": { 579 | "node": ">=10.0.0" 580 | }, 581 | "peerDependencies": { 582 | "bufferutil": "^4.0.1", 583 | "utf-8-validate": ">=5.0.2" 584 | }, 585 | "peerDependenciesMeta": { 586 | "bufferutil": { 587 | "optional": true 588 | }, 589 | "utf-8-validate": { 590 | "optional": true 591 | } 592 | } 593 | } 594 | } 595 | } 596 | --------------------------------------------------------------------------------