cereeenn120 commited on
Commit
f834d3b
·
1 Parent(s): d6f178c

Refactor app.py as modular

Browse files
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
- play_turn,
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 play_turn("I choose option 1.", history, state)
483
  def choice2_fn(history, state):
484
- yield from play_turn("I choose option 2.", history, state)
485
  def choice3_fn(history, state):
486
- yield from play_turn("I choose option 3.", history, state)
487
  def choice4_fn(history, state):
488
- yield from play_turn("I choose option 4.", history, state)
489
  def choice5_fn(history, state):
490
- yield from play_turn("I choose option 5.", history, state)
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