ginipick commited on
Commit
c0a4182
ยท
verified ยท
1 Parent(s): 780612c

Delete app-backup.py

Browse files
Files changed (1) hide show
  1. app-backup.py +0 -847
app-backup.py DELETED
@@ -1,847 +0,0 @@
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
- # ๋ฉ”์ธ ์ถ”๋ก  ํ•จ์ˆ˜ (web search ์ฒดํฌ ์‹œ ์ž๋™ ํ‚ค์›Œ๋“œ์ถ”์ถœ->๊ฒ€์ƒ‰->๊ฒฐ๊ณผ system msg)
471
- ##############################################################################
472
- @spaces.GPU(duration=120)
473
- def run(
474
- message: dict,
475
- history: list[dict],
476
- system_prompt: str = "",
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,
534
- tokenize=True,
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
- ##############################################################################
585
- # ์˜ˆ์‹œ๋“ค (๋ชจ๋‘ ์˜์–ด๋กœ)
586
- ##############################################################################
587
- examples = [
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",
594
- ],
595
- }
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",
639
- "assets/sample-images/09-3.png",
640
- "assets/sample-images/09-4.png",
641
- "assets/sample-images/09-5.png",
642
- ],
643
- }
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
- ]
670
-
671
- ##############################################################################
672
- # Gradio UI (Blocks) ๊ตฌ์„ฑ (์ขŒ์ธก ์‚ฌ์ด๋“œ ๋ฉ”๋‰ด ์—†์ด ์ „์ฒดํ™”๋ฉด ์ฑ„ํŒ…)
673
- ##############################################################################
674
- css = """
675
- /* 1) UI๋ฅผ ์ฒ˜์Œ๋ถ€ํ„ฐ ๊ฐ€์žฅ ๋„“๊ฒŒ (width 100%) ๊ณ ์ •ํ•˜์—ฌ ํ‘œ์‹œ */
676
- .gradio-container {
677
- background: rgba(255, 255, 255, 0.7); /* ๋ฐฐ๊ฒฝ ํˆฌ๋ช…๋„ ์ฆ๊ฐ€ */
678
- padding: 30px 40px;
679
- margin: 20px auto; /* ์œ„์•„๋ž˜ ์—ฌ๋ฐฑ๋งŒ ์œ ์ง€ */
680
- width: 100% !important;
681
- max-width: none !important; /* 1200px ์ œํ•œ ์ œ๊ฑฐ */
682
- }
683
- .fillable {
684
- width: 100% !important;
685
- max-width: 100% !important;
686
- }
687
- /* 2) ๋ฐฐ๊ฒฝ์„ ์™„์ „ํžˆ ํˆฌ๋ช…ํ•˜๊ฒŒ ๋ณ€๊ฒฝ */
688
- body {
689
- background: transparent; /* ์™„์ „ ํˆฌ๋ช… ๋ฐฐ๊ฒฝ */
690
- margin: 0;
691
- padding: 0;
692
- font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
693
- color: #333;
694
- }
695
- /* ๋ฒ„ํŠผ ์ƒ‰์ƒ ์™„์ „ํžˆ ์ œ๊ฑฐํ•˜๊ณ  ํˆฌ๋ช…ํ•˜๊ฒŒ */
696
- button, .btn {
697
- background: transparent !important; /* ์ƒ‰์ƒ ์™„์ „ํžˆ ์ œ๊ฑฐ */
698
- border: 1px solid #ddd; /* ๊ฒฝ๊ณ„์„ ๋งŒ ์‚ด์ง ์ถ”๊ฐ€ */
699
- color: #333;
700
- padding: 12px 24px;
701
- text-transform: uppercase;
702
- font-weight: bold;
703
- letter-spacing: 1px;
704
- cursor: pointer;
705
- }
706
- button:hover, .btn:hover {
707
- background: rgba(0, 0, 0, 0.05) !important; /* ํ˜ธ๋ฒ„ ์‹œ ์•„์ฃผ ์‚ด์ง ์–ด๋‘ก๊ฒŒ๋งŒ */
708
- }
709
-
710
- /* examples ๊ด€๋ จ ๋ชจ๋“  ์ƒ‰์ƒ ์ œ๊ฑฐ */
711
- #examples_container, .examples-container {
712
- margin: auto;
713
- width: 90%;
714
- background: transparent !important;
715
- }
716
- #examples_row, .examples-row {
717
- justify-content: center;
718
- background: transparent !important;
719
- }
720
-
721
- /* examples ๋ฒ„ํŠผ ๋‚ด๋ถ€์˜ ๋ชจ๋“  ์ƒ‰์ƒ ์ œ๊ฑฐ */
722
- .gr-samples-table button,
723
- .gr-samples-table .gr-button,
724
- .gr-samples-table .gr-sample-btn,
725
- .gr-examples button,
726
- .gr-examples .gr-button,
727
- .gr-examples .gr-sample-btn,
728
- .examples button,
729
- .examples .gr-button,
730
- .examples .gr-sample-btn {
731
- background: transparent !important;
732
- border: 1px solid #ddd;
733
- color: #333;
734
- }
735
-
736
- /* examples ๋ฒ„ํŠผ ํ˜ธ๋ฒ„ ์‹œ์—๋„ ์ƒ‰์ƒ ์—†๊ฒŒ */
737
- .gr-samples-table button:hover,
738
- .gr-samples-table .gr-button:hover,
739
- .gr-samples-table .gr-sample-btn:hover,
740
- .gr-examples button:hover,
741
- .gr-examples .gr-button:hover,
742
- .gr-examples .gr-sample-btn:hover,
743
- .examples button:hover,
744
- .examples .gr-button:hover,
745
- .examples .gr-sample-btn:hover {
746
- background: rgba(0, 0, 0, 0.05) !important;
747
- }
748
-
749
- /* ์ฑ„ํŒ… ์ธํ„ฐํŽ˜์ด์Šค ์š”์†Œ๋“ค๋„ ํˆฌ๋ช…ํ•˜๊ฒŒ */
750
- .chatbox, .chatbot, .message {
751
- background: transparent !important;
752
- }
753
-
754
- /* ์ž…๋ ฅ์ฐฝ ํˆฌ๋ช…๋„ ์กฐ์ • */
755
- .multimodal-textbox, textarea, input {
756
- background: rgba(255, 255, 255, 0.5) !important;
757
- }
758
-
759
- /* ๋ชจ๋“  ์ปจํ…Œ์ด๋„ˆ ์š”์†Œ์— ๋ฐฐ๊ฒฝ์ƒ‰ ์ œ๊ฑฐ */
760
- .container, .wrap, .box, .panel, .gr-panel {
761
- background: transparent !important;
762
- }
763
-
764
- /* ์˜ˆ์ œ ์„น์…˜์˜ ๋ชจ๋“  ์š”์†Œ์—์„œ ๋ฐฐ๊ฒฝ์ƒ‰ ์ œ๊ฑฐ */
765
- .gr-examples-container, .gr-examples, .gr-sample, .gr-sample-row, .gr-sample-cell {
766
- background: transparent !important;
767
- }
768
- """
769
-
770
- title_html = """
771
- <h1 align="center" style="margin-bottom: 0.2em; font-size: 1.6em;"> ๐Ÿ’˜ HeartSync๐Ÿ’˜ </h1>
772
- <p align="center" style="font-size:1.1em; color:#555;">
773
- โœ… Love Dating AI โœ…Reasoning & Uncensored โœ…Multimodal & VLM โœ…Deep-Research & RAG <br>
774
- </p>
775
- """
776
-
777
-
778
- with gr.Blocks(css=css, title="HeartSync") as demo:
779
- gr.Markdown(title_html)
780
-
781
- # Display the web search option (while the system prompt and token slider remain hidden)
782
- web_search_checkbox = gr.Checkbox(
783
- label="Deep Research",
784
- value=False
785
- )
786
-
787
- # Used internally but not visible to the user
788
- system_prompt_box = gr.Textbox(
789
- lines=3,
790
- value="You are a deep thinking AI that may use extremely long chains of thought to thoroughly analyze the problem and deliberate using systematic reasoning processes to arrive at a correct solution before answering.",
791
- visible=False # hidden from view
792
- )
793
-
794
- max_tokens_slider = gr.Slider(
795
- label="Max New Tokens",
796
- minimum=100,
797
- maximum=8000,
798
- step=50,
799
- value=1000,
800
- visible=False # hidden from view
801
- )
802
-
803
- web_search_text = gr.Textbox(
804
- lines=1,
805
- label="(Unused) Web Search Query",
806
- placeholder="No direct input needed",
807
- visible=False # hidden from view
808
- )
809
-
810
- # Configure the chat interface
811
- chat = gr.ChatInterface(
812
- fn=run,
813
- type="messages",
814
- chatbot=gr.Chatbot(type="messages", scale=1, allow_tags=["image"]),
815
- textbox=gr.MultimodalTextbox(
816
- file_types=[
817
- ".webp", ".png", ".jpg", ".jpeg", ".gif",
818
- ".mp4", ".csv", ".txt", ".pdf"
819
- ],
820
- file_count="multiple",
821
- autofocus=True
822
- ),
823
- multimodal=True,
824
- additional_inputs=[
825
- system_prompt_box,
826
- max_tokens_slider,
827
- web_search_checkbox,
828
- web_search_text,
829
- ],
830
- stop_btn=False,
831
- title='<a href="https://discord.gg/openfreeai" target="_blank">https://discord.gg/openfreeai</a>',
832
- examples=examples,
833
- run_examples_on_click=False,
834
- cache_examples=False,
835
- css_paths=None,
836
- delete_cache=(1800, 1800),
837
- )
838
-
839
- # Example section - since examples are already set in ChatInterface, this is for display only
840
- with gr.Row(elem_id="examples_row"):
841
- with gr.Column(scale=12, elem_id="examples_container"):
842
- gr.Markdown("### Example Inputs (click to load)")
843
-
844
-
845
- if __name__ == "__main__":
846
- # Run locally
847
- demo.launch()