itishalogicgo commited on
Commit
ade7b32
·
1 Parent(s): 71b3815

Add retry logic, HTML error detection, and increased timeout for PixVerse API calls

Browse files
Files changed (1) hide show
  1. services/pixverse_service.py +140 -28
services/pixverse_service.py CHANGED
@@ -9,6 +9,9 @@ class PixVerseService:
9
  def __init__(self):
10
  self.base_url = config.PIXVERSE_BASE_URL
11
  self.api_key = config.PIXVERSE_API_KEY
 
 
 
12
 
13
  def _get_headers(self) -> dict:
14
  return {
@@ -16,7 +19,112 @@ class PixVerseService:
16
  "Ai-trace-id": str(uuid.uuid4())
17
  }
18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  async def upload_image(self, image_content: bytes, content_type: str) -> dict:
 
20
  headers = self._get_headers()
21
 
22
  extension = content_type.split('/')[-1]
@@ -24,15 +132,15 @@ class PixVerseService:
24
  extension = 'jpg'
25
  filename = f"image.{extension}"
26
 
27
- async with httpx.AsyncClient(timeout=60.0) as client:
28
- files = {"image": (filename, image_content, content_type)}
29
- response = await client.post(
30
- f"{self.base_url}/image/upload",
31
- headers=headers,
32
- files=files
33
- )
34
- response.raise_for_status()
35
- return response.json()
36
 
37
  async def generate_video(
38
  self,
@@ -45,6 +153,7 @@ class PixVerseService:
45
  negative_prompt: Optional[str] = None,
46
  seed: Optional[int] = None
47
  ) -> dict:
 
48
  headers = self._get_headers()
49
  headers["Content-Type"] = "application/json"
50
 
@@ -62,27 +171,27 @@ class PixVerseService:
62
  if seed is not None:
63
  payload["seed"] = seed
64
 
65
- async with httpx.AsyncClient(timeout=60.0) as client:
66
- response = await client.post(
67
- f"{self.base_url}/video/img/generate",
68
- headers=headers,
69
- json=payload
70
- )
71
- response.raise_for_status()
72
- return response.json()
73
 
74
  async def get_video_status(self, video_id: int) -> dict:
 
75
  headers = self._get_headers()
76
 
77
- async with httpx.AsyncClient(timeout=30.0) as client:
78
- response = await client.get(
79
- f"{self.base_url}/video/result/{video_id}",
80
- headers=headers
81
- )
82
- response.raise_for_status()
83
- return response.json()
84
 
85
  async def wait_for_video(self, video_id: int, max_attempts: int = 60, delay: int = 5) -> dict:
 
86
  for attempt in range(max_attempts):
87
  result = await self.get_video_status(video_id)
88
 
@@ -106,10 +215,13 @@ class PixVerseService:
106
  raise Exception("Video generation timed out. Please try again later.")
107
 
108
  async def download_video(self, video_url: str) -> bytes:
109
- async with httpx.AsyncClient(timeout=120.0) as client:
110
- response = await client.get(video_url)
111
- response.raise_for_status()
112
- return response.content
 
 
 
113
 
114
 
115
  pixverse_service = PixVerseService()
 
9
  def __init__(self):
10
  self.base_url = config.PIXVERSE_BASE_URL
11
  self.api_key = config.PIXVERSE_API_KEY
12
+ self.default_timeout = 120.0 # 2 minutes timeout
13
+ self.max_retries = 3
14
+ self.retry_delay = 3 # seconds between retries
15
 
16
  def _get_headers(self) -> dict:
17
  return {
 
19
  "Ai-trace-id": str(uuid.uuid4())
20
  }
21
 
