WOOF / app.py
Wayfinder6's picture
Upload folder using huggingface_hub
8d34f0c verified
Raw
History Blame Contribute Delete
20.1 kB
"""
WOOF — What's Our Output, Fido?
Built for the Build Small Hackathon 2026.
Upload a photo of what your dog left you this morning.
Find out what they ate, whether to worry, and when to call the vet.
Every dog owner has stood over a mystery pile at 6am thinking
"what the FUCK did you eat." This app answers that question.
Also tracks gut health over time because your dog can't tell you
their stomach hurts but their output can.
"""
import gradio as gr
import os
import io
import base64
from openai import OpenAI
# --- NVIDIA Nemotron Nano Omni (sponsor model) ---
NVIDIA_API_KEY = os.environ.get("NVIDIA_API_KEY", "")
NEMOTRON_MODEL = "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning"
nvidia_client = None
if NVIDIA_API_KEY:
nvidia_client = OpenAI(
base_url="https://integrate.api.nvidia.com/v1",
api_key=NVIDIA_API_KEY,
)
print("NVIDIA Nemotron connected.")
else:
print("No NVIDIA_API_KEY — running in fallback mode.")
# --- Dog Health Knowledge Base ---
TOXIC_FOODS = {
"chocolate": {"severity": "HIGH", "note": "Theobromine toxicity. Dark chocolate is worse. Call vet if large amount."},
"grapes": {"severity": "HIGH", "note": "Can cause kidney failure even in small amounts. Call vet immediately."},
"raisins": {"severity": "HIGH", "note": "Same as grapes. Kidney failure risk. Call vet."},
"xylitol": {"severity": "HIGH", "note": "Found in sugar-free gum/candy. Causes liver failure. Emergency vet NOW."},
"onion": {"severity": "MEDIUM", "note": "Damages red blood cells. Toxic in quantity."},
"garlic": {"severity": "MEDIUM", "note": "Related to onion toxicity. Small amounts less dangerous than onion."},
"avocado": {"severity": "LOW", "note": "Persin in skin/pit can cause vomiting. Flesh is mostly fine."},
"macadamia": {"severity": "MEDIUM", "note": "Causes weakness, vomiting, tremors. Usually resolves in 48h."},
"alcohol": {"severity": "HIGH", "note": "Even small amounts dangerous. Causes vomiting, breathing problems."},
"coffee": {"severity": "MEDIUM", "note": "Caffeine toxicity similar to chocolate."},
"cooked bones": {"severity": "HIGH", "note": "Can splinter and puncture intestines. Watch for bloody stool."},
"corn cob": {"severity": "HIGH", "note": "Cannot be digested. Common cause of intestinal blockage. Vet if suspected."},
"sock": {"severity": "HIGH", "note": "Fabric doesn't digest. Intestinal blockage risk. Vet if not passed in 24h."},
"toy": {"severity": "HIGH", "note": "Foreign body obstruction. Watch for vomiting, lethargy, no appetite."},
"grass": {"severity": "LOW", "note": "Normal. Dogs eat grass to settle their stomach or because they like it."},
"cat poop": {"severity": "LOW", "note": "Gross but usually harmless. Dogs do this. It's disgusting. They don't care."},
"dirt": {"severity": "LOW", "note": "Usually fine. Can indicate mineral deficiency if persistent."},
"plastic": {"severity": "MEDIUM", "note": "Small pieces may pass. Large pieces can obstruct. Monitor closely."},
}
STOOL_GUIDE = {
"normal": {"color": "chocolate brown", "icon": "🟢", "note": "Healthy. Good job, dog."},
"black": {"color": "black/tarry", "icon": "🔴", "note": "Could indicate upper GI bleeding. Vet today."},
"red": {"color": "red/bloody", "icon": "🔴", "note": "Lower GI bleeding or colitis. Vet today if more than streaks."},
"yellow": {"color": "yellow/greasy", "icon": "🟡", "note": "Possible liver, gallbladder, or pancreas issue. Vet if persistent."},
"orange": {"color": "orange", "icon": "🟡", "note": "Bile or food-related. Monitor. Vet if continues 2+ days."},
"green": {"color": "green", "icon": "🟡", "note": "Ate grass, or gallbladder issue. Usually fine if one-time."},
"white": {"color": "white/chalky", "icon": "🟡", "note": "Too much calcium/bone. Adjust diet. Vet if can't poop."},
"grey": {"color": "grey", "icon": "🔴", "note": "Possible pancreas or biliary issue. Vet visit recommended."},
"mucus": {"color": "mucus-covered", "icon": "🟡", "note": "Some mucus is normal. Excessive = inflammation. Vet if bloody."},
}
VET_NOW = [
"bloody diarrhea lasting more than a few hours",
"vomiting AND diarrhea together",
"lethargy or won't get up",
"bloated/distended abdomen",
"known ingestion of xylitol, grapes, or large amount of chocolate",
"foreign object suspected (sock, toy, corn cob)",
"hasn't eaten in 24+ hours",
"straining to poop with nothing coming out",
]
SYSTEM_PROMPT = """You are WOOF — a dog gut health analyzer. A dog owner is describing what they found in their dog's output (poop or vomit) or uploading a photo of it.
Your job:
1. Figure out what the dog likely ate or what's going on
2. Assess whether it's concerning
3. Tell them what to do in plain language
Use the Bristol Stool Scale adapted for dogs:
- Type 1-2: Hard pellets = constipation, needs more water/fiber
- Type 3-4: Firm log, easy to pick up = ideal, healthy
- Type 5: Soft blob, loses shape = diet change or mild issue
- Type 6: Mushy, no shape = concerning if persistent
- Type 7: Liquid = diarrhea, monitor closely
Be direct. Be practical. Use humor LIGHTLY — these people are worried about their dog but also standing over poop at 6am. Don't be clinical. Be the friend who happens to know a lot about dog health.
Return your analysis in this format:
WHAT YOU'RE LOOKING AT: [brief assessment]
LIKELY CAUSE: [what probably happened]
CONCERN LEVEL: [🟢 LOW / 🟡 MONITOR / 🔴 VET NOW]
WHAT TO DO: [specific next steps]
WATCH FOR: [warning signs that mean escalate]
FUN FACT: [one actually interesting dog gut fact to lighten the mood]"""
def analyze_output(description, output_type, dog_size, photo):
"""Analyze dog output from description and/or photo."""
if not description and not photo:
return """<div class="wf-empty">Describe what you found or upload a photo. No judgment here.</div>"""
# Check for known toxic items in description
alerts = []
if description:
lower = description.lower()
for item, info in TOXIC_FOODS.items():
if item in lower:
alerts.append(f"⚠️ {item.upper()}: {info['note']} (Severity: {info['severity']})")
# Check for color keywords
color_match = None
if description:
lower = description.lower()
for key, info in STOOL_GUIDE.items():
if key in lower:
color_match = info
# Build the prompt
context_parts = []
if output_type: context_parts.append(f"Type: {output_type}")
if dog_size: context_parts.append(f"Dog size: {dog_size}")
context = " | ".join(context_parts)
user_text = f"My dog's {output_type or 'poop'}: {description or 'see photo'}"
if context: user_text += f"\n({context})"
# Try Nemotron
if nvidia_client:
try:
content = [{"type": "text", "text": user_text}]
if photo:
buffered = io.BytesIO()
photo.save(buffered, format="JPEG", quality=85)
b64 = base64.b64encode(buffered.getvalue()).decode()
content.append({
"type": "image_url",
"image_url": {"url": f"data:image/jpeg;base64,{b64}"}
})
response = nvidia_client.chat.completions.create(
model=NEMOTRON_MODEL,
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": content}
],
max_tokens=600,
temperature=0.3,
)
raw = response.choices[0].message.content.strip()
return format_analysis(raw, alerts, color_match)
except Exception as e:
print(f"Nemotron error: {e}")
return format_analysis(fallback_analysis(description, output_type), alerts, color_match)
else:
return format_analysis(fallback_analysis(description, output_type), alerts, color_match)
def fallback_analysis(description, output_type):
"""Rule-based fallback."""
if not description:
return """WHAT YOU'RE LOOKING AT: Can't tell without a description or working AI model.
LIKELY CAUSE: Unknown
CONCERN LEVEL: 🟡 MONITOR
WHAT TO DO: Describe what you see — color, consistency, anything unusual in it.
WATCH FOR: Blood, lethargy, vomiting, or loss of appetite.
FUN FACT: Dogs poop in alignment with Earth's magnetic field. North-south. Scientists confirmed this. Nobody knows why."""
lower = description.lower()
if any(w in lower for w in ["blood", "bloody", "red"]):
return """WHAT YOU'RE LOOKING AT: Blood in the output. This needs attention.
LIKELY CAUSE: Could be colitis, parasites, foreign object, or dietary issue. Bright red = lower GI. Dark/tarry = upper GI (more serious).
CONCERN LEVEL: 🔴 VET NOW if more than small streaks or if it continues.
WHAT TO DO: Note the color (bright red vs dark), amount, and how long it's been happening. Call your vet today.
WATCH FOR: Lethargy, loss of appetite, vomiting, or worsening. These make it urgent.
FUN FACT: A dog's GI tract is about 5x their body length. That's a lot of pipe to troubleshoot."""
elif any(w in lower for w in ["liquid", "watery", "diarrhea"]):
return """WHAT YOU'RE LOOKING AT: Diarrhea. Very common. Usually resolves on its own.
LIKELY CAUSE: Dietary change, ate something questionable, stress, or mild infection.
CONCERN LEVEL: 🟡 MONITOR for 24 hours.
WHAT TO DO: Withhold food for 12 hours (water always available). Then bland diet — boiled chicken and rice, small portions. Pumpkin puree (plain, not pie filling) helps firm things up.
WATCH FOR: Blood, vomiting, lethargy, or diarrhea lasting more than 48 hours. Then it's vet time.
FUN FACT: Dogs have about 1,700 bacterial species in their gut. Yours has about 1,000. Their microbiome is more diverse than yours."""
elif any(w in lower for w in ["grass", "green"]):
return """WHAT YOU'RE LOOKING AT: Grass in the output. Classic.
LIKELY CAUSE: Your dog ate grass. They do this. Theories: settling an upset stomach, boredom, they just like it, or instinct from wild ancestors.
CONCERN LEVEL: 🟢 LOW. This is normal dog behavior.
WHAT TO DO: Nothing unless it's happening constantly. Frequent grass eating CAN indicate nausea or dietary deficiency.
WATCH FOR: Vomiting after eating grass (sometimes that's the point — they're self-medicating), or doing it obsessively.
FUN FACT: About 80% of dogs eat grass regularly. Only about 25% vomit after. The rest are just vibing."""
else:
return """WHAT YOU'RE LOOKING AT: Need more detail for a specific read.
LIKELY CAUSE: Hard to say without more info.
CONCERN LEVEL: 🟡 MONITOR
WHAT TO DO: Note the color, consistency (hard pellets? soft? liquid?), any foreign material visible, and frequency. The more detail, the better the read.
WATCH FOR: Blood, mucus, foreign objects, very dark/tarry color, or any change in your dog's behavior.
FUN FACT: The average dog poops 1-5 times per day. If your dog suddenly changes frequency, that tells you something even if the poop looks normal."""
def format_analysis(text, alerts, color_match):
"""Format into the WOOF output."""
alert_html = ""
if alerts:
alert_items = "".join(f'<div class="wf-alert-item">{a}</div>' for a in alerts)
alert_html = f'<div class="wf-alerts">{alert_items}</div>'
color_html = ""
if color_match:
color_html = f'<div class="wf-color">{color_match["icon"]} Color match: {color_match["color"]}{color_match["note"]}</div>'
# Parse sections from response
sections = {}
current = None
for line in text.split('\n'):
upper = line.strip().upper()
if "LOOKING AT" in upper: current = "looking"; continue
elif "LIKELY CAUSE" in upper: current = "cause"; continue
elif "CONCERN LEVEL" in upper: current = "concern"; continue
elif "WHAT TO DO" in upper: current = "do"; continue
elif "WATCH FOR" in upper: current = "watch"; continue
elif "FUN FACT" in upper: current = "fun"; continue
if current and line.strip():
sections[current] = sections.get(current, "") + line.strip() + " "
concern = sections.get("concern", "🟡 MONITOR").strip()
concern_class = "wf-red" if "🔴" in concern or "VET" in concern.upper() else "wf-yellow" if "🟡" in concern else "wf-green"
html = f"""
<div class="wf-container">
{alert_html}
{color_html}
<div class="wf-concern {concern_class}">
{concern}
</div>
<div class="wf-section">
<div class="wf-icon">🔍</div>
<div class="wf-content">
<div class="wf-title">What you're looking at</div>
<div class="wf-body">{sections.get('looking', 'Analyzing...')}</div>
</div>
</div>
<div class="wf-section">
<div class="wf-icon">🤔</div>
<div class="wf-content">
<div class="wf-title">Likely cause</div>
<div class="wf-body">{sections.get('cause', 'Need more info.')}</div>
</div>
</div>
<div class="wf-section">
<div class="wf-icon">📋</div>
<div class="wf-content">
<div class="wf-title">What to do</div>
<div class="wf-body">{sections.get('do', 'Monitor and note any changes.')}</div>
</div>
</div>
<div class="wf-section">
<div class="wf-icon">👀</div>
<div class="wf-content">
<div class="wf-title">Watch for</div>
<div class="wf-body">{sections.get('watch', 'Blood, lethargy, loss of appetite.')}</div>
</div>
</div>
<div class="wf-section wf-fun">
<div class="wf-icon">🐕</div>
<div class="wf-content">
<div class="wf-body">{sections.get('fun', 'Dogs can smell about 10,000 times better than humans. Yes, even THAT.')}</div>
</div>
</div>
<div class="wf-vet-box">
<div class="wf-vet-title">Call the vet immediately if:</div>
<ul class="wf-vet-list">
<li>Bloody diarrhea lasting more than a few hours</li>
<li>Vomiting AND diarrhea together</li>
<li>Bloated or distended abdomen</li>
<li>Lethargy or won't get up</li>
<li>Known ingestion of xylitol, grapes, or chocolate</li>
<li>Straining with nothing coming out</li>
</ul>
</div>
</div>
"""
return html
# --- CSS ---
CUSTOM_CSS = """
@import url('https://fonts.googleapis.com/css2?family=Quicksand:wght@400;600;700&display=swap');
.wf-empty {
text-align: center; padding: 40px; color: #888;
font-family: 'Quicksand', sans-serif; font-size: 1.1em;
}
.wf-container {
font-family: 'Quicksand', sans-serif;
background: #FAFAF7; border-radius: 16px; padding: 24px;
max-width: 700px; margin: 0 auto;
}
.wf-alerts {
background: #FFF3F0; border: 1px solid #E07A5F; border-radius: 10px;
padding: 14px; margin-bottom: 16px;
}
.wf-alert-item { color: #C0392B; font-weight: 600; padding: 4px 0; font-size: 0.95em; }
.wf-color {
background: #F5F5F0; border-radius: 8px; padding: 10px 14px;
margin-bottom: 16px; font-size: 0.9em; color: #666;
}
.wf-concern {
text-align: center; font-size: 1.3em; font-weight: 700;
padding: 16px; border-radius: 12px; margin-bottom: 20px;
}
.wf-green { background: #E8F5E9; color: #2E7D32; }
.wf-yellow { background: #FFF8E1; color: #F57F17; }
.wf-red { background: #FFEBEE; color: #C62828; }
.wf-section {
display: flex; gap: 14px; padding: 14px 0;
border-bottom: 1px solid #EEECE8; align-items: flex-start;
}
.wf-section:last-of-type { border-bottom: none; }
.wf-icon { font-size: 1.4em; min-width: 32px; text-align: center; }
.wf-title {
font-size: 0.75em; font-weight: 700; color: #999;
text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 4px;
}
.wf-body { font-size: 0.95em; line-height: 1.7; color: #333; }
.wf-fun { border-top: 1px dashed #DDD; margin-top: 8px; padding-top: 12px; }
.wf-fun .wf-body { color: #888; font-style: italic; font-size: 0.85em; }
.wf-vet-box {
background: #FFF3F0; border-radius: 10px; padding: 16px;
margin-top: 20px; border-left: 4px solid #E07A5F;
}
.wf-vet-title { font-weight: 700; color: #C0392B; font-size: 0.85em; margin-bottom: 8px; }
.wf-vet-list { font-size: 0.85em; color: #666; padding-left: 20px; line-height: 1.8; }
.gradio-container {
background: #F5F4F0 !important; max-width: 800px !important; margin: 0 auto !important;
}
.gr-button-primary {
background: #8B6914 !important; border: none !important;
font-family: 'Quicksand', sans-serif !important; font-size: 1.05em !important;
padding: 12px 36px !important; border-radius: 12px !important;
}
.gr-button-primary:hover { background: #7A5B10 !important; }
footer { display: none !important; }
"""
# --- Examples ---
EXAMPLES = [
["Found something bright green and mushy this morning. She was in the yard a lot yesterday.", "Poop", "Medium (20-50 lbs)"],
["He threw up what looks like a piece of a sock. Only half of it.", "Vomit", "Large (50+ lbs)"],
["Liquid brown diarrhea three times since last night. She ate some table scraps.", "Poop", "Small (under 20 lbs)"],
["Normal looking but there's a weird white thing in it that moves. Like a grain of rice.", "Poop", "Medium (20-50 lbs)"],
["She got into the Halloween candy. Definitely ate some chocolate. Maybe 3-4 pieces.", "Vomit", "Small (under 20 lbs)"],
["Black and tarry. Smells worse than usual which is saying something.", "Poop", "Large (50+ lbs)"],
]
# --- App ---
with gr.Blocks(css=CUSTOM_CSS, title="WOOF", theme=gr.themes.Soft()) as app:
gr.HTML("""
<div style="text-align:center; padding: 20px 0 8px;">
<h1 style="font-family: 'Quicksand', sans-serif; color: #333; font-size: 2.4em; font-weight: 700;">
🐕 WOOF
</h1>
<p style="font-family: 'Quicksand', sans-serif; color: #999; font-size: 1.0em;">
What's Our Output, Fido?
</p>
<p style="font-family: 'Quicksand', sans-serif; color: #BBB; font-size: 0.8em; max-width: 450px; margin: 4px auto 0;">
Every dog owner has stood over a mystery pile at 6am. This app answers the question you're too embarrassed to Google.
</p>
</div>
""")
description = gr.Textbox(
label="What did you find?",
placeholder="Describe it. Color, consistency, anything weird in it. We've seen worse, promise.",
lines=3,
)
with gr.Row():
output_type = gr.Radio(
choices=["Poop", "Vomit", "Both (oh no)"],
value="Poop",
label="Type",
)
dog_size = gr.Radio(
choices=["Small (under 20 lbs)", "Medium (20-50 lbs)", "Large (50+ lbs)"],
value="Medium (20-50 lbs)",
label="Dog size",
)
photo = gr.Image(label="Upload a photo (optional — yes really)", type="pil")
btn = gr.Button("Analyze the Situation", variant="primary", size="lg")
output = gr.HTML()
btn.click(
fn=analyze_output,
inputs=[description, output_type, dog_size, photo],
outputs=[output],
)
gr.Examples(
examples=EXAMPLES,
inputs=[description, output_type, dog_size],
label="Common mysteries",
)
gr.HTML("""
<div style="text-align:center; padding: 16px 0 4px; color: #CCC; font-size: 0.7em; font-family: 'Quicksand', sans-serif; line-height: 1.8;">
Not a substitute for veterinary care. When in doubt, call your vet.<br>
Powered by NVIDIA Nemotron Nano Omni<br>
<span style="color:#8B6914;">Heuremen — Build Small Hackathon 2026</span>
</div>
""")
if __name__ == "__main__":
app.launch()