pipulate 2.57__tar.gz → 2.58__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. {pipulate-2.57/pipulate.egg-info → pipulate-2.58}/PKG-INFO +1 -1
  2. {pipulate-2.57 → pipulate-2.58}/__init__.py +2 -2
  3. {pipulate-2.57 → pipulate-2.58}/apps/010_introduction.py +12 -4
  4. {pipulate-2.57 → pipulate-2.58}/imports/voice_synthesis.py +159 -7
  5. {pipulate-2.57 → pipulate-2.58}/pipulate/core.py +1 -1
  6. {pipulate-2.57 → pipulate-2.58/pipulate.egg-info}/PKG-INFO +1 -1
  7. {pipulate-2.57 → pipulate-2.58}/pyproject.toml +1 -1
  8. {pipulate-2.57 → pipulate-2.58}/server.py +1 -1
  9. {pipulate-2.57 → pipulate-2.58}/tools/connector_tools.py +4 -4
  10. {pipulate-2.57 → pipulate-2.58}/tools/mcp_tools.py +1 -1
  11. {pipulate-2.57 → pipulate-2.58}/LICENSE +0 -0
  12. {pipulate-2.57 → pipulate-2.58}/MANIFEST.in +0 -0
  13. {pipulate-2.57 → pipulate-2.58}/README.md +0 -0
  14. {pipulate-2.57 → pipulate-2.58}/apps/015_config.py +0 -0
  15. {pipulate-2.57 → pipulate-2.58}/apps/020_profiles.py +0 -0
  16. {pipulate-2.57 → pipulate-2.58}/apps/025_aspect.py +0 -0
  17. {pipulate-2.57 → pipulate-2.58}/apps/030_roles.py +0 -0
  18. {pipulate-2.57 → pipulate-2.58}/apps/040_hello_workflow.py +0 -0
  19. {pipulate-2.57 → pipulate-2.58}/apps/050_documentation.py +0 -0
  20. {pipulate-2.57 → pipulate-2.58}/apps/060_tasks.py +0 -0
  21. {pipulate-2.57 → pipulate-2.58}/apps/070_history.py +0 -0
  22. {pipulate-2.57 → pipulate-2.58}/apps/110_parameter_buster.py +0 -0
  23. {pipulate-2.57 → pipulate-2.58}/apps/120_link_graph.py +0 -0
  24. {pipulate-2.57 → pipulate-2.58}/apps/130_gap_analysis.py +0 -0
  25. {pipulate-2.57 → pipulate-2.58}/apps/200_workflow_genesis.py +0 -0
  26. {pipulate-2.57 → pipulate-2.58}/apps/210_widget_examples.py +0 -0
  27. {pipulate-2.57 → pipulate-2.58}/apps/220_roadmap.py +0 -0
  28. {pipulate-2.57 → pipulate-2.58}/apps/230_dev_assistant.py +0 -0
  29. {pipulate-2.57 → pipulate-2.58}/apps/240_simon_mcp.py +0 -0
  30. {pipulate-2.57 → pipulate-2.58}/apps/300_blank_placeholder.py +0 -0
  31. {pipulate-2.57 → pipulate-2.58}/apps/400_botify_trifecta.py +0 -0
  32. {pipulate-2.57 → pipulate-2.58}/apps/440_browser_automation.py +0 -0
  33. {pipulate-2.57 → pipulate-2.58}/apps/450_stream_simulator.py +0 -0
  34. {pipulate-2.57 → pipulate-2.58}/apps/510_text_field.py +0 -0
  35. {pipulate-2.57 → pipulate-2.58}/apps/520_text_area.py +0 -0
  36. {pipulate-2.57 → pipulate-2.58}/apps/530_dropdown.py +0 -0
  37. {pipulate-2.57 → pipulate-2.58}/apps/540_checkboxes.py +0 -0
  38. {pipulate-2.57 → pipulate-2.58}/apps/550_radios.py +0 -0
  39. {pipulate-2.57 → pipulate-2.58}/apps/560_range.py +0 -0
  40. {pipulate-2.57 → pipulate-2.58}/apps/570_switch.py +0 -0
  41. {pipulate-2.57 → pipulate-2.58}/apps/580_upload.py +0 -0
  42. {pipulate-2.57 → pipulate-2.58}/apps/610_markdown.py +0 -0
  43. {pipulate-2.57 → pipulate-2.58}/apps/620_mermaid.py +0 -0
  44. {pipulate-2.57 → pipulate-2.58}/apps/630_prism.py +0 -0
  45. {pipulate-2.57 → pipulate-2.58}/apps/640_javascript.py +0 -0
  46. {pipulate-2.57 → pipulate-2.58}/apps/710_pandas.py +0 -0
  47. {pipulate-2.57 → pipulate-2.58}/apps/720_rich.py +0 -0
  48. {pipulate-2.57 → pipulate-2.58}/apps/730_matplotlib.py +0 -0
  49. {pipulate-2.57 → pipulate-2.58}/apps/810_webbrowser.py +0 -0
  50. {pipulate-2.57 → pipulate-2.58}/apps/820_selenium.py +0 -0
  51. {pipulate-2.57 → pipulate-2.58}/apps/830_pico_slider.py +0 -0
  52. {pipulate-2.57 → pipulate-2.58}/cli.py +0 -0
  53. {pipulate-2.57 → pipulate-2.58}/config.py +0 -0
  54. {pipulate-2.57 → pipulate-2.58}/imports/__init__.py +0 -0
  55. {pipulate-2.57 → pipulate-2.58}/imports/ai_dictdb.py +0 -0
  56. {pipulate-2.57 → pipulate-2.58}/imports/ai_tool_discovery_simple_parser.py +0 -0
  57. {pipulate-2.57 → pipulate-2.58}/imports/append_only_conversation.py +0 -0
  58. {pipulate-2.57 → pipulate-2.58}/imports/ascii_displays.py +0 -0
  59. {pipulate-2.57 → pipulate-2.58}/imports/botify/__init__.py +0 -0
  60. {pipulate-2.57 → pipulate-2.58}/imports/botify/code_generators.py +0 -0
  61. {pipulate-2.57 → pipulate-2.58}/imports/botify/true_schema_discoverer.py +0 -0
  62. {pipulate-2.57 → pipulate-2.58}/imports/botify_code_generation.py +0 -0
  63. {pipulate-2.57 → pipulate-2.58}/imports/crud.py +0 -0
  64. {pipulate-2.57 → pipulate-2.58}/imports/database_safety_wrapper.py +0 -0
  65. {pipulate-2.57 → pipulate-2.58}/imports/dom_processing/__init__.py +0 -0
  66. {pipulate-2.57 → pipulate-2.58}/imports/dom_processing/ai_dom_beautifier.py +0 -0
  67. {pipulate-2.57 → pipulate-2.58}/imports/dom_processing/enhanced_dom_processor.py +0 -0
  68. {pipulate-2.57 → pipulate-2.58}/imports/durable_backup_system.py +0 -0
  69. {pipulate-2.57 → pipulate-2.58}/imports/mcp_orchestrator.py +0 -0
  70. {pipulate-2.57 → pipulate-2.58}/imports/server_logging.py +0 -0
  71. {pipulate-2.57 → pipulate-2.58}/imports/stream_orchestrator.py +0 -0
  72. {pipulate-2.57 → pipulate-2.58}/pipulate/__init__.py +0 -0
  73. {pipulate-2.57 → pipulate-2.58}/pipulate.egg-info/SOURCES.txt +0 -0
  74. {pipulate-2.57 → pipulate-2.58}/pipulate.egg-info/dependency_links.txt +0 -0
  75. {pipulate-2.57 → pipulate-2.58}/pipulate.egg-info/entry_points.txt +0 -0
  76. {pipulate-2.57 → pipulate-2.58}/pipulate.egg-info/requires.txt +0 -0
  77. {pipulate-2.57 → pipulate-2.58}/pipulate.egg-info/top_level.txt +0 -0
  78. {pipulate-2.57 → pipulate-2.58}/setup.cfg +0 -0
  79. {pipulate-2.57 → pipulate-2.58}/tests/test_apply_airlock.py +0 -0
  80. {pipulate-2.57 → pipulate-2.58}/tests/test_mck_rep2.py +0 -0
  81. {pipulate-2.57 → pipulate-2.58}/tools/__init__.py +0 -0
  82. {pipulate-2.57 → pipulate-2.58}/tools/advanced_automation_tools.py +0 -0
  83. {pipulate-2.57 → pipulate-2.58}/tools/botify_tools.py +0 -0
  84. {pipulate-2.57 → pipulate-2.58}/tools/conversation_tools.py +0 -0
  85. {pipulate-2.57 → pipulate-2.58}/tools/dom_tools.py +0 -0
  86. {pipulate-2.57 → pipulate-2.58}/tools/keychain_tools.py +0 -0
  87. {pipulate-2.57 → pipulate-2.58}/tools/llm_optics.py +0 -0
  88. {pipulate-2.57 → pipulate-2.58}/tools/scraper_tools.py +0 -0
  89. {pipulate-2.57 → pipulate-2.58}/tools/system_tools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pipulate
