@oneciel-ai/ciel-runtime 0.2.4 → 0.2.6
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_support/config_repository.py +11 -0
- package/ciel_runtime_support/runtime_constants.py +1 -1
- package/ciel_runtime_support/speech_http_controller.py +68 -4
- package/ciel_runtime_support/web_ui.py +62 -0
- package/docs/COLAB_SPEECH.md +7 -3
- 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/colab/bootstrap_moss_tts.py +66 -22
- package/scripts/colab/bootstrap_qwen_asr.py +15 -5
- package/scripts/configure_speech_workers.py +75 -5
- package/scripts/deploy_colab_speech.ps1 +50 -14
|
@@ -27,6 +27,15 @@ def build_default_config(provider_defaults: dict[str, Any]) -> dict[str, Any]:
|
|
|
27
27
|
"tailscale_https": False,
|
|
28
28
|
},
|
|
29
29
|
"speech": {
|
|
30
|
+
"colab": {
|
|
31
|
+
"enabled": True,
|
|
32
|
+
"distribution": "Ubuntu-26.04",
|
|
33
|
+
"auth": "adc",
|
|
34
|
+
"asr_session": "ciel-asr",
|
|
35
|
+
"tts_session": "ciel-tts",
|
|
36
|
+
"asr_accelerator": "T4",
|
|
37
|
+
"tts_accelerator": "T4",
|
|
38
|
+
},
|
|
30
39
|
"asr": {
|
|
31
40
|
"enabled": False,
|
|
32
41
|
"base_url": "http://ciel-asr:8000",
|
|
@@ -44,6 +53,8 @@ def build_default_config(provider_defaults: dict[str, Any]) -> dict[str, Any]:
|
|
|
44
53
|
"model": "OpenMOSS-Team/MOSS-TTS-Nano",
|
|
45
54
|
"voice": "default",
|
|
46
55
|
"language": "ko",
|
|
56
|
+
"ref_audio": "",
|
|
57
|
+
"ref_text": "",
|
|
47
58
|
"response_format": "wav",
|
|
48
59
|
"speed": 1.0,
|
|
49
60
|
"auto_speak": False,
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import base64
|
|
6
6
|
import json
|
|
7
|
+
import re
|
|
7
8
|
import secrets
|
|
8
9
|
import urllib.error
|
|
9
10
|
import urllib.parse
|
|
@@ -65,11 +66,15 @@ class SpeechHttpController:
|
|
|
65
66
|
public: dict[str, Any] = {"ok": True}
|
|
66
67
|
for name in ("asr", "tts"):
|
|
67
68
|
source = speech.get(name) if isinstance(speech.get(name), dict) else {}
|
|
68
|
-
item = {key: value for key, value in source.items() if key
|
|
69
|
+
item = {key: value for key, value in source.items() if key not in {"api_key", "ref_audio"}}
|
|
69
70
|
item["api_key_set"] = bool(str(source.get("api_key") or "").strip())
|
|
71
|
+
if name == "tts":
|
|
72
|
+
item["ref_audio_set"] = bool(str(source.get("ref_audio") or "").strip())
|
|
70
73
|
public[name] = item
|
|
71
74
|
tailscale = speech.get("tailscale")
|
|
72
75
|
public["tailscale"] = dict(tailscale) if isinstance(tailscale, dict) else {}
|
|
76
|
+
colab = speech.get("colab")
|
|
77
|
+
public["colab"] = dict(colab) if isinstance(colab, dict) else {}
|
|
73
78
|
public["endpoints"] = self.discovery_payload()["endpoints"]
|
|
74
79
|
return public
|
|
75
80
|
|
|
@@ -127,13 +132,15 @@ class SpeechHttpController:
|
|
|
127
132
|
current = {}
|
|
128
133
|
speech[name] = current
|
|
129
134
|
for key, value in incoming.items():
|
|
130
|
-
if key in {"api_key_set", "clear_api_key"}:
|
|
135
|
+
if key in {"api_key_set", "clear_api_key", "ref_audio_set", "clear_ref_audio"}:
|
|
131
136
|
continue
|
|
132
|
-
if key
|
|
137
|
+
if key in {"api_key", "ref_audio"} and not str(value or "").strip():
|
|
133
138
|
continue
|
|
134
139
|
current[key] = self._validated_value(name, key, value)
|
|
135
140
|
if incoming.get("clear_api_key") is True:
|
|
136
141
|
current["api_key"] = ""
|
|
142
|
+
if name == "tts" and incoming.get("clear_ref_audio") is True:
|
|
143
|
+
current["ref_audio"] = ""
|
|
137
144
|
tailscale = update.get("tailscale")
|
|
138
145
|
if isinstance(tailscale, dict):
|
|
139
146
|
current_tailscale = speech.setdefault("tailscale", {})
|
|
@@ -143,6 +150,14 @@ class SpeechHttpController:
|
|
|
143
150
|
for key in ("enabled", "asr_hostname", "tts_hostname"):
|
|
144
151
|
if key in tailscale:
|
|
145
152
|
current_tailscale[key] = tailscale[key]
|
|
153
|
+
colab = update.get("colab")
|
|
154
|
+
if isinstance(colab, dict):
|
|
155
|
+
current_colab = speech.setdefault("colab", {})
|
|
156
|
+
if not isinstance(current_colab, dict):
|
|
157
|
+
current_colab = {}
|
|
158
|
+
speech["colab"] = current_colab
|
|
159
|
+
for key, value in colab.items():
|
|
160
|
+
current_colab[key] = self._validated_colab_value(key, value)
|
|
146
161
|
self.ports.save_config(config)
|
|
147
162
|
self.ports.write_json(handler, self.public_config())
|
|
148
163
|
except (UnicodeError, ValueError, TypeError) as exc:
|
|
@@ -153,7 +168,7 @@ class SpeechHttpController:
|
|
|
153
168
|
def _validated_value(service: str, key: str, value: Any) -> Any:
|
|
154
169
|
allowed = {
|
|
155
170
|
"asr": {"enabled", "base_url", "endpoint", "model", "language", "api_key", "timeout_seconds"},
|
|
156
|
-
"tts": {"enabled", "base_url", "endpoint", "voices_endpoint", "model", "voice", "language", "response_format", "speed", "auto_speak", "api_key", "timeout_seconds"},
|
|
171
|
+
"tts": {"enabled", "base_url", "endpoint", "voices_endpoint", "model", "voice", "language", "ref_audio", "ref_text", "response_format", "speed", "auto_speak", "api_key", "timeout_seconds"},
|
|
157
172
|
}
|
|
158
173
|
if key not in allowed[service]:
|
|
159
174
|
raise ValueError(f"unsupported {service} setting: {key}")
|
|
@@ -164,6 +179,21 @@ class SpeechHttpController:
|
|
|
164
179
|
if key == "speed":
|
|
165
180
|
return max(0.25, min(4.0, float(value)))
|
|
166
181
|
text = str(value or "").strip()
|
|
182
|
+
if key == "ref_audio":
|
|
183
|
+
if len(text) > 14_000_000:
|
|
184
|
+
raise ValueError("TTS reference audio must be 10 MB or smaller")
|
|
185
|
+
if text.startswith("data:audio/") and ";base64," in text:
|
|
186
|
+
try:
|
|
187
|
+
audio = base64.b64decode(text.split(",", 1)[1], validate=True)
|
|
188
|
+
except (ValueError, TypeError) as exc:
|
|
189
|
+
raise ValueError("invalid base64 TTS reference audio") from exc
|
|
190
|
+
if not audio or len(audio) > 10 * 1024 * 1024:
|
|
191
|
+
raise ValueError("TTS reference audio must be between 1 byte and 10 MB")
|
|
192
|
+
return text
|
|
193
|
+
parsed_ref = urllib.parse.urlparse(text)
|
|
194
|
+
if parsed_ref.scheme not in {"http", "https"} or not parsed_ref.netloc:
|
|
195
|
+
raise ValueError("TTS ref_audio must be an audio data URL or HTTP(S) URL")
|
|
196
|
+
return text
|
|
167
197
|
if key == "base_url":
|
|
168
198
|
parsed = urllib.parse.urlparse(text)
|
|
169
199
|
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
|
|
@@ -173,6 +203,36 @@ class SpeechHttpController:
|
|
|
173
203
|
raise ValueError(f"{service} {key} must begin with /")
|
|
174
204
|
return text
|
|
175
205
|
|
|
206
|
+
@staticmethod
|
|
207
|
+
def _validated_colab_value(key: str, value: Any) -> Any:
|
|
208
|
+
allowed = {
|
|
209
|
+
"enabled",
|
|
210
|
+
"distribution",
|
|
211
|
+
"auth",
|
|
212
|
+
"asr_session",
|
|
213
|
+
"tts_session",
|
|
214
|
+
"asr_accelerator",
|
|
215
|
+
"tts_accelerator",
|
|
216
|
+
}
|
|
217
|
+
if key not in allowed:
|
|
218
|
+
raise ValueError(f"unsupported colab setting: {key}")
|
|
219
|
+
if key == "enabled":
|
|
220
|
+
return bool(value)
|
|
221
|
+
text = str(value or "").strip()
|
|
222
|
+
if key == "auth":
|
|
223
|
+
auth = text.lower()
|
|
224
|
+
if auth not in {"adc", "oauth2"}:
|
|
225
|
+
raise ValueError("Colab auth must be adc or oauth2")
|
|
226
|
+
return auth
|
|
227
|
+
if key.endswith("_accelerator"):
|
|
228
|
+
accelerator = text.upper()
|
|
229
|
+
if accelerator not in {"T4", "L4", "G4", "A100", "H100"}:
|
|
230
|
+
raise ValueError("unsupported Colab accelerator")
|
|
231
|
+
return accelerator
|
|
232
|
+
if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]{0,127}", text):
|
|
233
|
+
raise ValueError(f"invalid Colab {key}")
|
|
234
|
+
return text
|
|
235
|
+
|
|
176
236
|
def _probe(self, name: str) -> dict[str, Any]:
|
|
177
237
|
config = self._service_config(name)
|
|
178
238
|
enabled = bool(config.get("enabled"))
|
|
@@ -237,6 +297,10 @@ class SpeechHttpController:
|
|
|
237
297
|
body.setdefault("model", str(config.get("model") or ""))
|
|
238
298
|
body.setdefault("voice", str(config.get("voice") or "default"))
|
|
239
299
|
body.setdefault("language", str(config.get("language") or "Auto"))
|
|
300
|
+
if str(config.get("ref_audio") or "").strip():
|
|
301
|
+
body.setdefault("ref_audio", str(config["ref_audio"]))
|
|
302
|
+
if str(config.get("ref_text") or "").strip():
|
|
303
|
+
body.setdefault("ref_text", str(config["ref_text"]))
|
|
240
304
|
body.setdefault("response_format", str(config.get("response_format") or "wav"))
|
|
241
305
|
body.setdefault("speed", float(config.get("speed") or 1.0))
|
|
242
306
|
raw = json.dumps(body, ensure_ascii=False).encode("utf-8")
|
|
@@ -224,9 +224,25 @@ def render_web_chat_page(
|
|
|
224
224
|
<label>Voice<input id="ttsVoice" placeholder="default"></label>
|
|
225
225
|
<label>Language<input id="ttsLanguage" placeholder="ko"></label>
|
|
226
226
|
<label class="wide">Model<input id="ttsModel" placeholder="OpenMOSS-Team/MOSS-TTS-Nano"></label>
|
|
227
|
+
<label class="wide">Reference voice (required by MOSS-TTS-Nano)<input id="ttsReferenceAudio" type="file" accept="audio/*"><span class="hint" id="ttsReferenceAudioStatus">No reference voice configured</span></label>
|
|
228
|
+
<label class="wide">Reference transcript (optional)<input id="ttsReferenceText" placeholder="Transcript of the reference clip"></label>
|
|
229
|
+
<label class="check wide"><input id="ttsClearReferenceAudio" type="checkbox"> Remove the saved reference voice</label>
|
|
227
230
|
<label class="wide">Remote bearer token<input id="ttsApiKey" type="password" autocomplete="new-password" placeholder="Leave blank to keep current token"></label>
|
|
228
231
|
</div>
|
|
229
232
|
</section>
|
|
233
|
+
<section class="settings-section">
|
|
234
|
+
<h3>Colab CLI connection</h3>
|
|
235
|
+
<div class="settings-grid">
|
|
236
|
+
<label class="check"><input id="colabEnabled" type="checkbox"> Manage workers with Colab CLI</label>
|
|
237
|
+
<label>WSL distribution<input id="colabDistribution" placeholder="Ubuntu-26.04"></label>
|
|
238
|
+
<label>Authentication<select id="colabAuth"><option value="adc">ADC</option><option value="oauth2">OAuth2</option></select></label>
|
|
239
|
+
<label>ASR session<input id="colabAsrSession" placeholder="ciel-asr"></label>
|
|
240
|
+
<label>TTS session<input id="colabTtsSession" placeholder="ciel-tts"></label>
|
|
241
|
+
<label>ASR GPU<select id="colabAsrAccelerator"><option>T4</option><option>L4</option><option>G4</option><option>A100</option><option>H100</option></select></label>
|
|
242
|
+
<label>TTS GPU<select id="colabTtsAccelerator"><option>T4</option><option>L4</option><option>G4</option><option>A100</option><option>H100</option></select></label>
|
|
243
|
+
</div>
|
|
244
|
+
<div class="hint">Saved here for scripts/deploy_colab_speech.ps1. Credentials remain in the Colab CLI profile and are never stored by Ciel.</div>
|
|
245
|
+
</section>
|
|
230
246
|
<section class="settings-section">
|
|
231
247
|
<h3>Tailscale tunnel</h3>
|
|
232
248
|
<div class="settings-grid">
|
|
@@ -287,6 +303,7 @@ def render_web_chat_page(
|
|
|
287
303
|
let mediaRecorder = null;
|
|
288
304
|
let mediaStream = null;
|
|
289
305
|
let recordingChunks = [];
|
|
306
|
+
let pendingTtsReferenceAudio = '';
|
|
290
307
|
function setState(text, cls = '') {{
|
|
291
308
|
statePill.textContent = text;
|
|
292
309
|
statePill.className = 'pill ' + cls;
|
|
@@ -553,9 +570,18 @@ def render_web_chat_page(
|
|
|
553
570
|
reader.readAsDataURL(file);
|
|
554
571
|
}});
|
|
555
572
|
}}
|
|
573
|
+
function fileToDataUrl(file) {{
|
|
574
|
+
return new Promise((resolve, reject) => {{
|
|
575
|
+
const reader = new FileReader();
|
|
576
|
+
reader.onload = () => resolve(String(reader.result || ''));
|
|
577
|
+
reader.onerror = () => reject(reader.error || new Error('Could not read file'));
|
|
578
|
+
reader.readAsDataURL(file);
|
|
579
|
+
}});
|
|
580
|
+
}}
|
|
556
581
|
function setSpeechForm(config) {{
|
|
557
582
|
const asr = config.asr || {{}};
|
|
558
583
|
const tts = config.tts || {{}};
|
|
584
|
+
const colab = config.colab || {{}};
|
|
559
585
|
const tailscale = config.tailscale || {{}};
|
|
560
586
|
document.getElementById('asrEnabled').checked = Boolean(asr.enabled);
|
|
561
587
|
document.getElementById('asrBaseUrl').value = asr.base_url || '';
|
|
@@ -569,8 +595,20 @@ def render_web_chat_page(
|
|
|
569
595
|
document.getElementById('ttsModel').value = tts.model || '';
|
|
570
596
|
document.getElementById('ttsVoice').value = tts.voice || 'default';
|
|
571
597
|
document.getElementById('ttsLanguage').value = tts.language || 'ko';
|
|
598
|
+
document.getElementById('ttsReferenceText').value = tts.ref_text || '';
|
|
599
|
+
document.getElementById('ttsReferenceAudioStatus').textContent = tts.ref_audio_set ? 'Reference voice saved securely on this Ciel router' : 'No reference voice configured';
|
|
600
|
+
document.getElementById('ttsClearReferenceAudio').checked = false;
|
|
601
|
+
document.getElementById('ttsReferenceAudio').value = '';
|
|
602
|
+
pendingTtsReferenceAudio = '';
|
|
572
603
|
document.getElementById('ttsApiKey').value = '';
|
|
573
604
|
document.getElementById('ttsApiKey').placeholder = tts.api_key_set ? 'Token is set; leave blank to keep it' : 'Optional remote bearer token';
|
|
605
|
+
document.getElementById('colabEnabled').checked = colab.enabled !== false;
|
|
606
|
+
document.getElementById('colabDistribution').value = colab.distribution || 'Ubuntu-26.04';
|
|
607
|
+
document.getElementById('colabAuth').value = colab.auth || 'adc';
|
|
608
|
+
document.getElementById('colabAsrSession').value = colab.asr_session || 'ciel-asr';
|
|
609
|
+
document.getElementById('colabTtsSession').value = colab.tts_session || 'ciel-tts';
|
|
610
|
+
document.getElementById('colabAsrAccelerator').value = colab.asr_accelerator || 'T4';
|
|
611
|
+
document.getElementById('colabTtsAccelerator').value = colab.tts_accelerator || 'T4';
|
|
574
612
|
document.getElementById('tailscaleEnabled').checked = tailscale.enabled !== false;
|
|
575
613
|
document.getElementById('tailscaleAsrHostname').value = tailscale.asr_hostname || 'ciel-asr';
|
|
576
614
|
document.getElementById('tailscaleTtsHostname').value = tailscale.tts_hostname || 'ciel-tts';
|
|
@@ -601,8 +639,20 @@ def render_web_chat_page(
|
|
|
601
639
|
model: document.getElementById('ttsModel').value,
|
|
602
640
|
voice: document.getElementById('ttsVoice').value,
|
|
603
641
|
language: document.getElementById('ttsLanguage').value,
|
|
642
|
+
ref_audio: pendingTtsReferenceAudio,
|
|
643
|
+
ref_text: document.getElementById('ttsReferenceText').value,
|
|
644
|
+
clear_ref_audio: document.getElementById('ttsClearReferenceAudio').checked,
|
|
604
645
|
api_key: document.getElementById('ttsApiKey').value,
|
|
605
646
|
}},
|
|
647
|
+
colab: {{
|
|
648
|
+
enabled: document.getElementById('colabEnabled').checked,
|
|
649
|
+
distribution: document.getElementById('colabDistribution').value,
|
|
650
|
+
auth: document.getElementById('colabAuth').value,
|
|
651
|
+
asr_session: document.getElementById('colabAsrSession').value,
|
|
652
|
+
tts_session: document.getElementById('colabTtsSession').value,
|
|
653
|
+
asr_accelerator: document.getElementById('colabAsrAccelerator').value,
|
|
654
|
+
tts_accelerator: document.getElementById('colabTtsAccelerator').value,
|
|
655
|
+
}},
|
|
606
656
|
tailscale: {{
|
|
607
657
|
enabled: document.getElementById('tailscaleEnabled').checked,
|
|
608
658
|
asr_hostname: document.getElementById('tailscaleAsrHostname').value,
|
|
@@ -888,6 +938,18 @@ def render_web_chat_page(
|
|
|
888
938
|
speechSettingsDialog.showModal();
|
|
889
939
|
}});
|
|
890
940
|
speechSettingsClose.addEventListener('click', () => speechSettingsDialog.close());
|
|
941
|
+
document.getElementById('ttsReferenceAudio').addEventListener('change', async event => {{
|
|
942
|
+
const file = event.target.files && event.target.files[0];
|
|
943
|
+
if (!file) return;
|
|
944
|
+
if (file.size > 10 * 1024 * 1024) {{
|
|
945
|
+
event.target.value = '';
|
|
946
|
+
addBubble('system', 'Reference voice must be 10 MB or smaller.');
|
|
947
|
+
return;
|
|
948
|
+
}}
|
|
949
|
+
pendingTtsReferenceAudio = await fileToDataUrl(file);
|
|
950
|
+
document.getElementById('ttsClearReferenceAudio').checked = false;
|
|
951
|
+
document.getElementById('ttsReferenceAudioStatus').textContent = file.name + ' (' + formatBytes(file.size) + ') ready to save';
|
|
952
|
+
}});
|
|
891
953
|
speechSettingsForm.addEventListener('submit', async event => {{
|
|
892
954
|
event.preventDefault();
|
|
893
955
|
try {{
|
package/docs/COLAB_SPEECH.md
CHANGED
|
@@ -5,8 +5,8 @@ Ciel Runtime can proxy its web chat and OpenAI-compatible audio API to two Colab
|
|
|
5
5
|
## One-time prerequisites
|
|
6
6
|
|
|
7
7
|
1. Authenticate the Colab CLI inside WSL. The installed CLI currently uses Google Application Default Credentials, so run `~/google-cloud-sdk/bin/gcloud auth application-default login` in `Ubuntu-26.04`.
|
|
8
|
-
2. Create a reusable
|
|
9
|
-
3. Optionally
|
|
8
|
+
2. Create a reusable Tailscale auth key (two workers must register) and set it only for the current PowerShell process with `$env:TAILSCALE_AUTHKEY = Read-Host`. Alternatively, use a separate fresh key for each worker. The CLI passes the key without writing it to the repository. A Colab `TAILSCALE_AUTHKEY` Secret is also supported as a fallback.
|
|
9
|
+
3. Optionally set `$env:CIEL_SPEECH_API_KEY = Read-Host` before deployment. Enter that value once in Web Chat > Speech Settings; Ciel stores it server-side and never returns it to the browser.
|
|
10
10
|
|
|
11
11
|
## Deploy
|
|
12
12
|
|
|
@@ -16,7 +16,11 @@ From PowerShell at the repository root:
|
|
|
16
16
|
.\scripts\deploy_colab_speech.ps1
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
Set the WSL distribution, authentication mode, ASR/TTS session names, and accelerators in **Web Chat > Speech Settings > Colab CLI connection**. These values are available through `GET|POST /ca/speech/config`; Ciel does not store Colab credentials. The deployment script reads the saved values automatically. Command-line parameters such as `-Distribution`, `-ColabAuth`, `-AsrSession`, and `-AsrAccelerator` override them for one run.
|
|
20
|
+
|
|
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
|
+
|
|
23
|
+
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`.
|
|
20
24
|
|
|
21
25
|
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.
|
|
22
26
|
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
@@ -9,6 +9,7 @@ from __future__ import annotations
|
|
|
9
9
|
import json
|
|
10
10
|
import os
|
|
11
11
|
from pathlib import Path
|
|
12
|
+
import site
|
|
12
13
|
import shutil
|
|
13
14
|
import subprocess
|
|
14
15
|
import sys
|
|
@@ -38,7 +39,18 @@ def secret(name: str, *, required: bool = False) -> str:
|
|
|
38
39
|
|
|
39
40
|
|
|
40
41
|
def run(*args: str, check: bool = True) -> subprocess.CompletedProcess[str]:
|
|
41
|
-
|
|
42
|
+
visible: list[str] = []
|
|
43
|
+
redact_next = False
|
|
44
|
+
for arg in args:
|
|
45
|
+
if redact_next:
|
|
46
|
+
visible.append("<redacted>")
|
|
47
|
+
redact_next = False
|
|
48
|
+
elif arg.startswith("--auth-key="):
|
|
49
|
+
visible.append("--auth-key=<redacted>")
|
|
50
|
+
else:
|
|
51
|
+
visible.append(arg)
|
|
52
|
+
redact_next = arg == "--api-key"
|
|
53
|
+
print("+", " ".join(visible))
|
|
42
54
|
return subprocess.run(args, check=check, text=True, capture_output=False)
|
|
43
55
|
|
|
44
56
|
|
|
@@ -52,29 +64,33 @@ def start_tailscale(auth_key: str) -> tuple[str, str]:
|
|
|
52
64
|
install_tailscale()
|
|
53
65
|
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
54
66
|
tail_log = (LOG_DIR / "tailscale-tts.log").open("ab")
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
67
|
+
if not Path(SOCKET).exists():
|
|
68
|
+
subprocess.Popen(
|
|
69
|
+
["tailscaled", f"--socket={SOCKET}", f"--state={STATE}", "--tun=userspace-networking"],
|
|
70
|
+
stdout=tail_log,
|
|
71
|
+
stderr=subprocess.STDOUT,
|
|
72
|
+
start_new_session=True,
|
|
73
|
+
)
|
|
61
74
|
for _ in range(60):
|
|
62
75
|
if Path(SOCKET).exists():
|
|
63
76
|
break
|
|
64
77
|
time.sleep(1)
|
|
65
|
-
run("tailscale", f"--socket={SOCKET}", "up", f"--auth-key={auth_key}", f"--hostname={HOSTNAME}", "--accept-dns=true", "--reset")
|
|
78
|
+
login = run("tailscale", f"--socket={SOCKET}", "up", f"--auth-key={auth_key}", f"--hostname={HOSTNAME}", "--accept-dns=true", "--reset", check=False)
|
|
79
|
+
if login.returncode:
|
|
80
|
+
raise RuntimeError("Tailscale authentication failed; use a valid reusable key or a fresh key for this second worker")
|
|
66
81
|
status = subprocess.check_output(["tailscale", f"--socket={SOCKET}", "status", "--json"], text=True)
|
|
67
82
|
dns_name = str(json.loads(status).get("Self", {}).get("DNSName") or HOSTNAME).rstrip(".")
|
|
68
|
-
serve = run("tailscale", f"--socket={SOCKET}", "serve", "--bg", "--https=443", f"http://127.0.0.1:{PORT}", check=False)
|
|
69
|
-
if serve.returncode == 0:
|
|
70
|
-
return dns_name, f"https://{dns_name}"
|
|
71
83
|
run("tailscale", f"--socket={SOCKET}", "serve", "--bg", "--http=80", f"http://127.0.0.1:{PORT}")
|
|
72
84
|
return dns_name, f"http://{dns_name}"
|
|
73
85
|
|
|
74
86
|
|
|
75
|
-
def wait_for_server(api_key: str) -> None:
|
|
87
|
+
def wait_for_server(api_key: str, process: subprocess.Popen[bytes]) -> None:
|
|
76
88
|
headers = {"authorization": f"Bearer {api_key}"} if api_key else {}
|
|
77
89
|
for _ in range(240):
|
|
90
|
+
if process.poll() is not None:
|
|
91
|
+
log_path = LOG_DIR / "moss-tts.log"
|
|
92
|
+
log_tail = log_path.read_text(encoding="utf-8", errors="replace")[-12000:] if log_path.exists() else "log unavailable"
|
|
93
|
+
raise RuntimeError(f"MOSS TTS exited with status {process.returncode}:\n{log_tail}")
|
|
78
94
|
try:
|
|
79
95
|
with urllib.request.urlopen(urllib.request.Request(f"http://127.0.0.1:{PORT}/health", headers=headers), timeout=3) as response:
|
|
80
96
|
if response.status < 500:
|
|
@@ -84,20 +100,48 @@ def wait_for_server(api_key: str) -> None:
|
|
|
84
100
|
raise RuntimeError("MOSS TTS did not become healthy; inspect /content/ciel-speech-logs/moss-tts.log")
|
|
85
101
|
|
|
86
102
|
|
|
103
|
+
def server_is_healthy(api_key: str) -> bool:
|
|
104
|
+
headers = {"authorization": f"Bearer {api_key}"} if api_key else {}
|
|
105
|
+
try:
|
|
106
|
+
with urllib.request.urlopen(urllib.request.Request(f"http://127.0.0.1:{PORT}/health", headers=headers), timeout=3) as response:
|
|
107
|
+
return response.status < 500
|
|
108
|
+
except Exception:
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
|
|
87
112
|
def main() -> None:
|
|
88
113
|
auth_key = secret("TAILSCALE_AUTHKEY", required=True)
|
|
89
114
|
api_key = secret("CIEL_SPEECH_API_KEY")
|
|
90
|
-
run(
|
|
115
|
+
run(
|
|
116
|
+
sys.executable,
|
|
117
|
+
"-m",
|
|
118
|
+
"pip",
|
|
119
|
+
"install",
|
|
120
|
+
"-U",
|
|
121
|
+
"nvidia-cuda-runtime==13.0.96",
|
|
122
|
+
"vllm==0.24.0",
|
|
123
|
+
"vllm-omni==0.24.0",
|
|
124
|
+
)
|
|
91
125
|
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
126
|
+
if not server_is_healthy(api_key):
|
|
127
|
+
command = [
|
|
128
|
+
"vllm-omni", "serve", "OpenMOSS-Team/MOSS-TTS-Nano", "--omni", "--host", "127.0.0.1", "--port", str(PORT),
|
|
129
|
+
"--gpu-memory-utilization", "0.72",
|
|
130
|
+
]
|
|
131
|
+
if api_key:
|
|
132
|
+
command.extend(["--api-key", api_key])
|
|
133
|
+
server_env = os.environ.copy()
|
|
134
|
+
cuda_runtime_libraries = [
|
|
135
|
+
library
|
|
136
|
+
for package_dir in site.getsitepackages()
|
|
137
|
+
for library in Path(package_dir).glob("**/libcudart.so.13")
|
|
138
|
+
]
|
|
139
|
+
if cuda_runtime_libraries:
|
|
140
|
+
existing_library_path = server_env.get("LD_LIBRARY_PATH", "")
|
|
141
|
+
server_env["LD_LIBRARY_PATH"] = str(cuda_runtime_libraries[0].parent) + (f":{existing_library_path}" if existing_library_path else "")
|
|
142
|
+
server_log = (LOG_DIR / "moss-tts.log").open("ab")
|
|
143
|
+
process = subprocess.Popen(command, stdout=server_log, stderr=subprocess.STDOUT, start_new_session=True, env=server_env)
|
|
144
|
+
wait_for_server(api_key, process)
|
|
101
145
|
dns_name, base_url = start_tailscale(auth_key)
|
|
102
146
|
print(json.dumps({"ok": True, "role": "tts", "hostname": dns_name, "base_url": base_url, "model": "OpenMOSS-Team/MOSS-TTS-Nano", "api_key_set": bool(api_key)}, indent=2))
|
|
103
147
|
|
|
@@ -38,7 +38,18 @@ def secret(name: str, *, required: bool = False) -> str:
|
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
def run(*args: str, check: bool = True) -> subprocess.CompletedProcess[str]:
|
|
41
|
-
|
|
41
|
+
visible: list[str] = []
|
|
42
|
+
redact_next = False
|
|
43
|
+
for arg in args:
|
|
44
|
+
if redact_next:
|
|
45
|
+
visible.append("<redacted>")
|
|
46
|
+
redact_next = False
|
|
47
|
+
elif arg.startswith("--auth-key="):
|
|
48
|
+
visible.append("--auth-key=<redacted>")
|
|
49
|
+
else:
|
|
50
|
+
visible.append(arg)
|
|
51
|
+
redact_next = arg == "--api-key"
|
|
52
|
+
print("+", " ".join(visible))
|
|
42
53
|
return subprocess.run(args, check=check, text=True, capture_output=False)
|
|
43
54
|
|
|
44
55
|
|
|
@@ -62,12 +73,11 @@ def start_tailscale(auth_key: str) -> tuple[str, str]:
|
|
|
62
73
|
if Path(SOCKET).exists():
|
|
63
74
|
break
|
|
64
75
|
time.sleep(1)
|
|
65
|
-
run("tailscale", f"--socket={SOCKET}", "up", f"--auth-key={auth_key}", f"--hostname={HOSTNAME}", "--accept-dns=true", "--reset")
|
|
76
|
+
login = run("tailscale", f"--socket={SOCKET}", "up", f"--auth-key={auth_key}", f"--hostname={HOSTNAME}", "--accept-dns=true", "--reset", check=False)
|
|
77
|
+
if login.returncode:
|
|
78
|
+
raise RuntimeError("Tailscale authentication failed; use a valid reusable key or a fresh key for this worker")
|
|
66
79
|
status = subprocess.check_output(["tailscale", f"--socket={SOCKET}", "status", "--json"], text=True)
|
|
67
80
|
dns_name = str(json.loads(status).get("Self", {}).get("DNSName") or HOSTNAME).rstrip(".")
|
|
68
|
-
serve = run("tailscale", f"--socket={SOCKET}", "serve", "--bg", "--https=443", f"http://127.0.0.1:{PORT}", check=False)
|
|
69
|
-
if serve.returncode == 0:
|
|
70
|
-
return dns_name, f"https://{dns_name}"
|
|
71
81
|
run("tailscale", f"--socket={SOCKET}", "serve", "--bg", "--http=80", f"http://127.0.0.1:{PORT}")
|
|
72
82
|
return dns_name, f"http://{dns_name}"
|
|
73
83
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import argparse
|
|
6
|
+
import json
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
import sys
|
|
8
9
|
from typing import Any
|
|
@@ -10,25 +11,94 @@ from typing import Any
|
|
|
10
11
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
11
12
|
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
DEFAULT_TTS_REFERENCE_AUDIO = "https://raw.githubusercontent.com/OpenMOSS/MOSS-TTS-Nano/main/assets/audio/zh_1.wav"
|
|
15
|
+
DEFAULT_COLAB_SETTINGS: dict[str, Any] = {
|
|
16
|
+
"enabled": True,
|
|
17
|
+
"distribution": "Ubuntu-26.04",
|
|
18
|
+
"auth": "adc",
|
|
19
|
+
"asr_session": "ciel-asr",
|
|
20
|
+
"tts_session": "ciel-tts",
|
|
21
|
+
"asr_accelerator": "T4",
|
|
22
|
+
"tts_accelerator": "T4",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def colab_settings(config: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
27
|
+
import ciel_runtime
|
|
28
|
+
|
|
29
|
+
active = config if config is not None else ciel_runtime.load_config()
|
|
30
|
+
speech = active.get("speech") if isinstance(active.get("speech"), dict) else {}
|
|
31
|
+
saved = speech.get("colab") if isinstance(speech.get("colab"), dict) else {}
|
|
32
|
+
return {**DEFAULT_COLAB_SETTINGS, **saved}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def configure(
|
|
36
|
+
asr_base_url: str,
|
|
37
|
+
tts_base_url: str,
|
|
38
|
+
tts_reference_audio: str = DEFAULT_TTS_REFERENCE_AUDIO,
|
|
39
|
+
*,
|
|
40
|
+
distribution: str | None = None,
|
|
41
|
+
auth: str | None = None,
|
|
42
|
+
asr_session: str | None = None,
|
|
43
|
+
tts_session: str | None = None,
|
|
44
|
+
asr_accelerator: str | None = None,
|
|
45
|
+
tts_accelerator: str | None = None,
|
|
46
|
+
) -> dict[str, Any]:
|
|
14
47
|
import ciel_runtime
|
|
15
48
|
|
|
16
49
|
config = ciel_runtime.load_config()
|
|
17
50
|
speech = config.setdefault("speech", {})
|
|
18
51
|
asr = speech.setdefault("asr", {})
|
|
19
52
|
tts = speech.setdefault("tts", {})
|
|
53
|
+
colab = colab_settings(config)
|
|
54
|
+
overrides = {
|
|
55
|
+
"distribution": distribution,
|
|
56
|
+
"auth": auth,
|
|
57
|
+
"asr_session": asr_session,
|
|
58
|
+
"tts_session": tts_session,
|
|
59
|
+
"asr_accelerator": asr_accelerator,
|
|
60
|
+
"tts_accelerator": tts_accelerator,
|
|
61
|
+
}
|
|
62
|
+
colab.update({key: value for key, value in overrides.items() if value is not None})
|
|
63
|
+
colab["enabled"] = True
|
|
64
|
+
speech["colab"] = colab
|
|
20
65
|
asr.update({"enabled": True, "base_url": asr_base_url.rstrip("/"), "model": "Qwen/Qwen3-ASR-0.6B"})
|
|
21
66
|
tts.update({"enabled": True, "base_url": tts_base_url.rstrip("/"), "model": "OpenMOSS-Team/MOSS-TTS-Nano"})
|
|
67
|
+
if tts_reference_audio and not str(tts.get("ref_audio") or "").strip():
|
|
68
|
+
tts["ref_audio"] = tts_reference_audio
|
|
22
69
|
ciel_runtime.save_config(config)
|
|
23
|
-
return {"asr": asr["base_url"], "tts": tts["base_url"]}
|
|
70
|
+
return {"asr": asr["base_url"], "tts": tts["base_url"], "colab": colab}
|
|
24
71
|
|
|
25
72
|
|
|
26
73
|
def main() -> int:
|
|
27
74
|
parser = argparse.ArgumentParser()
|
|
28
|
-
parser.add_argument("--asr-base-url"
|
|
29
|
-
parser.add_argument("--tts-base-url"
|
|
75
|
+
parser.add_argument("--asr-base-url")
|
|
76
|
+
parser.add_argument("--tts-base-url")
|
|
77
|
+
parser.add_argument("--tts-reference-audio", default=DEFAULT_TTS_REFERENCE_AUDIO)
|
|
78
|
+
parser.add_argument("--distribution")
|
|
79
|
+
parser.add_argument("--auth", choices=("adc", "oauth2"))
|
|
80
|
+
parser.add_argument("--asr-session")
|
|
81
|
+
parser.add_argument("--tts-session")
|
|
82
|
+
parser.add_argument("--asr-accelerator")
|
|
83
|
+
parser.add_argument("--tts-accelerator")
|
|
84
|
+
parser.add_argument("--print-colab-settings", action="store_true")
|
|
30
85
|
args = parser.parse_args()
|
|
31
|
-
|
|
86
|
+
if args.print_colab_settings:
|
|
87
|
+
print(json.dumps(colab_settings(), separators=(",", ":")))
|
|
88
|
+
return 0
|
|
89
|
+
if not args.asr_base_url or not args.tts_base_url:
|
|
90
|
+
parser.error("--asr-base-url and --tts-base-url are required unless --print-colab-settings is used")
|
|
91
|
+
result = configure(
|
|
92
|
+
args.asr_base_url,
|
|
93
|
+
args.tts_base_url,
|
|
94
|
+
args.tts_reference_audio,
|
|
95
|
+
distribution=args.distribution,
|
|
96
|
+
auth=args.auth,
|
|
97
|
+
asr_session=args.asr_session,
|
|
98
|
+
tts_session=args.tts_session,
|
|
99
|
+
asr_accelerator=args.asr_accelerator,
|
|
100
|
+
tts_accelerator=args.tts_accelerator,
|
|
101
|
+
)
|
|
32
102
|
print(f"Configured Ciel speech workers: ASR={result['asr']} TTS={result['tts']}")
|
|
33
103
|
return 0
|
|
34
104
|
|
|
@@ -1,34 +1,70 @@
|
|
|
1
1
|
param(
|
|
2
|
-
[string]$Distribution
|
|
3
|
-
[string]$
|
|
4
|
-
[string]$
|
|
2
|
+
[string]$Distribution,
|
|
3
|
+
[string]$ColabAuth,
|
|
4
|
+
[string]$AsrSession,
|
|
5
|
+
[string]$TtsSession,
|
|
6
|
+
[string]$AsrAccelerator,
|
|
7
|
+
[string]$TtsAccelerator
|
|
5
8
|
)
|
|
6
9
|
|
|
7
10
|
$ErrorActionPreference = "Stop"
|
|
8
11
|
$repo = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
|
|
12
|
+
$settingsJson = (& python (Join-Path $PSScriptRoot "configure_speech_workers.py") --print-colab-settings) -join "`n"
|
|
13
|
+
if ($LASTEXITCODE -ne 0) { throw "Could not read Ciel Colab settings." }
|
|
14
|
+
$settings = $settingsJson | ConvertFrom-Json
|
|
15
|
+
if ($settings.enabled -eq $false) { throw "Colab worker management is disabled in Web Chat > Speech Settings." }
|
|
16
|
+
if ([string]::IsNullOrWhiteSpace($Distribution)) { $Distribution = [string]$settings.distribution }
|
|
17
|
+
if ([string]::IsNullOrWhiteSpace($ColabAuth)) { $ColabAuth = [string]$settings.auth }
|
|
18
|
+
if ([string]::IsNullOrWhiteSpace($AsrSession)) { $AsrSession = [string]$settings.asr_session }
|
|
19
|
+
if ([string]::IsNullOrWhiteSpace($TtsSession)) { $TtsSession = [string]$settings.tts_session }
|
|
20
|
+
if ([string]::IsNullOrWhiteSpace($AsrAccelerator)) { $AsrAccelerator = [string]$settings.asr_accelerator }
|
|
21
|
+
if ([string]::IsNullOrWhiteSpace($TtsAccelerator)) { $TtsAccelerator = [string]$settings.tts_accelerator }
|
|
22
|
+
if ($Distribution -notmatch '^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$') { throw "Invalid WSL distribution name." }
|
|
23
|
+
if ($ColabAuth -notin @('adc', 'oauth2')) { throw "ColabAuth must be adc or oauth2." }
|
|
24
|
+
foreach ($session in @($AsrSession, $TtsSession)) {
|
|
25
|
+
if ($session -notmatch '^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$') { throw "Invalid Colab session name: $session" }
|
|
26
|
+
}
|
|
27
|
+
foreach ($accelerator in @($AsrAccelerator, $TtsAccelerator)) {
|
|
28
|
+
if ($accelerator -notin @('T4', 'L4', 'G4', 'A100', 'H100')) { throw "Unsupported Colab accelerator: $accelerator" }
|
|
29
|
+
}
|
|
9
30
|
$wslRepo = (& wsl -d $Distribution -- wslpath -a ($repo -replace '\\', '/')).Trim()
|
|
10
31
|
if (-not $wslRepo) { throw "Could not resolve the repository path in WSL." }
|
|
32
|
+
$bootstrapEnv = ""
|
|
33
|
+
if ($env:TAILSCALE_AUTHKEY) {
|
|
34
|
+
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
|
+
}
|
|
37
|
+
if ($env:CIEL_SPEECH_API_KEY) {
|
|
38
|
+
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
|
+
}
|
|
11
41
|
|
|
12
42
|
Write-Host "Checking Colab CLI authentication..."
|
|
13
|
-
& wsl -d $Distribution -- bash -lc "colab status >/dev/null"
|
|
43
|
+
& wsl -d $Distribution -- bash -lc "colab --auth $ColabAuth status >/dev/null"
|
|
14
44
|
if ($LASTEXITCODE -ne 0) {
|
|
15
|
-
throw "Colab CLI is not authenticated
|
|
45
|
+
throw "Colab CLI is not authenticated with '$ColabAuth' in WSL '$Distribution'."
|
|
16
46
|
}
|
|
17
47
|
|
|
18
|
-
|
|
19
|
-
& wsl -d $Distribution -- bash -lc "colab
|
|
20
|
-
if ($LASTEXITCODE -
|
|
48
|
+
function Ensure-ColabSession([string]$Session, [string]$Accelerator, [string]$Role) {
|
|
49
|
+
& wsl -d $Distribution -- bash -lc "colab --auth $ColabAuth status --session '$Session' >/dev/null 2>&1"
|
|
50
|
+
if ($LASTEXITCODE -eq 0) {
|
|
51
|
+
Write-Host "Reusing $Role $Accelerator session: $Session"
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
Write-Host "Creating $Role $Accelerator session: $Session"
|
|
55
|
+
& wsl -d $Distribution -- bash -lc "colab --auth $ColabAuth new --gpu $Accelerator --session '$Session'"
|
|
56
|
+
if ($LASTEXITCODE -ne 0) { throw "Could not create $Role Colab session." }
|
|
57
|
+
}
|
|
21
58
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if ($LASTEXITCODE -ne 0) { throw "Could not create TTS Colab session." }
|
|
59
|
+
Ensure-ColabSession $AsrSession $AsrAccelerator "ASR"
|
|
60
|
+
Ensure-ColabSession $TtsSession $TtsAccelerator "TTS"
|
|
25
61
|
|
|
26
62
|
Write-Host "Installing Qwen3-ASR and its Tailscale service..."
|
|
27
|
-
$asrOutput = (& wsl -d $Distribution -- bash -lc "colab exec --session '$AsrSession' --file '$wslRepo/scripts/colab/bootstrap_qwen_asr.py'" 2>&1 | Tee-Object -Variable asrDisplay) -join "`n"
|
|
63
|
+
$asrOutput = (& wsl -d $Distribution -- bash -lc "colab --auth $ColabAuth exec --session '$AsrSession'$bootstrapEnv --file '$wslRepo/scripts/colab/bootstrap_qwen_asr.py'" 2>&1 | Tee-Object -Variable asrDisplay) -join "`n"
|
|
28
64
|
if ($LASTEXITCODE -ne 0) { throw "ASR bootstrap failed." }
|
|
29
65
|
|
|
30
66
|
Write-Host "Installing MOSS-TTS-Nano and its Tailscale service..."
|
|
31
|
-
$ttsOutput = (& wsl -d $Distribution -- bash -lc "colab exec --session '$TtsSession' --file '$wslRepo/scripts/colab/bootstrap_moss_tts.py'" 2>&1 | Tee-Object -Variable ttsDisplay) -join "`n"
|
|
67
|
+
$ttsOutput = (& wsl -d $Distribution -- bash -lc "colab --auth $ColabAuth exec --session '$TtsSession'$bootstrapEnv --file '$wslRepo/scripts/colab/bootstrap_moss_tts.py'" 2>&1 | Tee-Object -Variable ttsDisplay) -join "`n"
|
|
32
68
|
if ($LASTEXITCODE -ne 0) { throw "TTS bootstrap failed." }
|
|
33
69
|
|
|
34
70
|
function Read-BootstrapResult([string]$Text, [string]$Role) {
|
|
@@ -41,7 +77,7 @@ function Read-BootstrapResult([string]$Text, [string]$Role) {
|
|
|
41
77
|
|
|
42
78
|
$asr = Read-BootstrapResult $asrOutput "asr"
|
|
43
79
|
$tts = Read-BootstrapResult $ttsOutput "tts"
|
|
44
|
-
& python (Join-Path $PSScriptRoot "configure_speech_workers.py") --asr-base-url $asr.base_url --tts-base-url $tts.base_url
|
|
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
|
|
45
81
|
if ($LASTEXITCODE -ne 0) { throw "Workers started, but Ciel speech configuration failed." }
|
|
46
82
|
|
|
47
83
|
Write-Host "Both services are running and connected to Web Chat > Speech Settings."
|