Merge branch 'staging' of https://github.com/Cohee1207/SillyTavern into staging

This commit is contained in:
RossAscends 2023-09-04 14:50:41 +09:00
commit f468a33d60
14 changed files with 1459 additions and 561 deletions

View File

@ -38,11 +38,14 @@ set "node_installer_path=%temp%\NodejsInstaller.msi"
REM Environment Variables (winget) REM Environment Variables (winget)
set "winget_path=%userprofile%\AppData\Local\Microsoft\WindowsApps" set "winget_path=%userprofile%\AppData\Local\Microsoft\WindowsApps"
REM Environment Variables (TOOLBOX Install Extras)
set "miniconda_path=%userprofile%\miniconda"
REM Check if Winget is installed; if not, then install it REM Check if Winget is installed; if not, then install it
winget --version > nul 2>&1 winget --version > nul 2>&1
if %errorlevel% neq 0 ( if %errorlevel% neq 0 (
echo %blue_fg_strong%[INFO]%reset% Winget is not installed on this system. echo %yellow_fg_strong%[WARN] Winget is not installed on this system.
echo %blue_fg_strong%[INFO]%reset% Installing Winget... echo %blue_fg_strong%[INFO]%reset% Installing Winget...
bitsadmin /transfer "Microsoft.DesktopAppInstaller_8wekyb3d8bbwe" /download /priority FOREGROUND "https://github.com/microsoft/winget-cli/releases/download/v1.5.2201/Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle" "%temp%\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle" bitsadmin /transfer "Microsoft.DesktopAppInstaller_8wekyb3d8bbwe" /download /priority FOREGROUND "https://github.com/microsoft/winget-cli/releases/download/v1.5.2201/Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle" "%temp%\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle"
start "" "%temp%\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle" start "" "%temp%\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle"
@ -106,33 +109,37 @@ echo %blue_fg_strong%/ Home%reset%
echo ------------------------------------- echo -------------------------------------
echo What would you like to do? echo What would you like to do?
echo 1. Start SillyTavern echo 1. Start SillyTavern
echo 2. Update echo 2. Start SillyTavern + Extras
echo 3. Switch to release branch echo 3. Update
echo 4. Switch to staging branch echo 4. Backup
echo 5. Backup echo 5. Switch branch
echo 6. Toolbox echo 6. Toolbox
echo 7. Exit echo 7. Exit
REM Get the current Git branch REM Get the current Git branch
for /f %%i in ('git branch --show-current') do set current_branch=%%i for /f %%i in ('git branch --show-current') do set current_branch=%%i
echo ======== VERSION STATUS ========= echo ======== VERSION STATUS =========
echo Current branch: %cyan_fg_strong%%current_branch%%reset% echo SillyTavern branch: %cyan_fg_strong%%current_branch%%reset%
echo Update Status: %update_status% echo Update Status: %update_status%
echo ================================= echo =================================
set /p choice=Choose Your Destiny:
set "choice="
set /p "choice=Choose Your Destiny (default is 1): "
REM Default to choice 1 if no input is provided
if not defined choice set "choice=1"
REM Home - backend REM Home - backend
if "%choice%"=="1" ( if "%choice%"=="1" (
call :start call :start
) else if "%choice%"=="2" ( ) else if "%choice%"=="2" (
call :update call :start_extras
) else if "%choice%"=="3" ( ) else if "%choice%"=="3" (
call :switch_release call :update
) else if "%choice%"=="4" ( ) else if "%choice%"=="4" (
call :switch_staging
) else if "%choice%"=="5" (
call :backup_menu call :backup_menu
) else if "%choice%"=="5" (
call :switchbrance_menu
) else if "%choice%"=="6" ( ) else if "%choice%"=="6" (
call :toolbox call :toolbox
) else if "%choice%"=="7" ( ) else if "%choice%"=="7" (
@ -144,6 +151,7 @@ if "%choice%"=="1" (
goto :home goto :home
) )
:start :start
REM Check if Node.js is installed REM Check if Node.js is installed
node --version > nul 2>&1 node --version > nul 2>&1
@ -154,20 +162,26 @@ if %errorlevel% neq 0 (
pause pause
goto :home goto :home
) )
echo %blue_fg_strong%[INFO]%reset% A new window has been launched.
echo Launching SillyTavern... start /wait cmd /c start.bat
cls
pushd %~dp0
call npm install --no-audit
node server.js
pause
popd
goto :home goto :home
:start_extras
REM Run conda activate from the Miniconda installation
call "%miniconda_path%\Scripts\activate.bat"
REM Activate the sillytavernextras environment
call conda activate sillytavernextras
REM Start SillyTavern Extras with desired configurations
python server.py --coqui-gpu --rvc-save-file --cuda-device=0 --max-content-length=1000 --enable-modules=caption,summarize,classify,rvc,coqui-tts --classification-model=joeddav/distilbert-base-uncased-go-emotions-student --share
goto :home
:update :update
echo Updating... echo Updating...
pushd %~dp0 pushd %~dp0
REM Check if git is installed REM Check if git is installed
git --version > nul 2>&1 git --version > nul 2>&1
if %errorlevel% neq 0 ( if %errorlevel% neq 0 (
@ -185,39 +199,76 @@ pause
goto :home goto :home
:switch_release REM Switch Brance - frontend
REM Check if git is installed :switchbrance_menu
git --version > nul 2>&1 cls
if %errorlevel% neq 0 ( echo %blue_fg_strong%/ Home / Switch Branch%reset%
echo %red_fg_strong%[ERROR] git command not found in PATH%reset% echo -------------------------------------
echo %red_bg%Please make sure Git is installed and added to your PATH.%reset% echo What would you like to do?
echo %blue_bg%To install Git go to Toolbox%reset% echo 1. Switch to Release - SillyTavern
pause echo 2. Switch to Staging - SillyTavern
echo 3. Switch to Main - Extras
echo 4. Switch to Neo - Extras
echo 5. Back to Home
REM Get the current Git branch
for /f %%i in ('git branch --show-current') do set current_branch=%%i
echo ======== VERSION STATUS =========
echo SillyTavern branch: %cyan_fg_strong%%current_branch%%reset%
echo Extras branch: %cyan_fg_strong%%current_branch%%reset%
echo =================================
set /p brance_choice=Choose Your Destiny:
REM Switch Brance - backend
if "%brance_choice%"=="1" (
call :switch_release_st
) else if "%brance_choice%"=="2" (
call :switch_staging_st
) else if "%brance_choice%"=="3" (
call :switch_main_ste
) else if "%brance_choice%"=="4" (
call :switch_neo_ste
) else if "%brance_choice%"=="5" (
goto :home goto :home
) else (
color 6
echo WARNING: Invalid number. Please insert a valid number.
pause
goto :switchbrance_menu
) )
echo Switching to release branch...
:switch_release_st
echo %blue_fg_strong%[INFO]%reset% Switching to release branch...
git switch release git switch release
pause pause
goto :home goto :switchbrance_menu
:switch_staging :switch_staging_st
REM Check if git is installed echo %blue_fg_strong%[INFO]%reset% Switching to staging branch...
git --version > nul 2>&1
if %errorlevel% neq 0 (
echo %red_fg_strong%[ERROR] git command not found in PATH%reset%
echo %red_bg%Please make sure git is installed and added to your PATH.%reset%
pause
goto :home
)
echo Switching to staging branch...
git switch staging git switch staging
pause pause
goto :home goto :switchbrance_menu
:switch_main_ste
echo %blue_fg_strong%[INFO]%reset% Switching to main branch...
cd SillyTavern-extras
git switch main
pause
goto :switchbrance_menu
REM backup - frontend
:switch_neo_ste
echo %blue_fg_strong%[INFO]%reset% Switching to neo branch...
cd SillyTavern-extras
git switch neo
pause
goto :switchbrance_menu
REM Backup - Frontend
:backup_menu :backup_menu
REM Check if 7-Zip is installed REM Check if 7-Zip is installed
7z > nul 2>&1 7z > nul 2>&1
@ -228,7 +279,6 @@ if %errorlevel% neq 0 (
pause pause
goto :home goto :home
) )
cls cls
echo %blue_fg_strong%/ Home / Backup%reset% echo %blue_fg_strong%/ Home / Backup%reset%
echo ------------------------------------- echo -------------------------------------
@ -240,7 +290,7 @@ echo 3. Back to Home
set /p backup_choice=Choose Your Destiny: set /p backup_choice=Choose Your Destiny:
REM backup - backend REM Backup - Backend
if "%backup_choice%"=="1" ( if "%backup_choice%"=="1" (
call :create_backup call :create_backup
) else if "%backup_choice%"=="2" ( ) else if "%backup_choice%"=="2" (
@ -254,207 +304,6 @@ if "%backup_choice%"=="1" (
goto :backup_menu goto :backup_menu
) )
REM toolbox - frontend
:toolbox
cls
echo %blue_fg_strong%/ Home / Toolbox%reset%
echo -------------------------------------
echo What would you like to do?
REM color 7
echo 1. Install 7-Zip
echo 2. Install FFmpeg
echo 3. Install Node.js
echo 4. Edit Environment - Power Users only!
echo 5. Reinstall SillyTavern
echo 6. Back to Home
set /p toolbox_choice=Choose Your Destiny:
REM toolbox - backend
if "%toolbox_choice%"=="1" (
call :install7zip
) else if "%toolbox_choice%"=="2" (
call :installffmpeg
) else if "%toolbox_choice%"=="3" (
call :installnodejs
) else if "%toolbox_choice%"=="4" (
call :editenvironment
) else if "%toolbox_choice%"=="5" (
call :reinstallsillytavern
) else if "%toolbox_choice%"=="6" (
goto :home
) else (
color 6
echo WARNING: Invalid number. Please insert a valid number.
pause
goto :toolbox
)
:install7zip
echo %blue_fg_strong%[INFO] Installing 7-Zip...%reset%
winget install -e --id 7zip.7zip
rem Get the current PATH value from the registry
for /f "tokens=2*" %%A in ('reg query "HKCU\Environment" /v PATH') do set "current_path=%%B"
rem Check if the paths are already in the current PATH
echo %current_path% | find /i "%zip7_install_path%" > nul
set "zip7_path_exists=%errorlevel%"
rem Append the new paths to the current PATH only if they don't exist
if %zip7_path_exists% neq 0 (
set "new_path=%current_path%;%zip7_install_path%"
echo %green_fg_strong%7-Zip added to PATH.%reset%
) else (
set "new_path=%current_path%"
echo %blue_fg_strong%[INFO] 7-Zip already exists in PATH.%reset%
)
rem Update the PATH value in the registry
reg add "HKCU\Environment" /v PATH /t REG_EXPAND_SZ /d "%new_path%" /f
rem Update the PATH value for the current session
setx PATH "%new_path%"
echo %green_fg_strong%7-Zip is installed. Please restart the Launcher.%reset%
pause
exit
:installffmpeg
REM Check if 7-Zip is installed
7z > nul 2>&1
if %errorlevel% neq 0 (
echo %red_fg_strong%[ERROR] 7z command not found in PATH%reset%
echo %red_bg%Please make sure 7-Zip is installed and added to your PATH.%reset%
echo %blue_bg%To install 7-Zip go to Toolbox%reset%
pause
goto :toolbox
)
echo %blue_fg_strong%[INFO]%reset% Downloading FFmpeg archive...
rem bitsadmin /transfer "ffmpeg" /download /priority FOREGROUND "%ffmpeg_url%" "%ffdownload_path%"
curl -o "%ffdownload_path%" "%ffmpeg_url%"
echo %blue_fg_strong%[INFO]%reset% Creating ffmpeg directory if it doesn't exist...
if not exist "%ffextract_path%" (
mkdir "%ffextract_path%"
)
echo %blue_fg_strong%[INFO]%reset% Extracting FFmpeg archive...
7z x "%ffdownload_path%" -o"%ffextract_path%"
echo %blue_fg_strong%[INFO]%reset% Moving FFmpeg contents to C:\ffmpeg...
for /d %%i in ("%ffextract_path%\ffmpeg-*-full_build") do (
xcopy "%%i\bin" "%ffextract_path%\bin" /E /I /Y
xcopy "%%i\doc" "%ffextract_path%\doc" /E /I /Y
xcopy "%%i\presets" "%ffextract_path%\presets" /E /I /Y
rd "%%i" /S /Q
)
rem Get the current PATH value from the registry
for /f "tokens=2*" %%A in ('reg query "HKCU\Environment" /v PATH') do set "current_path=%%B"
rem Check if the paths are already in the current PATH
echo %current_path% | find /i "%bin_path%" > nul
set "ff_path_exists=%errorlevel%"
rem Append the new paths to the current PATH only if they don't exist
if %ff_path_exists% neq 0 (
set "new_path=%current_path%;%bin_path%"
echo %green_fg_strong%ffmpeg added to PATH.%reset%
) else (
set "new_path=%current_path%"
echo %blue_fg_strong%[INFO] ffmpeg already exists in PATH.%reset%
)
rem Update the PATH value in the registry
reg add "HKCU\Environment" /v PATH /t REG_EXPAND_SZ /d "%new_path%" /f
rem Update the PATH value for the current session
setx PATH "%new_path%" > nul
del "%ffdownload_path%"
echo %green_fg_strong%FFmpeg is installed. Please restart the Launcher.%reset%
pause
exit
:installnodejs
echo %blue_fg_strong%[INFO]%reset% Installing Node.js...
winget install -e --id OpenJS.NodeJS
echo %green_fg_strong%Node.js is installed. Please restart the Launcher.%reset%
pause
exit
:editenvironment
rundll32.exe sysdm.cpl,EditEnvironmentVariables
goto :toolbox
:reinstallsillytavern
setlocal enabledelayedexpansion
chcp 65001 > nul
REM Define the names of items to be excluded
set "script_name=%~nx0"
set "excluded_folders=backups"
set "excluded_files=!script_name!"
REM Confirm with the user before proceeding
echo.
echo %red_bg%╔════ DANGER ZONE ══════════════════════════════════════════════════════════════════════════════╗%reset%
echo %red_bg%║ WARNING: This will delete all data in the current branch except the Backups. ║%reset%
echo %red_bg%║ If you want to keep any data, make sure to create a backup before proceeding. ║%reset%
echo %red_bg%╚═══════════════════════════════════════════════════════════════════════════════════════════════╝%reset%
echo.
echo Are you sure you want to proceed? (Y/N)
set /p "confirmation="
if /i "!confirmation!"=="Y" (
REM Remove non-excluded folders
for /d %%D in (*) do (
set "exclude_folder="
for %%E in (!excluded_folders!) do (
if "%%D"=="%%E" set "exclude_folder=true"
)
if not defined exclude_folder (
rmdir /s /q "%%D" 2>nul
)
)
REM Remove non-excluded files
for %%F in (*) do (
set "exclude_file="
for %%E in (!excluded_files!) do (
if "%%F"=="%%E" set "exclude_file=true"
)
if not defined exclude_file (
del /f /q "%%F" 2>nul
)
)
REM Clone repo into %temp% folder
git clone https://github.com/SillyTavern/SillyTavern.git "%temp%\SillyTavernTemp"
REM Move the contents of the temporary folder to the current directory
xcopy /e /y "%temp%\SillyTavernTemp\*" .
REM Clean up the temporary folder
rmdir /s /q "%temp%\SillyTavernTemp"
echo %green_fg_strong%SillyTavern reinstalled successfully!%reset%
) else (
echo Reinstall canceled.
)
endlocal
pause
goto :toolbox
:create_backup :create_backup
REM Create a backup using 7zip REM Create a backup using 7zip
7z a "backups\backup_.7z" ^ 7z a "backups\backup_.7z" ^
@ -549,3 +398,244 @@ if "%restore_choice%" geq "1" (
) )
pause pause
goto :backup_menu goto :backup_menu
REM Toolbox - Frontend
:toolbox
cls
echo %blue_fg_strong%/ Home / Toolbox%reset%
echo -------------------------------------
echo What would you like to do?
REM color 7
echo 1. Install 7-Zip
echo 2. Install FFmpeg
echo 3. Install Node.js
echo 4. Edit Environment
echo 5. Reinstall SillyTavern
echo 6. Reinstall Extras
echo 7. Back to Home
set /p toolbox_choice=Choose Your Destiny:
REM Toolbox - Backend
if "%toolbox_choice%"=="1" (
call :install7zip
) else if "%toolbox_choice%"=="2" (
call :installffmpeg
) else if "%toolbox_choice%"=="3" (
call :installnodejs
) else if "%toolbox_choice%"=="4" (
call :editenvironment
) else if "%toolbox_choice%"=="5" (
call :reinstallsillytavern
) else if "%toolbox_choice%"=="6" (
call :reinstallextras
) else if "%toolbox_choice%"=="7" (
goto :home
) else (
color 6
echo WARNING: Invalid number. Please insert a valid number.
pause
goto :toolbox
)
:install7zip
echo %blue_fg_strong%[INFO]%reset% Installing 7-Zip...
winget install -e --id 7zip.7zip
rem Get the current PATH value from the registry
for /f "tokens=2*" %%A in ('reg query "HKCU\Environment" /v PATH') do set "current_path=%%B"
rem Check if the paths are already in the current PATH
echo %current_path% | find /i "%zip7_install_path%" > nul
set "zip7_path_exists=%errorlevel%"
rem Append the new paths to the current PATH only if they don't exist
if %zip7_path_exists% neq 0 (
set "new_path=%current_path%;%zip7_install_path%"
echo %green_fg_strong%7-Zip added to PATH.%reset%
) else (
set "new_path=%current_path%"
echo %blue_fg_strong%[INFO] 7-Zip already exists in PATH.%reset%
)
rem Update the PATH value in the registry
reg add "HKCU\Environment" /v PATH /t REG_EXPAND_SZ /d "%new_path%" /f
rem Update the PATH value for the current session
setx PATH "%new_path%"
echo %green_fg_strong%7-Zip is installed. Please restart the Launcher.%reset%
pause
exit
:installffmpeg
REM Check if 7-Zip is installed
7z > nul 2>&1
if %errorlevel% neq 0 (
echo %red_fg_strong%[ERROR] 7z command not found in PATH%reset%
echo %red_bg%Please make sure 7-Zip is installed and added to your PATH.%reset%
echo %blue_bg%To install 7-Zip go to Toolbox%reset%
pause
goto :toolbox
)
echo %blue_fg_strong%[INFO]%reset% Downloading FFmpeg archive...
rem bitsadmin /transfer "ffmpeg" /download /priority FOREGROUND "%ffmpeg_url%" "%ffdownload_path%"
curl -o "%ffdownload_path%" "%ffmpeg_url%"
echo %blue_fg_strong%[INFO]%reset% Creating ffmpeg directory if it doesn't exist...
if not exist "%ffextract_path%" (
mkdir "%ffextract_path%"
)
echo %blue_fg_strong%[INFO]%reset% Extracting FFmpeg archive...
7z x "%ffdownload_path%" -o"%ffextract_path%"
echo %blue_fg_strong%[INFO]%reset% Moving FFmpeg contents to C:\ffmpeg...
for /d %%i in ("%ffextract_path%\ffmpeg-*-full_build") do (
xcopy "%%i\bin" "%ffextract_path%\bin" /E /I /Y
xcopy "%%i\doc" "%ffextract_path%\doc" /E /I /Y
xcopy "%%i\presets" "%ffextract_path%\presets" /E /I /Y
rd "%%i" /S /Q
)
rem Get the current PATH value from the registry
for /f "tokens=2*" %%A in ('reg query "HKCU\Environment" /v PATH') do set "current_path=%%B"
rem Check if the paths are already in the current PATH
echo %current_path% | find /i "%bin_path%" > nul
set "ff_path_exists=%errorlevel%"
rem Append the new paths to the current PATH only if they don't exist
if %ff_path_exists% neq 0 (
set "new_path=%current_path%;%bin_path%"
echo %green_fg_strong%ffmpeg added to PATH.%reset%
) else (
set "new_path=%current_path%"
echo %blue_fg_strong%[INFO] ffmpeg already exists in PATH.%reset%
)
rem Update the PATH value in the registry
reg add "HKCU\Environment" /v PATH /t REG_EXPAND_SZ /d "%new_path%" /f
rem Update the PATH value for the current session
setx PATH "%new_path%" > nul
del "%ffdownload_path%"
echo %green_fg_strong%FFmpeg is installed. Please restart the Launcher.%reset%
pause
exit
:installnodejs
echo %blue_fg_strong%[INFO]%reset% Installing Node.js...
winget install -e --id OpenJS.NodeJS
echo %green_fg_strong%Node.js is installed. Please restart the Launcher.%reset%
pause
exit
:editenvironment
rundll32.exe sysdm.cpl,EditEnvironmentVariables
goto :toolbox
:reinstallsillytavern
setlocal enabledelayedexpansion
chcp 65001 > nul
REM Define the names of items to be excluded
set "script_name=%~nx0"
set "excluded_folders=backups"
set "excluded_files=!script_name!"
REM Confirm with the user before proceeding
echo.
echo %red_bg%╔════ DANGER ZONE ══════════════════════════════════════════════════════════════════════════════╗%reset%
echo %red_bg%║ WARNING: This will delete all data in the current branch except the Backups. ║%reset%
echo %red_bg%║ If you want to keep any data, make sure to create a backup before proceeding. ║%reset%
echo %red_bg%╚═══════════════════════════════════════════════════════════════════════════════════════════════╝%reset%
echo.
echo Are you sure you want to proceed? [Y/N]
set /p "confirmation="
if /i "!confirmation!"=="Y" (
REM Remove non-excluded folders
for /d %%D in (*) do (
set "exclude_folder="
for %%E in (!excluded_folders!) do (
if "%%D"=="%%E" set "exclude_folder=true"
)
if not defined exclude_folder (
rmdir /s /q "%%D" 2>nul
)
)
REM Remove non-excluded files
for %%F in (*) do (
set "exclude_file="
for %%E in (!excluded_files!) do (
if "%%F"=="%%E" set "exclude_file=true"
)
if not defined exclude_file (
del /f /q "%%F" 2>nul
)
)
REM Clone repo into %temp% folder
git clone https://github.com/SillyTavern/SillyTavern.git "%temp%\SillyTavernTemp"
REM Move the contents of the temporary folder to the current directory
xcopy /e /y "%temp%\SillyTavernTemp\*" .
REM Clean up the temporary folder
rmdir /s /q "%temp%\SillyTavernTemp"
echo %green_fg_strong%SillyTavern reinstalled successfully!%reset%
) else (
echo Reinstall canceled.
)
endlocal
pause
goto :toolbox
:reinstallextras
cls
echo %blue_fg_strong%SillyTavern Extras%reset%
echo ---------------------------------------------------------------
echo %blue_fg_strong%[INFO]%reset% Installing SillyTavern Extras...
echo --------------------------------
echo %cyan_fg_strong%This may take a while. Please be patient.%reset%
winget install -e --id Anaconda.Miniconda3
REM Run conda activate from the Miniconda installation
call "%miniconda_path%\Scripts\activate.bat"
REM Create a Conda environment named sillytavernextras
call conda create -n sillytavernextras -y
REM Activate the sillytavernextras environment
call conda activate sillytavernextras
REM Install Python 3.11 and Git in the sillytavernextras environment
call conda install python=3.11 git -y
REM Clone the SillyTavern Extras repository
git clone https://github.com/SillyTavern/SillyTavern-extras
REM Navigate to the SillyTavern-extras directory
cd SillyTavern-extras
REM Install Python dependencies from requirements files
pip install -r requirements-complete.txt
pip install -r requirements-rvc.txt
REM Start SillyTavern Extras with desired configurations
python server.py --coqui-gpu --rvc-save-file --cuda-device=0 --max-content-length=1000 --enable-modules=caption,summarize,classify,rvc,coqui-tts --classification-model=joeddav/distilbert-base-uncased-go-emotions-student --share
echo.
echo %green_fg_strong%SillyTavern Extras have been successfully installed.%reset%
pause
goto :toolbox

View File

@ -270,6 +270,16 @@
} }
} }
@media screen and (min-width: 1001px) {
#PygOverrides,
#ContextFormatting,
#UI-Theme-Block,
#UI-Customization,
#power-user-options-block {
flex: 1;
}
}
/*landscape mode phones and ipads*/ /*landscape mode phones and ipads*/
@media screen and (max-width: 1000px) and (orientation: landscape) { @media screen and (max-width: 1000px) and (orientation: landscape) {
body.waifuMode img.expression { body.waifuMode img.expression {
@ -407,4 +417,4 @@
#horde_model { #horde_model {
height: unset; height: unset;
} }
} }

