Hermes Bot
648bf8b | #!/usr/bin/env python3 | |
| """ | |
| CritterCalm — AI Voice Cloning Animal Soother | |
| ============================================== | |
| Clone your voice, then generate calming audio for your pets. | |
| Targets: Backyard AI track + Off the Grid, Well-Tuned, Field Notes badges. | |
| Model stack (all ≤32B total): | |
| - OmniVoice (0.6B) — zero-shot voice cloning from 3-10 sec audio | |
| - Dolphin-X1-8B (8B) — calming script generation via llama.cpp | |
| - Kokoro TTS (82M) — fallback built-in soothing voices | |
| Author: Build Small Hackathon 2026 | |
| License: Apache 2.0 | |
| """ | |
| import os | |
| import sys | |
| import time | |
| import tempfile | |
| import logging | |
| from pathlib import Path | |
| from typing import Optional, Generator | |
| import gradio as gr | |
| import numpy as np | |
| import soundfile as sf | |
| # Project modules | |
| from content.templates import get_template | |
| from content.script_generator import generate_calming_script | |
| from voice_cloning.openvoice_cloner import clone_voice | |
| from utils.audio_utils import load_audio, save_audio, validate_voice_sample | |
| # --------------------------------------------------------------------------- | |
| # Logging | |
| # --------------------------------------------------------------------------- | |
| logging.basicConfig( | |
| level=logging.INFO, | |
| format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", | |
| ) | |
| log = logging.getLogger("crittercalm") | |
| # --------------------------------------------------------------------------- | |
| # Paths — models stored locally or via env vars | |
| # --------------------------------------------------------------------------- | |
| MODEL_DIR = Path(os.environ.get("CRITTERCALM_MODEL_DIR", Path(__file__).parent / "models")) | |
| MODEL_DIR.mkdir(parents=True, exist_ok=True) | |
| # Deprecated: Dolphin GGUF path kept as None. Script generation now uses | |
| # the HF Inference API via content.script_generator (no local GGUF build). | |
| DOLPHIN_MODEL_PATH = None | |
| OMNIVOICE_MODEL_ID = os.environ.get("OMNIVOICE_MODEL_ID", "k2-fsa/OmniVoice") | |
| KOKORO_MODEL_PATH = os.environ.get( | |
| "KOKORO_MODEL_PATH", | |
| str(MODEL_DIR / "kokoro-v1.0.onnx"), | |
| ) | |
| KOKORO_VOICES_PATH = os.environ.get( | |
| "KOKORO_VOICES_PATH", | |
| str(MODEL_DIR / "voices-v1.0.bin"), | |
| ) | |
| # --------------------------------------------------------------------------- | |
| # Lazy-loaded model singletons | |
| # --------------------------------------------------------------------------- | |
| _omnivoice_model: Optional[object] = None | |
| _dolphin_llm: Optional[object] = None | |
| _kokoro_tts: Optional[object] = None | |
| _cloned_voice_embedding: Optional[object] = None # cached voice embedding | |
| def get_omnivoice(): | |
| """Lazy-load OmniVoice (0.6B params, Apache 2.0).""" | |
| global _omnivoice_model | |
| if _omnivoice_model is not None: | |
| return _omnivoice_model | |
| try: | |
| from omnivoice import OmniVoice | |
| import torch | |
| device = "cuda" if torch.cuda.is_available() else "cpu" | |
| log.info(f"Loading OmniVoice on {device} …") | |
| _omnivoice_model = OmniVoice.from_pretrained( | |
| OMNIVOICE_MODEL_ID, | |
| device_map=device, | |
| dtype=torch.float16 if device == "cuda" else torch.float32, | |
| ) | |
| log.info("OmniVoice loaded ✓") | |
| return _omnivoice_model | |
| except ImportError: | |
| log.warning("omnivoice not installed. Install with: pip install omnivoice") | |
| return None | |
| except Exception as exc: | |
| log.error(f"OmniVoice load failed: {exc}") | |
| return None | |
| # Dolphin LLM is no longer used locally. Script generation now uses the | |
| # HF Inference API via content.script_generator. The shim below preserves | |
| # the call sites in case any external MCP tool references it. | |
| def get_dolphin_llm(): | |
| """Deprecated. Returns None — use the HF Inference API via script_generator.""" | |
| return None | |
| def get_kokoro(): | |
| """Lazy-load Kokoro TTS (82M params, Apache 2.0) — fallback voices.""" | |
| global _kokoro_tts | |
| if _kokoro_tts is not None: | |
| return _kokoro_tts | |
| onnx_path = Path(KOKORO_MODEL_PATH) | |
| voices_path = Path(KOKORO_VOICES_PATH) | |
| if not onnx_path.exists() or not voices_path.exists(): | |
| log.warning("Kokoro model files not found. " | |
| "Download from https://github.com/thewh1teagle/kokoro-onnx") | |
| return None | |
| try: | |
| from kokoro_onnx import Kokoro | |
| log.info("Loading Kokoro TTS …") | |
| _kokoro_tts = Kokoro(str(onnx_path), str(voices_path)) | |
| log.info("Kokoro TTS loaded ✓") | |
| return _kokoro_tts | |
| except ImportError: | |
| log.warning("kokoro-onnx not installed. Install with: pip install kokoro-onnx") | |
| return None | |
| except Exception as exc: | |
| log.error(f"Kokoro load failed: {exc}") | |
| return None | |
| # --------------------------------------------------------------------------- | |
| # Voice cloning | |
| # --------------------------------------------------------------------------- | |
| def clone_voice(audio_path: str, ref_text: str) -> Optional[str]: | |
| """ | |
| Clone a voice from the recorded audio sample. | |
| Returns a status message and caches the voice embedding. | |
| """ | |
| global _cloned_voice_embedding | |
| if not audio_path: | |
| return "⚠️ Please record a voice sample first." | |
| if not ref_text or len(ref_text.strip()) < 5: | |
| return "⚠️ Please enter the transcript of what you said (at least 5 characters)." | |
| model = get_omnivoice() | |
| if model is None: | |
| return ( | |
| "❌ OmniVoice model is not loaded. Please:\n" | |
| "1. Install: `pip install omnivoice`\n" | |
| "2. Ensure the model can be downloaded from Hugging Face\n" | |
| "3. Check your internet connection for first-time model download" | |
| ) | |
| try: | |
| log.info(f"Cloning voice from {audio_path} …") | |
| # Generate a short test phrase with the cloned voice | |
| test_audio = model.generate( | |
| text="This is your cloned voice. I'll use this to help calm your pets.", | |
| ref_audio=audio_path, | |
| ref_text=ref_text.strip(), | |
| ) | |
| # Save test output for preview | |
| preview_path = Path(tempfile.gettempdir()) / "crittercalm_voice_preview.wav" | |
| sf.write(str(preview_path), test_audio[0], 24000) | |
| _cloned_voice_embedding = { | |
| "ref_audio": audio_path, | |
| "ref_text": ref_text.strip(), | |
| } | |
| log.info("Voice cloned successfully ✓") | |
| return ( | |
| "✅ Voice cloned successfully! Your voice is ready.\n\n" | |
| "🎧 Listen to the preview below, then go to the **Calm Your Pet** tab " | |
| "to generate soothing audio." | |
| ), str(preview_path) | |
| except Exception as exc: | |
| log.error(f"Voice cloning error: {exc}") | |
| return f"❌ Cloning failed: {exc}", None | |
| # --------------------------------------------------------------------------- | |
| # Calming script generation (Dolphin-X1-8B) | |
| # --------------------------------------------------------------------------- | |
| CALMING_SYSTEM_PROMPT = """You are a compassionate animal behavior expert who creates calming, | |
| soothing spoken messages for pets. Your words will be spoken aloud to the animal. | |
| Guidelines: | |
| - Use simple, rhythmic language with gentle repetition | |
| - Speak directly to the animal using its name if provided | |
| - Match the tone to the situation (soothing for anxiety, steady for storms, etc.) | |
| - Incorporate species-specific calming techniques: | |
| * Dogs: calm reassurance, short phrases, mention of familiar routines | |
| * Cats: soft, slow cadence, blink references, safe-space imagery | |
| * Chickens: gentle clucking sounds described, flock-safety messaging | |
| * Birds: soft whistles, perch-and-rest imagery | |
| * Rabbits: gentle burrow imagery, safety themes | |
| * Horses: steady breathing cues, herd-companion reassurance | |
| - Keep messages between 30 seconds and 3 minutes when spoken | |
| - Never use scary words or raise alarm | |
| - End each message with a gentle fade-out phrase | |
| Output ONLY the spoken script — no stage directions, no explanations.""" | |
| def generate_calming_script( | |
| animal: str, | |
| situation: str, | |
| duration_minutes: int, | |
| custom_message: str = "", | |
| pet_name: str = "", | |
| ) -> str: | |
| """Generate a calming script using Dolphin-X1-8B or fallback templates.""" | |
| llm = get_dolphin_llm() | |
| # Build the generation prompt | |
| duration_words = ( | |
| "very brief, about 30 seconds" | |
| if duration_minutes <= 1 | |
| else f"about {duration_minutes} minutes when read aloud slowly" | |
| ) | |
| name_clause = f"named {pet_name}" if pet_name.strip() else "" | |
| custom_clause = ( | |
| f"\nInclude this personal message naturally: \"{custom_message}\"" | |
| if custom_message.strip() | |
| else "" | |
| ) | |
| user_prompt = ( | |
| f"Write a calming spoken message for a {animal} {name_clause}.\n" | |
| f"Situation: {situation}.\n" | |
| f"Length: {duration_words}.{custom_clause}\n" | |
| f"Make it warm, soothing, and specifically tailored to a {animal}'s needs." | |
| ) | |
| if llm is not None: | |
| try: | |
| response = llm.create_chat_completion( | |
| messages=[ | |
| {"role": "system", "content": CALMING_SYSTEM_PROMPT}, | |
| {"role": "user", "content": user_prompt}, | |
| ], | |
| temperature=0.7, | |
| max_tokens=1024, | |
| ) | |
| script = response["choices"][0]["message"]["content"].strip() | |
| log.info(f"LLM script generated: {len(script)} chars") | |
| return script | |
| except Exception as exc: | |
| log.warning(f"LLM generation failed, using template: {exc}") | |
| # Fallback: pre-written templates | |
| return _get_template_script(animal, situation, pet_name, custom_message) | |
| def _get_template_script( | |
| animal: str, situation: str, pet_name: str, custom_message: str | |
| ) -> str: | |
| """Pre-written calming templates for when the LLM is unavailable.""" | |
| from content.templates import get_template | |
| return get_template(animal, situation, pet_name, custom_message) | |
| # --------------------------------------------------------------------------- | |
| # Audio generation | |
| # --------------------------------------------------------------------------- | |
| def generate_calming_audio( | |
| animal: str, | |
| situation: str, | |
| duration_minutes: int, | |
| voice_source: str, | |
| custom_message: str, | |
| pet_name: str, | |
| progress: gr.Progress = gr.Progress(), | |
| ) -> tuple[Optional[str], Optional[str]]: | |
| """ | |
| Full pipeline: generate script → speak with chosen voice → output audio. | |
| Returns (audio_path, info_text). | |
| """ | |
| if duration_minutes < 1: | |
| duration_minutes = 1 | |
| if duration_minutes > 30: | |
| duration_minutes = 30 | |
| progress(0.0, desc="Generating calming script …") | |
| script = generate_calming_script( | |
| animal=animal, | |
| situation=situation, | |
| duration_minutes=duration_minutes, | |
| custom_message=custom_message, | |
| pet_name=pet_name, | |
| ) | |
| progress(0.3, desc="Script ready. Synthesizing speech …") | |
| # Pick voice source | |
| try: | |
| if voice_source == "Cloned Voice" and _cloned_voice_embedding is not None: | |
| # Use OmniVoice with cloned embedding | |
| model = get_omnivoice() | |
| if model is None: | |
| raise RuntimeError("OmniVoice unavailable") | |
| audio = model.generate( | |
| text=script, | |
| ref_audio=_cloned_voice_embedding["ref_audio"], | |
| ref_text=_cloned_voice_embedding["ref_text"], | |
| ) | |
| sample_rate = 24000 | |
| audio_data = audio[0] | |
| elif voice_source in ("Soothing Female", "Soothing Male", "Whisper"): | |
| # Use Kokoro TTS with built-in voices | |
| kokoro = get_kokoro() | |
| if kokoro is None: | |
| raise RuntimeError("Kokoro TTS unavailable") | |
| voice_map = { | |
| "Soothing Female": "af_heart", # warm, gentle female | |
| "Soothing Male": "am_adam", # calm male | |
| "Whisper": "af_bella", # soft, whisper-like | |
| } | |
| voice = voice_map.get(voice_source, "af_heart") | |
| # Kokoro doesn't take very long text at once; chunk it | |
| sentences = [s.strip() + "." for s in script.split(".") if s.strip()] | |
| chunks = [] | |
| sample_rate = 24000 | |
| for i, sentence in enumerate(sentences): | |
| progress(0.3 + 0.6 * (i / max(len(sentences), 1)), | |
| desc=f"Synthesizing … ({i+1}/{len(sentences)})") | |
| samples, sr = kokoro.create( | |
| text=sentence, | |
| voice=voice, | |
| speed=0.9, # slightly slower = more soothing | |
| lang="en-us", | |
| ) | |
| chunks.append(samples) | |
| audio_data = np.concatenate(chunks) if chunks else np.array([]) | |
| sample_rate = sr | |
| else: | |
| # Cloned Voice selected but no clone available — fallback to Soothing Female | |
| progress(0.3, desc="No cloned voice available, using Soothing Female fallback …") | |
| return generate_calming_audio( | |
| animal, situation, duration_minutes, | |
| "Soothing Female", custom_message, pet_name, progress, | |
| ) | |
| progress(0.95, desc="Saving audio …") | |
| output_path = Path(tempfile.gettempdir()) / f"crittercalm_{animal}_{situation.replace(' ', '_')}.wav" | |
| sf.write(str(output_path), audio_data, sample_rate) | |
| progress(1.0, desc="Done!") | |
| info = ( | |
| f"🎧 **Your calming audio is ready!**\n\n" | |
| f"🐾 Animal: **{animal}**\n" | |
| f"🎯 For: **{situation}**\n" | |
| f"⏱ Duration: ~{duration_minutes} min\n" | |
| f"🎤 Voice: **{voice_source}**\n\n" | |
| f"💡 **Tip:** Play this for your pet when you notice signs of stress. " | |
| f"Consistency helps — try using it daily." | |
| ) | |
| return str(output_path), info | |
| except Exception as exc: | |
| log.error(f"Audio generation failed: {exc}") | |
| return None, ( | |
| f"❌ Audio generation failed: {exc}\n\n" | |
| f"**Script was generated** — here it is:\n\n---\n{script}\n---\n\n" | |
| f"You can read this aloud to your pet while we troubleshoot." | |
| ) | |
| # --------------------------------------------------------------------------- | |
| # Model status checker | |
| # --------------------------------------------------------------------------- | |
| def get_model_status() -> str: | |
| """Return a markdown summary of which models are available + cooldown.""" | |
| # Get the current cooldown snapshot from the script generator | |
| try: | |
| from content.script_generator import cooldown_snapshot | |
| snap = cooldown_snapshot() | |
| cooldown_line = ( | |
| f"Inference model: `{snap['model']}` · " | |
| f"cooldown: {snap['cooldown']['active']} · " | |
| f"window: {snap['cooldown']['window_seconds']}s" | |
| ) | |
| except Exception as e: | |
| cooldown_line = f"cooldown status unavailable: {e}" | |
| lines = [f"> {cooldown_line}\n", "| Model | Status | Purpose |", "|-------|--------|---------|"] | |
| omni = get_omnivoice() | |
| lines.append( | |
| f"| OmniVoice (0.6B) | {'✅ Loaded' if omni else '⚠️ Not loaded'} | Voice cloning & TTS |" | |
| ) | |
| dolphin = get_dolphin_llm() | |
| lines.append( | |
| f"| Dolphin-X1-8B | {'✅ Loaded' if dolphin else '⚠️ Not loaded'} | Calming script generation |" | |
| ) | |
| kokoro = get_kokoro() | |
| lines.append( | |
| f"| Kokoro TTS (82M) | {'✅ Loaded' if kokoro else '⚠️ Not loaded'} | Built-in soothing voices |" | |
| ) | |
| cloned = "✅ Ready" if _cloned_voice_embedding else "— Not yet cloned" | |
| lines.append(f"| Voice Clone | {cloned} | Your personal voice |") | |
| return "\n".join(lines) | |
| # --------------------------------------------------------------------------- | |
| # Gradio App | |
| # --------------------------------------------------------------------------- | |
| CUSTOM_CSS = """ | |
| /* ========================================================================= | |
| CritterCalm — Anishinaabe Solarpunk Theme | |
| ---------------------------------------------------------------------------- | |
| Your pet's voice from the meadow. Sun, water, moss, and the small winds. | |
| ========================================================================= */ | |
| @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&family=Inter:wght@400;500;600&family=EB+Garamond:ital,wght@0,400;0,600;1,400&display=swap'); | |
| :root { | |
| /* Anishinaabe-Solarpunk palette */ | |
| --asp-sky: #5BA4D9; | |
| --asp-water: #1B4965; | |
| --asp-ice: #BEE9E8; | |
| --asp-frost: #CAF0F8; | |
| --asp-sun: #F2A93B; | |
| --asp-sunlight: #FFB347; | |
| --asp-ember: #E76F51; | |
| --asp-birch: #F5F1E8; | |
| --asp-terra: #C8553D; | |
| --asp-earth: #8B3A1F; | |
| --asp-moss: #588157; | |
| --asp-forest: #3D6A4A; | |
| --asp-spruce: #1B4332; | |
| --asp-night: #0F1A2C; | |
| --asp-ash: #3A2A2A; | |
| --asp-stone: #A89F91; | |
| /* Legacy aliases for downstream code */ | |
| --primary: var(--asp-moss); | |
| --primary-light: var(--asp-sunlight); | |
| --bg: var(--asp-night); | |
| --card: #142a3a; | |
| --text: var(--asp-birch); | |
| --accent: var(--asp-sun); | |
| --border: rgba(91, 164, 217, 0.3); | |
| --warm: var(--asp-sunlight); | |
| } | |
| body, .gradio-container { | |
| background: | |
| radial-gradient(ellipse at top, rgba(91, 164, 217, 0.18) 0%, transparent 60%), | |
| radial-gradient(ellipse at bottom, rgba(27, 73, 50, 0.25) 0%, transparent 70%), | |
| var(--asp-night) !important; | |
| color: var(--text) !important; | |
| font-family: 'Inter', system-ui, sans-serif !important; | |
| max-width: 100% !important; | |
| background-attachment: fixed; | |
| } | |
| h1, h2, h3, h4 { | |
| font-family: 'EB Garamond', 'Iowan Old Style', Georgia, serif !important; | |
| color: var(--accent) !important; | |
| font-weight: 700 !important; | |
| text-shadow: 0 0 20px rgba(242, 169, 59, 0.18); | |
| } | |
| .tab-nav button { | |
| background: var(--card) !important; | |
| border: 1px solid var(--border) !important; | |
| color: var(--text) !important; | |
| font-size: 1.1em !important; | |
| padding: 12px 24px !important; | |
| transition: all 0.2s !important; | |
| } | |
| .tab-nav button:hover { | |
| border-color: var(--accent) !important; | |
| box-shadow: 0 0 16px rgba(242, 169, 59, 0.25); | |
| } | |
| .tab-nav button.selected { | |
| background: linear-gradient(95deg, var(--asp-sun) 0%, var(--asp-sunlight) 100%) !important; | |
| color: var(--asp-night) !important; | |
| border-color: var(--accent) !important; | |
| box-shadow: 0 0 18px rgba(242, 169, 59, 0.35); | |
| } | |
| button.primary { | |
| background: linear-gradient(95deg, var(--asp-sun) 0%, var(--asp-sunlight) 100%) !important; | |
| border: none !important; | |
| color: var(--asp-night) !important; | |
| font-weight: 600 !important; | |
| padding: 12px 28px !important; | |
| border-radius: 8px !important; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| transition: all 0.2s !important; | |
| } | |
| button.primary:hover { | |
| background: linear-gradient(95deg, var(--asp-sunlight) 0%, var(--asp-birch) 100%) !important; | |
| box-shadow: 0 0 20px rgba(242, 169, 59, 0.5); | |
| transform: translateY(-1px); | |
| } | |
| .animal-emoji { | |
| font-size: 2em; | |
| filter: drop-shadow(0 0 6px rgba(242, 169, 59, 0.4)); | |
| } | |
| .model-status { | |
| background: var(--card) !important; | |
| border: 1px solid var(--border) !important; | |
| border-radius: 8px !important; | |
| padding: 16px !important; | |
| color: var(--text) !important; | |
| } | |
| footer { display: none !important; } | |
| /* === ASP Banner ====================================================== */ | |
| .asp-banner { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 0.6em; | |
| padding: 14px 18px; | |
| margin-bottom: 18px; | |
| background: linear-gradient(95deg, var(--asp-sky) 0%, var(--asp-water) 100%); | |
| color: var(--asp-birch); | |
| border-bottom: 1px solid rgba(255, 179, 71, 0.35); | |
| border-radius: 10px; | |
| font-family: 'EB Garamond', Georgia, serif; | |
| letter-spacing: 0.5px; | |
| text-shadow: 0 1px 2px rgba(15, 26, 44, 0.45); | |
| box-shadow: 0 4px 24px rgba(0, 0, 0, 0.35); | |
| } | |
| .asp-banner .syll { font-size: 1.5em; opacity: 0.9; } | |
| .asp-banner .title { font-size: 1.1em; font-weight: 600; } | |
| .asp-banner .glyph { color: var(--asp-sunlight); } | |
| .asp-banner .subtitle { | |
| color: var(--asp-frost); | |
| font-size: 0.9em; | |
| font-style: italic; | |
| opacity: 0.92; | |
| } | |
| """ | |
| def create_app() -> gr.Blocks: | |
| """Build the CritterCalm Gradio application.""" | |
| with gr.Blocks( | |
| title="CritterCalm — Maanamewin / Voice Comfort for Pets", | |
| ) as app: | |
| # Anishinaabe-Solarpunk banner | |
| gr.HTML(""" | |
| <div class="asp-banner"> | |
| <span class="syll">ᐴ</span> | |
| <span class="glyph">☼</span> | |
| <span class="title">CRITTERCALM</span> | |
| <span class="glyph">❀</span> | |
| <span class="subtitle">— Maanamewin: voice-comfort for the four-leggeds —</span> | |
| <span class="syll">ᔔ</span> | |
| </div> | |
| <h3 style="text-align:center; font-style:italic; color: var(--text); margin-top:0; margin-bottom:20px; opacity: 0.85;"> | |
| Clone your voice. Calm your companion. Even across distance. | |
| </h3> | |
| <p style="text-align:center; max-width:680px; margin: 0 auto 24px; color: var(--text); line-height:1.6;"> | |
| Animals respond to their person's voice — it's their ultimate comfort signal. | |
| <strong>CritterCalm</strong> clones your voice locally so your pet can hear you even when you're away. | |
| </p> | |
| """) | |
| # Model status accordion | |
| with gr.Accordion("🔧 Model Status", open=False): | |
| model_status = gr.Markdown(get_model_status, every=30) | |
| with gr.Tabs() as tabs: | |
| # ============================================================ | |
| # TAB 1 — Clone Your Voice | |
| # ============================================================ | |
| with gr.Tab("🎤 Clone Your Voice", id="clone"): | |
| gr.Markdown( | |
| """ | |
| ### Record a 10-second voice sample | |
| **Tips for best results:** | |
| - 🎙️ Speak clearly in a quiet room | |
| - 📝 Say exactly what you type in the transcript box | |
| - 🕐 Aim for 5–15 seconds of audio | |
| - 😌 Use your natural, gentle voice — the one your pet knows | |
| """ | |
| ) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| voice_input = gr.Audio( | |
| label="Record your voice", | |
| sources=["microphone", "upload"], | |
| type="filepath", | |
| ) | |
| ref_text = gr.Textbox( | |
| label="What did you say? (transcript)", | |
| placeholder="e.g., \"Hello my sweet pup, it's me. You're safe and loved.\"", | |
| lines=3, | |
| ) | |
| clone_btn = gr.Button( | |
| "🔮 Clone My Voice", | |
| variant="primary", | |
| size="lg", | |
| ) | |
| with gr.Column(scale=1): | |
| clone_status = gr.Markdown( | |
| "👆 Record a voice sample and enter the transcript, " | |
| "then click **Clone My Voice**." | |
| ) | |
| clone_preview = gr.Audio( | |
| label="Voice preview", | |
| type="filepath", | |
| interactive=False, | |
| ) | |
| clone_btn.click( | |
| fn=clone_voice, | |
| inputs=[voice_input, ref_text], | |
| outputs=[clone_status, clone_preview], | |
| ) | |
| # ============================================================ | |
| # TAB 2 — Calm Your Pet | |
| # ============================================================ | |
| with gr.Tab("🎧 Calm Your Pet", id="calm"): | |
| gr.Markdown( | |
| """ | |
| ### Generate a calming audio session for your pet | |
| Choose your pet type, what's stressing them, and we'll create | |
| a personalized soothing audio track in your voice. | |
| """ | |
| ) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| animal_type = gr.Dropdown( | |
| label="🐾 Animal Type", | |
| choices=[ | |
| "Dog", "Cat", "Chicken", "Bird", | |
| "Rabbit", "Horse", | |
| ], | |
| value="Dog", | |
| ) | |
| situation = gr.Dropdown( | |
| label="🎯 Situation", | |
| choices=[ | |
| "Separation Anxiety", | |
| "Thunderstorm / Fireworks", | |
| "Vet Visit", | |
| "General Calm", | |
| "Bedtime", | |
| "Travel / Car Ride", | |
| "New Environment", | |
| "Loud Noises", | |
| ], | |
| value="Separation Anxiety", | |
| ) | |
| pet_name = gr.Textbox( | |
| label="🐶 Pet's Name (optional)", | |
| placeholder="e.g., Buddy, Luna, Clucky …", | |
| ) | |
| with gr.Column(scale=1): | |
| voice_source = gr.Dropdown( | |
| label="🎤 Voice Source", | |
| choices=[ | |
| "Cloned Voice", | |
| "Soothing Female", | |
| "Soothing Male", | |
| "Whisper", | |
| ], | |
| value="Cloned Voice", | |
| ) | |
| duration = gr.Slider( | |
| label="⏱ Session Length (minutes)", | |
| minimum=1, | |
| maximum=30, | |
| value=5, | |
| step=1, | |
| ) | |
| custom_msg = gr.Textbox( | |
| label="💬 Custom Message (optional)", | |
| placeholder="Add a personal message to include …", | |
| lines=2, | |
| ) | |
| generate_btn = gr.Button( | |
| "✨ Generate Calming Audio", | |
| variant="primary", | |
| size="lg", | |
| ) | |
| with gr.Row(): | |
| audio_output = gr.Audio( | |
| label="🎧 Your Calming Audio", | |
| type="filepath", | |
| interactive=False, | |
| ) | |
| output_info = gr.Markdown( | |
| "👆 Configure your settings above and click **Generate**." | |
| ) | |
| generate_btn.click( | |
| fn=generate_calming_audio, | |
| inputs=[ | |
| animal_type, situation, duration, | |
| voice_source, custom_msg, pet_name, | |
| ], | |
| outputs=[audio_output, output_info], | |
| ) | |
| # ============================================================ | |
| # TAB 3 — About & Science | |
| # ============================================================ | |
| with gr.Tab("📚 About & Science", id="about"): | |
| gr.Markdown( | |
| """ | |
| ## 🧠 The Science of Voice & Animal Bonding | |
| ### Why animals respond to their owner's voice | |
| Research in animal cognition has shown that: | |
| - **Dogs** process their owner's voice in the same brain regions humans | |
| use for voice recognition. Your voice literally lights up their brain | |
| like a familiar face lights up yours. (Andics et al., 2014) | |
| - **Cats** show measurable stress reduction (lower cortisol) when they | |
| hear their owner's voice, especially in unfamiliar environments. | |
| (Saito & Shinozuka, 2013) | |
| - **Chickens** and other birds recognize individual human voices and | |
| show reduced alarm calls when hearing familiar, calm speech patterns. | |
| - **Horses** can distinguish between familiar and unfamiliar human voices | |
| and show lower heart rates with familiar voices during stress. | |
| ### How CritterCalm helps | |
| 1. **Voice Cloning** — We use OmniVoice (0.6B params, 646 languages), | |
| a state-of-the-art open-source voice cloning model that captures your | |
| vocal timbre, rhythm, and emotional tone from just 3–10 seconds of audio. | |
| 2. **Calming Scripts** — Dolphin-X1-8B generates species-appropriate | |
| soothing messages using evidence-based techniques: | |
| - Simple, rhythmic language | |
| - Familiar routines and references | |
| - Gradual calming progression | |
| 3. **Voice Synthesis** — Your cloned voice speaks the calming script, | |
| creating the experience of you being there, even when you're away. | |
| ### Best practices | |
| - 🕐 **Consistency matters** — use the same audio at the same times daily | |
| - 🔊 **Keep volume moderate** — your pet's hearing is more sensitive than yours | |
| - 📱 **Pair with positive experiences** — play during treats or petting at first | |
| - 🏠 **Start when you're home** — let them associate the sound with your presence | |
| - ⏰ **Session length** — start short (2–3 min) and gradually extend | |
| ### Privacy & Local-First | |
| CritterCalm runs **100% locally** on your device. Your voice recording, | |
| cloned voice, and generated audio never leave your computer. No cloud APIs, | |
| no data collection, no tracking. 🔌 **Off the Grid**. | |
| ### Models used | |
| - **OmniVoice** (0.6B) — Xiaomi AI Lab, Apache 2.0 | |
| - **Dolphin-X1-8B** (8B) — Llama 3.1 derivative, community license | |
| - **Kokoro TTS** (82M) — Apache 2.0 | |
| --- | |
| *Built with ❤️ for the Build Small Hackathon 2026* | |
| """ | |
| ) | |
| return app | |
| # --------------------------------------------------------------------------- | |
| # Entry point | |
| # --------------------------------------------------------------------------- | |
| if __name__ == "__main__": | |
| app = create_app() | |
| app.launch( | |
| server_name="0.0.0.0", | |
| server_port=int(os.environ.get("PORT", "7860")), | |
| share=False, | |
| show_error=True, | |
| mcp_server=True, | |
| css=CUSTOM_CSS, | |
| ) | |