Spaces:
Sleeping
Sleeping
# app.py - Fixed D&D Campaign and Character Creator with AI Agents | |
import gradio as gr | |
import logging | |
from typing import Dict, List, Tuple, Optional | |
import json | |
import os | |
from dotenv import load_dotenv | |
from dataclasses import dataclass | |
from enum import Enum | |
import random | |
# Load environment variables | |
load_dotenv() | |
# Set up logging | |
logging.basicConfig(level=logging.INFO) | |
logger = logging.getLogger(__name__) | |
# Load OpenAI API key | |
try: | |
import openai | |
api_key = os.getenv("OPENAI_API_KEY") | |
if api_key: | |
openai.api_key = api_key | |
logger.info("β OpenAI API key loaded") | |
else: | |
logger.warning("β οΈ No OpenAI API key found") | |
except ImportError: | |
logger.warning("β οΈ OpenAI package not installed") | |
# ===== DATA MODELS ===== | |
class Alignment(Enum): | |
LAWFUL_GOOD = "Lawful Good" | |
NEUTRAL_GOOD = "Neutral Good" | |
CHAOTIC_GOOD = "Chaotic Good" | |
LAWFUL_NEUTRAL = "Lawful Neutral" | |
TRUE_NEUTRAL = "True Neutral" | |
CHAOTIC_NEUTRAL = "Chaotic Neutral" | |
LAWFUL_EVIL = "Lawful Evil" | |
NEUTRAL_EVIL = "Neutral Evil" | |
CHAOTIC_EVIL = "Chaotic Evil" | |
class CharacterClass: | |
name: str | |
hit_die: int | |
primary_ability: List[str] | |
saving_throws: List[str] | |
skills: List[str] | |
class Race: | |
name: str | |
ability_modifiers: Dict[str, int] | |
traits: List[str] | |
languages: List[str] | |
class Character: | |
name: str | |
race: str | |
character_class: str | |
level: int | |
gender: str | |
alignment: Alignment | |
abilities: Dict[str, int] | |
hit_points: int | |
skills: List[str] | |
background: str | |
backstory: str | |
portrait_url: Optional[str] = None | |
class Campaign: | |
name: str | |
theme: str | |
level_range: str | |
description: str | |
locations: List[str] | |
npcs: List[str] | |
plot_hooks: List[str] | |
class NPC: | |
name: str | |
race: str | |
occupation: str | |
personality: str | |
secret: str | |
voice_description: str | |
relationship_to_party: str | |
# ===== AI AGENT CLASSES ===== | |
class DungeonMasterAgent: | |
"""AI agent that acts as a Dungeon Master""" | |
def __init__(self): | |
self.personality = "Creative, fair, and engaging storyteller" | |
self.knowledge_areas = ["D&D rules", "storytelling", "world-building", "character development"] | |
def generate_campaign_concept(self, theme: str, level: int, player_count: int) -> Dict: | |
"""Generate a complete campaign concept""" | |
try: | |
from openai import OpenAI | |
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) | |
prompt = f"""As an experienced D&D Dungeon Master, create a campaign concept for: | |
Theme: {theme} | |
Player Level: {level} | |
Number of Players: {player_count} | |
Provide: | |
1. Campaign Name | |
2. Core Plot Hook (2-3 sentences) | |
3. Main Antagonist | |
4. 3 Key Locations | |
5. Central Conflict | |
6. Estimated Campaign Length | |
7. Unique Elements/Mechanics | |
Make it engaging and ready to play!""" | |
response = client.chat.completions.create( | |
model="gpt-4", | |
messages=[{"role": "system", "content": "You are an expert D&D Dungeon Master with 20 years of experience creating memorable campaigns."}, | |
{"role": "user", "content": prompt}], | |
max_tokens=500, | |
temperature=0.8 | |
) | |
content = response.choices[0].message.content | |
return {"success": True, "content": content} | |
except Exception as e: | |
logger.error(f"Campaign generation failed: {e}") | |
return {"success": False, "error": str(e), "content": f"Mock Campaign: {theme} adventure for {player_count} level {level} characters"} | |
def generate_session_content(self, campaign_context: str, session_number: int) -> Dict: | |
"""Generate content for a specific session""" | |
try: | |
from openai import OpenAI | |
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) | |
prompt = f"""Create session {session_number} content for this campaign: | |
{campaign_context} | |
Generate: | |
1. Session Opening (scene description) | |
2. 3 Potential Encounters (combat, social, exploration) | |
3. Key NPCs for this session | |
4. Skill challenges or puzzles | |
5. Potential plot developments | |
6. Cliffhanger ending options | |
Make it detailed enough for a DM to run immediately.""" | |
response = client.chat.completions.create( | |
model="gpt-4", | |
messages=[{"role": "system", "content": "You are a D&D Dungeon Master preparing detailed session content."}, | |
{"role": "user", "content": prompt}], | |
max_tokens=600, | |
temperature=0.7 | |
) | |
return {"success": True, "content": response.choices[0].message.content} | |
except Exception as e: | |
logger.error(f"Session generation failed: {e}") | |
return {"success": False, "error": str(e), "content": f"Mock Session {session_number}: Adventure continues..."} | |
class NPCAgent: | |
"""AI agent specialized in creating and roleplaying NPCs""" | |
def __init__(self): | |
self.personality = "Versatile character actor with deep understanding of motivations" | |
self.specializations = ["Character creation", "Dialogue", "Motivations", "Voice acting"] | |
def generate_npc(self, context: str, role: str, importance: str) -> Dict: | |
"""Generate a detailed NPC""" | |
try: | |
from openai import OpenAI | |
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) | |
prompt = f"""Create a detailed NPC for: | |
Context: {context} | |
Role: {role} | |
Importance: {importance} | |
Generate: | |
1. Name and basic demographics | |
2. Personality traits (3-4 key traits) | |
3. Background and motivation | |
4. Speech patterns/accent description | |
5. Physical description | |
6. Secret or hidden agenda | |
7. How they react to different party approaches | |
8. Potential quest hooks they could provide | |
Make them memorable and three-dimensional!""" | |
response = client.chat.completions.create( | |
model="gpt-4", | |
messages=[{"role": "system", "content": "You are an expert at creating memorable, three-dimensional NPCs for D&D campaigns."}, | |
{"role": "user", "content": prompt}], | |
max_tokens=400, | |
temperature=0.8 | |
) | |
return {"success": True, "content": response.choices[0].message.content} | |
except Exception as e: | |
logger.error(f"NPC generation failed: {e}") | |
return {"success": False, "error": str(e), "content": f"Mock NPC: {role} character for {context}"} | |
def roleplay_npc(self, npc_description: str, player_input: str, context: str) -> Dict: | |
"""Roleplay as an NPC in response to player actions""" | |
try: | |
from openai import OpenAI | |
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) | |
prompt = f"""You are roleplaying as this NPC: | |
{npc_description} | |
Context: {context} | |
Player says/does: {player_input} | |
Respond in character with: | |
1. Dialogue (in quotes) | |
2. Actions/body language (in italics) | |
3. Internal thoughts/motivations (in parentheses) | |
Stay true to the character's personality and motivations!""" | |
response = client.chat.completions.create( | |
model="gpt-4", | |
messages=[{"role": "system", "content": "You are a skilled voice actor and D&D player, staying in character as NPCs."}, | |
{"role": "user", "content": prompt}], | |
max_tokens=200, | |
temperature=0.9 | |
) | |
return {"success": True, "content": response.choices[0].message.content} | |
except Exception as e: | |
logger.error(f"NPC roleplay failed: {e}") | |
return {"success": False, "error": str(e), "content": "Mock NPC Response: The character responds appropriately to your action."} | |
class WorldBuilderAgent: | |
"""AI agent focused on creating consistent world elements""" | |
def __init__(self): | |
self.personality = "Detail-oriented architect of fictional worlds" | |
self.specializations = ["Geography", "Politics", "Culture", "History", "Economics"] | |
def generate_location(self, location_type: str, theme: str, purpose: str) -> Dict: | |
"""Generate a detailed location""" | |
try: | |
from openai import OpenAI | |
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) | |
prompt = f"""Create a detailed {location_type} with: | |
Theme: {theme} | |
Purpose in campaign: {purpose} | |
Generate: | |
1. Name and general description | |
2. Key areas/rooms (at least 5) | |
3. Notable inhabitants | |
4. Hidden secrets or mysteries | |
5. Potential dangers or challenges | |
6. Valuable resources or rewards | |
7. Connections to broader world/campaign | |
8. Sensory details (sights, sounds, smells) | |
Make it feel lived-in and realistic!""" | |
response = client.chat.completions.create( | |
model="gpt-4", | |
messages=[{"role": "system", "content": "You are a master world-builder creating immersive D&D locations."}, | |
{"role": "user", "content": prompt}], | |
max_tokens=500, | |
temperature=0.7 | |
) | |
return {"success": True, "content": response.choices[0].message.content} | |
except Exception as e: | |
logger.error(f"Location generation failed: {e}") | |
return {"success": False, "error": str(e), "content": f"Mock Location: {theme} {location_type} for {purpose}"} | |
class LootMasterAgent: | |
"""AI agent specialized in creating balanced loot and magic items""" | |
def __init__(self): | |
self.personality = "Meticulous curator of magical treasures" | |
self.specializations = ["Game balance", "Magic item design", "Treasure distribution"] | |
def generate_loot_table(self, level: int, encounter_type: str, rarity: str) -> Dict: | |
"""Generate a balanced loot table""" | |
try: | |
from openai import OpenAI | |
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) | |
prompt = f"""Create a balanced loot table for: | |
Party Level: {level} | |
Encounter Type: {encounter_type} | |
Rarity Level: {rarity} | |
Generate: | |
1. Gold/Currency amounts | |
2. Common items (consumables, gear) | |
3. Uncommon magical items (if appropriate) | |
4. Rare items (if high level) | |
5. Unique/plot-relevant items | |
6. Alternative treasures (information, allies, etc.) | |
Ensure balance and appropriateness for the level!""" | |
response = client.chat.completions.create( | |
model="gpt-4", | |
messages=[{"role": "system", "content": "You are an expert at D&D game balance and treasure design."}, | |
{"role": "user", "content": prompt}], | |
max_tokens=300, | |
temperature=0.6 | |
) | |
return {"success": True, "content": response.choices[0].message.content} | |
except Exception as e: | |
logger.error(f"Loot generation failed: {e}") | |
return {"success": False, "error": str(e), "content": f"Mock Loot: Level {level} {encounter_type} {rarity} treasures"} | |
def create_custom_magic_item(self, item_concept: str, power_level: str, campaign_theme: str) -> Dict: | |
"""Create a custom magic item""" | |
try: | |
from openai import OpenAI | |
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) | |
prompt = f"""Design a custom magic item: | |
Concept: {item_concept} | |
Power Level: {power_level} | |
Campaign Theme: {campaign_theme} | |
Provide: | |
1. Item name and basic description | |
2. Mechanical effects (stats, abilities) | |
3. Activation requirements | |
4. Rarity and attunement needs | |
5. Physical appearance | |
6. Historical background/lore | |
7. Potential drawbacks or limitations | |
8. How it fits the campaign theme | |
Make it balanced and interesting!""" | |
response = client.chat.completions.create( | |
model="gpt-4", | |
messages=[{"role": "system", "content": "You are a master craftsperson of magical items for D&D, balancing power with narrative interest."}, | |
{"role": "user", "content": prompt}], | |
max_tokens=400, | |
temperature=0.7 | |
) | |
return {"success": True, "content": response.choices[0].message.content} | |
except Exception as e: | |
logger.error(f"Magic item creation failed: {e}") | |
return {"success": False, "error": str(e), "content": f"Mock Magic Item: {power_level} {item_concept} with {campaign_theme} theme"} | |
# ===== CHARACTER CREATOR CLASS ===== | |
class CharacterCreator: | |
"""Enhanced character creator with AI integration""" | |
def __init__(self): | |
self.classes = self._get_default_classes() | |
self.races = self._get_default_races() | |
self.backgrounds = self._get_backgrounds() | |
def _get_default_classes(self) -> Dict[str, CharacterClass]: | |
return { | |
"Fighter": CharacterClass( | |
name="Fighter", hit_die=10, | |
primary_ability=["Strength", "Dexterity"], | |
saving_throws=["Strength", "Constitution"], | |
skills=["Acrobatics", "Animal Handling", "Athletics", "History", "Insight", "Intimidation", "Perception", "Survival"] | |
), | |
"Wizard": CharacterClass( | |
name="Wizard", hit_die=6, | |
primary_ability=["Intelligence"], | |
saving_throws=["Intelligence", "Wisdom"], | |
skills=["Arcana", "History", "Insight", "Investigation", "Medicine", "Religion"] | |
), | |
"Rogue": CharacterClass( | |
name="Rogue", hit_die=8, | |
primary_ability=["Dexterity"], | |
saving_throws=["Dexterity", "Intelligence"], | |
skills=["Acrobatics", "Athletics", "Deception", "Insight", "Intimidation", "Investigation", "Perception", "Performance", "Persuasion", "Sleight of Hand", "Stealth"] | |
), | |
"Cleric": CharacterClass( | |
name="Cleric", hit_die=8, | |
primary_ability=["Wisdom"], | |
saving_throws=["Wisdom", "Charisma"], | |
skills=["History", "Insight", "Medicine", "Persuasion", "Religion"] | |
), | |
"Barbarian": CharacterClass( | |
name="Barbarian", hit_die=12, | |
primary_ability=["Strength"], | |
saving_throws=["Strength", "Constitution"], | |
skills=["Animal Handling", "Athletics", "Intimidation", "Nature", "Perception", "Survival"] | |
), | |
"Bard": CharacterClass( | |
name="Bard", hit_die=8, | |
primary_ability=["Charisma"], | |
saving_throws=["Dexterity", "Charisma"], | |
skills=["Any three of your choice"] | |
), | |
"Druid": CharacterClass( | |
name="Druid", hit_die=8, | |
primary_ability=["Wisdom"], | |
saving_throws=["Intelligence", "Wisdom"], | |
skills=["Arcana", "Animal Handling", "Insight", "Medicine", "Nature", "Perception", "Religion", "Survival"] | |
), | |
"Monk": CharacterClass( | |
name="Monk", hit_die=8, | |
primary_ability=["Dexterity", "Wisdom"], | |
saving_throws=["Strength", "Dexterity"], | |
skills=["Acrobatics", "Athletics", "History", "Insight", "Religion", "Stealth"] | |
), | |
"Paladin": CharacterClass( | |
name="Paladin", hit_die=10, | |
primary_ability=["Strength", "Charisma"], | |
saving_throws=["Wisdom", "Charisma"], | |
skills=["Athletics", "Insight", "Intimidation", "Medicine", "Persuasion", "Religion"] | |
), | |
"Ranger": CharacterClass( | |
name="Ranger", hit_die=10, | |
primary_ability=["Dexterity", "Wisdom"], | |
saving_throws=["Strength", "Dexterity"], | |
skills=["Animal Handling", "Athletics", "Insight", "Investigation", "Nature", "Perception", "Stealth", "Survival"] | |
), | |
"Sorcerer": CharacterClass( | |
name="Sorcerer", hit_die=6, | |
primary_ability=["Charisma"], | |
saving_throws=["Constitution", "Charisma"], | |
skills=["Arcana", "Deception", "Insight", "Intimidation", "Persuasion", "Religion"] | |
), | |
"Warlock": CharacterClass( | |
name="Warlock", hit_die=8, | |
primary_ability=["Charisma"], | |
saving_throws=["Wisdom", "Charisma"], | |
skills=["Arcana", "Deception", "History", "Intimidation", "Investigation", "Nature", "Religion"] | |
) | |
} | |
def _get_default_races(self) -> Dict[str, Race]: | |
return { | |
"Human": Race( | |
name="Human", | |
ability_modifiers={"All": 1}, | |
traits=["Extra Language", "Extra Skill", "Versatile"], | |
languages=["Common", "One other"] | |
), | |
"Elf": Race( | |
name="Elf", | |
ability_modifiers={"Dexterity": 2}, | |
traits=["Darkvision", "Keen Senses", "Fey Ancestry", "Trance"], | |
languages=["Common", "Elvish"] | |
), | |
"Dwarf": Race( | |
name="Dwarf", | |
ability_modifiers={"Constitution": 2}, | |
traits=["Darkvision", "Dwarven Resilience", "Stonecunning"], | |
languages=["Common", "Dwarvish"] | |
), | |
"Halfling": Race( | |
name="Halfling", | |
ability_modifiers={"Dexterity": 2}, | |
traits=["Lucky", "Brave", "Halfling Nimbleness"], | |
languages=["Common", "Halfling"] | |
), | |
"Dragonborn": Race( | |
name="Dragonborn", | |
ability_modifiers={"Strength": 2, "Charisma": 1}, | |
traits=["Draconic Ancestry", "Breath Weapon", "Damage Resistance"], | |
languages=["Common", "Draconic"] | |
), | |
"Gnome": Race( | |
name="Gnome", | |
ability_modifiers={"Intelligence": 2}, | |
traits=["Darkvision", "Gnome Cunning"], | |
languages=["Common", "Gnomish"] | |
), | |
"Half-Elf": Race( | |
name="Half-Elf", | |
ability_modifiers={"Charisma": 2, "Choice": 1}, | |
traits=["Darkvision", "Fey Ancestry", "Two Skills"], | |
languages=["Common", "Elvish", "One other"] | |
), | |
"Half-Orc": Race( | |
name="Half-Orc", | |
ability_modifiers={"Strength": 2, "Constitution": 1}, | |
traits=["Darkvision", "Relentless Endurance", "Savage Attacks"], | |
languages=["Common", "Orc"] | |
), | |
"Tiefling": Race( | |
name="Tiefling", | |
ability_modifiers={"Intelligence": 1, "Charisma": 2}, | |
traits=["Darkvision", "Hellish Resistance", "Infernal Legacy"], | |
languages=["Common", "Infernal"] | |
) | |
} | |
def _get_backgrounds(self) -> List[str]: | |
return [ | |
"Acolyte", "Criminal", "Folk Hero", "Noble", "Sage", "Soldier", | |
"Charlatan", "Entertainer", "Guild Artisan", "Hermit", "Outlander", "Sailor" | |
] | |
def roll_ability_scores(self) -> Dict[str, int]: | |
"""Roll 4d6, drop lowest, for each ability score""" | |
abilities = {} | |
for ability in ["Strength", "Dexterity", "Constitution", "Intelligence", "Wisdom", "Charisma"]: | |
rolls = [random.randint(1, 6) for _ in range(4)] | |
rolls.sort(reverse=True) | |
abilities[ability] = sum(rolls[:3]) # Take top 3 | |
return abilities | |
def calculate_ability_modifier(self, score: int) -> int: | |
"""Calculate ability modifier from score""" | |
return (score - 10) // 2 | |
def calculate_hit_points(self, char_class: str, level: int, constitution_modifier: int) -> int: | |
"""Calculate hit points based on class, level, and CON modifier""" | |
hit_die = self.classes[char_class].hit_die | |
base_hp = hit_die + constitution_modifier # Max HP at level 1 | |
# Add average HP for additional levels | |
for _ in range(level - 1): | |
base_hp += (hit_die // 2 + 1) + constitution_modifier | |
return max(1, base_hp) # Minimum 1 HP | |
def apply_racial_modifiers(self, base_abilities: Dict[str, int], race: str) -> Dict[str, int]: | |
"""Apply racial ability score modifiers""" | |
modified_abilities = base_abilities.copy() | |
race_data = self.races[race] | |
for ability, modifier in race_data.ability_modifiers.items(): | |
if ability == "All": | |
for ability_name in modified_abilities: | |
modified_abilities[ability_name] += modifier | |
elif ability == "Choice": | |
# For simplicity, add to lowest score | |
lowest_ability = min(modified_abilities, key=modified_abilities.get) | |
modified_abilities[lowest_ability] += modifier | |
else: | |
modified_abilities[ability] += modifier | |
return modified_abilities | |
# ===== IMAGE GENERATION ===== | |
def generate_image(prompt: str) -> str: | |
"""Generate image using OpenAI DALL-E""" | |
try: | |
from openai import OpenAI | |
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) | |
response = client.images.generate( | |
model="dall-e-3", | |
prompt=prompt, | |
size="1024x1024", | |
quality="standard", | |
n=1, | |
) | |
return response.data[0].url | |
except Exception as e: | |
logger.error(f"Image generation failed: {e}") | |
return "https://via.placeholder.com/512x512/dc2626/ffffff?text=Image+Generation+Failed" | |
# ===== MAIN INTERFACE ===== | |
def create_main_interface(): | |
"""Create the main D&D Campaign Manager interface""" | |
# Initialize agents | |
dm_agent = DungeonMasterAgent() | |
npc_agent = NPCAgent() | |
world_agent = WorldBuilderAgent() | |
loot_agent = LootMasterAgent() | |
character_creator = CharacterCreator() | |
custom_css = """ | |
.agent-card { | |
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
padding: 20px; | |
border-radius: 15px; | |
margin: 10px 0; | |
color: white; | |
} | |
.output-box { | |
background: #f8f9fa; | |
border: 1px solid #dee2e6; | |
border-radius: 10px; | |
padding: 15px; | |
margin: 10px 0; | |
} | |
""" | |
with gr.Blocks(css=custom_css, title="D&D Campaign Manager") as demo: | |
gr.Markdown(""" | |
# π° Advanced D&D Campaign Manager | |
*Your AI-powered toolkit for epic adventures* | |
**Features:** | |
- π AI Dungeon Master Assistant | |
- π₯ Intelligent NPC Creator & Roleplay | |
- πΊοΈ World Builder Agent | |
- π° Loot Master & Magic Item Designer | |
- π Enhanced Character Creator | |
""") | |
with gr.Tabs(): | |
# ===== CHARACTER CREATOR TAB ===== | |
with gr.TabItem("π Character Creator"): | |
with gr.Row(): | |
with gr.Column(scale=2): | |
gr.Markdown("## π Character Details") | |
character_name = gr.Textbox(label="Character Name", placeholder="Enter name...") | |
with gr.Row(): | |
race_dropdown = gr.Dropdown( | |
choices=list(character_creator.races.keys()), | |
label="Race", value="Human" | |
) | |
class_dropdown = gr.Dropdown( | |
choices=list(character_creator.classes.keys()), | |
label="Class", value="Fighter" | |
) | |
gender_dropdown = gr.Dropdown( | |
choices=["Male", "Female", "Non-binary", "Transgender Male", "Transgender Female", "Genderfluid", "Agender", "Other"], | |
label="Gender", value="Male" | |
) | |
with gr.Row(): | |
level_slider = gr.Slider(minimum=1, maximum=20, step=1, value=1, label="Level") | |
alignment_dropdown = gr.Dropdown( | |
choices=[alignment.value for alignment in Alignment], | |
label="Alignment", value=Alignment.LAWFUL_GOOD.value | |
) | |
background_dropdown = gr.Dropdown( | |
choices=character_creator._get_backgrounds(), | |
label="Background", value="Folk Hero" | |
) | |
# AI Enhancement Buttons | |
with gr.Row(): | |
generate_name_btn = gr.Button("β¨ AI Generate Name", variant="secondary") | |
generate_backstory_btn = gr.Button("π AI Generate Backstory", variant="secondary") | |
backstory = gr.Textbox(label="Backstory", lines=4, placeholder="Character background...") | |
# Ability Scores | |
gr.Markdown("## π² Ability Scores") | |
roll_btn = gr.Button("π² Roll Ability Scores", variant="primary") | |
with gr.Row(): | |
str_score = gr.Number(label="Strength", value=10, precision=0) | |
dex_score = gr.Number(label="Dexterity", value=10, precision=0) | |
con_score = gr.Number(label="Constitution", value=10, precision=0) | |
with gr.Row(): | |
int_score = gr.Number(label="Intelligence", value=10, precision=0) | |
wis_score = gr.Number(label="Wisdom", value=10, precision=0) | |
cha_score = gr.Number(label="Charisma", value=10, precision=0) | |
with gr.Column(scale=1): | |
gr.Markdown("## π Character Summary") | |
character_summary = gr.Markdown("*Create character to see summary*") | |
gr.Markdown("## π¨ Character Portrait") | |
portrait_btn = gr.Button("π¨ Generate AI Portrait", variant="primary") | |
character_portrait = gr.Image(label="Portrait", height=300) | |
gr.Markdown("## πΎ Export") | |
export_btn = gr.Button("π₯ Export Character") | |
export_file = gr.File(label="Character JSON") | |
# Hidden state | |
character_data = gr.State() | |
# ===== CAMPAIGN CREATOR TAB ===== | |
with gr.TabItem("π AI Dungeon Master"): | |
gr.Markdown("## οΏ½οΏ½ Campaign Generation", elem_classes=["agent-card"]) | |
with gr.Row(): | |
with gr.Column(): | |
campaign_theme = gr.Dropdown( | |
choices=["High Fantasy", "Dark Fantasy", "Urban Fantasy", "Steampunk", "Horror", "Comedy", "Political Intrigue", "Exploration"], | |
label="Campaign Theme", value="High Fantasy" | |
) | |
campaign_level = gr.Slider(minimum=1, maximum=20, value=5, label="Starting Level") | |
player_count = gr.Slider(minimum=1, maximum=8, value=4, label="Number of Players") | |
generate_campaign_btn = gr.Button("π² Generate Campaign Concept", variant="primary", size="lg") | |
with gr.Column(): | |
campaign_visual_btn = gr.Button("πΌοΈ Generate Campaign Art") | |
campaign_image = gr.Image(label="Campaign Visual", height=300) | |
campaign_output = gr.Textbox(label="Campaign Concept", lines=10, elem_classes=["output-box"]) | |
gr.Markdown("## π Session Planning") | |
with gr.Row(): | |
session_number = gr.Number(label="Session Number", value=1, precision=0) | |
generate_session_btn = gr.Button("π Generate Session Content", variant="secondary") | |
session_output = gr.Textbox(label="Session Content", lines=8, elem_classes=["output-box"]) | |
# ===== NPC CREATOR TAB ===== | |
with gr.TabItem("π₯ NPC Agent"): | |
gr.Markdown("## π NPC Creator & Roleplay Assistant", elem_classes=["agent-card"]) | |
with gr.Row(): | |
with gr.Column(): | |
gr.Markdown("### Create New NPC") | |
npc_context = gr.Textbox(label="Campaign/Scene Context", placeholder="Describe the setting or situation...") | |
npc_role = gr.Dropdown( | |
choices=["Ally", "Neutral", "Antagonist", "Quest Giver", "Merchant", "Authority Figure", "Mysterious Stranger"], | |
label="NPC Role", value="Neutral" | |
) | |
npc_importance = gr.Dropdown( | |
choices=["Minor", "Moderate", "Major", "Recurring"], | |
label="Importance Level", value="Moderate" | |
) | |
create_npc_btn = gr.Button("π Generate NPC", variant="primary") | |
npc_portrait_btn = gr.Button("π¨ Generate NPC Portrait") | |
npc_portrait = gr.Image(label="NPC Portrait", height=300) | |
with gr.Column(): | |
npc_output = gr.Textbox(label="Generated NPC", lines=12, elem_classes=["output-box"]) | |
gr.Markdown("### π¬ NPC Roleplay") | |
with gr.Row(): | |
with gr.Column(scale=2): | |
player_input = gr.Textbox(label="Player Action/Dialogue", placeholder="What does the player say or do?") | |
roleplay_context = gr.Textbox(label="Scene Context", placeholder="Describe the current situation...") | |
with gr.Column(scale=1): | |
roleplay_btn = gr.Button("π NPC Response", variant="secondary", size="lg") | |
npc_response = gr.Textbox(label="NPC Response", lines=4, elem_classes=["output-box"]) | |
# Hidden states for NPC | |
current_npc_data = gr.State() | |
# ===== WORLD BUILDER TAB ===== | |
with gr.TabItem("πΊοΈ World Builder"): | |
gr.Markdown("## ποΈ Location & World Generator", elem_classes=["agent-card"]) | |
with gr.Row(): | |
with gr.Column(): | |
location_type = gr.Dropdown( | |
choices=["Tavern", "Dungeon", "City", "Forest", "Castle", "Temple", "Shop", "Wilderness", "Mansion", "Cave System"], | |
label="Location Type", value="Tavern" | |
) | |
location_theme = gr.Dropdown( | |
choices=["Standard Fantasy", "Dark/Gothic", "Mystical", "Abandoned/Ruined", "Luxurious", "Dangerous", "Peaceful", "Mysterious"], | |
label="Theme", value="Standard Fantasy" | |
) | |
location_purpose = gr.Textbox(label="Purpose in Campaign", placeholder="How will this location be used?") | |
generate_location_btn = gr.Button("ποΈ Generate Location", variant="primary") | |
location_art_btn = gr.Button("πΌοΈ Generate Location Art") | |
with gr.Column(): | |
location_image = gr.Image(label="Location Visual", height=300) | |
location_output = gr.Textbox(label="Location Details", lines=12, elem_classes=["output-box"]) | |
# ===== LOOT MASTER TAB ===== | |
with gr.TabItem("π° Loot Master"): | |
gr.Markdown("## π Treasure & Magic Item Generator", elem_classes=["agent-card"]) | |
with gr.Row(): | |
with gr.Column(): | |
gr.Markdown("### π² Loot Table Generator") | |
loot_level = gr.Slider(minimum=1, maximum=20, value=5, label="Party Level") | |
encounter_type = gr.Dropdown( | |
choices=["Boss Fight", "Mini-boss", "Standard Combat", "Exploration Reward", "Quest Completion", "Treasure Hoard"], | |
label="Encounter Type", value="Standard Combat" | |
) | |
loot_rarity = gr.Dropdown( | |
choices=["Poor", "Standard", "Rich", "Legendary"], | |
label="Treasure Quality", value="Standard" | |
) | |
generate_loot_btn = gr.Button("π° Generate Loot Table", variant="primary") | |
with gr.Column(): | |
gr.Markdown("### β¨ Custom Magic Item") | |
item_concept = gr.Textbox(label="Item Concept", placeholder="What kind of item? (sword, ring, staff, etc.)") | |
power_level = gr.Dropdown( | |
choices=["Common", "Uncommon", "Rare", "Very Rare", "Legendary", "Artifact"], | |
label="Power Level", value="Uncommon" | |
) | |
campaign_theme_item = gr.Textbox(label="Campaign Theme", placeholder="How should it fit your campaign?") | |
create_item_btn = gr.Button("β¨ Create Magic Item", variant="secondary") | |
item_art_btn = gr.Button("π¨ Generate Item Art") | |
with gr.Row(): | |
loot_output = gr.Textbox(label="Loot Table", lines=8, elem_classes=["output-box"]) | |
magic_item_output = gr.Textbox(label="Magic Item Details", lines=8, elem_classes=["output-box"]) | |
item_image = gr.Image(label="Magic Item Visual", height=250) | |
# ===== CAMPAIGN TOOLS TAB ===== | |
with gr.TabItem("π οΈ Campaign Tools"): | |
gr.Markdown("## π― Advanced Campaign Management") | |
with gr.Tabs(): | |
with gr.TabItem("π Initiative Tracker"): | |
gr.Markdown("### βοΈ Combat Management") | |
with gr.Row(): | |
add_character = gr.Textbox(label="Character Name", placeholder="Add to initiative...") | |
add_initiative = gr.Number(label="Initiative Roll", value=10) | |
add_btn = gr.Button("β Add to Initiative") | |
initiative_list = gr.Dataframe( | |
headers=["Name", "Initiative", "HP", "AC", "Status"], | |
label="Initiative Order", | |
interactive=True | |
) | |
with gr.Row(): | |
next_turn_btn = gr.Button("βοΈ Next Turn", variant="primary") | |
reset_combat_btn = gr.Button("π Reset Combat") | |
with gr.TabItem("π Session Notes"): | |
gr.Markdown("### π Session Management") | |
session_date = gr.Textbox(label="Session Date", value="Session 1") | |
with gr.Row(): | |
with gr.Column(): | |
key_events = gr.Textbox(label="Key Events", lines=4, placeholder="What happened this session?") | |
npc_interactions = gr.Textbox(label="NPC Interactions", lines=3, placeholder="Who did they meet?") | |
with gr.Column(): | |
player_actions = gr.Textbox(label="Notable Player Actions", lines=4, placeholder="What did the players do?") | |
next_session_prep = gr.Textbox(label="Next Session Prep", lines=3, placeholder="What to prepare for next time?") | |
save_notes_btn = gr.Button("πΎ Save Session Notes", variant="primary") | |
notes_output = gr.File(label="Session Notes File") | |
with gr.TabItem("π² Random Generators"): | |
gr.Markdown("### π― Quick Generators") | |
with gr.Row(): | |
with gr.Column(): | |
gr.Markdown("**Name Generators**") | |
name_type = gr.Dropdown( | |
choices=["Human Male", "Human Female", "Elven", "Dwarven", "Orcish", "Fantasy Place", "Tavern", "Shop"], | |
value="Human Male" | |
) | |
gen_name_btn = gr.Button("π² Generate Name") | |
random_name = gr.Textbox(label="Generated Name") | |
with gr.Column(): | |
gr.Markdown("**Quick Encounters**") | |
encounter_level = gr.Slider(1, 20, value=5, label="Party Level") | |
encounter_difficulty = gr.Dropdown( | |
choices=["Easy", "Medium", "Hard", "Deadly"], | |
value="Medium" | |
) | |
gen_encounter_btn = gr.Button("βοΈ Generate Encounter") | |
random_encounter = gr.Textbox(label="Encounter", lines=3) | |
with gr.Row(): | |
with gr.Column(): | |
gr.Markdown("**Plot Hooks**") | |
hook_theme = gr.Dropdown( | |
choices=["Mystery", "Adventure", "Political", "Personal", "Rescue", "Exploration"], | |
value="Adventure" | |
) | |
gen_hook_btn = gr.Button("π£ Generate Plot Hook") | |
plot_hook = gr.Textbox(label="Plot Hook", lines=2) | |
with gr.Column(): | |
gr.Markdown("**Weather & Atmosphere**") | |
climate = gr.Dropdown( | |
choices=["Temperate", "Tropical", "Arctic", "Desert", "Mountainous"], | |
value="Temperate" | |
) | |
gen_weather_btn = gr.Button("π€οΈ Generate Weather") | |
weather = gr.Textbox(label="Weather & Mood", lines=2) | |
# ===== EVENT HANDLERS ===== | |
# Character Creator Events | |
def roll_abilities(): | |
abilities = character_creator.roll_ability_scores() | |
return ( | |
abilities["Strength"], abilities["Dexterity"], abilities["Constitution"], | |
abilities["Intelligence"], abilities["Wisdom"], abilities["Charisma"] | |
) | |
def generate_character_name(race, char_class, gender, alignment): | |
try: | |
from openai import OpenAI | |
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) | |
prompt = f"Generate a {gender} {race} {char_class} name appropriate for D&D. Return only the name." | |
response = client.chat.completions.create( | |
model="gpt-4", | |
messages=[{"role": "user", "content": prompt}], | |
max_tokens=30 | |
) | |
return response.choices[0].message.content.strip() | |
except Exception as e: | |
# Fallback to simple name generation | |
names = { | |
"Human": { | |
"Male": ["Garrett", "Marcus", "Thomas", "William", "James"], | |
"Female": ["Elena", "Sarah", "Miranda", "Catherine", "Rose"], | |
"Non-binary": ["Alex", "Jordan", "Riley", "Casey", "Taylor"], | |
"Other": ["River", "Sage", "Phoenix", "Ash", "Rowan"] | |
}, | |
"Elf": { | |
"Male": ["Aelar", "Berrian", "Drannor", "Enna", "Galinndan"], | |
"Female": ["Adrie", "Althaea", "Anastrianna", "Andraste", "Antinua"], | |
"Non-binary": ["Aerdyl", "Ahvir", "Aramil", "Aranea", "Berris"], | |
"Other": ["Dayereth", "Enna", "Galinndan", "Hadarai", "Halimath"] | |
}, | |
"Dwarf": { | |
"Male": ["Adrik", "Baern", "Darrak", "Delg", "Eberk"], | |
"Female": ["Amber", "Bardryn", "Diesa", "Eldeth", "Gunnloda"], | |
"Non-binary": ["Alberich", "Balin", "Dain", "Fundin", "GloΓn"], | |
"Other": ["HΓ‘var", "KΓli", "NΓ‘li", "Ori", "Thorek"] | |
}, | |
"Halfling": { | |
"Male": ["Alton", "Ander", "Cade", "Corrin", "Eldon"], | |
"Female": ["Andry", "Bree", "Callie", "Cora", "Euphemia"], | |
"Non-binary": ["Finnan", "Garret", "Lindal", "Lyle", "Merric"], | |
"Other": ["Nedda", "Paela", "Portia", "Seraphina", "Shaena"] | |
} | |
} | |
# Handle various gender identities | |
gender_key = gender | |
if gender in ["Transgender Male", "Male"]: | |
gender_key = "Male" | |
elif gender in ["Transgender Female", "Female"]: | |
gender_key = "Female" | |
elif gender in ["Non-binary", "Genderfluid", "Agender"]: | |
gender_key = "Non-binary" | |
else: | |
gender_key = "Other" | |
race_names = names.get(race, names["Human"]) | |
return random.choice(race_names.get(gender_key, race_names["Other"])) | |
def generate_character_backstory(name, race, char_class, gender, alignment, background): | |
try: | |
from openai import OpenAI | |
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) | |
prompt = f"""Create a compelling backstory for {name}, a {gender} {race} {char_class} with {alignment} alignment and {background} background. | |
Write 2-3 paragraphs about their origins, motivations, and key life events. Be respectful and inclusive in your portrayal.""" | |
response = client.chat.completions.create( | |
model="gpt-4", | |
messages=[{"role": "user", "content": prompt}], | |
max_tokens=300 | |
) | |
return response.choices[0].message.content.strip() | |
except Exception as e: | |
return f"{name} is a {gender} {race} {char_class} with a {background} background. Their journey began in their homeland, where they learned the skills that would define their path as an adventurer, embracing their identity and forging their own destiny." | |
def generate_character_portrait(character_data): | |
if not character_data: | |
return "Please create a character first" | |
prompt = f"Fantasy portrait of a {character_data.gender.lower()} {character_data.race.lower()} {character_data.character_class.lower()}, professional D&D character art, respectful and inclusive representation" | |
return generate_image(prompt) | |
def update_character_summary(name, race, char_class, level, gender, alignment, | |
str_val, dex_val, con_val, int_val, wis_val, cha_val, background, backstory): | |
if not all([str_val, dex_val, con_val, int_val, wis_val, cha_val]): | |
return "*Roll ability scores to see summary*", None | |
base_abilities = { | |
"Strength": int(str_val), "Dexterity": int(dex_val), "Constitution": int(con_val), | |
"Intelligence": int(int_val), "Wisdom": int(wis_val), "Charisma": int(cha_val) | |
} | |
final_abilities = character_creator.apply_racial_modifiers(base_abilities, race) | |
con_modifier = character_creator.calculate_ability_modifier(final_abilities["Constitution"]) | |
hit_points = character_creator.calculate_hit_points(char_class, int(level), con_modifier) | |
character = Character( | |
name=name or "Unnamed Character", race=race, character_class=char_class, | |
level=int(level), gender=gender, alignment=Alignment(alignment), abilities=final_abilities, | |
hit_points=hit_points, skills=character_creator.classes[char_class].skills[:2], | |
background=background, backstory=backstory | |
) | |
summary = f"""**{character.name}** | |
*Level {level} {gender} {race} {char_class} ({alignment})* | |
**Ability Scores:** | |
- STR: {final_abilities['Strength']} ({character_creator.calculate_ability_modifier(final_abilities['Strength']):+d}) | |
- DEX: {final_abilities['Dexterity']} ({character_creator.calculate_ability_modifier(final_abilities['Dexterity']):+d}) | |
- CON: {final_abilities['Constitution']} ({character_creator.calculate_ability_modifier(final_abilities['Constitution']):+d}) | |
- INT: {final_abilities['Intelligence']} ({character_creator.calculate_ability_modifier(final_abilities['Intelligence']):+d}) | |
- WIS: {final_abilities['Wisdom']} ({character_creator.calculate_ability_modifier(final_abilities['Wisdom']):+d}) | |
- CHA: {final_abilities['Charisma']} ({character_creator.calculate_ability_modifier(final_abilities['Charisma']):+d}) | |
**Combat Stats:** | |
- Hit Points: {hit_points} | |
- Hit Die: d{character_creator.classes[char_class].hit_die} | |
**Background:** {background}""" | |
return summary, character | |
# Campaign Events | |
def generate_campaign_concept(theme, level, players): | |
result = dm_agent.generate_campaign_concept(theme, level, players) | |
return result.get("content", "Error generating campaign") | |
def generate_campaign_art(theme, level): | |
prompt = f"{theme} D&D campaign art for level {level} adventurers, epic fantasy illustration" | |
return generate_image(prompt) | |
def generate_session_content(campaign_output, session_num): | |
if not campaign_output: | |
return "Please generate a campaign concept first" | |
result = dm_agent.generate_session_content(campaign_output, session_num) | |
return result.get("content", "Error generating session") | |
# NPC Events | |
def create_npc(context, role, importance): | |
result = npc_agent.generate_npc(context, role, importance) | |
return result.get("content", "Error creating NPC") | |
def generate_npc_portrait(npc_data): | |
if not npc_data: | |
return "Create an NPC first" | |
prompt = "Fantasy portrait of a D&D NPC character, detailed face, professional RPG art" | |
return generate_image(prompt) | |
def npc_roleplay_response(npc_data, player_input, context): | |
if not npc_data or not player_input: | |
return "Please create an NPC and enter player input" | |
result = npc_agent.roleplay_npc(npc_data, player_input, context) | |
return result.get("content", "Error in roleplay") | |
# World Builder Events | |
def create_location(loc_type, theme, purpose): | |
result = world_agent.generate_location(loc_type, theme, purpose) | |
return result.get("content", "Error creating location") | |
def generate_location_art(loc_type, theme): | |
prompt = f"{theme} {loc_type} fantasy location, detailed environment art, D&D setting" | |
return generate_image(prompt) | |
# Loot Events | |
def create_loot_table(level, encounter, rarity): | |
result = loot_agent.generate_loot_table(level, encounter, rarity) | |
return result.get("content", "Error creating loot") | |
def create_magic_item(concept, power, theme): | |
result = loot_agent.create_custom_magic_item(concept, power, theme) | |
return result.get("content", "Error creating item") | |
def generate_item_art(concept, power): | |
prompt = f"{power} magical {concept}, fantasy item illustration, glowing magical effects" | |
return generate_image(prompt) | |
# Random Generator Events | |
def generate_random_name(name_type): | |
names = { | |
"Human Male": ["Garrett", "Marcus", "Thomas", "William", "James"], | |
"Human Female": ["Elena", "Sarah", "Miranda", "Catherine", "Rose"], | |
"Elven": ["Aelar", "Berrian", "Drannor", "Enna", "Galinndan"], | |
"Dwarven": ["Adrik", "Baern", "Darrak", "Delg", "Eberk"], | |
"Orcish": ["Grul", "Thark", "Dench", "Feng", "Gell"], | |
"Fantasy Place": ["Ravenshollow", "Goldbrook", "Thornfield", "Mistral Keep"], | |
"Tavern": ["The Prancing Pony", "Dragon's Rest", "The Silver Tankard"], | |
"Shop": ["Magical Mysteries", "Fine Blades & More", "Potions & Remedies"] | |
} | |
return random.choice(names.get(name_type, ["Unknown"])) | |
def generate_random_encounter(level, difficulty): | |
encounters = [ | |
f"Bandit ambush (adapted for level {level})", | |
f"Wild animal encounter (CR {max(1, level//4)})", | |
f"Mysterious traveler with a quest", | |
f"Ancient ruins with {difficulty.lower()} traps", | |
f"Rival adventuring party", | |
f"Magical phenomenon requiring investigation" | |
] | |
return random.choice(encounters) | |
def generate_plot_hook(theme): | |
hooks = { | |
"Mystery": "A beloved local figure has vanished without a trace, leaving behind only cryptic clues.", | |
"Adventure": "Ancient maps surface pointing to a legendary treasure thought lost forever.", | |
"Political": "A diplomatic envoy requests secret protection during dangerous negotiations.", | |
"Personal": "A character's past catches up with them in an unexpected way.", | |
"Rescue": "Innocent people are trapped in a dangerous situation and need immediate help.", | |
"Exploration": "Uncharted territories beckon with promises of discovery and danger." | |
} | |
return hooks.get(theme, "A mysterious stranger approaches with an urgent request.") | |
def generate_weather(climate): | |
weather_options = { | |
"Temperate": ["Sunny and mild", "Light rain showers", "Overcast skies", "Gentle breeze"], | |
"Tropical": ["Hot and humid", "Sudden thunderstorm", "Sweltering heat", "Monsoon rains"], | |
"Arctic": ["Bitter cold winds", "Heavy snowfall", "Blizzard conditions", "Icy fog"], | |
"Desert": ["Scorching sun", "Sandstorm approaching", "Cool desert night", "Rare rainfall"], | |
"Mountainous": ["Mountain mist", "Alpine winds", "Rocky terrain", "Sudden weather change"] | |
} | |
return random.choice(weather_options.get(climate, ["Pleasant weather"])) | |
# Wire up all events | |
roll_btn.click(roll_abilities, outputs=[str_score, dex_score, con_score, int_score, wis_score, cha_score]) | |
generate_name_btn.click( | |
generate_character_name, | |
inputs=[race_dropdown, class_dropdown, gender_dropdown, alignment_dropdown], | |
outputs=[character_name] | |
) | |
generate_backstory_btn.click( | |
generate_character_backstory, | |
inputs=[character_name, race_dropdown, class_dropdown, gender_dropdown, alignment_dropdown, background_dropdown], | |
outputs=[backstory] | |
) | |
portrait_btn.click( | |
generate_character_portrait, | |
inputs=[character_data], | |
outputs=[character_portrait] | |
) | |
# Update character summary when inputs change | |
for component in [character_name, race_dropdown, class_dropdown, level_slider, gender_dropdown, alignment_dropdown, | |
str_score, dex_score, con_score, int_score, wis_score, cha_score, background_dropdown, backstory]: | |
component.change( | |
update_character_summary, | |
inputs=[character_name, race_dropdown, class_dropdown, level_slider, gender_dropdown, alignment_dropdown, | |
str_score, dex_score, con_score, int_score, wis_score, cha_score, background_dropdown, backstory], | |
outputs=[character_summary, character_data] | |
) | |
# Campaign events | |
generate_campaign_btn.click( | |
generate_campaign_concept, | |
inputs=[campaign_theme, campaign_level, player_count], | |
outputs=[campaign_output] | |
) | |
campaign_visual_btn.click( | |
generate_campaign_art, | |
inputs=[campaign_theme, campaign_level], | |
outputs=[campaign_image] | |
) | |
generate_session_btn.click( | |
generate_session_content, | |
inputs=[campaign_output, session_number], | |
outputs=[session_output] | |
) | |
# NPC events | |
create_npc_btn.click( | |
create_npc, | |
inputs=[npc_context, npc_role, npc_importance], | |
outputs=[npc_output] | |
) | |
npc_portrait_btn.click( | |
generate_npc_portrait, | |
inputs=[current_npc_data], | |
outputs=[npc_portrait] | |
) | |
roleplay_btn.click( | |
npc_roleplay_response, | |
inputs=[npc_output, player_input, roleplay_context], | |
outputs=[npc_response] | |
) | |
# World builder events | |
generate_location_btn.click( | |
create_location, | |
inputs=[location_type, location_theme, location_purpose], | |
outputs=[location_output] | |
) | |
location_art_btn.click( | |
generate_location_art, | |
inputs=[location_type, location_theme], | |
outputs=[location_image] | |
) | |
# Loot events | |
generate_loot_btn.click( | |
create_loot_table, | |
inputs=[loot_level, encounter_type, loot_rarity], | |
outputs=[loot_output] | |
) | |
create_item_btn.click( | |
create_magic_item, | |
inputs=[item_concept, power_level, campaign_theme_item], | |
outputs=[magic_item_output] | |
) | |
item_art_btn.click( | |
generate_item_art, | |
inputs=[item_concept, power_level], | |
outputs=[item_image] | |
) | |
# Random generator events | |
gen_name_btn.click(generate_random_name, inputs=[name_type], outputs=[random_name]) | |
gen_encounter_btn.click(generate_random_encounter, inputs=[encounter_level, encounter_difficulty], outputs=[random_encounter]) | |
gen_hook_btn.click(generate_plot_hook, inputs=[hook_theme], outputs=[plot_hook]) | |
gen_weather_btn.click(generate_weather, inputs=[climate], outputs=[weather]) | |
return demo | |
# Main entry point | |
if __name__ == "__main__": | |
logger.info("π° Starting Advanced D&D Campaign Manager...") | |
# Check for OpenAI API key | |
api_key = os.getenv("OPENAI_API_KEY") | |
if api_key: | |
logger.info("β OpenAI API key found - All AI features enabled!") | |
else: | |
logger.warning("β οΈ No OpenAI API key found - AI features will be limited") | |
logger.info("π‘ Add OPENAI_API_KEY=your_key to a .env file to enable all AI features") | |
demo = create_main_interface() | |
try: | |
demo.launch( | |
share=True, | |
server_name="0.0.0.0", | |
server_port=7860, | |
inbrowser=True | |
) | |
logger.info("π App launched successfully!") | |
logger.info("π Local URL: http://localhost:7860") | |
except Exception as e: | |
logger.error(f"β Launch failed: {e}") | |
logger.info("π‘ Try: pip install --upgrade gradio") | |