chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { execFileSync } = require('child_process');
|
||||
|
||||
const REPO_ROOT = path.resolve(__dirname, '..');
|
||||
const PACKAGE_JSON = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, 'package.json'), 'utf8'));
|
||||
const PRODUCT_NAME = PACKAGE_JSON.build?.productName || 'Open Generative AI';
|
||||
const PACKAGE_NAME = 'open-generative-ai';
|
||||
const COMMAND_NAME = 'open-generative-ai';
|
||||
const INSTALL_DIR_NAME = PACKAGE_NAME;
|
||||
const LINUX_DEPENDS = [
|
||||
'libasound2t64 | libasound2',
|
||||
'libatk-bridge2.0-0',
|
||||
'libatk1.0-0',
|
||||
'libc6',
|
||||
'libcairo2',
|
||||
'libcups2t64 | libcups2',
|
||||
'libdrm2',
|
||||
'libgbm1',
|
||||
'libglib2.0-0',
|
||||
'libgtk-3-0',
|
||||
'libnspr4',
|
||||
'libnss3',
|
||||
'libpango-1.0-0',
|
||||
'libx11-6',
|
||||
'libx11-xcb1',
|
||||
'libxcb-dri3-0',
|
||||
'libxcomposite1',
|
||||
'libxdamage1',
|
||||
'libxext6',
|
||||
'libxfixes3',
|
||||
'libxkbcommon0',
|
||||
'libxrandr2',
|
||||
'xdg-utils',
|
||||
].join(', ');
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = {};
|
||||
|
||||
for (let i = 0; i < argv.length; i += 1) {
|
||||
const arg = argv[i];
|
||||
if (!arg.startsWith('--')) continue;
|
||||
|
||||
const [rawKey, inlineValue] = arg.slice(2).split('=');
|
||||
if (inlineValue !== undefined) {
|
||||
args[rawKey] = inlineValue;
|
||||
continue;
|
||||
}
|
||||
|
||||
const nextValue = argv[i + 1];
|
||||
if (nextValue && !nextValue.startsWith('--')) {
|
||||
args[rawKey] = nextValue;
|
||||
i += 1;
|
||||
} else {
|
||||
args[rawKey] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
function toDebArch(arch) {
|
||||
if (arch === 'x64') return 'amd64';
|
||||
if (arch === 'arm64') return 'arm64';
|
||||
return arch;
|
||||
}
|
||||
|
||||
function getDefaultAppDir(arch) {
|
||||
const folderName = arch === 'arm64' ? 'linux-arm64-unpacked' : 'linux-unpacked';
|
||||
return path.join(REPO_ROOT, 'release', folderName);
|
||||
}
|
||||
|
||||
function detectExecutableName(appDir) {
|
||||
const preferredNames = [PRODUCT_NAME, PACKAGE_JSON.productName, PACKAGE_JSON.name]
|
||||
.filter(Boolean);
|
||||
|
||||
for (const fileName of preferredNames) {
|
||||
const fullPath = path.join(appDir, fileName);
|
||||
if (fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) {
|
||||
return fileName;
|
||||
}
|
||||
}
|
||||
|
||||
const denyList = new Set([
|
||||
'chrome-sandbox',
|
||||
'chrome_crashpad_handler',
|
||||
]);
|
||||
|
||||
const candidates = fs.readdirSync(appDir, { withFileTypes: true })
|
||||
.filter((entry) => entry.isFile())
|
||||
.map((entry) => entry.name)
|
||||
.filter((name) => !denyList.has(name))
|
||||
.filter((name) => !name.endsWith('.so') && !name.includes('.so.'))
|
||||
.filter((name) => (fs.statSync(path.join(appDir, name)).mode & 0o111) !== 0);
|
||||
|
||||
if (candidates.length === 0) {
|
||||
throw new Error(`Could not detect the packaged executable in ${appDir}`);
|
||||
}
|
||||
|
||||
return candidates[0];
|
||||
}
|
||||
|
||||
function writeFile(targetPath, contents, mode) {
|
||||
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
||||
fs.writeFileSync(targetPath, contents);
|
||||
if (mode) fs.chmodSync(targetPath, mode);
|
||||
}
|
||||
|
||||
function main() {
|
||||
if (process.platform !== 'linux') {
|
||||
console.error('This packaging script must be run on Linux.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
const arch = args.arch || process.arch;
|
||||
const debArch = toDebArch(arch);
|
||||
const appDir = path.resolve(args['app-dir'] || getDefaultAppDir(arch));
|
||||
const outputDir = path.resolve(args['output-dir'] || path.join(REPO_ROOT, 'release'));
|
||||
const version = args.version || PACKAGE_JSON.version;
|
||||
|
||||
if (!fs.existsSync(appDir)) {
|
||||
console.error(`Packaged app directory not found: ${appDir}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const executableName = detectExecutableName(appDir);
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'oga-deb-'));
|
||||
const packageRoot = path.join(tempRoot, 'pkgroot');
|
||||
const installDir = path.join(packageRoot, 'opt', INSTALL_DIR_NAME);
|
||||
const wrapperPath = path.join(packageRoot, 'usr', 'bin', COMMAND_NAME);
|
||||
const desktopPath = path.join(packageRoot, 'usr', 'share', 'applications', `${PACKAGE_NAME}.desktop`);
|
||||
const iconPath = path.join(packageRoot, 'usr', 'share', 'pixmaps', `${PACKAGE_NAME}.png`);
|
||||
const controlPath = path.join(packageRoot, 'DEBIAN', 'control');
|
||||
const outputPath = path.join(outputDir, `${PACKAGE_NAME}_${version}_${debArch}.deb`);
|
||||
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
fs.cpSync(appDir, installDir, { recursive: true });
|
||||
|
||||
const chromeSandboxPath = path.join(installDir, 'chrome-sandbox');
|
||||
if (fs.existsSync(chromeSandboxPath)) {
|
||||
fs.chmodSync(chromeSandboxPath, 0o4755);
|
||||
}
|
||||
|
||||
writeFile(
|
||||
wrapperPath,
|
||||
`#!/bin/sh\nexec "/opt/${INSTALL_DIR_NAME}/${executableName}" "$@"\n`,
|
||||
0o755
|
||||
);
|
||||
|
||||
writeFile(
|
||||
desktopPath,
|
||||
`[Desktop Entry]
|
||||
Name=${PRODUCT_NAME}
|
||||
Exec=${COMMAND_NAME}
|
||||
Icon=${PACKAGE_NAME}
|
||||
Type=Application
|
||||
Categories=Graphics;Utility;
|
||||
Terminal=false
|
||||
StartupNotify=true
|
||||
`,
|
||||
0o644
|
||||
);
|
||||
|
||||
fs.mkdirSync(path.dirname(iconPath), { recursive: true });
|
||||
fs.copyFileSync(path.join(REPO_ROOT, 'public', 'banner.png'), iconPath);
|
||||
fs.chmodSync(iconPath, 0o644);
|
||||
|
||||
writeFile(
|
||||
controlPath,
|
||||
`Package: ${PACKAGE_NAME}
|
||||
Version: ${version}
|
||||
Section: graphics
|
||||
Priority: optional
|
||||
Architecture: ${debArch}
|
||||
Maintainer: Open Generative AI Team
|
||||
Depends: ${LINUX_DEPENDS}
|
||||
Description: Local-first generative AI studio for image, video, and design workflows
|
||||
`,
|
||||
0o644
|
||||
);
|
||||
|
||||
execFileSync('dpkg-deb', ['--build', '--root-owner-group', packageRoot, outputPath], {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
fs.rmSync(tempRoot, { recursive: true, force: true });
|
||||
console.log(`Created ${outputPath}`);
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,81 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const REQUIRED_FILES = {
|
||||
darwin: ['sd-cli', 'libstable-diffusion.dylib'],
|
||||
linux: ['sd-cli', 'libstable-diffusion.so'],
|
||||
win32: ['sd-cli.exe'],
|
||||
};
|
||||
|
||||
const OPTIONAL_FILES = ['sd-server'];
|
||||
|
||||
function resolveSourceBinDir(sourcePath) {
|
||||
const absoluteSourcePath = path.resolve(sourcePath);
|
||||
const nestedBinDir = path.join(absoluteSourcePath, 'bin');
|
||||
|
||||
if (fs.existsSync(nestedBinDir) && fs.statSync(nestedBinDir).isDirectory()) {
|
||||
return nestedBinDir;
|
||||
}
|
||||
|
||||
return absoluteSourcePath;
|
||||
}
|
||||
|
||||
function stageLocalAiBinary({ platform, arch, sourcePath }) {
|
||||
const sourceBinDir = resolveSourceBinDir(sourcePath);
|
||||
const requiredFiles = REQUIRED_FILES[platform];
|
||||
|
||||
if (!requiredFiles) {
|
||||
throw new Error(`Unsupported platform "${platform}". Expected one of: ${Object.keys(REQUIRED_FILES).join(', ')}`);
|
||||
}
|
||||
|
||||
const missingFiles = requiredFiles.filter((fileName) => !fs.existsSync(path.join(sourceBinDir, fileName)));
|
||||
if (missingFiles.length > 0) {
|
||||
throw new Error(`Missing required files in ${sourceBinDir}: ${missingFiles.join(', ')}`);
|
||||
}
|
||||
|
||||
const repoRoot = path.resolve(__dirname, '..');
|
||||
const stageDir = path.join(repoRoot, 'build', 'local-ai', `${platform}-${arch}`, 'bin');
|
||||
|
||||
fs.rmSync(stageDir, { recursive: true, force: true });
|
||||
fs.mkdirSync(stageDir, { recursive: true });
|
||||
|
||||
for (const fileName of [...requiredFiles, ...OPTIONAL_FILES]) {
|
||||
const sourceFile = path.join(sourceBinDir, fileName);
|
||||
if (!fs.existsSync(sourceFile)) continue;
|
||||
|
||||
const targetFile = path.join(stageDir, fileName);
|
||||
fs.copyFileSync(sourceFile, targetFile);
|
||||
|
||||
if (platform !== 'win32') {
|
||||
fs.chmodSync(targetFile, 0o755);
|
||||
}
|
||||
}
|
||||
|
||||
return stageDir;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const [platform, arch, sourcePath] = process.argv.slice(2);
|
||||
|
||||
if (!platform || !arch || !sourcePath) {
|
||||
console.error('Usage: node scripts/stage-local-ai-binary.js <platform> <arch> <source-path>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
const stageDir = stageLocalAiBinary({ platform, arch, sourcePath });
|
||||
console.log(`Staged local AI binaries in ${stageDir}`);
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
resolveSourceBinDir,
|
||||
stageLocalAiBinary,
|
||||
};
|
||||
@@ -0,0 +1,179 @@
|
||||
/**
|
||||
* Test script for MiniMax provider integration.
|
||||
*
|
||||
* Verifies that the MiniMax Image 01 model is correctly registered in models.js
|
||||
* and that the model definition has the expected structure.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/test_minimax_provider.js
|
||||
*
|
||||
* Set MUAPI_KEY env var to run the live API smoke test:
|
||||
* MUAPI_KEY=your_key node scripts/test_minimax_provider.js
|
||||
*/
|
||||
|
||||
import { readFileSync } from "fs";
|
||||
import { fileURLToPath } from "url";
|
||||
import { dirname, join } from "path";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(__dirname, "..");
|
||||
|
||||
// ── 1. Model registration check ──────────────────────────────────────────────
|
||||
|
||||
const modelsContent = readFileSync(
|
||||
join(ROOT, "src", "lib", "models.js"),
|
||||
"utf-8"
|
||||
);
|
||||
|
||||
// Extract the t2iModels JSON array via a simple regex
|
||||
const t2iMatch = modelsContent.match(/export const t2iModels = (\[[\s\S]*?\]);/);
|
||||
if (!t2iMatch) {
|
||||
console.error("FAIL: Could not parse t2iModels from src/lib/models.js");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let t2iModels;
|
||||
try {
|
||||
t2iModels = JSON.parse(t2iMatch[1]);
|
||||
} catch (err) {
|
||||
console.error("FAIL: t2iModels is not valid JSON:", err.message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const minimaxModel = t2iModels.find((m) => m.id === "minimax-image-01");
|
||||
|
||||
if (!minimaxModel) {
|
||||
console.error(
|
||||
'FAIL: "minimax-image-01" not found in t2iModels.\n' +
|
||||
"Expected it to be registered in src/lib/models.js."
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
const required = ["id", "name", "endpoint", "family", "inputs"];
|
||||
for (const field of required) {
|
||||
if (!minimaxModel[field]) {
|
||||
console.error(`FAIL: minimax-image-01 is missing required field: ${field}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (minimaxModel.family !== "minimax") {
|
||||
console.error(
|
||||
`FAIL: expected family "minimax", got "${minimaxModel.family}"`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!minimaxModel.inputs.prompt) {
|
||||
console.error("FAIL: minimax-image-01 inputs missing 'prompt' field");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!minimaxModel.inputs.aspect_ratio?.enum?.includes("1:1")) {
|
||||
console.error(
|
||||
"FAIL: minimax-image-01 aspect_ratio enum does not include '1:1'"
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log("PASS: minimax-image-01 is correctly registered in t2iModels");
|
||||
console.log(
|
||||
` endpoint=${minimaxModel.endpoint} family=${minimaxModel.family}`
|
||||
);
|
||||
console.log(
|
||||
` aspect ratios: ${minimaxModel.inputs.aspect_ratio.enum.join(", ")}`
|
||||
);
|
||||
|
||||
// ── 2. models_dump.json check ─────────────────────────────────────────────────
|
||||
|
||||
const dump = JSON.parse(
|
||||
readFileSync(join(ROOT, "models_dump.json"), "utf-8")
|
||||
);
|
||||
const dumpEntry = dump.t2i?.find((m) => m.id === "minimax-image-01");
|
||||
if (!dumpEntry) {
|
||||
console.error(
|
||||
'FAIL: "minimax-image-01" not found in models_dump.json t2i section'
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log("PASS: minimax-image-01 found in models_dump.json");
|
||||
|
||||
// ── 3. Live API smoke test (optional) ────────────────────────────────────────
|
||||
|
||||
const apiKey = process.env.MUAPI_KEY;
|
||||
if (!apiKey) {
|
||||
console.log(
|
||||
"\nINFO: Skipping live API test (set MUAPI_KEY env var to enable)."
|
||||
);
|
||||
console.log("\nAll checks passed.");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log("\nRunning live API smoke test against muapi.ai …");
|
||||
|
||||
const MUAPI_BASE = "https://api.muapi.ai";
|
||||
|
||||
async function testMiniMaxImageGeneration() {
|
||||
const endpoint = minimaxModel.endpoint;
|
||||
const url = `${MUAPI_BASE}/api/v1/${endpoint}`;
|
||||
|
||||
const payload = {
|
||||
prompt: "A simple test: a red apple on a white background.",
|
||||
aspect_ratio: "1:1",
|
||||
num_images: 1,
|
||||
};
|
||||
|
||||
console.log(`POST ${url}`);
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", "x-api-key": apiKey },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
console.error(`FAIL: API returned ${res.status}: ${text.slice(0, 200)}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
const requestId = data.request_id || data.id;
|
||||
if (!requestId) {
|
||||
console.error("FAIL: No request_id in response:", JSON.stringify(data));
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`PASS: Generation queued — request_id=${requestId}`);
|
||||
|
||||
// Poll for result (max 60 s)
|
||||
const pollUrl = `${MUAPI_BASE}/api/v1/predictions/${requestId}/result`;
|
||||
for (let i = 0; i < 30; i++) {
|
||||
await new Promise((r) => setTimeout(r, 2000));
|
||||
const poll = await fetch(pollUrl, {
|
||||
headers: { "Content-Type": "application/json", "x-api-key": apiKey },
|
||||
});
|
||||
if (!poll.ok) continue;
|
||||
const result = await poll.json();
|
||||
const status = result.status?.toLowerCase();
|
||||
if (status === "completed" || status === "succeeded" || status === "success") {
|
||||
const imageUrl =
|
||||
result.outputs?.[0] || result.url || result.output?.url;
|
||||
console.log(`PASS: Generation complete — image URL: ${imageUrl}`);
|
||||
console.log("\nAll checks passed.");
|
||||
return;
|
||||
}
|
||||
if (status === "failed" || status === "error") {
|
||||
console.error("FAIL: Generation failed:", result.error);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(` Polling … status=${status}`);
|
||||
}
|
||||
console.error("FAIL: Timed out waiting for generation result.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
testMiniMaxImageGeneration().catch((err) => {
|
||||
console.error("FAIL: Unexpected error:", err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user