@gobing-ai/knowledge-kit 0.0.29 → 0.0.31
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 +20 -3
- package/plugins/generations/dailynews-gen/src/index.ts +7 -0
- package/plugins/generations/dailynews-gen/src/script-builder.ts +4 -2
- package/plugins/generations/dailynews-gen/src/script-plan.ts +21 -3
- package/plugins/generations/episode-plan-gen/dist/index.js +66 -5
- package/plugins/generations/episode-plan-gen/src/index.ts +103 -4
- 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 +32 -0
- package/plugins/kk/skills/content-judge/references/workflow-integration.md +10 -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
|
@@ -248,9 +248,30 @@ function passesGates(doc: Doc, planDate: string): boolean {
|
|
|
248
248
|
return published >= Date.parse(planDate) - FRESHNESS_DAYS * 86_400_000;
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
-
/**
|
|
251
|
+
/**
|
|
252
|
+
* Research-note section headings: the horizon/last30days note format labels its sections with a
|
|
253
|
+
* bold corner-bracket heading (`**「社区讨论」** 评论区…`, `**「背景」**`, `**「背景:GCN 架构」**`,
|
|
254
|
+
* `**「方案」**`, …). Spoken from a VoiceScript segment they read as scaffolding, and the content
|
|
255
|
+
* gates reject a segment that opens with one (`voice-gen`'s `voicescript.ts`, the
|
|
256
|
+
* `validate-voicescript.ts` sidecar) — which fails the whole unattended daily run at the `script`
|
|
257
|
+
* state (2026-10-03 「背景」/「社区讨论」, 2026-10-04 「社区讨论」). The bold corner-bracket markup is
|
|
258
|
+
* the discriminator: these note bodies never bold-quote conversational prose, so the label list
|
|
259
|
+
* stays open-ended.
|
|
260
|
+
*/
|
|
261
|
+
const SCAFFOLD_HEADING_RE = /^\s*\*\*\s*[「『][^」』\n]{1,80}[」』]\s*\*\*\s*[::]?(?:\s+|$)/;
|
|
262
|
+
|
|
263
|
+
/** Strip `⭐️ x.y/10` ratings, `### [title](url)` heading copies and `**「label」**` research-note
|
|
264
|
+
* section headings from a body. A heading ahead of prose keeps the prose; a heading alone on its
|
|
265
|
+
* line disappears with it. */
|
|
252
266
|
export function cleanBody(body: string): string {
|
|
253
|
-
|
|
267
|
+
const kept: string[] = [];
|
|
268
|
+
for (const line of body.split('\n')) {
|
|
269
|
+
const stripped = line.replace(SCAFFOLD_HEADING_RE, '');
|
|
270
|
+
if (stripped.trim() === '' && line.trim() !== '') continue; // the line held only a heading
|
|
271
|
+
kept.push(stripped);
|
|
272
|
+
}
|
|
273
|
+
return kept
|
|
274
|
+
.join('\n')
|
|
254
275
|
.replace(/⭐️\s*[\d.]+\s*\/\s*10/g, '')
|
|
255
276
|
.replace(/^#{1,6}\s*\[[^\]]*\]\([^)]*\)\s*$/gm, '')
|
|
256
277
|
.replace(/\n{3,}/g, '\n\n')
|
|
@@ -635,6 +656,8 @@ export interface FilterConfig {
|
|
|
635
656
|
minImportance: number;
|
|
636
657
|
minUrgency: number;
|
|
637
658
|
minImpact: number;
|
|
659
|
+
/** Episode floor: when fewer docs survive, importance-only rejects backfill up to this count. */
|
|
660
|
+
minEpisodeItems: number;
|
|
638
661
|
}
|
|
639
662
|
|
|
640
663
|
/** A doc the filter dropped, with every failed check (`category:not-in-allowlist`, `quality:2<3`, …). */
|
|
@@ -659,6 +682,13 @@ export const DEFAULT_FILTER_CONFIG: FilterConfig = {
|
|
|
659
682
|
minImportance: 3,
|
|
660
683
|
minUrgency: 2,
|
|
661
684
|
minImpact: 1,
|
|
685
|
+
// 20261002 dogfood: a weak, policy-heavy day collapsed the episode to 2 spoken items
|
|
686
|
+
// (~5 min audio) because importance (source-tier base) is the first dimension a tail-source
|
|
687
|
+
// day fails. Backfill admits importance-only rejects — quality/urgency/impact/category
|
|
688
|
+
// minimums stay hard — so a thin news day deepens instead of shipping a stub. Floor is 8,
|
|
689
|
+
// not 6: the prompt-layer 选题边界 (script-write) drops policy items AFTER this gate (today
|
|
690
|
+
// 2 of 5), so the floor compensates upstream to land ~4-6 spoken items on a thin day.
|
|
691
|
+
minEpisodeItems: 8,
|
|
662
692
|
};
|
|
663
693
|
|
|
664
694
|
/** One QC_MIN_* threshold: an integer 0–5 or absent (→ fallback); anything else fails loud — never clamped. */
|
|
@@ -671,6 +701,17 @@ function parseFilterThreshold(raw: string | undefined, name: string, fallback: n
|
|
|
671
701
|
return parsed;
|
|
672
702
|
}
|
|
673
703
|
|
|
704
|
+
/** QC_MIN_EPISODE_ITEMS: a positive integer count or absent (→ fallback); anything else fails loud. */
|
|
705
|
+
function parseFloorCount(raw: string | undefined, name: string, fallback: number): number {
|
|
706
|
+
if (raw === undefined || raw.trim() === '') return fallback;
|
|
707
|
+
const value = raw.trim();
|
|
708
|
+
if (!/^\d+$/.test(value)) throw new Error(`${name}='${raw}' is not a positive integer (fail loud, no clamping)`);
|
|
709
|
+
const parsed = Number.parseInt(value, 10);
|
|
710
|
+
if (parsed < 1)
|
|
711
|
+
throw new Error(`${name}=${parsed} must be >= 1 (a 0 floor disables the gate — drop the env instead)`);
|
|
712
|
+
return parsed;
|
|
713
|
+
}
|
|
714
|
+
|
|
674
715
|
/** QC_CATEGORIES: a comma list of MECE categories; unknown entries fail loud (no silent drops). */
|
|
675
716
|
function parseFilterCategories(raw: string | undefined): Category[] {
|
|
676
717
|
if (raw === undefined || raw.trim() === '') return [...DEFAULT_FILTER_CONFIG.categories];
|
|
@@ -697,6 +738,11 @@ export function filterConfigFromEnv(env: Record<string, string | undefined> = pr
|
|
|
697
738
|
),
|
|
698
739
|
minUrgency: parseFilterThreshold(env.QC_MIN_URGENCY, 'QC_MIN_URGENCY', DEFAULT_FILTER_CONFIG.minUrgency),
|
|
699
740
|
minImpact: parseFilterThreshold(env.QC_MIN_IMPACT, 'QC_MIN_IMPACT', DEFAULT_FILTER_CONFIG.minImpact),
|
|
741
|
+
minEpisodeItems: parseFloorCount(
|
|
742
|
+
env.QC_MIN_EPISODE_ITEMS,
|
|
743
|
+
'QC_MIN_EPISODE_ITEMS',
|
|
744
|
+
DEFAULT_FILTER_CONFIG.minEpisodeItems,
|
|
745
|
+
),
|
|
700
746
|
};
|
|
701
747
|
}
|
|
702
748
|
|
|
@@ -749,6 +795,53 @@ export function filterAnnotated(docs: Doc[], config: FilterConfig): { kept: Doc[
|
|
|
749
795
|
return { kept, rejected };
|
|
750
796
|
}
|
|
751
797
|
|
|
798
|
+
function importanceOf(doc: Doc): number {
|
|
799
|
+
const scores = asRecord(asRecord(doc.metadata).scores);
|
|
800
|
+
return typeof scores.importance === 'number' ? scores.importance : 0;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function planScoreOf(doc: Doc): number {
|
|
804
|
+
const value = asRecord(doc.metadata).planScore;
|
|
805
|
+
return typeof value === 'number' ? value : 0;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Episode floor (20261002): when the strict filter leaves fewer than `minEpisodeItems` docs,
|
|
810
|
+
* backfill from rejects that failed ONLY the importance minimum — importance is the source-tier
|
|
811
|
+
* base (tail sources carry 2), so on a thin day it is the softest dimension, while quality/
|
|
812
|
+
* urgency/impact/category stay hard gates. Admission order: importance desc, planScore desc,
|
|
813
|
+
* plan order. Admitted docs are stamped `metadata.backfilled: true` and leave the rejected
|
|
814
|
+
* audit (they are spoken, not dropped). Pure: input docs are never mutated.
|
|
815
|
+
*/
|
|
816
|
+
export function backfillToFloor(
|
|
817
|
+
kept: Doc[],
|
|
818
|
+
rejected: RejectedDoc[],
|
|
819
|
+
config: FilterConfig,
|
|
820
|
+
): { kept: Doc[]; rejected: RejectedDoc[]; admitted: Doc[] } {
|
|
821
|
+
if (kept.length >= config.minEpisodeItems) return { kept, rejected, admitted: [] };
|
|
822
|
+
const eligible = rejected
|
|
823
|
+
.map((entry, index) => ({ entry, index }))
|
|
824
|
+
.filter(({ entry }) => entry.reasons.length > 0 && entry.reasons.every((r) => r.startsWith('importance:')));
|
|
825
|
+
eligible.sort((a, b) => {
|
|
826
|
+
const byImportance = importanceOf(b.entry.doc) - importanceOf(a.entry.doc);
|
|
827
|
+
if (byImportance !== 0) return byImportance;
|
|
828
|
+
const byPlanScore = planScoreOf(b.entry.doc) - planScoreOf(a.entry.doc);
|
|
829
|
+
if (byPlanScore !== 0) return byPlanScore;
|
|
830
|
+
return a.index - b.index;
|
|
831
|
+
});
|
|
832
|
+
const taken = eligible.slice(0, config.minEpisodeItems - kept.length);
|
|
833
|
+
const takenIds = new Set(taken.map(({ entry }) => entry.doc.id));
|
|
834
|
+
const admitted = taken.map(({ entry }) => ({
|
|
835
|
+
...entry.doc,
|
|
836
|
+
metadata: { ...asRecord(entry.doc.metadata), backfilled: true },
|
|
837
|
+
}));
|
|
838
|
+
return {
|
|
839
|
+
kept: [...kept, ...admitted],
|
|
840
|
+
rejected: rejected.filter((entry) => !takenIds.has(entry.doc.id)),
|
|
841
|
+
admitted,
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
|
|
752
845
|
/** Sibling rejected-audit path: `<out>.json` → `<out>.rejected.json` (candidates.json → candidates.rejected.json). */
|
|
753
846
|
export function rejectedOutPath(outputPath: string): string {
|
|
754
847
|
return outputPath.endsWith('.json')
|
|
@@ -792,9 +885,15 @@ async function processFilterIO(inputPath: string, outputPath: string): Promise<C
|
|
|
792
885
|
const config = filterConfigFromEnv();
|
|
793
886
|
const docs = DocListSchema.parse(await readJsonFile(inputPath));
|
|
794
887
|
const { kept, rejected } = filterAnnotated(docs, config);
|
|
795
|
-
const
|
|
888
|
+
const filled = backfillToFloor(kept, rejected, config);
|
|
889
|
+
if (filled.admitted.length > 0) {
|
|
890
|
+
echoError(
|
|
891
|
+
`episode floor backfill: admitted ${filled.admitted.length} importance-only reject(s) to reach ${config.minEpisodeItems} items (${filled.admitted.map((d) => d.id).join(', ')})`,
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
const content = planContent(filled.kept, resolvePlanDate());
|
|
796
895
|
await atomicWrite(outputPath, content);
|
|
797
|
-
await atomicWrite(rejectedOutPath(outputPath), { rejected, config, total: docs.length });
|
|
896
|
+
await atomicWrite(rejectedOutPath(outputPath), { rejected: filled.rejected, config, total: docs.length });
|
|
798
897
|
return content;
|
|
799
898
|
}
|
|
800
899
|
|
|
@@ -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
|
},
|