Nx-Neuralon commited on
Commit
5d3abae
·
verified ·
1 Parent(s): c90ca9c

Update app/video_preprocess.py

Browse files
Files changed (1) hide show
  1. app/video_preprocess.py +142 -159
app/video_preprocess.py CHANGED
@@ -1,159 +1,142 @@
1
- from __future__ import annotations
2
-
3
- import os
4
- import shutil
5
- import subprocess
6
- from dataclasses import dataclass
7
- from datetime import datetime
8
-
9
-
10
- @dataclass
11
- class PreprocessResult:
12
- input_path: str
13
- output_path: str
14
- used_ffmpeg: bool
15
- message: str
16
- mode: str
17
- ffmpeg_cmd: list[str]
18
-
19
-
20
- def check_ffmpeg_available() -> bool:
21
- return shutil.which("ffmpeg") is not None
22
-
23
-
24
- def ensure_dir(path: str) -> None:
25
- os.makedirs(path, exist_ok=True)
26
-
27
-
28
- def build_preprocessed_video_path(output_dir: str, input_path: str, mode: str) -> str:
29
- ensure_dir(output_dir)
30
- stem = os.path.splitext(os.path.basename(input_path))[0]
31
- ts = datetime.now().strftime("%Y%m%d_%H%M%S")
32
- return os.path.join(output_dir, f"{stem}_{mode}_preprocessed_{ts}.mp4")
33
-
34
-
35
- def _build_ffmpeg_cmd(
36
- input_path: str,
37
- output_path: str,
38
- *,
39
- mode: str = "analysis",
40
- remove_audio: bool = False,
41
- ) -> list[str]:
42
- """
43
- 三种模式:
44
- - preview: 调试/预览,体积更小
45
- - analysis: 正式分析,推荐,尽量保真
46
- - archival: 更高保真,体积更大
47
-
48
- 说明:
49
- - 音频默认保留
50
- - 使用 H.264 + AAC,兼容性最好
51
- - 使用 CRF 控质量;CRF 越小越保真
52
- """
53
- mode = (mode or "analysis").lower().strip()
54
- if mode not in {"preview", "analysis", "archival"}:
55
- raise ValueError(f"不支持的预处理模式: {mode}")
56
-
57
- if mode == "preview":
58
- # 调试演示用,较小
59
- width = 640
60
- fps = 12
61
- crf = 26
62
- preset = "veryfast"
63
- audio_bitrate = "96k"
64
- elif mode == "analysis":
65
- # 推荐:正式分析档
66
- width = 1280
67
- fps = 15
68
- crf = 18
69
- preset = "slow"
70
- audio_bitrate = "128k"
71
- else: # archival
72
- # 更高保真
73
- width = 1280
74
- fps = 20
75
- crf = 16
76
- preset = "slow"
77
- audio_bitrate = "160k"
78
-
79
- # 保持纵横比,高度自动偶数对齐
80
- vf = f"scale='min({width},iw)':-2,fps={fps}"
81
-
82
- cmd = [
83
- "ffmpeg",
84
- "-y",
85
- "-i", input_path,
86
- "-vf", vf,
87
- "-c:v", "libx264",
88
- "-preset", preset,
89
- "-crf", str(crf),
90
- "-movflags", "+faststart",
91
- "-pix_fmt", "yuv420p",
92
- ]
93
-
94
- if remove_audio:
95
- cmd += ["-an"]
96
- else:
97
- # 明确保留音频
98
- cmd += [
99
- "-c:a", "aac",
100
- "-b:a", audio_bitrate,
101
- "-ac", "1",
102
- "-ar", "16000",
103
- ]
104
-
105
- cmd.append(output_path)
106
- return cmd
107
-
108
-
109
- def preprocess_video(
110
- input_path: str,
111
- output_dir: str,
112
- mode: str = "analysis",
113
- remove_audio: bool = False,
114
- ) -> PreprocessResult:
115
- """
116
- 使用 ffmpeg 对视频做轻量预处理。
117
-
118
- 推荐:
119
- - 正式分析:mode="analysis"
120
- - 预览调试:mode="preview"
121
- - 高保真留档:mode="archival"
122
-
123
- 注意:
124
- - 这里默认保留音频
125
- - 若 remove_audio=True,会显式去掉音频
126
- """
127
- if not os.path.exists(input_path):
128
- raise FileNotFoundError(f"视频文件不存在: {input_path}")
129
-
130
- if not check_ffmpeg_available():
131
- raise RuntimeError("未检测到 ffmpeg,请先安装 ffmpeg。")
132
-
133
- output_path = build_preprocessed_video_path(output_dir, input_path, mode)
134
- cmd = _build_ffmpeg_cmd(
135
- input_path=input_path,
136
- output_path=output_path,
137
- mode=mode,
138
- remove_audio=remove_audio,
139
- )
140
-
141
- proc = subprocess.run(
142
- cmd,
143
- stdout=subprocess.PIPE,
144
- stderr=subprocess.PIPE,
145
- text=True,
146
- )
147
-
148
- if proc.returncode != 0:
149
- raise RuntimeError(f"ffmpeg 预处理失败:\n{proc.stderr}")
150
-
151
- return PreprocessResult(
152
- input_path=input_path,
153
- output_path=output_path,
154
- used_ffmpeg=True,
155
- message=f"视频预处理完成(mode={mode}, audio={'removed' if remove_audio else 'kept'})",
156
- mode=mode,
157
- ffmpeg_cmd=cmd,
158
- )
159
-
 
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import shutil
5
+ import subprocess
6
+ from dataclasses import dataclass
7
+ from datetime import datetime
8
+
9
+
10
+ @dataclass
11
+ class PreprocessResult:
12
+ input_path: str
13
+ output_path: str
14
+ used_ffmpeg: bool
15
+ message: str
16
+ mode: str
17
+ ffmpeg_cmd: list[str]
18
+
19
+
20
+ def check_ffmpeg_available() -> bool:
21
+ return shutil.which("ffmpeg") is not None
22
+
23
+
24
+ def ensure_dir(path: str) -> None:
25
+ os.makedirs(path, exist_ok=True)
26
+
27
+
28
+ def build_preprocessed_video_path(output_dir: str, input_path: str, mode: str) -> str:
29
+ ensure_dir(output_dir)
30
+ stem = os.path.splitext(os.path.basename(input_path))[0]
31
+ ts = datetime.now().strftime("%Y%m%d_%H%M%S")
32
+ return os.path.join(output_dir, f"{stem}_{mode}_preprocessed_{ts}.mp4")
33
+
34
+
35
+ def _build_ffmpeg_cmd(
36
+ input_path: str,
37
+ output_path: str,
38
+ *,
39
+ mode: str = "analysis",
40
+ remove_audio: bool = False,
41
+ ) -> list[str]:
42
+ """
43
+ 我在这里设置了三种可选择的模式:
44
+ - preview: 调试/预览,体积更小
45
+ - analysis: 正式分析,推荐,尽量保真
46
+ - archival: 更高保真,体积更大
47
+
48
+ 说明一下
49
+ - 音频默认保留
50
+ - 使用 H.264 + AAC,兼容性最好
51
+ - 使用 CRF 控质量;CRF 越小越保真
52
+ """
53
+ mode = (mode or "analysis").lower().strip()
54
+ if mode not in {"preview", "analysis", "archival"}:
55
+ raise ValueError(f"不支持的预处理模式: {mode}")
56
+
57
+ if mode == "preview":
58
+ width = 640
59
+ fps = 12
60
+ crf = 26
61
+ preset = "veryfast"
62
+ audio_bitrate = "96k"
63
+ elif mode == "analysis":
64
+ width = 1280
65
+ fps = 15
66
+ crf = 18
67
+ preset = "slow"
68
+ audio_bitrate = "128k"
69
+ else: # archival
70
+ width = 1280
71
+ fps = 20
72
+ crf = 16
73
+ preset = "slow"
74
+ audio_bitrate = "160k"
75
+
76
+ vf = f"scale='min({width},iw)':-2,fps={fps}"
77
+
78
+ cmd = [
79
+ "ffmpeg",
80
+ "-y",
81
+ "-i", input_path,
82
+ "-vf", vf,
83
+ "-c:v", "libx264",
84
+ "-preset", preset,
85
+ "-crf", str(crf),
86
+ "-movflags", "+faststart",
87
+ "-pix_fmt", "yuv420p",
88
+ ]
89
+
90
+ if remove_audio:
91
+ cmd += ["-an"]
92
+ else:
93
+ cmd += [
94
+ "-c:a", "aac",
95
+ "-b:a", audio_bitrate,
96
+ "-ac", "1",
97
+ "-ar", "16000",
98
+ ]
99
+
100
+ cmd.append(output_path)
101
+ return cmd
102
+
103
+
104
+ def preprocess_video(
105
+ input_path: str,
106
+ output_dir: str,
107
+ mode: str = "analysis",
108
+ remove_audio: bool = False,
109
+ ) -> PreprocessResult:
110
+ if not os.path.exists(input_path):
111
+ raise FileNotFoundError(f"视频文件不存在: {input_path}")
112
+
113
+ if not check_ffmpeg_available():
114
+ raise RuntimeError("未检测到 ffmpeg,请先安装 ffmpeg。")
115
+
116
+ output_path = build_preprocessed_video_path(output_dir, input_path, mode)
117
+ cmd = _build_ffmpeg_cmd(
118
+ input_path=input_path,
119
+ output_path=output_path,
120
+ mode=mode,
121
+ remove_audio=remove_audio,
122
+ )
123
+
124
+ proc = subprocess.run(
125
+ cmd,
126
+ stdout=subprocess.PIPE,
127
+ stderr=subprocess.PIPE,
128
+ text=True,
129
+ )
130
+
131
+ if proc.returncode != 0:
132
+ raise RuntimeError(f"ffmpeg 预处理失败:\n{proc.stderr}")
133
+
134
+ return PreprocessResult(
135
+ input_path=input_path,
136
+ output_path=output_path,
137
+ used_ffmpeg=True,
138
+ message=f"视频预处理完成(mode={mode}, audio={'removed' if remove_audio else 'kept'})",
139
+ mode=mode,
140
+ ffmpeg_cmd=cmd,
141
+ )
142
+