@oneciel-ai/ciel-runtime 0.2.7 → 0.2.8
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/channel_transcript_repository.py +47 -0
- package/ciel_runtime_support/channel_wake_context.py +2 -1
- package/ciel_runtime_support/runtime_constants.py +1 -1
- package/ciel_runtime_support/runtime_launch.py +1 -0
- package/ciel_runtime_support/web_ui.py +85 -16
- 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/ciel_runtime.py
CHANGED
|
@@ -4388,8 +4388,8 @@ _CHANNEL_TRANSCRIPT_SCOPE: dict[str, Any] = {'runtime': '', 'started_at': 0.0, '
|
|
|
4388
4388
|
_CHANNEL_STDIN_RECOVERY_CACHE: dict[str, Any] = {'checked_at': 0.0, 'last_id': None, 'marker': None, 'recovered_last_id': None}
|
|
4389
4389
|
def channel_transcript_repository() -> ChannelTranscriptRepository: return channel_wake_context().transcript_repository()
|
|
4390
4390
|
|
|
4391
|
-
def _set_channel_transcript_scope(runtime: str, *, started_at: float | None = None, codex_home: Path | None = None) -> None:
|
|
4392
|
-
channel_wake_context().set_transcript_scope(runtime, started_at=started_at, codex_home=codex_home)
|
|
4391
|
+
def _set_channel_transcript_scope(runtime: str, *, started_at: float | None = None, codex_home: Path | None = None, cwd: Path | None = None) -> None:
|
|
4392
|
+
channel_wake_context().set_transcript_scope(runtime, started_at=started_at, codex_home=codex_home, cwd=cwd)
|
|
4393
4393
|
|
|
4394
4394
|
def _channel_transcript_roots() -> tuple[tuple[Path, str], ...]: return channel_wake_context().transcript_roots()
|
|
4395
4395
|
def _latest_claude_transcript_path(ttl_seconds: float = 2.0) -> Path | None: return channel_wake_context().latest_transcript_path(ttl_seconds)
|
|
@@ -4,6 +4,8 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from collections.abc import Callable
|
|
6
6
|
from dataclasses import dataclass
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
import json
|
|
7
9
|
from pathlib import Path
|
|
8
10
|
from typing import Any
|
|
9
11
|
|
|
@@ -21,6 +23,7 @@ class ChannelTranscriptRepository:
|
|
|
21
23
|
*,
|
|
22
24
|
started_at: float | None = None,
|
|
23
25
|
codex_home: Path | None = None,
|
|
26
|
+
cwd: Path | None = None,
|
|
24
27
|
) -> None:
|
|
25
28
|
self.scope["runtime"] = str(runtime or "").strip().casefold()
|
|
26
29
|
self.scope["started_at"] = (
|
|
@@ -31,6 +34,7 @@ class ChannelTranscriptRepository:
|
|
|
31
34
|
if codex_home is not None
|
|
32
35
|
else None
|
|
33
36
|
)
|
|
37
|
+
self.scope["cwd"] = Path(cwd).expanduser() if cwd is not None else None
|
|
34
38
|
self.cache.clear()
|
|
35
39
|
self.cache.update({"checked_at": 0.0, "path": None})
|
|
36
40
|
|
|
@@ -59,6 +63,8 @@ class ChannelTranscriptRepository:
|
|
|
59
63
|
latest: Path | None = None
|
|
60
64
|
latest_mtime = -1.0
|
|
61
65
|
scope_started_at = float(self.scope.get("started_at") or 0.0)
|
|
66
|
+
runtime = str(self.scope.get("runtime") or "").strip().casefold()
|
|
67
|
+
scope_cwd = self._normalized_cwd(self.scope.get("cwd"))
|
|
62
68
|
for root, pattern in self.roots():
|
|
63
69
|
try:
|
|
64
70
|
paths = root.glob(pattern)
|
|
@@ -71,6 +77,17 @@ class ChannelTranscriptRepository:
|
|
|
71
77
|
continue
|
|
72
78
|
if scope_started_at > 0 and mtime < scope_started_at - 1.0:
|
|
73
79
|
continue
|
|
80
|
+
if runtime == "codex" and (scope_started_at > 0 or scope_cwd):
|
|
81
|
+
session_started_at, session_cwd = self._codex_session_identity(path)
|
|
82
|
+
if (
|
|
83
|
+
session_started_at is not None
|
|
84
|
+
and scope_started_at > 0
|
|
85
|
+
and session_started_at < scope_started_at - 1.0
|
|
86
|
+
):
|
|
87
|
+
continue
|
|
88
|
+
normalized_session_cwd = self._normalized_cwd(session_cwd)
|
|
89
|
+
if scope_cwd and normalized_session_cwd and normalized_session_cwd != scope_cwd:
|
|
90
|
+
continue
|
|
74
91
|
if mtime > latest_mtime:
|
|
75
92
|
latest = path
|
|
76
93
|
latest_mtime = mtime
|
|
@@ -78,6 +95,36 @@ class ChannelTranscriptRepository:
|
|
|
78
95
|
self.cache["path"] = latest
|
|
79
96
|
return latest
|
|
80
97
|
|
|
98
|
+
@staticmethod
|
|
99
|
+
def _normalized_cwd(value: Any) -> str:
|
|
100
|
+
if value is None:
|
|
101
|
+
return ""
|
|
102
|
+
return str(value).strip().replace("\\", "/").rstrip("/").casefold()
|
|
103
|
+
|
|
104
|
+
@staticmethod
|
|
105
|
+
def _codex_session_identity(path: Path) -> tuple[float | None, str]:
|
|
106
|
+
try:
|
|
107
|
+
with path.open("r", encoding="utf-8", errors="replace") as stream:
|
|
108
|
+
record = json.loads(stream.readline(64 * 1024))
|
|
109
|
+
except (OSError, UnicodeError, ValueError, TypeError):
|
|
110
|
+
return None, ""
|
|
111
|
+
if not isinstance(record, dict):
|
|
112
|
+
return None, ""
|
|
113
|
+
payload = record.get("payload")
|
|
114
|
+
metadata = payload if isinstance(payload, dict) else {}
|
|
115
|
+
raw_timestamp = metadata.get("timestamp") or record.get("timestamp")
|
|
116
|
+
started_at: float | None = None
|
|
117
|
+
if isinstance(raw_timestamp, (int, float)):
|
|
118
|
+
started_at = float(raw_timestamp)
|
|
119
|
+
elif isinstance(raw_timestamp, str) and raw_timestamp.strip():
|
|
120
|
+
try:
|
|
121
|
+
started_at = datetime.fromisoformat(
|
|
122
|
+
raw_timestamp.strip().replace("Z", "+00:00")
|
|
123
|
+
).timestamp()
|
|
124
|
+
except ValueError:
|
|
125
|
+
started_at = None
|
|
126
|
+
return started_at, str(metadata.get("cwd") or "")
|
|
127
|
+
|
|
81
128
|
@staticmethod
|
|
82
129
|
def read_tail_text(
|
|
83
130
|
path: Path,
|
|
@@ -358,9 +358,10 @@ class ChannelWakeContext:
|
|
|
358
358
|
*,
|
|
359
359
|
started_at: float | None = None,
|
|
360
360
|
codex_home: Path | None = None,
|
|
361
|
+
cwd: Path | None = None,
|
|
361
362
|
) -> None:
|
|
362
363
|
self.transcript_repository().set_scope(
|
|
363
|
-
runtime, started_at=started_at, codex_home=codex_home
|
|
364
|
+
runtime, started_at=started_at, codex_home=codex_home, cwd=cwd
|
|
364
365
|
)
|
|
365
366
|
|
|
366
367
|
def transcript_roots(self) -> tuple[tuple[Path, str], ...]:
|
|
@@ -1000,6 +1000,7 @@ def run_codex(
|
|
|
1000
1000
|
_set_channel_transcript_scope(
|
|
1001
1001
|
"codex",
|
|
1002
1002
|
codex_home=Path(env.get("CODEX_HOME") or (Path.home() / ".codex")),
|
|
1003
|
+
cwd=launch_cwd,
|
|
1003
1004
|
)
|
|
1004
1005
|
if not use_native_codex:
|
|
1005
1006
|
start_codex_mcp_channel_sse_for_launch(cfg, codex_mcp_config, allowed_server_names=codex_channel_owned_names)
|
|
@@ -303,6 +303,11 @@ def render_web_chat_page(
|
|
|
303
303
|
let mediaRecorder = null;
|
|
304
304
|
let mediaStream = null;
|
|
305
305
|
let recordingChunks = [];
|
|
306
|
+
let audioContext = null;
|
|
307
|
+
let audioInput = null;
|
|
308
|
+
let audioProcessor = null;
|
|
309
|
+
let pcmChunks = [];
|
|
310
|
+
let voiceRecording = false;
|
|
306
311
|
let pendingTtsReferenceAudio = '';
|
|
307
312
|
function setState(text, cls = '') {{
|
|
308
313
|
statePill.textContent = text;
|
|
@@ -709,27 +714,91 @@ def render_web_chat_page(
|
|
|
709
714
|
prompt.focus();
|
|
710
715
|
setState('transcribed', 'ok');
|
|
711
716
|
}}
|
|
717
|
+
function encodePcmWav(chunks, sampleRate) {{
|
|
718
|
+
const sampleCount = chunks.reduce((total, chunk) => total + chunk.length, 0);
|
|
719
|
+
const buffer = new ArrayBuffer(44 + sampleCount * 2);
|
|
720
|
+
const view = new DataView(buffer);
|
|
721
|
+
const writeAscii = (offset, value) => {{
|
|
722
|
+
for (let index = 0; index < value.length; index += 1) view.setUint8(offset + index, value.charCodeAt(index));
|
|
723
|
+
}};
|
|
724
|
+
writeAscii(0, 'RIFF');
|
|
725
|
+
view.setUint32(4, 36 + sampleCount * 2, true);
|
|
726
|
+
writeAscii(8, 'WAVE');
|
|
727
|
+
writeAscii(12, 'fmt ');
|
|
728
|
+
view.setUint32(16, 16, true);
|
|
729
|
+
view.setUint16(20, 1, true);
|
|
730
|
+
view.setUint16(22, 1, true);
|
|
731
|
+
view.setUint32(24, sampleRate, true);
|
|
732
|
+
view.setUint32(28, sampleRate * 2, true);
|
|
733
|
+
view.setUint16(32, 2, true);
|
|
734
|
+
view.setUint16(34, 16, true);
|
|
735
|
+
writeAscii(36, 'data');
|
|
736
|
+
view.setUint32(40, sampleCount * 2, true);
|
|
737
|
+
let offset = 44;
|
|
738
|
+
chunks.forEach(chunk => chunk.forEach(rawSample => {{
|
|
739
|
+
const sample = Math.max(-1, Math.min(1, rawSample));
|
|
740
|
+
view.setInt16(offset, sample < 0 ? sample * 0x8000 : sample * 0x7fff, true);
|
|
741
|
+
offset += 2;
|
|
742
|
+
}}));
|
|
743
|
+
return new Blob([buffer], {{type: 'audio/wav'}});
|
|
744
|
+
}}
|
|
745
|
+
async function finishVoiceInput(blob) {{
|
|
746
|
+
if (mediaStream) mediaStream.getTracks().forEach(track => track.stop());
|
|
747
|
+
mediaStream = null;
|
|
748
|
+
micButton.textContent = 'Start voice input';
|
|
749
|
+
micButton.classList.remove('recording');
|
|
750
|
+
try {{ await transcribeRecording(blob); }} catch (err) {{ setState('STT error', 'error'); addBubble('system', 'STT failed: ' + String(err && err.message ? err.message : err)); }}
|
|
751
|
+
}}
|
|
712
752
|
async function startVoiceInput() {{
|
|
713
|
-
if (!navigator.mediaDevices
|
|
753
|
+
if (!navigator.mediaDevices) throw new Error('This browser does not support microphone recording');
|
|
714
754
|
mediaStream = await navigator.mediaDevices.getUserMedia({{audio: true}});
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
755
|
+
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
|
|
756
|
+
if (AudioContextClass) {{
|
|
757
|
+
audioContext = new AudioContextClass();
|
|
758
|
+
if (audioContext.state === 'suspended') await audioContext.resume();
|
|
759
|
+
audioInput = audioContext.createMediaStreamSource(mediaStream);
|
|
760
|
+
audioProcessor = audioContext.createScriptProcessor(4096, 1, 1);
|
|
761
|
+
pcmChunks = [];
|
|
762
|
+
audioProcessor.onaudioprocess = event => pcmChunks.push(new Float32Array(event.inputBuffer.getChannelData(0)));
|
|
763
|
+
audioInput.connect(audioProcessor);
|
|
764
|
+
audioProcessor.connect(audioContext.destination);
|
|
765
|
+
}} else if (window.MediaRecorder) {{
|
|
766
|
+
recordingChunks = [];
|
|
767
|
+
mediaRecorder = new MediaRecorder(mediaStream);
|
|
768
|
+
mediaRecorder.addEventListener('dataavailable', event => {{ if (event.data && event.data.size) recordingChunks.push(event.data); }});
|
|
769
|
+
mediaRecorder.addEventListener('stop', async () => {{
|
|
770
|
+
const blob = new Blob(recordingChunks, {{type: mediaRecorder.mimeType || 'audio/webm'}});
|
|
771
|
+
await finishVoiceInput(blob);
|
|
772
|
+
}}, {{once: true}});
|
|
773
|
+
mediaRecorder.start();
|
|
774
|
+
}} else {{
|
|
775
|
+
mediaStream.getTracks().forEach(track => track.stop());
|
|
721
776
|
mediaStream = null;
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
}}, {{once: true}});
|
|
726
|
-
mediaRecorder.start();
|
|
777
|
+
throw new Error('This browser does not support microphone recording');
|
|
778
|
+
}}
|
|
779
|
+
voiceRecording = true;
|
|
727
780
|
micButton.textContent = 'Stop and transcribe';
|
|
728
781
|
micButton.classList.add('recording');
|
|
729
782
|
setState('recording', 'error');
|
|
730
783
|
}}
|
|
731
|
-
function stopVoiceInput() {{
|
|
732
|
-
if (
|
|
784
|
+
async function stopVoiceInput() {{
|
|
785
|
+
if (!voiceRecording) return;
|
|
786
|
+
voiceRecording = false;
|
|
787
|
+
if (audioProcessor && audioContext) {{
|
|
788
|
+
const sampleRate = audioContext.sampleRate;
|
|
789
|
+
audioProcessor.onaudioprocess = null;
|
|
790
|
+
audioInput.disconnect();
|
|
791
|
+
audioProcessor.disconnect();
|
|
792
|
+
await audioContext.close();
|
|
793
|
+
const blob = encodePcmWav(pcmChunks, sampleRate);
|
|
794
|
+
audioContext = null;
|
|
795
|
+
audioInput = null;
|
|
796
|
+
audioProcessor = null;
|
|
797
|
+
pcmChunks = [];
|
|
798
|
+
await finishVoiceInput(blob);
|
|
799
|
+
}} else if (mediaRecorder && mediaRecorder.state !== 'inactive') {{
|
|
800
|
+
mediaRecorder.stop();
|
|
801
|
+
}}
|
|
733
802
|
}}
|
|
734
803
|
async function uploadAttachment(file) {{
|
|
735
804
|
const content = await fileToBase64(file);
|
|
@@ -924,8 +993,8 @@ def render_web_chat_page(
|
|
|
924
993
|
}});
|
|
925
994
|
attachButton.addEventListener('click', () => fileInput.click());
|
|
926
995
|
micButton.addEventListener('click', async () => {{
|
|
927
|
-
if (
|
|
928
|
-
stopVoiceInput();
|
|
996
|
+
if (voiceRecording) {{
|
|
997
|
+
await stopVoiceInput();
|
|
929
998
|
return;
|
|
930
999
|
}}
|
|
931
1000
|
try {{ await startVoiceInput(); }} catch (err) {{
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|