Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| from .config import FEATURE_SEQUENCE, SECTIONS, HF_TEXT_MODELS, HF_IMAGE_MODELS, GEMINI_API_KEY | |
| from .core_logic import ( | |
| features_data, generate_prompt, handle_regeneration, | |
| save_character, load_character, get_example_list, load_example_character | |
| ) | |
| from .integrations import ( | |
| get_ollama_models, check_comfy_availability, refine_master, generate_image_master | |
| ) | |
| from .name_generator import generate_fantasy_name | |
| UI_CSS = """ | |
| .container span { | |
| line-height: 1.6 !important; | |
| margin-bottom: 8px !important; | |
| display: inline-block !important; | |
| } | |
| .container textarea { | |
| min-height: 42px !important; | |
| line-height: 1.5 !important; | |
| overflow-y: visible !important; | |
| padding-top: 8px !important; | |
| padding-bottom: 8px !important; | |
| } | |
| /* Firefox specific fixes */ | |
| @-moz-document url-prefix() { | |
| .container textarea { | |
| padding-top: 10px !important; | |
| padding-bottom: 10px !important; | |
| } | |
| .container span { | |
| padding-bottom: 2px !important; | |
| } | |
| } | |
| """ | |
| def build_ui(): | |
| with gr.Blocks(title="Chronicle Portrait Studio") as demo: | |
| with gr.Row(variant="compact"): | |
| gr.HTML(''' | |
| <div style="display: flex; justify-content: center; background: #121212; padding: 10px; border-radius: 10px;"> | |
| <svg width="600" height="120" viewBox="0 0 600 120" xmlns="http://www.w3.org/2000/svg"> | |
| <defs> | |
| <linearGradient id="goldGrad" x1="0%" y1="0%" x2="0%" y2="100%"> | |
| <stop offset="0%" style="stop-color:#FFD700;stop-opacity:1" /> | |
| <stop offset="100%" style="stop-color:#B8860B;stop-opacity:1" /> | |
| </linearGradient> | |
| <filter id="shadow" x="-20%" y="-20%" width="140%" height="140%"> | |
| <feGaussianBlur in="SourceAlpha" stdDeviation="2" /> | |
| <feOffset dx="2" dy="2" result="offsetblur" /> | |
| <feComponentTransfer> | |
| <feFuncA type="linear" slope="0.5" /> | |
| </feComponentTransfer> | |
| <feMerge> | |
| <feMergeNode /> | |
| <feMergeNode in="SourceGraphic" /> | |
| </feMerge> | |
| </filter> | |
| </defs> | |
| <rect width="600" height="120" fill="#121212" rx="10" /> | |
| <g transform="translate(60, 60)" filter="url(#shadow)"> | |
| <path d="M0 -35 L25 -25 L25 5 Q25 25 0 35 Q-25 25 -25 5 L-25 -25 Z" fill="url(#goldGrad)" opacity="0.9" /> | |
| <path d="M0 -30 L20 -22 L20 5 Q20 20 0 30 Q-20 20 -20 5 L-20 -22 Z" fill="#121212" /> | |
| <text x="0" y="10" font-family="serif" font-size="28" fill="url(#goldGrad)" text-anchor="middle" font-weight="bold">C</text> | |
| </g> | |
| <text x="340" y="62" font-family="'Georgia', serif" font-size="42" fill="url(#goldGrad)" style="filter:url(#shadow)" font-weight="bold" text-anchor="middle">CHRONICLE</text> | |
| <text x="340" y="98" font-family="'Georgia', serif" font-size="20" fill="#bdc3c7" letter-spacing="2" opacity="0.8" text-anchor="middle">PORTRAIT STUDIO</text> | |
| <rect x="5" y="5" width="590" height="110" fill="none" stroke="#B8860B" stroke-width="1" rx="8" stroke-dasharray="15,10" opacity="0.3" /> | |
| </svg> | |
| </div> | |
| ''') | |
| gr.Markdown("# ⚔️ Chronicle Portrait Studio") | |
| gr.Markdown("Craft legendary AI-powered portraits for your RPG adventures.") | |
| dropdowns = [] | |
| checkboxes = [] | |
| extra_texts = [] | |
| def create_feature_ui(category, subcategory, label, default_value): | |
| choices = list(features_data[category][subcategory].keys()) | |
| gr.Markdown(f"**{label}**") | |
| with gr.Row(equal_height=True): | |
| dd = gr.Dropdown(choices=choices, value=default_value, scale=12, show_label=False) | |
| cb = gr.Checkbox(label="🎲", value=False, scale=1, min_width=50, container=False) | |
| dropdowns.append(dd) | |
| checkboxes.append(cb) | |
| return dd, cb | |
| with gr.Row(): | |
| with gr.Column(scale=2): | |
| with gr.Tabs(): | |
| with gr.TabItem("👤 Identity & Expression"): | |
| gr.Markdown("### 👤 Identity") | |
| with gr.Row(): | |
| character_name = gr.Textbox(label="Character Name", placeholder="Enter name...", value="Unnamed Hero", scale=9) | |
| name_gen_btn = gr.Button("🎲", scale=1, variant="secondary") | |
| race_dd, _ = create_feature_ui('identity', 'race', "Race", "Human") | |
| create_feature_ui('identity', 'class', "Class", "Fighter") | |
| create_feature_ui('identity', 'gender', "Gender", "Male") | |
| create_feature_ui('identity', 'age', "Age", "Young Adult") | |
| extra_id = gr.Textbox(placeholder="Extra Identity details (e.g. lineage, title)", label="Additional Identity Info") | |
| extra_texts.append(extra_id) | |
| gr.Markdown("### 🎭 Expression & Pose") | |
| create_feature_ui('expression_pose', 'expression', "Expression", "Determined") | |
| create_feature_ui('expression_pose', 'pose', "Pose", "Standing") | |
| with gr.TabItem("🎨 Appearance"): | |
| gr.Markdown("### 🎨 Appearance") | |
| create_feature_ui('appearance', 'hair_color', "Hair Color", "Brown") | |
| create_feature_ui('appearance', 'hair_style', "Hair Style", "Short") | |
| create_feature_ui('appearance', 'eye_color', "Eye Color", "Brown") | |
| create_feature_ui('appearance', 'build', "Build", "Average") | |
| create_feature_ui('appearance', 'skin_tone', "Skin Tone", "Fair") | |
| create_feature_ui('appearance', 'distinguishing_feature', "Distinguishing Feature", "None") | |
| extra_app = gr.Textbox(placeholder="Extra Appearance details (e.g. tattoos, scars)", label="Additional Appearance Info") | |
| extra_texts.append(extra_app) | |
| with gr.TabItem("⚔️ Equipment"): | |
| gr.Markdown("### ⚔️ Equipment") | |
| create_feature_ui('equipment', 'armor', "Armor/Clothing", "Travelers Clothes") | |
| create_feature_ui('equipment', 'weapon', "Primary Weapon", "Longsword") | |
| create_feature_ui('equipment', 'accessory', "Accessory 1", "None") | |
| create_feature_ui('equipment', 'accessory', "Accessory 2", "None") | |
| create_feature_ui('equipment', 'material', "Material Detail", "Weathered") | |
| extra_eq = gr.Textbox(placeholder="Extra Equipment details (e.g. weapon enchantments)", label="Additional Equipment Info") | |
| extra_texts.append(extra_eq) | |
| with gr.TabItem("🌍 Environment"): | |
| gr.Markdown("### 🌍 Environment") | |
| create_feature_ui('environment', 'background', "Background", "Forest") | |
| create_feature_ui('environment', 'lighting', "Lighting", "Natural Sunlight") | |
| create_feature_ui('environment', 'atmosphere', "Atmosphere", "Clear") | |
| extra_env = gr.Textbox(placeholder="Extra Environment details (e.g. time of day, weather)", label="Additional Environment Info") | |
| extra_texts.append(extra_env) | |
| with gr.TabItem("🪄 Style & Technical"): | |
| gr.Markdown("### 🪄 Style & Effects") | |
| create_feature_ui('vfx_style', 'vfx', "Special Effects", "None") | |
| create_feature_ui('vfx_style', 'style', "Art Style", "Digital Illustration") | |
| create_feature_ui('vfx_style', 'mood', "Mood", "Heroic") | |
| create_feature_ui('vfx_style', 'camera', "Camera Angle", "Bust") | |
| extra_sty = gr.Textbox(placeholder="Extra Style details (e.g. specific artists, colors)", label="Additional Style Info") | |
| extra_texts.append(extra_sty) | |
| gr.Markdown("### ⚙️ Technical") | |
| create_feature_ui('technical', 'aspect_ratio', "Aspect Ratio", "1:1") | |
| gr.Markdown("---") | |
| with gr.Row(): | |
| example_dropdown = gr.Dropdown(choices=get_example_list(), label="Example Characters", scale=3) | |
| load_example_btn = gr.Button("📂 Load Example", variant="secondary", scale=1) | |
| save_btn = gr.Button("💾 Save Character", variant="secondary", scale=1) | |
| load_btn = gr.UploadButton("📂 Load Character", file_types=[".json"], variant="secondary", scale=1) | |
| with gr.Group(): | |
| gr.Markdown("### ⚙️ AI Backend Configuration") | |
| with gr.Row(): | |
| ollama_models = get_ollama_models() | |
| ollama_active = len(ollama_models) > 0 | |
| comfy_active = check_comfy_availability() | |
| refinement_choices = ["Hugging Face (Cloud)"] | |
| if GEMINI_API_KEY: | |
| refinement_choices.insert(0, "Gemini (Cloud)") | |
| if ollama_active: | |
| refinement_choices.append("Ollama (Local)") | |
| refinement_backend = gr.Radio( | |
| choices=refinement_choices, | |
| value=refinement_choices[0], | |
| label="Prompt Refinement Backend", | |
| scale=2 | |
| ) | |
| with gr.Column(scale=1): | |
| ollama_model_dropdown = gr.Dropdown( | |
| choices=ollama_models, | |
| value=ollama_models[0] if ollama_active else None, | |
| label="Olama Model", | |
| visible=False | |
| ) | |
| hf_text_model_dropdown = gr.Dropdown( | |
| choices=HF_TEXT_MODELS, | |
| value=HF_TEXT_MODELS[0], | |
| label="HF Text Model", | |
| visible=("Hugging Face (Cloud)" in refinement_choices and refinement_choices[0] == "Hugging Face (Cloud)") | |
| ) | |
| hf_text_provider_input = gr.Textbox( | |
| value="auto", | |
| placeholder="Optional: e.g. fal-ai", | |
| label="HF Text Provider", | |
| visible=("Hugging Face (Cloud)" in refinement_choices and refinement_choices[0] == "Hugging Face (Cloud)") | |
| ) | |
| with gr.Row(): | |
| img_choices = ["Hugging Face (Cloud)"] | |
| if GEMINI_API_KEY: | |
| img_choices.insert(0, "Gemini (Cloud)") | |
| if comfy_active: | |
| img_choices.append("ComfyUI (Local)") | |
| backend_selector = gr.Radio( | |
| choices=img_choices, | |
| value=img_choices[0], | |
| label="Image Generation Backend", | |
| scale=2 | |
| ) | |
| with gr.Column(scale=1): | |
| hf_image_model_dropdown = gr.Dropdown( | |
| choices=HF_IMAGE_MODELS, | |
| value=HF_IMAGE_MODELS[0], | |
| label="HF Image Model", | |
| visible=("Hugging Face (Cloud)" in img_choices and img_choices[0] == "Hugging Face (Cloud)") | |
| ) | |
| hf_image_provider_input = gr.Textbox( | |
| value="auto", | |
| placeholder="Optional: e.g. fal-ai", | |
| label="HF Image Provider", | |
| visible=("Hugging Face (Cloud)" in img_choices and img_choices[0] == "Hugging Face (Cloud)") | |
| ) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| hf_login = gr.LoginButton() | |
| gr.Markdown("*Note: HF Login is disabled in 'shared' mode. Use an individual token for remote access.*") | |
| with gr.Column(scale=2): | |
| hf_token_input = gr.Textbox( | |
| label="Individual Hugging Face Token (Optional)", | |
| placeholder="Paste your hf_... token here for custom rate limits", | |
| type="password" | |
| ) | |
| with gr.Column(scale=1): | |
| gr.Markdown("### 📝 Prompts & Output") | |
| prompt_output = gr.Textbox(label="Generated Technical Prompt", lines=4, interactive=False, buttons=["copy"]) | |
| refine_btn = gr.Button("🧠 Refine Prompt", variant="primary") | |
| regenerate_btn = gr.Button("✨ Randomize Features", variant="secondary") | |
| refined_output = gr.Textbox(label="Refined Artistic Prompt", lines=6, interactive=True, buttons=["copy", "paste", "clear"]) | |
| gr.Markdown("---") | |
| image_output = gr.Image(label="Portrait", show_label=False) | |
| gen_img_btn = gr.Button("🖼️ Generate Image", variant="primary", scale=1) | |
| download_img_btn = gr.DownloadButton("📥 Download Portrait (PNG)", variant="secondary", visible=False) | |
| status_msg = gr.Markdown("") | |
| download_file = gr.File(label="Saved Character JSON", visible=False) | |
| all_input_components = [character_name] + dropdowns + checkboxes + extra_texts | |
| for comp in all_input_components: | |
| comp.change(fn=generate_prompt, inputs=all_input_components, outputs=prompt_output) | |
| regenerate_btn.click( | |
| fn=handle_regeneration, | |
| inputs=dropdowns + checkboxes, | |
| outputs=dropdowns | |
| ).then( | |
| fn=generate_prompt, | |
| inputs=all_input_components, | |
| outputs=prompt_output | |
| ).then( | |
| fn=lambda: "", | |
| outputs=refined_output | |
| ) | |
| name_gen_btn.click( | |
| fn=lambda r: generate_fantasy_name(r), | |
| inputs=[race_dd], | |
| outputs=character_name | |
| ) | |
| refine_btn.click( | |
| fn=refine_master, | |
| inputs=[prompt_output, refinement_backend, ollama_model_dropdown, hf_text_model_dropdown, hf_text_provider_input, hf_token_input, character_name], | |
| outputs=[refined_output, status_msg] | |
| ) | |
| gen_img_btn.click( | |
| fn=generate_image_master, | |
| inputs=[refined_output, prompt_output, dropdowns[-1], backend_selector, hf_image_model_dropdown, hf_image_provider_input, hf_token_input, character_name], | |
| outputs=[image_output, download_img_btn, status_msg] | |
| ).then( | |
| fn=lambda x: gr.update(value=x, visible=True) if x else gr.update(visible=False), | |
| inputs=download_img_btn, | |
| outputs=download_img_btn | |
| ) | |
| refinement_backend.change( | |
| fn=lambda b: ( | |
| gr.update(visible=(b == "Ollama (Local)")), | |
| gr.update(visible=(b == "Hugging Face (Cloud)")), | |
| gr.update(visible=(b == "Hugging Face (Cloud)")) | |
| ), | |
| inputs=refinement_backend, | |
| outputs=[ollama_model_dropdown, hf_text_model_dropdown, hf_text_provider_input] | |
| ) | |
| backend_selector.change( | |
| fn=lambda b: ( | |
| gr.update(visible=(b == "Hugging Face (Cloud)")), | |
| gr.update(visible=(b == "Hugging Face (Cloud)")) | |
| ), | |
| inputs=backend_selector, | |
| outputs=[hf_image_model_dropdown, hf_image_provider_input] | |
| ) | |
| save_btn.click( | |
| fn=save_character, | |
| inputs=all_input_components, | |
| outputs=download_file | |
| ).then( | |
| fn=lambda: gr.update(visible=True), | |
| outputs=download_file | |
| ) | |
| load_btn.upload( | |
| fn=load_character, | |
| inputs=load_btn, | |
| outputs=all_input_components | |
| ).then( | |
| fn=generate_prompt, | |
| inputs=all_input_components, | |
| outputs=prompt_output | |
| ).then( | |
| fn=lambda: "", | |
| outputs=refined_output | |
| ) | |
| load_example_btn.click( | |
| fn=load_example_character, | |
| inputs=example_dropdown, | |
| outputs=all_input_components | |
| ).then( | |
| fn=generate_prompt, | |
| inputs=all_input_components, | |
| outputs=prompt_output | |
| ).then( | |
| fn=lambda: "", | |
| outputs=refined_output | |
| ) | |
| demo.load(fn=generate_prompt, inputs=all_input_components, outputs=prompt_output) | |
| return demo | |