Merge branch 'staging' into claude-caching-at-depth

This commit is contained in:
Cohee
2024-11-18 21:03:33 +02:00
18 changed files with 413 additions and 166 deletions

View File

@@ -0,0 +1,137 @@
{
"3": {
"inputs": {
"seed": "%seed%",
"steps": "%steps%",
"cfg": "%scale%",
"sampler_name": "%sampler%",
"scheduler": "%scheduler%",
"denoise": "%denoise%",
"model": [
"4",
0
],
"positive": [
"6",
0
],
"negative": [
"7",
0
],
"latent_image": [
"12",
0
]
},
"class_type": "KSampler",
"_meta": {
"title": "KSampler"
}
},
"4": {
"inputs": {
"ckpt_name": "%model%"
},
"class_type": "CheckpointLoaderSimple",
"_meta": {
"title": "Load Checkpoint"
}
},
"6": {
"inputs": {
"text": "%prompt%",
"clip": [
"4",
1
]
},
"class_type": "CLIPTextEncode",
"_meta": {
"title": "CLIP Text Encode (Prompt)"
}
},
"7": {
"inputs": {
"text": "%negative_prompt%",
"clip": [
"4",
1
]
},
"class_type": "CLIPTextEncode",
"_meta": {
"title": "CLIP Text Encode (Negative Prompt)"
}
},
"8": {
"inputs": {
"samples": [
"3",
0
],
"vae": [
"4",
2
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE Decode"
}
},
"9": {
"inputs": {
"filename_prefix": "SillyTavern",
"images": [
"8",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "Save Image"
}
},
"10": {
"inputs": {
"image": "%char_avatar%"
},
"class_type": "ETN_LoadImageBase64",
"_meta": {
"title": "Load Image (Base64) [https://github.com/Acly/comfyui-tooling-nodes]"
}
},
"12": {
"inputs": {
"pixels": [
"13",
0
],
"vae": [
"4",
2
]
},
"class_type": "VAEEncode",
"_meta": {
"title": "VAE Encode"
}
},
"13": {
"inputs": {
"upscale_method": "bicubic",
"width": "%width%",
"height": "%height%",
"crop": "center",
"image": [
"10",
0
]
},
"class_type": "ImageScale",
"_meta": {
"title": "Upscale Image"
}
}
}

View File

@@ -135,6 +135,10 @@
"filename": "Default_Comfy_Workflow.json", "filename": "Default_Comfy_Workflow.json",
"type": "workflow" "type": "workflow"
}, },
{
"filename": "Char_Avatar_Comfy_Workflow.json",
"type": "workflow"
},
{ {
"filename": "presets/kobold/Ace of Spades.json", "filename": "presets/kobold/Ace of Spades.json",
"type": "kobold_preset" "type": "kobold_preset"

6
package-lock.json generated
View File

@@ -3019,9 +3019,9 @@
} }
}, },
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.3", "version": "7.0.5",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"path-key": "^3.1.0", "path-key": "^3.1.0",

View File

