feat: bump feature manifest to v2 with archive metadata

This commit is contained in:
SnapOtter
2026-06-13 16:27:24 +08:00
parent b8b6b0a44a
commit a4ac7cf7d6
2 changed files with 167 additions and 2 deletions
+100 -1
View File
@@ -1,13 +1,28 @@
{
"manifestVersion": 1,
"manifestVersion": 2,
"imageVersion": "2.0.0",
"pythonVersion": "3.12",
"basePackages": ["numpy==1.26.4", "Pillow==12.2.0", "opencv-python-headless==4.10.0.84"],
"bundleRepo": "snapotter/feature-bundles",
"bundles": {
"background-removal": {
"name": "Background Removal",
"description": "Remove image backgrounds with AI",
"estimatedSize": "4-5 GB",
"archives": {
"amd64-gpu": {
"file": "v2.0.0/background-removal-amd64-gpu.tar.gz",
"sha256": "0000000000000000000000000000000000000000000000000000000000000000",
"compressedSize": 1,
"extractedSize": 1
},
"arm64-cpu": {
"file": "v2.0.0/background-removal-arm64-cpu.tar.gz",
"sha256": "0000000000000000000000000000000000000000000000000000000000000000",
"compressedSize": 1,
"extractedSize": 1
}
},
"packages": {
"common": ["rembg==2.0.62"],
"amd64": ["onnxruntime-gpu==1.20.1", "mediapipe>=0.10.21"],
@@ -61,6 +76,20 @@
"name": "Face Detection",
"description": "Detect and blur faces, fix red-eye, smart crop",
"estimatedSize": "200-300 MB",
"archives": {
"amd64-gpu": {
"file": "v2.0.0/face-detection-amd64-gpu.tar.gz",
"sha256": "0000000000000000000000000000000000000000000000000000000000000000",
"compressedSize": 1,
"extractedSize": 1
},
"arm64-cpu": {
"file": "v2.0.0/face-detection-arm64-cpu.tar.gz",
"sha256": "0000000000000000000000000000000000000000000000000000000000000000",
"compressedSize": 1,
"extractedSize": 1
}
},
"packages": {
"common": [],
"amd64": ["mediapipe>=0.10.21"],
@@ -88,6 +117,20 @@
"name": "Object Eraser & Colorize",
"description": "Erase objects from photos and colorize B&W images",
"estimatedSize": "1-2 GB",
"archives": {
"amd64-gpu": {
"file": "v2.0.0/object-eraser-colorize-amd64-gpu.tar.gz",
"sha256": "0000000000000000000000000000000000000000000000000000000000000000",
"compressedSize": 1,
"extractedSize": 1
},
"arm64-cpu": {
"file": "v2.0.0/object-eraser-colorize-arm64-cpu.tar.gz",
"sha256": "0000000000000000000000000000000000000000000000000000000000000000",
"compressedSize": 1,
"extractedSize": 1
}
},
"packages": {
"common": ["huggingface-hub"],
"amd64": ["onnxruntime-gpu==1.20.1"],
@@ -140,6 +183,20 @@
"name": "Upscale & Enhance",
"description": "AI upscaling, face enhancement, and noise removal",
"estimatedSize": "4-5 GB",
"archives": {
"amd64-gpu": {
"file": "v2.0.0/upscale-enhance-amd64-gpu.tar.gz",
"sha256": "0000000000000000000000000000000000000000000000000000000000000000",
"compressedSize": 1,
"extractedSize": 1
},
"arm64-cpu": {
"file": "v2.0.0/upscale-enhance-arm64-cpu.tar.gz",
"sha256": "0000000000000000000000000000000000000000000000000000000000000000",
"compressedSize": 1,
"extractedSize": 1
}
},
"packages": {
"common": ["codeformer-pip==0.0.4", "huggingface-hub", "einops", "setuptools<75"],
"amd64": [
@@ -216,6 +273,20 @@
"name": "Photo Restoration",
"description": "Restore old or damaged photos",
"estimatedSize": "800 MB - 1 GB",
"archives": {
"amd64-gpu": {
"file": "v2.0.0/photo-restoration-amd64-gpu.tar.gz",
"sha256": "0000000000000000000000000000000000000000000000000000000000000000",
"compressedSize": 1,
"extractedSize": 1
},
"arm64-cpu": {
"file": "v2.0.0/photo-restoration-arm64-cpu.tar.gz",
"sha256": "0000000000000000000000000000000000000000000000000000000000000000",
"compressedSize": 1,
"extractedSize": 1
}
},
"packages": {
"common": ["codeformer-pip==0.0.4", "huggingface-hub", "setuptools<75"],
"amd64": [
@@ -299,6 +370,20 @@
"name": "OCR",
"description": "Extract text from images",
"estimatedSize": "3-4 GB",
"archives": {
"amd64-gpu": {
"file": "v2.0.0/ocr-amd64-gpu.tar.gz",
"sha256": "0000000000000000000000000000000000000000000000000000000000000000",
"compressedSize": 1,
"extractedSize": 1
},
"arm64-cpu": {
"file": "v2.0.0/ocr-arm64-cpu.tar.gz",
"sha256": "0000000000000000000000000000000000000000000000000000000000000000",
"compressedSize": 1,
"extractedSize": 1
}
},
"packages": {
"common": ["huggingface-hub"],
"amd64": [
@@ -357,6 +442,20 @@
"name": "Transcription",
"description": "Speech to text for audio and video (subtitles)",
"estimatedSize": "~600 MB",
"archives": {
"amd64-gpu": {
"file": "v2.0.0/transcription-amd64-gpu.tar.gz",
"sha256": "0000000000000000000000000000000000000000000000000000000000000000",
"compressedSize": 1,
"extractedSize": 1
},
"arm64-cpu": {
"file": "v2.0.0/transcription-arm64-cpu.tar.gz",
"sha256": "0000000000000000000000000000000000000000000000000000000000000000",
"compressedSize": 1,
"extractedSize": 1
}
},
"packages": { "common": ["faster-whisper>=1.0.0"], "amd64": [], "arm64": [] },
"pipFlags": {},
"postInstall": [],
+67 -1
View File
@@ -32,7 +32,7 @@ function archPackagesInclude(bundleId: string, arch: "amd64" | "arm64", pkg: str
describe("Feature manifest structure", () => {
it("manifest has valid version fields", () => {
expect(manifest.manifestVersion).toBe(1);
expect(manifest.manifestVersion).toBe(2);
expect(manifest.pythonVersion).toBeDefined();
expect(manifest.basePackages).toBeInstanceOf(Array);
});
@@ -211,3 +211,69 @@ describe("Feature manifest: enablesTools consistency with shared features", () =
}
});
});
describe("Manifest v2 archive fields", () => {
const ARCH_VARIANTS = ["amd64-gpu", "arm64-cpu"] as const;
it("manifest version is 2", () => {
expect(manifest.manifestVersion).toBe(2);
});
it("has bundleRepo field", () => {
expect(manifest.bundleRepo).toBe("snapotter/feature-bundles");
});
it("every bundle has archives with both arch variants", () => {
for (const [id, bundle] of Object.entries<Record<string, unknown>>(bundles)) {
const archives = bundle.archives as Record<string, unknown>;
expect(archives, `${id} missing archives`).toBeDefined();
for (const arch of ARCH_VARIANTS) {
expect(archives[arch], `${id} missing archives["${arch}"]`).toBeDefined();
}
}
});
it("each archive entry has file, sha256, compressedSize, extractedSize", () => {
for (const [id, bundle] of Object.entries<Record<string, unknown>>(bundles)) {
const archives = bundle.archives as Record<string, Record<string, unknown>>;
for (const arch of ARCH_VARIANTS) {
const entry = archives[arch];
expect(typeof entry.file, `${id}/${arch} file should be string`).toBe("string");
expect(entry.file as string, `${id}/${arch} file should end with .tar.gz`).toMatch(
/\.tar\.gz$/,
);
expect(typeof entry.sha256, `${id}/${arch} sha256 should be string`).toBe("string");
expect(entry.sha256 as string, `${id}/${arch} sha256 should be 64 hex chars`).toMatch(
/^[0-9a-f]{64}$/,
);
expect(typeof entry.compressedSize, `${id}/${arch} compressedSize should be number`).toBe(
"number",
);
expect(
entry.compressedSize as number,
`${id}/${arch} compressedSize should be > 0`,
).toBeGreaterThan(0);
expect(typeof entry.extractedSize, `${id}/${arch} extractedSize should be number`).toBe(
"number",
);
expect(
entry.extractedSize as number,
`${id}/${arch} extractedSize should be > 0`,
).toBeGreaterThan(0);
}
}
});
it("archive file paths include version prefix", () => {
const version = manifest.imageVersion;
for (const [id, bundle] of Object.entries<Record<string, unknown>>(bundles)) {
const archives = bundle.archives as Record<string, Record<string, unknown>>;
for (const arch of ARCH_VARIANTS) {
expect(
archives[arch].file as string,
`${id}/${arch} file should include v${version}/`,
).toContain(`v${version}/`);
}
}
});
});