jacky3102 commited on
Commit
577a69e
·
verified ·
1 Parent(s): 74ef3d5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +107 -74
app.py CHANGED
@@ -1,5 +1,5 @@
1
  from fastapi import FastAPI, File, UploadFile, HTTPException, Form
2
- from fastapi.responses import FileResponse
3
  import tempfile
4
  import os
5
  import numpy as np
@@ -12,11 +12,13 @@ from pathlib import Path
12
  from typing import Optional, List
13
  import random
14
  import math
 
 
15
 
16
  app = FastAPI(title="Video Generator with Effects API")
17
 
18
  # ============================================================
19
- # 特效系统
20
  # ============================================================
21
 
22
  class Particle:
@@ -53,7 +55,6 @@ def draw_snowflake(draw, x, y, size, opacity):
53
  r = int(size * 3)
54
  color = (255, 255, 255, int(opacity))
55
  draw.ellipse([x - r, y - r, x + r, y + r], fill=color)
56
- # 十字光芒
57
  for angle in [0, math.pi / 3, math.pi * 2 / 3]:
58
  ex = x + math.cos(angle) * r * 1.5
59
  ey = y + math.sin(angle) * r * 1.5
@@ -64,7 +65,6 @@ def draw_leaf(draw, x, y, size, opacity, rotation):
64
  leaf_colors = [(180, 60, 30), (200, 120, 20), (220, 160, 40), (160, 80, 20)]
65
  color = random.choice(leaf_colors) + (int(opacity),)
66
  r = int(size * 5)
67
- # 简单椭圆叶片
68
  img = Image.new('RGBA', (r * 2, r * 2), (0, 0, 0, 0))
69
  leaf_draw = ImageDraw.Draw(img)
70
  leaf_draw.ellipse([r * 0.2, r * 0.5, r * 1.8, r * 1.5], fill=color)
@@ -82,7 +82,6 @@ def draw_sparkle(draw, x, y, size, opacity):
82
  gold = (255, 215, 0, int(opacity))
83
  r = int(size * 2)
84
  draw.ellipse([x - r, y - r, x + r, y + r], fill=gold)
85
- # 光芒
86
  for angle in range(0, 360, 45):
87
  rad = math.radians(angle)
88
  ex = x + math.cos(rad) * r * 2
@@ -97,15 +96,14 @@ def draw_dust(draw, x, y, size, opacity):
97
 
98
 
99
  def apply_effect(frame, particles, effect_type):
100
- """在单帧上叠加特效"""
101
  img = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGBA))
102
  overlay = Image.new('RGBA', img.size, (0, 0, 0, 0))
103
  draw = ImageDraw.Draw(overlay)
104
- draw._image = overlay # 为了让 draw_leaf 能访问
105
 
106
  for p in particles:
107
  p.update()
108
- opacity = p.opacity * (1 - (p.y / (p.h + 50))) # 远处淡一点
109
  opacity = max(10, min(255, opacity))
110
 
111
  if effect_type == "snow":
@@ -124,41 +122,27 @@ def apply_effect(frame, particles, effect_type):
124
 
125
 
126
  def apply_old_film(frame, frame_idx, fps):
127
- """老电影效果:随机划痕+闪烁+褪色"""
128
  h, w = frame.shape[:2]
129
-
130
- # 随机闪烁
131
  if random.random() < 0.03:
132
  flicker = random.uniform(0.6, 1.0)
133
  frame = (frame * flicker).astype(np.uint8)
134
-
135
- # 随机划痕
136
  if random.random() < 0.02:
137
  x = random.randint(0, w - 1)
