lct-hack/backend/scripts/download_local_models.py
2026-09-24 01:10:49 +03:00

131 lines
5.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Скачать ровно проверенные файлы трёх локальных моделей.
Файлы лежат в backend/models/ (не в Git). Скрипт одинаково работает на
Windows, macOS и Linux; при повторном запуске продолжает неполный файл.
После переноса каталога models/ интернет для занятия не нужен.
"""
import hashlib
import argparse
import shutil
import ssl
import sys
import urllib.request
from dataclasses import dataclass
from pathlib import Path
import certifi
ROOT = Path(__file__).resolve().parents[1]
MODELS = ROOT / "models"
@dataclass(frozen=True)
class Artifact:
repo: str
name: str
target: str
size: int
sha256: str
ARTIFACTS = (
Artifact("Qwen/Qwen3-1.7B-GGUF", "Qwen3-1.7B-Q8_0.gguf",
"qwen3-1.7b/Qwen3-1.7B-Q8_0.gguf", 1834426016,
"061b54daade076b5d3362dac252678d17da8c68f07560be70818cace6590cb1a"),
Artifact("Vikhrmodels/Vikhr-Llama-3.2-1B-instruct-GGUF",
"Vikhr-Llama-3.2-1B-Q4_K_M.gguf",
"vikhr-1b/Vikhr-Llama-3.2-1B-Q4_K_M.gguf", 807694432,
"a85c0ef4b6476ff5fcf2c961226234ab7008d17087c2152416e61a9b7e74b32d"),
Artifact("ggerganov/whisper.cpp", "ggml-small-q5_1.bin",
"whisper-small/ggml-small-q5_1.bin", 190085487,
"ae85e4a935d7a567bd102fe55afc16bb595bdb618e11b2fc7591bc08120411bb"),
)
def digest(path: Path) -> str:
sha = hashlib.sha256()
with path.open("rb") as stream:
while chunk := stream.read(1 << 20):
sha.update(chunk)
return sha.hexdigest()
def verify(item: Artifact) -> None:
"""Проверить перенесённый вес без сети и без изменения файла."""
target = MODELS / item.target
if not target.is_file():
raise RuntimeError(f"нет локального файла: {target}")
if target.stat().st_size != item.size:
raise RuntimeError(
f"неверный размер {item.target}: {target.stat().st_size}, ожидался {item.size} байт"
)
actual = digest(target)
if actual != item.sha256:
raise RuntimeError(f"SHA-256 не совпал для {item.target}: {actual}")
print(f"проверено SHA-256: {item.target}", flush=True)
def download(item: Artifact) -> None:
target = MODELS / item.target
target.parent.mkdir(parents=True, exist_ok=True)
if target.exists():
try:
verify(item)
except RuntimeError:
pass
else:
print(f"готово: {item.target}", flush=True)
return
partial = target.with_name(target.name + ".part")
offset = partial.stat().st_size if partial.exists() else 0
if offset > item.size:
raise RuntimeError(f"некорректный частичный файл: {partial}")
free = shutil.disk_usage(MODELS if MODELS.exists() else ROOT).free
if free < item.size - offset + 256 * 1024 * 1024:
raise RuntimeError(f"мало свободного места для {item.target}: осталось {free // (1 << 20)} МиБ")
url = f"https://huggingface.co/{item.repo}/resolve/main/{item.name}"
request = urllib.request.Request(url, headers={"Range": f"bytes={offset}-"} if offset else {})
print(f"скачиваю {item.target} ({item.size // (1 << 20)} МиБ)", flush=True)
with urllib.request.urlopen(
request, timeout=120, context=ssl.create_default_context(cafile=certifi.where())
) as response:
# Сервер может проигнорировать Range: тогда начинаем заново, не
# приписываем полный файл к частичному.
resumed = offset > 0 and response.status == 206
with partial.open("ab" if resumed else "wb") as out:
count = offset if resumed else 0
while chunk := response.read(1 << 20):
out.write(chunk)
count += len(chunk)
if count % (64 << 20) < len(chunk) or count == item.size:
print(f" {count // (1 << 20)}/{item.size // (1 << 20)} МиБ", flush=True)
if partial.stat().st_size != item.size or digest(partial) != item.sha256:
raise RuntimeError(f"контроль размера/SHA-256 не пройден: {partial}")
partial.replace(target)
print(f"проверено SHA-256: {item.target}", flush=True)
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--verify-only", action="store_true",
help="проверить уже загруженные/перенесённые веса без сети и без изменения файлов",
)
args = parser.parse_args()
try:
for item in ARTIFACTS:
verify(item) if args.verify_only else download(item)
except (OSError, RuntimeError) as exc:
action = "проверка прервана" if args.verify_only else "загрузка прервана"
print(f"{action}: {exc}", file=sys.stderr)
return 1
if args.verify_only:
print("Все три локальные модели прошли офлайн-проверку; файлы не изменялись.")
else:
print("Все три локальные модели проверены. Для голоса нужны также whisper-server, VAD и TTS.")
return 0
if __name__ == "__main__":
raise SystemExit(main())