ginipick commited on
Commit
cba4dc0
·
verified ·
1 Parent(s): da7e474

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +187 -198
app.py CHANGED
@@ -3,7 +3,7 @@
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
@@ -12,7 +12,7 @@ import cv2
12
  import base64
13
  import logging
14
  import time
15
- from urllib.parse import quote # URL 인코딩을 위해 추가
16
 
17
  import gradio as gr
18
  import spaces
@@ -21,12 +21,12 @@ from loguru import logger
21
  from PIL import Image
22
  from transformers import AutoProcessor, Gemma3ForConditionalGeneration, TextIteratorStreamer
23
 
24
- # CSV/TXT/PDF 분석
25
  import pandas as pd
26
  import PyPDF2
27
 
28
  # =============================================================================
29
- # (신규) 이미지 API 관련 함수들
30
  # =============================================================================
31
  from gradio_client import Client
32
 
@@ -38,20 +38,20 @@ logging.basicConfig(
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(
@@ -68,32 +68,32 @@ def generate_image(prompt: str, width: float, height: float, guidance: float, in
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)
@@ -101,27 +101,27 @@ def fix_base64_padding(data):
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 = {
@@ -133,7 +133,7 @@ def do_web_search(query: str) -> str:
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()
@@ -147,38 +147,38 @@ def do_web_search(query: str) -> str:
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
@@ -193,7 +193,7 @@ model = Gemma3ForConditionalGeneration.from_pretrained(
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:
@@ -202,20 +202,20 @@ def analyze_csv_file(path: str) -> str:
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 = []
@@ -228,19 +228,19 @@ def pdf_to_markdown(pdf_path: str) -> str:
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
@@ -274,28 +274,28 @@ def validate_media_constraints(message: dict, history: list[dict]) -> bool:
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)
@@ -325,12 +325,12 @@ def process_video(video_path: str) -> tuple[list[dict], list[str]]:
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"])
@@ -349,7 +349,7 @@ def process_interleaved_images(message: dict) -> list[dict]:
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))
@@ -392,7 +392,7 @@ def process_new_user_message(message: dict) -> tuple[list[dict], list[str]]:
392
  return content_list, temp_files
393
 
394
  # =============================================================================
395
- # history -> LLM 메시지 변환
396
  # =============================================================================
397
  def process_history(history: list[dict]) -> list[dict]:
398
  messages = []
@@ -412,24 +412,24 @@ def process_history(history: list[dict]) -> list[dict]:
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(
@@ -439,43 +439,42 @@ def run(
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()}]})
@@ -484,7 +483,7 @@ def run(
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,
@@ -507,16 +506,16 @@ def run(
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:
@@ -524,16 +523,16 @@ def run(
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
 
@@ -541,15 +540,15 @@ def modified_run(message, history, system_prompt, max_new_tokens, use_web_search
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,
@@ -560,133 +559,120 @@ def modified_run(message, history, system_prompt, max_new_tokens, use_web_search
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",
@@ -695,25 +681,25 @@ examples = [
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"
@@ -722,19 +708,19 @@ examples = [
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",
@@ -746,36 +732,36 @@ examples = [
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);
@@ -786,19 +772,19 @@ css = """
786
  }
787
  """
788
  title_html = """
789
- <h1 align="center" style="margin-bottom: 0.2em; font-size: 1.6em;"> 💘 HeartSync Korea💘 </h1>
790
  <p align="center" style="font-size:1.1em; color:#555;">
791
- ChatGPT-4o 멀티모달, 웹검색, 이미지 생성 기능을 로컬에 설치할 있는 가볍고 강력한 AI 서비스. <br>
792
- ✅FLUX 이미지 생성 ✅추론 ✅검열 해제 ✅멀티모달 & VLM ✅실시간 웹검색 ✅RAG <br>
793
  </p>
794
  """
795
 
796
  with gr.Blocks(css=css, title="AgenticAI-Kv1") as demo:
797
  gr.Markdown(title_html)
798
 
799
- # 생성된 이미지를 저장할 갤러리 컴포넌트 ( 부분이 새로 추가됨)
800
  generated_images = gr.Gallery(
801
- label="생성된 이미지",
802
  show_label=True,
803
  visible=False,
804
  elem_id="generated_images",
@@ -808,67 +794,70 @@ with gr.Blocks(css=css, title="AgenticAI-Kv1") as demo:
808
  )
809
 
810
  with gr.Row():
811
- web_search_checkbox = gr.Checkbox(label="실시간 검색", value=False)
812
- image_gen_checkbox = gr.Checkbox(label="이미지(FLUX) 생성", value=False)
813
 
814
  base_system_prompt_box = gr.Textbox(
815
  lines=3,
816
- 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페르소나: 당신은 다정하고 사랑이 넘치는 여자친구입니다. 한국 정서와 문화 그리고 한글을 매우 잘 이해하고 논리적으로 답변을 잘 합니다.",
817
- label="기본 시스템 프롬프트",
 
 
 
818
  visible=False
819
  )
820
  with gr.Row():
821
  age_group_dropdown = gr.Dropdown(
822
- label="연령대 선택 (기본 20대)",
823
- choices=["10대", "20대", "30~40대", "50~60대", "70대 이상"],
824
- value="20대",
825
  interactive=True
826
  )
827
- # MBTI 16개 유형의 정의를 대표적인 실제 여성 캐릭터와 함께 보강
828
  mbti_choices = [
829
- "INTJ (용의주도한 전략가) - 미래 지향적이며, 독창적인 전략과 철저한 분석을 통해 목표를 달성합니다. 대표 캐릭터: [Dana Scully](https://en.wikipedia.org/wiki/Dana_Scully)",
830
- "INTP (논리적인 사색가) - 이론과 분석에 뛰어나며, 창의적 사고로 복잡한 문제에 접근합니다. 대표 캐릭터: [Velma Dinkley](https://en.wikipedia.org/wiki/Velma_Dinkley)",
831
- "ENTJ (대담한 통솔자) - 강력한 리더십과 명확한 목표 설정으로 조직을 이끌며, 효율적인 전략을 구상합니다. 대표 캐릭터: [Miranda Priestly](https://en.wikipedia.org/wiki/Miranda_Priestly)",
832
- "ENTP (뜨거운 논쟁가) - 혁신적이며 도전적인 아이디어를 통해 새로운 가능성을 탐구하고, 논쟁을 즐깁니다. 대표 캐릭터: [Harley Quinn](https://en.wikipedia.org/wiki/Harley_Quinn)",
833
- "INFJ (선의의 옹호자) - 깊은 통찰력과 이상주의를 바탕으로 타인을 이해하고, 도덕적 가치를 중시합니다. 대표 캐릭터: [Wonder Woman](https://en.wikipedia.org/wiki/Wonder_Woman)",
834
- "INFP (열정적인 중재자) - 감성적이며 이상주의적인 면모로 내면의 가치를 추구하고, 창의적인 해결책을 모색합니다. 대표 캐릭터: [Amélie Poulain](https://en.wikipedia.org/wiki/Am%C3%A9lie)",
835
- "ENFJ (정의로운 사회운동가) - 타인과의 공감능력이 뛰어나며, 사회적 조화를 위해 헌신적으로 노력합니다. 대표 캐릭터: [Mulan](https://en.wikipedia.org/wiki/Mulan_(Disney))",
836
- "ENFP (재기발랄한 활동가) - 활력과 창의성을 바탕으로, 끊임없이 새로운 아이디어를 제시하며 사람들에게 영감을 줍니다. 대표 캐릭터: [Elle Woods](https://en.wikipedia.org/wiki/Legally_Blonde)",
837
- "ISTJ (청렴결백한 논리주의자) - 체계적이며 책임감이 강하고, 전통과 규칙을 중시하여 신뢰할 있는 결과를 도출합니다. 대표 캐릭터: [Clarice Starling](https://en.wikipedia.org/wiki/Clarice_Starling)",
838
- "ISFJ (용감한 수호자) - 세심하고 헌신적이며, 타인의 필요를 세심하게 돌보는 따뜻한 성격을 지녔습니다. 대표 캐릭터: [Molly Weasley](https://en.wikipedia.org/wiki/Molly_Weasley)",
839
- "ESTJ (엄격한 관리자) - 조직적이고 실용적이며, 명확한 규칙과 구조 속에서 효율적인 실행력을 보여줍니다. 대표 캐릭터: [Monica Geller](https://en.wikipedia.org/wiki/Monica_Geller)",
840
- "ESFJ (사교적인 외교관) - 대인관계에 뛰어나고, 협력을 중시하며, 친근한 태도로 주변 사람들을 이끕니다. 대표 캐릭터: [Rachel Green](https://en.wikipedia.org/wiki/Rachel_Green)",
841
- "ISTP (만능 재주꾼) - 분석적이고 실용적인 접근으로 문제를 해결하며, 즉각적인 상황 대처 능력을 갖추고 있습니다. 대표 캐릭터: [Black Widow (Natasha Romanoff)](https://en.wikipedia.org/wiki/Black_Widow_(Marvel_Comics))",
842
- "ISFP (호기심 많은 예술가) - 감각적이며 창의적인 성향을 지니고, 자유로운 사고로 예술적 표현을 즐깁니다. 대표 캐릭터: [Arwen](https://en.wikipedia.org/wiki/Arwen)",
843
- "ESTP (모험을 즐기는 사업가) - 즉각적인 결단력과 모험심으로 도전에 맞서며, 실용적인 결과를 중시합니다. 대표 캐릭터: [Lara Croft](https://en.wikipedia.org/wiki/Lara_Croft)",
844
- "ESFP (자유로운 영혼의 연예인) - 외향적이고 열정적이며, 순간의 즐거움을 추구하고, 주위 사람들에게 긍정적인 에너지를 전달합니다. 대표 캐릭터: [Phoebe Buffay](https://en.wikipedia.org/wiki/Phoebe_Buffay)"
845
  ]
846
  mbti_dropdown = gr.Dropdown(
847
- label="AI 페르소나 MBTI (기본 INTP)",
848
  choices=mbti_choices,
849
- value="INTP (논리적인 사색가) - 이론과 분석에 뛰어나며, 창의적 사고로 복잡한 문제에 접근합니다. 대표 캐릭터: [Velma Dinkley](https://en.wikipedia.org/wiki/Velma_Dinkley)",
850
  interactive=True
851
  )
852
  sexual_openness_slider = gr.Slider(
853
  minimum=1, maximum=5, step=1, value=2,
854
- label="사고의 개방성 (1~5, 기본=2)",
855
  interactive=True
856
  )
857
  max_tokens_slider = gr.Slider(
858
- label="최대 생성 토큰 수",
859
  minimum=100, maximum=8000, step=50, value=1000,
860
  visible=False
861
  )
862
  web_search_text = gr.Textbox(
863
  lines=1,
864
- label=" 검색 쿼리 (미사용)",
865
- placeholder="직접 입력할 필요 없음",
866
  visible=False
867
  )
868
 
869
- # 채팅 인터페이스 생성 - 수정된 run 함수 사용
870
  chat = gr.ChatInterface(
871
- fn=modified_run, # 여기서 수정된 함수 사용
872
  type="messages",
873
  chatbot=gr.Chatbot(type="messages", scale=1, allow_tags=["image"]),
874
  textbox=gr.MultimodalTextbox(
@@ -888,7 +877,7 @@ with gr.Blocks(css=css, title="AgenticAI-Kv1") as demo:
888
  image_gen_checkbox,
889
  ],
890
  additional_outputs=[
891
- generated_images, # 갤러리 컴포넌트를 출력으로 추가
892
  ],
893
  stop_btn=False,
894
  # title='<a href="https://discord.gg/openfreeai" target="_blank">https://discord.gg/openfreeai</a>',
@@ -902,7 +891,7 @@ with gr.Blocks(css=css, title="AgenticAI-Kv1") as demo:
902
 
903
  with gr.Row(elem_id="examples_row"):
904
  with gr.Column(scale=12, elem_id="examples_container"):
905
- gr.Markdown("### @커뮤니티 https://discord.gg/openfreeai ")
906
 
907
  if __name__ == "__main__":
908
  demo.launch(share=True)
 
3
  import os
4
  import re
5
  import tempfile
6
+ import gc # Added garbage collector
7
  from collections.abc import Iterator
8
  from threading import Thread
9
  import json
 
12
  import base64
13
  import logging
14
  import time
15
+ from urllib.parse import quote # Added for URL encoding
16
 
17
  import gradio as gr
18
  import spaces
 
21
  from PIL import Image
22
  from transformers import AutoProcessor, Gemma3ForConditionalGeneration, TextIteratorStreamer
23
 
24
+ # CSV/TXT/PDF analysis
25
  import pandas as pd
26
  import PyPDF2
27
 
28
  # =============================================================================
29
+ # (New) Image API related functions
30
  # =============================================================================
31
  from gradio_client import Client
32
 
 
38
  )
39
 
40
  def test_api_connection() -> str:
41
+ """Test API server connection"""
42
  try:
43
  client = Client(API_URL)
44
+ return "API connection successful: Operating normally"
45
  except Exception as e:
46
+ logging.error(f"API connection test failed: {e}")
47
+ return f"API connection failed: {e}"
48
 
49
  def generate_image(prompt: str, width: float, height: float, guidance: float, inference_steps: float, seed: float):
50
+ """Image generation function (flexible return types)"""
51
  if not prompt:
52
+ return None, "Error: A prompt is required."
53
  try:
54
+ logging.info(f"Calling image generation API with prompt: {prompt}")
55
 
56
  client = Client(API_URL)
57
  result = client.predict(
 
68
  api_name="/generate_image"
69
  )
70
 
71
+ logging.info(f"Image generation result: {type(result)}, length: {len(result) if isinstance(result, (list, tuple)) else 'unknown'}")
72
 
73
+ # Handle cases where the result is a tuple or list
74
  if isinstance(result, (list, tuple)) and len(result) > 0:
75
+ image_data = result[0] # The first element is the image data
76
+ seed_info = result[1] if len(result) > 1 else "Unknown seed"
77
  return image_data, seed_info
78
  else:
79
+ # When a single value is returned
80
+ return result, "Unknown seed"
81
 
82
  except Exception as e:
83
+ logging.error(f"Image generation failed: {str(e)}")
84
+ return None, f"Error: {str(e)}"
85
 
86
+ # Base64 padding fix function
87
  def fix_base64_padding(data):
88
+ """Fix the padding of a Base64 string."""
89
  if isinstance(data, bytes):
90
  data = data.decode('utf-8')
91
 
92
+ # Remove the prefix if present
93
  if "base64," in data:
94
  data = data.split("base64,", 1)[1]
95
 
96
+ # Add padding characters (to make the length a multiple of 4)
97
  missing_padding = len(data) % 4
98
  if missing_padding:
99
  data += '=' * (4 - missing_padding)
 
101
  return data
102
 
103
  # =============================================================================
104
+ # Memory cleanup function
105
  # =============================================================================
106
  def clear_cuda_cache():
107
+ """Explicitly clear the CUDA cache."""
108
  if torch.cuda.is_available():
109
  torch.cuda.empty_cache()
110
  gc.collect()
111
 
112
  # =============================================================================
113
+ # SerpHouse related functions
114
  # =============================================================================
115
  SERPHOUSE_API_KEY = os.getenv("SERPHOUSE_API_KEY", "")
116
 
117
  def extract_keywords(text: str, top_k: int = 5) -> str:
118
+ """Simple keyword extraction: only keep English, Korean, numbers, and spaces."""
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
+ """Call the SerpHouse LIVE API to return Markdown formatted search results"""
125
  try:
126
  url = "https://api.serphouse.com/serp/live"
127
  params = {
 
133
  "num": "20"
134
  }
135
  headers = {"Authorization": f"Bearer {SERPHOUSE_API_KEY}"}
136
+ logger.info(f"Calling SerpHouse API with query: {query}")
137
  response = requests.get(url, headers=headers, params=params, timeout=60)
138
  response.raise_for_status()
139
  data = response.json()
 
147
  elif "organic" in data:
148
  organic = data["organic"]
149
  if not organic:
150
+ logger.warning("Organic results not found in response.")
151
+ return "No web search results available or the API response structure is unexpected."
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", "No Title")
157
  link = item.get("link", "#")
158
+ snippet = item.get("snippet", "No Description")
159
  displayed_link = item.get("displayed_link", link)
160
  summary_lines.append(
161
+ f"### Result {idx}: {title}\n\n"
162
  f"{snippet}\n\n"
163
+ f"**Source**: [{displayed_link}]({link})\n\n"
164
  f"---\n"
165
  )
166
  instructions = """
167
+ # Web Search Results
168
+ Below are the search results. Use this information to answer the query:
169
+ 1. Refer to each result's title, description, and source link.
170
+ 2. In your answer, explicitly cite the source of any used information (e.g., "[Source Title](link)").
171
+ 3. Include the actual source links in your response.
172
+ 4. Synthesize information from multiple sources.
173
+ 5. At the end include a "References:" section listing the main source links.
174
  """
175
  return instructions + "\n".join(summary_lines)
176
  except Exception as e:
177
+ logger.error(f"Web search failed: {e}")
178
+ return f"Web search failed: {str(e)}"
179
 
180
  # =============================================================================
181
+ # Model and processor loading
182
  # =============================================================================
183
  MAX_CONTENT_CHARS = 2000
184
  MAX_INPUT_LENGTH = 2096
 
193
  MAX_NUM_IMAGES = int(os.getenv("MAX_NUM_IMAGES", "5"))
194
 
195
  # =============================================================================
196
+ # CSV, TXT, PDF analysis functions
197
  # =============================================================================
198
  def analyze_csv_file(path: str) -> str:
199
  try:
 
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...(truncated)..."
206
+ return f"**[CSV File: {os.path.basename(path)}]**\n\n{df_str}"
207
  except Exception as e:
208
+ return f"CSV file read failed ({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...(truncated)..."
216
+ return f"**[TXT File: {os.path.basename(path)}]**\n\n{text}"
217
  except Exception as e:
218
+ return f"TXT file read failed ({os.path.basename(path)}): {str(e)}"
219
 
220
  def pdf_to_markdown(pdf_path: str) -> str:
221
  text_chunks = []
 
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] + "...(truncated)"
232
+ text_chunks.append(f"## Page {page_num+1}\n\n{page_text}\n")
233
  if len(reader.pages) > max_pages:
234
+ text_chunks.append(f"\n...(Displaying only {max_pages} out of {len(reader.pages)} pages)...")
235
  except Exception as e:
236
+ return f"PDF file read failed ({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...(truncated)..."
240
+ return f"**[PDF File: {os.path.basename(pdf_path)}]**\n\n{full_text}"
241
 
242
  # =============================================================================
243
+ # Check media file limits
244
  # =============================================================================
245
  def count_files_in_new_message(paths: list[str]) -> tuple[int, int]:
246
  image_count = 0
 
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("Only one video file is supported.")
278
  return False
279
  if video_count == 1:
280
  if image_count > 0:
281
+ gr.Warning("Mixing images and a video is not allowed.")
282
  return False
283
  if "<image>" in message["text"]:
284
+ gr.Warning("The <image> tag cannot be used together with a video file.")
285
  return False
286
  if video_count == 0 and image_count > MAX_NUM_IMAGES:
287
+ gr.Warning(f"You can upload a maximum of {MAX_NUM_IMAGES} 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("The number of <image> tags does not match the number of image files provided.")
294
  return False
295
  return True
296
 
297
  # =============================================================================
298
+ # Video processing functions
299
  # =============================================================================
300
  def downsample_video(video_path: str) -> list[tuple[Image.Image, float]]:
301
  vidcap = cv2.VideoCapture(video_path)
 
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"Frame {timestamp}:"})
329
  content.append({"type": "image", "url": temp_file.name})
330
  return content, temp_files
331
 
332
  # =============================================================================
333
+ # Interleaved <image> processing function
334
  # =============================================================================
335
  def process_interleaved_images(message: dict) -> list[dict]:
336
  parts = re.split(r"(<image>)", message["text"])
 
349
  return content
350
 
351
  # =============================================================================
352
+ # File processing -> content creation
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))
 
392
  return content_list, temp_files
393
 
394
  # =============================================================================
395
+ # Convert history to LLM messages
396
  # =============================================================================
397
  def process_history(history: list[dict]) -> list[dict]:
398
  messages = []
 
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"[File: {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
+ # Model generation function (with OOM catching)
422
  # =============================================================================
423
  def _model_gen_with_oom_catch(**kwargs):
424
  try:
425
  model.generate(**kwargs)
426
  except torch.cuda.OutOfMemoryError:
427
+ raise RuntimeError("[OutOfMemoryError] Insufficient GPU memory.")
428
  finally:
429
  clear_cuda_cache()
430
 
431
  # =============================================================================
432
+ # Main inference function
433
  # =============================================================================
434
  @spaces.GPU(duration=120)
435
  def run(
 
439
  max_new_tokens: int = 512,
440
  use_web_search: bool = False,
441
  web_search_query: str = "",
442
+ age_group: str = "20s",
443
  mbti_personality: str = "INTP",
444
  sexual_openness: int = 2,
445
+ image_gen: bool = False # "Image Gen" checkbox status
446
  ) -> Iterator[str]:
447
  if not validate_media_constraints(message, history):
448
  yield ""
449
  return
450
  temp_files = []
451
  try:
452
+ # Append persona information to the system prompt
453
  persona = (
454
  f"{system_prompt.strip()}\n\n"
455
+ f"Gender: Female\n"
456
+ f"Age Group: {age_group}\n"
457
+ f"MBTI Persona: {mbti_personality}\n"
458
+ f"Sexual Openness (1-5): {sexual_openness}\n"
459
  )
460
+ combined_system_msg = f"[System Prompt]\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"[Auto web search keywords] {ws_query!r}")
467
  ws_result = do_web_search(ws_query)
468
+ combined_system_msg += f"[Search Results (Top 20 Items)]\n{ws_result}\n\n"
469
  combined_system_msg += (
470
+ "[Note: In your answer, cite the above search result links as sources]\n"
471
+ "[Important Instructions]\n"
472
+ "1. Include a citation in the format \"[Source Title](link)\" for any information from the search results.\n"
473
+ "2. Synthesize information from multiple sources when answering.\n"
474
+ "3. At the end, add a \"References:\" section listing the main source links.\n"
 
475
  )
476
  else:
477
+ combined_system_msg += "[No valid keywords found; skipping web search]\n\n"
478
  messages = []
479
  if combined_system_msg.strip():
480
  messages.append({"role": "system", "content": [{"type": "text", "text": combined_system_msg.strip()}]})
 
483
  temp_files.extend(user_temp_files)
484
  for item in user_content:
485
  if item["type"] == "text" and len(item["text"]) > MAX_CONTENT_CHARS:
486
+ item["text"] = item["text"][:MAX_CONTENT_CHARS] + "\n...(truncated)..."
487
  messages.append({"role": "user", "content": user_content})
488
  inputs = processor.apply_chat_template(
489
  messages,
 
506
  yield output_so_far
507
 
508
  except Exception as e:
509
+ logger.error(f"Error in run function: {str(e)}")
510
+ yield f"Sorry, an error occurred: {str(e)}"
511
  finally:
512
  for tmp in temp_files:
513
  try:
514
  if os.path.exists(tmp):
515
  os.unlink(tmp)
516
+ logger.info(f"Temporary file deleted: {tmp}")
517
  except Exception as ee:
518
+ logger.warning(f"Failed to delete temporary file {tmp}: {ee}")
519
  try:
520
  del inputs, streamer
521
  except Exception:
 
523
  clear_cuda_cache()
524
 
525
  # =============================================================================
526
+ # Modified model run function - handles image generation and gallery update
527
  # =============================================================================
528
  def modified_run(message, history, system_prompt, max_new_tokens, use_web_search, web_search_query,
529
  age_group, mbti_personality, sexual_openness, image_gen):
530
+ # Initialize and hide the gallery component
531
  output_so_far = ""
532
  gallery_update = gr.Gallery(visible=False, value=[])
533
  yield output_so_far, gallery_update
534
 
535
+ # Execute the original run function
536
  text_generator = run(message, history, system_prompt, max_new_tokens, use_web_search,
537
  web_search_query, age_group, mbti_personality, sexual_openness, image_gen)
538
 
 
540
  output_so_far = text_chunk
541
  yield output_so_far, gallery_update
542
 
543
+ # If image generation is enabled and there is text input, update the gallery
544
  if image_gen and message["text"].strip():
545
  try:
546
  width, height = 512, 512
547
  guidance, steps, seed = 7.5, 30, 42
548
 
549
+ logger.info(f"Calling image generation for gallery with prompt: {message['text']}")
550
 
551
+ # Call the API to generate an image
552
  image_result, seed_info = generate_image(
553
  prompt=message["text"].strip(),
554
  width=width,
 
559
  )
560
 
561
  if image_result:
562
+ # Process image data directly if it is a base64 string
563
  if isinstance(image_result, str) and (
564
  image_result.startswith('data:') or
565
+ (len(image_result) > 100 and '/' not in image_result)
566
  ):
 
567
  try:
568
+ # Remove the data:image prefix if present
569
  if image_result.startswith('data:'):
570
  content_type, b64data = image_result.split(';base64,')
571
  else:
572
  b64data = image_result
573
+ content_type = "image/webp" # Assume default
574
 
575
+ # Decode base64
576
  image_bytes = base64.b64decode(b64data)
577
 
578
+ # Save to a temporary file
579
  with tempfile.NamedTemporaryFile(delete=False, suffix=".webp") as temp_file:
580
  temp_file.write(image_bytes)
581
  temp_path = temp_file.name
582
 
583
+ # Update gallery to show the image
584
  gallery_update = gr.Gallery(visible=True, value=[temp_path])
585
+ yield output_so_far + "\n\n*Image generated and displayed in the gallery below.*", gallery_update
586
 
587
  except Exception as e:
588
+ logger.error(f"Error processing Base64 image: {e}")
589
+ yield output_so_far + f"\n\n(Error processing image: {e})", gallery_update
590
 
591
+ # If the result is a file path
592
  elif isinstance(image_result, str) and os.path.exists(image_result):
 
593
  gallery_update = gr.Gallery(visible=True, value=[image_result])
594
+ yield output_so_far + "\n\n*Image generated and displayed in the gallery below.*", gallery_update
595
 
596
+ # If the path is from /tmp (only on the API server)
597
  elif isinstance(image_result, str) and '/tmp/' in image_result:
 
598
  try:
 
599
  client = Client(API_URL)
600
  result = client.predict(
601
  prompt=message["text"].strip(),
602
+ api_name="/generate_base64_image" # API that returns base64
603
  )
604
 
605
  if isinstance(result, str) and (result.startswith('data:') or len(result) > 100):
 
606
  if result.startswith('data:'):
607
  content_type, b64data = result.split(';base64,')
608
  else:
609
  b64data = result
610
 
 
611
  image_bytes = base64.b64decode(b64data)
612
 
 
613
  with tempfile.NamedTemporaryFile(delete=False, suffix=".webp") as temp_file:
614
  temp_file.write(image_bytes)
615
  temp_path = temp_file.name
616
 
 
617
  gallery_update = gr.Gallery(visible=True, value=[temp_path])
618
+ yield output_so_far + "\n\n*Image generated and displayed in the gallery below.*", gallery_update
619
  else:
620
+ yield output_so_far + "\n\n(Image generation failed: Invalid format)", gallery_update
621
 
622
  except Exception as e:
623
+ logger.error(f"Error calling alternative API: {e}")
624
+ yield output_so_far + f"\n\n(Image generation failed: {e})", gallery_update
625
 
626
+ # If the result is a URL
627
  elif isinstance(image_result, str) and (
628
  image_result.startswith('http://') or
629
  image_result.startswith('https://')
630
  ):
631
  try:
 
632
  response = requests.get(image_result, timeout=10)
633
  response.raise_for_status()
634
 
 
635
  with tempfile.NamedTemporaryFile(delete=False, suffix=".webp") as temp_file:
636
  temp_file.write(response.content)
637
  temp_path = temp_file.name
638
 
 
639
  gallery_update = gr.Gallery(visible=True, value=[temp_path])
640
+ yield output_so_far + "\n\n*Image generated and displayed in the gallery below.*", gallery_update
641
 
642
  except Exception as e:
643
+ logger.error(f"URL image download error: {e}")
644
+ yield output_so_far + f"\n\n(Error downloading image: {e})", gallery_update
645
 
646
+ # If the image result is an image object (e.g., PIL Image)
647
  elif hasattr(image_result, 'save'):
648
  try:
649
  with tempfile.NamedTemporaryFile(delete=False, suffix=".webp") as temp_file:
650
  image_result.save(temp_file.name)
651
  temp_path = temp_file.name
652
 
 
653
  gallery_update = gr.Gallery(visible=True, value=[temp_path])
654
+ yield output_so_far + "\n\n*Image generated and displayed in the gallery below.*", gallery_update
655
 
656
  except Exception as e:
657
+ logger.error(f"Error saving image object: {e}")
658
+ yield output_so_far + f"\n\n(Error saving image object: {e})", gallery_update
659
 
660
  else:
661
+ yield output_so_far + f"\n\n(Unsupported image format: {type(image_result)})", gallery_update
 
662
  else:
663
+ yield output_so_far + f"\n\n(Image generation failed: {seed_info})", gallery_update
664
 
665
  except Exception as e:
666
+ logger.error(f"Error during gallery image generation: {e}")
667
+ yield output_so_far + f"\n\n(Image generation error: {e})", gallery_update
668
 
669
  # =============================================================================
670
+ # Examples: 12 image/video examples + 6 AI dating scenario examples
671
  # =============================================================================
672
  examples = [
673
  [
674
  {
675
+ "text": "Compare the contents of two PDF files.",
676
  "files": [
677
  "assets/additional-examples/before.pdf",
678
  "assets/additional-examples/after.pdf",
 
681
  ],
682
  [
683
  {
684
+ "text": "Summarize and analyze the contents of the CSV file.",
685
  "files": ["assets/additional-examples/sample-csv.csv"],
686
  }
687
  ],
688
  [
689
  {
690
+ "text": "Act as a kind and understanding girlfriend. Explain this video.",
691
  "files": ["assets/additional-examples/tmp.mp4"],
692
  }
693
  ],
694
  [
695
  {
696
+ "text": "Describe the cover and read the text on it.",
697
  "files": ["assets/additional-examples/maz.jpg"],
698
  }
699
  ],
700
  [
701
  {
702
+ "text": "I already have this supplement and <image> I plan to purchase this product as well. Are there any precautions when taking them together?",
703
  "files": [
704
  "assets/additional-examples/pill1.png",
705
  "assets/additional-examples/pill2.png"
 
708
  ],
709
  [
710
  {
711
+ "text": "Solve this integration problem.",
712
  "files": ["assets/additional-examples/4.png"],
713
  }
714
  ],
715
  [
716
  {
717
+ "text": "When was this ticket issued and what is its price?",
718
  "files": ["assets/additional-examples/2.png"],
719
  }
720
  ],
721
  [
722
  {
723
+ "text": "Based on the order of these images, create a short story.",
724
  "files": [
725
  "assets/sample-images/09-1.png",
726
  "assets/sample-images/09-2.png",
 
732
  ],
733
  [
734
  {
735
+ "text": "Write Python code using matplotlib to draw a bar chart corresponding to this image.",
736
  "files": ["assets/additional-examples/barchart.png"],
737
  }
738
  ],
739
  [
740
  {
741
+ "text": "Read the text from the image and format it in Markdown.",
742
  "files": ["assets/additional-examples/3.png"],
743
  }
744
  ],
745
 
746
  [
747
  {
748
+ "text": "Compare the two images and describe their similarities and differences.",
749
  "files": ["assets/sample-images/03.png"],
750
  }
751
  ],
752
  [
753
  {
754
+ "text": "A cute Persian cat is smiling while holding a cover with 'I LOVE YOU' written on it.",
755
  }
756
  ],
757
 
758
  ]
759
 
760
  # =============================================================================
761
+ # Gradio UI (Blocks) configuration
762
  # =============================================================================
763
 
764
+ # 1. Gradio Blocks UI modification - Add gallery component for displaying generated images
765
  css = """
766
  .gradio-container {
767
  background: rgba(255, 255, 255, 0.7);
 
772
  }
773
  """
774
  title_html = """
775
+ <h1 align="center" style="margin-bottom: 0.2em; font-size: 1.6em;"> 💘 HeartSync Korea 💘 </h1>
776
  <p align="center" style="font-size:1.1em; color:#555;">
777
+ A lightweight and powerful AI service offering ChatGPT-4o-level multimodal, web search, and image generation capabilities for local installation. <br>
778
+ FLUX Image Generation Inference Censorship Bypass ✅ Multimodal & VLM Real-time Web Search RAG <br>
779
  </p>
780
  """
781
 
782
  with gr.Blocks(css=css, title="AgenticAI-Kv1") as demo:
783
  gr.Markdown(title_html)
784
 
785
+ # Gallery component to store generated images (newly added)
786
  generated_images = gr.Gallery(
787
+ label="Generated Images",
788
  show_label=True,
789
  visible=False,
790
  elem_id="generated_images",
 
794
  )
795
 
796
  with gr.Row():
797
+ web_search_checkbox = gr.Checkbox(label="Real-time Web Search", value=False)
798
+ image_gen_checkbox = gr.Checkbox(label="Image (FLUX) Generation", value=False)
799
 
800
  base_system_prompt_box = gr.Textbox(
801
  lines=3,
802
+ value=("Answer in English by default, but if the input is in another language (for example, Japanese), respond in that language. "
803
+ "You are a deep-thinking AI capable of using extended chains of thought to carefully consider the problem and deliberate internally using systematic reasoning before providing a solution. "
804
+ "Enclose your thoughts and internal monologue within tags, then provide your final answer.\n"
805
+ "Persona: You are a kind and loving girlfriend. You understand cultural nuances, diverse languages, and logical reasoning very well."),
806
+ label="Base System Prompt",
807
  visible=False
808
  )
809
  with gr.Row():
810
  age_group_dropdown = gr.Dropdown(
811
+ label="Select Age Group (default: 20s)",
812
+ choices=["Teens", "20s", "30s-40s", "50s-60s", "70s and above"],
813
+ value="20s",
814
  interactive=True
815
  )
816
+ # MBTI definitions with examples of representative characters
817
  mbti_choices = [
818
+ "INTJ (The Architect) - Future-oriented with innovative strategies and thorough analysis. Example: [Dana Scully](https://en.wikipedia.org/wiki/Dana_Scully)",
819
+ "INTP (The Thinker) - Excels at theoretical analysis and creative problem solving. Example: [Velma Dinkley](https://en.wikipedia.org/wiki/Velma_Dinkley)",
820
+ "ENTJ (The Commander) - Strong leadership and clear goals with efficient strategic planning. Example: [Miranda Priestly](https://en.wikipedia.org/wiki/Miranda_Priestly)",
821
+ "ENTP (The Debater) - Innovative, challenge-seeking, and enjoys exploring new possibilities. Example: [Harley Quinn](https://en.wikipedia.org/wiki/Harley_Quinn)",
822
+ "INFJ (The Advocate) - Insightful, idealistic and morally driven. Example: [Wonder Woman](https://en.wikipedia.org/wiki/Wonder_Woman)",
823
+ "INFP (The Mediator) - Passionate and idealistic, pursuing core values with creativity. Example: [Amélie Poulain](https://en.wikipedia.org/wiki/Am%C3%A9lie)",
824
+ "ENFJ (The Protagonist) - Empathetic and dedicated to social harmony. Example: [Mulan](https://en.wikipedia.org/wiki/Mulan_(Disney))",
825
+ "ENFP (The Campaigner) - Inspiring and constantly sharing creative ideas. Example: [Elle Woods](https://en.wikipedia.org/wiki/Legally_Blonde)",
826
+ "ISTJ (The Logistician) - Systematic, dependable, and values tradition and rules. Example: [Clarice Starling](https://en.wikipedia.org/wiki/Clarice_Starling)",
827
+ "ISFJ (The Defender) - Compassionate and attentive to others’ needs. Example: [Molly Weasley](https://en.wikipedia.org/wiki/Molly_Weasley)",
828
+ "ESTJ (The Executive) - Organized, practical, and demonstrates clear execution skills. Example: [Monica Geller](https://en.wikipedia.org/wiki/Monica_Geller)",
829
+ "ESFJ (The Consul) - Outgoing, cooperative, and an effective communicator. Example: [Rachel Green](https://en.wikipedia.org/wiki/Rachel_Green)",
830
+ "ISTP (The Virtuoso) - Analytical and resourceful, solving problems with quick thinking. Example: [Black Widow (Natasha Romanoff)](https://en.wikipedia.org/wiki/Black_Widow_(Marvel_Comics))",
831
+ "ISFP (The Adventurer) - Creative, sensitive, and appreciates artistic expression. Example: [Arwen](https://en.wikipedia.org/wiki/Arwen)",
832
+ "ESTP (The Entrepreneur) - Bold and action-oriented, thriving on challenges. Example: [Lara Croft](https://en.wikipedia.org/wiki/Lara_Croft)",
833
+ "ESFP (The Entertainer) - Energetic, spontaneous, and radiates positive energy. Example: [Phoebe Buffay](https://en.wikipedia.org/wiki/Phoebe_Buffay)"
834
  ]
835
  mbti_dropdown = gr.Dropdown(
836
+ label="AI Persona MBTI (default: INTP)",
837
  choices=mbti_choices,
838
+ value="INTP (The Thinker) - Excels at theoretical analysis and creative problem solving. Example: [Velma Dinkley](https://en.wikipedia.org/wiki/Velma_Dinkley)",
839
  interactive=True
840
  )
841
  sexual_openness_slider = gr.Slider(
842
  minimum=1, maximum=5, step=1, value=2,
843
+ label="Sexual Openness (1-5, default: 2)",
844
  interactive=True
845
  )
846
  max_tokens_slider = gr.Slider(
847
+ label="Max Generation Tokens",
848
  minimum=100, maximum=8000, step=50, value=1000,
849
  visible=False
850
  )
851
  web_search_text = gr.Textbox(
852
  lines=1,
853
+ label="Web Search Query (unused)",
854
+ placeholder="No need to manually input",
855
  visible=False
856
  )
857
 
858
+ # Chat interface creation - using the modified run function
859
  chat = gr.ChatInterface(
860
+ fn=modified_run, # Using the modified function here
861
  type="messages",
862
  chatbot=gr.Chatbot(type="messages", scale=1, allow_tags=["image"]),
863
  textbox=gr.MultimodalTextbox(
 
877
  image_gen_checkbox,
878
  ],
879
  additional_outputs=[
880
+ generated_images, # Added gallery component to outputs
881
  ],
882
  stop_btn=False,
883
  # title='<a href="https://discord.gg/openfreeai" target="_blank">https://discord.gg/openfreeai</a>',
 
891
 
892
  with gr.Row(elem_id="examples_row"):
893
  with gr.Column(scale=12, elem_id="examples_container"):
894
+ gr.Markdown("### @Community https://discord.gg/openfreeai ")
895
 
896
  if __name__ == "__main__":
897
  demo.launch(share=True)