sona-ai-voice 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +164 -0
- package/bin/blue +27 -0
- package/bin/sona +27 -0
- package/package.json +65 -0
- package/packages/audio/package.json +16 -0
- package/packages/cache/package.json +22 -0
- package/packages/cli/dist/audio-capture.d.ts +40 -0
- package/packages/cli/dist/audio-capture.d.ts.map +1 -0
- package/packages/cli/dist/audio-capture.js +122 -0
- package/packages/cli/dist/audio-capture.js.map +1 -0
- package/packages/cli/dist/audio-playback.d.ts +49 -0
- package/packages/cli/dist/audio-playback.d.ts.map +1 -0
- package/packages/cli/dist/audio-playback.js +153 -0
- package/packages/cli/dist/audio-playback.js.map +1 -0
- package/packages/cli/dist/echo-canceller.d.ts +53 -0
- package/packages/cli/dist/echo-canceller.d.ts.map +1 -0
- package/packages/cli/dist/echo-canceller.js +159 -0
- package/packages/cli/dist/echo-canceller.js.map +1 -0
- package/packages/cli/dist/gui/particles.html +383 -0
- package/packages/cli/dist/gui-server.d.ts +32 -0
- package/packages/cli/dist/gui-server.d.ts.map +1 -0
- package/packages/cli/dist/gui-server.js +140 -0
- package/packages/cli/dist/gui-server.js.map +1 -0
- package/packages/cli/dist/index.d.ts +7 -0
- package/packages/cli/dist/index.d.ts.map +1 -0
- package/packages/cli/dist/index.js +154 -0
- package/packages/cli/dist/index.js.map +1 -0
- package/packages/cli/dist/particle-ui.d.ts +105 -0
- package/packages/cli/dist/particle-ui.d.ts.map +1 -0
- package/packages/cli/dist/particle-ui.js +358 -0
- package/packages/cli/dist/particle-ui.js.map +1 -0
- package/packages/cli/dist/setup.d.ts +46 -0
- package/packages/cli/dist/setup.d.ts.map +1 -0
- package/packages/cli/dist/setup.js +206 -0
- package/packages/cli/dist/setup.js.map +1 -0
- package/packages/cli/dist/tools/codebase-context.d.ts +72 -0
- package/packages/cli/dist/tools/codebase-context.d.ts.map +1 -0
- package/packages/cli/dist/tools/codebase-context.js +339 -0
- package/packages/cli/dist/tools/codebase-context.js.map +1 -0
- package/packages/cli/dist/tools/tool-executor.d.ts +70 -0
- package/packages/cli/dist/tools/tool-executor.d.ts.map +1 -0
- package/packages/cli/dist/tools/tool-executor.js +302 -0
- package/packages/cli/dist/tools/tool-executor.js.map +1 -0
- package/packages/cli/dist/tools/web-search.d.ts +43 -0
- package/packages/cli/dist/tools/web-search.d.ts.map +1 -0
- package/packages/cli/dist/tools/web-search.js +169 -0
- package/packages/cli/dist/tools/web-search.js.map +1 -0
- package/packages/cli/dist/voice-conversation.d.ts +56 -0
- package/packages/cli/dist/voice-conversation.d.ts.map +1 -0
- package/packages/cli/dist/voice-conversation.js +352 -0
- package/packages/cli/dist/voice-conversation.js.map +1 -0
- package/packages/cli/package.json +31 -0
- package/packages/cli/src/gui/particles.html +383 -0
- package/packages/indexer/package.json +22 -0
- package/packages/realtime/dist/index.d.ts +5 -0
- package/packages/realtime/dist/index.d.ts.map +1 -0
- package/packages/realtime/dist/index.js +21 -0
- package/packages/realtime/dist/index.js.map +1 -0
- package/packages/realtime/dist/session.d.ts +91 -0
- package/packages/realtime/dist/session.d.ts.map +1 -0
- package/packages/realtime/dist/session.js +348 -0
- package/packages/realtime/dist/session.js.map +1 -0
- package/packages/realtime/package.json +22 -0
- package/packages/shared/dist/config.d.ts +14 -0
- package/packages/shared/dist/config.d.ts.map +1 -0
- package/packages/shared/dist/config.js +247 -0
- package/packages/shared/dist/config.js.map +1 -0
- package/packages/shared/dist/constants.d.ts +34 -0
- package/packages/shared/dist/constants.d.ts.map +1 -0
- package/packages/shared/dist/constants.js +61 -0
- package/packages/shared/dist/constants.js.map +1 -0
- package/packages/shared/dist/index.d.ts +7 -0
- package/packages/shared/dist/index.d.ts.map +1 -0
- package/packages/shared/dist/index.js +23 -0
- package/packages/shared/dist/index.js.map +1 -0
- package/packages/shared/dist/types.d.ts +101 -0
- package/packages/shared/dist/types.d.ts.map +1 -0
- package/packages/shared/dist/types.js +71 -0
- package/packages/shared/dist/types.js.map +1 -0
- package/packages/shared/package.json +22 -0
- package/packages/terminal/package.json +19 -0
package/README.md
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# Sona
|
|
2
|
+
|
|
3
|
+
A production-ready voice-to-voice CLI companion powered by OpenAI's Realtime API. Natural speech-to-speech interaction with codebase awareness and web search capabilities.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Real-time voice conversation** - Natural speech-to-speech interaction
|
|
8
|
+
- **Codebase awareness** - Indexes your project and understands your code
|
|
9
|
+
- **Web search** - Search the web for current information (no API key needed)
|
|
10
|
+
- **Particle GUI** - Visual feedback (optional)
|
|
11
|
+
- **Secure** - API keys stored locally with restricted permissions
|
|
12
|
+
- **Barge-in support** - Interrupt Sona mid-sentence
|
|
13
|
+
- **Echo cancellation** - Prevents feedback loops
|
|
14
|
+
|
|
15
|
+
## Prerequisites
|
|
16
|
+
|
|
17
|
+
- **Node.js** >= 18.0.0
|
|
18
|
+
- **sox** (for audio capture/playback)
|
|
19
|
+
```bash
|
|
20
|
+
# macOS
|
|
21
|
+
brew install sox
|
|
22
|
+
|
|
23
|
+
# Ubuntu/Debian
|
|
24
|
+
sudo apt-get install sox
|
|
25
|
+
|
|
26
|
+
# Windows
|
|
27
|
+
choco install sox
|
|
28
|
+
```
|
|
29
|
+
- **OpenAI API key** with Realtime API access
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
### Quick Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install -g sona-ai-voice
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Fix Permission Errors (macOS/Linux)
|
|
40
|
+
|
|
41
|
+
If you see `EACCES: permission denied` errors, configure npm to use a user-owned directory:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# 1. Create a directory for global packages
|
|
45
|
+
mkdir -p ~/.npm-global
|
|
46
|
+
|
|
47
|
+
# 2. Configure npm to use this directory
|
|
48
|
+
npm config set prefix '~/.npm-global'
|
|
49
|
+
|
|
50
|
+
# 3. Add to your PATH (choose your shell):
|
|
51
|
+
# For zsh (macOS default):
|
|
52
|
+
echo 'export PATH=~/.npm-global/bin:$PATH' >> ~/.zshrc
|
|
53
|
+
source ~/.zshrc
|
|
54
|
+
|
|
55
|
+
# For bash:
|
|
56
|
+
echo 'export PATH=~/.npm-global/bin:$PATH' >> ~/.bashrc
|
|
57
|
+
source ~/.bashrc
|
|
58
|
+
|
|
59
|
+
# 4. Install Sona
|
|
60
|
+
npm install -g sona-ai-voice
|
|
61
|
+
|
|
62
|
+
# 5. Verify installation
|
|
63
|
+
sona --help
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Alternative**: Use `sudo` (not recommended):
|
|
67
|
+
```bash
|
|
68
|
+
sudo npm install -g sona-ai-voice
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Setup
|
|
72
|
+
|
|
73
|
+
First time setup - enter your OpenAI API key:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
sona setup
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Your API key will be stored securely in `~/.sona/config` with restricted file permissions.
|
|
80
|
+
|
|
81
|
+
## Usage
|
|
82
|
+
|
|
83
|
+
### Start a voice conversation
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# Navigate to any project directory
|
|
87
|
+
cd /path/to/your-project
|
|
88
|
+
|
|
89
|
+
# Start Sona (indexes current directory)
|
|
90
|
+
sona talk
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Options
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
sona talk # Full experience with GUI
|
|
97
|
+
sona talk --no-gui # Terminal only (no particle window)
|
|
98
|
+
sona talk --no-index # Skip codebase indexing
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Other commands
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
sona setup # Configure API key
|
|
105
|
+
sona test # Test API connection
|
|
106
|
+
sona logout # Remove stored API key
|
|
107
|
+
sona --help # Show all commands
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## How It Works
|
|
111
|
+
|
|
112
|
+
1. **Indexing**: When you run `sona talk`, it indexes the current directory's source files
|
|
113
|
+
2. **Context**: Sona understands your codebase structure and can answer questions about it
|
|
114
|
+
3. **Voice**: Uses OpenAI's Realtime API for natural voice conversation
|
|
115
|
+
4. **Web Search**: Can search the web for information it doesn't know
|
|
116
|
+
|
|
117
|
+
## Example Conversations
|
|
118
|
+
|
|
119
|
+
- "What files are in this project?"
|
|
120
|
+
- "Explain how the authentication works"
|
|
121
|
+
- "What dependencies does this project use?"
|
|
122
|
+
- "Search for the latest React 19 features"
|
|
123
|
+
|
|
124
|
+
## Security
|
|
125
|
+
|
|
126
|
+
- API keys are stored in `~/.sona/config` with `600` permissions (owner-only)
|
|
127
|
+
- Keys are obfuscated (not plain text)
|
|
128
|
+
- Never committed to git (`.env.local` is gitignored)
|
|
129
|
+
- You can remove your key anytime with `sona logout`
|
|
130
|
+
|
|
131
|
+
## Troubleshooting
|
|
132
|
+
|
|
133
|
+
### "EACCES: permission denied" when installing
|
|
134
|
+
This happens because npm tries to install to `/usr/local` which requires admin access. **Solution**: Configure npm to use a user-owned directory (see Installation section above).
|
|
135
|
+
|
|
136
|
+
### "sox not found"
|
|
137
|
+
Install sox: `brew install sox` (macOS) or `apt-get install sox` (Linux)
|
|
138
|
+
|
|
139
|
+
### "API key required"
|
|
140
|
+
Run `sona setup` to configure your OpenAI API key
|
|
141
|
+
|
|
142
|
+
### "sona: command not found"
|
|
143
|
+
Make sure `~/.npm-global/bin` (or your npm global bin path) is in your PATH. Check with:
|
|
144
|
+
```bash
|
|
145
|
+
npm config get prefix
|
|
146
|
+
which sona
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Sona not responding
|
|
150
|
+
- Check your microphone is working
|
|
151
|
+
- Speak clearly and close to the mic
|
|
152
|
+
- Try `sona test` to verify API connection
|
|
153
|
+
|
|
154
|
+
## Development
|
|
155
|
+
|
|
156
|
+
For development, see the [GitHub repository](https://github.com/zerish/Blue).
|
|
157
|
+
|
|
158
|
+
## License
|
|
159
|
+
|
|
160
|
+
MIT
|
|
161
|
+
|
|
162
|
+
## Author
|
|
163
|
+
|
|
164
|
+
zerish
|
package/bin/blue
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sona CLI - Global Entry Point
|
|
5
|
+
* This file allows Sona to be run from anywhere
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { spawn } = require('child_process');
|
|
10
|
+
|
|
11
|
+
// Get the directory where Sona is installed
|
|
12
|
+
const sonaRoot = path.resolve(__dirname, '..');
|
|
13
|
+
const cliPath = path.join(sonaRoot, 'packages', 'cli', 'dist', 'index.js');
|
|
14
|
+
|
|
15
|
+
// Run the CLI with all arguments passed through
|
|
16
|
+
const child = spawn('node', [cliPath, ...process.argv.slice(2)], {
|
|
17
|
+
stdio: 'inherit',
|
|
18
|
+
cwd: process.cwd(), // Use current working directory for indexing
|
|
19
|
+
env: {
|
|
20
|
+
...process.env,
|
|
21
|
+
SONA_ROOT: sonaRoot,
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
child.on('exit', (code) => {
|
|
26
|
+
process.exit(code || 0);
|
|
27
|
+
});
|
package/bin/sona
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sona CLI - Global Entry Point
|
|
5
|
+
* This file allows Sona to be run from anywhere
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { spawn } = require('child_process');
|
|
10
|
+
|
|
11
|
+
// Get the directory where Sona is installed
|
|
12
|
+
const sonaRoot = path.resolve(__dirname, '..');
|
|
13
|
+
const cliPath = path.join(sonaRoot, 'packages', 'cli', 'dist', 'index.js');
|
|
14
|
+
|
|
15
|
+
// Run the CLI with all arguments passed through
|
|
16
|
+
const child = spawn('node', [cliPath, ...process.argv.slice(2)], {
|
|
17
|
+
stdio: 'inherit',
|
|
18
|
+
cwd: process.cwd(), // Use current working directory for indexing
|
|
19
|
+
env: {
|
|
20
|
+
...process.env,
|
|
21
|
+
SONA_ROOT: sonaRoot,
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
child.on('exit', (code) => {
|
|
26
|
+
process.exit(code || 0);
|
|
27
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sona-ai-voice",
|
|
3
|
+
"version": "0.1.5",
|
|
4
|
+
"description": "Voice-to-voice AI CLI companion powered by OpenAI Realtime API",
|
|
5
|
+
"author": "zerish",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/zerish/Blue.git"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"ai",
|
|
13
|
+
"voice",
|
|
14
|
+
"openai",
|
|
15
|
+
"realtime",
|
|
16
|
+
"cli",
|
|
17
|
+
"speech-to-speech",
|
|
18
|
+
"conversational-ai"
|
|
19
|
+
],
|
|
20
|
+
"bin": {
|
|
21
|
+
"sona": "packages/cli/dist/index.js"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"packages/*/dist/**/*",
|
|
25
|
+
"packages/*/package.json",
|
|
26
|
+
"packages/cli/src/gui/**/*",
|
|
27
|
+
"bin/**/*"
|
|
28
|
+
],
|
|
29
|
+
"workspaces": [
|
|
30
|
+
"packages/*"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "bash scripts/build-for-publish.sh",
|
|
34
|
+
"build:all": "npm run build --workspaces --if-present",
|
|
35
|
+
"dev": "npm run dev --workspace=packages/cli",
|
|
36
|
+
"clean": "rm -rf packages/*/dist packages/*/*.tsbuildinfo node_modules/.cache",
|
|
37
|
+
"test": "npm run test --workspaces --if-present",
|
|
38
|
+
"lint": "eslint packages --ext .ts,.tsx",
|
|
39
|
+
"format": "prettier --write \"packages/**/*.{ts,tsx,json,md}\"",
|
|
40
|
+
"cache:clear": "rm -rf node_modules/.cache .sona-cache",
|
|
41
|
+
"prepublishOnly": "npm run build"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"chalk": "^4.1.2",
|
|
45
|
+
"commander": "^11.1.0",
|
|
46
|
+
"dotenv": "^16.0.0",
|
|
47
|
+
"ws": "^8.19.0",
|
|
48
|
+
"zod": "^3.22.0"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/node": "^20.10.0",
|
|
52
|
+
"@typescript-eslint/eslint-plugin": "^6.13.0",
|
|
53
|
+
"@typescript-eslint/parser": "^6.13.0",
|
|
54
|
+
"eslint": "^8.54.0",
|
|
55
|
+
"eslint-config-airbnb-base": "^15.0.0",
|
|
56
|
+
"eslint-config-airbnb-typescript": "^17.1.0",
|
|
57
|
+
"eslint-plugin-import": "^2.29.0",
|
|
58
|
+
"prettier": "^3.1.0",
|
|
59
|
+
"typescript": "^5.3.0"
|
|
60
|
+
},
|
|
61
|
+
"engines": {
|
|
62
|
+
"node": ">=18.0.0",
|
|
63
|
+
"npm": ">=9.0.0"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@blue/audio",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Audio capture, playback, VAD, and endpointing for Blue",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"dev": "tsc --watch"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"typescript": "^5.3.2",
|
|
14
|
+
"@types/node": "^20.10.0"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@blue/cache",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "Multi-layer caching system for API optimization",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc -b",
|
|
10
|
+
"dev": "tsc -b --watch",
|
|
11
|
+
"clean": "rm -rf dist *.tsbuildinfo",
|
|
12
|
+
"test": "jest"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"lru-cache": "^10.1.0",
|
|
16
|
+
"xxhash-wasm": "^1.0.2"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/node": "^20.10.0",
|
|
20
|
+
"typescript": "^5.3.0"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audio Capture Module
|
|
3
|
+
* Mic capture with pause/resume for echo cancellation
|
|
4
|
+
*/
|
|
5
|
+
import { EventEmitter } from 'events';
|
|
6
|
+
export interface AudioCaptureConfig {
|
|
7
|
+
readonly sampleRate: number;
|
|
8
|
+
readonly channels: number;
|
|
9
|
+
readonly bitDepth: number;
|
|
10
|
+
}
|
|
11
|
+
export interface AudioCaptureEvents {
|
|
12
|
+
'data': (audioBuffer: Buffer) => void;
|
|
13
|
+
'error': (error: Error) => void;
|
|
14
|
+
'started': () => void;
|
|
15
|
+
'stopped': () => void;
|
|
16
|
+
}
|
|
17
|
+
export declare class AudioCapture extends EventEmitter {
|
|
18
|
+
private micProcess;
|
|
19
|
+
private isRecording;
|
|
20
|
+
private isPaused;
|
|
21
|
+
private config;
|
|
22
|
+
constructor(config: AudioCaptureConfig);
|
|
23
|
+
start(): void;
|
|
24
|
+
private spawnMic;
|
|
25
|
+
/**
|
|
26
|
+
* Pause audio capture (for echo cancellation during playback)
|
|
27
|
+
* Kills the mic process to ensure no audio leaks through
|
|
28
|
+
*/
|
|
29
|
+
pause(): void;
|
|
30
|
+
/**
|
|
31
|
+
* Resume audio capture after playback ends
|
|
32
|
+
*/
|
|
33
|
+
resume(): void;
|
|
34
|
+
stop(): void;
|
|
35
|
+
get recording(): boolean;
|
|
36
|
+
get paused(): boolean;
|
|
37
|
+
on<K extends keyof AudioCaptureEvents>(event: K, listener: AudioCaptureEvents[K]): this;
|
|
38
|
+
emit<K extends keyof AudioCaptureEvents>(event: K, ...args: Parameters<AudioCaptureEvents[K]>): boolean;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=audio-capture.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audio-capture.d.ts","sourceRoot":"","sources":["../src/audio-capture.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAEtC,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IAChC,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,SAAS,EAAE,MAAM,IAAI,CAAC;CACvB;AAED,qBAAa,YAAa,SAAQ,YAAY;IAC5C,OAAO,CAAC,UAAU,CAA6B;IAC/C,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,MAAM,CAAqB;gBAEvB,MAAM,EAAE,kBAAkB;IAKtC,KAAK,IAAI,IAAI;IAKb,OAAO,CAAC,QAAQ;IAgDhB;;;OAGG;IACH,KAAK,IAAI,IAAI;IAWb;;OAEG;IACH,MAAM,IAAI,IAAI;IAQd,IAAI,IAAI,IAAI;IAYZ,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,IAAI,MAAM,IAAI,OAAO,CAEpB;IAEQ,EAAE,CAAC,CAAC,SAAS,MAAM,kBAAkB,EAC5C,KAAK,EAAE,CAAC,EACR,QAAQ,EAAE,kBAAkB,CAAC,CAAC,CAAC,GAC9B,IAAI;IAIE,IAAI,CAAC,CAAC,SAAS,MAAM,kBAAkB,EAC9C,KAAK,EAAE,CAAC,EACR,GAAG,IAAI,EAAE,UAAU,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,GACzC,OAAO;CAGX"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Audio Capture Module
|
|
4
|
+
* Mic capture with pause/resume for echo cancellation
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.AudioCapture = void 0;
|
|
8
|
+
const child_process_1 = require("child_process");
|
|
9
|
+
const events_1 = require("events");
|
|
10
|
+
class AudioCapture extends events_1.EventEmitter {
|
|
11
|
+
micProcess = null;
|
|
12
|
+
isRecording = false;
|
|
13
|
+
isPaused = false;
|
|
14
|
+
config;
|
|
15
|
+
constructor(config) {
|
|
16
|
+
super();
|
|
17
|
+
this.config = config;
|
|
18
|
+
}
|
|
19
|
+
start() {
|
|
20
|
+
if (this.isRecording)
|
|
21
|
+
return;
|
|
22
|
+
this.spawnMic();
|
|
23
|
+
}
|
|
24
|
+
spawnMic() {
|
|
25
|
+
if (this.micProcess) {
|
|
26
|
+
try {
|
|
27
|
+
this.micProcess.kill('SIGKILL');
|
|
28
|
+
}
|
|
29
|
+
catch { }
|
|
30
|
+
this.micProcess = null;
|
|
31
|
+
}
|
|
32
|
+
this.micProcess = (0, child_process_1.spawn)('sox', [
|
|
33
|
+
'-d', // Default audio device
|
|
34
|
+
'-t', 'raw', // Raw PCM output
|
|
35
|
+
'-b', '16', // 16-bit
|
|
36
|
+
'-e', 'signed-integer', // Signed integer
|
|
37
|
+
'-r', '24000', // 24kHz (required by OpenAI)
|
|
38
|
+
'-c', '1', // Mono
|
|
39
|
+
'-q', // Quiet mode
|
|
40
|
+
'-', // Output to stdout
|
|
41
|
+
]);
|
|
42
|
+
if (!this.micProcess.stdout) {
|
|
43
|
+
this.emit('error', new Error('Failed to start microphone'));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
this.micProcess.stdout.on('data', (chunk) => {
|
|
47
|
+
if (!this.isPaused) {
|
|
48
|
+
this.emit('data', chunk);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
this.micProcess.stderr?.on('data', (data) => {
|
|
52
|
+
const msg = data.toString();
|
|
53
|
+
if (msg.includes('FAIL') || msg.includes('error')) {
|
|
54
|
+
this.emit('error', new Error(msg));
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
this.micProcess.on('error', (error) => {
|
|
58
|
+
this.emit('error', error);
|
|
59
|
+
});
|
|
60
|
+
this.micProcess.on('exit', () => {
|
|
61
|
+
this.isRecording = false;
|
|
62
|
+
});
|
|
63
|
+
this.isRecording = true;
|
|
64
|
+
this.isPaused = false;
|
|
65
|
+
this.emit('started');
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Pause audio capture (for echo cancellation during playback)
|
|
69
|
+
* Kills the mic process to ensure no audio leaks through
|
|
70
|
+
*/
|
|
71
|
+
pause() {
|
|
72
|
+
if (!this.isRecording || this.isPaused)
|
|
73
|
+
return;
|
|
74
|
+
this.isPaused = true;
|
|
75
|
+
// Kill the mic process to ensure complete silence
|
|
76
|
+
if (this.micProcess) {
|
|
77
|
+
try {
|
|
78
|
+
this.micProcess.kill('SIGKILL');
|
|
79
|
+
}
|
|
80
|
+
catch { }
|
|
81
|
+
this.micProcess = null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Resume audio capture after playback ends
|
|
86
|
+
*/
|
|
87
|
+
resume() {
|
|
88
|
+
if (!this.isPaused)
|
|
89
|
+
return;
|
|
90
|
+
this.isPaused = false;
|
|
91
|
+
// Restart the mic process
|
|
92
|
+
this.spawnMic();
|
|
93
|
+
}
|
|
94
|
+
stop() {
|
|
95
|
+
if (!this.isRecording)
|
|
96
|
+
return;
|
|
97
|
+
if (this.micProcess) {
|
|
98
|
+
try {
|
|
99
|
+
this.micProcess.kill('SIGTERM');
|
|
100
|
+
}
|
|
101
|
+
catch { }
|
|
102
|
+
this.micProcess = null;
|
|
103
|
+
}
|
|
104
|
+
this.isRecording = false;
|
|
105
|
+
this.isPaused = false;
|
|
106
|
+
this.emit('stopped');
|
|
107
|
+
}
|
|
108
|
+
get recording() {
|
|
109
|
+
return this.isRecording && !this.isPaused;
|
|
110
|
+
}
|
|
111
|
+
get paused() {
|
|
112
|
+
return this.isPaused;
|
|
113
|
+
}
|
|
114
|
+
on(event, listener) {
|
|
115
|
+
return super.on(event, listener);
|
|
116
|
+
}
|
|
117
|
+
emit(event, ...args) {
|
|
118
|
+
return super.emit(event, ...args);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
exports.AudioCapture = AudioCapture;
|
|
122
|
+
//# sourceMappingURL=audio-capture.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audio-capture.js","sourceRoot":"","sources":["../src/audio-capture.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;AAEH,iDAAoD;AACpD,mCAAsC;AAetC,MAAa,YAAa,SAAQ,qBAAY;IACpC,UAAU,GAAwB,IAAI,CAAC;IACvC,WAAW,GAAG,KAAK,CAAC;IACpB,QAAQ,GAAG,KAAK,CAAC;IACjB,MAAM,CAAqB;IAEnC,YAAY,MAA0B;QACpC,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;IAED,KAAK;QACH,IAAI,IAAI,CAAC,WAAW;YAAE,OAAO;QAC7B,IAAI,CAAC,QAAQ,EAAE,CAAC;IAClB,CAAC;IAEO,QAAQ;QACd,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACpB,IAAI,CAAC;gBAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YAAC,CAAC;YAAC,MAAM,CAAC,CAAA,CAAC;YACjD,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;QACzB,CAAC;QAED,IAAI,CAAC,UAAU,GAAG,IAAA,qBAAK,EAAC,KAAK,EAAE;YAC7B,IAAI,EAAqB,uBAAuB;YAChD,IAAI,EAAE,KAAK,EAAc,iBAAiB;YAC1C,IAAI,EAAE,IAAI,EAAe,SAAS;YAClC,IAAI,EAAE,gBAAgB,EAAG,iBAAiB;YAC1C,IAAI,EAAE,OAAO,EAAY,6BAA6B;YACtD,IAAI,EAAE,GAAG,EAAgB,OAAO;YAChC,IAAI,EAAqB,aAAa;YACtC,GAAG,EAAsB,mBAAmB;SAC7C,CAAC,CAAC;QAEH,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC;YAC5B,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,KAAK,CAAC,4BAA4B,CAAC,CAAC,CAAC;YAC5D,OAAO;QACT,CAAC;QAED,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE;YAClD,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACnB,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;YAC3B,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE;YAC1C,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC5B,IAAI,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;gBAClD,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;YACrC,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE;YACpC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QAC5B,CAAC,CAAC,CAAC;QAEH,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE;YAC9B,IAAI,CAAC,WAAW,GAAG,KAAK,CAAC;QAC3B,CAAC,CAAC,CAAC;QAEH,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC;QACxB,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC;QACtB,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACvB,CAAC;IAED;;;OAGG;IACH,KAAK;QACH,IAAI,CAAC,IAAI,CAAC,WAAW,IAAI,IAAI,CAAC,QAAQ;YAAE,OAAO;QAC/C,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QAErB,kDAAkD;QAClD,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACpB,IAAI,CAAC;gBAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YAAC,CAAC;YAAC,MAAM,CAAC,CAAA,CAAC;YACjD,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;QACzB,CAAC;IACH,CAAC;IAED;;OAEG;IACH,MAAM;QACJ,IAAI,CAAC,IAAI,CAAC,QAAQ;YAAE,OAAO;QAC3B,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC;QAEtB,0BAA0B;QAC1B,IAAI,CAAC,QAAQ,EAAE,CAAC;IAClB,CAAC;IAED,IAAI;QACF,IAAI,CAAC,IAAI,CAAC,WAAW;YAAE,OAAO;QAE9B,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACpB,IAAI,CAAC;gBAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YAAC,CAAC;YAAC,MAAM,CAAC,CAAA,CAAC;YACjD,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;QACzB,CAAC;QACD,IAAI,CAAC,WAAW,GAAG,KAAK,CAAC;QACzB,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC;QACtB,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACvB,CAAC;IAED,IAAI,SAAS;QACX,OAAO,IAAI,CAAC,WAAW,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC;IAC5C,CAAC;IAED,IAAI,MAAM;QACR,OAAO,IAAI,CAAC,QAAQ,CAAC;IACvB,CAAC;IAEQ,EAAE,CACT,KAAQ,EACR,QAA+B;QAE/B,OAAO,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;IACnC,CAAC;IAEQ,IAAI,CACX,KAAQ,EACR,GAAG,IAAuC;QAE1C,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,GAAG,IAAI,CAAC,CAAC;IACpC,CAAC;CACF;AA3HD,oCA2HC"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audio Playback Module
|
|
3
|
+
* Smooth speaker output using sox with buffering to prevent audio cracks
|
|
4
|
+
*/
|
|
5
|
+
import { EventEmitter } from 'events';
|
|
6
|
+
export interface AudioPlaybackConfig {
|
|
7
|
+
readonly sampleRate: number;
|
|
8
|
+
readonly channels: number;
|
|
9
|
+
readonly bitDepth: number;
|
|
10
|
+
}
|
|
11
|
+
export interface AudioPlaybackEvents {
|
|
12
|
+
'started': () => void;
|
|
13
|
+
'stopped': () => void;
|
|
14
|
+
'finished': () => void;
|
|
15
|
+
'error': (error: Error) => void;
|
|
16
|
+
}
|
|
17
|
+
export declare class AudioPlayback extends EventEmitter {
|
|
18
|
+
private playProcess;
|
|
19
|
+
private isWritable;
|
|
20
|
+
private config;
|
|
21
|
+
private audioBuffer;
|
|
22
|
+
private isPlaying;
|
|
23
|
+
private totalBytesQueued;
|
|
24
|
+
private finishRequested;
|
|
25
|
+
private drainTimeout;
|
|
26
|
+
constructor(config: AudioPlaybackConfig);
|
|
27
|
+
start(): void;
|
|
28
|
+
private spawnProcess;
|
|
29
|
+
/**
|
|
30
|
+
* Queue audio for playback
|
|
31
|
+
*/
|
|
32
|
+
play(audioBuffer: Buffer): void;
|
|
33
|
+
/**
|
|
34
|
+
* Signal that we're done sending audio for this response.
|
|
35
|
+
*/
|
|
36
|
+
finishPlayback(): void;
|
|
37
|
+
/**
|
|
38
|
+
* Reset state for next response
|
|
39
|
+
*/
|
|
40
|
+
private resetForNextResponse;
|
|
41
|
+
/**
|
|
42
|
+
* Immediately stop playback (for barge-in)
|
|
43
|
+
*/
|
|
44
|
+
stop(): void;
|
|
45
|
+
get playing(): boolean;
|
|
46
|
+
on<K extends keyof AudioPlaybackEvents>(event: K, listener: AudioPlaybackEvents[K]): this;
|
|
47
|
+
emit<K extends keyof AudioPlaybackEvents>(event: K, ...args: Parameters<AudioPlaybackEvents[K]>): boolean;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=audio-playback.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audio-playback.d.ts","sourceRoot":"","sources":["../src/audio-playback.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAEtC,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CACjC;AAED,qBAAa,aAAc,SAAQ,YAAY;IAC7C,OAAO,CAAC,WAAW,CAA6B;IAChD,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,MAAM,CAAsB;IAGpC,OAAO,CAAC,WAAW,CAAgB;IACnC,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,YAAY,CAA+B;gBAEvC,MAAM,EAAE,mBAAmB;IAKvC,KAAK,IAAI,IAAI;IAIb,OAAO,CAAC,YAAY;IA+CpB;;OAEG;IACH,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI;IAoB/B;;OAEG;IACH,cAAc,IAAI,IAAI;IAqBtB;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAU5B;;OAEG;IACH,IAAI,IAAI,IAAI;IA0BZ,IAAI,OAAO,IAAI,OAAO,CAErB;IAEQ,EAAE,CAAC,CAAC,SAAS,MAAM,mBAAmB,EAC7C,KAAK,EAAE,CAAC,EACR,QAAQ,EAAE,mBAAmB,CAAC,CAAC,CAAC,GAC/B,IAAI;IAIE,IAAI,CAAC,CAAC,SAAS,MAAM,mBAAmB,EAC/C,KAAK,EAAE,CAAC,EACR,GAAG,IAAI,EAAE,UAAU,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC,GAC1C,OAAO;CAGX"}
|