File size: 26,671 Bytes
91d209c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66e744c
91d209c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b7ac53c
91d209c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b7ac53c
91d209c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66e744c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91d209c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
"""
Video Generation API endpoints
Handles KIE API integration with SSE support for real-time updates
"""

from fastapi import APIRouter, HTTPException, Request
from fastapi.responses import StreamingResponse, JSONResponse, Response
from pydantic import BaseModel
from typing import List, Optional, Dict, Any
import httpx
import asyncio
import json
import os
from datetime import datetime

from utils.image_processor import compress_and_store_image
from utils.storage import video_results, sse_clients, cleanup_old_results
import asyncio

router = APIRouter()

KIE_API_BASE = "https://api.kie.ai"

# Request/Response Models
class VideoGenerationRequest(BaseModel):
    prompt: Any  # Can be string (legacy) or dict/object (structured JSON)
    imageUrls: Optional[List[str]] = []
    model: Optional[str] = "veo3_fast"
    aspectRatio: Optional[str] = "9:16"
    generationType: Optional[str] = None
    seeds: Optional[int] = None  # Seed for consistent lighting/style (e.g., 12005)
    voiceType: Optional[str] = None  # Voice type for audio generation (e.g., "Deep", "Warm", "Crisp", "None")

class VideoExtendRequest(BaseModel):
    taskId: str
    prompt: Any  # Can be string or structured JSON
    seeds: Optional[int] = None
    watermark: Optional[str] = None
    voiceType: Optional[str] = None  # Voice type for audio generation

class VideoGenerationResponse(BaseModel):
    taskId: str
    status: str

class CallbackData(BaseModel):
    code: int
    msg: str
    data: Optional[Dict[str, Any]] = None

# Helper functions
def get_kie_api_key():
    """Get KIE API key from environment"""
    api_key = os.getenv('KIE_API_KEY')
    if not api_key:
        raise HTTPException(
            status_code=500,
            detail="KIE_API_KEY not configured on server."
        )
    return api_key

async def send_sse_event(task_id: str, data: dict):
    """Send Server-Sent Event to connected client"""
    if task_id in sse_clients:
        queue = sse_clients[task_id]
        await queue.put(data)

