feat: Integrate Gemini AI for prompt refinement and image generation
Browse files- Migrated to google-genai SDK.
- Implemented prompt refinement with gemini-3-pro-preview.
- Integrated image generation using imagen-4.0-generate-001.
- Fixed image display by converting Gemini response to PIL Image.
- Added PNG conversion and friendly filenames (rpg_portrait.png).
- Expanded exotic weapon, armor, and accessory variants in features.yaml.
- Added support for dual accessory selection.
- Implemented "Save Character" and "Load Character" functionality (JSON).
- Added detailed traceback logging for debugging.
- Fixed Gradio theme and CopyButton compatibility issues.
- app.py +270 -50
- features.yaml +18 -1
- requirements.txt +2 -0
app.py
CHANGED
|
@@ -2,6 +2,28 @@ import gradio as gr
|
|
| 2 |
import yaml
|
| 3 |
import random
|
| 4 |
import os
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
# Load features from YAML
|
| 7 |
def load_features():
|
|
@@ -10,59 +32,99 @@ def load_features():
|
|
| 10 |
|
| 11 |
features_data = load_features()
|
| 12 |
|
| 13 |
-
# Define
|
| 14 |
-
#
|
| 15 |
FEATURE_SEQUENCE = [
|
| 16 |
-
|
| 17 |
-
('identity', '
|
| 18 |
-
|
| 19 |
-
('
|
| 20 |
-
|
| 21 |
-
('
|
| 22 |
-
('appearance', '
|
| 23 |
-
|
| 24 |
-
('
|
| 25 |
-
|
| 26 |
-
('
|
| 27 |
-
|
| 28 |
-
('
|
| 29 |
-
|
| 30 |
-
('
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
]
|
| 32 |
|
|
|
|
|
|
|
|
|
|
| 33 |
def get_detail(category, subcategory, key):
|
| 34 |
"""Retrieves the detailed description for a given key in a category/subcategory."""
|
| 35 |
return features_data.get(category, {}).get(subcategory, {}).get(key, key)
|
| 36 |
|
| 37 |
def generate_prompt(*args):
|
| 38 |
"""
|
| 39 |
-
Assembles the prompt based on dropdown selections
|
| 40 |
"""
|
| 41 |
num_features = len(FEATURE_SEQUENCE)
|
| 42 |
feature_keys = args[:num_features]
|
|
|
|
| 43 |
|
| 44 |
template = features_data.get("templates", {}).get("default", "")
|
| 45 |
|
| 46 |
-
# Build context dictionary dynamically based on FEATURE_SEQUENCE
|
| 47 |
context = {}
|
| 48 |
-
for i, (cat, subcat) in enumerate(FEATURE_SEQUENCE):
|
| 49 |
key = feature_keys[i]
|
| 50 |
-
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
|
| 55 |
def handle_regeneration(*args):
|
| 56 |
-
"""
|
| 57 |
-
Checks each randomization checkbox. If checked, picks a random item for the dropdown.
|
| 58 |
-
Returns the new list of dropdown values.
|
| 59 |
-
"""
|
| 60 |
num_features = len(FEATURE_SEQUENCE)
|
| 61 |
current_values = list(args[:num_features])
|
| 62 |
-
checkboxes = args[num_features:]
|
| 63 |
|
| 64 |
new_values = []
|
| 65 |
-
for i, (is_random, (cat, subcat)) in enumerate(zip(checkboxes, FEATURE_SEQUENCE)):
|
| 66 |
if is_random:
|
| 67 |
choices = list(features_data[cat][subcat].keys())
|
| 68 |
new_values.append(random.choice(choices))
|
|
@@ -71,13 +133,119 @@ def handle_regeneration(*args):
|
|
| 71 |
|
| 72 |
return new_values
|
| 73 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
def build_ui():
|
| 75 |
-
with gr.Blocks(title="RPGPortrait Prompt Builder") as demo:
|
| 76 |
-
gr.Markdown("# 🎨 RPGPortrait Prompt Builder")
|
| 77 |
-
gr.Markdown("Create
|
| 78 |
|
| 79 |
dropdowns = []
|
| 80 |
checkboxes = []
|
|
|
|
| 81 |
|
| 82 |
def create_feature_ui(category, subcategory, label, default_value):
|
| 83 |
choices = list(features_data[category][subcategory].keys())
|
|
@@ -87,7 +255,6 @@ def build_ui():
|
|
| 87 |
dropdowns.append(dd)
|
| 88 |
checkboxes.append(cb)
|
| 89 |
|
| 90 |
-
# UI Creation order matches FEATURE_SEQUENCE exactly
|
| 91 |
with gr.Row():
|
| 92 |
with gr.Column():
|
| 93 |
gr.Markdown("### 👤 Identity")
|
|
@@ -95,6 +262,8 @@ def build_ui():
|
|
| 95 |
create_feature_ui('identity', 'class', "Class", "Fighter")
|
| 96 |
create_feature_ui('identity', 'gender', "Gender", "Male")
|
| 97 |
create_feature_ui('identity', 'age', "Age", "Young Adult")
|
|
|
|
|
|
|
| 98 |
|
| 99 |
gr.Markdown("### 🎭 Expression & Pose")
|
| 100 |
create_feature_ui('expression_pose', 'expression', "Expression", "Determined")
|
|
@@ -108,20 +277,27 @@ def build_ui():
|
|
| 108 |
create_feature_ui('appearance', 'build', "Build", "Average")
|
| 109 |
create_feature_ui('appearance', 'skin_tone', "Skin Tone", "Fair")
|
| 110 |
create_feature_ui('appearance', 'distinguishing_feature', "Distinguishing Feature", "None")
|
|
|
|
|
|
|
| 111 |
|
| 112 |
with gr.Row():
|
| 113 |
with gr.Column():
|
| 114 |
gr.Markdown("### ⚔️ Equipment")
|
| 115 |
create_feature_ui('equipment', 'armor', "Armor/Clothing", "Travelers Clothes")
|
| 116 |
create_feature_ui('equipment', 'weapon', "Primary Weapon", "Longsword")
|
| 117 |
-
create_feature_ui('equipment', 'accessory', "Accessory", "None")
|
|
|
|
| 118 |
create_feature_ui('equipment', 'material', "Material Detail", "Weathered")
|
|
|
|
|
|
|
| 119 |
|
| 120 |
with gr.Column():
|
| 121 |
gr.Markdown("### 🌍 Environment")
|
| 122 |
create_feature_ui('environment', 'background', "Background", "Forest")
|
| 123 |
create_feature_ui('environment', 'lighting', "Lighting", "Natural Sunlight")
|
| 124 |
create_feature_ui('environment', 'atmosphere', "Atmosphere", "Clear")
|
|
|
|
|
|
|
| 125 |
|
| 126 |
with gr.Row():
|
| 127 |
with gr.Column():
|
|
@@ -130,6 +306,8 @@ def build_ui():
|
|
| 130 |
create_feature_ui('vfx_style', 'style', "Art Style", "Digital Illustration")
|
| 131 |
create_feature_ui('vfx_style', 'mood', "Mood", "Heroic")
|
| 132 |
create_feature_ui('vfx_style', 'camera', "Camera Angle", "Bust")
|
|
|
|
|
|
|
| 133 |
|
| 134 |
with gr.Column():
|
| 135 |
gr.Markdown("### ⚙️ Technical")
|
|
@@ -138,37 +316,79 @@ def build_ui():
|
|
| 138 |
gr.Markdown("---")
|
| 139 |
|
| 140 |
with gr.Row():
|
| 141 |
-
regenerate_btn = gr.Button("✨
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
|
| 143 |
with gr.Row():
|
| 144 |
-
|
| 145 |
-
label="Generated
|
| 146 |
-
lines=
|
| 147 |
-
|
| 148 |
-
|
|
|
|
|
|
|
|
|
|
| 149 |
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
#
|
| 153 |
-
for
|
| 154 |
-
|
| 155 |
|
| 156 |
# Regenerate button logic
|
| 157 |
regenerate_btn.click(
|
| 158 |
fn=handle_regeneration,
|
| 159 |
-
inputs=
|
| 160 |
outputs=dropdowns
|
| 161 |
).then(
|
|
|
|
| 162 |
fn=generate_prompt,
|
| 163 |
-
inputs=
|
| 164 |
outputs=prompt_output
|
| 165 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
|
| 167 |
-
# Initialize
|
| 168 |
-
demo.load(fn=generate_prompt, inputs=
|
| 169 |
|
| 170 |
return demo
|
| 171 |
|
| 172 |
if __name__ == "__main__":
|
| 173 |
demo = build_ui()
|
| 174 |
-
demo.launch()
|
|
|
|
| 2 |
import yaml
|
| 3 |
import random
|
| 4 |
import os
|
| 5 |
+
from dotenv import load_dotenv
|
| 6 |
+
from google import genai
|
| 7 |
+
from google.genai import types
|
| 8 |
+
from PIL import Image
|
| 9 |
+
import io
|
| 10 |
+
import json
|
| 11 |
+
import tempfile
|
| 12 |
+
import traceback
|
| 13 |
+
|
| 14 |
+
# Load environment variables
|
| 15 |
+
load_dotenv()
|
| 16 |
+
|
| 17 |
+
# Setup Gemini
|
| 18 |
+
api_key = os.getenv("GEMINI_API_KEY")
|
| 19 |
+
client = None
|
| 20 |
+
gemini_active = False
|
| 21 |
+
if api_key:
|
| 22 |
+
try:
|
| 23 |
+
client = genai.Client(api_key=api_key)
|
| 24 |
+
gemini_active = True
|
| 25 |
+
except Exception as e:
|
| 26 |
+
print(f"Error initializing Gemini: {e}")
|
| 27 |
|
| 28 |
# Load features from YAML
|
| 29 |
def load_features():
|
|
|
|
| 32 |
|
| 33 |
features_data = load_features()
|
| 34 |
|
| 35 |
+
# Define segments for prompt building
|
| 36 |
+
# (Category, Subcategory, Template Key)
|
| 37 |
FEATURE_SEQUENCE = [
|
| 38 |
+
('identity', 'race', 'race'),
|
| 39 |
+
('identity', 'class', 'class'),
|
| 40 |
+
('identity', 'gender', 'gender'),
|
| 41 |
+
('identity', 'age', 'age'),
|
| 42 |
+
('expression_pose', 'expression', 'expression'),
|
| 43 |
+
('expression_pose', 'pose', 'pose'),
|
| 44 |
+
('appearance', 'hair_color', 'hair_color'),
|
| 45 |
+
('appearance', 'hair_style', 'hair_style'),
|
| 46 |
+
('appearance', 'eye_color', 'eye_color'),
|
| 47 |
+
('appearance', 'build', 'build'),
|
| 48 |
+
('appearance', 'skin_tone', 'skin_tone'),
|
| 49 |
+
('appearance', 'distinguishing_feature', 'distinguishing_feature'),
|
| 50 |
+
('equipment', 'armor', 'armor'),
|
| 51 |
+
('equipment', 'weapon', 'weapon'),
|
| 52 |
+
('equipment', 'accessory', 'accessory'),
|
| 53 |
+
('equipment', 'accessory', 'accessory2'),
|
| 54 |
+
('equipment', 'material', 'material'),
|
| 55 |
+
('environment', 'background', 'background'),
|
| 56 |
+
('environment', 'lighting', 'lighting'),
|
| 57 |
+
('environment', 'atmosphere', 'atmosphere'),
|
| 58 |
+
('vfx_style', 'vfx', 'vfx'),
|
| 59 |
+
('vfx_style', 'style', 'style'),
|
| 60 |
+
('vfx_style', 'mood', 'mood'),
|
| 61 |
+
('vfx_style', 'camera', 'camera'),
|
| 62 |
+
('technical', 'aspect_ratio', 'aspect_ratio')
|
| 63 |
]
|
| 64 |
|
| 65 |
+
# Section names for extra info
|
| 66 |
+
SECTIONS = ['Identity', 'Appearance', 'Equipment', 'Environment', 'Style']
|
| 67 |
+
|
| 68 |
def get_detail(category, subcategory, key):
|
| 69 |
"""Retrieves the detailed description for a given key in a category/subcategory."""
|
| 70 |
return features_data.get(category, {}).get(subcategory, {}).get(key, key)
|
| 71 |
|
| 72 |
def generate_prompt(*args):
|
| 73 |
"""
|
| 74 |
+
Assembles the prompt based on dropdown selections and extra text info.
|
| 75 |
"""
|
| 76 |
num_features = len(FEATURE_SEQUENCE)
|
| 77 |
feature_keys = args[:num_features]
|
| 78 |
+
extra_infos = args[num_features : num_features + len(SECTIONS)]
|
| 79 |
|
| 80 |
template = features_data.get("templates", {}).get("default", "")
|
| 81 |
|
|
|
|
| 82 |
context = {}
|
| 83 |
+
for i, (cat, subcat, t_key) in enumerate(FEATURE_SEQUENCE):
|
| 84 |
key = feature_keys[i]
|
| 85 |
+
context[t_key] = get_detail(cat, subcat, key)
|
| 86 |
+
|
| 87 |
+
# Handle multiple accessories dynamically
|
| 88 |
+
acc_list = []
|
| 89 |
+
# Identify accessory keys in context
|
| 90 |
+
for i, (cat, subcat, t_key) in enumerate(FEATURE_SEQUENCE):
|
| 91 |
+
if subcat == 'accessory' and feature_keys[i] != "None":
|
| 92 |
+
acc_list.append(context[t_key])
|
| 93 |
+
|
| 94 |
+
if acc_list:
|
| 95 |
+
context['accessories'] = ", and has " + " as well as ".join(acc_list)
|
| 96 |
+
else:
|
| 97 |
+
context['accessories'] = ""
|
| 98 |
+
|
| 99 |
+
# Inject extra info into the context or append to segments
|
| 100 |
+
# For simplicity, we'll append the extra info to specific segments if present
|
| 101 |
+
# Mapping section index to template keys
|
| 102 |
+
# Identity (0-3), Appearance (4-11), Equipment (12-15), Environment (16-18), Style (19-22)
|
| 103 |
|
| 104 |
+
if extra_infos[0]: # Identity
|
| 105 |
+
context['age'] += f", {extra_infos[0]}"
|
| 106 |
+
if extra_infos[1]: # Appearance
|
| 107 |
+
context['distinguishing_feature'] += f", also {extra_infos[1]}"
|
| 108 |
+
if extra_infos[2]: # Equipment
|
| 109 |
+
context['accessory'] += f", complemented by {extra_infos[2]}"
|
| 110 |
+
if extra_infos[3]: # Environment
|
| 111 |
+
context['atmosphere'] += f", additionally {extra_infos[3]}"
|
| 112 |
+
if extra_infos[4]: # Style
|
| 113 |
+
context['camera'] += f", art style notes: {extra_infos[4]}"
|
| 114 |
+
|
| 115 |
+
try:
|
| 116 |
+
return template.format(**context)
|
| 117 |
+
except Exception as e:
|
| 118 |
+
return f"Error building prompt: {e}"
|
| 119 |
|
| 120 |
def handle_regeneration(*args):
|
| 121 |
+
"""Randomizes checkboxes and returns new values for dropdowns."""
|
|
|
|
|
|
|
|
|
|
| 122 |
num_features = len(FEATURE_SEQUENCE)
|
| 123 |
current_values = list(args[:num_features])
|
| 124 |
+
checkboxes = args[num_features : num_features*2]
|
| 125 |
|
| 126 |
new_values = []
|
| 127 |
+
for i, (is_random, (cat, subcat, t_key)) in enumerate(zip(checkboxes, FEATURE_SEQUENCE)):
|
| 128 |
if is_random:
|
| 129 |
choices = list(features_data[cat][subcat].keys())
|
| 130 |
new_values.append(random.choice(choices))
|
|
|
|
| 133 |
|
| 134 |
return new_values
|
| 135 |
|
| 136 |
+
def save_character(*args):
|
| 137 |
+
"""Saves all current UI values to a JSON file."""
|
| 138 |
+
num_features = len(FEATURE_SEQUENCE)
|
| 139 |
+
feature_keys = args[:num_features]
|
| 140 |
+
checkboxes = args[num_features : num_features*2]
|
| 141 |
+
extra_infos = args[num_features*2 : num_features*2 + len(SECTIONS)]
|
| 142 |
+
|
| 143 |
+
data = {
|
| 144 |
+
"features": {FEATURE_SEQUENCE[i][2]: feature_keys[i] for i in range(num_features)},
|
| 145 |
+
"randomization": {FEATURE_SEQUENCE[i][2]: checkboxes[i] for i in range(num_features)},
|
| 146 |
+
"extra_info": {SECTIONS[i].lower(): extra_infos[i] for i in range(len(SECTIONS))}
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
# Save to a file with a friendly name in a temp directory
|
| 150 |
+
temp_dir = tempfile.mkdtemp()
|
| 151 |
+
path = os.path.join(temp_dir, "rpg_character_config.json")
|
| 152 |
+
with open(path, 'w') as f:
|
| 153 |
+
json.dump(data, f, indent=4)
|
| 154 |
+
return path
|
| 155 |
+
|
| 156 |
+
def load_character(file):
|
| 157 |
+
"""Loads UI values from a JSON file."""
|
| 158 |
+
if file is None:
|
| 159 |
+
return [gr.update()] * (len(FEATURE_SEQUENCE) + len(SECTIONS))
|
| 160 |
+
|
| 161 |
+
try:
|
| 162 |
+
with open(file.name, 'r') as f:
|
| 163 |
+
data = json.load(f)
|
| 164 |
+
|
| 165 |
+
updates = []
|
| 166 |
+
# Update dropdowns
|
| 167 |
+
for _, _, t_key in FEATURE_SEQUENCE:
|
| 168 |
+
updates.append(data.get("features", {}).get(t_key, gr.update()))
|
| 169 |
+
|
| 170 |
+
# Note: We don't necessarily update checkboxes unless requested,
|
| 171 |
+
# but for a full load, let's just return the values for dropdowns and textboxes first.
|
| 172 |
+
# Following the pattern of handle_regeneration, we might need to return specific updates.
|
| 173 |
+
|
| 174 |
+
# Update extra info textboxes
|
| 175 |
+
for section in SECTIONS:
|
| 176 |
+
updates.append(data.get("extra_info", {}).get(section.lower(), ""))
|
| 177 |
+
|
| 178 |
+
return updates
|
| 179 |
+
except Exception as e:
|
| 180 |
+
print(f"Error loading character: {e}")
|
| 181 |
+
return [gr.update()] * (len(FEATURE_SEQUENCE) + len(SECTIONS))
|
| 182 |
+
|
| 183 |
+
def refine_with_gemini(prompt):
|
| 184 |
+
if not gemini_active:
|
| 185 |
+
return "Gemini API key not found in .env file."
|
| 186 |
+
|
| 187 |
+
system_prompt = (
|
| 188 |
+
"You are an expert prompt engineer for AI image generators. "
|
| 189 |
+
"Your task is to take the provided technical prompt and refine it into a more vivid, "
|
| 190 |
+
"artistic, and detailed description while maintaining all the core features. "
|
| 191 |
+
"Enhance the vocabulary, lighting, and textures to be professional quality. "
|
| 192 |
+
"IMPORTANT: RETURN ONLY THE REFINED PROMPT TEXT. DO NOT INCLUDE ANY CONVERSATIONAL FILLER, MARKDOWN CODE BLOCKS, OR EXPLANATIONS."
|
| 193 |
+
)
|
| 194 |
+
|
| 195 |
+
try:
|
| 196 |
+
response = client.models.generate_content(
|
| 197 |
+
model='gemini-3-pro-preview',
|
| 198 |
+
contents=f"{system_prompt}\n\nOriginal Prompt: {prompt}"
|
| 199 |
+
)
|
| 200 |
+
text = response.text.strip()
|
| 201 |
+
# Remove common markdown code block wrappings if present
|
| 202 |
+
if text.startswith("```"):
|
| 203 |
+
lines = text.splitlines()
|
| 204 |
+
if lines[0].startswith("```"):
|
| 205 |
+
lines = lines[1:]
|
| 206 |
+
if lines and lines[-1].startswith("```"):
|
| 207 |
+
lines = lines[:-1]
|
| 208 |
+
text = "\n".join(lines).strip()
|
| 209 |
+
return text
|
| 210 |
+
except Exception as e:
|
| 211 |
+
return f"Error refining prompt with Gemini: {e}"
|
| 212 |
+
|
| 213 |
+
def generate_image_with_gemini(refined_prompt, technical_prompt):
|
| 214 |
+
if not gemini_active:
|
| 215 |
+
return None, "Gemini API key not found in .env file."
|
| 216 |
+
|
| 217 |
+
# Priority: Refined Prompt -> Technical Prompt
|
| 218 |
+
final_prompt = refined_prompt.strip() if refined_prompt and refined_prompt.strip() else technical_prompt.strip()
|
| 219 |
+
|
| 220 |
+
if not final_prompt:
|
| 221 |
+
return None, "No prompt available for generation."
|
| 222 |
+
|
| 223 |
+
try:
|
| 224 |
+
# Using the new SDK's generate_images method
|
| 225 |
+
response = client.models.generate_images(
|
| 226 |
+
model='imagen-4.0-generate-001',
|
| 227 |
+
prompt=final_prompt
|
| 228 |
+
)
|
| 229 |
+
if response.generated_images:
|
| 230 |
+
img = Image.open(io.BytesIO(response.generated_images[0].image.image_bytes))
|
| 231 |
+
# Save as PNG to a temp file for download
|
| 232 |
+
temp_dir = tempfile.mkdtemp()
|
| 233 |
+
img_path = os.path.join(temp_dir, "rpg_portrait.png")
|
| 234 |
+
img.save(img_path, "PNG")
|
| 235 |
+
return img, img_path, f"Image generated using {'refined' if refined_prompt.strip() else 'technical'} prompt!"
|
| 236 |
+
return None, None, "Gemini Image generation did not return any images."
|
| 237 |
+
except Exception as e:
|
| 238 |
+
traceback.print_exc()
|
| 239 |
+
return None, f"Image Generation Error: {e}"
|
| 240 |
+
|
| 241 |
def build_ui():
|
| 242 |
+
with gr.Blocks(title="RPGPortrait Prompt Builder Pro") as demo:
|
| 243 |
+
gr.Markdown("# 🎨 RPGPortrait Prompt Builder Pro")
|
| 244 |
+
gr.Markdown("Create professional AI prompts and generate portraits with Gemini integration.")
|
| 245 |
|
| 246 |
dropdowns = []
|
| 247 |
checkboxes = []
|
| 248 |
+
extra_texts = []
|
| 249 |
|
| 250 |
def create_feature_ui(category, subcategory, label, default_value):
|
| 251 |
choices = list(features_data[category][subcategory].keys())
|
|
|
|
| 255 |
dropdowns.append(dd)
|
| 256 |
checkboxes.append(cb)
|
| 257 |
|
|
|
|
| 258 |
with gr.Row():
|
| 259 |
with gr.Column():
|
| 260 |
gr.Markdown("### 👤 Identity")
|
|
|
|
| 262 |
create_feature_ui('identity', 'class', "Class", "Fighter")
|
| 263 |
create_feature_ui('identity', 'gender', "Gender", "Male")
|
| 264 |
create_feature_ui('identity', 'age', "Age", "Young Adult")
|
| 265 |
+
extra_id = gr.Textbox(placeholder="Extra Identity details (e.g. lineage, title)", label="Additional Identity Info")
|
| 266 |
+
extra_texts.append(extra_id)
|
| 267 |
|
| 268 |
gr.Markdown("### 🎭 Expression & Pose")
|
| 269 |
create_feature_ui('expression_pose', 'expression', "Expression", "Determined")
|
|
|
|
| 277 |
create_feature_ui('appearance', 'build', "Build", "Average")
|
| 278 |
create_feature_ui('appearance', 'skin_tone', "Skin Tone", "Fair")
|
| 279 |
create_feature_ui('appearance', 'distinguishing_feature', "Distinguishing Feature", "None")
|
| 280 |
+
extra_app = gr.Textbox(placeholder="Extra Appearance details (e.g. tattoos, scars)", label="Additional Appearance Info")
|
| 281 |
+
extra_texts.append(extra_app)
|
| 282 |
|
| 283 |
with gr.Row():
|
| 284 |
with gr.Column():
|
| 285 |
gr.Markdown("### ⚔️ Equipment")
|
| 286 |
create_feature_ui('equipment', 'armor', "Armor/Clothing", "Travelers Clothes")
|
| 287 |
create_feature_ui('equipment', 'weapon', "Primary Weapon", "Longsword")
|
| 288 |
+
create_feature_ui('equipment', 'accessory', "Accessory 1", "None")
|
| 289 |
+
create_feature_ui('equipment', 'accessory', "Accessory 2", "None")
|
| 290 |
create_feature_ui('equipment', 'material', "Material Detail", "Weathered")
|
| 291 |
+
extra_eq = gr.Textbox(placeholder="Extra Equipment details (e.g. weapon enchantments)", label="Additional Equipment Info")
|
| 292 |
+
extra_texts.append(extra_eq)
|
| 293 |
|
| 294 |
with gr.Column():
|
| 295 |
gr.Markdown("### 🌍 Environment")
|
| 296 |
create_feature_ui('environment', 'background', "Background", "Forest")
|
| 297 |
create_feature_ui('environment', 'lighting', "Lighting", "Natural Sunlight")
|
| 298 |
create_feature_ui('environment', 'atmosphere', "Atmosphere", "Clear")
|
| 299 |
+
extra_env = gr.Textbox(placeholder="Extra Environment details (e.g. time of day, weather)", label="Additional Environment Info")
|
| 300 |
+
extra_texts.append(extra_env)
|
| 301 |
|
| 302 |
with gr.Row():
|
| 303 |
with gr.Column():
|
|
|
|
| 306 |
create_feature_ui('vfx_style', 'style', "Art Style", "Digital Illustration")
|
| 307 |
create_feature_ui('vfx_style', 'mood', "Mood", "Heroic")
|
| 308 |
create_feature_ui('vfx_style', 'camera', "Camera Angle", "Bust")
|
| 309 |
+
extra_sty = gr.Textbox(placeholder="Extra Style details (e.g. specific artists, colors)", label="Additional Style Info")
|
| 310 |
+
extra_texts.append(extra_sty)
|
| 311 |
|
| 312 |
with gr.Column():
|
| 313 |
gr.Markdown("### ⚙️ Technical")
|
|
|
|
| 316 |
gr.Markdown("---")
|
| 317 |
|
| 318 |
with gr.Row():
|
| 319 |
+
regenerate_btn = gr.Button("✨ Randomize Features", variant="secondary")
|
| 320 |
+
save_btn = gr.Button("💾 Save Character", variant="secondary")
|
| 321 |
+
load_btn = gr.UploadButton("📂 Load Character", file_types=[".json"], variant="secondary")
|
| 322 |
+
refine_btn = gr.Button("🧠 Refine with Gemini", variant="primary")
|
| 323 |
+
gen_img_btn = gr.Button("🖼️ Generate Image (Gemini)", variant="primary")
|
| 324 |
|
| 325 |
with gr.Row():
|
| 326 |
+
with gr.Column(scale=2):
|
| 327 |
+
prompt_output = gr.Textbox(label="Generated Technical Prompt", lines=5, interactive=False)
|
| 328 |
+
refined_output = gr.Textbox(label="Gemini Refined Prompt", lines=5, interactive=True)
|
| 329 |
+
with gr.Column(scale=1):
|
| 330 |
+
image_output = gr.Image(label="Gemini Generated Portrait")
|
| 331 |
+
download_img_btn = gr.DownloadButton("📥 Download Portrait (PNG)", variant="secondary", visible=False)
|
| 332 |
+
download_file = gr.File(label="Saved Character JSON", visible=False)
|
| 333 |
+
status_msg = gr.Markdown("")
|
| 334 |
|
| 335 |
+
all_input_components = dropdowns + extra_texts
|
| 336 |
+
|
| 337 |
+
# Automatic prompt update on any input change
|
| 338 |
+
for comp in all_input_components:
|
| 339 |
+
comp.change(fn=generate_prompt, inputs=all_input_components, outputs=prompt_output)
|
| 340 |
|
| 341 |
# Regenerate button logic
|
| 342 |
regenerate_btn.click(
|
| 343 |
fn=handle_regeneration,
|
| 344 |
+
inputs=dropdowns + checkboxes,
|
| 345 |
outputs=dropdowns
|
| 346 |
).then(
|
| 347 |
+
# Trigger prompt update after dropdowns change
|
| 348 |
fn=generate_prompt,
|
| 349 |
+
inputs=all_input_components,
|
| 350 |
outputs=prompt_output
|
| 351 |
)
|
| 352 |
+
|
| 353 |
+
# Gemini Refinement
|
| 354 |
+
refine_btn.click(
|
| 355 |
+
fn=refine_with_gemini,
|
| 356 |
+
inputs=prompt_output,
|
| 357 |
+
outputs=refined_output
|
| 358 |
+
)
|
| 359 |
+
|
| 360 |
+
# Gemini Image Generation
|
| 361 |
+
gen_img_btn.click(
|
| 362 |
+
fn=generate_image_with_gemini,
|
| 363 |
+
inputs=[refined_output, prompt_output],
|
| 364 |
+
outputs=[image_output, download_img_btn, status_msg]
|
| 365 |
+
).then(
|
| 366 |
+
fn=lambda x: gr.update(value=x, visible=True) if x else gr.update(visible=False),
|
| 367 |
+
inputs=download_img_btn,
|
| 368 |
+
outputs=download_img_btn
|
| 369 |
+
)
|
| 370 |
+
|
| 371 |
+
# Save/Load Logic
|
| 372 |
+
save_btn.click(
|
| 373 |
+
fn=save_character,
|
| 374 |
+
inputs=dropdowns + checkboxes + extra_texts,
|
| 375 |
+
outputs=download_file
|
| 376 |
+
).then(
|
| 377 |
+
fn=lambda: gr.update(visible=True),
|
| 378 |
+
outputs=download_file
|
| 379 |
+
)
|
| 380 |
+
|
| 381 |
+
load_btn.upload(
|
| 382 |
+
fn=load_character,
|
| 383 |
+
inputs=load_btn,
|
| 384 |
+
outputs=dropdowns + extra_texts
|
| 385 |
+
)
|
| 386 |
|
| 387 |
+
# Initialize
|
| 388 |
+
demo.load(fn=generate_prompt, inputs=all_input_components, outputs=prompt_output)
|
| 389 |
|
| 390 |
return demo
|
| 391 |
|
| 392 |
if __name__ == "__main__":
|
| 393 |
demo = build_ui()
|
| 394 |
+
demo.launch(theme=gr.themes.Soft())
|
features.yaml
CHANGED
|
@@ -105,6 +105,9 @@ equipment:
|
|
| 105 |
Chainmail: "clad in a suit of shimmering, interlocking chainmail"
|
| 106 |
Travelers Clothes: "dressed in practical, durable travelers' garments"
|
| 107 |
Elegant Robes: "wearing flowing, silk-threaded elegant robes with intricate embroidery"
|
|
|
|
|
|
|
|
|
|
| 108 |
Ragged Tunic: "attired in a simple, worn, and slightly ragged tunic"
|
| 109 |
weapon:
|
| 110 |
Longsword: "confidently wielding a gleaming steel longsword in a two-handed grip"
|
|
@@ -112,6 +115,14 @@ equipment:
|
|
| 112 |
Daggers: "gripping a pair of obsidian daggers, ready for a swift strike"
|
| 113 |
Bow and Quiver: "carrying a finely crafted longbow with a full quiver of arrows slung over the shoulder"
|
| 114 |
Greataxe: "resting a massive, notched greataxe upon one shoulder"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
Shield: "defensively holding a heavy heater shield emblazoned with a crest"
|
| 116 |
None: "with hands held open and empty"
|
| 117 |
accessory:
|
|
@@ -119,6 +130,12 @@ equipment:
|
|
| 119 |
Hood: "a deep hood casting shadows over the face"
|
| 120 |
Amulet: "a glowing amulet hanging from a silver chain"
|
| 121 |
Belt with Pouches: "a sturdy leather belt laden with various pouches and tools"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
Eyepatch: "a leather eyepatch covering one eye"
|
| 123 |
None: "no visible accessories"
|
| 124 |
material:
|
|
@@ -193,4 +210,4 @@ technical:
|
|
| 193 |
"2:3": "--ar 2:3"
|
| 194 |
|
| 195 |
templates:
|
| 196 |
-
default: "A {mood} {style} portrait of {race} {class}, {gender}, {age}. {build} build, {hair_style} {hair_color} hair, {eye_color} eyes, with {distinguishing_feature}. The character is {expression} while {pose}, {armor}, {weapon}
|
|
|
|
| 105 |
Chainmail: "clad in a suit of shimmering, interlocking chainmail"
|
| 106 |
Travelers Clothes: "dressed in practical, durable travelers' garments"
|
| 107 |
Elegant Robes: "wearing flowing, silk-threaded elegant robes with intricate embroidery"
|
| 108 |
+
Bone Armor: "wearing intimidating, primal armor crafted from the bones of prehistoric beasts"
|
| 109 |
+
Arcane Plate: "clad in futuristic, bioluminescent plate armor etched with glowing circuitry"
|
| 110 |
+
Leaf-weave Armor: "attired in flexible armor made of magically hardened, vibrant-green leaves"
|
| 111 |
Ragged Tunic: "attired in a simple, worn, and slightly ragged tunic"
|
| 112 |
weapon:
|
| 113 |
Longsword: "confidently wielding a gleaming steel longsword in a two-handed grip"
|
|
|
|
| 115 |
Daggers: "gripping a pair of obsidian daggers, ready for a swift strike"
|
| 116 |
Bow and Quiver: "carrying a finely crafted longbow with a full quiver of arrows slung over the shoulder"
|
| 117 |
Greataxe: "resting a massive, notched greataxe upon one shoulder"
|
| 118 |
+
Katana: "holding a slender, razor-sharp katana with a traditional silken hilt-wrap"
|
| 119 |
+
Whip-blade: "wielding a segmented, metallic whip-blade that can transition between sword and whip"
|
| 120 |
+
Twin Scimitars: "deftly handling a pair of curved, engraved scimitars"
|
| 121 |
+
Meteor Hammer: "swinging a heavy, spiked metal sphere attached to a long, sturdy chain"
|
| 122 |
+
Primitive Club: "gripping a heavy, notched primitive club made of dark, weathered wood"
|
| 123 |
+
Rapier: "wielding a slender, elegant rapier with an intricate basket hilt"
|
| 124 |
+
Morning Star: "holding a lethal, spiked morning star, its heavy head gleaming with menace"
|
| 125 |
+
Sword and Shield: "wielding a balanced shortsword in one hand and a sturdy, emblazoned heater shield in the other"
|
| 126 |
Shield: "defensively holding a heavy heater shield emblazoned with a crest"
|
| 127 |
None: "with hands held open and empty"
|
| 128 |
accessory:
|
|
|
|
| 130 |
Hood: "a deep hood casting shadows over the face"
|
| 131 |
Amulet: "a glowing amulet hanging from a silver chain"
|
| 132 |
Belt with Pouches: "a sturdy leather belt laden with various pouches and tools"
|
| 133 |
+
Feathered Hat: "wearing a wide-brimmed hat adorned with a long, colorful feather"
|
| 134 |
+
Silk Scarf: "wrapped in a long, vibrant silk scarf that catches the breeze"
|
| 135 |
+
Adventurer's Backpack: "shouldering a heavy leather backpack laden with supplies and rope"
|
| 136 |
+
Oil Lantern: "carrying a flickering oil lantern that casts a warm, localized glow"
|
| 137 |
+
Ornate Mask: "wearing a decorative gold and velvet masquerade mask"
|
| 138 |
+
Jeweled Circlet: "adorned with a delicate jeweled circlet that rests upon the brow"
|
| 139 |
Eyepatch: "a leather eyepatch covering one eye"
|
| 140 |
None: "no visible accessories"
|
| 141 |
material:
|
|
|
|
| 210 |
"2:3": "--ar 2:3"
|
| 211 |
|
| 212 |
templates:
|
| 213 |
+
default: "A {mood} {style} portrait of {race} {class}, {gender}, {age}. {build} build, {hair_style} {hair_color} hair, {eye_color} eyes, with {distinguishing_feature}. The character is {expression} while {pose}, {armor}, {weapon}{accessories}. All equipment is {material}. The setting is a {background}, {lighting}, {atmosphere}. {vfx}. {camera}. Highly detailed, 8k resolution. {aspect_ratio}"
|
requirements.txt
CHANGED
|
@@ -1,2 +1,4 @@
|
|
| 1 |
gradio
|
| 2 |
PyYAML
|
|
|
|
|
|
|
|
|
| 1 |
gradio
|
| 2 |
PyYAML
|
| 3 |
+
python-dotenv
|
| 4 |
+
google-genai
|