فهرست منبع

对代码进行结构整理

zhch158_admin 1 ماه پیش
والد
کامیت
2aa94dd62e
6فایلهای تغییر یافته به همراه1386 افزوده شده و 1304 حذف شده
  1. 14 1304
      streamlit_ocr_validator.py
  2. 137 0
      streamlit_validator_core.py
  3. 453 0
      streamlit_validator_cross.py
  4. 387 0
      streamlit_validator_result.py
  5. 266 0
      streamlit_validator_table.py
  6. 129 0
      streamlit_validator_ui.py

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 14 - 1304
streamlit_ocr_validator.py


+ 137 - 0
streamlit_validator_core.py

@@ -0,0 +1,137 @@
+"""
+核心验证器类
+"""
+import streamlit as st
+from pathlib import Path
+from typing import Dict, List, Optional
+import json
+
+from ocr_validator_utils import (
+    load_config, load_ocr_data_file, process_ocr_data,
+    get_ocr_statistics, find_available_ocr_files_multi_source, 
+    get_data_source_display_name
+)
+from ocr_validator_layout import OCRLayoutManager
+
+
+class StreamlitOCRValidator:
+    """核心验证器类"""
+    
+    def __init__(self):
+        self.config = load_config()
+        self.ocr_data = []
+        self.md_content = ""
+        self.image_path = ""
+        self.text_bbox_mapping = {}
+        self.selected_text = None
+        self.marked_errors = set()
+        
+        # 多数据源相关
+        self.all_sources = {}
+        self.current_source_key = None
+        self.current_source_config = None
+        self.file_info = []
+        self.selected_file_index = -1
+        self.display_options = []
+        self.file_paths = []
+        
+        # 交叉验证数据源
+        self.verify_source_key = None
+        self.verify_source_config = None
+        self.verify_file_info = []
+        self.verify_display_options = []
+        self.verify_file_paths = []
+
+        # 初始化布局管理器
+        self.layout_manager = OCRLayoutManager(self)
+
+        # 加载多数据源文件信息
+        self.load_multi_source_info()
+        
+    def load_multi_source_info(self):
+        """加载多数据源文件信息"""
+        self.all_sources = find_available_ocr_files_multi_source(self.config)
+        
+        if self.all_sources:
+            source_keys = list(self.all_sources.keys())
+            first_source_key = source_keys[0]
+            self.switch_to_source(first_source_key)
+            
+            if len(source_keys) > 1:
+                self.switch_to_verify_source(source_keys[1])
+    
+    def switch_to_source(self, source_key: str):
+        """切换到指定OCR数据源"""
+        if source_key in self.all_sources:
+            self.current_source_key = source_key
+            source_data = self.all_sources[source_key]
+            self.current_source_config = source_data['config']
+            self.file_info = source_data['files']
+            
+            if self.file_info:
+                self.display_options = [f"{info['display_name']}" for info in self.file_info]
+                self.file_paths = [info['path'] for info in self.file_info]
+                self.selected_file_index = -1
+                print(f"✅ 切换到OCR数据源: {source_key}")
+            else:
+                print(f"⚠️ 数据源 {source_key} 没有可用文件")
+    
+    def switch_to_verify_source(self, source_key: str):
+        """切换到指定验证数据源"""
+        if source_key in self.all_sources:
+            self.verify_source_key = source_key
+            source_data = self.all_sources[source_key]
+            self.verify_source_config = source_data['config']
+            self.verify_file_info = source_data['files']
+            
+            if self.verify_file_info:
+                self.verify_display_options = [f"{info['display_name']}" for info in self.verify_file_info]
+                self.verify_file_paths = [info['path'] for info in self.verify_file_info]
+                print(f"✅ 切换到验证数据源: {source_key}")
+            else:
+                print(f"⚠️ 验证数据源 {source_key} 没有可用文件")
+
+    def load_ocr_data(self, json_path: str, md_path: Optional[str] = None, image_path: Optional[str] = None):
+        """加载OCR相关数据"""
+        try:
+            if self.current_source_config:
+                temp_config = self.config.copy()
+                temp_config['paths'] = {
+                    'ocr_out_dir': self.current_source_config['ocr_out_dir'],
+                    'src_img_dir': self.current_source_config.get('src_img_dir', ''),
+                    'pre_validation_dir': self.config['pre_validation']['out_dir']
+                }
+                temp_config['current_ocr_tool'] = self.current_source_config['ocr_tool']
+                
+                self.ocr_data, self.md_content, self.image_path = load_ocr_data_file(json_path, temp_config)
+            else:
+                self.ocr_data, self.md_content, self.image_path = load_ocr_data_file(json_path, self.config)
+                
+            self.process_data()
+        except Exception as e:
+            st.error(f"❌ 加载失败: {e}")
+            st.exception(e)
+    
+    def process_data(self):
+        """处理OCR数据"""
+        self.text_bbox_mapping = process_ocr_data(self.ocr_data, self.config)
+    
+    def get_statistics(self) -> Dict:
+        """获取统计信息"""
+        return get_ocr_statistics(self.ocr_data, self.text_bbox_mapping, self.marked_errors)
+    
+    def find_verify_md_path(self, selected_file_index: int) -> Optional[Path]:
+        """查找当前OCR文件对应的验证文件路径"""
+        current_page = self.file_info[selected_file_index]['page']
+        verify_md_path = None
+
+        for i, info in enumerate(self.verify_file_info):
+            if info['page'] == current_page:
+                verify_md_path = Path(self.verify_file_paths[i]).with_suffix('.md')
+                break
+
+        return verify_md_path
+
+    def create_compact_layout(self, config):
+        """创建紧凑布局"""
+        return self.layout_manager.create_compact_layout(config)

+ 453 - 0
streamlit_validator_cross.py

