openfree commited on
Commit
5dea862
·
verified ·
1 Parent(s): ead9c45

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +209 -891
app.py CHANGED
@@ -1,48 +1,32 @@
 
 
 
 
1
  import tempfile
2
  import time
3
- from collections.abc import Sequence
4
- from typing import Any, cast
5
- import os
6
  from huggingface_hub import login, hf_hub_download
7
 
8
  import gradio as gr
9
  import numpy as np
10
- import pillow_heif
11
- import spaces
12
  import torch
13
- from gradio_image_annotation import image_annotator
14
- from gradio_imageslider import ImageSlider
15
- from PIL import Image
16
- from pymatting.foreground.estimate_foreground_ml import estimate_foreground_ml
17
- from refiners.fluxion.utils import no_grad
18
- from refiners.solutions import BoxSegmenter
19
- from transformers import GroundingDinoForObjectDetection, GroundingDinoProcessor
20
- from diffusers import FluxPipeline
21
- from transformers import pipeline, AutoTokenizer, AutoModelForSeq2SeqLM
22
- import gc
23
-
24
  from PIL import Image, ImageDraw, ImageFont
25
- from PIL import Image
26
- from gradio_client import Client, handle_file
27
- import uuid
28
-
29
- import random
30
- from datetime import datetime
31
 
 
32
  def clear_memory():
33
- """메모리 정리 함수"""
34
  gc.collect()
35
  try:
36
  if torch.cuda.is_available():
37
- with torch.cuda.device(0): # 명시적으로 device 0 사용
38
  torch.cuda.empty_cache()
39
  except:
40
  pass
41
 
42
  # GPU 설정
43
- device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") # 명시적으로 cuda:0 지정
44
 
45
- # GPU 설정을 try-except로 감싸기
46
  if torch.cuda.is_available():
47
  try:
48
  with torch.cuda.device(0):
@@ -52,29 +36,6 @@ if torch.cuda.is_available():
52
  except:
53
  print("Warning: Could not configure CUDA settings")
54
 
55
- # 번역 모델 초기화
56
- model_name = "Helsinki-NLP/opus-mt-ko-en"
57
- tokenizer = AutoTokenizer.from_pretrained(model_name)
58
- model = AutoModelForSeq2SeqLM.from_pretrained(model_name).to('cpu')
59
- translator = pipeline("translation", model=model, tokenizer=tokenizer, device=-1)
60
-
61
- def translate_to_english(text: str) -> str:
62
- """한글 텍스트를 영어로 번역"""
63
- try:
64
- if any(ord('가') <= ord(char) <= ord('힣') for char in text):
65
- translated = translator(text, max_length=128)[0]['translation_text']
66
- print(f"Translated '{text}' to '{translated}'")
67
- return translated
68
- return text
69
- except Exception as e:
70
- print(f"Translation error: {str(e)}")
71
- return text
72
-
73
- BoundingBox = tuple[int, int, int, int]
74
-
75
- pillow_heif.register_heif_opener()
76
- pillow_heif.register_avif_opener()
77
-
78
  # HF 토큰 설정
79
  HF_TOKEN = os.getenv("HF_TOKEN")
80
  if HF_TOKEN is None:
@@ -85,17 +46,6 @@ try:
85
  except Exception as e:
86
  raise ValueError(f"Failed to login to Hugging Face: {str(e)}")
87
 
88
- # 모델 초기화
89
- segmenter = BoxSegmenter(device="cpu")
90
- segmenter.device = device
91
- segmenter.model = segmenter.model.to(device=segmenter.device)
92
-
93
- gd_model_path = "IDEA-Research/grounding-dino-base"
94
- gd_processor = GroundingDinoProcessor.from_pretrained(gd_model_path)
95
- gd_model = GroundingDinoForObjectDetection.from_pretrained(gd_model_path, torch_dtype=torch.float32)
96
- gd_model = gd_model.to(device=device)
97
- assert isinstance(gd_model, GroundingDinoForObjectDetection)
98
-
99
  # FLUX 파이프라인 초기화
