MacBook pro commited on
Commit
037a833
Β·
1 Parent(s): db46fbe

LivePortrait: auto-downgrade opset>19 to 19 for appearance/motion; force re-download flags; register ort-extensions; retry loaders

Browse files
Files changed (4) hide show
  1. Dockerfile +3 -1
  2. liveportrait_engine.py +25 -6
  3. model_downloader.py +69 -0
  4. webrtc_fallback.py +0 -38
Dockerfile CHANGED
@@ -94,7 +94,9 @@ ENV HOME=/app \
94
  INSIGHTFACE_HOME=/app/.insightface \
95
  MPLCONFIGDIR=/tmp/matplotlib \
96
  MIRAGE_ORT_DISABLE_SHAPE_INFERENCE=1 \
97
- MIRAGE_FORCE_DOWNLOAD_GENERATOR=1
 
 
98
 
99
  # Enforce single neural path (SCRFD + LivePortrait generator)
100
  ENV MIRAGE_REQUIRE_NEURAL=1
 
94
  INSIGHTFACE_HOME=/app/.insightface \
95
  MPLCONFIGDIR=/tmp/matplotlib \
96
  MIRAGE_ORT_DISABLE_SHAPE_INFERENCE=1 \
97
+ MIRAGE_FORCE_DOWNLOAD_GENERATOR=1 \
98
+ MIRAGE_FORCE_DOWNLOAD_APPEARANCE=1 \
99
+ MIRAGE_FORCE_DOWNLOAD_MOTION=1
100
 
101
  # Enforce single neural path (SCRFD + LivePortrait generator)
102
  ENV MIRAGE_REQUIRE_NEURAL=1
liveportrait_engine.py CHANGED
@@ -8,6 +8,8 @@ import cv2
8
  import torch
9
  import onnxruntime as ort
10
  import os
 
 
11
  from typing import Optional, Tuple, Dict, Any
12
  from pathlib import Path
13
  import logging
@@ -110,12 +112,28 @@ class LivePortraitONNX:
110
  except Exception:
111
  pass
112
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  # Load appearance feature extractor (required)
114
  if self.appearance_model_path.exists():
115
- logger.info(f"Loading appearance model: {self.appearance_model_path}")
 
116
  try:
117
  self.appearance_session = ort.InferenceSession(
118
- str(self.appearance_model_path),
119
  providers=providers,
120
  sess_options=sess_options
121
  )
@@ -124,7 +142,7 @@ class LivePortraitONNX:
124
  # Retry with default provider list only
125
  basic_providers = [p for p in providers]
126
  self.appearance_session = ort.InferenceSession(
127
- str(self.appearance_model_path),
128
  providers=basic_providers
129
  )
130
  else:
@@ -133,10 +151,11 @@ class LivePortraitONNX:
133
 
134
  # Load motion extractor (required)
135
  if self.motion_model_path.exists():
136
- logger.info(f"Loading motion model: {self.motion_model_path}")
 
137
  try:
138
  self.motion_session = ort.InferenceSession(
139
- str(self.motion_model_path),
140
  providers=providers,
141
  sess_options=sess_options
142
  )
@@ -144,7 +163,7 @@ class LivePortraitONNX:
144
  logger.warning(f"Motion model failed with tuned providers, retrying basic: {e}")
145
  basic_providers = [p for p in providers]
146
  self.motion_session = ort.InferenceSession(
147
- str(self.motion_model_path),
148
  providers=basic_providers
149
  )
150
  else:
 
8
  import torch
9
  import onnxruntime as ort
10
  import os
11
+ import onnx # type: ignore
12
+ from onnx import version_converter # type: ignore
13
  from typing import Optional, Tuple, Dict, Any
14
  from pathlib import Path
15
  import logging
 
112
  except Exception:
113
  pass
114
 
115
+ # Helper to ensure opset <= 19 by converting if required
116
+ def _ensure_opset_compat(path: Path) -> Path:
117
+ try:
118
+ model = onnx.load(str(path), load_external_data=True)
119
+ max_opset = max((imp.version for imp in model.opset_import), default=0)
120
+ if max_opset > 19:
121
+ logger.info(f"Converting ONNX opset from {max_opset} to 19 for {path.name}")
122
+ converted = version_converter.convert_version(model, 19)
123
+ out_path = path.with_name(path.stem + "_op19.onnx")
124
+ onnx.save(converted, str(out_path))
125
+ return out_path
126
+ except Exception as ce:
127
+ logger.warning(f"Opset conversion skipped for {path.name}: {ce}")
128
+ return path
129
+
130
  # Load appearance feature extractor (required)
131
  if self.appearance_model_path.exists():
132
+ app_path = _ensure_opset_compat(self.appearance_model_path)
133
+ logger.info(f"Loading appearance model: {app_path}")
134
  try:
135
  self.appearance_session = ort.InferenceSession(
136
+ str(app_path),
137
  providers=providers,
138
  sess_options=sess_options
139
  )
 
