Complete DDS training workflow and delivery package
This commit is contained in:
parent
68dd83c7c2
commit
4c4b91064f
229 changed files with 11969 additions and 1024 deletions
131
backend/scripts/download_local_models.py
Normal file
131
backend/scripts/download_local_models.py
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
"""Скачать ровно проверенные файлы трёх локальных моделей.
|
||||
|
||||
Файлы лежат в 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())
|
||||
Loading…
Reference in a new issue