@@ -0,0 +1,453 @@
+"""
+交叉验证功能模块
+"""
+import streamlit as st
+import pandas as pd
+import json
+from pathlib import Path
+from io import BytesIO
+import plotly.express as px
+
+from compare_ocr_results import compare_ocr_results
+from ocr_validator_utils import get_data_source_display_name
+
+
+@st.dialog("交叉验证", width="large", dismissible=True, on_dismiss="rerun")
+def cross_validation_dialog(validator):
+    """交叉验证对话框"""
+    if validator.current_source_key == validator.verify_source_key:
+        st.error("❌ OCR数据源和验证数据源不能相同")
+        return
+    
+    if 'cross_validation_batch_result' not in st.session_state:
+        st.session_state.cross_validation_batch_result = None
+    
+    st.header("🔄 批量交叉验证")
+    
+    col1, col2 = st.columns(2)
+    with col1:
+        st.info(f"**OCR数据源:** {get_data_source_display_name(validator.current_source_config)}")
+        st.write(f"📁 文件数量: {len(validator.file_info)}")
+    with col2:
+        st.info(f"**验证数据源:** {get_data_source_display_name(validator.verify_source_config)}")
+        st.write(f"📁 文件数量: {len(validator.verify_file_info)}")
+    
+    with st.expander("⚙️ 验证选项", expanded=True):
+        col1, col2 = st.columns(2)
+        with col1:
+            table_mode = st.selectbox(
+                "表格比对模式",
+                options=['standard', 'flow_list'],
+                index=1,
+                format_func=lambda x: '流水表格模式' if x == 'flow_list' else '标准模式',
+                help="选择表格比对算法"
+            )
+        with col2:
+            similarity_algorithm = st.selectbox(
+                "相似度算法",
+                options=['ratio', 'partial_ratio', 'token_sort_ratio', 'token_set_ratio'],
+                index=0,
+                help="选择文本相似度计算算法"
+            )
+    
+    if st.button("🚀 开始批量验证", type="primary", use_container_width=True):
+        run_batch_cross_validation(validator, table_mode, similarity_algorithm)
+    
+    if 'cross_validation_batch_result' in st.session_state and st.session_state.cross_validation_batch_result:
+        st.markdown("---")
+        display_batch_validation_results(st.session_state.cross_validation_batch_result)
+
+
+def run_batch_cross_validation(validator, table_mode: str, similarity_algorithm: str):
+    """执行批量交叉验证"""
+    pre_validation_dir = Path(validator.config['pre_validation'].get('out_dir', './output/pre_validation/')).resolve()
+    pre_validation_dir.mkdir(parents=True, exist_ok=True)
+    
+    batch_results = _initialize_batch_results(validator, table_mode, similarity_algorithm)
+    
+    progress_bar = st.progress(0)
+    status_text = st.empty()
+    
+    ocr_page_map = {info['page']: i for i, info in enumerate(validator.file_info)}
+    verify_page_map = {info['page']: i for i, info in enumerate(validator.verify_file_info)}
+    
+    common_pages = sorted(set(ocr_page_map.keys()) & set(verify_page_map.keys()))
+    
+    if not common_pages:
+        st.error("❌ 两个数据源没有共同的页码,无法进行对比")
+        return
+    
+    batch_results['summary']['total_pages'] = len(common_pages)
+    
+    with st.expander("📋 详细对比日志", expanded=True):
+        log_container = st.container()
+    
+    for idx, page_num in enumerate(common_pages):
+        try:
+            progress = (idx + 1) / len(common_pages)
+            progress_bar.progress(progress)
+            status_text.text(f"正在对比第 {page_num} 页... ({idx + 1}/{len(common_pages)})")
+            
+            ocr_file_index = ocr_page_map[page_num]
+            verify_file_index = verify_page_map[page_num]
+            
+            ocr_md_path = Path(validator.file_paths[ocr_file_index]).with_suffix('.md')
+            verify_md_path = Path(validator.verify_file_paths[verify_file_index]).with_suffix('.md')
+            
+            if not ocr_md_path.exists() or not verify_md_path.exists():
+                with log_container:
+                    st.warning(f"⚠️ 第 {page_num} 页:文件不存在,跳过")
+                batch_results['summary']['failed_pages'] += 1
+                continue
+            
+            comparison_result_path = pre_validation_dir / f"{ocr_md_path.stem}_cross_validation"
+            
+            import io
+            import contextlib
+            
+            output_buffer = io.StringIO()
+            
+            with contextlib.redirect_stdout(output_buffer):
+                comparison_result = compare_ocr_results(
+                    file1_path=str(ocr_md_path),
+                    file2_path=str(verify_md_path),
+                    output_file=str(comparison_result_path),
+                    output_format='both',
+                    ignore_images=True,
+                    table_mode=table_mode,
+                    similarity_algorithm=similarity_algorithm
+                )
+            
+            _process_comparison_result(batch_results, comparison_result, page_num, 
+                                      ocr_md_path, verify_md_path, comparison_result_path)
+            
+            with log_container:
+                if comparison_result['statistics']['total_differences'] == 0:
+                    st.success(f"✅ 第 {page_num} 页:完全匹配")
+                else:
+                    st.warning(f"⚠️ 第 {page_num} 页:发现 {comparison_result['statistics']['total_differences']} 个差异")
+            
+        except Exception as e:
+            with log_container:
+                st.error(f"❌ 第 {page_num} 页:对比失败 - {str(e)}")
+            
+            batch_results['pages'].append({
+                'page_num': page_num,
+                'status': 'failed',
+                'error': str(e)
+            })
+            batch_results['summary']['failed_pages'] += 1
+    
+    _save_batch_results(validator, batch_results, pre_validation_dir)
+    
+    progress_bar.progress(1.0)
+    status_text.text("✅ 批量验证完成!")
+    
+    st.success(f"🎉 批量验证完成!成功: {batch_results['summary']['successful_pages']}, 失败: {batch_results['summary']['failed_pages']}")
+
+
+def _initialize_batch_results(validator, table_mode: str, similarity_algorithm: str) -> dict:
+    """初始化批量结果存储"""
+    return {
+        'ocr_source': get_data_source_display_name(validator.current_source_config),
+        'verify_source': get_data_source_display_name(validator.verify_source_config),
+        'table_mode': table_mode,
+        'similarity_algorithm': similarity_algorithm,
+        'timestamp': pd.Timestamp.now().strftime('%Y-%m-%d %H:%M:%S'),
+        'pages': [],
+        'summary': {
+            'total_pages': 0,
+            'successful_pages': 0,
+            'failed_pages': 0,
+            'total_differences': 0,
+            'total_table_differences': 0,
+            'total_amount_differences': 0,
+            'total_datetime_differences': 0,
+            'total_text_differences': 0,
+            'total_paragraph_differences': 0,
+            'total_table_pre_header': 0,
+            'total_table_header_position': 0,
+            'total_table_header_critical': 0,
+            'total_table_row_missing': 0,
+            'total_high_severity': 0,
+            'total_medium_severity': 0,
+            'total_low_severity': 0
+        }
+    }
+
+
+def _process_comparison_result(batch_results: dict, comparison_result: dict, page_num: int,
+                               ocr_md_path: Path, verify_md_path: Path, comparison_result_path: Path):
+    """处理对比结果"""
+    stats = comparison_result['statistics']
+    
+    page_result = {
+        'page_num': page_num,
+        'ocr_file': str(ocr_md_path.name),
+        'verify_file': str(verify_md_path.name),
+        'total_differences': stats['total_differences'],
+        'table_differences': stats['table_differences'],
+        'amount_differences': stats.get('amount_differences', 0),
+        'datetime_differences': stats.get('datetime_differences', 0),
+        'text_differences': stats.get('text_differences', 0),
+        'paragraph_differences': stats['paragraph_differences'],
+        'table_pre_header': stats.get('table_pre_header', 0),
+        'table_header_position': stats.get('table_header_position', 0),
+        'table_header_critical': stats.get('table_header_critical', 0),
+        'table_row_missing': stats.get('table_row_missing', 0),
+        'high_severity': stats.get('high_severity', 0),
+        'medium_severity': stats.get('medium_severity', 0),
+        'low_severity': stats.get('low_severity', 0),
+        'status': 'success',
+        'comparison_json': f"{comparison_result_path}.json",
+        'comparison_md': f"{comparison_result_path}.md"
+    }
+    
+    batch_results['pages'].append(page_result)
+    batch_results['summary']['successful_pages'] += 1
+    
+    # 更新汇总统计
+    for key in stats:
+        total_key = f'total_{key}'
+        if total_key in batch_results['summary']:
+            batch_results['summary'][total_key] += stats.get(key, 0)
+
+
+def _save_batch_results(validator, batch_results: dict, pre_validation_dir: Path):
+    """保存批量结果"""
+    batch_result_path = pre_validation_dir / f"{validator.current_source_config['name']}_{validator.current_source_config['ocr_tool']}_vs_{validator.verify_source_config['ocr_tool']}_batch_cross_validation"
+    
+    with open(f"{batch_result_path}.json", "w", encoding="utf-8") as f:
+        json.dump(batch_results, f, ensure_ascii=False, indent=2)
+    
+    generate_batch_validation_markdown(batch_results, f"{batch_result_path}.md")
+    
+    st.session_state.cross_validation_batch_result = batch_results
+
+
+def generate_batch_validation_markdown(batch_results: dict, output_path: str):
+    """生成批量验证的Markdown报告"""
+    with open(output_path, "w", encoding="utf-8") as f:
+        f.write("# 批量交叉验证报告\n\n")
+        
+        # 基本信息
+        f.write("## 📋 基本信息\n\n")
+        f.write(f"- **OCR数据源:** {batch_results['ocr_source']}\n")
+        f.write(f"- **验证数据源:** {batch_results['verify_source']}\n")
+        f.write(f"- **表格模式:** {batch_results['table_mode']}\n")
+        f.write(f"- **相似度算法:** {batch_results['similarity_algorithm']}\n")
+        f.write(f"- **验证时间:** {batch_results['timestamp']}\n\n")
+        
+        # 汇总统计
+        summary = batch_results['summary']
+        f.write("## 📊 汇总统计\n\n")
+        f.write(f"- **总页数:** {summary['total_pages']}\n")
+        f.write(f"- **成功页数:** {summary['successful_pages']}\n")
+        f.write(f"- **失败页数:** {summary['failed_pages']}\n")
+        f.write(f"- **总差异数:** {summary['total_differences']}\n")
+        f.write(f"- **表格差异:** {summary['total_table_differences']}\n")
+        f.write(f"  - 金额差异: {summary.get('total_amount_differences', 0)}\n")
+        f.write(f"  - 日期差异: {summary.get('total_datetime_differences', 0)}\n")
+        f.write(f"  - 文本差异: {summary.get('total_text_differences', 0)}\n")
+        f.write(f"  - 表头前差异: {summary.get('total_table_pre_header', 0)}\n")
+        f.write(f"  - 表头位置差异: {summary.get('total_table_header_position', 0)}\n")
+        f.write(f"  - 表头严重错误: {summary.get('total_table_header_critical', 0)}\n")
+        f.write(f"  - 行缺失: {summary.get('total_table_row_missing', 0)}\n")
+        f.write(f"- **段落差异:** {summary['total_paragraph_differences']}\n")
+        f.write(f"- **严重程度统计:**\n")
+        f.write(f"  - 高严重度: {summary.get('total_high_severity', 0)}\n")
+        f.write(f"  - 中严重度: {summary.get('total_medium_severity', 0)}\n")
+        f.write(f"  - 低严重度: {summary.get('total_low_severity', 0)}\n\n")
+        
+        # 详细结果表格
+        f.write("## 📄 各页差异统计\n\n")
+        f.write("| 页码 | 状态 | 总差异 | 表格差异 | 金额 | 日期 | 文本 | 段落 | 表头前 | 表头位置 | 表头错误 | 行缺失 | 高 | 中 | 低 |\n")
+        f.write("|------|------|--------|----------|------|------|------|------|--------|----------|----------|--------|----|----|----|\n")
+        
+        for page in batch_results['pages']:
+            if page['status'] == 'success':
+                status_icon = "✅" if page['total_differences'] == 0 else "⚠️"
+                f.write(f"| {page['page_num']} | {status_icon} | ")
+                f.write(f"{page['total_differences']} | ")
+                f.write(f"{page['table_differences']} | ")
+                f.write(f"{page.get('amount_differences', 0)} | ")
+                f.write(f"{page.get('datetime_differences', 0)} | ")
+                f.write(f"{page.get('text_differences', 0)} | ")
+                f.write(f"{page['paragraph_differences']} | ")
+                f.write(f"{page.get('table_pre_header', 0)} | ")
+                f.write(f"{page.get('table_header_position', 0)} | ")
+                f.write(f"{page.get('table_header_critical', 0)} | ")
+                f.write(f"{page.get('table_row_missing', 0)} | ")
+                f.write(f"{page.get('high_severity', 0)} | ")
+                f.write(f"{page.get('medium_severity', 0)} | ")
+                f.write(f"{page.get('low_severity', 0)} |\n")
+            else:
+                f.write(f"| {page['page_num']} | ❌ | - | - | - | - | - | - | - | - | - | - | - | - | - |\n")
+        
+        f.write("\n")
+        
+        # 问题汇总
+        f.write("## 🔍 问题汇总\n\n")
+        
+        high_diff_pages = [p for p in batch_results['pages'] 
+                            if p['status'] == 'success' and p['total_differences'] > 10]
+        if high_diff_pages:
+            f.write("### ⚠️ 高差异页面(差异>10)\n\n")
+            for page in high_diff_pages:
+                f.write(f"- 第 {page['page_num']} 页:{page['total_differences']} 个差异\n")
+            f.write("\n")
+        
+        amount_error_pages = [p for p in batch_results['pages'] 
+                            if p['status'] == 'success' and p.get('amount_differences', 0) > 0]
+        if amount_error_pages:
+            f.write("### 💰 金额差异页面\n\n")
+            for page in amount_error_pages:
+                f.write(f"- 第 {page['page_num']} 页:{page.get('amount_differences', 0)} 个金额差异\n")
+            f.write("\n")
+        
+        header_error_pages = [p for p in batch_results['pages'] 
+                            if p['status'] == 'success' and p.get('table_header_critical', 0) > 0]
+        if header_error_pages:
+            f.write("### ❌ 表头严重错误页面\n\n")
+            for page in header_error_pages:
+                f.write(f"- 第 {page['page_num']} 页:{page['table_header_critical']} 个表头错误\n")
+            f.write("\n")
+        
+        failed_pages = [p for p in batch_results['pages'] if p['status'] == 'failed']
+        if failed_pages:
+            f.write("### 💥 验证失败页面\n\n")
+            for page in failed_pages:
+                f.write(f"- 第 {page['page_num']} 页:{page.get('error', '未知错误')}\n")
+            f.write("\n")
+
+
+
+def display_batch_validation_results(batch_results: dict):
+    """显示批量验证结果"""
+    st.header("📊 批量验证结果")
+    
+    summary = batch_results['summary']
+    
+    col1, col2, col3, col4 = st.columns(4)
+    with col1:
+        st.metric("总页数", summary['total_pages'])
+    with col2:
+        st.metric("成功页数", summary['successful_pages'], 
+                 delta=f"{summary['successful_pages']/summary['total_pages']*100:.1f}%")
+    with col3:
+        st.metric("失败页数", summary['failed_pages'],
+                 delta=f"-{summary['failed_pages']}" if summary['failed_pages'] > 0 else "0")
+    with col4:
+        st.metric("总差异数", summary['total_differences'])
+    
+    # ✅ 详细差异类型统计 - 更新展示
+    st.subheader("📈 差异类型统计")
+    
+    col1, col2, col3 = st.columns(3)
+    with col1:
+        st.metric("表格差异", summary['total_table_differences'])
+        st.caption(f"金额: {summary.get('total_amount_differences', 0)} | 日期: {summary.get('total_datetime_differences', 0)} | 文本: {summary.get('total_text_differences', 0)}")
+    with col2:
+        st.metric("段落差异", summary['total_paragraph_differences'])
+    with col3:
+        st.metric("严重度", f"高:{summary.get('total_high_severity', 0)} 中:{summary.get('total_medium_severity', 0)} 低:{summary.get('total_low_severity', 0)}")
+    
+    # 表格结构差异统计
+    with st.expander("📋 表格结构差异详情", expanded=False):
+        col1, col2, col3, col4 = st.columns(4)
+        with col1:
+            st.metric("表头前", summary.get('total_table_pre_header', 0))
+        with col2:
+            st.metric("表头位置", summary.get('total_table_header_position', 0))
+        with col3:
+            st.metric("表头错误", summary.get('total_table_header_critical', 0))
+        with col4:
+            st.metric("行缺失", summary.get('total_table_row_missing', 0))
+    
+    # ✅ 各页详细结果表格 - 更新列
+    st.subheader("📄 各页详细结果")
+    
+    # 准备DataFrame
+    page_data = []
+    for page in batch_results['pages']:
+        if page['status'] == 'success':
+            page_data.append({
+                '页码': page['page_num'],
+                '状态': '✅ 成功' if page['total_differences'] == 0 else '⚠️ 有差异',
+                '总差异': page['total_differences'],
+                '表格差异': page['table_differences'],
+                '金额': page.get('amount_differences', 0),
+                '日期': page.get('datetime_differences', 0),
+                '文本': page.get('text_differences', 0),
+                '段落': page['paragraph_differences'],
+                '表头前': page.get('table_pre_header', 0),
+                '表头位置': page.get('table_header_position', 0),
+                '表头错误': page.get('table_header_critical', 0),
+                '行缺失': page.get('table_row_missing', 0),
+                '高': page.get('high_severity', 0),
+                '中': page.get('medium_severity', 0),
+                '低': page.get('low_severity', 0)
+            })
+        else:
+            page_data.append({
+                '页码': page['page_num'],
+                '状态': '❌ 失败',
+                '总差异': '-', '表格差异': '-', '金额': '-', '日期': '-', 
+                '文本': '-', '段落': '-', '表头前': '-', '表头位置': '-',
+                '表头错误': '-', '行缺失': '-', '高': '-', '中': '-', '低': '-'
+            })
+    
+    df_pages = pd.DataFrame(page_data)
+    
+    # 显示表格
+    st.dataframe(
+        df_pages,
+        use_container_width=True,
+        hide_index=True,
+        column_config={
+            "页码": st.column_config.NumberColumn("页码", width="small"),
+            "状态": st.column_config.TextColumn("状态", width="small"),
+            "总差异": st.column_config.NumberColumn("总差异", width="small"),
+            "表格差异": st.column_config.NumberColumn("表格", width="small"),
+            "金额": st.column_config.NumberColumn("金额", width="small"),
+            "日期": st.column_config.NumberColumn("日期", width="small"),
+            "文本": st.column_config.NumberColumn("文本", width="small"),
+            "段落": st.column_config.NumberColumn("段落", width="small"),
+        }
+    )
+    
+    # 下载选项
+    st.subheader("📥 导出报告")
+    
+    col1, col2 = st.columns(2)
+    
+    with col1:
+        # 导出Excel
+        excel_buffer = BytesIO()
+        df_pages.to_excel(excel_buffer, index=False, sheet_name='验证结果')
+        
+        st.download_button(
+            label="📊 下载Excel报告",
+            data=excel_buffer.getvalue(),
+            file_name=f"batch_validation_{pd.Timestamp.now().strftime('%Y%m%d_%H%M%S')}.xlsx",
+            mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
+        )
+    
+    with col2:
+        # 导出JSON
+        json_data = json.dumps(batch_results, ensure_ascii=False, indent=2)
+        
+        st.download_button(
+            label="📄 下载JSON报告",
+            data=json_data,
+            file_name=f"batch_validation_{pd.Timestamp.now().strftime('%Y%m%d_%H%M%S')}.json",
+            mime="application/json"
+    )
+
+@st.dialog("查看交叉验证结果", width="large", dismissible=True, on_dismiss="rerun")
+def show_batch_cross_validation_results_dialog():
+    """显示批量验证结果对话框"""
+    if 'cross_validation_batch_result' in st.session_state and st.session_state.cross_validation_batch_result:
+        display_batch_validation_results(st.session_state.cross_validation_batch_result)
+    else:
+        st.info("暂无交叉验证结果,请先运行交叉验证")

