pipulate 2.57__tar.gz → 2.59__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.59}/PKG-INFO +1 -1
- {pipulate-2.57 → pipulate-2.59}/__init__.py +2 -2
- {pipulate-2.57 → pipulate-2.59}/apps/010_introduction.py +12 -4
- {pipulate-2.57 → pipulate-2.59}/imports/voice_synthesis.py +221 -19
- {pipulate-2.57 → pipulate-2.59}/pipulate/core.py +1 -1
- {pipulate-2.57 → pipulate-2.59/pipulate.egg-info}/PKG-INFO +1 -1
- {pipulate-2.57 → pipulate-2.59}/pyproject.toml +1 -1
- {pipulate-2.57 → pipulate-2.59}/server.py +1 -1
- {pipulate-2.57 → pipulate-2.59}/tools/connector_tools.py +4 -4
- {pipulate-2.57 → pipulate-2.59}/tools/mcp_tools.py +1 -1
- {pipulate-2.57 → pipulate-2.59}/tools/scraper_tools.py +93 -41
- {pipulate-2.57 → pipulate-2.59}/LICENSE +0 -0
- {pipulate-2.57 → pipulate-2.59}/MANIFEST.in +0 -0
- {pipulate-2.57 → pipulate-2.59}/README.md +0 -0
- {pipulate-2.57 → pipulate-2.59}/apps/015_config.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/apps/020_profiles.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/apps/025_aspect.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/apps/030_roles.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/apps/040_hello_workflow.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/apps/050_documentation.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/apps/060_tasks.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/apps/070_history.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/apps/110_parameter_buster.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/apps/120_link_graph.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/apps/130_gap_analysis.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/apps/200_workflow_genesis.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/apps/210_widget_examples.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/apps/220_roadmap.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/apps/230_dev_assistant.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/apps/240_simon_mcp.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/apps/300_blank_placeholder.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/apps/400_botify_trifecta.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/apps/440_browser_automation.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/apps/450_stream_simulator.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/apps/510_text_field.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/apps/520_text_area.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/apps/530_dropdown.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/apps/540_checkboxes.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/apps/550_radios.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/apps/560_range.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/apps/570_switch.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/apps/580_upload.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/apps/610_markdown.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/apps/620_mermaid.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/apps/630_prism.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/apps/640_javascript.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/apps/710_pandas.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/apps/720_rich.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/apps/730_matplotlib.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/apps/810_webbrowser.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/apps/820_selenium.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/apps/830_pico_slider.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/cli.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/config.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/imports/__init__.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/imports/ai_dictdb.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/imports/ai_tool_discovery_simple_parser.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/imports/append_only_conversation.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/imports/ascii_displays.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/imports/botify/__init__.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/imports/botify/code_generators.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/imports/botify/true_schema_discoverer.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/imports/botify_code_generation.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/imports/crud.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/imports/database_safety_wrapper.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/imports/dom_processing/__init__.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/imports/dom_processing/ai_dom_beautifier.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/imports/dom_processing/enhanced_dom_processor.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/imports/durable_backup_system.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/imports/mcp_orchestrator.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/imports/server_logging.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/imports/stream_orchestrator.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/pipulate/__init__.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/pipulate.egg-info/SOURCES.txt +0 -0
- {pipulate-2.57 → pipulate-2.59}/pipulate.egg-info/dependency_links.txt +0 -0
- {pipulate-2.57 → pipulate-2.59}/pipulate.egg-info/entry_points.txt +0 -0
- {pipulate-2.57 → pipulate-2.59}/pipulate.egg-info/requires.txt +0 -0
- {pipulate-2.57 → pipulate-2.59}/pipulate.egg-info/top_level.txt +0 -0
- {pipulate-2.57 → pipulate-2.59}/setup.cfg +0 -0
- {pipulate-2.57 → pipulate-2.59}/tests/test_apply_airlock.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/tests/test_mck_rep2.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/tools/__init__.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/tools/advanced_automation_tools.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/tools/botify_tools.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/tools/conversation_tools.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/tools/dom_tools.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/tools/keychain_tools.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/tools/llm_optics.py +0 -0
- {pipulate-2.57 → pipulate-2.59}/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.59
|
|
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.59"
|
|
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 New-B Empathy"
|
|
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,9 +12,12 @@ 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
|
|
18
|
+
import contextlib
|
|
17
19
|
import logging
|
|
20
|
+
import warnings
|
|
18
21
|
|
|
19
22
|
# Try to import voice synthesis dependencies
|
|
20
23
|
try:
|
|
@@ -34,6 +37,128 @@ except ImportError:
|
|
|
34
37
|
|
|
35
38
|
logger = logging.getLogger(__name__)
|
|
36
39
|
|
|
40
|
+
|
|
41
|
+
@contextlib.contextmanager
|
|
42
|
+
def _quiet_hub():
|
|
43
|
+
"""Silence the hub for the length of the one download it ever makes.
|
|
44
|
+
|
|
45
|
+
THE QUIET WALK (2026-09-18, Mac receipt, deed 1480): the first consent
|
|
46
|
+
printed a three-line deprecation notice, a token nag and three progress
|
|
47
|
+
bars between the card and the first spoken step. The hub speaks through
|
|
48
|
+
two channels, its logger and the warnings module, and draws its bars
|
|
49
|
+
through its own switch; all three are turned down here and restored on
|
|
50
|
+
the way out, so nothing outside this block changes. The download itself
|
|
51
|
+
is announced by one plain line in the caller.
|
|
52
|
+
"""
|
|
53
|
+
try:
|
|
54
|
+
from huggingface_hub.utils import (
|
|
55
|
+
are_progress_bars_disabled,
|
|
56
|
+
disable_progress_bars,
|
|
57
|
+
enable_progress_bars,
|
|
58
|
+
)
|
|
59
|
+
except ImportError:
|
|
60
|
+
are_progress_bars_disabled = disable_progress_bars = enable_progress_bars = None
|
|
61
|
+
hub_logger = logging.getLogger("huggingface_hub")
|
|
62
|
+
previous_level = hub_logger.level
|
|
63
|
+
hub_logger.setLevel(logging.ERROR)
|
|
64
|
+
bars_were_disabled = True if disable_progress_bars is None else are_progress_bars_disabled()
|
|
65
|
+
if not bars_were_disabled:
|
|
66
|
+
disable_progress_bars()
|
|
67
|
+
try:
|
|
68
|
+
with warnings.catch_warnings():
|
|
69
|
+
warnings.simplefilter("ignore")
|
|
70
|
+
yield
|
|
71
|
+
finally:
|
|
72
|
+
hub_logger.setLevel(previous_level)
|
|
73
|
+
if not bars_were_disabled:
|
|
74
|
+
enable_progress_bars()
|
|
75
|
+
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
# THE VOICE ASKS BEFORE IT SPEAKS (2026-09-17). Two functions, one file.
|
|
78
|
+
# voice_consent() is the BARRIER: every path to sound reads it, fresh, on every
|
|
79
|
+
# call. The answer comes from PIPULATE_VOICE (an unattended shell declaring
|
|
80
|
+
# intent, the way PIPULATE_BOOT_MENU=0 does) or from ~/.config/pipulate/voice,
|
|
81
|
+
# outside the worktree, so it survives rm -rf on the folder and is asked once
|
|
82
|
+
# per machine rather than once per install.
|
|
83
|
+
# ask_voice_consent() is the CEREMONY: it prints the card and reads one line
|
|
84
|
+
# from /dev/tty. The two are SEPARATE on purpose. flake.nix's door-1 greeting
|
|
85
|
+
# runs as a backgrounded python with stdout on /dev/null while the server holds
|
|
86
|
+
# the foreground on the same terminal; a speaker that ASKED would print the
|
|
87
|
+
# card into /dev/null and take the keystrokes meant for a prompt nobody can
|
|
88
|
+
# see. So the speaker never asks. The rider asks at the start of a walk, and
|
|
89
|
+
# the `voice` word asks again later; both own a terminal.
|
|
90
|
+
# NOTHING IS DOWNLOADED BEFORE A YES: the model loads in ensure_voice(), on
|
|
91
|
+
# the first speak after consent, never at import. Importing this module used
|
|
92
|
+
# to fetch the voice and spend seconds before anyone had asked to speak or
|
|
93
|
+
# been asked whether it may; that was also the import tax the tool roster
|
|
94
|
+
# measured at ~3.8 s.
|
|
95
|
+
# ---------------------------------------------------------------------------
|
|
96
|
+
VOICE_CONSENT_FILE = Path.home() / ".config" / "pipulate" / "voice"
|
|
97
|
+
VOICE_CARD = (
|
|
98
|
+
"\nThe steps can be read aloud.\n\n"
|
|
99
|
+
"The voice is Piper, a small text-to-speech program that runs on this\n"
|
|
100
|
+
"computer; it was made for a Raspberry Pi. It reads sentences a person\n"
|
|
101
|
+
"typed in advance. No AI writes them, and nothing you say or type is\n"
|
|
102
|
+
"sent anywhere. Answering yes downloads the voice once, about 60 MB.\n\n"
|
|
103
|
+
"Read the steps aloud? [y/N] "
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def voice_consent() -> str:
|
|
108
|
+
"""'yes', 'no' or 'unset': the environment first, then the recorded answer."""
|
|
109
|
+
env = os.environ.get("PIPULATE_VOICE", "").strip().lower()
|
|
110
|
+
if env in ("0", "no", "off", "false"):
|
|
111
|
+
return "no"
|
|
112
|
+
if env in ("1", "yes", "on", "true"):
|
|
113
|
+
return "yes"
|
|
114
|
+
try:
|
|
115
|
+
recorded = VOICE_CONSENT_FILE.read_text(encoding="utf-8").strip().lower()
|
|
116
|
+
except OSError:
|
|
117
|
+
return "unset"
|
|
118
|
+
if recorded in ("yes", "no"):
|
|
119
|
+
return recorded
|
|
120
|
+
return "unset"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def ask_voice_consent(later_hint: str = "voice", force: bool = False) -> str:
|
|
124
|
+
"""Ask on /dev/tty and record the answer. Returns 'yes', 'no' or 'unavailable'.
|
|
125
|
+
|
|
126
|
+
Asks only when nothing is recorded, unless force is set (the `voice` word),
|
|
127
|
+
and never when PIPULATE_VOICE decides. Without a terminal to ask on -- a
|
|
128
|
+
pipe, a compile-lane probe, a backgrounded job -- it prints nothing and
|
|
129
|
+
returns 'unavailable', so no prompt can ever block an unattended caller.
|
|
130
|
+
Enter alone means no. A Ctrl+C is not recorded, so the question returns.
|
|
131
|
+
"""
|
|
132
|
+
env = os.environ.get("PIPULATE_VOICE", "").strip().lower()
|
|
133
|
+
if env in ("0", "no", "off", "false", "1", "yes", "on", "true"):
|
|
134
|
+
return voice_consent()
|
|
135
|
+
recorded = voice_consent()
|
|
136
|
+
if recorded != "unset" and not force:
|
|
137
|
+
return recorded
|
|
138
|
+
if not sys.stdout.isatty():
|
|
139
|
+
return "unavailable"
|
|
140
|
+
try:
|
|
141
|
+
tty = open("/dev/tty", "r", encoding="utf-8")
|
|
142
|
+
except OSError:
|
|
143
|
+
return "unavailable"
|
|
144
|
+
try:
|
|
145
|
+
print(VOICE_CARD, end="", flush=True)
|
|
146
|
+
answer = tty.readline()
|
|
147
|
+
except (OSError, KeyboardInterrupt):
|
|
148
|
+
print()
|
|
149
|
+
return "no"
|
|
150
|
+
finally:
|
|
151
|
+
tty.close()
|
|
152
|
+
decision = "yes" if answer.strip().lower() in ("y", "yes") else "no"
|
|
153
|
+
try:
|
|
154
|
+
VOICE_CONSENT_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
155
|
+
VOICE_CONSENT_FILE.write_text(decision + "\n", encoding="utf-8")
|
|
156
|
+
except OSError as exc:
|
|
157
|
+
print(f"(could not record the answer, so it will be asked again: {exc})")
|
|
158
|
+
if decision == "no":
|
|
159
|
+
print(f"Staying quiet. To turn the voice on later, type: {later_hint}")
|
|
160
|
+
return decision
|
|
161
|
+
|
|
37
162
|
class ChipVoiceSystem:
|
|
38
163
|
"""
|
|
39
164
|
Voice synthesis system for Chip O'Theseus
|
|
@@ -49,9 +174,9 @@ class ChipVoiceSystem:
|
|
|
49
174
|
self.current_process = None # Track the running audio process
|
|
50
175
|
self.last_error = None # Populated by synthesize_and_play on failure
|
|
51
176
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
177
|
+
# No download and no model load here: see ensure_voice(). Importing
|
|
178
|
+
# this module must cost nothing and ask nothing.
|
|
179
|
+
if not VOICE_SYNTHESIS_AVAILABLE:
|
|
55
180
|
logger.warning(f"🎤 Voice synthesis not available: {IMPORT_ERROR}")
|
|
56
181
|
|
|
57
182
|
def setup_voice_model(self):
|
|
@@ -70,34 +195,76 @@ class ChipVoiceSystem:
|
|
|
70
195
|
self.model_path = str(local_model_dir / model_path_in_repo)
|
|
71
196
|
self.config_path = str(local_model_dir / config_path_in_repo)
|
|
72
197
|
|
|
198
|
+
downloaded = False
|
|
73
199
|
# Download files if they don't exist
|
|
74
200
|
if not os.path.exists(self.model_path):
|
|
201
|
+
print("Downloading the voice, about 60 MB, once...", flush=True)
|
|
75
202
|
logger.info(f"🎤 Downloading voice model: {model_path_in_repo}...")
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
203
|
+
with _quiet_hub():
|
|
204
|
+
hf_hub_download(
|
|
205
|
+
repo_id=repo_id,
|
|
206
|
+
filename=model_path_in_repo,
|
|
207
|
+
local_dir=local_model_dir,
|
|
208
|
+
)
|
|
209
|
+
downloaded = True
|
|
82
210
|
|
|
83
211
|
if not os.path.exists(self.config_path):
|
|
84
212
|
logger.info(f"🎤 Downloading voice config: {config_path_in_repo}...")
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
213
|
+
with _quiet_hub():
|
|
214
|
+
hf_hub_download(
|
|
215
|
+
repo_id=repo_id,
|
|
216
|
+
filename=config_path_in_repo,
|
|
217
|
+
local_dir=local_model_dir,
|
|
218
|
+
)
|
|
219
|
+
downloaded = True
|
|
91
220
|
|
|
92
221
|
# Load the voice model
|
|
93
222
|
self.voice = PiperVoice.load(self.model_path, config_path=self.config_path)
|
|
94
223
|
self.voice_ready = True
|
|
95
224
|
logger.info("🎤 Voice model loaded successfully")
|
|
225
|
+
if downloaded:
|
|
226
|
+
print("Voice ready.", flush=True)
|
|
96
227
|
|
|
97
228
|
except Exception as e:
|
|
98
229
|
logger.error(f"🎤 Failed to setup voice model: {e}")
|
|
230
|
+
self.last_error = f"voice model setup failed: {e}"
|
|
99
231
|
self.voice_ready = False
|
|
100
232
|
|
|
233
|
+
def ensure_voice(self) -> bool:
|
|
234
|
+
"""Load the voice on first use, never at import. Returns readiness.
|
|
235
|
+
|
|
236
|
+
Reached only after voice_consent() read yes, so the download the card
|
|
237
|
+
announces happens after the answer and not before it.
|
|
238
|
+
"""
|
|
239
|
+
if self.voice_ready:
|
|
240
|
+
return True
|
|
241
|
+
if not VOICE_SYNTHESIS_AVAILABLE:
|
|
242
|
+
self.last_error = f"voice synthesis not installed: {IMPORT_ERROR}"
|
|
243
|
+
return False
|
|
244
|
+
# THE PHONEME LINE (TODO of 2026-09-15, discharged 2026-09-18): piper
|
|
245
|
+
# warns, through a logger nobody configured, about one syllabic mark
|
|
246
|
+
# in the word "written", and Python's last-resort handler prints the
|
|
247
|
+
# bare message on every walk. The flake's door-1 greeting already sets
|
|
248
|
+
# this level; the speaker now sets it for every caller.
|
|
249
|
+
logging.getLogger("piper").setLevel(logging.ERROR)
|
|
250
|
+
self.setup_voice_model()
|
|
251
|
+
return self.voice_ready
|
|
252
|
+
|
|
253
|
+
def can_speak(self) -> bool:
|
|
254
|
+
"""True only once a human has said yes AND the voice is loaded.
|
|
255
|
+
|
|
256
|
+
THE FLAG WENT COLD (2026-09-17, the census after the lazy load).
|
|
257
|
+
voice_ready read True at import on any machine with the model on disk,
|
|
258
|
+
and four files gate on it BEFORE they ever call speak_text:
|
|
259
|
+
apps/010_introduction.py, pipulate/core.py, server.py and
|
|
260
|
+
tools/mcp_tools.py. With the load deferred to first use the flag reads
|
|
261
|
+
False in every fresh process until something speaks, so none of those
|
|
262
|
+
gates opens. This is the question they meant to ask: consent first,
|
|
263
|
+
then the load, which downloads only after a yes. A status read that
|
|
264
|
+
must never download keeps reading voice_ready, which now means loaded.
|
|
265
|
+
"""
|
|
266
|
+
return voice_consent() == "yes" and self.ensure_voice()
|
|
267
|
+
|
|
101
268
|
def stop_speaking(self):
|
|
102
269
|
"""
|
|
103
270
|
Silence! Kill the current audio process if it exists.
|
|
@@ -129,7 +296,13 @@ class ChipVoiceSystem:
|
|
|
129
296
|
Returns:
|
|
130
297
|
bool: True if successful, False otherwise
|
|
131
298
|
"""
|
|
132
|
-
if
|
|
299
|
+
if voice_consent() != "yes":
|
|
300
|
+
# THE BARRIER. Every path to sound passes here, and the answer is
|
|
301
|
+
# read fresh each time, so a no recorded mid-session holds at once.
|
|
302
|
+
self.last_error = "voice not allowed"
|
|
303
|
+
logger.info("🎤 Voice not allowed; nothing spoken")
|
|
304
|
+
return False
|
|
305
|
+
if not self.ensure_voice():
|
|
133
306
|
logger.warning("🎤 Voice synthesis not ready")
|
|
134
307
|
return False
|
|
135
308
|
|
|
@@ -318,10 +491,22 @@ class ChipVoiceSystem:
|
|
|
318
491
|
Returns:
|
|
319
492
|
Dict with success status and details
|
|
320
493
|
"""
|
|
321
|
-
|
|
494
|
+
consent = voice_consent()
|
|
495
|
+
if consent != "yes":
|
|
496
|
+
# DECLINED IS NOT FAILED. Callers read success=False either way;
|
|
497
|
+
# this key is how they tell a human's answer from a broken player.
|
|
498
|
+
return {
|
|
499
|
+
"success": False,
|
|
500
|
+
"declined": True,
|
|
501
|
+
"consent": consent,
|
|
502
|
+
"text": text,
|
|
503
|
+
"error": "voice declined" if consent == "no"
|
|
504
|
+
else "voice not yet allowed: the voice card has not been answered",
|
|
505
|
+
}
|
|
506
|
+
if not self.ensure_voice():
|
|
322
507
|
return {
|
|
323
508
|
"success": False,
|
|
324
|
-
"error": "Voice synthesis not available",
|
|
509
|
+
"error": self.last_error or "Voice synthesis not available",
|
|
325
510
|
"details": "Piper TTS model not loaded"
|
|
326
511
|
}
|
|
327
512
|
|
|
@@ -467,7 +652,7 @@ def test_voice_synthesis():
|
|
|
467
652
|
# Test basic voice synthesis
|
|
468
653
|
test_text = "Hello Mike, this is Chip O'Theseus speaking. I can now remember what happened before the server restart!"
|
|
469
654
|
|
|
470
|
-
if chip_voice_system and chip_voice_system.
|
|
655
|
+
if chip_voice_system and chip_voice_system.can_speak():
|
|
471
656
|
result = chip_voice_system.speak_text(test_text)
|
|
472
657
|
if result["success"]:
|
|
473
658
|
print("✅ Voice synthesis test successful")
|
|
@@ -510,6 +695,23 @@ def test_memory_voice_integration():
|
|
|
510
695
|
print("❌ No memories found to test")
|
|
511
696
|
return False
|
|
512
697
|
|
|
698
|
+
if __name__ == "__main__" and sys.argv[1:2] == ["ask"]:
|
|
699
|
+
# `voice`, the word the card names: re-ask and record, then speak one
|
|
700
|
+
# sentence on yes so the audio path is heard rather than assumed.
|
|
701
|
+
_env = os.environ.get("PIPULATE_VOICE", "").strip()
|
|
702
|
+
if _env:
|
|
703
|
+
print(f"voice: PIPULATE_VOICE={_env!r} decides; unset it to be asked.")
|
|
704
|
+
raise SystemExit(0)
|
|
705
|
+
_decision = ask_voice_consent(later_hint="voice", force=True)
|
|
706
|
+
if _decision == "unavailable":
|
|
707
|
+
print("voice: no terminal to ask on; run this from a terminal.")
|
|
708
|
+
raise SystemExit(1)
|
|
709
|
+
print(f"voice: {_decision} (recorded in {VOICE_CONSENT_FILE})")
|
|
710
|
+
if _decision == "yes" and chip_voice_system is not None:
|
|
711
|
+
_result = chip_voice_system.speak_text("This is Piper, reading a script. The voice is on.")
|
|
712
|
+
if not _result.get("success"):
|
|
713
|
+
print(f"voice: nothing was heard: {_result.get('error')}")
|
|
714
|
+
raise SystemExit(0)
|
|
513
715
|
if __name__ == "__main__":
|
|
514
716
|
# Run tests if script is executed directly
|
|
515
717
|
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.59
|
|
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.59"
|
|
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"
|
|
@@ -3,6 +3,7 @@ import asyncio
|
|
|
3
3
|
import hashlib
|
|
4
4
|
import json
|
|
5
5
|
import os
|
|
6
|
+
import shlex
|
|
6
7
|
import sys
|
|
7
8
|
import shutil
|
|
8
9
|
import tempfile
|
|
@@ -21,6 +22,18 @@ from selenium.webdriver.common.by import By
|
|
|
21
22
|
from tools import auto_tool
|
|
22
23
|
from . import dom_tools
|
|
23
24
|
|
|
25
|
+
# THE HEAL HAPPENS ONCE PER RUN (2026-09-18, Mac receipt, deed 1480). The
|
|
26
|
+
# flake pins no Chrome on a Mac, so the driver is matched to the host's
|
|
27
|
+
# Chrome at launch: the first uc.Chrome raises on the version mismatch, the
|
|
28
|
+
# window it had opened closes, and the retry below relaunches with
|
|
29
|
+
# version_main set to the browser's major version. That version was thrown
|
|
30
|
+
# away after every stop, so a three-stop walk paid the close-and-reopen at
|
|
31
|
+
# every stop and printed a WARNING three times for the ordinary case. It is
|
|
32
|
+
# remembered here for the life of the process: the next launch passes it in
|
|
33
|
+
# and never mismatches. A dict, not a bare name, so no function needs a
|
|
34
|
+
# global statement to write it.
|
|
35
|
+
_HEAL = {"version_main": None}
|
|
36
|
+
|
|
24
37
|
async def generate_optics_subprocess(target_dir_path: str):
|
|
25
38
|
"""Isolated wrapper to call llm_optics.py as a subprocess, protecting the event loop."""
|
|
26
39
|
script_path = Path(__file__).resolve().parent / "llm_optics.py"
|
|
@@ -59,35 +72,38 @@ def _guided_path_component(url: str) -> tuple[str, str]:
|
|
|
59
72
|
|
|
60
73
|
|
|
61
74
|
# --- Optional audio during browser startup ---
|
|
62
|
-
#
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
# never touch any other aplay pipeline. The wav lives in repo negative space
|
|
66
|
-
# (gitignored) or ~/.local/share/pipulate/; absent file = silent no-op.
|
|
75
|
+
# Operator-owned courtesy sounds live outside the repo. tick.wav loops while
|
|
76
|
+
# the browser is opening; ding.wav plays once when the page-load phase ends.
|
|
77
|
+
# No file means no sound. Linux uses aplay; Darwin uses afplay.
|
|
67
78
|
SCRAPE_MUSIC_MARKER = "pipulate-scrape-music"
|
|
79
|
+
SOUND_DIR = Path.home() / ".local/share/pipulate"
|
|
68
80
|
|
|
69
81
|
|
|
70
|
-
def
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
82
|
+
def _sound_file(name):
|
|
83
|
+
path = SOUND_DIR / name
|
|
84
|
+
return path if path.is_file() else None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _player_args(sound):
|
|
88
|
+
if sound is None:
|
|
89
|
+
return None
|
|
90
|
+
if sys.platform == "darwin":
|
|
91
|
+
player = shutil.which("afplay")
|
|
92
|
+
return [player, str(sound)] if player else None
|
|
93
|
+
player = shutil.which("aplay")
|
|
94
|
+
return [player, "-q", "-D", "default", str(sound)] if player else None
|
|
80
95
|
|
|
81
96
|
|
|
82
97
|
def _start_scrape_music(verbose=True):
|
|
83
98
|
import subprocess
|
|
84
|
-
music = _find_music_file()
|
|
85
|
-
if not music:
|
|
86
|
-
return None
|
|
87
99
|
|
|
100
|
+
args = _player_args(_sound_file("tick.wav"))
|
|
101
|
+
if not args:
|
|
102
|
+
return None
|
|
103
|
+
command = " ".join(shlex.quote(part) for part in args)
|
|
88
104
|
try:
|
|
89
105
|
return subprocess.Popen(
|
|
90
|
-
["sh", "-c", f
|
|
106
|
+
["sh", "-c", f"while :; do {command}; done # {SCRAPE_MUSIC_MARKER}"],
|
|
91
107
|
stdout=subprocess.DEVNULL,
|
|
92
108
|
stderr=subprocess.DEVNULL,
|
|
93
109
|
preexec_fn=os.setsid,
|
|
@@ -96,18 +112,36 @@ def _start_scrape_music(verbose=True):
|
|
|
96
112
|
return None
|
|
97
113
|
|
|
98
114
|
|
|
99
|
-
def _stop_scrape_music(proc):
|
|
115
|
+
def _stop_scrape_music(proc, ding=False):
|
|
100
116
|
import signal
|
|
101
117
|
import subprocess
|
|
118
|
+
|
|
102
119
|
if proc is not None:
|
|
103
120
|
try:
|
|
104
121
|
os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
|
|
105
122
|
except Exception:
|
|
106
123
|
pass
|
|
107
124
|
try:
|
|
108
|
-
subprocess.run(
|
|
125
|
+
subprocess.run(
|
|
126
|
+
["pkill", "-f", SCRAPE_MUSIC_MARKER],
|
|
127
|
+
check=False,
|
|
128
|
+
stdout=subprocess.DEVNULL,
|
|
129
|
+
stderr=subprocess.DEVNULL,
|
|
130
|
+
)
|
|
109
131
|
except Exception:
|
|
110
132
|
pass
|
|
133
|
+
if ding:
|
|
134
|
+
args = _player_args(_sound_file("ding.wav"))
|
|
135
|
+
if args:
|
|
136
|
+
try:
|
|
137
|
+
subprocess.run(
|
|
138
|
+
args,
|
|
139
|
+
check=False,
|
|
140
|
+
stdout=subprocess.DEVNULL,
|
|
141
|
+
stderr=subprocess.DEVNULL,
|
|
142
|
+
)
|
|
143
|
+
except Exception:
|
|
144
|
+
pass
|
|
111
145
|
|
|
112
146
|
|
|
113
147
|
def _simplify_html_for_llm(html_content, default_title=""):
|
|
@@ -223,7 +257,7 @@ def _document_candidates(cdp_events: list, domain: str, final_url: str = "") ->
|
|
|
223
257
|
return on_host
|
|
224
258
|
|
|
225
259
|
|
|
226
|
-
def _capture_checkpoint(stdin=None, stdout=None) -> dict:
|
|
260
|
+
def _capture_checkpoint(stdin=None, stdout=None, before_prompt=None) -> dict:
|
|
227
261
|
"""Require an explicit CAPTURE token from the human's keyboard.
|
|
228
262
|
|
|
229
263
|
/DEV/TTY PREFERENCE (convicted 2026-07-29, public_walk stop 1): the
|
|
@@ -265,8 +299,16 @@ def _capture_checkpoint(stdin=None, stdout=None) -> dict:
|
|
|
265
299
|
}
|
|
266
300
|
|
|
267
301
|
try:
|
|
302
|
+
if before_prompt is not None:
|
|
303
|
+
try:
|
|
304
|
+
before_prompt()
|
|
305
|
+
except Exception as exc:
|
|
306
|
+
output_stream.write(f"\n(checkpoint narration unavailable: {exc})\n")
|
|
307
|
+
else:
|
|
308
|
+
output_stream.write(
|
|
309
|
+
"\nWhen the page you want is ready, type CAPTURE and press Enter.\n"
|
|
310
|
+
)
|
|
268
311
|
output_stream.write(
|
|
269
|
-
"\nWhen the page you want is ready, type CAPTURE and press Enter.\n"
|
|
270
312
|
"Any other response aborts without capturing artifacts.\n"
|
|
271
313
|
"CAPTURE> "
|
|
272
314
|
)
|
|
@@ -560,7 +602,8 @@ async def _selenium_capture(params: dict, checkpoint=None) -> dict:
|
|
|
560
602
|
driver = uc.Chrome(options=options,
|
|
561
603
|
user_data_dir=str(profile_path),
|
|
562
604
|
browser_executable_path=browser_path,
|
|
563
|
-
driver_executable_path=driver_path
|
|
605
|
+
driver_executable_path=driver_path,
|
|
606
|
+
version_main=_HEAL["version_main"])
|
|
564
607
|
except Exception as e:
|
|
565
608
|
error_msg = str(e)
|
|
566
609
|
if "Current browser version is" in error_msg:
|
|
@@ -568,7 +611,10 @@ async def _selenium_capture(params: dict, checkpoint=None) -> dict:
|
|
|
568
611
|
match = re.search(r'Current browser version is (\d+)', error_msg)
|
|
569
612
|
if match:
|
|
570
613
|
fallback_version = int(match.group(1))
|
|
571
|
-
|
|
614
|
+
_HEAL["version_main"] = fallback_version
|
|
615
|
+
logger.info(f"Chrome {fallback_version}: driver matched; relaunching once for this run")
|
|
616
|
+
if verbose:
|
|
617
|
+
print(f"Chrome closed and is reopening once while its driver is matched to Chrome {fallback_version}; that is normal.", flush=True)
|
|
572
618
|
# UC consumes the options object. We must forge a fresh one.
|
|
573
619
|
fresh_options = uc.ChromeOptions()
|
|
574
620
|
fresh_options.set_capability("goog:loggingPrefs", {"performance": "ALL"})
|
|
@@ -592,20 +638,25 @@ async def _selenium_capture(params: dict, checkpoint=None) -> dict:
|
|
|
592
638
|
logger.info(f"Navigating to: {url}")
|
|
593
639
|
driver.get(url)
|
|
594
640
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
641
|
+
# A human-guided ride already has the CAPTURE fence as its settle
|
|
642
|
+
# detector. The 20-second staleness wait exists for unattended sigil
|
|
643
|
+
# scrapes, where a security interstitial may reload underneath us.
|
|
644
|
+
if not interactive:
|
|
645
|
+
try:
|
|
646
|
+
if verbose: logger.info("Waiting for security challenge to trigger a reload (Stage 1)...")
|
|
647
|
+
initial_body = driver.find_element(By.TAG_NAME, 'body')
|
|
648
|
+
WebDriverWait(driver, 20).until(EC.staleness_of(initial_body))
|
|
649
|
+
if verbose: logger.success("✅ Page reload detected!")
|
|
650
|
+
|
|
651
|
+
if verbose: logger.info("Waiting for main content to appear after reload (Stage 2)...")
|
|
652
|
+
WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.CSS_SELECTOR, "body")))
|
|
653
|
+
if verbose: logger.success("✅ Main content located!")
|
|
654
|
+
except Exception:
|
|
655
|
+
if verbose: logger.info("Did not detect a page reload for security challenge. Proceeding anyway.")
|
|
606
656
|
|
|
607
|
-
# The
|
|
608
|
-
|
|
657
|
+
# The page-load phase is over: stop ticking and, if the operator
|
|
658
|
+
# supplied one, play the ready sound exactly once.
|
|
659
|
+
_stop_scrape_music(music_proc, ding=True)
|
|
609
660
|
music_proc = None
|
|
610
661
|
|
|
611
662
|
if checkpoint is not None:
|
|
@@ -737,8 +788,8 @@ async def _selenium_capture(params: dict, checkpoint=None) -> dict:
|
|
|
737
788
|
# ledger-only so it can be replayed offline over any cached capture.
|
|
738
789
|
#
|
|
739
790
|
# [-1] SURVIVES, and its rationale is still UNWITNESSED: the
|
|
740
|
-
# staleness_of() wait above exists because a security
|
|
741
|
-
# serves an interstitial Document and then reloads, making the LAST
|
|
791
|
+
# non-interactive staleness_of() wait above exists because a security
|
|
792
|
+
# challenge serves an interstitial Document and then reloads, making the LAST
|
|
742
793
|
# candidate the real page. No capture on this machine has ever been
|
|
743
794
|
# challenged -- every scrape logs "Did not detect a page reload."
|
|
744
795
|
# The reasoning is sound from the code and has no receipt behind it.
|
|
@@ -956,7 +1007,7 @@ async def selenium_automation(params: dict) -> dict:
|
|
|
956
1007
|
return await _selenium_capture(params)
|
|
957
1008
|
|
|
958
1009
|
|
|
959
|
-
async def guided_browser_capture(params: dict, stdin=None, stdout=None) -> dict:
|
|
1010
|
+
async def guided_browser_capture(params: dict, stdin=None, stdout=None, before_prompt=None) -> dict:
|
|
960
1011
|
"""Capture one human-guided page through one visible persistent driver."""
|
|
961
1012
|
if not isinstance(params, dict):
|
|
962
1013
|
return {
|
|
@@ -1029,5 +1080,6 @@ async def guided_browser_capture(params: dict, stdin=None, stdout=None) -> dict:
|
|
|
1029
1080
|
checkpoint=lambda: _capture_checkpoint(
|
|
1030
1081
|
stdin=input_stream,
|
|
1031
1082
|
stdout=stdout,
|
|
1083
|
+
before_prompt=before_prompt,
|
|
1032
1084
|
),
|
|
1033
1085
|
)
|
|
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
|