feat: spirit engine — seance WS, entity minting/Codex, Piper TTS voices, wire telemetry

- WS /ws/session: modes, summon, anomaly fragments, streaming direct contact,
  passive wire-ghost ambient loop, per-user rate limits, event transcript
- entities: anomaly-signature fingerprinting, LLM minting + procedural
  fallback, Codex matching with contact counts and sightings
- tts: 8 local Piper voices (EN/ES), per-entity voice profiles, numpy
  effects chain (pitch/rate/bitcrush/echo/static)
- llm: streaming client, submit_stream in bounded queue, SpiritService
  with offline fallbacks for every channel
- routes: public /api/codex, /api/codex/{id}, /api/stats; /audio static mount
- models: Entity, EntitySighting, Event, ContactSession(entity_id, language)
This commit is contained in:
Indiana
2026-07-20 20:13:28 +00:00
parent 52afad1ad5
commit b9110f45de
28 changed files with 1997 additions and 4 deletions

113
backend/app/telemetry.py Normal file
View File

@@ -0,0 +1,113 @@
"""Wire Ghost telemetry: real, non-content network metrics from the app host.
Hard privacy boundary (spec §3.3): only counters and timings are ever read —
interface byte counters from /proc/net/dev, TCP connect latency, DNS
resolution timing. Packet payloads are never inspected.
"""
import asyncio
import socket
import time
from dataclasses import dataclass, field
# Hosts we measure connect latency against (timing only, no data exchanged
# beyond the handshake).
REFERENCE_HOSTS = [("1.1.1.1", 53), ("10.30.20.107", 11434)]
REFERENCE_DNS = ["example.com", "cloudflare.com"]
@dataclass
class TelemetrySample:
jitter_bytes_per_s: float = 0.0
latency_variance_ms: float = 0.0
latency_mean_ms: float = 0.0
dns_ms: float = 0.0
extra: dict = field(default_factory=dict)
def as_dict(self) -> dict:
return {
"jitter_bytes_per_s": self.jitter_bytes_per_s,
"latency_variance_ms": self.latency_variance_ms,
"latency_mean_ms": self.latency_mean_ms,
"dns_ms": self.dns_ms,
}
def parse_proc_net_dev(text: str) -> dict[str, tuple[int, int]]:
"""Parse /proc/net/dev into {interface: (rx_bytes, tx_bytes)}."""
counters: dict[str, tuple[int, int]] = {}
for line in text.splitlines()[2:]:
if ":" not in line:
continue
iface, rest = line.split(":", 1)
fields = rest.split()
if len(fields) >= 9:
counters[iface.strip()] = (int(fields[0]), int(fields[8]))
return counters
def _total_bytes(counters: dict[str, tuple[int, int]]) -> int:
return sum(rx + tx for iface, (rx, tx) in counters.items() if iface != "lo")
async def _read_counters() -> int:
def _read() -> int:
with open("/proc/net/dev") as f:
return _total_bytes(parse_proc_net_dev(f.read()))
return await asyncio.to_thread(_read)
async def _tcp_rtt(host: str, port: int, timeout: float = 1.5) -> float | None:
"""Connect-then-close round trip time in ms. Nothing is sent or read."""
start = time.perf_counter()
try:
reader, writer = await asyncio.wait_for(
asyncio.open_connection(host, port), timeout=timeout
)
writer.close()
try:
await writer.wait_closed()
except (ConnectionError, OSError):
pass
return (time.perf_counter() - start) * 1000
except (asyncio.TimeoutError, ConnectionError, OSError):
return None
async def _dns_timing(name: str, timeout: float = 1.5) -> float | None:
start = time.perf_counter()
try:
await asyncio.wait_for(asyncio.to_thread(socket.getaddrinfo, name, None), timeout)
return (time.perf_counter() - start) * 1000
except (asyncio.TimeoutError, socket.gaierror, OSError):
return None
async def sample_network(period_s: float = 1.0) -> TelemetrySample:
"""Take one telemetry sample: byte-counter jitter over `period_s`,
plus latency variance and DNS hesitation."""
before = await _read_counters()
await asyncio.sleep(period_s)
after = await _read_counters()
jitter = abs(after - before) / period_s
rtts = [
rtt
for rtt in await asyncio.gather(*(_tcp_rtt(h, p) for h, p in REFERENCE_HOSTS))
if rtt is not None
]
dns_times = [
ms
for ms in await asyncio.gather(*(_dns_timing(n) for n in REFERENCE_DNS))
if ms is not None
]
sample = TelemetrySample(jitter_bytes_per_s=jitter)
if rtts:
mean = sum(rtts) / len(rtts)
sample.latency_mean_ms = mean
sample.latency_variance_ms = sum((r - mean) ** 2 for r in rtts) / len(rtts)
if dns_times:
sample.dns_ms = sum(dns_times) / len(dns_times)
return sample