3
- Version: 2.57
3
+ Version: 2.58
4
4
  Summary: The context compiler that helps you prompt well — local-first, Nix-reproducible, replayable. The successor to AI SEO software.
5
5
  Author-email: Mike Levin <pipulate@gmail.com>
6
6
  License-Expression: AGPL-3.0-or-later
@@ -12,7 +12,7 @@ Usage:
12
12
  pipulate
13
13
  """
14
14
 
15
- __version__ = "2.57"
15
+ __version__ = "2.58"
16
16
  # APOSTROPHES RESTORED (2026-08-04). They were stripped as a workaround for
17
17
  # flake.nix's descMatch regex, whose character class excluded ' from the
18
18
  # CAPTURE and truncated the banner to "(So)". That regex was fixed in the same
@@ -23,7 +23,7 @@ __version__ = "2.57"
23
23
  # is not a property of the system; the regex is. Blast radius is one banner:
24
24
  # nothing but flake.nix reads this name -- version_sync.py syncs __version__
25
25
  # and __description__, never this. So'wI' chu' -- "engage the cloaking device."
26
- __version_description__ = "NPvg.org Walk Walking"
26
+ __version_description__ = "NPvg.org 1st 10 Minutes"
27
27
  # SPDX expression, single source of truth, synced into pyproject.toml by
28
28
  # scripts/release/version_sync.py. "-or-later" (not bare AGPL-3.0, which is
29
29
  # deprecated SPDX) because the header below grants "any later version".
@@ -155,10 +155,18 @@ class IntroductionPlugin:
155
155
 
156
156
 
157
157
  from imports.voice_synthesis import chip_voice_system
158
- if chip_voice_system and chip_voice_system.voice_ready:
159
- chip_voice_system.stop_speaking() # 🛑 INTERRUPT: Prevent voice overlapping on Back button
160
- logger.info(f"🎤 Speaking: {step_id}")
161
- asyncio.create_task(asyncio.to_thread(chip_voice_system.speak_text, text))
158
+ if chip_voice_system:
159
+ chip_voice_system.stop_speaking() # 🛑 INTERRUPT: Prevent voice overlapping on Back button
160
+ def _speak():
161
+ # THE FLAG WENT COLD (2026-09-18): the gate moved off the event
162
+ # loop. can_speak() reads the consent card and loads the model
163
+ # on first use, seconds long, so it runs here, in the thread; a
164
+ # declined or unanswered card returns False with no download,
165
+ # and speak_text keeps its own barrier behind this one.
166
+ if chip_voice_system.can_speak():
167
+ logger.info(f"🎤 Speaking: {step_id}")
168
+ chip_voice_system.speak_text(text)
169
+ asyncio.create_task(asyncio.to_thread(_speak))
162
170
 
163
171
  return ""
164
172
 
@@ -12,6 +12,7 @@ import wave
12
12
  import subprocess
13
13
  import tempfile
14
14
  import signal
15
+ import sys
15
16
  from pathlib import Path
16
17
  from typing import Optional, Dict, Any
17
18
  import logging
@@ -34,6 +35,92 @@ except ImportError:
34
35
 
35
36
  logger = logging.getLogger(__name__)
36
37
 
38
+ # ---------------------------------------------------------------------------
39
+ # THE VOICE ASKS BEFORE IT SPEAKS (2026-09-17). Two functions, one file.
40
+ # voice_consent() is the BARRIER: every path to sound reads it, fresh, on every
41
+ # call. The answer comes from PIPULATE_VOICE (an unattended shell declaring
42
+ # intent, the way PIPULATE_BOOT_MENU=0 does) or from ~/.config/pipulate/voice,
43
+ # outside the worktree, so it survives rm -rf on the folder and is asked once
44
+ # per machine rather than once per install.
45
+ # ask_voice_consent() is the CEREMONY: it prints the card and reads one line
46
+ # from /dev/tty. The two are SEPARATE on purpose. flake.nix's door-1 greeting
47
+ # runs as a backgrounded python with stdout on /dev/null while the server holds
48
+ # the foreground on the same terminal; a speaker that ASKED would print the
49
+ # card into /dev/null and take the keystrokes meant for a prompt nobody can
50
+ # see. So the speaker never asks. The rider asks at the start of a walk, and
51
+ # the `voice` word asks again later; both own a terminal.
52
+ # NOTHING IS DOWNLOADED BEFORE A YES: the model loads in ensure_voice(), on
53
+ # the first speak after consent, never at import. Importing this module used
54
+ # to fetch the voice and spend seconds before anyone had asked to speak or
55
+ # been asked whether it may; that was also the import tax the tool roster
56
+ # measured at ~3.8 s.
57
+ # ---------------------------------------------------------------------------
58
+ VOICE_CONSENT_FILE = Path.home() / ".config" / "pipulate" / "voice"
59
+ VOICE_CARD = (
60
+ "\nThe steps can be read aloud.\n\n"
61
+ "The voice is Piper, a small text-to-speech program that runs on this\n"
62
+ "computer; it was made for a Raspberry Pi. It reads sentences a person\n"
63
+ "typed in advance. No AI writes them, and nothing you say or type is\n"
64
+ "sent anywhere. Answering yes downloads the voice once, about 60 MB.\n\n"
65
+ "Read the steps aloud? [y/N] "
66
+ )
67
+
68
+
69
+ def voice_consent() -> str:
70
+ """'yes', 'no' or 'unset': the environment first, then the recorded answer."""
71
+ env = os.environ.get("PIPULATE_VOICE", "").strip().lower()
72
+ if env in ("0", "no", "off", "false"):
73
+ return "no"
74
+ if env in ("1", "yes", "on", "true"):
75
+ return "yes"
76
+ try:
77
+ recorded = VOICE_CONSENT_FILE.read_text(encoding="utf-8").strip().lower()
78
+ except OSError:
79
+ return "unset"
80
+ if recorded in ("yes", "no"):
81
+ return recorded
82
+ return "unset"
83
+
84
+
85
+ def ask_voice_consent(later_hint: str = "voice", force: bool = False) -> str:
86
+ """Ask on /dev/tty and record the answer. Returns 'yes', 'no' or 'unavailable'.
87
+
88
+ Asks only when nothing is recorded, unless force is set (the `voice` word),
89
+ and never when PIPULATE_VOICE decides. Without a terminal to ask on -- a
90
+ pipe, a compile-lane probe, a backgrounded job -- it prints nothing and
91
+ returns 'unavailable', so no prompt can ever block an unattended caller.
92
+ Enter alone means no. A Ctrl+C is not recorded, so the question returns.
93
+ """
94
+ env = os.environ.get("PIPULATE_VOICE", "").strip().lower()
95
+ if env in ("0", "no", "off", "false", "1", "yes", "on", "true"):
96
+ return voice_consent()
97
+ recorded = voice_consent()
98
+ if recorded != "unset" and not force:
99
+ return recorded
100
+ if not sys.stdout.isatty():
101
+ return "unavailable"
102
+ try:
103
+ tty = open("/dev/tty", "r", encoding="utf-8")
104
+ except OSError:
105
+ return "unavailable"
106
+ try:
107
+ print(VOICE_CARD, end="", flush=True)
108
+ answer = tty.readline()
109
+ except (OSError, KeyboardInterrupt):
110
+ print()
111
+ return "no"
112
+ finally:
113
+ tty.close()
114
+ decision = "yes" if answer.strip().lower() in ("y", "yes") else "no"
115
+ try:
116
+ VOICE_CONSENT_FILE.parent.mkdir(parents=True, exist_ok=True)
117
+ VOICE_CONSENT_FILE.write_text(decision + "\n", encoding="utf-8")
118
+ except OSError as exc:
119
+ print(f"(could not record the answer, so it will be asked again: {exc})")
120
+ if decision == "no":
121
+ print(f"Staying quiet. To turn the voice on later, type: {later_hint}")
122
+ return decision
123
+
37
124
  class ChipVoiceSystem:
38
125
  """
