Spaces:
Sleeping
Sleeping
MelkortheCorrupt
commited on
Commit
·
409063d
1
Parent(s):
7c6055c
Deploy D&D Toolkit
Browse files- .gitignore +23 -0
- utils/__init__.py +0 -0
- utils/generators.py +661 -0
- utils/image_utils.py +181 -0
.gitignore
ADDED
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
# Virtual environments
|
3 |
+
env/
|
4 |
+
venv/
|
5 |
+
.env
|
6 |
+
|
7 |
+
# Python cache
|
8 |
+
__pycache__/
|
9 |
+
*.pyc
|
10 |
+
*.pyo
|
11 |
+
|
12 |
+
# Temporary files
|
13 |
+
temp_*.png
|
14 |
+
placeholder_*.png
|
15 |
+
*.tmp
|
16 |
+
generation_history_*.json
|
17 |
+
|
18 |
+
# Mac files
|
19 |
+
.DS_Store
|
20 |
+
|
21 |
+
# IDE files
|
22 |
+
.vscode/
|
23 |
+
.idea/
|
utils/__init__.py
ADDED
File without changes
|
utils/generators.py
ADDED
@@ -0,0 +1,661 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import random
|
2 |
+
from typing import Dict, List, Any, Optional
|
3 |
+
|
4 |
+
# ===== DATA BANKS =====
|
5 |
+
|
6 |
+
RACES = ["Human", "Elf", "Dwarf", "Halfling", "Dragonborn", "Gnome", "Half-Elf", "Half-Orc", "Tiefling"]
|
7 |
+
CLASSES = ["Fighter", "Wizard", "Rogue", "Cleric", "Ranger", "Paladin", "Barbarian", "Bard", "Druid", "Monk", "Sorcerer", "Warlock"]
|
8 |
+
ALIGNMENTS = ["Lawful Good", "Neutral Good", "Chaotic Good", "Lawful Neutral", "True Neutral", "Chaotic Neutral", "Lawful Evil", "Neutral Evil", "Chaotic Evil"]
|
9 |
+
BACKGROUNDS = ["Acolyte", "Criminal", "Folk Hero", "Noble", "Sage", "Soldier", "Charlatan", "Entertainer", "Guild Artisan", "Hermit", "Outlander", "Sailor"]
|
10 |
+
|
11 |
+
PERSONALITY_TRAITS = [
|
12 |
+
"I always have a plan for what to do when things go wrong",
|
13 |
+
"I am always calm, no matter what the situation",
|
14 |
+
"I would rather make a new friend than a new enemy",
|
15 |
+
"I have a strong sense of fair play and always try to find the most equitable solution",
|
16 |
+
"I'm confident in my own abilities and do what I can to instill confidence in others"
|
17 |
+
]
|
18 |
+
|
19 |
+
IDEALS = [
|
20 |
+
"Respect. People deserve to be treated with dignity and respect",
|
21 |
+
"Fairness. No one should get preferential treatment before the law",
|
22 |
+
"Freedom. Chains are meant to be broken, as are those who would forge them",
|
23 |
+
"Might. The strongest are meant to rule",
|
24 |
+
"Sincerity. There's no good in pretending to be something I'm not"
|
25 |
+
]
|
26 |
+
|
27 |
+
BONDS = [
|
28 |
+
"I would die to recover an ancient relic that was stolen from my temple",
|
29 |
+
"Someone I loved died because of a mistake I made",
|
30 |
+
"I owe my life to the priest who took me in when my parents died",
|
31 |
+
"Everything I do is for the common people",
|
32 |
+
"I will face any challenge to win the approval of my family"
|
33 |
+
]
|
34 |
+
|
35 |
+
FLAWS = [
|
36 |
+
"I turn tail and run when things look bad",
|
37 |
+
"I have a 'tell' that reveals when I'm lying",
|
38 |
+
"I can't keep a secret to save my life",
|
39 |
+
"I'm too greedy for my own good",
|
40 |
+
"I can't resist a pretty face"
|
41 |
+
]
|
42 |
+
|
43 |
+
OCCUPATIONS = [
|
44 |
+
"Merchant", "Guard", "Innkeeper", "Blacksmith", "Priest", "Noble", "Farmer",
|
45 |
+
"Scholar", "Thief", "Soldier", "Alchemist", "Baker", "Carpenter", "Hunter"
|
46 |
+
]
|
47 |
+
|
48 |
+
MOTIVATIONS = [
|
49 |
+
"Seeking wealth and power", "Protecting their family", "Serving their god", "Pursuing knowledge",
|
50 |
+
"Seeking revenge", "Finding love", "Escaping their past", "Gaining recognition"
|
51 |
+
]
|
52 |
+
|
53 |
+
SECRETS = [
|
54 |
+
"Is actually a spy for a rival faction", "Has a hidden magical talent", "Is fleeing from a dark past",
|
55 |
+
"Is secretly in love with someone inappropriate", "Has a gambling addiction", "Is being blackmailed"
|
56 |
+
]
|
57 |
+
|
58 |
+
ITEM_TYPES = {
|
59 |
+
"Weapon": ["Sword", "Axe", "Bow", "Dagger", "Mace", "Spear", "Crossbow", "Warhammer"],
|
60 |
+
"Armor": ["Chain Mail", "Plate Armor", "Leather Armor", "Scale Mail", "Hide Armor"],
|
61 |
+
"Shield": ["Round Shield", "Tower Shield", "Buckler", "Kite Shield"],
|
62 |
+
"Accessory": ["Ring", "Amulet", "Cloak", "Belt", "Bracers", "Crown", "Boots"],
|
63 |
+
"Consumable": ["Potion", "Scroll", "Oil", "Poison", "Bomb", "Elixir"],
|
64 |
+
"Tool": ["Lockpicks", "Rope", "Lantern", "Compass", "Spyglass", "Hammer"],
|
65 |
+
"Treasure": ["Gem", "Coins", "Art Object", "Jewelry", "Statue", "Book"]
|
66 |
+
}
|
67 |
+
|
68 |
+
MAGICAL_PROPERTIES = [
|
69 |
+
"+1 to attack and damage rolls", "Resistance to fire damage", "Advantage on stealth checks",
|
70 |
+
"Cast a spell once per day", "Glows in the presence of evil", "Never breaks or dulls",
|
71 |
+
"Returns when thrown", "Speaks telepathically", "Changes color based on emotion"
|
72 |
+
]
|
73 |
+
|
74 |
+
LOCATION_TYPES = {
|
75 |
+
"City": ["Metropolis", "Trading Hub", "Capital", "Port City", "Fortress City"],
|
76 |
+
"Village": ["Farming Village", "Mining Town", "Fishing Village", "Trading Post"],
|
77 |
+
"Dungeon": ["Ancient Tomb", "Underground Lair", "Ruined Tower", "Cave System"],
|
78 |
+
"Castle": ["Royal Castle", "Fortress", "Ruined Keep", "Wizard's Tower"],
|
79 |
+
"Forest": ["Ancient Woods", "Dark Forest", "Enchanted Grove", "Sacred Grove"],
|
80 |
+
"Temple": ["Cathedral", "Shrine", "Monastery", "Sacred Site"],
|
81 |
+
"Tavern": ["Roadside Inn", "City Tavern", "Adventurer's Rest", "Noble's Club"]
|
82 |
+
}
|
83 |
+
|
84 |
+
FACTION_TYPES = {
|
85 |
+
"Guild": ["Merchant Guild", "Thieves Guild", "Artisan Guild", "Adventurer's Guild"],
|
86 |
+
"Religious Order": ["Temple", "Monastery", "Cult", "Holy Order"],
|
87 |
+
"Military": ["Army", "Guard", "Mercenaries", "Knights"],
|
88 |
+
"Criminal": ["Thieves", "Smugglers", "Assassins", "Pirates"],
|
89 |
+
"Political": ["Royal Court", "City Council", "Rebels", "Loyalists"],
|
90 |
+
"Academic": ["University", "Research Group", "Library", "School"]
|
91 |
+
}
|
92 |
+
|
93 |
+
DEITY_DOMAINS = {
|
94 |
+
"War": ["Battle", "Strategy", "Conquest", "Honor"],
|
95 |
+
"Knowledge": ["Wisdom", "Learning", "Magic", "Secrets"],
|
96 |
+
"Life": ["Healing", "Growth", "Birth", "Protection"],
|
97 |
+
"Death": ["Endings", "Judgment", "Undeath", "Graves"],
|
98 |
+
"Nature": ["Animals", "Plants", "Seasons", "Weather"],
|
99 |
+
"Light": ["Sun", "Dawn", "Hope", "Renewal"],
|
100 |
+
"Trickery": ["Deception", "Theft", "Luck", "Mischief"]
|
101 |
+
}
|
102 |
+
|
103 |
+
# ===== MAIN GENERATION FUNCTIONS =====
|
104 |
+
|
105 |
+
def generate_character(name: str = "", race: str = "Random", char_class: str = "Random",
|
106 |
+
level: int = 5, alignment: str = "Random") -> Dict[str, Any]:
|
107 |
+
"""Generate a complete D&D character"""
|
108 |
+
|
109 |
+
if not name:
|
110 |
+
name = generate_fantasy_name()
|
111 |
+
if race == "Random":
|
112 |
+
race = random.choice(RACES)
|
113 |
+
if char_class == "Random":
|
114 |
+
char_class = random.choice(CLASSES)
|
115 |
+
if alignment == "Random":
|
116 |
+
alignment = random.choice(ALIGNMENTS)
|
117 |
+
|
118 |
+
background = random.choice(BACKGROUNDS)
|
119 |
+
personality = random.choice(PERSONALITY_TRAITS)
|
120 |
+
ideal = random.choice(IDEALS)
|
121 |
+
bond = random.choice(BONDS)
|
122 |
+
flaw = random.choice(FLAWS)
|
123 |
+
appearance = generate_appearance(race)
|
124 |
+
equipment = generate_class_equipment(char_class, level)
|
125 |
+
backstory = generate_backstory(name, race, char_class, background)
|
126 |
+
hooks = generate_adventure_hooks(name, background)
|
127 |
+
|
128 |
+
return {
|
129 |
+
"name": name,
|
130 |
+
"race": race,
|
131 |
+
"character_class": char_class,
|
132 |
+
"level": level,
|
133 |
+
"alignment": alignment,
|
134 |
+
"background": background,
|
135 |
+
"personality": personality,
|
136 |
+
"ideal": ideal,
|
137 |
+
"bond": bond,
|
138 |
+
"flaw": flaw,
|
139 |
+
"appearance": appearance,
|
140 |
+
"equipment": equipment,
|
141 |
+
"backstory": backstory,
|
142 |
+
"hooks": hooks
|
143 |
+
}
|
144 |
+
|
145 |
+
def generate_npc(name: str = "", race: str = "Random", occupation: str = "Random",
|
146 |
+
location: str = "", relationship: str = "Neutral") -> Dict[str, Any]:
|
147 |
+
"""Generate a detailed NPC"""
|
148 |
+
|
149 |
+
if not name:
|
150 |
+
name = generate_fantasy_name()
|
151 |
+
if race == "Random":
|
152 |
+
race = random.choice(RACES)
|
153 |
+
if occupation == "Random":
|
154 |
+
occupation = random.choice(OCCUPATIONS)
|
155 |
+
|
156 |
+
alignment = random.choice(ALIGNMENTS)
|
157 |
+
personality = random.choice(PERSONALITY_TRAITS)
|
158 |
+
appearance = generate_appearance(race)
|
159 |
+
motivation = random.choice(MOTIVATIONS)
|
160 |
+
secret = random.choice(SECRETS)
|
161 |
+
relationships = generate_relationships(name, occupation)
|
162 |
+
|
163 |
+
return {
|
164 |
+
"name": name,
|
165 |
+
"race": race,
|
166 |
+
"occupation": occupation,
|
167 |
+
"alignment": alignment,
|
168 |
+
"personality": personality,
|
169 |
+
"appearance": appearance,
|
170 |
+
"motivation": motivation,
|
171 |
+
"secret": secret,
|
172 |
+
"relationships": relationships,
|
173 |
+
"location": location or "Unknown",
|
174 |
+
"relationship_to_party": relationship
|
175 |
+
}
|
176 |
+
|
177 |
+
def generate_item(name: str = "", item_type: str = "Random", rarity: str = "Random",
|
178 |
+
magical: bool = True, cursed: bool = False) -> Dict[str, Any]:
|
179 |
+
"""Generate a magical or mundane item"""
|
180 |
+
|
181 |
+
if item_type == "Random":
|
182 |
+
item_type = random.choice(list(ITEM_TYPES.keys()))
|
183 |
+
|
184 |
+
if not name:
|
185 |
+
base_item = random.choice(ITEM_TYPES[item_type])
|
186 |
+
name = generate_item_name(base_item, magical)
|
187 |
+
|
188 |
+
if rarity == "Random":
|
189 |
+
rarity = random.choice(["Common", "Uncommon", "Rare", "Very Rare", "Legendary"])
|
190 |
+
|
191 |
+
description = generate_item_description(name, item_type, magical)
|
192 |
+
properties = []
|
193 |
+
|
194 |
+
if magical:
|
195 |
+
num_properties = {"Common": 1, "Uncommon": 1, "Rare": 2, "Very Rare": 3, "Legendary": 4}.get(rarity, 1)
|
196 |
+
properties = random.sample(MAGICAL_PROPERTIES, min(num_properties, len(MAGICAL_PROPERTIES)))
|
197 |
+
|
198 |
+
if cursed:
|
199 |
+
properties.append(generate_curse())
|
200 |
+
|
201 |
+
base_values = {"Common": 50, "Uncommon": 500, "Rare": 5000, "Very Rare": 50000, "Legendary": 500000}
|
202 |
+
value = f"{base_values.get(rarity, 50):,} gp"
|
203 |
+
|
204 |
+
requirements = "Attunement" if rarity in ["Rare", "Very Rare", "Legendary"] and magical else "None"
|
205 |
+
|
206 |
+
return {
|
207 |
+
"name": name,
|
208 |
+
"item_type": item_type,
|
209 |
+
"rarity": rarity,
|
210 |
+
"description": description,
|
211 |
+
"properties": properties,
|
212 |
+
"value": value,
|
213 |
+
"requirements": requirements,
|
214 |
+
"magical": magical,
|
215 |
+
"cursed": cursed
|
216 |
+
}
|
217 |
+
|
218 |
+
def generate_location(name: str = "", loc_type: str = "Random", size: str = "Medium",
|
219 |
+
danger_level: str = "Moderate", theme: str = "Standard Fantasy") -> Dict[str, Any]:
|
220 |
+
"""Generate a detailed location"""
|
221 |
+
|
222 |
+
if loc_type == "Random":
|
223 |
+
loc_type = random.choice(list(LOCATION_TYPES.keys()))
|
224 |
+
|
225 |
+
if not name:
|
226 |
+
subtype = random.choice(LOCATION_TYPES[loc_type])
|
227 |
+
name = generate_location_name(subtype)
|
228 |
+
|
229 |
+
description = generate_location_description(name, loc_type, size, theme)
|
230 |
+
inhabitants = generate_inhabitants(loc_type, size)
|
231 |
+
features = generate_location_features(loc_type)
|
232 |
+
dangers = generate_location_dangers(danger_level)
|
233 |
+
treasures = generate_location_treasures(danger_level)
|
234 |
+
atmosphere = generate_atmosphere(loc_type, theme)
|
235 |
+
|
236 |
+
return {
|
237 |
+
"name": name,
|
238 |
+
"location_type": loc_type,
|
239 |
+
"size": size,
|
240 |
+
"danger_level": danger_level,
|
241 |
+
"theme": theme,
|
242 |
+
"description": description,
|
243 |
+
"inhabitants": inhabitants,
|
244 |
+
"features": features,
|
245 |
+
"dangers": dangers,
|
246 |
+
"treasures": treasures,
|
247 |
+
"atmosphere": atmosphere
|
248 |
+
}
|
249 |
+
|
250 |
+
def generate_faction(name: str = "", faction_type: str = "Random", alignment: str = "Random",
|
251 |
+
size: str = "Medium", influence: str = "Regional") -> Dict[str, Any]:
|
252 |
+
"""Generate a detailed faction"""
|
253 |
+
|
254 |
+
if faction_type == "Random":
|
255 |
+
faction_type = random.choice(list(FACTION_TYPES.keys()))
|
256 |
+
|
257 |
+
if not name:
|
258 |
+
subtype = random.choice(FACTION_TYPES[faction_type])
|
259 |
+
name = generate_faction_name(subtype)
|
260 |
+
|
261 |
+
if alignment == "Random":
|
262 |
+
alignment = random.choice(ALIGNMENTS)
|
263 |
+
|
264 |
+
goals = generate_faction_goals(faction_type)
|
265 |
+
methods = generate_faction_methods(alignment)
|
266 |
+
resources = generate_faction_resources(size)
|
267 |
+
enemies = generate_faction_enemies(faction_type)
|
268 |
+
allies = generate_faction_allies(faction_type)
|
269 |
+
headquarters = generate_faction_headquarters(faction_type)
|
270 |
+
|
271 |
+
return {
|
272 |
+
"name": name,
|
273 |
+
"faction_type": faction_type,
|
274 |
+
"alignment": alignment,
|
275 |
+
"size": size,
|
276 |
+
"influence": influence,
|
277 |
+
"goals": goals,
|
278 |
+
"methods": methods,
|
279 |
+
"resources": resources,
|
280 |
+
"enemies": enemies,
|
281 |
+
"allies": allies,
|
282 |
+
"headquarters": headquarters
|
283 |
+
}
|
284 |
+
|
285 |
+
def generate_deity(name: str = "", domain: str = "Random", alignment: str = "Random",
|
286 |
+
pantheon: str = "Generic Fantasy", power_level: str = "Intermediate") -> Dict[str, Any]:
|
287 |
+
"""Generate a detailed deity"""
|
288 |
+
|
289 |
+
if domain == "Random":
|
290 |
+
domain = random.choice(list(DEITY_DOMAINS.keys()))
|
291 |
+
|
292 |
+
if not name:
|
293 |
+
name = generate_deity_name(domain, pantheon)
|
294 |
+
|
295 |
+
if alignment == "Random":
|
296 |
+
alignment = random.choice(ALIGNMENTS)
|
297 |
+
|
298 |
+
appearance = generate_deity_appearance(domain, alignment)
|
299 |
+
personality = generate_deity_personality(domain)
|
300 |
+
portfolio = random.sample(DEITY_DOMAINS[domain], min(3, len(DEITY_DOMAINS[domain])))
|
301 |
+
worshippers = generate_deity_worshippers(domain)
|
302 |
+
temples = generate_deity_temples(power_level)
|
303 |
+
holy_symbol = generate_holy_symbol(domain)
|
304 |
+
|
305 |
+
return {
|
306 |
+
"name": name,
|
307 |
+
"domain": domain,
|
308 |
+
"alignment": alignment,
|
309 |
+
"pantheon": pantheon,
|
310 |
+
"power_level": power_level,
|
311 |
+
"appearance": appearance,
|
312 |
+
"personality": personality,
|
313 |
+
"portfolio": portfolio,
|
314 |
+
"worshippers": worshippers,
|
315 |
+
"temples": temples,
|
316 |
+
"holy_symbol": holy_symbol
|
317 |
+
}
|
318 |
+
|
319 |
+
def generate_scenario(title: str = "", scenario_type: str = "Random", level: str = "Medium",
|
320 |
+
duration: str = "Medium (2-3 hours)", setting: str = "Mixed") -> Dict[str, Any]:
|
321 |
+
"""Generate a detailed scenario/adventure"""
|
322 |
+
|
323 |
+
scenario_types = ["Combat Encounter", "Social Encounter", "Exploration", "Mystery",
|
324 |
+
"Heist", "Rescue", "Dungeon Crawl", "Political Intrigue"]
|
325 |
+
|
326 |
+
if scenario_type == "Random":
|
327 |
+
scenario_type = random.choice(scenario_types)
|
328 |
+
|
329 |
+
if not title:
|
330 |
+
title = generate_scenario_title(scenario_type)
|
331 |
+
|
332 |
+
description = generate_scenario_description(title, scenario_type, setting)
|
333 |
+
objective = generate_scenario_objective(scenario_type)
|
334 |
+
complications = generate_scenario_complications(level)
|
335 |
+
npcs_involved = generate_scenario_npcs(scenario_type)
|
336 |
+
locations = generate_scenario_locations(setting)
|
337 |
+
rewards = generate_scenario_rewards(level)
|
338 |
+
hooks = generate_scenario_hooks(scenario_type, title)
|
339 |
+
|
340 |
+
return {
|
341 |
+
"title": title,
|
342 |
+
"scenario_type": scenario_type,
|
343 |
+
"difficulty_level": level,
|
344 |
+
"duration": duration,
|
345 |
+
"setting": setting,
|
346 |
+
"description": description,
|
347 |
+
"objective": objective,
|
348 |
+
"complications": complications,
|
349 |
+
"npcs_involved": npcs_involved,
|
350 |
+
"locations": locations,
|
351 |
+
"rewards": rewards,
|
352 |
+
"hooks": hooks
|
353 |
+
}
|
354 |
+
|
355 |
+
# ===== HELPER FUNCTIONS =====
|
356 |
+
|
357 |
+
def generate_fantasy_name() -> str:
|
358 |
+
"""Generate a random fantasy name"""
|
359 |
+
prefixes = ["Ar", "El", "Gal", "Mor", "Sil", "Tha", "Val", "Zar"]
|
360 |
+
middles = ["and", "eth", "dor", "wen", "ion", "las", "mir", "nal"]
|
361 |
+
suffixes = ["iel", "wen", "dil", "las", "mir", "nal", "oth", "ril"]
|
362 |
+
|
363 |
+
return random.choice(prefixes) + random.choice(middles) + random.choice(suffixes)
|
364 |
+
|
365 |
+
def generate_appearance(race: str) -> str:
|
366 |
+
"""Generate physical appearance based on race"""
|
367 |
+
appearances = {
|
368 |
+
"Human": "tall and sturdy with kind eyes",
|
369 |
+
"Elf": "tall and graceful with ethereal beauty",
|
370 |
+
"Dwarf": "short and stocky with a braided beard",
|
371 |
+
"Halfling": "small and cheerful with curly hair",
|
372 |
+
"Dragonborn": "scaled skin with draconic features",
|
373 |
+
"Tiefling": "demonic horns with exotic features"
|
374 |
+
}
|
375 |
+
|
376 |
+
return appearances.get(race, "distinctive appearance")
|
377 |
+
|
378 |
+
def generate_class_equipment(char_class: str, level: int) -> List[str]:
|
379 |
+
"""Generate equipment based on class"""
|
380 |
+
base_equipment = {
|
381 |
+
"Fighter": ["Sword", "Shield", "Armor"],
|
382 |
+
"Wizard": ["Spellbook", "Staff", "Robes"],
|
383 |
+
"Rogue": ["Daggers", "Thieves' Tools", "Leather Armor"],
|
384 |
+
"Cleric": ["Mace", "Shield", "Holy Symbol"],
|
385 |
+
"Ranger": ["Bow", "Arrows", "Survival Gear"]
|
386 |
+
}
|
387 |
+
|
388 |
+
equipment = base_equipment.get(char_class, ["Basic Gear"])
|
389 |
+
|
390 |
+
if level > 5:
|
391 |
+
equipment.append("Magic Item")
|
392 |
+
|
393 |
+
return equipment
|
394 |
+
|
395 |
+
def generate_backstory(name: str, race: str, char_class: str, background: str) -> str:
|
396 |
+
"""Generate character backstory"""
|
397 |
+
return f"{name} grew up as a {background.lower()} among the {race.lower()} people, drawn to the path of the {char_class.lower()}."
|
398 |
+
|
399 |
+
def generate_adventure_hooks(name: str, background: str) -> List[str]:
|
400 |
+
"""Generate adventure hooks"""
|
401 |
+
return [
|
402 |
+
f"{name}'s {background.lower()} background connects to this adventure",
|
403 |
+
f"An old contact needs {name}'s help",
|
404 |
+
f"The character's past comes back to haunt them"
|
405 |
+
]
|
406 |
+
|
407 |
+
def generate_item_name(base_item: str, magical: bool) -> str:
|
408 |
+
"""Generate item names"""
|
409 |
+
if not magical:
|
410 |
+
return base_item
|
411 |
+
|
412 |
+
prefixes = ["Enchanted", "Mystic", "Ancient", "Blessed", "Legendary"]
|
413 |
+
return f"{random.choice(prefixes)} {base_item}"
|
414 |
+
|
415 |
+
def generate_curse() -> str:
|
416 |
+
"""Generate item curse"""
|
417 |
+
curses = [
|
418 |
+
"Compels the wielder to attack allies",
|
419 |
+
"Drains health slowly",
|
420 |
+
"Causes the wielder to speak in riddles",
|
421 |
+
"Makes the wielder tell the truth"
|
422 |
+
]
|
423 |
+
return random.choice(curses)
|
424 |
+
|
425 |
+
def generate_item_description(name: str, item_type: str, magical: bool) -> str:
|
426 |
+
"""Generate item descriptions"""
|
427 |
+
base = f"A well-crafted {item_type.lower()}"
|
428 |
+
if magical:
|
429 |
+
return f"{base} emanating with magical energy"
|
430 |
+
return f"{base} of excellent quality"
|
431 |
+
|
432 |
+
def generate_location_name(subtype: str) -> str:
|
433 |
+
"""Generate location names"""
|
434 |
+
prefixes = ["Ancient", "Lost", "Hidden", "Sacred", "Golden"]
|
435 |
+
return f"{random.choice(prefixes)} {subtype}"
|
436 |
+
|
437 |
+
def generate_location_description(name: str, loc_type: str, size: str, theme: str) -> str:
|
438 |
+
"""Generate location descriptions"""
|
439 |
+
return f"A {size.lower()} {loc_type.lower()} with {theme.lower()} characteristics"
|
440 |
+
|
441 |
+
def generate_inhabitants(loc_type: str, size: str) -> List[str]:
|
442 |
+
"""Generate location inhabitants"""
|
443 |
+
base_inhabitants = {
|
444 |
+
"City": ["Merchants", "Guards", "Nobles"],
|
445 |
+
"Village": ["Farmers", "Blacksmith", "Elder"],
|
446 |
+
"Dungeon": ["Monsters", "Undead", "Bandits"]
|
447 |
+
}
|
448 |
+
return base_inhabitants.get(loc_type, ["Various Creatures"])
|
449 |
+
|
450 |
+
def generate_location_features(loc_type: str) -> List[str]:
|
451 |
+
"""Generate location features"""
|
452 |
+
return ["Notable landmark", "Gathering place", "Hidden area"]
|
453 |
+
|
454 |
+
def generate_location_dangers(danger_level: str) -> List[str]:
|
455 |
+
"""Generate location dangers"""
|
456 |
+
dangers = {
|
457 |
+
"Safe": ["Pickpockets", "Overpriced goods"],
|
458 |
+
"Low": ["Wild animals", "Natural hazards"],
|
459 |
+
"Moderate": ["Monsters", "Traps"],
|
460 |
+
"High": ["Deadly creatures", "Curses"],
|
461 |
+
"Extreme": ["Ancient evils", "Reality distortions"]
|
462 |
+
}
|
463 |
+
return dangers.get(danger_level, ["Unknown threats"])
|
464 |
+
|
465 |
+
def generate_location_treasures(danger_level: str) -> List[str]:
|
466 |
+
"""Generate location treasures"""
|
467 |
+
treasures = {
|
468 |
+
"Safe": ["Copper coins", "Common items"],
|
469 |
+
"Moderate": ["Gold coins", "Rare items"],
|
470 |
+
"Extreme": ["Legendary items", "Artifacts"]
|
471 |
+
}
|
472 |
+
return treasures.get(danger_level, ["Hidden treasures"])
|
473 |
+
|
474 |
+
def generate_atmosphere(loc_type: str, theme: str) -> str:
|
475 |
+
"""Generate atmospheric description"""
|
476 |
+
return f"The air is thick with {random.choice(['mystery', 'tension', 'magic'])}"
|
477 |
+
|
478 |
+
def generate_relationships(name: str, occupation: str) -> List[str]:
|
479 |
+
"""Generate NPC relationships"""
|
480 |
+
return [
|
481 |
+
f"Knows the local {random.choice(['merchant', 'guard', 'priest'])}",
|
482 |
+
f"Has connections to {random.choice(['traders', 'criminals', 'officials'])}",
|
483 |
+
f"Family ties to nearby communities"
|
484 |
+
]
|
485 |
+
|
486 |
+
# Faction functions
|
487 |
+
def generate_faction_name(subtype: str) -> str:
|
488 |
+
"""Generate faction names"""
|
489 |
+
prefixes = ["Order of the", "Brotherhood of", "Guild of"]
|
490 |
+
themes = ["Silver Dawn", "Golden Eagle", "Iron Fist"]
|
491 |
+
return f"{random.choice(prefixes)} {random.choice(themes)}"
|
492 |
+
|
493 |
+
def generate_faction_goals(faction_type: str) -> List[str]:
|
494 |
+
"""Generate faction goals"""
|
495 |
+
goals = {
|
496 |
+
"Guild": ["Increase profits", "Expand influence"],
|
497 |
+
"Military": ["Defend territory", "Maintain order"],
|
498 |
+
"Criminal": ["Control trade", "Eliminate rivals"]
|
499 |
+
}
|
500 |
+
return goals.get(faction_type, ["Gain power", "Protect interests"])
|
501 |
+
|
502 |
+
def generate_faction_methods(alignment: str) -> List[str]:
|
503 |
+
"""Generate faction methods"""
|
504 |
+
if "Good" in alignment:
|
505 |
+
return ["Diplomacy", "Charity", "Cooperation"]
|
506 |
+
elif "Evil" in alignment:
|
507 |
+
return ["Intimidation", "Corruption", "Violence"]
|
508 |
+
else:
|
509 |
+
return ["Politics", "Economics", "Information"]
|
510 |
+
|
511 |
+
def generate_faction_resources(size: str) -> List[str]:
|
512 |
+
"""Generate faction resources"""
|
513 |
+
resources = {
|
514 |
+
"Small": ["Limited funds", "Few contacts"],
|
515 |
+
"Medium": ["Moderate wealth", "Local connections"],
|
516 |
+
"Large": ["Significant wealth", "Regional network"],
|
517 |
+
"Massive": ["Vast fortune", "International contacts"]
|
518 |
+
}
|
519 |
+
return resources.get(size, ["Basic resources"])
|
520 |
+
|
521 |
+
def generate_faction_enemies(faction_type: str) -> List[str]:
|
522 |
+
"""Generate faction enemies"""
|
523 |
+
return [f"Rival {faction_type.lower()} groups", "Government opposition", "Ancient enemies"]
|
524 |
+
|
525 |
+
def generate_faction_allies(faction_type: str) -> List[str]:
|
526 |
+
"""Generate faction allies"""
|
527 |
+
return [f"Friendly {faction_type.lower()} groups", "Sympathetic nobles", "Mercenary companies"]
|
528 |
+
|
529 |
+
def generate_faction_headquarters(faction_type: str) -> str:
|
530 |
+
"""Generate faction headquarters"""
|
531 |
+
headquarters = {
|
532 |
+
"Guild": "Guild Hall",
|
533 |
+
"Military": "Fortress",
|
534 |
+
"Criminal": "Hidden Safehouse",
|
535 |
+
"Religious Order": "Temple"
|
536 |
+
}
|
537 |
+
return headquarters.get(faction_type, "Secure Location")
|
538 |
+
|
539 |
+
# Deity functions
|
540 |
+
def generate_deity_name(domain: str, pantheon: str) -> str:
|
541 |
+
"""Generate deity names"""
|
542 |
+
names = {
|
543 |
+
"War": ["Valorian", "Marticus", "Bellona"],
|
544 |
+
"Knowledge": ["Athenis", "Scholaris", "Wisdomia"],
|
545 |
+
"Life": ["Vitalis", "Celestine", "Healara"]
|
546 |
+
}
|
547 |
+
return random.choice(names.get(domain, ["Divinus", "Godara", "Sanctus"]))
|
548 |
+
|
549 |
+
def generate_deity_appearance(domain: str, alignment: str) -> str:
|
550 |
+
"""Generate deity appearance"""
|
551 |
+
appearances = {
|
552 |
+
"War": "battle-scarred warrior in ornate armor",
|
553 |
+
"Knowledge": "wise figure surrounded by floating books",
|
554 |
+
"Life": "radiant being with healing aura"
|
555 |
+
}
|
556 |
+
return appearances.get(domain, "divine otherworldly presence")
|
557 |
+
|
558 |
+
def generate_deity_personality(domain: str) -> str:
|
559 |
+
"""Generate deity personality"""
|
560 |
+
personalities = {
|
561 |
+
"War": "Strategic and honorable, values courage",
|
562 |
+
"Knowledge": "Wise and patient, values learning",
|
563 |
+
"Life": "Nurturing and protective, values growth"
|
564 |
+
}
|
565 |
+
return personalities.get(domain, "Mysterious and powerful")
|
566 |
+
|
567 |
+
def generate_deity_worshippers(domain: str) -> List[str]:
|
568 |
+
"""Generate deity worshippers"""
|
569 |
+
worshippers = {
|
570 |
+
"War": ["Soldiers", "Warriors", "Generals"],
|
571 |
+
"Knowledge": ["Scholars", "Wizards", "Students"],
|
572 |
+
"Life": ["Healers", "Farmers", "Clerics"]
|
573 |
+
}
|
574 |
+
return worshippers.get(domain, ["Various followers"])
|
575 |
+
|
576 |
+
def generate_deity_temples(power_level: str) -> str:
|
577 |
+
"""Generate deity temples"""
|
578 |
+
temples = {
|
579 |
+
"Lesser": "Small local shrines",
|
580 |
+
"Intermediate": "Regional temples with clergy",
|
581 |
+
"Greater": "Grand cathedrals and pilgrimage sites"
|
582 |
+
}
|
583 |
+
return temples.get(power_level, "Modest temples")
|
584 |
+
|
585 |
+
def generate_holy_symbol(domain: str) -> str:
|
586 |
+
"""Generate holy symbols"""
|
587 |
+
symbols = {
|
588 |
+
"War": "Crossed swords",
|
589 |
+
"Knowledge": "Open book with eye",
|
590 |
+
"Life": "Tree of life"
|
591 |
+
}
|
592 |
+
return symbols.get(domain, "Sacred emblem")
|
593 |
+
|
594 |
+
# Scenario functions
|
595 |
+
def generate_scenario_title(scenario_type: str) -> str:
|
596 |
+
"""Generate scenario titles"""
|
597 |
+
titles = {
|
598 |
+
"Combat Encounter": ["The Bandit Ambush", "Goblin Raid", "Dragon's Lair"],
|
599 |
+
"Social Encounter": ["The Noble's Feast", "Tavern Negotiations", "Court Intrigue"],
|
600 |
+
"Mystery": ["The Missing Merchant", "Murder at the Inn", "The Cursed Artifact"]
|
601 |
+
}
|
602 |
+
return random.choice(titles.get(scenario_type, ["The Unknown Adventure"]))
|
603 |
+
|
604 |
+
def generate_scenario_description(title: str, scenario_type: str, setting: str) -> str:
|
605 |
+
"""Generate scenario descriptions"""
|
606 |
+
return f"In this {scenario_type.lower()}, the party must deal with {title.lower()} in a {setting.lower()} environment"
|
607 |
+
|
608 |
+
def generate_scenario_objective(scenario_type: str) -> str:
|
609 |
+
"""Generate scenario objectives"""
|
610 |
+
objectives = {
|
611 |
+
"Combat Encounter": "Defeat the hostile creatures",
|
612 |
+
"Social Encounter": "Navigate complex social dynamics",
|
613 |
+
"Mystery": "Solve the central mystery"
|
614 |
+
}
|
615 |
+
return objectives.get(scenario_type, "Complete the adventure successfully")
|
616 |
+
|
617 |
+
def generate_scenario_complications(level: str) -> List[str]:
|
618 |
+
"""Generate scenario complications"""
|
619 |
+
complications = {
|
620 |
+
"Easy": ["Minor obstacles", "Simple challenges"],
|
621 |
+
"Medium": ["Competing factions", "Hidden enemies"],
|
622 |
+
"Hard": ["Powerful adversaries", "Complex puzzles"],
|
623 |
+
"Deadly": ["Overwhelming odds", "Reality-threatening consequences"]
|
624 |
+
}
|
625 |
+
return complications.get(level, ["Unexpected challenges"])
|
626 |
+
|
627 |
+
def generate_scenario_npcs(scenario_type: str) -> List[str]:
|
628 |
+
"""Generate scenario NPCs"""
|
629 |
+
npcs = {
|
630 |
+
"Combat Encounter": ["Enemy Leader", "Local Guard"],
|
631 |
+
"Social Encounter": ["Influential Noble", "Wise Elder"],
|
632 |
+
"Mystery": ["Prime Suspect", "Key Witness"]
|
633 |
+
}
|
634 |
+
return npcs.get(scenario_type, ["Important NPC"])
|
635 |
+
|
636 |
+
def generate_scenario_locations(setting: str) -> List[str]:
|
637 |
+
"""Generate scenario locations"""
|
638 |
+
locations = {
|
639 |
+
"Urban": ["City Square", "Noble District", "Marketplace"],
|
640 |
+
"Wilderness": ["Forest Clearing", "Mountain Pass", "Cave"],
|
641 |
+
"Mixed": ["Village Center", "Roadside Inn", "Ancient Shrine"]
|
642 |
+
}
|
643 |
+
return locations.get(setting, ["Notable Location"])
|
644 |
+
|
645 |
+
def generate_scenario_rewards(level: str) -> List[str]:
|
646 |
+
"""Generate scenario rewards"""
|
647 |
+
rewards = {
|
648 |
+
"Easy": ["50-100 gold", "Common magic item"],
|
649 |
+
"Medium": ["200-500 gold", "Uncommon magic item"],
|
650 |
+
"Hard": ["1000+ gold", "Rare magic item"],
|
651 |
+
"Deadly": ["5000+ gold", "Very rare magic item"]
|
652 |
+
}
|
653 |
+
return rewards.get(level, ["Appropriate treasure"])
|
654 |
+
|
655 |
+
def generate_scenario_hooks(scenario_type: str, title: str) -> List[str]:
|
656 |
+
"""Generate scenario hooks"""
|
657 |
+
return [
|
658 |
+
f"A messenger arrives seeking help with {title.lower()}",
|
659 |
+
f"The party overhears rumors about {title.lower()}",
|
660 |
+
f"An old contact asks for help investigating {title.lower()}"
|
661 |
+
]
|
utils/image_utils.py
ADDED
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import logging
|
3 |
+
import requests
|
4 |
+
import base64
|
5 |
+
from io import BytesIO
|
6 |
+
from PIL import Image, ImageDraw, ImageFont
|
7 |
+
import openai
|
8 |
+
from typing import Optional, Dict, Any
|
9 |
+
import time
|
10 |
+
import random
|
11 |
+
|
12 |
+
logger = logging.getLogger(__name__)
|
13 |
+
|
14 |
+
class ImageGenerationError(Exception):
|
15 |
+
"""Custom exception for image generation failures"""
|
16 |
+
pass
|
17 |
+
|
18 |
+
def generate_image(prompt: str, content_type: str, content_info: str = "") -> Optional[str]:
|
19 |
+
"""
|
20 |
+
Main function to generate images for any D&D content type
|
21 |
+
|
22 |
+
Args:
|
23 |
+
prompt: Detailed image generation prompt
|
24 |
+
content_type: Type of content (character_portrait, item, etc.)
|
25 |
+
content_info: Brief info about content for placeholder
|
26 |
+
|
27 |
+
Returns:
|
28 |
+
str: Path to generated image or None if all methods fail
|
29 |
+
"""
|
30 |
+
|
31 |
+
# Check for API keys
|
32 |
+
openai_key = os.getenv('OPENAI_API_KEY')
|
33 |
+
stability_key = os.getenv('STABILITY_API_KEY')
|
34 |
+
|
35 |
+
if openai_key:
|
36 |
+
try:
|
37 |
+
return generate_with_dalle(prompt)
|
38 |
+
except Exception as e:
|
39 |
+
logger.warning(f"DALL-E failed: {e}")
|
40 |
+
|
41 |
+
if stability_key:
|
42 |
+
try:
|
43 |
+
return generate_with_stability(prompt)
|
44 |
+
except Exception as e:
|
45 |
+
logger.warning(f"Stability AI failed: {e}")
|
46 |
+
|
47 |
+
# Generate placeholder if no APIs work
|
48 |
+
return generate_placeholder(content_type, content_info)
|
49 |
+
|
50 |
+
def generate_with_dalle(prompt: str, size: str = "1024x1024") -> Optional[str]:
|
51 |
+
"""Generate image using OpenAI's DALL-E"""
|
52 |
+
try:
|
53 |
+
logger.info(f"Generating image with DALL-E: {prompt[:50]}...")
|
54 |
+
|
55 |
+
openai.api_key = os.getenv('OPENAI_API_KEY')
|
56 |
+
|
57 |
+
response = openai.images.generate(
|
58 |
+
model="dall-e-3",
|
59 |
+
prompt=prompt,
|
60 |
+
size=size,
|
61 |
+
quality="standard",
|
62 |
+
n=1,
|
63 |
+
)
|
64 |
+
|
65 |
+
image_url = response.data[0].url
|
66 |
+
logger.info("DALL-E image generated successfully")
|
67 |
+
return image_url
|
68 |
+
|
69 |
+
except Exception as e:
|
70 |
+
logger.error(f"DALL-E generation failed: {e}")
|
71 |
+
raise ImageGenerationError(f"DALL-E failed: {str(e)}")
|
72 |
+
|
73 |
+
def generate_with_stability(prompt: str) -> Optional[str]:
|
74 |
+
"""Generate image using Stability AI"""
|
75 |
+
try:
|
76 |
+
logger.info(f"Generating image with Stability AI: {prompt[:50]}...")
|
77 |
+
|
78 |
+
url = "https://api.stability.ai/v1/generation/stable-diffusion-xl-1024-v1-0/text-to-image"
|
79 |
+
|
80 |
+
headers = {
|
81 |
+
"Accept": "application/json",
|
82 |
+
"Content-Type": "application/json",
|
83 |
+
"Authorization": f"Bearer {os.getenv('STABILITY_API_KEY')}",
|
84 |
+
}
|
85 |
+
|
86 |
+
body = {
|
87 |
+
"text_prompts": [{"text": prompt, "weight": 1}],
|
88 |
+
"cfg_scale": 7,
|
89 |
+
"height": 1024,
|
90 |
+
"width": 1024,
|
91 |
+
"samples": 1,
|
92 |
+
"steps": 30,
|
93 |
+
}
|
94 |
+
|
95 |
+
response = requests.post(url, headers=headers, json=body)
|
96 |
+
|
97 |
+
if response.status_code != 200:
|
98 |
+
raise ImageGenerationError(f"Stability API error: {response.status_code}")
|
99 |
+
|
100 |
+
data = response.json()
|
101 |
+
|
102 |
+
# Convert base64 to image
|
103 |
+
image_data = base64.b64decode(data["artifacts"][0]["base64"])
|
104 |
+
img = Image.open(BytesIO(image_data))
|
105 |
+
|
106 |
+
# Save temporarily and return path
|
107 |
+
temp_path = f"temp_{int(time.time())}_{random.randint(1000,9999)}.png"
|
108 |
+
img.save(temp_path)
|
109 |
+
logger.info("Stability AI image generated successfully")
|
110 |
+
return temp_path
|
111 |
+
|
112 |
+
except Exception as e:
|
113 |
+
logger.error(f"Stability AI generation failed: {e}")
|
114 |
+
raise ImageGenerationError(f"Stability AI failed: {str(e)}")
|
115 |
+
|
116 |
+
def generate_placeholder(content_type: str, content_info: str) -> str:
|
117 |
+
"""Generate a themed placeholder image when AI services fail"""
|
118 |
+
try:
|
119 |
+
# Create different colored placeholders for different content types
|
120 |
+
colors = {
|
121 |
+
"character_portrait": (70, 130, 180), # Steel Blue
|
122 |
+
"npc_portrait": (139, 69, 19), # Saddle Brown
|
123 |
+
"item": (255, 215, 0), # Gold
|
124 |
+
"location": (34, 139, 34), # Forest Green
|
125 |
+
"faction_symbol": (128, 0, 128), # Purple
|
126 |
+
"deity": (255, 255, 255), # White
|
127 |
+
"scenario": (220, 20, 60) # Crimson
|
128 |
+
}
|
129 |
+
|
130 |
+
color = colors.get(content_type, (128, 128, 128))
|
131 |
+
|
132 |
+
# Create image
|
133 |
+
img = Image.new('RGB', (512, 512), color=color)
|
134 |
+
draw = ImageDraw.Draw(img)
|
135 |
+
|
136 |
+
# Try to load a font, fall back to default if not available
|
137 |
+
try:
|
138 |
+
font = ImageFont.truetype("arial.ttf", 24)
|
139 |
+
except:
|
140 |
+
font = ImageFont.load_default()
|
141 |
+
|
142 |
+
# Add text
|
143 |
+
text_lines = [
|
144 |
+
f"{content_type.replace('_', ' ').title()}",
|
145 |
+
"Placeholder Image",
|
146 |
+
content_info[:30] + "..." if len(content_info) > 30 else content_info
|
147 |
+
]
|
148 |
+
|
149 |
+
y_offset = 200
|
150 |
+
for line in text_lines:
|
151 |
+
# Get text bounding box
|
152 |
+
bbox = draw.textbbox((0, 0), line, font=font)
|
153 |
+
text_width = bbox[2] - bbox[0]
|
154 |
+
text_height = bbox[3] - bbox[1]
|
155 |
+
|
156 |
+
# Center text
|
157 |
+
x = (512 - text_width) // 2
|
158 |
+
draw.text((x, y_offset), line, fill=(255, 255, 255), font=font)
|
159 |
+
y_offset += text_height + 10
|
160 |
+
|
161 |
+
# Save placeholder
|
162 |
+
temp_path = f"placeholder_{content_type}_{int(time.time())}.png"
|
163 |
+
img.save(temp_path)
|
164 |
+
|
165 |
+
logger.info(f"Generated {content_type} placeholder image")
|
166 |
+
return temp_path
|
167 |
+
|
168 |
+
except Exception as e:
|
169 |
+
logger.error(f"Failed to create placeholder: {e}")
|
170 |
+
return None
|
171 |
+
|
172 |
+
def cleanup_temp_files():
|
173 |
+
"""Clean up temporary image files"""
|
174 |
+
import glob
|
175 |
+
temp_files = glob.glob("temp_*.png") + glob.glob("placeholder_*.png")
|
176 |
+
for file in temp_files:
|
177 |
+
try:
|
178 |
+
os.remove(file)
|
179 |
+
logger.info(f"Cleaned up temporary file: {file}")
|
180 |
+
except Exception as e:
|
181 |
+
logger.warning(f"Failed to clean up {file}: {e}")
|