Ryanus commited on
Commit
758e10b
·
verified ·
1 Parent(s): 41cc9f0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +274 -35
app.py CHANGED
@@ -4,16 +4,124 @@ import tempfile
4
  import random
5
  import subprocess
6
  import shutil
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
 
8
  def ffmpeg_cut_video(input_path, start_time, duration, output_path):
9
  command = [
10
- 'ffmpeg',
11
- '-ss', str(start_time),
12
- '-i', input_path,
13
- '-t', str(duration),
14
- '-c', 'copy',
15
- '-y',
16
- output_path
17
  ]
18
  process = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
19
  return process.returncode == 0
@@ -26,13 +134,8 @@ def concat_videos_ffmpeg(file_list, output_path):
26
  list_file.close()
27
 
28
  command = [
29
- 'ffmpeg',
30
- '-f', 'concat',
31
- '-safe', '0',
32
- '-i', list_file.name,
33
- '-c', 'copy',
34
- '-y',
35
- output_path
36
  ]
37
  process = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
38
  return process.returncode == 0
@@ -46,13 +149,12 @@ def extract_clips_from_videos(video_files, clip_duration):
46
  for idx, video_file in enumerate(video_files):
47
  video_path = video_file.name
48
  cmd_duration = [
49
- 'ffprobe', '-v', 'error',
50
- '-show_entries', 'format=duration',
51
- '-of', 'default=noprint_wrappers=1:nokey=1',
52
- video_path
53
  ]
54
  result = subprocess.run(cmd_duration, capture_output=True, text=True)
55
  total_duration = float(result.stdout.strip())
 
56
  start = 0.0
57
  count = 0
58
  while start < total_duration:
@@ -74,6 +176,7 @@ def generate_mixed_videos(clips, num_output_videos):
74
  random.shuffle(clips)
