chenbhao commited on
Commit
6db367d
·
1 Parent(s): 0db027e

feat: voice stt+tts just first have voice

Browse files
scripts/transcribe.py CHANGED
@@ -11,36 +11,38 @@ def write_wav(path: str, raw_pcm: bytes, sample_rate: int = 16000):
11
  w.setframerate(sample_rate)
12
  w.writeframes(raw_pcm)
13
 
14
- def transcribe(wav_path: str, model_size: str = "large-v3-turbo", language: str | None = None) -> dict:
 
 
 
 
 
 
15
  try:
16
- import whisper
17
- model = whisper.load_model(model_size, download_root=WHISPER_CACHE)
18
- opts = {}
 
19
  if language:
20
  opts["language"] = language
21
- result = model.transcribe(wav_path, **opts)
22
- return {"success": True, "text": result["text"].strip(), "language": result.get("language", "")}
 
23
  except ImportError:
24
  pass
25
 
26
  try:
27
- from faster_whisper import WhisperModel
28
- model_path = model_size
29
- if not os.path.isdir(model_path):
30
- local_path = os.path.expanduser(f"~/.cache/huggingface/hub/models--Systran--faster-whisper-{model_size}")
31
- if os.path.isdir(local_path):
32
- model_path = local_path
33
- model = WhisperModel(model_path, device="cpu", compute_type="int8")
34
- opts = {"beam_size": 1}
35
  if language:
36
  opts["language"] = language
37
- segments, info = model.transcribe(wav_path, **opts)
38
- text = " ".join(seg.text for seg in segments)
39
- return {"success": True, "text": text.strip(), "language": info.language}
40
  except ImportError:
41
  pass
42
 
43
- return {"success": False, "error": "Neither openai-whisper nor faster-whisper is installed. Run: pip install openai-whisper"}
44
 
45
  def main():
46
  if len(sys.argv) < 2:
@@ -48,7 +50,7 @@ def main():
48
  sys.exit(1)
49
 
50
  wav_path = sys.argv[1]
51
- model_size = "large-v3-turbo"
52
  language = None
53
 
54
  i = 2
 
11
  w.setframerate(sample_rate)
12
  w.writeframes(raw_pcm)
13
 
14
+ def get_model_path(name):
15
+ lp = os.path.expanduser(f"~/.cache/huggingface/hub/models--Systran--faster-whisper-{name}")
16
+ if os.path.isdir(lp):
17
+ return lp
18
+ return name
19
+
20
+ def transcribe(wav_path: str, model_size: str = "small", language: str | None = None) -> dict:
21
  try:
22
+ from faster_whisper import WhisperModel
23
+ model_path = get_model_path(model_size)
24
+ model = WhisperModel(model_path, device="cuda", compute_type="int8_float16")
25
+ opts = {"beam_size": 5}
26
  if language:
27
  opts["language"] = language
28
+ segments, info = model.transcribe(wav_path, **opts)
29
+ text = " ".join(seg.text for seg in segments)
30
+ return {"success": True, "text": text.strip(), "language": info.language}
31
  except ImportError:
32
  pass
33
 
34
  try:
35
+ import whisper
36
+ model = whisper.load_model(model_size, device="cuda", download_root=WHISPER_CACHE)
37
+ opts = {}
 
 
 
 
 
38
  if language:
39
  opts["language"] = language
40
+ result = model.transcribe(wav_path, **opts)
41
+ return {"success": True, "text": result["text"].strip(), "language": result.get("language", "")}
 
42
  except ImportError:
43
  pass
44
 
45
+ return {"success": False, "error": "Neither faster-whisper nor openai-whisper is installed. Run: pip install faster-whisper"}
46
 
47
  def main():
48
  if len(sys.argv) < 2:
 
50
  sys.exit(1)
51
 
52
  wav_path = sys.argv[1]
53
+ model_size = "small"
54
  language = None
55
 
56
  i = 2
scripts/whisper_server.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json, os, sys, wave
2
+
3
+ WHISPER_CACHE = os.path.expanduser("~/.cache/whisper")
4
+
5
+ def write_wav(path, raw_pcm, sample_rate=16000):
6
+ with wave.open(path, 'wb') as w:
7
+ w.setnchannels(1)
8
+ w.setsampwidth(2)
9
+ w.setframerate(sample_rate)
10
+ w.writeframes(raw_pcm)
11
+
12
+ def get_model_path(name):
13
+ lp = os.path.expanduser(f'~/.cache/huggingface/hub/models--Systran--faster-whisper-{name}')
14
+ if os.path.isdir(lp):
15
+ return lp
16
+ return name
17
+
18
+ def main():
19
+ model = None
20
+ model_name = None
21
+
22
+ for line in sys.stdin:
23
+ line = line.strip()
24
+ if not line:
25
+ continue
26
+ try:
27
+ msg = json.loads(line)
28
+ except:
29
+ continue
30
+
31
+ msg_type = msg.get('type')
32
+
33
+ if msg_type == 'load':
34
+ name = msg.get('model', 'small')
35
+ print(json.dumps({'type': 'ready', 'model': name}), flush=True)
36
+ sys.stdout.flush()
37
+
38
+ elif msg_type == 'transcribe':
39
+ wav_path = msg.get('wav')
40
+ language = msg.get('language')
41
+
42
+ if not model or model_name != msg.get('model'):
43
+ model_name = msg.get('model', 'base')
44
+ print(json.dumps({'type': 'status', 'message': f'Loading model {model_name}...'}), flush=True)
45
+ try:
46
+ import whisper
47
+ model = whisper.load_model(model_name, device='cuda', download_root=WHISPER_CACHE)
48
+ print(json.dumps({'type': 'status', 'message': 'Model loaded'}), flush=True)
49
+ except ImportError:
50
+ try:
51
+ from faster_whisper import WhisperModel
52
+ mp = get_model_path(model_name)
53
+ model = WhisperModel(mp, device='cuda', compute_type='int8_float16')
54
+ print(json.dumps({'type': 'status', 'message': 'Model loaded'}), flush=True)
55
+ except (ImportError, RuntimeError) as e:
56
+ print(json.dumps({'type': 'error', 'message': f'No whisper library available: {e}'}), flush=True)
57
+ sys.stdout.flush()
58
+ continue
59
+
60
+ if not wav_path or not os.path.exists(wav_path):
61
+ print(json.dumps({'type': 'error', 'message': 'WAV file not found'}), flush=True)
62
+ sys.stdout.flush()
63
+ continue
64
+
65
+ try:
66
+ opts = {}
67
+ if language:
68
+ opts['language'] = language
69
+
70
+ result = model.transcribe(wav_path, **opts)
71
+ text = result['text'].strip() if isinstance(result, dict) else ''
72
+ lang = result.get('language', language or 'en') if isinstance(result, dict) else (language or 'en')
73
+
74
+ print(json.dumps({'type': 'result', 'text': text, 'language': lang}), flush=True)
75
+ except Exception as e:
76
+ print(json.dumps({'type': 'error', 'message': str(e)}), flush=True)
77
+ sys.stdout.flush()
78
+
79
+ if __name__ == '__main__':
80
+ main()
src/commands/config/config.tsx CHANGED
@@ -1,7 +1,7 @@
1
- import * as React from 'react';
2
- import { Settings } from '../../components/Settings/Settings.js';
3
- import type { LocalJSXCommandCall } from '../../types/command.js';
 
4
  export const call: LocalJSXCommandCall = async (onDone, context) => {
5
- return <Settings onClose={onDone} context={context} defaultTab="Config" />;
6
- };
7
- //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsIlNldHRpbmdzIiwiTG9jYWxKU1hDb21tYW5kQ2FsbCIsImNhbGwiLCJvbkRvbmUiLCJjb250ZXh0Il0sInNvdXJjZXMiOlsiY29uZmlnLnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgKiBhcyBSZWFjdCBmcm9tICdyZWFjdCdcbmltcG9ydCB7IFNldHRpbmdzIH0gZnJvbSAnLi4vLi4vY29tcG9uZW50cy9TZXR0aW5ncy9TZXR0aW5ncy5qcydcbmltcG9ydCB0eXBlIHsgTG9jYWxKU1hDb21tYW5kQ2FsbCB9IGZyb20gJy4uLy4uL3R5cGVzL2NvbW1hbmQuanMnXG5cbmV4cG9ydCBjb25zdCBjYWxsOiBMb2NhbEpTWENvbW1hbmRDYWxsID0gYXN5bmMgKG9uRG9uZSwgY29udGV4dCkgPT4ge1xuICByZXR1cm4gPFNldHRpbmdzIG9uQ2xvc2U9e29uRG9uZX0gY29udGV4dD17Y29udGV4dH0gZGVmYXVsdFRhYj1cIkNvbmZpZ1wiIC8+XG59XG4iXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBS0EsS0FBSyxNQUFNLE9BQU87QUFDOUIsU0FBU0MsUUFBUSxRQUFRLHVDQUF1QztBQUNoRSxjQUFjQyxtQkFBbUIsUUFBUSx3QkFBd0I7QUFFakUsT0FBTyxNQUFNQyxJQUFJLEVBQUVELG1CQUFtQixHQUFHLE1BQUFDLENBQU9DLE1BQU0sRUFBRUMsT0FBTyxLQUFLO0VBQ2xFLE9BQU8sQ0FBQyxRQUFRLENBQUMsT0FBTyxDQUFDLENBQUNELE1BQU0sQ0FBQyxDQUFDLE9BQU8sQ0FBQyxDQUFDQyxPQUFPLENBQUMsQ0FBQyxVQUFVLENBQUMsUUFBUSxHQUFHO0FBQzVFLENBQUMiLCJpZ25vcmVMaXN0IjpbXX0=
 
1
+ import * as React from 'react'
2
+ import { Settings } from '../../components/Settings/Settings.js'
3
+ import type { LocalJSXCommandCall } from '../../types/command.js'
4
+
5
  export const call: LocalJSXCommandCall = async (onDone, context) => {
6
+ return <Settings onClose={onDone} context={context} defaultTab="Config" />
7
+ }
 
