From d3fde1cbb8af5ad87ac7f1583b0cd0bb96886d75 Mon Sep 17 00:00:00 2001 From: kai Date: Wed, 22 Apr 2026 13:12:01 +0800 Subject: [PATCH] v0.6-wip: build_report \u7edf\u4e00\u5165\u53e3 + build_glossary \u672f\u8bed\u6838\u67e5 \u7ee7\u7eed\u89e3\u51b3\u7528\u6237\u53cd\u9988\u7684 PDF \u95ee\u9898\u3002 scripts/build_report.py\uff08\u65b0\u589e\uff09\uff1a - \u5355\u4e00\u5165\u53e3\u540c\u65f6\u51fa PDF + DOCX - \u6587\u4ef6\u540d\u81ea\u52a8\u4ece manifest.report_title \u751f\u6210\uff08\u89e3\u51b3 "final.pdf" \u6CDB\u540d\u95EE\u9898\uff09 - pandoc --from=markdown-tex_math_dollars \u4fee\u590d DOCX \u751f\u6210\u65f6\u7684 $ \u8bef\u89e3 - \u81ea\u52a8\u5bfb\u627e phase2/sources.jsonl \u4f5c\u4e3a\u53c2\u8003\u6587\u732e\u5f15\u6587\u6e90 scripts/lib/search_client.py\uff08\u65b0\u589e\uff09\uff1a - Exa \u4e3b\u529b + Tavily fallback \u7684\u7edf\u4e00\u63a5\u53e3 - \u5173\u952e\u4fee\u590d\uff1atrust_env=False \u7ed5\u5f00\u7cfb\u7edf socks5 \u4ee3\u7406 \uff08Clash on macOS \u5c0a httpx TLS \u63e1\u624b\u5728 CONNECT \u540e EOF\uff09 scripts/build_glossary.py\uff08\u65b0\u589e\uff09\uff1a - \u7528\u7684\u4e92\u65b9\u5f0f\u89e3\u51b3\u4e86\u7528\u6237\u53cd\u9988 #6\uff1a\u672f\u8bed\u7ffb\u8bd1\u4e0d\u4e13\u4e1a / \u4e8b\u5b9e\u9519\u8bef - ThreadPoolExecutor \u5e76\u53d1\uff08\u9ed8\u8ba4 6 worker\uff09\uff0c\u6bcf\u4e2a\u672f\u8bed\u72ec\u7acb\uff1a Search \u2192 Top-3 snippet \u2192 Haiku \u5224\u5b9a \u2192 \u8fd4\u56de {zh, en_full, confidence, issue} - \u5b9e\u6d4b\u6210\u529f\u8bc6\u522b "Maywavee" \u4e3a "Mabwell" \u7684\u62fc\u5199\u9519\u8bef\u5e76\u6807\u51fa issue - \u65ad\u70b9\u7eed\u4f20\uff08\u5df2\u6807 verified_at \u7684\u9ed8\u8ba4\u8df3\u8fc7\uff09 - Haiku \u6210\u672c\u6781\u4f4e\uff083 \u4e2a\u672f\u8bed\u8c03\u7528 \u2248 0.01 \u7f8e\u5206\uff09 - \u652f\u6301 --extra terms.txt \u8865\u5145\u7ffb\u8bd1\u9636\u6bb5\u672a\u6536\u5165\u7684\u672f\u8bed scripts/prompts/glossary_system.txt\uff08\u65b0\u589e\uff09\uff1a - Haiku \u6838\u67e5\u672f\u8bed\u7684 prompt\uff0c\u660e\u786e\u5224\u5b9a\u7ef4\u5ea6\u548c JSON \u8f93\u51fa\u683c\u5f0f Co-authored-by: User --- .../phase4/glossary.json | 1267 +++++++++++++---- .../phase4/logs/glossary.jsonl | 3 + scripts/build_glossary.py | 332 +++++ scripts/build_report.py | 238 ++++ scripts/lib/search_client.py | 177 +++ scripts/prompts/glossary_system.txt | 54 + 6 files changed, 1761 insertions(+), 310 deletions(-) create mode 100644 projects/dual-target-rnai-pipeline-2026/phase4/logs/glossary.jsonl create mode 100644 scripts/build_glossary.py create mode 100644 scripts/build_report.py create mode 100644 scripts/lib/search_client.py create mode 100644 scripts/prompts/glossary_system.txt diff --git a/projects/dual-target-rnai-pipeline-2026/phase4/glossary.json b/projects/dual-target-rnai-pipeline-2026/phase4/glossary.json index 00b6bb1..2837741 100644 --- a/projects/dual-target-rnai-pipeline-2026/phase4/glossary.json +++ b/projects/dual-target-rnai-pipeline-2026/phase4/glossary.json @@ -1,312 +1,959 @@ { - "2'-F-CTP": "2'-F-CTP", - "2'-F-UTP": "2'-F-UTP", - "2'-OMe-ATP": "2'-OMe-ATP", - "2'-OMe-UTP": "2'-OMe-UTP", - "2'-acetal levulinic ester (ALE) phosphoramidite": "2'-缩醛乙酰丙酸酯(ALE)亚磷酰胺", - "32P-end-labeling": "32P末端标记", - "500 Å pore": "500 Å孔径", - "AGT": "AGT", - "AJIPHASE®": "AJIPHASE®", - "ANGPTL3": "ANGPTL3", - "AOF formulation": "无动物及人源成分(AOF)配方", - "API sameness": "API同一性", - "APOC3": "APOC3", - "ASCVD": "动脉粥样硬化性心血管疾病", - "ASGPR": "去唾液酸糖蛋白受体", - "ASMF": "ASMF备案", - "Adaptive Laboratory Evolution": "适应性实验室进化", - "Ajinomoto Bio-Pharma Services": "味之素生物制药服务", - "Ajinomoto OmniChem": "Ajinomoto OmniChem", - "Amide linker": "酰胺接头", - "Angiotensinogen": "血管紧张素原", - "Antibody-Drug Conjugate": "抗体偶联药物", - "BD value": "BD交易价值", - "BIOSECURE Act": "《生物安全法案》", - "BTT activation": "BTT活化", - "Bachem": "Bachem", - "BeBetter Med": "必贝特", - "Beyotime": "碧云天", - "Biologics License Application": "生物制品上市许可申请", - "Biomaide": "博迈德", - "BirA/AviTag": "BirA/AviTag", - "Bis-RNAi": "Bis-RNAi", - "C5": "C5", - "CDE": "药品审评中心", - "CDMO": "合同开发与生产组织", - "CID fragmentation": "碰撞诱导解离(CID)碎裂", - "CLEA-LentiKats lipase formulation": "CLEA-LentiKats脂肪酶制剂", - "CMC development timeline": "CMC开发周期", - "CPG support": "CPG载体", - "CTA": "临床试验申请", - "Calantha™": "Calantha™", - "Calf Intestinal Alkaline Phosphatase": "小牛肠碱性磷酸酶", - "Candida antarctica lipase B": "南极假丝酵母脂肪酶B", - "Category I": "第I类", - "Category II": "第II类", - "Category III": "第III类", - "Category IV": "第IV类", - "ChemGenes": "ChemGenes", - "Chinese IND": "中国IND", - "Class 3": "第3类", - "Complement Factor B": "补体因子B", - "Complete Response Letter": "完全答复函", - "Controlled pore glass": "受控孔径玻璃", - "Cross-Linked Enzyme Aggregates": "交联酶聚集体", - "CuAAC": "铜催化叠氮-炔烃环加成", - "D-galactosamine": "D-半乳糖胺", - "DBCO": "二苯并环辛炔(DBCO)", - "DMF": "DMF备案", - "DMT-on HPLC purification handle": "DMT-on HPLC纯化把手", - "DNase I": "DNase I", - "Deep Eutectic Solvent": "深共熔溶剂", - "Dibenzocyclooctyne": "二苯并环辛炔", - "Dual-Target RNAi Drug": "双靶点RNAi药物", - "ECO GMP Manufacturing Center": "ECO GMP生产中心", - "ECO Synthesis platform": "ECO Synthesis平台", - "ECO ligase": "ECO连接酶", - "FDA/CDER": "FDA/CDER", - "Factor XI": "凝血因子XI", - "Fall Line Endurance Fund": "Fall Line Endurance Fund", - "Full-Length Product": "全长产物", - "G5 GalNAc support": "G5 GalNAc载体", - "G5 ribofuranose": "G5核糖呋喃糖", - "GDOC": "GDOC平台", - "GDOC platform": "GDOC平台", - "GEMINI™": "GEMINI™平台", - "GMP-grade QC enzyme panel": "GMP级质控酶组合", - "GT-multi-siRNA": "GT-multi-siRNA", - "GalAhead": "GalAhead™", - "GalNAc-PA": "GalNAc-亚磷酰胺(GalNAc-PA)", - "GalNAc-siRNA": "GalNAc-siRNA", - "GalT": "GalT", - "GnTI": "GnTI", - "GreenLight Biosciences": "GreenLight Biosciences", - "H1 subunit": "H1亚基", - "H2 subunit": "H2亚基", - "Hongene": "兆维", - "Host-Cell Protein": "宿主细胞蛋白", - "Huntingtin": "亨廷顿蛋白", - "Hydroxyprolinol scaffold": "羟脯氨醇(tHP)骨架", - "ICH Q11 Q&A": "ICH Q11问答", - "ICH Q13": "ICH Q13", - "ICH Q2(R1)": "ICH Q2(R1)", - "ICH Q3D(R2)": "ICH Q3D(R2)", - "ICP-MS": "ICP-MS", - "IND-to-dosing gap": "IND获批与首次给药时间差", - "IVT": "体外转录", - "Just Climate": "Just Climate", - "Kd": "Kd值", - "Kinovate Life Sciences": "Kinovate Life Sciences", - "L96 ligand": "L96配体", - "LDL-C": "LDL-C", - "LGC Biosearch Technologies": "LGC Biosearch Technologies", - "Leloir glycosyltransferases": "Leloir糖基转移酶", - "Locked Nucleic Acid": "锁核酸", - "Lp(a)": "Lp(a)", - "ManII": "ManII", - "Maywavee": "迈威生物", - "MutS Homolog 3": "MutS同源物3", - "N-acetylgalactosamine": "N-乙酰半乳糖胺", - "NAG37": "NAG37", - "NDA": "新药申请", - "NMPA": "国家药品监督管理局", - "Nitto Denko Avecia": "Nitto Denko Avecia", - "NittoPhase HL": "NittoPhase HL", - "Norroa": "Norroa", - "Notice No. 21 of 2026": "2026年第21号通告", - "Nuclease P1": "核酸酶P1", - "Nucleoside Triphosphate": "核苷三磷酸", - "OPR&D": "《有机工艺研究与开发》(OPR&D)", - "PCSK9": "PCSK9", - "PDE": "允许日暴露量", - "PMO synthesis": "PMO合成", - "PVA entrapment": "PVA包埋", - "Phosphodiester linker": "磷酸二酯接头", - "Polynucleotide Kinase (T4)": "多核苷酸激酶(T4)", - "Poresyn Solutions": "Poresyn Solutions", - "Prime Synthesis CPG": "Prime Synthesis CPG", - "PrimeMax siRNA CPG": "PrimeMax siRNA CPG", - "Process Analytical Technology": "过程分析技术", - "Process Atlas": "工艺图谱", - "Process Mass Intensity": "工艺质量强度", - "Q3M": "Q3M", - "Q6M": "Q6M", - "QC biocatalyst": "质控生物催化剂", - "RADS™": "RADS™", - "RNA interference": "RNA干扰", - "RNA-Induced Silencing Complex": "RNA诱导沉默复合体", - "RNase A": "核糖核酸酶A", - "RNase H": "核糖核酸酶H", - "RNase Inhibitor": "RNase Inhibitor", - "RNase T1": "核糖核酸酶T1", - "RSC 2.0 modification": "RSC 2.0修饰", - "RiboGalSTAR™": "RiboGalSTAR™", - "Ribonuclease T1": "核糖核酸酶T1", - "Roche Custom Biotech": "罗氏定制生物技术", - "SBIA": "SBIA", - "SBS": "SBS", - "SMART Digest RNase T1 kit": "SMART Digest RNase T1试剂盒", - "SPOS": "固相合成(SPOS)", - "ST Pharm": "ST Pharm", - "STP122G": "STP122G", - "SUGAR-TARGET glycosyl-transferase cascade": "SUGAR-TARGET糖基转移酶级联反应", - "Sangon Biotech": "生工", - "SiaT": "SiaT", - "Snake Venom Phosphodiesterase": "蛇毒磷酸二酯酶", - "Sollbruchstellen": "易断位点", - "SpyCatcher/SpyTag": "SpyCatcher/SpyTag", - "Strain-Promoted Azide–Alkyne Cycloaddition": "应变促进叠氮–炔烃环加成", - "Supply-Chain Opportunity Map": "供应链机会地图", - "Suzhou Taike": "苏州泰科", - "Synthesis Modality": "合成模式", - "T4 RNA Ligase 1": "T4 RNA连接酶1", - "T4 RNA Ligase 2": "T4 RNA连接酶2", - "T7 RNA polymerase": "T7 RNA聚合酶", - "TRL": "技术成熟度", - "TRiM™": "TRiM™平台", - "Takara Bio": "宝生物工程", - "Technical Guidelines for Pharmaceutical Research on Chemically Synthesized Oligonucleotide Drugs (Innovative Drugs)": "化学合成寡核苷酸药物(创新药)药学研究技术指导原则(试行)", - "Terminal Deoxynucleotidyl Transferase": "末端脱氧核苷酸转移酶", - "Tianjin Orilife": "天津奥利法", - "Tier 1": "第一层级", - "Tier 2": "第二层级", - "Tier 3": "第三层级", - "Triazole linker": "三唑接头", - "TrisGal-6 scaffold": "TrisGal-6骨架", - "UDP-Gal": "UDP-Gal", - "UDP-GalNAc": "UDP-GalNAc", - "Unylinker-functionalized polystyrene support": "Unylinker功能化聚苯乙烯载体", - "Vazyme": "诺唯赞", - "Worthington Biochemical": "Worthington Biochemical", - "WuXi AppTec": "药明康德", - "Wuhu Huaren": "芜湖华仁", - "XBP-1": "XBP-1", - "Yeasen Biotech": "翌圣生物科技", - "acceptable daily intake": "每日可接受摄入量", - "activated-intermediate shelf life": "活化中间体货架期", - "anion-exchange polishing": "阴离子交换精制", - "antisolvent": "反溶剂", - "arm-coupling reaction": "臂偶联反应", - "atom-economy penalty": "原子经济性损失", - "avidity plateau": "亲合力平台", - "bead attrition": "微珠磨损", - "bottom-up mapping": "自下而上图谱分析", - "branching-point coupling": "分支点偶联", - "bundled enzyme-carrier product": "酶-载体捆绑产品", - "cardiometabolic disease": "心脏代谢疾病", - "cell-free synthesis": "无细胞合成", - "channeling": "沟流", - "charge-state deconvolution": "电荷态反卷积", - "chemoenzymatic": "化学酶法", - "choline chloride/glycerol": "氯化胆碱/甘油", - "choline chloride/urea (reline)": "氯化胆碱/尿素(reline)", - "class extension": "品类延伸", - "clinical translation": "临床转化", - "cluster-arm variants": "簇臂变体", - "co-dosing program": "联合给药项目", - "cocktail formulation": "鸡尾酒制剂", - "complement dysregulation": "补体失调", - "continuous process verification": "连续工艺验证", - "continuous-flow microgel reactor": "连续流微凝胶反应器", - "control threshold": "控制阈值", - "convergent assembly": "汇聚式组装", - "copper scavenging": "铜清除", - "copper-residue burden": "铜残留合规负担", - "coronary heart disease": "冠心病", - "covalently-linked tandem siRNA": "共价连接串联siRNA", - "cross-activity": "交叉活性", - "denaturing IP-RP-LC-MS": "变性离子对反相液相色谱-质谱", - "dendritic scaffold": "树枝状骨架", - "design paradigm": "设计范式", - "di-valent branched construct": "二价分支构建体", - "diamine scaffold": "二胺骨架", - "disulfide bond": "二硫键", - "domestic-substitution feasibility": "国产替代可行性", - "droplet microfluidics": "液滴微流控技术", - "dsRNA": "dsRNA", - "dual-functional RNAi therapeutic": "双功能RNAi治疗药物", - "enantiomeric excess": "对映体过量值", - "enzymatic-ligation": "酶连接", - "enzyme loading per gram": "每克载体酶载量", - "fixed-bed copper-scavenging resin": "固定床铜清除树脂", - "glutaraldehyde": "戊二醛", - "glycosyl-transferase": "糖基转移酶", - "hepatic parenchymal clearance": "肝实质清除率", - "hetero-duplex": "异源双链体", - "hexaethyleneglycol": "六乙二醇", - "hexavalent GalNAc cluster": "六价GalNAc簇", - "high-load polymeric support": "高载量聚合物载体", - "homo-duplex": "同源双链体", - "hypertriglyceridemia": "高甘油三酯血症", - "immobilized enzyme": "固定化酶", - "immobilized polymerase/phosphatase reactor": "固定化聚合酶/磷酸酶反应器", - "inline conversion monitoring": "在线转化率监测", - "inter-lot specific activity variation": "批间比活力变异", - "junction-verification assay": "连接位点验证检测", - "lenticular beads": "扁豆形微珠", - "lipid nanoparticle": "脂质纳米颗粒", - "liquid-phase oligonucleotide synthesis": "液相寡核苷酸合成", - "liquid-phase synthesis": "液相合成", - "mRNAi GOLD™": "mRNAi GOLD™", - "maleimide-activated agarose": "马来酰亚胺活化琼脂糖", - "manufacturing stack": "制造体系", - "material diversion strategy": "物料转移策略", - "membrane bioreactor": "膜生物反应器", - "methacrylate copolymer beads": "甲基丙烯酸酯共聚物微珠", - "mixed hyperlipidemia": "混合型高脂血症", - "monoantennary GalNAc": "单天线GalNAc", - "monomer diversity index": "单体多样性指标", - "muRNA": "muRNA", - "multivalent GalNAc cluster": "多价GalNAc簇", - "multivalent GalNAc scaffold": "多价GalNAc骨架", - "murine TdT": "鼠源TdT", - "mxRNA": "mxRNA", - "neurodegeneration": "神经退行性疾病", - "nonclinical guidance": "非临床指南", - "nuclease mapping": "核酸酶图谱分析", - "packed-bed reactor": "填充床反应器", - "partially conjugated strand": "部分偶联链", - "particle-integrity monitoring": "颗粒完整性监测", - "pentavalent GalNAc": "五价GalNAc", - "phosphoramidite monomer": "亚磷酰胺单体", - "pipeline inflation": "管线虚胖", - "platform-multiplication event": "平台倍增事件", - "polymer microgels": "聚合物微凝胶", - "pre-validated package": "预验证文件包", - "pressure-drop effects": "压降效应", - "process debt": "工艺债务", - "process node": "工艺节点", - "process signature": "工艺特征", - "product-specific guidance": "产品专项指南", - "protecting-group strategy": "保护基策略", - "protective allele": "保护性等位基因", - "pyranose": "吡喃糖", - "qualification barrier": "资质壁垒", - "rSAP": "rSAP", - "ratio QC": "比例质控", - "receptor-mediated uptake": "受体内吞", - "regulatory vector": "监管向量", - "release-profile QC": "释放谱质控", - "residual enzyme surveillance": "残留酶监控", - "ribofuranose scaffold": "核糖呋喃糖骨架", - "serially assembled trivalent construct": "序贯组装三价构建体", - "siRELIS facility": "siRELIS工厂", - "silica bead fines": "硅胶微珠细粉", - "solid-phase phosphoramidite synthesis": "固相亚磷酰胺合成", - "solid-phase synthesis": "固相合成", - "soluble anchor": "可溶性锚定基团", - "spacer accessibility": "间隔臂可及性", - "splint removal": "DNA夹板去除", - "streptavidin-coated silica beads": "链霉亲和素包被硅胶微珠", - "structure-activity study": "构效关系研究", - "sub-saturation binding regime": "未饱和结合区间", - "supplier concentration": "供应商集中度", - "swelling index": "溶胀指数", - "tetraantennary": "四天线", - "tetraloop": "四环体", - "top-down intact-mass sequencing": "自上而下完整质量测序", - "toxicokinetic subfactor justification": "毒代动力学亚因子论证", - "triantennary GalNAc": "三天线GalNAc", - "truncated cluster impurity": "截短型簇杂质" + "2'-F-CTP": { + "zh": "2'-F-CTP" + }, + "2'-F-UTP": { + "zh": "2'-F-UTP" + }, + "2'-OMe-ATP": { + "zh": "2'-OMe-ATP" + }, + "2'-OMe-UTP": { + "zh": "2'-OMe-UTP" + }, + "2'-acetal levulinic ester (ALE) phosphoramidite": { + "zh": "2'-缩醛乙酰丙酸酯(ALE)亚磷酰胺" + }, + "32P-end-labeling": { + "zh": "32P末端标记" + }, + "500 Å pore": { + "zh": "500 Å孔径" + }, + "AGT": { + "zh": "AGT" + }, + "AJIPHASE®": { + "zh": "AJIPHASE®" + }, + "ANGPTL3": { + "zh": "ANGPTL3" + }, + "AOF formulation": { + "zh": "无动物及人源成分(AOF)配方" + }, + "API sameness": { + "zh": "API同一性" + }, + "APOC3": { + "zh": "APOC3" + }, + "ASCVD": { + "zh": "动脉粥样硬化性心血管疾病" + }, + "ASGPR": { + "zh": "去唾液酸糖蛋白受体" + }, + "ASMF": { + "zh": "ASMF备案" + }, + "Adaptive Laboratory Evolution": { + "zh": "适应性实验室进化" + }, + "Ajinomoto Bio-Pharma Services": { + "zh": "味之素生物制药服务" + }, + "Ajinomoto OmniChem": { + "zh": "Ajinomoto OmniChem" + }, + "Amide linker": { + "zh": "酰胺接头" + }, + "Angiotensinogen": { + "zh": "血管紧张素原" + }, + "Antibody-Drug Conjugate": { + "zh": "抗体偶联药物" + }, + "BD value": { + "zh": "BD交易价值" + }, + "BIOSECURE Act": { + "zh": "《生物安全法案》" + }, + "BTT activation": { + "zh": "BTT活化" + }, + "Bachem": { + "zh": "Bachem" + }, + "BeBetter Med": { + "zh": "必贝特" + }, + "Beyotime": { + "zh": "碧云天" + }, + "Biologics License Application": { + "zh": "生物制品上市许可申请" + }, + "Biomaide": { + "zh": "博迈德" + }, + "BirA/AviTag": { + "zh": "BirA/AviTag" + }, + "Bis-RNAi": { + "zh": "Bis-RNAi" + }, + "C5": { + "zh": "C5" + }, + "CDE": { + "zh": "药品审评中心" + }, + "CDMO": { + "zh": "合同开发与生产组织" + }, + "CID fragmentation": { + "zh": "碰撞诱导解离(CID)碎裂" + }, + "CLEA-LentiKats lipase formulation": { + "zh": "CLEA-LentiKats脂肪酶制剂" + }, + "CMC development timeline": { + "zh": "CMC开发周期" + }, + "CPG support": { + "zh": "CPG载体" + }, + "CTA": { + "zh": "临床试验申请" + }, + "Calantha™": { + "zh": "Calantha™" + }, + "Calf Intestinal Alkaline Phosphatase": { + "zh": "小牛肠碱性磷酸酶" + }, + "Candida antarctica lipase B": { + "zh": "南极假丝酵母脂肪酶B" + }, + "Category I": { + "zh": "第I类" + }, + "Category II": { + "zh": "第II类" + }, + "Category III": { + "zh": "第III类" + }, + "Category IV": { + "zh": "第IV类" + }, + "ChemGenes": { + "zh": "ChemGenes" + }, + "Chinese IND": { + "zh": "中国IND" + }, + "Class 3": { + "zh": "第3类" + }, + "Complement Factor B": { + "zh": "补体因子B" + }, + "Complete Response Letter": { + "zh": "完全答复函" + }, + "Controlled pore glass": { + "zh": "受控孔径玻璃" + }, + "Cross-Linked Enzyme Aggregates": { + "zh": "交联酶聚集体" + }, + "CuAAC": { + "zh": "铜催化叠氮-炔烃环加成" + }, + "D-galactosamine": { + "zh": "D-半乳糖胺" + }, + "DBCO": { + "zh": "二苯并环辛炔(DBCO)" + }, + "DMF": { + "zh": "DMF备案" + }, + "DMT-on HPLC purification handle": { + "zh": "DMT-on HPLC纯化把手" + }, + "DNase I": { + "zh": "DNase I" + }, + "Deep Eutectic Solvent": { + "zh": "深共熔溶剂" + }, + "Dibenzocyclooctyne": { + "zh": "二苯并环辛炔" + }, + "Dual-Target RNAi Drug": { + "zh": "双靶点RNAi药物" + }, + "ECO GMP Manufacturing Center": { + "zh": "ECO GMP生产中心" + }, + "ECO Synthesis platform": { + "zh": "ECO Synthesis平台" + }, + "ECO ligase": { + "zh": "ECO连接酶" + }, + "FDA/CDER": { + "zh": "FDA/CDER" + }, + "Factor XI": { + "zh": "凝血因子XI" + }, + "Fall Line Endurance Fund": { + "zh": "Fall Line Endurance Fund" + }, + "Full-Length Product": { + "zh": "全长产物" + }, + "G5 GalNAc support": { + "zh": "G5 GalNAc载体" + }, + "G5 ribofuranose": { + "zh": "G5核糖呋喃糖" + }, + "GDOC": { + "zh": "GDOC平台" + }, + "GDOC platform": { + "zh": "GDOC平台" + }, + "GEMINI™": { + "zh": "GEMINI™平台" + }, + "GMP-grade QC enzyme panel": { + "zh": "GMP级质控酶组合" + }, + "GT-multi-siRNA": { + "zh": "GT-multi-siRNA" + }, + "GalAhead": { + "zh": "GalAhead™" + }, + "GalNAc-PA": { + "zh": "GalNAc-亚磷酰胺(GalNAc-PA)" + }, + "GalNAc-siRNA": { + "zh": "GalNAc-siRNA" + }, + "GalT": { + "zh": "GalT" + }, + "GnTI": { + "zh": "GnTI" + }, + "GreenLight Biosciences": { + "zh": "GreenLight Biosciences" + }, + "H1 subunit": { + "zh": "H1亚基" + }, + "H2 subunit": { + "zh": "H2亚基" + }, + "Hongene": { + "zh": "兆维" + }, + "Host-Cell Protein": { + "zh": "宿主细胞蛋白" + }, + "Huntingtin": { + "zh": "亨廷顿蛋白" + }, + "Hydroxyprolinol scaffold": { + "zh": "羟脯氨醇(tHP)骨架" + }, + "ICH Q11 Q&A": { + "zh": "ICH Q11问答" + }, + "ICH Q13": { + "zh": "ICH Q13" + }, + "ICH Q2(R1)": { + "zh": "ICH Q2(R1)" + }, + "ICH Q3D(R2)": { + "zh": "ICH Q3D(R2)" + }, + "ICP-MS": { + "zh": "ICP-MS" + }, + "IND-to-dosing gap": { + "zh": "IND获批与首次给药时间差" + }, + "IVT": { + "zh": "体外转录" + }, + "Just Climate": { + "zh": "Just Climate" + }, + "Kd": { + "zh": "Kd值" + }, + "Kinovate Life Sciences": { + "zh": "Kinovate Life Sciences" + }, + "L96 ligand": { + "zh": "L96配体" + }, + "LDL-C": { + "zh": "LDL-C" + }, + "LGC Biosearch Technologies": { + "zh": "LGC Biosearch Technologies" + }, + "Leloir glycosyltransferases": { + "zh": "Leloir糖基转移酶" + }, + "Locked Nucleic Acid": { + "zh": "锁核酸" + }, + "Lp(a)": { + "zh": "Lp(a)" + }, + "ManII": { + "zh": "ManII" + }, + "Maywavee": { + "confidence": "high", + "en_full": "Mabwell", + "issue": "term 'Maywavee' 为拼写错误,正确英文名为 'Mabwell',中文规范译名为 '迈威生物'。搜索结果中多条权威医药媒体(新药情报库、医药魔方)均确认 Mabwell → 迈威生物的对应关系。", + "sources": [ + "https://synapse.zhihuiya.com/blog/%E8%BF%88%E5%A8%81%E7%94%9F%E7%89%A910%E4%BA%BF%E7%BE%8Eyuan%E5%8F%8C%E9%9D%B6sirna%E8%8D%AF%E7%89%A9newco%E5%85%A8%E7%90%83%E6%88%98%E7%95%A5%E8%B5%B7%E8%88%AA", + "https://synapse.zhihuiya.com/blog/%E8%BF%88%E5%A8%81%E7%94%9F%E7%89%A910%E4%BA%BF%E7%BE%8Eyuan%E5%8F%8C%E9%9D%B6%E7%82%B9sirna%E6%96%B0%E8%8D%AF%E6%88%90%E5%8A%9F%E7%AD%BE%E7%BD%B2newco%E4%BA%A4%E6%98%93", + "https://bydrug.pharmcube.com/news/detail/06fbf6f2e1174c99eb5f99dc8cceaef8" + ], + "verified_at": "2026-04-22", + "zh": "迈威生物" + }, + "MutS Homolog 3": { + "zh": "MutS同源物3" + }, + "N-acetylgalactosamine": { + "zh": "N-乙酰半乳糖胺" + }, + "NAG37": { + "zh": "NAG37" + }, + "NDA": { + "zh": "新药申请" + }, + "NMPA": { + "zh": "国家药品监督管理局" + }, + "Nitto Denko Avecia": { + "zh": "Nitto Denko Avecia" + }, + "NittoPhase HL": { + "zh": "NittoPhase HL" + }, + "Norroa": { + "zh": "Norroa" + }, + "Notice No. 21 of 2026": { + "zh": "2026年第21号通告" + }, + "Nuclease P1": { + "zh": "核酸酶P1" + }, + "Nucleoside Triphosphate": { + "zh": "核苷三磷酸" + }, + "OPR&D": { + "zh": "《有机工艺研究与开发》(OPR&D)" + }, + "PCSK9": { + "zh": "PCSK9" + }, + "PDE": { + "zh": "允许日暴露量" + }, + "PMO synthesis": { + "zh": "PMO合成" + }, + "PVA entrapment": { + "zh": "PVA包埋" + }, + "Phosphodiester linker": { + "zh": "磷酸二酯接头" + }, + "Polynucleotide Kinase (T4)": { + "zh": "多核苷酸激酶(T4)" + }, + "Poresyn Solutions": { + "zh": "Poresyn Solutions" + }, + "Prime Synthesis CPG": { + "zh": "Prime Synthesis CPG" + }, + "PrimeMax siRNA CPG": { + "zh": "PrimeMax siRNA CPG" + }, + "Process Analytical Technology": { + "zh": "过程分析技术" + }, + "Process Atlas": { + "zh": "工艺图谱" + }, + "Process Mass Intensity": { + "zh": "工艺质量强度" + }, + "Q3M": { + "zh": "Q3M" + }, + "Q6M": { + "zh": "Q6M" + }, + "QC biocatalyst": { + "zh": "质控生物催化剂" + }, + "RADS™": { + "zh": "RADS™" + }, + "RNA interference": { + "confidence": "high", + "en_full": "RNA interference", + "issue": "", + "sources": [ + "https://www.ronatherapeutics.com/cn/news/45", + "https://www.zhihuiya.com/news/info_12579.html", + "https://www.delveinsight.com/report-store/rna-interference-therapy-competitive-landscape" + ], + "verified_at": "2026-04-22", + "zh": "RNA干扰" + }, + "RNA-Induced Silencing Complex": { + "zh": "RNA诱导沉默复合体" + }, + "RNase A": { + "zh": "核糖核酸酶A" + }, + "RNase H": { + "zh": "核糖核酸酶H" + }, + "RNase Inhibitor": { + "zh": "RNase Inhibitor" + }, + "RNase T1": { + "zh": "核糖核酸酶T1" + }, + "RSC 2.0 modification": { + "zh": "RSC 2.0修饰" + }, + "RiboGalSTAR™": { + "zh": "RiboGalSTAR™" + }, + "Ribonuclease T1": { + "zh": "核糖核酸酶T1" + }, + "Roche Custom Biotech": { + "zh": "罗氏定制生物技术" + }, + "SBIA": { + "zh": "SBIA" + }, + "SBS": { + "zh": "SBS" + }, + "SMART Digest RNase T1 kit": { + "zh": "SMART Digest RNase T1试剂盒" + }, + "SPOS": { + "zh": "固相合成(SPOS)" + }, + "ST Pharm": { + "zh": "ST Pharm" + }, + "STP122G": { + "zh": "STP122G" + }, + "SUGAR-TARGET glycosyl-transferase cascade": { + "zh": "SUGAR-TARGET糖基转移酶级联反应" + }, + "Sangon Biotech": { + "zh": "生工" + }, + "SiaT": { + "zh": "SiaT" + }, + "Snake Venom Phosphodiesterase": { + "zh": "蛇毒磷酸二酯酶" + }, + "Sollbruchstellen": { + "zh": "易断位点" + }, + "SpyCatcher/SpyTag": { + "zh": "SpyCatcher/SpyTag" + }, + "Strain-Promoted Azide–Alkyne Cycloaddition": { + "zh": "应变促进叠氮–炔烃环加成" + }, + "Supply-Chain Opportunity Map": { + "zh": "供应链机会地图" + }, + "Suzhou Taike": { + "zh": "苏州泰科" + }, + "Synthesis Modality": { + "zh": "合成模式" + }, + "T4 RNA Ligase 1": { + "zh": "T4 RNA连接酶1" + }, + "T4 RNA Ligase 2": { + "zh": "T4 RNA连接酶2" + }, + "T7 RNA polymerase": { + "zh": "T7 RNA聚合酶" + }, + "TRL": { + "zh": "技术成熟度" + }, + "TRiM™": { + "zh": "TRiM™平台" + }, + "Takara Bio": { + "zh": "宝生物工程" + }, + "Technical Guidelines for Pharmaceutical Research on Chemically Synthesized Oligonucleotide Drugs (Innovative Drugs)": { + "zh": "化学合成寡核苷酸药物(创新药)药学研究技术指导原则(试行)" + }, + "Terminal Deoxynucleotidyl Transferase": { + "zh": "末端脱氧核苷酸转移酶" + }, + "Tianjin Orilife": { + "zh": "天津奥利法" + }, + "Tier 1": { + "zh": "第一层级" + }, + "Tier 2": { + "zh": "第二层级" + }, + "Tier 3": { + "zh": "第三层级" + }, + "Triazole linker": { + "zh": "三唑接头" + }, + "TrisGal-6 scaffold": { + "zh": "TrisGal-6骨架" + }, + "UDP-Gal": { + "zh": "UDP-Gal" + }, + "UDP-GalNAc": { + "zh": "UDP-GalNAc" + }, + "Unylinker-functionalized polystyrene support": { + "zh": "Unylinker功能化聚苯乙烯载体" + }, + "Vazyme": { + "zh": "诺唯赞" + }, + "Worthington Biochemical": { + "zh": "Worthington Biochemical" + }, + "WuXi AppTec": { + "zh": "药明康德" + }, + "Wuhu Huaren": { + "zh": "芜湖华仁" + }, + "XBP-1": { + "zh": "XBP-1" + }, + "Yeasen Biotech": { + "zh": "翌圣生物科技" + }, + "acceptable daily intake": { + "zh": "每日可接受摄入量" + }, + "activated-intermediate shelf life": { + "zh": "活化中间体货架期" + }, + "anion-exchange polishing": { + "zh": "阴离子交换精制" + }, + "antisolvent": { + "zh": "反溶剂" + }, + "arm-coupling reaction": { + "zh": "臂偶联反应" + }, + "atom-economy penalty": { + "zh": "原子经济性损失" + }, + "avidity plateau": { + "zh": "亲合力平台" + }, + "bead attrition": { + "zh": "微珠磨损" + }, + "bottom-up mapping": { + "zh": "自下而上图谱分析" + }, + "branching-point coupling": { + "zh": "分支点偶联" + }, + "bundled enzyme-carrier product": { + "zh": "酶-载体捆绑产品" + }, + "cardiometabolic disease": { + "zh": "心脏代谢疾病" + }, + "cell-free synthesis": { + "zh": "无细胞合成" + }, + "channeling": { + "zh": "沟流" + }, + "charge-state deconvolution": { + "zh": "电荷态反卷积" + }, + "chemoenzymatic": { + "zh": "化学酶法" + }, + "choline chloride/glycerol": { + "zh": "氯化胆碱/甘油" + }, + "choline chloride/urea (reline)": { + "zh": "氯化胆碱/尿素(reline)" + }, + "class extension": { + "zh": "品类延伸" + }, + "clinical translation": { + "zh": "临床转化" + }, + "cluster-arm variants": { + "zh": "簇臂变体" + }, + "co-dosing program": { + "zh": "联合给药项目" + }, + "cocktail formulation": { + "zh": "鸡尾酒制剂" + }, + "complement dysregulation": { + "zh": "补体失调" + }, + "continuous process verification": { + "zh": "连续工艺验证" + }, + "continuous-flow microgel reactor": { + "zh": "连续流微凝胶反应器" + }, + "control threshold": { + "zh": "控制阈值" + }, + "convergent assembly": { + "zh": "汇聚式组装" + }, + "copper scavenging": { + "zh": "铜清除" + }, + "copper-residue burden": { + "zh": "铜残留合规负担" + }, + "coronary heart disease": { + "zh": "冠心病" + }, + "covalently-linked tandem siRNA": { + "zh": "共价连接串联siRNA" + }, + "cross-activity": { + "zh": "交叉活性" + }, + "denaturing IP-RP-LC-MS": { + "zh": "变性离子对反相液相色谱-质谱" + }, + "dendritic scaffold": { + "zh": "树枝状骨架" + }, + "design paradigm": { + "zh": "设计范式" + }, + "di-valent branched construct": { + "zh": "二价分支构建体" + }, + "diamine scaffold": { + "zh": "二胺骨架" + }, + "disulfide bond": { + "zh": "二硫键" + }, + "domestic-substitution feasibility": { + "zh": "国产替代可行性" + }, + "droplet microfluidics": { + "zh": "液滴微流控技术" + }, + "dsRNA": { + "zh": "dsRNA" + }, + "dual-functional RNAi therapeutic": { + "zh": "双功能RNAi治疗药物" + }, + "enantiomeric excess": { + "zh": "对映体过量值" + }, + "enzymatic-ligation": { + "zh": "酶连接" + }, + "enzyme loading per gram": { + "zh": "每克载体酶载量" + }, + "fixed-bed copper-scavenging resin": { + "zh": "固定床铜清除树脂" + }, + "glutaraldehyde": { + "zh": "戊二醛" + }, + "glycosyl-transferase": { + "zh": "糖基转移酶" + }, + "hepatic parenchymal clearance": { + "zh": "肝实质清除率" + }, + "hetero-duplex": { + "zh": "异源双链体" + }, + "hexaethyleneglycol": { + "zh": "六乙二醇" + }, + "hexavalent GalNAc cluster": { + "zh": "六价GalNAc簇" + }, + "high-load polymeric support": { + "zh": "高载量聚合物载体" + }, + "homo-duplex": { + "zh": "同源双链体" + }, + "hypertriglyceridemia": { + "zh": "高甘油三酯血症" + }, + "immobilized enzyme": { + "zh": "固定化酶" + }, + "immobilized polymerase/phosphatase reactor": { + "zh": "固定化聚合酶/磷酸酶反应器" + }, + "inline conversion monitoring": { + "zh": "在线转化率监测" + }, + "inter-lot specific activity variation": { + "zh": "批间比活力变异" + }, + "junction-verification assay": { + "zh": "连接位点验证检测" + }, + "lenticular beads": { + "zh": "扁豆形微珠" + }, + "lipid nanoparticle": { + "zh": "脂质纳米颗粒" + }, + "liquid-phase oligonucleotide synthesis": { + "zh": "液相寡核苷酸合成" + }, + "liquid-phase synthesis": { + "zh": "液相合成" + }, + "mRNAi GOLD™": { + "zh": "mRNAi GOLD™" + }, + "maleimide-activated agarose": { + "zh": "马来酰亚胺活化琼脂糖" + }, + "manufacturing stack": { + "zh": "制造体系" + }, + "material diversion strategy": { + "zh": "物料转移策略" + }, + "membrane bioreactor": { + "zh": "膜生物反应器" + }, + "methacrylate copolymer beads": { + "zh": "甲基丙烯酸酯共聚物微珠" + }, + "mixed hyperlipidemia": { + "zh": "混合型高脂血症" + }, + "monoantennary GalNAc": { + "zh": "单天线GalNAc" + }, + "monomer diversity index": { + "zh": "单体多样性指标" + }, + "muRNA": { + "zh": "muRNA" + }, + "multivalent GalNAc cluster": { + "zh": "多价GalNAc簇" + }, + "multivalent GalNAc scaffold": { + "zh": "多价GalNAc骨架" + }, + "murine TdT": { + "zh": "鼠源TdT" + }, + "mxRNA": { + "zh": "mxRNA" + }, + "neurodegeneration": { + "zh": "神经退行性疾病" + }, + "nonclinical guidance": { + "zh": "非临床指南" + }, + "nuclease mapping": { + "zh": "核酸酶图谱分析" + }, + "packed-bed reactor": { + "zh": "填充床反应器" + }, + "partially conjugated strand": { + "zh": "部分偶联链" + }, + "particle-integrity monitoring": { + "zh": "颗粒完整性监测" + }, + "pentavalent GalNAc": { + "zh": "五价GalNAc" + }, + "phosphoramidite monomer": { + "confidence": "high", + "en_full": "phosphoramidite monomer", + "issue": "", + "sources": [ + "https://broadpharm.com/blog/Modified-Phosphoramidites-in-RNAi-Research", + "https://www.ractigen.com/sirna-aco-is-a-convenient-phosphoramidite-based-conjugate-that-enables-rnai-in-the-cns-via-local-administration-with-superior-efficacy-in-the-treatment-of-als-rodent-models-poster/", + "https://pmc.ncbi.nlm.nih.gov/articles/PMC11472056/" + ], + "verified_at": "2026-04-22", + "zh": "亚磷酰胺单体" + }, + "pipeline inflation": { + "zh": "管线虚胖" + }, + "platform-multiplication event": { + "zh": "平台倍增事件" + }, + "polymer microgels": { + "zh": "聚合物微凝胶" + }, + "pre-validated package": { + "zh": "预验证文件包" + }, + "pressure-drop effects": { + "zh": "压降效应" + }, + "process debt": { + "zh": "工艺债务" + }, + "process node": { + "zh": "工艺节点" + }, + "process signature": { + "zh": "工艺特征" + }, + "product-specific guidance": { + "zh": "产品专项指南" + }, + "protecting-group strategy": { + "zh": "保护基策略" + }, + "protective allele": { + "zh": "保护性等位基因" + }, + "pyranose": { + "zh": "吡喃糖" + }, + "qualification barrier": { + "zh": "资质壁垒" + }, + "rSAP": { + "zh": "rSAP" + }, + "ratio QC": { + "zh": "比例质控" + }, + "receptor-mediated uptake": { + "zh": "受体内吞" + }, + "regulatory vector": { + "zh": "监管向量" + }, + "release-profile QC": { + "zh": "释放谱质控" + }, + "residual enzyme surveillance": { + "zh": "残留酶监控" + }, + "ribofuranose scaffold": { + "zh": "核糖呋喃糖骨架" + }, + "serially assembled trivalent construct": { + "zh": "序贯组装三价构建体" + }, + "siRELIS facility": { + "zh": "siRELIS工厂" + }, + "silica bead fines": { + "zh": "硅胶微珠细粉" + }, + "solid-phase phosphoramidite synthesis": { + "zh": "固相亚磷酰胺合成" + }, + "solid-phase synthesis": { + "zh": "固相合成" + }, + "soluble anchor": { + "zh": "可溶性锚定基团" + }, + "spacer accessibility": { + "zh": "间隔臂可及性" + }, + "splint removal": { + "zh": "DNA夹板去除" + }, + "streptavidin-coated silica beads": { + "zh": "链霉亲和素包被硅胶微珠" + }, + "structure-activity study": { + "zh": "构效关系研究" + }, + "sub-saturation binding regime": { + "zh": "未饱和结合区间" + }, + "supplier concentration": { + "zh": "供应商集中度" + }, + "swelling index": { + "zh": "溶胀指数" + }, + "tetraantennary": { + "zh": "四天线" + }, + "tetraloop": { + "zh": "四环体" + }, + "top-down intact-mass sequencing": { + "zh": "自上而下完整质量测序" + }, + "toxicokinetic subfactor justification": { + "zh": "毒代动力学亚因子论证" + }, + "triantennary GalNAc": { + "zh": "三天线GalNAc" + }, + "truncated cluster impurity": { + "zh": "截短型簇杂质" + } } diff --git a/projects/dual-target-rnai-pipeline-2026/phase4/logs/glossary.jsonl b/projects/dual-target-rnai-pipeline-2026/phase4/logs/glossary.jsonl new file mode 100644 index 0000000..3c1afda --- /dev/null +++ b/projects/dual-target-rnai-pipeline-2026/phase4/logs/glossary.jsonl @@ -0,0 +1,3 @@ +{"tag": "glossary:phosphoramidite monomer", "model": "anthropic/claude-haiku-4.5", "attempt": 0, "elapsed": 2.81, "usage": {"completion_tokens": 48, "prompt_tokens": 2187, "total_tokens": 2235, "prompt_tokens_details": {"cached_tokens": 0, "ephemeral_5m_input_tokens": 0, "ephemeral_1h_input_tokens": 0, "web_search": 0, "cacheCreationInputTokens": 0}}, "out_chars": 101, "status": 200} +{"tag": "glossary:RNA interference", "model": "anthropic/claude-haiku-4.5", "attempt": 0, "elapsed": 2.82, "usage": {"completion_tokens": 37, "prompt_tokens": 2585, "total_tokens": 2622, "prompt_tokens_details": {"cached_tokens": 0, "ephemeral_5m_input_tokens": 0, "ephemeral_1h_input_tokens": 0, "web_search": 0, "cacheCreationInputTokens": 0}}, "out_chars": 93, "status": 200} +{"tag": "glossary:Maywavee", "model": "anthropic/claude-haiku-4.5", "attempt": 0, "elapsed": 3.71, "usage": {"completion_tokens": 136, "prompt_tokens": 3185, "total_tokens": 3321, "prompt_tokens_details": {"cached_tokens": 0, "ephemeral_5m_input_tokens": 0, "ephemeral_1h_input_tokens": 0, "web_search": 0, "cacheCreationInputTokens": 0}}, "out_chars": 186, "status": 200} diff --git a/scripts/build_glossary.py b/scripts/build_glossary.py new file mode 100644 index 0000000..5602183 --- /dev/null +++ b/scripts/build_glossary.py @@ -0,0 +1,332 @@ +#!/usr/bin/env python3 +"""双语术语表事实核查脚本。 + +输入: +- /phase4/glossary.json(来自 translate.py 累积的初版术语表) +- 可选:--extra terms.txt(每行一个英文术语,补充进来一起核查) + +流程(每个术语独立可并行): +1. 用 SearchClient(Exa > Tavily)搜一次(query = " ") +2. 把 top 3-5 snippet 喂给 Haiku,让模型返回 {zh, en_full, confidence, issue} +3. 合并回 glossary,字段扩展: + { + "Mabwell": { + "zh": "迈威生物", + "en_full": "Mabwell (Shanghai) Bioscience Co., Ltd.", + "confidence": "high", + "issue": "...", + "verified_at": "2026-04-22", + "sources": ["https://mabwell.com/", ...] + } + } + +用法: + uv run python scripts/build_glossary.py + uv run python scripts/build_glossary.py --workers 6 + uv run python scripts/build_glossary.py --only "Mabwell,Maywavee" + uv run python scripts/build_glossary.py --force + +断点续传:已核查过的条目(有 verified_at 字段)默认跳过;--force 全部重跑。 +""" + +from __future__ import annotations + +import argparse +import concurrent.futures +import datetime as dt +import json +import sys +import time +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from scripts.lib.search_client import SearchClient, SearchError +from scripts.lib.zenmux_client import ZenMuxClient, ZenMuxError, load_secrets + +DEFAULT_MODEL = "anthropic/claude-haiku-4.5" +PROMPT_FILE = Path(__file__).parent / "prompts" / "glossary_system.txt" + + +def resolve_project(arg: str) -> Path: + p = Path(arg) + if p.is_dir(): + return p + cand = Path.cwd() / "projects" / arg + if cand.is_dir(): + return cand + raise SystemExit(f"project not found: {arg}") + + +def load_glossary(path: Path) -> dict: + if not path.exists(): + return {} + return json.loads(path.read_text(encoding="utf-8")) + + +def save_glossary(path: Path, glossary: dict) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text( + json.dumps(glossary, ensure_ascii=False, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + + +def migrate_legacy_entry(value) -> dict: + """旧版 glossary 里 value 是字符串;升级为对象格式。""" + if isinstance(value, str): + return {"zh": value} + if isinstance(value, dict): + return value + return {"zh": str(value)} + + +def build_query(term: str, domain: str) -> str: + if domain: + return f"{term} {domain} 中文名" + return term + + +def parse_json_line(text: str) -> dict: + """模型返回的单行 JSON。容忍前后额外字符。""" + s = text.strip() + l = s.find("{") + r = s.rfind("}") + if l == -1 or r == -1: + raise ValueError(f"no JSON object: {text[:200]}") + obj = json.loads(s[l : r + 1]) + if not isinstance(obj, dict): + raise ValueError("top-level JSON not object") + return obj + + +def build_user_prompt(term: str, domain: str, current_zh: str, hits: list) -> str: + hits_text = "\n\n".join( + f"[{i+1}] {h.title}\n URL: {h.url}\n {h.snippet[:500]}" + for i, h in enumerate(hits[:5]) + ) + if not hits_text: + hits_text = "(无搜索结果)" + return ( + f"term: {term}\n" + f"domain: {domain or '(未指定)'}\n" + f"current_zh: {current_zh or '(空)'}\n\n" + f"search_hits:\n{hits_text}\n" + ) + + +def verify_term( + term: str, + current_zh: str, + domain: str, + search_client: SearchClient, + llm_client: ZenMuxClient, + *, + model: str, + system_prompt: str, +) -> dict: + try: + hits = search_client.search( + build_query(term, domain), + num_results=4, + ) + except SearchError as e: + return { + "zh": current_zh or "", + "en_full": term, + "confidence": "low", + "issue": f"搜索失败:{e}", + "sources": [], + } + + user = build_user_prompt(term, domain, current_zh, hits) + try: + raw = llm_client.chat_complete( + model=model, + system=system_prompt, + user=user, + temperature=0.1, + max_tokens=800, + tag=f"glossary:{term[:30]}", + ) + obj = parse_json_line(raw) + except (ZenMuxError, ValueError) as e: + return { + "zh": current_zh or "", + "en_full": term, + "confidence": "low", + "issue": f"LLM 核查失败:{e}", + "sources": [h.url for h in hits[:3]], + } + + # 规范化输出 + return { + "zh": str(obj.get("zh", current_zh or "")), + "en_full": str(obj.get("en_full", term)), + "confidence": str(obj.get("confidence", "low")), + "issue": str(obj.get("issue", "")), + "sources": [h.url for h in hits[:3]], + } + + +def main() -> int: + parser = argparse.ArgumentParser(description="双语术语表事实核查(Haiku + Exa)") + parser.add_argument("project", help="项目 slug 或完整路径") + parser.add_argument( + "--input", + default="phase4/glossary.json", + help="初版术语表路径(相对项目根)", + ) + parser.add_argument( + "--output", + default="phase4/glossary.json", + help="输出路径(默认原地覆盖)", + ) + parser.add_argument("--model", default=DEFAULT_MODEL, help="LLM 模型") + parser.add_argument( + "--workers", type=int, default=6, help="并发度(默认 6,Exa 限速 5 QPS)" + ) + parser.add_argument( + "--force", action="store_true", + help="忽略已核查状态,全部重跑", + ) + parser.add_argument( + "--only", + default=None, + help="只核查指定术语(逗号分隔,大小写敏感)", + ) + parser.add_argument( + "--extra", + default=None, + help="每行一个英文术语的文本文件,补充进术语表一起核查", + ) + parser.add_argument( + "--domain", + default=None, + help="术语领域提示(默认自动读 manifest.topic)", + ) + args = parser.parse_args() + + load_secrets() + project_root = resolve_project(args.project) + input_path = project_root / args.input + output_path = project_root / args.output + + manifest_path = project_root / "manifest.json" + domain = args.domain + if not domain and manifest_path.exists(): + m = json.loads(manifest_path.read_text(encoding="utf-8")) + domain = m.get("topic") or m.get("report_title") or "" + + glossary = load_glossary(input_path) + # migration + for k, v in list(glossary.items()): + glossary[k] = migrate_legacy_entry(v) + + if args.extra: + extra_path = Path(args.extra) + if not extra_path.exists(): + raise SystemExit(f"--extra 文件不存在:{extra_path}") + for line in extra_path.read_text(encoding="utf-8").splitlines(): + term = line.strip() + if term and term not in glossary: + glossary[term] = {"zh": ""} + + only_terms: set[str] | None = None + if args.only: + only_terms = {t.strip() for t in args.only.split(",") if t.strip()} + + system_prompt = PROMPT_FILE.read_text(encoding="utf-8") + + # 筛选需要核查的 + todo: list[str] = [] + for term, entry in glossary.items(): + if only_terms is not None and term not in only_terms: + continue + if not args.force and entry.get("verified_at"): + continue + todo.append(term) + + print(f"Project: {project_root.name}") + print(f"Domain hint: {domain or '(none)'}") + print(f"Glossary size: {len(glossary)} | to verify: {len(todo)} | workers: {args.workers}") + print(f"Model: {args.model}") + if not todo: + print(" 没有需要核查的条目(使用 --force 强制重跑)") + save_glossary(output_path, glossary) + return 0 + + today = dt.date.today().isoformat() + logs_dir = project_root / "phase4" / "logs" + log_file = logs_dir / "glossary.jsonl" + + start = time.time() + done_count = 0 + failed: list[str] = [] + + # 并发执行:SearchClient/ZenMuxClient 都是 thread-safe(httpx.Client 支持) + with SearchClient() as search_client, ZenMuxClient(log_file=log_file) as llm_client: + + def worker(term: str) -> tuple[str, dict]: + current_zh = glossary.get(term, {}).get("zh", "") + result = verify_term( + term, + current_zh, + domain or "", + search_client, + llm_client, + model=args.model, + system_prompt=system_prompt, + ) + return term, result + + with concurrent.futures.ThreadPoolExecutor(max_workers=args.workers) as pool: + futures = {pool.submit(worker, term): term for term in todo} + for fut in concurrent.futures.as_completed(futures): + term = futures[fut] + try: + _term, result = fut.result() + except Exception as e: + print(f" [FAIL] {term}: {e}") + failed.append(term) + continue + # 合并到 glossary + old = glossary.get(term, {}) + old.update(result) + old["verified_at"] = today + glossary[term] = old + done_count += 1 + conf = result.get("confidence", "?") + issue = result.get("issue", "") + zh = result.get("zh") or "(保留英文)" + marker = {"high": "✓", "medium": "~", "low": "?"}.get(conf, " ") + issue_str = f" ⚠ {issue[:80]}" if issue else "" + print(f" [{marker}] {term:<35} → {zh}{issue_str}") + # 阶段性存盘,避免中途挂掉丢数据 + if done_count % 10 == 0: + save_glossary(output_path, glossary) + + save_glossary(output_path, glossary) + + elapsed = time.time() - start + print(f"\n完成:{done_count}/{len(todo)}(失败 {len(failed)},耗时 {elapsed:.1f}s)") + print(f"术语表:{output_path.relative_to(project_root)}") + print(llm_client.usage.summary()) + + # 高警示项汇总 + issues = [ + (k, v) for k, v in glossary.items() + if v.get("issue") and v.get("confidence") != "high" + ] + if issues: + print(f"\n⚠ 低置信度或带问题的条目({len(issues)} 条):") + for k, v in issues[:15]: + print(f" - {k:<30} [{v.get('confidence','?')}] {v.get('issue','')[:100]}") + if len(issues) > 15: + print(f" … 还有 {len(issues) - 15} 条,见 {output_path.name}") + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/build_report.py b/scripts/build_report.py new file mode 100644 index 0000000..aced9ff --- /dev/null +++ b/scripts/build_report.py @@ -0,0 +1,238 @@ +#!/usr/bin/env python3 +"""Phase 4 成稿阶段:统一入口。 + +从 final_zh_polished.md(或指定的 Markdown)+ manifest.json 生成: +- .pdf ReportLab 出中文 PDF +- <title>.docx Pandoc 出 DOCX +- <title>-en.pdf 如果存在 final_en.md 也一并出英文版(可选) + +文件名来自 manifest.report_title(去掉非法字符),不再用 "final.pdf" 这种通用名。 + +用法: + uv run python scripts/build_report.py <project_slug> + + # 只生成 PDF: + uv run python scripts/build_report.py <project_slug> --no-docx + + # 从自定义 md 生成: + uv run python scripts/build_report.py <project_slug> --input phase4/final_zh.md + +环境依赖: +- reportlab, pypandoc, 思源字体(bash .opencode/templates/fonts/download-fonts.sh) +- pandoc 可执行文件在 PATH +""" + +from __future__ import annotations + +import argparse +import json +import re +import shutil +import subprocess +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from scripts.lib.zenmux_client import load_secrets # noqa: F401 (为一致性) + + +REPO_ROOT = Path(__file__).resolve().parent.parent +PDF_TEMPLATE = REPO_ROOT / ".opencode" / "templates" / "report-template.py" +DEFAULT_FONTS_DIR = REPO_ROOT / ".opencode" / "templates" / "fonts" + + +def resolve_project(arg: str) -> Path: + p = Path(arg) + if p.is_dir(): + return p.resolve() + cand = REPO_ROOT / "projects" / arg + if cand.is_dir(): + return cand.resolve() + raise SystemExit(f"project not found: {arg}") + + +# 文件系统对文件名的常见限制:Windows 更严格,按最小公倍数来 +_FS_ILLEGAL_RE = re.compile(r'[\\/:*?"<>|\r\n\t]+') +_WHITESPACE_RE = re.compile(r"\s+") + + +def sanitize_filename(name: str, max_len: int = 120) -> str: + """把报告标题变成可跨 OS 使用的文件名。""" + if not name: + return "report" + # 去掉非法字符 + cleaned = _FS_ILLEGAL_RE.sub(" ", name) + # 合并空白 + cleaned = _WHITESPACE_RE.sub(" ", cleaned).strip() + # 首尾 . 空格 . (Windows 要求) + cleaned = cleaned.strip(". ").strip() + if len(cleaned) > max_len: + cleaned = cleaned[:max_len].rstrip() + return cleaned or "report" + + +def load_manifest(path: Path) -> dict: + return json.loads(path.read_text(encoding="utf-8")) + + +def determine_input_md(project_root: Path, preferred: str | None) -> Path: + """决定用哪个 Markdown 出稿。 + + 优先级:--input 指定 > final_zh_polished.md > final_zh.md > final_en.md + """ + if preferred: + p = project_root / preferred + if not p.exists(): + raise SystemExit(f"指定的 --input 不存在:{p}") + return p + for candidate in ( + "phase4/final_zh_polished.md", + "phase4/final_zh.md", + "phase4/final_en.md", + ): + p = project_root / candidate + if p.exists(): + return p + raise SystemExit( + f"找不到任何 Markdown 源。项目根:{project_root}\n" + "先跑 translate.py / polish.py 或使用 --input 指定路径。" + ) + + +def build_pdf( + md_path: Path, + manifest_path: Path, + output_pdf: Path, + fonts_dir: Path, + sources_path: Path | None, +) -> None: + """调用 report-template.py 生成 PDF。""" + cmd = [ + sys.executable, + str(PDF_TEMPLATE), + "--input", str(md_path), + "--manifest", str(manifest_path), + "--output", str(output_pdf), + "--fonts-dir", str(fonts_dir), + ] + if sources_path and sources_path.exists(): + cmd += ["--sources", str(sources_path)] + + print(f"\n→ 生成 PDF:{output_pdf.name}") + result = subprocess.run(cmd, check=False) + if result.returncode != 0: + raise SystemExit(f"PDF 生成失败,返回码 {result.returncode}") + + +def build_docx(md_path: Path, output_docx: Path, title: str) -> None: + """用 pandoc 生成 DOCX。""" + if not shutil.which("pandoc"): + print(f" ⚠ pandoc 不在 PATH,跳过 DOCX 生成", file=sys.stderr) + return + + print(f"\n→ 生成 DOCX:{output_docx.name}") + # 关闭 tex_math_dollars/tex_math_single_backslash 防止文中 "$100" "$10^6" 被当数学公式 + cmd = [ + "pandoc", + str(md_path), + "-o", str(output_docx), + "--from=markdown-tex_math_dollars-tex_math_single_backslash-raw_tex", + "--to=docx", + "--standalone", + "-M", f"title={title}", + "--wrap=preserve", + ] + # reference-doc 如果存在就用 + ref_doc = REPO_ROOT / ".opencode" / "templates" / "reference.docx" + if ref_doc.exists(): + cmd += ["--reference-doc", str(ref_doc)] + + result = subprocess.run(cmd, check=False) + if result.returncode != 0: + print(f" ⚠ DOCX 生成失败(返回码 {result.returncode}),但不阻断流程", file=sys.stderr) + + +def main() -> int: + parser = argparse.ArgumentParser(description="Phase 4 成稿(PDF + DOCX)") + parser.add_argument("project", help="项目 slug 或完整路径") + parser.add_argument( + "--input", + default=None, + help="Markdown 源(默认自动寻找 phase4/final_zh_polished.md)", + ) + parser.add_argument( + "--output-dir", + default="phase4", + help="输出目录(相对项目根,默认 phase4)", + ) + parser.add_argument( + "--fonts-dir", + default=str(DEFAULT_FONTS_DIR), + help="字体目录", + ) + parser.add_argument( + "--sources", + default=None, + help="sources.jsonl 路径(默认 phase2/sources.jsonl)", + ) + parser.add_argument("--no-docx", action="store_true", help="跳过 DOCX 生成") + parser.add_argument("--no-pdf", action="store_true", help="跳过 PDF 生成") + parser.add_argument( + "--basename", + default=None, + help="文件名 stem(不带扩展名),默认从 manifest.report_title 生成", + ) + args = parser.parse_args() + + project_root = resolve_project(args.project) + manifest_path = project_root / "manifest.json" + if not manifest_path.exists(): + raise SystemExit(f"manifest.json not found: {manifest_path}") + + manifest = load_manifest(manifest_path) + md_path = determine_input_md(project_root, args.input) + + title = manifest.get("report_title") or manifest.get("topic") or "Deep Research Report" + basename = args.basename or sanitize_filename(title) + output_dir = project_root / args.output_dir + output_dir.mkdir(parents=True, exist_ok=True) + + pdf_path = output_dir / f"{basename}.pdf" + docx_path = output_dir / f"{basename}.docx" + + fonts_dir = Path(args.fonts_dir) + if not fonts_dir.is_absolute(): + fonts_dir = REPO_ROOT / fonts_dir + + sources_path: Path | None = None + if args.sources: + sources_path = Path(args.sources) + else: + default_src = project_root / "phase2" / "sources.jsonl" + if default_src.exists(): + sources_path = default_src + + print("========== Phase 4 成稿 ==========") + print(f"项目: {project_root.name}") + print(f"Markdown: {md_path.relative_to(project_root)}") + print(f"标题: {title}") + print(f"输出名: {basename}") + print(f"字体目录: {fonts_dir}") + print(f"Sources: {sources_path if sources_path else '(缺失)'}") + + if not args.no_pdf: + build_pdf(md_path, manifest_path, pdf_path, fonts_dir, sources_path) + if not args.no_docx: + build_docx(md_path, docx_path, title) + + print("\n========== 完成 ==========") + if pdf_path.exists(): + print(f" PDF: {pdf_path.relative_to(project_root)} ({pdf_path.stat().st_size // 1024} KB)") + if docx_path.exists(): + print(f" DOCX: {docx_path.relative_to(project_root)} ({docx_path.stat().st_size // 1024} KB)") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/lib/search_client.py b/scripts/lib/search_client.py new file mode 100644 index 0000000..3bf6807 --- /dev/null +++ b/scripts/lib/search_client.py @@ -0,0 +1,177 @@ +"""通用搜索客户端(Exa 优先,Tavily fallback)。 + +为 build_glossary.py 这类术语核查场景服务。 + +关键设计: +- `trust_env=False` 绕开系统 socks 代理(Clash on macOS 配 socks5 时 httpx 会 TLS EOF) +- Exa 优先:LinkedIn / 官网 / 百度百科返回质量最高 +- 遇到配额问题自动降级到 Tavily 或返回 empty +- 不做深度 crawl,只要摘要 +""" + +from __future__ import annotations + +import os +from dataclasses import dataclass +from typing import Any + +import httpx + + +@dataclass +class SearchHit: + title: str + url: str + snippet: str + + +class SearchError(RuntimeError): + pass + + +class ExaClient: + def __init__(self, api_key: str | None = None, timeout: float = 30.0) -> None: + self.api_key = api_key or os.environ.get("EXA_API_KEY") + if not self.api_key: + raise SearchError("EXA_API_KEY not set") + # trust_env=False 关键:不吃系统代理,避免 TLS EOF + self._client = httpx.Client(trust_env=False, timeout=timeout) + + def close(self) -> None: + self._client.close() + + def __enter__(self) -> "ExaClient": + return self + + def __exit__(self, *_args: Any) -> None: + self.close() + + def search(self, query: str, *, num_results: int = 5) -> list[SearchHit]: + body = { + "query": query, + "numResults": num_results, + "type": "auto", + "contents": {"text": {"maxCharacters": 800}}, + } + r = self._client.post( + "https://api.exa.ai/search", + json=body, + headers={"x-api-key": self.api_key, "Content-Type": "application/json"}, + ) + if r.status_code != 200: + raise SearchError(f"Exa HTTP {r.status_code}: {r.text[:200]}") + data = r.json() + out: list[SearchHit] = [] + for item in data.get("results", [])[:num_results]: + out.append( + SearchHit( + title=(item.get("title") or "")[:200], + url=item.get("url") or "", + snippet=(item.get("text") or item.get("snippet") or "")[:600], + ) + ) + return out + + +class TavilyClient: + def __init__(self, api_key: str | None = None, timeout: float = 30.0) -> None: + self.api_key = api_key or os.environ.get("TAVILY_API_KEY") + if not self.api_key: + raise SearchError("TAVILY_API_KEY not set") + self._client = httpx.Client(trust_env=False, timeout=timeout) + + def close(self) -> None: + self._client.close() + + def __enter__(self) -> "TavilyClient": + return self + + def __exit__(self, *_args: Any) -> None: + self.close() + + def search(self, query: str, *, num_results: int = 5) -> list[SearchHit]: + body = { + "api_key": self.api_key, + "query": query, + "search_depth": "basic", + "max_results": num_results, + "include_answer": False, + "include_raw_content": False, + } + r = self._client.post("https://api.tavily.com/search", json=body) + if r.status_code != 200: + raise SearchError(f"Tavily HTTP {r.status_code}: {r.text[:200]}") + data = r.json() + out: list[SearchHit] = [] + for item in data.get("results", [])[:num_results]: + out.append( + SearchHit( + title=(item.get("title") or "")[:200], + url=item.get("url") or "", + snippet=(item.get("content") or "")[:600], + ) + ) + return out + + +class SearchClient: + """统一搜索门面:先用 Exa,失败/配额问题降级 Tavily。""" + + def __init__(self) -> None: + self._exa: ExaClient | None = None + self._tavily: TavilyClient | None = None + try: + self._exa = ExaClient() + except SearchError: + pass + try: + self._tavily = TavilyClient() + except SearchError: + pass + if not (self._exa or self._tavily): + raise SearchError( + "neither EXA_API_KEY nor TAVILY_API_KEY available" + ) + + def close(self) -> None: + if self._exa: + self._exa.close() + if self._tavily: + self._tavily.close() + + def __enter__(self) -> "SearchClient": + return self + + def __exit__(self, *_args: Any) -> None: + self.close() + + def search(self, query: str, *, num_results: int = 5) -> list[SearchHit]: + # 优先 Exa + if self._exa: + try: + return self._exa.search(query, num_results=num_results) + except SearchError as e: + msg = str(e).lower() + if "exceed" in msg or "quota" in msg or "429" in msg or "402" in msg: + # 降级 + pass + else: + # 其它错误继续往下试 + pass + if self._tavily: + try: + return self._tavily.search(query, num_results=num_results) + except SearchError: + pass + return [] + + +if __name__ == "__main__": + from scripts.lib.zenmux_client import load_secrets + load_secrets() + with SearchClient() as c: + hits = c.search("Mabwell 迈威生物 biopharmaceutical", num_results=3) + for i, h in enumerate(hits, 1): + print(f"[{i}] {h.title[:80]}") + print(f" {h.url}") + print(f" {h.snippet[:160]}") diff --git a/scripts/prompts/glossary_system.txt b/scripts/prompts/glossary_system.txt new file mode 100644 index 0000000..955a7f5 --- /dev/null +++ b/scripts/prompts/glossary_system.txt @@ -0,0 +1,54 @@ +你是一名生物医药行业的资深双语术语编辑。现在要根据网络搜索片段,核查并确定一个英文术语/专有名词的"中文规范译名"、"英文全称"与"可信度"。 + +## 你将收到 + +- `term`:待核查的英文术语(可能是公司名、缩写、化学品、平台名、药物通用名等) +- `domain`:研究领域(例如 "生物医药双靶点 RNAi 药物"),作为消歧背景 +- `current_zh`:系统已有的中文译名(可能来自上一步翻译,也可能为空) +- `search_hits`:3–5 条 web 搜索结果(title + url + snippet) + +## 你要判断 + +1. **中文规范译名**(`zh`): + - 如果搜索结果里有权威的中文表达(公司官网、维基、百度百科、权威医药媒体),以它为准 + - 如果 `current_zh` 已经正确,沿用它,避免无谓变更 + - 如果无中文通用译名(小众学术术语、新兴化合物),保留英文原文,`zh` 字段置空字符串 + - 对公司/机构名:必须使用工商注册的正式中文名(例如 "Mabwell → 迈威生物"、"Sirnaomics → 圣诺生物") + - 对药物:优先 INN 通用名(例如 "inclisiran → 英克司兰") + - 对缩写/技术术语:使用业内通行译名(例如 "GalNAc → N-乙酰半乳糖胺","RNAi → RNA 干扰") + +2. **英文全称**(`en_full`): + - 如果术语是缩写(≤ 8 字符全大写或混合大小写),给出英文全称(例如 "ASGPR → Asialoglycoprotein Receptor") + - 如果术语本身就是全称,填相同的字符串或其最规范的写法 + - 如果术语是公司名,填其英文法人全称(例如 "Mabwell → Mabwell (Shanghai) Bioscience Co., Ltd.") + +3. **可信度**(`confidence`): + - `high`:至少两条独立、高质量信源(官网/监管机构/权威媒体)一致支持 + - `medium`:一条高质量信源支持,或多条一般信源一致支持 + - `low`:只能推断或无法确认,建议人工复核 + +4. **问题/警示**(`issue`): + - 如果 `current_zh` 明显错误(例如把 "Mabwell" 译成 "Maywavee"),在 `issue` 中指出错在哪里 + - 如果术语有多种译法争议,在 `issue` 中简述 + - 如果搜索结果完全不相关(错别字、生僻词),在 `issue` 中说"搜索无有效结果" + - 无异常则留空字符串 + +## 输出格式(严格) + +输出**一行 JSON**,不要用代码围栏,不要加任何解释。 + +``` +{"zh": "迈威生物", "en_full": "Mabwell (Shanghai) Bioscience Co., Ltd.", "confidence": "high", "issue": ""} +``` + +如果 `current_zh` 错了: + +``` +{"zh": "迈威生物", "en_full": "Mabwell (Shanghai) Bioscience Co., Ltd.", "confidence": "high", "issue": "current_zh 'Maywavee' 为拼写错误,正确为 Mabwell → 迈威生物"} +``` + +如果无需中文译名(保留英文): + +``` +{"zh": "", "en_full": "Phosphoramidite", "confidence": "high", "issue": ""} +```