| import os |
| import gc |
| import uuid |
| import time |
| import random |
| import requests |
|
|
| import torch |
| import gradio as gr |
|
|
| from PIL import Image |
| from diffusers import StableDiffusionPipeline, StableDiffusionImg2ImgPipeline |
|
|
|
|
| |
| |
| |
| device = "cpu" |
|
|
| |
| |
| |
| MODEL_ID = "Lykon/dreamshaper-8" |
|
|
| |
| |
| |
| GROQ_API_KEY = os.getenv("GROQ_API_KEY", "") |
| GROQ_MODEL = os.getenv("GROQ_MODEL", "llama-3.3-70b-versatile") |
| GROQ_API_URL = "https://api.groq.com/openai/v1/chat/completions" |
|
|
| |
| |
| |
| current_pipe = None |
| current_mode = None |
| history = [] |
|
|
|
|
| |
| |
| |
| STYLE_PRESETS = { |
| "None": "", |
| "Realistic": "photorealistic, realistic lighting, realistic details, natural skin texture", |
| "Anime": "anime style, vibrant colors, clean line art, detailed anime illustration", |
| "Cinematic": "cinematic lighting, dramatic shadows, film still, movie composition", |
| "Fantasy": "fantasy art, magical atmosphere, epic composition, highly detailed", |
| "Product": "professional product photography, studio lighting, clean background", |
| } |
|
|
|
|
| |
| |
| |
| NEGATIVE_PROMPT = ( |
| "worst quality, low quality, blurry, bad anatomy, bad proportions, " |
| "extra limbs, missing limbs, distorted face, deformed hands, bad hands, " |
| "extra fingers, missing fingers, duplicated body parts, ugly, watermark, " |
| "text, logo, signature, jpeg artifacts, oversaturated" |
| ) |
|
|
|
|
| |
| |
| |
| |
| def translate_prompt(prompt): |
| if not prompt: |
| return "" |
|
|
| mapping = { |
| "pria": "man", |
| "laki-laki": "man", |
| "wanita": "woman", |
| "perempuan": "woman", |
| "anak": "child", |
| "wajah": "face", |
| "potret": "portrait", |
| "gunung": "mountain", |
| "pantai": "beach", |
| "kota": "city", |
| "malam": "night", |
| "siang": "day", |
| "pagi": "morning", |
| "sore": "afternoon", |
| "senja": "sunset", |
| "laut": "sea", |
| "langit": "sky", |
| "hutan": "forest", |
| "mobil": "car", |
| "motor": "motorcycle", |
| "rumah": "house", |
| "jalan": "street", |
| "sungai": "river", |
| "danau": "lake", |
| "kucing": "cat", |
| "anjing": "dog", |
| "burung": "bird", |
| "bunga": "flower", |
| "pohon": "tree", |
| "robot": "robot", |
| "hujan": "rain", |
| "salju": "snow", |
| "api": "fire", |
| "air": "water", |
| "cantik": "beautiful", |
| "tampan": "handsome", |
| "futuristik": "futuristic", |
| "tradisional": "traditional", |
| "jawa": "javanese", |
| "indonesia": "indonesia", |
| "desa": "village", |
| "sawah": "rice field", |
| "kerajaan": "kingdom", |
| "istana": "palace", |
| "emas": "gold", |
| "perak": "silver", |
| "hitam": "black", |
| "putih": "white", |
| "merah": "red", |
| "biru": "blue", |
| "hijau": "green", |
| "kuning": "yellow", |
| } |
|
|
| prompt = prompt.lower() |
|
|
| for k, v in mapping.items(): |
| prompt = prompt.replace(k, v) |
|
|
| return prompt |
|
|
|
|
| |
| |
| |
| def detect_type(prompt): |
| p = prompt.lower() |
|
|
| portrait_keywords = [ |
| "person", "man", "woman", "girl", "boy", |
| "face", "portrait", "child", "people" |
| ] |
|
|
| scene_keywords = [ |
| "mountain", "city", "forest", "beach", "sky", |
| "sea", "street", "river", "lake", "tree", |
| "village", "landscape", "room", "house", |
| "rice field", "palace" |
| ] |
|
|
| if any(k in p for k in portrait_keywords): |
| return "portrait" |
|
|
| if any(k in p for k in scene_keywords): |
| return "scene" |
|
|
| return "object" |
|
|
|
|
| |
| |
| |
| def build_prompt_local(prompt, style): |
| """ |
| Fallback prompt engine lokal jika Groq API tidak tersedia. |
| """ |
| prompt = translate_prompt(prompt) |
| style_text = STYLE_PRESETS.get(style, "") |
|
|
| base = "masterpiece, best quality, ultra detailed, sharp focus, high detail" |
| detail = "intricate details, detailed texture, balanced composition" |
|
|
| ptype = detect_type(prompt) |
|
|
| if ptype == "portrait": |
| extra = ( |
| "detailed face, natural skin texture, expressive eyes, " |
| "soft lighting, portrait composition, depth of field, " |
| "realistic facial proportions" |
| ) |
| elif ptype == "scene": |
| extra = ( |
| "wide shot, cinematic composition, environmental detail, " |
| "atmospheric lighting, realistic perspective, immersive scene" |
| ) |
| else: |
| extra = ( |
| "centered composition, studio lighting, clean background, " |
| "sharp object details, professional photography" |
| ) |
|
|
| parts = [ |
| prompt, |
| style_text, |
| base, |
| detail, |
| extra |
| ] |
|
|
| return ", ".join([p for p in parts if p]) |
|
|
|
|
| |
| |
| |
| def enhance_prompt_with_groq(user_prompt, style): |
| """ |
| Mengubah prompt Bahasa Indonesia menjadi prompt Bahasa Inggris |
| yang lebih optimal untuk DreamShaper / Stable Diffusion menggunakan Groq API. |
| """ |
|
|
| if not user_prompt: |
| return "" |
|
|
| if not GROQ_API_KEY: |
| print("GROQ_API_KEY belum diset. Menggunakan prompt lokal.") |
| return build_prompt_local(user_prompt, style) |
|
|
| system_prompt = """ |
| You are an expert prompt engineer for Stable Diffusion DreamShaper. |
| |
| Your task: |
| - Convert Indonesian image prompts into high-quality English prompts. |
| - Preserve the user's exact visual intent. |
| - Do not change the main subject. |
| - Do not add unrelated objects. |
| - Add useful visual details only when helpful: |
| composition, lighting, atmosphere, camera angle, lens, texture, detail level, realism, style. |
| - Make the final prompt suitable for Stable Diffusion / DreamShaper. |
| - Return only one final English prompt. |
| - Do not use markdown. |
| - Do not use bullet points. |
| - Do not explain anything. |
| |
| Important: |
| - Keep the output concise but rich in visual detail. |
| - Avoid overly long prompts. |
| - Keep the prompt general-audience safe. |
| - If the request is unsafe or inappropriate, rewrite it into a safe, non-explicit, general-audience visual prompt. |
| """ |
|
|
| selected_style = STYLE_PRESETS.get(style, "") |
|
|
| user_message = f""" |
| User Indonesian prompt: |
| {user_prompt} |
| |
| Selected style: |
| {style} |
| |
| Style keyword: |
| {selected_style} |
| |
| Create one optimized English prompt for DreamShaper. |
| """ |
|
|
| headers = { |
| "Authorization": f"Bearer {GROQ_API_KEY}", |
| "Content-Type": "application/json" |
| } |
|
|
| payload = { |
| "model": GROQ_MODEL, |
| "messages": [ |
| { |
| "role": "system", |
| "content": system_prompt |
| }, |
| { |
| "role": "user", |
| "content": user_message |
| } |
| ], |
| "temperature": 0.35, |
| "max_tokens": 300 |
| } |
|
|
| try: |
| response = requests.post( |
| GROQ_API_URL, |
| headers=headers, |
| json=payload, |
| timeout=30 |
| ) |
|
|
| response.raise_for_status() |
|
|
| data = response.json() |
|
|
| enhanced_prompt = data["choices"][0]["message"]["content"].strip() |
| enhanced_prompt = enhanced_prompt.replace("\n", " ").strip() |
|
|
| if not enhanced_prompt: |
| return build_prompt_local(user_prompt, style) |
|
|
| return enhanced_prompt |
|
|
| except Exception as e: |
| print("Groq prompt enhancer error:", e) |
| print("Fallback ke prompt lokal.") |
| return build_prompt_local(user_prompt, style) |
|
|
|
|
| |
| |
| |
| def build_prompt(prompt, style, use_groq=True): |
| """ |
| Prompt utama. |
| Jika use_groq aktif dan API key tersedia, prompt diproses Groq. |
| Jika gagal, fallback ke prompt lokal. |
| """ |
| if use_groq: |
| return enhance_prompt_with_groq(prompt, style) |
|
|
| return build_prompt_local(prompt, style) |
|
|
|
|
| |
| |
| |
| def load_pipe(mode): |
| """ |
| mode: |
| - txt2img |
| - img2img |
| """ |
| global current_pipe, current_mode |
|
|
| if current_pipe is not None and current_mode == mode: |
| return current_pipe |
|
|
| if current_pipe is not None: |
| print("Clearing previous pipeline...") |
| del current_pipe |
| current_pipe = None |
| current_mode = None |
| gc.collect() |
|
|
| print(f"Loading DreamShaper pipeline mode: {mode}") |
|
|
| if mode == "txt2img": |
| pipe = StableDiffusionPipeline.from_pretrained( |
| MODEL_ID, |
| torch_dtype=torch.float32, |
| low_cpu_mem_usage=True |
| ) |
|
|
| elif mode == "img2img": |
| pipe = StableDiffusionImg2ImgPipeline.from_pretrained( |
| MODEL_ID, |
| torch_dtype=torch.float32, |
| low_cpu_mem_usage=True |
| ) |
|
|
| else: |
| raise ValueError("Unknown pipeline mode") |
|
|
| pipe.to(device) |
| pipe.enable_attention_slicing() |
|
|
| |
| pipe.safety_checker = None |
|
|
| current_pipe = pipe |
| current_mode = mode |
|
|
| return pipe |
|
|
|
|
| |
| |
| |
| def normalize_seed(seed): |
| try: |
| seed = int(seed) |
| except Exception: |
| seed = -1 |
|
|
| if seed == -1: |
| seed = random.randint(0, 999999) |
|
|
| return seed |
|
|
|
|
| def normalize_size(value, default=512): |
| try: |
| value = int(value) |
| except Exception: |
| value = default |
|
|
| value = max(256, min(value, 768)) |
|
|
| |
| value = value - (value % 8) |
|
|
| return value |
|
|
|
|
| def upscale(img): |
| return img.resize( |
| (img.width * 2, img.height * 2), |
| Image.LANCZOS |
| ) |
|
|
|
|
| def save_image(img): |
| os.makedirs("outputs", exist_ok=True) |
|
|
| filename = f"{uuid.uuid4().hex}.png" |
| path = os.path.join("outputs", filename) |
|
|
| img.save(path) |
|
|
| return path |
|
|
|
|
| def prepare_img2img_input(input_image): |
| if input_image is None: |
| return None |
|
|
| if not isinstance(input_image, Image.Image): |
| input_image = Image.fromarray(input_image) |
|
|
| input_image = input_image.convert("RGB") |
|
|
| input_image.thumbnail( |
| (768, 768), |
| Image.LANCZOS |
| ) |
|
|
| w = input_image.width - (input_image.width % 8) |
| h = input_image.height - (input_image.height % 8) |
|
|
| w = max(256, w) |
| h = max(256, h) |
|
|
| input_image = input_image.resize( |
| (w, h), |
| Image.LANCZOS |
| ) |
|
|
| return input_image |
|
|
|
|
| |
| |
| |
| def generate_text_to_image( |
| prompt, |
| style, |
| use_groq, |
| steps, |
| width, |
| height, |
| seed, |
| up |
| ): |
| pipe = load_pipe("txt2img") |
|
|
| final_prompt = build_prompt(prompt, style, use_groq) |
|
|
| print("FINAL TXT2IMG PROMPT:", final_prompt) |
|
|
| width = normalize_size(width) |
| height = normalize_size(height) |
| seed = normalize_seed(seed) |
|
|
| generator = torch.Generator(device=device).manual_seed(seed) |
|
|
| image = pipe( |
| prompt=final_prompt, |
| negative_prompt=NEGATIVE_PROMPT, |
| width=width, |
| height=height, |
| guidance_scale=7.5, |
| num_inference_steps=int(steps), |
| generator=generator |
| ).images[0] |
|
|
| if up: |
| image = upscale(image) |
|
|
| history.append(image) |
|
|
| file_path = save_image(image) |
|
|
| return image, file_path, seed, final_prompt, history |
|
|
|
|
| |
| |
| |
| def generate_image_to_image( |
| prompt, |
| style, |
| use_groq, |
| steps, |
| seed, |
| input_image, |
| strength, |
| up |
| ): |
| if input_image is None: |
| return None, None, None, "Input image belum diisi.", history |
|
|
| pipe = load_pipe("img2img") |
|
|
| final_prompt = build_prompt(prompt, style, use_groq) |
|
|
| print("FINAL IMG2IMG PROMPT:", final_prompt) |
|
|
| seed = normalize_seed(seed) |
| input_image = prepare_img2img_input(input_image) |
|
|
| generator = torch.Generator(device=device).manual_seed(seed) |
|
|
| image = pipe( |
| prompt=final_prompt, |
| negative_prompt=NEGATIVE_PROMPT, |
| image=input_image, |
| strength=float(strength), |
| guidance_scale=7.5, |
| num_inference_steps=int(steps), |
| generator=generator |
| ).images[0] |
|
|
| if up: |
| image = upscale(image) |
|
|
| history.append(image) |
|
|
| file_path = save_image(image) |
|
|
| return image, file_path, seed, final_prompt, history |
|
|
|
|
| |
| |
| |
| def run_txt2img( |
| prompt, |
| style, |
| use_groq, |
| steps, |
| width, |
| height, |
| seed, |
| up |
| ): |
| for _ in range(2): |
| try: |
| return generate_text_to_image( |
| prompt, |
| style, |
| use_groq, |
| steps, |
| width, |
| height, |
| seed, |
| up |
| ) |
| except Exception as e: |
| print("Retry Text to Image:", e) |
| gc.collect() |
| time.sleep(2) |
|
|
| return None, None, None, "Generation failed. Check Space logs.", history |
|
|
|
|
| def run_img2img( |
| prompt, |
| style, |
| use_groq, |
| steps, |
| seed, |
| input_image, |
| strength, |
| up |
| ): |
| for _ in range(2): |
| try: |
| return generate_image_to_image( |
| prompt, |
| style, |
| use_groq, |
| steps, |
| seed, |
| input_image, |
| strength, |
| up |
| ) |
| except Exception as e: |
| print("Retry Image to Image:", e) |
| gc.collect() |
| time.sleep(2) |
|
|
| return None, None, None, "Generation failed. Check Space logs.", history |
|
|
|
|
| |
| |
| |
| def select_history(evt: gr.SelectData): |
| if evt.index is None: |
| return None, None |
|
|
| if evt.index >= len(history): |
| return None, None |
|
|
| selected_image = history[evt.index] |
|
|
| return selected_image, evt.index |
|
|
|
|
| def reuse_history(selected_index): |
| if selected_index is None: |
| return None, gr.update(selected="history") |
|
|
| try: |
| selected_index = int(selected_index) |
| selected_image = history[selected_index] |
| except Exception: |
| return None, gr.update(selected="history") |
|
|
| return selected_image, gr.update(selected="img2img") |
|
|
|
|
| def clear_history(): |
| history.clear() |
| gc.collect() |
|
|
| return [], None, None |
|
|
|
|
| def check_groq_status(): |
| if GROQ_API_KEY: |
| return f"✅ Groq API aktif. Model: `{GROQ_MODEL}`" |
|
|
| return ( |
| "⚠️ `GROQ_API_KEY` belum ditemukan. " |
| "Prompt enhancer akan memakai fallback lokal." |
| ) |
|
|
|
|
| |
| |
| |
| with gr.Blocks() as demo: |
| gr.Markdown("# 🚀 AI Image Studio - DreamShaper + Groq Prompt Enhancer") |
|
|
| gr.Markdown( |
| """ |
| Masukkan prompt dalam **Bahasa Indonesia**. |
| Jika **Groq Prompt Enhancer** aktif, prompt akan diubah otomatis menjadi prompt Bahasa Inggris yang lebih optimal untuk DreamShaper. |
| """ |
| ) |
|
|
| gr.Markdown(check_groq_status()) |
|
|
| selected_history_index = gr.State(value=None) |
|
|
| with gr.Tabs(selected="txt2img") as tabs: |
|
|
| |
| |
| |
| with gr.Tab("Text to Image", id="txt2img"): |
| txt_prompt = gr.Textbox( |
| lines=3, |
| label="Prompt Bahasa Indonesia", |
| placeholder="Contoh: pria memakai jaket hitam berdiri di jalan kota saat hujan malam hari" |
| ) |
|
|
| txt_style = gr.Dropdown( |
| choices=list(STYLE_PRESETS.keys()), |
| value="Realistic", |
| label="Style" |
| ) |
|
|
| txt_use_groq = gr.Checkbox( |
| label="Gunakan Groq Prompt Enhancer", |
| value=True |
| ) |
|
|
| txt_steps = gr.Slider( |
| minimum=10, |
| maximum=30, |
| value=20, |
| step=1, |
| label="Steps" |
| ) |
|
|
| txt_seed = gr.Number( |
| value=-1, |
| label="Seed (-1 = random)" |
| ) |
|
|
| txt_width = gr.Slider( |
| minimum=256, |
| maximum=768, |
| value=512, |
| step=64, |
| label="Width" |
| ) |
|
|
| txt_height = gr.Slider( |
| minimum=256, |
| maximum=768, |
| value=512, |
| step=64, |
| label="Height" |
| ) |
|
|
| txt_upscale = gr.Checkbox( |
| label="Upscale 2x", |
| value=False |
| ) |
|
|
| txt_button = gr.Button("Generate Text to Image") |
|
|
| txt_result = gr.Image( |
| label="Result" |
| ) |
|
|
| txt_seed_output = gr.Number( |
| label="Seed Used" |
| ) |
|
|
| txt_final_prompt = gr.Textbox( |
| lines=5, |
| label="Final Prompt ke DreamShaper", |
| interactive=False |
| ) |
|
|
| txt_file_output = gr.File( |
| label="Download Image" |
| ) |
|
|
| |
| |
| |
| with gr.Tab("Image to Image", id="img2img"): |
| img_prompt = gr.Textbox( |
| lines=3, |
| label="Prompt Bahasa Indonesia", |
| placeholder="Contoh: ubah menjadi cinematic style, lighting dramatis, detail lebih realistis" |
| ) |
|
|
| img_input = gr.Image( |
| type="pil", |
| label="Input Image" |
| ) |
|
|
| img_style = gr.Dropdown( |
| choices=list(STYLE_PRESETS.keys()), |
| value="Realistic", |
| label="Style" |
| ) |
|
|
| img_use_groq = gr.Checkbox( |
| label="Gunakan Groq Prompt Enhancer", |
| value=True |
| ) |
|
|
| img_strength = gr.Slider( |
| minimum=0.1, |
| maximum=1.0, |
| value=0.5, |
| step=0.05, |
| label="Strength" |
| ) |
|
|
| img_steps = gr.Slider( |
| minimum=10, |
| maximum=30, |
| value=20, |
| step=1, |
| label="Steps" |
| ) |
|
|
| img_seed = gr.Number( |
| value=-1, |
| label="Seed (-1 = random)" |
| ) |
|
|
| img_upscale = gr.Checkbox( |
| label="Upscale 2x", |
| value=False |
| ) |
|
|
| img_button = gr.Button("Generate Image to Image") |
|
|
| img_result = gr.Image( |
| label="Result" |
| ) |
|
|
| img_seed_output = gr.Number( |
| label="Seed Used" |
| ) |
|
|
| img_final_prompt = gr.Textbox( |
| lines=5, |
| label="Final Prompt ke DreamShaper", |
| interactive=False |
| ) |
|
|
| img_file_output = gr.File( |
| label="Download Image" |
| ) |
|
|
| |
| |
| |
| with gr.Tab("History", id="history"): |
| gr.Markdown( |
| """ |
| Gambar yang dibuat dari **Text to Image** atau **Image to Image** |
| otomatis muncul di sini. |
| |
| Cara memakai ulang: |
| 1. Klik salah satu gambar di History |
| 2. Klik tombol **Reuse to Image to Image** |
| 3. Gambar akan masuk ke menu Image to Image |
| """ |
| ) |
|
|
| history_gallery = gr.Gallery( |
| label="History", |
| columns=3, |
| height=420 |
| ) |
|
|
| selected_history_image = gr.Image( |
| label="Selected Image" |
| ) |
|
|
| reuse_button = gr.Button( |
| "♻️ Reuse to Image to Image" |
| ) |
|
|
| clear_history_button = gr.Button( |
| "🗑️ Clear History" |
| ) |
|
|
| |
| |
| |
|
|
| txt_button.click( |
| fn=run_txt2img, |
| inputs=[ |
| txt_prompt, |
| txt_style, |
| txt_use_groq, |
| txt_steps, |
| txt_width, |
| txt_height, |
| txt_seed, |
| txt_upscale |
| ], |
| outputs=[ |
| txt_result, |
| txt_file_output, |
| txt_seed_output, |
| txt_final_prompt, |
| history_gallery |
| ] |
| ) |
|
|
| img_button.click( |
| fn=run_img2img, |
| inputs=[ |
| img_prompt, |
| img_style, |
| img_use_groq, |
| img_steps, |
| img_seed, |
| img_input, |
| img_strength, |
| img_upscale |
| ], |
| outputs=[ |
| img_result, |
| img_file_output, |
| img_seed_output, |
| img_final_prompt, |
| history_gallery |
| ] |
| ) |
|
|
| history_gallery.select( |
| fn=select_history, |
| outputs=[ |
| selected_history_image, |
| selected_history_index |
| ] |
| ) |
|
|
| reuse_button.click( |
| fn=reuse_history, |
| inputs=[ |
| selected_history_index |
| ], |
| outputs=[ |
| img_input, |
| tabs |
| ] |
| ) |
|
|
| clear_history_button.click( |
| fn=clear_history, |
| inputs=[], |
| outputs=[ |
| history_gallery, |
| selected_history_image, |
| selected_history_index |
| ] |
| ) |
|
|
| demo.load( |
| fn=lambda: history, |
| inputs=[], |
| outputs=[ |
| history_gallery |
| ] |
| ) |
|
|
| |
| |
| |
| demo.queue(max_size=10) |
|
|
| if __name__ == "__main__": |
| demo.launch(server_name="0.0.0.0", server_port=7860) |