PlotweaverModel commited on
Commit
fcd28fa
·
verified ·
1 Parent(s): e6b274c

updated app

Browse files
Files changed (1) hide show
  1. app.py +131 -40
app.py CHANGED
@@ -428,68 +428,144 @@ def generate_video_happyhorse_app(prompt, api_key, duration=5, aspect="16:9", im
428
  "aspect_ratio": aspect,
429
  "sound": False,
430
  }
431
- if image_url:
 
432
  payload["image_urls"] = [image_url]
433
 
434
- resp = http_requests.post(
435
- HAPPYHORSE_PROVIDERS["happyhorse.app"]["generate"],
436
- json=payload, headers=headers, timeout=30,
437
- )
438
- if resp.status_code != 200:
439
- raise RuntimeError(f"happyhorse.app submit failed: {resp.text[:200]}")
440
 
441
- task_id = resp.json().get("data", {}).get("task_id")
 
 
 
 
 
 
 
 
 
 
 
 
 
442
  if not task_id:
443
- raise RuntimeError("No task_id returned")
 
 
444
 
 
445
  status_url = HAPPYHORSE_PROVIDERS["happyhorse.app"]["status"]
446
- for _ in range(120):
447
  time.sleep(10)
448
- s = http_requests.get(f"{status_url}?task_id={task_id}", headers=headers, timeout=30).json()
449
- status = s.get("data", {}).get("status", "")
450
- if status == "SUCCESS":
451
- urls = s.get("data", {}).get("response", {}).get("resultUrls", [])
452
- return urls[0] if urls else None
453
- elif status == "FAILED":
454
- raise RuntimeError(f"happyhorse.app failed: {s.get('data', {}).get('error_message', '')}")
455
- raise RuntimeError("happyhorse.app timeout")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
456
 
457
 
458
  def generate_video_dashscope(prompt, api_key, duration=5, aspect="16:9", image_url=None):
459
  """Generate video via DashScope Bailian (async task API)."""
460
- headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json", "X-DashScope-Async": "enable"}
 
 
 
 
461
  payload = {
462
  "model": "happyhorse-1.0",
463
  "input": {"prompt": prompt},
464
  "parameters": {"duration": duration, "aspect_ratio": aspect},
465
  }
466
- if image_url:
467
  payload["input"]["image_url"] = image_url
468
 
469
- resp = http_requests.post(
470
- HAPPYHORSE_PROVIDERS["DashScope (Bailian)"]["generate"],
471
- json=payload, headers=headers, timeout=30,
472
- )
 
 
 
473
  if resp.status_code != 200:
474
- raise RuntimeError(f"DashScope submit failed: {resp.text[:200]}")
 
 
 
 
 
475
 
476
- task_id = resp.json().get("output", {}).get("task_id")
477
  if not task_id:
478
- raise RuntimeError("No task_id returned from DashScope")
479
 
480
- for _ in range(120):
 
 
481
  time.sleep(10)
482
- s = http_requests.get(
483
- f"{HAPPYHORSE_PROVIDERS['DashScope (Bailian)']['status']}/{task_id}",
484
- headers={"Authorization": f"Bearer {api_key}"}, timeout=30,
485
- ).json()
 
 
 
 
 
486
  status = s.get("output", {}).get("task_status", "")
 
 
487
  if status == "SUCCEEDED":
488
  results = s.get("output", {}).get("results", [])
489
- return results[0].get("url") if results else None
 
 
 
 
490
  elif status == "FAILED":
491
- raise RuntimeError(f"DashScope failed: {s.get('output', {}).get('message', '')}")
492
- raise RuntimeError("DashScope timeout")
 
 
493
 
494
 
495
  def generate_video(prompt, provider, api_key, duration=5, aspect="16:9", image_url=None):
