mirror of
https://github.com/justLV/onju-v2
synced 2026-04-21 15:47:55 +00:00
- SSE sentence-level streaming: consume agent deltas, split on sentence boundaries (handles no-space chunk joins), synthesize+send each sentence as it forms; intermediate sends keep mic_timeout=0 - Gemini-backed stall classifier for agentic mode only: narrow to retrieval-only, pass prev user/assistant for context awareness, avoid action promises the stall can't honor, sub-second latency via reasoning_effort=none - Rename backends: local -> conversational, managed -> agentic (files, classes, config keys) - PTT interrupt fix: set device.interrupted when button-press frames arrive mid-response and keep buffering so the next utterance captures cleanly instead of being dropped - Startup summary log showing ASR, LLM, STALL, and TTS config at a glance - run.sh launcher with Homebrew libopus path for macOS - voice_prompt config for per-turn agentic reminders; inline continuity note injection so the agent knows what the stall just said aloud - README section on streaming, stalls, and the first-turn OpenClaw caveat
47 lines
1.8 KiB
Python
47 lines
1.8 KiB
Python
import re
|
|
from typing import AsyncIterator
|
|
|
|
from pipeline.conversation.base import ConversationBackend
|
|
from pipeline.conversation.conversational import ConversationalBackend
|
|
from pipeline.conversation.agentic import AgenticBackend
|
|
|
|
# Primary: punctuation followed by whitespace (safe, standard).
|
|
_SENTENCE_END = re.compile(r"[.!?\n]+\s+")
|
|
# Fallback: punctuation with no space, but only when preceded by a lowercase
|
|
# letter and followed by uppercase. Catches OpenClaw's chunk-boundary joins
|
|
# ("now.The") without breaking abbreviations like "U.S." (uppercase before dot).
|
|
_SENTENCE_END_NOSPACE = re.compile(r"(?<=[a-z])[.!?]+(?=[A-Z])")
|
|
|
|
|
|
async def sentence_chunks(deltas: AsyncIterator[str]) -> AsyncIterator[str]:
|
|
"""Buffer text deltas and yield one sentence at a time, plus any trailing
|
|
fragment when the stream ends."""
|
|
buffer = ""
|
|
async for delta in deltas:
|
|
buffer += delta
|
|
while True:
|
|
m = _SENTENCE_END.search(buffer)
|
|
if not m:
|
|
m = _SENTENCE_END_NOSPACE.search(buffer)
|
|
if not m:
|
|
break
|
|
sentence = buffer[: m.end()].strip()
|
|
buffer = buffer[m.end():]
|
|
if sentence:
|
|
yield sentence
|
|
tail = buffer.strip()
|
|
if tail:
|
|
yield tail
|
|
|
|
|
|
def create_backend(config: dict, device_id: str) -> ConversationBackend:
|
|
"""Create a conversation backend based on config."""
|
|
conv_cfg = config["conversation"]
|
|
backend = conv_cfg.get("backend", "conversational")
|
|
|
|
if backend == "conversational":
|
|
return ConversationalBackend(conv_cfg["conversational"], device_id)
|
|
elif backend == "agentic":
|
|
return AgenticBackend(conv_cfg["agentic"], device_id)
|
|
else:
|
|
raise ValueError(f"Unknown conversation backend: {backend}")
|