上传文件至 8.0

This commit is contained in:
jcy 2025-05-14 17:49:03 +08:00
parent 6de1b4d12a
commit df9963eb0f
5 changed files with 633 additions and 0 deletions

45
8.0/README.md Normal file
View File

@ -0,0 +1,45 @@
# 视频处理系统
这是一个基于Flask的视频处理Web应用提供视频剪辑、合并、滤镜等功能。
## 系统要求
- Python 3.7+
- Windows 10/11或Linux系统
- 现代浏览器Chrome、Firefox、Edge等
## 安装
1. 解压缩本包到任意目录
2. 安装依赖:
```
pip install -r requirements.txt
```
## 使用方法
### Windows用户
双击运行 `启动服务.bat`,将自动启动服务并打开浏览器。
### Linux/Mac用户
在终端中运行:
```
python server.py
```
然后在浏览器中访问http://localhost:5000
## 文件夹说明
- `templates/`: 网页模板
- `static/`: 静态资源文件
- `uploads/`: 上传的视频将存储在此处
- `output/`: 处理后的视频输出目录
## 注意事项
- 首次使用时,系统会自动创建必要的目录
- 视频处理可能需要较高的系统资源,请确保计算机有足够内存
- 支持的视频格式MP4, AVI, MOV等常见格式

BIN
8.0/process.log Normal file

Binary file not shown.

82
8.0/requirements.txt Normal file
View File

@ -0,0 +1,82 @@
# 视频处理系统核心依赖
flask
flask-cors
imageio
imageio-ffmpeg
numpy
opencv-python
Pillow
# 自动检测到的依赖(可能需要手动筛选)
audioread==3.0.1
blinker @ file:///C:/b/abs_b1i87khtob/croot/blinker_1737448732095/work
certifi==2025.1.31
cffi==1.17.1
chardet==5.2.0
charset-normalizer==3.4.1
click @ file:///C:/b/abs_42rbjngfys/croot/click_1744271606983/work
colorama @ file:///C:/b/abs_a9ozq0l032/croot/colorama_1672387194846/work
contourpy==1.3.2
cycler==0.12.1
decorator==4.4.2
filelock==3.18.0
Flask @ file:///C:/b/abs_a6m37uz578/croot/flask_1737454335662/work
Flask-Cors @ file:///tmp/build/80754af9/flask-cors_1609957731919/work
fonttools==4.57.0
fsspec==2025.3.2
huggingface-hub==0.30.2
idna==3.10
itsdangerous @ file:///C:/b/abs_c4vwgdr5yn/croot/itsdangerous_1716533399914/work
Jinja2 @ file:///C:/b/abs_920kup4e6u/croot/jinja2_1741711580669/work
joblib==1.4.2
kiwisolver==1.4.8
lazy_loader==0.4
librosa==0.11.0
llvmlite==0.44.0
lxml==5.3.2
MarkupSafe @ file:///C:/b/abs_a0ma7ge0jc/croot/markupsafe_1738584052792/work
matplotlib==3.10.1
more-itertools==10.6.0
moviepy==1.0.3
mpmath==1.3.0
msgpack==1.1.0
networkx==3.4.2
numba==0.61.2
openai-whisper==20240930
opencv-python-headless==4.10.0
packaging==24.2
pdfkit==1.0.0
platformdirs==4.3.7
pooch==1.8.2
proglog==0.1.11
psutil==7.0.0
pycparser==2.22
pydub==0.25.1
pyparsing==3.2.3
pytesseract==0.3.13
python-dateutil==2.9.0.post0
python-docx==0.8.11
PyYAML==6.0.2
regex==2024.11.6
reportlab==4.4.0
requests==2.32.3
safetensors==0.5.3
scikit-image==0.25.2
scikit-learn==1.6.1
scipy==1.15.2
six @ file:///C:/b/abs_149wuyuo1o/croot/six_1744271521515/work
soundfile==0.13.1
soxr==0.5.0.post1
sympy==1.13.1
threadpoolctl==3.6.0
tifffile==2025.3.30
tiktoken==0.9.0
tokenizers==0.13.3
torch==2.6.0
torchaudio==2.6.0
tqdm==4.67.1
transformers==4.30.2
typing_extensions==4.13.2
urllib3==2.4.0
Werkzeug @ file:///C:/b/abs_c7bupijx2_/croot/werkzeug_1737448709012/work
whisper==1.1.10

