Delete text2roi_colab_demo.ipynb
Browse files- text2roi_colab_demo.ipynb +0 -301
text2roi_colab_demo.ipynb
DELETED
|
@@ -1,301 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"cells": [
|
| 3 |
-
{
|
| 4 |
-
"cell_type": "markdown",
|
| 5 |
-
"metadata": {},
|
| 6 |
-
"source": [
|
| 7 |
-
"# Amphora NeuroText — Colab Demo\n",
|
| 8 |
-
"\n",
|
| 9 |
-
"**Type text → predict 56 brain ROI activations → visualize on fsaverage5 cortical maps (MNE + Nilearn).**\n",
|
| 10 |
-
"\n",
|
| 11 |
-
"Open this notebook via the **GitHub Colab link** on the model card (recommended).\n",
|
| 12 |
-
"\n",
|
| 13 |
-
"Weights download from the public HF repo `ffh92r32rm0/Amphora_NeuroText` — no login required.\n",
|
| 14 |
-
"\n",
|
| 15 |
-
"Pipeline:\n",
|
| 16 |
-
"```\n",
|
| 17 |
-
"text → Qwen3-Embedding-4B → Text2ROI MLP (3M params) → 56 ROI scores → HCP-MMP parcels → fsaverage5 surface\n",
|
| 18 |
-
"```\n",
|
| 19 |
-
"\n",
|
| 20 |
-
"Brain plotting uses **MNE + Nilearn only** (BSD/MIT-friendly) — no tribev2 import (CC BY-NC).\n",
|
| 21 |
-
"\n",
|
| 22 |
-
"**Setup:** Runtime → Change runtime type → **GPU** (T4 is enough), then run all cells.\n",
|
| 23 |
-
"\n",
|
| 24 |
-
"Model: [ffh92r32rm0/Amphora_NeuroText](https://huggingface.co/ffh92r32rm0/Amphora_NeuroText) · MIT license\n",
|
| 25 |
-
"\n",
|
| 26 |
-
"> ⚠️ Predictions are **model estimates** from group fMRI training — not measurements of anyone's private thoughts."
|
| 27 |
-
]
|
| 28 |
-
},
|
| 29 |
-
{
|
| 30 |
-
"cell_type": "code",
|
| 31 |
-
"metadata": {},
|
| 32 |
-
"source": [
|
| 33 |
-
"# Install dependencies (first run ~5–8 min)\n",
|
| 34 |
-
"# Commercial-friendly: MNE + Nilearn for brain viz — no tribev2 (CC BY-NC).\n",
|
| 35 |
-
"import sys\n",
|
| 36 |
-
"\n",
|
| 37 |
-
"!{sys.executable} -m pip install -q --upgrade pip\n",
|
| 38 |
-
"!{sys.executable} -m pip install -q \"torch>=2.3\" \"transformers>=4.44\" huggingface_hub ipywidgets\n",
|
| 39 |
-
"!{sys.executable} -m pip install -q nilearn mne nibabel scipy matplotlib numpy\n",
|
| 40 |
-
"\n",
|
| 41 |
-
"print(\"Deps installed (MNE + Nilearn for fsaverage5 brain maps).\")"
|
| 42 |
-
],
|
| 43 |
-
"execution_count": null,
|
| 44 |
-
"outputs": []
|
| 45 |
-
},
|
| 46 |
-
{
|
| 47 |
-
"cell_type": "code",
|
| 48 |
-
"metadata": {},
|
| 49 |
-
"source": [
|
| 50 |
-
"import json\n",
|
| 51 |
-
"from pathlib import Path\n",
|
| 52 |
-
"\n",
|
| 53 |
-
"import matplotlib.pyplot as plt\n",
|
| 54 |
-
"import numpy as np\n",
|
| 55 |
-
"import torch\n",
|
| 56 |
-
"import torch.nn as nn\n",
|
| 57 |
-
"import torch.nn.functional as F\n",
|
| 58 |
-
"from huggingface_hub import hf_hub_download\n",
|
| 59 |
-
"from IPython.display import display, clear_output\n",
|
| 60 |
-
"import ipywidgets as widgets\n",
|
| 61 |
-
"\n",
|
| 62 |
-
"HF_REPO = \"ffh92r32rm0/Amphora_NeuroText\"\n",
|
| 63 |
-
"\n",
|
| 64 |
-
"DEVICE = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n",
|
| 65 |
-
"print(f\"Device: {DEVICE}\")\n",
|
| 66 |
-
"\n",
|
| 67 |
-
"CKPT_PATH = hf_hub_download(HF_REPO, \"text2roi_projector.pt\")\n",
|
| 68 |
-
"print(f\"Checkpoint: {CKPT_PATH}\")"
|
| 69 |
-
],
|
| 70 |
-
"execution_count": null,
|
| 71 |
-
"outputs": []
|
| 72 |
-
},
|
| 73 |
-
{
|
| 74 |
-
"cell_type": "code",
|
| 75 |
-
"metadata": {},
|
| 76 |
-
"source": [
|
| 77 |
-
"ROI_NAMES_56 = [\n",
|
| 78 |
-
" \"V1\", \"V2\", \"V3\", \"V4\", \"V3A\", \"V3B\", \"LO1\", \"LO2\",\n",
|
| 79 |
-
" \"MT\", \"MST\", \"V7\", \"IPS1\",\n",
|
| 80 |
-
" \"FFA-1\", \"FFA-2\", \"PPA\", \"RSC\", \"OFA\", \"EBA\",\n",
|
| 81 |
-
" \"IPS2\", \"IPS3\", \"IPS4\", \"IPS5\", \"SPL1\", \"hIP1\", \"hIP2\", \"hIP3\",\n",
|
| 82 |
-
" \"dlPFC\", \"vlPFC\", \"OFC\", \"ACC\", \"mPFC\", \"FP1\", \"FP2\",\n",
|
| 83 |
-
" \"IFG\", \"IFGorb\", \"STG\", \"STS\", \"MTG\", \"AG\",\n",
|
| 84 |
-
" \"PCC\", \"mPFC_dmn\", \"LP_L\", \"LP_R\", \"HPC_L\", \"HPC_R\",\n",
|
| 85 |
-
" \"AI\", \"dACC\", \"sgACC\", \"vmPFC\",\n",
|
| 86 |
-
" \"Amygdala_L\", \"Amygdala_R\", \"Caudate_L\", \"Caudate_R\",\n",
|
| 87 |
-
" \"Putamen_L\", \"Putamen_R\", \"Thalamus\",\n",
|
| 88 |
-
"]\n",
|
| 89 |
-
"\n",
|
| 90 |
-
"class Text2ROI(nn.Module):\n",
|
| 91 |
-
" def __init__(self, in_dim=2560, hidden=1024, out_dim=56, dropout=0.1):\n",
|
| 92 |
-
" super().__init__()\n",
|
| 93 |
-
" self.net = nn.Sequential(\n",
|
| 94 |
-
" nn.Linear(in_dim, hidden), nn.GELU(), nn.Dropout(dropout), nn.LayerNorm(hidden),\n",
|
| 95 |
-
" nn.Linear(hidden, hidden // 2), nn.GELU(), nn.Dropout(dropout),\n",
|
| 96 |
-
" nn.Linear(hidden // 2, out_dim),\n",
|
| 97 |
-
" )\n",
|
| 98 |
-
" def forward(self, x):\n",
|
| 99 |
-
" return self.net(x)\n",
|
| 100 |
-
"\n",
|
| 101 |
-
"def load_projector(path):\n",
|
| 102 |
-
" ckpt = torch.load(path, map_location=\"cpu\", weights_only=False)\n",
|
| 103 |
-
" model = Text2ROI(int(ckpt[\"in_dim\"]), int(ckpt[\"hidden\"]), int(ckpt[\"n_roi\"]))\n",
|
| 104 |
-
" model.load_state_dict(ckpt[\"state_dict\"])\n",
|
| 105 |
-
" model.to(DEVICE).eval()\n",
|
| 106 |
-
" names = [str(x) for x in ckpt.get(\"roi_names\", ROI_NAMES_56)]\n",
|
| 107 |
-
" return model, names, ckpt\n",
|
| 108 |
-
"\n",
|
| 109 |
-
"PROJECTOR, ROI_NAMES, CKPT_META = load_projector(CKPT_PATH)\n",
|
| 110 |
-
"print(f\"Loaded Text2ROI v1 — val R={CKPT_META.get('best_val_r', 0):+.3f}, {len(ROI_NAMES)} ROIs\")"
|
| 111 |
-
],
|
| 112 |
-
"execution_count": null,
|
| 113 |
-
"outputs": []
|
| 114 |
-
},
|
| 115 |
-
{
|
| 116 |
-
"cell_type": "code",
|
| 117 |
-
"metadata": {},
|
| 118 |
-
"source": [
|
| 119 |
-
"QWEN_ID = \"Qwen/Qwen3-Embedding-4B\"\n",
|
| 120 |
-
"from transformers import AutoModel, AutoTokenizer\n",
|
| 121 |
-
"\n",
|
| 122 |
-
"print(\"Loading Qwen3 embedder (first run downloads ~8 GB)...\")\n",
|
| 123 |
-
"_tok = AutoTokenizer.from_pretrained(QWEN_ID, padding_side=\"left\")\n",
|
| 124 |
-
"_qwen = AutoModel.from_pretrained(QWEN_ID, dtype=torch.bfloat16).to(DEVICE).eval()\n",
|
| 125 |
-
"print(\"Qwen3 ready.\")\n",
|
| 126 |
-
"\n",
|
| 127 |
-
"@torch.no_grad()\n",
|
| 128 |
-
"def embed_text(text: str) -> np.ndarray:\n",
|
| 129 |
-
" tok = _tok([text], return_tensors=\"pt\", padding=True, truncation=True, max_length=512).to(DEVICE)\n",
|
| 130 |
-
" out = _qwen(**tok)\n",
|
| 131 |
-
" emb = out.last_hidden_state[:, -1].float()\n",
|
| 132 |
-
" emb = F.normalize(emb, p=2, dim=1).cpu().numpy().astype(np.float32)\n",
|
| 133 |
-
" return emb\n",
|
| 134 |
-
"\n",
|
| 135 |
-
"@torch.no_grad()\n",
|
| 136 |
-
"def predict_rois(text: str) -> dict:\n",
|
| 137 |
-
" x = torch.from_numpy(embed_text(text)).to(DEVICE)\n",
|
| 138 |
-
" pred = PROJECTOR(x).cpu().numpy().reshape(-1)\n",
|
| 139 |
-
" return {name: float(pred[i]) for i, name in enumerate(ROI_NAMES)}"
|
| 140 |
-
],
|
| 141 |
-
"execution_count": null,
|
| 142 |
-
"outputs": []
|
| 143 |
-
},
|
| 144 |
-
{
|
| 145 |
-
"cell_type": "code",
|
| 146 |
-
"metadata": {},
|
| 147 |
-
"source": [
|
| 148 |
-
"# Map 56 ROI values → fsaverage5 vertices (MNE + Nilearn — no tribev2)\n",
|
| 149 |
-
"import sys\n",
|
| 150 |
-
"import urllib.request\n",
|
| 151 |
-
"from pathlib import Path\n",
|
| 152 |
-
"\n",
|
| 153 |
-
"_helper = Path(\"text2roi_to_fsaverage.py\")\n",
|
| 154 |
-
"if not _helper.exists():\n",
|
| 155 |
-
" try:\n",
|
| 156 |
-
" from huggingface_hub import hf_hub_download\n",
|
| 157 |
-
" _helper = Path(hf_hub_download(HF_REPO, \"text2roi_to_fsaverage.py\"))\n",
|
| 158 |
-
" except Exception:\n",
|
| 159 |
-
" urllib.request.urlretrieve(\n",
|
| 160 |
-
" \"https://raw.githubusercontent.com/hamcoderfran/neuroevolution/main/huggingface/text2roi/text2roi_to_fsaverage.py\",\n",
|
| 161 |
-
" _helper,\n",
|
| 162 |
-
" )\n",
|
| 163 |
-
"sys.path.insert(0, str(_helper.parent))\n",
|
| 164 |
-
"from text2roi_to_fsaverage import roi_dict_to_fsaverage5, plot_fsaverage5_brain\n",
|
| 165 |
-
"\n",
|
| 166 |
-
"print(\"fsaverage5 plotting helpers ready (MNE + Nilearn).\")"
|
| 167 |
-
],
|
| 168 |
-
"execution_count": null,
|
| 169 |
-
"outputs": []
|
| 170 |
-
},
|
| 171 |
-
{
|
| 172 |
-
"cell_type": "code",
|
| 173 |
-
"metadata": {},
|
| 174 |
-
"source": [
|
| 175 |
-
"def plot_results(text: str, roi_values: dict):\n",
|
| 176 |
-
" vertex_map, skipped = roi_dict_to_fsaverage5(roi_values)\n",
|
| 177 |
-
"\n",
|
| 178 |
-
" # --- ROI bar chart ---\n",
|
| 179 |
-
" top = sorted(roi_values.items(), key=lambda kv: abs(kv[1]), reverse=True)[:16]\n",
|
| 180 |
-
" names = [t[0] for t in top]\n",
|
| 181 |
-
" vals = [t[1] for t in top]\n",
|
| 182 |
-
" colors = [\"#c44e52\" if v < 0 else \"#4c72b0\" for v in vals]\n",
|
| 183 |
-
"\n",
|
| 184 |
-
" fig, ax = plt.subplots(figsize=(7, 5))\n",
|
| 185 |
-
" ax.barh(names[::-1], vals[::-1], color=colors[::-1])\n",
|
| 186 |
-
" ax.axvline(0, color=\"#333\", lw=0.8)\n",
|
| 187 |
-
" ax.set_title(\"Top ROI predictions\")\n",
|
| 188 |
-
" ax.set_xlabel(\"Predicted activation\")\n",
|
| 189 |
-
" plt.tight_layout()\n",
|
| 190 |
-
" plt.show()\n",
|
| 191 |
-
"\n",
|
| 192 |
-
" # --- fsaverage5 brain maps (Nilearn) ---\n",
|
| 193 |
-
" skip_note = f\" — subcortical skipped: {len(skipped)}\" if skipped else \"\"\n",
|
| 194 |
-
" plot_fsaverage5_brain(\n",
|
| 195 |
-
" vertex_map,\n",
|
| 196 |
-
" views=[\"left\", \"right\", \"dorsal\"],\n",
|
| 197 |
-
" cmap=\"RdBu_r\",\n",
|
| 198 |
-
" threshold=0.02,\n",
|
| 199 |
-
" title=f\"fsaverage5 cortical map (HCP-MMP ROIs){skip_note}\",\n",
|
| 200 |
-
" )\n",
|
| 201 |
-
" plt.show()\n",
|
| 202 |
-
"\n",
|
| 203 |
-
" print(\"\\nTop ROIs:\")\n",
|
| 204 |
-
" for n, v in top[:10]:\n",
|
| 205 |
-
" print(f\" {n:<16} {v:+.4f}\")\n",
|
| 206 |
-
" if skipped:\n",
|
| 207 |
-
" print(f\"\\n(Subcortical / unmapped ROIs not painted on cortex: {', '.join(skipped[:8])}{'…' if len(skipped)>8 else ''})\")"
|
| 208 |
-
],
|
| 209 |
-
"execution_count": null,
|
| 210 |
-
"outputs": []
|
| 211 |
-
},
|
| 212 |
-
{
|
| 213 |
-
"cell_type": "markdown",
|
| 214 |
-
"metadata": {},
|
| 215 |
-
"source": [
|
| 216 |
-
"## Try it — enter text and click **Predict**\n",
|
| 217 |
-
"\n",
|
| 218 |
-
"Or use a preset example below."
|
| 219 |
-
]
|
| 220 |
-
},
|
| 221 |
-
{
|
| 222 |
-
"cell_type": "code",
|
| 223 |
-
"metadata": {},
|
| 224 |
-
"source": [
|
| 225 |
-
"text_in = widgets.Textarea(\n",
|
| 226 |
-
" value=\"a dog running through a sunny park\",\n",
|
| 227 |
-
" description=\"Text:\",\n",
|
| 228 |
-
" layout=widgets.Layout(width=\"90%\", height=\"80px\"),\n",
|
| 229 |
-
")\n",
|
| 230 |
-
"btn = widgets.Button(description=\"Predict brain map\", button_style=\"primary\", icon=\"play\")\n",
|
| 231 |
-
"out = widgets.Output()\n",
|
| 232 |
-
"\n",
|
| 233 |
-
"PRESETS = {\n",
|
| 234 |
-
" \"Sunny park\": \"a dog running through a sunny park\",\n",
|
| 235 |
-
" \"Scary scene\": \"a terrifying monster jumps out in a dark alley\",\n",
|
| 236 |
-
" \"Romantic\": \"two people sharing a quiet candlelit dinner\",\n",
|
| 237 |
-
" \"Math puzzle\": \"solving a difficult calculus proof step by step\",\n",
|
| 238 |
-
"}\n",
|
| 239 |
-
"preset_btns = [widgets.Button(description=k, layout=widgets.Layout(width=\"140px\")) for k in PRESETS]\n",
|
| 240 |
-
"\n",
|
| 241 |
-
"def _on_preset(btn):\n",
|
| 242 |
-
" text_in.value = PRESETS[btn.description]\n",
|
| 243 |
-
"\n",
|
| 244 |
-
"for b in preset_btns:\n",
|
| 245 |
-
" b.on_click(_on_preset)\n",
|
| 246 |
-
"\n",
|
| 247 |
-
"def _on_predict(_):\n",
|
| 248 |
-
" with out:\n",
|
| 249 |
-
" clear_output(wait=True)\n",
|
| 250 |
-
" text = text_in.value.strip()\n",
|
| 251 |
-
" if not text:\n",
|
| 252 |
-
" print(\"Enter some text first.\")\n",
|
| 253 |
-
" return\n",
|
| 254 |
-
" print(f\"Predicting for: {text!r} …\")\n",
|
| 255 |
-
" rois = predict_rois(text)\n",
|
| 256 |
-
" plot_results(text, rois)\n",
|
| 257 |
-
"\n",
|
| 258 |
-
"btn.on_click(_on_predict)\n",
|
| 259 |
-
"\n",
|
| 260 |
-
"display(widgets.VBox([\n",
|
| 261 |
-
" text_in,\n",
|
| 262 |
-
" widgets.HBox([btn] + preset_btns),\n",
|
| 263 |
-
" out,\n",
|
| 264 |
-
"]))"
|
| 265 |
-
],
|
| 266 |
-
"execution_count": null,
|
| 267 |
-
"outputs": []
|
| 268 |
-
},
|
| 269 |
-
{
|
| 270 |
-
"cell_type": "markdown",
|
| 271 |
-
"metadata": {},
|
| 272 |
-
"source": [
|
| 273 |
-
"## What the output means\n",
|
| 274 |
-
"\n",
|
| 275 |
-
"| Output | Description |\n",
|
| 276 |
-
"|--------|-------------|\n",
|
| 277 |
-
"| **Bar chart** | Top 16 of 56 ROI activation scores (positive = predicted up-regulation) |\n",
|
| 278 |
-
"| **Brain surface** | ROI values painted onto **fsaverage5** via HCP-MMP1 parcels (MNE + Nilearn) |\n",
|
| 279 |
-
"| **Skipped ROIs** | Subcortical regions (amygdala, striatum, thalamus) are not on the cortical surface |\n",
|
| 280 |
-
"\n",
|
| 281 |
-
"**Links:** [Model on HF](https://huggingface.co/ffh92r32rm0/Amphora_NeuroText) · [Code](https://github.com/hamcoderfran/neuroevolution)"
|
| 282 |
-
]
|
| 283 |
-
}
|
| 284 |
-
],
|
| 285 |
-
"metadata": {
|
| 286 |
-
"colab": {
|
| 287 |
-
"provenance": [],
|
| 288 |
-
"gpuType": "T4"
|
| 289 |
-
},
|
| 290 |
-
"kernelspec": {
|
| 291 |
-
"display_name": "Python 3",
|
| 292 |
-
"name": "python3"
|
| 293 |
-
},
|
| 294 |
-
"language_info": {
|
| 295 |
-
"name": "python"
|
| 296 |
-
},
|
| 297 |
-
"accelerator": "GPU"
|
| 298 |
-
},
|
| 299 |
-
"nbformat": 4,
|
| 300 |
-
"nbformat_minor": 5
|
| 301 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|