Scrypt / scrypt /warden /voice.py
IMJONEZZ's picture
SCRYPT: initial commit — game, sandbox, Warden, Space web layer
9fca766
Raw
History Blame Contribute Delete
2.5 kB
"""The Warden's voice: live LLM dialogue with scripted fallback.
The contract with the UI: react() yields a sanitized line; if the backend
is slow, dead, or rude, it yields nothing and the caller plays a scripted
line instead. The game never stalls and never shows raw output.
Lines are buffered and sanitized BEFORE any text is released — partial
streaming could leak the prefix of a blocked line. The UI's typewriter
provides the gradual reveal instead, so nothing is lost dramatically.
"""
from __future__ import annotations
import asyncio
import random
from typing import AsyncIterator
from scrypt.inference.backend import Backend
from .context import build_messages
from .guardrails import clean_dialogue
from .memory import ShardStore
TOTAL_TIMEOUT = 25.0 # CPU prefill on a cold cache is the slow case
# How often the machine thinks out loud: the reasoning block is enabled
# for this fraction of generations and surfaced as a terminal comment.
REVEAL_ODDS = 1 / 3
THINKING_MAX_TOKENS = 192 # room for the thought AND the line
class WardenVoice:
def __init__(
self,
backend: Backend,
store: ShardStore | None = None,
rng: random.Random | None = None,
):
self.backend = backend
self.store = store or ShardStore()
self.rng = rng or random.Random()
async def react(
self,
moment: str,
*,
digest: str = "",
tags: set[str] | None = None,
taboo: str = "",
) -> AsyncIterator[str]:
"""Stream a one-liner reaction to a game moment. taboo: raw player
text the reply must never parrot (echo-exfiltration guard)."""
messages = build_messages(
f"MOMENT\n{moment}\n\nReact in one short line of at most 18 words, in voice.",
digest=digest,
shards=self.store.render(tags or set()),
)
thinking = self.rng.random() < REVEAL_ODDS
buffer = ""
try:
async with asyncio.timeout(TOTAL_TIMEOUT):
async for chunk in self.backend.stream(
messages,
max_tokens=THINKING_MAX_TOKENS if thinking else 48,
temperature=0.6,
thinking=thinking,
):
buffer += chunk
except Exception:
pass # keep whatever arrived; a cut-off line beats silence
cleaned = clean_dialogue(buffer, taboo=taboo)
if cleaned:
yield cleaned