Spaces:
Running on Zero
Running on Zero
Upload 4 files
Browse files- README.md +114 -25
- build_prebuilt_assets.py +324 -0
- hudanet_prebuilt_guard.py +325 -0
- patch_app_v41_0_2.py +106 -0
README.md
CHANGED
|
@@ -1,25 +1,114 @@
|
|
| 1 |
-
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
-
|
| 17 |
-
-
|
| 18 |
-
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# HUDA-Net v41.0.2 Prebuilt Retrieval Fix
|
| 2 |
+
|
| 3 |
+
This package fixes the Hugging Face Space startup timeout caused by rebuilding
|
| 4 |
+
the full bilingual hybrid retrieval stack on every restart.
|
| 5 |
+
|
| 6 |
+
## Files
|
| 7 |
+
|
| 8 |
+
- `hudanet_prebuilt_guard.py`
|
| 9 |
+
- Production runtime guard.
|
| 10 |
+
- Downloads a prebuilt retrieval bundle.
|
| 11 |
+
- Verifies HUDA-Net's own KB fingerprint and every SHA-256.
|
| 12 |
+
- Restores the final runtime files.
|
| 13 |
+
- Refuses to rebuild in production if assets are missing or stale.
|
| 14 |
+
|
| 15 |
+
- `build_prebuilt_assets.py`
|
| 16 |
+
- One-time builder/publisher.
|
| 17 |
+
- Imports the CURRENT `app.py` with `HUDANET_SKIP_APP_INIT=1`.
|
| 18 |
+
- Runs `_prepare_kb_native_runtime_ui()` once.
|
| 19 |
+
- Packages `HYBRID_FILES` and Arabic/English embedding indexes.
|
| 20 |
+
- Creates/updates a private Hugging Face Dataset.
|
| 21 |
+
|
| 22 |
+
- `patch_app_v41_0_2.py`
|
| 23 |
+
- Safely patches the CURRENT `app.py`.
|
| 24 |
+
- Makes a backup.
|
| 25 |
+
- Inserts the guard immediately before `HUDA_DEMO`.
|
| 26 |
+
- Changes 41.0.1 strings to 41.0.2.
|
| 27 |
+
- Compile-checks and restores backup if patching fails.
|
| 28 |
+
|
| 29 |
+
## Recommended repository
|
| 30 |
+
|
| 31 |
+
Default:
|
| 32 |
+
`dakheel/hudanet-v41-prebuilt-retrieval`
|
| 33 |
+
|
| 34 |
+
Override everywhere with:
|
| 35 |
+
`HUDANET_PREBUILT_REPO_ID`
|
| 36 |
+
|
| 37 |
+
## One-time setup
|
| 38 |
+
|
| 39 |
+
Place these three `.py` files beside the CURRENT `app.py`.
|
| 40 |
+
|
| 41 |
+
Patch the current app:
|
| 42 |
+
|
| 43 |
+
```bash
|
| 44 |
+
python patch_app_v41_0_2.py
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
Build and publish the retrieval bundle in Kaggle/local/build environment:
|
| 48 |
+
|
| 49 |
+
Linux/macOS:
|
| 50 |
+
|
| 51 |
+
```bash
|
| 52 |
+
export HF_TOKEN="READ_TOKEN"
|
| 53 |
+
export HF_WRITE_TOKEN="WRITE_TOKEN"
|
| 54 |
+
export HUDANET_PREBUILT_REPO_ID="dakheel/hudanet-v41-prebuilt-retrieval"
|
| 55 |
+
python build_prebuilt_assets.py
|
| 56 |
+
```
|
| 57 |
+
|
| 58 |
+
PowerShell:
|
| 59 |
+
|
| 60 |
+
```powershell
|
| 61 |
+
$env:HF_TOKEN="READ_TOKEN"
|
| 62 |
+
$env:HF_WRITE_TOKEN="WRITE_TOKEN"
|
| 63 |
+
$env:HUDANET_PREBUILT_REPO_ID="dakheel/hudanet-v41-prebuilt-retrieval"
|
| 64 |
+
python build_prebuilt_assets.py
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
If the same token has both read and write permission, `HF_WRITE_TOKEN` can be
|
| 68 |
+
omitted and the builder will use `HF_TOKEN`.
|
| 69 |
+
|
| 70 |
+
## Production Space
|
| 71 |
+
|
| 72 |
+
Commit these files to the Space:
|
| 73 |
+
|
| 74 |
+
- patched `app.py`
|
| 75 |
+
- `hudanet_prebuilt_guard.py`
|
| 76 |
+
|
| 77 |
+
`build_prebuilt_assets.py` and `patch_app_v41_0_2.py` may remain in the repo,
|
| 78 |
+
but they are not run automatically.
|
| 79 |
+
|
| 80 |
+
Space secrets/variables:
|
| 81 |
+
|
| 82 |
+
- `HF_TOKEN`: read access to all private HUDA-Net datasets, including the new
|
| 83 |
+
prebuilt retrieval dataset.
|
| 84 |
+
- optional `HUDANET_PREBUILT_REPO_ID` if you used a different repo id.
|
| 85 |
+
|
| 86 |
+
Do NOT put a write token in the production Space unless you genuinely need it.
|
| 87 |
+
|
| 88 |
+
## Expected startup
|
| 89 |
+
|
| 90 |
+
You should see something similar to:
|
| 91 |
+
|
| 92 |
+
```text
|
| 93 |
+
🔒 HUDA-Net v41.0.2 prebuilt retrieval guard active | production rebuild fallback disabled
|
| 94 |
+
⬇️ Loading HUDA-Net prebuilt retrieval assets from dakheel/hudanet-v41-prebuilt-retrieval...
|
| 95 |
+
⚡ HUDA-Net prebuilt retrieval restored | ... files | fingerprint=...
|
| 96 |
+
✅ HUDA-Net v41 KB-native Runtime ready
|
| 97 |
+
```
|
| 98 |
+
|
| 99 |
+
You should NOT see:
|
| 100 |
+
|
| 101 |
+
```text
|
| 102 |
+
💾 ar: completed batch 1/384
|
| 103 |
+
...
|
| 104 |
+
💾 en: completed batch 1/384
|
| 105 |
+
...
|
| 106 |
+
```
|
| 107 |
+
|
| 108 |
+
## When the Knowledge Base changes
|
| 109 |
+
|
| 110 |
+
Run `build_prebuilt_assets.py` again.
|
| 111 |
+
|
| 112 |
+
The production guard compares the bundle fingerprint with the current KB. A
|
| 113 |
+
mismatch fails fast with a clear "STALE" message instead of silently using old
|
| 114 |
+
embeddings or spending ~30 minutes rebuilding during Space startup.
|
build_prebuilt_assets.py
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
HUDA-Net v41.0.2 One-Time Prebuilt Retrieval Builder
|
| 4 |
+
====================================================
|
| 5 |
+
|
| 6 |
+
Run this file OUTSIDE the production Space, for example in Kaggle, a local
|
| 7 |
+
machine, or another build environment.
|
| 8 |
+
|
| 9 |
+
Requirements
|
| 10 |
+
------------
|
| 11 |
+
1. Put this file beside the CURRENT v41 app.py.
|
| 12 |
+
2. app.py must support HUDANET_SKIP_APP_INIT=1 (v41.0.1 already does).
|
| 13 |
+
3. Set:
|
| 14 |
+
HF_TOKEN=<token that can read the private HUDA-Net source datasets>
|
| 15 |
+
HF_WRITE_TOKEN=<token that can create/write the prebuilt dataset>
|
| 16 |
+
If one token has both permissions, HF_WRITE_TOKEN may be omitted.
|
| 17 |
+
4. Optional:
|
| 18 |
+
HUDANET_PREBUILT_REPO_ID=dakheel/hudanet-v41-prebuilt-retrieval
|
| 19 |
+
|
| 20 |
+
What it does
|
| 21 |
+
------------
|
| 22 |
+
- Imports the CURRENT app.py without starting Gradio.
|
| 23 |
+
- Runs HUDA-Net's own KB-native runtime preparation ONCE.
|
| 24 |
+
- Lets the original hybrid builder calculate BM25, E5 dense vectors and
|
| 25 |
+
calibration exactly as HUDA-Net already does.
|
| 26 |
+
- Packages the final runtime retrieval files plus Arabic/English embedding
|
| 27 |
+
indexes.
|
| 28 |
+
- Writes a fingerprinted SHA-256 manifest.
|
| 29 |
+
- Creates/updates a private Hugging Face Dataset and uploads the bundle.
|
| 30 |
+
"""
|
| 31 |
+
|
| 32 |
+
from __future__ import annotations
|
| 33 |
+
|
| 34 |
+
import hashlib
|
| 35 |
+
import importlib
|
| 36 |
+
import json
|
| 37 |
+
import os
|
| 38 |
+
import shutil
|
| 39 |
+
import sys
|
| 40 |
+
import tempfile
|
| 41 |
+
from datetime import datetime, timezone
|
| 42 |
+
from pathlib import Path
|
| 43 |
+
from typing import Any, Dict, Iterable, List, Mapping, Set
|
| 44 |
+
|
| 45 |
+
from huggingface_hub import HfApi
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
BUNDLE_MANIFEST = "hudanet_prebuilt_bundle_v41.json"
|
| 49 |
+
DEFAULT_REPO_ID = "dakheel/hudanet-v41-prebuilt-retrieval"
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def _sha256_file(path: Path, chunk_size: int = 8 * 1024 * 1024) -> str:
|
| 53 |
+
digest = hashlib.sha256()
|
| 54 |
+
with Path(path).open("rb") as handle:
|
| 55 |
+
while True:
|
| 56 |
+
chunk = handle.read(chunk_size)
|
| 57 |
+
if not chunk:
|
| 58 |
+
break
|
| 59 |
+
digest.update(chunk)
|
| 60 |
+
return digest.hexdigest()
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def _utc_now() -> str:
|
| 64 |
+
return datetime.now(timezone.utc).isoformat()
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def _copy_file_preserve_relative(root: Path, source: Path, stage: Path) -> str:
|
| 68 |
+
root = Path(root).resolve()
|
| 69 |
+
source = Path(source).resolve()
|
| 70 |
+
stage = Path(stage)
|
| 71 |
+
|
| 72 |
+
try:
|
| 73 |
+
rel = source.relative_to(root)
|
| 74 |
+
except Exception as exc:
|
| 75 |
+
raise RuntimeError(
|
| 76 |
+
f"Refusing to package file outside runtime root: {source}"
|
| 77 |
+
) from exc
|
| 78 |
+
|
| 79 |
+
dst = stage / rel
|
| 80 |
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
| 81 |
+
shutil.copy2(source, dst)
|
| 82 |
+
return rel.as_posix()
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def _collect_runtime_files(hudanet: Any, runtime_root: Path) -> List[Path]:
|
| 86 |
+
runtime_root = Path(runtime_root)
|
| 87 |
+
files: Set[Path] = set()
|
| 88 |
+
|
| 89 |
+
hybrid_files = getattr(hudanet, "HYBRID_FILES", None)
|
| 90 |
+
if not isinstance(hybrid_files, Mapping):
|
| 91 |
+
raise RuntimeError("Current app.py has no HYBRID_FILES mapping.")
|
| 92 |
+
|
| 93 |
+
for filename in hybrid_files.values():
|
| 94 |
+
path = runtime_root / str(filename)
|
| 95 |
+
if not path.is_file():
|
| 96 |
+
raise RuntimeError(f"Required hybrid runtime file is missing: {path}")
|
| 97 |
+
files.add(path)
|
| 98 |
+
|
| 99 |
+
embedding_index_fn = getattr(hudanet, "_embedding_index_path", None)
|
| 100 |
+
if callable(embedding_index_fn):
|
| 101 |
+
for lang in ("ar", "en"):
|
| 102 |
+
path = Path(embedding_index_fn(runtime_root, lang))
|
| 103 |
+
if path.is_file():
|
| 104 |
+
files.add(path)
|
| 105 |
+
else:
|
| 106 |
+
raise RuntimeError(
|
| 107 |
+
f"Final {lang} embedding index is missing: {path}"
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
return sorted(files, key=lambda p: str(p))
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def main() -> None:
|
| 114 |
+
# Critical: allow the original expensive builder only in this one-time build.
|
| 115 |
+
os.environ["HUDANET_SKIP_APP_INIT"] = "1"
|
| 116 |
+
os.environ["HUDANET_PREBUILT_BUILD_MODE"] = "1"
|
| 117 |
+
|
| 118 |
+
repo_id = os.getenv(
|
| 119 |
+
"HUDANET_PREBUILT_REPO_ID",
|
| 120 |
+
DEFAULT_REPO_ID,
|
| 121 |
+
).strip()
|
| 122 |
+
|
| 123 |
+
read_token = os.getenv("HF_TOKEN", "").strip()
|
| 124 |
+
write_token = os.getenv("HF_WRITE_TOKEN", "").strip() or read_token
|
| 125 |
+
|
| 126 |
+
if not read_token:
|
| 127 |
+
raise RuntimeError(
|
| 128 |
+
"HF_TOKEN is missing. It must be able to read HUDA-Net private datasets."
|
| 129 |
+
)
|
| 130 |
+
if not write_token:
|
| 131 |
+
raise RuntimeError(
|
| 132 |
+
"HF_WRITE_TOKEN is missing. It must be able to create/write the "
|
| 133 |
+
"prebuilt retrieval dataset."
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
print("=" * 88)
|
| 137 |
+
print("🕋 HUDA-Net v41.0.2 ONE-TIME PREBUILT RETRIEVAL BUILD")
|
| 138 |
+
print("=" * 88)
|
| 139 |
+
print(f"Target dataset: {repo_id}")
|
| 140 |
+
|
| 141 |
+
# Import CURRENT app.py, but do not create the Gradio app.
|
| 142 |
+
hudanet = importlib.import_module("app")
|
| 143 |
+
|
| 144 |
+
prepare_fn = getattr(hudanet, "_prepare_kb_native_runtime_ui", None)
|
| 145 |
+
if not callable(prepare_fn):
|
| 146 |
+
raise RuntimeError(
|
| 147 |
+
"Current app.py does not expose _prepare_kb_native_runtime_ui()."
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
+
print("🧠 Running the original KB-native builder once...")
|
| 151 |
+
runtime_root, runtime_manifest = prepare_fn()
|
| 152 |
+
runtime_root = Path(runtime_root)
|
| 153 |
+
|
| 154 |
+
if not runtime_root.exists():
|
| 155 |
+
raise RuntimeError(f"Builder returned missing runtime root: {runtime_root}")
|
| 156 |
+
|
| 157 |
+
fingerprint_fn = getattr(hudanet, "_hybrid_fingerprint", None)
|
| 158 |
+
if not callable(fingerprint_fn):
|
| 159 |
+
raise RuntimeError("Current app.py has no _hybrid_fingerprint(df).")
|
| 160 |
+
|
| 161 |
+
# Resolve the dataframe from the current runtime without guessing schema.
|
| 162 |
+
# First try well-known module-level dataframe holders, then load the runtime records.
|
| 163 |
+
df = None
|
| 164 |
+
for candidate_name in (
|
| 165 |
+
"RUNTIME_DF",
|
| 166 |
+
"KB_DF",
|
| 167 |
+
"KNOWLEDGE_BASE_DF",
|
| 168 |
+
"DF",
|
| 169 |
+
"df",
|
| 170 |
+
):
|
| 171 |
+
candidate = getattr(hudanet, candidate_name, None)
|
| 172 |
+
if candidate is not None and hasattr(candidate, "__len__") and hasattr(candidate, "columns"):
|
| 173 |
+
try:
|
| 174 |
+
if len(candidate) > 0:
|
| 175 |
+
df = candidate
|
| 176 |
+
break
|
| 177 |
+
except Exception:
|
| 178 |
+
pass
|
| 179 |
+
|
| 180 |
+
if df is None:
|
| 181 |
+
import pandas as pd
|
| 182 |
+
|
| 183 |
+
candidates = [
|
| 184 |
+
runtime_root / "records.parquet",
|
| 185 |
+
runtime_root / "records.csv.gz",
|
| 186 |
+
runtime_root / "records.csv",
|
| 187 |
+
]
|
| 188 |
+
for path in candidates:
|
| 189 |
+
if not path.exists():
|
| 190 |
+
continue
|
| 191 |
+
if path.suffix == ".parquet":
|
| 192 |
+
df = pd.read_parquet(path)
|
| 193 |
+
else:
|
| 194 |
+
df = pd.read_csv(path, dtype=str, keep_default_na=False)
|
| 195 |
+
break
|
| 196 |
+
|
| 197 |
+
if df is None:
|
| 198 |
+
# v41 KB-native runtime may expose a dedicated loader.
|
| 199 |
+
loader_candidates = [
|
| 200 |
+
"_load_kb_native_dataframe",
|
| 201 |
+
"_load_knowledge_base_dataframe",
|
| 202 |
+
"load_knowledge_base",
|
| 203 |
+
]
|
| 204 |
+
for name in loader_candidates:
|
| 205 |
+
fn = getattr(hudanet, name, None)
|
| 206 |
+
if callable(fn):
|
| 207 |
+
try:
|
| 208 |
+
loaded = fn()
|
| 209 |
+
if hasattr(loaded, "columns"):
|
| 210 |
+
df = loaded
|
| 211 |
+
break
|
| 212 |
+
if isinstance(loaded, tuple):
|
| 213 |
+
for item in loaded:
|
| 214 |
+
if hasattr(item, "columns"):
|
| 215 |
+
df = item
|
| 216 |
+
break
|
| 217 |
+
except TypeError:
|
| 218 |
+
continue
|
| 219 |
+
|
| 220 |
+
if df is None:
|
| 221 |
+
# Final reliable source: hybrid_manifest.json already contains the exact
|
| 222 |
+
# fingerprint produced by HUDA-Net's own build. We don't need to invent
|
| 223 |
+
# a second fingerprint implementation.
|
| 224 |
+
hybrid_manifest_name = str(
|
| 225 |
+
hudanet.HYBRID_FILES.get("hybrid_manifest", "hybrid_manifest.json")
|
| 226 |
+
)
|
| 227 |
+
hm_path = runtime_root / hybrid_manifest_name
|
| 228 |
+
if not hm_path.exists():
|
| 229 |
+
raise RuntimeError(
|
| 230 |
+
"Could not resolve the KB dataframe and hybrid_manifest.json is missing."
|
| 231 |
+
)
|
| 232 |
+
hybrid_meta = json.loads(hm_path.read_text(encoding="utf-8"))
|
| 233 |
+
fingerprint = str(hybrid_meta.get("fingerprint", ""))
|
| 234 |
+
records = int(hybrid_meta.get("records", 0))
|
| 235 |
+
else:
|
| 236 |
+
fingerprint = str(fingerprint_fn(df))
|
| 237 |
+
records = int(len(df))
|
| 238 |
+
|
| 239 |
+
if not fingerprint:
|
| 240 |
+
raise RuntimeError("Could not resolve HUDA-Net hybrid fingerprint.")
|
| 241 |
+
|
| 242 |
+
files = _collect_runtime_files(hudanet, runtime_root)
|
| 243 |
+
|
| 244 |
+
stage = Path(tempfile.mkdtemp(prefix="hudanet_v41_prebuilt_stage_"))
|
| 245 |
+
print(f"📦 Staging bundle at: {stage}")
|
| 246 |
+
|
| 247 |
+
try:
|
| 248 |
+
packaged_files: Dict[str, Dict[str, Any]] = {}
|
| 249 |
+
|
| 250 |
+
for source in files:
|
| 251 |
+
rel = _copy_file_preserve_relative(runtime_root, source, stage)
|
| 252 |
+
staged = stage / rel
|
| 253 |
+
packaged_files[rel] = {
|
| 254 |
+
"size": int(staged.stat().st_size),
|
| 255 |
+
"sha256": _sha256_file(staged),
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
hybrid_manifest_name = str(
|
| 259 |
+
hudanet.HYBRID_FILES.get("hybrid_manifest", "hybrid_manifest.json")
|
| 260 |
+
)
|
| 261 |
+
|
| 262 |
+
bundle_manifest = {
|
| 263 |
+
"schema_version": 1,
|
| 264 |
+
"created_at": _utc_now(),
|
| 265 |
+
"hudanet_version": str(getattr(hudanet, "VERSION", "unknown")),
|
| 266 |
+
"hybrid_asset_version": str(
|
| 267 |
+
getattr(hudanet, "HYBRID_ASSET_VERSION", "unknown")
|
| 268 |
+
),
|
| 269 |
+
"fingerprint": fingerprint,
|
| 270 |
+
"records": records,
|
| 271 |
+
"hybrid_manifest_file": hybrid_manifest_name,
|
| 272 |
+
"source_runtime_root_name": runtime_root.name,
|
| 273 |
+
"files": packaged_files,
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
(stage / BUNDLE_MANIFEST).write_text(
|
| 277 |
+
json.dumps(bundle_manifest, ensure_ascii=False, indent=2),
|
| 278 |
+
encoding="utf-8",
|
| 279 |
+
)
|
| 280 |
+
|
| 281 |
+
print(
|
| 282 |
+
f"✅ Bundle ready | {len(packaged_files)} runtime files | "
|
| 283 |
+
f"records={records} | fingerprint={fingerprint[:16]}..."
|
| 284 |
+
)
|
| 285 |
+
|
| 286 |
+
api = HfApi(token=write_token)
|
| 287 |
+
api.create_repo(
|
| 288 |
+
repo_id=repo_id,
|
| 289 |
+
repo_type="dataset",
|
| 290 |
+
private=True,
|
| 291 |
+
exist_ok=True,
|
| 292 |
+
token=write_token,
|
| 293 |
+
)
|
| 294 |
+
|
| 295 |
+
print("⬆️ Uploading prebuilt retrieval bundle to Hugging Face...")
|
| 296 |
+
api.upload_folder(
|
| 297 |
+
repo_id=repo_id,
|
| 298 |
+
repo_type="dataset",
|
| 299 |
+
folder_path=str(stage),
|
| 300 |
+
path_in_repo=".",
|
| 301 |
+
token=write_token,
|
| 302 |
+
commit_message=(
|
| 303 |
+
f"HUDA-Net prebuilt retrieval "
|
| 304 |
+
f"{getattr(hudanet, 'VERSION', 'v41')} "
|
| 305 |
+
f"{fingerprint[:12]}"
|
| 306 |
+
),
|
| 307 |
+
)
|
| 308 |
+
|
| 309 |
+
print("=" * 88)
|
| 310 |
+
print("✅ PREBUILT RETRIEVAL DATASET PUBLISHED SUCCESSFULLY")
|
| 311 |
+
print(f"Repo : {repo_id}")
|
| 312 |
+
print(f"Fingerprint: {fingerprint}")
|
| 313 |
+
print(f"Files : {len(packaged_files)}")
|
| 314 |
+
print("=" * 88)
|
| 315 |
+
|
| 316 |
+
finally:
|
| 317 |
+
if os.getenv("HUDANET_KEEP_PREBUILT_STAGE", "0").strip().casefold() not in {
|
| 318 |
+
"1", "true", "yes", "on"
|
| 319 |
+
}:
|
| 320 |
+
shutil.rmtree(stage, ignore_errors=True)
|
| 321 |
+
|
| 322 |
+
|
| 323 |
+
if __name__ == "__main__":
|
| 324 |
+
main()
|
hudanet_prebuilt_guard.py
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
HUDA-Net v41.0.2 Prebuilt Retrieval Guard
|
| 4 |
+
=========================================
|
| 5 |
+
|
| 6 |
+
Purpose
|
| 7 |
+
-------
|
| 8 |
+
Prevent Hugging Face Space startup from rebuilding BM25, dense E5 embeddings,
|
| 9 |
+
and calibration assets for the full KB on every restart.
|
| 10 |
+
|
| 11 |
+
Production behavior
|
| 12 |
+
-------------------
|
| 13 |
+
- Download one prebuilt retrieval bundle from a private Hugging Face Dataset.
|
| 14 |
+
- Verify the bundle fingerprint against the CURRENT KB using HUDA-Net's own
|
| 15 |
+
_hybrid_fingerprint(df) function.
|
| 16 |
+
- Verify every file SHA-256.
|
| 17 |
+
- Restore the packaged hybrid files into the runtime target directory.
|
| 18 |
+
- NEVER fall back to expensive production-time rebuilding.
|
| 19 |
+
|
| 20 |
+
Build behavior
|
| 21 |
+
--------------
|
| 22 |
+
When HUDANET_PREBUILT_BUILD_MODE=1, this module does nothing so the original
|
| 23 |
+
HUDA-Net builder can run once in an offline/build environment.
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
from __future__ import annotations
|
| 27 |
+
|
| 28 |
+
import hashlib
|
| 29 |
+
import json
|
| 30 |
+
import os
|
| 31 |
+
import shutil
|
| 32 |
+
from pathlib import Path
|
| 33 |
+
from typing import Any, Dict, Mapping, MutableMapping, Optional, Tuple
|
| 34 |
+
|
| 35 |
+
from huggingface_hub import snapshot_download
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
BUNDLE_MANIFEST = "hudanet_prebuilt_bundle_v41.json"
|
| 39 |
+
DEFAULT_REPO_ID = "dakheel/hudanet-v41-prebuilt-retrieval"
|
| 40 |
+
DEFAULT_LOCAL_ROOT = Path("/tmp/hudanet_v41_prebuilt_retrieval")
|
| 41 |
+
|
| 42 |
+
_BUNDLE_ROOT: Optional[Path] = None
|
| 43 |
+
_INSTALLED = False
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def _truthy(value: Any) -> bool:
|
| 47 |
+
return str(value or "").strip().casefold() in {"1", "true", "yes", "on"}
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def _sha256_file(path: Path, chunk_size: int = 8 * 1024 * 1024) -> str:
|
| 51 |
+
digest = hashlib.sha256()
|
| 52 |
+
with Path(path).open("rb") as handle:
|
| 53 |
+
while True:
|
| 54 |
+
chunk = handle.read(chunk_size)
|
| 55 |
+
if not chunk:
|
| 56 |
+
break
|
| 57 |
+
digest.update(chunk)
|
| 58 |
+
return digest.hexdigest()
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def _safe_relative_path(value: str) -> Path:
|
| 62 |
+
rel = Path(str(value).replace("\\", "/"))
|
| 63 |
+
if rel.is_absolute() or ".." in rel.parts:
|
| 64 |
+
raise RuntimeError(f"Unsafe path in prebuilt bundle: {value!r}")
|
| 65 |
+
return rel
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def _link_or_copy(src: Path, dst: Path) -> None:
|
| 69 |
+
src = Path(src)
|
| 70 |
+
dst = Path(dst)
|
| 71 |
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
| 72 |
+
|
| 73 |
+
if dst.exists() or dst.is_symlink():
|
| 74 |
+
if dst.is_dir() and not dst.is_symlink():
|
| 75 |
+
shutil.rmtree(dst)
|
| 76 |
+
else:
|
| 77 |
+
dst.unlink()
|
| 78 |
+
|
| 79 |
+
try:
|
| 80 |
+
os.link(src, dst)
|
| 81 |
+
except Exception:
|
| 82 |
+
shutil.copy2(src, dst)
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def _download_bundle() -> Path:
|
| 86 |
+
global _BUNDLE_ROOT
|
| 87 |
+
|
| 88 |
+
if _BUNDLE_ROOT is not None:
|
| 89 |
+
manifest = _BUNDLE_ROOT / BUNDLE_MANIFEST
|
| 90 |
+
if manifest.exists():
|
| 91 |
+
return _BUNDLE_ROOT
|
| 92 |
+
|
| 93 |
+
repo_id = os.getenv("HUDANET_PREBUILT_REPO_ID", DEFAULT_REPO_ID).strip()
|
| 94 |
+
token = os.getenv("HF_TOKEN", "").strip() or None
|
| 95 |
+
local_root = Path(
|
| 96 |
+
os.getenv("HUDANET_PREBUILT_LOCAL_ROOT", str(DEFAULT_LOCAL_ROOT))
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
local_root.mkdir(parents=True, exist_ok=True)
|
| 100 |
+
|
| 101 |
+
print(f"⬇️ Loading HUDA-Net prebuilt retrieval assets from {repo_id}...")
|
| 102 |
+
|
| 103 |
+
snapshot_download(
|
| 104 |
+
repo_id=repo_id,
|
| 105 |
+
repo_type="dataset",
|
| 106 |
+
token=token,
|
| 107 |
+
local_dir=str(local_root),
|
| 108 |
+
max_workers=8,
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
manifest = local_root / BUNDLE_MANIFEST
|
| 112 |
+
if not manifest.exists():
|
| 113 |
+
raise RuntimeError(
|
| 114 |
+
f"Prebuilt retrieval bundle is missing {BUNDLE_MANIFEST} in {repo_id}. "
|
| 115 |
+
"Run build_prebuilt_assets.py once before starting the Space."
|
| 116 |
+
)
|
| 117 |
+
|
| 118 |
+
_BUNDLE_ROOT = local_root
|
| 119 |
+
return local_root
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def _read_bundle_manifest(bundle_root: Path) -> Dict[str, Any]:
|
| 123 |
+
path = Path(bundle_root) / BUNDLE_MANIFEST
|
| 124 |
+
try:
|
| 125 |
+
data = json.loads(path.read_text(encoding="utf-8"))
|
| 126 |
+
except Exception as exc:
|
| 127 |
+
raise RuntimeError(f"Could not read prebuilt bundle manifest: {path}") from exc
|
| 128 |
+
|
| 129 |
+
if int(data.get("schema_version", 0)) != 1:
|
| 130 |
+
raise RuntimeError(
|
| 131 |
+
f"Unsupported HUDA-Net prebuilt schema: {data.get('schema_version')!r}"
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
files = data.get("files")
|
| 135 |
+
if not isinstance(files, dict) or not files:
|
| 136 |
+
raise RuntimeError("Prebuilt retrieval manifest has no files.")
|
| 137 |
+
|
| 138 |
+
return data
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def _validate_bundle(
|
| 142 |
+
ns: Mapping[str, Any],
|
| 143 |
+
df: Any,
|
| 144 |
+
bundle_root: Path,
|
| 145 |
+
bundle_meta: Mapping[str, Any],
|
| 146 |
+
) -> str:
|
| 147 |
+
fingerprint_fn = ns.get("_hybrid_fingerprint")
|
| 148 |
+
if not callable(fingerprint_fn):
|
| 149 |
+
raise RuntimeError(
|
| 150 |
+
"HUDA-Net _hybrid_fingerprint(df) is unavailable. "
|
| 151 |
+
"The prebuilt guard must be installed after hybrid functions are defined."
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
current_fingerprint = str(fingerprint_fn(df))
|
| 155 |
+
packaged_fingerprint = str(bundle_meta.get("fingerprint", ""))
|
| 156 |
+
|
| 157 |
+
if not packaged_fingerprint:
|
| 158 |
+
raise RuntimeError("Prebuilt retrieval bundle does not contain a fingerprint.")
|
| 159 |
+
|
| 160 |
+
if packaged_fingerprint != current_fingerprint:
|
| 161 |
+
raise RuntimeError(
|
| 162 |
+
"HUDA-Net prebuilt retrieval assets are STALE for the current Knowledge Base.\n"
|
| 163 |
+
f"Current KB fingerprint : {current_fingerprint}\n"
|
| 164 |
+
f"Packaged fingerprint : {packaged_fingerprint}\n"
|
| 165 |
+
"Run build_prebuilt_assets.py again and publish the new bundle. "
|
| 166 |
+
"Production-time rebuilding is intentionally disabled."
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
files = bundle_meta["files"]
|
| 170 |
+
for rel_text, file_meta in files.items():
|
| 171 |
+
rel = _safe_relative_path(rel_text)
|
| 172 |
+
src = Path(bundle_root) / rel
|
| 173 |
+
|
| 174 |
+
if not src.is_file():
|
| 175 |
+
raise RuntimeError(f"Missing prebuilt retrieval file: {rel}")
|
| 176 |
+
|
| 177 |
+
expected_size = int(file_meta.get("size", -1))
|
| 178 |
+
actual_size = src.stat().st_size
|
| 179 |
+
if expected_size >= 0 and actual_size != expected_size:
|
| 180 |
+
raise RuntimeError(
|
| 181 |
+
f"Prebuilt file size mismatch for {rel}: "
|
| 182 |
+
f"expected={expected_size}, actual={actual_size}"
|
| 183 |
+
)
|
| 184 |
+
|
| 185 |
+
expected_sha = str(file_meta.get("sha256", "")).strip().lower()
|
| 186 |
+
if expected_sha:
|
| 187 |
+
actual_sha = _sha256_file(src)
|
| 188 |
+
if actual_sha.lower() != expected_sha:
|
| 189 |
+
raise RuntimeError(
|
| 190 |
+
f"Prebuilt file SHA-256 mismatch for {rel}: "
|
| 191 |
+
f"expected={expected_sha}, actual={actual_sha}"
|
| 192 |
+
)
|
| 193 |
+
|
| 194 |
+
return current_fingerprint
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
def _restore_bundle_to_target(
|
| 198 |
+
ns: Mapping[str, Any],
|
| 199 |
+
df: Any,
|
| 200 |
+
target_root: Path,
|
| 201 |
+
) -> Tuple[Path, Dict[str, Any]]:
|
| 202 |
+
target_root = Path(target_root)
|
| 203 |
+
target_root.mkdir(parents=True, exist_ok=True)
|
| 204 |
+
|
| 205 |
+
bundle_root = _download_bundle()
|
| 206 |
+
bundle_meta = _read_bundle_manifest(bundle_root)
|
| 207 |
+
fingerprint = _validate_bundle(ns, df, bundle_root, bundle_meta)
|
| 208 |
+
|
| 209 |
+
copied = 0
|
| 210 |
+
for rel_text in bundle_meta["files"].keys():
|
| 211 |
+
rel = _safe_relative_path(rel_text)
|
| 212 |
+
src = bundle_root / rel
|
| 213 |
+
dst = target_root / rel
|
| 214 |
+
_link_or_copy(src, dst)
|
| 215 |
+
copied += 1
|
| 216 |
+
|
| 217 |
+
hybrid_files = ns.get("HYBRID_FILES")
|
| 218 |
+
if not isinstance(hybrid_files, Mapping):
|
| 219 |
+
raise RuntimeError("HUDA-Net HYBRID_FILES mapping is unavailable.")
|
| 220 |
+
|
| 221 |
+
hybrid_manifest_name = str(
|
| 222 |
+
bundle_meta.get(
|
| 223 |
+
"hybrid_manifest_file",
|
| 224 |
+
hybrid_files.get("hybrid_manifest", "hybrid_manifest.json"),
|
| 225 |
+
)
|
| 226 |
+
)
|
| 227 |
+
hybrid_manifest_path = target_root / _safe_relative_path(hybrid_manifest_name)
|
| 228 |
+
|
| 229 |
+
if not hybrid_manifest_path.exists():
|
| 230 |
+
raise RuntimeError(
|
| 231 |
+
f"Restored prebuilt bundle but hybrid manifest is missing: "
|
| 232 |
+
f"{hybrid_manifest_path}"
|
| 233 |
+
)
|
| 234 |
+
|
| 235 |
+
try:
|
| 236 |
+
hybrid_meta = json.loads(hybrid_manifest_path.read_text(encoding="utf-8"))
|
| 237 |
+
except Exception as exc:
|
| 238 |
+
raise RuntimeError(
|
| 239 |
+
f"Could not parse restored hybrid manifest: {hybrid_manifest_path}"
|
| 240 |
+
) from exc
|
| 241 |
+
|
| 242 |
+
if str(hybrid_meta.get("fingerprint", "")) != fingerprint:
|
| 243 |
+
raise RuntimeError(
|
| 244 |
+
"Restored hybrid_manifest.json fingerprint does not match the current KB."
|
| 245 |
+
)
|
| 246 |
+
|
| 247 |
+
required_names = list(hybrid_files.values())
|
| 248 |
+
missing = [
|
| 249 |
+
name for name in required_names
|
| 250 |
+
if not (target_root / str(name)).exists()
|
| 251 |
+
]
|
| 252 |
+
if missing:
|
| 253 |
+
raise RuntimeError(
|
| 254 |
+
"Prebuilt retrieval bundle is incomplete. Missing runtime files: "
|
| 255 |
+
+ ", ".join(map(str, missing))
|
| 256 |
+
)
|
| 257 |
+
|
| 258 |
+
print(
|
| 259 |
+
f"⚡ HUDA-Net prebuilt retrieval restored | "
|
| 260 |
+
f"{copied} files | fingerprint={fingerprint[:16]}..."
|
| 261 |
+
)
|
| 262 |
+
return target_root, hybrid_meta
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
def install_prebuilt_guard(ns: MutableMapping[str, Any]) -> None:
|
| 266 |
+
"""
|
| 267 |
+
Install production-only wrappers into app.py globals().
|
| 268 |
+
|
| 269 |
+
Call this ONCE near the bottom of app.py, immediately before HUDA_DEMO is
|
| 270 |
+
created. In build mode the wrappers are intentionally not installed.
|
| 271 |
+
"""
|
| 272 |
+
global _INSTALLED
|
| 273 |
+
|
| 274 |
+
if _INSTALLED:
|
| 275 |
+
return
|
| 276 |
+
|
| 277 |
+
if _truthy(os.getenv("HUDANET_PREBUILT_BUILD_MODE", "0")):
|
| 278 |
+
print("🛠️ HUDA-Net prebuilt BUILD MODE: original hybrid builder remains enabled.")
|
| 279 |
+
_INSTALLED = True
|
| 280 |
+
return
|
| 281 |
+
|
| 282 |
+
original_build = ns.get("build_hybrid_runtime_assets")
|
| 283 |
+
if not callable(original_build):
|
| 284 |
+
raise RuntimeError(
|
| 285 |
+
"build_hybrid_runtime_assets is not defined yet. "
|
| 286 |
+
"Install the prebuilt guard near the bottom of app.py."
|
| 287 |
+
)
|
| 288 |
+
|
| 289 |
+
def build_hybrid_runtime_assets_prebuilt(
|
| 290 |
+
df: Any,
|
| 291 |
+
sparse_assets: Mapping[str, Any],
|
| 292 |
+
target_root: Path,
|
| 293 |
+
*,
|
| 294 |
+
force: bool = False,
|
| 295 |
+
previous_root: Optional[Path] = None,
|
| 296 |
+
**kwargs: Any,
|
| 297 |
+
) -> Dict[str, Any]:
|
| 298 |
+
del sparse_assets, force, previous_root, kwargs
|
| 299 |
+
_, meta = _restore_bundle_to_target(ns, df, Path(target_root))
|
| 300 |
+
return meta
|
| 301 |
+
|
| 302 |
+
ns["_build_hybrid_runtime_assets_original_v41_0_1"] = original_build
|
| 303 |
+
ns["build_hybrid_runtime_assets"] = build_hybrid_runtime_assets_prebuilt
|
| 304 |
+
|
| 305 |
+
original_ensure = ns.get("ensure_hybrid_runtime_assets")
|
| 306 |
+
if callable(original_ensure):
|
| 307 |
+
def ensure_hybrid_runtime_assets_prebuilt(
|
| 308 |
+
df: Any,
|
| 309 |
+
sparse_assets: Mapping[str, Any],
|
| 310 |
+
runtime_root: Path,
|
| 311 |
+
*args: Any,
|
| 312 |
+
**kwargs: Any,
|
| 313 |
+
) -> Tuple[Path, Dict[str, Any]]:
|
| 314 |
+
del sparse_assets, args, kwargs
|
| 315 |
+
root, meta = _restore_bundle_to_target(ns, df, Path(runtime_root))
|
| 316 |
+
return root, meta
|
| 317 |
+
|
| 318 |
+
ns["_ensure_hybrid_runtime_assets_original_v41_0_1"] = original_ensure
|
| 319 |
+
ns["ensure_hybrid_runtime_assets"] = ensure_hybrid_runtime_assets_prebuilt
|
| 320 |
+
|
| 321 |
+
_INSTALLED = True
|
| 322 |
+
print(
|
| 323 |
+
"🔒 HUDA-Net v41.0.2 prebuilt retrieval guard active | "
|
| 324 |
+
"production rebuild fallback disabled"
|
| 325 |
+
)
|
patch_app_v41_0_2.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
HUDA-Net v41.0.2 app.py Patcher
|
| 4 |
+
===============================
|
| 5 |
+
|
| 6 |
+
Run once in the SAME folder as the CURRENT v41.0.1 app.py:
|
| 7 |
+
|
| 8 |
+
python patch_app_v41_0_2.py
|
| 9 |
+
|
| 10 |
+
It will:
|
| 11 |
+
- create a backup of app.py,
|
| 12 |
+
- change visible 41.0.1 version strings to 41.0.2,
|
| 13 |
+
- install hudanet_prebuilt_guard immediately before HUDA_DEMO creation,
|
| 14 |
+
- compile-check the modified app.py,
|
| 15 |
+
- restore the backup automatically if syntax validation fails.
|
| 16 |
+
|
| 17 |
+
This patcher does NOT replace your v41 logic and does NOT overwrite the
|
| 18 |
+
10,000+ line application with an older copy.
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
from __future__ import annotations
|
| 22 |
+
|
| 23 |
+
import py_compile
|
| 24 |
+
import re
|
| 25 |
+
import shutil
|
| 26 |
+
from pathlib import Path
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
APP = Path("app.py")
|
| 30 |
+
GUARD = Path("hudanet_prebuilt_guard.py")
|
| 31 |
+
BACKUP = Path("app.py.v41_0_1.backup")
|
| 32 |
+
|
| 33 |
+
MARKER = "# HUDA-Net v41.0.2 prebuilt retrieval startup guard"
|
| 34 |
+
INJECTION = f"""
|
| 35 |
+
{MARKER}
|
| 36 |
+
from hudanet_prebuilt_guard import install_prebuilt_guard as _install_hudanet_prebuilt_guard
|
| 37 |
+
_install_hudanet_prebuilt_guard(globals())
|
| 38 |
+
|
| 39 |
+
"""
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def main() -> None:
|
| 43 |
+
if not APP.is_file():
|
| 44 |
+
raise RuntimeError("app.py was not found in the current folder.")
|
| 45 |
+
|
| 46 |
+
if not GUARD.is_file():
|
| 47 |
+
raise RuntimeError(
|
| 48 |
+
"hudanet_prebuilt_guard.py was not found beside app.py."
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
original = APP.read_text(encoding="utf-8")
|
| 52 |
+
|
| 53 |
+
if MARKER in original:
|
| 54 |
+
print("ℹ️ v41.0.2 prebuilt guard is already installed.")
|
| 55 |
+
py_compile.compile(str(APP), doraise=True)
|
| 56 |
+
print("✅ app.py syntax check passed.")
|
| 57 |
+
return
|
| 58 |
+
|
| 59 |
+
# The v41.0.1 logs prove this startup assignment exists.
|
| 60 |
+
match = re.search(
|
| 61 |
+
r"(?m)^(?P<indent>[ \t]*)HUDA_DEMO\s*=\s*None\s+if\s+os\.getenv\("
|
| 62 |
+
r"[\"']HUDANET_SKIP_APP_INIT[\"']",
|
| 63 |
+
original,
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
if not match:
|
| 67 |
+
# Fallback for formatting changes while keeping the same variable.
|
| 68 |
+
match = re.search(r"(?m)^(?P<indent>[ \t]*)HUDA_DEMO\s*=", original)
|
| 69 |
+
|
| 70 |
+
if not match:
|
| 71 |
+
raise RuntimeError(
|
| 72 |
+
"Could not find the HUDA_DEMO startup assignment. "
|
| 73 |
+
"No changes were made."
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
if BACKUP.exists():
|
| 77 |
+
raise RuntimeError(
|
| 78 |
+
f"Backup already exists: {BACKUP}. "
|
| 79 |
+
"Move/delete it only if you intentionally want a new backup."
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
shutil.copy2(APP, BACKUP)
|
| 83 |
+
|
| 84 |
+
try:
|
| 85 |
+
updated = original.replace("41.0.1", "41.0.2")
|
| 86 |
+
insertion_at = match.start()
|
| 87 |
+
|
| 88 |
+
# Because version replacement does not change text length for 41.0.1 -> 41.0.2,
|
| 89 |
+
# the original match offset remains correct.
|
| 90 |
+
updated = updated[:insertion_at] + INJECTION + updated[insertion_at:]
|
| 91 |
+
|
| 92 |
+
APP.write_text(updated, encoding="utf-8")
|
| 93 |
+
py_compile.compile(str(APP), doraise=True)
|
| 94 |
+
|
| 95 |
+
except Exception:
|
| 96 |
+
shutil.copy2(BACKUP, APP)
|
| 97 |
+
raise
|
| 98 |
+
|
| 99 |
+
print("✅ app.py patched to HUDA-Net v41.0.2.")
|
| 100 |
+
print(f"✅ Backup created: {BACKUP}")
|
| 101 |
+
print("✅ Prebuilt retrieval guard inserted before HUDA_DEMO creation.")
|
| 102 |
+
print("✅ Python syntax validation passed.")
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
if __name__ == "__main__":
|
| 106 |
+
main()
|