Zack3D commited on
Commit
8a5fbc3
Β·
verified Β·
1 Parent(s): 8c5533b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +128 -155
app.py CHANGED
@@ -1,26 +1,16 @@
1
  #!/usr/bin/env python3
2
  """
3
- SillyTavern Character‑Card Generator β€” version 2.0.1Β (AprΒ 2025)
4
  ──────────────────────────────────────────────────────────────
5
- Patch notes 2.0.1
6
- β€’ Fixes Gradio 4.43.x OpenAPI‑schema crash (TypeError: … 'bool' is not iterable)
7
- β€’ Adds `share=True` fallback so app still launches in sandboxed / cloud
8
- environments where localhost is not publicly reachable.
9
- β€’ Bumps the minimum Gradio to 4.44.1 (where the schema bug is fixed).
10
- β€’ Minor: logs version on start‑up.
11
-
12
- No functional behaviour changed β€” only the launch wrapper.
13
  """
14
 
15
  from __future__ import annotations
16
 
17
- import json
18
- import os
19
- import sys
20
- import uuid
21
  from dataclasses import dataclass
22
  from functools import cached_property
23
- from io import BytesIO
24
  from pathlib import Path
25
  from typing import Any, Dict, List, Tuple, Union
26
 
@@ -28,164 +18,147 @@ import gradio as gr
28
  from PIL import Image
29
  from PIL.PngImagePlugin import PngInfo
30
 
31
- # ─── Version & deps ─────────────────────────────────────────────────────────
32
- __version__ = "2.0.1"
33
 
34
- # Third‑party SDKs β€” optional -------------------------------------------------
35
- try:
36
- from anthropic import Anthropic, APITimeoutError as AnthropicTimeout
37
- except ImportError:
38
- Anthropic = None # type: ignore
39
-
40
- try:
41
- from openai import OpenAI, APITimeoutError as OpenAITimeout
42
- except ImportError:
43
- OpenAI = None # type: ignore
44
-
45
- ###############################################################################
46
- # Model catalog (unchanged)
47
- ###############################################################################
48
-
49
- CLAUDE_MODELS: List[str] = [
50
- "claude-3-opus-20240229",
51
- "claude-3-sonnet-20240229",
52
- "claude-3-haiku-20240307",
53
- "claude-3-5-sonnet-20240620",
54
- "claude-3-5-sonnet-20241022",
55
- "claude-3-5-haiku-20241022",
56
  "claude-3-7-sonnet-20250219",
57
  ]
58
-
59
- OPENAI_MODELS: List[str] = [
60
- "o3", "o3-mini", "o4-mini",
61
- "gpt-4.1", "gpt-4.1-mini", "gpt-4.1-nano",
62
- "gpt-4o", "gpt-4o-mini", "gpt-4", "gpt-4-32k",
63
- "gpt-4-0125-preview", "gpt-4-turbo-preview", "gpt-4-1106-preview",
64
- "gpt-3.5-turbo",
65
  ]
66
-
67
  ALL_MODELS = CLAUDE_MODELS + OPENAI_MODELS
68
- DEFAULT_ANTHROPIC_ENDPOINT = "https://api.anthropic.com"
69
- DEFAULT_OPENAI_ENDPOINT = "https://api.openai.com/v1"
70
-
71
- ###############################################################################
72
- # Helper classes (unchanged logic)
73
- ###############################################################################
74
-
75
- JsonDict = Dict[str, Any]
76
 
 
 
 
 
 
 
 
 
 
 
77
 
78
  @dataclass
79
  class APIConfig:
80
- endpoint: str
81
- api_key: str
82
- model: str
83
- temperature: float = 0.7
84
- top_p: float = 0.9
85
- thinking: bool = False
86
-
87
  @cached_property
88
- def provider(self) -> str:
89
- if self.model in CLAUDE_MODELS:
90
- return "anthropic"
91
- if self.model in OPENAI_MODELS:
92
- return "openai"
93
- raise ValueError(self.model)
94
-
95
  @cached_property
96
  def sdk(self):