+ 387 - 0
streamlit_validator_result.py

@@ -0,0 +1,387 @@
+"""
+验证结果展示模块
+"""
+import streamlit as st
+import pandas as pd
+import plotly.express as px
+from io import BytesIO
+import json
+from pathlib import Path
+
+
+def display_single_page_cross_validation(validator, config):
+    """显示单页交叉验证结果
+    
+    Args:
+        validator: OCR验证器实例
+        config: 配置字典
+    """
+    current_md_path = Path(validator.file_paths[validator.selected_file_index]).with_suffix('.md')
+    pre_validation_dir = Path(validator.config['pre_validation'].get('out_dir', './output/pre_validation/')).resolve()
+    comparison_result_path = pre_validation_dir / f"{current_md_path.stem}_cross_validation.json"
+    verify_md_path = validator.find_verify_md_path(validator.selected_file_index)
+    
+    # 检查验证结果是否与当前数据源匹配
+    result_is_valid = False
+    comparison_result = None
+    
+    if comparison_result_path.exists():
+        try:
+            with open(comparison_result_path, "r", encoding="utf-8") as f:
+                comparison_result = json.load(f)
+            
+            # 检查文件路径是否匹配(验证结果是否为当前页面生成)
+            if (comparison_result.get('file1_path') == str(current_md_path) and 
+                comparison_result.get('file2_path') == str(verify_md_path)):
+                result_is_valid = True
+        except Exception as e:
+            st.error(f"读取验证结果失败: {e}")
+    
+    if result_is_valid:
+        _display_valid_cross_validation_result(
+            validator, config, current_md_path, verify_md_path, comparison_result
+        )
+    else:
+        _display_no_validation_result_prompt(validator)
+
+
+def _display_valid_cross_validation_result(validator, config, current_md_path, verify_md_path, comparison_result):
+    """显示有效的交叉验证结果
+    
+    Args:
+        validator: OCR验证器实例
+        config: 配置字典
+        current_md_path: 当前OCR文件路径
+        verify_md_path: 验证文件路径
+        comparison_result: 对比结果字典
+    """
+    col1, col2 = st.columns([1, 1])
+    
+    # 左侧:原OCR识别结果
+    with col1:
+        st.subheader("🤖 原OCR识别结果")
+        if current_md_path.exists():
+            with open(current_md_path, "r", encoding="utf-8") as f:
+                original_md_content = f.read()
+            
+            font_size = config['styles'].get('font_size', 10)
+            height = config['styles']['layout'].get('default_height', 800)
+            validator.layout_manager.render_content_by_mode(
+                original_md_content, "HTML渲染", font_size, height, "compact"
+            )
+        else:
+            st.error("原OCR文件不存在")
+    
+    # 右侧:验证识别结果
+    with col2:
+        st.subheader("🤖 验证识别结果")
+        if verify_md_path and verify_md_path.exists():
+            with open(str(verify_md_path), "r", encoding="utf-8") as f:
+                verify_md_content = f.read()
+            
+            font_size = config['styles'].get('font_size', 10)
+            height = config['styles']['layout'].get('default_height', 800)
+            validator.layout_manager.render_content_by_mode(
+                verify_md_content, "HTML渲染", font_size, height, "compact"
+            )
+        else:
+            st.warning("验证文件不存在")
+    
+    st.markdown("---")
+    
+    # 显示详细的对比结果
+    display_comparison_results(comparison_result, detailed=True)
+
+
+def _display_no_validation_result_prompt(validator):
+    """显示无验证结果的提示信息
+    
+    Args:
+        validator: OCR验证器实例
+    """
+    st.info("💡 暂无当前页面的交叉验证结果,请点击上方「交叉验证」按钮运行验证")
+    
+    # 显示当前数据源信息
+    col1, col2 = st.columns(2)
+    
+    with col1:
+        st.write("**当前OCR数据源:**")
+        from ocr_validator_utils import get_data_source_display_name
+        
+        if validator.current_source_config and validator.file_info:
+            current_source_name = get_data_source_display_name(validator.current_source_config)
+            current_page = validator.file_info[validator.selected_file_index]['page']
+            st.code(f"{current_source_name}\n第 {current_page} 页")
+        else:
+            st.warning("未选择OCR数据源")
+    
+    with col2:
+        st.write("**当前验证数据源:**")
+        if validator.verify_source_config:
+            from ocr_validator_utils import get_data_source_display_name
+            verify_source_name = get_data_source_display_name(validator.verify_source_config)
+            st.code(verify_source_name)
+        else:
+            st.warning("未选择验证数据源")
+    
+    # 添加操作提示
+    st.markdown("---")
+    st.markdown("""
+    ### 📝 操作步骤:
+    
+    1. **选择数据源**: 在页面顶部选择不同的OCR数据源和验证数据源
+    2. **运行验证**: 点击「交叉验证」按钮开始批量验证
+    3. **查看结果**: 验证完成后,在此处查看详细对比结果
+    
+    💡 **提示**: 
+    - 确保两个数据源包含相同页码的文件
+    - 建议选择不同OCR工具的结果进行交叉验证
+    - 验证结果会自动保存,可随时查看
+    """)
+
+
+def display_comparison_results(comparison_result: dict, detailed: bool = True):
+    """显示对比结果
+    
+    Args:
+        comparison_result: 对比结果字典
+        detailed: 是否显示详细信息
+    """
+    st.header("📊 交叉验证结果")
+    
+    stats = comparison_result['statistics']
+    
+    # 显示主要指标
+    col1, col2, col3, col4 = st.columns(4)
+    with col1:
+        st.metric("总差异数", stats['total_differences'])
+    with col2:
+        st.metric("表格差异", stats['table_differences'])
+    with col3:
+        st.metric("金额差异", stats.get('amount_differences', 0))
+    with col4:
+        st.metric("段落差异", stats['paragraph_differences'])
+    
+    # 根据差异数量显示不同的提示
+    if stats['total_differences'] == 0:
+        st.success("🎉 完美匹配!两个数据源结果完全一致")
+    else:
+        st.warning(f"⚠️ 发现 {stats['total_differences']} 个差异,建议人工检查")
+        
+        if comparison_result['differences'] and detailed:
+            _display_differences_dataframe(comparison_result)
+            _display_difference_details(comparison_result)
+            _display_difference_charts(comparison_result)
+            _provide_download_options(comparison_result)
+
+
+def _display_differences_dataframe(comparison_result: dict):
+    """显示差异DataFrame"""
+    st.subheader("🔍 差异详情对比")
+    
+    diff_data = []
+    for i, diff in enumerate(comparison_result['differences'], 1):
+        diff_data.append({
+            '序号': i,
+            '位置': diff['position'],
+            '类型': diff['type'],
+            '原OCR结果': diff['file1_value'][:100] + ('...' if len(diff['file1_value']) > 100 else ''),
+            '验证结果': diff['file2_value'][:100] + ('...' if len(diff['file2_value']) > 100 else ''),
+            '描述': diff['description'][:80] + ('...' if len(diff['description']) > 80 else ''),
+            '严重程度': _get_severity_level(diff)
+        })
+    
+    df_differences = pd.DataFrame(diff_data)
+    
+    def highlight_severity(val):
+        if val == '高':
+            return 'background-color: #ffebee; color: #c62828'
+        elif val == '中':
+            return 'background-color: #fff3e0; color: #ef6c00'
+        elif val == '低':
+            return 'background-color: #e8f5e8; color: #2e7d32'
+        return ''
+    
+    styled_df = df_differences.style.applymap(
+        highlight_severity, 
+        subset=['严重程度']
+    ).format({'序号': '{:d}'})
+    
+    st.dataframe(styled_df, use_container_width=True, height=400, hide_index=True)
+
+
+def _display_difference_details(comparison_result: dict):
+    """显示详细差异"""
+    st.subheader("🔍 详细差异查看")
+    
+    selected_diff_index = st.selectbox(
+        "选择要查看的差异:",
+        options=range(len(comparison_result['differences'])),
+        format_func=lambda x: f"差异 {x+1}: {comparison_result['differences'][x]['position']} - {comparison_result['differences'][x]['type']}",
+        key="selected_diff"
+    )
+    
+    if selected_diff_index is not None:
+        diff = comparison_result['differences'][selected_diff_index]
+        
+        col1, col2 = st.columns(2)
+        
+        with col1:
+            st.write("**原OCR结果:**")
+            st.text_area("原OCR结果详情", value=diff['file1_value'], height=200, 
+                        key=f"original_{selected_diff_index}", label_visibility="collapsed")
+        
+        with col2:
+            st.write("**验证结果:**")
+            st.text_area("验证结果详情", value=diff['file2_value'], height=200, 
+                        key=f"verify_{selected_diff_index}", label_visibility="collapsed")
+        
+        st.info(f"**位置:** {diff['position']}")
+        st.info(f"**类型:** {diff['type']}")
+        st.info(f"**描述:** {diff['description']}")
+        st.info(f"**严重程度:** {_get_severity_level(diff)}")
+
+
+def _display_difference_charts(comparison_result: dict):
+    """显示差异统计图表"""
+    st.subheader("📈 差异类型分布")
+    
+    type_counts = {}
+    severity_counts = {'高': 0, '中': 0, '低': 0}
+    
+    for diff in comparison_result['differences']:
+        diff_type = diff['type']
+        type_counts[diff_type] = type_counts.get(diff_type, 0) + 1
+        
+        severity = _get_severity_level(diff)
+        severity_counts[severity] += 1
+    
+    col1, col2 = st.columns(2)
+    
+    with col1:
+        if type_counts:
+            fig_type = px.pie(
+                values=list(type_counts.values()),
+                names=list(type_counts.keys()),
+                title="差异类型分布"
+            )
+            st.plotly_chart(fig_type, use_container_width=True)
+    
+    with col2:
+        fig_severity = px.bar(
+            x=list(severity_counts.keys()),
+            y=list(severity_counts.values()),
+            title="差异严重程度分布",
+            color=list(severity_counts.keys()),
+            color_discrete_map={'高': '#f44336', '中': '#ff9800', '低': '#4caf50'}
+        )
+        st.plotly_chart(fig_severity, use_container_width=True)
+
+
+def _provide_download_options(comparison_result: dict):
+    """提供下载选项"""
+    st.subheader("📥 导出验证结果")
+    
+    col1, col2, col3 = st.columns(3)
+    
+    with col1:
+        if comparison_result['differences']:
+            diff_data = []
+            for i, diff in enumerate(comparison_result['differences'], 1):
+                diff_data.append({
+                    '序号': i,
+                    '位置': diff['position'],
+                    '类型': diff['type'],
+                    '原OCR结果': diff['file1_value'],
+                    '验证结果': diff['file2_value'],
+                    '描述': diff['description'],
+                    '严重程度': _get_severity_level(diff)
+                })
+            
+            df_export = pd.DataFrame(diff_data)
+            excel_buffer = BytesIO()
+            df_export.to_excel(excel_buffer, index=False, sheet_name='差异详情')
+            
+            st.download_button(
+                label="📊 下载差异详情(Excel)",
+                data=excel_buffer.getvalue(),
+                file_name=f"comparison_differences_{pd.Timestamp.now().strftime('%Y%m%d_%H%M%S')}.xlsx",
+                mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+                key="download_differences_excel"
+            )
+    
+    with col2:
+        stats_data = {
+            '统计项目': ['总差异数', '表格差异', '金额差异', '段落差异'],
+            '数量': [
+                comparison_result['statistics']['total_differences'],
+                comparison_result['statistics']['table_differences'],
+                comparison_result['statistics'].get('amount_differences', 0),
+                comparison_result['statistics']['paragraph_differences']
+            ]
+        }
+        
+        df_stats = pd.DataFrame(stats_data)
+        csv_stats = df_stats.to_csv(index=False)
+        
+        st.download_button(
+            label="📈 下载统计报告(CSV)",
+            data=csv_stats,
+            file_name=f"comparison_stats_{pd.Timestamp.now().strftime('%Y%m%d_%H%M%S')}.csv",
+            mime="text/csv",
+            key="download_stats_csv"
+        )
+    
+    with col3:
+        report_json = json.dumps(comparison_result, ensure_ascii=False, indent=2)
+        
+        st.download_button(
+            label="📄 下载完整报告(JSON)",
+            data=report_json,
+            file_name=f"comparison_full_{pd.Timestamp.now().strftime('%Y%m%d_%H%M%S')}.json",
+            mime="application/json",
+            key="download_full_json"
+        )
+
+
+def _get_severity_level(diff: dict) -> str:
+    """判断严重程度
+    
+    Args:
+        diff: 差异字典
+    
+    Returns:
+        严重程度: '高', '中', '低'
+    """
+    if 'severity' in diff:
+        severity_map = {'critical': '高', 'high': '高', 'medium': '中', 'low': '低'}
+        return severity_map.get(diff['severity'], '中')
+    
+    diff_type = diff['type'].lower()
+    
+    # 金额和数字类差异为高严重度
+    if 'amount' in diff_type or 'number' in diff_type:
+        return '高'
+    
+    # 表格和结构类差异为中严重度
+    if 'table' in diff_type or 'structure' in diff_type:
+        return '中'
+    
+    # 根据相似度判断
+    if 'similarity' in diff:
+        similarity = diff['similarity']
+        if similarity < 50:
+            return '高'
+        elif similarity < 85:
+            return '中'
+        else:
+            return '低'
+    
+    # 根据长度差异判断
+    len_diff = abs(len(diff['file1_value']) - len(diff['file2_value']))
+    if len_diff > 50:
+        return '高'
+    elif len_diff > 10:
+        return '中'
+    else:
+        return '低'