142
  # Retry with default provider list only
143
  basic_providers = [p for p in providers]
144
  self.appearance_session = ort.InferenceSession(
145
+ str(app_path),
146
  providers=basic_providers
147
  )
148
  else:
 
151
 
152
  # Load motion extractor (required)
153
  if self.motion_model_path.exists():
154
+ mot_path = _ensure_opset_compat(self.motion_model_path)
155
+ logger.info(f"Loading motion model: {mot_path}")
156
  try:
157
  self.motion_session = ort.InferenceSession(
158
+ str(mot_path),
159
  providers=providers,
160
  sess_options=sess_options
161
  )
 
163
  logger.warning(f"Motion model failed with tuned providers, retrying basic: {e}")
164
  basic_providers = [p for p in providers]
165
  self.motion_session = ort.InferenceSession(
166
+ str(mot_path),
167
  providers=basic_providers
168
  )
169
  else:
model_downloader.py CHANGED
@@ -26,6 +26,11 @@ try:
26
  import onnx # type: ignore
27
  except Exception:
28
  onnx = None
 
 
 
 
 
29
 
30
  try:
31
  from huggingface_hub import hf_hub_download # type: ignore
@@ -92,6 +97,25 @@ def _is_valid_onnx(path: Path) -> bool:
92
  except Exception:
93
  return False
94
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  def _download(url: str, dest: Path):
96
  dest.parent.mkdir(parents=True, exist_ok=True)
97
  try:
@@ -116,34 +140,79 @@ def maybe_download() -> bool:
116
 
117
  app_url = os.getenv('MIRAGE_LP_APPEARANCE_URL')
118
  motion_url = os.getenv('MIRAGE_LP_MOTION_URL')
 
 
119
  success = True
120
 
121
  # Download LivePortrait appearance extractor
122
  if app_url:
123
  dest = LP_DIR / 'appearance_feature_extractor.onnx'
 
 
 
 
 
 
124
  if not dest.exists():
125
  try:
126
  print(f'[downloader] Downloading appearance extractor...')
127
  _download(app_url, dest)
 
 
 
 
 
 
 
 
 
128
  print(f'[downloader] βœ… Downloaded: {dest}')
129
  except Exception as e:
130
  print(f'[downloader] ❌ Failed to download appearance extractor: {e}')
131
  success = False
132
  else:
 
 
 
 
 
 
 
 
133
  print(f'[downloader] βœ… Appearance extractor already exists: {dest}')
134
 
135
  # Download LivePortrait motion extractor
136
  if motion_url:
137
  dest = LP_DIR / 'motion_extractor.onnx'
 
 
 
 
 
 
138
  if not dest.exists():
139
  try:
140
  print(f'[downloader] Downloading motion extractor...')
141
  _download(motion_url, dest)
 
 
 
 
 
 
 
142
  print(f'[downloader] βœ… Downloaded: {dest}')
143
  except Exception as e:
144
  print(f'[downloader] ❌ Failed to download motion extractor: {e}')
145
  success = False
146
  else:
 
 
 
 
 
 
 
147
  print(f'[downloader] βœ… Motion extractor already exists: {dest}')
148
 
149
  # Download additional models (generator required in neural-only mode)
 
26
  import onnx # type: ignore
27
  except Exception:
28
  onnx = None
29
+ try:
30
+ # Optional: version converter for opset downgrade
31
+ from onnx import version_converter # type: ignore
32
+ except Exception:
33
+ version_converter = None # type: ignore
34
 
35
  try:
36
  from huggingface_hub import hf_hub_download # type: ignore
 
97
  except Exception:
98
  return False
99
 
100
+ def _maybe_convert_opset_to_19(path: Path) -> Path:
101
+ """If ONNX opset > 19, attempt to convert to opset 19 for ORT 1.16.3 compatibility.
102
+ Returns the path to a converted file (sibling with _op19 suffix) or the original path on failure/no-op.
103
+ """
104
+ if onnx is None or version_converter is None or path.suffix != ".onnx":
105
+ return path
106
+ try:
107
+ model = onnx.load(str(path), load_external_data=True)
108
+ max_opset = max((imp.version for imp in model.opset_import), default=0)
109
+ if max_opset and max_opset > 19:
110
+ print(f"[downloader] Downgrading opset from {max_opset} to 19 for {path.name}")
111
+ converted = version_converter.convert_version(model, 19)
112
+ out_path = path.with_name(path.stem + "_op19.onnx")
113
+ onnx.save(converted, str(out_path))
114
+ return out_path
115
+ except Exception as e:
116
+ print(f"[downloader] Opset conversion skipped for {path.name}: {e}")
117
+ return path
118
+
119
  def _download(url: str, dest: Path):
120
  dest.parent.mkdir(parents=True, exist_ok=True)
121
  try:
 
140
 
141
  app_url = os.getenv('MIRAGE_LP_APPEARANCE_URL')
142
  motion_url = os.getenv('MIRAGE_LP_MOTION_URL')
