@gobing-ai/knowledge-kit 0.0.29 → 0.0.30
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/.claude-plugin/marketplace.json +1 -1
- package/dist/index.js +114 -38
- package/package.json +1 -1
- package/plugins/generations/daily-article-gen/dist/index.js +24 -7
- package/plugins/generations/daily-article-gen/src/index.ts +36 -12
- package/plugins/generations/dailynews-gen/dist/index.js +19 -2
- package/plugins/generations/dailynews-gen/src/index.ts +7 -0
- package/plugins/generations/dailynews-gen/src/script-plan.ts +21 -3
- package/plugins/generations/episode-plan-gen/dist/index.js +55 -4
- package/plugins/generations/episode-plan-gen/src/index.ts +80 -2
- package/plugins/generations/image-gen/dist/index.js +122 -20
- package/plugins/generations/image-gen/src/bytes.ts +106 -18
- package/plugins/generations/image-gen/src/index.ts +46 -6
- package/plugins/generations/image-gen/src/job.ts +6 -0
- package/plugins/generations/image-gen/src/providers/minimax.ts +12 -4
- package/plugins/generations/image-gen/src/providers/types.ts +12 -1
- package/plugins/kk/plugin.json +1 -1
- package/plugins/kk/scripts/daily-voice-style.zh.md +5 -5
- package/plugins/kk/scripts/itc-stages.ts +21 -3
- package/plugins/kk/scripts/kk-workflow-stages.ts +199 -7
- package/plugins/kk/scripts/topic-boundary.json +3 -12
- package/plugins/kk/skills/content-judge/SKILL.md +30 -0
- package/plugins/kk/skills/content-judge/references/workflow-integration.md +9 -2
- package/plugins/kk/skills/publish/SKILL.md +1 -1
- package/plugins/kk/workflows/judge-gated-publish-example.yaml +18 -10
- package/plugins/kk/workflows/kk-daily-ai-voice.yaml +39 -14
- package/plugins/kk/workflows/kk-itc.yaml +21 -8
- package/plugins/publishings/xhs-pub/dist/index.js +1 -1
- package/plugins/publishings/xhs-pub/src/automation-guard.ts +2 -2
- package/plugins/publishings/xhs-pub/src/index.ts +1 -1
- package/plugins/publishings/xhs-pub/src/profile-check.ts +2 -2
- package/plugins/sp/lib/env.ts +0 -46
- package/plugins/sp/lib/inline-run.generated.mjs +0 -1489
- package/plugins/sp/scripts/batch-preflight.mjs +0 -346
- package/plugins/sp/scripts/batch-preflight.ts +0 -459
- package/plugins/sp/scripts/daily-summary/daily-summary.mjs +0 -615
- package/plugins/sp/scripts/daily-summary/daily-summary.ts +0 -846
- package/plugins/sp/scripts/daily-summary/logger.ts +0 -28
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +0 -223
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +0 -367
- package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +0 -132
- package/plugins/sp/scripts/dogfood-testing/validate-report.ts +0 -169
- package/plugins/sp/scripts/feature-dev-precheck.mjs +0 -171
- package/plugins/sp/scripts/feature-dev-precheck.ts +0 -238
- package/plugins/sp/scripts/feature-sync-bounded.mjs +0 -285
- package/plugins/sp/scripts/feature-sync-bounded.ts +0 -478
- package/plugins/sp/scripts/feature-verification-steps.ts +0 -275
- package/plugins/sp/scripts/history-anatomy-cache.mjs +0 -902
- package/plugins/sp/scripts/history-anatomy-cache.ts +0 -1028
- package/plugins/sp/scripts/idea-handoff.mjs +0 -22
- package/plugins/sp/scripts/idea-handoff.ts +0 -44
- package/plugins/sp/scripts/inline-pipeline-parity-check.ts +0 -185
- package/plugins/sp/scripts/inline-run-setup.ts +0 -722
- package/plugins/sp/scripts/pr-reviewing.mjs +0 -769
- package/plugins/sp/scripts/pr-reviewing.ts +0 -925
- package/plugins/sp/scripts/quality-gate.mjs +0 -179
- package/plugins/sp/scripts/quality-gate.ts +0 -217
- package/plugins/sp/scripts/script-contract-check.ts +0 -319
- package/plugins/sp/scripts/stage-registry-adapter.ts +0 -1533
- package/plugins/sp/scripts/surface-drift-inventory.ts +0 -929
- package/plugins/sp/scripts/task-evidence-precheck.ts +0 -181
- package/plugins/sp/scripts/task-size-precheck.ts +0 -175
- package/plugins/sp/scripts/transition-shim-check.ts +0 -238
- package/plugins/sp/scripts/validate-commands.ts +0 -689
- package/plugins/sp/scripts/validate-flag-contracts.ts +0 -878
- package/plugins/sp/scripts/verify-answer-lint.ts +0 -530
- package/plugins/sp/scripts/workflow-step-profile.mjs +0 -316
- package/plugins/sp/scripts/workflow-step-profile.ts +0 -456
- package/plugins/sp/scripts/wrapup-drift-probe.mjs +0 -190
- package/plugins/sp/scripts/wrapup-drift-probe.ts +0 -258
- package/plugins/sp/scripts/wrapup-steps.mjs +0 -444
- package/plugins/sp/scripts/wrapup-steps.ts +0 -552
|
@@ -635,6 +635,8 @@ export interface FilterConfig {
|
|
|
635
635
|
minImportance: number;
|
|
636
636
|
minUrgency: number;
|
|
637
637
|
minImpact: number;
|
|
638
|
+
/** Episode floor: when fewer docs survive, importance-only rejects backfill up to this count. */
|
|
639
|
+
minEpisodeItems: number;
|
|
638
640
|
}
|
|
639
641
|
|
|
640
642
|
/** A doc the filter dropped, with every failed check (`category:not-in-allowlist`, `quality:2<3`, …). */
|
|
@@ -659,6 +661,13 @@ export const DEFAULT_FILTER_CONFIG: FilterConfig = {
|
|
|
659
661
|
minImportance: 3,
|
|
660
662
|
minUrgency: 2,
|
|
661
663
|
minImpact: 1,
|
|
664
|
+
// 20261002 dogfood: a weak, policy-heavy day collapsed the episode to 2 spoken items
|
|
665
|
+
// (~5 min audio) because importance (source-tier base) is the first dimension a tail-source
|
|
666
|
+
// day fails. Backfill admits importance-only rejects — quality/urgency/impact/category
|
|
667
|
+
// minimums stay hard — so a thin news day deepens instead of shipping a stub. Floor is 8,
|
|
668
|
+
// not 6: the prompt-layer 选题边界 (script-write) drops policy items AFTER this gate (today
|
|
669
|
+
// 2 of 5), so the floor compensates upstream to land ~4-6 spoken items on a thin day.
|
|
670
|
+
minEpisodeItems: 8,
|
|
662
671
|
};
|
|
663
672
|
|
|
664
673
|
/** One QC_MIN_* threshold: an integer 0–5 or absent (→ fallback); anything else fails loud — never clamped. */
|
|
@@ -671,6 +680,17 @@ function parseFilterThreshold(raw: string | undefined, name: string, fallback: n
|
|
|
671
680
|
return parsed;
|
|
672
681
|
}
|
|
673
682
|
|
|
683
|
+
/** QC_MIN_EPISODE_ITEMS: a positive integer count or absent (→ fallback); anything else fails loud. */
|
|
684
|
+
function parseFloorCount(raw: string | undefined, name: string, fallback: number): number {
|
|
685
|
+
if (raw === undefined || raw.trim() === '') return fallback;
|
|
686
|
+
const value = raw.trim();
|
|
687
|
+
if (!/^\d+$/.test(value)) throw new Error(`${name}='${raw}' is not a positive integer (fail loud, no clamping)`);
|
|
688
|
+
const parsed = Number.parseInt(value, 10);
|
|
689
|
+
if (parsed < 1)
|
|
690
|
+
throw new Error(`${name}=${parsed} must be >= 1 (a 0 floor disables the gate — drop the env instead)`);
|
|
691
|
+
return parsed;
|
|
692
|
+
}
|
|
693
|
+
|
|
674
694
|
/** QC_CATEGORIES: a comma list of MECE categories; unknown entries fail loud (no silent drops). */
|
|
675
695
|
function parseFilterCategories(raw: string | undefined): Category[] {
|
|
676
696
|
if (raw === undefined || raw.trim() === '') return [...DEFAULT_FILTER_CONFIG.categories];
|
|
@@ -697,6 +717,11 @@ export function filterConfigFromEnv(env: Record<string, string | undefined> = pr
|
|
|
697
717
|
),
|
|
698
718
|
minUrgency: parseFilterThreshold(env.QC_MIN_URGENCY, 'QC_MIN_URGENCY', DEFAULT_FILTER_CONFIG.minUrgency),
|
|
699
719
|
minImpact: parseFilterThreshold(env.QC_MIN_IMPACT, 'QC_MIN_IMPACT', DEFAULT_FILTER_CONFIG.minImpact),
|
|
720
|
+
minEpisodeItems: parseFloorCount(
|
|
721
|
+
env.QC_MIN_EPISODE_ITEMS,
|
|
722
|
+
'QC_MIN_EPISODE_ITEMS',
|
|
723
|
+
DEFAULT_FILTER_CONFIG.minEpisodeItems,
|
|
724
|
+
),
|
|
700
725
|
};
|
|
701
726
|
}
|
|
702
727
|
|
|
@@ -749,6 +774,53 @@ export function filterAnnotated(docs: Doc[], config: FilterConfig): { kept: Doc[
|
|
|
749
774
|
return { kept, rejected };
|
|
750
775
|
}
|
|
751
776
|
|
|
777
|
+
function importanceOf(doc: Doc): number {
|
|
778
|
+
const scores = asRecord(asRecord(doc.metadata).scores);
|
|
779
|
+
return typeof scores.importance === 'number' ? scores.importance : 0;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function planScoreOf(doc: Doc): number {
|
|
783
|
+
const value = asRecord(doc.metadata).planScore;
|
|
784
|
+
return typeof value === 'number' ? value : 0;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* Episode floor (20261002): when the strict filter leaves fewer than `minEpisodeItems` docs,
|
|
789
|
+
* backfill from rejects that failed ONLY the importance minimum — importance is the source-tier
|
|
790
|
+
* base (tail sources carry 2), so on a thin day it is the softest dimension, while quality/
|
|
791
|
+
* urgency/impact/category stay hard gates. Admission order: importance desc, planScore desc,
|
|
792
|
+
* plan order. Admitted docs are stamped `metadata.backfilled: true` and leave the rejected
|
|
793
|
+
* audit (they are spoken, not dropped). Pure: input docs are never mutated.
|
|
794
|
+
*/
|
|
795
|
+
export function backfillToFloor(
|
|
796
|
+
kept: Doc[],
|
|
797
|
+
rejected: RejectedDoc[],
|
|
798
|
+
config: FilterConfig,
|
|
799
|
+
): { kept: Doc[]; rejected: RejectedDoc[]; admitted: Doc[] } {
|
|
800
|
+
if (kept.length >= config.minEpisodeItems) return { kept, rejected, admitted: [] };
|
|
801
|
+
const eligible = rejected
|
|
802
|
+
.map((entry, index) => ({ entry, index }))
|
|
803
|
+
.filter(({ entry }) => entry.reasons.length > 0 && entry.reasons.every((r) => r.startsWith('importance:')));
|
|
804
|
+
eligible.sort((a, b) => {
|
|
805
|
+
const byImportance = importanceOf(b.entry.doc) - importanceOf(a.entry.doc);
|
|
806
|
+
if (byImportance !== 0) return byImportance;
|
|
807
|
+
const byPlanScore = planScoreOf(b.entry.doc) - planScoreOf(a.entry.doc);
|
|
808
|
+
if (byPlanScore !== 0) return byPlanScore;
|
|
809
|
+
return a.index - b.index;
|
|
810
|
+
});
|
|
811
|
+
const taken = eligible.slice(0, config.minEpisodeItems - kept.length);
|
|
812
|
+
const takenIds = new Set(taken.map(({ entry }) => entry.doc.id));
|
|
813
|
+
const admitted = taken.map(({ entry }) => ({
|
|
814
|
+
...entry.doc,
|
|
815
|
+
metadata: { ...asRecord(entry.doc.metadata), backfilled: true },
|
|
816
|
+
}));
|
|
817
|
+
return {
|
|
818
|
+
kept: [...kept, ...admitted],
|
|
819
|
+
rejected: rejected.filter((entry) => !takenIds.has(entry.doc.id)),
|
|
820
|
+
admitted,
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
|
|
752
824
|
/** Sibling rejected-audit path: `<out>.json` → `<out>.rejected.json` (candidates.json → candidates.rejected.json). */
|
|
753
825
|
export function rejectedOutPath(outputPath: string): string {
|
|
754
826
|
return outputPath.endsWith('.json')
|
|
@@ -792,9 +864,15 @@ async function processFilterIO(inputPath: string, outputPath: string): Promise<C
|
|
|
792
864
|
const config = filterConfigFromEnv();
|
|
793
865
|
const docs = DocListSchema.parse(await readJsonFile(inputPath));
|
|
794
866
|
const { kept, rejected } = filterAnnotated(docs, config);
|
|
795
|
-
const
|
|
867
|
+
const filled = backfillToFloor(kept, rejected, config);
|
|
868
|
+
if (filled.admitted.length > 0) {
|
|
869
|
+
echoError(
|
|
870
|
+
`episode floor backfill: admitted ${filled.admitted.length} importance-only reject(s) to reach ${config.minEpisodeItems} items (${filled.admitted.map((d) => d.id).join(', ')})`,
|
|
871
|
+
);
|
|
872
|
+
}
|
|
873
|
+
const content = planContent(filled.kept, resolvePlanDate());
|
|
796
874
|
await atomicWrite(outputPath, content);
|
|
797
|
-
await atomicWrite(rejectedOutPath(outputPath), { rejected, config, total: docs.length });
|
|
875
|
+
await atomicWrite(rejectedOutPath(outputPath), { rejected: filled.rejected, config, total: docs.length });
|
|
798
876
|
return content;
|
|
799
877
|
}
|
|
800
878
|
|
|
@@ -21818,23 +21818,101 @@ async function readBytes(path2) {
|
|
|
21818
21818
|
return new Uint8Array(await readFile(path2));
|
|
21819
21819
|
}
|
|
21820
21820
|
function imageDimensions(bytes) {
|
|
21821
|
+
if (isPng(bytes))
|
|
21822
|
+
return pngDimensions(bytes);
|
|
21823
|
+
if (isJpeg(bytes))
|
|
21824
|
+
return jpegDimensions(bytes);
|
|
21825
|
+
if (isWebp(bytes))
|
|
21826
|
+
return webpDimensions(bytes);
|
|
21827
|
+
return null;
|
|
21828
|
+
}
|
|
21829
|
+
function isPng(b) {
|
|
21821
21830
|
const PNG_SIG = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
21822
|
-
if (
|
|
21823
|
-
|
|
21824
|
-
}
|
|
21831
|
+
if (b.byteLength < 24)
|
|
21832
|
+
return false;
|
|
21825
21833
|
for (let i = 0;i < 8; i++) {
|
|
21826
|
-
if (
|
|
21827
|
-
|
|
21828
|
-
}
|
|
21834
|
+
if (b[i] !== PNG_SIG[i])
|
|
21835
|
+
return false;
|
|
21829
21836
|
}
|
|
21830
|
-
|
|
21837
|
+
return true;
|
|
21838
|
+
}
|
|
21839
|
+
function pngDimensions(b) {
|
|
21840
|
+
const view = new DataView(b.buffer, b.byteOffset, b.byteLength);
|
|
21831
21841
|
const ihdrLength = view.getUint32(8, false);
|
|
21832
|
-
if (ihdrLength !== 13 ||
|
|
21833
|
-
|
|
21842
|
+
if (ihdrLength !== 13 || view.getUint32(12, false) !== 1229472850) {
|
|
21843
|
+
return null;
|
|
21834
21844
|
}
|
|
21835
|
-
|
|
21836
|
-
|
|
21837
|
-
|
|
21845
|
+
return { width: view.getUint32(16, false), height: view.getUint32(20, false) };
|
|
21846
|
+
}
|
|
21847
|
+
function isJpeg(b) {
|
|
21848
|
+
const view = new DataView(b.buffer, b.byteOffset, b.byteLength);
|
|
21849
|
+
return b.byteLength >= 4 && view.getUint16(0, false) === 65496 && view.getUint8(2) === 255;
|
|
21850
|
+
}
|
|
21851
|
+
function jpegDimensions(b) {
|
|
21852
|
+
const view = new DataView(b.buffer, b.byteOffset, b.byteLength);
|
|
21853
|
+
let at = 2;
|
|
21854
|
+
while (at + 4 <= b.byteLength) {
|
|
21855
|
+
if (view.getUint8(at) !== 255)
|
|
21856
|
+
return null;
|
|
21857
|
+
const marker = view.getUint8(at + 1);
|
|
21858
|
+
at += 2;
|
|
21859
|
+
if (marker === 1 || marker >= 208 && marker <= 215)
|
|
21860
|
+
continue;
|
|
21861
|
+
if (at + 2 > b.byteLength)
|
|
21862
|
+
return null;
|
|
21863
|
+
const length = view.getUint16(at, false);
|
|
21864
|
+
if (length < 2)
|
|
21865
|
+
return null;
|
|
21866
|
+
const isSof = marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204;
|
|
21867
|
+
if (isSof) {
|
|
21868
|
+
if (at + 7 > b.byteLength)
|
|
21869
|
+
return null;
|
|
21870
|
+
return { height: view.getUint16(at + 3, false), width: view.getUint16(at + 5, false) };
|
|
21871
|
+
}
|
|
21872
|
+
at += length;
|
|
21873
|
+
}
|
|
21874
|
+
return null;
|
|
21875
|
+
}
|
|
21876
|
+
function isWebp(b) {
|
|
21877
|
+
if (b.byteLength < 20)
|
|
21878
|
+
return false;
|
|
21879
|
+
const view = new DataView(b.buffer, b.byteOffset, b.byteLength);
|
|
21880
|
+
return view.getUint32(0, true) === 1179011410 && view.getUint32(8, true) === 1346520407;
|
|
21881
|
+
}
|
|
21882
|
+
function webpDimensions(b) {
|
|
21883
|
+
const view = new DataView(b.buffer, b.byteOffset, b.byteLength);
|
|
21884
|
+
let at = 12;
|
|
21885
|
+
while (at + 8 <= b.byteLength) {
|
|
21886
|
+
const id = String.fromCharCode(view.getUint8(at), view.getUint8(at + 1), view.getUint8(at + 2), view.getUint8(at + 3));
|
|
21887
|
+
const size = view.getUint32(at + 4, true);
|
|
21888
|
+
const body = at + 8;
|
|
21889
|
+
if (id === "VP8 ") {
|
|
21890
|
+
if (size < 10 || body + 10 > b.byteLength)
|
|
21891
|
+
return null;
|
|
21892
|
+
if (view.getUint8(body + 3) !== 157 || view.getUint8(body + 4) !== 1 || view.getUint8(body + 5) !== 42) {
|
|
21893
|
+
return null;
|
|
21894
|
+
}
|
|
21895
|
+
return {
|
|
21896
|
+
width: view.getUint16(body + 6, true) & 16383,
|
|
21897
|
+
height: view.getUint16(body + 8, true) & 16383
|
|
21898
|
+
};
|
|
21899
|
+
}
|
|
21900
|
+
if (id === "VP8L") {
|
|
21901
|
+
if (size < 5 || body + 5 > b.byteLength || view.getUint8(body) !== 47)
|
|
21902
|
+
return null;
|
|
21903
|
+
const bits = view.getUint32(body + 1, true);
|
|
21904
|
+
return { width: (bits & 16383) + 1, height: (bits >>> 14 & 16383) + 1 };
|
|
21905
|
+
}
|
|
21906
|
+
if (id === "VP8X") {
|
|
21907
|
+
if (size < 10 || body + 10 > b.byteLength)
|
|
21908
|
+
return null;
|
|
21909
|
+
const width = view.getUint8(body + 4) | view.getUint8(body + 5) << 8 | view.getUint8(body + 6) << 16;
|
|
21910
|
+
const height = view.getUint8(body + 7) | view.getUint8(body + 8) << 8 | view.getUint8(body + 9) << 16;
|
|
21911
|
+
return { width: width + 1, height: height + 1 };
|
|
21912
|
+
}
|
|
21913
|
+
at = body + size + size % 2;
|
|
21914
|
+
}
|
|
21915
|
+
return null;
|
|
21838
21916
|
}
|
|
21839
21917
|
var init_bytes = () => {};
|
|
21840
21918
|
|
|
@@ -24240,11 +24318,14 @@ import { Buffer as Buffer7 } from "buffer";
|
|
|
24240
24318
|
var DEFAULT_MODEL9 = "image-01";
|
|
24241
24319
|
var MAX_REFERENCE_IMAGE_BYTES = 10 * 1024 * 1024;
|
|
24242
24320
|
var SUPPORTED_ASPECT_RATIOS = new Set(["1:1", "16:9", "4:3", "3:2", "2:3", "3:4", "9:16", "21:9"]);
|
|
24321
|
+
function isAcceptablePixelSize(width, height) {
|
|
24322
|
+
return width >= 512 && width <= 2048 && height >= 512 && height <= 2048 && width % 8 === 0 && height % 8 === 0;
|
|
24323
|
+
}
|
|
24243
24324
|
function validatePixelSize(width, height) {
|
|
24244
|
-
if (width
|
|
24245
|
-
|
|
24246
|
-
|
|
24247
|
-
|
|
24325
|
+
if (!isAcceptablePixelSize(width, height)) {
|
|
24326
|
+
if (width < 512 || width > 2048 || height < 512 || height > 2048) {
|
|
24327
|
+
throw new Error("MiniMax custom size must keep width and height between 512 and 2048.");
|
|
24328
|
+
}
|
|
24248
24329
|
throw new Error("MiniMax custom size requires width and height divisible by 8.");
|
|
24249
24330
|
}
|
|
24250
24331
|
}
|
|
@@ -24326,6 +24407,7 @@ var createMinimax = (options) => {
|
|
|
24326
24407
|
defaultModel: () => DEFAULT_MODEL9,
|
|
24327
24408
|
isAvailable: () => readEnv("MINIMAX_API_KEY") !== "",
|
|
24328
24409
|
supportsReferenceImages: () => true,
|
|
24410
|
+
exactSize: (model, width, height) => model === "image-01" && isAcceptablePixelSize(width, height),
|
|
24329
24411
|
validate(req) {
|
|
24330
24412
|
requestBody(req.prompt, req.model, req);
|
|
24331
24413
|
},
|
|
@@ -25489,15 +25571,17 @@ async function processGeneratorIO(inputPath, outputPath, options = {}) {
|
|
|
25489
25571
|
}
|
|
25490
25572
|
const provider = at === 0 ? primary : createCandidate(name);
|
|
25491
25573
|
const model = at === 0 ? spec.model ?? provider.defaultModel() : provider.defaultModel();
|
|
25574
|
+
const exact = format.width !== undefined && format.height !== undefined && provider.exactSize?.(model, format.width, format.height) === true;
|
|
25492
25575
|
const request = {
|
|
25493
25576
|
prompt,
|
|
25494
25577
|
model,
|
|
25495
|
-
aspectRatio: format.aspectRatio,
|
|
25578
|
+
...exact ? { width: format.width, height: format.height } : { aspectRatio: format.aspectRatio },
|
|
25496
25579
|
...spec.referenceImages !== undefined && spec.referenceImages.length > 0 ? { referenceImages: spec.referenceImages } : {}
|
|
25497
25580
|
};
|
|
25498
25581
|
try {
|
|
25499
25582
|
provider.validate?.(request);
|
|
25500
|
-
|
|
25583
|
+
const result = await provider.generate(request);
|
|
25584
|
+
return { result, sizeRequest: exact ? "exact" : "aspect" };
|
|
25501
25585
|
} catch (err) {
|
|
25502
25586
|
if (spec.fallback === undefined) {
|
|
25503
25587
|
throw err;
|
|
@@ -25520,7 +25604,15 @@ async function processGeneratorIO(inputPath, outputPath, options = {}) {
|
|
|
25520
25604
|
};
|
|
25521
25605
|
let body = doc2?.body ?? "";
|
|
25522
25606
|
for (const [index, prompt] of prompts.entries()) {
|
|
25523
|
-
const result = await generateWithFallback(index + 1, prompt);
|
|
25607
|
+
const { result, sizeRequest } = await generateWithFallback(index + 1, prompt);
|
|
25608
|
+
const delivered = imageDimensions(result.bytes);
|
|
25609
|
+
if (format.width !== undefined && format.height !== undefined) {
|
|
25610
|
+
if (delivered === null) {
|
|
25611
|
+
echoError(`WARN image-gen: ${result.provider}/${result.model} delivered unparseable image bytes; dimensions omitted`);
|
|
25612
|
+
} else if (delivered.width !== format.width || delivered.height !== format.height) {
|
|
25613
|
+
echoError(`WARN image-gen: ${result.provider}/${result.model} delivered ${delivered.width}x${delivered.height}; format '${format.name}' declares ${format.width}x${format.height}`);
|
|
25614
|
+
}
|
|
25615
|
+
}
|
|
25524
25616
|
let anchor;
|
|
25525
25617
|
if (spec.job === "illustration") {
|
|
25526
25618
|
anchor = positions?.[index];
|
|
@@ -25564,6 +25656,9 @@ async function processGeneratorIO(inputPath, outputPath, options = {}) {
|
|
|
25564
25656
|
await writeBytes(cardPath, cardBytes);
|
|
25565
25657
|
written.push(cardPath);
|
|
25566
25658
|
const dims = imageDimensions(cardBytes);
|
|
25659
|
+
if (dims === null) {
|
|
25660
|
+
throw new Error(`compose: composed card dimensions are unparseable; format '${format.name}' requires ${format.width}x${format.height}`);
|
|
25661
|
+
}
|
|
25567
25662
|
if (dims.width !== format.width || dims.height !== format.height) {
|
|
25568
25663
|
throw new Error(`compose: card dimensions ${dims.width}x${dims.height} do not match format canvas ${format.width}x${format.height}`);
|
|
25569
25664
|
}
|
|
@@ -25571,13 +25666,19 @@ async function processGeneratorIO(inputPath, outputPath, options = {}) {
|
|
|
25571
25666
|
artifactBytes = cardBytes.byteLength;
|
|
25572
25667
|
artifactWidth = dims.width;
|
|
25573
25668
|
artifactHeight = dims.height;
|
|
25574
|
-
illustrationMeta = {
|
|
25669
|
+
illustrationMeta = {
|
|
25670
|
+
path: illustrationArtifactPath,
|
|
25671
|
+
bytes: result.bytes.byteLength,
|
|
25672
|
+
...delivered !== null ? { width: delivered.width, height: delivered.height } : {}
|
|
25673
|
+
};
|
|
25575
25674
|
} else {
|
|
25576
25675
|
const path2 = artifactPath(outputPath, stem, format.name, index + 1, result.extension);
|
|
25577
25676
|
await writeBytes(path2, result.bytes);
|
|
25578
25677
|
written.push(path2);
|
|
25579
25678
|
artifactPathValue = path2;
|
|
25580
25679
|
artifactBytes = result.bytes.byteLength;
|
|
25680
|
+
artifactWidth = delivered?.width;
|
|
25681
|
+
artifactHeight = delivered?.height;
|
|
25581
25682
|
}
|
|
25582
25683
|
const artifact = {
|
|
25583
25684
|
path: artifactPathValue,
|
|
@@ -25587,6 +25688,7 @@ async function processGeneratorIO(inputPath, outputPath, options = {}) {
|
|
|
25587
25688
|
model: result.model,
|
|
25588
25689
|
prompt,
|
|
25589
25690
|
bytes: artifactBytes,
|
|
25691
|
+
sizeRequest,
|
|
25590
25692
|
...artifactWidth !== undefined ? { width: artifactWidth } : {},
|
|
25591
25693
|
...artifactHeight !== undefined ? { height: artifactHeight } : {},
|
|
25592
25694
|
...illustrationMeta !== undefined ? { illustration: illustrationMeta } : {}
|
|
@@ -19,29 +19,117 @@ export async function readBytes(path: string): Promise<Uint8Array> {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
* Task 0180 — pure
|
|
23
|
-
* Reads `width
|
|
24
|
-
*
|
|
25
|
-
*
|
|
22
|
+
* Task 0180 (PNG) + task 0182 (JPEG/WebP, `null` contract) — pure header-only dimension
|
|
23
|
+
* reader. Reads `width`/`height` from PNG (IHDR), JPEG (SOF0–SOF15 except DHT/DAC/JPG)
|
|
24
|
+
* and WebP (VP8 lossy, VP8L lossless, VP8X extended) bytes; no decode. Returns `null`
|
|
25
|
+
* when the bytes cannot be parsed — the caller decides whether that is fatal (the compose
|
|
26
|
+
* canvas assertion) or a WARN with dimensions omitted (artifact recording, R3: the bytes
|
|
27
|
+
* are already paid for).
|
|
26
28
|
*/
|
|
27
|
-
export function imageDimensions(bytes: Uint8Array): { width: number; height: number } {
|
|
28
|
-
|
|
29
|
+
export function imageDimensions(bytes: Uint8Array): { width: number; height: number } | null {
|
|
30
|
+
if (isPng(bytes)) return pngDimensions(bytes);
|
|
31
|
+
if (isJpeg(bytes)) return jpegDimensions(bytes);
|
|
32
|
+
if (isWebp(bytes)) return webpDimensions(bytes);
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isPng(b: Uint8Array): boolean {
|
|
29
37
|
const PNG_SIG = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
|
|
30
|
-
if (
|
|
31
|
-
throw new Error('imageDimensions: not a PNG — buffer too small');
|
|
32
|
-
}
|
|
38
|
+
if (b.byteLength < 24) return false;
|
|
33
39
|
for (let i = 0; i < 8; i++) {
|
|
34
|
-
if (
|
|
35
|
-
throw new Error('imageDimensions: not a PNG — invalid signature');
|
|
36
|
-
}
|
|
40
|
+
if (b[i] !== PNG_SIG[i]) return false;
|
|
37
41
|
}
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function pngDimensions(b: Uint8Array): { width: number; height: number } | null {
|
|
38
46
|
// IHDR chunk: offset 8 = chunk data length (must be 13), offset 12 = 'IHDR' tag.
|
|
39
|
-
const view = new DataView(
|
|
47
|
+
const view = new DataView(b.buffer, b.byteOffset, b.byteLength);
|
|
40
48
|
const ihdrLength = view.getUint32(8, false);
|
|
41
|
-
if (ihdrLength !== 13 ||
|
|
42
|
-
|
|
49
|
+
if (ihdrLength !== 13 || view.getUint32(12, false) !== 0x49484452) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
return { width: view.getUint32(16, false), height: view.getUint32(20, false) };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isJpeg(b: Uint8Array): boolean {
|
|
56
|
+
const view = new DataView(b.buffer, b.byteOffset, b.byteLength);
|
|
57
|
+
// SOI + the first marker's 0xFF prefix.
|
|
58
|
+
return b.byteLength >= 4 && view.getUint16(0, false) === 0xffd8 && view.getUint8(2) === 0xff;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function jpegDimensions(b: Uint8Array): { width: number; height: number } | null {
|
|
62
|
+
const view = new DataView(b.buffer, b.byteOffset, b.byteLength);
|
|
63
|
+
let at = 2; // after SOI
|
|
64
|
+
while (at + 4 <= b.byteLength) {
|
|
65
|
+
if (view.getUint8(at) !== 0xff) return null; // marker desync — not a parseable JPEG stream
|
|
66
|
+
const marker = view.getUint8(at + 1);
|
|
67
|
+
at += 2;
|
|
68
|
+
// Standalone markers (TEM, RSTn) carry no length segment.
|
|
69
|
+
if (marker === 0x01 || (marker >= 0xd0 && marker <= 0xd7)) continue;
|
|
70
|
+
if (at + 2 > b.byteLength) return null;
|
|
71
|
+
const length = view.getUint16(at, false);
|
|
72
|
+
if (length < 2) return null;
|
|
73
|
+
// SOF0–SOF15 minus DHT (C4), JPG (C8) and DAC (CC) carry the canvas.
|
|
74
|
+
const isSof = marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc;
|
|
75
|
+
if (isSof) {
|
|
76
|
+
// SOF segment: length(2) precision(1) height(2) width(2).
|
|
77
|
+
if (at + 7 > b.byteLength) return null;
|
|
78
|
+
return { height: view.getUint16(at + 3, false), width: view.getUint16(at + 5, false) };
|
|
79
|
+
}
|
|
80
|
+
at += length;
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function isWebp(b: Uint8Array): boolean {
|
|
86
|
+
if (b.byteLength < 20) return false;
|
|
87
|
+
const view = new DataView(b.buffer, b.byteOffset, b.byteLength);
|
|
88
|
+
return view.getUint32(0, true) === 0x46464952 && view.getUint32(8, true) === 0x50424557; // 'RIFF' 'WEBP'
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function webpDimensions(b: Uint8Array): { width: number; height: number } | null {
|
|
92
|
+
const view = new DataView(b.buffer, b.byteOffset, b.byteLength);
|
|
93
|
+
// Walk the RIFF chunks after the 12-byte file header until a size-bearing chunk appears.
|
|
94
|
+
let at = 12;
|
|
95
|
+
while (at + 8 <= b.byteLength) {
|
|
96
|
+
const id = String.fromCharCode(
|
|
97
|
+
view.getUint8(at),
|
|
98
|
+
view.getUint8(at + 1),
|
|
99
|
+
view.getUint8(at + 2),
|
|
100
|
+
view.getUint8(at + 3),
|
|
101
|
+
);
|
|
102
|
+
const size = view.getUint32(at + 4, true);
|
|
103
|
+
const body = at + 8;
|
|
104
|
+
if (id === 'VP8 ') {
|
|
105
|
+
// Lossy: frame tag (3) + sync code 0x9d 0x01 0x2a + 14-bit LE width/height.
|
|
106
|
+
if (size < 10 || body + 10 > b.byteLength) return null;
|
|
107
|
+
if (
|
|
108
|
+
view.getUint8(body + 3) !== 0x9d ||
|
|
109
|
+
view.getUint8(body + 4) !== 0x01 ||
|
|
110
|
+
view.getUint8(body + 5) !== 0x2a
|
|
111
|
+
) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
width: view.getUint16(body + 6, true) & 0x3fff,
|
|
116
|
+
height: view.getUint16(body + 8, true) & 0x3fff,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
if (id === 'VP8L') {
|
|
120
|
+
// Lossless: signature 0x2f, then 14-bit width-1 / height-1 packed little-endian.
|
|
121
|
+
if (size < 5 || body + 5 > b.byteLength || view.getUint8(body) !== 0x2f) return null;
|
|
122
|
+
const bits = view.getUint32(body + 1, true);
|
|
123
|
+
return { width: (bits & 0x3fff) + 1, height: ((bits >>> 14) & 0x3fff) + 1 };
|
|
124
|
+
}
|
|
125
|
+
if (id === 'VP8X') {
|
|
126
|
+
// Extended: 4 flag/reserved bytes, then 24-bit LE width-1 / height-1.
|
|
127
|
+
if (size < 10 || body + 10 > b.byteLength) return null;
|
|
128
|
+
const width = view.getUint8(body + 4) | (view.getUint8(body + 5) << 8) | (view.getUint8(body + 6) << 16);
|
|
129
|
+
const height = view.getUint8(body + 7) | (view.getUint8(body + 8) << 8) | (view.getUint8(body + 9) << 16);
|
|
130
|
+
return { width: width + 1, height: height + 1 };
|
|
131
|
+
}
|
|
132
|
+
at = body + size + (size % 2); // RIFF chunks pad to even sizes
|
|
43
133
|
}
|
|
44
|
-
|
|
45
|
-
const height = view.getUint32(20, false);
|
|
46
|
-
return { width, height };
|
|
134
|
+
return null;
|
|
47
135
|
}
|
|
@@ -390,7 +390,10 @@ export async function processGeneratorIO(
|
|
|
390
390
|
const captions: CaptionEntry[] = [];
|
|
391
391
|
const attempts: ProviderAttempt[] = [];
|
|
392
392
|
let at = 0; // sticky index into `order`
|
|
393
|
-
const generateWithFallback = async (
|
|
393
|
+
const generateWithFallback = async (
|
|
394
|
+
image: number,
|
|
395
|
+
prompt: string,
|
|
396
|
+
): Promise<{ result: ImageResult; sizeRequest: 'exact' | 'aspect' }> => {
|
|
394
397
|
const start = attempts.length; // this image's failed attempts begin here (R5 summarises the slice)
|
|
395
398
|
for (;;) {
|
|
396
399
|
const name = order[at];
|
|
@@ -401,17 +404,26 @@ export async function processGeneratorIO(
|
|
|
401
404
|
const provider = at === 0 ? primary : createCandidate(name);
|
|
402
405
|
// spec.model is provider-specific (D6): only the requested provider receives it.
|
|
403
406
|
const model = at === 0 ? (spec.model ?? provider.defaultModel()) : provider.defaultModel();
|
|
407
|
+
// 0182 R1/R4: the exact-size gate runs per candidate, with that candidate's own
|
|
408
|
+
// model and its own capability report. Without it the request keeps today's
|
|
409
|
+
// aspect-only shape (R2); on the exact path aspectRatio is omitted (R1 — MiniMax
|
|
410
|
+
// reads width/height only when aspect_ratio is absent).
|
|
411
|
+
const exact =
|
|
412
|
+
format.width !== undefined &&
|
|
413
|
+
format.height !== undefined &&
|
|
414
|
+
provider.exactSize?.(model, format.width, format.height) === true;
|
|
404
415
|
const request: ImageRequest = {
|
|
405
416
|
prompt,
|
|
406
417
|
model,
|
|
407
|
-
aspectRatio: format.aspectRatio,
|
|
418
|
+
...(exact ? { width: format.width, height: format.height } : { aspectRatio: format.aspectRatio }),
|
|
408
419
|
...(spec.referenceImages !== undefined && spec.referenceImages.length > 0
|
|
409
420
|
? { referenceImages: spec.referenceImages }
|
|
410
421
|
: {}),
|
|
411
422
|
};
|
|
412
423
|
try {
|
|
413
424
|
provider.validate?.(request);
|
|
414
|
-
|
|
425
|
+
const result = await provider.generate(request);
|
|
426
|
+
return { result, sizeRequest: exact ? 'exact' : 'aspect' };
|
|
415
427
|
} catch (err: unknown) {
|
|
416
428
|
if (spec.fallback === undefined) {
|
|
417
429
|
throw err; // D6 R1: strict mode propagates the original error, unwrapped.
|
|
@@ -441,7 +453,22 @@ export async function processGeneratorIO(
|
|
|
441
453
|
};
|
|
442
454
|
let body = doc?.body ?? '';
|
|
443
455
|
for (const [index, prompt] of prompts.entries()) {
|
|
444
|
-
const result = await generateWithFallback(index + 1, prompt);
|
|
456
|
+
const { result, sizeRequest } = await generateWithFallback(index + 1, prompt);
|
|
457
|
+
// 0182 R3: measure the raw provider delivery once — both branches read it. A declared
|
|
458
|
+
// canvas that came back different (or unparseable) warns and never fails: the bytes
|
|
459
|
+
// are already paid for, and failing would delete them through the cleanup path.
|
|
460
|
+
const delivered = imageDimensions(result.bytes);
|
|
461
|
+
if (format.width !== undefined && format.height !== undefined) {
|
|
462
|
+
if (delivered === null) {
|
|
463
|
+
echoError(
|
|
464
|
+
`WARN image-gen: ${result.provider}/${result.model} delivered unparseable image bytes; dimensions omitted`,
|
|
465
|
+
);
|
|
466
|
+
} else if (delivered.width !== format.width || delivered.height !== format.height) {
|
|
467
|
+
echoError(
|
|
468
|
+
`WARN image-gen: ${result.provider}/${result.model} delivered ${delivered.width}x${delivered.height}; format '${format.name}' declares ${format.width}x${format.height}`,
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
445
472
|
// Re-check against the spliced body; the pre-spend plan check is assertAnchorPlan (0169).
|
|
446
473
|
let anchor: string | undefined;
|
|
447
474
|
if (spec.job === 'illustration') {
|
|
@@ -505,6 +532,11 @@ export async function processGeneratorIO(
|
|
|
505
532
|
|
|
506
533
|
// 4. Measure the card dimensions via imageDimensions and assert canvas.
|
|
507
534
|
const dims = imageDimensions(cardBytes);
|
|
535
|
+
if (dims === null) {
|
|
536
|
+
throw new Error(
|
|
537
|
+
`compose: composed card dimensions are unparseable; format '${format.name}' requires ${format.width}x${format.height}`,
|
|
538
|
+
);
|
|
539
|
+
}
|
|
508
540
|
if (dims.width !== format.width || dims.height !== format.height) {
|
|
509
541
|
throw new Error(
|
|
510
542
|
`compose: card dimensions ${dims.width}x${dims.height} do not match format canvas ${format.width}x${format.height}`,
|
|
@@ -515,14 +547,21 @@ export async function processGeneratorIO(
|
|
|
515
547
|
artifactBytes = cardBytes.byteLength;
|
|
516
548
|
artifactWidth = dims.width;
|
|
517
549
|
artifactHeight = dims.height;
|
|
518
|
-
illustrationMeta = {
|
|
550
|
+
illustrationMeta = {
|
|
551
|
+
path: illustrationArtifactPath,
|
|
552
|
+
bytes: result.bytes.byteLength,
|
|
553
|
+
// 0182 R3/R5: the raw provider delivery, measured from its own bytes.
|
|
554
|
+
...(delivered !== null ? { width: delivered.width, height: delivered.height } : {}),
|
|
555
|
+
};
|
|
519
556
|
} else {
|
|
520
|
-
// Non-compose: existing path (R6).
|
|
557
|
+
// Non-compose: existing path (R6); the artifact records the delivered size (0182 R3).
|
|
521
558
|
const path = artifactPath(outputPath, stem, format.name, index + 1, result.extension);
|
|
522
559
|
await writeBytes(path, result.bytes);
|
|
523
560
|
written.push(path);
|
|
524
561
|
artifactPathValue = path;
|
|
525
562
|
artifactBytes = result.bytes.byteLength;
|
|
563
|
+
artifactWidth = delivered?.width;
|
|
564
|
+
artifactHeight = delivered?.height;
|
|
526
565
|
}
|
|
527
566
|
|
|
528
567
|
const artifact: ImageArtifact = {
|
|
@@ -533,6 +572,7 @@ export async function processGeneratorIO(
|
|
|
533
572
|
model: result.model,
|
|
534
573
|
prompt,
|
|
535
574
|
bytes: artifactBytes,
|
|
575
|
+
sizeRequest,
|
|
536
576
|
...(artifactWidth !== undefined ? { width: artifactWidth } : {}),
|
|
537
577
|
...(artifactHeight !== undefined ? { height: artifactHeight } : {}),
|
|
538
578
|
...(illustrationMeta !== undefined ? { illustration: illustrationMeta } : {}),
|
|
@@ -76,6 +76,12 @@ export interface ImageArtifact {
|
|
|
76
76
|
/** The exact prompt string sent. */
|
|
77
77
|
prompt: string;
|
|
78
78
|
seed?: number;
|
|
79
|
+
/**
|
|
80
|
+
* 0182 R3: what the provider was asked for — 'exact' when the request carried the
|
|
81
|
+
* preset's declared width/height (the provider reported it could honour them for this
|
|
82
|
+
* model), 'aspect' for the aspect-only shape. Present on every written artifact.
|
|
83
|
+
*/
|
|
84
|
+
sizeRequest?: 'exact' | 'aspect';
|
|
79
85
|
width?: number;
|
|
80
86
|
height?: number;
|
|
81
87
|
bytes: number;
|
|
@@ -50,11 +50,17 @@ export function parsePixelSize(size: string): { width: number; height: number }
|
|
|
50
50
|
return { width, height };
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/** 0182 R4: image-01 accepts an exact canvas of 512..2048 per edge, divisible by 8. */
|
|
54
|
+
function isAcceptablePixelSize(width: number, height: number): boolean {
|
|
55
|
+
return width >= 512 && width <= 2048 && height >= 512 && height <= 2048 && width % 8 === 0 && height % 8 === 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Throwing twin of {@link isAcceptablePixelSize} for the request-body path; messages are frozen (tests pin them). */
|
|
53
59
|
function validatePixelSize(width: number, height: number): void {
|
|
54
|
-
if (width
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
60
|
+
if (!isAcceptablePixelSize(width, height)) {
|
|
61
|
+
if (width < 512 || width > 2048 || height < 512 || height > 2048) {
|
|
62
|
+
throw new Error('MiniMax custom size must keep width and height between 512 and 2048.');
|
|
63
|
+
}
|
|
58
64
|
throw new Error('MiniMax custom size requires width and height divisible by 8.');
|
|
59
65
|
}
|
|
60
66
|
}
|
|
@@ -146,6 +152,8 @@ export const createMinimax: ProviderFactory = (options?: ProviderOptions): Image
|
|
|
146
152
|
defaultModel: () => DEFAULT_MODEL,
|
|
147
153
|
isAvailable: () => readEnv('MINIMAX_API_KEY') !== '',
|
|
148
154
|
supportsReferenceImages: () => true,
|
|
155
|
+
exactSize: (model: string, width: number, height: number): boolean =>
|
|
156
|
+
model === 'image-01' && isAcceptablePixelSize(width, height),
|
|
149
157
|
validate(req: ImageRequest): void {
|
|
150
158
|
requestBody(req.prompt, req.model, req);
|
|
151
159
|
},
|
|
@@ -32,7 +32,11 @@ export interface ImageRequest {
|
|
|
32
32
|
model: string;
|
|
33
33
|
/** e.g. '21:9' — from FormatPreset.aspectRatio (0102). */
|
|
34
34
|
aspectRatio?: string;
|
|
35
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* Declared preset canvas (0182 R1). Sent only when the resolved provider's
|
|
37
|
+
* `exactSize(model, width, height)` returns true — with `aspectRatio` omitted;
|
|
38
|
+
* every other provider receives the aspect-only request shape and never sees them.
|
|
39
|
+
*/
|
|
36
40
|
width?: number;
|
|
37
41
|
height?: number;
|
|
38
42
|
quality?: ImageQuality;
|
|
@@ -64,6 +68,13 @@ export interface ImageProvider {
|
|
|
64
68
|
isAvailable(): boolean;
|
|
65
69
|
/** Queryable ref-image capability; 0105 routes `--ref` work on this fact. */
|
|
66
70
|
supportsReferenceImages(model: string): boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Exact-size capability (0182 R1/R4): can this adapter deliver the exact pixel canvas
|
|
73
|
+
* for this model? Absent = aspect-only — `processGeneratorIO` then sends only
|
|
74
|
+
* `aspectRatio`, byte-for-byte as before 0182. Adapters opt in one at a time, on
|
|
75
|
+
* measured evidence (task 0182 Q&A, closed).
|
|
76
|
+
*/
|
|
77
|
+
exactSize?(model: string, width: number, height: number): boolean;
|
|
67
78
|
/** Throw on an impossible combination (e.g. Imagen + referenceImages) before spending money. */
|
|
68
79
|
validate?(req: ImageRequest): void;
|
|
69
80
|
generate(req: ImageRequest): Promise<ImageResult>;
|