Spaces:
Running
Running
updated app
Browse files
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 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
|
| 441 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 442 |
if not task_id:
|
| 443 |
-
raise RuntimeError("No task_id
|
|
|
|
|
|
|
| 444 |
|
|
|
|
| 445 |
status_url = HAPPYHORSE_PROVIDERS["happyhorse.app"]["status"]
|
| 446 |
-
for
|
| 447 |
time.sleep(10)
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 = {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
|
|
|
|
|
|
|
|
|
| 473 |
if resp.status_code != 200:
|
| 474 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 475 |
|
| 476 |
-
task_id =
|
| 477 |
if not task_id:
|
| 478 |
-
raise RuntimeError("No task_id
|
| 479 |
|
| 480 |
-
|
|
|
|
|
|
|
| 481 |
time.sleep(10)
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 486 |
status = s.get("output", {}).get("task_status", "")
|
|
|
|
|
|
|
| 487 |
if status == "SUCCEEDED":
|
| 488 |
results = s.get("output", {}).get("results", [])
|
| 489 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 490 |
elif status == "FAILED":
|
| 491 |
-
|
| 492 |
-
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 675 |
except Exception as e:
|
| 676 |
-
|
| 677 |
-
scene_video_path =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 678 |
subprocess.run([
|
| 679 |
"ffmpeg", "-y", "-f", "lavfi", "-i",
|
| 680 |
-
f"color=c=
|
|
|
|
|
|
|
| 681 |
"-c:v", "libx264", "-preset", "fast",
|
| 682 |
scene_video_path,
|
| 683 |
], capture_output=True, check=True)
|
| 684 |
-
all_transcripts.append(f"Scene {i+1}
|
| 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...")
|