From 71d9700f5533dec6b4601f3bbc72746cbfb55ff9 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Wed, 28 Oct 2026 18:57:41 +0700 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D0=B8=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5:=20DUB-AI=20?= =?UTF-8?q?=E2=86=92=20grabber-ai=20(=D0=BE=D0=B1=D1=80=D0=B0=D0=B7,=20?= =?UTF-8?q?=D1=81=D0=B5=D1=80=D0=B2=D0=B8=D1=81,=20=D0=B4=D0=BE=D0=BA?= =?UTF-8?q?=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=D1=86=D0=B8=D1=8F,=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B8=D0=BC=D0=B5=D1=80=D1=8B)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- .env.example | 4 ++-- .gitea/workflows/docker.yml | 4 ++-- Dockerfile | 4 ++-- README.md | 2 +- docker-compose.yml | 8 ++++---- docs/README.md | 2 +- docs/docker.md | 26 +++++++++++++------------- docs/integration.md | 4 ++-- docs/training-windows.md | 2 +- examples/powershell/predict.ps1 | 2 +- examples/rust/http_client/Cargo.lock | 2 +- examples/rust/http_client/Cargo.toml | 2 +- examples/rust/http_client/src/main.rs | 2 +- examples/rust/onnx_local/Cargo.lock | 2 +- examples/rust/onnx_local/Cargo.toml | 2 +- examples/rust/onnx_local/README.md | 6 +++--- examples/rust/onnx_local/src/main.rs | 2 +- examples/typescript/onnx_local.mjs | 2 +- examples/typescript/predict.mjs | 2 +- examples/typescript/predict.ts | 2 +- src/__init__.py | 2 +- src/service.py | 4 ++-- tests/run_tests.py | 2 +- tools/collect.py | 2 +- tools/retrain.py | 2 +- tools/ui.py | 2 +- 26 files changed, 48 insertions(+), 48 deletions(-) diff --git a/.env.example b/.env.example index ec2a11a..a0daac1 100644 --- a/.env.example +++ b/.env.example @@ -1,12 +1,12 @@ # Скопируйте в .env и поправьте. Используется docker-compose.yml. # Образ: локальная сборка или из реестра Gitea (host/owner/repo:tag) -DUB_IMAGE=dub-ai:local +DUB_IMAGE=grabber-ai:local # Порт на хосте DUB_PUBLISH_PORT=8000 -# FlareSolverr: адрес, по которому ему доступен контейнер dub-ai. +# FlareSolverr: адрес, по которому ему доступен контейнер grabber-ai. # В compose поднимается рядом (http://flaresolverr:8191); внешний — укажите его адрес. # Пусто — без обхода Cloudflare. DUB_FLARESOLVERR_URL=http://flaresolverr:8191 diff --git a/.gitea/workflows/docker.yml b/.gitea/workflows/docker.yml index d1f7233..515c56a 100644 --- a/.gitea/workflows/docker.yml +++ b/.gitea/workflows/docker.yml @@ -1,4 +1,4 @@ -# Сборка и публикация Docker-образа DUB-AI в реестр контейнеров Gitea. +# Сборка и публикация Docker-образа grabber-ai в реестр контейнеров Gitea. # # Запускается: пуш в main/master, тег v*, вручную (workflow_dispatch). # Нужно один раз настроить (подробно — docs/docker.md, раздел «CI/CD в Gitea»): @@ -17,7 +17,7 @@ on: workflow_dispatch: env: - IMAGE_NAME: dub-ai + IMAGE_NAME: grabber-ai # Публичный адрес реестра (HTTPS). Нельзя брать из GITHUB_SERVER_URL: внутри раннера это внутренний # адрес Gitea (http://192.168.x.x:3000), у него нет HTTPS. Переопределяется переменной REGISTRY_HOST. REGISTRY_HOST: ${{ vars.REGISTRY_HOST || 'git.shx.one' }} diff --git a/Dockerfile b/Dockerfile index 1424d64..2f60063 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,8 @@ # syntax=docker/dockerfile:1 -# DUB-AI: HTTP-сервис определения дорожек озвучки (FastAPI), CPU. +# grabber-ai: HTTP-сервис определения дорожек озвучки (FastAPI), CPU. # # python tools/pack_models.py --stage docker-models # положить модели в образ (необязательно) -# docker build -t dub-ai . +# docker build -t grabber-ai . # # Без шага pack_models образ собирается без моделей — тогда смонтируйте каталог: -v ./models:/app/models:ro FROM python:3.12-slim diff --git a/README.md b/README.md index 8b28d7b..3008332 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# DUB-AI — нейро-модель «тип озвучки + автор + 18+» +# grabber-ai — нейро-модель «тип озвучки + автор + 18+» По тексту дорожки озвучки (или HTML-странице релиза с трекеров rutracker / kinozal / nnm-club) модель определяет: diff --git a/docker-compose.yml b/docker-compose.yml index 6365fd7..f602df4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ -# DUB-AI + FlareSolverr. docker compose up -d (настройки — в .env, см. .env.example) +# grabber-ai + FlareSolverr. docker compose up -d (настройки — в .env, см. .env.example) services: - dub-ai: - image: ${DUB_IMAGE:-dub-ai:local} + grabber-ai: + image: ${DUB_IMAGE:-grabber-ai:local} build: . restart: unless-stopped ports: @@ -24,4 +24,4 @@ services: restart: unless-stopped environment: LOG_LEVEL: info - # порт 8191 наружу не публикуется: доступен только контейнеру dub-ai + # порт 8191 наружу не публикуется: доступен только контейнеру grabber-ai diff --git a/docs/README.md b/docs/README.md index beec0be..0ee8f1e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,4 +1,4 @@ -# Документация DUB-AI +# Документация grabber-ai | Документ | Содержание | |---|---| diff --git a/docs/docker.md b/docs/docker.md index 11fd48b..bc7507e 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -1,11 +1,11 @@ -# Docker: запуск DUB-AI на VPS и сборка в Gitea CI/CD +# Docker: запуск grabber-ai на VPS и сборка в Gitea CI/CD Контейнер запускает HTTP-сервис (`python -m src.service`): по ссылке на раздачу или по HTML определяет дорожки озвучки (тип, студия, 18+, язык, роль, параметры аудио). Для трекеров за Cloudflare сервис обращается к вашему **FlareSolverr**, адрес которого задаётся переменной окружения. ``` -клиент ──HTTP──▶ dub-ai (:8000) ──▶ трекер (rutracker / nnmclub / kinozal) +клиент ──HTTP──▶ grabber-ai (:8000) ──▶ трекер (rutracker / nnmclub / kinozal) (grabber) │ ▲ при 403/503 или странице-заглушке └─▶ FlareSolverr ──┘ (DUB_FLARESOLVERR_URL) ``` @@ -72,7 +72,7 @@ curl -X POST http://localhost:8000/predict \ | `DUB_SEGMENTER_DIR` | `/app/models/segmenter` | каталог сегментатора страниц; нет каталога — работают правила | | `DUB_HOST`, `DUB_PORT` | `0.0.0.0`, `8000` | адрес/порт внутри контейнера (наружу — `DUB_PUBLISH_PORT` в compose) | | `DUB_DEVICE` | авто | `cpu` или `cuda` (в образе только CPU-сборка torch) | -| `DUB_IMAGE` | `dub-ai:local` | (compose) какой образ запускать, напр. из реестра Gitea | +| `DUB_IMAGE` | `grabber-ai:local` | (compose) какой образ запускать, напр. из реестра Gitea | | `DUB_PUBLISH_PORT` | `8000` | (compose) порт на хосте | В образе жёстко заданы `HF_HUB_OFFLINE=1` и `TRANSFORMERS_OFFLINE=1`: сервис ничего не скачивает с @@ -88,19 +88,19 @@ Hugging Face. Все файлы моделей, включая архитект Три способа подключить: 1. **Вместе с compose (по умолчанию).** Сервис `flaresolverr` поднимается рядом, порт наружу не - публикуется, `dub-ai` ходит к нему по `http://flaresolverr:8191`. + публикуется, `grabber-ai` ходит к нему по `http://flaresolverr:8191`. 2. **Свой FlareSolverr на другом хосте.** В `.env`: `DUB_FLARESOLVERR_URL=http://192.168.0.24:8191`, и - запускайте только `docker compose up -d dub-ai` (контейнер FlareSolverr не нужен). Адрес должен быть + запускайте только `docker compose up -d grabber-ai` (контейнер FlareSolverr не нужен). Адрес должен быть достижим из контейнера: с VPS до домашней сети — через VPN/туннель, не открывайте FlareSolverr в интернет. 3. **`docker run`:** ```bash - docker run -d --name dub-ai -p 8000:8000 \ + docker run -d --name grabber-ai -p 8000:8000 \ -e DUB_FLARESOLVERR_URL=http://flaresolverr:8191 -e DUB_API_TOKEN=... \ - --network <сеть_с_flaresolverr> dub-ai:local + --network <сеть_с_flaresolverr> grabber-ai:local ``` Проверить, что адрес подхватился: `GET /health` → `"flaresolverr": true`. Проверка связи: -`docker compose exec dub-ai python -c "import urllib.request as u; print(u.urlopen('http://flaresolverr:8191/').read()[:80])"`. +`docker compose exec grabber-ai python -c "import urllib.request as u; print(u.urlopen('http://flaresolverr:8191/').read()[:80])"`. Замечания: * FlareSolverr нестабилен: бывают `Timeout after 15.0 seconds` и ответ ~40 с. Это ошибка 400/502 для @@ -172,11 +172,11 @@ python tools/pack_models.py # -> models-bundle.tar.gz (≈ 75 # выкладка в реестр пакетов Gitea (версия — любая строка, например дата или хэш модели) curl --fail -H "Authorization: token " \ --upload-file models-bundle.tar.gz \ - https:///api/packages//generic/dub-ai-models/2026-10-28/models-bundle.tar.gz + https:///api/packages//generic/grabber-ai-models/2026-10-28/models-bundle.tar.gz ``` Ссылка для скачивания (она же `MODELS_URL`): -`https:///api/packages//generic/dub-ai-models/2026-10-28/models-bundle.tar.gz`. +`https:///api/packages//generic/grabber-ai-models/2026-10-28/models-bundle.tar.gz`. Если у вашей Gitea ограничен размер пакета (`[packages] LIMIT_SIZE_GENERIC`, по умолчанию без лимита, но nginx перед ней часто режет загрузки ~1 МБ), поднимите лимиты или используйте способ Б. @@ -186,7 +186,7 @@ nginx перед ней часто режет загрузки ~1 МБ), под ## CI/CD в Gitea Workflow `.gitea/workflows/docker.yml` собирает образ и публикует его в **реестр контейнеров Gitea** -(`///dub-ai`). +(`///grabber-ai`). ### Что настроить один раз @@ -230,7 +230,7 @@ Workflow `.gitea/workflows/docker.yml` собирает образ и публи ``` 2. Положить на VPS `docker-compose.yml` и `.env` (из `.env.example`): ``` - DUB_IMAGE=///dub-ai:latest + DUB_IMAGE=///grabber-ai:latest DUB_API_TOKEN= DUB_FLARESOLVERR_URL=http://flaresolverr:8191 ``` @@ -261,7 +261,7 @@ Workflow `.gitea/workflows/docker.yml` собирает образ и публи | `/predict` → 400 «страница недоступна: Cloudflare challenge … (задайте FlareSolverr)» | не задан `DUB_FLARESOLVERR_URL` или контейнер его не видит (`/health` → `flaresolverr`), проверьте сеть/имя сервиса | | 400 `FlareSolverr: … Timeout` | FlareSolverr не успел решить challenge; повторите запрос, увеличьте `DUB_FETCH_TIMEOUT`, перезапустите FlareSolverr | | 401 | не передан токен или он отличается от `DUB_API_TOKEN` | -| Контейнер `unhealthy` в первые минуты | модель ещё грузится; `docker logs dub-ai`. Если не проходит — не хватает памяти (`docker stats`) | +| Контейнер `unhealthy` в первые минуты | модель ещё грузится; `docker logs grabber-ai`. Если не проходит — не хватает памяти (`docker stats`) | | CI: `Cannot connect to the Docker daemon` | у раннера нет доступа к Docker (см. «Что настроить один раз») | | CI: `server gave HTTP response to HTTPS client` | workflow взял внутренний адрес Gitea; должен быть `REGISTRY_HOST` = `git.shx.one` (уже так в workflow) | | CI: `unauthorized` при push | у `ACTION_PAT` нет `write:package` или токен принадлежит не владельцу/участник пространства имён репозитория | diff --git a/docs/integration.md b/docs/integration.md index ce74bfc..fa25cf6 100644 --- a/docs/integration.md +++ b/docs/integration.md @@ -145,7 +145,7 @@ kinozal требует авторизации, поэтому grabber скачи (`KinozalClient`) и отправляет сервису готовый HTML вместе с заголовком раздачи: ```php -$ch = curl_init('http://dub-ai:8000/predict'); +$ch = curl_init('http://grabber-ai:8000/predict'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10, CURLOPT_HTTPHEADER => ['Content-Type: application/json'], @@ -192,7 +192,7 @@ python -m src.export_onnx # -> models/best-onnx/ + reports/onnx_parit ```bash cd examples/rust/onnx_local && cargo build ORT_DYLIB_PATH="$(find ../../.venv -name onnxruntime.dll | head -1)" \ - ./target/debug/dub-ai-onnx-local.exe ../../models/best-onnx \ + ./target/debug/grabber-ai-onnx-local.exe ../../models/best-onnx \ "Перевод: Дублированный | MovieDalen" ``` diff --git a/docs/training-windows.md b/docs/training-windows.md index 7017cde..20ecc9a 100644 --- a/docs/training-windows.md +++ b/docs/training-windows.md @@ -10,7 +10,7 @@ torch 2.11.0+cu128). ## Установка (один раз) ```bat -cd C:\Users\shats\Dev\DUB-AI +cd C:\Users\shats\Dev\grabber-ai python -m venv .venv .venv\Scripts\python -m pip install --upgrade pip diff --git a/examples/powershell/predict.ps1 b/examples/powershell/predict.ps1 index 58c9b96..88d813e 100644 --- a/examples/powershell/predict.ps1 +++ b/examples/powershell/predict.ps1 @@ -1,4 +1,4 @@ -# Вызов сервиса DUB-AI из PowerShell. +# Вызов сервиса grabber-ai из PowerShell. # # Примеры (из корня проекта): # powershell -NoProfile -ExecutionPolicy Bypass -File examples\powershell\predict.ps1 -Text "Перевод: Дублированный | MovieDalen" diff --git a/examples/rust/http_client/Cargo.lock b/examples/rust/http_client/Cargo.lock index 3b18765..448c2bf 100644 --- a/examples/rust/http_client/Cargo.lock +++ b/examples/rust/http_client/Cargo.lock @@ -98,7 +98,7 @@ dependencies = [ ] [[package]] -name = "dub-ai-client" +name = "grabber-ai-client" version = "0.1.0" dependencies = [ "reqwest", diff --git a/examples/rust/http_client/Cargo.toml b/examples/rust/http_client/Cargo.toml index d2a11fc..69a8185 100644 --- a/examples/rust/http_client/Cargo.toml +++ b/examples/rust/http_client/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "dub-ai-client" +name = "grabber-ai-client" version = "0.1.0" edition = "2021" diff --git a/examples/rust/http_client/src/main.rs b/examples/rust/http_client/src/main.rs index 3520f6c..1a23561 100644 --- a/examples/rust/http_client/src/main.rs +++ b/examples/rust/http_client/src/main.rs @@ -1,4 +1,4 @@ -//! HTTP-клиент DUB-AI (путь А: сервис `python -m src.service`). +//! HTTP-клиент grabber-ai (путь А: сервис `python -m src.service`). //! //! Запуск: //! cargo run -- --text "Перевод: Дублированный | MovieDalen" diff --git a/examples/rust/onnx_local/Cargo.lock b/examples/rust/onnx_local/Cargo.lock index 07bb7d5..538ef8a 100644 --- a/examples/rust/onnx_local/Cargo.lock +++ b/examples/rust/onnx_local/Cargo.lock @@ -228,7 +228,7 @@ dependencies = [ ] [[package]] -name = "dub-ai-onnx-local" +name = "grabber-ai-onnx-local" version = "0.1.0" dependencies = [ "ort", diff --git a/examples/rust/onnx_local/Cargo.toml b/examples/rust/onnx_local/Cargo.toml index 1e1c5fc..ef08258 100644 --- a/examples/rust/onnx_local/Cargo.toml +++ b/examples/rust/onnx_local/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "dub-ai-onnx-local" +name = "grabber-ai-onnx-local" version = "0.1.0" edition = "2021" diff --git a/examples/rust/onnx_local/README.md b/examples/rust/onnx_local/README.md index 042ef5c..bb4206a 100644 --- a/examples/rust/onnx_local/README.md +++ b/examples/rust/onnx_local/README.md @@ -1,10 +1,10 @@ -# onnx_local — локальный инференс DUB-AI на Rust (без Python) +# onnx_local — локальный инференс grabber-ai на Rust (без Python) ```bash cargo build # onnxruntime.dll грузится динамически; можно взять из pip-пакета onnxruntime # (уже есть в .venv проекта) или из официального релиза onnxruntime -ORT_DYLIB_PATH=<путь>/onnxruntime.dll ./target/debug/dub-ai-onnx-local.exe \ +ORT_DYLIB_PATH=<путь>/onnxruntime.dll ./target/debug/grabber-ai-onnx-local.exe \ "<путь к models/best-onnx>" "Перевод: Дублированный | MovieDalen" ``` @@ -13,7 +13,7 @@ ORT_DYLIB_PATH=<путь>/onnxruntime.dll ./target/debug/dub-ai-onnx-local.exe \ ```bash cd examples/rust/onnx_local ORT_DYLIB_PATH="$(find ../../.venv -name onnxruntime.dll | head -1)" \ - ./target/debug/dub-ai-onnx-local.exe ../../models/best-onnx \ + ./target/debug/grabber-ai-onnx-local.exe ../../models/best-onnx \ "Авторский (одноголосый закадровый) - Юрий Сербин" ``` diff --git a/examples/rust/onnx_local/src/main.rs b/examples/rust/onnx_local/src/main.rs index 89ca40d..e925046 100644 --- a/examples/rust/onnx_local/src/main.rs +++ b/examples/rust/onnx_local/src/main.rs @@ -1,4 +1,4 @@ -//! Локальный инференс DUB-AI без Python (путь Б: ONNX). +//! Локальный инференс grabber-ai без Python (путь Б: ONNX). //! //! cargo run --release -- <путь к models/best-onnx> "Перевод: Дублированный | MovieDalen" //! diff --git a/examples/typescript/onnx_local.mjs b/examples/typescript/onnx_local.mjs index c844a6f..bb64048 100644 --- a/examples/typescript/onnx_local.mjs +++ b/examples/typescript/onnx_local.mjs @@ -1,4 +1,4 @@ -// Локальный инференс DUB-AI без Python (путь Б: ONNX), Node.js. +// Локальный инференс grabber-ai без Python (путь Б: ONNX), Node.js. // // Подготовка (один раз): // cd examples/typescript && npm init -y && npm i onnxruntime-node @huggingface/transformers diff --git a/examples/typescript/predict.mjs b/examples/typescript/predict.mjs index 7ab0a51..a93c0ee 100644 --- a/examples/typescript/predict.mjs +++ b/examples/typescript/predict.mjs @@ -1,4 +1,4 @@ -// HTTP-клиент DUB-AI для Node.js (без зависимостей, node >= 18). +// HTTP-клиент grabber-ai для Node.js (без зависимостей, node >= 18). // // Запуск (требуется запущенный сервис `python -m src.service`): // node predict.mjs --text "Перевод: Дублированный | MovieDalen" diff --git a/examples/typescript/predict.ts b/examples/typescript/predict.ts index c6d59a3..40a4b07 100644 --- a/examples/typescript/predict.ts +++ b/examples/typescript/predict.ts @@ -1,4 +1,4 @@ -// Типизированный HTTP-клиент DUB-AI (TypeScript-проекты). +// Типизированный HTTP-клиент grabber-ai (TypeScript-проекты). // Запуск без компиляции: npx tsx predict.ts --text "Одноголосый закадровый — Дмитрий Есарев" // Зависимости: не требуются (используется глобальный fetch, Node >= 18). diff --git a/src/__init__.py b/src/__init__.py index 4318997..ea0c54f 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,4 +1,4 @@ -"""DUB-AI: нейро-модель «тип озвучки + автор озвучки». +"""grabber-ai: нейро-модель «тип озвучки + автор озвучки». Модули: studio_norm — словарь алиасов студий, чистка и канонизация имён, поиск спана diff --git a/src/service.py b/src/service.py index 0bce804..09cd2cc 100644 --- a/src/service.py +++ b/src/service.py @@ -64,7 +64,7 @@ def env_float(name: str, default: float) -> float: def create_app(predictor: DubPredictor, api_token: str | None = None, fetch_timeout: float = 60.0) -> FastAPI: - app = FastAPI(title="DUB-AI", version="0.1.0", + app = FastAPI(title="grabber-ai", version="0.1.0", description="Тип озвучки + автор озвучки", default_response_class=Utf8JSONResponse) @@ -114,7 +114,7 @@ def create_app(predictor: DubPredictor, api_token: str | None = None, def main() -> None: env = os.environ.get ap = argparse.ArgumentParser( - description="HTTP-сервис предсказаний DUB-AI (см. docstring модуля)", + description="HTTP-сервис предсказаний grabber-ai (см. docstring модуля)", formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument("--model", default=env("DUB_MODEL_DIR") or "models/best") ap.add_argument("--segmenter", default=env("DUB_SEGMENTER_DIR") or "models/segmenter") diff --git a/tests/run_tests.py b/tests/run_tests.py index b343ed9..5c1dc1d 100644 --- a/tests/run_tests.py +++ b/tests/run_tests.py @@ -1,4 +1,4 @@ -"""Юнит-тесты DUB-AI (запуск: .venv/Scripts/python tests/run_tests.py). +"""Юнит-тесты grabber-ai (запуск: .venv/Scripts/python tests/run_tests.py). Проверяют ключевые части без обращения к GPU: * поиск спана автора (точно / алиас / инициалы / кавычки / разделители); diff --git a/tools/collect.py b/tools/collect.py index 9ab9880..80153a2 100644 --- a/tools/collect.py +++ b/tools/collect.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -"""Интерактивный сборщик датасета DUB-AI (rutracker / nnm-club / kinozal). +"""Интерактивный сборщик датасета grabber-ai (rutracker / nnm-club / kinozal). Запуск без аргументов открывает меню: diff --git a/tools/retrain.py b/tools/retrain.py index dea36d7..ac22fd6 100644 --- a/tools/retrain.py +++ b/tools/retrain.py @@ -1,4 +1,4 @@ -"""Переобучение и проверка моделей DUB-AI (используется из tools/collect.py). +"""Переобучение и проверка моделей grabber-ai (используется из tools/collect.py). Шаги вынесены в функции, чтобы меню вызывало их по очереди: prepare() data/labels.csv -> data/processed diff --git a/tools/ui.py b/tools/ui.py index ee1f510..2f68b0f 100644 --- a/tools/ui.py +++ b/tools/ui.py @@ -214,7 +214,7 @@ def sections_table(site: str, entries: list[dict], rows: int = 14) -> Panel: def banner(lines: list[str]) -> Panel: body = Text("\n".join(lines)) - return Panel(body, title="[bold]DUB-AI · датасет и модель[/bold]", border_style="green", + return Panel(body, title="[bold]grabber-ai · датасет и модель[/bold]", border_style="green", padding=(0, 2))