kingkont 0.20.18 → 0.20.20

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kingkont",
3
- "version": "0.20.18",
3
+ "version": "0.20.20",
4
4
  "description": "KingKont \u00b7 Chatium \u2014 \u043d\u043e\u0434-\u0440\u0435\u0434\u0430\u043a\u0442\u043e\u0440 \u0441\u0446\u0435\u043d \u0441 AI-\u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0435\u0439 (\u043a\u0430\u0440\u0442\u0438\u043d\u043a\u0438/\u0432\u0438\u0434\u0435\u043e/\u0433\u043e\u043b\u043e\u0441/SFX/\u043c\u0443\u0437\u044b\u043a\u0430/\u0442\u0435\u043a\u0441\u0442)",
5
5
  "main": "main.js",
6
6
  "bin": {
package/renderer/board.js CHANGED
@@ -4099,10 +4099,21 @@ function showNodeContextMenu(node, clientX, clientY) {
4099
4099
  });
4100
4100
  }
4101
4101
  else if (node.status === 'draft') {
4102
- add('▶ Запустить генерацию', () => regenerateNode(node));
4102
+ // Раньше обе кнопки звали regenerateNode → обе открывали modal,
4103
+ // юзер не понимал чем они отличаются. Теперь:
4104
+ // ▶ Запустить генерацию — direct-run без модалки (сохранённые
4105
+ // параметры + incoming-edge рефы).
4106
+ // ✎ Изменить и запустить — открыть modal для правки промпта/
4107
+ // модели/настроек, потом запустить.
4108
+ add('▶ Запустить генерацию', () => runNodeJobDirectly(node));
4103
4109
  add('✎ Изменить и запустить', () => regenerateNode(node));
4104
4110
  }
4105
4111
  else if (node.generated) {
4112
+ // Готовая нода: даём оба варианта симметрично с draft-кейсом.
4113
+ // «↻ Перегенерировать» — без модалки (быстрый ре-ран), «(правка)»
4114
+ // — с модалкой. Файл текущей версии уйдёт в node.history[] (см.
4115
+ // regenerateInto / startGenerationJob — там делается syncHistorySlot).
4116
+ add('↻ Перегенерировать', () => runNodeJobDirectly(node));
4106
4117
  add('↻ Перегенерировать (правка)', () => regenerateNode(node));
4107
4118
  }
4108
4119
  if (node.type === 'image' || node.type === 'video' || node.type === 'audio') {
@@ -2128,6 +2128,113 @@ async function startGenerationJob(node, kind, prompt, mediaRefs, boardHandle, bK
2128
2128
  }
2129
2129
  }
2130
2130
 
