genlab 0.1.1 → 0.1.2
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.mjs +117 -143
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -18,7 +18,7 @@ import { execFile } from "node:child_process";
|
|
|
18
18
|
import { parseEnv, promisify } from "node:util";
|
|
19
19
|
import * as p from "@clack/prompts";
|
|
20
20
|
// package.json
|
|
21
|
-
var version = "0.1.
|
|
21
|
+
var version = "0.1.2";
|
|
22
22
|
// src/errors.ts
|
|
23
23
|
/** 面向使用者的预期错误:只输出 message,不带堆栈。 */
|
|
24
24
|
var CliError = class extends Error {
|
|
@@ -656,42 +656,10 @@ async function claimAssetDir(project, location) {
|
|
|
656
656
|
throw error;
|
|
657
657
|
}
|
|
658
658
|
}
|
|
659
|
-
// src/generation/state.ts
|
|
660
|
-
/** 一个 kie 任务对应的版本里,积分只记在结果序号 0 上,避免重复计量。 */
|
|
661
|
-
function creditsFor(version, credits) {
|
|
662
|
-
return (version.resultIndex ?? 0) === 0 ? credits : null;
|
|
663
|
-
}
|
|
664
|
-
function failVersion(record, number, reason, task = {
|
|
665
|
-
credits: null,
|
|
666
|
-
kieRecord: null
|
|
667
|
-
}) {
|
|
668
|
-
const version = record.versions.find((item) => item.number === number);
|
|
669
|
-
if (!version || version.state !== "generating") return;
|
|
670
|
-
version.state = "failed";
|
|
671
|
-
version.failReason = reason;
|
|
672
|
-
version.credits = creditsFor(version, task.credits);
|
|
673
|
-
version.kieRecord = task.kieRecord;
|
|
674
|
-
version.updatedAt = now();
|
|
675
|
-
if (record.currentVersion === number) {
|
|
676
|
-
const next = nextCurrentAfterFailure(record.versions, version);
|
|
677
|
-
if (next != null) record.currentVersion = next;
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
function fulfillVersion(record, number, product, task) {
|
|
681
|
-
const version = record.versions.find((item) => item.number === number);
|
|
682
|
-
if (!version || version.state !== "generating") return;
|
|
683
|
-
Object.assign(version, product, {
|
|
684
|
-
state: "ready",
|
|
685
|
-
failReason: null,
|
|
686
|
-
credits: creditsFor(version, task.credits),
|
|
687
|
-
kieRecord: task.kieRecord,
|
|
688
|
-
updatedAt: now()
|
|
689
|
-
});
|
|
690
|
-
}
|
|
691
659
|
// src/generation/submit.ts
|
|
692
660
|
/**
|
|
693
|
-
*
|
|
694
|
-
*
|
|
661
|
+
* 发起生成:按模型的 schema 校验,上传文件并向 kie 下单,拿到任务编号后才写进记录。
|
|
662
|
+
* 记录里生成中的版本因此都有任务编号;下单失败直接报错,不留下版本。
|
|
695
663
|
*/
|
|
696
664
|
const defaultDeps$1 = {
|
|
697
665
|
upload: uploadKieFile,
|
|
@@ -699,13 +667,30 @@ const defaultDeps$1 = {
|
|
|
699
667
|
};
|
|
700
668
|
/** 一次生成最多并列几个 kie 任务。 */
|
|
701
669
|
const MAX_TASKS = 10;
|
|
702
|
-
/**
|
|
670
|
+
/** 新建素材并发起第一次生成;新素材的当前版本取版本号最靠前的版本。 */
|
|
703
671
|
async function createAsset(project, location, options, deps = defaultDeps$1) {
|
|
704
672
|
const plan = await planGeneration(options, null);
|
|
673
|
+
const references = await inspectReferences(project.root, plan.input);
|
|
705
674
|
await claimAssetDir(project, location);
|
|
706
|
-
let created;
|
|
707
675
|
try {
|
|
708
|
-
|
|
676
|
+
const orders = await placeOrders(plan.model.model, plan.input, null, plan.tasks, deps);
|
|
677
|
+
const record = {
|
|
678
|
+
schema: 1,
|
|
679
|
+
id: randomUUID(),
|
|
680
|
+
kind: plan.model.kind,
|
|
681
|
+
currentVersion: 0,
|
|
682
|
+
createdAt: now(),
|
|
683
|
+
generations: [],
|
|
684
|
+
versions: []
|
|
685
|
+
};
|
|
686
|
+
const id = nextGenerationId(record.generations);
|
|
687
|
+
const versions = appendGeneration(record, id, plan, await copyReferences(location.dir, id, references), orders);
|
|
688
|
+
record.currentVersion = versions[0];
|
|
689
|
+
await writeFile(assetFile(location.dir), `${JSON.stringify(record, null, 2)}\n`, { flag: "wx" });
|
|
690
|
+
return {
|
|
691
|
+
versions,
|
|
692
|
+
errors: orders.errors
|
|
693
|
+
};
|
|
709
694
|
} catch (error) {
|
|
710
695
|
await rm(location.dir, {
|
|
711
696
|
recursive: true,
|
|
@@ -713,8 +698,6 @@ async function createAsset(project, location, options, deps = defaultDeps$1) {
|
|
|
713
698
|
});
|
|
714
699
|
throw error;
|
|
715
700
|
}
|
|
716
|
-
await dispatch(location.dir, created.generation, created.versions, deps);
|
|
717
|
-
return created.versions;
|
|
718
701
|
}
|
|
719
702
|
/**
|
|
720
703
|
* 在已有素材里发起生成。from 指定时沿用该版本的模型与 input(重新生成),`-p` 给出的
|
|
@@ -745,27 +728,30 @@ async function editVersion(project, location, sourceVersion, options, deps = def
|
|
|
745
728
|
model: options.model ?? findGeneration(record, source.generation)?.model
|
|
746
729
|
}, sourceVersion), deps);
|
|
747
730
|
}
|
|
748
|
-
/**
|
|
731
|
+
/** 原地重试失败的版本:沿用它那次生成的输入下一个新任务,下单成功后版本才回到生成中。 */
|
|
749
732
|
async function retryVersion(location, number, deps = defaultDeps$1) {
|
|
750
733
|
const record = await readAsset(location.dir);
|
|
751
734
|
const version = findVersion(record, number);
|
|
752
735
|
const generation = findGeneration(record, version.generation);
|
|
753
736
|
if (version.state !== "failed") throw new CliError("只有失败的版本可以重试");
|
|
754
737
|
if (!generation) throw new CliError(`v${number} 不是生成产物,无法重试`);
|
|
738
|
+
const orders = await placeOrders(generation.model, absoluteTemplate(location.dir, generation.input), sourceFile(location, record, generation.sourceVersion), 1, deps);
|
|
755
739
|
await updateAsset(location.dir, (latest) => {
|
|
756
740
|
Object.assign(findVersion(latest, number), {
|
|
757
741
|
state: "generating",
|
|
758
742
|
resultIndex: 0,
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
kieRequest: null,
|
|
743
|
+
taskId: orders.taskIds[0],
|
|
744
|
+
kieRequest: orders.request,
|
|
762
745
|
kieRecord: null,
|
|
746
|
+
failReason: null,
|
|
763
747
|
credits: null,
|
|
764
748
|
updatedAt: now()
|
|
765
749
|
});
|
|
766
750
|
});
|
|
767
|
-
|
|
768
|
-
|
|
751
|
+
return {
|
|
752
|
+
versions: [number],
|
|
753
|
+
errors: []
|
|
754
|
+
};
|
|
769
755
|
}
|
|
770
756
|
/**
|
|
771
757
|
* 解析 `-p` 并与沿用的 input 合并,按模型 schema 校验(值类型就地转换)。
|
|
@@ -805,30 +791,19 @@ function isLocalFile(text) {
|
|
|
805
791
|
async function appendAndDispatch(project, location, record, plan, deps) {
|
|
806
792
|
if (plan.model.kind !== record.kind) throw new CliError(`模型 ${plan.model.model} 生成 ${plan.model.kind},与素材类型 ${record.kind} 不符`);
|
|
807
793
|
const references = await inspectReferences(project.root, plan.input);
|
|
808
|
-
const
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
/** 写入新素材的记录与参考输入副本;新素材的当前版本取版本号最靠前的版本壳。 */
|
|
816
|
-
async function writeNewAsset(project, location, plan) {
|
|
817
|
-
const references = await inspectReferences(project.root, plan.input);
|
|
818
|
-
const record = {
|
|
819
|
-
schema: 1,
|
|
820
|
-
id: randomUUID(),
|
|
821
|
-
kind: plan.model.kind,
|
|
822
|
-
currentVersion: 0,
|
|
823
|
-
createdAt: now(),
|
|
824
|
-
generations: [],
|
|
825
|
-
versions: []
|
|
794
|
+
const orders = await placeOrders(plan.model.model, plan.input, sourceFile(location, record, plan.sourceVersion), plan.tasks, deps);
|
|
795
|
+
return {
|
|
796
|
+
versions: await updateAsset(location.dir, async (latest) => {
|
|
797
|
+
const id = nextGenerationId(latest.generations);
|
|
798
|
+
return appendGeneration(latest, id, plan, await copyReferences(location.dir, id, references), orders);
|
|
799
|
+
}),
|
|
800
|
+
errors: orders.errors
|
|
826
801
|
};
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
return
|
|
802
|
+
}
|
|
803
|
+
/** 源版本产物的绝对路径;没有源版本时为 null。 */
|
|
804
|
+
function sourceFile(location, record, sourceVersion) {
|
|
805
|
+
const file = sourceVersion == null ? null : findVersion(record, sourceVersion).file;
|
|
806
|
+
return file ? path.join(location.dir, file) : null;
|
|
832
807
|
}
|
|
833
808
|
/** input 引用的本机文件:识别类型;位于库内某个版本产物时记下出处。 */
|
|
834
809
|
async function inspectReferences(root, input) {
|
|
@@ -877,21 +852,12 @@ async function copyReferences(dir, generationId, references) {
|
|
|
877
852
|
}
|
|
878
853
|
return records;
|
|
879
854
|
}
|
|
880
|
-
/**
|
|
881
|
-
function appendGeneration(record, id, plan, references) {
|
|
855
|
+
/** 追加一次生成与它的版本(每个 kie 接受的任务一个,就地修改 record);返回新版本号。 */
|
|
856
|
+
function appendGeneration(record, id, plan, references, orders) {
|
|
882
857
|
const timestamp = now();
|
|
883
858
|
const copies = new Map(references.map((reference) => [reference.source, reference.file]));
|
|
884
|
-
const generation = {
|
|
885
|
-
id,
|
|
886
|
-
model: plan.model.model,
|
|
887
|
-
input: mapStrings(plan.input, (text) => isLocalFile(text) && copies.has(text.slice(1)) ? `@${copies.get(text.slice(1))}` : text),
|
|
888
|
-
references: references.map(({ source: _source, ...reference }) => reference),
|
|
889
|
-
sourceVersion: plan.sourceVersion,
|
|
890
|
-
createdAt: timestamp
|
|
891
|
-
};
|
|
892
859
|
const base = nextVersionNumber(record.versions);
|
|
893
|
-
const versions =
|
|
894
|
-
for (let position = 0; position < plan.tasks; position++) {
|
|
860
|
+
const versions = orders.taskIds.map((taskId, position) => {
|
|
895
861
|
const number = base + position;
|
|
896
862
|
record.versions.push({
|
|
897
863
|
number,
|
|
@@ -901,8 +867,8 @@ function appendGeneration(record, id, plan, references) {
|
|
|
901
867
|
position,
|
|
902
868
|
resultIndex: 0,
|
|
903
869
|
sourceVersion: plan.sourceVersion,
|
|
904
|
-
taskId
|
|
905
|
-
kieRequest:
|
|
870
|
+
taskId,
|
|
871
|
+
kieRequest: orders.request,
|
|
906
872
|
kieRecord: null,
|
|
907
873
|
failReason: null,
|
|
908
874
|
credits: null,
|
|
@@ -916,60 +882,42 @@ function appendGeneration(record, id, plan, references) {
|
|
|
916
882
|
createdAt: timestamp,
|
|
917
883
|
updatedAt: timestamp
|
|
918
884
|
});
|
|
919
|
-
|
|
920
|
-
}
|
|
921
|
-
record.generations.push(
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
885
|
+
return number;
|
|
886
|
+
});
|
|
887
|
+
record.generations.push({
|
|
888
|
+
id,
|
|
889
|
+
model: plan.model.model,
|
|
890
|
+
input: mapStrings(plan.input, (text) => isLocalFile(text) && copies.has(text.slice(1)) ? `@${copies.get(text.slice(1))}` : text),
|
|
891
|
+
references: references.map(({ source: _source, ...reference }) => reference),
|
|
892
|
+
sourceVersion: plan.sourceVersion,
|
|
893
|
+
createdAt: timestamp
|
|
894
|
+
});
|
|
895
|
+
return versions;
|
|
926
896
|
}
|
|
927
897
|
/**
|
|
928
|
-
* 上传 input
|
|
929
|
-
*
|
|
898
|
+
* 上传 input 引用的文件与源版本换取临时地址(每次下单都重新上传,不保存),再下 count
|
|
899
|
+
* 个任务。全部下单失败时报错;部分失败时返回成功的任务与失败原因。
|
|
930
900
|
*/
|
|
931
|
-
async function
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
const files = new Map(filePaths(generation.input).map((file) => [`@${file}`, path.join(dir, file)]));
|
|
937
|
-
if (sourceFile && usesSource(generation.input)) files.set(SOURCE_TOKEN, path.join(dir, sourceFile));
|
|
938
|
-
const urls = new Map(await Promise.all([...files].map(async ([token, file]) => [token, await deps.upload(file)])));
|
|
939
|
-
task = {
|
|
940
|
-
model: generation.model,
|
|
941
|
-
input: resolveTokens(generation.input, (token) => urls.get(token))
|
|
942
|
-
};
|
|
943
|
-
} catch (error) {
|
|
944
|
-
await failVersions(dir, versions, error.message);
|
|
945
|
-
return;
|
|
901
|
+
async function placeOrders(model, input, source, count, deps) {
|
|
902
|
+
const files = new Map(filePaths(input).map((file) => [`@${file}`, file]));
|
|
903
|
+
if (usesSource(input)) {
|
|
904
|
+
if (!source) throw new CliError("源版本没有产物文件");
|
|
905
|
+
files.set(SOURCE_TOKEN, source);
|
|
946
906
|
}
|
|
947
|
-
await Promise.all(
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
}
|
|
962
|
-
});
|
|
963
|
-
}));
|
|
964
|
-
}
|
|
965
|
-
async function failVersions(dir, numbers, reason, task = null) {
|
|
966
|
-
await updateAsset(dir, (record) => {
|
|
967
|
-
for (const number of numbers) {
|
|
968
|
-
const version = record.versions.find((item) => item.number === number);
|
|
969
|
-
if (version) version.kieRequest = task;
|
|
970
|
-
failVersion(record, number, reason);
|
|
971
|
-
}
|
|
972
|
-
});
|
|
907
|
+
const urls = new Map(await Promise.all([...files].map(async ([token, file]) => [token, await deps.upload(file)])));
|
|
908
|
+
const request = {
|
|
909
|
+
model,
|
|
910
|
+
input: resolveTokens(input, (token) => urls.get(token))
|
|
911
|
+
};
|
|
912
|
+
const results = await Promise.allSettled(Array.from({ length: count }, () => deps.createTask(request)));
|
|
913
|
+
const taskIds = results.flatMap((result) => result.status === "fulfilled" ? [result.value] : []);
|
|
914
|
+
const errors = [...new Set(results.flatMap((result) => result.status === "rejected" ? [result.reason.message] : []))];
|
|
915
|
+
if (taskIds.length === 0) throw new CliError(errors[0] ?? "下单失败");
|
|
916
|
+
return {
|
|
917
|
+
request,
|
|
918
|
+
taskIds,
|
|
919
|
+
errors
|
|
920
|
+
};
|
|
973
921
|
}
|
|
974
922
|
// src/kie/record.ts
|
|
975
923
|
/**
|
|
@@ -1022,6 +970,38 @@ function parseResults(resultJson) {
|
|
|
1022
970
|
}));
|
|
1023
971
|
return [];
|
|
1024
972
|
}
|
|
973
|
+
// src/generation/state.ts
|
|
974
|
+
/** 一个 kie 任务对应的版本里,积分只记在结果序号 0 上,避免重复计量。 */
|
|
975
|
+
function creditsFor(version, credits) {
|
|
976
|
+
return (version.resultIndex ?? 0) === 0 ? credits : null;
|
|
977
|
+
}
|
|
978
|
+
function failVersion(record, number, reason, task = {
|
|
979
|
+
credits: null,
|
|
980
|
+
kieRecord: null
|
|
981
|
+
}) {
|
|
982
|
+
const version = record.versions.find((item) => item.number === number);
|
|
983
|
+
if (!version || version.state !== "generating") return;
|
|
984
|
+
version.state = "failed";
|
|
985
|
+
version.failReason = reason;
|
|
986
|
+
version.credits = creditsFor(version, task.credits);
|
|
987
|
+
version.kieRecord = task.kieRecord;
|
|
988
|
+
version.updatedAt = now();
|
|
989
|
+
if (record.currentVersion === number) {
|
|
990
|
+
const next = nextCurrentAfterFailure(record.versions, version);
|
|
991
|
+
if (next != null) record.currentVersion = next;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
function fulfillVersion(record, number, product, task) {
|
|
995
|
+
const version = record.versions.find((item) => item.number === number);
|
|
996
|
+
if (!version || version.state !== "generating") return;
|
|
997
|
+
Object.assign(version, product, {
|
|
998
|
+
state: "ready",
|
|
999
|
+
failReason: null,
|
|
1000
|
+
credits: creditsFor(version, task.credits),
|
|
1001
|
+
kieRecord: task.kieRecord,
|
|
1002
|
+
updatedAt: now()
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1025
1005
|
// src/generation/sync.ts
|
|
1026
1006
|
/**
|
|
1027
1007
|
* 向 kie 求证并收取产物:按任务分组,一次 recordInfo 推进同一任务的全部版本。
|
|
@@ -1032,8 +1012,6 @@ const defaultDeps = {
|
|
|
1032
1012
|
getRecord: getKieRecord,
|
|
1033
1013
|
download: downloadFile
|
|
1034
1014
|
};
|
|
1035
|
-
/** 提交到一半中断(有记录、没任务编号)的版本,超过这个时长按失败处理,可重试。 */
|
|
1036
|
-
const STUCK_DISPATCH_MS = 6e5;
|
|
1037
1015
|
const POLL_INTERVAL_MS = 5e3;
|
|
1038
1016
|
/** 同步一批素材里所有生成中的版本;返回过程中遇到的问题(不中断其余任务)。 */
|
|
1039
1017
|
async function syncAssets(assetDirs, deps = defaultDeps) {
|
|
@@ -1065,12 +1043,7 @@ async function allSettled(targets) {
|
|
|
1065
1043
|
}
|
|
1066
1044
|
async function syncAsset(dir, deps) {
|
|
1067
1045
|
const record = await readAsset(dir);
|
|
1068
|
-
const
|
|
1069
|
-
const stuck = generating.filter((version) => !version.taskId && Date.now() - Date.parse(version.updatedAt) > STUCK_DISPATCH_MS);
|
|
1070
|
-
if (stuck.length > 0) await updateAsset(dir, (latest) => {
|
|
1071
|
-
for (const version of stuck) if (!latest.versions.find((item) => item.number === version.number)?.taskId) failVersion(latest, version.number, "提交中断,可重试");
|
|
1072
|
-
});
|
|
1073
|
-
const taskIds = [...new Set(generating.map((version) => version.taskId).filter((taskId) => taskId != null))];
|
|
1046
|
+
const taskIds = [...new Set(record.versions.filter((version) => version.state === "generating").map((version) => version.taskId).filter((taskId) => taskId != null))];
|
|
1074
1047
|
const warnings = [];
|
|
1075
1048
|
for (const taskId of taskIds) try {
|
|
1076
1049
|
await syncTask(dir, record, taskId, deps);
|
|
@@ -1421,8 +1394,9 @@ function toGenerateOptions(args) {
|
|
|
1421
1394
|
count: args.count
|
|
1422
1395
|
};
|
|
1423
1396
|
}
|
|
1424
|
-
/**
|
|
1425
|
-
async function finish(project, location, versions, options) {
|
|
1397
|
+
/** 部分下单失败的原因作为警告输出;按需等待新版本完成,再输出这些版本的状态。 */
|
|
1398
|
+
async function finish(project, location, { versions, errors }, options) {
|
|
1399
|
+
errors.forEach((error) => warn(`部分任务下单失败:${error}`));
|
|
1426
1400
|
if (options.wait) {
|
|
1427
1401
|
const { warnings } = await waitForVersions([{
|
|
1428
1402
|
dir: location.dir,
|