@gobing-ai/knowledge-kit 0.0.13 → 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.
Files changed (109) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/dist/index.js +20 -0
  3. package/package.json +1 -1
  4. package/plugins/generations/content-gen/dist/index.js +20 -0
  5. package/plugins/generations/core-facts-gen/dist/index.js +20 -0
  6. package/plugins/generations/daily-article-gen/dist/index.js +29 -1
  7. package/plugins/generations/daily-article-gen/src/index.ts +13 -1
  8. package/plugins/generations/dailynews-gen/dist/index.js +20 -0
  9. package/plugins/generations/episode-plan-gen/dist/index.js +24 -0
  10. package/plugins/generations/episode-plan-gen/src/index.ts +11 -0
  11. package/plugins/generations/image-gen/dist/index.js +2296 -53
  12. package/plugins/generations/image-gen/src/providers/agnes.ts +110 -0
  13. package/plugins/generations/image-gen/src/providers/azure.ts +153 -0
  14. package/plugins/generations/image-gen/src/providers/codex-cli.ts +170 -0
  15. package/plugins/generations/image-gen/src/providers/dashscope.ts +485 -0
  16. package/plugins/generations/image-gen/src/providers/google.ts +268 -0
  17. package/plugins/generations/image-gen/src/providers/huggingface.ts +59 -0
  18. package/plugins/generations/image-gen/src/providers/jimeng.ts +259 -0
  19. package/plugins/generations/image-gen/src/providers/minimax.ts +171 -0
  20. package/plugins/generations/image-gen/src/providers/openai.ts +319 -0
  21. package/plugins/generations/image-gen/src/providers/openrouter.ts +257 -0
  22. package/plugins/generations/image-gen/src/providers/refs.ts +24 -0
  23. package/plugins/generations/image-gen/src/providers/replicate.ts +279 -0
  24. package/plugins/generations/image-gen/src/providers/seedream.ts +1 -1
  25. package/plugins/generations/image-gen/src/providers/types.ts +52 -51
  26. package/plugins/generations/image-gen/src/providers/zai.ts +237 -0
  27. package/plugins/generations/news-report-gen/dist/index.js +22193 -0
  28. package/plugins/generations/news-report-gen/package.json +17 -0
  29. package/plugins/generations/news-report-gen/plugin.json +7 -0
  30. package/plugins/generations/news-report-gen/src/index.ts +308 -0
  31. package/plugins/generations/news-report-gen/tsconfig.json +4 -0
  32. package/plugins/generations/omni-voice-gen/Makefile +14 -0
  33. package/plugins/generations/omni-voice-gen/README.md +112 -0
  34. package/plugins/generations/omni-voice-gen/bin/omni-voice-gen +2 -0
  35. package/plugins/generations/omni-voice-gen/dist/omni-voice-gen-prr8skpb. +2 -0
  36. package/plugins/generations/omni-voice-gen/dist/omni-voice-gen.js +6 -0
  37. package/plugins/generations/omni-voice-gen/plugin.json +6 -0
  38. package/plugins/generations/omni-voice-gen/profiles.json +12 -0
  39. package/plugins/generations/omni-voice-gen/pyproject.toml +25 -0
  40. package/plugins/generations/omni-voice-gen/scripts/coverage_gate.py +74 -0
  41. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/__init__.py +1 -0
  42. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/__main__.py +39 -0
  43. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/audio.py +190 -0
  44. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/backend.py +150 -0
  45. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/contract.py +76 -0
  46. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/mp3.py +60 -0
  47. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/pipeline.py +289 -0
  48. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/profiles.py +100 -0
  49. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/qc.py +234 -0
  50. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/voicescript.py +352 -0
  51. package/plugins/generations/omni-voice-gen/uv.lock +3510 -0
  52. package/plugins/generations/voice-gen/dist/index.js +30 -2
  53. package/plugins/generations/voice-gen/src/index.ts +16 -1
  54. package/plugins/generations/voice-gen/src/voicebox-client.ts +3 -1
  55. package/plugins/ingestions/aihot-ingest/dist/index.js +20 -0
  56. package/plugins/ingestions/horizon-ingest/dist/index.js +20 -0
  57. package/plugins/ingestions/last30days-ingest/dist/index.js +20 -0
  58. package/plugins/ingestions/web-search/dist/index.js +20 -0
  59. package/plugins/kk/commands/image-extract.md +40 -0
  60. package/plugins/kk/commands/image-generate.md +18 -1
  61. package/plugins/kk/plugin.json +1 -1
  62. package/plugins/kk/skills/image-authoring/SKILL.md +10 -3
  63. package/plugins/kk/skills/image-authoring/references/format-drafting.md +57 -0
  64. package/plugins/kk/skills/image-authoring/references/style-extraction.md +14 -9
  65. package/plugins/kk/workflows/kk-daily-ai-voice.yaml +130 -30
  66. package/plugins/publishings/emdash-pub/dist/index.js +20 -0
  67. package/plugins/publishings/podcast-pub/dist/index.js +57 -3
  68. package/plugins/publishings/podcast-pub/src/index.ts +18 -2
  69. package/plugins/publishings/podcast-pub/src/show-notes.ts +56 -9
  70. package/plugins/publishings/qiita-pub/dist/index.js +20 -0
  71. package/plugins/publishings/surfdash-pub/dist/index.js +96 -6
  72. package/plugins/publishings/surfdash-pub/src/index.ts +109 -9
  73. package/plugins/publishings/zenn-pub/dist/index.js +20 -0
  74. package/plugins/sp/scripts/batch-preflight.mjs +346 -0
  75. package/plugins/sp/scripts/batch-preflight.ts +459 -0
  76. package/plugins/sp/scripts/daily-summary/daily-summary.mjs +615 -0
  77. package/plugins/sp/scripts/daily-summary/daily-summary.ts +846 -0
  78. package/plugins/sp/scripts/daily-summary/logger.ts +28 -0
  79. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +223 -0
  80. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +367 -0
  81. package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +132 -0
  82. package/plugins/sp/scripts/dogfood-testing/validate-report.ts +169 -0
  83. package/plugins/sp/scripts/feature-dev-precheck.mjs +171 -0
  84. package/plugins/sp/scripts/feature-dev-precheck.ts +238 -0
  85. package/plugins/sp/scripts/feature-sync-bounded.mjs +285 -0
  86. package/plugins/sp/scripts/feature-sync-bounded.ts +478 -0
  87. package/plugins/sp/scripts/history-anatomy-cache.mjs +902 -0
  88. package/plugins/sp/scripts/history-anatomy-cache.ts +1028 -0
  89. package/plugins/sp/scripts/idea-handoff.mjs +22 -0
  90. package/plugins/sp/scripts/idea-handoff.ts +44 -0
  91. package/plugins/sp/scripts/inline-pipeline-parity-check.ts +185 -0
  92. package/plugins/sp/scripts/inline-run-setup.ts +198 -0
  93. package/plugins/sp/scripts/pr-reviewing.mjs +769 -0
  94. package/plugins/sp/scripts/pr-reviewing.ts +925 -0
  95. package/plugins/sp/scripts/quality-gate.mjs +179 -0
  96. package/plugins/sp/scripts/quality-gate.ts +217 -0
  97. package/plugins/sp/scripts/script-contract-check.ts +319 -0
  98. package/plugins/sp/scripts/stage-registry-adapter.ts +1533 -0
  99. package/plugins/sp/scripts/surface-drift-inventory.ts +929 -0
  100. package/plugins/sp/scripts/task-evidence-precheck.ts +181 -0
  101. package/plugins/sp/scripts/task-size-precheck.ts +175 -0
  102. package/plugins/sp/scripts/transition-shim-check.ts +238 -0
  103. package/plugins/sp/scripts/validate-commands.ts +689 -0
  104. package/plugins/sp/scripts/validate-flag-contracts.ts +878 -0
  105. package/plugins/sp/scripts/verify-answer-lint.ts +530 -0
  106. package/plugins/sp/scripts/workflow-step-profile.mjs +316 -0
  107. package/plugins/sp/scripts/workflow-step-profile.ts +456 -0
  108. package/plugins/sp/scripts/wrapup-steps.mjs +373 -0
  109. package/plugins/sp/scripts/wrapup-steps.ts +466 -0