src/commands/config/config.tsx.bak ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import * as React from 'react';
2
+ import { Settings } from '../../components/Settings/Settings.js';
3
+ import type { LocalJSXCommandCall } from '../../types/command.js';
4
+ export const call: LocalJSXCommandCall = async (onDone, context) => {
5
+ return <Settings onClose={onDone} context={context} defaultTab="Config" />;
6
+ };
7
+ //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsIlNldHRpbmdzIiwiTG9jYWxKU1hDb21tYW5kQ2FsbCIsImNhbGwiLCJvbkRvbmUiLCJjb250ZXh0Il0sInNvdXJjZXMiOlsiY29uZmlnLnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgKiBhcyBSZWFjdCBmcm9tICdyZWFjdCdcbmltcG9ydCB7IFNldHRpbmdzIH0gZnJvbSAnLi4vLi4vY29tcG9uZW50cy9TZXR0aW5ncy9TZXR0aW5ncy5qcydcbmltcG9ydCB0eXBlIHsgTG9jYWxKU1hDb21tYW5kQ2FsbCB9IGZyb20gJy4uLy4uL3R5cGVzL2NvbW1hbmQuanMnXG5cbmV4cG9ydCBjb25zdCBjYWxsOiBMb2NhbEpTWENvbW1hbmRDYWxsID0gYXN5bmMgKG9uRG9uZSwgY29udGV4dCkgPT4ge1xuICByZXR1cm4gPFNldHRpbmdzIG9uQ2xvc2U9e29uRG9uZX0gY29udGV4dD17Y29udGV4dH0gZGVmYXVsdFRhYj1cIkNvbmZpZ1wiIC8+XG59XG4iXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBS0EsS0FBSyxNQUFNLE9BQU87QUFDOUIsU0FBU0MsUUFBUSxRQUFRLHVDQUF1QztBQUNoRSxjQUFjQyxtQkFBbUIsUUFBUSx3QkFBd0I7QUFFakUsT0FBTyxNQUFNQyxJQUFJLEVBQUVELG1CQUFtQixHQUFHLE1BQUFDLENBQU9DLE1BQU0sRUFBRUMsT0FBTyxLQUFLO0VBQ2xFLE9BQU8sQ0FBQyxRQUFRLENBQUMsT0FBTyxDQUFDLENBQUNELE1BQU0sQ0FBQyxDQUFDLE9BQU8sQ0FBQyxDQUFDQyxPQUFPLENBQUMsQ0FBQyxVQUFVLENBQUMsUUFBUSxHQUFHO0FBQzVFLENBQUMiLCJpZ25vcmVMaXN0IjpbXX0=
src/commands/voice/voice.ts CHANGED
@@ -92,6 +92,7 @@ export const call: LocalCommandCall = async () => {
92
  const result = updateSettingsForSource('userSettings', {
93
  voiceEnabled: true,
94
  voiceProvider: 'local' as const,
 
95
  })
96
  if (result.error) {
97
  return {
 
92
  const result = updateSettingsForSource('userSettings', {
93
  voiceEnabled: true,
94
  voiceProvider: 'local' as const,
95
+ voiceAutoTTS: true,
96
  })
97
  if (result.error) {
98
  return {
src/components/Settings/Config.tsx CHANGED
The diff for this file is too large to render. See raw diff
 
src/components/Settings/Config.tsx.bak ADDED
The diff for this file is too large to render. See raw diff
 
src/components/Settings/Settings.tsx CHANGED
@@ -1,137 +1,136 @@
1
- import { c as _c } from "react/compiler-runtime";
2
  // biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered
3
- import * as React from 'react';
4
- import { Suspense, useState } from 'react';
5
- import { useKeybinding } from '../../keybindings/useKeybinding.js';
6
- import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js';
7
- import { useTerminalSize } from '../../hooks/useTerminalSize.js';
8
- import { useIsInsideModal, useModalOrTerminalSize } from '../../context/modalContext.js';
9
- import { Pane } from '../design-system/Pane.js';
10
- import { Tabs, Tab } from '../design-system/Tabs.js';
11
- import { Status, buildDiagnostics } from './Status.js';
12
- import { Config } from './Config.js';
13
- import { Usage } from './Usage.js';
14
- import type { LocalJSXCommandContext, CommandResultDisplay } from '../../commands.js';
 
 
 
 
 
 
 
15
  type Props = {
16
- onClose: (result?: string, options?: {
17
- display?: CommandResultDisplay;
18
- }) => void;
19
- context: LocalJSXCommandContext;
20
- defaultTab: 'Status' | 'Config' | 'Usage' | 'Gates';
21
- };
22
- export function Settings(t0) {
23
- const $ = _c(25);
24
- const {
25
- onClose,
26
- context,
27
- defaultTab
28
- } = t0;
29
- const [selectedTab, setSelectedTab] = useState(defaultTab);
30
- const [tabsHidden, setTabsHidden] = useState(false);
31
- const [configOwnsEsc, setConfigOwnsEsc] = useState(false);
32
- const [gatesOwnsEsc, setGatesOwnsEsc] = useState(false);
33
- const insideModal = useIsInsideModal();
34
- const {
35
- rows
36
- } = useModalOrTerminalSize(useTerminalSize());
37
- const contentHeight = insideModal ? rows + 1 : Math.max(15, Math.min(Math.floor(rows * 0.8), 30));
38
- const [diagnosticsPromise] = useState(_temp2);
39
- useExitOnCtrlCDWithKeybindings();
40
- let t1;
41
- if ($[0] !== onClose || $[1] !== tabsHidden) {
42
- t1 = () => {
43
- if (tabsHidden) {
44
- return;
45
- }
46
- onClose("Status dialog dismissed", {
47
- display: "system"
48
- });
49
- };
50
- $[0] = onClose;
51
- $[1] = tabsHidden;
52
- $[2] = t1;
53
- } else {
54
- t1 = $[2];
55
- }
56
- const handleEscape = t1;
57
- const t2 = !tabsHidden && !(selectedTab === "Config" && configOwnsEsc) && !(selectedTab === "Gates" && gatesOwnsEsc);
58
- let t3;
59
- if ($[3] !== t2) {
60
- t3 = {
61
- context: "Settings",
62
- isActive: t2
63
- };
64
- $[3] = t2;
65
- $[4] = t3;
66
- } else {
67
- t3 = $[4];
68
- }
69
- useKeybinding("confirm:no", handleEscape, t3);
70
- let t4;
71
- if ($[5] !== context || $[6] !== diagnosticsPromise) {
72
- t4 = <Tab key="status" title="Status"><Status context={context} diagnosticsPromise={diagnosticsPromise} /></Tab>;
73
- $[5] = context;
74
- $[6] = diagnosticsPromise;
75
- $[7] = t4;
76
- } else {
77
- t4 = $[7];
78
- }
79
- let t5;
80
- if ($[8] !== contentHeight || $[9] !== context || $[10] !== onClose) {
81
- t5 = <Tab key="config" title="Config"><Suspense fallback={null}><Config context={context} onClose={onClose} setTabsHidden={setTabsHidden} onIsSearchModeChange={setConfigOwnsEsc} contentHeight={contentHeight} /></Suspense></Tab>;
82
- $[8] = contentHeight;
83
- $[9] = context;
84
- $[10] = onClose;
85
- $[11] = t5;
86
- } else {
87
- t5 = $[11];
88
- }
89
- let t6;
90
- if ($[12] === Symbol.for("react.memo_cache_sentinel")) {
91
- t6 = <Tab key="usage" title="Usage"><Usage /></Tab>;
92
- $[12] = t6;
93
- } else {
94
- t6 = $[12];
95
- }
96
- let t7;
97
- if ($[13] !== contentHeight) {
98
- t7 = false ? [<Tab key="gates" title="Gates"><Gates onOwnsEscChange={setGatesOwnsEsc} contentHeight={contentHeight} /></Tab>] : [];
99
- $[13] = contentHeight;
100
- $[14] = t7;
101
- } else {
102
- t7 = $[14];
103
- }
104
- let t8;
105
- if ($[15] !== t4 || $[16] !== t5 || $[17] !== t7) {
106
- t8 = [t4, t5, t6, ...t7];
107
- $[15] = t4;
108
- $[16] = t5;
109
- $[17] = t7;
110
- $[18] = t8;
111
- } else {
112
- t8 = $[18];
113
- }
114
- const tabs = t8;
115
- const t9 = defaultTab !== "Config" && defaultTab !== "Gates";
116
- const t10 = tabsHidden || insideModal ? undefined : contentHeight;
117
- let t11;
118
- if ($[19] !== selectedTab || $[20] !== t10 || $[21] !== t9 || $[22] !== tabs || $[23] !== tabsHidden) {
119
- t11 = <Pane color="permission"><Tabs color="permission" selectedTab={selectedTab} onTabChange={setSelectedTab} hidden={tabsHidden} initialHeaderFocused={t9} contentHeight={t10}>{tabs}</Tabs></Pane>;
120
- $[19] = selectedTab;
121
- $[20] = t10;
122
- $[21] = t9;
123
- $[22] = tabs;
124
- $[23] = tabsHidden;
125
- $[24] = t11;
126
- } else {
127
- t11 = $[24];
128
- }
129
- return t11;
130
  }
131
- function _temp2() {
132
- return buildDiagnostics().catch(_temp);
133
- }
134
- function _temp() {
135
- return [];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  }
137
- //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsIlN1c3BlbnNlIiwidXNlU3RhdGUiLCJ1c2VLZXliaW5kaW5nIiwidXNlRXhpdE9uQ3RybENEV2l0aEtleWJpbmRpbmdzIiwidXNlVGVybWluYWxTaXplIiwidXNlSXNJbnNpZGVNb2RhbCIsInVzZU1vZGFsT3JUZXJtaW5hbFNpemUiLCJQYW5lIiwiVGFicyIsIlRhYiIsIlN0YXR1cyIsImJ1aWxkRGlhZ25vc3RpY3MiLCJDb25maWciLCJVc2FnZSIsIkxvY2FsSlNYQ29tbWFuZENvbnRleHQiLCJDb21tYW5kUmVzdWx0RGlzcGxheSIsIlByb3BzIiwib25DbG9zZSIsInJlc3VsdCIsIm9wdGlvbnMiLCJkaXNwbGF5IiwiY29udGV4dCIsImRlZmF1bHRUYWIiLCJTZXR0aW5ncyIsInQwIiwiJCIsIl9jIiwic2VsZWN0ZWRUYWIiLCJzZXRTZWxlY3RlZFRhYiIsInRhYnNIaWRkZW4iLCJzZXRUYWJzSGlkZGVuIiwiY29uZmlnT3duc0VzYyIsInNldENvbmZpZ093bnNFc2MiLCJnYXRlc093bnNFc2MiLCJzZXRHYXRlc093bnNFc2MiLCJpbnNpZGVNb2RhbCIsInJvd3MiLCJjb250ZW50SGVpZ2h0IiwiTWF0aCIsIm1heCIsIm1pbiIsImZsb29yIiwiZGlhZ25vc3RpY3NQcm9taXNlIiwiX3RlbXAyIiwidDEiLCJoYW5kbGVFc2NhcGUiLCJ0MiIsInQzIiwiaXNBY3RpdmUiLCJ0NCIsInQ1IiwidDYiLCJTeW1ib2wiLCJmb3IiLCJ0NyIsInQ4IiwidGFicyIsInQ5IiwidDEwIiwidW5kZWZpbmVkIiwidDExIiwiY2F0Y2giLCJfdGVtcCJdLCJzb3VyY2VzIjpbIlNldHRpbmdzLnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyIvLyBiaW9tZS1pZ25vcmUtYWxsIGFzc2lzdC9zb3VyY2Uvb3JnYW5pemVJbXBvcnRzOiBBTlQtT05MWSBpbXBvcnQgbWFya2VycyBtdXN0IG5vdCBiZSByZW9yZGVyZWRcbmltcG9ydCAqIGFzIFJlYWN0IGZyb20gJ3JlYWN0J1xuaW1wb3J0IHsgU3VzcGVuc2UsIHVzZVN0YXRlIH0gZnJvbSAncmVhY3QnXG5pbXBvcnQgeyB1c2VLZXliaW5kaW5nIH0gZnJvbSAnLi4vLi4va2V5YmluZGluZ3MvdXNlS2V5YmluZGluZy5qcydcbmltcG9ydCB7IHVzZUV4aXRPbkN0cmxDRFdpdGhLZXliaW5kaW5ncyB9IGZyb20gJy4uLy4uL2hvb2tzL3VzZUV4aXRPbkN0cmxDRFdpdGhLZXliaW5kaW5ncy5qcydcbmltcG9ydCB7IHVzZVRlcm1pbmFsU2l6ZSB9IGZyb20gJy4uLy4uL2hvb2tzL3VzZVRlcm1pbmFsU2l6ZS5qcydcbmltcG9ydCB7XG4gIHVzZUlzSW5zaWRlTW9kYWwsXG4gIHVzZU1vZGFsT3JUZXJtaW5hbFNpemUsXG59IGZyb20gJy4uLy4uL2NvbnRleHQvbW9kYWxDb250ZXh0LmpzJ1xuaW1wb3J0IHsgUGFuZSB9IGZyb20gJy4uL2Rlc2lnbi1zeXN0ZW0vUGFuZS5qcydcbmltcG9ydCB7IFRhYnMsIFRhYiB9IGZyb20gJy4uL2Rlc2lnbi1zeXN0ZW0vVGFicy5qcydcbmltcG9ydCB7IFN0YXR1cywgYnVpbGREaWFnbm9zdGljcyB9IGZyb20gJy4vU3RhdHVzLmpzJ1xuaW1wb3J0IHsgQ29uZmlnIH0gZnJvbSAnLi9Db25maWcuanMnXG5pbXBvcnQgeyBVc2FnZSB9IGZyb20gJy4vVXNhZ2UuanMnXG5pbXBvcnQgdHlwZSB7XG4gIExvY2FsSlNYQ29tbWFuZENvbnRleHQsXG4gIENvbW1hbmRSZXN1bHREaXNwbGF5LFxufSBmcm9tICcuLi8uLi9jb21tYW5kcy5qcydcblxudHlwZSBQcm9wcyA9IHtcbiAgb25DbG9zZTogKFxuICAgIHJlc3VsdD86IHN0cmluZyxcbiAgICBvcHRpb25zPzogeyBkaXNwbGF5PzogQ29tbWFuZFJlc3VsdERpc3BsYXkgfSxcbiAgKSA9PiB2b2lkXG4gIGNvbnRleHQ6IExvY2FsSlNYQ29tbWFuZENvbnRleHRcbiAgZGVmYXVsdFRhYjogJ1N0YXR1cycgfCAnQ29uZmlnJyB8ICdVc2FnZScgfCAnR2F0ZXMnXG59XG5cbmV4cG9ydCBmdW5jdGlvbiBTZXR0aW5ncyh7XG4gIG9uQ2xvc2UsXG4gIGNvbnRleHQsXG4gIGRlZmF1bHRUYWIsXG59OiBQcm9wcyk6IFJlYWN0LlJlYWN0Tm9kZSB7XG4gIGNvbnN0IFtzZWxlY3RlZFRhYiwgc2V0U2VsZWN0ZWRUYWJdID0gdXNlU3RhdGU8c3RyaW5nPihkZWZhdWx0VGFiKVxuICBjb25zdCBbdGFic0hpZGRlbiwgc2V0VGFic0hpZGRlbl0gPSB1c2VTdGF0ZShmYWxzZSlcbiAgLy8gVHJ1ZSB3aGlsZSBDb25maWcncyBvd24gRXNjIGhhbmRsZXIgaXMgYWN0aXZlIChzZWFyY2ggbW9kZSB3aXRoIGNvbnRlbnRcbiAgLy8gZm9jdXNlZCkuIFNldHRpbmdzIG11c3QgY2VkZSBFc2Mgc28gc2VhcmNoIGNhbiBjbGVhci9leGl0IGZpcnN0LlxuICBjb25zdCBbY29uZmlnT3duc0VzYywgc2V0Q29uZmlnT3duc0VzY10gPSB1c2VTdGF0ZShmYWxzZSlcbiAgY29uc3QgW2dhdGVzT3duc0VzYywgc2V0R2F0ZXNPd25zRXNjXSA9IHVzZVN0YXRlKGZhbHNlKVxuICAvLyBGaXhlZCBjb250ZW50IGhlaWdodCBzbyBzd2l0Y2hpbmcgdGFicyBkb2Vzbid0IHNoaWZ0IHRoZSBwYW5lIGhlaWdodC5cbiAgLy8gT3V0c2lkZSBtb2RhbHMgY2FwIGF0IG1pbig4MCUgdmlld3BvcnQsIDMwKS4gSW5zaWRlIGEgTW9kYWwgdGhlIG1vZGFsJ3NcbiAgLy8gaW5uZXJTaXplLnJvd3MgSVMgdGhlIFNjcm9sbEJveCB2aWV3cG9ydCDigJQgdGhlIDAuOCBtdWx0aXBsaWVyIG92ZXItXG4gIC8vIHNocmlua3MsIGxlYXZpbmcgZW1wdHkgcm93cyB3aGlsZSBDb25maWcgc2hvd3MgXCLihpMgTiBtb3JlIGJlbG93XCIuXG4gIC8vXG4gIC8vIEluc2lkZS1tb2RhbCBtYXRoOiBDb25maWcncyBwYW5lQ2FwLTEwIGNocm9tZSBlc3RpbWF0ZSB3YXMgdHVuZWQgZm9yXG4gIC8vIG1hcmdpblk9ezF9ICgyIHJvd3MpIHdoaWNoIGlzIHN0cmlwcGVkIGluc2lkZSBtb2RhbHMg4oaSICsyIHRvIHJlY292ZXIuXG4gIC8vIFRoZW4gLTIgZm9yIFRhYnMnIGhlYWRlciByb3cgKyBpdHMgbWFyZ2luVG9wPTEuIFBsdXMgKzEgb2JzZXJ2ZWQgZ2FwXG4gIC8vIGZyb20gdGhlIHBhbmVDYXAtMTAgZXN0aW1hdGUgYmVpbmcgc2xpZ2h0bHkgZ2VuZXJvdXMuIE5ldDogcm93cyArIDEuXG4gIGNvbnN0IGluc2lkZU1vZGFsID0gdXNlSXNJbnNpZGVNb2RhbCgpXG4gIGNvbnN0IHsgcm93cyB9ID0gdXNlTW9kYWxPclRlcm1pbmFsU2l6ZSh1c2VUZXJtaW5hbFNpemUoKSlcbiAgY29uc3QgY29udGVudEhlaWdodCA9IGluc2lkZU1vZGFsXG4gICAgPyByb3dzICsgMVxuICAgIDogTWF0aC5tYXgoMTUsIE1hdGgubWluKE1hdGguZmxvb3Iocm93cyAqIDAuOCksIDMwKSlcbiAgLy8gS2ljayBvZmYgZGlhZ25vc3RpY3Mgb25jZSB3aGVuIHRoZSBwYW5lIG9wZW5zLiBTdGF0dXMgdXNlKClzIHRoaXMgc29cbiAgLy8gaXQgcmVzb2x2ZXMgb25jZSBwZXIgL2NvbmZpZyBpbnZvY2F0aW9uIOKAlCBubyByZS1mZXRjaCBmbGFzaCB3aGVuXG4gIC8vIHRhYmJpbmcgYmFjayB0byBTdGF0dXMgKFRhYiB1bm1vdW50cyBjaGlsZHJlbiB3aGVuIG5vdCBzZWxlY3RlZCkuXG4gIGNvbnN0IFtkaWFnbm9zdGljc1Byb21pc2VdID0gdXNlU3RhdGUoKCkgPT5cbiAgICBidWlsZERpYWdub3N0aWNzKCkuY2F0Y2goKCkgPT4gW10pLFxuICApXG5cbiAgdXNlRXhpdE9uQ3RybENEV2l0aEtleWJpbmRpbmdzKClcblxuICAvLyBIYW5kbGUgZXNjYXBlIHZpYSBrZXliaW5kaW5nIC0gb25seSB3aGVuIG5vdCBpbiBzdWJtZW51XG4gIGNvbnN0IGhhbmRsZUVzY2FwZSA9ICgpID0+IHtcbiAgICAvLyBEb24ndCBoYW5kbGUgZXNjYXBlIHdoZW4gYSBzdWJtZW51IGlzIHNob3dpbmcgKHRhYnNIaWRkZW4gbWVhbnMgc3VibWVudSBpcyBvcGVuKVxuICAgIC8vIExldCB0aGUgc3VibWVudSBoYW5kbGUgZXNjYXBlIHRvIHJldHVybiB0byB0aGUgbWFpbiBtZW51XG4gICAgaWYgKHRhYnNIaWRkZW4pIHtcbiAgICAgIHJldHVyblxuICAgIH1cbiAgICAvLyBUT0RPOiBVcGRhdGUgdG8gXCJTZXR0aW5nc1wiIGRpYWxvZyBvbmNlIHdlIGRlZmluZSAnL3NldHRpbmdzJy5cbiAgICBvbkNsb3NlKCdTdGF0dXMgZGlhbG9nIGRpc21pc3NlZCcsIHsgZGlzcGxheTogJ3N5c3RlbScgfSlcbiAgfVxuXG4gIC8vIERpc2FibGUgd2hlbiBzdWJtZW51IGlzIG9wZW4gc28gdGhlIHN1Ym1lbnUncyBEaWFsb2cgY2FuIGhhbmRsZSBFU0MsXG4gIC8vIGFuZCB3aGVuIENvbmZpZydzIHNlYXJjaCBtb2RlIGlzIGFjdGl2ZSBzbyBpdHMgdXNlSW5wdXQgaGFuZGxlclxuICAvLyAoY2xlYXIgcXVlcnkg4oaSIGV4aXQgc2VhcmNoKSBwcm9jZXNzZXMgRXNjYXBlIGZpcnN0LlxuICB1c2VLZXliaW5kaW5nKCdjb25maXJtOm5vJywgaGFuZGxlRXNjYXBlLCB7XG4gICAgY29udGV4dDogJ1NldHRpbmdzJyxcbiAgICBpc0FjdGl2ZTpcbiAgICAgICF0YWJzSGlkZGVuICYmXG4gICAgICAhKHNlbGVjdGVkVGFiID09PSAnQ29uZmlnJyAmJiBjb25maWdPd25zRXNjKSAmJlxuICAgICAgIShzZWxlY3RlZFRhYiA9PT0gJ0dhdGVzJyAmJiBnYXRlc093bnNFc2MpLFxuICB9KVxuXG4gIGNvbnN0IHRhYnMgPSBbXG4gICAgPFRhYiBrZXk9XCJzdGF0dXNcIiB0aXRsZT1cIlN0YXR1c1wiPlxuICAgICAgPFN0YXR1cyBjb250ZXh0PXtjb250ZXh0fSBkaWFnbm9zdGljc1Byb21pc2U9e2RpYWdub3N0aWNzUHJvbWlzZX0gLz5cbiAgICA8L1RhYj4sXG4gICAgPFRhYiBrZXk9XCJjb25maWdcIiB0aXRsZT1cIkNvbmZpZ1wiPlxuICAgICAgPFN1c3BlbnNlIGZhbGxiYWNrPXtudWxsfT5cbiAgICAgICAgPENvbmZpZ1xuICAgICAgICAgIGNvbnRleHQ9e2NvbnRleHR9XG4gICAgICAgICAgb25DbG9zZT17b25DbG9zZX1cbiAgICAgICAgICBzZXRUYWJzSGlkZGVuPXtzZXRUYWJzSGlkZGVufVxuICAgICAgICAgIG9uSXNTZWFyY2hNb2RlQ2hhbmdlPXtzZXRDb25maWdPd25zRXNjfVxuICAgICAgICAgIGNvbnRlbnRIZWlnaHQ9e2NvbnRlbnRIZWlnaHR9XG4gICAgICAgIC8+XG4gICAgICA8L1N1c3BlbnNlPlxuICAgIDwvVGFiPixcbiAgICA8VGFiIGtleT1cInVzYWdlXCIgdGl0bGU9XCJVc2FnZVwiPlxuICAgICAgPFVzYWdlIC8+XG4gICAgPC9UYWI+LFxuICAgIC4uLihcImV4dGVybmFsXCIgPT09ICdhbnQnXG4gICAgICA/IFtcbiAgICAgICAgICA8VGFiIGtleT1cImdhdGVzXCIgdGl0bGU9XCJHYXRlc1wiPlxuICAgICAgICAgICAgPEdhdGVzXG4gICAgICAgICAgICAgIG9uT3duc0VzY0NoYW5nZT17c2V0R2F0ZXNPd25zRXNjfVxuICAgICAgICAgICAgICBjb250ZW50SGVpZ2h0PXtjb250ZW50SGVpZ2h0fVxuICAgICAgICAgICAgLz5cbiAgICAgICAgICA8L1RhYj4sXG4gICAgICAgIF1cbiAgICAgIDogW10pLFxuICBdXG5cbiAgcmV0dXJuIChcbiAgICA8UGFuZSBjb2xvcj1cInBlcm1pc3Npb25cIj5cbiAgICAgIDxUYWJzXG4gICAgICAgIGNvbG9yPVwicGVybWlzc2lvblwiXG4gICAgICAgIHNlbGVjdGVkVGFiPXtzZWxlY3RlZFRhYn1cbiAgICAgICAgb25UYWJDaGFuZ2U9e3NldFNlbGVjdGVkVGFifVxuICAgICAgICBoaWRkZW49e3RhYnNIaWRkZW59XG4gICAgICAgIC8vIENvbmZpZyBoYXMgaW50ZXJhY3RpdmUgY29udGVudCDigJQgc3RhcnQgd2l0aCBoZWFkZXIgdW5mb2N1c2VkIHNvXG4gICAgICAgIC8vIGxlZnQvcmlnaHQvdGFiIGN5Y2xlIG9wdGlvbiB2YWx1ZXMgaW5zdGVhZCBvZiBzd2l0Y2hpbmcgdGFicy5cbiAgICAgICAgaW5pdGlhbEhlYWRlckZvY3VzZWQ9e2RlZmF1bHRUYWIgIT09ICdDb25maWcnICYmIGRlZmF1bHRUYWIgIT09ICdHYXRlcyd9XG4gICAgICAgIC8vIEluc2lkZSBhIE1vZGFsLCBza2lwIHRoZSBUYWJzLWxldmVsIGNhcCBzbyB0YWxsIHRhYnMgKFN0YXR1cydzXG4gICAgICAgIC8vIE1DUCBsaXN0KSBmbG93IHRvIHRoZWlyIG5hdHVyYWwgaGVpZ2h0IGZvciB0aGUgTW9kYWwncyBTY3JvbGxCb3hcbiAgICAgICAgLy8gdG8gc2Nyb2xsLiBDb25maWcvR2F0ZXMgc3RpbGwgZ2V0IGNvbnRlbnRIZWlnaHQgYWJvdmUg4oCUIHRoZXlcbiAgICAgICAgLy8gcGFnaW5hdGUgaW50ZXJuYWxseSBzbyB0aGlzIG9ubHkgYWZmZWN0cyBTdGF0dXMvVXNhZ2UuXG4gICAgICAgIGNvbnRlbnRIZWlnaHQ9e3RhYnNIaWRkZW4gfHwgaW5zaWRlTW9kYWwgPyB1bmRlZmluZWQgOiBjb250ZW50SGVpZ2h0fVxuICAgICAgPlxuICAgICAgICB7dGFic31cbiAgICAgIDwvVGFicz5cbiAgICA8L1BhbmU+XG4gIClcbn1cbiJdLCJtYXBwaW5ncyI6IjtBQUFBO0FBQ0EsT0FBTyxLQUFLQSxLQUFLLE1BQU0sT0FBTztBQUM5QixTQUFTQyxRQUFRLEVBQUVDLFFBQVEsUUFBUSxPQUFPO0FBQzFDLFNBQVNDLGFBQWEsUUFBUSxvQ0FBb0M7QUFDbEUsU0FBU0MsOEJBQThCLFFBQVEsK0NBQStDO0FBQzlGLFNBQVNDLGVBQWUsUUFBUSxnQ0FBZ0M7QUFDaEUsU0FDRUMsZ0JBQWdCLEVBQ2hCQyxzQkFBc0IsUUFDakIsK0JBQStCO0FBQ3RDLFNBQVNDLElBQUksUUFBUSwwQkFBMEI7QUFDL0MsU0FBU0MsSUFBSSxFQUFFQyxHQUFHLFFBQVEsMEJBQTBCO0FBQ3BELFNBQVNDLE1BQU0sRUFBRUMsZ0JBQWdCLFFBQVEsYUFBYTtBQUN0RCxTQUFTQyxNQUFNLFFBQVEsYUFBYTtBQUNwQyxTQUFTQyxLQUFLLFFBQVEsWUFBWTtBQUNsQyxjQUNFQyxzQkFBc0IsRUFDdEJDLG9CQUFvQixRQUNmLG1CQUFtQjtBQUUxQixLQUFLQyxLQUFLLEdBQUc7RUFDWEMsT0FBTyxFQUFFLENBQ1BDLE1BQWUsQ0FBUixFQUFFLE1BQU0sRUFDZkMsT0FBNEMsQ0FBcEMsRUFBRTtJQUFFQyxPQUFPLENBQUMsRUFBRUwsb0JBQW9CO0VBQUMsQ0FBQyxFQUM1QyxHQUFHLElBQUk7RUFDVE0sT0FBTyxFQUFFUCxzQkFBc0I7RUFDL0JRLFVBQVUsRUFBRSxRQUFRLEdBQUcsUUFBUSxHQUFHLE9BQU8sR0FBRyxPQUFPO0FBQ3JELENBQUM7QUFFRCxPQUFPLFNBQUFDLFNBQUFDLEVBQUE7RUFBQSxNQUFBQyxDQUFBLEdBQUFDLEVBQUE7RUFBa0I7SUFBQVQsT0FBQTtJQUFBSSxPQUFBO0lBQUFDO0VBQUEsSUFBQUUsRUFJakI7RUFDTixPQUFBRyxXQUFBLEVBQUFDLGNBQUEsSUFBc0MzQixRQUFRLENBQVNxQixVQUFVLENBQUM7RUFDbEUsT0FBQU8sVUFBQSxFQUFBQyxhQUFBLElBQW9DN0IsUUFBUSxDQUFDLEtBQUssQ0FBQztFQUduRCxPQUFBOEIsYUFBQSxFQUFBQyxnQkFBQSxJQUEwQy9CLFFBQVEsQ0FBQyxLQUFLLENBQUM7RUFDekQsT0FBQWdDLFlBQUEsRUFBQUMsZUFBQSxJQUF3Q2pDLFFBQVEsQ0FBQyxLQUFLLENBQUM7RUFVdkQsTUFBQWtDLFdBQUEsR0FBb0I5QixnQkFBZ0IsQ0FBQyxDQUFDO0VBQ3RDO0lBQUErQjtFQUFBLElBQWlCOUIsc0JBQXNCLENBQUNGLGVBQWUsQ0FBQyxDQUFDLENBQUM7RUFDMUQsTUFBQWlDLGFBQUEsR0FBc0JGLFdBQVcsR0FDN0JDLElBQUksR0FBRyxDQUMyQyxHQUFsREUsSUFBSSxDQUFBQyxHQUFJLENBQUMsRUFBRSxFQUFFRCxJQUFJLENBQUFFLEdBQUksQ0FBQ0YsSUFBSSxDQUFBRyxLQUFNLENBQUNMLElBQUksR0FBRyxHQUFHLENBQUMsRUFBRSxFQUFFLENBQUMsQ0FBQztFQUl0RCxPQUFBTSxrQkFBQSxJQUE2QnpDLFFBQVEsQ0FBQzBDLE1BRXRDLENBQUM7RUFFRHhDLDhCQUE4QixDQUFDLENBQUM7RUFBQSxJQUFBeUMsRUFBQTtFQUFBLElBQUFuQixDQUFBLFFBQUFSLE9BQUEsSUFBQVEsQ0FBQSxRQUFBSSxVQUFBO0lBR1hlLEVBQUEsR0FBQUEsQ0FBQTtNQUduQixJQUFJZixVQUFVO1FBQUE7TUFBQTtNQUlkWixPQUFPLENBQUMseUJBQXlCLEVBQUU7UUFBQUcsT0FBQSxFQUFXO01BQVMsQ0FBQyxDQUFDO0lBQUEsQ0FDMUQ7SUFBQUssQ0FBQSxNQUFBUixPQUFBO0lBQUFRLENBQUEsTUFBQUksVUFBQTtJQUFBSixDQUFBLE1BQUFtQixFQUFBO0VBQUE7SUFBQUEsRUFBQSxHQUFBbkIsQ0FBQTtFQUFBO0VBUkQsTUFBQW9CLFlBQUEsR0FBcUJELEVBUXBCO0VBUUcsTUFBQUUsRUFBQSxJQUFDakIsVUFDMkMsSUFENUMsRUFDRUYsV0FBVyxLQUFLLFFBQXlCLElBQXpDSSxhQUF5QyxDQUNELElBRjFDLEVBRUVKLFdBQVcsS0FBSyxPQUF1QixJQUF2Q00sWUFBdUMsQ0FBQztFQUFBLElBQUFjLEVBQUE7RUFBQSxJQUFBdEIsQ0FBQSxRQUFBcUIsRUFBQTtJQUxKQyxFQUFBO01BQUExQixPQUFBLEVBQy9CLFVBQVU7TUFBQTJCLFFBQUEsRUFFakJGO0lBR0osQ0FBQztJQUFBckIsQ0FBQSxNQUFBcUIsRUFBQTtJQUFBckIsQ0FBQSxNQUFBc0IsRUFBQTtFQUFBO0lBQUFBLEVBQUEsR0FBQXRCLENBQUE7RUFBQTtFQU5EdkIsYUFBYSxDQUFDLFlBQVksRUFBRTJDLFlBQVksRUFBRUUsRUFNekMsQ0FBQztFQUFBLElBQUFFLEVBQUE7RUFBQSxJQUFBeEIsQ0FBQSxRQUFBSixPQUFBLElBQUFJLENBQUEsUUFBQWlCLGtCQUFBO0lBR0FPLEVBQUEsSUFBQyxHQUFHLENBQUssR0FBUSxDQUFSLFFBQVEsQ0FBTyxLQUFRLENBQVIsUUFBUSxDQUM5QixDQUFDLE1BQU0sQ0FBVTVCLE9BQU8sQ0FBUEEsUUFBTSxDQUFDLENBQXNCcUIsa0JBQWtCLENBQWxCQSxtQkFBaUIsQ0FBQyxHQUNsRSxFQUZDLEdBQUcsQ0FFRTtJQUFBakIsQ0FBQSxNQUFBSixPQUFBO0lBQUFJLENBQUEsTUFBQWlCLGtCQUFBO0lBQUFqQixDQUFBLE1BQUF3QixFQUFBO0VBQUE7SUFBQUEsRUFBQSxHQUFBeEIsQ0FBQTtFQUFBO0VBQUEsSUFBQXlCLEVBQUE7RUFBQSxJQUFBekIsQ0FBQSxRQUFBWSxhQUFBLElBQUFaLENBQUEsUUFBQUosT0FBQSxJQUFBSSxDQUFBLFNBQUFSLE9BQUE7SUFDTmlDLEVBQUEsSUFBQyxHQUFHLENBQUssR0FBUSxDQUFSLFFBQVEsQ0FBTyxLQUFRLENBQVIsUUFBUSxDQUM5QixDQUFDLFFBQVEsQ0FBVyxRQUFJLENBQUosS0FBRyxDQUFDLENBQ3RCLENBQUMsTUFBTSxDQUNJN0IsT0FBTyxDQUFQQSxRQUFNLENBQUMsQ0FDUEosT0FBTyxDQUFQQSxRQUFNLENBQUMsQ0FDRGEsYUFBYSxDQUFiQSxjQUFZLENBQUMsQ0FDTkUsb0JBQWdCLENBQWhCQSxpQkFBZSxDQUFDLENBQ3ZCSyxhQUFhLENBQWJBLGNBQVksQ0FBQyxHQUVoQyxFQVJDLFFBQVEsQ0FTWCxFQVZDLEdBQUcsQ0FVRTtJQUFBWixDQUFBLE1BQUFZLGFBQUE7SUFBQVosQ0FBQSxNQUFBSixPQUFBO0lBQUFJLENBQUEsT0FBQVIsT0FBQTtJQUFBUSxDQUFBLE9BQUF5QixFQUFBO0VBQUE7SUFBQUEsRUFBQSxHQUFBekIsQ0FBQTtFQUFBO0VBQUEsSUFBQTBCLEVBQUE7RUFBQSxJQUFBMUIsQ0FBQSxTQUFBMkIsTUFBQSxDQUFBQyxHQUFBO0lBQ05GLEVBQUEsSUFBQyxHQUFHLENBQUssR0FBTyxDQUFQLE9BQU8sQ0FBTyxLQUFPLENBQVAsT0FBTyxDQUM1QixDQUFDLEtBQUssR0FDUixFQUZDLEdBQUcsQ0FFRTtJQUFBMUIsQ0FBQSxPQUFBMEIsRUFBQTtFQUFBO0lBQUFBLEVBQUEsR0FBQTFCLENBQUE7RUFBQTtFQUFBLElBQUE2QixFQUFBO0VBQUEsSUFBQTdCLENBQUEsU0FBQVksYUFBQTtJQUNGaUIsRUFBQSxRQUFvQixHQUFwQixDQUVFLENBQUMsR0FBRyxDQUFLLEdBQU8sQ0FBUCxPQUFPLENBQU8sS0FBTyxDQUFQLE9BQU8sQ0FDNUIsQ0FBQyxLQUFLLENBQ2FwQixlQUFlLENBQWZBLGdCQUFjLENBQUMsQ0FDakJHLGFBQWEsQ0FBYkEsY0FBWSxDQUFDLEdBRWhDLEVBTEMsR0FBRyxDQUtFLENBRU4sR0FURixFQVNFO0lBQUFaLENBQUEsT0FBQVksYUFBQTtJQUFBWixDQUFBLE9BQUE2QixFQUFBO0VBQUE7SUFBQUEsRUFBQSxHQUFBN0IsQ0FBQTtFQUFBO0VBQUEsSUFBQThCLEVBQUE7RUFBQSxJQUFBOUIsQ0FBQSxTQUFBd0IsRUFBQSxJQUFBeEIsQ0FBQSxTQUFBeUIsRUFBQSxJQUFBekIsQ0FBQSxTQUFBNkIsRUFBQTtJQTNCS0MsRUFBQSxJQUNYTixFQUVNLEVBQ05DLEVBVU0sRUFDTkMsRUFFTSxLQUNGRyxFQVNFLENBQ1A7SUFBQTdCLENBQUEsT0FBQXdCLEVBQUE7SUFBQXhCLENBQUEsT0FBQXlCLEVBQUE7SUFBQXpCLENBQUEsT0FBQTZCLEVBQUE7SUFBQTdCLENBQUEsT0FBQThCLEVBQUE7RUFBQTtJQUFBQSxFQUFBLEdBQUE5QixDQUFBO0VBQUE7RUE1QkQsTUFBQStCLElBQUEsR0FBYUQsRUE0Qlo7RUFXMkIsTUFBQUUsRUFBQSxHQUFBbkMsVUFBVSxLQUFLLFFBQWtDLElBQXRCQSxVQUFVLEtBQUssT0FBTztFQUt4RCxNQUFBb0MsR0FBQSxHQUFBN0IsVUFBeUIsSUFBekJNLFdBQXFELEdBQXJEd0IsU0FBcUQsR0FBckR0QixhQUFxRDtFQUFBLElBQUF1QixHQUFBO0VBQUEsSUFBQW5DLENBQUEsU0FBQUUsV0FBQSxJQUFBRixDQUFBLFNBQUFpQyxHQUFBLElBQUFqQyxDQUFBLFNBQUFnQyxFQUFBLElBQUFoQyxDQUFBLFNBQUErQixJQUFBLElBQUEvQixDQUFBLFNBQUFJLFVBQUE7SUFieEUrQixHQUFBLElBQUMsSUFBSSxDQUFPLEtBQVksQ0FBWixZQUFZLENBQ3RCLENBQUMsSUFBSSxDQUNHLEtBQVksQ0FBWixZQUFZLENBQ0xqQyxXQUFXLENBQVhBLFlBQVUsQ0FBQyxDQUNYQyxXQUFjLENBQWRBLGVBQWEsQ0FBQyxDQUNuQkMsTUFBVSxDQUFWQSxXQUFTLENBQUMsQ0FHSSxvQkFBaUQsQ0FBakQsQ0FBQTRCLEVBQWdELENBQUMsQ0FLeEQsYUFBcUQsQ0FBckQsQ0FBQUMsR0FBb0QsQ0FBQyxDQUVuRUYsS0FBRyxDQUNOLEVBZkMsSUFBSSxDQWdCUCxFQWpCQyxJQUFJLENBaUJFO0lBQUEvQixDQUFBLE9BQUFFLFdBQUE7SUFBQUYsQ0FBQSxPQUFBaUMsR0FBQTtJQUFBakMsQ0FBQSxPQUFBZ0MsRUFBQTtJQUFBaEMsQ0FBQSxPQUFBK0IsSUFBQTtJQUFBL0IsQ0FBQSxPQUFBSSxVQUFBO0lBQUFKLENBQUEsT0FBQW1DLEdBQUE7RUFBQTtJQUFBQSxHQUFBLEdBQUFuQyxDQUFBO0VBQUE7RUFBQSxPQWpCUG1DLEdBaUJPO0FBQUE7QUF4R0osU0FBQWpCLE9BQUE7RUFBQSxPQTZCSGhDLGdCQUFnQixDQUFDLENBQUMsQ0FBQWtELEtBQU0sQ0FBQ0MsS0FBUSxDQUFDO0FBQUE7QUE3Qi9CLFNBQUFBLE1BQUE7RUFBQSxPQTZCNEIsRUFBRTtBQUFBIiwiaWdub3JlTGlzdCI6W119
 
 
1
  // biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered
2
+ import * as React from 'react'
3
+ import { Suspense, useState } from 'react'
4
+ import { useKeybinding } from '../../keybindings/useKeybinding.js'
5
+ import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js'
6
+ import { useTerminalSize } from '../../hooks/useTerminalSize.js'
7
+ import {
8
+ useIsInsideModal,
9
+ useModalOrTerminalSize,
10
+ } from '../../context/modalContext.js'
11
+ import { Pane } from '../design-system/Pane.js'
12
+ import { Tabs, Tab } from '../design-system/Tabs.js'
13
+ import { Status, buildDiagnostics } from './Status.js'
14
+ import { Config } from './Config.js'
15
+ import { Usage } from './Usage.js'
16
+ import type {
17
+ LocalJSXCommandContext,
18
+ CommandResultDisplay,
19
+ } from '../../commands.js'
20
+
21
  type Props = {
22
+ onClose: (
23
+ result?: string,
24
+ options?: { display?: CommandResultDisplay },
25
+ ) => void
26
+ context: LocalJSXCommandContext
27
+ defaultTab: 'Status' | 'Config' | 'Usage' | 'Gates'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  }
29
+
30
+ export function Settings({
31
+ onClose,
32
+ context,
33
+ defaultTab,
34
+ }: Props): React.ReactNode {
35
+ const [selectedTab, setSelectedTab] = useState<string>(defaultTab)
36
+ const [tabsHidden, setTabsHidden] = useState(false)
37
+ // True while Config's own Esc handler is active (search mode with content
38
+ // focused). Settings must cede Esc so search can clear/exit first.
39
+ const [configOwnsEsc, setConfigOwnsEsc] = useState(false)
40
+ const [gatesOwnsEsc, setGatesOwnsEsc] = useState(false)
41
+ // Fixed content height so switching tabs doesn't shift the pane height.
42
+ // Outside modals cap at min(80% viewport, 30). Inside a Modal the modal's
43
+ // innerSize.rows IS the ScrollBox viewport — the 0.8 multiplier over-
44
+ // shrinks, leaving empty rows while Config shows "↓ N more below".
45
+ //
46
+ // Inside-modal math: Config's paneCap-10 chrome estimate was tuned for
47
+ // marginY={1} (2 rows) which is stripped inside modals → +2 to recover.
48
+ // Then -2 for Tabs' header row + its marginTop=1. Plus +1 observed gap
49
+ // from the paneCap-10 estimate being slightly generous. Net: rows + 1.
50
+ const insideModal = useIsInsideModal()
51
+ const { rows } = useModalOrTerminalSize(useTerminalSize())
52
+ const contentHeight = insideModal
53
+ ? rows + 1
54
+ : Math.max(15, Math.min(Math.floor(rows * 0.8), 30))
55
+ // Kick off diagnostics once when the pane opens. Status use()s this so
56
+ // it resolves once per /config invocation — no re-fetch flash when
57
+ // tabbing back to Status (Tab unmounts children when not selected).
58
+ const [diagnosticsPromise] = useState(() =>
59
+ buildDiagnostics().catch(() => []),
60
+ )
61
+
62
+ useExitOnCtrlCDWithKeybindings()
63
+
64
+ // Handle escape via keybinding - only when not in submenu
65
+ const handleEscape = () => {
66
+ // Don't handle escape when a submenu is showing (tabsHidden means submenu is open)
67
+ // Let the submenu handle escape to return to the main menu
68
+ if (tabsHidden) {
69
+ return
70
+ }
71
+ // TODO: Update to "Settings" dialog once we define '/settings'.
72
+ onClose('Status dialog dismissed', { display: 'system' })
73
+ }
74
+
75
+ // Disable when submenu is open so the submenu's Dialog can handle ESC,
76
+ // and when Config's search mode is active so its useInput handler
77
+ // (clear query → exit search) processes Escape first.
78
+ useKeybinding('confirm:no', handleEscape, {
79
+ context: 'Settings',
80
+ isActive:
81
+ !tabsHidden &&
82
+ !(selectedTab === 'Config' && configOwnsEsc) &&
83
+ !(selectedTab === 'Gates' && gatesOwnsEsc),
84
+ })
85
+
86
+ const tabs = [
87
+ <Tab key="status" title="Status">
88
+ <Status context={context} diagnosticsPromise={diagnosticsPromise} />
89
+ </Tab>,
90
+ <Tab key="config" title="Config">
91
+ <Suspense fallback={null}>
92
+ <Config
93
+ context={context}
94
+ onClose={onClose}
95
+ setTabsHidden={setTabsHidden}
96
+ onIsSearchModeChange={setConfigOwnsEsc}
97
+ contentHeight={contentHeight}
98
+ />
99
+ </Suspense>
100
+ </Tab>,
101
+ <Tab key="usage" title="Usage">
102
+ <Usage />
103
+ </Tab>,
104
+ ...("external" === 'ant'
105
+ ? [
106
+ <Tab key="gates" title="Gates">
107
+ <Gates
108
+ onOwnsEscChange={setGatesOwnsEsc}
109
+ contentHeight={contentHeight}
110
+ />
111
+ </Tab>,
112
+ ]
113
+ : []),
114
+ ]
115
+
116
+ return (
117
+ <Pane color="permission">
118
+ <Tabs
119
+ color="permission"
120
+ selectedTab={selectedTab}
121
+ onTabChange={setSelectedTab}
122
+ hidden={tabsHidden}
123
+ // Config has interactive content — start with header unfocused so
124
+ // left/right/tab cycle option values instead of switching tabs.
125
+ initialHeaderFocused={defaultTab !== 'Config' && defaultTab !== 'Gates'}
126
+ // Inside a Modal, skip the Tabs-level cap so tall tabs (Status's
127
+ // MCP list) flow to their natural height for the Modal's ScrollBox
128
+ // to scroll. Config/Gates still get contentHeight above — they
129
+ // paginate internally so this only affects Status/Usage.
130
+ contentHeight={tabsHidden || insideModal ? undefined : contentHeight}
131
+ >
132
+ {tabs}
133
+ </Tabs>
134
+ </Pane>
135
+ )
136
  }
 
src/components/Settings/Settings.tsx.bak ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { c as _c } from "react/compiler-runtime";
2
+ // biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered
3
+ import * as React from 'react';
4
+ import { Suspense, useState } from 'react';
5
+ import { useKeybinding } from '../../keybindings/useKeybinding.js';
6
+ import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js';
7
+ import { useTerminalSize } from '../../hooks/useTerminalSize.js';
8
+ import { useIsInsideModal, useModalOrTerminalSize } from '../../context/modalContext.js';
9
+ import { Pane } from '../design-system/Pane.js';
10
+ import { Tabs, Tab } from '../design-system/Tabs.js';
11
+ import { Status, buildDiagnostics } from './Status.js';
12
+ import { Config } from './Config.js';
13
+ import { Usage } from './Usage.js';
14
+ import type { LocalJSXCommandContext, CommandResultDisplay } from '../../commands.js';
15
+ type Props = {
16
+ onClose: (result?: string, options?: {
17
+ display?: CommandResultDisplay;
18
+ }) => void;
19
+ context: LocalJSXCommandContext;
20
+ defaultTab: 'Status' | 'Config' | 'Usage' | 'Gates';
21
+ };
22
+ export function Settings(t0) {
23
+ const $ = _c(25);
24
+ const {
25
+ onClose,
26
+ context,
27
+ defaultTab
28
+ } = t0;
29
+ const [selectedTab, setSelectedTab] = useState(defaultTab);
30
+ const [tabsHidden, setTabsHidden] = useState(false);
31
+ const [configOwnsEsc, setConfigOwnsEsc] = useState(false);
32
+ const [gatesOwnsEsc, setGatesOwnsEsc] = useState(false);
33
+ const insideModal = useIsInsideModal();
34
+ const {
35
+ rows
36
+ } = useModalOrTerminalSize(useTerminalSize());
37
+ const contentHeight = insideModal ? rows + 1 : Math.max(15, Math.min(Math.floor(rows * 0.8), 30));
38
+ const [diagnosticsPromise] = useState(_temp2);
39
+ useExitOnCtrlCDWithKeybindings();
40
+ let t1;
41
+ if ($[0] !== onClose || $[1] !== tabsHidden) {
42
+ t1 = () => {
43
+ if (tabsHidden) {
44
+ return;
45
+ }
46
+ onClose("Status dialog dismissed", {
47
+ display: "system"
48
+ });
49
+ };
50
+ $[0] = onClose;
51
+ $[1] = tabsHidden;
52
+ $[2] = t1;
53
+ } else {
54
+ t1 = $[2];
55
+ }
56
+ const handleEscape = t1;
57
+ const t2 = !tabsHidden && !(selectedTab === "Config" && configOwnsEsc) && !(selectedTab === "Gates" && gatesOwnsEsc);
58
+ let t3;
59
+ if ($[3] !== t2) {
60
+ t3 = {
61
+ context: "Settings",
62
+ isActive: t2
63
+ };
64
+ $[3] = t2;
65
+ $[4] = t3;
66
+ } else {
67
+ t3 = $[4];
68
+ }
69
+ useKeybinding("confirm:no", handleEscape, t3);
70
+ let t4;
71
+ if ($[5] !== context || $[6] !== diagnosticsPromise) {
72
+ t4 = <Tab key="status" title="Status"><Status context={context} diagnosticsPromise={diagnosticsPromise} /></Tab>;
73
+ $[5] = context;
74
+ $[6] = diagnosticsPromise;
75
+ $[7] = t4;
76
+ } else {
77
+ t4 = $[7];
78
+ }
79
+ let t5;
80
+ if ($[8] !== contentHeight || $[9] !== context || $[10] !== onClose) {
81
+ t5 = <Tab key="config" title="Config"><Suspense fallback={null}><Config context={context} onClose={onClose} setTabsHidden={setTabsHidden} onIsSearchModeChange={setConfigOwnsEsc} contentHeight={contentHeight} /></Suspense></Tab>;
82
+ $[8] = contentHeight;
83
+ $[9] = context;
84
+ $[10] = onClose;
85
+ $[11] = t5;
86
+ } else {
87
+ t5 = $[11];
88
+ }
89
+ let t6;
90
+ if ($[12] === Symbol.for("react.memo_cache_sentinel")) {
91
+ t6 = <Tab key="usage" title="Usage"><Usage /></Tab>;
92
+ $[12] = t6;
93
+ } else {
94
+ t6 = $[12];
95
+ }
96
+ let t7;
97
+ if ($[13] !== contentHeight) {
98
+ t7 = false ? [<Tab key="gates" title="Gates"><Gates onOwnsEscChange={setGatesOwnsEsc} contentHeight={contentHeight} /></Tab>] : [];
99
+ $[13] = contentHeight;
100
+ $[14] = t7;
101
+ } else {
102
+ t7 = $[14];
103
+ }
104
+ let t8;
105
+ if ($[15] !== t4 || $[16] !== t5 || $[17] !== t7) {
106
+ t8 = [t4, t5, t6, ...t7];
107
+ $[15] = t4;
108
+ $[16] = t5;
109
+ $[17] = t7;
110
+ $[18] = t8;
111
+ } else {
112
+ t8 = $[18];
113
+ }
114
+ const tabs = t8;
115
+ const t9 = defaultTab !== "Config" && defaultTab !== "Gates";
116
+ const t10 = tabsHidden || insideModal ? undefined : contentHeight;
117
+ let t11;
118
+ if ($[19] !== selectedTab || $[20] !== t10 || $[21] !== t9 || $[22] !== tabs || $[23] !== tabsHidden) {
119
+ t11 = <Pane color="permission"><Tabs color="permission" selectedTab={selectedTab} onTabChange={setSelectedTab} hidden={tabsHidden} initialHeaderFocused={t9} contentHeight={t10}>{tabs}</Tabs></Pane>;
120
+ $[19] = selectedTab;
121
+ $[20] = t10;
122
+ $[21] = t9;
123
+ $[22] = tabs;
124
+ $[23] = tabsHidden;
125
+ $[24] = t11;
126
+ } else {
127
+ t11 = $[24];
128
+ }
129
+ return t11;
130
+ }
131
+ function _temp2() {
132
+ return buildDiagnostics().catch(_temp);
133
+ }
134
+ function _temp() {
135
+ return [];
136
+ }
137
+ //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsIlN1c3BlbnNlIiwidXNlU3RhdGUiLCJ1c2VLZXliaW5kaW5nIiwidXNlRXhpdE9uQ3RybENEV2l0aEtleWJpbmRpbmdzIiwidXNlVGVybWluYWxTaXplIiwidXNlSXNJbnNpZGVNb2RhbCIsInVzZU1vZGFsT3JUZXJtaW5hbFNpemUiLCJQYW5lIiwiVGFicyIsIlRhYiIsIlN0YXR1cyIsImJ1aWxkRGlhZ25vc3RpY3MiLCJDb25maWciLCJVc2FnZSIsIkxvY2FsSlNYQ29tbWFuZENvbnRleHQiLCJDb21tYW5kUmVzdWx0RGlzcGxheSIsIlByb3BzIiwib25DbG9zZSIsInJlc3VsdCIsIm9wdGlvbnMiLCJkaXNwbGF5IiwiY29udGV4dCIsImRlZmF1bHRUYWIiLCJTZXR0aW5ncyIsInQwIiwiJCIsIl9jIiwic2VsZWN0ZWRUYWIiLCJzZXRTZWxlY3RlZFRhYiIsInRhYnNIaWRkZW4iLCJzZXRUYWJzSGlkZGVuIiwiY29uZmlnT3duc0VzYyIsInNldENvbmZpZ093bnNFc2MiLCJnYXRlc093bnNFc2MiLCJzZXRHYXRlc093bnNFc2MiLCJpbnNpZGVNb2RhbCIsInJvd3MiLCJjb250ZW50SGVpZ2h0IiwiTWF0aCIsIm1heCIsIm1pbiIsImZsb29yIiwiZGlhZ25vc3RpY3NQcm9taXNlIiwiX3RlbXAyIiwidDEiLCJoYW5kbGVFc2NhcGUiLCJ0MiIsInQzIiwiaXNBY3RpdmUiLCJ0NCIsInQ1IiwidDYiLCJTeW1ib2wiLCJmb3IiLCJ0NyIsInQ4IiwidGFicyIsInQ5IiwidDEwIiwidW5kZWZpbmVkIiwidDExIiwiY2F0Y2giLCJfdGVtcCJdLCJzb3VyY2VzIjpbIlNldHRpbmdzLnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyIvLyBiaW9tZS1pZ25vcmUtYWxsIGFzc2lzdC9zb3VyY2Uvb3JnYW5pemVJbXBvcnRzOiBBTlQtT05MWSBpbXBvcnQgbWFya2VycyBtdXN0IG5vdCBiZSByZW9yZGVyZWRcbmltcG9ydCAqIGFzIFJlYWN0IGZyb20gJ3JlYWN0J1xuaW1wb3J0IHsgU3VzcGVuc2UsIHVzZVN0YXRlIH0gZnJvbSAncmVhY3QnXG5pbXBvcnQgeyB1c2VLZXliaW5kaW5nIH0gZnJvbSAnLi4vLi4va2V5YmluZGluZ3MvdXNlS2V5YmluZGluZy5qcydcbmltcG9ydCB7IHVzZUV4aXRPbkN0cmxDRFdpdGhLZXliaW5kaW5ncyB9IGZyb20gJy4uLy4uL2hvb2tzL3VzZUV4aXRPbkN0cmxDRFdpdGhLZXliaW5kaW5ncy5qcydcbmltcG9ydCB7IHVzZVRlcm1pbmFsU2l6ZSB9IGZyb20gJy4uLy4uL2hvb2tzL3VzZVRlcm1pbmFsU2l6ZS5qcydcbmltcG9ydCB7XG4gIHVzZUlzSW5zaWRlTW9kYWwsXG4gIHVzZU1vZGFsT3JUZXJtaW5hbFNpemUsXG59IGZyb20gJy4uLy4uL2NvbnRleHQvbW9kYWxDb250ZXh0LmpzJ1xuaW1wb3J0IHsgUGFuZSB9IGZyb20gJy4uL2Rlc2lnbi1zeXN0ZW0vUGFuZS5qcydcbmltcG9ydCB7IFRhYnMsIFRhYiB9IGZyb20gJy4uL2Rlc2lnbi1zeXN0ZW0vVGFicy5qcydcbmltcG9ydCB7IFN0YXR1cywgYnVpbGREaWFnbm9zdGljcyB9IGZyb20gJy4vU3RhdHVzLmpzJ1xuaW1wb3J0IHsgQ29uZmlnIH0gZnJvbSAnLi9Db25maWcuanMnXG5pbXBvcnQgeyBVc2FnZSB9IGZyb20gJy4vVXNhZ2UuanMnXG5pbXBvcnQgdHlwZSB7XG4gIExvY2FsSlNYQ29tbWFuZENvbnRleHQsXG4gIENvbW1hbmRSZXN1bHREaXNwbGF5LFxufSBmcm9tICcuLi8uLi9jb21tYW5kcy5qcydcblxudHlwZSBQcm9wcyA9IHtcbiAgb25DbG9zZTogKFxuICAgIHJlc3VsdD86IHN0cmluZyxcbiAgICBvcHRpb25zPzogeyBkaXNwbGF5PzogQ29tbWFuZFJlc3VsdERpc3BsYXkgfSxcbiAgKSA9PiB2b2lkXG4gIGNvbnRleHQ6IExvY2FsSlNYQ29tbWFuZENvbnRleHRcbiAgZGVmYXVsdFRhYjogJ1N0YXR1cycgfCAnQ29uZmlnJyB8ICdVc2FnZScgfCAnR2F0ZXMnXG59XG5cbmV4cG9ydCBmdW5jdGlvbiBTZXR0aW5ncyh7XG4gIG9uQ2xvc2UsXG4gIGNvbnRleHQsXG4gIGRlZmF1bHRUYWIsXG59OiBQcm9wcyk6IFJlYWN0LlJlYWN0Tm9kZSB7XG4gIGNvbnN0IFtzZWxlY3RlZFRhYiwgc2V0U2VsZWN0ZWRUYWJdID0gdXNlU3RhdGU8c3RyaW5nPihkZWZhdWx0VGFiKVxuICBjb25zdCBbdGFic0hpZGRlbiwgc2V0VGFic0hpZGRlbl0gPSB1c2VTdGF0ZShmYWxzZSlcbiAgLy8gVHJ1ZSB3aGlsZSBDb25maWcncyBvd24gRXNjIGhhbmRsZXIgaXMgYWN0aXZlIChzZWFyY2ggbW9kZSB3aXRoIGNvbnRlbnRcbiAgLy8gZm9jdXNlZCkuIFNldHRpbmdzIG11c3QgY2VkZSBFc2Mgc28gc2VhcmNoIGNhbiBjbGVhci9leGl0IGZpcnN0LlxuICBjb25zdCBbY29uZmlnT3duc0VzYywgc2V0Q29uZmlnT3duc0VzY10gPSB1c2VTdGF0ZShmYWxzZSlcbiAgY29uc3QgW2dhdGVzT3duc0VzYywgc2V0R2F0ZXNPd25zRXNjXSA9IHVzZVN0YXRlKGZhbHNlKVxuICAvLyBGaXhlZCBjb250ZW50IGhlaWdodCBzbyBzd2l0Y2hpbmcgdGFicyBkb2Vzbid0IHNoaWZ0IHRoZSBwYW5lIGhlaWdodC5cbiAgLy8gT3V0c2lkZSBtb2RhbHMgY2FwIGF0IG1pbig4MCUgdmlld3BvcnQsIDMwKS4gSW5zaWRlIGEgTW9kYWwgdGhlIG1vZGFsJ3NcbiAgLy8gaW5uZXJTaXplLnJvd3MgSVMgdGhlIFNjcm9sbEJveCB2aWV3cG9ydCDigJQgdGhlIDAuOCBtdWx0aXBsaWVyIG92ZXItXG4gIC8vIHNocmlua3MsIGxlYXZpbmcgZW1wdHkgcm93cyB3aGlsZSBDb25maWcgc2hvd3MgXCLihpMgTiBtb3JlIGJlbG93XCIuXG4gIC8vXG4gIC8vIEluc2lkZS1tb2RhbCBtYXRoOiBDb25maWcncyBwYW5lQ2FwLTEwIGNocm9tZSBlc3RpbWF0ZSB3YXMgdHVuZWQgZm9yXG4gIC8vIG1hcmdpblk9ezF9ICgyIHJvd3MpIHdoaWNoIGlzIHN0cmlwcGVkIGluc2lkZSBtb2RhbHMg4oaSICsyIHRvIHJlY292ZXIuXG4gIC8vIFRoZW4gLTIgZm9yIFRhYnMnIGhlYWRlciByb3cgKyBpdHMgbWFyZ2luVG9wPTEuIFBsdXMgKzEgb2JzZXJ2ZWQgZ2FwXG4gIC8vIGZyb20gdGhlIHBhbmVDYXAtMTAgZXN0aW1hdGUgYmVpbmcgc2xpZ2h0bHkgZ2VuZXJvdXMuIE5ldDogcm93cyArIDEuXG4gIGNvbnN0IGluc2lkZU1vZGFsID0gdXNlSXNJbnNpZGVNb2RhbCgpXG4gIGNvbnN0IHsgcm93cyB9ID0gdXNlTW9kYWxPclRlcm1pbmFsU2l6ZSh1c2VUZXJtaW5hbFNpemUoKSlcbiAgY29uc3QgY29udGVudEhlaWdodCA9IGluc2lkZU1vZGFsXG4gICAgPyByb3dzICsgMVxuICAgIDogTWF0aC5tYXgoMTUsIE1hdGgubWluKE1hdGguZmxvb3Iocm93cyAqIDAuOCksIDMwKSlcbiAgLy8gS2ljayBvZmYgZGlhZ25vc3RpY3Mgb25jZSB3aGVuIHRoZSBwYW5lIG9wZW5zLiBTdGF0dXMgdXNlKClzIHRoaXMgc29cbiAgLy8gaXQgcmVzb2x2ZXMgb25jZSBwZXIgL2NvbmZpZyBpbnZvY2F0aW9uIOKAlCBubyByZS1mZXRjaCBmbGFzaCB3aGVuXG4gIC8vIHRhYmJpbmcgYmFjayB0byBTdGF0dXMgKFRhYiB1bm1vdW50cyBjaGlsZHJlbiB3aGVuIG5vdCBzZWxlY3RlZCkuXG4gIGNvbnN0IFtkaWFnbm9zdGljc1Byb21pc2VdID0gdXNlU3RhdGUoKCkgPT5cbiAgICBidWlsZERpYWdub3N0aWNzKCkuY2F0Y2goKCkgPT4gW10pLFxuICApXG5cbiAgdXNlRXhpdE9uQ3RybENEV2l0aEtleWJpbmRpbmdzKClcblxuICAvLyBIYW5kbGUgZXNjYXBlIHZpYSBrZXliaW5kaW5nIC0gb25seSB3aGVuIG5vdCBpbiBzdWJtZW51XG4gIGNvbnN0IGhhbmRsZUVzY2FwZSA9ICgpID0+IHtcbiAgICAvLyBEb24ndCBoYW5kbGUgZXNjYXBlIHdoZW4gYSBzdWJtZW51IGlzIHNob3dpbmcgKHRhYnNIaWRkZW4gbWVhbnMgc3VibWVudSBpcyBvcGVuKVxuICAgIC8vIExldCB0aGUgc3VibWVudSBoYW5kbGUgZXNjYXBlIHRvIHJldHVybiB0byB0aGUgbWFpbiBtZW51XG4gICAgaWYgKHRhYnNIaWRkZW4pIHtcbiAgICAgIHJldHVyblxuICAgIH1cbiAgICAvLyBUT0RPOiBVcGRhdGUgdG8gXCJTZXR0aW5nc1wiIGRpYWxvZyBvbmNlIHdlIGRlZmluZSAnL3NldHRpbmdzJy5cbiAgICBvbkNsb3NlKCdTdGF0dXMgZGlhbG9nIGRpc21pc3NlZCcsIHsgZGlzcGxheTogJ3N5c3RlbScgfSlcbiAgfVxuXG4gIC8vIERpc2FibGUgd2hlbiBzdWJtZW51IGlzIG9wZW4gc28gdGhlIHN1Ym1lbnUncyBEaWFsb2cgY2FuIGhhbmRsZSBFU0MsXG4gIC8vIGFuZCB3aGVuIENvbmZpZydzIHNlYXJjaCBtb2RlIGlzIGFjdGl2ZSBzbyBpdHMgdXNlSW5wdXQgaGFuZGxlclxuICAvLyAoY2xlYXIgcXVlcnkg4oaSIGV4aXQgc2VhcmNoKSBwcm9jZXNzZXMgRXNjYXBlIGZpcnN0LlxuICB1c2VLZXliaW5kaW5nKCdjb25maXJtOm5vJywgaGFuZGxlRXNjYXBlLCB7XG4gICAgY29udGV4dDogJ1NldHRpbmdzJyxcbiAgICBpc0FjdGl2ZTpcbiAgICAgICF0YWJzSGlkZGVuICYmXG4gICAgICAhKHNlbGVjdGVkVGFiID09PSAnQ29uZmlnJyAmJiBjb25maWdPd25zRXNjKSAmJlxuICAgICAgIShzZWxlY3RlZFRhYiA9PT0gJ0dhdGVzJyAmJiBnYXRlc093bnNFc2MpLFxuICB9KVxuXG4gIGNvbnN0IHRhYnMgPSBbXG4gICAgPFRhYiBrZXk9XCJzdGF0dXNcIiB0aXRsZT1cIlN0YXR1c1wiPlxuICAgICAgPFN0YXR1cyBjb250ZXh0PXtjb250ZXh0fSBkaWFnbm9zdGljc1Byb21pc2U9e2RpYWdub3N0aWNzUHJvbWlzZX0gLz5cbiAgICA8L1RhYj4sXG4gICAgPFRhYiBrZXk9XCJjb25maWdcIiB0aXRsZT1cIkNvbmZpZ1wiPlxuICAgICAgPFN1c3BlbnNlIGZhbGxiYWNrPXtudWxsfT5cbiAgICAgICAgPENvbmZpZ1xuICAgICAgICAgIGNvbnRleHQ9e2NvbnRleHR9XG4gICAgICAgICAgb25DbG9zZT17b25DbG9zZX1cbiAgICAgICAgICBzZXRUYWJzSGlkZGVuPXtzZXRUYWJzSGlkZGVufVxuICAgICAgICAgIG9uSXNTZWFyY2hNb2RlQ2hhbmdlPXtzZXRDb25maWdPd25zRXNjfVxuICAgICAgICAgIGNvbnRlbnRIZWlnaHQ9e2NvbnRlbnRIZWlnaHR9XG4gICAgICAgIC8+XG4gICAgICA8L1N1c3BlbnNlPlxuICAgIDwvVGFiPixcbiAgICA8VGFiIGtleT1cInVzYWdlXCIgdGl0bGU9XCJVc2FnZVwiPlxuICAgICAgPFVzYWdlIC8+XG4gICAgPC9UYWI+LFxuICAgIC4uLihcImV4dGVybmFsXCIgPT09ICdhbnQnXG4gICAgICA/IFtcbiAgICAgICAgICA8VGFiIGtleT1cImdhdGVzXCIgdGl0bGU9XCJHYXRlc1wiPlxuICAgICAgICAgICAgPEdhdGVzXG4gICAgICAgICAgICAgIG9uT3duc0VzY0NoYW5nZT17c2V0R2F0ZXNPd25zRXNjfVxuICAgICAgICAgICAgICBjb250ZW50SGVpZ2h0PXtjb250ZW50SGVpZ2h0fVxuICAgICAgICAgICAgLz5cbiAgICAgICAgICA8L1RhYj4sXG4gICAgICAgIF1cbiAgICAgIDogW10pLFxuICBdXG5cbiAgcmV0dXJuIChcbiAgICA8UGFuZSBjb2xvcj1cInBlcm1pc3Npb25cIj5cbiAgICAgIDxUYWJzXG4gICAgICAgIGNvbG9yPVwicGVybWlzc2lvblwiXG4gICAgICAgIHNlbGVjdGVkVGFiPXtzZWxlY3RlZFRhYn1cbiAgICAgICAgb25UYWJDaGFuZ2U9e3NldFNlbGVjdGVkVGFifVxuICAgICAgICBoaWRkZW49e3RhYnNIaWRkZW59XG4gICAgICAgIC8vIENvbmZpZyBoYXMgaW50ZXJhY3RpdmUgY29udGVudCDigJQgc3RhcnQgd2l0aCBoZWFkZXIgdW5mb2N1c2VkIHNvXG4gICAgICAgIC8vIGxlZnQvcmlnaHQvdGFiIGN5Y2xlIG9wdGlvbiB2YWx1ZXMgaW5zdGVhZCBvZiBzd2l0Y2hpbmcgdGFicy5cbiAgICAgICAgaW5pdGlhbEhlYWRlckZvY3VzZWQ9e2RlZmF1bHRUYWIgIT09ICdDb25maWcnICYmIGRlZmF1bHRUYWIgIT09ICdHYXRlcyd9XG4gICAgICAgIC8vIEluc2lkZSBhIE1vZGFsLCBza2lwIHRoZSBUYWJzLWxldmVsIGNhcCBzbyB0YWxsIHRhYnMgKFN0YXR1cydzXG4gICAgICAgIC8vIE1DUCBsaXN0KSBmbG93IHRvIHRoZWlyIG5hdHVyYWwgaGVpZ2h0IGZvciB0aGUgTW9kYWwncyBTY3JvbGxCb3hcbiAgICAgICAgLy8gdG8gc2Nyb2xsLiBDb25maWcvR2F0ZXMgc3RpbGwgZ2V0IGNvbnRlbnRIZWlnaHQgYWJvdmUg4oCUIHRoZXlcbiAgICAgICAgLy8gcGFnaW5hdGUgaW50ZXJuYWxseSBzbyB0aGlzIG9ubHkgYWZmZWN0cyBTdGF0dXMvVXNhZ2UuXG4gICAgICAgIGNvbnRlbnRIZWlnaHQ9e3RhYnNIaWRkZW4gfHwgaW5zaWRlTW9kYWwgPyB1bmRlZmluZWQgOiBjb250ZW50SGVpZ2h0fVxuICAgICAgPlxuICAgICAgICB7dGFic31cbiAgICAgIDwvVGFicz5cbiAgICA8L1BhbmU+XG4gIClcbn1cbiJdLCJtYXBwaW5ncyI6IjtBQUFBO0FBQ0EsT0FBTyxLQUFLQSxLQUFLLE1BQU0sT0FBTztBQUM5QixTQUFTQyxRQUFRLEVBQUVDLFFBQVEsUUFBUSxPQUFPO0FBQzFDLFNBQVNDLGFBQWEsUUFBUSxvQ0FBb0M7QUFDbEUsU0FBU0MsOEJBQThCLFFBQVEsK0NBQStDO0FBQzlGLFNBQVNDLGVBQWUsUUFBUSxnQ0FBZ0M7QUFDaEUsU0FDRUMsZ0JBQWdCLEVBQ2hCQyxzQkFBc0IsUUFDakIsK0JBQStCO0FBQ3RDLFNBQVNDLElBQUksUUFBUSwwQkFBMEI7QUFDL0MsU0FBU0MsSUFB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
src/hooks/useAutoTTS.ts CHANGED
@@ -5,7 +5,6 @@ import type { RenderableMessage } from '../types/message.js'
5
 
6
  export function useAutoTTS(messages: RenderableMessage[]): void {
7
  const triggeredIdsRef = useRef<Set<string>>(new Set())
8
- const pendingPlayRef = useRef<Promise<void> | null>(null)
9
 
10
  useEffect(() => {
11
  const settings = getInitialSettings()
 
5
 
6
  export function useAutoTTS(messages: RenderableMessage[]): void {
7
  const triggeredIdsRef = useRef<Set<string>>(new Set())
 
8
 
9
  useEffect(() => {
10
  const settings = getInitialSettings()
src/hooks/useVoice.ts CHANGED
@@ -22,7 +22,7 @@ import {
22
  type VoiceStreamConnection,
23
  } from '../services/voiceStreamSTT.js'
24
  import { connectDoubaoStream } from '../services/doubaoSTT.js'
25
- import { connectLocalWhisperStream } from '../services/voice/whisperSTT.js'
26
  import { logForDebugging } from '../utils/debug.js'
27
  import { toError } from '../utils/errors.js'
28
  import { getSystemLocaleLanguage } from '../utils/intl.js'
@@ -804,6 +804,7 @@ export function useVoice({
804
  opts: { language: string; keyterms: string[] },
805
  ) => Promise<VoiceStreamConnection | null>
806
  if (isLocalProvider()) {
 
807
  connectFn = (cbs, _opts) =>
808
  connectLocalWhisperStream(cbs, { language: stt.code })
809
  } else if (isDoubaoProvider()) {
 
22
  type VoiceStreamConnection,
23
  } from '../services/voiceStreamSTT.js'
24
  import { connectDoubaoStream } from '../services/doubaoSTT.js'
25
+ import { connectLocalWhisperStream, preloadWhisperModel } from '../services/voice/whisperSTT.js'
26
  import { logForDebugging } from '../utils/debug.js'
27
  import { toError } from '../utils/errors.js'
28
  import { getSystemLocaleLanguage } from '../utils/intl.js'
 
804
  opts: { language: string; keyterms: string[] },
805
  ) => Promise<VoiceStreamConnection | null>
806
  if (isLocalProvider()) {
807
+ void preloadWhisperModel({ language: stt.code })
808
  connectFn = (cbs, _opts) =>
809
  connectLocalWhisperStream(cbs, { language: stt.code })
810
  } else if (isDoubaoProvider()) {
src/screens/REPL.tsx CHANGED
The diff for this file is too large to render. See raw diff
 
src/screens/REPL.tsx.bak ADDED
The diff for this file is too large to render. See raw diff
 
src/services/voice/edgeTTS.ts CHANGED
@@ -81,8 +81,12 @@ export async function speakWithEdgeTTS(
81
  }
82
 
83
  try {
84
- const result = JSON.parse(stdout)
85
- resolve(result)
 
 
 
 
86
  } catch {
87
  resolve({
88
  success: false,
 
81
  }
82
 
83
  try {
84
+ const raw = JSON.parse(stdout)
85
+ resolve({
86
+ success: raw.success,
87
+ audioPath: raw.audio_path ?? raw.audioPath,
88
+ error: raw.error,
89
+ })
90
  } catch {
91
  resolve({
92
  success: false,
src/services/voice/whisperSTT.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { spawn } from 'child_process'
2
  import { existsSync, mkdtempSync, writeFileSync, unlinkSync, rmdirSync } from 'fs'
3
  import { tmpdir } from 'os'
4
  import { join, dirname } from 'path'
@@ -21,14 +21,7 @@ const PROJECT_ROOT = findProjectRoot()
21
  const SCRIPTS_DIR = join(PROJECT_ROOT, 'scripts')
22
  const VENV_PYTHON = join(PROJECT_ROOT, '.venv', 'bin', 'python')
23
 
24
- const LOG = '/tmp/vc_whisper.log'
25
- function log(...args: unknown[]) {
26
- const line = `[${new Date().toISOString()}] ${args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ')}\n`
27
- try { writeFileSync(LOG, line, { flag: 'a' }) } catch {}
28
- }
29
-
30
- function resolvePythonPath(customPath?: string): string {
31
- if (customPath) return customPath
32
  if (existsSync(VENV_PYTHON)) return VENV_PYTHON
33
  return 'python3'
34
  }
@@ -39,91 +32,159 @@ type WhisperOptions = {
39
  pythonPath?: string
40
  }
41
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  export function connectLocalWhisperStream(
43
  callbacks: VoiceStreamCallbacks,
44
  options?: WhisperOptions,
45
  ): Promise<VoiceStreamConnection | null> {
46
- return new Promise(resolve => {
 
 
 
 
47
  const chunks: Buffer[] = []
48
  let finalized = false
49
  let tmpDir: string | null = null
50
 
51
- const python = resolvePythonPath(options?.pythonPath)
52
- const model = options?.model ?? 'large-v3-turbo'
53
- const language = options?.language
54
- log('projectRoot', PROJECT_ROOT, 'python', python, 'model', model, 'language', language)
55
-
56
  const connection: VoiceStreamConnection = {
57
  send(chunk: Buffer) {
58
  if (finalized) return
59
  chunks.push(Buffer.from(chunk))
60
- if (chunks.length === 1) log('first chunk', chunk.length, 'bytes')
61
  },
62
 
63
  async finalize(): Promise<FinalizeSource> {
64
  if (finalized) return 'ws_already_closed'
65
  finalized = true
66
- log('finalize called, chunks', chunks.length)
67
 
68
  if (chunks.length === 0) {
69
- log('no chunks, returning no_data_timeout')
70
  callbacks.onClose()
71
  return 'no_data_timeout'
72
  }
73
 
74
  const audioBuf = Buffer.concat(chunks)
75
- log('total audio', audioBuf.length, 'bytes')
76
 
77
  try {
78
  tmpDir = mkdtempSync(join(tmpdir(), 'vc-whisper-'))
79
  const wavPath = join(tmpDir, 'input.wav')
80
-
81
  writeWavHeader(wavPath, audioBuf, 16000)
82
 
83
- const args = [join(SCRIPTS_DIR, 'transcribe.py'), wavPath, '--model', model]
84
- if (language) {
85
- args.push('--language', language)
86
- }
87
- log('spawning', python, args.join(' '))
88
-
89
- const proc = spawn(python, args, { stdio: ['ignore', 'pipe', 'pipe'] })
90
-
91
- let stdout = ''
92
- let stderr = ''
93
-
94
- proc.stdout.on('data', (data: Buffer) => {
95
- stdout += data.toString()
96
- })
97
-
98
- proc.stderr.on('data', (data: Buffer) => {
99
- stderr += data.toString()
100
- })
101
-
102
- const exitCode = await new Promise<number>(resolveExit => {
103
- proc.on('close', resolveExit)
104
- })
105
-
106
- log('exitCode', exitCode, 'stdout', stdout.slice(0, 500), 'stderr', stderr.slice(0, 500))
107
-
108
- if (exitCode !== 0) {
109
- callbacks.onError(`Whisper transcription failed: ${stderr || 'unknown error'}`, {
110
- fatal: true,
111
- })
112
- callbacks.onClose()
113
- return 'ws_close'
114
- }
115
-
116
- const result = JSON.parse(stdout)
117
- log('result', result)
118
- if (result.success && result.text) {
119
  callbacks.onTranscript(result.text, true)
120
- } else if (result.success && !result.text) {
121
- callbacks.onTranscript('', true)
122
  } else {
123
- callbacks.onError(
124
- result.error ?? 'Transcription failed',
125
- { fatal: true },
126
- )
127
  }
128
  } catch (err) {
129
  callbacks.onError(
@@ -132,12 +193,8 @@ export function connectLocalWhisperStream(
132
  )
133
  } finally {
134
  if (tmpDir) {
135
- try {
136
- unlinkSync(join(tmpDir, 'input.wav'))
137
- } catch {}
138
- try {
139
- rmdirSync(tmpDir)
140
- } catch {}
141
  }
142
  callbacks.onClose()
143
  }
@@ -160,29 +217,20 @@ export function connectLocalWhisperStream(
160
  })
161
  }
162
 
163
- export async function checkLocalWhisperAvailable(pythonPath?: string): Promise<boolean> {
164
- const python = resolvePythonPath(pythonPath)
165
- return new Promise(resolve => {
166
- const proc = spawn(python, [
167
- '-c',
168
- 'import json;' +
169
- 'try: import whisper; print(json.dumps({"ok": True, "backend": "whisper"}))' +
170
- 'except ImportError:' +
171
- ' try: from faster_whisper import WhisperModel; print(json.dumps({"ok": True, "backend": "faster-whisper"}))' +
172
- ' except ImportError: print(json.dumps({"ok": False}))',
173
- ], { stdio: ['ignore', 'pipe', 'pipe'] })
174
-
175
- let stdout = ''
176
- proc.stdout.on('data', (data: Buffer) => { stdout += data.toString() })
177
- proc.on('close', () => {
178
- try {
179
- const result = JSON.parse(stdout)
180
- resolve(result.ok === true)
181
- } catch {
182
- resolve(false)
183
- }
184
  })
185
- })
 
 
186
  }
187
 
188
  function writeWavHeader(path: string, pcmData: Buffer, sampleRate: number): void {
@@ -193,11 +241,9 @@ function writeWavHeader(path: string, pcmData: Buffer, sampleRate: number): void
193
  const dataSize = pcmData.length
194
 
195
  const header = Buffer.alloc(44)
196
-
197
  header.write('RIFF', 0)
198
  header.writeUInt32LE(36 + dataSize, 4)
199
  header.write('WAVE', 8)
200
-
201
  header.write('fmt ', 12)
202
  header.writeUInt32LE(16, 16)
203
  header.writeUInt16LE(1, 20)
@@ -206,9 +252,7 @@ function writeWavHeader(path: string, pcmData: Buffer, sampleRate: number): void
206
  header.writeUInt32LE(byteRate, 28)
207
  header.writeUInt16LE(blockAlign, 32)
208
  header.writeUInt16LE(bitsPerSample, 34)
209
-
210
  header.write('data', 36)
211
  header.writeUInt32LE(dataSize, 40)
212
-
213
  writeFileSync(path, Buffer.concat([header, pcmData]))
214
  }
 
1
+ import { spawn, type ChildProcess } from 'child_process'
2
  import { existsSync, mkdtempSync, writeFileSync, unlinkSync, rmdirSync } from 'fs'
3
  import { tmpdir } from 'os'
4
  import { join, dirname } from 'path'
 
21
  const SCRIPTS_DIR = join(PROJECT_ROOT, 'scripts')
22
  const VENV_PYTHON = join(PROJECT_ROOT, '.venv', 'bin', 'python')
23
 
24
+ function resolvePythonPath(): string {
 
 
 
 
 
 
 
25
  if (existsSync(VENV_PYTHON)) return VENV_PYTHON
26
  return 'python3'
27
  }
 
32
  pythonPath?: string
33
  }
34
 
35
+ let serverProc: ChildProcess | null = null
36
+ let serverLoaded = false
37
+ let serverLoadingResolve: (() => void) | null = null
38
+ let serverLoadingReject: ((err: Error) => void) | null = null
39
+
40
+ function getServer(): ChildProcess {
41
+ if (serverProc && serverProc.exitCode === null) {
42
+ return serverProc
43
+ }
44
+ const python = resolvePythonPath()
45
+ serverProc = spawn(python, [join(SCRIPTS_DIR, 'whisper_server.py')], {
46
+ stdio: ['pipe', 'pipe', 'pipe'],
47
+ })
48
+
49
+ let buf = ''
50
+ serverProc.stdout!.on('data', (data: Buffer) => {
51
+ buf += data.toString()
52
+ const lines = buf.split('\n')
53
+ buf = lines.pop() ?? ''
54
+ for (const line of lines) {
55
+ if (!line) continue
56
+ try {
57
+ const msg = JSON.parse(line)
58
+ if (msg.type === 'ready') {
59
+ serverLoaded = true
60
+ serverLoadingResolve?.()
61
+ serverLoadingResolve = null
62
+ serverLoadingReject = null
63
+ } else if (msg.type === 'error') {
64
+ serverLoaded = false
65
+ serverLoadingReject?.(new Error(msg.message))
66
+ serverLoadingResolve = null
67
+ serverLoadingReject = null
68
+ }
69
+ } catch {}
70
+ }
71
+ })
72
+
73
+ serverProc.on('error', () => {
74
+ serverProc = null
75
+ serverLoaded = false
76
+ serverLoadingReject?.(new Error('Server process error'))
77
+ })
78
+ serverProc.on('close', () => {
79
+ serverProc = null
80
+ serverLoaded = false
81
+ })
82
+
83
+ return serverProc
84
+ }
85
+
86
+ export async function preloadWhisperModel(options?: WhisperOptions): Promise<void> {
87
+ if (serverLoaded) return
88
+
89
+ const proc = getServer()
90
+ const model = options?.model ?? 'small'
91
+
92
+ const responseReady = new Promise<void>((resolve, reject) => {
93
+ serverLoadingResolve = resolve
94
+ serverLoadingReject = reject
95
+ proc.stdin!.write(JSON.stringify({ type: 'load', model }) + '\n')
96
+ })
97
+
98
+ const timeout = new Promise<never>((_, reject) => {
99
+ setTimeout(() => reject(new Error('Model preload timeout')), 30000)
100
+ })
101
+
102
+ await Promise.race([responseReady, timeout])
103
+ }
104
+
105
+ async function transcribeWithServer(
106
+ wavPath: string,
107
+ language?: string | null,
108
+ ): Promise<{ text: string; language: string }> {
109
+ const proc = getServer()
110
+
111
+ const result = await new Promise<{ text: string; language: string }>((resolve, reject) => {
112
+ const timeout = setTimeout(() => {
113
+ cleanup()
114
+ reject(new Error('Transcription timeout'))
115
+ }, 30000)
116
+
117
+ let buf = ''
118
+ const cleanup = () => {
119
+ clearTimeout(timeout)
120
+ proc.stdout!.removeAllListeners('data')
121
+ }
122
+
123
+ proc.stdout!.on('data', (data: Buffer) => {
124
+ buf += data.toString()
125
+ const lines = buf.split('\n')
126
+ buf = lines.pop() ?? ''
127
+ for (const line of lines) {
128
+ if (!line) continue
129
+ try {
130
+ const msg = JSON.parse(line)
131
+ if (msg.type === 'result') {
132
+ cleanup()
133
+ resolve({ text: msg.text, language: msg.language })
134
+ } else if (msg.type === 'error') {
135
+ cleanup()
136
+ reject(new Error(msg.message))
137
+ }
138
+ } catch {}
139
+ }
140
+ })
141
+
142
+ proc.stdin!.write(JSON.stringify({ type: 'transcribe', wav: wavPath, language }) + '\n')
143
+ })
144
+
145
+ return result
146
+ }
147
+
148
  export function connectLocalWhisperStream(
149
  callbacks: VoiceStreamCallbacks,
150
  options?: WhisperOptions,
151
  ): Promise<VoiceStreamConnection | null> {
152
+ return new Promise(async resolve => {
153
+ if (!serverLoaded) {
154
+ await preloadWhisperModel(options)
155
+ }
156
+
157
  const chunks: Buffer[] = []
158
  let finalized = false
159
  let tmpDir: string | null = null
160
 
 
 
 
 
 
161
  const connection: VoiceStreamConnection = {
162
  send(chunk: Buffer) {
163
  if (finalized) return
164
  chunks.push(Buffer.from(chunk))
 
165
  },
166
 
167
  async finalize(): Promise<FinalizeSource> {
168
  if (finalized) return 'ws_already_closed'
169
  finalized = true
 
170
 
171
  if (chunks.length === 0) {
 
172
  callbacks.onClose()
173
  return 'no_data_timeout'
174
  }
175
 
176
  const audioBuf = Buffer.concat(chunks)
 
177
 
178
  try {
179
  tmpDir = mkdtempSync(join(tmpdir(), 'vc-whisper-'))
180
  const wavPath = join(tmpDir, 'input.wav')
 
181
  writeWavHeader(wavPath, audioBuf, 16000)
182
 
183
+ const result = await transcribeWithServer(wavPath, options?.language)
184
+ if (result.text) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  callbacks.onTranscript(result.text, true)
 
 
186
  } else {
187
+ callbacks.onTranscript('', true)
 
 
 
188
  }
189
  } catch (err) {
190
  callbacks.onError(
 
193
  )
194
  } finally {
195
  if (tmpDir) {
196
+ try { unlinkSync(join(tmpDir, 'input.wav')) } catch {}
197
+ try { rmdirSync(tmpDir) } catch {}
 
 
 
 
198
  }
199
  callbacks.onClose()
200
  }
 
217
  })
218
  }
219
 
220
+ export async function checkLocalWhisperAvailable(): Promise<boolean> {
221
+ try {
222
+ const python = resolvePythonPath()
223
+ return await new Promise(resolve => {
224
+ const proc = spawn(python, ['-c', 'import whisper; print("ok")'], {
225
+ stdio: ['ignore', 'pipe', 'pipe'],
226
+ })
227
+ let out = ''
228
+ proc.stdout.on('data', d => { out += d.toString() })
229
+ proc.on('close', () => { resolve(out.trim() === 'ok') })
 
 
 
 
 
 
 
 
 
 
 
230
  })
231
+ } catch {
232
+ return false
233
+ }
234
  }
235
 
236
  function writeWavHeader(path: string, pcmData: Buffer, sampleRate: number): void {
 
241
  const dataSize = pcmData.length
242
 
243
  const header = Buffer.alloc(44)
 
244
  header.write('RIFF', 0)
245
  header.writeUInt32LE(36 + dataSize, 4)
246
  header.write('WAVE', 8)
 
247
  header.write('fmt ', 12)
248
  header.writeUInt32LE(16, 16)
249
  header.writeUInt16LE(1, 20)
 
252
  header.writeUInt32LE(byteRate, 28)
253
  header.writeUInt16LE(blockAlign, 32)
254
  header.writeUInt16LE(bitsPerSample, 34)
 
255
  header.write('data', 36)
256
  header.writeUInt32LE(dataSize, 40)
 
257
  writeFileSync(path, Buffer.concat([header, pcmData]))
258
  }
src/utils/processUserInput/processSlashCommand.tsx CHANGED
The diff for this file is too large to render. See raw diff
 
src/utils/processUserInput/processSlashCommand.tsx.bak ADDED
The diff for this file is too large to render. See raw diff