Zack3D commited on
Commit
6e2642c
Β·
verified Β·
1 Parent(s): 5272a9c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +337 -288
app.py CHANGED
@@ -1,302 +1,351 @@
1
- import gradio as gr
2
- from anthropic import Anthropic
3
- from openai import OpenAI
4
- import openai
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  import json
6
- import uuid
7
  import os
8
- import base64
 
 
 
 
 
 
 
9
  from PIL import Image
10
  from PIL.PngImagePlugin import PngInfo
11
- from io import BytesIO
12
 
13
- default_urls = ["https://api.anthropic.com", "https://api.openai.com/v1"]
14
-
15
- # List of available Claude models
16
- claude_models = ["claude-3-5-sonnet-20240620", "claude-3-opus-20240229", "claude-3-sonnet-20240229", "claude-3-haiku-20240307"]
17
-
18
- # List of available OpenAI models
19
- openai_models = ["gpt-4o", "gpt-4o-mini", "gpt-4", "gpt-4-32k", "gpt-3.5-turbo", "gpt-4-0125-preview", "gpt-4-turbo-preview", "gpt-4-1106-preview", "gpt-4-0613"]
20
-
21
- image_prompter = ["SDXL", "midjourney"]
22
-
23
- both_models = claude_models + openai_models
24
-
25
- def generate_response(endpoint, api_key, model, user_prompt):
26
- print(endpoint)
27
- if endpoint in default_urls:
28
- #check api keys as normal
29
- if api_key.startswith("sk-ant-"):
30
- client = Anthropic(api_key=api_key, base_url=endpoint)
31
- system_prompt_path = __file__.replace("app.py", "json.txt")
32
- elif api_key.startswith("sk-"):
33
- client = OpenAI(api_key=api_key, base_url=endpoint)
34
- system_prompt_path = __file__.replace("app.py", "json.txt")
35
- else:
36
- print(f"Invalid API key: {api_key}")
37
- return "Invalid API key", "Invalid API key", None
38
- else:
39
- if model in claude_models:
40
- # Set the Anthropic API key
41
- client = Anthropic(api_key=api_key, base_url=endpoint)
42
- system_prompt_path = __file__.replace("app.py", "json.txt")
43
- else:
44
- # Set the OpenAI API key
45
- client = OpenAI(api_key=api_key, base_url=endpoint)
46
- system_prompt_path = __file__.replace("app.py", "json.txt")
47
-
48
- # Read the system prompt from a text file
49
- with open(system_prompt_path, "r") as file:
50
- system_prompt = file.read()
51
-
52
- if model in claude_models:
53
- # Generate a response using the selected Anthropic model
54
- try:
55
- response = client.messages.create(
56
- system=system_prompt,
57
- messages=[{"role": "user", "content": user_prompt}],
58
- model=model,
59
- max_tokens=4096
60
- )
61
- response_text = response.content[0].text
62
- except Exception as e:
63
- print(e)
64
- response_text = f"An error occurred while generating the response. Check that your API key is correct! More info: {e}"
65
- else:
66
- try:
67
- # Generate a response using the selected OpenAI model
68
- response = client.chat.completions.create(
69
- model=model,
70
- messages=[
71
- {"role": "system", "content": system_prompt},
72
- {"role": "user", "content": user_prompt}
73
- ],
74
- max_tokens=4096
75
- )
76
- response_text = response.choices[0].message.content
77
- except Exception as e:
78
- print(e)
79
- response_text = f"An error occurred while generating the response. Check that your API key is correct! More info: {e}"
80
-
81
- json_string, json_json = extract_json(response_text)
82
- json_file = json_string if json_string else None
83
- create_unique_id = str(uuid.uuid4())
84
-
85
- json_folder = __file__.replace("app.py", f"outputs/")
86
- if not os.path.exists(json_folder):
87
- os.makedirs(json_folder)
88
- path = None
89
- if json_string:
90
- with open(f"{json_folder}{json_json['name']}_{create_unique_id}.json", "w") as file:
91
- file.write(json_file)
92
- path = f"{json_folder}{json_json['name']}_{create_unique_id}.json"
93
- else:
94
- json_string = "No JSON data was found, or the JSON data was incomplete."
95
- return response_text, json_string or "", path
96
-
97
- def extract_json(generated_output):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  try:
99
- generated_output = generated_output.replace("```json", "").replace("```", "").strip()
100
- # Find the JSON string in the generated output
101
- json_start = generated_output.find("{")
102
- json_end = generated_output.rfind("}") + 1
103
- json_string = generated_output[json_start:json_end]
104
- print(json_string)
105
-
106
- # Parse the JSON string
107
- json_data = json.loads(json_string)
108
- json_data['name'] = json_data['char_name']
109
- json_data['personality'] = json_data['char_persona']
110
- json_data['scenario'] = json_data['world_scenario']
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
- def generate_second_response(endpoint, api_key, model, generated_output, image_model):
123
- if endpoint in default_urls:
124
- #check api keys as normal
125
- if api_key.startswith("sk-ant-"):
126
- client = Anthropic(api_key=api_key, base_url=endpoint)
127
- system_prompt_path = __file__.replace("app.py", f"{image_model}.txt")
128
- elif api_key.startswith("sk-"):
129
- client = OpenAI(api_key=api_key, base_url=endpoint)
130
- system_prompt_path = __file__.replace("app.py", f"{image_model}.txt")
131
- else:
132
- print("Invalid API key")
133
- return "Invalid API key", "Invalid API key", None
134
- else:
135
- if model in claude_models:
136
- # Set the Anthropic API key
137
- client = Anthropic(api_key=api_key, base_url=endpoint)
138
- system_prompt_path = __file__.replace("app.py", f"{image_model}.txt")
139
- else:
140
- # Set the OpenAI API key
141
- client = OpenAI(api_key=api_key, base_url=endpoint)
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
- # Convert the JSON data to bytes
212
- json_bytes = json.dumps(json_data).encode('utf-8')
213
-
214
- # Create a new PNG image with the JSON data injected into the tEXT chunk
215
- output = BytesIO()
216
- img.save(output, format='PNG')
217
- output.seek(0)
218
-
219
- # Add the tEXT chunk with the tag 'chara'
220
- metadata = PngInfo()
221
- metadata.add_text("chara", base64.b64encode(json_bytes))
222
-
223
- # Save the modified PNG image to a BytesIO object
224
- output = BytesIO()
225
- create_unique_id = str(uuid.uuid4())
226
- if json_data['name']:
227
- filename = f"{json_data['name']}_{create_unique_id}.png"
228
- img_folder = __file__.replace("app.py", f"outputs/")
229
- img.save(f"{img_folder}/{filename}", format='PNG', pnginfo=metadata)
230
-
231
- return f"{img_folder}/{filename}"
232
-
233
- # Set up the Gradio interface
234
- with gr.Blocks() as demo:
235
- gr.Markdown("# SillyTavern Character Generator")
236
-
237
- #Text explaining that you can use the API key from the Anthropic API or the OpenAI API
238
- gr.Markdown("You can use the API key from the Anthropic API or the OpenAI API. The API key should start with 'sk-ant-' for Anthropic or 'sk-' for OpenAI.")
239
- gr.Markdown("Please Note: If you use a proxy it must support the OpenAI or Anthropic standard api calls! khanon does, Openrouter based ones usually do not.")
240
- gr.Markdown("Generating images locally and want to use the prompts from here in your workflow? Well do that I guess... a link to the virus used to be here. Maybe check me out? https://goocat.gay")
241
- with gr.Tab("JSON Generate"):
242
- with gr.Row():
243
- with gr.Column():
244
- endpoint = gr.Textbox(label="Endpoint", value="https://api.anthropic.com")
245
- api_key = gr.Textbox(label="API Key", type="password", placeholder="sk-ant-api03-... or sk-...")
246
- model_dropdown = gr.Dropdown(choices=[], label="Select a model")
247
- user_prompt = gr.Textbox(label="User Prompt", value="Make me a card for a panther made of translucent pastel colored goo. Its color never changes once it exists but each 'copy' has a different color. The creature comes out of a small jar, seemingly defying physics with its size. It is the size of a real panther, and as strong as one too. By default its female but is able to change gender. It can even split into multiple copies of itself if needed with no change in its own size or mass. Its outside is normally lightly squishy but solid, but on command it can become viscous like non-newtonian fluids. Be descriptive when describing this character, and make sure to describe all of its features in char_persona just like you do in description. Make sure to describe commonly used features in detail (visual, smell, taste, touch, etc).")
248
- generate_button = gr.Button("Generate JSON")
249
-
250
- with gr.Column():
251
- generated_output = gr.Textbox(label="Generated Output")
252
- json_output = gr.Textbox(label="JSON Output")
253
- json_download = gr.File(label="Download JSON")
254
-
255
- with gr.Row():
256
- with gr.Column():
257
- image_model = gr.Dropdown(choices=image_prompter, label="Image Model to prompt for", value="SDXL")
258
- generate_button_2 = gr.Button("Generate SDXL Prompt")
259
-
260
- with gr.Column():
261
- generated_output_2 = gr.Textbox(label="Generated SDXL Prompt")
262
-
263
- def update_models(api_key):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
  if api_key.startswith("sk-ant-"):