75
  clips_per_video = max(1, len(clips) // num_output_videos)
76
  output_files = []
 
77
  try:
78
  for i in range(num_output_videos):
79
  start_idx = i * clips_per_video
@@ -89,34 +192,170 @@ def generate_mixed_videos(clips, num_output_videos):
89
  shutil.rmtree(temp_dir, ignore_errors=True)
90
  raise
91
 
92
- def process_auto_mixing(video_files, clip_duration, num_output_videos):
93
  if not video_files or len(video_files) == 0:
94
- return "❌ 请上传至少一个视频文件", []
 
95
  try:
 
96
  clips, clips_temp_dir = extract_clips_from_videos(video_files, clip_duration)
97
  output_files, output_temp_dir = generate_mixed_videos(clips, num_output_videos)
98
- # 清理临时切割片段目录
99
  shutil.rmtree(clips_temp_dir, ignore_errors=True)
100
- return f"成功生成 {len(output_files)} 个自动混剪视频", output_files
101
- except Exception as e:
102
- return f"❌ 处理失败: {str(e)}", []
 
 
 
 
 
 
 
 
 
 
 
 
103
 
104
- def main():
105
- with gr.Blocks() as demo:
106
- gr.Markdown("# FFmpeg 自动剪辑 + 长视频自动切片 + 自动混剪")
 
107
 
108
- video_input = gr.File(label="上传视频文件", file_types=[".mp4", ".mov", ".avi"], file_count="multiple")
109
- clip_duration = gr.Number(value=8, label="单个剪辑片段时长(秒)", minimum=1, maximum=60)
110
- num_output = gr.Number(value=3, label="生成混剪视频数量", minimum=1, maximum=10)
111
- btn = gr.Button("开始自动剪辑并混剪")
112
 
113
- status_box = gr.Textbox(label="处理状态", interactive=False, lines=4)
114
- output_files = gr.File(label="下载混剪视频文件", file_count="multiple")
115
 
116
- btn.click(process_auto_mixing, inputs=[video_input, clip_duration, num_output], outputs=[status_box, output_files])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  demo.launch()
119
 
120
  if __name__ == "__main__":
121
  main()
122
-
 
4
  import random
5
  import subprocess
6
  import shutil
7
+ import json
8
+ import pickle
9
+ from datetime import datetime
10
+
11
+ # Google API 导入
12
+ try:
13
+ from googleapiclient.discovery import build
14
+ from googleapiclient.http import MediaFileUpload
15
+ from google_auth_oauthlib.flow import InstalledAppFlow
16
+ from google.auth.transport.requests import Request
17
+ YOUTUBE_AVAILABLE = True
18
+ except ImportError:
19
+ YOUTUBE_AVAILABLE = False
20
+
21
+ # 内置Google Cloud凭据(用户无需设置)
22
+ CLIENT_SECRETS = {
23
+ "installed": {
24
+ "client_id": "288005932157-k7dhq8c7rgmt8km0nm50ntuif9lse7j1.apps.googleusercontent.com",
25
+ "project_id": "corded-palisade-458105-p7",
26
+ "auth_uri": "https://accounts.google.com/o/oauth2/auth",
27
+ "token_uri": "https://oauth2.googleapis.com/token",
28
+ "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
29
+ "client_secret": "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",
30
+ "redirect_uris": ["http://localhost"]
31
+ }
32
+ }
33
+
34
+ SCOPES = ['https://www.googleapis.com/auth/youtube.upload']
35
+ CREDENTIALS_FILE = 'youtube_token.pickle'
36
+
37
+ class SimpleYouTubeUploader:
38
+ def __init__(self):
39
+ self.youtube = None
40
+ self.authenticated = False
41
+
42
+ def authenticate(self):
43
+ if not YOUTUBE_AVAILABLE:
44
+ return False, "❌ YouTube API库未安装"
45
+
46
+ try:
47
+ creds = None
48
+ if os.path.exists(CREDENTIALS_FILE):
49
+ with open(CREDENTIALS_FILE, 'rb') as token:
50
+ creds = pickle.load(token)
51
+
52
+ if not creds or not creds.valid:
53
+ if creds and creds.expired and creds.refresh_token:
54
+ creds.refresh(Request())
55
+ else:
56
+ # 创建临时凭据文件
57
+ temp_secrets = tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False)
58
+ json.dump(CLIENT_SECRETS, temp_secrets)
59
+ temp_secrets.close()
60
+
61
+ flow = InstalledAppFlow.from_client_secrets_file(temp_secrets.name, SCOPES)
62
+ creds = flow.run_local_server(port=0, open_browser=False)
63
+
64
+ # 清理临时文件
65
+ os.unlink(temp_secrets.name)
66
+
67
+ with open(CREDENTIALS_FILE, 'wb') as token:
68
+ pickle.dump(creds, token)
69
+
70
+ self.youtube = build('youtube', 'v3', credentials=creds)
71
+ self.authenticated = True
72
+ return True, "✅ YouTube认证成功"
73
+
74
+ except Exception as e:
75
+ return False, f"❌ 认证失败: {str(e)}"
76
+
77
+ def upload_video(self, video_path, title, description="", privacy="private"):
78
+ if not self.authenticated:
79
+ return {"success": False, "error": "请先完成认证"}
80
+
81
+ if not os.path.exists(video_path):
82
+ return {"success": False, "error": f"文件不存在: {video_path}"}
83
+
84
+ body = {
85
+ 'snippet': {
86
+ 'title': title,
87
+ 'description': description,
88
+ 'tags': ["混剪", "自动生成", "短视频", "创意"],
89
+ 'categoryId': '22'
90
+ },
91
+ 'status': {
92
+ 'privacyStatus': privacy,
93
+ 'selfDeclaredMadeForKids': False
94
+ }
95
+ }
96
+
97
+ try:
98
+ media = MediaFileUpload(video_path, resumable=True, mimetype='video/*')
99
+ request = self.youtube.videos().insert(
100
+ part=','.join(body.keys()),
101
+ body=body,
102
+ media_body=media
103
+ )
104
+
105
+ response = request.execute()
106
+ video_id = response['id']
107
+ url = f"https://www.youtube.com/watch?v={video_id}"
108
+
109
+ return {
110
+ "success": True,
111
+ "video_id": video_id,
112
+ "url": url,
113
+ "title": title
114
+ }
115
+ except Exception as e:
116
+ return {"success": False, "error": str(e)}
117
+
118
+ # 创建全局上传器实例
119
+ youtube_uploader = SimpleYouTubeUploader()
120
 
