Add model metadata to Horde models endpoint Display model metadata in Horde model picker
| @@ -1,10 +1,10 @@ | ||
| 1 | 1 | import { |
| 2 | 2 | saveSettingsDebouncedamount_gen, |
| 3 | 3 | callPopup, |
| 4 | - setGenerationProgress, | |
| 5 | 4 | getRequestHeaders, |
| 6 | 5 | max_context, |
| 7 | 6 | amount_gensaveSettingsDebounced, |
| 7 | + setGenerationProgress, | |
| 8 | 8 | } from '../script.js'; |
| 9 | 9 | import { SECRET_KEYS, writeSecret } from './secrets.js'; |
| 10 | 10 | import { delay } from './utils.js'; |
| @@ -45,8 +45,7 @@ async function getWorkers(force) { | ||
| 45 | 45 | headers: getRequestHeaders(), |
| 46 | 46 | body: JSON.stringify({ force }), |
| 47 | 47 | }); |
| 48 | 48 | const data =return await response.json(); |
| 49 | - return data; | |
| 50 | 49 | } |
| 51 | 50 | |
| 52 | 51 | /** |
| @@ -61,16 +60,18 @@ async function getModels(force) { | ||
| 61 | 60 | body: JSON.stringify({ force }), |
| 62 | 61 | }); |
| 63 | 62 | const data = await response.json(); |
| 63 | + console.log('getModels', data); | |
| 64 | 64 | return data; |
| 65 | 65 | } |
| 66 | 66 | |
| 67 | + | |
| 67 | 68 | /** |
| 68 | 69 | * Gets the status of a Horde task. |
| 69 | 70 | * @param {string} taskId Task ID |
| 70 | 71 | * @returns {Promise<Object>} Task status |
| 71 | 72 | */ |
| 72 | 73 | async function getTaskStatus(taskId) { |
| 73 | 74 | const response = await fetch('/api/horde/task-status', { |
| 74 | 75 | method: 'POST', |
| 75 | 76 | headers: getRequestHeaders(), |
| 76 | 77 | body: JSON.stringify({ taskId }), |
| @@ -80,8 +81,7 @@ async function getTaskStatus(taskId) { | ||
| 80 | 81 | throw new Error(`Failed to get task status: ${response.statusText}`); |
| 81 | 82 | } |
| 82 | 83 | |
| 83 | 84 | const data =return await response.json(); |
| 84 | - return data; | |
| 85 | 85 | } |
| 86 | 86 | |
| 87 | 87 | /** |
| @@ -148,7 +148,7 @@ async function adjustHordeGenerationParams(max_context_length, max_length) { | ||
| 148 | 148 | |
| 149 | 149 | for (const model of selectedModels) { |
| 150 | 150 | for (const worker of workers) { |
| 151 | 151 | if (model.cluster === worker.cluster && worker.models.includes(model.name)) { |
| 152 | 152 | // Skip workers that are not trusted if the option is enabled |
| 153 | 153 | if (horde_settings.trusted_workers_only && !worker.trusted) { |
| 154 | 154 | continue; |
| @@ -250,12 +250,10 @@ async function generateHorde(prompt, params, signal, reportProgress) { | ||
| 250 | 250 | console.log(generatedText); |
| 251 | 251 | console.log(`Generated by Horde Worker: ${WorkerName} [${WorkerModel}]`); |
| 252 | 252 | return { text: generatedText, workerName: `Generated by Horde worker: ${WorkerName} [${WorkerModel}]` }; |
| 253 | - } | |
| 253 | + } else if (!queue_position_first) { | |
| 254 | - else if (!queue_position_first) { | |
| 255 | 254 | queue_position_first = statusCheckJson.queue_position; |
| 256 | 255 | reportProgress && setGenerationProgress(0); |
| 257 | - } | |
| 256 | + } else if (statusCheckJson.queue_position >= 0) { | |
| 258 | - else if (statusCheckJson.queue_position >= 0) { | |
| 259 | 257 | let queue_position = statusCheckJson.queue_position; |
| 260 | 258 | const progress = Math.round(100 - (queue_position / queue_position_first * 100)); |
| 261 | 259 | reportProgress && setGenerationProgress(progress); |
| @@ -268,17 +266,25 @@ async function generateHorde(prompt, params, signal, reportProgress) { | ||
| 268 | 266 | throw new Error('Horde timeout'); |
| 269 | 267 | } |
| 270 | 268 | |
| 269 | + | |
| 271 | 270 | /** |
| 272 | 271 | * Displays the available models in the Horde model selection dropdown. |
| 273 | 272 | * @param {boolean} force Force refresh of the models |
| 274 | 273 | */ |
| 275 | 274 | async function getHordeModels(force) { |
| 275 | + const sortByPerformance = (a, b) => b.performance - a.performance; | |
| 276 | + const sortByWhitelisted = (a, b) => b.is_whitelisted - a.is_whitelisted; | |
| 277 | + const sortByPopular = (a, b) => b.tags?.includes('popular') - a.tags?.includes('popular'); | |
| 278 | + | |
| 276 | 279 | $('#horde_model').empty(); |
| 277 | 280 | models = (await getModels(force)).sort((a, b) => b.performance - a.performance);{ |
| 281 | + return sortByWhitelisted(a, b) || sortByPopular(a, b) || sortByPerformance(a, b); | |
| 282 | + }); | |
| 278 | 283 | for (const model of models) { |
| 284 | + console.log('getHordeModels', model); | |
| 279 | 285 | const option = document.createElement('option'); |
| 280 | 286 | option.value = model.name; |
| 281 | - option.innerText = `${model.name} (ETA: ${model.eta}s, Speed: ${model.performance}, Queue: ${model.queued}, Workers: ${model.count})`; | |
| 287 | + option.innerText = hordeModelTextString(model); | |
| 282 | 288 | option.selected = horde_settings.models.includes(model.name); |
| 283 | 289 | $('#horde_model').append(option); |
| 284 | 290 | } |
| @@ -323,8 +329,66 @@ async function showKudos() { | ||
| 323 | 329 | toastr.info(`Kudos: ${data.kudos}`, data.username); |
| 324 | 330 | } |
| 325 | 331 | |
| 332 | +function hordeModelTextString(model) { | |
| 333 | + const q = hordeModelQueueStateString(model); | |
| 334 | + return `${model.name} (${q})`; | |
| 335 | +} | |
| 336 | + | |
| 337 | +function hordeModelQueueStateString(model) { | |
| 338 | + return `ETA: ${model.eta}s, Speed: ${model.performance}, Queue: ${model.queued}, Workers: ${model.count}`; | |
| 339 | +} | |
| 340 | + | |
| 341 | +function getHordeModelTemplate(option) { | |
| 342 | + const model = models.find(x => x.name === option?.element?.value); | |
| 343 | + | |
| 344 | + if (!option.id || !model) { | |
| 345 | + console.debug('No model found for option', option, option?.element?.value); | |
| 346 | + console.debug('Models', models); | |
| 347 | + return option.text; | |
| 348 | + } | |
| 349 | + | |
| 350 | + const strip = html => { | |
| 351 | + const tmp = document.createElement('DIV'); | |
| 352 | + tmp.innerHTML = html || ''; | |
| 353 | + return tmp.textContent || tmp.innerText || ''; | |
| 354 | + }; | |
| 355 | + | |
| 356 | + // how much do we trust the metadata from the models repo? about this much | |
| 357 | + const displayName = strip(model.display_name || model.name).replace(/.*\//g, ''); | |
| 358 | + const description = strip(model.description); | |
| 359 | + const tags = model.tags ? model.tags.map(strip) : []; | |
| 360 | + const url = strip(model.url); | |
| 361 | + const style = strip(model.style); | |
| 362 | + | |
| 363 | + const workerInfo = hordeModelQueueStateString(model); | |
| 364 | + const isPopular = model.tags?.includes('popular'); | |
| 365 | + const descriptionDiv = description ? `<div style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${description}</div>` : ''; | |
| 366 | + const tagSpans = tags.length > 0 && | |
| 367 | + `${tags.map(tag => `<span class="tag tag_name">${tag}</span>`).join('')}</span>` || ''; | |
| 368 | + | |
| 369 | + const modelDetailsLink = url && `<a href="${url}" target="_blank" rel="noopener noreferrer" class="model-details-link fa-solid fa-circle-question"> </a>`; | |
| 370 | + const capitalize = s => s ? s[0].toUpperCase() + s.slice(1) : ''; | |
| 371 | + const innerContent = [ | |
| 372 | + `<strong>${displayName}</strong> ${modelDetailsLink}`, | |
| 373 | + style ? `${capitalize(style)}` : '', | |
| 374 | + tagSpans ? `<span class="tags tags_inline inline-flex margin-r2">${tagSpans}</span>` : '', | |
| 375 | + ].filter(Boolean).join(' | '); | |
| 376 | + | |
| 377 | + return $((` | |
| 378 | + <div class="flex-container flexFlowColumn"> | |
| 379 | + <div> | |
| 380 | + ${isPopular ? '<span class="fa-fw fa-solid fa-star" title="Popular"></span>' : ''} | |
| 381 | + ${innerContent} | |
| 382 | + </div> | |
| 383 | + ${descriptionDiv} | |
| 384 | + <div><small>${workerInfo}</small></div> | |
| 385 | + </div> | |
| 386 | + `)); | |
| 387 | +} | |
| 388 | + | |
| 326 | 389 | jQuery(function () { |
| 327 | 390 | $('#horde_model').on('mousedown change', async function (e) { |
| 391 | + console.log('Horde model change', e); | |
| 328 | 392 | horde_settings.models = $('#horde_model').val(); |
| 329 | 393 | console.log('Updated Horde models', horde_settings.models); |
| 330 | 394 | |
| @@ -374,10 +438,7 @@ jQuery(function () { | ||
| 374 | 438 | // Customize the pillbox text by shortening the full text |
| 375 | 439 | return data.id; |
| 376 | 440 | }, |
| 377 | - templateResult: function (data) { | |
| 441 | + templateResult: getHordeModelTemplate, | |
| 378 | - // Return the full text for the dropdown | |
| 379 | - return data.text; | |
| 380 | - }, | |
| 381 | 442 | }); |
| 382 | 443 | } |
| 383 | 444 | }); |
| @@ -6,6 +6,7 @@ const { readSecret, SECRET_KEYS } = require('./secrets'); | ||
| 6 | 6 | const { jsonParser } = require('../express-common'); |
| 7 | 7 | |
| 8 | 8 | const ANONYMOUS_KEY = '0000000000'; |
| 9 | +const HORDE_TEXT_MODEL_METADATA_URL = 'https://raw.githubusercontent.com/db0/AI-Horde-text-model-reference/main/db.json'; | |
| 9 | 10 | const cache = new Cache(60 * 1000); |
| 10 | 11 | const router = express.Router(); |
| 11 | 12 | |
| @@ -23,10 +24,9 @@ async function getClientAgent() { | ||
| 23 | 24 | * @returns {Promise<AIHorde>} AIHorde client |
| 24 | 25 | */ |
| 25 | 26 | async function getHordeClient() { |
| 26 | 27 | const ai_horde =return new AIHorde({ |
| 27 | 28 | client_agent: await getClientAgent(), |
| 28 | 29 | }); |
| 29 | - return ai_horde; | |
| 30 | 30 | } |
| 31 | 31 | |
| 32 | 32 | /** |
| @@ -79,10 +79,24 @@ router.post('/text-workers', jsonParser, async (request, response) => { | ||
| 79 | 79 | } |
| 80 | 80 | }); |
| 81 | 81 | |
| 82 | +async function getHordeTextModelMetadata() { | |
| 83 | + const response = await fetch(HORDE_TEXT_MODEL_METADATA_URL); | |
| 84 | + return await response.json(); | |
| 85 | +} | |
| 86 | + | |
| 87 | +async function mergeModelsAndMetadata(models, metadata) { | |
| 88 | + return models.map(model => { | |
| 89 | + const metadataModel = metadata[model.name]; | |
| 90 | + if (!metadataModel) { | |
| 91 | + return { ...model, is_whitelisted: false }; | |
| 92 | + } | |
| 93 | + return { ...model, ...metadataModel, is_whitelisted: true }; | |
| 94 | + }); | |
| 95 | +} | |
| 96 | + | |
| 82 | 97 | router.post('/text-models', jsonParser, async (request, response) => { |
| 83 | 98 | try { |
| 84 | 99 | const cachedModels = cache.get('models'); |
| 85 | - | |
| 86 | 100 | if (cachedModels && !request.body.force) { |
| 87 | 101 | return response.send(cachedModels); |
| 88 | 102 | } |
| @@ -94,7 +108,17 @@ router.post('/text-models', jsonParser, async (request, response) => { | ||
| 94 | 108 | }, |
| 95 | 109 | }); |
| 96 | 110 | |
| 97 | 111 | constlet data = await fetchResult.json(); |
| 112 | + | |
| 113 | + // attempt to fetch and merge models metadata | |
| 114 | + try { | |
| 115 | + const metadata = await getHordeTextModelMetadata(); | |
| 116 | + data = await mergeModelsAndMetadata(data, metadata); | |
| 117 | + } | |
| 118 | + catch (error) { | |
| 119 | + console.error('Failed to fetch metadata:', error); | |
| 120 | + } | |
| 121 | + | |
| 98 | 122 | cache.set('models', data); |
| 99 | 123 | return response.send(data); |
| 100 | 124 | } catch (error) { |
| @@ -310,6 +334,7 @@ router.post('/generate-image', jsonParser, async (request, response) => { | ||
| 310 | 334 | console.log('Stable Horde request:', request.body); |
| 311 | 335 | |
| 312 | 336 | const ai_horde = await getHordeClient(); |
| 337 | + // noinspection JSCheckFunctionSignatures -- see @ts-ignore - use_gfpgan | |
| 313 | 338 | const generation = await ai_horde.postAsyncImageGenerate( |
| 314 | 339 | { |
| 315 | 340 | prompt: `${request.body.prompt} ### ${request.body.negative_prompt}`, |