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.
- {pipulate-2.57/pipulate.egg-info → pipulate-2.58}/PKG-INFO +1 -1
- {pipulate-2.57 → pipulate-2.58}/__init__.py +2 -2
- {pipulate-2.57 → pipulate-2.58}/apps/010_introduction.py +12 -4
- {pipulate-2.57 → pipulate-2.58}/imports/voice_synthesis.py +159 -7
- {pipulate-2.57 → pipulate-2.58}/pipulate/core.py +1 -1
- {pipulate-2.57 → pipulate-2.58/pipulate.egg-info}/PKG-INFO +1 -1
- {pipulate-2.57 → pipulate-2.58}/pyproject.toml +1 -1
- {pipulate-2.57 → pipulate-2.58}/server.py +1 -1
- {pipulate-2.57 → pipulate-2.58}/tools/connector_tools.py +4 -4
- {pipulate-2.57 → pipulate-2.58}/tools/mcp_tools.py +1 -1
- {pipulate-2.57 → pipulate-2.58}/LICENSE +0 -0
- {pipulate-2.57 → pipulate-2.58}/MANIFEST.in +0 -0
- {pipulate-2.57 → pipulate-2.58}/README.md +0 -0
- {pipulate-2.57 → pipulate-2.58}/apps/015_config.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/apps/020_profiles.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/apps/025_aspect.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/apps/030_roles.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/apps/040_hello_workflow.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/apps/050_documentation.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/apps/060_tasks.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/apps/070_history.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/apps/110_parameter_buster.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/apps/120_link_graph.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/apps/130_gap_analysis.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/apps/200_workflow_genesis.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/apps/210_widget_examples.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/apps/220_roadmap.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/apps/230_dev_assistant.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/apps/240_simon_mcp.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/apps/300_blank_placeholder.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/apps/400_botify_trifecta.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/apps/440_browser_automation.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/apps/450_stream_simulator.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/apps/510_text_field.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/apps/520_text_area.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/apps/530_dropdown.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/apps/540_checkboxes.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/apps/550_radios.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/apps/560_range.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/apps/570_switch.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/apps/580_upload.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/apps/610_markdown.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/apps/620_mermaid.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/apps/630_prism.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/apps/640_javascript.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/apps/710_pandas.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/apps/720_rich.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/apps/730_matplotlib.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/apps/810_webbrowser.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/apps/820_selenium.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/apps/830_pico_slider.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/cli.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/config.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/imports/__init__.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/imports/ai_dictdb.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/imports/ai_tool_discovery_simple_parser.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/imports/append_only_conversation.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/imports/ascii_displays.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/imports/botify/__init__.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/imports/botify/code_generators.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/imports/botify/true_schema_discoverer.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/imports/botify_code_generation.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/imports/crud.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/imports/database_safety_wrapper.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/imports/dom_processing/__init__.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/imports/dom_processing/ai_dom_beautifier.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/imports/dom_processing/enhanced_dom_processor.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/imports/durable_backup_system.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/imports/mcp_orchestrator.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/imports/server_logging.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/imports/stream_orchestrator.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/pipulate/__init__.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/pipulate.egg-info/SOURCES.txt +0 -0
- {pipulate-2.57 → pipulate-2.58}/pipulate.egg-info/dependency_links.txt +0 -0
- {pipulate-2.57 → pipulate-2.58}/pipulate.egg-info/entry_points.txt +0 -0
- {pipulate-2.57 → pipulate-2.58}/pipulate.egg-info/requires.txt +0 -0
- {pipulate-2.57 → pipulate-2.58}/pipulate.egg-info/top_level.txt +0 -0
- {pipulate-2.57 → pipulate-2.58}/setup.cfg +0 -0
- {pipulate-2.57 → pipulate-2.58}/tests/test_apply_airlock.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/tests/test_mck_rep2.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/tools/__init__.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/tools/advanced_automation_tools.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/tools/botify_tools.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/tools/conversation_tools.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/tools/dom_tools.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/tools/keychain_tools.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/tools/llm_optics.py +0 -0
- {pipulate-2.57 → pipulate-2.58}/tools/scraper_tools.py +0 -0
- {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.
|
|
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.
|
|
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
|
|
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
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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 / "
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|