@oneciel-ai/ciel-runtime 0.2.5 → 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.
@@ -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",
@@ -93,7 +93,7 @@ OFFICIAL_CHANNEL_PLUGINS = {
93
93
  }
94
94
 
95
95
  APP_NAME = "Ciel Runtime"
96
- VERSION = "0.2.5"
96
+ VERSION = "0.2.6"
97
97
  CREDITS = "Credits: One Ciel LLC"
98
98
  PRELAUNCH_CANCEL = 10
99
99
  PRELAUNCH_LAUNCH_CODEX = 11
@@ -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
@@ -72,6 +73,8 @@ class SpeechHttpController:
72
73
  public[name] = item
73
74
  tailscale = speech.get("tailscale")
74
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 {}
75
78
  public["endpoints"] = self.discovery_payload()["endpoints"]
76
79
  return public
77
80
 
@@ -147,6 +150,14 @@ class SpeechHttpController:
147
150
  for key in ("enabled", "asr_hostname", "tts_hostname"):
148
151
  if key in tailscale:
149
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)
150
161
  self.ports.save_config(config)
151
162
  self.ports.write_json(handler, self.public_config())
152
163
  except (UnicodeError, ValueError, TypeError) as exc:
@@ -192,6 +203,36 @@ class SpeechHttpController:
192
203
  raise ValueError(f"{service} {key} must begin with /")
193
204
  return text
194
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
+
195
236
  def _probe(self, name: str) -> dict[str, Any]:
196
237
  config = self._service_config(name)
197
238
  enabled = bool(config.get("enabled"))
