Spaces:
Sleeping
Sleeping
Download app.py from sofiamak/PawfectMatch: direct link, hf CLI and curl.
- Browser
- Download file 9.52 kB
-
https://huggingface.co/spaces/sofiamak/PawfectMatch/resolve/main/app.py
- Command line
-
hf download hf://spaces/sofiamak/PawfectMatch/app.py
-
curl -L -o app.py https://huggingface.co/spaces/sofiamak/PawfectMatch/resolve/main/app.py
9.52 kB
| import os, html, random, re | |
| import gradio as gr | |
| from openai import OpenAI | |
| # ===== 基础配置 ===== | |
| API_KEY = os.getenv("OPENAI_API_KEY") | |
| MODEL_NAME = "gpt-4o-mini" | |
| client = OpenAI(api_key=API_KEY) | |
| # ===== 系统提示:短、好念、可自创、真实名言 ===== | |
| SYSTEM_PROMPT = """You are a playful, high-creativity pet name generator. | |
| Goal: Produce EXACTLY ONE short, cute, kid-pronounceable pet name + one authentic quote line. | |
| NAME rules (STRICT): | |
| - The name must be VERY short: | |
| • Chinese/Japanese/Korean: 1–2 characters/syllables (e.g., “豆豆”, “Momo”, “Bori”). | |
| • English/French: 1–2 syllables only (e.g., “Pip”, “Lulu”, “Nori”, “Chou”). | |
| - You MAY invent new words if they sound natural and easy. | |
| - NO numbers, NO hyphens, NO long phrases, NO public-figure names. | |
| - Output only ONE name. | |
| - Add a micro-meaning (≤5 words) after the name. | |
| QUOTE rules (must be real): | |
| - After the name line, provide ONE authentic, warm animal-related quote with attribution. | |
| - Keep the quote short (≤20 words). Attribution required (e.g., — George Eliot). | |
| - Translate the quote text into the requested language if needed; attribution kept. | |
| STRICT OUTPUT FORMAT (2 lines only): | |
| Name — short meaning | |
| Quote: “...” — Attribution | |
| """ | |
| def build_prompt(species, look, personality, gender, language, inspiration): | |
| return f""" | |
| Requested language: {language} | |
| Inspiration source (primary): {inspiration} | |
| Refinements (secondary only, may be empty): | |
| - Species: {species or '(unspecified)'} | |
| - Appearance: {look or '(unspecified)'} | |
| - Personality: {personality or '(unspecified)'} | |
| - Gender: {gender} | |
| Task: | |
| 1) Create ONE ultra-short, cute, kid-pronounceable pet name (length limits above). | |
| 2) Add a micro meaning (≤5 words). | |
| 3) Output ONE authentic animal-related quote with attribution. | |
| Remember: you may invent new names; do NOT include any digits or hyphens in the name. | |
| """ | |
| # ==== 名言兜底 ==== | |
| FALLBACK_QUOTES = { | |
| "English": [ | |
| ('"Until one has loved an animal, a part of one\'s soul remains unawakened."', "Anatole France"), | |
| ('"An animal\'s eyes have the power to speak a great language."', "Martin Buber"), | |
| ('"Animals are such agreeable friends—they ask no questions; they pass no criticisms."', "George Eliot"), | |
| ], | |
| "中文": [ | |
| ('“一个人若未曾爱过动物,他的灵魂尚未苏醒。”', "Anatole France"), | |
| ('“动物的眼睛自有它们的语言。”', "Martin Buber"), | |
| ('“动物是极好的朋友——它们不发问,也不挑剔。”', "George Eliot"), | |
| ], | |
| "日本語": [ | |
| ('「動物を愛したことのない者の魂は、まだ目覚めていない。」', "Anatole France"), | |
| ('「動物の瞳は偉大な言葉を語る力を持つ。」', "Martin Buber"), | |
| ('「動物は実に快い友である——問いも批判もない。」', "George Eliot"), | |
| ], | |
| "한국어": [ | |
| ('“동물을 사랑해 보지 않은 영혼은 아직 깨어나지 않았다.”', "Anatole France"), | |
| ('“동물의 눈에는 위대한 언어가 담겨 있다.”', "Martin Buber"), | |
| ('“동물은 참으로 상냥한 친구다—묻지도 비난하지도 않는다.”', "George Eliot"), | |
| ], | |
| "Français": [ | |
| ("« Jusqu'à ce qu'on ait aimé un animal, une partie de notre âme reste endormie. »", "Anatole France"), | |
| ("« Les yeux d’un animal ont le pouvoir de parler un grand langage. »", "Martin Buber"), | |
| ("« Les animaux sont de si agréables amis — ils ne posent aucune question et ne font aucune critique. »", "George Eliot"), | |
| ], | |
| } | |
| def _fallback_quote(language: str) -> str: | |
| lst = FALLBACK_QUOTES.get(language, FALLBACK_QUOTES["English"]) | |
| text, author = random.choice(lst) | |
| return f'Quote: {text} — {author}' | |
| def _ensure_two_lines_and_strip_digits(content: str, language: str): | |
| if not content: | |
| return None, None | |
| lines = [ln.strip() for ln in content.splitlines() if ln.strip()] | |
| name_line = lines[0] if lines else "" | |
| quote_line = next((ln for ln in lines[1:] if ln.lower().startswith("quote:")), None) | |
| if not quote_line: | |
| quote_line = _fallback_quote(language) | |
| sep = "—" if "—" in name_line else ("-" if "-" in name_line else None) | |
| if sep: | |
| parts = name_line.split(sep, 1) | |
| name_part = re.sub(r"[0-9\\-]", "", parts[0].strip()) | |
| meaning_part = parts[1].strip() if len(parts) > 1 else "" | |
| if not name_part: | |
| name_part = "Momo" if language in ["English", "Français"] else ("豆豆" if language == "中文" else "モモ") | |
| name_line = f"{name_part} — {meaning_part}" if meaning_part else name_part | |
| else: | |
| cleaned = re.sub(r"[0-9\\-]", "", name_line).strip() | |
| if not cleaned: | |
| cleaned = "Momo" if language in ["English", "Français"] else ("豆豆" if language == "中文" else "モモ") | |
| name_line = cleaned | |
| return name_line, quote_line | |
| def generate_modal_html(species, look, personality, gender, language, inspiration): | |
| try: | |
| prompt = build_prompt( | |
| (species or "").strip(), | |
| (look or "").strip(), | |
| (personality or "").strip(), | |
| gender, language, inspiration | |
| ) | |
| resp = client.chat.completions.create( | |
| model=MODEL_NAME, | |
| temperature=0.95, | |
| max_tokens=200, | |
| messages=[ | |
| {"role": "system", "content": SYSTEM_PROMPT}, | |
| {"role": "user", "content": prompt} | |
| ] | |
| ) | |
| raw = (resp.choices[0].message.content or "").strip() | |
| name_line, quote_line = _ensure_two_lines_and_strip_digits(raw, language) | |
| name_html, quote_html = html.escape(name_line), html.escape(quote_line) | |
| return f""" | |
| <div class="pm-overlay" onclick="this.remove()"> | |
| <div class="pm-modal" onclick="event.stopPropagation()"> | |
| <div class="pm-modal-title">Pawfect Match</div> | |
| <div class="pm-modal-body"> | |
| <div class="pm-name">{name_html}</div> | |
| <div class="pm-quote">{quote_html}</div> | |
| </div> | |
| <div class="pm-modal-actions"> | |
| <button class="pm-btn" onclick="document.querySelector('.pm-overlay')?.remove()">关闭</button> | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| except Exception as e: | |
| err = html.escape(str(e)) | |
| return f"<div class='pm-overlay'><div class='pm-modal'><div class='pm-name'>⚠️ 出错</div><div class='pm-quote'>{err}</div></div></div>" | |
| # ===== 样式与界面 ===== | |
| theme = gr.themes.Soft(primary_hue="pink", secondary_hue="cyan", neutral_hue="slate") | |
| CUSTOM_CSS = """ | |
| :root{--pm-bg:#faf7fb;--pm-surface:#fff;--pm-border:#eddcf3;--pm-text:#111;--pm-muted:#6b7280;--pm-accent:#ff4fa3;} | |
| body,.gradio-container{background:var(--pm-bg)!important;} | |
| .pm-topbar{display:flex;align-items:center;justify-content:space-between;padding:12px;background:linear-gradient(90deg,#ffe8f3,#e8fbff);border-radius:12px;border:1px solid var(--pm-border);} | |
| .pm-brand{display:flex;align-items:center;gap:10px;} | |
| .pm-logo{font-size:24px;} | |
| .pm-title{font-weight:800;font-size:20px;color:var(--pm-text);} | |
| .pm-overlay{position:fixed;inset:0;background:rgba(0,0,0,.35);display:flex;align-items:center;justify-content:center;z-index:9999;} | |
| .pm-modal{width:min(560px,92vw);background:var(--pm-surface);border:1px solid var(--pm-border);border-radius:14px;box-shadow:0 20px 60px rgba(0,0,0,.18);} | |
| .pm-modal-title{padding:14px 16px;font-weight:800;border-bottom:1px solid var(--pm-border);} | |
| .pm-modal-body{padding:16px;} | |
| .pm-name{font-weight:800;font-size:22px;margin-bottom:8px;color:#111;} | |
| .pm-quote{font-weight:600;font-size:14px;color:#374151;opacity:.95;} | |
| .pm-modal-actions{padding:12px 16px;display:flex;justify-content:flex-end;border-top:1px solid var(--pm-border);} | |
| .pm-btn{padding:10px 14px;border-radius:10px;border:1px solid var(--pm-border);background:var(--pm-surface);cursor:pointer;font-weight:700;} | |
| .pm-btn:hover{border-color:var(--pm-accent);color:var(--pm-accent);} | |
| """ | |
| with gr.Blocks(title="Pawfect Match", theme=theme, css=CUSTOM_CSS) as demo: | |
| gr.HTML(""" | |
| <div class="pm-topbar"> | |
| <div class="pm-brand"> | |
| <div class="pm-logo">🐾</div> | |
| <div class="pm-title">Pawfect Match</div> | |
| </div> | |
| </div> | |
| """) | |
| with gr.Group(): | |
| species = gr.Textbox(label="宠物种类") | |
| look = gr.Textbox(label="外表特征") | |
| personality = gr.Textbox(label="性格特点") | |
| with gr.Row(): | |
| gender = gr.Radio(["male","female"], value="male", label="宠物性别") | |
| language = gr.Radio(["中文","English","日本語","한국어","Français"], value="English", label="输出语言") | |
| inspiration = gr.Radio(["山海经","迪士尼","哈利波特","生活"], value="生活", label="灵感来源") | |
| with gr.Row(): | |
| btn = gr.Button("✨ 起名", variant="primary") | |
| clear_btn = gr.Button("清空", variant="secondary") | |
| modal = gr.HTML(visible=True, value="") | |
| btn.click(generate_modal_html, | |
| inputs=[species, look, personality, gender, language, inspiration], | |
| outputs=modal) | |
| clear_btn.click(lambda: ("","","","male","English","生活",""), | |
| outputs=[species, look, personality, gender, language, inspiration, modal]) | |
| if __name__ == "__main__": | |
| demo.launch(ssr_mode=False, share=True) |