194130157a commited on
Commit
5196360
·
verified ·
1 Parent(s): 8ee6fce

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +97 -116
app.py CHANGED
@@ -24,16 +24,16 @@ YUNWU_API_KEY = "sk-Vhxjwm4XXu5fKrAtRNbRZGdPbocDZjG7B9UsSUjAdOQLyMUA"
24
  # 2. Sora 专用 API Key
25
  SORA_API_KEY = "sk-heZhMAAncKvJybPfhfx6rbj6ek0CoImJxrGPeRaXqSRpQR2t"
26
 
27
- # 模型选项
 
28
  MODEL_OPTIONS = ["sora-2-all", "veo_3_1-fast"]
29
- TEXT_MODEL = "gemini-3-pro-preview-thinking" # 用于商品分析
30
 
31
  # 输出目录
32
- OUTPUT_DIR = "Ecommerce_Batch_Output"
33
 
34
  # 并发配置
35
- MAX_WORKERS = 10 # 线程池大小
36
- VIDEO_WORKERS = 2 # 视频生成并发数 (建议保守设置)
37
 
38
  # ================= 提示词模版 (竖屏电商专用) =================
39
 
@@ -85,7 +85,6 @@ def image_to_base64(image_path):
85
  def image_to_data_uri(image_path):
86
  if not image_path: return None
87
  b64 = image_to_base64(image_path)
88
- # 根据文件扩展名判断 mime type,默认 png
89
  return f"data:image/png;base64,{b64}"
90
 
91
  def download_file(url):
@@ -114,13 +113,13 @@ def clear_output_dir():
114
  if os.path.exists(OUTPUT_DIR): shutil.rmtree(OUTPUT_DIR)
115
  os.makedirs(OUTPUT_DIR, exist_ok=True)
116
 
117
- # ================= 核心 API 交互类 (双模型支持) =================
118
 
119
  class EcommerceDirector:
120
  def __init__(self, base_url):
121
  self.base_url = base_url
122
 
123
- # Step 1: 分析图片和文本,生成 Prompts
124
  def analyze_and_plan(self, image_path, description, count):
125
  headers = {
126
  "Authorization": f"Bearer {YUNWU_API_KEY}",
@@ -155,15 +154,18 @@ class EcommerceDirector:
155
  print(f"API Error: {e}")
156
  return [f"Showcase video of product {i+1} vertical style" for i in range(count)]
157
 
158
- # Step 2: 视频生成路由器 (Router)
159
- def generate_video(self, model_name, prompt, ref_image_path=None, use_ref=False):
 
 
 
160
  if "sora" in model_name.lower():
161
- return self._generate_sora(model_name, prompt, ref_image_path, use_ref)
162
  else:
163
- return self._generate_veo(model_name, prompt, ref_image_path, use_ref)
164
 
165
- # === VEO 专用逻辑 (Multipart, 竖屏 9x16) ===
166
- def _generate_veo(self, model_name, prompt, ref_image_path, use_ref):
167
  url = f"{self.base_url}/v1/videos"
168
  headers = {"Authorization": f"Bearer {YUNWU_API_KEY}"}
169
 
@@ -173,22 +175,17 @@ class EcommerceDirector:
173
  'model': model_name,
174
  'prompt': prompt,
175
  'seconds': '5',
176
- 'size': '9x16', # 【修改】竖屏
177
  'watermark': 'false'
178
  }
179
- files = None
180
- f_img = None
181
 
182
- if use_ref and ref_image_path:
183
- f_img = open(ref_image_path, 'rb')
184
  files = [('input_reference', (os.path.basename(ref_image_path), f_img, 'image/png'))]
185
-
186
- resp = requests.post(url, headers=headers, data=data, files=files, timeout=120)
187
- if f_img: f_img.close()
188
 
189
  if resp.status_code == 200:
190
  task_id = resp.json().get('id')
191
- return self._poll_result(task_id, headers)
192
 
193
  print(f"[Veo] Submit Fail ({attempt}): {resp.text}")
194
  time.sleep(2)
@@ -197,8 +194,8 @@ class EcommerceDirector:
197
  time.sleep(2)
198
  return None, "Veo Failed"
199
 
200
- # === SORA 专用逻辑 (JSON + Base64, 严格参照文档) ===
201
- def _generate_sora(self, model_name, prompt, ref_image_path, use_ref):
202
  url = f"{self.base_url}/v1/video/create"
203
  headers = {
204
  "Authorization": f"Bearer {SORA_API_KEY}",
@@ -208,31 +205,25 @@ class EcommerceDirector:
208
 
209
  for attempt in range(1, 4):
210
  try:
211
- # 【修改】严格参照 OpenAPI 规范和截图
 
212
  payload = {
213
- "model": model_name, # sora-2-all
214
- "orientation": "portrait", # 【修改】竖屏
215
  "prompt": prompt,
216
  "size": "large",
217
  "duration": 5,
218
- "watermark": False, # 根据示例传递 Boolean
219
- "images": [] # 初始化数组
220
  }
221
-
222
- # Sora 垫图逻辑:Data URI 放入数组
223
- if use_ref and ref_image_path:
224
- data_uri = image_to_data_uri(ref_image_path)
225
- if data_uri:
226
- payload["images"] = [data_uri]
227
 
228
- # 发送 JSON 请求
229
  resp = requests.post(url, headers=headers, json=payload, timeout=120)
230
 
231
  if resp.status_code == 200:
232
  resp_json = resp.json()
233
  task_id = resp_json.get('id')
234
  if task_id:
235
- return self._poll_result(task_id, headers)
236
 
237
  print(f"[Sora] Submit Fail ({attempt}): {resp.text}")
238
  time.sleep(2)
@@ -241,22 +232,39 @@ class EcommerceDirector:
241
  time.sleep(2)
242
  return None, "Sora Failed"
243
 
244
- # 通用轮询逻辑
245
- def _poll_result(self, task_id, headers):
246
- poll_url = f"{self.base_url}/v1/videos/{task_id}"
247
-
248
- for _ in range(60): # 轮询 3 分钟
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
  time.sleep(3)
250
  try:
251
- resp = requests.get(poll_url, headers=headers)
252
  if resp.status_code == 200:
253
  data = resp.json()
254
  status = data.get('status')
 
255
  if status in ['succeeded', 'success', 'completed']:
256
- url = self._deep_find_url(data)
257
- if url: return url, "OK"
 
258
  elif status == 'failed':
259
- return None, f"Remote Fail: {data.get('error')}"
260
  except: pass
261
  return None, "Timeout"
262
 
@@ -282,55 +290,48 @@ director = EcommerceDirector(BASE_URL)
282
 
283
  def run_analysis_step(image, desc, count):
284
  if not image and not desc:
285
- return "⚠️ 请至少上传图片或填写描述", None, gr.update(visible=False)
286
 
287
  logger.log(f"🕵️ Analyzing Product... Target: {count} videos")
288
  prompts = director.analyze_and_plan(image, desc, count)
 
 
289
  df_data = [[i+1, p] for i, p in enumerate(prompts)]
290
 
291
- logger.log(f"✅ Analysis Done. Generated {len(prompts)} prompts.")
292
  return logger.log("Ready to generate."), df_data, gr.update(visible=True)
293
 
294
- def run_generation_step(image, prompt_data, mode_str, model_name):
295
- # 【修改】修复 DataFrame 真值判断错误
296
- # prompt_data 可能是 None, List, 或 Pandas DataFrame
297
  data_list = []
298
-
299
  if prompt_data is None:
300
- return "⚠️ 无有效提示词", None, "Failed"
301
-
302
  if isinstance(prompt_data, list):
303
  data_list = prompt_data
304
- elif hasattr(prompt_data, 'values'): # 判断是否为 DataFrame
305
- if prompt_data.empty:
306
- return "⚠️ 提示词列表为空", None, "Failed"
307
  data_list = prompt_data.values.tolist()
308
 
309
- if len(data_list) == 0:
310
- return "⚠️ 提示词列表为空", None, "Failed"
 
 
311
 
312
  clear_output_dir()
313
- logger.log(f"🎬 Batch Start. Model: {model_name} | Mode: {mode_str} | Count: {len(data_list)}")
314
-
315
- use_ref = ("Image" in mode_str)
316
- img_path = image if image else None
317
 
318
- if use_ref and not img_path:
319
- return logger.log("⚠️ Error: Image mode selected but no image uploaded."), None, "Error"
320
-
321
  futures = []
322
  video_executor = concurrent.futures.ThreadPoolExecutor(max_workers=VIDEO_WORKERS)
323
 
324
- # 提交任务
325
  for row in data_list:
326
  idx = row[0]
327
  prompt = row[1]
328
  logger.log(f"➕ Queueing Video {idx} ({model_name})...")
329
- futures.append(video_executor.submit(process_single_video, idx, prompt, img_path, use_ref, model_name))
330
 
331
- # 等待结果
332
  completed = 0
333
  total = len(futures)
 
334
  for f in concurrent.futures.as_completed(futures):
335
  idx, status = f.result()
336
  if status == "OK":
@@ -342,11 +343,12 @@ def run_generation_step(image, prompt_data, mode_str, model_name):
342
  video_executor.shutdown(wait=True)
343
  logger.log("📦 Zipping videos...")
344
  zip_path = create_zip(OUTPUT_DIR, "Ecommerce_Videos")
345
- return logger.log("🎉 All Done! Ready to download."), zip_path, f"Completed {completed}/{total}"
 
346
 
347
- def process_single_video(idx, prompt, img_path, use_ref, model_name):
348
  try:
349
- url, msg = director.generate_video(model_name, prompt, img_path, use_ref)
350
  if url:
351
  vid_bytes = download_file(url)
352
  if vid_bytes:
@@ -358,67 +360,46 @@ def process_single_video(idx, prompt, img_path, use_ref, model_name):
358
  print(f"Worker Error: {e}")
359
  return idx, "Fail"
360
 
361
- # ================= UI 界面 =================
362
 
363
- dark_css = """
364
- body, .gradio-container { background-color: #0b0f19 !important; color: #e5e7eb !important; }
365
- .sidebar { background-color: #111827 !important; border-right: 1px solid #374151; padding: 20px; }
366
- .primary-btn { background: linear-gradient(90deg, #3b82f6, #2563eb) !important; border:none; color:white; font-weight:bold; }
367
- .secondary-btn { background-color: #374151 !important; color: white !important; border: 1px solid #4b5563 !important; }
368
- textarea, input { background-color: #1f2937 !important; color: #fff !important; border: 1px solid #374151 !important; }
369
- """
370
-
371
- with gr.Blocks(title="Ecommerce Video Batch Agent", css=dark_css) as demo:
372
 
373
  gr.Markdown("## 🛍️ 电商竖屏视频批量生成 (Sora-2 & Veo)")
 
374
 
375
  with gr.Row():
376
- # 左侧配置
377
- with gr.Column(scale=1, elem_classes="sidebar"):
378
- gr.Markdown("### 1. 商品信息输入")
379
- input_image = gr.Image(label="商品图 (Product Image)", type="filepath", height=250)
380
- input_desc = gr.Textbox(label="商品描述 (Description)", placeholder="输入商品详情页卖点文案...", lines=4)
381
 
382
  gr.Markdown("### 2. 生成配置")
383
- count_slider = gr.Slider(minimum=1, maximum=100, value=5, step=1, label="生成视频数量")
384
-
385
- # 模型选择
386
- model_dropdown = gr.Dropdown(
387
- choices=MODEL_OPTIONS,
388
- value="sora-2-all",
389
- label="选择视频模型 (Model)"
390
- )
391
-
392
- # 模式选择
393
- mode_dropdown = gr.Dropdown(
394
- choices=["Text Only (纯文生)", "Image + Text (垫图生成)"],
395
- value="Image + Text (垫图生成)",
396
- label="生成模式 (Mode)"
397
- )
398
 
399
- analyze_btn = gr.Button("🔍 1. 分析并生成脚本", elem_classes="primary-btn")
400
 
401
- # 右侧操作
402
  with gr.Column(scale=2):
403
- gr.Markdown("### 3. 脚本确认与修改")
404
  prompt_dataframe = gr.Dataframe(
405
  headers=["ID", "Prompt"],
406
  datatype=["number", "str"],
407
  col_count=(2, "fixed"),
408
  interactive=True,
409
- label="生成的视频提示词 (可在生成前修改)",
410
- wrap=True,
411
- value=[[1, "Waiting for analysis..."]]
412
  )
413
 
414
- generate_btn = gr.Button("🎬 2. 开始批量生成", elem_classes="primary-btn", visible=False)
415
 
416
- gr.Markdown("### 4. 运行日志与下载")
417
- log_box = gr.TextArea(label="系统日志", lines=8, interactive=False)
418
- status_box = gr.Textbox(label="最终状态", interactive=False)
419
- download_zip = gr.File(label="📦 下载视频包 (Download ZIP)", interactive=False)
420
 
421
- # 交互逻辑绑定
422
  analyze_btn.click(
423
  fn=run_analysis_step,
424
  inputs=[input_image, input_desc, count_slider],
@@ -427,7 +408,7 @@ with gr.Blocks(title="Ecommerce Video Batch Agent", css=dark_css) as demo:
427
 
428
  generate_btn.click(
429
  fn=run_generation_step,
430
- inputs=[input_image, prompt_dataframe, mode_dropdown, model_dropdown],
431
  outputs=[log_box, download_zip, status_box]
432
  )
433
 
 
24
  # 2. Sora 专用 API Key
25
  SORA_API_KEY = "sk-heZhMAAncKvJybPfhfx6rbj6ek0CoImJxrGPeRaXqSRpQR2t"
26
 
27
+ # 模型配置
28
+ TEXT_MODEL = "gemini-3-pro-preview-thinking"
29
  MODEL_OPTIONS = ["sora-2-all", "veo_3_1-fast"]
 
30
 
31
  # 输出目录
32
+ OUTPUT_DIR = "Ecommerce_Vertical_Output"
33
 
34
  # 并发配置
35
+ MAX_WORKERS = 10
36
+ VIDEO_WORKERS = 2 # 视频生成并发数
37
 
38
  # ================= 提示词模版 (竖屏电商专用) =================
39
 
 
85
  def image_to_data_uri(image_path):
86
  if not image_path: return None
87
  b64 = image_to_base64(image_path)
 
88
  return f"data:image/png;base64,{b64}"
89
 
90
  def download_file(url):
 
113
  if os.path.exists(OUTPUT_DIR): shutil.rmtree(OUTPUT_DIR)
114
  os.makedirs(OUTPUT_DIR, exist_ok=True)
115
 
116
+ # ================= 核心 API 交互类 =================
117
 
118
  class EcommerceDirector:
119
  def __init__(self, base_url):
120
  self.base_url = base_url
121
 
122
+ # Step 1: 分析 (Gemini)
123
  def analyze_and_plan(self, image_path, description, count):
124
  headers = {
125
  "Authorization": f"Bearer {YUNWU_API_KEY}",
 
154
  print(f"API Error: {e}")
155
  return [f"Showcase video of product {i+1} vertical style" for i in range(count)]
156
 
157
+ # Step 2: 路由
158
+ def generate_video(self, model_name, prompt, ref_image_path):
159
+ if not ref_image_path:
160
+ return None, "Error: Reference image is mandatory."
161
+
162
  if "sora" in model_name.lower():
163
+ return self._generate_sora(model_name, prompt, ref_image_path)
164
  else:
165
+ return self._generate_veo(model_name, prompt, ref_image_path)
166
 
167
+ # === VEO 逻辑 (9x16, Multipart) ===
168
+ def _generate_veo(self, model_name, prompt, ref_image_path):
169
  url = f"{self.base_url}/v1/videos"
170
  headers = {"Authorization": f"Bearer {YUNWU_API_KEY}"}
171
 
 
175
  'model': model_name,
176
  'prompt': prompt,
177
  'seconds': '5',
178
+ 'size': '9x16', # 竖屏
179
  'watermark': 'false'
180
  }
 
 
181
 
182
+ with open(ref_image_path, 'rb') as f_img:
 
183
  files = [('input_reference', (os.path.basename(ref_image_path), f_img, 'image/png'))]
184
+ resp = requests.post(url, headers=headers, data=data, files=files, timeout=120)
 
 
185
 
186
  if resp.status_code == 200:
187
  task_id = resp.json().get('id')
188
+ return self._poll_veo(task_id) # Veo 使用标准轮询
189
 
190
  print(f"[Veo] Submit Fail ({attempt}): {resp.text}")
191
  time.sleep(2)
 
194
  time.sleep(2)
195
  return None, "Veo Failed"
196
 
197
+ # === SORA 逻辑 (Portrait, JSON+Base64, 独立 Query) ===
198
+ def _generate_sora(self, model_name, prompt, ref_image_path):
199
  url = f"{self.base_url}/v1/video/create"
200
  headers = {
201
  "Authorization": f"Bearer {SORA_API_KEY}",
 
205
 
206
  for attempt in range(1, 4):
207
  try:
208
+ data_uri = image_to_data_uri(ref_image_path)
209
+
210
  payload = {
211
+ "model": model_name,
212
+ "orientation": "portrait", # 竖屏
213
  "prompt": prompt,
214
  "size": "large",
215
  "duration": 5,
216
+ "watermark": False,
217
+ "images": [data_uri] # 强制垫图
218
  }
 
 
 
 
 
 
219
 
 
220
  resp = requests.post(url, headers=headers, json=payload, timeout=120)
221
 
222
  if resp.status_code == 200:
223
  resp_json = resp.json()
224
  task_id = resp_json.get('id')
225
  if task_id:
226
+ return self._poll_sora(task_id) # Sora 使用特殊轮询
227
 
228
  print(f"[Sora] Submit Fail ({attempt}): {resp.text}")
229
  time.sleep(2)
 
232
  time.sleep(2)
233
  return None, "Sora Failed"
234
 
235
+ # --- Veo 轮询 (标准) ---
236
+ def _poll_veo(self, task_id):
237
+ url = f"{self.base_url}/v1/videos/{task_id}"
238
+ headers = {"Authorization": f"Bearer {YUNWU_API_KEY}"}
239
+ return self._do_poll(url, headers)
240
+
241
+ # --- Sora 轮询 (Query 参数) ---
242
+ def _poll_sora(self, task_id):
243
+ # 按照文档:/v1/video/query?id=task_id
244
+ url = f"{self.base_url}/v1/video/query"
245
+ headers = {
246
+ "Authorization": f"Bearer {SORA_API_KEY}",
247
+ "Accept": "application/json"
248
+ }
249
+ # requests params 会自动拼接 ?id=...
250
+ return self._do_poll(url, headers, params={"id": task_id})
251
+
252
+ # --- 通用轮询器 ---
253
+ def _do_poll(self, url, headers, params=None):
254
+ for _ in range(60): # 3分钟
255
  time.sleep(3)
256
  try:
257
+ resp = requests.get(url, headers=headers, params=params)
258
  if resp.status_code == 200:
259
  data = resp.json()
260
  status = data.get('status')
261
+
262
  if status in ['succeeded', 'success', 'completed']:
263
+ # 深度查找 video_url
264
+ final_url = self._deep_find_url(data)
265
+ if final_url: return final_url, "OK"
266
  elif status == 'failed':
267
+ return None, f"Remote Fail: {data.get('error') or 'Unknown'}"
268
  except: pass
269
  return None, "Timeout"
270
 
 
290
 
291
  def run_analysis_step(image, desc, count):
292
  if not image and not desc:
293
+ return "⚠️ 请上传图片或填写描述", None, gr.update(visible=False)
294
 
295
  logger.log(f"🕵️ Analyzing Product... Target: {count} videos")
296
  prompts = director.analyze_and_plan(image, desc, count)
297
+
298
+ # Dataframe: [ID, Prompt]
299
  df_data = [[i+1, p] for i, p in enumerate(prompts)]
300
 
301
+ logger.log(f"✅ Generated {len(prompts)} prompts.")
302
  return logger.log("Ready to generate."), df_data, gr.update(visible=True)
303
 
304
+ def run_generation_step(image, prompt_data, model_name):
305
+ # 解析 Dataframe
 
306
  data_list = []
 
307
  if prompt_data is None:
308
+ return "⚠️ 无提示词", None, "Failed"
 
309
  if isinstance(prompt_data, list):
310
  data_list = prompt_data
311
+ elif hasattr(prompt_data, 'values'):
312
+ if prompt_data.empty: return "⚠️ 提示词为空", None, "Failed"
 
313
  data_list = prompt_data.values.tolist()
314
 
315
+ if len(data_list) == 0: return "⚠️ 列表为空", None, "Failed"
316
+
317
+ if not image:
318
+ return logger.log("⚠️ Error: 必须提供垫图 (Reference Image)"), None, "Error"
319
 
320
  clear_output_dir()
321
+ logger.log(f"🎬 Batch Start. Model: {model_name} | Count: {len(data_list)}")
 
 
 
322
 
 
 
 
323
  futures = []
324
  video_executor = concurrent.futures.ThreadPoolExecutor(max_workers=VIDEO_WORKERS)
325
 
 
326
  for row in data_list:
327
  idx = row[0]
328
  prompt = row[1]
329
  logger.log(f"➕ Queueing Video {idx} ({model_name})...")
330
+ futures.append(video_executor.submit(process_single_video, idx, prompt, image, model_name))
331
 
 
332
  completed = 0
333
  total = len(futures)
334
+
335
  for f in concurrent.futures.as_completed(futures):
336
  idx, status = f.result()
337
  if status == "OK":
 
343
  video_executor.shutdown(wait=True)
344
  logger.log("📦 Zipping videos...")
345
  zip_path = create_zip(OUTPUT_DIR, "Ecommerce_Videos")
346
+
347
+ return logger.log("🎉 All Done!"), zip_path, f"Completed {completed}/{total}"
348
 
349
+ def process_single_video(idx, prompt, img_path, model_name):
350
  try:
351
+ url, msg = director.generate_video(model_name, prompt, img_path)
352
  if url:
353
  vid_bytes = download_file(url)
354
  if vid_bytes:
 
360
  print(f"Worker Error: {e}")
361
  return idx, "Fail"
362
 
363
+ # ================= UI 界面 (默认颜色) =================
364
 
365
+ with gr.Blocks(title="Ecommerce Video Generator") as demo:
 
 
 
 
 
 
 
 
366
 
367
  gr.Markdown("## 🛍️ 电商竖屏视频批量生成 (Sora-2 & Veo)")
368
+ gr.Markdown("单图生视频模式:Step 1 分析并生成分镜脚本 -> Step 2 使用主图批量生成视频")
369
 
370
  with gr.Row():
371
+ # 左侧配置
372
+ with gr.Column(scale=1):
373
+ gr.Markdown("### 1. 商品信息 (必填)")
374
+ input_image = gr.Image(label="商品图 (必须上传,用于垫图)", type="filepath", height=250)
375
+ input_desc = gr.Textbox(label="商品描述", placeholder="输入商品卖点...", lines=4)
376
 
377
  gr.Markdown("### 2. 生成配置")
378
+ count_slider = gr.Slider(minimum=1, maximum=100, value=5, step=1, label="生成数量")
379
+ model_dropdown = gr.Dropdown(choices=MODEL_OPTIONS, value="sora-2-all", label="视频模型")
 
 
 
 
 
 
 
 
 
 
 
 
 
380
 
381
+ analyze_btn = gr.Button("🔍 1. 分析并生成脚本", variant="primary")
382
 
383
+ # 右侧操作
384
  with gr.Column(scale=2):
385
+ gr.Markdown("### 3. 脚本确认")
386
  prompt_dataframe = gr.Dataframe(
387
  headers=["ID", "Prompt"],
388
  datatype=["number", "str"],
389
  col_count=(2, "fixed"),
390
  interactive=True,
391
+ label="生成的分镜提示词 (可修改)",
392
+ value=[[1, "等待分析..."]]
 
393
  )
394
 
395
+ generate_btn = gr.Button("🎬 2. 开始批量生成 (使用主图)", variant="primary", visible=False)
396
 
397
+ gr.Markdown("### 4. 结果")
398
+ log_box = gr.TextArea(label="日志", lines=8, interactive=False)
399
+ status_box = gr.Textbox(label="状态", interactive=False)
400
+ download_zip = gr.File(label="下载视频包")
401
 
402
+ # 逻辑绑定
403
  analyze_btn.click(
404
  fn=run_analysis_step,
405
  inputs=[input_image, input_desc, count_slider],
 
408
 
409
  generate_btn.click(
410
  fn=run_generation_step,
411
+ inputs=[input_image, prompt_dataframe, model_dropdown],
412
  outputs=[log_box, download_zip, status_box]
413
  )
414