kingkont 0.7.61 → 0.7.62

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
@@ -440,6 +440,16 @@
440
440
  <label id="locPickRow" style="display:none;">Локация
441
441
  <select id="locPickSelect"><option value="">— не выбрана —</option></select>
442
442
  </label>
443
+ <!-- Дефолтный промпт сцены (read-only превью + чекбокс «применить») -->
444
+ <div id="genDefaultPromptRow" style="display:none; margin-bottom:8px;">
445
+ <label style="display:flex; align-items:flex-start; gap:8px; cursor:pointer; padding:8px 10px; background:#1a1a1a; border:1px solid #333; border-radius:6px;">
446
+ <input type="checkbox" id="genDefaultPromptToggle" checked style="margin-top:2px; flex-shrink:0; accent-color:#7c3aed;">
447
+ <div style="flex:1; min-width:0;">
448
+ <div style="font-size:11px; color:#888; text-transform:uppercase; letter-spacing:0.5px; margin-bottom:3px;">Дефолт сцены — добавится к промпту при генерации</div>
449
+ <div id="genDefaultPromptText" style="font-size:12px; color:#aaccdd; font-family:ui-monospace,monospace; white-space:pre-wrap; word-break:break-word;"></div>
450
+ </div>
451
+ </label>
452
+ </div>
443
453
  <label><span style="white-space:nowrap; overflow:hidden; text-overflow:ellipsis; display:block;">Описание · <span style="color:#aae06a; font-family:ui-monospace,monospace; text-transform:none;">@имя</span> для ссылки на ноду</span>
444
454
  <textarea id="genPrompt" placeholder="Опиши, что должно быть. Печатай @ чтобы вставить ссылку на ноду..."></textarea>
445
455
  <div id="mentionPopup" class="mention-popup hidden"></div>
@@ -455,6 +465,34 @@
455
465
  </div>
456
466
 
457
467
  <!-- ===== Полноэкранный просмотр (image/video) ===== -->
468
+ <!-- ===== Дефолтные промпты сцены (per-kind) ===== -->
469
+ <div class="modal hidden" id="defaultPromptsModal">
470
+ <div class="modal-card" style="min-width:520px;">
471
+ <h3 id="defaultPromptsTitle">Дефолтные промпты сцены</h3>
472
+ <div class="hint" style="margin-bottom:14px;">
473
+ Текст добавляется к каждому новому промпту в этой сцене (НЕ редактируется
474
+ в поле промпта — применяется в момент генерации). В gen-модалке можно
475
+ отключить для конкретной генерации.
476
+ </div>
477
+ <label>Картинка
478
+ <textarea id="dpImage" rows="2" placeholder="например: cinematic lighting, soft shadows" style="width:100%;font-family:inherit;"></textarea>
479
+ </label>
480
+ <label>Видео
481
+ <textarea id="dpVideo" rows="2" placeholder="например: smooth camera movement, 24fps cinematic" style="width:100%;font-family:inherit;"></textarea>
482
+ </label>
483
+ <label>Аудио (TTS / SFX / музыка)
484
+ <textarea id="dpAudio" rows="2" placeholder="например: тёплый, мягкий тон" style="width:100%;font-family:inherit;"></textarea>
485
+ </label>
486
+ <label>Текст
487
+ <textarea id="dpText" rows="2" placeholder="например: пиши кратко, в стиле сценария" style="width:100%;font-family:inherit;"></textarea>
488
+ </label>
489
+ <div class="modal-actions">
490
+ <button id="dpCancel">Отмена</button>
491
+ <button id="dpSave" class="primary">Сохранить</button>
492
+ </div>
493
+ </div>
494
+ </div>
495
+
458
496
  <div class="fs-modal hidden" id="fsModal">
459
497
  <div class="fs-stage" id="fsStage"></div>
460
498
  <button id="fsPrev" title="Предыдущая (←)" class="fs-nav fs-nav-prev">‹</button>
