Merge branch 'staging' of github.com-RA:sillytavern/sillytavern into staging
| @@ -8,6 +8,9 @@ | |||
| 8 | "openrouter_sort_models": "alphabetically", | 8 | "openrouter_sort_models": "alphabetically", |
| 9 | "ai21_model": "jamba-large", | 9 | "ai21_model": "jamba-large", |
| 10 | "mistralai_model": "mistral-large-latest", | 10 | "mistralai_model": "mistral-large-latest", |
| 11 | "electronhub_model": "gpt-4o-mini", | ||
| 12 | "electronhub_sort_models": "alphabetically", | ||
| 13 | "electronhub_group_models": false, | ||
| 11 | "custom_model": "", | 14 | "custom_model": "", |
| 12 | "custom_url": "", | 15 | "custom_url": "", |
| 13 | "custom_include_body": "", | 16 | "custom_include_body": "", |
| @@ -88,6 +88,24 @@ body.reduced-motion #bg_custom { | |||
| 88 | gap: 5px; | 88 | gap: 5px; |
| 89 | } | 89 | } |
| 90 | 90 | ||
| 91 | /* Control buttons in header */ | ||
| 92 | .heading-container-with-controls { | ||
| 93 | position: relative; | ||
| 94 | } | ||
| 95 | |||
| 96 | .heading-container-with-controls .heading-text { | ||
| 97 | margin: 10px 0; | ||
| 98 | } | ||
| 99 | |||
| 100 | .heading-container-with-controls .heading-controls { | ||
| 101 | position: absolute; | ||
| 102 | right: 5px; | ||
| 103 | top: 50%; | ||
| 104 | transform: translateY(-50%); | ||
| 105 | display: flex; | ||
| 106 | gap: 5px; | ||
| 107 | } | ||
| 108 | |||
| 91 | #bg-scrollable-content { | 109 | #bg-scrollable-content { |
| 92 | flex-grow: 1; | 110 | flex-grow: 1; |
| 93 | overflow-y: auto; | 111 | overflow-y: auto; |
| @@ -95,46 +113,43 @@ body.reduced-motion #bg_custom { | |||
| 95 | padding: 0 5px 15px; | 113 | padding: 0 5px 15px; |
| 96 | } | 114 | } |
| 97 | 115 | ||
| 116 | #bg_menu_content, | ||
| 117 | #bg_custom_content { | ||
| 118 | display: grid; | ||
| 119 | gap: 5px; | ||
| 120 | width: 100%; | ||
| 121 | grid-template-columns: repeat(var(--bg-thumb-columns, 5), 1fr); | ||
| 122 | } | ||
| 123 | |||
| 98 | #bg-filter { | 124 | #bg-filter { |
| 99 | font-size: calc(var(--mainFontSize) * 0.95); | 125 | font-size: calc(var(--mainFontSize) * 0.95); |
| 100 | } | 126 | } |
| 101 | 127 | ||
| 102 | /* Thumbnail Menu & Buttons */ | 128 | /* Thumbnails */ |
| 129 | .bg_example:hover .BGSampleTitle { | ||
| 130 | opacity: 1; | ||
| 131 | } | ||
| 132 | |||
| 103 | .bg_example .mobile-only-menu-toggle { | 133 | .bg_example .mobile-only-menu-toggle { |
| 104 | display: none; | 134 | display: none; |
| 105 | } | 135 | } |
| 106 | 136 | ||
| 107 | .bg_example.flex-container { | 137 | .bg_example { |
| 108 | width: 30%; | ||
| 109 | max-width: 200px; | ||
| 110 | margin: 5px; | ||
| 111 | aspect-ratio: 16/9; | ||
| 112 | cursor: pointer; | 138 | cursor: pointer; |
| 113 | box-shadow: 0 0 7px var(--black50a); | 139 | box-shadow: 0 0 7px var(--black50a); |
| 114 | |||
| 115 | position: relative; | 140 | position: relative; |
| 116 | overflow: hidden; | 141 | overflow: hidden; |
| 117 | border-radius: 8px; | 142 | border-radius: 8px; |
| 118 | border: 0px solid transparent; | 143 | border: 0px solid transparent; |
| 119 | outline: 2px solid var(--SmartThemeBorderColor); | 144 | outline: 2px solid var(--SmartThemeBorderColor); |
| 120 | outline-offset: -1px; | 145 | outline-offset: -1px; |
| 121 | } | ||
| 122 | 146 | ||
| 123 | .bg_example.flex-container:focus-visible { | 147 | height: auto; |
| 124 | outline-offset: inherit; | 148 | aspect-ratio: 16 / 9; |
| 125 | } | 149 | } |
| 126 | 150 | ||
| 127 | .bg_example_img { | 151 | .bg_example:focus-visible { |
| 128 | position: absolute; | 152 | outline-offset: inherit; |
| 129 | top: -2px; | ||
| 130 | left: -2px; | ||
| 131 | right: -2px; | ||
| 132 | bottom: -2px; | ||
| 133 | |||
| 134 | background-image: inherit; | ||
| 135 | |||
| 136 | background-size: cover; | ||
| 137 | background-position: center; | ||
| 138 | } | 153 | } |
| 139 | 154 | ||
| 140 | .bg_example .jg-menu { | 155 | .bg_example .jg-menu { |
| @@ -143,9 +158,8 @@ body.reduced-motion #bg_custom { | |||
| 143 | top: 2px; | 158 | top: 2px; |
| 144 | right: 2px; | 159 | right: 2px; |
| 145 | background-color: rgba(0, 0, 0, 0.5); | 160 | background-color: rgba(0, 0, 0, 0.5); |
| 146 | border-radius: 8px; | 161 | border-radius: 5px; |
| 147 | gap: 3px; | 162 | padding: 3px 3px; |
| 148 | padding: 3px 5px; | ||
| 149 | z-index: 3; | 163 | z-index: 3; |
| 150 | backdrop-filter: blur(4px); | 164 | backdrop-filter: blur(4px); |
| 151 | border: 1px solid var(--SmartThemeBorderColor); | 165 | border: 1px solid var(--SmartThemeBorderColor); |
| @@ -168,14 +182,14 @@ body.reduced-motion #bg_custom { | |||
| 168 | 182 | ||
| 169 | .bg_example .jg-button { | 183 | .bg_example .jg-button { |
| 170 | display: flex; | 184 | display: flex; |
| 171 | width: 30px; | 185 | width: 24px; |
| 172 | height: 30px; | 186 | height: 24px; |
| 173 | align-items: center; | 187 | align-items: center; |
| 174 | justify-content: center; | 188 | justify-content: center; |
| 175 | color: white; | 189 | color: white; |
| 176 | padding: 5px; | 190 | padding: 5px; |
| 177 | font-size: 1.1em; | 191 | font-size: 1.1em; |
| 178 | border-radius: 6px; | 192 | border-radius: 5px; |
| 179 | transition: background-color var(--animation-duration) ease; | 193 | transition: background-color var(--animation-duration) ease; |
| 180 | } | 194 | } |
| 181 | 195 | ||
| @@ -195,6 +209,18 @@ body.reduced-motion #bg_custom { | |||
| 195 | display: flex; | 209 | display: flex; |
| 196 | } | 210 | } |
| 197 | 211 | ||
| 212 | .thumbnail-clipper { | ||
| 213 | position: absolute; | ||
| 214 | top: -2px; | ||
| 215 | left: -2px; | ||
| 216 | right: -2px; | ||
| 217 | bottom: -2px; | ||
| 218 | overflow: hidden; | ||
| 219 | border-radius: inherit; | ||
| 220 | background-size: cover; | ||
| 221 | background-position: center; | ||
| 222 | } | ||
| 223 | |||
| 198 | .bg_example:not([custom="true"]) .jg-copy, | 224 | .bg_example:not([custom="true"]) .jg-copy, |
| 199 | .bg_example[custom="true"] .jg-edit { | 225 | .bg_example[custom="true"] .jg-edit { |
| 200 | display: none; | 226 | display: none; |
| @@ -30,6 +30,14 @@ | |||
| 30 | display: none; | 30 | display: none; |
| 31 | } | 31 | } |
| 32 | 32 | ||
| 33 | #bg_menu_content, #bg_custom_content { | ||
| 34 | grid-template-columns: repeat(var(--bg-thumb-columns, 3), 1fr); | ||
| 35 | } | ||
| 36 | |||
| 37 | .bg_list { | ||
| 38 | width: unset; | ||
| 39 | } | ||
| 40 | |||
| 33 | .bg_button { | 41 | .bg_button { |
| 34 | font-size: 15px; | 42 | font-size: 15px; |
| 35 | } | 43 | } |
| @@ -49,6 +57,11 @@ | |||
| 49 | z-index: 4; | 57 | z-index: 4; |
| 50 | } | 58 | } |
| 51 | 59 | ||
| 60 | .bg_example.mobile-menu-open .mobile-only-menu-toggle { | ||
| 61 | opacity: 0; | ||
| 62 | pointer-events: none; | ||
| 63 | } | ||
| 64 | |||
| 52 | .bg_example .mobile-only-menu-toggle { | 65 | .bg_example .mobile-only-menu-toggle { |
| 53 | display: flex; | 66 | display: flex; |
| 54 | align-items: center; | 67 | align-items: center; |
| @@ -66,6 +79,11 @@ | |||
| 66 | backdrop-filter: blur(2px); | 79 | backdrop-filter: blur(2px); |
| 67 | } | 80 | } |
| 68 | 81 | ||
| 82 | .bg_example .jg-button { | ||
| 83 | width: 30px; | ||
| 84 | height: 30px; | ||
| 85 | } | ||
| 86 | |||
| 69 | #bg-header-controls { | 87 | #bg-header-controls { |
| 70 | flex-wrap: wrap; | 88 | flex-wrap: wrap; |
| 71 | row-gap: 10px; | 89 | row-gap: 10px; |
| @@ -438,10 +456,6 @@ | |||
| 438 | .horde_multiple_hint { | 456 | .horde_multiple_hint { |
| 439 | display: none; | 457 | display: none; |
| 440 | } | 458 | } |
| 441 | |||
| 442 | .bg_list { | ||
| 443 | width: unset; | ||
| 444 | } | ||
| 445 | } | 459 | } |
| 446 | 460 | ||
| 447 | /*landscape mode phones and ipads*/ | 461 | /*landscape mode phones and ipads*/ |
| @@ -676,6 +676,9 @@ | |||
| 676 | <div data-source="openrouter"> | 676 | <div data-source="openrouter"> |
| 677 | <span data-i18n="Max prompt cost:">Max prompt cost:</span> <span id="openrouter_max_prompt_cost" data-i18n="Unknown">Unknown</span> | 677 | <span data-i18n="Max prompt cost:">Max prompt cost:</span> <span id="openrouter_max_prompt_cost" data-i18n="Unknown">Unknown</span> |
| 678 | </div> | 678 | </div> |
| 679 | <div data-source="electronhub"> | ||
| 680 | <span data-i18n="Max prompt cost:">Max prompt cost:</span> <span id="electronhub_max_prompt_cost" data-i18n="Unknown">Unknown</span> | ||
| 681 | </div> | ||
| 679 | <hr> | 682 | <hr> |
| 680 | <div class="range-block"> | 683 | <div class="range-block"> |
| 681 | <label for="stream_toggle" title="Enable OpenAI completion streaming" data-i18n="[title]Enable OpenAI completion streaming" class="checkbox_label widthFreeExpand"> | 684 | <label for="stream_toggle" title="Enable OpenAI completion streaming" data-i18n="[title]Enable OpenAI completion streaming" class="checkbox_label widthFreeExpand"> |
| @@ -2123,7 +2126,7 @@ | |||
| 2123 | </div> | 2126 | </div> |
| 2124 | </div> | 2127 | </div> |
| 2125 | </div> | 2128 | </div> |
| 2126 | <div class="range-block m-t-1" data-source="openai,aimlapi,openrouter,custom,azure_openai"> | 2129 | <div class="range-block m-t-1" data-source="openai,aimlapi,openrouter,custom,electronhub,azure_openai"> |
| 2127 | <div id="logit_bias_openai" class="range-block-title openai_restorable" data-i18n="Logit Bias"> | 2130 | <div id="logit_bias_openai" class="range-block-title openai_restorable" data-i18n="Logit Bias"> |
| 2128 | Logit Bias | 2131 | Logit Bias |
| 2129 | </div> | 2132 | </div> |
| @@ -3061,7 +3064,7 @@ | |||
| 3061 | <div class="marginTopBot5"> | 3064 | <div class="marginTopBot5"> |
| 3062 | <div class="inline-drawer wide100p"> | 3065 | <div class="inline-drawer wide100p"> |
| 3063 | <div class="inline-drawer-toggle inline-drawer-header"> | 3066 | <div class="inline-drawer-toggle inline-drawer-header"> |
| 3064 | <b data-i18n="Model Order">OpenRouter Model Sorting</b> | 3067 | <b data-i18n="OpenRouter Model Sorting">OpenRouter Model Sorting</b> |
| 3065 | <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div> | 3068 | <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div> |
| 3066 | </div> | 3069 | </div> |
| 3067 | <div class="inline-drawer-content m-b-1"> | 3070 | <div class="inline-drawer-content m-b-1"> |
| @@ -3452,6 +3455,9 @@ | |||
| 3452 | </div> | 3455 | </div> |
| 3453 | <div id="electronhub_form" data-source="electronhub"> | 3456 | <div id="electronhub_form" data-source="electronhub"> |
| 3454 | <h4 data-i18n="Electron Hub API Key">Electron Hub API Key</h4> | 3457 | <h4 data-i18n="Electron Hub API Key">Electron Hub API Key</h4> |
| 3458 | <div> | ||
| 3459 | <a href="https://playground.electronhub.ai/console" target="_blank" data-i18n="View Remaining Credits">View Remaining Credits</a> | ||
| 3460 | </div> | ||
| 3455 | <div class="flex-container"> | 3461 | <div class="flex-container"> |
| 3456 | <input id="api_key_electronhub" name="api_key_electronhub" class="text_pole flex1" value="" type="text" autocomplete="off"> | 3462 | <input id="api_key_electronhub" name="api_key_electronhub" class="text_pole flex1" value="" type="text" autocomplete="off"> |
| 3457 | <div title="Manage API keys" data-i18n="[title]Manage API keys" class="menu_button fa-solid fa-key fa-fw manage-api-keys" data-key="api_key_electronhub"></div> | 3463 | <div title="Manage API keys" data-i18n="[title]Manage API keys" class="menu_button fa-solid fa-key fa-fw manage-api-keys" data-key="api_key_electronhub"></div> |
| @@ -3459,10 +3465,43 @@ | |||
| 3459 | <div data-for="api_key_electronhub" class="neutral_warning" data-i18n="For privacy reasons, your API key will be hidden after you click 'Connect'."> | 3465 | <div data-for="api_key_electronhub" class="neutral_warning" data-i18n="For privacy reasons, your API key will be hidden after you click 'Connect'."> |
| 3460 | For privacy reasons, your API key will be hidden after you click 'Connect'. | 3466 | For privacy reasons, your API key will be hidden after you click 'Connect'. |
| 3461 | </div> | 3467 | </div> |
| 3462 | <h4 data-i18n="Electron Hub Model">Electron Hub Model</h4> | 3468 | <div> |
| 3463 | <select id="model_electronhub_select"> | 3469 | <h4 data-i18n="Electron Hub Model">Electron Hub Model</h4> |
| 3464 | <option value="" data-i18n="-- Connect to the API --">-- Connect to the API --</option> | 3470 | <select id="model_electronhub_select"> |
| 3465 | </select> | 3471 | <option value="" data-i18n="-- Connect to the API --">-- Connect to the API --</option> |
| 3472 | </select> | ||
| 3473 | </div> | ||
| 3474 | <div class="marginTopBot5"> | ||
| 3475 | <div class="inline-drawer wide100p"> | ||
| 3476 | <div class="inline-drawer-toggle inline-drawer-header"> | ||
| 3477 | <b data-i18n="Electron Hub Model Sorting">Electron Hub Model Sorting</b> | ||
| 3478 | <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div> | ||
| 3479 | </div> | ||
| 3480 | <div class="inline-drawer-content m-b-1"> | ||
| 3481 | <div class="marginTopBot5"> | ||
| 3482 | <label for="electronhub_sort_models" class="checkbox_label"> | ||
| 3483 | <select id="electronhub_sort_models"> | ||
| 3484 | <option data-i18n="Alphabetically" value="alphabetically">Alphabetically</option> | ||
| 3485 | <option data-i18n="Input Price" value="pricing.input">Input Price (cheapest)</option> | ||
| 3486 | <option data-i18n="Output Price" value="pricing.output">Output Price (cheapest)</option> | ||
| 3487 | <option data-i18n="Context Size" value="context_length">Context Size</option> | ||
| 3488 | </select> | ||
| 3489 | </label> | ||
| 3490 | </div> | ||
| 3491 | <div class="marginTopBot5"> | ||
| 3492 | <label for="electronhub_group_models" class="checkbox_label"> | ||
| 3493 | <input id="electronhub_group_models" type="checkbox" /> | ||
| 3494 | <span data-i18n="Group by vendors">Group by vendors</span> | ||
| 3495 | </label> | ||
| 3496 | <div class="toggle-description justifyLeft wide100p"> | ||
| 3497 | <span data-i18n="Group by vendors Description"> | ||
| 3498 | Put OpenAI models in one group, Anthropic models in other group, etc. Can be combined with sorting. | ||
| 3499 | </span> | ||
| 3500 | </div> | ||
| 3501 | </div> | ||
| 3502 | </div> | ||
| 3503 | </div> | ||
| 3504 | </div> | ||
| 3466 | </div> | 3505 | </div> |
| 3467 | <div id="nanogpt_form" data-source="nanogpt"> | 3506 | <div id="nanogpt_form" data-source="nanogpt"> |
| 3468 | <h4 data-i18n="NanoGPT API Key">NanoGPT API Key</h4> | 3507 | <h4 data-i18n="NanoGPT API Key">NanoGPT API Key</h4> |
| @@ -5345,9 +5384,19 @@ | |||
| 5345 | </div> | 5384 | </div> |
| 5346 | </div> | 5385 | </div> |
| 5347 | <div id="bg-scrollable-content"> | 5386 | <div id="bg-scrollable-content"> |
| 5348 | <h3 data-i18n="System Backgrounds" class="wide100p textAlignCenter"> | 5387 | <div class="heading-container-with-controls"> |
| 5349 | System Backgrounds | 5388 | <h3 data-i18n="System Backgrounds" class="wide100p textAlignCenter heading-text"> |
| 5350 | </h3> | 5389 | System Backgrounds |
| 5390 | </h3> | ||
| 5391 | <div class="heading-controls"> | ||
| 5392 | <button id="bg_thumb_zoom_out" class="menu_button menu_button_icon" title="Make thumbnails smaller" data-i18n="[title]Make thumbnails smaller"> | ||
| 5393 | <i class="fa-solid fa-minus"></i> | ||
| 5394 | </button> | ||
| 5395 | <button id="bg_thumb_zoom_in" class="menu_button menu_button_icon" title="Make thumbnails larger" data-i18n="[title]Make thumbnails larger"> | ||
| 5396 | <i class="fa-solid fa-plus"></i> | ||
| 5397 | </button> | ||
| 5398 | </div> | ||
| 5399 | </div> | ||
| 5351 | <div id="bg_menu_content" class="bg_list"> | 5400 | <div id="bg_menu_content" class="bg_list"> |
| 5352 | </div> | 5401 | </div> |
| 5353 | <h3 data-i18n="Chat Backgrounds" class="wide100p textAlignCenter"> | 5402 | <h3 data-i18n="Chat Backgrounds" class="wide100p textAlignCenter"> |
| @@ -7749,4 +7798,4 @@ | |||
| 7749 | </script> | 7798 | </script> |
| 7750 | </body> | 7799 | </body> |
| 7751 | 7800 | ||
| 7752 | </html> | ||
| 7752 | \ No newline at end of file | \ No newline at end of file | |
| 7801 | </html> | ||
| @@ -369,7 +369,7 @@ | |||
| 369 | "Anthropic's developer console": "وحدة تحكم المطور في Anthropic", | 369 | "Anthropic's developer console": "وحدة تحكم المطور في Anthropic", |
| 370 | "Claude Model": "نموذج Claude", | 370 | "Claude Model": "نموذج Claude", |
| 371 | "Window AI Model": "نموذج Window AI", | 371 | "Window AI Model": "نموذج Window AI", |
| 372 | "Model Order": "فرز نموذج OpenRouter", | 372 | "OpenRouter Model Sorting": "فرز نموذج OpenRouter", |
| 373 | "Alphabetically": "أبجديا", | 373 | "Alphabetically": "أبجديا", |
| 374 | "Price": "السعر (الأرخص)", | 374 | "Price": "السعر (الأرخص)", |
| 375 | "Context Size": "حجم السياق", | 375 | "Context Size": "حجم السياق", |
| @@ -369,7 +369,7 @@ | |||
| 369 | "Anthropic's developer console": "Anthropics Entwicklerkonsole", | 369 | "Anthropic's developer console": "Anthropics Entwicklerkonsole", |
| 370 | "Claude Model": "Claude-Modell", | 370 | "Claude Model": "Claude-Modell", |
| 371 | "Window AI Model": "Fenster AI-Modell", | 371 | "Window AI Model": "Fenster AI-Modell", |
| 372 | "Model Order": "Sortierung des OpenRouter-Modells", | 372 | "OpenRouter Model Sorting": "Sortierung des OpenRouter-Modells", |
| 373 | "Alphabetically": "Alphabetisch", | 373 | "Alphabetically": "Alphabetisch", |
| 374 | "Price": "Preis (am günstigsten)", | 374 | "Price": "Preis (am günstigsten)", |
| 375 | "Context Size": "Kontextgröße", | 375 | "Context Size": "Kontextgröße", |
| @@ -369,7 +369,7 @@ | |||
| 369 | "Anthropic's developer console": "la consola de desarrolladores de Anthropic", | 369 | "Anthropic's developer console": "la consola de desarrolladores de Anthropic", |
| 370 | "Claude Model": "Modelo de Claude", | 370 | "Claude Model": "Modelo de Claude", |
| 371 | "Window AI Model": "Modelo de Window AI", | 371 | "Window AI Model": "Modelo de Window AI", |
| 372 | "Model Order": "Clasificación de modelos de OpenRouter", | 372 | "OpenRouter Model Sorting": "Clasificación de modelos de OpenRouter", |
| 373 | "Alphabetically": "Alfabéticamente", | 373 | "Alphabetically": "Alfabéticamente", |
| 374 | "Price": "Precio (más barato)", | 374 | "Price": "Precio (más barato)", |
| 375 | "Context Size": "Tamaño del contexto", | 375 | "Context Size": "Tamaño del contexto", |
| @@ -348,7 +348,7 @@ | |||
| 348 | "Anthropic's developer console": "Console de développement d'Anthropic", | 348 | "Anthropic's developer console": "Console de développement d'Anthropic", |
| 349 | "Claude Model": "Modèle Claude", | 349 | "Claude Model": "Modèle Claude", |
| 350 | "Window AI Model": "Modèle Window AI", | 350 | "Window AI Model": "Modèle Window AI", |
| 351 | "Model Order": "Tri des modèles OpenRouter", | 351 | "OpenRouter Model Sorting": "Tri des modèles OpenRouter", |
| 352 | "Alphabetically": "Alphabétiquement", | 352 | "Alphabetically": "Alphabétiquement", |
| 353 | "Price": "Prix (le moins cher)", | 353 | "Price": "Prix (le moins cher)", |
| 354 | "Context Size": "Taille du contexte", | 354 | "Context Size": "Taille du contexte", |
| @@ -1566,7 +1566,7 @@ | |||
| 1566 | "These files are available for all characters in the current chat.": "Ces fichiers sont disponibles pour tous les personnages du chat actuel.", | 1566 | "These files are available for all characters in the current chat.": "Ces fichiers sont disponibles pour tous les personnages du chat actuel.", |
| 1567 | "Image Captioning": "Légende des images", | 1567 | "Image Captioning": "Légende des images", |
| 1568 | "Local": "Local", | 1568 | "Local": "Local", |
| 1569 | "Multimodal (OpenAI / Anthropic / llama / Google)": "Multimodal (OpenAI / Anthropic / llama / Google)", | 1569 | "Multimodal": "Multimodal", |
| 1570 | "Extras": "Extras", | 1570 | "Extras": "Extras", |
| 1571 | "Horde": "Horde", | 1571 | "Horde": "Horde", |
| 1572 | "API": "API", | 1572 | "API": "API", |
| @@ -367,7 +367,7 @@ | |||
| 367 | "Anthropic's developer console": "Uppbyggingaraðilar Forritara stjórnborð", | 367 | "Anthropic's developer console": "Uppbyggingaraðilar Forritara stjórnborð", |
| 368 | "Claude Model": "Claude módel", | 368 | "Claude Model": "Claude módel", |
| 369 | "Window AI Model": "Vindauga AI módel", | 369 | "Window AI Model": "Vindauga AI módel", |
| 370 | "Model Order": "OpenRouter líkanaflokkun", | 370 | "OpenRouter Model Sorting": "OpenRouter líkanaflokkun", |
| 371 | "Alphabetically": "Stafrófsröð", | 371 | "Alphabetically": "Stafrófsröð", |
| 372 | "Price": "Verð (ódýrast)", | 372 | "Price": "Verð (ódýrast)", |
| 373 | "Context Size": "Samhengisstærð", | 373 | "Context Size": "Samhengisstærð", |
| @@ -369,7 +369,7 @@ | |||
| 369 | "Anthropic's developer console": "Console dello sviluppatore di Anthropic", | 369 | "Anthropic's developer console": "Console dello sviluppatore di Anthropic", |
| 370 | "Claude Model": "Modello Claude", | 370 | "Claude Model": "Modello Claude", |
| 371 | "Window AI Model": "Modello AI di Window", | 371 | "Window AI Model": "Modello AI di Window", |
| 372 | "Model Order": "Ordinamento dei modelli OpenRouter", | 372 | "OpenRouter Model Sorting": "Ordinamento dei modelli OpenRouter", |
| 373 | "Alphabetically": "In ordine alfabetico", | 373 | "Alphabetically": "In ordine alfabetico", |
| 374 | "Price": "Prezzo (il più economico)", | 374 | "Price": "Prezzo (il più economico)", |
| 375 | "Context Size": "Dimensione del contesto", | 375 | "Context Size": "Dimensione del contesto", |
| @@ -369,7 +369,7 @@ | |||
| 369 | "Anthropic's developer console": "Anthropicの開発者コンソール", | 369 | "Anthropic's developer console": "Anthropicの開発者コンソール", |
| 370 | "Claude Model": "Claudeモデル", | 370 | "Claude Model": "Claudeモデル", |
| 371 | "Window AI Model": "Window AIモデル", | 371 | "Window AI Model": "Window AIモデル", |
| 372 | "Model Order": "OpenRouter モデルのソート", | 372 | "OpenRouter Model Sorting": "OpenRouter モデルのソート", |
| 373 | "Alphabetically": "アルファベット順", | 373 | "Alphabetically": "アルファベット順", |
| 374 | "Price": "価格(最安値)", | 374 | "Price": "価格(最安値)", |
| 375 | "Context Size": "コンテキストサイズ", | 375 | "Context Size": "コンテキストサイズ", |
| @@ -374,7 +374,7 @@ | |||
| 374 | "Slack and Poe cookies will not work here, do not bother trying.": "Slack과 Poe 쿠키는 여기서 작동하지 않습니다. 시도하지 마세요.", | 374 | "Slack and Poe cookies will not work here, do not bother trying.": "Slack과 Poe 쿠키는 여기서 작동하지 않습니다. 시도하지 마세요.", |
| 375 | "Claude Model": "Claude 모델", | 375 | "Claude Model": "Claude 모델", |
| 376 | "Window AI Model": "Window AI 모델", | 376 | "Window AI Model": "Window AI 모델", |
| 377 | "Model Order": "OpenRouter 모델 정렬", | 377 | "OpenRouter Model Sorting": "OpenRouter 모델 정렬", |
| 378 | "Alphabetically": "알파벳순", | 378 | "Alphabetically": "알파벳순", |
| 379 | "Price": "가격(가장 저렴)", | 379 | "Price": "가격(가장 저렴)", |
| 380 | "Context Size": "컨텍스트 크기", | 380 | "Context Size": "컨텍스트 크기", |
| @@ -367,7 +367,7 @@ | |||
| 367 | "Anthropic's developer console": "Anthropic's ontwikkelaarsconsole", | 367 | "Anthropic's developer console": "Anthropic's ontwikkelaarsconsole", |
| 368 | "Claude Model": "Claude-model", | 368 | "Claude Model": "Claude-model", |
| 369 | "Window AI Model": "Window AI-model", | 369 | "Window AI Model": "Window AI-model", |
| 370 | "Model Order": "Sorteren van OpenRouter-modellen", | 370 | "OpenRouter Model Sorting": "Sorteren van OpenRouter-modellen", |
| 371 | "Alphabetically": "Alfabetisch", | 371 | "Alphabetically": "Alfabetisch", |
| 372 | "Price": "Prijs (goedkoopste)", | 372 | "Price": "Prijs (goedkoopste)", |
| 373 | "Context Size": "Contextgrootte", | 373 | "Context Size": "Contextgrootte", |
| @@ -369,7 +369,7 @@ | |||
| 369 | "Anthropic's developer console": "console de desenvolvedor da Anthropic", | 369 | "Anthropic's developer console": "console de desenvolvedor da Anthropic", |
| 370 | "Claude Model": "Modelo Claude", | 370 | "Claude Model": "Modelo Claude", |
| 371 | "Window AI Model": "Modelo Window AI", | 371 | "Window AI Model": "Modelo Window AI", |
| 372 | "Model Order": "Classificação de modelo OpenRouter", | 372 | "OpenRouter Model Sorting": "Classificação de modelo OpenRouter", |
| 373 | "Alphabetically": "Alfabeticamente", | 373 | "Alphabetically": "Alfabeticamente", |
| 374 | "Price": "Preço (mais barato)", | 374 | "Price": "Preço (mais barato)", |
| 375 | "Context Size": "Tamanho do contexto", | 375 | "Context Size": "Tamanho do contexto", |
| @@ -718,7 +718,7 @@ | |||
| 718 | "Google Model": "Модель Google", | 718 | "Google Model": "Модель Google", |
| 719 | "Cohere API Key": "Ключ от API Cohere", | 719 | "Cohere API Key": "Ключ от API Cohere", |
| 720 | "Cohere Model": "Модель Cohere", | 720 | "Cohere Model": "Модель Cohere", |
| 721 | "Model Order": "Сортировка моделей OpenRouter", | 721 | "OpenRouter Model Sorting": "Сортировка моделей OpenRouter", |
| 722 | "Alphabetically": "По алфавиту", | 722 | "Alphabetically": "По алфавиту", |
| 723 | "Price": "По цене (наиболее низкая)", | 723 | "Price": "По цене (наиболее низкая)", |
| 724 | "Context Size": "По размеру контекста", | 724 | "Context Size": "По размеру контекста", |
| @@ -369,7 +369,7 @@ | |||
| 369 | "Anthropic's developer console": "консолі розробника Anthropic", | 369 | "Anthropic's developer console": "консолі розробника Anthropic", |
| 370 | "Claude Model": "Модель Claude", | 370 | "Claude Model": "Модель Claude", |
| 371 | "Window AI Model": "Модель Window AI", | 371 | "Window AI Model": "Модель Window AI", |
| 372 | "Model Order": "Сортування моделі OpenRouter", | 372 | "OpenRouter Model Sorting": "Сортування моделі OpenRouter", |
| 373 | "Alphabetically": "За алфавітом", | 373 | "Alphabetically": "За алфавітом", |
| 374 | "Price": "Ціна (найдешевша)", | 374 | "Price": "Ціна (найдешевша)", |
| 375 | "Context Size": "Розмір контексту", | 375 | "Context Size": "Розмір контексту", |
| @@ -369,7 +369,7 @@ | |||
| 369 | "Anthropic's developer console": "developer console của Anthropic", | 369 | "Anthropic's developer console": "developer console của Anthropic", |
| 370 | "Claude Model": "Model Claude", | 370 | "Claude Model": "Model Claude", |
| 371 | "Window AI Model": "Model Window AI", | 371 | "Window AI Model": "Model Window AI", |
| 372 | "Model Order": "Sắp xếp model OpenRouter", | 372 | "OpenRouter Model Sorting": "Sắp xếp model OpenRouter", |
| 373 | "Alphabetically": "Theo thứ tự bảng chữ cái", | 373 | "Alphabetically": "Theo thứ tự bảng chữ cái", |
| 374 | "Price": "Giá (rẻ nhất)", | 374 | "Price": "Giá (rẻ nhất)", |
| 375 | "Context Size": "Kích thước bối cảnh", | 375 | "Context Size": "Kích thước bối cảnh", |
| @@ -452,7 +452,7 @@ | |||
| 452 | "Claude Model": "Claude 模型", | 452 | "Claude Model": "Claude 模型", |
| 453 | "Allow fallback routes Description": "如果所选模型无法响应您的请求,则自动选择备用模型。", | 453 | "Allow fallback routes Description": "如果所选模型无法响应您的请求,则自动选择备用模型。", |
| 454 | "Allow fallback models": "允许后备模型", | 454 | "Allow fallback models": "允许后备模型", |
| 455 | "Model Order": "OpenRouter 模型顺序", | 455 | "OpenRouter Model Sorting": "OpenRouter 模型顺序", |
| 456 | "Alphabetically": "按字母顺序", | 456 | "Alphabetically": "按字母顺序", |
| 457 | "Price": "价格(最便宜)", | 457 | "Price": "价格(最便宜)", |
| 458 | "Context Size": "上下文长度", | 458 | "Context Size": "上下文长度", |
| @@ -1479,7 +1479,7 @@ | |||
| 1479 | "Image Captioning": "图像描述", | 1479 | "Image Captioning": "图像描述", |
| 1480 | "Source": "来源", | 1480 | "Source": "来源", |
| 1481 | "Local": "本地", | 1481 | "Local": "本地", |
| 1482 | "Multimodal (OpenAI / Anthropic / llama / Google)": "多模态(OpenAI / Anthropic / llama / Google)", | 1482 | "Multimodal": "多模态", |
| 1483 | "Extras": "更多", | 1483 | "Extras": "更多", |
| 1484 | "Horde": "Horde", | 1484 | "Horde": "Horde", |
| 1485 | "API": "API", | 1485 | "API": "API", |
| @@ -368,7 +368,7 @@ | |||
| 368 | "Anthropic's developer console": "Anthropic 的開發者控制台", | 368 | "Anthropic's developer console": "Anthropic 的開發者控制台", |
| 369 | "Claude Model": "Claude 模型", | 369 | "Claude Model": "Claude 模型", |
| 370 | "Window AI Model": "Window AI 模型", | 370 | "Window AI Model": "Window AI 模型", |
| 371 | "Model Order": "模型順序", | 371 | "OpenRouter Model Sorting": "模型順序", |
| 372 | "Alphabetically": "按字母順序", | 372 | "Alphabetically": "按字母順序", |
| 373 | "Price": "價格(最便宜的)", | 373 | "Price": "價格(最便宜的)", |
| 374 | "Context Size": "上下文長度", | 374 | "Context Size": "上下文長度", |
| @@ -1674,7 +1674,7 @@ | |||
| 1674 | "Message Template": "訊息範本", | 1674 | "Message Template": "訊息範本", |
| 1675 | "Model ID": "模型 ID", | 1675 | "Model ID": "模型 ID", |
| 1676 | "mui_reset": "重設", | 1676 | "mui_reset": "重設", |
| 1677 | "Multimodal (OpenAI / Anthropic / llama / Google)": "多模態(OpenAI/Anthropic/llama/Google)", | 1677 | "Multimodal": "多模態", |
| 1678 | "must be set in Tabby's config.yml to switch models.": "須在 Tabby's config.yml 中設定以切換模型。", | 1678 | "must be set in Tabby's config.yml to switch models.": "須在 Tabby's config.yml 中設定以切換模型。", |
| 1679 | "Names as Stop Strings": "將名稱用作停止字串", | 1679 | "Names as Stop Strings": "將名稱用作停止字串", |
| 1680 | "Never": "從不", | 1680 | "Never": "從不", |
| @@ -3,7 +3,8 @@ import { chat_metadata, eventSource, event_types, generateQuietPrompt, getCurren | |||
| 3 | import { openThirdPartyExtensionMenu, saveMetadataDebounced } from './extensions.js'; | 3 | import { openThirdPartyExtensionMenu, saveMetadataDebounced } from './extensions.js'; |
| 4 | import { SlashCommand } from './slash-commands/SlashCommand.js'; | 4 | import { SlashCommand } from './slash-commands/SlashCommand.js'; |
| 5 | import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; | 5 | import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; |
| 6 | import { createThumbnail, flashHighlight, getBase64Async, stringFormat } from './utils.js'; | 6 | import { createThumbnail, flashHighlight, getBase64Async, stringFormat, debounce } from './utils.js'; |
| 7 | import { debounce_timeout } from './constants.js'; | ||
| 7 | import { t } from './i18n.js'; | 8 | import { t } from './i18n.js'; |
| 8 | import { Popup } from './popup.js'; | 9 | import { Popup } from './popup.js'; |
| 9 | 10 | ||
| @@ -15,6 +16,11 @@ const PNG_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkY | |||
| 15 | const PNG_PIXEL_BLOB = new Blob([Uint8Array.from(atob(PNG_PIXEL), c => c.charCodeAt(0))], { type: 'image/png' }); | 16 | const PNG_PIXEL_BLOB = new Blob([Uint8Array.from(atob(PNG_PIXEL), c => c.charCodeAt(0))], { type: 'image/png' }); |
| 16 | const PLACEHOLDER_IMAGE = `url('data:image/png;base64,${PNG_PIXEL}')`; | 17 | const PLACEHOLDER_IMAGE = `url('data:image/png;base64,${PNG_PIXEL}')`; |
| 17 | 18 | ||
| 19 | const THUMBNAIL_COLUMNS_MIN = 2; | ||
| 20 | const THUMBNAIL_COLUMNS_MAX = 8; | ||
| 21 | const THUMBNAIL_COLUMNS_DEFAULT_DESKTOP = 5; | ||
| 22 | const THUMBNAIL_COLUMNS_DEFAULT_MOBILE = 3; | ||
| 23 | |||
| 18 | /** | 24 | /** |
| 19 | * Storage for frontend-generated background thumbnails. | 25 | * Storage for frontend-generated background thumbnails. |
| 20 | * This is used to store thumbnails for backgrounds that cannot be generated on the server. | 26 | * This is used to store thumbnails for backgrounds that cannot be generated on the server. |
| @@ -45,6 +51,53 @@ export let background_settings = { | |||
| 45 | animation: false, | 51 | animation: false, |
| 46 | }; | 52 | }; |
| 47 | 53 | ||
| 54 | /** | ||
| 55 | * Creates a single thumbnail DOM element. The CSS now handles all sizing. | ||
| 56 | * @param {object} imageData - Data for the image (filename, isCustom). | ||
| 57 | * @returns {HTMLElement} The created thumbnail element. | ||
| 58 | */ | ||
| 59 | function createThumbnailElement(imageData) { | ||
| 60 | const bg = imageData.filename; | ||
| 61 | const isCustom = imageData.isCustom; | ||
| 62 | |||
| 63 | const thumbnail = $('#background_template .bg_example').clone(); | ||
| 64 | |||
| 65 | const clipper = document.createElement('div'); | ||
| 66 | clipper.className = 'thumbnail-clipper lazy-load-background'; | ||
| 67 | clipper.style.backgroundImage = PLACEHOLDER_IMAGE; | ||
| 68 | |||
| 69 | const titleElement = thumbnail.find('.BGSampleTitle'); | ||
| 70 | clipper.appendChild(titleElement.get(0)); | ||
| 71 | thumbnail.append(clipper); | ||
| 72 | |||
| 73 | const url = generateUrlParameter(bg, isCustom); | ||
| 74 | const title = isCustom ? bg.split('/').pop() : bg; | ||
| 75 | const friendlyTitle = title.slice(0, title.lastIndexOf('.')); | ||
| 76 | |||
| 77 | thumbnail.attr('title', title); | ||
| 78 | thumbnail.attr('bgfile', bg); | ||
| 79 | thumbnail.attr('custom', String(isCustom)); | ||
| 80 | thumbnail.data('url', url); | ||
| 81 | titleElement.text(friendlyTitle); | ||
| 82 | |||
| 83 | return thumbnail.get(0); | ||
| 84 | } | ||
| 85 | |||
| 86 | /** | ||
| 87 | * Applies the thumbnail column count to the CSS and updates button states. | ||
| 88 | * @param {number} count - The number of columns to display. | ||
| 89 | */ | ||
| 90 | function applyThumbnailColumns(count) { | ||
| 91 | const newCount = Math.max(THUMBNAIL_COLUMNS_MIN, Math.min(count, THUMBNAIL_COLUMNS_MAX)); | ||
| 92 | background_settings.thumbnailColumns = newCount; | ||
| 93 | document.documentElement.style.setProperty('--bg-thumb-columns', newCount.toString()); | ||
| 94 | |||
| 95 | $('#bg_thumb_zoom_in').prop('disabled', newCount <= THUMBNAIL_COLUMNS_MIN); | ||
| 96 | $('#bg_thumb_zoom_out').prop('disabled', newCount >= THUMBNAIL_COLUMNS_MAX); | ||
| 97 | |||
| 98 | saveSettingsDebounced(); | ||
| 99 | } | ||
| 100 | |||
| 48 | export function loadBackgroundSettings(settings) { | 101 | export function loadBackgroundSettings(settings) { |
| 49 | let backgroundSettings = settings.background; | 102 | let backgroundSettings = settings.background; |
| 50 | if (!backgroundSettings || !backgroundSettings.name || !backgroundSettings.url) { | 103 | if (!backgroundSettings || !backgroundSettings.name || !backgroundSettings.url) { |
| @@ -56,6 +109,16 @@ export function loadBackgroundSettings(settings) { | |||
| 56 | if (!Object.hasOwn(backgroundSettings, 'animation')) { | 109 | if (!Object.hasOwn(backgroundSettings, 'animation')) { |
| 57 | backgroundSettings.animation = false; | 110 | backgroundSettings.animation = false; |
| 58 | } | 111 | } |
| 112 | |||
| 113 | // If a value is already saved, use it. Otherwise, determine default based on screen size. | ||
| 114 | let columns = backgroundSettings.thumbnailColumns; | ||
| 115 | if (!columns) { | ||
| 116 | const isNarrowScreen = window.matchMedia('(max-width: 480px)').matches; | ||
| 117 | columns = isNarrowScreen ? THUMBNAIL_COLUMNS_DEFAULT_MOBILE : THUMBNAIL_COLUMNS_DEFAULT_DESKTOP; | ||
| 118 | } | ||
| 119 | background_settings.thumbnailColumns = columns; | ||
| 120 | applyThumbnailColumns(background_settings.thumbnailColumns); | ||
| 121 | |||
| 59 | setBackground(backgroundSettings.name, backgroundSettings.url); | 122 | setBackground(backgroundSettings.name, backgroundSettings.url); |
| 60 | setFittingClass(backgroundSettings.fitting); | 123 | setFittingClass(backgroundSettings.fitting); |
| 61 | $('#background_fitting').val(backgroundSettings.fitting); | 124 | $('#background_fitting').val(backgroundSettings.fitting); |
| @@ -75,7 +138,7 @@ async function forceSetBackground(backgroundInfo) { | |||
| 75 | list.push(bg); | 138 | list.push(bg); |
| 76 | chat_metadata[LIST_METADATA_KEY] = list; | 139 | chat_metadata[LIST_METADATA_KEY] = list; |
| 77 | saveMetadataDebounced(); | 140 | saveMetadataDebounced(); |
| 78 | await getChatBackgroundsList(); | 141 | renderChatBackgrounds(); |
| 79 | highlightNewBackground(bg); | 142 | highlightNewBackground(bg); |
| 80 | highlightLockedBackground(); | 143 | highlightLockedBackground(); |
| 81 | } | 144 | } |
| @@ -88,28 +151,10 @@ async function onChatChanged() { | |||
| 88 | unsetCustomBackground(); | 151 | unsetCustomBackground(); |
| 89 | } | 152 | } |
| 90 | 153 | ||
| 91 | await getChatBackgroundsList(); | 154 | renderChatBackgrounds(); |
| 92 | highlightLockedBackground(); | 155 | highlightLockedBackground(); |
| 93 | } | 156 | } |
| 94 | 157 | ||
| 95 | async function getChatBackgroundsList() { | ||
| 96 | const list = chat_metadata[LIST_METADATA_KEY]; | ||
| 97 | const listEmpty = !Array.isArray(list) || list.length === 0; | ||
| 98 | |||
| 99 | $('#bg_custom_content').empty(); | ||
| 100 | $('#bg_chat_hint').toggle(listEmpty); | ||
| 101 | |||
| 102 | if (listEmpty) { | ||
| 103 | return; | ||
| 104 | } | ||
| 105 | |||
| 106 | for (const bg of list) { | ||
| 107 | const template = await getBackgroundFromTemplate(bg, true); | ||
| 108 | $('#bg_custom_content').append(template); | ||
| 109 | } | ||
| 110 | activateLazyLoader(); | ||
| 111 | } | ||
| 112 | |||
| 113 | function getBackgroundPath(fileUrl) { | 158 | function getBackgroundPath(fileUrl) { |
| 114 | return `backgrounds/${encodeURIComponent(fileUrl)}`; | 159 | return `backgrounds/${encodeURIComponent(fileUrl)}`; |
| 115 | } | 160 | } |
| @@ -257,7 +302,7 @@ async function onCopyToSystemBackgroundClick(e) { | |||
| 257 | const index = list.indexOf(bgNames.oldBg); | 302 | const index = list.indexOf(bgNames.oldBg); |
| 258 | list.splice(index, 1); | 303 | list.splice(index, 1); |
| 259 | saveMetadataDebounced(); | 304 | saveMetadataDebounced(); |
| 260 | await getChatBackgroundsList(); | 305 | renderChatBackgrounds(); |
| 261 | } | 306 | } |
| 262 | 307 | ||
| 263 | /** | 308 | /** |
| @@ -382,17 +427,21 @@ async function onDeleteBackgroundClick(e) { | |||
| 382 | list.splice(index, 1); | 427 | list.splice(index, 1); |
| 383 | } | 428 | } |
| 384 | 429 | ||
| 385 | const siblingSelector = '.bg_example:not(#form_bg_download)'; | 430 | if (bg === background_settings.name) { |
| 386 | const nextBg = bgToDelete.next(siblingSelector); | 431 | const siblingSelector = '.bg_example'; |
| 387 | const prevBg = bgToDelete.prev(siblingSelector); | 432 | const nextBg = bgToDelete.next(siblingSelector); |
| 388 | const anyBg = $(siblingSelector); | 433 | const prevBg = bgToDelete.prev(siblingSelector); |
| 389 | 434 | ||
| 390 | if (nextBg.length > 0) { | 435 | if (nextBg.length > 0) { |
| 391 | nextBg.trigger('click'); | 436 | nextBg.trigger('click'); |
| 392 | } else if (prevBg.length > 0) { | 437 | } else if (prevBg.length > 0) { |
| 393 | prevBg.trigger('click'); | 438 | prevBg.trigger('click'); |
| 394 | } else { | 439 | } else { |
| 395 | $(anyBg[Math.floor(Math.random() * anyBg.length)]).trigger('click'); | 440 | const anyOtherBg = $('.bg_example').not(bgToDelete).first(); |
| 441 | if (anyOtherBg.length > 0) { | ||
| 442 | anyOtherBg.trigger('click'); | ||
| 443 | } | ||
| 444 | } | ||
| 396 | } | 445 | } |
| 397 | 446 | ||
| 398 | bgToDelete.remove(); | 447 | bgToDelete.remove(); |
| @@ -404,7 +453,7 @@ async function onDeleteBackgroundClick(e) { | |||
| 404 | } | 453 | } |
| 405 | 454 | ||
| 406 | if (isCustom) { | 455 | if (isCustom) { |
| 407 | await getChatBackgroundsList(); | 456 | renderChatBackgrounds(); |
| 408 | saveMetadataDebounced(); | 457 | saveMetadataDebounced(); |
| 409 | } | 458 | } |
| 410 | } | 459 | } |
| @@ -445,6 +494,47 @@ async function autoBackgroundCommand() { | |||
| 445 | return ''; | 494 | return ''; |
| 446 | } | 495 | } |
| 447 | 496 | ||
| 497 | /** | ||
| 498 | * Renders the system backgrounds gallery. | ||
| 499 | * @param {string[]} [backgrounds] - Optional filtered list of backgrounds. | ||
| 500 | */ | ||
| 501 | function renderSystemBackgrounds(backgrounds) { | ||
| 502 | const sourceList = backgrounds || []; | ||
| 503 | const container = $('#bg_menu_content'); | ||
| 504 | container.empty(); | ||
| 505 | |||
| 506 | if (sourceList.length === 0) return; | ||
| 507 | |||
| 508 | sourceList.forEach(bg => { | ||
| 509 | const imageData = { filename: bg, isCustom: false }; | ||
| 510 | const thumbnail = createThumbnailElement(imageData); | ||
| 511 | container.append(thumbnail); | ||
| 512 | }); | ||
| 513 | |||
| 514 | activateLazyLoader(); | ||
| 515 | } | ||
| 516 | |||
| 517 | /** | ||
| 518 | * Renders the chat-specific (custom) backgrounds gallery. | ||
| 519 | * @param {string[]} [backgrounds] - Optional filtered list of backgrounds. | ||
| 520 | */ | ||
| 521 | function renderChatBackgrounds(backgrounds) { | ||
| 522 | const sourceList = backgrounds ?? (chat_metadata[LIST_METADATA_KEY] || []); | ||
| 523 | const container = $('#bg_custom_content'); | ||
| 524 | container.empty(); | ||
| 525 | $('#bg_chat_hint').toggle(!sourceList.length); | ||
| 526 | |||
| 527 | if (sourceList.length === 0) return; | ||
| 528 | |||
| 529 | sourceList.forEach(bg => { | ||
| 530 | const imageData = { filename: bg, isCustom: true }; | ||
| 531 | const thumbnail = createThumbnailElement(imageData); | ||
| 532 | container.append(thumbnail); | ||
| 533 | }); | ||
| 534 | |||
| 535 | activateLazyLoader(); | ||
| 536 | } | ||
| 537 | |||
| 448 | export async function getBackgrounds() { | 538 | export async function getBackgrounds() { |
| 449 | const response = await fetch('/api/backgrounds/all', { | 539 | const response = await fetch('/api/backgrounds/all', { |
| 450 | method: 'POST', | 540 | method: 'POST', |
| @@ -454,12 +544,7 @@ export async function getBackgrounds() { | |||
| 454 | if (response.ok) { | 544 | if (response.ok) { |
| 455 | const { images, config } = await response.json(); | 545 | const { images, config } = await response.json(); |
| 456 | Object.assign(THUMBNAIL_CONFIG, config); | 546 | Object.assign(THUMBNAIL_CONFIG, config); |
| 457 | $('#bg_menu_content').children('div').remove(); | 547 | renderSystemBackgrounds(images); |
| 458 | for (const bg of images) { | ||
| 459 | const template = await getBackgroundFromTemplate(bg, false); | ||
| 460 | $('#bg_menu_content').append(template); | ||
| 461 | } | ||
| 462 | activateLazyLoader(); | ||
| 463 | } | 548 | } |
| 464 | } | 549 | } |
| 465 | 550 | ||
| @@ -481,14 +566,19 @@ function activateLazyLoader() { | |||
| 481 | lazyLoadObserver = new IntersectionObserver((entries, observer) => { | 566 | lazyLoadObserver = new IntersectionObserver((entries, observer) => { |
| 482 | entries.forEach(entry => { | 567 | entries.forEach(entry => { |
| 483 | if (entry.target instanceof HTMLElement && entry.isIntersecting) { | 568 | if (entry.target instanceof HTMLElement && entry.isIntersecting) { |
| 484 | const target = entry.target; | 569 | const clipper = entry.target; |
| 485 | const bg = target.getAttribute('bgfile'); | 570 | const parentThumbnail = clipper.closest('.bg_example'); |
| 486 | const isCustom = target.getAttribute('custom') === 'true'; | 571 | |
| 487 | resolveImageUrl(bg, isCustom) | 572 | if (parentThumbnail) { |
| 488 | .then(url => { target.style.backgroundImage = url; }) | 573 | const bg = parentThumbnail.getAttribute('bgfile'); |
| 489 | .catch(() => { target.style.backgroundImage = PLACEHOLDER_IMAGE; }); | 574 | const isCustom = parentThumbnail.getAttribute('custom') === 'true'; |
| 490 | target.classList.remove('lazy-load-background'); | 575 | resolveImageUrl(bg, isCustom) |
| 491 | observer.unobserve(target); | 576 | .then(url => { clipper.style.backgroundImage = url; }) |
| 577 | .catch(() => { clipper.style.backgroundImage = PLACEHOLDER_IMAGE; }); | ||
| 578 | } | ||
| 579 | |||
| 580 | clipper.classList.remove('lazy-load-background'); | ||
| 581 | observer.unobserve(clipper); | ||
| 492 | } | 582 | } |
| 493 | }); | 583 | }); |
| 494 | }, options); | 584 | }, options); |
| @@ -529,28 +619,6 @@ async function resolveImageUrl(bg, isCustom) { | |||
| 529 | return `url("${thumbnailUrl}")`; | 619 | return `url("${thumbnailUrl}")`; |
| 530 | } | 620 | } |
| 531 | 621 | ||
| 532 | /** | ||
| 533 | * Instantiates a background template | ||
| 534 | * @param {string} bg Path to background | ||
| 535 | * @param {boolean} isCustom Whether the background is custom | ||
| 536 | * @returns {Promise<JQuery<HTMLElement>>} Background template | ||
| 537 | */ | ||
| 538 | async function getBackgroundFromTemplate(bg, isCustom) { | ||
| 539 | const template = $('#background_template .bg_example').clone(); | ||
| 540 | const url = generateUrlParameter(bg, isCustom); | ||
| 541 | const title = isCustom ? bg.split('/').pop() : bg; | ||
| 542 | const friendlyTitle = title.slice(0, title.lastIndexOf('.')); | ||
| 543 | |||
| 544 | template.attr('title', title); | ||
| 545 | template.attr('bgfile', bg); | ||
| 546 | template.attr('custom', String(isCustom)); | ||
| 547 | template.data('url', url); | ||
| 548 | template.addClass('lazy-load-background'); | ||
| 549 | template.css('background-image', PLACEHOLDER_IMAGE); | ||
| 550 | template.find('.BGSampleTitle').text(friendlyTitle); | ||
| 551 | return template; | ||
| 552 | } | ||
| 553 | |||
| 554 | async function setBackground(bg, url) { | 622 | async function setBackground(bg, url) { |
| 555 | $('#bg1').css('background-image', url); | 623 | $('#bg1').css('background-image', url); |
| 556 | background_settings.name = bg; | 624 | background_settings.name = bg; |
| @@ -688,17 +756,17 @@ function setFittingClass(fitting) { | |||
| 688 | } | 756 | } |
| 689 | 757 | ||
| 690 | function onBackgroundFilterInput() { | 758 | function onBackgroundFilterInput() { |
| 691 | const filterValue = String($(this).val()).toLowerCase(); | 759 | const filterValue = String($('#bg-filter').val()).toLowerCase(); |
| 692 | $('#bg_menu_content > div').each(function () { | 760 | $('#bg_menu_content > .bg_example, #bg_custom_content > .bg_example').each(function () { |
| 693 | const $bgContent = $(this); | 761 | const $bg = $(this); |
| 694 | if ($bgContent.attr('title').toLowerCase().includes(filterValue)) { | 762 | const title = $bg.attr('title') || ''; |
| 695 | $bgContent.show(); | 763 | const hasMatch = title.toLowerCase().includes(filterValue); |
| 696 | } else { | 764 | $bg.toggle(hasMatch); |
| 697 | $bgContent.hide(); | ||
| 698 | } | ||
| 699 | }); | 765 | }); |
| 700 | } | 766 | } |
| 701 | 767 | ||
| 768 | const debouncedOnBackgroundFilterInput = debounce(onBackgroundFilterInput, debounce_timeout.standard); | ||
| 769 | |||
| 702 | export function initBackgrounds() { | 770 | export function initBackgrounds() { |
| 703 | eventSource.on(event_types.CHAT_CHANGED, onChatChanged); | 771 | eventSource.on(event_types.CHAT_CHANGED, onChatChanged); |
| 704 | eventSource.on(event_types.FORCE_SET_BACKGROUND, forceSetBackground); | 772 | eventSource.on(event_types.FORCE_SET_BACKGROUND, forceSetBackground); |
| @@ -715,6 +783,11 @@ export function initBackgrounds() { | |||
| 715 | $context.addClass('mobile-menu-open'); | 783 | $context.addClass('mobile-menu-open'); |
| 716 | } | 784 | } |
| 717 | }) | 785 | }) |
| 786 | .off('blur', '.bg_example.mobile-menu-open').on('blur', '.bg_example.mobile-menu-open', function () { | ||
| 787 | if (!$(this).is(':focus-within')) { | ||
| 788 | $(this).removeClass('mobile-menu-open'); | ||
| 789 | } | ||
| 790 | }) | ||
| 718 | .off('click', '.jg-button').on('click', '.jg-button', function (e) { | 791 | .off('click', '.jg-button').on('click', '.jg-button', function (e) { |
| 719 | e.stopPropagation(); | 792 | e.stopPropagation(); |
| 720 | const action = $(this).data('action'); | 793 | const action = $(this).data('action'); |
| @@ -738,9 +811,15 @@ export function initBackgrounds() { | |||
| 738 | } | 811 | } |
| 739 | }); | 812 | }); |
| 740 | 813 | ||
| 814 | $('#bg_thumb_zoom_in').on('click', () => { | ||
| 815 | applyThumbnailColumns(background_settings.thumbnailColumns - 1); | ||
| 816 | }); | ||
| 817 | $('#bg_thumb_zoom_out').on('click', () => { | ||
| 818 | applyThumbnailColumns(background_settings.thumbnailColumns + 1); | ||
| 819 | }); | ||
| 741 | $('#auto_background').on('click', autoBackgroundCommand); | 820 | $('#auto_background').on('click', autoBackgroundCommand); |
| 742 | $('#add_bg_button').on('change', onBackgroundUploadSelected); | 821 | $('#add_bg_button').on('change', onBackgroundUploadSelected); |
| 743 | $('#bg-filter').on('input', onBackgroundFilterInput); | 822 | $('#bg-filter').on('input', () => debouncedOnBackgroundFilterInput()); |
| 744 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | 823 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 745 | name: 'lockbg', | 824 | name: 'lockbg', |
| 746 | callback: () => onLockBackgroundClick(new CustomEvent('click')), | 825 | callback: () => onLockBackgroundClick(new CustomEvent('click')), |
| @@ -9,7 +9,7 @@ | |||
| 9 | <label for="caption_source" data-i18n="Source">Source</label> | 9 | <label for="caption_source" data-i18n="Source">Source</label> |
| 10 | <select id="caption_source" class="text_pole"> | 10 | <select id="caption_source" class="text_pole"> |
| 11 | <option value="local" data-i18n="Local">Local</option> | 11 | <option value="local" data-i18n="Local">Local</option> |
| 12 | <option value="multimodal" data-i18n="Multimodal (OpenAI / Anthropic / llama / Google)">Multimodal (OpenAI / Anthropic / llama / Google)</option> | 12 | <option value="multimodal" data-i18n="Multimodal">Multimodal</option> |
| 13 | <option value="extras" data-i18n="Extras">Extras (deprecated)</option> | 13 | <option value="extras" data-i18n="Extras">Extras (deprecated)</option> |
| 14 | <option value="horde" data-i18n="Horde">Horde</option> | 14 | <option value="horde" data-i18n="Horde">Horde</option> |
| 15 | </select> | 15 | </select> |
| @@ -18,7 +18,7 @@ | |||
| 18 | <label for="caption_multimodal_api" data-i18n="API">API</label> | 18 | <label for="caption_multimodal_api" data-i18n="API">API</label> |
| 19 | <select id="caption_multimodal_api" class="flex1 text_pole"> | 19 | <select id="caption_multimodal_api" class="flex1 text_pole"> |
| 20 | <option value="aimlapi">AI/ML API</option> | 20 | <option value="aimlapi">AI/ML API</option> |
| 21 | <option value="anthropic">Anthropic</option> | 21 | <option value="anthropic">Claude</option> |
| 22 | <option value="cohere">Cohere</option> | 22 | <option value="cohere">Cohere</option> |
| 23 | <option value="custom" data-i18n="Custom (OpenAI-compatible)">Custom (OpenAI-compatible)</option> | 23 | <option value="custom" data-i18n="Custom (OpenAI-compatible)">Custom (OpenAI-compatible)</option> |
| 24 | <option value="electronhub">Electron Hub</option> | 24 | <option value="electronhub">Electron Hub</option> |
| @@ -0,0 +1,455 @@ | |||
| 1 | import { event_types, eventSource, getRequestHeaders } from '../../../script.js'; | ||
| 2 | import { SECRET_KEYS, secret_state } from '../../secrets.js'; | ||
| 3 | import { getPreviewString, saveTtsProviderSettings, initVoiceMap } from './index.js'; | ||
| 4 | |||
| 5 | export { ElectronHubTtsProvider }; | ||
| 6 | |||
| 7 | class ElectronHubTtsProvider { | ||
| 8 | settings; | ||
| 9 | voices = []; | ||
| 10 | models = []; | ||
| 11 | separator = ' . '; | ||
| 12 | audioElement = document.createElement('audio'); | ||
| 13 | |||
| 14 | defaultSettings = { | ||
| 15 | voiceMap: {}, | ||
| 16 | model: 'tts-1', | ||
| 17 | speed: 1, | ||
| 18 | temperature: 1, | ||
| 19 | top_p: 1, | ||
| 20 | // GPT-4o Mini TTS | ||
| 21 | instructions: '', | ||
| 22 | // Dia | ||
| 23 | speaker_transcript: '', | ||
| 24 | cfg_filter_top_k: 25, | ||
| 25 | cfg_scale: 3, | ||
| 26 | // Microsoft TTS | ||
| 27 | speech_rate: 0, | ||
| 28 | pitch_adjustment: 0, | ||
| 29 | emotional_style: '', | ||
| 30 | }; | ||
| 31 | |||
| 32 | get settingsHtml() { | ||
| 33 | let html = ` | ||
| 34 | <div>Electron Hub unified TTS API.</div> | ||
| 35 | <div class="flex-container alignItemsCenter"> | ||
| 36 | <div class="flex1"></div> | ||
| 37 | <div id="electronhub_tts_key" class="menu_button menu_button_icon manage-api-keys" data-key="api_key_electronhub"> | ||
| 38 | <i class="fa-solid fa-key"></i> | ||
| 39 | <span>API Key</span> | ||
| 40 | </div> | ||
| 41 | </div> | ||
| 42 | <div class="flex-container flexGap10 wrap"> | ||
| 43 | <div class="flex1"> | ||
| 44 | <label for="electronhub_tts_model">Model</label> | ||
| 45 | <select id="electronhub_tts_model" class="text_pole"></select> | ||
| 46 | </div> | ||
| 47 | <div> | ||
| 48 | <label for="electronhub_tts_speed">Speed <span id="electronhub_tts_speed_output"></span></label> | ||
| 49 | <input type="range" id="electronhub_tts_speed" value="1" min="0.25" max="4" step="0.05"> | ||
| 50 | </div> | ||
| 51 | <div> | ||
| 52 | <label for="electronhub_tts_temperature">Temperature</label> | ||
| 53 | <input id="electronhub_tts_temperature" class="text_pole" type="number" min="0" max="2" step="0.1" value="1" /> | ||
| 54 | </div> | ||
| 55 | <div id="electronhub_block_top_p" style="display:none;"> | ||
| 56 | <label for="electronhub_tts_top_p">Top-p</label> | ||
| 57 | <input id="electronhub_tts_top_p" class="text_pole" type="number" min="0" max="1" step="0.01" value="1" /> | ||
| 58 | </div> | ||
| 59 | </div> | ||
| 60 | |||
| 61 | <div id="electronhub_block_instructions" style="display:none;"> | ||
| 62 | <label for="electronhub_tts_instructions">Instructions (GPT-4o Mini TTS):</label> | ||
| 63 | <textarea id="electronhub_tts_instructions" class="textarea_compact autoSetHeight" placeholder="e.g., 'Speak cheerfully and energetically'"></textarea> | ||
| 64 | </div> | ||
| 65 | |||
| 66 | <div id="electronhub_block_dia" style="display:none;"> | ||
| 67 | <label for="electronhub_tts_speaker_transcript">Speaker transcript (Dia):</label> | ||
| 68 | <textarea id="electronhub_tts_speaker_transcript" class="textarea_compact autoSetHeight" maxlength="1000"></textarea> | ||
| 69 | <label for="electronhub_tts_cfg_scale">CFG scale (1-5):</label> | ||
| 70 | <input id="electronhub_tts_cfg_scale" type="number" min="1" max="5" step="1" /> | ||
| 71 | <label for="electronhub_tts_cfg_topk">CFG filter top_k (15-50):</label> | ||
| 72 | <input id="electronhub_tts_cfg_topk" type="number" min="15" max="50" step="1" /> | ||
| 73 | </div> | ||
| 74 | |||
| 75 | <div id="electronhub_block_msft" style="display:none;"> | ||
| 76 | <div class="flex-container flexGap10 wrap"> | ||
| 77 | <div> | ||
| 78 | <label for="electronhub_tts_speech_rate">Speech rate (-100..100)</label> | ||
| 79 | <input id="electronhub_tts_speech_rate" class="text_pole" type="number" min="-100" max="100" step="1" style="width:120px;" /> | ||
| 80 | </div> | ||
| 81 | <div> | ||
| 82 | <label for="electronhub_tts_pitch_adjustment">Pitch adjustment (-100..100)</label> | ||
| 83 | <input id="electronhub_tts_pitch_adjustment" class="text_pole" type="number" min="-100" max="100" step="1" style="width:120px;" /> | ||
| 84 | </div> | ||
| 85 | </div> | ||
| 86 | <div class="flex-container flexGap10"> | ||
| 87 | <div class="flex1"> | ||
| 88 | <label for="electronhub_tts_emotional_style">Emotional style</label> | ||
| 89 | <input id="electronhub_tts_emotional_style" class="text_pole" type="text" placeholder="cheerful, sad, angry, gentle..." /> | ||
| 90 | </div> | ||
| 91 | </div> | ||
| 92 | </div> | ||
| 93 | |||
| 94 | <div id="electronhub_dynamic_params" class="flex-container flexGap10 wrap" style="display:none;"></div>`; | ||
| 95 | return html; | ||
| 96 | } | ||
| 97 | |||
| 98 | constructor() { | ||
| 99 | this.handler = async function (/** @type {string} */ key) { | ||
| 100 | if (key !== SECRET_KEYS.ELECTRONHUB) return; | ||
| 101 | $('#electronhub_tts_key').toggleClass('success', !!secret_state[SECRET_KEYS.ELECTRONHUB]); | ||
| 102 | await this.onRefreshClick(); | ||
| 103 | }.bind(this); | ||
| 104 | } | ||
| 105 | |||
| 106 | dispose() { | ||
| 107 | [event_types.SECRET_WRITTEN, event_types.SECRET_DELETED, event_types.SECRET_ROTATED].forEach(event => { | ||
| 108 | eventSource.removeListener(event, this.handler); | ||
| 109 | }); | ||
| 110 | } | ||
| 111 | |||
| 112 | async loadSettings(settings) { | ||
| 113 | if (Object.keys(settings).length == 0) { | ||
| 114 | console.info('Using default Electron Hub TTS settings'); | ||
| 115 | } | ||
| 116 | |||
| 117 | this.settings = { ...this.defaultSettings, ...settings }; | ||
| 118 | |||
| 119 | await this.loadModels(); | ||
| 120 | this.populateModelSelect(); | ||
| 121 | |||
| 122 | $('#electronhub_tts_model').val(this.settings.model); | ||
| 123 | $('#electronhub_tts_model').on('change', () => { this.onSettingsChange(); }); | ||
| 124 | |||
| 125 | $('#electronhub_tts_speed').val(this.settings.speed); | ||
| 126 | $('#electronhub_tts_speed_output').text(this.settings.speed); | ||
| 127 | $('#electronhub_tts_speed').on('input', () => { this.onSettingsChange(); }); | ||
| 128 | |||
| 129 | $('#electronhub_tts_temperature').val(this.settings.temperature); | ||
| 130 | $('#electronhub_tts_temperature').on('input', () => { this.onSettingsChange(); }); | ||
| 131 | |||
| 132 | $('#electronhub_tts_top_p').val(this.settings.top_p); | ||
| 133 | $('#electronhub_tts_top_p').on('input', () => { this.onSettingsChange(); }); | ||
| 134 | |||
| 135 | $('#electronhub_tts_instructions').val(this.settings.instructions); | ||
| 136 | $('#electronhub_tts_instructions').on('input', () => { this.onSettingsChange(); }); | ||
| 137 | |||
| 138 | $('#electronhub_tts_speaker_transcript').val(this.settings.speaker_transcript); | ||
| 139 | $('#electronhub_tts_speaker_transcript').on('input', () => { this.onSettingsChange(); }); | ||
| 140 | $('#electronhub_tts_cfg_scale').val(this.settings.cfg_scale); | ||
| 141 | $('#electronhub_tts_cfg_scale').on('input', () => { this.onSettingsChange(); }); | ||
| 142 | $('#electronhub_tts_cfg_topk').val(this.settings.cfg_filter_top_k); | ||
| 143 | $('#electronhub_tts_cfg_topk').on('input', () => { this.onSettingsChange(); }); | ||
| 144 | |||
| 145 | $('#electronhub_tts_speech_rate').val(this.settings.speech_rate); | ||
| 146 | $('#electronhub_tts_speech_rate').on('input', () => { this.onSettingsChange(); }); | ||
| 147 | $('#electronhub_tts_pitch_adjustment').val(this.settings.pitch_adjustment); | ||
| 148 | $('#electronhub_tts_pitch_adjustment').on('input', () => { this.onSettingsChange(); }); | ||
| 149 | $('#electronhub_tts_emotional_style').val(this.settings.emotional_style); | ||
| 150 | $('#electronhub_tts_emotional_style').on('input', () => { this.onSettingsChange(); }); | ||
| 151 | |||
| 152 | $('#electronhub_tts_key').toggleClass('success', !!secret_state[SECRET_KEYS.ELECTRONHUB]); | ||
| 153 | [event_types.SECRET_WRITTEN, event_types.SECRET_DELETED, event_types.SECRET_ROTATED].forEach(event => { | ||
| 154 | eventSource.on(event, this.handler); | ||
| 155 | }); | ||
| 156 | |||
| 157 | await this.checkReady(); | ||
| 158 | this.updateConditionalBlocks(); | ||
| 159 | this.renderDynamicParams(); | ||
| 160 | console.debug('Electron Hub TTS: Settings loaded'); | ||
| 161 | } | ||
| 162 | |||
| 163 | async onSettingsChange() { | ||
| 164 | const previousModel = this.settings.model; | ||
| 165 | this.settings.model = String($('#electronhub_tts_model').find(':selected').val() || this.settings.model); | ||
| 166 | this.settings.speed = Number($('#electronhub_tts_speed').val()); | ||
| 167 | $('#electronhub_tts_speed_output').text(this.settings.speed); | ||
| 168 | this.settings.temperature = Number($('#electronhub_tts_temperature').val()); | ||
| 169 | this.settings.top_p = Number($('#electronhub_tts_top_p').val()); | ||
| 170 | this.settings.instructions = String($('#electronhub_tts_instructions').val() || ''); | ||
| 171 | this.settings.speaker_transcript = String($('#electronhub_tts_speaker_transcript').val() || ''); | ||
| 172 | this.settings.cfg_scale = Number($('#electronhub_tts_cfg_scale').val()); | ||
| 173 | this.settings.cfg_filter_top_k = Number($('#electronhub_tts_cfg_topk').val()); | ||
| 174 | this.settings.speech_rate = Number($('#electronhub_tts_speech_rate').val()); | ||
| 175 | this.settings.pitch_adjustment = Number($('#electronhub_tts_pitch_adjustment').val()); | ||
| 176 | this.settings.emotional_style = String($('#electronhub_tts_emotional_style').val() || ''); | ||
| 177 | this.updateConditionalBlocks(); | ||
| 178 | this.renderDynamicParams(); | ||
| 179 | saveTtsProviderSettings(); | ||
| 180 | if (previousModel !== this.settings.model) { | ||
| 181 | this.voices = await this.fetchTtsVoiceObjects(); | ||
| 182 | await initVoiceMap(); | ||
| 183 | } | ||
| 184 | } | ||
| 185 | |||
| 186 | async loadModels() { | ||
| 187 | try { | ||
| 188 | const response = await fetch('/api/openai/electronhub/models', { | ||
| 189 | method: 'POST', | ||
| 190 | headers: getRequestHeaders(), | ||
| 191 | }); | ||
| 192 | if (!response.ok) { | ||
| 193 | throw new Error(`HTTP ${response.status}: ${await response.text()}`); | ||
| 194 | } | ||
| 195 | /** @type {Array<any>} */ | ||
| 196 | const data = await response.json(); | ||
| 197 | const allModels = Array.isArray(data) ? data : []; | ||
| 198 | const ttsModels = allModels.filter(m => { | ||
| 199 | const eps = Array.isArray(m?.endpoints) ? m.endpoints : []; | ||
| 200 | return eps.some(ep => { | ||
| 201 | if (typeof ep !== 'string') return false; | ||
| 202 | return ep === '/v1/audio/speech' || ep.endsWith('/audio/speech') || ep === 'audio/speech'; | ||
| 203 | }); | ||
| 204 | }); | ||
| 205 | |||
| 206 | this.models = ttsModels; | ||
| 207 | |||
| 208 | if (this.models.length > 0 && !this.models.find(m => m.id === this.settings.model)) { | ||
| 209 | this.settings.model = this.models[0].id; | ||
| 210 | saveTtsProviderSettings(); | ||
| 211 | } | ||
| 212 | } catch (err) { | ||
| 213 | console.warn('Electron Hub models fetch failed', err); | ||
| 214 | this.models = []; | ||
| 215 | } | ||
| 216 | } | ||
| 217 | |||
| 218 | populateModelSelect() { | ||
| 219 | const select = $('#electronhub_tts_model'); | ||
| 220 | select.empty(); | ||
| 221 | const groups = this.groupByVendor(this.models); | ||
| 222 | for (const [vendor, models] of groups.entries()) { | ||
| 223 | const optgroup = document.createElement('optgroup'); | ||
| 224 | optgroup.label = vendor; | ||
| 225 | for (const m of models) { | ||
| 226 | const opt = document.createElement('option'); | ||
| 227 | opt.value = m.id; | ||
| 228 | opt.text = m.name || m.id; | ||
| 229 | optgroup.appendChild(opt); | ||
| 230 | } | ||
| 231 | select.append(optgroup); | ||
| 232 | } | ||
| 233 | |||
| 234 | if (this.models.find(x => x.id === this.settings.model)) { | ||
| 235 | select.val(this.settings.model); | ||
| 236 | } | ||
| 237 | } | ||
| 238 | |||
| 239 | /** | ||
| 240 | * Group models by vendor prefix from name before ':' | ||
| 241 | * @param {Array<any>} array | ||
| 242 | * @returns {Map<string, any[]>} | ||
| 243 | */ | ||
| 244 | groupByVendor(array) { | ||
| 245 | return array.reduce((acc, curr) => { | ||
| 246 | const name = String(curr?.name || curr?.id || 'Other'); | ||
| 247 | const vendor = name.split(':')[0].trim() || 'Other'; | ||
| 248 | if (!acc.has(vendor)) acc.set(vendor, []); | ||
| 249 | acc.get(vendor).push(curr); | ||
| 250 | return acc; | ||
| 251 | }, new Map()); | ||
| 252 | } | ||
| 253 | |||
| 254 | updateConditionalBlocks() { | ||
| 255 | const modelId = this.settings.model; | ||
| 256 | const model = this.models.find(m => m.id === modelId); | ||
| 257 | const params = model?.parameters || {}; | ||
| 258 | const vendorName = String(model?.name || '').split(':')[0].trim().toLowerCase(); | ||
| 259 | |||
| 260 | const hasInstructions = 'instructions' in params || modelId === 'gpt-4o-mini-tts'; | ||
| 261 | const hasDia = 'speaker_transcript' in params || 'cfg_scale' in params || 'cfg_filter_top_k' in params || modelId.includes('dia'); | ||
| 262 | |||
| 263 | const hasMsft = 'speech_rate' in params || 'pitch_adjustment' in params || 'emotional_style' in params || vendorName === 'microsoft' || modelId === 'microsoft-tts'; | ||
| 264 | const hasTopP = 'top_p' in params; | ||
| 265 | |||
| 266 | $('#electronhub_block_instructions').toggle(!!hasInstructions); | ||
| 267 | $('#electronhub_block_dia').toggle(!!hasDia); | ||
| 268 | $('#electronhub_block_msft').toggle(!!hasMsft); | ||
| 269 | $('#electronhub_block_top_p').toggle(!!hasTopP); | ||
| 270 | } | ||
| 271 | |||
| 272 | /** | ||
| 273 | * Build UI for additional model parameters dynamically | ||
| 274 | */ | ||
| 275 | renderDynamicParams() { | ||
| 276 | const container = $('#electronhub_dynamic_params'); | ||
| 277 | container.empty(); | ||
| 278 | const model = this.models.find(m => m.id === this.settings.model); | ||
| 279 | const params = model?.parameters || {}; | ||
| 280 | const modelHasVoices = Array.isArray(model?.voices) && model.voices.length > 0; | ||
| 281 | const exclude = new Set(['input', 'response_format', 'model', 'speed', 'temperature', 'top_p', 'instructions', 'speaker_transcript', 'cfg_scale', 'cfg_filter_top_k', 'speech_rate', 'pitch_adjustment', 'emotional_style']); | ||
| 282 | if (modelHasVoices) exclude.add('voice'); | ||
| 283 | |||
| 284 | const entries = Object.entries(params).filter(([k]) => !exclude.has(k)); | ||
| 285 | container.toggle(entries.length > 0); | ||
| 286 | if (entries.length === 0) return; | ||
| 287 | |||
| 288 | for (const [key, spec] of entries) { | ||
| 289 | const nice = key.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()); | ||
| 290 | const type = String(spec?.type || 'string'); | ||
| 291 | const id = `electronhub_dyn_${key.replace(/[^a-zA-Z0-9_-]/g, '_')}`; | ||
| 292 | |||
| 293 | if (Array.isArray(spec?.enum) && spec.enum.length) { | ||
| 294 | const select = $(`<div><label for="${id}">${nice}</label><select id="${id}" class="text_pole"></select></div>`); | ||
| 295 | container.append(select); | ||
| 296 | const el = select.find('select'); | ||
| 297 | for (const opt of spec.enum) el.append(new Option(String(opt), String(opt))); | ||
| 298 | const val = this.settings[key] ?? spec.default ?? spec.enum[0]; | ||
| 299 | el.val(String(val)); | ||
| 300 | el.on('change', () => { this.settings[key] = String(el.val() || ''); saveTtsProviderSettings(); }); | ||
| 301 | continue; | ||
| 302 | } | ||
| 303 | |||
| 304 | if (type === 'boolean') { | ||
| 305 | const block = $(`<label class="checkbox_label" for="${id}"><input type="checkbox" id="${id}"> <small>${nice}</small></label>`); | ||
| 306 | container.append(block); | ||
| 307 | const el = block.find('input'); | ||
| 308 | el.prop('checked', !!(this.settings[key] ?? spec.default ?? false)); | ||
| 309 | el.on('change', () => { this.settings[key] = !!el.is(':checked'); saveTtsProviderSettings(); }); | ||
| 310 | continue; | ||
| 311 | } | ||
| 312 | |||
| 313 | if (type === 'number' || type === 'integer') { | ||
| 314 | const min = spec.minimum ?? undefined; | ||
| 315 | const max = spec.maximum ?? undefined; | ||
| 316 | const step = type === 'integer' ? 1 : (spec.step ?? 0.01); | ||
| 317 | const block = $(`<div><label for="${id}">${nice}${(min != null || max != null) ? ` (${min ?? ''}..${max ?? ''})` : ''}:</label><input id="${id}" type="number" class="text_pole" ${min != null ? `min="${min}"` : ''} ${max != null ? `max="${max}"` : ''} step="${step}"></div>`); | ||
| 318 | container.append(block); | ||
| 319 | const el = block.find('input'); | ||
| 320 | const val = this.settings[key] ?? spec.default ?? ''; | ||
| 321 | if (val !== '') el.val(val); | ||
| 322 | el.on('input', () => { | ||
| 323 | const raw = el.val(); | ||
| 324 | this.settings[key] = (raw === '') ? '' : Number(raw); | ||
| 325 | saveTtsProviderSettings(); | ||
| 326 | }); | ||
| 327 | continue; | ||
| 328 | } | ||
| 329 | |||
| 330 | const isLong = /instructions|transcript|style|prompt|description/i.test(key); | ||
| 331 | if (isLong) { | ||
| 332 | const block = $(`<div><label for="${id}">${nice}</label><textarea id="${id}" class="textarea_compact autoSetHeight"></textarea></div>`); | ||
| 333 | container.append(block); | ||
| 334 | const el = block.find('textarea'); | ||
| 335 | el.val(String(this.settings[key] ?? spec.default ?? '')); | ||
| 336 | el.on('input', () => { this.settings[key] = String(el.val() || ''); saveTtsProviderSettings(); }); | ||
| 337 | } else { | ||
| 338 | const block = $(`<div><label for="${id}">${nice}</label><input id="${id}" type="text" class="text_pole" /></div>`); | ||
| 339 | container.append(block); | ||
| 340 | const el = block.find('input'); | ||
| 341 | el.val(String(this.settings[key] ?? spec.default ?? '')); | ||
| 342 | el.on('input', () => { this.settings[key] = String(el.val() || ''); saveTtsProviderSettings(); }); | ||
| 343 | } | ||
| 344 | } | ||
| 345 | } | ||
| 346 | |||
| 347 | async checkReady() { | ||
| 348 | this.voices = await this.fetchTtsVoiceObjects(); | ||
| 349 | } | ||
| 350 | |||
| 351 | async onRefreshClick() { | ||
| 352 | await this.loadModels(); | ||
| 353 | this.populateModelSelect(); | ||
| 354 | this.voices = await this.fetchTtsVoiceObjects(); | ||
| 355 | this.updateConditionalBlocks(); | ||
| 356 | this.renderDynamicParams(); | ||
| 357 | saveTtsProviderSettings(); | ||
| 358 | } | ||
| 359 | |||
| 360 | async getVoice(voiceName) { | ||
| 361 | if (this.voices.length == 0) { | ||
| 362 | this.voices = await this.fetchTtsVoiceObjects(); | ||
| 363 | } | ||
| 364 | const match = this.voices.filter(v => v.name == voiceName)[0]; | ||
| 365 | if (!match) { | ||
| 366 | throw `TTS Voice name ${voiceName} not found`; | ||
| 367 | } | ||
| 368 | return match; | ||
| 369 | } | ||
| 370 | |||
| 371 | async generateTts(text, voiceId) { | ||
| 372 | const response = await this.fetchTtsGeneration(text, voiceId); | ||
| 373 | return response; | ||
| 374 | } | ||
| 375 | |||
| 376 | async fetchTtsVoiceObjects() { | ||
| 377 | const modelId = this.settings.model; | ||
| 378 | const model = this.models.find(m => m.id === modelId); | ||
| 379 | if (model && Array.isArray(model.voices) && model.voices.length) { | ||
| 380 | return model.voices.map(name => ({ name, voice_id: name, lang: 'en-US' })); | ||
| 381 | } | ||
| 382 | // Fallback to common OpenAI voices | ||
| 383 | const fallback = ['alloy', 'ash', 'ballad', 'coral', 'echo', 'fable', 'onyx', 'nova', 'sage', 'shimmer', 'verse']; | ||
| 384 | return fallback.map(name => ({ name, voice_id: name, lang: 'en-US' })); | ||
| 385 | } | ||
| 386 | |||
| 387 | async previewTtsVoice(voiceId) { | ||
| 388 | this.audioElement.pause(); | ||
| 389 | this.audioElement.currentTime = 0; | ||
| 390 | const text = getPreviewString('en-US'); | ||
| 391 | const response = await this.fetchTtsGeneration(text, voiceId); | ||
| 392 | if (!response.ok) { | ||
| 393 | throw new Error(`HTTP ${response.status}`); | ||
| 394 | } | ||
| 395 | const audio = await response.blob(); | ||
| 396 | const url = URL.createObjectURL(audio); | ||
| 397 | this.audioElement.src = url; | ||
| 398 | this.audioElement.play(); | ||
| 399 | this.audioElement.onended = () => URL.revokeObjectURL(url); | ||
| 400 | } | ||
| 401 | |||
| 402 | async fetchTtsGeneration(inputText, voiceId) { | ||
| 403 | console.info(`Generating Electron Hub TTS for voice_id ${voiceId}`); | ||
| 404 | const body = { | ||
| 405 | input: inputText, | ||
| 406 | voice: voiceId, | ||
| 407 | speed: this.settings.speed, | ||
| 408 | temperature: this.settings.temperature, | ||
| 409 | model: this.settings.model, | ||
| 410 | }; | ||
| 411 | |||
| 412 | const model = (this.settings.model || '').toLowerCase(); | ||
| 413 | if (model === 'gpt-4o-mini-tts') { | ||
| 414 | if (this.settings.instructions?.trim()) body.instructions = this.settings.instructions.trim(); | ||
| 415 | } | ||
| 416 | if (model.includes('dia')) { | ||
| 417 | if (this.settings.speaker_transcript?.trim()) body.speaker_transcript = this.settings.speaker_transcript.trim(); | ||
| 418 | if (Number.isFinite(this.settings.cfg_scale)) body.cfg_scale = Number(this.settings.cfg_scale); | ||
| 419 | if (Number.isFinite(this.settings.cfg_filter_top_k)) body.cfg_filter_top_k = Number(this.settings.cfg_filter_top_k); | ||
| 420 | } | ||
| 421 | if (model.includes('microsoft-tts')) { | ||
| 422 | if (Number.isFinite(this.settings.speech_rate)) body.speech_rate = Number(this.settings.speech_rate); | ||
| 423 | if (Number.isFinite(this.settings.pitch_adjustment)) body.pitch_adjustment = Number(this.settings.pitch_adjustment); | ||
| 424 | if ((this.settings.emotional_style || '').trim()) body.emotional_style = String(this.settings.emotional_style).trim(); | ||
| 425 | } | ||
| 426 | if (Number.isFinite(this.settings.top_p)) { | ||
| 427 | body.top_p = Number(this.settings.top_p); | ||
| 428 | } | ||
| 429 | |||
| 430 | // add dynamic params based on schema | ||
| 431 | const modelObj = this.models.find(m => m.id === this.settings.model); | ||
| 432 | const params = modelObj?.parameters || {}; | ||
| 433 | const modelHasVoices = Array.isArray(modelObj?.voices) && modelObj.voices.length > 0; | ||
| 434 | const exclude = new Set(['input', 'response_format', 'model', 'speed', 'temperature', 'top_p', 'instructions', 'speaker_transcript', 'cfg_scale', 'cfg_filter_top_k', 'speech_rate', 'pitch_adjustment', 'emotional_style']); | ||
| 435 | if (modelHasVoices) exclude.add('voice'); | ||
| 436 | for (const key of Object.keys(params)) { | ||
| 437 | if (exclude.has(key)) continue; | ||
| 438 | const val = this.settings[key]; | ||
| 439 | if (val === undefined || val === '') continue; | ||
| 440 | body[key] = val; | ||
| 441 | } | ||
| 442 | |||
| 443 | const response = await fetch('/api/openai/electronhub/generate-voice', { | ||
| 444 | method: 'POST', | ||
| 445 | headers: getRequestHeaders(), | ||
| 446 | body: JSON.stringify(body), | ||
| 447 | }); | ||
| 448 | |||
| 449 | if (!response.ok) { | ||
| 450 | throw new Error(`HTTP ${response.status}: ${await response.text()}`); | ||
| 451 | } | ||
| 452 | |||
| 453 | return response; | ||
| 454 | } | ||
| 455 | } | ||
| @@ -33,6 +33,7 @@ import { KokoroTtsProvider } from './kokoro.js'; | |||
| 33 | import { TtsWebuiProvider } from './tts-webui.js'; | 33 | import { TtsWebuiProvider } from './tts-webui.js'; |
| 34 | import { PollinationsTtsProvider } from './pollinations.js'; | 34 | import { PollinationsTtsProvider } from './pollinations.js'; |
| 35 | import { MiniMaxTtsProvider } from './minimax.js'; | 35 | import { MiniMaxTtsProvider } from './minimax.js'; |
| 36 | import { ElectronHubTtsProvider } from './electronhub.js'; | ||
| 36 | 37 | ||
| 37 | const UPDATE_INTERVAL = 1000; | 38 | const UPDATE_INTERVAL = 1000; |
| 38 | const wrapper = new ModuleWorkerWrapper(moduleWorker); | 39 | const wrapper = new ModuleWorkerWrapper(moduleWorker); |
| @@ -123,6 +124,7 @@ const ttsProviders = { | |||
| 123 | 'CosyVoice (Unofficial)': CosyVoiceProvider, | 124 | 'CosyVoice (Unofficial)': CosyVoiceProvider, |
| 124 | Edge: EdgeTtsProvider, | 125 | Edge: EdgeTtsProvider, |
| 125 | ElevenLabs: ElevenLabsTtsProvider, | 126 | ElevenLabs: ElevenLabsTtsProvider, |
| 127 | 'Electron Hub': ElectronHubTtsProvider, | ||
| 126 | 'Google Translate': GoogleTranslateTtsProvider, | 128 | 'Google Translate': GoogleTranslateTtsProvider, |
| 127 | 'Google Gemini TTS': GoogleNativeTtsProvider, | 129 | 'Google Gemini TTS': GoogleNativeTtsProvider, |
| 128 | GSVI: GSVITtsProvider, | 130 | GSVI: GSVITtsProvider, |
| @@ -276,6 +276,8 @@ export const settingsToUpdate = { | |||
| 276 | perplexity_model: ['#model_perplexity_select', 'perplexity_model', false, true], | 276 | perplexity_model: ['#model_perplexity_select', 'perplexity_model', false, true], |
| 277 | groq_model: ['#model_groq_select', 'groq_model', false, true], | 277 | groq_model: ['#model_groq_select', 'groq_model', false, true], |
| 278 | electronhub_model: ['#model_electronhub_select', 'electronhub_model', false, true], | 278 | electronhub_model: ['#model_electronhub_select', 'electronhub_model', false, true], |
| 279 | electronhub_sort_models: ['#electronhub_sort_models', 'electronhub_sort_models', false, true], | ||
| 280 | electronhub_group_models: ['#electronhub_group_models', 'electronhub_group_models', false, true], | ||
| 279 | nanogpt_model: ['#model_nanogpt_select', 'nanogpt_model', false, true], | 281 | nanogpt_model: ['#model_nanogpt_select', 'nanogpt_model', false, true], |
| 280 | deepseek_model: ['#model_deepseek_select', 'deepseek_model', false, true], | 282 | deepseek_model: ['#model_deepseek_select', 'deepseek_model', false, true], |
| 281 | aimlapi_model: ['#model_aimlapi_select', 'aimlapi_model', false, true], | 283 | aimlapi_model: ['#model_aimlapi_select', 'aimlapi_model', false, true], |
| @@ -379,6 +381,8 @@ const default_settings = { | |||
| 379 | perplexity_model: 'sonar-pro', | 381 | perplexity_model: 'sonar-pro', |
| 380 | groq_model: 'llama-3.3-70b-versatile', | 382 | groq_model: 'llama-3.3-70b-versatile', |
| 381 | electronhub_model: 'gpt-4o-mini', | 383 | electronhub_model: 'gpt-4o-mini', |
| 384 | electronhub_sort_models: 'alphabetically', | ||
| 385 | electronhub_group_models: false, | ||
| 382 | nanogpt_model: 'gpt-4o-mini', | 386 | nanogpt_model: 'gpt-4o-mini', |
| 383 | deepseek_model: 'deepseek-chat', | 387 | deepseek_model: 'deepseek-chat', |
| 384 | aimlapi_model: 'gpt-4o-mini-2024-07-18', | 388 | aimlapi_model: 'gpt-4o-mini-2024-07-18', |
| @@ -473,6 +477,8 @@ const oai_settings = { | |||
| 473 | perplexity_model: 'sonar-pro', | 477 | perplexity_model: 'sonar-pro', |
| 474 | groq_model: 'llama-3.1-70b-versatile', | 478 | groq_model: 'llama-3.1-70b-versatile', |
| 475 | electronhub_model: 'gpt-4o-mini', | 479 | electronhub_model: 'gpt-4o-mini', |
| 480 | electronhub_sort_models: 'alphabetically', | ||
| 481 | electronhub_group_models: false, | ||
| 476 | nanogpt_model: 'gpt-4o-mini', | 482 | nanogpt_model: 'gpt-4o-mini', |
| 477 | deepseek_model: 'deepseek-chat', | 483 | deepseek_model: 'deepseek-chat', |
| 478 | aimlapi_model: 'gpt-4-turbo', | 484 | aimlapi_model: 'gpt-4-turbo', |
| @@ -1710,6 +1716,59 @@ function calculateOpenRouterCost() { | |||
| 1710 | $('#openrouter_max_prompt_cost').text(cost); | 1716 | $('#openrouter_max_prompt_cost').text(cost); |
| 1711 | } | 1717 | } |
| 1712 | 1718 | ||
| 1719 | function getElectronHubModelTemplate(option) { | ||
| 1720 | const model = model_list.find(x => x.id === option?.element?.value); | ||
| 1721 | |||
| 1722 | if (!option.id || !model) { | ||
| 1723 | return option.text; | ||
| 1724 | } | ||
| 1725 | |||
| 1726 | const inputPrice = model.pricing?.input; | ||
| 1727 | const outputPrice = model.pricing?.output; | ||
| 1728 | const price = inputPrice && outputPrice ? `$${inputPrice}/$${outputPrice} in/out Mtoken` : 'Unknown'; | ||
| 1729 | |||
| 1730 | const visionIcon = model.metadata?.vision ? '<i class="fa-solid fa-eye fa-sm" title="This model supports vision"></i>' : ''; | ||
| 1731 | const reasoningIcon = model.metadata?.reasoning ? '<i class="fa-solid fa-brain fa-sm" title="This model supports reasoning"></i>' : ''; | ||
| 1732 | const toolCallsIcon = model.metadata?.function_call ? '<i class="fa-solid fa-wrench fa-sm" title="This model supports function tools"></i>' : ''; | ||
| 1733 | const premiumIcon = model?.premium_model ? '<i class="fa-solid fa-crown fa-sm" title="This model requires a subscription"></i>' : ''; | ||
| 1734 | |||
| 1735 | const iconsContainer = document.createElement('span'); | ||
| 1736 | iconsContainer.insertAdjacentHTML('beforeend', visionIcon); | ||
| 1737 | iconsContainer.insertAdjacentHTML('beforeend', reasoningIcon); | ||
| 1738 | iconsContainer.insertAdjacentHTML('beforeend', toolCallsIcon); | ||
| 1739 | iconsContainer.insertAdjacentHTML('beforeend', premiumIcon); | ||
| 1740 | |||
| 1741 | const capabilities = (iconsContainer.children.length) ? ` | ${iconsContainer.innerHTML}` : ''; | ||
| 1742 | |||
| 1743 | return $((` | ||
| 1744 | <div class="flex-container alignItemsBaseline" title="${DOMPurify.sanitize(model.id)}"> | ||
| 1745 | <strong>${DOMPurify.sanitize(model.name)}</strong> | ${model.tokens} ctx | <small>${price}</small>${capabilities} | ||
| 1746 | </div> | ||
| 1747 | `)); | ||
| 1748 | } | ||
| 1749 | |||
| 1750 | function calculateElectronHubCost() { | ||
| 1751 | if (oai_settings.chat_completion_source !== chat_completion_sources.ELECTRONHUB) { | ||
| 1752 | return; | ||
| 1753 | } | ||
| 1754 | |||
| 1755 | let cost = 'Unknown'; | ||
| 1756 | const model = model_list.find(x => x.id === oai_settings.electronhub_model); | ||
| 1757 | |||
| 1758 | if (model?.pricing) { | ||
| 1759 | const outputCost = Number(model.pricing.output / 1000000); | ||
| 1760 | const inputCost = Number(model.pricing.input / 1000000); | ||
| 1761 | const outputTokens = oai_settings.openai_max_tokens; | ||
| 1762 | const inputTokens = (oai_settings.openai_max_context - outputTokens); | ||
| 1763 | const totalCost = (outputCost * outputTokens) + (inputCost * inputTokens); | ||
| 1764 | if (!isNaN(totalCost)) { | ||
| 1765 | cost = '$' + totalCost.toFixed(4); | ||
| 1766 | } | ||
| 1767 | } | ||
| 1768 | |||
| 1769 | $('#electronhub_max_prompt_cost').text(cost); | ||
| 1770 | } | ||
| 1771 | |||
| 1713 | function saveModelList(data) { | 1772 | function saveModelList(data) { |
| 1714 | model_list = data.map((model) => ({ ...model })); | 1773 | model_list = data.map((model) => ({ ...model })); |
| 1715 | model_list.sort((a, b) => a?.id && b?.id && a.id.localeCompare(b.id)); | 1774 | model_list.sort((a, b) => a?.id && b?.id && a.id.localeCompare(b.id)); |
| @@ -1803,16 +1862,14 @@ function saveModelList(data) { | |||
| 1803 | } | 1862 | } |
| 1804 | 1863 | ||
| 1805 | if (oai_settings.chat_completion_source == chat_completion_sources.ELECTRONHUB) { | 1864 | if (oai_settings.chat_completion_source == chat_completion_sources.ELECTRONHUB) { |
| 1865 | model_list = model_list.filter(model => model?.endpoints?.includes('/v1/chat/completions')); | ||
| 1866 | |||
| 1867 | model_list = electronHubSortBy(model_list, oai_settings.electronhub_sort_models); | ||
| 1868 | |||
| 1806 | $('#model_electronhub_select').empty(); | 1869 | $('#model_electronhub_select').empty(); |
| 1807 | model_list.forEach((model) => { | 1870 | |
| 1808 | if (model?.endpoints?.includes('/v1/chat/completions')) { | 1871 | const groupedList = oai_settings.electronhub_group_models ? electronHubGroupByVendor(model_list) : model_list; |
| 1809 | $('#model_electronhub_select').append( | 1872 | appendElectronHubOptions(groupedList, oai_settings.electronhub_group_models); |
| 1810 | $('<option>', { | ||
| 1811 | value: model.id, | ||
| 1812 | text: model.name, | ||
| 1813 | })); | ||
| 1814 | } | ||
| 1815 | }); | ||
| 1816 | 1873 | ||
| 1817 | const selectedModel = model_list.find(model => model.id === oai_settings.electronhub_model); | 1874 | const selectedModel = model_list.find(model => model.id === oai_settings.electronhub_model); |
| 1818 | if (model_list.length > 0 && (!selectedModel || !oai_settings.electronhub_model)) { | 1875 | if (model_list.length > 0 && (!selectedModel || !oai_settings.electronhub_model)) { |
| @@ -2041,6 +2098,61 @@ function openRouterGroupByVendor(array) { | |||
| 2041 | }, new Map()); | 2098 | }, new Map()); |
| 2042 | } | 2099 | } |
| 2043 | 2100 | ||
| 2101 | function appendElectronHubOptions(model_list, groupModels = false) { | ||
| 2102 | const appendOption = (model, parent = null) => { | ||
| 2103 | (parent || $('#model_electronhub_select')).append( | ||
| 2104 | $('<option>', { | ||
| 2105 | value: model.id, | ||
| 2106 | text: model.name, | ||
| 2107 | })); | ||
| 2108 | }; | ||
| 2109 | |||
| 2110 | if (groupModels) { | ||
| 2111 | model_list.forEach((models, vendor) => { | ||
| 2112 | const optgroup = $('<optgroup>').attr('label', vendor); | ||
| 2113 | |||
| 2114 | models.forEach((model) => { | ||
| 2115 | appendOption(model, optgroup); | ||
| 2116 | }); | ||
| 2117 | |||
| 2118 | $('#model_electronhub_select').append(optgroup); | ||
| 2119 | }); | ||
| 2120 | } else { | ||
| 2121 | model_list.forEach((model) => { | ||
| 2122 | appendOption(model); | ||
| 2123 | }); | ||
| 2124 | } | ||
| 2125 | |||
| 2126 | } | ||
| 2127 | |||
| 2128 | function electronHubSortBy(data, property = 'alphabetically') { | ||
| 2129 | return data.sort((a, b) => { | ||
| 2130 | if (property === 'context_length') { | ||
| 2131 | return b.tokens - a.tokens; | ||
| 2132 | } else if (property === 'pricing.input') { | ||
| 2133 | return parseFloat(a.pricing.input) - parseFloat(b.pricing.input); | ||
| 2134 | } else if (property === 'pricing.output') { | ||
| 2135 | return parseFloat(a.pricing.output) - parseFloat(b.pricing.output); | ||
| 2136 | } else { | ||
| 2137 | return a?.name && b?.name && a.name.localeCompare(b.name); | ||
| 2138 | } | ||
| 2139 | }); | ||
| 2140 | } | ||
| 2141 | |||
| 2142 | function electronHubGroupByVendor(array) { | ||
| 2143 | return array.reduce((acc, curr) => { | ||
| 2144 | const vendor = String(curr?.name || curr?.id || 'Other').split(':')[0].trim() || 'Other'; | ||
| 2145 | |||
| 2146 | if (!acc.has(vendor)) { | ||
| 2147 | acc.set(vendor, []); | ||
| 2148 | } | ||
| 2149 | |||
| 2150 | acc.get(vendor).push(curr); | ||
| 2151 | |||
| 2152 | return acc; | ||
| 2153 | }, new Map()); | ||
| 2154 | } | ||
| 2155 | |||
| 2044 | function aimlapiGroupByVendor(array) { | 2156 | function aimlapiGroupByVendor(array) { |
| 2045 | return array.reduce((acc, curr) => { | 2157 | return array.reduce((acc, curr) => { |
| 2046 | const vendor = curr.info.developer; | 2158 | const vendor = curr.info.developer; |
| @@ -2191,7 +2303,7 @@ async function sendOpenAIRequest(type, messages, signal, { jsonSchema = null } = | |||
| 2191 | const useLogprobs = !!power_user.request_token_probabilities; | 2303 | const useLogprobs = !!power_user.request_token_probabilities; |
| 2192 | const canMultiSwipe = oai_settings.n > 1 && !isContinue && !isImpersonate && !isQuiet && (isOAI || isAzureOpenAI || isCustom || isXAI || isAimlapi || isMoonshot); | 2304 | const canMultiSwipe = oai_settings.n > 1 && !isContinue && !isImpersonate && !isQuiet && (isOAI || isAzureOpenAI || isCustom || isXAI || isAimlapi || isMoonshot); |
| 2193 | 2305 | ||
| 2194 | const logitBiasSources = [chat_completion_sources.OPENAI, chat_completion_sources.AZURE_OPENAI, chat_completion_sources.OPENROUTER, chat_completion_sources.CUSTOM]; | 2306 | const logitBiasSources = [chat_completion_sources.OPENAI, chat_completion_sources.AZURE_OPENAI, chat_completion_sources.OPENROUTER, chat_completion_sources.ELECTRONHUB, chat_completion_sources.CUSTOM]; |
| 2195 | if (oai_settings.bias_preset_selected | 2307 | if (oai_settings.bias_preset_selected |
| 2196 | && logitBiasSources.includes(oai_settings.chat_completion_source) | 2308 | && logitBiasSources.includes(oai_settings.chat_completion_source) |
| 2197 | && Array.isArray(oai_settings.bias_presets[oai_settings.bias_preset_selected]) | 2309 | && Array.isArray(oai_settings.bias_presets[oai_settings.bias_preset_selected]) |
| @@ -3523,6 +3635,8 @@ function loadOpenAISettings(data, settings) { | |||
| 3523 | oai_settings.perplexity_model = settings.perplexity_model ?? default_settings.perplexity_model; | 3635 | oai_settings.perplexity_model = settings.perplexity_model ?? default_settings.perplexity_model; |
| 3524 | oai_settings.groq_model = settings.groq_model ?? default_settings.groq_model; | 3636 | oai_settings.groq_model = settings.groq_model ?? default_settings.groq_model; |
| 3525 | oai_settings.electronhub_model = settings.electronhub_model ?? default_settings.electronhub_model; | 3637 | oai_settings.electronhub_model = settings.electronhub_model ?? default_settings.electronhub_model; |
| 3638 | oai_settings.electronhub_sort_models = settings.electronhub_sort_models ?? default_settings.electronhub_sort_models; | ||
| 3639 | oai_settings.electronhub_group_models = settings.electronhub_group_models ?? default_settings.electronhub_group_models; | ||
| 3526 | oai_settings.nanogpt_model = settings.nanogpt_model ?? default_settings.nanogpt_model; | 3640 | oai_settings.nanogpt_model = settings.nanogpt_model ?? default_settings.nanogpt_model; |
| 3527 | oai_settings.deepseek_model = settings.deepseek_model ?? default_settings.deepseek_model; | 3641 | oai_settings.deepseek_model = settings.deepseek_model ?? default_settings.deepseek_model; |
| 3528 | oai_settings.aimlapi_model = settings.aimlapi_model ?? default_settings.aimlapi_model; | 3642 | oai_settings.aimlapi_model = settings.aimlapi_model ?? default_settings.aimlapi_model; |
| @@ -3666,6 +3780,8 @@ function loadOpenAISettings(data, settings) { | |||
| 3666 | $('#openrouter_allow_fallbacks').prop('checked', oai_settings.openrouter_allow_fallbacks); | 3780 | $('#openrouter_allow_fallbacks').prop('checked', oai_settings.openrouter_allow_fallbacks); |
| 3667 | $('#openrouter_providers_chat').val(oai_settings.openrouter_providers).trigger('change'); | 3781 | $('#openrouter_providers_chat').val(oai_settings.openrouter_providers).trigger('change'); |
| 3668 | $('#openrouter_middleout').val(oai_settings.openrouter_middleout); | 3782 | $('#openrouter_middleout').val(oai_settings.openrouter_middleout); |
| 3783 | $('#electronhub_sort_models').val(oai_settings.electronhub_sort_models); | ||
| 3784 | $('#electronhub_group_models').prop('checked', oai_settings.electronhub_group_models); | ||
| 3669 | $('#squash_system_messages').prop('checked', oai_settings.squash_system_messages); | 3785 | $('#squash_system_messages').prop('checked', oai_settings.squash_system_messages); |
| 3670 | $('#continue_prefill').prop('checked', oai_settings.continue_prefill); | 3786 | $('#continue_prefill').prop('checked', oai_settings.continue_prefill); |
| 3671 | $('#openai_function_calling').prop('checked', oai_settings.function_calling); | 3787 | $('#openai_function_calling').prop('checked', oai_settings.function_calling); |
| @@ -3928,6 +4044,8 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) { | |||
| 3928 | pollinations_model: settings.pollinations_model, | 4044 | pollinations_model: settings.pollinations_model, |
| 3929 | aimlapi_model: settings.aimlapi_model, | 4045 | aimlapi_model: settings.aimlapi_model, |
| 3930 | electronhub_model: settings.electronhub_model, | 4046 | electronhub_model: settings.electronhub_model, |
| 4047 | electronhub_sort_models: settings.electronhub_sort_models, | ||
| 4048 | electronhub_group_models: settings.electronhub_group_models, | ||
| 3931 | moonshot_model: settings.moonshot_model, | 4049 | moonshot_model: settings.moonshot_model, |
| 3932 | fireworks_model: settings.fireworks_model, | 4050 | fireworks_model: settings.fireworks_model, |
| 3933 | cometapi_model: settings.cometapi_model, | 4051 | cometapi_model: settings.cometapi_model, |
| @@ -5093,6 +5211,8 @@ async function onModelChange() { | |||
| 5093 | $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input'); | 5211 | $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input'); |
| 5094 | oai_settings.temp_openai = Math.min(oai_max_temp, oai_settings.temp_openai); | 5212 | oai_settings.temp_openai = Math.min(oai_max_temp, oai_settings.temp_openai); |
| 5095 | $('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input'); | 5213 | $('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input'); |
| 5214 | |||
| 5215 | calculateElectronHubCost(); | ||
| 5096 | } | 5216 | } |
| 5097 | 5217 | ||
| 5098 | if (oai_settings.chat_completion_source === chat_completion_sources.NANOGPT) { | 5218 | if (oai_settings.chat_completion_source === chat_completion_sources.NANOGPT) { |
| @@ -5220,6 +5340,10 @@ async function onOpenrouterModelSortChange() { | |||
| 5220 | await getStatusOpen(); | 5340 | await getStatusOpen(); |
| 5221 | } | 5341 | } |
| 5222 | 5342 | ||
| 5343 | async function onElectronHubModelSortChange() { | ||
| 5344 | await getStatusOpen(); | ||
| 5345 | } | ||
| 5346 | |||
| 5223 | async function onNewPresetClick() { | 5347 | async function onNewPresetClick() { |
| 5224 | const name = await Popup.show.input(t`Preset name:`, t`Hint: Use a character/group name to bind preset to a specific chat.`, oai_settings.preset_settings_openai); | 5348 | const name = await Popup.show.input(t`Preset name:`, t`Hint: Use a character/group name to bind preset to a specific chat.`, oai_settings.preset_settings_openai); |
| 5225 | 5349 | ||
| @@ -6117,12 +6241,14 @@ export function initOpenAI() { | |||
| 6117 | oai_settings.openai_max_context = Number($(this).val()); | 6241 | oai_settings.openai_max_context = Number($(this).val()); |
| 6118 | $('#openai_max_context_counter').val(`${$(this).val()}`); | 6242 | $('#openai_max_context_counter').val(`${$(this).val()}`); |
| 6119 | calculateOpenRouterCost(); | 6243 | calculateOpenRouterCost(); |
| 6244 | calculateElectronHubCost(); | ||
| 6120 | saveSettingsDebounced(); | 6245 | saveSettingsDebounced(); |
| 6121 | }); | 6246 | }); |
| 6122 | 6247 | ||
| 6123 | $('#openai_max_tokens').on('input', function () { | 6248 | $('#openai_max_tokens').on('input', function () { |
| 6124 | oai_settings.openai_max_tokens = Number($(this).val()); | 6249 | oai_settings.openai_max_tokens = Number($(this).val()); |
| 6125 | calculateOpenRouterCost(); | 6250 | calculateOpenRouterCost(); |
| 6251 | calculateElectronHubCost(); | ||
| 6126 | saveSettingsDebounced(); | 6252 | saveSettingsDebounced(); |
| 6127 | }); | 6253 | }); |
| 6128 | 6254 | ||
| @@ -6326,6 +6452,16 @@ export function initOpenAI() { | |||
| 6326 | saveSettingsDebounced(); | 6452 | saveSettingsDebounced(); |
| 6327 | }); | 6453 | }); |
| 6328 | 6454 | ||
| 6455 | $('#electronhub_sort_models').on('input', function () { | ||
| 6456 | oai_settings.electronhub_sort_models = String($(this).val()); | ||
| 6457 | saveSettingsDebounced(); | ||
| 6458 | }); | ||
| 6459 | |||
| 6460 | $('#electronhub_group_models').on('input', function () { | ||
| 6461 | oai_settings.electronhub_group_models = !!$(this).prop('checked'); | ||
| 6462 | saveSettingsDebounced(); | ||
| 6463 | }); | ||
| 6464 | |||
| 6329 | $('#squash_system_messages').on('input', function () { | 6465 | $('#squash_system_messages').on('input', function () { |
| 6330 | oai_settings.squash_system_messages = !!$(this).prop('checked'); | 6466 | oai_settings.squash_system_messages = !!$(this).prop('checked'); |
| 6331 | saveSettingsDebounced(); | 6467 | saveSettingsDebounced(); |
| @@ -6503,6 +6639,14 @@ export function initOpenAI() { | |||
| 6503 | width: '100%', | 6639 | width: '100%', |
| 6504 | templateResult: getAimlapiModelTemplate, | 6640 | templateResult: getAimlapiModelTemplate, |
| 6505 | }); | 6641 | }); |
| 6642 | $('#model_electronhub_select').select2({ | ||
| 6643 | placeholder: t`Select a model`, | ||
| 6644 | searchInputPlaceholder: t`Search models...`, | ||
| 6645 | searchInputCssClass: 'text_pole', | ||
| 6646 | width: '100%', | ||
| 6647 | templateResult: getElectronHubModelTemplate, | ||
| 6648 | matcher: textValueMatcher, | ||
| 6649 | }); | ||
| 6506 | $('#completion_prompt_manager_popup_entry_form_injection_trigger').select2({ | 6650 | $('#completion_prompt_manager_popup_entry_form_injection_trigger').select2({ |
| 6507 | placeholder: t`All types (default)`, | 6651 | placeholder: t`All types (default)`, |
| 6508 | width: '100%', | 6652 | width: '100%', |
| @@ -6549,6 +6693,8 @@ export function initOpenAI() { | |||
| 6549 | $('#model_openrouter_select').on('change', onModelChange); | 6693 | $('#model_openrouter_select').on('change', onModelChange); |
| 6550 | $('#openrouter_group_models').on('change', onOpenrouterModelSortChange); | 6694 | $('#openrouter_group_models').on('change', onOpenrouterModelSortChange); |
| 6551 | $('#openrouter_sort_models').on('change', onOpenrouterModelSortChange); | 6695 | $('#openrouter_sort_models').on('change', onOpenrouterModelSortChange); |
| 6696 | $('#electronhub_group_models').on('change', onElectronHubModelSortChange); | ||
| 6697 | $('#electronhub_sort_models').on('change', onElectronHubModelSortChange); | ||
| 6552 | $('#model_ai21_select').on('change', onModelChange); | 6698 | $('#model_ai21_select').on('change', onModelChange); |
| 6553 | $('#model_mistralai_select').on('change', onModelChange); | 6699 | $('#model_mistralai_select').on('change', onModelChange); |
| 6554 | $('#model_cohere_select').on('change', onModelChange); | 6700 | $('#model_cohere_select').on('change', onModelChange); |
| @@ -648,6 +648,57 @@ export function getTokenizerModel() { | |||
| 648 | } | 648 | } |
| 649 | } | 649 | } |
| 650 | 650 | ||
| 651 | if (oai_settings.chat_completion_source == chat_completion_sources.ELECTRONHUB && oai_settings.electronhub_model) { | ||
| 652 | if (oai_settings.electronhub_model.includes('gpt-4o') || oai_settings.electronhub_model.includes('gpt-5')) { | ||
| 653 | return gpt4oTokenizer; | ||
| 654 | } | ||
| 655 | else if (oai_settings.electronhub_model.includes('gpt-4.1') || oai_settings.electronhub_model.includes('gpt-4.5')) { | ||
| 656 | return gpt4oTokenizer; | ||
| 657 | } | ||
| 658 | else if (oai_settings.electronhub_model.includes('gpt-4')) { | ||
| 659 | return gpt4Tokenizer; | ||
| 660 | } | ||
| 661 | else if (oai_settings.electronhub_model.includes('gpt-3.5-turbo')) { | ||
| 662 | return turboTokenizer; | ||
| 663 | } | ||
| 664 | else if (oai_settings.electronhub_model.includes('claude')) { | ||
| 665 | return claudeTokenizer; | ||
| 666 | } | ||
| 667 | else if (oai_settings.electronhub_model.includes('jamba')) { | ||
| 668 | return jambaTokenizer; | ||
| 669 | } | ||
| 670 | else if (oai_settings.electronhub_model.includes('deepseek') || oai_settings.electronhub_model.includes('sonar-reasoning') || oai_settings.electronhub_model.includes('r1')) { | ||
| 671 | return deepseekTokenizer; | ||
| 672 | } | ||
| 673 | else if (oai_settings.electronhub_model.includes('qwen')) { | ||
| 674 | return qwen2Tokenizer; | ||
| 675 | } | ||
| 676 | else if (oai_settings.electronhub_model.includes('gemma')) { | ||
| 677 | return gemmaTokenizer; | ||
| 678 | } | ||
| 679 | else if (oai_settings.electronhub_model.includes('mistral')) { | ||
| 680 | return mistralTokenizer; | ||
| 681 | } | ||
| 682 | else if (oai_settings.electronhub_model.includes('yi')) { | ||
| 683 | return yiTokenizer; | ||
| 684 | } | ||
| 685 | else if (oai_settings.electronhub_model.includes('llama3') || oai_settings.electronhub_model.includes('llama-3') || oai_settings.electronhub_model.startsWith('l3')) { | ||
| 686 | return llama3Tokenizer; | ||
| 687 | } | ||
| 688 | else if (oai_settings.electronhub_model.includes('llama')) { | ||
| 689 | return llamaTokenizer; | ||
| 690 | } | ||
| 691 | else if (oai_settings.electronhub_model.includes('command-a')) { | ||
| 692 | return commandATokenizer; | ||
| 693 | } | ||
| 694 | else if (oai_settings.electronhub_model.includes('command-r')) { | ||
| 695 | return commandRTokenizer; | ||
| 696 | } | ||
| 697 | else if (oai_settings.electronhub_model.includes('nemo')) { | ||
| 698 | return nemoTokenizer; | ||
| 699 | } | ||
| 700 | } | ||
| 701 | |||
| 651 | if (oai_settings.chat_completion_source == chat_completion_sources.COHERE) { | 702 | if (oai_settings.chat_completion_source == chat_completion_sources.COHERE) { |
| 652 | if (oai_settings.cohere_model.includes('command-a')) { | 703 | if (oai_settings.cohere_model.includes('command-a')) { |
| 653 | return commandATokenizer; | 704 | return commandATokenizer; |
| @@ -2940,6 +2940,7 @@ select option:not(:checked) { | |||
| 2940 | text-align: center; | 2940 | text-align: center; |
| 2941 | } | 2941 | } |
| 2942 | 2942 | ||
| 2943 | .menu_button[disabled], | ||
| 2943 | .menu_button.disabled { | 2944 | .menu_button.disabled { |
| 2944 | filter: brightness(75%) grayscale(1); | 2945 | filter: brightness(75%) grayscale(1); |
| 2945 | opacity: 0.5; | 2946 | opacity: 0.5; |
| @@ -3675,8 +3676,8 @@ grammarly-extension { | |||
| 3675 | min-width: calc(1.25em + 12px); | 3676 | min-width: calc(1.25em + 12px); |
| 3676 | } | 3677 | } |
| 3677 | 3678 | ||
| 3678 | .menu_button:not(.disabled):hover, | 3679 | .menu_button:not(.disabled):not([disabled]):hover, |
| 3679 | .menu_button:not(.disabled).active { | 3680 | .menu_button:not(.disabled):not([disabled]).active { |
| 3680 | background-color: var(--white30a); | 3681 | background-color: var(--white30a); |
| 3681 | } | 3682 | } |
| 3682 | 3683 | ||
| @@ -1255,6 +1255,7 @@ async function sendElectronHubRequest(request, response) { | |||
| 1255 | 'frequency_penalty': request.body.frequency_penalty, | 1255 | 'frequency_penalty': request.body.frequency_penalty, |
| 1256 | 'top_p': request.body.top_p, | 1256 | 'top_p': request.body.top_p, |
| 1257 | 'top_k': request.body.top_k, | 1257 | 'top_k': request.body.top_k, |
| 1258 | 'logit_bias': request.body.logit_bias, | ||
| 1258 | 'seed': request.body.seed, | 1259 | 'seed': request.body.seed, |
| 1259 | ...bodyParams, | 1260 | ...bodyParams, |
| 1260 | }; | 1261 | }; |
| @@ -325,6 +325,104 @@ router.post('/generate-voice', async (request, response) => { | |||
| 325 | } | 325 | } |
| 326 | }); | 326 | }); |
| 327 | 327 | ||
| 328 | // ElectronHub TTS proxy | ||
| 329 | router.post('/electronhub/generate-voice', async (request, response) => { | ||
| 330 | try { | ||
| 331 | const key = readSecret(request.user.directories, SECRET_KEYS.ELECTRONHUB); | ||
| 332 | |||
| 333 | if (!key) { | ||
| 334 | console.warn('No ElectronHub key found'); | ||
| 335 | return response.sendStatus(400); | ||
| 336 | } | ||
| 337 | |||
| 338 | const requestBody = { | ||
| 339 | input: request.body.input, | ||
| 340 | voice: request.body.voice, | ||
| 341 | speed: request.body.speed ?? 1, | ||
| 342 | temperature: request.body.temperature ?? undefined, | ||
| 343 | model: request.body.model || 'tts-1', | ||
| 344 | response_format: 'mp3', | ||
| 345 | }; | ||
| 346 | |||
| 347 | // Optional provider-specific params | ||
| 348 | if (request.body.instructions) requestBody.instructions = request.body.instructions; | ||
| 349 | if (request.body.speaker_transcript) requestBody.speaker_transcript = request.body.speaker_transcript; | ||
| 350 | if (Number.isFinite(request.body.cfg_scale)) requestBody.cfg_scale = Number(request.body.cfg_scale); | ||
| 351 | if (Number.isFinite(request.body.cfg_filter_top_k)) requestBody.cfg_filter_top_k = Number(request.body.cfg_filter_top_k); | ||
| 352 | if (Number.isFinite(request.body.speech_rate)) requestBody.speech_rate = Number(request.body.speech_rate); | ||
| 353 | if (Number.isFinite(request.body.pitch_adjustment)) requestBody.pitch_adjustment = Number(request.body.pitch_adjustment); | ||
| 354 | if (request.body.emotional_style) requestBody.emotional_style = request.body.emotional_style; | ||
| 355 | |||
| 356 | // Handle dynamic parameters sent from the frontend | ||
| 357 | const knownParams = new Set(Object.keys(requestBody)); | ||
| 358 | for (const key in request.body) { | ||
| 359 | if (!knownParams.has(key) && request.body[key] !== undefined) { | ||
| 360 | requestBody[key] = request.body[key]; | ||
| 361 | } | ||
| 362 | } | ||
| 363 | |||
| 364 | // Clean undefineds | ||
| 365 | Object.keys(requestBody).forEach(k => requestBody[k] === undefined && delete requestBody[k]); | ||
| 366 | |||
| 367 | console.debug('ElectronHub TTS request', requestBody); | ||
| 368 | |||
| 369 | const result = await fetch('https://api.electronhub.ai/v1/audio/speech', { | ||
| 370 | method: 'POST', | ||
| 371 | headers: { | ||
| 372 | 'Content-Type': 'application/json', | ||
| 373 | Authorization: `Bearer ${key}`, | ||
| 374 | }, | ||
| 375 | body: JSON.stringify(requestBody), | ||
| 376 | }); | ||
| 377 | |||
| 378 | if (!result.ok) { | ||
| 379 | const text = await result.text(); | ||
| 380 | console.warn('ElectronHub TTS request failed', result.statusText, text); | ||
| 381 | return response.status(500).send(text); | ||
| 382 | } | ||
| 383 | |||
| 384 | const contentType = result.headers.get('content-type') || 'audio/mpeg'; | ||
| 385 | const buffer = await result.arrayBuffer(); | ||
| 386 | response.setHeader('Content-Type', contentType); | ||
| 387 | return response.send(Buffer.from(buffer)); | ||
| 388 | } catch (error) { | ||
| 389 | console.error('ElectronHub TTS generation failed', error); | ||
| 390 | response.status(500).send('Internal server error'); | ||
| 391 | } | ||
| 392 | }); | ||
| 393 | |||
| 394 | // ElectronHub model list | ||
| 395 | router.post('/electronhub/models', async (request, response) => { | ||
| 396 | try { | ||
| 397 | const key = readSecret(request.user.directories, SECRET_KEYS.ELECTRONHUB); | ||
| 398 | |||
| 399 | if (!key) { | ||
| 400 | console.warn('No ElectronHub key found'); | ||
| 401 | return response.sendStatus(400); | ||
| 402 | } | ||
| 403 | |||
| 404 | const result = await fetch('https://api.electronhub.ai/v1/models', { | ||
| 405 | method: 'GET', | ||
| 406 | headers: { | ||
| 407 | Authorization: `Bearer ${key}`, | ||
| 408 | }, | ||
| 409 | }); | ||
| 410 | |||
| 411 | if (!result.ok) { | ||
| 412 | const text = await result.text(); | ||
| 413 | console.warn('ElectronHub models request failed', result.statusText, text); | ||
| 414 | return response.status(500).send(text); | ||
| 415 | } | ||
| 416 | |||
| 417 | const data = await result.json(); | ||
| 418 | const models = data && Array.isArray(data['data']) ? data['data'] : []; | ||
| 419 | return response.json(models); | ||
| 420 | } catch (error) { | ||
| 421 | console.error('ElectronHub models fetch failed', error); | ||
| 422 | response.status(500).send('Internal server error'); | ||
| 423 | } | ||
| 424 | }); | ||
| 425 | |||
| 328 | router.post('/generate-image', async (request, response) => { | 426 | router.post('/generate-image', async (request, response) => { |
| 329 | try { | 427 | try { |
| 330 | const key = readSecret(request.user.directories, SECRET_KEYS.OPENAI); | 428 | const key = readSecret(request.user.directories, SECRET_KEYS.OPENAI); |