# Endpoints
@router.post("/veo/generate", response_model=VideoGenerationResponse)
async def generate_video(request: VideoGenerationRequest, req: Request):
    """
    Generate video using KIE Veo 3.1 API
    Supports text-to-video and image-to-video generation
    """
    try:
        api_key = get_kie_api_key()
        
        # Build public URL for callback
        public_url = os.getenv('VITE_API_BASE_URL', f"http://localhost:{os.getenv('SERVER_PORT', 4000)}")
        callback_url = f"{public_url}/api/veo/callback"
        
        # Process image URLs
        public_image_urls = []
        if request.imageUrls:
            print(f"πŸ“· Processing {len(request.imageUrls)} images...")
            for image_url in request.imageUrls:
                # If it's already a public URL, use it as-is
                if image_url.startswith(('http://', 'https://')):
                    print(f"   Using external URL: {image_url}")
                    public_image_urls.append(image_url)
                else:
                    # Compress and host the data URL
                    hosted_url = await compress_and_store_image(image_url, public_url)
                    print(f"   Hosted image: {hosted_url}")
                    public_image_urls.append(hosted_url)
        
        # Determine generation type
        generation_type = request.generationType
        if not generation_type:
            generation_type = "FIRST_AND_LAST_FRAMES_2_VIDEO" if public_image_urls else "TEXT_2_VIDEO"
        
        # Log prompt format and seed
        if isinstance(request.prompt, dict):
            print(f"πŸ“ Sending structured JSON prompt to Veo 3.1")
        else:
            print(f"πŸ“ Sending text prompt to Veo 3.1")
        
        if request.seeds:
            print(f"🎲 Using seed: {request.seeds} (warm, flattering lighting)")
        
        # Call KIE API
        async with httpx.AsyncClient(timeout=30.0) as client:
            payload = {
                "prompt": request.prompt,  # Can be string or structured JSON object
                "imageUrls": public_image_urls,
                "model": request.model,
                "aspectRatio": request.aspectRatio,
                "generationType": generation_type,
                "enableTranslation": True,
                "callBackUrl": callback_url
            }
            
            # Add optional seed parameter
            if request.seeds is not None:
                payload["seeds"] = request.seeds
            
            # Add voice type for audio generation (if not "None")
            if request.voiceType and request.voiceType.lower() != "none":
                payload["voiceType"] = request.voiceType
                print(f"🎀 Using voice type: {request.voiceType}")
            else:
                print(f"πŸ”‡ No voice/audio requested (voiceType: {request.voiceType})")
            
            response = await client.post(
                f"{KIE_API_BASE}/api/v1/veo/generate",
                headers={
                    "Authorization": f"Bearer {api_key}",
                    "Content-Type": "application/json"
                },
                json=payload
            )
        
        # Log raw response for debugging
        print(f"πŸ“‘ KIE API response status: {response.status_code}")
        
        # Check HTTP status first
        if response.status_code != 200:
            error_text = response.text
            content_type = response.headers.get('content-type', '').lower()
            
            # Handle HTML error responses (like 502 Bad Gateway pages)
            if 'text/html' in content_type or error_text.strip().startswith('<!DOCTYPE') or error_text.strip().startswith('<html'):
                # Extract meaningful error from HTML if possible
                error_message = f"KIE API service unavailable (HTTP {response.status_code})"
                
                # Try to extract title or error message from HTML
                if '<title>' in error_text:
                    import re
                    title_match = re.search(r'<title>(.*?)</title>', error_text, re.IGNORECASE | re.DOTALL)
                    if title_match:
                        title = title_match.group(1).strip()
                        # Extract just the error part (e.g., "502: Bad gateway" from "kie.ai | 502: Bad gateway")
                        if ':' in title:
                            error_message = f"KIE API error: {title.split('|')[-1].strip()}"
                        else:
                            error_message = f"KIE API error: {title}"
                
                print(f"❌ KIE API HTTP error: {response.status_code} - {error_message}")
                raise HTTPException(
                    status_code=502,  # Bad Gateway - the KIE API is down/unavailable
                    detail=error_message
                )
            else:
                # Non-HTML error response, try to extract JSON error if possible
                try:
                    error_data = response.json()
                    error_message = error_data.get('msg') or error_data.get('message') or error_data.get('detail') or f"KIE API error (HTTP {response.status_code})"
                except (json.JSONDecodeError, ValueError):
                    # Not JSON, use text (truncated)
                    error_message = error_text[:200] if len(error_text) > 200 else error_text
                
                print(f"❌ KIE API HTTP error: {response.status_code} - {error_message[:200]}")
            raise HTTPException(
                status_code=response.status_code,
                    detail=f"KIE API error: {error_message}"
            )
        
        result = response.json()
        print(f"πŸ“‘ KIE API result code: {result.get('code')}, msg: {result.get('msg')}")
        
        if result.get('code') != 200:
            raise HTTPException(
                status_code=result.get('code', 500),
                detail=result.get('msg', 'KIE API request failed')
            )
        
        task_id = result['data']['taskId']
        print(f"βœ… Video generation started: {task_id}")
        
        return VideoGenerationResponse(
            taskId=task_id,
            status="processing"
        )
    
    except HTTPException:
        raise
    except httpx.HTTPStatusError as e:
        error_text = e.response.text
        content_type = e.response.headers.get('content-type', '').lower()
        
        # Handle HTML error responses
        if 'text/html' in content_type or error_text.strip().startswith('<!DOCTYPE') or error_text.strip().startswith('<html'):
            error_msg = f"KIE API service unavailable (HTTP {e.response.status_code})"
            # Try to extract meaningful error from HTML
            if '<title>' in error_text:
                import re
                title_match = re.search(r'<title>(.*?)</title>', error_text, re.IGNORECASE | re.DOTALL)
                if title_match:
                    title = title_match.group(1).strip()
                    if ':' in title:
                        error_msg = f"KIE API error: {title.split('|')[-1].strip()}"
        else:
            # Try to extract JSON error if possible
            try:
                error_data = e.response.json()
                error_msg = error_data.get('msg') or error_data.get('message') or error_data.get('detail') or f"KIE API error (HTTP {e.response.status_code})"
            except (json.JSONDecodeError, ValueError):
                error_msg = error_text[:200] if len(error_text) > 200 else error_text
        
        print(f"❌ {error_msg}")
        raise HTTPException(status_code=502 if 'text/html' in content_type else e.response.status_code, detail=error_msg)
    except httpx.RequestError as e:
        error_msg = f"KIE API request error: {type(e).__name__} - {str(e)}"
        print(f"❌ {error_msg}")
        raise HTTPException(status_code=502, detail=error_msg)
    except json.JSONDecodeError as e:
        error_msg = f"Invalid JSON response from KIE API. The service may be unavailable."
        print(f"❌ JSON decode error: {str(e)}")
        raise HTTPException(status_code=502, detail=error_msg)
    except Exception as e:
        import traceback
        error_msg = f"{type(e).__name__}: {str(e)}"
        print(f"❌ Video generation error: {error_msg}")
        traceback.print_exc()
        raise HTTPException(
            status_code=500,
            detail=f"Video generation request failed: {error_msg}"
        )

