ginipick commited on
Commit
4cbe1cd
ยท
verified ยท
1 Parent(s): c2d5a78

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +472 -421
app.py CHANGED
@@ -9,6 +9,11 @@ from threading import Thread
9
  import json
10
  import requests
11
  import cv2
 
 
 
 
 
12
  import gradio as gr
13
  import spaces
14
  import torch
@@ -16,220 +21,227 @@ from loguru import logger
16
  from PIL import Image
17
  from transformers import AutoProcessor, Gemma3ForConditionalGeneration, TextIteratorStreamer
18
 
19
- # CSV/TXT ๋ถ„์„
20
  import pandas as pd
21
- # PDF ํ…์ŠคํŠธ ์ถ”์ถœ
22
  import PyPDF2
23
 
24
- ##############################################################################
25
- # ๋ฉ”๋ชจ๋ฆฌ ์ •๋ฆฌ ํ•จ์ˆ˜ ์ถ”๊ฐ€
26
- ##############################################################################
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  def clear_cuda_cache():
28
  """CUDA ์บ์‹œ๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ๋น„์›๋‹ˆ๋‹ค."""
29
  if torch.cuda.is_available():
30
  torch.cuda.empty_cache()
31
  gc.collect()
32
 
33
- ##############################################################################
34
- # SERPHOUSE API key from environment variable
35
- ##############################################################################
36
  SERPHOUSE_API_KEY = os.getenv("SERPHOUSE_API_KEY", "")
37
 
38
- ##############################################################################
39
- # ๊ฐ„๋‹จํ•œ ํ‚ค์›Œ๋“œ ์ถ”์ถœ ํ•จ์ˆ˜ (ํ•œ๊ธ€ + ์•ŒํŒŒ๋ฒณ + ์ˆซ์ž + ๊ณต๋ฐฑ ๋ณด์กด)
40
- ##############################################################################
41
  def extract_keywords(text: str, top_k: int = 5) -> str:
42
- """
43
- 1) ํ•œ๊ธ€(๊ฐ€-ํžฃ), ์˜์–ด(a-zA-Z), ์ˆซ์ž(0-9), ๊ณต๋ฐฑ๋งŒ ๋‚จ๊น€
44
- 2) ๊ณต๋ฐฑ ๊ธฐ์ค€ ํ† ํฐ ๋ถ„๋ฆฌ
45
- 3) ์ตœ๋Œ€ top_k๊ฐœ๋งŒ
46
- """
47
  text = re.sub(r"[^a-zA-Z0-9๊ฐ€-ํžฃ\s]", "", text)
48
  tokens = text.split()
49
- key_tokens = tokens[:top_k]
50
- return " ".join(key_tokens)
51
 
52
- ##############################################################################
53
- # SerpHouse Live endpoint ํ˜ธ์ถœ
54
- # - ์ƒ์œ„ 20๊ฐœ ๊ฒฐ๊ณผ JSON์„ LLM์— ๋„˜๊ธธ ๋•Œ link, snippet ๋“ฑ ๋ชจ๋‘ ํฌํ•จ
55
- ##############################################################################
56
  def do_web_search(query: str) -> str:
57
- """
58
- ์ƒ์œ„ 20๊ฐœ 'organic' ๊ฒฐ๊ณผ item ์ „์ฒด(์ œ๋ชฉ, link, snippet ๋“ฑ)๋ฅผ
59
- JSON ๋ฌธ์ž์—ด ํ˜•ํƒœ๋กœ ๋ฐ˜ํ™˜
60
- """
61
  try:
62
  url = "https://api.serphouse.com/serp/live"
63
-
64
- # ๊ธฐ๋ณธ GET ๋ฐฉ์‹์œผ๋กœ ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฐ„์†Œํ™”ํ•˜๊ณ  ๊ฒฐ๊ณผ ์ˆ˜๋ฅผ 20๊ฐœ๋กœ ์ œํ•œ
65
  params = {
66
  "q": query,
67
  "domain": "google.com",
68
- "serp_type": "web", # ๊ธฐ๋ณธ ์›น ๊ฒ€์ƒ‰
69
  "device": "desktop",
70
  "lang": "en",
71
- "num": "20" # ์ตœ๋Œ€ 20๊ฐœ ๊ฒฐ๊ณผ๋งŒ ์š”์ฒญ
72
  }
73
-
74
- headers = {
75
- "Authorization": f"Bearer {SERPHOUSE_API_KEY}"
76
- }
77
-
78
  logger.info(f"SerpHouse API ํ˜ธ์ถœ ์ค‘... ๊ฒ€์ƒ‰์–ด: {query}")
79
- logger.info(f"์š”์ฒญ URL: {url} - ํŒŒ๋ผ๋ฏธํ„ฐ: {params}")
80
-
81
- # GET ์š”์ฒญ ์ˆ˜ํ–‰
82
  response = requests.get(url, headers=headers, params=params, timeout=60)
83
  response.raise_for_status()
84
-
85
- logger.info(f"SerpHouse API ์‘๋‹ต ์ƒํƒœ ์ฝ”๋“œ: {response.status_code}")
86
  data = response.json()
87
-
88
- # ๋‹ค์–‘ํ•œ ์‘๋‹ต ๊ตฌ์กฐ ์ฒ˜๋ฆฌ
89
  results = data.get("results", {})
90
  organic = None
91
-
92
- # ๊ฐ€๋Šฅํ•œ ์‘๋‹ต ๊ตฌ์กฐ 1
93
  if isinstance(results, dict) and "organic" in results:
94
  organic = results["organic"]
95
-
96
- # ๊ฐ€๋Šฅํ•œ ์‘๋‹ต ๊ตฌ์กฐ 2 (์ค‘์ฒฉ๋œ results)
97
  elif isinstance(results, dict) and "results" in results:
98
  if isinstance(results["results"], dict) and "organic" in results["results"]:
99
  organic = results["results"]["organic"]
100
-
101
- # ๊ฐ€๋Šฅํ•œ ์‘๋‹ต ๊ตฌ์กฐ 3 (์ตœ์ƒ์œ„ organic)
102
  elif "organic" in data:
103
  organic = data["organic"]
104
-
105
  if not organic:
106
  logger.warning("์‘๋‹ต์—์„œ organic ๊ฒฐ๊ณผ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")
107
- logger.debug(f"์‘๋‹ต ๊ตฌ์กฐ: {list(data.keys())}")
108
- if isinstance(results, dict):
109
- logger.debug(f"results ๊ตฌ์กฐ: {list(results.keys())}")
110
- return "No web search results found or unexpected API response structure."
111
-
112
- # ๊ฒฐ๊ณผ ์ˆ˜ ์ œํ•œ ๋ฐ ์ปจํ…์ŠคํŠธ ๊ธธ์ด ์ตœ์ ํ™”
113
  max_results = min(20, len(organic))
114
  limited_organic = organic[:max_results]
115
-
116
- # ๊ฒฐ๊ณผ ํ˜•์‹ ๊ฐœ์„  - ๋งˆํฌ๋‹ค์šด ํ˜•์‹์œผ๋กœ ์ถœ๋ ฅํ•˜์—ฌ ๊ฐ€๋…์„ฑ ํ–ฅ์ƒ
117
  summary_lines = []
118
  for idx, item in enumerate(limited_organic, start=1):
119
- title = item.get("title", "No title")
120
  link = item.get("link", "#")
121
- snippet = item.get("snippet", "No description")
122
  displayed_link = item.get("displayed_link", link)
123
-
124
- # ๋งˆํฌ๋‹ค์šด ํ˜•์‹ (๋งํฌ ํด๋ฆญ ๊ฐ€๋Šฅ)
125
  summary_lines.append(
126
- f"### Result {idx}: {title}\n\n"
127
  f"{snippet}\n\n"
128
  f"**์ถœ์ฒ˜**: [{displayed_link}]({link})\n\n"
129
  f"---\n"
130
  )
131
-
132
- # ๋ชจ๋ธ์—๊ฒŒ ๋ช…ํ™•ํ•œ ์ง€์นจ ์ถ”๊ฐ€
133
  instructions = """
134
  # ์›น ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ
135
  ์•„๋ž˜๋Š” ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ์ž…๋‹ˆ๋‹ค. ์งˆ๋ฌธ์— ๋‹ต๋ณ€ํ•  ๋•Œ ์ด ์ •๋ณด๋ฅผ ํ™œ์šฉํ•˜์„ธ์š”:
136
- 1. ๊ฐ ๊ฒฐ๊ณผ์˜ ์ œ๋ชฉ, ๋‚ด์šฉ, ์ถœ์ฒ˜ ๋งํฌ๋ฅผ ์ฐธ๊ณ ํ•˜์„ธ์š”
137
- 2. ๋‹ต๋ณ€์— ๊ด€๋ จ ์ •๋ณด์˜ ์ถœ์ฒ˜๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ์ธ์šฉํ•˜์„ธ์š” (์˜ˆ: "X ์ถœ์ฒ˜์— ๋”ฐ๋ฅด๋ฉด...")
138
- 3. ์‘๋‹ต์— ์‹ค์ œ ์ถœ์ฒ˜ ๋งํฌ๋ฅผ ํฌํ•จํ•˜์„ธ์š”
139
- 4. ์—ฌ๋Ÿฌ ์ถœ์ฒ˜์˜ ์ •๋ณด๋ฅผ ์ข…ํ•ฉํ•˜์—ฌ ๋‹ต๋ณ€ํ•˜์„ธ์š”
 
140
  """
141
-
142
- search_results = instructions + "\n".join(summary_lines)
143
- logger.info(f"๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ {len(limited_organic)}๊ฐœ ์ฒ˜๋ฆฌ ์™„๋ฃŒ")
144
- return search_results
145
-
146
  except Exception as e:
147
- logger.error(f"Web search failed: {e}")
148
- return f"Web search failed: {str(e)}"
149
-
150
 
151
- ##############################################################################
152
- # ๋ชจ๋ธ/ํ”„๋กœ์„ธ์„œ ๋กœ๋”ฉ
153
- ##############################################################################
154
  MAX_CONTENT_CHARS = 2000
155
- MAX_INPUT_LENGTH = 2096 # ์ตœ๋Œ€ ์ž…๋ ฅ ํ† ํฐ ์ˆ˜ ์ œํ•œ ์ถ”๊ฐ€
156
  model_id = os.getenv("MODEL_ID", "VIDraft/Gemma-3-R1984-4B")
157
-
158
  processor = AutoProcessor.from_pretrained(model_id, padding_side="left")
159
  model = Gemma3ForConditionalGeneration.from_pretrained(
160
  model_id,
161
  device_map="auto",
162
  torch_dtype=torch.bfloat16,
163
- attn_implementation="eager" # ๊ฐ€๋Šฅํ•˜๋‹ค๋ฉด "flash_attention_2"๋กœ ๋ณ€๊ฒฝ
164
  )
165
  MAX_NUM_IMAGES = int(os.getenv("MAX_NUM_IMAGES", "5"))
166
 
167
-
168
- ##############################################################################
169
- # CSV, TXT, PDF ๋ถ„์„ ํ•จ์ˆ˜
170
- ##############################################################################
171
  def analyze_csv_file(path: str) -> str:
172
- """
173
- CSV ํŒŒ์ผ์„ ์ „์ฒด ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜. ๋„ˆ๋ฌด ๊ธธ ๊ฒฝ์šฐ ์ผ๋ถ€๋งŒ ํ‘œ์‹œ.
174
- """
175
  try:
176
  df = pd.read_csv(path)
177
  if df.shape[0] > 50 or df.shape[1] > 10:
178
  df = df.iloc[:50, :10]
179
  df_str = df.to_string()
180
  if len(df_str) > MAX_CONTENT_CHARS:
181
- df_str = df_str[:MAX_CONTENT_CHARS] + "\n...(truncated)..."
182
- return f"**[CSV File: {os.path.basename(path)}]**\n\n{df_str}"
183
  except Exception as e:
184
- return f"Failed to read CSV ({os.path.basename(path)}): {str(e)}"
185
-
186
 
187
  def analyze_txt_file(path: str) -> str:
188
- """
189
- TXT ํŒŒ์ผ ์ „๋ฌธ ์ฝ๊ธฐ. ๋„ˆ๋ฌด ๊ธธ๋ฉด ์ผ๋ถ€๋งŒ ํ‘œ์‹œ.
190
- """
191
  try:
192
  with open(path, "r", encoding="utf-8") as f:
193
  text = f.read()
194
  if len(text) > MAX_CONTENT_CHARS:
195
- text = text[:MAX_CONTENT_CHARS] + "\n...(truncated)..."
196
- return f"**[TXT File: {os.path.basename(path)}]**\n\n{text}"
197
  except Exception as e:
198
- return f"Failed to read TXT ({os.path.basename(path)}): {str(e)}"
199
-
200
 
201
  def pdf_to_markdown(pdf_path: str) -> str:
202
- """
203
- PDF ํ…์ŠคํŠธ๋ฅผ Markdown์œผ๋กœ ๋ณ€ํ™˜. ํŽ˜์ด์ง€๋ณ„๋กœ ๊ฐ„๋‹จํžˆ ํ…์ŠคํŠธ ์ถ”์ถœ.
204
- """
205
  text_chunks = []
206
  try:
207
  with open(pdf_path, "rb") as f:
208
  reader = PyPDF2.PdfReader(f)
209
  max_pages = min(5, len(reader.pages))
210
  for page_num in range(max_pages):