View File

@ -1019,8 +1019,8 @@
</label> </label>
</div> </div>
<div class="range-block"> <div class="range-block">
<label class="checkbox_label" for="use_default_badwordids"> <label class="checkbox_label" for="use_default_badwordsids">
<input id="use_default_badwordids" type="checkbox" /> <input id="use_default_badwordsids" type="checkbox" />
<span data-i18n="Ban EOS Token"> <span data-i18n="Ban EOS Token">
Ban EOS Token Ban EOS Token
</span> </span>
@ -1049,7 +1049,7 @@
</div> </div>
<div class="range-block-range-and-counter"> <div class="range-block-range-and-counter">
<div class="range-block-range"> <div class="range-block-range">
<input type="range" id="mirostat_tau_kobold" name="volume" min="0" max="10" step="0.01" /> <input type="range" id="mirostat_tau_kobold" name="volume" min="0" max="20" step="0.01" />
</div> </div>
<div class="range-block-counter"> <div class="range-block-counter">
<div contenteditable="true" data-for="mirostat_tau_kobold" id="mirostat_tau_counter_kobold"> <div contenteditable="true" data-for="mirostat_tau_kobold" id="mirostat_tau_counter_kobold">
@ -1587,7 +1587,7 @@
</div> </div>
<div class="range-block-range-and-counter"> <div class="range-block-range-and-counter">
<div class="range-block-range"> <div class="range-block-range">
<input type="range" id="mirostat_tau_textgenerationwebui" name="volume" min="0" max="10" step="0.01" /> <input type="range" id="mirostat_tau_textgenerationwebui" name="volume" min="0" max="20" step="0.01" />
</div> </div>
<div class="range-block-counter"> <div class="range-block-counter">
<div contenteditable="true" data-for="mirostat_tau_textgenerationwebui" id="mirostat_tau_counter_textgenerationwebui"> <div contenteditable="true" data-for="mirostat_tau_textgenerationwebui" id="mirostat_tau_counter_textgenerationwebui">
@ -2151,13 +2151,13 @@
</a> </a>
</h3> </h3>
<div class="flex-container"> <div class="flex-container">
<div name="PygOverrides" class="flex1"> <div id="PygOverrides">
<div> <div>
<h4 data-i18n="Context Template"> <h4 data-i18n="Context Template">
Context Template Context Template
</h4> </h4>
<div class="preset_buttons flex-container"> <div class="preset_buttons flex-container">
<select id="context_presets" data-preset-manager-for="context" class="flex1 widthFitContent"></select> <select id="context_presets" data-preset-manager-for="context" class="flex1"></select>
<input type="file" hidden data-preset-manager-file="context" accept=".json, .settings"> <input type="file" hidden data-preset-manager-file="context" accept=".json, .settings">
<i id="context_set_default" class="menu_button fa-solid fa-heart" title="Auto-select this preset for Instruct Mode."></i> <i id="context_set_default" class="menu_button fa-solid fa-heart" title="Auto-select this preset for Instruct Mode."></i>
<i data-newbie-hidden data-preset-manager-update="context" class="menu_button fa-solid fa-save" title="Update current preset" data-i18n="[title]Update current preset"></i> <i data-newbie-hidden data-preset-manager-update="context" class="menu_button fa-solid fa-save" title="Update current preset" data-i18n="[title]Update current preset"></i>
@ -2357,7 +2357,7 @@
<small data-i18n="Stop Sequence">Stop Sequence</small> <small data-i18n="Stop Sequence">Stop Sequence</small>
</label> </label>
<div> <div>
<textarea id="instruct_stop_sequence" class="text_pole textarea_compact autoSetHeight" maxlength="200k0" placeholder="&mdash;" rows="1"></textarea> <textarea id="instruct_stop_sequence" class="text_pole textarea_compact autoSetHeight" maxlength="2000" placeholder="&mdash;" rows="1"></textarea>
</div> </div>
</div> </div>
<div class="flex1"> <div class="flex1">
@ -2374,7 +2374,7 @@
</div> </div>
</div> </div>
<div name="ContextFormatting" class="flex1"> <div id="ContextFormatting">
<div data-newbie-hidden> <div data-newbie-hidden>
<h4><span data-i18n="Tokenizer">Tokenizer</span> <h4><span data-i18n="Tokenizer">Tokenizer</span>
<a href="https://docs.sillytavern.app/usage/core-concepts/advancedformatting/#tokenizer" class="notes-link" target="_blank"> <a href="https://docs.sillytavern.app/usage/core-concepts/advancedformatting/#tokenizer" class="notes-link" target="_blank">
@ -2701,7 +2701,7 @@
<div id="version_display"></div> <div id="version_display"></div>
</div> </div>
<div class="flex-container spaceEvenly"> <div class="flex-container spaceEvenly">
<div id="UI-Theme-Block" class="flex-container flexFlowColumn drawer33pWidth"> <div id="UI-Theme-Block" class="flex-container flexFlowColumn wide100p">
<div id="color-picker-block" class="flex-container flexFlowColumn flexNoGap"> <div id="color-picker-block" class="flex-container flexFlowColumn flexNoGap">
<div id="UI-Mode-Block"> <div id="UI-Mode-Block">
<h4 data-i18n="UI Mode"> <h4 data-i18n="UI Mode">
@ -2837,7 +2837,7 @@
</div> </div>
</div> </div>
<div name="UI Customization" class="flex-container drawer33pWidth"> <div id="UI-Customization" class="flex-container wide100p">
<div class="ui-settings"> <div class="ui-settings">
<h4><span data-i18n="UI Customization">UI Customization</span></h4> <h4><span data-i18n="UI Customization">UI Customization</span></h4>
<div data-newbie-hidden class="range-block"> <div data-newbie-hidden class="range-block">
@ -2966,14 +2966,13 @@
<option value="-1" data-i18n="Always disabled">Always disabled</option> <option value="-1" data-i18n="Always disabled">Always disabled</option>
<option value="0" data-i18n="Automatic (desktop)">Automatic (desktop)</option> <option value="0" data-i18n="Automatic (desktop)">Automatic (desktop)</option>
<option value="1" data-i18n="Always enabled">Always enabled</option> <option value="1" data-i18n="Always enabled">Always enabled</option>
</select> </select>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div id="power-user-options-block" class="flex-container wide100p">
<div id="power-user-options-block" class="flex-container drawer33pWidth">
<div id="power-user-option-checkboxes"> <div id="power-user-option-checkboxes">
<h4 data-i18n="Power User Options">Power User Options</h4> <h4 data-i18n="Power User Options">Power User Options</h4>
<label data-newbie-hidden class="checkbox_label" for="swipes-checkbox"> <label data-newbie-hidden class="checkbox_label" for="swipes-checkbox">
@ -3146,12 +3145,12 @@
</div> </div>
<div class="alignitemsflexstart flex-container wide100p"> <div class="alignitemsflexstart flex-container wide100p">
<input id="extensions_url" type="text" class="flex1 heightFitContent text_pole widthNatural" maxlength="250" data-i18n="[placeholder]Extensions URL" placeholder="Extensions URL"> <input id="extensions_url" type="text" class="flex1 heightFitContent text_pole widthNatural" maxlength="250" data-i18n="[placeholder]Extensions URL" placeholder="Extensions URL">
<input id="extensions_api_key" type="text" class="flex1 heightFitContent text_pole widthNatural" maxlength="250" data-i18n="[placeholder]API key" placeholder="API key"> <input id="extensions_api_key" type="text" class="flex1 heightFitContent text_pole widthNatural" maxlength="250" data-i18n="[placeholder]API key" placeholder="Extras API key">
<div class="alignitemsflexstart extensions_url_block"> <div class="extensions_url_block">
<div class="" style="text-align: center"> <div id="extensions_connect" class="menu_button" data-i18n="Connect">Connect</div>
<input id="extensions_connect" class="menu_button" type="submit" data-i18n="Connect">Connect</input> <div id="extensions_details" class="menu_button_icon menu_button">
Manage extensions
</div> </div>
<input id="extensions_details" class="alignitemsflexstart menu_button" type="button" value="Manage extensions">
<div id="third_party_extension_button" title="Import Extension From Git Repo" class="menu_button fa-solid fa-cloud-arrow-down faSmallFontSquareFix"></div> <div id="third_party_extension_button" title="Import Extension From Git Repo" class="menu_button fa-solid fa-cloud-arrow-down faSmallFontSquareFix"></div>
</div> </div>
</div> </div>
@ -3533,8 +3532,8 @@
<select id="character_sort_order" title="Characters sorting order" data-i18n="[title]Characters sorting order"> <select id="character_sort_order" title="Characters sorting order" data-i18n="[title]Characters sorting order">
<option data-field="name" data-order="asc" data-i18n="A-Z">A-Z</option> <option data-field="name" data-order="asc" data-i18n="A-Z">A-Z</option>
<option data-field="name" data-order="desc" data-i18n="Z-A">Z-A</option> <option data-field="name" data-order="desc" data-i18n="Z-A">Z-A</option>
<option data-field="date_added" data-order="desc" data-i18n="Newest">Newest</option> <option data-field="create_date" data-order="desc" data-i18n="Newest">Newest</option>
<option data-field="date_added" data-order="asc" data-i18n="Oldest">Oldest</option> <option data-field="create_date" data-order="asc" data-i18n="Oldest">Oldest</option>
<option data-field="fav" data-order="desc" data-rule="boolean" data-i18n="Favorites">Favorites</option> <option data-field="fav" data-order="desc" data-rule="boolean" data-i18n="Favorites">Favorites</option>
<option data-field="date_last_chat" data-order="desc" data-i18n="Recent">Recent</option> <option data-field="date_last_chat" data-order="desc" data-i18n="Recent">Recent</option>
<option data-field="chat_size" data-order="desc" data-i18n="Most chats">Most chats</option> <option data-field="chat_size" data-order="desc" data-i18n="Most chats">Most chats</option>
@ -4536,6 +4535,7 @@
toastr.options.extendedTimeOut = 10000; // How long the toast will display after a user hovers over it toastr.options.extendedTimeOut = 10000; // How long the toast will display after a user hovers over it
toastr.options.progressBar = true; // Visually indicate how long before a toast expires. toastr.options.progressBar = true; // Visually indicate how long before a toast expires.
toastr.options.closeButton = true; // enable a close button toastr.options.closeButton = true; // enable a close button
toastr.options.positionClass = "toast-top-center"; // Where to position the toast container
</script> </script>
<script> <script>

