Spaces:
Running
on
Zero
Running
on
Zero
Update app.py
Browse files
app.py
CHANGED
@@ -9,41 +9,186 @@ import torch
|
|
9 |
from diffusers import DiffusionPipeline
|
10 |
from PIL import Image
|
11 |
|
12 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
13 |
import gradio_client.utils
|
14 |
import types
|
15 |
|
16 |
-
# Patch 1: Fix the _json_schema_to_python_type function
|
17 |
original_json_schema = gradio_client.utils._json_schema_to_python_type
|
18 |
-
|
19 |
def patched_json_schema(schema, defs=None):
|
20 |
-
# Handle boolean values directly
|
21 |
if isinstance(schema, bool):
|
22 |
return "bool"
|
23 |
-
|
24 |
-
# Handle cases where 'additionalProperties' is a boolean
|
25 |
try:
|
26 |
if "additionalProperties" in schema and isinstance(schema["additionalProperties"], bool):
|
27 |
schema["additionalProperties"] = {"type": "any"}
|
28 |
except (TypeError, KeyError):
|
29 |
pass
|
30 |
-
|
31 |
-
# Call the original function
|
32 |
try:
|
33 |
return original_json_schema(schema, defs)
|
34 |
except Exception as e:
|
35 |
-
# Fallback to a safe value when the schema can't be parsed
|
36 |
return "any"
|
37 |
-
|
38 |
-
# Replace the original function with our patched version
|
39 |
gradio_client.utils._json_schema_to_python_type = patched_json_schema
|
40 |
|
41 |
-
|
42 |
-
SAVE_DIR = "saved_images" # Gradio will handle the persistence
|
43 |
if not os.path.exists(SAVE_DIR):
|
44 |
os.makedirs(SAVE_DIR, exist_ok=True)
|
45 |
|
46 |
-
# Safe settings for model loading
|
47 |
device = "cuda" if torch.cuda.is_available() else "cpu"
|
48 |
repo_id = "black-forest-labs/FLUX.1-dev"
|
49 |
adapter_id = "openfree/flux-chatgpt-ghibli-lora"
|
@@ -72,37 +217,27 @@ def load_model_with_retry(max_retries=5):
|
|
72 |
else:
|
73 |
raise Exception(f"Failed to load model after {max_retries} attempts: {e}")
|
74 |
|
75 |
-
# Load the model
|
76 |
pipeline = load_model_with_retry()
|
77 |
|
78 |
MAX_SEED = np.iinfo(np.int32).max
|
79 |
MAX_IMAGE_SIZE = 1024
|
80 |
|
81 |
def save_generated_image(image, prompt):
|
82 |
-
# Generate unique filename with timestamp
|
83 |
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
84 |
unique_id = str(uuid.uuid4())[:8]
|
85 |
filename = f"{timestamp}_{unique_id}.png"
|
86 |
filepath = os.path.join(SAVE_DIR, filename)
|
87 |
-
|
88 |
-
# Save the image
|
89 |
image.save(filepath)
|
90 |
-
|
91 |
-
# Save metadata
|
92 |
metadata_file = os.path.join(SAVE_DIR, "metadata.txt")
|
93 |
with open(metadata_file, "a", encoding="utf-8") as f:
|
94 |
f.write(f"{filename}|{prompt}|{timestamp}\n")
|
95 |
-
|
96 |
return filepath
|
97 |
|
98 |
def load_generated_images():
|
99 |
if not os.path.exists(SAVE_DIR):
|
100 |
return []
|
101 |
-
|
102 |
-
# Load all images from the directory
|
103 |
image_files = [os.path.join(SAVE_DIR, f) for f in os.listdir(SAVE_DIR)
|
104 |
if f.endswith(('.png', '.jpg', '.jpeg', '.webp'))]
|
105 |
-
# Sort by creation time (newest first)
|
106 |
image_files.sort(key=lambda x: os.path.getctime(x), reverse=True)
|
107 |
return image_files
|
108 |
|
@@ -121,8 +256,6 @@ def inference(
|
|
121 |
if randomize_seed:
|
122 |
seed = random.randint(0, MAX_SEED)
|
123 |
generator = torch.Generator(device=device).manual_seed(seed)
|
124 |
-
|
125 |
-
# Error handling for the inference process
|
126 |
try:
|
127 |
image = pipeline(
|
128 |
prompt=prompt,
|
@@ -133,33 +266,25 @@ def inference(
|
|
133 |
generator=generator,
|
134 |
joint_attention_kwargs={"scale": lora_scale},
|
135 |
).images[0]
|
136 |
-
|
137 |
-
# Save the generated image
|
138 |
filepath = save_generated_image(image, prompt)
|
139 |
-
|
140 |
-
# Return the image, seed, and updated gallery
|
141 |
return image, seed, load_generated_images()
|
142 |
except Exception as e:
|
143 |
-
# Log the error and return a simple error image
|
144 |
print(f"Error during inference: {e}")
|
145 |
error_img = Image.new('RGB', (width, height), color='red')
|
146 |
return error_img, seed, load_generated_images()
|
147 |
|
|
|
|
|
|
|
148 |
examples = [
|
149 |
"Ghibli style futuristic stormtrooper with glossy white armor and a sleek helmet, standing heroically on a lush alien planet, vibrant flowers blooming around, soft sunlight illuminating the scene, a gentle breeze rustling the leaves. The armor reflects the pink and purple hues of the alien sunset, creating an ethereal glow around the figure. [trigger]",
|
150 |
-
|
151 |
"Ghibli style young mechanic girl in a floating workshop, surrounded by hovering tools and glowing mechanical parts, her blue overalls covered in oil stains, tinkering with a semi-transparent robot companion. Magical sparks fly as she works, while floating islands with waterfalls drift past her open workshop window. [trigger]",
|
152 |
-
|
153 |
"Ghibli style ancient forest guardian robot, covered in moss and flowering vines, sitting peacefully in a crystal-clear lake. Its gentle eyes glow with soft blue light, while bioluminescent dragonflies dance around its weathered metal frame. Ancient tech symbols on its surface pulse with a gentle rhythm. [trigger]",
|
154 |
-
|
155 |
"Ghibli style sky whale transport ship, its metallic skin adorned with traditional Japanese patterns, gliding through cotton candy clouds at sunrise. Small floating gardens hang from its sides, where workers in futuristic kimonos tend to glowing plants. Rainbow auroras shimmer in the background. [trigger]",
|
156 |
-
|
157 |
"Ghibli style cyber-shrine maiden with flowing holographic robes, performing a ritual dance among floating lanterns and digital cherry blossoms. Her traditional headdress emits soft light patterns, while spirit-like AI constructs swirl around her in elegant patterns. The scene is set in a modern shrine with both ancient wood and sleek chrome elements. [trigger]",
|
158 |
-
|
159 |
"Ghibli style robot farmer tending to floating rice paddies in the sky, wearing a traditional straw hat with advanced sensors. Its gentle movements create ripples in the water as it plants glowing rice seedlings. Flying fish leap between the terraced fields, leaving trails of sparkles in their wake, while future Tokyo's spires gleam in the distance. [trigger]"
|
160 |
]
|
161 |
|
162 |
-
# Enhanced CSS for a more visually refined UI
|
163 |
css = """
|
164 |
:root {
|
165 |
--primary-color: #6a92cc;
|
@@ -172,17 +297,14 @@ css = """
|
|
172 |
--shadow: 0 4px 12px rgba(0,0,0,0.08);
|
173 |
--font-main: 'Poppins', -apple-system, BlinkMacSystemFont, sans-serif;
|
174 |
}
|
175 |
-
|
176 |
body {
|
177 |
background-color: var(--background-color);
|
178 |
font-family: var(--font-main);
|
179 |
}
|
180 |
-
|
181 |
.gradio-container {
|
182 |
margin: 0 auto;
|
183 |
max-width: 1200px !important;
|
184 |
}
|
185 |
-
|
186 |
.main-header {
|
187 |
text-align: center;
|
188 |
padding: 2rem 1rem 1rem;
|
@@ -192,32 +314,27 @@ body {
|
|
192 |
border-radius: var(--border-radius);
|
193 |
box-shadow: var(--shadow);
|
194 |
}
|
195 |
-
|
196 |
.main-header h1 {
|
197 |
font-size: 2.5rem;
|
198 |
margin-bottom: 0.5rem;
|
199 |
font-weight: 700;
|
200 |
text-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
201 |
}
|
202 |
-
|
203 |
.main-header p {
|
204 |
font-size: 1rem;
|
205 |
margin-bottom: 0.5rem;
|
206 |
opacity: 0.9;
|
207 |
}
|
208 |
-
|
209 |
.main-header a {
|
210 |
color: var(--secondary-color);
|
211 |
text-decoration: none;
|
212 |
font-weight: 600;
|
213 |
transition: all 0.2s ease;
|
214 |
}
|
215 |
-
|
216 |
.main-header a:hover {
|
217 |
text-decoration: underline;
|
218 |
opacity: 0.9;
|
219 |
}
|
220 |
-
|
221 |
.container {
|
222 |
background-color: var(--panel-background);
|
223 |
padding: 1.5rem;
|
@@ -225,7 +342,6 @@ body {
|
|
225 |
box-shadow: var(--shadow);
|
226 |
margin-bottom: 1.5rem;
|
227 |
}
|
228 |
-
|
229 |
button.primary {
|
230 |
background: var(--primary-color) !important;
|
231 |
border: none !important;
|
@@ -236,13 +352,11 @@ button.primary {
|
|
236 |
box-shadow: 0 2px 5px rgba(0,0,0,0.1) !important;
|
237 |
transition: all 0.2s ease !important;
|
238 |
}
|
239 |
-
|
240 |
button.primary:hover {
|
241 |
background: var(--primary-hover) !important;
|
242 |
transform: translateY(-2px) !important;
|
243 |
box-shadow: 0 4px 8px rgba(0,0,0,0.15) !important;
|
244 |
}
|
245 |
-
|
246 |
button.secondary {
|
247 |
background: white !important;
|
248 |
border: 1px solid #ddd !important;
|
@@ -253,64 +367,51 @@ button.secondary {
|
|
253 |
box-shadow: 0 2px 5px rgba(0,0,0,0.05) !important;
|
254 |
transition: all 0.2s ease !important;
|
255 |
}
|
256 |
-
|
257 |
button.secondary:hover {
|
258 |
background: #f5f5f5 !important;
|
259 |
transform: translateY(-2px) !important;
|
260 |
}
|
261 |
-
|
262 |
.gr-box {
|
263 |
border-radius: var(--border-radius) !important;
|
264 |
border: 1px solid #e0e0e0 !important;
|
265 |
}
|
266 |
-
|
267 |
.gr-panel {
|
268 |
border-radius: var(--border-radius) !important;
|
269 |
}
|
270 |
-
|
271 |
.gr-input {
|
272 |
border-radius: 8px !important;
|
273 |
border: 1px solid #ddd !important;
|
274 |
padding: 12px !important;
|
275 |
}
|
276 |
-
|
277 |
.gr-form {
|
278 |
border-radius: var(--border-radius) !important;
|
279 |
background-color: var(--panel-background) !important;
|
280 |
}
|
281 |
-
|
282 |
.gr-accordion {
|
283 |
border-radius: var(--border-radius) !important;
|
284 |
overflow: hidden !important;
|
285 |
}
|
286 |
-
|
287 |
.gr-button {
|
288 |
border-radius: 8px !important;
|
289 |
}
|
290 |
-
|
291 |
.gallery-item {
|
292 |
border-radius: var(--border-radius) !important;
|
293 |
transition: all 0.3s ease !important;
|
294 |
}
|
295 |
-
|
296 |
.gallery-item:hover {
|
297 |
transform: scale(1.02) !important;
|
298 |
box-shadow: 0 6px 15px rgba(0,0,0,0.1) !important;
|
299 |
}
|
300 |
-
|
301 |
.tabs {
|
302 |
border-radius: var(--border-radius) !important;
|
303 |
overflow: hidden !important;
|
304 |
}
|
305 |
-
|
306 |
footer {
|
307 |
display: none !important;
|
308 |
}
|
309 |
-
|
310 |
.settings-accordion legend span {
|
311 |
font-weight: 600 !important;
|
312 |
}
|
313 |
-
|
314 |
.example-prompt {
|
315 |
font-size: 0.9rem;
|
316 |
color: #555;
|
@@ -322,12 +423,9 @@ footer {
|
|
322 |
cursor: pointer;
|
323 |
transition: all 0.2s;
|
324 |
}
|
325 |
-
|
326 |
.example-prompt:hover {
|
327 |
background: #eef2f8;
|
328 |
}
|
329 |
-
|
330 |
-
/* Status indicators */
|
331 |
.status-generating {
|
332 |
color: #ffa200;
|
333 |
font-weight: 500;
|
@@ -335,7 +433,6 @@ footer {
|
|
335 |
align-items: center;
|
336 |
gap: 8px;
|
337 |
}
|
338 |
-
|
339 |
.status-generating::before {
|
340 |
content: "";
|
341 |
display: inline-block;
|
@@ -345,7 +442,6 @@ footer {
|
|
345 |
background-color: #ffa200;
|
346 |
animation: pulse 1.5s infinite;
|
347 |
}
|
348 |
-
|
349 |
.status-complete {
|
350 |
color: #00c853;
|
351 |
font-weight: 500;
|
@@ -353,7 +449,6 @@ footer {
|
|
353 |
align-items: center;
|
354 |
gap: 8px;
|
355 |
}
|
356 |
-
|
357 |
.status-complete::before {
|
358 |
content: "";
|
359 |
display: inline-block;
|
@@ -362,42 +457,27 @@ footer {
|
|
362 |
border-radius: 50%;
|
363 |
background-color: #00c853;
|
364 |
}
|
365 |
-
|
366 |
@keyframes pulse {
|
367 |
-
0% {
|
368 |
-
|
369 |
-
}
|
370 |
-
50% {
|
371 |
-
opacity: 1;
|
372 |
-
}
|
373 |
-
100% {
|
374 |
-
opacity: 0.6;
|
375 |
-
}
|
376 |
}
|
377 |
-
|
378 |
-
/* Custom accordions and tabs styling */
|
379 |
.gr-accordion-title {
|
380 |
font-weight: 600 !important;
|
381 |
color: var(--text-color) !important;
|
382 |
}
|
383 |
-
|
384 |
.tabs button {
|
385 |
font-weight: 500 !important;
|
386 |
padding: 10px 16px !important;
|
387 |
}
|
388 |
-
|
389 |
.tabs button.selected {
|
390 |
font-weight: 600 !important;
|
391 |
color: var(--primary-color) !important;
|
392 |
background: rgba(106, 146, 204, 0.1) !important;
|
393 |
}
|
394 |
-
|
395 |
-
/* Improve slider appearance */
|
396 |
.gr-slider-container {
|
397 |
padding: 10px 0 !important;
|
398 |
}
|
399 |
-
|
400 |
-
/* Enhanced Markdown content */
|
401 |
.gr-prose h3 {
|
402 |
font-weight: 600 !important;
|
403 |
color: var(--primary-color) !important;
|
@@ -405,10 +485,8 @@ footer {
|
|
405 |
}
|
406 |
"""
|
407 |
|
408 |
-
# Use a cleaner, more visual UI configuration
|
409 |
with gr.Blocks(css=css, analytics_enabled=False, theme="soft") as demo:
|
410 |
with gr.Column():
|
411 |
-
# Custom header with improved styling
|
412 |
gr.HTML('''
|
413 |
<div class="main-header">
|
414 |
<h1>โจ FLUX Ghibli LoRA Generator โจ</h1>
|
@@ -424,6 +502,12 @@ with gr.Blocks(css=css, analytics_enabled=False, theme="soft") as demo:
|
|
424 |
placeholder="Describe your Ghibli-style image here...",
|
425 |
lines=3
|
426 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
427 |
|
428 |
with gr.Row():
|
429 |
run_button = gr.Button("โจ Generate Image", elem_classes="primary")
|
@@ -439,7 +523,6 @@ with gr.Blocks(css=css, analytics_enabled=False, theme="soft") as demo:
|
|
439 |
value=42,
|
440 |
)
|
441 |
randomize_seed = gr.Checkbox(label="Randomize seed", value=True)
|
442 |
-
|
443 |
with gr.Row():
|
444 |
width = gr.Slider(
|
445 |
label="Width",
|
@@ -455,7 +538,6 @@ with gr.Blocks(css=css, analytics_enabled=False, theme="soft") as demo:
|
|
455 |
step=32,
|
456 |
value=768,
|
457 |
)
|
458 |
-
|
459 |
with gr.Row():
|
460 |
guidance_scale = gr.Slider(
|
461 |
label="Guidance scale",
|
@@ -464,7 +546,6 @@ with gr.Blocks(css=css, analytics_enabled=False, theme="soft") as demo:
|
|
464 |
step=0.1,
|
465 |
value=3.5,
|
466 |
)
|
467 |
-
|
468 |
with gr.Row():
|
469 |
num_inference_steps = gr.Slider(
|
470 |
label="Steps",
|
@@ -483,13 +564,11 @@ with gr.Blocks(css=css, analytics_enabled=False, theme="soft") as demo:
|
|
483 |
|
484 |
with gr.Group(elem_classes="container"):
|
485 |
gr.Markdown("### โจ Example Prompts")
|
486 |
-
# Create HTML for examples manually
|
487 |
examples_html = '\n'.join([f'<div class="example-prompt">{example}</div>' for example in examples])
|
488 |
example_container = gr.HTML(examples_html)
|
489 |
|
490 |
with gr.Column(scale=4):
|
491 |
with gr.Group(elem_classes="container"):
|
492 |
-
# Image result container with status indicator
|
493 |
with gr.Group():
|
494 |
generation_status = gr.HTML('<div class="status-complete">Ready to generate</div>')
|
495 |
result = gr.Image(label="Generated Image", elem_id="result-image")
|
@@ -509,7 +588,6 @@ with gr.Blocks(css=css, analytics_enabled=False, theme="soft") as demo:
|
|
509 |
elem_classes="gallery-item"
|
510 |
)
|
511 |
|
512 |
-
# Event handlers
|
513 |
def refresh_gallery():
|
514 |
return load_generated_images()
|
515 |
|
@@ -534,7 +612,7 @@ with gr.Blocks(css=css, analytics_enabled=False, theme="soft") as demo:
|
|
534 |
outputs=[prompt, result, seed_text, generation_status]
|
535 |
)
|
536 |
|
537 |
-
#
|
538 |
run_button.click(
|
539 |
fn=before_generate,
|
540 |
inputs=None,
|
@@ -556,6 +634,10 @@ with gr.Blocks(css=css, analytics_enabled=False, theme="soft") as demo:
|
|
556 |
fn=after_generate,
|
557 |
inputs=[result, seed_text, generated_gallery],
|
558 |
outputs=[result, seed_text, generated_gallery, generation_status],
|
|
|
|
|
|
|
|
|
559 |
)
|
560 |
|
561 |
prompt.submit(
|
@@ -579,36 +661,34 @@ with gr.Blocks(css=css, analytics_enabled=False, theme="soft") as demo:
|
|
579 |
fn=after_generate,
|
580 |
inputs=[result, seed_text, generated_gallery],
|
581 |
outputs=[result, seed_text, generated_gallery, generation_status],
|
|
|
|
|
|
|
|
|
582 |
)
|
583 |
|
584 |
-
# Custom JavaScript for handling example prompts
|
585 |
gr.HTML("""
|
586 |
<script>
|
587 |
document.addEventListener('DOMContentLoaded', function() {
|
588 |
-
// Add click handlers to example prompts
|
589 |
setTimeout(() => {
|
590 |
const examples = document.querySelectorAll('.example-prompt');
|
591 |
const promptInput = document.querySelector('textarea');
|
592 |
-
|
593 |
examples.forEach(example => {
|
594 |
example.addEventListener('click', function() {
|
595 |
promptInput.value = this.textContent.trim();
|
596 |
-
// Trigger input event to update Gradio's state
|
597 |
const event = new Event('input', { bubbles: true });
|
598 |
promptInput.dispatchEvent(event);
|
599 |
});
|
600 |
});
|
601 |
-
}, 1000);
|
602 |
});
|
603 |
</script>
|
604 |
""")
|
605 |
|
606 |
-
# Launch with fallback options
|
607 |
try:
|
608 |
demo.queue(concurrency_count=1, max_size=20)
|
609 |
demo.launch(debug=True, show_api=False)
|
610 |
except Exception as e:
|
611 |
print(f"Error during launch: {e}")
|
612 |
print("Trying alternative launch configuration...")
|
613 |
-
|
614 |
-
demo.launch(debug=True, show_api=False, share=False)
|
|
|
9 |
from diffusers import DiffusionPipeline
|
10 |
from PIL import Image
|
11 |
|
12 |
+
# -----------------------------
|
13 |
+
# Gemini API & Text Rendering ๊ด๋ จ ์ถ๊ฐ ๋ชจ๋
|
14 |
+
# -----------------------------
|
15 |
+
import re
|
16 |
+
import tempfile
|
17 |
+
import io
|
18 |
+
import logging
|
19 |
+
import base64
|
20 |
+
import string
|
21 |
+
import requests
|
22 |
+
from google import genai
|
23 |
+
from google.genai import types
|
24 |
+
|
25 |
+
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
|
26 |
+
|
27 |
+
def maybe_translate_to_english(text: str) -> str:
|
28 |
+
"""
|
29 |
+
ํ
์คํธ์ ํ๊ธ์ด ํฌํจ๋์ด ์์ผ๋ฉด ๊ฐ๋จํ ๊ท์น์ ๋ฐ๋ผ ์์ด๋ก ๋ณํ.
|
30 |
+
"""
|
31 |
+
if not text or not re.search("[๊ฐ-ํฃ]", text):
|
32 |
+
return text
|
33 |
+
try:
|
34 |
+
translations = {
|
35 |
+
"์๋
ํ์ธ์": "Hello",
|
36 |
+
"ํ์ํฉ๋๋ค": "Welcome",
|
37 |
+
"์๋ฆ๋ค์ด ๋น์ ": "Beautiful You",
|
38 |
+
"์๋
": "Hello",
|
39 |
+
"๊ณ ์์ด": "Cat",
|
40 |
+
"๋ฐฐ๋": "Banner",
|
41 |
+
"์ฌ๊ธ๋ผ์ค": "Sunglasses",
|
42 |
+
"์ฐฉ์ฉํ": "wearing",
|
43 |
+
"ํฐ์": "white"
|
44 |
+
}
|
45 |
+
for kr, en in translations.items():
|
46 |
+
if kr in text:
|
47 |
+
text = text.replace(kr, en)
|
48 |
+
print(f"[TRANSLATE] Translated Korean text: '{text}'")
|
49 |
+
return text
|
50 |
+
except Exception as e:
|
51 |
+
print(f"[WARNING] Translation failed: {e}")
|
52 |
+
return text
|
53 |
+
|
54 |
+
def save_binary_file(file_name, data):
|
55 |
+
with open(file_name, "wb") as f:
|
56 |
+
f.write(data)
|
57 |
+
|
58 |
+
def generate_by_google_genai(text, file_name, model="gemini-2.0-flash-exp"):
|
59 |
+
"""
|
60 |
+
Gemini API๋ฅผ ํธ์ถํ์ฌ ํ
์คํธ ๊ธฐ๋ฐ ์ด๋ฏธ์ง ํธ์ง/์์ฑ์ ์ํ.
|
61 |
+
"""
|
62 |
+
api_key = os.getenv("GAPI_TOKEN", None)
|
63 |
+
if not api_key:
|
64 |
+
raise ValueError("GAPI_TOKEN is missing. Please set an API key.")
|
65 |
+
client = genai.Client(api_key=api_key)
|
66 |
+
files = [client.files.upload(file=file_name)]
|
67 |
+
contents = [
|
68 |
+
types.Content(
|
69 |
+
role="user",
|
70 |
+
parts=[
|
71 |
+
types.Part.from_uri(
|
72 |
+
file_uri=files[0].uri,
|
73 |
+
mime_type=files[0].mime_type,
|
74 |
+
),
|
75 |
+
types.Part.from_text(text=text),
|
76 |
+
],
|
77 |
+
),
|
78 |
+
]
|
79 |
+
generate_content_config = types.GenerateContentConfig(
|
80 |
+
temperature=1,
|
81 |
+
top_p=0.95,
|
82 |
+
top_k=40,
|
83 |
+
max_output_tokens=8192,
|
84 |
+
response_modalities=["image", "text"],
|
85 |
+
response_mime_type="text/plain",
|
86 |
+
)
|
87 |
+
text_response = ""
|
88 |
+
image_path = None
|
89 |
+
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp:
|
90 |
+
temp_path = tmp.name
|
91 |
+
for chunk in client.models.generate_content_stream(
|
92 |
+
model=model,
|
93 |
+
contents=contents,
|
94 |
+
config=generate_content_config,
|
95 |
+
):
|
96 |
+
if not chunk.candidates or not chunk.candidates[0].content or not chunk.candidates[0].content.parts:
|
97 |
+
continue
|
98 |
+
candidate = chunk.candidates[0].content.parts[0]
|
99 |
+
if candidate.inline_data:
|
100 |
+
save_binary_file(temp_path, candidate.inline_data.data)
|
101 |
+
print(f"File of mime type {candidate.inline_data.mime_type} saved to: {temp_path}")
|
102 |
+
image_path = temp_path
|
103 |
+
break
|
104 |
+
else:
|
105 |
+
text_response += chunk.text + "\n"
|
106 |
+
del files
|
107 |
+
return image_path, text_response
|
108 |
+
|
109 |
+
def change_text_in_image_two_times(original_image, instruction):
|
110 |
+
"""
|
111 |
+
Gemini API๋ฅผ ๋ ๋ฒ ํธ์ถํ์ฌ ํ
์คํธ ์์ ๋ ์ด๋ฏธ์ง 2๊ฐ(๋ฒ์ A, B)๋ฅผ ๋ฐํ.
|
112 |
+
"""
|
113 |
+
if original_image is None:
|
114 |
+
raise gr.Error("์ฒ๋ฆฌํ ์ด๋ฏธ์ง๊ฐ ์์ต๋๋ค. ๋จผ์ ์ด๋ฏธ์ง๋ฅผ ์์ฑํด์ฃผ์ธ์.")
|
115 |
+
results = []
|
116 |
+
for version_tag in ["(A)", "(B)"]:
|
117 |
+
mod_instruction = f"{instruction} {version_tag}"
|
118 |
+
try:
|
119 |
+
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp:
|
120 |
+
original_path = tmp.name
|
121 |
+
if isinstance(original_image, Image.Image):
|
122 |
+
original_image.save(original_path, format="PNG")
|
123 |
+
print(f"[DEBUG] Saved image to temporary file: {original_path}")
|
124 |
+
else:
|
125 |
+
raise gr.Error(f"์์๋ PIL Image๊ฐ ์๋ {type(original_image)} ํ์
์ด ์ ๊ณต๋์์ต๋๋ค.")
|
126 |
+
print(f"[DEBUG] Google Gemini API์ ๋ณด๋ด๋ ์ง์์ฌํญ: {mod_instruction}")
|
127 |
+
image_path, text_response = generate_by_google_genai(
|
128 |
+
text=mod_instruction,
|
129 |
+
file_name=original_path
|
130 |
+
)
|
131 |
+
if image_path:
|
132 |
+
try:
|
133 |
+
with open(image_path, "rb") as f:
|
134 |
+
image_data = f.read()
|
135 |
+
new_img = Image.open(io.BytesIO(image_data))
|
136 |
+
results.append(new_img)
|
137 |
+
except Exception as img_err:
|
138 |
+
print(f"[ERROR] Failed to process Gemini image: {img_err}")
|
139 |
+
results.append(original_image)
|
140 |
+
else:
|
141 |
+
print(f"[WARNING] ์ด๋ฏธ์ง๊ฐ ๋ฐํ๋์ง ์์์ต๋๋ค. ํ
์คํธ ์๋ต: {text_response}")
|
142 |
+
results.append(original_image)
|
143 |
+
except Exception as e:
|
144 |
+
logging.exception(f"Text modification error: {e}")
|
145 |
+
results.append(original_image)
|
146 |
+
return results
|
147 |
+
|
148 |
+
def gemini_text_rendering(image, rendering_text):
|
149 |
+
"""
|
150 |
+
์ฃผ์ด์ง ์ด๋ฏธ์ง์ ๋ํด Gemini API๋ฅผ ์ฌ์ฉํด ํ
์คํธ ๋ ๋๋ง์ ์ ์ฉ.
|
151 |
+
"""
|
152 |
+
rendering_text_en = maybe_translate_to_english(rendering_text)
|
153 |
+
instruction = f"Render the following text on the image in a clear, visually appealing manner: {rendering_text_en}."
|
154 |
+
rendered_images = change_text_in_image_two_times(image, instruction)
|
155 |
+
if rendered_images and len(rendered_images) > 0:
|
156 |
+
return rendered_images[0]
|
157 |
+
return image
|
158 |
+
|
159 |
+
def apply_text_rendering(image, rendering_text):
|
160 |
+
"""
|
161 |
+
์
๋ ฅ๋ ํ
์คํธ๊ฐ ์์ผ๋ฉด Gemini API๋ฅผ ํตํด ์ด๋ฏธ์ง์ ํ
์คํธ ๋ ๋๋ง์ ์ ์ฉํ๊ณ , ์์ผ๋ฉด ์๋ณธ ์ด๋ฏธ์ง๋ฅผ ๊ทธ๋๋ก ๋ฐํ.
|
162 |
+
"""
|
163 |
+
if rendering_text and rendering_text.strip():
|
164 |
+
return gemini_text_rendering(image, rendering_text)
|
165 |
+
return image
|
166 |
+
|
167 |
+
# -----------------------------
|
168 |
+
# ๊ธฐ์กด Diffusion Pipeline ๊ด๋ จ ์ฝ๋
|
169 |
+
# -----------------------------
|
170 |
import gradio_client.utils
|
171 |
import types
|
172 |
|
|
|
173 |
original_json_schema = gradio_client.utils._json_schema_to_python_type
|
|
|
174 |
def patched_json_schema(schema, defs=None):
|
|
|
175 |
if isinstance(schema, bool):
|
176 |
return "bool"
|
|
|
|
|
177 |
try:
|
178 |
if "additionalProperties" in schema and isinstance(schema["additionalProperties"], bool):
|
179 |
schema["additionalProperties"] = {"type": "any"}
|
180 |
except (TypeError, KeyError):
|
181 |
pass
|
|
|
|
|
182 |
try:
|
183 |
return original_json_schema(schema, defs)
|
184 |
except Exception as e:
|
|
|
185 |
return "any"
|
|
|
|
|
186 |
gradio_client.utils._json_schema_to_python_type = patched_json_schema
|
187 |
|
188 |
+
SAVE_DIR = "saved_images"
|
|
|
189 |
if not os.path.exists(SAVE_DIR):
|
190 |
os.makedirs(SAVE_DIR, exist_ok=True)
|
191 |
|
|
|
192 |
device = "cuda" if torch.cuda.is_available() else "cpu"
|
193 |
repo_id = "black-forest-labs/FLUX.1-dev"
|
194 |
adapter_id = "openfree/flux-chatgpt-ghibli-lora"
|
|
|
217 |
else:
|
218 |
raise Exception(f"Failed to load model after {max_retries} attempts: {e}")
|
219 |
|
|
|
220 |
pipeline = load_model_with_retry()
|
221 |
|
222 |
MAX_SEED = np.iinfo(np.int32).max
|
223 |
MAX_IMAGE_SIZE = 1024
|
224 |
|
225 |
def save_generated_image(image, prompt):
|
|
|
226 |
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
227 |
unique_id = str(uuid.uuid4())[:8]
|
228 |
filename = f"{timestamp}_{unique_id}.png"
|
229 |
filepath = os.path.join(SAVE_DIR, filename)
|
|
|
|
|
230 |
image.save(filepath)
|
|
|
|
|
231 |
metadata_file = os.path.join(SAVE_DIR, "metadata.txt")
|
232 |
with open(metadata_file, "a", encoding="utf-8") as f:
|
233 |
f.write(f"{filename}|{prompt}|{timestamp}\n")
|
|
|
234 |
return filepath
|
235 |
|
236 |
def load_generated_images():
|
237 |
if not os.path.exists(SAVE_DIR):
|
238 |
return []
|
|
|
|
|
239 |
image_files = [os.path.join(SAVE_DIR, f) for f in os.listdir(SAVE_DIR)
|
240 |
if f.endswith(('.png', '.jpg', '.jpeg', '.webp'))]
|
|
|
241 |
image_files.sort(key=lambda x: os.path.getctime(x), reverse=True)
|
242 |
return image_files
|
243 |
|
|
|
256 |
if randomize_seed:
|
257 |
seed = random.randint(0, MAX_SEED)
|
258 |
generator = torch.Generator(device=device).manual_seed(seed)
|
|
|
|
|
259 |
try:
|
260 |
image = pipeline(
|
261 |
prompt=prompt,
|
|
|
266 |
generator=generator,
|
267 |
joint_attention_kwargs={"scale": lora_scale},
|
268 |
).images[0]
|
|
|
|
|
269 |
filepath = save_generated_image(image, prompt)
|
|
|
|
|
270 |
return image, seed, load_generated_images()
|
271 |
except Exception as e:
|
|
|
272 |
print(f"Error during inference: {e}")
|
273 |
error_img = Image.new('RGB', (width, height), color='red')
|
274 |
return error_img, seed, load_generated_images()
|
275 |
|
276 |
+
# -----------------------------
|
277 |
+
# Gradio UI (์
๋ ฅ ํ๋กฌํํธ ์๋์ Text Rendering ์
๋ ฅ๋ ์ถ๊ฐ)
|
278 |
+
# -----------------------------
|
279 |
examples = [
|
280 |
"Ghibli style futuristic stormtrooper with glossy white armor and a sleek helmet, standing heroically on a lush alien planet, vibrant flowers blooming around, soft sunlight illuminating the scene, a gentle breeze rustling the leaves. The armor reflects the pink and purple hues of the alien sunset, creating an ethereal glow around the figure. [trigger]",
|
|
|
281 |
"Ghibli style young mechanic girl in a floating workshop, surrounded by hovering tools and glowing mechanical parts, her blue overalls covered in oil stains, tinkering with a semi-transparent robot companion. Magical sparks fly as she works, while floating islands with waterfalls drift past her open workshop window. [trigger]",
|
|
|
282 |
"Ghibli style ancient forest guardian robot, covered in moss and flowering vines, sitting peacefully in a crystal-clear lake. Its gentle eyes glow with soft blue light, while bioluminescent dragonflies dance around its weathered metal frame. Ancient tech symbols on its surface pulse with a gentle rhythm. [trigger]",
|
|
|
283 |
"Ghibli style sky whale transport ship, its metallic skin adorned with traditional Japanese patterns, gliding through cotton candy clouds at sunrise. Small floating gardens hang from its sides, where workers in futuristic kimonos tend to glowing plants. Rainbow auroras shimmer in the background. [trigger]",
|
|
|
284 |
"Ghibli style cyber-shrine maiden with flowing holographic robes, performing a ritual dance among floating lanterns and digital cherry blossoms. Her traditional headdress emits soft light patterns, while spirit-like AI constructs swirl around her in elegant patterns. The scene is set in a modern shrine with both ancient wood and sleek chrome elements. [trigger]",
|
|
|
285 |
"Ghibli style robot farmer tending to floating rice paddies in the sky, wearing a traditional straw hat with advanced sensors. Its gentle movements create ripples in the water as it plants glowing rice seedlings. Flying fish leap between the terraced fields, leaving trails of sparkles in their wake, while future Tokyo's spires gleam in the distance. [trigger]"
|
286 |
]
|
287 |
|
|
|
288 |
css = """
|
289 |
:root {
|
290 |
--primary-color: #6a92cc;
|
|
|
297 |
--shadow: 0 4px 12px rgba(0,0,0,0.08);
|
298 |
--font-main: 'Poppins', -apple-system, BlinkMacSystemFont, sans-serif;
|
299 |
}
|
|
|
300 |
body {
|
301 |
background-color: var(--background-color);
|
302 |
font-family: var(--font-main);
|
303 |
}
|
|
|
304 |
.gradio-container {
|
305 |
margin: 0 auto;
|
306 |
max-width: 1200px !important;
|
307 |
}
|
|
|
308 |
.main-header {
|
309 |
text-align: center;
|
310 |
padding: 2rem 1rem 1rem;
|
|
|
314 |
border-radius: var(--border-radius);
|
315 |
box-shadow: var(--shadow);
|
316 |
}
|
|
|
317 |
.main-header h1 {
|
318 |
font-size: 2.5rem;
|
319 |
margin-bottom: 0.5rem;
|
320 |
font-weight: 700;
|
321 |
text-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
322 |
}
|
|
|
323 |
.main-header p {
|
324 |
font-size: 1rem;
|
325 |
margin-bottom: 0.5rem;
|
326 |
opacity: 0.9;
|
327 |
}
|
|
|
328 |
.main-header a {
|
329 |
color: var(--secondary-color);
|
330 |
text-decoration: none;
|
331 |
font-weight: 600;
|
332 |
transition: all 0.2s ease;
|
333 |
}
|
|
|
334 |
.main-header a:hover {
|
335 |
text-decoration: underline;
|
336 |
opacity: 0.9;
|
337 |
}
|
|
|
338 |
.container {
|
339 |
background-color: var(--panel-background);
|
340 |
padding: 1.5rem;
|
|
|
342 |
box-shadow: var(--shadow);
|
343 |
margin-bottom: 1.5rem;
|
344 |
}
|
|
|
345 |
button.primary {
|
346 |
background: var(--primary-color) !important;
|
347 |
border: none !important;
|
|
|
352 |
box-shadow: 0 2px 5px rgba(0,0,0,0.1) !important;
|
353 |
transition: all 0.2s ease !important;
|
354 |
}
|
|
|
355 |
button.primary:hover {
|
356 |
background: var(--primary-hover) !important;
|
357 |
transform: translateY(-2px) !important;
|
358 |
box-shadow: 0 4px 8px rgba(0,0,0,0.15) !important;
|
359 |
}
|
|
|
360 |
button.secondary {
|
361 |
background: white !important;
|
362 |
border: 1px solid #ddd !important;
|
|
|
367 |
box-shadow: 0 2px 5px rgba(0,0,0,0.05) !important;
|
368 |
transition: all 0.2s ease !important;
|
369 |
}
|
|
|
370 |
button.secondary:hover {
|
371 |
background: #f5f5f5 !important;
|
372 |
transform: translateY(-2px) !important;
|
373 |
}
|
|
|
374 |
.gr-box {
|
375 |
border-radius: var(--border-radius) !important;
|
376 |
border: 1px solid #e0e0e0 !important;
|
377 |
}
|
|
|
378 |
.gr-panel {
|
379 |
border-radius: var(--border-radius) !important;
|
380 |
}
|
|
|
381 |
.gr-input {
|
382 |
border-radius: 8px !important;
|
383 |
border: 1px solid #ddd !important;
|
384 |
padding: 12px !important;
|
385 |
}
|
|
|
386 |
.gr-form {
|
387 |
border-radius: var(--border-radius) !important;
|
388 |
background-color: var(--panel-background) !important;
|
389 |
}
|
|
|
390 |
.gr-accordion {
|
391 |
border-radius: var(--border-radius) !important;
|
392 |
overflow: hidden !important;
|
393 |
}
|
|
|
394 |
.gr-button {
|
395 |
border-radius: 8px !important;
|
396 |
}
|
|
|
397 |
.gallery-item {
|
398 |
border-radius: var(--border-radius) !important;
|
399 |
transition: all 0.3s ease !important;
|
400 |
}
|
|
|
401 |
.gallery-item:hover {
|
402 |
transform: scale(1.02) !important;
|
403 |
box-shadow: 0 6px 15px rgba(0,0,0,0.1) !important;
|
404 |
}
|
|
|
405 |
.tabs {
|
406 |
border-radius: var(--border-radius) !important;
|
407 |
overflow: hidden !important;
|
408 |
}
|
|
|
409 |
footer {
|
410 |
display: none !important;
|
411 |
}
|
|
|
412 |
.settings-accordion legend span {
|
413 |
font-weight: 600 !important;
|
414 |
}
|
|
|
415 |
.example-prompt {
|
416 |
font-size: 0.9rem;
|
417 |
color: #555;
|
|
|
423 |
cursor: pointer;
|
424 |
transition: all 0.2s;
|
425 |
}
|
|
|
426 |
.example-prompt:hover {
|
427 |
background: #eef2f8;
|
428 |
}
|
|
|
|
|
429 |
.status-generating {
|
430 |
color: #ffa200;
|
431 |
font-weight: 500;
|
|
|
433 |
align-items: center;
|
434 |
gap: 8px;
|
435 |
}
|
|
|
436 |
.status-generating::before {
|
437 |
content: "";
|
438 |
display: inline-block;
|
|
|
442 |
background-color: #ffa200;
|
443 |
animation: pulse 1.5s infinite;
|
444 |
}
|
|
|
445 |
.status-complete {
|
446 |
color: #00c853;
|
447 |
font-weight: 500;
|
|
|
449 |
align-items: center;
|
450 |
gap: 8px;
|
451 |
}
|
|
|
452 |
.status-complete::before {
|
453 |
content: "";
|
454 |
display: inline-block;
|
|
|
457 |
border-radius: 50%;
|
458 |
background-color: #00c853;
|
459 |
}
|
|
|
460 |
@keyframes pulse {
|
461 |
+
0% { opacity: 0.6; }
|
462 |
+
50% { opacity: 1; }
|
463 |
+
100% { opacity: 0.6; }
|
|
|
|
|
|
|
|
|
|
|
|
|
464 |
}
|
|
|
|
|
465 |
.gr-accordion-title {
|
466 |
font-weight: 600 !important;
|
467 |
color: var(--text-color) !important;
|
468 |
}
|
|
|
469 |
.tabs button {
|
470 |
font-weight: 500 !important;
|
471 |
padding: 10px 16px !important;
|
472 |
}
|
|
|
473 |
.tabs button.selected {
|
474 |
font-weight: 600 !important;
|
475 |
color: var(--primary-color) !important;
|
476 |
background: rgba(106, 146, 204, 0.1) !important;
|
477 |
}
|
|
|
|
|
478 |
.gr-slider-container {
|
479 |
padding: 10px 0 !important;
|
480 |
}
|
|
|
|
|
481 |
.gr-prose h3 {
|
482 |
font-weight: 600 !important;
|
483 |
color: var(--primary-color) !important;
|
|
|
485 |
}
|
486 |
"""
|
487 |
|
|
|
488 |
with gr.Blocks(css=css, analytics_enabled=False, theme="soft") as demo:
|
489 |
with gr.Column():
|
|
|
490 |
gr.HTML('''
|
491 |
<div class="main-header">
|
492 |
<h1>โจ FLUX Ghibli LoRA Generator โจ</h1>
|
|
|
502 |
placeholder="Describe your Ghibli-style image here...",
|
503 |
lines=3
|
504 |
)
|
505 |
+
# โ
์๋กญ๊ฒ ์ถ๊ฐ๋ Text Rendering ์
๋ ฅ๋
|
506 |
+
text_rendering = gr.Textbox(
|
507 |
+
label="Text Rendering (Multilingual: English, Korean...)",
|
508 |
+
placeholder="Man saying '์๋
' in 'speech bubble'",
|
509 |
+
lines=1
|
510 |
+
)
|
511 |
|
512 |
with gr.Row():
|
513 |
run_button = gr.Button("โจ Generate Image", elem_classes="primary")
|
|
|
523 |
value=42,
|
524 |
)
|
525 |
randomize_seed = gr.Checkbox(label="Randomize seed", value=True)
|
|
|
526 |
with gr.Row():
|
527 |
width = gr.Slider(
|
528 |
label="Width",
|
|
|
538 |
step=32,
|
539 |
value=768,
|
540 |
)
|
|
|
541 |
with gr.Row():
|
542 |
guidance_scale = gr.Slider(
|
543 |
label="Guidance scale",
|
|
|
546 |
step=0.1,
|
547 |
value=3.5,
|
548 |
)
|
|
|
549 |
with gr.Row():
|
550 |
num_inference_steps = gr.Slider(
|
551 |
label="Steps",
|
|
|
564 |
|
565 |
with gr.Group(elem_classes="container"):
|
566 |
gr.Markdown("### โจ Example Prompts")
|
|
|
567 |
examples_html = '\n'.join([f'<div class="example-prompt">{example}</div>' for example in examples])
|
568 |
example_container = gr.HTML(examples_html)
|
569 |
|
570 |
with gr.Column(scale=4):
|
571 |
with gr.Group(elem_classes="container"):
|
|
|
572 |
with gr.Group():
|
573 |
generation_status = gr.HTML('<div class="status-complete">Ready to generate</div>')
|
574 |
result = gr.Image(label="Generated Image", elem_id="result-image")
|
|
|
588 |
elem_classes="gallery-item"
|
589 |
)
|
590 |
|
|
|
591 |
def refresh_gallery():
|
592 |
return load_generated_images()
|
593 |
|
|
|
612 |
outputs=[prompt, result, seed_text, generation_status]
|
613 |
)
|
614 |
|
615 |
+
# ์ฒด์ธ์ ๋ง์ง๋ง์ ํ
์คํธ ๋ ๋๋ง ์ ์ฉ (text_rendering ์
๋ ฅ๊ฐ์ด ์์ผ๋ฉด)
|
616 |
run_button.click(
|
617 |
fn=before_generate,
|
618 |
inputs=None,
|
|
|
634 |
fn=after_generate,
|
635 |
inputs=[result, seed_text, generated_gallery],
|
636 |
outputs=[result, seed_text, generated_gallery, generation_status],
|
637 |
+
).then(
|
638 |
+
fn=apply_text_rendering,
|
639 |
+
inputs=[result, text_rendering],
|
640 |
+
outputs=result,
|
641 |
)
|
642 |
|
643 |
prompt.submit(
|
|
|
661 |
fn=after_generate,
|
662 |
inputs=[result, seed_text, generated_gallery],
|
663 |
outputs=[result, seed_text, generated_gallery, generation_status],
|
664 |
+
).then(
|
665 |
+
fn=apply_text_rendering,
|
666 |
+
inputs=[result, text_rendering],
|
667 |
+
outputs=result,
|
668 |
)
|
669 |
|
|
|
670 |
gr.HTML("""
|
671 |
<script>
|
672 |
document.addEventListener('DOMContentLoaded', function() {
|
|
|
673 |
setTimeout(() => {
|
674 |
const examples = document.querySelectorAll('.example-prompt');
|
675 |
const promptInput = document.querySelector('textarea');
|
|
|
676 |
examples.forEach(example => {
|
677 |
example.addEventListener('click', function() {
|
678 |
promptInput.value = this.textContent.trim();
|
|
|
679 |
const event = new Event('input', { bubbles: true });
|
680 |
promptInput.dispatchEvent(event);
|
681 |
});
|
682 |
});
|
683 |
+
}, 1000);
|
684 |
});
|
685 |
</script>
|
686 |
""")
|
687 |
|
|
|
688 |
try:
|
689 |
demo.queue(concurrency_count=1, max_size=20)
|
690 |
demo.launch(debug=True, show_api=False)
|
691 |
except Exception as e:
|
692 |
print(f"Error during launch: {e}")
|
693 |
print("Trying alternative launch configuration...")
|
694 |
+
demo.launch(debug=True, show_api=False, share=False)
|
|