npcsh 1.1.22__py3-none-any.whl → 1.1.23__py3-none-any.whl
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.
- npcsh/_state.py +272 -120
- npcsh/benchmark/npcsh_agent.py +77 -240
- npcsh/benchmark/templates/install-npcsh.sh.j2 +12 -4
- npcsh/config.py +5 -2
- npcsh/npc_team/alicanto.npc +4 -8
- npcsh/npc_team/corca.npc +5 -11
- npcsh/npc_team/frederic.npc +4 -6
- npcsh/npc_team/guac.npc +4 -4
- npcsh/npc_team/jinxs/lib/core/delegate.jinx +1 -1
- npcsh/npc_team/jinxs/lib/core/edit_file.jinx +1 -1
- npcsh/npc_team/jinxs/lib/core/sh.jinx +1 -1
- npcsh/npc_team/jinxs/lib/core/skill.jinx +59 -0
- npcsh/npc_team/jinxs/lib/utils/help.jinx +194 -10
- npcsh/npc_team/jinxs/lib/utils/init.jinx +528 -37
- npcsh/npc_team/jinxs/lib/utils/jinxs.jinx +0 -1
- npcsh/npc_team/jinxs/lib/utils/serve.jinx +938 -21
- npcsh-1.1.22.data/data/npcsh/npc_team/config_tui.jinx → npcsh/npc_team/jinxs/modes/config.jinx +1 -1
- npcsh/npc_team/jinxs/modes/convene.jinx +76 -3
- npcsh/npc_team/jinxs/modes/crond.jinx +818 -0
- npcsh/npc_team/jinxs/modes/plonk.jinx +76 -14
- npcsh/npc_team/jinxs/modes/roll.jinx +368 -55
- npcsh/npc_team/jinxs/modes/skills.jinx +621 -0
- npcsh/npc_team/jinxs/modes/yap.jinx +504 -30
- npcsh/npc_team/jinxs/skills/code-review/SKILL.md +45 -0
- npcsh/npc_team/jinxs/skills/debugging/SKILL.md +44 -0
- npcsh/npc_team/jinxs/skills/git-workflow.jinx +44 -0
- npcsh/npc_team/kadiefa.npc +4 -5
- npcsh/npc_team/npcsh.ctx +16 -0
- npcsh/npc_team/plonk.npc +5 -9
- npcsh/npc_team/sibiji.npc +13 -5
- npcsh/npcsh.py +1 -0
- npcsh/routes.py +0 -4
- npcsh/yap.py +22 -4
- npcsh-1.1.23.data/data/npcsh/npc_team/SKILL.md +44 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/alicanto.npc +4 -8
- npcsh/npc_team/jinxs/modes/config_tui.jinx → npcsh-1.1.23.data/data/npcsh/npc_team/config.jinx +1 -1
- npcsh-1.1.23.data/data/npcsh/npc_team/convene.jinx +670 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/corca.npc +5 -11
- npcsh-1.1.23.data/data/npcsh/npc_team/crond.jinx +818 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/delegate.jinx +1 -1
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/edit_file.jinx +1 -1
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/frederic.npc +4 -6
- npcsh-1.1.23.data/data/npcsh/npc_team/git-workflow.jinx +44 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/guac.npc +4 -4
- npcsh-1.1.23.data/data/npcsh/npc_team/help.jinx +236 -0
- npcsh-1.1.23.data/data/npcsh/npc_team/init.jinx +532 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/jinxs.jinx +0 -1
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/kadiefa.npc +4 -5
- npcsh-1.1.23.data/data/npcsh/npc_team/npcsh.ctx +34 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonk.jinx +76 -14
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonk.npc +5 -9
- npcsh-1.1.23.data/data/npcsh/npc_team/roll.jinx +378 -0
- npcsh-1.1.23.data/data/npcsh/npc_team/serve.jinx +943 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sh.jinx +1 -1
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sibiji.npc +13 -5
- npcsh-1.1.23.data/data/npcsh/npc_team/skill.jinx +59 -0
- npcsh-1.1.23.data/data/npcsh/npc_team/skills.jinx +621 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/yap.jinx +504 -30
- {npcsh-1.1.22.dist-info → npcsh-1.1.23.dist-info}/METADATA +168 -7
- npcsh-1.1.23.dist-info/RECORD +216 -0
- npcsh/npc_team/jinxs/incognide/add_tab.jinx +0 -11
- npcsh/npc_team/jinxs/incognide/close_pane.jinx +0 -9
- npcsh/npc_team/jinxs/incognide/close_tab.jinx +0 -10
- npcsh/npc_team/jinxs/incognide/confirm.jinx +0 -10
- npcsh/npc_team/jinxs/incognide/focus_pane.jinx +0 -9
- npcsh/npc_team/jinxs/incognide/list_panes.jinx +0 -8
- npcsh/npc_team/jinxs/incognide/navigate.jinx +0 -10
- npcsh/npc_team/jinxs/incognide/notify.jinx +0 -10
- npcsh/npc_team/jinxs/incognide/open_pane.jinx +0 -13
- npcsh/npc_team/jinxs/incognide/read_pane.jinx +0 -9
- npcsh/npc_team/jinxs/incognide/run_terminal.jinx +0 -10
- npcsh/npc_team/jinxs/incognide/send_message.jinx +0 -10
- npcsh/npc_team/jinxs/incognide/split_pane.jinx +0 -12
- npcsh/npc_team/jinxs/incognide/switch_npc.jinx +0 -10
- npcsh/npc_team/jinxs/incognide/switch_tab.jinx +0 -10
- npcsh/npc_team/jinxs/incognide/write_file.jinx +0 -11
- npcsh/npc_team/jinxs/incognide/zen_mode.jinx +0 -9
- npcsh/npc_team/jinxs/lib/core/convene.jinx +0 -232
- npcsh-1.1.22.data/data/npcsh/npc_team/add_tab.jinx +0 -11
- npcsh-1.1.22.data/data/npcsh/npc_team/close_pane.jinx +0 -9
- npcsh-1.1.22.data/data/npcsh/npc_team/close_tab.jinx +0 -10
- npcsh-1.1.22.data/data/npcsh/npc_team/confirm.jinx +0 -10
- npcsh-1.1.22.data/data/npcsh/npc_team/convene.jinx +0 -232
- npcsh-1.1.22.data/data/npcsh/npc_team/focus_pane.jinx +0 -9
- npcsh-1.1.22.data/data/npcsh/npc_team/help.jinx +0 -52
- npcsh-1.1.22.data/data/npcsh/npc_team/init.jinx +0 -41
- npcsh-1.1.22.data/data/npcsh/npc_team/list_panes.jinx +0 -8
- npcsh-1.1.22.data/data/npcsh/npc_team/navigate.jinx +0 -10
- npcsh-1.1.22.data/data/npcsh/npc_team/notify.jinx +0 -10
- npcsh-1.1.22.data/data/npcsh/npc_team/npcsh.ctx +0 -18
- npcsh-1.1.22.data/data/npcsh/npc_team/open_pane.jinx +0 -13
- npcsh-1.1.22.data/data/npcsh/npc_team/read_pane.jinx +0 -9
- npcsh-1.1.22.data/data/npcsh/npc_team/roll.jinx +0 -65
- npcsh-1.1.22.data/data/npcsh/npc_team/run_terminal.jinx +0 -10
- npcsh-1.1.22.data/data/npcsh/npc_team/send_message.jinx +0 -10
- npcsh-1.1.22.data/data/npcsh/npc_team/serve.jinx +0 -26
- npcsh-1.1.22.data/data/npcsh/npc_team/split_pane.jinx +0 -12
- npcsh-1.1.22.data/data/npcsh/npc_team/switch_npc.jinx +0 -10
- npcsh-1.1.22.data/data/npcsh/npc_team/switch_tab.jinx +0 -10
- npcsh-1.1.22.data/data/npcsh/npc_team/write_file.jinx +0 -11
- npcsh-1.1.22.data/data/npcsh/npc_team/zen_mode.jinx +0 -9
- npcsh-1.1.22.dist-info/RECORD +0 -240
- /npcsh/npc_team/jinxs/{incognide → lib/utils}/incognide.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/alicanto.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/alicanto.png +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/arxiv.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/benchmark.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/browser_action.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/browser_screenshot.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/build.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/chat.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/click.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/cmd.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/compile.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/compress.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/corca.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/corca.png +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/corca_example.png +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/db_search.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/file_search.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/frederic4.png +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/git.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/guac.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/guac.png +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/incognide.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/kadiefa.png +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/key_press.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/kg.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/launch_app.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/load_file.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/memories.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/models.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/nql.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/open_browser.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/ots.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/papers.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/paste.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonk.png +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonkjr.png +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/pti.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/python.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/reattach.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sample.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/screenshot.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/set.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/setup.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/shh.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sibiji.png +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sleep.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/spool.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/spool.png +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sql.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/switch.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/switches.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sync.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/team.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/teamviz.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/trigger.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/type_text.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/usage.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/verbose.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/vixynt.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/wait.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/wander.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/web_search.jinx +0 -0
- {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/yap.png +0 -0
- {npcsh-1.1.22.dist-info → npcsh-1.1.23.dist-info}/WHEEL +0 -0
- {npcsh-1.1.22.dist-info → npcsh-1.1.23.dist-info}/entry_points.txt +0 -0
- {npcsh-1.1.22.dist-info → npcsh-1.1.23.dist-info}/licenses/LICENSE +0 -0
- {npcsh-1.1.22.dist-info → npcsh-1.1.23.dist-info}/top_level.txt +0 -0
|
@@ -7,6 +7,7 @@ inputs:
|
|
|
7
7
|
- tts_model: kokoro
|
|
8
8
|
- voice: af_heart
|
|
9
9
|
- files: null
|
|
10
|
+
- show_setup: false
|
|
10
11
|
|
|
11
12
|
steps:
|
|
12
13
|
- name: yap_tui
|
|
@@ -23,17 +24,17 @@ steps:
|
|
|
23
24
|
import wave
|
|
24
25
|
import numpy as np
|
|
25
26
|
from faster_whisper import WhisperModel
|
|
26
|
-
from gtts import gTTS
|
|
27
27
|
from npcpy.data.audio import (
|
|
28
28
|
FORMAT, CHANNELS, RATE, CHUNK,
|
|
29
29
|
transcribe_recording, convert_mp3_to_wav
|
|
30
30
|
)
|
|
31
|
+
from npcpy.gen.audio_gen import text_to_speech, get_available_engines, get_available_voices
|
|
31
32
|
AUDIO_AVAILABLE = True
|
|
32
33
|
except ImportError:
|
|
33
34
|
AUDIO_AVAILABLE = False
|
|
34
35
|
|
|
35
36
|
from npcpy.llm_funcs import get_llm_response
|
|
36
|
-
from npcpy.npc_sysenv import get_system_message, render_markdown
|
|
37
|
+
from npcpy.npc_sysenv import get_system_message, render_markdown, get_locally_available_models
|
|
37
38
|
from npcpy.data.load import load_file_contents
|
|
38
39
|
from npcpy.data.text import rag_search
|
|
39
40
|
|
|
@@ -42,7 +43,8 @@ steps:
|
|
|
42
43
|
messages = context.get('messages', [])
|
|
43
44
|
files = context.get('files')
|
|
44
45
|
tts_model_name = context.get('tts_model', 'kokoro')
|
|
45
|
-
voice_name = context.get('voice'
|
|
46
|
+
voice_name = context.get('voice') or None
|
|
47
|
+
show_setup = context.get('show_setup', False)
|
|
46
48
|
|
|
47
49
|
if isinstance(npc, str) and team:
|
|
48
50
|
npc = team.get(npc) if hasattr(team, 'get') else None
|
|
@@ -61,6 +63,285 @@ steps:
|
|
|
61
63
|
context['messages'] = messages
|
|
62
64
|
exit()
|
|
63
65
|
|
|
66
|
+
# ================================================================
|
|
67
|
+
# Gather available options for setup/modal
|
|
68
|
+
# ================================================================
|
|
69
|
+
_all_engines = []
|
|
70
|
+
_engine_voices = {}
|
|
71
|
+
try:
|
|
72
|
+
_engines_info = get_available_engines()
|
|
73
|
+
for ename, einfo in _engines_info.items():
|
|
74
|
+
_all_engines.append(ename)
|
|
75
|
+
try:
|
|
76
|
+
_engine_voices[ename] = [v['id'] if isinstance(v, dict) else str(v) for v in get_available_voices(ename)]
|
|
77
|
+
except Exception:
|
|
78
|
+
_engine_voices[ename] = []
|
|
79
|
+
except Exception:
|
|
80
|
+
_all_engines = ['kokoro', 'qwen3', 'elevenlabs', 'openai', 'gemini', 'gtts']
|
|
81
|
+
_engine_voices = {e: [] for e in _all_engines}
|
|
82
|
+
|
|
83
|
+
if not _all_engines:
|
|
84
|
+
_all_engines = ['kokoro']
|
|
85
|
+
|
|
86
|
+
_all_models = []
|
|
87
|
+
_all_providers = []
|
|
88
|
+
try:
|
|
89
|
+
_local = get_locally_available_models(os.getcwd())
|
|
90
|
+
_seen_models = set()
|
|
91
|
+
_seen_providers = set()
|
|
92
|
+
for entry in _local:
|
|
93
|
+
m = entry.get('model', '') if isinstance(entry, dict) else str(entry)
|
|
94
|
+
p = entry.get('provider', '') if isinstance(entry, dict) else ''
|
|
95
|
+
if m and m not in _seen_models:
|
|
96
|
+
_all_models.append(m)
|
|
97
|
+
_seen_models.add(m)
|
|
98
|
+
if p and p not in _seen_providers:
|
|
99
|
+
_all_providers.append(p)
|
|
100
|
+
_seen_providers.add(p)
|
|
101
|
+
except Exception:
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
if not _all_models:
|
|
105
|
+
_all_models = [model or 'gemma3:4b']
|
|
106
|
+
if not _all_providers:
|
|
107
|
+
_all_providers = [provider or 'ollama']
|
|
108
|
+
|
|
109
|
+
# Ensure current selections are in the lists
|
|
110
|
+
if tts_model_name not in _all_engines:
|
|
111
|
+
_all_engines.insert(0, tts_model_name)
|
|
112
|
+
if model and model not in _all_models:
|
|
113
|
+
_all_models.insert(0, model)
|
|
114
|
+
if provider and provider not in _all_providers:
|
|
115
|
+
_all_providers.insert(0, provider)
|
|
116
|
+
|
|
117
|
+
def _voices_for_engine(eng):
|
|
118
|
+
v = _engine_voices.get(eng, [])
|
|
119
|
+
if not v:
|
|
120
|
+
defaults = {'kokoro': ['af_heart'], 'qwen3': ['ryan'], 'elevenlabs': ['rachel'],
|
|
121
|
+
'openai': ['alloy'], 'gemini': ['en-US-Standard-A'], 'gtts': ['en']}
|
|
122
|
+
v = defaults.get(eng, ['default'])
|
|
123
|
+
return v
|
|
124
|
+
|
|
125
|
+
# ================================================================
|
|
126
|
+
# Setup screen
|
|
127
|
+
# ================================================================
|
|
128
|
+
def run_setup():
|
|
129
|
+
nonlocal tts_model_name, voice_name, model, provider
|
|
130
|
+
|
|
131
|
+
TURQ = '\033[38;2;64;224;208m'
|
|
132
|
+
PURPLE = '\033[38;2;180;130;255m'
|
|
133
|
+
ORANGE = '\033[38;2;255;165;0m'
|
|
134
|
+
GREEN = '\033[32m'
|
|
135
|
+
DIM = '\033[90m'
|
|
136
|
+
BOLD = '\033[1m'
|
|
137
|
+
REV = '\033[7m'
|
|
138
|
+
RST = '\033[0m'
|
|
139
|
+
|
|
140
|
+
def _sz():
|
|
141
|
+
try:
|
|
142
|
+
s = os.get_terminal_size()
|
|
143
|
+
return s.columns, s.lines
|
|
144
|
+
except:
|
|
145
|
+
return 80, 24
|
|
146
|
+
|
|
147
|
+
# Setup state
|
|
148
|
+
fields = ['model', 'provider', 'engine', 'voice']
|
|
149
|
+
field_labels = {'model': 'LLM Model', 'provider': 'LLM Provider',
|
|
150
|
+
'engine': 'TTS Engine', 'voice': 'Voice'}
|
|
151
|
+
|
|
152
|
+
model_idx = _all_models.index(model) if model in _all_models else 0
|
|
153
|
+
provider_idx = _all_providers.index(provider) if provider in _all_providers else 0
|
|
154
|
+
engine_idx = _all_engines.index(tts_model_name) if tts_model_name in _all_engines else 0
|
|
155
|
+
cur_voices = _voices_for_engine(_all_engines[engine_idx])
|
|
156
|
+
voice_idx = 0
|
|
157
|
+
if voice_name and voice_name in cur_voices:
|
|
158
|
+
voice_idx = cur_voices.index(voice_name)
|
|
159
|
+
|
|
160
|
+
field_options = {
|
|
161
|
+
'model': (_all_models, model_idx),
|
|
162
|
+
'provider': (_all_providers, provider_idx),
|
|
163
|
+
'engine': (_all_engines, engine_idx),
|
|
164
|
+
'voice': (cur_voices, voice_idx),
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
sel = 0 # 0-3 = fields, 4 = save default, 5 = dont show again
|
|
168
|
+
save_default = True
|
|
169
|
+
dont_show = False
|
|
170
|
+
total_rows = 6 # 4 fields + 2 checkboxes
|
|
171
|
+
|
|
172
|
+
def _get_val(f):
|
|
173
|
+
opts, idx = field_options[f]
|
|
174
|
+
return opts[idx] if opts else '?'
|
|
175
|
+
|
|
176
|
+
def _render_setup():
|
|
177
|
+
w, h = _sz()
|
|
178
|
+
box_w = min(50, w - 4)
|
|
179
|
+
box_h = 18
|
|
180
|
+
sx = max(1, (w - box_w) // 2)
|
|
181
|
+
sy = max(1, (h - box_h) // 2)
|
|
182
|
+
|
|
183
|
+
buf = ['\033[2J\033[H']
|
|
184
|
+
|
|
185
|
+
# Box border
|
|
186
|
+
buf.append(f'\033[{sy};{sx}H{PURPLE}{"─" * box_w}{RST}')
|
|
187
|
+
title = " YAP Voice Chat Setup "
|
|
188
|
+
tp = sx + (box_w - len(title)) // 2
|
|
189
|
+
buf.append(f'\033[{sy};{tp}H{BOLD}{PURPLE}{title}{RST}')
|
|
190
|
+
|
|
191
|
+
y = sy + 2
|
|
192
|
+
for i, f in enumerate(fields):
|
|
193
|
+
opts, idx = field_options[f]
|
|
194
|
+
val = opts[idx] if opts else '?'
|
|
195
|
+
label = field_labels[f]
|
|
196
|
+
lpad = sx + 2
|
|
197
|
+
|
|
198
|
+
if i == sel:
|
|
199
|
+
arrow_l = f'{TURQ}\u25c4{RST}'
|
|
200
|
+
arrow_r = f'{TURQ}\u25ba{RST}'
|
|
201
|
+
buf.append(f'\033[{y};{lpad}H{REV} {label}: {RST} {arrow_l} {BOLD}{val}{RST} {arrow_r}')
|
|
202
|
+
else:
|
|
203
|
+
buf.append(f'\033[{y};{lpad}H {DIM}{label}:{RST} {val}')
|
|
204
|
+
y += 2
|
|
205
|
+
|
|
206
|
+
# Separator
|
|
207
|
+
buf.append(f'\033[{y};{sx + 2}H{DIM}{"─" * (box_w - 4)}{RST}')
|
|
208
|
+
y += 1
|
|
209
|
+
|
|
210
|
+
# Checkboxes
|
|
211
|
+
ck_save = f'{GREEN}[x]{RST}' if save_default else '[ ]'
|
|
212
|
+
ck_dont = f'{GREEN}[x]{RST}' if dont_show else '[ ]'
|
|
213
|
+
|
|
214
|
+
if sel == 4:
|
|
215
|
+
buf.append(f'\033[{y};{sx + 2}H{REV} {ck_save} Save as default {RST}')
|
|
216
|
+
else:
|
|
217
|
+
buf.append(f'\033[{y};{sx + 2}H {ck_save} Save as default')
|
|
218
|
+
y += 1
|
|
219
|
+
|
|
220
|
+
if sel == 5:
|
|
221
|
+
buf.append(f'\033[{y};{sx + 2}H{REV} {ck_dont} Don\'t show again {RST}')
|
|
222
|
+
else:
|
|
223
|
+
buf.append(f'\033[{y};{sx + 2}H {ck_dont} Don\'t show again')
|
|
224
|
+
y += 2
|
|
225
|
+
|
|
226
|
+
# Hints
|
|
227
|
+
buf.append(f'\033[{y};{sx + 2}H{DIM}\u2191/\u2193:Navigate \u2190/\u2192:Change Space:Toggle Enter:Start Ctrl+Q:Quit{RST}')
|
|
228
|
+
y += 1
|
|
229
|
+
buf.append(f'\033[{y};{sx}H{PURPLE}{"─" * box_w}{RST}')
|
|
230
|
+
|
|
231
|
+
sys.stdout.write(''.join(buf))
|
|
232
|
+
sys.stdout.flush()
|
|
233
|
+
|
|
234
|
+
fd = sys.stdin.fileno()
|
|
235
|
+
old = termios.tcgetattr(fd)
|
|
236
|
+
try:
|
|
237
|
+
tty.setcbreak(fd)
|
|
238
|
+
sys.stdout.write('\033[?25l')
|
|
239
|
+
running = True
|
|
240
|
+
while running:
|
|
241
|
+
_render_setup()
|
|
242
|
+
if _sel.select([fd], [], [], 0.1)[0]:
|
|
243
|
+
c = os.read(fd, 1).decode('latin-1')
|
|
244
|
+
if c == '\x11' or c == '\x03': # Ctrl+Q / Ctrl+C
|
|
245
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old)
|
|
246
|
+
sys.stdout.write('\033[?25h\033[2J\033[H')
|
|
247
|
+
sys.stdout.flush()
|
|
248
|
+
context['output'] = "Setup cancelled."
|
|
249
|
+
context['messages'] = messages
|
|
250
|
+
exit()
|
|
251
|
+
|
|
252
|
+
elif c == '\x1b': # Escape sequence
|
|
253
|
+
if _sel.select([fd], [], [], 0.05)[0]:
|
|
254
|
+
c2 = os.read(fd, 1).decode('latin-1')
|
|
255
|
+
if c2 == '[':
|
|
256
|
+
c3 = os.read(fd, 1).decode('latin-1')
|
|
257
|
+
if c3 == 'A': # Up
|
|
258
|
+
sel = max(0, sel - 1)
|
|
259
|
+
elif c3 == 'B': # Down
|
|
260
|
+
sel = min(total_rows - 1, sel + 1)
|
|
261
|
+
elif c3 == 'D': # Left
|
|
262
|
+
if sel < 4:
|
|
263
|
+
f = fields[sel]
|
|
264
|
+
opts, idx = field_options[f]
|
|
265
|
+
if opts:
|
|
266
|
+
new_idx = (idx - 1) % len(opts)
|
|
267
|
+
field_options[f] = (opts, new_idx)
|
|
268
|
+
if f == 'engine':
|
|
269
|
+
nv = _voices_for_engine(opts[new_idx])
|
|
270
|
+
field_options['voice'] = (nv, 0)
|
|
271
|
+
elif c3 == 'C': # Right
|
|
272
|
+
if sel < 4:
|
|
273
|
+
f = fields[sel]
|
|
274
|
+
opts, idx = field_options[f]
|
|
275
|
+
if opts:
|
|
276
|
+
new_idx = (idx + 1) % len(opts)
|
|
277
|
+
field_options[f] = (opts, new_idx)
|
|
278
|
+
if f == 'engine':
|
|
279
|
+
nv = _voices_for_engine(opts[new_idx])
|
|
280
|
+
field_options['voice'] = (nv, 0)
|
|
281
|
+
|
|
282
|
+
elif c == 'k':
|
|
283
|
+
sel = max(0, sel - 1)
|
|
284
|
+
elif c == 'j':
|
|
285
|
+
sel = min(total_rows - 1, sel + 1)
|
|
286
|
+
elif c == 'h':
|
|
287
|
+
if sel < 4:
|
|
288
|
+
f = fields[sel]
|
|
289
|
+
opts, idx = field_options[f]
|
|
290
|
+
if opts:
|
|
291
|
+
new_idx = (idx - 1) % len(opts)
|
|
292
|
+
field_options[f] = (opts, new_idx)
|
|
293
|
+
if f == 'engine':
|
|
294
|
+
nv = _voices_for_engine(opts[new_idx])
|
|
295
|
+
field_options['voice'] = (nv, 0)
|
|
296
|
+
elif c == 'l':
|
|
297
|
+
if sel < 4:
|
|
298
|
+
f = fields[sel]
|
|
299
|
+
opts, idx = field_options[f]
|
|
300
|
+
if opts:
|
|
301
|
+
new_idx = (idx + 1) % len(opts)
|
|
302
|
+
field_options[f] = (opts, new_idx)
|
|
303
|
+
if f == 'engine':
|
|
304
|
+
nv = _voices_for_engine(opts[new_idx])
|
|
305
|
+
field_options['voice'] = (nv, 0)
|
|
306
|
+
elif c == ' ':
|
|
307
|
+
if sel == 4:
|
|
308
|
+
save_default = not save_default
|
|
309
|
+
elif sel == 5:
|
|
310
|
+
dont_show = not dont_show
|
|
311
|
+
elif c in ('\r', '\n'):
|
|
312
|
+
running = False
|
|
313
|
+
|
|
314
|
+
finally:
|
|
315
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old)
|
|
316
|
+
sys.stdout.write('\033[?25h\033[2J\033[H')
|
|
317
|
+
sys.stdout.flush()
|
|
318
|
+
|
|
319
|
+
# Apply selections
|
|
320
|
+
model = _get_val('model')
|
|
321
|
+
provider = _get_val('provider')
|
|
322
|
+
tts_model_name = _get_val('engine')
|
|
323
|
+
voice_name = _get_val('voice')
|
|
324
|
+
|
|
325
|
+
# Persist if requested
|
|
326
|
+
if save_default or dont_show:
|
|
327
|
+
from npcsh.config import set_npcsh_config_value
|
|
328
|
+
if save_default:
|
|
329
|
+
set_npcsh_config_value("NPCSH_TTS_ENGINE", tts_model_name)
|
|
330
|
+
set_npcsh_config_value("NPCSH_TTS_VOICE", voice_name)
|
|
331
|
+
set_npcsh_config_value("NPCSH_CHAT_MODEL", model)
|
|
332
|
+
set_npcsh_config_value("NPCSH_CHAT_PROVIDER", provider)
|
|
333
|
+
if dont_show:
|
|
334
|
+
set_npcsh_config_value("NPCSH_YAP_SETUP_DONE", "1")
|
|
335
|
+
|
|
336
|
+
if show_setup:
|
|
337
|
+
run_setup()
|
|
338
|
+
|
|
339
|
+
# Set default voice if still None
|
|
340
|
+
if not voice_name:
|
|
341
|
+
defaults = {'kokoro': 'af_heart', 'qwen3': 'ryan', 'elevenlabs': 'rachel',
|
|
342
|
+
'openai': 'alloy', 'gemini': 'en-US-Standard-A', 'gtts': 'en'}
|
|
343
|
+
voice_name = defaults.get(tts_model_name, 'default')
|
|
344
|
+
|
|
64
345
|
# ================================================================
|
|
65
346
|
# Audio models
|
|
66
347
|
# ================================================================
|
|
@@ -138,8 +419,28 @@ steps:
|
|
|
138
419
|
edit_buf = ""
|
|
139
420
|
edit_key = ""
|
|
140
421
|
|
|
422
|
+
# Modal state
|
|
423
|
+
modal_open = False
|
|
424
|
+
modal_sel = 0
|
|
425
|
+
modal_engine_idx = 0
|
|
426
|
+
modal_voice_idx = 0
|
|
427
|
+
modal_model_idx = 0
|
|
428
|
+
modal_provider_idx = 0
|
|
429
|
+
modal_save = False
|
|
430
|
+
|
|
141
431
|
ui = UI()
|
|
142
432
|
|
|
433
|
+
# Initialize modal indices to current selections
|
|
434
|
+
if tts_model_name in _all_engines:
|
|
435
|
+
ui.modal_engine_idx = _all_engines.index(tts_model_name)
|
|
436
|
+
if model and model in _all_models:
|
|
437
|
+
ui.modal_model_idx = _all_models.index(model)
|
|
438
|
+
if provider and provider in _all_providers:
|
|
439
|
+
ui.modal_provider_idx = _all_providers.index(provider)
|
|
440
|
+
cur_v = _voices_for_engine(tts_model_name)
|
|
441
|
+
if voice_name in cur_v:
|
|
442
|
+
ui.modal_voice_idx = cur_v.index(voice_name)
|
|
443
|
+
|
|
143
444
|
# ================================================================
|
|
144
445
|
# Helpers
|
|
145
446
|
# ================================================================
|
|
@@ -159,7 +460,7 @@ steps:
|
|
|
159
460
|
REV = '\033[7m'
|
|
160
461
|
RST = '\033[0m'
|
|
161
462
|
RED = '\033[31m'
|
|
162
|
-
SPINNERS = ['
|
|
463
|
+
SPINNERS = ['\u280b', '\u2819', '\u2839', '\u2838', '\u283c', '\u2834', '\u2826', '\u2827', '\u2807', '\u280f']
|
|
163
464
|
|
|
164
465
|
def wrap_text(text, width):
|
|
165
466
|
lines = []
|
|
@@ -191,20 +492,35 @@ steps:
|
|
|
191
492
|
return
|
|
192
493
|
try:
|
|
193
494
|
ui.speaking = True
|
|
194
|
-
tts = gTTS(text=text, lang='en')
|
|
195
|
-
mp3_f = tempfile.NamedTemporaryFile(suffix='.mp3', delete=False)
|
|
196
|
-
mp3_path = mp3_f.name
|
|
197
|
-
mp3_f.close()
|
|
198
|
-
tts.save(mp3_path)
|
|
199
|
-
wav_path = mp3_path.replace('.mp3', '.wav')
|
|
200
|
-
convert_mp3_to_wav(mp3_path, wav_path)
|
|
201
495
|
import subprocess
|
|
496
|
+
|
|
497
|
+
audio_bytes = text_to_speech(text, engine=tts_model_name, voice=voice_name)
|
|
498
|
+
|
|
499
|
+
# Determine file format from engine
|
|
500
|
+
if tts_model_name in ('elevenlabs', 'gtts'):
|
|
501
|
+
suffix = '.mp3'
|
|
502
|
+
else:
|
|
503
|
+
suffix = '.wav'
|
|
504
|
+
|
|
505
|
+
tmp = tempfile.NamedTemporaryFile(suffix=suffix, delete=False)
|
|
506
|
+
tmp_path = tmp.name
|
|
507
|
+
tmp.write(audio_bytes)
|
|
508
|
+
tmp.close()
|
|
509
|
+
|
|
510
|
+
# If mp3, convert to wav for playback
|
|
511
|
+
play_path = tmp_path
|
|
512
|
+
if suffix == '.mp3':
|
|
513
|
+
wav_path = tmp_path.replace('.mp3', '.wav')
|
|
514
|
+
convert_mp3_to_wav(tmp_path, wav_path)
|
|
515
|
+
play_path = wav_path
|
|
516
|
+
|
|
202
517
|
if sys.platform == 'darwin':
|
|
203
|
-
subprocess.run(['afplay',
|
|
518
|
+
subprocess.run(['afplay', play_path], check=True, timeout=60)
|
|
204
519
|
elif sys.platform == 'linux':
|
|
205
|
-
subprocess.run(['aplay',
|
|
520
|
+
subprocess.run(['aplay', play_path], check=True, timeout=60,
|
|
206
521
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
207
|
-
|
|
522
|
+
|
|
523
|
+
for _p in set([tmp_path, play_path]):
|
|
208
524
|
try: os.remove(_p)
|
|
209
525
|
except: pass
|
|
210
526
|
except Exception as e:
|
|
@@ -387,18 +703,18 @@ steps:
|
|
|
387
703
|
|
|
388
704
|
mic = ''
|
|
389
705
|
if ui.recording:
|
|
390
|
-
mic = f'{RED}
|
|
706
|
+
mic = f'{RED}\u25cf REC {ui.rec_seconds:.1f}s{RST}'
|
|
391
707
|
elif ui.transcribing:
|
|
392
|
-
mic = f'{ORANGE}
|
|
708
|
+
mic = f'{ORANGE}\u25cf transcribing...{RST}'
|
|
393
709
|
elif ui.speaking:
|
|
394
|
-
mic = f'{GREEN}
|
|
710
|
+
mic = f'{GREEN}\u25cf speaking...{RST}'
|
|
395
711
|
elif ui.thinking:
|
|
396
712
|
sp = SPINNERS[ui.spinner_frame % len(SPINNERS)]
|
|
397
713
|
mic = f'{ORANGE}{sp} thinking...{RST}'
|
|
398
714
|
elif ui.listening:
|
|
399
|
-
mic = f'{TURQ}
|
|
715
|
+
mic = f'{TURQ}\u25cf listening{RST}'
|
|
400
716
|
|
|
401
|
-
audio_st = '
|
|
717
|
+
audio_st = '\U0001f3a4' if ui.listening else ('\U0001f507' if not AUDIO_AVAILABLE else '\u23f8')
|
|
402
718
|
right = f'{npc_name} | {audio_st} | {model or "?"}@{provider or "?"}'
|
|
403
719
|
pad = w - 12 - len(right) - 20
|
|
404
720
|
header = f'{PURPLE}YAP{RST} {tabs}{" " * max(0, pad)}{mic} {DIM}{right}{RST}'
|
|
@@ -409,6 +725,10 @@ steps:
|
|
|
409
725
|
elif ui.tab == 1:
|
|
410
726
|
render_settings(buf, w, h)
|
|
411
727
|
|
|
728
|
+
# Draw modal on top if open
|
|
729
|
+
if ui.modal_open:
|
|
730
|
+
render_modal(buf, w, h)
|
|
731
|
+
|
|
412
732
|
sys.stdout.write(''.join(buf))
|
|
413
733
|
sys.stdout.flush()
|
|
414
734
|
|
|
@@ -437,18 +757,18 @@ steps:
|
|
|
437
757
|
tw = w - 5
|
|
438
758
|
wrapped = wrap_text(text, tw)
|
|
439
759
|
for i, l in enumerate(wrapped):
|
|
440
|
-
prefix = f' {TURQ}
|
|
760
|
+
prefix = f' {TURQ}\u2139 ' if i == 0 else ' '
|
|
441
761
|
all_lines.append(f'{prefix}{l}{RST}' if i == 0 else f' {l}')
|
|
442
762
|
elif role == 'error':
|
|
443
763
|
tw = w - 5
|
|
444
764
|
wrapped = wrap_text(text, tw)
|
|
445
765
|
for i, l in enumerate(wrapped):
|
|
446
|
-
prefix = f' {RED}
|
|
766
|
+
prefix = f' {RED}\u2717 ' if i == 0 else ' '
|
|
447
767
|
all_lines.append(f'{prefix}{l}{RST}' if i == 0 else f' {l}')
|
|
448
768
|
|
|
449
769
|
if ui.recording:
|
|
450
770
|
secs = ui.rec_seconds
|
|
451
|
-
all_lines.append(f' {RED}
|
|
771
|
+
all_lines.append(f' {RED}\U0001f399 Recording... {secs:.1f}s{RST}')
|
|
452
772
|
elif ui.transcribing:
|
|
453
773
|
sp = SPINNERS[ui.spinner_frame % len(SPINNERS)]
|
|
454
774
|
all_lines.append(f' {ORANGE}{sp} Transcribing...{RST}')
|
|
@@ -456,7 +776,7 @@ steps:
|
|
|
456
776
|
sp = SPINNERS[ui.spinner_frame % len(SPINNERS)]
|
|
457
777
|
all_lines.append(f' {ORANGE}{sp} thinking...{RST}')
|
|
458
778
|
elif ui.speaking:
|
|
459
|
-
all_lines.append(f' {GREEN}
|
|
779
|
+
all_lines.append(f' {GREEN}\U0001f50a Speaking...{RST}')
|
|
460
780
|
|
|
461
781
|
# Scrolling
|
|
462
782
|
if ui.chat_scroll == -1:
|
|
@@ -473,7 +793,7 @@ steps:
|
|
|
473
793
|
|
|
474
794
|
# Input area
|
|
475
795
|
div_y = 2 + chat_h
|
|
476
|
-
buf.append(f'\033[{div_y};1H\033[K{DIM}{"
|
|
796
|
+
buf.append(f'\033[{div_y};1H\033[K{DIM}{"\u2500" * w}{RST}')
|
|
477
797
|
input_y = div_y + 1
|
|
478
798
|
visible = ui.input_buf[-(w-4):] if len(ui.input_buf) > w - 4 else ui.input_buf
|
|
479
799
|
buf.append(f'\033[{input_y};1H\033[K {BOLD}>{RST} {visible}\033[?25h')
|
|
@@ -481,9 +801,9 @@ steps:
|
|
|
481
801
|
# Status bar
|
|
482
802
|
if AUDIO_AVAILABLE:
|
|
483
803
|
ltog = 'Ctrl+L:Pause' if ui.listening else 'Ctrl+L:Listen'
|
|
484
|
-
hints = f'Enter:Send {ltog}
|
|
804
|
+
hints = f'Enter:Send {ltog} Ctrl+S:Settings Tab:Settings Ctrl+Q:Quit'
|
|
485
805
|
else:
|
|
486
|
-
hints = 'Enter:Send
|
|
806
|
+
hints = 'Enter:Send Ctrl+S:Settings Tab:Settings Ctrl+Q:Quit'
|
|
487
807
|
buf.append(f'\033[{h};1H\033[K{REV} {hints[:w-2].ljust(w-2)} {RST}')
|
|
488
808
|
|
|
489
809
|
def render_settings(buf, w, h):
|
|
@@ -496,7 +816,7 @@ steps:
|
|
|
496
816
|
]
|
|
497
817
|
|
|
498
818
|
buf.append(f'\033[3;3H{BOLD}Voice Settings{RST}')
|
|
499
|
-
buf.append(f'\033[4;3H{DIM}{"
|
|
819
|
+
buf.append(f'\033[4;3H{DIM}{"\u2500" * (w - 6)}{RST}')
|
|
500
820
|
|
|
501
821
|
y = 6
|
|
502
822
|
for i, (key, label, val) in enumerate(settings):
|
|
@@ -508,6 +828,10 @@ steps:
|
|
|
508
828
|
buf.append(f'\033[{y};3H {BOLD}{label}:{RST} {val}')
|
|
509
829
|
y += 2
|
|
510
830
|
|
|
831
|
+
y += 1
|
|
832
|
+
buf.append(f'\033[{y};3H{DIM}TTS Engine: {tts_model_name} Voice: {voice_name or "default"}{RST}')
|
|
833
|
+
y += 1
|
|
834
|
+
buf.append(f'\033[{y};3H{DIM}LLM: {model or "?"}@{provider or "?"}{RST}')
|
|
511
835
|
y += 1
|
|
512
836
|
buf.append(f'\033[{y};3H{DIM}Audio: {"Available" if AUDIO_AVAILABLE else "Not available"}{RST}')
|
|
513
837
|
y += 1
|
|
@@ -522,12 +846,158 @@ steps:
|
|
|
522
846
|
if ui.editing:
|
|
523
847
|
buf.append(f'\033[{h};1H\033[K{REV} Enter:Save Esc:Cancel {RST}')
|
|
524
848
|
else:
|
|
525
|
-
buf.append(f'\033[{h};1H\033[K{REV} j/k:Navigate Space:Toggle e:Edit Tab:Chat Ctrl+Q:Quit {RST}')
|
|
849
|
+
buf.append(f'\033[{h};1H\033[K{REV} j/k:Navigate Space:Toggle e:Edit Ctrl+S:Quick Switch Tab:Chat Ctrl+Q:Quit {RST}')
|
|
850
|
+
|
|
851
|
+
# ================================================================
|
|
852
|
+
# Modal rendering and handling
|
|
853
|
+
# ================================================================
|
|
854
|
+
def render_modal(buf, w, h):
|
|
855
|
+
box_w = min(48, w - 4)
|
|
856
|
+
box_h = 16
|
|
857
|
+
sx = max(1, (w - box_w) // 2)
|
|
858
|
+
sy = max(1, (h - box_h) // 2)
|
|
859
|
+
|
|
860
|
+
# Clear box area
|
|
861
|
+
for y in range(sy, sy + box_h):
|
|
862
|
+
buf.append(f'\033[{y};{sx}H{" " * box_w}')
|
|
863
|
+
|
|
864
|
+
# Border
|
|
865
|
+
buf.append(f'\033[{sy};{sx}H{PURPLE}\u250c{"\u2500" * (box_w - 2)}\u2510{RST}')
|
|
866
|
+
for y in range(sy + 1, sy + box_h - 1):
|
|
867
|
+
buf.append(f'\033[{y};{sx}H{PURPLE}\u2502{RST}{" " * (box_w - 2)}{PURPLE}\u2502{RST}')
|
|
868
|
+
buf.append(f'\033[{sy + box_h - 1};{sx}H{PURPLE}\u2514{"\u2500" * (box_w - 2)}\u2518{RST}')
|
|
869
|
+
|
|
870
|
+
# Title
|
|
871
|
+
title = " Quick Settings "
|
|
872
|
+
tp = sx + (box_w - len(title)) // 2
|
|
873
|
+
buf.append(f'\033[{sy};{tp}H{BOLD}{PURPLE}{title}{RST}')
|
|
874
|
+
|
|
875
|
+
lpad = sx + 3
|
|
876
|
+
y = sy + 2
|
|
877
|
+
|
|
878
|
+
modal_fields = [
|
|
879
|
+
('LLM Model', _all_models, ui.modal_model_idx),
|
|
880
|
+
('Provider', _all_providers, ui.modal_provider_idx),
|
|
881
|
+
('TTS Engine', _all_engines, ui.modal_engine_idx),
|
|
882
|
+
('Voice', _voices_for_engine(_all_engines[ui.modal_engine_idx]), ui.modal_voice_idx),
|
|
883
|
+
]
|
|
884
|
+
|
|
885
|
+
for i, (label, opts, idx) in enumerate(modal_fields):
|
|
886
|
+
val = opts[idx] if idx < len(opts) else '?'
|
|
887
|
+
if i == ui.modal_sel:
|
|
888
|
+
al = f'{TURQ}\u25c4{RST}'
|
|
889
|
+
ar = f'{TURQ}\u25ba{RST}'
|
|
890
|
+
buf.append(f'\033[{y};{lpad}H{REV} {label}: {RST} {al} {BOLD}{val}{RST} {ar}')
|
|
891
|
+
else:
|
|
892
|
+
buf.append(f'\033[{y};{lpad}H {DIM}{label}:{RST} {val}')
|
|
893
|
+
y += 2
|
|
894
|
+
|
|
895
|
+
# Save checkbox
|
|
896
|
+
ck = f'{GREEN}[x]{RST}' if ui.modal_save else '[ ]'
|
|
897
|
+
if ui.modal_sel == 4:
|
|
898
|
+
buf.append(f'\033[{y};{lpad}H{REV} {ck} Save as default {RST}')
|
|
899
|
+
else:
|
|
900
|
+
buf.append(f'\033[{y};{lpad}H {ck} Save as default')
|
|
901
|
+
y += 2
|
|
902
|
+
|
|
903
|
+
# Hints
|
|
904
|
+
buf.append(f'\033[{y};{lpad}H{DIM}\u2191\u2193:Nav \u2190\u2192:Change Spc:Toggle Enter:Apply Esc:Cancel{RST}')
|
|
905
|
+
|
|
906
|
+
def open_modal():
|
|
907
|
+
ui.modal_open = True
|
|
908
|
+
ui.modal_sel = 0
|
|
909
|
+
ui.modal_save = False
|
|
910
|
+
# Sync indices to current values
|
|
911
|
+
if tts_model_name in _all_engines:
|
|
912
|
+
ui.modal_engine_idx = _all_engines.index(tts_model_name)
|
|
913
|
+
if model and model in _all_models:
|
|
914
|
+
ui.modal_model_idx = _all_models.index(model)
|
|
915
|
+
if provider and provider in _all_providers:
|
|
916
|
+
ui.modal_provider_idx = _all_providers.index(provider)
|
|
917
|
+
cur_v = _voices_for_engine(_all_engines[ui.modal_engine_idx])
|
|
918
|
+
if voice_name in cur_v:
|
|
919
|
+
ui.modal_voice_idx = cur_v.index(voice_name)
|
|
920
|
+
else:
|
|
921
|
+
ui.modal_voice_idx = 0
|
|
922
|
+
|
|
923
|
+
def apply_modal():
|
|
924
|
+
nonlocal tts_model_name, voice_name, model, provider
|
|
925
|
+
model = _all_models[ui.modal_model_idx] if ui.modal_model_idx < len(_all_models) else model
|
|
926
|
+
provider = _all_providers[ui.modal_provider_idx] if ui.modal_provider_idx < len(_all_providers) else provider
|
|
927
|
+
tts_model_name = _all_engines[ui.modal_engine_idx] if ui.modal_engine_idx < len(_all_engines) else tts_model_name
|
|
928
|
+
cur_v = _voices_for_engine(tts_model_name)
|
|
929
|
+
voice_name = cur_v[ui.modal_voice_idx] if ui.modal_voice_idx < len(cur_v) else voice_name
|
|
930
|
+
|
|
931
|
+
ui.chat_log.append(('info', f'Settings: {model}@{provider}, TTS: {tts_model_name}/{voice_name}'))
|
|
932
|
+
|
|
933
|
+
if ui.modal_save:
|
|
934
|
+
from npcsh.config import set_npcsh_config_value
|
|
935
|
+
set_npcsh_config_value("NPCSH_TTS_ENGINE", tts_model_name)
|
|
936
|
+
set_npcsh_config_value("NPCSH_TTS_VOICE", voice_name)
|
|
937
|
+
set_npcsh_config_value("NPCSH_CHAT_MODEL", model)
|
|
938
|
+
set_npcsh_config_value("NPCSH_CHAT_PROVIDER", provider)
|
|
939
|
+
ui.chat_log.append(('info', 'Saved as defaults.'))
|
|
940
|
+
|
|
941
|
+
ui.modal_open = False
|
|
942
|
+
|
|
943
|
+
def handle_modal_key(c, fd):
|
|
944
|
+
if c == '\x1b': # Esc
|
|
945
|
+
if _sel.select([fd], [], [], 0.05)[0]:
|
|
946
|
+
c2 = os.read(fd, 1).decode('latin-1')
|
|
947
|
+
if c2 == '[':
|
|
948
|
+
c3 = os.read(fd, 1).decode('latin-1')
|
|
949
|
+
if c3 == 'A': # Up
|
|
950
|
+
ui.modal_sel = max(0, ui.modal_sel - 1)
|
|
951
|
+
elif c3 == 'B': # Down
|
|
952
|
+
ui.modal_sel = min(4, ui.modal_sel + 1)
|
|
953
|
+
elif c3 == 'D': # Left
|
|
954
|
+
_modal_cycle(-1)
|
|
955
|
+
elif c3 == 'C': # Right
|
|
956
|
+
_modal_cycle(1)
|
|
957
|
+
return True
|
|
958
|
+
# bare Esc = close
|
|
959
|
+
ui.modal_open = False
|
|
960
|
+
return True
|
|
961
|
+
|
|
962
|
+
if c == 'k':
|
|
963
|
+
ui.modal_sel = max(0, ui.modal_sel - 1)
|
|
964
|
+
elif c == 'j':
|
|
965
|
+
ui.modal_sel = min(4, ui.modal_sel + 1)
|
|
966
|
+
elif c == 'h':
|
|
967
|
+
_modal_cycle(-1)
|
|
968
|
+
elif c == 'l':
|
|
969
|
+
_modal_cycle(1)
|
|
970
|
+
elif c == ' ':
|
|
971
|
+
if ui.modal_sel == 4:
|
|
972
|
+
ui.modal_save = not ui.modal_save
|
|
973
|
+
elif c in ('\r', '\n'):
|
|
974
|
+
apply_modal()
|
|
975
|
+
elif c == '\x11': # Ctrl+Q in modal just closes it
|
|
976
|
+
ui.modal_open = False
|
|
977
|
+
return True
|
|
978
|
+
|
|
979
|
+
def _modal_cycle(direction):
|
|
980
|
+
if ui.modal_sel == 0:
|
|
981
|
+
ui.modal_model_idx = (ui.modal_model_idx + direction) % len(_all_models)
|
|
982
|
+
elif ui.modal_sel == 1:
|
|
983
|
+
ui.modal_provider_idx = (ui.modal_provider_idx + direction) % len(_all_providers)
|
|
984
|
+
elif ui.modal_sel == 2:
|
|
985
|
+
ui.modal_engine_idx = (ui.modal_engine_idx + direction) % len(_all_engines)
|
|
986
|
+
# Reset voice index when engine changes
|
|
987
|
+
ui.modal_voice_idx = 0
|
|
988
|
+
elif ui.modal_sel == 3:
|
|
989
|
+
cur_v = _voices_for_engine(_all_engines[ui.modal_engine_idx])
|
|
990
|
+
if cur_v:
|
|
991
|
+
ui.modal_voice_idx = (ui.modal_voice_idx + direction) % len(cur_v)
|
|
526
992
|
|
|
527
993
|
# ================================================================
|
|
528
994
|
# Input handling
|
|
529
995
|
# ================================================================
|
|
530
996
|
def handle_key(c, fd):
|
|
997
|
+
# Modal intercepts all keys
|
|
998
|
+
if ui.modal_open:
|
|
999
|
+
return handle_modal_key(c, fd)
|
|
1000
|
+
|
|
531
1001
|
if c == '\t':
|
|
532
1002
|
if not ui.editing:
|
|
533
1003
|
ui.tab = (ui.tab + 1) % 2
|
|
@@ -536,6 +1006,9 @@ steps:
|
|
|
536
1006
|
return False
|
|
537
1007
|
if c == '\x03': # Ctrl+C
|
|
538
1008
|
return True
|
|
1009
|
+
if c == '\x13': # Ctrl+S = open settings modal
|
|
1010
|
+
open_modal()
|
|
1011
|
+
return True
|
|
539
1012
|
|
|
540
1013
|
# Escape sequences
|
|
541
1014
|
if c == '\x1b':
|
|
@@ -676,11 +1149,12 @@ steps:
|
|
|
676
1149
|
# Welcome
|
|
677
1150
|
# ================================================================
|
|
678
1151
|
ui.chat_log.append(('info', f'YAP voice chat. NPC: {npc_name}.'))
|
|
1152
|
+
ui.chat_log.append(('info', f'TTS: {tts_model_name}/{voice_name} LLM: {model or "?"}@{provider or "?"}'))
|
|
679
1153
|
if AUDIO_AVAILABLE:
|
|
680
1154
|
ui.chat_log.append(('info', 'Listening for speech. Just start talking, or type text.'))
|
|
681
|
-
ui.chat_log.append(('info', 'Ctrl+L to pause/resume listening.'))
|
|
1155
|
+
ui.chat_log.append(('info', 'Ctrl+L to pause/resume listening. Ctrl+S to change settings.'))
|
|
682
1156
|
else:
|
|
683
|
-
ui.chat_log.append(('info', 'Audio not available. Text mode only.'))
|
|
1157
|
+
ui.chat_log.append(('info', 'Audio not available. Text mode only. Ctrl+S to change settings.'))
|
|
684
1158
|
if loaded_chunks:
|
|
685
1159
|
ui.chat_log.append(('info', f'{len(loaded_chunks)} files loaded for context.'))
|
|
686
1160
|
|