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.
Files changed (2) hide show
  1. package/dist/cli.mjs +117 -143
  2. 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.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
- * 发起生成:先按模型的 schema 完成校验,再落记录(版本壳为生成中),最后上传文件、
694
- * 向 kie 下单并回填任务编号。下单失败的版本直接标失败,可重试。
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
- created = await writeNewAsset(project, location, plan);
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
- failReason: null,
760
- taskId: null,
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
- await dispatch(location.dir, generation, [number], deps);
768
- return [number];
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 { generation, versions } = await updateAsset(location.dir, async (latest) => {
809
- const id = nextGenerationId(latest.generations);
810
- return appendGeneration(latest, id, plan, await copyReferences(location.dir, id, references));
811
- });
812
- await dispatch(location.dir, generation, versions, deps);
813
- return versions;
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
- const id = nextGenerationId(record.generations);
828
- const appended = appendGeneration(record, id, plan, await copyReferences(location.dir, id, references));
829
- record.currentVersion = appended.versions[0];
830
- await writeFile(assetFile(location.dir), `${JSON.stringify(record, null, 2)}\n`, { flag: "wx" });
831
- return appended;
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
- /** 追加一次生成与它的版本壳(每个任务一个,就地修改 record);返回新版本号。 */
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: null,
905
- kieRequest: null,
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
- versions.push(number);
920
- }
921
- record.generations.push(generation);
922
- return {
923
- generation,
924
- versions
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 dispatch(dir, generation, versions, deps) {
932
- let task;
933
- try {
934
- const record = await readAsset(dir);
935
- const sourceFile = generation.sourceVersion != null ? findVersion(record, generation.sourceVersion).file : null;
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(versions.map(async (number) => {
948
- let taskId;
949
- try {
950
- taskId = await deps.createTask(task);
951
- } catch (error) {
952
- await failVersions(dir, [number], error.message, task);
953
- return;
954
- }
955
- await updateAsset(dir, (record) => {
956
- const version = record.versions.find((item) => item.number === number);
957
- if (version?.state === "generating") {
958
- version.taskId = taskId;
959
- version.kieRequest = task;
960
- version.updatedAt = now();
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 generating = record.versions.filter((version) => version.state === "generating");
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,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "genlab",
3
3
  "type": "module",
4
- "version": "0.1.1",
4
+ "version": "0.1.2",
5
5
  "description": "给本地 agent 用的 AI 媒体生成命令行工具(kie.ai),产物与完整生成记录保存在本机",
6
6
  "author": "WBBB",
7
7
  "license": "MIT",