Spaces:
Running
Running
Upload app.py
Browse files
app.py
CHANGED
|
@@ -7,7 +7,7 @@ Features:
|
|
| 7 |
- Translates + narrates into 36 languages (preset or cloned voice)
|
| 8 |
- Generates AI video per scene via HappyHorse 1.0
|
| 9 |
- Three video modes: text-to-video, image-to-video, auto scene prompts
|
| 10 |
-
-
|
| 11 |
- Combines narrated audio + video scenes into final MP4
|
| 12 |
|
| 13 |
Deploy as a Hugging Face Space:
|
|
@@ -417,23 +417,37 @@ def narrate_scene_cloned(client, text, voice_id, language, lang_config, translat
|
|
| 417 |
# ==========================================
|
| 418 |
# HAPPYHORSE VIDEO GENERATION
|
| 419 |
# ==========================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 420 |
def generate_video_happyhorse_app(prompt, api_key, duration=5, aspect="16:9", image_url=None):
|
| 421 |
"""Generate video via happyhorse.app."""
|
| 422 |
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
|
| 423 |
|
| 424 |
-
|
| 425 |
-
if image_url and not image_url.startswith("data:"):
|
| 426 |
-
mode = "image-to-video"
|
| 427 |
|
| 428 |
payload = {
|
| 429 |
"model": "happyhorse-1.0/video",
|
| 430 |
-
"prompt":
|
| 431 |
-
"mode":
|
| 432 |
-
"duration": duration,
|
| 433 |
"aspect_ratio": aspect,
|
| 434 |
"sound": False,
|
| 435 |
}
|
| 436 |
-
if
|
| 437 |
payload["image_urls"] = [image_url]
|
| 438 |
|
| 439 |
generate_url = HAPPYHORSE_PROVIDERS["happyhorse.app"]["generate"]
|
|
@@ -509,19 +523,32 @@ def generate_video_happyhorse_app(prompt, api_key, duration=5, aspect="16:9", im
|
|
| 509 |
|
| 510 |
|
| 511 |
def generate_video_dashscope(prompt, api_key, duration=5, aspect="16:9", image_url=None):
|
| 512 |
-
"""Generate video via DashScope Bailian (async task API).
|
|
|
|
|
|
|
| 513 |
headers = {
|
| 514 |
"Authorization": f"Bearer {api_key}",
|
| 515 |
"Content-Type": "application/json",
|
| 516 |
"X-DashScope-Async": "enable",
|
| 517 |
}
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
}
|
| 523 |
if image_url and not image_url.startswith("data:"):
|
| 524 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 525 |
|
| 526 |
generate_url = HAPPYHORSE_PROVIDERS["DashScope (Bailian)"]["generate"]
|
| 527 |
print(f"[DashScope] Submitting to {generate_url}")
|
|
@@ -930,7 +957,6 @@ def generate_wrapper(text_input, file_input, lang, voice_mode, preset_voice,
|
|
| 930 |
|
| 931 |
with gr.Blocks(
|
| 932 |
title="Visual Storybook Generator",
|
| 933 |
-
theme=gr.themes.Soft(primary_hue="amber", secondary_hue="orange", neutral_hue="stone"),
|
| 934 |
) as demo:
|
| 935 |
|
| 936 |
gr.Markdown(DESCRIPTION)
|
|
@@ -956,7 +982,7 @@ with gr.Blocks(
|
|
| 956 |
with gr.Tab("Video"):
|
| 957 |
video_provider = gr.Dropdown(
|
| 958 |
choices=list(HAPPYHORSE_PROVIDERS.keys()),
|
| 959 |
-
value="
|
| 960 |
label="HappyHorse API Provider",
|
| 961 |
info="Each provider needs its own API key in Secrets",
|
| 962 |
)
|
|
@@ -980,6 +1006,7 @@ with gr.Blocks(
|
|
| 980 |
video_duration = gr.Slider(minimum=3, maximum=10, value=5, step=1, label="Video Duration (sec/scene)")
|
| 981 |
|
| 982 |
generate_btn = gr.Button("Generate Visual Storybook", variant="primary", size="lg")
|
|
|
|
| 983 |
|
| 984 |
# -- RIGHT: Output --
|
| 985 |
with gr.Column(scale=1):
|
|
@@ -987,12 +1014,94 @@ with gr.Blocks(
|
|
| 987 |
stats_output = gr.Markdown(label="Stats")
|
| 988 |
with gr.Accordion("Scene Transcripts", open=False):
|
| 989 |
transcript_output = gr.Markdown()
|
|
|
|
| 990 |
|
| 991 |
# Events
|
| 992 |
sample_btn.click(fn=lambda: SAMPLE_TEXT, outputs=text_input)
|
| 993 |
voice_mode.change(fn=on_voice_mode_change, inputs=voice_mode, outputs=[preset_voice, clone_audio])
|
| 994 |
video_mode.change(fn=on_video_mode_change, inputs=video_mode, outputs=[scene_images])
|
| 995 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 996 |
generate_btn.click(
|
| 997 |
fn=generate_wrapper,
|
| 998 |
inputs=[text_input, file_input, target_lang, voice_mode, preset_voice,
|
|
@@ -1012,4 +1121,4 @@ with gr.Blocks(
|
|
| 1012 |
)
|
| 1013 |
|
| 1014 |
if __name__ == "__main__":
|
| 1015 |
-
demo.launch()
|
|
|
|
| 7 |
- Translates + narrates into 36 languages (preset or cloned voice)
|
| 8 |
- Generates AI video per scene via HappyHorse 1.0
|
| 9 |
- Three video modes: text-to-video, image-to-video, auto scene prompts
|
| 10 |
+
- Two HappyHorse API providers: happyhorse.app, DashScope (Wan 2.1 fallback)
|
| 11 |
- Combines narrated audio + video scenes into final MP4
|
| 12 |
|
| 13 |
Deploy as a Hugging Face Space:
|
|
|
|
| 417 |
# ==========================================
|
| 418 |
# HAPPYHORSE VIDEO GENERATION
|
| 419 |
# ==========================================
|
| 420 |
+
def sanitize_prompt(prompt, max_length=2000):
|
| 421 |
+
"""Clean and truncate prompt for HappyHorse API."""
|
| 422 |
+
# Replace smart quotes and special chars
|
| 423 |
+
prompt = prompt.replace("\u2018", "'").replace("\u2019", "'")
|
| 424 |
+
prompt = prompt.replace("\u201c", '"').replace("\u201d", '"')
|
| 425 |
+
prompt = prompt.replace("\u2014", " -- ").replace("\u2013", " - ")
|
| 426 |
+
# Remove any control characters
|
| 427 |
+
prompt = re.sub(r'[\x00-\x1f\x7f-\x9f]', ' ', prompt)
|
| 428 |
+
# Collapse multiple spaces
|
| 429 |
+
prompt = re.sub(r'\s+', ' ', prompt).strip()
|
| 430 |
+
# Truncate
|
| 431 |
+
if len(prompt) > max_length:
|
| 432 |
+
prompt = prompt[:max_length].rsplit(' ', 1)[0]
|
| 433 |
+
return prompt
|
| 434 |
+
|
| 435 |
+
|
| 436 |
def generate_video_happyhorse_app(prompt, api_key, duration=5, aspect="16:9", image_url=None):
|
| 437 |
"""Generate video via happyhorse.app."""
|
| 438 |
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
|
| 439 |
|
| 440 |
+
clean_prompt = sanitize_prompt(prompt, max_length=2000)
|
|
|
|
|
|
|
| 441 |
|
| 442 |
payload = {
|
| 443 |
"model": "happyhorse-1.0/video",
|
| 444 |
+
"prompt": clean_prompt,
|
| 445 |
+
"mode": "pro",
|
| 446 |
+
"duration": int(duration),
|
| 447 |
"aspect_ratio": aspect,
|
| 448 |
"sound": False,
|
| 449 |
}
|
| 450 |
+
if image_url and not image_url.startswith("data:"):
|
| 451 |
payload["image_urls"] = [image_url]
|
| 452 |
|
| 453 |
generate_url = HAPPYHORSE_PROVIDERS["happyhorse.app"]["generate"]
|
|
|
|
| 523 |
|
| 524 |
|
| 525 |
def generate_video_dashscope(prompt, api_key, duration=5, aspect="16:9", image_url=None):
|
| 526 |
+
"""Generate video via DashScope Bailian (async task API).
|
| 527 |
+
Uses wan2.1-t2v-plus since happyhorse-1.0 is not yet available on DashScope.
|
| 528 |
+
"""
|
| 529 |
headers = {
|
| 530 |
"Authorization": f"Bearer {api_key}",
|
| 531 |
"Content-Type": "application/json",
|
| 532 |
"X-DashScope-Async": "enable",
|
| 533 |
}
|
| 534 |
+
|
| 535 |
+
clean_prompt = sanitize_prompt(prompt, max_length=2000)
|
| 536 |
+
|
| 537 |
+
# Use Wan 2.1 T2V Plus (available on DashScope) as HappyHorse is not live yet
|
|
|
|
| 538 |
if image_url and not image_url.startswith("data:"):
|
| 539 |
+
model = "wan2.1-i2v-plus"
|
| 540 |
+
payload = {
|
| 541 |
+
"model": model,
|
| 542 |
+
"input": {"prompt": clean_prompt, "img_url": image_url},
|
| 543 |
+
"parameters": {"resolution": "1280*720"},
|
| 544 |
+
}
|
| 545 |
+
else:
|
| 546 |
+
model = "wan2.1-t2v-plus"
|
| 547 |
+
payload = {
|
| 548 |
+
"model": model,
|
| 549 |
+
"input": {"prompt": clean_prompt},
|
| 550 |
+
"parameters": {"size": "1280*720"},
|
| 551 |
+
}
|
| 552 |
|
| 553 |
generate_url = HAPPYHORSE_PROVIDERS["DashScope (Bailian)"]["generate"]
|
| 554 |
print(f"[DashScope] Submitting to {generate_url}")
|
|
|
|
| 957 |
|
| 958 |
with gr.Blocks(
|
| 959 |
title="Visual Storybook Generator",
|
|
|
|
| 960 |
) as demo:
|
| 961 |
|
| 962 |
gr.Markdown(DESCRIPTION)
|
|
|
|
| 982 |
with gr.Tab("Video"):
|
| 983 |
video_provider = gr.Dropdown(
|
| 984 |
choices=list(HAPPYHORSE_PROVIDERS.keys()),
|
| 985 |
+
value="happyhorse.app",
|
| 986 |
label="HappyHorse API Provider",
|
| 987 |
info="Each provider needs its own API key in Secrets",
|
| 988 |
)
|
|
|
|
| 1006 |
video_duration = gr.Slider(minimum=3, maximum=10, value=5, step=1, label="Video Duration (sec/scene)")
|
| 1007 |
|
| 1008 |
generate_btn = gr.Button("Generate Visual Storybook", variant="primary", size="lg")
|
| 1009 |
+
test_btn = gr.Button("Test Video API Connection", variant="secondary", size="sm")
|
| 1010 |
|
| 1011 |
# -- RIGHT: Output --
|
| 1012 |
with gr.Column(scale=1):
|
|
|
|
| 1014 |
stats_output = gr.Markdown(label="Stats")
|
| 1015 |
with gr.Accordion("Scene Transcripts", open=False):
|
| 1016 |
transcript_output = gr.Markdown()
|
| 1017 |
+
test_output = gr.Markdown(label="API Test Result", visible=True)
|
| 1018 |
|
| 1019 |
# Events
|
| 1020 |
sample_btn.click(fn=lambda: SAMPLE_TEXT, outputs=text_input)
|
| 1021 |
voice_mode.change(fn=on_voice_mode_change, inputs=voice_mode, outputs=[preset_voice, clone_audio])
|
| 1022 |
video_mode.change(fn=on_video_mode_change, inputs=video_mode, outputs=[scene_images])
|
| 1023 |
|
| 1024 |
+
def test_video_api(provider):
|
| 1025 |
+
"""Test the video API connection with a simple request."""
|
| 1026 |
+
config = HAPPYHORSE_PROVIDERS[provider]
|
| 1027 |
+
api_key = os.environ.get(config["key_env"], "")
|
| 1028 |
+
|
| 1029 |
+
if not api_key:
|
| 1030 |
+
return f"**FAILED:** `{config['key_env']}` is not set in Secrets."
|
| 1031 |
+
|
| 1032 |
+
results = []
|
| 1033 |
+
results.append(f"**Provider:** {provider}")
|
| 1034 |
+
results.append(f"**API Key:** `{api_key[:8]}...{api_key[-4:]}`")
|
| 1035 |
+
|
| 1036 |
+
# Step 1: Submit a short test generation
|
| 1037 |
+
if provider == "happyhorse.app":
|
| 1038 |
+
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
|
| 1039 |
+
payload = {
|
| 1040 |
+
"model": "happyhorse-1.0/video",
|
| 1041 |
+
"prompt": "A golden sunset over calm ocean waves",
|
| 1042 |
+
"mode": "std",
|
| 1043 |
+
"duration": 3,
|
| 1044 |
+
"aspect_ratio": "16:9",
|
| 1045 |
+
"sound": False,
|
| 1046 |
+
}
|
| 1047 |
+
url = config["generate"]
|
| 1048 |
+
else:
|
| 1049 |
+
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json", "X-DashScope-Async": "enable"}
|
| 1050 |
+
payload = {
|
| 1051 |
+
"model": "wan2.1-t2v-plus",
|
| 1052 |
+
"input": {"prompt": "A golden sunset over calm ocean waves"},
|
| 1053 |
+
"parameters": {"size": "1280*720"},
|
| 1054 |
+
}
|
| 1055 |
+
url = config["generate"]
|
| 1056 |
+
|
| 1057 |
+
results.append(f"**URL:** `{url}`")
|
| 1058 |
+
results.append(f"**Payload:** `{json.dumps(payload)}`")
|
| 1059 |
+
|
| 1060 |
+
try:
|
| 1061 |
+
resp = http_requests.post(url, json=payload, headers=headers, timeout=30)
|
| 1062 |
+
results.append(f"**HTTP Status:** {resp.status_code}")
|
| 1063 |
+
results.append(f"**Response:** ```{resp.text[:500]}```")
|
| 1064 |
+
|
| 1065 |
+
if resp.status_code == 200:
|
| 1066 |
+
data = resp.json()
|
| 1067 |
+
task_id = (
|
| 1068 |
+
data.get("data", {}).get("task_id")
|
| 1069 |
+
or data.get("task_id")
|
| 1070 |
+
or data.get("output", {}).get("task_id")
|
| 1071 |
+
)
|
| 1072 |
+
if task_id:
|
| 1073 |
+
results.append(f"**Task ID:** `{task_id}` -- API is working! Task submitted successfully.")
|
| 1074 |
+
|
| 1075 |
+
# Quick poll once
|
| 1076 |
+
time.sleep(5)
|
| 1077 |
+
if provider == "happyhorse.app":
|
| 1078 |
+
s_resp = http_requests.get(
|
| 1079 |
+
f"{config['status']}?task_id={task_id}",
|
| 1080 |
+
headers=headers, timeout=15,
|
| 1081 |
+
)
|
| 1082 |
+
else:
|
| 1083 |
+
s_resp = http_requests.get(
|
| 1084 |
+
f"{config['status']}/{task_id}",
|
| 1085 |
+
headers={"Authorization": f"Bearer {api_key}"}, timeout=15,
|
| 1086 |
+
)
|
| 1087 |
+
results.append(f"**Status poll:** {s_resp.status_code}")
|
| 1088 |
+
results.append(f"**Status body:** ```{s_resp.text[:500]}```")
|
| 1089 |
+
else:
|
| 1090 |
+
results.append(f"**WARNING:** 200 OK but no task_id found in response")
|
| 1091 |
+
elif resp.status_code == 401:
|
| 1092 |
+
results.append("**ERROR:** Invalid API key (401 Unauthorized)")
|
| 1093 |
+
elif resp.status_code == 402:
|
| 1094 |
+
results.append("**ERROR:** Insufficient credits (402 Payment Required)")
|
| 1095 |
+
else:
|
| 1096 |
+
results.append(f"**ERROR:** Unexpected status {resp.status_code}")
|
| 1097 |
+
|
| 1098 |
+
except Exception as e:
|
| 1099 |
+
results.append(f"**EXCEPTION:** {str(e)}")
|
| 1100 |
+
|
| 1101 |
+
return "\n\n".join(results)
|
| 1102 |
+
|
| 1103 |
+
test_btn.click(fn=test_video_api, inputs=[video_provider], outputs=[test_output])
|
| 1104 |
+
|
| 1105 |
generate_btn.click(
|
| 1106 |
fn=generate_wrapper,
|
| 1107 |
inputs=[text_input, file_input, target_lang, voice_mode, preset_voice,
|
|
|
|
| 1121 |
)
|
| 1122 |
|
| 1123 |
if __name__ == "__main__":
|
| 1124 |
+
demo.launch(theme=gr.themes.Soft(primary_hue="amber", secondary_hue="orange", neutral_hue="stone"))
|