View File

@ -557,6 +557,12 @@ export function dragElement(elmnt) {
//set a listener for mouseup to save new width/height //set a listener for mouseup to save new width/height
elmnt.off('mouseup').on('mouseup', () => { elmnt.off('mouseup').on('mouseup', () => {
console.debug(`Saving ${elmntName} Height/Width`) console.debug(`Saving ${elmntName} Height/Width`)
// check if the height or width actually changed
if (power_user.movingUIState[elmntName].width === width && power_user.movingUIState[elmntName].height === height) {
console.debug('no change detected, aborting save')
return
}
power_user.movingUIState[elmntName].width = width; power_user.movingUIState[elmntName].width = width;
power_user.movingUIState[elmntName].height = height; power_user.movingUIState[elmntName].height = height;
eventSource.emit('resizeUI', elmntName); eventSource.emit('resizeUI', elmntName);

View File

@ -75,20 +75,8 @@ async function initGallery(items, url) {
fnThumbnailOpen: viewWithDragbox, fnThumbnailOpen: viewWithDragbox,
}); });
$(document).ready(function () {
$('.nGY2GThumbnailImage').on('click', function () {
let imageUrl = $(this).find('.nGY2GThumbnailImg').attr('src');
// Do what you want with the imageUrl, for example:
// Display it in a full-size view or replace the gallery grid content with this image
console.log(imageUrl);
});
});
eventSource.on('resizeUI', function (elmntName) { eventSource.on('resizeUI', function (elmntName) {
console.log('resizeUI saw', elmntName);
// Your logic here
// If you want to resize the nanogallery2 instance when this event is triggered:
jQuery("#dragGallery").nanogallery2('resize'); jQuery("#dragGallery").nanogallery2('resize');
}); });
@ -98,6 +86,8 @@ async function initGallery(items, url) {
dropZone.off('dragleave'); dropZone.off('dragleave');
dropZone.off('drop'); dropZone.off('drop');
// Set dropzone height to be the same as the parent
dropZone.css('height', dropZone.parent().css('height'));
// Initialize dropzone handlers // Initialize dropzone handlers
dropZone.on('dragover', function (e) { dropZone.on('dragover', function (e) {

View File

@ -10,11 +10,13 @@ import {
appendImageToMessage, appendImageToMessage,
generateQuietPrompt, generateQuietPrompt,
this_chid, this_chid,
getCurrentChatId,
} from "../../../script.js"; } from "../../../script.js";
import { getApiUrl, getContext, extension_settings, doExtrasFetch, modules } from "../../extensions.js"; import { getApiUrl, getContext, extension_settings, doExtrasFetch, modules, renderExtensionTemplate } from "../../extensions.js";
import { selected_group } from "../../group-chats.js"; import { selected_group } from "../../group-chats.js";
import { stringFormat, initScrollHeight, resetScrollHeight, timestampToMoment, getCharaFilename, saveBase64AsFile } from "../../utils.js"; import { stringFormat, initScrollHeight, resetScrollHeight, timestampToMoment, getCharaFilename, saveBase64AsFile } from "../../utils.js";
import { getMessageTimeStamp, humanizedDateTime } from "../../RossAscends-mods.js"; import { getMessageTimeStamp, humanizedDateTime } from "../../RossAscends-mods.js";
import { SECRET_KEYS, secret_state } from "../../secrets.js";
export { MODULE_NAME }; export { MODULE_NAME };
// Wraps a string into monospace font-face span // Wraps a string into monospace font-face span
@ -27,10 +29,12 @@ const p = a => `<p>${a}</p>`
const MODULE_NAME = 'sd'; const MODULE_NAME = 'sd';
const UPDATE_INTERVAL = 1000; const UPDATE_INTERVAL = 1000;
const postHeaders = { const sources = {
'Content-Type': 'application/json', extras: 'extras',
'Bypass-Tunnel-Reminder': 'bypass', horde: 'horde',
}; auto: 'auto',
novel: 'novel',
}
const generationMode = { const generationMode = {
CHARACTER: 0, CHARACTER: 0,
@ -116,6 +120,8 @@ const helpString = [
].join('<br>'); ].join('<br>');
const defaultSettings = { const defaultSettings = {
source: sources.extras,
// CFG Scale // CFG Scale
scale_min: 1, scale_min: 1,
scale_max: 30, scale_max: 30,
@ -153,13 +159,48 @@ const defaultSettings = {
refine_mode: false, refine_mode: false,
prompts: promptTemplates, prompts: promptTemplates,
// AUTOMATIC1111 settings
auto_url: 'http://localhost:7860',
auto_auth: '',
hr_upscaler: 'Latent',
hr_scale: 2.0,
hr_scale_min: 1.0,
hr_scale_max: 4.0,
hr_scale_step: 0.1,
denoising_strength: 0.7,
denoising_strength_min: 0.0,
denoising_strength_max: 1.0,
denoising_strength_step: 0.01,
hr_second_pass_steps: 0,
hr_second_pass_steps_min: 0,
hr_second_pass_steps_max: 150,
hr_second_pass_steps_step: 1,
}
const getAutoRequestBody = () => ({ url: extension_settings.sd.auto_url, auth: extension_settings.sd.auto_auth });
function toggleSourceControls() {
$('.sd_settings [data-sd-source]').each(function () {
const source = $(this).data('sd-source');
$(this).toggle(source === extension_settings.sd.source);
});
} }
async function loadSettings() { async function loadSettings() {
// Initialize settings
if (Object.keys(extension_settings.sd).length === 0) { if (Object.keys(extension_settings.sd).length === 0) {
Object.assign(extension_settings.sd, defaultSettings); Object.assign(extension_settings.sd, defaultSettings);
} }
// Insert missing settings
for (const [key, value] of Object.entries(defaultSettings)) {
if (extension_settings.sd[key] === undefined) {
extension_settings.sd[key] = value;
}
}
if (extension_settings.sd.prompts === undefined) { if (extension_settings.sd.prompts === undefined) {
extension_settings.sd.prompts = promptTemplates; extension_settings.sd.prompts = promptTemplates;
} }
@ -175,19 +216,26 @@ async function loadSettings() {
extension_settings.sd.character_prompts = {}; extension_settings.sd.character_prompts = {};
} }
$('#sd_source').val(extension_settings.sd.source);
$('#sd_scale').val(extension_settings.sd.scale).trigger('input'); $('#sd_scale').val(extension_settings.sd.scale).trigger('input');
$('#sd_steps').val(extension_settings.sd.steps).trigger('input'); $('#sd_steps').val(extension_settings.sd.steps).trigger('input');
$('#sd_prompt_prefix').val(extension_settings.sd.prompt_prefix).trigger('input'); $('#sd_prompt_prefix').val(extension_settings.sd.prompt_prefix).trigger('input');
$('#sd_negative_prompt').val(extension_settings.sd.negative_prompt).trigger('input'); $('#sd_negative_prompt').val(extension_settings.sd.negative_prompt).trigger('input');
$('#sd_width').val(extension_settings.sd.width).trigger('input'); $('#sd_width').val(extension_settings.sd.width).trigger('input');
$('#sd_height').val(extension_settings.sd.height).trigger('input'); $('#sd_height').val(extension_settings.sd.height).trigger('input');
$('#sd_hr_scale').val(extension_settings.sd.hr_scale).trigger('input');
$('#sd_denoising_strength').val(extension_settings.sd.denoising_strength).trigger('input');
$('#sd_hr_second_pass_steps').val(extension_settings.sd.hr_second_pass_steps).trigger('input');
$('#sd_horde').prop('checked', extension_settings.sd.horde); $('#sd_horde').prop('checked', extension_settings.sd.horde);
$('#sd_horde_nsfw').prop('checked', extension_settings.sd.horde_nsfw); $('#sd_horde_nsfw').prop('checked', extension_settings.sd.horde_nsfw);
$('#sd_horde_karras').prop('checked', extension_settings.sd.horde_karras); $('#sd_horde_karras').prop('checked', extension_settings.sd.horde_karras);
$('#sd_restore_faces').prop('checked', extension_settings.sd.restore_faces); $('#sd_restore_faces').prop('checked', extension_settings.sd.restore_faces);
$('#sd_enable_hr').prop('checked', extension_settings.sd.enable_hr); $('#sd_enable_hr').prop('checked', extension_settings.sd.enable_hr);
$('#sd_refine_mode').prop('checked', extension_settings.sd.refine_mode); $('#sd_refine_mode').prop('checked', extension_settings.sd.refine_mode);
$('#sd_auto_url').val(extension_settings.sd.auto_url);
$('#sd_auto_auth').val(extension_settings.sd.auto_auth);
toggleSourceControls();
addPromptTemplates(); addPromptTemplates();
await Promise.all([loadSamplers(), loadModels()]); await Promise.all([loadSamplers(), loadModels()]);
@ -332,10 +380,11 @@ function onHeightInput() {
saveSettingsDebounced(); saveSettingsDebounced();
} }
async function onHordeInput() { async function onSourceChange() {
extension_settings.sd.source = $('#sd_source').find(':selected').val();
extension_settings.sd.model = null; extension_settings.sd.model = null;
extension_settings.sd.sampler = null; extension_settings.sd.sampler = null;
extension_settings.sd.horde = !!$(this).prop('checked'); toggleSourceControls();
saveSettingsDebounced(); saveSettingsDebounced();
await Promise.all([loadModels(), loadSamplers()]); await Promise.all([loadModels(), loadSamplers()]);
} }
@ -360,13 +409,140 @@ function onHighResFixInput() {
saveSettingsDebounced(); saveSettingsDebounced();
} }
function onAutoUrlInput() {
extension_settings.sd.auto_url = $('#sd_auto_url').val();
saveSettingsDebounced();
}
function onAutoAuthInput() {
extension_settings.sd.auto_auth = $('#sd_auto_auth').val();
saveSettingsDebounced();
}
function onHrUpscalerChange() {
extension_settings.sd.hr_upscaler = $('#sd_hr_upscaler').find(':selected').val();
saveSettingsDebounced();
}
function onHrScaleInput() {
extension_settings.sd.hr_scale = Number($('#sd_hr_scale').val());
$('#sd_hr_scale_value').text(extension_settings.sd.hr_scale.toFixed(1));
saveSettingsDebounced();
}
function onDenoisingStrengthInput() {
extension_settings.sd.denoising_strength = Number($('#sd_denoising_strength').val());
$('#sd_denoising_strength_value').text(extension_settings.sd.denoising_strength.toFixed(2));
saveSettingsDebounced();
}
function onHrSecondPassStepsInput() {
extension_settings.sd.hr_second_pass_steps = Number($('#sd_hr_second_pass_steps').val());
$('#sd_hr_second_pass_steps_value').text(extension_settings.sd.hr_second_pass_steps);
saveSettingsDebounced();
}
async function validateAutoUrl() {
try {
if (!extension_settings.sd.auto_url) {
throw new Error('URL is not set.');
}
const result = await fetch('/api/sd/ping', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify(getAutoRequestBody()),
});
if (!result.ok) {
throw new Error('SD WebUI returned an error.');
}
await loadSamplers();
await loadModels();
toastr.success('SD WebUI API connected.');
} catch (error) {
toastr.error(`Could not validate SD WebUI API: ${error.message}`);
}
}
async function onModelChange() { async function onModelChange() {
extension_settings.sd.model = $('#sd_model').find(':selected').val(); extension_settings.sd.model = $('#sd_model').find(':selected').val();
saveSettingsDebounced(); saveSettingsDebounced();
if (!extension_settings.sd.horde) { const cloudSources = [sources.horde, sources.novel];
if (cloudSources.includes(extension_settings.sd.source)) {
return;
}
toastr.info('Updating remote model...', 'Please wait');
if (extension_settings.sd.source === sources.extras) {
await updateExtrasRemoteModel(); await updateExtrasRemoteModel();
} }
if (extension_settings.sd.source === sources.auto) {
await updateAutoRemoteModel();
}
toastr.success('Model successfully loaded!', 'Stable Diffusion');
}
async function getAutoRemoteModel() {
try {
const result = await fetch('/api/sd/get-model', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify(getAutoRequestBody()),
});
if (!result.ok) {
throw new Error('SD WebUI returned an error.');
}
const data = await result.text();
return data;
} catch (error) {
console.error(error);
return null;
}
}
async function getAutoRemoteUpscalers() {
try {
const result = await fetch('/api/sd/upscalers', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify(getAutoRequestBody()),
});
if (!result.ok) {
throw new Error('SD WebUI returned an error.');
}
const data = await result.json();
return data;
} catch (error) {
console.error(error);
return [extension_settings.sd.hr_upscaler];
}
}
async function updateAutoRemoteModel() {
try {
const result = await fetch('/api/sd/set-model', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ ...getAutoRequestBody(), model: extension_settings.sd.model }),
});
if (!result.ok) {
throw new Error('SD WebUI returned an error.');
}
console.log('Model successfully updated on SD WebUI remote.');
} catch (error) {
console.error(error);
toastr.error(`Could not update SD WebUI model: ${error.message}`);
}
} }
async function updateExtrasRemoteModel() { async function updateExtrasRemoteModel() {
@ -374,7 +550,6 @@ async function updateExtrasRemoteModel() {
url.pathname = '/api/image/model'; url.pathname = '/api/image/model';
const getCurrentModelResult = await doExtrasFetch(url, { const getCurrentModelResult = await doExtrasFetch(url, {
method: 'POST', method: 'POST',
headers: postHeaders,
body: JSON.stringify({ model: extension_settings.sd.model }), body: JSON.stringify({ model: extension_settings.sd.model }),
}); });
@ -387,10 +562,19 @@ async function loadSamplers() {
$('#sd_sampler').empty(); $('#sd_sampler').empty();
let samplers = []; let samplers = [];
if (extension_settings.sd.horde) { switch (extension_settings.sd.source) {
samplers = await loadHordeSamplers(); case sources.extras:
} else { samplers = await loadExtrasSamplers();
samplers = await loadExtrasSamplers(); break;
case sources.horde:
samplers = await loadHordeSamplers();
break;
case sources.auto:
samplers = await loadAutoSamplers();
break;
case sources.novel:
samplers = await loadNovelSamplers();
break;
} }
for (const sampler of samplers) { for (const sampler of samplers) {
@ -433,14 +617,63 @@ async function loadExtrasSamplers() {
return []; return [];
} }
async function loadAutoSamplers() {
if (!extension_settings.sd.auto_url) {
return [];
}
try {
const result = await fetch('/api/sd/samplers', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify(getAutoRequestBody()),
});
if (!result.ok) {
throw new Error('SD WebUI returned an error.');
}
const data = await result.json();
return data;
} catch (error) {
return [];
}
}
async function loadNovelSamplers() {
if (!secret_state[SECRET_KEYS.NOVEL]) {
toastr.warning('NovelAI API key is not set.');
return [];
}
return [
'k_dpmpp_2m',
'k_dpmpp_sde',
'k_dpmpp_2s_ancestral',
'k_euler',
'k_euler_ancestral',
'k_dpm_fast',
'ddim',
];
}
async function loadModels() { async function loadModels() {
$('#sd_model').empty(); $('#sd_model').empty();
let models = []; let models = [];
if (extension_settings.sd.horde) { switch (extension_settings.sd.source) {
models = await loadHordeModels(); case sources.extras:
} else { models = await loadExtrasModels();
models = await loadExtrasModels(); break;
case sources.horde:
models = await loadHordeModels();
break;
case sources.auto:
models = await loadAutoModels();
break;
case sources.novel:
models = await loadNovelModels();
break;
} }
for (const model of models) { for (const model of models) {
@ -495,6 +728,71 @@ async function loadExtrasModels() {
return []; return [];
} }
async function loadAutoModels() {
if (!extension_settings.sd.auto_url) {
return [];
}
try {
const currentModel = await getAutoRemoteModel();
if (currentModel) {
extension_settings.sd.model = currentModel;
}
const result = await fetch('/api/sd/models', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify(getAutoRequestBody()),
});
if (!result.ok) {
throw new Error('SD WebUI returned an error.');
}
const upscalers = await getAutoRemoteUpscalers();
if (Array.isArray(upscalers) && upscalers.length > 0) {
$('#sd_hr_upscaler').empty();
for (const upscaler of upscalers) {
const option = document.createElement('option');
option.innerText = upscaler;
option.value = upscaler;
option.selected = upscaler === extension_settings.sd.hr_upscaler;
$('#sd_hr_upscaler').append(option);
}
}
const data = await result.json();
return data;
} catch (error) {
return [];
}
}
async function loadNovelModels() {
if (!secret_state[SECRET_KEYS.NOVEL]) {
toastr.warning('NovelAI API key is not set.');
return [];
}
return [
{
value: 'nai-diffusion',
text: 'Full',
},
{
value: 'safe-diffusion',
text: 'Safe',
},
{
value: 'nai-diffusion-furry',
text: 'Furry',
},
];
}
function getGenerationType(prompt) { function getGenerationType(prompt) {
for (const [key, values] of Object.entries(triggerWords)) { for (const [key, values] of Object.entries(triggerWords)) {
for (const value of values) { for (const value of values) {
@ -537,7 +835,6 @@ function processReply(str) {
return str; return str;
} }
function getRawLastMessage() { function getRawLastMessage() {
const getLastUsableMessage = () => { const getLastUsableMessage = () => {
for (const message of context.chat.slice().reverse()) { for (const message of context.chat.slice().reverse()) {
@ -565,7 +862,7 @@ async function generatePicture(_, trigger, message, callback) {
return; return;
} }
if (!modules.includes('sd') && !extension_settings.sd.horde) { if (!isValidState()) {
toastr.warning("Extensions API is not connected or doesn't provide SD module. Enable Stable Horde to generate images."); toastr.warning("Extensions API is not connected or doesn't provide SD module. Enable Stable Horde to generate images.");
return; return;
} }
@ -600,10 +897,10 @@ async function generatePicture(_, trigger, message, callback) {
const callbackOriginal = callback; const callbackOriginal = callback;
callback = async function (prompt, base64Image) { callback = async function (prompt, base64Image) {
const imagePath = base64Image; const imagePath = base64Image;
const imgUrl = `url('${encodeURIComponent(base64Image)}')`; const imgUrl = `url("${encodeURI(base64Image)}")`;
if ('forceSetBackground' in window) { if (typeof window['forceSetBackground'] === 'function') {
forceSetBackground(imgUrl); window['forceSetBackground'](imgUrl);
} else { } else {
toastr.info('Background image will not be preserved.', '"Chat backgrounds" extension is disabled.'); toastr.info('Background image will not be preserved.', '"Chat backgrounds" extension is disabled.');
$('#bg_custom').css('background-image', imgUrl); $('#bg_custom').css('background-image', imgUrl);
@ -669,32 +966,60 @@ async function sendGenerationRequest(generationType, prompt, characterName = nul
? combinePrefixes(extension_settings.sd.prompt_prefix, getCharacterPrefix()) ? combinePrefixes(extension_settings.sd.prompt_prefix, getCharacterPrefix())
: extension_settings.sd.prompt_prefix; : extension_settings.sd.prompt_prefix;
if (extension_settings.sd.horde) { const prefixedPrompt = combinePrefixes(prefix, prompt);
await generateHordeImage(prompt, prefix, characterName, callback);
} else { let result = { format: '', data: '' };
await generateExtrasImage(prompt, prefix, characterName, callback); const currentChatId = getCurrentChatId();
try {
switch (extension_settings.sd.source) {
case sources.extras:
result = await generateExtrasImage(prefixedPrompt);
break;
case sources.horde:
result = await generateHordeImage(prefixedPrompt);
break;
case sources.auto:
result = await generateAutoImage(prefixedPrompt);
break;
case sources.novel:
result = await generateNovelImage(prefixedPrompt);
break;
}
if (!result.data) {
throw new Error();
}
} catch (err) {
toastr.error('Image generation failed. Please try again', 'Stable Diffusion');
return;
} }
if (currentChatId !== getCurrentChatId()) {
console.warn('Chat changed, aborting SD result saving');
toastr.warning('Chat changed, generated image discarded.', 'Stable Diffusion');
return;
}
const filename = `${characterName}_${humanizedDateTime()}`;
const base64Image = await saveBase64AsFile(result.data, characterName, filename, result.format);
callback ? callback(prompt, base64Image) : sendMessage(prompt, base64Image);
} }
/** /**
* Generates an "extras" image using a provided prompt and other settings, * Generates an "extras" image using a provided prompt and other settings.
* then saves the generated image and either invokes a callback or sends a message with the image.
* *
* @param {string} prompt - The main instruction used to guide the image generation. * @param {string} prompt - The main instruction used to guide the image generation.
* @param {string} prefix - Additional context or prefix to guide the image generation. * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete.
* @param {string} characterName - The name used to determine the sub-directory for saving.
* @param {function} [callback] - Optional callback function invoked with the prompt and saved image.
* If not provided, `sendMessage` is called instead.
*
* @returns {Promise<void>} - A promise that resolves when the image generation and processing are complete.
*/ */
async function generateExtrasImage(prompt, prefix, characterName, callback) { async function generateExtrasImage(prompt) {
console.debug(extension_settings.sd);
const url = new URL(getApiUrl()); const url = new URL(getApiUrl());
url.pathname = '/api/image'; url.pathname = '/api/image';
const result = await doExtrasFetch(url, { const result = await doExtrasFetch(url, {
method: 'POST', method: 'POST',
headers: postHeaders, headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ body: JSON.stringify({
prompt: prompt, prompt: prompt,
sampler: extension_settings.sd.sampler, sampler: extension_settings.sd.sampler,
@ -702,38 +1027,32 @@ async function generateExtrasImage(prompt, prefix, characterName, callback) {
scale: extension_settings.sd.scale, scale: extension_settings.sd.scale,
width: extension_settings.sd.width, width: extension_settings.sd.width,
height: extension_settings.sd.height, height: extension_settings.sd.height,
prompt_prefix: prefix,
negative_prompt: extension_settings.sd.negative_prompt, negative_prompt: extension_settings.sd.negative_prompt,
restore_faces: !!extension_settings.sd.restore_faces, restore_faces: !!extension_settings.sd.restore_faces,
enable_hr: !!extension_settings.sd.enable_hr, enable_hr: !!extension_settings.sd.enable_hr,
karras: !!extension_settings.sd.horde_karras, karras: !!extension_settings.sd.horde_karras,
hr_upscaler: extension_settings.sd.hr_upscaler,
hr_scale: extension_settings.sd.hr_scale,
denoising_strength: extension_settings.sd.denoising_strength,
hr_second_pass_steps: extension_settings.sd.hr_second_pass_steps,
}), }),
}); });
if (result.ok) { if (result.ok) {
const data = await result.json(); const data = await result.json();
//filename should be character name + human readable timestamp + generation mode return { format: 'jpg', data: data.image };
const filename = `${characterName}_${humanizedDateTime()}`;
const base64Image = await saveBase64AsFile(data.image, characterName, filename, "jpg");
callback ? callback(prompt, base64Image) : sendMessage(prompt, base64Image);
} else { } else {
callPopup('Image generation has failed. Please try again.', 'text'); throw new Error();
} }
} }
/** /**
* Generates a "horde" image using the provided prompt and configuration settings, * Generates a "horde" image using the provided prompt and configuration settings.
* then saves the generated image and either invokes a callback or sends a message with the image.
* *
* @param {string} prompt - The main instruction used to guide the image generation. * @param {string} prompt - The main instruction used to guide the image generation.
* @param {string} prefix - Additional context or prefix to guide the image generation. * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete.
* @param {string} characterName - The name used to determine the sub-directory for saving.
* @param {function} [callback] - Optional callback function invoked with the prompt and saved image.
* If not provided, `sendMessage` is called instead.
*
* @returns {Promise<void>} - A promise that resolves when the image generation and processing are complete.
*/ */
async function generateHordeImage(prompt, prefix, characterName, callback) { async function generateHordeImage(prompt) {
const result = await fetch('/horde_generateimage', { const result = await fetch('/horde_generateimage', {
method: 'POST', method: 'POST',
headers: getRequestHeaders(), headers: getRequestHeaders(),
@ -744,7 +1063,6 @@ async function generateHordeImage(prompt, prefix, characterName, callback) {
scale: extension_settings.sd.scale, scale: extension_settings.sd.scale,
width: extension_settings.sd.width, width: extension_settings.sd.width,
height: extension_settings.sd.height, height: extension_settings.sd.height,
prompt_prefix: prefix,
negative_prompt: extension_settings.sd.negative_prompt, negative_prompt: extension_settings.sd.negative_prompt,
model: extension_settings.sd.model, model: extension_settings.sd.model,
nsfw: extension_settings.sd.horde_nsfw, nsfw: extension_settings.sd.horde_nsfw,
@ -755,11 +1073,80 @@ async function generateHordeImage(prompt, prefix, characterName, callback) {
if (result.ok) { if (result.ok) {
const data = await result.text(); const data = await result.text();
const filename = `${characterName}_${humanizedDateTime()}`; return { format: 'webp', data: data };
const base64Image = await saveBase64AsFile(data, characterName, filename, "webp");
callback ? callback(prompt, base64Image) : sendMessage(prompt, base64Image);
} else { } else {
toastr.error('Image generation has failed. Please try again.'); throw new Error();
}
}
/**
* Generates an image in SD WebUI API using the provided prompt and configuration settings.
*
* @param {string} prompt - The main instruction used to guide the image generation.
* @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete.
*/
async function generateAutoImage(prompt) {
const result = await fetch('/api/sd/generate', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({
...getAutoRequestBody(),
prompt: prompt,
negative_prompt: extension_settings.sd.negative_prompt,
sampler_name: extension_settings.sd.sampler,
steps: extension_settings.sd.steps,
cfg_scale: extension_settings.sd.scale,
width: extension_settings.sd.width,
height: extension_settings.sd.height,
restore_faces: !!extension_settings.sd.restore_faces,
enable_hr: !!extension_settings.sd.enable_hr,
hr_upscaler: extension_settings.sd.hr_upscaler,
hr_scale: extension_settings.sd.hr_scale,
denoising_strength: extension_settings.sd.denoising_strength,
hr_second_pass_steps: extension_settings.sd.hr_second_pass_steps,
// Ensure generated img is saved to disk
save_images: true,
send_images: true,
do_not_save_grid: false,
do_not_save_samples: false,
}),
});
if (result.ok) {
const data = await result.json();
return { format: 'png', data: data.images[0] };
} else {
throw new Error();
}
}
/**
* Generates an image in NovelAI API using the provided prompt and configuration settings.
*
* @param {string} prompt - The main instruction used to guide the image generation.
* @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete.
*/
async function generateNovelImage(prompt) {
const result = await fetch('/api/novelai/generate-image', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({
prompt: prompt,
model: extension_settings.sd.model,
sampler: extension_settings.sd.sampler,
steps: extension_settings.sd.steps,
scale: extension_settings.sd.scale,
width: extension_settings.sd.width,
height: extension_settings.sd.height,
negative_prompt: extension_settings.sd.negative_prompt,
}),
});
if (result.ok) {
const data = await result.text();
return { format: 'png', data: data };
} else {
throw new Error();
} }
} }
@ -842,12 +1229,21 @@ function addSDGenButtons() {
}); });
} }
function isConnectedToExtras() { function isValidState() {
return modules.includes('sd'); switch (extension_settings.sd.source) {
case sources.extras:
return modules.includes('sd');
case sources.horde:
return true;
case sources.auto:
return !!extension_settings.sd.auto_url;
case sources.novel:
return secret_state[SECRET_KEYS.NOVEL];
}
} }
async function moduleWorker() { async function moduleWorker() {
if (isConnectedToExtras() || extension_settings.sd.horde) { if (isValidState()) {
$('#sd_gen').show(); $('#sd_gen').show();
$('.sd_message_gen').show(); $('.sd_message_gen').show();
} }
@ -942,82 +1338,8 @@ $("#sd_dropdown [id]").on("click", function () {
jQuery(async () => { jQuery(async () => {
getContext().registerSlashCommand('sd', generatePicture, [], helpString, true, true); getContext().registerSlashCommand('sd', generatePicture, [], helpString, true, true);
const settingsHtml = ` $('#extensions_settings').append(renderExtensionTemplate('stable-diffusion', 'settings', defaultSettings));
<div class="sd_settings"> $('#sd_source').on('change', onSourceChange);
<div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header">
<b>Stable Diffusion</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<small><i>Use slash commands or the bottom Paintbrush button to generate images. Type <span class="monospace">/help</span> in chat for more details</i></small>
<br>
<small><i>Hint: Save an API key in Horde KoboldAI API settings to use it here.</i></small>
<label for="sd_refine_mode" class="checkbox_label" title="Allow to edit prompts manually before sending them to generation API">
<input id="sd_refine_mode" type="checkbox" />
Edit prompts before generation
</label>
<div class="flex-container flexGap5 marginTop10 margin-bot-10px">
<label class="checkbox_label">
<input id="sd_horde" type="checkbox" />
Use Stable Horde
</label>
<label style="margin-left:1em;" class="checkbox_label">
<input id="sd_horde_nsfw" type="checkbox" />
Allow NSFW images from Horde
</label>
</div>
<label for="sd_scale">CFG Scale (<span id="sd_scale_value"></span>)</label>
<input id="sd_scale" type="range" min="${defaultSettings.scale_min}" max="${defaultSettings.scale_max}" step="${defaultSettings.scale_step}" value="${defaultSettings.scale}" />
<label for="sd_steps">Sampling steps (<span id="sd_steps_value"></span>)</label>
<input id="sd_steps" type="range" min="${defaultSettings.steps_min}" max="${defaultSettings.steps_max}" step="${defaultSettings.steps_step}" value="${defaultSettings.steps}" />
<label for="sd_width">Width (<span id="sd_width_value"></span>)</label>
<input id="sd_width" type="range" max="${defaultSettings.dimension_max}" min="${defaultSettings.dimension_min}" step="${defaultSettings.dimension_step}" value="${defaultSettings.width}" />
<label for="sd_height">Height (<span id="sd_height_value"></span>)</label>
<input id="sd_height" type="range" max="${defaultSettings.dimension_max}" min="${defaultSettings.dimension_min}" step="${defaultSettings.dimension_step}" value="${defaultSettings.height}" />
<div><small>Only for Horde or remote Stable Diffusion Web UI:</small></div>
<div class="flex-container marginTop10 margin-bot-10px">
<label class="flex1 checkbox_label">
<input id="sd_restore_faces" type="checkbox" />
Restore Faces
</label>
<label class="flex1 checkbox_label">
<input id="sd_enable_hr" type="checkbox" />
Hires. Fix
</label>
</div>
<label for="sd_model">Stable Diffusion model</label>
<select id="sd_model"></select>
<label for="sd_sampler">Sampling method</label>
<select id="sd_sampler"></select>
<div class="flex-container flexGap5 margin-bot-10px">
<label class="checkbox_label">
<input id="sd_horde_karras" type="checkbox" />
Karras (only for Horde, not all samplers supported)
</label>
</div>
<label for="sd_prompt_prefix">Common prompt prefix</label>
<textarea id="sd_prompt_prefix" class="text_pole textarea_compact" rows="3"></textarea>
<div id="sd_character_prompt_block">
<label for="sd_character_prompt">Character-specific prompt prefix</label>
<small>Won't be used in groups.</small>
<textarea id="sd_character_prompt" class="text_pole textarea_compact" rows="3" placeholder="Any characteristics that describe the currently selected character. Will be added after a common prefix.&#10;Example: female, green eyes, brown hair, pink shirt"></textarea>
</div>
<label for="sd_negative_prompt">Negative prompt</label>
<textarea id="sd_negative_prompt" class="text_pole textarea_compact" rows="3"></textarea>
</div>
</div>
<div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header">
<b>SD Prompt Templates</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div id="sd_prompt_templates" class="inline-drawer-content">
</div>
</div>
</div>`;
$('#extensions_settings').append(settingsHtml);
$('#sd_scale').on('input', onScaleInput); $('#sd_scale').on('input', onScaleInput);
$('#sd_steps').on('input', onStepsInput); $('#sd_steps').on('input', onStepsInput);
$('#sd_model').on('change', onModelChange); $('#sd_model').on('change', onModelChange);
@ -1026,13 +1348,19 @@ jQuery(async () => {
$('#sd_negative_prompt').on('input', onNegativePromptInput); $('#sd_negative_prompt').on('input', onNegativePromptInput);
$('#sd_width').on('input', onWidthInput); $('#sd_width').on('input', onWidthInput);
$('#sd_height').on('input', onHeightInput); $('#sd_height').on('input', onHeightInput);
$('#sd_horde').on('input', onHordeInput);
$('#sd_horde_nsfw').on('input', onHordeNsfwInput); $('#sd_horde_nsfw').on('input', onHordeNsfwInput);
$('#sd_horde_karras').on('input', onHordeKarrasInput); $('#sd_horde_karras').on('input', onHordeKarrasInput);
$('#sd_restore_faces').on('input', onRestoreFacesInput); $('#sd_restore_faces').on('input', onRestoreFacesInput);
$('#sd_enable_hr').on('input', onHighResFixInput); $('#sd_enable_hr').on('input', onHighResFixInput);
$('#sd_refine_mode').on('input', onRefineModeInput); $('#sd_refine_mode').on('input', onRefineModeInput);
$('#sd_character_prompt').on('input', onCharacterPromptInput); $('#sd_character_prompt').on('input', onCharacterPromptInput);
$('#sd_auto_validate').on('click', validateAutoUrl);
$('#sd_auto_url').on('input', onAutoUrlInput);
$('#sd_auto_auth').on('input', onAutoAuthInput);
$('#sd_hr_upscaler').on('change', onHrUpscalerChange);
$('#sd_hr_scale').on('input', onHrScaleInput);
$('#sd_denoising_strength').on('input', onDenoisingStrengthInput);
$('#sd_hr_second_pass_steps').on('input', onHrSecondPassStepsInput);
$('#sd_character_prompt_block').hide(); $('#sd_character_prompt_block').hide();
$('.sd_settings .inline-drawer-toggle').on('click', function () { $('.sd_settings .inline-drawer-toggle').on('click', function () {

View File

@ -0,0 +1,106 @@
<div class="sd_settings">
<div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header">
<b>Stable Diffusion</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<small><i>Use slash commands or the bottom Paintbrush button to generate images. Type <span class="monospace">/help</span> in chat for more details</i></small>
<br>
<label for="sd_refine_mode" class="checkbox_label" title="Allow to edit prompts manually before sending them to generation API">
<input id="sd_refine_mode" type="checkbox" />
Edit prompts before generation
</label>
<label for="sd_source">Source</label>
<select id="sd_source">
<option value="extras">Extras API (local / remote)</option>
<option value="horde">Stable Horde</option>
<option value="auto">Stable Diffusion Web UI (AUTOMATIC1111)</option>
<option value="novel">NovelAI Diffusion</option>
</select>
<div data-sd-source="auto">
<label for="sd_auto_url">SD Web UI URL</label>
<div class="flex-container flexnowrap">
<input id="sd_auto_url" type="text" class="text_pole" placeholder="Example: {{auto_url}}" value="{{auto_url}}" />
<div id="sd_auto_validate" class="menu_button menu_button_icon">
<i class="fa-solid fa-check"></i>
<span data-i18n="Connect">
Connect
</span>
</div>
</div>
<label for="sd_auto_auth">Authentication (optional)</label>
<input id="sd_auto_auth" type="text" class="text_pole" placeholder="Example: username:password" value="" />
<i><b>Important:</b> run SD Web UI with the <tt>--api</tt> flag! The server must be accessible from the SillyTavern host machine.</i>
</div>
<div data-sd-source="horde">
<i>Hint: Save an API key in Horde KoboldAI API settings to use it here.</i>
<label for="sd_horde_nsfw" class="checkbox_label">
<input id="sd_horde_nsfw" type="checkbox" />
<span data-i18n="Allow NSFW images from Horde">
Allow NSFW images from Horde
</span>
</label>
<label for="sd_horde_karras" class="checkbox_label">
<input id="sd_horde_karras" type="checkbox" />
<span data-i18n="Karras (not all samplers supported)">
Karras (not all samplers supported)
</span>
</label>
</div>
<div data-sd-source="novel">
<i>Hint: Save an API key in the NovelAI API settings to use it here.</i>
</div>
<label for="sd_scale">CFG Scale (<span id="sd_scale_value"></span>)</label>
<input id="sd_scale" type="range" min="{{scale_min}}" max="{{scale_max}}" step="{{scale_step}}" value="{{scale}}" />
<label for="sd_steps">Sampling steps (<span id="sd_steps_value"></span>)</label>
<input id="sd_steps" type="range" min="{{steps_min}}" max="{{steps_max}}" step="{{steps_step}}" value="{{steps}}" />
<label for="sd_width">Width (<span id="sd_width_value"></span>)</label>
<input id="sd_width" type="range" max="{{dimension_max}}" min="{{dimension_min}}" step="{{dimension_step}}" value="{{width}}" />
<label for="sd_height">Height (<span id="sd_height_value"></span>)</label>
<input id="sd_height" type="range" max="{{dimension_max}}" min="{{dimension_min}}" step="{{dimension_step}}" value="{{height}}" />
<label for="sd_model">Stable Diffusion model</label>
<select id="sd_model"></select>
<label for="sd_sampler">Sampling method</label>
<select id="sd_sampler"></select>
<div class="flex-container marginTop10 margin-bot-10px">
<label class="flex1 checkbox_label">
<input id="sd_restore_faces" type="checkbox" />
Restore Faces
</label>
<label class="flex1 checkbox_label">
<input id="sd_enable_hr" type="checkbox" />
Hires. Fix
</label>
</div>
<div data-sd-source="auto">
<label for="sd_hr_upscaler">Upscaler</label>
<select id="sd_hr_upscaler"></select>
<label for="sd_hr_scale">Upscale by (<span id="sd_hr_scale_value"></span>)</label>
<input id="sd_hr_scale" type="range" min="{{hr_scale_min}}" max="{{hr_scale_max}}" step="{{hr_scale_step}}" value="{{hr_scale}}" />
<label for="sd_denoising_strength">Denoising strength (<span id="sd_denoising_strength_value"></span>)</label>
<input id="sd_denoising_strength" type="range" min="{{denoising_strength_min}}" max="{{denoising_strength_max}}" step="{{denoising_strength_step}}" value="{{denoising_strength}}" />
<label for="sd_hr_second_pass_steps">Hires steps (2nd pass) (<span id="sd_hr_second_pass_steps_value"></span>)</label>
<input id="sd_hr_second_pass_steps" type="range" min="{{hr_second_pass_steps_min}}" max="{{hr_second_pass_steps_max}}" step="{{hr_second_pass_steps_max}}" value="{{hr_second_pass_steps}}" />
</div>
<label for="sd_prompt_prefix">Common prompt prefix</label>
<textarea id="sd_prompt_prefix" class="text_pole textarea_compact" rows="3"></textarea>
<div id="sd_character_prompt_block">
<label for="sd_character_prompt">Character-specific prompt prefix</label>
<small>Won't be used in groups.</small>
<textarea id="sd_character_prompt" class="text_pole textarea_compact" rows="3" placeholder="Any characteristics that describe the currently selected character. Will be added after a common prefix.&#10;Example: female, green eyes, brown hair, pink shirt"></textarea>
</div>
<label for="sd_negative_prompt">Negative prompt</label>
<textarea id="sd_negative_prompt" class="text_pole textarea_compact" rows="3"></textarea>
</div>
</div>
<div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header">
<b>SD Prompt Templates</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div id="sd_prompt_templates" class="inline-drawer-content">
</div>
</div>
</div>

View File

@ -558,7 +558,7 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
} }
if (activatedMembers.length === 0) { if (activatedMembers.length === 0) {
toastr.warning('All group members are disabled. Enable at least one to get a reply.'); //toastr.warning('All group members are disabled. Enable at least one to get a reply.');
// Send user message as is // Send user message as is
const bias = getBiasStrings(userInput, type); const bias = getBiasStrings(userInput, type);

View File

@ -80,9 +80,9 @@ export function loadKoboldSettings(preset) {
kai_settings.streaming_kobold = preset.streaming_kobold; kai_settings.streaming_kobold = preset.streaming_kobold;
$('#streaming_kobold').prop('checked', kai_settings.streaming_kobold); $('#streaming_kobold').prop('checked', kai_settings.streaming_kobold);
} }
if (preset.hasOwnProperty('use_default_badwordids')) { if (preset.hasOwnProperty('use_default_badwordsids')) {
kai_settings.use_default_badwordids = preset.use_default_badwordids; kai_settings.use_default_badwordsids = preset.use_default_badwordsids;
$('#use_default_badwordids').prop('checked', kai_settings.use_default_badwordids); $('#use_default_badwordsids').prop('checked', kai_settings.use_default_badwordsids);
} }
} }
@ -351,9 +351,9 @@ jQuery(function () {
saveSettingsDebounced(); saveSettingsDebounced();
}); });
$('#use_default_badwordids').on("input", function () { $('#use_default_badwordsids').on("input", function () {
const value = !!$(this).prop('checked'); const value = !!$(this).prop('checked');
kai_settings.use_default_badwordids = value; kai_settings.use_default_badwordsids = value;
saveSettingsDebounced(); saveSettingsDebounced();
}); });

View File

@ -705,7 +705,7 @@ function preparePromptsForChatCompletion({Scenario, charPersonality, name2, worl
}); });
// Persona Description // Persona Description
if (power_user.persona_description) { if (power_user.persona_description && power_user.persona_description_position === persona_description_positions.IN_PROMPT) {
systemPrompts.push({ role: 'system', content: power_user.persona_description, identifier: 'personaDescription' }); systemPrompts.push({ role: 'system', content: power_user.persona_description, identifier: 'personaDescription' });
} }
@ -1417,7 +1417,7 @@ class Message {
this.role = role; this.role = role;
this.content = content; this.content = content;
if (typeof this.content === 'string') { if (typeof this.content === 'string' && this.content.length > 0) {
this.tokens = tokenHandler.count({ role: this.role, content: this.content }); this.tokens = tokenHandler.count({ role: this.role, content: this.content });
} else { } else {
this.tokens = 0; this.tokens = 0;

BIN
public/st-launcher.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

View File

@ -2066,6 +2066,10 @@ grammarly-extension {
} }
/* Override toastr default styles */ /* Override toastr default styles */
body #toast-container {
top: var(--topBarBlockSize);
}
body #toast-container>div { body #toast-container>div {
opacity: 0.95; opacity: 0.95;
} }
@ -3589,3 +3593,19 @@ a {
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
} }
/* Jank mobile support for gallery and future draggables */
@media screen and (max-width: 1000px) {
#gallery {
display: block;
width: 100vw;
height: 100vh;
z-index: 9999;
}
.draggable {
display: block;
width: 100vw;
height: 100vh;
z-index: 9999;
}
}

622
server.js
View File

@ -193,6 +193,16 @@ function getOverrideHeaders(urlHost) {
} }
} }
/**
* Encodes the Basic Auth header value for the given user and password.
* @param {string} auth username:password
* @returns {string} Basic Auth header value
*/
function getBasicAuthHeader(auth) {
const encoded = Buffer.from(`${auth}`).toString('base64');
return `Basic ${encoded}`;
}
//RossAscends: Added function to format dates used in files and chat timestamps to a humanized format. //RossAscends: Added function to format dates used in files and chat timestamps to a humanized format.
//Mostly I wanted this to be for file names, but couldn't figure out exactly where the filename save code was as everything seemed to be connected. //Mostly I wanted this to be for file names, but couldn't figure out exactly where the filename save code was as everything seemed to be connected.
//During testing, this performs the same as previous date.now() structure. //During testing, this performs the same as previous date.now() structure.
@ -299,8 +309,8 @@ function getTiktokenTokenizer(model) {
return tokenizer; return tokenizer;
} }
function humanizedISO8601DateTime() { function humanizedISO8601DateTime(date = Date.now()) {
let baseDate = new Date(Date.now()); let baseDate = new Date(date);
let humanYear = baseDate.getFullYear(); let humanYear = baseDate.getFullYear();
let humanMonth = (baseDate.getMonth() + 1); let humanMonth = (baseDate.getMonth() + 1);
let humanDate = baseDate.getDate(); let humanDate = baseDate.getDate();
@ -555,15 +565,14 @@ app.post("/generate", jsonParser, async function (request, response_generate) {
const MAX_RETRIES = 50; const MAX_RETRIES = 50;
const delayAmount = 2500; const delayAmount = 2500;
let url, response;
for (let i = 0; i < MAX_RETRIES; i++) { for (let i = 0; i < MAX_RETRIES; i++) {
try { try {
url = request.body.streaming ? `${api_server}/extra/generate/stream` : `${api_server}/v1/generate`; const url = request.body.streaming ? `${api_server}/extra/generate/stream` : `${api_server}/v1/generate`;
response = await fetch(url, { method: 'POST', timeout: 0, ...args }); const response = await fetch(url, { method: 'POST', timeout: 0, ...args });
if (request.body.streaming) { if (request.body.streaming) {
request.socket.on('close', function () { request.socket.on('close', function () {
response.body.destroy(); // Close the remote stream if (response.body instanceof Readable) response.body.destroy(); // Close the remote stream
response_generate.end(); // End the Express response response_generate.end(); // End the Express response
}); });
@ -630,7 +639,7 @@ app.post("/generate_textgenerationwebui", jsonParser, async function (request, r
if (request.header('X-Response-Streaming')) { if (request.header('X-Response-Streaming')) {
const streamingUrlHeader = request.header('X-Streaming-URL'); const streamingUrlHeader = request.header('X-Streaming-URL');
if (streamingUrlHeader === undefined) return response_generate.sendStatus(400); if (streamingUrlHeader === undefined) return response_generate.sendStatus(400);
const streamingUrl = streamingUrlHeader.replace("localhost", "127.0.0.1"); const streamingUrlString = streamingUrlHeader.replace("localhost", "127.0.0.1");
response_generate.writeHead(200, { response_generate.writeHead(200, {
'Content-Type': 'text/plain;charset=utf-8', 'Content-Type': 'text/plain;charset=utf-8',
@ -639,7 +648,6 @@ app.post("/generate_textgenerationwebui", jsonParser, async function (request, r
}); });
async function* readWebsocket() { async function* readWebsocket() {
const streamingUrlString = request.header('X-Streaming-URL').replace("localhost", "127.0.0.1");
const streamingUrl = new URL(streamingUrlString); const streamingUrl = new URL(streamingUrlString);
const websocket = new WebSocket(streamingUrl); const websocket = new WebSocket(streamingUrl);
@ -915,7 +923,7 @@ function convertToV2(char) {
}); });
result.chat = char.chat ?? humanizedISO8601DateTime(); result.chat = char.chat ?? humanizedISO8601DateTime();
result.create_date = char.create_date; result.create_date = char.create_date ?? humanizedISO8601DateTime();
return result; return result;
} }
@ -973,7 +981,7 @@ function readFromV2(char) {
}); });
char['chat'] = char['chat'] ?? humanizedISO8601DateTime(); char['chat'] = char['chat'] ?? humanizedISO8601DateTime();
char['create_date'] = char['create_date'] || humanizedISO8601DateTime(); char['create_date'] = char['create_date'] ?? humanizedISO8601DateTime();
return char; return char;
} }
@ -1309,6 +1317,9 @@ async function tryReadImage(img_url, crop) {
if (crop.want_resize) { if (crop.want_resize) {
final_width = AVATAR_WIDTH final_width = AVATAR_WIDTH
final_height = AVATAR_HEIGHT final_height = AVATAR_HEIGHT
} else {
final_width = crop.width;
final_height = crop.height;
} }
} }
@ -1372,6 +1383,7 @@ const processCharacter = async (item, i) => {
characters[i]['json_data'] = img_data; characters[i]['json_data'] = img_data;
const charStat = fs.statSync(path.join(charactersPath, item)); const charStat = fs.statSync(path.join(charactersPath, item));
characters[i]['date_added'] = charStat.birthtimeMs; characters[i]['date_added'] = charStat.birthtimeMs;
characters[i]['create_date'] = jsonObject['create_date'] || humanizedISO8601DateTime(charStat.birthtimeMs);
const char_dir = path.join(chatsPath, item.replace('.png', '')); const char_dir = path.join(chatsPath, item.replace('.png', ''));
const { chatSize, dateLastChat } = calculateChatSize(char_dir); const { chatSize, dateLastChat } = calculateChatSize(char_dir);
@ -1807,7 +1819,12 @@ app.post('/savequickreply', jsonParser, (request, response) => {
return response.sendStatus(200); return response.sendStatus(200);
}); });
/**
* @param {string} name Name of World Info file
* @param {object} entries Entries object
*/
function convertWorldInfoToCharacterBook(name, entries) { function convertWorldInfoToCharacterBook(name, entries) {
/** @type {{ entries: object[]; name: string }} */
const result = { entries: [], name }; const result = { entries: [], name };
for (const index in entries) { for (const index in entries) {
@ -1989,7 +2006,7 @@ app.post("/generate_novelai", jsonParser, async function (request, response_gene
response.body.pipe(response_generate_novel); response.body.pipe(response_generate_novel);
request.socket.on('close', function () { request.socket.on('close', function () {
response.body.destroy(); // Close the remote stream if (response.body instanceof Readable) response.body.destroy(); // Close the remote stream
response_generate_novel.end(); // End the Express response response_generate_novel.end(); // End the Express response
}); });
@ -2220,7 +2237,8 @@ app.post("/importcharacter", urlencodedParser, async function (request, response
importRisuSprites(jsonData); importRisuSprites(jsonData);
unsetFavFlag(jsonData); unsetFavFlag(jsonData);
jsonData = readFromV2(jsonData); jsonData = readFromV2(jsonData);
let char = JSON.stringify(jsonData); jsonData["create_date"] = humanizedISO8601DateTime();
const char = JSON.stringify(jsonData);
await charaWrite(uploadPath, char, png_name, response, { file_name: png_name }); await charaWrite(uploadPath, char, png_name, response, { file_name: png_name });
fs.unlinkSync(uploadPath); fs.unlinkSync(uploadPath);
} else if (jsonData.name !== undefined) { } else if (jsonData.name !== undefined) {
@ -2246,8 +2264,8 @@ app.post("/importcharacter", urlencodedParser, async function (request, response
"tags": jsonData.tags ?? '', "tags": jsonData.tags ?? '',
}; };
char = convertToV2(char); char = convertToV2(char);
char = JSON.stringify(char); const charJSON = JSON.stringify(char);
await charaWrite(uploadPath, char, png_name, response, { file_name: png_name }); await charaWrite(uploadPath, charJSON, png_name, response, { file_name: png_name });
fs.unlinkSync(uploadPath); fs.unlinkSync(uploadPath);
} else { } else {
console.log('Unknown character card format'); console.log('Unknown character card format');
@ -2276,17 +2294,31 @@ app.post("/dupecharacter", jsonParser, async function (request, response) {
} }
let suffix = 1; let suffix = 1;
let newFilename = filename; let newFilename = filename;
// If filename ends with a _number, increment the number
const nameParts = path.basename(filename, path.extname(filename)).split('_');
const lastPart = nameParts[nameParts.length - 1];
let baseName;
if (!isNaN(Number(lastPart)) && nameParts.length > 1) {
suffix = parseInt(lastPart) + 1;
baseName = nameParts.slice(0, -1).join("_"); // construct baseName without suffix
} else {
baseName = nameParts.join("_"); // original filename is completely the baseName
}
newFilename = path.join(directories.characters, `${baseName}_${suffix}${path.extname(filename)}`);
while (fs.existsSync(newFilename)) { while (fs.existsSync(newFilename)) {
let suffixStr = "_" + suffix; let suffixStr = "_" + suffix;
let ext = path.extname(filename); newFilename = path.join(directories.characters, `${baseName}${suffixStr}${path.extname(filename)}`);
newFilename = filename.slice(0, -ext.length) + suffixStr + ext;
suffix++; suffix++;
} }
fs.copyFile(filename, newFilename, (err) => {
if (err) throw err; fs.copyFileSync(filename, newFilename);
console.log(`${filename} was copied to ${newFilename}`); console.log(`${filename} was copied to ${newFilename}`);
response.sendStatus(200); response.sendStatus(200);
});
} }
catch (error) { catch (error) {
console.error(error); console.error(error);
@ -2379,6 +2411,7 @@ app.post("/exportcharacter", jsonParser, async function (request, response) {
case 'json': { case 'json': {
try { try {
let json = await charaRead(filename); let json = await charaRead(filename);
if (json === false || json === undefined) return response.sendStatus(400);
let jsonObject = getCharaCardV2(json5.parse(json)); let jsonObject = getCharaCardV2(json5.parse(json));
return response.type('json').send(jsonObject) return response.type('json').send(jsonObject)
} }
@ -2389,6 +2422,7 @@ app.post("/exportcharacter", jsonParser, async function (request, response) {
case 'webp': { case 'webp': {
try { try {
let json = await charaRead(filename); let json = await charaRead(filename);
if (json === false || json === undefined) return response.sendStatus(400);
let stringByteArray = utf8Encode.encode(json).toString(); let stringByteArray = utf8Encode.encode(json).toString();
let inputWebpPath = path.join(UPLOADS_PATH, `${Date.now()}_input.webp`); let inputWebpPath = path.join(UPLOADS_PATH, `${Date.now()}_input.webp`);
let outputWebpPath = path.join(UPLOADS_PATH, `${Date.now()}_output.webp`); let outputWebpPath = path.join(UPLOADS_PATH, `${Date.now()}_output.webp`);
@ -2426,6 +2460,11 @@ app.post("/exportcharacter", jsonParser, async function (request, response) {
app.post("/importgroupchat", urlencodedParser, function (request, response) { app.post("/importgroupchat", urlencodedParser, function (request, response) {
try { try {
const filedata = request.file; const filedata = request.file;
if (!filedata) {
return response.sendStatus(400);
}
const chatname = humanizedISO8601DateTime(); const chatname = humanizedISO8601DateTime();
const pathToUpload = path.join(UPLOADS_PATH, filedata.filename); const pathToUpload = path.join(UPLOADS_PATH, filedata.filename);
const pathToNewFile = path.join(directories.groupChats, `${chatname}.jsonl`); const pathToNewFile = path.join(directories.groupChats, `${chatname}.jsonl`);
@ -2447,128 +2486,118 @@ app.post("/importchat", urlencodedParser, function (request, response) {
let ch_name = request.body.character_name; let ch_name = request.body.character_name;
let user_name = request.body.user_name || 'You'; let user_name = request.body.user_name || 'You';
if (filedata) { if (!filedata) {
return response.sendStatus(400);
}
try {
const data = fs.readFileSync(path.join(UPLOADS_PATH, filedata.filename), 'utf8');
if (format === 'json') { if (format === 'json') {
fs.readFile(path.join(UPLOADS_PATH, filedata.filename), 'utf8', (err, data) => { const jsonData = json5.parse(data);
if (jsonData.histories !== undefined) {
if (err) { //console.log('/importchat confirms JSON histories are defined');
console.log(err); const chat = {
response.send({ error: true }); from(history) {
return [
{
user_name: user_name,
character_name: ch_name,
create_date: humanizedISO8601DateTime(),
},
...history.msgs.map(
(message) => ({
name: message.src.is_human ? user_name : ch_name,
is_user: message.src.is_human,
is_name: true,
send_date: humanizedISO8601DateTime(),
mes: message.text,
})
)];
}
} }
const jsonData = json5.parse(data); const newChats = [];
if (jsonData.histories !== undefined) { (jsonData.histories.histories ?? []).forEach((history) => {
//console.log('/importchat confirms JSON histories are defined'); newChats.push(chat.from(history));
const chat = { });
from(history) {
return [ const errors = [];
{
user_name: user_name, for (const chat of newChats) {
character_name: ch_name, const filePath = `${chatsPath + avatar_url}/${ch_name} - ${humanizedISO8601DateTime()} imported.jsonl`;
create_date: humanizedISO8601DateTime(), const fileContent = chat.map(tryParse).filter(x => x).join('\n');
},
...history.msgs.map( try {
(message) => ({ writeFileAtomicSync(filePath, fileContent, 'utf8');
name: message.src.is_human ? user_name : ch_name, } catch (err) {
is_user: message.src.is_human, errors.push(err);
is_name: true,
send_date: humanizedISO8601DateTime(),
mes: message.text,
})
)];
}
} }
const newChats = [];
(jsonData.histories.histories ?? []).forEach((history) => {
newChats.push(chat.from(history));
});
const errors = [];
for (const chat of newChats) {
const filePath = `${chatsPath + avatar_url}/${ch_name} - ${humanizedISO8601DateTime()} imported.jsonl`;
const fileContent = chat.map(tryParse).filter(x => x).join('\n');
try {
writeFileAtomicSync(filePath, fileContent, 'utf8');
} catch (err) {
errors.push(err);
}
}
if (0 < errors.length) {
response.send('Errors occurred while writing character files. Errors: ' + JSON.stringify(errors));
}
response.send({ res: true });
} else if (Array.isArray(jsonData.data_visible)) {
// oobabooga's format
const chat = [{
user_name: user_name,
character_name: ch_name,
create_date: humanizedISO8601DateTime(),
}];
for (const arr of jsonData.data_visible) {
if (arr[0]) {
const userMessage = {
name: user_name,
is_user: true,
is_name: true,
send_date: humanizedISO8601DateTime(),
mes: arr[0],
};
chat.push(userMessage);
}
if (arr[1]) {
const charMessage = {
name: ch_name,
is_user: false,
is_name: true,
send_date: humanizedISO8601DateTime(),
mes: arr[1],
};
chat.push(charMessage);
}
}
writeFileAtomicSync(`${chatsPath + avatar_url}/${ch_name} - ${humanizedISO8601DateTime()} imported.jsonl`, chat.map(JSON.stringify).join('\n'), 'utf8');
response.send({ res: true });
} else {
response.send({ error: true });
} }
});
if (0 < errors.length) {
response.send('Errors occurred while writing character files. Errors: ' + JSON.stringify(errors));
}
response.send({ res: true });
} else if (Array.isArray(jsonData.data_visible)) {
// oobabooga's format
/** @type {object[]} */
const chat = [{
user_name: user_name,
character_name: ch_name,
create_date: humanizedISO8601DateTime(),
}];
for (const arr of jsonData.data_visible) {
if (arr[0]) {
const userMessage = {
name: user_name,
is_user: true,
is_name: true,
send_date: humanizedISO8601DateTime(),
mes: arr[0],
};
chat.push(userMessage);
}
if (arr[1]) {
const charMessage = {
name: ch_name,
is_user: false,
is_name: true,
send_date: humanizedISO8601DateTime(),
mes: arr[1],
};
chat.push(charMessage);
}
}
const chatContent = chat.map(obj => JSON.stringify(obj)).join('\n');
writeFileAtomicSync(`${chatsPath + avatar_url}/${ch_name} - ${humanizedISO8601DateTime()} imported.jsonl`, chatContent, 'utf8');
response.send({ res: true });
} else {
console.log('Incorrect chat format .json');
return response.send({ error: true });
}
} }
if (format === 'jsonl') { if (format === 'jsonl') {
//console.log(humanizedISO8601DateTime()+':imported chat format is JSONL'); const line = data.split('\n')[0];
const fileStream = fs.createReadStream(path.join(UPLOADS_PATH, filedata.filename));
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity
});
rl.once('line', (line) => { let jsonData = json5.parse(line);
let jsonData = json5.parse(line);
if (jsonData.user_name !== undefined || jsonData.name !== undefined) { if (jsonData.user_name !== undefined || jsonData.name !== undefined) {
fs.copyFile(path.join(UPLOADS_PATH, filedata.filename), (`${chatsPath + avatar_url}/${ch_name} - ${humanizedISO8601DateTime()}.jsonl`), (err) => { fs.copyFileSync(path.join(UPLOADS_PATH, filedata.filename), (`${chatsPath + avatar_url}/${ch_name} - ${humanizedISO8601DateTime()}.jsonl`));
if (err) { response.send({ res: true });
response.send({ error: true }); } else {
return console.log(err); console.log('Incorrect chat format .jsonl');
} else { return response.send({ error: true });
response.send({ res: true }); }
return;
}
});
} else {
response.send({ error: true });
return;
}
rl.close();
});
} }
} catch (error) {
console.error(error);
return response.send({ error: true });
} }
}); });
@ -2837,7 +2866,7 @@ app.post('/getgroupchat', jsonParser, (request, response) => {
const lines = data.split('\n'); const lines = data.split('\n');
// Iterate through the array of strings and parse each line as JSON // Iterate through the array of strings and parse each line as JSON
const jsonData = lines.map(json5.parse); const jsonData = lines.map(line => tryParse(line)).filter(x => x);
return response.send(jsonData); return response.send(jsonData);
} else { } else {
return response.send([]); return response.send([]);
@ -2942,7 +2971,7 @@ app.get('/discover_extensions', jsonParser, function (_, response) {
}); });
app.get('/get_sprites', jsonParser, function (request, response) { app.get('/get_sprites', jsonParser, function (request, response) {
const name = request.query.name; const name = String(request.query.name);
const spritesPath = path.join(directories.characters, name); const spritesPath = path.join(directories.characters, name);
let sprites = []; let sprites = [];
@ -3092,7 +3121,7 @@ async function generateThumbnail(type, file) {
try { try {
const image = await jimp.read(pathToOriginalFile); const image = await jimp.read(pathToOriginalFile);
buffer = await image.cover(mySize[0], mySize[1]).quality(95).getBufferAsync(mime.lookup('jpg')); buffer = await image.cover(mySize[0], mySize[1]).quality(95).getBufferAsync('image/jpeg');
} }
catch (inner) { catch (inner) {
console.warn(`Thumbnailer can not process the image: ${pathToOriginalFile}. Using original size`); console.warn(`Thumbnailer can not process the image: ${pathToOriginalFile}. Using original size`);
@ -4388,7 +4417,7 @@ app.post('/horde_generateimage', jsonParser, async (request, response) => {
const ai_horde = getHordeClient(); const ai_horde = getHordeClient();
const generation = await ai_horde.postAsyncImageGenerate( const generation = await ai_horde.postAsyncImageGenerate(
{ {
prompt: `${request.body.prompt_prefix} ${request.body.prompt} ### ${request.body.negative_prompt}`, prompt: `${request.body.prompt} ### ${request.body.negative_prompt}`,
params: params:
{ {
sampler_name: request.body.sampler, sampler_name: request.body.sampler,
@ -4442,6 +4471,325 @@ app.post('/horde_generateimage', jsonParser, async (request, response) => {
} }
}); });
app.post('/api/novelai/generate-image', jsonParser, async (request, response) => {
if (!request.body) {
return response.sendStatus(400);
}
const key = readSecret(SECRET_KEYS.NOVEL);
if (!key) {
return response.sendStatus(401);
}
try {
console.log('NAI Diffusion request:', request.body);
const url = `${API_NOVELAI}/ai/generate-image`;
const result = await fetch(url, {
method: 'POST',
headers: {
'Authorization': `Bearer ${key}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
action: 'generate',
input: request.body.prompt,
model: request.body.model ?? 'nai-diffusion',
parameters: {
negative_prompt: request.body.negative_prompt ?? '',
height: request.body.height ?? 512,
width: request.body.width ?? 512,
scale: request.body.scale ?? 9,
seed: Math.floor(Math.random() * 9999999999),
sampler: request.body.sampler ?? 'k_dpmpp_2m',
steps: request.body.steps ?? 28,
n_samples: 1,
// NAI handholding for prompts
ucPreset: 0,
qualityToggle: false,
},
}),
});
if (!result.ok) {
return response.sendStatus(500);
}
const archiveBuffer = await result.arrayBuffer();
const imageBuffer = await new Promise((resolve, reject) => yauzl.fromBuffer(Buffer.from(archiveBuffer), { lazyEntries: true }, (err, zipfile) => {
if (err) {
reject(err);
}
zipfile.readEntry();
zipfile.on('entry', (entry) => {
if (entry.fileName.endsWith('.png')) {
console.log(`Extracting ${entry.fileName}`);
zipfile.openReadStream(entry, (err, readStream) => {
if (err) {
reject(err);
} else {
const chunks = [];
readStream.on('data', (chunk) => {
chunks.push(chunk);
});
readStream.on('end', () => {
const buffer = Buffer.concat(chunks);
resolve(buffer);
zipfile.readEntry(); // Continue to the next entry
});
}
});
} else {
zipfile.readEntry(); // Continue to the next entry
}
});
}));
const base64 = imageBuffer.toString('base64');
return response.send(base64);
} catch (error) {
console.log(error);
return response.sendStatus(500);
}
});
app.post('/api/sd/ping', jsonParser, async (request, response) => {
try {
const url = new URL(request.body.url);
url.pathname = '/internal/ping';
const result = await fetch(url, {
method: 'GET',
headers: {
'Authorization': getBasicAuthHeader(request.body.auth),
}
});
if (!result.ok) {
throw new Error('SD WebUI returned an error.');
}
return response.sendStatus(200);
} catch (error) {
console.log(error);
return response.sendStatus(500);
}
});
app.post('/api/sd/upscalers', jsonParser, async (request, response) => {
try {
async function getUpscalerModels() {
const url = new URL(request.body.url);
url.pathname = '/sdapi/v1/upscalers';
const result = await fetch(url, {
method: 'GET',
headers: {
'Authorization': getBasicAuthHeader(request.body.auth),
},
});
if (!result.ok) {
throw new Error('SD WebUI returned an error.');
}
const data = await result.json();
const names = data.map(x => x.name);
return names;
}
async function getLatentUpscalers() {
const url = new URL(request.body.url);
url.pathname = '/sdapi/v1/latent-upscale-modes';
const result = await fetch(url, {
method: 'GET',
headers: {
'Authorization': getBasicAuthHeader(request.body.auth),
},
});
if (!result.ok) {
throw new Error('SD WebUI returned an error.');
}
const data = await result.json();
const names = data.map(x => x.name);
return names;
}
const [upscalers, latentUpscalers] = await Promise.all([getUpscalerModels(), getLatentUpscalers()]);
// 0 = None, then Latent Upscalers, then Upscalers
upscalers.splice(1, 0, ...latentUpscalers);
return response.send(upscalers);
} catch (error) {
console.log(error);
return response.sendStatus(500);
}
});
app.post('/api/sd/samplers', jsonParser, async (request, response) => {
try {
const url = new URL(request.body.url);
url.pathname = '/sdapi/v1/samplers';
const result = await fetch(url, {
method: 'GET',
headers: {
'Authorization': getBasicAuthHeader(request.body.auth),
},
});
if (!result.ok) {
throw new Error('SD WebUI returned an error.');
}
const data = await result.json();
const names = data.map(x => x.name);
return response.send(names);
} catch (error) {
console.log(error);
return response.sendStatus(500);
}
});
app.post('/api/sd/models', jsonParser, async (request, response) => {
try {
const url = new URL(request.body.url);
url.pathname = '/sdapi/v1/sd-models';
const result = await fetch(url, {
method: 'GET',
headers: {
'Authorization': getBasicAuthHeader(request.body.auth),
},
});
if (!result.ok) {
throw new Error('SD WebUI returned an error.');
}
const data = await result.json();
const models = data.map(x => ({ value: x.title, text: x.title }));
return response.send(models);
} catch (error) {
console.log(error);
return response.sendStatus(500);
}
});
app.post('/api/sd/get-model', jsonParser, async (request, response) => {
try {
const url = new URL(request.body.url);
url.pathname = '/sdapi/v1/options';
const result = await fetch(url, {
method: 'GET',
headers: {
'Authorization': getBasicAuthHeader(request.body.auth),
},
});
const data = await result.json();
return response.send(data['sd_model_checkpoint']);
} catch (error) {
console.log(error);
return response.sendStatus(500);
}
});
app.post('/api/sd/set-model', jsonParser, async (request, response) => {
try {
async function getProgress() {
const url = new URL(request.body.url);
url.pathname = '/sdapi/v1/progress';
const result = await fetch(url, {
method: 'GET',
headers: {
'Authorization': getBasicAuthHeader(request.body.auth),
},
});
const data = await result.json();
return data;
}
const url = new URL(request.body.url);
url.pathname = '/sdapi/v1/options';
const options = {
sd_model_checkpoint: request.body.model,
};
const result = await fetch(url, {
method: 'POST',
body: JSON.stringify(options),
headers: {
'Content-Type': 'application/json',
'Authorization': getBasicAuthHeader(request.body.auth),
},
});
if (!result.ok) {
throw new Error('SD WebUI returned an error.');
}
const MAX_ATTEMPTS = 10;
const CHECK_INTERVAL = 2000;
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
const progressState = await getProgress();
const progress = progressState["progress"]
const jobCount = progressState["state"]["job_count"];
if (progress == 0.0 && jobCount === 0) {
break;
}
console.log(`Waiting for SD WebUI to finish model loading... Progress: ${progress}; Job count: ${jobCount}`);
await delay(CHECK_INTERVAL);
}
return response.sendStatus(200);
} catch (error) {
console.log(error);
return response.sendStatus(500);
}
});
app.post('/api/sd/generate', jsonParser, async (request, response) => {
try {
console.log('SD WebUI request:', request.body);
const url = new URL(request.body.url);
url.pathname = '/sdapi/v1/txt2img';
const result = await fetch(url, {
method: 'POST',
body: JSON.stringify(request.body),
headers: {
'Content-Type': 'application/json',
'Authorization': getBasicAuthHeader(request.body.auth),
},
});
if (!result.ok) {
throw new Error('SD WebUI returned an error.');
}
const data = await result.json();
return response.send(data);
} catch (error) {
console.log(error);
return response.sendStatus(500);
}
});
app.post('/libre_translate', jsonParser, async (request, response) => { app.post('/libre_translate', jsonParser, async (request, response) => {
const key = readSecret(SECRET_KEYS.LIBRE); const key = readSecret(SECRET_KEYS.LIBRE);
const url = readSecret(SECRET_KEYS.LIBRE_URL); const url = readSecret(SECRET_KEYS.LIBRE_URL);

View File

@ -49,7 +49,7 @@ const krakeBadWordsList = [
const badWordsList = [ const badWordsList = [
[23], [49209, 23], [23], [49209, 23], [23], [49209, 23], [23], [49209, 23], [23], [49209, 23], [21], [49209, 21], [23], [49209, 23], [23], [49209, 23], [23], [49209, 23], [23], [49209, 23], [23], [49209, 23], [21], [49209, 21],
[21], [49209, 21], [21], [49209, 21], [21], [49209, 21], [21], [49209, 21], [3], [49356], [1431], [31715], [34387], [21], [49209, 21], [21], [49209, 21], [21], [49209, 21], [21], [49209, 21], [3], [49356], [1431], [31715], [34387],
[20765], [30702], [10691], [49333], [1266], [26523], [41471], [2936], [85, 85], [49332], [7286], [1115] [20765], [30702], [10691], [49333], [1266], [26523], [41471], [2936], [85, 85], [49332], [7286], [1115], [19438, 2542],
] ]
const hypeBotBadWordsList = [ const hypeBotBadWordsList = [