Upload 4 files
Browse files- bootstrap.py +98 -9
bootstrap.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
import os
|
|
|
|
| 2 |
import importlib
|
| 3 |
from pathlib import Path
|
| 4 |
|
|
@@ -79,19 +80,26 @@ def _openai_translate(text: str, lang_from: str = "auto", lang_to: str = "zh") -
|
|
| 79 |
|
| 80 |
url = base_url.rstrip("/") + "/chat/completions"
|
| 81 |
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
payload = {
|
| 83 |
"model": model,
|
| 84 |
"temperature": 0,
|
| 85 |
"messages": [
|
| 86 |
-
{
|
| 87 |
-
"role": "system",
|
| 88 |
-
"content": (
|
| 89 |
-
"You are a professional translation engine. "
|
| 90 |
-
f"Translate the user text to {target_lang}. "
|
| 91 |
-
"Only output the translated text without any extra words, quotes, or explanations. "
|
| 92 |
-
"Preserve numbers, emoji, and links."
|
| 93 |
-
),
|
| 94 |
-
},
|
| 95 |
{"role": "user", "content": text},
|
| 96 |
],
|
| 97 |
}
|
|
@@ -109,6 +117,33 @@ def _openai_translate(text: str, lang_from: str = "auto", lang_to: str = "zh") -
|
|
| 109 |
if not content:
|
| 110 |
raise MemeFeedback("OpenAI 翻译失败:空结果")
|
| 111 |
result = str(content).strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
print(f"[translate/openai] success result_len={len(result)}", flush=True)
|
| 113 |
return result
|
| 114 |
except Exception as e:
|
|
@@ -165,6 +200,60 @@ def render_list(params: RenderMemeListRequest = RenderMemeListRequest()):
|
|
| 165 |
from fastapi import Response
|
| 166 |
return Response(content=content, media_type=media_type)
|
| 167 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
# Load contrib + emoji
|
| 169 |
load_memes("/app/meme-generator-contrib/memes")
|
| 170 |
load_memes("/app/meme_emoji/emoji")
|
|
|
|
| 1 |
import os
|
| 2 |
+
import re
|
| 3 |
import importlib
|
| 4 |
from pathlib import Path
|
| 5 |
|
|
|
|
| 80 |
|
| 81 |
url = base_url.rstrip("/") + "/chat/completions"
|
| 82 |
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
|
| 83 |
+
# Build a stronger instruction, especially for Japanese output
|
| 84 |
+
extra_rules = ""
|
| 85 |
+
if target_lang.lower().startswith("japan"):
|
| 86 |
+
extra_rules = (
|
| 87 |
+
" Use natural Japanese. Prefer including hiragana/katakana (kana) where appropriate; "
|
| 88 |
+
"do not just output Chinese hanzi. If the input is Chinese, do not return the same text."
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
system_prompt = (
|
| 92 |
+
"You are a professional translation engine."
|
| 93 |
+
f" Translate the user text to {target_lang}."
|
| 94 |
+
" Only output the translated text without any extra words, quotes, or explanations."
|
| 95 |
+
" Preserve numbers, emoji, and links." + extra_rules
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
payload = {
|
| 99 |
"model": model,
|
| 100 |
"temperature": 0,
|
| 101 |
"messages": [
|
| 102 |
+
{"role": "system", "content": system_prompt},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
{"role": "user", "content": text},
|
| 104 |
],
|
| 105 |
}
|
|
|
|
| 117 |
if not content:
|
| 118 |
raise MemeFeedback("OpenAI 翻译失败:空结果")
|
| 119 |
result = str(content).strip()
|
| 120 |
+
# If target is Japanese but no kana detected, attempt a single retry with stricter rule
|
| 121 |
+
def has_kana(s: str) -> bool:
|
| 122 |
+
return bool(re.search(r"[\u3040-\u30FF]", s))
|
| 123 |
+
|
| 124 |
+
if target_lang.lower().startswith("japan") and not has_kana(result):
|
| 125 |
+
print("[translate/openai] no kana detected, retrying with stricter Japanese rule", flush=True)
|
| 126 |
+
strict_prompt = (
|
| 127 |
+
system_prompt
|
| 128 |
+
+ " Ensure the output contains kana (hiragana or katakana) and is not identical to the input."
|
| 129 |
+
)
|
| 130 |
+
payload_retry = {
|
| 131 |
+
"model": model,
|
| 132 |
+
"temperature": 0,
|
| 133 |
+
"messages": [
|
| 134 |
+
{"role": "system", "content": strict_prompt},
|
| 135 |
+
{"role": "user", "content": text},
|
| 136 |
+
],
|
| 137 |
+
}
|
| 138 |
+
r2 = httpx.post(url, headers=headers, json=payload_retry, timeout=60)
|
| 139 |
+
print(f"[translate/openai] retry response status={r2.status_code}", flush=True)
|
| 140 |
+
r2.raise_for_status()
|
| 141 |
+
data2 = r2.json()
|
| 142 |
+
choices2 = data2.get("choices") or []
|
| 143 |
+
result2 = (choices2[0].get("message", {}).get("content") or "").strip() if choices2 else ""
|
| 144 |
+
if result2:
|
| 145 |
+
print(f"[translate/openai] retry success result_len={len(result2)}", flush=True)
|
| 146 |
+
return result2
|
| 147 |
print(f"[translate/openai] success result_len={len(result)}", flush=True)
|
| 148 |
return result
|
| 149 |
except Exception as e:
|
|
|
|
| 200 |
from fastapi import Response
|
| 201 |
return Response(content=content, media_type=media_type)
|
| 202 |
|
| 203 |
+
# Dynamic infos.json and keyMap.json derived from loaded memes (not aggregated assets)
|
| 204 |
+
from collections import OrderedDict
|
| 205 |
+
from meme_generator.app import MemeInfoResponse, MemeParamsResponse
|
| 206 |
+
|
| 207 |
+
def build_infos_and_keymap():
|
| 208 |
+
infos = {}
|
| 209 |
+
pairs = [] # (keyword, meme_key)
|
| 210 |
+
for meme in sorted(get_memes(), key=lambda m: m.key):
|
| 211 |
+
args_type_response = None
|
| 212 |
+
if meme.params_type.args_type:
|
| 213 |
+
args_model = meme.params_type.args_type.args_model
|
| 214 |
+
args_type_response = {
|
| 215 |
+
"args_model": args_model.model_json_schema() if hasattr(args_model, "model_json_schema") else {},
|
| 216 |
+
"args_examples": [
|
| 217 |
+
getattr(x, "model_dump", lambda: x)() if hasattr(x, "model_dump") else x
|
| 218 |
+
for x in meme.params_type.args_type.args_examples
|
| 219 |
+
],
|
| 220 |
+
"parser_options": meme.params_type.args_type.parser_options,
|
| 221 |
+
}
|
| 222 |
+
infos[meme.key] = {
|
| 223 |
+
"key": meme.key,
|
| 224 |
+
"params_type": {
|
| 225 |
+
"min_images": meme.params_type.min_images,
|
| 226 |
+
"max_images": meme.params_type.max_images,
|
| 227 |
+
"min_texts": meme.params_type.min_texts,
|
| 228 |
+
"max_texts": meme.params_type.max_texts,
|
| 229 |
+
"default_texts": meme.params_type.default_texts,
|
| 230 |
+
"args_type": args_type_response,
|
| 231 |
+
},
|
| 232 |
+
"keywords": meme.keywords,
|
| 233 |
+
"shortcuts": meme.shortcuts,
|
| 234 |
+
"tags": list(meme.tags),
|
| 235 |
+
"date_created": meme.date_created,
|
| 236 |
+
"date_modified": meme.date_modified,
|
| 237 |
+
}
|
| 238 |
+
for kw in meme.keywords:
|
| 239 |
+
pairs.append((kw, meme.key))
|
| 240 |
+
|
| 241 |
+
keymap = OrderedDict()
|
| 242 |
+
for kw, key in sorted(pairs, key=lambda x: len(x[0]), reverse=True):
|
| 243 |
+
if kw not in keymap:
|
| 244 |
+
keymap[kw] = key
|
| 245 |
+
return infos, keymap
|
| 246 |
+
|
| 247 |
+
@app.get("/memes/static/infos.json")
|
| 248 |
+
def infos_json():
|
| 249 |
+
infos, _ = build_infos_and_keymap()
|
| 250 |
+
return infos
|
| 251 |
+
|
| 252 |
+
@app.get("/memes/static/keyMap.json")
|
| 253 |
+
def keymap_json():
|
| 254 |
+
_, keymap = build_infos_and_keymap()
|
| 255 |
+
return keymap
|
| 256 |
+
|
| 257 |
# Load contrib + emoji
|
| 258 |
load_memes("/app/meme-generator-contrib/memes")
|
| 259 |
load_memes("/app/meme_emoji/emoji")
|