zjrwtxtechstudio commited on
Commit
6888b5c
·
verified ·
1 Parent(s): 7795add

Upload 4 files

Browse files
Files changed (4) hide show
  1. app.py +329 -0
  2. app_gradio.py +329 -0
  3. readme.md +25 -0
  4. requirements.txt +5 -0
app.py ADDED
@@ -0,0 +1,329 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import time
3
+ import cv2
4
+ import imutils
5
+ import shutil
6
+ import img2pdf
7
+ import glob
8
+ from skimage.metrics import structural_similarity
9
+ import gradio as gr
10
+ import tempfile
11
+
12
+ ############# Define constants
13
+
14
+ OUTPUT_SLIDES_DIR = f"./output"
15
+
16
+ FRAME_RATE = 3 # no.of frames per second that needs to be processed, fewer the count faster the speed
17
+ WARMUP = FRAME_RATE # initial number of frames to be skipped
18
+ FGBG_HISTORY = FRAME_RATE * 15 # no.of frames in background object
19
+ VAR_THRESHOLD = 16 # Threshold on the squared Mahalanobis distance between the pixel and the model to decide whether a pixel is well described by the background model.
20
+ DETECT_SHADOWS = False # If true, the algorithm will detect shadows and mark them.
21
+ MIN_PERCENT = 0.1 # min % of diff between foreground and background to detect if motion has stopped
22
+ MAX_PERCENT = 3 # max % of diff between foreground and background to detect if frame is still in motion
23
+ SSIM_THRESHOLD = 0.9 # SSIM threshold of two consecutive frame
24
+
25
+
26
+ def get_frames(video_path):
27
+ '''A fucntion to return the frames from a video located at video_path
28
+ this function skips frames as defined in FRAME_RATE'''
29
+
30
+
31
+ # open a pointer to the video file initialize the width and height of the frame
32
+ vs = cv2.VideoCapture(video_path)
33
+ if not vs.isOpened():
34
+ raise Exception(f'unable to open file {video_path}')
35
+
36
+
37
+ total_frames = vs.get(cv2.CAP_PROP_FRAME_COUNT)
38
+ frame_time = 0
39
+ frame_count = 0
40
+
41
+ # loop over the frames of the video
42
+ while True:
43
+ vs.set(cv2.CAP_PROP_POS_MSEC, frame_time * 1000) # move frame to a timestamp
44
+ frame_time += 1/FRAME_RATE
45
+
46
+ (_, frame) = vs.read()
47
+ # if the frame is None, then we have reached the end of the video file
48
+ if frame is None:
49
+ break
50
+
51
+ frame_count += 1
52
+ yield frame_count, frame_time, frame
53
+
54
+ vs.release()
55
+
56
+
57
+
58
+ def detect_unique_screenshots(video_path, output_folder_screenshot_path, progress=gr.Progress()):
59
+ '''Extract unique screenshots from video'''
60
+ fgbg = cv2.createBackgroundSubtractorMOG2(history=FGBG_HISTORY, varThreshold=VAR_THRESHOLD,detectShadows=DETECT_SHADOWS)
61
+
62
+ captured = False
63
+ start_time = time.time()
64
+ (W, H) = (None, None)
65
+
66
+ # Get total frames for progress calculation
67
+ cap = cv2.VideoCapture(video_path)
68
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
69
+ cap.release()
70
+
71
+ screenshoots_count = 0
72
+ last_screenshot = None
73
+ saved_files = []
74
+
75
+ progress(0, desc="初始化视频处理...")
76
+
77
+ for frame_count, frame_time, frame in get_frames(video_path):
78
+ # Update progress
79
+ progress((frame_count / total_frames) * 0.7, desc=f"处理视频帧 {frame_count}/{total_frames}")
80
+
81
+ orig = frame.copy()
82
+ frame = imutils.resize(frame, width=600)
83
+ mask = fgbg.apply(frame)
84
+
85
+ if W is None or H is None:
86
+ (H, W) = mask.shape[:2]
87
+
88
+ p_diff = (cv2.countNonZero(mask) / float(W * H)) * 100
89
+
90
+ if p_diff < MIN_PERCENT and not captured and frame_count > WARMUP:
91
+ captured = True
92
+ filename = f"{screenshoots_count:03}_{round(frame_time/60, 2)}.png"
93
+ path = os.path.join(output_folder_screenshot_path, filename)
94
+
95
+ image_ssim = 0.0
96
+ if last_screenshot is not None:
97
+ image_ssim = structural_similarity(last_screenshot, orig, channel_axis=2, data_range=255)
98
+
99
+ if image_ssim < SSIM_THRESHOLD:
100
+ try:
101
+ progress(0.7 + (screenshoots_count * 0.1), desc=f"保存截图 {screenshoots_count + 1}")
102
+ print("saving {}".format(path))
103
+ cv2.imwrite(str(path), orig)
104
+ last_screenshot = orig
105
+ saved_files.append(path)
106
+ screenshoots_count += 1
107
+ except Exception as e:
108
+ print(f"Error saving image: {str(e)}")
109
+ continue
110
+
111
+ elif captured and p_diff >= MAX_PERCENT:
112
+ captured = False
113
+
114
+ progress(0.8, desc="截图提取完成")
115
+ print(f'{screenshoots_count} screenshots Captured!')
116
+ print(f'Time taken {time.time()-start_time}s')
117
+ return saved_files
118
+
119
+
120
+ def initialize_output_folder(video_path):
121
+ '''Clean the output folder if already exists'''
122
+ # Create a safe folder name from video filename
123
+ video_filename = os.path.splitext(os.path.basename(video_path))[0]
124
+ # Replace potentially problematic characters
125
+ safe_filename = "".join(x for x in video_filename if x.isalnum() or x in (' ', '-', '_'))
126
+ output_folder_screenshot_path = os.path.join(OUTPUT_SLIDES_DIR, safe_filename)
127
+
128
+ if os.path.exists(output_folder_screenshot_path):
129
+ shutil.rmtree(output_folder_screenshot_path)
130
+
131
+ os.makedirs(output_folder_screenshot_path, exist_ok=True)
132
+ print('initialized output folder', output_folder_screenshot_path)
133
+ return output_folder_screenshot_path
134
+
135
+
136
+ def convert_screenshots_to_pdf(video_path, output_folder_screenshot_path):
137
+ # Create a safe filename
138
+ video_filename = os.path.splitext(os.path.basename(video_path))[0]
139
+ safe_filename = "".join(x for x in video_filename if x.isalnum() or x in (' ', '-', '_'))
140
+ output_pdf_path = os.path.join(OUTPUT_SLIDES_DIR, f"{safe_filename}.pdf")
141
+
142
+ try:
143
+ print('output_folder_screenshot_path', output_folder_screenshot_path)
144
+ print('output_pdf_path', output_pdf_path)
145
+ print('converting images to pdf..')
146
+
147
+ # Get all PNG files and ensure they exist
148
+ png_files = sorted(glob.glob(os.path.join(output_folder_screenshot_path, "*.png")))
149
+ if not png_files:
150
+ raise Exception("No PNG files found to convert to PDF")
151
+
152
+ with open(output_pdf_path, "wb") as f:
153
+ f.write(img2pdf.convert(png_files))
154
+
155
+ print('Pdf Created!')
156
+ print('pdf saved at', output_pdf_path)
157
+ return output_pdf_path
158
+ except Exception as e:
159
+ print(f"Error creating PDF: {str(e)}")
160
+ raise
161
+
162
+
163
+ def video_to_slides(video_path, progress=gr.Progress()):
164
+ progress(0.1, desc="准备处理视频...")
165
+ output_folder_screenshot_path = initialize_output_folder(video_path)
166
+ saved_files = detect_unique_screenshots(video_path, output_folder_screenshot_path, progress)
167
+ return output_folder_screenshot_path, saved_files
168
+
169
+
170
+ def slides_to_pdf(video_path, output_folder_screenshot_path, saved_files, progress=gr.Progress()):
171
+ video_filename = os.path.splitext(os.path.basename(video_path))[0]
172
+ safe_filename = "".join(x for x in video_filename if x.isalnum() or x in (' ', '-', '_'))
173
+ output_pdf_path = os.path.join(OUTPUT_SLIDES_DIR, f"{safe_filename}.pdf")
174
+
175
+ try:
176
+ progress(0.9, desc="正在生成PDF...")
177
+ print('output_folder_screenshot_path', output_folder_screenshot_path)
178
+ print('output_pdf_path', output_pdf_path)
179
+
180
+ if not saved_files:
181
+ raise Exception("未从视频中捕获到截图")
182
+
183
+ existing_files = [f for f in saved_files if os.path.exists(f)]
184
+ if not existing_files:
185
+ raise Exception("未找到保存的截图文件")
186
+
187
+ with open(output_pdf_path, "wb") as f:
188
+ f.write(img2pdf.convert(existing_files))
189
+
190
+ progress(1.0, desc="处理完成!")
191
+ print('PDF创建成功!')
192
+ print('PDF保存位置:', output_pdf_path)
193
+ return output_pdf_path
194
+ except Exception as e:
195
+ print(f"创建PDF时出错: {str(e)}")
196
+ raise
197
+
198
+
199
+ def run_app(video_path, progress=gr.Progress()):
200
+ try:
201
+ if not video_path:
202
+ raise gr.Error("请选择要处理的视频文件")
203
+
204
+ progress(0, desc="开始处理...")
205
+ output_folder_screenshot_path, saved_files = video_to_slides(video_path, progress)
206
+ return slides_to_pdf(video_path, output_folder_screenshot_path, saved_files, progress)
207
+ except Exception as e:
208
+ raise gr.Error(f"处理失败: {str(e)}")
209
+
210
+
211
+ def process_video_file(video_file):
212
+ """Handle uploaded video file and return PDF"""
213
+ try:
214
+ # If video_file is a string (path), use it directly
215
+ if isinstance(video_file, str):
216
+ if video_file.strip() == "":
217
+ return None
218
+ return run_app(video_file)
219
+
220
+ # If it's an uploaded file, create a temporary file
221
+ if video_file is not None:
222
+ # Generate a unique filename for the temporary video
223
+ temp_filename = f"temp_video_{int(time.time())}.mp4"
224
+ temp_path = os.path.join(tempfile.gettempdir(), temp_filename)
225
+
226
+ try:
227
+ if hasattr(video_file, 'name'): # If it's already a file path
228
+ shutil.copyfile(video_file, temp_path)
229
+ else: # If it's file content
230
+ with open(temp_path, 'wb') as f:
231
+ f.write(video_file)
232
+
233
+ # Process the video
234
+ output_folder_screenshot_path, saved_files = video_to_slides(temp_path)
235
+ pdf_path = slides_to_pdf(temp_path, output_folder_screenshot_path, saved_files)
236
+
237
+ # Cleanup
238
+ if os.path.exists(temp_path):
239
+ os.unlink(temp_path)
240
+ return pdf_path
241
+
242
+ except Exception as e:
243
+ if os.path.exists(temp_path):
244
+ os.unlink(temp_path)
245
+ raise gr.Error(f"处理视频时出错: {str(e)}")
246
+ return None
247
+ except Exception as e:
248
+ raise gr.Error(f"处理视频时出错: {str(e)}")
249
+
250
+ # Create a modern interface with custom CSS
251
+ css = """
252
+ .gradio-container {
253
+ font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
254
+ }
255
+ .container {
256
+ max-width: 900px;
257
+ margin: auto;
258
+ padding: 20px;
259
+ }
260
+ .gr-button {
261
+ background: linear-gradient(90deg, #2563eb, #3b82f6);
262
+ border: none;
263
+ color: white;
264
+ }
265
+ .gr-button:hover {
266
+ background: linear-gradient(90deg, #1d4ed8, #2563eb);
267
+ transform: translateY(-1px);
268
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
269
+ }
270
+ .status-info {
271
+ margin-top: 10px;
272
+ padding: 10px;
273
+ border-radius: 4px;
274
+ background-color: #f3f4f6;
275
+ }
276
+ """
277
+
278
+ with gr.Blocks(css=css) as iface:
279
+ gr.Markdown(
280
+ """
281
+ # 🎥 视频转PDF智能助手
282
+
283
+ ### 轻松将视频转换为高质量PDF文档
284
+ 公众号:正经人王同学 | 全网同名
285
+ """
286
+ )
287
+
288
+ with gr.Row():
289
+ with gr.Column():
290
+ video_input = gr.Video(label="上传视频")
291
+ video_path = gr.Textbox(label="或输入视频路径", placeholder="例如: ./input/video.mp4")
292
+ convert_btn = gr.Button("开始转换", variant="primary")
293
+
294
+ with gr.Row():
295
+ output_file = gr.File(label="下载PDF")
296
+
297
+ with gr.Row():
298
+ status = gr.Markdown(value="", elem_classes=["status-info"])
299
+
300
+ gr.Markdown(
301
+ """
302
+ ### 使用说明
303
+ 1. 上传视频文件 或 输入视频文件路径
304
+ 2. 点击"开始转换"按钮
305
+ 3. 等待处理完成后下载生成的PDF文件
306
+
307
+ ### 特点
308
+ - 智能检测视频关键帧
309
+ - 高质量PDF输出
310
+ - 支持多种视频格式
311
+ """
312
+ )
313
+
314
+ def process_video(video, path):
315
+ if video:
316
+ return run_app(video)
317
+ elif path:
318
+ return run_app(path)
319
+ else:
320
+ raise gr.Error("请上传视频或输入视频路径")
321
+
322
+ convert_btn.click(
323
+ fn=process_video,
324
+ inputs=[video_input, video_path],
325
+ outputs=[output_file],
326
+ )
327
+
328
+ if __name__ == "__main__":
329
+ iface.launch()
app_gradio.py ADDED
@@ -0,0 +1,329 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import time
3
+ import cv2
4
+ import imutils
5
+ import shutil
6
+ import img2pdf
7
+ import glob
8
+ from skimage.metrics import structural_similarity
9
+ import gradio as gr
10
+ import tempfile
11
+
12
+ ############# Define constants
13
+
14
+ OUTPUT_SLIDES_DIR = f"./output"
15
+
16
+ FRAME_RATE = 3 # no.of frames per second that needs to be processed, fewer the count faster the speed
17
+ WARMUP = FRAME_RATE # initial number of frames to be skipped
18
+ FGBG_HISTORY = FRAME_RATE * 15 # no.of frames in background object
19
+ VAR_THRESHOLD = 16 # Threshold on the squared Mahalanobis distance between the pixel and the model to decide whether a pixel is well described by the background model.
20
+ DETECT_SHADOWS = False # If true, the algorithm will detect shadows and mark them.
21
+ MIN_PERCENT = 0.1 # min % of diff between foreground and background to detect if motion has stopped
22
+ MAX_PERCENT = 3 # max % of diff between foreground and background to detect if frame is still in motion
23
+ SSIM_THRESHOLD = 0.9 # SSIM threshold of two consecutive frame
24
+
25
+
26
+ def get_frames(video_path):
27
+ '''A fucntion to return the frames from a video located at video_path
28
+ this function skips frames as defined in FRAME_RATE'''
29
+
30
+
31
+ # open a pointer to the video file initialize the width and height of the frame
32
+ vs = cv2.VideoCapture(video_path)
33
+ if not vs.isOpened():
34
+ raise Exception(f'unable to open file {video_path}')
35
+
36
+
37
+ total_frames = vs.get(cv2.CAP_PROP_FRAME_COUNT)
38
+ frame_time = 0
39
+ frame_count = 0
40
+
41
+ # loop over the frames of the video
42
+ while True:
43
+ vs.set(cv2.CAP_PROP_POS_MSEC, frame_time * 1000) # move frame to a timestamp
44
+ frame_time += 1/FRAME_RATE
45
+
46
+ (_, frame) = vs.read()
47
+ # if the frame is None, then we have reached the end of the video file
48
+ if frame is None:
49
+ break
50
+
51
+ frame_count += 1
52
+ yield frame_count, frame_time, frame
53
+
54
+ vs.release()
55
+
56
+
57
+
58
+ def detect_unique_screenshots(video_path, output_folder_screenshot_path, progress=gr.Progress()):
59
+ '''Extract unique screenshots from video'''
60
+ fgbg = cv2.createBackgroundSubtractorMOG2(history=FGBG_HISTORY, varThreshold=VAR_THRESHOLD,detectShadows=DETECT_SHADOWS)
61
+
62
+ captured = False
63
+ start_time = time.time()
64
+ (W, H) = (None, None)
65
+
66
+ # Get total frames for progress calculation
67
+ cap = cv2.VideoCapture(video_path)
68
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
69
+ cap.release()
70
+
71
+ screenshoots_count = 0
72
+ last_screenshot = None
73
+ saved_files = []
74
+
75
+ progress(0, desc="初始化视频处理...")
76
+
77
+ for frame_count, frame_time, frame in get_frames(video_path):
78
+ # Update progress
79
+ progress((frame_count / total_frames) * 0.7, desc=f"处理视频帧 {frame_count}/{total_frames}")
80
+
81
+ orig = frame.copy()
82
+ frame = imutils.resize(frame, width=600)
83
+ mask = fgbg.apply(frame)
84
+
85
+ if W is None or H is None:
86
+ (H, W) = mask.shape[:2]
87
+
88
+ p_diff = (cv2.countNonZero(mask) / float(W * H)) * 100
89
+
90
+ if p_diff < MIN_PERCENT and not captured and frame_count > WARMUP:
91
+ captured = True
92
+ filename = f"{screenshoots_count:03}_{round(frame_time/60, 2)}.png"
93
+ path = os.path.join(output_folder_screenshot_path, filename)
94
+
95
+ image_ssim = 0.0
96
+ if last_screenshot is not None:
97
+ image_ssim = structural_similarity(last_screenshot, orig, channel_axis=2, data_range=255)
98
+
99
+ if image_ssim < SSIM_THRESHOLD:
100
+ try:
101
+ progress(0.7 + (screenshoots_count * 0.1), desc=f"保存截图 {screenshoots_count + 1}")
102
+ print("saving {}".format(path))
103
+ cv2.imwrite(str(path), orig)
104
+ last_screenshot = orig
105
+ saved_files.append(path)
106
+ screenshoots_count += 1
107
+ except Exception as e:
108
+ print(f"Error saving image: {str(e)}")
109
+ continue
110
+
111
+ elif captured and p_diff >= MAX_PERCENT:
112
+ captured = False
113
+
114
+ progress(0.8, desc="截图提取完成")
115
+ print(f'{screenshoots_count} screenshots Captured!')
116
+ print(f'Time taken {time.time()-start_time}s')
117
+ return saved_files
118
+
119
+
120
+ def initialize_output_folder(video_path):
121
+ '''Clean the output folder if already exists'''
122
+ # Create a safe folder name from video filename
123
+ video_filename = os.path.splitext(os.path.basename(video_path))[0]
124
+ # Replace potentially problematic characters
125
+ safe_filename = "".join(x for x in video_filename if x.isalnum() or x in (' ', '-', '_'))
126
+ output_folder_screenshot_path = os.path.join(OUTPUT_SLIDES_DIR, safe_filename)
127
+
128
+ if os.path.exists(output_folder_screenshot_path):
129
+ shutil.rmtree(output_folder_screenshot_path)
130
+
131
+ os.makedirs(output_folder_screenshot_path, exist_ok=True)
132
+ print('initialized output folder', output_folder_screenshot_path)
133
+ return output_folder_screenshot_path
134
+
135
+
136
+ def convert_screenshots_to_pdf(video_path, output_folder_screenshot_path):
137
+ # Create a safe filename
138
+ video_filename = os.path.splitext(os.path.basename(video_path))[0]
139
+ safe_filename = "".join(x for x in video_filename if x.isalnum() or x in (' ', '-', '_'))
140
+ output_pdf_path = os.path.join(OUTPUT_SLIDES_DIR, f"{safe_filename}.pdf")
141
+
142
+ try:
143
+ print('output_folder_screenshot_path', output_folder_screenshot_path)
144
+ print('output_pdf_path', output_pdf_path)
145
+ print('converting images to pdf..')
146
+
147
+ # Get all PNG files and ensure they exist
148
+ png_files = sorted(glob.glob(os.path.join(output_folder_screenshot_path, "*.png")))
149
+ if not png_files:
150
+ raise Exception("No PNG files found to convert to PDF")
151
+
152
+ with open(output_pdf_path, "wb") as f:
153
+ f.write(img2pdf.convert(png_files))
154
+
155
+ print('Pdf Created!')
156
+ print('pdf saved at', output_pdf_path)
157
+ return output_pdf_path
158
+ except Exception as e:
159
+ print(f"Error creating PDF: {str(e)}")
160
+ raise
161
+
162
+
163
+ def video_to_slides(video_path, progress=gr.Progress()):
164
+ progress(0.1, desc="准备处理视频...")
165
+ output_folder_screenshot_path = initialize_output_folder(video_path)
166
+ saved_files = detect_unique_screenshots(video_path, output_folder_screenshot_path, progress)
167
+ return output_folder_screenshot_path, saved_files
168
+
169
+
170
+ def slides_to_pdf(video_path, output_folder_screenshot_path, saved_files, progress=gr.Progress()):
171
+ video_filename = os.path.splitext(os.path.basename(video_path))[0]
172
+ safe_filename = "".join(x for x in video_filename if x.isalnum() or x in (' ', '-', '_'))
173
+ output_pdf_path = os.path.join(OUTPUT_SLIDES_DIR, f"{safe_filename}.pdf")
174
+
175
+ try:
176
+ progress(0.9, desc="正在生成PDF...")
177
+ print('output_folder_screenshot_path', output_folder_screenshot_path)
178
+ print('output_pdf_path', output_pdf_path)
179
+
180
+ if not saved_files:
181
+ raise Exception("未从视频中捕获到截图")
182
+
183
+ existing_files = [f for f in saved_files if os.path.exists(f)]
184
+ if not existing_files:
185
+ raise Exception("未找到保存的截图文件")
186
+
187
+ with open(output_pdf_path, "wb") as f:
188
+ f.write(img2pdf.convert(existing_files))
189
+
190
+ progress(1.0, desc="处理完成!")
191
+ print('PDF创建成功!')
192
+ print('PDF保存位置:', output_pdf_path)
193
+ return output_pdf_path
194
+ except Exception as e:
195
+ print(f"创建PDF时出错: {str(e)}")
196
+ raise
197
+
198
+
199
+ def run_app(video_path, progress=gr.Progress()):
200
+ try:
201
+ if not video_path:
202
+ raise gr.Error("请选择要处理的视频文件")
203
+
204
+ progress(0, desc="开始处理...")
205
+ output_folder_screenshot_path, saved_files = video_to_slides(video_path, progress)
206
+ return slides_to_pdf(video_path, output_folder_screenshot_path, saved_files, progress)
207
+ except Exception as e:
208
+ raise gr.Error(f"处理失败: {str(e)}")
209
+
210
+
211
+ def process_video_file(video_file):
212
+ """Handle uploaded video file and return PDF"""
213
+ try:
214
+ # If video_file is a string (path), use it directly
215
+ if isinstance(video_file, str):
216
+ if video_file.strip() == "":
217
+ return None
218
+ return run_app(video_file)
219
+
220
+ # If it's an uploaded file, create a temporary file
221
+ if video_file is not None:
222
+ # Generate a unique filename for the temporary video
223
+ temp_filename = f"temp_video_{int(time.time())}.mp4"
224
+ temp_path = os.path.join(tempfile.gettempdir(), temp_filename)
225
+
226
+ try:
227
+ if hasattr(video_file, 'name'): # If it's already a file path
228
+ shutil.copyfile(video_file, temp_path)
229
+ else: # If it's file content
230
+ with open(temp_path, 'wb') as f:
231
+ f.write(video_file)
232
+
233
+ # Process the video
234
+ output_folder_screenshot_path, saved_files = video_to_slides(temp_path)
235
+ pdf_path = slides_to_pdf(temp_path, output_folder_screenshot_path, saved_files)
236
+
237
+ # Cleanup
238
+ if os.path.exists(temp_path):
239
+ os.unlink(temp_path)
240
+ return pdf_path
241
+
242
+ except Exception as e:
243
+ if os.path.exists(temp_path):
244
+ os.unlink(temp_path)
245
+ raise gr.Error(f"处理视频时出错: {str(e)}")
246
+ return None
247
+ except Exception as e:
248
+ raise gr.Error(f"处理视频时出错: {str(e)}")
249
+
250
+ # Create a modern interface with custom CSS
251
+ css = """
252
+ .gradio-container {
253
+ font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
254
+ }
255
+ .container {
256
+ max-width: 900px;
257
+ margin: auto;
258
+ padding: 20px;
259
+ }
260
+ .gr-button {
261
+ background: linear-gradient(90deg, #2563eb, #3b82f6);
262
+ border: none;
263
+ color: white;
264
+ }
265
+ .gr-button:hover {
266
+ background: linear-gradient(90deg, #1d4ed8, #2563eb);
267
+ transform: translateY(-1px);
268
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
269
+ }
270
+ .status-info {
271
+ margin-top: 10px;
272
+ padding: 10px;
273
+ border-radius: 4px;
274
+ background-color: #f3f4f6;
275
+ }
276
+ """
277
+
278
+ with gr.Blocks(css=css) as iface:
279
+ gr.Markdown(
280
+ """
281
+ # 🎥 视频转PDF智能助手
282
+
283
+ ### 轻松将视频转换为高质量PDF文档
284
+ 公众号:正经人王同学 | 全网同名
285
+ """
286
+ )
287
+
288
+ with gr.Row():
289
+ with gr.Column():
290
+ video_input = gr.Video(label="上传视频")
291
+ video_path = gr.Textbox(label="或输入视频路径", placeholder="例如: ./input/video.mp4")
292
+ convert_btn = gr.Button("开始转换", variant="primary")
293
+
294
+ with gr.Row():
295
+ output_file = gr.File(label="下载PDF")
296
+
297
+ with gr.Row():
298
+ status = gr.Markdown(value="", elem_classes=["status-info"])
299
+
300
+ gr.Markdown(
301
+ """
302
+ ### 使用说明
303
+ 1. 上传视频文件 或 输入视频文件路径
304
+ 2. 点击"开始转换"按钮
305
+ 3. 等待处理完成后下载生成的PDF文件
306
+
307
+ ### 特点
308
+ - 智能检测视频关键帧
309
+ - 高质量PDF输出
310
+ - 支持多种视频格式
311
+ """
312
+ )
313
+
314
+ def process_video(video, path):
315
+ if video:
316
+ return run_app(video)
317
+ elif path:
318
+ return run_app(path)
319
+ else:
320
+ raise gr.Error("请上传视频或输入视频路径")
321
+
322
+ convert_btn.click(
323
+ fn=process_video,
324
+ inputs=[video_input, video_path],
325
+ outputs=[output_file],
326
+ )
327
+
328
+ if __name__ == "__main__":
329
+ iface.launch()
readme.md ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 视频转PDF智能助手
2
+
3
+ 这是一个基于 Gradio 的 Web 应用,可以将视频自动转换为 PDF 文档。应用会智能检测视频中的关键帧,并将其转换为高质量的 PDF 文件。
4
+
5
+ ## 功能特点
6
+
7
+ - 智能检测视频关键帧
8
+ - 自动生成高质量PDF
9
+ - 支持多种视频格式
10
+ - 简单易用的Web界面
11
+
12
+ ## 使用方法
13
+
14
+ 1. 上传视频文件或输入视频路径
15
+ 2. 点击"开始转换"按钮
16
+ 3. 等待处理完成后下载生成的PDF
17
+
18
+ ## 技术栈
19
+
20
+ - Python
21
+ - OpenCV
22
+ - Gradio
23
+ - scikit-image
24
+ - img2pdf
25
+
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ opencv-python==4.8.1.78
2
+ imutils==0.5.4
3
+ scikit-image==0.22.0
4
+ gradio==4.8.0
5
+ img2pdf==0.5.1