265
- return gr.Dropdown(choices=claude_models), gr.Textbox(label="Endpoint", value="https://api.anthropic.com")
266
- elif api_key.startswith("sk-"):
267
- return gr.Dropdown(choices=openai_models), gr.Textbox(label="Endpoint", value="https://api.openai.com/v1")
268
- else:
269
- return gr.Dropdown(choices=both_models), gr.Textbox(label="Endpoint", value="https://api.anthropic.com")
270
-
271
- api_key.change(update_models, inputs=api_key, outputs=[model_dropdown, endpoint])
272
-
273
- generate_button.click(generate_response, inputs=[endpoint, api_key, model_dropdown, user_prompt], outputs=[generated_output, json_output, json_download])
274
- generate_button_2.click(generate_second_response, inputs=[endpoint, api_key, model_dropdown, generated_output, image_model], outputs=generated_output_2)
275
- with gr.Tab("PNG Inject"):
276
- gr.Markdown("# PNG Inject")
277
- gr.Markdown("Upload a PNG image and inject JSON content into the PNG. PNG gets resized to 400x600 Center Crop.")
278
-
279
- with gr.Row():
280
- with gr.Column():
281
- image_input = gr.Image(type="filepath", label="Upload PNG Image")
282
- json_input = gr.Textbox(label="JSON Data")
283
- json_file_input = gr.File(label="Or Upload JSON File", file_types=[".json"])
284
- inject_button = gr.Button("Inject JSON and Download PNG")
285
-
286
- with gr.Column():
287
- injected_image_output = gr.File(label="Download Injected PNG")
288
-
289
- def inject_json(image, json_data, json_file):
290
- if json_file:
291
- jsonc = open(json_file,)
292
- json_data = json.load(jsonc)
293
- if image is None:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
294
  return None
295
- if json_data is None:
 
 
296
  return None
297
- injected_image = inject_json_to_png(image, json_data)
298
- return injected_image
 
 
 
 
299
 
300
- inject_button.click(inject_json, inputs=[image_input, json_input, json_file_input], outputs=injected_image_output)
 
 
301
 
302
- demo.launch()
 
 
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()