Agent Zero AI framework
native models, log trunc fix, speech skip
moved whisper and kokoro to native runtime log kvps truncation fix skip speech on context switch
frdel committed
Jul 14, 2025 at 14:30 UTC
dcae2b74ac8e05e8ebb2f49ef2aa3a88dcb6cf6e
7 files changed
+126
-58
preload.py
+5
-1
@@ -40,7 +40,11 @@ async def preload():
40
PrintStyle().error(f"Error in preload_kokoro: {e}")
41
42
# async tasks to preload
43
- tasks = [preload_whisper(), preload_embedding(), preload_kokoro()]
43
+ tasks = [
44
+ preload_embedding(),
45
+ # preload_whisper(),
46
+ # preload_kokoro()
47
+ ] # no longer preload kokoro and whisper, do it JIT
48
49
await asyncio.gather(*tasks, return_exceptions=True)
50
PrintStyle().print("Preload completed")
python/api/synthesize.py
+1
-1
@@ -12,7 +12,7 @@ class Synthesize(ApiHandler):
12
ctxid = input.get("ctxid", "")
13
14
context = self.get_context(ctxid)
15
- if await kokoro_tts.is_downloading():
15
+ if not await kokoro_tts.is_downloaded():
16
context.log.log(type="info", content="Kokoro TTS model is currently being downloaded, please wait...")
17
18
try:
python/api/transcribe.py
+2
-2
@@ -9,8 +9,8 @@ class Transcribe(ApiHandler):
9
ctxid = input.get("ctxid", "")
10
11
context = self.get_context(ctxid)
12
- if await whisper.is_downloading():
13
- context.log.log(type="info", content="Whisper model is currently being downloaded, please wait...")
12
+ if not await whisper.is_downloaded():
13
+ context.log.log(type="info", content="Whisper STT model is currently being downloaded, please wait...")
14
15
set = settings.get_settings()
16
result = await whisper.transcribe(set["stt_model_size"], audio) # type: ignore
python/helpers/kokoro_tts.py
+44
-22
@@ -15,52 +15,74 @@ _voice = "am_puck,am_onyx"
15
_speed = 1.1
16
is_updating_model = False
17
18
+
19
async def preload():
20
try:
20
- return await runtime.call_development_function(_preload)
21
+ # return await runtime.call_development_function(_preload)
22
+ return await _preload()
23
except Exception as e:
22
- if not runtime.is_development():
23
- raise e
24
+ # if not runtime.is_development():
25
+ raise e
26
# Fallback to direct execution if RFC fails in development
25
- PrintStyle.standard("RFC failed, falling back to direct execution...")
26
- return await _preload()
27
+ # PrintStyle.standard("RFC failed, falling back to direct execution...")
28
+ # return await _preload()
29
+
30
31
async def _preload():
32
global _pipeline, is_updating_model
30
-
33
+
34
while is_updating_model:
35
await asyncio.sleep(0.1)
33
-
36
+
37
try:
38
is_updating_model = True
39
if not _pipeline:
40
PrintStyle.standard("Loading Kokoro TTS model...")
41
from kokoro import KPipeline
39
- _pipeline = KPipeline(lang_code='a')
42
+ _pipeline = KPipeline(lang_code="a")
43
finally:
44
is_updating_model = False
45
46
+
47
async def is_downloading():
48
try:
45
- return await runtime.call_development_function(_is_downloading)
49
+ # return await runtime.call_development_function(_is_downloading)
50
+ return _is_downloading()
51
except Exception as e:
47
- if not runtime.is_development():
48
- raise e
52
+ # if not runtime.is_development():
53
+ raise e
54
# Fallback to direct execution if RFC fails in development
50
- return _is_downloading()
55
+ # return _is_downloading()
56
+
57
58
def _is_downloading():
59
return is_updating_model
60
61
+async def is_downloaded():
62
+ try:
63
+ # return await runtime.call_development_function(_is_downloaded)
64
+ return _is_downloaded()
65
+ except Exception as e:
66
+ # if not runtime.is_development():
67
+ raise e
68
+ # Fallback to direct execution if RFC fails in development
69
+ # return _is_downloaded()
70
+
71
+def _is_downloaded():
72
+ return _pipeline is not None
73
+
74
+
75
async def synthesize_sentences(sentences: list[str]):
76
"""Generate audio for multiple sentences and return concatenated base64 audio"""
77
try:
58
- return await runtime.call_development_function(_synthesize_sentences, sentences)
78
+ # return await runtime.call_development_function(_synthesize_sentences, sentences)
79
+ return await _synthesize_sentences(sentences)
80
except Exception as e:
60
- if not runtime.is_development():
61
- raise e
81
+ # if not runtime.is_development():
82
+ raise e
83
# Fallback to direct execution if RFC fails in development
63
- return await _synthesize_sentences(sentences)
84
+ # return await _synthesize_sentences(sentences)
85
+
86
87
async def _synthesize_sentences(sentences: list[str]):
88
await _preload()
@@ -70,22 +92,22 @@ async def _synthesize_sentences(sentences: list[str]):
92
try:
93
for sentence in sentences:
94
if sentence.strip():
73
- segments = _pipeline(sentence.strip(), voice=_voice, speed=_speed)
95
+ segments = _pipeline(sentence.strip(), voice=_voice, speed=_speed) # type: ignore
96
segment_list = list(segments)
75
-
97
+
98
for segment in segment_list:
99
audio_tensor = segment.audio
78
- audio_numpy = audio_tensor.detach().cpu().numpy()
100
+ audio_numpy = audio_tensor.detach().cpu().numpy() # type: ignore
101
combined_audio.extend(audio_numpy)
102
103
# Convert combined audio to bytes
104
buffer = io.BytesIO()
83
- sf.write(buffer, combined_audio, 24000, format='WAV')
105
+ sf.write(buffer, combined_audio, 24000, format="WAV")
106
audio_bytes = buffer.getvalue()
107
108
# Return base64 encoded audio
87
- return base64.b64encode(audio_bytes).decode('utf-8')
109
+ return base64.b64encode(audio_bytes).decode("utf-8")
110
111
except Exception as e:
112
PrintStyle.error(f"Error in Kokoro TTS synthesis: {e}")
91
- raise
\ No newline at end of file
113
+ raise
\ No newline at end of file
python/helpers/log.py
+31
-17
@@ -4,6 +4,7 @@ from typing import Any, Literal, Optional, Dict
4
import uuid
5
from collections import OrderedDict # Import OrderedDict
6
from python.helpers.strings import truncate_text_by_ratio
7
+import copy
8
9
Type = Literal[
10
"agent",
@@ -25,9 +26,9 @@ ProgressUpdate = Literal["persistent", "temporary", "none"]
26
27
28
HEADING_MAX_LEN: int = 120
28
-CONTENT_MAX_LEN: int = 4000
29
+CONTENT_MAX_LEN: int = 10000
30
KEY_MAX_LEN: int = 60
30
-VALUE_MAX_LEN: int = 1000
31
+VALUE_MAX_LEN: int = 3000
32
PROGRESS_MAX_LEN: int = 120
33
34
@@ -44,29 +45,36 @@ def _truncate_progress(text: str | None) -> str:
45
def _truncate_key(text: str) -> str:
46
return truncate_text_by_ratio(str(text), KEY_MAX_LEN, "...", ratio=1.0)
47
47
-def _truncate_value(text: Any) -> Any:
48
+def _truncate_value(val: Any) -> Any:
49
+ # If dict, recursively truncate each value
50
+ if isinstance(val, dict):
51
+ for k in list(val.keys()):
52
+ val[k] = _truncate_value(val[k])
53
+ return val
54
+ # If list or tuple, recursively truncate each item
55
+ if isinstance(val, list):
56
+ for i in range(len(val)):
57
+ val[i] = _truncate_value(val[i])
58
+ return val
59
+ if isinstance(val, tuple):
60
+ return tuple(_truncate_value(x) for x in val)
61
+
62
# Convert non-str values to json for consistent length measurement
49
- if isinstance(text, str):
50
- raw = text
63
+ if isinstance(val, str):
64
+ raw = val
65
else:
66
try:
53
- raw = json.dumps(text, ensure_ascii=False)
67
+ raw = json.dumps(val, ensure_ascii=False)
68
except Exception:
55
- raw = str(text)
69
+ raw = str(val)
70
71
if len(raw) <= VALUE_MAX_LEN:
58
- return text # No truncation needed, preserve original type
72
+ return val # No truncation needed, preserve original type
73
60
- # Determine removed characters dynamically to build replacement string
74
+ # Do a single truncation calculation
75
removed = len(raw) - VALUE_MAX_LEN
62
- while True:
63
- replacement = f"\n\n<< {removed} Characters hidden >>\n\n"
64
- truncated = truncate_text_by_ratio(raw, VALUE_MAX_LEN, replacement, ratio=0.3)
65
- new_removed = len(raw) - (len(truncated) - len(replacement))
66
- if new_removed == removed:
67
- break
68
- removed = new_removed
69
-
76
+ replacement = f"\n\n<< {removed} Characters hidden >>\n\n"
77
+ truncated = truncate_text_by_ratio(raw, VALUE_MAX_LEN, replacement, ratio=0.3)
78
return truncated
79
80
def _truncate_content(text: str | None) -> str:
@@ -177,10 +185,13 @@ class Log:
185
186
# Truncate kvps
187
if kvps is not None:
188
+ kvps = copy.deepcopy(kvps) # deep copy to avoid modifying the original kvps
189
kvps = OrderedDict({
190
_truncate_key(k): _truncate_value(v) for k, v in kvps.items()
191
})
192
# Apply truncation to kwargs merged into kvps later
193
+ if kwargs is not None:
194
+ kwargs = copy.deepcopy(kwargs) # deep copy to avoid modifying the original kwargs
195
kwargs = { _truncate_key(k): _truncate_value(v) for k, v in (kwargs or {}).items() }
196
197
# Ensure kvps is OrderedDict even if None
@@ -231,6 +242,7 @@ class Log:
242
item.content = _truncate_content(content)
243
244
if kvps is not None:
245
+ kvps = copy.deepcopy(kvps) # deep copy to avoid modifying the original kvps
246
item.kvps = OrderedDict({
247
_truncate_key(k): _truncate_value(v) for k, v in kvps.items()
248
}) # Ensure order
@@ -239,11 +251,13 @@ class Log:
251
item.temp = temp
252
253
if kwargs:
254
+ kwargs = copy.deepcopy(kwargs) # deep copy to avoid modifying the original kwargs
255
if item.kvps is None:
256
item.kvps = OrderedDict() # Ensure kvps is an OrderedDict
257
for k, v in kwargs.items():
258
item.kvps[_truncate_key(k)] = _truncate_value(v)
259
260
+
261
self.updates += [item.no]
262
self._update_progress_from_item(item)
263
python/helpers/whisper.py
+23
-7
@@ -3,7 +3,7 @@ import warnings
3
import whisper
4
import tempfile
5
import asyncio
6
-from python.helpers import runtime, rfc, settings
6
+from python.helpers import runtime, rfc, settings, files
7
from python.helpers.print_style import PrintStyle
8
9
# Suppress FutureWarning from torch.load
@@ -15,10 +15,11 @@ is_updating_model = False # Tracks whether the model is currently updating
15
16
async def preload(model_name:str):
17
try:
18
- return await runtime.call_development_function(_preload, model_name)
18
+ # return await runtime.call_development_function(_preload, model_name)
19
+ return await _preload(model_name)
20
except Exception as e:
20
- if not runtime.is_development():
21
- raise e
21
+ # if not runtime.is_development():
22
+ raise e
23
24
async def _preload(model_name:str):
25
global _model, _model_name, is_updating_model
@@ -30,19 +31,34 @@ async def _preload(model_name:str):
31
is_updating_model = True
32
if not _model or _model_name != model_name:
33
PrintStyle.standard(f"Loading Whisper model: {model_name}")
33
- _model = whisper.load_model(name=model_name) # type: ignore
34
+ _model = whisper.load_model(name=model_name, download_root=files.get_abs_path("/tmp/models/whisper")) # type: ignore
35
_model_name = model_name
36
finally:
37
is_updating_model = False
38
39
async def is_downloading():
39
- return await runtime.call_development_function(_is_downloading)
40
+ # return await runtime.call_development_function(_is_downloading)
41
+ return _is_downloading()
42
43
def _is_downloading():
44
return is_updating_model
45
46
+async def is_downloaded():
47
+ try:
48
+ # return await runtime.call_development_function(_is_downloaded)
49
+ return _is_downloaded()
50
+ except Exception as e:
51
+ # if not runtime.is_development():
52
+ raise e
53
+ # Fallback to direct execution if RFC fails in development
54
+ # return _is_downloaded()
55
+
56
+def _is_downloaded():
57
+ return _model is not None
58
+
59
async def transcribe(model_name:str, audio_bytes_b64: str):
45
- return await runtime.call_development_function(_transcribe, model_name, audio_bytes_b64)
60
+ # return await runtime.call_development_function(_transcribe, model_name, audio_bytes_b64)
61
+ return await _transcribe(model_name, audio_bytes_b64)
62
63
64
async def _transcribe(model_name:str, audio_bytes_b64: str):
webui/index.js
+20
-8
@@ -24,6 +24,7 @@ const timeDate = document.getElementById("time-date-container");
24
let autoScroll = true;
25
let context = "";
26
let resetCounter = 0;
27
+let skipOneSpeech = false;
28
let connectionStatus = false;
29
30
// Initialize the toggle button
@@ -503,6 +504,10 @@ function afterMessagesUpdate(logs) {
504
}
505
506
function speakMessages(logs) {
507
+ if (skipOneSpeech) {
508
+ skipOneSpeech = false;
509
+ return;
510
+ }
511
// log.no, log.type, log.heading, log.content
512
for (let i = logs.length - 1; i >= 0; i--) {
513
const log = logs[i];
@@ -512,10 +517,14 @@ function speakMessages(logs) {
517
518
// finished response
519
if (log.type == "response") {
515
- // lastSpokenNo = log.no;
516
- speechStore.speakStream(getChatBasedId(log.no), log.content, log.kvps?.finished);
517
- return;
518
-
520
+ // lastSpokenNo = log.no;
521
+ speechStore.speakStream(
522
+ getChatBasedId(log.no),
523
+ log.content,
524
+ log.kvps?.finished
525
+ );
526
+ return;
527
+
528
// finished LLM headline, not response
529
} else if (
530
log.type == "agent" &&
@@ -524,9 +533,9 @@ function speakMessages(logs) {
533
log.kvps.tool_args &&
534
log.kvps.tool_name != "response"
535
) {
527
- // lastSpokenNo = log.no;
528
- speechStore.speakStream(getChatBasedId(log.no), log.kvps.headline, true);
529
- return;
536
+ // lastSpokenNo = log.no;
537
+ speechStore.speakStream(getChatBasedId(log.no), log.kvps.headline, true);
538
+ return;
539
}
540
}
541
}
@@ -736,6 +745,9 @@ export const setContext = function (id) {
745
if (tasksAD) tasksAD.selected = id;
746
}
747
}
748
+
749
+ //skip one speech if enabled when switching context
750
+ if (localStorage.getItem("speech") == "true") skipOneSpeech = true;
751
};
752
753
export const getContext = function () {
@@ -743,7 +755,7 @@ export const getContext = function () {
755
};
756
757
export const getChatBasedId = function (id) {
746
- return context+"-"+resetCounter+"-"+id;
758
+ return context + "-" + resetCounter + "-" + id;
759
};
760
761
window.toggleAutoScroll = async function (_autoScroll) {