@@ -662,6 +738,7 @@ def generate_storybook(
662
 
663
  # --- Step 2: Generate video ---
664
  progress(base_frac + 0.02, desc=f"Scene {i+1}/{total}: Generating video...")
 
665
  try:
666
  video_url = generate_video(
667
  video_prompt, video_provider, video_key,
@@ -670,18 +747,32 @@ def generate_storybook(
670
  scene_video_path = os.path.join(tmp_dir, f"scene_{i:03d}_video.mp4")
671
  if video_url:
672
  download_video(video_url, scene_video_path)
 
 
 
 
 
673
  else:
674
- raise RuntimeError("No video URL returned")
 
675
  except Exception as e:
676
- # Create a black placeholder video
677
- scene_video_path = os.path.join(tmp_dir, f"scene_{i:03d}_black.mp4")
 
 
 
 
 
 
678
  subprocess.run([
679
  "ffmpeg", "-y", "-f", "lavfi", "-i",
680
- f"color=c=black:s=1280x720:d={video_duration}:r=24",
 
 
681
  "-c:v", "libx264", "-preset", "fast",
682
  scene_video_path,
683
  ], capture_output=True, check=True)
684
- all_transcripts.append(f"Scene {i+1} video failed: {str(e)[:100]}")
685
 
686
  # --- Step 3: Generate narration audio ---
687
  progress(base_frac + 0.04, desc=f"Scene {i+1}/{total}: Narrating...")
 
428
  "aspect_ratio": aspect,
429
  "sound": False,
430
  }
431
+ if image_url and not image_url.startswith("data:"):
432
+ # Only pass public URLs, not data URIs (happyhorse.app may not support data URIs)
433
  payload["image_urls"] = [image_url]
434
 
435
+ generate_url = HAPPYHORSE_PROVIDERS["happyhorse.app"]["generate"]
436
+ print(f"[HappyHorse] Submitting to {generate_url}")
437
+ print(f"[HappyHorse] Prompt: {prompt[:100]}...")
438
+
439
+ resp = http_requests.post(generate_url, json=payload, headers=headers, timeout=60)
440
+ print(f"[HappyHorse] Submit response: {resp.status_code}")
441
 
442
+ if resp.status_code != 200:
443
+ resp_text = resp.text[:500]
444
+ print(f"[HappyHorse] Error: {resp_text}")
445
+ raise RuntimeError(f"happyhorse.app submit failed ({resp.status_code}): {resp_text}")
446
+
447
+ resp_data = resp.json()
448
+ print(f"[HappyHorse] Response data: {json.dumps(resp_data)[:300]}")
449
+
450
+ # Handle different response structures
451
+ task_id = (
452
+ resp_data.get("data", {}).get("task_id")
453
+ or resp_data.get("task_id")
454
+ or resp_data.get("id")
455
+ )
456
  if not task_id:
457
+ raise RuntimeError(f"No task_id in response: {json.dumps(resp_data)[:300]}")
458
+
459
+ print(f"[HappyHorse] Task ID: {task_id}")
460
 
461
+ # Poll for completion
462
  status_url = HAPPYHORSE_PROVIDERS["happyhorse.app"]["status"]
463
+ for attempt in range(90): # ~15 minutes max
464
  time.sleep(10)
465
+ try:
466
+ s = http_requests.get(
467
+ f"{status_url}?task_id={task_id}",
468
+ headers=headers, timeout=30,
469
+ ).json()
470
+ except Exception as e:
471
+ print(f"[HappyHorse] Poll error (attempt {attempt}): {e}")
472
+ continue
473
+
474
+ # Handle different status response structures
475
+ status_data = s.get("data", s)
476
+ status = (
477
+ status_data.get("status", "")
478
+ or s.get("status", "")
479
+ )
480
+ print(f"[HappyHorse] Poll {attempt}: status={status}")
481
+
482
+ if status in ("SUCCESS", "COMPLETED", "completed", "succeeded"):
483
+ # Try different result URL paths
484
+ urls = (
485
+ status_data.get("response", {}).get("resultUrls", [])
486
+ or status_data.get("resultUrls", [])
487
+ or [status_data.get("video_url")]
488
+ or [status_data.get("output", {}).get("video_url")]
489
+ )
490
+ urls = [u for u in urls if u] # Filter None
491
+ if urls:
492
+ print(f"[HappyHorse] Video URL: {urls[0][:100]}")
493
+ return urls[0]
494
+ raise RuntimeError(f"Status SUCCESS but no video URL found in: {json.dumps(s)[:300]}")
495
+
496
+ elif status in ("FAILED", "failed", "error"):
497
+ error_msg = (
498
+ status_data.get("error_message", "")
499
+ or status_data.get("error", "")
500
+ or "Unknown error"
501
+ )
502
+ raise RuntimeError(f"happyhorse.app failed: {error_msg}")
503
+
504
+ raise RuntimeError("happyhorse.app timeout after 15 minutes")
505
 
506
 
507
  def generate_video_dashscope(prompt, api_key, duration=5, aspect="16:9", image_url=None):
508
  """Generate video via DashScope Bailian (async task API)."""
509
+ headers = {
510
+ "Authorization": f"Bearer {api_key}",
511
+ "Content-Type": "application/json",
512
+ "X-DashScope-Async": "enable",
513
+ }
514
  payload = {
515
  "model": "happyhorse-1.0",
516
  "input": {"prompt": prompt},
517
  "parameters": {"duration": duration, "aspect_ratio": aspect},
518
  }
519
+ if image_url and not image_url.startswith("data:"):
520
  payload["input"]["image_url"] = image_url
521
 
522
+ generate_url = HAPPYHORSE_PROVIDERS["DashScope (Bailian)"]["generate"]
523
+ print(f"[DashScope] Submitting to {generate_url}")
524
+ print(f"[DashScope] Prompt: {prompt[:100]}...")
525
+
526
+ resp = http_requests.post(generate_url, json=payload, headers=headers, timeout=60)
527
+ print(f"[DashScope] Submit response: {resp.status_code}")
528
+
529
  if resp.status_code != 200:
530
+ resp_text = resp.text[:500]
531
+ print(f"[DashScope] Error: {resp_text}")
532
+ raise RuntimeError(f"DashScope submit failed ({resp.status_code}): {resp_text}")
533
+
534
+ resp_data = resp.json()
535
+ print(f"[DashScope] Response: {json.dumps(resp_data)[:300]}")
536
 
537
+ task_id = resp_data.get("output", {}).get("task_id")
538
  if not task_id:
539
+ raise RuntimeError(f"No task_id from DashScope: {json.dumps(resp_data)[:300]}")
540
 
541
+ print(f"[DashScope] Task ID: {task_id}")
542
+
543
+ for attempt in range(90):
544
  time.sleep(10)
545
+ try:
546
+ s = http_requests.get(
547
+ f"{HAPPYHORSE_PROVIDERS['DashScope (Bailian)']['status']}/{task_id}",
548
+ headers={"Authorization": f"Bearer {api_key}"}, timeout=30,
549
+ ).json()
550
+ except Exception as e:
551
+ print(f"[DashScope] Poll error (attempt {attempt}): {e}")
552
+ continue
553
+
554
  status = s.get("output", {}).get("task_status", "")
555
+ print(f"[DashScope] Poll {attempt}: status={status}")
556
+
557
  if status == "SUCCEEDED":
558
  results = s.get("output", {}).get("results", [])
559
+ if results:
560
+ video_url = results[0].get("url")
561
+ print(f"[DashScope] Video URL: {video_url[:100] if video_url else 'None'}")
562
+ return video_url
563
+ raise RuntimeError(f"SUCCEEDED but no results: {json.dumps(s)[:300]}")
564
  elif status == "FAILED":
565
+ msg = s.get("output", {}).get("message", "Unknown error")
566
+ raise RuntimeError(f"DashScope failed: {msg}")
567
+
568
+ raise RuntimeError("DashScope timeout after 15 minutes")
569
 
570
 
571
  def generate_video(prompt, provider, api_key, duration=5, aspect="16:9", image_url=None):
 
738
 
739
  # --- Step 2: Generate video ---
740
  progress(base_frac + 0.02, desc=f"Scene {i+1}/{total}: Generating video...")
741
+ video_error = None
742
  try:
743
  video_url = generate_video(
744
  video_prompt, video_provider, video_key,
 
747
  scene_video_path = os.path.join(tmp_dir, f"scene_{i:03d}_video.mp4")
748
  if video_url:
749
  download_video(video_url, scene_video_path)
750
+ # Verify the downloaded file is a valid video
751
+ file_size = os.path.getsize(scene_video_path)
752
+ if file_size < 1000:
753
+ video_error = f"Downloaded file too small ({file_size} bytes) - likely not a video"
754
+ scene_video_path = None
755
  else:
756
+ video_error = "No video URL returned from API"
757
+ scene_video_path = None
758
  except Exception as e:
759
+ video_error = str(e)
760
+ scene_video_path = None
761
+
762
+ if scene_video_path is None:
763
+ # Create a placeholder with text overlay showing the error
764
+ scene_video_path = os.path.join(tmp_dir, f"scene_{i:03d}_placeholder.mp4")
765
+ # Escape special chars for ffmpeg drawtext
766
+ safe_prompt = video_prompt[:80].replace("'", "").replace('"', '').replace(':', ' ')
767
  subprocess.run([
768
  "ffmpeg", "-y", "-f", "lavfi", "-i",
769
+ f"color=c=0x1a1a2e:s=1280x720:d={video_duration}:r=24",
770
+ "-vf", f"drawtext=text='Scene {i+1}':fontsize=48:fontcolor=white:x=(w-text_w)/2:y=(h-text_h)/2-40,"
771
+ f"drawtext=text='{safe_prompt[:60]}':fontsize=20:fontcolor=0xaaaaaa:x=(w-text_w)/2:y=(h-text_h)/2+30",
772
  "-c:v", "libx264", "-preset", "fast",
773
  scene_video_path,
774
  ], capture_output=True, check=True)
775
+ all_transcripts.append(f"**Scene {i+1} VIDEO ERROR:** {video_error}")
776
 
777
  # --- Step 3: Generate narration audio ---
778
  progress(base_frac + 0.04, desc=f"Scene {i+1}/{total}: Narrating...")