121
  def ffmpeg_cut_video(input_path, start_time, duration, output_path):
122
  command = [
123
+ 'ffmpeg', '-ss', str(start_time), '-i', input_path,
124
+ '-t', str(duration), '-c', 'copy', '-y', output_path
 
 
 
 
 
125
  ]
126
  process = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
127
  return process.returncode == 0
 
134
  list_file.close()
135
 
136
  command = [
137
+ 'ffmpeg', '-f', 'concat', '-safe', '0', '-i', list_file.name,
138
+ '-c', 'copy', '-y', output_path
 
 
 
 
 
139
  ]
140
  process = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
141
  return process.returncode == 0
 
149
  for idx, video_file in enumerate(video_files):
150
  video_path = video_file.name
151
  cmd_duration = [
152
+ 'ffprobe', '-v', 'error', '-show_entries', 'format=duration',
153
+ '-of', 'default=noprint_wrappers=1:nokey=1', video_path
 
 
154
  ]
155
  result = subprocess.run(cmd_duration, capture_output=True, text=True)
156
  total_duration = float(result.stdout.strip())
157
+
158
  start = 0.0
159
  count = 0
160
  while start < total_duration:
 
176
  random.shuffle(clips)
177
  clips_per_video = max(1, len(clips) // num_output_videos)
178
  output_files = []
179
+
180
  try:
181
  for i in range(num_output_videos):
182
  start_idx = i * clips_per_video
 
192
  shutil.rmtree(temp_dir, ignore_errors=True)
193
  raise
194
 
195
+ def process_and_upload(video_files, clip_duration, num_output_videos, upload_to_youtube, privacy_setting):
196
  if not video_files or len(video_files) == 0:
197
+ return "❌ 请上传至少一个视频文件", [], ""
198
+
199
  try:
200
+ # 1. 视频处理
201
  clips, clips_temp_dir = extract_clips_from_videos(video_files, clip_duration)
202
  output_files, output_temp_dir = generate_mixed_videos(clips, num_output_videos)
 
203
  shutil.rmtree(clips_temp_dir, ignore_errors=True)
204
+
205
+ status_msg = f"✅ 成功生成 {len(output_files)} 个混剪视频"
206
+ upload_result = ""
207
+
208
+ # 2. YouTube上传(如果勾选)
209
+ if upload_to_youtube and YOUTUBE_AVAILABLE:
210
+ auth_success, auth_msg = youtube_uploader.authenticate()
211
+ upload_result += auth_msg + "\n\n"
212
+
213
+ if auth_success:
214
+ upload_result += "🚀 开始上传到YouTube...\n\n"
215
+
216
+ for i, video_path in enumerate(output_files, 1):
217
+ title = f"自动混剪视频 #{i} - {datetime.now().strftime('%Y%m%d_%H%M')}"
218
+ description = f"""🎬 自动生成的混剪视频
219
 
220
+ 📹 **制作信息:**
221
+ 切片时长:{clip_duration}秒
222
+ • 生成时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
223
+ • 自动随机混剪
224
 
225
+ 🔧 **制作工具:**
226
+ FFmpeg 自动剪辑
227
+ Python 批量处理
228
+ 智能片段组合
229
 
230
+ 👍 **如果喜欢请点赞支持!**
 
231
 
232
+ #混剪 #自动生成 #短视频 #创意 #FFmpeg"""
233
+
234
+ result = youtube_uploader.upload_video(
235
+ video_path=video_path,
236
+ title=title,
237
+ description=description,
238
+ privacy=privacy_setting
239
+ )
240
+
241
+ if result["success"]:
242
+ upload_result += f"✅ 视频 {i} 上传成功\n🔗 {result['url']}\n\n"
243
+ else:
244
+ upload_result += f"❌ 视频 {i} 上传失败: {result['error']}\n\n"
245
+ else:
246
+ upload_result += "❌ YouTube认证失败,无法上传"
247
+
248
+ elif upload_to_youtube and not YOUTUBE_AVAILABLE:
249
+ upload_result = "⚠️ YouTube API库未安装,无法上传"
250
+
251
+ return status_msg, output_files, upload_result
252
+
253
+ except Exception as e:
254
+ return f"❌ 处理失败: {str(e)}", [], ""
255
 
256
+ def main():
257
+ with gr.Blocks(
258
+ title="FFmpeg自动剪辑+YouTube一键上传",
259
+ theme=gr.themes.Soft()
260
+ ) as demo:
261
+
262
+ gr.HTML("""
263
+ <div style="text-align: center; padding: 20px; background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); border-radius: 10px; color: white; margin-bottom: 20px;">
264
+ <h1>🎬 FFmpeg 自动剪辑 + YouTube 一键上传</h1>
265
+ <p style="margin: 10px 0 0 0;">长视频自动切片 → 智能混剪 → 一键上传YouTube</p>
266
+ </div>
267
+ """)
268
+
269
+ with gr.Row():
270
+ with gr.Column(scale=2):
271
+ video_input = gr.File(
272
+ label="📤 上传视频文件",
273
+ file_types=[".mp4", ".mov", ".avi"],
274
+ file_count="multiple",
275
+ height=100
276
+ )
277
+
278
+ with gr.Row():
279
+ clip_duration = gr.Number(
280
+ value=3,
281
+ label="切片时长(秒)",
282
+ minimum=1,
283
+ maximum=30
284
+ )
285
+ num_output = gr.Number(
286
+ value=3,
287
+ label="生成视频数",
288
+ minimum=1,
289
+ maximum=6
290
+ )
291
+
292
+ with gr.Row():
293
+ upload_youtube = gr.Checkbox(
294
+ label="🚀 一键上传到YouTube",
295
+ value=False
296
+ )
297
+ privacy_setting = gr.Dropdown(
298
+ choices=["private", "unlisted", "public"],
299
+ value="private",
300
+ label="YouTube隐私设置"
301
+ )
302
+
303
+ process_btn = gr.Button(
304
+ "🎬 开始处理并上传",
305
+ variant="primary",
306
+ size="lg"
307
+ )
308
+
309
+ with gr.Column(scale=1):
310
+ status_output = gr.Textbox(
311
+ label="📊 处理状态",
312
+ lines=6,
313
+ interactive=False
314
+ )
315
+
316
+ with gr.Row():
317
+ with gr.Column():
318
+ output_files = gr.File(
319
+ label="📁 下载生成的视频",
320
+ file_count="multiple"
321
+ )
322
+
323
+ with gr.Column():
324
+ upload_result = gr.Textbox(
325
+ label="🚀 YouTube上传结果",
326
+ lines=10,
327
+ interactive=False,
328
+ show_copy_button=True
329
+ )
330
+
331
+ process_btn.click(
332
+ fn=process_and_upload,
333
+ inputs=[video_input, clip_duration, num_output, upload_youtube, privacy_setting],
334
+ outputs=[status_output, output_files, upload_result]
335
+ )
336
+
337
+ gr.Markdown("""
338
+ ---
339
+ ### 📖 使用说明
340
+
341
+ **🎬 视频处理:**
342
+ - 上传一个或多个视频文件
343
+ - 自动按指定时长切片
344
+ - 随机重组生成新的混剪视频
345
+
346
+ **🚀 YouTube上传:**
347
+ - 勾选"一键上传到YouTube"
348
+ - **首次使用**:会弹出授权链接,需要在浏览器中登录Google账号授权
349
+ - **后续使用**:直接上传,无需重复授权
350
+ - 支持设置视频隐私级别
351
+
352
+ **⚠️ 注意事项:**
353
+ - 需要环境安装FFmpeg
354
+ - YouTube上传需要Google账号授权(仅需授权一次)
355
+ - 建议先设为私密视频测试
356
+ """)
357
+
358
  demo.launch()
359
 
360
  if __name__ == "__main__":
361
  main()