diff --git a/8.0/README.md b/8.0/README.md new file mode 100644 index 0000000..f5ec85d --- /dev/null +++ b/8.0/README.md @@ -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等常见格式 diff --git a/8.0/process.log b/8.0/process.log new file mode 100644 index 0000000..06d7405 Binary files /dev/null and b/8.0/process.log differ diff --git a/8.0/requirements.txt b/8.0/requirements.txt new file mode 100644 index 0000000..12e5b39 --- /dev/null +++ b/8.0/requirements.txt @@ -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 diff --git a/8.0/server.log b/8.0/server.log new file mode 100644 index 0000000..c104e64 --- /dev/null +++ b/8.0/server.log @@ -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 diff --git a/8.0/server.py b/8.0/server.py new file mode 100644 index 0000000..844c2ed --- /dev/null +++ b/8.0/server.py @@ -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""" + + + + + 处理状态 + + + +

视频处理完成

+
+

视频已成功处理,但未找到标准报告文件。

+

请检查服务器日志以获取详细信息。

+

处理时间: {round(end_time - start_time, 2)}秒

+
+ + + """) + 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/') +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/') +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/', 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//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/', 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//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) \ No newline at end of file