ginipick commited on
Commit
8db4315
·
verified ·
1 Parent(s): ddf9c43

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +268 -147
app.py CHANGED
@@ -148,7 +148,8 @@ from concurrent.futures import ThreadPoolExecutor
148
 
149
  # ────────────────────────────────────────────────────────────
150
 
151
-
 
152
 
153
 
154
  # 환경 변수 설정으로 torch.load 체크 우회 (임시 해결책)
@@ -206,8 +207,12 @@ TRANSLATOR = None
206
  # API URLs
207
  TEXT2IMG_API_URL = "http://211.233.58.201:7896"
208
  VIDEO_API_URL = "http://211.233.58.201:7875"
209
- AVATAR_API_URL = "http://211.233.58.201:7862"
210
 
 
 
 
 
 
211
  # Image size presets
212
  IMAGE_PRESETS = {
213
  "커스텀": {"width": 1024, "height": 1024},
@@ -988,130 +993,83 @@ def merge_videos_with_audio(video_files, audio_file, audio_mode, audio_volume, o
988
  traceback.print_exc()
989
  return None, f"❌ 오류 발생: {str(e)}"
990
 
991
- @spaces.GPU(duration=180)
992
- def generate_avatar_animation(portrait_image, driving_audio, guidance_scale, inference_steps):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
993
  """이미지와 오디오로 아바타 애니메이션 생성"""
994
- if portrait_image is None:
995
- return None, None, "초상화 이미지를 업로드해주세요."
996
-
997
- if driving_audio is None:
998
- return None, None, "오디오 파일을 업로드해주세요."
999
 
1000
  try:
1001
- # 상태 업데이트
1002
- status = "아바타 생성 시작..."
1003
-
1004
- # 이미지 처리 - filepath로 받으므로 직접 사용
1005
- portrait_path = portrait_image
1006
-
1007
- # 이미지가 실제로 존재하는지 확인
1008
- if not os.path.exists(portrait_path):
1009
- # 만약 numpy array나 PIL 이미지로 왔다면 저장
1010
- with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as fp:
1011
- temp_portrait_path = fp.name
1012
- if isinstance(portrait_image, np.ndarray):
1013
- Image.fromarray(portrait_image).save(temp_portrait_path)
1014
- elif isinstance(portrait_image, Image.Image):
1015
- portrait_image.save(temp_portrait_path)
1016
- else:
1017
- return None, None, "❌ 이미지 형식을 인식할 수 없습니다."
1018
- portrait_path = temp_portrait_path
1019
-
1020
- # 오디오 경로 확인
1021
- audio_path = driving_audio
1022
-
1023
- # 경로 로깅
1024
- logging.info(f"Portrait path: {portrait_path}")
1025
- logging.info(f"Audio path: {audio_path}")
1026
- logging.info(f"Guidance Scale: {guidance_scale}")
1027
- logging.info(f"Inference Steps: {inference_steps}")
1028
 
1029
- # 파일 존재 확인
1030
- if not os.path.exists(portrait_path):
1031
- return None, None, f"❌ 이미지 파일을 찾을 수 없습니다: {portrait_path}"
1032
 
1033
- if not os.path.exists(audio_path):
1034
- return None, None, f"❌ 오디오 파일을 찾을 수 없습니다: {audio_path}"
1035
-
1036
- # API 연결 재시도 로직
1037
- max_retries = 3
1038
- retry_delay = 5 # 초
 
 
1039
 
1040
- for attempt in range(max_retries):
1041
- try:
1042
- logging.info(f"API 연결 시도 {attempt + 1}/{max_retries}")
1043
-
1044
- # 간단한 Client 생성 (timeout 매개변수 없이)
1045
- client = Client(AVATAR_API_URL)
1046
-
1047
- logging.info("API 클라이언트 생성 성공")
1048
-
1049
- # API 호출
1050
- result = client.predict(
1051
- portrait_path, # 직접 경로 전달
1052
- audio_path, # 직접 경로 전달
1053
- float(guidance_scale),
1054
- float(inference_steps),
1055
- api_name="/generate_animation"
1056
- )
1057
-
1058
- # 성공하면 루프 종료
1059
- break
1060
-
1061
- except Exception as e:
1062
- logging.warning(f"API 호출 오류 (시도 {attempt + 1}/{max_retries}): {str(e)}")
1063
- if "Connection" in str(e) or "Timeout" in str(e):
1064
- if attempt < max_retries - 1:
1065
- logging.info(f"{retry_delay}초 후 재시도...")
1066
- time.sleep(retry_delay)
1067
- else:
1068
- return None, None, "❌ API 서버에 연결할 수 없습니다. 잠시 후 다시 시도해주세요."
1069
  else:
1070
- # 다른 예외는 즉시 처리
1071
- raise e
1072
-
1073
- # 임시 파일 삭제 (있는 경우)
1074
- if 'temp_portrait_path' in locals() and os.path.exists(temp_portrait_path):
1075
- os.unlink(temp_portrait_path)
1076
-
1077
- # 결과 처리
1078
- if result and len(result) >= 2:
1079
- animation_result = result[0]
1080
- comparison_result = result[1]
1081
-
1082
- # 결과가 dict인지 직접 경로인지 확인
1083
- if isinstance(animation_result, dict):
1084
- animation_video = animation_result.get("video")
1085
- else:
1086
- animation_video = animation_result
1087
-
1088
- if isinstance(comparison_result, dict):
1089
- comparison_video = comparison_result.get("video")
1090
  else:
1091
- comparison_video = comparison_result
 
 
 
 
 
1092
 
1093
- # 비디오 파일 존재 확인
1094
- if animation_video and os.path.exists(str(animation_video)):
1095
- logging.info(f"Animation video created: {animation_video}")
1096
- else:
1097
- logging.warning(f"Animation video not found or invalid: {animation_video}")
1098
-
1099
- if comparison_video and os.path.exists(str(comparison_video)):
1100
- logging.info(f"Comparison video created: {comparison_video}")
1101
  else:
1102
- logging.warning(f"Comparison video not found or invalid: {comparison_video}")
1103
-
1104
- return animation_video, comparison_video, "✅ 아바타 애니메이션 생성 완료!"
1105
  else:
1106
- logging.error(f"Unexpected API response: {result}")
1107
- return None, None, "❌ API 응답이 예상과 다릅니다."
1108
-
1109
  except Exception as e:
1110
- logging.error(f"Avatar generation error: {str(e)}")
1111
- import traceback
1112
- traceback.print_exc()
1113
- return None, None, f"❌ 오류 발생: {str(e)}"
1114
-
1115
  # CSS
1116
  css = """
1117
  :root {
@@ -1137,13 +1095,16 @@ css = """
1137
  padding: 20px !important;
1138
  margin-bottom: 20px !important;
1139
  }
1140
- #generate-btn, #video-btn, #outpaint-btn, #preview-btn, #audio-btn, #bg-remove-btn, #merge-btn, #avatar-btn {
1141
  background: linear-gradient(135deg, #ff9a9e, #fad0c4) !important;
1142
  font-size: 1.1rem !important;
1143
  padding: 12px 24px !important;
1144
  margin-top: 10px !important;
1145
  width: 100% !important;
1146
  }
 
 
 
1147
  .tabitem {
1148
  min-height: 700px !important;
1149
  }
@@ -1543,74 +1504,229 @@ with demo:
1543
  **참고**: GPU 제한으로 한 번에 약 200프레임까지 처리 가능합니다.
1544
  긴 비디오는 작은 조각으로 나누어 처리하세요.
1545
  """)
1546
-
 
 
1547
  # 여섯 번째 탭: 이미지to아바타
1548
  with gr.Tab("이미지to아바타", elem_classes="tabitem"):
1549
  with gr.Row(equal_height=True):
1550
  # 입력 컬럼
1551
  with gr.Column(scale=1):
1552
  with gr.Group(elem_classes="panel-box"):
1553
- gr.Markdown("### 🖼️ 초상화 이미지 업로드")
 
 
 
 
 
 
 
1554
 
1555
- avatar_portrait = gr.Image(
1556
- label="초상화 이미지 (아무 비율 가능)",
1557
  type="filepath",
1558
- sources=["upload"]
1559
  )
1560
 
 
 
 
 
 
 
1561
  with gr.Group(elem_classes="panel-box"):
1562
- gr.Markdown("### 🎵 오디오 업로드")
1563
 
1564
- avatar_audio = gr.Audio(
1565
- label="구동 오디오",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1566
  type="filepath",
1567
- sources=["upload"]
1568
  )
1569
 
 
 
 
 
 
 
1570
  with gr.Group(elem_classes="panel-box"):
1571
  gr.Markdown("### ⚙️ 생성 설정")
1572
 
1573
- avatar_guidance = gr.Slider(
1574
  minimum=1.0,
1575
- maximum=7.0,
1576
- value=2.5,
1577
  step=0.1,
1578
  label="가이던스 스케일",
1579
- info="값이 높을수록 더 강한 가이드"
1580
  )
1581
 
1582
- avatar_steps = gr.Slider(
1583
  minimum=5,
1584
  maximum=30,
1585
- value=15,
1586
  step=1,
1587
  label="추론 스텝",
1588
- info="스텝이 많을수록 품질 향상"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1589
  )
1590
 
1591
- avatar_btn = gr.Button("🎭 아바타 생성", variant="primary", elem_id="avatar-btn")
 
 
 
 
1592
 
1593
  # 출력 컬럼
1594
  with gr.Column(scale=1):
1595
  with gr.Group(elem_classes="panel-box"):
1596
- gr.Markdown("### 🎬 생성 결과")
 
 
 
 
 
1597
 
1598
- avatar_status = gr.Textbox(label="처리 상태", interactive=False)
1599
- avatar_result = gr.Video(label="애니메이션 결과")
1600
- avatar_comparison = gr.Video(label="원본-애니메이션 비교")
 
 
 
 
 
 
 
 
 
 
1601
 
1602
  gr.Markdown("""
1603
- ### ℹ️ 사용 방법
1604
- 1. 정면을 보고 있는 초상화 이미지를 업로드하세요
1605
- 2. 음성이나 음악 오디오 파일을 업로드하세요
1606
- 3. 가이던스와 스텝 설정을 조정하세요
1607
- 4. '아바타 생성' 버튼을 클릭하세요
 
 
 
 
 
1608
 
1609
  **팁**:
1610
- - 정면 얼굴 이미지가 가장 좋은 결과를 만듭니다
1611
  - 오디오의 음성이 명확할수록 립싱크가 정확합니다
1612
- - 가이던스를 높이면 움직임이 강해집니다
1613
  """)
 
 
 
1614
 
1615
  # 모델 로드 함수 실행
1616
  def on_demo_load():
@@ -1700,13 +1816,18 @@ with demo:
1700
  fps_slider, video_handling_radio, fast_mode_checkbox, max_workers_slider],
1701
  outputs=[stream_image, output_bg_video, time_textbox]
1702
  )
 
 
 
 
 
1703
 
1704
- # 이벤트 연결 - 여섯 번째 탭 (이미지to아바타)
1705
- avatar_btn.click(
1706
  generate_avatar_animation,
1707
- inputs=[avatar_portrait, avatar_audio, avatar_guidance, avatar_steps],
1708
- outputs=[avatar_result, avatar_comparison, avatar_status]
1709
  )
 
1710
 
1711
  # 데모 로드 시 실행
1712
  demo.load(on_demo_load, outputs=model_status)
 
148
 
149
  # ────────────────────────────────────────────────────────────
150
 
151
+ import httpx
152
+ from datetime import datetime
153
 
154
 
155
  # 환경 변수 설정으로 torch.load 체크 우회 (임시 해결책)
 
207
  # API URLs
208
  TEXT2IMG_API_URL = "http://211.233.58.201:7896"
209
  VIDEO_API_URL = "http://211.233.58.201:7875"
 
210
 
211
+ ANIM_API_URL = os.getenv("ANIM_API_URL", "http://211.233.58.201:7862/")
212
+
213
+ # HTTP 타임아웃 설정
214
+ ANIM_TIMEOUT = httpx.Timeout(connect=30.0, read=120.0, write=120.0, pool=30.0
215
+
216
  # Image size presets
217
  IMAGE_PRESETS = {
218
  "커스텀": {"width": 1024, "height": 1024},
 
993
  traceback.print_exc()
994
  return None, f"❌ 오류 발생: {str(e)}"
995
 
996
+
997
+ def test_anim_api_connection():
998
+ """애니메이션 서버 연결 테스트"""
999
+ now = datetime.now().strftime("%H:%M:%S")
1000
+ try:
1001
+ resp = httpx.get(f"{ANIM_API_URL.rstrip('/')}/healthz", timeout=ANIM_TIMEOUT)
1002
+ ready = resp.json().get("ready", False)
1003
+ msg = f"[{now}] 애니메이션 서버 연결 성공 ✅ (ready={ready})"
1004
+ logging.info(msg)
1005
+ return True, msg
1006
+ except Exception as e:
1007
+ msg = f"[{now}] 애니메이션 서버 연결 실패 ❌ : {e}"
1008
+ logging.error(msg)
1009
+ return False, msg
1010
+
1011
+ def generate_avatar_animation(image, audio, guidance_scale, steps, progress=gr.Progress()):
1012
  """이미지와 오디오로 아바타 애니메이션 생성"""
1013
+ start = datetime.now().strftime("%H:%M:%S")
1014
+ logs = [f"[{start}] 요청 시작"]
 
 
 
1015
 
1016
  try:
1017
+ if image is None or audio is None:
1018
+ raise ValueError("이미지와 오디오를 모두 업로드하세요.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1019
 
1020
+ progress(0.05, desc="파일 준비")
1021
+ client = Client(ANIM_API_URL)
 
1022
 
1023
+ progress(0.15, desc="서버 호출 중… (수 분 소요 가능)")
1024
+ result = client.predict(
1025
+ image_path=handle_file(image),
1026
+ audio_path=handle_file(audio),
1027
+ guidance_scale=guidance_scale,
1028
+ steps=steps,
1029
+ api_name="/generate_animation"
1030
+ )
1031
 
1032
+ progress(0.95, desc="결과 정리")
1033
+
1034
+ # 결과 처리 - dict 형태 처리 추가
1035
+ def extract_video_path(obj):
1036
+ """비디오 객체에서 경로 추출"""
1037
+ if isinstance(obj, str):
1038
+ return obj
1039
+ elif isinstance(obj, dict):
1040
+ # Gradio의 FileData dict 처리
1041
+ if 'video' in obj:
1042
+ return obj['video'] # {'video': '경로', 'subtitles': None} 형태 처리
1043
+ elif 'path' in obj:
1044
+ return obj['path']
1045
+ elif 'url' in obj:
1046
+ return obj['url']
1047
+ elif 'name' in obj:
1048
+ return obj['name']
 
 
 
 
 
 
 
 
 
 
 
 
1049
  else:
1050
+ logging.warning(f"Unexpected dict structure: {obj.keys()}")
1051
+ return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1052
  else:
1053
+ logging.warning(f"Unexpected type: {type(obj)}")
1054
+ return None
1055
+
1056
+ if isinstance(result, (list, tuple)) and len(result) >= 2:
1057
+ anim_path = extract_video_path(result[0])
1058
+ comp_path = extract_video_path(result[1])
1059
 
1060
+ if anim_path and comp_path:
1061
+ logs.append(f"[{datetime.now().strftime('%H:%M:%S')}] 성공")
1062
+ return anim_path, comp_path, "\n".join(logs)
 
 
 
 
 
1063
  else:
1064
+ raise RuntimeError(f"비디오 경로 추출 실패: {result}")
 
 
1065
  else:
1066
+ raise RuntimeError(f"예상치 못한 반환 형식: {type(result)}")
1067
+
 
1068
  except Exception as e:
1069
+ logs.append(f"[{datetime.now().strftime('%H:%M:%S')}] 오류: {e}")
1070
+ logging.error(f"Avatar animation generation error: {e}", exc_info=True)
1071
+ return None, None, "\n".join(logs)
1072
+
 
1073
  # CSS
1074
  css = """
1075
  :root {
 
1095
  padding: 20px !important;
1096
  margin-bottom: 20px !important;
1097
  }
1098
+ #generate-btn, #video-btn, #outpaint-btn, #preview-btn, #audio-btn, #bg-remove-btn, #merge-btn, #avatar-btn, #test-connection-btn {
1099
  background: linear-gradient(135deg, #ff9a9e, #fad0c4) !important;
1100
  font-size: 1.1rem !important;
1101
  padding: 12px 24px !important;
1102
  margin-top: 10px !important;
1103
  width: 100% !important;
1104
  }
1105
+ #avatar-btn, #test-connection-btn {
1106
+ background: linear-gradient(135deg, #667eea, #764ba2) !important;
1107
+ }
1108
  .tabitem {
1109
  min-height: 700px !important;
1110
  }
 
1504
  **참고**: GPU 제한으로 한 번에 약 200프레임까지 처리 가능합니다.
1505
  긴 비디오는 작은 조각으로 나누어 처리하세요.
1506
  """)
1507
+
1508
+
1509
+
1510
  # 여섯 번째 탭: 이미지to아바타
1511
  with gr.Tab("이미지to아바타", elem_classes="tabitem"):
1512
  with gr.Row(equal_height=True):
1513
  # 입력 컬럼
1514
  with gr.Column(scale=1):
1515
  with gr.Group(elem_classes="panel-box"):
1516
+ gr.Markdown("### 🎭 아바타 애니메이션 생성")
1517
+ gr.Markdown("""
1518
+ 포트레이트 이미지와 오디오를 업로드하면 말하는 아바타 애니메이션을 생성합니다.
1519
+
1520
+ **권장 사항**:
1521
+ - 이미지: 정면을 보고 있는 얼굴 사진
1522
+ - 오디오: 명확한 음성이 담긴 오디오 파일
1523
+ """)
1524
 
1525
+ avatar_image = gr.Image(
1526
+ label="포트레이트 이미지",
1527
  type="filepath",
1528
+ elem_classes="panel-box"
1529
  )
1530
 
1531
+ avatar_audio = gr.Audio(
1532
+ label="드라이빙 오디오",
1533
+ type="filepath",
1534
+ elem_classes="panel-box"
1535
+ )
1536
+
1537
  with gr.Group(elem_classes="panel-box"):
1538
+ gr.Markdown("### ⚙️ 생성 설정")
1539
 
1540
+ guidance_scale = gr.Slider(
1541
+ minimum=1.0,
1542
+ maximum=10.0,
1543
+ value=3.0,
1544
+ step=0.1,
1545
+ label="가이던스 스케일",
1546
+ info="높을수록 오디오에 더 충실한 움직임 생성"
1547
+ )
1548
+
1549
+ inference_steps = gr.Slider(
1550
+ minimum=5,
1551
+ maximum=30,
1552
+ value=10,
1553
+ step=1,
1554
+ label="추론 스텝",
1555
+ info="높을수록 품질이 좋아지지만 생성 시간이 증가"
1556
+ )
1557
+
1558
+ # 서버 상태 체크
1559
+ with gr.Row():
1560
+ test_connection_btn = gr.Button(
1561
+ "🔌 서버 연결 테스트",
1562
+ elem_id="test-connection-btn",
1563
+ scale=1
1564
+ )
1565
+
1566
+ anim_status = gr.Textbox(
1567
+ label="서버 상태",
1568
+ interactive=False,
1569
+ elem_classes="panel-box"
1570
+ )
1571
+
1572
+ generate_avatar_btn = gr.Button(
1573
+ "🎬 아바타 생성",
1574
+ variant="primary",
1575
+ elem_id="avatar-btn"
1576
+ )
1577
+
1578
+ # 출력 컬럼
1579
+ with gr.Column(scale=1):
1580
+ with gr.Group(elem_classes="panel-box"):
1581
+ gr.Markdown("### 🎭 생성 결과")
1582
+
1583
+ avatar_result = gr.Video(
1584
+ label="애니메이션 결과",
1585
+ elem_classes="panel-box"
1586
+ )
1587
+
1588
+ avatar_comparison = gr.Video(
1589
+ label="원본 대비 결과 (Side-by-Side)",
1590
+ elem_classes="panel-box"
1591
+ )
1592
+
1593
+ with gr.Accordion("실행 로그", open=False):
1594
+ avatar_logs = gr.Textbox(
1595
+ label="로그",
1596
+ lines=10,
1597
+ max_lines=20,
1598
+ interactive=False,
1599
+ elem_classes="panel-box"
1600
+ )
1601
+
1602
+ gr.Markdown("""
1603
+ ### ℹ️ 사용 안내
1604
+
1605
+ 1. **포트레이트 이미지 업로드**: 정면을 보고 있는 선명한 얼굴 사진
1606
+ 2. **오디오 업로드**: 애니메이션에 사용할 음성 파일
1607
+ 3. **설정 조정**: 가이던스 스케일과 추론 스텝 조정
1608
+ 4. **생성 시작**: '아바타 생성' 버튼 클릭
1609
+
1610
+ **처리 시간**:
1611
+ - 일반적으로 2-5분 소요
1612
+ - 긴 오디오일수록 처리 시간 증가
1613
+
1614
+ **팁**:
1615
+ - 배경이 단순한 이미지가 더 좋은 결과를 생성합니다
1616
+ - 오디오의 음성이 명확할수록 립싱크가 정확합니다
1617
+ """)
1618
+
1619
+ # 여섯 번째 탭: 이미지to아바타
1620
+ with gr.Tab("이미지to아바타", elem_classes="tabitem"):
1621
+ with gr.Row(equal_height=True):
1622
+ # 입력 컬럼
1623
+ with gr.Column(scale=1):
1624
+ with gr.Group(elem_classes="panel-box"):
1625
+ gr.Markdown("### 🎭 아바타 애니메이션 생성")
1626
+ gr.Markdown("""
1627
+ 포트레이트 이미지와 오디오를 업로드하면 말하는 아바타 애니메이션을 생성합니다.
1628
+
1629
+ **권장 사항**:
1630
+ - 이미지: 정면을 보고 있는 얼굴 사진
1631
+ - 오디오: 명확한 음성이 담긴 오디오 파일
1632
+ """)
1633
+
1634
+ avatar_image = gr.Image(
1635
+ label="포트레이트 이미지",
1636
  type="filepath",
1637
+ elem_classes="panel-box"
1638
  )
1639
 
1640
+ avatar_audio = gr.Audio(
1641
+ label="드라이빙 오디오",
1642
+ type="filepath",
1643
+ elem_classes="panel-box"
1644
+ )
1645
+
1646
  with gr.Group(elem_classes="panel-box"):
1647
  gr.Markdown("### ⚙️ 생성 설정")
1648
 
1649
+ guidance_scale = gr.Slider(
1650
  minimum=1.0,
1651
+ maximum=10.0,
1652
+ value=3.0,
1653
  step=0.1,
1654
  label="가이던스 스케일",
1655
+ info="높을수록 오디오에 충실한 움직임 생성"
1656
  )
1657
 
1658
+ inference_steps = gr.Slider(
1659
  minimum=5,
1660
  maximum=30,
1661
+ value=10,
1662
  step=1,
1663
  label="추론 스텝",
1664
+ info="높을수록 품질이 좋아지지만 생성 시간이 증가"
1665
+ )
1666
+
1667
+ # 서버 상태 체크
1668
+ with gr.Row():
1669
+ test_connection_btn = gr.Button(
1670
+ "🔌 서버 연결 테스트",
1671
+ elem_id="test-connection-btn",
1672
+ scale=1
1673
+ )
1674
+
1675
+ anim_status = gr.Textbox(
1676
+ label="서버 상태",
1677
+ interactive=False,
1678
+ elem_classes="panel-box"
1679
  )
1680
 
1681
+ generate_avatar_btn = gr.Button(
1682
+ "🎬 아바타 생성",
1683
+ variant="primary",
1684
+ elem_id="avatar-btn"
1685
+ )
1686
 
1687
  # 출력 컬럼
1688
  with gr.Column(scale=1):
1689
  with gr.Group(elem_classes="panel-box"):
1690
+ gr.Markdown("### 🎭 생성 결과")
1691
+
1692
+ avatar_result = gr.Video(
1693
+ label="애니메이션 결과",
1694
+ elem_classes="panel-box"
1695
+ )
1696
 
1697
+ avatar_comparison = gr.Video(
1698
+ label="원본 대비 결과 (Side-by-Side)",
1699
+ elem_classes="panel-box"
1700
+ )
1701
+
1702
+ with gr.Accordion("실행 로그", open=False):
1703
+ avatar_logs = gr.Textbox(
1704
+ label="로그",
1705
+ lines=10,
1706
+ max_lines=20,
1707
+ interactive=False,
1708
+ elem_classes="panel-box"
1709
+ )
1710
 
1711
  gr.Markdown("""
1712
+ ### ℹ️ 사용 안내
1713
+
1714
+ 1. **포트레이트 이미지 업로드**: 정면을 보고 있는 선명한 얼굴 사진
1715
+ 2. **오디오 업로드**: 애니메이션에 사용할 음성 파일
1716
+ 3. **설정 조정**: 가이던스 스케일과 추론 스텝 조정
1717
+ 4. **생성 시작**: '아바타 생성' 버튼 클릭
1718
+
1719
+ **처리 시간**:
1720
+ - 일반적으로 2-5분 소요
1721
+ - 긴 오디오일수록 처리 시간 증가
1722
 
1723
  **팁**:
1724
+ - 배경이 단순한 이미지가 좋은 결과를 생성합니다
1725
  - 오디오의 음성이 명확할수록 립싱크가 정확합니다
 
1726
  """)
1727
+
1728
+
1729
+
1730
 
1731
  # 모델 로드 함수 실행
1732
  def on_demo_load():
 
1816
  fps_slider, video_handling_radio, fast_mode_checkbox, max_workers_slider],
1817
  outputs=[stream_image, output_bg_video, time_textbox]
1818
  )
1819
+
1820
+ test_connection_btn.click(
1821
+ test_anim_api_connection,
1822
+ outputs=[anim_status, anim_status]
1823
+ )
1824
 
1825
+ generate_avatar_btn.click(
 
1826
  generate_avatar_animation,
1827
+ inputs=[avatar_image, avatar_audio, guidance_scale, inference_steps],
1828
+ outputs=[avatar_result, avatar_comparison, avatar_logs]
1829
  )
1830
+
1831
 
1832
  # 데모 로드 시 실행
1833
  demo.load(on_demo_load, outputs=model_status)