nchdlhbctm commited on
Commit
646535a
·
verified ·
1 Parent(s): 3e328ba

Upload 13 files

Browse files
app.py ADDED
@@ -0,0 +1,367 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import os
3
+ import time
4
+ from PIL import Image
5
+
6
+ # 全局缓存 Whisper 语音大模型
7
+ @st.cache_resource
8
+ def load_whisper_model():
9
+ import whisper
10
+ return whisper.load_model("small")
11
+
12
+ # 页面配置
13
+ st.set_page_config(
14
+ page_title="多模态AI生成痕迹鉴别系统",
15
+ page_icon="🔍",
16
+ layout="wide"
17
+ )
18
+
19
+ # ==========================================
20
+ # 侧边栏:系统设置与说明
21
+ # ==========================================
22
+ with st.sidebar:
23
+ st.title("⚙️ 鉴别引擎设置")
24
+ st.markdown("提供轻量、可解释的多模态AI生成内容快速鉴别能力。")
25
+ st.divider()
26
+
27
+ st.header("🎚️ 动态判定阈值")
28
+ st.markdown("调整敏感度,适应不同审核场景:")
29
+ high_risk_threshold = st.slider(
30
+ "高危报警阈值",
31
+ min_value=0.60, max_value=0.95, value=0.80, step=0.05,
32
+ help="高于此值判定为“高度疑似AI生成”"
33
+ )
34
+ warning_threshold = st.slider(
35
+ "存疑缓冲阈值",
36
+ min_value=0.20, max_value=0.55, value=0.40, step=0.05,
37
+ help="介于警告阈值与报警阈值之间时,标记为“存疑”"
38
+ )
39
+
40
+ st.divider()
41
+ st.caption("🔹 引擎状态:轻量模型已缓存 · 本地推理")
42
+
43
+ # ==========================================
44
+ # 主界面标题
45
+ # ==========================================
46
+ st.title("🔍 多模态AI生成痕迹快速鉴别系统")
47
+ st.markdown(
48
+ "同时支持**图像、文本、视频**单模态扫描,以及**图文/视文**联合跨模态融合鉴定。"
49
+ )
50
+
51
+ work_mode = st.radio(
52
+ "请选择检测模式:",
53
+ ["单模态独立检测(图片 / 文本 / 视频)", "多模态联合检测(图片+文案 / 视频+文案)"],
54
+ horizontal=True
55
+ )
56
+
57
+ st.divider()
58
+
59
+ # ==========================================
60
+ # 模式一:单模态独立检测
61
+ # ==========================================
62
+ if "单模态独立检测" in work_mode:
63
+ with st.container(border=True):
64
+ uploaded_file = st.file_uploader(
65
+ "拖拽或点击上传待检测文件",
66
+ type=["jpg", "png", "jpeg", "txt", "docx", "mp4", "avi", "mov"],
67
+ help="支持常见图片、文本、视频格式,单文件≤50MB,视频时长建议≤5分钟"
68
+ )
69
+
70
+ if uploaded_file is not None:
71
+ file_type = uploaded_file.name.split('.')[-1].lower()
72
+ st.success(f"✅ 已接收:**{uploaded_file.name}** | 启动鉴别引擎...")
73
+
74
+ col_content, col_result = st.columns([1, 1.2], gap="large")
75
+
76
+ # ----------------- 图像检测 -----------------
77
+ if file_type in ['jpg', 'png', 'jpeg']:
78
+ with col_content:
79
+ st.subheader("原始图像")
80
+ st.image(uploaded_file, use_container_width=True)
81
+
82
+ st.subheader("AI痕迹热力图(Grad-CAM)")
83
+ heatmap_placeholder = st.empty()
84
+
85
+ with col_result:
86
+ st.subheader("⚙️ 特征提取中...")
87
+ progress_bar = st.progress(0)
88
+ status_text = st.empty()
89
+
90
+ for percent_complete in range(100):
91
+ time.sleep(0.01)
92
+ progress_bar.progress(percent_complete + 1)
93
+ status_text.text(f"正在提取LBP纹理、频域伪影及深度语义... {percent_complete + 1}%")
94
+
95
+ from image_module import analyze_image, load_deep_image_model, generate_image_heatmap
96
+
97
+ result = analyze_image(uploaded_file)
98
+
99
+ model, device = load_deep_image_model()
100
+ # 图像模块内部我们已经处理过 seek(0),所以这里可以直接传
101
+ heatmap_img = generate_image_heatmap(uploaded_file, model, device)
102
+ heatmap_placeholder.image(
103
+ heatmap_img,
104
+ caption="🔴 红色区域为模型判定的高可疑伪造区域",
105
+ use_container_width=True
106
+ )
107
+
108
+ status_text.text("特征提取完成")
109
+
110
+ st.subheader("检测报告")
111
+ final_prob = result['final_probability']
112
+
113
+ if final_prob >= high_risk_threshold:
114
+ st.error(f"高度疑似AI生成图像(生成概率:{final_prob * 100:.1f}%)")
115
+ elif warning_threshold <= final_prob < high_risk_threshold:
116
+ st.warning(f"可疑图像,可能经过AI修图或局部生成(概率:{final_prob * 100:.1f}%)")
117
+ else:
118
+ st.success(f"真实图像,未检出明显生成痕迹(概率:{final_prob * 100:.1f}%)")
119
+
120
+ st.progress(float(final_prob))
121
+ with st.expander("展开底层特征参数", expanded=True):
122
+ st.info(
123
+ f"**双轨特征得分:**\n\n"
124
+ f"- 传统��理特征异常度:{result['traditional_score'] * 100:.1f}%\n"
125
+ f"- MobileNetV2 深度特征:{result['deep_score'] * 100:.1f}%"
126
+ )
127
+
128
+ # ----------------- 文本检测 -----------------
129
+ elif file_type in ['txt', 'docx']:
130
+ text_content = ""
131
+ uploaded_file.seek(0) # 核心修复:倒带文件指针
132
+ if file_type == 'txt':
133
+ text_content = uploaded_file.read().decode("utf-8")
134
+ elif file_type == 'docx':
135
+ import docx
136
+ doc = docx.Document(uploaded_file)
137
+ text_content = "\n".join([para.text for para in doc.paragraphs])
138
+
139
+ with col_content:
140
+ st.subheader("原始文本内容")
141
+ text_placeholder = st.empty()
142
+ text_placeholder.text_area("提取的文本", text_content, height=350)
143
+
144
+ with col_result:
145
+ st.subheader("⚙️ 语义连贯性分析中...")
146
+ with st.spinner("正在计算困惑度、句法复杂度及BERT深度特征..."):
147
+ from text_module import analyze_text, get_custom_text_model, generate_text_highlight_html
148
+
149
+ result = analyze_text(text_content)
150
+
151
+ tokenizer, model = get_custom_text_model()
152
+ highlighted_html = generate_text_highlight_html(text_content, tokenizer, model)
153
+ text_placeholder.markdown("**🔥 AI痕迹逐句高亮(黄色为高风险句式)**", unsafe_allow_html=True)
154
+ text_placeholder.markdown(highlighted_html, unsafe_allow_html=True)
155
+
156
+ st.subheader("检测报告")
157
+ final_prob = result['final_probability']
158
+
159
+ if final_prob >= high_risk_threshold:
160
+ st.error(f"高度疑似大语言模型生成文本(生成概率:{final_prob * 100:.1f}%)")
161
+ elif warning_threshold <= final_prob < high_risk_threshold:
162
+ st.warning(f"可疑文本,可能经AI润色或拼接(概率:{final_prob * 100:.1f}%)")
163
+ else:
164
+ st.success(f"真实人类写作风格,低风险(概率:{final_prob * 100:.1f}%)")
165
+
166
+ st.progress(float(final_prob))
167
+ with st.expander("展开底层特征参数", expanded=True):
168
+ st.info(
169
+ f"**多维度文本特征:**\n\n"
170
+ f"- 统计学风格异常度:{result['stat_score'] * 100:.1f}%\n"
171
+ f"- DistilBERT 语义鉴别得分:{result['deep_score'] * 100:.1f}%\n"
172
+ f"- 补充指标:{result['details']}"
173
+ )
174
+
175
+ # ----------------- 视频检测 -----------------
176
+ elif file_type in ['mp4', 'avi', 'mov']:
177
+ with col_content:
178
+ st.subheader("视频内容预览")
179
+ st.video(uploaded_file)
180
+
181
+ with col_result:
182
+ st.subheader("⚙️ 时空特征提取中...")
183
+ with st.spinner("正在进行关键帧抽帧、光流计算及音频频谱分析..."):
184
+ temp_video_path = f"temp_uploaded_video.{file_type}"
185
+ with open(temp_video_path, "wb") as f:
186
+ uploaded_file.seek(0) # 核心修复:倒带文件指针
187
+ f.write(uploaded_file.read())
188
+
189
+ from video_module import analyze_video
190
+ result = analyze_video(temp_video_path)
191
+
192
+ if os.path.exists(temp_video_path):
193
+ os.remove(temp_video_path)
194
+
195
+ if "error" in result:
196
+ st.error(result["error"])
197
+ else:
198
+ st.subheader("检测报告")
199
+ final_prob = result['avg_probability']
200
+
201
+ if final_prob >= high_risk_threshold:
202
+ st.error(f"整体视频高度疑似AI生成(综合概率:{final_prob * 100:.1f}%)")
203
+ elif warning_threshold <= final_prob < high_risk_threshold:
204
+ st.warning(f"视频存疑,存在异常帧或拼接痕迹(概率:{final_prob * 100:.1f}%)")
205
+ else:
206
+ st.success(f"未发现明显时序伪造痕迹,低风险(概率:{final_prob * 100:.1f}%)")
207
+
208
+ st.progress(float(final_prob))
209
+ with st.expander("展开抽帧分析详情", expanded=True):
210
+ st.info(
211
+ f"**视频物理特征:**\n\n"
212
+ f"- 总帧数:{result['total_frames']} 帧 (帧率 {result['fps']:.1f} fps)\n"
213
+ f"- 关键帧采样数:{result['sampled_frames']} 帧\n"
214
+ f"- 单帧最高风险值:{result['max_probability'] * 100:.1f}%"
215
+ )
216
+
217
+ # ==========================================
218
+ # 模式二:多模态联合检测
219
+ # ==========================================
220
+ elif "多模态联合检测" in work_mode:
221
+ st.subheader("🔗 跨模态联合检测场景")
222
+ st.markdown(
223
+ "模拟真实审核场景:同时检测**图像/视频**与**配套文本**,通过决策级加权融合输出综合AI生成概率。"
224
+ )
225
+
226
+ # 初始化session_state记忆体(用于自动提取的文本)
227
+ if 'auto_text' not in st.session_state:
228
+ st.session_state['auto_text'] = ""
229
+
230
+ col_media, col_text = st.columns(2, gap="large")
231
+
232
+ with col_media:
233
+ multi_media = st.file_uploader(
234
+ "🖼️ / 🎞️ 第一步:上传媒体内容(图片或短视频)",
235
+ type=["jpg", "png", "jpeg", "mp4", "avi", "mov"],
236
+ key="multi_media"
237
+ )
238
+ file_type = ""
239
+ if multi_media:
240
+ file_type = multi_media.name.split('.')[-1].lower()
241
+ if file_type in ['mp4', 'avi', 'mov']:
242
+ st.video(multi_media)
243
+
244
+ # 智能语音提取按钮
245
+ if st.button("🎙️ 自动从视频提取语音转文字", use_container_width=True):
246
+ with st.spinner("正在加载Whisper模型并转录音频(若视频较长请耐心等待数分钟)..."):
247
+ try:
248
+ from moviepy import VideoFileClip
249
+ import whisper
250
+
251
+ # 1. 保存临时视频
252
+ temp_vid = "temp_for_audio.mp4"
253
+ with open(temp_vid, "wb") as f:
254
+ multi_media.seek(0)
255
+ f.write(multi_media.read())
256
+
257
+ # 2. 读取并剥离音频
258
+ temp_audio = "temp_audio.wav"
259
+ my_clip = VideoFileClip(temp_vid)
260
+
261
+ # 【新增防护】检查视频到底有没有声音!
262
+ if my_clip.audio is None:
263
+ st.error("❌ 提取失败:检测到该视频没有声音轨道!")
264
+ else:
265
+ my_clip.audio.write_audiofile(temp_audio, logger=None)
266
+ my_clip.close()
267
+
268
+ # 3. 语音识别
269
+ model = load_whisper_model()
270
+ result = model.transcribe(
271
+ temp_audio,
272
+ language="zh",
273
+ initial_prompt="以下是一段标准的简体中文普通话录音。",
274
+ fp16=False
275
+ )
276
+
277
+ st.session_state['auto_text'] = result["text"]
278
+ st.success("✅ 提取成功!")
279
+ time.sleep(1) # 停留1秒让用户看到成功提示
280
+ st.rerun()
281
+
282
+ except Exception as e:
283
+ # 【新增防护】无论出什么错,直接弹在网页上!
284
+ st.error(f"❌ 提取失败,底层报错信息:{str(e)}")
285
+ st.info(
286
+ "💡 提示:如果看到 'ffprobe' 或 'ffmpeg' 等字眼,请确保系统已通过 conda 安装了 ffmpeg。")
287
+
288
+ finally:
289
+ # 【新增防护】哪怕中途崩溃,也要把占硬盘的临时文件删掉
290
+ if os.path.exists(temp_vid):
291
+ try:
292
+ os.remove(temp_vid)
293
+ except:
294
+ pass
295
+ if os.path.exists(temp_audio):
296
+ try:
297
+ os.remove(temp_audio)
298
+ except:
299
+ pass
300
+ else:
301
+ st.image(multi_media, use_container_width=True)
302
+
303
+ with col_text:
304
+ multi_txt = st.text_area(
305
+ "📝 第二步:输入配套文本(或点击左侧自动提取)",
306
+ value=st.session_state['auto_text'],
307
+ height=200,
308
+ key="multi_txt"
309
+ )
310
+
311
+ # 联合检测按钮
312
+ if multi_media and multi_txt:
313
+ if st.button("🔍 启动跨模态融合鉴别", type="primary", use_container_width=True):
314
+ st.divider()
315
+ st.subheader("📊 联合检测报告")
316
+
317
+ from text_module import analyze_text
318
+
319
+ with st.spinner("并行调用视觉鉴别引擎与文本鉴别引擎..."):
320
+ file_type = multi_media.name.split('.')[-1].lower()
321
+ p_media = 0.0
322
+ media_label = ""
323
+
324
+ if file_type in ['mp4', 'avi', 'mov']:
325
+ from video_module import analyze_video
326
+ temp_path = f"temp_multi_video.{file_type}"
327
+ with open(temp_path, "wb") as f:
328
+ multi_media.seek(0) # 核心修复:倒带文件指针
329
+ f.write(multi_media.read())
330
+ res_media = analyze_video(temp_path)
331
+ p_media = res_media.get('avg_probability', 0)
332
+ media_label = "🎞️ 视频维度AI生成概率"
333
+ if os.path.exists(temp_path):
334
+ os.remove(temp_path)
335
+ else:
336
+ from image_module import analyze_image
337
+ res_media = analyze_image(multi_media)
338
+ p_media = res_media['final_probability']
339
+ media_label = "🖼️ 图像维度AI生成概率"
340
+
341
+ # 文本检测
342
+ res_txt = analyze_text(multi_txt)
343
+ p_txt = res_txt['final_probability']
344
+
345
+ # 融合权重(视觉60%,文本40%)
346
+ joint_prob = (p_media * 0.60) + (p_txt * 0.40)
347
+
348
+ # 展示三指标
349
+ col_d1, col_d2, col_d3 = st.columns(3)
350
+ col_d1.metric(label=media_label, value=f"{p_media * 100:.1f}%")
351
+ col_d2.metric(label="📝 文本维度AI生成概率", value=f"{p_txt * 100:.1f}%")
352
+ col_d3.metric(
353
+ label="🔗 多模态综合判定概率",
354
+ value=f"{joint_prob * 100:.1f}%",
355
+ delta="加权融合 (视觉0.6 + 文本0.4)",
356
+ delta_color="off"
357
+ )
358
+
359
+ st.progress(float(joint_prob))
360
+
361
+ # 综合判定
362
+ if joint_prob >= high_risk_threshold:
363
+ st.error("🚨 **联合研判结论:高度疑似AI生成的多模态内容**(图像/视频与文本均呈现显著生成特征)")
364
+ elif warning_threshold <= joint_prob < high_risk_threshold:
365
+ st.warning("⚠️ **联合研判结论:内容存疑**,可能存在局部AI修改或跨模态不一致,建议人工复核")
366
+ else:
367
+ st.success("✅ **联合研判结论:内容安全**,未见明显多模态伪造痕迹")
download_data.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datasets import load_dataset
2
+ import os
3
+
4
+ print("正在连接 Hugging Face 并提取高清数据集...")
5
+ dataset = load_dataset("Parveshiiii/AI-vs-Real", split="train", verification_mode="no_checks")
6
+
7
+ label_col = 'binary_label'
8
+ print(f"✅ 成功锁定标签列:'{label_col}'\n")
9
+
10
+ os.makedirs("./data/real_batch", exist_ok=True)
11
+ os.makedirs("./data/ai_batch", exist_ok=True)
12
+
13
+ # 设置我们想要的配额
14
+ target_count = 2000
15
+ real_saved = 0
16
+ ai_saved = 0
17
+
18
+ print(f"开始精准抓取:目标 {target_count}张真实图 + {target_count}张AI图...")
19
+
20
+ # 遍历整个数据集,直到两个配额都装满
21
+ for item in dataset:
22
+ img = item['image']
23
+ label = item[label_col]
24
+
25
+ # 工业级防御:统一转为 RGB 格式
26
+ if img.mode != 'RGB':
27
+ img = img.convert('RGB')
28
+
29
+ # 分流并计数
30
+ if label == 1 and real_saved < target_count:
31
+ img.save(f"./data/real_batch/real_hd_{real_saved}.jpg")
32
+ real_saved += 1
33
+
34
+ elif label == 0 and ai_saved < target_count:
35
+ img.save(f"./data/ai_batch/ai_hd_{ai_saved}.jpg")
36
+ ai_saved += 1
37
+
38
+ # 每抓够 100 张,在终端汇报一下进度,让你心里有底
39
+ if (real_saved + ai_saved) % 100 == 0:
40
+ print(f"当前进度 -> 真实图: {real_saved}/{target_count} | AI图: {ai_saved}/{target_count}")
41
+
42
+ # 如果两边的配额都满了,就停止遍历
43
+ if real_saved >= target_count and ai_saved >= target_count:
44
+ break
45
+
46
+ print("\n🎉 完美收工!这次两个文件夹绝对都塞得满满当当了!")
download_text_data.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datasets import load_dataset
2
+ import pandas as pd
3
+ import os
4
+
5
+ print("正在连接 Hugging Face 拉取 HC3 中文语料库...")
6
+ # 使用 verification_mode="no_checks" 防止部分网络下元数据校验报错
7
+ dataset = load_dataset("Hello-SimpleAI/HC3-Chinese", name="all", split="train", verification_mode="no_checks", trust_remote_code=True)
8
+
9
+ human_texts = []
10
+ ai_texts = []
11
+ target_count = 500 # 咱们先各抓 500 条用来快速跑通流程
12
+
13
+ print(f"开始清洗数据,目标:提取 {target_count}条人类文本 + {target_count}条AI文本...")
14
+
15
+ for item in dataset:
16
+ # 提取人类回答 (剔除太短的废话)
17
+ if len(item['human_answers']) > 0 and len(item['human_answers'][0]) > 20:
18
+ if len(human_texts) < target_count:
19
+ human_texts.append(item['human_answers'][0])
20
+
21
+ # 提取 ChatGPT 回答
22
+ if len(item['chatgpt_answers']) > 0 and len(item['chatgpt_answers'][0]) > 20:
23
+ if len(ai_texts) < target_count:
24
+ ai_texts.append(item['chatgpt_answers'][0])
25
+
26
+ if len(human_texts) >= target_count and len(ai_texts) >= target_count:
27
+ break
28
+
29
+ # 制作结构化的 DataFrame (标签规则统一:0代表AI,1代表真实)
30
+ df_human = pd.DataFrame({'text': human_texts, 'label': 1})
31
+ df_ai = pd.DataFrame({'text': ai_texts, 'label': 0})
32
+
33
+ # 合并并打乱顺序 (Shuffle)
34
+ df_all = pd.concat([df_human, df_ai]).sample(frac=1, random_state=42).reset_index(drop=True)
35
+
36
+ # 确保 data 文件夹存在
37
+ os.makedirs("./data", exist_ok=True)
38
+ csv_path = "./data/text_dataset.csv"
39
+ df_all.to_csv(csv_path, index=False, encoding='utf-8')
40
+
41
+ print(f"✅ 语料库构建完毕!已保存为 {csv_path} (共 {len(df_all)} 条数据)")
42
+ print("你可以打开这个 csv 文件看看,里面的文本非常有意思!")
image_module.py ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import numpy as np
3
+ import torch
4
+ import torch.nn as nn
5
+ from torchvision import models, transforms
6
+ from skimage import feature
7
+ from scipy.fftpack import dct
8
+ from PIL import Image, ImageOps
9
+ import streamlit as st
10
+
11
+ # 引入热力图相关库
12
+ from pytorch_grad_cam import GradCAM
13
+ from pytorch_grad_cam.utils.model_targets import ClassifierOutputTarget
14
+
15
+
16
+ # ==========================================
17
+ # 辅助函数:智能读取图像(修正 EXIF 方向)
18
+ # ==========================================
19
+ def get_pil_image(image_input):
20
+ if isinstance(image_input, str):
21
+ img = Image.open(image_input)
22
+ else:
23
+ image_input.seek(0)
24
+ img = Image.open(image_input)
25
+
26
+ # 强制应用隐藏的 EXIF 旋转信息,还原真实方向
27
+ img = ImageOps.exif_transpose(img)
28
+ return img.convert('RGB')
29
+
30
+
31
+ def get_cv_image(image_input):
32
+ pil_img = get_pil_image(image_input)
33
+ # 保证 OpenCV 提取物理特征时,方向与 PIL 绝对一致
34
+ return cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)
35
+
36
+
37
+ # ==========================================
38
+ # 第一部分:传统物理特征检测 (权重 15%)
39
+ # ==========================================
40
+ def extract_traditional_features(image_input):
41
+ img = get_cv_image(image_input)
42
+ if img is None:
43
+ return 0.0
44
+
45
+ gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
46
+
47
+ # 1. LBP 局部二值模式
48
+ lbp = feature.local_binary_pattern(gray, P=8, R=1, method="uniform")
49
+ hist, _ = np.histogram(lbp.ravel(), bins=np.arange(0, 11), density=True)
50
+ lbp_entropy = -np.sum(hist * np.log2(hist + 1e-7))
51
+
52
+ # 2. DCT 离散余弦变换
53
+ dct_data = dct(dct(gray.T, norm='ortho').T, norm='ortho')
54
+ high_freq = np.sum(np.abs(dct_data[10:, 10:]))
55
+ total_energy = np.sum(np.abs(dct_data))
56
+ dct_ratio = high_freq / (total_energy + 1e-7)
57
+
58
+ # 3. FFT 快速傅里叶变换
59
+ f = np.fft.fft2(gray)
60
+ fshift = np.fft.fftshift(f)
61
+ magnitude = 20 * np.log(np.abs(fshift) + 1)
62
+ h, w = magnitude.shape
63
+ center_h, center_w = h // 2, w // 2
64
+ top_left = magnitude[0:center_h, 0:center_w]
65
+ bottom_right = np.flip(magnitude[center_h + (h % 2):, center_w + (w % 2):])
66
+
67
+ if top_left.shape != bottom_right.shape:
68
+ bottom_right = cv2.resize(bottom_right, (top_left.shape[1], top_left.shape[0]))
69
+
70
+ fft_sym_error = np.mean(np.abs(top_left - bottom_right))
71
+
72
+ score = 0.0
73
+ if lbp_entropy > 3.6: score += 0.3
74
+ if dct_ratio < 0.985: score += 0.1
75
+ if fft_sym_error > 13.8: score += 0.1
76
+
77
+ return min(score, 1.0)
78
+
79
+
80
+ # ==========================================
81
+ # 第二部分:深度模型缓存与提取
82
+ # ==========================================
83
+ @st.cache_resource
84
+ def load_deep_image_model():
85
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
86
+ model = models.mobilenet_v2(weights=None)
87
+ num_ftrs = model.classifier[1].in_features
88
+ model.classifier[1] = nn.Linear(num_ftrs, 2)
89
+
90
+ # 加载自己炼丹生成的权重文件
91
+ model.load_state_dict(torch.load('mobilenet_finetuned.pth', map_location=device))
92
+ model = model.to(device)
93
+ model.eval()
94
+
95
+ return model, device
96
+
97
+
98
+ def extract_deep_features(image_input, model, device):
99
+ pil_img = get_pil_image(image_input)
100
+ transform = transforms.Compose([
101
+ transforms.Resize((224, 224)),
102
+ transforms.ToTensor(),
103
+ transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
104
+ ])
105
+ input_tensor = transform(pil_img).unsqueeze(0).to(device)
106
+
107
+ with torch.no_grad():
108
+ outputs = model(input_tensor)
109
+ probs = torch.softmax(outputs, dim=1)
110
+ fake_prob = probs[0][0].item()
111
+
112
+ return fake_prob
113
+
114
+
115
+ # ==========================================
116
+ # 第三部分:多模态加权融合引擎
117
+ # ==========================================
118
+ def analyze_image(image_input):
119
+ trad_score = extract_traditional_features(image_input)
120
+ model, device = load_deep_image_model()
121
+ deep_score = extract_deep_features(image_input, model, device)
122
+
123
+ final_prob = (trad_score * 0.15) + (deep_score * 0.85)
124
+
125
+ return {
126
+ 'traditional_score': trad_score,
127
+ 'deep_score': deep_score,
128
+ 'final_probability': final_prob
129
+ }
130
+
131
+
132
+ # ==========================================
133
+ # 第四部分:XAI 热力图渲染引擎
134
+ # ==========================================
135
+ def generate_image_heatmap(image_input, model, device):
136
+ """使用 Grad-CAM 生成热力图,并无损拉伸回原图尺寸,带动态透明度"""
137
+ # ⚠️ 获取原图,绝对没有任何 resize 操作干扰!
138
+ original_pil_img = get_pil_image(image_input)
139
+ orig_width, orig_height = original_pil_img.size
140
+
141
+ transform = transforms.Compose([
142
+ transforms.Resize((224, 224)),
143
+ transforms.ToTensor(),
144
+ transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
145
+ ])
146
+ input_tensor = transform(original_pil_img).unsqueeze(0).to(device)
147
+
148
+ target_layers = [model.features[-1]]
149
+
150
+ try:
151
+ with GradCAM(model=model, target_layers=target_layers) as cam:
152
+ targets = [ClassifierOutputTarget(0)]
153
+ # 跑出 224x224 的 0~1 原始热力图矩阵
154
+ grayscale_cam_224 = cam(input_tensor=input_tensor, targets=targets)[0, :]
155
+
156
+ # 将 224x224 的热力图拉伸回真实的高清原图宽高!
157
+ grayscale_cam_resized = cv2.resize(grayscale_cam_224, (orig_width, orig_height))
158
+
159
+ heatmap_color = cv2.applyColorMap(np.uint8(255 * grayscale_cam_resized), cv2.COLORMAP_JET)
160
+ heatmap_color = cv2.cvtColor(heatmap_color, cv2.COLOR_BGR2RGB)
161
+ heatmap_color = np.float32(heatmap_color) / 255.0
162
+
163
+ orig_img_array = np.array(original_pil_img, dtype=np.float32) / 255.0
164
+
165
+ alpha = grayscale_cam_resized[..., np.newaxis]
166
+ alpha = np.power(alpha, 1.5) * 0.65
167
+
168
+ visualization = orig_img_array * (1 - alpha) + heatmap_color * alpha
169
+ return np.uint8(255 * visualization)
170
+
171
+ except Exception as e:
172
+ return np.array(original_pil_img)
mobilenet_finetuned.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:22343e127cd0d6a43a5cb78176171b808a3956cd8dfac8db799b77f753e9620c
3
+ size 9149899
packages.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ ffmpeg
requirements.txt CHANGED
@@ -1,3 +1,12 @@
1
- altair
2
- pandas
3
- streamlit
 
 
 
 
 
 
 
 
 
 
1
+ streamlit==1.32.0
2
+ torch
3
+ torchvision
4
+ transformers
5
+ opencv-python-headless
6
+ moviepy
7
+ openai-whisper
8
+ pytorch-grad-cam
9
+ scikit-image
10
+ scipy
11
+ Pillow
12
+ numpy
test.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ import streamlit as st
2
+ st.title("如果能看到这行字,说明环境没问题!")
text_module.py ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import jieba
2
+ import torch
3
+ import re
4
+ import numpy as np
5
+ import streamlit as st
6
+ from transformers import AutoTokenizer, AutoModelForSequenceClassification
7
+
8
+
9
+ # ==========================================
10
+ # 第一部分:统计风格特征提取 (占权重 20%)
11
+ # ==========================================
12
+ def extract_text_statistics(text):
13
+ """
14
+ 提取文本的统计学与风格特征。
15
+ """
16
+ words = list(jieba.cut(text))
17
+ words = [w for w in words if w.strip() and len(w) > 0]
18
+ if len(words) == 0: return 0.0, 0, 0
19
+
20
+ unique_words = set(words)
21
+ richness = len(unique_words) / len(words)
22
+
23
+ sentences = re.split(r'[。!?\n]', text)
24
+ sentences = [s for s in sentences if len(s.strip()) > 0]
25
+
26
+ sentence_lengths = [len(s) for s in sentences]
27
+ avg_sentence_len = np.mean(sentence_lengths) if sentence_lengths else 0
28
+ sentence_len_std = np.std(sentence_lengths) if sentence_lengths else 0
29
+
30
+ score = 0.0
31
+ if richness < 0.55:
32
+ score += 0.5
33
+ if sentence_len_std < 10.0 and avg_sentence_len > 15:
34
+ score += 0.5
35
+
36
+ return min(score, 1.0), richness, sentence_len_std
37
+
38
+
39
+ # ==========================================
40
+ # 第二部分:深度语义特征提取 (换上我们自己微调的专属模型)
41
+ # ==========================================
42
+ @st.cache_resource
43
+ def get_custom_text_model():
44
+ """加载我们刚刚炼丹微调出来的专属 BERT 模型"""
45
+ # 直接指向你刚刚生成的那个文件夹
46
+ model_path = "./finetuned_text_model"
47
+ tokenizer = AutoTokenizer.from_pretrained(model_path)
48
+ model = AutoModelForSequenceClassification.from_pretrained(model_path)
49
+ model.eval()
50
+ return tokenizer, model
51
+
52
+
53
+ def extract_deep_text_features(text, tokenizer, model):
54
+ """使用专属模型进行推理打分"""
55
+ inputs = tokenizer(text, return_tensors="pt", truncation=True, max_length=128)
56
+ with torch.no_grad():
57
+ outputs = model(**inputs)
58
+
59
+ # 提取输出并转换为概率分布 (0: AI, 1: 真实)
60
+ logits = outputs.logits
61
+ probs = torch.softmax(logits, dim=1)
62
+
63
+ # 获取它被判定为 AI (标签0) 的概率百分比
64
+ fake_prob = probs[0][0].item()
65
+ return fake_prob
66
+
67
+
68
+ # ==========================================
69
+ # 第三部分:多模态加权融合
70
+ # ==========================================
71
+ def analyze_text(text_content):
72
+ # 1. 统计特征
73
+ stat_score, richness, std_len = extract_text_statistics(text_content)
74
+
75
+ # 2. 深度特征 (加载你的专属模型)
76
+ tokenizer, model = get_custom_text_model()
77
+ deep_score = extract_deep_text_features(text_content, tokenizer, model)
78
+
79
+ # 3. 加权融合 (既然我们自己炼了丹,深度模型变得极强,权重拉高到 80%)
80
+ final_prob = (stat_score * 0.2) + (deep_score * 0.8)
81
+
82
+ return {
83
+ "stat_score": stat_score,
84
+ "deep_score": deep_score,
85
+ "final_probability": final_prob,
86
+ "details": f"词汇丰富度: {richness:.2f} | 句长波动率: {std_len:.1f}"
87
+ }
88
+
89
+
90
+ def generate_text_highlight_html(text, tokenizer, model):
91
+ """逐句扫描文本,生成带有高亮背景色的 HTML 代码"""
92
+ # 按照标点符号将文章拆分为句子
93
+ sentences = re.split(r'([。!?\n])', text)
94
+ # 重新把句子和标点拼装起来
95
+ sentences = ["".join(i) for i in zip(sentences[0::2], sentences[1::2] + [""])]
96
+
97
+ html_content = '<div style="line-height: 1.6; font-size: 16px; padding: 10px; border: 1px solid #ddd; border-radius: 5px;">'
98
+
99
+ for s in sentences:
100
+ if len(s.strip()) < 2:
101
+ html_content += s.replace('\n', '<br>')
102
+ continue
103
+
104
+ # 让专属模型给这一句话单独打分
105
+ prob = extract_deep_text_features(s, tokenizer, model)
106
+
107
+ # 只有当 AI 概率大于 50% 时才开始变红,概率越高颜色越深
108
+ if prob > 0.5:
109
+ alpha = (prob - 0.5) * 2 # 映射到 0~1 透明度
110
+ color = f"rgba(255, 75, 75, {alpha:.2f})"
111
+ html_content += f'<span style="background-color: {color}; border-radius: 3px;">{s}</span>'
112
+ else:
113
+ html_content += s
114
+
115
+ html_content += "</div>"
116
+ return html_content
train_image_model.py ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import torch
3
+ import torch.nn as nn
4
+ import torch.optim as optim
5
+ from torchvision import datasets, models, transforms
6
+ from torch.utils.data import DataLoader
7
+ from tqdm import tqdm # 引入了实时进度条神器
8
+
9
+
10
+ def train_model():
11
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
12
+ print(f"当前使用的计算设备: {device}")
13
+
14
+ if device.type == 'cpu':
15
+ print("⚠️ 警告:当前正在使用 CPU 训练,4000张图片预计每轮需要 15-30 分钟,请保持耐心!")
16
+
17
+ data_transforms = transforms.Compose([
18
+ transforms.Resize((224, 224)),
19
+ transforms.RandomHorizontalFlip(),
20
+ transforms.ToTensor(),
21
+ transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
22
+ ])
23
+
24
+ data_dir = './data'
25
+ image_dataset = datasets.ImageFolder(data_dir, data_transforms)
26
+ # CPU 训练比较慢,我们把 batch_size 稍微调大一点点到 16
27
+ dataloader = DataLoader(image_dataset, batch_size=16, shuffle=True)
28
+
29
+ print(f"总计训练图片数量: {len(image_dataset)} 张\n")
30
+
31
+ print("正在加载 MobileNetV2 模型...")
32
+ model = models.mobilenet_v2(weights=models.MobileNet_V2_Weights.IMAGENET1K_V1)
33
+
34
+ num_ftrs = model.classifier[1].in_features
35
+ model.classifier[1] = nn.Linear(num_ftrs, 2)
36
+ model = model.to(device)
37
+
38
+ criterion = nn.CrossEntropyLoss()
39
+ optimizer = optim.Adam(model.parameters(), lr=1e-4)
40
+
41
+ # 为了用 CPU 能快点看到结果,我们先设为 3 轮
42
+ num_epochs = 3
43
+ print("\n--- 开始模型微调 ---")
44
+
45
+ for epoch in range(num_epochs):
46
+ model.train()
47
+ running_loss = 0.0
48
+ corrects = 0
49
+
50
+ # 【核心修改】:用 tqdm 包装 dataloader,生成实时进度条
51
+ progress_bar = tqdm(dataloader, desc=f"第 {epoch + 1}/{num_epochs} 轮", leave=False, colour='green')
52
+
53
+ for inputs, labels in progress_bar:
54
+ inputs = inputs.to(device)
55
+ labels = labels.to(device)
56
+
57
+ optimizer.zero_grad()
58
+ outputs = model(inputs)
59
+ _, preds = torch.max(outputs, 1)
60
+ loss = criterion(outputs, labels)
61
+
62
+ loss.backward()
63
+ optimizer.step()
64
+
65
+ running_loss += loss.item() * inputs.size(0)
66
+ corrects += torch.sum(preds == labels.data)
67
+
68
+ # 让进度条实时显示当前的误差值
69
+ progress_bar.set_postfix({'loss': f"{loss.item():.4f}"})
70
+
71
+ epoch_loss = running_loss / len(image_dataset)
72
+ epoch_acc = corrects.double() / len(image_dataset)
73
+ print(f"✅ 第 {epoch + 1}/{num_epochs} 轮完成 | 平均损失: {epoch_loss:.4f} | 准确率: {epoch_acc:.4f}")
74
+
75
+ save_path = 'mobilenet_finetuned.pth'
76
+ torch.save(model.state_dict(), save_path)
77
+ print(f"\n🎉 训练完成!模型权重已保存至: {save_path}")
78
+
79
+
80
+ if __name__ == '__main__':
81
+ train_model()
train_text_model.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import torch
3
+ from torch.utils.data import Dataset, DataLoader
4
+ from transformers import AutoTokenizer, AutoModelForSequenceClassification
5
+ from torch.optim import AdamW
6
+ from tqdm import tqdm
7
+
8
+
9
+ # 1. 定义专属的 PyTorch 文本数据集
10
+ class AITextDataset(Dataset):
11
+ def __init__(self, csv_file, tokenizer, max_len=128):
12
+ self.data = pd.read_csv(csv_file)
13
+ self.tokenizer = tokenizer
14
+ self.max_len = max_len
15
+
16
+ def __len__(self):
17
+ return len(self.data)
18
+
19
+ def __getitem__(self, index):
20
+ text = str(self.data.iloc[index, 0])
21
+ label = int(self.data.iloc[index, 1])
22
+
23
+ # 将汉字切成 token 序列
24
+ encoding = self.tokenizer(
25
+ text,
26
+ add_special_tokens=True,
27
+ max_length=self.max_len,
28
+ padding='max_length',
29
+ truncation=True,
30
+ return_attention_mask=True,
31
+ return_tensors='pt',
32
+ )
33
+ return {
34
+ 'input_ids': encoding['input_ids'].flatten(),
35
+ 'attention_mask': encoding['attention_mask'].flatten(),
36
+ 'labels': torch.tensor(label, dtype=torch.long)
37
+ }
38
+
39
+
40
+ def train_text():
41
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
42
+ print(f"💻 当前计算设备: {device}")
43
+ if device.type == 'cpu':
44
+ print("⚠️ 警告:当前使用 CPU 炼丹。NLP模型参数量巨大,这可能需要一些时间,请耐心等待...")
45
+
46
+ print("正在加载预训练的中文 BERT 分词器与模型权重...")
47
+
48
+ # 【核心修复】:换成了官方真实存在、最经典的 bert-base-chinese
49
+ model_name = "bert-base-chinese"
50
+ tokenizer = AutoTokenizer.from_pretrained(model_name)
51
+ model = AutoModelForSequenceClassification.from_pretrained(model_name, num_labels=2)
52
+ model = model.to(device)
53
+
54
+ print("正在封装数据...")
55
+ dataset = AITextDataset('./data/text_dataset.csv', tokenizer, max_len=128)
56
+ # 批量大小设为 8,防止 CPU 内存吃紧
57
+ dataloader = DataLoader(dataset, batch_size=8, shuffle=True)
58
+
59
+ optimizer = AdamW(model.parameters(), lr=2e-5)
60
+
61
+ epochs = 1
62
+
63
+ print("\n🚀 --- 开始文本模型微调 ---")
64
+ model.train()
65
+
66
+ for epoch in range(epochs):
67
+ progress_bar = tqdm(dataloader, desc=f"第 {epoch + 1}/{epochs} 轮", leave=True, colour='blue')
68
+ running_loss = 0.0
69
+
70
+ for batch in progress_bar:
71
+ optimizer.zero_grad()
72
+
73
+ input_ids = batch['input_ids'].to(device)
74
+ attention_mask = batch['attention_mask'].to(device)
75
+ labels = batch['labels'].to(device)
76
+
77
+ outputs = model(input_ids, attention_mask=attention_mask, labels=labels)
78
+ loss = outputs.loss
79
+
80
+ loss.backward()
81
+ optimizer.step()
82
+
83
+ running_loss += loss.item()
84
+ progress_bar.set_postfix({'loss': f"{loss.item():.4f}"})
85
+
86
+ print(f"✅ 第 {epoch + 1} 轮完成 | 平均 Loss: {running_loss / len(dataloader):.4f}")
87
+
88
+ # 保存咱们微调后的专属大模型权重
89
+ save_dir = "./finetuned_text_model"
90
+ model.save_pretrained(save_dir)
91
+ tokenizer.save_pretrained(save_dir)
92
+ print(f"\n🎉 炼丹成功!专属的文本鉴别模型已保存在: {save_dir} 文件夹中。")
93
+
94
+
95
+ if __name__ == "__main__":
96
+ train_text()
tune_thresholds.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import cv2
3
+ import numpy as np
4
+ from image_module import get_lbp_entropy, get_dct_high_freq_energy, get_fft_symmetry_error
5
+
6
+
7
+ def process_folder(folder_path, label):
8
+ results = []
9
+ print(f"正在分析 [{label}] 样本集: {folder_path}")
10
+
11
+ for filename in os.listdir(folder_path):
12
+ if filename.lower().endswith(('.png', '.jpg', '.jpeg')):
13
+ filepath = os.path.join(folder_path, filename)
14
+ # 读取图片并转换为灰度图
15
+ img = cv2.imdecode(np.fromfile(filepath, dtype=np.uint8), cv2.IMREAD_COLOR)
16
+ if img is None: continue
17
+ gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
18
+
19
+ # 提取特征
20
+ lbp = get_lbp_entropy(gray)
21
+ dct = get_dct_high_freq_energy(gray)
22
+ fft = get_fft_symmetry_error(gray)
23
+
24
+ results.append((lbp, dct, fft))
25
+
26
+ if not results:
27
+ return None
28
+
29
+ results_arr = np.array(results)
30
+ print(f"--- {label} 统计结果 ({len(results)}张) ---")
31
+ print(
32
+ f"LBP 熵 : 平均 {np.mean(results_arr[:, 0]):.3f} | 范围 [{np.min(results_arr[:, 0]):.3f} - {np.max(results_arr[:, 0]):.3f}]")
33
+ print(
34
+ f"DCT 占比: 平均 {np.mean(results_arr[:, 1]):.3f} | 范围 [{np.min(results_arr[:, 1]):.3f} - {np.max(results_arr[:, 1]):.3f}]")
35
+ print(
36
+ f"FFT 误差: 平均 {np.mean(results_arr[:, 2]):.3f} | 范围 [{np.min(results_arr[:, 2]):.3f} - {np.max(results_arr[:, 2]):.3f}]\n")
37
+ return results_arr
38
+
39
+
40
+ if __name__ == "__main__":
41
+ # 请在当前目录下新建这两个文件夹,分别放几十张真实的和AI生成的图进去
42
+ real_folder = "./data/real"
43
+ ai_folder = "./data/ai"
44
+
45
+ print("开始执行特征分布寻优...\n")
46
+ process_folder(real_folder, "真实图像")
47
+ process_folder(ai_folder, "AI生成图像")
video_module.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import numpy as np
3
+ import os
4
+ from PIL import Image
5
+ from image_module import analyze_image
6
+
7
+
8
+ def analyze_video(video_path, num_samples=10):
9
+ """
10
+ 使用 OpenCV 对视频进行均匀抽帧,并复用图像引擎进行鉴别
11
+ :param video_path: 视频文件的路径
12
+ :param num_samples: 准备抽取的代表性帧数(默认 10 帧)
13
+ """
14
+ # 1. 打开视频文件
15
+ cap = cv2.VideoCapture(video_path)
16
+ if not cap.isOpened():
17
+ return {"error": "无法打开视频文件"}
18
+
19
+ # 2. 获取视频的基础信息
20
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
21
+ fps = cap.get(cv2.CAP_PROP_FPS)
22
+
23
+ # 3. 计算均匀抽帧的索引 (比如从 300 帧里均匀选 10 个时间点)
24
+ if total_frames < num_samples:
25
+ num_samples = total_frames # 如果视频太短,有几帧抽几帧
26
+ intervals = np.linspace(0, total_frames - 1, num_samples, dtype=int)
27
+
28
+ frame_scores = []
29
+
30
+ # 4. 开始逐帧提取
31
+ for frame_idx in intervals:
32
+ cap.set(cv2.CAP_PROP_POS_FRAMES, frame_idx) # 跳转到指定帧
33
+ ret, frame = cap.read()
34
+
35
+ if ret:
36
+ # OpenCV 默认读取的是 BGR 格式,我们需要转成正常的 RGB 格式
37
+ frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
38
+ pil_img = Image.fromarray(frame_rgb)
39
+
40
+ # 临时保存为图片文件,喂给咱们之前写好的图像模块
41
+ temp_path = f"temp_frame_{frame_idx}.jpg"
42
+ pil_img.save(temp_path)
43
+
44
+ try:
45
+ # 🌟 核心:直接调用咱们炼好的图像鉴别引擎!
46
+ result = analyze_image(temp_path)
47
+ frame_scores.append(result['final_probability'])
48
+ finally:
49
+ # 阅后即焚,清理临时文件
50
+ if os.path.exists(temp_path):
51
+ os.remove(temp_path)
52
+
53
+ cap.release()
54
+
55
+ if not frame_scores:
56
+ return {"error": "未能成功提取任何视频帧"}
57
+
58
+ # 5. 综合计算这 10 张图的得分
59
+ avg_score = np.mean(frame_scores)
60
+ max_score = np.max(frame_scores) # 记录最可疑的一帧
61
+
62
+ return {
63
+ "avg_probability": avg_score,
64
+ "max_probability": max_score,
65
+ "sampled_frames": num_samples,
66
+ "total_frames": total_frames,
67
+ "fps": fps
68
+ }