@router.post("/veo/callback")
async def veo_callback(callback_data: CallbackData):
    """
    Callback endpoint for KIE API
    Receives video generation status updates
    """
    try:
        data = callback_data.data or {}
        task_id = data.get('taskId')
        info = data.get('info', {})
        fallback_flag = data.get('fallbackFlag')
        
        print(f"πŸ“₯ Callback received for task {task_id}: code={callback_data.code}, msg={callback_data.msg}")
        
        # Store result
        video_results[task_id] = {
            'code': callback_data.code,
            'msg': callback_data.msg,
            'taskId': task_id,
            'info': info,
            'fallbackFlag': fallback_flag,
            'timestamp': datetime.now().timestamp()
        }
        
        # Send SSE update to client
        if callback_data.code == 200 and info:
            await send_sse_event(task_id, {
                'status': 'succeeded',
                'url': info.get('resultUrls', [None])[0],
                'resultUrls': info.get('resultUrls', []),
                'originUrls': info.get('originUrls', []),
                'resolution': info.get('resolution'),
                'fallbackFlag': fallback_flag
            })
        else:
            await send_sse_event(task_id, {
                'status': 'failed',
                'error': callback_data.msg,
                'code': callback_data.code
            })
        
        # Clean up old results
        cleanup_old_results()
        
        return JSONResponse(
            status_code=200,
            content={'code': 200, 'msg': 'success'}
        )
    
    except Exception as e:
        print(f"❌ Callback processing error: {str(e)}")
        raise HTTPException(
            status_code=500,
            detail="Failed to process callback"
        )


@router.post("/veo/extend", response_model=VideoGenerationResponse)
async def extend_video(request: VideoExtendRequest):
    """
    Extend an existing video using KIE Veo 3.1 extend API
    Takes an existing taskId and extends it with new prompt
    """
    try:
        api_key = get_kie_api_key()
        
        # Build public URL for callback
        public_url = os.getenv('VITE_API_BASE_URL', f"http://localhost:{os.getenv('SERVER_PORT', 4000)}")
        callback_url = f"{public_url}/api/veo/callback"
        
        print(f"🎬 Extending video from task: {request.taskId}")
        
        # Log prompt format and seed
        if isinstance(request.prompt, dict):
            print(f"πŸ“ Extending with structured JSON prompt")
        else:
            print(f"πŸ“ Extending with text prompt")
        
        if request.seeds:
            print(f"🎲 Using seed: {request.seeds} (consistent lighting)")
        
        # Call KIE extend API
        async with httpx.AsyncClient(timeout=30.0) as client:
            payload = {
                "taskId": request.taskId,
                "prompt": request.prompt,
                "callBackUrl": callback_url
            }
            
            # Add optional parameters
            if request.seeds is not None:
                payload["seeds"] = request.seeds
            if request.watermark:
                payload["watermark"] = request.watermark
            if request.voiceType and request.voiceType.lower() != "none":
                payload["voiceType"] = request.voiceType
                print(f"🎀 Using voice type: {request.voiceType}")
            else:
                print(f"πŸ”‡ No voice/audio requested (voiceType: {request.voiceType})")
            
            response = await client.post(
                f"{KIE_API_BASE}/api/v1/veo/extend",
                headers={
                    "Authorization": f"Bearer {api_key}",
                    "Content-Type": "application/json"
                },
                json=payload
            )
        
        # Check for HTML error responses
        if response.status_code != 200:
            error_text = response.text
            content_type = response.headers.get('content-type', '').lower()
            
            if 'text/html' in content_type or error_text.strip().startswith('<!DOCTYPE') or error_text.strip().startswith('<html'):
                error_message = f"KIE API service unavailable (HTTP {response.status_code})"
                if '<title>' in error_text:
                    import re
                    title_match = re.search(r'<title>(.*?)</title>', error_text, re.IGNORECASE | re.DOTALL)
                    if title_match:
                        title = title_match.group(1).strip()
                        if ':' in title:
                            error_message = f"KIE API error: {title.split('|')[-1].strip()}"
                raise HTTPException(status_code=502, detail=error_message)
        
        result = response.json()
        
        if result.get('code') != 200:
            raise HTTPException(
                status_code=result.get('code', 500),
                detail=result.get('msg', 'KIE extend API request failed')
            )
        
        new_task_id = result['data']['taskId']
        print(f"βœ… Video extension started: {new_task_id}")
        
        return VideoGenerationResponse(
            taskId=new_task_id,
            status="processing"
        )
    
    except HTTPException:
        raise
    except httpx.HTTPStatusError as e:
        error_text = e.response.text
        content_type = e.response.headers.get('content-type', '').lower()
        
        if 'text/html' in content_type or error_text.strip().startswith('<!DOCTYPE') or error_text.strip().startswith('<html'):
            error_msg = f"KIE API service unavailable (HTTP {e.response.status_code})"
            if '<title>' in error_text:
                import re
                title_match = re.search(r'<title>(.*?)</title>', error_text, re.IGNORECASE | re.DOTALL)
                if title_match:
                    title = title_match.group(1).strip()
                    if ':' in title:
                        error_msg = f"KIE API error: {title.split('|')[-1].strip()}"
        else:
            try:
                error_data = e.response.json()
                error_msg = error_data.get('msg') or error_data.get('message') or error_data.get('detail') or f"KIE API error (HTTP {e.response.status_code})"
            except (json.JSONDecodeError, ValueError):
                error_msg = error_text[:200] if len(error_text) > 200 else error_text
        
        print(f"❌ {error_msg}")
        raise HTTPException(status_code=502 if 'text/html' in content_type else e.response.status_code, detail=error_msg)
    except httpx.RequestError as e:
        error_msg = f"KIE API request error: {type(e).__name__} - {str(e)}"
        print(f"❌ {error_msg}")
        raise HTTPException(status_code=502, detail=error_msg)
    except json.JSONDecodeError as e:
        error_msg = f"Invalid JSON response from KIE API. The service may be unavailable."
        print(f"❌ JSON decode error: {str(e)}")
        raise HTTPException(status_code=502, detail=error_msg)
    except Exception as e:
        import traceback
        error_msg = f"{type(e).__name__}: {str(e)}"
        print(f"❌ Video extension error: {error_msg}")
        traceback.print_exc()
        raise HTTPException(
            status_code=500,
            detail=f"Video extension error: {error_msg}"
        )