39
126
  Voice synthesis system for Chip O'Theseus
@@ -49,9 +136,9 @@ class ChipVoiceSystem:
49
136
  self.current_process = None # Track the running audio process
50
137
  self.last_error = None # Populated by synthesize_and_play on failure
51
138
 
52
- if VOICE_SYNTHESIS_AVAILABLE:
53
- self.setup_voice_model()
54
- else:
139
+ # No download and no model load here: see ensure_voice(). Importing
140
+ # this module must cost nothing and ask nothing.
141
+ if not VOICE_SYNTHESIS_AVAILABLE:
55
142
  logger.warning(f"🎤 Voice synthesis not available: {IMPORT_ERROR}")
56
143
 
57
144
  def setup_voice_model(self):
@@ -96,8 +183,38 @@ class ChipVoiceSystem:
96
183
 
97
184
  except Exception as e:
98
185
  logger.error(f"🎤 Failed to setup voice model: {e}")
186
+ self.last_error = f"voice model setup failed: {e}"
99
187
  self.voice_ready = False
100
188
 
189
+ def ensure_voice(self) -> bool:
190
+ """Load the voice on first use, never at import. Returns readiness.
191
+
192
+ Reached only after voice_consent() read yes, so the download the card
193
+ announces happens after the answer and not before it.
194
+ """
195
+ if self.voice_ready:
196
+ return True
197
+ if not VOICE_SYNTHESIS_AVAILABLE:
198
+ self.last_error = f"voice synthesis not installed: {IMPORT_ERROR}"
199
+ return False
200
+ self.setup_voice_model()
201
+ return self.voice_ready
202
+
203
+ def can_speak(self) -> bool:
204
+ """True only once a human has said yes AND the voice is loaded.
205
+
206
+ THE FLAG WENT COLD (2026-09-17, the census after the lazy load).
207
+ voice_ready read True at import on any machine with the model on disk,
208
+ and four files gate on it BEFORE they ever call speak_text:
209
+ apps/010_introduction.py, pipulate/core.py, server.py and
210
+ tools/mcp_tools.py. With the load deferred to first use the flag reads
211
+ False in every fresh process until something speaks, so none of those
212
+ gates opens. This is the question they meant to ask: consent first,
213
+ then the load, which downloads only after a yes. A status read that
214
+ must never download keeps reading voice_ready, which now means loaded.
215
+ """
216
+ return voice_consent() == "yes" and self.ensure_voice()
217
+
101
218
  def stop_speaking(self):