100
  pipe = FluxPipeline.from_pretrained(
101
  "black-forest-labs/FLUX.1-dev",
@@ -104,468 +54,84 @@ pipe = FluxPipeline.from_pretrained(
104
  )
105
  pipe.enable_attention_slicing(slice_size="auto")
106
 
107
- # LoRA 가중치 로드
108
- pipe.load_lora_weights(
109
- hf_hub_download(
110
- "ByteDance/Hyper-SD",
111
- "Hyper-FLUX.1-dev-8steps-lora.safetensors",
112
  use_auth_token=HF_TOKEN
113
  )
114
- )
115
- pipe.fuse_lora(lora_scale=0.125)
116
-
117
- # GPU 설정을 try-except로 감싸기
118
- try:
119
- if torch.cuda.is_available():
120
- pipe = pipe.to("cuda:0") # 명시적으로 cuda:0 지정
121
  except Exception as e:
122
- print(f"Warning: Could not move pipeline to CUDA: {str(e)}")
123
-
124
- client = Client("NabeelShar/BiRefNet_for_text_writing")
125
-
126
- class timer:
127
- def __init__(self, method_name="timed process"):
128
- self.method = method_name
129
- def __enter__(self):
130
- self.start = time.time()
131
- print(f"{self.method} starts")
132
- def __exit__(self, exc_type, exc_val, exc_tb):
133
- end = time.time()
134
- print(f"{self.method} took {str(round(end - self.start, 2))}s")
135
-
136
- def bbox_union(bboxes: Sequence[list[int]]) -> BoundingBox | None:
137
- if not bboxes:
138
- return None
139
- for bbox in bboxes:
140
- assert len(bbox) == 4
141
- assert all(isinstance(x, int) for x in bbox)
142
- return (
143
- min(bbox[0] for bbox in bboxes),
144
- min(bbox[1] for bbox in bboxes),
145
- max(bbox[2] for bbox in bboxes),
146
- max(bbox[3] for bbox in bboxes),
147
- )
148
 
149
- def corners_to_pixels_format(bboxes: torch.Tensor, width: int, height: int) -> torch.Tensor:
150
- x1, y1, x2, y2 = bboxes.round().to(torch.int32).unbind(-1)
151
- return torch.stack((x1.clamp_(0, width), y1.clamp_(0, height), x2.clamp_(0, width), y2.clamp_(0, height)), dim=-1)
152
-
153
- def gd_detect(img: Image.Image, prompt: str) -> BoundingBox | None:
154
- inputs = gd_processor(images=img, text=f"{prompt}.", return_tensors="pt").to(device=device)
155
- with no_grad():
156
- outputs = gd_model(**inputs)
157
- width, height = img.size
158
- results: dict[str, Any] = gd_processor.post_process_grounded_object_detection(
159
- outputs,
160
- inputs["input_ids"],
161
- target_sizes=[(height, width)],
162
- )[0]
163
- assert "boxes" in results and isinstance(results["boxes"], torch.Tensor)
164
- bboxes = corners_to_pixels_format(results["boxes"].cpu(), width, height)
165
- return bbox_union(bboxes.numpy().tolist())
166
-
167
- def apply_mask(img: Image.Image, mask_img: Image.Image, defringe: bool = True) -> Image.Image:
168
- assert img.size == mask_img.size
169
- img = img.convert("RGB")
170
- mask_img = mask_img.convert("L")
171
- if defringe:
172
- rgb, alpha = np.asarray(img) / 255.0, np.asarray(mask_img) / 255.0
173
- foreground = cast(np.ndarray[Any, np.dtype[np.uint8]], estimate_foreground_ml(rgb, alpha))
174
- img = Image.fromarray((foreground * 255).astype("uint8"))
175
- result = Image.new("RGBA", img.size)
176
- result.paste(img, (0, 0), mask_img)
177
- return result
178
-
179
-
180
- def adjust_size_to_multiple_of_8(width: int, height: int) -> tuple[int, int]:
181
- """이미지 크기를 8의 배수로 조정하는 함수"""
182
- new_width = ((width + 7) // 8) * 8
183
- new_height = ((height + 7) // 8) * 8
184
- return new_width, new_height
185
-
186
- def calculate_dimensions(aspect_ratio: str, base_size: int = 512) -> tuple[int, int]:
187
- """선택된 비율에 따라 이미지 크기 계산"""
188
- if aspect_ratio == "1:1":
189
- return base_size, base_size
190
- elif aspect_ratio == "16:9":
191
- return base_size * 16 // 9, base_size
192
- elif aspect_ratio == "9:16":
193
- return base_size, base_size * 16 // 9
194
- elif aspect_ratio == "4:3":
195
- return base_size * 4 // 3, base_size
196
- return base_size, base_size
197
-
198
- @spaces.GPU(duration=20) # 40초에서 20초로 감소
199
- def generate_background(prompt: str, aspect_ratio: str) -> Image.Image:
200
- try:
201
- width, height = calculate_dimensions(aspect_ratio)
202
- width, height = adjust_size_to_multiple_of_8(width, height)
203
-
204
- max_size = 768
205
- if width > max_size or height > max_size:
206
- ratio = max_size / max(width, height)
207
- width = int(width * ratio)
208
- height = int(height * ratio)
209
- width, height = adjust_size_to_multiple_of_8(width, height)
210
-
211
- with timer("Background generation"):
212
- try:
213
- with torch.inference_mode():
214
- image = pipe(
215
- prompt=prompt,
216
- width=width,
217
- height=height,
218
- num_inference_steps=8,
219
- guidance_scale=4.0
220
- ).images[0]
221
- except Exception as e:
222
- print(f"Pipeline error: {str(e)}")
223
- return Image.new('RGB', (width, height), 'white')
224
-
225
- return image
226
- except Exception as e:
227
- print(f"Background generation error: {str(e)}")
228
- return Image.new('RGB', (512, 512), 'white')
229
-
230
- def create_position_grid():
231
- return """
232
- <div class="position-grid" style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; width: 150px; margin: auto;">
233
- <button class="position-btn" data-pos="top-left">↖</button>
234
- <button class="position-btn" data-pos="top-center">↑</button>
235
- <button class="position-btn" data-pos="top-right">↗</button>
236
- <button class="position-btn" data-pos="middle-left">←</button>
237
- <button class="position-btn" data-pos="middle-center">•</button>
238
- <button class="position-btn" data-pos="middle-right">→</button>
239
- <button class="position-btn" data-pos="bottom-left">↙</button>
240
- <button class="position-btn" data-pos="bottom-center" data-default="true">↓</button>
241
- <button class="position-btn" data-pos="bottom-right">↘</button>
242
- </div>
243
- """
244
-
245
- def calculate_object_position(position: str, bg_size: tuple[int, int], obj_size: tuple[int, int]) -> tuple[int, int]:
246
- """오브젝트의 위치 계산"""
247
- bg_width, bg_height = bg_size
248
- obj_width, obj_height = obj_size
249
-
250
- positions = {
251
- "top-left": (0, 0),
252
- "top-center": ((bg_width - obj_width) // 2, 0),
253
- "top-right": (bg_width - obj_width, 0),
254
- "middle-left": (0, (bg_height - obj_height) // 2),
255
- "middle-center": ((bg_width - obj_width) // 2, (bg_height - obj_height) // 2),
256
- "middle-right": (bg_width - obj_width, (bg_height - obj_height) // 2),
257
- "bottom-left": (0, bg_height - obj_height),
258
- "bottom-center": ((bg_width - obj_width) // 2, bg_height - obj_height),
259
- "bottom-right": (bg_width - obj_width, bg_height - obj_height)
260
- }
261
-
262
- return positions.get(position, positions["bottom-center"])
263
-
264
- def resize_object(image: Image.Image, scale_percent: float) -> Image.Image:
265
- """오브젝트 크기 조정"""
266
- width = int(image.width * scale_percent / 100)
267
- height = int(image.height * scale_percent / 100)
268
- return image.resize((width, height), Image.Resampling.LANCZOS)
269
-
270
- def combine_with_background(foreground: Image.Image, background: Image.Image,
271
- position: str = "bottom-center", scale_percent: float = 100) -> Image.Image:
272
- """전경과 배경 합성 함수"""
273
- print(f"Combining with position: {position}, scale: {scale_percent}")
274
-
275
- result = background.convert('RGBA')
276
- scaled_foreground = resize_object(foreground, scale_percent)
277
-
278
- x, y = calculate_object_position(position, result.size, scaled_foreground.size)
279
- print(f"Calculated position coordinates: ({x}, {y})")
280
-
281
- result.paste(scaled_foreground, (x, y), scaled_foreground)
282
- return result
283
-
284
- @spaces.GPU(duration=30) # 120초에서 30초로 감소
285
- def _gpu_process(img: Image.Image, prompt: str | BoundingBox | None) -> tuple[Image.Image, BoundingBox | None, list[str]]:
286
- time_log: list[str] = []
287
  try:
288
- if isinstance(prompt, str):
289
- t0 = time.time()
290
- bbox = gd_detect(img, prompt)
291
- time_log.append(f"detect: {time.time() - t0}")
292
- if not bbox:
293
- print(time_log[0])
294
- raise gr.Error("No object detected")
295
- else:
296
- bbox = prompt
297
- t0 = time.time()
298
- mask = segmenter(img, bbox)
299
- time_log.append(f"segment: {time.time() - t0}")
300
- return mask, bbox, time_log
301
  except Exception as e:
302
- print(f"GPU process error: {str(e)}")
303
- raise
304
-
305
- def _process(img: Image.Image, prompt: str | BoundingBox | None, bg_prompt: str | None = None, aspect_ratio: str = "1:1") -> tuple[tuple[Image.Image, Image.Image, Image.Image], gr.DownloadButton]:
306
- try:
307
- # 입력 이미지 크기 제한
308
- max_size = 1024
309
- if img.width > max_size or img.height > max_size:
310
- ratio = max_size / max(img.width, img.height)
311
- new_size = (int(img.width * ratio), int(img.height * ratio))
312
- img = img.resize(new_size, Image.LANCZOS)
313
-
314
- # CUDA 메모리 관리 수정
315
- try:
316
- if torch.cuda.is_available():
317
- current_device = torch.cuda.current_device()
318
- with torch.cuda.device(current_device):
319
- torch.cuda.empty_cache()
320
- except Exception as e:
321
- print(f"CUDA memory management failed: {e}")
322
-
323
- with torch.cuda.amp.autocast(enabled=torch.cuda.is_available()):
324
- mask, bbox, time_log = _gpu_process(img, prompt)
325
- masked_alpha = apply_mask(img, mask, defringe=True)
326
-
327
- if bg_prompt:
328
- background = generate_background(bg_prompt, aspect_ratio)
329
- combined = background
330
- else:
331
- combined = Image.alpha_composite(Image.new("RGBA", masked_alpha.size, "white"), masked_alpha)
332
-
333
- clear_memory()
334
-
335
- with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as temp:
336
- combined.save(temp.name)
337
- return (img, combined, masked_alpha), gr.DownloadButton(value=temp.name, interactive=True)
338
- except Exception as e:
339
- clear_memory()
340
- print(f"Processing error: {str(e)}")
341
- raise gr.Error(f"Processing failed: {str(e)}")
342
 
343
- def on_change_bbox(prompts: dict[str, Any] | None):
344
- return gr.update(interactive=prompts is not None)
 
 
345
 
 
 
346
 
347
- def on_change_prompt(img: Image.Image | None, prompt: str | None, bg_prompt: str | None = None):
348
- return gr.update(interactive=bool(img and prompt))
 
 
 
 
 
349
 
350
 
351
- def process_prompt(img: Image.Image, prompt: str, bg_prompt: str | None = None,
352
- aspect_ratio: str = "1:1", position: str = "bottom-center",
353
- scale_percent: float = 100) -> tuple[Image.Image, Image.Image]:
 
 
 
 
 
 
 
 
354
  try:
355
- if img is None or prompt.strip() == "":
356
- raise gr.Error("Please provide both image and prompt")
357
-
358
- print(f"Processing with position: {position}, scale: {scale_percent}") # 디버깅용
359
 
360
- try:
361
- prompt = translate_to_english(prompt)
362
- if bg_prompt:
363
- bg_prompt = translate_to_english(bg_prompt)
364
- except Exception as e:
365
- print(f"Translation error (continuing with original text): {str(e)}")
366
 
367
- results, _ = _process(img, prompt, bg_prompt, aspect_ratio)
 
 
 
 
 
 
 
 
368
 
369
- if bg_prompt:
370
- try:
371
- print(f"Using position: {position}") # 디버깅용
372
- # 위치 값 검증
373
- valid_positions = ["top-left", "top-center", "top-right",
374
- "middle-left", "middle-center", "middle-right",
375
- "bottom-left", "bottom-center", "bottom-right"]
376
- if position not in valid_positions:
377
- position = "bottom-center"
378
- print(f"Invalid position, using default: {position}")
379
-
380
- combined = combine_with_background(
381
- foreground=results[2],
382
- background=results[1],
383
- position=position,
384
- scale_percent=scale_percent
385
- )
386
- return combined, results[2]
387
- except Exception as e:
388
- print(f"Combination error: {str(e)}")
389
- return results[1], results[2]
390
 
391
- return results[1], results[2] # 기본 반환 추가
392
  except Exception as e:
393
- print(f"Error in process_prompt: {str(e)}")
394
- raise gr.Error(str(e))
395
  finally:
396
  clear_memory()
397
 
398
-
399
- def process_bbox(img: Image.Image, box_input: str) -> tuple[Image.Image, Image.Image]:
400
- try:
401
- if img is None or box_input.strip() == "":
402
- raise gr.Error("Please provide both image and bounding box coordinates")
403
-
404
- try:
405
- coords = eval(box_input)
406
- if not isinstance(coords, list) or len(coords) != 4:
407
- raise ValueError("Invalid box format")
408
- bbox = tuple(int(x) for x in coords)
409
- except:
410
- raise gr.Error("Invalid box format. Please provide [xmin, ymin, xmax, ymax]")
411
-
412
- # Process the image
413
- results, _ = _process(img, bbox)
414
-
415
- # 합성된 이미지와 추출된 이미지만 반환
416
- return results[1], results[2]
417
- except Exception as e:
418
- raise gr.Error(str(e))
419
-
420
- # Event handler functions 수정
421
- def update_process_button(img, prompt):
422
- return gr.update(
423
- interactive=bool(img and prompt),
424
- variant="primary" if bool(img and prompt) else "secondary"
425
- )
426
-
427
- def update_box_button(img, box_input):
428
- try:
429
- if img and box_input:
430
- coords = eval(box_input)
431
- if isinstance(coords, list) and len(coords) == 4:
432
- return gr.update(interactive=True, variant="primary")
433
- return gr.update(interactive=False, variant="secondary")
434
- except:
435
- return gr.update(interactive=False, variant="secondary")
436
-
437
-
438
- css = """
439
- footer {display: none}
440
- .main-title {
441
- text-align: center;
442
- margin: 1em 0;
443
- padding: 1.5em;
444
- background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
445
- border-radius: 15px;
446
- box-shadow: 0 4px 6px rgba(0,0,0,0.1);
447
- }
448
- .main-title h1 {
449
- color: #2196F3;
450
- font-size: 2.8em;
451
- margin-bottom: 0.3em;
452
- font-weight: 700;
453
- }
454
- .main-title p {
455
- color: #555;
456
- font-size: 1.3em;
457
- line-height: 1.4;
458
- }
459
- .container {
460
- max-width: 1200px;
461
- margin: auto;
462
- padding: 20px;
463
- }
464
- .input-panel, .output-panel {
465
- background: white;
466
- padding: 1.5em;
467
- border-radius: 12px;
468
- box-shadow: 0 2px 8px rgba(0,0,0,0.08);
469
- margin-bottom: 1em;
470
- }
471
- .controls-panel {
472
- background: #f8f9fa;
473
- padding: 1em;
474
- border-radius: 8px;
475
- margin: 1em 0;
476
- }
477
- .image-display {
478
- min-height: 512px;
479
- display: flex;
480
- align-items: center;
481
- justify-content: center;
482
- background: #fafafa;
483
- border-radius: 8px;
484
- margin: 1em 0;
485
- }
486
- .example-section {
487
- text-align: center;
488
- padding: 2em;
489
- background: #f5f5f5;
490
- border-radius: 12px;
491
- margin-top: 2em;
492
- }
493
- .example-section img {
494
- max-width: 100%;
495
- border-radius: 8px;
496
- box-shadow: 0 4px 8px rgba(0,0,0,0.1);
497
- }
498
- .accordion {
499
- border: 1px solid #e0e0e0;
500
- border-radius: 8px;
501
- margin: 1em 0;
502
- }
503
- .accordion-header {
504
- padding: 1em;
505
- background: #f5f5f5;
506
- cursor: pointer;
507
- }
508
- .accordion-content {
509
- padding: 1em;
510
- display: none;
511
- }
512
- .accordion.open .accordion-content {
513
- display: block;
514
- }
515
- .position-grid {
516
- display: grid;
517
- grid-template-columns: repeat(3, 1fr);
518
- gap: 8px;
519
- margin: 1em 0;
520
- }
521
- .position-btn {
522
- padding: 10px;
523
- border: 1px solid #ddd;
524
- border-radius: 4px;
525
- background: white;
526
- cursor: pointer;
527
- transition: all 0.3s ease;
528
- width: 40px;
529
- height: 40px;
530
- display: flex;
531
- align-items: center;
532
- justify-content: center;
533
- }
534
- .position-btn:hover {
535
- background: #e3f2fd;
536
- }
537
- .position-btn.selected {
538
- background-color: #2196F3;
539
- color: white;
540
- border-color: #1976D2;
541
- }
542
- """
543
-
544
-
545
  def add_text_with_stroke(draw, text, x, y, font, text_color, stroke_width):
546
- """Helper function to draw text with stroke"""
547
- # Draw the stroke/outline
548
  for adj_x in range(-stroke_width, stroke_width + 1):
549
  for adj_y in range(-stroke_width, stroke_width + 1):
550
  draw.text((x + adj_x, y + adj_y), text, font=font, fill=text_color)
551
 
552
- def remove_background(image):
553
- # Save the image to a specific location
554
- filename = f"image_{uuid.uuid4()}.png" # Generates a universally unique identifier (UUID) for the filename
555
- image.save(filename)
556
- # Call gradio client for background removal
557
- result = client.predict(images=handle_file(filename), api_name="/image")
558
- return Image.open(result[0])
559
-
560
- def superimpose(image_with_text, overlay_image):
561
- # Open image as RGBA to handle transparency
562
- overlay_image = overlay_image.convert("RGBA")
563
- # Paste overlay on the background
564
- image_with_text.paste(overlay_image, (0, 0), overlay_image)
565
- # Save the final image
566
- # image_with_text.save("output_image.png")
567
- return image_with_text
568
-
569
  def add_text_to_image(
570
  input_image,
571
  text,
@@ -582,7 +148,6 @@ def add_text_to_image(
582
  if input_image is None or text.strip() == "":
583
  return input_image
584
 
585
- # PIL Image 객체로 변환
586
  if not isinstance(input_image, Image.Image):
587
  if isinstance(input_image, np.ndarray):
588
  image = Image.fromarray(input_image)
@@ -591,11 +156,9 @@ def add_text_to_image(
591
  else:
592
  image = input_image.copy()
593
 
594
- # 이미지를 RGBA 모드로 변환
595
  if image.mode != 'RGBA':
596
  image = image.convert('RGBA')
597
 
598
- # 폰트 설정
599
  font_files = {
600
  "Default": "DejaVuSans.ttf",
601
  "Korean Regular": "ko-Regular.ttf"
@@ -608,7 +171,6 @@ def add_text_to_image(
608
  print(f"Font loading error ({font_choice}): {str(e)}")
609
  font = ImageFont.load_default()
610
 
611
- # 색상 설정
612
  color_map = {
613
  'White': (255, 255, 255),
614
  'Black': (0, 0, 0),
@@ -620,419 +182,187 @@ def add_text_to_image(
620
  }
621
  rgb_color = color_map.get(color, (255, 255, 255))
622
 
623
- # 임시 Draw 객체 생성하여 텍스트 크기 계산
624
  temp_draw = ImageDraw.Draw(image)
625
  text_bbox = temp_draw.textbbox((0, 0), text, font=font)
626
  text_width = text_bbox[2] - text_bbox[0]
627
  text_height = text_bbox[3] - text_bbox[1]
628
 
629
- # 위치 계산
630
  actual_x = int((image.width - text_width) * (x_position / 100))
631
  actual_y = int((image.height - text_height) * (y_position / 100))
632
 
633
- # 텍스트 색상 설정
634
  text_color = (*rgb_color, int(opacity))
635
 
636
- if text_position_type == "Text Behind Image":
637
- try:
638
- # 원본 이미지에서 전경 객체만 추출
639
- foreground = remove_background(image)
640
-
641
- # 배경 이미지 생성 (원본 이미지 복사)
642
- background = image.copy()
643
-
644
- # 텍스트를 그릴 임시 레이어 생성
645
- text_layer = Image.new('RGBA', image.size, (255, 255, 255, 0))
646
- draw_text = ImageDraw.Draw(text_layer)
647
-
648
- # 텍스트 그리기
649
- add_text_with_stroke(
650
- draw_text,
651
- text,
652
- actual_x,
653
- actual_y,
654
- font,
655
- text_color,
656
- int(thickness)
657
- )
658
-
659
- # 배경에 텍스트 합성
660
- background = Image.alpha_composite(background, text_layer)
661
-
662
- # 텍스트가 있는 배경 위에 전경 객체 합성
663
- output_image = Image.alpha_composite(background, foreground)
664
- except Exception as e:
665
- print(f"Error in Text Behind Image processing: {str(e)}")
666
- return input_image
667
- else:
668
- # 텍스트 오버레이 생성
669
- txt_overlay = Image.new('RGBA', image.size, (255, 255, 255, 0))
670
- draw = ImageDraw.Draw(txt_overlay)
671
-
672
- # 텍스트를 이미지 위에 그리기
673
- add_text_with_stroke(
674
- draw,
675
- text,
676
- actual_x,
677
- actual_y,
678
- font,
679
- text_color,
680
- int(thickness)
681
- )
682
- output_image = Image.alpha_composite(image, txt_overlay)
683
 
684
- # RGB로 변환
685
  output_image = output_image.convert('RGB')
686
 
687
  return output_image
688
 
689
  except Exception as e:
690
  print(f"Error in add_text_to_image: {str(e)}")
691
- return input_image
692
-
693
-
694
- def update_position(new_position):
695
- """위치 업데이트 함수"""
696
- print(f"Position updated to: {new_position}")
697
- return new_position
698
-
699
- def update_controls(bg_prompt):
700
- """배경 프롬프트 입력 여부에 따라 컨트롤 표시 업데이트"""
701
- is_visible = bool(bg_prompt)
702
- return [
703
- gr.update(visible=is_visible), # aspect_ratio
704
- gr.update(visible=is_visible), # object_controls
705
- ]
706
-
707
-
708
- # 저장 디렉토리 설정
709
- SAVE_DIR = "saved_images"
710
- if not os.path.exists(SAVE_DIR):
711
- os.makedirs(SAVE_DIR, exist_ok=True)
712
-
713
- MAX_SEED = np.iinfo(np.int32).max
714
- MAX_IMAGE_SIZE = 1024
715
 
716
- def save_generated_image(image, prompt):
717
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
718
- unique_id = str(uuid.uuid4())[:8]
719
- filename = f"{timestamp}_{unique_id}.png"
720
- filepath = os.path.join(SAVE_DIR, filename)
721
-
722
- image.save(filepath)
723
- return filepath
724
 
725
- gen_pipe = FluxPipeline.from_pretrained(
726
- "black-forest-labs/FLUX.1-dev",
727
- torch_dtype=torch.float16,
728
- use_auth_token=HF_TOKEN
729
- )
730
- gen_pipe.enable_attention_slicing(slice_size="auto")
731
-
732
- # 이미지 생성용 LoRA 가중치 로드
733
- try:
734
- lora_path = hf_hub_download(
735
- "ginipick/flux-lora-eric-cat",
736
- "flux-lora-eric-cat.safetensors", # 실제 파일명 확인 필요
737
- use_auth_token=HF_TOKEN
738
- )
739
- gen_pipe.load_lora_weights(lora_path)
740
- gen_pipe.fuse_lora(lora_scale=0.125)
741
- except Exception as e:
742
- print(f"Error loading generation LoRA weights: {str(e)}")
743
- raise ValueError("Failed to load generation LoRA weights. Please check your HF_TOKEN and model access.")
744
-
745
- # GPU로 이동
746
- if torch.cuda.is_available():
747
- try:
748
- gen_pipe = gen_pipe.to("cuda:0")
749
- except Exception as e:
750
- print(f"Warning: Could not move generation pipeline to CUDA: {str(e)}")
751
-
752
- # generate_image 함수 수정
753
- @spaces.GPU(duration=60)
754
- def generate_image(
755
- prompt: str,
756
- seed: int,
757
- randomize_seed: bool,
758
- width: int,
759
- height: int,
760
- guidance_scale: float,
761
- num_inference_steps: int,
762
- progress: gr.Progress = gr.Progress()
763
- ):
764
- try:
765
- if randomize_seed:
766
- seed = random.randint(0, MAX_SEED)
767
-
768
- generator = torch.Generator(device=device).manual_seed(seed)
769
-
770
- with torch.inference_mode():
771
- # gen_pipe 사용
772
- image = gen_pipe(
773
- prompt=prompt,
774
- width=width,
775
- height=height,
776
- num_inference_steps=num_inference_steps,
777
- guidance_scale=guidance_scale,
778
- generator=generator,
779
- ).images[0]
780
-
781
- filepath = save_generated_image(image, prompt)
782
- return image, seed
783
-
784
- except Exception as e:
785
- raise gr.Error(f"Image generation failed: {str(e)}")
786
- finally:
787
- clear_memory()
788
 
789
  with gr.Blocks(theme=gr.themes.Soft(), css=css) as demo:
790
- position = gr.State(value="bottom-center")
791
-
792
  gr.HTML("""
793
  <div class="main-title">
794
  <h1>🎨 Webtoon Canvas</h1>
795
- <p>Webtoon generated, Remove background of specified objects, generate new backgrounds, and insert text over or behind images with prompts.</p>
796
  </div>
797
  """)
798
 
799
- with gr.Tabs():
800
- with gr.Tab("Edit & Combine"):
801
- with gr.Row(equal_height=True):
802
- # 왼쪽 패널 (입력)
803
- with gr.Column(scale=1):
804
- with gr.Group(elem_classes="input-panel"):
805
- input_image = gr.Image(
806
- type="pil",
807
- label="Upload Image",
808
- interactive=True,
809
- height=400
810
- )
811
- text_prompt = gr.Textbox(
812
- label="Object to Extract",
813
- placeholder="Enter what you want to extract...",
814
- interactive=True
815
- )
816
- with gr.Row():
817
- bg_prompt = gr.Textbox(
818
- label="Background Prompt (optional)",
819
- placeholder="Describe the background...",
820
- interactive=True,
821
- scale=3
822
- )
823
- aspect_ratio = gr.Dropdown(
824
- choices=["1:1", "16:9", "9:16", "4:3"],
825
- value="1:1",
826
- label="Aspect Ratio",
827
- interactive=True,
828
- visible=True,
829
- scale=1
830
- )
831
-
832
- with gr.Group(elem_classes="controls-panel", visible=False) as object_controls:
833
- with gr.Column(scale=1):
834
- position = gr.State(value="bottom-center")
835
- with gr.Row():
836
- btn_top_left = gr.Button("↖", elem_classes="position-btn")
837
- btn_top_center = gr.Button("↑", elem_classes="position-btn")
838
- btn_top_right = gr.Button("↗", elem_classes="position-btn")
839
- with gr.Row():
840
- btn_middle_left = gr.Button("←", elem_classes="position-btn")
841
- btn_middle_center = gr.Button("•", elem_classes="position-btn")
842
- btn_middle_right = gr.Button("→", elem_classes="position-btn")
843
- with gr.Row():
844
- btn_bottom_left = gr.Button("↙", elem_classes="position-btn")
845
- btn_bottom_center = gr.Button("↓", elem_classes="position-btn", value="selected")
846
- btn_bottom_right = gr.Button("↘", elem_classes="position-btn")
847
- with gr.Column(scale=1):
848
- scale_slider = gr.Slider(
849
- minimum=10,
850
- maximum=200,
851
- value=50,
852
- step=5,
853
- label="Object Size (%)"
854
- )
855
-
856
- process_btn = gr.Button(
857
- "Process",
858
- variant="primary",
859
- interactive=False,
860
- size="lg"
861
- )
862
-
863
- # 오른쪽 패널 (출력)
864
- with gr.Column(scale=1):
865
- with gr.Group(elem_classes="output-panel"):
866
- combined_image = gr.Image(
867
- label="Combined Result",
868
- show_download_button=True,
869
- type="pil",
870
- height=400
871
- )
872
-
873
- with gr.Accordion("Text Insertion Options", open=False):
874
- with gr.Group():
875
- with gr.Row():
876
- text_input = gr.Textbox(
877
- label="Text Content",
878
- placeholder="Enter text to add..."
879
- )
880
- text_position_type = gr.Radio(
881
- choices=["Text Over Image", "Text Behind Image"],
882
- value="Text Over Image",
883
- label="Text Position"
884
- )
885
-
886
- with gr.Row():
887
- with gr.Column(scale=1):
888
- font_choice = gr.Dropdown(
889
- choices=["Default", "Korean Regular"],
890
- value="Default",
891
- label="Font Selection",
892
- interactive=True
893
- )
894
- font_size = gr.Slider(
895
- minimum=10,
896
- maximum=200,
897
- value=40,
898
- step=5,
899
- label="Font Size"
900
- )
901
- color_dropdown = gr.Dropdown(
902
- choices=["White", "Black", "Red", "Green", "Blue", "Yellow", "Purple"],
903
- value="White",
904
- label="Text Color"
905
- )
906
- thickness = gr.Slider(
907
- minimum=0,
908
- maximum=10,
909
- value=1,
910
- step=1,
911
- label="Text Thickness"
912
- )
913
- with gr.Column(scale=1):
914
- opacity_slider = gr.Slider(
915
- minimum=0,
916
- maximum=255,
917
- value=255,
918
- step=1,
919
- label="Opacity"
920
- )
921
- x_position = gr.Slider(
922
- minimum=0,
923
- maximum=100,
924
- value=50,
925
- step=1,
926
- label="Left(0%)~Right(100%)"
927
- )
928
- y_position = gr.Slider(
929
- minimum=0,
930
- maximum=100,
931
- value=50,
932
- step=1,
933
- label="High(0%)~Low(100%)"
934
- )
935
- add_text_btn = gr.Button("Apply Text", variant="primary")
936
-
937
- extracted_image = gr.Image(
938
- label="Extracted Object",
939
- show_download_button=True,
940
- type="pil",
941
- height=200
942
- )
943
 
944
- with gr.Tab("Generate Image"):
945
- with gr.Column():
946
- gen_prompt = gr.Textbox(
947
- label="Generation Prompt",
948
- placeholder="Enter your image generation prompt..."
949
  )
950
  with gr.Row():
951
- gen_width = gr.Slider(512, 1024, 768, step=64, label="Width")
952
- gen_height = gr.Slider(512, 1024, 768, step=64, label="Height")
953
-
 
 
 
 
 
 
 
 
 
 
954
  with gr.Row():
955
- guidance_scale = gr.Slider(1, 20, 7.5, step=0.5, label="Guidance Scale")
956
- num_steps = gr.Slider(1, 50, 30, step=1, label="Number of Steps")
957
-
 
 
 
 
 
 
 
 
 
958
  with gr.Row():
959
- seed = gr.Number(label="Seed", value=-1)
960
- randomize_seed = gr.Checkbox(label="Randomize Seed", value=True)
961
-
962
- generate_btn = gr.Button("Generate Image", variant="primary")
963
-
964
- output_image = gr.Image(label="Generated Image", type="pil")
965
- output_seed = gr.Number(label="Used Seed", interactive=False)
966
-
967
- # CSS 클래스를 위한 스타일 추가
968
- gr.HTML("""
969
- <style>
970
- .position-btn.selected {
971
- background-color: #2196F3 !important;
972
- color: white !important;
973
- }
974
- </style>
975
- """)
976
-
977
- # 버튼 클릭 이벤트 바인딩
978
- position_mapping = {
979
- btn_top_left: "top-left",
980
- btn_top_center: "top-center",
981
- btn_top_right: "top-right",
982
- btn_middle_left: "middle-left",
983
- btn_middle_center: "middle-center",
984
- btn_middle_right: "middle-right",
985
- btn_bottom_left: "bottom-left",
986
- btn_bottom_center: "bottom-center",
987
- btn_bottom_right: "bottom-right"
988
- }
989
-
990
- for btn, pos in position_mapping.items():
991
- btn.click(
992
- fn=lambda pos=pos: update_position(pos),
993
- outputs=position
994
- )
995
 
996
  # 이벤트 바인딩
997
- bg_prompt.change(
998
- fn=update_controls,
999
- inputs=bg_prompt,
1000
- outputs=[aspect_ratio, object_controls],
1001
- queue=False
1002
- )
1003
-
1004
- input_image.change(
1005
- fn=update_process_button,
1006
- inputs=[input_image, text_prompt],
1007
- outputs=process_btn,
1008
- queue=False
1009
- )
1010
-
1011
- text_prompt.change(
1012
- fn=update_process_button,
1013
- inputs=[input_image, text_prompt],
1014
- outputs=process_btn,
1015
- queue=False
1016
- )
1017
-
1018
- process_btn.click(
1019
- fn=process_prompt,
1020
  inputs=[
1021
- input_image,
1022
- text_prompt,
1023
- bg_prompt,
1024
- aspect_ratio,
1025
- position,
1026
- scale_slider
 
1027
  ],
1028
- outputs=[combined_image, extracted_image],
1029
- queue=True
1030
  )
1031
 
1032
  add_text_btn.click(
1033
  fn=add_text_to_image,
1034
  inputs=[
1035
- combined_image,
1036
  text_input,
1037
  font_size,
1038
  color_dropdown,
@@ -1040,25 +370,10 @@ with gr.Blocks(theme=gr.themes.Soft(), css=css) as demo:
1040
  x_position,
1041
  y_position,
1042
  thickness,
1043
- text_position_type,
1044
  font_choice
1045
  ],
1046
- outputs=combined_image,
1047
- api_name="add_text"
1048
- )
1049
-
1050
- generate_btn.click(
1051
- fn=generate_image,
1052
- inputs=[
1053
- gen_prompt,
1054
- seed,
1055
- randomize_seed,
1056
- gen_width,
1057
- gen_height,
1058
- guidance_scale,
1059
- num_steps,
1060
- ],
1061
- outputs=[output_image, output_seed]
1062
  )
1063
 
1064
  demo.queue(max_size=5)
@@ -1067,4 +382,7 @@ demo.launch(
1067
  server_port=7860,
1068
  share=False,
1069
  max_threads=2
1070
- )
 
 
 
 
1
+ import os
2
+ import gc
3
+ import uuid
4
+ import random
5
  import tempfile
6
  import time
7
+ from datetime import datetime
8
+ from typing import Any
 
9
  from huggingface_hub import login, hf_hub_download
10
 
11
  import gradio as gr
12
  import numpy as np
 
 
13
  import torch
 
 
 
 
 
 
 
 
 
 
 
14
  from PIL import Image, ImageDraw, ImageFont
15
+ from diffusers import FluxPipeline
 
 
 
 
 
16
 
17
+ # 메모리 정리 함수
18
  def clear_memory():
 
19
  gc.collect()
20
  try:
21
  if torch.cuda.is_available():
22
+ with torch.cuda.device(0):
23
  torch.cuda.empty_cache()
24
  except:
25
  pass
26
 
27
  # GPU 설정
28
+ device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
29
 
 
30
  if torch.cuda.is_available():
31
  try:
32
  with torch.cuda.device(0):
 
36
  except:
37
  print("Warning: Could not configure CUDA settings")
38
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  # HF 토큰 설정
40
  HF_TOKEN = os.getenv("HF_TOKEN")
41
  if HF_TOKEN is None:
 
46
  except Exception as e:
47
  raise ValueError(f"Failed to login to Hugging Face: {str(e)}")
48
 
 
 
 
 
 
 
 
 
 
 
 
49
  # FLUX 파이프라인 초기화
50
  pipe = FluxPipeline.from_pretrained(
51
  "black-forest-labs/FLUX.1-dev",
 
54
  )
55
  pipe.enable_attention_slicing(slice_size="auto")
56
 
57
+ # Eric cat LoRA 가중치 로드
58
+ try:
59
+ lora_path = hf_hub_download(
60
+ "ginipick/flux-lora-eric-cat",
61
+ "flux-lora-eric-cat.safetensors",
62
  use_auth_token=HF_TOKEN
63
  )
64
+ pipe.load_lora_weights(lora_path)
65
+ pipe.fuse_lora(lora_scale=0.125)
 
 
 
 
 
66
  except Exception as e:
67
+ print(f"Error loading LoRA weights: {str(e)}")
68
+ raise ValueError("Failed to load LoRA weights. Please check your HF_TOKEN and model access.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
 
70
+ # GPU로 이동
71
+ if torch.cuda.is_available():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  try:
73
+ pipe = pipe.to("cuda:0")
 
 
 
 
 
 
 
 
 
 
 
 
74
  except Exception as e:
75
+ print(f"Warning: Could not move pipeline to CUDA: {str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
 
77
+ # 저장 디렉토리 설정
78
+ SAVE_DIR = "saved_images"
79
+ if not os.path.exists(SAVE_DIR):
80
+ os.makedirs(SAVE_DIR, exist_ok=True)
81
 
82
+ MAX_SEED = np.iinfo(np.int32).max
83
+ MAX_IMAGE_SIZE = 1024
84
 
85
+ def save_generated_image(image, prompt):
86
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
87
+ unique_id = str(uuid.uuid4())[:8]
88
+ filename = f"{timestamp}_{unique_id}.png"
89
+ filepath = os.path.join(SAVE_DIR, filename)
90
+ image.save(filepath)
91
+ return filepath
92
 
93
 
94
+ @gr.GPU(duration=60)
95
+ def generate_image(
96
+ prompt: str,
97
+ seed: int,
98
+ randomize_seed: bool,
99
+ width: int,
100
+ height: int,
101
+ guidance_scale: float,
102
+ num_inference_steps: int,
103
+ progress: gr.Progress = gr.Progress()
104
+ ):
105
  try:
106
+ if randomize_seed:
107
+ seed = random.randint(0, MAX_SEED)
 
 
108
 
109
+ generator = torch.Generator(device=device).manual_seed(seed)
 
 
 
 
 
110
 
111
+ with torch.inference_mode():
112
+ image = pipe(
113
+ prompt=prompt,
114
+ width=width,
115
+ height=height,
116
+ num_inference_steps=num_inference_steps,
117
+ guidance_scale=guidance_scale,
118
+ generator=generator,
119
+ ).images[0]
120
 
121
+ filepath = save_generated_image(image, prompt)
122
+ return image, seed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
 
 
124
  except Exception as e:
125
+ raise gr.Error(f"Image generation failed: {str(e)}")
 
126
  finally:
127
  clear_memory()
128
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  def add_text_with_stroke(draw, text, x, y, font, text_color, stroke_width):
130
+ """텍스트에 외곽선을 추가하는 함수"""
 
131
  for adj_x in range(-stroke_width, stroke_width + 1):
132
  for adj_y in range(-stroke_width, stroke_width + 1):
133
  draw.text((x + adj_x, y + adj_y), text, font=font, fill=text_color)
134
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  def add_text_to_image(
136
  input_image,
137
  text,
 
148
  if input_image is None or text.strip() == "":
149
  return input_image
150
 
 
151
  if not isinstance(input_image, Image.Image):
152
  if isinstance(input_image, np.ndarray):
153
  image = Image.fromarray(input_image)
 
156
  else:
157
  image = input_image.copy()
158
 
 
159
  if image.mode != 'RGBA':
160
  image = image.convert('RGBA')
161
 
 
162
  font_files = {
163
  "Default": "DejaVuSans.ttf",
164
  "Korean Regular": "ko-Regular.ttf"
 
171
  print(f"Font loading error ({font_choice}): {str(e)}")
172
  font = ImageFont.load_default()
173
 
 
174
  color_map = {
175
  'White': (255, 255, 255),
176
  'Black': (0, 0, 0),
 
182
  }
183
  rgb_color = color_map.get(color, (255, 255, 255))
184
 
 
185
  temp_draw = ImageDraw.Draw(image)
186
  text_bbox = temp_draw.textbbox((0, 0), text, font=font)
187
  text_width = text_bbox[2] - text_bbox[0]
188
  text_height = text_bbox[3] - text_bbox[1]
189
 
 
190
  actual_x = int((image.width - text_width) * (x_position / 100))
191
  actual_y = int((image.height - text_height) * (y_position / 100))
192
 
 
193
  text_color = (*rgb_color, int(opacity))
194
 
195
+ txt_overlay = Image.new('RGBA', image.size, (255, 255, 255, 0))
196
+ draw = ImageDraw.Draw(txt_overlay)
197
+
198
+ add_text_with_stroke(
199
+ draw,
200
+ text,
201
+ actual_x,
202
+ actual_y,
203
+ font,
204
+ text_color,
205
+ int(thickness)
206
+ )
207
+ output_image = Image.alpha_composite(image, txt_overlay)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
 
 
209
  output_image = output_image.convert('RGB')
210
 
211
  return output_image
212
 
213
  except Exception as e:
214
  print(f"Error in add_text_to_image: {str(e)}")
215
+ return input_image
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
 
 
 
 
 
 
 
 
 
217
 
218
+ css = """
219
+ footer {display: none}
220
+ .main-title {
221
+ text-align: center;
222
+ margin: 1em 0;
223
+ padding: 1.5em;
224
+ background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
225
+ border-radius: 15px;
226
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1);
227
+ }
228
+ .main-title h1 {
229
+ color: #2196F3;
230
+ font-size: 2.8em;
231
+ margin-bottom: 0.3em;
232
+ font-weight: 700;
233
+ }
234
+ .main-title p {
235
+ color: #555;
236
+ font-size: 1.3em;
237
+ line-height: 1.4;
238
+ }
239
+ .container {
240
+ max-width: 1200px;
241
+ margin: auto;
242
+ padding: 20px;
243
+ }
244
+ .input-panel, .output-panel {
245
+ background: white;
246
+ padding: 1.5em;
247
+ border-radius: 12px;
248
+ box-shadow: 0 2px 8px rgba(0,0,0,0.08);
249
+ margin-bottom: 1em;
250
+ }
251
+ """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
 
253
  with gr.Blocks(theme=gr.themes.Soft(), css=css) as demo:
 
 
254
  gr.HTML("""
255
  <div class="main-title">
256
  <h1>🎨 Webtoon Canvas</h1>
257
+ <p>Generate webtoon-style images and add text with various styles and positions.</p>
258
  </div>
259
  """)
260
 
261
+ with gr.Row():
262
+ with gr.Column(scale=1):
263
+ # 이미지 생성 섹션
264
+ gen_prompt = gr.Textbox(
265
+ label="Generation Prompt",
266
+ placeholder="Enter your image generation prompt..."
267
+ )
268
+ with gr.Row():
269
+ gen_width = gr.Slider(512, 1024, 768, step=64, label="Width")
270
+ gen_height = gr.Slider(512, 1024, 768, step=64, label="Height")
271
+
272
+ with gr.Row():
273
+ guidance_scale = gr.Slider(1, 20, 7.5, step=0.5, label="Guidance Scale")
274
+ num_steps = gr.Slider(1, 50, 30, step=1, label="Number of Steps")
275
+
276
+ with gr.Row():
277
+ seed = gr.Number(label="Seed", value=-1)
278
+ randomize_seed = gr.Checkbox(label="Randomize Seed", value=True)
279
+
280
+ generate_btn = gr.Button("Generate Image", variant="primary")
281
+
282
+ output_image = gr.Image(
283
+ label="Generated Image",
284
+ type="pil",
285
+ show_download_button=True
286
+ )
287
+ output_seed = gr.Number(label="Used Seed", interactive=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
288
 
289
+ # 텍스트 추가 섹션
290
+ with gr.Accordion("Text Options", open=False):
291
+ text_input = gr.Textbox(
292
+ label="Text Content",
293
+ placeholder="Enter text to add..."
294
  )
295
  with gr.Row():
296
+ font_choice = gr.Dropdown(
297
+ choices=["Default", "Korean Regular"],
298
+ value="Default",
299
+ label="Font Selection",
300
+ interactive=True
301
+ )
302
+ font_size = gr.Slider(
303
+ minimum=10,
304
+ maximum=200,
305
+ value=40,
306
+ step=5,
307
+ label="Font Size"
308
+ )
309
  with gr.Row():
310
+ color_dropdown = gr.Dropdown(
311
+ choices=["White", "Black", "Red", "Green", "Blue", "Yellow", "Purple"],
312
+ value="White",
313
+ label="Text Color"
314
+ )
315
+ thickness = gr.Slider(
316
+ minimum=0,
317
+ maximum=10,
318
+ value=1,
319
+ step=1,
320
+ label="Text Thickness"
321
+ )
322
  with gr.Row():
323
+ opacity_slider = gr.Slider(
324
+ minimum=0,
325
+ maximum=255,
326
+ value=255,
327
+ step=1,
328
+ label="Opacity"
329
+ )
330
+ with gr.Row():
331
+ x_position = gr.Slider(
332
+ minimum=0,
333
+ maximum=100,
334
+ value=50,
335
+ step=1,
336
+ label="Left(0%)~Right(100%)"
337
+ )
338
+ y_position = gr.Slider(
339
+ minimum=0,
340
+ maximum=100,
341
+ value=50,
342
+ step=1,
343
+ label="High(0%)~Low(100%)"
344
+ )
345
+ add_text_btn = gr.Button("Apply Text", variant="primary")
 
 
 
 
 
 
 
 
 
 
 
 
 
346
 
347
  # 이벤트 바인딩
348
+ generate_btn.click(
349
+ fn=generate_image,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
350
  inputs=[
351
+ gen_prompt,
352
+ seed,
353
+ randomize_seed,
354
+ gen_width,
355
+ gen_height,
356
+ guidance_scale,
357
+ num_steps,
358
  ],
359
+ outputs=[output_image, output_seed]
 
360
  )
361
 
362
  add_text_btn.click(
363
  fn=add_text_to_image,
364
  inputs=[
365
+ output_image,
366
  text_input,
367
  font_size,
368
  color_dropdown,
 
370
  x_position,
371
  y_position,
372
  thickness,
373
+ "Text Over Image", # text_position_type 고정
374
  font_choice
375
  ],
376
+ outputs=output_image
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
377
  )
378
 
379
  demo.queue(max_size=5)
 
382
  server_port=7860,
383
  share=False,
384
  max_threads=2
385
+ )
386
+
387
+
388
+