kingkont 0.7.68 → 0.7.70

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/index.html CHANGED
@@ -343,6 +343,7 @@
343
343
  <label id="imageModelRow">Модель для картинки
344
344
  <div class="seg-control">
345
345
  <button class="seg active" data-img-model="nano-banana-2" type="button" title="Высокое качество, медленно">Nano Banana 2</button>
346
+ <button class="seg" data-img-model="nano-banana-pro" type="button" title="Pro-версия nano-banana, выше качество">Nano Banana Pro</button>
346
347
  <button class="seg" data-img-model="grok" type="button">Grok</button>
347
348
  <button class="seg" data-img-model="seedream" type="button">Seedream 4.5</button>
348
349
  <button class="seg" data-img-model="seedream-5-lite" type="button">Seedream 5 Lite</button>
@@ -350,6 +351,13 @@
350
351
  <button class="seg" data-img-model="sdxl-lightning" type="button" title="Очень быстрая (3-5с)">⚡ SDXL Lightning</button>
351
352
  </div>
352
353
  </label>
354
+ <label id="imageQualityRow">Качество
355
+ <div class="seg-control" id="imageQualityCtl">
356
+ <button class="seg" data-img-quality="low" type="button" title="Быстрее, дешевле">low</button>
357
+ <button class="seg active" data-img-quality="medium" type="button">medium</button>
358
+ <button class="seg" data-img-quality="high" type="button" title="Лучше детализация, медленнее">high</button>
359
+ </div>
360
+ </label>
353
361
  <label id="imageOptionsRow">Соотношение сторон
354
362
  <div class="seg-control" id="imageAspectCtl">
355
363
  <button class="seg" data-img-asp="source" type="button" id="imgAspSource"
