Локальный FastAPI-сервис поверх GigaAM v3 и sherpa-onnx: приём аудио, очередь задач, разделение по говорящим, постобработка терминов. Доставка на Windows - ZIP со встроенным Python, без установки чего-либо. Обновление кода при запуске тянется из релизов Gitea: меняется только папка app, десятки килобайт вместо всего пакета. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
101 lines
3.6 KiB
Python
101 lines
3.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Публикует новую версию кода как релиз в Gitea.
|
|
|
|
В релиз кладётся только папка app - несколько десятков килобайт. Целевая машина
|
|
скачивает этот архив при следующем запуске и обновляется, не трогая Python,
|
|
библиотеки, ffmpeg и модели.
|
|
|
|
uv run --with requests python build/release.py 0.2.0 -m "что изменилось"
|
|
"""
|
|
import argparse
|
|
import hashlib
|
|
import io
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import zipfile
|
|
from pathlib import Path
|
|
|
|
import requests
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
SERVER = "https://git.netranking.ru"
|
|
REPO = "bryzgalov/talkscore-asr"
|
|
|
|
|
|
def get_token() -> str:
|
|
token = os.environ.get("GITEA_RELEASE_TOKEN")
|
|
if token:
|
|
return token
|
|
# Секреты лежат зашифрованными, достаём их штатной утилитой.
|
|
try:
|
|
out = subprocess.run(["secrets", "get", "GITEA_RELEASE_TOKEN"],
|
|
capture_output=True, text=True, check=True)
|
|
return out.stdout.strip()
|
|
except (subprocess.SubprocessError, FileNotFoundError):
|
|
sys.exit("не найден GITEA_RELEASE_TOKEN")
|
|
|
|
|
|
def bump_version(version: str) -> None:
|
|
"""Записывает версию в app/version.py, чтобы она уехала внутрь архива."""
|
|
path = ROOT / "app" / "version.py"
|
|
path.write_text(f'__version__ = "{version}"\n', encoding="utf-8")
|
|
|
|
|
|
def pack_app() -> bytes:
|
|
buf = io.BytesIO()
|
|
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as z:
|
|
for file in sorted((ROOT / "app").rglob("*.py")):
|
|
if "__pycache__" in file.parts:
|
|
continue
|
|
z.write(file, Path("app") / file.relative_to(ROOT / "app"))
|
|
return buf.getvalue()
|
|
|
|
|
|
def api(method: str, path: str, token: str, **kwargs):
|
|
response = requests.request(
|
|
method, f"{SERVER}/api/v1/{path}",
|
|
headers={"Authorization": f"token {token}"}, timeout=60, **kwargs)
|
|
response.raise_for_status()
|
|
return response
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("version", help="номер версии, например 0.2.0")
|
|
ap.add_argument("-m", "--message", default="", help="описание изменений")
|
|
args = ap.parse_args()
|
|
|
|
if not re.fullmatch(r"\d+\.\d+\.\d+", args.version):
|
|
sys.exit("версия должна быть вида 1.2.3")
|
|
|
|
token = get_token()
|
|
bump_version(args.version)
|
|
payload = pack_app()
|
|
digest = hashlib.sha256(payload).hexdigest()
|
|
name = f"app-{args.version}.zip"
|
|
print(f"Архив {name}: {len(payload) / 1024:.0f} КБ")
|
|
|
|
tag = f"v{args.version}"
|
|
release = api("POST", f"repos/{REPO}/releases", token, json={
|
|
"tag_name": tag,
|
|
"name": tag,
|
|
"body": args.message or f"Версия {args.version}",
|
|
"draft": False,
|
|
"prerelease": False,
|
|
}).json()
|
|
|
|
api("POST", f"repos/{REPO}/releases/{release['id']}/assets?name={name}", token,
|
|
files={"attachment": (name, payload, "application/zip")})
|
|
api("POST", f"repos/{REPO}/releases/{release['id']}/assets?name={name}.sha256", token,
|
|
files={"attachment": (f"{name}.sha256", f"{digest} {name}\n".encode(), "text/plain")})
|
|
|
|
print(f"Опубликовано: {SERVER}/{REPO}/releases/tag/{tag}")
|
|
print("Целевая машина подхватит обновление при следующем запуске start.bat")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|