8
8.0/server.log Normal file
View File

@ -0,0 +1,8 @@
2025-05-14 17:47:06,505 - server - INFO - 启动服务器...
2025-05-14 17:47:28,178 - server - INFO - 收到上传请求
2025-05-14 17:47:28,233 - server - INFO - [任务 463da5e4-0f14-45e5-be84-f92118e11070] 上传文件信息: 文件名=input4.mp4, 大小=3834889字节
2025-05-14 17:47:28,234 - server - INFO - [任务 463da5e4-0f14-45e5-be84-f92118e11070] 正在保存文件到: D:\cursor-test\视频处理系统_v\uploads\463da5e4-0f14-45e5-be84-f92118e11070_input4.mp4
2025-05-14 17:47:28,235 - server - INFO - [任务 463da5e4-0f14-45e5-be84-f92118e11070] 磁盘空间检查: 需要 0.01GB,可用 39.96GB
2025-05-14 17:47:28,245 - server - INFO - [任务 463da5e4-0f14-45e5-be84-f92118e11070] 文件保存成功: D:\cursor-test\视频处理系统_v\uploads\463da5e4-0f14-45e5-be84-f92118e11070_input4.mp4
2025-05-14 17:47:28,246 - server - INFO - [任务 463da5e4-0f14-45e5-be84-f92118e11070] 文件大小验证: 上传=3834889字节, 保存=3834706字节, 差异=183字节
2025-05-14 17:47:28,249 - server - INFO - [任务 463da5e4-0f14-45e5-be84-f92118e11070] 开始处理视频: input4.mp4

498
8.0/server.py Normal file
View File

