deeme commited on
Commit
8b906ea
·
verified ·
1 Parent(s): c0372f3

Upload 3 files

Browse files
Files changed (3) hide show
  1. Dockerfile +2 -1
  2. app.py +272 -137
  3. requirements.txt +0 -1
Dockerfile CHANGED
@@ -16,7 +16,8 @@ RUN pip install --no-cache-dir -r requirements.txt
16
  COPY . .
17
 
18
  # 创建必要的目录
19
- RUN mkdir -p temp
 
20
 
21
  # 暴露端口
22
  EXPOSE 8000
 
16
  COPY . .
17
 
18
  # 创建必要的目录
19
+ RUN mkdir -p temp && chmod 777 temp
20
+ RUN mkdir -p storage && chmod 777 storage
21
 
22
  # 暴露端口
23
  EXPOSE 8000
app.py CHANGED
@@ -1,7 +1,7 @@
1
  from fastapi import FastAPI, HTTPException, BackgroundTasks
2
  from fastapi.middleware.cors import CORSMiddleware
3
  from pydantic import BaseModel
4
- from typing import List
5
  import os
6
  import uuid
7
  import aiohttp
@@ -10,9 +10,12 @@ import logging
10
  import tempfile
11
  import openai
12
  from pathlib import Path
13
- import webdav3.client as wc
14
  import subprocess
15
  import shutil
 
 
 
 
16
 
17
  # 配置日志
18
  logging.basicConfig(level=logging.INFO)
@@ -21,9 +24,7 @@ logger = logging.getLogger(__name__)
21
  # 环境变量
22
  OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
23
  OPENAI_BASE_URL = os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1")
24
- WEBDAV_URL = os.getenv("WEBDAV_URL")
25
- WEBDAV_USERNAME = os.getenv("WEBDAV_USERNAME")
26
- WEBDAV_PASSWORD = os.getenv("WEBDAV_PASSWORD")
27
 
28
  # 初始化OpenAI
29
  openai.api_key = OPENAI_API_KEY
@@ -32,6 +33,8 @@ if OPENAI_BASE_URL:
32
 
33
  app = FastAPI()
34
 
 
 
35
  # 配置CORS
