# app.py - Fixed version (Microphone-only input) # Drop this into your HF Space alongside: avatar.png, avatar.js, style.css # Requirements: gradio, transformers, torch, gtts, pydub, faster-whisper (optional) # Notes: This version uses microphone-only input (gr.Audio sources=["microphone"]) import os import uuid import tempfile import json from pathlib import Path from datetime import datetime import gradio as gr # STT (prefer faster_whisper if available; fallback to whisper) WHISPER_AVAILABLE = False try: from faster_whisper import WhisperModel whisper_model = WhisperModel("small", device="cpu", compute_type="int8") WHISPER_AVAILABLE = True except Exception: try: import whisper whisper_model = whisper.load_model("small") except Exception: whisper_model = None # LLM: use a lightweight ungated model that's safe for public Spaces # Flan-T5 small is reliable on CPU and avoids gated repos from transformers import pipeline, AutoTokenizer, AutoModelForSeq2SeqLM LLM_MODEL = "google/flan-t5-small" try: tokenizer = AutoTokenizer.from_pretrained(LLM_MODEL) model = AutoModelForSeq2SeqLM.from_pretrained(LLM_MODEL) llm_pipe = pipeline("text2text-generation", model=model, tokenizer=tokenizer) except Exception as e: llm_pipe = None print("LLM load error:", e) # TTS from gtts import gTTS from pydub import AudioSegment # Memory persistence MEMORY_FILE = Path("memory.json") if MEMORY_FILE.exists(): try: with open(MEMORY_FILE, "r", encoding="utf-8") as f: MEMORY = json.load(f) except Exception: MEMORY = {"history": []} else: MEMORY = {"history": []} def save_memory(): try: with open(MEMORY_FILE, "w", encoding="utf-8") as f: json.dump(MEMORY, f, ensure_ascii=False, indent=2) except Exception as e: print("Memory save error:", e) # ---------------- Helpers ---------------- def transcribe_audio(path): """Transcribe audio using faster_whisper or whisper.""" if whisper_model is None: return "" try: if WHISPER_AVAILABLE: segments, _ = whisper_model.transcribe(path) return " ".join([s.text for s in segments]) else: res = whisper_model.transcribe(path) return res.get("text", "") except Exception as e: print("Transcription error:", e) return "" def ask_llm(user_text, max_length=200): """Ask the LLM and return a concise reply with memory context.""" if llm_pipe is None: return "Sorry — language model temporarily unavailable." try: # Build a compact context from memory recent = MEMORY.get("history", [])[-8:] ctx_lines = [] for item in recent: if "user" in item: ctx_lines.append(f"User: {item['user']}") if "assistant" in item: ctx_lines.append(f"Assistant: {item['assistant']}") ctx = "\n".join(ctx_lines) prompt = ( "You are a kind, highly capable AI companion and tutor. Answer concisely, " "give a short practical example when relevant, and ask one follow-up question. " "Match the user's language and tone.\n\n" f"{ctx}\nUser: {user_text}\nAssistant:" ) out = llm_pipe(prompt, max_length=max_length, do_sample=False) text = out[0]["generated_text"] # Trim possible preamble if "Assistant:" in text: text = text.split("Assistant:")[-1].strip() return text.strip() except Exception as e: print("LLM error:", e) return "I couldn't think clearly — try asking again." def tts_gtts_save(text, out_path, lang="en"): """Create an mp3 via gTTS (multilingual).""" try: if not text or not text.strip(): silent = AudioSegment.silent(duration=400) silent.export(out_path, format="mp3") return out_path tts = gTTS(text=text, lang=lang, slow=False) tts.save(out_path) return out_path except Exception as e: print("TTS error:", e) silent = AudioSegment.silent(duration=400) silent.export(out_path, format="mp3") return out_path # ---------------- Main pipeline ---------------- def process_interaction(avatar_upload, mic_audio, typed_text, tts_lang): """ microphone-only mode: mic_audio will be provided by gradio avatar_upload: optional file upload (if user wants to override repo avatar) typed_text: optional typed message tts_lang: language code for TTS """ uid = uuid.uuid4().hex[:8] tmpdir = Path(tempfile.gettempdir()) / f"avatar_{uid}" tmpdir.mkdir(parents=True, exist_ok=True) # Avatar selection: prefer repo avatar.png if exists, else avatar_upload repo_avatar = Path("avatar.png") if repo_avatar.exists(): avatar_path = str(repo_avatar) elif avatar_upload: # avatar_upload is a file-like object (UploadedFile); save it try: p = tmpdir / "avatar_upload.png" with open(p, "wb") as f: f.write(avatar_upload.read()) avatar_path = str(p) except Exception: avatar_path = None else: avatar_path = None if avatar_path is None: return gr.HTML("No avatar found. Upload avatar.png to the repo or use the upload field."), "No avatar", None # Determine user text user_text = "" if typed_text and typed_text.strip(): user_text = typed_text.strip() else: if mic_audio is None: return None, "Provide audio (microphone) or type a message.", None # mic_audio is a tempfile-like object with .read(); save it audio_path = tmpdir / "user_in.wav" try: with open(audio_path, "wb") as f: f.write(mic_audio.read()) except Exception as e: print("Audio save error:", e) return None, "Failed to read microphone audio.", None user_text = transcribe_audio(str(audio_path)) if not user_text: return None, "Couldn't transcribe. Try again or type your message.", None # Add to memory MEMORY.setdefault("history", []).append({"user": user_text, "time": datetime.utcnow().isoformat()}) MEMORY["history"] = MEMORY["history"][-200:] save_memory() # LLM reply reply_text = ask_llm(user_text) # Save assistant reply to memory MEMORY.setdefault("history", []).append({"assistant": reply_text, "time": datetime.utcnow().isoformat()}) MEMORY["history"] = MEMORY["history"][-200:] save_memory() # TTS tts_path = tmpdir / "reply.mp3" tts_gtts_save(reply_text, str(tts_path), lang=tts_lang or "en") # Build HTML to show avatar canvas and trigger lip-sync using avatar.js # Use Gradio-served file path (client will resolve) audio_file_url = str(tts_path) html = f"""