@ -0,0 +1,498 @@
from flask import Flask, request, jsonify, render_template, send_from_directory
from flask_cors import CORS
import os
import traceback
import logging
import ctypes
import shutil
import threading
import time
import uuid
import json
from concurrent.futures import ThreadPoolExecutor
from werkzeug.utils import secure_filename
from 毕设 import main_process
# 导入补丁模块 - 用于解决wkhtmltopdf依赖问题
try:
import pdfkit_patch as pdfkit
print("已加载pdfkit补丁模块")
except ImportError:
print("未找到pdfkit补丁模块PDF生成功能可能不可用")
# 配置日志
log_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'server.log')
logger = logging.getLogger('server')
logger.setLevel(logging.DEBUG)
# 创建文件处理器指定编码为utf-8
file_handler = logging.FileHandler(log_file, encoding='utf-8', mode='w')
file_handler.setLevel(logging.DEBUG)
# 创建控制台处理器
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.DEBUG)
# 创建格式化器
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
file_handler.setFormatter(formatter)
console_handler.setFormatter(formatter)
# 添加处理器到logger
logger.addHandler(file_handler)
logger.addHandler(console_handler)
# 确保日志文件存在
if not os.path.exists(log_file):
with open(log_file, 'w', encoding='utf-8') as f:
f.write('')
app = Flask(__name__)
CORS(app) # 启用CORS支持
# 配置上传文件夹
UPLOAD_FOLDER = os.path.abspath('uploads')
OUTPUT_FOLDER = os.path.abspath('output')
TASK_STATUS_FILE = os.path.join(OUTPUT_FOLDER, 'task_status.json')
# 创建必要的目录
for folder in [UPLOAD_FOLDER, OUTPUT_FOLDER]:
if not os.path.exists(folder):
os.makedirs(folder)
logger.info(f"创建目录: {folder}")
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['OUTPUT_FOLDER'] = OUTPUT_FOLDER
app.config['MAX_CONTENT_LENGTH'] = 512 * 1024 * 1024 # 增加上传大小限制到512MB
ALLOWED_EXTENSIONS = {'mp4', 'avi', 'mov'}
# 创建线程池,限制并发处理任务数
MAX_WORKERS = 2 # 最多同时处理2个视频
executor = ThreadPoolExecutor(max_workers=MAX_WORKERS)
# 初始化任务状态存储
tasks = {}
if os.path.exists(TASK_STATUS_FILE):
try:
with open(TASK_STATUS_FILE, 'r', encoding='utf-8') as f:
tasks = json.load(f)
except:
logger.error("无法读取任务状态文件,创建新的状态跟踪")
tasks = {}
def save_task_status():
"""保存任务状态到文件"""
try:
with open(TASK_STATUS_FILE, 'w', encoding='utf-8') as f:
json.dump(tasks, f, ensure_ascii=False, indent=2)
except Exception as e:
logger.error(f"保存任务状态失败: {str(e)}")
def get_free_space_gb(dirname):
"""获取指定目录所在磁盘的可用空间GB"""
free_bytes = ctypes.c_ulonglong(0)
ctypes.windll.kernel32.GetDiskFreeSpaceExW(
ctypes.c_wchar_p(dirname), None, None, ctypes.pointer(free_bytes)
)
return free_bytes.value / 1024 / 1024 / 1024 # 转换为GB
def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
def process_video_async(task_id, filepath, filename):
"""异步处理视频的函数"""
try:
# 更新任务状态为处理中
tasks[task_id]['status'] = 'processing'
tasks[task_id]['progress'] = 10
save_task_status()
# 处理视频文件
logger.info(f"[任务 {task_id}] 开始处理视频: {filename}")
# 创建任务专属的输出目录
task_output_dir = os.path.join(app.config['OUTPUT_FOLDER'], task_id)
os.makedirs(task_output_dir, exist_ok=True)
# 设置进度回调
def progress_callback(progress, message=None):
tasks[task_id]['progress'] = progress
if message:
tasks[task_id]['message'] = message
save_task_status()
# 处理视频
start_time = time.time()
result = main_process(filepath, output_dir=task_output_dir, progress_callback=progress_callback)
end_time = time.time()
if result:
# 处理成功
tasks[task_id]['status'] = 'completed'
tasks[task_id]['progress'] = 100
tasks[task_id]['message'] = '处理完成'
# 修改报告路径格式,确保前端能正确访问
# 检查报告文件是否存在
html_path = os.path.join(task_output_dir, "summary.html")
if os.path.exists(html_path):
# 使用专门的查看报告路由
tasks[task_id]['result_path'] = f"/view_report/{task_id}"
logger.info(f"[任务 {task_id}] 报告预览路径: {tasks[task_id]['result_path']}")
else:
# 查找是否有备用报告文件
backup_files = [f for f in os.listdir(task_output_dir) if f.endswith('.html')]
if backup_files:
tasks[task_id]['result_path'] = f"/view_report/{task_id}"
logger.info(f"[任务 {task_id}] 使用备用报告预览路径: {tasks[task_id]['result_path']}")
else:
# 创建一个简单的状态页面
simple_html = os.path.join(task_output_dir, "status.html")
with open(simple_html, "w", encoding="utf-8") as f:
f.write(f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>处理状态</title>
<style>
body {{ font-family: Arial, sans-serif; padding: 20px; max-width: 800px; margin: 0 auto; }}
.status {{ padding: 15px; background-color: #f8f9fa; border-radius: 5px; margin-top: 20px; }}
</style>
</head>
<body>
<h1>视频处理完成</h1>
<div class="status">
<p>视频已成功处理但未找到标准报告文件</p>
<p>请检查服务器日志以获取详细信息</p>
<p>处理时间: {round(end_time - start_time, 2)}</p>
</div>
</body>
</html>
""")
tasks[task_id]['result_path'] = f"/view_report/{task_id}"
logger.info(f"[任务 {task_id}] 创建状态页面: {simple_html}")
tasks[task_id]['process_time'] = round(end_time - start_time, 2)
logger.info(f"[任务 {task_id}] 视频处理成功,耗时: {tasks[task_id]['process_time']}")
else:
# 处理失败
tasks[task_id]['status'] = 'failed'
tasks[task_id]['message'] = '处理失败,请检查日志'
logger.error(f"[任务 {task_id}] 视频处理失败")
save_task_status()
except Exception as e:
# 处理异常
logger.error(f"[任务 {task_id}] 处理过程中发生异常: {str(e)}")
try:
logger.error(traceback.format_exc())
except Exception:
logger.error("无法获取详细错误信息traceback模块不可用")
tasks[task_id]['status'] = 'failed'
tasks[task_id]['message'] = f'错误: {str(e)}'
save_task_status()
@app.route('/')
def index():
return render_template('index.html')
@app.route('/output/<path:filename>')
def serve_output(filename):
"""提供输出文件下载"""
try:
logger.info(f"请求输出文件: {filename}")
# 检查文件名是否包含任务ID路径
if '/' in filename:
task_id = filename.split('/')[0]
file_path = '/'.join(filename.split('/')[1:])
task_output_dir = os.path.join(app.config['OUTPUT_FOLDER'], task_id)
if not os.path.exists(task_output_dir):
logger.error(f"任务输出目录不存在: {task_output_dir}")
return f"任务输出目录不存在: {task_id}", 404
file_full_path = os.path.join(task_output_dir, file_path)
if not os.path.exists(file_full_path):
logger.error(f"请求的文件不存在: {file_full_path}")
return f"请求的文件不存在: {file_path}", 404
logger.info(f"提供文件: {file_full_path}")
return send_from_directory(task_output_dir, file_path)
else:
# 处理不包含任务ID的直接文件
output_dir = app.config['OUTPUT_FOLDER']
file_full_path = os.path.join(output_dir, filename)
if not os.path.exists(file_full_path):
logger.error(f"请求的文件不存在: {file_full_path}")
return f"请求的文件不存在: {filename}", 404
logger.info(f"提供文件: {file_full_path}")
return send_from_directory(output_dir, filename)
except Exception as e:
logger.error(f"提供输出文件时出错: {str(e)}")
return f"服务器错误: {str(e)}", 500
@app.route('/view_report/<task_id>')
def view_report(task_id):
"""提供报告预览页面"""
try:
if task_id not in tasks:
return "任务不存在", 404
task = tasks[task_id]
if task['status'] != 'completed':
return f"任务尚未完成,当前状态: {task['status']}", 400
task_output_dir = os.path.join(app.config['OUTPUT_FOLDER'], task_id)
html_path = os.path.join(task_output_dir, "summary.html")
if os.path.exists(html_path):
logger.info(f"提供报告预览: {html_path}")
with open(html_path, 'r', encoding='utf-8') as f:
html_content = f.read()
return html_content
else:
# 查找是否有备用报告文件
backup_files = [f for f in os.listdir(task_output_dir) if f.endswith('.html')]
if backup_files:
alt_html_path = os.path.join(task_output_dir, backup_files[0])
logger.info(f"提供备用报告预览: {alt_html_path}")
with open(alt_html_path, 'r', encoding='utf-8') as f:
html_content = f.read()
return html_content
else:
return "找不到报告文件", 404
except Exception as e:
logger.error(f"提供报告预览时出错: {str(e)}")
return f"服务器错误: {str(e)}", 500
@app.route('/api/upload', methods=['POST'])
def upload_file():
try:
logger.info("收到上传请求")
# 检查是否有文件被上传
if 'file' not in request.files:
logger.error("没有文件被上传")
return jsonify({'error': '没有文件被上传'}), 400
file = request.files['file']
if file.filename == '':
logger.error("没有选择文件")
return jsonify({'error': '没有选择文件'}), 400
if not allowed_file(file.filename):
logger.error(f"不支持的文件类型: {file.filename}")
return jsonify({'error': '不支持的文件类型请上传MP4、AVI或MOV格式的视频'}), 400
# 生成唯一任务ID
task_id = str(uuid.uuid4())
# 保存文件
original_filename = file.filename
safe_filename = secure_filename(original_filename)
filepath = os.path.join(app.config['UPLOAD_FOLDER'], f"{task_id}_{safe_filename}")
logger.info(f"[任务 {task_id}] 上传文件信息: 文件名={original_filename}, 大小={request.content_length}字节")
logger.info(f"[任务 {task_id}] 正在保存文件到: {filepath}")
# 检查磁盘空间
required_space = request.content_length / (1024 * 1024 * 1024) * 2 # 需要2倍文件大小的空间
free_space = get_free_space_gb(UPLOAD_FOLDER)
logger.info(f"[任务 {task_id}] 磁盘空间检查: 需要 {required_space:.2f}GB可用 {free_space:.2f}GB")
if free_space < required_space:
logger.error(f"[任务 {task_id}] 磁盘空间不足: 需要 {required_space:.2f}GB可用 {free_space:.2f}GB")
return jsonify({'error': '磁盘空间不足,请清理磁盘后重试'}), 400
# 保存任务状态
tasks[task_id] = {
'id': task_id,
'filename': original_filename,
'filepath': filepath,
'uploaded_at': time.strftime('%Y-%m-%d %H:%M:%S'),
'size': request.content_length,
'status': 'uploading',
'progress': 0,
'message': '正在上传文件...'
}
save_task_status()
try:
# 保存文件
file.save(filepath)
logger.info(f"[任务 {task_id}] 文件保存成功: {filepath}")
# 验证文件大小
saved_size = os.path.getsize(filepath)
size_diff = abs(request.content_length - saved_size)
logger.info(f"[任务 {task_id}] 文件大小验证: 上传={request.content_length}字节, 保存={saved_size}字节, 差异={size_diff}字节")
if size_diff > 1024: # 允许1KB的差异
try:
os.remove(filepath)
logger.info(f"[任务 {task_id}] 已删除不完整的文件: {filepath}")
except Exception as e:
logger.error(f"[任务 {task_id}] 删除不完整文件失败: {str(e)}")
tasks[task_id]['status'] = 'failed'
tasks[task_id]['message'] = '文件上传不完整,请重试'
save_task_status()
return jsonify({'error': '文件保存不完整,请重试', 'task_id': task_id}), 500
# 更新任务状态为等待处理
tasks[task_id]['status'] = 'pending'
tasks[task_id]['message'] = '等待处理...'
tasks[task_id]['progress'] = 5
save_task_status()
# 提交异步处理任务
executor.submit(process_video_async, task_id, filepath, original_filename)
# 返回任务ID
return jsonify({
'message': '文件上传成功,已加入处理队列',
'task_id': task_id
}), 200
except Exception as e:
logger.error(f"[任务 {task_id}] 保存或准备处理时出错: {str(e)}")
try:
logger.error(traceback.format_exc())
except Exception:
logger.error("无法获取详细错误信息traceback模块不可用")
tasks[task_id]['status'] = 'failed'
tasks[task_id]['message'] = f'错误: {str(e)}'
save_task_status()
return jsonify({'error': f'上传失败: {str(e)}', 'task_id': task_id}), 500
except Exception as e:
logger.error(f"上传过程中发生错误: {str(e)}")
try:
logger.error(traceback.format_exc())
except Exception:
logger.error("无法获取详细错误信息traceback模块不可用")
return jsonify({'error': f'上传失败: {str(e)}'}), 500
@app.route('/api/tasks', methods=['GET'])
def get_all_tasks():
"""获取所有任务的状态"""
return jsonify({'tasks': list(tasks.values())}), 200
@app.route('/api/tasks/<task_id>', methods=['GET'])
def get_task_status(task_id):
"""获取特定任务的状态"""
if task_id in tasks:
return jsonify(tasks[task_id]), 200
else:
return jsonify({'error': '任务不存在'}), 404
@app.route('/api/tasks/<task_id>/cancel', methods=['POST'])
def cancel_task(task_id):
"""取消任务"""
if task_id not in tasks:
return jsonify({'error': '任务不存在'}), 404
if tasks[task_id]['status'] in ['completed', 'failed']:
return jsonify({'error': '无法取消已完成或失败的任务'}), 400
# 更新任务状态
tasks[task_id]['status'] = 'cancelled'
tasks[task_id]['message'] = '任务已取消'
save_task_status()
# 清理文件
try:
if os.path.exists(tasks[task_id]['filepath']):
os.remove(tasks[task_id]['filepath'])
except Exception as e:
logger.error(f"删除已取消任务的文件失败: {str(e)}")
return jsonify({'message': '任务已取消'}), 200
@app.route('/api/tasks/<task_id>', methods=['DELETE'])
def delete_task(task_id):
"""删除任务"""
if task_id not in tasks:
return jsonify({'error': '任务不存在'}), 404
# 如果任务正在处理中,不允许删除
if tasks[task_id]['status'] in ['processing', 'uploading']:
return jsonify({'error': '无法删除正在处理中的任务,请先取消'}), 400
# 清理文件
try:
# 删除上传的文件
if os.path.exists(tasks[task_id]['filepath']):
os.remove(tasks[task_id]['filepath'])
logger.info(f"[任务 {task_id}] 已删除上传文件: {tasks[task_id]['filepath']}")
# 删除输出目录
task_output_dir = os.path.join(app.config['OUTPUT_FOLDER'], task_id)
if os.path.exists(task_output_dir):
shutil.rmtree(task_output_dir)
logger.info(f"[任务 {task_id}] 已删除输出目录: {task_output_dir}")
except Exception as e:
logger.error(f"[任务 {task_id}] 删除文件失败: {str(e)}")
# 从任务列表中删除
del tasks[task_id]
save_task_status()
return jsonify({'message': '任务已删除'}), 200
@app.route('/api/tasks/<task_id>/retry', methods=['POST'])
def retry_task(task_id):
"""重试失败的任务"""
if task_id not in tasks:
return jsonify({'error': '任务不存在'}), 404
# 只允许重试失败或已取消的任务
if tasks[task_id]['status'] not in ['failed', 'cancelled']:
return jsonify({'error': f"无法重试处于 {tasks[task_id]['status']} 状态的任务"}), 400
# 检查文件是否还存在
if not os.path.exists(tasks[task_id]['filepath']):
return jsonify({'error': '原始视频文件已不存在,无法重试'}), 400
# 更新任务状态
original_filepath = tasks[task_id]['filepath']
original_filename = tasks[task_id]['filename']
# 更新任务状态
tasks[task_id]['status'] = 'pending'
tasks[task_id]['progress'] = 5
tasks[task_id]['message'] = '已重新加入处理队列...'
tasks[task_id]['retried_at'] = time.strftime('%Y-%m-%d %H:%M:%S')
# 清理旧的输出目录
task_output_dir = os.path.join(app.config['OUTPUT_FOLDER'], task_id)
if os.path.exists(task_output_dir):
try:
shutil.rmtree(task_output_dir)
logger.info(f"[任务 {task_id}] 已清理旧的输出目录")
except Exception as e:
logger.error(f"[任务 {task_id}] 清理旧输出目录失败: {str(e)}")
# 保存任务状态
save_task_status()
# 提交异步处理任务
executor.submit(process_video_async, task_id, original_filepath, original_filename)
return jsonify({
'message': '任务已重新提交处理',
'task_id': task_id
}), 200
if __name__ == '__main__':
logger.info("启动服务器...")
app.run(debug=True, port=5000, threaded=True)