Last active
June 15, 2026 14:10
-
-
Save coolk8/8d496d83bf42b110c4bff49585883d43 to your computer and use it in GitHub Desktop.
Анализ КТ грудной клетки с MedGemma 1.5 (Google Colab)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "nbformat": 4, | |
| "nbformat_minor": 0, | |
| "metadata": { | |
| "colab": { | |
| "provenance": [], | |
| "private_outputs": true | |
| }, | |
| "kernelspec": { | |
| "name": "python3", | |
| "display_name": "Python 3" | |
| }, | |
| "language_info": { | |
| "name": "python" | |
| } | |
| }, | |
| "cells": [ | |
| { | |
| "cell_type": "markdown", | |
| "metadata": {}, | |
| "source": [ | |
| "# Анализ КТ грудной клетки с MedGemma 1.5 (via Modal)\n", | |
| "\n", | |
| "Загружаете DICOM-файлы → ноутбук обрабатывает → MedGemma на L4 GPU (Modal) анализирует.\n", | |
| "\n", | |
| "**GPU в Colab не нужен** — бесплатный CPU runtime достаточно.\n", | |
| "\n", | |
| "### Как работает\n", | |
| "1. **Обзорный проход** — 85 равномерно отобранных срезов, модель указывает номера срезов с находками\n", | |
| "2. **Прицельный проход** — плотное покрытие зоны интереса (все срезы из указанного диапазона)\n", | |
| "\n", | |
| "> **Важно:** Образовательный проект. Не медицинское изделие, не для диагностики." | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": null, | |
| "metadata": { | |
| "cellView": "form" | |
| }, | |
| "outputs": [], | |
| "source": [ | |
| "#@title 1. Установка зависимостей\n", | |
| "%%capture\n", | |
| "!pip install pydicom requests" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": null, | |
| "metadata": { | |
| "cellView": "form" | |
| }, | |
| "outputs": [], | |
| "source": "#@title 2. Подключение Google Drive и выбор папки с DICOM { display-mode: \"form\" }\n\n#@markdown Загрузите DICOM-файлы (или ZIP) в Google Drive,\n#@markdown укажите путь к папке и запустите.\n#@markdown ---\nsource = \"Google Drive\" #@param [\"Google Drive\", \"Загрузить вручную (upload)\"]\ndrive_path = \"CT\" #@param {type:\"string\"}\n#@markdown ↑ Путь внутри My Drive (без `/content/drive/MyDrive/`). Пример: `CT/chest_2026`\n#@markdown ---\n\nimport zipfile, shutil, sys, os\nfrom pathlib import Path\n\ngoogle_colab = \"google.colab\" in sys.modules\nDICOM_DIR = Path(\"/content/ct_dicom\")\n\nif source == \"Google Drive\":\n from google.colab import drive\n drive.mount(\"/content/drive\", force_remount=False)\n src = Path(\"/content/drive/MyDrive\") / drive_path.strip(\"/\")\n if not src.exists():\n print(f\"❌ Папка не найдена: {src}\")\n print(f\"\\nСодержимое MyDrive:\")\n for p in sorted(Path(\"/content/drive/MyDrive\").iterdir())[:20]:\n print(f\" {'[DIR]' if p.is_dir() else ' '} {p.name}\")\n raise FileNotFoundError(f\"{src} не существует\")\n\n zips = list(src.glob(\"*.zip\"))\n if zips:\n if DICOM_DIR.exists(): shutil.rmtree(DICOM_DIR)\n DICOM_DIR.mkdir(parents=True)\n for zf in zips:\n print(f\"Распаковка {zf.name}...\")\n with zipfile.ZipFile(zf) as z:\n z.extractall(DICOM_DIR)\n else:\n DICOM_DIR = src\n\n print(f\"Источник: Google Drive / {drive_path}\")\n\nelse:\n UPLOAD_DIR = Path(\"/content/ct_upload\")\n for d in [UPLOAD_DIR, DICOM_DIR]:\n if d.exists(): shutil.rmtree(d)\n d.mkdir(parents=True)\n\n if google_colab:\n from google.colab import files\n print(\"Выберите DICOM-файлы или ZIP-архив:\")\n uploaded = files.upload()\n for name, data in uploaded.items():\n (UPLOAD_DIR / name).write_bytes(data)\n else:\n print(f\"Положите файлы в {UPLOAD_DIR}\")\n\n for zf in UPLOAD_DIR.glob(\"*.zip\"):\n print(f\"Распаковка {zf.name}...\")\n with zipfile.ZipFile(zf) as z:\n z.extractall(DICOM_DIR)\n\n for f in UPLOAD_DIR.iterdir():\n if f.suffix.lower() != \".zip\":\n shutil.copy2(f, DICOM_DIR / f.name)\n\ndicom_files = sorted(\n [p for p in DICOM_DIR.rglob(\"*\") if p.is_file() and not p.name.startswith(\".\") and p.name != \"DICOMDIR\"]\n)\nprint(f\"Найдено файлов: {len(dicom_files)}\")" | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": null, | |
| "metadata": { | |
| "cellView": "form" | |
| }, | |
| "outputs": [], | |
| "source": "#@title 3. Чтение DICOM — выбор серии и подготовка объёма\n\nimport pydicom\nimport numpy as np\nimport PIL.Image\nimport io, base64\nimport IPython.display\nfrom collections import defaultdict\n\nMAX_SLICES = 85\n\n# --- Чтение всех DICOM ---\nall_dcm = []\nskipped = 0\nfor path in dicom_files:\n try:\n dcm = pydicom.dcmread(str(path))\n if hasattr(dcm, \"pixel_array\"):\n all_dcm.append(dcm)\n else:\n skipped += 1\n except Exception:\n skipped += 1\n\nif skipped:\n print(f\"Пропущено файлов (не DICOM / без пикселей): {skipped}\")\n\n# --- Группировка по SeriesInstanceUID ---\nseries_map = defaultdict(list)\nfor dcm in all_dcm:\n uid = getattr(dcm, \"SeriesInstanceUID\", \"unknown\")\n series_map[uid].append(dcm)\n\nprint(f\"Найдено серий: {len(series_map)}\\n\")\nprint(f\"{'#':<4} {'Срезов':<8} {'Описание серии':<40} {'Толщина':<10} {'SeriesInstanceUID'}\")\nprint(\"-\" * 100)\n\nseries_list = []\nfor i, (uid, dcms) in enumerate(series_map.items(), 1):\n desc = getattr(dcms[0], \"SeriesDescription\", \"—\")\n thickness = getattr(dcms[0], \"SliceThickness\", \"—\")\n rows = getattr(dcms[0], \"Rows\", \"?\")\n cols = getattr(dcms[0], \"Columns\", \"?\")\n print(f\"{i:<4} {len(dcms):<8} {str(desc):<40} {str(thickness):<10} {uid[:40]}...\")\n series_list.append((uid, dcms, desc, thickness))\n\nif len(series_list) == 1:\n chosen = 0\n print(f\"\\nОдна серия — используется автоматически.\")\nelse:\n print(f\"\\n⬇️ Укажите номер серии в следующей ячейке (Шаг 3b).\")" | |
| }, | |
| { | |
| "cell_type": "code", | |
| "source": "#@title 3b. Выбор серии и подготовка срезов { display-mode: \"form\" }\n\n#@markdown Укажите номер серии из таблицы выше:\nseries_number = 1 #@param {type:\"integer\"}\n#@markdown ---\n\nchosen = series_number - 1\nif chosen < 0 or chosen >= len(series_list):\n raise ValueError(f\"Номер серии должен быть от 1 до {len(series_list)}\")\n\nuid, chosen_dcms, desc, thickness = series_list[chosen]\nprint(f\"Выбрана серия #{series_number}: {desc} ({len(chosen_dcms)} срезов, толщина {thickness} мм)\")\n\n# --- Сортировка ---\ndef sort_key(dcm):\n if hasattr(dcm, \"ImagePositionPatient\"):\n return float(dcm.ImagePositionPatient[2])\n if hasattr(dcm, \"InstanceNumber\"):\n return int(dcm.InstanceNumber)\n return 0\n\nchosen_dcms.sort(key=sort_key)\nTOTAL = len(chosen_dcms)\n\n# --- HU conversion ---\nall_hu = []\nfor dcm in chosen_dcms:\n px = dcm.pixel_array.astype(np.float32)\n slope = float(getattr(dcm, \"RescaleSlope\", 1))\n intercept = float(getattr(dcm, \"RescaleIntercept\", 0))\n all_hu.append(px * slope + intercept)\n\n# --- Windowing helpers ---\nWINDOWS = [(-1024, 1024), (-135, 215), (0, 80)]\n\ndef norm(arr, vmin, vmax):\n arr = np.clip(arr, vmin, vmax).astype(np.float32)\n return (arr - vmin) / (vmax - vmin) * 255.0\n\ndef window_and_encode(hu_slices):\n b64_list, rgb_list = [], []\n for s in hu_slices:\n rgb = np.stack([norm(s, lo, hi) for lo, hi in WINDOWS], axis=-1)\n rgb = np.round(rgb).astype(np.uint8)\n rgb_list.append(rgb)\n buf = io.BytesIO()\n PIL.Image.fromarray(rgb).save(buf, format=\"JPEG\", quality=90)\n b64_list.append(base64.b64encode(buf.getvalue()).decode(\"utf-8\"))\n return b64_list, rgb_list\n\ndef subsample(volume, max_n):\n n = len(volume)\n if n <= max_n:\n return volume, list(range(n))\n indices = [int(round(i / max_n * (n - 1))) for i in range(1, max_n + 1)]\n return [volume[i] for i in indices], indices\n\n# --- Обзорная выборка ---\noverview_hu, overview_orig_idx = subsample(all_hu, MAX_SLICES)\nslices_b64, windowed_rgb = window_and_encode(overview_hu)\n\nprint(f\"\\nОбзорный набор: {len(slices_b64)} срезов\" + (f\" (из {TOTAL})\" if TOTAL > MAX_SLICES else \"\"))\nif TOTAL > MAX_SLICES:\n step = TOTAL / len(overview_orig_idx)\n print(f\"Шаг выборки: ~1 срез из {step:.1f}\")\n\n# --- GIF preview ---\nimgs = [PIL.Image.fromarray(s) for s in windowed_rgb]\nwith io.BytesIO() as buf:\n imgs[0].save(buf, format=\"GIF\", loop=0, save_all=True,\n append_images=imgs[1:], optimize=False, duration=80)\n gif = buf.getvalue()\n\nprint(f\"\\nПросмотр (цветное — норма, RGB = 3 окна HU):\")\nIPython.display.display(IPython.display.Image(data=gif, format=\"GIF\"))", | |
| "metadata": {}, | |
| "execution_count": null, | |
| "outputs": [] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": null, | |
| "metadata": { | |
| "cellView": "form" | |
| }, | |
| "outputs": [], | |
| "source": "#@title 4. Обзорный анализ { display-mode: \"form\" }\n\n#@markdown ---\n#@markdown ### Режим:\nanalysis_type = \"Полный анализ грудной клетки\" #@param [\"Полный анализ грудной клетки\", \"Только лёгкие\", \"Онкопоиск\", \"Свой вопрос\"]\ncustom_query = \"\" #@param {type:\"string\"}\n#@markdown ---\n\nimport requests, json, time\nfrom IPython.display import display, Markdown\n\nMODAL_URL = \"https://coolk8--medgemma-ct-medgemmact-web-analyze.modal.run\"\n\nPROMPTS = {\n \"Полный анализ грудной клетки\": (\n \"You are an experienced radiologist reviewing a chest CT scan. \"\n \"Please review the slices provided below carefully.\",\n \"\\n\\nBased on the visual evidence in the slices provided above, \"\n \"please provide a comprehensive radiology report covering the lungs, \"\n \"pleura, mediastinum, heart, great vessels, and osseous structures. \"\n \"Comment on any abnormalities found. Provide your reasoning and \"\n \"conclude with an overall impression.\"\n ),\n \"Только лёгкие\": (\n \"You are an experienced pulmonologist reviewing a chest CT scan. \"\n \"Please review the slices provided below carefully.\",\n \"\\n\\nBased on the visual evidence in the slices provided above, \"\n \"focus on the lung parenchyma. Comment on any ground-glass opacities, \"\n \"consolidations, nodules, emphysematous changes, fibrotic changes, \"\n \"bronchiectasis, or interstitial abnormalities. \"\n \"Provide your reasoning and conclude with an overall impression.\"\n ),\n \"Онкопоиск\": (\n \"You are an experienced radiologist screening a chest CT scan \"\n \"for signs of malignancy. Please review the slices provided below carefully.\",\n \"\\n\\nBased on the visual evidence in the slices provided above, \"\n \"identify any pulmonary nodules, masses, or suspicious consolidations. \"\n \"For each finding, describe its location, approximate size, margins, \"\n \"and density. Comment on mediastinal and hilar lymphadenopathy, \"\n \"pleural abnormalities, and osseous lesions. \"\n \"Provide your reasoning and conclude with an overall assessment.\"\n ),\n}\n\nif analysis_type == \"Свой вопрос\" and custom_query.strip():\n instruction = \"You are an experienced radiologist. Please review the slices provided below carefully.\"\n query = f\"\\n\\n{custom_query.strip()}\"\nelif analysis_type in PROMPTS:\n instruction, query = PROMPTS[analysis_type]\nelse:\n instruction, query = list(PROMPTS.values())[0]\n\nprint(f\"Режим: {analysis_type}\")\nprint(f\"Срезов: {len(slices_b64)}\")\nprint(f\"Отправка на Modal...\")\n\nt0 = time.time()\nresp = requests.post(\n MODAL_URL,\n json={\"slices_b64\": slices_b64, \"instruction\": instruction, \"query\": query},\n timeout=600,\n)\nelapsed = time.time() - t0\n\nif resp.status_code == 200:\n overview_result = resp.json()[\"result\"]\n print(f\"Готово за {elapsed:.0f} сек\\n\")\n display(Markdown(f\"---\\n### Результат анализа MedGemma\\n\\n{overview_result}\\n\\n---\"))\nelse:\n print(f\"Ошибка {resp.status_code}: {resp.text}\")" | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": null, | |
| "metadata": { | |
| "cellView": "form" | |
| }, | |
| "outputs": [], | |
| "source": "#@title 5a. Прицельный анализ — превью диапазона { display-mode: \"form\" }\n\n#@markdown Укажите диапазон **оригинальных** номеров срезов.\n#@markdown Меняйте и перезапускайте пока не увидите нужную зону.\n#@markdown ---\nstart_slice = 1 #@param {type:\"integer\"}\nend_slice = 50 #@param {type:\"integer\"}\n#@markdown ---\n\nstart_idx = max(0, start_slice - 1)\nend_idx = min(TOTAL, end_slice)\nregion_hu = all_hu[start_idx:end_idx]\n\nif not region_hu:\n print(f\"Пустой диапазон! Укажите значения от 1 до {TOTAL}.\")\nelse:\n region_sampled, region_idx = subsample(region_hu, MAX_SLICES)\n region_b64, region_rgb = window_and_encode(region_sampled)\n\n coverage = len(region_sampled) / len(region_hu) * 100\n print(f\"Зона: оригинальные срезы {start_slice}–{end_slice} ({len(region_hu)} срезов)\")\n print(f\"Будет отправлено: {len(region_sampled)} срезов ({coverage:.0f}% покрытие зоны)\")\n\n # GIF preview\n t_imgs = [PIL.Image.fromarray(s) for s in region_rgb]\n with io.BytesIO() as buf:\n t_imgs[0].save(buf, format=\"GIF\", loop=0, save_all=True,\n append_images=t_imgs[1:], optimize=False, duration=80)\n t_gif = buf.getvalue()\n\n print(f\"\\nПревью зоны {start_slice}–{end_slice}:\")\n IPython.display.display(IPython.display.Image(data=t_gif, format=\"GIF\"))\n print(f\"\\n✅ Если зона верная — запустите следующую ячейку (5b).\")\n print(f\"🔄 Если нет — измените start_slice/end_slice и перезапустите эту ячейку.\")" | |
| }, | |
| { | |
| "cell_type": "code", | |
| "source": "#@title 5b. Отправить зону на анализ { display-mode: \"form\" }\n\n#@markdown Запускайте только после проверки превью в 5a.\n#@markdown ---\ntargeted_query = \"Based on the visual evidence in the slices provided above, please describe all abnormalities you observe. Comment on any nodules, masses, consolidations, ground-glass opacities, or other findings. Provide your reasoning and conclude with an overall impression.\" #@param {type:\"string\"}\n#@markdown ---\n\nif not region_b64:\n print(\"Сначала запустите шаг 5a!\")\nelse:\n targeted_instruction = (\n \"You are an experienced radiologist performing a detailed review \"\n \"of a focused region of a chest CT scan. \"\n \"Please review the slices provided below carefully.\"\n )\n\n print(f\"Зона: срезы {start_slice}–{end_slice} ({len(region_b64)} срезов)\")\n print(\"Отправка на Modal...\")\n\n t0 = time.time()\n resp_t = requests.post(\n MODAL_URL,\n json={\"slices_b64\": region_b64, \"instruction\": targeted_instruction, \"query\": f\"\\n\\n{targeted_query}\"},\n timeout=600,\n )\n elapsed = time.time() - t0\n\n if resp_t.status_code == 200:\n targeted_result = resp_t.json()[\"result\"]\n print(f\"Готово за {elapsed:.0f} сек\\n\")\n display(Markdown(\n f\"---\\n### Прицельный анализ (срезы {start_slice}–{end_slice})\\n\\n\"\n f\"{targeted_result}\\n\\n---\"\n ))\n else:\n print(f\"Ошибка {resp_t.status_code}: {resp_t.text}\")", | |
| "metadata": {}, | |
| "execution_count": null, | |
| "outputs": [] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": null, | |
| "metadata": { | |
| "cellView": "form" | |
| }, | |
| "outputs": [], | |
| "source": "#@title 6. Свободный вопрос (опционально)\n\nq_start = 1 #@param {type:\"integer\"}\nq_end = 0 #@param {type:\"integer\"}\n#@markdown ↑ `q_end = 0` — использовать весь объём (обзорные 85 срезов)\nquestion = \"Is there evidence of interstitial lung disease or fibrosis? Please provide your reasoning.\" #@param {type:\"string\"}\n\nif q_end == 0:\n q_b64 = slices_b64\n range_label = \"весь объём (обзорные)\"\nelse:\n q_region = all_hu[max(0, q_start-1):min(TOTAL, q_end)]\n q_sampled, _ = subsample(q_region, MAX_SLICES)\n q_b64, _ = window_and_encode(q_sampled)\n range_label = f\"срезы {q_start}–{q_end} ({len(q_sampled)} шт)\"\n\nprint(f\"Диапазон: {range_label}\")\nprint(f\"Вопрос: {question[:80]}...\")\nprint(\"Отправка...\")\n\nt0 = time.time()\nresp_q = requests.post(\n MODAL_URL,\n json={\n \"slices_b64\": q_b64,\n \"instruction\": \"You are an experienced radiologist. Please review the slices provided below carefully.\",\n \"query\": f\"\\n\\n{question}\",\n },\n timeout=600,\n)\nelapsed = time.time() - t0\n\nif resp_q.status_code == 200:\n q_result = resp_q.json()[\"result\"]\n print(f\"Готово за {elapsed:.0f} сек\\n\")\n display(Markdown(f\"---\\n### Ответ ({range_label})\\n\\n{q_result}\\n\\n---\"))\nelse:\n print(f\"Ошибка {resp_q.status_code}: {resp_q.text}\")" | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "metadata": {}, | |
| "source": [ | |
| "---\n", | |
| "\n", | |
| "> **MedGemma** — экспериментальная модель. Результаты носят ознакомительный характер и не заменяют заключение врача-рентгенолога.\n", | |
| ">\n", | |
| "> Модель: `google/medgemma-1.5-4b-it` (bf16, без квантизации) на L4 GPU через [Modal](https://modal.com)" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "source": "#@title 7. Gemini 3.1 Pro + Claude Opus 4.6 (через OpenRouter) { display-mode: \"form\" }\n\n#@markdown Оба получают **ВСЕ** срезы серии в стандартном ч/б лёгочном окне.\n#@markdown\n#@markdown **Ключ OpenRouter:** добавьте в Colab Secrets (🔑 слева) с именем `OPENROUTER_KEY`\n#@markdown ---\nor_series = 2 #@param {type:\"integer\"}\nor_query = \"Based on the visual evidence in the slices provided above, please provide a comprehensive radiology report. Comment on any abnormalities in the lungs, pleura, mediastinum, heart, and osseous structures. Provide your reasoning and conclude with an overall impression.\" #@param {type:\"string\"}\n#@markdown ---\n\nimport requests, time, json\nfrom IPython.display import display, Markdown\n\n# --- Ключ из Colab Secrets ---\ntry:\n from google.colab import userdata\n OPENROUTER_KEY = userdata.get(\"OPENROUTER_KEY\")\nexcept Exception:\n raise ValueError(\"Добавьте OPENROUTER_KEY в Colab Secrets (🔑 слева)\")\n\nOPENROUTER_URL = \"https://openrouter.ai/api/v1/chat/completions\"\nGEMINI_MODEL = \"google/gemini-3.1-pro-preview\"\nCLAUDE_MODEL = \"anthropic/claude-opus-4.6\"\n\n# --- Подготовка серии ---\nidx = or_series - 1\nif idx < 0 or idx >= len(series_list):\n raise ValueError(f\"Номер серии от 1 до {len(series_list)}\")\n\nor_uid, or_dcms, or_desc, or_thick = series_list[idx]\nor_dcms_sorted = sorted(or_dcms, key=sort_key)\nprint(f\"Серия #{or_series}: {or_desc} ({len(or_dcms_sorted)} срезов)\")\n\nor_hu = []\nfor dcm in or_dcms_sorted:\n px = dcm.pixel_array.astype(np.float32)\n slope = float(getattr(dcm, \"RescaleSlope\", 1))\n intercept = float(getattr(dcm, \"RescaleIntercept\", 0))\n or_hu.append(px * slope + intercept)\n\n# --- Стандартное лёгочное окно (grayscale), как видит рентгенолог ---\n# WW=1500, WC=-600 → диапазон -1350..150 HU\nLUNG_WC = -600\nLUNG_WW = 1500\nLUNG_MIN = LUNG_WC - LUNG_WW / 2 # -1350\nLUNG_MAX = LUNG_WC + LUNG_WW / 2 # 150\nRESIZE_TO = 256\nJPEG_QUALITY = 75\n\nor_b64 = []\nfor s in or_hu:\n gray = np.clip(s, LUNG_MIN, LUNG_MAX).astype(np.float32)\n gray = (gray - LUNG_MIN) / (LUNG_MAX - LUNG_MIN) * 255.0\n gray = np.round(gray).astype(np.uint8)\n img = PIL.Image.fromarray(gray, mode=\"L\").resize((RESIZE_TO, RESIZE_TO), PIL.Image.LANCZOS)\n buf = io.BytesIO()\n img.save(buf, format=\"JPEG\", quality=JPEG_QUALITY)\n or_b64.append(base64.b64encode(buf.getvalue()).decode(\"utf-8\"))\n\ntotal_mb = sum(len(b) for b in or_b64) * 3 / 4 / 1024 / 1024\nprint(f\"Оба получат: {len(or_b64)} срезов ({total_mb:.1f} MB, {RESIZE_TO}px, lung window, grayscale)\")\n\ninstruction = (\n \"You are an experienced radiologist reviewing a chest CT scan \"\n \"displayed in standard lung window (WW=1500, WC=-600). \"\n \"Please review the slices provided below carefully.\"\n)\n\ndef build_openrouter_messages(slices_b64, instruction, query):\n content = [{\"type\": \"text\", \"text\": instruction}]\n for i, b64 in enumerate(slices_b64, 1):\n content.append({\n \"type\": \"image_url\",\n \"image_url\": {\"url\": f\"data:image/jpeg;base64,{b64}\"}\n })\n content.append({\"type\": \"text\", \"text\": query})\n return [{\"role\": \"user\", \"content\": content}]\n\ndef call_openrouter(model, messages, timeout=600):\n headers = {\n \"Authorization\": f\"Bearer {OPENROUTER_KEY}\",\n \"Content-Type\": \"application/json\",\n \"HTTP-Referer\": \"https://colab.research.google.com\",\n \"X-Title\": \"CT Analysis Notebook\",\n }\n body = {\n \"model\": model,\n \"messages\": messages,\n \"max_tokens\": 4000,\n }\n resp = requests.post(OPENROUTER_URL, headers=headers, json=body, timeout=timeout)\n if resp.status_code == 200:\n data = resp.json()\n return data[\"choices\"][0][\"message\"][\"content\"]\n else:\n return f\"Ошибка {resp.status_code}: {resp.text[:500]}\"\n\nmsgs = build_openrouter_messages(or_b64, instruction, f\"\\n\\n{or_query}\")\n\n# --- Gemini ---\nprint(f\"\\n{'='*60}\")\nprint(f\"Отправка в Gemini 3.1 Pro ({len(or_b64)} срезов)...\")\nt0 = time.time()\ngemini_result = call_openrouter(GEMINI_MODEL, msgs)\ngemini_time = time.time() - t0\nprint(f\"Gemini: готово за {gemini_time:.0f} сек\")\n\n# --- Claude ---\nprint(f\"\\nОтправка в Claude Opus 4.6 ({len(or_b64)} срезов)...\")\nt0 = time.time()\nclaude_result = call_openrouter(CLAUDE_MODEL, msgs)\nclaude_time = time.time() - t0\nprint(f\"Claude: готово за {claude_time:.0f} сек\")\n\n# --- Вывод ---\ndisplay(Markdown(\n f\"---\\n\"\n f\"### Gemini 3.1 Pro ({len(or_b64)} срезов, {gemini_time:.0f} сек)\\n\\n\"\n f\"{gemini_result}\\n\\n\"\n f\"---\\n\"\n f\"### Claude Opus 4.6 ({len(or_b64)} срезов, {claude_time:.0f} сек)\\n\\n\"\n f\"{claude_result}\\n\\n\"\n f\"---\"\n))", | |
| "metadata": {}, | |
| "execution_count": null, | |
| "outputs": [] | |
| } | |
| ] | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment