From 146e82e44a9fdab0cd7eeea2b1c07864aad50589 Mon Sep 17 00:00:00 2001 From: ceruleandeep Date: Tue, 17 Sep 2024 14:41:59 +1000 Subject: [PATCH 1/2] Add model metadata to Horde models endpoint Display model metadata in Horde model picker --- public/scripts/horde.js | 99 +++++++++++++++++++++++++++++++++-------- src/endpoints/horde.js | 33 ++++++++++++-- 2 files changed, 109 insertions(+), 23 deletions(-) diff --git a/public/scripts/horde.js b/public/scripts/horde.js index 2581d522a..4b67dc8fc 100644 --- a/public/scripts/horde.js +++ b/public/scripts/horde.js @@ -1,10 +1,10 @@ import { - saveSettingsDebounced, + amount_gen, callPopup, - setGenerationProgress, getRequestHeaders, max_context, - amount_gen, + saveSettingsDebounced, + setGenerationProgress, } from '../script.js'; import { SECRET_KEYS, writeSecret } from './secrets.js'; import { delay } from './utils.js'; @@ -45,8 +45,7 @@ async function getWorkers(force) { headers: getRequestHeaders(), body: JSON.stringify({ force }), }); - const data = await response.json(); - return data; + return await response.json(); } /** @@ -61,16 +60,18 @@ async function getModels(force) { body: JSON.stringify({ force }), }); const data = await response.json(); + console.log('getModels', data); return data; } + /** * Gets the status of a Horde task. * @param {string} taskId Task ID * @returns {Promise} Task status */ async function getTaskStatus(taskId) { - const response = await fetch('/api/horde/task-status', { + const response = await fetch('/api/horde/task-status', { method: 'POST', headers: getRequestHeaders(), body: JSON.stringify({ taskId }), @@ -80,8 +81,7 @@ async function getTaskStatus(taskId) { throw new Error(`Failed to get task status: ${response.statusText}`); } - const data = await response.json(); - return data; + return await response.json(); } /** @@ -148,7 +148,7 @@ async function adjustHordeGenerationParams(max_context_length, max_length) { for (const model of selectedModels) { for (const worker of workers) { - if (model.cluster == worker.cluster && worker.models.includes(model.name)) { + if (model.cluster === worker.cluster && worker.models.includes(model.name)) { // Skip workers that are not trusted if the option is enabled if (horde_settings.trusted_workers_only && !worker.trusted) { continue; @@ -250,12 +250,10 @@ async function generateHorde(prompt, params, signal, reportProgress) { console.log(generatedText); console.log(`Generated by Horde Worker: ${WorkerName} [${WorkerModel}]`); return { text: generatedText, workerName: `Generated by Horde worker: ${WorkerName} [${WorkerModel}]` }; - } - else if (!queue_position_first) { + } else if (!queue_position_first) { queue_position_first = statusCheckJson.queue_position; reportProgress && setGenerationProgress(0); - } - else if (statusCheckJson.queue_position >= 0) { + } else if (statusCheckJson.queue_position >= 0) { let queue_position = statusCheckJson.queue_position; const progress = Math.round(100 - (queue_position / queue_position_first * 100)); reportProgress && setGenerationProgress(progress); @@ -268,17 +266,25 @@ async function generateHorde(prompt, params, signal, reportProgress) { throw new Error('Horde timeout'); } + /** * Displays the available models in the Horde model selection dropdown. * @param {boolean} force Force refresh of the models */ async function getHordeModels(force) { + const sortByPerformance = (a, b) => b.performance - a.performance; + const sortByWhitelisted = (a, b) => b.is_whitelisted - a.is_whitelisted; + const sortByPopular = (a, b) => b.tags?.includes('popular') - a.tags?.includes('popular'); + $('#horde_model').empty(); - models = (await getModels(force)).sort((a, b) => b.performance - a.performance); + models = (await getModels(force)).sort((a, b) => { + return sortByWhitelisted(a, b) || sortByPopular(a, b) || sortByPerformance(a, b); + }); for (const model of models) { + console.log('getHordeModels', model); const option = document.createElement('option'); option.value = model.name; - option.innerText = `${model.name} (ETA: ${model.eta}s, Speed: ${model.performance}, Queue: ${model.queued}, Workers: ${model.count})`; + option.innerText = hordeModelTextString(model); option.selected = horde_settings.models.includes(model.name); $('#horde_model').append(option); } @@ -323,8 +329,66 @@ async function showKudos() { toastr.info(`Kudos: ${data.kudos}`, data.username); } +function hordeModelTextString(model) { + const q = hordeModelQueueStateString(model); + return `${model.name} (${q})`; +} + +function hordeModelQueueStateString(model) { + return `ETA: ${model.eta}s, Speed: ${model.performance}, Queue: ${model.queued}, Workers: ${model.count}`; +} + +function getHordeModelTemplate(option) { + const model = models.find(x => x.name === option?.element?.value); + + if (!option.id || !model) { + console.debug('No model found for option', option, option?.element?.value); + console.debug('Models', models); + return option.text; + } + + const strip = html => { + const tmp = document.createElement('DIV'); + tmp.innerHTML = html || ''; + return tmp.textContent || tmp.innerText || ''; + }; + + // how much do we trust the metadata from the models repo? about this much + const displayName = strip(model.display_name || model.name).replace(/.*\//g, ''); + const description = strip(model.description); + const tags = model.tags ? model.tags.map(strip) : []; + const url = strip(model.url); + const style = strip(model.style); + + const workerInfo = hordeModelQueueStateString(model); + const isPopular = model.tags?.includes('popular'); + const descriptionDiv = description ? `
${description}
` : ''; + const tagSpans = tags.length > 0 && + `${tags.map(tag => `${tag}`).join('')}` || ''; + + const modelDetailsLink = url && ` `; + const capitalize = s => s ? s[0].toUpperCase() + s.slice(1) : ''; + const innerContent = [ + `${displayName} ${modelDetailsLink}`, + style ? `${capitalize(style)}` : '', + tagSpans ? `${tagSpans}` : '', + ].filter(Boolean).join(' | '); + + return $((` +
+
+ ${isPopular ? '' : ''} + ${innerContent} +
+ ${descriptionDiv} +
${workerInfo}
+
+ `)); +} + jQuery(function () { $('#horde_model').on('mousedown change', async function (e) { + console.log('Horde model change', e); horde_settings.models = $('#horde_model').val(); console.log('Updated Horde models', horde_settings.models); @@ -374,10 +438,7 @@ jQuery(function () { // Customize the pillbox text by shortening the full text return data.id; }, - templateResult: function (data) { - // Return the full text for the dropdown - return data.text; - }, + templateResult: getHordeModelTemplate, }); } }); diff --git a/src/endpoints/horde.js b/src/endpoints/horde.js index cc8b0bce8..4ca11a192 100644 --- a/src/endpoints/horde.js +++ b/src/endpoints/horde.js @@ -6,6 +6,7 @@ const { readSecret, SECRET_KEYS } = require('./secrets'); const { jsonParser } = require('../express-common'); const ANONYMOUS_KEY = '0000000000'; +const HORDE_TEXT_MODEL_METADATA_URL = 'https://raw.githubusercontent.com/db0/AI-Horde-text-model-reference/main/db.json'; const cache = new Cache(60 * 1000); const router = express.Router(); @@ -23,10 +24,9 @@ async function getClientAgent() { * @returns {Promise} AIHorde client */ async function getHordeClient() { - const ai_horde = new AIHorde({ + return new AIHorde({ client_agent: await getClientAgent(), }); - return ai_horde; } /** @@ -79,10 +79,24 @@ router.post('/text-workers', jsonParser, async (request, response) => { } }); +async function getHordeTextModelMetadata() { + const response = await fetch(HORDE_TEXT_MODEL_METADATA_URL); + return await response.json(); +} + +async function mergeModelsAndMetadata(models, metadata) { + return models.map(model => { + const metadataModel = metadata[model.name]; + if (!metadataModel) { + return { ...model, is_whitelisted: false }; + } + return { ...model, ...metadataModel, is_whitelisted: true }; + }); +} + router.post('/text-models', jsonParser, async (request, response) => { try { const cachedModels = cache.get('models'); - if (cachedModels && !request.body.force) { return response.send(cachedModels); } @@ -94,7 +108,17 @@ router.post('/text-models', jsonParser, async (request, response) => { }, }); - const data = await fetchResult.json(); + let data = await fetchResult.json(); + + // attempt to fetch and merge models metadata + try { + const metadata = await getHordeTextModelMetadata(); + data = await mergeModelsAndMetadata(data, metadata); + } + catch (error) { + console.error('Failed to fetch metadata:', error); + } + cache.set('models', data); return response.send(data); } catch (error) { @@ -310,6 +334,7 @@ router.post('/generate-image', jsonParser, async (request, response) => { console.log('Stable Horde request:', request.body); const ai_horde = await getHordeClient(); + // noinspection JSCheckFunctionSignatures -- see @ts-ignore - use_gfpgan const generation = await ai_horde.postAsyncImageGenerate( { prompt: `${request.body.prompt} ### ${request.body.negative_prompt}`, From a73b8077f6b6a1d9027d3c798c5936b1c3a347f4 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Tue, 17 Sep 2024 22:06:54 +0300 Subject: [PATCH 2/2] Display more of the description. Remove debug log --- public/scripts/horde.js | 3 +-- public/style.css | 8 ++++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/public/scripts/horde.js b/public/scripts/horde.js index 4b67dc8fc..1e0141fb0 100644 --- a/public/scripts/horde.js +++ b/public/scripts/horde.js @@ -281,7 +281,6 @@ async function getHordeModels(force) { return sortByWhitelisted(a, b) || sortByPopular(a, b) || sortByPerformance(a, b); }); for (const model of models) { - console.log('getHordeModels', model); const option = document.createElement('option'); option.value = model.name; option.innerText = hordeModelTextString(model); @@ -362,7 +361,7 @@ function getHordeModelTemplate(option) { const workerInfo = hordeModelQueueStateString(model); const isPopular = model.tags?.includes('popular'); - const descriptionDiv = description ? `
${description}
` : ''; + const descriptionDiv = description ? `
${description}
` : ''; const tagSpans = tags.length > 0 && `${tags.map(tag => `${tag}`).join('')}` || ''; diff --git a/public/style.css b/public/style.css index 21387cfd4..eb0595926 100644 --- a/public/style.css +++ b/public/style.css @@ -3518,6 +3518,14 @@ grammarly-extension { column-gap: 20px; } +.horde-model-description { + -webkit-line-clamp: 3; + line-clamp: 3; + font-size: 0.9em; + overflow: hidden; + text-overflow: ellipsis; +} + .drag-handle { cursor: grab; /* Make the drag handle not selectable in most browsers */