@gobing-ai/knowledge-kit 0.0.12 → 0.0.14
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 +120 -26
- package/package.json +1 -1
- package/plugins/generations/content-gen/dist/index.js +22187 -0
- package/plugins/generations/content-gen/plugin.json +1 -1
- package/plugins/generations/core-facts-gen/dist/index.js +22068 -0
- package/plugins/generations/core-facts-gen/plugin.json +1 -1
- package/plugins/generations/daily-article-gen/dist/index.js +22050 -0
- package/plugins/generations/daily-article-gen/plugin.json +1 -1
- package/plugins/generations/daily-article-gen/src/index.ts +13 -1
- package/plugins/generations/dailynews-gen/dist/index.js +22344 -0
- package/plugins/generations/dailynews-gen/plugin.json +1 -1
- package/plugins/generations/episode-plan-gen/dist/index.js +22503 -0
- package/plugins/generations/episode-plan-gen/plugin.json +1 -1
- package/plugins/generations/episode-plan-gen/src/index.ts +11 -0
- package/plugins/generations/image-gen/config.example.yaml +75 -0
- package/plugins/generations/image-gen/dist/index.js +24862 -0
- package/plugins/generations/image-gen/package.json +17 -0
- package/plugins/generations/image-gen/plugin.json +7 -0
- package/plugins/generations/image-gen/presets/formats/cover.yaml +57 -0
- package/plugins/generations/image-gen/presets/formats/free.yaml +46 -0
- package/plugins/generations/image-gen/presets/formats/illustration.yaml +48 -0
- package/plugins/generations/image-gen/presets/styles/clean-webapp-ui.yaml +28 -0
- package/plugins/generations/image-gen/presets/styles/cute.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/editorial.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/fresh.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/minimalist.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/photorealistic.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/sketch.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/technical-diagram.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/vibrant.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/warm.yaml +3 -0
- package/plugins/generations/image-gen/src/bytes.ts +19 -0
- package/plugins/generations/image-gen/src/index.ts +319 -0
- package/plugins/generations/image-gen/src/job.ts +143 -0
- package/plugins/generations/image-gen/src/paths.ts +31 -0
- package/plugins/generations/image-gen/src/presets.ts +344 -0
- package/plugins/generations/image-gen/src/providers/agnes.ts +110 -0
- package/plugins/generations/image-gen/src/providers/azure.ts +153 -0
- package/plugins/generations/image-gen/src/providers/codex-cli.ts +170 -0
- package/plugins/generations/image-gen/src/providers/dashscope.ts +485 -0
- package/plugins/generations/image-gen/src/providers/google.ts +268 -0
- package/plugins/generations/image-gen/src/providers/huggingface.ts +59 -0
- package/plugins/generations/image-gen/src/providers/jimeng.ts +259 -0
- package/plugins/generations/image-gen/src/providers/minimax.ts +171 -0
- package/plugins/generations/image-gen/src/providers/openai.ts +319 -0
- package/plugins/generations/image-gen/src/providers/openrouter.ts +257 -0
- package/plugins/generations/image-gen/src/providers/refs.ts +24 -0
- package/plugins/generations/image-gen/src/providers/replicate.ts +279 -0
- package/plugins/generations/image-gen/src/providers/seedream.ts +128 -0
- package/plugins/generations/image-gen/src/providers/types.ts +286 -0
- package/plugins/generations/image-gen/src/providers/zai.ts +237 -0
- package/plugins/generations/image-gen/tsconfig.json +8 -0
- package/plugins/generations/news-report-gen/dist/index.js +22193 -0
- package/plugins/generations/news-report-gen/package.json +17 -0
- package/plugins/generations/news-report-gen/plugin.json +7 -0
- package/plugins/generations/news-report-gen/src/index.ts +308 -0
- package/plugins/generations/news-report-gen/tsconfig.json +4 -0
- package/plugins/generations/omni-voice-gen/Makefile +14 -0
- package/plugins/generations/omni-voice-gen/README.md +112 -0
- package/plugins/generations/omni-voice-gen/bin/omni-voice-gen +2 -0
- package/plugins/generations/omni-voice-gen/dist/omni-voice-gen-prr8skpb. +2 -0
- package/plugins/generations/omni-voice-gen/dist/omni-voice-gen.js +6 -0
- package/plugins/generations/omni-voice-gen/plugin.json +6 -0
- package/plugins/generations/omni-voice-gen/profiles.json +12 -0
- package/plugins/generations/omni-voice-gen/pyproject.toml +25 -0
- package/plugins/generations/omni-voice-gen/scripts/coverage_gate.py +74 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/__init__.py +1 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/__main__.py +39 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/audio.py +190 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/backend.py +150 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/contract.py +76 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/mp3.py +60 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/pipeline.py +289 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/profiles.py +100 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/qc.py +234 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/voicescript.py +352 -0
- package/plugins/generations/omni-voice-gen/uv.lock +3510 -0
- package/plugins/generations/voice-gen/dist/index.js +23055 -0
- package/plugins/generations/voice-gen/plugin.json +1 -1
- package/plugins/generations/voice-gen/src/index.ts +16 -1
- package/plugins/generations/voice-gen/src/voicebox-client.ts +3 -1
- package/plugins/ingestions/aihot-ingest/dist/index.js +22378 -0
- package/plugins/ingestions/aihot-ingest/plugin.json +1 -1
- package/plugins/ingestions/horizon-ingest/dist/index.js +22125 -0
- package/plugins/ingestions/horizon-ingest/plugin.json +1 -1
- package/plugins/ingestions/karakeep-local/dist/index.js +24204 -0
- package/plugins/ingestions/karakeep-local/plugin.json +1 -1
- package/plugins/ingestions/last30days-ingest/dist/index.js +22070 -0
- package/plugins/ingestions/last30days-ingest/plugin.json +1 -1
- package/plugins/ingestions/web-search/dist/index.js +24399 -0
- package/plugins/ingestions/web-search/plugin.json +1 -1
- package/plugins/kk/commands/image-extract.md +40 -0
- package/plugins/kk/commands/image-generate.md +32 -0
- package/plugins/kk/config.example.yaml +80 -0
- package/plugins/kk/plugin.json +1 -1
- package/plugins/kk/skills/image-authoring/SKILL.md +257 -0
- package/plugins/kk/skills/image-authoring/references/format-drafting.md +57 -0
- package/plugins/kk/skills/image-authoring/references/illustration-positions.md +87 -0
- package/plugins/kk/skills/image-authoring/references/migrating-from-wt.md +31 -0
- package/plugins/kk/skills/image-authoring/references/providers.md +52 -0
- package/plugins/kk/skills/image-authoring/references/style-extraction.md +139 -0
- package/plugins/kk/workflows/kk-daily-ai-voice.yaml +130 -30
- package/plugins/publishings/emdash-pub/dist/index.js +22263 -0
- package/plugins/publishings/emdash-pub/plugin.json +1 -1
- package/plugins/publishings/podcast-pub/dist/index.js +22650 -0
- package/plugins/publishings/podcast-pub/plugin.json +8 -2
- package/plugins/publishings/podcast-pub/src/index.ts +18 -2
- package/plugins/publishings/podcast-pub/src/show-notes.ts +56 -9
- package/plugins/publishings/qiita-pub/dist/index.js +22101 -0
- package/plugins/publishings/qiita-pub/plugin.json +1 -1
- package/plugins/publishings/surfdash-pub/dist/index.js +22323 -0
- package/plugins/publishings/surfdash-pub/plugin.json +1 -1
- package/plugins/publishings/surfdash-pub/src/index.ts +109 -9
- package/plugins/publishings/zenn-pub/dist/index.js +22142 -0
- package/plugins/publishings/zenn-pub/plugin.json +1 -1
- package/plugins/sp/scripts/batch-preflight.mjs +346 -0
- package/plugins/sp/scripts/batch-preflight.ts +459 -0
- package/plugins/sp/scripts/daily-summary/daily-summary.mjs +615 -0
- package/plugins/sp/scripts/daily-summary/daily-summary.ts +846 -0
- package/plugins/sp/scripts/daily-summary/logger.ts +28 -0
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +223 -0
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +367 -0
- package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +132 -0
- package/plugins/sp/scripts/dogfood-testing/validate-report.ts +169 -0
- package/plugins/sp/scripts/feature-dev-precheck.mjs +171 -0
- package/plugins/sp/scripts/feature-dev-precheck.ts +238 -0
- package/plugins/sp/scripts/feature-sync-bounded.mjs +285 -0
- package/plugins/sp/scripts/feature-sync-bounded.ts +478 -0
- package/plugins/sp/scripts/history-anatomy-cache.mjs +902 -0
- package/plugins/sp/scripts/history-anatomy-cache.ts +1028 -0
- package/plugins/sp/scripts/idea-handoff.mjs +22 -0
- package/plugins/sp/scripts/idea-handoff.ts +44 -0
- package/plugins/sp/scripts/inline-pipeline-parity-check.ts +185 -0
- package/plugins/sp/scripts/inline-run-setup.ts +198 -0
- package/plugins/sp/scripts/pr-reviewing.mjs +769 -0
- package/plugins/sp/scripts/pr-reviewing.ts +925 -0
- package/plugins/sp/scripts/quality-gate.mjs +179 -0
- package/plugins/sp/scripts/quality-gate.ts +217 -0
- package/plugins/sp/scripts/script-contract-check.ts +319 -0
- package/plugins/sp/scripts/stage-registry-adapter.ts +1533 -0
- package/plugins/sp/scripts/surface-drift-inventory.ts +929 -0
- package/plugins/sp/scripts/task-evidence-precheck.ts +181 -0
- package/plugins/sp/scripts/task-size-precheck.ts +175 -0
- package/plugins/sp/scripts/transition-shim-check.ts +238 -0
- package/plugins/sp/scripts/validate-commands.ts +689 -0
- package/plugins/sp/scripts/validate-flag-contracts.ts +878 -0
- package/plugins/sp/scripts/verify-answer-lint.ts +530 -0
- package/plugins/sp/scripts/workflow-step-profile.mjs +316 -0
- package/plugins/sp/scripts/workflow-step-profile.ts +456 -0
- package/plugins/sp/scripts/wrapup-steps.mjs +373 -0
- package/plugins/sp/scripts/wrapup-steps.ts +466 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
import { refToDataUri } from './refs';
|
|
3
|
+
import type { ImageProvider, ImageRequest, ImageResult, ProviderFactory, ProviderOptions } from './types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* OpenRouter adapter (0115 R1) — port of
|
|
7
|
+
* vendors/baoyu-skills/skills/baoyu-image-gen/scripts/providers/openrouter.ts
|
|
8
|
+
* (MIT © 2026 Jim Liu). `CliArgs.size` becomes `ImageRequest.width/height`.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const DEFAULT_MODEL = 'google/gemini-3.1-flash-image';
|
|
12
|
+
const COMMON_ASPECT_RATIOS = ['1:1', '2:3', '3:2', '3:4', '4:3', '4:5', '5:4', '9:16', '16:9', '21:9'];
|
|
13
|
+
const GEMINI_EXTENDED_ASPECT_RATIOS = ['1:4', '4:1', '1:8', '8:1'];
|
|
14
|
+
|
|
15
|
+
interface OpenRouterImageEntry {
|
|
16
|
+
image_url?: string | { url?: string | null } | null;
|
|
17
|
+
imageUrl?: string | { url?: string | null } | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface OpenRouterMessagePart extends OpenRouterImageEntry {
|
|
21
|
+
type?: string;
|
|
22
|
+
text?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface OpenRouterResponse {
|
|
26
|
+
choices?: Array<{
|
|
27
|
+
finish_reason?: string | null;
|
|
28
|
+
native_finish_reason?: string | null;
|
|
29
|
+
message?: {
|
|
30
|
+
images?: OpenRouterImageEntry[];
|
|
31
|
+
content?: string | OpenRouterMessagePart[] | null;
|
|
32
|
+
};
|
|
33
|
+
}>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function normalizeModelId(model: string): string {
|
|
37
|
+
return model.trim().toLowerCase().split(':')[0] as string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function isTextAndImageModel(model: string): boolean {
|
|
41
|
+
const normalized = normalizeModelId(model);
|
|
42
|
+
if (normalized === 'openrouter/auto') return true;
|
|
43
|
+
if (normalized.startsWith('google/gemini-') && normalized.includes('image')) return true;
|
|
44
|
+
if (normalized.startsWith('openai/gpt-') && normalized.includes('image')) return true;
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getSupportedAspectRatios(model: string): Set<string> {
|
|
49
|
+
const normalized = normalizeModelId(model);
|
|
50
|
+
if (normalized !== 'google/gemini-3.1-flash-image' && normalized !== 'google/gemini-3.1-flash-image-preview') {
|
|
51
|
+
return new Set(COMMON_ASPECT_RATIOS);
|
|
52
|
+
}
|
|
53
|
+
return new Set([...COMMON_ASPECT_RATIOS, ...GEMINI_EXTENDED_ASPECT_RATIOS]);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function gcd(a: number, b: number): number {
|
|
57
|
+
let x = Math.abs(a);
|
|
58
|
+
let y = Math.abs(b);
|
|
59
|
+
while (y !== 0) {
|
|
60
|
+
const next = x % y;
|
|
61
|
+
x = y;
|
|
62
|
+
y = next;
|
|
63
|
+
}
|
|
64
|
+
return x || 1;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function inferAspectRatio(size: string | null): string | null {
|
|
68
|
+
if (size === null) return null;
|
|
69
|
+
const parsed = size.match(/^(\d+)\s*[xX]\s*(\d+)$/);
|
|
70
|
+
if (parsed === null) return null;
|
|
71
|
+
const width = Number.parseInt(parsed[1] as string, 10);
|
|
72
|
+
const height = Number.parseInt(parsed[2] as string, 10);
|
|
73
|
+
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) return null;
|
|
74
|
+
const divisor = gcd(width, height);
|
|
75
|
+
return `${width / divisor}:${height / divisor}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function inferImageSize(size: string | null): '1K' | '2K' | '4K' | null {
|
|
79
|
+
if (size === null) return null;
|
|
80
|
+
const parsed = size.match(/^(\d+)\s*[xX]\s*(\d+)$/);
|
|
81
|
+
if (parsed === null) return null;
|
|
82
|
+
const width = Number.parseInt(parsed[1] as string, 10);
|
|
83
|
+
const height = Number.parseInt(parsed[2] as string, 10);
|
|
84
|
+
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) return null;
|
|
85
|
+
const longestEdge = Math.max(width, height);
|
|
86
|
+
if (longestEdge <= 1024) return '1K';
|
|
87
|
+
if (longestEdge <= 2048) return '2K';
|
|
88
|
+
return '4K';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function getImageSize(req: Pick<ImageRequest, 'width' | 'height' | 'quality'>): '1K' | '2K' | '4K' | null {
|
|
92
|
+
if (req.width !== undefined && req.height !== undefined) return inferImageSize(`${req.width}x${req.height}`);
|
|
93
|
+
if (req.quality === 'normal') return '1K';
|
|
94
|
+
if (req.quality === '2k') return '2K';
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function getAspectRatio(preferred: string | null, size: string | null, model: string): string | null {
|
|
99
|
+
if (preferred !== null) return preferred;
|
|
100
|
+
const inferred = inferAspectRatio(size);
|
|
101
|
+
if (inferred === null || !getSupportedAspectRatios(model).has(inferred)) return null;
|
|
102
|
+
return inferred;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Reject explicit ratios (or width/height ratios) the model cannot honour, before spend. */
|
|
106
|
+
export function validateAspectRatio(model: string, aspectRatio: string | null, size: string | null): void {
|
|
107
|
+
const requested = aspectRatio ?? inferAspectRatio(size);
|
|
108
|
+
if (requested === null) return;
|
|
109
|
+
const supported = getSupportedAspectRatios(model);
|
|
110
|
+
if (supported.has(requested)) return;
|
|
111
|
+
const requestedValue =
|
|
112
|
+
aspectRatio !== null ? `aspect ratio ${requested}` : `size ${size} (aspect ratio ${requested})`;
|
|
113
|
+
throw new Error(
|
|
114
|
+
`OpenRouter model ${model} does not support ${requestedValue}. Supported values: ${Array.from(supported).join(', ')}`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function buildContent(prompt: string, referenceImages: string[]): string | Array<Record<string, unknown>> {
|
|
119
|
+
if (referenceImages.length === 0) return prompt;
|
|
120
|
+
const content: Array<Record<string, unknown>> = [{ type: 'text', text: prompt }];
|
|
121
|
+
for (const imageUrl of referenceImages) {
|
|
122
|
+
content.push({ type: 'image_url', image_url: { url: imageUrl } });
|
|
123
|
+
}
|
|
124
|
+
return content;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function extractImageUrl(entry: OpenRouterImageEntry | OpenRouterMessagePart): string | null {
|
|
128
|
+
const value = entry.image_url ?? entry.imageUrl;
|
|
129
|
+
if (value === null || value === undefined) return null;
|
|
130
|
+
if (typeof value === 'string') return value;
|
|
131
|
+
return value.url ?? null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function decodeDataUrl(value: string): Uint8Array | null {
|
|
135
|
+
const match = value.match(/^data:image\/[^;]+;base64,([A-Za-z0-9+/=]+)$/);
|
|
136
|
+
if (match === null) return null;
|
|
137
|
+
return Uint8Array.from(Buffer.from(match[1] as string, 'base64'));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function downloadImage(value: string, fetchFn: typeof fetch): Promise<Uint8Array> {
|
|
141
|
+
const inline = decodeDataUrl(value);
|
|
142
|
+
if (inline !== null) return inline;
|
|
143
|
+
if (value.startsWith('http://') || value.startsWith('https://')) {
|
|
144
|
+
const response = await fetchFn(value);
|
|
145
|
+
if (!response.ok) throw new Error(`Failed to download OpenRouter image: ${response.status}`);
|
|
146
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
147
|
+
}
|
|
148
|
+
return Uint8Array.from(Buffer.from(value, 'base64'));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export async function extractImageFromResponse(
|
|
152
|
+
result: OpenRouterResponse,
|
|
153
|
+
fetchFn: typeof fetch = fetch,
|
|
154
|
+
): Promise<Uint8Array> {
|
|
155
|
+
const choice = result.choices?.[0];
|
|
156
|
+
const message = choice?.message;
|
|
157
|
+
for (const image of message?.images ?? []) {
|
|
158
|
+
const imageUrl = extractImageUrl(image);
|
|
159
|
+
if (imageUrl !== null) return downloadImage(imageUrl, fetchFn);
|
|
160
|
+
}
|
|
161
|
+
if (Array.isArray(message?.content)) {
|
|
162
|
+
for (const item of message.content) {
|
|
163
|
+
const imageUrl = extractImageUrl(item);
|
|
164
|
+
if (imageUrl !== null) return downloadImage(imageUrl, fetchFn);
|
|
165
|
+
if (item.type === 'text' && item.text !== undefined) {
|
|
166
|
+
const inline = decodeDataUrl(item.text);
|
|
167
|
+
if (inline !== null) return inline;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} else if (typeof message?.content === 'string') {
|
|
171
|
+
const inline = decodeDataUrl(message.content);
|
|
172
|
+
if (inline !== null) return inline;
|
|
173
|
+
}
|
|
174
|
+
const finishReason = choice?.native_finish_reason || choice?.finish_reason || 'unknown';
|
|
175
|
+
throw new Error(`No image in OpenRouter response (finish_reason=${finishReason})`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function buildRequestBody(
|
|
179
|
+
prompt: string,
|
|
180
|
+
model: string,
|
|
181
|
+
req: ImageRequest,
|
|
182
|
+
referenceImages: string[],
|
|
183
|
+
): Record<string, unknown> {
|
|
184
|
+
validateAspectRatio(
|
|
185
|
+
model,
|
|
186
|
+
req.aspectRatio ?? null,
|
|
187
|
+
req.width !== undefined && req.height !== undefined ? `${req.width}x${req.height}` : null,
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const imageConfig: Record<string, string> = {};
|
|
191
|
+
const imageSize = getImageSize(req);
|
|
192
|
+
if (imageSize !== null) imageConfig.image_size = imageSize;
|
|
193
|
+
const aspectRatio = getAspectRatio(
|
|
194
|
+
req.aspectRatio ?? null,
|
|
195
|
+
req.width !== undefined && req.height !== undefined ? `${req.width}x${req.height}` : null,
|
|
196
|
+
model,
|
|
197
|
+
);
|
|
198
|
+
if (aspectRatio !== null) imageConfig.aspect_ratio = aspectRatio;
|
|
199
|
+
|
|
200
|
+
const body: Record<string, unknown> = {
|
|
201
|
+
messages: [{ role: 'user', content: buildContent(prompt, referenceImages) }],
|
|
202
|
+
modalities: isTextAndImageModel(model) ? ['image', 'text'] : ['image'],
|
|
203
|
+
stream: false,
|
|
204
|
+
};
|
|
205
|
+
if (Object.keys(imageConfig).length > 0) {
|
|
206
|
+
body.image_config = imageConfig;
|
|
207
|
+
body.provider = { require_parameters: true };
|
|
208
|
+
}
|
|
209
|
+
return body;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export const createOpenrouter: ProviderFactory = (options?: ProviderOptions): ImageProvider => {
|
|
213
|
+
const fetchFn = options?.fetch ?? fetch;
|
|
214
|
+
const env = options?.env ?? process.env;
|
|
215
|
+
const readEnv = (key: string): string => env[key] ?? '';
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
name: 'openrouter',
|
|
219
|
+
defaultModel: () => DEFAULT_MODEL,
|
|
220
|
+
isAvailable: () => readEnv('OPENROUTER_API_KEY') !== '',
|
|
221
|
+
supportsReferenceImages: () => true,
|
|
222
|
+
async generate(req: ImageRequest): Promise<ImageResult> {
|
|
223
|
+
const apiKey = readEnv('OPENROUTER_API_KEY');
|
|
224
|
+
if (apiKey === '')
|
|
225
|
+
throw new Error('OPENROUTER_API_KEY is required. Get one at https://openrouter.ai/settings/keys');
|
|
226
|
+
|
|
227
|
+
const headers: Record<string, string> = {
|
|
228
|
+
'content-type': 'application/json',
|
|
229
|
+
authorization: `Bearer ${apiKey}`,
|
|
230
|
+
};
|
|
231
|
+
const referer = readEnv('OPENROUTER_HTTP_REFERER').trim();
|
|
232
|
+
if (referer !== '') headers['HTTP-Referer'] = referer;
|
|
233
|
+
const title = readEnv('OPENROUTER_TITLE').trim();
|
|
234
|
+
if (title !== '') {
|
|
235
|
+
headers['X-OpenRouter-Title'] = title;
|
|
236
|
+
headers['X-Title'] = title;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const referenceImages: string[] = [];
|
|
240
|
+
for (const refPath of req.referenceImages ?? []) {
|
|
241
|
+
referenceImages.push(await refToDataUri(refPath));
|
|
242
|
+
}
|
|
243
|
+
const body = { model: req.model, ...buildRequestBody(req.prompt, req.model, req, referenceImages) };
|
|
244
|
+
const base = (readEnv('OPENROUTER_BASE_URL') || 'https://openrouter.ai/api/v1').replace(/\/+$/g, '');
|
|
245
|
+
const response = await fetchFn(`${base}/chat/completions`, {
|
|
246
|
+
method: 'POST',
|
|
247
|
+
headers,
|
|
248
|
+
body: JSON.stringify(body),
|
|
249
|
+
});
|
|
250
|
+
if (!response.ok) {
|
|
251
|
+
throw new Error(`OpenRouter API error (${response.status}): ${await response.text()}`);
|
|
252
|
+
}
|
|
253
|
+
const bytes = await extractImageFromResponse((await response.json()) as OpenRouterResponse, fetchFn);
|
|
254
|
+
return { bytes, extension: 'png', provider: 'openrouter', model: req.model };
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { extname } from 'node:path';
|
|
2
|
+
import { readBytes } from '../bytes';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Shared reference-image plumbing for the ported baoyu adapters (task 0115): local
|
|
6
|
+
* paths ride the wire as `data:` URIs, remote http(s) URLs pass through untouched,
|
|
7
|
+
* and MIME detection follows each provider's own extension table.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export function mimeForPath(path: string): string {
|
|
11
|
+
const ext = extname(path).toLowerCase();
|
|
12
|
+
if (ext === '.jpg' || ext === '.jpeg') return 'image/jpeg';
|
|
13
|
+
if (ext === '.webp') return 'image/webp';
|
|
14
|
+
if (ext === '.gif') return 'image/gif';
|
|
15
|
+
if (ext === '.bmp') return 'image/bmp';
|
|
16
|
+
return 'image/png';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Lift a reference image to a `data:` URI; http(s) URLs ride verbatim. */
|
|
20
|
+
export async function refToDataUri(path: string): Promise<string> {
|
|
21
|
+
if (/^https?:\/\//i.test(path)) return path;
|
|
22
|
+
const bytes = await readBytes(path);
|
|
23
|
+
return `data:${mimeForPath(path)};base64,${Buffer.from(bytes).toString('base64')}`;
|
|
24
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { refToDataUri } from './refs';
|
|
2
|
+
import { extensionFromUrl } from './seedream';
|
|
3
|
+
import type { ImageProvider, ImageRequest, ImageResult, ProviderFactory, ProviderOptions } from './types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Replicate adapter (0115 R1) — port of
|
|
7
|
+
* vendors/baoyu-skills/skills/baoyu-image-gen/scripts/providers/replicate.ts
|
|
8
|
+
* (MIT © 2026 Jim Liu). `CliArgs.size` becomes width/height; per-family input
|
|
9
|
+
* builders, polling and output extraction ride verbatim.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const DEFAULT_MODEL = 'google/nano-banana-2';
|
|
13
|
+
const SYNC_WAIT_SECONDS = 60;
|
|
14
|
+
const POLL_INTERVAL_MS = 2000;
|
|
15
|
+
const MAX_POLL_MS = 300_000;
|
|
16
|
+
const DOCUMENTED_REPLICATE_ASPECT_RATIOS = new Set([
|
|
17
|
+
'1:1',
|
|
18
|
+
'2:3',
|
|
19
|
+
'3:2',
|
|
20
|
+
'3:4',
|
|
21
|
+
'4:3',
|
|
22
|
+
'5:4',
|
|
23
|
+
'4:5',
|
|
24
|
+
'9:16',
|
|
25
|
+
'16:9',
|
|
26
|
+
'21:9',
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
export type ReplicateModelFamily =
|
|
30
|
+
| 'nano-banana'
|
|
31
|
+
| 'seedream45'
|
|
32
|
+
| 'seedream5lite'
|
|
33
|
+
| 'wan27image'
|
|
34
|
+
| 'wan27imagepro'
|
|
35
|
+
| 'unknown';
|
|
36
|
+
|
|
37
|
+
type PixelSize = { width: number; height: number };
|
|
38
|
+
type Seedream45Size = '2K' | '4K' | PixelSize;
|
|
39
|
+
|
|
40
|
+
export function parseModelId(model: string): { owner: string; name: string; version: string | null } {
|
|
41
|
+
const [ownerName, version] = model.split(':');
|
|
42
|
+
const parts = (ownerName ?? '').split('/');
|
|
43
|
+
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
|
44
|
+
throw new Error(`Invalid Replicate model format: "${model}". Expected "owner/name" or "owner/name:version".`);
|
|
45
|
+
}
|
|
46
|
+
return { owner: parts[0], name: parts[1], version: version ?? null };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function getModelFamily(model: string): ReplicateModelFamily {
|
|
50
|
+
const normalized = model.trim().toLowerCase().split(':')[0] as string;
|
|
51
|
+
if (['google/nano-banana', 'google/nano-banana-pro', 'google/nano-banana-2'].includes(normalized))
|
|
52
|
+
return 'nano-banana';
|
|
53
|
+
if (normalized === 'bytedance/seedream-4.5') return 'seedream45';
|
|
54
|
+
if (normalized === 'bytedance/seedream-5-lite') return 'seedream5lite';
|
|
55
|
+
if (normalized === 'wan-video/wan-2.7-image') return 'wan27image';
|
|
56
|
+
if (normalized === 'wan-video/wan-2.7-image-pro') return 'wan27imagepro';
|
|
57
|
+
return 'unknown';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function validateDocumentedAspectRatio(model: string, aspectRatio: string): void {
|
|
61
|
+
if (aspectRatio === 'match_input_image' || DOCUMENTED_REPLICATE_ASPECT_RATIOS.has(aspectRatio)) return;
|
|
62
|
+
throw new Error(
|
|
63
|
+
`Replicate model ${model} does not support aspect ratio ${aspectRatio}. Supported values: ${Array.from(DOCUMENTED_REPLICATE_ASPECT_RATIOS).join(', ')}`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function getNanoBananaResolution(req: ImageRequest): '1K' | '2K' {
|
|
68
|
+
if (req.width !== undefined && req.height !== undefined) {
|
|
69
|
+
const longestEdge = Math.max(req.width, req.height);
|
|
70
|
+
if (longestEdge <= 1024) return '1K';
|
|
71
|
+
if (longestEdge <= 2048) return '2K';
|
|
72
|
+
throw new Error('Replicate nano-banana only supports sizes that map to 1K or 2K output.');
|
|
73
|
+
}
|
|
74
|
+
return req.quality === 'normal' ? '1K' : '2K';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function resolveSeedream45Size(req: ImageRequest): Seedream45Size {
|
|
78
|
+
if (req.width !== undefined && req.height !== undefined) {
|
|
79
|
+
if (req.width < 1024 || req.width > 4096 || req.height < 1024 || req.height > 4096) {
|
|
80
|
+
throw new Error('Replicate Seedream 4.5 custom --size must keep width and height between 1024 and 4096.');
|
|
81
|
+
}
|
|
82
|
+
return { width: req.width, height: req.height };
|
|
83
|
+
}
|
|
84
|
+
return req.quality === 'normal' ? '2K' : '4K';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function resolveSeedream5LiteSize(req: ImageRequest): '2K' | '3K' {
|
|
88
|
+
return req.quality === 'normal' ? '2K' : '3K';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function resolveWanSizeFromAspectRatio(aspectRatio: string, maxDimension: number): string {
|
|
92
|
+
const match = aspectRatio.trim().match(/^(\d+)\s*:\s*(\d+)$/);
|
|
93
|
+
if (match === null) throw new Error(`Replicate Wan aspect ratio must be in W:H format, got ${aspectRatio}.`);
|
|
94
|
+
const ratioWidth = Number.parseInt(match[1] as string, 10);
|
|
95
|
+
const ratioHeight = Number.parseInt(match[2] as string, 10);
|
|
96
|
+
if (!Number.isFinite(ratioWidth) || !Number.isFinite(ratioHeight) || ratioWidth <= 0 || ratioHeight <= 0) {
|
|
97
|
+
throw new Error(`Replicate Wan aspect ratio must be in W:H format, got ${aspectRatio}.`);
|
|
98
|
+
}
|
|
99
|
+
const scale = Math.min(maxDimension / ratioWidth, maxDimension / ratioHeight);
|
|
100
|
+
const width = Math.max(1, Math.floor(ratioWidth * scale));
|
|
101
|
+
const height = Math.max(1, Math.floor(ratioHeight * scale));
|
|
102
|
+
return `${width}*${height}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function resolveWanSize(family: 'wan27image' | 'wan27imagepro', req: ImageRequest): '1K' | '2K' | '4K' | string {
|
|
106
|
+
const referenceMode = (req.referenceImages?.length ?? 0) > 0;
|
|
107
|
+
const maxDimension = family === 'wan27imagepro' && !referenceMode ? 4096 : 2048;
|
|
108
|
+
if (req.width !== undefined && req.height !== undefined) {
|
|
109
|
+
const parsed = { width: req.width, height: req.height };
|
|
110
|
+
if (
|
|
111
|
+
req.width === undefined ||
|
|
112
|
+
req.height === undefined ||
|
|
113
|
+
parsed.width > maxDimension ||
|
|
114
|
+
parsed.height > maxDimension
|
|
115
|
+
) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`Replicate ${family === 'wan27imagepro' ? 'Wan 2.7 Image Pro' : 'Wan 2.7 Image'} custom --size must keep width and height at or below ${maxDimension}px in the current mode.`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
return `${parsed.width}*${parsed.height}`;
|
|
121
|
+
}
|
|
122
|
+
if (req.aspectRatio !== undefined) {
|
|
123
|
+
return resolveWanSizeFromAspectRatio(req.aspectRatio, req.quality === 'normal' ? 1024 : 2048);
|
|
124
|
+
}
|
|
125
|
+
return req.quality === 'normal' ? '1K' : '2K';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function buildInput(
|
|
129
|
+
req: Pick<ImageRequest, 'model' | 'quality' | 'aspectRatio' | 'width' | 'height' | 'referenceImages'> & {
|
|
130
|
+
prompt: string;
|
|
131
|
+
},
|
|
132
|
+
): Record<string, unknown> {
|
|
133
|
+
const family = getModelFamily(req.model);
|
|
134
|
+
const referenceImages = req.referenceImages ?? [];
|
|
135
|
+
|
|
136
|
+
if (family === 'nano-banana') {
|
|
137
|
+
const input: Record<string, unknown> = {
|
|
138
|
+
prompt: req.prompt,
|
|
139
|
+
resolution: getNanoBananaResolution(req),
|
|
140
|
+
output_format: 'png',
|
|
141
|
+
};
|
|
142
|
+
if (req.aspectRatio !== undefined) {
|
|
143
|
+
validateDocumentedAspectRatio(req.model, req.aspectRatio);
|
|
144
|
+
input.aspect_ratio = req.aspectRatio;
|
|
145
|
+
} else if (referenceImages.length > 0) {
|
|
146
|
+
input.aspect_ratio = 'match_input_image';
|
|
147
|
+
}
|
|
148
|
+
if (referenceImages.length > 0) input.image_input = referenceImages;
|
|
149
|
+
return input;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (family === 'seedream45' || family === 'seedream5lite') {
|
|
153
|
+
const size = family === 'seedream45' ? resolveSeedream45Size(req) : resolveSeedream5LiteSize(req);
|
|
154
|
+
const input: Record<string, unknown> = { prompt: req.prompt };
|
|
155
|
+
if (family === 'seedream45' && typeof size === 'object') {
|
|
156
|
+
input.size = 'custom';
|
|
157
|
+
input.width = size.width;
|
|
158
|
+
input.height = size.height;
|
|
159
|
+
} else {
|
|
160
|
+
input.size = size as string;
|
|
161
|
+
}
|
|
162
|
+
if (referenceImages.length > 0) input.image_input = referenceImages;
|
|
163
|
+
if (req.aspectRatio !== undefined) {
|
|
164
|
+
validateDocumentedAspectRatio(req.model, req.aspectRatio);
|
|
165
|
+
input.aspect_ratio = req.aspectRatio;
|
|
166
|
+
} else if (referenceImages.length > 0 && family === 'seedream45') {
|
|
167
|
+
input.aspect_ratio = 'match_input_image';
|
|
168
|
+
}
|
|
169
|
+
return input;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (family === 'wan27image' || family === 'wan27imagepro') {
|
|
173
|
+
const input: Record<string, unknown> = { prompt: req.prompt, size: resolveWanSize(family, req) };
|
|
174
|
+
if (referenceImages.length > 0) input.images = referenceImages;
|
|
175
|
+
return input;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return { prompt: req.prompt };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export interface PredictionResponse {
|
|
182
|
+
id?: string;
|
|
183
|
+
status: string;
|
|
184
|
+
output: unknown;
|
|
185
|
+
error: string | null;
|
|
186
|
+
urls?: { get?: string };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function extractOutputUrl(prediction: PredictionResponse): string {
|
|
190
|
+
const output = prediction.output;
|
|
191
|
+
if (typeof output === 'string') return output;
|
|
192
|
+
if (Array.isArray(output)) {
|
|
193
|
+
if (output.length !== 1) {
|
|
194
|
+
throw new Error(
|
|
195
|
+
`Replicate returned ${output.length} outputs, but kk image-gen currently supports saving exactly one image per request.`,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
const first = output[0];
|
|
199
|
+
if (typeof first === 'string') return first;
|
|
200
|
+
}
|
|
201
|
+
if (output !== null && typeof output === 'object' && 'url' in output) {
|
|
202
|
+
const url = (output as Record<string, unknown>).url;
|
|
203
|
+
if (typeof url === 'string') return url;
|
|
204
|
+
}
|
|
205
|
+
throw new Error(`Unexpected Replicate output format: ${JSON.stringify(output)}`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export const createReplicate: ProviderFactory = (options?: ProviderOptions): ImageProvider => {
|
|
209
|
+
const fetchFn = options?.fetch ?? fetch;
|
|
210
|
+
const env = options?.env ?? process.env;
|
|
211
|
+
const readEnv = (key: string): string => env[key] ?? '';
|
|
212
|
+
|
|
213
|
+
async function downloadImage(url: string): Promise<Uint8Array> {
|
|
214
|
+
const res = await fetchFn(url);
|
|
215
|
+
if (!res.ok) throw new Error(`Failed to download image from Replicate: ${res.status}`);
|
|
216
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function pollPrediction(apiToken: string, getUrl: string): Promise<PredictionResponse> {
|
|
220
|
+
const start = Date.now();
|
|
221
|
+
while (Date.now() - start < MAX_POLL_MS) {
|
|
222
|
+
const res = await fetchFn(getUrl, { headers: { authorization: `Bearer ${apiToken}` } });
|
|
223
|
+
if (!res.ok) throw new Error(`Replicate poll error (${res.status}): ${await res.text()}`);
|
|
224
|
+
const prediction = (await res.json()) as PredictionResponse;
|
|
225
|
+
if (prediction.status === 'succeeded') return prediction;
|
|
226
|
+
if (prediction.status === 'failed' || prediction.status === 'canceled') {
|
|
227
|
+
throw new Error(`Replicate prediction ${prediction.status}: ${prediction.error ?? 'unknown error'}`);
|
|
228
|
+
}
|
|
229
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
230
|
+
}
|
|
231
|
+
throw new Error(`Replicate prediction timed out after ${MAX_POLL_MS / 1000}s`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
name: 'replicate',
|
|
236
|
+
defaultModel: () => DEFAULT_MODEL,
|
|
237
|
+
isAvailable: () => readEnv('REPLICATE_API_TOKEN') !== '',
|
|
238
|
+
supportsReferenceImages: () => true,
|
|
239
|
+
async generate(req: ImageRequest): Promise<ImageResult> {
|
|
240
|
+
const apiToken = readEnv('REPLICATE_API_TOKEN');
|
|
241
|
+
if (apiToken === '') {
|
|
242
|
+
throw new Error('REPLICATE_API_TOKEN is required. Get one at https://replicate.com/account/api-tokens');
|
|
243
|
+
}
|
|
244
|
+
const model = parseModelId(req.model);
|
|
245
|
+
const refDataUrls: string[] = [];
|
|
246
|
+
for (const refPath of req.referenceImages ?? []) {
|
|
247
|
+
refDataUrls.push(await refToDataUri(refPath));
|
|
248
|
+
}
|
|
249
|
+
const input = buildInput({ ...req, referenceImages: refDataUrls });
|
|
250
|
+
const baseUrl = (readEnv('REPLICATE_BASE_URL') || 'https://api.replicate.com').replace(/\/+$/g, '');
|
|
251
|
+
const url =
|
|
252
|
+
model.version !== null
|
|
253
|
+
? `${baseUrl}/v1/predictions`
|
|
254
|
+
: `${baseUrl}/v1/models/${model.owner}/${model.name}/predictions`;
|
|
255
|
+
const body: Record<string, unknown> = { input };
|
|
256
|
+
if (model.version !== null) body.version = model.version;
|
|
257
|
+
const headers: Record<string, string> = {
|
|
258
|
+
authorization: `Bearer ${apiToken}`,
|
|
259
|
+
'content-type': 'application/json',
|
|
260
|
+
Prefer: `wait=${SYNC_WAIT_SECONDS}`,
|
|
261
|
+
};
|
|
262
|
+
let prediction = (await (
|
|
263
|
+
await fetchFn(url, { method: 'POST', headers, body: JSON.stringify(body) })
|
|
264
|
+
).json()) as PredictionResponse;
|
|
265
|
+
if (prediction.status !== 'succeeded') {
|
|
266
|
+
if (prediction.urls?.get === undefined)
|
|
267
|
+
throw new Error('Replicate prediction did not return a poll URL');
|
|
268
|
+
prediction = await pollPrediction(apiToken, prediction.urls.get);
|
|
269
|
+
}
|
|
270
|
+
const outputUrl = extractOutputUrl(prediction);
|
|
271
|
+
return {
|
|
272
|
+
bytes: await downloadImage(outputUrl),
|
|
273
|
+
extension: extensionFromUrl(outputUrl),
|
|
274
|
+
provider: 'replicate',
|
|
275
|
+
model: req.model,
|
|
276
|
+
};
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
};
|