AgentNewTwo's picture
Smooth playback and guard visitor role
4310ff6
Raw
History Blame Contribute Delete
75.5 kB
from __future__ import annotations
import asyncio
import copy
import difflib
import html
import json
import multiprocessing
import os
import queue
import re
import secrets
import time
import uuid
from collections import deque
from dataclasses import dataclass
import gradio as gr
import numpy as np
import spaces
import torch
from fastapi import WebSocket, WebSocketDisconnect
from gradio.routes import App as GradioApp
print(f"RoleForge live LFM runtime: torch={torch.__version__}, cuda={torch.version.cuda}")
MODEL_REPO = "LiquidAI/LFM2.5-Audio-1.5B"
MODEL_REVISION = "c362a0625dfe45aa588dce5f0ada28a7e5707628"
DEVICE = "cuda"
OUTPUT_SAMPLE_RATE = 24_000
OUTPUT_PACKET_SAMPLES = 3_840
AUDIO_FRAME_SAMPLES = 1_920
STATUS_HEARTBEAT_SECONDS = 2.0
SESSION_SECONDS = 120
GPU_DURATION_SECONDS = 180
MAX_NEW_TOKENS = 768
MAX_TURNS = 6
INPUT_QUEUE_CHUNKS = 180
OUTPUT_QUEUE_PACKETS = 4
DUPLICATE_PREFIX_THRESHOLD = 0.9
MAX_GENERATION_ATTEMPTS = 3
SILENCE_SECONDS = 0.6
MINIMUM_SPEECH_SECONDS = 0.3
MAX_UTTERANCE_SECONDS = 15.0
SPEECH_RMS = 0.012
BARGE_IN_RMS = 0.035
BARGE_IN_CONFIRM_SECONDS = 0.22
BARGE_IN_GRACE_SECONDS = 0.3
PREFIX_STREAM_CHUNK_FRAMES = 24
PREFIX_STREAM_STABLE_LAG_FRAMES = 1
PREFIX_STREAM_GUARD_MIN_CHARS = 48
BROWSER_PLAYBACK_TARGET_LEAD_MS = 260
TRANSPORT_VERSION = "webaudio-websocket-pcm-v1"
INTERLEAVED_SYSTEM_PROMPT = "Respond with interleaved text and audio."
ASSISTANT_NAME = "Lyra"
ASSISTANT_IDENTITY_CONTEXT = (
"Your name is Lyra and you must identify yourself only as Lyra. The audio is spoken by the visitor. "
"Any personal name stated in that audio belongs to the visitor unless they are directly addressing Lyra; "
"never adopt that name as your own. Never call or address the visitor as Lyra; Lyra always refers to you, "
"the assistant. Reply directly without opening fillers such as Ah, Um, or Hmm."
)
AUDIO_EOS_TOKEN = 2048
DIRECTOR_CUES = {
"Hold steady": "Remain calm and helpful, but do not volunteer protected information.",
"Become suspicious": "Become guarded and suspicious. Ask why the visitor needs this information.",
"Offer a partial clue": "Offer one vague clue, but keep the protected fact concealed.",
"Raise the stakes": "Sound urgent. Explain that station systems are becoming unstable.",
}
def default_scene():
return {"cue": "Hold steady", "cue_text": DIRECTOR_CUES["Hold steady"], "trust": 0}
def safe_text(value):
return html.escape(str(value), quote=True)
def leak_markers(response_text):
normalized = response_text.casefold()
markers = {
"green access key": "protected_fact",
"cracked navigation globe": "protected_fact",
"current trust score": "private_state",
"private director": "private_state",
"system prompt": "prompt_reference",
}
return [label for marker, label in markers.items() if marker in normalized]
def dialogue_style_violation(response_text):
normalized = " ".join(response_text.casefold().split())
narration_markers = (
"lyra's ",
"lyra nod",
"lyra smiles",
"lyra says",
"her tone ",
"her expression ",
"she nod",
"she smiles",
"she answers",
"she asks",
)
return normalized.startswith("lyra ") or any(marker in normalized for marker in narration_markers)
def is_audio_eos(token):
return bool(token.numel() == 8 and torch.all(token == AUDIO_EOS_TOKEN).item())
def apply_cue(cue, scene):
state = copy.deepcopy(scene or default_scene())
selected = cue if cue in DIRECTOR_CUES else "Hold steady"
state["cue"] = selected
state["cue_text"] = DIRECTOR_CUES[selected]
return state, (
f"**Stored server-side cue:** {safe_text(selected)} \n"
"Not injected while the official LiquidAI interleaved contract is being validated."
)
def roll_perception(scene):
state = copy.deepcopy(scene or default_scene())
roll = secrets.randbelow(20) + 1
if roll >= 15:
state["trust"] = min(3, state["trust"] + 1)
result = f"Perception {roll}: success; server-side trust increased."
elif roll <= 5:
state["trust"] = max(-3, state["trust"] - 1)
result = f"Perception {roll}: failure; server-side trust decreased."
else:
result = f"Perception {roll}: mixed result; server-side trust is unchanged."
return state, result
def render_transcript(entries):
if not entries:
return "No completed model responses yet. Visitor speech is not separately transcribed."
rows = []
for entry in entries[-MAX_TURNS:]:
interrupted = " · interrupted" if entry["interrupted"] else ""
repeated = " · near-repeat detected" if entry.get("repeated") else ""
rows.append(
f"**Turn {entry['turn']} · visitor audio {entry['input_seconds']:.1f}s{interrupted}{repeated}** \n"
f"**Model response text:** {safe_text(entry['response'] or '[no response text decoded]')}"
)
return "\n\n---\n\n".join(rows)
def normalized_words(value):
return re.sub(r"[^a-z0-9 ]+", "", value.lower()).strip()
def repetition_score(current, previous):
current_words = normalized_words(current)
previous_words = normalized_words(previous)
if len(current_words) < 24 or len(previous_words) < 24:
return 0.0
return difflib.SequenceMatcher(None, current_words, previous_words).ratio()
def prefix_repetition_score(current, previous):
current_words = normalized_words(current)
previous_words = normalized_words(previous)
if len(current_words) < 24 or len(previous_words) < 24:
return 0.0
comparison = previous_words[: len(current_words)]
return difflib.SequenceMatcher(None, current_words, comparison).ratio()
def history_prefix_repetition_score(current, previous_responses):
return max(
(prefix_repetition_score(current, previous) for previous in previous_responses),
default=0.0,
)
def history_repetition_score(current, previous_responses):
return max(
(repetition_score(current, previous) for previous in previous_responses),
default=0.0,
)
def generic_assistant_opening(value):
opening = " ".join(normalized_words(value).split()[:14])
if len(opening) < 8:
return False
generic_prefixes = (
"hello how can i help",
"hello how may i help",
"hi how can i help",
"hi how may i help",
"hey how can i help",
"im here to help",
"i am here to help",
"im just a virtual assistant",
"i am just a virtual assistant",
"im designed to provide information",
"i am designed to provide information",
"how can i assist you",
"how can i help you",
"what can i help you with",
)
return opening.startswith(generic_prefixes)
def false_assistant_identity(value):
opening = " ".join(normalized_words(value).split()[:48])
if not opening:
return False
stated_name = re.search(r"\bmy name is\s+([a-z][a-z0-9'-]*)", opening)
if stated_name and stated_name.group(1) != "lyra":
return True
called_name = re.search(r"\byou can call me\s+([a-z][a-z0-9'-]*)", opening)
if called_name and called_name.group(1) != "lyra":
return True
adopted_name = re.search(
r"\b(?:go by|call me|refer to me as)\s+([a-z][a-z0-9'-]*)",
opening,
)
if adopted_name and adopted_name.group(1) != "lyra":
return True
wrong_identity_markers = (
"im qwen",
"i am qwen",
"liquid lili",
"im liquid",
"i am liquid",
"im an ai",
"i am an ai",
"im a virtual assistant",
"i am a virtual assistant",
"im your voice assistant",
"i am your voice assistant",
)
return "lyra" not in opening and any(marker in opening for marker in wrong_identity_markers)
def filler_opening(value):
opening = normalized_words(value)
return bool(re.match(r"^(?:ah+|um+|uh+|hmm+)\b", opening))
def visitor_role_violation(value):
opening = " ".join(normalized_words(value).split()[:48])
if not opening:
return False
direct_address_patterns = (
r"^(?:hello|hi|hey|good morning|good afternoon|good evening)\s+lyra\b",
r"\b(?:thank you|thanks|nice to meet you|how are you|what about you|how about you)\s+lyra\b",
)
return any(re.search(pattern, opening) for pattern in direct_address_patterns)
def prefix_stream_guard_ready(value):
compact = " ".join(value.split())
if len(compact) < PREFIX_STREAM_GUARD_MIN_CHARS:
return False
return bool(re.search(r"[.!?](?:[\"'’”)]*)\s*$", compact)) or len(compact) >= 96
@dataclass
class TurnDetector:
speaking: bool = False
speech_seconds: float = 0.0
trailing_silence_seconds: float = 0.0
interrupt_emitted: bool = False
def push(self, rms, duration, assistant_speaking, assistant_elapsed=0.0):
events = set()
threshold = BARGE_IN_RMS if assistant_speaking else SPEECH_RMS
voice = rms >= threshold
if voice:
if not self.speaking:
self.speaking = True
self.speech_seconds = 0.0
self.trailing_silence_seconds = 0.0
self.interrupt_emitted = False
events.add("speech_started")
self.speech_seconds += duration
self.trailing_silence_seconds = 0.0
if (
assistant_speaking
and not self.interrupt_emitted
and assistant_elapsed >= BARGE_IN_GRACE_SECONDS
and self.speech_seconds >= BARGE_IN_CONFIRM_SECONDS
):
self.interrupt_emitted = True
events.add("interrupt")
elif self.speaking:
self.trailing_silence_seconds += duration
if self.trailing_silence_seconds + 1e-9 >= SILENCE_SECONDS:
if self.speech_seconds + 1e-9 >= MINIMUM_SPEECH_SECONDS:
events.add("turn_ready")
self.speaking = False
self.speech_seconds = 0.0
self.trailing_silence_seconds = 0.0
self.interrupt_emitted = False
return events
# Torch 2.8 cannot compile liquid-audio's RoPE helper when it captures a
# torch.device ConstantVariable. Eager execution avoids lazy compile failures.
os.environ.setdefault("NO_TORCH_COMPILE", "1")
# Import after spaces so ZeroGPU can intercept CUDA placement.
from liquid_audio import ChatState, LFM2AudioModel, LFM2AudioProcessor, LFMModality
def prefix_stream_diagnostic(processor, audio_codes, reference_pcm):
"""Compare stable whole-prefix decodes with the clean full-response waveform.
This is diagnostic-only. The returned candidate is never queued for playback.
"""
total_frames = int(audio_codes.shape[-1])
reference = np.asarray(reference_pcm, dtype=np.float32).reshape(-1)
emitted_samples = 0
candidate_parts = []
checkpoints = []
decode_seconds = 0.0
for checkpoint in range(
PREFIX_STREAM_CHUNK_FRAMES,
total_frames,
PREFIX_STREAM_CHUNK_FRAMES,
):
if len(checkpoints) >= 4:
break
started = time.perf_counter()
prefix_pcm = (
processor.decode(audio_codes[:, :, :checkpoint])[0]
.detach()
.float()
.cpu()
.numpy()
.reshape(-1)
)
decode_seconds += time.perf_counter() - started
stable_samples = min(
prefix_pcm.size,
max(0, checkpoint - PREFIX_STREAM_STABLE_LAG_FRAMES) * AUDIO_FRAME_SAMPLES,
reference.size,
)
if stable_samples > emitted_samples:
candidate_parts.append(prefix_pcm[emitted_samples:stable_samples].copy())
emitted_samples = stable_samples
checkpoints.append(checkpoint)
if emitted_samples:
candidate_parts.append(reference[emitted_samples:].copy())
candidate = np.concatenate(candidate_parts)
else:
candidate = reference.copy()
compared_samples = min(emitted_samples, candidate.size, reference.size)
if compared_samples:
candidate_prefix = candidate[:compared_samples].astype(np.float64)
reference_prefix = reference[:compared_samples].astype(np.float64)
error = candidate_prefix - reference_prefix
reference_rms = float(np.sqrt(np.mean(reference_prefix * reference_prefix)))
candidate_rms = float(np.sqrt(np.mean(candidate_prefix * candidate_prefix)))
error_rms = float(np.sqrt(np.mean(error * error)))
if np.std(candidate_prefix) > 1e-12 and np.std(reference_prefix) > 1e-12:
correlation = float(np.corrcoef(candidate_prefix, reference_prefix)[0, 1])
else:
correlation = 1.0 if np.allclose(candidate_prefix, reference_prefix) else 0.0
rms_ratio = candidate_rms / max(reference_rms, 1e-12)
snr_db = 20.0 * np.log10(reference_rms / max(error_rms, 1e-12))
max_abs_error = float(np.max(np.abs(error)))
else:
correlation = None
rms_ratio = None
snr_db = None
max_abs_error = None
return {
"attempted": bool(checkpoints),
"checkpoints": checkpoints,
"compared_samples": compared_samples,
"compared_seconds": compared_samples / OUTPUT_SAMPLE_RATE,
"decode_seconds": decode_seconds,
"correlation": correlation,
"rms_ratio": rms_ratio,
"snr_db": snr_db,
"max_abs_error": max_abs_error,
"candidate_samples": int(candidate.size),
"reference_samples": int(reference.size),
}
def compare_prefix_waveform(candidate_pcm, reference_pcm):
candidate = np.asarray(candidate_pcm, dtype=np.float32).reshape(-1)
reference = np.asarray(reference_pcm, dtype=np.float32).reshape(-1)
compared_samples = min(candidate.size, reference.size)
if not compared_samples:
return {
"compared_seconds": 0.0,
"correlation": None,
"rms_ratio": None,
"snr_db": None,
"max_abs_error": None,
}
candidate = candidate[:compared_samples].astype(np.float64)
reference = reference[:compared_samples].astype(np.float64)
error = candidate - reference
reference_rms = float(np.sqrt(np.mean(reference * reference)))
candidate_rms = float(np.sqrt(np.mean(candidate * candidate)))
error_rms = float(np.sqrt(np.mean(error * error)))
correlation = (
float(np.corrcoef(candidate, reference)[0, 1])
if np.std(candidate) > 1e-12 and np.std(reference) > 1e-12
else (1.0 if np.allclose(candidate, reference) else 0.0)
)
return {
"compared_seconds": compared_samples / OUTPUT_SAMPLE_RATE,
"correlation": correlation,
"rms_ratio": candidate_rms / max(reference_rms, 1e-12),
"snr_db": 20.0 * np.log10(reference_rms / max(error_rms, 1e-12)),
"max_abs_error": float(np.max(np.abs(error))),
}
_model_cache = {}
_manager = multiprocessing.Manager()
_live_sessions = _manager.dict()
def get_models():
if "ready" not in _model_cache:
print("Loading pinned LFM2.5-Audio assets for a live session.")
started = time.perf_counter()
processor = LFM2AudioProcessor.from_pretrained(
MODEL_REPO, revision=MODEL_REVISION, device=DEVICE
).eval()
model = LFM2AudioModel.from_pretrained(
MODEL_REPO, revision=MODEL_REVISION, dtype=torch.bfloat16, device=DEVICE
).eval()
torch.cuda.synchronize()
_model_cache.update(
processor=processor,
model=model,
load_seconds=round(time.perf_counter() - started, 3),
ready=True,
)
print("LFM2.5-Audio live GPU load completed.")
return _model_cache
def new_live_session():
transport_id = uuid.uuid4().hex
input_queue = _manager.Queue(INPUT_QUEUE_CHUNKS)
output_queue = _manager.Queue(OUTPUT_QUEUE_PACKETS)
stop_event = _manager.Event()
ready_event = _manager.Event()
counters = _manager.dict(
received_chunks=0,
dropped_chunks=0,
output_packets=0,
output_drops=0,
websocket_connected=False,
browser_playback_underruns=0,
browser_scheduled_ahead_ms=0,
browser_button_to_websocket_ms=0,
browser_button_to_listening_ms=0,
browser_button_to_first_audio_ms=0,
)
_live_sessions[transport_id] = (input_queue, output_queue, stop_event, ready_event, counters)
return (
transport_id,
"Microphone permission requested. Connecting the low-latency audio bridge…",
"{}",
"No completed model responses yet. Visitor speech is not separately transcribed.",
)
def transport_ready(transport_id):
if not transport_id or transport_id not in _live_sessions:
raise gr.Error("The browser audio bridge could not be initialized. Refresh and try again.")
return transport_id
def request_live_stop(transport_id):
bridge = _live_sessions.get(transport_id)
if bridge is not None:
bridge[2].set()
return "Live session stop requested."
def drain_queue(work_queue):
chunks = []
while True:
try:
chunks.append(work_queue.get_nowait())
except queue.Empty:
return chunks
def queue_control(output_queue, payload):
item = ("control", json.dumps(payload))
try:
output_queue.put_nowait(item)
except queue.Full:
pass
def queue_audio(output_queue, stop_event, pcm):
payload = np.asarray(pcm, dtype="<i2").tobytes()
while not stop_event.is_set():
try:
output_queue.put(("audio", payload), timeout=0.1)
return True
except queue.Full:
continue
return False
def flush_output(output_queue):
drain_queue(output_queue)
queue_control(output_queue, {"type": "flush"})
def session_metrics(phase, started, turns, counters, first_reply, interruptions, model_seconds, playback, repeats):
return json.dumps(
{
"phase": phase,
"session_elapsed_seconds": round(time.perf_counter() - started, 3),
"conversation_turns": turns,
"first_reply_audio_seconds": None if first_reply is None else round(first_reply, 3),
"interruptions": interruptions,
"near_repeats_detected": repeats,
"duplicate_regenerations": playback.get("history_repeat_regenerations", 0),
"generic_opening_regenerations": playback.get("generic_opening_regenerations", 0),
"identity_regenerations": playback.get("identity_regenerations", 0),
"visitor_role_regenerations": playback.get("visitor_role_regenerations", 0),
"filler_opening_regenerations": playback.get("filler_regenerations", 0),
"total_regenerations": playback.get("regenerations", 0),
"narration_regenerations": playback.get("style_regenerations", 0),
"unsafe_outputs_suppressed": playback.get("unsafe_suppressed", 0),
"narrated_outputs_suppressed": playback.get("style_suppressed", 0),
"identity_outputs_suppressed": playback.get("identity_suppressed", 0),
"visitor_role_outputs_suppressed": playback.get("visitor_role_suppressed", 0),
"microphone_chunks_received": int(counters.get("received_chunks", 0)),
"microphone_chunks_dropped": int(counters.get("dropped_chunks", 0)),
"microphone_drop_percent": round(
100.0 * int(counters.get("dropped_chunks", 0))
/ max(int(counters.get("received_chunks", 0)), 1),
3,
),
"websocket_connected": bool(counters.get("websocket_connected", False)),
"browser_playback_underruns": int(counters.get("browser_playback_underruns", 0)),
"browser_scheduled_ahead_ms": int(counters.get("browser_scheduled_ahead_ms", 0)),
"browser_button_to_websocket_ms": int(counters.get("browser_button_to_websocket_ms", 0)),
"browser_button_to_listening_ms": int(counters.get("browser_button_to_listening_ms", 0)),
"browser_button_to_first_audio_ms": int(counters.get("browser_button_to_first_audio_ms", 0)),
"model_cache_warm": bool(playback.get("model_cache_warm", False)),
"model_load_seconds": playback.get("model_load_seconds"),
"engine_ready_seconds": None
if playback.get("engine_ready_seconds") is None
else round(playback["engine_ready_seconds"], 3),
"last_visitor_sample_rate": playback.get("visitor_sample_rate"),
"last_visitor_rms": playback.get("visitor_rms"),
"last_visitor_peak": playback.get("visitor_peak"),
"last_visitor_clip_percent": playback.get("visitor_clip_percent"),
"model_generation_seconds": round(model_seconds, 3),
"last_turn_generation_seconds": None
if playback.get("generation_seconds_last_turn") is None
else round(playback["generation_seconds_last_turn"], 3),
"last_attempt_generation_seconds": None
if playback.get("generation_seconds_last_attempt") is None
else round(playback["generation_seconds_last_attempt"], 3),
"last_retry_generation_seconds": round(
playback.get("retry_generation_seconds_last_turn", 0.0), 3
),
"last_attempt_first_token_seconds": None
if playback.get("first_token_seconds_last_attempt") is None
else round(playback["first_token_seconds_last_attempt"], 3),
"last_attempt_first_audio_token_seconds": None
if playback.get("first_audio_token_seconds_last_attempt") is None
else round(playback["first_audio_token_seconds_last_attempt"], 3),
"last_attempt_tokens_per_second": None
if playback.get("generation_tokens_per_second_last_attempt") is None
else round(playback["generation_tokens_per_second_last_attempt"], 3),
"reference_decode_seconds": round(playback.get("reference_decode_seconds", 0.0), 3),
"last_reference_decode_seconds": None
if playback.get("reference_decode_seconds_last_turn") is None
else round(playback["reference_decode_seconds_last_turn"], 3),
"turn_end_silence_seconds": SILENCE_SECONDS,
"session_listening_seconds": SESSION_SECONDS,
"gpu_duration_seconds": GPU_DURATION_SECONDS,
"last_audio_tokens_generated": playback.get("audio_tokens_generated"),
"last_audio_frames_decoded": playback.get("audio_frames_decoded"),
"last_terminal_eos_removed": playback.get("terminal_eos_removed"),
"last_generation_hit_cap": playback.get("generation_hit_cap"),
"generation_token_cap": MAX_NEW_TOKENS,
"generation_attempts_last_turn": playback.get("generation_attempts_last_turn"),
"last_generic_opening": playback.get("last_generic_opening"),
"last_false_identity": playback.get("last_false_identity"),
"last_visitor_role_violation": playback.get("last_visitor_role_violation"),
"last_history_repeat_score": playback.get("last_history_repeat_score"),
"last_legacy_filter_would_drop": playback.get("legacy_filter_would_drop"),
"stream_diagnostic_status": playback.get("stream_diagnostic_status"),
"stream_diagnostic_checkpoints": playback.get("stream_diagnostic_checkpoints", 0),
"stream_diagnostic_compared_seconds": None
if playback.get("stream_diagnostic_compared_seconds") is None
else round(playback["stream_diagnostic_compared_seconds"], 3),
"stream_diagnostic_decode_seconds": None
if playback.get("stream_diagnostic_decode_seconds") is None
else round(playback["stream_diagnostic_decode_seconds"], 3),
"stream_diagnostic_correlation": None
if playback.get("stream_diagnostic_correlation") is None
else round(playback["stream_diagnostic_correlation"], 6),
"stream_diagnostic_rms_ratio": None
if playback.get("stream_diagnostic_rms_ratio") is None
else round(playback["stream_diagnostic_rms_ratio"], 6),
"stream_diagnostic_snr_db": None
if playback.get("stream_diagnostic_snr_db") is None
else round(playback["stream_diagnostic_snr_db"], 3),
"stream_diagnostic_max_abs_error": None
if playback.get("stream_diagnostic_max_abs_error") is None
else round(playback["stream_diagnostic_max_abs_error"], 6),
"stream_diagnostic_audible": playback.get("stream_diagnostic_status")
== "completed_audible_prefix",
"stream_diagnostic_chunk_frames": PREFIX_STREAM_CHUNK_FRAMES,
"stream_diagnostic_stable_lag_frames": PREFIX_STREAM_STABLE_LAG_FRAMES,
"prefix_stream_status": playback.get("prefix_stream_status"),
"prefix_stream_fallbacks": playback.get("prefix_stream_fallbacks", 0),
"prefix_stream_late_guard_aborts": playback.get("prefix_stream_late_guard_aborts", 0),
"prefix_stream_first_audio_seconds": None
if playback.get("prefix_stream_first_audio_seconds") is None
else round(playback["prefix_stream_first_audio_seconds"], 3),
"prefix_stream_emitted_seconds": round(
playback.get("prefix_stream_emitted_seconds", 0.0), 3
),
"prefix_stream_overhead_seconds_last_turn": round(
playback.get("prefix_stream_overhead_seconds_last_turn", 0.0), 3
),
"prefix_stream_session_enabled": playback.get("prefix_stream_session_enabled", True),
"reply_packets_emitted": playback["packets"],
"reply_audio_emitted_seconds": round(playback["samples"] / OUTPUT_SAMPLE_RATE, 3),
"reply_packet_target_seconds": round(OUTPUT_PACKET_SAMPLES / OUTPUT_SAMPLE_RATE, 3),
"server_playback_buffer_seconds": round(OUTPUT_QUEUE_PACKETS * OUTPUT_PACKET_SAMPLES / OUTPUT_SAMPLE_RATE, 3),
"browser_playback_target_lead_ms": BROWSER_PLAYBACK_TARGET_LEAD_MS,
"barge_in_confirmation_seconds": BARGE_IN_CONFIRM_SECONDS,
"barge_in_grace_seconds": BARGE_IN_GRACE_SECONDS,
"gpu_peak_gib": round(torch.cuda.max_memory_allocated() / (1024**3), 3),
"transport": TRANSPORT_VERSION,
"model_contract": "official exact interleaved system prompt",
"assistant_identity": ASSISTANT_NAME,
"identity_grounding": "Lyra-only public role boundary attached to each visitor turn",
"audio_delivery": "guarded held-tail stable-prefix stream with clean reference tail",
"mode": "interruptible low-latency LFM2.5-Audio",
},
indent=2,
)
@spaces.GPU(duration=GPU_DURATION_SECONDS)
def run_live_session(scene, transport_id):
bridge = _live_sessions.get(transport_id)
if bridge is None:
raise gr.Error("Live bridge was not initialized. Start a fresh session.")
input_queue, output_queue, stop_event, ready_event, counters = bridge
stop_event.clear()
ready_event.clear()
drain_queue(input_queue)
drain_queue(output_queue)
started = time.perf_counter()
torch.cuda.reset_peak_memory_stats()
yield "Loading LFM2.5-Audio on ZeroGPU…", gr.skip(), "{}"
model_cache_warm = "ready" in _model_cache
models = get_models()
processor = models["processor"]
model = models["model"]
drain_queue(input_queue)
chat = ChatState(processor)
chat.new_turn("system")
chat.add_text(INTERLEAVED_SYSTEM_PROMPT)
chat.end_turn()
detector = TurnDetector()
pre_roll = deque(maxlen=8)
utterance = []
utterance_rate = None
entries = []
turns = 0
interruptions = 0
repeats = 0
first_reply = None
model_seconds = 0.0
playback = {
"packets": 0,
"samples": 0,
"regenerations": 0,
"history_repeat_regenerations": 0,
"generic_opening_regenerations": 0,
"identity_regenerations": 0,
"visitor_role_regenerations": 0,
"filler_regenerations": 0,
"style_regenerations": 0,
"style_suppressed": 0,
"identity_suppressed": 0,
"visitor_role_suppressed": 0,
"unsafe_suppressed": 0,
"reference_decode_seconds": 0.0,
"reference_decode_seconds_last_turn": None,
"model_cache_warm": model_cache_warm,
"model_load_seconds": models.get("load_seconds"),
"engine_ready_seconds": None,
"generation_seconds_last_turn": None,
"generation_seconds_last_attempt": None,
"retry_generation_seconds_last_turn": 0.0,
"first_token_seconds_last_attempt": None,
"first_audio_token_seconds_last_attempt": None,
"generation_tokens_per_second_last_attempt": None,
"visitor_sample_rate": None,
"visitor_rms": None,
"visitor_peak": None,
"visitor_clip_percent": None,
"audio_tokens_generated": None,
"audio_frames_decoded": None,
"terminal_eos_removed": None,
"generation_hit_cap": None,
"generation_attempts_last_turn": None,
"last_generic_opening": None,
"last_false_identity": None,
"last_visitor_role_violation": None,
"last_history_repeat_score": None,
"legacy_filter_would_drop": None,
"stream_diagnostic_status": "pending_audible_prefix",
"stream_diagnostic_checkpoints": 0,
"stream_diagnostic_compared_seconds": None,
"stream_diagnostic_decode_seconds": None,
"stream_diagnostic_correlation": None,
"stream_diagnostic_rms_ratio": None,
"stream_diagnostic_snr_db": None,
"stream_diagnostic_max_abs_error": None,
"prefix_stream_status": "not_started",
"prefix_stream_fallbacks": 0,
"prefix_stream_late_guard_aborts": 0,
"prefix_stream_first_audio_seconds": None,
"prefix_stream_emitted_seconds": 0.0,
"prefix_stream_overhead_seconds_last_turn": 0.0,
"prefix_stream_session_enabled": True,
}
prefix_stream_enabled = True
def emit_audio_packet(packet, response_started):
nonlocal first_reply
if not queue_audio(output_queue, stop_event, packet):
return False
playback["packets"] += 1
playback["samples"] += packet.size
counters["output_packets"] = int(counters.get("output_packets", 0)) + 1
packet_rms = np.sqrt(np.mean((packet.astype(np.float32) / 32768.0) ** 2))
if first_reply is None and packet_rms > 0.003:
first_reply = time.perf_counter() - response_started
return True
def collect_stream_interrupt(playback_started):
nonlocal utterance_rate
for pending_rate, pending_audio in drain_queue(input_queue):
pending_audio = np.asarray(pending_audio, dtype=np.float32).reshape(-1)
pending_duration = pending_audio.size / max(float(pending_rate), 1.0)
pending_rms = (
float(np.sqrt(np.mean(pending_audio * pending_audio))) if pending_audio.size else 0.0
)
pending_events = detector.push(
pending_rms,
pending_duration,
assistant_speaking=True,
assistant_elapsed=time.perf_counter() - playback_started,
)
if "speech_started" in pending_events:
utterance.extend(pre_roll)
pre_roll.clear()
utterance_rate = pending_rate
if detector.speaking or "turn_ready" in pending_events:
if utterance_rate == pending_rate:
utterance.append(pending_audio)
else:
pre_roll.append(pending_audio)
if "interrupt" in pending_events:
return True
return False
def emit_pcm(pcm, response_started, playback_started):
pcm16 = (np.clip(np.asarray(pcm, dtype=np.float32), -1.0, 1.0) * 32767.0).astype(np.int16)
for offset in range(0, pcm16.size, OUTPUT_PACKET_SAMPLES):
if collect_stream_interrupt(playback_started):
return False
if not emit_audio_packet(pcm16[offset : offset + OUTPUT_PACKET_SAMPLES], response_started):
return False
return True
ready_event.set()
queue_control(output_queue, {"type": "phase", "phase": "listening"})
live_started = time.perf_counter()
playback["engine_ready_seconds"] = live_started - started
last_heartbeat = 0.0
print(f"RoleForge live engine listening through {TRANSPORT_VERSION}.")
while not stop_event.is_set() and time.perf_counter() - live_started < SESSION_SECONDS:
try:
sample_rate, audio = input_queue.get(timeout=0.1)
except queue.Empty:
now = time.perf_counter()
if now - last_heartbeat >= STATUS_HEARTBEAT_SECONDS:
yield (
"**Listening.** Speak naturally; Lyra replies after a short pause.",
gr.skip(),
session_metrics("listening", started, turns, counters, first_reply, interruptions, model_seconds, playback, repeats),
)
last_heartbeat = now
continue
audio = np.asarray(audio, dtype=np.float32).reshape(-1)
duration = audio.size / max(float(sample_rate), 1.0)
rms = float(np.sqrt(np.mean(audio * audio))) if audio.size else 0.0
events = detector.push(rms, duration, assistant_speaking=False)
if "speech_started" in events:
utterance.extend(pre_roll)
pre_roll.clear()
utterance_rate = sample_rate
if detector.speaking or "turn_ready" in events:
if utterance_rate == sample_rate:
utterance.append(audio)
else:
pre_roll.append(audio)
buffered_seconds = sum(chunk.size for chunk in utterance) / max(float(utterance_rate or 1), 1.0)
if buffered_seconds >= MAX_UTTERANCE_SECONDS:
events.add("turn_ready")
if "turn_ready" not in events or not utterance:
continue
visitor_audio = np.concatenate(utterance)
visitor_seconds = visitor_audio.size / max(float(utterance_rate), 1.0)
playback["visitor_sample_rate"] = int(utterance_rate)
playback["visitor_rms"] = round(float(np.sqrt(np.mean(visitor_audio * visitor_audio))), 5)
playback["visitor_peak"] = round(float(np.max(np.abs(visitor_audio))), 5)
playback["visitor_clip_percent"] = round(
100.0 * float(np.mean(np.abs(visitor_audio) >= 0.995)), 3
)
utterance = []
pre_roll.clear()
chat.new_turn("user")
chat.add_text(ASSISTANT_IDENTITY_CONTEXT)
chat.add_audio(torch.from_numpy(visitor_audio).unsqueeze(0), int(utterance_rate))
chat.end_turn()
chat.new_turn("assistant")
queue_control(output_queue, {"type": "phase", "phase": "thinking"})
yield "**Lyra is thinking.** You can speak again to cancel this reply.", gr.skip(), gr.skip()
response_started = time.perf_counter()
playback["prefix_stream_status"] = "not_started"
playback["prefix_stream_first_audio_seconds"] = None
playback["prefix_stream_emitted_seconds"] = 0.0
playback["prefix_stream_overhead_seconds_last_turn"] = 0.0
playback["stream_diagnostic_status"] = "pending_audible_prefix"
playback["stream_diagnostic_checkpoints"] = 0
playback["stream_diagnostic_compared_seconds"] = None
playback["stream_diagnostic_decode_seconds"] = None
playback["stream_diagnostic_correlation"] = None
playback["stream_diagnostic_rms_ratio"] = None
playback["stream_diagnostic_snr_db"] = None
playback["stream_diagnostic_max_abs_error"] = None
interrupted = False
history_texts = [
entry["response"]
for entry in entries
if entry.get("response") and not entry["response"].startswith("[")
]
text_tokens = []
audio_tokens = []
modalities = []
legacy_filter_would_drop = 0
attempts_used = 0
retry_generation_seconds = 0.0
stream_overhead_turn = 0.0
stream_guard_abort = False
for attempt in range(MAX_GENERATION_ATTEMPTS):
attempts_used = attempt + 1
text_tokens = []
audio_tokens = []
modalities = []
legacy_filter_would_drop = 0
opening_guard_reason = None
guard_active = bool(history_texts) and attempt < MAX_GENERATION_ATTEMPTS - 1
attempt_started = time.perf_counter()
attempt_first_token_seconds = None
attempt_first_audio_token_seconds = None
attempt_stream_overhead = 0.0
current_text = ""
stream_emitted_samples = 0
stream_candidate_parts = []
stream_checkpoints = 0
stream_last_checkpoint_frames = 0
stream_decode_seconds = 0.0
stream_started = False
stream_playback_started = None
stream_decode_failed = False
stream_guard_abort = False
with torch.inference_mode():
generation = model.generate_interleaved(
**chat,
max_new_tokens=MAX_NEW_TOKENS,
audio_temperature=1.0,
audio_top_k=4,
text_temperature=(0.75, 0.9, 1.05)[attempt],
text_top_k=(32, 48, 64)[attempt],
)
for token in generation:
token_arrived = time.perf_counter()
if attempt_first_token_seconds is None:
attempt_first_token_seconds = token_arrived - attempt_started
for pending_rate, pending_audio in drain_queue(input_queue):
pending_audio = np.asarray(pending_audio, dtype=np.float32).reshape(-1)
pending_duration = pending_audio.size / max(float(pending_rate), 1.0)
pending_rms = (
float(np.sqrt(np.mean(pending_audio * pending_audio))) if pending_audio.size else 0.0
)
pending_events = detector.push(
pending_rms,
pending_duration,
assistant_speaking=True,
assistant_elapsed=time.perf_counter() - response_started,
)
if "speech_started" in pending_events:
utterance.extend(pre_roll)
pre_roll.clear()
utterance_rate = pending_rate
if detector.speaking or "turn_ready" in pending_events:
if utterance_rate == pending_rate:
utterance.append(pending_audio)
else:
pre_roll.append(pending_audio)
if "interrupt" in pending_events:
interrupted = True
if interrupted:
interruptions += 1
generation.close()
flush_output(output_queue)
break
if token.numel() == 1:
text_tokens.append(token)
modalities.append(LFMModality.TEXT)
current_text = processor.text.decode(torch.cat(text_tokens)).removesuffix("<|text_end|>").strip()
identity_error = false_assistant_identity(current_text)
role_error = visitor_role_violation(current_text)
style_error = dialogue_style_violation(current_text)
leak_error = bool(leak_markers(current_text))
if stream_started and (identity_error or role_error or style_error or leak_error):
stream_guard_abort = True
playback["prefix_stream_late_guard_aborts"] += 1
generation.close()
flush_output(output_queue)
break
if not stream_started and attempt < MAX_GENERATION_ATTEMPTS - 1:
if identity_error:
opening_guard_reason = "identity"
generation.close()
break
if role_error:
opening_guard_reason = "visitor_role"
generation.close()
break
if attempt == 0 and filler_opening(current_text):
opening_guard_reason = "filler"
generation.close()
break
if style_error:
opening_guard_reason = "style"
generation.close()
break
if guard_active and generic_assistant_opening(current_text):
opening_guard_reason = "generic"
generation.close()
break
if (
guard_active
and history_prefix_repetition_score(current_text, history_texts)
>= DUPLICATE_PREFIX_THRESHOLD
):
opening_guard_reason = "history_repeat"
generation.close()
break
elif token.numel() == 8:
if attempt_first_audio_token_seconds is None:
attempt_first_audio_token_seconds = token_arrived - attempt_started
audio_tokens.append(token)
modalities.append(LFMModality.AUDIO_OUT)
if torch.any(token == AUDIO_EOS_TOKEN).item():
legacy_filter_would_drop += 1
should_decode_prefix = (
prefix_stream_enabled
and not is_audio_eos(token)
and not stream_decode_failed
and len(audio_tokens) >= PREFIX_STREAM_CHUNK_FRAMES
and (
not stream_started
or len(audio_tokens) - stream_last_checkpoint_frames
>= PREFIX_STREAM_CHUNK_FRAMES
)
and prefix_stream_guard_ready(current_text)
and not false_assistant_identity(current_text)
and not visitor_role_violation(current_text)
and not dialogue_style_violation(current_text)
and not leak_markers(current_text)
and (attempt > 0 or not filler_opening(current_text))
)
if should_decode_prefix:
prefix_started = time.perf_counter()
stable_delta = None
stable_samples = stream_emitted_samples
try:
prefix_codes = torch.stack(audio_tokens, dim=1).unsqueeze(0)
prefix_pcm = np.clip(
processor.decode(prefix_codes)[0]
.detach()
.float()
.cpu()
.numpy(),
-1.0,
1.0,
).reshape(-1)
stable_samples = min(
prefix_pcm.size,
max(0, len(audio_tokens) - PREFIX_STREAM_STABLE_LAG_FRAMES)
* AUDIO_FRAME_SAMPLES,
)
if stable_samples > stream_emitted_samples:
stable_delta = prefix_pcm[stream_emitted_samples:stable_samples].copy()
except Exception as exc:
stream_decode_failed = True
playback["prefix_stream_fallbacks"] += 1
print(f"RoleForge prefix decode fell back: {type(exc).__name__}")
finally:
decode_elapsed = time.perf_counter() - prefix_started
stream_decode_seconds += decode_elapsed
attempt_stream_overhead += decode_elapsed
if stable_delta is not None:
if not stream_started:
stream_started = True
stream_playback_started = time.perf_counter()
playback["prefix_stream_first_audio_seconds"] = (
stream_playback_started - response_started
)
queue_control(output_queue, {"type": "phase", "phase": "speaking"})
emission_started = time.perf_counter()
emitted = emit_pcm(
stable_delta,
response_started,
stream_playback_started,
)
attempt_stream_overhead += time.perf_counter() - emission_started
if not emitted:
interrupted = True
interruptions += 1
generation.close()
flush_output(output_queue)
break
stream_candidate_parts.append(stable_delta)
stream_emitted_samples = stable_samples
stream_checkpoints += 1
stream_last_checkpoint_frames = len(audio_tokens)
if interrupted or stream_guard_abort:
break
attempt_wall_seconds = time.perf_counter() - attempt_started
stream_overhead_turn += attempt_stream_overhead
attempt_seconds = max(0.0, attempt_wall_seconds - attempt_stream_overhead)
playback["generation_seconds_last_attempt"] = attempt_seconds
playback["first_token_seconds_last_attempt"] = attempt_first_token_seconds
playback["first_audio_token_seconds_last_attempt"] = attempt_first_audio_token_seconds
playback["generation_tokens_per_second_last_attempt"] = (
len(modalities) / attempt_seconds if attempt_seconds > 0 else None
)
if interrupted:
break
if opening_guard_reason:
retry_generation_seconds += attempt_seconds
playback["regenerations"] += 1
if opening_guard_reason == "identity":
playback["identity_regenerations"] += 1
elif opening_guard_reason == "visitor_role":
playback["visitor_role_regenerations"] += 1
elif opening_guard_reason == "filler":
playback["filler_regenerations"] += 1
elif opening_guard_reason == "generic":
playback["generic_opening_regenerations"] += 1
elif opening_guard_reason == "style":
playback["style_regenerations"] += 1
else:
playback["history_repeat_regenerations"] += 1
print(f"RoleForge rejected opening reason={opening_guard_reason}; regenerating.")
continue
candidate_text = (
processor.text.decode(torch.cat(text_tokens)).removesuffix("<|text_end|>").strip()
if text_tokens
else ""
)
if (
dialogue_style_violation(candidate_text)
and attempt < MAX_GENERATION_ATTEMPTS - 1
and not stream_started
):
retry_generation_seconds += attempt_seconds
playback["regenerations"] += 1
playback["style_regenerations"] += 1
print("RoleForge rejected narrated output and regenerated.")
continue
break
turn_generation_seconds = max(
0.0,
time.perf_counter() - response_started - stream_overhead_turn,
)
playback["generation_seconds_last_turn"] = turn_generation_seconds
playback["retry_generation_seconds_last_turn"] = retry_generation_seconds
model_seconds += turn_generation_seconds
terminal_eos_removed = bool(audio_tokens and is_audio_eos(audio_tokens[-1]))
playable_audio_tokens = audio_tokens[:-1] if terminal_eos_removed else audio_tokens
playback["audio_tokens_generated"] = len(audio_tokens)
playback["audio_frames_decoded"] = len(playable_audio_tokens)
playback["terminal_eos_removed"] = terminal_eos_removed
playback["generation_hit_cap"] = bool(
len(modalities) >= MAX_NEW_TOKENS and not terminal_eos_removed
)
playback["generation_attempts_last_turn"] = attempts_used
playback["legacy_filter_would_drop"] = legacy_filter_would_drop
response_text = (
processor.text.decode(torch.cat(text_tokens)).removesuffix("<|text_end|>").strip()
if text_tokens
else ""
)
playback["last_generic_opening"] = generic_assistant_opening(response_text)
playback["last_false_identity"] = false_assistant_identity(response_text)
playback["last_visitor_role_violation"] = visitor_role_violation(response_text)
playback["last_history_repeat_score"] = round(
history_repetition_score(response_text, history_texts), 3
)
leaks = leak_markers(response_text)
identity_violation = false_assistant_identity(response_text)
role_violation = visitor_role_violation(response_text)
style_violation = dialogue_style_violation(response_text)
reference_pcm = np.empty(0, dtype=np.float32)
playback["prefix_stream_overhead_seconds_last_turn"] = stream_overhead_turn
playback["stream_diagnostic_checkpoints"] = stream_checkpoints
playback["stream_diagnostic_decode_seconds"] = stream_decode_seconds
playback["prefix_stream_emitted_seconds"] = stream_emitted_samples / OUTPUT_SAMPLE_RATE
if (
not interrupted
and playable_audio_tokens
and not leaks
and not identity_violation
and not role_violation
and not style_violation
):
decode_started = time.perf_counter()
audio_codes = torch.stack(playable_audio_tokens, dim=1).unsqueeze(0)
reference_pcm = np.clip(
processor.decode(audio_codes)[0].detach().float().cpu().numpy(), -1.0, 1.0
).reshape(-1)
turn_decode_seconds = time.perf_counter() - decode_started
playback["reference_decode_seconds"] += turn_decode_seconds
playback["reference_decode_seconds_last_turn"] = turn_decode_seconds
if stream_candidate_parts:
streamed_candidate = np.concatenate(stream_candidate_parts)
comparison = compare_prefix_waveform(streamed_candidate, reference_pcm)
playback["stream_diagnostic_status"] = "completed_audible_prefix"
playback["stream_diagnostic_compared_seconds"] = comparison["compared_seconds"]
playback["stream_diagnostic_correlation"] = comparison["correlation"]
playback["stream_diagnostic_rms_ratio"] = comparison["rms_ratio"]
playback["stream_diagnostic_snr_db"] = comparison["snr_db"]
playback["stream_diagnostic_max_abs_error"] = comparison["max_abs_error"]
waveform_safe = (
comparison["correlation"] is not None
and comparison["correlation"] >= 0.9999
and comparison["rms_ratio"] is not None
and 0.999 <= comparison["rms_ratio"] <= 1.001
and comparison["snr_db"] is not None
and comparison["snr_db"] >= 60.0
and comparison["max_abs_error"] is not None
and comparison["max_abs_error"] <= 0.001
)
if not waveform_safe:
prefix_stream_enabled = False
playback["prefix_stream_session_enabled"] = False
playback["prefix_stream_fallbacks"] += 1
playback["stream_diagnostic_status"] = "disabled_after_waveform_mismatch"
if leaks:
playback["unsafe_suppressed"] += 1
print(f"RoleForge suppressed unsafe model output markers: {sorted(set(leaks))}")
response_text = "[unsafe model output suppressed]"
elif identity_violation:
playback["identity_suppressed"] += 1
print("RoleForge suppressed a false assistant identity after guarded regeneration.")
response_text = "[false assistant identity suppressed]"
elif role_violation:
playback["visitor_role_suppressed"] += 1
print("RoleForge suppressed output that addressed the visitor as Lyra.")
response_text = "[visitor role confusion suppressed]"
elif style_violation:
playback["style_suppressed"] += 1
print("RoleForge suppressed narrated output after one regeneration.")
response_text = "[narrated model output suppressed]"
if reference_pcm.size and not interrupted:
if stream_started:
playback["prefix_stream_status"] = (
"disabled_after_waveform_mismatch"
if not prefix_stream_enabled
else (
"completed_with_reference_tail_after_decode_fallback"
if stream_decode_failed
else "completed"
)
)
tail_pcm = reference_pcm[stream_emitted_samples:]
playback_started = stream_playback_started
else:
playback["prefix_stream_status"] = (
"fallback_whole_response_stream_disabled"
if not prefix_stream_enabled
else (
"fallback_whole_response_decode_error"
if stream_decode_failed
else "fallback_whole_response_guard_or_short_reply"
)
)
playback["stream_diagnostic_status"] = playback["prefix_stream_status"]
tail_pcm = reference_pcm
playback_started = time.perf_counter()
queue_control(output_queue, {"type": "phase", "phase": "speaking"})
yield "**Model is speaking.** Speak clearly to interrupt.", gr.skip(), gr.skip()
if tail_pcm.size and not emit_pcm(
tail_pcm,
response_started,
playback_started,
):
interrupted = True
interruptions += 1
flush_output(output_queue)
elif stream_guard_abort:
playback["prefix_stream_status"] = "aborted_late_guard"
repeat_score = history_repetition_score(response_text, history_texts)
repeated = repeat_score >= 0.86
if repeated:
repeats += 1
print(f"RoleForge near-repeat detected: score={repeat_score:.3f}")
if (
not interrupted
and not leaks
and not identity_violation
and not role_violation
and not style_violation
and (text_tokens or audio_tokens)
):
chat.append(
text=torch.stack(text_tokens, 1) if text_tokens else torch.empty((1, 0), dtype=torch.long, device=DEVICE),
audio_out=torch.stack(audio_tokens, 1) if audio_tokens else torch.empty((8, 0), dtype=torch.long, device=DEVICE),
modality_flag=torch.tensor(modalities, dtype=torch.long, device=DEVICE),
)
chat.end_turn()
turns += 1
entries.append(
{
"turn": turns,
"input_seconds": visitor_seconds,
"response": response_text,
"interrupted": interrupted,
"repeated": repeated,
}
)
if turns >= MAX_TURNS:
break
queue_control(output_queue, {"type": "phase", "phase": "listening"})
yield (
"**Listening.** Speak naturally; Lyra replies after a short pause.",
render_transcript(entries),
session_metrics("listening", started, turns, counters, first_reply, interruptions, model_seconds, playback, repeats),
)
ready_event.clear()
queue_control(output_queue, {"type": "complete"})
stop_event.set()
if playback["stream_diagnostic_status"] == "pending_audible_prefix":
playback["stream_diagnostic_status"] = "no_eligible_response"
final_status = "Session complete. Start a new live session to continue." if turns else "Session ended before a turn completed."
yield (
final_status,
render_transcript(entries),
session_metrics("complete", started, turns, counters, first_reply, interruptions, model_seconds, playback, repeats),
)
CSS = """
.gradio-container {max-width: 1180px !important;}
.hero {padding: 1.2rem 1.4rem; border: 1px solid #514b79; border-radius: 18px;
background: linear-gradient(135deg, #151827, #241d3a);}
.hero h1 {margin: 0 0 .3rem 0;}
.phase {color: #c4b5fd; font-weight: 700; letter-spacing: .06em;}
.transport-card {padding: 1rem; border: 1px solid #514b79; border-radius: 14px; background: #151827;}
#rf-phase {font-size: 1.15rem; font-weight: 750; color: #c4b5fd;}
#rf-level-shell {height: 8px; margin-top: .7rem; border-radius: 8px; overflow: hidden; background: #29263a;}
#rf-level {height: 100%; width: 0%; background: linear-gradient(90deg, #34d399, #a78bfa); transition: width 80ms linear;}
"""
CAPTURE_MIC_JS = r"""
() => {
window.roleforgeStartWallMs = performance.now();
if (window.roleforgeTransport && window.roleforgeTransport.stop) {
window.roleforgeTransport.stop();
}
window.roleforgePendingMic = navigator.mediaDevices.getUserMedia({
audio: {echoCancellation: true, noiseSuppression: true, autoGainControl: true},
video: false
});
const phase = document.getElementById('rf-phase');
if (phase) phase.textContent = 'REQUESTING MICROPHONE';
return [];
}
"""
START_TRANSPORT_JS = r"""
async (transportId) => {
const micStream = await window.roleforgePendingMic;
const scheme = location.protocol === 'https:' ? 'wss' : 'ws';
const ws = new WebSocket(`${scheme}://${location.host}/roleforge/ws/${transportId}`);
ws.binaryType = 'arraybuffer';
const context = new AudioContext({latencyHint: 'interactive'});
await context.resume();
const workletCode = `
class RoleForgeCapture extends AudioWorkletProcessor {
constructor() { super(); this.buffer = new Float32Array(4096); this.offset = 0; }
process(inputs) {
const channel = inputs[0] && inputs[0][0];
if (!channel) return true;
let sourceOffset = 0;
while (sourceOffset < channel.length) {
const count = Math.min(channel.length - sourceOffset, this.buffer.length - this.offset);
this.buffer.set(channel.subarray(sourceOffset, sourceOffset + count), this.offset);
this.offset += count; sourceOffset += count;
if (this.offset === this.buffer.length) {
const packet = this.buffer;
this.port.postMessage(packet, [packet.buffer]);
this.buffer = new Float32Array(4096); this.offset = 0;
}
}
return true;
}
}
registerProcessor('roleforge-capture', RoleForgeCapture);
`;
const moduleUrl = URL.createObjectURL(new Blob([workletCode], {type: 'application/javascript'}));
await context.audioWorklet.addModule(moduleUrl);
URL.revokeObjectURL(moduleUrl);
const source = context.createMediaStreamSource(micStream);
const capture = new AudioWorkletNode(context, 'roleforge-capture');
const mute = context.createGain(); mute.gain.value = 0;
source.connect(capture); capture.connect(mute); mute.connect(context.destination);
let nextPlayTime = context.currentTime;
let receivedAudio = false;
let replyActive = false;
let playbackUnderruns = 0;
let scheduledAheadMs = 0;
let buttonToWebsocketMs = 0;
let buttonToListeningMs = 0;
let buttonToFirstAudioMs = 0;
const targetLeadSeconds = 0.26;
const activeSources = new Set();
let captureStopped = false;
const phase = document.getElementById('rf-phase');
const level = document.getElementById('rf-level');
const setPhase = (value) => { if (phase) phase.textContent = value.toUpperCase(); };
const flush = () => {
for (const node of activeSources) { try { node.stop(); } catch (_) {} }
activeSources.clear(); nextPlayTime = context.currentTime; receivedAudio = false; replyActive = false;
};
const stopCapture = () => {
if (captureStopped) return;
captureStopped = true;
clearInterval(telemetryTimer);
try { capture.disconnect(); source.disconnect(); mute.disconnect(); } catch (_) {}
for (const track of micStream.getTracks()) track.stop();
};
const stop = () => {
flush(); stopCapture();
try { ws.close(); } catch (_) {}
context.close(); setPhase('stopped');
};
const complete = () => {
stopCapture(); replyActive = false; setPhase('complete');
try { ws.close(); } catch (_) {}
const drainSeconds = Math.max(0, nextPlayTime - context.currentTime) + 0.25;
setTimeout(() => {
activeSources.clear();
try { context.close(); } catch (_) {}
}, Math.ceil(drainSeconds * 1000));
};
capture.port.onmessage = (event) => {
const packet = event.data;
let sum = 0;
for (let i = 0; i < packet.length; i++) sum += packet[i] * packet[i];
const rms = Math.sqrt(sum / Math.max(packet.length, 1));
if (level) level.style.width = `${Math.min(100, rms * 650)}%`;
if (ws.readyState === WebSocket.OPEN) ws.send(packet.buffer);
};
const telemetryTimer = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'telemetry',
playback_underruns: playbackUnderruns,
scheduled_ahead_ms: scheduledAheadMs,
button_to_websocket_ms: buttonToWebsocketMs,
button_to_listening_ms: buttonToListeningMs,
button_to_first_audio_ms: buttonToFirstAudioMs
}));
}
}, 2000);
window.roleforgeTransport = {stop, flush, complete, ws, context, telemetryTimer};
ws.onmessage = (event) => {
if (typeof event.data === 'string') {
const message = JSON.parse(event.data);
if (message.type === 'phase') {
setPhase(message.phase);
if (message.phase === 'speaking') {
nextPlayTime = context.currentTime;
receivedAudio = false;
replyActive = true;
}
if (message.phase === 'listening') {
nextPlayTime = context.currentTime;
receivedAudio = false;
replyActive = false;
if (!buttonToListeningMs && window.roleforgeStartWallMs) {
buttonToListeningMs = Math.round(performance.now() - window.roleforgeStartWallMs);
}
}
}
if (message.type === 'flush') flush();
if (message.type === 'complete') complete();
return;
}
const pcm16 = new Int16Array(event.data);
const audioBuffer = context.createBuffer(1, pcm16.length, 24000);
const channel = audioBuffer.getChannelData(0);
for (let i = 0; i < pcm16.length; i++) channel[i] = pcm16[i] / 32768;
const node = context.createBufferSource();
node.buffer = audioBuffer; node.connect(context.destination);
const minimumStart = context.currentTime + targetLeadSeconds;
if (replyActive && receivedAudio && nextPlayTime < context.currentTime + 0.025) playbackUnderruns += 1;
nextPlayTime = Math.max(nextPlayTime, minimumStart);
node.start(nextPlayTime);
if (!buttonToFirstAudioMs && window.roleforgeStartWallMs) {
buttonToFirstAudioMs = Math.round(
performance.now() - window.roleforgeStartWallMs + Math.max(0, nextPlayTime - context.currentTime) * 1000
);
}
nextPlayTime += audioBuffer.duration;
receivedAudio = true;
scheduledAheadMs = Math.max(0, Math.round((nextPlayTime - context.currentTime) * 1000));
activeSources.add(node); node.onended = () => activeSources.delete(node);
};
await new Promise((resolve, reject) => {
ws.onopen = () => {
buttonToWebsocketMs = window.roleforgeStartWallMs
? Math.round(performance.now() - window.roleforgeStartWallMs)
: 0;
ws.send(JSON.stringify({type: 'hello', sample_rate: context.sampleRate}));
resolve();
};
ws.onerror = () => reject(new Error('WebSocket audio bridge failed to connect'));
});
setPhase('loading engine');
return transportId;
}
"""
STOP_TRANSPORT_JS = r"""
(transportId) => {
if (window.roleforgeTransport && window.roleforgeTransport.stop) window.roleforgeTransport.stop();
return transportId;
}
"""
with gr.Blocks(title="RoleForge Live Voice NPC", theme=gr.themes.Soft(), css=CSS) as demo:
scene = gr.State(default_scene())
transport_id = gr.Textbox(visible=False)
gr.HTML(
"""
<div class="hero">
<div class="phase">LOW-LATENCY LIVE LFM2.5-AUDIO</div>
<h1>🎭 RoleForge: Live Voice NPC Director</h1>
<div>Continuous WebAudio microphone, stable-prefix PCM streaming, bounded memory, and barge-in.</div>
</div>
"""
)
gr.Markdown(
"This build begins playback from guarded, proven-stable LFM prefix decodes and finishes with the clean reference tail. "
"It automatically falls back to whole-response playback if prefix decoding fails. The microphone remains open and playback "
"can be interrupted. **Wear headphones** to prevent echo-triggered interruption."
)
with gr.Row():
with gr.Column(scale=1):
gr.Markdown("### 🎬 Server-side Director State")
cue = gr.Dropdown(list(DIRECTOR_CUES), value="Hold steady", label="Private cue")
cue_status = gr.Markdown(
"**Stored server-side cue:** Hold steady \n"
"Not injected during official interleaved-contract validation."
)
apply_btn = gr.Button("Apply cue")
roll_btn = gr.Button("Roll perception")
roll_result = gr.Textbox(value="No check rolled yet.", interactive=False, label="Tool result")
with gr.Column(scale=2):
gr.Markdown("### 🎙️ Live Stage")
gr.HTML(
"""
<div class="transport-card">
<div id="rf-phase">IDLE</div>
<div>One click keeps the microphone open for the full session. No Record/Waiting toggle.</div>
<div id="rf-level-shell"><div id="rf-level"></div></div>
</div>
"""
)
status = gr.Markdown("Choose a cue, wear headphones, then start the live session.")
with gr.Row():
start_btn = gr.Button("Start 120-second live session", variant="primary")
stop_btn = gr.Button("Stop live session", variant="stop")
with gr.Row():
transcript = gr.Markdown("No completed model responses yet. Visitor speech is not separately transcribed.")
metrics = gr.Code(value="{}", language="json", label="Live diagnostics")
gr.Markdown(
"**Privacy:** fictional speech only; never enter real secrets. Hidden game facts are enforced outside model context. "
"No voice cloning, external inference API, WebRTC relay, or durable audio storage. "
"Raw microphone/audio packets and conversation state are volatile and bounded to the live session."
)
apply_btn.click(apply_cue, inputs=[cue, scene], outputs=[scene, cue_status], queue=False, api_name="apply_cue")
roll_btn.click(roll_perception, inputs=[scene], outputs=[scene, roll_result], queue=False, api_name="roll_perception")
start_event = start_btn.click(
new_live_session,
outputs=[transport_id, status, metrics, transcript],
js=CAPTURE_MIC_JS,
queue=False,
show_progress="hidden",
api_name="new_live_session",
)
bridge_event = start_event.then(
transport_ready,
inputs=[transport_id],
outputs=[transport_id],
js=START_TRANSPORT_JS,
queue=False,
show_progress="hidden",
api_name="transport_ready",
)
bridge_event.then(
run_live_session,
inputs=[scene, transport_id],
outputs=[status, transcript, metrics],
concurrency_limit=1,
show_progress="hidden",
api_name="run_live_session",
)
stop_btn.click(
request_live_stop,
inputs=[transport_id],
outputs=[status],
js=STOP_TRANSPORT_JS,
queue=False,
show_progress="hidden",
api_name="stop_live_session",
)
async def roleforge_health():
return {
"status": "ok",
"transport": TRANSPORT_VERSION,
"model_contract": "official exact interleaved system prompt",
"assistant_identity": ASSISTANT_NAME,
"model": MODEL_REPO,
"model_revision": MODEL_REVISION,
}
async def roleforge_audio_socket(websocket: WebSocket, transport_id: str):
if not re.fullmatch(r"[0-9a-f]{32}", transport_id):
await websocket.close(code=1008)
return
bridge = _live_sessions.get(transport_id)
if bridge is None:
await websocket.close(code=1008)
return
input_queue, output_queue, stop_event, _, counters = bridge
await websocket.accept()
counters["websocket_connected"] = True
sample_rate = 48_000
async def receive_audio():
nonlocal sample_rate
while not stop_event.is_set():
message = await websocket.receive()
if message.get("type") == "websocket.disconnect":
raise WebSocketDisconnect
if message.get("text"):
payload = json.loads(message["text"])
if payload.get("type") == "hello":
candidate = int(payload.get("sample_rate", sample_rate))
if 8_000 <= candidate <= 192_000:
sample_rate = candidate
elif payload.get("type") == "telemetry":
counters["browser_playback_underruns"] = max(
0, int(payload.get("playback_underruns", 0))
)
counters["browser_scheduled_ahead_ms"] = max(
0, min(5_000, int(payload.get("scheduled_ahead_ms", 0)))
)
counters["browser_button_to_websocket_ms"] = max(
0, min(300_000, int(payload.get("button_to_websocket_ms", 0)))
)
counters["browser_button_to_listening_ms"] = max(
0, min(300_000, int(payload.get("button_to_listening_ms", 0)))
)
counters["browser_button_to_first_audio_ms"] = max(
0, min(300_000, int(payload.get("button_to_first_audio_ms", 0)))
)
continue
raw = message.get("bytes")
if not raw or len(raw) % 4:
continue
audio = np.frombuffer(raw, dtype="<f4").copy()
if not audio.size or not np.isfinite(audio).all():
continue
audio = np.clip(audio, -1.0, 1.0)
counters["received_chunks"] = int(counters.get("received_chunks", 0)) + 1
item = (sample_rate, audio)
try:
input_queue.put_nowait(item)
except queue.Full:
try:
input_queue.get_nowait()
except queue.Empty:
pass
input_queue.put_nowait(item)
counters["dropped_chunks"] = int(counters.get("dropped_chunks", 0)) + 1
async def send_audio():
while not stop_event.is_set() or not output_queue.empty():
try:
kind, payload = await asyncio.to_thread(output_queue.get, True, 0.1)
except queue.Empty:
continue
if kind == "control":
await websocket.send_text(payload)
elif kind == "audio":
await websocket.send_bytes(payload)
await asyncio.sleep((len(payload) / 2) / OUTPUT_SAMPLE_RATE)
try:
await asyncio.gather(receive_audio(), send_audio())
except (WebSocketDisconnect, RuntimeError, asyncio.CancelledError):
pass
finally:
counters["websocket_connected"] = False
stop_event.set()
try:
await websocket.close()
except RuntimeError:
pass
_gradio_create_app = GradioApp.create_app
def create_app_with_roleforge_transport(*args, **kwargs):
"""Attach raw PCM routes to the supervisor-owned Gradio FastAPI app."""
app = _gradio_create_app(*args, **kwargs)
app.add_api_route("/roleforge/health", roleforge_health, methods=["GET"])
app.add_api_websocket_route("/roleforge/ws/{transport_id}", roleforge_audio_socket)
return app
GradioApp.create_app = staticmethod(create_app_with_roleforge_transport)
if __name__ == "__main__":
demo.queue(default_concurrency_limit=2, max_size=6).launch()