diff --git a/converter/__init__.py b/converter/__init__.py index 78bee2b..ccdb2f9 100644 --- a/converter/__init__.py +++ b/converter/__init__.py @@ -41,6 +41,12 @@ (255, 128, 0), # 橙 ] +# 验证调色板完整性 +assert len(COLOR_PALETTE_16) == 16, "Color palette must have exactly 16 colors" +for i, color in enumerate(COLOR_PALETTE_16): + assert len(color) == 3, f"Color {i} must be RGB tuple" + assert all(0 <= c <= 255 for c in color), f"Color {i} values must be in range 0-255" + # 视频参数预设 VIDEO_PRESETS = { "4K": {"width": 3840, "height": 2160}, diff --git a/requirements.txt b/requirements.txt index a589af3..f298a52 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,26 +1,23 @@ # Core dependencies -numpy>=1.21.0 -numba>=0.55.1 -opencv-python>=4.5.5.64 -flask>=2.0.1 -flask-socketio>=5.1.1 +numpy>=1.21.0,<2.0.0 +numba==0.59.1 +opencv-python>=4.9.0.0 +flask>=2.0.1,<3.0.0 +flask-socketio>=5.1.1,<6.0.0 Pillow>=8.3.1 psutil>=5.9.0 watchdog>=2.1.6 tqdm>=4.62.3 python-magic>=0.4.24 -# GPU acceleration (optional but recommended) -# Install appropriate version based on CUDA version: +# GPU acceleration - Choose ONE based on CUDA version # For CUDA 12.x: -cupy-cuda12x>=12.0.0 -# For CUDA 11.x (comment above and uncomment below): -# cupy-cuda11x>=11.0.0 - cupy-cuda12x==13.2.0 -numba==0.59.1 +# For CUDA 11.x (comment above, uncomment below): +# cupy-cuda11x>=11.0.0,<12.0.0 + +# Raptor codes implementation pyldpc==0.5.2 -opencv-python>=4.9.0.0 -# Error correction (removed reedsolo, using GPU Raptor codes instead) -# reedsolo>=1.5.4 # Replaced by GPU implementation +# Remove old error correction dependency +# reedsolo>=1.5.4 # REMOVED - replaced by GPU implementation diff --git a/web_ui/server.py b/web_ui/server.py index 46a29ad..1aa0052 100644 --- a/web_ui/server.py +++ b/web_ui/server.py @@ -22,9 +22,51 @@ import hashlib import numpy as np -from converter.gpu_error_correction import get_optimal_error_corrector -from converter.gpu_frame_generator import GPUFrameGenerator -from converter.video_raptor_encoder import VideoRaptorEncoder +# GPU模块条件导入 - 完整fallback机制 +try: + from converter.gpu_error_correction import get_optimal_error_corrector + GPU_ERROR_CORRECTION_AVAILABLE = True + logger.info("GPU error correction available") +except ImportError as e: + logger.warning(f"GPU error correction not available: {e}") + GPU_ERROR_CORRECTION_AVAILABLE = False + + def get_optimal_error_corrector(redundancy_ratio): + from converter.error_correction import ReedSolomonEncoder + redundancy_bytes = max(1, int(255 * redundancy_ratio)) + return ReedSolomonEncoder(redundancy_bytes=redundancy_bytes) + +try: + from converter.gpu_frame_generator import GPUFrameGenerator + GPU_FRAME_GENERATION_AVAILABLE = True + logger.info("GPU frame generation available") +except ImportError as e: + logger.warning(f"GPU frame generation not available: {e}") + GPU_FRAME_GENERATION_AVAILABLE = False + +try: + from converter.video_raptor_encoder import VideoRaptorEncoder + RAPTOR_ENCODER_AVAILABLE = True + logger.info("Raptor encoder available") +except ImportError as e: + logger.warning(f"Raptor encoder not available: {e}") + RAPTOR_ENCODER_AVAILABLE = False + + class VideoRaptorEncoder: + def __init__(self, width, height, fps): + self.width = width + self.height = height + self.fps = fps + + def create_metadata_frame(self, file_info): + return np.zeros((self.height, self.width, 3), dtype=np.uint8) + + def create_calibration_frame(self): + return np.zeros((self.height, self.width, 3), dtype=np.uint8) + + def _add_sync_pattern(self, frame): + frame[::20, :] = 255 + frame[:, ::20] = 255 # 添加项目根目录到路径 # Add project root to path @@ -314,154 +356,75 @@ def _frame_generated_callback(self, frame_idx, total_frames, frame): except Exception as e: logger.error(f"进度更新错误 [{self.task_id}]: {e}", exc_info=True) def _verify_output_video(self): - """ - 验证输出视频文件是否有效 - - Returns: - tuple: (是否有效, 错误消息) - """ + """验证输出AVI文件 - 针对未压缩AVI优化""" if not self.output_path.exists(): return False, "输出文件不存在" - + if self.output_path.stat().st_size == 0: return False, "输出文件大小为0" - + try: - # 首先尝试修复AVI文件 - logger.info(f"尝试修复AVI文件: {self.output_path}") - - # 创建临时文件路径 - temp_output = Path(str(self.output_path) + ".fixed.avi") - - # 首先尝试将文件复制到新位置以修复可能的问题 - fix_cmd = [ - "ffmpeg", - "-v", "warning", - "-i", str(self.output_path), - "-c", "copy", - "-movflags", "faststart", # 这个选项会将元数据移到文件开头,解决moov atom问题 - str(temp_output) - ] - - logger.info(f"执行修复命令: {' '.join(fix_cmd)}") - fix_result = subprocess.run( - fix_cmd, - stderr=subprocess.PIPE, - stdout=subprocess.PIPE, - timeout=60 - ) - - if fix_result.returncode == 0 and temp_output.exists() and temp_output.stat().st_size > 0: - # 修复成功,替换原文件 - logger.info(f"AVI文件修复成功: {self.output_path}") - shutil.copy2(temp_output, self.output_path) - temp_output.unlink() - else: - # 修复失败,记录错误 - error_msg = fix_result.stderr.decode('utf-8', errors='ignore') - logger.warning(f"AVI文件修复失败: {error_msg}") - if temp_output.exists(): - temp_output.unlink() - - # 使用ffprobe验证视频 - info_cmd = [ - "ffprobe", - "-v", "error", - "-select_streams", "v:0", - "-show_entries", "stream=width,height,codec_name,duration,nb_frames", - "-of", "json", - str(self.output_path) - ] - - logger.info(f"执行验证命令: {' '.join(info_cmd)}") - info_result = subprocess.run( - info_cmd, - stderr=subprocess.PIPE, - stdout=subprocess.PIPE, - timeout=30 + from converter.avi_validator import validate_avi_file + + validation_results = validate_avi_file( + str(self.output_path), + callback=None ) - - if info_result.returncode == 0: - # 尝试解析视频信息 - try: - video_info = json.loads(info_result.stdout) - logger.info(f"视频信息获取成功: {json.dumps(video_info, indent=2)}") - - if "streams" in video_info and len(video_info["streams"]) > 0: - stream_info = video_info["streams"][0] - width = stream_info.get("width") - height = stream_info.get("height") - codec = stream_info.get("codec_name") - - # 验证基本参数是否合理 - if width and height and codec: - logger.info(f"视频有效: {width}x{height}, 编码: {codec}") - return True, "视频文件有效" - else: - logger.warning(f"视频缺少关键信息: {stream_info}") - return False, "视频信息不完整" - else: - logger.warning("无视频流信息") - return False, "未找到视频流" - except json.JSONDecodeError as e: - logger.error(f"JSON解析错误: {e}") - return False, f"解析视频信息失败: {e}" - else: - error_msg = info_result.stderr.decode('utf-8', errors='ignore') - logger.error(f"视频文件验证失败: {error_msg}") - - # 尝试更简单的验证方法 - 仅检查文件是否可以打开 - try_cmd = [ - "ffmpeg", - "-v", "error", - "-i", str(self.output_path), - "-t", "0.1", # 只读取前0.1秒 - "-f", "null", - "-" - ] - - logger.info(f"尝试简单验证: {' '.join(try_cmd)}") - try_result = subprocess.run( - try_cmd, - stderr=subprocess.PIPE, - stdout=subprocess.PIPE, - timeout=30 + + if validation_results['overall_success']: + structure = validation_results['structure_validation'] + logger.info( + f"AVI验证成功: {structure['resolution']}, " + f"{structure['frame_count']} frames, {structure['fps']} fps" ) - - if try_result.returncode == 0: - logger.info("简单验证通过,视频可以读取") - return True, "简单验证通过,视频可以读取" - else: - try_error = try_result.stderr.decode('utf-8', errors='ignore') - logger.error(f"简单验证失败: {try_error}") - return False, f"视频验证失败: {error_msg}\n简单验证失败: {try_error}" - - except subprocess.TimeoutExpired: - return False, "视频验证超时" + return True, "AVI文件验证通过" + else: + error_details = [] + if 'structure_validation' in validation_results: + error_details.extend(validation_results['structure_validation'].get('error_details', [])) + if 'integrity_validation' in validation_results: + error_details.extend(validation_results['integrity_validation'].get('error_details', [])) + + error_msg = "; ".join(error_details) if error_details else "未知验证错误" + logger.error(f"AVI验证失败: {error_msg}") + return False, f"AVI验证失败: {error_msg}" + + except ImportError: + logger.warning("AVI validator not available, using basic validation") + return True, "基础验证通过(验证器不可用)" except Exception as e: - logger.error(f"视频验证出错: {e}", exc_info=True) - return False, f"视频验证出错: {str(e)}" + logger.error(f"AVI验证过程出错: {e}", exc_info=True) + return False, f"验证过程出错: {str(e)}" def _conversion_worker(self): """GPU-aware conversion worker thread""" try: self._update_task_status("initializing") - # ---------- 1. GPU error-correction ---------- - if self.error_correction_enabled: - logger.info(f"Initializing Raptor error correction [{self.task_id}]") + from converter.encoder import StreamingDirectAVIEncoder # lazy import + + # ---------- 1. GPU error-correction ---------- + if self.error_correction_enabled and GPU_ERROR_CORRECTION_AVAILABLE: + logger.info(f"Initializing GPU error correction [{self.task_id}]") self.error_correction = get_optimal_error_corrector(self.error_correction_ratio) + elif self.error_correction_enabled: + logger.info(f"Using CPU error correction [{self.task_id}]") + self.error_correction = ReedSolomonEncoder(redundancy_bytes=int(255 * self.error_correction_ratio)) - # ---------- 2. Frame generator ---------- + # ---------- 2. Frame generator ---------- try: - self.frame_generator = GPUFrameGenerator( - resolution=self.resolution, - fps=self.fps, - color_count=self.color_count, - nine_to_one=self.nine_to_one - ) - logger.info(f"Using GPU-accelerated frame generator [{self.task_id}]") - except RuntimeError: + if GPU_FRAME_GENERATION_AVAILABLE: + self.frame_generator = GPUFrameGenerator( + resolution=self.resolution, + fps=self.fps, + color_count=self.color_count, + nine_to_one=self.nine_to_one + ) + logger.info(f"Using GPU-accelerated frame generator [{self.task_id}]") + else: + raise RuntimeError("GPU not available") + except (RuntimeError, Exception) as e: + logger.info(f"GPU frame generation failed, using CPU: {e}") generator_class = OptimizedFrameGenerator if self.use_optimized_generator else FrameGenerator self.frame_generator = generator_class( resolution=self.resolution, @@ -488,7 +451,7 @@ def _conversion_worker(self): self.video_encoder.start() # ---------- 4. Metadata / calibration frames ---------- - if self.params.get("metadata_frames", True): + if self.params.get("metadata_frames", True) and RAPTOR_ENCODER_AVAILABLE: logger.info(f"Adding metadata frames [{self.task_id}]") file_info = { @@ -907,6 +870,57 @@ def download_file_by_name(filename): return jsonify({"error": str(e)}), 500 +@app.route('/api/verify-video/', methods=['POST']) +def verify_video(task_id): + """验证生成的AVI视频文件""" + try: + with task_lock: + if task_id not in task_registry: + return jsonify({"error": "找不到指定的任务"}), 404 + + task_info = task_registry[task_id] + + if task_info["status"] != "completed": + return jsonify({"error": "任务尚未完成"}), 400 + + if not task_info.get("output_path"): + return jsonify({"error": "没有可用的输出文件"}), 404 + + output_path = Path(task_info["output_path"]) + + if not output_path.exists(): + return jsonify({"error": "输出文件不存在"}), 404 + + from converter.avi_validator import validate_avi_file + + def progress_callback(percentage, frame_num, bytes_processed): + socketio.emit('verification_progress', { + 'task_id': task_id, + 'percentage': percentage, + 'frame': frame_num, + 'bytes': bytes_processed + }) + + validation_results = validate_avi_file( + str(output_path), + callback=progress_callback + ) + + socketio.emit('verification_complete', { + 'task_id': task_id, + 'results': validation_results + }) + + return jsonify({ + "success": True, + "validation_results": validation_results + }) + + except Exception as e: + logger.error(f"视频验证错误: {e}", exc_info=True) + return jsonify({"error": str(e)}), 500 + + # 添加缺失的清理缓存API @app.route('/api/clear-cache', methods=['POST']) def clear_cache():