157 lines
5.0 KiB
Python
157 lines
5.0 KiB
Python
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import asyncio
|
||
|
|
import logging
|
||
|
|
|
||
|
|
logger = logging.getLogger(__name__)
|
||
|
|
|
||
|
|
|
||
|
|
class TelegramStreaming:
|
||
|
|
streaming_mode = "partial"
|
||
|
|
preview_stream_throttle_ms = 160
|
||
|
|
preview_min_initial_chars = 18
|
||
|
|
|
||
|
|
block_streaming_enabled = True
|
||
|
|
block_streaming_break = "text_end"
|
||
|
|
block_streaming_chunk_min_chars = 800
|
||
|
|
block_streaming_chunk_max_chars = 1200
|
||
|
|
block_streaming_chunk_break_preference = "paragraph"
|
||
|
|
block_streaming_coalesce_defaults = {
|
||
|
|
"min_chars": 400,
|
||
|
|
"max_chars": 800,
|
||
|
|
"idle_ms": 1000,
|
||
|
|
}
|
||
|
|
|
||
|
|
def create_draft_stream_session(self, target_id: str) -> dict:
|
||
|
|
return {"target_id": target_id, "mode": "partial", "message_id": None}
|
||
|
|
|
||
|
|
def create_block_chunker(self) -> dict:
|
||
|
|
return {
|
||
|
|
"mode": "length",
|
||
|
|
"min_chars": self.block_streaming_chunk_min_chars,
|
||
|
|
"max_chars": self.block_streaming_chunk_max_chars,
|
||
|
|
}
|
||
|
|
|
||
|
|
def create_preview_stream(
|
||
|
|
self,
|
||
|
|
outbound,
|
||
|
|
target_id: str,
|
||
|
|
account_id: str,
|
||
|
|
*,
|
||
|
|
thread_id: str | None = None,
|
||
|
|
throttle_ms: int | None = None,
|
||
|
|
min_initial_chars: int | None = None,
|
||
|
|
chunk_limit: int = 4096,
|
||
|
|
):
|
||
|
|
return PreviewStream(
|
||
|
|
outbound=outbound,
|
||
|
|
target_id=target_id,
|
||
|
|
account_id=account_id,
|
||
|
|
thread_id=thread_id,
|
||
|
|
throttle_ms=throttle_ms or self.preview_stream_throttle_ms,
|
||
|
|
min_initial_chars=min_initial_chars or self.preview_min_initial_chars,
|
||
|
|
chunk_limit=chunk_limit,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
class PreviewStream:
|
||
|
|
def __init__(
|
||
|
|
self,
|
||
|
|
outbound,
|
||
|
|
target_id: str,
|
||
|
|
account_id: str,
|
||
|
|
*,
|
||
|
|
thread_id: str | None = None,
|
||
|
|
throttle_ms: int = 160,
|
||
|
|
min_initial_chars: int = 18,
|
||
|
|
chunk_limit: int = 4096,
|
||
|
|
):
|
||
|
|
self._outbound = outbound
|
||
|
|
self._target_id = target_id
|
||
|
|
self._account_id = account_id
|
||
|
|
self._thread_id = thread_id
|
||
|
|
self._throttle_ms = throttle_ms
|
||
|
|
self._min_initial_chars = min_initial_chars
|
||
|
|
self._chunk_limit = chunk_limit
|
||
|
|
|
||
|
|
self._buffer = ""
|
||
|
|
self._draft_message_id: str | None = None
|
||
|
|
self._sent_length = 0
|
||
|
|
self._last_edit_time = 0.0
|
||
|
|
self._queue: asyncio.Queue[str] = asyncio.Queue()
|
||
|
|
self._done = asyncio.Event()
|
||
|
|
self._final_text = ""
|
||
|
|
|
||
|
|
async def feed(self, token: str) -> None:
|
||
|
|
self._buffer += token
|
||
|
|
await self._queue.put(token)
|
||
|
|
|
||
|
|
async def finalize(self, final_text: str) -> None:
|
||
|
|
self._final_text = final_text
|
||
|
|
self._done.set()
|
||
|
|
|
||
|
|
if self._draft_message_id:
|
||
|
|
try:
|
||
|
|
await self._outbound.edit_message(
|
||
|
|
self._target_id,
|
||
|
|
self._draft_message_id,
|
||
|
|
final_text,
|
||
|
|
account_id=self._account_id,
|
||
|
|
)
|
||
|
|
except Exception:
|
||
|
|
logger.exception("PreviewStream finalize edit failed for msg %s", self._draft_message_id)
|
||
|
|
else:
|
||
|
|
try:
|
||
|
|
await self._outbound.send_text_raw(
|
||
|
|
self._target_id,
|
||
|
|
final_text,
|
||
|
|
account_id=self._account_id,
|
||
|
|
thread_id=self._thread_id,
|
||
|
|
)
|
||
|
|
except Exception:
|
||
|
|
logger.exception("PreviewStream finalize send failed")
|
||
|
|
|
||
|
|
async def run(self) -> None:
|
||
|
|
loop = asyncio.get_running_loop()
|
||
|
|
|
||
|
|
while not self._done.is_set():
|
||
|
|
try:
|
||
|
|
await asyncio.wait_for(self._queue.get(), timeout=0.5)
|
||
|
|
except TimeoutError:
|
||
|
|
continue
|
||
|
|
|
||
|
|
current_len = len(self._buffer)
|
||
|
|
|
||
|
|
if self._draft_message_id is None:
|
||
|
|
if current_len >= self._min_initial_chars:
|
||
|
|
try:
|
||
|
|
self._draft_message_id = await self._outbound.send_text_raw(
|
||
|
|
self._target_id,
|
||
|
|
self._buffer,
|
||
|
|
account_id=self._account_id,
|
||
|
|
thread_id=self._thread_id,
|
||
|
|
)
|
||
|
|
self._last_edit_time = loop.time()
|
||
|
|
self._sent_length = current_len
|
||
|
|
except Exception:
|
||
|
|
logger.exception("PreviewStream initial send failed")
|
||
|
|
continue
|
||
|
|
|
||
|
|
if current_len <= self._sent_length:
|
||
|
|
continue
|
||
|
|
|
||
|
|
now = loop.time()
|
||
|
|
if now - self._last_edit_time >= self._throttle_ms / 1000:
|
||
|
|
try:
|
||
|
|
display = self._buffer[:self._chunk_limit]
|
||
|
|
await self._outbound.edit_message(
|
||
|
|
self._target_id,
|
||
|
|
self._draft_message_id,
|
||
|
|
display,
|
||
|
|
account_id=self._account_id,
|
||
|
|
)
|
||
|
|
self._last_edit_time = now
|
||
|
|
self._sent_length = current_len
|
||
|
|
except Exception:
|
||
|
|
pass
|