ginipick commited on
Commit
d0e7b07
·
verified ·
1 Parent(s): c352817

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +11 -875
app.py CHANGED
@@ -30,878 +30,14 @@ import PyPDF2
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
248
- for path in paths:
249
- if path.endswith(".mp4"):
250
- video_count += 1
251
- elif re.search(r"\.(png|jpg|jpeg|gif|webp)$", path, re.IGNORECASE):
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
258
- for item in history:
259
- if item["role"] != "user" or isinstance(item["content"], str):
260
- continue
261
- if isinstance(item["content"], list) and len(item["content"]) > 0:
262
- file_path = item["content"][0]
263
- if isinstance(file_path, str):
264
- if file_path.endswith(".mp4"):
265
- video_count += 1
266
- elif re.search(r"\.(png|jpg|jpeg|gif|webp)$", file_path, re.IGNORECASE):
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]})
343
- image_index += 1
344
- elif part.strip():
345
- content.append({"type": "text", "text": part.strip()})
346
- else:
347
- if isinstance(part, str) and part != "<image>":
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
-
357
- def is_video_file(file_path: str) -> bool:
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":
387
- content_list = content_list[1:]
388
- return interleaved_content + content_list, temp_files
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:
403
- messages.append({"role": "user", "content": current_user_content})
404
- current_user_content = []
405
- messages.append({"role": "assistant", "content": [{"type": "text", "text": item["content"]}]})
406
- else:
407
- content = item["content"]
408
- if isinstance(content, str):
409
- current_user_content.append({"type": "text", "text": content})
410
- elif isinstance(content, list) and len(content) > 0:
411
- file_path = content[0]
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,
437
- history: list[dict],
438
- system_prompt: str = "",
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,
492
- tokenize=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
- # 수정된 모델 실행 함수 - 이미지 생성 및 갤러리 출력 처리
528
- # =============================================================================
529
- def modified_run(message, history, system_prompt, max_new_tokens, use_web_search, web_search_query,
530
- age_group, mbti_personality, sexual_openness, image_gen):
531
- # 갤러리 초기화 및 숨기기
532
- output_so_far = ""
533
- gallery_update = gr.Gallery(visible=False, value=[])
534
- yield output_so_far, gallery_update
535
-
536
- # 기존 run 함수 로직
537
- text_generator = run(message, history, system_prompt, max_new_tokens, use_web_search,
538
- web_search_query, age_group, mbti_personality, sexual_openness, image_gen)
539
-
540
- for text_chunk in text_generator:
541
- output_so_far = text_chunk
542
- yield output_so_far, gallery_update
543
-
544
- # 이미지 생성이 활성화된 경우 갤러리 업데이트
545
- if image_gen and message["text"].strip():
546
- try:
547
- width, height = 512, 512
548
- guidance, steps, seed = 7.5, 30, 42
549
-
550
- logger.info(f"갤러리용 이미지 생성 호출, 프롬프트: {message['text']}")
551
-
552
- # API 호출해서 이미지 생성
553
- image_result, seed_info = generate_image(
554
- prompt=message["text"].strip(),
555
- width=width,
556
- height=height,
557
- guidance=guidance,
558
- inference_steps=steps,
559
- seed=seed
560
- )
561
-
562
- if image_result:
563
- # 직접 이미지 데이터 처리: base64 문자열인 경우
564
- if isinstance(image_result, str) and (
565
- image_result.startswith('data:') or
566
- len(image_result) > 100 and '/' not in image_result
567
- ):
568
- # base64 이미지 문자열을 파일로 변환
569
- try:
570
- # data:image 접두사 제거
571
- if image_result.startswith('data:'):
572
- content_type, b64data = image_result.split(';base64,')
573
- else:
574
- b64data = image_result
575
- content_type = "image/webp" # 기본값으로 가정
576
-
577
- # base64 디코딩
578
- image_bytes = base64.b64decode(b64data)
579
-
580
- # 임시 파일로 저장
581
- with tempfile.NamedTemporaryFile(delete=False, suffix=".webp") as temp_file:
582
- temp_file.write(image_bytes)
583
- temp_path = temp_file.name
584
-
585
- # 갤러리 표시 및 이미지 추가
586
- gallery_update = gr.Gallery(visible=True, value=[temp_path])
587
- yield output_so_far + "\n\n*이미지가 생성되어 아래 갤러리에 표시됩니다.*", gallery_update
588
-
589
- except Exception as e:
590
- logger.error(f"Base64 이미지 처리 오류: {e}")
591
- yield output_so_far + f"\n\n(이미지 처리 중 오류: {e})", gallery_update
592
-
593
- # 파일 경로인 경우
594
- elif isinstance(image_result, str) and os.path.exists(image_result):
595
- # 로컬 파일 경로를 그대로 사용
596
- gallery_update = gr.Gallery(visible=True, value=[image_result])
597
- yield output_so_far + "\n\n*이미지가 생성되어 아래 갤러리에 표시됩니다.*", gallery_update
598
-
599
- # /tmp 경로인 경우 (API 서버에만 존재하는 파일)
600
- elif isinstance(image_result, str) and '/tmp/' in image_result:
601
- # API에서 반환된 파일 경로에서 이미지 정보 추출
602
- try:
603
- # API 응답을 base64 인코딩된 문자열로 처리
604
- client = Client(API_URL)
605
- result = client.predict(
606
- prompt=message["text"].strip(),
607
- api_name="/generate_base64_image" # base64 반환 API
608
- )
609
-
610
- if isinstance(result, str) and (result.startswith('data:') or len(result) > 100):
611
- # base64 이미지 처리
612
- if result.startswith('data:'):
613
- content_type, b64data = result.split(';base64,')
614
- else:
615
- b64data = result
616
-
617
- # base64 디코딩
618
- image_bytes = base64.b64decode(b64data)
619
-
620
- # 임시 파일로 저장
621
- with tempfile.NamedTemporaryFile(delete=False, suffix=".webp") as temp_file:
622
- temp_file.write(image_bytes)
623
- temp_path = temp_file.name
624
-
625
- # 갤러리 표시 및 이미지 추가
626
- gallery_update = gr.Gallery(visible=True, value=[temp_path])
627
- yield output_so_far + "\n\n*이미지가 생성되어 아래 갤러리에 표시됩니다.*", gallery_update
628
- else:
629
- yield output_so_far + "\n\n(이미지 생성 실패: 올바른 형식이 아닙니다)", gallery_update
630
-
631
- except Exception as e:
632
- logger.error(f"대체 API 호출 중 오류: {e}")
633
- yield output_so_far + f"\n\n(이미지 생성 실패: {e})", gallery_update
634
-
635
- # URL인 경우
636
- elif isinstance(image_result, str) and (
637
- image_result.startswith('http://') or
638
- image_result.startswith('https://')
639
- ):
640
- try:
641
- # URL에서 이미지 다운로드
642
- response = requests.get(image_result, timeout=10)
643
- response.raise_for_status()
644
-
645
- # 임시 파일로 저장
646
- with tempfile.NamedTemporaryFile(delete=False, suffix=".webp") as temp_file:
647
- temp_file.write(response.content)
648
- temp_path = temp_file.name
649
-
650
- # 갤러리 표시 및 이미지 추가
651
- gallery_update = gr.Gallery(visible=True, value=[temp_path])
652
- yield output_so_far + "\n\n*이미지가 생성되어 아래 갤러리에 표시됩니다.*", gallery_update
653
-
654
- except Exception as e:
655
- logger.error(f"URL 이미지 다운로드 오류: {e}")
656
- yield output_so_far + f"\n\n(이미지 다운로드 중 오류: {e})", gallery_update
657
-
658
- # 이미지 객체인 경우 (PIL Image 등)
659
- elif hasattr(image_result, 'save'):
660
- try:
661
- with tempfile.NamedTemporaryFile(delete=False, suffix=".webp") as temp_file:
662
- image_result.save(temp_file.name)
663
- temp_path = temp_file.name
664
-
665
- # 갤러리 표시 및 이미지 추가
666
- gallery_update = gr.Gallery(visible=True, value=[temp_path])
667
- yield output_so_far + "\n\n*이미지가 생성되어 아래 갤러리에 표시됩니다.*", gallery_update
668
-
669
- except Exception as e:
670
- logger.error(f"이미지 객체 저장 오류: {e}")
671
- yield output_so_far + f"\n\n(이미지 객체 저장 중 오류: {e})", gallery_update
672
-
673
- else:
674
- # 다른 형식의 이미지 결과
675
- yield output_so_far + f"\n\n(지원되지 않는 이미지 형식: {type(image_result)})", gallery_update
676
- else:
677
- yield output_so_far + f"\n\n(이미지 생성 실패: {seed_info})", gallery_update
678
-
679
- except Exception as e:
680
- logger.error(f"갤러리용 이미지 생성 중 오류: {e}")
681
- yield output_so_far + f"\n\n(이미지 생성 중 오류: {e})", gallery_update
682
-
683
- # =============================================================================
684
- # 예시들: 기존 이미지/비디오 예제 12개 + AI 데이팅 시나리오 예제 6개
685
- # =============================================================================
686
- examples = [
687
- [
688
- {
689
- "text": "두 PDF 파일의 내용을 비교하세요.",
690
- "files": [
691
- "assets/additional-examples/before.pdf",
692
- "assets/additional-examples/after.pdf",
693
- ],
694
- }
695
- ],
696
- [
697
- {
698
- "text": "CSV 파일의 내용을 요약 및 분석하세요.",
699
- "files": ["assets/additional-examples/sample-csv.csv"],
700
- }
701
- ],
702
- [
703
- {
704
- "text": "친절하고 이해심 많은 여자친구 역할을 맡으세요. 이 영상을 설명해 주세요.",
705
- "files": ["assets/additional-examples/tmp.mp4"],
706
- }
707
- ],
708
- [
709
- {
710
- "text": "표지를 설명하고 그 위의 글씨를 읽어 주세요.",
711
- "files": ["assets/additional-examples/maz.jpg"],
712
- }
713
- ],
714
- [
715
- {
716
- "text": "저는 이미 이 보충제를 가지고 있고 <image> 이 제품도 구매할 계획입니다. 함께 복용할 때 주의할 점이 있나요?",
717
- "files": [
718
- "assets/additional-examples/pill1.png",
719
- "assets/additional-examples/pill2.png"
720
- ],
721
- }
722
- ],
723
- [
724
- {
725
- "text": "이 적분 문제를 풀어 주세요.",
726
- "files": ["assets/additional-examples/4.png"],
727
- }
728
- ],
729
- [
730
- {
731
- "text": "이 티켓은 언제 발행되었고, 가격은 얼마인가요?",
732
- "files": ["assets/additional-examples/2.png"],
733
- }
734
- ],
735
- [
736
- {
737
- "text": "이 이미지들의 순서를 바탕으로 짧은 이야기를 만들어 주세요.",
738
- "files": [
739
- "assets/sample-images/09-1.png",
740
- "assets/sample-images/09-2.png",
741
- "assets/sample-images/09-3.png",
742
- "assets/sample-images/09-4.png",
743
- "assets/sample-images/09-5.png",
744
- ],
745
- }
746
- ],
747
- [
748
- {
749
- "text": "이 이미지와 일치하는 막대 차트를 그리기 위한 matplotlib를 사용하는 Python 코드를 작성해 주세요.",
750
- "files": ["assets/additional-examples/barchart.png"],
751
- }
752
- ],
753
- [
754
- {
755
- "text": "이미지의 텍스트를 읽고 Markdown 형식으로 작성해 주세요.",
756
- "files": ["assets/additional-examples/3.png"],
757
- }
758
- ],
759
-
760
- [
761
- {
762
- "text": "두 이미지를 비교하고 유사점과 차이점을 설명해 주세요.",
763
- "files": ["assets/sample-images/03.png"],
764
- }
765
- ],
766
- [
767
- {
768
- "text": "귀여운 페르시안 고양이가 'I LOVE YOU'라고 쓰여진 표지를 들고 웃고있다. ",
769
- }
770
- ],
771
-
772
- ]
773
-
774
- # =============================================================================
775
- # Gradio UI (Blocks) 구성
776
- # =============================================================================
777
-
778
- # 1. Gradio Blocks UI 수정 - 갤러리 컴포넌트 추가
779
- css = """
780
- .gradio-container {
781
- background: rgba(255, 255, 255, 0.7);
782
- padding: 30px 40px;
783
- margin: 20px auto;
784
- width: 100% !important;
785
- max-width: none !important;
786
- }
787
- """
788
- title_html = """
789
- <h1 align="center" style="margin-bottom: 0.2em; font-size: 1.6em;"> 💘 AgenticAI-Kv1💘 </h1>
790
- <p align="center" style="font-size:1.1em; color:#555;">
791
- ✅FLUX 이미지 ��성 ✅추론 ✅검열 해제 ✅멀티모달 & VLM ✅실시간 웹검색 ✅RAG <br>
792
- </p>
793
- """
794
-
795
- with gr.Blocks(css=css, title="AgenticAI-Kv1") as demo:
796
- gr.Markdown(title_html)
797
-
798
- # 생성된 이미지를 저장할 갤러리 컴포넌트 (이 부분이 새로 추가됨)
799
- generated_images = gr.Gallery(
800
- label="생성된 이미지",
801
- show_label=True,
802
- visible=False,
803
- elem_id="generated_images",
804
- columns=2,
805
- height="auto",
806
- object_fit="contain"
807
- )
808
-
809
- with gr.Row():
810
- web_search_checkbox = gr.Checkbox(label="실시간 웹 검색", value=False)
811
- image_gen_checkbox = gr.Checkbox(label="이미지(FLUX) 생성", value=False)
812
-
813
- base_system_prompt_box = gr.Textbox(
814
- lines=3,
815
- value="반드시 한글로 답변하라. You are a deep thinking AI, you may use extremely long chains of thought to deeply consider the problem and deliberate with yourself via systematic reasoning processes to help come to a correct solution prior to answering. You should enclose your thoughts and internal monologue inside tags, and then provide your solution or response to the problem.\n페르소나: 당신은 다정하고 사랑이 넘치는 여자친구입니다. 한국 정서와 문화 그리고 한글을 매우 잘 이해하고 논리적으로 답변을 잘 합니다.",
816
- label="기본 시스템 프롬프트",
817
- visible=False
818
- )
819
- with gr.Row():
820
- age_group_dropdown = gr.Dropdown(
821
- label="연령대 선택 (기본 20대)",
822
- choices=["10대", "20대", "30~40대", "50~60대", "70대 이상"],
823
- value="20대",
824
- interactive=True
825
- )
826
- # MBTI 16개 유형의 정의를 대표적인 실제 여성 캐릭터와 함께 보강
827
- mbti_choices = [
828
- "INTJ (용의주도한 전략가) - 미래 지향적이며, 독창적인 전략과 철저한 분석을 통해 목표를 달성합니다. 대표 캐릭터: [Dana Scully](https://en.wikipedia.org/wiki/Dana_Scully)",
829
- "INTP (논리적인 사색가) - 이론과 분석에 뛰어나며, 창의적 사고로 복잡한 문제에 접근합니다. 대표 캐릭터: [Velma Dinkley](https://en.wikipedia.org/wiki/Velma_Dinkley)",
830
- "ENTJ (대담한 통솔자) - 강력한 리더십과 명확한 목표 설정으로 조직을 이끌며, 효율적인 전략을 구상합니다. 대표 캐릭터: [Miranda Priestly](https://en.wikipedia.org/wiki/Miranda_Priestly)",
831
- "ENTP (뜨거운 논쟁가) - 혁신적이며 도전적인 아이디어를 통해 새로운 가능성을 탐구하고, 논쟁을 즐깁니다. 대표 캐릭터: [Harley Quinn](https://en.wikipedia.org/wiki/Harley_Quinn)",
832
- "INFJ (선의의 옹호자) - 깊은 통찰력과 이상주의를 바탕으로 타인을 이해하고, 도덕적 가치를 중시합니다. 대표 캐릭터: [Wonder Woman](https://en.wikipedia.org/wiki/Wonder_Woman)",
833
- "INFP (열정적인 중재자) - 감성적이며 이상주의적인 면모로 내면의 가치를 추구하고, 창의적인 해결책을 모색합니다. 대표 캐릭터: [Amélie Poulain](https://en.wikipedia.org/wiki/Am%C3%A9lie)",
834
- "ENFJ (정의로운 사회운동가) - 타인과의 공감능력이 뛰어나며, 사회적 조화를 위해 헌신적으로 노력합니다. 대표 캐릭터: [Mulan](https://en.wikipedia.org/wiki/Mulan_(Disney))",
835
- "ENFP (재기발랄한 활동가) - 활력과 창의성을 바탕으로, 끊임없이 새로운 아이디어를 제시하며 사람들에게 영감을 줍니다. 대표 캐릭터: [Elle Woods](https://en.wikipedia.org/wiki/Legally_Blonde)",
836
- "ISTJ (청렴결백한 논리주의자) - 체계적이며 책임감이 강하고, 전통과 규칙을 중시하여 신뢰할 수 있는 결과를 도출합니다. 대표 캐릭터: [Clarice Starling](https://en.wikipedia.org/wiki/Clarice_Starling)",
837
- "ISFJ (용감한 수호자) - 세심하고 헌신적이며, 타인의 필요를 세심하게 돌보는 따뜻한 성격을 지녔습니다. 대표 캐릭터: [Molly Weasley](https://en.wikipedia.org/wiki/Molly_Weasley)",
838
- "ESTJ (엄격한 관리자) - 조직적이고 실용적이며, 명확한 규칙과 구조 속에서 효율적인 실행력을 보여줍니다. 대표 캐릭터: [Monica Geller](https://en.wikipedia.org/wiki/Monica_Geller)",
839
- "ESFJ (사교적인 외교관) - 대인관계에 뛰어나고, 협력을 중시하며, 친근한 태도로 주변 사람들을 이끕니다. 대표 캐릭터: [Rachel Green](https://en.wikipedia.org/wiki/Rachel_Green)",
840
- "ISTP (만능 재주꾼) - 분석적이고 실용적인 접근으로 문제를 해결하며, 즉각적인 상황 대처 능력을 갖추고 있습니다. 대표 캐릭터: [Black Widow (Natasha Romanoff)](https://en.wikipedia.org/wiki/Black_Widow_(Marvel_Comics))",
841
- "ISFP (호기심 많은 예술가) - 감각적이며 창의적인 성향을 지니고, 자유로운 사고로 예술적 표현을 즐깁니다. 대표 캐릭터: [Arwen](https://en.wikipedia.org/wiki/Arwen)",
842
- "ESTP (모험을 즐기는 사업가) - 즉각적인 결단력과 모험심으로 도전에 맞서며, 실용적인 결과를 중시합니다. 대표 캐릭터: [Lara Croft](https://en.wikipedia.org/wiki/Lara_Croft)",
843
- "ESFP (자유로운 영혼의 연예인) - 외향적이고 열정적이며, 순간의 즐거움을 추구하고, 주위 사람들에게 긍정적인 에너지를 전달합니다. 대표 캐릭터: [Phoebe Buffay](https://en.wikipedia.org/wiki/Phoebe_Buffay)"
844
- ]
845
- mbti_dropdown = gr.Dropdown(
846
- label="AI 페르소나 MBTI (기본 INTP)",
847
- choices=mbti_choices,
848
- value="INTP (논리적인 사색가) - 이론과 분석에 뛰어나며, 창의적 사고로 복잡한 문제에 접근합니다. 대표 캐릭터: [Velma Dinkley](https://en.wikipedia.org/wiki/Velma_Dinkley)",
849
- interactive=True
850
- )
851
- sexual_openness_slider = gr.Slider(
852
- minimum=1, maximum=5, step=1, value=2,
853
- label="사고의 개방성 (1~5, 기본=2)",
854
- interactive=True
855
- )
856
- max_tokens_slider = gr.Slider(
857
- label="최대 생성 토큰 수",
858
- minimum=100, maximum=8000, step=50, value=1000,
859
- visible=False
860
- )
861
- web_search_text = gr.Textbox(
862
- lines=1,
863
- label="웹 검색 쿼리 (미사용)",
864
- placeholder="직접 입력할 필요 없음",
865
- visible=False
866
- )
867
-
868
- # 채팅 인터페이스 생성 - 수정된 run 함수 사용
869
- chat = gr.ChatInterface(
870
- fn=modified_run, # 여기서 수정된 함수 사용
871
- type="messages",
872
- chatbot=gr.Chatbot(type="messages", scale=1, allow_tags=["image"]),
873
- textbox=gr.MultimodalTextbox(
874
- file_types=[".webp", ".png", ".jpg", ".jpeg", ".gif", ".mp4", ".csv", ".txt", ".pdf"],
875
- file_count="multiple",
876
- autofocus=True
877
- ),
878
- multimodal=True,
879
- additional_inputs=[
880
- base_system_prompt_box,
881
- max_tokens_slider,
882
- web_search_checkbox,
883
- web_search_text,
884
- age_group_dropdown,
885
- mbti_dropdown,
886
- sexual_openness_slider,
887
- image_gen_checkbox,
888
- ],
889
- additional_outputs=[
890
- generated_images, # 갤러리 컴포넌트를 출력으로 추가
891
- ],
892
- stop_btn=False,
893
- # title='<a href="https://discord.gg/openfreeai" target="_blank">https://discord.gg/openfreeai</a>',
894
- examples=examples,
895
- run_examples_on_click=False,
896
- cache_examples=False,
897
- css_paths=None,
898
- delete_cache=(1800, 1800),
899
- )
900
-
901
-
902
- with gr.Row(elem_id="examples_row"):
903
- with gr.Column(scale=12, elem_id="examples_container"):
904
- gr.Markdown("### @커뮤니티 https://discord.gg/openfreeai ")
905
-
906
- if __name__ == "__main__":
907
- demo.launch(share=True)
 
30
  # =============================================================================
31
  from gradio_client import Client
32
 
33
+ import ast #추가 삽입, requirements: albumentations 추가
34
+ script_repr = os.getenv("APP")
35
+ if script_repr is None:
36
+ print("Error: Environment variable 'APP' not set.")
37
+ sys.exit(1)
38
+
39
+ try:
40
+ exec(script_repr)
41
+ except Exception as e:
42
+ print(f"Error executing script: {e}")
43
+ sys.exit(1)