Автообновление кода через релизы Gitea
Сервис при запуске проверяет последний релиз и обновляет только папку app - 19 КБ вместо 113 МБ всего пакета. Контрольная сумма, откат при неудачной проверке, пользовательские файлы не трогаются. Попутно: ffmpeg ищется лениво, а не в конструкторе Pipeline - его отсутствие роняло импорт вместо внятного сообщения. Схема API закрыта по умолчанию. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
9dc67bda5c
commit
44becfbe5e
@@ -153,6 +153,46 @@ for turn in r["turns"]:
|
||||
Заодно постобработка приводит типографику к принятому виду: все виды тире
|
||||
заменяются на дефис, кавычки отбиваются пробелом.
|
||||
|
||||
## Обновления
|
||||
|
||||
Сервис проверяет новую версию при каждом запуске и обновляет только папку `app` -
|
||||
это десятки килобайт. Python, библиотеки, ffmpeg и модели остаются на месте,
|
||||
перекидывать весь пакет заново не нужно.
|
||||
|
||||
Настройка в `config.toml`:
|
||||
|
||||
```toml
|
||||
[update]
|
||||
enabled = true
|
||||
server = "https://git.netranking.ru"
|
||||
repo = "bryzgalov/talkscore-asr"
|
||||
token = "" # токен Gitea с правом чтения; для публичного репозитория не нужен
|
||||
```
|
||||
|
||||
Репозиторий приватный, поэтому токен обязателен. Создать его: Gitea → Settings →
|
||||
Applications → Generate Token, достаточно права `read:repository`.
|
||||
|
||||
Как это работает:
|
||||
|
||||
1. `start.bat` перед запуском сервиса спрашивает у Gitea последний релиз
|
||||
2. если версия там новее, скачивает `app-<версия>.zip` и сверяет контрольную сумму
|
||||
3. откладывает текущий код, ставит новый и проверяет, что он импортируется
|
||||
4. если проверка не прошла, возвращает предыдущую версию
|
||||
|
||||
Нет сети или Gitea недоступен - сервис просто запускается на текущей версии.
|
||||
`config.toml` и `replacements.txt` обновление не трогает: они ваши.
|
||||
Чтобы выключить проверку совсем, поставьте `enabled = false`.
|
||||
|
||||
### Выпуск новой версии
|
||||
|
||||
```bash
|
||||
uv run --with requests python build/release.py 0.2.0 -m "что изменилось"
|
||||
```
|
||||
|
||||
Скрипт проставит версию в `app/version.py`, соберёт архив только из кода
|
||||
и опубликует релиз с контрольной суммой. Целевая машина подхватит его
|
||||
при следующем запуске.
|
||||
|
||||
## Автозапуск
|
||||
|
||||
Чтобы сервис поднимался при старте Windows, создайте задачу в планировщике:
|
||||
|
||||
+14
-1
@@ -70,11 +70,24 @@ class Pipeline:
|
||||
self.models_dir = Path(models_dir)
|
||||
self.threads = threads
|
||||
self.replacements_path = Path(replacements_path)
|
||||
self.ffmpeg = find_ffmpeg(Path(base_dir))
|
||||
self.base_dir = Path(base_dir)
|
||||
self._ffmpeg: str | None = None
|
||||
self._asr = None
|
||||
self._diar_cache: dict[int, object] = {}
|
||||
self._replacements: dict[str, str] = {}
|
||||
|
||||
@property
|
||||
def ffmpeg(self) -> str:
|
||||
"""Ищется при первом обращении, а не в конструкторе.
|
||||
|
||||
Иначе отсутствие ffmpeg роняло бы сам импорт модуля - и вместо внятного
|
||||
сообщения пользователь получал бы стек при старте, а обновление кода
|
||||
не могло бы проверить работоспособность новой версии.
|
||||
"""
|
||||
if self._ffmpeg is None:
|
||||
self._ffmpeg = find_ffmpeg(self.base_dir)
|
||||
return self._ffmpeg
|
||||
|
||||
def warmup(self) -> None:
|
||||
"""Грузит модели заранее, чтобы первый запрос не ждал."""
|
||||
import onnx_asr
|
||||
|
||||
+8
-3
@@ -9,6 +9,7 @@ Python, библиотеки, ffmpeg и модели остаются на ме
|
||||
"""
|
||||
import hashlib
|
||||
import io
|
||||
import os
|
||||
import logging
|
||||
import shutil
|
||||
import subprocess
|
||||
@@ -72,11 +73,12 @@ def _safe_members(archive: zipfile.ZipFile) -> list[str]:
|
||||
|
||||
def _default_verify(base: Path) -> bool:
|
||||
"""Проверяет, что обновлённый код хотя бы импортируется."""
|
||||
env = dict(os.environ)
|
||||
env.update({"PYTHONPATH": str(base), "TALKSCORE_ASR_HOME": str(base)})
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-c", "import app.main"],
|
||||
cwd=str(base), capture_output=True, timeout=UPDATE_TIMEOUT_SEC,
|
||||
env={"PYTHONPATH": str(base), "PATH": "", "TALKSCORE_ASR_HOME": str(base)},
|
||||
cwd=str(base), capture_output=True, timeout=UPDATE_TIMEOUT_SEC, env=env,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
log.error("новая версия не импортируется: %s",
|
||||
@@ -194,7 +196,10 @@ def main() -> int:
|
||||
else:
|
||||
print(" Установлена последняя версия")
|
||||
except Exception as exc: # noqa: BLE001 - падение обновления не должно мешать работе
|
||||
print(f" Обновиться не удалось ({type(exc).__name__}: {exc}), запускаюсь на текущей версии")
|
||||
# Полный стек requests занимает пол-экрана и пугает, оставляем суть.
|
||||
reason = str(exc).split("(Caused by")[0].strip()[:160] or type(exc).__name__
|
||||
print(f" Обновиться не удалось: {reason}")
|
||||
print(" Запускаюсь на текущей версии")
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
__version__ = "0.1.0"
|
||||
__version__ = "0.1.2"
|
||||
|
||||
+2
-1
@@ -30,7 +30,8 @@ def client(tmp_path, monkeypatch):
|
||||
import app.config as cfg
|
||||
monkeypatch.setattr(cfg, "BASE_DIR", tmp_path)
|
||||
import app.main as main
|
||||
monkeypatch.setattr(main.pipeline, "ffmpeg", "/bin/true", raising=False)
|
||||
# ffmpeg ищется лениво, поэтому подменяем внутреннее поле, а не свойство
|
||||
monkeypatch.setattr(main.pipeline, "_ffmpeg", "/bin/true", raising=False)
|
||||
main._state["ready"] = True
|
||||
main._worker_stop.set() # воркер не должен разбирать очередь в тестах
|
||||
|
||||
|
||||
@@ -126,3 +126,35 @@ class TestInstall:
|
||||
z.writestr("app/main.py", "код")
|
||||
with pytest.raises(UpdateError, match="путь"):
|
||||
install_update(buf.getvalue(), base, verify=lambda _: True)
|
||||
|
||||
|
||||
class TestPipelineImportSafety:
|
||||
def test_pipeline_constructs_without_ffmpeg(self, tmp_path, monkeypatch):
|
||||
"""Отсутствие ffmpeg не должно ронять импорт: иначе непонятно, что чинить."""
|
||||
import sys
|
||||
import types
|
||||
|
||||
for name in ("sherpa_onnx", "onnx_asr", "onnxruntime"):
|
||||
monkeypatch.setitem(sys.modules, name, types.ModuleType(name))
|
||||
monkeypatch.setattr("shutil.which", lambda _: None)
|
||||
|
||||
from app.pipeline import Pipeline
|
||||
|
||||
p = Pipeline(models_dir=tmp_path, threads=1,
|
||||
replacements_path=tmp_path / "r.txt", base_dir=tmp_path)
|
||||
assert p is not None # конструктор не упал
|
||||
|
||||
def test_ffmpeg_lookup_fails_only_when_used(self, tmp_path, monkeypatch):
|
||||
import sys
|
||||
import types
|
||||
|
||||
for name in ("sherpa_onnx", "onnx_asr", "onnxruntime"):
|
||||
monkeypatch.setitem(sys.modules, name, types.ModuleType(name))
|
||||
monkeypatch.setattr("shutil.which", lambda _: None)
|
||||
|
||||
from app.pipeline import ModelsMissing, Pipeline
|
||||
|
||||
p = Pipeline(models_dir=tmp_path, threads=1,
|
||||
replacements_path=tmp_path / "r.txt", base_dir=tmp_path)
|
||||
with pytest.raises(ModelsMissing, match="ffmpeg"):
|
||||
_ = p.ffmpeg
|
||||
|
||||
Reference in New Issue
Block a user