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)
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
winget --version > nul 2>&1
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...
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"
@ -106,33 +109,37 @@ echo %blue_fg_strong%/ Home%reset%
echo -------------------------------------
echo What would you like to do?
echo 1. Start SillyTavern
echo 2. Update
echo 3. Switch to release branch
echo 4. Switch to staging branch
echo 5. Backup
echo 2. Start SillyTavern + Extras
echo 3. Update
echo 4. Backup
echo 5. Switch branch
echo 6. Toolbox
echo 7. Exit
REM Get the current Git branch
for /f %%i in ('git branch --show-current') do set current_branch=%%i
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 =================================
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
if "%choice%"=="1" (
call :start
) else if "%choice%"=="2" (
call :update
call :start_extras
) else if "%choice%"=="3" (
call :switch_release
call :update
) else if "%choice%"=="4" (
call :switch_staging
) else if "%choice%"=="5" (
call :backup_menu
) else if "%choice%"=="5" (
call :switchbrance_menu
) else if "%choice%"=="6" (
call :toolbox
) else if "%choice%"=="7" (
@ -144,6 +151,7 @@ if "%choice%"=="1" (
goto :home
)
:start
REM Check if Node.js is installed
node --version > nul 2>&1
@ -154,20 +162,26 @@ if %errorlevel% neq 0 (
pause
goto :home
)
echo Launching SillyTavern...
cls
pushd %~dp0
call npm install --no-audit
node server.js
pause
popd
echo %blue_fg_strong%[INFO]%reset% A new window has been launched.
start /wait cmd /c start.bat
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
echo Updating...
pushd %~dp0
REM Check if git is installed
git --version > nul 2>&1
if %errorlevel% neq 0 (
@ -185,39 +199,76 @@ pause
goto :home
:switch_release
REM Check if git is installed
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%
echo %blue_bg%To install Git go to Toolbox%reset%
pause
REM Switch Brance - frontend
:switchbrance_menu
cls
echo %blue_fg_strong%/ Home / Switch Branch%reset%
echo -------------------------------------
echo What would you like to do?
echo 1. Switch to Release - SillyTavern
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
) 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
pause
goto :home
goto :switchbrance_menu
:switch_staging
REM Check if git is installed
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...
:switch_staging_st
echo %blue_fg_strong%[INFO]%reset% Switching to staging branch...
git switch staging
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
REM Check if 7-Zip is installed
7z > nul 2>&1
@ -228,7 +279,6 @@ if %errorlevel% neq 0 (
pause
goto :home
)
cls
echo %blue_fg_strong%/ Home / Backup%reset%
echo -------------------------------------
@ -240,7 +290,7 @@ echo 3. Back to Home
set /p backup_choice=Choose Your Destiny:
REM backup - backend
REM Backup - Backend
if "%backup_choice%"=="1" (
call :create_backup
) else if "%backup_choice%"=="2" (
@ -254,207 +304,6 @@ if "%backup_choice%"=="1" (
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
REM Create a backup using 7zip
7z a "backups\backup_.7z" ^
@ -549,3 +398,244 @@ if "%restore_choice%" geq "1" (
)
pause
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*/
@media screen and (max-width: 1000px) and (orientation: landscape) {
body.waifuMode img.expression {
@ -407,4 +417,4 @@
#horde_model {
height: unset;
}
}
}

View File

@ -1019,8 +1019,8 @@
</label>
</div>
<div class="range-block">
<label class="checkbox_label" for="use_default_badwordids">
<input id="use_default_badwordids" type="checkbox" />
<label class="checkbox_label" for="use_default_badwordsids">
<input id="use_default_badwordsids" type="checkbox" />
<span data-i18n="Ban EOS Token">
Ban EOS Token
</span>
@ -1049,7 +1049,7 @@
</div>
<div class="range-block-range-and-counter">
<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 class="range-block-counter">
<div contenteditable="true" data-for="mirostat_tau_kobold" id="mirostat_tau_counter_kobold">
@ -1587,7 +1587,7 @@
</div>
<div class="range-block-range-and-counter">
<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 class="range-block-counter">
<div contenteditable="true" data-for="mirostat_tau_textgenerationwebui" id="mirostat_tau_counter_textgenerationwebui">
@ -2151,13 +2151,13 @@
</a>
</h3>
<div class="flex-container">
<div name="PygOverrides" class="flex1">
<div id="PygOverrides">
<div>
<h4 data-i18n="Context Template">
Context Template
</h4>
<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">
<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>
@ -2357,7 +2357,7 @@
<small data-i18n="Stop Sequence">Stop Sequence</small>
</label>
<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 class="flex1">
@ -2374,7 +2374,7 @@
</div>
</div>
<div name="ContextFormatting" class="flex1">
<div id="ContextFormatting">
<div data-newbie-hidden>
<h4><span data-i18n="Tokenizer">Tokenizer</span>
<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>
<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="UI-Mode-Block">
<h4 data-i18n="UI Mode">
@ -2837,7 +2837,7 @@
</div>
</div>
<div name="UI Customization" class="flex-container drawer33pWidth">
<div id="UI-Customization" class="flex-container wide100p">
<div class="ui-settings">
<h4><span data-i18n="UI Customization">UI Customization</span></h4>
<div data-newbie-hidden class="range-block">
@ -2966,14 +2966,13 @@
<option value="-1" data-i18n="Always disabled">Always disabled</option>
<option value="0" data-i18n="Automatic (desktop)">Automatic (desktop)</option>
<option value="1" data-i18n="Always enabled">Always enabled</option>
</select>
</select>
</div>
</div>
</div>
</div>
<div id="power-user-options-block" class="flex-container drawer33pWidth">
<div id="power-user-options-block" class="flex-container wide100p">
<div id="power-user-option-checkboxes">
<h4 data-i18n="Power User Options">Power User Options</h4>
<label data-newbie-hidden class="checkbox_label" for="swipes-checkbox">
@ -3146,12 +3145,12 @@
</div>
<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_api_key" type="text" class="flex1 heightFitContent text_pole widthNatural" maxlength="250" data-i18n="[placeholder]API key" placeholder="API key">
<div class="alignitemsflexstart extensions_url_block">
<div class="" style="text-align: center">
<input id="extensions_connect" class="menu_button" type="submit" data-i18n="Connect">Connect</input>
<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="extensions_url_block">
<div id="extensions_connect" class="menu_button" data-i18n="Connect">Connect</div>
<div id="extensions_details" class="menu_button_icon menu_button">
Manage extensions
</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>
</div>
@ -3533,8 +3532,8 @@
<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="desc" data-i18n="Z-A">Z-A</option>
<option data-field="date_added" 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="desc" data-i18n="Newest">Newest</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="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>
@ -4536,6 +4535,7 @@
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.closeButton = true; // enable a close button
toastr.options.positionClass = "toast-top-center"; // Where to position the toast container
</script>
<script>

View File

@ -557,6 +557,12 @@ export function dragElement(elmnt) {
//set a listener for mouseup to save new width/height
elmnt.off('mouseup').on('mouseup', () => {
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].height = height;
eventSource.emit('resizeUI', elmntName);

View File

@ -75,20 +75,8 @@ async function initGallery(items, url) {
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) {
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');
});
@ -98,6 +86,8 @@ async function initGallery(items, url) {
dropZone.off('dragleave');
dropZone.off('drop');
// Set dropzone height to be the same as the parent
dropZone.css('height', dropZone.parent().css('height'));
// Initialize dropzone handlers
dropZone.on('dragover', function (e) {

View File

@ -10,11 +10,13 @@ import {
appendImageToMessage,
generateQuietPrompt,
this_chid,
getCurrentChatId,
} 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 { stringFormat, initScrollHeight, resetScrollHeight, timestampToMoment, getCharaFilename, saveBase64AsFile } from "../../utils.js";
import { getMessageTimeStamp, humanizedDateTime } from "../../RossAscends-mods.js";
import { SECRET_KEYS, secret_state } from "../../secrets.js";
export { MODULE_NAME };
// Wraps a string into monospace font-face span
@ -27,10 +29,12 @@ const p = a => `<p>${a}</p>`
const MODULE_NAME = 'sd';
const UPDATE_INTERVAL = 1000;
const postHeaders = {
'Content-Type': 'application/json',
'Bypass-Tunnel-Reminder': 'bypass',
};
const sources = {
extras: 'extras',
horde: 'horde',
auto: 'auto',
novel: 'novel',
}
const generationMode = {
CHARACTER: 0,
@ -116,6 +120,8 @@ const helpString = [
].join('<br>');
const defaultSettings = {
source: sources.extras,
// CFG Scale
scale_min: 1,
scale_max: 30,
@ -153,13 +159,48 @@ const defaultSettings = {
refine_mode: false,
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() {
// Initialize settings
if (Object.keys(extension_settings.sd).length === 0) {
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) {
extension_settings.sd.prompts = promptTemplates;
}
@ -175,19 +216,26 @@ async function loadSettings() {
extension_settings.sd.character_prompts = {};
}
$('#sd_source').val(extension_settings.sd.source);
$('#sd_scale').val(extension_settings.sd.scale).trigger('input');
$('#sd_steps').val(extension_settings.sd.steps).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_width').val(extension_settings.sd.width).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_nsfw').prop('checked', extension_settings.sd.horde_nsfw);
$('#sd_horde_karras').prop('checked', extension_settings.sd.horde_karras);
$('#sd_restore_faces').prop('checked', extension_settings.sd.restore_faces);
$('#sd_enable_hr').prop('checked', extension_settings.sd.enable_hr);
$('#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();
await Promise.all([loadSamplers(), loadModels()]);
@ -332,10 +380,11 @@ function onHeightInput() {
saveSettingsDebounced();
}
async function onHordeInput() {
async function onSourceChange() {
extension_settings.sd.source = $('#sd_source').find(':selected').val();
extension_settings.sd.model = null;
extension_settings.sd.sampler = null;
extension_settings.sd.horde = !!$(this).prop('checked');
toggleSourceControls();
saveSettingsDebounced();
await Promise.all([loadModels(), loadSamplers()]);
}
@ -360,13 +409,140 @@ function onHighResFixInput() {
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() {
extension_settings.sd.model = $('#sd_model').find(':selected').val();
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();
}
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() {
@ -374,7 +550,6 @@ async function updateExtrasRemoteModel() {
url.pathname = '/api/image/model';
const getCurrentModelResult = await doExtrasFetch(url, {
method: 'POST',
headers: postHeaders,
body: JSON.stringify({ model: extension_settings.sd.model }),
});
@ -387,10 +562,19 @@ async function loadSamplers() {
$('#sd_sampler').empty();
let samplers = [];
if (extension_settings.sd.horde) {
samplers = await loadHordeSamplers();
} else {
samplers = await loadExtrasSamplers();
switch (extension_settings.sd.source) {
case sources.extras:
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) {
@ -433,14 +617,63 @@ async function loadExtrasSamplers() {
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() {
$('#sd_model').empty();
let models = [];
if (extension_settings.sd.horde) {
models = await loadHordeModels();
} else {
models = await loadExtrasModels();
switch (extension_settings.sd.source) {
case sources.extras:
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) {
@ -495,6 +728,71 @@ async function loadExtrasModels() {
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) {
for (const [key, values] of Object.entries(triggerWords)) {
for (const value of values) {
@ -537,7 +835,6 @@ function processReply(str) {
return str;
}
function getRawLastMessage() {
const getLastUsableMessage = () => {
for (const message of context.chat.slice().reverse()) {
@ -565,7 +862,7 @@ async function generatePicture(_, trigger, message, callback) {
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.");
return;
}
@ -600,10 +897,10 @@ async function generatePicture(_, trigger, message, callback) {
const callbackOriginal = callback;
callback = async function (prompt, base64Image) {
const imagePath = base64Image;
const imgUrl = `url('${encodeURIComponent(base64Image)}')`;
const imgUrl = `url("${encodeURI(base64Image)}")`;
if ('forceSetBackground' in window) {
forceSetBackground(imgUrl);
if (typeof window['forceSetBackground'] === 'function') {
window['forceSetBackground'](imgUrl);
} else {
toastr.info('Background image will not be preserved.', '"Chat backgrounds" extension is disabled.');
$('#bg_custom').css('background-image', imgUrl);
@ -669,32 +966,60 @@ async function sendGenerationRequest(generationType, prompt, characterName = nul
? combinePrefixes(extension_settings.sd.prompt_prefix, getCharacterPrefix())
: extension_settings.sd.prompt_prefix;
if (extension_settings.sd.horde) {
await generateHordeImage(prompt, prefix, characterName, callback);
} else {
await generateExtrasImage(prompt, prefix, characterName, callback);
const prefixedPrompt = combinePrefixes(prefix, prompt);
let result = { format: '', data: '' };
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,
* then saves the generated image and either invokes a callback or sends a message with the image.
* Generates an "extras" image using a provided prompt and other settings.
*
* @param {string} prompt - The main instruction used to guide the image generation.
* @param {string} prefix - Additional context or prefix to guide the image generation.
* @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.
* @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete.
*/
async function generateExtrasImage(prompt, prefix, characterName, callback) {
console.debug(extension_settings.sd);
async function generateExtrasImage(prompt) {
const url = new URL(getApiUrl());
url.pathname = '/api/image';
const result = await doExtrasFetch(url, {
method: 'POST',
headers: postHeaders,
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
prompt: prompt,
sampler: extension_settings.sd.sampler,
@ -702,38 +1027,32 @@ async function generateExtrasImage(prompt, prefix, characterName, callback) {
scale: extension_settings.sd.scale,
width: extension_settings.sd.width,
height: extension_settings.sd.height,
prompt_prefix: prefix,
negative_prompt: extension_settings.sd.negative_prompt,
restore_faces: !!extension_settings.sd.restore_faces,
enable_hr: !!extension_settings.sd.enable_hr,
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) {
const data = await result.json();
//filename should be character name + human readable timestamp + generation mode
const filename = `${characterName}_${humanizedDateTime()}`;
const base64Image = await saveBase64AsFile(data.image, characterName, filename, "jpg");
callback ? callback(prompt, base64Image) : sendMessage(prompt, base64Image);
return { format: 'jpg', data: data.image };
} else {
callPopup('Image generation has failed. Please try again.', 'text');
throw new Error();
}
}
/**
* 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.
* Generates a "horde" image using the provided prompt and configuration settings.
*
* @param {string} prompt - The main instruction used to guide the image generation.
* @param {string} prefix - Additional context or prefix to guide the image generation.
* @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.
* @returns {Promise<{format: string, data: string}>} - 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', {
method: 'POST',
headers: getRequestHeaders(),
@ -744,7 +1063,6 @@ async function generateHordeImage(prompt, prefix, characterName, callback) {
scale: extension_settings.sd.scale,
width: extension_settings.sd.width,
height: extension_settings.sd.height,
prompt_prefix: prefix,
negative_prompt: extension_settings.sd.negative_prompt,
model: extension_settings.sd.model,
nsfw: extension_settings.sd.horde_nsfw,
@ -755,11 +1073,80 @@ async function generateHordeImage(prompt, prefix, characterName, callback) {
if (result.ok) {
const data = await result.text();
const filename = `${characterName}_${humanizedDateTime()}`;
const base64Image = await saveBase64AsFile(data, characterName, filename, "webp");
callback ? callback(prompt, base64Image) : sendMessage(prompt, base64Image);
return { format: 'webp', data: data };
} 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() {
return modules.includes('sd');
function isValidState() {
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() {
if (isConnectedToExtras() || extension_settings.sd.horde) {
if (isValidState()) {
$('#sd_gen').show();
$('.sd_message_gen').show();
}
@ -942,82 +1338,8 @@ $("#sd_dropdown [id]").on("click", function () {
jQuery(async () => {
getContext().registerSlashCommand('sd', generatePicture, [], helpString, true, true);
const settingsHtml = `
<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>
<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);
$('#extensions_settings').append(renderExtensionTemplate('stable-diffusion', 'settings', defaultSettings));
$('#sd_source').on('change', onSourceChange);
$('#sd_scale').on('input', onScaleInput);
$('#sd_steps').on('input', onStepsInput);
$('#sd_model').on('change', onModelChange);
@ -1026,13 +1348,19 @@ jQuery(async () => {
$('#sd_negative_prompt').on('input', onNegativePromptInput);
$('#sd_width').on('input', onWidthInput);
$('#sd_height').on('input', onHeightInput);
$('#sd_horde').on('input', onHordeInput);
$('#sd_horde_nsfw').on('input', onHordeNsfwInput);
$('#sd_horde_karras').on('input', onHordeKarrasInput);
$('#sd_restore_faces').on('input', onRestoreFacesInput);
$('#sd_enable_hr').on('input', onHighResFixInput);
$('#sd_refine_mode').on('input', onRefineModeInput);
$('#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_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) {
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
const bias = getBiasStrings(userInput, type);

View File

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

View File

@ -705,7 +705,7 @@ function preparePromptsForChatCompletion({Scenario, charPersonality, name2, worl
});
// 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' });
}
@ -1417,7 +1417,7 @@ class Message {
this.role = role;
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 });
} else {
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 */
body #toast-container {
top: var(--topBarBlockSize);
}
body #toast-container>div {
opacity: 0.95;
}
@ -3589,3 +3593,19 @@ a {
height: 100%;
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.
//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.
@ -299,8 +309,8 @@ function getTiktokenTokenizer(model) {
return tokenizer;
}
function humanizedISO8601DateTime() {
let baseDate = new Date(Date.now());
function humanizedISO8601DateTime(date = Date.now()) {
let baseDate = new Date(date);
let humanYear = baseDate.getFullYear();
let humanMonth = (baseDate.getMonth() + 1);
let humanDate = baseDate.getDate();
@ -555,15 +565,14 @@ app.post("/generate", jsonParser, async function (request, response_generate) {
const MAX_RETRIES = 50;
const delayAmount = 2500;
let url, response;
for (let i = 0; i < MAX_RETRIES; i++) {
try {
url = request.body.streaming ? `${api_server}/extra/generate/stream` : `${api_server}/v1/generate`;
response = await fetch(url, { method: 'POST', timeout: 0, ...args });
const url = request.body.streaming ? `${api_server}/extra/generate/stream` : `${api_server}/v1/generate`;
const response = await fetch(url, { method: 'POST', timeout: 0, ...args });
if (request.body.streaming) {
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
});
@ -630,7 +639,7 @@ app.post("/generate_textgenerationwebui", jsonParser, async function (request, r
if (request.header('X-Response-Streaming')) {
const streamingUrlHeader = request.header('X-Streaming-URL');
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, {
'Content-Type': 'text/plain;charset=utf-8',
@ -639,7 +648,6 @@ app.post("/generate_textgenerationwebui", jsonParser, async function (request, r
});
async function* readWebsocket() {
const streamingUrlString = request.header('X-Streaming-URL').replace("localhost", "127.0.0.1");
const streamingUrl = new URL(streamingUrlString);
const websocket = new WebSocket(streamingUrl);
@ -915,7 +923,7 @@ function convertToV2(char) {
});
result.chat = char.chat ?? humanizedISO8601DateTime();
result.create_date = char.create_date;
result.create_date = char.create_date ?? humanizedISO8601DateTime();
return result;
}
@ -973,7 +981,7 @@ function readFromV2(char) {
});
char['chat'] = char['chat'] ?? humanizedISO8601DateTime();
char['create_date'] = char['create_date'] || humanizedISO8601DateTime();
char['create_date'] = char['create_date'] ?? humanizedISO8601DateTime();
return char;
}
@ -1309,6 +1317,9 @@ async function tryReadImage(img_url, crop) {
if (crop.want_resize) {
final_width = AVATAR_WIDTH
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;
const charStat = fs.statSync(path.join(charactersPath, item));
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 { chatSize, dateLastChat } = calculateChatSize(char_dir);
@ -1807,7 +1819,12 @@ app.post('/savequickreply', jsonParser, (request, response) => {
return response.sendStatus(200);
});
/**
* @param {string} name Name of World Info file
* @param {object} entries Entries object
*/
function convertWorldInfoToCharacterBook(name, entries) {
/** @type {{ entries: object[]; name: string }} */
const result = { entries: [], name };
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);
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
});
@ -2220,7 +2237,8 @@ app.post("/importcharacter", urlencodedParser, async function (request, response
importRisuSprites(jsonData);
unsetFavFlag(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 });
fs.unlinkSync(uploadPath);
} else if (jsonData.name !== undefined) {
@ -2246,8 +2264,8 @@ app.post("/importcharacter", urlencodedParser, async function (request, response
"tags": jsonData.tags ?? '',
};
char = convertToV2(char);
char = JSON.stringify(char);
await charaWrite(uploadPath, char, png_name, response, { file_name: png_name });
const charJSON = JSON.stringify(char);
await charaWrite(uploadPath, charJSON, png_name, response, { file_name: png_name });
fs.unlinkSync(uploadPath);
} else {
console.log('Unknown character card format');
@ -2276,17 +2294,31 @@ app.post("/dupecharacter", jsonParser, async function (request, response) {
}
let suffix = 1;
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)) {
let suffixStr = "_" + suffix;
let ext = path.extname(filename);
newFilename = filename.slice(0, -ext.length) + suffixStr + ext;
newFilename = path.join(directories.characters, `${baseName}${suffixStr}${path.extname(filename)}`);
suffix++;
}
fs.copyFile(filename, newFilename, (err) => {
if (err) throw err;
console.log(`${filename} was copied to ${newFilename}`);
response.sendStatus(200);
});
fs.copyFileSync(filename, newFilename);
console.log(`${filename} was copied to ${newFilename}`);
response.sendStatus(200);
}
catch (error) {
console.error(error);
@ -2379,6 +2411,7 @@ app.post("/exportcharacter", jsonParser, async function (request, response) {
case 'json': {
try {
let json = await charaRead(filename);
if (json === false || json === undefined) return response.sendStatus(400);
let jsonObject = getCharaCardV2(json5.parse(json));
return response.type('json').send(jsonObject)
}
@ -2389,6 +2422,7 @@ app.post("/exportcharacter", jsonParser, async function (request, response) {
case 'webp': {
try {
let json = await charaRead(filename);
if (json === false || json === undefined) return response.sendStatus(400);
let stringByteArray = utf8Encode.encode(json).toString();
let inputWebpPath = path.join(UPLOADS_PATH, `${Date.now()}_input.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) {
try {
const filedata = request.file;
if (!filedata) {
return response.sendStatus(400);
}
const chatname = humanizedISO8601DateTime();
const pathToUpload = path.join(UPLOADS_PATH, filedata.filename);
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 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') {
fs.readFile(path.join(UPLOADS_PATH, filedata.filename), 'utf8', (err, data) => {
if (err) {
console.log(err);
response.send({ error: true });
const jsonData = json5.parse(data);
if (jsonData.histories !== undefined) {
//console.log('/importchat confirms JSON histories are defined');
const chat = {
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);
if (jsonData.histories !== undefined) {
//console.log('/importchat confirms JSON histories are defined');
const chat = {
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 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);
}
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') {
//console.log(humanizedISO8601DateTime()+':imported chat format is JSONL');
const fileStream = fs.createReadStream(path.join(UPLOADS_PATH, filedata.filename));
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity
});
const line = data.split('\n')[0];
rl.once('line', (line) => {
let jsonData = json5.parse(line);
let jsonData = json5.parse(line);
if (jsonData.user_name !== undefined || jsonData.name !== undefined) {
fs.copyFile(path.join(UPLOADS_PATH, filedata.filename), (`${chatsPath + avatar_url}/${ch_name} - ${humanizedISO8601DateTime()}.jsonl`), (err) => {
if (err) {
response.send({ error: true });
return console.log(err);
} else {
response.send({ res: true });
return;
}
});
} else {
response.send({ error: true });
return;
}
rl.close();
});
if (jsonData.user_name !== undefined || jsonData.name !== undefined) {
fs.copyFileSync(path.join(UPLOADS_PATH, filedata.filename), (`${chatsPath + avatar_url}/${ch_name} - ${humanizedISO8601DateTime()}.jsonl`));
response.send({ res: true });
} else {
console.log('Incorrect chat format .jsonl');
return response.send({ error: true });
}
}
} 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');
// 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);
} else {
return response.send([]);
@ -2942,7 +2971,7 @@ app.get('/discover_extensions', jsonParser, function (_, 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);
let sprites = [];
@ -3092,7 +3121,7 @@ async function generateThumbnail(type, file) {
try {
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) {
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 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:
{
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) => {
const key = readSecret(SECRET_KEYS.LIBRE);
const url = readSecret(SECRET_KEYS.LIBRE_URL);

View File

@ -49,7 +49,7 @@ const krakeBadWordsList = [
const badWordsList = [
[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],
[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 = [