package/lib/cli.js CHANGED
@@ -248,6 +248,7 @@ async function cmdGen({ positional, flags }) {
248
248
  aspectRatio: finalAspect,
249
249
  resolution: flags.resolution,
250
250
  duration: flags.duration,
251
+ quality: flags.quality,
251
252
  firstFrame: flags['first-frame'] || flags.firstFrame,
252
253
  lastFrame: flags['last-frame'] || flags.lastFrame,
253
254
  settings, flags,
@@ -370,9 +371,9 @@ async function cmdUpload({ positional, flags }) {
370
371
  // =============================================================================
371
372
 
372
373
  async function runMediaGeneration(root, ref, node, args) {
373
- const { kind, prompt, modelKey, imageInputs, videoInputs, aspectRatio, resolution, duration, firstFrame, lastFrame, settings, flags } = args;
374
+ const { kind, prompt, modelKey, imageInputs, videoInputs, aspectRatio, resolution, duration, quality, firstFrame, lastFrame, settings, flags } = args;
374
375
  const start = await providers.startGeneration({
375
- kind, prompt, modelKey, imageInputs, videoInputs, aspectRatio, resolution, duration, firstFrame, lastFrame, settings,
376
+ kind, prompt, modelKey, imageInputs, videoInputs, aspectRatio, resolution, duration, quality, firstFrame, lastFrame, settings,
376
377
  });
377
378
  console.error(`[task] ${start.taskId} provider=${start.provider}`);
378
379
  await updateNode(root, ref, node.id, n => {
package/lib/providers.js CHANGED
@@ -52,6 +52,7 @@ function summarizeBody(body) {
52
52
 
53
53
  const KIE_IMAGE_MODELS = {
54
54
  'nano-banana-2': 'nano-banana-2',
55
+ 'nano-banana-pro': 'nano-banana-pro',
55
56
  'grok': 'grok-imagine/text-to-image',
56
57
  'seedream': 'seedream/4.5-text-to-image',
57
58
  'seedream-5-lite': 'seedream/5-lite-text-to-image',
@@ -212,7 +213,7 @@ async function kiePoll(taskId) {
212
213
  */
213
214
  async function startGeneration(args) {
214
215
  const { kind, prompt, modelKey, imageInputs, videoInputs, firstFrame, lastFrame,
215
- aspectRatio, resolution, duration, settings: s } = args;
216
+ aspectRatio, resolution, duration, quality, settings: s } = args;
216
217
  if (kind !== 'image' && kind !== 'video') throw new Error(`unknown kind: ${kind}`);
217
218
  if (!prompt && kind !== 'video') throw new Error('prompt обязателен');
218
219
 
@@ -229,7 +230,7 @@ async function startGeneration(args) {
229
230
  const kieSupportsModel = !!kieMap[kieKey];
230
231
 
231
232
  if (kieAvailable && kieSupportsModel) {
232
- return await _startGenerationViaKie({ kind, prompt, key: kieKey, imageInputs, videoInputs, aspectRatio, resolution, duration });
233
+ return await _startGenerationViaKie({ kind, prompt, key: kieKey, imageInputs, videoInputs, aspectRatio, resolution, duration, quality });
233
234
  }
234
235
 
235
236
  // Chatium-путь.
@@ -252,6 +253,7 @@ async function startGeneration(args) {
252
253
  prompt, model: fullModel, aspectRatio, resolution,
253
254
  outputFormat: isSeedream ? 'jpeg' : 'jpg',
254
255
  };
256
+ if (quality) body.quality = quality; // 'low' | 'medium' | 'high' — модель сама решит как мапить
255
257
  if (!isSeedream && Array.isArray(imageInputs) && imageInputs.length) {
256
258
  body.imageInputs = imageInputs;
257
259
  }
@@ -271,29 +273,31 @@ async function startGeneration(args) {
271
273
 
272
274
  // Внутренний helper: KIE-путь startGeneration. Вынесен чтобы не дублировать
273
275
  // логику между «KIE первичный» и старым «KIE fallback».
274
- async function _startGenerationViaKie({ kind, prompt, key, imageInputs, videoInputs, aspectRatio, resolution, duration }) {
276
+ async function _startGenerationViaKie({ kind, prompt, key, imageInputs, videoInputs, aspectRatio, resolution, duration, quality }) {
275
277
  const map = kind === 'video' ? KIE_VIDEO_MODELS : KIE_IMAGE_MODELS;
276
278
  const fullModel = map[key];
277
279
  if (!fullModel) throw new Error(`unknown ${kind} model: ${key}`);
278
280
 
279
281
  const input = { prompt };
280
282
  if (kind === 'image') {
281
- if (key === 'nano-banana-2') {
283
+ if (key === 'nano-banana-2' || key === 'nano-banana-pro') {
282
284
  if (imageInputs?.length) input.image_input = imageInputs;
283
285
  if (aspectRatio) input.aspect_ratio = aspectRatio;
284
286
  input.resolution = resolution || '1K';
285
287
  input.output_format = 'jpg';
288
+ if (quality) input.quality = quality;
286
289
  } else if (key === 'grok') {
287
290
  if (aspectRatio) input.aspect_ratio = aspectRatio;
288
291
  input.nsfw_checker = false;
289
- input.enable_pro = false;
292
+ input.enable_pro = quality === 'high'; // pro-mode для high
290
293
  } else if (key === 'seedream' || key === 'seedream-5-lite') {
291
294
  input.aspect_ratio = aspectRatio || '16:9';
292
- input.quality = 'high';
295
+ input.quality = quality || 'high';
293
296
  input.nsfw_checker = false;
294
297
  } else if (key === 'flux-schnell' || key === 'sdxl-lightning') {
295
298
  if (aspectRatio) input.aspect_ratio = aspectRatio;
296
299
  input.output_format = 'jpg';
300
+ // flux/sdxl быстрые, quality для них N/A — пропускаем.
297
301
  }
298
302
  } else {
299
303
  if (imageInputs?.length) input.reference_image_urls = imageInputs;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kingkont",
3
- "version": "0.7.68",
3
+ "version": "0.7.70",
4
4
  "description": "KingKont · Chatium — нод-редактор сцен с AI-генерацией (картинки/видео/голос/SFX/музыка/текст)",
5
5
  "main": "main.js",
6
6
  "bin": {
package/renderer/board.js CHANGED
@@ -799,7 +799,18 @@ function _dpReadList() {
799
799
  return out;
800
800
  }
801
801
 
802
+ // Cleanup от предыдущего вызова openDefaultPromptsDialog, чтобы handler'ы
803
+ // не накапливались. Накопление было настоящим багом: если юзер закрывал
804
+ // dialog через Esc (а не «Отмена»), handler'ы оставались привязанными к
805
+ // старому item; в следующем открытии для ДРУГОЙ сцены клик Save срабатывал
806
+ // для обоих item'ов одновременно — дефолты сохранялись в обе сцены.
807
+ let _dpDialogCleanup = null;
808
+
802
809
  async function openDefaultPromptsDialog(kind, item) {
810
+ // Снимаем handler'ы прошлого открытия (если осталось).
811
+ if (typeof _dpDialogCleanup === 'function') _dpDialogCleanup();
812
+ _dpDialogCleanup = null;
813
+
803
814
  const isActive = state.currentBoard?.kind === kind && state.currentBoard.name === item.name;
804
815
  let metaSettings;
805
816
  if (isActive) {
@@ -823,8 +834,8 @@ async function openDefaultPromptsDialog(kind, item) {
823
834
  document.getElementById('defaultPromptsModal').classList.remove('hidden');
824
835
  setTimeout(() => list.querySelector('textarea')?.focus(), 30);
825
836
 
826
- // Save / Cancel / Add — навешиваем фрешевые handler'ы removeEventListener
827
- // на close, чтобы при следующем openDefaultPromptsDialog не накапливались).
837
+ // Save / Cancel / Add — handler'ы привязаны к ЭТОМУ конкретному item
838
+ // через замыкание. Cleanup гарантирует что прошлые handler'ы уже сняты.
828
839
  const saveBtn = document.getElementById('dpSave');
829
840
  const cancelBtn = document.getElementById('dpCancel');
830
841
  const addBtn = document.getElementById('dpAdd');
@@ -833,6 +844,7 @@ async function openDefaultPromptsDialog(kind, item) {
833
844
  saveBtn.removeEventListener('click', onSave);
834
845
  cancelBtn.removeEventListener('click', onCancel);
835
846
  addBtn.removeEventListener('click', onAdd);
847
+ _dpDialogCleanup = null;
836
848
  };
837
849
  const onAdd = () => {
838
850
  list.appendChild(_dpRenderItem({ id: crypto.randomUUID(), text: '', kinds: ['image'], enabled: true }));
@@ -858,6 +870,9 @@ async function openDefaultPromptsDialog(kind, item) {
858
870
  saveBtn.addEventListener('click', onSave);
859
871
  cancelBtn.addEventListener('click', onCancel);
860
872
  addBtn.addEventListener('click', onAdd);
873
+ // Сохраняем cleanup чтобы вызвать при закрытии через Esc или повторном
874
+ // openDefaultPromptsDialog (см. строку выше).
875
+ _dpDialogCleanup = cleanup;
861
876
  }
862
877
 
863
878
  // Переименовать board (папку): создать новую папку с тем же именем, перенести
@@ -104,6 +104,7 @@ function openPhraseFor(charInfo) {
104
104
  $('imageModelRow').style.display = 'none';
105
105
 
106
106
  $('imageOptionsRow').style.display = 'none';
107
+ $('imageQualityRow').style.display = 'none';
107
108
  $('videoOptionsRow').style.display = 'none';
108
109
 
109
110
  $('videoModelRow').style.display = 'none';
@@ -301,6 +302,7 @@ async function openGenModal(kind) {
301
302
  $('imageModelRow').style.display = kind === 'image' ? '' : 'none';
302
303
 
303
304
  $('imageOptionsRow').style.display = kind === 'image' ? '' : 'none';
305
+ $('imageQualityRow').style.display = kind === 'image' ? '' : 'none';
304
306
  $('videoOptionsRow').style.display = kind === 'video' ? '' : 'none';
305
307
 
306
308
  $('videoModelRow').style.display = kind === 'video' ? '' : 'none';
@@ -742,6 +744,7 @@ document.querySelectorAll('#genModal [data-kind]').forEach(b => {
742
744
  $('imageModelRow').style.display = state.genKind === 'image' ? '' : 'none';
743
745
 
744
746
  $('imageOptionsRow').style.display = state.genKind === 'image' ? '' : 'none';
747
+ $('imageQualityRow').style.display = state.genKind === 'image' ? '' : 'none';
745
748
  $('videoOptionsRow').style.display = state.genKind === 'video' ? '' : 'none';
746
749
 
747
750
  $('videoModelRow').style.display = state.genKind === 'video' ? '' : 'none';
@@ -1161,8 +1164,30 @@ document.querySelectorAll('#genModal [data-img-model]').forEach(b => {
1161
1164
  document.querySelectorAll('#genModal [data-img-model]').forEach(x => x.classList.remove('active'));
1162
1165
  b.classList.add('active');
1163
1166
  state.imageModel = b.dataset.imgModel;
1167
+ localStorage.setItem('imageModel', state.imageModel);
1164
1168
  });
1165
1169
  });
1170
+ // При первом открытии: подсветить активную из state (а не первую в DOM).
1171
+ function syncImageModelActive() {
1172
+ document.querySelectorAll('#genModal [data-img-model]').forEach(b =>
1173
+ b.classList.toggle('active', b.dataset.imgModel === state.imageModel));
1174
+ }
1175
+ syncImageModelActive();
1176
+ // Переключатель качества (low / medium / high) — пишется в каждый запрос
1177
+ // генерации картинки (см. submit handler).
1178
+ document.querySelectorAll('#genModal [data-img-quality]').forEach(b => {
1179
+ b.addEventListener('click', () => {
1180
+ document.querySelectorAll('#genModal [data-img-quality]').forEach(x => x.classList.remove('active'));
1181
+ b.classList.add('active');
1182
+ state.imageQuality = b.dataset.imgQuality;
1183
+ localStorage.setItem('imageQuality', state.imageQuality);
1184
+ });
1185
+ });
1186
+ function syncImageQualityActive() {
1187
+ document.querySelectorAll('#genModal [data-img-quality]').forEach(b =>
1188
+ b.classList.toggle('active', b.dataset.imgQuality === state.imageQuality));
1189
+ }
1190
+ syncImageQualityActive();
1166
1191
  // Переключатель модели видео
1167
1192
  document.querySelectorAll('#genModal [data-vid-model]').forEach(b => {
1168
1193
  b.addEventListener('click', () => {
@@ -1499,6 +1524,7 @@ $('genSubmit').addEventListener('click', async () => {
1499
1524
  'seedream': 'seedream/4.5-text-to-image',
1500
1525
  'seedream-5-lite': 'seedream/5-lite-text-to-image',
1501
1526
  'nano-banana-2': 'nano-banana-2',
1527
+ 'nano-banana-pro': 'nano-banana-pro',
1502
1528
  }[state.imageModel] || 'nano-banana-2')
1503
1529
  : ({
1504
1530
  'seedance-2': 'bytedance/seedance-2',
@@ -1518,6 +1544,7 @@ $('genSubmit').addEventListener('click', async () => {
1518
1544
  } : {}),
1519
1545
  ...(kind === 'image' ? {
1520
1546
  aspectRatio: resolvedImageAspect,
1547
+ quality: state.imageQuality,
1521
1548
  } : {}),
1522
1549
  },
1523
1550
  };
@@ -1678,6 +1705,8 @@ async function startGenerationJob(node, kind, prompt, mediaRefs, boardHandle, bK
1678
1705
  // Grok-imagine требует aspect_ratio из {2:3, 3:2, 1:1, 9:16, 16:9}.
1679
1706
  // Остальные модели (nano-banana-2, seedream и др.) тоже принимают.
1680
1707
  submitBody.aspectRatio = node.generated?.aspectRatio ?? boardAspect ?? state.imageAspect;
1708
+ const q = node.generated?.quality ?? state.imageQuality;
1709
+ if (q) submitBody.quality = q;
1681
1710
  }
1682
1711
  logJob(node.id, `POST /api/generate body: ${logSafe(submitBody)}`);
1683
1712
  logJob(node.id, `POST /api/generate (image_input=${imageInputs.length}, video_input=${videoInputs.length}, model=${modelKey})`);
package/renderer/state.js CHANGED
@@ -397,7 +397,8 @@ const state = {
397
397
  filmHandle: null,
398
398
  currentBoard: null, // { kind, name, key, handle, metadata, urls }
399
399
  genKind: 'image',
400
- imageModel: 'nano-banana-2', // 'nano-banana-2' | 'grok' | ...
400
+ imageModel: localStorage.getItem('imageModel') || 'nano-banana-2', // 'nano-banana-2' | 'nano-banana-pro' | 'grok' | ...
401
+ imageQuality: localStorage.getItem('imageQuality') || 'medium', // low | medium | high
401
402
  imageAspect: localStorage.getItem('imageAspect') || '1:1', // 1:1 / 16:9 / 9:16 / 3:2 / 2:3 / 4:3 / 3:4
402
403
  videoModel: localStorage.getItem('videoModel') || 'seedance-2', // 'seedance-2' | 'kling-o1' | 'kling-3.0' | ...
403
404
  ttsModel: localStorage.getItem('ttsModel') || 'qwen/qwen3-tts', // qwen/elevenlabs/v3/minimax/speech-02-hd/gemini
package/server.js CHANGED
@@ -91,11 +91,11 @@ function sendError(res, e, defaultStatus = 500, providerHeader = null) {
91
91
 
92
92
  async function handleGenerate(req, res) {
93
93
  const body = await readJson(req);
94
- const { kind, prompt, imageInputs, videoInputs, aspectRatio, resolution, duration, model: modelKey } = body;
94
+ const { kind, prompt, imageInputs, videoInputs, aspectRatio, resolution, duration, quality, model: modelKey } = body;
95
95
  if (!prompt || !prompt.trim()) return send(res, 400, { error: 'prompt обязателен' });
96
96
  try {
97
97
  const r = await providers.startGeneration({
98
- kind, prompt, modelKey, imageInputs, videoInputs, aspectRatio, resolution, duration,
98
+ kind, prompt, modelKey, imageInputs, videoInputs, aspectRatio, resolution, duration, quality,
99
99
  settings: getSettings(),
100
100
  });
101
101
  send(res, 200, { taskId: r.taskId }, { 'X-Provider': r.provider });