Update app.py
Browse files
app.py
CHANGED
@@ -1,302 +1,351 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
|
4 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
5 |
import json
|
6 |
-
import uuid
|
7 |
import os
|
8 |
-
import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
9 |
from PIL import Image
|
10 |
from PIL.PngImagePlugin import PngInfo
|
11 |
-
from io import BytesIO
|
12 |
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
98 |
try:
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
json_data['first_mes'] = json_data['char_greeting']
|
112 |
-
# Check if all the required keys are present
|
113 |
-
required_keys = ["char_name", "char_persona", "world_scenario", "char_greeting", "example_dialogue", "description"]
|
114 |
-
if all(key in json_data for key in required_keys):
|
115 |
-
return json.dumps(json_data), json_data
|
116 |
-
else:
|
117 |
-
return None, None
|
118 |
-
except Exception as e:
|
119 |
-
print(e)
|
120 |
return None, None
|
121 |
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
system_prompt_path = __file__.replace("app.py", f"{image_model}.txt")
|
143 |
-
|
144 |
-
# Read the system prompt from a text file
|
145 |
-
with open(system_prompt_path, "r") as file:
|
146 |
-
system_prompt = file.read()
|
147 |
-
|
148 |
-
if model in claude_models:
|
149 |
-
try:
|
150 |
-
# Generate a second response using the selected Anthropic model and the previously generated output
|
151 |
-
response = client.messages.create(
|
152 |
-
system=system_prompt,
|
153 |
-
messages=[{"role": "user", "content": generated_output}],
|
154 |
-
model=model,
|
155 |
-
max_tokens=4096
|
156 |
-
)
|
157 |
-
response_text = response.content[0].text
|
158 |
-
except Exception as e:
|
159 |
-
print(e)
|
160 |
-
response_text = f"An error occurred while generating the response. Check that your API key is correct! More info: {e}"
|
161 |
-
else:
|
162 |
-
try:
|
163 |
-
# Generate a response using the selected OpenAI model
|
164 |
-
response = client.chat.completions.create(
|
165 |
-
model=model,
|
166 |
-
messages=[
|
167 |
-
{"role": "system", "content": system_prompt},
|
168 |
-
{"role": "user", "content": generated_output}
|
169 |
-
],
|
170 |
-
max_tokens=4096
|
171 |
-
)
|
172 |
-
response_text = response.choices[0].message.content
|
173 |
-
except Exception as e:
|
174 |
-
print(e)
|
175 |
-
response_text = f"An error occurred while generating the response. Check that your API key is correct! More info: {e}"
|
176 |
-
|
177 |
-
return response_text
|
178 |
-
|
179 |
-
def inject_json_to_png(image, json_data):
|
180 |
-
if isinstance(json_data, str):
|
181 |
-
json_data = json.loads(json_data)
|
182 |
-
|
183 |
-
img = Image.open(image)
|
184 |
-
|
185 |
-
# Calculate the aspect ratio of the original image
|
186 |
-
width, height = img.size
|
187 |
-
aspect_ratio = width / height
|
188 |
-
|
189 |
-
# Calculate the cropping dimensions based on the aspect ratio
|
190 |
-
if aspect_ratio > 400 / 600:
|
191 |
-
# Image is wider than 400x600, crop the sides
|
192 |
-
new_width = int(height * 400 / 600)
|
193 |
-
left = (width - new_width) // 2
|
194 |
-
right = left + new_width
|
195 |
-
top = 0
|
196 |
-
bottom = height
|
197 |
-
else:
|
198 |
-
# Image is taller than 400x600, crop the top and bottom
|
199 |
-
new_height = int(width * 600 / 400)
|
200 |
-
left = 0
|
201 |
-
right = width
|
202 |
-
top = (height - new_height) // 2
|
203 |
-
bottom = top + new_height
|
204 |
-
|
205 |
-
# Perform cropping
|
206 |
-
img = img.crop((left, top, right, bottom))
|
207 |
-
|
208 |
-
# Resize the cropped image to 400x600 pixels
|
209 |
img = img.resize((400, 600), Image.LANCZOS)
|
210 |
-
|
211 |
-
#
|
212 |
-
|
213 |
-
|
214 |
-
|
215 |
-
|
216 |
-
|
217 |
-
|
218 |
-
|
219 |
-
|
220 |
-
|
221 |
-
|
222 |
-
|
223 |
-
|
224 |
-
|
225 |
-
|
226 |
-
|
227 |
-
|
228 |
-
|
229 |
-
|
230 |
-
|
231 |
-
|
232 |
-
|
233 |
-
#
|
234 |
-
|
235 |
-
|
236 |
-
|
237 |
-
|
238 |
-
|
239 |
-
|
240 |
-
|
241 |
-
|
242 |
-
|
243 |
-
|
244 |
-
|
245 |
-
|
246 |
-
|
247 |
-
|
248 |
-
|
249 |
-
|
250 |
-
|
251 |
-
|
252 |
-
|
253 |
-
|
254 |
-
|
255 |
-
|
256 |
-
|
257 |
-
|
258 |
-
|
259 |
-
|
260 |
-
|
261 |
-
|
262 |
-
|
263 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
264 |
if api_key.startswith("sk-ant-"):
|
265 |
-
return
|
266 |
-
|
267 |
-
return
|
268 |
-
|
269 |
-
|
270 |
-
|
271 |
-
|
272 |
-
|
273 |
-
|
274 |
-
|
275 |
-
|
276 |
-
|
277 |
-
|
278 |
-
|
279 |
-
|
280 |
-
|
281 |
-
|
282 |
-
|
283 |
-
|
284 |
-
|
285 |
-
|
286 |
-
|
287 |
-
|
288 |
-
|
289 |
-
|
290 |
-
|
291 |
-
|
292 |
-
|
293 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
294 |
return None
|
295 |
-
if
|
|
|
|
|
296 |
return None
|
297 |
-
|
298 |
-
return
|
|
|
|
|
|
|
|
|
299 |
|
300 |
-
|
|
|
|
|
301 |
|
302 |
-
|
|
|
|
1 |
+
#!/usr/bin/env python3
|
2 |
+
"""
|
3 |
+
SillyTavern CharacterβCard Generator βΒ versionΒ 2.0 (AprilΒ 2025)
|
4 |
+
----------------------------------------------------------------
|
5 |
+
* Adds the newest "thinking / reasoning" model names (oβseries, ClaudeΒ 3.7, GPTβ4.1 β¦)
|
6 |
+
* Unifies Anthropic & OpenAI logic behind a tiny abstraction layer
|
7 |
+
* Adds an optional **Thinking mode** toggle (Gradio UI) βΒ when enabled we
|
8 |
+
pass providerβspecific knobs that push the model to reason more deeply
|
9 |
+
(e.g. `vision: "detailed"` for Anthropic, `reasoning_mode: "enhanced"` for
|
10 |
+
OpenAI; both are ignored gracefully by older models)
|
11 |
+
* Factorises duplicated code, trims global namespace, and drops legacy bits
|
12 |
+
that are no longer needed.
|
13 |
+
|
14 |
+
The public interface of the script is still the same:Β run it, fill in your key,
|
15 |
+
choose a model, hit *Generate JSON*, then optionally *Generate SDXL Prompt* or
|
16 |
+
*Inject into PNG*.
|
17 |
+
"""
|
18 |
+
|
19 |
+
from __future__ import annotations
|
20 |
+
|
21 |
import json
|
|
|
22 |
import os
|
23 |
+
import uuid
|
24 |
+
from dataclasses import dataclass
|
25 |
+
from functools import cached_property
|
26 |
+
from io import BytesIO
|
27 |
+
from pathlib import Path
|
28 |
+
from typing import Any, Dict, List, Tuple, Union
|
29 |
+
|
30 |
+
import gradio as gr
|
31 |
from PIL import Image
|
32 |
from PIL.PngImagePlugin import PngInfo
|
|
|
33 |
|
34 |
+
# Thirdβparty SDKs βΒ import lazily to avoid mandatory install when not used
|
35 |
+
try:
|
36 |
+
from anthropic import Anthropic, APITimeoutError as AnthropicTimeout
|
37 |
+
except ImportError: # pragma: no cover
|
38 |
+
Anthropic = None # type: ignore
|
39 |
+
|
40 |
+
try:
|
41 |
+
from openai import OpenAI, APITimeoutError as OpenAITimeout
|
42 |
+
except ImportError: # pragma: no cover
|
43 |
+
OpenAI = None # type: ignore
|
44 |
+
|
45 |
+
###############################################################################
|
46 |
+
# Model catalog
|
47 |
+
###############################################################################
|
48 |
+
|
49 |
+
CLAUDE_MODELS: List[str] = [
|
50 |
+
# β classic 3βseries
|
51 |
+
"claude-3-opus-20240229",
|
52 |
+
"claude-3-sonnet-20240229",
|
53 |
+
"claude-3-haiku-20240307",
|
54 |
+
# β 3.5 refreshes
|
55 |
+
"claude-3-5-sonnet-20240620",
|
56 |
+
"claude-3-5-sonnet-20241022",
|
57 |
+
"claude-3-5-haiku-20241022",
|
58 |
+
# β newest 3.7 extendedβthinking (FebΒ 2025)
|
59 |
+
"claude-3-7-sonnet-20250219",
|
60 |
+
]
|
61 |
+
|
62 |
+
OPENAI_MODELS: List[str] = [
|
63 |
+
# oβseries βΒ small/medium reasoning models (AprΒ 2025)
|
64 |
+
"o3", "o3-mini", "o4-mini",
|
65 |
+
# 4.1 family βΒ 1β―Mβtoken context (AprΒ 2025)
|
66 |
+
"gpt-4.1", "gpt-4.1-mini", "gpt-4.1-nano",
|
67 |
+
# 4βseries mainline
|
68 |
+
"gpt-4o", "gpt-4o-mini", "gpt-4", "gpt-4-32k",
|
69 |
+
"gpt-4-0125-preview", "gpt-4-turbo-preview", "gpt-4-1106-preview",
|
70 |
+
# 3.5 for cheap/fast baseline
|
71 |
+
"gpt-3.5-turbo",
|
72 |
+
]
|
73 |
+
|
74 |
+
ALL_MODELS = CLAUDE_MODELS + OPENAI_MODELS
|
75 |
+
DEFAULT_ANTHROPIC_ENDPOINT = "https://api.anthropic.com"
|
76 |
+
DEFAULT_OPENAI_ENDPOINT = "https://api.openai.com/v1"
|
77 |
+
|
78 |
+
###############################################################################
|
79 |
+
# Helper classes
|
80 |
+
###############################################################################
|
81 |
+
|
82 |
+
JsonDict = Dict[str, Any]
|
83 |
+
|
84 |
+
|
85 |
+
@dataclass
|
86 |
+
class APIConfig:
|
87 |
+
endpoint: str
|
88 |
+
api_key: str
|
89 |
+
model: str
|
90 |
+
|
91 |
+
# UI knobs β not required but exposed for flexibility
|
92 |
+
temperature: float = 0.7
|
93 |
+
top_p: float = 0.9
|
94 |
+
thinking: bool = False # when True we request "enhanced reasoning"
|
95 |
+
|
96 |
+
# ---------------------------------------------------------------------
|
97 |
+
# Derived helpers
|
98 |
+
# ---------------------------------------------------------------------
|
99 |
+
|
100 |
+
@cached_property
|
101 |
+
def provider(self) -> str:
|
102 |
+
if self.model in CLAUDE_MODELS:
|
103 |
+
return "anthropic"
|
104 |
+
elif self.model in OPENAI_MODELS:
|
105 |
+
return "openai"
|
106 |
+
raise ValueError(f"Unknown model family for {self.model}")
|
107 |
+
|
108 |
+
@cached_property
|
109 |
+
def sdk(self):
|
110 |
+
if self.provider == "anthropic":
|
111 |
+
if Anthropic is None:
|
112 |
+
raise RuntimeError("anthropicβpython not installed βΒ `pip install anthropic`")
|
113 |
+
return Anthropic(api_key=self.api_key, base_url=self.endpoint)
|
114 |
+
if self.provider == "openai":
|
115 |
+
if OpenAI is None:
|
116 |
+
raise RuntimeError("openaiβpython not installed βΒ `pip install openai`")
|
117 |
+
return OpenAI(api_key=self.api_key, base_url=self.endpoint)
|
118 |
+
raise AssertionError # unreachable
|
119 |
+
|
120 |
+
# ------------------------------------------------------------------
|
121 |
+
# Public send convenience
|
122 |
+
# ------------------------------------------------------------------
|
123 |
+
|
124 |
+
def chat(self, user_prompt: str, system_prompt: str = "", max_tokens: int = 4096) -> str:
|
125 |
+
"""Send the prompt; return *content* string."""
|
126 |
+
if self.provider == "anthropic":
|
127 |
+
opts = {
|
128 |
+
"model": self.model,
|
129 |
+
"system": system_prompt,
|
130 |
+
"messages": [{"role": "user", "content": user_prompt}],
|
131 |
+
"max_tokens": max_tokens,
|
132 |
+
"temperature": self.temperature,
|
133 |
+
"top_p": self.top_p,
|
134 |
+
}
|
135 |
+
if self.thinking:
|
136 |
+
opts["vision"] = "detailed" # currently Anthropic's knob for deeperβreasoning & image understanding
|
137 |
+
resp = self.sdk.messages.create(**opts)
|
138 |
+
return resp.content[0].text
|
139 |
+
|
140 |
+
# OpenAI branch --------------------------------------------------
|
141 |
+
opts = {
|
142 |
+
"model": self.model,
|
143 |
+
"messages": [
|
144 |
+
{"role": "system", "content": system_prompt},
|
145 |
+
{"role": "user", "content": user_prompt},
|
146 |
+
],
|
147 |
+
"max_tokens": max_tokens,
|
148 |
+
"temperature": self.temperature,
|
149 |
+
"top_p": self.top_p,
|
150 |
+
}
|
151 |
+
if self.thinking:
|
152 |
+
opts["reasoning_mode"] = "enhanced" # silently ignored by models that do not support it
|
153 |
+
resp = self.sdk.chat.completions.create(**opts)
|
154 |
+
return resp.choices[0].message.content
|
155 |
+
|
156 |
+
###############################################################################
|
157 |
+
# Characterβcard helpers (unchanged logic, just cleaner)
|
158 |
+
###############################################################################
|
159 |
+
|
160 |
+
CARD_REQUIRED_KEYS = {
|
161 |
+
"char_name",
|
162 |
+
"char_persona",
|
163 |
+
"world_scenario",
|
164 |
+
"char_greeting",
|
165 |
+
"example_dialogue",
|
166 |
+
"description",
|
167 |
+
}
|
168 |
+
|
169 |
+
|
170 |
+
def extract_card_json(generated_output: str) -> Tuple[str | None, JsonDict | None]:
|
171 |
+
"""Return (raw_json_str_or_None, parsed_json_or_None)."""
|
172 |
try:
|
173 |
+
snippet = generated_output.replace("```json", "").replace("```", "").strip()
|
174 |
+
raw = snippet[snippet.find("{") : snippet.rfind("}") + 1]
|
175 |
+
data: JsonDict = json.loads(raw)
|
176 |
+
# remap
|
177 |
+
data["name"] = data["char_name"]
|
178 |
+
data["personality"] = data["char_persona"]
|
179 |
+
data["scenario"] = data["world_scenario"]
|
180 |
+
data["first_mes"] = data["char_greeting"]
|
181 |
+
if CARD_REQUIRED_KEYS.issubset(data):
|
182 |
+
return json.dumps(data, ensure_ascii=False, indent=2), data
|
183 |
+
return None, None
|
184 |
+
except Exception:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
185 |
return None, None
|
186 |
|
187 |
+
|
188 |
+
###############################################################################
|
189 |
+
# PNG utils
|
190 |
+
###############################################################################
|
191 |
+
|
192 |
+
def inject_card_into_png(image_path: str | Path, card_json: Union[str, JsonDict]) -> Path:
|
193 |
+
card: JsonDict = json.loads(card_json) if isinstance(card_json, str) else card_json
|
194 |
+
|
195 |
+
img = Image.open(image_path)
|
196 |
+
# Centreβcrop to 400Γ600 while preserving aspect ratio ----------------
|
197 |
+
w, h = img.size
|
198 |
+
target_ratio = 400 / 600
|
199 |
+
if w / h > target_ratio: # wider β crop sides
|
200 |
+
new_w = int(h * target_ratio)
|
201 |
+
left = (w - new_w) // 2
|
202 |
+
img = img.crop((left, 0, left + new_w, h))
|
203 |
+
else: # taller β crop top/bottom
|
204 |
+
new_h = int(w / target_ratio)
|
205 |
+
top = (h - new_h) // 2
|
206 |
+
img = img.crop((0, top, w, top + new_h))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
207 |
img = img.resize((400, 600), Image.LANCZOS)
|
208 |
+
|
209 |
+
# Embed the JSON as base64 inside a tEXt chunk ------------------------
|
210 |
+
meta = PngInfo()
|
211 |
+
meta.add_text("chara", json.dumps(card).encode("utfβ8").hex())
|
212 |
+
|
213 |
+
out_dir = Path(__file__).with_name("outputs")
|
214 |
+
out_dir.mkdir(exist_ok=True)
|
215 |
+
filename = f"{card['name']}_{uuid.uuid4()}.png"
|
216 |
+
out_path = out_dir / filename
|
217 |
+
img.save(out_path, format="PNG", pnginfo=meta)
|
218 |
+
return out_path
|
219 |
+
|
220 |
+
###############################################################################
|
221 |
+
# Gradio UI
|
222 |
+
###############################################################################
|
223 |
+
|
224 |
+
def build_ui():
|
225 |
+
"""Return a readyβtoβlaunch Gradio Blocks app."""
|
226 |
+
|
227 |
+
with gr.Blocks(title="SillyTavern Character Generator 2.0") as demo:
|
228 |
+
gr.Markdown("# π SillyTavern Character GeneratorΒ 2.0")
|
229 |
+
|
230 |
+
# ----------------------------------------------------------------
|
231 |
+
# Tab: JSON generator
|
232 |
+
# ----------------------------------------------------------------
|
233 |
+
with gr.Tab("JSON Generate"):
|
234 |
+
with gr.Row():
|
235 |
+
with gr.Column():
|
236 |
+
endpoint_box = gr.Textbox(label="Endpoint", value=DEFAULT_ANTHROPIC_ENDPOINT)
|
237 |
+
api_key_box = gr.Textbox(label="API Key", type="password", placeholder="sk-ant-β¦ or sk-β¦")
|
238 |
+
model_dropdown = gr.Dropdown(choices=ALL_MODELS, label="Model")
|
239 |
+
thinking_toggle = gr.Checkbox(label="Thinking / deepβreasoning mode", value=False)
|
240 |
+
temp_slider = gr.Slider(label="Temperature", minimum=0.0, maximum=1.0, value=0.7, step=0.05)
|
241 |
+
topp_slider = gr.Slider(label="TopβP", minimum=0.0, maximum=1.0, value=0.9, step=0.05)
|
242 |
+
|
243 |
+
user_prompt_box = gr.Textbox(
|
244 |
+
label="Prompt",
|
245 |
+
placeholder="Describe the character you wantβ¦",
|
246 |
+
lines=6,
|
247 |
+
)
|
248 |
+
gen_btn = gr.Button("Generate JSON")
|
249 |
+
|
250 |
+
with gr.Column():
|
251 |
+
llm_raw_out = gr.Textbox(label="Model output (raw)")
|
252 |
+
json_out = gr.Textbox(label="Extracted JSON (ready for PNG)")
|
253 |
+
json_download = gr.File(label="Download .json")
|
254 |
+
|
255 |
+
# SDXL prompt tab (unchanged logic) --------------------------
|
256 |
+
with gr.Row():
|
257 |
+
image_model_dropdown = gr.Dropdown(["SDXL", "midjourney"], label="Image model", value="SDXL")
|
258 |
+
gen_sdxl_btn = gr.Button("Generate image prompt")
|
259 |
+
sdxl_out = gr.Textbox(label="Generated prompt")
|
260 |
+
|
261 |
+
# ----------------------------------------------------------------
|
262 |
+
# Tab: PNG injector
|
263 |
+
# ----------------------------------------------------------------
|
264 |
+
with gr.Tab("PNG Inject"):
|
265 |
+
gr.Markdown("Upload an image (any resolution) and embed the JSON card.")
|
266 |
+
with gr.Row():
|
267 |
+
img_upload = gr.Image(type="filepath", label="PNG image")
|
268 |
+
json_text = gr.Textbox(label="JSON")
|
269 |
+
json_file = gr.File(label="β¦or choose a .json file", file_types=[".json"])
|
270 |
+
inject_btn = gr.Button("Inject & download")
|
271 |
+
injected_png = gr.File(label="Download new PNG")
|
272 |
+
|
273 |
+
# ββ Callbacks ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
274 |
+
|
275 |
+
def _choose_default_endpoint(api_key: str):
|
276 |
if api_key.startswith("sk-ant-"):
|
277 |
+
return DEFAULT_ANTHROPIC_ENDPOINT
|
278 |
+
if api_key.startswith("sk-"):
|
279 |
+
return DEFAULT_OPENAI_ENDPOINT
|
280 |
+
return DEFAULT_ANTHROPIC_ENDPOINT
|
281 |
+
|
282 |
+
api_key_box.change(_choose_default_endpoint, api_key_box, endpoint_box)
|
283 |
+
|
284 |
+
# Main JSON generation ------------------------------------------
|
285 |
+
def _generate_json(endpoint: str, api_key: str, model: str, thinking: bool,
|
286 |
+
temp: float, top_p: float, prompt: str):
|
287 |
+
cfg = APIConfig(endpoint.strip(), api_key.strip(), model, temp, top_p, thinking)
|
288 |
+
|
289 |
+
# System prompt lives next to this script -------------------
|
290 |
+
sys_prompt_path = Path(__file__).with_name("json.txt")
|
291 |
+
system_prompt = sys_prompt_path.read_text(encoding="utfβ8") if sys_prompt_path.exists() else ""
|
292 |
+
|
293 |
+
try:
|
294 |
+
llm_output = cfg.chat(prompt, system_prompt)
|
295 |
+
except (AnthropicTimeout, OpenAITimeout) as exc: # type: ignore
|
296 |
+
return f"Request timed out: {exc}", "", None
|
297 |
+
except Exception as exc:
|
298 |
+
return f"Error: {exc}", "", None
|
299 |
+
|
300 |
+
raw_json, parsed = extract_card_json(llm_output)
|
301 |
+
if raw_json and parsed:
|
302 |
+
out_dir = Path(__file__).with_name("outputs")
|
303 |
+
out_dir.mkdir(exist_ok=True)
|
304 |
+
path = out_dir / f"{parsed['name']}_{uuid.uuid4()}.json"
|
305 |
+
path.write_text(raw_json, encoding="utfβ8")
|
306 |
+
return llm_output, raw_json, str(path)
|
307 |
+
return llm_output, "(no valid JSON detected)", None
|
308 |
+
|
309 |
+
gen_btn.click(
|
310 |
+
_generate_json,
|
311 |
+
[endpoint_box, api_key_box, model_dropdown, thinking_toggle, temp_slider, topp_slider, user_prompt_box],
|
312 |
+
[llm_raw_out, json_out, json_download],
|
313 |
+
)
|
314 |
+
|
315 |
+
# SDXL prompt generation ---------------------------------------
|
316 |
+
def _generate_sdxl_prompt(endpoint: str, api_key: str, model: str, prompt: str, image_model: str):
|
317 |
+
cfg = APIConfig(endpoint.strip(), api_key.strip(), model)
|
318 |
+
system_path = Path(__file__).with_name(f"{image_model}.txt")
|
319 |
+
system_prompt = system_path.read_text(encoding="utfβ8") if system_path.exists() else ""
|
320 |
+
try:
|
321 |
+
return cfg.chat(prompt, system_prompt)
|
322 |
+
except Exception as exc:
|
323 |
+
return f"Error: {exc}"
|
324 |
+
|
325 |
+
gen_sdxl_btn.click(
|
326 |
+
_generate_sdxl_prompt,
|
327 |
+
[endpoint_box, api_key_box, model_dropdown, llm_raw_out, image_model_dropdown],
|
328 |
+
sdxl_out,
|
329 |
+
)
|
330 |
+
|
331 |
+
# PNG injection --------------------------------------------------
|
332 |
+
def _inject_png(img_path: str | None, json_str: str | None, json_file_path: str | None):
|
333 |
+
if not img_path:
|
334 |
return None
|
335 |
+
if json_file_path:
|
336 |
+
json_str = Path(json_file_path).read_text(encoding="utfβ8")
|
337 |
+
if not json_str:
|
338 |
return None
|
339 |
+
new_png = inject_card_into_png(img_path, json_str)
|
340 |
+
return str(new_png)
|
341 |
+
|
342 |
+
inject_btn.click(_inject_png, [img_upload, json_text, json_file], injected_png)
|
343 |
+
|
344 |
+
return demo
|
345 |
|
346 |
+
###############################################################################
|
347 |
+
# Entrypoint
|
348 |
+
###############################################################################
|
349 |
|
350 |
+
if __name__ == "__main__":
|
351 |
+
build_ui().launch()
|