102
219
  """
103
220
  Silence! Kill the current audio process if it exists.
@@ -129,7 +246,13 @@ class ChipVoiceSystem:
129
246
  Returns:
130
247
  bool: True if successful, False otherwise
131
248
  """
132
- if not self.voice_ready:
249
+ if voice_consent() != "yes":
250
+ # THE BARRIER. Every path to sound passes here, and the answer is
251
+ # read fresh each time, so a no recorded mid-session holds at once.
252
+ self.last_error = "voice not allowed"
253
+ logger.info("🎤 Voice not allowed; nothing spoken")
254
+ return False
255
+ if not self.ensure_voice():
133
256
  logger.warning("🎤 Voice synthesis not ready")
134
257
  return False
135
258
 
@@ -318,10 +441,22 @@ class ChipVoiceSystem:
318
441
  Returns:
319
442
  Dict with success status and details
320
443
  """
321
- if not self.voice_ready:
444
+ consent = voice_consent()
445
+ if consent != "yes":
446
+ # DECLINED IS NOT FAILED. Callers read success=False either way;
447
+ # this key is how they tell a human's answer from a broken player.
448
+ return {
449
+ "success": False,
450
+ "declined": True,
451
+ "consent": consent,
452
+ "text": text,
453
+ "error": "voice declined" if consent == "no"
454
+ else "voice not yet allowed: the voice card has not been answered",
455
+ }
456
+ if not self.ensure_voice():
322
457
  return {
323
458
  "success": False,
324
- "error": "Voice synthesis not available",
459
+ "error": self.last_error or "Voice synthesis not available",
325
460
  "details": "Piper TTS model not loaded"
326
461
  }
327
462
 
@@ -467,7 +602,7 @@ def test_voice_synthesis():
467
602
  # Test basic voice synthesis
468
603
  test_text = "Hello Mike, this is Chip O'Theseus speaking. I can now remember what happened before the server restart!"
469
604
 
470
- if chip_voice_system and chip_voice_system.voice_ready:
605
+ if chip_voice_system and chip_voice_system.can_speak():
471
606
  result = chip_voice_system.speak_text(test_text)
472
607
  if result["success"]:
473
608
  print("✅ Voice synthesis test successful")
@@ -510,6 +645,23 @@ def test_memory_voice_integration():
510
645
  print("❌ No memories found to test")
511
646
  return False
512
647
 
648
+ if __name__ == "__main__" and sys.argv[1:2] == ["ask"]:
649
+ # `voice`, the word the card names: re-ask and record, then speak one
650
+ # sentence on yes so the audio path is heard rather than assumed.
651
+ _env = os.environ.get("PIPULATE_VOICE", "").strip()
652
+ if _env:
653
+ print(f"voice: PIPULATE_VOICE={_env!r} decides; unset it to be asked.")
654
+ raise SystemExit(0)
655
+ _decision = ask_voice_consent(later_hint="voice", force=True)
656
+ if _decision == "unavailable":
657
+ print("voice: no terminal to ask on; run this from a terminal.")
658
+ raise SystemExit(1)
659
+ print(f"voice: {_decision} (recorded in {VOICE_CONSENT_FILE})")
660
+ if _decision == "yes" and chip_voice_system is not None:
661
+ _result = chip_voice_system.speak_text("This is Piper, reading a script. The voice is on.")
662
+ if not _result.get("success"):
663
+ print(f"voice: nothing was heard: {_result.get('error')}")
664
+ raise SystemExit(0)
513
665
  if __name__ == "__main__":
514
666
  # Run tests if script is executed directly
515
667
  print("🎤🧠 Chip O'Theseus Voice System Tests")
@@ -555,7 +555,7 @@ class Pipulate:
555
555
  try:
556
556
  # Import here to avoid circular dependencies
557
557
  from imports.voice_synthesis import chip_voice_system
558
- if chip_voice_system and chip_voice_system.voice_ready:
558
+ if chip_voice_system and chip_voice_system.can_speak():
559
559
  # Acoustic Sanitization (Using the voice_text so it doesn't read URLs!)
560
560
  safe_text = voice_text.replace('\u0329', '')
561
561
  safe_text = safe_text.replace('—', ', ').replace('–', ', ')
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pipulate
3
- Version: 2.57
3
+ Version: 2.58
4
4
  Summary: The context compiler that helps you prompt well — local-first, Nix-reproducible, replayable. The successor to AI SEO software.
5
5
  Author-email: Mike Levin <pipulate@gmail.com>
6
6
  License-Expression: AGPL-3.0-or-later
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "pipulate"
7
7
  # VERSION NOTE: Synced from pipulate/__init__.py.__version__ via python version_sync.py
8
- version = "2.57"
8
+ version = "2.58"
9
9
  authors = [
10
10
  { name="Mike Levin", email="pipulate@gmail.com" },
11
11
  ]
@@ -3156,7 +3156,7 @@ async def test_voice_synthesis_endpoint(request):
3156
3156
  return JSONResponse({"success": False, "error": "Voice synthesis not available"})
3157
3157
 
3158
3158
  # Check if voice system is ready
3159
- if not chip_voice_system.voice_ready:
3159
+ if not chip_voice_system.can_speak():
3160
3160
  logger.error("🎤 Voice system not ready")
3161
3161
  return JSONResponse({"success": False, "error": "Voice system not ready"})
3162
3162
 
@@ -1,5 +1,5 @@
1
1
  """
