Spaces:
Sleeping
Sleeping
Commit ·
f834d3b
1
Parent(s): d6f178c
Refactor app.py as modular
Browse files- app.py +26 -238
- data/save_system.py +35 -0
- examples/face_swap.py +277 -0
- examples/image-gen.py +436 -0
- game/llm_engine.py +52 -0
- game/player.py +24 -0
- game/translate.py +34 -0
- game/turn_engine.py +54 -0
- game/vlm_engine.py +39 -0
- systems/alignment_system.py +36 -0
- tmp/trans.py → systems/inventory_system.py +0 -0
- systems/journal_system.py +0 -0
- systems/memory_system.py +0 -0
- systems/quest_system.py +0 -0
- systems/xp_system.py +0 -0
app.py
CHANGED
|
@@ -8,6 +8,12 @@ from openai import OpenAI
|
|
| 8 |
from huggingface_hub import HfApi
|
| 9 |
from huggingface_hub import InferenceClient
|
| 10 |
from datasets import Dataset, load_dataset
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
print("APP STARTING...")
|
| 13 |
|
|
@@ -38,7 +44,6 @@ image_client = InferenceClient(
|
|
| 38 |
token=token
|
| 39 |
)
|
| 40 |
|
| 41 |
-
|
| 42 |
DATASET_REPO = f"{username}/ai-rpg-saves"
|
| 43 |
|
| 44 |
# ----------------------------
|
|
@@ -52,234 +57,6 @@ if os.path.exists(prompt_file_path):
|
|
| 52 |
else:
|
| 53 |
print("SYSTEM_PROMPT FILE DOES NOT EXIST")
|
| 54 |
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
# ----------------------------
|
| 58 |
-
# PLAYER INIT
|
| 59 |
-
# ----------------------------
|
| 60 |
-
|
| 61 |
-
def create_player(name, gender, avatar, language):
|
| 62 |
-
return {
|
| 63 |
-
"id": str(uuid.uuid4()),
|
| 64 |
-
"name": name,
|
| 65 |
-
"language": language,
|
| 66 |
-
"gender": gender,
|
| 67 |
-
"avatar": avatar,
|
| 68 |
-
"light_dark": 0, # -100 dark, +100 light
|
| 69 |
-
"order_chaos": 0, # -100 chaos, +100 order
|
| 70 |
-
"strength": 5,
|
| 71 |
-
"agility": 5,
|
| 72 |
-
"intelligence": 5,
|
| 73 |
-
"charisma": 5,
|
| 74 |
-
"willpower": 5,
|
| 75 |
-
}
|
| 76 |
-
|
| 77 |
-
# ----------------------------
|
| 78 |
-
# SAVE / LOAD (HF DATASET)
|
| 79 |
-
# ----------------------------
|
| 80 |
-
|
| 81 |
-
def save_to_hf(state_json, history_json):
|
| 82 |
-
data = {
|
| 83 |
-
"state": [state_json],
|
| 84 |
-
"history": [history_json]
|
| 85 |
-
}
|
| 86 |
-
|
| 87 |
-
ds = Dataset.from_dict(data)
|
| 88 |
-
|
| 89 |
-
ds.push_to_hub(
|
| 90 |
-
DATASET_REPO,
|
| 91 |
-
private=True
|
| 92 |
-
)
|
| 93 |
-
|
| 94 |
-
return "Game Saved ☁️"
|
| 95 |
-
|
| 96 |
-
def load_from_hf():
|
| 97 |
-
ds = load_dataset(
|
| 98 |
-
DATASET_REPO,
|
| 99 |
-
split="train"
|
| 100 |
-
)
|
| 101 |
-
|
| 102 |
-
state_json = ds[0]["state"]
|
| 103 |
-
history_json = ds[0]["history"]
|
| 104 |
-
|
| 105 |
-
return state_json, history_json
|
| 106 |
-
|
| 107 |
-
# ----------------------------
|
| 108 |
-
# LLM STREAM
|
| 109 |
-
# ----------------------------
|
| 110 |
-
|
| 111 |
-
def generate_stream(message, history, state):
|
| 112 |
-
player_lang = state.get("language", "English")
|
| 113 |
-
|
| 114 |
-
struct_instruction = f"""
|
| 115 |
-
\n\nCRITICAL RULE 1: You MUST write your entire response, all storytelling, dialogue, and exactly 5 choices EXCLUSIVELY in {player_lang}.
|
| 116 |
-
CRITICAL RULE 2: At the very end of your response, you MUST append a hidden visual description for the image generator. Use EXACTLY this format (raw JSON only, no markdown):
|
| 117 |
-
<SCENE_DATA>
|
| 118 |
-
{{"image_prompt": "A highly detailed, purely visual description of the current scene, focusing on characters, lighting, and environment.", "location": "Name of the current place"}}
|
| 119 |
-
</SCENE_DATA>
|
| 120 |
-
"""
|
| 121 |
-
|
| 122 |
-
dynamic_system_prompt = SYSTEM_PROMPT + struct_instruction
|
| 123 |
-
|
| 124 |
-
context = f"Player State:\n{json.dumps(state, indent=2)}"
|
| 125 |
-
|
| 126 |
-
messages = [
|
| 127 |
-
{"role": "system", "content": dynamic_system_prompt},
|
| 128 |
-
{"role": "system", "content": context},
|
| 129 |
-
]
|
| 130 |
-
|
| 131 |
-
for h in history:
|
| 132 |
-
messages.append({"role": "user", "content": h[0]})
|
| 133 |
-
messages.append({"role": "assistant", "content": h[1]})
|
| 134 |
-
|
| 135 |
-
messages.append({"role": "user", "content": message})
|
| 136 |
-
|
| 137 |
-
stream = text_client.chat.completions.create(
|
| 138 |
-
model=TEXT_MODEL_NAME,
|
| 139 |
-
messages=messages,
|
| 140 |
-
temperature=0.95,
|
| 141 |
-
top_p=0.9,
|
| 142 |
-
presence_penalty=0.6,
|
| 143 |
-
frequency_penalty=0.4,
|
| 144 |
-
max_tokens=1200,
|
| 145 |
-
stream=True,
|
| 146 |
-
)
|
| 147 |
-
|
| 148 |
-
partial = ""
|
| 149 |
-
for chunk in stream:
|
| 150 |
-
if chunk.choices and chunk.choices[0].delta.content:
|
| 151 |
-
partial += chunk.choices[0].delta.content
|
| 152 |
-
display_text = partial.split("<SCENE_DATA>")[0].strip() # JSON bloğunu UI'dan gizlemek için ayırıyoruz
|
| 153 |
-
yield display_text, partial
|
| 154 |
-
|
| 155 |
-
# ----------------------------
|
| 156 |
-
# IMAGE GENERATION
|
| 157 |
-
# ----------------------------
|
| 158 |
-
|
| 159 |
-
def extract_scene_data(full_text):
|
| 160 |
-
"""LLM çıktısındaki gizli SCENE_DATA bloğunu bulur ve JSON'a çevirir."""
|
| 161 |
-
pattern = r"<SCENE_DATA>(.*?)</SCENE_DATA>"
|
| 162 |
-
match = re.search(pattern, full_text, re.DOTALL | re.IGNORECASE)
|
| 163 |
-
|
| 164 |
-
if match:
|
| 165 |
-
json_str = match.group(1).strip()
|
| 166 |
-
# Llama bazen markdown (```json) ekleyebilir, onu temizliyoruz
|
| 167 |
-
json_str = json_str.replace("```json", "").replace("```", "").strip()
|
| 168 |
-
try:
|
| 169 |
-
return json.loads(json_str)
|
| 170 |
-
except Exception as e:
|
| 171 |
-
print(f"JSON Parse Error: {e}")
|
| 172 |
-
return None
|
| 173 |
-
|
| 174 |
-
def generate_scene_image_structured(scene_data):
|
| 175 |
-
"""Sadece LLM'den gelen saf 'image_prompt'u kullanarak görsel üretir."""
|
| 176 |
-
if not scene_data or "image_prompt" not in scene_data:
|
| 177 |
-
return None
|
| 178 |
-
|
| 179 |
-
# Ana Star Wars şablonumuz
|
| 180 |
-
base_prompt = "Cinematic Star Wars concept art, highly detailed, dark atmosphere, dramatic lighting, 8k resolution, masterpiece. "
|
| 181 |
-
final_prompt = base_prompt + scene_data["image_prompt"]
|
| 182 |
-
|
| 183 |
-
try:
|
| 184 |
-
image = image_client.text_to_image(final_prompt)
|
| 185 |
-
return image
|
| 186 |
-
except Exception as e:
|
| 187 |
-
print(f"Image generation failed via Router: {e}")
|
| 188 |
-
return None
|
| 189 |
-
|
| 190 |
-
#def generate_scene_image(ai_story_text):
|
| 191 |
-
# if not ai_story_text:
|
| 192 |
-
# return None
|
| 193 |
-
#
|
| 194 |
-
# base_prompt = "Cinematic Star Wars concept art, highly detailed, dark atmosphere, dramatic lighting, 8k resolution, masterpiece. "
|
| 195 |
-
# scene_description = ai_story_text[:400].replace("\n", " ").replace('"', "'")
|
| 196 |
-
# final_prompt = base_prompt + scene_description
|
| 197 |
-
#
|
| 198 |
-
# try:
|
| 199 |
-
# image = image_client.text_to_image(final_prompt)
|
| 200 |
-
# return image
|
| 201 |
-
# except Exception as e:
|
| 202 |
-
# print(f"Image generation failed via correct Router: {e}")
|
| 203 |
-
# return None
|
| 204 |
-
|
| 205 |
-
# ----------------------------
|
| 206 |
-
# ALIGNMENT STREAM
|
| 207 |
-
# ----------------------------
|
| 208 |
-
|
| 209 |
-
def evaluate_alignment(user_input, last_context=""):
|
| 210 |
-
eval_prompt = f"""You are a hidden Game Master evaluating player morality in a Star Wars RPG.
|
| 211 |
-
Story Context: "{last_context[-200:]}"
|
| 212 |
-
Player Action: "{user_input}"
|
| 213 |
-
|
| 214 |
-
Evaluate the alignment shift for this action.
|
| 215 |
-
Light/Dark: + (Light: compassion, healing) to - (Dark: murder, selfishness, anger).
|
| 216 |
-
Order/Chaos: + (Order: following rules, loyalty) to - (Chaos: rebellion, deception, breaking laws).
|
| 217 |
-
|
| 218 |
-
Score both from -5 to +5.
|
| 219 |
-
Return ONLY two integers separated by a comma. NO other text.
|
| 220 |
-
Example: -3, 2
|
| 221 |
-
"""
|
| 222 |
-
try:
|
| 223 |
-
# Arka planda hızlıca puanlama yapması için temperature=0 kullanıyoruz
|
| 224 |
-
response = text_client.chat.completions.create(
|
| 225 |
-
model=TEXT_MODEL_NAME,
|
| 226 |
-
messages=[{"role": "user", "content": eval_prompt}],
|
| 227 |
-
max_tokens=10,
|
| 228 |
-
temperature=0.0
|
| 229 |
-
)
|
| 230 |
-
result = response.choices[0].message.content.strip()
|
| 231 |
-
ld_shift, oc_shift = result.split(',')
|
| 232 |
-
return int(ld_shift.strip()), int(oc_shift.strip())
|
| 233 |
-
except Exception as e:
|
| 234 |
-
print("Alignment parsing error:", e)
|
| 235 |
-
return 0, 0 # Hata olursa puanı değiştirme
|
| 236 |
-
|
| 237 |
-
# ----------------------------
|
| 238 |
-
# GAME TURN
|
| 239 |
-
# ----------------------------
|
| 240 |
-
|
| 241 |
-
def play_turn(user_input, history, state_json):
|
| 242 |
-
if history is None:
|
| 243 |
-
history = []
|
| 244 |
-
|
| 245 |
-
state = json.loads(state_json)
|
| 246 |
-
|
| 247 |
-
last_ai_message = history[-1][1] if len(history) > 0 else ""
|
| 248 |
-
|
| 249 |
-
history.append([user_input, ""])
|
| 250 |
-
|
| 251 |
-
full_ai_response = ""
|
| 252 |
-
for display_text, full_text in generate_stream(user_input, history[:-1], state):
|
| 253 |
-
history[-1][1] = display_text
|
| 254 |
-
full_ai_response = full_text # Gizli JSON dahil tam metni arka planda tutuyoruz
|
| 255 |
-
yield (
|
| 256 |
-
history,
|
| 257 |
-
json.dumps(state),
|
| 258 |
-
state["light_dark"],
|
| 259 |
-
state["order_chaos"],
|
| 260 |
-
gr.update()
|
| 261 |
-
)
|
| 262 |
-
|
| 263 |
-
# UI'da görünen temiz metni alignment için değerlendiriyoruz
|
| 264 |
-
if user_input.strip() and "Start the adventure" not in user_input:
|
| 265 |
-
ld_shift, oc_shift = evaluate_alignment(user_input, last_ai_message)
|
| 266 |
-
state["light_dark"] = max(-100, min(100, state["light_dark"] + ld_shift))
|
| 267 |
-
state["order_chaos"] = max(-100, min(100, state["order_chaos"] + oc_shift))
|
| 268 |
-
|
| 269 |
-
# YENİ: Arka planda biriken tam metinden JSON'u çıkarıyoruz
|
| 270 |
-
scene_data = extract_scene_data(full_ai_response)
|
| 271 |
-
|
| 272 |
-
# JSON başarıyla alındıysa görseli üretiyoruz
|
| 273 |
-
new_image = generate_scene_image_structured(scene_data) if scene_data else None
|
| 274 |
-
|
| 275 |
-
yield (
|
| 276 |
-
history,
|
| 277 |
-
json.dumps(state),
|
| 278 |
-
state["light_dark"],
|
| 279 |
-
state["order_chaos"],
|
| 280 |
-
new_image if new_image else gr.update()
|
| 281 |
-
)
|
| 282 |
-
|
| 283 |
# ----------------------------
|
| 284 |
# UI
|
| 285 |
# ----------------------------
|
|
@@ -454,8 +231,19 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
|
| 454 |
None
|
| 455 |
)
|
| 456 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 457 |
def first_turn(history, state_json):
|
| 458 |
-
yield from play_turn("Start the adventure. Describe my surroundings and give me 5 choices.", history, state_json)
|
| 459 |
|
| 460 |
finish_stats.click(
|
| 461 |
start_game,
|
|
@@ -470,7 +258,7 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
|
| 470 |
)
|
| 471 |
|
| 472 |
send_btn.click(
|
| 473 |
-
|
| 474 |
inputs=[custom_input, chatbot, player_state],
|
| 475 |
outputs=[chatbot, player_state, light_dark_slider, order_chaos_slider, scene_image],
|
| 476 |
api_name=False
|
|
@@ -479,15 +267,15 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
|
| 479 |
choice_outputs = [chatbot, player_state, light_dark_slider, order_chaos_slider, scene_image]
|
| 480 |
|
| 481 |
def choice1_fn(history, state):
|
| 482 |
-
yield from
|
| 483 |
def choice2_fn(history, state):
|
| 484 |
-
yield from
|
| 485 |
def choice3_fn(history, state):
|
| 486 |
-
yield from
|
| 487 |
def choice4_fn(history, state):
|
| 488 |
-
yield from
|
| 489 |
def choice5_fn(history, state):
|
| 490 |
-
yield from
|
| 491 |
|
| 492 |
choice1.click(choice1_fn, inputs=[chatbot, player_state], outputs=choice_outputs, api_name=False)
|
| 493 |
choice2.click(choice2_fn, inputs=[chatbot, player_state], outputs=choice_outputs, api_name=False)
|
|
@@ -496,10 +284,10 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
|
| 496 |
choice5.click(choice5_fn, inputs=[chatbot, player_state], outputs=choice_outputs, api_name=False)
|
| 497 |
|
| 498 |
def save_wrapper(state_json, history_list):
|
| 499 |
-
return save_to_hf(state_json, json.dumps(history_list))
|
| 500 |
|
| 501 |
def load_wrapper():
|
| 502 |
-
state_json, history_json = load_from_hf()
|
| 503 |
return state_json, json.loads(history_json)
|
| 504 |
|
| 505 |
save_btn.click(
|
|
|
|
| 8 |
from huggingface_hub import HfApi
|
| 9 |
from huggingface_hub import InferenceClient
|
| 10 |
from datasets import Dataset, load_dataset
|
| 11 |
+
from game.player import create_player
|
| 12 |
+
from game.turn_engine import play_turn
|
| 13 |
+
from systems.alignment_system import evaluate_alignment
|
| 14 |
+
from game.llm_engine import generate_llm_stream
|
| 15 |
+
from game.vlm_engine import extract_scene_data, generate_scene_image_structured
|
| 16 |
+
from data.save_system import save_to_hf, load_from_hf
|
| 17 |
|
| 18 |
print("APP STARTING...")
|
| 19 |
|
|
|
|
| 44 |
token=token
|
| 45 |
)
|
| 46 |
|
|
|
|
| 47 |
DATASET_REPO = f"{username}/ai-rpg-saves"
|
| 48 |
|
| 49 |
# ----------------------------
|
|
|
|
| 57 |
else:
|
| 58 |
print("SYSTEM_PROMPT FILE DOES NOT EXIST")
|
| 59 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
# ----------------------------
|
| 61 |
# UI
|
| 62 |
# ----------------------------
|
|
|
|
| 231 |
None
|
| 232 |
)
|
| 233 |
|
| 234 |
+
def play_turn_wrapper(user_input, history, state_json):
|
| 235 |
+
return play_turn(
|
| 236 |
+
user_input,
|
| 237 |
+
history,
|
| 238 |
+
state_json,
|
| 239 |
+
text_client,
|
| 240 |
+
image_client,
|
| 241 |
+
TEXT_MODEL_NAME,
|
| 242 |
+
SYSTEM_PROMPT
|
| 243 |
+
)
|
| 244 |
+
|
| 245 |
def first_turn(history, state_json):
|
| 246 |
+
yield from play_turn("Start the adventure. Describe my surroundings and give me 5 choices.", history, state_json, text_client, image_client, TEXT_MODEL_NAME, SYSTEM_PROMPT)
|
| 247 |
|
| 248 |
finish_stats.click(
|
| 249 |
start_game,
|
|
|
|
| 258 |
)
|
| 259 |
|
| 260 |
send_btn.click(
|
| 261 |
+
play_turn_wrapper,
|
| 262 |
inputs=[custom_input, chatbot, player_state],
|
| 263 |
outputs=[chatbot, player_state, light_dark_slider, order_chaos_slider, scene_image],
|
| 264 |
api_name=False
|
|
|
|
| 267 |
choice_outputs = [chatbot, player_state, light_dark_slider, order_chaos_slider, scene_image]
|
| 268 |
|
| 269 |
def choice1_fn(history, state):
|
| 270 |
+
yield from play_turn_wrapper("I choose option 1.", history, state)
|
| 271 |
def choice2_fn(history, state):
|
| 272 |
+
yield from play_turn_wrapper("I choose option 2.", history, state)
|
| 273 |
def choice3_fn(history, state):
|
| 274 |
+
yield from play_turn_wrapper("I choose option 3.", history, state)
|
| 275 |
def choice4_fn(history, state):
|
| 276 |
+
yield from play_turn_wrapper("I choose option 4.", history, state)
|
| 277 |
def choice5_fn(history, state):
|
| 278 |
+
yield from play_turn_wrapper("I choose option 5.", history, state)
|
| 279 |
|
| 280 |
choice1.click(choice1_fn, inputs=[chatbot, player_state], outputs=choice_outputs, api_name=False)
|
| 281 |
choice2.click(choice2_fn, inputs=[chatbot, player_state], outputs=choice_outputs, api_name=False)
|
|
|
|
| 284 |
choice5.click(choice5_fn, inputs=[chatbot, player_state], outputs=choice_outputs, api_name=False)
|
| 285 |
|
| 286 |
def save_wrapper(state_json, history_list):
|
| 287 |
+
return save_to_hf(DATASET_REPO, state_json, json.dumps(history_list))
|
| 288 |
|
| 289 |
def load_wrapper():
|
| 290 |
+
state_json, history_json = load_from_hf(DATASET_REPO)
|
| 291 |
return state_json, json.loads(history_json)
|
| 292 |
|
| 293 |
save_btn.click(
|
data/save_system.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
import uuid
|
| 4 |
+
import time
|
| 5 |
+
import re
|
| 6 |
+
from datasets import Dataset, load_dataset
|
| 7 |
+
# ----------------------------
|
| 8 |
+
# SAVE / LOAD (HF DATASET)
|
| 9 |
+
# ----------------------------
|
| 10 |
+
|
| 11 |
+
def save_to_hf(dataset_repo, state_json, history_json):
|
| 12 |
+
data = {
|
| 13 |
+
"state": [state_json],
|
| 14 |
+
"history": [history_json]
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
ds = Dataset.from_dict(data)
|
| 18 |
+
|
| 19 |
+
ds.push_to_hub(
|
| 20 |
+
dataset_repo,
|
| 21 |
+
private=True
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
return "Game Saved ☁️"
|
| 25 |
+
|
| 26 |
+
def load_from_hf(dataset_repo):
|
| 27 |
+
ds = load_dataset(
|
| 28 |
+
dataset_repo,
|
| 29 |
+
split="train"
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
state_json = ds[0]["state"]
|
| 33 |
+
history_json = ds[0]["history"]
|
| 34 |
+
|
| 35 |
+
return state_json, history_json
|
examples/face_swap.py
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import gradio as gr
|
| 3 |
+
import numpy as np
|
| 4 |
+
import random
|
| 5 |
+
import spaces
|
| 6 |
+
import torch
|
| 7 |
+
from diffusers import Flux2KleinPipeline
|
| 8 |
+
from PIL import Image
|
| 9 |
+
|
| 10 |
+
dtype = torch.bfloat16
|
| 11 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 12 |
+
|
| 13 |
+
MAX_SEED = np.iinfo(np.int32).max
|
| 14 |
+
|
| 15 |
+
# Model repository ID for 9B distilled
|
| 16 |
+
REPO_ID_DISTILLED = "black-forest-labs/FLUX.2-klein-9B"
|
| 17 |
+
|
| 18 |
+
# LoRA repository and file
|
| 19 |
+
LORA_REPO_ID = "Alissonerdx/BFS-Best-Face-Swap"
|
| 20 |
+
LORA_FILENAME = "bfs_head_v1_flux-klein_9b_step3750_rank64.safetensors"
|
| 21 |
+
|
| 22 |
+
# Fixed prompt for face swapping
|
| 23 |
+
#FACE_SWAP_PROMPT = "head_swap: start with Picture 1 as the base image, keeping its lighting, environment, and background. remove the head from Picture 1 completely and replace it with the head from Picture 2, strictly preserving the hair, eye color, nose structure of Picture 2. copy the direction of the eye, head rotation, micro expressions from Picture 1, high quality, sharp details, 4k."
|
| 24 |
+
|
| 25 |
+
FACE_SWAP_PROMPT = """head_swap: start with Picture 1 as the base image, keeping its lighting, environment, and background. Remove the head from Picture 1 completely and replace it with the head from Picture 2.
|
| 26 |
+
FROM PICTURE 1 (strictly preserve):
|
| 27 |
+
- Scene: lighting conditions, shadows, highlights, color temperature, environment, background
|
| 28 |
+
- Head positioning: exact rotation angle, tilt, direction the head is facing
|
| 29 |
+
- Expression: facial expression, micro-expressions, eye gaze direction, mouth position, emotion
|
| 30 |
+
FROM PICTURE 2 (strictly preserve identity):
|
| 31 |
+
- Facial structure: face shape, bone structure, jawline, chin
|
| 32 |
+
- All facial features: eye color, eye shape, nose structure, lip shape and fullness, eyebrows
|
| 33 |
+
- Hair: color, style, texture, hairline
|
| 34 |
+
- Skin: texture, tone, complexion
|
| 35 |
+
The replaced head must seamlessly match Picture 1's lighting and expression while maintaining the complete identity from Picture 2. High quality, photorealistic, sharp details, 4k."""
|
| 36 |
+
|
| 37 |
+
print("Loading FLUX.2 Klein 9B Distilled model...")
|
| 38 |
+
pipe = Flux2KleinPipeline.from_pretrained(REPO_ID_DISTILLED, torch_dtype=dtype)
|
| 39 |
+
pipe.to(device)
|
| 40 |
+
|
| 41 |
+
print(f"Loading LoRA from {LORA_REPO_ID}...")
|
| 42 |
+
pipe.load_lora_weights(LORA_REPO_ID, weight_name=LORA_FILENAME)
|
| 43 |
+
print("LoRA loaded successfully!")
|
| 44 |
+
|
| 45 |
+
def update_dimensions_from_image(target_image):
|
| 46 |
+
"""
|
| 47 |
+
Update width/height based on target image aspect ratio.
|
| 48 |
+
Keeps one side at 1024 and scales the other proportionally,
|
| 49 |
+
with both sides as multiples of 8.
|
| 50 |
+
Args:
|
| 51 |
+
target_image: PIL Image of the target/body image.
|
| 52 |
+
Returns:
|
| 53 |
+
tuple: A tuple of (width, height) integers, both multiples of 8.
|
| 54 |
+
"""
|
| 55 |
+
if target_image is None:
|
| 56 |
+
return 1024, 1024 # Default dimensions
|
| 57 |
+
|
| 58 |
+
img_width, img_height = target_image.size
|
| 59 |
+
|
| 60 |
+
aspect_ratio = img_width / img_height
|
| 61 |
+
|
| 62 |
+
if aspect_ratio >= 1: # Landscape or square
|
| 63 |
+
new_width = 1024
|
| 64 |
+
new_height = int(1024 / aspect_ratio)
|
| 65 |
+
else: # Portrait
|
| 66 |
+
new_height = 1024
|
| 67 |
+
new_width = int(1024 * aspect_ratio)
|
| 68 |
+
|
| 69 |
+
# Round to nearest multiple of 8
|
| 70 |
+
new_width = round(new_width / 8) * 8
|
| 71 |
+
new_height = round(new_height / 8) * 8
|
| 72 |
+
|
| 73 |
+
# Ensure within valid range (minimum 256, maximum 1024)
|
| 74 |
+
new_width = max(256, min(1024, new_width))
|
| 75 |
+
new_height = max(256, min(1024, new_height))
|
| 76 |
+
|
| 77 |
+
return new_width, new_height
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
@spaces.GPU(duration=45)
|
| 81 |
+
def face_swap(
|
| 82 |
+
reference_face: Image.Image,
|
| 83 |
+
target_image: Image.Image,
|
| 84 |
+
seed: int = 42,
|
| 85 |
+
randomize_seed: bool = False,
|
| 86 |
+
width: int = 1024,
|
| 87 |
+
height: int = 1024,
|
| 88 |
+
num_inference_steps: int = 4,
|
| 89 |
+
guidance_scale: float = 1.0,
|
| 90 |
+
progress=gr.Progress(track_tqdm=True)
|
| 91 |
+
):
|
| 92 |
+
"""
|
| 93 |
+
Perform face swapping using FLUX.2 Klein 9B with LoRA.
|
| 94 |
+
Args:
|
| 95 |
+
reference_face: The face image to swap in (Picture 2).
|
| 96 |
+
target_image: The target body/base image (Picture 1).
|
| 97 |
+
seed: Random seed for reproducible generation.
|
| 98 |
+
randomize_seed: Set to True to use a random seed.
|
| 99 |
+
width: Output image width in pixels (256-1024, must be multiple of 8).
|
| 100 |
+
height: Output image height in pixels (256-1024, must be multiple of 8).
|
| 101 |
+
num_inference_steps: Number of denoising steps (default 4 for distilled).
|
| 102 |
+
guidance_scale: How closely to follow the prompt (default 1.0 for distilled).
|
| 103 |
+
Returns:
|
| 104 |
+
tuple: A tuple containing the generated PIL Image and the seed used.
|
| 105 |
+
"""
|
| 106 |
+
if reference_face is None or target_image is None:
|
| 107 |
+
raise gr.Error("Please provide both a reference face and a target image!")
|
| 108 |
+
|
| 109 |
+
if randomize_seed:
|
| 110 |
+
seed = random.randint(0, MAX_SEED)
|
| 111 |
+
|
| 112 |
+
generator = torch.Generator(device=device).manual_seed(seed)
|
| 113 |
+
|
| 114 |
+
# Important: Pass target image (body) first, then reference face
|
| 115 |
+
# This matches the prompt structure: Picture 1 = target, Picture 2 = reference
|
| 116 |
+
image_list = [target_image, reference_face]
|
| 117 |
+
|
| 118 |
+
progress(0.2, desc="Swapping face...")
|
| 119 |
+
|
| 120 |
+
image = pipe(
|
| 121 |
+
prompt=FACE_SWAP_PROMPT,
|
| 122 |
+
image=image_list,
|
| 123 |
+
height=height,
|
| 124 |
+
width=width,
|
| 125 |
+
num_inference_steps=num_inference_steps,
|
| 126 |
+
guidance_scale=guidance_scale,
|
| 127 |
+
generator=generator,
|
| 128 |
+
).images[0]
|
| 129 |
+
|
| 130 |
+
# Return slider comparison (before, after) and seed
|
| 131 |
+
return (target_image, image), seed
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
css = """
|
| 135 |
+
#col-container {
|
| 136 |
+
margin: 0 auto;
|
| 137 |
+
max-width: 1200px;
|
| 138 |
+
}
|
| 139 |
+
.image-container img {
|
| 140 |
+
object-fit: contain;
|
| 141 |
+
}
|
| 142 |
+
"""
|
| 143 |
+
|
| 144 |
+
with gr.Blocks(css=css) as demo:
|
| 145 |
+
|
| 146 |
+
with gr.Column(elem_id="col-container"):
|
| 147 |
+
gr.Markdown("""# Face Swap with FLUX.2 Klein 9B
|
| 148 |
+
Swap faces using Flux.2 Klein 9B [Alissonerdx/BFS-Best-Face-Swap](https://huggingface.co/Alissonerdx/BFS-Best-Face-Swap) LoRA
|
| 149 |
+
""")
|
| 150 |
+
|
| 151 |
+
with gr.Row():
|
| 152 |
+
with gr.Column():
|
| 153 |
+
with gr.Row():
|
| 154 |
+
reference_face = gr.Image(
|
| 155 |
+
label="Reference Face",
|
| 156 |
+
type="pil",
|
| 157 |
+
sources=["upload"],
|
| 158 |
+
elem_classes="image-container"
|
| 159 |
+
)
|
| 160 |
+
|
| 161 |
+
target_image = gr.Image(
|
| 162 |
+
label="Target Image (Body/Scene)",
|
| 163 |
+
type="pil",
|
| 164 |
+
sources=["upload"],
|
| 165 |
+
elem_classes="image-container"
|
| 166 |
+
)
|
| 167 |
+
run_button = gr.Button("Swap Face", visible=False)
|
| 168 |
+
with gr.Accordion("Advanced Settings", open=False):
|
| 169 |
+
seed = gr.Slider(
|
| 170 |
+
label="Seed",
|
| 171 |
+
minimum=0,
|
| 172 |
+
maximum=MAX_SEED,
|
| 173 |
+
step=1,
|
| 174 |
+
value=0,
|
| 175 |
+
)
|
| 176 |
+
|
| 177 |
+
randomize_seed = gr.Checkbox(label="Randomize seed", value=True)
|
| 178 |
+
|
| 179 |
+
with gr.Row():
|
| 180 |
+
width = gr.Slider(
|
| 181 |
+
label="Width",
|
| 182 |
+
minimum=256,
|
| 183 |
+
maximum=1024,
|
| 184 |
+
step=8,
|
| 185 |
+
value=1024,
|
| 186 |
+
)
|
| 187 |
+
|
| 188 |
+
height = gr.Slider(
|
| 189 |
+
label="Height",
|
| 190 |
+
minimum=256,
|
| 191 |
+
maximum=1024,
|
| 192 |
+
step=8,
|
| 193 |
+
value=1024,
|
| 194 |
+
)
|
| 195 |
+
|
| 196 |
+
with gr.Row():
|
| 197 |
+
num_inference_steps = gr.Slider(
|
| 198 |
+
label="Inference Steps",
|
| 199 |
+
minimum=1,
|
| 200 |
+
maximum=20,
|
| 201 |
+
step=1,
|
| 202 |
+
value=4,
|
| 203 |
+
info="Number of denoising steps (4 is optimal for distilled model)"
|
| 204 |
+
)
|
| 205 |
+
|
| 206 |
+
guidance_scale = gr.Slider(
|
| 207 |
+
label="Guidance Scale",
|
| 208 |
+
minimum=0.0,
|
| 209 |
+
maximum=5.0,
|
| 210 |
+
step=0.1,
|
| 211 |
+
value=1.0,
|
| 212 |
+
info="How closely to follow the prompt (1.0 is optimal for distilled model)"
|
| 213 |
+
)
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
comparison_slider = gr.ImageSlider(
|
| 217 |
+
label="Before / After",
|
| 218 |
+
type="pil"
|
| 219 |
+
)
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
seed_output = gr.Number(label="Seed Used", visible=False)
|
| 225 |
+
|
| 226 |
+
# Auto-update dimensions when target image is uploaded
|
| 227 |
+
target_image.upload(
|
| 228 |
+
fn=update_dimensions_from_image,
|
| 229 |
+
inputs=[target_image],
|
| 230 |
+
outputs=[width, height]
|
| 231 |
+
)
|
| 232 |
+
|
| 233 |
+
# Create a shared input/output configuration
|
| 234 |
+
swap_inputs = [
|
| 235 |
+
reference_face,
|
| 236 |
+
target_image,
|
| 237 |
+
seed,
|
| 238 |
+
randomize_seed,
|
| 239 |
+
width,
|
| 240 |
+
height,
|
| 241 |
+
num_inference_steps,
|
| 242 |
+
guidance_scale
|
| 243 |
+
]
|
| 244 |
+
swap_outputs = [comparison_slider, seed_output]
|
| 245 |
+
|
| 246 |
+
# Manual trigger via button
|
| 247 |
+
run_button.click(
|
| 248 |
+
fn=face_swap,
|
| 249 |
+
inputs=swap_inputs,
|
| 250 |
+
outputs=swap_outputs,
|
| 251 |
+
)
|
| 252 |
+
|
| 253 |
+
# Auto-trigger when both images are uploaded
|
| 254 |
+
def auto_swap_wrapper(ref_face, target_img, s, rand_s, w, h, steps, cfg):
|
| 255 |
+
"""Only run face swap if both images are provided"""
|
| 256 |
+
if ref_face is not None and target_img is not None:
|
| 257 |
+
result = face_swap(ref_face, target_img, s, rand_s, w, h, steps, cfg)
|
| 258 |
+
# Show the button after first generation
|
| 259 |
+
return result[0], result[1], gr.update(visible=True)
|
| 260 |
+
return None, s, gr.update(visible=False)
|
| 261 |
+
|
| 262 |
+
# Trigger on reference face upload/change
|
| 263 |
+
reference_face.change(
|
| 264 |
+
fn=auto_swap_wrapper,
|
| 265 |
+
inputs=swap_inputs,
|
| 266 |
+
outputs=[comparison_slider, seed_output, run_button],
|
| 267 |
+
)
|
| 268 |
+
|
| 269 |
+
# Trigger on target image upload/change
|
| 270 |
+
target_image.change(
|
| 271 |
+
fn=auto_swap_wrapper,
|
| 272 |
+
inputs=swap_inputs,
|
| 273 |
+
outputs=[comparison_slider, seed_output, run_button],
|
| 274 |
+
)
|
| 275 |
+
|
| 276 |
+
if __name__ == "__main__":
|
| 277 |
+
demo.launch(share=True, theme=gr.themes.Citrus())
|
examples/image-gen.py
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import subprocess
|
| 3 |
+
import sys
|
| 4 |
+
import io
|
| 5 |
+
import gradio as gr
|
| 6 |
+
import numpy as np
|
| 7 |
+
import random
|
| 8 |
+
import spaces
|
| 9 |
+
import torch
|
| 10 |
+
from diffusers import Flux2KleinPipeline
|
| 11 |
+
import requests
|
| 12 |
+
from PIL import Image
|
| 13 |
+
import json
|
| 14 |
+
import base64
|
| 15 |
+
from huggingface_hub import InferenceClient
|
| 16 |
+
|
| 17 |
+
dtype = torch.bfloat16
|
| 18 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 19 |
+
|
| 20 |
+
MAX_SEED = np.iinfo(np.int32).max
|
| 21 |
+
MAX_IMAGE_SIZE = 1024
|
| 22 |
+
|
| 23 |
+
hf_client = InferenceClient(
|
| 24 |
+
api_key=os.environ.get("HF_TOKEN"),
|
| 25 |
+
)
|
| 26 |
+
VLM_MODEL = "baidu/ERNIE-4.5-VL-424B-A47B-Base-PT"
|
| 27 |
+
|
| 28 |
+
SYSTEM_PROMPT_TEXT_ONLY = """You are an expert prompt engineer for FLUX.2 by Black Forest Labs. Rewrite user prompts to be more descriptive while strictly preserving their core subject and intent.
|
| 29 |
+
Guidelines:
|
| 30 |
+
1. Structure: Keep structured inputs structured (enhance within fields). Convert natural language to detailed paragraphs.
|
| 31 |
+
2. Details: Add concrete visual specifics - form, scale, textures, materials, lighting (quality, direction, color), shadows, spatial relationships, and environmental context.
|
| 32 |
+
3. Text in Images: Put ALL text in quotation marks, matching the prompt's language. Always provide explicit quoted text for objects that would contain text in reality (signs, labels, screens, etc.) - without it, the model generates gibberish.
|
| 33 |
+
Output only the revised prompt and nothing else."""
|
| 34 |
+
|
| 35 |
+
SYSTEM_PROMPT_WITH_IMAGES = """You are FLUX.2 by Black Forest Labs, an image-editing expert. You convert editing requests into one concise instruction (50-80 words, ~30 for brief requests).
|
| 36 |
+
Rules:
|
| 37 |
+
- Single instruction only, no commentary
|
| 38 |
+
- Use clear, analytical language (avoid "whimsical," "cascading," etc.)
|
| 39 |
+
- Specify what changes AND what stays the same (face, lighting, composition)
|
| 40 |
+
- Reference actual image elements
|
| 41 |
+
- Turn negatives into positives ("don't change X" → "keep X")
|
| 42 |
+
- Make abstractions concrete ("futuristic" → "glowing cyan neon, metallic panels")
|
| 43 |
+
- Keep content PG-13
|
| 44 |
+
Output only the final instruction in plain text and nothing else."""
|
| 45 |
+
|
| 46 |
+
# Model repository IDs for 9B
|
| 47 |
+
REPO_ID_REGULAR = "black-forest-labs/FLUX.2-klein-base-9B"
|
| 48 |
+
REPO_ID_DISTILLED = "black-forest-labs/FLUX.2-klein-9B"
|
| 49 |
+
|
| 50 |
+
# Load both 9B models
|
| 51 |
+
print("Loading 9B Regular model...")
|
| 52 |
+
pipe_regular = Flux2KleinPipeline.from_pretrained(REPO_ID_REGULAR, torch_dtype=dtype)
|
| 53 |
+
pipe_regular.to("cuda")
|
| 54 |
+
|
| 55 |
+
print("Loading 9B Distilled model...")
|
| 56 |
+
pipe_distilled = Flux2KleinPipeline.from_pretrained(REPO_ID_DISTILLED, torch_dtype=dtype)
|
| 57 |
+
pipe_distilled.to("cuda")
|
| 58 |
+
|
| 59 |
+
# Dictionary for easy access
|
| 60 |
+
pipes = {
|
| 61 |
+
"Distilled (4 steps)": pipe_distilled,
|
| 62 |
+
"Base (50 steps)": pipe_regular,
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
# Default steps for each mode
|
| 66 |
+
DEFAULT_STEPS = {
|
| 67 |
+
"Distilled (4 steps)": 4,
|
| 68 |
+
"Base (50 steps)": 50,
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
DEFAULT_CFG = {
|
| 72 |
+
"Distilled (4 steps)": 1.0,
|
| 73 |
+
"Base (50 steps)": 4.0,
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
def image_to_data_uri(img):
|
| 77 |
+
"""
|
| 78 |
+
Convert a PIL Image to a base64 data URI.
|
| 79 |
+
Args:
|
| 80 |
+
img: The PIL Image to convert.
|
| 81 |
+
Returns:
|
| 82 |
+
str: A data URI string containing the base64-encoded PNG image.
|
| 83 |
+
"""
|
| 84 |
+
buffered = io.BytesIO()
|
| 85 |
+
img.save(buffered, format="PNG")
|
| 86 |
+
img_str = base64.b64encode(buffered.getvalue()).decode("utf-8")
|
| 87 |
+
return f"data:image/png;base64,{img_str}"
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def upsample_prompt_logic(prompt, image_list):
|
| 91 |
+
"""
|
| 92 |
+
Enhance a text prompt using a Vision-Language Model.
|
| 93 |
+
Args:
|
| 94 |
+
prompt (str): The original text prompt to enhance.
|
| 95 |
+
image_list: Optional list of PIL Images for context-aware enhancement.
|
| 96 |
+
Returns:
|
| 97 |
+
str: The enhanced prompt, or the original prompt if enhancement fails.
|
| 98 |
+
"""
|
| 99 |
+
try:
|
| 100 |
+
if image_list and len(image_list) > 0:
|
| 101 |
+
# Image + Text Editing Mode
|
| 102 |
+
system_content = SYSTEM_PROMPT_WITH_IMAGES
|
| 103 |
+
|
| 104 |
+
# Construct user message with text and images
|
| 105 |
+
user_content = [{"type": "text", "text": prompt}]
|
| 106 |
+
|
| 107 |
+
for img in image_list:
|
| 108 |
+
data_uri = image_to_data_uri(img)
|
| 109 |
+
user_content.append({
|
| 110 |
+
"type": "image_url",
|
| 111 |
+
"image_url": {"url": data_uri}
|
| 112 |
+
})
|
| 113 |
+
|
| 114 |
+
messages = [
|
| 115 |
+
{"role": "system", "content": system_content},
|
| 116 |
+
{"role": "user", "content": user_content}
|
| 117 |
+
]
|
| 118 |
+
else:
|
| 119 |
+
# Text Only Mode
|
| 120 |
+
system_content = SYSTEM_PROMPT_TEXT_ONLY
|
| 121 |
+
messages = [
|
| 122 |
+
{"role": "system", "content": system_content},
|
| 123 |
+
{"role": "user", "content": prompt}
|
| 124 |
+
]
|
| 125 |
+
|
| 126 |
+
completion = hf_client.chat.completions.create(
|
| 127 |
+
model=VLM_MODEL,
|
| 128 |
+
messages=messages,
|
| 129 |
+
max_tokens=1024
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
return completion.choices[0].message.content
|
| 133 |
+
except Exception as e:
|
| 134 |
+
print(f"Upsampling failed: {e}")
|
| 135 |
+
return prompt
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
def update_dimensions_from_image(image_list):
|
| 139 |
+
"""
|
| 140 |
+
Update width/height based on uploaded image aspect ratio.
|
| 141 |
+
|
| 142 |
+
Keeps one side at 1024 and scales the other proportionally,
|
| 143 |
+
with both sides as multiples of 8.
|
| 144 |
+
Args:
|
| 145 |
+
image_list: Gallery list of tuples (image, caption) from Gradio.
|
| 146 |
+
Returns:
|
| 147 |
+
tuple: A tuple of (width, height) integers, both multiples of 8.
|
| 148 |
+
"""
|
| 149 |
+
if image_list is None or len(image_list) == 0:
|
| 150 |
+
return 1024, 1024 # Default dimensions
|
| 151 |
+
|
| 152 |
+
# Get the first image to determine dimensions
|
| 153 |
+
img = image_list[0][0] # Gallery returns list of tuples (image, caption)
|
| 154 |
+
img_width, img_height = img.size
|
| 155 |
+
|
| 156 |
+
aspect_ratio = img_width / img_height
|
| 157 |
+
|
| 158 |
+
if aspect_ratio >= 1: # Landscape or square
|
| 159 |
+
new_width = 1024
|
| 160 |
+
new_height = int(1024 / aspect_ratio)
|
| 161 |
+
else: # Portrait
|
| 162 |
+
new_height = 1024
|
| 163 |
+
new_width = int(1024 * aspect_ratio)
|
| 164 |
+
|
| 165 |
+
# Round to nearest multiple of 8
|
| 166 |
+
new_width = round(new_width / 8) * 8
|
| 167 |
+
new_height = round(new_height / 8) * 8
|
| 168 |
+
|
| 169 |
+
# Ensure within valid range (minimum 256, maximum 1024)
|
| 170 |
+
new_width = max(256, min(1024, new_width))
|
| 171 |
+
new_height = max(256, min(1024, new_height))
|
| 172 |
+
|
| 173 |
+
return new_width, new_height
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
def update_steps_from_mode(mode_choice):
|
| 177 |
+
"""
|
| 178 |
+
Update inference steps and guidance scale based on the selected mode.
|
| 179 |
+
Args:
|
| 180 |
+
mode_choice (str): The selected mode, either "Distilled (4 steps)" or "Base (50 steps)".
|
| 181 |
+
Returns:
|
| 182 |
+
tuple: A tuple of (num_inference_steps, guidance_scale).
|
| 183 |
+
"""
|
| 184 |
+
return DEFAULT_STEPS[mode_choice], DEFAULT_CFG[mode_choice]
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
@spaces.GPU(duration=85)
|
| 188 |
+
def infer(
|
| 189 |
+
prompt: str,
|
| 190 |
+
input_images=None,
|
| 191 |
+
mode_choice: str = "Distilled (4 steps)",
|
| 192 |
+
seed: int = 42,
|
| 193 |
+
randomize_seed: bool = False,
|
| 194 |
+
width: int = 1024,
|
| 195 |
+
height: int = 1024,
|
| 196 |
+
num_inference_steps: int = 4,
|
| 197 |
+
guidance_scale: float = 4.0,
|
| 198 |
+
prompt_upsampling: bool = False,
|
| 199 |
+
progress=gr.Progress(track_tqdm=True)
|
| 200 |
+
):
|
| 201 |
+
"""
|
| 202 |
+
Generate or edit images using FLUX.2 Klein 9B model.
|
| 203 |
+
|
| 204 |
+
This tool can generate images from text prompts, or edit/combine existing images
|
| 205 |
+
based on text instructions. Use the distilled mode for fast 4-step generation,
|
| 206 |
+
or base mode for higher quality 50-step generation.
|
| 207 |
+
Args:
|
| 208 |
+
prompt (str): Text description of the image to generate, or editing instructions when input images are provided.
|
| 209 |
+
input_images: Optional list of input images for editing or combining. Provide image URLs.
|
| 210 |
+
mode_choice (str): Model mode - "Distilled (4 steps)" for fast generation or "Base (50 steps)" for higher quality.
|
| 211 |
+
seed (str): Random seed for reproducible generation. Use "0" with randomize_seed=True for random results.
|
| 212 |
+
randomize_seed (str): Set to "true" to use a random seed, "false" to use the specified seed.
|
| 213 |
+
width (str): Output image width in pixels (256-1024, must be multiple of 8).
|
| 214 |
+
height (str): Output image height in pixels (256-1024, must be multiple of 8).
|
| 215 |
+
num_inference_steps (str): Number of denoising steps. Use "4" for distilled mode, "50" for base mode.
|
| 216 |
+
guidance_scale (str): How closely to follow the prompt. Use "1.0" for distilled, "4.0" for base mode.
|
| 217 |
+
prompt_upsampling (str): Set to "true" to automatically enhance the prompt using a VLM.
|
| 218 |
+
Returns:
|
| 219 |
+
tuple: A tuple containing the generated PIL Image and the seed used.
|
| 220 |
+
"""
|
| 221 |
+
# Convert string inputs to proper types for MCP compatibility
|
| 222 |
+
if isinstance(seed, str):
|
| 223 |
+
seed = int(seed)
|
| 224 |
+
if isinstance(randomize_seed, str):
|
| 225 |
+
randomize_seed = randomize_seed.lower() == "true"
|
| 226 |
+
if isinstance(width, str):
|
| 227 |
+
width = int(width)
|
| 228 |
+
if isinstance(height, str):
|
| 229 |
+
height = int(height)
|
| 230 |
+
if isinstance(num_inference_steps, str):
|
| 231 |
+
num_inference_steps = int(num_inference_steps)
|
| 232 |
+
if isinstance(guidance_scale, str):
|
| 233 |
+
guidance_scale = float(guidance_scale)
|
| 234 |
+
if isinstance(prompt_upsampling, str):
|
| 235 |
+
prompt_upsampling = prompt_upsampling.lower() == "true"
|
| 236 |
+
|
| 237 |
+
if randomize_seed:
|
| 238 |
+
seed = random.randint(0, MAX_SEED)
|
| 239 |
+
|
| 240 |
+
# Select the appropriate pipeline based on mode choice
|
| 241 |
+
pipe = pipes[mode_choice]
|
| 242 |
+
|
| 243 |
+
# Prepare image list (convert None or empty gallery to None)
|
| 244 |
+
image_list = None
|
| 245 |
+
if input_images is not None and len(input_images) > 0:
|
| 246 |
+
image_list = []
|
| 247 |
+
for item in input_images:
|
| 248 |
+
image_list.append(item[0])
|
| 249 |
+
|
| 250 |
+
# 1. Upsampling (Network bound)
|
| 251 |
+
final_prompt = prompt
|
| 252 |
+
if prompt_upsampling:
|
| 253 |
+
progress(0.1, desc="Upsampling prompt...")
|
| 254 |
+
final_prompt = upsample_prompt_logic(prompt, image_list)
|
| 255 |
+
print(f"Original Prompt: {prompt}")
|
| 256 |
+
print(f"Upsampled Prompt: {final_prompt}")
|
| 257 |
+
|
| 258 |
+
# 2. Image Generation
|
| 259 |
+
progress(0.2, desc=f"Generating image with 9B {mode_choice}...")
|
| 260 |
+
|
| 261 |
+
generator = torch.Generator(device=device).manual_seed(seed)
|
| 262 |
+
|
| 263 |
+
pipe_kwargs = {
|
| 264 |
+
"prompt": final_prompt,
|
| 265 |
+
"height": height,
|
| 266 |
+
"width": width,
|
| 267 |
+
"num_inference_steps": num_inference_steps,
|
| 268 |
+
"guidance_scale": guidance_scale,
|
| 269 |
+
"generator": generator,
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
# Add images if provided
|
| 273 |
+
if image_list is not None:
|
| 274 |
+
pipe_kwargs["image"] = image_list
|
| 275 |
+
|
| 276 |
+
image = pipe(**pipe_kwargs).images[0]
|
| 277 |
+
|
| 278 |
+
return image, seed
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
examples = [
|
| 282 |
+
["Create a vase on a table in living room, the color of the vase is a gradient of color, starting with #02eb3c color and finishing with #edfa3c. The flowers inside the vase have the color #ff0088"],
|
| 283 |
+
["Photorealistic infographic showing the complete Berlin TV Tower (Fernsehturm) from ground base to antenna tip, full vertical view with entire structure visible including concrete shaft, metallic sphere, and antenna spire. Slight upward perspective angle looking up toward the iconic sphere, perfectly centered on clean white background. Left side labels with thin horizontal connector lines: the text '368m' in extra large bold dark grey numerals (#2D3748) positioned at exactly the antenna tip with 'TOTAL HEIGHT' in small caps below. The text '207m' in extra large bold with 'TELECAFÉ' in small caps below, with connector line touching the sphere precisely at the window level. Right side label with horizontal connector line touching the sphere's equator: the text '32m' in extra large bold dark grey numerals with 'SPHERE DIAMETER' in small caps below. Bottom section arranged in three balanced columns: Left - Large text '986' in extra bold dark grey with 'STEPS' in caps below. Center - 'BERLIN TV TOWER' in bold caps with 'FERNSEHTURM' in lighter weight below. Right - 'INAUGURATED' in bold caps with 'OCTOBER 3, 1969' below. All typography in modern sans-serif font (such as Inter or Helvetica), color #2D3748, clean minimal technical diagram style. Horizontal connector lines are thin, precise, and clearly visible, touching the tower structure at exact corresponding measurement points. Professional architectural elevation drawing aesthetic with dynamic low angle perspective creating sense of height and grandeur, poster-ready infographic design with perfect visual hierarchy."],
|
| 284 |
+
["Soaking wet capybara taking shelter under a banana leaf in the rainy jungle, close up photo"],
|
| 285 |
+
["A kawaii die-cut sticker of a chubby orange cat, featuring big sparkly eyes and a happy smile with paws raised in greeting and a heart-shaped pink nose. The design should have smooth rounded lines with black outlines and soft gradient shading with pink cheeks."],
|
| 286 |
+
]
|
| 287 |
+
|
| 288 |
+
examples_images = [
|
| 289 |
+
["The person from image 1 is petting the cat from image 2, the bird from image 3 is next to them", ["woman1.webp", "cat_window.webp", "bird.webp"]]
|
| 290 |
+
]
|
| 291 |
+
|
| 292 |
+
css = """
|
| 293 |
+
#col-container {
|
| 294 |
+
margin: 0 auto;
|
| 295 |
+
max-width: 1200px;
|
| 296 |
+
}
|
| 297 |
+
.gallery-container img{
|
| 298 |
+
object-fit: contain;
|
| 299 |
+
}
|
| 300 |
+
"""
|
| 301 |
+
|
| 302 |
+
with gr.Blocks(css=css) as demo:
|
| 303 |
+
|
| 304 |
+
with gr.Column(elem_id="col-container"):
|
| 305 |
+
gr.Markdown(f"""# FLUX.2 [Klein] - 9B
|
| 306 |
+
FLUX.2 [Klein] is a distilled model capable of generating, editing and combining images based on text instructions [[model](https://huggingface.co/black-forest-labs/FLUX.2-klein-9B)], [[blog](https://bfl.ai/blog/flux-2)]
|
| 307 |
+
""")
|
| 308 |
+
with gr.Row():
|
| 309 |
+
with gr.Column():
|
| 310 |
+
with gr.Row():
|
| 311 |
+
prompt = gr.Text(
|
| 312 |
+
label="Prompt",
|
| 313 |
+
show_label=False,
|
| 314 |
+
max_lines=2,
|
| 315 |
+
placeholder="Enter your prompt",
|
| 316 |
+
container=False,
|
| 317 |
+
scale=3
|
| 318 |
+
)
|
| 319 |
+
|
| 320 |
+
run_button = gr.Button("Run", scale=1)
|
| 321 |
+
|
| 322 |
+
with gr.Accordion("Input image(s) (optional)", open=False):
|
| 323 |
+
input_images = gr.Gallery(
|
| 324 |
+
label="Input Image(s)",
|
| 325 |
+
type="pil",
|
| 326 |
+
columns=3,
|
| 327 |
+
rows=1,
|
| 328 |
+
)
|
| 329 |
+
|
| 330 |
+
mode_choice = gr.Radio(
|
| 331 |
+
label="Mode",
|
| 332 |
+
choices=["Distilled (4 steps)", "Base (50 steps)"],
|
| 333 |
+
value="Distilled (4 steps)",
|
| 334 |
+
)
|
| 335 |
+
|
| 336 |
+
with gr.Accordion("Advanced Settings", open=False):
|
| 337 |
+
|
| 338 |
+
prompt_upsampling = gr.Checkbox(
|
| 339 |
+
label="Prompt Upsampling",
|
| 340 |
+
value=False,
|
| 341 |
+
info="Automatically enhance the prompt using a VLM"
|
| 342 |
+
)
|
| 343 |
+
|
| 344 |
+
seed = gr.Slider(
|
| 345 |
+
label="Seed",
|
| 346 |
+
minimum=0,
|
| 347 |
+
maximum=MAX_SEED,
|
| 348 |
+
step=1,
|
| 349 |
+
value=0,
|
| 350 |
+
)
|
| 351 |
+
|
| 352 |
+
randomize_seed = gr.Checkbox(label="Randomize seed", value=True)
|
| 353 |
+
|
| 354 |
+
with gr.Row():
|
| 355 |
+
|
| 356 |
+
width = gr.Slider(
|
| 357 |
+
label="Width",
|
| 358 |
+
minimum=256,
|
| 359 |
+
maximum=MAX_IMAGE_SIZE,
|
| 360 |
+
step=8,
|
| 361 |
+
value=1024,
|
| 362 |
+
)
|
| 363 |
+
|
| 364 |
+
height = gr.Slider(
|
| 365 |
+
label="Height",
|
| 366 |
+
minimum=256,
|
| 367 |
+
maximum=MAX_IMAGE_SIZE,
|
| 368 |
+
step=8,
|
| 369 |
+
value=1024,
|
| 370 |
+
)
|
| 371 |
+
|
| 372 |
+
with gr.Row():
|
| 373 |
+
|
| 374 |
+
num_inference_steps = gr.Slider(
|
| 375 |
+
label="Number of inference steps",
|
| 376 |
+
minimum=1,
|
| 377 |
+
maximum=100,
|
| 378 |
+
step=1,
|
| 379 |
+
value=4,
|
| 380 |
+
)
|
| 381 |
+
|
| 382 |
+
guidance_scale = gr.Slider(
|
| 383 |
+
label="Guidance scale",
|
| 384 |
+
minimum=0.0,
|
| 385 |
+
maximum=10.0,
|
| 386 |
+
step=0.1,
|
| 387 |
+
value=1.0,
|
| 388 |
+
)
|
| 389 |
+
|
| 390 |
+
|
| 391 |
+
with gr.Column():
|
| 392 |
+
result = gr.Image(label="Result", show_label=False)
|
| 393 |
+
|
| 394 |
+
|
| 395 |
+
gr.Examples(
|
| 396 |
+
examples=examples,
|
| 397 |
+
fn=infer,
|
| 398 |
+
inputs=[prompt],
|
| 399 |
+
outputs=[result, seed],
|
| 400 |
+
cache_examples=True,
|
| 401 |
+
cache_mode="lazy"
|
| 402 |
+
)
|
| 403 |
+
|
| 404 |
+
gr.Examples(
|
| 405 |
+
examples=examples_images,
|
| 406 |
+
fn=infer,
|
| 407 |
+
inputs=[prompt, input_images],
|
| 408 |
+
outputs=[result, seed],
|
| 409 |
+
cache_examples=True,
|
| 410 |
+
cache_mode="lazy"
|
| 411 |
+
)
|
| 412 |
+
|
| 413 |
+
# Auto-update dimensions when images are uploaded
|
| 414 |
+
input_images.upload(
|
| 415 |
+
fn=update_dimensions_from_image,
|
| 416 |
+
inputs=[input_images],
|
| 417 |
+
outputs=[width, height]
|
| 418 |
+
)
|
| 419 |
+
|
| 420 |
+
# Auto-update steps when mode changes
|
| 421 |
+
mode_choice.change(
|
| 422 |
+
fn=update_steps_from_mode,
|
| 423 |
+
inputs=[mode_choice],
|
| 424 |
+
outputs=[num_inference_steps, guidance_scale]
|
| 425 |
+
)
|
| 426 |
+
|
| 427 |
+
gr.on(
|
| 428 |
+
triggers=[run_button.click, prompt.submit],
|
| 429 |
+
fn=infer,
|
| 430 |
+
inputs=[prompt, input_images, mode_choice, seed, randomize_seed, width, height, num_inference_steps, guidance_scale, prompt_upsampling],
|
| 431 |
+
outputs=[result, seed],
|
| 432 |
+
api_name="generate" # Explicit API name for MCP tool
|
| 433 |
+
)
|
| 434 |
+
|
| 435 |
+
# Launch with MCP server enabled
|
| 436 |
+
demo.launch(mcp_server=True)
|
game/llm_engine.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
import uuid
|
| 4 |
+
import time
|
| 5 |
+
import re
|
| 6 |
+
# ----------------------------
|
| 7 |
+
# LLM STREAM
|
| 8 |
+
# ----------------------------
|
| 9 |
+
|
| 10 |
+
def generate_llm_stream(client, model_name, system_prompt, message, history, state):
|
| 11 |
+
player_lang = state.get("language", "English")
|
| 12 |
+
|
| 13 |
+
struct_instruction = f"""
|
| 14 |
+
\n\nCRITICAL RULE 1: You MUST write your entire response, all storytelling, dialogue, and exactly 5 choices EXCLUSIVELY in {player_lang}.
|
| 15 |
+
CRITICAL RULE 2: At the very end of your response, you MUST append a hidden visual description for the image generator. Use EXACTLY this format (raw JSON only, no markdown):
|
| 16 |
+
<SCENE_DATA>
|
| 17 |
+
{{"image_prompt": "A highly detailed, purely visual description of the current scene, focusing on characters, lighting, and environment.", "location": "Name of the current place"}}
|
| 18 |
+
</SCENE_DATA>
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
dynamic_system_prompt = system_prompt + struct_instruction
|
| 22 |
+
|
| 23 |
+
context = f"Player State:\n{json.dumps(state, indent=2)}"
|
| 24 |
+
|
| 25 |
+
messages = [
|
| 26 |
+
{"role": "system", "content": dynamic_system_prompt},
|
| 27 |
+
{"role": "system", "content": context},
|
| 28 |
+
]
|
| 29 |
+
|
| 30 |
+
for h in history:
|
| 31 |
+
messages.append({"role": "user", "content": h[0]})
|
| 32 |
+
messages.append({"role": "assistant", "content": h[1]})
|
| 33 |
+
|
| 34 |
+
messages.append({"role": "user", "content": message})
|
| 35 |
+
|
| 36 |
+
stream = client.chat.completions.create(
|
| 37 |
+
model=model_name,
|
| 38 |
+
messages=messages,
|
| 39 |
+
temperature=0.95,
|
| 40 |
+
top_p=0.9,
|
| 41 |
+
presence_penalty=0.6,
|
| 42 |
+
frequency_penalty=0.4,
|
| 43 |
+
max_tokens=1200,
|
| 44 |
+
stream=True,
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
partial = ""
|
| 48 |
+
for chunk in stream:
|
| 49 |
+
if chunk.choices and chunk.choices[0].delta.content:
|
| 50 |
+
partial += chunk.choices[0].delta.content
|
| 51 |
+
display_text = partial.split("<SCENE_DATA>")[0].strip() # JSON bloğunu UI'dan gizlemek için ayırıyoruz
|
| 52 |
+
yield display_text, partial
|
game/player.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
import uuid
|
| 4 |
+
import time
|
| 5 |
+
import re
|
| 6 |
+
# ----------------------------
|
| 7 |
+
# PLAYER INIT
|
| 8 |
+
# ----------------------------
|
| 9 |
+
|
| 10 |
+
def create_player(name, gender, avatar, language):
|
| 11 |
+
return {
|
| 12 |
+
"id": str(uuid.uuid4()),
|
| 13 |
+
"name": name,
|
| 14 |
+
"language": language,
|
| 15 |
+
"gender": gender,
|
| 16 |
+
"avatar": avatar,
|
| 17 |
+
"light_dark": 0, # -100 dark, +100 light
|
| 18 |
+
"order_chaos": 0, # -100 chaos, +100 order
|
| 19 |
+
"strength": 5,
|
| 20 |
+
"agility": 5,
|
| 21 |
+
"intelligence": 5,
|
| 22 |
+
"charisma": 5,
|
| 23 |
+
"willpower": 5,
|
| 24 |
+
}
|
game/translate.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import perchance
|
| 3 |
+
|
| 4 |
+
#English Deutsch Español Français Türkçe
|
| 5 |
+
langs_list = ["en", "de", "es", "fr", "tr"]
|
| 6 |
+
|
| 7 |
+
def translate_to_en(text, lang):
|
| 8 |
+
if lang == "en":
|
| 9 |
+
return text
|
| 10 |
+
|
| 11 |
+
if lang not in langs_list
|
| 12 |
+
return text
|
| 13 |
+
|
| 14 |
+
prompt = f"Translate this to English:\n{text}"
|
| 15 |
+
return llm(prompt)
|
| 16 |
+
|
| 17 |
+
def translate_from_en(text, lang):
|
| 18 |
+
if lang == "en":
|
| 19 |
+
return text
|
| 20 |
+
|
| 21 |
+
if lang not in langs_list
|
| 22 |
+
return text
|
| 23 |
+
|
| 24 |
+
prompt = f"Translate this to {lang}:\n{text}"
|
| 25 |
+
return llm(prompt)
|
| 26 |
+
|
| 27 |
+
async def generate(prompt):
|
| 28 |
+
gen = perchance.TextGenerator()
|
| 29 |
+
text = ""
|
| 30 |
+
|
| 31 |
+
async for chunk in gen.text(prompt):
|
| 32 |
+
text += chunk
|
| 33 |
+
|
| 34 |
+
return text
|
game/turn_engine.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import json
|
| 3 |
+
import os
|
| 4 |
+
import uuid
|
| 5 |
+
import time
|
| 6 |
+
import re
|
| 7 |
+
from game.llm_engine import generate_llm_stream
|
| 8 |
+
from game.vlm_engine import extract_scene_data, generate_scene_image_structured
|
| 9 |
+
from systems.alignment_system import evaluate_alignment
|
| 10 |
+
# ----------------------------
|
| 11 |
+
# GAME TURN
|
| 12 |
+
# ----------------------------
|
| 13 |
+
|
| 14 |
+
def play_turn(user_input, history, state_json, text_client, image_client, model_name, system_prompt):
|
| 15 |
+
if history is None:
|
| 16 |
+
history = []
|
| 17 |
+
|
| 18 |
+
state = json.loads(state_json)
|
| 19 |
+
|
| 20 |
+
last_ai_message = history[-1][1] if len(history) > 0 else ""
|
| 21 |
+
|
| 22 |
+
history.append([user_input, ""])
|
| 23 |
+
|
| 24 |
+
full_ai_response = ""
|
| 25 |
+
for display_text, full_text in generate_llm_stream(text_client, model_name, system_prompt, user_input, history[:-1], state):
|
| 26 |
+
history[-1][1] = display_text
|
| 27 |
+
full_ai_response = full_text # Gizli JSON dahil tam metni arka planda tutuyoruz
|
| 28 |
+
yield (
|
| 29 |
+
history,
|
| 30 |
+
json.dumps(state),
|
| 31 |
+
state["light_dark"],
|
| 32 |
+
state["order_chaos"],
|
| 33 |
+
gr.update()
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
# UI'da görünen temiz metni alignment için değerlendiriyoruz
|
| 37 |
+
if user_input.strip() and "Start the adventure" not in user_input:
|
| 38 |
+
ld_shift, oc_shift = evaluate_alignment(text_client, model_name, user_input, last_ai_message)
|
| 39 |
+
state["light_dark"] = max(-100, min(100, state["light_dark"] + ld_shift))
|
| 40 |
+
state["order_chaos"] = max(-100, min(100, state["order_chaos"] + oc_shift))
|
| 41 |
+
|
| 42 |
+
# YENİ: Arka planda biriken tam metinden JSON'u çıkarıyoruz
|
| 43 |
+
scene_data = extract_scene_data(full_ai_response)
|
| 44 |
+
|
| 45 |
+
# JSON başarıyla alındıysa görseli üretiyoruz
|
| 46 |
+
new_image = generate_scene_image_structured(image_client, scene_data) if scene_data else None
|
| 47 |
+
|
| 48 |
+
yield (
|
| 49 |
+
history,
|
| 50 |
+
json.dumps(state),
|
| 51 |
+
state["light_dark"],
|
| 52 |
+
state["order_chaos"],
|
| 53 |
+
new_image if new_image else gr.update()
|
| 54 |
+
)
|
game/vlm_engine.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
import uuid
|
| 4 |
+
import time
|
| 5 |
+
import re
|
| 6 |
+
# ----------------------------
|
| 7 |
+
# IMAGE GENERATION
|
| 8 |
+
# ----------------------------
|
| 9 |
+
|
| 10 |
+
def extract_scene_data(full_text):
|
| 11 |
+
"""LLM çıktısındaki gizli SCENE_DATA bloğunu bulur ve JSON'a çevirir."""
|
| 12 |
+
pattern = r"<SCENE_DATA>(.*?)</SCENE_DATA>"
|
| 13 |
+
match = re.search(pattern, full_text, re.DOTALL | re.IGNORECASE)
|
| 14 |
+
|
| 15 |
+
if match:
|
| 16 |
+
json_str = match.group(1).strip()
|
| 17 |
+
# Llama bazen markdown (```json) ekleyebilir, onu temizliyoruz
|
| 18 |
+
json_str = json_str.replace("```json", "").replace("```", "").strip()
|
| 19 |
+
try:
|
| 20 |
+
return json.loads(json_str)
|
| 21 |
+
except Exception as e:
|
| 22 |
+
print(f"JSON Parse Error: {e}")
|
| 23 |
+
return None
|
| 24 |
+
|
| 25 |
+
def generate_scene_image_structured(client, scene_data):
|
| 26 |
+
"""Sadece LLM'den gelen saf 'image_prompt'u kullanarak görsel üretir."""
|
| 27 |
+
if not scene_data or "image_prompt" not in scene_data:
|
| 28 |
+
return None
|
| 29 |
+
|
| 30 |
+
# Ana Star Wars şablonumuz
|
| 31 |
+
base_prompt = "Cinematic Star Wars concept art, highly detailed, dark atmosphere, dramatic lighting, 8k resolution, masterpiece. "
|
| 32 |
+
final_prompt = base_prompt + scene_data["image_prompt"]
|
| 33 |
+
|
| 34 |
+
try:
|
| 35 |
+
image = client.text_to_image(final_prompt)
|
| 36 |
+
return image
|
| 37 |
+
except Exception as e:
|
| 38 |
+
print(f"Image generation failed via Router: {e}")
|
| 39 |
+
return None
|
systems/alignment_system.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
import uuid
|
| 4 |
+
import time
|
| 5 |
+
import re
|
| 6 |
+
# ----------------------------
|
| 7 |
+
# ALIGNMENT STREAM
|
| 8 |
+
# ----------------------------
|
| 9 |
+
|
| 10 |
+
def evaluate_alignment(client, model_name, user_input, last_context=""):
|
| 11 |
+
eval_prompt = f"""You are a hidden Game Master evaluating player morality in a Star Wars RPG.
|
| 12 |
+
Story Context: "{last_context[-200:]}"
|
| 13 |
+
Player Action: "{user_input}"
|
| 14 |
+
|
| 15 |
+
Evaluate the alignment shift for this action.
|
| 16 |
+
Light/Dark: + (Light: compassion, healing) to - (Dark: murder, selfishness, anger).
|
| 17 |
+
Order/Chaos: + (Order: following rules, loyalty) to - (Chaos: rebellion, deception, breaking laws).
|
| 18 |
+
|
| 19 |
+
Score both from -5 to +5.
|
| 20 |
+
Return ONLY two integers separated by a comma. NO other text.
|
| 21 |
+
Example: -3, 2
|
| 22 |
+
"""
|
| 23 |
+
try:
|
| 24 |
+
# Arka planda hızlıca puanlama yapması için temperature=0 kullanıyoruz
|
| 25 |
+
response = client.chat.completions.create(
|
| 26 |
+
model=model_name,
|
| 27 |
+
messages=[{"role": "user", "content": eval_prompt}],
|
| 28 |
+
max_tokens=10,
|
| 29 |
+
temperature=0.0
|
| 30 |
+
)
|
| 31 |
+
result = response.choices[0].message.content.strip()
|
| 32 |
+
ld_shift, oc_shift = result.split(',')
|
| 33 |
+
return int(ld_shift.strip()), int(oc_shift.strip())
|
| 34 |
+
except Exception as e:
|
| 35 |
+
print("Alignment parsing error:", e)
|
| 36 |
+
return 0, 0 # Hata olursa puanı değiştirme
|
tmp/trans.py → systems/inventory_system.py
RENAMED
|
File without changes
|
systems/journal_system.py
ADDED
|
File without changes
|
systems/memory_system.py
ADDED
|
File without changes
|
systems/quest_system.py
ADDED
|
File without changes
|
systems/xp_system.py
ADDED
|
File without changes
|