CoolFace
Apppublic

RenJW/RNAediting

sourceHugging Facemitupdated 10mo agoView on Hugging Face
0likes
app.py898 linesDownload Raw Back to root
1import gradio as gr2import os3import shutil4import subprocess5import hashlib6from datetime import datetime7from pathlib import Path8import glob9import re10import urllib.request11import stat12import zipfile13import threading14import time15 16# 安装 fastp(如果不存在)17def setup_fastp():18    """下载并安装 fastp 预编译二进制"""19    fastp_path = "/tmp/fastp"20    if not os.path.exists(fastp_path):21        try:22            print("📥 正在下载 fastp...")23            url = "http://opengene.org/fastp/fastp"24            urllib.request.urlretrieve(url, fastp_path)25            os.chmod(fastp_path, stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH)26            # 添加到 PATH27            os.environ["PATH"] = "/tmp:" + os.environ.get("PATH", "")28            print("✅ fastp 安装成功")29            return True30        except Exception as e:31            print(f"⚠️ fastp 安装失败: {e}")32            print("   分析流程将跳过 fastp 步骤(如果可能)")33            return False34    else:35        print("✓ fastp 已存在")36        os.environ["PATH"] = "/tmp:" + os.environ.get("PATH", "")37        return True38 39def setup_ossutil():40    """设置 ossutil64 执行权限"""41    ossutil_path = "./ossutil64"42    if os.path.exists(ossutil_path):43        try:44            os.chmod(ossutil_path, stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH)45            print("✓ ossutil64 权限已设置")46        except Exception as e:47            print(f"⚠️ 无法设置 ossutil64 权限: {e}")48 49# 在启动时安装工具50setup_fastp()51setup_ossutil()52 53def schedule_deletion(project_dir, delay_hours=24):54    """55    安排在指定时间后删除项目目录56    57    Args:58        project_dir: 项目目录路径59        delay_hours: 延迟删除的小时数 (默认24小时)60    """61    def delete_after_delay():62        time.sleep(delay_hours * 3600)  # 转换为秒63        try:64            if os.path.exists(project_dir):65                shutil.rmtree(project_dir)66                print(f"✅ 已自动删除项目目录: {project_dir}")67            68            # 同时删除压缩包69            full_zip = f"{project_dir}_full.zip"70            results_zip = f"{project_dir}_results.zip"71            if os.path.exists(full_zip):72                os.remove(full_zip)73                print(f"✅ 已自动删除: {full_zip}")74            if os.path.exists(results_zip):75                os.remove(results_zip)76                print(f"✅ 已自动删除: {results_zip}")77        except Exception as e:78            print(f"⚠️ 自动删除失败: {e}")79    80    # 在后台线程中执行删除81    thread = threading.Thread(target=delete_after_delay, daemon=True)82    thread.start()83    print(f"⏰ 已安排24小时后自动删除: {project_dir}")84 85from data_handler import DataHandler86from script_generator import ScriptGenerator87from task_executor import TaskExecutor88from task_manager import TaskManager89 90class NGSWebApp:91    def __init__(self):92        self.data_handler = DataHandler()93        self.script_generator = ScriptGenerator()94        self.task_executor = TaskExecutor()95        self.task_manager = TaskManager()96        97    def process_pipeline(self, 98                         upload_method,99                         # OSS方式参数100                         access_key_id,101                         access_key_secret,102                         oss_path,103                         # 样本选择104                         selected_samples,105                         # 直接上传参数106                         uploaded_files,107                         # 参考序列参数108                         use_multiple_refs,109                         ref_sequence_single,110                         target_sites,111                         # 多参考序列参数112                         ref_configs,113                         # 进度更新回调114                         progress=gr.Progress()):115        """116        处理整个NGS数据分析流程117        """118        log_messages = []119        120        def log_and_yield(message, full_zip=None, results_zip=None):121            """输出日志并实时更新界面"""122            if message:  # 只有在有消息时才添加123                log_messages.append(message)124            current_log = "\n".join(log_messages)125            # 返回三个值: 日志文本、完整包路径、结果包路径126            return current_log, full_zip, results_zip127        128        try:129            130            # 1. 创建日期时间文件夹131            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")132            task_id = timestamp  # 任务ID就是时间戳133            project_dir = Path(f"./{timestamp}").resolve()  # 转换为绝对路径134            data_dir = project_dir / "00-data"135            data_dir.mkdir(parents=True, exist_ok=True)136            137            yield log_and_yield(f"✅ 创建项目目录: {project_dir}")138            yield log_and_yield(f"📋 任务ID: {task_id}")139            yield log_and_yield(f"💡 提示: 保存此任务ID以便稍后查询进度")140            141            # 2. 处理数据上传/下载142            if upload_method == "OSS下载(金唯智)":143                yield log_and_yield("📥 开始从OSS下载数据...")144                result = self.data_handler.download_from_oss(145                    access_key_id, 146                    access_key_secret, 147                    oss_path, 148                    str(data_dir)149                )150                yield log_and_yield(result)151                152                # MD5校验153                yield log_and_yield("🔍 开始MD5校验...")154                md5_result = self.data_handler.check_md5(str(data_dir))155                yield log_and_yield(md5_result)156                157                # 移动文件158                yield log_and_yield("📦 整理数据文件...")159                move_result = self.data_handler.move_fastq_files(str(data_dir))160                yield log_and_yield(move_result)161                162                # 提取样本名163                yield log_and_yield("🔬 提取样本名...")164                all_samples = self.data_handler.extract_sample_names(str(data_dir))165                if not all_samples:166                    yield log_and_yield("❌ 错误: 未找到有效的测序文件")167                    return168                yield log_and_yield(f"  找到 {len(all_samples)} 个样本: {', '.join(all_samples)}")169                170                # OSS方式:根据用户选择过滤样本171                if selected_samples and isinstance(selected_samples, list) and len(selected_samples) > 0:172                    # 转换为集合以便快速查找173                    selected_set = set(selected_samples)174                    samples = [s for s in all_samples if s in selected_set]175                    176                    # 删除未选择的样本文件177                    removed_samples = [s for s in all_samples if s not in selected_set]178                    if removed_samples:179                        yield log_and_yield(f"🗑️  删除未选择的样本...")180                        for sample in removed_samples:181                            # 删除该样本的所有文件182                            for fq_file in data_dir.glob(f"{sample}_*.fq.gz"):183                                fq_file.unlink()184                                yield log_and_yield(f"    ✓ 已删除: {fq_file.name}")185                        yield log_and_yield(f"  已删除 {len(removed_samples)} 个未选择的样本")186                    187                    if not samples:188                        yield log_and_yield("❌ 错误: 没有选择任何样本进行分析")189                        return190                        191                    yield log_and_yield(f"✅ 将分析 {len(samples)} 个选中的样本: {', '.join(samples)}")192                else:193                    # 未指定选择,使用全部样本194                    samples = all_samples195                    yield log_and_yield(f"✅ 将分析全部 {len(samples)} 个样本")196                197            else:  # 直接上传198                yield log_and_yield("📤 处理上传的文件...")199                if not uploaded_files:200                    yield log_and_yield("❌ 错误: 请上传测序文件")201                    return202                    203                for file in uploaded_files:204                    if file is not None:205                        file_name = os.path.basename(file.name)206                        target_path = data_dir / file_name207                        shutil.copy(file.name, target_path)208                        yield log_and_yield(f"  ✓ 已保存: {file_name}")209                210                # 提取样本名211                yield log_and_yield("🔬 提取样本名...")212                all_samples = self.data_handler.extract_sample_names(str(data_dir))213                if not all_samples:214                    yield log_and_yield("❌ 错误: 未找到有效的测序文件")215                    return216                yield log_and_yield(f"  找到 {len(all_samples)} 个样本: {', '.join(all_samples)}")217                218                # 直接上传方式:使用全部样本219                samples = all_samples220                yield log_and_yield(f"✅ 将分析全部 {len(samples)} 个样本")221            222            # 3. 创建任务记录223            yield log_and_yield("📝 创建任务记录...")224            task_info = self.task_manager.create_task(225                task_id=task_id,226                samples=samples,227                ref_mapping={},  # 稍后更新228                upload_method=upload_method229            )230            yield log_and_yield(f"  ✓ 任务记录已创建")231            232            # 4. 创建参考序列233            ref_dir = project_dir / "ref"234            ref_dir.mkdir(exist_ok=True)235            236            if use_multiple_refs and ref_configs:237                yield log_and_yield("📝 创建多个参考序列...")238                ref_mapping = {}239                for config in ref_configs:240                    if config and len(config) >= 3:241                        ref_name, sample_names_str, ref_seq = config[0], config[1], config[2]242                        ref_file = ref_dir / f"{ref_name}.fa"243                        with open(ref_file, 'w') as f:244                            f.write(ref_seq)245                        yield log_and_yield(f"  ✓ 创建参考序列: {ref_name}.fa")246                        247                        # 解析样本名列表(用逗号分隔)248                        specified_samples = [s.strip() for s in sample_names_str.split(',') if s.strip()]249                        250                        # 匹配样本名251                        for specified_sample in specified_samples:252                            if specified_sample in samples:253                                ref_mapping[specified_sample] = ref_name254                                yield log_and_yield(f"    → 样本 {specified_sample} 使用 {ref_name}")255                            else:256                                yield log_and_yield(f"    ⚠️ 样本 {specified_sample} 未找到")257                258                # 检查是否有样本未分配参考序列259                unassigned_samples = [s for s in samples if s not in ref_mapping]260                if unassigned_samples:261                    yield log_and_yield(f"  ⚠️ 以下样本未分配参考序列: {', '.join(unassigned_samples)}")262            else:263                yield log_and_yield("📝 创建参考序列...")264                ref_file = ref_dir / "ref.fa"265                with open(ref_file, 'w') as f:266                    f.write(ref_sequence_single)267                yield log_and_yield("  ✓ 创建参考序列: ref.fa")268                ref_mapping = {sample: "ref" for sample in samples}269            270            # 更新任务的参考序列映射271            self.task_manager.update_task(task_id, ref_mapping=ref_mapping)272            273            # 5. 生成脚本274            yield log_and_yield("⚙️ 生成执行脚本...")275            276            # 生成 download.sh (如果是OSS方式)277            if upload_method == "OSS下载(金唯智)":278                download_script = self.script_generator.generate_download_script(279                    access_key_id, access_key_secret, oss_path280                )281                with open(project_dir / "download.sh", 'w') as f:282                    f.write(download_script)283                yield log_and_yield("  ✓ 生成 download.sh")284            285            # 生成 sbatch.sh286            sbatch_script = self.script_generator.generate_sbatch_script(287                samples, ref_mapping288            )289            sbatch_path = project_dir / "sbatch.sh"290            with open(sbatch_path, 'w') as f:291                f.write(sbatch_script)292            yield log_and_yield("  ✓ 生成 sbatch.sh")293            294            # 生成 pipeline.sh295            pipeline_script = self.script_generator.generate_pipeline_script(296                str(project_dir)297            )298            pipeline_path = project_dir / "pipeline.sh"299            with open(pipeline_path, 'w') as f:300                f.write(pipeline_script)301            yield log_and_yield("  ✓ 生成 pipeline.sh")302            303            # 6. 预创建所有必要的目录304            yield log_and_yield("📁 创建分析目录...")305            analysis_dirs = [306                project_dir / "01-fastqc",307                project_dir / "02-fastp", 308                project_dir / "03-fastqc",309                project_dir / "04-bowtie2",310                project_dir / "05-reditools2",311                project_dir / "logs"312            ]313            for dir_path in analysis_dirs:314                dir_path.mkdir(exist_ok=True)315            yield log_and_yield("  ✓ 目录创建完成")316            317            # 7. 执行任务318            yield log_and_yield("🚀 开始执行分析任务...")319            exec_result = self.task_executor.execute_tasks(320                str(project_dir),321                samples,322                ref_mapping323            )324            yield log_and_yield(exec_result)325            326            # 8. 创建下载包327            yield log_and_yield("📦 准备下载文件...")328            329            # 创建完整项目压缩包330            full_zip_path = f"{project_dir}_full.zip"331            with zipfile.ZipFile(full_zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:332                for root, dirs, files in os.walk(project_dir):333                    for file in files:334                        file_path = os.path.join(root, file)335                        arcname = os.path.relpath(file_path, project_dir)336                        zipf.write(file_path, arcname)337            yield log_and_yield(f"  ✓ 完整项目包: {full_zip_path}", full_zip_path, None)338            339            # 创建仅结果文件压缩包 (05-reditools2文件夹)340            results_zip_path = None341            reditools_dir = project_dir / "05-reditools2"342            343            # 调试信息344            print(f"DEBUG: 检查目录 {reditools_dir}")345            print(f"DEBUG: 目录是否存在: {reditools_dir.exists()}")346            if reditools_dir.exists():347                file_list = list(reditools_dir.iterdir())348                print(f"DEBUG: 文件数量: {len(file_list)}")349                print(f"DEBUG: 文件列表: {[f.name for f in file_list]}")350            351            if reditools_dir.exists() and any(reditools_dir.iterdir()):352                results_zip_path = f"{project_dir}_results.zip"353                with zipfile.ZipFile(results_zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:354                    for root, dirs, files in os.walk(reditools_dir):355                        for file in files:356                            file_path = os.path.join(root, file)357                            # 使用相对于reditools_dir的路径,保持文件夹结构358                            arcname = os.path.relpath(file_path, reditools_dir)359                            zipf.write(file_path, f"05-reditools2/{arcname}")360                            print(f"DEBUG: 添加文件到压缩包: {arcname}")361                yield log_and_yield(f"  ✓ REDItools结果包: {results_zip_path}", full_zip_path, results_zip_path)362            else:363                yield log_and_yield(f"  ⚠️ 未找到05-reditools2文件夹或文件夹为空", full_zip_path, None)364            365            # 更新任务状态为完成366            self.task_manager.update_task(367                task_id,368                status="completed",369                full_zip=full_zip_path,370                results_zip=results_zip_path371            )372            373            final_message = "\n".join(log_messages)374            final_message += f"\n\n✅ 分析完成!"375            final_message += f"\n📋 任务ID: {task_id}"376            final_message += f"\n💡 可使用任务ID查询结果: {task_id}"377            final_message += f"\n⏰ 注意: 所有文件将在24小时后自动删除"378            379            # 安排24小时后自动删除380            schedule_deletion(str(project_dir), delay_hours=24)381            382            yield log_and_yield("", full_zip_path, results_zip_path)383            yield final_message, full_zip_path, results_zip_path384            385        except Exception as e:386            import traceback387            error_msg = str(e)388            # 更新任务状态为失败389            if 'task_id' in locals():390                self.task_manager.update_task(task_id, status="failed", error=error_msg)391            error_msg = f"❌ 错误: {str(e)}\n{traceback.format_exc()}"392            yield log_and_yield(error_msg)393    394    def preview_samples_from_oss(self, access_key_id, access_key_secret, oss_path):395        """396        预览OSS中的样本列表(不下载完整文件)397        398        Args:399            access_key_id: OSS访问密钥ID400            access_key_secret: OSS访问密钥401            oss_path: OSS路径402            403        Returns:404            tuple: (状态消息, 样本列表)405        """406        if not access_key_id or not access_key_secret or not oss_path:407            return "❌ 请填写完整的OSS配置信息", []408        409        try:410            # 创建临时目录411            import tempfile412            temp_dir = Path(tempfile.mkdtemp())413            414            # 下载文件列表(只获取文件名,不下载内容)415            result = self.data_handler.download_from_oss(416                access_key_id,417                access_key_secret, 418                oss_path,419                str(temp_dir)420            )421            422            # 提取样本名423            samples = self.data_handler.extract_sample_names(str(temp_dir))424            425            # 清理临时目录426            shutil.rmtree(temp_dir, ignore_errors=True)427            428            if samples:429                message = f"✅ 找到 {len(samples)} 个样本:\n" + "\n".join(f"  • {s}" for s in samples)430                return message, samples431            else:432                return "⚠️ 未找到有效的样本文件", []433                434        except Exception as e:435            return f"❌ 预览失败: {str(e)}", []436    437    def query_task_status(self, task_id):438        """439        查询任务状态440        441        Args:442            task_id: 任务ID443            444        Returns:445            tuple: (状态消息, 完整包路径, 结果包路径)446        """447        if not task_id or not task_id.strip():448            return "❌ 请输入任务ID", None, None449        450        task_id = task_id.strip()451        452        # 获取任务状态453        status_info = self.task_manager.get_task_status(task_id)454        455        if not status_info["exists"]:456            return status_info["message"], None, None457        458        # 构建状态消息459        message_parts = []460        message_parts.append(f"📋 任务ID: {task_id}")461        message_parts.append(f"📍 状态: {status_info['message']}")462        message_parts.append("")463        464        task = status_info["task"]465        message_parts.append(f"⏰ 创建时间: {task['created_at']}")466        message_parts.append(f"🔄 更新时间: {task['updated_at']}")467        message_parts.append(f"📊 样本总数: {task['total_samples']}")468        message_parts.append(f"📁 项目目录: {task['project_dir']}")469        message_parts.append("")470        471        # 样本进度472        if status_info.get("results_status"):473            message_parts.append("📊 样本进度:")474            for sample, status in status_info["results_status"].items():475                message_parts.append(f"  {status} {sample}")476            message_parts.append("")477        478        # 日志信息(只显示最近的几行)479        if status_info.get("logs"):480            message_parts.append("📝 最新日志:")481            for sample, log in list(status_info["logs"].items())[:3]:  # 只显示前3个样本482                message_parts.append(f"\n--- {sample} ---")483                log_lines = log.split('\n')484                message_parts.append('\n'.join(log_lines[-5:]))  # 每个样本最后5行485            message_parts.append("")486        487        # 下载文件488        full_zip = status_info.get("full_zip_path")489        results_zip = status_info.get("results_zip_path")490        491        if full_zip or results_zip:492            message_parts.append("📥 可下载文件:")493            if full_zip:494                message_parts.append("  ✅ 完整项目包")495            if results_zip:496                message_parts.append("  ✅ REDItools结果包")497        else:498            message_parts.append("⚠️ 下载文件尚未生成或已被删除")499        500        return "\n".join(message_parts), full_zip, results_zip501 502def create_interface():503    """创建Gradio界面"""504    app = NGSWebApp()505    506    # 自定义CSS样式和JavaScript507    custom_css = """508    .gradio-container {509        font-family: 'Microsoft YaHei', Arial, sans-serif !important;510    }511    .main-header {512        text-align: center;513        background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);514        padding: 2rem;515        border-radius: 10px;516        color: white;517        margin-bottom: 2rem;518    }519    .tab-nav button {520        font-size: 1.1rem !important;521        font-weight: 600 !important;522    }523    .download-box {524        background: #f0f9ff;525        border: 2px solid #0ea5e9;526        border-radius: 8px;527        padding: 1rem;528        margin-top: 1rem;529    }530    """531    532    with gr.Blocks(title="NGS测序数据分析平台", css=custom_css, theme=gr.themes.Soft()) as interface:533        534        # 顶部标题区域535        with gr.Row():536            gr.HTML("""537                <div class="main-header">538                    <h1 style="margin:0; font-size: 2.5rem;">🧬 NGS 测序数据分析平台</h1>539                    <p style="margin-top: 0.5rem; font-size: 1.1rem; opacity: 0.9;">540                        RNA编辑位点检测 | REDItools 2.0 | 支持批量分析 | 任务追踪541                    </p>542                </div>543            """)544        545        # 使用 Tabs 分离新建任务和查询任务546        with gr.Tabs():547            # Tab 1: 新建分析任务548            with gr.Tab("🚀 新建分析任务"):549                550                # 数据上传部分551                gr.Markdown("## 步骤 1: 选择数据来源")552                553                upload_method = gr.Radio(554                    choices=["直接上传", "OSS下载(金唯智)"],555                    label="数据上传方式",556                    value="直接上传",557                    info="支持本地上传测序文件或从金唯智OSS下载"558                )559                560                # 直接上传区域561                with gr.Group(visible=True) as upload_group:562                    gr.Markdown("#### 📤 上传测序文件")563                    uploaded_files = gr.File(564                        label="拖拽或点击上传双端测序文件 (*_1.fq.gz 和 *_2.fq.gz)",565                        file_count="multiple",566                        file_types=[".fq.gz", ".fastq.gz"],567                        height=200568                    )569                    gr.Markdown("💡 *提示: 可以一次性上传多个样本的测序文件*")570                571                # OSS下载区域572                with gr.Group(visible=False) as oss_group:573                    gr.Markdown("#### 🔐 OSS配置 (金唯智)")574                    with gr.Row():575                        access_key_id = gr.Textbox(576                            label="Access Key ID",577                            placeholder="输入您的 Access Key ID",578                            scale=1579                        )580                        access_key_secret = gr.Textbox(581                            label="Access Key Secret",582                            placeholder="输入您的 Access Key Secret",583                            type="password",584                            scale=1585                        )586                    oss_path = gr.Textbox(587                        label="OSS路径",588                        placeholder="例如: oss://ngscustomerdata/2025.11/80-2013656037/...",589                        lines=2590                    )591                    592                    gr.Markdown("""593                    #### 📋 样本选择说明594                    595                    💡 **操作流程**:596                    1. 填写OSS配置后,点击"开始分析"按钮597                    2. 系统会先下载所有样本文件598                    3. 下载完成后,在日志中会显示所有样本名599                    4. **暂停执行** - 在继续分析前,您可以手动记录样本名600                    601                    ⚠️ **注意**: 目前所有下载的样本都会被分析。如需选择特定样本,请在下载后联系管理员。602                    """)603                604                gr.Markdown("---")605                606                # 参考序列配置部分607                gr.Markdown("## 步骤 2: 配置参考序列")608                609                use_multiple_refs = gr.Checkbox(610                    label="使用多个参考序列 (不同样本使用不同参考)",611                    value=False,612                    info="勾选后可以为不同样本指定不同的参考序列"613                )614                615                # 单个参考序列616                with gr.Group(visible=True) as single_ref_group:617                    gr.Markdown("#### 📝 单一参考序列 (应用于所有样本)")618                    ref_sequence_single = gr.Textbox(619                        label="参考序列 (FASTA格式)",620                        placeholder=">reference\nATCGATCGATCG...",621                        lines=10622                    )623                    target_sites = gr.Textbox(624                        label="靶位点 (可选)",625                        placeholder="输入靶位点信息",626                        lines=3627                    )628                629                # 多个参考序列630                with gr.Group(visible=False) as multiple_ref_group:631                    gr.Markdown("#### 📚 多个参考序列配置")632                    gr.Markdown("为不同的样本指定不同的参考序列")633                    634                    num_refs = gr.Slider(635                        minimum=2,636                        maximum=10,637                        step=1,638                        value=2,639                        label="参考序列数量",640                        interactive=True,641                        info="拖动滑块选择需要配置的参考序列数量"642                    )643                    644                    # 动态生成的参考序列输入框容器645                    ref_inputs_container = gr.Column()646                    647                    # 预创建10组输入框(根据slider显示/隐藏)648                    ref_name_inputs = []649                    ref_sample_inputs = []650                    ref_seq_inputs = []651                    ref_groups = []652                    653                    with ref_inputs_container:654                        for i in range(10):655                            with gr.Group(visible=(i < 2)) as ref_group:656                                with gr.Accordion(f"参考序列 {i+1}", open=(i < 2)):657                                    ref_name = gr.Textbox(658                                        label="参考序列名称",659                                        placeholder=f"例如: ref{i+1}",660                                        value=f"ref{i+1}"661                                    )662                                    ref_sample = gr.Textbox(663                                        label="对应样本名(不带_1.fq.gz或_2.fq.gz)",664                                        placeholder="例如: sample1, sample2 (用逗号分隔多个样本)"665                                    )666                                    ref_seq = gr.Textbox(667                                        label="参考序列 (FASTA格式)",668                                        placeholder=">reference\nATCGATCG...",669                                        lines=6670                                    )671                                ref_name_inputs.append(ref_name)672                                ref_sample_inputs.append(ref_sample)673                                ref_seq_inputs.append(ref_seq)674                                ref_groups.append(ref_group)675                676                gr.Markdown("---")677                678                # 执行按钮679                gr.Markdown("## 步骤 3: 开始分析")680                gr.Markdown("配置完成后,点击下方按钮开始分析")681                682                with gr.Row():683                    submit_btn = gr.Button("🚀 开始分析", variant="primary", size="lg", scale=2)684                685                gr.Markdown("---")686                687                # 实时执行日志688                gr.Markdown("## 📊 分析进度与结果")689                gr.Markdown("### 实时执行日志")690                691                output = gr.Textbox(692                    label="实时日志输出",693                    lines=20,694                    max_lines=50,695                    show_copy_button=True696                )697                698                gr.Markdown("### 📥 下载结果")699                700                with gr.Row():701                    with gr.Column(scale=1):702                        gr.Markdown("""703                        <div class="download-box">704                            <h4>📦 完整项目包</h4>705                            <p>包含所有分析文件和中间结果</p>706                        </div>707                        """)708                        full_download = gr.File(709                            label="下载完整项目",710                            interactive=False711                        )712                    713                    with gr.Column(scale=1):714                        gr.Markdown("""715                        <div class="download-box">716                            <h4>🎯 仅编辑结果</h4>717                            <p>仅包含REDItools分析结果文件(05-reditools2)</p>718                        </div>719                        """)720                        results_download = gr.File(721                            label="下载编辑结果",722                            interactive=False723                        )724                725                # 事件处理726                def toggle_upload_method(method):727                    if method == "直接上传":728                        return gr.update(visible=True), gr.update(visible=False)729                    else:730                        return gr.update(visible=False), gr.update(visible=True)731                732                def toggle_ref_mode(use_multiple):733                    if use_multiple:734                        return gr.update(visible=False), gr.update(visible=True)735                    else:736                        return gr.update(visible=True), gr.update(visible=False)737                738                def update_ref_inputs_visibility(num):739                    """根据选择的数量更新输入框可见性"""740                    num = int(num)741                    updates = []742                    for i in range(10):743                        updates.append(gr.update(visible=(i < num)))744                    return updates745                746                # 包装process_pipeline函数以接收动态参数747                def process_with_dynamic_refs(748                    upload_method,749                    access_key_id,750                    access_key_secret,751                    oss_path,752                    uploaded_files,753                    use_multiple_refs,754                    ref_sequence_single,755                    target_sites,756                    num_refs,757                    *ref_inputs  # 动态参数: ref_name_1, ref_sample_1, ref_seq_1, ...758                ):759                    # 将动态输入转换为ref_configs格式760                    ref_configs = []761                    if use_multiple_refs:762                        num_refs = int(num_refs)763                        # 每3个参数为一组 (name, sample, seq)764                        for i in range(num_refs):765                            idx = i * 3766                            if idx + 2 < len(ref_inputs):767                                ref_name = ref_inputs[idx]768                                ref_sample = ref_inputs[idx + 1]769                                ref_seq = ref_inputs[idx + 2]770                                if ref_name and ref_sample and ref_seq:771                                    ref_configs.append([ref_name, ref_sample, ref_seq])772                    773                    # 使用 yield from 来转发生成器的输出774                    # OSS方式不支持样本选择,传入None775                    yield from app.process_pipeline(776                        upload_method,777                        access_key_id,778                        access_key_secret,779                        oss_path,780                        None,  # selected_samples - OSS方式下不支持781                        uploaded_files,782                        use_multiple_refs,783                        ref_sequence_single,784                        target_sites,785                        ref_configs786                    )787                788                upload_method.change(789                    fn=toggle_upload_method,790                    inputs=[upload_method],791                    outputs=[upload_group, oss_group]792                )793                794                use_multiple_refs.change(795                    fn=toggle_ref_mode,796                    inputs=[use_multiple_refs],797                    outputs=[single_ref_group, multiple_ref_group]798                )799                800                # 数量滑块改变时更新输入框可见性801                num_refs.change(802                    fn=update_ref_inputs_visibility,803                    inputs=[num_refs],804                    outputs=ref_groups805                )806                807                # 准备所有输入参数808                all_ref_inputs = []809                for i in range(10):810                    all_ref_inputs.extend([ref_name_inputs[i], ref_sample_inputs[i], ref_seq_inputs[i]])811                812                # 点击开始分析直接执行813                submit_btn.click(814                    fn=process_with_dynamic_refs,815                    inputs=[816                        upload_method,817                        access_key_id,818                        access_key_secret,819                        oss_path,820                        uploaded_files,821                        use_multiple_refs,822                        ref_sequence_single,823                        target_sites,824                        num_refs,825                        *all_ref_inputs826                    ],827                    outputs=[output, full_download, results_download]828                )829 830            # Tab 2: 查询任务状态831            with gr.Tab("🔍 查询任务状态"):832                gr.Markdown("""833                ## 查询任务进度和结果834                835                输入任务ID查询分析进度、下载结果文件。836                837                💡 **提示**: 任务ID格式为时间戳,例如 `20251113_125002`838                """)839                840                with gr.Row():841                    with gr.Column(scale=3):842                        query_task_id = gr.Textbox(843                            label="任务ID",844                            placeholder="输入任务ID,例如: 20251113_125002",845                            lines=1846                        )847                    with gr.Column(scale=1):848                        query_btn = gr.Button("🔍 查询任务", variant="primary", size="lg")849                850                gr.Markdown("---")851                852                query_output = gr.Textbox(853                    label="任务状态",854                    lines=20,855                    max_lines=50,856                    show_copy_button=True857                )858                859                gr.Markdown("### 📥 下载结果文件")860                861                with gr.Row():862                    with gr.Column(scale=1):863                        gr.Markdown("""864                        <div class="download-box">865                            <h4>📦 完整项目包</h4>866                            <p>包含所有分析文件和中间结果</p>867                        </div>868                        """)869                        query_full_download = gr.File(870                            label="下载完整项目",871                            interactive=False872                        )873                    874                    with gr.Column(scale=1):875                        gr.Markdown("""876                        <div class="download-box">877                            <h4>🎯 仅编辑结果</h4>878                            <p>仅包含REDItools分析结果文件</p>879                        </div>880                        """)881                        query_results_download = gr.File(882                            label="下载编辑结果",883                            interactive=False884                        )885                886                # 查询按钮事件887                query_btn.click(888                    fn=app.query_task_status,889                    inputs=[query_task_id],890                    outputs=[query_output, query_full_download, query_results_download]891                )892    893    return interface894 895if __name__ == "__main__":896    interface = create_interface()897    interface.launch(server_name="0.0.0.0", server_port=7860)898