Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,59 +1,208 @@
|
|
| 1 |
-
import os, html
|
| 2 |
import gradio as gr
|
| 3 |
from openai import OpenAI
|
| 4 |
|
|
|
|
| 5 |
API_KEY = os.getenv("OPENAI_API_KEY")
|
| 6 |
MODEL_NAME = "gpt-4o-mini"
|
| 7 |
client = OpenAI(api_key=API_KEY)
|
| 8 |
|
|
|
|
| 9 |
SYSTEM_PROMPT = """You are a playful, high-creativity pet name generator.
|
| 10 |
Goal: Produce EXACTLY ONE cute, kid-pronounceable pet name + one authentic quote line.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
"""
|
| 12 |
|
| 13 |
def build_prompt(species, look, personality, gender, language, inspiration):
|
| 14 |
return f"""
|
| 15 |
Requested language: {language}
|
| 16 |
-
Primary inspiration
|
| 17 |
-
Secondary refinements:
|
| 18 |
- Species: {species or '(unspecified)'}
|
| 19 |
- Appearance: {look or '(unspecified)'}
|
| 20 |
- Personality: {personality or '(unspecified)'}
|
| 21 |
- Gender: {gender}
|
|
|
|
| 22 |
Task:
|
| 23 |
-
1)
|
| 24 |
-
2)
|
|
|
|
| 25 |
"""
|
| 26 |
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
try:
|
| 29 |
-
prompt = build_prompt(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
resp = client.chat.completions.create(
|
| 31 |
model=MODEL_NAME,
|
| 32 |
-
temperature=0.
|
|
|
|
| 33 |
max_tokens=280,
|
| 34 |
messages=[
|
| 35 |
{"role": "system", "content": SYSTEM_PROMPT},
|
| 36 |
-
{"role": "user",
|
| 37 |
]
|
| 38 |
)
|
| 39 |
raw = (resp.choices[0].message.content or "").strip()
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
except Exception as e:
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
|
|
|
| 44 |
theme = gr.themes.Soft(primary_hue="pink", secondary_hue="cyan", neutral_hue="slate")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
-
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
gr.HTML("""
|
| 49 |
-
<div
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
<div style="font-size:24px;">🐾</div>
|
| 54 |
-
<div style="font-weight:800;font-size:20px;color:#111;">Pawfect Match</div>
|
| 55 |
</div>
|
| 56 |
-
<div style="color:#6b7280;font-size:12px;">v1.
|
| 57 |
</div>
|
| 58 |
""")
|
| 59 |
|
|
@@ -70,28 +219,18 @@ with gr.Blocks(title="Pawfect Match", theme=theme) as demo:
|
|
| 70 |
btn = gr.Button("✨ 起名", variant="primary")
|
| 71 |
clear_btn = gr.Button("清空", variant="secondary")
|
| 72 |
|
| 73 |
-
#
|
| 74 |
-
|
| 75 |
|
| 76 |
btn.click(
|
| 77 |
-
|
| 78 |
inputs=[species, look, personality, gender, language, inspiration],
|
| 79 |
-
outputs=
|
| 80 |
-
js="""
|
| 81 |
-
(result) => {
|
| 82 |
-
if (result) {
|
| 83 |
-
alert(result);
|
| 84 |
-
} else {
|
| 85 |
-
alert("没有生成结果,请重试。");
|
| 86 |
-
}
|
| 87 |
-
return result;
|
| 88 |
-
}
|
| 89 |
-
"""
|
| 90 |
)
|
| 91 |
|
| 92 |
clear_btn.click(
|
| 93 |
-
lambda: ("","","","male","English","生活"),
|
| 94 |
-
outputs=[species, look, personality, gender, language, inspiration]
|
| 95 |
)
|
| 96 |
|
| 97 |
if __name__ == "__main__":
|
|
|
|
| 1 |
+
import os, html, random
|
| 2 |
import gradio as gr
|
| 3 |
from openai import OpenAI
|
| 4 |
|
| 5 |
+
# ===== 基础配置 =====
|
| 6 |
API_KEY = os.getenv("OPENAI_API_KEY")
|
| 7 |
MODEL_NAME = "gpt-4o-mini"
|
| 8 |
client = OpenAI(api_key=API_KEY)
|
| 9 |
|
| 10 |
+
# ===== 系统提示(可爱名字 + 真实名言,灵感优先)=====
|
| 11 |
SYSTEM_PROMPT = """You are a playful, high-creativity pet name generator.
|
| 12 |
Goal: Produce EXACTLY ONE cute, kid-pronounceable pet name + one authentic quote line.
|
| 13 |
+
|
| 14 |
+
Creative balance:
|
| 15 |
+
- Inspiration source (山海经 / 迪士尼 / 哈利波特 / 生活) drives the concept (~70%).
|
| 16 |
+
- User inputs (species/appearance/personality/gender) refine the vibe (~30%).
|
| 17 |
+
|
| 18 |
+
NAME rules (kid-friendly):
|
| 19 |
+
- Cute, easy to call; allow coinages that sound natural.
|
| 20 |
+
- Chinese: 1–2 simple characters; English: 1–2 syllables; Japanese: 2–3 mora; Korean: 2 simple syllables; French: 1–2 syllables with soft endings.
|
| 21 |
+
- Output in requested language (Chinese/English/Japanese/Korean/French).
|
| 22 |
+
- ONE short meaning (≤12 words).
|
| 23 |
+
|
| 24 |
+
QUOTE rules (must be real):
|
| 25 |
+
- ONE short, warm, authentic quote about animals/love/companionship/playfulness with attribution (e.g., — Anatole France).
|
| 26 |
+
- Translate quote text into the requested language if needed; keep attribution name in Latin script.
|
| 27 |
+
|
| 28 |
+
STRICT OUTPUT (exactly two lines):
|
| 29 |
+
Name — short meaning
|
| 30 |
+
Quote: “...” — Attribution
|
| 31 |
"""
|
| 32 |
|
| 33 |
def build_prompt(species, look, personality, gender, language, inspiration):
|
| 34 |
return f"""
|
| 35 |
Requested language: {language}
|
| 36 |
+
Primary inspiration: {inspiration}
|
| 37 |
+
Secondary refinements (may be empty):
|
| 38 |
- Species: {species or '(unspecified)'}
|
| 39 |
- Appearance: {look or '(unspecified)'}
|
| 40 |
- Personality: {personality or '(unspecified)'}
|
| 41 |
- Gender: {gender}
|
| 42 |
+
|
| 43 |
Task:
|
| 44 |
+
1) Create EXACTLY ONE cute, kid-pronounceable pet name (≤12-word meaning).
|
| 45 |
+
2) Then output ONE authentic, warm animal-related quote with attribution.
|
| 46 |
+
Keep both lines in {language}. Inspiration ~70%, traits ~30%.
|
| 47 |
"""
|
| 48 |
|
| 49 |
+
# 真实名言兜底(防止偶尔没返回 quote 行)
|
| 50 |
+
FALLBACK_QUOTES = {
|
| 51 |
+
"English": [
|
| 52 |
+
('"Until one has loved an animal, a part of one\'s soul remains unawakened."', "Anatole France"),
|
| 53 |
+
('"An animal\'s eyes have the power to speak a great language."', "Martin Buber"),
|
| 54 |
+
('"Animals are such agreeable friends—they ask no questions; they pass no criticisms."', "George Eliot"),
|
| 55 |
+
],
|
| 56 |
+
"中文": [
|
| 57 |
+
('“一个人若未曾爱过动物,他的灵魂尚未苏醒。”', "Anatole France"),
|
| 58 |
+
('“动物的眼睛自有它们的语言。”', "Martin Buber"),
|
| 59 |
+
('“动物是极好的朋友——它们不发问,也不挑剔。”', "George Eliot"),
|
| 60 |
+
],
|
| 61 |
+
"日本語": [
|
| 62 |
+
('「動物を愛したことのない者の魂は、まだ目覚めていない。」', "Anatole France"),
|
| 63 |
+
('「動物の瞳は偉大な言葉を語る力を持つ。」', "Martin Buber"),
|
| 64 |
+
('「動物は実に快い友である——問いも批判もない。」', "George Eliot"),
|
| 65 |
+
],
|
| 66 |
+
"한국어": [
|
| 67 |
+
('“동물을 사랑해 보지 않은 영혼은 아직 깨어나지 않았다.”', "Anatole France"),
|
| 68 |
+
('“동물의 눈에는 위대한 언어가 담겨 있다.”', "Martin Buber"),
|
| 69 |
+
('“동물은 참으로 상냥한 친구다—묻지도 비난하지도 않는다.”', "George Eliot"),
|
| 70 |
+
],
|
| 71 |
+
"Français": [
|
| 72 |
+
("« Jusqu'à ce qu'on ait aimé un animal, une partie de notre âme reste endormie. »", "Anatole France"),
|
| 73 |
+
("« Les yeux d’un animal ont le pouvoir de parler un grand langage. »", "Martin Buber"),
|
| 74 |
+
("« Les animaux sont de si agréables amis — ils ne posent aucune question et ne font aucune critique. »", "George Eliot"),
|
| 75 |
+
],
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
def _fallback_quote(language: str) -> str:
|
| 79 |
+
lst = FALLBACK_QUOTES.get(language, FALLBACK_QUOTES["English"])
|
| 80 |
+
text, author = random.choice(lst)
|
| 81 |
+
return f'Quote: {text} — {author}'
|
| 82 |
+
|
| 83 |
+
def _ensure_two_lines(content: str, language: str):
|
| 84 |
+
if not content:
|
| 85 |
+
return None, None
|
| 86 |
+
lines = [ln.strip() for ln in content.splitlines() if ln.strip()]
|
| 87 |
+
name_line = lines[0] if lines else ""
|
| 88 |
+
quote_line = next((ln for ln in lines[1:] if ln.lower().startswith("quote:")), None)
|
| 89 |
+
if not quote_line:
|
| 90 |
+
quote_line = _fallback_quote(language)
|
| 91 |
+
return name_line, quote_line
|
| 92 |
+
|
| 93 |
+
def generate_modal_html(species, look, personality, gender, language, inspiration):
|
| 94 |
+
"""返回一个HTML覆盖层(伪弹窗),不使用 js 参数,兼容所有版本。"""
|
| 95 |
try:
|
| 96 |
+
prompt = build_prompt(
|
| 97 |
+
(species or "").strip(),
|
| 98 |
+
(look or "").strip(),
|
| 99 |
+
(personality or "").strip(),
|
| 100 |
+
gender, language, inspiration
|
| 101 |
+
)
|
| 102 |
resp = client.chat.completions.create(
|
| 103 |
model=MODEL_NAME,
|
| 104 |
+
temperature=0.95,
|
| 105 |
+
top_p=0.95,
|
| 106 |
max_tokens=280,
|
| 107 |
messages=[
|
| 108 |
{"role": "system", "content": SYSTEM_PROMPT},
|
| 109 |
+
{"role": "user", "content": prompt}
|
| 110 |
]
|
| 111 |
)
|
| 112 |
raw = (resp.choices[0].message.content or "").strip()
|
| 113 |
+
name_line, quote_line = _ensure_two_lines(raw, language)
|
| 114 |
+
if not name_line:
|
| 115 |
+
name_line = "(没有生成内容)"
|
| 116 |
+
if not quote_line:
|
| 117 |
+
quote_line = _fallback_quote(language)
|
| 118 |
+
|
| 119 |
+
# 构造覆盖层HTML(带关闭按钮,纯CSS/HTML,不依赖 .click 的 js 参数)
|
| 120 |
+
name_html = html.escape(name_line)
|
| 121 |
+
quote_html = html.escape(quote_line)
|
| 122 |
+
return f"""
|
| 123 |
+
<div class="pm-overlay" onclick="this.remove()">
|
| 124 |
+
<div class="pm-modal" onclick="event.stopPropagation()">
|
| 125 |
+
<div class="pm-modal-title">Pawfect Match</div>
|
| 126 |
+
<div class="pm-modal-body">
|
| 127 |
+
<div class="pm-name">{name_html}</div>
|
| 128 |
+
<div class="pm-quote">{quote_html}</div>
|
| 129 |
+
</div>
|
| 130 |
+
<div class="pm-modal-actions">
|
| 131 |
+
<button class="pm-btn" onclick="document.querySelector('.pm-overlay')?.remove()">关闭</button>
|
| 132 |
+
</div>
|
| 133 |
+
</div>
|
| 134 |
+
</div>
|
| 135 |
+
"""
|
| 136 |
except Exception as e:
|
| 137 |
+
err = html.escape(str(e))
|
| 138 |
+
return f"""
|
| 139 |
+
<div class="pm-overlay" onclick="this.remove()">
|
| 140 |
+
<div class="pm-modal" onclick="event.stopPropagation()">
|
| 141 |
+
<div class="pm-modal-title">Pawfect Match</div>
|
| 142 |
+
<div class="pm-modal-body">
|
| 143 |
+
<div class="pm-name">⚠️ 出错</div>
|
| 144 |
+
<div class="pm-quote">{err}</div>
|
| 145 |
+
</div>
|
| 146 |
+
<div class="pm-modal-actions">
|
| 147 |
+
<button class="pm-btn" onclick="document.querySelector('.pm-overlay')?.remove()">关闭</button>
|
| 148 |
+
</div>
|
| 149 |
+
</div>
|
| 150 |
+
</div>
|
| 151 |
+
"""
|
| 152 |
|
| 153 |
+
# ===== 主题 & CSS =====
|
| 154 |
theme = gr.themes.Soft(primary_hue="pink", secondary_hue="cyan", neutral_hue="slate")
|
| 155 |
+
CUSTOM_CSS = """
|
| 156 |
+
:root{
|
| 157 |
+
--pm-bg:#faf7fb; --pm-surface:#ffffff; --pm-border:#eddcf3; --pm-text:#111; --pm-muted:#6b7280;
|
| 158 |
+
--pm-accent:#ff4fa3;
|
| 159 |
+
}
|
| 160 |
+
body, .gradio-container{ background:var(--pm-bg) !important; }
|
| 161 |
+
.pm-topbar{
|
| 162 |
+
display:flex; align-items:center; justify-content:space-between;
|
| 163 |
+
padding:12px; background:linear-gradient(90deg,#ffe8f3,#e8fbff);
|
| 164 |
+
border-radius:12px; border:1px solid var(--pm-border);
|
| 165 |
+
}
|
| 166 |
+
.pm-brand{ display:flex; align-items:center; gap:10px; }
|
| 167 |
+
.pm-logo{ font-size:24px; }
|
| 168 |
+
.pm-title{ font-weight:800; font-size:20px; color:var(--pm-text); }
|
| 169 |
|
| 170 |
+
/* 覆盖层(伪弹窗) */
|
| 171 |
+
.pm-overlay{
|
| 172 |
+
position:fixed; inset:0; background:rgba(0,0,0,.35);
|
| 173 |
+
display:flex; align-items:center; justify-content:center; z-index:9999;
|
| 174 |
+
}
|
| 175 |
+
.pm-modal{
|
| 176 |
+
width:min(560px, 92vw); background:var(--pm-surface); border:1px solid var(--pm-border);
|
| 177 |
+
border-radius:14px; box-shadow:0 20px 60px rgba(0,0,0,.18);
|
| 178 |
+
overflow:hidden; animation:pm-pop .14s ease-out;
|
| 179 |
+
}
|
| 180 |
+
.pm-modal-title{
|
| 181 |
+
padding:14px 16px; font-weight:800; border-bottom:1px solid var(--pm-border);
|
| 182 |
+
}
|
| 183 |
+
.pm-modal-body{ padding:16px; }
|
| 184 |
+
.pm-name{ font-weight:800; font-size:22px; margin-bottom:8px; color:#111; }
|
| 185 |
+
.pm-quote{ font-weight:600; font-size:14px; color:#374151; opacity:.95; }
|
| 186 |
+
.pm-modal-actions{ padding:12px 16px; display:flex; justify-content:flex-end; gap:8px; border-top:1px solid var(--pm-border); }
|
| 187 |
+
.pm-btn{
|
| 188 |
+
padding:10px 14px; border-radius:10px; border:1px solid var(--pm-border); background:var(--pm-surface);
|
| 189 |
+
cursor:pointer; font-weight:700;
|
| 190 |
+
}
|
| 191 |
+
.pm-btn:hover{ border-color: var(--pm-accent); color: var(--pm-accent); }
|
| 192 |
+
|
| 193 |
+
@keyframes pm-pop{ from{ transform:translateY(6px); opacity:0; } to{ transform:translateY(0); opacity:1; } }
|
| 194 |
+
"""
|
| 195 |
+
|
| 196 |
+
# ===== 界面 =====
|
| 197 |
+
with gr.Blocks(title="Pawfect Match", theme=theme, css=CUSTOM_CSS) as demo:
|
| 198 |
+
# 顶栏(去掉副标题)
|
| 199 |
gr.HTML("""
|
| 200 |
+
<div class="pm-topbar">
|
| 201 |
+
<div class="pm-brand">
|
| 202 |
+
<div class="pm-logo">🐾</div>
|
| 203 |
+
<div class="pm-title">Pawfect Match</div>
|
|
|
|
|
|
|
| 204 |
</div>
|
| 205 |
+
<div style="color:#6b7280;font-size:12px;">v1.7</div>
|
| 206 |
</div>
|
| 207 |
""")
|
| 208 |
|
|
|
|
| 219 |
btn = gr.Button("✨ 起名", variant="primary")
|
| 220 |
clear_btn = gr.Button("清空", variant="secondary")
|
| 221 |
|
| 222 |
+
# 输出:只放一个隐藏的 HTML 容器,点“起名”时返回覆盖层 HTML
|
| 223 |
+
modal = gr.HTML(visible=True, value="") # 初始为空,不显示
|
| 224 |
|
| 225 |
btn.click(
|
| 226 |
+
generate_modal_html,
|
| 227 |
inputs=[species, look, personality, gender, language, inspiration],
|
| 228 |
+
outputs=modal
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 229 |
)
|
| 230 |
|
| 231 |
clear_btn.click(
|
| 232 |
+
lambda: ("","","","male","English","生活",""),
|
| 233 |
+
outputs=[species, look, personality, gender, language, inspiration, modal]
|
| 234 |
)
|
| 235 |
|
| 236 |
if __name__ == "__main__":
|