@zalify/cli 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +153 -46
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -11849,29 +11849,112 @@ async function workspaceSet(slugOrId) {
|
|
|
11849
11849
|
|
|
11850
11850
|
// src/images.ts
|
|
11851
11851
|
import { createHash as createHash2 } from "node:crypto";
|
|
11852
|
-
import { readFileSync as readFileSync3, writeFileSync as writeFileSync4, existsSync as existsSync3 } from "node:fs";
|
|
11852
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync4, existsSync as existsSync3, rmSync as rmSync2 } from "node:fs";
|
|
11853
11853
|
import { join as join3, resolve as resolve2 } from "node:path";
|
|
11854
|
-
var
|
|
11855
|
-
|
|
11856
|
-
|
|
11857
|
-
|
|
11858
|
-
|
|
11859
|
-
|
|
11860
|
-
|
|
11854
|
+
var MAX_JOBS_PER_REQUEST = 40;
|
|
11855
|
+
var POLL_INTERVAL_MS = 5000;
|
|
11856
|
+
var POLL_TIMEOUT_MS = 30 * 60 * 1000;
|
|
11857
|
+
var STREAM_BATCH = 20;
|
|
11858
|
+
function buildPrompt(manifest, entry) {
|
|
11859
|
+
const style = entry.type === "model" ? manifest.modelStyle ?? manifest.style : manifest.style;
|
|
11860
|
+
return `${style}
|
|
11861
|
+
|
|
11862
|
+
${entry.prompt}`;
|
|
11863
|
+
}
|
|
11864
|
+
function apiError(auth, label, status, json) {
|
|
11865
|
+
if (json.code === "UPGRADE_REQUIRED") {
|
|
11866
|
+
return new Error(`${json.error ?? "Paid plan required."}
|
|
11867
|
+
Upgrade: ${auth.appUrl}/store/${auth.workspaceSlug}/settings/billing`);
|
|
11861
11868
|
}
|
|
11862
|
-
|
|
11863
|
-
|
|
11864
|
-
|
|
11865
|
-
|
|
11866
|
-
|
|
11867
|
-
|
|
11868
|
-
|
|
11869
|
+
return new Error(`${label} ${status}: ${JSON.stringify(json)}`);
|
|
11870
|
+
}
|
|
11871
|
+
function sleep(ms) {
|
|
11872
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
11873
|
+
}
|
|
11874
|
+
async function saveProduced(imagesDir, indexPath, index, file2, asset) {
|
|
11875
|
+
const img = await fetch(asset.url);
|
|
11876
|
+
if (!img.ok) {
|
|
11877
|
+
console.error(` ✗ ${file2}: download ${img.status}`);
|
|
11878
|
+
return false;
|
|
11869
11879
|
}
|
|
11870
|
-
|
|
11871
|
-
|
|
11872
|
-
|
|
11873
|
-
|
|
11874
|
-
|
|
11880
|
+
const bytes = Buffer.from(await img.arrayBuffer());
|
|
11881
|
+
writeFileSync4(join3(imagesDir, file2), bytes);
|
|
11882
|
+
index[file2] = {
|
|
11883
|
+
assetId: asset.id,
|
|
11884
|
+
url: asset.url,
|
|
11885
|
+
checksum: createHash2("sha256").update(bytes).digest("hex")
|
|
11886
|
+
};
|
|
11887
|
+
writeFileSync4(indexPath, JSON.stringify(index, null, 2) + `
|
|
11888
|
+
`);
|
|
11889
|
+
return true;
|
|
11890
|
+
}
|
|
11891
|
+
async function submitJob(auth, manifest, entries) {
|
|
11892
|
+
const res = await fetch(`${auth.appUrl}/api/images/jobs`, {
|
|
11893
|
+
method: "POST",
|
|
11894
|
+
headers: {
|
|
11895
|
+
"Content-Type": "application/json",
|
|
11896
|
+
Authorization: `Bearer ${auth.key}`
|
|
11897
|
+
},
|
|
11898
|
+
body: JSON.stringify({
|
|
11899
|
+
workspaceId: auth.workspaceId,
|
|
11900
|
+
jobs: entries.map((e) => ({
|
|
11901
|
+
prompt: buildPrompt(manifest, e),
|
|
11902
|
+
size: "1024x1024",
|
|
11903
|
+
count: 1
|
|
11904
|
+
}))
|
|
11905
|
+
})
|
|
11906
|
+
});
|
|
11907
|
+
if (res.status === 404)
|
|
11908
|
+
return null;
|
|
11909
|
+
const json = await res.json().catch(() => ({}));
|
|
11910
|
+
if (!res.ok)
|
|
11911
|
+
throw apiError(auth, "images/jobs", res.status, json);
|
|
11912
|
+
if (!json.jobId)
|
|
11913
|
+
throw new Error(`images/jobs: no jobId in response`);
|
|
11914
|
+
return json.jobId;
|
|
11915
|
+
}
|
|
11916
|
+
async function pollJob(auth, jobId, files, imagesDir, indexPath, index, counters) {
|
|
11917
|
+
const handled = new Set;
|
|
11918
|
+
const reported = new Set;
|
|
11919
|
+
const deadline = Date.now() + POLL_TIMEOUT_MS;
|
|
11920
|
+
while (true) {
|
|
11921
|
+
const res = await fetch(`${auth.appUrl}/api/images/jobs/${jobId}?workspaceId=${encodeURIComponent(auth.workspaceId)}`, { headers: { Authorization: `Bearer ${auth.key}` } });
|
|
11922
|
+
const json = await res.json().catch(() => ({}));
|
|
11923
|
+
if (!res.ok)
|
|
11924
|
+
throw apiError(auth, `images/jobs/${jobId}`, res.status, json);
|
|
11925
|
+
const total = json.total || files.length;
|
|
11926
|
+
for (const item of json.produced ?? []) {
|
|
11927
|
+
if (handled.has(item.jobIndex))
|
|
11928
|
+
continue;
|
|
11929
|
+
handled.add(item.jobIndex);
|
|
11930
|
+
const file2 = files[item.jobIndex];
|
|
11931
|
+
if (!file2)
|
|
11932
|
+
continue;
|
|
11933
|
+
if (await saveProduced(imagesDir, indexPath, index, file2, item.asset)) {
|
|
11934
|
+
counters.produced++;
|
|
11935
|
+
console.log(` ✓ ${file2} (${handled.size + reported.size}/${total})`);
|
|
11936
|
+
} else {
|
|
11937
|
+
counters.failed++;
|
|
11938
|
+
}
|
|
11939
|
+
}
|
|
11940
|
+
for (const f of json.failed ?? []) {
|
|
11941
|
+
if (reported.has(f.jobIndex))
|
|
11942
|
+
continue;
|
|
11943
|
+
reported.add(f.jobIndex);
|
|
11944
|
+
counters.failed++;
|
|
11945
|
+
console.error(` ✗ ${files[f.jobIndex] ?? `job ${f.jobIndex}`}: ${f.message}`);
|
|
11946
|
+
}
|
|
11947
|
+
if (json.status === "done")
|
|
11948
|
+
return;
|
|
11949
|
+
if (Date.now() >= deadline) {
|
|
11950
|
+
throw new Error(`Timed out after 30 minutes waiting for job ${jobId}. ` + `The job keeps running server-side — re-run \`zalify images generate\` to resume.`);
|
|
11951
|
+
}
|
|
11952
|
+
await sleep(POLL_INTERVAL_MS);
|
|
11953
|
+
}
|
|
11954
|
+
}
|
|
11955
|
+
async function streamingGenerate(auth, manifest, missing, imagesDir, indexPath, index, counters) {
|
|
11956
|
+
for (let i = 0;i < missing.length; i += STREAM_BATCH) {
|
|
11957
|
+
const batch = missing.slice(i, i + STREAM_BATCH);
|
|
11875
11958
|
const res = await fetch(`${auth.appUrl}/api/assets/generate`, {
|
|
11876
11959
|
method: "POST",
|
|
11877
11960
|
headers: {
|
|
@@ -11881,9 +11964,7 @@ async function imagesGenerate(storeDir) {
|
|
|
11881
11964
|
body: JSON.stringify({
|
|
11882
11965
|
workspaceId: auth.workspaceId,
|
|
11883
11966
|
jobs: batch.map((e) => ({
|
|
11884
|
-
prompt:
|
|
11885
|
-
|
|
11886
|
-
${e.prompt}`,
|
|
11967
|
+
prompt: buildPrompt(manifest, e),
|
|
11887
11968
|
count: 1,
|
|
11888
11969
|
size: "1024x1024"
|
|
11889
11970
|
}))
|
|
@@ -11891,11 +11972,7 @@ ${e.prompt}`,
|
|
|
11891
11972
|
});
|
|
11892
11973
|
if (!res.ok || !res.body) {
|
|
11893
11974
|
const json = await res.json().catch(() => ({}));
|
|
11894
|
-
|
|
11895
|
-
throw new Error(`${json.error ?? "Paid plan required."}
|
|
11896
|
-
Upgrade: ${auth.appUrl}/store/${auth.workspaceSlug}/settings/billing`);
|
|
11897
|
-
}
|
|
11898
|
-
throw new Error(`generate ${res.status}: ${JSON.stringify(json)}`);
|
|
11975
|
+
throw apiError(auth, "generate", res.status, json);
|
|
11899
11976
|
}
|
|
11900
11977
|
const reader = res.body.getReader();
|
|
11901
11978
|
const decoder = new TextDecoder;
|
|
@@ -11921,32 +11998,62 @@ ${e.prompt}`,
|
|
|
11921
11998
|
const entry = batch[event.jobIndex];
|
|
11922
11999
|
if (!entry)
|
|
11923
12000
|
continue;
|
|
11924
|
-
|
|
11925
|
-
|
|
11926
|
-
console.
|
|
11927
|
-
|
|
11928
|
-
|
|
12001
|
+
if (await saveProduced(imagesDir, indexPath, index, entry.file, event.asset)) {
|
|
12002
|
+
counters.produced++;
|
|
12003
|
+
console.log(` ✓ ${entry.file} (${counters.produced + counters.failed}/${missing.length})`);
|
|
12004
|
+
} else {
|
|
12005
|
+
counters.failed++;
|
|
11929
12006
|
}
|
|
11930
|
-
const bytes = Buffer.from(await img.arrayBuffer());
|
|
11931
|
-
writeFileSync4(join3(imagesDir, entry.file), bytes);
|
|
11932
|
-
index[entry.file] = {
|
|
11933
|
-
assetId: event.asset.id,
|
|
11934
|
-
url: event.asset.url,
|
|
11935
|
-
checksum: createHash2("sha256").update(bytes).digest("hex")
|
|
11936
|
-
};
|
|
11937
|
-
writeFileSync4(indexPath, JSON.stringify(index, null, 2) + `
|
|
11938
|
-
`);
|
|
11939
|
-
produced++;
|
|
11940
|
-
console.log(` ✓ ${entry.file} (${produced + failed}/${missing.length})`);
|
|
11941
12007
|
} else if (event.type === "error") {
|
|
11942
12008
|
const entry = batch[event.jobIndex];
|
|
11943
|
-
failed++;
|
|
12009
|
+
counters.failed++;
|
|
11944
12010
|
console.error(` ✗ ${entry?.file ?? `job ${event.jobIndex}`}: ${event.message}`);
|
|
11945
12011
|
}
|
|
11946
12012
|
}
|
|
11947
12013
|
}
|
|
11948
12014
|
}
|
|
11949
|
-
|
|
12015
|
+
}
|
|
12016
|
+
async function imagesGenerate(storeDir) {
|
|
12017
|
+
const auth = requireActive();
|
|
12018
|
+
const imagesDir = join3(resolve2(storeDir), "images");
|
|
12019
|
+
const manifestPath = join3(imagesDir, "manifest.json");
|
|
12020
|
+
if (!existsSync3(manifestPath)) {
|
|
12021
|
+
throw new Error(`No images/manifest.json in ${storeDir}`);
|
|
12022
|
+
}
|
|
12023
|
+
const manifest = JSON.parse(readFileSync3(manifestPath, "utf8"));
|
|
12024
|
+
const indexPath = join3(imagesDir, "assets.json");
|
|
12025
|
+
const index = existsSync3(indexPath) ? JSON.parse(readFileSync3(indexPath, "utf8")) : {};
|
|
12026
|
+
const jobStatePath = join3(imagesDir, ".imagegen-job.json");
|
|
12027
|
+
const counters = { produced: 0, failed: 0 };
|
|
12028
|
+
if (existsSync3(jobStatePath)) {
|
|
12029
|
+
const state = JSON.parse(readFileSync3(jobStatePath, "utf8"));
|
|
12030
|
+
console.log(`Resuming job ${state.jobId}`);
|
|
12031
|
+
await pollJob(auth, state.jobId, state.files, imagesDir, indexPath, index, counters);
|
|
12032
|
+
rmSync2(jobStatePath, { force: true });
|
|
12033
|
+
console.log(`Done: ${counters.produced} generated (already in the asset library), ${counters.failed} failed${counters.failed ? " — re-run to retry" : ""}.`);
|
|
12034
|
+
return;
|
|
12035
|
+
}
|
|
12036
|
+
const missing = manifest.images.filter((e) => !existsSync3(join3(imagesDir, e.file)));
|
|
12037
|
+
if (missing.length === 0) {
|
|
12038
|
+
console.log("All manifest images already exist locally.");
|
|
12039
|
+
return;
|
|
12040
|
+
}
|
|
12041
|
+
console.log(`Generating ${missing.length} image(s) on Zalify (workspace "${auth.workspaceName}")…`);
|
|
12042
|
+
for (let i = 0;i < missing.length; i += MAX_JOBS_PER_REQUEST) {
|
|
12043
|
+
const chunk = missing.slice(i, i + MAX_JOBS_PER_REQUEST);
|
|
12044
|
+
const jobId = await submitJob(auth, manifest, chunk);
|
|
12045
|
+
if (jobId === null) {
|
|
12046
|
+
console.log("job API unavailable — using streaming mode");
|
|
12047
|
+
await streamingGenerate(auth, manifest, missing.slice(i), imagesDir, indexPath, index, counters);
|
|
12048
|
+
break;
|
|
12049
|
+
}
|
|
12050
|
+
const state = { jobId, files: chunk.map((e) => e.file) };
|
|
12051
|
+
writeFileSync4(jobStatePath, JSON.stringify(state, null, 2) + `
|
|
12052
|
+
`);
|
|
12053
|
+
await pollJob(auth, jobId, state.files, imagesDir, indexPath, index, counters);
|
|
12054
|
+
rmSync2(jobStatePath, { force: true });
|
|
12055
|
+
}
|
|
12056
|
+
console.log(`Done: ${counters.produced} generated (already in the asset library), ${counters.failed} failed${counters.failed ? " — re-run to retry" : ""}.`);
|
|
11950
12057
|
}
|
|
11951
12058
|
|
|
11952
12059
|
// src/cli.ts
|