| """Kith Local — Build Small spike. |
| |
| A relationship memory on small open models you own. Audio → Cohere Transcribe |
| (2B, on ZeroGPU) → MiniCPM extraction (OpenBMB) → SQLite → the Today feed, |
| served as a hand-written HTML page over gr.Server(). |
| """ |
|
|
| import base64 |
| import os |
| import tempfile |
| from datetime import date |
|
|
| import gradio as gr |
| import spaces |
| import torch |
| from fastapi.responses import FileResponse, HTMLResponse |
|
|
| import brain |
| import db |
| from frontend import FRONTEND_HTML |
|
|
| ASR_MODEL_ID = "CohereLabs/cohere-transcribe-03-2026" |
| BRAIN_MODEL_ID = os.environ.get("KITH_GPU_MODEL", "openbmb/MiniCPM4.1-8B") |
| HF_TOKEN = os.environ.get("HF_TOKEN") |
|
|
| device = "cuda" if torch.cuda.is_available() else "cpu" |
|
|
| |
| |
| |
| ON_GPU_SPACE = bool(os.environ.get("SPACES_ZERO_GPU")) or torch.cuda.is_available() |
|
|
| |
| |
| SKIP_ASR = os.environ.get("KITH_SKIP_ASR") == "1" |
|
|
| |
| |
| |
| |
| brain.KITH_BRAIN = os.environ.get("KITH_BRAIN") or ("gpu" if ON_GPU_SPACE else "hosted") |
|
|
| if not SKIP_ASR: |
| from transformers import AutoProcessor, CohereAsrForConditionalGeneration |
|
|
| processor = AutoProcessor.from_pretrained(ASR_MODEL_ID, token=HF_TOKEN) |
| asr_model = CohereAsrForConditionalGeneration.from_pretrained( |
| ASR_MODEL_ID, dtype="auto", token=HF_TOKEN |
| ).to(device) |
|
|
| |
| |
| if brain.KITH_BRAIN == "gpu": |
| from transformers import AutoModelForCausalLM, AutoTokenizer |
|
|
| brain_tokenizer = AutoTokenizer.from_pretrained(BRAIN_MODEL_ID, trust_remote_code=True, token=HF_TOKEN) |
| brain_model = AutoModelForCausalLM.from_pretrained( |
| BRAIN_MODEL_ID, dtype="auto", trust_remote_code=True, token=HF_TOKEN, |
| low_cpu_mem_usage=True, |
| ).to(device) |
|
|
| @spaces.GPU(duration=120) |
| def _brain_chat_gpu(messages: list, temperature: float) -> str: |
| """Local MiniCPM4.1-8B generate. Same (messages, temp) -> text contract |
| brain.extract() expects; thinking disabled, JSON enforced by the prompt.""" |
| dev = "cuda" if torch.cuda.is_available() else "cpu" |
| brain_model.to(dev) |
| inputs = brain_tokenizer.apply_chat_template( |
| messages, |
| add_generation_prompt=True, |
| tokenize=True, |
| return_dict=True, |
| return_tensors="pt", |
| enable_thinking=False, |
| ).to(dev) |
| out = brain_model.generate( |
| **inputs, |
| max_new_tokens=2500, |
| do_sample=temperature > 0, |
| temperature=temperature if temperature > 0 else None, |
| pad_token_id=brain_tokenizer.eos_token_id, |
| ) |
| gen = out[0][inputs["input_ids"].shape[1]:] |
| return brain_tokenizer.decode(gen, skip_special_tokens=True) |
|
|
| brain.set_gpu_chat(_brain_chat_gpu) |
|
|
|
|
| @spaces.GPU(duration=45) |
| def _transcribe_file(wav_path: str) -> str: |
| import librosa |
|
|
| |
| dev = "cuda" if torch.cuda.is_available() else "cpu" |
| asr_model.to(dev) |
| audio, _ = librosa.load(wav_path, sr=16000, mono=True) |
| inputs = processor( |
| audio=audio, |
| sampling_rate=16000, |
| return_tensors="pt", |
| language="en", |
| punctuation=True, |
| ).to(dev) |
| inputs["input_features"] = inputs["input_features"].to(asr_model.dtype) |
| generated = asr_model.generate(**inputs, max_new_tokens=448) |
| chunks = processor.batch_decode(generated, skip_special_tokens=True) |
| return " ".join(c.strip() for c in chunks).strip() |
|
|
|
|
| def _transcribe_stub(_wav_path: str) -> str: |
| sample = os.path.join(os.path.dirname(__file__), "data", "sample-transcript.txt") |
| with open(sample) as f: |
| return f.read().strip() |
|
|
|
|
| |
| |
| |
| server = gr.Server() |
|
|
|
|
| @server.get("/", response_class=HTMLResponse) |
| async def homepage() -> str: |
| return FRONTEND_HTML |
|
|
|
|
| @server.get("/sample-audio") |
| async def sample_audio() -> FileResponse: |
| return FileResponse( |
| os.path.join(os.path.dirname(__file__), "data", "sample-conversation.mp3"), |
| media_type="audio/mpeg", |
| ) |
|
|
|
|
| |
| DEMO_CLIPS = {"sara-wedding", "daniel-career", "aunt-rose-call"} |
|
|
|
|
| @server.get("/clip/{name}") |
| async def demo_clip(name: str) -> FileResponse: |
| from fastapi import HTTPException |
|
|
| if name not in DEMO_CLIPS: |
| raise HTTPException(status_code=404, detail="unknown clip") |
| return FileResponse( |
| os.path.join(os.path.dirname(__file__), "data", "demo-clips", f"{name}.mp3"), |
| media_type="audio/mpeg", |
| ) |
|
|
|
|
| @server.api(name="transcribe") |
| def transcribe_api(audio_b64: str) -> str: |
| """16kHz mono WAV, base64-encoded by the browser -> transcript text.""" |
| raw = base64.b64decode(audio_b64) |
| with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f: |
| f.write(raw) |
| path = f.name |
| try: |
| return _transcribe_stub(path) if SKIP_ASR else _transcribe_file(path) |
| finally: |
| os.unlink(path) |
|
|
|
|
| @server.api(name="ingest") |
| def ingest_api(transcript: str) -> dict: |
| """Transcript -> MiniCPM extraction -> SQLite. Returns counts + extraction.""" |
| today = date.today().isoformat() |
| conversation_id = db.insert_conversation(transcript) |
| extraction = brain.extract(transcript, today) |
| counts = db.apply_extraction(conversation_id, extraction, today) |
| return {"counts": counts, "extraction": extraction} |
|
|
|
|
| @server.api(name="today") |
| def today_api(_ping: str = "") -> list: |
| """The Today feed: up to 6 cards (thoughtful / owed / reconnect).""" |
| return db.today_feed() |
|
|
|
|
| @server.api(name="conversations") |
| def conversations_api(_ping: str = "") -> list: |
| """History for the sidebar: every processed conversation, newest first.""" |
| return db.list_conversations() |
|
|
|
|
| @server.api(name="conversation_detail") |
| def conversation_detail_api(conversation_id: int) -> dict: |
| """One past conversation: its transcript + everything extracted from it.""" |
| return db.conversation_detail(conversation_id) |
|
|
|
|
| @server.api(name="card_detail") |
| def card_detail_api(card_id: str) -> dict: |
| """Everything behind one card: source quote + the rest of that person's memory.""" |
| return db.card_detail(card_id) |
|
|
|
|
| @server.api(name="dismiss") |
| def dismiss_api(card_id: str) -> dict: |
| """Hide one card; keep the underlying memory. Returns the refreshed feed.""" |
| db.dismiss_card(card_id) |
| return {"cards": db.today_feed()} |
|
|
|
|
| @server.api(name="reset") |
| def reset_api(_confirm: str = "") -> dict: |
| """Forget everything in the local memory.""" |
| return {"counts": db.reset_all()} |
|
|
|
|
| if __name__ == "__main__": |
| server.launch(server_name="0.0.0.0", server_port=7860, show_error=True) |
|
|