138
  length = random.randint(10, h // 3)
139
  y = random.randint(0, h - length)
140
  cv2.line(frame, (x, y), (x + random.randint(-2, 2), y + length), (255, 255, 255), 1)
141
-
142
- # 褪色效果
143
  frame = cv2.addWeighted(frame, 0.85, np.full_like(frame, (200, 180, 150)), 0.15, 0)
144
-
145
- # 暗角
146
  mask = np.zeros((h, w), dtype=np.float32)
147
  cv2.ellipse(mask, (w // 2, h // 2), (int(w * 0.7), int(h * 0.7)), 0, 0, 360, 1, -1)
148
  mask = cv2.GaussianBlur(mask, (0, 0), w // 3)
149
  mask = mask.reshape(h, w, 1)
150
  frame = (frame * (0.4 + 0.6 * mask)).astype(np.uint8)
151
-
152
  return frame
153
 
154
 
155
  def apply_heat_haze(frame, frame_idx, fps):
156
- """热气蒸腾效果:局部扭曲"""
157
  h, w = frame.shape[:2]
158
- # 只在底部产生热浪
159
  y_start = h * 3 // 4
160
- region = frame[y_start:, :]
161
- # 正弦波位移
162
  displacement = (np.sin(np.linspace(0, math.pi * 4, w) + frame_idx * 0.3) * 3).astype(np.int32)
163
  map_x = np.tile(np.arange(w, dtype=np.float32), (h - y_start, 1))
164
  map_y = np.tile(np.arange(y_start, h, dtype=np.float32).reshape(-1, 1), (1, w))
@@ -170,24 +154,17 @@ def apply_heat_haze(frame, frame_idx, fps):
170
 
171
 
172
  def apply_flame(frame, frame_idx, fps):
173
- """火焰效果:画面底部橙色渐变+闪烁"""
174
  h, w = frame.shape[:2]
175
  flame_h = h // 5
176
  flame = np.zeros((flame_h, w, 3), dtype=np.uint8)
177
-
178
  for i in range(flame_h):
179
  alpha = 1 - (i / flame_h)
180
- # 火焰颜色:从橙色到黄色
181
  r = int(255 * alpha)
182
  g = int(140 * alpha * (0.5 + 0.5 * math.sin(frame_idx * 0.5 + i * 0.3)))
183
  b = 0
184
  flame[i, :] = (b, g, r)
185
-
186
- # 闪烁
187
  flicker = 0.7 + 0.3 * math.sin(frame_idx * 0.8) * random.uniform(0.8, 1.2)
188
  flame = (flame * flicker).astype(np.uint8)
189
-
190
- # 合成
191
  roi = frame[h - flame_h:, :]
192
  blended = cv2.addWeighted(roi, 0.4, flame, 0.6, 0)
193
  frame[h - flame_h:, :] = blended
@@ -199,7 +176,6 @@ def apply_flame(frame, frame_idx, fps):
199
  # ============================================================
200
 
201
  def parse_srt(srt_text: str):
202
- """解析 SRT 为 [(start_sec, end_sec, text), ...]"""
203
  segments = []
204
  blocks = srt_text.strip().split("\n\n")
205
  for block in blocks:
@@ -220,7 +196,6 @@ def timestamp_to_seconds(ts: str):
220
 
221
 
222
  def get_chinese_font():
223
- """尝试获取中文字体"""
224
  fonts = [
225
  "/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc",
226
  "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
@@ -236,7 +211,6 @@ def get_chinese_font():
236
 
237
 
238
  def draw_subtitle(frame, text, font, w, h):
239
- """在画面底部绘制字幕"""
240
  if not text:
241
  return frame
242
  img = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
@@ -248,7 +222,6 @@ def draw_subtitle(frame, text, font, w, h):
248
  except:
249
  font_obj = ImageFont.load_default()
250
 
251
- # 文字换行
252
  max_width = w * 0.85
253
  lines = []
254
  current_line = ""
@@ -263,12 +236,10 @@ def draw_subtitle(frame, text, font, w, h):
263
  if current_line:
264
  lines.append(current_line)
265
 
266
- # 计算位置
267
  line_height = font_size + 8
268
  total_height = len(lines) * line_height
269
  y_start = h - total_height - 60
270
 
271
- # 半透明背景
272
  for i, line in enumerate(lines):
273
  bbox = draw.textbbox((0, 0), line, font=font_obj)
274
  text_w = bbox[2] - bbox[0]
@@ -298,48 +269,54 @@ def generate_video(
298
  ):
299
  # 解析字幕
300
  subtitle_segments = parse_srt(srt_text)
 
301
 
302
  # 获取音频时长
303
  audio = AudioSegment.from_file(audio_path)
304
- total_duration = len(audio) / 1000.0 # 秒
305
  total_frames = int(total_duration * fps)
 
306
 
307
  # 加载图片
308
  images = [cv2.imread(p) for p in image_paths]
309
  images = [img for img in images if img is not None]
310
  if not images:
311
- raise ValueError("No valid images found")
 
312
 
313
- # 统一图片尺寸
314
- h, w = 720, 1280 # 默认 720p 竖屏,可改
315
  images_resized = []
316
- for img in images:
317
  img = cv2.resize(img, (w, h))
318
  images_resized.append(img)
 
319
 
320
- # 每张图展示的帧数
321
  frames_per_image = total_frames // len(images_resized)
322
 
323
- # 初始化特效粒子
324
  particles = []
325
  if effect in ("snow", "leaves", "rain", "sparkle", "dust"):
326
  for _ in range(80):
327
  particles.append(Particle(w, h, effect))
328
 
329
- # 字体
330
  font_path = get_chinese_font()
 
331
 
332
  # 视频写入
333
  fourcc = cv2.VideoWriter_fourcc(*'mp4v')
334
  temp_video = output_path.replace(".mp4", "_temp.mp4")
335
  out = cv2.VideoWriter(temp_video, fourcc, fps, (w, h))
336
 
 
 
 
 
337
  for frame_idx in range(total_frames):
338
- # 选择当前图片
339
  img_idx = min(frame_idx // frames_per_image, len(images_resized) - 1)
340
  frame = images_resized[img_idx].copy()
341
 
342
- # 应用特效
343
  if effect in ("snow", "leaves", "rain", "sparkle", "dust"):
344
  frame = apply_effect(frame, particles, effect)
345
  elif effect == "flame":
@@ -349,44 +326,49 @@ def generate_video(
349
  elif effect == "heat":
350
  frame = apply_heat_haze(frame, frame_idx, fps)
351
 
352
- # 当前时间
353
  current_time = frame_idx / fps
354
-
355
- # 查找当前字幕
356
  current_sub = ""
357
  for start, end, text in subtitle_segments:
358
  if start <= current_time <= end:
359
  current_sub = text
360
  break
361
-
362
- # 绘制字幕
363
  if current_sub:
364
  frame = draw_subtitle(frame, current_sub, font_path, w, h)
365
 
366
  out.write(frame)
367
 
 
 
 
368
  out.release()
 
369
 
370
- # 音频处理
371
- if bgm_path:
 
 
 
 
 
372
  bgm = AudioSegment.from_file(bgm_path)
373
- # BGM 循环或截断到人声长度
374
  if len(bgm) < len(audio):
375
  loops = len(audio) // len(bgm) + 1
376
  bgm = bgm * loops
377
  bgm = bgm[:len(audio)]
378
- # BGM 降低音量作为背景
379
  bgm = bgm - 15
380
- # 混合
381
  mixed_audio = audio.overlay(bgm)
 
382
  else:
383
  mixed_audio = audio
384
 
385
  temp_audio = output_path.replace(".mp4", "_temp.wav")
386
  mixed_audio.export(temp_audio, format="wav")
 
387
 
388
  # FFmpeg 合成
389
- subprocess.run([
 
390
  "ffmpeg", "-y",
391
  "-i", temp_video,
392
  "-i", temp_audio,
@@ -395,12 +377,24 @@ def generate_video(
395
  "-pix_fmt", "yuv420p",
396
  "-shortest",
397
  output_path
398
- ], check=True, capture_output=True)
 
 
 
 
 
 
399
 
400
- # 清理
401
- os.unlink(temp_video)
402
- os.unlink(temp_audio)
 
403
 
 
 
 
 
 
404
  return output_path
405
 
406
 
@@ -408,7 +402,7 @@ def generate_video(
408
  # API 路由
409
  # ============================================================
410
 
411
- EFFECTS = ["snow", "leaves", "rain", "sparkle", "dust", "flame", "oldfilm", "heat", "none"]
412
 
413
 
414
  @app.get("/")
@@ -416,7 +410,8 @@ async def root():
416
  return {
417
  "message": "Video Generator with Effects API",
418
  "endpoints": {
419
- "/generate": "POST - Generate video with effects"
 
420
  },
421
  "effects": EFFECTS,
422
  "input": {
@@ -425,7 +420,7 @@ async def root():
425
  "srt": "SRT subtitle text as string",
426
  "bgm": "Background music file (optional)",
427
  "effect": f"One of: {EFFECTS}",
428
- "resolution": "720p or 1080p (default 720p)"
429
  }
430
  }
431
 
@@ -442,48 +437,86 @@ async def generate(
442
  srt_text: str = Form(...),
443
  bgm: Optional[UploadFile] = File(None),
444
  effect: str = Form("snow"),
445
- resolution: str = Form("720p")
446
  ):
447
  if effect not in EFFECTS:
448
- raise HTTPException(status_code=400, detail=f"Invalid effect. Choose from: {EFFECTS}")
449
 
450
- # 保存上传文件
451
  tmp_dir = tempfile.mkdtemp()
 
 
452
  try:
453
  # 保存图片
454
  image_paths = []
455
  for i, img in enumerate(images):
456
  suffix = Path(img.filename).suffix if img.filename else ".jpg"
457
  path = os.path.join(tmp_dir, f"image_{i}{suffix}")
 
458
  with open(path, "wb") as f:
459
- f.write(await img.read())
460
  image_paths.append(path)
 
461
 
462
  # 保存音频
463
- audio_path = os.path.join(tmp_dir, f"audio{Path(audio.filename).suffix or '.wav'}")
 
 
464
  with open(audio_path, "wb") as f:
465
- f.write(await audio.read())
 
466
 
467
  # 保存 BGM
468
  bgm_path = None
469
  if bgm:
470
- bgm_path = os.path.join(tmp_dir, f"bgm{Path(bgm.filename).suffix or '.mp3'}")
 
 
471
  with open(bgm_path, "wb") as f:
472
- f.write(await bgm.read())
 
 
 
 
 
473
 
474
  # 生成视频
475
  output_path = os.path.join(tmp_dir, "output.mp4")
 
 
476
  generate_video(image_paths, audio_path, srt_text, bgm_path, effect, output_path)
477
 
478
- return FileResponse(output_path, media_type="video/mp4", filename="output.mp4")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
479
 
480
  except Exception as e:
481
- raise HTTPException(status_code=500, detail=f"Generation failed: {str(e)}")
482
- finally:
 
 
483
  # 清理
484
  import shutil
485
  shutil.rmtree(tmp_dir, ignore_errors=True)
486
 
 
 
487
 
488
  if __name__ == "__main__":
489
  import uvicorn
 
1
  from fastapi import FastAPI, File, UploadFile, HTTPException, Form
2
+ from fastapi.responses import FileResponse, PlainTextResponse
3
  import tempfile
4
  import os
5
  import numpy as np
 
12
  from typing import Optional, List
13
  import random
14
  import math
15
+ import traceback
16
+ import sys
17
 
18
  app = FastAPI(title="Video Generator with Effects API")
19
 
20
  # ============================================================
21
+ # 特效系统(和你之前一样,此处省略完整代码)
22
  # ============================================================
23
 
24
  class Particle:
 
55
  r = int(size * 3)
56
  color = (255, 255, 255, int(opacity))
57
  draw.ellipse([x - r, y - r, x + r, y + r], fill=color)
 
58
  for angle in [0, math.pi / 3, math.pi * 2 / 3]:
59
  ex = x + math.cos(angle) * r * 1.5
60
  ey = y + math.sin(angle) * r * 1.5
 
65
  leaf_colors = [(180, 60, 30), (200, 120, 20), (220, 160, 40), (160, 80, 20)]
66
  color = random.choice(leaf_colors) + (int(opacity),)
67
  r = int(size * 5)
 
68
  img = Image.new('RGBA', (r * 2, r * 2), (0, 0, 0, 0))
69
  leaf_draw = ImageDraw.Draw(img)
70
  leaf_draw.ellipse([r * 0.2, r * 0.5, r * 1.8, r * 1.5], fill=color)
 
82
  gold = (255, 215, 0, int(opacity))
83
  r = int(size * 2)
84
  draw.ellipse([x - r, y - r, x + r, y + r], fill=gold)
 
85
  for angle in range(0, 360, 45):
86
  rad = math.radians(angle)
87
  ex = x + math.cos(rad) * r * 2
 
96
 
97
 
98
  def apply_effect(frame, particles, effect_type):
 
99
  img = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGBA))
100
  overlay = Image.new('RGBA', img.size, (0, 0, 0, 0))
101
  draw = ImageDraw.Draw(overlay)
102
+ draw._image = overlay
103
 
104
  for p in particles:
105
  p.update()
106
+ opacity = p.opacity * (1 - (p.y / (p.h + 50)))
107
  opacity = max(10, min(255, opacity))
108
 
109
  if effect_type == "snow":
 
122
 
123
 
124
  def apply_old_film(frame, frame_idx, fps):
 
125
  h, w = frame.shape[:2]
 
 
126
  if random.random() < 0.03:
127
  flicker = random.uniform(0.6, 1.0)
128
  frame = (frame * flicker).astype(np.uint8)
 
 
129
  if random.random() < 0.02:
130
  x = random.randint(0, w - 1)
131
  length = random.randint(10, h // 3)
132
  y = random.randint(0, h - length)
133
  cv2.line(frame, (x, y), (x + random.randint(-2, 2), y + length), (255, 255, 255), 1)
 
 
134
  frame = cv2.addWeighted(frame, 0.85, np.full_like(frame, (200, 180, 150)), 0.15, 0)
 
 
135
  mask = np.zeros((h, w), dtype=np.float32)
136
  cv2.ellipse(mask, (w // 2, h // 2), (int(w * 0.7), int(h * 0.7)), 0, 0, 360, 1, -1)
137
  mask = cv2.GaussianBlur(mask, (0, 0), w // 3)
138
  mask = mask.reshape(h, w, 1)
139
  frame = (frame * (0.4 + 0.6 * mask)).astype(np.uint8)
 
140
  return frame
141
 
142
 
143
  def apply_heat_haze(frame, frame_idx, fps):
 
144
  h, w = frame.shape[:2]
 
145
  y_start = h * 3 // 4
 
 
146
  displacement = (np.sin(np.linspace(0, math.pi * 4, w) + frame_idx * 0.3) * 3).astype(np.int32)
147
  map_x = np.tile(np.arange(w, dtype=np.float32), (h - y_start, 1))
148
  map_y = np.tile(np.arange(y_start, h, dtype=np.float32).reshape(-1, 1), (1, w))
 
154
 
155
 
156
  def apply_flame(frame, frame_idx, fps):
 
157
  h, w = frame.shape[:2]
158
  flame_h = h // 5
159
  flame = np.zeros((flame_h, w, 3), dtype=np.uint8)
 
160
  for i in range(flame_h):
161
  alpha = 1 - (i / flame_h)
 
162
  r = int(255 * alpha)
163
  g = int(140 * alpha * (0.5 + 0.5 * math.sin(frame_idx * 0.5 + i * 0.3)))
164
  b = 0
165
  flame[i, :] = (b, g, r)
 
 
166
  flicker = 0.7 + 0.3 * math.sin(frame_idx * 0.8) * random.uniform(0.8, 1.2)
167
  flame = (flame * flicker).astype(np.uint8)
 
 
168
  roi = frame[h - flame_h:, :]
169
  blended = cv2.addWeighted(roi, 0.4, flame, 0.6, 0)
170
  frame[h - flame_h:, :] = blended
 
176
  # ============================================================
177
 
178
  def parse_srt(srt_text: str):
 
179
  segments = []
180
  blocks = srt_text.strip().split("\n\n")
181
  for block in blocks:
 
196
 
197
 
198
  def get_chinese_font():
 
199
  fonts = [
200
  "/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc",
201
  "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
 
211
 
212
 
213
  def draw_subtitle(frame, text, font, w, h):
 
214
  if not text:
215
  return frame
216
  img = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
 
222
  except:
223
  font_obj = ImageFont.load_default()
224
 
 
225
  max_width = w * 0.85
226
  lines = []
227
  current_line = ""
 
236
  if current_line:
237
  lines.append(current_line)
238
 
 
239
  line_height = font_size + 8
240
  total_height = len(lines) * line_height
241
  y_start = h - total_height - 60
242
 
 
243
  for i, line in enumerate(lines):
244
  bbox = draw.textbbox((0, 0), line, font=font_obj)
245
  text_w = bbox[2] - bbox[0]
 
269
  ):
270
  # 解析字幕
271
  subtitle_segments = parse_srt(srt_text)
272
+ print(f"📝 解析到 {len(subtitle_segments)} 条字幕", file=sys.stderr)
273
 
274
  # 获取音频时长
275
  audio = AudioSegment.from_file(audio_path)
276
+ total_duration = len(audio) / 1000.0
277
  total_frames = int(total_duration * fps)
278
+ print(f"🎵 音频时长: {total_duration:.1f}秒, 总帧数: {total_frames}", file=sys.stderr)
279
 
280
  # 加载图片
281
  images = [cv2.imread(p) for p in image_paths]
282
  images = [img for img in images if img is not None]
283
  if not images:
284
+ raise ValueError("没有有效的图片文件")
285
+ print(f"🖼️ 加载了 {len(images)} 张图片", file=sys.stderr)
286
 
287
+ # 统一尺寸
288
+ h, w = 720, 1280
289
  images_resized = []
290
+ for i, img in enumerate(images):
291
  img = cv2.resize(img, (w, h))
292
  images_resized.append(img)
293
+ print(f" 图片{i+1}: {img.shape}", file=sys.stderr)
294
 
 
295
  frames_per_image = total_frames // len(images_resized)
296
 
297
+ # 初始化粒子
298
  particles = []
299
  if effect in ("snow", "leaves", "rain", "sparkle", "dust"):
300
  for _ in range(80):
301
  particles.append(Particle(w, h, effect))
302
 
 
303
  font_path = get_chinese_font()
304
+ print(f"🔤 字体: {font_path or '默认'}", file=sys.stderr)
305
 
306
  # 视频写入
307
  fourcc = cv2.VideoWriter_fourcc(*'mp4v')
308
  temp_video = output_path.replace(".mp4", "_temp.mp4")
309
  out = cv2.VideoWriter(temp_video, fourcc, fps, (w, h))
310
 
311
+ if not out.isOpened():
312
+ raise RuntimeError(f"无法创建视频文件: {temp_video}")
313
+
314
+ print(f"🎬 开始写入视频帧...", file=sys.stderr)
315
  for frame_idx in range(total_frames):
 
316
  img_idx = min(frame_idx // frames_per_image, len(images_resized) - 1)
317
  frame = images_resized[img_idx].copy()
318
 
319
+ # 特效
320
  if effect in ("snow", "leaves", "rain", "sparkle", "dust"):
321
  frame = apply_effect(frame, particles, effect)
322
  elif effect == "flame":
 
326
  elif effect == "heat":
327
  frame = apply_heat_haze(frame, frame_idx, fps)
328
 
329
+ # 字幕
330
  current_time = frame_idx / fps
 
 
331
  current_sub = ""
332
  for start, end, text in subtitle_segments:
333
  if start <= current_time <= end:
334
  current_sub = text
335
  break
 
 
336
  if current_sub:
337
  frame = draw_subtitle(frame, current_sub, font_path, w, h)
338
 
339
  out.write(frame)
340
 
341
+ if frame_idx % (fps * 10) == 0:
342
+ print(f" 进度: {frame_idx}/{total_frames} 帧 ({100*frame_idx//total_frames}%)", file=sys.stderr)
343
+
344
  out.release()
345
+ print(f"✅ 视频帧写入完成", file=sys.stderr)
346
 
347
+ # 检查临时视频
348
+ if not os.path.exists(temp_video) or os.path.getsize(temp_video) == 0:
349
+ raise RuntimeError(f"临时视频文件生成失败: {temp_video}")
350
+
351
+ # 音频混音
352
+ print(f"🔊 处理音频...", file=sys.stderr)
353
+ if bgm_path and os.path.exists(bgm_path):
354
  bgm = AudioSegment.from_file(bgm_path)
 
355
  if len(bgm) < len(audio):
356
  loops = len(audio) // len(bgm) + 1
357
  bgm = bgm * loops
358
  bgm = bgm[:len(audio)]
 
359
  bgm = bgm - 15
 
360
  mixed_audio = audio.overlay(bgm)
361
+ print(f" BGM已混合", file=sys.stderr)
362
  else:
363
  mixed_audio = audio
364
 
365
  temp_audio = output_path.replace(".mp4", "_temp.wav")
366
  mixed_audio.export(temp_audio, format="wav")
367
+ print(f"✅ 音频导出完成", file=sys.stderr)
368
 
369
  # FFmpeg 合成
370
+ print(f"🎬 FFmpeg 合成中...", file=sys.stderr)
371
+ result = subprocess.run([
372
  "ffmpeg", "-y",
373
  "-i", temp_video,
374
  "-i", temp_audio,
 
377
  "-pix_fmt", "yuv420p",
378
  "-shortest",
379
  output_path
380
+ ], check=False, capture_output=True, text=True)
381
+
382
+ if result.returncode != 0:
383
+ print(f"❌ FFmpeg 错误:\n{result.stderr}", file=sys.stderr)
384
+ raise RuntimeError(f"FFmpeg 合成失败: {result.stderr[:500]}")
385
+
386
+ print(f"✅ FFmpeg 合成完成", file=sys.stderr)
387
 
388
+ # 清理临时文件
389
+ for tmp in [temp_video, temp_audio]:
390
+ if os.path.exists(tmp):
391
+ os.unlink(tmp)
392
 
393
+ # 最终检查
394
+ if not os.path.exists(output_path) or os.path.getsize(output_path) == 0:
395
+ raise RuntimeError(f"最终视频文件生成失败: {output_path}")
396
+
397
+ print(f"✅ 视频生成成功: {output_path} ({os.path.getsize(output_path)} bytes)", file=sys.stderr)
398
  return output_path
399
 
400
 
 
402
  # API 路由
403
  # ============================================================
404
 
405
+ EFFECTS = ["snow", "leaves", "rain", "sparkle", "dust", "flame", "heat", "oldfilm", "none"]
406
 
407
 
408
  @app.get("/")
 
410
  return {
411
  "message": "Video Generator with Effects API",
412
  "endpoints": {
413
+ "/generate": "POST - Generate video with effects",
414
+ "/health": "GET - Health check"
415
  },
416
  "effects": EFFECTS,
417
  "input": {
 
420
  "srt": "SRT subtitle text as string",
421
  "bgm": "Background music file (optional)",
422
  "effect": f"One of: {EFFECTS}",
423
+ "resolution": "720p"
424
  }
425
  }
426
 
 
437
  srt_text: str = Form(...),
438
  bgm: Optional[UploadFile] = File(None),
439
  effect: str = Form("snow"),
 
440
  ):
441
  if effect not in EFFECTS:
442
+ raise HTTPException(status_code=400, detail=f"无效特效。可选: {EFFECTS}")
443
 
 
444
  tmp_dir = tempfile.mkdtemp()
445
+ print(f"📂 临时目录: {tmp_dir}", file=sys.stderr)
446
+
447
  try:
448
  # 保存图片
449
  image_paths = []
450
  for i, img in enumerate(images):
451
  suffix = Path(img.filename).suffix if img.filename else ".jpg"
452
  path = os.path.join(tmp_dir, f"image_{i}{suffix}")
453
+ content = await img.read()
454
  with open(path, "wb") as f:
455
+ f.write(content)
456
  image_paths.append(path)
457
+ print(f" 保存图片{i+1}: {path} ({len(content)} bytes)", file=sys.stderr)
458
 
459
  # 保存音频
460
+ audio_suffix = Path(audio.filename).suffix if audio.filename else ".wav"
461
+ audio_path = os.path.join(tmp_dir, f"audio{audio_suffix}")
462
+ audio_content = await audio.read()
463
  with open(audio_path, "wb") as f:
464
+ f.write(audio_content)
465
+ print(f" 保存音频: {audio_path} ({len(audio_content)} bytes)", file=sys.stderr)
466
 
467
  # 保存 BGM
468
  bgm_path = None
469
  if bgm:
470
+ bgm_suffix = Path(bgm.filename).suffix if bgm.filename else ".mp3"
471
+ bgm_path = os.path.join(tmp_dir, f"bgm{bgm_suffix}")
472
+ bgm_content = await bgm.read()
473
  with open(bgm_path, "wb") as f:
474
+ f.write(bgm_content)
475
+ print(f" 保存BGM: {bgm_path} ({len(bgm_content)} bytes)", file=sys.stderr)
476
+
477
+ # 字幕信息
478
+ srt_preview = srt_text[:100].replace("\n", "\\n")
479
+ print(f" 字幕: {srt_preview}...", file=sys.stderr)
480
 
481
  # 生成视频
482
  output_path = os.path.join(tmp_dir, "output.mp4")
483
+ print(f"🚀 开始生成视频 → {output_path}", file=sys.stderr)
484
+
485
  generate_video(image_paths, audio_path, srt_text, bgm_path, effect, output_path)
486
 
487
+ # 验证文件存在
488
+ if not os.path.exists(output_path):
489
+ raise RuntimeError(f"视频生成失败,文件不存在: {output_path}")
490
+
491
+ file_size = os.path.getsize(output_path)
492
+ print(f"✅ 生成成功: {output_path} ({file_size} bytes)", file=sys.stderr)
493
+
494
+ # 直接读取文件内容返回,避免 FileResponse 的路径问题
495
+ with open(output_path, "rb") as f:
496
+ video_bytes = f.read()
497
+
498
+ # 清理
499
+ import shutil
500
+ shutil.rmtree(tmp_dir, ignore_errors=True)
501
+
502
+ from fastapi.responses import Response
503
+ return Response(
504
+ content=video_bytes,
505
+ media_type="video/mp4",
506
+ headers={"Content-Disposition": "attachment; filename=output.mp4"}
507
+ )
508
 
509
  except Exception as e:
510
+ error_msg = f"生成失败: {str(e)}"
511
+ print(f"❌ {error_msg}", file=sys.stderr)
512
+ print(traceback.format_exc(), file=sys.stderr)
513
+
514
  # 清理
515
  import shutil
516
  shutil.rmtree(tmp_dir, ignore_errors=True)
517
 
518
+ raise HTTPException(status_code=500, detail=error_msg)
519
+
520
 
521
  if __name__ == "__main__":
522
  import uvicorn