2131
+ // Запустить генерацию ноды НАПРЯМУЮ (без gen-modal'а), с сохранёнными
2132
+ // параметрами в node.generated. Юзер: «неясно чем отличаются кнопки в ПКМ
2133
+ // 'Запустить генерацию' и 'Изменить и запустить' — обе открывают модалку».
2134
+ // Теперь:
2135
+ // • «▶ Запустить генерацию» → runNodeJobDirectly (без модалки).
2136
+ // • «✎ Изменить и запустить» → regenerateNode (открывает модалку с
2137
+ // предзаполненными полями для правки).
2138
+ // Логика маршрутизации совпадает с resumeJob (kind/subKind → конкретная
2139
+ // job-функция). Дополнительно — собираем incoming-edge refs (см.
2140
+ // gatherIncomingMediaRefs), чтобы юзер не терял авто-рефы при direct-run.
2141
+ async function runNodeJobDirectly(node) {
2142
+ if (!node) return;
2143
+ if (node.status === 'generating') return; // защита от повторного клика
2144
+ const g = node.generated;
2145
+ if (!g || !g.prompt) {
2146
+ alert('У ноды нет промпта — открой ПКМ → «Изменить и запустить».');
2147
+ return;
2148
+ }
2149
+ const board = state.currentBoard;
2150
+ if (!board) return;
2151
+ const bKey = board.key;
2152
+ const boardHandle = board.handle;
2153
+ const kind = g.kind || node.type;
2154
+ const subKind = g.subKind || (kind === 'audio' ? 'voice' : null);
2155
+
2156
+ // Готовим refs: сохранённые в node.generated.refs + incoming-edges (если
2157
+ // граф расширили с момента прошлой генерации). Дедуп по file+boardHandle.
2158
+ // boardHandle re-attach через текущий — refs из других досок (sheet'ы)
2159
+ // здесь не поддерживаем при direct-run, для них юзер должен открыть modal.
2160
+ let refs = (g.refs || []).map(r => ({ ...r, boardHandle: r.boardHandle || boardHandle }));
2161
+ if (typeof gatherIncomingMediaRefs === 'function') {
2162
+ for (const ir of gatherIncomingMediaRefs(node.id)) {
2163
+ if (refs.some(r => r.file === ir.file && r.boardHandle === ir.boardHandle)) continue;
2164
+ refs.push(ir);
2165
+ }
2166
+ }
2167
+
2168
+ // Если у ноды есть готовый файл (готовая нода, direct-rerun) — seedим
2169
+ // history-слот ТЕКУЩЕЙ версией перед запуском, чтобы юзер мог откатиться
2170
+ // на старый файл через ←/→ в footer'е. Без этого syncHistorySlot (внутри
2171
+ // mutateNode) ничего не делает на пустом node.history (см. media.js:531).
2172
+ // regenerateInto делает это руками — копируем сюда тот же приём.
2173
+ if (node.file && (!Array.isArray(node.history) || !node.history.length)) {
2174
+ node.history = [{
2175
+ file: node.file,
2176
+ generated: { ...g },
2177
+ status: undefined,
2178
+ error: undefined,
2179
+ }];
2180
+ node.historyIndex = 0;
2181
+ }
2182
+
2183
+ // Помечаем как generating ДО запуска — UI сразу показывает spinner.
2184
+ node.status = 'generating';
2185
+ node.error = undefined;
2186
+ node.generated = { ...g, refs: refs.map(r => ({ name: r.name, key: r.key, type: r.type, file: r.file })) };
2187
+ scheduleSave();
2188
+ // Перерисовываем ноду — body уйдёт в spinner-state.
2189
+ const el = canvas.querySelector(`.node[data-id="${node.id}"]`);
2190
+ if (el) {
2191
+ const body = el.querySelector('.node-body');
2192
+ if (body && typeof renderNodeBody === 'function') await renderNodeBody(node, body);
2193
+ }
2194
+
2195
+ // Маршрутизация: повторяет resumeJob + chat.js generate_node tool.
2196
+ try {
2197
+ if (kind === 'audio') {
2198
+ if (subKind === 'music' && typeof runMusicJob === 'function') {
2199
+ await runMusicJob(node, g.prompt, g.durationMs || null, boardHandle, bKey);
2200
+ } else if (subKind === 'sfx' && typeof runSfxJob === 'function') {
2201
+ await runSfxJob(node, g.prompt, g.durationMs ? g.durationMs / 1000 : null, boardHandle, bKey);
2202
+ } else {
2203
+ await runTTSJob(node, g.prompt, boardHandle, bKey, g.voiceId);
2204
+ }
2205
+ } else if (kind === 'text') {
2206
+ const imageRefs = refs.filter(r => r.type === 'image' && r.file);
2207
+ const model = g.model || g.modelKey || 'anthropic/claude-sonnet-4';
2208
+ await runTextJob(node, g.prompt, model, boardHandle, bKey, imageRefs);
2209
+ } else {
2210
+ // image / video — modelKey должен быть short-key (state.videoModel-style),
2211
+ // а не full-slug. Если g.modelKey не сохранён (старые ноды) — конвертируем
2212
+ // из g.model через _shortVideoModelFromGen/_shortImageModelFromGen.
2213
+ let modelKey = g.modelKey;
2214
+ if (!modelKey) {
2215
+ if (kind === 'video' && typeof _shortVideoModelFromGen === 'function') {
2216
+ modelKey = _shortVideoModelFromGen(g);
2217
+ } else if (kind === 'image' && typeof _shortImageModelFromGen === 'function') {
2218
+ modelKey = _shortImageModelFromGen(g);
2219
+ }
2220
+ }
2221
+ console.log(`[runNodeJobDirectly ${kind}] using node-saved params:`, {
2222
+ modelKey,
2223
+ ...(kind === 'video' ? {
2224
+ duration: g.duration, resolution: g.resolution, aspectRatio: g.aspectRatio,
2225
+ } : { aspectRatio: g.aspectRatio }),
2226
+ });
2227
+ await startGenerationJob(node, kind, g.prompt, refs, boardHandle, bKey, modelKey);
2228
+ }
2229
+ } catch (e) {
2230
+ console.error('runNodeJobDirectly failed', e);
2231
+ node.status = 'error';
2232
+ node.error = e?.message || String(e);
2233
+ scheduleSave();
2234
+ }
2235
+ }
2236
+ window.runNodeJobDirectly = runNodeJobDirectly;
2237
+
2131
2238
  async function resumeJob(node, bKey, boardHandle) {
2132
2239
  if (state.jobs.has(node.id)) return;
2133
2240
  if (!node.generated || node.status !== 'generating') return;
package/renderer/media.js CHANGED
@@ -648,6 +648,39 @@ async function restartJob(nodeId) {
648
648
  }
649
649
  }
650
650
 
