RenJW/RNAediting
0
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 