36
  app.add_middleware(
37
  CORSMiddleware,
@@ -47,26 +50,23 @@ class ComicData(BaseModel):
47
  speeches: List[str]
48
  panels: List[str] # 图片URLs
49
 
50
- # WebDAV客户端配置
51
- def get_webdav_client():
52
- options = {
53
- 'webdav_hostname': WEBDAV_URL,
54
- 'webdav_login': WEBDAV_USERNAME,
55
- 'webdav_password': WEBDAV_PASSWORD
56
- }
57
- return wc.Client(options)
58
-
59
  # 下载图片
60
- async def download_image(session, url, output_path):
61
  try:
62
- async with session.get(url) as response:
63
- if response.status == 200:
64
- with open(output_path, 'wb') as f:
65
- f.write(await response.read())
66
- return output_path
67
- else:
68
- logger.error(f"Failed to download image: {response.status}")
69
- return None
 
 
 
 
 
 
70
  except Exception as e:
71
  logger.error(f"Error downloading image: {e}")
72
  return None
@@ -77,87 +77,80 @@ async def generate_speech(text, voice="alloy", output_path=None):
77
  if not output_path:
78
  output_path = f"{uuid.uuid4()}.mp3"
79
 
80
- response = await openai.audio.speech.create(
81
  model="tts-1",
82
  voice=voice,
83
  input=text
84
  )
85
 
86
- response.stream_to_file(output_path)
 
 
 
87
  return output_path
88
  except Exception as e:
89
  logger.error(f"Error generating speech: {e}")
90
  return None
91
 
92
- # 创建视频
93
- def create_video(project_dir, image_paths, subtitle_file, audio_file, output_video):
94
  try:
95
- # 创建帧列表文件
96
- frames_list = os.path.join(project_dir, "frames.txt")
97
- with open(frames_list, "w") as f:
98
- for img in image_paths:
99
- # 每个图片显示5秒
100
- f.write(f"file '{img}'\n")
101
- f.write(f"duration 5\n")
102
- # 最后一张图片需要单独添加,否则会被忽略
103
- f.write(f"file '{image_paths[-1]}'\n")
 
104
 
105
- # 使用FFmpeg创建视频
106
- cmd = [
107
- "ffmpeg", "-y",
108
- "-f", "concat", "-safe", "0", "-i", frames_list,
109
- "-i", audio_file,
110
- "-vf", f"subtitles={subtitle_file}",
111
- "-c:v", "libx264", "-pix_fmt", "yuv420p",
112
- "-c:a", "aac", "-strict", "experimental",
113
- output_video
114
- ]
 
 
 
 
 
115
 
116
- subprocess.run(cmd, check=True)
117
- return output_video
118
  except Exception as e:
119
- logger.error(f"Error creating video: {e}")
120
  return None
121
 
122
- # 创建字幕文件
123
- def create_subtitle_file(project_dir, captions, speeches):
124
  try:
125
- subtitle_file = os.path.join(project_dir, "subtitles.srt")
126
 
127
  with open(subtitle_file, "w", encoding="utf-8") as f:
128
  subtitle_index = 1
129
- current_time = 0
130
 
131
- # 处理每个面板的字幕和对话
132
- for i, (caption, speech) in enumerate(zip(captions, speeches)):
133
- # 每个面板展示5秒
134
- panel_duration = 5
 
 
135
 
136
- # 字幕开始和结束时间
137
- start_time = current_time
138
-
139
- # 字幕显示
140
- if caption:
141
- end_time = start_time + 2.5
142
- f.write(f"{subtitle_index}\n")
143
- f.write(f"{format_time(start_time)} --> {format_time(end_time)}\n")
144
- f.write(f"{caption}\n\n")
145
- subtitle_index += 1
146
-
147
- # 对话显示
148
- if speech:
149
- speech_start = start_time + 2.5 if caption else start_time
150
- speech_end = current_time + panel_duration
151
- f.write(f"{subtitle_index}\n")
152
- f.write(f"{format_time(speech_start)} --> {format_time(speech_end)}\n")
153
- f.write(f"{speech}\n\n")
154
- subtitle_index += 1
155
-
156
- current_time += panel_duration
157
 
158
  return subtitle_file
159
  except Exception as e:
160
- logger.error(f"Error creating subtitle file: {e}")
161
  return None
162
 
163
  # 格式化时间为SRT格式
@@ -172,131 +165,273 @@ def format_time(seconds):
172
  async def create_audio_file(project_dir, captions, speeches):
173
  try:
174
  audio_parts = []
 
 
175
  current_time = 0
 
176
 
177
  # 为每个面板生成音频
178
  for i, (caption, speech) in enumerate(zip(captions, speeches)):
 
 
 
179
  # 每个面板的旁白
180
  if caption:
181
  caption_audio = os.path.join(project_dir, f"caption_{i}.mp3")
182
- await generate_speech(caption, "alloy", caption_audio)
183
- audio_parts.append(caption_audio)
 
 
 
 
184
 
185
  # 每个面板的对话
186
  if speech:
187
  speech_audio = os.path.join(project_dir, f"speech_{i}.mp3")
188
- await generate_speech(speech, "echo", speech_audio)
189
- audio_parts.append(speech_audio)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
 
191
- # 合并所有音频部分
 
 
 
 
192
  combined_audio = os.path.join(project_dir, "combined_audio.mp3")
 
 
 
 
 
 
 
193
 
194
- # 使用FFmpeg合并音频
195
- audio_list = os.path.join(project_dir, "audio_list.txt")
196
- with open(audio_list, "w") as f:
197
- for audio in audio_parts:
198
- f.write(f"file '{audio}'\n")
199
 
200
- subprocess.run([
201
- "ffmpeg", "-y", "-f", "concat", "-safe", "0",
202
- "-i", audio_list, "-c", "copy", combined_audio
203
- ], check=True)
204
 
205
- return combined_audio
206
  except Exception as e:
207
  logger.error(f"Error creating audio file: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
  return None
209
 
210
- # 上传到WebDAV
211
- def upload_to_webdav(local_path, remote_path):
212
  try:
213
- client = get_webdav_client()
 
 
 
 
 
 
214
 
215
- # 确保远程目录存在
216
- remote_dir = os.path.dirname(remote_path)
217
- if not client.check(remote_dir):
218
- client.mkdir(remote_dir)
219
 
220
- # 上传文件
221
- client.upload_sync(local_path=local_path, remote_path=remote_path)
222
 
223
- # 获取公共URL
224
- return f"{WEBDAV_URL}/{remote_path}"
 
 
225
  except Exception as e:
226
- logger.error(f"Error uploading to WebDAV: {e}")
 
 
227
  return None
228
 
229
  @app.post("/api/generate-video")
230
  async def generate_video(comic_data: ComicData, background_tasks: BackgroundTasks):
231
  # 创建唯一项目ID
232
  project_id = str(uuid.uuid4())
233
- project_dir = f"temp/{project_id}"
 
234
  os.makedirs(project_dir, exist_ok=True)
235
 
 
 
236
  try:
237
  # 下载图片
238
  image_paths = []
239
- async with aiohttp.ClientSession() as session:
240
- download_tasks = []
241
- for i, panel_url in enumerate(comic_data.panels):
242
- output_path = os.path.join(project_dir, f"panel_{i}.jpg")
243
- download_tasks.append(download_image(session, panel_url, output_path))
244
-
245
- image_paths = await asyncio.gather(*download_tasks)
246
- image_paths = [p for p in image_paths if p] # 过滤失败的下载
247
 
248
  if not image_paths:
249
  raise HTTPException(status_code=500, detail="Failed to download images")
250
 
251
- # 创建字幕文件
252
- subtitle_file = create_subtitle_file(project_dir, comic_data.captions, comic_data.speeches)
253
- if not subtitle_file:
254
- raise HTTPException(status_code=500, detail="Failed to create subtitle file")
255
 
256
  # 创建音频文件
257
- audio_file = await create_audio_file(project_dir, comic_data.captions, comic_data.speeches)
 
 
258
  if not audio_file:
259
  raise HTTPException(status_code=500, detail="Failed to create audio file")
260
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
261
  # 创建视频
262
  output_video = os.path.join(project_dir, "output.mp4")
263
- result = create_video(project_dir, image_paths, subtitle_file, audio_file, output_video)
 
 
 
264
  if not result:
265
  raise HTTPException(status_code=500, detail="Failed to create video")
266
 
267
- # 创建WebDAV目录结构
268
- webdav_base_path = f"comic_videos/{project_id}"
269
 
270
- # 上传所有资源到WebDAV
271
- video_url = upload_to_webdav(output_video, f"{webdav_base_path}/video.mp4")
272
- subtitle_url = upload_to_webdav(subtitle_file, f"{webdav_base_path}/subtitles.srt")
273
- audio_url = upload_to_webdav(audio_file, f"{webdav_base_path}/audio.mp3")
274
 
275
  # 上传图片
276
- image_urls = []
277
- for i, img_path in enumerate(image_paths):
278
- remote_path = f"{webdav_base_path}/images/panel_{i}.jpg"
279
- img_url = upload_to_webdav(img_path, remote_path)
280
- if img_url:
281
- image_urls.append(img_url)
282
 
283
  # 后台任务清理临时文件
284
  background_tasks.add_task(lambda: shutil.rmtree(project_dir, ignore_errors=True))
285
 
286
  return {
287
  "videoUrl": video_url,
288
- "subtitleUrl": subtitle_url,
289
- "audioUrl": audio_url,
290
- "imageUrls": image_urls,
291
  "projectId": project_id
292
  }
293
  except Exception as e:
294
  # 清理临时文件
295
  shutil.rmtree(project_dir, ignore_errors=True)
296
  logger.error(f"Error generating video: {e}")
 
 
297
  raise HTTPException(status_code=500, detail=str(e))
298
 
299
- # 健康检查端点health
300
  @app.get("/")
301
  async def health_check():
302
  return {"status": "ok"}
 
1
  from fastapi import FastAPI, HTTPException, BackgroundTasks
2
  from fastapi.middleware.cors import CORSMiddleware
3
  from pydantic import BaseModel
4
+ from typing import List, Dict
5
  import os
6
  import uuid
7
  import aiohttp
 
10
  import tempfile
11
  import openai
12
  from pathlib import Path
 
13
  import subprocess
14
  import shutil
15
+ import ssl
16
+ import json
17
+ from fastapi.staticfiles import StaticFiles
18
+ from pydub import AudioSegment
19
 
20
  # 配置日志
21
  logging.basicConfig(level=logging.INFO)
 
24
  # 环境变量
25
  OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
26
  OPENAI_BASE_URL = os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1")
27
+ BASE_URL = os.getenv("BASE_URL", "http://localhost:8000")
 
 
28
 
29
  # 初始化OpenAI
30
  openai.api_key = OPENAI_API_KEY
 
33
 
34
  app = FastAPI()
35
 
36
+ app.mount("/storage", StaticFiles(directory="storage"), name="storage")
37
+
38
  # 配置CORS
39
  app.add_middleware(
40
  CORSMiddleware,
 
50
  speeches: List[str]
51
  panels: List[str] # 图片URLs
52
 
 
 
 
 
 
 
 
 
 
53
  # 下载图片
54
+ async def download_image(url, output_path):
55
  try:
56
+ # 创建一个不验证SSL的TCP连接器
57
+ ssl_context = ssl.create_default_context()
58
+ ssl_context.check_hostname = False
59
+ ssl_context.verify_mode = ssl.CERT_NONE
60
+
61
+ async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=ssl_context)) as session:
62
+ async with session.get(url) as response:
63
+ if response.status == 200:
64
+ with open(output_path, 'wb') as f:
65
+ f.write(await response.read())
66
+ return output_path
67
+ else:
68
+ logger.error(f"Failed to download image: {response.status}")
69
+ return None
70
  except Exception as e:
71
  logger.error(f"Error downloading image: {e}")
72
  return None
 
77
  if not output_path:
78
  output_path = f"{uuid.uuid4()}.mp3"
79
 
80
+ response = openai.audio.speech.create(
81
  model="tts-1",
82
  voice=voice,
83
  input=text
84
  )
85
 
86
+ # 直接将内容写入文件
87
+ with open(output_path, "wb") as f:
88
+ f.write(response.content)
89
+
90
  return output_path
91
  except Exception as e:
92
  logger.error(f"Error generating speech: {e}")
93
  return None
94
 
95
+ # 获取音频文件时长
96
+ def get_audio_duration(audio_path):
97
  try:
98
+ audio = AudioSegment.from_file(audio_path)
99
+ return len(audio) / 1000.0 # 转换为秒
100
+ except Exception as e:
101
+ logger.error(f"Error getting audio duration: {e}")
102
+ return 5.0 # 默认5秒
103
+
104
+ # 创建caption字幕文件(底部显示)
105
+ def create_caption_subtitle_file(project_dir, captions, panel_start_times, panel_durations):
106
+ try:
107
+ subtitle_file = os.path.join(project_dir, "captions.srt")
108
 
109
+ with open(subtitle_file, "w", encoding="utf-8") as f:
110
+ subtitle_index = 1
111
+
112
+ # 处理每个面板的旁白字幕
113
+ for i, caption in enumerate(captions):
114
+ # 获取当前面板的开始时间和持续时间
115
+ start_time = panel_start_times[i]
116
+ duration = panel_durations[i]
117
+ end_time = start_time + duration
118
+
119
+ # 字幕显示整个面板的时长
120
+ f.write(f"{subtitle_index}\n")
121
+ f.write(f"{format_time(start_time)} --> {format_time(end_time)}\n")
122
+ f.write(f"{caption}\n\n")
123
+ subtitle_index += 1
124
 
125
+ return subtitle_file
 
126
  except Exception as e:
127
+ logger.error(f"Error creating caption subtitle file: {e}")
128
  return None
129
 
130
+ # 创建speech字幕文件(顶部显示)
131
+ def create_speech_subtitle_file(project_dir, speeches, panel_start_times, panel_durations):
132
  try:
133
+ subtitle_file = os.path.join(project_dir, "speeches.srt")
134
 
135
  with open(subtitle_file, "w", encoding="utf-8") as f:
136
  subtitle_index = 1
 
137
 
138
+ # 处理每个面板的对话字幕
139
+ for i, speech in enumerate(speeches):
140
+ # 获取当前面板的开始时间和持续时间
141
+ start_time = panel_start_times[i]
142
+ duration = panel_durations[i]
143
+ end_time = start_time + duration
144
 
145
+ # 字幕显示整个面板的时长
146
+ f.write(f"{subtitle_index}\n")
147
+ f.write(f"{format_time(start_time)} --> {format_time(end_time)}\n")
148
+ f.write(f"{speech}\n\n")
149
+ subtitle_index += 1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
 
151
  return subtitle_file
152
  except Exception as e:
153
+ logger.error(f"Error creating speech subtitle file: {e}")
154
  return None
155
 
156
  # 格式化时间为SRT格式
 
165
  async def create_audio_file(project_dir, captions, speeches):
166
  try:
167
  audio_parts = []
168
+ audio_durations = {}
169
+ panel_start_times = [0] # 第一个面板从0秒开始
170
  current_time = 0
171
+ panel_durations = []
172
 
173
  # 为每个面板生成音频
174
  for i, (caption, speech) in enumerate(zip(captions, speeches)):
175
+ panel_audio_parts = []
176
+ panel_duration = 0
177
+
178
  # 每个面板的旁白
179
  if caption:
180
  caption_audio = os.path.join(project_dir, f"caption_{i}.mp3")
181
+ result = await generate_speech(caption, "alloy", caption_audio)
182
+ if result:
183
+ duration = get_audio_duration(caption_audio)
184
+ audio_durations[f"caption_{i}"] = duration
185
+ panel_audio_parts.append(caption_audio)
186
+ panel_duration += duration
187
 
188
  # 每个面板的对话
189
  if speech:
190
  speech_audio = os.path.join(project_dir, f"speech_{i}.mp3")
191
+ result = await generate_speech(speech, "nova", speech_audio)
192
+ if result:
193
+ duration = get_audio_duration(speech_audio)
194
+ audio_durations[f"speech_{i}"] = duration
195
+ panel_audio_parts.append(speech_audio)
196
+ panel_duration += duration
197
+
198
+ # 确保每个面板至少有最小时长
199
+ if panel_duration == 0:
200
+ panel_duration = 5.0 # 默认5秒
201
+
202
+ panel_durations.append(panel_duration)
203
+
204
+ # 合并当前面板的音频(先caption后speech)
205
+ if panel_audio_parts:
206
+ panel_combined = os.path.join(project_dir, f"panel_{i}_combined.mp3")
207
+ combined = AudioSegment.empty()
208
+
209
+ for audio_path in panel_audio_parts:
210
+ segment = AudioSegment.from_file(audio_path)
211
+ combined += segment
212
+
213
+ combined.export(panel_combined, format="mp3")
214
+ audio_parts.append(panel_combined)
215
+
216
+ # 更新下一个面板的开始时间
217
+ current_time += panel_duration
218
+ if i < len(captions) - 1: # 不是最后一个面板
219
+ panel_start_times.append(current_time)
220
 
221
+ if not audio_parts:
222
+ logger.error("No audio parts generated")
223
+ return None, {}, [], []
224
+
225
+ # 合并所有面板的音频
226
  combined_audio = os.path.join(project_dir, "combined_audio.mp3")
227
+ final_combined = AudioSegment.empty()
228
+
229
+ for audio_path in audio_parts:
230
+ segment = AudioSegment.from_file(audio_path)
231
+ final_combined += segment
232
+
233
+ final_combined.export(combined_audio, format="mp3")
234
 
235
+ # 保存音频时长信息
236
+ durations_file = os.path.join(project_dir, "audio_durations.json")
237
+ with open(durations_file, "w") as f:
238
+ json.dump(audio_durations, f)
 
239
 
240
+ # 保存面板开始时间信息
241
+ panel_times_file = os.path.join(project_dir, "panel_times.json")
242
+ with open(panel_times_file, "w") as f:
243
+ json.dump({"start_times": panel_start_times, "durations": panel_durations}, f)
244
 
245
+ return combined_audio, audio_durations, panel_start_times, panel_durations
246
  except Exception as e:
247
  logger.error(f"Error creating audio file: {e}")
248
+ import traceback
249
+ logger.error(traceback.format_exc())
250
+ return None, {}, [], []
251
+
252
+ # 创建视频
253
+ def create_video(project_dir, image_paths, caption_subtitle_file, speech_subtitle_file, audio_file, output_video, audio_durations, panel_start_times, panel_durations):
254
+ try:
255
+ # 创建帧列表文件 - 使用精确的面板持续时间
256
+ frames_list = os.path.join(project_dir, "frames.txt")
257
+ with open(frames_list, "w") as f:
258
+ for i, (img, duration) in enumerate(zip(image_paths, panel_durations)):
259
+ f.write(f"file '{img.replace(os.sep, '/')}'\n")
260
+ f.write(f"duration {duration}\n")
261
+
262
+ # 最后一张图片需要单独添加,否则会被忽略
263
+ f.write(f"file '{image_paths[-1].replace(os.sep, '/')}'\n")
264
+
265
+ # 先创建无字幕的视频 - 使用更精确的同步选项
266
+ temp_video = os.path.join(project_dir, "temp_video.mp4")
267
+ cmd1 = [
268
+ "ffmpeg", "-y",
269
+ "-f", "concat", "-safe", "0", "-i", frames_list,
270
+ "-i", audio_file,
271
+ "-c:v", "libx264", "-pix_fmt", "yuv420p",
272
+ "-c:a", "aac", "-strict", "experimental",
273
+ "-vsync", "vfr", # 确保视频帧率与时间轴匹配
274
+ "-async", "1", # 音频同步
275
+ temp_video
276
+ ]
277
+
278
+ subprocess.run(cmd1, check=True)
279
+
280
+ # 然后添加字幕 - 先添加caption字幕(底部)
281
+ temp_video2 = os.path.join(project_dir, "temp_video2.mp4")
282
+ caption_filter = f"subtitles='{caption_subtitle_file.replace(os.sep, '/').replace(':', '\\:')}':force_style='Fontname=Consolas,Alignment=2,FontSize=15,PrimaryColour=&H00E0FFFF,OutlineColour=&H80000000,BackColour=&H40000000,BorderStyle=1,Outline=0.8,Shadow=0.5,MarginV=10'"
283
+
284
+ cmd2 = [
285
+ "ffmpeg", "-y",
286
+ "-i", temp_video,
287
+ "-vf", caption_filter,
288
+ "-c:a", "copy",
289
+ temp_video2
290
+ ]
291
+
292
+ subprocess.run(cmd2, check=True)
293
+
294
+ # 再��加speech字幕(顶部)
295
+ speech_filter = f"subtitles='{speech_subtitle_file.replace(os.sep, '/').replace(':', '\\:')}':force_style='Fontname=Consolas,Alignment=6,FontSize=15,PrimaryColour=&H00FFCCE6,OutlineColour=&H80000000,BackColour=&H40000000,BorderStyle=1,Outline=0.8,Shadow=0.5,MarginV=10,MarginR=15'"
296
+
297
+ cmd3 = [
298
+ "ffmpeg", "-y",
299
+ "-i", temp_video2,
300
+ "-vf", speech_filter,
301
+ "-c:a", "copy",
302
+ output_video
303
+ ]
304
+
305
+ subprocess.run(cmd3, check=True)
306
+
307
+ # 删除临时文件
308
+ os.remove(temp_video)
309
+ os.remove(temp_video2)
310
+
311
+ return output_video
312
+ except Exception as e:
313
+ logger.error(f"Error creating video: {e}")
314
+ import traceback
315
+ logger.error(traceback.format_exc())
316
  return None
317
 
318
+ # 使用本地存储
319
+ def upload_to_local_storage(local_path, relative_path):
320
  try:
321
+ # 创建存储目录
322
+ storage_dir = os.path.abspath("storage")
323
+ os.makedirs(storage_dir, exist_ok=True)
324
+
325
+ # 构建目标路径
326
+ target_dir = os.path.dirname(os.path.join(storage_dir, relative_path))
327
+ os.makedirs(target_dir, exist_ok=True)
328
 
329
+ target_path = os.path.join(storage_dir, relative_path)
 
 
 
330
 
331
+ # 复制文件
332
+ shutil.copy2(local_path, target_path)
333
 
334
+ # 返回完整URL
335
+ relative_url = f"/storage/{relative_path.replace(os.sep, '/')}"
336
+ full_url = f"{BASE_URL}{relative_url}"
337
+ return full_url
338
  except Exception as e:
339
+ logger.error(f"Error copying to local storage: {e}")
340
+ import traceback
341
+ logger.error(traceback.format_exc())
342
  return None
343
 
344
  @app.post("/api/generate-video")
345
  async def generate_video(comic_data: ComicData, background_tasks: BackgroundTasks):
346
  # 创建唯一项目ID
347
  project_id = str(uuid.uuid4())
348
+ # 使用绝对路径创建项目目录
349
+ project_dir = os.path.abspath(os.path.join("temp", project_id))
350
  os.makedirs(project_dir, exist_ok=True)
351
 
352
+ logger.info(f"Created project directory: {project_dir}")
353
+
354
  try:
355
  # 下载图片
356
  image_paths = []
357
+ for i, panel_url in enumerate(comic_data.panels):
358
+ output_path = os.path.join(project_dir, f"panel_{i}.jpg")
359
+ result = await download_image(panel_url, output_path)
360
+ if result:
361
+ image_paths.append(result)
 
 
 
362
 
363
  if not image_paths:
364
  raise HTTPException(status_code=500, detail="Failed to download images")
365
 
366
+ logger.info(f"Downloaded {len(image_paths)} images")
 
 
 
367
 
368
  # 创建音频文件
369
+ audio_file, audio_durations, panel_start_times, panel_durations = await create_audio_file(
370
+ project_dir, comic_data.captions, comic_data.speeches
371
+ )
372
  if not audio_file:
373
  raise HTTPException(status_code=500, detail="Failed to create audio file")
374
 
375
+ logger.info(f"Created audio file: {audio_file}")
376
+
377
+ # 创建字幕文件 - 分别为caption和speech创建
378
+ caption_subtitle_file = create_caption_subtitle_file(
379
+ project_dir, comic_data.captions, panel_start_times, panel_durations
380
+ )
381
+ if not caption_subtitle_file:
382
+ raise HTTPException(status_code=500, detail="Failed to create caption subtitle file")
383
+
384
+ speech_subtitle_file = create_speech_subtitle_file(
385
+ project_dir, comic_data.speeches, panel_start_times, panel_durations
386
+ )
387
+ if not speech_subtitle_file:
388
+ raise HTTPException(status_code=500, detail="Failed to create speech subtitle file")
389
+
390
+ logger.info(f"Created subtitle files: {caption_subtitle_file}, {speech_subtitle_file}")
391
+
392
  # 创建视频
393
  output_video = os.path.join(project_dir, "output.mp4")
394
+ result = create_video(
395
+ project_dir, image_paths, caption_subtitle_file, speech_subtitle_file,
396
+ audio_file, output_video, audio_durations, panel_start_times, panel_durations
397
+ )
398
  if not result:
399
  raise HTTPException(status_code=500, detail="Failed to create video")
400
 
401
+ logger.info(f"Created video: {output_video}")
 
402
 
403
+ # 上传视频
404
+ video_url = upload_to_local_storage(output_video, f"{project_id}/video.mp4")
405
+ # subtitle_url = upload_to_local_storage(subtitle_file, f"{project_id}/captions.srt")
406
+ # audio_url = upload_to_local_storage(audio_file, f"{project_id}/combined_audio.mp3")
407
 
408
  # 上传图片
409
+ # image_urls = []
410
+ # for i, img_path in enumerate(image_paths):
411
+ # remote_path = f"{project_id}/images/panel_{i}.jpg"
412
+ # img_url = upload_to_local_storage(img_path, remote_path)
413
+ # if img_url:
414
+ # image_urls.append(img_url)
415
 
416
  # 后台任务清理临时文件
417
  background_tasks.add_task(lambda: shutil.rmtree(project_dir, ignore_errors=True))
418
 
419
  return {
420
  "videoUrl": video_url,
421
+ # "subtitleUrl": subtitle_url,
422
+ # "audioUrl": audio_url,
423
+ # "imageUrls": image_urls,
424
  "projectId": project_id
425
  }
426
  except Exception as e:
427
  # 清理临时文件
428
  shutil.rmtree(project_dir, ignore_errors=True)
429
  logger.error(f"Error generating video: {e}")
430
+ import traceback
431
+ logger.error(traceback.format_exc())
432
  raise HTTPException(status_code=500, detail=str(e))
433
 
434
+ # 健康检查端点
435
  @app.get("/")
436
  async def health_check():
437
  return {"status": "ok"}
requirements.txt CHANGED
@@ -3,5 +3,4 @@ uvicorn>=0.21.1
3
  aiohttp>=3.8.4
4
  openai>=1.2.0
5
  python-multipart>=0.0.6
6
- webdavclient3>=3.14.6
7
  pydantic>=1.10.7
 
3
  aiohttp>=3.8.4
4
  openai>=1.2.0
5
  python-multipart>=0.0.6
 
6
  pydantic>=1.10.7