131 lines
5.5 KiB
Python
131 lines
5.5 KiB
Python
|
|
"""Скачать ровно проверенные файлы трёх локальных моделей.
|
|||
|
|
|
|||
|
|
Файлы лежат в 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())
|