97
- if self.provider == "anthropic":
98
- if Anthropic is None:
99
- raise RuntimeError("Install anthropic‑python for Claude models.")
100
- return Anthropic(api_key=self.api_key, base_url=self.endpoint)
101
- if self.provider == "openai":
102
- if OpenAI is None:
103
- raise RuntimeError("Install openai‑python for GPT/o‑series models.")
104
- return OpenAI(api_key=self.api_key, base_url=self.endpoint)
105
- raise AssertionError
106
-
107
- # ------------------------------------------------------------------
108
- def chat(self, user_prompt: str, system_prompt: str = "", max_tokens: int = 4096) -> str:
109
- if self.provider == "anthropic":
110
- opts = {
111
- "model": self.model,
112
- "system": system_prompt,
113
- "messages": [{"role": "user", "content": user_prompt}],
114
- "max_tokens": max_tokens,
115
- "temperature": self.temperature,
116
- "top_p": self.top_p,
117
- }
118
- if self.thinking:
119
- opts["vision"] = "detailed"
120
- resp = self.sdk.messages.create(**opts)
121
- return resp.content[0].text
122
-
123
- opts = {
124
- "model": self.model,
125
- "messages": [
126
- {"role": "system", "content": system_prompt},
127
- {"role": "user", "content": user_prompt},
128
- ],
129
- "max_tokens": max_tokens,
130
- "temperature": self.temperature,
131
- "top_p": self.top_p,
132
- }
133
- if self.thinking:
134
- opts["reasoning_mode"] = "enhanced"
135
- resp = self.sdk.chat.completions.create(**opts)
136
- return resp.choices[0].message.content
137
-
138
- ###############################################################################
139
- # Card helpers & PNG (unchanged)
140
- ###############################################################################
141
-
142
- CARD_REQUIRED_KEYS = {"char_name", "char_persona", "world_scenario", "char_greeting", "example_dialogue", "description"}
143
-
144
-
145
- def extract_card_json(text: str) -> Tuple[str | None, JsonDict | None]:
146
  try:
147
- body = text.replace("```json", "").replace("```", "").strip()
148
- raw = body[body.find("{") : body.rfind("}") + 1]
149
- data: JsonDict = json.loads(raw)
150
- data.update({
151
- "name": data["char_name"],
152
- "personality": data["char_persona"],
153
- "scenario": data["world_scenario"],
154
- "first_mes": data["char_greeting"],
155
- })
156
- return (json.dumps(data, ensure_ascii=False, indent=2), data) if CARD_REQUIRED_KEYS.issubset(data) else (None, None)
157
  except Exception:
158
- return None, None
159
-
160
-
161
- def inject_card_into_png(img_path: str | Path, card: Union[str, JsonDict]) -> Path:
162
- card_json: JsonDict = json.loads(card) if isinstance(card, str) else card
163
- img = Image.open(img_path)
164
- w, h = img.size
165
- target_ratio = 400 / 600
166
- if w / h > target_ratio:
167
- new_w = int(h * target_ratio); left = (w - new_w) // 2; img = img.crop((left, 0, left + new_w, h))
168
- else:
169
- new_h = int(w / target_ratio); top = (h - new_h) // 2; img = img.crop((0, top, w, top + new_h))
170
- img = img.resize((400, 600), Image.LANCZOS)
171
- meta = PngInfo(); meta.add_text("chara", json.dumps(card_json).encode().hex())
172
- out = Path(__file__).with_name("outputs"); out.mkdir(exist_ok=True)
173
- result = out / f"{card_json['name']}_{uuid.uuid4()}.png"
174
- img.save(result, "PNG", pnginfo=meta)
175
- return result
176
-
177
- ###############################################################################
178
- # Gradio UI (only change: launch params, show_api=False)
179
- ###############################################################################
180
-
181
- def build_ui() -> gr.Blocks:
182
  with gr.Blocks(title=f"SillyTavern Card Gen {__version__}") as demo:
183
  gr.Markdown(f"### πŸƒ SillyTavern Character Generator v{__version__}")
184
- # … UI body unchanged … (see previous version for full layout)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
 
186
  return demo
187
 
188
- ###############################################################################
189
- if __name__ == "__main__":
190
  print(f"== SillyTavern Card Gen {__version__} ==")
191
- build_ui().launch(share=True, show_api=False)
 
1
  #!/usr/bin/env python3
2
  """
3
+ SillyTavern Character‑Card Generator β€” version 2.0.2Β (AprΒ 2025)
4
  ──────────────────────────────────────────────────────────────
5
+ β€’ 2.0.1 accidentally stubbed‑out the UI; 2.0.2 restores the full layout.
6
+ β€’ Keeps the Gradio‑schema crash fix & `share=True` launch params.
 
 
 
 
 
 
7
  """
8
 
9
  from __future__ import annotations
10
 
11
+ import json, sys, uuid
 
 
 
12
  from dataclasses import dataclass
13
  from functools import cached_property
 
14
  from pathlib import Path
15
  from typing import Any, Dict, List, Tuple, Union
16
 
 
18
  from PIL import Image
19
  from PIL.PngImagePlugin import PngInfo
20
 
21
+ __version__ = "2.0.2"
 
22
 
