from __future__ import annotations import asyncio import time class BilibiliBlockChunker: def __init__( self, outbound, target_id: str, anti_risk, chunk_min_chars: int = 200, chunk_max_chars: int = 1200, coalesce_min_chars: int = 80, coalesce_max_chars: int = 400, coalesce_idle_ms: int = 500, ): self._outbound = outbound self._target_id = target_id self._anti_risk = anti_risk self._chunk_min = chunk_min_chars self._chunk_max = chunk_max_chars self._coalesce_min = coalesce_min_chars self._coalesce_max = coalesce_max_chars self._coalesce_idle = coalesce_idle_ms / 1000.0 self._buffer = "" self._last_flush = time.time() async def append(self, text: str) -> None: self._buffer += text should_flush = len(self._buffer) >= self._chunk_max or ( len(self._buffer) >= self._chunk_min and time.time() - self._last_flush > self._coalesce_idle ) if should_flush: await self._flush() async def finalize(self) -> None: if self._buffer: await self._flush() async def _flush(self) -> None: chunks = self._split_chunks(self._buffer, self._chunk_max) for chunk in chunks: await self._outbound.send_text(to=self._target_id, text=chunk) await asyncio.sleep(0.5) self._buffer = "" self._last_flush = time.time() def _split_chunks(self, text: str, limit: int) -> list[str]: if len(text) <= limit: return [text] if text else [] chunks = [] remaining = text while len(remaining) > limit: window = remaining[:limit] last_para = window.rfind("\n\n") if last_para > limit * 0.5: chunks.append(window[:last_para].strip()) remaining = remaining[last_para + 2 :] continue for sep in ["。\n", "。", "!", "?", "\n", " "]: idx = window.rfind(sep) if idx > limit * 0.3: chunks.append(window[: idx + len(sep)].strip()) remaining = remaining[idx + len(sep) :] break else: chunks.append(window.strip()) remaining = remaining[limit:] if remaining.strip(): chunks.append(remaining.strip()) return chunks