open-agents-ai 0.103.80 → 0.103.81

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.
Files changed (2) hide show
  1. package/dist/index.js +187 -75
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -38957,6 +38957,19 @@ var init_voice = __esm({
38957
38957
  this.config = null;
38958
38958
  this.mlxActive = false;
38959
38959
  this.luxttsActive = false;
38960
+ if (this._luxttsDaemon && !this._luxttsDaemon.killed) {
38961
+ try {
38962
+ this._luxttsDaemon.stdin.write('{"action":"quit"}\n');
38963
+ } catch {
38964
+ }
38965
+ setTimeout(() => {
38966
+ try {
38967
+ this._luxttsDaemon?.kill();
38968
+ } catch {
38969
+ }
38970
+ }, 2e3);
38971
+ }
38972
+ this._luxttsDaemon = null;
38960
38973
  }
38961
38974
  // -------------------------------------------------------------------------
38962
38975
  // Text chunking for long TTS input
@@ -39610,70 +39623,95 @@ Error: ${err2 instanceof Error ? err2.message : String(err2)}`);
39610
39623
  }
39611
39624
  }
39612
39625
  }
39613
- /** Write the LuxTTS inference helper Python script */
39626
+ /** Persistent LuxTTS daemon process keeps model warm in GPU memory */
39627
+ _luxttsDaemon = null;
39628
+ _luxttsPending = /* @__PURE__ */ new Map();
39629
+ _luxttsBuffer = "";
39630
+ /** Write the LuxTTS persistent daemon Python script */
39614
39631
  writeLuxttsInferScript() {
39615
39632
  const script = `#!/usr/bin/env python3
39616
- """LuxTTS inference helper for open-agents voice engine.
39617
- Called by voice.ts via execSync with JSON args.
39633
+ """LuxTTS persistent daemon for open-agents voice engine.
39634
+ Keeps model warm in GPU memory. Reads JSON requests from stdin, writes JSON responses to stdout.
39618
39635
  """
39619
- import sys, json, os, wave, struct
39636
+ import sys, json, os, wave, signal
39620
39637
  import numpy as np
39621
39638
 
39622
39639
  def main():
39623
- args = json.loads(sys.argv[1])
39624
- action = args.get('action', 'synthesize')
39625
-
39626
- repo_path = args.get('repo_path', '')
39640
+ repo_path = os.environ.get('LUXTTS_REPO_PATH', '')
39627
39641
  if repo_path:
39628
39642
  sys.path.insert(0, repo_path)
39629
39643
 
39630
- if action == 'check':
39644
+ import torch
39645
+ from zipvoice.luxvoice import LuxTTS
39646
+
39647
+ # Auto-detect best device
39648
+ if torch.cuda.is_available():
39649
+ device = 'cuda'
39650
+ elif hasattr(torch.backends, 'mps') and torch.backends.mps.is_available():
39651
+ device = 'mps'
39652
+ else:
39653
+ device = 'cpu'
39654
+
39655
+ # Load model ONCE \u2014 stays warm in GPU/CPU memory
39656
+ tts = LuxTTS(model_path='YatharthS/LuxTTS', device=device, threads=4)
39657
+ sys.stdout.write(json.dumps({'type': 'ready', 'device': device}) + '\\n')
39658
+ sys.stdout.flush()
39659
+
39660
+ # Cache encoded prompts by clone_ref path (avoid re-encoding same voice)
39661
+ prompt_cache = {}
39662
+
39663
+ for line in sys.stdin:
39664
+ line = line.strip()
39665
+ if not line:
39666
+ continue
39631
39667
  try:
39632
- from zipvoice.luxvoice import LuxTTS
39633
- print(json.dumps({'ok': True}))
39668
+ req = json.loads(line)
39634
39669
  except Exception as e:
39635
- print(json.dumps({'ok': False, 'error': str(e)}))
39636
- return
39637
-
39638
- if action == 'synthesize':
39639
- import torch
39640
- from zipvoice.luxvoice import LuxTTS
39641
-
39642
- text = args['text']
39643
- clone_ref = args['clone_ref']
39644
- output_path = args['output_path']
39645
- speed = args.get('speed', 1.0)
39646
-
39647
- # Auto-detect best device
39648
- if torch.cuda.is_available():
39649
- device = 'cuda'
39650
- elif hasattr(torch.backends, 'mps') and torch.backends.mps.is_available():
39651
- device = 'mps'
39652
- else:
39653
- device = 'cpu'
39654
-
39655
- threads = 4 if device == 'cpu' else 4
39656
- tts = LuxTTS(model_path='YatharthS/LuxTTS', device=device, threads=threads)
39657
- encoded = tts.encode_prompt(clone_ref, duration=5, rms=0.001)
39658
- wav_tensor = tts.generate_speech(text, encoded, num_steps=4, guidance_scale=3.0, t_shift=0.5, speed=speed)
39659
- wav_np = wav_tensor.cpu().numpy().squeeze()
39660
-
39661
- # Write as 48kHz 16-bit mono WAV using stdlib
39662
- int16_data = (np.clip(wav_np, -1.0, 1.0) * 32767).astype(np.int16)
39663
- with wave.open(output_path, 'wb') as wf:
39664
- wf.setnchannels(1)
39665
- wf.setsampwidth(2)
39666
- wf.setframerate(48000)
39667
- wf.writeframes(int16_data.tobytes())
39668
-
39669
- print(json.dumps({
39670
- 'ok': True,
39671
- 'path': output_path,
39672
- 'sample_rate': 48000,
39673
- 'device': device,
39674
- 'samples': len(int16_data),
39675
- }))
39676
- return
39670
+ sys.stdout.write(json.dumps({'type': 'error', 'id': '', 'error': str(e)}) + '\\n')
39671
+ sys.stdout.flush()
39672
+ continue
39673
+
39674
+ req_id = req.get('id', '')
39675
+
39676
+ if req.get('action') == 'check':
39677
+ sys.stdout.write(json.dumps({'type': 'result', 'id': req_id, 'ok': True}) + '\\n')
39678
+ sys.stdout.flush()
39679
+ continue
39680
+
39681
+ if req.get('action') == 'quit':
39682
+ break
39683
+
39684
+ if req.get('action') == 'synthesize':
39685
+ try:
39686
+ text = req['text']
39687
+ clone_ref = req['clone_ref']
39688
+ output_path = req['output_path']
39689
+ speed = req.get('speed', 1.0)
39690
+
39691
+ # Use cached prompt encoding if available
39692
+ if clone_ref not in prompt_cache:
39693
+ prompt_cache[clone_ref] = tts.encode_prompt(clone_ref, duration=5, rms=0.001)
39694
+ encoded = prompt_cache[clone_ref]
39695
+
39696
+ wav_tensor = tts.generate_speech(text, encoded, num_steps=4, guidance_scale=3.0, t_shift=0.5, speed=speed)
39697
+ wav_np = wav_tensor.cpu().numpy().squeeze()
39698
+
39699
+ int16_data = (np.clip(wav_np, -1.0, 1.0) * 32767).astype(np.int16)
39700
+ with wave.open(output_path, 'wb') as wf:
39701
+ wf.setnchannels(1)
39702
+ wf.setsampwidth(2)
39703
+ wf.setframerate(48000)
39704
+ wf.writeframes(int16_data.tobytes())
39705
+
39706
+ sys.stdout.write(json.dumps({
39707
+ 'type': 'result', 'id': req_id, 'ok': True,
39708
+ 'path': output_path, 'sample_rate': 48000,
39709
+ 'device': device, 'samples': len(int16_data),
39710
+ }) + '\\n')
39711
+ sys.stdout.flush()
39712
+ except Exception as e:
39713
+ sys.stdout.write(json.dumps({'type': 'error', 'id': req_id, 'error': str(e)}) + '\\n')
39714
+ sys.stdout.flush()
39677
39715
 
39678
39716
  if __name__ == '__main__':
39679
39717
  main()
@@ -39682,6 +39720,84 @@ if __name__ == '__main__':
39682
39720
  mkdirSync14(voiceDir(), { recursive: true });
39683
39721
  writeFileSync14(scriptPath2, script);
39684
39722
  }
39723
+ /** Ensure the LuxTTS daemon is running, spawn if needed */
39724
+ async ensureLuxttsDaemon() {
39725
+ if (this._luxttsDaemon && !this._luxttsDaemon.killed)
39726
+ return true;
39727
+ const venvPy = luxttsVenvPy();
39728
+ if (!existsSync34(venvPy))
39729
+ return false;
39730
+ return new Promise((resolve31) => {
39731
+ const env = { ...process.env, LUXTTS_REPO_PATH: luxttsRepoDir() };
39732
+ const daemon = nodeSpawn(venvPy, [luxttsInferScript()], {
39733
+ stdio: ["pipe", "pipe", "pipe"],
39734
+ cwd: tmpdir6(),
39735
+ env
39736
+ });
39737
+ this._luxttsDaemon = daemon;
39738
+ this._luxttsBuffer = "";
39739
+ daemon.stdout.on("data", (chunk) => {
39740
+ this._luxttsBuffer += chunk.toString();
39741
+ const lines = this._luxttsBuffer.split("\n");
39742
+ this._luxttsBuffer = lines.pop();
39743
+ for (const line of lines) {
39744
+ if (!line.trim())
39745
+ continue;
39746
+ try {
39747
+ const msg = JSON.parse(line);
39748
+ if (msg.type === "ready") {
39749
+ resolve31(true);
39750
+ } else if (msg.type === "result" || msg.type === "error") {
39751
+ const pending = this._luxttsPending.get(msg.id);
39752
+ if (pending) {
39753
+ this._luxttsPending.delete(msg.id);
39754
+ if (msg.type === "error")
39755
+ pending.reject(new Error(msg.error));
39756
+ else
39757
+ pending.resolve(line);
39758
+ }
39759
+ }
39760
+ } catch {
39761
+ }
39762
+ }
39763
+ });
39764
+ daemon.on("exit", () => {
39765
+ this._luxttsDaemon = null;
39766
+ for (const [, p] of this._luxttsPending) {
39767
+ p.reject(new Error("LuxTTS daemon exited"));
39768
+ }
39769
+ this._luxttsPending.clear();
39770
+ });
39771
+ daemon.on("error", () => {
39772
+ this._luxttsDaemon = null;
39773
+ resolve31(false);
39774
+ });
39775
+ setTimeout(() => {
39776
+ if (this._luxttsDaemon === daemon && !this._luxttsPending.has("__ready__")) {
39777
+ resolve31(false);
39778
+ }
39779
+ }, 6e4);
39780
+ });
39781
+ }
39782
+ /** Send a request to the LuxTTS daemon and await the response */
39783
+ luxttsRequest(req) {
39784
+ return new Promise((resolve31, reject) => {
39785
+ if (!this._luxttsDaemon || this._luxttsDaemon.killed) {
39786
+ reject(new Error("LuxTTS daemon not running"));
39787
+ return;
39788
+ }
39789
+ const id = `req_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
39790
+ req.id = id;
39791
+ this._luxttsPending.set(id, { resolve: resolve31, reject });
39792
+ this._luxttsDaemon.stdin.write(JSON.stringify(req) + "\n");
39793
+ setTimeout(() => {
39794
+ if (this._luxttsPending.has(id)) {
39795
+ this._luxttsPending.delete(id);
39796
+ reject(new Error("LuxTTS synthesis timeout"));
39797
+ }
39798
+ }, 12e4);
39799
+ });
39800
+ }
39685
39801
  /**
39686
39802
  * Synthesize and play audio using LuxTTS voice cloning.
39687
39803
  * Speed is passed natively to LuxTTS. Pitch is post-processed via resampling.
@@ -39694,20 +39810,18 @@ if __name__ == '__main__':
39694
39810
  const cleaned = text.replace(/\*/g, "").trim();
39695
39811
  if (!cleaned)
39696
39812
  return;
39697
- const venvPy = luxttsVenvPy();
39698
- if (!existsSync34(venvPy))
39813
+ const ready = await this.ensureLuxttsDaemon();
39814
+ if (!ready)
39699
39815
  return;
39700
39816
  const wavPath = join43(tmpdir6(), `oa-luxtts-${Date.now()}.wav`);
39701
- const inferArgs = JSON.stringify({
39702
- action: "synthesize",
39703
- repo_path: luxttsRepoDir(),
39704
- text: cleaned,
39705
- clone_ref: this.luxttsCloneRef,
39706
- output_path: wavPath,
39707
- speed: speedFactor
39708
- });
39709
39817
  try {
39710
- execSync27(`${JSON.stringify(venvPy)} ${JSON.stringify(luxttsInferScript())} ${JSON.stringify(inferArgs)}`, { stdio: "pipe", timeout: 12e4, cwd: tmpdir6() });
39818
+ await this.luxttsRequest({
39819
+ action: "synthesize",
39820
+ text: cleaned,
39821
+ clone_ref: this.luxttsCloneRef,
39822
+ output_path: wavPath,
39823
+ speed: speedFactor
39824
+ });
39711
39825
  } catch {
39712
39826
  return;
39713
39827
  }
@@ -39770,20 +39884,18 @@ if __name__ == '__main__':
39770
39884
  const cleaned = text.replace(/\*/g, "").trim();
39771
39885
  if (!cleaned)
39772
39886
  return null;
39773
- const venvPy = luxttsVenvPy();
39774
- if (!existsSync34(venvPy))
39887
+ const ready = await this.ensureLuxttsDaemon();
39888
+ if (!ready)
39775
39889
  return null;
39776
39890
  const wavPath = join43(tmpdir6(), `oa-luxtts-buf-${Date.now()}.wav`);
39777
- const inferArgs = JSON.stringify({
39778
- action: "synthesize",
39779
- repo_path: luxttsRepoDir(),
39780
- text: cleaned,
39781
- clone_ref: this.luxttsCloneRef,
39782
- output_path: wavPath,
39783
- speed: 1
39784
- });
39785
39891
  try {
39786
- execSync27(`${JSON.stringify(venvPy)} ${JSON.stringify(luxttsInferScript())} ${JSON.stringify(inferArgs)}`, { stdio: "pipe", timeout: 12e4, cwd: tmpdir6() });
39892
+ await this.luxttsRequest({
39893
+ action: "synthesize",
39894
+ text: cleaned,
39895
+ clone_ref: this.luxttsCloneRef,
39896
+ output_path: wavPath,
39897
+ speed: 1
39898
+ });
39787
39899
  } catch {
39788
39900
  return null;
39789
39901
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-agents-ai",
3
- "version": "0.103.80",
3
+ "version": "0.103.81",
4
4
  "description": "AI coding agent powered by open-source models (Ollama/vLLM) — interactive TUI with agentic tool-calling loop",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",