651
+ // Конвертер full-slug → short-key для UI-кнопок [data-vid-model] / [data-img-model].
652
+ // Старые ноды могли сохранять только g.model (полный slug провайдера),
653
+ // а селектор кнопки сравнивает с short-key (state.videoModel = 'kling-3.0').
654
+ // Без этой конверсии restore модели из ноды ничего не делал, и юзер видел
655
+ // "последний выбранный мной", а не модель ноды.
656
+ const _VIDEO_SLUG_TO_KEY = {
657
+ 'bytedance/seedance-2': 'seedance-2',
658
+ 'bytedance/seedance-2-fast': 'seedance-2-fast',
659
+ 'kwaivgi/kling-o1': 'kling-o1',
660
+ 'kling-3.0/video': 'kling-3.0',
661
+ };
662
+ const _IMAGE_SLUG_TO_KEY = {
663
+ 'nano-banana-2': 'nano-banana-2',
664
+ 'nano-banana-pro': 'nano-banana-pro',
665
+ 'grok-imagine/text-to-image': 'grok',
666
+ 'seedream/4.5-text-to-image': 'seedream',
667
+ 'seedream/5-lite-text-to-image': 'seedream-5-lite',
668
+ 'gpt-image-2-text-to-image': 'gpt-image-2',
669
+ 'gpt-image/1.5-text-to-image': 'gpt-image-1.5',
670
+ 'flux/schnell': 'flux-schnell',
671
+ 'sdxl/lightning': 'sdxl-lightning',
672
+ };
673
+ function _shortVideoModelFromGen(g) {
674
+ if (g.modelKey) return g.modelKey;
675
+ if (g.model && _VIDEO_SLUG_TO_KEY[g.model]) return _VIDEO_SLUG_TO_KEY[g.model];
676
+ return null;
677
+ }
678
+ function _shortImageModelFromGen(g) {
679
+ if (g.modelKey) return g.modelKey;
680
+ if (g.model && _IMAGE_SLUG_TO_KEY[g.model]) return _IMAGE_SLUG_TO_KEY[g.model];
681
+ return null;
682
+ }
683
+
651
684
  async function regenerateNode(node) {
652
685
  if (node.status === 'generating') return;
653
686
  const g = node.generated || {};
@@ -742,23 +775,33 @@ async function regenerateNode(node) {
742
775
 
743
776
  $('ttsModelRow').style.display = state.genKind === 'audio' ? '' : 'none';
744
777
 
745
- if (g.modelKey && state.genKind === 'image') {
746
- state.imageModel = g.modelKey;
747
- document.querySelectorAll('#genModal [data-img-model]').forEach(b =>
748
- b.classList.toggle('active', b.dataset.imgModel === g.modelKey));
749
- }
750
778
  if (state.genKind === 'image') {
779
+ const imgKey = _shortImageModelFromGen(g);
780
+ if (imgKey) state.imageModel = imgKey;
781
+ document.querySelectorAll('#genModal [data-img-model]').forEach(b =>
782
+ b.classList.toggle('active', b.dataset.imgModel === state.imageModel));
751
783
  if (g.aspectRatio) state.imageAspect = g.aspectRatio;
752
784
  syncImageAspectActive();
753
785
  }
754
- // Видео: подставляем сохранённые duration/resolution/aspect для regenerate
786
+ // Видео: подставляем сохранённые duration/resolution/aspect/model для
787
+ // regenerate. Юзер: «если в ноде есть указанные параметры длины видео,
788
+ // модели, ratio — безусловно используй их в диалоге и в самой генерации».
789
+ // Раньше: `if (g.modelKey) state.videoModel = ...` — если у ноды только
790
+ // полный slug (g.model = 'bytedance/seedance-2', а modelKey пустой), мы
791
+ // оставляли state.videoModel = "последнее выбранное юзером". Теперь
792
+ // конвертируем full-slug → short-key через _shortVideoModelFromGen.
793
+ // duration приводим к number — старые ноды могли сохранять строкой.
755
794
  if (state.genKind === 'video') {
756
- if (g.duration) state.videoDuration = g.duration;
795
+ const vidKey = _shortVideoModelFromGen(g);
796
+ if (vidKey) state.videoModel = vidKey;
797
+ if (g.duration != null) state.videoDuration = +g.duration || state.videoDuration;
757
798
  if (g.resolution) state.videoResolution = g.resolution;
758
799
  if (g.aspectRatio) state.videoAspect = g.aspectRatio;
759
- if (g.modelKey) state.videoModel = g.modelKey;
760
800
  syncVideoOptionsActive();
761
801
  syncVideoModelActive();
802
+ console.log('[regenerateNode video] restored from node:',
803
+ { modelKey: state.videoModel, duration: state.videoDuration,
804
+ resolution: state.videoResolution, aspect: state.videoAspect });
762
805
  }
763
806
  if (state.genKind === 'audio') {
764
807
  if (g.ttsModel) state.ttsModel = g.ttsModel;