fix(ai): pin huggingface-hub so bundle rebuilds cannot strand hub 1.x (#683)

The arm64 transcription bundle baked huggingface_hub 1.22.0 while inpaint-hq's transformers needs hub <1.0; last-writer-wins in the shared venv made the hq install fail on arm64. Constrain hub at build time, pin it in transcription's package list, and lock both invariants with manifest unit tests. The arm64 transcription bundle still needs a rebuild and republish to ship the fix. Fixes #669.
This commit is contained in:
SnapOtter
2026-07-30 10:04:15 +08:00
committed by GitHub
parent 890b38a71a
commit 470a0a4acb
2 changed files with 54 additions and 3 deletions
+7 -3
View File
@@ -1,7 +1,10 @@
{
"manifestVersion": 2,
"imageVersion": "2.0.0",
"pythonVersion": { "amd64": "3.12", "arm64": "3.11" },
"pythonVersion": {
"amd64": "3.12",
"arm64": "3.11"
},
"basePackages": [
"numpy==1.26.4",
"Pillow==12.3.0",
@@ -13,7 +16,8 @@
"scipy==1.12.0",
"scikit-learn==1.4.2",
"scikit-image==0.24.0",
"pandas==2.2.2"
"pandas==2.2.2",
"huggingface-hub==0.36.2"
],
"bundleRepo": "deepsafe/feature-bundles",
"bundles": {
@@ -560,7 +564,7 @@
}
},
"packages": {
"common": ["faster-whisper>=1.0.0"],
"common": ["faster-whisper>=1.0.0", "huggingface-hub[hf_xet]==0.36.2"],
"amd64": [],
"arm64": ["protobuf>=4.25.3,<5"]
},
@@ -75,6 +75,53 @@ describe("Feature manifest structure", () => {
});
});
describe("Feature manifest: cross-bundle dependency agreement", () => {
// #669: the arm64 transcription bundle baked huggingface_hub 1.22.0 (pulled
// transitively by unpinned faster-whisper at bundle-build time) while
// inpaint-hq's transformers needs hub <1.0. Bundles ship as diffs against
// the base venv, so last-writer-wins left 1.22.0 in the shared venv and the
// hq install failed verification on arm64.
function pinnedVersions(): Map<string, Map<string, string>> {
const byPackage = new Map<string, Map<string, string>>();
for (const [bundleId, bundle] of Object.entries<Record<string, unknown>>(bundles)) {
const pkgs = bundle.packages as Record<string, string[]>;
const specs = [...(pkgs.common ?? []), ...(pkgs.amd64 ?? []), ...(pkgs.arm64 ?? [])];
for (const spec of specs) {
const m = /^([A-Za-z0-9_.-]+)(?:\[[^\]]*\])?==(\S+)$/.exec(spec);
if (!m) continue;
const name = m[1].toLowerCase();
const perBundle = byPackage.get(name) ?? new Map<string, string>();
perBundle.set(bundleId, m[2]);
byPackage.set(name, perBundle);
}
}
return byPackage;
}
it("bundles that pin the same package agree on the version", () => {
for (const [pkg, byBundle] of pinnedVersions()) {
const versions = new Set(byBundle.values());
expect(
versions.size,
`${pkg} is pinned to different versions across bundles: ${JSON.stringify([...byBundle.entries()])}`,
).toBe(1);
}
});
it("huggingface-hub is constrained and every bundle pin matches the constraint", () => {
const constraints: string[] = manifest.constraints ?? [];
const hub = constraints.find((c) => c.toLowerCase().startsWith("huggingface-hub=="));
expect(
hub,
"constraints must pin huggingface-hub so no bundle rebuild can strand hub 1.x in the shared venv",
).toBeDefined();
const version = (hub ?? "").split("==")[1];
for (const [bundleId, v] of pinnedVersions().get("huggingface-hub") ?? new Map()) {
expect(v, `${bundleId} pins huggingface-hub differently from the constraint`).toBe(version);
}
});
});
describe("Feature manifest: shared-venv numpy-1.x ABI closure lock", () => {
// Legacy feature bundles still share one venv. A package that upgrades numpy
// can strand scipy/scikit-learn/pandas wheels on the wrong ABI and break every