@@ -0,0 +1,268 @@
1
+ import { Buffer } from 'node:buffer';
2
+ import { execFileSync } from 'node:child_process';
3
+ import { readBytes } from '../bytes';
4
+ import { mimeForPath } from './refs';
5
+ import type { ImageProvider, ImageRequest, ImageResult, ProviderFactory, ProviderOptions } from './types';
6
+
7
+ /**
8
+ * Google Gemini / Imagen adapter (0115 R1) — port of
9
+ * vendors/baoyu-skills/skills/baoyu-image-gen/scripts/providers/google.ts
10
+ * (MIT © 2026 Jim Liu), mapped from `CliArgs` onto `ImageRequest`.
11
+ */
12
+
13
+ const GOOGLE_MULTIMODAL_MODELS = [
14
+ 'gemini-3-pro-image',
15
+ 'gemini-3.1-flash-image',
16
+ 'gemini-3-pro-image-preview',
17
+ 'gemini-3-flash-preview',
18
+ 'gemini-3.1-flash-image-preview',
19
+ ];
20
+ const GOOGLE_IMAGEN_MODELS = ['imagen-3.0-generate-002', 'imagen-3.0-generate-001'];
21
+
22
+ const DEFAULT_MODEL = 'gemini-3-pro-image';
23
+
24
+ export function normalizeGoogleModelId(model: string): string {
25
+ return model.startsWith('models/') ? model.slice('models/'.length) : model;
26
+ }
27
+
28
+ export function isGoogleMultimodal(model: string): boolean {
29
+ const normalized = normalizeGoogleModelId(model);
30
+ return GOOGLE_MULTIMODAL_MODELS.some((fragment) => normalized.includes(fragment));
31
+ }
32
+
33
+ export function isGoogleImagen(model: string): boolean {
34
+ const normalized = normalizeGoogleModelId(model);
35
+ return GOOGLE_IMAGEN_MODELS.some((fragment) => normalized.includes(fragment));
36
+ }
37
+
38
+ export function buildGoogleUrl(base: string, pathname: string): string {
39
+ const cleaned = base.replace(/\/+$/g, '').replace(/^\/+/g, '');
40
+ const cleanedPath = pathname.replace(/^\/+/g, '');
41
+ if (cleaned.endsWith('/v1beta')) return `${cleaned}/${cleanedPath}`;
42
+ return `${cleaned}/v1beta/${cleanedPath}`;
43
+ }
44
+
45
+ /** Aspect hint appended verbatim; 2k adds the resolution segment (baoyu buildPromptWithAspect). */
46
+ export function buildPromptWithAspect(
47
+ prompt: string,
48
+ aspectRatio: string | undefined,
49
+ quality: 'normal' | '2k' | undefined,
50
+ ): string {
51
+ let result = prompt;
52
+ if (aspectRatio !== undefined) result += ` Aspect ratio: ${aspectRatio}.`;
53
+ if (quality === '2k') result += ' High resolution 2048px.';
54
+ return result;
55
+ }
56
+
57
+ export function extractInlineImageData(response: {
58
+ candidates?: Array<{ content?: { parts?: Array<{ inlineData?: { data?: string } }> } }>;
59
+ }): string | null {
60
+ for (const candidate of response.candidates ?? []) {
61
+ for (const part of candidate.content?.parts ?? []) {
62
+ const data = part.inlineData?.data;
63
+ if (typeof data === 'string' && data.length > 0) return data;
64
+ }
65
+ }
66
+ return null;
67
+ }
68
+
69
+ interface PredictedEntry {
70
+ imageBytes?: unknown;
71
+ bytesBase64Encoded?: unknown;
72
+ data?: unknown;
73
+ image?: unknown;
74
+ }
75
+
76
+ export function extractPredictedImageData(response: {
77
+ predictions?: PredictedEntry[];
78
+ generatedImages?: PredictedEntry[];
79
+ }): string | null {
80
+ for (const candidate of [...(response.predictions ?? []), ...(response.generatedImages ?? [])]) {
81
+ if (candidate === null || typeof candidate !== 'object') continue;
82
+ const fields: string[] = [candidate.imageBytes, candidate.bytesBase64Encoded, candidate.data].flatMap(
83
+ (value) => (typeof value === 'string' && value.length > 0 ? [value] : []),
84
+ );
85
+ if (fields.length > 0) return fields[0] ?? null;
86
+ const image = candidate.image;
87
+ if (image !== null && typeof image === 'object') {
88
+ const record = image as PredictedEntry;
89
+ for (const value of [record.imageBytes, record.bytesBase64Encoded, record.data]) {
90
+ if (typeof value === 'string' && value.length > 0) return value;
91
+ }
92
+ }
93
+ }
94
+ return null;
95
+ }
96
+
97
+ function resolveSize(req: ImageRequest): '1K' | '2K' | '4K' | undefined {
98
+ if (req.width !== undefined && req.height !== undefined) {
99
+ return Math.max(req.width, req.height) <= 1024 ? '1K' : Math.max(req.width, req.height) <= 2048 ? '2K' : '4K';
100
+ }
101
+ return undefined;
102
+ }
103
+
104
+ function getHttpProxy(readEnv: (key: string) => string): string | null {
105
+ return (
106
+ readEnv('https_proxy') ||
107
+ readEnv('HTTPS_PROXY') ||
108
+ readEnv('http_proxy') ||
109
+ readEnv('HTTP_PROXY') ||
110
+ readEnv('ALL_PROXY') ||
111
+ null
112
+ );
113
+ }
114
+
115
+ interface GeminiResponse {
116
+ candidates?: Array<{ content?: { parts?: Array<{ inlineData?: { data?: string } }> } }>;
117
+ }
118
+
119
+ interface ImagenResponse {
120
+ predictions?: PredictedEntry[];
121
+ generatedImages?: PredictedEntry[];
122
+ }
123
+
124
+ export const createGoogle: ProviderFactory = (options?: ProviderOptions): ImageProvider => {
125
+ const fetchFn = options?.fetch ?? fetch;
126
+ const env = options?.env ?? process.env;
127
+ const readEnv = (key: string): string => env[key] ?? '';
128
+ const refModels = [
129
+ 'gemini-3-pro-image',
130
+ 'gemini-3.1-flash-image',
131
+ 'gemini-3-pro-image-preview',
132
+ 'gemini-3-flash-preview',
133
+ 'gemini-3.1-flash-image-preview',
134
+ ];
135
+ const supportsRef = (model: string): boolean =>
136
+ refModels.some((fragment) => normalizeGoogleModelId(model).includes(fragment));
137
+
138
+ async function postGoogleJson<T>(pathname: string, body: unknown): Promise<T> {
139
+ const apiKey = readEnv('GOOGLE_API_KEY') || readEnv('GEMINI_API_KEY');
140
+ if (apiKey === '') throw new Error('GOOGLE_API_KEY or GEMINI_API_KEY is required');
141
+ const base = readEnv('GOOGLE_BASE_URL') || 'https://generativelanguage.googleapis.com';
142
+ const url = buildGoogleUrl(base, pathname);
143
+ const proxy = getHttpProxy(readEnv);
144
+ // Bun's fetch drops long-lived proxied sockets; upstream works around it with curl (0115 port).
145
+ if (proxy !== null && options?.fetch === undefined) {
146
+ const result = (() => {
147
+ try {
148
+ return execFileSync(
149
+ 'curl',
150
+ [
151
+ '-s',
152
+ '--connect-timeout',
153
+ '30',
154
+ '--max-time',
155
+ '300',
156
+ '-x',
157
+ proxy,
158
+ url,
159
+ '-H',
160
+ 'Content-Type: application/json',
161
+ '-H',
162
+ `x-goog-api-key: ${apiKey}`,
163
+ '-d',
164
+ '@-',
165
+ ],
166
+ { input: JSON.stringify(body), encoding: 'utf8', timeout: 310_000 },
167
+ );
168
+ } catch (error: unknown) {
169
+ const e = error as { message?: string; stderr?: string | Buffer };
170
+ const details =
171
+ (typeof e.stderr === 'string' ? e.stderr.trim() : '') || e.message || 'curl request failed';
172
+ throw new Error(`Google API request failed via curl: ${details}`);
173
+ }
174
+ })();
175
+ const parsed = JSON.parse(result) as T & { error?: { code?: string; message?: string } };
176
+ if (parsed.error !== undefined) {
177
+ throw new Error(`Google API error (${parsed.error.code}): ${parsed.error.message}`);
178
+ }
179
+ return parsed;
180
+ }
181
+ const res = await fetchFn(url, {
182
+ method: 'POST',
183
+ headers: { 'content-type': 'application/json', 'x-goog-api-key': apiKey },
184
+ body: JSON.stringify(body),
185
+ });
186
+ if (!res.ok) {
187
+ const err = await res.text();
188
+ throw new Error(`Google API error (${res.status}): ${err}`);
189
+ }
190
+ return (await res.json()) as T;
191
+ }
192
+
193
+ async function generateWithGemini(req: ImageRequest): Promise<Uint8Array> {
194
+ const promptWithAspect = buildPromptWithAspect(req.prompt, req.aspectRatio, req.quality);
195
+ const parts: Array<{ text?: string; inlineData?: { data: string; mimeType: string } }> = [];
196
+ for (const refPath of req.referenceImages ?? []) {
197
+ parts.push({
198
+ inlineData: {
199
+ data: Buffer.from(await readBytes(refPath)).toString('base64'),
200
+ mimeType: mimeForPath(refPath),
201
+ },
202
+ });
203
+ }
204
+ parts.push({ text: promptWithAspect });
205
+ const response = await postGoogleJson<GeminiResponse>(
206
+ `models/${normalizeGoogleModelId(req.model)}:generateContent`,
207
+ {
208
+ contents: [{ role: 'user', parts }],
209
+ generationConfig: {
210
+ responseModalities: ['IMAGE'],
211
+ imageConfig: { imageSize: resolveSize(req) ?? (req.quality === '2k' ? '2K' : '1K') },
212
+ },
213
+ },
214
+ );
215
+ const imageData = extractInlineImageData(response);
216
+ if (imageData !== null) return Uint8Array.from(Buffer.from(imageData, 'base64'));
217
+ throw new Error('No image in response');
218
+ }
219
+
220
+ async function generateWithImagen(req: ImageRequest): Promise<Uint8Array> {
221
+ const fullPrompt = buildPromptWithAspect(req.prompt, req.aspectRatio, req.quality);
222
+ const imageSize = resolveSize(req) ?? (req.quality === '2k' ? '2K' : '1K');
223
+ const parameters: Record<string, unknown> = { sampleCount: req.n ?? 1 };
224
+ if (req.aspectRatio !== undefined) parameters.aspectRatio = req.aspectRatio;
225
+ parameters.imageSize = imageSize === '4K' ? '2K' : imageSize;
226
+ const response = await postGoogleJson<ImagenResponse>(`models/${normalizeGoogleModelId(req.model)}:predict`, {
227
+ instances: [{ prompt: fullPrompt }],
228
+ parameters,
229
+ });
230
+ const imageData = extractPredictedImageData(response);
231
+ if (imageData !== null) return Uint8Array.from(Buffer.from(imageData, 'base64'));
232
+ throw new Error('No image in response');
233
+ }
234
+
235
+ async function generate(req: ImageRequest): Promise<ImageResult> {
236
+ const refs = req.referenceImages;
237
+ if (isGoogleImagen(req.model)) {
238
+ if (refs !== undefined && refs.length > 0) {
239
+ throw new Error(
240
+ 'Reference images are not supported with Imagen models. Use a Gemini multimodal model such as gemini-3-pro-image, gemini-3.1-flash-image, gemini-3-pro-image-preview, gemini-3-flash-preview, or gemini-3.1-flash-image-preview.',
241
+ );
242
+ }
243
+ return { bytes: await generateWithImagen(req), extension: 'png', provider: 'google', model: req.model };
244
+ }
245
+ if (!isGoogleMultimodal(req.model) && refs !== undefined && refs.length > 0) {
246
+ throw new Error(
247
+ 'Reference images are only supported with Gemini multimodal models such as gemini-3-pro-image, gemini-3.1-flash-image, gemini-3-pro-image-preview, gemini-3-flash-preview, or gemini-3.1-flash-image-preview.',
248
+ );
249
+ }
250
+ const bytes = await generateWithGemini(req);
251
+ return { bytes, extension: 'png', provider: 'google', model: req.model };
252
+ }
253
+
254
+ return {
255
+ name: 'google',
256
+ defaultModel: () => DEFAULT_MODEL,
257
+ isAvailable: () => (readEnv('GOOGLE_API_KEY') || readEnv('GEMINI_API_KEY')) !== '',
258
+ supportsReferenceImages: supportsRef,
259
+ validate(req: ImageRequest): void {
260
+ if ((req.referenceImages?.length ?? 0) > 0 && !supportsRef(req.model)) {
261
+ throw new Error(
262
+ `google model '${req.model}' does not accept reference images; use a Gemini multimodal model`,
263
+ );
264
+ }
265
+ },
266
+ generate,
267
+ };
268
+ };
@@ -0,0 +1,59 @@
1
+ import type { ImageProvider, ImageRequest, ImageResult, ProviderFactory, ProviderOptions } from './types';
2
+
3
+ /**
4
+ * HuggingFace Inference adapter (0116 R1) — port of wt-image-generate's
5
+ * `HuggingFaceBackend.generate()` (scripts/image_generator.py:142). POST JSON
6
+ * {inputs, parameters:{width,height,num_inference_steps}} to
7
+ * https://api-inference.huggingface.co/models/<model>; HTTP 200 returns raw image
8
+ * bytes, 503 means the model is still cold-loading (loud error, wt parity).
9
+ * Text-to-image only — wt's backend has no image-to-image path.
10
+ */
11
+
12
+ const DEFAULT_MODEL = 'stabilityai/stable-diffusion-xl-base-1.0';
13
+ const BASE_URL = 'https://api-inference.huggingface.co/models';
14
+
15
+ export const buildPayload = (
16
+ req: Pick<ImageRequest, 'prompt' | 'width' | 'height' | 'quality'>,
17
+ ): Record<string, unknown> => ({
18
+ inputs: req.prompt,
19
+ parameters: {
20
+ width: req.width ?? 1024,
21
+ height: req.height ?? 1024,
22
+ num_inference_steps: req.quality === 'normal' ? 30 : 50,
23
+ },
24
+ });
25
+
26
+ export const createHuggingface: ProviderFactory = (options?: ProviderOptions): ImageProvider => {
27
+ const fetchFn = options?.fetch ?? fetch;
28
+ const env = options?.env ?? process.env;
29
+ const readEnv = (key: string): string => env[key] ?? '';
30
+
31
+ return {
32
+ name: 'huggingface',
33
+ defaultModel: () => readEnv('HUGGINGFACE_MODEL') || DEFAULT_MODEL,
34
+ isAvailable: () => readEnv('HUGGINGFACE_API_TOKEN') !== '',
35
+ supportsReferenceImages: () => false,
36
+
37
+ async generate(req: ImageRequest): Promise<ImageResult> {
38
+ const token = readEnv('HUGGINGFACE_API_TOKEN');
39
+ if (token === '') {
40
+ throw new Error('HUGGINGFACE_API_TOKEN is required. Get one at https://huggingface.co/settings/tokens');
41
+ }
42
+ const model = req.model.startsWith('hf:') ? req.model.slice(3) : req.model;
43
+ const res = await fetchFn(`${BASE_URL}/${model}`, {
44
+ method: 'POST',
45
+ headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' },
46
+ body: JSON.stringify(buildPayload(req)),
47
+ });
48
+ if (!res.ok) {
49
+ const text = (await res.text()).slice(0, 200);
50
+ if (res.status === 503) throw new Error(`HuggingFace model loading (HTTP 503): ${text}`);
51
+ throw new Error(`HuggingFace HTTP ${res.status}: ${text}`);
52
+ }
53
+ const bytes = new Uint8Array(await res.arrayBuffer());
54
+ const ct = res.headers.get('content-type') ?? '';
55
+ const extension = ct.includes('jpeg') ? 'jpg' : ct.includes('webp') ? 'webp' : 'png';
56
+ return { bytes, extension, provider: 'huggingface', model };
57
+ },
58
+ };
59
+ };
@@ -0,0 +1,259 @@
1
+ import * as crypto from 'node:crypto';
2
+ import type { ImageProvider, ImageRequest, ImageResult, ProviderFactory, ProviderOptions } from './types';
3
+
4
+ /**
5
+ * Volcengine Jimeng adapter (0115 R1) — port of
6
+ * vendors/baoyu-skills/skills/baoyu-image-gen/scripts/providers/jimeng.ts
7
+ * (MIT © 2026 Jim Liu). Text-to-image only; signed submit + poll (HMAC-SHA256,
8
+ * https://www.volcengine.com/docs/85621/1817045).
9
+ */
10
+
11
+ const DEFAULT_MODEL = 'jimeng_t2i_v40';
12
+
13
+ type JimengSizePreset = 'normal' | '2k' | '4k';
14
+
15
+ const SIZE_PRESETS: Record<JimengSizePreset, Record<string, string>> = {
16
+ normal: {
17
+ '1:1': '1024x1024',
18
+ '4:3': '1360x1020',
19
+ '16:9': '1536x864',
20
+ '3:2': '1440x960',
21
+ '21:9': '1920x824',
22
+ },
23
+ '2k': {
24
+ '1:1': '2048x2048',
25
+ '4:3': '2304x1728',
26
+ '16:9': '2560x1440',
27
+ '3:2': '2496x1664',
28
+ '21:9': '3024x1296',
29
+ },
30
+ '4k': {
31
+ '1:1': '4096x4096',
32
+ '4:3': '4694x3520',
33
+ '16:9': '5404x3040',
34
+ '3:2': '4992x3328',
35
+ '21:9': '6198x2656',
36
+ },
37
+ };
38
+
39
+ export function generateSignature(
40
+ method: string,
41
+ query: Record<string, string>,
42
+ headers: Record<string, string>,
43
+ body: string,
44
+ accessKey: string,
45
+ secretKey: string,
46
+ region: string,
47
+ service: string,
48
+ canonicalUri: string,
49
+ ): string {
50
+ const sortedQuery = Object.entries(query)
51
+ .sort(([a], [b]) => a.localeCompare(b))
52
+ .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
53
+ .join('&');
54
+ const sortedHeaders = Object.entries(headers)
55
+ .sort(([a], [b]) => a.localeCompare(b))
56
+ .map(([k, v]) => `${k.toLowerCase()}:${v.trim()}\n`)
57
+ .join('');
58
+ const signedHeaders = Object.keys(headers)
59
+ .sort()
60
+ .map((k) => k.toLowerCase())
61
+ .join(';');
62
+ const hashedPayload = crypto.createHash('sha256').update(body, 'utf8').digest('hex');
63
+ const canonicalRequest = [method, canonicalUri, sortedQuery, sortedHeaders, signedHeaders, hashedPayload].join(
64
+ '\n',
65
+ );
66
+ const hashedCanonicalRequest = crypto.createHash('sha256').update(canonicalRequest, 'utf8').digest('hex');
67
+
68
+ const algorithm = 'HMAC-SHA256';
69
+ const timestamp = headers['X-Date'] ?? headers['x-date'];
70
+ if (timestamp === undefined) throw new Error('Jimeng signature generation requires an X-Date header.');
71
+ const dateStamp = timestamp.slice(0, 8);
72
+ const credentialScope = `${dateStamp}/${region}/${service}/request`;
73
+ const stringToSign = [algorithm, timestamp, credentialScope, hashedCanonicalRequest].join('\n');
74
+
75
+ const kDate = crypto.createHmac('sha256', secretKey).update(dateStamp).digest();
76
+ const kRegion = crypto.createHmac('sha256', kDate).update(region).digest();
77
+ const kService = crypto.createHmac('sha256', kRegion).update(service).digest();
78
+ const kSigning = crypto.createHmac('sha256', kService).update('request').digest();
79
+ const signature = crypto.createHmac('sha256', kSigning).update(stringToSign).digest('hex');
80
+
81
+ return `${algorithm} Credential=${accessKey}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
82
+ }
83
+
84
+ function resolveEndpoint(
85
+ baseUrl: string,
86
+ query: Record<string, string>,
87
+ ): { url: string; host: string; canonicalUri: string } {
88
+ let parsed: URL;
89
+ try {
90
+ parsed = new URL(baseUrl);
91
+ } catch {
92
+ throw new Error(`Invalid JIMENG_BASE_URL: ${baseUrl}`);
93
+ }
94
+ parsed.search = '';
95
+ for (const [key, value] of Object.entries(query).sort(([a], [b]) => a.localeCompare(b))) {
96
+ parsed.searchParams.set(key, value);
97
+ }
98
+ return { url: parsed.toString(), host: parsed.host, canonicalUri: parsed.pathname || '/' };
99
+ }
100
+
101
+ function getClosestPresetSize(ar: string | undefined, qualityLevel: JimengSizePreset): string {
102
+ const presets = SIZE_PRESETS[qualityLevel];
103
+ const defaultSize = presets['1:1'] as string;
104
+ if (ar === undefined) return defaultSize;
105
+ const match = ar.match(/^(\d+(?:\.\d+)?):(\d+(?:\.\d+)?)$/);
106
+ if (match === null) return defaultSize;
107
+ const targetRatio = Number(match[1]) / Number(match[2]);
108
+ let bestMatch = defaultSize;
109
+ let bestDiff = Number.POSITIVE_INFINITY;
110
+ for (const [ratio, size] of Object.entries(presets)) {
111
+ const [w, h] = ratio.split(':').map(Number) as [number, number];
112
+ const diff = Math.abs(w / h - targetRatio);
113
+ if (diff < bestDiff) {
114
+ bestDiff = diff;
115
+ bestMatch = size;
116
+ }
117
+ }
118
+ return bestMatch;
119
+ }
120
+
121
+ /** Explicit pixels win; else the quality-level preset closest to the aspect ratio. */
122
+ export function getImageSize(
123
+ ar: string | undefined,
124
+ quality: 'normal' | '2k' | undefined,
125
+ width?: number,
126
+ height?: number,
127
+ ): string {
128
+ if (width !== undefined && height !== undefined) return `${width}x${height}`;
129
+ const qualityLevel: JimengSizePreset = quality === 'normal' ? 'normal' : '2k';
130
+ return getClosestPresetSize(ar, qualityLevel);
131
+ }
132
+
133
+ function timestampHeader(): string {
134
+ return new Date().toISOString().replace(/[:-]|\.\d{3}/g, '');
135
+ }
136
+
137
+ interface JimengTaskResponse {
138
+ code?: number;
139
+ message?: string;
140
+ data?: { task_id?: string; status?: string; image_urls?: string[]; binary_data_base64?: string[] };
141
+ }
142
+
143
+ export const createJimeng: ProviderFactory = (options?: ProviderOptions): ImageProvider => {
144
+ const fetchFn = options?.fetch ?? fetch;
145
+ const env = options?.env ?? process.env;
146
+ const readEnv = (key: string): string => env[key] ?? '';
147
+
148
+ async function signedPost(
149
+ query: Record<string, string>,
150
+ body: string,
151
+ accessKey: string,
152
+ secretKey: string,
153
+ region: string,
154
+ ): Promise<Response> {
155
+ const base = readEnv('JIMENG_BASE_URL') || 'https://visual.volcengineapi.com';
156
+ const endpoint = resolveEndpoint(base, query);
157
+ const headers: Record<string, string> = {
158
+ 'content-type': 'application/json',
159
+ 'X-Date': timestampHeader(),
160
+ Host: endpoint.host,
161
+ };
162
+ const authorization = generateSignature(
163
+ 'POST',
164
+ query,
165
+ headers,
166
+ body,
167
+ accessKey,
168
+ secretKey,
169
+ region,
170
+ 'cv',
171
+ endpoint.canonicalUri,
172
+ );
173
+ return fetchFn(endpoint.url, { method: 'POST', headers: { ...headers, authorization }, body });
174
+ }
175
+
176
+ async function submitTask(
177
+ req: ImageRequest,
178
+ size: string,
179
+ accessKey: string,
180
+ secretKey: string,
181
+ region: string,
182
+ ): Promise<string> {
183
+ const query = { Action: 'CVSync2AsyncSubmitTask', Version: '2022-08-31' };
184
+ const [width, height] = size.split('x').map(Number) as [number, number];
185
+ const body = JSON.stringify({ req_key: req.model, prompt: req.prompt, width, height });
186
+ const res = await signedPost(query, body, accessKey, secretKey, region);
187
+ if (!res.ok) throw new Error(`Jimeng API submit error (${res.status}): ${await res.text()}`);
188
+ const result = (await res.json()) as JimengTaskResponse;
189
+ if (result.code !== 10_000 || result.data?.task_id === undefined) {
190
+ throw new Error(`Failed to submit task: ${result.message ?? 'Unknown error'}`);
191
+ }
192
+ return result.data.task_id;
193
+ }
194
+
195
+ async function pollForResult(
196
+ taskId: string,
197
+ req: ImageRequest,
198
+ accessKey: string,
199
+ secretKey: string,
200
+ region: string,
201
+ ): Promise<Uint8Array> {
202
+ const maxAttempts = 60;
203
+ const pollIntervalMs = 2000;
204
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
205
+ const query = { Action: 'CVSync2AsyncGetResult', Version: '2022-08-31' };
206
+ const body = JSON.stringify({ req_key: req.model, task_id: taskId });
207
+ const res = await signedPost(query, body, accessKey, secretKey, region);
208
+ if (!res.ok) throw new Error(`Jimeng API poll error (${res.status}): ${await res.text()}`);
209
+ const result = (await res.json()) as JimengTaskResponse;
210
+ if (result.code === 10_000 && result.data !== undefined) {
211
+ const { status, image_urls, binary_data_base64 } = result.data;
212
+ if (binary_data_base64 !== undefined && binary_data_base64.length > 0) {
213
+ return new Uint8Array(Buffer.from(binary_data_base64[0] as string, 'base64'));
214
+ }
215
+ if (status === 'done' && image_urls !== undefined && image_urls.length > 0) {
216
+ const url = image_urls[0] as string;
217
+ const imgRes = await fetchFn(url);
218
+ if (!imgRes.ok) throw new Error(`Failed to download image from ${url}`);
219
+ return new Uint8Array(await imgRes.arrayBuffer());
220
+ }
221
+ if (status === 'in_queue' || status === 'generating') {
222
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
223
+ continue;
224
+ }
225
+ if (status === 'fail') throw new Error(`Jimeng task failed: ${result.message ?? 'Generation failed'}`);
226
+ }
227
+ throw new Error(`Unexpected response during polling: ${result.message ?? 'Unknown error'}`);
228
+ }
229
+ throw new Error('Task timeout: image generation took too long');
230
+ }
231
+
232
+ async function generate(req: ImageRequest): Promise<ImageResult> {
233
+ if ((req.referenceImages?.length ?? 0) > 0) {
234
+ throw new Error(
235
+ 'Jimeng does not support reference images. Use --provider google, openai, openrouter, or replicate.',
236
+ );
237
+ }
238
+ const accessKey = readEnv('JIMENG_ACCESS_KEY_ID');
239
+ const secretKey = readEnv('JIMENG_SECRET_ACCESS_KEY');
240
+ if (accessKey === '' || secretKey === '') {
241
+ throw new Error(
242
+ 'JIMENG_ACCESS_KEY_ID and JIMENG_SECRET_ACCESS_KEY are required. Get your credentials from https://console.volcengine.com/iam/keymanage',
243
+ );
244
+ }
245
+ const region = readEnv('JIMENG_REGION') || 'cn-north-1';
246
+ const size = getImageSize(req.aspectRatio, req.quality, req.width, req.height);
247
+ const taskId = await submitTask(req, size, accessKey, secretKey, region);
248
+ const bytes = await pollForResult(taskId, req, accessKey, secretKey, region);
249
+ return { bytes, extension: 'png', provider: 'jimeng', model: req.model };
250
+ }
251
+
252
+ return {
253
+ name: 'jimeng',
254
+ defaultModel: () => DEFAULT_MODEL,
255
+ isAvailable: () => readEnv('JIMENG_ACCESS_KEY_ID') !== '' && readEnv('JIMENG_SECRET_ACCESS_KEY') !== '',
256
+ supportsReferenceImages: () => true,
257
+ generate,
258
+ };
259
+ };