mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-02-03 12:47:35 +01:00
Merge branch 'staging' into claude-caching-at-depth
This commit is contained in:
commit
c3483bc432
137
default/content/Char_Avatar_Comfy_Workflow.json
Normal file
137
default/content/Char_Avatar_Comfy_Workflow.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
@ -135,6 +135,10 @@
|
||||
"filename": "Default_Comfy_Workflow.json",
|
||||
"type": "workflow"
|
||||
},
|
||||
{
|
||||
"filename": "Char_Avatar_Comfy_Workflow.json",
|
||||
"type": "workflow"
|
||||
},
|
||||
{
|
||||
"filename": "presets/kobold/Ace of Spades.json",
|
||||
"type": "kobold_preset"
|
||||
|
6
package-lock.json
generated
6
package-lock.json
generated
@ -3019,9 +3019,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
|
||||
"version": "7.0.5",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz",
|
||||
"integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
|
@ -72,6 +72,14 @@
|
||||
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 {
|
||||
grid-row-start: 3;
|
||||
grid-row-end: 4;
|
||||
|
19
public/css/scrollable-button.css
Normal file
19
public/css/scrollable-button.css
Normal 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;
|
||||
}
|
@ -3026,6 +3026,7 @@
|
||||
<option value="codestral-latest">codestral-latest</option>
|
||||
<option value="codestral-mamba-latest">codestral-mamba-latest</option>
|
||||
<option value="pixtral-12b-latest">pixtral-12b-latest</option>
|
||||
<option value="pixtral-large-latest">pixtral-large-latest</option>
|
||||
</optgroup>
|
||||
<optgroup label="Sub-versions">
|
||||
<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-large-2402">mistral-large-2402</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-blue">codestral-2405-blue</option>
|
||||
<option value="codestral-mamba-2407">codestral-mamba-2407</option>
|
||||
<option value="pixtral-12b-2409">pixtral-12b-2409</option>
|
||||
<option value="pixtral-large-2411">pixtral-large-2411</option>
|
||||
</optgroup>
|
||||
<optgroup id="mistralai_other_models" label="Other"></optgroup>
|
||||
</select>
|
||||
@ -4503,7 +4506,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
|
||||
</div>
|
||||
@ -6631,8 +6634,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<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>
|
||||
<hr>
|
||||
<div id="logprobs_generation_output"></div>
|
||||
|
@ -703,16 +703,13 @@ const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
|
||||
*/
|
||||
function autoFitSendTextArea() {
|
||||
const originalScrollBottom = chatBlock.scrollHeight - (chatBlock.scrollTop + chatBlock.offsetHeight);
|
||||
if (Math.ceil(sendTextArea.scrollHeight + 3) >= Math.floor(sendTextArea.offsetHeight)) {
|
||||
const sendTextAreaMinHeight = '0px';
|
||||
sendTextArea.style.height = sendTextAreaMinHeight;
|
||||
}
|
||||
const newHeight = sendTextArea.scrollHeight + 3;
|
||||
|
||||
sendTextArea.style.height = '1px'; // Reset height to 1px to force recalculation of scrollHeight
|
||||
const newHeight = sendTextArea.scrollHeight;
|
||||
sendTextArea.style.height = `${newHeight}px`;
|
||||
|
||||
if (!isFirefox) {
|
||||
const newScrollTop = Math.round(chatBlock.scrollHeight - (chatBlock.offsetHeight + originalScrollBottom));
|
||||
chatBlock.scrollTop = newScrollTop;
|
||||
chatBlock.scrollTop = chatBlock.scrollHeight - (chatBlock.offsetHeight + originalScrollBottom);
|
||||
}
|
||||
}
|
||||
export const autoFitSendTextAreaDebounced = debounce(autoFitSendTextArea, debounce_timeout.short);
|
||||
|
@ -37,6 +37,8 @@
|
||||
<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-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="openai" value="gpt-4-vision-preview">gpt-4-vision-preview</option>
|
||||
<option data-type="openai" value="gpt-4-turbo">gpt-4-turbo</option>
|
||||
|
@ -17,6 +17,7 @@
|
||||
<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="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="width" class="sd_comfy_workflow_editor_not_found">"%width%"</li>
|
||||
<li data-placeholder="height" class="sd_comfy_workflow_editor_not_found">"%height%"</li>
|
||||
|
@ -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);
|
||||
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 => {
|
||||
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());
|
||||
if (response.ok) {
|
||||
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));
|
||||
} else {
|
||||
workflow = workflow.replaceAll('"%user_avatar%"', JSON.stringify(PNG_PIXEL));
|
||||
@ -3289,7 +3294,8 @@ async function generateComfyImage(prompt, negativePrompt, signal) {
|
||||
const response = await fetch(getCharacterAvatarUrl());
|
||||
if (response.ok) {
|
||||
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));
|
||||
} else {
|
||||
workflow = workflow.replaceAll('"%char_avatar%"', JSON.stringify(PNG_PIXEL));
|
||||
|
@ -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}}" >
|
||||
</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>
|
||||
<span data-i18n="Denoising strength">Denoising strength</span>
|
||||
</small>
|
||||
|
@ -23,7 +23,7 @@ import {
|
||||
import { collapseNewlines, registerDebugFunction } from '../../power-user.js';
|
||||
import { SECRET_KEYS, secret_state, writeSecret } from '../../secrets.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 { getSortedEntries } from '../../world-info.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_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 = {
|
||||
// For both
|
||||
source: 'transformers',
|
||||
@ -125,7 +128,7 @@ async function onVectorizeAllClick() {
|
||||
// upon request of a full vectorise
|
||||
cachedSummaries.clear();
|
||||
|
||||
const batchSize = 5;
|
||||
const batchSize = getBatchSize();
|
||||
const elapsedLog = [];
|
||||
let finished = false;
|
||||
$('#vectorize_progress').show();
|
||||
@ -560,7 +563,9 @@ async function vectorizeFile(fileText, fileName, collectionId, chunkSize, overla
|
||||
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 delimiters = getChunkDelimiters();
|
||||
// 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);
|
||||
|
||||
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);
|
||||
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>
|
||||
Unique hashes: <b>${uniqueHashes}</b><br><br>
|
||||
I'll mark collected messages with a green circle.`,
|
||||
`Stats for chat ${chatId}`,
|
||||
`Stats for chat ${escapeHtml(chatId)}`,
|
||||
{ timeOut: 10000, escapeHtml: false },
|
||||
);
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
import {
|
||||
animation_duration,
|
||||
callPopup,
|
||||
chat,
|
||||
cleanUpMessage,
|
||||
event_types,
|
||||
@ -13,15 +12,22 @@ import {
|
||||
import { debounce, delay, getStringHash } from './utils.js';
|
||||
import { decodeTextTokens, getTokenizerBestMatch } from './tokenizers.js';
|
||||
import { power_user } from './power-user.js';
|
||||
import { callGenericPopup, POPUP_TYPE } from './popup.js';
|
||||
import { t } from './i18n.js';
|
||||
|
||||
const TINTS = 4;
|
||||
const MAX_MESSAGE_LOGPROBS = 100;
|
||||
const REROLL_BUTTON = $('#logprobsReroll');
|
||||
|
||||
/**
|
||||
* Tuple of a candidate token and its logarithm of probability of being chosen
|
||||
* @typedef {[string, number]} Candidate - (token, logprob)
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {(Node|JQuery<Text>|JQuery<HTMLElement>)[]} NodeArray - Array of DOM nodes
|
||||
*/
|
||||
|
||||
/**
|
||||
* Logprob data for a single message
|
||||
* @typedef {Object} MessageLogprobData
|
||||
@ -43,17 +49,26 @@ const MAX_MESSAGE_LOGPROBS = 100;
|
||||
* @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,
|
||||
/** @type {Map<number, MessageLogprobData>} */
|
||||
messageLogprobs: new Map(),
|
||||
};
|
||||
|
||||
/**
|
||||
* renderAlternativeTokensView renders the Token Probabilities UI and all
|
||||
* subviews with the active message's logprobs data. If the message has no token
|
||||
* logprobs, a zero-state is rendered.
|
||||
* Renders the Token Probabilities UI and all subviews with the active message's
|
||||
* logprobs data. If the message has no token logprobs, a message is displayed.
|
||||
*/
|
||||
function renderAlternativeTokensView() {
|
||||
const view = $('#logprobs_generation_output');
|
||||
@ -68,13 +83,14 @@ function renderAlternativeTokensView() {
|
||||
const usingSmoothStreaming = isStreamingEnabled() && power_user.smooth_streaming;
|
||||
if (!messageLogprobs?.length || usingSmoothStreaming) {
|
||||
const emptyState = $('<div></div>');
|
||||
const noTokensMsg = usingSmoothStreaming
|
||||
? 'Token probabilities are not available when using Smooth Streaming.'
|
||||
: 'No token probabilities available for the current message.';
|
||||
const msg = power_user.request_token_probabilities
|
||||
? noTokensMsg
|
||||
: '<span>Enable <b>Request token probabilities</b> in the User Settings menu to use this feature.</span>';
|
||||
emptyState.html(msg);
|
||||
const noTokensMsg = !power_user.request_token_probabilities
|
||||
? '<span>Enable <b>Request token probabilities</b> in the User Settings menu to use this feature.</span>'
|
||||
: usingSmoothStreaming
|
||||
? t`Token probabilities are not available when using Smooth Streaming.`
|
||||
: is_send_press
|
||||
? t`Generation in progress...`
|
||||
: t`No token probabilities available for the current message.`;
|
||||
emptyState.html(noTokensMsg);
|
||||
emptyState.addClass('logprobs_empty_state');
|
||||
view.append(emptyState);
|
||||
return;
|
||||
@ -82,16 +98,34 @@ function renderAlternativeTokensView() {
|
||||
|
||||
const prefix = continueFrom || '';
|
||||
const tokenSpans = [];
|
||||
REROLL_BUTTON.toggle(!!prefix);
|
||||
|
||||
if (prefix) {
|
||||
const prefixSpan = $('<span></span>');
|
||||
prefixSpan.text(prefix);
|
||||
prefixSpan.html(prefixSpan.html().replace(/\n/g, '<br>'));
|
||||
prefixSpan.addClass('logprobs_output_prefix');
|
||||
prefixSpan.attr('title', 'Select to reroll the last \'Continue\' generation.\nHold the CTRL key when clicking to reroll from before that word.');
|
||||
prefixSpan.click(onPrefixClicked);
|
||||
addKeyboardProps(prefixSpan);
|
||||
tokenSpans.push(...withVirtualWhitespace(prefix, prefixSpan));
|
||||
REROLL_BUTTON.off('click').on('click', () => onPrefixClicked(prefix.length));
|
||||
|
||||
let cumulativeOffset = 0;
|
||||
const words = prefix.split(/\s+/);
|
||||
const delimiters = prefix.match(/\s+/g) || []; // Capture the actual delimiters
|
||||
|
||||
words.forEach((word, i) => {
|
||||
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) => {
|
||||
@ -101,7 +135,7 @@ function renderAlternativeTokensView() {
|
||||
span.text(text);
|
||||
span.addClass('logprobs_output_token');
|
||||
span.addClass('logprobs_tint_' + (i % TINTS));
|
||||
span.click(() => onSelectedTokenChanged(tokenData, span));
|
||||
span.on('click', () => onSelectedTokenChanged(tokenData, span));
|
||||
addKeyboardProps(span);
|
||||
tokenSpans.push(...withVirtualWhitespace(token, span));
|
||||
});
|
||||
@ -129,6 +163,10 @@ function addKeyboardProps(element) {
|
||||
/**
|
||||
* renderTopLogprobs renders the top logprobs subview with the currently
|
||||
* 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() {
|
||||
$('#logprobs_top_logprobs_hint').hide();
|
||||
@ -150,8 +188,7 @@ function renderTopLogprobs() {
|
||||
const probability = Math.exp(log);
|
||||
sum += probability;
|
||||
return [text, probability, log];
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return [text, log, null];
|
||||
}
|
||||
});
|
||||
@ -167,15 +204,15 @@ function renderTopLogprobs() {
|
||||
container.addClass('selected');
|
||||
}
|
||||
|
||||
const tokenText = $('<span></span>').text(`${toVisibleWhitespace(token)}`);
|
||||
const percentText = $('<span></span>').text(`${(probability * 100).toFixed(2)}%`);
|
||||
const tokenText = $('<span></span>').text(`${toVisibleWhitespace(token.toString())}`);
|
||||
const percentText = $('<span></span>').text(`${(+probability * 100).toFixed(2)}%`);
|
||||
container.append(tokenText, percentText);
|
||||
if (log) {
|
||||
container.attr('title', `logarithm: ${log}`);
|
||||
}
|
||||
addKeyboardProps(container);
|
||||
if (token !== '<others>') {
|
||||
container.click(() => onAlternativeClicked(state.selectedTokenLogprobs, token));
|
||||
container.on('click', () => onAlternativeClicked(state.selectedTokenLogprobs, token.toString()));
|
||||
} else {
|
||||
container.prop('disabled', true);
|
||||
}
|
||||
@ -192,11 +229,10 @@ function renderTopLogprobs() {
|
||||
}
|
||||
|
||||
/**
|
||||
* onSelectedTokenChanged is called when the user clicks on a token in the
|
||||
* token output view. It updates the selected token state and re-renders the
|
||||
* top logprobs view, or deselects the token if it was already selected.
|
||||
* User clicks on a token in the token output view. It updates the selected token state
|
||||
* and re-renders the top logprobs view, or deselects the token if it was already selected.
|
||||
* @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) {
|
||||
$('.logprobs_output_token.selected').removeClass('selected');
|
||||
@ -223,7 +259,10 @@ function onAlternativeClicked(tokenLogprobs, alternative) {
|
||||
}
|
||||
|
||||
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();
|
||||
@ -234,79 +273,29 @@ function onAlternativeClicked(tokenLogprobs, alternative) {
|
||||
|
||||
const prefix = continueFrom || '';
|
||||
const prompt = prefix + tokens.join('');
|
||||
const messageId = chat.length - 1;
|
||||
createSwipe(messageId, prompt);
|
||||
|
||||
$('.swipe_right:last').click(); // :see_no_evil:
|
||||
|
||||
Generate('continue').then(_ => void _);
|
||||
addGeneration(prompt);
|
||||
}
|
||||
|
||||
/**
|
||||
* getTextBeforeClickedWord retrieves the portion of text within a span
|
||||
* that appears before the word clicked by the user. Using the x and y
|
||||
* coordinates from a PointerEvent, this function identifies the exact
|
||||
* word clicked and returns the text preceding it within the span.
|
||||
* User clicks on the reroll button in the token output view, or on a word in the
|
||||
* prefix. Retrieve the prefix for the current message and truncate it at the
|
||||
* offset for the selected word. Then request a `continue` completion from the
|
||||
* model with the new prompt.
|
||||
*
|
||||
* If the clicked position does not resolve to a valid word or text node,
|
||||
* the entire span text is returned as a fallback.
|
||||
* If no offset is provided, the entire prefix will be rerolled.
|
||||
*
|
||||
* @param {PointerEvent} event - The click event containing the x and y coordinates.
|
||||
* @param {string} spanText - The full text content of the span element.
|
||||
* @returns {string} The text before the clicked word, or the entire span text as fallback.
|
||||
* @param {number} offset - index of the token in the prefix to reroll from
|
||||
* @returns {void}
|
||||
* @param offset
|
||||
*/
|
||||
function getTextBeforeClickedWord(event, spanText) {
|
||||
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() {
|
||||
function onPrefixClicked(offset = undefined) {
|
||||
if (!checkGenerateReady()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { continueFrom } = getActiveMessageLogprobData();
|
||||
const messageId = chat.length - 1;
|
||||
|
||||
// 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 _);
|
||||
const { continueFrom } = getActiveMessageLogprobData() || {};
|
||||
const prefix = continueFrom ? continueFrom.substring(0, offset) : '';
|
||||
addGeneration(prefix);
|
||||
}
|
||||
|
||||
function checkGenerateReady() {
|
||||
@ -317,6 +306,22 @@ function checkGenerateReady() {
|
||||
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
|
||||
@ -356,15 +361,14 @@ function onToggleLogprobsPanel() {
|
||||
}
|
||||
|
||||
/**
|
||||
* createSwipe appends a new swipe to the target chat message with the given
|
||||
* text.
|
||||
* Appends a new swipe to the target chat message with the given text.
|
||||
* @param {number} messageId - target chat message ID
|
||||
* @param {string} prompt - initial prompt text which will be continued
|
||||
*/
|
||||
function createSwipe(messageId, prompt) {
|
||||
// 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
|
||||
const cleanedPrompt = cleanUpMessage(prompt, false, false);
|
||||
const cleanedPrompt = cleanUpMessage(prompt, false, false, true);
|
||||
|
||||
const msg = chat[messageId];
|
||||
const newSwipeInfo = {
|
||||
@ -399,10 +403,11 @@ function toVisibleWhitespace(input) {
|
||||
* 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.
|
||||
* @param {string} text - token text being evaluated for whitespace
|
||||
* @param {Element} span - target span node to be wrapped
|
||||
* @returns {Element[]} array of nodes to be appended to the DOM
|
||||
* @param {Node|JQuery} span - target span node to be wrapped
|
||||
* @returns {NodeArray} - array of nodes to be appended to the parent element
|
||||
*/
|
||||
function withVirtualWhitespace(text, span) {
|
||||
/** @type {NodeArray} */
|
||||
const result = [span];
|
||||
if (text.match(/^\s/)) {
|
||||
result.unshift(document.createTextNode('\u200b'));
|
||||
@ -430,12 +435,16 @@ function withVirtualWhitespace(text, span) {
|
||||
}
|
||||
|
||||
/**
|
||||
* saveLogprobsForActiveMessage receives an array of TokenLogprobs objects
|
||||
* representing the top logprobs for each token in a message and associates it
|
||||
* with the active message.
|
||||
* Receives 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 {string | null} continueFrom - for 'continue' generations, the prompt
|
||||
*/
|
||||
@ -445,7 +454,10 @@ export function saveLogprobsForActiveMessage(logprobs, continueFrom) {
|
||||
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;
|
||||
/** @type {MessageLogprobData} */
|
||||
@ -491,17 +503,18 @@ function getActiveMessageLogprobData() {
|
||||
return state.messageLogprobs.get(hash) || null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* convertLogprobTokenIdsToText mutates the given logprobs data's topLogprobs
|
||||
* field keyed by token text instead of token ID. This is only necessary for
|
||||
* APIs which only return token IDs in their logprobs data; for others this
|
||||
* function is a no-op.
|
||||
* convertLogprobTokenIdsToText replaces token IDs in logprobs data with text tokens,
|
||||
* for APIs that return token IDs instead of text tokens, to wit: NovelAI.
|
||||
*
|
||||
* @param {TokenLogprobs[]} input - logprobs data with numeric token IDs
|
||||
*/
|
||||
function convertTokenIdLogprobsToText(input) {
|
||||
const api = getGeneratingApi();
|
||||
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);
|
||||
@ -512,7 +525,8 @@ function convertTokenIdLogprobsToText(input) {
|
||||
)));
|
||||
|
||||
// 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]]));
|
||||
|
||||
// Fixup logprobs data with token text
|
||||
@ -525,9 +539,10 @@ function convertTokenIdLogprobsToText(input) {
|
||||
}
|
||||
|
||||
export function initLogprobs() {
|
||||
REROLL_BUTTON.hide();
|
||||
const debouncedRender = debounce(renderAlternativeTokensView);
|
||||
$('#logprobsViewerClose').click(onToggleLogprobsPanel);
|
||||
$('#option_toggle_logprobs').click(onToggleLogprobsPanel);
|
||||
$('#logprobsViewerClose').on('click', onToggleLogprobsPanel);
|
||||
$('#option_toggle_logprobs').on('click', onToggleLogprobsPanel);
|
||||
eventSource.on(event_types.CHAT_CHANGED, debouncedRender);
|
||||
eventSource.on(event_types.CHARACTER_MESSAGE_RENDERED, debouncedRender);
|
||||
eventSource.on(event_types.IMPERSONATE_READY, debouncedRender);
|
||||
|
@ -4165,7 +4165,7 @@ async function onModelChange() {
|
||||
$('#openai_max_context').attr('max', unlocked_max);
|
||||
} else if (oai_settings.mistralai_model.includes('codestral-mamba')) {
|
||||
$('#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);
|
||||
} else if (oai_settings.mistralai_model.includes('mistral-nemo')) {
|
||||
$('#openai_max_context').attr('max', max_128k);
|
||||
@ -4764,6 +4764,8 @@ export function isImageInliningSupported() {
|
||||
'pixtral-12b-latest',
|
||||
'pixtral-12b',
|
||||
'pixtral-12b-2409',
|
||||
'pixtral-large-latest',
|
||||
'pixtral-large-2411',
|
||||
];
|
||||
|
||||
switch (oai_settings.chat_completion_source) {
|
||||
|
@ -2083,7 +2083,10 @@ async function buttonsCallback(args, text) {
|
||||
let popup;
|
||||
|
||||
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) {
|
||||
const buttonElement = document.createElement('div');
|
||||
@ -2096,9 +2099,16 @@ async function buttonsCallback(args, text) {
|
||||
buttonContainer.appendChild(buttonElement);
|
||||
}
|
||||
|
||||
scrollableContainer.appendChild(buttonContainer);
|
||||
|
||||
const popupContainer = document.createElement('div');
|
||||
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.show()
|
||||
|
@ -23,29 +23,42 @@ export let openRouterModels = [];
|
||||
const OPENROUTER_PROVIDERS = [
|
||||
'OpenAI',
|
||||
'Anthropic',
|
||||
'HuggingFace',
|
||||
'Google',
|
||||
'Mancer',
|
||||
'Mancer 2',
|
||||
'Google AI Studio',
|
||||
'Groq',
|
||||
'SambaNova',
|
||||
'Cohere',
|
||||
'Mistral',
|
||||
'Together',
|
||||
'Together 2',
|
||||
'Fireworks',
|
||||
'DeepInfra',
|
||||
'Lepton',
|
||||
'Novita',
|
||||
'Avian',
|
||||
'Lambda',
|
||||
'Azure',
|
||||
'Modal',
|
||||
'AnyScale',
|
||||
'Replicate',
|
||||
'Perplexity',
|
||||
'Recursal',
|
||||
'Fireworks',
|
||||
'Mistral',
|
||||
'Groq',
|
||||
'Cohere',
|
||||
'Lepton',
|
||||
'OctoAI',
|
||||
'Novita',
|
||||
'Lynn',
|
||||
'Lynn 2',
|
||||
'DeepSeek',
|
||||
'Infermatic',
|
||||
'AI21',
|
||||
'Featherless',
|
||||
'Inflection',
|
||||
'xAI',
|
||||
'01.AI',
|
||||
'HuggingFace',
|
||||
'Mancer',
|
||||
'Mancer 2',
|
||||
'Hyperbolic',
|
||||
'Hyperbolic 2',
|
||||
'Lynn 2',
|
||||
'Lynn',
|
||||
'Reflection',
|
||||
];
|
||||
|
||||
export async function loadOllamaModels(data) {
|
||||
|
@ -9,6 +9,7 @@
|
||||
@import url(css/logprobs.css);
|
||||
@import url(css/accounts.css);
|
||||
@import url(css/tags.css);
|
||||
@import url(css/scrollable-button.css);
|
||||
|
||||
:root {
|
||||
--doc-height: 100%;
|
||||
|
@ -7,7 +7,7 @@ import sanitize from 'sanitize-filename';
|
||||
import { sync as writeFileAtomicSync } from 'write-file-atomic';
|
||||
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 { readSecret, SECRET_KEYS } from './secrets.js';
|
||||
|
||||
@ -19,7 +19,7 @@ import { readSecret, SECRET_KEYS } from './secrets.js';
|
||||
function getComfyWorkflows(directories) {
|
||||
return fs
|
||||
.readdirSync(directories.comfyWorkflows)
|
||||
.filter(file => file[0] != '.' && file.toLowerCase().endsWith('.json'))
|
||||
.filter(file => file[0] !== '.' && file.toLowerCase().endsWith('.json'))
|
||||
.sort(Intl.Collator().compare);
|
||||
}
|
||||
|
||||
@ -67,8 +67,7 @@ router.post('/upscalers', jsonParser, async (request, response) => {
|
||||
|
||||
/** @type {any} */
|
||||
const data = await result.json();
|
||||
const names = data.map(x => x.name);
|
||||
return names;
|
||||
return data.map(x => x.name);
|
||||
}
|
||||
|
||||
async function getLatentUpscalers() {
|
||||
@ -88,8 +87,7 @@ router.post('/upscalers', jsonParser, async (request, response) => {
|
||||
|
||||
/** @type {any} */
|
||||
const data = await result.json();
|
||||
const names = data.map(x => x.name);
|
||||
return names;
|
||||
return data.map(x => x.name);
|
||||
}
|
||||
|
||||
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),
|
||||
},
|
||||
});
|
||||
const data = await result.json();
|
||||
return data;
|
||||
return await result.json();
|
||||
}
|
||||
|
||||
const url = new URL(request.body.url);
|
||||
@ -274,7 +271,7 @@ router.post('/set-model', jsonParser, async (request, response) => {
|
||||
|
||||
const progress = progressState['progress'];
|
||||
const jobCount = progressState['state']['job_count'];
|
||||
if (progress == 0.0 && jobCount === 0) {
|
||||
if (progress === 0.0 && jobCount === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
@ -412,8 +409,19 @@ comfy.post('/models', jsonParser, async (request, response) => {
|
||||
}
|
||||
/** @type {any} */
|
||||
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);
|
||||
return response.sendStatus(500);
|
||||
}
|
||||
@ -527,7 +535,8 @@ comfy.post('/generate', jsonParser, async (request, response) => {
|
||||
body: request.body.prompt,
|
||||
});
|
||||
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} */
|
||||
@ -550,7 +559,13 @@ comfy.post('/generate', jsonParser, async (request, response) => {
|
||||
await delay(100);
|
||||
}
|
||||
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 imgUrl = new URL(request.body.url);
|
||||
@ -560,11 +575,12 @@ comfy.post('/generate', jsonParser, async (request, response) => {
|
||||
if (!imgResponse.ok) {
|
||||
throw new Error('ComfyUI returned an error.');
|
||||
}
|
||||
const imgBuffer = await imgResponse.buffer();
|
||||
return response.send(imgBuffer.toString('base64'));
|
||||
const imgBuffer = await imgResponse.arrayBuffer();
|
||||
return response.send(Buffer.from(imgBuffer).toString('base64'));
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return response.sendStatus(500);
|
||||
console.log('ComfyUI error:', error);
|
||||
response.status(500).send(error.message);
|
||||
return response;
|
||||
}
|
||||
});
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user