@router.get("/veo/events/{task_id}")
async def sse_events(task_id: str):
    """
    Server-Sent Events endpoint for real-time updates
    """
    async def event_generator():
        # Create queue for this client
        queue = asyncio.Queue()
        sse_clients[task_id] = queue
        
        print(f"πŸ”Œ SSE client connected for task {task_id}")
        
        try:
            # Check if result already exists
            if task_id in video_results:
                result = video_results[task_id]
                if result['code'] == 200 and result.get('info'):
                    info = result['info']
                    event_data = {
                        'status': 'succeeded',
                        'url': info.get('resultUrls', [None])[0],
                        'resultUrls': info.get('resultUrls', []),
                        'originUrls': info.get('originUrls', []),
                        'resolution': info.get('resolution'),
                        'fallbackFlag': result.get('fallbackFlag')
                    }
                else:
                    event_data = {
                        'status': 'failed',
                        'error': result['msg'],
                        'code': result['code']
                    }
                yield f"data: {json.dumps(event_data)}\n\n"
            
            # Stream events
            while True:
                data = await queue.get()
                yield f"data: {json.dumps(data)}\n\n"
        
        except asyncio.CancelledError:
            print(f"πŸ”Œ SSE client disconnected for task {task_id}")
        finally:
            if task_id in sse_clients:
                del sse_clients[task_id]
    
    return StreamingResponse(
        event_generator(),
        media_type="text/event-stream",
        headers={
            "Cache-Control": "no-cache",
            "Connection": "keep-alive"
        }
    )