+ 266 - 0
streamlit_validator_table.py

@@ -0,0 +1,266 @@
+"""
+表格处理和分析功能
+"""
+import streamlit as st
+import pandas as pd
+import numpy as np
+from io import BytesIO
+from ocr_validator_file_utils import parse_html_tables
+
+
+def display_html_table_as_dataframe(html_content: str, enable_editing: bool = False):
+    """将HTML表格解析为DataFrame显示"""
+    tables = parse_html_tables(html_content)
+    wide_table_threshold = 15
+    
+    if not tables:
+        st.warning("未找到可解析的表格")
+        st.markdown("""
+        <style>
+        .scrollable-table {
+            overflow-x: auto;
+            white-space: nowrap;
+            border: 1px solid #ddd;
+            border-radius: 5px;
+            margin: 10px 0;
+        }
+        .scrollable-table table {
+            width: 100%;
+            border-collapse: collapse;
+        }
+        .scrollable-table th, .scrollable-table td {
+            border: 1px solid #ddd;
+            padding: 8px;
+            text-align: left;
+            min-width: 100px;
+        }
+        .scrollable-table th {
+            background-color: #f5f5f5;
+            font-weight: bold;
+        }
+        </style>
+        """, unsafe_allow_html=True)
+        
+        st.markdown(f'<div class="scrollable-table">{html_content}</div>', unsafe_allow_html=True)
+        return
+        
+    for i, table in enumerate(tables):
+        st.subheader(f"📊 表格 {i+1}")
+        
+        col_info1, col_info2, col_info3, col_info4 = st.columns(4)
+        with col_info1:
+            st.metric("行数", len(table))
+        with col_info2:
+            st.metric("列数", len(table.columns))
+        with col_info3:
+            is_wide_table = len(table.columns) > wide_table_threshold
+            st.metric("表格类型", "超宽表格" if is_wide_table else "普通表格")
+        with col_info4:
+            display_mode = st.selectbox(
+                f"显示模式 (表格{i+1})",
+                ["完整显示", "分页显示", "筛选列显示"],
+                key=f"display_mode_{i}"
+            )
+        
+        col1, col2, col3, col4 = st.columns(4)
+        with col1:
+            show_info = st.checkbox(f"显示详细信息", key=f"info_{i}")
+        with col2:
+            show_stats = st.checkbox(f"显示统计信息", key=f"stats_{i}")
+        with col3:
+            enable_filter = st.checkbox(f"启用过滤", key=f"filter_{i}")
+        with col4:
+            enable_sort = st.checkbox(f"启用排序", key=f"sort_{i}")
+        
+        display_table = _process_table_display_mode(table, i, display_mode)
+        filtered_table = _apply_table_filters_and_sorts(display_table, i, enable_filter, enable_sort)
+        
+        _render_table_with_style(filtered_table, table, i, enable_editing, wide_table_threshold)
+        _display_table_info_and_stats(table, filtered_table, show_info, show_stats, i)
+        
+        st.markdown("---")
+
+
+def _process_table_display_mode(table: pd.DataFrame, table_index: int, display_mode: str) -> pd.DataFrame:
+    """根据显示模式处理表格"""
+    if display_mode == "分页显示":
+        page_size = st.selectbox(
+            f"每页显示行数 (表格 {table_index+1})",
+            [10, 20, 50, 100],
+            key=f"page_size_{table_index}"
+        )
+        
+        total_pages = (len(table) - 1) // page_size + 1
+        
+        if total_pages > 1:
+            page_number = st.selectbox(
+                f"页码 (表格 {table_index+1})",
+                range(1, total_pages + 1),
+                key=f"page_number_{table_index}"
+            )
+            
+            start_idx = (page_number - 1) * page_size
+            end_idx = start_idx + page_size
+            return table.iloc[start_idx:end_idx]
+        
+        return table
+        
+    elif display_mode == "筛选列显示":
+        if len(table.columns) > 5:
+            selected_columns = st.multiselect(
+                f"选择要显示的列 (表格 {table_index+1})",
+                table.columns.tolist(),
+                default=table.columns.tolist()[:5],
+                key=f"selected_columns_{table_index}"
+            )
+            
+            if selected_columns:
+                return table[selected_columns]
+        
+        return table
+        
+    else:
+        return table
+
+
+def _apply_table_filters_and_sorts(table: pd.DataFrame, table_index: int, 
+                                   enable_filter: bool, enable_sort: bool) -> pd.DataFrame:
+    """应用表格过滤和排序"""
+    filtered_table = table.copy()
+    
+    if enable_filter and not table.empty:
+        filter_col = st.selectbox(
+            f"选择过滤列 (表格 {table_index+1})", 
+            options=['无'] + list(table.columns),
+            key=f"filter_col_{table_index}"
+        )
+        
+        if filter_col != '无':
+            filter_value = st.text_input(f"过滤值 (表格 {table_index+1})", key=f"filter_value_{table_index}")
+            if filter_value:
+                filtered_table = table[table[filter_col].astype(str).str.contains(filter_value, na=False)]
+    
+    if enable_sort and not filtered_table.empty:
+        sort_col = st.selectbox(
+            f"选择排序列 (表格 {table_index+1})", 
+            options=['无'] + list(filtered_table.columns),
+            key=f"sort_col_{table_index}"
+        )
+        
+        if sort_col != '无':
+            sort_order = st.radio(
+                f"排序方式 (表格 {table_index+1})",
+                options=['升序', '降序'],
+                horizontal=True,
+                key=f"sort_order_{table_index}"
+            )
+            ascending = (sort_order == '升序')
+            filtered_table = filtered_table.sort_values(sort_col, ascending=ascending)
+    
+    return filtered_table
+
+
+def _render_table_with_style(filtered_table: pd.DataFrame, original_table: pd.DataFrame,
+                             table_index: int, enable_editing: bool, wide_table_threshold: int):
+    """渲染表格并应用样式"""
+    st.markdown("""
+    <style>
+    .dataframe-container {
+        overflow-x: auto;
+        border: 1px solid #ddd;
+        border-radius: 5px;
+        margin: 10px 0;
+    }
+    
+    .wide-table-container {
+        overflow-x: auto;
+        max-height: 500px;
+        overflow-y: auto;
+        border: 2px solid #0288d1;
+        border-radius: 8px;
+        background: linear-gradient(90deg, #f8f9fa 0%, #ffffff 100%);
+    }
+    
+    .dataframe thead th {
+        position: sticky;
+        top: 0;
+        background-color: #f5f5f5 !important;
+        z-index: 10;
+        border-bottom: 2px solid #0288d1;
+    }
+    
+    .dataframe tbody td {
+        white-space: nowrap;
+        min-width: 100px;
+        max-width: 300px;
+        overflow: hidden;
+        text-overflow: ellipsis;
+    }
+    </style>
+    """, unsafe_allow_html=True)
+    
+    container_class = "wide-table-container" if len(original_table.columns) > wide_table_threshold else "dataframe-container"
+    
+    if enable_editing:
+        st.markdown(f'<div class="{container_class}">', unsafe_allow_html=True)
+        edited_table = st.data_editor(
+            filtered_table, 
+            use_container_width=True, 
+            key=f"editor_{table_index}",
+            height=400 if len(original_table.columns) > 8 else None
+        )
+        st.markdown('</div>', unsafe_allow_html=True)
+        
+        if not edited_table.equals(filtered_table):
+            st.success("✏️ 表格已编辑,可以导出修改后的数据")
+    else:
+        st.markdown(f'<div class="{container_class}">', unsafe_allow_html=True)
+        st.dataframe(
+            filtered_table, 
+            width=400 if len(original_table.columns) > wide_table_threshold else "stretch"
+        )
+        st.markdown('</div>', unsafe_allow_html=True)
+
+
+def _display_table_info_and_stats(original_table: pd.DataFrame, filtered_table: pd.DataFrame, 
+                                  show_info: bool, show_stats: bool, table_index: int):
+    """显示表格信息和统计数据"""
+    if show_info:
+        st.write("**表格信息:**")
+        st.write(f"- 原始行数: {len(original_table)}")
+        st.write(f"- 过滤后行数: {len(filtered_table)}")
+        st.write(f"- 列数: {len(original_table.columns)}")
+        st.write(f"- 列名: {', '.join(original_table.columns)}")
+    
+    if show_stats:
+        st.write("**统计信息:**")
+        numeric_cols = filtered_table.select_dtypes(include=[np.number]).columns
+        if len(numeric_cols) > 0:
+            st.dataframe(filtered_table[numeric_cols].describe())
+        else:
+            st.info("表格中没有数值列")
+    
+    if st.button(f"📥 导出表格 {table_index+1}", key=f"export_{table_index}"):
+        _create_export_buttons(filtered_table, table_index)
+
+
+def _create_export_buttons(table: pd.DataFrame, table_index: int):
+    """创建导出按钮"""
+    csv_data = table.to_csv(index=False)
+    st.download_button(
+        label=f"下载CSV (表格 {table_index+1})",
+        data=csv_data,
+        file_name=f"table_{table_index+1}.csv",
+        mime="text/csv",
+        key=f"download_csv_{table_index}"
+    )
+    
+    excel_buffer = BytesIO()
+    table.to_excel(excel_buffer, index=False)
+    st.download_button(
+        label=f"下载Excel (表格 {table_index+1})",
+        data=excel_buffer.getvalue(),
+        file_name=f"table_{table_index+1}.xlsx",
+        mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+        key=f"download_excel_{table_index}"
+    )