22
+ def _is_html_response(self, response: httpx.Response) -> bool:
23
+ """Check if response is HTML (error page) instead of JSON"""
24
+ content_type = response.headers.get("content-type", "")
25
+ if "text/html" in content_type:
26
+ return True
27
+ # Also check response text for HTML doctype
28
+ try:
29
+ text = response.text
30
+ if text.strip().startswith("<!DOCTYPE") or text.strip().startswith("<html"):
31
+ return True
32
+ except:
33
+ pass
34
+ return False
35
+
36
+ async def _request_with_retry(
37
+ self,
38
+ method: str,
39
+ url: str,
40
+ retries: int = None,
41
+ **kwargs
42
+ ) -> httpx.Response:
43
+ """
44
+ Make HTTP request with automatic retry on failure or HTML error response.
45
+
46
+ Args:
47
+ method: HTTP method (GET, POST)
48
+ url: Full URL to request
49
+ retries: Number of retries (default: self.max_retries)
50
+ **kwargs: Additional arguments for httpx request
51
+
52
+ Returns:
53
+ httpx.Response object
54
+
55
+ Raises:
56
+ Exception if all retries exhausted
57
+ """
58
+ if retries is None:
59
+ retries = self.max_retries
60
+
61
+ last_error = None
62
+
63
+ for attempt in range(retries + 1):
64
+ try:
65
+ async with httpx.AsyncClient(timeout=self.default_timeout) as client:
66
+ if method.upper() == "GET":
67
+ response = await client.get(url, **kwargs)
68
+ elif method.upper() == "POST":
69
+ response = await client.post(url, **kwargs)
70
+ else:
71
+ raise ValueError(f"Unsupported HTTP method: {method}")
72
+
73
+ # Check for HTML error response (Hugging Face/upstream error)
74
+ if self._is_html_response(response):
75
+ error_msg = f"Received HTML error response from upstream (attempt {attempt + 1}/{retries + 1})"
76
+ print(f"[PixVerse] {error_msg}")
77
+
78
+ if attempt < retries:
79
+ print(f"[PixVerse] Retrying in {self.retry_delay} seconds...")
80
+ await asyncio.sleep(self.retry_delay)
81
+ continue
82
+ else:
83
+ raise Exception("Upstream service returned HTML error. Please try again later.")
84
+
85
+ # Raise for HTTP errors
86
+ response.raise_for_status()
87
+ return response
88
+
89
+ except httpx.TimeoutException as e:
90
+ last_error = e
91
+ error_msg = f"Request timeout (attempt {attempt + 1}/{retries + 1})"
92
+ print(f"[PixVerse] {error_msg}")
93
+
94
+ if attempt < retries:
95
+ print(f"[PixVerse] Retrying in {self.retry_delay} seconds...")
96
+ await asyncio.sleep(self.retry_delay)
97
+ continue
98
+
99
+ except httpx.HTTPStatusError as e:
100
+ last_error = e
101
+ # Don't retry on 4xx client errors (except 429 rate limit)
102
+ if 400 <= e.response.status_code < 500 and e.response.status_code != 429:
103
+ raise
104
+
105
+ error_msg = f"HTTP error {e.response.status_code} (attempt {attempt + 1}/{retries + 1})"
106
+ print(f"[PixVerse] {error_msg}")
107
+
108
+ if attempt < retries:
109
+ print(f"[PixVerse] Retrying in {self.retry_delay} seconds...")
110
+ await asyncio.sleep(self.retry_delay)
111
+ continue
112
+
113
+ except Exception as e:
114
+ last_error = e
115
+ error_msg = f"Request failed: {str(e)[:100]} (attempt {attempt + 1}/{retries + 1})"
116
+ print(f"[PixVerse] {error_msg}")
117
+
118
+ if attempt < retries:
119
+ print(f"[PixVerse] Retrying in {self.retry_delay} seconds...")
120
+ await asyncio.sleep(self.retry_delay)
121
+ continue
122
+
123
+ # All retries exhausted
124
+ raise Exception(f"Request failed after {retries + 1} attempts: {str(last_error)}")
125
+
126
  async def upload_image(self, image_content: bytes, content_type: str) -> dict:
127
+ """Upload image to PixVerse with retry logic"""
128
  headers = self._get_headers()
129
 
130
  extension = content_type.split('/')[-1]
 
132
  extension = 'jpg'
133
  filename = f"image.{extension}"
134
 
135
+ files = {"image": (filename, image_content, content_type)}
136
+
137
+ response = await self._request_with_retry(
138
+ method="POST",
139
+ url=f"{self.base_url}/image/upload",
140
+ headers=headers,
141
+ files=files
142
+ )
143
+ return response.json()
144
 
145
  async def generate_video(
146
  self,
 
153
  negative_prompt: Optional[str] = None,
154
  seed: Optional[int] = None
155
  ) -> dict:
156
+ """Generate video from image with retry logic"""
157
  headers = self._get_headers()
158
  headers["Content-Type"] = "application/json"
159
 
 
171
  if seed is not None:
172
  payload["seed"] = seed
173
 
174
+ response = await self._request_with_retry(
175
+ method="POST",
176
+ url=f"{self.base_url}/video/img/generate",
177
+ headers=headers,
178
+ json=payload
179
+ )
180
+ return response.json()
 
181
 
182
  async def get_video_status(self, video_id: int) -> dict:
183
+ """Get video generation status with retry logic"""
184
  headers = self._get_headers()
185
 
186
+ response = await self._request_with_retry(
187
+ method="GET",
188
+ url=f"{self.base_url}/video/result/{video_id}",
189
+ headers=headers
190
+ )
191
+ return response.json()
 
192
 
193
  async def wait_for_video(self, video_id: int, max_attempts: int = 60, delay: int = 5) -> dict:
194
+ """Wait for video generation to complete"""
195
  for attempt in range(max_attempts):
196
  result = await self.get_video_status(video_id)
197
 
 
215
  raise Exception("Video generation timed out. Please try again later.")
216
 
217
  async def download_video(self, video_url: str) -> bytes:
218
+ """Download generated video with retry logic"""
219
+ response = await self._request_with_retry(
220
+ method="GET",
221
+ url=video_url,
222
+ retries=3 # More retries for download
223
+ )
224
+ return response.content
225
 
226
 
227
  pixverse_service = PixVerseService()