kingkont 0.15.0 → 0.15.1

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/lib/providers.js CHANGED
@@ -602,7 +602,12 @@ async function generateMusic(args) {
602
602
  const { prompt, durationMs, settings: s } = args;
603
603
  if (!prompt) throw new Error('нужен prompt');
604
604
 
605
- if (s.useChatium && s.chatium?.token && s.chatium?.base) {
605
+ // Приоритет direct ElevenLabs если включён + есть ключ — у юзера могут
606
+ // быть кастомные саб-настройки и платный ElevenLabs (зачем тратить
607
+ // Chatium-кредиты). Аналогичная логика в generateTts/generateSfx.
608
+ const directElevenAvailable = s.useElevenlabs && process.env.ELEVENLABS_API_KEY;
609
+
610
+ if (s.useChatium && s.chatium?.token && s.chatium?.base && !directElevenAvailable) {
606
611
  return await audioViaChatium(s, { kind: 'music', prompt, durationMs });
607
612
  }
608
613
  if (!s.useElevenlabs) throw new Error('Войдите в KingKont или ElevenLabs для аудио.');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kingkont",
3
- "version": "0.15.0",
3
+ "version": "0.15.1",
4
4
  "description": "KingKont · Chatium — нод-редактор сцен с AI-генерацией (картинки/видео/голос/SFX/музыка/текст)",
5
5
  "main": "main.js",
6
6
  "bin": {
package/renderer/chat.js CHANGED
@@ -183,6 +183,50 @@
183
183
  },
184
184
  },
185
185
 
186
+ move_node: {
187
+ description: 'Передвинуть ноду в новые координаты (или сдвинуть относительно текущих через dx/dy). x/y абсолютные в canvas-coord. Если нужно сдвинуть несколько нод подряд — выдавай несколько move_node.',
188
+ params: '{"id":"<node-id>","x":<absolute>,"y":<absolute>,"dx":<relative-shift>,"dy":<relative-shift>}',
189
+ async handler({ id, x, y, dx, dy }) {
190
+ const b = state.currentBoard;
191
+ if (!b) throw new Error('доска не выбрана');
192
+ const node = b.metadata.nodes.find(n => n.id === id);
193
+ if (!node) throw new Error(`нода не найдена: ${id}`);
194
+ if (typeof x === 'number') node.x = x;
195
+ if (typeof y === 'number') node.y = y;
196
+ if (typeof dx === 'number') node.x = (node.x || 0) + dx;
197
+ if (typeof dy === 'number') node.y = (node.y || 0) + dy;
198
+ // Clamp к >= 50 чтобы не уехала в негатив за viewport.
199
+ if (node.x < 50) node.x = 50;
200
+ if (node.y < 50) node.y = 50;
201
+ scheduleSave();
202
+ if (typeof renderCanvas === 'function') await renderCanvas();
203
+ return { ok: true, x: node.x, y: node.y };
204
+ },
205
+ },
206
+
207
+ edit_scene_json: {
208
+ description: 'Прямое редактирование scene.json текущей доски — для нестандартных операций (массовая правка позиций, sort нод, чистка metadata, и т.п.). Передай patch как объект — поля заменяют существующие. Для замены ВСЕГО массива nodes — передай {nodes: [...]}. ВНИМАНИЕ: ошибка тут разрушит сцену — обязательно вызови take_snapshot ПЕРЕД.',
209
+ params: '{"patch":{...}}',
210
+ async handler({ patch }) {
211
+ const b = state.currentBoard;
212
+ if (!b) throw new Error('доска не выбрана');
213
+ if (!patch || typeof patch !== 'object') throw new Error('patch должен быть объектом');
214
+ // Применяем patch к metadata (top-level merge). Для nodes/connections
215
+ // если переданы — заменяем массив целиком (не merge).
216
+ for (const [k, v] of Object.entries(patch)) {
217
+ b.metadata[k] = v;
218
+ }
219
+ scheduleSave();
220
+ if (typeof renderCanvas === 'function') await renderCanvas();
221
+ if (typeof renderConnections === 'function') renderConnections();
222
+ return {
223
+ ok: true,
224
+ nodesCount: (b.metadata.nodes || []).length,
225
+ connectionsCount: (b.metadata.connections || []).length,
226
+ };
227
+ },
228
+ },
229
+
186
230
  update_node_prompt: {
187
231
  description: 'Обновить промпт у image/video/audio ноды (без re-генерации).',
188
232
  params: '{"id":"<node-id>","prompt":"<new>"}',
@@ -278,9 +322,18 @@
278
322
  async handler({ nodeId, trackKind, start, duration }) {
279
323
  const b = state.currentBoard;
280
324
  if (!b) throw new Error('доска не выбрана');
325
+ // Re-read scene с диска чтобы видеть свежий node.file (после
326
+ // background-генерации). Без этого chat'оподобный flow «добавь и
327
+ // сразу в таймлайн» — файла ещё нет в node.file in-memory.
328
+ try {
329
+ const sceneFh = await b.handle.getFileHandle('scene.json');
330
+ const txt = await (await sceneFh.getFile()).text();
331
+ const scene = JSON.parse(txt);
332
+ if (Array.isArray(scene.nodes)) b.metadata.nodes = scene.nodes;
333
+ } catch {}
281
334
  const node = b.metadata.nodes.find(n => n.id === nodeId);
282
335
  if (!node) throw new Error(`нода не найдена: ${nodeId}`);
283
- if (!node.file) throw new Error('у ноды нет файла (нужно сначала сгенерировать)');
336
+ if (!node.file) throw new Error('у ноды нет файла (нужно сначала сгенерировать и дождаться)');
284
337
  if (typeof getTimeline !== 'function') throw new Error('getTimeline недоступен');
285
338
  const tl = getTimeline();
286
339
  const tk = trackKind === 'audio' ? 'audio' : 'video';
@@ -304,8 +357,14 @@
304
357
  };
305
358
  track.clips.push(clip);
306
359
  scheduleSave();
307
- if (!document.getElementById('timelinePanel').classList.contains('hidden')) {
308
- if (typeof renderTimeline === 'function') renderTimeline();
360
+ // Force open + render: иначе thumbnails не загружаются (рендер скип'ался
361
+ // при скрытой панели, и при следующем открытии состояние UI было stale).
362
+ const tlPanel = document.getElementById('timelinePanel');
363
+ if (tlPanel?.classList.contains('hidden')) {
364
+ // Открываем таймлайн чтобы юзер видел что добавилось.
365
+ document.getElementById('timelineBtn')?.click();
366
+ } else if (typeof renderTimeline === 'function') {
367
+ await renderTimeline();
309
368
  }
310
369
  return { ok: true, clipId: clip.id, trackId: track.id, start: clip.start, duration: clip.duration };
311
370
  },
@@ -1423,12 +1482,15 @@
1423
1482
  history = []; snapshots = [];
1424
1483
  renderHistory(); renderSnapshotBar();
1425
1484
  },
1426
- // Reset-on-close: останавливаем polling, чистим UI. История на сервере
1427
- // остаётся (server-side persistence). re-open того же проекта подгрузит.
1485
+ // Reset-on-close: НЕ останавливаем polling серверный loop может ещё
1486
+ // ждать tool-results. Если бросим polling, loop зависает (tools никто
1487
+ // не исполнит). Клиентский UI чистим, но poll продолжается в фоне на
1488
+ // сессию ПОСЛЕДНЕГО открытого проекта (она же станет «фоновым chat'ом»).
1489
+ // При открытии другого проекта — startPolling переключит sessionKey.
1428
1490
  resetInMemory: async () => {
1429
- stopPolling();
1430
1491
  history = []; snapshots = [];
1431
1492
  renderHistory(); renderSnapshotBar();
1493
+ // Polling оставляем активным — это и есть «бг-чат».
1432
1494
  },
1433
1495
  // board.js зовёт после openFilm чтобы подгрузить chat-историю проекта.
1434
1496
  loadFromCurrentProject: () => loadHistoryFromCurrentProject(),