@@ -230,6 +230,19 @@ def render_web_chat_page(
230
230
  <label class="wide">Remote bearer token<input id="ttsApiKey" type="password" autocomplete="new-password" placeholder="Leave blank to keep current token"></label>
231
231
  </div>
232
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>
233
246
  <section class="settings-section">
234
247
  <h3>Tailscale tunnel</h3>
235
248
  <div class="settings-grid">
@@ -568,6 +581,7 @@ def render_web_chat_page(
568
581
  function setSpeechForm(config) {{
569
582
  const asr = config.asr || {{}};
570
583
  const tts = config.tts || {{}};
584
+ const colab = config.colab || {{}};
571
585
  const tailscale = config.tailscale || {{}};
572
586
  document.getElementById('asrEnabled').checked = Boolean(asr.enabled);
573
587
  document.getElementById('asrBaseUrl').value = asr.base_url || '';
@@ -588,6 +602,13 @@ def render_web_chat_page(
588
602
  pendingTtsReferenceAudio = '';
589
603
  document.getElementById('ttsApiKey').value = '';
590
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';
591
612
  document.getElementById('tailscaleEnabled').checked = tailscale.enabled !== false;
592
613
  document.getElementById('tailscaleAsrHostname').value = tailscale.asr_hostname || 'ciel-asr';
593
614
  document.getElementById('tailscaleTtsHostname').value = tailscale.tts_hostname || 'ciel-tts';
@@ -623,6 +644,15 @@ def render_web_chat_page(
623
644
  clear_ref_audio: document.getElementById('ttsClearReferenceAudio').checked,
624
645
  api_key: document.getElementById('ttsApiKey').value,
625
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
+ }},
626
656
  tailscale: {{
627
657
  enabled: document.getElementById('tailscaleEnabled').checked,
628
658
  asr_hostname: document.getElementById('tailscaleAsrHostname').value,
@@ -16,7 +16,9 @@ From PowerShell at the repository root:
16
16
  .\scripts\deploy_colab_speech.ps1
17
17
  ```
18
18
 
19
- The script creates `ciel-asr` and `ciel-tts` T4 sessions, 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.
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.
20
22
 
21
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`.
22
24
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oneciel-ai/ciel-runtime",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "Universal AI coding-agent runtime and model-routing layer for Claude, Codex, AGY, and compatible runtimes.",
5
5
  "license": "MIT",
6
6
  "author": "One Ciel LLC",
@@ -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
@@ -11,30 +12,93 @@ 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
+ }
14
24
 
15
25
 
16
- def configure(asr_base_url: str, tts_base_url: str, tts_reference_audio: str = DEFAULT_TTS_REFERENCE_AUDIO) -> dict[str, Any]:
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]:
17
47
  import ciel_runtime
18
48
 
19
49
  config = ciel_runtime.load_config()
20
50
  speech = config.setdefault("speech", {})
21
51
  asr = speech.setdefault("asr", {})
22
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
23
65
  asr.update({"enabled": True, "base_url": asr_base_url.rstrip("/"), "model": "Qwen/Qwen3-ASR-0.6B"})
24
66
  tts.update({"enabled": True, "base_url": tts_base_url.rstrip("/"), "model": "OpenMOSS-Team/MOSS-TTS-Nano"})
25
67
  if tts_reference_audio and not str(tts.get("ref_audio") or "").strip():
26
68
  tts["ref_audio"] = tts_reference_audio
27
69
  ciel_runtime.save_config(config)
28
- return {"asr": asr["base_url"], "tts": tts["base_url"]}
70
+ return {"asr": asr["base_url"], "tts": tts["base_url"], "colab": colab}
29
71
 
30
72
 
31
73
  def main() -> int:
32
74
  parser = argparse.ArgumentParser()
33
- parser.add_argument("--asr-base-url", required=True)
34
- parser.add_argument("--tts-base-url", required=True)
75
+ parser.add_argument("--asr-base-url")
76
+ parser.add_argument("--tts-base-url")
35
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")
36
85
  args = parser.parse_args()
37
- result = configure(args.asr_base_url, args.tts_base_url, args.tts_reference_audio)
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
+ )
38
102
  print(f"Configured Ciel speech workers: ASR={result['asr']} TTS={result['tts']}")
39
103
  return 0
40
104
 
@@ -1,11 +1,32 @@
1
1
  param(
2
- [string]$Distribution = "Ubuntu-26.04",
3
- [string]$AsrSession = "ciel-asr",
4
- [string]$TtsSession = "ciel-tts"
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." }
11
32
  $bootstrapEnv = ""
@@ -19,25 +40,31 @@ if ($env:CIEL_SPEECH_API_KEY) {
19
40
  }
20
41
 
21
42
  Write-Host "Checking Colab CLI authentication..."
22
- & wsl -d $Distribution -- bash -lc "colab --auth adc status >/dev/null"
43
+ & wsl -d $Distribution -- bash -lc "colab --auth $ColabAuth status >/dev/null"
23
44
  if ($LASTEXITCODE -ne 0) {
24
- throw "Colab CLI is not authenticated. Configure ADC in WSL (gcloud auth application-default login), then rerun."
45
+ throw "Colab CLI is not authenticated with '$ColabAuth' in WSL '$Distribution'."
25
46
  }
26
47
 
27
- Write-Host "Creating ASR T4 session: $AsrSession"
28
- & wsl -d $Distribution -- bash -lc "colab --auth adc new --gpu T4 --session '$AsrSession'"
29
- if ($LASTEXITCODE -ne 0) { throw "Could not create ASR Colab session." }
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
+ }
30
58
 
31
- Write-Host "Creating TTS T4 session: $TtsSession"
32
- & wsl -d $Distribution -- bash -lc "colab --auth adc new --gpu T4 --session '$TtsSession'"
33
- if ($LASTEXITCODE -ne 0) { throw "Could not create TTS Colab session." }
59
+ Ensure-ColabSession $AsrSession $AsrAccelerator "ASR"
60
+ Ensure-ColabSession $TtsSession $TtsAccelerator "TTS"
34
61
 
35
62
  Write-Host "Installing Qwen3-ASR and its Tailscale service..."
36
- $asrOutput = (& wsl -d $Distribution -- bash -lc "colab --auth adc exec --session '$AsrSession'$bootstrapEnv --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"
37
64
  if ($LASTEXITCODE -ne 0) { throw "ASR bootstrap failed." }
38
65
 
39
66
  Write-Host "Installing MOSS-TTS-Nano and its Tailscale service..."
40
- $ttsOutput = (& wsl -d $Distribution -- bash -lc "colab --auth adc exec --session '$TtsSession'$bootstrapEnv --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"
41
68
  if ($LASTEXITCODE -ne 0) { throw "TTS bootstrap failed." }
42
69
 
43
70
  function Read-BootstrapResult([string]$Text, [string]$Role) {
@@ -50,7 +77,7 @@ function Read-BootstrapResult([string]$Text, [string]$Role) {
50
77
 
51
78
  $asr = Read-BootstrapResult $asrOutput "asr"
52
79
  $tts = Read-BootstrapResult $ttsOutput "tts"
53
- & 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
54
81
  if ($LASTEXITCODE -ne 0) { throw "Workers started, but Ciel speech configuration failed." }
55
82
 
56
83
  Write-Host "Both services are running and connected to Web Chat > Speech Settings."