Merge branch 'staging' of github.com-RA:sillytavern/sillytavern into staging
| @@ -8,6 +8,9 @@ | ||
| 8 | 8 | "openrouter_sort_models": "alphabetically", |
| 9 | 9 | "ai21_model": "jamba-large", |
| 10 | 10 | "mistralai_model": "mistral-large-latest", |
| 11 | + "electronhub_model": "gpt-4o-mini", | |
| 12 | + "electronhub_sort_models": "alphabetically", | |
| 13 | + "electronhub_group_models": false, | |
| 11 | 14 | "custom_model": "", |
| 12 | 15 | "custom_url": "", |
| 13 | 16 | "custom_include_body": "", |
| @@ -88,6 +88,24 @@ body.reduced-motion #bg_custom { | ||
| 88 | 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 | 109 | #bg-scrollable-content { |
| 92 | 110 | flex-grow: 1; |
| 93 | 111 | overflow-y: auto; |
| @@ -95,46 +113,43 @@ body.reduced-motion #bg_custom { | ||
| 95 | 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 | 124 | #bg-filter { |
| 99 | 125 | font-size: calc(var(--mainFontSize) * 0.95); |
| 100 | 126 | } |
| 101 | 127 | |
| 102 | 128 | /* Thumbnail Menu & ButtonsThumbnails */ |
| 129 | +.bg_example:hover .BGSampleTitle { | |
| 130 | + opacity: 1; | |
| 131 | +} | |
| 132 | + | |
| 103 | 133 | .bg_example .mobile-only-menu-toggle { |
| 104 | 134 | display: none; |
| 105 | 135 | } |
| 106 | 136 | |
| 107 | 137 | .bg_example.flex-container { |
| 108 | - width: 30%; | |
| 109 | - max-width: 200px; | |
| 110 | - margin: 5px; | |
| 111 | - aspect-ratio: 16/9; | |
| 112 | 138 | cursor: pointer; |
| 113 | 139 | box-shadow: 0 0 7px var(--black50a); |
| 114 | - | |
| 115 | 140 | position: relative; |
| 116 | 141 | overflow: hidden; |
| 117 | 142 | border-radius: 8px; |
| 118 | 143 | border: 0px solid transparent; |
| 119 | 144 | outline: 2px solid var(--SmartThemeBorderColor); |
| 120 | 145 | outline-offset: -1px; |
| 121 | -} | |
| 122 | 146 | |
| 123 | -.bg_example.flex-container:focus-visible { | |
| 147 | + height: auto; | |
| 124 | 148 | outlineaspect-offsetratio: inherit16 / 9; |
| 125 | 149 | } |
| 126 | 150 | |
| 127 | 151 | .bg_example_imgbg_example:focus-visible { |
| 128 | 152 | positionoutline-offset: absoluteinherit; |
| 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 | 155 | .bg_example .jg-menu { |
| @@ -143,9 +158,8 @@ body.reduced-motion #bg_custom { | ||
| 143 | 158 | top: 2px; |
| 144 | 159 | right: 2px; |
| 145 | 160 | background-color: rgba(0, 0, 0, 0.5); |
| 146 | 161 | border-radius: 8px5px; |
| 147 | 162 | gappadding: 3px 3px; |
| 148 | - padding: 3px 5px; | |
| 149 | 163 | z-index: 3; |
| 150 | 164 | backdrop-filter: blur(4px); |
| 151 | 165 | border: 1px solid var(--SmartThemeBorderColor); |
| @@ -168,14 +182,14 @@ body.reduced-motion #bg_custom { | ||
| 168 | 182 | |
| 169 | 183 | .bg_example .jg-button { |
| 170 | 184 | display: flex; |
| 171 | 185 | width: 30px24px; |
| 172 | 186 | height: 30px24px; |
| 173 | 187 | align-items: center; |
| 174 | 188 | justify-content: center; |
| 175 | 189 | color: white; |
| 176 | 190 | padding: 5px; |
| 177 | 191 | font-size: 1.1em; |
| 178 | 192 | border-radius: 6px5px; |
| 179 | 193 | transition: background-color var(--animation-duration) ease; |
| 180 | 194 | } |
| 181 | 195 | |
| @@ -195,6 +209,18 @@ body.reduced-motion #bg_custom { | ||
| 195 | 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 | 224 | .bg_example:not([custom="true"]) .jg-copy, |
| 199 | 225 | .bg_example[custom="true"] .jg-edit { |
| 200 | 226 | display: none; |
| @@ -30,6 +30,14 @@ | ||
| 30 | 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 | 41 | .bg_button { |
| 34 | 42 | font-size: 15px; |
| 35 | 43 | } |
| @@ -49,6 +57,11 @@ | ||
| 49 | 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 | 65 | .bg_example .mobile-only-menu-toggle { |
| 53 | 66 | display: flex; |
| 54 | 67 | align-items: center; |
| @@ -66,6 +79,11 @@ | ||
| 66 | 79 | backdrop-filter: blur(2px); |
| 67 | 80 | } |
| 68 | 81 | |
| 82 | + .bg_example .jg-button { | |
| 83 | + width: 30px; | |
| 84 | + height: 30px; | |
| 85 | + } | |
| 86 | + | |
| 69 | 87 | #bg-header-controls { |
| 70 | 88 | flex-wrap: wrap; |
| 71 | 89 | row-gap: 10px; |
| @@ -438,10 +456,6 @@ | ||
| 438 | 456 | .horde_multiple_hint { |
| 439 | 457 | display: none; |
| 440 | 458 | } |
| 441 | - | |
| 442 | - .bg_list { | |
| 443 | - width: unset; | |
| 444 | - } | |
| 445 | 459 | } |
| 446 | 460 | |
| 447 | 461 | /*landscape mode phones and ipads*/ |
| @@ -676,6 +676,9 @@ | ||
| 676 | 676 | <div data-source="openrouter"> |
| 677 | 677 | <span data-i18n="Max prompt cost:">Max prompt cost:</span> <span id="openrouter_max_prompt_cost" data-i18n="Unknown">Unknown</span> |
| 678 | 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 | 682 | <hr> |
| 680 | 683 | <div class="range-block"> |
| 681 | 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 | 2126 | </div> |
| 2124 | 2127 | </div> |
| 2125 | 2128 | </div> |
| 2126 | 2129 | <div class="range-block m-t-1" data-source="openai,aimlapi,openrouter,custom,electronhub,azure_openai"> |
| 2127 | 2130 | <div id="logit_bias_openai" class="range-block-title openai_restorable" data-i18n="Logit Bias"> |
| 2128 | 2131 | Logit Bias |
| 2129 | 2132 | </div> |
| @@ -3061,7 +3064,7 @@ | ||
| 3061 | 3064 | <div class="marginTopBot5"> |
| 3062 | 3065 | <div class="inline-drawer wide100p"> |
| 3063 | 3066 | <div class="inline-drawer-toggle inline-drawer-header"> |
| 3064 | 3067 | <b data-i18n="OpenRouter Model OrderSorting">OpenRouter Model Sorting</b> |
| 3065 | 3068 | <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div> |
| 3066 | 3069 | </div> |
| 3067 | 3070 | <div class="inline-drawer-content m-b-1"> |
| @@ -3452,6 +3455,9 @@ | ||
| 3452 | 3455 | </div> |
| 3453 | 3456 | <div id="electronhub_form" data-source="electronhub"> |
| 3454 | 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 | 3461 | <div class="flex-container"> |
| 3456 | 3462 | <input id="api_key_electronhub" name="api_key_electronhub" class="text_pole flex1" value="" type="text" autocomplete="off"> |
| 3457 | 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,11 +3465,44 @@ | ||
| 3459 | 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 | 3466 | For privacy reasons, your API key will be hidden after you click 'Connect'. |
| 3461 | 3467 | </div> |
| 3468 | + <div> | |
| 3462 | 3469 | <h4 data-i18n="Electron Hub Model">Electron Hub Model</h4> |
| 3463 | 3470 | <select id="model_electronhub_select"> |
| 3464 | 3471 | <option value="" data-i18n="-- Connect to the API --">-- Connect to the API --</option> |
| 3465 | 3472 | </select> |
| 3466 | 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> | |
| 3505 | + </div> | |
| 3467 | 3506 | <div id="nanogpt_form" data-source="nanogpt"> |
| 3468 | 3507 | <h4 data-i18n="NanoGPT API Key">NanoGPT API Key</h4> |
| 3469 | 3508 | <div class="flex-container"> |
| @@ -5345,9 +5384,19 @@ | ||
| 5345 | 5384 | </div> |
| 5346 | 5385 | </div> |
| 5347 | 5386 | <div id="bg-scrollable-content"> |
| 5348 | - <h3 data-i18n="System Backgrounds" class="wide100p textAlignCenter"> | |
| 5387 | + <div class="heading-container-with-controls"> | |
| 5388 | + <h3 data-i18n="System Backgrounds" class="wide100p textAlignCenter heading-text"> | |
| 5349 | 5389 | System Backgrounds |
| 5350 | 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 | 5400 | <div id="bg_menu_content" class="bg_list"> |
| 5352 | 5401 | </div> |
| 5353 | 5402 | <h3 data-i18n="Chat Backgrounds" class="wide100p textAlignCenter"> |
| @@ -369,7 +369,7 @@ | ||
| 369 | 369 | "Anthropic's developer console": "وحدة تحكم المطور في Anthropic", |
| 370 | 370 | "Claude Model": "نموذج Claude", |
| 371 | 371 | "Window AI Model": "نموذج Window AI", |
| 372 | 372 | "OpenRouter Model OrderSorting": "فرز نموذج OpenRouter", |
| 373 | 373 | "Alphabetically": "أبجديا", |
| 374 | 374 | "Price": "السعر (الأرخص)", |
| 375 | 375 | "Context Size": "حجم السياق", |
| @@ -369,7 +369,7 @@ | ||
| 369 | 369 | "Anthropic's developer console": "Anthropics Entwicklerkonsole", |
| 370 | 370 | "Claude Model": "Claude-Modell", |
| 371 | 371 | "Window AI Model": "Fenster AI-Modell", |
| 372 | 372 | "OpenRouter Model OrderSorting": "Sortierung des OpenRouter-Modells", |
| 373 | 373 | "Alphabetically": "Alphabetisch", |
| 374 | 374 | "Price": "Preis (am günstigsten)", |
| 375 | 375 | "Context Size": "Kontextgröße", |
| @@ -369,7 +369,7 @@ | ||
| 369 | 369 | "Anthropic's developer console": "la consola de desarrolladores de Anthropic", |
| 370 | 370 | "Claude Model": "Modelo de Claude", |
| 371 | 371 | "Window AI Model": "Modelo de Window AI", |
| 372 | 372 | "OpenRouter Model OrderSorting": "Clasificación de modelos de OpenRouter", |
| 373 | 373 | "Alphabetically": "Alfabéticamente", |
| 374 | 374 | "Price": "Precio (más barato)", |
| 375 | 375 | "Context Size": "Tamaño del contexto", |
| @@ -348,7 +348,7 @@ | ||
| 348 | 348 | "Anthropic's developer console": "Console de développement d'Anthropic", |
| 349 | 349 | "Claude Model": "Modèle Claude", |
| 350 | 350 | "Window AI Model": "Modèle Window AI", |
| 351 | 351 | "OpenRouter Model OrderSorting": "Tri des modèles OpenRouter", |
| 352 | 352 | "Alphabetically": "Alphabétiquement", |
| 353 | 353 | "Price": "Prix (le moins cher)", |
| 354 | 354 | "Context Size": "Taille du contexte", |
| @@ -1566,7 +1566,7 @@ | ||
| 1566 | 1566 | "These files are available for all characters in the current chat.": "Ces fichiers sont disponibles pour tous les personnages du chat actuel.", |
| 1567 | 1567 | "Image Captioning": "Légende des images", |
| 1568 | 1568 | "Local": "Local", |
| 1569 | - "Multimodal (OpenAI / Anthropic / llama / Google)": "Multimodal (OpenAI / Anthropic / llama / Google)", | |
| 1569 | + "Multimodal": "Multimodal", | |
| 1570 | 1570 | "Extras": "Extras", |
| 1571 | 1571 | "Horde": "Horde", |
| 1572 | 1572 | "API": "API", |
| @@ -367,7 +367,7 @@ | ||
| 367 | 367 | "Anthropic's developer console": "Uppbyggingaraðilar Forritara stjórnborð", |
| 368 | 368 | "Claude Model": "Claude módel", |
| 369 | 369 | "Window AI Model": "Vindauga AI módel", |
| 370 | 370 | "OpenRouter Model OrderSorting": "OpenRouter líkanaflokkun", |
| 371 | 371 | "Alphabetically": "Stafrófsröð", |
| 372 | 372 | "Price": "Verð (ódýrast)", |
| 373 | 373 | "Context Size": "Samhengisstærð", |
| @@ -369,7 +369,7 @@ | ||
| 369 | 369 | "Anthropic's developer console": "Console dello sviluppatore di Anthropic", |
| 370 | 370 | "Claude Model": "Modello Claude", |
| 371 | 371 | "Window AI Model": "Modello AI di Window", |
| 372 | 372 | "OpenRouter Model OrderSorting": "Ordinamento dei modelli OpenRouter", |
| 373 | 373 | "Alphabetically": "In ordine alfabetico", |
| 374 | 374 | "Price": "Prezzo (il più economico)", |
| 375 | 375 | "Context Size": "Dimensione del contesto", |
| @@ -369,7 +369,7 @@ | ||
| 369 | 369 | "Anthropic's developer console": "Anthropicの開発者コンソール", |
| 370 | 370 | "Claude Model": "Claudeモデル", |
| 371 | 371 | "Window AI Model": "Window AIモデル", |
| 372 | 372 | "OpenRouter Model OrderSorting": "OpenRouter モデルのソート", |
| 373 | 373 | "Alphabetically": "アルファベット順", |
| 374 | 374 | "Price": "価格(最安値)", |
| 375 | 375 | "Context Size": "コンテキストサイズ", |
| @@ -374,7 +374,7 @@ | ||
| 374 | 374 | "Slack and Poe cookies will not work here, do not bother trying.": "Slack과 Poe 쿠키는 여기서 작동하지 않습니다. 시도하지 마세요.", |
| 375 | 375 | "Claude Model": "Claude 모델", |
| 376 | 376 | "Window AI Model": "Window AI 모델", |
| 377 | 377 | "OpenRouter Model OrderSorting": "OpenRouter 모델 정렬", |
| 378 | 378 | "Alphabetically": "알파벳순", |
| 379 | 379 | "Price": "가격(가장 저렴)", |
| 380 | 380 | "Context Size": "컨텍스트 크기", |
| @@ -367,7 +367,7 @@ | ||
| 367 | 367 | "Anthropic's developer console": "Anthropic's ontwikkelaarsconsole", |
| 368 | 368 | "Claude Model": "Claude-model", |
| 369 | 369 | "Window AI Model": "Window AI-model", |
| 370 | 370 | "OpenRouter Model OrderSorting": "Sorteren van OpenRouter-modellen", |
| 371 | 371 | "Alphabetically": "Alfabetisch", |
| 372 | 372 | "Price": "Prijs (goedkoopste)", |
| 373 | 373 | "Context Size": "Contextgrootte", |
| @@ -369,7 +369,7 @@ | ||
| 369 | 369 | "Anthropic's developer console": "console de desenvolvedor da Anthropic", |
| 370 | 370 | "Claude Model": "Modelo Claude", |
| 371 | 371 | "Window AI Model": "Modelo Window AI", |
| 372 | 372 | "OpenRouter Model OrderSorting": "Classificação de modelo OpenRouter", |
| 373 | 373 | "Alphabetically": "Alfabeticamente", |
| 374 | 374 | "Price": "Preço (mais barato)", |
| 375 | 375 | "Context Size": "Tamanho do contexto", |
| @@ -718,7 +718,7 @@ | ||
| 718 | 718 | "Google Model": "Модель Google", |
| 719 | 719 | "Cohere API Key": "Ключ от API Cohere", |
| 720 | 720 | "Cohere Model": "Модель Cohere", |
| 721 | 721 | "OpenRouter Model OrderSorting": "Сортировка моделей OpenRouter", |
| 722 | 722 | "Alphabetically": "По алфавиту", |
| 723 | 723 | "Price": "По цене (наиболее низкая)", |
| 724 | 724 | "Context Size": "По размеру контекста", |
| @@ -369,7 +369,7 @@ | ||
| 369 | 369 | "Anthropic's developer console": "консолі розробника Anthropic", |
| 370 | 370 | "Claude Model": "Модель Claude", |
| 371 | 371 | "Window AI Model": "Модель Window AI", |
| 372 | 372 | "OpenRouter Model OrderSorting": "Сортування моделі OpenRouter", |
| 373 | 373 | "Alphabetically": "За алфавітом", |
| 374 | 374 | "Price": "Ціна (найдешевша)", |
| 375 | 375 | "Context Size": "Розмір контексту", |
| @@ -369,7 +369,7 @@ | ||
| 369 | 369 | "Anthropic's developer console": "developer console của Anthropic", |
| 370 | 370 | "Claude Model": "Model Claude", |
| 371 | 371 | "Window AI Model": "Model Window AI", |
| 372 | 372 | "OpenRouter Model OrderSorting": "Sắp xếp model OpenRouter", |
| 373 | 373 | "Alphabetically": "Theo thứ tự bảng chữ cái", |
| 374 | 374 | "Price": "Giá (rẻ nhất)", |
| 375 | 375 | "Context Size": "Kích thước bối cảnh", |
| @@ -452,7 +452,7 @@ | ||
| 452 | 452 | "Claude Model": "Claude 模型", |
| 453 | 453 | "Allow fallback routes Description": "如果所选模型无法响应您的请求,则自动选择备用模型。", |
| 454 | 454 | "Allow fallback models": "允许后备模型", |
| 455 | 455 | "OpenRouter Model OrderSorting": "OpenRouter 模型顺序", |
| 456 | 456 | "Alphabetically": "按字母顺序", |
| 457 | 457 | "Price": "价格(最便宜)", |
| 458 | 458 | "Context Size": "上下文长度", |
| @@ -1479,7 +1479,7 @@ | ||
| 1479 | 1479 | "Image Captioning": "图像描述", |
| 1480 | 1480 | "Source": "来源", |
| 1481 | 1481 | "Local": "本地", |
| 1482 | - "Multimodal (OpenAI / Anthropic / llama / Google)": "多模态(OpenAI / Anthropic / llama / Google)", | |
| 1482 | + "Multimodal": "多模态", | |
| 1483 | 1483 | "Extras": "更多", |
| 1484 | 1484 | "Horde": "Horde", |
| 1485 | 1485 | "API": "API", |
| @@ -368,7 +368,7 @@ | ||
| 368 | 368 | "Anthropic's developer console": "Anthropic 的開發者控制台", |
| 369 | 369 | "Claude Model": "Claude 模型", |
| 370 | 370 | "Window AI Model": "Window AI 模型", |
| 371 | 371 | "OpenRouter Model OrderSorting": "模型順序", |
| 372 | 372 | "Alphabetically": "按字母順序", |
| 373 | 373 | "Price": "價格(最便宜的)", |
| 374 | 374 | "Context Size": "上下文長度", |
| @@ -1674,7 +1674,7 @@ | ||
| 1674 | 1674 | "Message Template": "訊息範本", |
| 1675 | 1675 | "Model ID": "模型 ID", |
| 1676 | 1676 | "mui_reset": "重設", |
| 1677 | - "Multimodal (OpenAI / Anthropic / llama / Google)": "多模態(OpenAI/Anthropic/llama/Google)", | |
| 1677 | + "Multimodal": "多模態", | |
| 1678 | 1678 | "must be set in Tabby's config.yml to switch models.": "須在 Tabby's config.yml 中設定以切換模型。", |
| 1679 | 1679 | "Names as Stop Strings": "將名稱用作停止字串", |
| 1680 | 1680 | "Never": "從不", |
| @@ -3,7 +3,8 @@ import { chat_metadata, eventSource, event_types, generateQuietPrompt, getCurren | ||
| 3 | 3 | import { openThirdPartyExtensionMenu, saveMetadataDebounced } from './extensions.js'; |
| 4 | 4 | import { SlashCommand } from './slash-commands/SlashCommand.js'; |
| 5 | 5 | import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; |
| 6 | 6 | import { createThumbnail, flashHighlight, getBase64Async, stringFormat, debounce } from './utils.js'; |
| 7 | +import { debounce_timeout } from './constants.js'; | |
| 7 | 8 | import { t } from './i18n.js'; |
| 8 | 9 | import { Popup } from './popup.js'; |
| 9 | 10 | |
| @@ -15,6 +16,11 @@ const PNG_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkY | ||
| 15 | 16 | const PNG_PIXEL_BLOB = new Blob([Uint8Array.from(atob(PNG_PIXEL), c => c.charCodeAt(0))], { type: 'image/png' }); |
| 16 | 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 | 25 | * Storage for frontend-generated background thumbnails. |
| 20 | 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 | 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 | 101 | export function loadBackgroundSettings(settings) { |
| 49 | 102 | let backgroundSettings = settings.background; |
| 50 | 103 | if (!backgroundSettings || !backgroundSettings.name || !backgroundSettings.url) { |
| @@ -56,6 +109,16 @@ export function loadBackgroundSettings(settings) { | ||
| 56 | 109 | if (!Object.hasOwn(backgroundSettings, 'animation')) { |
| 57 | 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 | 122 | setBackground(backgroundSettings.name, backgroundSettings.url); |
| 60 | 123 | setFittingClass(backgroundSettings.fitting); |
| 61 | 124 | $('#background_fitting').val(backgroundSettings.fitting); |
| @@ -75,7 +138,7 @@ async function forceSetBackground(backgroundInfo) { | ||
| 75 | 138 | list.push(bg); |
| 76 | 139 | chat_metadata[LIST_METADATA_KEY] = list; |
| 77 | 140 | saveMetadataDebounced(); |
| 78 | 141 | await getChatBackgroundsListrenderChatBackgrounds(); |
| 79 | 142 | highlightNewBackground(bg); |
| 80 | 143 | highlightLockedBackground(); |
| 81 | 144 | } |
| @@ -88,28 +151,10 @@ async function onChatChanged() { | ||
| 88 | 151 | unsetCustomBackground(); |
| 89 | 152 | } |
| 90 | 153 | |
| 91 | 154 | await getChatBackgroundsListrenderChatBackgrounds(); |
| 92 | 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 | 158 | function getBackgroundPath(fileUrl) { |
| 114 | 159 | return `backgrounds/${encodeURIComponent(fileUrl)}`; |
| 115 | 160 | } |
| @@ -257,7 +302,7 @@ async function onCopyToSystemBackgroundClick(e) { | ||
| 257 | 302 | const index = list.indexOf(bgNames.oldBg); |
| 258 | 303 | list.splice(index, 1); |
| 259 | 304 | saveMetadataDebounced(); |
| 260 | 305 | await getChatBackgroundsListrenderChatBackgrounds(); |
| 261 | 306 | } |
| 262 | 307 | |
| 263 | 308 | /** |
| @@ -382,17 +427,21 @@ async function onDeleteBackgroundClick(e) { | ||
| 382 | 427 | list.splice(index, 1); |
| 383 | 428 | } |
| 384 | 429 | |
| 385 | - const siblingSelector = '.bg_example:not(#form_bg_download)'; | |
| 430 | + if (bg === background_settings.name) { | |
| 431 | + const siblingSelector = '.bg_example'; | |
| 386 | 432 | const nextBg = bgToDelete.next(siblingSelector); |
| 387 | 433 | const prevBg = bgToDelete.prev(siblingSelector); |
| 388 | - const anyBg = $(siblingSelector); | |
| 389 | 434 | |
| 390 | 435 | if (nextBg.length > 0) { |
| 391 | 436 | nextBg.trigger('click'); |
| 392 | 437 | } else if (prevBg.length > 0) { |
| 393 | 438 | prevBg.trigger('click'); |
| 394 | 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 | 447 | bgToDelete.remove(); |
| @@ -404,7 +453,7 @@ async function onDeleteBackgroundClick(e) { | ||
| 404 | 453 | } |
| 405 | 454 | |
| 406 | 455 | if (isCustom) { |
| 407 | 456 | await getChatBackgroundsListrenderChatBackgrounds(); |
| 408 | 457 | saveMetadataDebounced(); |
| 409 | 458 | } |
| 410 | 459 | } |
| @@ -445,6 +494,47 @@ async function autoBackgroundCommand() { | ||
| 445 | 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 | 538 | export async function getBackgrounds() { |
| 449 | 539 | const response = await fetch('/api/backgrounds/all', { |
| 450 | 540 | method: 'POST', |
| @@ -454,12 +544,7 @@ export async function getBackgrounds() { | ||
| 454 | 544 | if (response.ok) { |
| 455 | 545 | const { images, config } = await response.json(); |
| 456 | 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 | 566 | lazyLoadObserver = new IntersectionObserver((entries, observer) => { |
| 482 | 567 | entries.forEach(entry => { |
| 483 | 568 | if (entry.target instanceof HTMLElement && entry.isIntersecting) { |
| 484 | 569 | const targetclipper = entry.target; |
| 485 | 570 | const bgparentThumbnail = targetclipper.getAttributeclosest('bgfile.bg_example'); |
| 486 | - const isCustom = target.getAttribute('custom') === 'true'; | |
| 571 | + | |
| 572 | + if (parentThumbnail) { | |
| 573 | + const bg = parentThumbnail.getAttribute('bgfile'); | |
| 574 | + const isCustom = parentThumbnail.getAttribute('custom') === 'true'; | |
| 487 | 575 | resolveImageUrl(bg, isCustom) |
| 488 | 576 | .then(url => { targetclipper.style.backgroundImage = url; }) |
| 489 | 577 | .catch(() => { targetclipper.style.backgroundImage = PLACEHOLDER_IMAGE; }); |
| 490 | - target.classList.remove('lazy-load-background'); | |
| 578 | + } | |
| 491 | - observer.unobserve(target); | |
| 579 | + | |
| 580 | + clipper.classList.remove('lazy-load-background'); | |
| 581 | + observer.unobserve(clipper); | |
| 492 | 582 | } |
| 493 | 583 | }); |
| 494 | 584 | }, options); |
| @@ -529,28 +619,6 @@ async function resolveImageUrl(bg, isCustom) { | ||
| 529 | 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 | 622 | async function setBackground(bg, url) { |
| 555 | 623 | $('#bg1').css('background-image', url); |
| 556 | 624 | background_settings.name = bg; |
| @@ -688,17 +756,17 @@ function setFittingClass(fitting) { | ||
| 688 | 756 | } |
| 689 | 757 | |
| 690 | 758 | function onBackgroundFilterInput() { |
| 691 | 759 | const filterValue = String($(this'#bg-filter').val()).toLowerCase(); |
| 692 | 760 | $('#bg_menu_content > div.bg_example, #bg_custom_content > .bg_example').each(function () { |
| 693 | 761 | const $bgContentbg = $(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 | 770 | export function initBackgrounds() { |
| 703 | 771 | eventSource.on(event_types.CHAT_CHANGED, onChatChanged); |
| 704 | 772 | eventSource.on(event_types.FORCE_SET_BACKGROUND, forceSetBackground); |
| @@ -715,6 +783,11 @@ export function initBackgrounds() { | ||
| 715 | 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 | 791 | .off('click', '.jg-button').on('click', '.jg-button', function (e) { |
| 719 | 792 | e.stopPropagation(); |
| 720 | 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 | 820 | $('#auto_background').on('click', autoBackgroundCommand); |
| 742 | 821 | $('#add_bg_button').on('change', onBackgroundUploadSelected); |
| 743 | 822 | $('#bg-filter').on('input', onBackgroundFilterInput() => debouncedOnBackgroundFilterInput()); |
| 744 | 823 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 745 | 824 | name: 'lockbg', |
| 746 | 825 | callback: () => onLockBackgroundClick(new CustomEvent('click')), |
| @@ -9,7 +9,7 @@ | ||
| 9 | 9 | <label for="caption_source" data-i18n="Source">Source</label> |
| 10 | 10 | <select id="caption_source" class="text_pole"> |
| 11 | 11 | <option value="local" data-i18n="Local">Local</option> |
| 12 | 12 | <option value="multimodal" data-i18n="Multimodal (OpenAI / Anthropic / llama / Google)">Multimodal (OpenAI / Anthropic / llama / Google)</option> |
| 13 | 13 | <option value="extras" data-i18n="Extras">Extras (deprecated)</option> |
| 14 | 14 | <option value="horde" data-i18n="Horde">Horde</option> |
| 15 | 15 | </select> |
| @@ -18,7 +18,7 @@ | ||
| 18 | 18 | <label for="caption_multimodal_api" data-i18n="API">API</label> |
| 19 | 19 | <select id="caption_multimodal_api" class="flex1 text_pole"> |
| 20 | 20 | <option value="aimlapi">AI/ML API</option> |
| 21 | 21 | <option value="anthropic">AnthropicClaude</option> |
| 22 | 22 | <option value="cohere">Cohere</option> |
| 23 | 23 | <option value="custom" data-i18n="Custom (OpenAI-compatible)">Custom (OpenAI-compatible)</option> |
| 24 | 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 | 33 | import { TtsWebuiProvider } from './tts-webui.js'; |
| 34 | 34 | import { PollinationsTtsProvider } from './pollinations.js'; |
| 35 | 35 | import { MiniMaxTtsProvider } from './minimax.js'; |
| 36 | +import { ElectronHubTtsProvider } from './electronhub.js'; | |
| 36 | 37 | |
| 37 | 38 | const UPDATE_INTERVAL = 1000; |
| 38 | 39 | const wrapper = new ModuleWorkerWrapper(moduleWorker); |
| @@ -123,6 +124,7 @@ const ttsProviders = { | ||
| 123 | 124 | 'CosyVoice (Unofficial)': CosyVoiceProvider, |
| 124 | 125 | Edge: EdgeTtsProvider, |
| 125 | 126 | ElevenLabs: ElevenLabsTtsProvider, |
| 127 | + 'Electron Hub': ElectronHubTtsProvider, | |
| 126 | 128 | 'Google Translate': GoogleTranslateTtsProvider, |
| 127 | 129 | 'Google Gemini TTS': GoogleNativeTtsProvider, |
| 128 | 130 | GSVI: GSVITtsProvider, |
| @@ -276,6 +276,8 @@ export const settingsToUpdate = { | ||
| 276 | 276 | perplexity_model: ['#model_perplexity_select', 'perplexity_model', false, true], |
| 277 | 277 | groq_model: ['#model_groq_select', 'groq_model', false, true], |
| 278 | 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 | 281 | nanogpt_model: ['#model_nanogpt_select', 'nanogpt_model', false, true], |
| 280 | 282 | deepseek_model: ['#model_deepseek_select', 'deepseek_model', false, true], |
| 281 | 283 | aimlapi_model: ['#model_aimlapi_select', 'aimlapi_model', false, true], |
| @@ -379,6 +381,8 @@ const default_settings = { | ||
| 379 | 381 | perplexity_model: 'sonar-pro', |
| 380 | 382 | groq_model: 'llama-3.3-70b-versatile', |
| 381 | 383 | electronhub_model: 'gpt-4o-mini', |
| 384 | + electronhub_sort_models: 'alphabetically', | |
| 385 | + electronhub_group_models: false, | |
| 382 | 386 | nanogpt_model: 'gpt-4o-mini', |
| 383 | 387 | deepseek_model: 'deepseek-chat', |
| 384 | 388 | aimlapi_model: 'gpt-4o-mini-2024-07-18', |
| @@ -473,6 +477,8 @@ const oai_settings = { | ||
| 473 | 477 | perplexity_model: 'sonar-pro', |
| 474 | 478 | groq_model: 'llama-3.1-70b-versatile', |
| 475 | 479 | electronhub_model: 'gpt-4o-mini', |
| 480 | + electronhub_sort_models: 'alphabetically', | |
| 481 | + electronhub_group_models: false, | |
| 476 | 482 | nanogpt_model: 'gpt-4o-mini', |
| 477 | 483 | deepseek_model: 'deepseek-chat', |
| 478 | 484 | aimlapi_model: 'gpt-4-turbo', |
| @@ -1710,6 +1716,59 @@ function calculateOpenRouterCost() { | ||
| 1710 | 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 | 1772 | function saveModelList(data) { |
| 1714 | 1773 | model_list = data.map((model) => ({ ...model })); |
| 1715 | 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 | 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 | 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 | 1874 | const selectedModel = model_list.find(model => model.id === oai_settings.electronhub_model); |
| 1818 | 1875 | if (model_list.length > 0 && (!selectedModel || !oai_settings.electronhub_model)) { |
| @@ -2041,6 +2098,61 @@ function openRouterGroupByVendor(array) { | ||
| 2041 | 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 | 2156 | function aimlapiGroupByVendor(array) { |
| 2045 | 2157 | return array.reduce((acc, curr) => { |
| 2046 | 2158 | const vendor = curr.info.developer; |
| @@ -2191,7 +2303,7 @@ async function sendOpenAIRequest(type, messages, signal, { jsonSchema = null } = | ||
| 2191 | 2303 | const useLogprobs = !!power_user.request_token_probabilities; |
| 2192 | 2304 | const canMultiSwipe = oai_settings.n > 1 && !isContinue && !isImpersonate && !isQuiet && (isOAI || isAzureOpenAI || isCustom || isXAI || isAimlapi || isMoonshot); |
| 2193 | 2305 | |
| 2194 | 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 | 2307 | if (oai_settings.bias_preset_selected |
| 2196 | 2308 | && logitBiasSources.includes(oai_settings.chat_completion_source) |
| 2197 | 2309 | && Array.isArray(oai_settings.bias_presets[oai_settings.bias_preset_selected]) |
| @@ -3523,6 +3635,8 @@ function loadOpenAISettings(data, settings) { | ||
| 3523 | 3635 | oai_settings.perplexity_model = settings.perplexity_model ?? default_settings.perplexity_model; |
| 3524 | 3636 | oai_settings.groq_model = settings.groq_model ?? default_settings.groq_model; |
| 3525 | 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 | 3640 | oai_settings.nanogpt_model = settings.nanogpt_model ?? default_settings.nanogpt_model; |
| 3527 | 3641 | oai_settings.deepseek_model = settings.deepseek_model ?? default_settings.deepseek_model; |
| 3528 | 3642 | oai_settings.aimlapi_model = settings.aimlapi_model ?? default_settings.aimlapi_model; |
| @@ -3666,6 +3780,8 @@ function loadOpenAISettings(data, settings) { | ||
| 3666 | 3780 | $('#openrouter_allow_fallbacks').prop('checked', oai_settings.openrouter_allow_fallbacks); |
| 3667 | 3781 | $('#openrouter_providers_chat').val(oai_settings.openrouter_providers).trigger('change'); |
| 3668 | 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 | 3785 | $('#squash_system_messages').prop('checked', oai_settings.squash_system_messages); |
| 3670 | 3786 | $('#continue_prefill').prop('checked', oai_settings.continue_prefill); |
| 3671 | 3787 | $('#openai_function_calling').prop('checked', oai_settings.function_calling); |
| @@ -3928,6 +4044,8 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) { | ||
| 3928 | 4044 | pollinations_model: settings.pollinations_model, |
| 3929 | 4045 | aimlapi_model: settings.aimlapi_model, |
| 3930 | 4046 | electronhub_model: settings.electronhub_model, |
| 4047 | + electronhub_sort_models: settings.electronhub_sort_models, | |
| 4048 | + electronhub_group_models: settings.electronhub_group_models, | |
| 3931 | 4049 | moonshot_model: settings.moonshot_model, |
| 3932 | 4050 | fireworks_model: settings.fireworks_model, |
| 3933 | 4051 | cometapi_model: settings.cometapi_model, |
| @@ -5093,6 +5211,8 @@ async function onModelChange() { | ||
| 5093 | 5211 | $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input'); |
| 5094 | 5212 | oai_settings.temp_openai = Math.min(oai_max_temp, oai_settings.temp_openai); |
| 5095 | 5213 | $('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input'); |
| 5214 | + | |
| 5215 | + calculateElectronHubCost(); | |
| 5096 | 5216 | } |
| 5097 | 5217 | |
| 5098 | 5218 | if (oai_settings.chat_completion_source === chat_completion_sources.NANOGPT) { |
| @@ -5220,6 +5340,10 @@ async function onOpenrouterModelSortChange() { | ||
| 5220 | 5340 | await getStatusOpen(); |
| 5221 | 5341 | } |
| 5222 | 5342 | |
| 5343 | +async function onElectronHubModelSortChange() { | |
| 5344 | + await getStatusOpen(); | |
| 5345 | +} | |
| 5346 | + | |
| 5223 | 5347 | async function onNewPresetClick() { |
| 5224 | 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 | 6241 | oai_settings.openai_max_context = Number($(this).val()); |
| 6118 | 6242 | $('#openai_max_context_counter').val(`${$(this).val()}`); |
| 6119 | 6243 | calculateOpenRouterCost(); |
| 6244 | + calculateElectronHubCost(); | |
| 6120 | 6245 | saveSettingsDebounced(); |
| 6121 | 6246 | }); |
| 6122 | 6247 | |
| 6123 | 6248 | $('#openai_max_tokens').on('input', function () { |
| 6124 | 6249 | oai_settings.openai_max_tokens = Number($(this).val()); |
| 6125 | 6250 | calculateOpenRouterCost(); |
| 6251 | + calculateElectronHubCost(); | |
| 6126 | 6252 | saveSettingsDebounced(); |
| 6127 | 6253 | }); |
| 6128 | 6254 | |
| @@ -6326,6 +6452,16 @@ export function initOpenAI() { | ||
| 6326 | 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 | 6465 | $('#squash_system_messages').on('input', function () { |
| 6330 | 6466 | oai_settings.squash_system_messages = !!$(this).prop('checked'); |
| 6331 | 6467 | saveSettingsDebounced(); |
| @@ -6503,6 +6639,14 @@ export function initOpenAI() { | ||
| 6503 | 6639 | width: '100%', |
| 6504 | 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 | 6650 | $('#completion_prompt_manager_popup_entry_form_injection_trigger').select2({ |
| 6507 | 6651 | placeholder: t`All types (default)`, |
| 6508 | 6652 | width: '100%', |
| @@ -6549,6 +6693,8 @@ export function initOpenAI() { | ||
| 6549 | 6693 | $('#model_openrouter_select').on('change', onModelChange); |
| 6550 | 6694 | $('#openrouter_group_models').on('change', onOpenrouterModelSortChange); |
| 6551 | 6695 | $('#openrouter_sort_models').on('change', onOpenrouterModelSortChange); |
| 6696 | + $('#electronhub_group_models').on('change', onElectronHubModelSortChange); | |
| 6697 | + $('#electronhub_sort_models').on('change', onElectronHubModelSortChange); | |
| 6552 | 6698 | $('#model_ai21_select').on('change', onModelChange); |
| 6553 | 6699 | $('#model_mistralai_select').on('change', onModelChange); |
| 6554 | 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 | 702 | if (oai_settings.chat_completion_source == chat_completion_sources.COHERE) { |
| 652 | 703 | if (oai_settings.cohere_model.includes('command-a')) { |
| 653 | 704 | return commandATokenizer; |
| @@ -2940,6 +2940,7 @@ select option:not(:checked) { | ||
| 2940 | 2940 | text-align: center; |
| 2941 | 2941 | } |
| 2942 | 2942 | |
| 2943 | +.menu_button[disabled], | |
| 2943 | 2944 | .menu_button.disabled { |
| 2944 | 2945 | filter: brightness(75%) grayscale(1); |
| 2945 | 2946 | opacity: 0.5; |
| @@ -3675,8 +3676,8 @@ grammarly-extension { | ||
| 3675 | 3676 | min-width: calc(1.25em + 12px); |
| 3676 | 3677 | } |
| 3677 | 3678 | |
| 3678 | 3679 | .menu_button:not(.disabled):not([disabled]):hover, |
| 3679 | 3680 | .menu_button:not(.disabled):not([disabled]).active { |
| 3680 | 3681 | background-color: var(--white30a); |
| 3681 | 3682 | } |
| 3682 | 3683 | |
| @@ -1255,6 +1255,7 @@ async function sendElectronHubRequest(request, response) { | ||
| 1255 | 1255 | 'frequency_penalty': request.body.frequency_penalty, |
| 1256 | 1256 | 'top_p': request.body.top_p, |
| 1257 | 1257 | 'top_k': request.body.top_k, |
| 1258 | + 'logit_bias': request.body.logit_bias, | |
| 1258 | 1259 | 'seed': request.body.seed, |
| 1259 | 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 | 426 | router.post('/generate-image', async (request, response) => { |
| 329 | 427 | try { |
| 330 | 428 | const key = readSecret(request.user.directories, SECRET_KEYS.OPENAI); |