23
+ # ─── Model lists ───────────────────────────────────────────────────────────
24
+ CLAUDE_MODELS = [
25
+ "claude-3-opus-20240229","claude-3-sonnet-20240229","claude-3-haiku-20240307",
26
+ "claude-3-5-sonnet-20240620","claude-3-5-sonnet-20241022","claude-3-5-haiku-20241022",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  "claude-3-7-sonnet-20250219",
28
  ]
29
+ OPENAI_MODELS = [
30
+ "o3","o3-mini","o4-mini","gpt-4.1","gpt-4.1-mini","gpt-4.1-nano",
31
+ "gpt-4o","gpt-4o-mini","gpt-4","gpt-4-32k","gpt-4-0125-preview",
32
+ "gpt-4-turbo-preview","gpt-4-1106-preview","gpt-3.5-turbo",
 
 
 
33
  ]
 
34
  ALL_MODELS = CLAUDE_MODELS + OPENAI_MODELS
35
+ DEFAULT_ANTHROPIC_ENDPOINT="https://api.anthropic.com"
36
+ DEFAULT_OPENAI_ENDPOINT="https://api.openai.com/v1"
 
 
 
 
 
 
37
 
38
+ # ─── API wrapper ───────────────────────────────────────────────────────────
39
+ JsonDict = Dict[str,Any]
40
+ try:
41
+ from anthropic import Anthropic,APITimeoutError as AnthropicTimeout
42
+ except ImportError:
43
+ Anthropic=None
44
+ try:
45
+ from openai import OpenAI,APITimeoutError as OpenAITimeout
46
+ except ImportError:
47
+ OpenAI=None
48
 
49
  @dataclass
50
  class APIConfig:
51
+ endpoint:str; api_key:str; model:str
52
+ temperature:float=0.7; top_p:float=0.9; thinking:bool=False
 
 
 
 
 
53
  @cached_property
54
+ def provider(self):
55
+ return "anthropic" if self.model in CLAUDE_MODELS else "openai"
 
 
 
 
 
56
  @cached_property
57
  def sdk(self):
58
+ if self.provider=="anthropic":
59
+ if not Anthropic: raise RuntimeError("install anthropic-python")
60
+ return Anthropic(api_key=self.api_key,base_url=self.endpoint)
61
+ if not OpenAI: raise RuntimeError("install openai-python")
62
+ return OpenAI(api_key=self.api_key,base_url=self.endpoint)
63
+ def chat(self,user:str,system:str="",max_tokens:int=4096)->str:
64
+ if self.provider=="anthropic":
65
+ args=dict(model=self.model,system=system,messages=[{"role":"user","content":user}],max_tokens=max_tokens,temperature=self.temperature,top_p=self.top_p)
66
+ if self.thinking: args["vision"]="detailed"
67
+ return self.sdk.messages.create(**args).content[0].text
68
+ args=dict(model=self.model,messages=[["system",system],["user",user]],max_tokens=max_tokens,temperature=self.temperature,top_p=self.top_p)
69
+ if self.thinking: args["reasoning_mode"]="enhanced"
70
+ return self.sdk.chat.completions.create(**args).choices[0].message.content
71
+
72
+ # ─── card helpers ──────────────────────────────────────────────────────────
73
+ CARD_REQUIRED={"char_name","char_persona","world_scenario","char_greeting","example_dialogue","description"}
74
+
75
+ def extract_card_json(txt:str)->Tuple[str|None,JsonDict|None]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  try:
77
+ body=txt.replace("```json","",1).replace("```","",1).strip()
78
+ raw=body[body.find("{"):body.rfind("}")+1]
79
+ data=json.loads(raw)
80
+ data.update(name=data["char_name"],personality=data["char_persona"],scenario=data["world_scenario"],first_mes=data["char_greeting"])
81
+ return (json.dumps(data,indent=2),data) if CARD_REQUIRED<=data.keys() else (None,None)
 
 
 
 
 
82
  except Exception:
