dseditor commited on
Commit
fe72b7c
·
verified ·
1 Parent(s): ac5e4c8

Upload 3 files

Browse files
Files changed (2) hide show
  1. app.py +81 -27
  2. video_merger.py +84 -0
app.py CHANGED
@@ -2,14 +2,16 @@ import gradio as gr
2
  import subprocess
3
  import os
4
  import tempfile
 
 
5
 
6
  def extract_last_frame(video_file):
7
  if video_file is None:
8
  return None, "請上傳影片檔案"
9
 
10
  try:
11
- # 創建臨時輸出檔案
12
- output_path = tempfile.mktemp(suffix='.jpg')
13
 
14
  # 使用 ffmpeg 獲取影片總幀數並提取最後一幀
15
  # 先獲取影片時長
@@ -32,14 +34,19 @@ def extract_last_frame(video_file):
32
  '-update', '1',
33
  '-frames:v', '1',
34
  '-q:v', '2',
35
- output_path,
36
  '-y'
37
  ]
38
 
39
  subprocess.run(extract_cmd, check=True, capture_output=True)
40
 
41
- if os.path.exists(output_path) and os.path.getsize(output_path) > 0:
42
- return output_path, "成功提取最後一幀!"
 
 
 
 
 
43
  else:
44
  return None, "提取失敗,請確認影片格式正確"
45
 
@@ -48,33 +55,80 @@ def extract_last_frame(video_file):
48
  except Exception as e:
49
  return None, f"發生錯誤: {str(e)}"
50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  # 創建 Gradio 界面
52
- with gr.Blocks(title="影片最後一幀提取器") as demo:
53
- gr.Markdown("# 影片最後一幀提取器")
54
- gr.Markdown("上傳影片(最大 100MB),自動提取最後一幀為 JPEG 圖片")
55
 
56
- with gr.Row():
57
- with gr.Column():
58
- video_input = gr.Video(
59
- label="上傳影片",
60
- max_length=None,
61
- height=400
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  )
63
- extract_btn = gr.Button("提取最後一幀", variant="primary")
64
 
65
- with gr.Column():
66
- image_output = gr.Image(
67
- label="最後一幀",
68
- type="filepath",
69
- height=400
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  )
71
- status_output = gr.Textbox(label="狀態", interactive=False)
72
-
73
- extract_btn.click(
74
- fn=extract_last_frame,
75
- inputs=[video_input],
76
- outputs=[image_output, status_output]
77
- )
78
 
79
  if __name__ == "__main__":
80
  demo.launch()
 
2
  import subprocess
3
  import os
4
  import tempfile
5
+ import shutil
6
+ from video_merger import merge_videos
7
 
8
  def extract_last_frame(video_file):
9
  if video_file is None:
10
  return None, "請上傳影片檔案"
11
 
12
  try:
13
+ # 創建固定名稱的輸出檔案
14
+ temp_output = tempfile.mktemp(suffix='.jpg')
15
 
16
  # 使用 ffmpeg 獲取影片總幀數並提取最後一幀
17
  # 先獲取影片時長
 
34
  '-update', '1',
35
  '-frames:v', '1',
36
  '-q:v', '2',
37
+ temp_output,
38
  '-y'
39
  ]
40
 
41
  subprocess.run(extract_cmd, check=True, capture_output=True)
42
 
43
+ if os.path.exists(temp_output) and os.path.getsize(temp_output) > 0:
44
+ # 重新命名為 lastframe.jpg
45
+ output_dir = tempfile.gettempdir()
46
+ final_output = os.path.join(output_dir, 'lastframe.jpg')
47
+ shutil.copy2(temp_output, final_output)
48
+ os.remove(temp_output)
49
+ return final_output, "成功提取最後一幀!"
50
  else:
51
  return None, "提取失敗,請確認影片格式正確"
52
 
 
55
  except Exception as e:
56
  return None, f"發生錯誤: {str(e)}"
57
 
58
+ def merge_videos_wrapper(video_files):
59
+ """包裝合併函數以處理檔名"""
60
+ if not video_files:
61
+ return None, "請上傳影片檔案"
62
+
63
+ temp_output, message = merge_videos(video_files)
64
+
65
+ if temp_output and os.path.exists(temp_output):
66
+ # 重新命名為 final.mp4
67
+ output_dir = tempfile.gettempdir()
68
+ final_output = os.path.join(output_dir, 'final.mp4')
69
+ shutil.copy2(temp_output, final_output)
70
+ os.remove(temp_output)
71
+ return final_output, message
72
+ else:
73
+ return None, message
74
+
75
  # 創建 Gradio 界面
76
+ with gr.Blocks(title="影片工具箱") as demo:
77
+ gr.Markdown("# 影片工具箱")
78
+ gr.Markdown("提供影片最後一幀提取和多段影片合併功能")
79
 