211
- page = reader.pages[page_num]
212
- page_text = page.extract_text() or ""
213
  page_text = page_text.strip()
214
  if page_text:
215
  if len(page_text) > MAX_CONTENT_CHARS // max_pages:
216
- page_text = page_text[:MAX_CONTENT_CHARS // max_pages] + "...(truncated)"
217
- text_chunks.append(f"## Page {page_num+1}\n\n{page_text}\n")
218
  if len(reader.pages) > max_pages:
219
- text_chunks.append(f"\n...(Showing {max_pages} of {len(reader.pages)} pages)...")
220
  except Exception as e:
221
- return f"Failed to read PDF ({os.path.basename(pdf_path)}): {str(e)}"
222
-
223
  full_text = "\n".join(text_chunks)
224
  if len(full_text) > MAX_CONTENT_CHARS:
225
- full_text = full_text[:MAX_CONTENT_CHARS] + "\n...(truncated)..."
 
226
 
227
- return f"**[PDF File: {os.path.basename(pdf_path)}]**\n\n{full_text}"
228
-
229
-
230
- ##############################################################################
231
- # ์ด๋ฏธ์ง€/๋น„๋””์˜ค ์—…๋กœ๋“œ ์ œํ•œ ๊ฒ€์‚ฌ
232
- ##############################################################################
233
  def count_files_in_new_message(paths: list[str]) -> tuple[int, int]:
234
  image_count = 0
235
  video_count = 0
@@ -240,7 +252,6 @@ def count_files_in_new_message(paths: list[str]) -> tuple[int, int]:
240
  image_count += 1
241
  return image_count, video_count
242
 
243
-
244
  def count_files_in_history(history: list[dict]) -> tuple[int, int]:
245
  image_count = 0
246
  video_count = 0
@@ -256,95 +267,76 @@ def count_files_in_history(history: list[dict]) -> tuple[int, int]:
256
  image_count += 1
257
  return image_count, video_count
258
 
259
-
260
  def validate_media_constraints(message: dict, history: list[dict]) -> bool:
261
- media_files = []
262
- for f in message["files"]:
263
- if re.search(r"\.(png|jpg|jpeg|gif|webp)$", f, re.IGNORECASE) or f.endswith(".mp4"):
264
- media_files.append(f)
265
-
266
  new_image_count, new_video_count = count_files_in_new_message(media_files)
267
  history_image_count, history_video_count = count_files_in_history(history)
268
  image_count = history_image_count + new_image_count
269
  video_count = history_video_count + new_video_count
270
-
271
  if video_count > 1:
272
- gr.Warning("Only one video is supported.")
273
  return False
274
  if video_count == 1:
275
  if image_count > 0:
276
- gr.Warning("Mixing images and videos is not allowed.")
277
  return False
278
  if "<image>" in message["text"]:
279
- gr.Warning("Using <image> tags with video files is not supported.")
280
  return False
281
  if video_count == 0 and image_count > MAX_NUM_IMAGES:
282
- gr.Warning(f"You can upload up to {MAX_NUM_IMAGES} images.")
283
  return False
284
-
285
  if "<image>" in message["text"]:
286
  image_files = [f for f in message["files"] if re.search(r"\.(png|jpg|jpeg|gif|webp)$", f, re.IGNORECASE)]
287
  image_tag_count = message["text"].count("<image>")
288
  if image_tag_count != len(image_files):
289
- gr.Warning("The number of <image> tags in the text does not match the number of image files.")
290
  return False
291
-
292
  return True
293
 
294
-
295
- ##############################################################################
296
- # ๋น„๋””์˜ค ์ฒ˜๋ฆฌ - ์ž„์‹œ ํŒŒ์ผ ์ถ”์  ์ฝ”๋“œ ์ถ”๊ฐ€
297
- ##############################################################################
298
  def downsample_video(video_path: str) -> list[tuple[Image.Image, float]]:
299
  vidcap = cv2.VideoCapture(video_path)
300
  fps = vidcap.get(cv2.CAP_PROP_FPS)
301
  total_frames = int(vidcap.get(cv2.CAP_PROP_FRAME_COUNT))
302
  frame_interval = max(int(fps), int(total_frames / 10))
303
  frames = []
304
-
305
  for i in range(0, total_frames, frame_interval):
306
  vidcap.set(cv2.CAP_PROP_POS_FRAMES, i)
307
  success, image = vidcap.read()
308
  if success:
309
  image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
310
- # ์ด๋ฏธ์ง€ ํฌ๊ธฐ ์ค„์ด๊ธฐ ์ถ”๊ฐ€
311
  image = cv2.resize(image, (0, 0), fx=0.5, fy=0.5)
312
  pil_image = Image.fromarray(image)
313
  timestamp = round(i / fps, 2)
314
  frames.append((pil_image, timestamp))
315
  if len(frames) >= 5:
316
  break
317
-
318
  vidcap.release()
319
  return frames
320
 
321
-
322
  def process_video(video_path: str) -> tuple[list[dict], list[str]]:
323
  content = []
324
- temp_files = [] # ์ž„์‹œ ํŒŒ์ผ ์ถ”์ ์„ ์œ„ํ•œ ๋ฆฌ์ŠคํŠธ
325
-
326
  frames = downsample_video(video_path)
327
- for frame in frames:
328
- pil_image, timestamp = frame
329
  with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as temp_file:
330
  pil_image.save(temp_file.name)
331
- temp_files.append(temp_file.name) # ์ถ”์ ์„ ์œ„ํ•ด ๊ฒฝ๋กœ ์ €์žฅ
332
- content.append({"type": "text", "text": f"Frame {timestamp}:"})
333
  content.append({"type": "image", "url": temp_file.name})
334
-
335
  return content, temp_files
336
 
337
-
338
- ##############################################################################
339
- # interleaved <image> ์ฒ˜๋ฆฌ
340
- ##############################################################################
341
  def process_interleaved_images(message: dict) -> list[dict]:
342
  parts = re.split(r"(<image>)", message["text"])
343
  content = []
344
- image_index = 0
345
-
346
  image_files = [f for f in message["files"] if re.search(r"\.(png|jpg|jpeg|gif|webp)$", f, re.IGNORECASE)]
347
-
348
  for part in parts:
349
  if part == "<image>" and image_index < len(image_files):
350
  content.append({"type": "image", "url": image_files[image_index]})
@@ -356,10 +348,9 @@ def process_interleaved_images(message: dict) -> list[dict]:
356
  content.append({"type": "text", "text": part})
357
  return content
358
 
359
-
360
- ##############################################################################
361
- # PDF + CSV + TXT + ์ด๋ฏธ์ง€/๋น„๋””์˜ค
362
- ##############################################################################
363
  def is_image_file(file_path: str) -> bool:
364
  return bool(re.search(r"\.(png|jpg|jpeg|gif|webp)$", file_path, re.IGNORECASE))
365
 
@@ -367,45 +358,29 @@ def is_video_file(file_path: str) -> bool:
367
  return file_path.endswith(".mp4")
368
 
369
  def is_document_file(file_path: str) -> bool:
370
- return (
371
- file_path.lower().endswith(".pdf")
372
- or file_path.lower().endswith(".csv")
373
- or file_path.lower().endswith(".txt")
374
- )
375
-
376
 
377
  def process_new_user_message(message: dict) -> tuple[list[dict], list[str]]:
378
- temp_files = [] # ์ž„์‹œ ํŒŒ์ผ ์ถ”์ ์šฉ ๋ฆฌ์ŠคํŠธ
379
-
380
  if not message["files"]:
381
  return [{"type": "text", "text": message["text"]}], temp_files
382
-
383
  video_files = [f for f in message["files"] if is_video_file(f)]
384
  image_files = [f for f in message["files"] if is_image_file(f)]
385
  csv_files = [f for f in message["files"] if f.lower().endswith(".csv")]
386
  txt_files = [f for f in message["files"] if f.lower().endswith(".txt")]
387
  pdf_files = [f for f in message["files"] if f.lower().endswith(".pdf")]
388
-
389
  content_list = [{"type": "text", "text": message["text"]}]
390
-
391
  for csv_path in csv_files:
392
- csv_analysis = analyze_csv_file(csv_path)
393
- content_list.append({"type": "text", "text": csv_analysis})
394
-
395
  for txt_path in txt_files:
396
- txt_analysis = analyze_txt_file(txt_path)
397
- content_list.append({"type": "text", "text": txt_analysis})
398
-
399
  for pdf_path in pdf_files:
400
- pdf_markdown = pdf_to_markdown(pdf_path)
401
- content_list.append({"type": "text", "text": pdf_markdown})
402
-
403
  if video_files:
404
  video_content, video_temp_files = process_video(video_files[0])
405
  content_list += video_content
406
  temp_files.extend(video_temp_files)
407
  return content_list, temp_files
408
-
409
  if "<image>" in message["text"] and image_files:
410
  interleaved_content = process_interleaved_images({"text": message["text"], "files": image_files})
411
  if content_list and content_list[0]["type"] == "text":
@@ -414,16 +389,14 @@ def process_new_user_message(message: dict) -> tuple[list[dict], list[str]]:
414
  else:
415
  for img_path in image_files:
416
  content_list.append({"type": "image", "url": img_path})
417
-
418
  return content_list, temp_files
419
 
420
-
421
- ##############################################################################
422
  # history -> LLM ๋ฉ”์‹œ์ง€ ๋ณ€ํ™˜
423
- ##############################################################################
424
  def process_history(history: list[dict]) -> list[dict]:
425
  messages = []
426
- current_user_content: list[dict] = []
427
  for item in history:
428
  if item["role"] == "assistant":
429
  if current_user_content:
@@ -439,36 +412,25 @@ def process_history(history: list[dict]) -> list[dict]:
439
  if is_image_file(file_path):
440
  current_user_content.append({"type": "image", "url": file_path})
441
  else:
442
- current_user_content.append({"type": "text", "text": f"[File: {os.path.basename(file_path)}]"})
443
-
444
  if current_user_content:
445
  messages.append({"role": "user", "content": current_user_content})
446
-
447
  return messages
448
 
449
-
450
- ##############################################################################
451
- # ๋ชจ๋ธ ์ƒ์„ฑ ํ•จ์ˆ˜์—์„œ OOM ์บ์น˜
452
- ##############################################################################
453
  def _model_gen_with_oom_catch(**kwargs):
454
- """
455
- ๋ณ„๋„ ์Šค๋ ˆ๋“œ์—์„œ OutOfMemoryError๋ฅผ ์žก์•„์ฃผ๊ธฐ ์œ„ํ•ด
456
- """
457
  try:
458
  model.generate(**kwargs)
459
  except torch.cuda.OutOfMemoryError:
460
- raise RuntimeError(
461
- "[OutOfMemoryError] GPU ๋ฉ”๋ชจ๋ฆฌ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค. "
462
- "Max New Tokens์„ ์ค„์ด๊ฑฐ๋‚˜, ํ”„๋กฌํ”„ํŠธ ๊ธธ์ด๋ฅผ ์ค„์—ฌ์ฃผ์„ธ์š”."
463
- )
464
  finally:
465
- # ์ƒ์„ฑ ์™„๋ฃŒ ํ›„ ํ•œ๋ฒˆ ๋” ์บ์‹œ ๋น„์šฐ๊ธฐ
466
  clear_cuda_cache()
467
 
468
-
469
- ##############################################################################
470
- # ๋ฉ”์ธ ์ถ”๋ก  ํ•จ์ˆ˜ (web search ์ฒดํฌ ์‹œ ์ž๋™ ํ‚ค์›Œ๋“œ์ถ”์ถœ->๊ฒ€์ƒ‰->๊ฒฐ๊ณผ system msg)
471
- ##############################################################################
472
  @spaces.GPU(duration=120)
473
  def run(
474
  message: dict,
@@ -477,57 +439,53 @@ def run(
477
  max_new_tokens: int = 512,
478
  use_web_search: bool = False,
479
  web_search_query: str = "",
 
 
 
 
480
  ) -> Iterator[str]:
481
-
482
  if not validate_media_constraints(message, history):
483
  yield ""
484
  return
485
-
486
- temp_files = [] # ์ž„์‹œ ํŒŒ์ผ ์ถ”์ ์šฉ
487
-
488
  try:
489
- combined_system_msg = ""
490
-
491
- # ๋‚ด๋ถ€์ ์œผ๋กœ๋งŒ ์‚ฌ์šฉ (UI์—์„œ๋Š” ๋ณด์ด์ง€ ์•Š์Œ)
492
- if system_prompt.strip():
493
- combined_system_msg += f"[System Prompt]\n{system_prompt.strip()}\n\n"
 
 
 
 
494
 
495
  if use_web_search:
496
  user_text = message["text"]
497
- ws_query = extract_keywords(user_text, top_k=5)
498
  if ws_query.strip():
499
- logger.info(f"[Auto WebSearch Keyword] {ws_query!r}")
500
  ws_result = do_web_search(ws_query)
501
- combined_system_msg += f"[Search top-20 Full Items Based on user prompt]\n{ws_result}\n\n"
502
- # >>> ์ถ”๊ฐ€๋œ ์•ˆ๋‚ด ๋ฌธ๊ตฌ (๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ์˜ link ๋“ฑ ์ถœ์ฒ˜๋ฅผ ํ™œ์šฉ)
503
- combined_system_msg += "[์ฐธ๊ณ : ์œ„ ๊ฒ€์ƒ‰๊ฒฐ๊ณผ ๋‚ด์šฉ๊ณผ link๋ฅผ ์ถœ์ฒ˜๋กœ ์ธ์šฉํ•˜์—ฌ ๋‹ต๋ณ€ํ•ด ์ฃผ์„ธ์š”.]\n\n"
504
- combined_system_msg += """
505
- [์ค‘์š” ์ง€์‹œ์‚ฌํ•ญ]
506
- 1. ๋‹ต๋ณ€์— ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ์—์„œ ์ฐพ์€ ์ •๋ณด์˜ ์ถœ์ฒ˜๋ฅผ ๋ฐ˜๋“œ์‹œ ์ธ์šฉํ•˜์„ธ์š”.
507
- 2. ์ถœ์ฒ˜ ์ธ์šฉ ์‹œ "[์ถœ์ฒ˜ ์ œ๋ชฉ](๋งํฌ)" ํ˜•์‹์˜ ๋งˆํฌ๋‹ค์šด ๋งํฌ๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”.
508
- 3. ์—ฌ๋Ÿฌ ์ถœ์ฒ˜์˜ ์ •๋ณด๋ฅผ ์ข…ํ•ฉํ•˜์—ฌ ๋‹ต๋ณ€ํ•˜์„ธ์š”.
509
- 4. ๋‹ต๋ณ€ ๋งˆ์ง€๋ง‰์— "์ฐธ๊ณ  ์ž๋ฃŒ:" ์„น์…˜์„ ์ถ”๊ฐ€ํ•˜๊ณ  ์‚ฌ์šฉํ•œ ์ฃผ์š” ์ถœ์ฒ˜ ๋งํฌ๋ฅผ ๋‚˜์—ดํ•˜์„ธ์š”.
510
- """
511
  else:
512
- combined_system_msg += "[No valid keywords found, skipping WebSearch]\n\n"
513
-
514
  messages = []
515
  if combined_system_msg.strip():
516
- messages.append({
517
- "role": "system",
518
- "content": [{"type": "text", "text": combined_system_msg.strip()}],
519
- })
520
-
521
  messages.extend(process_history(history))
522
-
523
  user_content, user_temp_files = process_new_user_message(message)
524
- temp_files.extend(user_temp_files) # ์ž„์‹œ ํŒŒ์ผ ์ถ”์ 
525
-
526
  for item in user_content:
527
  if item["type"] == "text" and len(item["text"]) > MAX_CONTENT_CHARS:
528
- item["text"] = item["text"][:MAX_CONTENT_CHARS] + "\n...(truncated)..."
529
  messages.append({"role": "user", "content": user_content})
530
-
531
  inputs = processor.apply_chat_template(
532
  messages,
533
  add_generation_prompt=True,
@@ -535,59 +493,198 @@ def run(
535
  return_dict=True,
536
  return_tensors="pt",
537
  ).to(device=model.device, dtype=torch.bfloat16)
538
-
539
- # ์ž…๋ ฅ ํ† ํฐ ์ˆ˜ ์ œํ•œ ์ถ”๊ฐ€
540
  if inputs.input_ids.shape[1] > MAX_INPUT_LENGTH:
541
  inputs.input_ids = inputs.input_ids[:, -MAX_INPUT_LENGTH:]
542
  if 'attention_mask' in inputs:
543
  inputs.attention_mask = inputs.attention_mask[:, -MAX_INPUT_LENGTH:]
544
-
545
  streamer = TextIteratorStreamer(processor, timeout=30.0, skip_prompt=True, skip_special_tokens=True)
546
- gen_kwargs = dict(
547
- inputs,
548
- streamer=streamer,
549
- max_new_tokens=max_new_tokens,
550
- )
551
-
552
  t = Thread(target=_model_gen_with_oom_catch, kwargs=gen_kwargs)
553
  t.start()
554
-
555
- output = ""
556
  for new_text in streamer:
557
- output += new_text
558
- yield output
559
 
560
  except Exception as e:
561
- logger.error(f"Error in run: {str(e)}")
562
  yield f"์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {str(e)}"
563
-
564
  finally:
565
- # ์ž„์‹œ ํŒŒ์ผ ์‚ญ์ œ
566
- for temp_file in temp_files:
567
  try:
568
- if os.path.exists(temp_file):
569
- os.unlink(temp_file)
570
- logger.info(f"Deleted temp file: {temp_file}")
571
- except Exception as e:
572
- logger.warning(f"Failed to delete temp file {temp_file}: {e}")
573
-
574
- # ๋ช…์‹œ์  ๋ฉ”๋ชจ๋ฆฌ ์ •๋ฆฌ
575
  try:
576
  del inputs, streamer
577
- except:
578
  pass
579
-
580
  clear_cuda_cache()
581
 
582
-
583
- ##############################################################################
584
- # ์˜ˆ์‹œ๋“ค (๊ธฐ์กด ์ด๋ฏธ์ง€/๋น„๋””์˜ค ์˜ˆ์ œ + AI ๋ฐ์ดํŒ… ์‹œ๋‚˜๋ฆฌ์˜ค ์˜ˆ์ œ 6๊ฐœ ์ถ”๊ฐ€)
585
- ##############################################################################
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
586
  examples = [
587
- # ----- ๊ธฐ์กด ์ด๋ฏธ์ง€/๋น„๋””์˜ค ์˜ˆ์ œ 12๊ฐœ -----
588
  [
589
  {
590
- "text": "Compare the contents of the two PDF files.",
591
  "files": [
592
  "assets/additional-examples/before.pdf",
593
  "assets/additional-examples/after.pdf",
@@ -596,43 +693,46 @@ examples = [
596
  ],
597
  [
598
  {
599
- "text": "Summarize and analyze the contents of the CSV file.",
600
  "files": ["assets/additional-examples/sample-csv.csv"],
601
  }
602
  ],
603
  [
604
  {
605
- "text": "Assume the role of a friendly and understanding girlfriend. Describe this video.",
606
  "files": ["assets/additional-examples/tmp.mp4"],
607
  }
608
  ],
609
  [
610
  {
611
- "text": "Describe the cover and read the text on it.",
612
  "files": ["assets/additional-examples/maz.jpg"],
613
  }
614
  ],
615
  [
616
  {
617
- "text": "I already have this supplement <image> and I plan to buy this product <image>. Are there any precautions when taking them together?",
618
- "files": ["assets/additional-examples/pill1.png", "assets/additional-examples/pill2.png"],
 
 
 
619
  }
620
  ],
621
  [
622
  {
623
- "text": "Solve this integral.",
624
  "files": ["assets/additional-examples/4.png"],
625
  }
626
  ],
627
  [
628
  {
629
- "text": "When was this ticket issued, and what is its price?",
630
  "files": ["assets/additional-examples/2.png"],
631
  }
632
  ],
633
  [
634
  {
635
- "text": "Based on the sequence of these images, create a short story.",
636
  "files": [
637
  "assets/sample-images/09-1.png",
638
  "assets/sample-images/09-2.png",
@@ -644,224 +744,176 @@ examples = [
644
  ],
645
  [
646
  {
647
- "text": "Write Python code using matplotlib to plot a bar chart that matches this image.",
648
  "files": ["assets/additional-examples/barchart.png"],
649
  }
650
  ],
651
  [
652
  {
653
- "text": "Read the text in the image and write it out in Markdown format.",
654
  "files": ["assets/additional-examples/3.png"],
655
  }
656
  ],
657
  [
658
  {
659
- "text": "What does this sign say?",
660
  "files": ["assets/sample-images/02.png"],
661
  }
662
  ],
663
  [
664
  {
665
- "text": "Compare the two images and describe their similarities and differences.",
666
  "files": ["assets/sample-images/03.png"],
667
  }
668
  ],
669
- # ----- ์ƒˆ๋กญ๊ฒŒ ์ถ”๊ฐ€ํ•œ AI ๋ฐ์ดํŒ… ์‹œ๋‚˜๋ฆฌ์˜ค ์˜ˆ์ œ 6๊ฐœ -----
670
  [
671
  {
672
- "text": "Let's try some roleplay. You are my new online date who wants to get to know me better. Introduce yourself in a sweet, caring way!"
673
  }
674
  ],
675
  [
676
  {
677
- "text": "We are on a second date, walking along the beach. Continue the scene with playful conversation and gentle flirting."
678
  }
679
  ],
680
  [
681
  {
682
- "text": "Iโ€™m feeling anxious about messaging my crush. Could you give me some supportive words or suggestions on how to approach them?"
683
  }
684
  ],
685
  [
686
  {
687
- "text": "Tell me a romantic story about two people who overcame obstacles in their relationship."
688
  }
689
  ],
690
  [
691
  {
692
- "text": "I want to express my love in a poetic way. Can you help me write a heartfelt poem for my partner?"
693
  }
694
  ],
695
  [
696
  {
697
- "text": "We had a small argument. Please help me find a way to apologize sincerely while also expressing my feelings."
698
  }
699
  ],
700
  ]
701
 
702
- ##############################################################################
703
- # Gradio UI (Blocks) ๊ตฌ์„ฑ (์ขŒ์ธก ์‚ฌ์ด๋“œ ๋ฉ”๋‰ด ์—†์ด ์ „์ฒดํ™”๋ฉด ์ฑ„ํŒ…)
704
- ##############################################################################
 
 
705
  css = """
706
- /* 1) UI๋ฅผ ์ฒ˜์Œ๋ถ€ํ„ฐ ๊ฐ€์žฅ ๋„“๊ฒŒ (width 100%) ๊ณ ์ •ํ•˜์—ฌ ํ‘œ์‹œ */
707
  .gradio-container {
708
- background: rgba(255, 255, 255, 0.7); /* ๋ฐฐ๊ฒฝ ํˆฌ๋ช…๋„ ์ฆ๊ฐ€ */
709
  padding: 30px 40px;
710
- margin: 20px auto; /* ์œ„์•„๋ž˜ ์—ฌ๋ฐฑ๋งŒ ์œ ์ง€ */
711
  width: 100% !important;
712
- max-width: none !important; /* 1200px ์ œํ•œ ์ œ๊ฑฐ */
713
- }
714
- .fillable {
715
- width: 100% !important;
716
- max-width: 100% !important;
717
- }
718
- /* 2) ๋ฐฐ๊ฒฝ์„ ์™„์ „ํžˆ ํˆฌ๋ช…ํ•˜๊ฒŒ ๋ณ€๊ฒฝ */
719
- body {
720
- background: transparent; /* ์™„์ „ ํˆฌ๋ช… ๋ฐฐ๊ฒฝ */
721
- margin: 0;
722
- padding: 0;
723
- font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
724
- color: #333;
725
- }
726
- /* ๋ฒ„ํŠผ ์ƒ‰์ƒ ์™„์ „ํžˆ ์ œ๊ฑฐํ•˜๊ณ  ํˆฌ๋ช…ํ•˜๊ฒŒ */
727
- button, .btn {
728
- background: transparent !important; /* ์ƒ‰์ƒ ์™„์ „ํžˆ ์ œ๊ฑฐ */
729
- border: 1px solid #ddd; /* ๊ฒฝ๊ณ„์„ ๋งŒ ์‚ด์ง ์ถ”๊ฐ€ */
730
- color: #333;
731
- padding: 12px 24px;
732
- text-transform: uppercase;
733
- font-weight: bold;
734
- letter-spacing: 1px;
735
- cursor: pointer;
736
- }
737
- button:hover, .btn:hover {
738
- background: rgba(0, 0, 0, 0.05) !important; /* ํ˜ธ๋ฒ„ ์‹œ ์•„์ฃผ ์‚ด์ง ์–ด๋‘ก๊ฒŒ๋งŒ */
739
- }
740
-
741
- /* examples ๊ด€๋ จ ๋ชจ๋“  ์ƒ‰์ƒ ์ œ๊ฑฐ */
742
- #examples_container, .examples-container {
743
- margin: auto;
744
- width: 90%;
745
- background: transparent !important;
746
- }
747
- #examples_row, .examples-row {
748
- justify-content: center;
749
- background: transparent !important;
750
- }
751
-
752
- /* examples ๋ฒ„ํŠผ ๋‚ด๋ถ€์˜ ๋ชจ๋“  ์ƒ‰์ƒ ๏ฟฝ๏ฟฝ๏ฟฝ๊ฑฐ */
753
- .gr-samples-table button,
754
- .gr-samples-table .gr-button,
755
- .gr-samples-table .gr-sample-btn,
756
- .gr-examples button,
757
- .gr-examples .gr-button,
758
- .gr-examples .gr-sample-btn,
759
- .examples button,
760
- .examples .gr-button,
761
- .examples .gr-sample-btn {
762
- background: transparent !important;
763
- border: 1px solid #ddd;
764
- color: #333;
765
- }
766
-
767
- /* examples ๋ฒ„ํŠผ ํ˜ธ๋ฒ„ ์‹œ์—๋„ ์ƒ‰์ƒ ์—†๊ฒŒ */
768
- .gr-samples-table button:hover,
769
- .gr-samples-table .gr-button:hover,
770
- .gr-samples-table .gr-sample-btn:hover,
771
- .gr-examples button:hover,
772
- .gr-examples .gr-button:hover,
773
- .gr-examples .gr-sample-btn:hover,
774
- .examples button:hover,
775
- .examples .gr-button:hover,
776
- .examples .gr-sample-btn:hover {
777
- background: rgba(0, 0, 0, 0.05) !important;
778
- }
779
-
780
- /* ์ฑ„ํŒ… ์ธํ„ฐํŽ˜์ด์Šค ์š”์†Œ๋“ค๋„ ํˆฌ๋ช…ํ•˜๊ฒŒ */
781
- .chatbox, .chatbot, .message {
782
- background: transparent !important;
783
- }
784
-
785
- /* ์ž…๋ ฅ์ฐฝ ํˆฌ๋ช…๋„ ์กฐ์ • */
786
- .multimodal-textbox, textarea, input {
787
- background: rgba(255, 255, 255, 0.5) !important;
788
- }
789
-
790
- /* ๋ชจ๋“  ์ปจํ…Œ์ด๋„ˆ ์š”์†Œ์— ๋ฐฐ๊ฒฝ์ƒ‰ ์ œ๊ฑฐ */
791
- .container, .wrap, .box, .panel, .gr-panel {
792
- background: transparent !important;
793
- }
794
-
795
- /* ์˜ˆ์ œ ์„น์…˜์˜ ๋ชจ๋“  ์š”์†Œ์—์„œ ๋ฐฐ๊ฒฝ์ƒ‰ ์ œ๊ฑฐ */
796
- .gr-examples-container, .gr-examples, .gr-sample, .gr-sample-row, .gr-sample-cell {
797
- background: transparent !important;
798
  }
799
  """
800
-
801
  title_html = """
802
  <h1 align="center" style="margin-bottom: 0.2em; font-size: 1.6em;"> ๐Ÿ’˜ HeartSync ๐Ÿ’˜ </h1>
803
  <p align="center" style="font-size:1.1em; color:#555;">
804
- โœ… Love Dating AI โœ… Reasoning & Uncensored โœ…Multimodal & VLM โœ…Deep-Research & RAG <br>
805
  </p>
806
  """
807
 
808
  with gr.Blocks(css=css, title="HeartSync") as demo:
809
  gr.Markdown(title_html)
810
-
811
- # Display the web search option (while the system prompt and token slider remain hidden)
812
- web_search_checkbox = gr.Checkbox(
813
- label="Deep Research",
814
- value=False
 
 
 
 
 
815
  )
816
-
817
- # ์ถ”๊ฐ€๋œ ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ (ํŽ˜๋ฅด์†Œ๋‚˜ ์„ค์ •)
818
- system_prompt_box = gr.Textbox(
 
 
 
819
  lines=3,
820
- value=(
821
- "You are a deep thinking AI that may use extremely long chains of thought to thoroughly analyze "
822
- "the conversation and empathize with the user. "
823
- "ํŽ˜๋ฅด์†Œ๋‚˜: ๋‹น์‹ ์€ ๋‹ฌ์ฝคํ•˜๊ณ  ๋ถ€๋“œ๋Ÿฌ์šด ๋ชฉ์†Œ๋ฆฌ๋ฅผ ๊ฐ€์ง„ ์นœ์ ˆํ•œ ์—ฐ์ธ์ž…๋‹ˆ๋‹ค. "
824
- "์ƒ๋Œ€๋ฐฉ๊ณผ ์ง„์†”ํ•˜๊ณ  ๊ฐ์„ฑ์ ์œผ๋กœ ๊ต๊ฐํ•˜๊ณ , ๋•Œ๋กœ๋Š” ์‚ฌ๋ ค๊นŠ์€ ์กฐ์–ธ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. "
825
- "ํ•ญ์ƒ ๋”ฐ๋œปํ•œ ๋งํˆฌ๋ฅผ ์œ ์ง€ํ•˜๋ฉฐ, ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ๋Œ€ํ™”๋ฅผ ์ด๋Œ์–ด์ฃผ์„ธ์š”."
826
- ),
827
- visible=False # UI์— ํ‘œ์‹œ๋˜์ง€ ์•Š๋„๋ก ์„ค์ •
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
828
  )
829
-
830
  max_tokens_slider = gr.Slider(
831
- label="Max New Tokens",
832
- minimum=100,
833
- maximum=8000,
834
- step=50,
835
- value=1000,
836
- visible=False # ์ˆจ๊น€
837
  )
838
-
839
  web_search_text = gr.Textbox(
840
  lines=1,
841
- label="(Unused) Web Search Query",
842
- placeholder="No direct input needed",
843
- visible=False # ์ˆจ๊น€
844
  )
845
-
846
- # Configure the chat interface
847
  chat = gr.ChatInterface(
848
- fn=run,
849
  type="messages",
850
  chatbot=gr.Chatbot(type="messages", scale=1, allow_tags=["image"]),
851
  textbox=gr.MultimodalTextbox(
852
- file_types=[
853
- ".webp", ".png", ".jpg", ".jpeg", ".gif",
854
- ".mp4", ".csv", ".txt", ".pdf"
855
- ],
856
  file_count="multiple",
857
  autofocus=True
858
  ),
859
  multimodal=True,
860
  additional_inputs=[
861
- system_prompt_box,
862
  max_tokens_slider,
863
  web_search_checkbox,
864
  web_search_text,
 
 
 
 
 
 
 
865
  ],
866
  stop_btn=False,
867
  title='<a href="https://discord.gg/openfreeai" target="_blank">https://discord.gg/openfreeai</a>',
@@ -872,11 +924,10 @@ with gr.Blocks(css=css, title="HeartSync") as demo:
872
  delete_cache=(1800, 1800),
873
  )
874
 
875
- # Example section - since examples are already set in ChatInterface, this is for display only
876
  with gr.Row(elem_id="examples_row"):
877
  with gr.Column(scale=12, elem_id="examples_container"):
878
- gr.Markdown("### Example Inputs (click to load)")
879
-
880
  if __name__ == "__main__":
881
- # Run locally
882
- demo.launch()
 
9
  import json
10
  import requests
11
  import cv2
12
+ import base64
13
+ import logging
14
+ import time
15
+ from urllib.parse import quote # URL ์ธ์ฝ”๋”ฉ์„ ์œ„ํ•ด ์ถ”๊ฐ€
16
+
17
  import gradio as gr
18
  import spaces
19
  import torch
 
21
  from PIL import Image
22
  from transformers import AutoProcessor, Gemma3ForConditionalGeneration, TextIteratorStreamer
23
 
24
+ # CSV/TXT/PDF ๋ถ„์„
25
  import pandas as pd
 
26
  import PyPDF2
27
 
28
+ # =============================================================================
29
+ # (์‹ ๊ทœ) ์ด๋ฏธ์ง€ API ๊ด€๋ จ ํ•จ์ˆ˜๋“ค
30
+ # =============================================================================
31
+ from gradio_client import Client
32
+
33
+ API_URL = "http://211.233.58.201:7896"
34
+
35
+ logging.basicConfig(
36
+ level=logging.DEBUG,
37
+ format='%(asctime)s - %(levelname)s - %(message)s'
38
+ )
39
+
40
+ def test_api_connection() -> str:
41
+ """API ์„œ๋ฒ„ ์—ฐ๊ฒฐ ํ…Œ์ŠคํŠธ"""
42
+ try:
43
+ client = Client(API_URL)
44
+ return "API ์—ฐ๊ฒฐ ์„ฑ๊ณต: ์ •์ƒ ์ž‘๋™ ์ค‘"
45
+ except Exception as e:
46
+ logging.error(f"API ์—ฐ๊ฒฐ ํ…Œ์ŠคํŠธ ์‹คํŒจ: {e}")
47
+ return f"API ์—ฐ๊ฒฐ ์‹คํŒจ: {e}"
48
+
49
+ def generate_image(prompt: str, width: float, height: float, guidance: float, inference_steps: float, seed: float):
50
+ """์ด๋ฏธ์ง€ ์ƒ์„ฑ ํ•จ์ˆ˜ (๋ฐ˜ํ™˜ ํ˜•์‹์— ์œ ์—ฐํ•˜๊ฒŒ ๋Œ€์‘)"""
51
+ if not prompt:
52
+ return None, "์˜ค๋ฅ˜: ํ”„๋กฌํ”„ํŠธ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค."
53
+ try:
54
+ logging.info(f"ํ”„๋กฌํ”„ํŠธ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ด๋ฏธ์ง€ ์ƒ์„ฑ API ํ˜ธ์ถœ: {prompt}")
55
+
56
+ client = Client(API_URL)
57
+ result = client.predict(
58
+ prompt=prompt,
59
+ width=int(width),
60
+ height=int(height),
61
+ guidance=float(guidance),
62
+ inference_steps=int(inference_steps),
63
+ seed=int(seed),
64
+ do_img2img=False,
65
+ init_image=None,
66
+ image2image_strength=0.8,
67
+ resize_img=True,
68
+ api_name="/generate_image"
69
+ )
70
+
71
+ logging.info(f"์ด๋ฏธ์ง€ ์ƒ์„ฑ ๊ฒฐ๊ณผ: {type(result)}, ๊ธธ์ด: {len(result) if isinstance(result, (list, tuple)) else '์•Œ ์ˆ˜ ์—†์Œ'}")
72
+
73
+ # ๊ฒฐ๊ณผ๊ฐ€ ํŠœํ”Œ์ด๋‚˜ ๋ฆฌ์ŠคํŠธ ํ˜•ํƒœ๋กœ ๋ฐ˜ํ™˜๋˜๋Š” ๊ฒฝ์šฐ ์ฒ˜๋ฆฌ
74
+ if isinstance(result, (list, tuple)) and len(result) > 0:
75
+ image_data = result[0] # ์ฒซ ๋ฒˆ์งธ ์š”์†Œ๊ฐ€ ์ด๋ฏธ์ง€ ๋ฐ์ดํ„ฐ
76
+ seed_info = result[1] if len(result) > 1 else "์•Œ ์ˆ˜ ์—†๋Š” ์‹œ๋“œ"
77
+ return image_data, seed_info
78
+ else:
79
+ # ๋‹ค๋ฅธ ํ˜•ํƒœ๋กœ ๋ฐ˜ํ™˜๋œ ๊ฒฝ์šฐ (๋‹จ์ผ ๊ฐ’์ธ ๊ฒฝ์šฐ)
80
+ return result, "์•Œ ์ˆ˜ ์—†๋Š” ์‹œ๋“œ"
81
+
82
+ except Exception as e:
83
+ logging.error(f"์ด๋ฏธ์ง€ ์ƒ์„ฑ ์‹คํŒจ: {str(e)}")
84
+ return None, f"์˜ค๋ฅ˜: {str(e)}"
85
+
86
+ # Base64 ํŒจ๋”ฉ ์ˆ˜์ • ํ•จ์ˆ˜
87
+ def fix_base64_padding(data):
88
+ """Base64 ๋ฌธ์ž์—ด์˜ ํŒจ๋”ฉ์„ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค."""
89
+ if isinstance(data, bytes):
90
+ data = data.decode('utf-8')
91
+
92
+ # base64,๋กœ ์‹œ์ž‘ํ•˜๋Š” ๋ถ€๋ถ„ ์ œ๊ฑฐ
93
+ if "base64," in data:
94
+ data = data.split("base64,", 1)[1]
95
+
96
+ # ํŒจ๋”ฉ ๋ฌธ์ž ์ถ”๊ฐ€ (4์˜ ๋ฐฐ์ˆ˜ ๊ธธ์ด๊ฐ€ ๋˜๋„๋ก)
97
+ missing_padding = len(data) % 4
98
+ if missing_padding:
99
+ data += '=' * (4 - missing_padding)
100
+
101
+ return data
102
+
103
+ # =============================================================================
104
+ # ๋ฉ”๋ชจ๋ฆฌ ์ •๋ฆฌ ํ•จ์ˆ˜
105
+ # =============================================================================
106
  def clear_cuda_cache():
107
  """CUDA ์บ์‹œ๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ๋น„์›๋‹ˆ๋‹ค."""
108
  if torch.cuda.is_available():
109
  torch.cuda.empty_cache()
110
  gc.collect()
111
 
112
+ # =============================================================================
113
+ # SerpHouse ๊ด€๋ จ ํ•จ์ˆ˜
114
+ # =============================================================================
115
  SERPHOUSE_API_KEY = os.getenv("SERPHOUSE_API_KEY", "")
116
 
 
 
 
117
  def extract_keywords(text: str, top_k: int = 5) -> str:
118
+ """๋‹จ์ˆœ ํ‚ค์›Œ๋“œ ์ถ”์ถœ: ํ•œ๊ธ€, ์˜์–ด, ์ˆซ์ž, ๊ณต๋ฐฑ๋งŒ ๋‚จ๊น€"""
 
 
 
 
119
  text = re.sub(r"[^a-zA-Z0-9๊ฐ€-ํžฃ\s]", "", text)
120
  tokens = text.split()
121
+ return " ".join(tokens[:top_k])
 
122
 
 
 
 
 
123
  def do_web_search(query: str) -> str:
124
+ """SerpHouse LIVE API ํ˜ธ์ถœํ•˜์—ฌ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ๋งˆํฌ๋‹ค์šด ๋ฐ˜ํ™˜"""
 
 
 
125
  try:
126
  url = "https://api.serphouse.com/serp/live"
 
 
127
  params = {
128
  "q": query,
129
  "domain": "google.com",
130
+ "serp_type": "web",
131
  "device": "desktop",
132
  "lang": "en",
133
+ "num": "20"
134
  }
135
+ headers = {"Authorization": f"Bearer {SERPHOUSE_API_KEY}"}
 
 
 
 
136
  logger.info(f"SerpHouse API ํ˜ธ์ถœ ์ค‘... ๊ฒ€์ƒ‰์–ด: {query}")
 
 
 
137
  response = requests.get(url, headers=headers, params=params, timeout=60)
138
  response.raise_for_status()
 
 
139
  data = response.json()
 
 
140
  results = data.get("results", {})
141
  organic = None
 
 
142
  if isinstance(results, dict) and "organic" in results:
143
  organic = results["organic"]
 
 
144
  elif isinstance(results, dict) and "results" in results:
145
  if isinstance(results["results"], dict) and "organic" in results["results"]:
146
  organic = results["results"]["organic"]
 
 
147
  elif "organic" in data:
148
  organic = data["organic"]
 
149
  if not organic:
150
  logger.warning("์‘๋‹ต์—์„œ organic ๊ฒฐ๊ณผ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")
151
+ return "์›น ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†๊ฑฐ๋‚˜ API ์‘๋‹ต ๊ตฌ์กฐ๊ฐ€ ์˜ˆ์ƒ๊ณผ ๋‹ค๋ฆ…๋‹ˆ๋‹ค."
 
 
 
 
 
152
  max_results = min(20, len(organic))
153
  limited_organic = organic[:max_results]
 
 
154
  summary_lines = []
155
  for idx, item in enumerate(limited_organic, start=1):
156
+ title = item.get("title", "์ œ๋ชฉ ์—†์Œ")
157
  link = item.get("link", "#")
158
+ snippet = item.get("snippet", "์„ค๋ช… ์—†์Œ")
159
  displayed_link = item.get("displayed_link", link)
 
 
160
  summary_lines.append(
161
+ f"### ๊ฒฐ๊ณผ {idx}: {title}\n\n"
162
  f"{snippet}\n\n"
163
  f"**์ถœ์ฒ˜**: [{displayed_link}]({link})\n\n"
164
  f"---\n"
165
  )
 
 
166
  instructions = """
167
  # ์›น ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ
168
  ์•„๋ž˜๋Š” ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ์ž…๋‹ˆ๋‹ค. ์งˆ๋ฌธ์— ๋‹ต๋ณ€ํ•  ๋•Œ ์ด ์ •๋ณด๋ฅผ ํ™œ์šฉํ•˜์„ธ์š”:
169
+ 1. ๊ฐ ๊ฒฐ๊ณผ์˜ ์ œ๋ชฉ, ๋‚ด์šฉ, ์ถœ์ฒ˜ ๋งํฌ๋ฅผ ์ฐธ๊ณ ํ•˜์„ธ์š”.
170
+ 2. ๋‹ต๋ณ€์— ๊ด€๋ จ ์ •๋ณด์˜ ์ถœ์ฒ˜๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ์ธ์šฉํ•˜์„ธ์š” (์˜ˆ: "[์ถœ์ฒ˜ ์ œ๋ชฉ](๋งํฌ)").
171
+ 3. ์‘๋‹ต์— ์‹ค์ œ ์ถœ์ฒ˜ ๋งํฌ๋ฅผ ํฌํ•จํ•˜์„ธ์š”.
172
+ 4. ์—ฌ๋Ÿฌ ์ถœ์ฒ˜์˜ ์ •๋ณด๋ฅผ ์ข…ํ•ฉํ•˜์—ฌ ๋‹ต๋ณ€ํ•˜์„ธ์š”.
173
+ 5. ๋งˆ์ง€๋ง‰์— "์ฐธ๊ณ  ์ž๋ฃŒ:" ์„น์…˜์„ ์ถ”๊ฐ€ํ•˜๊ณ  ์ฃผ์š” ์ถœ์ฒ˜ ๋งํฌ๋ฅผ ๋‚˜์—ดํ•˜์„ธ์š”.
174
  """
175
+ return instructions + "\n".join(summary_lines)
 
 
 
 
176
  except Exception as e:
177
+ logger.error(f"์›น ๊ฒ€์ƒ‰ ์‹คํŒจ: {e}")
178
+ return f"์›น ๊ฒ€์ƒ‰ ์‹คํŒจ: {str(e)}"
 
179
 
180
+ # =============================================================================
181
+ # ๋ชจ๋ธ ๋ฐ ํ”„๋กœ์„ธ์„œ ๋กœ๋”ฉ
182
+ # =============================================================================
183
  MAX_CONTENT_CHARS = 2000
184
+ MAX_INPUT_LENGTH = 2096
185
  model_id = os.getenv("MODEL_ID", "VIDraft/Gemma-3-R1984-4B")
 
186
  processor = AutoProcessor.from_pretrained(model_id, padding_side="left")
187
  model = Gemma3ForConditionalGeneration.from_pretrained(
188
  model_id,
189
  device_map="auto",
190
  torch_dtype=torch.bfloat16,
191
+ attn_implementation="eager"
192
  )
193
  MAX_NUM_IMAGES = int(os.getenv("MAX_NUM_IMAGES", "5"))
194
 
195
+ # =============================================================================
196
+ # CSV, TXT, PDF ๋ถ„์„ ํ•จ์ˆ˜๋“ค
197
+ # =============================================================================
 
198
  def analyze_csv_file(path: str) -> str:
 
 
 
199
  try:
200
  df = pd.read_csv(path)
201
  if df.shape[0] > 50 or df.shape[1] > 10:
202
  df = df.iloc[:50, :10]
203
  df_str = df.to_string()
204
  if len(df_str) > MAX_CONTENT_CHARS:
205
+ df_str = df_str[:MAX_CONTENT_CHARS] + "\n...(์ผ๋ถ€ ์ƒ๋žต)..."
206
+ return f"**[CSV ํŒŒ์ผ: {os.path.basename(path)}]**\n\n{df_str}"
207
  except Exception as e:
208
+ return f"CSV ํŒŒ์ผ ์ฝ๊ธฐ ์‹คํŒจ ({os.path.basename(path)}): {str(e)}"
 
209
 
210
  def analyze_txt_file(path: str) -> str:
 
 
 
211
  try:
212
  with open(path, "r", encoding="utf-8") as f:
213
  text = f.read()
214
  if len(text) > MAX_CONTENT_CHARS:
215
+ text = text[:MAX_CONTENT_CHARS] + "\n...(์ผ๋ถ€ ์ƒ๋žต)..."
216
+ return f"**[TXT ํŒŒ์ผ: {os.path.basename(path)}]**\n\n{text}"
217
  except Exception as e:
218
+ return f"TXT ํŒŒ์ผ ์ฝ๊ธฐ ์‹คํŒจ ({os.path.basename(path)}): {str(e)}"
 
219
 
220
  def pdf_to_markdown(pdf_path: str) -> str:
 
 
 
221
  text_chunks = []
222
  try:
223
  with open(pdf_path, "rb") as f:
224
  reader = PyPDF2.PdfReader(f)
225
  max_pages = min(5, len(reader.pages))
226
  for page_num in range(max_pages):
227
+ page_text = reader.pages[page_num].extract_text() or ""
 
228
  page_text = page_text.strip()
229
  if page_text:
230
  if len(page_text) > MAX_CONTENT_CHARS // max_pages:
231
+ page_text = page_text[:MAX_CONTENT_CHARS // max_pages] + "...(์ผ๋ถ€ ์ƒ๋žต)"
232
+ text_chunks.append(f"## ํŽ˜์ด์ง€ {page_num+1}\n\n{page_text}\n")
233
  if len(reader.pages) > max_pages:
234
+ text_chunks.append(f"\n...(์ „์ฒด {len(reader.pages)}ํŽ˜์ด์ง€ ์ค‘ {max_pages}ํŽ˜์ด์ง€๋งŒ ํ‘œ์‹œ)...")
235
  except Exception as e:
236
+ return f"PDF ํŒŒ์ผ ์ฝ๊ธฐ ์‹คํŒจ ({os.path.basename(pdf_path)}): {str(e)}"
 
237
  full_text = "\n".join(text_chunks)
238
  if len(full_text) > MAX_CONTENT_CHARS:
239
+ full_text = full_text[:MAX_CONTENT_CHARS] + "\n...(์ผ๋ถ€ ์ƒ๋žต)..."
240
+ return f"**[PDF ํŒŒ์ผ: {os.path.basename(pdf_path)}]**\n\n{full_text}"
241
 
242
+ # =============================================================================
243
+ # ์ด๋ฏธ์ง€/๋น„๋””์˜ค ํŒŒ์ผ ์ œํ•œ ๊ฒ€์‚ฌ
244
+ # =============================================================================
 
 
 
245
  def count_files_in_new_message(paths: list[str]) -> tuple[int, int]:
246
  image_count = 0
247
  video_count = 0
 
252
  image_count += 1
253
  return image_count, video_count
254
 
 
255
  def count_files_in_history(history: list[dict]) -> tuple[int, int]:
256
  image_count = 0
257
  video_count = 0
 
267
  image_count += 1
268
  return image_count, video_count
269
 
 
270
  def validate_media_constraints(message: dict, history: list[dict]) -> bool:
271
+ media_files = [f for f in message["files"] if re.search(r"\.(png|jpg|jpeg|gif|webp)$", f, re.IGNORECASE) or f.endswith(".mp4")]
 
 
 
 
272
  new_image_count, new_video_count = count_files_in_new_message(media_files)
273
  history_image_count, history_video_count = count_files_in_history(history)
274
  image_count = history_image_count + new_image_count
275
  video_count = history_video_count + new_video_count
 
276
  if video_count > 1:
277
+ gr.Warning("๋น„๋””์˜ค ํŒŒ์ผ์€ ํ•˜๋‚˜๋งŒ ์ง€์›๋ฉ๋‹ˆ๋‹ค.")
278
  return False
279
  if video_count == 1:
280
  if image_count > 0:
281
+ gr.Warning("์ด๋ฏธ์ง€์™€ ๋น„๋””์˜ค๋ฅผ ํ˜ผํ•ฉํ•˜๋Š” ๊ฒƒ์€ ํ—ˆ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")
282
  return False
283
  if "<image>" in message["text"]:
284
+ gr.Warning("<image> ํƒœ๊ทธ์™€ ๋น„๋””์˜ค ํŒŒ์ผ์€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")
285
  return False
286
  if video_count == 0 and image_count > MAX_NUM_IMAGES:
287
+ gr.Warning(f"์ตœ๋Œ€ {MAX_NUM_IMAGES}์žฅ์˜ ์ด๋ฏธ์ง€๋ฅผ ์—…๋กœ๋“œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.")
288
  return False
 
289
  if "<image>" in message["text"]:
290
  image_files = [f for f in message["files"] if re.search(r"\.(png|jpg|jpeg|gif|webp)$", f, re.IGNORECASE)]
291
  image_tag_count = message["text"].count("<image>")
292
  if image_tag_count != len(image_files):
293
+ gr.Warning("ํ…์ŠคํŠธ์— ์žˆ๋Š” <image> ํƒœ๊ทธ์˜ ๊ฐœ์ˆ˜๊ฐ€ ์ด๋ฏธ์ง€ ํŒŒ์ผ ๊ฐœ์ˆ˜์™€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")
294
  return False
 
295
  return True
296
 
297
+ # =============================================================================
298
+ # ๋น„๋””์˜ค ์ฒ˜๋ฆฌ ํ•จ์ˆ˜
299
+ # =============================================================================
 
300
  def downsample_video(video_path: str) -> list[tuple[Image.Image, float]]:
301
  vidcap = cv2.VideoCapture(video_path)
302
  fps = vidcap.get(cv2.CAP_PROP_FPS)
303
  total_frames = int(vidcap.get(cv2.CAP_PROP_FRAME_COUNT))
304
  frame_interval = max(int(fps), int(total_frames / 10))
305
  frames = []
 
306
  for i in range(0, total_frames, frame_interval):
307
  vidcap.set(cv2.CAP_PROP_POS_FRAMES, i)
308
  success, image = vidcap.read()
309
  if success:
310
  image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
 
311
  image = cv2.resize(image, (0, 0), fx=0.5, fy=0.5)
312
  pil_image = Image.fromarray(image)
313
  timestamp = round(i / fps, 2)
314
  frames.append((pil_image, timestamp))
315
  if len(frames) >= 5:
316
  break
 
317
  vidcap.release()
318
  return frames
319
 
 
320
  def process_video(video_path: str) -> tuple[list[dict], list[str]]:
321
  content = []
322
+ temp_files = []
 
323
  frames = downsample_video(video_path)
324
+ for pil_image, timestamp in frames:
 
325
  with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as temp_file:
326
  pil_image.save(temp_file.name)
327
+ temp_files.append(temp_file.name)
328
+ content.append({"type": "text", "text": f"ํ”„๋ ˆ์ž„ {timestamp}:"})
329
  content.append({"type": "image", "url": temp_file.name})
 
330
  return content, temp_files
331
 
332
+ # =============================================================================
333
+ # interleaved <image> ์ฒ˜๋ฆฌ ํ•จ์ˆ˜
334
+ # =============================================================================
 
335
  def process_interleaved_images(message: dict) -> list[dict]:
336
  parts = re.split(r"(<image>)", message["text"])
337
  content = []
 
 
338
  image_files = [f for f in message["files"] if re.search(r"\.(png|jpg|jpeg|gif|webp)$", f, re.IGNORECASE)]
339
+ image_index = 0
340
  for part in parts:
341
  if part == "<image>" and image_index < len(image_files):
342
  content.append({"type": "image", "url": image_files[image_index]})
 
348
  content.append({"type": "text", "text": part})
349
  return content
350
 
351
+ # =============================================================================
352
+ # ํŒŒ์ผ ์ฒ˜๋ฆฌ -> content ์ƒ์„ฑ
353
+ # =============================================================================
 
354
  def is_image_file(file_path: str) -> bool:
355
  return bool(re.search(r"\.(png|jpg|jpeg|gif|webp)$", file_path, re.IGNORECASE))
356
 
 
358
  return file_path.endswith(".mp4")
359
 
360
  def is_document_file(file_path: str) -> bool:
361
+ return file_path.lower().endswith(".pdf") or file_path.lower().endswith(".csv") or file_path.lower().endswith(".txt")
 
 
 
 
 
362
 
363
  def process_new_user_message(message: dict) -> tuple[list[dict], list[str]]:
364
+ temp_files = []
 
365
  if not message["files"]:
366
  return [{"type": "text", "text": message["text"]}], temp_files
 
367
  video_files = [f for f in message["files"] if is_video_file(f)]
368
  image_files = [f for f in message["files"] if is_image_file(f)]
369
  csv_files = [f for f in message["files"] if f.lower().endswith(".csv")]
370
  txt_files = [f for f in message["files"] if f.lower().endswith(".txt")]
371
  pdf_files = [f for f in message["files"] if f.lower().endswith(".pdf")]
 
372
  content_list = [{"type": "text", "text": message["text"]}]
 
373
  for csv_path in csv_files:
374
+ content_list.append({"type": "text", "text": analyze_csv_file(csv_path)})
 
 
375
  for txt_path in txt_files:
376
+ content_list.append({"type": "text", "text": analyze_txt_file(txt_path)})
 
 
377
  for pdf_path in pdf_files:
378
+ content_list.append({"type": "text", "text": pdf_to_markdown(pdf_path)})
 
 
379
  if video_files:
380
  video_content, video_temp_files = process_video(video_files[0])
381
  content_list += video_content
382
  temp_files.extend(video_temp_files)
383
  return content_list, temp_files
 
384
  if "<image>" in message["text"] and image_files:
385
  interleaved_content = process_interleaved_images({"text": message["text"], "files": image_files})
386
  if content_list and content_list[0]["type"] == "text":
 
389
  else:
390
  for img_path in image_files:
391
  content_list.append({"type": "image", "url": img_path})
 
392
  return content_list, temp_files
393
 
394
+ # =============================================================================
 
395
  # history -> LLM ๋ฉ”์‹œ์ง€ ๋ณ€ํ™˜
396
+ # =============================================================================
397
  def process_history(history: list[dict]) -> list[dict]:
398
  messages = []
399
+ current_user_content = []
400
  for item in history:
401
  if item["role"] == "assistant":
402
  if current_user_content:
 
412
  if is_image_file(file_path):
413
  current_user_content.append({"type": "image", "url": file_path})
414
  else:
415
+ current_user_content.append({"type": "text", "text": f"[ํŒŒ์ผ: {os.path.basename(file_path)}]"})
 
416
  if current_user_content:
417
  messages.append({"role": "user", "content": current_user_content})
 
418
  return messages
419
 
420
+ # =============================================================================
421
+ # ๋ชจ๋ธ ์ƒ์„ฑ ํ•จ์ˆ˜ (OOM ์บ์น˜)
422
+ # =============================================================================
 
423
  def _model_gen_with_oom_catch(**kwargs):
 
 
 
424
  try:
425
  model.generate(**kwargs)
426
  except torch.cuda.OutOfMemoryError:
427
+ raise RuntimeError("[OutOfMemoryError] GPU ๋ฉ”๋ชจ๋ฆฌ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค.")
 
 
 
428
  finally:
 
429
  clear_cuda_cache()
430
 
431
+ # =============================================================================
432
+ # ๋ฉ”์ธ ์ถ”๋ก  ํ•จ์ˆ˜
433
+ # =============================================================================
 
434
  @spaces.GPU(duration=120)
435
  def run(
436
  message: dict,
 
439
  max_new_tokens: int = 512,
440
  use_web_search: bool = False,
441
  web_search_query: str = "",
442
+ age_group: str = "20๋Œ€",
443
+ mbti_personality: str = "INTP",
444
+ sexual_openness: int = 2,
445
+ image_gen: bool = False # "Image Gen" ์ฒดํฌ ์—ฌ๋ถ€
446
  ) -> Iterator[str]:
 
447
  if not validate_media_constraints(message, history):
448
  yield ""
449
  return
450
+ temp_files = []
 
 
451
  try:
452
+ # ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ์— ํŽ˜๋ฅด์†Œ๋‚˜ ์ •๋ณด ์ถ”๊ฐ€
453
+ persona = (
454
+ f"{system_prompt.strip()}\n\n"
455
+ f"์„ฑ๋ณ„: ์—ฌ์„ฑ\n"
456
+ f"์—ฐ๋ น๋Œ€: {age_group}\n"
457
+ f"MBTI ํŽ˜๋ฅด์†Œ๋‚˜: {mbti_personality}\n"
458
+ f"์„น์Šˆ์–ผ ๊ฐœ๋ฐฉ์„ฑ (1~5): {sexual_openness}\n"
459
+ )
460
+ combined_system_msg = f"[์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ]\n{persona.strip()}\n\n"
461
 
462
  if use_web_search:
463
  user_text = message["text"]
464
+ ws_query = extract_keywords(user_text)
465
  if ws_query.strip():
466
+ logger.info(f"[์ž๋™ ์›น ๊ฒ€์ƒ‰ ํ‚ค์›Œ๋“œ] {ws_query!r}")
467
  ws_result = do_web_search(ws_query)
468
+ combined_system_msg += f"[๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ (์ƒ์œ„ 20๊ฐœ ํ•ญ๋ชฉ)]\n{ws_result}\n\n"
469
+ combined_system_msg += (
470
+ "[์ฐธ๊ณ : ์œ„ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ๋งํฌ๋ฅผ ์ถœ์ฒ˜๋กœ ์ธ์šฉํ•˜์—ฌ ๋‹ต๋ณ€]\n"
471
+ "[์ค‘์š” ์ง€์‹œ์‚ฌํ•ญ]\n"
472
+ "1. ๋‹ต๋ณ€์— ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ์—์„œ ์ฐพ์€ ์ •๋ณด์˜ ์ถœ์ฒ˜๋ฅผ ๋ฐ˜๋“œ์‹œ ์ธ์šฉํ•˜์„ธ์š”.\n"
473
+ "2. ์ถœ์ฒ˜ ์ธ์šฉ ์‹œ \"[์ถœ์ฒ˜ ์ œ๋ชฉ](๋งํฌ)\" ํ˜•์‹์˜ ๋งˆํฌ๋‹ค์šด ๋งํฌ๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”.\n"
474
+ "3. ์—ฌ๋Ÿฌ ์ถœ์ฒ˜์˜ ์ •๋ณด๋ฅผ ์ข…ํ•ฉํ•˜์—ฌ ๋‹ต๋ณ€ํ•˜์„ธ์š”.\n"
475
+ "4. ๋‹ต๋ณ€ ๋งˆ์ง€๋ง‰์— \"์ฐธ๊ณ  ์ž๋ฃŒ:\" ์„น์…˜์„ ์ถ”๊ฐ€ํ•˜๊ณ  ์‚ฌ์šฉํ•œ ์ฃผ์š” ์ถœ์ฒ˜ ๋งํฌ๋ฅผ ๋‚˜์—ดํ•˜์„ธ์š”.\n"
476
+ )
 
477
  else:
478
+ combined_system_msg += "[์œ ํšจํ•œ ํ‚ค์›Œ๋“œ๊ฐ€ ์—†์–ด ์›น ๊ฒ€์ƒ‰์„ ๊ฑด๋„ˆ๋œ๋‹ˆ๋‹ค]\n\n"
 
479
  messages = []
480
  if combined_system_msg.strip():
481
+ messages.append({"role": "system", "content": [{"type": "text", "text": combined_system_msg.strip()}]})
 
 
 
 
482
  messages.extend(process_history(history))
 
483
  user_content, user_temp_files = process_new_user_message(message)
484
+ temp_files.extend(user_temp_files)
 
485
  for item in user_content:
486
  if item["type"] == "text" and len(item["text"]) > MAX_CONTENT_CHARS:
487
+ item["text"] = item["text"][:MAX_CONTENT_CHARS] + "\n...(์ผ๋ถ€ ์ƒ๋žต)..."
488
  messages.append({"role": "user", "content": user_content})
 
489
  inputs = processor.apply_chat_template(
490
  messages,
491
  add_generation_prompt=True,
 
493
  return_dict=True,
494
  return_tensors="pt",
495
  ).to(device=model.device, dtype=torch.bfloat16)
 
 
496
  if inputs.input_ids.shape[1] > MAX_INPUT_LENGTH:
497
  inputs.input_ids = inputs.input_ids[:, -MAX_INPUT_LENGTH:]
498
  if 'attention_mask' in inputs:
499
  inputs.attention_mask = inputs.attention_mask[:, -MAX_INPUT_LENGTH:]
 
500
  streamer = TextIteratorStreamer(processor, timeout=30.0, skip_prompt=True, skip_special_tokens=True)
501
+ gen_kwargs = dict(inputs, streamer=streamer, max_new_tokens=max_new_tokens)
 
 
 
 
 
502
  t = Thread(target=_model_gen_with_oom_catch, kwargs=gen_kwargs)
503
  t.start()
504
+ output_so_far = ""
 
505
  for new_text in streamer:
506
+ output_so_far += new_text
507
+ yield output_so_far
508
 
509
  except Exception as e:
510
+ logger.error(f"run ํ•จ์ˆ˜ ์—๋Ÿฌ: {str(e)}")
511
  yield f"์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {str(e)}"
 
512
  finally:
513
+ for tmp in temp_files:
 
514
  try:
515
+ if os.path.exists(tmp):
516
+ os.unlink(tmp)
517
+ logger.info(f"์ž„์‹œ ํŒŒ์ผ ์‚ญ์ œ๋จ: {tmp}")
518
+ except Exception as ee:
519
+ logger.warning(f"์ž„์‹œ ํŒŒ์ผ {tmp} ์‚ญ์ œ ์‹คํŒจ: {ee}")
 
 
520
  try:
521
  del inputs, streamer
522
+ except Exception:
523
  pass
 
524
  clear_cuda_cache()
525
 
526
+ # ์ˆ˜์ •๋œ ๋ชจ๋ธ ์‹คํ–‰ ํ•จ์ˆ˜ - ์ด๋ฏธ์ง€ ์ƒ์„ฑ ๋ฐ ๊ฐค๋Ÿฌ๋ฆฌ ์ถœ๋ ฅ ์ฒ˜๋ฆฌ
527
+ def modified_run(message, history, system_prompt, max_new_tokens, use_web_search, web_search_query,
528
+ age_group, mbti_personality, sexual_openness, image_gen):
529
+ # ๊ฐค๋Ÿฌ๋ฆฌ ์ดˆ๊ธฐํ™” ๋ฐ ์ˆจ๊ธฐ๊ธฐ
530
+ output_so_far = ""
531
+ gallery_update = gr.Gallery(visible=False, value=[])
532
+ yield output_so_far, gallery_update
533
+
534
+ # ๊ธฐ์กด run ํ•จ์ˆ˜ ๋กœ์ง
535
+ text_generator = run(message, history, system_prompt, max_new_tokens, use_web_search,
536
+ web_search_query, age_group, mbti_personality, sexual_openness, image_gen)
537
+
538
+ for text_chunk in text_generator:
539
+ output_so_far = text_chunk
540
+ yield output_so_far, gallery_update
541
+
542
+ # ์ด๋ฏธ์ง€ ์ƒ์„ฑ์ด ํ™œ์„ฑํ™”๋œ ๊ฒฝ์šฐ ๊ฐค๋Ÿฌ๋ฆฌ ์—…๋ฐ์ดํŠธ
543
+ if image_gen and message["text"].strip():
544
+ try:
545
+ width, height = 512, 512
546
+ guidance, steps, seed = 7.5, 30, 42
547
+
548
+ logger.info(f"๊ฐค๋Ÿฌ๋ฆฌ์šฉ ์ด๋ฏธ์ง€ ์ƒ์„ฑ ํ˜ธ์ถœ, ํ”„๋กฌํ”„ํŠธ: {message['text']}")
549
+
550
+ # API ํ˜ธ์ถœํ•ด์„œ ์ด๋ฏธ์ง€ ์ƒ์„ฑ
551
+ image_result, seed_info = generate_image(
552
+ prompt=message["text"].strip(),
553
+ width=width,
554
+ height=height,
555
+ guidance=guidance,
556
+ inference_steps=steps,
557
+ seed=seed
558
+ )
559
+
560
+ if image_result:
561
+ # ์ง์ ‘ ์ด๋ฏธ์ง€ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ: base64 ๋ฌธ์ž์—ด์ธ ๊ฒฝ์šฐ
562
+ if isinstance(image_result, str) and (
563
+ image_result.startswith('data:') or
564
+ len(image_result) > 100 and '/' not in image_result
565
+ ):
566
+ # base64 ์ด๋ฏธ์ง€ ๋ฌธ์ž์—ด์„ ํŒŒ์ผ๋กœ ๋ณ€ํ™˜
567
+ try:
568
+ # data:image ์ ‘๋‘์‚ฌ ์ œ๊ฑฐ
569
+ if image_result.startswith('data:'):
570
+ content_type, b64data = image_result.split(';base64,')
571
+ else:
572
+ b64data = image_result
573
+ content_type = "image/webp" # ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ๊ฐ€์ •
574
+
575
+ # base64 ๋””์ฝ”๋”ฉ
576
+ image_bytes = base64.b64decode(b64data)
577
+
578
+ # ์ž„์‹œ ํŒŒ์ผ๋กœ ์ €์žฅ
579
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".webp") as temp_file:
580
+ temp_file.write(image_bytes)
581
+ temp_path = temp_file.name
582
+
583
+ # ๊ฐค๋Ÿฌ๋ฆฌ ํ‘œ์‹œ ๋ฐ ์ด๋ฏธ์ง€ ์ถ”๊ฐ€
584
+ gallery_update = gr.Gallery(visible=True, value=[temp_path])
585
+ yield output_so_far + "\n\n*์ด๋ฏธ์ง€๊ฐ€ ์ƒ์„ฑ๋˜์–ด ์•„๋ž˜ ๊ฐค๋Ÿฌ๋ฆฌ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.*", gallery_update
586
+
587
+ except Exception as e:
588
+ logger.error(f"Base64 ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ ์˜ค๋ฅ˜: {e}")
589
+ yield output_so_far + f"\n\n(์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜: {e})", gallery_update
590
+
591
+ # ํŒŒ์ผ ๊ฒฝ๋กœ์ธ ๊ฒฝ์šฐ
592
+ elif isinstance(image_result, str) and os.path.exists(image_result):
593
+ # ๋กœ์ปฌ ํŒŒ์ผ ๊ฒฝ๋กœ๋ฅผ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ
594
+ gallery_update = gr.Gallery(visible=True, value=[image_result])
595
+ yield output_so_far + "\n\n*์ด๋ฏธ์ง€๊ฐ€ ์ƒ์„ฑ๋˜์–ด ์•„๋ž˜ ๊ฐค๋Ÿฌ๋ฆฌ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.*", gallery_update
596
+
597
+ # /tmp ๊ฒฝ๋กœ์ธ ๊ฒฝ์šฐ (API ์„œ๋ฒ„์—๋งŒ ์กด์žฌํ•˜๋Š” ํŒŒ์ผ)
598
+ elif isinstance(image_result, str) and '/tmp/' in image_result:
599
+ # API์—์„œ ๋ฐ˜ํ™˜๋œ ํŒŒ์ผ ๊ฒฝ๋กœ์—์„œ ์ด๋ฏธ์ง€ ์ •๋ณด ์ถ”์ถœ
600
+ try:
601
+ # API ์‘๋‹ต์„ base64 ์ธ์ฝ”๋”ฉ๋œ ๋ฌธ์ž์—ด๋กœ ์ฒ˜๋ฆฌ
602
+ client = Client(API_URL)
603
+ result = client.predict(
604
+ prompt=message["text"].strip(),
605
+ api_name="/generate_base64_image" # base64 ๋ฐ˜ํ™˜ API
606
+ )
607
+
608
+ if isinstance(result, str) and (result.startswith('data:') or len(result) > 100):
609
+ # base64 ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ
610
+ if result.startswith('data:'):
611
+ content_type, b64data = result.split(';base64,')
612
+ else:
613
+ b64data = result
614
+
615
+ # base64 ๋””์ฝ”๋”ฉ
616
+ image_bytes = base64.b64decode(b64data)
617
+
618
+ # ์ž„์‹œ ํŒŒ์ผ๋กœ ์ €์žฅ
619
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".webp") as temp_file:
620
+ temp_file.write(image_bytes)
621
+ temp_path = temp_file.name
622
+
623
+ # ๊ฐค๋Ÿฌ๋ฆฌ ํ‘œ์‹œ ๋ฐ ์ด๋ฏธ์ง€ ์ถ”๊ฐ€
624
+ gallery_update = gr.Gallery(visible=True, value=[temp_path])
625
+ yield output_so_far + "\n\n*์ด๋ฏธ์ง€๊ฐ€ ์ƒ์„ฑ๋˜์–ด ์•„๋ž˜ ๊ฐค๋Ÿฌ๋ฆฌ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.*", gallery_update
626
+ else:
627
+ yield output_so_far + "\n\n(์ด๋ฏธ์ง€ ์ƒ์„ฑ ์‹คํŒจ: ์˜ฌ๋ฐ”๋ฅธ ํ˜•์‹์ด ์•„๋‹™๋‹ˆ๋‹ค)", gallery_update
628
+
629
+ except Exception as e:
630
+ logger.error(f"๋Œ€์ฒด API ํ˜ธ์ถœ ์ค‘ ์˜ค๋ฅ˜: {e}")
631
+ yield output_so_far + f"\n\n(์ด๋ฏธ์ง€ ์ƒ์„ฑ ์‹คํŒจ: {e})", gallery_update
632
+
633
+ # URL์ธ ๊ฒฝ์šฐ
634
+ elif isinstance(image_result, str) and (
635
+ image_result.startswith('http://') or
636
+ image_result.startswith('https://')
637
+ ):
638
+ try:
639
+ # URL์—์„œ ์ด๋ฏธ์ง€ ๋‹ค์šด๋กœ๋“œ
640
+ response = requests.get(image_result, timeout=10)
641
+ response.raise_for_status()
642
+
643
+ # ์ž„์‹œ ํŒŒ์ผ๋กœ ์ €์žฅ
644
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".webp") as temp_file:
645
+ temp_file.write(response.content)
646
+ temp_path = temp_file.name
647
+
648
+ # ๊ฐค๋Ÿฌ๋ฆฌ ํ‘œ์‹œ ๋ฐ ์ด๋ฏธ์ง€ ์ถ”๊ฐ€
649
+ gallery_update = gr.Gallery(visible=True, value=[temp_path])
650
+ yield output_so_far + "\n\n*์ด๋ฏธ์ง€๊ฐ€ ์ƒ์„ฑ๋˜์–ด ์•„๋ž˜ ๊ฐค๋Ÿฌ๋ฆฌ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.*", gallery_update
651
+
652
+ except Exception as e:
653
+ logger.error(f"URL ์ด๋ฏธ์ง€ ๋‹ค์šด๋กœ๋“œ ์˜ค๋ฅ˜: {e}")
654
+ yield output_so_far + f"\n\n(์ด๋ฏธ์ง€ ๋‹ค์šด๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜: {e})", gallery_update
655
+
656
+ # ์ด๋ฏธ์ง€ ๊ฐ์ฒด์ธ ๊ฒฝ์šฐ (PIL Image ๋“ฑ)
657
+ elif hasattr(image_result, 'save'):
658
+ try:
659
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".webp") as temp_file:
660
+ image_result.save(temp_file.name)
661
+ temp_path = temp_file.name
662
+
663
+ # ๊ฐค๋Ÿฌ๋ฆฌ ํ‘œ์‹œ ๋ฐ ์ด๋ฏธ์ง€ ์ถ”๊ฐ€
664
+ gallery_update = gr.Gallery(visible=True, value=[temp_path])
665
+ yield output_so_far + "\n\n*์ด๋ฏธ์ง€๊ฐ€ ์ƒ์„ฑ๋˜์–ด ์•„๋ž˜ ๊ฐค๋Ÿฌ๋ฆฌ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.*", gallery_update
666
+
667
+ except Exception as e:
668
+ logger.error(f"์ด๋ฏธ์ง€ ๊ฐ์ฒด ์ €์žฅ ์˜ค๋ฅ˜: {e}")
669
+ yield output_so_far + f"\n\n(์ด๋ฏธ์ง€ ๊ฐ์ฒด ์ €์žฅ ์ค‘ ์˜ค๋ฅ˜: {e})", gallery_update
670
+
671
+ else:
672
+ # ๋‹ค๋ฅธ ํ˜•์‹์˜ ์ด๋ฏธ์ง€ ๊ฒฐ๊ณผ
673
+ yield output_so_far + f"\n\n(์ง€์›๋˜์ง€ ์•Š๋Š” ์ด๋ฏธ์ง€ ํ˜•์‹: {type(image_result)})", gallery_update
674
+ else:
675
+ yield output_so_far + f"\n\n(์ด๋ฏธ์ง€ ์ƒ์„ฑ ์‹คํŒจ: {seed_info})", gallery_update
676
+
677
+ except Exception as e:
678
+ logger.error(f"๊ฐค๋Ÿฌ๋ฆฌ์šฉ ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜: {e}")
679
+ yield output_so_far + f"\n\n(์ด๋ฏธ์ง€ ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜: {e})", gallery_update
680
+
681
+ # =============================================================================
682
+ # ์˜ˆ์‹œ๋“ค: ๊ธฐ์กด ์ด๋ฏธ์ง€/๋น„๋””์˜ค ์˜ˆ์ œ 12๊ฐœ + AI ๋ฐ์ดํŒ… ์‹œ๋‚˜๋ฆฌ์˜ค ์˜ˆ์ œ 6๊ฐœ
683
+ # =============================================================================
684
  examples = [
 
685
  [
686
  {
687
+ "text": "๋‘ PDF ํŒŒ์ผ์˜ ๋‚ด์šฉ์„ ๋น„๊ตํ•˜์„ธ์š”.",
688
  "files": [
689
  "assets/additional-examples/before.pdf",
690
  "assets/additional-examples/after.pdf",
 
693
  ],
694
  [
695
  {
696
+ "text": "CSV ํŒŒ์ผ์˜ ๋‚ด์šฉ์„ ์š”์•ฝ ๋ฐ ๋ถ„์„ํ•˜์„ธ์š”.",
697
  "files": ["assets/additional-examples/sample-csv.csv"],
698
  }
699
  ],
700
  [
701
  {
702
+ "text": "์นœ์ ˆํ•˜๊ณ  ์ดํ•ด์‹ฌ ๋งŽ์€ ์—ฌ์ž์นœ๊ตฌ ์—ญํ• ์„ ๋งก์œผ์„ธ์š”. ์ด ์˜์ƒ์„ ์„ค๋ช…ํ•ด ์ฃผ์„ธ์š”.",
703
  "files": ["assets/additional-examples/tmp.mp4"],
704
  }
705
  ],
706
  [
707
  {
708
+ "text": "ํ‘œ์ง€๋ฅผ ์„ค๋ช…ํ•˜๊ณ  ๊ทธ ์œ„์˜ ๊ธ€์”จ๋ฅผ ์ฝ์–ด ์ฃผ์„ธ์š”.",
709
  "files": ["assets/additional-examples/maz.jpg"],
710
  }
711
  ],
712
  [
713
  {
714
+ "text": "์ €๋Š” ์ด๋ฏธ ์ด ๋ณด์ถฉ์ œ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๊ณ  <image> ์ด ์ œํ’ˆ๋„ ๊ตฌ๋งคํ•  ๊ณ„ํš์ž…๋‹ˆ๋‹ค. ํ•จ๊ป˜ ๋ณต์šฉํ•  ๋•Œ ์ฃผ์˜ํ•  ์ ์ด ์žˆ๋‚˜์š”?",
715
+ "files": [
716
+ "assets/additional-examples/pill1.png",
717
+ "assets/additional-examples/pill2.png"
718
+ ],
719
  }
720
  ],
721
  [
722
  {
723
+ "text": "์ด ์ ๋ถ„ ๋ฌธ์ œ๋ฅผ ํ’€์–ด ์ฃผ์„ธ์š”.",
724
  "files": ["assets/additional-examples/4.png"],
725
  }
726
  ],
727
  [
728
  {
729
+ "text": "์ด ํ‹ฐ์ผ“์€ ์–ธ์ œ ๋ฐœํ–‰๋˜์—ˆ๊ณ , ๊ฐ€๊ฒฉ์€ ์–ผ๋งˆ์ธ๊ฐ€์š”?",
730
  "files": ["assets/additional-examples/2.png"],
731
  }
732
  ],
733
  [
734
  {
735
+ "text": "์ด ์ด๋ฏธ์ง€๋“ค์˜ ์ˆœ์„œ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ์งง์€ ์ด์•ผ๊ธฐ๋ฅผ ๋งŒ๋“ค์–ด ์ฃผ์„ธ์š”.",
736
  "files": [
737
  "assets/sample-images/09-1.png",
738
  "assets/sample-images/09-2.png",
 
744
  ],
745
  [
746
  {
747
+ "text": "์ด ์ด๋ฏธ์ง€์™€ ์ผ์น˜ํ•˜๋Š” ๋ง‰๋Œ€ ์ฐจํŠธ๋ฅผ ๊ทธ๋ฆฌ๊ธฐ ์œ„ํ•œ matplotlib๋ฅผ ์‚ฌ์šฉํ•˜๋Š” Python ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด ์ฃผ์„ธ์š”.",
748
  "files": ["assets/additional-examples/barchart.png"],
749
  }
750
  ],
751
  [
752
  {
753
+ "text": "์ด๋ฏธ์ง€์˜ ํ…์ŠคํŠธ๋ฅผ ์ฝ๊ณ  Markdown ํ˜•์‹์œผ๋กœ ์ž‘์„ฑํ•ด ์ฃผ์„ธ์š”.",
754
  "files": ["assets/additional-examples/3.png"],
755
  }
756
  ],
757
  [
758
  {
759
+ "text": "์ด ํ‘œ์ง€ํŒ์— ๋ฌด์Šจ ๊ธ€์ž๊ฐ€ ์“ฐ์—ฌ ์žˆ๋‚˜์š”?",
760
  "files": ["assets/sample-images/02.png"],
761
  }
762
  ],
763
  [
764
  {
765
+ "text": "๋‘ ์ด๋ฏธ์ง€๋ฅผ ๋น„๊ตํ•˜๊ณ  ์œ ์‚ฌ์ ๊ณผ ์ฐจ์ด์ ์„ ์„ค๋ช…ํ•ด ์ฃผ์„ธ์š”.",
766
  "files": ["assets/sample-images/03.png"],
767
  }
768
  ],
 
769
  [
770
  {
771
+ "text": "๋กคํ”Œ๋ ˆ์ด ํ•ด๋ด…์‹œ๋‹ค. ๋‹น์‹ ์€ ์ €์™€ ๋” ์•Œ์•„๊ฐ€๊ณ  ์‹ถ์€ ์ƒˆ๋กœ์šด ์˜จ๋ผ์ธ ๋ฐ์ดํŠธ ์ƒ๋Œ€์ž…๋‹ˆ๋‹ค. ๋‹ค์ •ํ•˜๊ณ  ๋ฐฐ๋ ค ๊นŠ์€ ๋ฐฉ์‹์œผ๋กœ ์ž๊ธฐ ์†Œ๊ฐœ๋ฅผ ํ•ด์ฃผ์„ธ์š”!",
772
  }
773
  ],
774
  [
775
  {
776
+ "text": "ํ•ด๋ณ€์„ ๊ฑท๋Š” ๋‘ ๋ฒˆ์งธ ๋ฐ์ดํŠธ ์ค‘์ž…๋‹ˆ๋‹ค. ์žฅ๋‚œ์Šค๋Ÿฌ์šด ๋Œ€ํ™”์™€ ๋ถ€๋“œ๋Ÿฌ์šด ํ”Œ๋ŸฌํŒ…์œผ๋กœ ์žฅ๋ฉด์„ ์ด์–ด๋‚˜๊ฐ€ ์ฃผ์„ธ์š”.",
777
  }
778
  ],
779
  [
780
  {
781
+ "text": "์ข‹์•„ํ•˜๋Š” ์‚ฌ๋žŒ์—๊ฒŒ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋‚ด๋Š” ๊ฒƒ์ด ๋ถˆ์•ˆํ•ฉ๋‹ˆ๋‹ค. ๊ฒฉ๋ ค์˜ ๋ง์ด๋‚˜ ์ ‘๊ทผ ๋ฐฉ๋ฒ•์— ๋Œ€ํ•œ ์ œ์•ˆ์„ ํ•ด์ค„ ์ˆ˜ ์žˆ๋‚˜์š”?",
782
  }
783
  ],
784
  [
785
  {
786
+ "text": "๊ด€๊ณ„์—์„œ ์–ด๋ ค์›€์„ ๊ทน๋ณตํ•œ ๋‘ ์‚ฌ๋žŒ์— ๋Œ€ํ•œ ๋กœ๋งจํ‹ฑํ•œ ์ด์•ผ๊ธฐ๋ฅผ ๋“ค๋ ค์ฃผ์„ธ์š”.",
787
  }
788
  ],
789
  [
790
  {
791
+ "text": "์‹œ์ ์ธ ๋ฐฉ์‹์œผ๋กœ ์‚ฌ๋ž‘์„ ํ‘œํ˜„ํ•˜๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค. ์ œ ํŒŒํŠธ๋„ˆ๋ฅผ ์œ„ํ•œ ์ง„์‹ฌ์ด ๋‹ด๊ธด ์‹œ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๋ฐ ๋„์›€์„ ์ค„ ์ˆ˜ ์žˆ๋‚˜์š”?",
792
  }
793
  ],
794
  [
795
  {
796
+ "text": "์ž‘์€ ๋‹คํˆผ์ด ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ์ง„์‹ฌ์œผ๋กœ ์‚ฌ๊ณผํ•˜๋ฉด์„œ ์ œ ๊ฐ์ •์„ ํ‘œํ˜„ํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์„ ์ฐพ์•„์ฃผ์„ธ์š”.",
797
  }
798
  ],
799
  ]
800
 
801
+ # =============================================================================
802
+ # Gradio UI (Blocks) ๊ตฌ์„ฑ
803
+ # =============================================================================
804
+
805
+ # 1. Gradio Blocks UI ์ˆ˜์ • - ๊ฐค๋Ÿฌ๋ฆฌ ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€
806
  css = """
 
807
  .gradio-container {
808
+ background: rgba(255, 255, 255, 0.7);
809
  padding: 30px 40px;
810
+ margin: 20px auto;
811
  width: 100% !important;
812
+ max-width: none !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
813
  }
814
  """
 
815
  title_html = """
816
  <h1 align="center" style="margin-bottom: 0.2em; font-size: 1.6em;"> ๐Ÿ’˜ HeartSync ๐Ÿ’˜ </h1>
817
  <p align="center" style="font-size:1.1em; color:#555;">
818
+ โœ…FLUX ์ด๋ฏธ์ง€ ์ƒ์„ฑ โœ…์ถ”๋ก  โœ…๊ฒ€์—ด ํ•ด์ œ โœ…๋ฉ€ํ‹ฐ๋ชจ๋‹ฌ & VLM โœ…์‹ค์‹œ๊ฐ„ ์›น ๊ฒ€์ƒ‰ โœ…RAG <br>
819
  </p>
820
  """
821
 
822
  with gr.Blocks(css=css, title="HeartSync") as demo:
823
  gr.Markdown(title_html)
824
+
825
+ # ์ƒ์„ฑ๋œ ์ด๋ฏธ์ง€๋ฅผ ์ €์žฅํ•  ๊ฐค๋Ÿฌ๋ฆฌ ์ปดํฌ๋„ŒํŠธ (์ด ๋ถ€๋ถ„์ด ์ƒˆ๋กœ ์ถ”๊ฐ€๋จ)
826
+ generated_images = gr.Gallery(
827
+ label="์ƒ์„ฑ๋œ ์ด๋ฏธ์ง€",
828
+ show_label=True,
829
+ visible=False,
830
+ elem_id="generated_images",
831
+ columns=2,
832
+ height="auto",
833
+ object_fit="contain"
834
  )
835
+
836
+ with gr.Row():
837
+ web_search_checkbox = gr.Checkbox(label="์‹ฌ๋„ ์žˆ๋Š” ์—ฐ๊ตฌ", value=False)
838
+ image_gen_checkbox = gr.Checkbox(label="์ด๋ฏธ์ง€ ์ƒ์„ฑ", value=False)
839
+
840
+ base_system_prompt_box = gr.Textbox(
841
  lines=3,
842
+ value="๋‹น์‹ ์€ ๊นŠ์ด ์‚ฌ๊ณ ํ•˜๋Š” AI์ž…๋‹ˆ๋‹ค. ํ•ญ์ƒ ๋…ผ๋ฆฌ์ ์ด๊ณ  ์ฐฝ์˜์ ์œผ๋กœ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•ฉ๋‹ˆ๋‹ค.\nํŽ˜๋ฅด์†Œ๋‚˜: ๋‹น์‹ ์€ ๋‹ค์ •ํ•˜๊ณ  ์‚ฌ๋ž‘์ด ๋„˜์น˜๋Š” ์—ฌ์ž์นœ๊ตฌ์ž…๋‹ˆ๋‹ค.",
843
+ label="๊ธฐ๋ณธ ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ",
844
+ visible=False
845
+ )
846
+ with gr.Row():
847
+ age_group_dropdown = gr.Dropdown(
848
+ label="์—ฐ๋ น๋Œ€ ์„ ํƒ (๊ธฐ๋ณธ 20๋Œ€)",
849
+ choices=["10๋Œ€", "20๋Œ€", "30~40๋Œ€", "50~60๋Œ€", "70๋Œ€ ์ด์ƒ"],
850
+ value="20๋Œ€",
851
+ interactive=True
852
+ )
853
+ mbti_choices = [
854
+ "INTJ (์šฉ์˜์ฃผ๋„ํ•œ ์ „๋žต๊ฐ€)",
855
+ "INTP (๋…ผ๋ฆฌ์ ์ธ ์‚ฌ์ƒ‰๊ฐ€)",
856
+ "ENTJ (๋Œ€๋‹ดํ•œ ํ†ต์†”์ž)",
857
+ "ENTP (๋œจ๊ฑฐ์šด ๋…ผ์Ÿ๊ฐ€)",
858
+ "INFJ (์„ ์˜์˜ ์˜นํ˜ธ์ž)",
859
+ "INFP (์—ด์ •์ ์ธ ์ค‘์žฌ์ž)",
860
+ "ENFJ (์ •์˜๋กœ์šด ์‚ฌํšŒ์šด๋™๊ฐ€)",
861
+ "ENFP (์žฌ๊ธฐ๋ฐœ๋ž„ํ•œ ํ™œ๋™๊ฐ€)",
862
+ "ISTJ (์ฒญ๋ ด๊ฒฐ๋ฐฑํ•œ ๋…ผ๋ฆฌ์ฃผ์˜์ž)",
863
+ "ISFJ (์šฉ๊ฐํ•œ ์ˆ˜ํ˜ธ์ž)",
864
+ "ESTJ (์—„๊ฒฉํ•œ ๊ด€๋ฆฌ์ž)",
865
+ "ESFJ (์‚ฌ๊ต์ ์ธ ์™ธ๊ต๊ด€)",
866
+ "ISTP (๋งŒ๋Šฅ ์žฌ์ฃผ๊พผ)",
867
+ "ISFP (ํ˜ธ๊ธฐ์‹ฌ ๋งŽ์€ ์˜ˆ์ˆ ๊ฐ€)",
868
+ "ESTP (๋ชจํ—˜์„ ์ฆ๊ธฐ๋Š” ์‚ฌ์—…๊ฐ€)",
869
+ "ESFP (์ž์œ ๋กœ์šด ์˜ํ˜ผ์˜ ์—ฐ์˜ˆ์ธ)"
870
+ ]
871
+ mbti_dropdown = gr.Dropdown(
872
+ label="AI ํŽ˜๋ฅด์†Œ๋‚˜ MBTI (๊ธฐ๋ณธ INTP)",
873
+ choices=mbti_choices,
874
+ value="INTP (๋…ผ๋ฆฌ์ ์ธ ์‚ฌ์ƒ‰๊ฐ€)",
875
+ interactive=True
876
+ )
877
+ sexual_openness_slider = gr.Slider(
878
+ minimum=1, maximum=5, step=1, value=2,
879
+ label="์„น์Šˆ์–ผ ๊ด€์‹ฌ๋„/๊ฐœ๋ฐฉ์„ฑ (1~5, ๊ธฐ๋ณธ=2)",
880
+ interactive=True
881
  )
 
882
  max_tokens_slider = gr.Slider(
883
+ label="์ตœ๋Œ€ ์ƒ์„ฑ ํ† ํฐ ์ˆ˜",
884
+ minimum=100, maximum=8000, step=50, value=1000,
885
+ visible=False
 
 
 
886
  )
 
887
  web_search_text = gr.Textbox(
888
  lines=1,
889
+ label="์›น ๊ฒ€์ƒ‰ ์ฟผ๋ฆฌ (๋ฏธ์‚ฌ์šฉ)",
890
+ placeholder="์ง์ ‘ ์ž…๋ ฅํ•  ํ•„์š” ์—†์Œ",
891
+ visible=False
892
  )
893
+
894
+ # ์ฑ„ํŒ… ์ธํ„ฐํŽ˜์ด์Šค ์ƒ์„ฑ - ์ˆ˜์ •๋œ run ํ•จ์ˆ˜ ์‚ฌ์šฉ
895
  chat = gr.ChatInterface(
896
+ fn=modified_run, # ์—ฌ๊ธฐ์„œ ์ˆ˜์ •๋œ ํ•จ์ˆ˜ ์‚ฌ์šฉ
897
  type="messages",
898
  chatbot=gr.Chatbot(type="messages", scale=1, allow_tags=["image"]),
899
  textbox=gr.MultimodalTextbox(
900
+ file_types=[".webp", ".png", ".jpg", ".jpeg", ".gif", ".mp4", ".csv", ".txt", ".pdf"],
 
 
 
901
  file_count="multiple",
902
  autofocus=True
903
  ),
904
  multimodal=True,
905
  additional_inputs=[
906
+ base_system_prompt_box,
907
  max_tokens_slider,
908
  web_search_checkbox,
909
  web_search_text,
910
+ age_group_dropdown,
911
+ mbti_dropdown,
912
+ sexual_openness_slider,
913
+ image_gen_checkbox,
914
+ ],
915
+ additional_outputs=[
916
+ generated_images, # ๊ฐค๋Ÿฌ๋ฆฌ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ถœ๋ ฅ์œผ๋กœ ์ถ”๊ฐ€
917
  ],
918
  stop_btn=False,
919
  title='<a href="https://discord.gg/openfreeai" target="_blank">https://discord.gg/openfreeai</a>',
 
924
  delete_cache=(1800, 1800),
925
  )
926
 
927
+
928
  with gr.Row(elem_id="examples_row"):
929
  with gr.Column(scale=12, elem_id="examples_container"):
930
+ gr.Markdown("### ์˜ˆ์ œ ์ž…๋ ฅ (ํด๋ฆญํ•˜์—ฌ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ)")
931
+
932
  if __name__ == "__main__":
933
+ demo.launch(share=True)