deeme commited on
Commit
c0372f3
·
verified ·
1 Parent(s): 400e08a

Upload 4 files

Browse files
Files changed (4) hide show
  1. Dockerfile +25 -10
  2. README.md +3 -4
  3. app.py +306 -0
  4. requirements.txt +7 -0
Dockerfile CHANGED
@@ -1,10 +1,25 @@
1
- FROM node:20
2
-
3
- # 安装 http-server
4
- RUN npm install -g http-server
5
-
6
- # 创建一个简单的 HTML 文件来实现跳转
7
- RUN echo '<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0; url=https://comic.168369.xyz/"></head><body></body></html>' > /index.html
8
-
9
- # 启动 http-server 以提供该文件
10
- CMD ["http-server", "-p", "3000", "-c-1"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # 安装FFmpeg
6
+ RUN apt-get update && \
7
+ apt-get install -y ffmpeg && \
8
+ apt-get clean && \
9
+ rm -rf /var/lib/apt/lists/*
10
+
11
+ # 安装Python依赖
12
+ COPY requirements.txt .
13
+ RUN pip install --no-cache-dir -r requirements.txt
14
+
15
+ # 复制应用代码
16
+ COPY . .
17
+
18
+ # 创建必要的目录
19
+ RUN mkdir -p temp
20
+
21
+ # 暴露端口
22
+ EXPOSE 8000
23
+
24
+ # 运行应用
25
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
README.md CHANGED
@@ -1,10 +1,9 @@
1
  ---
2
- title: Comic
3
  emoji: 👩‍🎨
4
  colorFrom: red
5
  colorTo: yellow
6
  sdk: docker
7
  pinned: false
8
- app_port: 3000
9
- ---
10
-
 
1
  ---
2
+ title: comic
3
  emoji: 👩‍🎨
4
  colorFrom: red
5
  colorTo: yellow
6
  sdk: docker
7
  pinned: false
8
+ app_port: 8000
9
+ ---
 
app.py ADDED
@@ -0,0 +1,306 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
8
+ import asyncio
9
+ 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)
19
+ logger = logging.getLogger(__name__)
20
+
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
30
+ if OPENAI_BASE_URL:
31
+ openai.api_base = OPENAI_BASE_URL
32
+
33
+ app = FastAPI()
34
+
35
+ # 配置CORS
36
+ app.add_middleware(
37
+ CORSMiddleware,
38
+ allow_origins=["*"], # 生产环境中应该限制来源
39
+ allow_credentials=True,
40
+ allow_methods=["*"],
41
+ allow_headers=["*"],
42
+ )
43
+
44
+ # 请求模型
45
+ class ComicData(BaseModel):
46
+ captions: List[str]
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
73
+
74
+ # 生成语音
75
+ async def generate_speech(text, voice="alloy", output_path=None):
76
+ try:
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格式
164
+ def format_time(seconds):
165
+ hours = int(seconds / 3600)
166
+ minutes = int((seconds % 3600) / 60)
167
+ secs = int(seconds % 60)
168
+ millisecs = int((seconds - int(seconds)) * 1000)
169
+ return f"{hours:02}:{minutes:02}:{secs:02},{millisecs:03}"
170
+
171
+ # 创建音频文件
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"}
303
+
304
+ if __name__ == "__main__":
305
+ import uvicorn
306
+ uvicorn.run(app, host="0.0.0.0", port=8000)
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ fastapi>=0.95.0
2
+ 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