Add model metadata to Horde models endpoint Display model metadata in Horde model picker
| @@ -1,10 +1,10 @@ | |||
| 1 | import { | 1 | import { |
| 2 | saveSettingsDebounced, | 2 | amount_gen, |
| 3 | callPopup, | 3 | callPopup, |
| 4 | setGenerationProgress, | ||
| 5 | getRequestHeaders, | 4 | getRequestHeaders, |
| 6 | max_context, | 5 | max_context, |
| 7 | amount_gen, | 6 | saveSettingsDebounced, |
| 7 | setGenerationProgress, | ||
| 8 | } from '../script.js'; | 8 | } from '../script.js'; |
| 9 | import { SECRET_KEYS, writeSecret } from './secrets.js'; | 9 | import { SECRET_KEYS, writeSecret } from './secrets.js'; |
| 10 | import { delay } from './utils.js'; | 10 | import { delay } from './utils.js'; |
| @@ -45,8 +45,7 @@ async function getWorkers(force) { | |||
| 45 | headers: getRequestHeaders(), | 45 | headers: getRequestHeaders(), |
| 46 | body: JSON.stringify({ force }), | 46 | body: JSON.stringify({ force }), |
| 47 | }); | 47 | }); |
| 48 | const data = await response.json(); | 48 | return await response.json(); |
| 49 | return data; | ||
| 50 | } | 49 | } |
| 51 | 50 | ||
| 52 | /** | 51 | /** |
| @@ -61,9 +60,11 @@ async function getModels(force) { | |||
| 61 | body: JSON.stringify({ force }), | 60 | body: JSON.stringify({ force }), |
| 62 | }); | 61 | }); |
| 63 | const data = await response.json(); | 62 | const data = await response.json(); |
| 63 | console.log('getModels', data); | ||
| 64 | return data; | 64 | return data; |
| 65 | } | 65 | } |
| 66 | 66 | ||
| 67 | |||
| 67 | /** | 68 | /** |
| 68 | * Gets the status of a Horde task. | 69 | * Gets the status of a Horde task. |
| 69 | * @param {string} taskId Task ID | 70 | * @param {string} taskId Task ID |
| @@ -80,8 +81,7 @@ async function getTaskStatus(taskId) { | |||
| 80 | throw new Error(`Failed to get task status: ${response.statusText}`); | 81 | throw new Error(`Failed to get task status: ${response.statusText}`); |
| 81 | } | 82 | } |
| 82 | 83 | ||
| 83 | const data = await response.json(); | 84 | 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 | for (const model of selectedModels) { | 149 | for (const model of selectedModels) { |
| 150 | for (const worker of workers) { | 150 | for (const worker of workers) { |
| 151 | if (model.cluster == worker.cluster && worker.models.includes(model.name)) { | 151 | if (model.cluster === worker.cluster && worker.models.includes(model.name)) { |
| 152 | // Skip workers that are not trusted if the option is enabled | 152 | // Skip workers that are not trusted if the option is enabled |
| 153 | if (horde_settings.trusted_workers_only && !worker.trusted) { | 153 | if (horde_settings.trusted_workers_only && !worker.trusted) { |
| 154 | continue; | 154 | continue; |
| @@ -250,12 +250,10 @@ async function generateHorde(prompt, params, signal, reportProgress) { | |||
| 250 | console.log(generatedText); | 250 | console.log(generatedText); |
| 251 | console.log(`Generated by Horde Worker: ${WorkerName} [${WorkerModel}]`); | 251 | console.log(`Generated by Horde Worker: ${WorkerName} [${WorkerModel}]`); |
| 252 | return { text: generatedText, workerName: `Generated by Horde worker: ${WorkerName} [${WorkerModel}]` }; | 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 | queue_position_first = statusCheckJson.queue_position; | 254 | queue_position_first = statusCheckJson.queue_position; |
| 256 | reportProgress && setGenerationProgress(0); | 255 | reportProgress && setGenerationProgress(0); |
| 257 | } | 256 | } else if (statusCheckJson.queue_position >= 0) { |
| 258 | else if (statusCheckJson.queue_position >= 0) { | ||
| 259 | let queue_position = statusCheckJson.queue_position; | 257 | let queue_position = statusCheckJson.queue_position; |
| 260 | const progress = Math.round(100 - (queue_position / queue_position_first * 100)); | 258 | const progress = Math.round(100 - (queue_position / queue_position_first * 100)); |
| 261 | reportProgress && setGenerationProgress(progress); | 259 | reportProgress && setGenerationProgress(progress); |
| @@ -268,17 +266,25 @@ async function generateHorde(prompt, params, signal, reportProgress) { | |||
| 268 | throw new Error('Horde timeout'); | 266 | throw new Error('Horde timeout'); |
| 269 | } | 267 | } |
| 270 | 268 | ||
| 269 | |||
| 271 | /** | 270 | /** |
| 272 | * Displays the available models in the Horde model selection dropdown. | 271 | * Displays the available models in the Horde model selection dropdown. |
| 273 | * @param {boolean} force Force refresh of the models | 272 | * @param {boolean} force Force refresh of the models |
| 274 | */ | 273 | */ |
| 275 | async function getHordeModels(force) { | 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 | $('#horde_model').empty(); | 279 | $('#horde_model').empty(); |
| 277 | models = (await getModels(force)).sort((a, b) => b.performance - a.performance); | 280 | models = (await getModels(force)).sort((a, b) => { |
| 281 | return sortByWhitelisted(a, b) || sortByPopular(a, b) || sortByPerformance(a, b); | ||
| 282 | }); | ||
| 278 | for (const model of models) { | 283 | for (const model of models) { |
| 284 | console.log('getHordeModels', model); | ||
| 279 | const option = document.createElement('option'); | 285 | const option = document.createElement('option'); |
| 280 | option.value = model.name; | 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 | option.selected = horde_settings.models.includes(model.name); | 288 | option.selected = horde_settings.models.includes(model.name); |
| 283 | $('#horde_model').append(option); | 289 | $('#horde_model').append(option); |
| 284 | } | 290 | } |
| @@ -323,8 +329,66 @@ async function showKudos() { | |||
| 323 | toastr.info(`Kudos: ${data.kudos}`, data.username); | 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 | jQuery(function () { | 389 | jQuery(function () { |
| 327 | $('#horde_model').on('mousedown change', async function (e) { | 390 | $('#horde_model').on('mousedown change', async function (e) { |
| 391 | console.log('Horde model change', e); | ||
| 328 | horde_settings.models = $('#horde_model').val(); | 392 | horde_settings.models = $('#horde_model').val(); |
| 329 | console.log('Updated Horde models', horde_settings.models); | 393 | console.log('Updated Horde models', horde_settings.models); |
| 330 | 394 | ||
| @@ -374,10 +438,7 @@ jQuery(function () { | |||
| 374 | // Customize the pillbox text by shortening the full text | 438 | // Customize the pillbox text by shortening the full text |
| 375 | return data.id; | 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 | const { jsonParser } = require('../express-common'); | 6 | const { jsonParser } = require('../express-common'); |
| 7 | 7 | ||
| 8 | const ANONYMOUS_KEY = '0000000000'; | 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 | const cache = new Cache(60 * 1000); | 10 | const cache = new Cache(60 * 1000); |
| 10 | const router = express.Router(); | 11 | const router = express.Router(); |
| 11 | 12 | ||
| @@ -23,10 +24,9 @@ async function getClientAgent() { | |||
| 23 | * @returns {Promise<AIHorde>} AIHorde client | 24 | * @returns {Promise<AIHorde>} AIHorde client |
| 24 | */ | 25 | */ |
| 25 | async function getHordeClient() { | 26 | async function getHordeClient() { |
| 26 | const ai_horde = new AIHorde({ | 27 | return new AIHorde({ |
| 27 | client_agent: await getClientAgent(), | 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 | router.post('/text-models', jsonParser, async (request, response) => { | 97 | router.post('/text-models', jsonParser, async (request, response) => { |
| 83 | try { | 98 | try { |
| 84 | const cachedModels = cache.get('models'); | 99 | const cachedModels = cache.get('models'); |
| 85 | |||
| 86 | if (cachedModels && !request.body.force) { | 100 | if (cachedModels && !request.body.force) { |
| 87 | return response.send(cachedModels); | 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 | const data = await fetchResult.json(); | 111 | let 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 | cache.set('models', data); | 122 | cache.set('models', data); |
| 99 | return response.send(data); | 123 | return response.send(data); |
| 100 | } catch (error) { | 124 | } catch (error) { |
| @@ -310,6 +334,7 @@ router.post('/generate-image', jsonParser, async (request, response) => { | |||
| 310 | console.log('Stable Horde request:', request.body); | 334 | console.log('Stable Horde request:', request.body); |
| 311 | 335 | ||
| 312 | const ai_horde = await getHordeClient(); | 336 | const ai_horde = await getHordeClient(); |
| 337 | // noinspection JSCheckFunctionSignatures -- see @ts-ignore - use_gfpgan | ||
| 313 | const generation = await ai_horde.postAsyncImageGenerate( | 338 | const generation = await ai_horde.postAsyncImageGenerate( |
| 314 | { | 339 | { |
| 315 | prompt: `${request.body.prompt} ### ${request.body.negative_prompt}`, | 340 | prompt: `${request.body.prompt} ### ${request.body.negative_prompt}`, |