@brainpilot/skills 0.0.6 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/skills/01_Meta-Skills/academic-research-hub/SKILL.md +108 -0
- package/skills/01_Meta-Skills/academic-research-hub/scripts/requirements.txt +17 -0
- package/skills/01_Meta-Skills/academic-research-hub/scripts/research.py +781 -0
- package/skills/01_Meta-Skills/beautiful-log/SKILL.md +64 -0
- package/skills/01_Meta-Skills/beautiful-log/scripts/beautiful_log.py +274 -0
- package/skills/01_Meta-Skills/ethoclaw-daily-paper/SKILL.md +130 -0
- package/skills/01_Meta-Skills/ethoclaw-daily-paper/assets/config.template.yaml +54 -0
- package/skills/01_Meta-Skills/ethoclaw-daily-paper/assets/top5_digest_template.md +5 -0
- package/skills/01_Meta-Skills/ethoclaw-daily-paper/scripts/build_top5_digest.py +300 -0
- package/skills/01_Meta-Skills/ethoclaw-daily-paper/scripts/common.py +137 -0
- package/skills/01_Meta-Skills/ethoclaw-daily-paper/scripts/merge_results.py +106 -0
- package/skills/01_Meta-Skills/ethoclaw-daily-paper/scripts/run_pipeline.py +177 -0
- package/skills/01_Meta-Skills/ethoclaw-daily-paper/scripts/search_arxiv.py +162 -0
- package/skills/01_Meta-Skills/ethoclaw-daily-paper/scripts/search_pubmed.py +202 -0
- package/skills/01_Meta-Skills/ethoclaw-normalize-tabular/SKILL.md +173 -0
- package/skills/01_Meta-Skills/ethoclaw-normalize-tabular/scripts/normalize_data.py +874 -0
- package/skills/01_Meta-Skills/ethoclaw-pdf-research/SKILL.md +134 -0
- package/skills/01_Meta-Skills/ethoclaw-pdf-research/references/confirmation-prompts.md +31 -0
- package/skills/01_Meta-Skills/ethoclaw-pdf-research/references/output-patterns.md +45 -0
- package/skills/01_Meta-Skills/ethoclaw-pdf-research/scripts/build_markdown_deliverables.py +41 -0
- package/skills/01_Meta-Skills/ethoclaw-pdf-research/scripts/build_research_log.py +84 -0
- package/skills/01_Meta-Skills/ethoclaw-pdf-research/scripts/build_summary_md.py +63 -0
- package/skills/01_Meta-Skills/ethoclaw-pdf-research/scripts/extract_pdf_bundle.py +140 -0
- package/skills/01_Meta-Skills/experiment-controller/SKILL.md +140 -0
- package/skills/01_Meta-Skills/knowledge-graph-builder/SKILL.md +366 -0
- package/skills/01_Meta-Skills/knowledge-graph-builder/scripts/entity_resolution.py +120 -0
- package/skills/01_Meta-Skills/knowledge-graph-builder/scripts/extraction_prompt_template.txt +19 -0
- package/skills/01_Meta-Skills/knowledge-graph-builder/scripts/graph_query.py +106 -0
- package/skills/01_Meta-Skills/knowledge-graph-builder/scripts/hypothesis_cli_reference.py +42 -0
- package/skills/01_Meta-Skills/knowledge-graph-builder/scripts/new_data_source_template.py +116 -0
- package/skills/01_Meta-Skills/knowledge-graph-builder/scripts/requirements.txt +15 -0
- package/skills/01_Meta-Skills/method-design/SKILL.md +61 -0
- package/skills/01_Meta-Skills/multi-search-engine/SKILL.md +119 -0
- package/skills/01_Meta-Skills/research-idea/SKILL.md +65 -0
- package/skills/05_EEG_ERP/eeg-skill/SKILL.md +197 -0
- package/skills/05_EEG_ERP/meg-skill/SKILL.md +188 -0
- package/skills/05_EEG_ERP/meg-skill/scripts/time_frequency.py +223 -0
- package/skills/05_EEG_ERP/mne-eeg-tool/SKILL.md +165 -0
- package/skills/05_EEG_ERP/mne-eeg-tool/scripts/eeg_pipeline_reference.py +231 -0
- package/skills/05_EEG_ERP/seed-iv-skill/SKILL.md +184 -0
- package/skills/05_EEG_ERP/seed-iv-skill/scripts/classify_seed_iv.py +154 -0
- package/skills/05_EEG_ERP/seed-iv-skill/scripts/extract_seed_iv_features.py +190 -0
- package/skills/05_EEG_ERP/seed-iv-skill/scripts/validate_seed_iv.py +102 -0
- package/skills/05_EEG_ERP/seed-vig-skill/SKILL.md +182 -0
- package/skills/05_EEG_ERP/seed-vig-skill/scripts/classify_seed_vig.py +165 -0
- package/skills/05_EEG_ERP/seed-vig-skill/scripts/extract_seed_vig_features.py +185 -0
- package/skills/05_EEG_ERP/seed-vig-skill/scripts/validate_seed_vig.py +88 -0
- package/skills/06_fMRI_Neuroimaging/abcd-skill/SKILL.md +308 -0
- package/skills/06_fMRI_Neuroimaging/abcd-skill/scripts/abcd_qc_summary.py +449 -0
- package/skills/06_fMRI_Neuroimaging/abcd-skill/scripts/extract_abcd_phenotype.py +292 -0
- package/skills/06_fMRI_Neuroimaging/abcd-skill/scripts/reorganize_abcd.py +387 -0
- package/skills/06_fMRI_Neuroimaging/abide-skill/SKILL.md +302 -0
- package/skills/06_fMRI_Neuroimaging/abide-skill/scripts/abide_qc_summary.py +317 -0
- package/skills/06_fMRI_Neuroimaging/abide-skill/scripts/extract_abide_phenotype.py +267 -0
- package/skills/06_fMRI_Neuroimaging/abide-skill/scripts/reorganize_abide.py +387 -0
- package/skills/06_fMRI_Neuroimaging/adhd200-skill/SKILL.md +244 -0
- package/skills/06_fMRI_Neuroimaging/adhd200-skill/scripts/adhd200_qc_summary.py +98 -0
- package/skills/06_fMRI_Neuroimaging/adhd200-skill/scripts/extract_adhd200_phenotype.py +134 -0
- package/skills/06_fMRI_Neuroimaging/adhd200-skill/scripts/reorganize_adhd200.py +206 -0
- package/skills/06_fMRI_Neuroimaging/adni-skill/SKILL.md +358 -0
- package/skills/06_fMRI_Neuroimaging/adni-skill/scripts/generate_adni_task_files.py +1305 -0
- package/skills/06_fMRI_Neuroimaging/adni-skill/scripts/generate_vqa_from_tasks.py +766 -0
- package/skills/06_fMRI_Neuroimaging/adni-skill/scripts/reorganize_adni.py +491 -0
- package/skills/06_fMRI_Neuroimaging/aibl-skill/SKILL.md +295 -0
- package/skills/06_fMRI_Neuroimaging/aibl-skill/scripts/aibl_qc_summary.py +260 -0
- package/skills/06_fMRI_Neuroimaging/aibl-skill/scripts/extract_aibl_phenotype.py +365 -0
- package/skills/06_fMRI_Neuroimaging/aibl-skill/scripts/reorganize_aibl.py +394 -0
- package/skills/06_fMRI_Neuroimaging/aomic-skill/SKILL.md +292 -0
- package/skills/06_fMRI_Neuroimaging/aomic-skill/scripts/aomic_qc_summary.py +258 -0
- package/skills/06_fMRI_Neuroimaging/aomic-skill/scripts/extract_aomic_phenotype.py +284 -0
- package/skills/06_fMRI_Neuroimaging/aomic-skill/scripts/reorganize_aomic.py +322 -0
- package/skills/06_fMRI_Neuroimaging/asl-skill/SKILL.md +168 -0
- package/skills/06_fMRI_Neuroimaging/asl-skill/scripts/compute_cbf.py +224 -0
- package/skills/06_fMRI_Neuroimaging/bids-organizer/SKILL.md +241 -0
- package/skills/06_fMRI_Neuroimaging/bold5000-skill/SKILL.md +186 -0
- package/skills/06_fMRI_Neuroimaging/bold5000-skill/scripts/bold5000_qc_summary.py +96 -0
- package/skills/06_fMRI_Neuroimaging/bold5000-skill/scripts/extract_bold5000_stimulus.py +125 -0
- package/skills/06_fMRI_Neuroimaging/bold5000-skill/scripts/reorganize_bold5000.py +102 -0
- package/skills/06_fMRI_Neuroimaging/camcan-skill/SKILL.md +213 -0
- package/skills/06_fMRI_Neuroimaging/camcan-skill/scripts/camcan_qc_summary.py +131 -0
- package/skills/06_fMRI_Neuroimaging/camcan-skill/scripts/extract_camcan_phenotype.py +145 -0
- package/skills/06_fMRI_Neuroimaging/camcan-skill/scripts/validate_camcan.py +141 -0
- package/skills/06_fMRI_Neuroimaging/cobre-skill/SKILL.md +201 -0
- package/skills/06_fMRI_Neuroimaging/cobre-skill/scripts/cobre_qc_summary.py +95 -0
- package/skills/06_fMRI_Neuroimaging/cobre-skill/scripts/extract_cobre_phenotype.py +104 -0
- package/skills/06_fMRI_Neuroimaging/cobre-skill/scripts/reorganize_cobre.py +140 -0
- package/skills/06_fMRI_Neuroimaging/conn-tool/SKILL.md +180 -0
- package/skills/06_fMRI_Neuroimaging/dcm2nii/SKILL.md +189 -0
- package/skills/06_fMRI_Neuroimaging/dmt-har-med-skill/SKILL.md +183 -0
- package/skills/06_fMRI_Neuroimaging/dmt-har-med-skill/scripts/dmt_har_med_qc_summary.py +96 -0
- package/skills/06_fMRI_Neuroimaging/dmt-har-med-skill/scripts/extract_dmt_har_med_phenotype.py +121 -0
- package/skills/06_fMRI_Neuroimaging/dmt-har-med-skill/scripts/reorganize_dmt_har_med.py +125 -0
- package/skills/06_fMRI_Neuroimaging/dwi-skill/SKILL.md +359 -0
- package/skills/06_fMRI_Neuroimaging/fmri-skill/SKILL.md +371 -0
- package/skills/06_fMRI_Neuroimaging/fmriprep-tool/SKILL.md +228 -0
- package/skills/06_fMRI_Neuroimaging/freesurfer-tool/SKILL.md +286 -0
- package/skills/06_fMRI_Neuroimaging/freesurfer-tool/scripts/freesurfer_processor.py +145 -0
- package/skills/06_fMRI_Neuroimaging/fsl-tool/SKILL.md +208 -0
- package/skills/06_fMRI_Neuroimaging/hbn-skill/SKILL.md +271 -0
- package/skills/06_fMRI_Neuroimaging/hbn-skill/scripts/extract_hbn_phenotype.py +107 -0
- package/skills/06_fMRI_Neuroimaging/hbn-skill/scripts/hbn_qc_summary.py +96 -0
- package/skills/06_fMRI_Neuroimaging/hbn-skill/scripts/reorganize_hbn.py +150 -0
- package/skills/06_fMRI_Neuroimaging/hcpa-skill/SKILL.md +210 -0
- package/skills/06_fMRI_Neuroimaging/hcpa-skill/scripts/extract_hcpa_phenotype.py +146 -0
- package/skills/06_fMRI_Neuroimaging/hcpa-skill/scripts/hcpa_qc_summary.py +120 -0
- package/skills/06_fMRI_Neuroimaging/hcpa-skill/scripts/reorganize_hcpa.py +155 -0
- package/skills/06_fMRI_Neuroimaging/hcpd-skill/SKILL.md +210 -0
- package/skills/06_fMRI_Neuroimaging/hcpd-skill/scripts/extract_hcpd_phenotype.py +148 -0
- package/skills/06_fMRI_Neuroimaging/hcpd-skill/scripts/hcpd_qc_summary.py +125 -0
- package/skills/06_fMRI_Neuroimaging/hcpd-skill/scripts/reorganize_hcpd.py +146 -0
- package/skills/06_fMRI_Neuroimaging/hcpep-skill/SKILL.md +215 -0
- package/skills/06_fMRI_Neuroimaging/hcpep-skill/scripts/extract_hcpep_phenotype.py +157 -0
- package/skills/06_fMRI_Neuroimaging/hcpep-skill/scripts/hcpep_qc_summary.py +143 -0
- package/skills/06_fMRI_Neuroimaging/hcpep-skill/scripts/reorganize_hcpep.py +146 -0
- package/skills/06_fMRI_Neuroimaging/hcppipeline-tool/SKILL.md +217 -0
- package/skills/06_fMRI_Neuroimaging/hcpya-skill/SKILL.md +214 -0
- package/skills/06_fMRI_Neuroimaging/hcpya-skill/scripts/extract_hcpya_phenotype.py +190 -0
- package/skills/06_fMRI_Neuroimaging/hcpya-skill/scripts/hcpya_qc_summary.py +152 -0
- package/skills/06_fMRI_Neuroimaging/hcpya-skill/scripts/reorganize_hcpya.py +203 -0
- package/skills/06_fMRI_Neuroimaging/ixi-skill/SKILL.md +198 -0
- package/skills/06_fMRI_Neuroimaging/ixi-skill/scripts/ixi_qc_summary.py +137 -0
- package/skills/06_fMRI_Neuroimaging/ixi-skill/scripts/reorganize_ixi.py +190 -0
- package/skills/06_fMRI_Neuroimaging/mnd-skill/SKILL.md +191 -0
- package/skills/06_fMRI_Neuroimaging/mnd-skill/scripts/extract_mnd_phenotype.py +143 -0
- package/skills/06_fMRI_Neuroimaging/mnd-skill/scripts/mnd_qc_summary.py +120 -0
- package/skills/06_fMRI_Neuroimaging/mnd-skill/scripts/validate_mnd.py +107 -0
- package/skills/06_fMRI_Neuroimaging/mschallenge-skill/SKILL.md +203 -0
- package/skills/06_fMRI_Neuroimaging/mschallenge-skill/scripts/analyze_lesions.py +119 -0
- package/skills/06_fMRI_Neuroimaging/mschallenge-skill/scripts/longitudinal_lesion.py +148 -0
- package/skills/06_fMRI_Neuroimaging/mschallenge-skill/scripts/mschallenge_qc_summary.py +132 -0
- package/skills/06_fMRI_Neuroimaging/mschallenge-skill/scripts/validate_mschallenge.py +116 -0
- package/skills/06_fMRI_Neuroimaging/nibabel-skill/SKILL.md +184 -0
- package/skills/06_fMRI_Neuroimaging/nibabel-skill/scripts/atlas_coordinate_reference.py +61 -0
- package/skills/06_fMRI_Neuroimaging/nibabel-skill/scripts/freesurfer_io_reference.py +34 -0
- package/skills/06_fMRI_Neuroimaging/nibabel-skill/scripts/nifti_inspection_reference.py +35 -0
- package/skills/06_fMRI_Neuroimaging/nifd-skill/SKILL.md +205 -0
- package/skills/06_fMRI_Neuroimaging/nifd-skill/scripts/extract_nifd_phenotype.py +132 -0
- package/skills/06_fMRI_Neuroimaging/nifd-skill/scripts/nifd_qc_summary.py +111 -0
- package/skills/06_fMRI_Neuroimaging/nifd-skill/scripts/validate_nifd.py +111 -0
- package/skills/06_fMRI_Neuroimaging/nii2dcm/SKILL.md +143 -0
- package/skills/06_fMRI_Neuroimaging/nilearn-tool/SKILL.md +266 -0
- package/skills/06_fMRI_Neuroimaging/nilearn-tool/scripts/connectome_reference.py +65 -0
- package/skills/06_fMRI_Neuroimaging/nilearn-tool/scripts/denoise_timeseries_reference.py +58 -0
- package/skills/06_fMRI_Neuroimaging/nilearn-tool/scripts/hierarchical_parcellation_reference.py +53 -0
- package/skills/06_fMRI_Neuroimaging/nilearn-tool/scripts/kmeans_parcellation_reference.py +53 -0
- package/skills/06_fMRI_Neuroimaging/nilearn-tool/scripts/preprocess_bold_reference.py +76 -0
- package/skills/06_fMRI_Neuroimaging/nilearn-tool/scripts/rest_dictlearning_reference.py +56 -0
- package/skills/06_fMRI_Neuroimaging/nilearn-tool/scripts/rest_ica_reference.py +59 -0
- package/skills/06_fMRI_Neuroimaging/nilearn-tool/scripts/second_level_glm_reference.py +58 -0
- package/skills/06_fMRI_Neuroimaging/nilearn-tool/scripts/spacenet_classifier_reference.py +59 -0
- package/skills/06_fMRI_Neuroimaging/nilearn-tool/scripts/svm_classifier_reference.py +60 -0
- package/skills/06_fMRI_Neuroimaging/nilearn-tool/scripts/task_glm_reference.py +63 -0
- package/skills/06_fMRI_Neuroimaging/nilearn-tool/scripts/zalff_summary_reference.py +109 -0
- package/skills/06_fMRI_Neuroimaging/nsd-skill/SKILL.md +210 -0
- package/skills/06_fMRI_Neuroimaging/nsd-skill/scripts/extract_nsd_stimulus.py +171 -0
- package/skills/06_fMRI_Neuroimaging/nsd-skill/scripts/nsd_qc_summary.py +142 -0
- package/skills/06_fMRI_Neuroimaging/nsd-skill/scripts/validate_nsd.py +142 -0
- package/skills/06_fMRI_Neuroimaging/oasis-skill/SKILL.md +205 -0
- package/skills/06_fMRI_Neuroimaging/oasis-skill/scripts/extract_oasis_phenotype.py +126 -0
- package/skills/06_fMRI_Neuroimaging/oasis-skill/scripts/oasis_qc_summary.py +115 -0
- package/skills/06_fMRI_Neuroimaging/oasis-skill/scripts/validate_oasis.py +119 -0
- package/skills/06_fMRI_Neuroimaging/pet-skill/SKILL.md +173 -0
- package/skills/06_fMRI_Neuroimaging/pet-skill/scripts/compute_suvr.py +202 -0
- package/skills/06_fMRI_Neuroimaging/pnc-skill/SKILL.md +206 -0
- package/skills/06_fMRI_Neuroimaging/pnc-skill/scripts/extract_pnc_phenotype.py +136 -0
- package/skills/06_fMRI_Neuroimaging/pnc-skill/scripts/pnc_qc_summary.py +116 -0
- package/skills/06_fMRI_Neuroimaging/pnc-skill/scripts/validate_pnc.py +120 -0
- package/skills/06_fMRI_Neuroimaging/ppmi-skill/SKILL.md +209 -0
- package/skills/06_fMRI_Neuroimaging/ppmi-skill/scripts/extract_ppmi_phenotype.py +138 -0
- package/skills/06_fMRI_Neuroimaging/ppmi-skill/scripts/ppmi_qc_summary.py +111 -0
- package/skills/06_fMRI_Neuroimaging/ppmi-skill/scripts/validate_ppmi.py +117 -0
- package/skills/06_fMRI_Neuroimaging/qsiprep-tool/SKILL.md +320 -0
- package/skills/06_fMRI_Neuroimaging/rest-mneta-mdd-skill/SKILL.md +215 -0
- package/skills/06_fMRI_Neuroimaging/rest-mneta-mdd-skill/scripts/extract_rest_mdd_phenotype.py +132 -0
- package/skills/06_fMRI_Neuroimaging/rest-mneta-mdd-skill/scripts/harmonize_sites.py +152 -0
- package/skills/06_fMRI_Neuroimaging/rest-mneta-mdd-skill/scripts/rest_mdd_qc_summary.py +124 -0
- package/skills/06_fMRI_Neuroimaging/rest-mneta-mdd-skill/scripts/validate_rest_mdd.py +103 -0
- package/skills/06_fMRI_Neuroimaging/smri-skill/SKILL.md +302 -0
- package/skills/06_fMRI_Neuroimaging/tcp-skill/SKILL.md +204 -0
- package/skills/06_fMRI_Neuroimaging/tcp-skill/scripts/extract_tcp_phenotype.py +139 -0
- package/skills/06_fMRI_Neuroimaging/tcp-skill/scripts/tcp_qc_summary.py +111 -0
- package/skills/06_fMRI_Neuroimaging/tcp-skill/scripts/validate_tcp.py +99 -0
- package/skills/06_fMRI_Neuroimaging/ucla-cnp-skill/SKILL.md +217 -0
- package/skills/06_fMRI_Neuroimaging/ucla-cnp-skill/scripts/extract_ucla_cnp_phenotype.py +145 -0
- package/skills/06_fMRI_Neuroimaging/ucla-cnp-skill/scripts/ucla_cnp_qc_summary.py +111 -0
- package/skills/06_fMRI_Neuroimaging/ucla-cnp-skill/scripts/validate_ucla_cnp.py +113 -0
- package/skills/06_fMRI_Neuroimaging/ukb-skill/SKILL.md +310 -0
- package/skills/06_fMRI_Neuroimaging/ukb-skill/scripts/build_ukb_survival.py +210 -0
- package/skills/06_fMRI_Neuroimaging/ukb-skill/scripts/extract_ukb_cases.py +308 -0
- package/skills/06_fMRI_Neuroimaging/ukb-skill/scripts/extract_ukb_phenotype.py +232 -0
- package/skills/06_fMRI_Neuroimaging/ukb-skill/scripts/ukb_qc_summary.py +158 -0
- package/skills/06_fMRI_Neuroimaging/wmh-segmentation/SKILL.md +133 -0
- package/skills/07_Computational_Modeling/detrending/SKILL.md +118 -0
- package/skills/07_Computational_Modeling/dictlearning/SKILL.md +122 -0
- package/skills/07_Computational_Modeling/filtering/SKILL.md +121 -0
- package/skills/07_Computational_Modeling/glm/SKILL.md +153 -0
- package/skills/07_Computational_Modeling/hierarchical/SKILL.md +121 -0
- package/skills/07_Computational_Modeling/ica/SKILL.md +122 -0
- package/skills/07_Computational_Modeling/kmeans/SKILL.md +119 -0
- package/skills/07_Computational_Modeling/run_models/SKILL.md +427 -0
- package/skills/07_Computational_Modeling/spacenet/SKILL.md +122 -0
- package/skills/07_Computational_Modeling/svm/SKILL.md +120 -0
- package/skills/08_Computational_Neuroscience/brain_gnn/SKILL.md +183 -0
- package/skills/08_Computational_Neuroscience/dipy-tool/SKILL.md +239 -0
- package/skills/08_Computational_Neuroscience/dipy-tool/scripts/dti_metrics_reference.py +70 -0
- package/skills/08_Computational_Neuroscience/dipy-tool/scripts/load_and_mask_reference.py +76 -0
- package/skills/08_Computational_Neuroscience/dipy-tool/scripts/roi_stats_reference.py +59 -0
- package/skills/08_Computational_Neuroscience/fm_app/SKILL.md +195 -0
- package/skills/08_Computational_Neuroscience/neurostorm/SKILL.md +151 -0
- package/skills/13_Visualization/brain-visualization/SKILL.md +191 -0
- package/skills/13_Visualization/brain-visualization/scripts/connectome_reference.py +108 -0
- package/skills/13_Visualization/brain-visualization/scripts/freesurfer_ply_reference.py +54 -0
- package/skills/13_Visualization/brain-visualization/scripts/zalff_summary_reference.py +116 -0
- package/skills/13_Visualization/ethoclaw-paper-figure-layout/SKILL.md +78 -0
- package/skills/13_Visualization/ethoclaw-paper-figure-layout/assets/naturecomm_figures.tex +74 -0
- package/skills/13_Visualization/ethoclaw-paper-figure-layout/scripts/layout_results_foldered.py +579 -0
- package/skills/14_Writing/overleaf-skill/SKILL.md +184 -0
- package/skills/14_Writing/overleaf-skill/scripts/install.sh +30 -0
- package/skills/14_Writing/paper-writing/SKILL.md +146 -0
- package/skills/14_Writing/paper-writing/scripts/data_statement_templates.py +164 -0
- package/skills/14_Writing/paper-writing/scripts/figure_templates.py +315 -0
- package/skills/14_Writing/paper-writing/scripts/nature_figure_style.py +214 -0
- package/skills/14_Writing/paper-writing/scripts/section_phrasebank.py +246 -0
- package/skills/16_Animal_Behavior/deeplabcut/SKILL.md +154 -0
- package/skills/16_Animal_Behavior/deeplabcut/references/3d-pose.md +89 -0
- package/skills/16_Animal_Behavior/deeplabcut/references/maDLC.md +123 -0
- package/skills/16_Animal_Behavior/deeplabcut/references/modelzoo.md +98 -0
- package/skills/16_Animal_Behavior/deeplabcut/references/standard-pipeline.md +165 -0
- package/skills/16_Animal_Behavior/deeplabcut/references/utilities.md +146 -0
- package/skills/16_Animal_Behavior/ethoclaw-analysis-report/SKILL.md +274 -0
- package/skills/16_Animal_Behavior/ethoclaw-analysis-report/assets/report_template_en.html +112 -0
- package/skills/16_Animal_Behavior/ethoclaw-analysis-report/assets/report_template_en.md +21 -0
- package/skills/16_Animal_Behavior/ethoclaw-analysis-report/assets/section_templates/cluster-section.md +5 -0
- package/skills/16_Animal_Behavior/ethoclaw-analysis-report/assets/section_templates/heatmap-section.md +5 -0
- package/skills/16_Animal_Behavior/ethoclaw-analysis-report/assets/section_templates/integrated-interpretation.md +3 -0
- package/skills/16_Animal_Behavior/ethoclaw-analysis-report/assets/section_templates/overview.md +3 -0
- package/skills/16_Animal_Behavior/ethoclaw-analysis-report/assets/section_templates/project-summary.md +3 -0
- package/skills/16_Animal_Behavior/ethoclaw-analysis-report/assets/section_templates/radar-section.md +5 -0
- package/skills/16_Animal_Behavior/ethoclaw-analysis-report/assets/section_templates/raw-trajectory.md +3 -0
- package/skills/16_Animal_Behavior/ethoclaw-analysis-report/assets/section_templates/sample-check.md +3 -0
- package/skills/16_Animal_Behavior/ethoclaw-analysis-report/assets/section_templates/single-subject-section.md +3 -0
- package/skills/16_Animal_Behavior/ethoclaw-analysis-report/assets/section_templates/stats-section.md +5 -0
- package/skills/16_Animal_Behavior/ethoclaw-analysis-report/references/experiment-types/epm.md +52 -0
- package/skills/16_Animal_Behavior/ethoclaw-analysis-report/references/experiment-types/fst.md +37 -0
- package/skills/16_Animal_Behavior/ethoclaw-analysis-report/references/experiment-types/nor.md +39 -0
- package/skills/16_Animal_Behavior/ethoclaw-analysis-report/references/experiment-types/oft.md +43 -0
- package/skills/16_Animal_Behavior/ethoclaw-analysis-report/references/experiment-types/tcst.md +45 -0
- package/skills/16_Animal_Behavior/ethoclaw-analysis-report/references/experiment-types/tst.md +36 -0
- package/skills/16_Animal_Behavior/ethoclaw-analysis-report/references/input-types.md +59 -0
- package/skills/16_Animal_Behavior/ethoclaw-analysis-report/references/interpretation-guardrails.md +45 -0
- package/skills/16_Animal_Behavior/ethoclaw-analysis-report/references/metadata-schema.md +57 -0
- package/skills/16_Animal_Behavior/ethoclaw-analysis-report/references/report-sections.md +86 -0
- package/skills/16_Animal_Behavior/ethoclaw-analysis-report/references/section-selection-rules.md +169 -0
- package/skills/16_Animal_Behavior/ethoclaw-analysis-report/scripts/build_report_manifest.py +27 -0
- package/skills/16_Animal_Behavior/ethoclaw-analysis-report/scripts/render_report.py +34 -0
- package/skills/16_Animal_Behavior/ethoclaw-analysis-report/scripts/report_utils.py +1121 -0
- package/skills/16_Animal_Behavior/ethoclaw-animal-grounding/SKILL.md +390 -0
- package/skills/16_Animal_Behavior/ethoclaw-animal-grounding/reference_code.py +98 -0
- package/skills/16_Animal_Behavior/ethoclaw-animal-pose-estimation/SKILL.md +336 -0
- package/skills/16_Animal_Behavior/ethoclaw-kinematic-parameter-generator/README.md +21 -0
- package/skills/16_Animal_Behavior/ethoclaw-kinematic-parameter-generator/SKILL.md +41 -0
- package/skills/16_Animal_Behavior/ethoclaw-kinematic-parameter-generator/batch_kinematic_generator.py +663 -0
- package/skills/16_Animal_Behavior/ethoclaw-kinematic-parameter-generator/config.json +19 -0
- package/skills/16_Animal_Behavior/ethoclaw-kinematic-parameter-generator/generate_kinematic_parameter.py +401 -0
- package/skills/16_Animal_Behavior/ethoclaw-kinematic-parameter-generator/kinematic_generator.py +265 -0
- package/skills/16_Animal_Behavior/ethoclaw-multiparameter-clustermap-generate/SKILL.md +72 -0
- package/skills/16_Animal_Behavior/ethoclaw-multiparameter-clustermap-generate/references/config.example.toml +56 -0
- package/skills/16_Animal_Behavior/ethoclaw-multiparameter-clustermap-generate/scripts/cluster_all_params.py +232 -0
- package/skills/16_Animal_Behavior/ethoclaw-multiparameter-clustermap-generate/scripts/cluster_all_params_from_config.py +236 -0
- package/skills/16_Animal_Behavior/ethoclaw-multiparameter-radar-generate/SKILL.md +68 -0
- package/skills/16_Animal_Behavior/ethoclaw-multiparameter-radar-generate/references/notes.md +5 -0
- package/skills/16_Animal_Behavior/ethoclaw-multiparameter-radar-generate/scripts/plot_h5_radar.py +513 -0
- package/skills/16_Animal_Behavior/ethoclaw-multiparameter-violin-stats-generate/SKILL.md +52 -0
- package/skills/16_Animal_Behavior/ethoclaw-multiparameter-violin-stats-generate/config.toml +81 -0
- package/skills/16_Animal_Behavior/ethoclaw-multiparameter-violin-stats-generate/references/stats-rule.md +18 -0
- package/skills/16_Animal_Behavior/ethoclaw-multiparameter-violin-stats-generate/scripts/h5_inspect.py +79 -0
- package/skills/16_Animal_Behavior/ethoclaw-multiparameter-violin-stats-generate/scripts/h5_violin_batch.py +624 -0
- package/skills/16_Animal_Behavior/ethoclaw-multiparameter-violin-stats-generate/scripts/h5_violin_stats.py +438 -0
- package/skills/16_Animal_Behavior/ethoclaw-trajectory-velocity-heatmap-generate/SKILL.md +280 -0
- package/skills/16_Animal_Behavior/ethoclaw-trajectory-velocity-heatmap-generate/core_scripts/heatmap_trajectory.py +790 -0
- package/skills/16_Animal_Behavior/ethoclaw-trajectory-velocity-heatmap-generate/core_scripts/heatmap_velocity.py +855 -0
- package/skills/16_Animal_Behavior/ethoclaw-trajectory-velocity-heatmap-generate/reference_data/reference_2d.csv +101 -0
- package/skills/16_Animal_Behavior/ethoclaw-trajectory-velocity-heatmap-generate/reference_data/reference_2d.h5 +0 -0
- package/skills/16_Animal_Behavior/ethoclaw-trajectory-velocity-heatmap-generate/reference_data/reference_data_readme.md +126 -0
|
@@ -0,0 +1,1121 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import csv
|
|
4
|
+
import base64
|
|
5
|
+
import html
|
|
6
|
+
import io
|
|
7
|
+
import json
|
|
8
|
+
import math
|
|
9
|
+
import mimetypes
|
|
10
|
+
import re
|
|
11
|
+
import statistics
|
|
12
|
+
from collections import Counter
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Callable
|
|
15
|
+
from urllib.parse import unquote, urlparse
|
|
16
|
+
from urllib.request import url2pathname
|
|
17
|
+
|
|
18
|
+
from PIL import Image
|
|
19
|
+
|
|
20
|
+
SKILL_ROOT = Path(__file__).resolve().parents[1]
|
|
21
|
+
ASSETS_DIR = SKILL_ROOT / "assets"
|
|
22
|
+
SECTION_DIR = ASSETS_DIR / "section_templates"
|
|
23
|
+
|
|
24
|
+
IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".svg"}
|
|
25
|
+
RASTER_IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg"}
|
|
26
|
+
MAX_EMBED_IMAGE_SIZE = (1600, 1600)
|
|
27
|
+
JPEG_QUALITY = 82
|
|
28
|
+
IGNORED_OUTPUT_DIR_PREFIXES = {"report_output"}
|
|
29
|
+
IGNORED_FILE_NAMES = {"manifest.json"}
|
|
30
|
+
KNOWN_EXPERIMENT_TYPES = ("TCST", "OFT", "TST", "EPM", "FST", "NOR")
|
|
31
|
+
OBVIOUS_GROUP_LABELS = {
|
|
32
|
+
"control",
|
|
33
|
+
"ctrl",
|
|
34
|
+
"model",
|
|
35
|
+
"sham",
|
|
36
|
+
"vehicle",
|
|
37
|
+
"treated",
|
|
38
|
+
"treatment",
|
|
39
|
+
"drug",
|
|
40
|
+
"test",
|
|
41
|
+
"ko",
|
|
42
|
+
"wt",
|
|
43
|
+
"het",
|
|
44
|
+
"tg",
|
|
45
|
+
}
|
|
46
|
+
SAMPLE_SUFFIX_PATTERNS =[
|
|
47
|
+
r"_pose$",
|
|
48
|
+
r"_region_dict$",
|
|
49
|
+
r"_stat$",
|
|
50
|
+
r"_trajectory_heatmap$",
|
|
51
|
+
r"_timeseries$",
|
|
52
|
+
r"_regional_atlas$",
|
|
53
|
+
r"_statistics_analysis_combined$",
|
|
54
|
+
r"_statistics_analysis$",
|
|
55
|
+
r"_combined$",
|
|
56
|
+
r"_heatmap$",
|
|
57
|
+
r"_trajectory$",
|
|
58
|
+
r"_atlas$",
|
|
59
|
+
r"^violin_",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
IMAGE_LINE_RE = re.compile(r"^!\[(.*?)\]\((.*?)\)$")
|
|
63
|
+
LINK_RE = re.compile(r"\[([^\]]+)\]\(([^)]+)\)")
|
|
64
|
+
CODE_RE = re.compile(r"`([^`]+)`")
|
|
65
|
+
|
|
66
|
+
SECTION_SPECS =[
|
|
67
|
+
{
|
|
68
|
+
"id": "project_summary",
|
|
69
|
+
"title": "Project Summary and Materials",
|
|
70
|
+
"section_key": "project_summary_section",
|
|
71
|
+
"body_key": "project_summary_body",
|
|
72
|
+
"template": "project-summary.md",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"id": "overview",
|
|
76
|
+
"title": "Project Overview",
|
|
77
|
+
"section_key": "overview_section",
|
|
78
|
+
"body_key": "overview_body",
|
|
79
|
+
"template": "overview.md",
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"id": "sample_check",
|
|
83
|
+
"title": "Sample and Group Check",
|
|
84
|
+
"section_key": "sample_check_section",
|
|
85
|
+
"body_key": "sample_check_body",
|
|
86
|
+
"template": "sample-check.md",
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"id": "raw_trajectory",
|
|
90
|
+
"title": "Raw Trajectory Summary",
|
|
91
|
+
"section_key": "raw_trajectory_section",
|
|
92
|
+
"body_key": "raw_trajectory_body",
|
|
93
|
+
"template": "raw-trajectory.md",
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"id": "heatmap",
|
|
97
|
+
"title": "Heatmap and Trajectory Results",
|
|
98
|
+
"section_key": "heatmap_section",
|
|
99
|
+
"body_key": "heatmap_body",
|
|
100
|
+
"template": "heatmap-section.md",
|
|
101
|
+
"gallery_key": "heatmap_gallery",
|
|
102
|
+
"gallery_list_key": "heatmap",
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"id": "radar",
|
|
106
|
+
"title": "Radar Chart Results",
|
|
107
|
+
"section_key": "radar_section",
|
|
108
|
+
"body_key": "radar_body",
|
|
109
|
+
"template": "radar-section.md",
|
|
110
|
+
"gallery_key": "radar_gallery",
|
|
111
|
+
"gallery_list_key": "radar",
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"id": "stats",
|
|
115
|
+
"title": "Statistics and Summary Chart Results",
|
|
116
|
+
"section_key": "stats_section",
|
|
117
|
+
"body_key": "stats_body",
|
|
118
|
+
"template": "stats-section.md",
|
|
119
|
+
"gallery_key": "stats_gallery",
|
|
120
|
+
"gallery_list_key": "stats",
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"id": "cluster",
|
|
124
|
+
"title": "Clustering Results",
|
|
125
|
+
"section_key": "cluster_section",
|
|
126
|
+
"body_key": "cluster_body",
|
|
127
|
+
"template": "cluster-section.md",
|
|
128
|
+
"gallery_key": "cluster_gallery",
|
|
129
|
+
"gallery_list_key": "cluster",
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"id": "single_subject",
|
|
133
|
+
"title": "Single Subject Results Overview",
|
|
134
|
+
"section_key": "single_subject_section",
|
|
135
|
+
"body_key": "single_subject_body",
|
|
136
|
+
"template": "single-subject-section.md",
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"id": "integrated_interpretation",
|
|
140
|
+
"title": "Integrated Interpretation",
|
|
141
|
+
"section_key": "integrated_interpretation_section",
|
|
142
|
+
"body_key": "integrated_interpretation_body",
|
|
143
|
+
"template": "integrated-interpretation.md",
|
|
144
|
+
},
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
SECTION_GUIDANCE = {
|
|
148
|
+
"project_summary_body": {
|
|
149
|
+
"purpose": "Compress the project path, core materials, current analyzable scope, and report mode into a short section to avoid piling up too much meta-information at the beginning of the report.",
|
|
150
|
+
"write_when": "Always fill in.",
|
|
151
|
+
"source_fields":[
|
|
152
|
+
"project_path",
|
|
153
|
+
"facts.project_path_confirmation",
|
|
154
|
+
"facts.input_completeness",
|
|
155
|
+
"facts.materials_inventory",
|
|
156
|
+
"report_mode",
|
|
157
|
+
"report_mode_reason",
|
|
158
|
+
],
|
|
159
|
+
"rules":[
|
|
160
|
+
"Keep it within 3 to 5 sentences, prioritizing letting the user quickly know what was used, what is missing, and how far the current analysis can go.",
|
|
161
|
+
"Only name the most critical material directories or file types, do not expand into a large list.",
|
|
162
|
+
"Directly mention obvious sample counts, obvious group prefixes, and the most worthy materials for further analysis.",
|
|
163
|
+
],
|
|
164
|
+
},
|
|
165
|
+
"overview_body": {
|
|
166
|
+
"purpose": "Provide a brief project-level overview, add a sentence about the experimental purpose and basic process, and point out the most prominent result features of the current data first.",
|
|
167
|
+
"write_when": "Usually fill in; even if the background is incomplete, first summarize the main signals of the current data.",
|
|
168
|
+
"source_fields": ["facts.overview", "report_mode", "facts.unconfirmed_items"],
|
|
169
|
+
"rules":[
|
|
170
|
+
"Explain the project name, experimental paradigm, and current report purpose; the experimental paradigm can be inferred from the project name.",
|
|
171
|
+
"If the experimental paradigm is known, use 1 to 2 sentences to add what the experiment usually evaluates and what the basic process is.",
|
|
172
|
+
"First give 1 to 2 of the most noteworthy result summaries, then add what analyses the current materials support.",
|
|
173
|
+
"Limit information to being mentioned only when necessary, do not let the entire section become a method description.",
|
|
174
|
+
],
|
|
175
|
+
},
|
|
176
|
+
"sample_check_body": {
|
|
177
|
+
"purpose": "Check sample counts, sample IDs, and group information, and directly write out candidate groups when grouping is very obvious.",
|
|
178
|
+
"write_when": "Always fill in.",
|
|
179
|
+
"source_fields":["facts.sample_check", "facts.group_inference", "scan.detected"],
|
|
180
|
+
"rules":[
|
|
181
|
+
"First write the total sample count and sample ID range.",
|
|
182
|
+
"If obvious groupings like control, model, sham, vehicle appear in the file name prefixes, they can be directly written out as candidate groups.",
|
|
183
|
+
"For obvious labels, it is allowed to treat control as a candidate control group; only keep it as 'to be confirmed' when encountering ambiguous abbreviations.",
|
|
184
|
+
],
|
|
185
|
+
},
|
|
186
|
+
"raw_trajectory_body": {
|
|
187
|
+
"purpose": "When only raw skeleton or trajectory data is available, provide a simple summary of behavioral regions and activity patterns based on coordinate distribution and path length.",
|
|
188
|
+
"write_when": "Fill in only when the manifest provides a raw trajectory summary.",
|
|
189
|
+
"source_fields":["facts.raw_trajectory_summary", "facts.group_inference", "facts.overview"],
|
|
190
|
+
"rules":[
|
|
191
|
+
"First write which raw trajectory files or coordinate sources were used, then write the most obvious activity region and movement range features.",
|
|
192
|
+
"Multi-sample projects prioritize comparing the most intuitive differences between groups or samples, such as activity range, principal axis distribution, and path length.",
|
|
193
|
+
"When the experimental paradigm is known, the common readout language of that paradigm can be used directly; if the apparatus orientation is unknown, describe the longitudinal/transverse principal axis or center region, and do not force naming open and closed arms.",
|
|
194
|
+
],
|
|
195
|
+
},
|
|
196
|
+
"heatmap_body": {
|
|
197
|
+
"purpose": "Describe the spatial distribution and behavioral patterns shown in heatmaps, trajectory maps, atlases, or time series graphs.",
|
|
198
|
+
"write_when": "Fill in only when the manifest contains a heatmap gallery.",
|
|
199
|
+
"source_fields":["galleries.heatmap", "scan.figure_files", "facts.overview"],
|
|
200
|
+
"rules":[
|
|
201
|
+
"First name which graphs were used, then describe the observed distribution or trajectory features.",
|
|
202
|
+
"Prioritize summarizing the most intuitive and obvious spatial distribution or trajectory features.",
|
|
203
|
+
"When the experimental paradigm is known, directly combine regional meanings to write a concise summary.",
|
|
204
|
+
],
|
|
205
|
+
},
|
|
206
|
+
"radar_body": {
|
|
207
|
+
"purpose": "Summarize the multi-indicator profiles reflected in the radar charts.",
|
|
208
|
+
"write_when": "Fill in only when the manifest contains a radar gallery.",
|
|
209
|
+
"source_fields": ["galleries.radar", "scan.figure_files", "facts.sample_check"],
|
|
210
|
+
"rules":[
|
|
211
|
+
"Specify whether the chart compares single samples or multi-group profiles.",
|
|
212
|
+
"Prioritize summarizing the most prominent high/low features and profile differences.",
|
|
213
|
+
"When there is no reliable grouping, do not write formal inter-group comparisons; but patterns at the single sample or candidate label level can be normally summarized.",
|
|
214
|
+
],
|
|
215
|
+
},
|
|
216
|
+
"stats_body": {
|
|
217
|
+
"purpose": "Summarize comparison results supported by statistical tables or explicit statistical charts; single-sample projects can also write numerical summaries.",
|
|
218
|
+
"write_when": "Fill in when the manifest contains a stats gallery; single-sample projects write statistical summaries, and multi-sample projects write comparison results when there is sufficient basis.",
|
|
219
|
+
"source_fields":["galleries.stats", "scan.data_files", "facts.sample_check"],
|
|
220
|
+
"rules":[
|
|
221
|
+
"First write which tables or charts the statistical basis comes from, then write the results.",
|
|
222
|
+
"Do not write significance conclusions when there are no statistical tables.",
|
|
223
|
+
"Single-sample projects can directly summarize the main numerical features, regional preferences, or behavioral trends.",
|
|
224
|
+
"When group meanings are not fully confirmed, do not write formal mechanistic comparisons, but original difference directions can be summarized based on obvious group names.",
|
|
225
|
+
],
|
|
226
|
+
},
|
|
227
|
+
"cluster_body": {
|
|
228
|
+
"purpose": "Describe the sample or indicator structure presented in the clustering charts.",
|
|
229
|
+
"write_when": "Fill in only when the manifest contains a cluster gallery.",
|
|
230
|
+
"source_fields":["galleries.cluster", "scan.figure_files", "facts.sample_check"],
|
|
231
|
+
"rules":[
|
|
232
|
+
"Specify whether the clustering object is samples or indicators.",
|
|
233
|
+
"Only describe structural proximity or separation trends.",
|
|
234
|
+
"Do not write clustering separation directly as statistically significant differences.",
|
|
235
|
+
],
|
|
236
|
+
},
|
|
237
|
+
"single_subject_body": {
|
|
238
|
+
"purpose": "In single-sample mode, summarize the core results of a single individual or single record.",
|
|
239
|
+
"write_when": "Fill in only when report_mode is single-subject.",
|
|
240
|
+
"source_fields":["facts.single_subject_stats", "facts.raw_trajectory_summary", "scan.figure_files", "galleries.heatmap"],
|
|
241
|
+
"rules":[
|
|
242
|
+
"Prioritize covering total duration, effective detection duration, total distance, and regional stays and entries.",
|
|
243
|
+
"If there is only raw skeleton data, also write out the most prominent activity region or movement pattern of this sample based on the trajectory distribution.",
|
|
244
|
+
"First give a main single-sample summary sentence, then expand on key indicators.",
|
|
245
|
+
"Do not write single-sample phenomena as population laws, but clearly write out the most prominent behavioral features of this sample.",
|
|
246
|
+
],
|
|
247
|
+
},
|
|
248
|
+
"integrated_interpretation_body": {
|
|
249
|
+
"purpose": "Integrate multiple result sources across chart types to form a direct and clear comprehensive summary.",
|
|
250
|
+
"write_when": "Fill in only when at least two types of evidence sources exist simultaneously.",
|
|
251
|
+
"source_fields":["galleries", "facts.overview", "facts.raw_trajectory_summary", "facts.unconfirmed_items", "report_mode"],
|
|
252
|
+
"rules":[
|
|
253
|
+
"Clarify which chart types, statistics, or raw trajectory sources are integrated.",
|
|
254
|
+
"First write a direct comprehensive conclusion, then supplement with the charts and data supporting it.",
|
|
255
|
+
"Separate observed facts from explanations, but do not write the entire section as a disclaimer.",
|
|
256
|
+
"Do not write mechanistic or causal conclusions when not authorized.",
|
|
257
|
+
],
|
|
258
|
+
},
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def ensure_project_path(project_path: str | Path) -> Path:
|
|
263
|
+
path = Path(project_path).expanduser().resolve()
|
|
264
|
+
if not path.exists() or not path.is_dir():
|
|
265
|
+
raise FileNotFoundError(f"Project path does not exist or is not a directory: {path}")
|
|
266
|
+
return path
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def read_text(path: Path) -> str:
|
|
270
|
+
return path.read_text(encoding="utf-8-sig")
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def write_text(path: Path, content: str) -> None:
|
|
274
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
275
|
+
path.write_text(content, encoding="utf-8")
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def load_json(path: Path) -> Any:
|
|
279
|
+
return json.loads(read_text(path))
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def save_json(path: Path, payload: Any) -> None:
|
|
283
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
284
|
+
path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def relative_posix(path: Path, root: Path) -> str:
|
|
288
|
+
return path.relative_to(root).as_posix()
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def classify_file(path: Path) -> tuple[str, str]:
|
|
292
|
+
name = path.name.lower()
|
|
293
|
+
ext = path.suffix.lower()
|
|
294
|
+
parent_text = " ".join(part.lower() for part in path.parts)
|
|
295
|
+
|
|
296
|
+
if ext in {".yaml", ".yml", ".json"}:
|
|
297
|
+
if "stat" in name or "summary" in name:
|
|
298
|
+
return "data", "stats_json"
|
|
299
|
+
return "metadata", "metadata"
|
|
300
|
+
if ext == ".h5":
|
|
301
|
+
return "data", "skeleton_data"
|
|
302
|
+
if ext in {".csv", ".tsv", ".xlsx", ".xls"}:
|
|
303
|
+
if any(token in name for token in ["pose", "skeleton", "keypoint"]) or "skeleton" in parent_text:
|
|
304
|
+
return "data", "skeleton_data"
|
|
305
|
+
if any(token in name for token in["region_dict", "behavior", "summary"]):
|
|
306
|
+
return "data", "behavior_summary"
|
|
307
|
+
if any(token in name for token in ["stats", "stat", "pairwise", "overall", "kruskal"]):
|
|
308
|
+
return "data", "stats_table"
|
|
309
|
+
return "data", "table"
|
|
310
|
+
if ext in IMAGE_EXTENSIONS:
|
|
311
|
+
if any(token in name for token in["clustermap", "cluster", "dendrogram"]):
|
|
312
|
+
return "figure", "cluster"
|
|
313
|
+
if "radar" in name:
|
|
314
|
+
return "figure", "radar"
|
|
315
|
+
if any(token in name for token in ["heatmap", "trajectory"]):
|
|
316
|
+
return "figure", "heatmap"
|
|
317
|
+
if any(token in name for token in["violin", "boxplot", "statistics_analysis_combined", "statistics"]):
|
|
318
|
+
return "figure", "stats_figure"
|
|
319
|
+
if "timeseries" in name:
|
|
320
|
+
return "figure", "timeseries"
|
|
321
|
+
if "atlas" in name:
|
|
322
|
+
return "figure", "atlas"
|
|
323
|
+
return "figure", "other_figure"
|
|
324
|
+
return "other", ext.lstrip(".") or "no_extension"
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def normalize_sample_id(stem: str) -> str:
|
|
328
|
+
sample = stem
|
|
329
|
+
for pattern in SAMPLE_SUFFIX_PATTERNS:
|
|
330
|
+
sample = re.sub(pattern, "", sample, flags=re.IGNORECASE)
|
|
331
|
+
sample = re.sub(r"__[^_]+__[^_]+__p\d+$", "", sample, flags=re.IGNORECASE)
|
|
332
|
+
return sample.strip("_- ") or stem
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def infer_experiment_type(project_path: Path, stats_payload: dict[str, Any] | None) -> str | None:
|
|
336
|
+
analysis_type = str((stats_payload or {}).get("analysis_type") or "").strip()
|
|
337
|
+
if analysis_type:
|
|
338
|
+
upper = analysis_type.upper()
|
|
339
|
+
if upper in KNOWN_EXPERIMENT_TYPES:
|
|
340
|
+
return upper
|
|
341
|
+
|
|
342
|
+
candidates =[project_path.name, str(project_path)]
|
|
343
|
+
pattern_map = {name: re.compile(rf"(?<![A-Za-z]){name}(?![A-Za-z])", re.IGNORECASE) for name in KNOWN_EXPERIMENT_TYPES}
|
|
344
|
+
for candidate in candidates:
|
|
345
|
+
for name, pattern in pattern_map.items():
|
|
346
|
+
if pattern.search(candidate):
|
|
347
|
+
return name
|
|
348
|
+
return None
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def infer_obvious_groups(sample_ids: list[str]) -> dict[str, Any]:
|
|
352
|
+
sample_to_group: dict[str, str] = {}
|
|
353
|
+
counts: Counter[str] = Counter()
|
|
354
|
+
for sample_id in sample_ids:
|
|
355
|
+
match = re.match(r"^([A-Za-z]+)", sample_id)
|
|
356
|
+
if not match:
|
|
357
|
+
continue
|
|
358
|
+
label = match.group(1).lower()
|
|
359
|
+
if label not in OBVIOUS_GROUP_LABELS:
|
|
360
|
+
continue
|
|
361
|
+
sample_to_group[sample_id] = label
|
|
362
|
+
counts[label] += 1
|
|
363
|
+
|
|
364
|
+
valid_labels = sorted(label for label, count in counts.items() if count >= 2)
|
|
365
|
+
if len(valid_labels) >= 2:
|
|
366
|
+
filtered_counts = {label: counts[label] for label in valid_labels}
|
|
367
|
+
filtered_sample_map = {sample_id: label for sample_id, label in sample_to_group.items() if label in valid_labels}
|
|
368
|
+
control_group = "control" if "control" in filtered_counts else None
|
|
369
|
+
return {
|
|
370
|
+
"status": "inferred",
|
|
371
|
+
"method": "filename-prefix",
|
|
372
|
+
"has_groups": True,
|
|
373
|
+
"labels": valid_labels,
|
|
374
|
+
"display_mapping": {label: label for label in valid_labels},
|
|
375
|
+
"group_counts": filtered_counts,
|
|
376
|
+
"sample_to_group": filtered_sample_map,
|
|
377
|
+
"control_group": control_group,
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return {
|
|
381
|
+
"status": "unknown",
|
|
382
|
+
"method": "none",
|
|
383
|
+
"has_groups": None,
|
|
384
|
+
"labels":[],
|
|
385
|
+
"display_mapping": {},
|
|
386
|
+
"group_counts": {},
|
|
387
|
+
"sample_to_group": {},
|
|
388
|
+
"control_group": None,
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def detect_track_columns(fieldnames: list[str]) -> tuple[str, str, str] | None:
|
|
393
|
+
candidates =[
|
|
394
|
+
("back_x", "back_y", "back"),
|
|
395
|
+
("nose_x", "nose_y", "nose"),
|
|
396
|
+
("tail_x", "tail_y", "tail"),
|
|
397
|
+
("x", "y", "center"),
|
|
398
|
+
]
|
|
399
|
+
available = set(fieldnames)
|
|
400
|
+
for x_key, y_key, label in candidates:
|
|
401
|
+
if x_key in available and y_key in available:
|
|
402
|
+
return x_key, y_key, label
|
|
403
|
+
return None
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def summarize_track_file(path: Path) -> dict[str, Any] | None:
|
|
407
|
+
delimiter = "\t" if path.suffix.lower() == ".tsv" else ","
|
|
408
|
+
try:
|
|
409
|
+
with path.open("r", encoding="utf-8", newline="") as handle:
|
|
410
|
+
reader = csv.DictReader(handle, delimiter=delimiter)
|
|
411
|
+
if not reader.fieldnames:
|
|
412
|
+
return None
|
|
413
|
+
track_columns = detect_track_columns(list(reader.fieldnames))
|
|
414
|
+
if not track_columns:
|
|
415
|
+
return None
|
|
416
|
+
|
|
417
|
+
x_key, y_key, point_label = track_columns
|
|
418
|
+
xs: list[float] = []
|
|
419
|
+
ys: list[float] = []
|
|
420
|
+
prev: tuple[float, float] | None = None
|
|
421
|
+
path_length = 0.0
|
|
422
|
+
for row in reader:
|
|
423
|
+
try:
|
|
424
|
+
x_value = float(row[x_key])
|
|
425
|
+
y_value = float(row[y_key])
|
|
426
|
+
except Exception:
|
|
427
|
+
continue
|
|
428
|
+
xs.append(x_value)
|
|
429
|
+
ys.append(y_value)
|
|
430
|
+
if prev is not None:
|
|
431
|
+
path_length += math.hypot(x_value - prev[0], y_value - prev[1])
|
|
432
|
+
prev = (x_value, y_value)
|
|
433
|
+
except Exception:
|
|
434
|
+
return None
|
|
435
|
+
|
|
436
|
+
if not xs or not ys:
|
|
437
|
+
return None
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
"sample_id": normalize_sample_id(path.stem),
|
|
441
|
+
"source_file": path.name,
|
|
442
|
+
"point_label": point_label,
|
|
443
|
+
"frame_count": len(xs),
|
|
444
|
+
"min_x": min(xs),
|
|
445
|
+
"max_x": max(xs),
|
|
446
|
+
"min_y": min(ys),
|
|
447
|
+
"max_y": max(ys),
|
|
448
|
+
"mean_x": statistics.fmean(xs),
|
|
449
|
+
"mean_y": statistics.fmean(ys),
|
|
450
|
+
"x_span": max(xs) - min(xs),
|
|
451
|
+
"y_span": max(ys) - min(ys),
|
|
452
|
+
"path_length": path_length,
|
|
453
|
+
"points": list(zip(xs, ys)),
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def build_raw_trajectory_summary(
|
|
458
|
+
project_path: Path,
|
|
459
|
+
scan: dict[str, Any],
|
|
460
|
+
experiment_type: str | None,
|
|
461
|
+
group_info: dict[str, Any],
|
|
462
|
+
) -> dict[str, Any] | None:
|
|
463
|
+
track_summaries: list[dict[str, Any]] = []
|
|
464
|
+
for item in scan["data_files"]:
|
|
465
|
+
rel_path = item["path"]
|
|
466
|
+
if Path(rel_path).suffix.lower() not in {".csv", ".tsv"}:
|
|
467
|
+
continue
|
|
468
|
+
summary = summarize_track_file(project_path / rel_path)
|
|
469
|
+
if not summary:
|
|
470
|
+
continue
|
|
471
|
+
sample_id = summary["sample_id"]
|
|
472
|
+
summary["group"] = group_info["sample_to_group"].get(sample_id)
|
|
473
|
+
track_summaries.append(summary)
|
|
474
|
+
|
|
475
|
+
if not track_summaries:
|
|
476
|
+
return None
|
|
477
|
+
|
|
478
|
+
point_label = track_summaries[0]["point_label"]
|
|
479
|
+
total_frames = sum(int(item["frame_count"]) for item in track_summaries)
|
|
480
|
+
group_counts = dict(group_info.get("group_counts") or {})
|
|
481
|
+
highlights =[
|
|
482
|
+
f"Read {len(track_summaries)} raw trajectory files in total, with coordinate anchor at {point_label}, accumulating approximately {total_frames} frames."
|
|
483
|
+
]
|
|
484
|
+
|
|
485
|
+
samples_by_group: dict[str, list[dict[str, Any]]] = {}
|
|
486
|
+
for summary in track_summaries:
|
|
487
|
+
group_name = summary.get("group") or "all"
|
|
488
|
+
samples_by_group.setdefault(group_name,[]).append(summary)
|
|
489
|
+
|
|
490
|
+
per_group_metrics: dict[str, Any] = {}
|
|
491
|
+
for group_name, rows in samples_by_group.items():
|
|
492
|
+
per_group_metrics[group_name] = {
|
|
493
|
+
"sample_count": len(rows),
|
|
494
|
+
"mean_path_length": statistics.fmean(item["path_length"] for item in rows),
|
|
495
|
+
"mean_x_span": statistics.fmean(item["x_span"] for item in rows),
|
|
496
|
+
"mean_y_span": statistics.fmean(item["y_span"] for item in rows),
|
|
497
|
+
"top_samples_by_path": [
|
|
498
|
+
{
|
|
499
|
+
"sample_id": item["sample_id"],
|
|
500
|
+
"path_length": item["path_length"],
|
|
501
|
+
"x_span": item["x_span"],
|
|
502
|
+
"y_span": item["y_span"],
|
|
503
|
+
}
|
|
504
|
+
for item in sorted(rows, key=lambda item: item["path_length"], reverse=True)[:3]
|
|
505
|
+
],
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if experiment_type == "EPM":
|
|
509
|
+
all_x = [point[0] for item in track_summaries for point in item["points"]]
|
|
510
|
+
all_y = [point[1] for item in track_summaries for point in item["points"]]
|
|
511
|
+
center_x = statistics.median(all_x)
|
|
512
|
+
center_y = statistics.median(all_y)
|
|
513
|
+
arm_half_width = max(35.0, min(80.0, 0.08 * min(max(all_x) - min(all_x), max(all_y) - min(all_y))))
|
|
514
|
+
|
|
515
|
+
for item in track_summaries:
|
|
516
|
+
counts = {"center": 0, "vertical_axis": 0, "horizontal_axis": 0, "corner": 0}
|
|
517
|
+
for x_value, y_value in item["points"]:
|
|
518
|
+
in_vertical = abs(x_value - center_x) <= arm_half_width
|
|
519
|
+
in_horizontal = abs(y_value - center_y) <= arm_half_width
|
|
520
|
+
if in_vertical and in_horizontal:
|
|
521
|
+
counts["center"] += 1
|
|
522
|
+
elif in_vertical:
|
|
523
|
+
counts["vertical_axis"] += 1
|
|
524
|
+
elif in_horizontal:
|
|
525
|
+
counts["horizontal_axis"] += 1
|
|
526
|
+
else:
|
|
527
|
+
counts["corner"] += 1
|
|
528
|
+
total = max(1, len(item["points"]))
|
|
529
|
+
item["epm_axis_ratios"] = {key: counts[key] / total for key in counts}
|
|
530
|
+
|
|
531
|
+
for group_name, rows in samples_by_group.items():
|
|
532
|
+
epm_rows = [item["epm_axis_ratios"] for item in rows if "epm_axis_ratios" in item]
|
|
533
|
+
if not epm_rows:
|
|
534
|
+
continue
|
|
535
|
+
per_group_metrics[group_name]["epm_axis_ratios"] = {
|
|
536
|
+
key: statistics.fmean(row[key] for row in epm_rows)
|
|
537
|
+
for key in["center", "vertical_axis", "horizontal_axis", "corner"]
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
overall_vertical = statistics.fmean(
|
|
541
|
+
item["epm_axis_ratios"]["vertical_axis"] for item in track_summaries if "epm_axis_ratios" in item
|
|
542
|
+
)
|
|
543
|
+
overall_horizontal = statistics.fmean(
|
|
544
|
+
item["epm_axis_ratios"]["horizontal_axis"] for item in track_summaries if "epm_axis_ratios" in item
|
|
545
|
+
)
|
|
546
|
+
highlights.append(
|
|
547
|
+
f"From a rough division of the coordinate principal axes, the trajectory as a whole is more concentrated on the longitudinal principal axis passing through the center (average proportion {overall_vertical:.1%}), while the transverse principal axis proportion is about {overall_horizontal:.1%}."
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
if "control" in per_group_metrics and "model" in per_group_metrics:
|
|
551
|
+
control_metrics = per_group_metrics["control"]
|
|
552
|
+
model_metrics = per_group_metrics["model"]
|
|
553
|
+
highlights.append(
|
|
554
|
+
"When divided by file name prefixes, the control group has a larger transverse activity range, "
|
|
555
|
+
f"with an average transverse span of about {control_metrics['mean_x_span']:.1f} pixels, "
|
|
556
|
+
f"which is higher than the {model_metrics['mean_x_span']:.1f} pixels of the model group."
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
return {
|
|
560
|
+
"source": "raw-skeleton",
|
|
561
|
+
"point_label": point_label,
|
|
562
|
+
"sample_count": len(track_summaries),
|
|
563
|
+
"group_counts": group_counts,
|
|
564
|
+
"per_group_metrics": per_group_metrics,
|
|
565
|
+
"highlights": highlights,
|
|
566
|
+
"samples": [
|
|
567
|
+
{
|
|
568
|
+
"sample_id": item["sample_id"],
|
|
569
|
+
"source_file": item["source_file"],
|
|
570
|
+
"group": item.get("group"),
|
|
571
|
+
"frame_count": item["frame_count"],
|
|
572
|
+
"path_length": item["path_length"],
|
|
573
|
+
"x_span": item["x_span"],
|
|
574
|
+
"y_span": item["y_span"],
|
|
575
|
+
}
|
|
576
|
+
for item in track_summaries
|
|
577
|
+
],
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def extract_group_labels_from_csv(path: Path, limit: int = 1000) -> list[str]:
|
|
582
|
+
labels: set[str] = set()
|
|
583
|
+
if path.suffix.lower() not in {".csv", ".tsv"}:
|
|
584
|
+
return[]
|
|
585
|
+
delimiter = "\t" if path.suffix.lower() == ".tsv" else ","
|
|
586
|
+
try:
|
|
587
|
+
with path.open("r", encoding="utf-8", newline="") as handle:
|
|
588
|
+
reader = csv.DictReader(handle, delimiter=delimiter)
|
|
589
|
+
if not reader.fieldnames or "group" not in reader.fieldnames:
|
|
590
|
+
return[]
|
|
591
|
+
for index, row in enumerate(reader):
|
|
592
|
+
if index >= limit:
|
|
593
|
+
break
|
|
594
|
+
value = (row.get("group") or "").strip()
|
|
595
|
+
if value:
|
|
596
|
+
labels.add(value)
|
|
597
|
+
except Exception:
|
|
598
|
+
return[]
|
|
599
|
+
return sorted(labels)
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def render_template(template_text: str, context: dict[str, Any]) -> str:
|
|
603
|
+
def replace(match: re.Match[str]) -> str:
|
|
604
|
+
key = match.group(1).strip()
|
|
605
|
+
value = context.get(key, "")
|
|
606
|
+
if value is None:
|
|
607
|
+
return ""
|
|
608
|
+
return str(value)
|
|
609
|
+
|
|
610
|
+
return re.sub(r"\{\{\s*([^}]+?)\s*\}\}", replace, template_text)
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def render_section(template_name: str, context: dict[str, Any], enabled: bool = True) -> str:
|
|
614
|
+
if not enabled:
|
|
615
|
+
return ""
|
|
616
|
+
content = render_template(read_text(SECTION_DIR / template_name), context).strip()
|
|
617
|
+
return content if content else ""
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def inline_markdown(text: str) -> str:
|
|
621
|
+
escaped = html.escape(text)
|
|
622
|
+
escaped = LINK_RE.sub(lambda match: f'<a href="{html.escape(match.group(2), quote=True)}">{match.group(1)}</a>', escaped)
|
|
623
|
+
escaped = CODE_RE.sub(lambda match: f"<code>{html.escape(match.group(1))}</code>", escaped)
|
|
624
|
+
return escaped
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def markdown_to_html(markdown_text: str, image_src_transform: Callable[[str], str] | None = None) -> str:
|
|
628
|
+
lines = markdown_text.splitlines()
|
|
629
|
+
parts: list[str] = []
|
|
630
|
+
paragraph: list[str] =[]
|
|
631
|
+
list_items: list[str] =[]
|
|
632
|
+
|
|
633
|
+
def flush_paragraph() -> None:
|
|
634
|
+
nonlocal paragraph
|
|
635
|
+
if paragraph:
|
|
636
|
+
parts.append(f"<p>{inline_markdown(' '.join(paragraph))}</p>")
|
|
637
|
+
paragraph =[]
|
|
638
|
+
|
|
639
|
+
def flush_list() -> None:
|
|
640
|
+
nonlocal list_items
|
|
641
|
+
if list_items:
|
|
642
|
+
parts.append("<ul>")
|
|
643
|
+
for item in list_items:
|
|
644
|
+
parts.append(f"<li>{inline_markdown(item)}</li>")
|
|
645
|
+
parts.append("</ul>")
|
|
646
|
+
list_items =[]
|
|
647
|
+
|
|
648
|
+
for raw_line in lines:
|
|
649
|
+
line = raw_line.rstrip()
|
|
650
|
+
if not line:
|
|
651
|
+
flush_paragraph()
|
|
652
|
+
flush_list()
|
|
653
|
+
continue
|
|
654
|
+
image_match = IMAGE_LINE_RE.match(line.strip())
|
|
655
|
+
if image_match:
|
|
656
|
+
flush_paragraph()
|
|
657
|
+
flush_list()
|
|
658
|
+
caption = image_match.group(1).strip() or "Figure"
|
|
659
|
+
src = image_match.group(2).strip()
|
|
660
|
+
if image_src_transform:
|
|
661
|
+
src = image_src_transform(src)
|
|
662
|
+
parts.append("<figure>")
|
|
663
|
+
parts.append(f'<img src="{html.escape(src, quote=True)}" alt="{html.escape(caption, quote=True)}" />')
|
|
664
|
+
parts.append(f"<figcaption>{inline_markdown(caption)}</figcaption>")
|
|
665
|
+
parts.append("</figure>")
|
|
666
|
+
continue
|
|
667
|
+
if line.startswith("### "):
|
|
668
|
+
flush_paragraph()
|
|
669
|
+
flush_list()
|
|
670
|
+
parts.append(f"<h3>{inline_markdown(line[4:])}</h3>")
|
|
671
|
+
continue
|
|
672
|
+
if line.startswith("## "):
|
|
673
|
+
flush_paragraph()
|
|
674
|
+
flush_list()
|
|
675
|
+
parts.append(f"<h2>{inline_markdown(line[3:])}</h2>")
|
|
676
|
+
continue
|
|
677
|
+
if line.startswith("# "):
|
|
678
|
+
flush_paragraph()
|
|
679
|
+
flush_list()
|
|
680
|
+
parts.append(f"<h1>{inline_markdown(line[2:])}</h1>")
|
|
681
|
+
continue
|
|
682
|
+
if line.startswith("- "):
|
|
683
|
+
flush_paragraph()
|
|
684
|
+
list_items.append(line[2:])
|
|
685
|
+
continue
|
|
686
|
+
flush_list()
|
|
687
|
+
paragraph.append(line)
|
|
688
|
+
|
|
689
|
+
flush_paragraph()
|
|
690
|
+
flush_list()
|
|
691
|
+
return "\n".join(parts)
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
def build_gallery(paths: list[str], project_path: Path) -> str:
|
|
695
|
+
entries: list[str] =[]
|
|
696
|
+
for rel_path in paths:
|
|
697
|
+
abs_path = (project_path / rel_path).resolve()
|
|
698
|
+
entries.append(f"})")
|
|
699
|
+
return "\n\n".join(entries)
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
def normalize_svg_text(text: str) -> str:
|
|
703
|
+
text = text.lstrip("\ufeff").strip()
|
|
704
|
+
text = re.sub(r">\s+<", "><", text)
|
|
705
|
+
text = re.sub(r"\s{2,}", " ", text)
|
|
706
|
+
return text
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
def encode_file_as_data_uri(path: Path) -> str:
|
|
710
|
+
suffix = path.suffix.lower()
|
|
711
|
+
if suffix == ".svg":
|
|
712
|
+
svg_text = normalize_svg_text(read_text(path))
|
|
713
|
+
payload = base64.b64encode(svg_text.encode("utf-8")).decode("ascii")
|
|
714
|
+
return f"data:image/svg+xml;base64,{payload}"
|
|
715
|
+
|
|
716
|
+
if suffix in RASTER_IMAGE_EXTENSIONS:
|
|
717
|
+
with Image.open(path) as image:
|
|
718
|
+
image.load()
|
|
719
|
+
image.thumbnail(MAX_EMBED_IMAGE_SIZE)
|
|
720
|
+
|
|
721
|
+
has_alpha = image.mode in {"RGBA", "LA"} or (image.mode == "P" and "transparency" in image.info)
|
|
722
|
+
buffer = io.BytesIO()
|
|
723
|
+
if has_alpha:
|
|
724
|
+
image.save(buffer, format="PNG", optimize=True, compress_level=9)
|
|
725
|
+
mime_type = "image/png"
|
|
726
|
+
else:
|
|
727
|
+
converted = image.convert("RGB") if image.mode != "RGB" else image
|
|
728
|
+
converted.save(buffer, format="JPEG", quality=JPEG_QUALITY, optimize=True, progressive=True)
|
|
729
|
+
mime_type = "image/jpeg"
|
|
730
|
+
payload = base64.b64encode(buffer.getvalue()).decode("ascii")
|
|
731
|
+
return f"data:{mime_type};base64,{payload}"
|
|
732
|
+
|
|
733
|
+
mime_type = mimetypes.guess_type(path.name)[0] or "application/octet-stream"
|
|
734
|
+
payload = base64.b64encode(path.read_bytes()).decode("ascii")
|
|
735
|
+
return f"data:{mime_type};base64,{payload}"
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
def file_uri_to_path(src: str) -> Path | None:
|
|
739
|
+
parsed = urlparse(src)
|
|
740
|
+
if parsed.scheme != "file":
|
|
741
|
+
return None
|
|
742
|
+
|
|
743
|
+
raw_path = url2pathname(unquote(parsed.path))
|
|
744
|
+
if parsed.netloc:
|
|
745
|
+
raw_path = f"//{parsed.netloc}{raw_path}"
|
|
746
|
+
return Path(raw_path)
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
def build_embedded_image_transform() -> Callable[[str], str]:
|
|
750
|
+
cache: dict[str, str] = {}
|
|
751
|
+
|
|
752
|
+
def transform(src: str) -> str:
|
|
753
|
+
if src in cache:
|
|
754
|
+
return cache[src]
|
|
755
|
+
|
|
756
|
+
path = file_uri_to_path(src)
|
|
757
|
+
if not path or not path.exists():
|
|
758
|
+
cache[src] = src
|
|
759
|
+
return src
|
|
760
|
+
|
|
761
|
+
embedded = encode_file_as_data_uri(path)
|
|
762
|
+
cache[src] = embedded
|
|
763
|
+
return embedded
|
|
764
|
+
|
|
765
|
+
return transform
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
def scan_project(project_path: Path) -> dict[str, Any]:
|
|
769
|
+
files =[
|
|
770
|
+
path
|
|
771
|
+
for path in project_path.rglob("*")
|
|
772
|
+
if path.is_file()
|
|
773
|
+
and not any(part.startswith(prefix) for part in path.parts for prefix in IGNORED_OUTPUT_DIR_PREFIXES)
|
|
774
|
+
and path.name not in IGNORED_FILE_NAMES
|
|
775
|
+
]
|
|
776
|
+
data_files: list[dict[str, str]] =[]
|
|
777
|
+
figure_files: list[dict[str, str]] = []
|
|
778
|
+
metadata_files: list[str] =[]
|
|
779
|
+
other_files: list[str] = []
|
|
780
|
+
sample_ids: set[str] = set()
|
|
781
|
+
group_labels: set[str] = set()
|
|
782
|
+
|
|
783
|
+
for path in sorted(files):
|
|
784
|
+
kind, subtype = classify_file(path)
|
|
785
|
+
rel_path = relative_posix(path, project_path)
|
|
786
|
+
record = {"path": rel_path, "subtype": subtype}
|
|
787
|
+
if kind == "data":
|
|
788
|
+
data_files.append(record)
|
|
789
|
+
if subtype in {"skeleton_data", "behavior_summary", "stats_json"}:
|
|
790
|
+
sample_ids.add(normalize_sample_id(path.stem))
|
|
791
|
+
if path.suffix.lower() in {".csv", ".tsv"}:
|
|
792
|
+
group_labels.update(extract_group_labels_from_csv(path))
|
|
793
|
+
elif kind == "figure":
|
|
794
|
+
figure_files.append(record)
|
|
795
|
+
if subtype in {"heatmap", "radar", "timeseries", "atlas", "stats_figure"}:
|
|
796
|
+
sample_ids.add(normalize_sample_id(path.stem))
|
|
797
|
+
elif kind == "metadata":
|
|
798
|
+
metadata_files.append(rel_path)
|
|
799
|
+
else:
|
|
800
|
+
other_files.append(rel_path)
|
|
801
|
+
|
|
802
|
+
obvious_group_info = infer_obvious_groups(sorted(sample_ids))
|
|
803
|
+
inferred_group_labels = sorted(set(obvious_group_info["labels"]))
|
|
804
|
+
|
|
805
|
+
detected = {
|
|
806
|
+
"sample_ids_detected": sorted(sample_ids),
|
|
807
|
+
"sample_count_detected": len(sample_ids),
|
|
808
|
+
"group_labels_detected": sorted(group_labels) or inferred_group_labels,
|
|
809
|
+
"has_skeleton_data": any(item["subtype"] == "skeleton_data" for item in data_files),
|
|
810
|
+
"has_behavior_summary": any(item["subtype"] == "behavior_summary" for item in data_files),
|
|
811
|
+
"has_stats_tables": any(item["subtype"] in {"stats_table", "stats_json"} for item in data_files),
|
|
812
|
+
"has_stats_figures": any(item["subtype"] == "stats_figure" for item in figure_files),
|
|
813
|
+
"has_heatmaps": any(item["subtype"] in {"heatmap", "timeseries", "atlas"} for item in figure_files),
|
|
814
|
+
"has_radar": any(item["subtype"] == "radar" for item in figure_files),
|
|
815
|
+
"has_cluster_figure": any(item["subtype"] == "cluster" for item in figure_files),
|
|
816
|
+
"metadata_files_found": metadata_files,
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
return {
|
|
820
|
+
"project_path": str(project_path),
|
|
821
|
+
"project_name": project_path.name,
|
|
822
|
+
"files_scanned": len(files),
|
|
823
|
+
"data_files": data_files,
|
|
824
|
+
"figure_files": figure_files,
|
|
825
|
+
"metadata_files": metadata_files,
|
|
826
|
+
"other_files": other_files,
|
|
827
|
+
"detected": detected,
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
def load_single_subject_stats(project_path: Path, scan: dict[str, Any]) -> dict[str, Any] | None:
|
|
832
|
+
stats_jsons = [item["path"] for item in scan["data_files"] if item["subtype"] == "stats_json"]
|
|
833
|
+
for rel_path in stats_jsons:
|
|
834
|
+
try:
|
|
835
|
+
payload = load_json(project_path / rel_path)
|
|
836
|
+
except Exception:
|
|
837
|
+
continue
|
|
838
|
+
if isinstance(payload, dict):
|
|
839
|
+
return payload
|
|
840
|
+
return None
|
|
841
|
+
|
|
842
|
+
|
|
843
|
+
def determine_report_mode(
|
|
844
|
+
scan: dict[str, Any],
|
|
845
|
+
group_info: dict[str, Any],
|
|
846
|
+
raw_summary: dict[str, Any] | None,
|
|
847
|
+
) -> tuple[str, str]:
|
|
848
|
+
detected = scan["detected"]
|
|
849
|
+
has_groups = group_info.get("has_groups") is True
|
|
850
|
+
|
|
851
|
+
if detected["sample_count_detected"] <= 1 and not has_groups:
|
|
852
|
+
return "single-subject", "Currently only one sample is detected, and there is no reliable grouping information."
|
|
853
|
+
if has_groups and (
|
|
854
|
+
detected["has_stats_tables"]
|
|
855
|
+
or detected["has_stats_figures"]
|
|
856
|
+
or detected["has_radar"]
|
|
857
|
+
or detected["has_cluster_figure"]
|
|
858
|
+
):
|
|
859
|
+
return "grouped-comparison", "Grouping information has been confirmed, and there are charts or statistical results supporting inter-group collation."
|
|
860
|
+
if has_groups and raw_summary:
|
|
861
|
+
return "grouped-raw-summary", "There is obvious grouping, and inter-group activity differences can be directly extracted from the raw skeleton trajectories."
|
|
862
|
+
if detected["sample_count_detected"] > 1 and not has_groups:
|
|
863
|
+
return "multi-sample-no-groups", "Multiple samples were detected, but there is no reliable grouping description."
|
|
864
|
+
if any(detected[key] for key in["has_heatmaps", "has_radar", "has_stats_figures", "has_cluster_figure"]):
|
|
865
|
+
return "figure-only-summary", "Currently it is more suitable to do descriptive collation based on existing image results."
|
|
866
|
+
if raw_summary:
|
|
867
|
+
return "raw-trajectory-summary", "Currently, a basic summary of activity regions and movement ranges can be mainly based on the raw skeleton trajectories."
|
|
868
|
+
return "data-inventory-only", "The materials are insufficient to support result interpretation, currently only material inventory can be done."
|
|
869
|
+
|
|
870
|
+
|
|
871
|
+
def build_unconfirmed_items(
|
|
872
|
+
scan: dict[str, Any],
|
|
873
|
+
stats_payload: dict[str, Any] | None,
|
|
874
|
+
experiment_type: str | None,
|
|
875
|
+
group_info: dict[str, Any],
|
|
876
|
+
) -> list[str]:
|
|
877
|
+
detected = scan["detected"]
|
|
878
|
+
items: list[str] =[]
|
|
879
|
+
|
|
880
|
+
items.append("The report purpose is unconfirmed; currently written from the perspective of results summary.")
|
|
881
|
+
if not experiment_type:
|
|
882
|
+
inferred = stats_payload.get("analysis_type") if stats_payload else None
|
|
883
|
+
if inferred:
|
|
884
|
+
items.append(f"The experimental paradigm is unclear; currently tentatively understood as {inferred}.")
|
|
885
|
+
else:
|
|
886
|
+
items.append("The experimental paradigm is unconfirmed.")
|
|
887
|
+
if group_info.get("status") == "unknown":
|
|
888
|
+
items.append("Whether grouping exists is unconfirmed.")
|
|
889
|
+
if group_info.get("status") == "inferred":
|
|
890
|
+
items.append("Current grouping is inferred from file name prefixes; if there is a formal grouping definition, it can be further supplemented.")
|
|
891
|
+
elif detected["group_labels_detected"]:
|
|
892
|
+
items.append("Candidate group labels have been detected, but the group meanings are unconfirmed.")
|
|
893
|
+
if group_info.get("has_groups") and not group_info.get("control_group"):
|
|
894
|
+
items.append("Grouping exists, but the control group is unconfirmed.")
|
|
895
|
+
items.append("Whether it is allowed to write explanatory conclusions is unconfirmed.")
|
|
896
|
+
if stats_payload and stats_payload.get("Total Distance (pixels)"):
|
|
897
|
+
items.append("The total distance is currently expressed in pixels, and it is unconfirmed whether it can be converted to actual length.")
|
|
898
|
+
return items
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
def summarize_region_stats(stats_payload: dict[str, Any]) -> tuple[list[str], list[str]]:
|
|
902
|
+
region_stats = stats_payload.get("statistics") or {}
|
|
903
|
+
percent_map = stats_payload.get("percent(%)") or {}
|
|
904
|
+
ordered: list[tuple[str, float, int, str]] =[]
|
|
905
|
+
|
|
906
|
+
for region, payload in region_stats.items():
|
|
907
|
+
if not isinstance(payload, dict):
|
|
908
|
+
continue
|
|
909
|
+
stay_time = float(payload.get("Stay Time (s)", 0.0) or 0.0)
|
|
910
|
+
enter_count = int(payload.get("Enter Count", 0) or 0)
|
|
911
|
+
percent = str(percent_map.get(region, ""))
|
|
912
|
+
ordered.append((region, stay_time, enter_count, percent))
|
|
913
|
+
|
|
914
|
+
ordered.sort(key=lambda item: item[1], reverse=True)
|
|
915
|
+
region_lines =[
|
|
916
|
+
f"{region}: stayed {stay_time:g} seconds, entered {enter_count} times, proportion {percent or 'not provided'}"
|
|
917
|
+
for region, stay_time, enter_count, percent in ordered
|
|
918
|
+
]
|
|
919
|
+
zero_regions =[region for region, stay_time, enter_count, _ in ordered if stay_time == 0 and enter_count == 0]
|
|
920
|
+
return region_lines, zero_regions
|
|
921
|
+
|
|
922
|
+
|
|
923
|
+
def select_section_specs(
|
|
924
|
+
report_mode: str,
|
|
925
|
+
galleries: dict[str, list[str]],
|
|
926
|
+
raw_summary: dict[str, Any] | None,
|
|
927
|
+
) -> list[dict[str, str]]:
|
|
928
|
+
enabled_ids = {
|
|
929
|
+
"project_summary",
|
|
930
|
+
"overview",
|
|
931
|
+
"sample_check",
|
|
932
|
+
}
|
|
933
|
+
if raw_summary:
|
|
934
|
+
enabled_ids.add("raw_trajectory")
|
|
935
|
+
if galleries["heatmap"]:
|
|
936
|
+
enabled_ids.add("heatmap")
|
|
937
|
+
if galleries["radar"]:
|
|
938
|
+
enabled_ids.add("radar")
|
|
939
|
+
if galleries["stats"]:
|
|
940
|
+
enabled_ids.add("stats")
|
|
941
|
+
if galleries["cluster"]:
|
|
942
|
+
enabled_ids.add("cluster")
|
|
943
|
+
if report_mode == "single-subject":
|
|
944
|
+
enabled_ids.add("single_subject")
|
|
945
|
+
evidence_count = sum(bool(paths) for paths in galleries.values()) + int(bool(raw_summary))
|
|
946
|
+
if evidence_count >= 2:
|
|
947
|
+
enabled_ids.add("integrated_interpretation")
|
|
948
|
+
return [spec for spec in SECTION_SPECS if spec["id"] in enabled_ids]
|
|
949
|
+
|
|
950
|
+
|
|
951
|
+
def build_section_bodies(specs: list[dict[str, str]]) -> dict[str, Any]:
|
|
952
|
+
section_bodies: dict[str, Any] = {}
|
|
953
|
+
for spec in specs:
|
|
954
|
+
guidance = SECTION_GUIDANCE[spec["body_key"]]
|
|
955
|
+
section_bodies[spec["body_key"]] = {
|
|
956
|
+
"section_id": spec["id"],
|
|
957
|
+
"title": spec["title"],
|
|
958
|
+
"purpose": guidance["purpose"],
|
|
959
|
+
"write_when": guidance["write_when"],
|
|
960
|
+
"source_fields": guidance["source_fields"],
|
|
961
|
+
"rules": guidance["rules"],
|
|
962
|
+
"body": "",
|
|
963
|
+
}
|
|
964
|
+
return section_bodies
|
|
965
|
+
|
|
966
|
+
|
|
967
|
+
def build_manifest(project_path: Path) -> dict[str, Any]:
|
|
968
|
+
scan_payload = scan_project(project_path)
|
|
969
|
+
stats_payload = load_single_subject_stats(project_path, scan_payload)
|
|
970
|
+
detected = scan_payload["detected"]
|
|
971
|
+
experiment_type = infer_experiment_type(project_path, stats_payload)
|
|
972
|
+
group_info = infer_obvious_groups(detected["sample_ids_detected"])
|
|
973
|
+
raw_summary = build_raw_trajectory_summary(project_path, scan_payload, experiment_type, group_info)
|
|
974
|
+
report_mode, report_mode_reason = determine_report_mode(scan_payload, group_info, raw_summary)
|
|
975
|
+
|
|
976
|
+
galleries = {
|
|
977
|
+
"heatmap": [item["path"] for item in scan_payload["figure_files"] if item["subtype"] in {"heatmap", "timeseries", "atlas"}],
|
|
978
|
+
"radar": [item["path"] for item in scan_payload["figure_files"] if item["subtype"] == "radar"],
|
|
979
|
+
"stats": [item["path"] for item in scan_payload["figure_files"] if item["subtype"] == "stats_figure"],
|
|
980
|
+
"cluster": [item["path"] for item in scan_payload["figure_files"] if item["subtype"] == "cluster"],
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
facts: dict[str, Any] = {
|
|
984
|
+
"project_path_confirmation": {
|
|
985
|
+
"project_path": str(project_path),
|
|
986
|
+
"files_scanned": scan_payload["files_scanned"],
|
|
987
|
+
"key_inputs_preview": (
|
|
988
|
+
[item["path"] for item in scan_payload["data_files"][:3]]
|
|
989
|
+
+[item["path"] for item in scan_payload["figure_files"][:3]]
|
|
990
|
+
+ scan_payload["metadata_files"][:2]
|
|
991
|
+
),
|
|
992
|
+
},
|
|
993
|
+
"input_completeness": {
|
|
994
|
+
"checks": [
|
|
995
|
+
{"label": "Skeleton or trajectory data", "available": detected["has_skeleton_data"]},
|
|
996
|
+
{"label": "Behavior or summary table", "available": detected["has_behavior_summary"]},
|
|
997
|
+
{"label": "Statistics table", "available": detected["has_stats_tables"]},
|
|
998
|
+
{"label": "Heatmap or trajectory map", "available": detected["has_heatmaps"]},
|
|
999
|
+
{"label": "Radar chart", "available": detected["has_radar"]},
|
|
1000
|
+
{"label": "Cluster chart", "available": detected["has_cluster_figure"]},
|
|
1001
|
+
]
|
|
1002
|
+
},
|
|
1003
|
+
"overview": {
|
|
1004
|
+
"project_name": scan_payload["project_name"],
|
|
1005
|
+
"report_mode": report_mode,
|
|
1006
|
+
"report_goal": "results-summary",
|
|
1007
|
+
"experiment_type": experiment_type,
|
|
1008
|
+
},
|
|
1009
|
+
"sample_check": {
|
|
1010
|
+
"sample_count_detected": detected["sample_count_detected"],
|
|
1011
|
+
"sample_ids_detected": detected["sample_ids_detected"],
|
|
1012
|
+
"has_groups": group_info.get("has_groups"),
|
|
1013
|
+
"group_labels_detected": group_info.get("labels") or detected["group_labels_detected"],
|
|
1014
|
+
"group_mapping": group_info.get("display_mapping") or {},
|
|
1015
|
+
"group_counts": group_info.get("group_counts") or {},
|
|
1016
|
+
"group_status": group_info.get("status"),
|
|
1017
|
+
"control_group": group_info.get("control_group"),
|
|
1018
|
+
},
|
|
1019
|
+
"group_inference": group_info,
|
|
1020
|
+
"materials_inventory": {
|
|
1021
|
+
"data_files": [item["path"] for item in scan_payload["data_files"]],
|
|
1022
|
+
"figure_files": [item["path"] for item in scan_payload["figure_files"]],
|
|
1023
|
+
"metadata_files": scan_payload["metadata_files"],
|
|
1024
|
+
"other_files": scan_payload["other_files"],
|
|
1025
|
+
},
|
|
1026
|
+
"unconfirmed_items": build_unconfirmed_items(scan_payload, stats_payload, experiment_type, group_info),
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
if stats_payload:
|
|
1030
|
+
region_lines, zero_regions = summarize_region_stats(stats_payload)
|
|
1031
|
+
facts["single_subject_stats"] = {
|
|
1032
|
+
"file_name": stats_payload.get("file_name"),
|
|
1033
|
+
"analysis_type": stats_payload.get("analysis_type"),
|
|
1034
|
+
"total_time_s": stats_payload.get("total_time(s)"),
|
|
1035
|
+
"detect_time_s": stats_payload.get("detect_time(s)"),
|
|
1036
|
+
"total_distance_pixels": stats_payload.get("Total Distance (pixels)"),
|
|
1037
|
+
"region_summary_lines": region_lines,
|
|
1038
|
+
"zero_regions": zero_regions,
|
|
1039
|
+
}
|
|
1040
|
+
if raw_summary:
|
|
1041
|
+
facts["raw_trajectory_summary"] = raw_summary
|
|
1042
|
+
|
|
1043
|
+
section_specs = select_section_specs(report_mode, galleries, raw_summary)
|
|
1044
|
+
|
|
1045
|
+
return {
|
|
1046
|
+
"manifest_version": 2,
|
|
1047
|
+
"project_path": str(project_path),
|
|
1048
|
+
"project_name": scan_payload["project_name"],
|
|
1049
|
+
"report_title": f"{scan_payload['project_name']} Analysis Report",
|
|
1050
|
+
"report_goal": "results-summary",
|
|
1051
|
+
"scan": scan_payload,
|
|
1052
|
+
"report_mode": report_mode,
|
|
1053
|
+
"report_mode_reason": report_mode_reason,
|
|
1054
|
+
"facts": facts,
|
|
1055
|
+
"galleries": galleries,
|
|
1056
|
+
"section_bodies": build_section_bodies(section_specs),
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
|
|
1060
|
+
def extract_body_text(section_entry: Any) -> str:
|
|
1061
|
+
if isinstance(section_entry, dict):
|
|
1062
|
+
return str(section_entry.get("body", "")).strip()
|
|
1063
|
+
if isinstance(section_entry, str):
|
|
1064
|
+
return section_entry.strip()
|
|
1065
|
+
return ""
|
|
1066
|
+
|
|
1067
|
+
|
|
1068
|
+
def assemble_render_context(manifest: dict[str, Any]) -> dict[str, Any]:
|
|
1069
|
+
section_bodies = manifest.get("section_bodies")
|
|
1070
|
+
if not isinstance(section_bodies, dict):
|
|
1071
|
+
raise ValueError("manifest.section_bodies must be an object.")
|
|
1072
|
+
|
|
1073
|
+
galleries = manifest["galleries"]
|
|
1074
|
+
project_path = Path(manifest["project_path"])
|
|
1075
|
+
context: dict[str, Any] = {
|
|
1076
|
+
"report_title": manifest["report_title"],
|
|
1077
|
+
"project_path": manifest["project_path"],
|
|
1078
|
+
"report_mode": manifest["report_mode"],
|
|
1079
|
+
"report_goal": manifest.get("report_goal") or "results-summary",
|
|
1080
|
+
"heatmap_gallery": build_gallery(galleries["heatmap"], project_path),
|
|
1081
|
+
"radar_gallery": build_gallery(galleries["radar"], project_path),
|
|
1082
|
+
"stats_gallery": build_gallery(galleries["stats"], project_path),
|
|
1083
|
+
"cluster_gallery": build_gallery(galleries["cluster"], project_path),
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
for spec in SECTION_SPECS:
|
|
1087
|
+
entry = section_bodies.get(spec["body_key"])
|
|
1088
|
+
body_text = extract_body_text(entry)
|
|
1089
|
+
context[spec["body_key"]] = body_text
|
|
1090
|
+
|
|
1091
|
+
gallery_key = spec.get("gallery_key")
|
|
1092
|
+
if gallery_key:
|
|
1093
|
+
context.setdefault(gallery_key, "")
|
|
1094
|
+
|
|
1095
|
+
enabled = bool(entry) and bool(body_text or context.get(gallery_key or "", ""))
|
|
1096
|
+
context[spec["section_key"]] = render_section(spec["template"], context, enabled=enabled)
|
|
1097
|
+
|
|
1098
|
+
return context
|
|
1099
|
+
|
|
1100
|
+
|
|
1101
|
+
def render_report_markdown(manifest: dict[str, Any]) -> str:
|
|
1102
|
+
context = assemble_render_context(manifest)
|
|
1103
|
+
return render_template(read_text(ASSETS_DIR / "report_template_en.md"), context).strip() + "\n"
|
|
1104
|
+
|
|
1105
|
+
|
|
1106
|
+
def render_report_html(manifest: dict[str, Any], markdown_text: str | None = None) -> str:
|
|
1107
|
+
markdown_body = markdown_text or render_report_markdown(manifest)
|
|
1108
|
+
title_line = f"# {manifest['report_title']}"
|
|
1109
|
+
if markdown_body.startswith(title_line):
|
|
1110
|
+
markdown_body = markdown_body[len(title_line):].lstrip()
|
|
1111
|
+
|
|
1112
|
+
image_src_transform = build_embedded_image_transform()
|
|
1113
|
+
|
|
1114
|
+
html_context = {
|
|
1115
|
+
"report_title": manifest["report_title"],
|
|
1116
|
+
"project_path": manifest["project_path"],
|
|
1117
|
+
"report_mode": manifest["report_mode"],
|
|
1118
|
+
"report_goal": manifest.get("report_goal") or "results-summary",
|
|
1119
|
+
"body_html": markdown_to_html(markdown_body, image_src_transform=image_src_transform),
|
|
1120
|
+
}
|
|
1121
|
+
return render_template(read_text(ASSETS_DIR / "report_template_en.html"), html_context)
|