@oneciel-ai/ciel-runtime 0.2.11 → 0.2.12
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.
- package/ciel_runtime.py +2 -2
- package/ciel_runtime_support/colab_speech_jobs.py +176 -0
- package/ciel_runtime_support/config_repository.py +1 -0
- package/ciel_runtime_support/runtime_constants.py +1 -1
- package/ciel_runtime_support/speech_http_controller.py +37 -0
- package/ciel_runtime_support/web_ui.py +62 -1
- package/docs/COLAB_SPEECH.md +20 -0
- package/install.ps1 +5 -0
- package/install.sh +3 -0
- package/package.json +1 -1
- package/scripts/colab/__pycache__/bootstrap_moss_tts.cpython-311.pyc +0 -0
- package/scripts/colab/__pycache__/bootstrap_qwen_asr.cpython-311.pyc +0 -0
- package/scripts/configure_speech_workers.py +5 -0
- package/scripts/deploy_colab_speech.ps1 +73 -10
package/ciel_runtime.py
CHANGED
|
@@ -500,6 +500,7 @@ from ciel_runtime_support.runtime_paths import (CHANNEL_COMPACT_REQUEST_PATH, #
|
|
|
500
500
|
from ciel_runtime_support.runtime_restart import forced_upgrade_environment
|
|
501
501
|
from ciel_runtime_support.runtime_restart import running_from_npm_package as detect_running_from_npm_package
|
|
502
502
|
from ciel_runtime_support.secure_json_repository import SecureJsonEffects, SecureJsonRepository
|
|
503
|
+
from ciel_runtime_support.colab_speech_jobs import colab_speech_job_status, launch_colab_speech_job
|
|
503
504
|
from ciel_runtime_support.speech_http_controller import SpeechHttpController, SpeechHttpPorts
|
|
504
505
|
from ciel_runtime_support.session_import import ImportSessionHttpController, ImportSessionHttpPorts, ImportSessionLimits, ImportSessionRepository, ImportSessionService, import_record_line, import_tool_text, normalize_import_source
|
|
505
506
|
from ciel_runtime_support.slash_command_assets import ADVISOR_NATIVE_DISABLED_SLASH_COMMAND # noqa: F401 - compatibility export
|
|
@@ -1700,8 +1701,7 @@ def web_ui_controller() -> WebUiController:
|
|
|
1700
1701
|
def render_router_home_html(cfg: dict[str, Any], provider: str, pcfg: dict[str, Any]) -> str: return web_ui_controller().render_router_home(cfg, provider, pcfg)
|
|
1701
1702
|
def render_web_chat_html(cfg: dict[str, Any], provider: str, pcfg: dict[str, Any]) -> str: return web_ui_controller().render_web_chat(cfg, provider, pcfg)
|
|
1702
1703
|
def handle_web_get(handler: BaseHTTPRequestHandler, path: str) -> bool: return web_ui_controller().handle_get(handler, path)
|
|
1703
|
-
|
|
1704
|
-
def speech_http_controller() -> SpeechHttpController: return SpeechHttpController(SpeechHttpPorts(load_config, save_config, write_json, router_log))
|
|
1704
|
+
def speech_http_controller() -> SpeechHttpController: return SpeechHttpController(SpeechHttpPorts(load_config, save_config, write_json, router_log, colab_action=launch_colab_speech_job, colab_status=colab_speech_job_status))
|
|
1705
1705
|
def parse_json_body(raw: bytes) -> dict[str, Any]:
|
|
1706
1706
|
try:
|
|
1707
1707
|
value = json.loads(raw.decode("utf-8") if raw else "{}")
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Background Colab speech deployment jobs launched by Web Chat."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import subprocess
|
|
8
|
+
import threading
|
|
9
|
+
import uuid
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from .runtime_paths import CONFIG_DIR
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
_ACTIONS = {"start", "deploy", "recreate", "status"}
|
|
18
|
+
_SAFE_NAME = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]{0,127}")
|
|
19
|
+
_SECRET_PATTERN = re.compile(r"(?:tskey-[A-Za-z0-9_-]+|Bearer\s+\S+)", re.IGNORECASE)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(slots=True)
|
|
23
|
+
class _Job:
|
|
24
|
+
job_id: str
|
|
25
|
+
action: str
|
|
26
|
+
profile: str
|
|
27
|
+
process: subprocess.Popen[bytes]
|
|
28
|
+
log_path: Path
|
|
29
|
+
redactions: tuple[str, ...]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ColabSpeechJobManager:
|
|
33
|
+
def __init__(self, script_path: Path, state_dir: Path) -> None:
|
|
34
|
+
self.script_path = script_path
|
|
35
|
+
self.state_dir = state_dir
|
|
36
|
+
self._jobs: dict[str, _Job] = {}
|
|
37
|
+
self._latest = ""
|
|
38
|
+
self._lock = threading.Lock()
|
|
39
|
+
|
|
40
|
+
@staticmethod
|
|
41
|
+
def _safe_name(value: Any, label: str) -> str:
|
|
42
|
+
text = str(value or "").strip()
|
|
43
|
+
if not _SAFE_NAME.fullmatch(text):
|
|
44
|
+
raise ValueError(f"invalid Colab {label}")
|
|
45
|
+
return text
|
|
46
|
+
|
|
47
|
+
def login_command(self, settings: dict[str, Any], *, reset: bool = False) -> str:
|
|
48
|
+
profile = self._safe_name(settings.get("profile") or "default", "profile")
|
|
49
|
+
distribution = self._safe_name(settings.get("distribution") or "Ubuntu-26.04", "distribution")
|
|
50
|
+
auth = str(settings.get("auth") or "oauth2").strip().lower()
|
|
51
|
+
if auth not in {"adc", "oauth2"}:
|
|
52
|
+
raise ValueError("Colab auth must be adc or oauth2")
|
|
53
|
+
suffix = " -ResetAuthentication" if reset else ""
|
|
54
|
+
return (
|
|
55
|
+
"powershell -ExecutionPolicy Bypass -File "
|
|
56
|
+
f'"{self.script_path}" -Action Login -Distribution "{distribution}" '
|
|
57
|
+
f'-ColabAuth "{auth}" -Profile "{profile}"{suffix}'
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
@staticmethod
|
|
61
|
+
def _redact(text: str, redactions: tuple[str, ...]) -> str:
|
|
62
|
+
result = _SECRET_PATTERN.sub("<redacted>", text)
|
|
63
|
+
for secret in redactions:
|
|
64
|
+
result = result.replace(secret, "<redacted>")
|
|
65
|
+
return result
|
|
66
|
+
|
|
67
|
+
def _scrub_completed_log(self, job: _Job) -> None:
|
|
68
|
+
job.process.wait()
|
|
69
|
+
try:
|
|
70
|
+
original = job.log_path.read_text(encoding="utf-8", errors="replace")
|
|
71
|
+
redacted = self._redact(original, job.redactions)
|
|
72
|
+
if redacted != original:
|
|
73
|
+
job.log_path.write_text(redacted, encoding="utf-8")
|
|
74
|
+
except OSError:
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
def launch(
|
|
78
|
+
self,
|
|
79
|
+
action: str,
|
|
80
|
+
settings: dict[str, Any],
|
|
81
|
+
secrets: dict[str, str] | None = None,
|
|
82
|
+
) -> dict[str, Any]:
|
|
83
|
+
normalized = str(action or "").strip().lower()
|
|
84
|
+
if normalized == "login":
|
|
85
|
+
return {
|
|
86
|
+
"ok": True,
|
|
87
|
+
"requires_terminal": True,
|
|
88
|
+
"command": self.login_command(settings, reset=bool((secrets or {}).get("reset_authentication"))),
|
|
89
|
+
}
|
|
90
|
+
if normalized not in _ACTIONS:
|
|
91
|
+
raise ValueError("Colab action must be start, deploy, recreate, status, or login")
|
|
92
|
+
if os.name != "nt":
|
|
93
|
+
raise RuntimeError("Web-managed Colab deployment currently requires Windows with WSL")
|
|
94
|
+
if not self.script_path.is_file():
|
|
95
|
+
raise RuntimeError(f"Colab deployment script is missing: {self.script_path}")
|
|
96
|
+
profile = self._safe_name(settings.get("profile") or "default", "profile")
|
|
97
|
+
with self._lock:
|
|
98
|
+
for job in self._jobs.values():
|
|
99
|
+
if job.profile == profile and job.process.poll() is None:
|
|
100
|
+
raise RuntimeError(f"Colab profile '{profile}' already has a running job")
|
|
101
|
+
self.state_dir.mkdir(parents=True, exist_ok=True)
|
|
102
|
+
job_id = uuid.uuid4().hex[:16]
|
|
103
|
+
log_path = self.state_dir / f"{job_id}.log"
|
|
104
|
+
command = [
|
|
105
|
+
"powershell.exe",
|
|
106
|
+
"-NoProfile",
|
|
107
|
+
"-ExecutionPolicy",
|
|
108
|
+
"Bypass",
|
|
109
|
+
"-File",
|
|
110
|
+
str(self.script_path),
|
|
111
|
+
"-Action",
|
|
112
|
+
normalized.capitalize(),
|
|
113
|
+
"-Profile",
|
|
114
|
+
profile,
|
|
115
|
+
]
|
|
116
|
+
environment = os.environ.copy()
|
|
117
|
+
supplied = secrets or {}
|
|
118
|
+
for source, target in (("tailscale_auth_key", "TAILSCALE_AUTHKEY"), ("speech_api_key", "CIEL_SPEECH_API_KEY")):
|
|
119
|
+
value = str(supplied.get(source) or "").strip()
|
|
120
|
+
if value:
|
|
121
|
+
environment[target] = value
|
|
122
|
+
with log_path.open("wb") as output:
|
|
123
|
+
process = subprocess.Popen(
|
|
124
|
+
command,
|
|
125
|
+
cwd=str(self.script_path.parent.parent),
|
|
126
|
+
env=environment,
|
|
127
|
+
stdin=subprocess.DEVNULL,
|
|
128
|
+
stdout=output,
|
|
129
|
+
stderr=subprocess.STDOUT,
|
|
130
|
+
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
|
131
|
+
)
|
|
132
|
+
redactions = tuple(str(supplied.get(key) or "").strip() for key in ("tailscale_auth_key", "speech_api_key") if str(supplied.get(key) or "").strip())
|
|
133
|
+
job = _Job(job_id, normalized, profile, process, log_path, redactions)
|
|
134
|
+
self._jobs[job_id] = job
|
|
135
|
+
self._latest = job_id
|
|
136
|
+
threading.Thread(target=self._scrub_completed_log, args=(job,), daemon=True).start()
|
|
137
|
+
return self.status(job_id)
|
|
138
|
+
|
|
139
|
+
def status(self, job_id: str = "") -> dict[str, Any]:
|
|
140
|
+
selected = str(job_id or "").strip() or self._latest
|
|
141
|
+
with self._lock:
|
|
142
|
+
job = self._jobs.get(selected)
|
|
143
|
+
if job is None:
|
|
144
|
+
return {"ok": True, "job": None}
|
|
145
|
+
return_code = job.process.poll()
|
|
146
|
+
try:
|
|
147
|
+
output = job.log_path.read_text(encoding="utf-8", errors="replace")[-16000:]
|
|
148
|
+
except OSError:
|
|
149
|
+
output = ""
|
|
150
|
+
redacted_output = self._redact(output, job.redactions)
|
|
151
|
+
return {
|
|
152
|
+
"ok": return_code in {None, 0},
|
|
153
|
+
"job": {
|
|
154
|
+
"id": job.job_id,
|
|
155
|
+
"action": job.action,
|
|
156
|
+
"profile": job.profile,
|
|
157
|
+
"running": return_code is None,
|
|
158
|
+
"return_code": return_code,
|
|
159
|
+
"output": redacted_output,
|
|
160
|
+
},
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
_SCRIPT_PATH = Path(__file__).resolve().parents[1] / "scripts" / "deploy_colab_speech.ps1"
|
|
165
|
+
_MANAGER = ColabSpeechJobManager(_SCRIPT_PATH, CONFIG_DIR / "colab-jobs")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def launch_colab_speech_job(action: str, settings: dict[str, Any], secrets: dict[str, str] | None = None) -> dict[str, Any]:
|
|
169
|
+
return _MANAGER.launch(action, settings, secrets)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def colab_speech_job_status(job_id: str = "") -> dict[str, Any]:
|
|
173
|
+
return _MANAGER.status(job_id)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
__all__ = ["ColabSpeechJobManager", "colab_speech_job_status", "launch_colab_speech_job"]
|
|
@@ -32,6 +32,7 @@ def build_default_config(provider_defaults: dict[str, Any]) -> dict[str, Any]:
|
|
|
32
32
|
"enabled": True,
|
|
33
33
|
"distribution": "Ubuntu-26.04",
|
|
34
34
|
"auth": "adc",
|
|
35
|
+
"profile": "default",
|
|
35
36
|
"asr_session": "ciel-asr",
|
|
36
37
|
"tts_session": "ciel-tts",
|
|
37
38
|
"asr_accelerator": "T4",
|
|
@@ -24,6 +24,8 @@ class SpeechHttpPorts:
|
|
|
24
24
|
write_json: Callable[..., None]
|
|
25
25
|
log: Callable[[str, str], None]
|
|
26
26
|
urlopen: Callable[..., Any] = urllib.request.urlopen
|
|
27
|
+
colab_action: Callable[[str, dict[str, Any], dict[str, str]], dict[str, Any]] | None = None
|
|
28
|
+
colab_status: Callable[[str], dict[str, Any]] | None = None
|
|
27
29
|
|
|
28
30
|
|
|
29
31
|
@dataclass(frozen=True, slots=True)
|
|
@@ -37,6 +39,10 @@ class SpeechHttpController:
|
|
|
37
39
|
if path == "/ca/speech/health":
|
|
38
40
|
self.ports.write_json(handler, self.health_payload())
|
|
39
41
|
return True
|
|
42
|
+
if path == "/ca/speech/colab/job":
|
|
43
|
+
payload = self.ports.colab_status("") if self.ports.colab_status else {"ok": True, "job": None}
|
|
44
|
+
self.ports.write_json(handler, payload)
|
|
45
|
+
return True
|
|
40
46
|
if path == "/ca/web/chat/api":
|
|
41
47
|
self.ports.write_json(handler, self.discovery_payload())
|
|
42
48
|
return True
|
|
@@ -53,6 +59,8 @@ class SpeechHttpController:
|
|
|
53
59
|
) -> bool:
|
|
54
60
|
if path == "/ca/speech/config":
|
|
55
61
|
return self._save_public_config(handler, raw)
|
|
62
|
+
if path == "/ca/speech/colab/action":
|
|
63
|
+
return self._start_colab_action(handler, raw)
|
|
56
64
|
if path in {"/v1/audio/transcriptions", "/v1/audio/translations"}:
|
|
57
65
|
return self._proxy_asr(handler, raw, content_type)
|
|
58
66
|
if path in {"/v1/audio/speech", "/v1/audio/speech/batch"}:
|
|
@@ -90,6 +98,8 @@ class SpeechHttpController:
|
|
|
90
98
|
"chat_files": "POST /ca/channel/files",
|
|
91
99
|
"speech_config": "GET|POST /ca/speech/config",
|
|
92
100
|
"speech_health": "GET /ca/speech/health",
|
|
101
|
+
"colab_action": "POST /ca/speech/colab/action",
|
|
102
|
+
"colab_job": "GET /ca/speech/colab/job",
|
|
93
103
|
"asr": "POST /v1/audio/transcriptions",
|
|
94
104
|
"asr_translate": "POST /v1/audio/translations",
|
|
95
105
|
"tts": "POST /v1/audio/speech",
|
|
@@ -164,6 +174,32 @@ class SpeechHttpController:
|
|
|
164
174
|
self.ports.write_json(handler, {"ok": False, "error": str(exc)}, 400)
|
|
165
175
|
return True
|
|
166
176
|
|
|
177
|
+
def _start_colab_action(self, handler: BaseHTTPRequestHandler, raw: bytes) -> bool:
|
|
178
|
+
try:
|
|
179
|
+
if self.ports.colab_action is None:
|
|
180
|
+
raise RuntimeError("Colab deployment jobs are unavailable")
|
|
181
|
+
body = json.loads(raw.decode("utf-8") if raw else "{}")
|
|
182
|
+
if not isinstance(body, dict):
|
|
183
|
+
raise ValueError("request must be a JSON object")
|
|
184
|
+
action = str(body.get("action") or "").strip().lower()
|
|
185
|
+
config = self._speech_config()
|
|
186
|
+
colab = config.get("colab") if isinstance(config.get("colab"), dict) else {}
|
|
187
|
+
if colab.get("enabled") is False:
|
|
188
|
+
raise ValueError("Colab worker management is disabled")
|
|
189
|
+
secrets_payload = body.get("secrets") if isinstance(body.get("secrets"), dict) else {}
|
|
190
|
+
secrets = {
|
|
191
|
+
"tailscale_auth_key": str(secrets_payload.get("tailscale_auth_key") or ""),
|
|
192
|
+
"speech_api_key": str(secrets_payload.get("speech_api_key") or ""),
|
|
193
|
+
"reset_authentication": "1" if body.get("reset_authentication") is True else "",
|
|
194
|
+
}
|
|
195
|
+
result = self.ports.colab_action(action, dict(colab), secrets)
|
|
196
|
+
self.ports.write_json(handler, result)
|
|
197
|
+
except (UnicodeError, json.JSONDecodeError, ValueError, TypeError) as exc:
|
|
198
|
+
self.ports.write_json(handler, {"ok": False, "error": str(exc)}, 400)
|
|
199
|
+
except RuntimeError as exc:
|
|
200
|
+
self.ports.write_json(handler, {"ok": False, "error": str(exc)}, 409)
|
|
201
|
+
return True
|
|
202
|
+
|
|
167
203
|
@staticmethod
|
|
168
204
|
def _validated_value(service: str, key: str, value: Any) -> Any:
|
|
169
205
|
allowed = {
|
|
@@ -215,6 +251,7 @@ class SpeechHttpController:
|
|
|
215
251
|
"enabled",
|
|
216
252
|
"distribution",
|
|
217
253
|
"auth",
|
|
254
|
+
"profile",
|
|
218
255
|
"asr_session",
|
|
219
256
|
"tts_session",
|
|
220
257
|
"asr_accelerator",
|
|
@@ -254,12 +254,18 @@ def render_web_chat_page(
|
|
|
254
254
|
<label class="check"><input id="colabEnabled" type="checkbox"> Manage workers with Colab CLI</label>
|
|
255
255
|
<label>WSL distribution<input id="colabDistribution" placeholder="Ubuntu-26.04"></label>
|
|
256
256
|
<label>Authentication<select id="colabAuth"><option value="adc">ADC</option><option value="oauth2">OAuth2</option></select></label>
|
|
257
|
+
<label>Account profile<input id="colabProfile" placeholder="default"></label>
|
|
257
258
|
<label>ASR session<input id="colabAsrSession" placeholder="ciel-asr"></label>
|
|
258
259
|
<label>TTS session<input id="colabTtsSession" placeholder="ciel-tts"></label>
|
|
259
260
|
<label>ASR GPU<select id="colabAsrAccelerator"><option>T4</option><option>L4</option><option>G4</option><option>A100</option><option>H100</option></select></label>
|
|
260
261
|
<label>TTS GPU<select id="colabTtsAccelerator"><option>T4</option><option>L4</option><option>G4</option><option>A100</option><option>H100</option></select></label>
|
|
262
|
+
<label class="wide">Tailscale auth key for this run<input id="colabTailscaleAuthKey" type="password" autocomplete="new-password" placeholder="Not saved; Colab Secret may be used instead"></label>
|
|
263
|
+
<label class="wide">Speech API key for this run<input id="colabSpeechApiKey" type="password" autocomplete="new-password" placeholder="Not saved; optional"></label>
|
|
264
|
+
<label class="check wide"><input id="colabResetAuthentication" type="checkbox"> Forget this profile's current login before generating a login command</label>
|
|
261
265
|
</div>
|
|
262
|
-
<div class="
|
|
266
|
+
<div class="settings-actions"><button class="ghost" id="colabLoginButton" type="button">Copy login command</button><button class="ghost" id="colabStatusButton" type="button">Check sessions</button><button class="ghost" id="colabStartButton" type="button">Start missing</button><button class="primary" id="colabDeployButton" type="button">Recover & deploy</button><button class="ghost" id="colabRecreateButton" type="button">Recreate all</button></div>
|
|
267
|
+
<pre class="hint" id="colabJobStatus">No Colab deployment job has been started.</pre>
|
|
268
|
+
<div class="hint">Named account profiles get an isolated WSL HOME, OAuth token, session state, and history; <code>default</code> keeps the existing Colab CLI login for compatibility. Login remains an interactive CLI step; deployment jobs run in the background. Ephemeral keys above are passed only to that job and are never saved by Ciel.</div>
|
|
263
269
|
</section>
|
|
264
270
|
<section class="settings-section">
|
|
265
271
|
<h3>Tailscale tunnel</h3>
|
|
@@ -747,10 +753,14 @@ def render_web_chat_page(
|
|
|
747
753
|
document.getElementById('colabEnabled').checked = colab.enabled !== false;
|
|
748
754
|
document.getElementById('colabDistribution').value = colab.distribution || 'Ubuntu-26.04';
|
|
749
755
|
document.getElementById('colabAuth').value = colab.auth || 'adc';
|
|
756
|
+
document.getElementById('colabProfile').value = colab.profile || 'default';
|
|
750
757
|
document.getElementById('colabAsrSession').value = colab.asr_session || 'ciel-asr';
|
|
751
758
|
document.getElementById('colabTtsSession').value = colab.tts_session || 'ciel-tts';
|
|
752
759
|
document.getElementById('colabAsrAccelerator').value = colab.asr_accelerator || 'T4';
|
|
753
760
|
document.getElementById('colabTtsAccelerator').value = colab.tts_accelerator || 'T4';
|
|
761
|
+
document.getElementById('colabTailscaleAuthKey').value = '';
|
|
762
|
+
document.getElementById('colabSpeechApiKey').value = '';
|
|
763
|
+
document.getElementById('colabResetAuthentication').checked = false;
|
|
754
764
|
document.getElementById('tailscaleEnabled').checked = tailscale.enabled !== false;
|
|
755
765
|
document.getElementById('tailscaleAsrHostname').value = tailscale.asr_hostname || 'ciel-asr';
|
|
756
766
|
document.getElementById('tailscaleTtsHostname').value = tailscale.tts_hostname || 'ciel-tts';
|
|
@@ -793,6 +803,7 @@ def render_web_chat_page(
|
|
|
793
803
|
enabled: document.getElementById('colabEnabled').checked,
|
|
794
804
|
distribution: document.getElementById('colabDistribution').value,
|
|
795
805
|
auth: document.getElementById('colabAuth').value,
|
|
806
|
+
profile: document.getElementById('colabProfile').value,
|
|
796
807
|
asr_session: document.getElementById('colabAsrSession').value,
|
|
797
808
|
tts_session: document.getElementById('colabTtsSession').value,
|
|
798
809
|
asr_accelerator: document.getElementById('colabAsrAccelerator').value,
|
|
@@ -811,6 +822,48 @@ def render_web_chat_page(
|
|
|
811
822
|
setSpeechForm(data);
|
|
812
823
|
return data;
|
|
813
824
|
}}
|
|
825
|
+
function renderColabJob(payload) {{
|
|
826
|
+
const output = document.getElementById('colabJobStatus');
|
|
827
|
+
const job = payload && payload.job;
|
|
828
|
+
if (!job) {{ output.textContent = 'No Colab deployment job has been started.'; return; }}
|
|
829
|
+
const state = job.running ? 'running' : job.return_code === 0 ? 'completed' : `failed (${{job.return_code}})`;
|
|
830
|
+
output.textContent = `${{job.action}} · profile ${{job.profile}} · ${{state}}\n${{job.output || ''}}`.trim();
|
|
831
|
+
output.scrollTop = output.scrollHeight;
|
|
832
|
+
}}
|
|
833
|
+
async function pollColabJob() {{
|
|
834
|
+
const response = await fetch('/ca/speech/colab/job', {{headers: {{'accept': 'application/json'}}, cache: 'no-store'}});
|
|
835
|
+
const data = await response.json();
|
|
836
|
+
renderColabJob(data);
|
|
837
|
+
if (data.job && data.job.running) setTimeout(() => pollColabJob().catch(() => {{}}), 2000);
|
|
838
|
+
else if (data.job) setState(data.job.return_code === 0 ? 'Colab job complete' : 'Colab job failed', data.job.return_code === 0 ? 'ok' : 'error');
|
|
839
|
+
return data;
|
|
840
|
+
}}
|
|
841
|
+
async function runColabAction(action) {{
|
|
842
|
+
await saveSpeechConfig();
|
|
843
|
+
const payload = {{
|
|
844
|
+
action,
|
|
845
|
+
reset_authentication: document.getElementById('colabResetAuthentication').checked,
|
|
846
|
+
secrets: {{
|
|
847
|
+
tailscale_auth_key: document.getElementById('colabTailscaleAuthKey').value,
|
|
848
|
+
speech_api_key: document.getElementById('colabSpeechApiKey').value,
|
|
849
|
+
}},
|
|
850
|
+
}};
|
|
851
|
+
const response = await fetch('/ca/speech/colab/action', {{method: 'POST', headers: {{'content-type': 'application/json', 'accept': 'application/json'}}, body: JSON.stringify(payload)}});
|
|
852
|
+
const data = await response.json();
|
|
853
|
+
document.getElementById('colabTailscaleAuthKey').value = '';
|
|
854
|
+
document.getElementById('colabSpeechApiKey').value = '';
|
|
855
|
+
if (!response.ok || !data.ok) throw new Error(data.error || `HTTP ${{response.status}}`);
|
|
856
|
+
if (data.requires_terminal) {{
|
|
857
|
+
await navigator.clipboard.writeText(data.command);
|
|
858
|
+
document.getElementById('colabJobStatus').textContent = 'Login command copied. Run it in a local terminal and complete authentication for the selected Google account.\\n\\n' + data.command;
|
|
859
|
+
setState('login command copied', 'ok');
|
|
860
|
+
return data;
|
|
861
|
+
}}
|
|
862
|
+
renderColabJob(data);
|
|
863
|
+
setState(`Colab ${{action}} started`, 'ok');
|
|
864
|
+
setTimeout(() => pollColabJob().catch(() => {{}}), 1000);
|
|
865
|
+
return data;
|
|
866
|
+
}}
|
|
814
867
|
function stopActiveSpeech() {{
|
|
815
868
|
if (speechGenerationController) speechGenerationController.abort();
|
|
816
869
|
speechGenerationController = null;
|
|
@@ -1276,6 +1329,14 @@ def render_web_chat_page(
|
|
|
1276
1329
|
speechSettingsDialog.showModal();
|
|
1277
1330
|
}});
|
|
1278
1331
|
speechSettingsClose.addEventListener('click', () => speechSettingsDialog.close());
|
|
1332
|
+
document.getElementById('colabLoginButton').addEventListener('click', () => runColabAction('login').catch(err => addBubble('system', 'Colab login command failed: ' + String(err && err.message ? err.message : err))));
|
|
1333
|
+
document.getElementById('colabStatusButton').addEventListener('click', () => runColabAction('status').catch(err => addBubble('system', 'Colab status failed: ' + String(err && err.message ? err.message : err))));
|
|
1334
|
+
document.getElementById('colabStartButton').addEventListener('click', () => runColabAction('start').catch(err => addBubble('system', 'Colab start failed: ' + String(err && err.message ? err.message : err))));
|
|
1335
|
+
document.getElementById('colabDeployButton').addEventListener('click', () => runColabAction('deploy').catch(err => addBubble('system', 'Colab deployment failed: ' + String(err && err.message ? err.message : err))));
|
|
1336
|
+
document.getElementById('colabRecreateButton').addEventListener('click', () => {{
|
|
1337
|
+
if (!confirm('Release both sessions in this account profile, create new instances, and redeploy ASR/TTS?')) return;
|
|
1338
|
+
runColabAction('recreate').catch(err => addBubble('system', 'Colab recreation failed: ' + String(err && err.message ? err.message : err)));
|
|
1339
|
+
}});
|
|
1279
1340
|
document.getElementById('ttsReferenceAudio').addEventListener('change', async event => {{
|
|
1280
1341
|
const file = event.target.files && event.target.files[0];
|
|
1281
1342
|
if (!file) return;
|
package/docs/COLAB_SPEECH.md
CHANGED
|
@@ -20,6 +20,24 @@ Set the WSL distribution, authentication mode, ASR/TTS session names, and accele
|
|
|
20
20
|
|
|
21
21
|
The script reuses matching active sessions when possible, otherwise creates them, installs Qwen3-ASR-0.6B and MOSS-TTS-Nano, starts Tailscale in userspace networking mode, publishes each localhost model server with Tailscale Serve, and saves both returned `base_url` values into Web Chat > Speech Settings automatically.
|
|
22
22
|
|
|
23
|
+
### Session recovery and account profiles
|
|
24
|
+
|
|
25
|
+
Web Chat > Speech Settings exposes **Check sessions**, **Start missing**, **Recover & deploy**, and **Recreate all**. Start creates only missing or expired ASR/TTS sessions. Recover creates missing sessions and then runs both bootstrap scripts. Recreate explicitly releases both sessions before creating and deploying replacements. These actions are also available through `POST /ca/speech/colab/action`, while `GET /ca/speech/colab/job` returns the latest background-job state and redacted output.
|
|
26
|
+
|
|
27
|
+
The `default` account profile reuses the existing WSL Colab CLI login. Every other profile name receives an isolated WSL `HOME`, so its OAuth token, ADC credentials, session state, and history cannot mix with another Google account. Choose OAuth2 for the simplest multi-account flow, click **Copy login command**, run that command in a local terminal, and complete the copy/paste authorization prompt with the intended Google account. The optional reset checkbox removes credentials only inside the selected profile before login.
|
|
28
|
+
|
|
29
|
+
Equivalent CLI actions are:
|
|
30
|
+
|
|
31
|
+
```powershell
|
|
32
|
+
.\scripts\deploy_colab_speech.ps1 -Action Login -Profile second-account -ColabAuth oauth2
|
|
33
|
+
.\scripts\deploy_colab_speech.ps1 -Action Status -Profile second-account
|
|
34
|
+
.\scripts\deploy_colab_speech.ps1 -Action Start -Profile second-account
|
|
35
|
+
.\scripts\deploy_colab_speech.ps1 -Action Deploy -Profile second-account
|
|
36
|
+
.\scripts\deploy_colab_speech.ps1 -Action Recreate -Profile second-account
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Tailscale and speech API keys entered in Web Chat are passed only to the selected background deployment process and are not persisted. They can instead be stored as authorized Colab Secrets for the selected Google account.
|
|
40
|
+
|
|
23
41
|
MOSS-TTS-Nano is a voice-cloning model without built-in speakers. Deployment configures the project's official `zh_1.wav` sample so the first request works immediately. In Web Chat > Speech Settings, upload a reference voice clip (10 MB maximum) to replace it. Ciel stores uploaded audio only in the local protected runtime configuration, omits it from configuration responses, and adds it to TTS requests automatically. API clients can instead pass `ref_audio` as an HTTP(S) URL or base64 audio data URL to `POST /v1/audio/speech`.
|
|
24
42
|
|
|
25
43
|
Colab sessions are ephemeral. Re-run the bootstrap after a runtime reset. The workers are reachable only by devices in the same tailnet unless an administrator separately enables Tailscale Funnel.
|
|
@@ -40,6 +58,8 @@ Web backend ownership is scoped to the normalized workspace and router port. A s
|
|
|
40
58
|
|
|
41
59
|
- `GET|POST /ca/speech/config`
|
|
42
60
|
- `GET /ca/speech/health`
|
|
61
|
+
- `POST /ca/speech/colab/action`
|
|
62
|
+
- `GET /ca/speech/colab/job`
|
|
43
63
|
- `POST /v1/audio/transcriptions`
|
|
44
64
|
- `POST /v1/audio/translations`
|
|
45
65
|
- `POST /v1/audio/speech`
|
package/install.ps1
CHANGED
|
@@ -12,6 +12,11 @@ if (Test-Path $supportDir) {
|
|
|
12
12
|
Remove-Item -Recurse -Force $supportDir
|
|
13
13
|
}
|
|
14
14
|
Copy-Item -Recurse -Force "ciel_runtime_support" $supportDir
|
|
15
|
+
$scriptsDir = Join-Path $shareDir "scripts"
|
|
16
|
+
if (Test-Path $scriptsDir) {
|
|
17
|
+
Remove-Item -Recurse -Force $scriptsDir
|
|
18
|
+
}
|
|
19
|
+
Copy-Item -Recurse -Force "scripts" $scriptsDir
|
|
15
20
|
Copy-Item -Force "ciel-runtime-menu.py" (Join-Path $binDir "ciel-runtime-menu.py")
|
|
16
21
|
Copy-Item -Force "ciel-runtime-tool-guard.py" (Join-Path $binDir "ciel-runtime-tool-guard.py")
|
|
17
22
|
Copy-Item -Force "ciel-runtime" (Join-Path $binDir "ciel-runtime")
|
package/install.sh
CHANGED
|
@@ -11,6 +11,9 @@ install -m 755 ciel_runtime.py "$SHARE_DIR/ciel_runtime.py"
|
|
|
11
11
|
rm -rf "$SHARE_DIR/ciel_runtime_support"
|
|
12
12
|
mkdir -p "$SHARE_DIR/ciel_runtime_support"
|
|
13
13
|
cp -R ciel_runtime_support/. "$SHARE_DIR/ciel_runtime_support/"
|
|
14
|
+
rm -rf "$SHARE_DIR/scripts"
|
|
15
|
+
mkdir -p "$SHARE_DIR/scripts"
|
|
16
|
+
cp -R scripts/. "$SHARE_DIR/scripts/"
|
|
14
17
|
install -m 755 ciel-runtime-menu.py "$BIN_DIR/ciel-runtime-menu"
|
|
15
18
|
install -m 755 ciel-runtime-tool-guard.py "$BIN_DIR/ciel-runtime-tool-guard"
|
|
16
19
|
install -m 755 ciel-runtime "$BIN_DIR/ciel-runtime"
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
@@ -16,6 +16,7 @@ DEFAULT_COLAB_SETTINGS: dict[str, Any] = {
|
|
|
16
16
|
"enabled": True,
|
|
17
17
|
"distribution": "Ubuntu-26.04",
|
|
18
18
|
"auth": "adc",
|
|
19
|
+
"profile": "default",
|
|
19
20
|
"asr_session": "ciel-asr",
|
|
20
21
|
"tts_session": "ciel-tts",
|
|
21
22
|
"asr_accelerator": "T4",
|
|
@@ -39,6 +40,7 @@ def configure(
|
|
|
39
40
|
*,
|
|
40
41
|
distribution: str | None = None,
|
|
41
42
|
auth: str | None = None,
|
|
43
|
+
profile: str | None = None,
|
|
42
44
|
asr_session: str | None = None,
|
|
43
45
|
tts_session: str | None = None,
|
|
44
46
|
asr_accelerator: str | None = None,
|
|
@@ -54,6 +56,7 @@ def configure(
|
|
|
54
56
|
overrides = {
|
|
55
57
|
"distribution": distribution,
|
|
56
58
|
"auth": auth,
|
|
59
|
+
"profile": profile,
|
|
57
60
|
"asr_session": asr_session,
|
|
58
61
|
"tts_session": tts_session,
|
|
59
62
|
"asr_accelerator": asr_accelerator,
|
|
@@ -77,6 +80,7 @@ def main() -> int:
|
|
|
77
80
|
parser.add_argument("--tts-reference-audio", default=DEFAULT_TTS_REFERENCE_AUDIO)
|
|
78
81
|
parser.add_argument("--distribution")
|
|
79
82
|
parser.add_argument("--auth", choices=("adc", "oauth2"))
|
|
83
|
+
parser.add_argument("--profile")
|
|
80
84
|
parser.add_argument("--asr-session")
|
|
81
85
|
parser.add_argument("--tts-session")
|
|
82
86
|
parser.add_argument("--asr-accelerator")
|
|
@@ -94,6 +98,7 @@ def main() -> int:
|
|
|
94
98
|
args.tts_reference_audio,
|
|
95
99
|
distribution=args.distribution,
|
|
96
100
|
auth=args.auth,
|
|
101
|
+
profile=args.profile,
|
|
97
102
|
asr_session=args.asr_session,
|
|
98
103
|
tts_session=args.tts_session,
|
|
99
104
|
asr_accelerator=args.asr_accelerator,
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
param(
|
|
2
|
+
[ValidateSet('Login', 'Status', 'Start', 'Deploy', 'Recreate')]
|
|
3
|
+
[string]$Action = 'Deploy',
|
|
4
|
+
[string]$Profile,
|
|
5
|
+
[switch]$ResetAuthentication,
|
|
2
6
|
[string]$Distribution,
|
|
3
7
|
[string]$ColabAuth,
|
|
4
8
|
[string]$AsrSession,
|
|
@@ -15,12 +19,15 @@ $settings = $settingsJson | ConvertFrom-Json
|
|
|
15
19
|
if ($settings.enabled -eq $false) { throw "Colab worker management is disabled in Web Chat > Speech Settings." }
|
|
16
20
|
if ([string]::IsNullOrWhiteSpace($Distribution)) { $Distribution = [string]$settings.distribution }
|
|
17
21
|
if ([string]::IsNullOrWhiteSpace($ColabAuth)) { $ColabAuth = [string]$settings.auth }
|
|
22
|
+
if ([string]::IsNullOrWhiteSpace($Profile)) { $Profile = [string]$settings.profile }
|
|
23
|
+
if ([string]::IsNullOrWhiteSpace($Profile)) { $Profile = "default" }
|
|
18
24
|
if ([string]::IsNullOrWhiteSpace($AsrSession)) { $AsrSession = [string]$settings.asr_session }
|
|
19
25
|
if ([string]::IsNullOrWhiteSpace($TtsSession)) { $TtsSession = [string]$settings.tts_session }
|
|
20
26
|
if ([string]::IsNullOrWhiteSpace($AsrAccelerator)) { $AsrAccelerator = [string]$settings.asr_accelerator }
|
|
21
27
|
if ([string]::IsNullOrWhiteSpace($TtsAccelerator)) { $TtsAccelerator = [string]$settings.tts_accelerator }
|
|
22
28
|
if ($Distribution -notmatch '^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$') { throw "Invalid WSL distribution name." }
|
|
23
29
|
if ($ColabAuth -notin @('adc', 'oauth2')) { throw "ColabAuth must be adc or oauth2." }
|
|
30
|
+
if ($Profile -notmatch '^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$') { throw "Invalid Colab account profile name." }
|
|
24
31
|
foreach ($session in @($AsrSession, $TtsSession)) {
|
|
25
32
|
if ($session -notmatch '^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$') { throw "Invalid Colab session name: $session" }
|
|
26
33
|
}
|
|
@@ -29,42 +36,98 @@ foreach ($accelerator in @($AsrAccelerator, $TtsAccelerator)) {
|
|
|
29
36
|
}
|
|
30
37
|
$wslRepo = (& wsl -d $Distribution -- wslpath -a ($repo -replace '\\', '/')).Trim()
|
|
31
38
|
if (-not $wslRepo) { throw "Could not resolve the repository path in WSL." }
|
|
32
|
-
$
|
|
39
|
+
$wslHome = (& wsl -d $Distribution -- bash -lc 'printf %s "$HOME"').Trim()
|
|
40
|
+
$colabExecutable = (& wsl -d $Distribution -- bash -lc 'command -v colab').Trim()
|
|
41
|
+
if (-not $wslHome -or -not $colabExecutable) { throw "Could not locate the WSL home directory or Colab CLI." }
|
|
42
|
+
$profileHome = if ($Profile -eq 'default') { $wslHome } else { "$wslHome/.config/ciel-runtime/colab-profiles/$Profile" }
|
|
43
|
+
& wsl -d $Distribution -- mkdir -p $profileHome
|
|
44
|
+
if ($LASTEXITCODE -ne 0) { throw "Could not create the Colab account profile directory." }
|
|
45
|
+
|
|
46
|
+
function Invoke-Colab([string[]]$Arguments) {
|
|
47
|
+
& wsl -d $Distribution -- env "HOME=$profileHome" $colabExecutable --auth $ColabAuth @Arguments
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if ($Action -eq 'Login') {
|
|
51
|
+
if ($ResetAuthentication) {
|
|
52
|
+
$tokenPath = "$profileHome/.config/colab-cli/token.json"
|
|
53
|
+
$adcPath = "$profileHome/.config/gcloud/application_default_credentials.json"
|
|
54
|
+
& wsl -d $Distribution -- rm -f $tokenPath $adcPath
|
|
55
|
+
if ($LASTEXITCODE -ne 0) { throw "Could not reset authentication for profile '$Profile'." }
|
|
56
|
+
}
|
|
57
|
+
Write-Host "Authenticating isolated Colab account profile: $Profile ($ColabAuth)"
|
|
58
|
+
if ($ColabAuth -eq 'adc') {
|
|
59
|
+
$scopes = 'openid,https://www.googleapis.com/auth/cloud-platform,https://www.googleapis.com/auth/userinfo.email,https://www.googleapis.com/auth/colaboratory'
|
|
60
|
+
& wsl -d $Distribution -- env "HOME=$profileHome" gcloud auth application-default login --scopes=$scopes
|
|
61
|
+
} else {
|
|
62
|
+
Invoke-Colab @('sessions')
|
|
63
|
+
}
|
|
64
|
+
if ($LASTEXITCODE -ne 0) { throw "Colab authentication failed for profile '$Profile'." }
|
|
65
|
+
Write-Host "Colab profile '$Profile' is authenticated."
|
|
66
|
+
exit 0
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if ($Action -eq 'Status') {
|
|
70
|
+
Write-Host "Colab sessions for isolated account profile: $Profile"
|
|
71
|
+
Invoke-Colab @('sessions')
|
|
72
|
+
if ($LASTEXITCODE -ne 0) { throw "Could not read Colab sessions for profile '$Profile'. Run the Login action first." }
|
|
73
|
+
exit 0
|
|
74
|
+
}
|
|
33
75
|
if ($env:TAILSCALE_AUTHKEY) {
|
|
34
76
|
if ($env:TAILSCALE_AUTHKEY -notmatch '^tskey-[A-Za-z0-9_-]+$') { throw "TAILSCALE_AUTHKEY has an unexpected format." }
|
|
35
|
-
$bootstrapEnv += " --env TAILSCALE_AUTHKEY=$($env:TAILSCALE_AUTHKEY)"
|
|
36
77
|
}
|
|
37
78
|
if ($env:CIEL_SPEECH_API_KEY) {
|
|
38
79
|
if ($env:CIEL_SPEECH_API_KEY -match '[\s''"]') { throw "CIEL_SPEECH_API_KEY cannot contain whitespace or quotes for CLI deployment." }
|
|
39
|
-
$bootstrapEnv += " --env CIEL_SPEECH_API_KEY=$($env:CIEL_SPEECH_API_KEY)"
|
|
40
80
|
}
|
|
41
81
|
|
|
42
82
|
Write-Host "Checking Colab CLI authentication..."
|
|
43
|
-
|
|
83
|
+
Invoke-Colab @('sessions') | Out-Null
|
|
44
84
|
if ($LASTEXITCODE -ne 0) {
|
|
45
|
-
throw "Colab
|
|
85
|
+
throw "Colab profile '$Profile' is not authenticated with '$ColabAuth' in WSL '$Distribution'. Run -Action Login -Profile '$Profile' first."
|
|
46
86
|
}
|
|
47
87
|
|
|
48
88
|
function Ensure-ColabSession([string]$Session, [string]$Accelerator, [string]$Role) {
|
|
49
|
-
|
|
89
|
+
Invoke-Colab @('status', '--session', $Session) *> $null
|
|
50
90
|
if ($LASTEXITCODE -eq 0) {
|
|
51
91
|
Write-Host "Reusing $Role $Accelerator session: $Session"
|
|
52
92
|
return
|
|
53
93
|
}
|
|
54
94
|
Write-Host "Creating $Role $Accelerator session: $Session"
|
|
55
|
-
|
|
95
|
+
Invoke-Colab @('new', '--gpu', $Accelerator, '--session', $Session)
|
|
56
96
|
if ($LASTEXITCODE -ne 0) { throw "Could not create $Role Colab session." }
|
|
57
97
|
}
|
|
58
98
|
|
|
99
|
+
function Stop-ColabSession([string]$Session, [string]$Role) {
|
|
100
|
+
Write-Host "Releasing existing $Role session if present: $Session"
|
|
101
|
+
Invoke-Colab @('stop', '--session', $Session) *> $null
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if ($Action -eq 'Recreate') {
|
|
105
|
+
Stop-ColabSession $AsrSession "ASR"
|
|
106
|
+
Stop-ColabSession $TtsSession "TTS"
|
|
107
|
+
}
|
|
108
|
+
|
|
59
109
|
Ensure-ColabSession $AsrSession $AsrAccelerator "ASR"
|
|
60
110
|
Ensure-ColabSession $TtsSession $TtsAccelerator "TTS"
|
|
61
111
|
|
|
112
|
+
if ($Action -eq 'Start') {
|
|
113
|
+
Write-Host "Colab sessions are allocated for profile '$Profile'. Run -Action Deploy to install and connect the workers."
|
|
114
|
+
exit 0
|
|
115
|
+
}
|
|
116
|
+
|
|
62
117
|
Write-Host "Installing Qwen3-ASR and its Tailscale service..."
|
|
63
|
-
$
|
|
118
|
+
$asrArguments = @('exec', '--session', $AsrSession)
|
|
119
|
+
if ($env:TAILSCALE_AUTHKEY) { $asrArguments += @('--env', "TAILSCALE_AUTHKEY=$($env:TAILSCALE_AUTHKEY)") }
|
|
120
|
+
if ($env:CIEL_SPEECH_API_KEY) { $asrArguments += @('--env', "CIEL_SPEECH_API_KEY=$($env:CIEL_SPEECH_API_KEY)") }
|
|
121
|
+
$asrArguments += @('--file', "$wslRepo/scripts/colab/bootstrap_qwen_asr.py")
|
|
122
|
+
$asrOutput = (Invoke-Colab $asrArguments 2>&1 | Tee-Object -Variable asrDisplay) -join "`n"
|
|
64
123
|
if ($LASTEXITCODE -ne 0) { throw "ASR bootstrap failed." }
|
|
65
124
|
|
|
66
125
|
Write-Host "Installing MOSS-TTS-Nano and its Tailscale service..."
|
|
67
|
-
$
|
|
126
|
+
$ttsArguments = @('exec', '--session', $TtsSession)
|
|
127
|
+
if ($env:TAILSCALE_AUTHKEY) { $ttsArguments += @('--env', "TAILSCALE_AUTHKEY=$($env:TAILSCALE_AUTHKEY)") }
|
|
128
|
+
if ($env:CIEL_SPEECH_API_KEY) { $ttsArguments += @('--env', "CIEL_SPEECH_API_KEY=$($env:CIEL_SPEECH_API_KEY)") }
|
|
129
|
+
$ttsArguments += @('--file', "$wslRepo/scripts/colab/bootstrap_moss_tts.py")
|
|
130
|
+
$ttsOutput = (Invoke-Colab $ttsArguments 2>&1 | Tee-Object -Variable ttsDisplay) -join "`n"
|
|
68
131
|
if ($LASTEXITCODE -ne 0) { throw "TTS bootstrap failed." }
|
|
69
132
|
|
|
70
133
|
function Read-BootstrapResult([string]$Text, [string]$Role) {
|
|
@@ -77,7 +140,7 @@ function Read-BootstrapResult([string]$Text, [string]$Role) {
|
|
|
77
140
|
|
|
78
141
|
$asr = Read-BootstrapResult $asrOutput "asr"
|
|
79
142
|
$tts = Read-BootstrapResult $ttsOutput "tts"
|
|
80
|
-
& python (Join-Path $PSScriptRoot "configure_speech_workers.py") --asr-base-url $asr.base_url --tts-base-url $tts.base_url --distribution $Distribution --auth $ColabAuth --asr-session $AsrSession --tts-session $TtsSession --asr-accelerator $AsrAccelerator --tts-accelerator $TtsAccelerator
|
|
143
|
+
& python (Join-Path $PSScriptRoot "configure_speech_workers.py") --asr-base-url $asr.base_url --tts-base-url $tts.base_url --distribution $Distribution --auth $ColabAuth --profile $Profile --asr-session $AsrSession --tts-session $TtsSession --asr-accelerator $AsrAccelerator --tts-accelerator $TtsAccelerator
|
|
81
144
|
if ($LASTEXITCODE -ne 0) { throw "Workers started, but Ciel speech configuration failed." }
|
|
82
145
|
|
|
83
146
|
Write-Host "Both services are running and connected to Web Chat > Speech Settings."
|