| import re, os, threading |
| from fastapi import FastAPI, Request |
| from fastapi.responses import JSONResponse, FileResponse, StreamingResponse |
| import uvicorn |
| from llama_cpp import Llama |
| from emotion import analyze_day |
|
|
| HERE = os.path.dirname(os.path.abspath(__file__)) |
| MODEL_PATH = os.path.join(HERE, "models", "qwen2.5-1.5b-instruct-q4_k_m.gguf") |
| if not os.path.exists(MODEL_PATH): |
| from huggingface_hub import hf_hub_download |
| print("Downloading Qwen2.5-1.5B Q4 GGUF...") |
| os.makedirs(os.path.join(HERE, "models"), exist_ok=True) |
| hf_hub_download("Qwen/Qwen2.5-1.5B-Instruct-GGUF", "qwen2.5-1.5b-instruct-q4_k_m.gguf", |
| local_dir=os.path.join(HERE, "models")) |
|
|
| WIT_PERSONA = ("You are Wit, the funniest friend in the group chat. Your ONE job is to make them laugh - not to " |
| "help, advise, or comfort. Lead with the funniest, most unexpected take on the SPECIFIC thing they said, then " |
| "add a playful follow-up so the banter keeps going. Be punchy and specific: one sharp joke beats three vague " |
| "ones. Your humor blends everyday life with an Indian flavor (chai, Maggi, Indian parents and shaadi, traffic, " |
| "cricket, Bollywood drama) so it lands for both Indian and Western friends. Never give life-coach advice, " |
| "never end on a cheesy moral or a platitude like 'beauty comes in many hues'. Warm and clever, never mean - " |
| "never joke about looks, body, religion, caste, or identity. Keep it short: 1-3 punchy sentences. Always reply in English.") |
| WIT_SHOTS = [ |
| ("It's Monday again.", "Monday: the sequel nobody asked for, breaking box-office records of dread every single week. What's today's first villain - your inbox, or a meeting that could've peacefully been an email?"), |
| ("I'm so bored.", "Boredom: the fancy word for opening the same three apps on a loop, praying one of them got interesting in the last 40 seconds. Bold of your brain to run out of ideas AND reject every suggestion. What's the last thing that actually held your attention?"), |
| ("My mom keeps asking when I'll get married.", "Ah, Operation Shaadi - mummy's longest-running startup, fully bootstrapped on 100% conviction and zero chill. What's her latest pitch: 'just meet once, no pressure'?"), |
| ("What's the colour of my eyes?", "I'm a chat box, not a mirror with opinions - but I'll guess 'the exact shade of someone avoiding their to-do list.' Why, planning to blame a bad decision on mysterious eyes?"), |
| ("I'm so tired.", "You've got the battery percentage of a phone stuck at 1% and refusing to die out of pure spite. When did you last sleep like an actual human and not a stressed-out raccoon?"), |
| ("Tell me a joke.", "Adulthood is just googling how to do basic things and praying the third YouTube video has an Indian guy who actually explains it. But your real life is funnier - what's going on today?"), |
| ] |
| BUDDY_PERSONA = ("You are Buddy, a warm, emotionally intelligent friend and supportive coach. Help the person " |
| "feel heard and a little lighter. Use these techniques naturally: reflect back what they're feeling so " |
| "they feel understood; validate their emotions without judging ('that's completely understandable'); " |
| "ask ONE gentle, open question to help them explore; and when it helps, offer a kind reframe or one " |
| "small doable step. Be genuine and conversational - never lecture, diagnose, or sound clinical. " |
| "Keep it short: 2-4 sentences. You're a caring friend, NOT a therapist: don't give medical or clinical " |
| "advice. If they mention self-harm, abuse, or crisis, gently encourage them to reach out to a " |
| "mental-health professional or a crisis line. Always reply in English.") |
| DAYCHECK_PERSONA = ("You are a warm, emotionally intelligent friend giving a short daily emotional check-in. " |
| "You are shown an emotional read (produced by a separate model) of what the person shared, plus their actual " |
| "words. Write a genuine, grounded 2-3 sentence note in one short paragraph. If they lean low (sadness, anger, fear), gently validate " |
| "the feeling and offer ONE small doable step. If they lean upbeat (joy, love), celebrate it warmly AND add a " |
| "light grounding nudge so they savor it without overdoing it. If the day reads calm or neutral, simply reflect " |
| "that back warmly without inventing drama. Always ground it in what they ACTUALLY said - if their words are " |
| "positive, never call the day negative. Do NOT open with a greeting like 'Hey there' or 'Hi' - start straight " |
| "into the reflection and vary how you open each time. Never lecture, never sound clinical, never ask them for " |
| "more personal details. Always reply in English.") |
|
|
| print("Loading Qwen2.5-1.5B Q4 (llama.cpp, CPU)...") |
| llm = Llama(model_path=MODEL_PATH, n_ctx=4096, n_gpu_layers=0, verbose=False) |
| _lock = threading.Lock() |
|
|
| STOPS = ["<|im_end|>", "<|im_start|>"] |
| FOREIGN = re.compile(r"[\u00c0-\u024f\u0370-\u05ff\u0600-\u074f\u0900-\u0fff\u1100-\u11ff\u3000-\u9fff\uac00-\ud7ff\uf900-\ufaff\uff00-\uffef]") |
| PROFANITY = [(re.compile(r"\bfuck\w*",re.I),"heck"),(re.compile(r"\bshit\w*",re.I),"crap"),(re.compile(r"\bbitch\w*",re.I),"menace"), |
| (re.compile(r"\bbastard\w*",re.I),"goofball"),(re.compile(r"\b(slut|whore)\w*",re.I),"menace"),(re.compile(r"\bdamn\w*",re.I),"darn"),(re.compile(r"\bass(hole|es)?\b",re.I),"clown")] |
|
|
| def scrub(t): |
| for p, rep in PROFANITY: t = p.sub(rep, t) |
| return t |
|
|
| def clean(t): |
| t = FOREIGN.sub("", t) |
| t = re.split(r"\n\s*(?:user|assistant|system)\b\s*:?", t)[0] |
| t = re.sub(r"/\*.*?\*/", "", t, flags=re.S) |
| t = re.sub(r"<\|[^>]*\|>", "", t) |
| t = re.sub(r"(?:/{2,}|-{4,}|={4,}|_{4,})", " ", t) |
| return re.sub(r"\s+", " ", t).strip() |
|
|
| def build_msgs(persona, shots, history, message): |
| msgs = [{"role":"system","content":persona}] |
| for u, a in shots: |
| msgs += [{"role":"user","content":u}, {"role":"assistant","content":a}] |
| for m in (history or []): |
| if isinstance(m, dict) and m.get("role") in ("user","assistant") and m.get("content"): |
| msgs.append({"role":m["role"], "content":m["content"]}) |
| msgs.append({"role":"user", "content":message}) |
| return msgs |
|
|
| def stream_reply(persona, shots, history, message, temperature, max_tokens, witty=False): |
| msgs = build_msgs(persona, shots, history, message) |
| with _lock: |
| for ch in llm.create_chat_completion(messages=msgs, max_tokens=max_tokens, temperature=temperature, |
| top_p=0.9, repeat_penalty=(1.15 if witty else 1.1), |
| stop=STOPS, stream=True): |
| d = ch["choices"][0]["delta"].get("content") |
| if not d: |
| continue |
| d = FOREIGN.sub("", d) |
| if witty: |
| d = scrub(d) |
| if d: |
| yield d |
|
|
| def generate_once(persona, shots, history, message, temperature, max_tokens): |
| msgs = build_msgs(persona, shots, history, message) |
| with _lock: |
| out = llm.create_chat_completion(messages=msgs, max_tokens=max_tokens, temperature=temperature, |
| top_p=0.9, repeat_penalty=1.1, stop=STOPS) |
| return clean(out["choices"][0]["message"]["content"]) |
|
|
| def daycheck_note(analysis, text): |
| snippet = re.sub(r"\s+", " ", (text or "").strip())[:500] |
| user = (f'Here is what I shared: "{snippet}"\n' |
| f'An emotion model read this as mostly {analysis.get("dominant","mixed")} ' |
| f'({analysis.get("valence","mixed")} overall). In 2-3 warm sentences as one short paragraph, ' |
| f'gently reflect this back to me and, if it fits, offer one small suggestion.') |
| return generate_once(DAYCHECK_PERSONA, [], [], user, 0.55, 140) |
|
|
| app = FastAPI() |
|
|
| @app.get("/") |
| def index(): |
| return FileResponse(os.path.join(HERE, "static", "index.html")) |
|
|
| @app.post("/api/chat") |
| async def chat(req: Request): |
| data = await req.json() |
| message = (data.get("message") or "").strip() |
| mode = data.get("mode", "buddy") |
| history = data.get("history", []) |
| try: temp = float(data.get("temperature", 0.6)) |
| except (TypeError, ValueError): temp = 0.6 |
| temp = max(0.1, min(1.2, temp)) |
| if not message: |
| return StreamingResponse(iter([""]), media_type="text/plain; charset=utf-8") |
| if mode == "wit": |
| gen = stream_reply(WIT_PERSONA, WIT_SHOTS, history, message, temp, 100, witty=True) |
| else: |
| gen = stream_reply(BUDDY_PERSONA, [], history, message, 0.7, 160, witty=False) |
| return StreamingResponse(gen, media_type="text/plain; charset=utf-8") |
|
|
| @app.post("/api/daycheck") |
| async def day_check(req: Request): |
| data = await req.json() |
| texts = data.get("texts") |
| if not isinstance(texts, list): |
| texts = [data.get("text", "")] |
| texts = [t for t in texts if isinstance(t, str) and t.strip()] |
| if not texts: |
| return JSONResponse({"empty": True}) |
| analysis = analyze_day(texts) |
| analysis["note"] = daycheck_note(analysis, " ".join(texts)) |
| return JSONResponse(analysis) |
|
|
| if __name__ == "__main__": |
| uvicorn.run(app, host="127.0.0.1", port=7860) |
|
|