83
+ return None,None
84
+
85
+ def inject_card_into_png(img_path:str,card:Union[str,JsonDict]):
86
+ card=json.loads(card) if isinstance(card,str) else card
87
+ img=Image.open(img_path)
88
+ w,h=img.size; ratio=400/600
89
+ img=img.crop(((w-int(h*ratio))//2,0,(w+int(h*ratio))//2,h)) if w/h>ratio else img.crop((0,(h-int(w/ratio))//2,w,(h+int(w/ratio))//2))
90
+ img=img.resize((400,600),Image.LANCZOS)
91
+ meta=PngInfo(); meta.add_text("chara",json.dumps(card).encode().hex())
92
+ out=Path(__file__).with_name("outputs"); out.mkdir(exist_ok=True)
93
+ dest=out/f"{card['name']}_{uuid.uuid4()}.png"
94
+ img.save(dest,"PNG",pnginfo=meta); return dest
95
+
96
+ # ─── Gradio UI ─────────────────────────────────────────────────────────────
97
+
98
+ def build_ui():
 
 
 
 
 
 
 
 
99
  with gr.Blocks(title=f"SillyTavern Card Gen {__version__}") as demo:
100
  gr.Markdown(f"### πŸƒ SillyTavern Character Generator v{__version__}")
101
+
102
+ with gr.Tab("JSON Generate"):
103
+ with gr.Row():
104
+ with gr.Column():
105
+ endpoint=gr.Textbox(label="Endpoint",value=DEFAULT_ANTHROPIC_ENDPOINT)
106
+ api_key=gr.Textbox(label="API Key",type="password")
107
+ model_dd=gr.Dropdown(ALL_MODELS,label="Model")
108
+ thinking=gr.Checkbox(label="Thinking mode")
109
+ temp=gr.Slider(0,1,0.7,label="Temperature")
110
+ topp=gr.Slider(0,1,0.9,label="Top‑P")
111
+ prompt=gr.Textbox(lines=5,label="Prompt")
112
+ gen=gr.Button("Generate JSON")
113
+ with gr.Column():
114
+ raw_out=gr.Textbox(label="Raw output")
115
+ json_out=gr.Textbox(label="Card JSON")
116
+ json_file=gr.File(label="Download .json")
117
+ with gr.Row():
118
+ img_model=gr.Dropdown(["SDXL","midjourney"],label="Image model",value="SDXL")
119
+ gen_img_prompt=gr.Button("Generate image prompt")
120
+ img_prompt_out=gr.Textbox(label="Image prompt")
121
+
122
+ with gr.Tab("PNG Inject"):
123
+ gr.Markdown("Upload an image and JSON to embed")
124
+ with gr.Row():
125
+ img_up=gr.Image(type="filepath")
126
+ json_text=gr.Textbox(label="JSON")
127
+ json_up=gr.File(label="Or upload .json",file_types=[".json"])
128
+ inject_btn=gr.Button("Inject")
129
+ png_out=gr.File(label="PNG with card")
130
+
131
+ # ── callbacks ───────────────────────────────────────────────────
132
+ def choose_endpoint(k):
133
+ return DEFAULT_ANTHROPIC_ENDPOINT if k.startswith("sk-ant-") else DEFAULT_OPENAI_ENDPOINT if k.startswith("sk-") else DEFAULT_ANTHROPIC_ENDPOINT
134
+ api_key.change(choose_endpoint,api_key,endpoint)
135
+
136
+ def generate_json(ep,k,m,think,t,p,user):
137
+ cfg=APIConfig(ep.strip(),k.strip(),m,t,p,think)
138
+ sys_prompt=Path(__file__).with_name("json.txt").read_text() if Path(__file__).with_name("json.txt").exists() else ""
139
+ out=cfg.chat(user,sys_prompt)
140
+ raw,parsed=extract_card_json(out)
141
+ if raw and parsed:
142
+ outdir=Path(__file__).with_name("outputs"); outdir.mkdir(exist_ok=True)
143
+ fp=outdir/f"{parsed['name']}_{uuid.uuid4()}.json"; fp.write_text(raw)
144
+ return out,raw,str(fp)
145
+ return out,"",None
146
+ gen.click(generate_json,[endpoint,api_key,model_dd,thinking,temp,topp,prompt],[raw_out,json_out,json_file])
147
+
148
+ def gen_img(ep,k,m,raw,image_model):
149
+ cfg=APIConfig(ep.strip(),k.strip(),m)
150
+ sys_prompt=Path(__file__).with_name(f"{image_model}.txt").read_text() if Path(__file__).with_name(f"{image_model}.txt").exists() else ""
151
+ return cfg.chat(raw,sys_prompt)
152
+ gen_img_prompt.click(gen_img,[endpoint,api_key,model_dd,raw_out,img_model],img_prompt_out)
153
+
154
+ def inject(image,json_str,json_path):
155
+ if json_path: json_str=Path(json_path).read_text()
156
+ return str(inject_card_into_png(image,json_str)) if image and json_str else None
157
+ inject_btn.click(inject,[img_up,json_text,json_up],png_out)
158
 
159
  return demo
160
 
161
+ # ─── main ───────────────────────────────────────────────────────────────────
162
+ if __name__=="__main__":
163
  print(f"== SillyTavern Card Gen {__version__} ==")
164
+ build_ui().launch(share=True,show_api=False)