+ 129 - 0
streamlit_validator_ui.py

@@ -0,0 +1,129 @@
+"""
+UI组件和页面配置
+"""
+import streamlit as st
+from ocr_validator_file_utils import load_css_styles
+from ocr_validator_utils import get_data_source_display_name
+
+
+def setup_page_config(config):
+    """设置页面配置"""
+    ui_config = config['ui']
+    st.set_page_config(
+        page_title=ui_config['page_title'],
+        page_icon=ui_config['page_icon'],
+        layout=ui_config['layout'],
+        initial_sidebar_state=ui_config['sidebar_state']
+    )
+    
+    css_content = load_css_styles()
+    st.markdown(f"<style>{css_content}</style>", unsafe_allow_html=True)
+
+
+def create_data_source_selector(validator):
+    """创建双数据源选择器"""
+    if not validator.all_sources:
+        st.warning("❌ 未找到任何数据源,请检查配置文件")
+        return
+    
+    source_options = {}
+    for source_key, source_data in validator.all_sources.items():
+        display_name = get_data_source_display_name(source_data['config'])
+        source_options[display_name] = source_key
+    
+    col1, col2 = st.columns(2)
+    
+    with col1:
+        st.markdown("#### 📊 OCR数据源")
+        current_display_name = None
+        if validator.current_source_key:
+            for display_name, key in source_options.items():
+                if key == validator.current_source_key:
+                    current_display_name = display_name
+                    break
+        
+        selected_ocr_display = st.selectbox(
+            "选择OCR数据源",
+            options=list(source_options.keys()),
+            index=list(source_options.keys()).index(current_display_name) if current_display_name else 0,
+            key="ocr_source_selector",
+            label_visibility="collapsed",
+            help="选择要分析的OCR数据源"
+        )
+        
+        selected_ocr_key = source_options[selected_ocr_display]
+        
+        if selected_ocr_key != validator.current_source_key:
+            validator.switch_to_source(selected_ocr_key)
+            if 'selected_file_index' in st.session_state:
+                st.session_state.selected_file_index = 0
+            # ✅ 数据源变更会在主函数中检测并重置验证结果
+            st.rerun()
+        
+        if validator.current_source_config:
+            with st.expander("📋 OCR数据源详情", expanded=False):
+                st.write(f"**工具:** {validator.current_source_config['ocr_tool']}")
+                st.write(f"**文件数:** {len(validator.file_info)}")
+    
+    with col2:
+        st.markdown("#### 🔍 验证数据源")
+        verify_display_name = None
+        if validator.verify_source_key:
+            for display_name, key in source_options.items():
+                if key == validator.verify_source_key:
+                    verify_display_name = display_name
+                    break
+        
+        selected_verify_display = st.selectbox(
+            "选择验证数据源",
+            options=list(source_options.keys()),
+            index=list(source_options.keys()).index(verify_display_name) if verify_display_name else (1 if len(source_options) > 1 else 0),
+            key="verify_source_selector",
+            label_visibility="collapsed",
+            help="选择用于交叉验证的数据源"
+        )
+        
+        selected_verify_key = source_options[selected_verify_display]
+        
+        if selected_verify_key != validator.verify_source_key:
+            validator.switch_to_verify_source(selected_verify_key)
+            # ✅ 数据源变更会在主函数中检测并重置验证结果
+            st.rerun()
+        
+        if validator.verify_source_config:
+            with st.expander("📋 验证数据源详情", expanded=False):
+                st.write(f"**工具:** {validator.verify_source_config['ocr_tool']}")
+                st.write(f"**文件数:** {len(validator.verify_file_info)}")
+    
+    # ✅ 显示数据源状态提示
+    if validator.current_source_key == validator.verify_source_key:
+        st.warning("⚠️ OCR数据源和验证数据源相同,建议选择不同的数据源进行交叉验证")
+    else:
+        # 检查是否有交叉验证结果
+        has_results = 'cross_validation_batch_result' in st.session_state and st.session_state.cross_validation_batch_result is not None
+        
+        if has_results:
+            # 检查验证结果是否与当前数据源匹配
+            result = st.session_state.cross_validation_batch_result
+            result_ocr_source = result.get('ocr_source', '')
+            result_verify_source = result.get('verify_source', '')
+            current_ocr_source = get_data_source_display_name(validator.current_source_config)
+            current_verify_source = get_data_source_display_name(validator.verify_source_config)
+            
+            if result_ocr_source == current_ocr_source and result_verify_source == current_verify_source:
+                st.success(f"✅ 已选择 {selected_ocr_display} 与 {selected_verify_display} 进行交叉验证(已有验证结果)")
+            else:
+                st.info(f"ℹ️ 已选择 {selected_ocr_display} 与 {selected_verify_display} 进行交叉验证(验证结果已过期,请重新验证)")
+        else:
+            st.success(f"✅ 已选择 {selected_ocr_display} 与 {selected_verify_display} 进行交叉验证")
+
+
+@st.dialog("message", width="small", dismissible=True, on_dismiss="rerun")
+def message_box(msg: str, msg_type: str = "info"):
+    """消息对话框"""
+    if msg_type == "info":
+        st.info(msg)
+    elif msg_type == "warning":
+        st.warning(msg)
+    elif msg_type == "error":
+        st.error(msg)

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است