PlotweaverModel commited on
Commit
6df8e6a
·
verified ·
1 Parent(s): 89c7c75

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +127 -18
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
- - Three HappyHorse API providers: fal.ai, happyhorse.app, DashScope
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
- mode = "text-to-video"
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": prompt,
431
- "mode": mode,
432
- "duration": duration,
433
  "aspect_ratio": aspect,
434
  "sound": False,
435
  }
436
- if mode == "image-to-video":
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
- payload = {
519
- "model": "happyhorse-1.0",
520
- "input": {"prompt": prompt},
521
- "parameters": {"duration": duration, "aspect_ratio": aspect},
522
- }
523
  if image_url and not image_url.startswith("data:"):
524
- payload["input"]["image_url"] = image_url
 
 
 
 
 
 
 
 
 
 
 
 
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="fal.ai",
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"))