@@ -72,6 +72,14 @@
opacity: 0.5; opacity: 0.5;
} }
.logprobs_output_prefix:hover {
background-color: rgba(255, 0, 50, 0.4);
}
.logprobs_output_prefix:hover ~ .logprobs_output_prefix {
background-color: rgba(255, 0, 50, 0.4);
}
.logprobs_candidate_list { .logprobs_candidate_list {
grid-row-start: 3; grid-row-start: 3;
grid-row-end: 4; grid-row-end: 4;

View File

@@ -0,0 +1,19 @@
.scrollable-buttons-container {
max-height: 50vh; /* Use viewport height instead of fixed pixels */
overflow-y: auto;
-webkit-overflow-scrolling: touch; /* Momentum scrolling on iOS */
margin-top: 1rem; /* m-t-1 is equivalent to margin-top: 1rem; */
flex-shrink: 1;
min-height: 0;
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.3) transparent;
}
.scrollable-buttons-container::-webkit-scrollbar {
width: 6px;
}
.scrollable-buttons-container::-webkit-scrollbar-thumb {
background-color: rgba(255, 255, 255, 0.3);
border-radius: 3px;
}

View File

@@ -3026,6 +3026,7 @@
<option value="codestral-latest">codestral-latest</option> <option value="codestral-latest">codestral-latest</option>
<option value="codestral-mamba-latest">codestral-mamba-latest</option> <option value="codestral-mamba-latest">codestral-mamba-latest</option>
<option value="pixtral-12b-latest">pixtral-12b-latest</option> <option value="pixtral-12b-latest">pixtral-12b-latest</option>
<option value="pixtral-large-latest">pixtral-large-latest</option>
</optgroup> </optgroup>
<optgroup label="Sub-versions"> <optgroup label="Sub-versions">
<option value="open-mistral-nemo-2407">open-mistral-nemo-2407</option> <option value="open-mistral-nemo-2407">open-mistral-nemo-2407</option>
@@ -3040,10 +3041,12 @@
<option value="mistral-medium-2312">mistral-medium-2312</option> <option value="mistral-medium-2312">mistral-medium-2312</option>
<option value="mistral-large-2402">mistral-large-2402</option> <option value="mistral-large-2402">mistral-large-2402</option>
<option value="mistral-large-2407">mistral-large-2407</option> <option value="mistral-large-2407">mistral-large-2407</option>
<option value="mistral-large-2411">mistral-large-2411</option>
<option value="codestral-2405">codestral-2405</option> <option value="codestral-2405">codestral-2405</option>
<option value="codestral-2405-blue">codestral-2405-blue</option> <option value="codestral-2405-blue">codestral-2405-blue</option>
<option value="codestral-mamba-2407">codestral-mamba-2407</option> <option value="codestral-mamba-2407">codestral-mamba-2407</option>
<option value="pixtral-12b-2409">pixtral-12b-2409</option> <option value="pixtral-12b-2409">pixtral-12b-2409</option>
<option value="pixtral-large-2411">pixtral-large-2411</option>
</optgroup> </optgroup>
<optgroup id="mistralai_other_models" label="Other"></optgroup> <optgroup id="mistralai_other_models" label="Other"></optgroup>
</select> </select>
@@ -4503,7 +4506,7 @@
</div> </div>
</div> </div>
<div name="AutoCompleteToggle" class="inline-drawer wide100p flexFlowColumn"> <div name="AutoCompleteToggle" class="inline-drawer wide100p flexFlowColumn">
<div class="inline-drawer-toggle inline-drawer-header userSettingsInnerExpandable" title="Options for the various autocompelte input boxes."> <div class="inline-drawer-toggle inline-drawer-header userSettingsInnerExpandable" title="Options for the various autocomplete input boxes.">
<b><span data-i18n="AutoComplete Settings">AutoComplete Settings</span></b> <b><span data-i18n="AutoComplete Settings">AutoComplete Settings</span></b>
<div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div> <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
</div> </div>
@@ -6631,8 +6634,11 @@
</div> </div>
</div> </div>
<div class="logprobs_panel_content inline-drawer-content flex-container flexFlowColumn"> <div class="logprobs_panel_content inline-drawer-content flex-container flexFlowColumn">
<small> <small class="flex-container alignItemsCenter justifySpaceBetween flexNoWrap">
<b data-i18n="Select a token to see alternatives considered by the AI.">Select a token to see alternatives considered by the AI.</b> <b data-i18n="Select a token to see alternatives considered by the AI.">Select a token to see alternatives considered by the AI.</b>
<button id="logprobsReroll" class="menu_button margin0" title="Reroll with the entire prefix" data-i18n="[title]Reroll with the entire prefix">
<span class="fa-solid fa-redo logprobs_reroll"></span>
</button>
</small> </small>
<hr> <hr>
<div id="logprobs_generation_output"></div> <div id="logprobs_generation_output"></div>

View File

@@ -703,16 +703,13 @@ const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
*/ */
function autoFitSendTextArea() { function autoFitSendTextArea() {
const originalScrollBottom = chatBlock.scrollHeight - (chatBlock.scrollTop + chatBlock.offsetHeight); const originalScrollBottom = chatBlock.scrollHeight - (chatBlock.scrollTop + chatBlock.offsetHeight);
if (Math.ceil(sendTextArea.scrollHeight + 3) >= Math.floor(sendTextArea.offsetHeight)) {
const sendTextAreaMinHeight = '0px'; sendTextArea.style.height = '1px'; // Reset height to 1px to force recalculation of scrollHeight
sendTextArea.style.height = sendTextAreaMinHeight; const newHeight = sendTextArea.scrollHeight;
}
const newHeight = sendTextArea.scrollHeight + 3;
sendTextArea.style.height = `${newHeight}px`; sendTextArea.style.height = `${newHeight}px`;
if (!isFirefox) { if (!isFirefox) {
const newScrollTop = Math.round(chatBlock.scrollHeight - (chatBlock.offsetHeight + originalScrollBottom)); chatBlock.scrollTop = chatBlock.scrollHeight - (chatBlock.offsetHeight + originalScrollBottom);
chatBlock.scrollTop = newScrollTop;
} }
} }
export const autoFitSendTextAreaDebounced = debounce(autoFitSendTextArea, debounce_timeout.short); export const autoFitSendTextAreaDebounced = debounce(autoFitSendTextArea, debounce_timeout.short);

View File

@@ -37,6 +37,8 @@
<select id="caption_multimodal_model" class="flex1 text_pole"> <select id="caption_multimodal_model" class="flex1 text_pole">
<option data-type="mistral" value="pixtral-12b-latest">pixtral-12b-latest</option> <option data-type="mistral" value="pixtral-12b-latest">pixtral-12b-latest</option>
<option data-type="mistral" value="pixtral-12b-2409">pixtral-12b-2409</option> <option data-type="mistral" value="pixtral-12b-2409">pixtral-12b-2409</option>
<option data-type="mistral" value="pixtral-large-latest">pixtral-large-latest</option>
<option data-type="mistral" value="pixtral-large-2411">pixtral-large-2411</option>
<option data-type="zerooneai" value="yi-vision">yi-vision</option> <option data-type="zerooneai" value="yi-vision">yi-vision</option>
<option data-type="openai" value="gpt-4-vision-preview">gpt-4-vision-preview</option> <option data-type="openai" value="gpt-4-vision-preview">gpt-4-vision-preview</option>
<option data-type="openai" value="gpt-4-turbo">gpt-4-turbo</option> <option data-type="openai" value="gpt-4-turbo">gpt-4-turbo</option>

View File

@@ -17,6 +17,7 @@
<li data-placeholder="scheduler" class="sd_comfy_workflow_editor_not_found">"%scheduler%"</li> <li data-placeholder="scheduler" class="sd_comfy_workflow_editor_not_found">"%scheduler%"</li>
<li data-placeholder="steps" class="sd_comfy_workflow_editor_not_found">"%steps%"</li> <li data-placeholder="steps" class="sd_comfy_workflow_editor_not_found">"%steps%"</li>
<li data-placeholder="scale" class="sd_comfy_workflow_editor_not_found">"%scale%"</li> <li data-placeholder="scale" class="sd_comfy_workflow_editor_not_found">"%scale%"</li>
<li data-placeholder="denoise" class="sd_comfy_workflow_editor_not_found">"%denoise%"</li>
<li data-placeholder="clip_skip" class="sd_comfy_workflow_editor_not_found">"%clip_skip%"</li> <li data-placeholder="clip_skip" class="sd_comfy_workflow_editor_not_found">"%clip_skip%"</li>
<li data-placeholder="width" class="sd_comfy_workflow_editor_not_found">"%width%"</li> <li data-placeholder="width" class="sd_comfy_workflow_editor_not_found">"%width%"</li>
<li data-placeholder="height" class="sd_comfy_workflow_editor_not_found">"%height%"</li> <li data-placeholder="height" class="sd_comfy_workflow_editor_not_found">"%height%"</li>

View File

@@ -3269,6 +3269,10 @@ async function generateComfyImage(prompt, negativePrompt, signal) {
const seed = extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : Math.round(Math.random() * Number.MAX_SAFE_INTEGER); const seed = extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : Math.round(Math.random() * Number.MAX_SAFE_INTEGER);
workflow = workflow.replaceAll('"%seed%"', JSON.stringify(seed)); workflow = workflow.replaceAll('"%seed%"', JSON.stringify(seed));
const denoising_strength = extension_settings.sd.denoising_strength === undefined ? 1.0 : extension_settings.sd.denoising_strength;
workflow = workflow.replaceAll('"%denoise%"', JSON.stringify(denoising_strength));
placeholders.forEach(ph => { placeholders.forEach(ph => {
workflow = workflow.replaceAll(`"%${ph}%"`, JSON.stringify(extension_settings.sd[ph])); workflow = workflow.replaceAll(`"%${ph}%"`, JSON.stringify(extension_settings.sd[ph]));
}); });
@@ -3279,7 +3283,8 @@ async function generateComfyImage(prompt, negativePrompt, signal) {
const response = await fetch(getUserAvatarUrl()); const response = await fetch(getUserAvatarUrl());
if (response.ok) { if (response.ok) {
const avatarBlob = await response.blob(); const avatarBlob = await response.blob();
const avatarBase64 = await getBase64Async(avatarBlob); const avatarBase64DataUrl = await getBase64Async(avatarBlob);
const avatarBase64 = avatarBase64DataUrl.split(',')[1];
workflow = workflow.replaceAll('"%user_avatar%"', JSON.stringify(avatarBase64)); workflow = workflow.replaceAll('"%user_avatar%"', JSON.stringify(avatarBase64));
} else { } else {
workflow = workflow.replaceAll('"%user_avatar%"', JSON.stringify(PNG_PIXEL)); workflow = workflow.replaceAll('"%user_avatar%"', JSON.stringify(PNG_PIXEL));
@@ -3289,7 +3294,8 @@ async function generateComfyImage(prompt, negativePrompt, signal) {
const response = await fetch(getCharacterAvatarUrl()); const response = await fetch(getCharacterAvatarUrl());
if (response.ok) { if (response.ok) {
const avatarBlob = await response.blob(); const avatarBlob = await response.blob();
const avatarBase64 = await getBase64Async(avatarBlob); const avatarBase64DataUrl = await getBase64Async(avatarBlob);
const avatarBase64 = avatarBase64DataUrl.split(',')[1];
workflow = workflow.replaceAll('"%char_avatar%"', JSON.stringify(avatarBase64)); workflow = workflow.replaceAll('"%char_avatar%"', JSON.stringify(avatarBase64));
} else { } else {
workflow = workflow.replaceAll('"%char_avatar%"', JSON.stringify(PNG_PIXEL)); workflow = workflow.replaceAll('"%char_avatar%"', JSON.stringify(PNG_PIXEL));

View File

@@ -319,7 +319,7 @@
<input class="neo-range-input" type="number" id="sd_hr_scale_value" data-for="sd_hr_scale" min="{{hr_scale_min}}" max="{{hr_scale_max}}" step="{{hr_scale_step}}" value="{{hr_scale}}" > <input class="neo-range-input" type="number" id="sd_hr_scale_value" data-for="sd_hr_scale" min="{{hr_scale_min}}" max="{{hr_scale_max}}" step="{{hr_scale_step}}" value="{{hr_scale}}" >
</div> </div>
<div class="alignitemscenter flex-container flexFlowColumn flexGrow flexShrink gap0 flexBasis48p" data-sd-source="auto,vlad"> <div class="alignitemscenter flex-container flexFlowColumn flexGrow flexShrink gap0 flexBasis48p" data-sd-source="auto,vlad,comfy">
<small> <small>
<span data-i18n="Denoising strength">Denoising strength</span> <span data-i18n="Denoising strength">Denoising strength</span>
</small> </small>

View File

@@ -23,7 +23,7 @@ import {
import { collapseNewlines, registerDebugFunction } from '../../power-user.js'; import { collapseNewlines, registerDebugFunction } from '../../power-user.js';
import { SECRET_KEYS, secret_state, writeSecret } from '../../secrets.js'; import { SECRET_KEYS, secret_state, writeSecret } from '../../secrets.js';
import { getDataBankAttachments, getDataBankAttachmentsForSource, getFileAttachment } from '../../chats.js'; import { getDataBankAttachments, getDataBankAttachmentsForSource, getFileAttachment } from '../../chats.js';
import { debounce, getStringHash as calculateHash, waitUntilCondition, onlyUnique, splitRecursive, trimToStartSentence, trimToEndSentence } from '../../utils.js'; import { debounce, getStringHash as calculateHash, waitUntilCondition, onlyUnique, splitRecursive, trimToStartSentence, trimToEndSentence, escapeHtml } from '../../utils.js';
import { debounce_timeout } from '../../constants.js'; import { debounce_timeout } from '../../constants.js';
import { getSortedEntries } from '../../world-info.js'; import { getSortedEntries } from '../../world-info.js';
import { textgen_types, textgenerationwebui_settings } from '../../textgen-settings.js'; import { textgen_types, textgenerationwebui_settings } from '../../textgen-settings.js';
@@ -44,6 +44,9 @@ const MODULE_NAME = 'vectors';
export const EXTENSION_PROMPT_TAG = '3_vectors'; export const EXTENSION_PROMPT_TAG = '3_vectors';
export const EXTENSION_PROMPT_TAG_DB = '4_vectors_data_bank'; export const EXTENSION_PROMPT_TAG_DB = '4_vectors_data_bank';
// Force solo chunks for sources that don't support batching.
const getBatchSize = () => ['transformers', 'palm', 'ollama'].includes(settings.source) ? 1 : 5;
const settings = { const settings = {
// For both // For both
source: 'transformers', source: 'transformers',
@@ -125,7 +128,7 @@ async function onVectorizeAllClick() {
// upon request of a full vectorise // upon request of a full vectorise
cachedSummaries.clear(); cachedSummaries.clear();
const batchSize = 5; const batchSize = getBatchSize();
const elapsedLog = []; const elapsedLog = [];
let finished = false; let finished = false;
$('#vectorize_progress').show(); $('#vectorize_progress').show();
@@ -560,7 +563,9 @@ async function vectorizeFile(fileText, fileName, collectionId, chunkSize, overla
fileText = translatedText; fileText = translatedText;
} }
const toast = toastr.info('Vectorization may take some time, please wait...', `Ingesting file ${fileName}`); const batchSize = getBatchSize();
const toastBody = $('<span>').text('This may take a while. Please wait...');
const toast = toastr.info(toastBody, `Ingesting file ${escapeHtml(fileName)}`, { closeButton: false, escapeHtml: false, timeOut: 0, extendedTimeOut: 0 });
const overlapSize = Math.round(chunkSize * overlapPercent / 100); const overlapSize = Math.round(chunkSize * overlapPercent / 100);
const delimiters = getChunkDelimiters(); const delimiters = getChunkDelimiters();
// Overlap should not be included in chunk size. It will be later compensated by overlapChunks // Overlap should not be included in chunk size. It will be later compensated by overlapChunks
@@ -569,7 +574,12 @@ async function vectorizeFile(fileText, fileName, collectionId, chunkSize, overla
console.debug(`Vectors: Split file ${fileName} into ${chunks.length} chunks with ${overlapPercent}% overlap`, chunks); console.debug(`Vectors: Split file ${fileName} into ${chunks.length} chunks with ${overlapPercent}% overlap`, chunks);
const items = chunks.map((chunk, index) => ({ hash: getStringHash(chunk), text: chunk, index: index })); const items = chunks.map((chunk, index) => ({ hash: getStringHash(chunk), text: chunk, index: index }));
await insertVectorItems(collectionId, items);
for (let i = 0; i < items.length; i += batchSize) {
toastBody.text(`${i}/${items.length} (${Math.round((i / items.length) * 100)}%) chunks processed`);
const chunkedBatch = items.slice(i, i + batchSize);
await insertVectorItems(collectionId, chunkedBatch);
}
toastr.clear(toast); toastr.clear(toast);
console.log(`Vectors: Inserted ${chunks.length} vector items for file ${fileName} into ${collectionId}`); console.log(`Vectors: Inserted ${chunks.length} vector items for file ${fileName} into ${collectionId}`);
@@ -1050,7 +1060,7 @@ async function onViewStatsClick() {
toastr.info(`Total hashes: <b>${totalHashes}</b><br> toastr.info(`Total hashes: <b>${totalHashes}</b><br>
Unique hashes: <b>${uniqueHashes}</b><br><br> Unique hashes: <b>${uniqueHashes}</b><br><br>
I'll mark collected messages with a green circle.`, I'll mark collected messages with a green circle.`,
`Stats for chat ${chatId}`, `Stats for chat ${escapeHtml(chatId)}`,
{ timeOut: 10000, escapeHtml: false }, { timeOut: 10000, escapeHtml: false },
); );

View File

@@ -1,6 +1,5 @@
import { import {
animation_duration, animation_duration,
callPopup,
chat, chat,
cleanUpMessage, cleanUpMessage,
event_types, event_types,
@@ -13,15 +12,22 @@ import {
import { debounce, delay, getStringHash } from './utils.js'; import { debounce, delay, getStringHash } from './utils.js';
import { decodeTextTokens, getTokenizerBestMatch } from './tokenizers.js'; import { decodeTextTokens, getTokenizerBestMatch } from './tokenizers.js';
import { power_user } from './power-user.js'; import { power_user } from './power-user.js';
import { callGenericPopup, POPUP_TYPE } from './popup.js';
import { t } from './i18n.js';
const TINTS = 4; const TINTS = 4;
const MAX_MESSAGE_LOGPROBS = 100; const MAX_MESSAGE_LOGPROBS = 100;
const REROLL_BUTTON = $('#logprobsReroll');
/** /**
* Tuple of a candidate token and its logarithm of probability of being chosen * Tuple of a candidate token and its logarithm of probability of being chosen
* @typedef {[string, number]} Candidate - (token, logprob) * @typedef {[string, number]} Candidate - (token, logprob)
*/ */
/**
* @typedef {(Node|JQuery<Text>|JQuery<HTMLElement>)[]} NodeArray - Array of DOM nodes
*/
/** /**
* Logprob data for a single message * Logprob data for a single message
* @typedef {Object} MessageLogprobData * @typedef {Object} MessageLogprobData
@@ -43,17 +49,26 @@ const MAX_MESSAGE_LOGPROBS = 100;
* @property {Candidate[]} topLogprobs - Array of top candidate tokens * @property {Candidate[]} topLogprobs - Array of top candidate tokens
*/ */
let state = { /**
/** @type {TokenLogprobs | null} */ * State object for Token Probabilities
* @typedef {Object} LogprobsState
* @property {?TokenLogprobs} selectedTokenLogprobs Log probabilities for
* currently-selected token.
* @property {Map<number, MessageLogprobData>} messageLogprobs Log probabilities for
* each message, keyed by message hash.
*/
/**
* @type {LogprobsState} state
*/
const state = {
selectedTokenLogprobs: null, selectedTokenLogprobs: null,
/** @type {Map<number, MessageLogprobData>} */
messageLogprobs: new Map(), messageLogprobs: new Map(),
}; };
/** /**
* renderAlternativeTokensView renders the Token Probabilities UI and all * Renders the Token Probabilities UI and all subviews with the active message's
* subviews with the active message's logprobs data. If the message has no token * logprobs data. If the message has no token logprobs, a message is displayed.
* logprobs, a zero-state is rendered.
*/ */
function renderAlternativeTokensView() { function renderAlternativeTokensView() {
const view = $('#logprobs_generation_output'); const view = $('#logprobs_generation_output');
@@ -68,13 +83,14 @@ function renderAlternativeTokensView() {
const usingSmoothStreaming = isStreamingEnabled() && power_user.smooth_streaming; const usingSmoothStreaming = isStreamingEnabled() && power_user.smooth_streaming;
if (!messageLogprobs?.length || usingSmoothStreaming) { if (!messageLogprobs?.length || usingSmoothStreaming) {
const emptyState = $('<div></div>'); const emptyState = $('<div></div>');
const noTokensMsg = usingSmoothStreaming const noTokensMsg = !power_user.request_token_probabilities
? 'Token probabilities are not available when using Smooth Streaming.' ? '<span>Enable <b>Request token probabilities</b> in the User Settings menu to use this feature.</span>'
: 'No token probabilities available for the current message.'; : usingSmoothStreaming
const msg = power_user.request_token_probabilities ? t`Token probabilities are not available when using Smooth Streaming.`
? noTokensMsg : is_send_press
: '<span>Enable <b>Request token probabilities</b> in the User Settings menu to use this feature.</span>'; ? t`Generation in progress...`
emptyState.html(msg); : t`No token probabilities available for the current message.`;
emptyState.html(noTokensMsg);
emptyState.addClass('logprobs_empty_state'); emptyState.addClass('logprobs_empty_state');
view.append(emptyState); view.append(emptyState);
return; return;
@@ -82,16 +98,34 @@ function renderAlternativeTokensView() {
const prefix = continueFrom || ''; const prefix = continueFrom || '';
const tokenSpans = []; const tokenSpans = [];
REROLL_BUTTON.toggle(!!prefix);
if (prefix) { if (prefix) {
const prefixSpan = $('<span></span>'); REROLL_BUTTON.off('click').on('click', () => onPrefixClicked(prefix.length));
prefixSpan.text(prefix);
prefixSpan.html(prefixSpan.html().replace(/\n/g, '<br>')); let cumulativeOffset = 0;
prefixSpan.addClass('logprobs_output_prefix'); const words = prefix.split(/\s+/);
prefixSpan.attr('title', 'Select to reroll the last \'Continue\' generation.\nHold the CTRL key when clicking to reroll from before that word.'); const delimiters = prefix.match(/\s+/g) || []; // Capture the actual delimiters
prefixSpan.click(onPrefixClicked);
addKeyboardProps(prefixSpan); words.forEach((word, i) => {
tokenSpans.push(...withVirtualWhitespace(prefix, prefixSpan)); const span = $('<span></span>');
span.text(`${word} `);
span.addClass('logprobs_output_prefix');
span.attr('title', t`Reroll from this point`);
let offset = cumulativeOffset;
span.on('click', () => onPrefixClicked(offset));
addKeyboardProps(span);
tokenSpans.push(span);
tokenSpans.push(delimiters[i]?.includes('\n')
? document.createElement('br')
: document.createTextNode(delimiters[i] || ' '),
);
cumulativeOffset += word.length + (delimiters[i]?.length || 0);
});
} }
messageLogprobs.forEach((tokenData, i) => { messageLogprobs.forEach((tokenData, i) => {
@@ -101,7 +135,7 @@ function renderAlternativeTokensView() {
span.text(text); span.text(text);
span.addClass('logprobs_output_token'); span.addClass('logprobs_output_token');
span.addClass('logprobs_tint_' + (i % TINTS)); span.addClass('logprobs_tint_' + (i % TINTS));
span.click(() => onSelectedTokenChanged(tokenData, span)); span.on('click', () => onSelectedTokenChanged(tokenData, span));
addKeyboardProps(span); addKeyboardProps(span);
tokenSpans.push(...withVirtualWhitespace(token, span)); tokenSpans.push(...withVirtualWhitespace(token, span));
}); });
@@ -129,6 +163,10 @@ function addKeyboardProps(element) {
/** /**
* renderTopLogprobs renders the top logprobs subview with the currently * renderTopLogprobs renders the top logprobs subview with the currently
* selected token highlighted. If no token is selected, the subview is hidden. * selected token highlighted. If no token is selected, the subview is hidden.
*
* Callers:
* - renderAlternativeTokensView, to render the entire view
* - onSelectedTokenChanged, to update the view when a token is selected
*/ */
function renderTopLogprobs() { function renderTopLogprobs() {
$('#logprobs_top_logprobs_hint').hide(); $('#logprobs_top_logprobs_hint').hide();
@@ -150,8 +188,7 @@ function renderTopLogprobs() {
const probability = Math.exp(log); const probability = Math.exp(log);
sum += probability; sum += probability;
return [text, probability, log]; return [text, probability, log];
} } else {
else {
return [text, log, null]; return [text, log, null];
} }
}); });
@@ -167,15 +204,15 @@ function renderTopLogprobs() {
container.addClass('selected'); container.addClass('selected');
} }
const tokenText = $('<span></span>').text(`${toVisibleWhitespace(token)}`); const tokenText = $('<span></span>').text(`${toVisibleWhitespace(token.toString())}`);
const percentText = $('<span></span>').text(`${(probability * 100).toFixed(2)}%`); const percentText = $('<span></span>').text(`${(+probability * 100).toFixed(2)}%`);
container.append(tokenText, percentText); container.append(tokenText, percentText);
if (log) { if (log) {
container.attr('title', `logarithm: ${log}`); container.attr('title', `logarithm: ${log}`);
} }
addKeyboardProps(container); addKeyboardProps(container);
if (token !== '<others>') { if (token !== '<others>') {
container.click(() => onAlternativeClicked(state.selectedTokenLogprobs, token)); container.on('click', () => onAlternativeClicked(state.selectedTokenLogprobs, token.toString()));
} else { } else {
container.prop('disabled', true); container.prop('disabled', true);
} }
@@ -192,11 +229,10 @@ function renderTopLogprobs() {
} }
/** /**
* onSelectedTokenChanged is called when the user clicks on a token in the * User clicks on a token in the token output view. It updates the selected token state
* token output view. It updates the selected token state and re-renders the * and re-renders the top logprobs view, or deselects the token if it was already selected.
* top logprobs view, or deselects the token if it was already selected.
* @param {TokenLogprobs} logprobs - logprob data for the selected token * @param {TokenLogprobs} logprobs - logprob data for the selected token
* @param {Element} span - target span node that was clicked * @param {Node|JQuery} span - target span node that was clicked
*/ */
function onSelectedTokenChanged(logprobs, span) { function onSelectedTokenChanged(logprobs, span) {
$('.logprobs_output_token.selected').removeClass('selected'); $('.logprobs_output_token.selected').removeClass('selected');
@@ -223,7 +259,10 @@ function onAlternativeClicked(tokenLogprobs, alternative) {
} }
if (getGeneratingApi() === 'openai') { if (getGeneratingApi() === 'openai') {
return callPopup('<h3>Feature unavailable</h3><p>Due to API limitations, rerolling a token is not supported with OpenAI. Try switching to a different API.</p>', 'text'); const title = t`Feature unavailable`;
const message = t`Due to API limitations, rerolling a token is not supported with OpenAI. Try switching to a different API.`;
const content = `<h3>${title}</h3><p>${message}</p>`;
return callGenericPopup(content, POPUP_TYPE.TEXT);
} }
const { messageLogprobs, continueFrom } = getActiveMessageLogprobData(); const { messageLogprobs, continueFrom } = getActiveMessageLogprobData();
@@ -234,79 +273,29 @@ function onAlternativeClicked(tokenLogprobs, alternative) {
const prefix = continueFrom || ''; const prefix = continueFrom || '';
const prompt = prefix + tokens.join(''); const prompt = prefix + tokens.join('');
const messageId = chat.length - 1; addGeneration(prompt);
createSwipe(messageId, prompt);
$('.swipe_right:last').click(); // :see_no_evil:
Generate('continue').then(_ => void _);
} }
/** /**
* getTextBeforeClickedWord retrieves the portion of text within a span * User clicks on the reroll button in the token output view, or on a word in the
* that appears before the word clicked by the user. Using the x and y * prefix. Retrieve the prefix for the current message and truncate it at the
* coordinates from a PointerEvent, this function identifies the exact * offset for the selected word. Then request a `continue` completion from the
* word clicked and returns the text preceding it within the span. * model with the new prompt.
* *
* If the clicked position does not resolve to a valid word or text node, * If no offset is provided, the entire prefix will be rerolled.
* the entire span text is returned as a fallback.
* *
* @param {PointerEvent} event - The click event containing the x and y coordinates. * @param {number} offset - index of the token in the prefix to reroll from
* @param {string} spanText - The full text content of the span element. * @returns {void}
* @returns {string} The text before the clicked word, or the entire span text as fallback. * @param offset
*/ */
function getTextBeforeClickedWord(event, spanText) { function onPrefixClicked(offset = undefined) {
const x = event.clientX;
const y = event.clientY;
const range = document.caretRangeFromPoint(x, y);
if (range && range.startContainer.nodeType === Node.TEXT_NODE) {
const textNode = range.startContainer;
const offset = range.startOffset;
// Get the full text content of the text node
const text = textNode.nodeValue;
// Find the boundaries of the clicked word
const start = text.lastIndexOf(' ', offset - 1) + 1;
// Return the text before the clicked word
return text.slice(0, start);
}
// If we can't determine the exact word, return the full span text as a fallback
return spanText;
}
/**
* onPrefixClicked is called when the user clicks on the carried-over prefix
* in the token output view. It allows them to reroll the last 'continue'
* completion with none of the output generated from it, in case they don't
* like the results.
*
* If the user holds the Ctrl key while clicking, only the portion of text
* before the clicked word is retained as the prefix for rerolling
*/
function onPrefixClicked() {
if (!checkGenerateReady()) { if (!checkGenerateReady()) {
return; return;
} }
const { continueFrom } = getActiveMessageLogprobData(); const { continueFrom } = getActiveMessageLogprobData() || {};
const messageId = chat.length - 1; const prefix = continueFrom ? continueFrom.substring(0, offset) : '';
addGeneration(prefix);
// Check if Ctrl key is pressed during the click
let prefix = continueFrom || '';
if (event.ctrlKey) {
// Ctrl is pressed - use the text before the clicked word
prefix = getTextBeforeClickedWord(event, continueFrom);
}
// Use the determined `prefix`
createSwipe(messageId, prefix);
$('.swipe_right:last').click();
Generate('continue').then(_ => void _);
} }
function checkGenerateReady() { function checkGenerateReady() {
@@ -317,6 +306,22 @@ function checkGenerateReady() {
return true; return true;
} }
/**
* Generates a new swipe as a continuation of the given prompt, when user selects
* an alternative token or rerolls from a prefix.
*
* @param prompt
*/
function addGeneration(prompt) {
const messageId = chat.length - 1;
if (prompt && prompt.length > 0) {
createSwipe(messageId, prompt);
$('.swipe_right:last').trigger('click');
void Generate('continue');
} else {
$('.swipe_right:last').trigger('click');
}
}
/** /**
* onToggleLogprobsPanel is called when the user performs an action that toggles * onToggleLogprobsPanel is called when the user performs an action that toggles
@@ -356,15 +361,14 @@ function onToggleLogprobsPanel() {
} }
/** /**
* createSwipe appends a new swipe to the target chat message with the given * Appends a new swipe to the target chat message with the given text.
* text.
* @param {number} messageId - target chat message ID * @param {number} messageId - target chat message ID
* @param {string} prompt - initial prompt text which will be continued * @param {string} prompt - initial prompt text which will be continued
*/ */
function createSwipe(messageId, prompt) { function createSwipe(messageId, prompt) {
// need to call `cleanUpMessage` on our new prompt, because we were working // need to call `cleanUpMessage` on our new prompt, because we were working
// with raw model output and our new prompt is missing trimming/macro replacements // with raw model output and our new prompt is missing trimming/macro replacements
const cleanedPrompt = cleanUpMessage(prompt, false, false); const cleanedPrompt = cleanUpMessage(prompt, false, false, true);
const msg = chat[messageId]; const msg = chat[messageId];
const newSwipeInfo = { const newSwipeInfo = {
@@ -399,10 +403,11 @@ function toVisibleWhitespace(input) {
* after the span node if its token begins or ends with whitespace in order to * after the span node if its token begins or ends with whitespace in order to
* allow text to wrap despite whitespace characters being replaced with a dot. * allow text to wrap despite whitespace characters being replaced with a dot.
* @param {string} text - token text being evaluated for whitespace * @param {string} text - token text being evaluated for whitespace
* @param {Element} span - target span node to be wrapped * @param {Node|JQuery} span - target span node to be wrapped
* @returns {Element[]} array of nodes to be appended to the DOM * @returns {NodeArray} - array of nodes to be appended to the parent element
*/ */
function withVirtualWhitespace(text, span) { function withVirtualWhitespace(text, span) {
/** @type {NodeArray} */
const result = [span]; const result = [span];
if (text.match(/^\s/)) { if (text.match(/^\s/)) {
result.unshift(document.createTextNode('\u200b')); result.unshift(document.createTextNode('\u200b'));
@@ -430,12 +435,16 @@ function withVirtualWhitespace(text, span) {
} }
/** /**
* saveLogprobsForActiveMessage receives an array of TokenLogprobs objects * Receives the top logprobs for each token in a message and associates it with the active message.
* representing the top logprobs for each token in a message and associates it *
* with the active message. * Ensure the active message has been updated and rendered before calling this function
* or the logprobs data will be saved to the wrong message.
*
* Callers:
* - Generate:onSuccess via saveLogprobsForActiveMessage, for non-streaming text completion
* - StreamingProcessor:onFinishStreaming, for streaming text completion
* - sendOpenAIRequest, for non-streaming chat completion
* *
* **Ensure the active message has been updated and rendered before calling
* this function or the logprobs data will be saved to the wrong message.**
* @param {TokenLogprobs[]} logprobs - array of logprobs data for each token * @param {TokenLogprobs[]} logprobs - array of logprobs data for each token
* @param {string | null} continueFrom - for 'continue' generations, the prompt * @param {string | null} continueFrom - for 'continue' generations, the prompt
*/ */
@@ -445,7 +454,10 @@ export function saveLogprobsForActiveMessage(logprobs, continueFrom) {
return; return;
} }
convertTokenIdLogprobsToText(logprobs); // NovelAI only returns token IDs in logprobs data; convert to text tokens in-place
if (getGeneratingApi() === 'novel') {
convertTokenIdLogprobsToText(logprobs);
}
const msgId = chat.length - 1; const msgId = chat.length - 1;
/** @type {MessageLogprobData} */ /** @type {MessageLogprobData} */
@@ -491,17 +503,18 @@ function getActiveMessageLogprobData() {
return state.messageLogprobs.get(hash) || null; return state.messageLogprobs.get(hash) || null;
} }
/** /**
* convertLogprobTokenIdsToText mutates the given logprobs data's topLogprobs * convertLogprobTokenIdsToText replaces token IDs in logprobs data with text tokens,
* field keyed by token text instead of token ID. This is only necessary for * for APIs that return token IDs instead of text tokens, to wit: NovelAI.
* APIs which only return token IDs in their logprobs data; for others this *
* function is a no-op.
* @param {TokenLogprobs[]} input - logprobs data with numeric token IDs * @param {TokenLogprobs[]} input - logprobs data with numeric token IDs
*/ */
function convertTokenIdLogprobsToText(input) { function convertTokenIdLogprobsToText(input) {
const api = getGeneratingApi(); const api = getGeneratingApi();
if (api !== 'novel') { if (api !== 'novel') {
return input; // should have been checked by the caller
throw new Error('convertTokenIdLogprobsToText should only be called for NovelAI');
} }
const tokenizerId = getTokenizerBestMatch(api); const tokenizerId = getTokenizerBestMatch(api);
@@ -512,7 +525,8 @@ function convertTokenIdLogprobsToText(input) {
))); )));
// Submit token IDs to tokenizer to get token text, then build ID->text map // Submit token IDs to tokenizer to get token text, then build ID->text map
const { chunks } = decodeTextTokens(tokenizerId, tokenIds); // noinspection JSCheckFunctionSignatures - mutates input in-place
const { chunks } = decodeTextTokens(tokenizerId, tokenIds.map(parseInt));
const tokenIdText = new Map(tokenIds.map((id, i) => [id, chunks[i]])); const tokenIdText = new Map(tokenIds.map((id, i) => [id, chunks[i]]));
// Fixup logprobs data with token text // Fixup logprobs data with token text
@@ -525,9 +539,10 @@ function convertTokenIdLogprobsToText(input) {
} }
export function initLogprobs() { export function initLogprobs() {
REROLL_BUTTON.hide();
const debouncedRender = debounce(renderAlternativeTokensView); const debouncedRender = debounce(renderAlternativeTokensView);
$('#logprobsViewerClose').click(onToggleLogprobsPanel); $('#logprobsViewerClose').on('click', onToggleLogprobsPanel);
$('#option_toggle_logprobs').click(onToggleLogprobsPanel); $('#option_toggle_logprobs').on('click', onToggleLogprobsPanel);
eventSource.on(event_types.CHAT_CHANGED, debouncedRender); eventSource.on(event_types.CHAT_CHANGED, debouncedRender);
eventSource.on(event_types.CHARACTER_MESSAGE_RENDERED, debouncedRender); eventSource.on(event_types.CHARACTER_MESSAGE_RENDERED, debouncedRender);
eventSource.on(event_types.IMPERSONATE_READY, debouncedRender); eventSource.on(event_types.IMPERSONATE_READY, debouncedRender);

View File

@@ -4165,7 +4165,7 @@ async function onModelChange() {
$('#openai_max_context').attr('max', unlocked_max); $('#openai_max_context').attr('max', unlocked_max);
} else if (oai_settings.mistralai_model.includes('codestral-mamba')) { } else if (oai_settings.mistralai_model.includes('codestral-mamba')) {
$('#openai_max_context').attr('max', max_256k); $('#openai_max_context').attr('max', max_256k);
} else if (['mistral-large-2407', 'mistral-large-latest'].includes(oai_settings.mistralai_model)) { } else if (['mistral-large-2407', 'mistral-large-2411', 'mistral-large-latest'].includes(oai_settings.mistralai_model)) {
$('#openai_max_context').attr('max', max_128k); $('#openai_max_context').attr('max', max_128k);
} else if (oai_settings.mistralai_model.includes('mistral-nemo')) { } else if (oai_settings.mistralai_model.includes('mistral-nemo')) {
$('#openai_max_context').attr('max', max_128k); $('#openai_max_context').attr('max', max_128k);
@@ -4764,6 +4764,8 @@ export function isImageInliningSupported() {
'pixtral-12b-latest', 'pixtral-12b-latest',
'pixtral-12b', 'pixtral-12b',
'pixtral-12b-2409', 'pixtral-12b-2409',
'pixtral-large-latest',
'pixtral-large-2411',
]; ];
switch (oai_settings.chat_completion_source) { switch (oai_settings.chat_completion_source) {

View File

@@ -2083,7 +2083,10 @@ async function buttonsCallback(args, text) {
let popup; let popup;
const buttonContainer = document.createElement('div'); const buttonContainer = document.createElement('div');
buttonContainer.classList.add('flex-container', 'flexFlowColumn', 'wide100p', 'm-t-1'); buttonContainer.classList.add('flex-container', 'flexFlowColumn', 'wide100p');
const scrollableContainer = document.createElement('div');
scrollableContainer.classList.add('scrollable-buttons-container');
for (const [result, button] of resultToButtonMap) { for (const [result, button] of resultToButtonMap) {
const buttonElement = document.createElement('div'); const buttonElement = document.createElement('div');
@@ -2096,9 +2099,16 @@ async function buttonsCallback(args, text) {
buttonContainer.appendChild(buttonElement); buttonContainer.appendChild(buttonElement);
} }
scrollableContainer.appendChild(buttonContainer);
const popupContainer = document.createElement('div'); const popupContainer = document.createElement('div');
popupContainer.innerHTML = safeValue; popupContainer.innerHTML = safeValue;
popupContainer.appendChild(buttonContainer); popupContainer.appendChild(scrollableContainer);
// Ensure the popup uses flex layout
popupContainer.style.display = 'flex';
popupContainer.style.flexDirection = 'column';
popupContainer.style.maxHeight = '80vh'; // Limit the overall height of the popup
popup = new Popup(popupContainer, POPUP_TYPE.TEXT, '', { okButton: 'Cancel', allowVerticalScrolling: true }); popup = new Popup(popupContainer, POPUP_TYPE.TEXT, '', { okButton: 'Cancel', allowVerticalScrolling: true });
popup.show() popup.show()

View File

@@ -23,29 +23,42 @@ export let openRouterModels = [];
const OPENROUTER_PROVIDERS = [ const OPENROUTER_PROVIDERS = [
'OpenAI', 'OpenAI',
'Anthropic', 'Anthropic',
'HuggingFace',
'Google', 'Google',
'Mancer', 'Google AI Studio',
'Mancer 2', 'Groq',
'SambaNova',
'Cohere',
'Mistral',
'Together', 'Together',
'Together 2',
'Fireworks',
'DeepInfra', 'DeepInfra',
'Lepton',
'Novita',
'Avian',
'Lambda',
'Azure', 'Azure',
'Modal', 'Modal',
'AnyScale', 'AnyScale',
'Replicate', 'Replicate',
'Perplexity', 'Perplexity',
'Recursal', 'Recursal',
'Fireworks',
'Mistral',
'Groq',
'Cohere',
'Lepton',
'OctoAI', 'OctoAI',
'Novita',
'Lynn',
'Lynn 2',
'DeepSeek', 'DeepSeek',
'Infermatic', 'Infermatic',
'AI21',
'Featherless',
'Inflection',
'xAI',
'01.AI',
'HuggingFace',
'Mancer',
'Mancer 2',
'Hyperbolic',
'Hyperbolic 2',
'Lynn 2',
'Lynn',
'Reflection',
]; ];
export async function loadOllamaModels(data) { export async function loadOllamaModels(data) {

View File

@@ -9,6 +9,7 @@
@import url(css/logprobs.css); @import url(css/logprobs.css);
@import url(css/accounts.css); @import url(css/accounts.css);
@import url(css/tags.css); @import url(css/tags.css);
@import url(css/scrollable-button.css);
:root { :root {
--doc-height: 100%; --doc-height: 100%;

View File

@@ -7,7 +7,7 @@ import sanitize from 'sanitize-filename';
import { sync as writeFileAtomicSync } from 'write-file-atomic'; import { sync as writeFileAtomicSync } from 'write-file-atomic';
import FormData from 'form-data'; import FormData from 'form-data';
import { getBasicAuthHeader, delay } from '../util.js'; import { delay, getBasicAuthHeader, tryParse } from '../util.js';
import { jsonParser } from '../express-common.js'; import { jsonParser } from '../express-common.js';
import { readSecret, SECRET_KEYS } from './secrets.js'; import { readSecret, SECRET_KEYS } from './secrets.js';
@@ -19,7 +19,7 @@ import { readSecret, SECRET_KEYS } from './secrets.js';
function getComfyWorkflows(directories) { function getComfyWorkflows(directories) {
return fs return fs
.readdirSync(directories.comfyWorkflows) .readdirSync(directories.comfyWorkflows)
.filter(file => file[0] != '.' && file.toLowerCase().endsWith('.json')) .filter(file => file[0] !== '.' && file.toLowerCase().endsWith('.json'))
.sort(Intl.Collator().compare); .sort(Intl.Collator().compare);
} }
@@ -67,8 +67,7 @@ router.post('/upscalers', jsonParser, async (request, response) => {
/** @type {any} */ /** @type {any} */
const data = await result.json(); const data = await result.json();
const names = data.map(x => x.name); return data.map(x => x.name);
return names;
} }
async function getLatentUpscalers() { async function getLatentUpscalers() {
@@ -88,8 +87,7 @@ router.post('/upscalers', jsonParser, async (request, response) => {
/** @type {any} */ /** @type {any} */
const data = await result.json(); const data = await result.json();
const names = data.map(x => x.name); return data.map(x => x.name);
return names;
} }
const [upscalers, latentUpscalers] = await Promise.all([getUpscalerModels(), getLatentUpscalers()]); const [upscalers, latentUpscalers] = await Promise.all([getUpscalerModels(), getLatentUpscalers()]);
@@ -241,8 +239,7 @@ router.post('/set-model', jsonParser, async (request, response) => {
'Authorization': getBasicAuthHeader(request.body.auth), 'Authorization': getBasicAuthHeader(request.body.auth),
}, },
}); });
const data = await result.json(); return await result.json();
return data;
} }
const url = new URL(request.body.url); const url = new URL(request.body.url);
@@ -274,7 +271,7 @@ router.post('/set-model', jsonParser, async (request, response) => {
const progress = progressState['progress']; const progress = progressState['progress'];
const jobCount = progressState['state']['job_count']; const jobCount = progressState['state']['job_count'];
if (progress == 0.0 && jobCount === 0) { if (progress === 0.0 && jobCount === 0) {
break; break;
} }
@@ -412,8 +409,19 @@ comfy.post('/models', jsonParser, async (request, response) => {
} }
/** @type {any} */ /** @type {any} */
const data = await result.json(); const data = await result.json();
return response.send(data.CheckpointLoaderSimple.input.required.ckpt_name[0].map(it => ({ value: it, text: it })));
} catch (error) { const ckpts = data.CheckpointLoaderSimple.input.required.ckpt_name[0].map(it => ({ value: it, text: it })) || [];
const unets = data.UNETLoader.input.required.unet_name[0].map(it => ({ value: it, text: `UNet: ${it}` })) || [];
// load list of GGUF unets from diffusion_models if the loader node is available
const ggufs = data.UnetLoaderGGUF?.input.required.unet_name[0].map(it => ({ value: it, text: `GGUF: ${it}` })) || [];
const models = [...ckpts, ...unets, ...ggufs];
// make the display names of the models somewhat presentable
models.forEach(it => it.text = it.text.replace(/\.[^.]*$/, '').replace(/_/g, ' '));
return response.send(models);
} catch (error) {
console.log(error); console.log(error);
return response.sendStatus(500); return response.sendStatus(500);
} }
@@ -527,7 +535,8 @@ comfy.post('/generate', jsonParser, async (request, response) => {
body: request.body.prompt, body: request.body.prompt,
}); });
if (!promptResult.ok) { if (!promptResult.ok) {
throw new Error('ComfyUI returned an error.'); const text = await promptResult.text();
throw new Error('ComfyUI returned an error.', { cause: tryParse(text) });
} }
/** @type {any} */ /** @type {any} */
@@ -550,7 +559,13 @@ comfy.post('/generate', jsonParser, async (request, response) => {
await delay(100); await delay(100);
} }
if (item.status.status_str === 'error') { if (item.status.status_str === 'error') {
throw new Error('ComfyUI generation did not succeed.'); // Report node tracebacks if available
const errorMessages = item.status?.messages
?.filter(it => it[0] === 'execution_error')
.map(it => it[1])
.map(it => `${it.node_type} [${it.node_id}] ${it.exception_type}: ${it.exception_message}`)
.join('\n') || '';
throw new Error(`ComfyUI generation did not succeed.\n\n${errorMessages}`.trim());
} }
const imgInfo = Object.keys(item.outputs).map(it => item.outputs[it].images).flat()[0]; const imgInfo = Object.keys(item.outputs).map(it => item.outputs[it].images).flat()[0];
const imgUrl = new URL(request.body.url); const imgUrl = new URL(request.body.url);
@@ -560,11 +575,12 @@ comfy.post('/generate', jsonParser, async (request, response) => {
if (!imgResponse.ok) { if (!imgResponse.ok) {
throw new Error('ComfyUI returned an error.'); throw new Error('ComfyUI returned an error.');
} }
const imgBuffer = await imgResponse.buffer(); const imgBuffer = await imgResponse.arrayBuffer();
return response.send(imgBuffer.toString('base64')); return response.send(Buffer.from(imgBuffer).toString('base64'));
} catch (error) { } catch (error) {
console.log(error); console.log('ComfyUI error:', error);
return response.sendStatus(500); response.status(500).send(error.message);
return response;
} }
}); });