@router.get("/veo/status/{task_id}")
async def get_video_status(task_id: str):
    """
    Get video generation status from KIE API
    """
    try:
        api_key = get_kie_api_key()
        
        async with httpx.AsyncClient(timeout=30.0) as client:
            response = await client.get(
                f"{KIE_API_BASE}/api/v1/veo/video/{task_id}",
                headers={
                    "Authorization": f"Bearer {api_key}"
                }
            )
        
        # Check for HTML error responses
        if response.status_code != 200:
            error_text = response.text
            content_type = response.headers.get('content-type', '').lower()
            
            if 'text/html' in content_type or error_text.strip().startswith('<!DOCTYPE') or error_text.strip().startswith('<html'):
                error_message = f"KIE API service unavailable (HTTP {response.status_code})"
                if '<title>' in error_text:
                    import re
                    title_match = re.search(r'<title>(.*?)</title>', error_text, re.IGNORECASE | re.DOTALL)
                    if title_match:
                        title = title_match.group(1).strip()
                        if ':' in title:
                            error_message = f"KIE API error: {title.split('|')[-1].strip()}"
                raise HTTPException(status_code=502, detail=error_message)
        
        result = response.json()
        
        if result.get('code') != 200:
            raise HTTPException(
                status_code=result.get('code', 500),
                detail=result.get('msg', 'Failed to get video status')
            )
        
        # Transform response
        status = result['data'].get('status')
        video_url = result['data'].get('videoUrl')
        
        return {
            'status': 'succeeded' if status == 'completed' else 'failed' if status == 'failed' else 'processing',
            'output': video_url if status == 'completed' else None,
            'url': video_url if status == 'completed' else None
        }
    
    except HTTPException:
        raise
    except httpx.HTTPStatusError as e:
        error_text = e.response.text
        content_type = e.response.headers.get('content-type', '').lower()
        
        if 'text/html' in content_type or error_text.strip().startswith('<!DOCTYPE') or error_text.strip().startswith('<html'):
            error_msg = f"KIE API service unavailable (HTTP {e.response.status_code})"
            if '<title>' in error_text:
                import re
                title_match = re.search(r'<title>(.*?)</title>', error_text, re.IGNORECASE | re.DOTALL)
                if title_match:
                    title = title_match.group(1).strip()
                    if ':' in title:
                        error_msg = f"KIE API error: {title.split('|')[-1].strip()}"
        else:
            try:
                error_data = e.response.json()
                error_msg = error_data.get('msg') or error_data.get('message') or error_data.get('detail') or f"KIE API error (HTTP {e.response.status_code})"
            except (json.JSONDecodeError, ValueError):
                error_msg = error_text[:200] if len(error_text) > 200 else error_text
        
        print(f"❌ {error_msg}")
        raise HTTPException(status_code=502 if 'text/html' in content_type else e.response.status_code, detail=error_msg)
    except httpx.RequestError as e:
        error_msg = f"KIE API request error: {type(e).__name__} - {str(e)}"
        print(f"❌ {error_msg}")
        raise HTTPException(status_code=502, detail=error_msg)
    except json.JSONDecodeError as e:
        error_msg = f"Invalid JSON response from KIE API. The service may be unavailable."
        print(f"❌ JSON decode error: {str(e)}")
        raise HTTPException(status_code=502, detail=error_msg)
    except Exception as e:
        import traceback
        error_msg = f"{type(e).__name__}: {str(e)}"
        print(f"❌ Status check error: {error_msg}")
        traceback.print_exc()
        raise HTTPException(
            status_code=500,
            detail=f"Failed to check video status: {error_msg}"
        )

@router.post("/veo/cancel/{task_id}")
async def cancel_video_generation(task_id: str):
    """
    Cancel an ongoing video generation task
    """
    try:
        # Send cancellation event to SSE clients
        if task_id in sse_clients:
            queue = sse_clients[task_id]
            await send_sse_event(task_id, {
                'status': 'cancelled',
                'message': 'Video generation cancelled by user'
            })
            # Close the SSE connection
            del sse_clients[task_id]
            print(f"βœ… Cancelled video generation: {task_id}")
        
        # Mark result as cancelled
        if task_id in video_results:
            video_results[task_id] = {
                'code': 499,  # Client Closed Request
                'msg': 'Video generation cancelled by user',
                'taskId': task_id,
                'timestamp': datetime.now().timestamp()
            }
        
        return JSONResponse(
            status_code=200,
            content={'code': 200, 'msg': 'Video generation cancelled', 'taskId': task_id}
        )
    
    except Exception as e:
        print(f"❌ Error cancelling video generation: {str(e)}")
        raise HTTPException(
            status_code=500,
            detail=f"Failed to cancel video generation: {str(e)}"
        )

@router.get("/veo/download")
async def download_video(url: str):
    """
    Download video from external URL
    Proxies the video stream to avoid CORS issues
    """
    if not url:
        raise HTTPException(status_code=400, detail="Missing url query parameter")
    
    try:
        async with httpx.AsyncClient(timeout=60.0) as client:
            response = await client.get(url)
            if response.status_code != 200:
                raise HTTPException(
                    status_code=response.status_code,
                    detail="Failed to download asset"
                )
            
            # Return the video content directly
            return Response(
                content=response.content,
                media_type=response.headers.get('content-type', 'video/mp4'),
                headers={
                    'Content-Disposition': 'attachment; filename="video.mp4"',
                    'Content-Length': str(len(response.content))
                }
            )
    
    except HTTPException:
        raise
    except Exception as e:
        print(f"❌ Download error: {str(e)}")
        raise HTTPException(
            status_code=500,
            detail=f"Failed to download asset: {str(e)}"
        )