80
+ with gr.Tabs():
81
+ # Tab 1: 提取最後一幀
82
+ with gr.Tab("提取最後一幀"):
83
+ gr.Markdown("上傳影片(最大 100MB),自動提取最後一幀為 JPEG 圖片")
84
+ with gr.Row():
85
+ with gr.Column():
86
+ video_input = gr.Video(
87
+ label="上傳影片",
88
+ max_length=None,
89
+ height=400
90
+ )
91
+ extract_btn = gr.Button("提取最後一幀", variant="primary")
92
+
93
+ with gr.Column():
94
+ image_output = gr.Image(
95
+ label="最後一幀 (lastframe.jpg)",
96
+ type="filepath",
97
+ height=400
98
+ )
99
+ status_output = gr.Textbox(label="狀態", interactive=False)
100
+
101
+ extract_btn.click(
102
+ fn=extract_last_frame,
103
+ inputs=[video_input],
104
+ outputs=[image_output, status_output]
105
  )
 
106
 
107
+ # Tab 2: 合併影片
108
+ with gr.Tab("合併影片"):
109
+ gr.Markdown("上傳多個 MP4 影片,按照上傳順序合併為一個影片")
110
+ with gr.Row():
111
+ with gr.Column():
112
+ videos_input = gr.File(
113
+ label="上傳影片檔案(可多選)",
114
+ file_count="multiple",
115
+ file_types=["video"],
116
+ height=300
117
+ )
118
+ merge_btn = gr.Button("合併影片", variant="primary")
119
+
120
+ with gr.Column():
121
+ merged_video_output = gr.Video(
122
+ label="合併後的影片 (final.mp4)",
123
+ height=400
124
+ )
125
+ merge_status_output = gr.Textbox(label="狀態", interactive=False)
126
+
127
+ merge_btn.click(
128
+ fn=merge_videos_wrapper,
129
+ inputs=[videos_input],
130
+ outputs=[merged_video_output, merge_status_output]
131
  )
 
 
 
 
 
 
 
132
 
133
  if __name__ == "__main__":
134
  demo.launch()
video_merger.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import subprocess
2
+ import os
3
+ import tempfile
4
+
5
+ def merge_videos(video_files):
6
+ """
7
+ 合併多個 MP4 影片檔案
8
+
9
+ Args:
10
+ video_files: 影片檔案路徑列表
11
+
12
+ Returns:
13
+ tuple: (輸出檔案路徑, 狀態訊息)
14
+ """
15
+ if not video_files or len(video_files) == 0:
16
+ return None, "請至少上傳一個影片檔案"
17
+
18
+ if len(video_files) == 1:
19
+ return None, "請上傳至少兩個影片進行合併"
20
+
21
+ try:
22
+ # 創建臨時檔案列表
23
+ list_file = tempfile.mktemp(suffix='.txt')
24
+ output_path = tempfile.mktemp(suffix='.mp4')
25
+
26
+ # 寫入 ffmpeg concat 檔案列表
27
+ with open(list_file, 'w', encoding='utf-8') as f:
28
+ for video_file in video_files:
29
+ # 使用絕對路徑並轉義特殊字元
30
+ abs_path = os.path.abspath(video_file)
31
+ f.write(f"file '{abs_path}'\n")
32
+
33
+ # 使用 ffmpeg concat demuxer 合併影片
34
+ merge_cmd = [
35
+ 'ffmpeg',
36
+ '-f', 'concat',
37
+ '-safe', '0',
38
+ '-i', list_file,
39
+ '-c', 'copy',
40
+ output_path,
41
+ '-y'
42
+ ]
43
+
44
+ result = subprocess.run(merge_cmd, capture_output=True, text=True)
45
+
46
+ # 清理臨時列表檔案
47
+ if os.path.exists(list_file):
48
+ os.remove(list_file)
49
+
50
+ if result.returncode != 0:
51
+ # 如果直接複製失敗,嘗試重新編碼
52
+ merge_cmd_reencode = [
53
+ 'ffmpeg',
54
+ '-f', 'concat',
55
+ '-safe', '0',
56
+ '-i', list_file,
57
+ '-c:v', 'libx264',
58
+ '-c:a', 'aac',
59
+ '-preset', 'medium',
60
+ output_path,
61
+ '-y'
62
+ ]
63
+
64
+ # 重新創建列表檔案
65
+ with open(list_file, 'w', encoding='utf-8') as f:
66
+ for video_file in video_files:
67
+ abs_path = os.path.abspath(video_file)
68
+ f.write(f"file '{abs_path}'\n")
69
+
70
+ result = subprocess.run(merge_cmd_reencode, capture_output=True, text=True)
71
+
72
+ if os.path.exists(list_file):
73
+ os.remove(list_file)
74
+
75
+ if result.returncode != 0:
76
+ return None, f"合併失敗: {result.stderr}"
77
+
78
+ if os.path.exists(output_path) and os.path.getsize(output_path) > 0:
79
+ return output_path, f"成功合併 {len(video_files)} 個影片!"
80
+ else:
81
+ return None, "合併失敗,請確認影片格式正確"
82
+
83
+ except Exception as e:
84
+ return None, f"發生錯誤: {str(e)}"