@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,485 @@
1
+ import { Buffer } from 'node:buffer';
2
+ import { echoError } from '@gobing-ai/ts-utils';
3
+ import { refToDataUri } from './refs';
4
+ import type { ImageProvider, ImageRequest, ImageResult, ProviderFactory, ProviderOptions } from './types';
5
+
6
+ /**
7
+ * DashScope adapter (0115 R1) — port of
8
+ * vendors/baoyu-skills/skills/baoyu-image-gen/scripts/providers/dashscope.ts
9
+ * (MIT © 2026 Jim Liu). `CliArgs.size` becomes width/height; reference images
10
+ * are local-path or http(s)-URL strings per the wan2.7 multimodal content array.
11
+ */
12
+
13
+ type DashScopeModelFamily = 'qwen2' | 'qwenFixed' | 'wan27' | 'legacy';
14
+
15
+ const DEFAULT_MODEL = 'qwen-image-2.0-pro';
16
+ const MIN_QWEN_2_TOTAL_PIXELS = 512 * 512;
17
+ const MAX_QWEN_2_TOTAL_PIXELS = 2048 * 2048;
18
+ const SIZE_STEP = 16;
19
+ const QWEN_NEGATIVE_PROMPT =
20
+ '低分辨率,低画质,肢体畸形,手指畸形,画面过饱和,蜡像感,人脸无细节,过度光滑,画面具有AI感,构图混乱,文字模糊,扭曲';
21
+
22
+ const QWEN_2_TARGET_PIXELS = { normal: 1024 * 1024, '2k': 1536 * 1536 } as const;
23
+ const MIN_WAN27_TOTAL_PIXELS = 768 * 768;
24
+ const MAX_WAN27_PRO_T2I_PIXELS = 4096 * 4096;
25
+ const MAX_WAN27_GENERAL_PIXELS = 2048 * 2048;
26
+ const WAN27_MAX_REFERENCE_IMAGES = 9;
27
+ const WAN27_TARGET_PIXELS = { normal: 1024 * 1024, '2k': 2048 * 2048 } as const;
28
+
29
+ const QWEN_2_RECOMMENDED: Record<string, Record<'normal' | '2k', string>> = {
30
+ '1:1': { normal: '1024*1024', '2k': '1536*1536' },
31
+ '2:3': { normal: '768*1152', '2k': '1024*1536' },
32
+ '3:2': { normal: '1152*768', '2k': '1536*1024' },
33
+ '3:4': { normal: '960*1280', '2k': '1080*1440' },
34
+ '4:3': { normal: '1280*960', '2k': '1440*1080' },
35
+ '9:16': { normal: '720*1280', '2k': '1080*1920' },
36
+ '16:9': { normal: '1280*720', '2k': '1920*1080' },
37
+ '21:9': { normal: '1344*576', '2k': '2048*872' },
38
+ };
39
+
40
+ const QWEN_FIXED_SIZES_BY_RATIO: Record<string, string> = {
41
+ '16:9': '1664*928',
42
+ '4:3': '1472*1104',
43
+ '1:1': '1328*1328',
44
+ '3:4': '1104*1472',
45
+ '9:16': '928*1664',
46
+ };
47
+
48
+ const QWEN_FIXED_SPEC_DEFAULT = QWEN_FIXED_SIZES_BY_RATIO['16:9'] as string;
49
+
50
+ const LEGACY_STANDARD_SIZES: Array<[number, number]> = [
51
+ [1024, 1024],
52
+ [1280, 720],
53
+ [720, 1280],
54
+ [1024, 768],
55
+ [768, 1024],
56
+ [1536, 1024],
57
+ [1024, 1536],
58
+ [1536, 864],
59
+ [864, 1536],
60
+ ];
61
+
62
+ const LEGACY_STANDARD_SIZES_2K: Array<[number, number]> = [
63
+ [1536, 1536],
64
+ [2048, 1152],
65
+ [1152, 2048],
66
+ [1536, 1024],
67
+ [1024, 1536],
68
+ [1536, 864],
69
+ [864, 1536],
70
+ [2048, 2048],
71
+ ];
72
+
73
+ interface DashScopeModelSpec {
74
+ family: DashScopeModelFamily;
75
+ defaultSize: string;
76
+ }
77
+
78
+ const MODEL_SPEC_ALIASES: Record<string, DashScopeModelSpec> = {
79
+ 'qwen-image-2.0-pro': { family: 'qwen2', defaultSize: '1024*1024' },
80
+ 'qwen-image-2.0-pro-2026-03-03': { family: 'qwen2', defaultSize: '1024*1024' },
81
+ 'qwen-image-2.0': { family: 'qwen2', defaultSize: '1024*1024' },
82
+ 'qwen-image-2.0-2026-03-03': { family: 'qwen2', defaultSize: '1024*1024' },
83
+ 'qwen-image-max': { family: 'qwenFixed', defaultSize: QWEN_FIXED_SPEC_DEFAULT },
84
+ 'qwen-image-max-2025-12-30': { family: 'qwenFixed', defaultSize: QWEN_FIXED_SPEC_DEFAULT },
85
+ 'qwen-image-plus': { family: 'qwenFixed', defaultSize: QWEN_FIXED_SPEC_DEFAULT },
86
+ 'qwen-image-plus-2026-01-09': { family: 'qwenFixed', defaultSize: QWEN_FIXED_SPEC_DEFAULT },
87
+ 'qwen-image': { family: 'qwenFixed', defaultSize: QWEN_FIXED_SPEC_DEFAULT },
88
+ 'wan2.7-image-pro': { family: 'wan27', defaultSize: '2048*2048' },
89
+ 'wan2.7-image': { family: 'wan27', defaultSize: '2048*2048' },
90
+ };
91
+
92
+ export function getModelSpec(model: string): DashScopeModelSpec {
93
+ return MODEL_SPEC_ALIASES[model.trim().toLowerCase()] ?? { family: 'legacy', defaultSize: '1536*1536' };
94
+ }
95
+
96
+ export function getModelFamily(model: string): DashScopeModelFamily {
97
+ return getModelSpec(model).family;
98
+ }
99
+
100
+ export function resolveSizeForModel(
101
+ model: string,
102
+ args: { size?: string; aspectRatio?: string; quality?: 'normal' | '2k'; referenceImages?: string[] },
103
+ ): string {
104
+ const spec = getModelSpec(model);
105
+ const referenceCount = args.referenceImages?.length ?? 0;
106
+ const quality = args.quality === 'normal' ? 'normal' : '2k';
107
+
108
+ if (spec.family === 'wan27') {
109
+ const maxPixels =
110
+ isWan27ProModel(model) && referenceCount === 0 ? MAX_WAN27_PRO_T2I_PIXELS : MAX_WAN27_GENERAL_PIXELS;
111
+ if (args.size !== undefined) return validateWan27Size(args.size, maxPixels, model);
112
+ return getWan27SizeFromAspectRatio(args.aspectRatio, quality, maxPixels);
113
+ }
114
+
115
+ if (args.size !== undefined) {
116
+ if (spec.family === 'qwen2') return validateQwen2Size(args.size);
117
+ if (spec.family === 'qwenFixed') return validateQwenFixedSize(args.size);
118
+ validateSizeFormat(args.size);
119
+ return normalizeSize(args.size);
120
+ }
121
+
122
+ if (spec.family === 'qwen2') return getQwen2SizeFromAspectRatio(args.aspectRatio, quality);
123
+ if (spec.family === 'qwenFixed') return getQwenFixedSizeFromAspectRatio(args.aspectRatio, quality);
124
+ return getSizeFromAspectRatio(args.aspectRatio, quality);
125
+ }
126
+
127
+ export function getSizeFromAspectRatio(ar: string | undefined, quality: 'normal' | '2k'): string {
128
+ const sizes = quality === '2k' ? LEGACY_STANDARD_SIZES_2K : LEGACY_STANDARD_SIZES;
129
+ const defaultSize = quality === '2k' ? '1536*1536' : '1024*1024';
130
+ const parsed = ar === undefined ? null : parseAspectRatio(ar);
131
+ if (parsed === null) return defaultSize;
132
+ const targetRatio = parsed.width / parsed.height;
133
+ let best = defaultSize;
134
+ let bestDiff = Number.POSITIVE_INFINITY;
135
+ for (const [width, height] of sizes) {
136
+ const diff = Math.abs(width / height - targetRatio);
137
+ if (diff < bestDiff) {
138
+ bestDiff = diff;
139
+ best = `${width}*${height}`;
140
+ }
141
+ }
142
+ return best;
143
+ }
144
+
145
+ export function getQwen2SizeFromAspectRatio(ar: string | undefined, quality: 'normal' | '2k'): string {
146
+ if (ar === undefined) {
147
+ return QWEN_2_RECOMMENDED['1:1']?.[quality] as string;
148
+ }
149
+ const recommendedRatio = findKnownRatioKey(ar, Object.keys(QWEN_2_RECOMMENDED));
150
+ if (recommendedRatio !== null) {
151
+ return QWEN_2_RECOMMENDED[recommendedRatio]?.[quality] as string;
152
+ }
153
+ const parsed = parseAspectRatio(ar);
154
+ if (parsed === null) {
155
+ return QWEN_2_RECOMMENDED['1:1']?.[quality] as string;
156
+ }
157
+ const targetRatio = parsed.width / parsed.height;
158
+
159
+ const targetPixels = QWEN_2_TARGET_PIXELS[quality];
160
+ const rawWidth = Math.sqrt(targetPixels * targetRatio);
161
+ const rawHeight = Math.sqrt(targetPixels / targetRatio);
162
+ const fitted = fitToPixelBudget(rawWidth, rawHeight, MIN_QWEN_2_TOTAL_PIXELS, MAX_QWEN_2_TOTAL_PIXELS);
163
+ return `${fitted.width}*${fitted.height}`;
164
+ }
165
+
166
+ function findKnownRatioKey(ar: string, candidates: string[], tolerance = 0.02): string | null {
167
+ const parsed = parseAspectRatio(ar);
168
+ if (parsed === null) return null;
169
+ const targetRatio = parsed.width / parsed.height;
170
+ let bestKey: string | null = null;
171
+ let bestDiff = Number.POSITIVE_INFINITY;
172
+ for (const candidate of candidates) {
173
+ const candidateParsed = parseAspectRatio(candidate);
174
+ if (candidateParsed === null) continue;
175
+ const diff = Math.abs(candidateParsed.width / candidateParsed.height - targetRatio);
176
+ if (diff < bestDiff) {
177
+ bestDiff = diff;
178
+ bestKey = candidate;
179
+ }
180
+ }
181
+ return bestDiff <= tolerance ? bestKey : null;
182
+ }
183
+
184
+ export function fitToPixelBudget(
185
+ width: number,
186
+ height: number,
187
+ minPixels: number,
188
+ maxPixels: number,
189
+ ): { width: number; height: number } {
190
+ let nextWidth = width;
191
+ let nextHeight = height;
192
+ let pixels = nextWidth * nextHeight;
193
+
194
+ if (pixels > maxPixels) {
195
+ const scale = Math.sqrt(maxPixels / pixels);
196
+ nextWidth *= scale;
197
+ nextHeight *= scale;
198
+ } else if (pixels < minPixels) {
199
+ const scale = Math.sqrt(minPixels / pixels);
200
+ nextWidth *= scale;
201
+ nextHeight *= scale;
202
+ }
203
+
204
+ let roundedWidth = roundToStep(nextWidth);
205
+ let roundedHeight = roundToStep(nextHeight);
206
+ pixels = roundedWidth * roundedHeight;
207
+ while (pixels > maxPixels && (roundedWidth > SIZE_STEP || roundedHeight > SIZE_STEP)) {
208
+ if (roundedWidth >= roundedHeight && roundedWidth > SIZE_STEP) {
209
+ roundedWidth -= SIZE_STEP;
210
+ } else if (roundedHeight > SIZE_STEP) {
211
+ roundedHeight -= SIZE_STEP;
212
+ } else {
213
+ break;
214
+ }
215
+ pixels = roundedWidth * roundedHeight;
216
+ }
217
+ while (pixels < minPixels) {
218
+ if (roundedWidth <= roundedHeight) {
219
+ roundedWidth += SIZE_STEP;
220
+ } else {
221
+ roundedHeight += SIZE_STEP;
222
+ }
223
+ pixels = roundedWidth * roundedHeight;
224
+ }
225
+ return { width: roundedWidth, height: roundedHeight };
226
+ }
227
+
228
+ function roundToStep(value: number): number {
229
+ return Math.max(SIZE_STEP, Math.round(value / SIZE_STEP) * SIZE_STEP);
230
+ }
231
+
232
+ function floorToStep(value: number): number {
233
+ return Math.max(SIZE_STEP, Math.floor(value / SIZE_STEP) * SIZE_STEP);
234
+ }
235
+
236
+ export function getWan27SizeFromAspectRatio(
237
+ ar: string | undefined,
238
+ quality: 'normal' | '2k',
239
+ maxPixels: number,
240
+ ): string {
241
+ const targetPixels = Math.min(WAN27_TARGET_PIXELS[quality], maxPixels);
242
+ if (ar === undefined) {
243
+ const side = roundToStep(Math.sqrt(targetPixels));
244
+ return `${side}*${side}`;
245
+ }
246
+ const parsed = parseAspectRatio(ar);
247
+ if (parsed === null) {
248
+ const side = roundToStep(Math.sqrt(targetPixels));
249
+ return `${side}*${side}`;
250
+ }
251
+ const ratio = parsed.width / parsed.height;
252
+ if (ratio < 1 / 8 || ratio > 8) {
253
+ throw new Error(`DashScope wan2.7 image models support aspect ratios in [1:8, 8:1]. Received "${ar}".`);
254
+ }
255
+ const rawWidth = Math.sqrt(targetPixels * ratio);
256
+ const rawHeight = Math.sqrt(targetPixels / ratio);
257
+ const fitted = fitToPixelBudget(rawWidth, rawHeight, MIN_WAN27_TOTAL_PIXELS, maxPixels);
258
+ const bounded = clampWan27DerivedSizeToRatioBounds(fitted);
259
+ return `${bounded.width}*${bounded.height}`;
260
+ }
261
+
262
+ function clampWan27DerivedSizeToRatioBounds(size: { width: number; height: number }): {
263
+ width: number;
264
+ height: number;
265
+ } {
266
+ let { width, height } = size;
267
+ const ratio = width / height;
268
+ if (ratio > 8) {
269
+ width = floorToStep(height * 8);
270
+ } else if (ratio < 1 / 8) {
271
+ height = floorToStep(width * 8);
272
+ }
273
+ return { width, height };
274
+ }
275
+
276
+ function isWan27ProModel(model: string): boolean {
277
+ return model.trim().toLowerCase() === 'wan2.7-image-pro';
278
+ }
279
+
280
+ export function normalizeSize(size: string): string {
281
+ return size.replace('x', '*');
282
+ }
283
+
284
+ export function parseAspectRatio(ar: string): { width: number; height: number } | null {
285
+ const match = ar.match(/^(\d+(?:\.\d+)?):(\d+(?:\.\d+)?)$/);
286
+ if (match === null) return null;
287
+ const w = Number.parseFloat(match[1] as string);
288
+ const h = Number.parseFloat(match[2] as string);
289
+ if (w <= 0 || h <= 0) return null;
290
+ return { width: w, height: h };
291
+ }
292
+
293
+ export function parseSize(size: string): { width: number; height: number } | null {
294
+ const match = normalizeSize(size).match(/^(\d+)\*(\d+)$/);
295
+ if (match === null) return null;
296
+ const width = Number(match[1]);
297
+ const height = Number(match[2]);
298
+ if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) return null;
299
+ return { width, height };
300
+ }
301
+
302
+ function validateSizeFormat(size: string): { width: number; height: number } {
303
+ const parsed = parseSize(size);
304
+ if (parsed === null) {
305
+ throw new Error(`Invalid DashScope size "${size}". Expected <width>x<height> or <width>*<height>.`);
306
+ }
307
+ return parsed;
308
+ }
309
+
310
+ function validateQwen2Size(size: string): string {
311
+ const normalized = normalizeSize(size);
312
+ const parsed = validateSizeFormat(normalized);
313
+ const totalPixels = parsed.width * parsed.height;
314
+ if (totalPixels < MIN_QWEN_2_TOTAL_PIXELS || totalPixels > MAX_QWEN_2_TOTAL_PIXELS) {
315
+ throw new Error(
316
+ `DashScope qwen-image-2.0* models require total pixels between ${MIN_QWEN_2_TOTAL_PIXELS} ` +
317
+ `and ${MAX_QWEN_2_TOTAL_PIXELS}. Received ${normalized} (${totalPixels} pixels).`,
318
+ );
319
+ }
320
+ return normalized;
321
+ }
322
+
323
+ function validateQwenFixedSize(size: string): string {
324
+ const normalized = normalizeSize(size);
325
+ validateSizeFormat(normalized);
326
+ if (!Object.values(QWEN_FIXED_SIZES_BY_RATIO).includes(normalized)) {
327
+ throw new Error(
328
+ `DashScope qwen-image-max/plus/image models support only these sizes: ` +
329
+ `${Object.values(QWEN_FIXED_SIZES_BY_RATIO).join(', ')}. Received ${normalized}.`,
330
+ );
331
+ }
332
+ return normalized;
333
+ }
334
+
335
+ function validateWan27Size(size: string, maxPixels: number, model: string): string {
336
+ const normalized = normalizeSize(size);
337
+ const parsed = validateSizeFormat(normalized);
338
+ const totalPixels = parsed.width * parsed.height;
339
+ if (totalPixels < MIN_WAN27_TOTAL_PIXELS || totalPixels > maxPixels) {
340
+ throw new Error(
341
+ `DashScope ${model} requires total pixels between 768*768 and ` +
342
+ `${maxPixels === MAX_WAN27_PRO_T2I_PIXELS ? '4096*4096' : '2048*2048'} for the current request. ` +
343
+ `Received ${normalized} (${totalPixels} pixels).`,
344
+ );
345
+ }
346
+ const ratio = parsed.width / parsed.height;
347
+ if (ratio < 1 / 8 || ratio > 8) {
348
+ throw new Error(
349
+ `DashScope wan2.7 image models support aspect ratios in [1:8, 8:1]. Received ${normalized} (ratio ${ratio.toFixed(3)}).`,
350
+ );
351
+ }
352
+ return normalized;
353
+ }
354
+
355
+ function getQwenFixedSizeFromAspectRatio(ar: string | undefined, quality: 'normal' | '2k'): string {
356
+ if (quality === 'normal') {
357
+ // ponytail: upstream warns on stderr; kk runs quiet — the fixed-size behaviour is unchanged.
358
+ echoError(
359
+ 'DashScope qwen-image-max/plus/image models use fixed output sizes; quality does not change the resolution.',
360
+ );
361
+ }
362
+ if (ar === undefined) return QWEN_FIXED_SPEC_DEFAULT;
363
+ const ratioKey = findKnownRatioKey(ar, Object.keys(QWEN_FIXED_SIZES_BY_RATIO));
364
+ if (ratioKey === null) {
365
+ throw new Error(
366
+ `DashScope model supports only fixed ratios ${Object.keys(QWEN_FIXED_SIZES_BY_RATIO).join(', ')}. ` +
367
+ `For custom ratios like "${ar}", use --model qwen-image-2.0-pro.`,
368
+ );
369
+ }
370
+ return QWEN_FIXED_SIZES_BY_RATIO[ratioKey] as string;
371
+ }
372
+
373
+ function buildParameters(family: DashScopeModelFamily, size: string): Record<string, unknown> {
374
+ if (family === 'wan27') {
375
+ return { size, n: 1, watermark: false };
376
+ }
377
+ const parameters: Record<string, unknown> = { prompt_extend: false, size };
378
+ if (family === 'qwen2' || family === 'qwenFixed') {
379
+ parameters.watermark = false;
380
+ parameters.negative_prompt = QWEN_NEGATIVE_PROMPT;
381
+ }
382
+ return parameters;
383
+ }
384
+
385
+ interface DashScopeResponse {
386
+ output?: {
387
+ result_image?: string;
388
+ choices?: Array<{ message?: { content?: Array<{ image?: string }> } }>;
389
+ };
390
+ }
391
+
392
+ async function extractImageFromResponse(result: DashScopeResponse, fetchFn: typeof fetch): Promise<Uint8Array> {
393
+ let imageData: string | null = null;
394
+ if (result.output?.result_image !== undefined) {
395
+ imageData = result.output.result_image;
396
+ } else if (result.output?.choices?.[0]?.message?.content !== undefined) {
397
+ for (const item of result.output.choices[0].message.content) {
398
+ if (item.image !== undefined) {
399
+ imageData = item.image;
400
+ break;
401
+ }
402
+ }
403
+ }
404
+ if (imageData === null) throw new Error('No image in response');
405
+ if (imageData.startsWith('http://') || imageData.startsWith('https://')) {
406
+ const imgRes = await fetchFn(imageData);
407
+ if (!imgRes.ok) throw new Error('Failed to download image');
408
+ return new Uint8Array(await imgRes.arrayBuffer());
409
+ }
410
+ return Uint8Array.from(Buffer.from(imageData, 'base64'));
411
+ }
412
+
413
+ export const createDashscope: ProviderFactory = (options?: ProviderOptions): ImageProvider => {
414
+ const fetchFn = options?.fetch ?? fetch;
415
+ const env = options?.env ?? process.env;
416
+ const readEnv = (key: string): string => env[key] ?? '';
417
+ const supportsRef = (model: string): boolean => model.includes('wan2.7');
418
+
419
+ return {
420
+ name: 'dashscope',
421
+ defaultModel: () => DEFAULT_MODEL,
422
+ isAvailable: () => readEnv('DASHSCOPE_API_KEY') !== '',
423
+ supportsReferenceImages: supportsRef,
424
+ validate(req: ImageRequest): void {
425
+ const spec = getModelSpec(req.model);
426
+ if ((req.referenceImages?.length ?? 0) > 0 && spec.family !== 'wan27') {
427
+ throw new Error(
428
+ 'Reference images are not supported with this DashScope model. Use a wan2.7 image model (--model wan2.7-image-pro or wan2.7-image), or switch to --provider google with a Gemini multimodal model.',
429
+ );
430
+ }
431
+ },
432
+ async generate(req: ImageRequest): Promise<ImageResult> {
433
+ const apiKey = readEnv('DASHSCOPE_API_KEY');
434
+ if (apiKey === '') throw new Error('DASHSCOPE_API_KEY is required');
435
+ const spec = getModelSpec(req.model);
436
+ const refs = req.referenceImages ?? [];
437
+ if (refs.length > 0 && spec.family !== 'wan27') {
438
+ throw new Error(
439
+ 'Reference images are not supported with this DashScope model. Use a wan2.7 image model (--model wan2.7-image-pro or wan2.7-image), or switch to --provider google with a Gemini multimodal model.',
440
+ );
441
+ }
442
+ if (refs.length > WAN27_MAX_REFERENCE_IMAGES) {
443
+ throw new Error(
444
+ `DashScope wan2.7 image models accept at most ${WAN27_MAX_REFERENCE_IMAGES} reference images. Received ${refs.length}.`,
445
+ );
446
+ }
447
+ if (spec.family === 'wan27' && (req.n ?? 1) !== 1) {
448
+ throw new Error(
449
+ 'DashScope wan2.7 image models in kk image-gen support exactly one output image per request (extra images would be billed but discarded). Remove --n or use --n 1.',
450
+ );
451
+ }
452
+
453
+ const size = resolveSizeForModel(req.model, {
454
+ ...(req.width !== undefined && req.height !== undefined ? { size: `${req.width}x${req.height}` } : {}),
455
+ aspectRatio: req.aspectRatio,
456
+ quality: req.quality,
457
+ referenceImages: refs,
458
+ });
459
+ const base = (readEnv('DASHSCOPE_BASE_URL') || 'https://dashscope.aliyuncs.com').replace(/\/+$/g, '');
460
+ const url = `${base}/api/v1/services/aigc/multimodal-generation/generation`;
461
+
462
+ const content: Array<Record<string, unknown>> = [];
463
+ if (spec.family === 'wan27' && refs.length > 0) {
464
+ for (const refPath of refs) {
465
+ content.push({ image: await refToDataUri(refPath) });
466
+ }
467
+ }
468
+ content.push({ text: req.prompt });
469
+
470
+ const body = {
471
+ model: req.model,
472
+ input: { messages: [{ role: 'user', content }] },
473
+ parameters: buildParameters(spec.family, size),
474
+ };
475
+ const res = await fetchFn(url, {
476
+ method: 'POST',
477
+ headers: { 'content-type': 'application/json', authorization: `Bearer ${apiKey}` },
478
+ body: JSON.stringify(body),
479
+ });
480
+ if (!res.ok) throw new Error(`DashScope API error (${res.status}): ${await res.text()}`);
481
+ const bytes = await extractImageFromResponse((await res.json()) as DashScopeResponse, fetchFn);
482
+ return { bytes, extension: 'png', provider: 'dashscope', model: req.model };
483
+ },
484
+ };
485
+ };