143
+ force_app = os.getenv('MIRAGE_FORCE_DOWNLOAD_APPEARANCE', '0').lower() in ('1','true','yes','on')
144
+ force_mot = os.getenv('MIRAGE_FORCE_DOWNLOAD_MOTION', '0').lower() in ('1','true','yes','on')
145
  success = True
146
 
147
  # Download LivePortrait appearance extractor
148
  if app_url:
149
  dest = LP_DIR / 'appearance_feature_extractor.onnx'
150
+ if force_app and dest.exists():
151
+ try:
152
+ dest.unlink()
153
+ print(f"[downloader] Forcing re-download of appearance: removed existing {dest}")
154
+ except Exception as e:
155
+ print(f"[downloader] Could not remove existing appearance for force download: {e}")
156
  if not dest.exists():
157
  try:
158
  print(f'[downloader] Downloading appearance extractor...')
159
  _download(app_url, dest)
160
+ # Optionally convert opset for compatibility
161
+ converted = _maybe_convert_opset_to_19(dest)
162
+ if converted != dest:
163
+ # Prefer converted model by replacing original
164
+ try:
165
+ shutil.copyfile(converted, dest)
166
+ print(f"[downloader] Replaced appearance with opset19: {converted.name}")
167
+ except Exception:
168
+ pass
169
  print(f'[downloader] βœ… Downloaded: {dest}')
170
  except Exception as e:
171
  print(f'[downloader] ❌ Failed to download appearance extractor: {e}')
172
  success = False
173
  else:
174
+ # Ensure compatibility if a cached file is opset>19
175
+ converted = _maybe_convert_opset_to_19(dest)
176
+ if converted != dest:
177
+ try:
178
+ shutil.copyfile(converted, dest)
179
+ print(f"[downloader] Updated cached appearance to opset19")
180
+ except Exception:
181
+ pass
182
  print(f'[downloader] βœ… Appearance extractor already exists: {dest}')
183
 
184
  # Download LivePortrait motion extractor
185
  if motion_url:
186
  dest = LP_DIR / 'motion_extractor.onnx'
187
+ if force_mot and dest.exists():
188
+ try:
189
+ dest.unlink()
190
+ print(f"[downloader] Forcing re-download of motion: removed existing {dest}")
191
+ except Exception as e:
192
+ print(f"[downloader] Could not remove existing motion for force download: {e}")
193
  if not dest.exists():
194
  try:
195
  print(f'[downloader] Downloading motion extractor...')
196
  _download(motion_url, dest)
197
+ converted = _maybe_convert_opset_to_19(dest)
198
+ if converted != dest:
199
+ try:
200
+ shutil.copyfile(converted, dest)
201
+ print(f"[downloader] Replaced motion with opset19: {converted.name}")
202
+ except Exception:
203
+ pass
204
  print(f'[downloader] βœ… Downloaded: {dest}')
205
  except Exception as e:
206
  print(f'[downloader] ❌ Failed to download motion extractor: {e}')
207
  success = False
208
  else:
209
+ converted = _maybe_convert_opset_to_19(dest)
210
+ if converted != dest:
211
+ try:
212
+ shutil.copyfile(converted, dest)
213
+ print(f"[downloader] Updated cached motion to opset19")
214
+ except Exception:
215
+ pass
216
  print(f'[downloader] βœ… Motion extractor already exists: {dest}')
217
 
218
  # Download additional models (generator required in neural-only mode)
webrtc_fallback.py CHANGED
@@ -1,38 +0,0 @@
1
- """
2
- Minimal fallback WebRTC router when aiortc isn't available.
3
- Provides basic endpoints that return appropriate errors instead of 503.
4
- """
5
- from fastapi import APIRouter, HTTPException
6
- from pydantic import BaseModel
7
- from typing import Dict, Any
8
-
9
- router = APIRouter(prefix="/webrtc", tags=["webrtc-fallback"])
10
-
11
- class OfferRequest(BaseModel):
12
- offer: Dict[str, Any]
13
-
14
- @router.get("/token")
15
- async def get_token():
16
- """Fallback token endpoint"""
17
- raise HTTPException(
18
- status_code=503,
19
- detail="WebRTC not available: aiortc dependencies missing. Please check Docker build logs."
20
- )
21
-
22
- @router.post("/offer")
23
- async def create_offer(request: OfferRequest):
24
- """Fallback offer endpoint"""
25
- raise HTTPException(
26
- status_code=503,
27
- detail="WebRTC not available: aiortc dependencies missing. Please check Docker build logs."
28
- )
29
-
30
- @router.get("/ping")
31
- async def ping():
32
- """Fallback ping endpoint"""
33
- return {"status": "WebRTC unavailable", "aiortc": False, "timestamp": __import__("time").time()}
34
-
35
- @router.post("/cleanup")
36
- async def cleanup():
37
- """Fallback cleanup endpoint"""
38
- return {"status": "no-op"}