kingkont 0.20.68 → 0.20.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 +7 -8
- package/lib/providers.js +33 -1
- package/package.json +1 -1
- package/renderer/board.js +0 -115
- package/renderer/generate.js +16 -0
- package/renderer/settings.js +14 -8
- package/renderer/styles.css +3 -0
package/index.html
CHANGED
|
@@ -45,14 +45,12 @@
|
|
|
45
45
|
<input id="sidebarSearch" type="text" placeholder="Поиск…" autocomplete="off">
|
|
46
46
|
</div>
|
|
47
47
|
|
|
48
|
-
<!-- Раздел «Персонажи»
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
<button id="newCharacter"></button>
|
|
55
|
-
<div id="characterList"></div>
|
|
48
|
+
<!-- Раздел «Персонажи» — возвращён по просьбе юзера. Данные (папки
|
|
49
|
+
_characters/<name>/) живут на сервере (cloud-проекты) и в локальной
|
|
50
|
+
копии — ничего не терялось при временном скрытии, только UI. -->
|
|
51
|
+
<div class="sidebar-section kk-edit-only">
|
|
52
|
+
<h3>Персонажи <button id="newCharacter" disabled title="Создать персонажа">+</button></h3>
|
|
53
|
+
<div class="sidebar-list" id="characterList"></div>
|
|
56
54
|
</div>
|
|
57
55
|
|
|
58
56
|
<div class="sidebar-section kk-edit-only">
|
|
@@ -458,6 +456,7 @@
|
|
|
458
456
|
<button class="seg" data-vid-model="seedance-2-fast" type="button" title="Быстрее, чуть проще качество">⚡ Seedance 2 Fast</button>
|
|
459
457
|
<button class="seg" data-vid-model="kling-o1" type="button" title="Kling O1 — креативный, поддерживает reference-видео">Kling O1</button>
|
|
460
458
|
<button class="seg" data-vid-model="kling-3.0" type="button" title="Kling 3.0 — multi-shot до 15с">Kling 3.0</button>
|
|
459
|
+
<button class="seg" data-vid-model="omni" type="button" title="Gemini Omni — text/image/video→video, до 7 image-рефов, аудио и персонажи">Gemini Omni</button>
|
|
461
460
|
</div>
|
|
462
461
|
</div>
|
|
463
462
|
<div class="field-row" id="videoOptionsRow" style="display: none;">Параметры видео
|
package/lib/providers.js
CHANGED
|
@@ -87,7 +87,14 @@ const KIE_IMAGE_MODELS = {
|
|
|
87
87
|
'gpt-image-1.5': 'gpt-image/1.5-text-to-image',
|
|
88
88
|
};
|
|
89
89
|
const KIE_VIDEO_MODELS = {
|
|
90
|
-
'seedance-2':
|
|
90
|
+
'seedance-2': 'bytedance/seedance-2',
|
|
91
|
+
'seedance-2-fast': 'bytedance/seedance-2-fast',
|
|
92
|
+
'kling-o1': 'kwaivgi/kling-o1',
|
|
93
|
+
'kling-3.0': 'kling-3.0/video',
|
|
94
|
+
// Gemini Omni: text/image/video → video. Поддерживает до 7 image-рефов,
|
|
95
|
+
// до 1 видео (≤30 с), до 3 audio_ids, до 3 character_ids. KIE-doc:
|
|
96
|
+
// docs.kie.ai/market/gemini-omni-video. Slug: gemini-omni-video.
|
|
97
|
+
'omni': 'gemini-omni-video',
|
|
91
98
|
};
|
|
92
99
|
|
|
93
100
|
const CHATIUM_IMAGE_MODELS = {
|
|
@@ -113,6 +120,10 @@ const CHATIUM_VIDEO_MODELS = {
|
|
|
113
120
|
'runway': 'runway',
|
|
114
121
|
'grok-i2v': 'grok-imagine/image-to-video',
|
|
115
122
|
'grok-t2v': 'grok-imagine/text-to-video',
|
|
123
|
+
// Gemini Omni — KIE-only (OpenRouter video API его не имеет, только
|
|
124
|
+
// multimodal-chat Gemini'и читают видео на вход). Через Chatium-proxy
|
|
125
|
+
// должно работать если @start/sdk прокидывает slug в KIE.
|
|
126
|
+
'omni': 'gemini-omni-video',
|
|
116
127
|
};
|
|
117
128
|
|
|
118
129
|
// OpenRouter image-gen модели — приоритетный путь для nano-banana
|
|
@@ -639,6 +650,27 @@ async function _startGenerationViaKie({ kind, prompt, key, imageInputs, videoInp
|
|
|
639
650
|
if (quality) input.quality = quality;
|
|
640
651
|
input.output_format = 'jpg';
|
|
641
652
|
}
|
|
653
|
+
} else if (key === 'omni') {
|
|
654
|
+
// Gemini Omni KIE-формат: image_urls (≤7), video_list ({url,start,ends}),
|
|
655
|
+
// duration ENUM '4'|'6'|'8'|'10' как string, aspect_ratio '16:9'|'9:16',
|
|
656
|
+
// resolution '720p'|'1080p'|'4k'. НЕ принимает reference_image_urls /
|
|
657
|
+
// reference_video_urls — это для seedance/kling.
|
|
658
|
+
if (imageInputs?.length) input.image_urls = imageInputs.slice(0, 7);
|
|
659
|
+
if (videoInputs?.length) {
|
|
660
|
+
// KIE требует объекты {url, start, ends}; берём первое видео целиком
|
|
661
|
+
// (start=0, ends оставим без указания — KIE возьмёт всю длину).
|
|
662
|
+
input.video_list = [{ url: videoInputs[0], start: 0 }];
|
|
663
|
+
}
|
|
664
|
+
if (aspectRatio === '16:9' || aspectRatio === '9:16') {
|
|
665
|
+
input.aspect_ratio = aspectRatio;
|
|
666
|
+
}
|
|
667
|
+
if (resolution === '720p' || resolution === '1080p' || resolution === '4k') {
|
|
668
|
+
input.resolution = resolution;
|
|
669
|
+
}
|
|
670
|
+
// duration в Omni — STRING из {'4','6','8','10'}. Если юзер дал 5 — округлим.
|
|
671
|
+
const dur = +duration || 4;
|
|
672
|
+
const omniDur = dur <= 4 ? '4' : dur <= 6 ? '6' : dur <= 8 ? '8' : '10';
|
|
673
|
+
input.duration = omniDur;
|
|
642
674
|
} else {
|
|
643
675
|
if (imageInputs?.length) input.reference_image_urls = imageInputs;
|
|
644
676
|
if (videoInputs?.length) input.reference_video_urls = videoInputs;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kingkont",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.70",
|
|
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
|
@@ -2647,102 +2647,6 @@ function attachInlineDescription(el, node) {
|
|
|
2647
2647
|
});
|
|
2648
2648
|
}
|
|
2649
2649
|
|
|
2650
|
-
// Копирование медиа-файла ноды в СИСТЕМНЫЙ буфер обмена (не в state.clipboard,
|
|
2651
|
-
// который для внутренней copy/paste нод). Юзер: «нужно чтобы любую картинку и
|
|
2652
|
-
// любое видео можно было скопировать в буфер обмена чтобы вставить в другое
|
|
2653
|
-
// приложение».
|
|
2654
|
-
//
|
|
2655
|
-
// IMAGE: ClipboardItem с image/png MIME. Если исходный формат — jpeg/webp/gif,
|
|
2656
|
-
// перерисовываем в PNG через canvas (большинство ОС-приложений принимают PNG
|
|
2657
|
-
// из clipboard). Также пишем оригинальный MIME как дополнительный тип —
|
|
2658
|
-
// некоторые приложения предпочитают исходный формат.
|
|
2659
|
-
// VIDEO: Web Clipboard API не поддерживает video MIME-типы (только image/* и
|
|
2660
|
-
// text/*). Fallback: пишем file:// URL (Electron) или name файла в text/plain
|
|
2661
|
-
// и blob:URL — юзер может вставить как ссылку. В Finder/проводник file://
|
|
2662
|
-
// URL открывает файл; в браузер — открывает в новой вкладке.
|
|
2663
|
-
async function copyMediaToSystemClipboard(node) {
|
|
2664
|
-
if (!node || !node.file) throw new Error('У ноды нет файла');
|
|
2665
|
-
if (!navigator.clipboard || !navigator.clipboard.write) {
|
|
2666
|
-
throw new Error('Clipboard API недоступен в этом браузере');
|
|
2667
|
-
}
|
|
2668
|
-
const fh = await resolveBoardFile(state.currentBoard.handle, node.file);
|
|
2669
|
-
const file = await fh.getFile();
|
|
2670
|
-
if (node.type === 'image') {
|
|
2671
|
-
const mime = file.type || _guessMimeFromName(node.file) || 'image/png';
|
|
2672
|
-
// Web Clipboard API в Chrome поддерживает только image/png для write
|
|
2673
|
-
// (image/jpeg, image/webp выбрасывают «Type X not supported on write»).
|
|
2674
|
-
// Поэтому если файл не PNG — перерисовываем через canvas в PNG.
|
|
2675
|
-
const pngBlob = (mime === 'image/png')
|
|
2676
|
-
? file
|
|
2677
|
-
: await _imageBlobToPng(file);
|
|
2678
|
-
await navigator.clipboard.write([new ClipboardItem({ 'image/png': pngBlob })]);
|
|
2679
|
-
return { format: 'image/png', size: pngBlob.size };
|
|
2680
|
-
}
|
|
2681
|
-
if (node.type === 'video') {
|
|
2682
|
-
// Video не поддерживается Clipboard API → ставим text/plain с file-name +
|
|
2683
|
-
// text/uri-list с blob:URL (приложения принимающие URL смогут открыть).
|
|
2684
|
-
const url = URL.createObjectURL(file);
|
|
2685
|
-
const filename = node.file.split('/').pop() || 'video';
|
|
2686
|
-
try {
|
|
2687
|
-
await navigator.clipboard.write([new ClipboardItem({
|
|
2688
|
-
'text/plain': new Blob([url], { type: 'text/plain' }),
|
|
2689
|
-
'text/uri-list': new Blob([url], { type: 'text/uri-list' }),
|
|
2690
|
-
})]);
|
|
2691
|
-
} catch {
|
|
2692
|
-
// Fallback на старый writeText API если ClipboardItem с uri-list не
|
|
2693
|
-
// поддерживается.
|
|
2694
|
-
await navigator.clipboard.writeText(url);
|
|
2695
|
-
}
|
|
2696
|
-
// ВНИМАНИЕ: blob:URL живёт только пока эта вкладка открыта. Юзер должен
|
|
2697
|
-
// вставить URL ДО reload'а. Альтернатива — использовать ПКМ «💾 Сохранить
|
|
2698
|
-
// как…» и потом drag-n-drop'ать файл из системы (сообщаем юзеру).
|
|
2699
|
-
if (typeof showToast === 'function') {
|
|
2700
|
-
showToast('⚠ Видео скопировано как blob:URL — вставь ДО перезагрузки. Для надёжного копирования используй «💾 Сохранить как…».', 'warn');
|
|
2701
|
-
}
|
|
2702
|
-
return { format: 'blob-url', filename };
|
|
2703
|
-
}
|
|
2704
|
-
throw new Error('Тип ноды не поддерживается: ' + node.type);
|
|
2705
|
-
}
|
|
2706
|
-
|
|
2707
|
-
// Конвертация blob картинки в PNG через canvas.
|
|
2708
|
-
async function _imageBlobToPng(blob) {
|
|
2709
|
-
return new Promise((resolve, reject) => {
|
|
2710
|
-
const img = new Image();
|
|
2711
|
-
const url = URL.createObjectURL(blob);
|
|
2712
|
-
img.onload = () => {
|
|
2713
|
-
try {
|
|
2714
|
-
const c = document.createElement('canvas');
|
|
2715
|
-
c.width = img.naturalWidth;
|
|
2716
|
-
c.height = img.naturalHeight;
|
|
2717
|
-
const ctx = c.getContext('2d');
|
|
2718
|
-
ctx.drawImage(img, 0, 0);
|
|
2719
|
-
c.toBlob(b => {
|
|
2720
|
-
URL.revokeObjectURL(url);
|
|
2721
|
-
if (b) resolve(b);
|
|
2722
|
-
else reject(new Error('canvas.toBlob вернул null'));
|
|
2723
|
-
}, 'image/png');
|
|
2724
|
-
} catch (e) {
|
|
2725
|
-
URL.revokeObjectURL(url);
|
|
2726
|
-
reject(e);
|
|
2727
|
-
}
|
|
2728
|
-
};
|
|
2729
|
-
img.onerror = () => {
|
|
2730
|
-
URL.revokeObjectURL(url);
|
|
2731
|
-
reject(new Error('Не удалось загрузить картинку'));
|
|
2732
|
-
};
|
|
2733
|
-
img.src = url;
|
|
2734
|
-
});
|
|
2735
|
-
}
|
|
2736
|
-
|
|
2737
|
-
function _guessMimeFromName(name) {
|
|
2738
|
-
const ext = (name || '').toLowerCase().split('.').pop();
|
|
2739
|
-
return ({
|
|
2740
|
-
png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg',
|
|
2741
|
-
webp: 'image/webp', gif: 'image/gif',
|
|
2742
|
-
mp4: 'video/mp4', webm: 'video/webm', mov: 'video/quicktime',
|
|
2743
|
-
})[ext] || null;
|
|
2744
|
-
}
|
|
2745
|
-
|
|
2746
2650
|
// Скопировать / вставить настройки сцены (aspectRatio + defaultPrompts).
|
|
2747
2651
|
// Хранение — in-memory state.clipboardSceneSettings (живёт до closeProject).
|
|
2748
2652
|
// Юзер: «сделай возможность скопировать настройки из одной сцены в другую».
|
|
@@ -4476,25 +4380,6 @@ function showNodeContextMenu(node, clientX, clientY) {
|
|
|
4476
4380
|
add('📥 Вставить (Cmd+V)', () => pasteClipboardNodes(), { disabled: !canPaste });
|
|
4477
4381
|
const canReplace = !!(state.clipboard?.length);
|
|
4478
4382
|
add('⇄ Заменить из буфера', () => replaceNodeFromClipboard(node), { disabled: !canReplace });
|
|
4479
|
-
// «📋 Копировать в системный буфер» — image/video. Юзер: «нужно чтобы
|
|
4480
|
-
// любую картинку и любое видео можно было скопировать в буфер обмена
|
|
4481
|
-
// чтобы вставить в другое приложение». Image — через ClipboardItem
|
|
4482
|
-
// (image/png). Video — Web Clipboard API не поддерживает video MIME,
|
|
4483
|
-
// делаем fallback: пишем file:// URL / название файла в text/plain,
|
|
4484
|
-
// юзер вставит в Finder/проводник (URL) или как текст.
|
|
4485
|
-
if ((node.type === 'image' || node.type === 'video') && node.file) {
|
|
4486
|
-
add('📋 Копировать в буфер (системный)', async () => {
|
|
4487
|
-
try {
|
|
4488
|
-
await copyMediaToSystemClipboard(node);
|
|
4489
|
-
if (typeof showToast === 'function') {
|
|
4490
|
-
showToast('📋 Скопировано в системный буфер', 'ok');
|
|
4491
|
-
}
|
|
4492
|
-
} catch (e) {
|
|
4493
|
-
console.error('clipboard copy failed:', e);
|
|
4494
|
-
alert('Не удалось скопировать: ' + (e?.message || e));
|
|
4495
|
-
}
|
|
4496
|
-
});
|
|
4497
|
-
}
|
|
4498
4383
|
// «Сохранить как…» — для любого медиа-файла (image / audio / video).
|
|
4499
4384
|
// Показывает нативный системный диалог через FS-Access-API; если API
|
|
4500
4385
|
// недоступен (web-режим) — auto-сохранение в Downloads/.
|
package/renderer/generate.js
CHANGED
|
@@ -39,6 +39,12 @@ canvasWrap.addEventListener('mousedown', e => {
|
|
|
39
39
|
}
|
|
40
40
|
if (e.button !== 0) return;
|
|
41
41
|
if (e.target.closest('.node, .conn, .anchor, .resize-handle, button, input, textarea, select, .modal, #addMenu, #nodeMenu')) return;
|
|
42
|
+
// preventDefault на mousedown пустого холста — иначе браузер начинает
|
|
43
|
+
// native text-selection и при rubber-band drag'е через ноду подсвечивает
|
|
44
|
+
// её inline-description / textarea текст-ноды. Юзер: «когда выделяем ноду
|
|
45
|
+
// движением drag&drop — выделяется текст на описаниях и внутри текстовых
|
|
46
|
+
// нод».
|
|
47
|
+
e.preventDefault();
|
|
42
48
|
// Если без модификатора — сбрасываем старое выделение
|
|
43
49
|
const additive = e.metaKey || e.ctrlKey || e.shiftKey;
|
|
44
50
|
if (!additive && state.selectedNodeIds.size) {
|
|
@@ -78,6 +84,13 @@ function startRubberBand(e, additive) {
|
|
|
78
84
|
const overlay = document.createElement('div');
|
|
79
85
|
overlay.style.cssText = 'position:fixed; pointer-events:none; border:1px dashed #6a8aaa; background:rgba(106,138,170,0.1); z-index:1000;';
|
|
80
86
|
document.body.appendChild(overlay);
|
|
87
|
+
// Глобально отключаем text-selection на время drag'а — иначе при движении
|
|
88
|
+
// курсора через ноды браузер подсвечивает description / textarea текст-нод.
|
|
89
|
+
// preventDefault на mousedown уже отбивает старт нативной селекции, но
|
|
90
|
+
// некоторые браузеры всё-равно расширяют выделение при движении —
|
|
91
|
+
// user-select:none на body это финальная страховка. Восстанавливаем в onUp.
|
|
92
|
+
const prevUserSelect = document.body.style.userSelect;
|
|
93
|
+
document.body.style.userSelect = 'none';
|
|
81
94
|
let moved = false;
|
|
82
95
|
const onMove = ev => {
|
|
83
96
|
moved = true;
|
|
@@ -92,6 +105,7 @@ function startRubberBand(e, additive) {
|
|
|
92
105
|
document.removeEventListener('mousemove', onMove);
|
|
93
106
|
document.removeEventListener('mouseup', onUp);
|
|
94
107
|
overlay.remove();
|
|
108
|
+
document.body.style.userSelect = prevUserSelect;
|
|
95
109
|
if (!moved) return;
|
|
96
110
|
const rect = canvas.getBoundingClientRect();
|
|
97
111
|
const endC = { x: (ev.clientX - rect.left) / state.zoom, y: (ev.clientY - rect.top) / state.zoom };
|
|
@@ -1714,6 +1728,7 @@ $('genSubmit').addEventListener('click', async () => {
|
|
|
1714
1728
|
'seedance-2-fast': 'bytedance/seedance-2-fast',
|
|
1715
1729
|
'kling-o1': 'kwaivgi/kling-o1',
|
|
1716
1730
|
'kling-3.0': 'kling-3.0/video',
|
|
1731
|
+
'omni': 'gemini-omni-video',
|
|
1717
1732
|
};
|
|
1718
1733
|
const mk = state.videoModel || 'seedance-2';
|
|
1719
1734
|
patch.modelKey = mk;
|
|
@@ -1963,6 +1978,7 @@ $('genSubmit').addEventListener('click', async () => {
|
|
|
1963
1978
|
'seedance-2-fast': 'bytedance/seedance-2-fast',
|
|
1964
1979
|
'kling-o1': 'kwaivgi/kling-o1',
|
|
1965
1980
|
'kling-3.0': 'kling-3.0/video',
|
|
1981
|
+
'omni': 'gemini-omni-video',
|
|
1966
1982
|
}[state.videoModel || 'seedance-2'] || 'bytedance/seedance-2'),
|
|
1967
1983
|
refs: mediaRefs.map(r => ({ name: r.name, type: r.type, file: r.file })),
|
|
1968
1984
|
// Связь с родительской нодой (выведена через "вытягивание"). Хранится
|
package/renderer/settings.js
CHANGED
|
@@ -1515,18 +1515,24 @@ async function copyNodeToClipboard(node) {
|
|
|
1515
1515
|
}
|
|
1516
1516
|
|
|
1517
1517
|
// Кладёт ноды в системный буфер обмена (где это имеет смысл):
|
|
1518
|
-
// • image — image/png
|
|
1518
|
+
// • первая image-нода (если есть среди выделенных) — image/png через
|
|
1519
|
+
// canvas-конвертацию. Web Clipboard API принимает ОДНУ запись и
|
|
1520
|
+
// receiver'ы (Telegram, Photoshop, Notes…) всё равно читают первый
|
|
1521
|
+
// image — выкладывать массив бессмысленно.
|
|
1519
1522
|
// • text/label — text/plain (если несколько — соединяем через \n\n)
|
|
1520
|
-
// • video/audio
|
|
1521
|
-
//
|
|
1522
|
-
//
|
|
1523
|
-
//
|
|
1523
|
+
// • video/audio в одиночку — имя файла как fallback.
|
|
1524
|
+
// Юзер: «когда выделяешь несколько нод и копируешь — не копируется».
|
|
1525
|
+
// Раньше для multi-selection ветка «один image» пропускалась (items.length
|
|
1526
|
+
// !== 1) и срабатывал text-fallback, который писал ИМЯ файла как текст
|
|
1527
|
+
// вместо самой картинки. Теперь — ищем ПЕРВУЮ image-ноду в selection'е
|
|
1528
|
+
// и копируем именно её.
|
|
1524
1529
|
async function writeNodesToSystemClipboard(items) {
|
|
1525
1530
|
if (!items?.length || !navigator.clipboard) return;
|
|
1526
1531
|
try {
|
|
1527
|
-
//
|
|
1528
|
-
|
|
1529
|
-
|
|
1532
|
+
// Ищем первую image-ноду в выделении — её копируем как image/png.
|
|
1533
|
+
const imgItem = items.find(it => it.blob && it.node?.type === 'image');
|
|
1534
|
+
if (imgItem) {
|
|
1535
|
+
const blob = imgItem.blob;
|
|
1530
1536
|
const pngBlob = blob.type === 'image/png' ? blob : await blobToPng(blob);
|
|
1531
1537
|
if (pngBlob && typeof ClipboardItem === 'function') {
|
|
1532
1538
|
await navigator.clipboard.write([new ClipboardItem({ 'image/png': pngBlob })]);
|
package/renderer/styles.css
CHANGED
|
@@ -144,6 +144,9 @@
|
|
|
144
144
|
background: transparent;
|
|
145
145
|
cursor: pointer;
|
|
146
146
|
transition: color 0.12s, background 0.12s;
|
|
147
|
+
/* Placeholder — это «кнопка-подсказка», не текст. Запрещаем выделение
|
|
148
|
+
чтобы rubber-band drag не подсвечивал его. */
|
|
149
|
+
user-select: none; -webkit-user-select: none;
|
|
147
150
|
}
|
|
148
151
|
.node-description-inline.empty:hover {
|
|
149
152
|
color: #aac8e6;
|