kingkont 0.7.60 → 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 +40 -0
- package/lib/cli.js +18 -5
- package/package.json +1 -1
- package/renderer/board.js +153 -21
- package/renderer/generate.js +52 -1
- package/renderer/settings.js +1 -0
- package/renderer/styles.css +13 -0
- package/skill/SKILL.md +16 -0
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,8 +465,38 @@
|
|
|
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>
|
|
498
|
+
<button id="fsPrev" title="Предыдущая (←)" class="fs-nav fs-nav-prev">‹</button>
|
|
499
|
+
<button id="fsNext" title="Следующая (→)" class="fs-nav fs-nav-next">›</button>
|
|
460
500
|
<button id="fsClose" title="Закрыть (Esc)">×</button>
|
|
461
501
|
</div>
|
|
462
502
|
|
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:
|
|
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:
|
|
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
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 сейчас открыт — переоткрываем после переноса.
|
|
@@ -1638,33 +1705,94 @@ function showNodeLogs(node) {
|
|
|
1638
1705
|
$('logsClose').addEventListener('click', () => $('logsModal').classList.add('hidden'));
|
|
1639
1706
|
|
|
1640
1707
|
// =================== Полноэкранный просмотр ===================
|
|
1708
|
+
// Текущая нода в полноэкранном просмотре + список соседей того же type
|
|
1709
|
+
// для навигации стрелками. Список считаем один раз при openFullscreen
|
|
1710
|
+
// чтобы не пересортировывать на каждом prev/next.
|
|
1711
|
+
let _fsCurrent = null;
|
|
1712
|
+
let _fsSiblings = [];
|
|
1713
|
+
|
|
1714
|
+
// Сортировка нод «слева направо, сверху вниз» row-major.
|
|
1715
|
+
// Группируем по строкам через round(y/ROW_HEIGHT) bucket — sort transitive
|
|
1716
|
+
// (важно, иначе порядок непредсказуемый при больших спредах по y).
|
|
1717
|
+
const FS_ROW_HEIGHT = 100;
|
|
1718
|
+
function _fsSortRowMajor(nodes) {
|
|
1719
|
+
return nodes.slice().sort((a, b) => {
|
|
1720
|
+
const ra = Math.round((a.y || 0) / FS_ROW_HEIGHT);
|
|
1721
|
+
const rb = Math.round((b.y || 0) / FS_ROW_HEIGHT);
|
|
1722
|
+
if (ra !== rb) return ra - rb;
|
|
1723
|
+
return (a.x || 0) - (b.x || 0);
|
|
1724
|
+
});
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1641
1727
|
async function openFullscreen(node) {
|
|
1642
1728
|
const stage = $('fsStage');
|
|
1643
1729
|
stage.innerHTML = '';
|
|
1644
1730
|
try {
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
img.src = url;
|
|
1655
|
-
stage.appendChild(img);
|
|
1656
|
-
} else if (node.type === 'video') {
|
|
1657
|
-
const v = document.createElement('video');
|
|
1658
|
-
v.src = url;
|
|
1659
|
-
v.controls = true;
|
|
1660
|
-
v.autoplay = true;
|
|
1661
|
-
stage.appendChild(v);
|
|
1731
|
+
// Собираем siblings того же type на текущей доске для навигации.
|
|
1732
|
+
// image-нода → все остальные image; video → video. Без файла исключаем.
|
|
1733
|
+
if (state.currentBoard?.metadata?.nodes) {
|
|
1734
|
+
const sameType = state.currentBoard.metadata.nodes.filter(n =>
|
|
1735
|
+
n.type === node.type && n.file
|
|
1736
|
+
);
|
|
1737
|
+
_fsSiblings = _fsSortRowMajor(sameType);
|
|
1738
|
+
} else {
|
|
1739
|
+
_fsSiblings = [node];
|
|
1662
1740
|
}
|
|
1741
|
+
_fsCurrent = node;
|
|
1742
|
+
await _fsLoadCurrent();
|
|
1743
|
+
_fsUpdateNavButtons();
|
|
1663
1744
|
$('fsModal').classList.remove('hidden');
|
|
1664
1745
|
} catch (e) {
|
|
1665
1746
|
console.error('openFullscreen failed:', e);
|
|
1666
1747
|
}
|
|
1667
1748
|
}
|
|
1749
|
+
|
|
1750
|
+
async function _fsLoadCurrent() {
|
|
1751
|
+
const node = _fsCurrent;
|
|
1752
|
+
const stage = $('fsStage');
|
|
1753
|
+
stage.innerHTML = '';
|
|
1754
|
+
if (!node) return;
|
|
1755
|
+
let url = state.currentBoard?.urls?.[node.file];
|
|
1756
|
+
if (!url && state.currentBoard?.handle) {
|
|
1757
|
+
const fh = await resolveBoardFile(state.currentBoard.handle, node.file);
|
|
1758
|
+
url = URL.createObjectURL(await fh.getFile());
|
|
1759
|
+
state.currentBoard.urls[node.file] = url;
|
|
1760
|
+
}
|
|
1761
|
+
if (!url) return;
|
|
1762
|
+
if (node.type === 'image') {
|
|
1763
|
+
const img = document.createElement('img');
|
|
1764
|
+
img.src = url;
|
|
1765
|
+
stage.appendChild(img);
|
|
1766
|
+
} else if (node.type === 'video') {
|
|
1767
|
+
const v = document.createElement('video');
|
|
1768
|
+
v.src = url;
|
|
1769
|
+
v.controls = true;
|
|
1770
|
+
v.autoplay = true;
|
|
1771
|
+
stage.appendChild(v);
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
function _fsUpdateNavButtons() {
|
|
1776
|
+
const prev = $('fsPrev');
|
|
1777
|
+
const next = $('fsNext');
|
|
1778
|
+
const idx = _fsCurrent ? _fsSiblings.indexOf(_fsCurrent) : -1;
|
|
1779
|
+
if (prev) prev.disabled = idx <= 0;
|
|
1780
|
+
if (next) next.disabled = idx < 0 || idx >= _fsSiblings.length - 1;
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
async function fsNavigate(direction /* -1 или +1 */) {
|
|
1784
|
+
if (!_fsCurrent) return;
|
|
1785
|
+
const idx = _fsSiblings.indexOf(_fsCurrent);
|
|
1786
|
+
const next = idx + direction;
|
|
1787
|
+
if (next < 0 || next >= _fsSiblings.length) return;
|
|
1788
|
+
_fsCurrent = _fsSiblings[next];
|
|
1789
|
+
// Останавливаем текущее video если было.
|
|
1790
|
+
const v = $('fsStage').querySelector('video');
|
|
1791
|
+
if (v) { try { v.pause(); } catch {} }
|
|
1792
|
+
await _fsLoadCurrent();
|
|
1793
|
+
_fsUpdateNavButtons();
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1668
1796
|
function closeFullscreen() {
|
|
1669
1797
|
const modal = $('fsModal');
|
|
1670
1798
|
if (modal.classList.contains('hidden')) return;
|
|
@@ -1672,16 +1800,20 @@ function closeFullscreen() {
|
|
|
1672
1800
|
if (v) { try { v.pause(); } catch {} }
|
|
1673
1801
|
$('fsStage').innerHTML = '';
|
|
1674
1802
|
modal.classList.add('hidden');
|
|
1803
|
+
_fsCurrent = null;
|
|
1804
|
+
_fsSiblings = [];
|
|
1675
1805
|
}
|
|
1676
1806
|
$('fsClose').addEventListener('click', closeFullscreen);
|
|
1807
|
+
$('fsPrev')?.addEventListener('click', e => { e.stopPropagation(); fsNavigate(-1); });
|
|
1808
|
+
$('fsNext')?.addEventListener('click', e => { e.stopPropagation(); fsNavigate(+1); });
|
|
1677
1809
|
$('fsModal').addEventListener('click', e => {
|
|
1678
1810
|
if (e.target.id === 'fsModal' || e.target.id === 'fsStage') closeFullscreen();
|
|
1679
1811
|
});
|
|
1680
1812
|
window.addEventListener('keydown', e => {
|
|
1681
|
-
if (
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
}
|
|
1813
|
+
if ($('fsModal').classList.contains('hidden')) return;
|
|
1814
|
+
if (e.key === 'Escape') { e.stopPropagation(); closeFullscreen(); }
|
|
1815
|
+
else if (e.key === 'ArrowLeft') { e.stopPropagation(); fsNavigate(-1); }
|
|
1816
|
+
else if (e.key === 'ArrowRight') { e.stopPropagation(); fsNavigate(+1); }
|
|
1685
1817
|
}, true);
|
|
1686
1818
|
// Универсальный copy-helper: clipboard API → fallback на execCommand.
|
|
1687
1819
|
async function copyText(text) {
|
package/renderer/generate.js
CHANGED
|
@@ -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} ${
|
|
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 (ниже).
|
package/renderer/settings.js
CHANGED
|
@@ -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/renderer/styles.css
CHANGED
|
@@ -463,6 +463,19 @@
|
|
|
463
463
|
}
|
|
464
464
|
.fs-modal #fsClose:hover { background: rgba(255,68,68,0.7); }
|
|
465
465
|
|
|
466
|
+
/* Кнопки навигации prev/next в полноэкранном просмотре */
|
|
467
|
+
.fs-modal .fs-nav {
|
|
468
|
+
position: absolute; top: 50%; transform: translateY(-50%);
|
|
469
|
+
background: rgba(0,0,0,0.55); border: 1px solid #444; color: #fff;
|
|
470
|
+
font-size: 36px; line-height: 1; padding: 4px 14px; border-radius: 6px;
|
|
471
|
+
cursor: pointer; user-select: none;
|
|
472
|
+
transition: background 0.12s;
|
|
473
|
+
}
|
|
474
|
+
.fs-modal .fs-nav:hover { background: rgba(0,0,0,0.85); }
|
|
475
|
+
.fs-modal .fs-nav:disabled { opacity: 0.3; cursor: default; }
|
|
476
|
+
.fs-modal .fs-nav-prev { left: 16px; }
|
|
477
|
+
.fs-modal .fs-nav-next { right: 16px; }
|
|
478
|
+
|
|
466
479
|
/* === Реплики (боковая панель справа) === */
|
|
467
480
|
.repliques-panel {
|
|
468
481
|
width: 380px; background: #1c1c1c; border-left: 1px solid #333;
|
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. Когда юзер просит «напиши диалог», «придумай реплику»,
|