Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -18,6 +18,8 @@ install_package("whisper", "openai-whisper")
|
|
| 18 |
install_package("PIL", "Pillow")
|
| 19 |
install_package("numpy")
|
| 20 |
install_package("opencc", "opencc-python-reimplemented")
|
|
|
|
|
|
|
| 21 |
|
| 22 |
# ==========================================
|
| 23 |
# 1. 正常导入库
|
|
@@ -27,9 +29,7 @@ import re
|
|
| 27 |
import time
|
| 28 |
import json
|
| 29 |
import requests
|
| 30 |
-
import zipfile
|
| 31 |
import queue
|
| 32 |
-
import math
|
| 33 |
import numpy as np
|
| 34 |
from datetime import datetime
|
| 35 |
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
@@ -59,9 +59,9 @@ VIDEOS_PER_BATCH = 10
|
|
| 59 |
|
| 60 |
# 4. 系统配置
|
| 61 |
BASE_OUTPUT_DIR = "project_thangka_final_pipeline"
|
| 62 |
-
VIDEO_WORKERS = 150
|
| 63 |
-
LLM_WORKERS = 5
|
| 64 |
-
VIDEO_TIMEOUT_SECONDS = 360 # 🔥
|
| 65 |
|
| 66 |
# 5. 字幕配置
|
| 67 |
FONT_URL = "https://github.com/googlefonts/noto-cjk/raw/main/Sans/OTF/TraditionalChinese/NotoSansCJKtc-Bold.otf"
|
|
@@ -327,10 +327,24 @@ class AudioAgent:
|
|
| 327 |
time.sleep(5)
|
| 328 |
return None
|
| 329 |
|
| 330 |
-
# 🔥 VideoAgent (
|
| 331 |
class VideoAgent:
|
| 332 |
def __init__(self):
|
| 333 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 334 |
|
| 335 |
def worker_task(self, prompt, global_video_idx, log_queue, save_dir):
|
| 336 |
save_path = os.path.join(save_dir, f"clip_{global_video_idx+1:04d}.mp4")
|
|
@@ -340,21 +354,40 @@ class VideoAgent:
|
|
| 340 |
final_prompt = (f"{FORCE_STYLE_PREFIX} {prompt} {FORCE_MOTION_BOOSTER} {FORCE_STYLE_SUFFIX} {FORCE_NEGATIVE_PROMPT}")
|
| 341 |
final_prompt = re.sub(r'\s+', ' ', final_prompt).replace(", ,", ",").replace("..", ".")
|
| 342 |
|
| 343 |
-
#
|
|
|
|
|
|
|
|
|
|
| 344 |
for attempt in range(5):
|
| 345 |
try:
|
| 346 |
-
# 提交任务
|
| 347 |
create_url = f"{MERCHANT_BASE_URL}/v1/videos"
|
|
|
|
|
|
|
| 348 |
form_data = {
|
| 349 |
"model": VEO_MODEL,
|
| 350 |
"prompt": final_prompt,
|
| 351 |
-
"seconds": VIDEO_DURATION_STR,
|
| 352 |
"size": VIDEO_SIZE,
|
| 353 |
-
"watermark": "false"
|
| 354 |
}
|
| 355 |
|
| 356 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 357 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 358 |
if req.status_code != 200:
|
| 359 |
try: err_msg = req.json()
|
| 360 |
except: err_msg = req.text[:100]
|
|
@@ -374,10 +407,9 @@ class VideoAgent:
|
|
| 374 |
# 轮询查询状态
|
| 375 |
start_time = time.time()
|
| 376 |
while True:
|
| 377 |
-
# 🔥 核心修改1:严格的 6分钟超时
|
| 378 |
if time.time() - start_time > VIDEO_TIMEOUT_SECONDS:
|
| 379 |
log_queue.put(f"⚠️ [超时] 视频{global_video_idx+1} 耗时超过6分钟,视为失败,正在重试...")
|
| 380 |
-
break
|
| 381 |
|
| 382 |
query_url = f"{MERCHANT_BASE_URL}/v1/videos/{task_id}"
|
| 383 |
status_resp = requests.get(query_url, headers=self.headers, timeout=30)
|
|
@@ -396,7 +428,6 @@ class VideoAgent:
|
|
| 396 |
else:
|
| 397 |
break
|
| 398 |
elif status == "failed":
|
| 399 |
-
# 捕获敏感词错误
|
| 400 |
log_queue.put(f"❌ [生成失败] 视频{global_video_idx+1}: {status_data}")
|
| 401 |
break
|
| 402 |
time.sleep(2)
|
|
@@ -549,7 +580,7 @@ def process_pipeline(full_text, audio_file, gen_system_prompt):
|
|
| 549 |
if new_msg: logs.append(f"[{datetime.now().strftime('%H:%M:%S')}] {new_msg}")
|
| 550 |
return "\n".join(logs[-100:])
|
| 551 |
|
| 552 |
-
yield update_ui("🚀 系统启动 (Async Veo API 版)..."), None
|
| 553 |
download_font_if_missing()
|
| 554 |
|
| 555 |
raw_segments = strict_text_splitter(full_text)
|
|
@@ -676,8 +707,8 @@ def process_pipeline(full_text, audio_file, gen_system_prompt):
|
|
| 676 |
# ================= UI 界面 =================
|
| 677 |
|
| 678 |
with gr.Blocks(title="Veo Pipeline Pro") as demo:
|
| 679 |
-
gr.Markdown("## 🎬 Veo 全自动音画工厂 (
|
| 680 |
-
gr.Markdown("🌟 **特性**:
|
| 681 |
with gr.Row():
|
| 682 |
with gr.Column(scale=3):
|
| 683 |
in_text = gr.Textbox(label="1. 输入长文案", lines=8)
|
|
|
|
| 18 |
install_package("PIL", "Pillow")
|
| 19 |
install_package("numpy")
|
| 20 |
install_package("opencc", "opencc-python-reimplemented")
|
| 21 |
+
install_package("gradio")
|
| 22 |
+
install_package("requests")
|
| 23 |
|
| 24 |
# ==========================================
|
| 25 |
# 1. 正常导入库
|
|
|
|
| 29 |
import time
|
| 30 |
import json
|
| 31 |
import requests
|
|
|
|
| 32 |
import queue
|
|
|
|
| 33 |
import numpy as np
|
| 34 |
from datetime import datetime
|
| 35 |
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
|
|
| 59 |
|
| 60 |
# 4. 系统配置
|
| 61 |
BASE_OUTPUT_DIR = "project_thangka_final_pipeline"
|
| 62 |
+
VIDEO_WORKERS = 150
|
| 63 |
+
LLM_WORKERS = 5
|
| 64 |
+
VIDEO_TIMEOUT_SECONDS = 360 # 🔥 单个视频超时设定为 6分钟
|
| 65 |
|
| 66 |
# 5. 字幕配置
|
| 67 |
FONT_URL = "https://github.com/googlefonts/noto-cjk/raw/main/Sans/OTF/TraditionalChinese/NotoSansCJKtc-Bold.otf"
|
|
|
|
| 327 |
time.sleep(5)
|
| 328 |
return None
|
| 329 |
|
| 330 |
+
# 🔥 VideoAgent (API格式修正版:Multipart/form-data)
|
| 331 |
class VideoAgent:
|
| 332 |
def __init__(self):
|
| 333 |
+
# 1. 修正:只保留鉴权,requests会自动处理 multipart boundary
|
| 334 |
+
self.headers = {
|
| 335 |
+
"Authorization": f"Bearer {YUNWU_API_KEY}"
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
def ensure_dummy_image(self):
|
| 339 |
+
# 创建一个黑色占位图,用于满足 API 的 input_reference 必填项
|
| 340 |
+
dummy_path = "_temp_blank_ref.png"
|
| 341 |
+
if not os.path.exists(dummy_path):
|
| 342 |
+
try:
|
| 343 |
+
img = Image.new('RGB', (720, 1280), (0, 0, 0))
|
| 344 |
+
img.save(dummy_path)
|
| 345 |
+
except Exception:
|
| 346 |
+
pass
|
| 347 |
+
return dummy_path
|
| 348 |
|
| 349 |
def worker_task(self, prompt, global_video_idx, log_queue, save_dir):
|
| 350 |
save_path = os.path.join(save_dir, f"clip_{global_video_idx+1:04d}.mp4")
|
|
|
|
| 354 |
final_prompt = (f"{FORCE_STYLE_PREFIX} {prompt} {FORCE_MOTION_BOOSTER} {FORCE_STYLE_SUFFIX} {FORCE_NEGATIVE_PROMPT}")
|
| 355 |
final_prompt = re.sub(r'\s+', ' ', final_prompt).replace(", ,", ",").replace("..", ".")
|
| 356 |
|
| 357 |
+
# 准备占位图
|
| 358 |
+
dummy_ref_path = self.ensure_dummy_image()
|
| 359 |
+
|
| 360 |
+
# 🔥 重试逻辑
|
| 361 |
for attempt in range(5):
|
| 362 |
try:
|
|
|
|
| 363 |
create_url = f"{MERCHANT_BASE_URL}/v1/videos"
|
| 364 |
+
|
| 365 |
+
# 2. 修正:所有字段必须是字符串 (data 部分)
|
| 366 |
form_data = {
|
| 367 |
"model": VEO_MODEL,
|
| 368 |
"prompt": final_prompt,
|
| 369 |
+
"seconds": str(VIDEO_DURATION_STR),
|
| 370 |
"size": VIDEO_SIZE,
|
| 371 |
+
"watermark": "false" # API要求 string 'false'
|
| 372 |
}
|
| 373 |
|
| 374 |
+
# 3. 修正:文件部分 (input_reference 必填)
|
| 375 |
+
# 使用 files 参数会自动触发 multipart/form-data
|
| 376 |
+
files_payload = {}
|
| 377 |
+
if os.path.exists(dummy_ref_path):
|
| 378 |
+
files_payload["input_reference"] = (
|
| 379 |
+
os.path.basename(dummy_ref_path),
|
| 380 |
+
open(dummy_ref_path, "rb"),
|
| 381 |
+
"image/png"
|
| 382 |
+
)
|
| 383 |
+
|
| 384 |
+
# 发送请求
|
| 385 |
+
req = requests.post(create_url, headers=self.headers, data=form_data, files=files_payload, timeout=60)
|
| 386 |
|
| 387 |
+
# 手动关闭文件句柄(如果打开了的话,requests通常会自动处理,但为安全起见)
|
| 388 |
+
if "input_reference" in files_payload:
|
| 389 |
+
files_payload["input_reference"][1].close()
|
| 390 |
+
|
| 391 |
if req.status_code != 200:
|
| 392 |
try: err_msg = req.json()
|
| 393 |
except: err_msg = req.text[:100]
|
|
|
|
| 407 |
# 轮询查询状态
|
| 408 |
start_time = time.time()
|
| 409 |
while True:
|
|
|
|
| 410 |
if time.time() - start_time > VIDEO_TIMEOUT_SECONDS:
|
| 411 |
log_queue.put(f"⚠️ [超时] 视频{global_video_idx+1} 耗时超过6分钟,视为失败,正在重试...")
|
| 412 |
+
break
|
| 413 |
|
| 414 |
query_url = f"{MERCHANT_BASE_URL}/v1/videos/{task_id}"
|
| 415 |
status_resp = requests.get(query_url, headers=self.headers, timeout=30)
|
|
|
|
| 428 |
else:
|
| 429 |
break
|
| 430 |
elif status == "failed":
|
|
|
|
| 431 |
log_queue.put(f"❌ [生成失败] 视频{global_video_idx+1}: {status_data}")
|
| 432 |
break
|
| 433 |
time.sleep(2)
|
|
|
|
| 580 |
if new_msg: logs.append(f"[{datetime.now().strftime('%H:%M:%S')}] {new_msg}")
|
| 581 |
return "\n".join(logs[-100:])
|
| 582 |
|
| 583 |
+
yield update_ui("🚀 系统启动 (Async Veo API V3.2 Fix版)..."), None
|
| 584 |
download_font_if_missing()
|
| 585 |
|
| 586 |
raw_segments = strict_text_splitter(full_text)
|
|
|
|
| 707 |
# ================= UI 界面 =================
|
| 708 |
|
| 709 |
with gr.Blocks(title="Veo Pipeline Pro") as demo:
|
| 710 |
+
gr.Markdown("## 🎬 Veo 全自动音画工厂 (Multipart 修正版)")
|
| 711 |
+
gr.Markdown("🌟 **特性**:自动生成垫图 | 修复API格式 | 实时渲染")
|
| 712 |
with gr.Row():
|
| 713 |
with gr.Column(scale=3):
|
| 714 |
in_text = gr.Textbox(label="1. 输入长文案", lines=8)
|