xiaoxishui commited on
Commit
68939c8
·
verified ·
1 Parent(s): 4f722b6

Update qwen3vl.py

Browse files
Files changed (1) hide show
  1. qwen3vl.py +533 -0
qwen3vl.py CHANGED
@@ -0,0 +1,533 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ 基于 Qwen3-VL 模型的视频内容分析工具
4
+ 支持读取本地 MP4 视频文件并提取视频内容描述
5
+ 支持生成 SORA2 文生视频提示词
6
+
7
+ 由于 API 有大小限制,采用提取视频关键帧的方式进行分析
8
+ """
9
+
10
+ import os
11
+ import sys
12
+ import base64
13
+ import argparse
14
+ import tempfile
15
+ import subprocess
16
+ from pathlib import Path
17
+ from openai import OpenAI
18
+ from dotenv import load_dotenv
19
+
20
+ # 加载环境变量
21
+ load_dotenv()
22
+
23
+ # API 配置
24
+ API_BASE_URL = os.getenv('QWEN_API_BASE_URL', 'https://api-inference.modelscope.cn/v1')
25
+ API_KEY = os.getenv('QWEN_API_KEY', 'aaa')
26
+ MODEL_ID = os.getenv('QWEN_MODEL_ID', 'Qwen/Qwen3-VL-8B-Instruct')
27
+
28
+ # 帧提取配置
29
+ MAX_FRAMES = 8 # 最多提取的帧数
30
+ FRAME_QUALITY = 85 # JPEG 质量
31
+
32
+
33
+ # SORA2 视频提示词专家系统提示词 - 基于复刻SORA2视频提示词专家模板
34
+ SORA2_SYSTEM_PROMPT = """你是 SORA2 视频复刻提示词专家。你的任务是根据视频关键帧分析,生成符合 Sora2 文生视频标准的高质量提示词。
35
+
36
+ ## Sora2 五大支柱框架
37
+ 生成提示词时必须包含以下五个核心要素:
38
+
39
+ 1. **主体与角色 (Subject & Character)**: 清晰定义人物/物体的外观、服装、情感状态
40
+ 2. **动作与运动 (Action & Movement)**: 使用具体动词描述正在发生的事情和交互方式
41
+ 3. **环境与背景 (Environment & Setting)**: 建立场景的位置、时间和氛围属性
42
+ 4. **电影构图 (Cinematography)**: 指定摄像机角度、运动和取景方式
43
+ 5. **美学与风格 (Aesthetics & Style)**: 确定视觉效果(真实感、动画、胶片类型)
44
+
45
+ ## 世界模拟范式
46
+ Sora2 是世界模拟器,有效提示应该:
47
+ - 提供初始条件和物理法则(重力、光线、反射)
48
+ - 明确物体如何相互作用
49
+ - 定义环境特性和材质属性
50
+ - 隐含或明确引导物理表现
51
+
52
+ ## 提示词结构模板
53
+
54
+ ### 第一部分:Style(风格定义)
55
+ - **Visual Texture(视觉纹理)**: 描述画面的质感特征、材质表面、AI/真实拍摄风格
56
+ - **Lighting Quality(光照质量)**: 光源类型、方向、强度和氛围(如 golden hour, three-point lighting)
57
+ - **Color Palette(色彩调板)**: 主导色调和配色方案,使用具体色彩名称
58
+ - **Atmosphere(氛围)**: 整体情绪和感受(如 playful, nostalgic, energetic)
59
+
60
+ ### 第二部分:Cinematography(电影摄影)
61
+ - **Camera(摄像机运动)**: 描述摄像机的移动方式(handheld, dolly, pan, tilt, zoom)
62
+ - **Lens(镜头特性)**: 镜头类型、焦距和景深效果(50mm, f/2.8, shallow depth of field)
63
+ - **Lighting(布光方案)**: 详细说明光照布置(key light, fill light, rim light)
64
+ - **Mood(情绪基调)**: 视觉情绪和节奏
65
+
66
+ ### 第三部分:Scene Breakdown(场景分解)
67
+ 按时间顺序描述每个场景,包含:
68
+ - **场景描述**: 1-3句话描述场景整体视觉呈现
69
+ - **Actions**: 具体动作列表,使用精确动词
70
+ - **Dialogue**: 对话内容或 "None"
71
+ - **Background Sound**: 音乐类型和环境音效
72
+
73
+ ## 质量检查清单
74
+ - [ ] 包含材质和纹理细节
75
+ - [ ] 明确光源方向和性质
76
+ - [ ] 使用具体色彩名称(至少3个)
77
+ - [ ] 描述摄像机运动方式和角度
78
+ - [ ] 每个场景标注时间戳
79
+ - [ ] 使用具体动词而非抽象描述
80
+ - [ ] 描述物体间的物理交互
81
+
82
+ ## 输出格式
83
+
84
+ 只输出以下内容,不要输出其他分析:
85
+
86
+ ## SORA2 Prompt (English)
87
+ ```
88
+ [完整的英文提示词,采用专业三段式结构:Style - Cinematography - Scene Breakdown]
89
+ [包含五大支柱要素,使用具体、专业的描述]
90
+ [约200-400词,适合高精度视频复刻]
91
+ ```
92
+
93
+ ## SORA2 提示词 (中文)
94
+ ```
95
+ [对应的中文提示词,保持专业术语和结构]
96
+ ```"""
97
+
98
+ SORA2_USER_PROMPT_TEMPLATE = """这是从一个视频中提取的 {num_frames} 帧关键画面(按时间顺序)。
99
+
100
+ 请作为 SORA2 视频复刻提示词专家,分析这些画面并生成专业的 SORA2 文生视频提示词。
101
+
102
+ ## 分析要求
103
+ 1. 仔细观察每帧画面的:主体特征、动作变化、场景环境、光影效果、色彩风格
104
+ 2. 识别摄像机运动轨迹和镜头切换点
105
+ 3. 推断场景的时间线顺序
106
+ 4. 注意材质细节、光源方向、色彩搭配
107
+
108
+ ## 生成要求
109
+ - 使用五大支柱框架组织提示词
110
+ - 采用三段式结构:Style → Cinematography → Scene Breakdown
111
+ - 每个场景使用具体时间戳(如 0:00s - 0:05s)
112
+ - 动作描述使用精确动词(press, pour, rotate, drift)
113
+ - 包含材质、物理效果、感官细节
114
+ - 英文提示词约 200-400 词
115
+
116
+ ## 输出格式
117
+ 中英文各一个完整的 SORA2 提示词"""
118
+
119
+
120
+ def get_video_files(directory: str = None) -> list:
121
+ """
122
+ 获取指定目录下的所有 MP4 视频文件
123
+
124
+ Args:
125
+ directory: 目录路径,默认为当前项目的 downloads 和 cache 目录
126
+
127
+ Returns:
128
+ 视频文件路径列表
129
+ """
130
+ video_files = []
131
+
132
+ if directory:
133
+ search_dirs = [directory]
134
+ else:
135
+ # 默认搜索目录
136
+ base_dir = Path(__file__).parent
137
+ search_dirs = [
138
+ base_dir / 'downloads',
139
+ base_dir / 'cache',
140
+ base_dir / 'static' / 'videos'
141
+ ]
142
+
143
+ for search_dir in search_dirs:
144
+ if Path(search_dir).exists():
145
+ for file in Path(search_dir).glob('*.mp4'):
146
+ video_files.append(str(file))
147
+
148
+ return video_files
149
+
150
+
151
+ def get_video_duration(video_path: str) -> float:
152
+ """获取视频时长(秒)"""
153
+ try:
154
+ result = subprocess.run(
155
+ [
156
+ 'ffprobe', '-v', 'error',
157
+ '-show_entries', 'format=duration',
158
+ '-of', 'default=noprint_wrappers=1:nokey=1',
159
+ video_path
160
+ ],
161
+ capture_output=True,
162
+ text=True
163
+ )
164
+ return float(result.stdout.strip())
165
+ except Exception:
166
+ return 0
167
+
168
+
169
+ def extract_frames(video_path: str, num_frames: int = MAX_FRAMES) -> list:
170
+ """
171
+ 从视频中提取关键帧
172
+
173
+ Args:
174
+ video_path: 视频文件路径
175
+ num_frames: 要提取的帧数
176
+
177
+ Returns:
178
+ 帧图片路径列表
179
+ """
180
+ duration = get_video_duration(video_path)
181
+ if duration <= 0:
182
+ print("警告: 无法获取视频时长,使用默认间隔")
183
+ duration = 60 # 默认假设60秒
184
+
185
+ # 计算时间间隔
186
+ interval = duration / (num_frames + 1)
187
+
188
+ frames = []
189
+ temp_dir = tempfile.mkdtemp(prefix='video_frames_')
190
+
191
+ print(f"视频时长: {duration:.1f}秒,提取 {num_frames} 帧...")
192
+
193
+ for i in range(num_frames):
194
+ timestamp = interval * (i + 1)
195
+ output_path = os.path.join(temp_dir, f'frame_{i:03d}.jpg')
196
+
197
+ try:
198
+ subprocess.run(
199
+ [
200
+ 'ffmpeg', '-y',
201
+ '-ss', str(timestamp),
202
+ '-i', video_path,
203
+ '-vframes', '1',
204
+ '-q:v', str(int((100 - FRAME_QUALITY) / 10) + 1),
205
+ output_path
206
+ ],
207
+ capture_output=True,
208
+ check=True
209
+ )
210
+
211
+ if os.path.exists(output_path):
212
+ frames.append(output_path)
213
+ print(f" 提取帧 {i+1}/{num_frames} @ {timestamp:.1f}s")
214
+
215
+ except subprocess.CalledProcessError as e:
216
+ print(f" 帧 {i+1} 提取失败: {e}")
217
+
218
+ return frames
219
+
220
+
221
+ def image_to_base64(image_path: str) -> str:
222
+ """将图片转换为 base64 编码"""
223
+ with open(image_path, 'rb') as f:
224
+ image_data = f.read()
225
+ return base64.b64encode(image_data).decode('utf-8')
226
+
227
+
228
+ def analyze_video(video_path: str, prompt: str = None, stream: bool = True,
229
+ num_frames: int = MAX_FRAMES, sora2_mode: bool = False) -> str:
230
+ """
231
+ 使用 Qwen3-VL 模型分析视频内容
232
+
233
+ Args:
234
+ video_path: 视频文件路径
235
+ prompt: 分析提示词
236
+ stream: 是否使用流式输出
237
+ num_frames: 提取的帧数
238
+ sora2_mode: 是否启用 SORA2 提示词生成模式
239
+
240
+ Returns:
241
+ 视频内容描述或 SORA2 提示词
242
+ """
243
+ if not os.path.exists(video_path):
244
+ raise FileNotFoundError(f"视频文件不存在: {video_path}")
245
+
246
+ file_size = os.path.getsize(video_path) / (1024 * 1024)
247
+ print(f"正在读取视频文件: {video_path} ({file_size:.1f}MB)")
248
+
249
+ # 提取视频帧
250
+ frames = extract_frames(video_path, num_frames)
251
+
252
+ if not frames:
253
+ raise RuntimeError("无法提取视频帧,请确保已安装 ffmpeg")
254
+
255
+ print(f"成功提取 {len(frames)} 帧")
256
+
257
+ # 获取视频时长用于 SORA2 分析
258
+ duration = get_video_duration(video_path)
259
+
260
+ # 根据模式选择提示词
261
+ if sora2_mode:
262
+ print("\n🎬 SORA2 提示词生成模式已启用")
263
+ print(f"📊 视频时长: {duration:.1f}秒")
264
+
265
+ # 使用 SORA2 专业提示词
266
+ user_prompt = SORA2_USER_PROMPT_TEMPLATE.format(num_frames=len(frames))
267
+ if duration > 0:
268
+ user_prompt += f"\n\n视频实际时长: {duration:.1f}秒,请根据此时长分配各场景时间。"
269
+
270
+ messages = [
271
+ {'role': 'system', 'content': SORA2_SYSTEM_PROMPT},
272
+ {'role': 'user', 'content': None} # 占位,后面会填充
273
+ ]
274
+ elif prompt:
275
+ user_prompt = prompt
276
+ messages = [{'role': 'user', 'content': None}]
277
+ else:
278
+ # 默认提示词
279
+ user_prompt = f"""这是从一个视频中提取的 {len(frames)} 帧关键画面。
280
+ 请根据这些画面,详细描述这个视频的内容,包括:
281
+ 1. 视频中出现的人物或物体
282
+ 2. 发生的事件或动作
283
+ 3. 场景环境
284
+ 4. 视频的主题或表达的意思
285
+ 5. 视频的整体叙事或故事线"""
286
+ messages = [{'role': 'user', 'content': None}]
287
+
288
+ # 构建消息内容
289
+ content = [{'type': 'text', 'text': user_prompt}]
290
+
291
+ for frame_path in frames:
292
+ frame_base64 = image_to_base64(frame_path)
293
+ content.append({
294
+ 'type': 'image_url',
295
+ 'image_url': {
296
+ 'url': f'data:image/jpeg;base64,{frame_base64}'
297
+ }
298
+ })
299
+
300
+ # 更新最后一条消息的内容
301
+ messages[-1]['content'] = content
302
+
303
+ # 创建 API 客户端
304
+ client = OpenAI(
305
+ base_url=API_BASE_URL,
306
+ api_key=API_KEY,
307
+ )
308
+
309
+ if sora2_mode:
310
+ print("\n🔄 正在分析视频并生成 SORA2 提示词...")
311
+ else:
312
+ print(f"正在分析视频...")
313
+ print("-" * 50)
314
+
315
+ # 调用 API
316
+ response = client.chat.completions.create(
317
+ model=MODEL_ID,
318
+ messages=messages,
319
+ stream=stream
320
+ )
321
+
322
+ # 处理响应
323
+ result = ""
324
+ if stream:
325
+ for chunk in response:
326
+ if chunk.choices and chunk.choices[0].delta.content:
327
+ chunk_content = chunk.choices[0].delta.content
328
+ print(chunk_content, end='', flush=True)
329
+ result += chunk_content
330
+ print() # 换行
331
+ else:
332
+ result = response.choices[0].message.content
333
+ print(result)
334
+
335
+ # 清理临时文件
336
+ for frame_path in frames:
337
+ try:
338
+ os.remove(frame_path)
339
+ except Exception:
340
+ pass
341
+
342
+ try:
343
+ os.rmdir(os.path.dirname(frames[0]))
344
+ except Exception:
345
+ pass
346
+
347
+ if sora2_mode:
348
+ print("\n" + "=" * 50)
349
+ print("✅ SORA2 提示词生成完成!")
350
+ print("💡 提示: 可直接复制上方 English Version 用于 SORA2")
351
+ print("=" * 50)
352
+
353
+ return result
354
+
355
+
356
+ def list_videos():
357
+ """列出项目中所有可用的视频文件"""
358
+ video_files = get_video_files()
359
+
360
+ if not video_files:
361
+ print("未找到任何视频文件")
362
+ return
363
+
364
+ print("=" * 60)
365
+ print("项目中的视频文件:")
366
+ print("=" * 60)
367
+
368
+ for i, video_file in enumerate(video_files, 1):
369
+ file_size = os.path.getsize(video_file) / (1024 * 1024)
370
+ file_name = os.path.basename(video_file)
371
+ duration = get_video_duration(video_file)
372
+ duration_str = f"{duration:.1f}s" if duration > 0 else "未知"
373
+ print(f"{i}. [{file_size:.1f}MB, {duration_str}] {file_name}")
374
+ print(f" 路径: {video_file}")
375
+
376
+ print("=" * 60)
377
+ return video_files
378
+
379
+
380
+ def interactive_mode(sora2_mode: bool = False):
381
+ """交互式模式,让用户选择视频进行分析"""
382
+ video_files = list_videos()
383
+
384
+ if not video_files:
385
+ return
386
+
387
+ mode_hint = " (SORA2模式)" if sora2_mode else ""
388
+ print(f"\n请输入要分析的视频编号{mode_hint} (输入 q 退出):")
389
+
390
+ while True:
391
+ try:
392
+ user_input = input("> ").strip()
393
+
394
+ if user_input.lower() == 'q':
395
+ print("退出程序")
396
+ break
397
+
398
+ index = int(user_input) - 1
399
+ if 0 <= index < len(video_files):
400
+ video_path = video_files[index]
401
+ print(f"\n选择的视频: {os.path.basename(video_path)}")
402
+
403
+ if not sora2_mode:
404
+ # 询问自定义提示词
405
+ custom_prompt = input("输入自定义提示词 (直接回车使用默认): ").strip()
406
+ else:
407
+ custom_prompt = None
408
+
409
+ print("\n" + "=" * 60)
410
+ analyze_video(
411
+ video_path,
412
+ custom_prompt if custom_prompt else None,
413
+ sora2_mode=sora2_mode
414
+ )
415
+ print("=" * 60)
416
+
417
+ print("\n继续选择其他视频,或输入 q 退出:")
418
+ else:
419
+ print(f"无效的编号,请输入 1-{len(video_files)} 之间的数字")
420
+
421
+ except ValueError:
422
+ print("请输入有效的数字")
423
+ except KeyboardInterrupt:
424
+ print("\n退出程序")
425
+ break
426
+ except Exception as e:
427
+ print(f"分析出错: {e}")
428
+ import traceback
429
+ traceback.print_exc()
430
+
431
+
432
+ def main():
433
+ """主函数"""
434
+ parser = argparse.ArgumentParser(
435
+ description='基于 Qwen3-VL 模型的视频内容分析工具 (支持 SORA2 提示词生成)',
436
+ formatter_class=argparse.RawDescriptionHelpFormatter,
437
+ epilog="""
438
+ 示例:
439
+ # 列出所有视频文件
440
+ python qwen3vl.py --list
441
+
442
+ # 分析指定视频
443
+ python qwen3vl.py --video downloads/video.mp4
444
+
445
+ # 🎬 生成 SORA2 文生视频提示词 (推荐)
446
+ python qwen3vl.py --video video.mp4 --sora2
447
+
448
+ # 使用更多帧数生成更精确的 SORA2 提示词
449
+ python qwen3vl.py --video video.mp4 --sora2 --frames 12
450
+
451
+ # 使用自定义提示词分析
452
+ python qwen3vl.py --video video.mp4 --prompt "这个视频讲的是什么故事?"
453
+
454
+ # 交互式 SORA2 模式
455
+ python qwen3vl.py --interactive --sora2
456
+ """
457
+ )
458
+
459
+ parser.add_argument(
460
+ '--video', '-v',
461
+ type=str,
462
+ help='要分析的视频文件路径'
463
+ )
464
+
465
+ parser.add_argument(
466
+ '--sora2', '-s',
467
+ action='store_true',
468
+ help='🎬 启用 SORA2 提示词生成模式,分析视频并输出文生视频提示词'
469
+ )
470
+
471
+ parser.add_argument(
472
+ '--prompt', '-p',
473
+ type=str,
474
+ default=None,
475
+ help='自定义分析提示词 (与 --sora2 互斥)'
476
+ )
477
+
478
+ parser.add_argument(
479
+ '--frames', '-f',
480
+ type=int,
481
+ default=MAX_FRAMES,
482
+ help=f'要提取的视频帧数 (默认: {MAX_FRAMES},SORA2 模式建议 8-12)'
483
+ )
484
+
485
+ parser.add_argument(
486
+ '--list', '-l',
487
+ action='store_true',
488
+ help='列出项目中所有视频文件'
489
+ )
490
+
491
+ parser.add_argument(
492
+ '--interactive', '-i',
493
+ action='store_true',
494
+ help='交互式模式'
495
+ )
496
+
497
+ parser.add_argument(
498
+ '--no-stream',
499
+ action='store_true',
500
+ help='禁用流式输出'
501
+ )
502
+
503
+ args = parser.parse_args()
504
+
505
+ # 如果没有任何参数,显示帮助
506
+ if len(sys.argv) == 1:
507
+ parser.print_help()
508
+ print("\n" + "=" * 60)
509
+ print("💡 快速开始: python qwen3vl.py --video 视频路径.mp4 --sora2")
510
+ print("=" * 60)
511
+ list_videos()
512
+ return
513
+
514
+ if args.list:
515
+ list_videos()
516
+ elif args.interactive:
517
+ interactive_mode(sora2_mode=args.sora2)
518
+ elif args.video:
519
+ # SORA2 模式下忽略自定义 prompt
520
+ prompt = None if args.sora2 else args.prompt
521
+ analyze_video(
522
+ args.video,
523
+ prompt=prompt,
524
+ stream=not args.no_stream,
525
+ num_frames=args.frames,
526
+ sora2_mode=args.sora2
527
+ )
528
+ else:
529
+ parser.print_help()
530
+
531
+
532
+ if __name__ == '__main__':
533
+ main()