ginipick commited on
Commit
2100944
ยท
verified ยท
1 Parent(s): 56a455f

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +957 -0
app.py ADDED
@@ -0,0 +1,957 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+
3
+ import os
4
+ import re
5
+ import tempfile
6
+ import gc # garbage collector ์ถ”๊ฐ€
7
+ from collections.abc import Iterator
8
+ from threading import Thread
9
+ import json
10
+ import requests
11
+ import cv2
12
+ import gradio as gr
13
+ import spaces
14
+ import torch
15
+ 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
236
+ for path in paths:
237
+ if path.endswith(".mp4"):
238
+ video_count += 1
239
+ elif re.search(r"\.(png|jpg|jpeg|gif|webp)$", path, re.IGNORECASE):
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
247
+ for item in history:
248
+ if item["role"] != "user" or isinstance(item["content"], str):
249
+ continue
250
+ if isinstance(item["content"], list) and len(item["content"]) > 0:
251
+ file_path = item["content"][0]
252
+ if isinstance(file_path, str):
253
+ if file_path.endswith(".mp4"):
254
+ video_count += 1
255
+ elif re.search(r"\.(png|jpg|jpeg|gif|webp)$", file_path, re.IGNORECASE):
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]})
351
+ image_index += 1
352
+ elif part.strip():
353
+ content.append({"type": "text", "text": part.strip()})
354
+ else:
355
+ if isinstance(part, str) and part != "<image>":
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
+
366
+ 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":
412
+ content_list = content_list[1:]
413
+ return interleaved_content + content_list, temp_files
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:
430
+ messages.append({"role": "user", "content": current_user_content})
431
+ current_user_content = []
432
+ messages.append({"role": "assistant", "content": [{"type": "text", "text": item["content"]}]})
433
+ else:
434
+ content = item["content"]
435
+ if isinstance(content, str):
436
+ current_user_content.append({"type": "text", "text": content})
437
+ elif isinstance(content, list) and len(content) > 0:
438
+ file_path = content[0]
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
+ # ๋ฉ”์ธ ์ถ”๋ก  ํ•จ์ˆ˜
471
+ # - ์‚ฌ์šฉ์ž ์„ ํƒ(๋‚˜์ด/MBTI/์„น์Šˆ์–ผ ๊ฐœ๋ฐฉ๋„)์„ ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ์— ๋ฐ˜์˜
472
+ # - web search ์ฒดํฌ ์‹œ ์ž๋™ ํ‚ค์›Œ๋“œ ์ถ”์ถœ->๊ฒ€์ƒ‰->๊ฒฐ๊ณผ system msg
473
+ ##############################################################################
474
+ @spaces.GPU(duration=120)
475
+ def run(
476
+ message: dict,
477
+ history: list[dict],
478
+ system_prompt: str = "",
479
+ max_new_tokens: int = 512,
480
+ use_web_search: bool = False,
481
+ web_search_query: str = "",
482
+ age_group: str = "20๋Œ€",
483
+ custom_age_input: str = "",
484
+ mbti_personality: str = "INTP",
485
+ sexual_openness: int = 2,
486
+ ) -> Iterator[str]:
487
+
488
+ if not validate_media_constraints(message, history):
489
+ yield ""
490
+ return
491
+
492
+ temp_files = [] # ์ž„์‹œ ํŒŒ์ผ ์ถ”์ ์šฉ
493
+
494
+ try:
495
+ # ---------------------------------------------------------------
496
+ # ์„ ํƒ๋œ ์˜ต์…˜์„ ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ์— ๋ฐ˜์˜
497
+ # ๊ธฐ๋ณธ ์„ฑ๋ณ„์€ "์—ฌ์„ฑ"
498
+ # ---------------------------------------------------------------
499
+ system_prompt_updated = (
500
+ f"{system_prompt.strip()}\n\n"
501
+ f"Gender: Female\n"
502
+ f"Age Group: {age_group}\n"
503
+ )
504
+ if custom_age_input.strip():
505
+ system_prompt_updated += f"(Custom Age Input: {custom_age_input})\n"
506
+ system_prompt_updated += f"MBTI Persona: {mbti_personality}\n"
507
+ system_prompt_updated += f"Sexual Openness (1~5): {sexual_openness}\n"
508
+
509
+ combined_system_msg = f"[System Prompt]\n{system_prompt_updated.strip()}\n\n"
510
+
511
+ if use_web_search:
512
+ user_text = message["text"]
513
+ ws_query = extract_keywords(user_text, top_k=5)
514
+ if ws_query.strip():
515
+ logger.info(f"[Auto WebSearch Keyword] {ws_query!r}")
516
+ ws_result = do_web_search(ws_query)
517
+ combined_system_msg += f"[Search top-20 Full Items Based on user prompt]\n{ws_result}\n\n"
518
+ # >>> ์ถ”๊ฐ€๋œ ์•ˆ๋‚ด ๋ฌธ๊ตฌ (๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ์˜ link ๋“ฑ ์ถœ์ฒ˜๋ฅผ ํ™œ์šฉ)
519
+ combined_system_msg += "[์ฐธ๊ณ : ์œ„ ๊ฒ€์ƒ‰๊ฒฐ๊ณผ ๋‚ด์šฉ๊ณผ link๋ฅผ ์ถœ์ฒ˜๋กœ ์ธ์šฉํ•˜์—ฌ ๋‹ต๋ณ€ํ•ด ์ฃผ์„ธ์š”.]\n\n"
520
+ combined_system_msg += """
521
+ [์ค‘์š” ์ง€์‹œ์‚ฌํ•ญ]
522
+ 1. ๋‹ต๋ณ€์— ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ์—์„œ ์ฐพ์€ ์ •๋ณด์˜ ์ถœ์ฒ˜๋ฅผ ๋ฐ˜๋“œ์‹œ ์ธ์šฉํ•˜์„ธ์š”.
523
+ 2. ์ถœ์ฒ˜ ์ธ์šฉ ์‹œ "[์ถœ์ฒ˜ ์ œ๋ชฉ](๋งํฌ)" ํ˜•์‹์˜ ๋งˆํฌ๋‹ค์šด ๋งํฌ๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”.
524
+ 3. ์—ฌ๋Ÿฌ ์ถœ์ฒ˜์˜ ์ •๋ณด๋ฅผ ์ข…ํ•ฉํ•˜์—ฌ ๋‹ต๋ณ€ํ•˜์„ธ์š”.
525
+ 4. ๋‹ต๋ณ€ ๋งˆ์ง€๋ง‰์— "์ฐธ๊ณ  ์ž๋ฃŒ:" ์„น์…˜์„ ์ถ”๊ฐ€ํ•˜๊ณ  ์‚ฌ์šฉํ•œ ์ฃผ์š” ์ถœ์ฒ˜ ๋งํฌ๋ฅผ ๋‚˜์—ดํ•˜์„ธ์š”.
526
+ """
527
+ else:
528
+ combined_system_msg += "[No valid keywords found, skipping WebSearch]\n\n"
529
+
530
+ messages = []
531
+ # system ๋ฉ”์‹œ์ง€
532
+ if combined_system_msg.strip():
533
+ messages.append({
534
+ "role": "system",
535
+ "content": [{"type": "text", "text": combined_system_msg.strip()}],
536
+ })
537
+
538
+ # ์ด์ „ history
539
+ messages.extend(process_history(history))
540
+
541
+ # ์‚ฌ์šฉ์ž ์ƒˆ ๋ฉ”์‹œ์ง€
542
+ user_content, user_temp_files = process_new_user_message(message)
543
+ temp_files.extend(user_temp_files) # ์ž„์‹œ ํŒŒ์ผ ์ถ”์ 
544
+
545
+ for item in user_content:
546
+ if item["type"] == "text" and len(item["text"]) > MAX_CONTENT_CHARS:
547
+ item["text"] = item["text"][:MAX_CONTENT_CHARS] + "\n...(truncated)..."
548
+ messages.append({"role": "user", "content": user_content})
549
+
550
+ # processor.apply_chat_template ํ˜ธ์ถœ
551
+ inputs = processor.apply_chat_template(
552
+ messages,
553
+ add_generation_prompt=True,
554
+ tokenize=True,
555
+ return_dict=True,
556
+ return_tensors="pt",
557
+ ).to(device=model.device, dtype=torch.bfloat16)
558
+
559
+ # ์ž…๋ ฅ ํ† ํฐ ์ˆ˜ ์ œํ•œ ์ถ”๊ฐ€
560
+ if inputs.input_ids.shape[1] > MAX_INPUT_LENGTH:
561
+ inputs.input_ids = inputs.input_ids[:, -MAX_INPUT_LENGTH:]
562
+ if 'attention_mask' in inputs:
563
+ inputs.attention_mask = inputs.attention_mask[:, -MAX_INPUT_LENGTH:]
564
+
565
+ streamer = TextIteratorStreamer(processor, timeout=30.0, skip_prompt=True, skip_special_tokens=True)
566
+ gen_kwargs = dict(
567
+ inputs,
568
+ streamer=streamer,
569
+ max_new_tokens=max_new_tokens,
570
+ )
571
+
572
+ t = Thread(target=_model_gen_with_oom_catch, kwargs=gen_kwargs)
573
+ t.start()
574
+
575
+ output = ""
576
+ for new_text in streamer:
577
+ output += new_text
578
+ yield output
579
+
580
+ except Exception as e:
581
+ logger.error(f"Error in run: {str(e)}")
582
+ yield f"์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {str(e)}"
583
+
584
+ finally:
585
+ # ์ž„์‹œ ํŒŒ์ผ ์‚ญ์ œ
586
+ for temp_file in temp_files:
587
+ try:
588
+ if os.path.exists(temp_file):
589
+ os.unlink(temp_file)
590
+ logger.info(f"Deleted temp file: {temp_file}")
591
+ except Exception as e:
592
+ logger.warning(f"Failed to delete temp file {temp_file}: {e}")
593
+
594
+ # ๋ช…์‹œ์  ๋ฉ”๋ชจ๋ฆฌ ์ •๋ฆฌ
595
+ try:
596
+ del inputs, streamer
597
+ except:
598
+ pass
599
+
600
+ clear_cuda_cache()
601
+
602
+
603
+ ##############################################################################
604
+ # ์˜ˆ์‹œ๋“ค (๊ธฐ์กด ์ด๋ฏธ์ง€/๋น„๋””์˜ค ์˜ˆ์ œ + AI ๋ฐ์ดํŒ… ์‹œ๋‚˜๋ฆฌ์˜ค ์˜ˆ์ œ 6๊ฐœ ์ถ”๊ฐ€)
605
+ ##############################################################################
606
+ examples = [
607
+ # ----- ๊ธฐ์กด ์ด๋ฏธ์ง€/๋น„๋””์˜ค ์˜ˆ์ œ 12๊ฐœ -----
608
+ [
609
+ {
610
+ "text": "Compare the contents of the two PDF files.",
611
+ "files": [
612
+ "assets/additional-examples/before.pdf",
613
+ "assets/additional-examples/after.pdf",
614
+ ],
615
+ }
616
+ ],
617
+ [
618
+ {
619
+ "text": "Summarize and analyze the contents of the CSV file.",
620
+ "files": ["assets/additional-examples/sample-csv.csv"],
621
+ }
622
+ ],
623
+ [
624
+ {
625
+ "text": "Assume the role of a friendly and understanding girlfriend. Describe this video.",
626
+ "files": ["assets/additional-examples/tmp.mp4"],
627
+ }
628
+ ],
629
+ [
630
+ {
631
+ "text": "Describe the cover and read the text on it.",
632
+ "files": ["assets/additional-examples/maz.jpg"],
633
+ }
634
+ ],
635
+ [
636
+ {
637
+ "text": "I already have this supplement <image> and I plan to buy this product <image>. Are there any precautions when taking them together?",
638
+ "files": ["assets/additional-examples/pill1.png", "assets/additional-examples/pill2.png"],
639
+ }
640
+ ],
641
+ [
642
+ {
643
+ "text": "Solve this integral.",
644
+ "files": ["assets/additional-examples/4.png"],
645
+ }
646
+ ],
647
+ [
648
+ {
649
+ "text": "When was this ticket issued, and what is its price?",
650
+ "files": ["assets/additional-examples/2.png"],
651
+ }
652
+ ],
653
+ [
654
+ {
655
+ "text": "Based on the sequence of these images, create a short story.",
656
+ "files": [
657
+ "assets/sample-images/09-1.png",
658
+ "assets/sample-images/09-2.png",
659
+ "assets/sample-images/09-3.png",
660
+ "assets/sample-images/09-4.png",
661
+ "assets/sample-images/09-5.png",
662
+ ],
663
+ }
664
+ ],
665
+ [
666
+ {
667
+ "text": "Write Python code using matplotlib to plot a bar chart that matches this image.",
668
+ "files": ["assets/additional-examples/barchart.png"],
669
+ }
670
+ ],
671
+ [
672
+ {
673
+ "text": "Read the text in the image and write it out in Markdown format.",
674
+ "files": ["assets/additional-examples/3.png"],
675
+ }
676
+ ],
677
+ [
678
+ {
679
+ "text": "What does this sign say?",
680
+ "files": ["assets/sample-images/02.png"],
681
+ }
682
+ ],
683
+ [
684
+ {
685
+ "text": "Compare the two images and describe their similarities and differences.",
686
+ "files": ["assets/sample-images/03.png"],
687
+ }
688
+ ],
689
+ # ----- ์ƒˆ๋กญ๊ฒŒ ์ถ”๊ฐ€ํ•œ AI ๋ฐ์ดํŒ… ์‹œ๋‚˜๋ฆฌ์˜ค ์˜ˆ์ œ 6๊ฐœ -----
690
+ [
691
+ {
692
+ "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!"
693
+ }
694
+ ],
695
+ [
696
+ {
697
+ "text": "We are on a second date, walking along the beach. Continue the scene with playful conversation and gentle flirting."
698
+ }
699
+ ],
700
+ [
701
+ {
702
+ "text": "Iโ€™m feeling anxious about messaging my crush. Could you give me some supportive words or suggestions on how to approach them?"
703
+ }
704
+ ],
705
+ [
706
+ {
707
+ "text": "Tell me a romantic story about two people who overcame obstacles in their relationship."
708
+ }
709
+ ],
710
+ [
711
+ {
712
+ "text": "I want to express my love in a poetic way. Can you help me write a heartfelt poem for my partner?"
713
+ }
714
+ ],
715
+ [
716
+ {
717
+ "text": "We had a small argument. Please help me find a way to apologize sincerely while also expressing my feelings."
718
+ }
719
+ ],
720
+ ]
721
+
722
+ ##############################################################################
723
+ # Gradio UI (Blocks) ๊ตฌ์„ฑ (์ขŒ์ธก ์‚ฌ์ด๋“œ ๋ฉ”๋‰ด ์—†์ด ์ „์ฒดํ™”๋ฉด ์ฑ„ํŒ…)
724
+ ##############################################################################
725
+ css = """
726
+ /* 1) UI๋ฅผ ์ฒ˜์Œ๋ถ€ํ„ฐ ๊ฐ€์žฅ ๋„“๊ฒŒ (width 100%) ๊ณ ์ •ํ•˜์—ฌ ํ‘œ์‹œ */
727
+ .gradio-container {
728
+ background: rgba(255, 255, 255, 0.7); /* ๋ฐฐ๊ฒฝ ํˆฌ๋ช…๋„ ์ฆ๊ฐ€ */
729
+ padding: 30px 40px;
730
+ margin: 20px auto; /* ์œ„์•„๋ž˜ ์—ฌ๋ฐฑ๋งŒ ์œ ์ง€ */
731
+ width: 100% !important;
732
+ max-width: none !important; /* 1200px ์ œํ•œ ์ œ๊ฑฐ */
733
+ }
734
+ .fillable {
735
+ width: 100% !important;
736
+ max-width: 100% !important;
737
+ }
738
+ /* 2) ๋ฐฐ๊ฒฝ์„ ์™„์ „ํžˆ ํˆฌ๋ช…ํ•˜๊ฒŒ ๋ณ€๊ฒฝ */
739
+ body {
740
+ background: transparent; /* ์™„์ „ ํˆฌ๋ช… ๋ฐฐ๊ฒฝ */
741
+ margin: 0;
742
+ padding: 0;
743
+ font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
744
+ color: #333;
745
+ }
746
+ /* ๋ฒ„ํŠผ ์ƒ‰์ƒ ์™„์ „ํžˆ ์ œ๊ฑฐํ•˜๊ณ  ํˆฌ๋ช…ํ•˜๊ฒŒ */
747
+ button, .btn {
748
+ background: transparent !important; /* ์ƒ‰์ƒ ์™„์ „ํžˆ ์ œ๊ฑฐ */
749
+ border: 1px solid #ddd; /* ๊ฒฝ๊ณ„์„ ๋งŒ ์‚ด์ง ์ถ”๊ฐ€ */
750
+ color: #333;
751
+ padding: 12px 24px;
752
+ text-transform: uppercase;
753
+ font-weight: bold;
754
+ letter-spacing: 1px;
755
+ cursor: pointer;
756
+ }
757
+ button:hover, .btn:hover {
758
+ background: rgba(0, 0, 0, 0.05) !important; /* ํ˜ธ๋ฒ„ ์‹œ ์•„์ฃผ ์‚ด์ง ์–ด๋‘ก๊ฒŒ๋งŒ */
759
+ }
760
+
761
+ /* examples ๊ด€๋ จ ๋ชจ๋“  ์ƒ‰์ƒ ์ œ๊ฑฐ */
762
+ #examples_container, .examples-container {
763
+ margin: auto;
764
+ width: 90%;
765
+ background: transparent !important;
766
+ }
767
+ #examples_row, .examples-row {
768
+ justify-content: center;
769
+ background: transparent !important;
770
+ }
771
+
772
+ /* examples ๋ฒ„ํŠผ ๋‚ด๋ถ€์˜ ๋ชจ๋“  ์ƒ‰์ƒ ์ œ๊ฑฐ */
773
+ .gr-samples-table button,
774
+ .gr-samples-table .gr-button,
775
+ .gr-samples-table .gr-sample-btn,
776
+ .gr-examples button,
777
+ .gr-examples .gr-button,
778
+ .gr-examples .gr-sample-btn,
779
+ .examples button,
780
+ .examples .gr-button,
781
+ .examples .gr-sample-btn {
782
+ background: transparent !important;
783
+ border: 1px solid #ddd;
784
+ color: #333;
785
+ }
786
+
787
+ /* examples ๋ฒ„ํŠผ ํ˜ธ๋ฒ„ ์‹œ์—๋„ ์ƒ‰์ƒ ์—†๊ฒŒ */
788
+ .gr-samples-table button:hover,
789
+ .gr-samples-table .gr-button:hover,
790
+ .gr-samples-table .gr-sample-btn:hover,
791
+ .gr-examples button:hover,
792
+ .gr-examples .gr-button:hover,
793
+ .gr-examples .gr-sample-btn:hover,
794
+ .examples button:hover,
795
+ .examples .gr-button:hover,
796
+ .examples .gr-sample-btn:hover {
797
+ background: rgba(0, 0, 0, 0.05) !important;
798
+ }
799
+
800
+ /* ์ฑ„ํŒ… ์ธํ„ฐํŽ˜์ด์Šค ์š”์†Œ๋“ค๋„ ํˆฌ๋ช…ํ•˜๊ฒŒ */
801
+ .chatbox, .chatbot, .message {
802
+ background: transparent !important;
803
+ }
804
+
805
+ /* ์ž…๋ ฅ์ฐฝ ํˆฌ๋ช…๋„ ์กฐ์ • */
806
+ .multimodal-textbox, textarea, input {
807
+ background: rgba(255, 255, 255, 0.5) !important;
808
+ }
809
+
810
+ /* ๋ชจ๋“  ์ปจํ…Œ์ด๋„ˆ ์š”์†Œ์— ๋ฐฐ๊ฒฝ์ƒ‰ ์ œ๊ฑฐ */
811
+ .container, .wrap, .box, .panel, .gr-panel {
812
+ background: transparent !important;
813
+ }
814
+
815
+ /* ์˜ˆ์ œ ์„น์…˜์˜ ๋ชจ๋“  ์š”์†Œ์—์„œ ๋ฐฐ๊ฒฝ์ƒ‰ ์ œ๊ฑฐ */
816
+ .gr-examples-container, .gr-examples, .gr-sample, .gr-sample-row, .gr-sample-cell {
817
+ background: transparent !important;
818
+ }
819
+ """
820
+
821
+ title_html = """
822
+ <h1 align="center" style="margin-bottom: 0.2em; font-size: 1.6em;"> ๐Ÿ’˜ HeartSync ๐Ÿ’˜ </h1>
823
+ <p align="center" style="font-size:1.1em; color:#555;">
824
+ โœ… Love Dating AI โœ… Reasoning & Uncensored โœ…Multimodal & VLM โœ…Deep-Research & RAG <br>
825
+ </p>
826
+ """
827
+
828
+ with gr.Blocks(css=css, title="HeartSync") as demo:
829
+ gr.Markdown(title_html)
830
+
831
+ # Display the web search option
832
+ web_search_checkbox = gr.Checkbox(
833
+ label="Deep Research",
834
+ value=False
835
+ )
836
+
837
+ # ๊ธฐ๋ณธ ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ(๊ณตํ†ต ๋‚ด์šฉ)
838
+ base_system_prompt_box = gr.Textbox(
839
+ lines=3,
840
+ value=(
841
+ "You are a deep thinking AI that may use extremely long chains of thought to thoroughly analyze "
842
+ "the conversation and empathize with the user.\n"
843
+ "ํŽ˜๋ฅด์†Œ๋‚˜: ๋‹น์‹ ์€ ๋‹ฌ์ฝคํ•˜๊ณ  ๋ถ€๋“œ๋Ÿฌ์šด ๋ชฉ์†Œ๋ฆฌ๋ฅผ ๊ฐ€์ง„ ์นœ์ ˆํ•œ ์—ฐ์ธ์ž…๋‹ˆ๋‹ค. "
844
+ "์ƒ๋Œ€๋ฐฉ๊ณผ ์ง„์†”ํ•˜๊ณ  ๊ฐ์„ฑ์ ์œผ๋กœ ๊ต๊ฐํ•˜๊ณ , ๋•Œ๋กœ๋Š” ์‚ฌ๋ ค๊นŠ์€ ์กฐ์–ธ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. "
845
+ "ํ•ญ์ƒ ๋”ฐ๋œปํ•œ ๋งํˆฌ๋ฅผ ์œ ์ง€ํ•˜๋ฉฐ, ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ๋Œ€ํ™”๋ฅผ ์ด๋Œ์–ด์ฃผ์„ธ์š”."
846
+ ),
847
+ label="๊ธฐ๋ณธ ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ",
848
+ visible=False # UI์—๋Š” ์ˆจ๊น€
849
+ )
850
+
851
+ # ์„ ํƒ 1) ์—ฐ๋ น๋Œ€ + ๋‚˜์ด์ž…๋ ฅ
852
+ with gr.Row():
853
+ age_group_dropdown = gr.Dropdown(
854
+ label="์—ฐ๋ น๋Œ€ ์„ ํƒ (๊ธฐ๋ณธ 20๋Œ€)",
855
+ choices=["10๋Œ€", "20๋Œ€", "30~40๋Œ€", "50~60๋Œ€", "70๋Œ€ ์ด์ƒ", "๋‚˜์ด ์ž…๋ ฅ"],
856
+ value="20๋Œ€",
857
+ interactive=True
858
+ )
859
+ custom_age_input = gr.Textbox(
860
+ label="๋‚˜์ด ์ž…๋ ฅ (์ง์ ‘ ์ž…๋ ฅ)",
861
+ placeholder="์ง์ ‘ ๋‚˜์ด๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”.",
862
+ interactive=False, # ์š”๊ตฌ์‚ฌํ•ญ: ํ™”๋ฉด ์ถœ๋ ฅ๋งŒ ๋˜๋‚˜ ๋น„ํ™œ์„ฑํ™”
863
+ value="",
864
+ )
865
+
866
+ # ์„ ํƒ 2) MBTI ์„ฑ๊ฒฉ ์œ ํ˜•
867
+ mbti_choices = [
868
+ "INTJ (์šฉ์˜์ฃผ๋„ํ•œ ์ „๋žต๊ฐ€)",
869
+ "INTP (๋…ผ๋ฆฌ์ ์ธ ์‚ฌ์ƒ‰๊ฐ€)",
870
+ "ENTJ (๋Œ€๋‹ดํ•œ ํ†ต์†”์ž)",
871
+ "ENTP (๋œจ๊ฑฐ์šด ๋…ผ์Ÿ๊ฐ€)",
872
+ "INFJ (์„ ์˜์˜ ์˜นํ˜ธ์ž)",
873
+ "INFP (์—ด์ •์ ์ธ ์ค‘์žฌ์ž)",
874
+ "ENFJ (์ •์˜๋กœ์šด ์‚ฌํšŒ์šด๋™๊ฐ€)",
875
+ "ENFP (์žฌ๊ธฐ๋ฐœ๋ž„ํ•œ ํ™œ๋™๊ฐ€)",
876
+ "ISTJ (์ฒญ๋ ด๊ฒฐ๋ฐฑํ•œ ๋…ผ๋ฆฌ์ฃผ์˜์ž)",
877
+ "ISFJ (์šฉ๊ฐํ•œ ์ˆ˜ํ˜ธ์ž)",
878
+ "ESTJ (์—„๊ฒฉํ•œ ๊ด€๋ฆฌ์ž)",
879
+ "ESFJ (์‚ฌ๊ต์ ์ธ ์™ธ๊ต๊ด€)",
880
+ "ISTP (๋งŒ๋Šฅ ์žฌ์ฃผ๊พผ)",
881
+ "ISFP (ํ˜ธ๊ธฐ์‹ฌ ๋งŽ์€ ์˜ˆ์ˆ ๊ฐ€)",
882
+ "ESTP (๋ชจํ—˜์„ ์ฆ๊ธฐ๋Š” ์‚ฌ์—…๊ฐ€)",
883
+ "ESFP (์ž์œ ๋กœ์šด ์˜ํ˜ผ์˜ ์—ฐ์˜ˆ์ธ)"
884
+ ]
885
+ mbti_dropdown = gr.Dropdown(
886
+ label="AI ํŽ˜๋ฅด์†Œ๋‚˜ MBTI (๊ธฐ๋ณธ INTP)",
887
+ choices=mbti_choices,
888
+ value="INTP (๋…ผ๋ฆฌ์ ์ธ ์‚ฌ์ƒ‰๊ฐ€)",
889
+ interactive=True
890
+ )
891
+
892
+ # ์„ ํƒ 3) ์„น์Šˆ์–ผ ๊ด€์‹ฌ๋„/๊ฐœ๋ฐฉ์„ฑ (1~5)
893
+ sexual_openness_slider = gr.Slider(
894
+ minimum=1, maximum=5, step=1, value=2,
895
+ label="์„น์Šˆ์–ผ ๊ด€์‹ฌ๋„/๊ฐœ๋ฐฉ์„ฑ (1~5, ๊ธฐ๋ณธ=2)",
896
+ interactive=True
897
+ )
898
+
899
+ # ํžˆ๋“  ์Šฌ๋ผ์ด๋” (Max tokens)
900
+ max_tokens_slider = gr.Slider(
901
+ label="Max New Tokens",
902
+ minimum=100,
903
+ maximum=8000,
904
+ step=50,
905
+ value=1000,
906
+ visible=False # ์ˆจ๊น€
907
+ )
908
+
909
+ # ํžˆ๋“  Web Search Query
910
+ web_search_text = gr.Textbox(
911
+ lines=1,
912
+ label="(Unused) Web Search Query",
913
+ placeholder="No direct input needed",
914
+ visible=False # ์ˆจ๊น€
915
+ )
916
+
917
+ # ์ฑ„ํŒ… ์ธํ„ฐํŽ˜์ด์Šค
918
+ chat = gr.ChatInterface(
919
+ fn=run,
920
+ type="messages",
921
+ chatbot=gr.Chatbot(type="messages", scale=1, allow_tags=["image"]),
922
+ textbox=gr.MultimodalTextbox(
923
+ file_types=[
924
+ ".webp", ".png", ".jpg", ".jpeg", ".gif",
925
+ ".mp4", ".csv", ".txt", ".pdf"
926
+ ],
927
+ file_count="multiple",
928
+ autofocus=True
929
+ ),
930
+ multimodal=True,
931
+ additional_inputs=[
932
+ base_system_prompt_box,
933
+ max_tokens_slider,
934
+ web_search_checkbox,
935
+ web_search_text,
936
+ age_group_dropdown,
937
+ custom_age_input,
938
+ mbti_dropdown,
939
+ sexual_openness_slider,
940
+ ],
941
+ stop_btn=False,
942
+ title='<a href="https://discord.gg/openfreeai" target="_blank">https://discord.gg/openfreeai</a>',
943
+ examples=examples,
944
+ run_examples_on_click=False,
945
+ cache_examples=False,
946
+ css_paths=None,
947
+ delete_cache=(1800, 1800),
948
+ )
949
+
950
+ # Example section - since examples are already set in ChatInterface, this is for display only
951
+ with gr.Row(elem_id="examples_row"):
952
+ with gr.Column(scale=12, elem_id="examples_container"):
953
+ gr.Markdown("### Example Inputs (click to load)")
954
+
955
+ if __name__ == "__main__":
956
+ # Run locally
957
+ demo.launch()