Update app.py
Browse files
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 |
-
|
992 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
993 |
"""이미지와 오디오로 아바타 애니메이션 생성"""
|
994 |
-
|
995 |
-
|
996 |
-
|
997 |
-
if driving_audio is None:
|
998 |
-
return None, None, "오디오 파일을 업로드해주세요."
|
999 |
|
1000 |
try:
|
1001 |
-
|
1002 |
-
|
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 |
-
|
1031 |
-
return None, None, f"❌ 이미지 파일을 찾을 수 없습니다: {portrait_path}"
|
1032 |
|
1033 |
-
|
1034 |
-
|
1035 |
-
|
1036 |
-
|
1037 |
-
|
1038 |
-
|
|
|
|
|
1039 |
|
1040 |
-
|
1041 |
-
|
1042 |
-
|
1043 |
-
|
1044 |
-
|
1045 |
-
|
1046 |
-
|
1047 |
-
|
1048 |
-
|
1049 |
-
|
1050 |
-
|
1051 |
-
|
1052 |
-
|
1053 |
-
|
1054 |
-
|
1055 |
-
|
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 |
-
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
1092 |
|
1093 |
-
|
1094 |
-
|
1095 |
-
|
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 |
-
|
1103 |
-
|
1104 |
-
return animation_video, comparison_video, "✅ 아바타 애니메이션 생성 완료!"
|
1105 |
else:
|
1106 |
-
|
1107 |
-
|
1108 |
-
|
1109 |
except Exception as e:
|
1110 |
-
|
1111 |
-
|
1112 |
-
|
1113 |
-
|
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 |
-
|
1556 |
-
label="
|
1557 |
type="filepath",
|
1558 |
-
|
1559 |
)
|
1560 |
|
|
|
|
|
|
|
|
|
|
|
|
|
1561 |
with gr.Group(elem_classes="panel-box"):
|
1562 |
-
gr.Markdown("###
|
1563 |
|
1564 |
-
|
1565 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1566 |
type="filepath",
|
1567 |
-
|
1568 |
)
|
1569 |
|
|
|
|
|
|
|
|
|
|
|
|
|
1570 |
with gr.Group(elem_classes="panel-box"):
|
1571 |
gr.Markdown("### ⚙️ 생성 설정")
|
1572 |
|
1573 |
-
|
1574 |
minimum=1.0,
|
1575 |
-
maximum=
|
1576 |
-
value=
|
1577 |
step=0.1,
|
1578 |
label="가이던스 스케일",
|
1579 |
-
info="
|
1580 |
)
|
1581 |
|
1582 |
-
|
1583 |
minimum=5,
|
1584 |
maximum=30,
|
1585 |
-
value=
|
1586 |
step=1,
|
1587 |
label="추론 스텝",
|
1588 |
-
info="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1589 |
)
|
1590 |
|
1591 |
-
|
|
|
|
|
|
|
|
|
1592 |
|
1593 |
# 출력 컬럼
|
1594 |
with gr.Column(scale=1):
|
1595 |
with gr.Group(elem_classes="panel-box"):
|
1596 |
-
gr.Markdown("###
|
|
|
|
|
|
|
|
|
|
|
1597 |
|
1598 |
-
|
1599 |
-
|
1600 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1601 |
|
1602 |
gr.Markdown("""
|
1603 |
-
### ℹ️ 사용
|
1604 |
-
|
1605 |
-
|
1606 |
-
|
1607 |
-
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
1705 |
-
avatar_btn.click(
|
1706 |
generate_avatar_animation,
|
1707 |
-
inputs=[
|
1708 |
-
outputs=[avatar_result, avatar_comparison,
|
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)
|