2
- connector_tools.py -- the registry face of scripts/connectors/*.py: one tool
2
+ connector_tools.py -- the registry face of connectors/*.py: one tool
3
3
  per connector, SAME argument shape, SAME backend, SAME docstring.
4
4
 
5
5
  This file exists for the two-arm experiment (shell verb vs registry tool) and
@@ -33,7 +33,7 @@ from pathlib import Path
33
33
  from tools import auto_tool
34
34
 
35
35
  REPO_ROOT = Path(__file__).resolve().parent.parent
36
- CONNECTORS = REPO_ROOT / "scripts" / "connectors"
36
+ CONNECTORS = REPO_ROOT / "connectors"
37
37
  TIMEOUT = 120
38
38
  KEY_TRAILER = (
39
39
  "\n\nRegistry form: --json-args keys mirror the connector's argument names --"
@@ -52,7 +52,7 @@ def _connector_doc(name):
52
52
 
53
53
 
54
54
  async def _run_connector(name, params):
55
- """Run scripts/connectors/<name>.py as a subprocess; return its three channels."""
55
+ """Run connectors/<name>.py as a subprocess; return its three channels."""
56
56
  if not isinstance(params, dict):
57
57
  return {"success": False, "error": "params must be a JSON object"}
58
58
  argv = [sys.executable, str(CONNECTORS / f"{name}.py")]
@@ -90,7 +90,7 @@ async def _run_connector(name, params):
90
90
 
91
91
  @auto_tool
92
92
  async def botify(params: dict) -> dict:
93
- """Registry face of scripts/connectors/botify.py; __doc__ is replaced at
93
+ """Registry face of connectors/botify.py; __doc__ is replaced at
94
94
  import with that file's own docstring so both arms read one description."""
95
95
  return await _run_connector("botify", params)
96
96
 
@@ -1464,7 +1464,7 @@ async def voice_synthesis(params: dict) -> dict:
1464
1464
  "error": "Voice synthesis not available - missing dependencies"
1465
1465
  }
1466
1466
 
1467
- if not chip_voice_system or not chip_voice_system.voice_ready:
1467
+ if not chip_voice_system:
1468
1468
  return {
1469
1469
  "success": False,
1470
1470
  "error": "Voice system not ready - check model initialization"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes