"""
AI Nexus · P1 demo — "Speech that stays yours"
A staged, visual walkthrough of the P1 assistive-speech pipeline:
1 · Heard — faster-whisper (ASR) turns degraded speech into a raw transcript
2 · Understood — a small local LLM corrects it into the intended message
3 · Spoken — Piper TTS says the clear message out loud
Plus a live "green meter" (energy + CO2 per run) via codecarbon.
This public Space runs in the cloud for convenience only.
The product itself runs the exact same pipeline fully on-device (Raspberry Pi 5).
"""
import difflib
import math
import os
import tempfile
import time
import wave
import gradio as gr
from faster_whisper import WhisperModel
from huggingface_hub import hf_hub_download
from llama_cpp import Llama
from piper import PiperVoice
# -----------------------------------------------------------------------------
# Configuration (override via Space "Variables" — no code changes needed)
# -----------------------------------------------------------------------------
WHISPER_MODEL = os.getenv("WHISPER_MODEL", "small") # tiny/base/small
ASR_LANG = os.getenv("ASR_LANG", "en")
# Default LLM is Qwen 2.5 1.5B Instruct (Apache-2.0, no license gate).
# To use Llama 3.2 1B instead, set Space variables:
# LLM_REPO = bartowski/Llama-3.2-1B-Instruct-GGUF
# LLM_FILE = Llama-3.2-1B-Instruct-Q4_K_M.gguf
LLM_REPO = os.getenv("LLM_REPO", "Qwen/Qwen2.5-1.5B-Instruct-GGUF")
LLM_FILE = os.getenv("LLM_FILE", "qwen2.5-1.5b-instruct-q4_k_m.gguf")
# A UK voice to match the team; swap for any voice in rhasspy/piper-voices.
PIPER_ONNX = os.getenv(
"PIPER_ONNX", "en/en_GB/alan/medium/en_GB-alan-medium.onnx"
)
CARBON_COUNTRY = os.getenv("CARBON_COUNTRY", "GBR")
# -----------------------------------------------------------------------------
# Load models once at startup
# -----------------------------------------------------------------------------
print(f"[startup] loading Whisper '{WHISPER_MODEL}' (CPU, int8)…")
ASR = WhisperModel(WHISPER_MODEL, device="cpu", compute_type="int8")
print(f"[startup] downloading LLM {LLM_REPO}/{LLM_FILE}…")
llm_path = hf_hub_download(repo_id=LLM_REPO, filename=LLM_FILE)
LLM = Llama(
model_path=llm_path,
n_ctx=2048,
n_threads=max(2, os.cpu_count() or 2),
verbose=False,
)
print(f"[startup] downloading Piper voice {PIPER_ONNX}…")
piper_model = hf_hub_download(repo_id="rhasspy/piper-voices", filename=PIPER_ONNX)
piper_cfg = hf_hub_download(
repo_id="rhasspy/piper-voices", filename=PIPER_ONNX + ".json"
) # downloaded so it sits next to the .onnx; Piper >=1.3 auto-detects it
TTS = PiperVoice.load(piper_model)
print("[startup] ready.")
SYSTEM_PROMPT = (
"You are the language layer of an assistive communication aid. "
"You receive the raw automatic transcript of speech from a person with a "
"speech impairment. The transcript may contain recognition errors, missing "
"words, or fragments. Rewrite it as the single most likely intended "
"message, in the first person, preserving the speaker's meaning and tone. "
"If the transcript is already clear, return it unchanged. "
"Reply with the corrected sentence only — no commentary, no quotes."
)
# -----------------------------------------------------------------------------
# Pipeline stages
# -----------------------------------------------------------------------------
def stage_heard(audio_path: str, phrase_bank: str):
"""ASR with optional phrase-bank biasing (personalization preview)."""
prompt = phrase_bank.strip()[:200] if phrase_bank else None
segments, _info = ASR.transcribe(
audio_path,
language=ASR_LANG,
beam_size=5,
vad_filter=True,
initial_prompt=prompt,
)
segs = list(segments)
text = " ".join(s.text.strip() for s in segs).strip()
if segs:
conf = sum(math.exp(s.avg_logprob) for s in segs) / len(segs)
else:
conf = 0.0
return text, conf
def stage_understood(raw: str) -> str:
"""Small-LLM correction into the intended message."""
out = LLM.create_chat_completion(
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": raw},
],
max_tokens=96,
temperature=0.2,
)
text = out["choices"][0]["message"]["content"].strip().strip('"')
return text or raw
def stage_spoken(text: str) -> str:
"""Piper TTS to a temp wav, returned as a filepath for gr.Audio."""
path = tempfile.NamedTemporaryFile(suffix=".wav", delete=False).name
with wave.open(path, "wb") as f:
TTS.synthesize_wav(text, f)
return path
def diff_html(raw: str, fixed: str) -> str:
"""Word-level diff: what the corrector changed."""
a, b = raw.split(), fixed.split()
sm = difflib.SequenceMatcher(None, a, b)
parts = []
for op, i1, i2, j1, j2 in sm.get_opcodes():
if op == "equal":
parts.append(" ".join(a[i1:i2]))
if op in ("replace", "delete") and i2 > i1:
parts.append(f'{" ".join(a[i1:i2])}')
if op in ("replace", "insert") and j2 > j1:
parts.append(f'{" ".join(b[j1:j2])}')
body = " ".join(p for p in parts if p)
return f'
Degraded speech in, clear speech out. ' "Watch each stage of the pipeline do its part.
" '