package/lib/cli.js CHANGED
@@ -202,13 +202,26 @@ async function cmdGen({ positional, flags }) {
202
202
  await fsLib.saveScene(root, ref, scene);
203
203
  console.error(`[node] создана ${node.id} (status=generating)`);
204
204
 
205
+ // Применяем scene.settings.defaultPrompts[kind] (если задан и не отключён
206
+ // флагом --no-default-prompt). Префиксует «<default>, <user prompt>» —
207
+ // та же логика что в renderer/generate.js applyDefaultPrompt().
208
+ let effectivePrompt = prompt;
209
+ const dp = scene.settings?.defaultPrompts || {};
210
+ const defForKind = (dp[kind] || '').trim();
211
+ const useDefault = defForKind && !flags['no-default-prompt'] && !flags.noDefaultPrompt;
212
+ if (useDefault) {
213
+ const u = (prompt || '').trim();
214
+ effectivePrompt = u ? (defForKind + ', ' + u) : defForKind;
215
+ console.error(`[gen] applied scene default prompt for ${kind}: "${defForKind.slice(0, 80)}${defForKind.length > 80 ? '…' : ''}"`);
216
+ }
217
+
205
218
  // 4) Запускаем генерацию через провайдер.
206
- if (kind === 'text') return await runTextGeneration(root, ref, node, { prompt, model: flags.model, settings, flags });
219
+ if (kind === 'text') return await runTextGeneration(root, ref, node, { prompt: effectivePrompt, model: flags.model, settings, flags });
207
220
  if (kind === 'audio') {
208
221
  const subKind = flags['sub-kind'] || flags.subKind || (flags.tts ? 'tts' : null);
209
- if (subKind === 'sfx') return await runSfxGeneration(root, ref, node, { text: prompt, durationSeconds: flags.duration, settings, flags });
210
- if (subKind === 'music') return await runMusicGeneration(root, ref, node, { prompt, durationMs: flags['duration-ms'] || flags.durationMs, settings, flags });
211
- return await runTtsGeneration(root, ref, node, { text: prompt, voice: flags.voice, ttsModel: flags['tts-model'] || flags.ttsModel, settings, flags });
222
+ if (subKind === 'sfx') return await runSfxGeneration(root, ref, node, { text: effectivePrompt, durationSeconds: flags.duration, settings, flags });
223
+ if (subKind === 'music') return await runMusicGeneration(root, ref, node, { prompt: effectivePrompt, durationMs: flags['duration-ms'] || flags.durationMs, settings, flags });
224
+ return await runTtsGeneration(root, ref, node, { text: effectivePrompt, voice: flags.voice, ttsModel: flags['tts-model'] || flags.ttsModel, settings, flags });
212
225
  }
213
226
  // image | video — aspectRatio с фоллбеком на scene.settings.aspectRatio
214
227
  // (per-board дефолт). Если ни флаг, ни scene не указали — провайдер
@@ -220,7 +233,7 @@ async function cmdGen({ positional, flags }) {
220
233
  console.error(`[gen] using board's default aspectRatio: ${aspectFromScene}`);
221
234
  }
222
235
  return await runMediaGeneration(root, ref, node, {
223
- kind, prompt, modelKey: flags.model, imageInputs, videoInputs,
236
+ kind, prompt: effectivePrompt, modelKey: flags.model, imageInputs, videoInputs,
224
237
  aspectRatio: finalAspect,
225
238
  resolution: flags.resolution,
226
239
  duration: flags.duration,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kingkont",
3
- "version": "0.7.61",
3
+ "version": "0.7.62",
4
4
  "description": "KingKont · Chatium — нод-редактор сцен с AI-генерацией (картинки/видео/голос/SFX/музыка/текст)",
5
5
  "main": "main.js",
6
6
  "bin": {
package/renderer/board.js CHANGED
@@ -679,6 +679,7 @@ function showBoardContextMenu(kind, item, clientX, clientY) {
679
679
  // как fallback когда нода ещё не имеет своего aspect и юзер не выбрал
680
680
  // его в gen-modal'е.
681
681
  add('▭ Соотношение сторон…', () => promptBoardAspectRatio(kind, item));
682
+ add('📝 Дефолтные промпты…', () => openDefaultPromptsDialog(kind, item));
682
683
  if (kind === 'character') {
683
684
  add('⚙ Свойства персонажа', () => {
684
685
  // Открываем панель этого персонажа и потом её character-modal
@@ -723,6 +724,72 @@ async function promptBoardAspectRatio(kind, item) {
723
724
  console.log(`[board] ${kind}/${item.name} aspectRatio → ${chosen}`);
724
725
  }
725
726
 
727
+ // Открыть модалку «Дефолтные промпты сцены» — 4 textarea (image/video/audio/
728
+ // text). Сохраняется в scene.json → settings.defaultPrompts.
729
+ // При генерации каждой ноды соответствующий промпт добавляется к user-input
730
+ // (см. handler в renderer/generate.js); юзер может отключить чекбоксом в
731
+ // gen-modal'e для конкретной генерации.
732
+ async function openDefaultPromptsDialog(kind, item) {
733
+ const isActive = state.currentBoard?.kind === kind && state.currentBoard.name === item.name;
734
+ let metaSettings;
735
+ if (isActive) {
736
+ metaSettings = state.currentBoard.metadata.settings || {};
737
+ } else {
738
+ const meta = await loadBoardMetadata(item.handle);
739
+ metaSettings = meta.settings || {};
740
+ }
741
+ const dp = metaSettings.defaultPrompts || {};
742
+ // Заголовок и заполнение полей.
743
+ const titleEl = document.getElementById('defaultPromptsTitle');
744
+ if (titleEl) titleEl.textContent = `Дефолтные промпты для «${item.name}»`;
745
+ document.getElementById('dpImage').value = dp.image || '';
746
+ document.getElementById('dpVideo').value = dp.video || '';
747
+ document.getElementById('dpAudio').value = dp.audio || '';
748
+ document.getElementById('dpText').value = dp.text || '';
749
+ document.getElementById('defaultPromptsModal').classList.remove('hidden');
750
+ setTimeout(() => document.getElementById('dpImage').focus(), 30);
751
+
752
+ // Save handler — переустанавливаем каждый раз с замыканием на текущий item.
753
+ const saveBtn = document.getElementById('dpSave');
754
+ const cancelBtn = document.getElementById('dpCancel');
755
+ const close = () => document.getElementById('defaultPromptsModal').classList.add('hidden');
756
+ const onSave = async () => {
757
+ const next = {
758
+ image: document.getElementById('dpImage').value.trim(),
759
+ video: document.getElementById('dpVideo').value.trim(),
760
+ audio: document.getElementById('dpAudio').value.trim(),
761
+ text: document.getElementById('dpText').value.trim(),
762
+ };
763
+ // Удаляем пустые ключи чтобы не плодить мусор в JSON.
764
+ for (const k of Object.keys(next)) if (!next[k]) delete next[k];
765
+ if (isActive) {
766
+ if (!state.currentBoard.metadata.settings) state.currentBoard.metadata.settings = {};
767
+ if (Object.keys(next).length) {
768
+ state.currentBoard.metadata.settings.defaultPrompts = next;
769
+ } else {
770
+ delete state.currentBoard.metadata.settings.defaultPrompts;
771
+ }
772
+ scheduleSave();
773
+ } else {
774
+ const meta = await loadBoardMetadata(item.handle);
775
+ meta.settings = meta.settings || {};
776
+ if (Object.keys(next).length) meta.settings.defaultPrompts = next;
777
+ else delete meta.settings.defaultPrompts;
778
+ await saveBoardMetadata(item.handle, meta);
779
+ }
780
+ close();
781
+ saveBtn.removeEventListener('click', onSave);
782
+ cancelBtn.removeEventListener('click', onCancel);
783
+ };
784
+ const onCancel = () => {
785
+ close();
786
+ saveBtn.removeEventListener('click', onSave);
787
+ cancelBtn.removeEventListener('click', onCancel);
788
+ };
789
+ saveBtn.addEventListener('click', onSave);
790
+ cancelBtn.addEventListener('click', onCancel);
791
+ }
792
+
726
793
  // Переименовать board (папку): создать новую папку с тем же именем, перенести
727
794
  // содержимое, удалить старую. На macOS-FSAH прямого rename нет, делаем
728
795
  // copy+remove. Если board сейчас открыт — переоткрываем после переноса.
@@ -330,6 +330,7 @@ async function openGenModal(kind) {
330
330
  if (kind === 'audio') { loadVoices(); }
331
331
  closeMentionPopup();
332
332
  syncSourceRefRow();
333
+ syncDefaultPromptRow();
333
334
  // Если открываем image-генерацию и есть исходная картинка-референс —
334
335
  // дефолт «↺ Как у исходной» (юзер обычно хочет такой же формат как у
335
336
  // источника). Если потом юзер выберет другой aspect — это его выбор,
@@ -746,6 +747,7 @@ document.querySelectorAll('#genModal [data-kind]').forEach(b => {
746
747
  $('genPrompt').setAttribute('placeholder', ph);
747
748
  syncSourceRefRow();
748
749
  syncCharLocRows();
750
+ syncDefaultPromptRow();
749
751
  // То же auto-default «↺ Как у исходной» при переключении на image-kind
750
752
  // (см. комментарий в openGenModal).
751
753
  if (state.genKind === 'image'
@@ -801,6 +803,47 @@ function nearestSupportedAspect(ratio) {
801
803
  return best.name;
802
804
  }
803
805
 
806
+ // Показать «Дефолт сцены» в gen-modal'е если задан scene.settings
807
+ // .defaultPrompts[currentKind]. Чекбокс «применить» включён по дефолту,
808
+ // его state хранится в state.useDefaultPrompt и сбрасывается на каждое
809
+ // открытие модалки.
810
+ function syncDefaultPromptRow() {
811
+ const row = document.getElementById('genDefaultPromptRow');
812
+ const text = document.getElementById('genDefaultPromptText');
813
+ const toggle = document.getElementById('genDefaultPromptToggle');
814
+ if (!row || !text || !toggle) return;
815
+ const dp = state.currentBoard?.metadata?.settings?.defaultPrompts || {};
816
+ const value = dp[state.genKind] || '';
817
+ if (value) {
818
+ text.textContent = value;
819
+ toggle.checked = true;
820
+ state.useDefaultPrompt = true;
821
+ row.style.display = '';
822
+ } else {
823
+ row.style.display = 'none';
824
+ state.useDefaultPrompt = false;
825
+ }
826
+ }
827
+
828
+ // Слушатель чекбокса (один раз навешиваем).
829
+ const _dpToggleEl = document.getElementById('genDefaultPromptToggle');
830
+ if (_dpToggleEl) _dpToggleEl.addEventListener('change', () => {
831
+ state.useDefaultPrompt = _dpToggleEl.checked;
832
+ });
833
+
834
+ // Применяет дефолтный промпт сцены к user-prompt'у. Префиксует с разделителем
835
+ // ", " (типичная конвенция для image/video моделей). Если default отключён
836
+ // или пуст — возвращает prompt как есть.
837
+ function applyDefaultPrompt(userPrompt, kind) {
838
+ if (!state.useDefaultPrompt) return userPrompt;
839
+ const dp = state.currentBoard?.metadata?.settings?.defaultPrompts || {};
840
+ const def = (dp[kind] || '').trim();
841
+ if (!def) return userPrompt;
842
+ const u = (userPrompt || '').trim();
843
+ if (!u) return def;
844
+ return def + ', ' + u;
845
+ }
846
+
804
847
  // === Source frame controls ===
805
848
  function syncSourceRefRow() {
806
849
  const showable = state.sourceRef && (state.genKind === 'image' || state.genKind === 'video');
@@ -1263,9 +1306,11 @@ $('genSubmit').addEventListener('click', async () => {
1263
1306
  alert('После раскрытия @-ссылок текст пустой. Проверь содержимое исходных нод.');
1264
1307
  return;
1265
1308
  }
1309
+ // Применяем дефолтный промпт сцены для audio (если включён в gen-modal'е).
1310
+ const withDefault = applyDefaultPrompt(resolvedRaw, 'audio');
1266
1311
  // Тоны прикладываем как ElevenLabs v3 audio-tags перед текстом
1267
1312
  const tonePrefix = state.activeTones.map(t => `[${t}]`).join(' ');
1268
- const finalText = tonePrefix ? `${tonePrefix} ${resolvedRaw}` : resolvedRaw;
1313
+ const finalText = tonePrefix ? `${tonePrefix} ${withDefault}` : withDefault;
1269
1314
  // Запоминаем имя персонажа ДО сброса timelineGenTarget — fallback на voiceId
1270
1315
  const charKey = state.timelineGenTarget?.charName || voiceId;
1271
1316
  const usedTones = [...state.activeTones];
@@ -1368,6 +1413,12 @@ $('genSubmit').addEventListener('click', async () => {
1368
1413
  }
1369
1414
  let resolvedPrompt = resolveMentions(rawPrompt, mediaRefs);
1370
1415
  if (sourceMarker) resolvedPrompt = sourceMarker + resolvedPrompt;
1416
+ // Применяем дефолтный промпт сцены (если включён в gen-modal'е).
1417
+ // Префиксует «<default>, <user prompt>». Пишется в API-запрос и в
1418
+ // node.generated.prompt — чтобы при regenerate взялся тот же
1419
+ // эффективный промпт. rawPrompt (без default) сохраняем тоже —
1420
+ // в node.generated.rawPrompt — для UI restore при «Изменить и запустить».
1421
+ resolvedPrompt = applyDefaultPrompt(resolvedPrompt, state.genKind);
1371
1422
 
1372
1423
  // Если выбран aspect 'source' — резолвим в реальный ratio из исходной
1373
1424
  // картинки ДО того как обнулим state.sourceRef (ниже).
@@ -476,6 +476,7 @@ async function openGenerateForRef(fromNode, clientX, clientY, forceKind) {
476
476
  $('genPrompt').value = '[@' + nodeRefKey(fromNode) + '] ';
477
477
  }
478
478
  syncSourceRefRow();
479
+ if (typeof syncDefaultPromptRow === 'function') syncDefaultPromptRow();
479
480
 
480
481
  // Если генерим image из существующей картинки-ноды — дефолт «↺ Как у
481
482
  // исходной» (юзер обычно хочет такой же формат как у источника).
package/skill/SKILL.md CHANGED
@@ -103,6 +103,22 @@ kingkont gen <project> 'Серия 1' --kind=image --prompt="..." --aspect-ratio
103
103
  `settings.aspectRatio`, если он отличается от текущего и юзер явно сказал
104
104
  «пусть в этой сцене будет такой формат» (или если поле было null).
105
105
 
106
+ ### Дефолтные промпты сцены
107
+
108
+ `scene.json → settings.defaultPrompts` — объект с ключами `image / video /
109
+ audio / text`. Если задан, CLI `kingkont gen` автоматически префиксует его
110
+ к user-prompt'у при генерации (`<default>, <user prompt>`). Это позволяет
111
+ юзеру задать общий стиль/тон для всей сцены один раз.
112
+
113
+ Поведение CLI:
114
+ - Default подхватывается из scene.json автоматически — флага не нужно.
115
+ - Чтобы отключить для одной генерации: `--no-default-prompt`.
116
+ - В stderr пишется `[gen] applied scene default prompt for <kind>: "..."`.
117
+
118
+ Юзер обычно настраивает defaults через UI (правый клик на сцену →
119
+ «📝 Дефолтные промпты…»), но может попросить установить через CLI —
120
+ правь `scene.json` напрямую: `settings.defaultPrompts.<kind> = "..."`.
121
+
106
122
  ## ⚠️ Text-ноды — генерируй САМ, не через `kingkont gen --kind=text`
107
123
 
108
124
  Ты — Claude. Когда юзер просит «напиши диалог», «придумай реплику»,