diff --git a/.gitignore b/.gitignore index 731c633..f8c55e8 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ __pycache__/ app.log temp_files frames +server.log diff --git a/README.md b/README.md index 441e202..e08a218 100644 --- a/README.md +++ b/README.md @@ -54,23 +54,98 @@ GMapiServer 是一个轻量级、模块化的 API 服务框架,旨在为开发 3. 视频帧提取接口 -• 功能: 从视频中提取指定分辨率、帧率的图片帧,支持强制分辨率调整和填充。 +• 功能: 从视频中提取指定分辨率、帧率的图片帧,支持强制分辨率调整和填充,支持B站BV号。 • 异步接口: POST /api/video_frame/async • 参数: - • video_url: 视频文件 URL + • video_url: 视频文件 URL (或使用 video_bv 参数替代) + + • video_bv: B站视频BV号 (自动转换为URL) • w: 输出宽度 • h: 输出高度 - • fps: 输出帧率 + • fps: 输出帧率 (默认: 30) - • force_resolution: 是否强制调整分辨率 + • force_resolution: 是否强制调整分辨率 (默认: false) - • pad_to_target: 是否填充到目标分辨率 + • pad_to_target: 是否填充到目标分辨率 (默认: false) + +• 返回示例: +```json +{ + "task_id": "abc12345", + "status_url": "http://localhost:5000/api/task/abc12345", + "message": "任务已创建,请使用状态URL查询进度" +} +``` + +• 实时任务状态响应(处理中): +```json +{ + "task_id": "abc12345", + "status": "running", + "progress": 50, + "create_time": 1770393314.1262555, + "start_time": 1770393314.1262555, + "end_time": null, + "type": "video_frame", + "result": { + "audio_urls": { + "audio_dfpwm_url": "/frames/abc12345/audio.dfpwm", + "audio_dfpwm_left_url": "/frames/abc12345/audio_left.dfpwm", + "audio_dfpwm_right_url": "/frames/abc12345/audio_right.dfpwm" + }, + "current_frames": 180, + "total_frames": 6059, + "output_resolution": {"w": 640, "h": 360}, + "frame_urls": [ + "/frames/abc12345/frame_000001.png", + "/frames/abc12345/frame_000002.png", + // ... 当前已生成的帧 + ] + }, + "total_logs": 15, + "last_index": 15, + "new_logs": ["日志1", "日志2"] +} +``` + +• 任务完成时结果: +```json +{ + "task_id": "abc12345", + "status": "completed", + "progress": 100, + "create_time": 1770393314.1262555, + "start_time": 1770393314.1262555, + "end_time": 1770393391.1024792, + "type": "video_frame", + "result": { + "audio_urls": { + "audio_dfpwm_url": "/frames/abc12345/audio.dfpwm", + "audio_dfpwm_left_url": "/frames/abc12345/audio_left.dfpwm", + "audio_dfpwm_right_url": "/frames/abc12345/audio_right.dfpwm" + }, + "duration_seconds": 303.019, + "current_frames": 6059, + "total_frames": 6059, + "fps": 20, + "output_resolution": {"w": 640, "h": 360}, + "frame_urls": [ + "/frames/abc12345/frame_000001.png", + "/frames/abc12345/frame_000002.png", + // ... 所有生成帧 + ] + }, + "total_logs": 164, + "last_index": 164, + "new_logs": ["最后日志"] +} +``` 4. 异步任务状态查询 @@ -178,7 +253,30 @@ return { # 在logging_config.py文件中修改缓存清理时间 CLEANUP_INTERVAL = 3600 # 清理临时文件的间隔(秒) FILE_EXPIRY = 7200 # 文件过期时间(秒) - + + +🔥 视频帧实时进度功能 + +GMapiServer 的视频帧提取功能支持实时进度跟踪,让客户端可以在任务处理过程中实时获取已生成的帧并进行下载。 + +• 实时帧进度跟踪: 自动解析FFmpeg输出的进度信息,实时更新当前帧数 +• 智能进度计算: 基于帧数自动计算转换进度(20-80%) +• 音频预处理: 音频在视频处理前提取,客户端可提前下载音频文件 +• 实时URL返回: 在处理过程中不断返回已生成的帧URL列表 + +## 优势特性 + +1. **减少等待时间**: 客户端无需等待整个视频处理完成 +2. **并行下载**: 可以在转换过程中并行下载已生成的帧 +3. **实时反馈**: 用户可以看到实时处理进度和日志 +4. **资源优化**: 避免了"堵车"效应,提高系统并发能力 + +## 使用建议 + +• 客户端可以轮询 `/api/task/` 接口获取最新状态 +• 通过检查 `result.frame_urls` 列表长度变化确定新生成的帧 +• 音频文件可以立即下载(音频处理优先级更高) + 🔧 异步任务处理流程 diff --git a/main.py b/main.py index c82a628..ba5f9ca 100644 --- a/main.py +++ b/main.py @@ -234,15 +234,15 @@ def video_frame_async_api(): @app.route('/api/task/', methods=['GET']) def get_task_status(task_id): - """查询任务状态和进度(自动返回新增日志)""" + """查询任务状态和进度(自动返回新增日志和实时帧信息)""" with task_lock: if task_id not in task_registry: return jsonify({'error': '任务不存在'}), 404 - + task_info = task_registry[task_id].copy() current_log_count = len(task_info['logs']) last_returned_index = task_info['last_returned_index'] - + # 计算新增日志 if last_returned_index < current_log_count: new_logs = task_info['logs'][last_returned_index:] @@ -250,31 +250,59 @@ def get_task_status(task_id): task_registry[task_id]['last_returned_index'] = current_log_count else: new_logs = [] - + # 构建响应 response = { 'task_id': task_id, 'status': task_info['status'], 'type': task_info['type'], 'create_time': task_info['create_time'], + 'start_time': task_info.get('start_time'), + 'end_time': task_info.get('end_time'), 'progress': task_info.get('progress', 0), 'total_logs': current_log_count, - 'new_logs': new_logs, - 'last_index': current_log_count # 返回当前日志总数,方便客户端跟踪 + 'last_index': current_log_count, + 'new_logs': new_logs } - - if 'start_time' in task_info: - response['start_time'] = task_info['start_time'] - - if 'end_time' in task_info: - response['end_time'] = task_info['end_time'] - - # 根据状态返回不同信息 - if task_info['status'] == 'completed': + + # 视频帧任务特殊处理 + if task_info['type'] == 'video_frame': + result_data = {} + + # 音频URL + if 'audio_urls' in task_info: + result_data['audio_urls'] = task_info['audio_urls'] + + # 当前帧信息 + if 'current_frames' in task_info: + result_data['current_frames'] = task_info['current_frames'] + + # 总帧数 + if 'estimated_total_frames' in task_info: + result_data['total_frames'] = task_info['estimated_total_frames'] + + # 输出分辨率 + if 'output_resolution' in task_info: + result_data['output_resolution'] = task_info['output_resolution'] + + # 帧URL列表 + if 'frame_job_dir' in task_info: + import os + import glob + job_dir = task_info['frame_job_dir'] + if os.path.exists(job_dir): + frame_files = sorted([f for f in os.listdir(job_dir) if f.endswith('.png')]) + frame_urls = [f"/frames/{task_id}/{f}" for f in frame_files] + result_data['frame_urls'] = frame_urls + + response['result'] = result_data + + # 其他类型任务的result处理 + elif task_info['status'] == 'completed': response['result'] = task_info['result'] elif task_info['status'] == 'error': response['error'] = task_info['error'] - + return jsonify(response), 200 # 原有的同步接口保持不变 diff --git a/server.log b/server.log index 1c5e36e..c967c13 100644 --- a/server.log +++ b/server.log @@ -155638,3 +155638,230 @@ MemoryError * Running on http://127.0.0.1:5000 * Running on http://192.168.2.200:5000 2026-01-09 21:38:10,853 - INFO - Press CTRL+C to quit +2026-02-06 22:30:38,133 - INFO - ʼִж... +2026-02-06 22:30:38,133 - INFO - ̨߳ +2026-02-06 22:30:38,134 - INFO - ɣɾ 0 ļ +2026-02-06 22:30:38,134 - INFO - 0 ¼ 0 ļ¼ +2026-02-06 22:30:43,387 - INFO - WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on all addresses (0.0.0.0) + * Running on http://127.0.0.1:5000 + * Running on http://192.168.2.200:5000 +2026-02-06 22:30:43,388 - INFO - Press CTRL+C to quit +2026-02-06 22:31:08,649 - INFO - յ첽Ƶ֡ȡAPI +2026-02-06 22:31:08,678 - INFO - ʼƵ֡ȡ: 32835e4a +2026-02-06 22:31:08,678 - INFO - 첽Ƶ֡ȡ: 32835e4a +2026-02-06 22:31:08,695 - INFO - 127.0.0.1 - - [06/Feb/2026 22:31:08] "POST /api/video_frame/async HTTP/1.1" 202 - +2026-02-06 22:31:16,537 - INFO - 127.0.0.1 - - [06/Feb/2026 22:31:16] "GET /api/task/32835e4a HTTP/1.1" 200 - +2026-02-06 22:31:18,951 - INFO - 127.0.0.1 - - [06/Feb/2026 22:31:18] "GET /api/task/32835e4a HTTP/1.1" 200 - +2026-02-06 22:31:20,228 - INFO - 127.0.0.1 - - [06/Feb/2026 22:31:20] "GET /api/task/32835e4a HTTP/1.1" 200 - +2026-02-06 22:31:21,323 - INFO - 127.0.0.1 - - [06/Feb/2026 22:31:21] "GET /api/task/32835e4a HTTP/1.1" 200 - +2026-02-06 22:31:44,253 - INFO - 127.0.0.1 - - [06/Feb/2026 22:31:44] "GET /api/task/32835e4a HTTP/1.1" 200 - +2026-02-06 22:31:45,386 - INFO - 127.0.0.1 - - [06/Feb/2026 22:31:45] "GET /api/task/32835e4a HTTP/1.1" 200 - +2026-02-06 22:32:00,735 - INFO - 127.0.0.1 - - [06/Feb/2026 22:32:00] "GET /api/task/32835e4a HTTP/1.1" 200 - +2026-02-06 22:32:50,417 - INFO - 127.0.0.1 - - [06/Feb/2026 22:32:50] "GET /api/task/32835e4a HTTP/1.1" 200 - +2026-02-06 22:33:31,467 - INFO - 127.0.0.1 - - [06/Feb/2026 22:33:31] "GET /api/task/32835e4a HTTP/1.1" 200 - +2026-02-06 22:33:36,087 - INFO - ʼִж... +2026-02-06 22:33:36,087 - INFO - ̨߳ +2026-02-06 22:33:36,088 - INFO - ɣɾ 0 ļ +2026-02-06 22:33:36,089 - INFO - 0 ¼ 0 ļ¼ +2026-02-06 22:33:41,091 - INFO - WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on all addresses (0.0.0.0) + * Running on http://127.0.0.1:5000 + * Running on http://192.168.2.200:5000 +2026-02-06 22:33:41,093 - INFO - Press CTRL+C to quit +2026-02-06 22:33:42,350 - INFO - յ첽Ƶ֡ȡAPI +2026-02-06 22:33:42,355 - INFO - ʼƵ֡ȡ: a4a4fdb3 +2026-02-06 22:33:42,355 - INFO - 첽Ƶ֡ȡ: a4a4fdb3 +2026-02-06 22:33:42,367 - INFO - 127.0.0.1 - - [06/Feb/2026 22:33:42] "POST /api/video_frame/async HTTP/1.1" 202 - +2026-02-06 22:33:48,889 - INFO - 127.0.0.1 - - [06/Feb/2026 22:33:48] "GET /api/task/a4a4fdb3 HTTP/1.1" 200 - +2026-02-06 22:34:44,421 - INFO - 127.0.0.1 - - [06/Feb/2026 22:34:44] "GET /api/task/a4a4fdb3 HTTP/1.1" 200 - +2026-02-06 22:34:50,862 - INFO - յ첽Ƶ֡ȡAPI +2026-02-06 22:34:50,884 - INFO - ʼƵ֡ȡ: da312040 +2026-02-06 22:34:50,885 - INFO - 첽Ƶ֡ȡ: da312040 +2026-02-06 22:34:50,897 - INFO - 127.0.0.1 - - [06/Feb/2026 22:34:50] "POST /api/video_frame/async HTTP/1.1" 202 - +2026-02-06 22:34:57,781 - INFO - 127.0.0.1 - - [06/Feb/2026 22:34:57] "GET /api/task/da312040 HTTP/1.1" 200 - +2026-02-06 23:22:31,898 - INFO - յ첽Ƶ֡ȡAPI +2026-02-06 23:22:31,910 - INFO - ʼƵ֡ȡ: 271b78f4 +2026-02-06 23:22:31,911 - INFO - 첽Ƶ֡ȡ: 271b78f4 +2026-02-06 23:22:31,920 - INFO - 127.0.0.1 - - [06/Feb/2026 23:22:31] "POST /api/video_frame/async HTTP/1.1" 202 - +2026-02-06 23:22:38,025 - INFO - 127.0.0.1 - - [06/Feb/2026 23:22:38] "GET /api/task/271b78f4 HTTP/1.1" 200 - +2026-02-06 23:23:10,137 - INFO - 127.0.0.1 - - [06/Feb/2026 23:23:10] "GET /api/task/271b78f4 HTTP/1.1" 200 - +2026-02-06 23:23:43,303 - INFO - 127.0.0.1 - - [06/Feb/2026 23:23:43] "GET /api/task/271b78f4 HTTP/1.1" 200 - +2026-02-06 23:24:09,824 - INFO - յ첽Ƶ֡ȡAPI +2026-02-06 23:24:09,827 - INFO - ʼƵ֡ȡ: 63b05a14 +2026-02-06 23:24:09,827 - INFO - 첽Ƶ֡ȡ: 63b05a14 +2026-02-06 23:24:09,828 - INFO - 127.0.0.1 - - [06/Feb/2026 23:24:09] "POST /api/video_frame/async HTTP/1.1" 202 - +2026-02-06 23:24:20,359 - INFO - 127.0.0.1 - - [06/Feb/2026 23:24:20] "GET /api/task/63b05a14 HTTP/1.1" 200 - +2026-02-06 23:24:30,622 - INFO - 127.0.0.1 - - [06/Feb/2026 23:24:30] "GET /api/task/63b05a14 HTTP/1.1" 200 - +2026-02-06 23:25:44,262 - INFO - 127.0.0.1 - - [06/Feb/2026 23:25:44] "GET /api/task/63b05a14 HTTP/1.1" 200 - +2026-02-06 23:26:47,228 - INFO - 127.0.0.1 - - [06/Feb/2026 23:26:47] "GET /api/task/63b05a14 HTTP/1.1" 200 - +2026-02-06 23:35:27,777 - INFO - ʼִж... +2026-02-06 23:35:27,777 - INFO - ̨߳ +2026-02-06 23:35:27,778 - INFO - ɣɾ 0 ļ +2026-02-06 23:35:27,778 - INFO - 0 ¼ 0 ļ¼ +2026-02-06 23:35:32,970 - INFO - WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on all addresses (0.0.0.0) + * Running on http://127.0.0.1:5000 + * Running on http://192.168.2.200:5000 +2026-02-06 23:35:32,971 - INFO - Press CTRL+C to quit +2026-02-06 23:35:33,484 - INFO - յ첽Ƶ֡ȡAPI +2026-02-06 23:35:33,486 - INFO - ʼƵ֡ȡ: 56f4b33b +2026-02-06 23:35:33,486 - INFO - 첽Ƶ֡ȡ: 56f4b33b +2026-02-06 23:35:33,487 - INFO - 127.0.0.1 - - [06/Feb/2026 23:35:33] "POST /api/video_frame/async HTTP/1.1" 202 - +2026-02-06 23:35:41,217 - INFO - 127.0.0.1 - - [06/Feb/2026 23:35:41] "GET /api/task/56f4b33b HTTP/1.1" 200 - +2026-02-06 23:54:36,540 - INFO - ʼִж... +2026-02-06 23:54:36,540 - INFO - ̨߳ +2026-02-06 23:54:36,542 - INFO - ɣɾ 0 ļ +2026-02-06 23:54:36,542 - INFO - 0 ¼ 0 ļ¼ +2026-02-06 23:54:41,569 - INFO - WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on all addresses (0.0.0.0) + * Running on http://127.0.0.1:5000 + * Running on http://192.168.2.200:5000 +2026-02-06 23:54:41,575 - INFO - Press CTRL+C to quit +2026-02-06 23:55:10,001 - INFO - 127.0.0.1 - - [06/Feb/2026 23:55:10] "GET /api/task/56f4b33b HTTP/1.1" 404 - +2026-02-06 23:55:14,122 - INFO - յ첽Ƶ֡ȡAPI +2026-02-06 23:55:14,126 - INFO - ʼƵ֡ȡ: 4e1b83d7 +2026-02-06 23:55:14,126 - INFO - 첽Ƶ֡ȡ: 4e1b83d7 +2026-02-06 23:55:14,131 - INFO - 127.0.0.1 - - [06/Feb/2026 23:55:14] "POST /api/video_frame/async HTTP/1.1" 202 - +2026-02-06 23:55:19,527 - INFO - 127.0.0.1 - - [06/Feb/2026 23:55:19] "GET /api/task/4e1b83d7 HTTP/1.1" 200 - +2026-02-06 23:55:34,479 - INFO - 127.0.0.1 - - [06/Feb/2026 23:55:34] "GET /api/task/4e1b83d7 HTTP/1.1" 200 - +2026-02-06 23:55:45,865 - INFO - 127.0.0.1 - - [06/Feb/2026 23:55:45] "GET /api/task/4e1b83d7 HTTP/1.1" 200 - +2026-02-06 23:55:53,720 - INFO - 127.0.0.1 - - [06/Feb/2026 23:55:53] "GET /api/task/4e1b83d7 HTTP/1.1" 200 - +2026-02-06 23:56:00,220 - INFO - 127.0.0.1 - - [06/Feb/2026 23:56:00] "GET /api/task/4e1b83d7 HTTP/1.1" 200 - +2026-02-06 23:57:33,074 - INFO - 127.0.0.1 - - [06/Feb/2026 23:57:33] "GET /api/task/4e1b83d7 HTTP/1.1" 200 - +2026-02-07 00:54:36,555 - INFO - ʼִж... +2026-02-07 00:54:36,573 - INFO - ɣɾ 0 ļ +2026-02-07 00:54:36,595 - INFO - 0 ¼ 0 ļ¼ +2026-02-07 01:52:29,676 - INFO - ʼִж... +2026-02-07 01:52:29,676 - INFO - ̨߳ +2026-02-07 01:52:29,677 - INFO - ɣɾ 0 ļ +2026-02-07 01:52:29,678 - INFO - 0 ¼ 0 ļ¼ +2026-02-07 01:52:34,846 - INFO - WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on all addresses (0.0.0.0) + * Running on http://127.0.0.1:5000 + * Running on http://192.168.2.200:5000 +2026-02-07 01:52:34,847 - INFO - Press CTRL+C to quit +2026-02-07 01:52:35,665 - INFO - յ첽Ƶ֡ȡAPI +2026-02-07 01:52:35,685 - INFO - ʼƵ֡ȡ: 3ab15220 +2026-02-07 01:52:35,685 - INFO - 첽Ƶ֡ȡ: 3ab15220 +2026-02-07 01:52:35,692 - INFO - 127.0.0.1 - - [07/Feb/2026 01:52:35] "POST /api/video_frame/async HTTP/1.1" 202 - +2026-02-07 01:52:41,568 - INFO - 127.0.0.1 - - [07/Feb/2026 01:52:41] "GET /api/task/3ab15220 HTTP/1.1" 200 - +2026-02-07 01:52:43,784 - INFO - 127.0.0.1 - - [07/Feb/2026 01:52:43] "GET /api/task/3ab15220 HTTP/1.1" 200 - +2026-02-07 01:52:44,790 - INFO - 127.0.0.1 - - [07/Feb/2026 01:52:44] "GET /api/task/3ab15220 HTTP/1.1" 200 - +2026-02-07 01:52:45,661 - INFO - 127.0.0.1 - - [07/Feb/2026 01:52:45] "GET /api/task/3ab15220 HTTP/1.1" 200 - +2026-02-07 01:52:50,458 - INFO - 127.0.0.1 - - [07/Feb/2026 01:52:50] "GET /api/task/3ab15220 HTTP/1.1" 200 - +2026-02-07 01:52:51,300 - INFO - 127.0.0.1 - - [07/Feb/2026 01:52:51] "GET /api/task/3ab15220 HTTP/1.1" 200 - +2026-02-07 01:53:02,403 - INFO - 127.0.0.1 - - [07/Feb/2026 01:53:02] "GET /api/task/3ab15220 HTTP/1.1" 200 - +2026-02-07 01:53:03,310 - INFO - 127.0.0.1 - - [07/Feb/2026 01:53:03] "GET /api/task/3ab15220 HTTP/1.1" 200 - +2026-02-07 01:53:04,251 - INFO - 127.0.0.1 - - [07/Feb/2026 01:53:04] "GET /api/task/3ab15220 HTTP/1.1" 200 - +2026-02-07 01:53:05,080 - INFO - 127.0.0.1 - - [07/Feb/2026 01:53:05] "GET /api/task/3ab15220 HTTP/1.1" 200 - +2026-02-07 01:53:05,803 - INFO - 127.0.0.1 - - [07/Feb/2026 01:53:05] "GET /api/task/3ab15220 HTTP/1.1" 200 - +2026-02-07 01:53:06,659 - INFO - 127.0.0.1 - - [07/Feb/2026 01:53:06] "GET /api/task/3ab15220 HTTP/1.1" 200 - +2026-02-07 01:53:07,289 - INFO - 127.0.0.1 - - [07/Feb/2026 01:53:07] "GET /api/task/3ab15220 HTTP/1.1" 200 - +2026-02-07 01:53:16,809 - INFO - ʼִж... +2026-02-07 01:53:16,810 - INFO - ̨߳ +2026-02-07 01:53:16,810 - INFO - ɣɾ 0 ļ +2026-02-07 01:53:16,810 - INFO - 0 ¼ 0 ļ¼ +2026-02-07 01:53:21,754 - INFO - WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on all addresses (0.0.0.0) + * Running on http://127.0.0.1:5000 + * Running on http://192.168.2.200:5000 +2026-02-07 01:53:21,754 - INFO - Press CTRL+C to quit +2026-02-07 01:53:22,104 - INFO - յ첽Ƶ֡ȡAPI +2026-02-07 01:53:22,105 - INFO - ʼƵ֡ȡ: ec19c8de +2026-02-07 01:53:22,106 - INFO - 첽Ƶ֡ȡ: ec19c8de +2026-02-07 01:53:22,106 - INFO - 127.0.0.1 - - [07/Feb/2026 01:53:22] "POST /api/video_frame/async HTTP/1.1" 202 - +2026-02-07 01:53:27,823 - INFO - 127.0.0.1 - - [07/Feb/2026 01:53:27] "GET /api/task/ec19c8de HTTP/1.1" 200 - +2026-02-07 01:53:30,306 - INFO - 127.0.0.1 - - [07/Feb/2026 01:53:30] "GET /api/task/ec19c8de HTTP/1.1" 200 - +2026-02-07 01:53:31,180 - INFO - 127.0.0.1 - - [07/Feb/2026 01:53:31] "GET /api/task/ec19c8de HTTP/1.1" 200 - +2026-02-07 01:53:37,386 - INFO - 127.0.0.1 - - [07/Feb/2026 01:53:37] "GET /api/task/ec19c8de HTTP/1.1" 200 - +2026-02-07 01:53:44,861 - INFO - 127.0.0.1 - - [07/Feb/2026 01:53:44] "GET /api/task/ec19c8de HTTP/1.1" 200 - +2026-02-07 01:53:45,844 - INFO - 127.0.0.1 - - [07/Feb/2026 01:53:45] "GET /api/task/ec19c8de HTTP/1.1" 200 - +2026-02-07 01:53:55,848 - INFO - 127.0.0.1 - - [07/Feb/2026 01:53:55] "GET /api/task/ec19c8de HTTP/1.1" 200 - +2026-02-07 01:53:57,028 - INFO - 127.0.0.1 - - [07/Feb/2026 01:53:57] "GET /api/task/ec19c8de HTTP/1.1" 200 - +2026-02-07 01:53:57,806 - INFO - 127.0.0.1 - - [07/Feb/2026 01:53:57] "GET /api/task/ec19c8de HTTP/1.1" 200 - +2026-02-07 01:54:11,517 - INFO - 127.0.0.1 - - [07/Feb/2026 01:54:11] "GET /api/task/ec19c8de HTTP/1.1" 200 - +2026-02-07 01:54:12,356 - INFO - 127.0.0.1 - - [07/Feb/2026 01:54:12] "GET /api/task/ec19c8de HTTP/1.1" 200 - +2026-02-07 01:54:13,112 - INFO - 127.0.0.1 - - [07/Feb/2026 01:54:13] "GET /api/task/ec19c8de HTTP/1.1" 200 - +2026-02-07 01:54:13,962 - INFO - 127.0.0.1 - - [07/Feb/2026 01:54:13] "GET /api/task/ec19c8de HTTP/1.1" 200 - +2026-02-07 01:54:14,796 - INFO - 127.0.0.1 - - [07/Feb/2026 01:54:14] "GET /api/task/ec19c8de HTTP/1.1" 200 - +2026-02-07 01:54:23,249 - INFO - 127.0.0.1 - - [07/Feb/2026 01:54:23] "GET /api/task/ec19c8de HTTP/1.1" 200 - +2026-02-07 01:54:24,088 - INFO - 127.0.0.1 - - [07/Feb/2026 01:54:24] "GET /api/task/ec19c8de HTTP/1.1" 200 - +2026-02-07 01:54:24,797 - INFO - 127.0.0.1 - - [07/Feb/2026 01:54:24] "GET /api/task/ec19c8de HTTP/1.1" 200 - +2026-02-07 01:54:27,677 - INFO - 127.0.0.1 - - [07/Feb/2026 01:54:27] "GET /api/task/ec19c8de HTTP/1.1" 200 - +2026-02-07 01:54:28,476 - INFO - 127.0.0.1 - - [07/Feb/2026 01:54:28] "GET /api/task/ec19c8de HTTP/1.1" 200 - +2026-02-07 01:54:29,159 - INFO - 127.0.0.1 - - [07/Feb/2026 01:54:29] "GET /api/task/ec19c8de HTTP/1.1" 200 - +2026-02-07 01:54:56,792 - INFO - 127.0.0.1 - - [07/Feb/2026 01:54:56] "GET /api/task/ec19c8de HTTP/1.1" 200 - +2026-02-07 01:54:57,709 - INFO - 127.0.0.1 - - [07/Feb/2026 01:54:57] "GET /api/task/ec19c8de HTTP/1.1" 200 - +2026-02-07 01:55:03,799 - INFO - ʼִж... +2026-02-07 01:55:03,799 - INFO - ̨߳ +2026-02-07 01:55:03,799 - INFO - ɣɾ 0 ļ +2026-02-07 01:55:03,800 - INFO - 0 ¼ 0 ļ¼ +2026-02-07 01:55:08,773 - INFO - WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on all addresses (0.0.0.0) + * Running on http://127.0.0.1:5000 + * Running on http://192.168.2.200:5000 +2026-02-07 01:55:08,774 - INFO - Press CTRL+C to quit +2026-02-07 01:55:21,159 - INFO - յ첽Ƶ֡ȡAPI +2026-02-07 01:55:21,160 - INFO - ʼƵ֡ȡ: 482dc8ca +2026-02-07 01:55:21,160 - INFO - 첽Ƶ֡ȡ: 482dc8ca +2026-02-07 01:55:21,160 - INFO - 127.0.0.1 - - [07/Feb/2026 01:55:21] "POST /api/video_frame/async HTTP/1.1" 202 - +2026-02-07 01:55:26,702 - INFO - 127.0.0.1 - - [07/Feb/2026 01:55:26] "GET /api/task/482dc8ca HTTP/1.1" 200 - +2026-02-07 01:55:54,574 - INFO - 127.0.0.1 - - [07/Feb/2026 01:55:54] "GET /api/task/482dc8ca HTTP/1.1" 200 - +2026-02-07 01:55:55,493 - INFO - 127.0.0.1 - - [07/Feb/2026 01:55:55] "GET /api/task/482dc8ca HTTP/1.1" 200 - +2026-02-07 01:55:58,750 - INFO - 127.0.0.1 - - [07/Feb/2026 01:55:58] "GET /api/task/482dc8ca HTTP/1.1" 200 - +2026-02-07 01:55:59,482 - INFO - 127.0.0.1 - - [07/Feb/2026 01:55:59] "GET /api/task/482dc8ca HTTP/1.1" 200 - +2026-02-07 01:56:00,212 - INFO - 127.0.0.1 - - [07/Feb/2026 01:56:00] "GET /api/task/482dc8ca HTTP/1.1" 200 - +2026-02-07 01:56:00,985 - INFO - 127.0.0.1 - - [07/Feb/2026 01:56:00] "GET /api/task/482dc8ca HTTP/1.1" 200 - +2026-02-07 01:56:01,521 - INFO - 127.0.0.1 - - [07/Feb/2026 01:56:01] "GET /api/task/482dc8ca HTTP/1.1" 200 - +2026-02-07 01:56:02,180 - INFO - 127.0.0.1 - - [07/Feb/2026 01:56:02] "GET /api/task/482dc8ca HTTP/1.1" 200 - +2026-02-07 01:56:02,773 - INFO - 127.0.0.1 - - [07/Feb/2026 01:56:02] "GET /api/task/482dc8ca HTTP/1.1" 200 - +2026-02-07 01:56:03,446 - INFO - 127.0.0.1 - - [07/Feb/2026 01:56:03] "GET /api/task/482dc8ca HTTP/1.1" 200 - +2026-02-07 01:57:40,035 - INFO - 127.0.0.1 - - [07/Feb/2026 01:57:40] "GET /api/task/482dc8ca HTTP/1.1" 200 - +2026-02-07 01:57:50,306 - INFO - 127.0.0.1 - - [07/Feb/2026 01:57:50] "GET /api/task/482dc8ca HTTP/1.1" 200 - +2026-02-07 01:57:53,023 - INFO - 127.0.0.1 - - [07/Feb/2026 01:57:53] "GET /api/task/482dc8ca HTTP/1.1" 200 - +2026-02-07 01:57:53,842 - INFO - 127.0.0.1 - - [07/Feb/2026 01:57:53] "GET /api/task/482dc8ca HTTP/1.1" 200 - +2026-02-07 01:57:54,885 - INFO - 127.0.0.1 - - [07/Feb/2026 01:57:54] "GET /api/task/482dc8ca HTTP/1.1" 200 - +2026-02-07 01:57:55,526 - INFO - 127.0.0.1 - - [07/Feb/2026 01:57:55] "GET /api/task/482dc8ca HTTP/1.1" 200 - +2026-02-07 01:57:55,928 - INFO - 127.0.0.1 - - [07/Feb/2026 01:57:55] "GET /api/task/482dc8ca HTTP/1.1" 200 - +2026-02-07 01:57:56,309 - INFO - 127.0.0.1 - - [07/Feb/2026 01:57:56] "GET /api/task/482dc8ca HTTP/1.1" 200 - +2026-02-07 01:57:56,788 - INFO - 127.0.0.1 - - [07/Feb/2026 01:57:56] "GET /api/task/482dc8ca HTTP/1.1" 200 - +2026-02-07 01:57:57,192 - INFO - 127.0.0.1 - - [07/Feb/2026 01:57:57] "GET /api/task/482dc8ca HTTP/1.1" 200 - +2026-02-07 01:58:17,463 - INFO - ʼִж... +2026-02-07 01:58:17,463 - INFO - ̨߳ +2026-02-07 01:58:17,464 - INFO - ɣɾ 0 ļ +2026-02-07 01:58:17,464 - INFO - 0 ¼ 0 ļ¼ +2026-02-07 01:58:22,439 - INFO - WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on all addresses (0.0.0.0) + * Running on http://127.0.0.1:5000 + * Running on http://192.168.2.200:5000 +2026-02-07 01:58:22,440 - INFO - Press CTRL+C to quit +2026-02-07 01:58:23,081 - INFO - յ첽Ƶ֡ȡAPI +2026-02-07 01:58:23,082 - INFO - ʼƵ֡ȡ: 41ca0a3f +2026-02-07 01:58:23,082 - INFO - 첽Ƶ֡ȡ: 41ca0a3f +2026-02-07 01:58:23,083 - INFO - 127.0.0.1 - - [07/Feb/2026 01:58:23] "POST /api/video_frame/async HTTP/1.1" 202 - +2026-02-07 01:58:28,344 - INFO - 127.0.0.1 - - [07/Feb/2026 01:58:28] "GET /api/task/41ca0a3f HTTP/1.1" 200 - +2026-02-07 01:58:35,347 - INFO - 127.0.0.1 - - [07/Feb/2026 01:58:35] "GET /api/task/41ca0a3f HTTP/1.1" 200 - +2026-02-07 01:58:36,870 - INFO - 127.0.0.1 - - [07/Feb/2026 01:58:36] "GET /api/task/41ca0a3f HTTP/1.1" 200 - +2026-02-07 01:59:04,697 - INFO - 127.0.0.1 - - [07/Feb/2026 01:59:04] "GET /api/task/41ca0a3f HTTP/1.1" 200 - +2026-02-07 02:02:15,718 - INFO - ʼִж... +2026-02-07 02:02:15,718 - INFO - ̨߳ +2026-02-07 02:02:15,719 - INFO - ɣɾ 0 ļ +2026-02-07 02:02:15,719 - INFO - 0 ¼ 0 ļ¼ +2026-02-07 02:02:20,676 - INFO - WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on all addresses (0.0.0.0) + * Running on http://127.0.0.1:5000 + * Running on http://192.168.2.200:5000 +2026-02-07 02:02:20,676 - INFO - Press CTRL+C to quit +2026-02-07 02:02:21,030 - INFO - յ첽Ƶ֡ȡAPI +2026-02-07 02:02:21,030 - INFO - ʼƵ֡ȡ: d3c23dac +2026-02-07 02:02:21,030 - INFO - 첽Ƶ֡ȡ: d3c23dac +2026-02-07 02:02:21,031 - INFO - 127.0.0.1 - - [07/Feb/2026 02:02:21] "POST /api/video_frame/async HTTP/1.1" 202 - +2026-02-07 02:02:27,517 - INFO - 127.0.0.1 - - [07/Feb/2026 02:02:27] "GET /api/task/d3c23dac HTTP/1.1" 200 - +2026-02-07 02:02:36,175 - INFO - 127.0.0.1 - - [07/Feb/2026 02:02:36] "GET /api/task/d3c23dac HTTP/1.1" 200 - +2026-02-07 02:02:40,816 - INFO - 127.0.0.1 - - [07/Feb/2026 02:02:40] "GET /api/task/d3c23dac HTTP/1.1" 200 - +2026-02-07 02:02:59,780 - INFO - 127.0.0.1 - - [07/Feb/2026 02:02:59] "GET /api/task/d3c23dac HTTP/1.1" 200 - +2026-02-07 02:03:15,855 - INFO - 127.0.0.1 - - [07/Feb/2026 02:03:15] "GET /api/task/d3c23dac HTTP/1.1" 200 - +2026-02-07 02:03:18,900 - INFO - 127.0.0.1 - - [07/Feb/2026 02:03:18] "GET /api/task/d3c23dac HTTP/1.1" 200 - +2026-02-07 02:03:20,213 - INFO - 127.0.0.1 - - [07/Feb/2026 02:03:20] "GET /api/task/d3c23dac HTTP/1.1" 200 - +2026-02-07 02:03:21,317 - INFO - 127.0.0.1 - - [07/Feb/2026 02:03:21] "GET /api/task/d3c23dac HTTP/1.1" 200 - diff --git a/video_frame_utils.py b/video_frame_utils.py index c29016e..fb0848a 100644 --- a/video_frame_utils.py +++ b/video_frame_utils.py @@ -29,6 +29,27 @@ else: FFMPEG_PATH = os.path.join(BASE_DIR, 'lib', 'ffmpeg', 'bin', 'ffmpeg') FFPROBE_PATH = os.path.join(BASE_DIR, 'lib', 'ffmpeg', 'bin', 'ffprobe') +def parse_ffmpeg_frame_progress(line, task_id, task_registry, task_lock): + """解析FFmpeg进度输出并更新任务状态""" + import re + # 匹配帧数:frame= 180 fps= 90 + frame_match = re.search(r'frame=\s*(\d+)', line) + if frame_match: + current_frame = int(frame_match.group(1)) + + # 更新任务状态的当前帧数 + with task_lock: + if task_id in task_registry: + task_registry[task_id]['current_frames'] = current_frame + + # 如果有总帧数估计,可以计算进度 + if 'estimated_total_frames' in task_registry[task_id]: + total_frames = task_registry[task_id]['estimated_total_frames'] + if total_frames > 0: + progress = min(80, int((current_frame / total_frames) * 60) + 20) # 20-80%为转换进度 + task_registry[task_id]['progress'] = progress + + def log_subprocess_output(pipe, task_id, task_registry, task_lock, prefix=""): """从管道实时读取并记录日志""" if not pipe: @@ -38,12 +59,30 @@ def log_subprocess_output(pipe, task_id, task_registry, task_lock, prefix=""): if line: clean_line = line.strip() if clean_line and task_id and task_registry and task_lock: - add_task_log(task_id, f"{prefix}{clean_line}", task_registry, task_lock) + # 过滤掉一些不必要的进度字符 + if '\r' in clean_line: + clean_line = clean_line.replace('\r', '') + + # 过滤掉过于频繁的进度更新(只记录有意义的内容) + if prefix.startswith("[yt-dlp") and clean_line.startswith('['): + # 只记录重要的进度信息,过滤掉过于频繁的百分比更新 + if '%' not in clean_line or '100%' in clean_line or 'Downloading' in clean_line: + add_task_log(task_id, f"{prefix}{clean_line}", task_registry, task_lock) + elif prefix == "[FFmpeg] " and "frame=" in clean_line: + # FFmpeg进度信息 + add_task_log(task_id, f"{prefix}{clean_line}", task_registry, task_lock) + parse_ffmpeg_frame_progress(clean_line, task_id, task_registry, task_lock) + else: + # 其他日志正常记录 + add_task_log(task_id, f"{prefix}{clean_line}", task_registry, task_lock) except Exception as e: if task_id and task_registry and task_lock: add_task_log(task_id, f"[日志读取错误] {e}", task_registry, task_lock) finally: - pipe.close() + try: + pipe.close() + except: + pass # ====================== # 任务处理函数 @@ -104,27 +143,46 @@ def process_video_frame_extraction(data, file_registry, file_lock, task_id=None, # === 替换 yt-dlp 下载部分 === if task_id and task_registry and task_lock: add_task_log(task_id, f"开始下载视频: {video_url}", task_registry, task_lock) + with task_lock: + if task_id in task_registry: + task_registry[task_id]['progress'] = 20 # 开始下载,进度20% yt_dlp_cmd = [ sys.executable, '-m', 'yt_dlp', video_url, '-o', temp_base, + '-f', 'bv*[height<=720]+ba/b', + '--no-warnings', + '--progress', # 启用进度显示 + '--newline', # 确保换行符正常 + '--console-title', # 确保进度信息正确输出 + '--no-colors', # 禁用颜色,避免控制字符干扰 ] - # 使用 Popen 实时捕获 stderr + # 使用 Popen 实时捕获 stdout 和 stderr proc = subprocess.Popen( yt_dlp_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1, - universal_newlines=True + universal_newlines=True, + encoding='utf-8', + errors='replace' ) - # 启动日志线程(只读 stderr,因为 yt-dlp 进度在 stderr) + # 启动日志线程(yt-dlp 进度主要在 stdout) + stdout_thread = threading.Thread( + target=log_subprocess_output, + args=(proc.stdout, task_id, task_registry, task_lock, "[yt-dlp] "), + daemon=True + ) + stdout_thread.start() + + # stderr 也可能有重要信息 stderr_thread = threading.Thread( target=log_subprocess_output, - args=(proc.stderr, task_id, task_registry, task_lock, "[yt-dlp] "), + args=(proc.stderr, task_id, task_registry, task_lock, "[yt-dlp-error] "), daemon=True ) stderr_thread.start() @@ -132,12 +190,34 @@ def process_video_frame_extraction(data, file_registry, file_lock, task_id=None, try: proc.wait(timeout=600) if proc.returncode != 0: - raise subprocess.CalledProcessError(proc.returncode, yt_dlp_cmd) + # 获取更详细的错误信息 + error_details = [] + try: + if proc.stderr: + stderr_content = proc.stderr.read() + if stderr_content: + error_details.append(f"stderr: {stderr_content}") + if proc.stdout: + stdout_content = proc.stdout.read() + if stdout_content: + error_details.append(f"stdout: {stdout_content}") + except: + pass + + error_msg = f"yt-dlp 下载失败 (返回码: {proc.returncode})" + if error_details: + error_msg += f" - {' '.join(error_details[:500])}" # 限制错误信息长度 + + raise subprocess.CalledProcessError(proc.returncode, yt_dlp_cmd, output=error_msg) except subprocess.TimeoutExpired: proc.kill() + if task_id and task_registry and task_lock: + add_task_log(task_id, "yt-dlp 下载超时(超过10分钟)", task_registry, task_lock) raise Exception("yt-dlp 下载超时(超过10分钟)") finally: - stderr_thread.join(timeout=5) # 等待日志线程结束 + # 等待日志线程结束 + stdout_thread.join(timeout=5) + stderr_thread.join(timeout=5) # 查找实际生成的文件 candidates = glob.glob(temp_base + ".*") @@ -153,9 +233,79 @@ def process_video_frame_extraction(data, file_registry, file_lock, task_id=None, if task_id in task_registry: task_registry[task_id]['progress'] = 40 + # 首先处理音频(在视频处理之前) + audio_exists = has_audio(temp_video) + audio_urls = {} + + if audio_exists: + if task_id and task_registry and task_lock: + add_task_log(task_id, "提取音频(左/右/混合声道)", task_registry, task_lock) + with task_lock: + if task_id in task_registry: + task_registry[task_id]['progress'] = 40 + + # 公共参数 + dfpwm_args = [ + FFMPEG_PATH, '-y', + '-i',temp_video, + '-vn', + '-ar', '48000', + '-ac', '1', + '-f', 'dfpwm' + ] + + # 混合声道 + dfpwm_path_mix = os.path.join(job_dir, "audio.dfpwm") + subprocess.run( + dfpwm_args + [dfpwm_path_mix], + check=True, capture_output=True + ) + + # 左声道 + dfpwm_path_left = os.path.join(job_dir, "audio_left.dfpwm") + subprocess.run( + dfpwm_args + ['-af', 'pan=mono|c0=c0'] + [dfpwm_path_left], + check=True, capture_output=True + ) + + # 右声道 + dfpwm_path_right = os.path.join(job_dir, "audio_right.dfpwm") + subprocess.run( + dfpwm_args + ['-af', 'pan=mono|c0=c1'] + [dfpwm_path_right], + check=True, capture_output=True + ) + + audio_urls = { + "audio_dfpwm_url": f"/frames/{task_id}/audio.dfpwm", + "audio_dfpwm_left_url": f"/frames/{task_id}/audio_left.dfpwm", + "audio_dfpwm_right_url": f"/frames/{task_id}/audio_right.dfpwm" + } + + # 更新音频URL到任务状态 + if task_id and task_registry and task_lock: + with task_lock: + if task_id in task_registry: + task_registry[task_id]['audio_urls'] = audio_urls + # 获取视频信息 duration = get_video_duration(temp_video) + # 估计总帧数 + estimated_total_frames = int(duration * fps) + + # 初始化任务状态的帧相关字段 + if task_id and task_registry and task_lock: + with task_lock: + if task_id in task_registry: + task_registry[task_id]['estimated_total_frames'] = estimated_total_frames + task_registry[task_id]['current_frames'] = 0 + task_registry[task_id]['generated_frame_urls'] = [] + task_registry[task_id]['frame_job_dir'] = job_dir + task_registry[task_id]['frame_params'] = { + 'w': w, 'h': h, 'fps': fps, + 'force_resolution': force_resolution, 'pad_to_target': pad_to_target + } + # 构建滤镜 vf = build_video_filter(w, h, fps, force_resolution, pad_to_target) @@ -212,46 +362,12 @@ def process_video_frame_extraction(data, file_registry, file_lock, task_id=None, out_w, out_h = get_output_resolution(job_dir) total_frames = len([f for f in os.listdir(job_dir) if f.endswith('.png')]) - # 检查音频 - audio_exists = has_audio(temp_video) - - if audio_exists: - if task_id and task_registry and task_lock: - add_task_log(task_id, "提取音频(左/右/混合声道)", task_registry, task_lock) - with task_lock: - if task_id in task_registry: - task_registry[task_id]['progress'] = 90 - - # 公共参数 - dfpwm_args = [ - FFMPEG_PATH, '-y', - '-i', temp_video, - '-vn', - '-ar', '48000', - '-ac', '1', - '-f', 'dfpwm' - ] - - # 混合声道 - dfpwm_path_mix = os.path.join(job_dir, "audio.dfpwm") - subprocess.run( - dfpwm_args + [dfpwm_path_mix], - check=True, capture_output=True - ) - - # 左声道 - dfpwm_path_left = os.path.join(job_dir, "audio_left.dfpwm") - subprocess.run( - dfpwm_args + ['-af', 'pan=mono|c0=c0'] + [dfpwm_path_left], - check=True, capture_output=True - ) - - # 右声道 - dfpwm_path_right = os.path.join(job_dir, "audio_right.dfpwm") - subprocess.run( - dfpwm_args + ['-af', 'pan=mono|c0=c1'] + [dfpwm_path_right], - check=True, capture_output=True - ) + # 保存输出分辨率和实际总帧数到任务状态 + if task_id and task_registry and task_lock: + with task_lock: + if task_id in task_registry: + task_registry[task_id]['output_resolution'] = {"w": out_w, "h": out_h} + task_registry[task_id]['estimated_total_frames'] = total_frames # 生成结果 result_data = {