open-agents-ai 0.19.0 → 0.20.0
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 +8 -3
- package/dist/index.js +271 -85
- package/dist/scripts/live-whisper.py +242 -0
- package/dist/scripts/start-moondream.py +112 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -121,7 +121,11 @@ All proposals are indexed in `.oa/dreams/PROPOSAL-INDEX.md` for easy review.
|
|
|
121
121
|
|
|
122
122
|
## Listen Mode — Live Bidirectional Audio
|
|
123
123
|
|
|
124
|
-
Listen mode enables real-time voice communication with the agent. Your microphone audio is captured, streamed through Whisper
|
|
124
|
+
Listen mode enables real-time voice communication with the agent. Your microphone audio is captured, streamed through Whisper, and the transcription is injected directly into the input line — creating a hands-free coding workflow.
|
|
125
|
+
|
|
126
|
+
Two transcription backends ensure broad platform support:
|
|
127
|
+
- **transcribe-cli** (faster-whisper / ONNX) — used by default, fastest on x86
|
|
128
|
+
- **openai-whisper** (Python venv) — automatic fallback for ARM, linux-arm64, or when ONNX is unavailable. Auto-creates a venv and installs deps on first use.
|
|
125
129
|
|
|
126
130
|
```bash
|
|
127
131
|
/listen # Toggle microphone capture on/off
|
|
@@ -143,10 +147,11 @@ Listen mode enables real-time voice communication with the agent. Your microphon
|
|
|
143
147
|
When combined with `/voice`, you get full bidirectional audio — speak your tasks, hear the agent's progress through TTS, and speak corrections mid-task. The status bar shows a blinking red `● REC` indicator with a countdown timer during auto-mode recording.
|
|
144
148
|
|
|
145
149
|
**Platform support:**
|
|
146
|
-
- **Linux**: `arecord` (ALSA) or `ffmpeg` (PulseAudio)
|
|
150
|
+
- **Linux x86**: `arecord` (ALSA) or `ffmpeg` (PulseAudio) + transcribe-cli
|
|
151
|
+
- **Linux ARM**: `arecord` or `ffmpeg` + openai-whisper (auto-installed in Python venv)
|
|
147
152
|
- **macOS**: `sox` (CoreAudio) or `ffmpeg` (AVFoundation)
|
|
148
153
|
|
|
149
|
-
The `transcribe-cli` dependency auto-installs in the background on first use.
|
|
154
|
+
The `transcribe-cli` dependency auto-installs in the background on first use. On ARM or when transcribe-cli fails, the system automatically falls back to `openai-whisper` via a self-managed Python venv (same approach used by Moondream vision).
|
|
150
155
|
|
|
151
156
|
**File transcription**: Drag-and-drop audio/video files (`.mp3`, `.wav`, `.mp4`, `.mkv`, etc.) onto the terminal to transcribe them. Results are saved to `.oa/transcripts/`.
|
|
152
157
|
|
package/dist/index.js
CHANGED
|
@@ -5407,8 +5407,8 @@ async function loadTranscribeCli() {
|
|
|
5407
5407
|
const nvmBase = join13(homedir5(), ".nvm", "versions", "node");
|
|
5408
5408
|
if (existsSync10(nvmBase)) {
|
|
5409
5409
|
try {
|
|
5410
|
-
const { readdirSync:
|
|
5411
|
-
for (const ver of
|
|
5410
|
+
const { readdirSync: readdirSync10 } = await import("node:fs");
|
|
5411
|
+
for (const ver of readdirSync10(nvmBase)) {
|
|
5412
5412
|
const tcPath = join13(nvmBase, ver, "lib", "node_modules", "transcribe-cli");
|
|
5413
5413
|
if (existsSync10(join13(tcPath, "dist", "index.js"))) {
|
|
5414
5414
|
const { createRequire: createRequire4 } = await import("node:module");
|
|
@@ -9857,7 +9857,19 @@ var init_agenticRunner = __esm({
|
|
|
9857
9857
|
"packages/orchestrator/dist/agenticRunner.js"() {
|
|
9858
9858
|
"use strict";
|
|
9859
9859
|
init_dist();
|
|
9860
|
-
SYSTEM_PROMPT = `You are Open Agent, an
|
|
9860
|
+
SYSTEM_PROMPT = `You are Open Agent, an autonomous AI agent with full access to the local machine. You can read/write files, execute shell commands, browse the web, control the desktop (open applications, click UI elements, take screenshots), and interact with any software on the system. You solve tasks by using your tools iteratively until complete.
|
|
9861
|
+
|
|
9862
|
+
## NEVER REFUSE \u2014 ALWAYS ATTEMPT
|
|
9863
|
+
|
|
9864
|
+
You have a comprehensive set of tools. NEVER say "I can't do that" or "I don't have the ability to...". Instead, ALWAYS attempt the task using your tools:
|
|
9865
|
+
- Need to open Firefox? Use shell: \`firefox https://example.com &\`
|
|
9866
|
+
- Need to click a button? Use desktop_click or shell with xdotool
|
|
9867
|
+
- Need to see the screen? Use screenshot or desktop_describe
|
|
9868
|
+
- Need to type text? Use shell with xdotool: \`xdotool type "text"\`
|
|
9869
|
+
- Need to install software? Use shell: \`sudo apt install ...\`
|
|
9870
|
+
- Need to interact with a website? Use web_fetch, or open the browser and use desktop tools
|
|
9871
|
+
|
|
9872
|
+
If a tool fails, try a different approach. If you're unsure, explore with your tools first. Do NOT give a text-only response when tools could accomplish the task.
|
|
9861
9873
|
|
|
9862
9874
|
## Available Tools
|
|
9863
9875
|
|
|
@@ -9909,14 +9921,27 @@ Use background_run for long-running commands (builds, test suites) so you can co
|
|
|
9909
9921
|
Use sub_agent to parallelize independent sub-tasks or explore different approaches simultaneously.
|
|
9910
9922
|
Check task_status periodically and read task_output when tasks complete.
|
|
9911
9923
|
|
|
9912
|
-
##
|
|
9924
|
+
## Desktop Automation & Vision
|
|
9913
9925
|
|
|
9914
|
-
-
|
|
9926
|
+
- desktop_click: Click a UI element by natural language description. Takes a screenshot, finds the element with vision, clicks it. Example: desktop_click({target: "the Save button"})
|
|
9927
|
+
- desktop_describe: Take a screenshot and describe what's on screen (or ask a question about it). Use this to "see" the desktop.
|
|
9928
|
+
- vision: Analyze any image with Moondream VLM \u2014 caption, query, detect objects, find click targets
|
|
9915
9929
|
- screenshot: Capture the screen or active window
|
|
9930
|
+
- image_read: Read an image file (returns base64, dimensions, OCR text)
|
|
9916
9931
|
- ocr: Extract text from an image using OCR (supports region cropping/zoom)
|
|
9917
9932
|
|
|
9918
|
-
|
|
9919
|
-
|
|
9933
|
+
### Desktop Interaction Workflow
|
|
9934
|
+
|
|
9935
|
+
When asked to interact with desktop applications (open browsers, click buttons, fill forms, etc.):
|
|
9936
|
+
1. Use shell to launch applications: \`firefox https://example.com &\`
|
|
9937
|
+
2. Use screenshot or desktop_describe to see what's on screen
|
|
9938
|
+
3. Use desktop_click to click UI elements: \`desktop_click({target: "Sign Up button"})\`
|
|
9939
|
+
4. Use shell with xdotool for keyboard input: \`xdotool type "username"\` and \`xdotool key Return\`
|
|
9940
|
+
5. Use shell with xdotool for navigation: \`xdotool key Tab\`, \`xdotool key ctrl+l\`
|
|
9941
|
+
6. Take screenshots between steps to verify progress
|
|
9942
|
+
|
|
9943
|
+
You CAN open Firefox, Chrome, or any application. You CAN click buttons, fill forms, and navigate websites.
|
|
9944
|
+
You CAN use xdotool for keyboard/mouse control. These are real capabilities, not hypothetical.
|
|
9920
9945
|
|
|
9921
9946
|
### Self-Guided Image Exploration
|
|
9922
9947
|
|
|
@@ -11712,10 +11737,12 @@ var init_dist5 = __esm({
|
|
|
11712
11737
|
|
|
11713
11738
|
// packages/cli/dist/tui/listen.js
|
|
11714
11739
|
import { spawn as spawn7, execSync as execSync12 } from "node:child_process";
|
|
11715
|
-
import { existsSync as existsSync13, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "node:fs";
|
|
11716
|
-
import { join as join19 } from "node:path";
|
|
11740
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5, readdirSync as readdirSync6 } from "node:fs";
|
|
11741
|
+
import { join as join19, dirname as dirname6 } from "node:path";
|
|
11717
11742
|
import { homedir as homedir6 } from "node:os";
|
|
11743
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
11718
11744
|
import { EventEmitter } from "node:events";
|
|
11745
|
+
import { createInterface as createInterface2 } from "node:readline";
|
|
11719
11746
|
function isAudioPath(path) {
|
|
11720
11747
|
const ext = path.toLowerCase().split(".").pop();
|
|
11721
11748
|
return ext ? AUDIO_EXTENSIONS.has(`.${ext}`) : false;
|
|
@@ -11794,6 +11821,49 @@ function findMicCaptureCommand() {
|
|
|
11794
11821
|
}
|
|
11795
11822
|
return null;
|
|
11796
11823
|
}
|
|
11824
|
+
function findLiveWhisperScript() {
|
|
11825
|
+
const thisDir = dirname6(fileURLToPath3(import.meta.url));
|
|
11826
|
+
const candidates = [
|
|
11827
|
+
join19(thisDir, "../../../../packages/execution/scripts/live-whisper.py"),
|
|
11828
|
+
join19(thisDir, "../../../packages/execution/scripts/live-whisper.py"),
|
|
11829
|
+
join19(thisDir, "../../execution/scripts/live-whisper.py"),
|
|
11830
|
+
// npm install layout — scripts bundled alongside dist
|
|
11831
|
+
join19(thisDir, "../scripts/live-whisper.py"),
|
|
11832
|
+
join19(thisDir, "../../scripts/live-whisper.py")
|
|
11833
|
+
];
|
|
11834
|
+
for (const p of candidates) {
|
|
11835
|
+
if (existsSync13(p))
|
|
11836
|
+
return p;
|
|
11837
|
+
}
|
|
11838
|
+
try {
|
|
11839
|
+
const globalRoot = execSync12("npm root -g", {
|
|
11840
|
+
encoding: "utf-8",
|
|
11841
|
+
timeout: 5e3,
|
|
11842
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
11843
|
+
}).trim();
|
|
11844
|
+
const candidates2 = [
|
|
11845
|
+
join19(globalRoot, "open-agents-ai", "dist", "scripts", "live-whisper.py"),
|
|
11846
|
+
join19(globalRoot, "open-agents-ai", "scripts", "live-whisper.py")
|
|
11847
|
+
];
|
|
11848
|
+
for (const p of candidates2) {
|
|
11849
|
+
if (existsSync13(p))
|
|
11850
|
+
return p;
|
|
11851
|
+
}
|
|
11852
|
+
} catch {
|
|
11853
|
+
}
|
|
11854
|
+
const nvmBase = join19(homedir6(), ".nvm", "versions", "node");
|
|
11855
|
+
if (existsSync13(nvmBase)) {
|
|
11856
|
+
try {
|
|
11857
|
+
for (const ver of readdirSync6(nvmBase)) {
|
|
11858
|
+
const p = join19(nvmBase, ver, "lib", "node_modules", "open-agents-ai", "dist", "scripts", "live-whisper.py");
|
|
11859
|
+
if (existsSync13(p))
|
|
11860
|
+
return p;
|
|
11861
|
+
}
|
|
11862
|
+
} catch {
|
|
11863
|
+
}
|
|
11864
|
+
}
|
|
11865
|
+
return null;
|
|
11866
|
+
}
|
|
11797
11867
|
function ensureTranscribeCliBackground() {
|
|
11798
11868
|
if (_bgInstallPromise)
|
|
11799
11869
|
return;
|
|
@@ -11827,7 +11897,7 @@ function getListenEngine(config) {
|
|
|
11827
11897
|
}
|
|
11828
11898
|
return _engine;
|
|
11829
11899
|
}
|
|
11830
|
-
var AUDIO_EXTENSIONS, VIDEO_EXTENSIONS, ListenEngine, _bgInstallPromise, _engine;
|
|
11900
|
+
var AUDIO_EXTENSIONS, VIDEO_EXTENSIONS, WhisperFallbackTranscriber, ListenEngine, _bgInstallPromise, _engine;
|
|
11831
11901
|
var init_listen = __esm({
|
|
11832
11902
|
"packages/cli/dist/tui/listen.js"() {
|
|
11833
11903
|
"use strict";
|
|
@@ -11852,11 +11922,104 @@ var init_listen = __esm({
|
|
|
11852
11922
|
".m4v",
|
|
11853
11923
|
".ts"
|
|
11854
11924
|
]);
|
|
11925
|
+
WhisperFallbackTranscriber = class extends EventEmitter {
|
|
11926
|
+
model;
|
|
11927
|
+
scriptPath;
|
|
11928
|
+
process = null;
|
|
11929
|
+
_ready = false;
|
|
11930
|
+
constructor(model, scriptPath2) {
|
|
11931
|
+
super();
|
|
11932
|
+
this.model = model;
|
|
11933
|
+
this.scriptPath = scriptPath2;
|
|
11934
|
+
}
|
|
11935
|
+
get ready() {
|
|
11936
|
+
return this._ready;
|
|
11937
|
+
}
|
|
11938
|
+
async start() {
|
|
11939
|
+
return new Promise((resolve19, reject) => {
|
|
11940
|
+
const timeout = setTimeout(() => {
|
|
11941
|
+
reject(new Error("Whisper fallback: model load timeout (5 min). First run downloads the model."));
|
|
11942
|
+
}, 3e5);
|
|
11943
|
+
this.process = spawn7("python3", [
|
|
11944
|
+
this.scriptPath,
|
|
11945
|
+
"--model",
|
|
11946
|
+
this.model,
|
|
11947
|
+
"--chunk-seconds",
|
|
11948
|
+
"3",
|
|
11949
|
+
"--window-seconds",
|
|
11950
|
+
"10"
|
|
11951
|
+
], {
|
|
11952
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
11953
|
+
env: { ...process.env }
|
|
11954
|
+
});
|
|
11955
|
+
const rl = createInterface2({ input: this.process.stdout });
|
|
11956
|
+
rl.on("line", (line) => {
|
|
11957
|
+
try {
|
|
11958
|
+
const evt = JSON.parse(line);
|
|
11959
|
+
switch (evt.type) {
|
|
11960
|
+
case "status":
|
|
11961
|
+
this.emit("status", evt.message);
|
|
11962
|
+
break;
|
|
11963
|
+
case "ready":
|
|
11964
|
+
this._ready = true;
|
|
11965
|
+
clearTimeout(timeout);
|
|
11966
|
+
this.emit("ready");
|
|
11967
|
+
resolve19();
|
|
11968
|
+
break;
|
|
11969
|
+
case "transcript":
|
|
11970
|
+
this.emit("transcript", {
|
|
11971
|
+
text: evt.text,
|
|
11972
|
+
isFinal: evt.isFinal ?? false
|
|
11973
|
+
});
|
|
11974
|
+
break;
|
|
11975
|
+
case "error":
|
|
11976
|
+
this.emit("error", new Error(evt.message));
|
|
11977
|
+
break;
|
|
11978
|
+
}
|
|
11979
|
+
} catch {
|
|
11980
|
+
}
|
|
11981
|
+
});
|
|
11982
|
+
this.process.stderr?.on("data", (data) => {
|
|
11983
|
+
const text = data.toString().trim();
|
|
11984
|
+
if (text)
|
|
11985
|
+
this.emit("status", text);
|
|
11986
|
+
});
|
|
11987
|
+
this.process.on("error", (err) => {
|
|
11988
|
+
clearTimeout(timeout);
|
|
11989
|
+
reject(err);
|
|
11990
|
+
});
|
|
11991
|
+
this.process.on("close", (code) => {
|
|
11992
|
+
if (!this._ready) {
|
|
11993
|
+
clearTimeout(timeout);
|
|
11994
|
+
reject(new Error(`Whisper worker exited with code ${code} before ready`));
|
|
11995
|
+
}
|
|
11996
|
+
});
|
|
11997
|
+
});
|
|
11998
|
+
}
|
|
11999
|
+
/** Write PCM16 audio data to the worker's stdin. */
|
|
12000
|
+
write(chunk) {
|
|
12001
|
+
if (this.process?.stdin?.writable) {
|
|
12002
|
+
this.process.stdin.write(chunk);
|
|
12003
|
+
}
|
|
12004
|
+
}
|
|
12005
|
+
/** Stop the worker. */
|
|
12006
|
+
stop() {
|
|
12007
|
+
if (this.process) {
|
|
12008
|
+
try {
|
|
12009
|
+
this.process.stdin?.end();
|
|
12010
|
+
this.process.kill("SIGTERM");
|
|
12011
|
+
} catch {
|
|
12012
|
+
}
|
|
12013
|
+
this.process = null;
|
|
12014
|
+
}
|
|
12015
|
+
this._ready = false;
|
|
12016
|
+
}
|
|
12017
|
+
};
|
|
11855
12018
|
ListenEngine = class extends EventEmitter {
|
|
11856
12019
|
config;
|
|
11857
12020
|
micProcess = null;
|
|
11858
12021
|
liveTranscriber = null;
|
|
11859
|
-
// TranscribeLive from transcribe-cli
|
|
12022
|
+
// TranscribeLive from transcribe-cli or WhisperFallbackTranscriber
|
|
11860
12023
|
active = false;
|
|
11861
12024
|
silenceTimer = null;
|
|
11862
12025
|
countdownInterval = null;
|
|
@@ -11939,8 +12102,8 @@ var init_listen = __esm({
|
|
|
11939
12102
|
const nvmBase = join19(homedir6(), ".nvm", "versions", "node");
|
|
11940
12103
|
if (existsSync13(nvmBase)) {
|
|
11941
12104
|
try {
|
|
11942
|
-
const { readdirSync:
|
|
11943
|
-
for (const ver of
|
|
12105
|
+
const { readdirSync: readdirSync10 } = await import("node:fs");
|
|
12106
|
+
for (const ver of readdirSync10(nvmBase)) {
|
|
11944
12107
|
const tcPath = join19(nvmBase, ver, "lib", "node_modules", "transcribe-cli");
|
|
11945
12108
|
if (existsSync13(join19(tcPath, "dist", "index.js"))) {
|
|
11946
12109
|
const { createRequire: createRequire4 } = await import("node:module");
|
|
@@ -11972,7 +12135,6 @@ var init_listen = __esm({
|
|
|
11972
12135
|
tc = await this.loadTranscribeCli();
|
|
11973
12136
|
}
|
|
11974
12137
|
if (!tc) {
|
|
11975
|
-
this.emit("info", "Installing transcribe-cli...");
|
|
11976
12138
|
try {
|
|
11977
12139
|
execSync12("npm i -g transcribe-cli", { stdio: "pipe", timeout: 18e4 });
|
|
11978
12140
|
this.transcribeCliAvailable = null;
|
|
@@ -11980,62 +12142,85 @@ var init_listen = __esm({
|
|
|
11980
12142
|
} catch {
|
|
11981
12143
|
}
|
|
11982
12144
|
}
|
|
11983
|
-
if (!tc) {
|
|
11984
|
-
return "Failed to install transcribe-cli. Try manually: npm i -g transcribe-cli";
|
|
11985
|
-
}
|
|
11986
12145
|
}
|
|
11987
|
-
|
|
11988
|
-
|
|
11989
|
-
|
|
11990
|
-
|
|
11991
|
-
|
|
11992
|
-
|
|
11993
|
-
|
|
11994
|
-
|
|
11995
|
-
|
|
11996
|
-
|
|
11997
|
-
|
|
11998
|
-
|
|
11999
|
-
|
|
12000
|
-
|
|
12001
|
-
|
|
12002
|
-
|
|
12003
|
-
|
|
12146
|
+
let usedFallback = false;
|
|
12147
|
+
let transcribeCliError = null;
|
|
12148
|
+
if (tc) {
|
|
12149
|
+
const TranscribeLive = tc.TranscribeLive;
|
|
12150
|
+
if (TranscribeLive) {
|
|
12151
|
+
try {
|
|
12152
|
+
this.liveTranscriber = new TranscribeLive({
|
|
12153
|
+
model: this.config.model,
|
|
12154
|
+
sampleRate: 16e3,
|
|
12155
|
+
channels: 1,
|
|
12156
|
+
sampleWidth: 2,
|
|
12157
|
+
chunkDuration: 3
|
|
12158
|
+
});
|
|
12159
|
+
this.liveTranscriber.on("transcript", (evt) => {
|
|
12160
|
+
if (!evt.text.trim())
|
|
12161
|
+
return;
|
|
12162
|
+
this.lastTranscriptTime = Date.now();
|
|
12163
|
+
this.pendingText = evt.text.trim();
|
|
12164
|
+
this.emit("transcript", this.pendingText, evt.isFinal);
|
|
12165
|
+
if (this.config.mode === "auto")
|
|
12166
|
+
this.resetSilenceTimer();
|
|
12167
|
+
});
|
|
12168
|
+
this.liveTranscriber.on("error", (err) => {
|
|
12169
|
+
this.emit("error", err);
|
|
12170
|
+
});
|
|
12171
|
+
await new Promise((resolve19, reject) => {
|
|
12172
|
+
const timeout = setTimeout(() => reject(new Error("Model load timeout (60s)")), 6e4);
|
|
12173
|
+
this.liveTranscriber.on("ready", () => {
|
|
12174
|
+
clearTimeout(timeout);
|
|
12175
|
+
resolve19();
|
|
12176
|
+
});
|
|
12177
|
+
this.liveTranscriber.on("error", (err) => {
|
|
12178
|
+
clearTimeout(timeout);
|
|
12179
|
+
reject(err);
|
|
12180
|
+
});
|
|
12181
|
+
});
|
|
12182
|
+
} catch (err) {
|
|
12183
|
+
try {
|
|
12184
|
+
this.liveTranscriber?.stop();
|
|
12185
|
+
} catch {
|
|
12186
|
+
}
|
|
12187
|
+
this.liveTranscriber = null;
|
|
12188
|
+
transcribeCliError = err instanceof Error ? err.message : String(err);
|
|
12189
|
+
}
|
|
12190
|
+
}
|
|
12004
12191
|
}
|
|
12005
|
-
this.liveTranscriber
|
|
12006
|
-
|
|
12007
|
-
|
|
12008
|
-
|
|
12009
|
-
|
|
12010
|
-
this.emit("transcript", this.pendingText, evt.isFinal);
|
|
12011
|
-
if (this.config.mode === "auto") {
|
|
12012
|
-
this.resetSilenceTimer();
|
|
12192
|
+
if (!this.liveTranscriber) {
|
|
12193
|
+
const scriptPath2 = findLiveWhisperScript();
|
|
12194
|
+
if (!scriptPath2) {
|
|
12195
|
+
const hint = transcribeCliError ? ` transcribe-cli error: ${transcribeCliError}` : "";
|
|
12196
|
+
return `No transcription backend available.${hint} live-whisper.py not found.`;
|
|
12013
12197
|
}
|
|
12014
|
-
|
|
12015
|
-
|
|
12016
|
-
|
|
12017
|
-
|
|
12018
|
-
|
|
12019
|
-
await new Promise((resolve19, reject) => {
|
|
12020
|
-
const timeout = setTimeout(() => reject(new Error("Model load timeout (60s)")), 6e4);
|
|
12021
|
-
this.liveTranscriber.on("ready", () => {
|
|
12022
|
-
clearTimeout(timeout);
|
|
12023
|
-
resolve19();
|
|
12198
|
+
try {
|
|
12199
|
+
const fallback = new WhisperFallbackTranscriber(this.config.model, scriptPath2);
|
|
12200
|
+
usedFallback = true;
|
|
12201
|
+
fallback.on("status", (msg) => {
|
|
12202
|
+
this.emit("info", msg);
|
|
12024
12203
|
});
|
|
12025
|
-
|
|
12026
|
-
|
|
12027
|
-
|
|
12204
|
+
await fallback.start();
|
|
12205
|
+
fallback.on("transcript", (evt) => {
|
|
12206
|
+
if (!evt.text.trim())
|
|
12207
|
+
return;
|
|
12208
|
+
this.lastTranscriptTime = Date.now();
|
|
12209
|
+
this.pendingText = evt.text.trim();
|
|
12210
|
+
this.emit("transcript", this.pendingText, evt.isFinal);
|
|
12211
|
+
if (this.config.mode === "auto")
|
|
12212
|
+
this.resetSilenceTimer();
|
|
12028
12213
|
});
|
|
12029
|
-
|
|
12030
|
-
|
|
12031
|
-
|
|
12032
|
-
this.liveTranscriber
|
|
12033
|
-
} catch {
|
|
12214
|
+
fallback.on("error", (err) => {
|
|
12215
|
+
this.emit("error", err);
|
|
12216
|
+
});
|
|
12217
|
+
this.liveTranscriber = fallback;
|
|
12218
|
+
} catch (err) {
|
|
12219
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
12220
|
+
const tcHint = transcribeCliError ? `
|
|
12221
|
+
transcribe-cli error: ${transcribeCliError}` : "";
|
|
12222
|
+
return `Failed to start live transcription: ${msg}${tcHint}`;
|
|
12034
12223
|
}
|
|
12035
|
-
this.liveTranscriber = null;
|
|
12036
|
-
const arch = process.arch;
|
|
12037
|
-
const armHint = arch === "arm64" || arch === "arm" ? ` The whisper model or phonemizer WASM may not support ${process.platform}-${arch}. File transcription (/transcribe <file>) may still work.` : "";
|
|
12038
|
-
return `Failed to start live transcription: ${err instanceof Error ? err.message : String(err)}${armHint}`;
|
|
12039
12224
|
}
|
|
12040
12225
|
this.micProcess = spawn7(micCmd.cmd, micCmd.args, {
|
|
12041
12226
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -12067,7 +12252,8 @@ var init_listen = __esm({
|
|
|
12067
12252
|
this.emit("started");
|
|
12068
12253
|
this.emit("recording", true);
|
|
12069
12254
|
const modeDesc = this.config.mode === "auto" ? `auto (${this.config.silenceTimeoutMs / 1e3}s timeout)` : "confirm (press Enter to submit)";
|
|
12070
|
-
|
|
12255
|
+
const backend = usedFallback ? "openai-whisper" : "transcribe-cli";
|
|
12256
|
+
return `Listening with ${this.config.model} model via ${backend} (${modeDesc})`;
|
|
12071
12257
|
}
|
|
12072
12258
|
/**
|
|
12073
12259
|
* Stop listening — cleanup mic, transcriber, timers.
|
|
@@ -13317,8 +13503,8 @@ Approach this task thoughtfully:
|
|
|
13317
13503
|
});
|
|
13318
13504
|
|
|
13319
13505
|
// packages/prompts/dist/index.js
|
|
13320
|
-
import { join as join20, dirname as
|
|
13321
|
-
import { fileURLToPath as
|
|
13506
|
+
import { join as join20, dirname as dirname7 } from "node:path";
|
|
13507
|
+
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
13322
13508
|
var _dir, _packageRoot;
|
|
13323
13509
|
var init_dist6 = __esm({
|
|
13324
13510
|
"packages/prompts/dist/index.js"() {
|
|
@@ -13327,13 +13513,13 @@ var init_dist6 = __esm({
|
|
|
13327
13513
|
init_render2();
|
|
13328
13514
|
init_task_templates();
|
|
13329
13515
|
init_render2();
|
|
13330
|
-
_dir =
|
|
13516
|
+
_dir = dirname7(fileURLToPath4(import.meta.url));
|
|
13331
13517
|
_packageRoot = join20(_dir, "..");
|
|
13332
13518
|
}
|
|
13333
13519
|
});
|
|
13334
13520
|
|
|
13335
13521
|
// packages/cli/dist/tui/oa-directory.js
|
|
13336
|
-
import { existsSync as existsSync14, mkdirSync as mkdirSync6, readFileSync as readFileSync12, writeFileSync as writeFileSync6, readdirSync as
|
|
13522
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync6, readFileSync as readFileSync12, writeFileSync as writeFileSync6, readdirSync as readdirSync7, statSync as statSync6, unlinkSync as unlinkSync2 } from "node:fs";
|
|
13337
13523
|
import { join as join21, relative as relative2, basename as basename5, extname as extname8 } from "node:path";
|
|
13338
13524
|
import { homedir as homedir7 } from "node:os";
|
|
13339
13525
|
function initOaDirectory(repoRoot) {
|
|
@@ -13514,7 +13700,7 @@ function loadRecentSessions(repoRoot, limit = 5) {
|
|
|
13514
13700
|
if (!existsSync14(historyDir))
|
|
13515
13701
|
return [];
|
|
13516
13702
|
try {
|
|
13517
|
-
const files =
|
|
13703
|
+
const files = readdirSync7(historyDir).filter((f) => f.endsWith(".json")).map((f) => {
|
|
13518
13704
|
const stat5 = statSync6(join21(historyDir, f));
|
|
13519
13705
|
return { file: f, mtime: stat5.mtimeMs };
|
|
13520
13706
|
}).sort((a, b) => b.mtime - a.mtime).slice(0, limit);
|
|
@@ -13612,7 +13798,7 @@ function buildDirTree(root, maxDepth, prefix = "", depth = 0) {
|
|
|
13612
13798
|
return "";
|
|
13613
13799
|
let result = "";
|
|
13614
13800
|
try {
|
|
13615
|
-
const entries =
|
|
13801
|
+
const entries = readdirSync7(root, { withFileTypes: true }).filter((e) => !e.name.startsWith(".") || e.name === ".github").filter((e) => !SKIP_DIRS.has(e.name)).sort((a, b) => {
|
|
13616
13802
|
if (a.isDirectory() && !b.isDirectory())
|
|
13617
13803
|
return -1;
|
|
13618
13804
|
if (!a.isDirectory() && b.isDirectory())
|
|
@@ -13627,7 +13813,7 @@ function buildDirTree(root, maxDepth, prefix = "", depth = 0) {
|
|
|
13627
13813
|
if (entry.isDirectory()) {
|
|
13628
13814
|
let fileCount = 0;
|
|
13629
13815
|
try {
|
|
13630
|
-
fileCount =
|
|
13816
|
+
fileCount = readdirSync7(join21(root, entry.name)).filter((f) => !f.startsWith(".")).length;
|
|
13631
13817
|
} catch {
|
|
13632
13818
|
}
|
|
13633
13819
|
result += `${prefix}${connector}${entry.name}/ (${fileCount})
|
|
@@ -14843,11 +15029,11 @@ async function handleUpdate(subcommand, ctx) {
|
|
|
14843
15029
|
let currentVersion = "0.0.0";
|
|
14844
15030
|
try {
|
|
14845
15031
|
const { createRequire: createRequire4 } = await import("node:module");
|
|
14846
|
-
const { fileURLToPath:
|
|
14847
|
-
const { dirname:
|
|
15032
|
+
const { fileURLToPath: fileURLToPath7 } = await import("node:url");
|
|
15033
|
+
const { dirname: dirname10, join: join32 } = await import("node:path");
|
|
14848
15034
|
const { existsSync: existsSync21 } = await import("node:fs");
|
|
14849
15035
|
const req = createRequire4(import.meta.url);
|
|
14850
|
-
const thisDir =
|
|
15036
|
+
const thisDir = dirname10(fileURLToPath7(import.meta.url));
|
|
14851
15037
|
const candidates = [
|
|
14852
15038
|
join32(thisDir, "..", "package.json"),
|
|
14853
15039
|
join32(thisDir, "..", "..", "package.json"),
|
|
@@ -14959,7 +15145,7 @@ var init_commands = __esm({
|
|
|
14959
15145
|
});
|
|
14960
15146
|
|
|
14961
15147
|
// packages/cli/dist/tui/project-context.js
|
|
14962
|
-
import { existsSync as existsSync16, readFileSync as readFileSync13, readdirSync as
|
|
15148
|
+
import { existsSync as existsSync16, readFileSync as readFileSync13, readdirSync as readdirSync8 } from "node:fs";
|
|
14963
15149
|
import { join as join23, basename as basename6 } from "node:path";
|
|
14964
15150
|
import { execSync as execSync14 } from "node:child_process";
|
|
14965
15151
|
import { homedir as homedir9, platform, release } from "node:os";
|
|
@@ -15045,7 +15231,7 @@ function loadMemoryDir(memDir, scope) {
|
|
|
15045
15231
|
return "";
|
|
15046
15232
|
const lines = [];
|
|
15047
15233
|
try {
|
|
15048
|
-
const files =
|
|
15234
|
+
const files = readdirSync8(memDir).filter((f) => f.endsWith(".json"));
|
|
15049
15235
|
for (const file of files.slice(0, 10)) {
|
|
15050
15236
|
try {
|
|
15051
15237
|
const raw = readFileSync13(join23(memDir, file), "utf-8");
|
|
@@ -17246,7 +17432,7 @@ var init_edit_history = __esm({
|
|
|
17246
17432
|
});
|
|
17247
17433
|
|
|
17248
17434
|
// packages/cli/dist/tui/dream-engine.js
|
|
17249
|
-
import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync9, readFileSync as readFileSync15, existsSync as existsSync18, cpSync, rmSync, readdirSync as
|
|
17435
|
+
import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync9, readFileSync as readFileSync15, existsSync as existsSync18, cpSync, rmSync, readdirSync as readdirSync9 } from "node:fs";
|
|
17250
17436
|
import { join as join26, basename as basename7 } from "node:path";
|
|
17251
17437
|
import { execSync as execSync16 } from "node:child_process";
|
|
17252
17438
|
function adaptTool(tool) {
|
|
@@ -17784,7 +17970,7 @@ Each proposal includes implementation entrypoints and estimated effort.
|
|
|
17784
17970
|
/** Update the master proposal index */
|
|
17785
17971
|
updateProposalIndex() {
|
|
17786
17972
|
try {
|
|
17787
|
-
const files =
|
|
17973
|
+
const files = readdirSync9(this.dreamsDir).filter((f) => f.endsWith(".md") && f !== "PROPOSAL-INDEX.md" && f !== "dream-state.json").sort();
|
|
17788
17974
|
const index = `# Dream Proposals Index
|
|
17789
17975
|
|
|
17790
17976
|
**Last updated**: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}
|
|
@@ -18186,15 +18372,15 @@ var init_status_bar = __esm({
|
|
|
18186
18372
|
import * as readline2 from "node:readline";
|
|
18187
18373
|
import { Writable } from "node:stream";
|
|
18188
18374
|
import { cwd } from "node:process";
|
|
18189
|
-
import { resolve as resolve16, join as join27, dirname as
|
|
18375
|
+
import { resolve as resolve16, join as join27, dirname as dirname8, extname as extname9 } from "node:path";
|
|
18190
18376
|
import { createRequire as createRequire2 } from "node:module";
|
|
18191
|
-
import { fileURLToPath as
|
|
18377
|
+
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
18192
18378
|
import { readFileSync as readFileSync16 } from "node:fs";
|
|
18193
18379
|
import { existsSync as existsSync19 } from "node:fs";
|
|
18194
18380
|
function getVersion() {
|
|
18195
18381
|
try {
|
|
18196
18382
|
const require2 = createRequire2(import.meta.url);
|
|
18197
|
-
const thisDir =
|
|
18383
|
+
const thisDir = dirname8(fileURLToPath5(import.meta.url));
|
|
18198
18384
|
const candidates = [
|
|
18199
18385
|
join27(thisDir, "..", "package.json"),
|
|
18200
18386
|
join27(thisDir, "..", "..", "package.json"),
|
|
@@ -20308,8 +20494,8 @@ init_output();
|
|
|
20308
20494
|
init_updater();
|
|
20309
20495
|
import { parseArgs as nodeParseArgs2 } from "node:util";
|
|
20310
20496
|
import { createRequire as createRequire3 } from "node:module";
|
|
20311
|
-
import { fileURLToPath as
|
|
20312
|
-
import { dirname as
|
|
20497
|
+
import { fileURLToPath as fileURLToPath6 } from "node:url";
|
|
20498
|
+
import { dirname as dirname9, join as join31 } from "node:path";
|
|
20313
20499
|
|
|
20314
20500
|
// packages/cli/dist/cli.js
|
|
20315
20501
|
import { createInterface } from "node:readline";
|
|
@@ -20416,7 +20602,7 @@ init_output();
|
|
|
20416
20602
|
function getVersion2() {
|
|
20417
20603
|
try {
|
|
20418
20604
|
const require2 = createRequire3(import.meta.url);
|
|
20419
|
-
const pkgPath = join31(
|
|
20605
|
+
const pkgPath = join31(dirname9(fileURLToPath6(import.meta.url)), "..", "package.json");
|
|
20420
20606
|
const pkg = require2(pkgPath);
|
|
20421
20607
|
return pkg.version;
|
|
20422
20608
|
} catch {
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
live-whisper.py — Self-contained live transcription worker using openai-whisper.
|
|
4
|
+
|
|
5
|
+
Fallback for transcribe-cli on platforms where faster-whisper / ONNX fails
|
|
6
|
+
(e.g. linux-arm64). Auto-creates a Python venv and installs openai-whisper.
|
|
7
|
+
|
|
8
|
+
Protocol:
|
|
9
|
+
stdin — raw PCM16 audio (16kHz, mono, 16-bit signed LE)
|
|
10
|
+
stdout — JSON lines:
|
|
11
|
+
{"type":"status","message":"Installing dependencies..."}
|
|
12
|
+
{"type":"status","message":"Loading model..."}
|
|
13
|
+
{"type":"ready"}
|
|
14
|
+
{"type":"transcript","text":"hello world","isFinal":false}
|
|
15
|
+
{"type":"transcript","text":"hello world how are you","isFinal":true}
|
|
16
|
+
{"type":"error","message":"..."}
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
arecord -f S16_LE -r 16000 -c 1 -t raw -q - | python3 live-whisper.py --model base
|
|
20
|
+
|
|
21
|
+
Based on the proven ARM-compatible approach from hydra/whisper_asr.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import sys
|
|
25
|
+
import os
|
|
26
|
+
import json
|
|
27
|
+
import subprocess
|
|
28
|
+
import importlib
|
|
29
|
+
import struct
|
|
30
|
+
import time
|
|
31
|
+
import threading
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
# Configuration
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
39
|
+
VENV = SCRIPT_DIR / ".whisper-venv"
|
|
40
|
+
PY = VENV / "bin" / "python"
|
|
41
|
+
PIP = VENV / "bin" / "pip"
|
|
42
|
+
|
|
43
|
+
SAMPLE_RATE = 16000
|
|
44
|
+
CHANNELS = 1
|
|
45
|
+
SAMPLE_WIDTH = 2 # 16-bit
|
|
46
|
+
CHUNK_SECONDS = 3 # Transcribe every N seconds of audio
|
|
47
|
+
WINDOW_SECONDS = 10 # Transcribe last N seconds (sliding window)
|
|
48
|
+
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
# Output helpers (JSON lines to stdout)
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
def emit(event: dict):
|
|
54
|
+
"""Write a JSON event to stdout and flush."""
|
|
55
|
+
sys.stdout.write(json.dumps(event) + "\n")
|
|
56
|
+
sys.stdout.flush()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def emit_status(msg: str):
|
|
60
|
+
emit({"type": "status", "message": msg})
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def emit_error(msg: str):
|
|
64
|
+
emit({"type": "error", "message": msg})
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def emit_transcript(text: str, is_final: bool = False):
|
|
68
|
+
emit({"type": "transcript", "text": text, "isFinal": is_final})
|
|
69
|
+
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
# Venv bootstrap (inspired by hydra/whisper_asr)
|
|
72
|
+
# ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
def _in_venv() -> bool:
|
|
75
|
+
return sys.prefix != sys.base_prefix
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _ensure_venv():
|
|
79
|
+
if VENV.exists():
|
|
80
|
+
return
|
|
81
|
+
emit_status("Creating Python venv for Whisper...")
|
|
82
|
+
import venv
|
|
83
|
+
venv.EnvBuilder(with_pip=True).create(str(VENV))
|
|
84
|
+
# Upgrade pip quietly
|
|
85
|
+
subprocess.check_call(
|
|
86
|
+
[str(PY), "-m", "pip", "install", "--upgrade", "pip"],
|
|
87
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _ensure_deps():
|
|
92
|
+
"""Check and install missing dependencies."""
|
|
93
|
+
need = []
|
|
94
|
+
try:
|
|
95
|
+
import numpy
|
|
96
|
+
except ImportError:
|
|
97
|
+
need.append("numpy")
|
|
98
|
+
try:
|
|
99
|
+
import whisper
|
|
100
|
+
except ImportError:
|
|
101
|
+
need.append("openai-whisper")
|
|
102
|
+
|
|
103
|
+
if need:
|
|
104
|
+
emit_status(f"Installing: {', '.join(need)}...")
|
|
105
|
+
subprocess.check_call(
|
|
106
|
+
[str(PIP), "install", *need],
|
|
107
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.PIPE,
|
|
108
|
+
)
|
|
109
|
+
# Force reimport
|
|
110
|
+
for mod in ["numpy", "whisper"]:
|
|
111
|
+
if mod in sys.modules:
|
|
112
|
+
del sys.modules[mod]
|
|
113
|
+
|
|
114
|
+
# ---------------------------------------------------------------------------
|
|
115
|
+
# Bootstrap: ensure we're in the venv with deps
|
|
116
|
+
# ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
if not _in_venv():
|
|
119
|
+
_ensure_venv()
|
|
120
|
+
# Re-exec this script inside the venv
|
|
121
|
+
os.execv(str(PY), [str(PY)] + sys.argv)
|
|
122
|
+
|
|
123
|
+
_ensure_deps()
|
|
124
|
+
|
|
125
|
+
# Now safe to import
|
|
126
|
+
import numpy as np
|
|
127
|
+
|
|
128
|
+
# ---------------------------------------------------------------------------
|
|
129
|
+
# Main transcription loop
|
|
130
|
+
# ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
def main():
|
|
133
|
+
import argparse
|
|
134
|
+
parser = argparse.ArgumentParser(description="Live Whisper transcription worker")
|
|
135
|
+
parser.add_argument("--model", default="base", help="Whisper model size (tiny/base/small/medium/large)")
|
|
136
|
+
parser.add_argument("--chunk-seconds", type=float, default=CHUNK_SECONDS, help="Transcribe interval")
|
|
137
|
+
parser.add_argument("--window-seconds", type=float, default=WINDOW_SECONDS, help="Sliding window size")
|
|
138
|
+
parser.add_argument("--language", default=None, help="Language code (e.g. en, es, fr). Auto-detect if omitted.")
|
|
139
|
+
args = parser.parse_args()
|
|
140
|
+
|
|
141
|
+
import whisper
|
|
142
|
+
|
|
143
|
+
# Load model
|
|
144
|
+
emit_status(f"Loading Whisper {args.model} model...")
|
|
145
|
+
try:
|
|
146
|
+
device = "cpu"
|
|
147
|
+
try:
|
|
148
|
+
import torch
|
|
149
|
+
if torch.cuda.is_available():
|
|
150
|
+
device = "cuda"
|
|
151
|
+
except ImportError:
|
|
152
|
+
pass
|
|
153
|
+
|
|
154
|
+
model = whisper.load_model(args.model, device=device)
|
|
155
|
+
except Exception as e:
|
|
156
|
+
emit_error(f"Failed to load model: {e}")
|
|
157
|
+
sys.exit(1)
|
|
158
|
+
|
|
159
|
+
emit({"type": "ready"})
|
|
160
|
+
|
|
161
|
+
# Audio buffer — accumulates PCM16 as float32 @ 16kHz
|
|
162
|
+
audio_buf = np.zeros(0, dtype=np.float32)
|
|
163
|
+
buf_lock = threading.Lock()
|
|
164
|
+
chunk_bytes = int(args.chunk_seconds * SAMPLE_RATE * SAMPLE_WIDTH)
|
|
165
|
+
window_samples = int(args.window_seconds * SAMPLE_RATE)
|
|
166
|
+
last_text = ""
|
|
167
|
+
running = True
|
|
168
|
+
|
|
169
|
+
def read_stdin():
|
|
170
|
+
"""Read PCM16 from stdin in a background thread."""
|
|
171
|
+
nonlocal audio_buf, running
|
|
172
|
+
try:
|
|
173
|
+
while running:
|
|
174
|
+
data = sys.stdin.buffer.read(chunk_bytes)
|
|
175
|
+
if not data:
|
|
176
|
+
break # EOF
|
|
177
|
+
# Convert PCM16 LE to float32 [-1, 1]
|
|
178
|
+
samples = np.frombuffer(data, dtype=np.int16).astype(np.float32) / 32768.0
|
|
179
|
+
with buf_lock:
|
|
180
|
+
audio_buf = np.concatenate([audio_buf, samples])
|
|
181
|
+
except Exception:
|
|
182
|
+
pass
|
|
183
|
+
finally:
|
|
184
|
+
running = False
|
|
185
|
+
|
|
186
|
+
# Start stdin reader thread
|
|
187
|
+
reader = threading.Thread(target=read_stdin, daemon=True)
|
|
188
|
+
reader.start()
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
while running:
|
|
192
|
+
time.sleep(args.chunk_seconds)
|
|
193
|
+
|
|
194
|
+
with buf_lock:
|
|
195
|
+
if len(audio_buf) < SAMPLE_RATE: # Need at least 1s of audio
|
|
196
|
+
continue
|
|
197
|
+
# Take the last window_seconds of audio
|
|
198
|
+
window = audio_buf[-window_samples:].copy() if len(audio_buf) > window_samples else audio_buf.copy()
|
|
199
|
+
|
|
200
|
+
# Transcribe
|
|
201
|
+
try:
|
|
202
|
+
fp16 = (device == "cuda")
|
|
203
|
+
result = model.transcribe(
|
|
204
|
+
window,
|
|
205
|
+
fp16=fp16,
|
|
206
|
+
language=args.language,
|
|
207
|
+
no_speech_threshold=0.6,
|
|
208
|
+
condition_on_previous_text=False,
|
|
209
|
+
)
|
|
210
|
+
text = result.get("text", "").strip()
|
|
211
|
+
if text and text != last_text:
|
|
212
|
+
last_text = text
|
|
213
|
+
emit_transcript(text, is_final=False)
|
|
214
|
+
except Exception as e:
|
|
215
|
+
emit_error(f"Transcription error: {e}")
|
|
216
|
+
|
|
217
|
+
except KeyboardInterrupt:
|
|
218
|
+
pass
|
|
219
|
+
|
|
220
|
+
# Final transcription of the full buffer
|
|
221
|
+
with buf_lock:
|
|
222
|
+
full_audio = audio_buf.copy()
|
|
223
|
+
|
|
224
|
+
if len(full_audio) >= SAMPLE_RATE:
|
|
225
|
+
try:
|
|
226
|
+
fp16 = (device == "cuda")
|
|
227
|
+
result = model.transcribe(
|
|
228
|
+
full_audio,
|
|
229
|
+
fp16=fp16,
|
|
230
|
+
language=args.language,
|
|
231
|
+
)
|
|
232
|
+
text = result.get("text", "").strip()
|
|
233
|
+
if text:
|
|
234
|
+
emit_transcript(text, is_final=True)
|
|
235
|
+
except Exception:
|
|
236
|
+
pass
|
|
237
|
+
|
|
238
|
+
running = False
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
if __name__ == "__main__":
|
|
242
|
+
main()
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Headless Moondream Station launcher for open-agents.
|
|
4
|
+
|
|
5
|
+
Starts the Moondream vision model REST API server on port 2020 without
|
|
6
|
+
the interactive REPL. Designed to be auto-launched by the VisionTool.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
python start-moondream.py [--port 2020] [--host 127.0.0.1]
|
|
10
|
+
|
|
11
|
+
Environment:
|
|
12
|
+
HF_TOKEN - HuggingFace token (optional, for gated models)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import sys
|
|
16
|
+
import signal
|
|
17
|
+
import time
|
|
18
|
+
import argparse
|
|
19
|
+
|
|
20
|
+
def main():
|
|
21
|
+
parser = argparse.ArgumentParser(description="Start Moondream Station REST server")
|
|
22
|
+
parser.add_argument("--port", type=int, default=2020, help="Server port (default: 2020)")
|
|
23
|
+
parser.add_argument("--host", default="127.0.0.1", help="Server host (default: 127.0.0.1)")
|
|
24
|
+
parser.add_argument("--model", default=None, help="Model to use (default: auto-detect, prefers non-gated moondream-2)")
|
|
25
|
+
args = parser.parse_args()
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
from moondream_station.core.config import ConfigManager
|
|
29
|
+
from moondream_station.core.manifest import ManifestManager
|
|
30
|
+
from moondream_station.core.models import ModelManager
|
|
31
|
+
from moondream_station.core.service import ServiceManager
|
|
32
|
+
from moondream_station.core.analytics import Analytics
|
|
33
|
+
from moondream_station.session import SessionState
|
|
34
|
+
from moondream_station.ui.display import Display
|
|
35
|
+
except ImportError:
|
|
36
|
+
print("ERROR: moondream-station not installed. Install with: pip install moondream-station", file=sys.stderr)
|
|
37
|
+
sys.exit(1)
|
|
38
|
+
|
|
39
|
+
print(f"[moondream] Initializing...", flush=True)
|
|
40
|
+
|
|
41
|
+
config = ConfigManager()
|
|
42
|
+
config.set("service_host", args.host)
|
|
43
|
+
config.set("service_port", args.port)
|
|
44
|
+
|
|
45
|
+
manifest_manager = ManifestManager(config)
|
|
46
|
+
analytics = Analytics(config, manifest_manager)
|
|
47
|
+
display = Display()
|
|
48
|
+
models = ModelManager(config, manifest_manager)
|
|
49
|
+
session_state = SessionState()
|
|
50
|
+
|
|
51
|
+
# Load manifest
|
|
52
|
+
manifest_url = "https://m87-md-prod-assets.s3.us-west-2.amazonaws.com/station/mds2/production_manifest.json"
|
|
53
|
+
print(f"[moondream] Loading manifest...", flush=True)
|
|
54
|
+
try:
|
|
55
|
+
manifest_manager.load_manifest(manifest_url, analytics, display)
|
|
56
|
+
except Exception as e:
|
|
57
|
+
print(f"ERROR: Failed to load manifest: {e}", file=sys.stderr)
|
|
58
|
+
sys.exit(1)
|
|
59
|
+
|
|
60
|
+
# Select model — prefer moondream-2 (non-gated) unless overridden
|
|
61
|
+
import os
|
|
62
|
+
model_name = args.model
|
|
63
|
+
if not model_name:
|
|
64
|
+
# Prefer moondream-2 (no HF token required) unless user has HF_TOKEN
|
|
65
|
+
has_hf_token = bool(os.environ.get("HF_TOKEN") or config.get("hf_token"))
|
|
66
|
+
if has_hf_token:
|
|
67
|
+
model_name = manifest_manager.get_available_default_model()
|
|
68
|
+
else:
|
|
69
|
+
model_name = "moondream-2"
|
|
70
|
+
if not model_name:
|
|
71
|
+
model_name = manifest_manager.get_available_default_model()
|
|
72
|
+
if not model_name:
|
|
73
|
+
print("ERROR: No model available", file=sys.stderr)
|
|
74
|
+
sys.exit(1)
|
|
75
|
+
|
|
76
|
+
print(f"[moondream] Switching to model: {model_name}", flush=True)
|
|
77
|
+
if not models.switch_model(model_name, display):
|
|
78
|
+
print(f"ERROR: Failed to switch to model {model_name}", file=sys.stderr)
|
|
79
|
+
sys.exit(1)
|
|
80
|
+
|
|
81
|
+
# Start REST server
|
|
82
|
+
service = ServiceManager(config, manifest_manager, session_state, analytics)
|
|
83
|
+
print(f"[moondream] Starting REST server on {args.host}:{args.port}...", flush=True)
|
|
84
|
+
|
|
85
|
+
if not service.start(model_name, args.port):
|
|
86
|
+
print("ERROR: Failed to start REST server", file=sys.stderr)
|
|
87
|
+
sys.exit(1)
|
|
88
|
+
|
|
89
|
+
print(f"[moondream] Server running at http://{args.host}:{args.port}/v1", flush=True)
|
|
90
|
+
print(f"[moondream] Endpoints: /v1/caption, /v1/query, /v1/detect, /v1/point", flush=True)
|
|
91
|
+
print(f"READY", flush=True)
|
|
92
|
+
|
|
93
|
+
# Handle shutdown
|
|
94
|
+
def shutdown(signum, frame):
|
|
95
|
+
print(f"\n[moondream] Shutting down...", flush=True)
|
|
96
|
+
service.stop()
|
|
97
|
+
sys.exit(0)
|
|
98
|
+
|
|
99
|
+
signal.signal(signal.SIGTERM, shutdown)
|
|
100
|
+
signal.signal(signal.SIGINT, shutdown)
|
|
101
|
+
|
|
102
|
+
# Keep alive
|
|
103
|
+
try:
|
|
104
|
+
while service.is_running():
|
|
105
|
+
time.sleep(1)
|
|
106
|
+
except KeyboardInterrupt:
|
|
107
|
+
shutdown(None, None)
|
|
108
|
+
|
|
109
|
+
print("[moondream] Server stopped", flush=True)
|
|
110
|
+
|
|
111
|
+
if __name__ == "__main__":
|
|
112
|
+
main()
|
package/package.json
CHANGED