├── Activate-venv.bat ├── EasyNovelAssistant ├── setup │ ├── ActivateVirtualEnvironment.bat │ ├── Install-EasyNovelAssistant.bat │ ├── Install-EasyNovelAssistant.sh │ ├── Run-Style-Bert-VITS2.bat │ ├── SetGitPath.bat │ ├── Setup-EasyNovelAssistant.bat │ ├── Setup-EasyNovelAssistant.sh │ ├── Setup-Style-Bert-VITS2.bat │ └── res │ │ ├── Server_cpu.bat │ │ ├── config.yml │ │ ├── default_config.json │ │ ├── default_llm.json │ │ ├── default_llm_sequence.json │ │ ├── requirements.txt │ │ └── tkinter-PythonSoftwareFoundationLicense.zip └── src │ ├── const.py │ ├── context.py │ ├── easy_novel_assistant.py │ ├── form.py │ ├── gen_area.py │ ├── generator.py │ ├── input_area.py │ ├── job_queue.py │ ├── kobold_cpp.py │ ├── menu │ ├── file_menu.py │ ├── gen_menu.py │ ├── help_menu.py │ ├── model_menu.py │ ├── sample_menu.py │ ├── setting_menu.py │ ├── speech_menu.py │ └── tool_menu.py │ ├── movie_maker.py │ ├── output_area.py │ ├── path.py │ └── style_bert_vits2.py ├── KoboldCpp └── Launch-Ocuteus-v1-Q8_0-C16K-L0.bat ├── LICENSE.txt ├── README.md ├── Run-EasyNovelAssistant.bat ├── Run-EasyNovelAssistant.sh ├── Update-KoboldCpp.bat └── Update-KoboldCpp_CUDA12.bat /Activate-venv.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | chcp 65001 > NUL 3 | call %~dp0venv\Scripts\activate.bat 4 | cmd /k 5 | -------------------------------------------------------------------------------- /EasyNovelAssistant/setup/ActivateVirtualEnvironment.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | chcp 65001 > NUL 3 | set PS_CMD=PowerShell -Version 5.1 -ExecutionPolicy Bypass 4 | set CURL_CMD=C:\Windows\System32\curl.exe -k 5 | set PYTHON_CMD=python 6 | 7 | set PYTHON_DIR=%~dp0lib\python 8 | set LOCAL_PYTHON_CMD=%PYTHON_DIR%\python.exe 9 | 10 | for /f "tokens=*" %%i in ('%PYTHON_CMD% --version 2^>^&1') do set PYTHON_VERSION_VAR=%%i 11 | if not "%PYTHON_VERSION_VAR:~7,4%"=="3.10" ( 12 | set PYTHON_CMD=%LOCAL_PYTHON_CMD% 13 | if not exist %PYTHON_DIR%\ ( 14 | echo https://www.python.org/ 15 | echo https://github.com/pypa/get-pip 16 | mkdir %PYTHON_DIR% 17 | 18 | echo %CURL_CMD% -Lo %~dp0lib\python.zip https://www.python.org/ftp/python/3.10.6/python-3.10.6-embed-amd64.zip 19 | %CURL_CMD% -Lo %~dp0lib\python.zip https://www.python.org/ftp/python/3.10.6/python-3.10.6-embed-amd64.zip 20 | if %errorlevel% neq 0 ( pause & exit /b 1 ) 21 | 22 | echo %PS_CMD% Expand-Archive -Path %~dp0lib\python.zip -DestinationPath %PYTHON_DIR% 23 | %PS_CMD% Expand-Archive -Path %~dp0lib\python.zip -DestinationPath %PYTHON_DIR% 24 | if %errorlevel% neq 0 ( pause & exit /b 1 ) 25 | 26 | echo del %~dp0lib\python.zip 27 | del %~dp0lib\python.zip 28 | if %errorlevel% neq 0 ( pause & exit /b 1 ) 29 | 30 | echo %PS_CMD% "try { &{(Get-Content '%PYTHON_DIR%/python310._pth') -creplace '#import site', 'import site' | Set-Content '%PYTHON_DIR%/python310._pth' } } catch { exit 1 }" 31 | %PS_CMD% "try { &{(Get-Content '%PYTHON_DIR%/python310._pth') -creplace '#import site', 'import site' | Set-Content '%PYTHON_DIR%/python310._pth' } } catch { exit 1 }" 32 | if %errorlevel% neq 0 ( pause & exit /b 1 ) 33 | 34 | echo %CURL_CMD% -Lo %PYTHON_DIR%\get-pip.py https://bootstrap.pypa.io/get-pip.py 35 | %CURL_CMD% -Lo %PYTHON_DIR%\get-pip.py https://bootstrap.pypa.io/get-pip.py 36 | @REM プロキシ環境用コマンド。ただし動作未確認、かつ Python をインストールしたほうが楽そう。 37 | @REM %CURL_CMD% -Lo %PYTHON_DIR%\get-pip.py https://bootstrap.pypa.io/get-pip.py --proxy="PROXY_SERVER:PROXY_PORT" 38 | if %errorlevel% neq 0 ( 39 | echo "[Error] プロキシ環境によりインストールに失敗した可能性があります。Python 3.10.6 を手動インストールしてパスを通してください。" 40 | start https://www.python.org/downloads/release/python-3106/ 41 | pause & exit /b 1 42 | ) 43 | 44 | echo %LOCAL_PYTHON_CMD% %PYTHON_DIR%\get-pip.py --no-warn-script-location 45 | %LOCAL_PYTHON_CMD% %PYTHON_DIR%\get-pip.py --no-warn-script-location 46 | if %errorlevel% neq 0 ( pause & exit /b 1 ) 47 | 48 | echo %LOCAL_PYTHON_CMD% -m pip install virtualenv --no-warn-script-location 49 | %LOCAL_PYTHON_CMD% -m pip install virtualenv --no-warn-script-location 50 | if %errorlevel% neq 0 ( pause & exit /b 1 ) 51 | ) 52 | ) 53 | 54 | for /f "tokens=*" %%i in ('%PYTHON_CMD% --version 2^>^&1') do set PYTHON_VERSION_VAR=%%i 55 | if not "%PYTHON_VERSION_VAR:~7,4%"=="3.10" ( 56 | echo %PYTHON_VERSION_VAR% 57 | echo "[Error] 何らかの理由で Python をインストールできませんでした。Python 3.10.6 を手動インストールしてパスを通してください。" 58 | start https://www.python.org/downloads/release/python-3106/ 59 | pause & exit /b 1 60 | ) 61 | 62 | if not "%~1"=="" ( 63 | set VIRTUAL_ENV_DIR=%~1 64 | ) else ( 65 | set VIRTUAL_ENV_DIR=venv 66 | ) 67 | 68 | if not exist %VIRTUAL_ENV_DIR%\ ( 69 | echo %PYTHON_CMD% -m venv %VIRTUAL_ENV_DIR% 70 | %PYTHON_CMD% -m venv %VIRTUAL_ENV_DIR% 71 | 72 | if not exist %VIRTUAL_ENV_DIR%\ ( 73 | echo %PYTHON_CMD% -m pip install virtualenv --no-warn-script-location 74 | %PYTHON_CMD% -m pip install virtualenv --no-warn-script-location 75 | 76 | echo %PYTHON_CMD% -m virtualenv --copies %VIRTUAL_ENV_DIR% 77 | %PYTHON_CMD% -m virtualenv --copies %VIRTUAL_ENV_DIR% 78 | ) 79 | 80 | if not exist %VIRTUAL_ENV_DIR%\ ( 81 | echo "[ERROR] Python 仮想環境を作成できませんでした。Python 3.10.6 を手動でパスを通してインストールしてください。" 82 | pause & exit /b 1 83 | ) 84 | ) 85 | 86 | call %VIRTUAL_ENV_DIR%\Scripts\activate.bat 87 | if %errorlevel% neq 0 ( pause & exit /b 1 ) 88 | -------------------------------------------------------------------------------- /EasyNovelAssistant/setup/Install-EasyNovelAssistant.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | chcp 65001 > NUL 3 | set PS_CMD=PowerShell -Version 5.1 -ExecutionPolicy Bypass 4 | set CURL_CMD=C:\Windows\System32\curl.exe 5 | 6 | if not exist %CURL_CMD% ( 7 | echo [ERROR] %CURL_CMD% が見つかりません。 8 | pause & popd & exit /b 1 9 | ) 10 | 11 | set APP_NAME=EasyNovelAssistant 12 | set APP_NAME_TEMP=%APP_NAME%-temp 13 | set APP_VENV_DIR=venv 14 | set APP_SETUP=%APP_NAME%\setup 15 | set APP_LIB_DIR=%APP_SETUP%\lib 16 | set PORTABLE_GIT_DIR=%~dp0%APP_LIB_DIR%\PortableGit\bin 17 | set PORTABLE_GIT_VER=2.44.0 18 | set CLONE_URL=https://github.com/Zuntan03/EasyNovelAssistant 19 | 20 | pushd %~dp0 21 | setlocal enabledelayedexpansion 22 | 23 | set "CURRENT_PATH=%CD%" 24 | if "!CURRENT_PATH: =!" neq "%CURRENT_PATH%" ( 25 | echo [ERROR] 現在のフォルダパスにスペースが含まれています。"%CURRENT_PATH%" 26 | echo スペースを含まないフォルダパスに bat ファイルを移動して、再実行してください。 27 | pause & popd & exit /b 1 28 | ) 29 | 30 | if not exist %APP_VENV_DIR%\ ( 31 | echo https://www.python.org 32 | echo https://github.com/pypa/get-pip 33 | echo https://github.com/git-for-windows 34 | echo https://github.com/Zuntan03/EasyNovelAssistant 35 | echo https://github.com/LostRuins/koboldcpp 36 | echo https://github.com/litagin02/Style-Bert-VITS2 37 | echo https://github.com/BtbN/FFmpeg-Builds 38 | echo. 39 | echo https://huggingface.co/mmnga/Vecteus-v1-gguf 40 | echo https://huggingface.co/kaunista/kaunista-style-bert-vits2-models 41 | echo https://huggingface.co/RinneAi/Rinne_Style-Bert-VITS2 42 | echo. 43 | echo "未成年の方はインストール禁止です。" 44 | echo "以上の配布元から関連ファイルをダウンロードして利用します(URL を Ctrl + クリックで開けます)。" 45 | echo よろしいですか? [y/n] 46 | set /p YES_OR_NO= 47 | if /i not "!YES_OR_NO!" == "y" ( popd & exit /b 1 ) 48 | ) 49 | 50 | where /Q git 51 | if !errorlevel! neq 0 ( 52 | cd > NUL 53 | if not exist %PORTABLE_GIT_DIR% ( 54 | if not exist %APP_LIB_DIR%\ ( mkdir %APP_LIB_DIR% ) 55 | 56 | echo %CURL_CMD% -k -Lo %APP_LIB_DIR%\PortableGit.7z.exe https://github.com/git-for-windows/git/releases/download/v%PORTABLE_GIT_VER%.windows.1/PortableGit-%PORTABLE_GIT_VER%-64-bit.7z.exe 57 | %CURL_CMD% -k -Lo %APP_LIB_DIR%\PortableGit.7z.exe https://github.com/git-for-windows/git/releases/download/v%PORTABLE_GIT_VER%.windows.1/PortableGit-%PORTABLE_GIT_VER%-64-bit.7z.exe 58 | if !errorlevel! neq 0 ( pause & popd & exit /b 1 ) 59 | 60 | start "" %PS_CMD% -Command "Start-Sleep -Seconds 2; $title='Portable Git for Windows 64-bit'; $window=Get-Process | Where-Object { $_.MainWindowTitle -eq $title } | Select-Object -First 1; if ($window -ne $null) { [void][System.Reflection.Assembly]::LoadWithPartialName('Microsoft.VisualBasic'); [Microsoft.VisualBasic.Interaction]::AppActivate($window.Id); Start-Sleep -Seconds 1; Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.SendKeys]::SendWait('{ENTER}') }" 61 | 62 | echo "設定を変更せずに、そのままインストールしてください。" 63 | %APP_LIB_DIR%\PortableGit.7z.exe 64 | if !errorlevel! neq 0 ( pause & popd & exit /b 1 ) 65 | 66 | echo del %APP_LIB_DIR%\PortableGit.7z.exe 67 | del %APP_LIB_DIR%\PortableGit.7z.exe 68 | if !errorlevel! neq 0 ( pause & popd & exit /b 1 ) 69 | ) 70 | ) 71 | 72 | if exist %PORTABLE_GIT_DIR% ( 73 | echo set "PATH=%PORTABLE_GIT_DIR%;%PATH%" 74 | set "PATH=%PORTABLE_GIT_DIR%;%PATH%" 75 | 76 | where /Q git 77 | if !errorlevel! neq 0 ( 78 | echo [Error] git を自動インストールできませんでした。Git for Windows を手動でインストールしてください。 79 | start https://gitforwindows.org/ 80 | pause & popd & exit /b 1 81 | ) 82 | cd > NUL 83 | 84 | if exist .git\ ( 85 | echo git pull 86 | git pull 87 | ) else ( 88 | echo git clone %CLONE_URL% %APP_NAME_TEMP% 89 | git clone %CLONE_URL% %APP_NAME_TEMP% 90 | ) 91 | if !errorlevel! neq 0 ( pause & popd & exit /b 1 ) 92 | ) else ( 93 | if exist .git\ ( 94 | echo git pull 95 | git pull 96 | ) else ( 97 | echo git clone %CLONE_URL% %APP_NAME_TEMP% 98 | git clone %CLONE_URL% %APP_NAME_TEMP% 99 | ) 100 | if !errorlevel! neq 0 ( pause & popd & exit /b 1 ) 101 | ) 102 | 103 | if exist %APP_NAME_TEMP%\ ( 104 | echo xcopy /SQYh %APP_NAME_TEMP%\ . 105 | xcopy /SQYh %APP_NAME_TEMP%\ . 106 | if !errorlevel! neq 0 ( pause & popd & exit /b 1 ) 107 | 108 | echo rmdir /S /Q %APP_NAME_TEMP%\ 109 | rmdir /S /Q %APP_NAME_TEMP%\ 110 | if !errorlevel! neq 0 ( pause & popd & exit /b 1 ) 111 | ) 112 | endlocal 113 | 114 | call %APP_NAME%\setup\Setup-%APP_NAME%.bat 115 | if %errorlevel% neq 0 ( popd & exit /b 1 ) 116 | 117 | start "" Run-%APP_NAME%.bat 118 | 119 | popd 120 | if not exist %~dp0Install-%APP_NAME%.bat ( exit /b 0 ) 121 | del %~dp0Install-%APP_NAME%.bat -------------------------------------------------------------------------------- /EasyNovelAssistant/setup/Install-EasyNovelAssistant.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | requirements_command=("curl" "git" "tar" "python") 4 | 5 | # check requirements command exists 6 | for i in "${requirements_command[@]}" 7 | do 8 | if ! command -v "$i" &> /dev/null; then 9 | echo "[ERROR] $i が見つかりません。お使いのパッケージマネージャでインストールしてください。" 10 | flag_not_found=true 11 | fi 12 | done 13 | 14 | if [ "$flag_not_found" = true ]; then 15 | exit 1 16 | fi 17 | 18 | if ! python -c "import tkinter" &> /dev/null; then 19 | echo "[ERROR] tkintr が見つかりません。お使いのパッケージマネージャで「python3-tk」をインストールしてください。" 20 | exit 1 21 | fi 22 | 23 | GITHUB="Zuntan03" 24 | APP_NAME="EasyNovelAssistant" 25 | APP_VENV_DIR="venv" 26 | CLONE_URL="https://github.com/"$GITHUB"/EasyNovelAssistant" 27 | 28 | if [ ! -d "$APP_VENV_DIR" ]; then 29 | echo "https://github.com/"$GITHUB"/EasyNovelAssistant" 30 | echo "https://github.com/LostRuins/koboldcpp" 31 | echo "https://github.com/litagin02/Style-Bert-VITS2" 32 | echo "https://github.com/BtbN/FFmpeg-Builds" 33 | echo 34 | echo "https://huggingface.co/mmnga/Vecteus-v1-gguf" 35 | echo "https://huggingface.co/kaunista/kaunista-style-bert-vits2-models" 36 | echo "https://huggingface.co/RinneAi/Rinne_Style-Bert-VITS2" 37 | echo 38 | echo "未成年の方はインストール禁止です。" 39 | echo "以上の配布元から関連ファイルをダウンロードして利用します。" 40 | read -p "よろしいですか? [y/n] " YES_OR_NO 41 | if [ "$YES_OR_NO" != "y" ]; then 42 | exit 1 43 | fi 44 | fi 45 | 46 | git clone $CLONE_URL 47 | # check return status 48 | if [ $? -ne 0 ]; then 49 | echo "[ERROR] git clone に失敗しました。" 50 | exit 1 51 | fi 52 | 53 | cd $APP_NAME 54 | 55 | chmod +x $APP_NAME/setup/Setup-$APP_NAME.sh 56 | $APP_NAME/setup/Setup-$APP_NAME.sh 57 | 58 | chmod +x ./Run-$APP_NAME.sh 59 | ./Run-$APP_NAME.sh 60 | 61 | cd - 62 | 63 | if [ -f "$(pwd)/Install-$APP_NAME.sh" ]; then 64 | rm "$(pwd)/Install-$APP_NAME.sh" 65 | fi 66 | -------------------------------------------------------------------------------- /EasyNovelAssistant/setup/Run-Style-Bert-VITS2.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | chcp 65001 > NUL 3 | 4 | if not exist %~dp0..\..\Style-Bert-VITS2 ( 5 | echo [Error] Style-Bert-VITS2 がインストールされていません。 6 | pause & exit /b 1 7 | ) 8 | 9 | pushd %~dp0..\..\Style-Bert-VITS2 10 | 11 | call %~dp0ActivateVirtualEnvironment.bat 12 | if %errorlevel% neq 0 ( popd & exit /b 1 ) 13 | 14 | @REM --cpu 15 | echo python server_fastapi.py %* 16 | python server_fastapi.py %* 17 | if %errorlevel% neq 0 ( pause & popd & exit /b 1 ) 18 | 19 | popd 20 | -------------------------------------------------------------------------------- /EasyNovelAssistant/setup/SetGitPath.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | chcp 65001 > NUL 3 | 4 | where /Q git 5 | if %ERRORLEVEL% equ 0 ( exit /b 0 ) 6 | cd > NUL 7 | 8 | set PORTABLE_GIT_DIR=%~dp0lib\PortableGit\bin 9 | if not exist %PORTABLE_GIT_DIR% ( 10 | echo [Error] git が見つかりませんでした。Git for Windows をインストールしてください。 11 | start https://gitforwindows.org/ 12 | pause & exit /b 1 13 | ) 14 | 15 | echo set "PATH=%PORTABLE_GIT_DIR%;%PATH%" 16 | set "PATH=%PORTABLE_GIT_DIR%;%PATH%" 17 | 18 | where /Q git 19 | if %ERRORLEVEL% equ 0 ( exit /b 0 ) 20 | cd > NUL 21 | 22 | echo [Error] git が見つかりませんでした。Git for Windows をインストールしてください。 23 | start https://gitforwindows.org/ 24 | pause & exit /b 1 25 | -------------------------------------------------------------------------------- /EasyNovelAssistant/setup/Setup-EasyNovelAssistant.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | chcp 65001 > NUL 3 | pushd %~dp0..\.. 4 | set PS_CMD=PowerShell -Version 5.1 -ExecutionPolicy Bypass 5 | set CURL_CMD=C:\Windows\System32\curl.exe -k 6 | 7 | set APP_VENV_DIR=venv 8 | set KOBOLD_CPP_DIR=KoboldCpp 9 | set KOBOLD_CPP_EXE=koboldcpp.exe 10 | 11 | echo copy /Y %~dp0Install-EasyNovelAssistant.bat Update-EasyNovelAssistant.bat > NUL 12 | copy /Y %~dp0Install-EasyNovelAssistant.bat Update-EasyNovelAssistant.bat > NUL 13 | if %errorlevel% neq 0 ( pause & popd & exit /b 1 ) 14 | 15 | call %~dp0ActivateVirtualEnvironment.bat %APP_VENV_DIR% 16 | if %errorlevel% neq 0 ( popd & exit /b 1 ) 17 | 18 | echo python -m pip install -q --upgrade pip 19 | python -m pip install -q --upgrade pip 20 | 21 | echo python -c "import tkinter" > NUL 2>&1 22 | python -c "import tkinter" > NUL 2>&1 23 | if %errorlevel% neq 0 ( 24 | cd > NUL 25 | echo %PS_CMD% Expand-Archive -Path %~dp0res\tkinter-PythonSoftwareFoundationLicense.zip -DestinationPath %APP_VENV_DIR% -Force 26 | %PS_CMD% Expand-Archive -Path %~dp0res\tkinter-PythonSoftwareFoundationLicense.zip -DestinationPath %APP_VENV_DIR% -Force 27 | ) 28 | 29 | echo pip install -q -r %~dp0res\requirements.txt 30 | pip install -q -r %~dp0res\requirements.txt 31 | if %errorlevel% neq 0 ( pause & popd & exit /b 1 ) 32 | 33 | if not exist %KOBOLD_CPP_DIR%\ ( mkdir %KOBOLD_CPP_DIR% ) 34 | popd 35 | pushd %~dp0..\..\%KOBOLD_CPP_DIR% 36 | setlocal enabledelayedexpansion 37 | 38 | if not exist koboldcpp.exe ( 39 | echo %CURL_CMD% -LO https://github.com/LostRuins/koboldcpp/releases/latest/download/koboldcpp.exe 40 | %CURL_CMD% -LO https://github.com/LostRuins/koboldcpp/releases/latest/download/koboldcpp.exe 41 | if !errorlevel! neq 0 ( pause & popd & exit /b 1 ) 42 | ) 43 | 44 | if not exist Vecteus-v1-IQ4_XS.gguf ( 45 | echo %CURL_CMD% -LO https://huggingface.co/mmnga/Vecteus-v1-gguf/resolve/main/Vecteus-v1-IQ4_XS.gguf 46 | %CURL_CMD% -LO https://huggingface.co/mmnga/Vecteus-v1-gguf/resolve/main/Vecteus-v1-IQ4_XS.gguf 47 | if !errorlevel! neq 0 ( pause & popd & exit /b 1 ) 48 | ) 49 | 50 | endlocal 51 | popd 52 | -------------------------------------------------------------------------------- /EasyNovelAssistant/setup/Setup-EasyNovelAssistant.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # create and activate venv 4 | if [ ! -d "venv" ]; then 5 | python -m venv venv 6 | fi 7 | source venv/bin/activate 8 | 9 | # install pip packages 10 | pip install -r ./EasyNovelAssistant/setup/res/requirements.txt 11 | 12 | # download kobold cpp 13 | mkdir -p KoboldCpp 14 | cd KoboldCpp 15 | 16 | if [ ! -e "koboldcpp-linux-x64-cuda1150" ]; then 17 | curl -LO https://github.com/LostRuins/koboldcpp/releases/latest/download/koboldcpp-linux-x64-cuda1150 18 | chmod +x koboldcpp-linux-x64-cuda1150 19 | fi 20 | 21 | if [ ! -e "Vecteus-v1-IQ4_XS.gguf" ]; then 22 | curl -LO https://huggingface.co/mmnga/Vecteus-v1-gguf/resolve/main/Vecteus-v1-IQ4_XS.gguf 23 | fi 24 | 25 | cd - 26 | -------------------------------------------------------------------------------- /EasyNovelAssistant/setup/Setup-Style-Bert-VITS2.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | chcp 65001 > NUL 3 | set CURL_CMD=C:\Windows\System32\curl.exe -k 4 | set PS_CMD=PowerShell -Version 5.1 -ExecutionPolicy Bypass 5 | 6 | echo call %~dp0SetGitPath.bat 7 | call %~dp0SetGitPath.bat 8 | if %errorlevel% neq 0 ( pause & popd & exit /b 1 ) 9 | 10 | pushd %~dp0..\.. 11 | setlocal enabledelayedexpansion 12 | 13 | if exist Style-Bert-VITS2\ ( 14 | echo git -C Style-Bert-VITS2 pull 15 | git -C Style-Bert-VITS2 pull 16 | if !errorlevel! neq 0 ( pause & popd & exit /b 1 ) 17 | ) else ( 18 | echo git clone https://github.com/litagin02/Style-Bert-VITS2 19 | git clone https://github.com/litagin02/Style-Bert-VITS2 20 | if !errorlevel! neq 0 ( pause & popd & exit /b 1 ) 21 | ) 22 | 23 | set LIB_DIR=%~dp0lib 24 | if not exist %LIB_DIR%\ ( mkdir %LIB_DIR% ) 25 | set FFMPEG_DIR=%LIB_DIR%\ffmpeg-master-latest-win64-gpl 26 | 27 | if not exist %FFMPEG_DIR%\ ( 28 | echo %CURL_CMD% -Lo %LIB_DIR%\ffmpeg.zip https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip 29 | %CURL_CMD% -Lo %LIB_DIR%\ffmpeg.zip https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip 30 | if !errorlevel! neq 0 ( pause & popd & exit /b 1 ) 31 | 32 | echo %PS_CMD% Expand-Archive -Path %LIB_DIR%\ffmpeg.zip -DestinationPath %LIB_DIR% -Force 33 | %PS_CMD% Expand-Archive -Path %LIB_DIR%\ffmpeg.zip -DestinationPath %LIB_DIR% -Force 34 | if !errorlevel! neq 0 ( pause & popd & exit /b 1 ) 35 | 36 | echo del %LIB_DIR%\ffmpeg.zip 37 | del %LIB_DIR%\ffmpeg.zip 38 | if !errorlevel! neq 0 ( pause & popd & exit /b 1 ) 39 | ) 40 | 41 | if not exist venv\Scripts\ffplay.exe ( 42 | echo xcopy /QY %FFMPEG_DIR%\bin\*.exe venv\Scripts\ 43 | xcopy /QY %FFMPEG_DIR%\bin\*.exe venv\Scripts\ 44 | if !errorlevel! neq 0 ( pause & popd & exit /b 1 ) 45 | ) 46 | 47 | endlocal 48 | popd 49 | 50 | pushd %~dp0..\..\Style-Bert-VITS2 51 | 52 | call %~dp0ActivateVirtualEnvironment.bat 53 | if %errorlevel% neq 0 ( popd & exit /b 1 ) 54 | 55 | echo python -m pip install -q --upgrade pip 56 | python -m pip install -q --upgrade pip 57 | if %errorlevel% neq 0 ( pause & popd & exit /b 1 ) 58 | 59 | echo pip install -q torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 60 | pip install -q torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 61 | if %errorlevel% neq 0 ( pause & popd & exit /b 1 ) 62 | 63 | @REM https://fate.5ch.net/test/read.cgi/liveuranus/1711873736/545 64 | @REM Fix https://github.com/litagin02/Style-Bert-VITS2/commit/053a6bf78505e427489e341805442db20400117a 65 | @REM echo pip install -q gradio==4.23.0 66 | @REM pip install -q gradio==4.23.0 67 | @REM if %errorlevel% neq 0 ( pause & popd & exit /b 1 ) 68 | 69 | echo pip install -q -r requirements.txt 70 | pip install -q -r requirements.txt 71 | if %errorlevel% neq 0 ( pause & popd & exit /b 1 ) 72 | 73 | @REM ModuleNotFoundError: No module named 'GPUtil' 74 | echo pip install -q GPUtil 75 | pip install -q GPUtil 76 | if %errorlevel% neq 0 ( pause & popd & exit /b 1 ) 77 | 78 | echo python initialize.py 79 | python initialize.py 80 | if %errorlevel% neq 0 ( pause & popd & exit /b 1 ) 81 | 82 | if not exist Server_cpu.bat ( 83 | echo copy %~dp0res\Server_cpu.bat . 84 | copy %~dp0res\Server_cpu.bat . 85 | ) 86 | 87 | call :DL_HF_MODEL RinneAi/Rinne_Style-Bert-VITS2 model_assets/Rinne Rinne Rinne 88 | if %errorlevel% neq 0 ( popd & exit /b 1 ) 89 | 90 | call :DL_HF_MODEL kaunista/kaunista-style-bert-vits2-models Anneli Anneli Anneli_e116_s32000 91 | if %errorlevel% neq 0 ( popd & exit /b 1 ) 92 | 93 | call :DL_HF_MODEL kaunista/kaunista-style-bert-vits2-models Anneli-nsfw Anneli-nsfw Anneli-nsfw_e300_s5100 94 | if %errorlevel% neq 0 ( popd & exit /b 1 ) 95 | 96 | if not exist config.yml ( 97 | echo copy %~dp0res\config.yml . 98 | copy %~dp0res\config.yml . 99 | ) 100 | 101 | popd 102 | exit /b 0 103 | 104 | :DL_HF_MODEL 105 | set HF_REP=%1 106 | set MODEL_DIR=%2 107 | set MODEL_NAME=%3 108 | set MODEL_SAFETENSORS=%4 109 | 110 | if not exist model_assets\%MODEL_NAME% ( mkdir model_assets\%MODEL_NAME% ) 111 | 112 | setlocal enabledelayedexpansion 113 | if not exist model_assets\%MODEL_NAME%\%MODEL_NAME%.safetensors ( 114 | echo %CURL_CMD% -Lo model_assets\%MODEL_NAME%\%MODEL_NAME%.safetensors https://huggingface.co/%HF_REP%/resolve/main/%MODEL_DIR%/%MODEL_SAFETENSORS%.safetensors 115 | %CURL_CMD% -Lo model_assets\%MODEL_NAME%\%MODEL_NAME%.safetensors https://huggingface.co/%HF_REP%/resolve/main/%MODEL_DIR%/%MODEL_SAFETENSORS%.safetensors 116 | if !errorlevel! neq 0 ( pause & popd & exit /b 1 ) 117 | ) 118 | 119 | if not exist model_assets\%MODEL_NAME%\config.json ( 120 | echo %CURL_CMD% -Lo model_assets\%MODEL_NAME%\config.json https://huggingface.co/%HF_REP%/resolve/main/%MODEL_DIR%/config.json 121 | %CURL_CMD% -Lo model_assets\%MODEL_NAME%\config.json https://huggingface.co/%HF_REP%/resolve/main/%MODEL_DIR%/config.json 122 | if !errorlevel! neq 0 ( pause & popd & exit /b 1 ) 123 | ) 124 | 125 | if not exist model_assets\%MODEL_NAME%\style_vectors.npy ( 126 | echo %CURL_CMD% -Lo model_assets\%MODEL_NAME%\style_vectors.npy https://huggingface.co/%HF_REP%/resolve/main/%MODEL_DIR%/style_vectors.npy 127 | %CURL_CMD% -Lo model_assets\%MODEL_NAME%\style_vectors.npy https://huggingface.co/%HF_REP%/resolve/main/%MODEL_DIR%/style_vectors.npy 128 | if !errorlevel! neq 0 ( pause & popd & exit /b 1 ) 129 | ) 130 | endlocal 131 | 132 | exit /b 0 133 | -------------------------------------------------------------------------------- /EasyNovelAssistant/setup/res/Server_cpu.bat: -------------------------------------------------------------------------------- 1 | chcp 65001 > NUL 2 | @echo off 3 | 4 | pushd %~dp0 5 | echo Running server_fastapi.py --cpu 6 | venv\Scripts\python server_fastapi.py --cpu 7 | 8 | if %errorlevel% neq 0 ( pause & popd & exit /b %errorlevel% ) 9 | 10 | popd 11 | pause -------------------------------------------------------------------------------- /EasyNovelAssistant/setup/res/config.yml: -------------------------------------------------------------------------------- 1 | model_name: "model_name" 2 | 3 | # If you want to use a specific dataset path, uncomment the following line. 4 | # Otherwise, the dataset path is `{dataset_root}/{model_name}`. 5 | 6 | # dataset_path: "your/dataset/path" 7 | 8 | resample: 9 | sampling_rate: 44100 10 | in_dir: "raw" 11 | out_dir: "wavs" 12 | 13 | preprocess_text: 14 | transcription_path: "esd.list" 15 | cleaned_path: "" 16 | train_path: "train.list" 17 | val_path: "val.list" 18 | config_path: "config.json" 19 | val_per_lang: 0 20 | max_val_total: 12 21 | clean: true 22 | 23 | bert_gen: 24 | config_path: "config.json" 25 | num_processes: 1 26 | device: "cuda" 27 | use_multi_device: false 28 | 29 | style_gen: 30 | config_path: "config.json" 31 | num_processes: 4 32 | device: "cuda" 33 | 34 | train_ms: 35 | env: 36 | MASTER_ADDR: "localhost" 37 | MASTER_PORT: 10086 38 | WORLD_SIZE: 1 39 | LOCAL_RANK: 0 40 | RANK: 0 41 | model_dir: "models" # The directory to save the model (for training), relative to `{dataset_root}/{model_name}`. 42 | config_path: "config.json" 43 | num_workers: 16 44 | spec_cache: True 45 | keep_ckpts: 1 # Set this to 0 to keep all checkpoints 46 | 47 | webui: # For `webui.py`, which is not supported yet in Style-Bert-VITS2. 48 | # 推理设备 49 | device: "cuda" 50 | # 模型路径 51 | model: "models/G_8000.pth" 52 | # 配置文件路径 53 | config_path: "config.json" 54 | # 端口号 55 | port: 7860 56 | # 是否公开部署,对外网开放 57 | share: false 58 | # 是否开启debug模式 59 | debug: false 60 | # 语种识别库,可选langid, fastlid 61 | language_identification_library: "langid" 62 | 63 | # server_fastapi's config 64 | server: 65 | port: 5000 66 | device: "cuda" 67 | language: "JP" 68 | limit: 4096 69 | origins: 70 | - "*" 71 | -------------------------------------------------------------------------------- /EasyNovelAssistant/setup/res/default_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "char_name": "さくら", 3 | "user_name": "俺くん", 4 | "input_text": "吾輩は猫である。名前はまだない。", 5 | "output_format": "\n### {0}\n{1}\n", 6 | "watch_file": false, 7 | "style_bert_vits2_host": "localhost", 8 | "style_bert_vits2_port": 5000, 9 | "style_bert_vits2_gpu": true, 10 | "style_bert_vits2_command_timeout": 0.05, 11 | "middle_click_speech": true, 12 | "auto_speech_char": true, 13 | "auto_speech_user": true, 14 | "auto_speech_other": true, 15 | "max_speech_queue": 3, 16 | "speech_volume": 50, 17 | "speech_speed": 1.0, 18 | "speech_interval": 0.3, 19 | "char_voice": "Rinne", 20 | "user_voice": "jvnv-F1-jp", 21 | "other_voice": "jvnv-M1-jp", 22 | "llm_name": "Vecteus-v1-IQ4_XS", 23 | "llm_context_size": 8192, 24 | "llm_gpu_layer": 16, 25 | "check_interval": 0.2, 26 | "auto_scroll": true, 27 | "max_length": 512, 28 | "rep_pen": 1.1, 29 | "rep_pen_range": 320, 30 | "rep_pen_slope": 0.7, 31 | "temperature": 0.7, 32 | "tfs": 1, 33 | "top_a": 0, 34 | "top_k": 100, 35 | "top_p": 0.92, 36 | "typical": 1, 37 | "min_p": 0, 38 | "sampler_order": [ 39 | 6, 40 | 0, 41 | 1, 42 | 3, 43 | 4, 44 | 2, 45 | 5 46 | ], 47 | "koboldcpp_host": "localhost", 48 | "koboldcpp_port": 5001, 49 | "koboldcpp_arg": "--usecublas", 50 | "koboldcpp_command_timeout": 0.05, 51 | "llm_gpu_layers": [ 52 | 0, 53 | 1, 54 | 2, 55 | 3, 56 | 4, 57 | 5, 58 | 6, 59 | 8, 60 | 10, 61 | 12, 62 | 14, 63 | 16, 64 | 20, 65 | 25, 66 | 30, 67 | 35, 68 | 40, 69 | 45, 70 | 50, 71 | 55, 72 | 60, 73 | 65, 74 | 70 75 | ], 76 | "max_lengths": [ 77 | 64, 78 | 96, 79 | 128, 80 | 192, 81 | 256, 82 | 384, 83 | 512, 84 | 768, 85 | 1024, 86 | 1536, 87 | 2048, 88 | 3072, 89 | 4096, 90 | 6144, 91 | 8192, 92 | 12288, 93 | 16384, 94 | 24576, 95 | 32768, 96 | 49152, 97 | 65536, 98 | 98304 99 | ], 100 | "mov_image_dir": "", 101 | "mov_movie_dir": "", 102 | "mov_subtitles": true, 103 | "mov_resize": 1200, 104 | "mov_crf": 26, 105 | "mov_volume_adjust": false, 106 | "mov_tempo_adjust": true, 107 | "text_area_font": "TkDefaultFont", 108 | "text_area_font_size": 11, 109 | "foreground_color": "#CCCCCC", 110 | "select_foreground_color": "#FFFFFF", 111 | "background_color": "#222222", 112 | "select_background_color": "#555555", 113 | "recent_files": [], 114 | "recent_dirs": [], 115 | "recents": 8, 116 | "win_width": 1280, 117 | "win_height": 768, 118 | "win_x": 0, 119 | "win_y": 0, 120 | "input_area_width": 640, 121 | "pane_v_width": 640, 122 | "gen_area_height": 128 123 | } -------------------------------------------------------------------------------- /EasyNovelAssistant/setup/res/default_llm.json: -------------------------------------------------------------------------------- 1 | { 2 | "(New!) Kagemusya-7B-v1-Q8_0": { 3 | "max_gpu_layer": 33, 4 | "context_size": 4096, 5 | "urls": [ 6 | "https://huggingface.co/Local-Novel-LLM-project/Kagemusya-7B-v1-GGUF/resolve/main/kagemusya-7b-v1Q8_0.gguf" 7 | ] 8 | }, 9 | "(New!) Shadows-MoE-Q6": { 10 | "max_gpu_layer": 33, 11 | "context_size": 32768, 12 | "urls": [ 13 | "https://huggingface.co/Local-Novel-LLM-project/Shadows-MoE-GGUF/resolve/main/Shadows-MoE-Q6.gguf" 14 | ] 15 | }, 16 | "(New!) Ninja-V3-Q4_K_M": { 17 | "max_gpu_layer": 33, 18 | "context_size": 32768, 19 | "urls": [ 20 | "https://huggingface.co/Local-Novel-LLM-project/Ninja-V3-GGUF/resolve/main/Ninja-V3-Q4_K_M.gguf" 21 | ] 22 | }, 23 | "Ninja-V2-7B-Q8_0": { 24 | "max_gpu_layer": 33, 25 | "context_size": 32768, 26 | "urls": [ 27 | "https://huggingface.co/Local-Novel-LLM-project/Ninja-V2-7B/resolve/main/Ninja-V2-7B-Q8_0.gguf" 28 | ] 29 | }, 30 | "Ninja-v1-RP-expressive-v2-IQ4_XS": { 31 | "max_gpu_layer": 33, 32 | "context_size": 4096, 33 | "urls": [ 34 | "https://huggingface.co/Aratako/Ninja-v1-RP-expressive-v2-GGUF/resolve/main/Ninja-v1-RP-expressive-v2_IQ4_XS.gguf" 35 | ] 36 | }, 37 | "Ninja-v1-RP-expressive-IQ4_XS": { 38 | "max_gpu_layer": 33, 39 | "context_size": 4096, 40 | "urls": [ 41 | "https://huggingface.co/Aratako/Ninja-v1-RP-expressive-GGUF/resolve/main/Ninja-v1-RP-expressive_IQ4_XS.gguf" 42 | ] 43 | }, 44 | "Japanese-Chat-Evolve-TEST-NSFW-IQ4_XS": { 45 | "max_gpu_layer": 33, 46 | "context_size": 4096, 47 | "urls": [ 48 | "https://huggingface.co/dddump/Japanese-Chat-Evolve-TEST-7B-NSFW-gguf/resolve/main/Japanese-Chat-Evolve-TEST-7B-NSFW_iMat_Ch200_IQ4_XS.gguf" 49 | ] 50 | }, 51 | "Japanese-TextGen-Kage-IQ4_XS": { 52 | "max_gpu_layer": 33, 53 | "context_size": 32768, 54 | "urls": [ 55 | "https://huggingface.co/dddump/Japanese-TextGen-Kage-v0.1-2x7B-gguf/resolve/main/Japanese-TextGen-Kage-v0.1-2x7B_iMat_Ch200_IQ4_XS.gguf" 56 | ] 57 | }, 58 | "Japanese-TextGen-MoE-TEST-2x7B-NSFW-IQ4_XS": { 59 | "max_gpu_layer": 33, 60 | "context_size": 4096, 61 | "urls": [ 62 | "https://huggingface.co/dddump/Japanese-TextGen-MoE-TEST-2x7B-NSFW-gguf/resolve/main/Japanese-TextGen-MoE-TEST-2x7B-NSFW_iMat_Ch200_IQ4_XS.gguf" 63 | ] 64 | }, 65 | "ArrowPro-7B-RobinHood-IQ4_XS": { 66 | "max_gpu_layer": 33, 67 | "context_size": 4096, 68 | "urls": [ 69 | "https://huggingface.co/mmnga/DataPilot-ArrowPro-7B-RobinHood-gguf/resolve/main/DataPilot-ArrowPro-7B-RobinHood-IQ4_XS.gguf" 70 | ] 71 | }, 72 | "ArrowPro-7B-RobinHood-toxic-IQ4_XS": { 73 | "max_gpu_layer": 33, 74 | "context_size": 4096, 75 | "urls": [ 76 | "https://huggingface.co/Aratako/ArrowPro-7B-RobinHood-toxic-GGUF/resolve/main/ArrowPro-7B-RobinHood-toxic_IQ4_XS.gguf" 77 | ] 78 | }, 79 | "ArrowPro-7B-KUJIRA-IQ4_XS": { 80 | "max_gpu_layer": 33, 81 | "context_size": 4096, 82 | "urls": [ 83 | "https://huggingface.co/mmnga/DataPilot-ArrowPro-7B-KUJIRA-gguf/resolve/main/DataPilot-ArrowPro-7B-KUJIRA-IQ4_XS.gguf" 84 | ] 85 | }, 86 | "Fugaku-LLM-13B-instruct-IQ4_XS": { 87 | "max_gpu_layer": 41, 88 | "context_size": 4096, 89 | "urls": [ 90 | "https://huggingface.co/mmnga/Fugaku-LLM-13B-instruct-gguf/resolve/main/Fugaku-LLM-13B-instruct-IQ4_XS.gguf" 91 | ] 92 | }, 93 | "Vecteus-v1-IQ4_XS": { 94 | "max_gpu_layer": 33, 95 | "context_size": 4096, 96 | "urls": [ 97 | "https://huggingface.co/mmnga/Vecteus-v1-gguf/resolve/main/Vecteus-v1-IQ4_XS.gguf" 98 | ] 99 | }, 100 | "[元祖] LightChatAssistant-TypeB-2x7B-IQ4_XS": { 101 | "max_gpu_layer": 33, 102 | "context_size": 32768, 103 | "urls": [ 104 | "https://huggingface.co/Sdff-Ltba/LightChatAssistant-TypeB-2x7B-GGUF/resolve/main/LightChatAssistant-TypeB-2x7B_iq4xs_imatrix.gguf" 105 | ] 106 | }, 107 | "[軽量] Ninja-v1-NSFW-128k-IQ4_XS": { 108 | "max_gpu_layer": 33, 109 | "context_size": 131072, 110 | "urls": [ 111 | "https://huggingface.co/mmnga/Ninja-v1-NSFW-128k-gguf/resolve/main/Ninja-v1-NSFW-128k-IQ4_XS.gguf" 112 | ] 113 | }, 114 | "[軽量] Ninja-v1-128k-IQ4_XS": { 115 | "max_gpu_layer": 33, 116 | "context_size": 131072, 117 | "urls": [ 118 | "https://huggingface.co/mmnga/Ninja-v1-128k-gguf/resolve/main/Ninja-v1-128k-IQ4_XS.gguf" 119 | ] 120 | }, 121 | "[VRAM 強者用] LightChatAssistant-4x7B-IQ4_XS": { 122 | "max_gpu_layer": 33, 123 | "context_size": 32768, 124 | "urls": [ 125 | "https://huggingface.co/Aratako/LightChatAssistant-4x7B-GGUF/resolve/main/LightChatAssistant-4x7B_IQ4_XS.gguf" 126 | ] 127 | }, 128 | "Novel-Writing シリーズ/SniffyOtter-7B-Novel-Writing-NSFW-IQ4_XS": { 129 | "max_gpu_layer": 33, 130 | "context_size": 8192, 131 | "urls": [ 132 | "https://huggingface.co/Aratako/SniffyOtter-7B-Novel-Writing-NSFW-GGUF/resolve/main/SniffyOtter-7B-Novel-Writing-NSFW_IQ4_XS.gguf" 133 | ] 134 | }, 135 | "Novel-Writing シリーズ/Antler-7B-Novel-Writing-IQ4_XS": { 136 | "max_gpu_layer": 33, 137 | "context_size": 4096, 138 | "urls": [ 139 | "https://huggingface.co/Aratako/Antler-7B-Novel-Writing-GGUF/resolve/main/Antler-7B-Novel-Writing_IQ4_XS.gguf" 140 | ] 141 | }, 142 | "LightChatAssistant バリエーション/LightChatAssistant-TypeB-2x7B-IQ3_XXS": { 143 | "max_gpu_layer": 33, 144 | "context_size": 32768, 145 | "urls": [ 146 | "https://huggingface.co/Sdff-Ltba/LightChatAssistant-TypeB-2x7B-GGUF/resolve/main/LightChatAssistant-TypeB-2x7B_iq3xxs_imatrix.gguf" 147 | ] 148 | }, 149 | "LightChatAssistant バリエーション/LightChatAssistant-2x7B-IQ4_XS": { 150 | "max_gpu_layer": 33, 151 | "context_size": 32768, 152 | "urls": [ 153 | "https://huggingface.co/Sdff-Ltba/LightChatAssistant-2x7B-GGUF/resolve/main/LightChatAssistant-2x7B_iq4xs_imatrix.gguf" 154 | ] 155 | }, 156 | "軽量級/JapaneseStarlingChatV-7B-Q4_K_M": { 157 | "max_gpu_layer": 33, 158 | "context_size": 32768, 159 | "urls": [ 160 | "https://huggingface.co/TFMC/Japanese-Starling-ChatV-7B-GGUF/resolve/main/japanese-starling-chatv-7b.Q4_K_M.gguf" 161 | ] 162 | }, 163 | "軽量級/umiyuki-Japanese-Chat-Umievo-itr001-7b-Q4_K_M": { 164 | "max_gpu_layer": 33, 165 | "context_size": 32768, 166 | "urls": [ 167 | "https://huggingface.co/mmnga/umiyuki-Japanese-Chat-Umievo-itr001-7b-gguf/resolve/main/umiyuki-Japanese-Chat-Umievo-itr001-7b-Q4_K_M.gguf" 168 | ] 169 | }, 170 | "軽量級/SniffyOtter-7B-Q4_0": { 171 | "max_gpu_layer": 33, 172 | "context_size": 8192, 173 | "urls": [ 174 | "https://huggingface.co/Elizezen/SniffyOtter-7B-GGUF/resolve/main/SniffyOtter-7B-q4_0.gguf" 175 | ] 176 | }, 177 | "重量級、放置生成用、要 64GB RAM/CommandRv1-Q4_K_M": { 178 | "max_gpu_layer": 41, 179 | "context_size": 131072, 180 | "urls": [ 181 | "https://huggingface.co/andrewcanis/c4ai-command-r-v01-GGUF/resolve/main/c4ai-command-r-v01-Q4_K_M.gguf" 182 | ] 183 | }, 184 | "重量級、放置生成用、要 64GB RAM/CommandRPlus-IQ3_S": { 185 | "max_gpu_layer": 65, 186 | "context_size": 131072, 187 | "urls": [ 188 | "https://huggingface.co/dranger003/c4ai-command-r-plus-iMat.GGUF/resolve/main/ggml-c4ai-command-r-plus-104b-iq3_s.gguf" 189 | ] 190 | }, 191 | "重量級、放置生成用、要 64GB RAM/CommandRPlus-IQ4_XS": { 192 | "max_gpu_layer": 65, 193 | "context_size": 131072, 194 | "urls": [ 195 | "https://huggingface.co/dranger003/c4ai-command-r-plus-iMat.GGUF/resolve/main/ggml-c4ai-command-r-plus-104b-iq4_xs-00001-of-00002.gguf", 196 | "https://huggingface.co/dranger003/c4ai-command-r-plus-iMat.GGUF/resolve/main/ggml-c4ai-command-r-plus-104b-iq4_xs-00002-of-00002.gguf" 197 | ] 198 | }, 199 | "重量級、放置生成用、要 64GB RAM/CommandRPlus-Q3_K_M": { 200 | "max_gpu_layer": 65, 201 | "context_size": 131072, 202 | "urls": [ 203 | "https://huggingface.co/pmysl/c4ai-command-r-plus-GGUF/resolve/main/command-r-plus-Q3_K_M-00001-of-00002.gguf", 204 | "https://huggingface.co/pmysl/c4ai-command-r-plus-GGUF/resolve/main/command-r-plus-Q3_K_M-00002-of-00002.gguf" 205 | ] 206 | } 207 | } -------------------------------------------------------------------------------- /EasyNovelAssistant/setup/res/default_llm_sequence.json: -------------------------------------------------------------------------------- 1 | { 2 | "None": { 3 | "model_names": [ 4 | "Vecteus", 5 | "Kagemusya" 6 | ], 7 | "instruct": "{0}", 8 | "stop": [] 9 | }, 10 | "Llama2Chat": { 11 | "model_names": [ 12 | "LightChatAssistant", 13 | "ArrowPro", 14 | "japanese-starling" 15 | ], 16 | "instruct": "[INST] <>\n<>\n\n{0} [/INST]", 17 | "stop": [ 18 | "[INST]", 19 | "[/INST]", 20 | "" 21 | ] 22 | }, 23 | "Llama2ChatSimple": { 24 | "model_names": [ 25 | "SniffyOtter", 26 | "Antler", 27 | "Japanese-Chat-Umievo" 28 | ], 29 | "instruct": "[INST] {0} [/INST]", 30 | "stop": [ 31 | "[INST]", 32 | "[/INST]" 33 | ] 34 | }, 35 | "VicunaSystem": { 36 | "model_names": [ 37 | "Japanese-TextGen-MoE-TEST", 38 | "Japanese-Chat-Evolve-TEST", 39 | "Japanese-TextGen-Kage" 40 | ], 41 | "instruct": "SYSTEM: \nUSER: {0}\nASSISTANT: ", 42 | "stop": [ 43 | "USER:", 44 | "ASSISTANT:", 45 | "" 46 | ] 47 | }, 48 | "VicunaUserS": { 49 | "model_names": [ 50 | "Ninja", 51 | "Shadows" 52 | ], 53 | "instruct": "\nUSER: {0}\nASSISTANT: ", 54 | "stop": [ 55 | "USER:", 56 | "ASSISTANT:", 57 | "" 58 | ] 59 | }, 60 | "Fugaku": { 61 | "model_names": [ 62 | "Fugaku" 63 | ], 64 | "instruct": "### 指示:\n{0}\n\n### 応答:\n", 65 | "stop": [ 66 | "### 指示:", 67 | "### 応答:" 68 | ] 69 | }, 70 | "CommandR": { 71 | "model_names": [ 72 | "command-r" 73 | ], 74 | "instruct": "<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|><|END_OF_TURN_TOKEN|>\n<|START_OF_TURN_TOKEN|><|USER_TOKEN|>{0}<|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>", 75 | "stop": [ 76 | "<|END_OF_TURN_TOKEN|>" 77 | ] 78 | } 79 | } -------------------------------------------------------------------------------- /EasyNovelAssistant/setup/res/requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.31.0 2 | tkinterdnd2==0.3.0 3 | scipy==1.13.0 4 | watchdog==4.0.0 5 | -------------------------------------------------------------------------------- /EasyNovelAssistant/setup/res/tkinter-PythonSoftwareFoundationLicense.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zuntan03/EasyNovelAssistant/d5e5756ae2f5f6ea9fb899ed71f737c699dbd66f/EasyNovelAssistant/setup/res/tkinter-PythonSoftwareFoundationLicense.zip -------------------------------------------------------------------------------- /EasyNovelAssistant/src/const.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | 3 | 4 | class Const: 5 | AREA_MIN_SIZE = 128 6 | 7 | @classmethod 8 | def init(cls, ctx): 9 | cls.TEXT_AREA_CONFIG = { 10 | "spacing1": 4, 11 | "spacing2": 4, 12 | "spacing3": 4, 13 | "wrap": tk.CHAR, 14 | } 15 | -------------------------------------------------------------------------------- /EasyNovelAssistant/src/context.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | from path import Path 5 | 6 | 7 | class Context: 8 | def __init__(self): 9 | self.cfg = None 10 | self.llm = None 11 | self.llm_sequence = None 12 | self._load_config() 13 | 14 | def _load_config(self): 15 | assert os.path.exists(Path.default_config) 16 | with open(Path.default_config, "r", encoding="utf-8-sig") as f: 17 | self.cfg = json.load(f) 18 | if os.path.exists(Path.config): 19 | with open(Path.config, "r", encoding="utf-8-sig") as f: 20 | self.cfg.update(json.load(f)) 21 | else: 22 | with open(Path.config, "w", encoding="utf-8-sig") as f: 23 | json.dump(self.cfg, f, indent=4, ensure_ascii=False) 24 | 25 | with open(Path.default_llm, "r", encoding="utf-8-sig") as f: 26 | self.llm = json.load(f) 27 | if os.path.exists(Path.llm): 28 | with open(Path.llm, "r", encoding="utf-8-sig") as f: 29 | self.llm.update(json.load(f)) 30 | else: 31 | with open(Path.llm, "w", encoding="utf-8-sig") as f: 32 | f.write("{}") 33 | 34 | with open(Path.default_llm_sequence, "r", encoding="utf-8-sig") as f: 35 | self.llm_sequence = json.load(f) 36 | if os.path.exists(Path.llm_sequence): 37 | with open(Path.llm_sequence, "r", encoding="utf-8-sig") as f: 38 | self.llm_sequence.update(json.load(f)) 39 | else: 40 | with open(Path.llm_sequence, "w", encoding="utf-8-sig") as f: 41 | f.write("{}") 42 | 43 | def __getitem__(self, item): 44 | return self.cfg.get(item, None) 45 | 46 | def __setitem__(self, key, value): 47 | self.cfg[key] = value 48 | 49 | def finalize(self): 50 | if not self.form.file_menu.ask_save(): # TODO: すべて閉じる 51 | return 52 | self.form.update_config() 53 | 54 | with open(Path.config, "w", encoding="utf-8-sig") as f: 55 | json.dump(self.cfg, f, indent=4, ensure_ascii=False) 56 | 57 | self.form.win.destroy() 58 | 59 | if self.generator.enabled: 60 | self.kobold_cpp.abort() 61 | -------------------------------------------------------------------------------- /EasyNovelAssistant/src/easy_novel_assistant.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from const import Const 4 | from context import Context 5 | from form import Form 6 | from generator import Generator 7 | from kobold_cpp import KoboldCpp 8 | from movie_maker import MovieMaker 9 | from path import Path 10 | from style_bert_vits2 import StyleBertVits2 11 | 12 | 13 | class EasyNovelAssistant: 14 | SLEEP_TIME = 50 15 | 16 | def __init__(self): 17 | self.ctx = Context() 18 | Path.init(self.ctx) 19 | Const.init(self.ctx) 20 | 21 | self.ctx.kobold_cpp = KoboldCpp(self.ctx) 22 | self.ctx.style_bert_vits2 = StyleBertVits2(self.ctx) 23 | self.ctx.movie_maker = MovieMaker(self.ctx) 24 | self.ctx.form = Form(self.ctx) 25 | self.ctx.generator = Generator(self.ctx) 26 | 27 | # TODO: 起動時引数でのフォルダ・ファイル読み込み 28 | self.ctx.form.input_area.open_tab(self.ctx["input_text"]) # 書き出しは Form の finalize 29 | 30 | self.ctx.form.win.after(self.SLEEP_TIME, self.mainloop) 31 | 32 | def run(self): 33 | self.ctx.form.run() 34 | 35 | def mainloop(self): 36 | self.ctx.generator.update() 37 | self.ctx.style_bert_vits2.update() 38 | self.ctx.form.input_area.update() 39 | self.ctx.form.win.after(self.SLEEP_TIME, self.mainloop) 40 | 41 | 42 | if __name__ == "__main__": 43 | easy_novel_assistant = EasyNovelAssistant() 44 | easy_novel_assistant.run() 45 | -------------------------------------------------------------------------------- /EasyNovelAssistant/src/form.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | 3 | from const import Const 4 | from gen_area import GenArea 5 | from input_area import InputArea 6 | from menu.file_menu import FileMenu 7 | from menu.gen_menu import GenMenu 8 | from menu.help_menu import HelpMenu 9 | from menu.model_menu import ModelMenu 10 | from menu.sample_menu import SampleMenu 11 | from menu.setting_menu import SettingMenu 12 | from menu.speech_menu import SpeechMenu 13 | from menu.tool_menu import ToolMenu 14 | from output_area import OutputArea 15 | from tkinterdnd2 import DND_FILES, TkinterDnD 16 | 17 | 18 | class Form: 19 | WIN_MIN_W = 640 20 | WIN_MIN_H = 480 21 | 22 | def __init__(self, ctx): 23 | self.ctx = ctx 24 | 25 | self.win = TkinterDnD.Tk() 26 | self.win.drop_target_register(DND_FILES) 27 | self.win.title("EasyNovelAssistant") 28 | self.win.minsize(self.WIN_MIN_W, self.WIN_MIN_H) 29 | win_geom = f'{self.ctx["win_width"]}x{self.ctx["win_height"]}' 30 | if self.ctx["win_x"] != 0 or self.ctx["win_y"] != 0: 31 | win_geom += f'+{ctx["win_x"]}+{self.ctx["win_y"]}' 32 | self.win.geometry(win_geom) 33 | self.win.protocol("WM_DELETE_WINDOW", self.ctx.finalize) 34 | self.win.dnd_bind("<>", lambda e: self.file_menu.dnd_file(e)) 35 | 36 | self.menu_bar = tk.Menu(self.win) 37 | self.win.config(menu=self.menu_bar) 38 | 39 | self.file_menu = FileMenu(self, ctx) 40 | self.model_menu = ModelMenu(self, ctx) 41 | self.gen_menu = GenMenu(self, ctx) 42 | self.speech_menu = SpeechMenu(self, ctx) 43 | self.setting_menu = SettingMenu(self, ctx) 44 | self.sample_menu = SampleMenu(self, ctx) 45 | self.tool_menu = ToolMenu(self, ctx) 46 | self.help_menu = HelpMenu(self, ctx) 47 | 48 | self.pane_h = tk.PanedWindow(self.win, orient=tk.HORIZONTAL, sashpad=2) 49 | 50 | self.input_area = InputArea(self.pane_h, ctx) 51 | 52 | self.pane_v = tk.PanedWindow(self.pane_h, orient=tk.VERTICAL, sashpad=2) 53 | self.pane_h.add(self.pane_v, width=ctx["pane_v_width"], minsize=Const.AREA_MIN_SIZE, stretch="always") 54 | 55 | self.output_area = OutputArea(self.pane_v, ctx) 56 | self.gen_area = GenArea(self.pane_v, ctx) 57 | 58 | self.pane_h.pack(fill=tk.BOTH, expand=True) 59 | 60 | def run(self): 61 | self.win.lift() 62 | self.win.mainloop() 63 | 64 | def update_title(self): 65 | title = "EasyNovelAssistant" 66 | if self.ctx.kobold_cpp.model_name is not None: 67 | title += f" - {self.ctx.kobold_cpp.model_name}" 68 | if self.ctx.generator.enabled: 69 | title += " [生成中]" 70 | file_path = self.ctx.form.input_area.get_file_path() 71 | if file_path is not None: 72 | title += f" - {file_path}" 73 | if (not self.ctx.generator.enabled) and (file_path is None): 74 | title += " - [生成] メニューの [生成を開始 (F3)] で生成を開始します。" 75 | self.win.title(title) 76 | 77 | def update_config(self): 78 | ctx = self.ctx 79 | ctx["win_width"] = self.win.winfo_width() 80 | ctx["win_height"] = self.win.winfo_height() 81 | ctx["win_x"] = self.win.winfo_x() 82 | ctx["win_y"] = self.win.winfo_y() 83 | 84 | input_area_width = self.input_area.notebook.winfo_width() 85 | if input_area_width != -1: 86 | ctx["input_area_width"] = input_area_width 87 | 88 | pane_v_width = self.pane_v.winfo_width() 89 | if pane_v_width != -1: 90 | ctx["pane_v_width"] = pane_v_width 91 | 92 | gen_area_height = self.gen_area.text_area.winfo_height() 93 | if gen_area_height != -1: 94 | ctx["gen_area_height"] = gen_area_height 95 | 96 | ctx["input_text"] = self.input_area.get_text() 97 | -------------------------------------------------------------------------------- /EasyNovelAssistant/src/gen_area.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import scrolledtext 3 | 4 | from const import Const 5 | 6 | 7 | class GenArea: 8 | def __init__(self, parent, ctx): 9 | self.ctx = ctx 10 | self.text_area = scrolledtext.ScrolledText(parent, state=tk.DISABLED) 11 | self.text_area.configure(Const.TEXT_AREA_CONFIG) 12 | self.text_area.pack(fill=tk.BOTH, expand=True) 13 | self.apply_text_setting() 14 | parent.add(self.text_area, height=ctx["gen_area_height"], minsize=Const.AREA_MIN_SIZE, stretch="never") 15 | 16 | self.ctx_menu = tk.Menu(self.text_area, tearoff=False) 17 | self.text_area.bind("", self._on_ctx_menu) 18 | 19 | self.text_area.bind("", self._on_middle_click) 20 | 21 | def apply_text_setting(self): 22 | self.text_area.configure(font=(self.ctx["text_area_font"], self.ctx["text_area_font_size"])) 23 | colors = { 24 | "fg": self.ctx["foreground_color"], 25 | "bg": self.ctx["background_color"], 26 | "selectforeground": self.ctx["select_foreground_color"], 27 | "insertbackground": self.ctx["select_foreground_color"], 28 | "selectbackground": self.ctx["select_background_color"], 29 | } 30 | self.text_area.configure(colors) 31 | 32 | def set_text(self, text): 33 | self.text_area.configure(state=tk.NORMAL) 34 | self.text_area.delete("1.0", tk.END) 35 | self.text_area.insert(tk.END, text) 36 | if self.ctx["auto_scroll"]: 37 | self.text_area.see(tk.END) 38 | self.text_area.configure(state=tk.DISABLED) 39 | 40 | def append_text(self, text): 41 | self.text_area.configure(state=tk.NORMAL) 42 | self.text_area.insert(tk.END, text) 43 | if self.ctx["auto_scroll"]: 44 | self.text_area.see(tk.END) 45 | self.text_area.configure(state=tk.DISABLED) 46 | 47 | def _speech(self, e): 48 | line_num = self.text_area.index(f"@{e.x},{e.y}").split(".")[0] 49 | text = self.text_area.get(f"{line_num}.0", f"{line_num}.end") + "\n" 50 | self.ctx.style_bert_vits2.generate(text) 51 | 52 | def _send_to_input(self, e): 53 | text = None 54 | if self.text_area.tag_ranges(tk.SEL): 55 | text = self.text_area.get(tk.SEL_FIRST, tk.SEL_LAST) 56 | else: 57 | line_num = self.text_area.index(f"@{e.x},{e.y}").split(".")[0] 58 | text = self.text_area.get(f"{line_num}.0", f"{line_num}.end") + "\n" 59 | self.ctx.form.input_area.insert_text(text) 60 | 61 | def _on_middle_click(self, e): 62 | if self.ctx["middle_click_speech"]: 63 | self._speech(e) 64 | else: 65 | self._send_to_input(e) 66 | return "break" 67 | 68 | def _on_ctx_menu(self, event): 69 | self.ctx_menu.delete(0, tk.END) 70 | 71 | if self.ctx.style_bert_vits2.models is None: 72 | self.ctx.style_bert_vits2.get_models() 73 | 74 | if self.ctx.style_bert_vits2.models is not None: 75 | self.ctx_menu.add_command(label="読み上げる", command=lambda: self._speech(event)) 76 | 77 | self.ctx_menu.add_command(label="入力欄に送る", command=lambda: self._send_to_input(event)) 78 | 79 | self.text_area.mark_set(tk.INSERT, f"@{event.x},{event.y}") 80 | self.ctx_menu.post(event.x_root, event.y_root) 81 | -------------------------------------------------------------------------------- /EasyNovelAssistant/src/generator.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from job_queue import JobQueue 4 | 5 | 6 | class Generator: 7 | def __init__(self, ctx): 8 | self.ctx = ctx 9 | 10 | self.gen_queue = JobQueue() 11 | self.check_queue = JobQueue() 12 | 13 | self.enabled = False 14 | self.generate_job = None 15 | self.check_job = None 16 | 17 | self.pre_check_time = time.perf_counter() 18 | self.gen_area_text = "" 19 | self.last_line = "" 20 | 21 | self.gen_queue.push(self.initial_launch) 22 | 23 | def initial_launch(self): 24 | model_name = self.ctx.kobold_cpp.get_model() 25 | if model_name is None: 26 | result = self.ctx.kobold_cpp.launch_server() 27 | if result is not None: 28 | print(result) 29 | else: 30 | self.enabled = True 31 | self.ctx.form.update_title() 32 | 33 | def update(self): 34 | if self.enabled: 35 | if self.generate_job is None: 36 | input_text = self.ctx.form.input_area.get_prompt_text() 37 | if input_text != "": 38 | self.generate_job = self.gen_queue.push(self._generate, input_text=input_text) 39 | elif self.generate_job.successful(): 40 | result = self.generate_job.result 41 | if result is None: 42 | if self.ctx.kobold_cpp.abort() is None: 43 | self.enabled = False 44 | self.ctx.form.update_title() 45 | else: 46 | result = self._get_last_line(self.generate_job.args["input_text"]) + result 47 | self.ctx.form.output_area.append_output(result) 48 | self.generate_job = None 49 | elif self.generate_job.canceled(): 50 | self.generate_job = None 51 | 52 | if self.check_job is None: 53 | now_time = time.perf_counter() 54 | if now_time - self.pre_check_time > self.ctx["check_interval"]: 55 | self.check_job = self.check_queue.push(self._check) 56 | self.pre_check_time = now_time 57 | elif self.check_job.successful(): 58 | result = self.check_job.result 59 | if result is not None: 60 | if self.generate_job is not None: 61 | result = self._get_last_line(self.generate_job.args["input_text"]) + result 62 | if result != self.gen_area_text: 63 | if result.startswith(self.gen_area_text): 64 | append_text = result[len(self.gen_area_text) :] 65 | 66 | lines = (self.last_line + append_text).splitlines() 67 | if len(lines) > 0: 68 | for line in lines[:-1]: 69 | self._auto_speech(line) 70 | self.last_line = lines[-1] 71 | if append_text.endswith("\n"): 72 | self._auto_speech(self.last_line) 73 | self.last_line = "" 74 | self.ctx.form.gen_area.append_text(append_text) 75 | else: 76 | lines = result.splitlines() 77 | if len(lines) > 0: 78 | for line in lines[:-1]: 79 | self._auto_speech(line) 80 | self.last_line = lines[-1] 81 | if result.endswith("\n"): 82 | self._auto_speech(self.last_line) 83 | self.last_line = "" 84 | else: 85 | self.last_line = "" 86 | self.ctx.form.gen_area.set_text(result) 87 | self.gen_area_text = result 88 | self.check_job = None 89 | elif self.check_job.canceled(): 90 | self.check_job = None 91 | else: 92 | if self.generate_job is not None: 93 | self.ctx.kobold_cpp.abort() 94 | self.gen_queue.cancel(self.generate_job) 95 | self.generate_job = None 96 | 97 | if self.check_job is not None: 98 | self.check_queue.cancel(self.check_job) 99 | self.check_job = None 100 | 101 | self.check_queue.update() 102 | self.gen_queue.update() 103 | 104 | def _auto_speech(self, text): 105 | if text == "": 106 | return 107 | if "「" in text: 108 | name = text.split("「", 1)[0] 109 | if self.ctx["char_name"] in name: 110 | if self.ctx["auto_speech_char"]: 111 | self.ctx.style_bert_vits2.generate(text) 112 | return 113 | elif self.ctx["user_name"] in name: 114 | if self.ctx["auto_speech_user"]: 115 | self.ctx.style_bert_vits2.generate(text) 116 | return 117 | if self.ctx["auto_speech_other"]: 118 | self.ctx.style_bert_vits2.generate(text) 119 | 120 | def _generate(self, input_text): 121 | return self.ctx.kobold_cpp.generate(input_text) 122 | 123 | def _check(self): 124 | return self.ctx.kobold_cpp.check() 125 | 126 | def _get_last_line(self, text): 127 | if text == "": 128 | return "" 129 | if text.endswith("\n"): 130 | return "" 131 | return text.splitlines()[-1] 132 | 133 | def abort(self): 134 | self.check_queue.push(self.ctx.kobold_cpp.abort) 135 | -------------------------------------------------------------------------------- /EasyNovelAssistant/src/input_area.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tkinter as tk 3 | import tkinter.ttk as ttk 4 | from tkinter import scrolledtext 5 | 6 | from const import Const 7 | from watchdog.events import FileSystemEventHandler 8 | from watchdog.observers import Observer 9 | 10 | 11 | class FileWatcher(FileSystemEventHandler): 12 | def __init__(self, parent, target_path): 13 | self.parent = parent 14 | self.target_path = target_path 15 | self.abs_path = os.path.abspath(target_path) 16 | 17 | def on_modified(self, event): 18 | if os.path.abspath(event.src_path) == self.abs_path: 19 | self.parent.open_request = self.target_path 20 | 21 | 22 | class InputTab: 23 | INTRO_PREFIX = "// intro\n" 24 | 25 | def __init__(self, ctx, notebook, file_text, file_path): 26 | self.ctx = ctx 27 | self.notebook = notebook 28 | 29 | self.observer = None 30 | self.watcher = None 31 | self.open_request = None 32 | 33 | self.text_area = scrolledtext.ScrolledText(self.notebook, undo=True, maxundo=-1) 34 | self.text_area.configure(Const.TEXT_AREA_CONFIG) 35 | self.text_area.pack(fill=tk.BOTH, expand=True) 36 | self._apply_text_setting() 37 | self.text_area.bind("", lambda e: self._update_title()) 38 | 39 | self.file_text = "" 40 | if file_text is not None: 41 | self.file_text = file_text 42 | self._set_text(file_text) 43 | 44 | self.file_path = file_path 45 | self.notebook.add(self.text_area) 46 | self._update_title() 47 | 48 | self.ctx_menu = tk.Menu(self.text_area, tearoff=False) 49 | self.text_area.bind("", self._on_ctx_menu) 50 | self.text_area.bind("", self._on_middle_click) 51 | 52 | self.notebook.select(self.text_area) 53 | 54 | def _update_title(self): 55 | title = "無題" 56 | if self.file_path is not None: 57 | title = os.path.splitext(os.path.basename(self.file_path))[0] 58 | 59 | if self.file_text != self._get_text(): 60 | title += " *" 61 | 62 | if self._is_intro(): 63 | title = "(i) " + title 64 | 65 | self.notebook.tab(self.text_area, text=title) 66 | 67 | def _apply_text_setting(self): 68 | self.text_area.configure(font=(self.ctx["text_area_font"], self.ctx["text_area_font_size"])) 69 | colors = { 70 | "fg": self.ctx["foreground_color"], 71 | "bg": self.ctx["background_color"], 72 | "selectforeground": self.ctx["select_foreground_color"], 73 | "insertbackground": self.ctx["select_foreground_color"], 74 | "selectbackground": self.ctx["select_background_color"], 75 | } 76 | self.text_area.configure(colors) 77 | 78 | def _on_middle_click(self, e): 79 | if self.ctx["middle_click_speech"]: 80 | self._speech(e) 81 | return "break" 82 | 83 | def _on_ctx_menu(self, e): 84 | self.ctx_menu.delete(0, tk.END) 85 | 86 | if self.ctx.style_bert_vits2.models is None: 87 | self.ctx.style_bert_vits2.get_models() 88 | 89 | if self.ctx.style_bert_vits2.models is not None: 90 | self.ctx_menu.add_command(label="読み上げる", command=lambda: self._speech(e)) 91 | self.ctx_menu.add_separator() 92 | 93 | sequence = self.ctx.kobold_cpp.get_instruct_sequence() 94 | if sequence is not None: 95 | self.ctx_menu.add_command(label="指示タグを挿入", command=self._insert_instruct_tag) 96 | self.ctx_menu.add_separator() 97 | 98 | def edit_undo(): 99 | self.text_area.edit_undo() 100 | self._update_title() 101 | 102 | def edit_redo(): 103 | self.text_area.edit_redo() 104 | self._update_title() 105 | 106 | def edit_clear(): 107 | self.text_area.delete("1.0", tk.END) 108 | self._update_title() 109 | 110 | self.ctx_menu.add_command(label="元に戻す (Ctrl+Z)", command=edit_undo) 111 | self.ctx_menu.add_command(label="やり直し (Ctrl+Y)", command=edit_redo) 112 | self.ctx_menu.add_separator() 113 | 114 | self.ctx_menu.add_command(label="クリア", command=edit_clear) 115 | 116 | self.text_area.mark_set(tk.INSERT, f"@{e.x},{e.y}") 117 | self.ctx_menu.post(e.x_root, e.y_root) 118 | 119 | def _speech(self, e): 120 | line_num = self.text_area.index(f"@{e.x},{e.y}").split(".")[0] 121 | text = self.text_area.get(f"{line_num}.0", f"{line_num}.end") + "\n" 122 | self.ctx.style_bert_vits2.generate(text) 123 | 124 | def _insert_instruct_tag(self): 125 | sequence = self.ctx.kobold_cpp.get_instruct_sequence() 126 | if sequence is None: 127 | return 128 | if self.text_area.tag_ranges(tk.SEL): 129 | sequence = sequence.format(self.text_area.get(tk.SEL_FIRST, tk.SEL_LAST)) 130 | self.text_area.replace(tk.SEL_FIRST, tk.SEL_LAST, sequence) 131 | else: 132 | self.text_area.insert(tk.INSERT, sequence.format("")) 133 | self._update_title() 134 | 135 | def _set_text(self, text): 136 | self.text_area.delete("1.0", tk.END) 137 | self._append_text(text) 138 | 139 | def _append_text(self, text): 140 | self.text_area.insert(tk.END, text) 141 | if self.ctx["auto_scroll"]: 142 | self.text_area.see(tk.END) 143 | self.text_area.mark_set(tk.INSERT, tk.END) 144 | 145 | def _insert_text(self, text): 146 | self.text_area.insert(tk.INSERT, text) 147 | if self.ctx["auto_scroll"]: 148 | self.text_area.see(tk.INSERT) 149 | 150 | def _get_text(self): 151 | return self.text_area.get("1.0", "end-1c") 152 | 153 | def _get_comment_removed_text(self): 154 | text = self._get_text() 155 | lines = text.splitlines() 156 | new_lines = [] 157 | for line in lines: 158 | if line.startswith("//"): 159 | continue 160 | new_lines.append(line) 161 | result = "\n".join(new_lines) 162 | if text.endswith("\n"): # splitlines は末尾改行を無視するため 163 | result += "\n" 164 | return result 165 | 166 | def _is_intro(self): 167 | return self._get_text().lower().startswith(self.INTRO_PREFIX) 168 | 169 | def _switch_intro(self): 170 | text = self._get_text() 171 | if text.lower().startswith(self.INTRO_PREFIX): 172 | self._set_text(text[len(self.INTRO_PREFIX) :]) 173 | else: 174 | self._set_text(self.INTRO_PREFIX + text) 175 | 176 | def _update(self): 177 | if self.ctx["watch_file"]: 178 | if (self.watcher is not None) and (self.watcher.target_path != self.file_path): 179 | self.observer.stop() 180 | self.observer.join() 181 | self.observer = None 182 | self.watcher = None 183 | self.open_request = None 184 | 185 | if (self.observer is None) and (self.file_path is not None) and (os.path.exists(self.file_path)): 186 | self.watcher = FileWatcher(self, self.file_path) 187 | self.observer = Observer() 188 | self.observer.schedule(self.watcher, os.path.dirname(self.file_path), recursive=False) 189 | self.open_request = None 190 | self.observer.start() 191 | 192 | if self.open_request is not None: 193 | self.ctx.form.file_menu._open_file(self.open_request, self.ctx.form.file_menu.save_as_file) 194 | self.open_request = None 195 | else: 196 | if self.observer is not None: 197 | self.observer.stop() 198 | self.observer.join() 199 | self.observer = None 200 | self.watcher = None 201 | self.open_request = None 202 | 203 | 204 | class InputArea: 205 | def __init__(self, parent, ctx): 206 | self.ctx = ctx 207 | self.notebook = ttk.Notebook(parent) 208 | self.notebook.bind("<>", self._on_tab_changed) 209 | self.notebook.bind("", self._on_middle_click) 210 | self.ctx_menu = tk.Menu(parent, tearoff=False) 211 | self.notebook.bind("", self._on_ctx_menu) 212 | 213 | parent.add(self.notebook, width=ctx["input_area_width"], minsize=Const.AREA_MIN_SIZE, stretch="always") 214 | 215 | self.tabs = [] 216 | self.tab = None 217 | 218 | def _on_ctx_menu(self, e): 219 | self.ctx_menu.delete(0, tk.END) 220 | try: 221 | tab_index = self.notebook.index("@%d,%d" % (e.x, e.y)) 222 | except tk.TclError: 223 | return "break" 224 | 225 | self.notebook.select(tab_index) 226 | current_tab = self.tabs[tab_index] 227 | 228 | def switch_intro(): 229 | current_tab._switch_intro() 230 | current_tab._update_title() 231 | 232 | intro_var = tk.BooleanVar(value=current_tab._is_intro()) 233 | self.ctx_menu.add_checkbutton(label="イントロプロンプト", variable=intro_var, command=switch_intro) 234 | 235 | self.ctx_menu.add_separator() 236 | 237 | def copy_tab(): 238 | # 末尾以外なら tabs と notebook の同期必要 239 | self.open_tab(current_tab._get_text(), None) 240 | 241 | self.ctx_menu.add_command(label="タブを複製", command=copy_tab) 242 | 243 | self.ctx_menu.add_separator() 244 | 245 | def close_right(): 246 | for tab in reversed(self.tabs[tab_index + 1 :]): 247 | self.select_tab(tab) 248 | self.ctx.form.file_menu.close_file() 249 | 250 | self.ctx_menu.add_command(label="右側を閉じる", command=close_right) 251 | 252 | def close_others(): 253 | for tab in reversed(self.tabs): 254 | if tab == current_tab: 255 | continue 256 | self.select_tab(tab) 257 | self.ctx.form.file_menu.close_file() 258 | 259 | self.ctx_menu.add_command(label="他を閉じる", command=close_others) 260 | 261 | # 閉じる 262 | self.ctx_menu.add_command(label="閉じる", command=lambda: self.ctx.form.file_menu.close_file()) 263 | 264 | self.ctx_menu.post(e.x_root, e.y_root) 265 | return "break" 266 | 267 | def _on_middle_click(self, e): 268 | try: 269 | tab_index = self.notebook.index("@%d,%d" % (e.x, e.y)) 270 | except tk.TclError: 271 | return "break" 272 | self.notebook.select(tab_index) 273 | self.ctx.form.file_menu.close_file() 274 | return "break" 275 | 276 | def _on_tab_changed(self, e): 277 | self.tab = self.tabs[self.notebook.index("current")] 278 | self.ctx.form.update_title() 279 | 280 | def open_tab(self, text=None, path=None): 281 | # パスが一致するタブがあれば再利用 282 | if path is not None: 283 | for tab in self.tabs: 284 | if tab.file_path == path: 285 | self.select_tab(tab) 286 | self.set_text(text) 287 | self.set_file(text, path) 288 | tab._update_title() 289 | return 290 | 291 | self.tab = InputTab(self.ctx, self.notebook, text, path) 292 | self.tabs.append(self.tab) 293 | 294 | def close_tab(self, safety=True): 295 | self.tabs.remove(self.tab) 296 | self.notebook.forget(self.tab.text_area) 297 | self.tab = None 298 | if len(self.tabs) > 0: 299 | self.tab = self.tabs[-1] 300 | self.notebook.select(self.tab.text_area) 301 | else: 302 | if safety: 303 | self.open_tab() 304 | 305 | def select_tab(self, tab): 306 | self.tab = tab 307 | self.notebook.select(tab.text_area) 308 | 309 | def close_unmodified_new_tab(self): 310 | if (self.tab.file_path is None) and (self.tab.file_text == self.tab._get_text()): 311 | self.close_tab(False) 312 | 313 | def apply_text_setting(self): 314 | for tab in self.tabs: 315 | tab._apply_text_setting() 316 | 317 | def set_text(self, text): 318 | self.tab._set_text(text) 319 | 320 | def get_file_path(self): 321 | return self.tab.file_path 322 | 323 | def get_file_text(self): 324 | return self.tab.file_text 325 | 326 | def set_file(self, file_text, file_path): 327 | self.tab.file_text = file_text 328 | self.tab.file_path = file_path 329 | self.tab._update_title() 330 | 331 | def append_text(self, text): 332 | self.tab._append_text(text) 333 | 334 | def insert_text(self, text): 335 | self.tab._insert_text(text) 336 | 337 | def get_text(self): 338 | return self.tab._get_text() 339 | 340 | def get_comment_removed_text(self): 341 | return self.tab._get_comment_removed_text() 342 | 343 | def get_prompt_text(self): 344 | text = "" 345 | for tab in self.tabs: 346 | if tab == self.tab: 347 | continue 348 | if tab._is_intro(): 349 | text += tab._get_comment_removed_text() 350 | text += self.get_comment_removed_text() 351 | return text 352 | 353 | def update(self): 354 | for tab in self.tabs: 355 | tab._update() 356 | -------------------------------------------------------------------------------- /EasyNovelAssistant/src/job_queue.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import time 3 | 4 | 5 | class Job: 6 | def __init__(self, func, **kwargs): 7 | self.func = func 8 | self.args = kwargs 9 | self.started = False 10 | self.finished = False 11 | self.result = None 12 | self.callback = None 13 | 14 | def cancel(self): 15 | if not self.started: 16 | self.finished = True 17 | 18 | def canceled(self): 19 | return (not self.started) and self.finished 20 | 21 | def successful(self): 22 | return self.started and self.finished 23 | 24 | 25 | class JobQueue: 26 | sleep_time = 0.05 27 | 28 | def __init__(self): 29 | self._queue = [] 30 | self._current = None 31 | self._thread = threading.Thread(target=self._run, daemon=True) 32 | self._thread.start() 33 | 34 | def _run(self): 35 | while True: 36 | job = self._current 37 | if (job is not None) and (not job.finished): 38 | assert job.started 39 | job.result = job.func(**job.args) 40 | job.finished = True 41 | if job.callback is not None: 42 | job.callback(job) 43 | time.sleep(JobQueue.sleep_time) 44 | 45 | def update(self) -> Job: 46 | for i in range((len(self._queue) - 1), -1, -1): 47 | if self._queue[i].finished: 48 | self._queue.pop(i) 49 | 50 | end_job = None 51 | if (self._current is None) or self._current.finished: 52 | start_job = None 53 | if len(self._queue) > 0: 54 | start_job = self._queue.pop(0) 55 | start_job.started = True 56 | end_job = self._current 57 | self._current = start_job 58 | return end_job 59 | 60 | def push(self, job_func, **kwargs) -> Job: 61 | job = Job(job_func, **kwargs) 62 | self._queue.append(job) 63 | return job 64 | 65 | def empty(self): 66 | return (len(self._queue) == 0) and ((self._current is None) or self._current.finished) 67 | 68 | def cancel(self, job): 69 | job.cancel() 70 | if job.finished and (job in self._queue): 71 | self._queue.remove(job) 72 | 73 | def cancel_all(self): 74 | for i in range((len(self._queue) - 1), -1, -1): 75 | self._queue[i].cancel() 76 | if self._queue[i].finished: 77 | self._queue.pop(i) 78 | 79 | def len(self): 80 | return len(self._queue) + (0 if (self._current is None) or self._current.finished else 1) 81 | -------------------------------------------------------------------------------- /EasyNovelAssistant/src/kobold_cpp.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import subprocess 4 | import webbrowser 5 | from sys import platform 6 | 7 | import requests 8 | from path import Path 9 | 10 | 11 | class KoboldCpp: 12 | BAT_TEMPLATE = """@echo off 13 | chcp 65001 > NUL 14 | pushd %~dp0 15 | set CURL_CMD=C:\Windows\System32\curl.exe -k 16 | 17 | @REM 7B: 33, 35B: 41, 70B: 65 18 | set GPU_LAYERS=0 19 | 20 | @REM 2048, 4096, 8192, 16384, 32768, 65536, 131072 21 | set CONTEXT_SIZE={context_size} 22 | 23 | {curl_cmd} 24 | koboldcpp.exe --gpulayers %GPU_LAYERS% {option} --contextsize %CONTEXT_SIZE% {file_name} 25 | if %errorlevel% neq 0 ( pause & popd & exit /b 1 ) 26 | popd 27 | """ 28 | 29 | CURL_TEMPLATE = """if not exist {file_name} ( 30 | start "" {info_url} 31 | %CURL_CMD% -LO {url} 32 | ) 33 | """ 34 | 35 | def __init__(self, ctx): 36 | self.ctx = ctx 37 | self.base_url = f'http://{ctx["koboldcpp_host"]}:{ctx["koboldcpp_port"]}' 38 | self.model_url = f"{self.base_url}/api/v1/model" 39 | self.generate_url = f"{self.base_url}/api/v1/generate" 40 | self.check_url = f"{self.base_url}/api/extra/generate/check" 41 | self.abort_url = f"{self.base_url}/api/extra/abort" 42 | 43 | self.model_name = None 44 | 45 | for llm_name in ctx.llm: 46 | llm = ctx.llm[llm_name] 47 | name = llm_name 48 | if "/" in llm_name: 49 | _, name = llm_name.split("/") 50 | if " " in name: 51 | name = name.split(" ")[-1] 52 | llm["name"] = name 53 | 54 | llm["file_names"] = [] 55 | for url in llm["urls"]: 56 | llm["file_names"].append(url.split("/")[-1]) 57 | llm["file_name"] = llm["file_names"][0] 58 | # urls[0]の "/resolve/main/" より前を取得 59 | llm["info_url"] = llm["urls"][0].split("/resolve/main/")[0] 60 | 61 | context_size = min(llm["context_size"], ctx["llm_context_size"]) 62 | bat_file = os.path.join(Path.kobold_cpp, f'Run-{llm["name"]}-C{context_size // 1024}K-L0.bat') 63 | 64 | curl_cmd = "" 65 | for url in llm["urls"]: 66 | curl_cmd += self.CURL_TEMPLATE.format(url=url, file_name=url.split("/")[-1], info_url=llm["info_url"]) 67 | bat_text = self.BAT_TEMPLATE.format( 68 | curl_cmd=curl_cmd, 69 | option=ctx["koboldcpp_arg"], 70 | context_size=context_size, 71 | file_name=llm["file_name"], 72 | ) 73 | with open(bat_file, "w", encoding="utf-8") as f: 74 | f.write(bat_text) 75 | 76 | def get_model(self): 77 | try: 78 | response = requests.get(self.model_url, timeout=self.ctx["koboldcpp_command_timeout"]) 79 | if response.status_code == 200: 80 | self.model_name = response.json()["result"].split("/")[-1] 81 | return self.model_name 82 | except Exception as e: 83 | pass 84 | self.model_name = None 85 | return self.model_name 86 | 87 | def get_instruct_sequence(self): 88 | if self.model_name is not None: 89 | for sequence in self.ctx.llm_sequence.values(): 90 | for model_name in sequence["model_names"]: 91 | if model_name in self.model_name: 92 | return sequence["instruct"] 93 | return None 94 | 95 | def get_stop_sequence(self): 96 | if self.model_name is not None: 97 | for sequence in self.ctx.llm_sequence.values(): 98 | for model_name in sequence["model_names"]: 99 | if model_name in self.model_name: 100 | return sequence["stop"] 101 | return [] 102 | 103 | def download_model(self, llm_name): 104 | llm = self.ctx.llm[llm_name] 105 | webbrowser.open(llm["info_url"]) 106 | for url in llm["urls"]: 107 | curl_cmd = f"curl -k -LO {url}" 108 | if subprocess.run(curl_cmd, shell=True, cwd=Path.kobold_cpp).returncode != 0: 109 | return f"{llm_name} のダウンロードに失敗しました。\n{curl_cmd}" 110 | return None 111 | 112 | def launch_server(self): 113 | loaded_model = self.get_model() 114 | if loaded_model is not None: 115 | return f"{loaded_model} がすでにロード済みです。\nモデルサーバーのコマンドプロンプトを閉じてからロードしてください。" 116 | 117 | if self.ctx["llm_name"] not in self.ctx.llm: 118 | self.ctx["llm_name"] = "[元祖] LightChatAssistant-TypeB-2x7B-IQ4_XS" 119 | self.ctx["llm_gpu_layer"] = 0 120 | 121 | llm_name = self.ctx["llm_name"] 122 | gpu_layer = self.ctx["llm_gpu_layer"] 123 | 124 | llm = self.ctx.llm[llm_name] 125 | llm_path = os.path.join(Path.kobold_cpp, llm["file_name"]) 126 | 127 | if not os.path.exists(llm_path): 128 | result = self.download_model(llm_name) 129 | if result is not None: 130 | return result 131 | 132 | if not os.path.exists(llm_path): 133 | return f"{llm_path} がありません。" 134 | 135 | context_size = min(llm["context_size"], self.ctx["llm_context_size"]) 136 | command_args = f'{self.ctx["koboldcpp_arg"]} --gpulayers {gpu_layer} --contextsize {context_size} {llm_path}' 137 | if platform == "win32": 138 | command = ["start", f"{llm_name} L{gpu_layer}", "cmd", "/c"] 139 | command.append(f"{Path.kobold_cpp_win} {command_args} || pause") 140 | subprocess.run(command, cwd=Path.kobold_cpp, shell=True) 141 | else: 142 | subprocess.Popen(f"{Path.kobold_cpp_linux} {command_args}", cwd=Path.kobold_cpp, shell=True) 143 | return None 144 | 145 | def generate(self, text): 146 | ctx = self.ctx 147 | 148 | if self.ctx["llm_name"] not in self.ctx.llm: 149 | self.ctx["llm_name"] = "[元祖] LightChatAssistant-TypeB-2x7B-IQ4_XS" 150 | self.ctx["llm_gpu_layer"] = 0 151 | 152 | llm_name = ctx["llm_name"] 153 | llm = ctx.llm[llm_name] 154 | 155 | # api/extra/true_max_context_length なら立ち上げ済みサーバーに対応可能 156 | max_context_length = min(llm["context_size"], ctx["llm_context_size"]) 157 | if ctx["max_length"] >= max_context_length: 158 | print( 159 | f'生成文の長さ ({ctx["max_length"]}) がコンテキストサイズ上限 ({max_context_length}) 以上なため、{max_context_length // 2} に短縮します。' 160 | ) 161 | ctx["max_length"] = max_context_length // 2 162 | 163 | args = { 164 | "max_context_length": max_context_length, 165 | "max_length": ctx["max_length"], 166 | "prompt": text, 167 | "quiet": False, 168 | "stop_sequence": self.get_stop_sequence(), 169 | "rep_pen": ctx["rep_pen"], 170 | "rep_pen_range": ctx["rep_pen_range"], 171 | "rep_pen_slope": ctx["rep_pen_slope"], 172 | "temperature": ctx["temperature"], 173 | "tfs": ctx["tfs"], 174 | "top_a": ctx["top_a"], 175 | "top_k": ctx["top_k"], 176 | "top_p": ctx["top_p"], 177 | "typical": ctx["typical"], 178 | "min_p": ctx["min_p"], 179 | "sampler_order": ctx["sampler_order"], 180 | } 181 | print(f"KoboldCpp.generate({args})") 182 | try: 183 | response = requests.post(self.generate_url, json=args) 184 | if response.status_code == 200: 185 | if self.model_name is not None: 186 | args["model_name"] = self.model_name 187 | args["result"] = response.json()["results"][0]["text"] 188 | print(f'KoboldCpp.generate(): {args["result"]}') 189 | with open(Path.generate_log, "a", encoding="utf-8-sig") as f: 190 | json.dump(args, f, indent=4, ensure_ascii=False) 191 | f.write("\n") 192 | return args["result"] 193 | print(f"[失敗] KoboldCpp.generate(): {response.text}") 194 | except Exception as e: 195 | print(f"[例外] KoboldCpp.generate(): {e}") 196 | return None 197 | 198 | def check(self): 199 | try: 200 | response = requests.get(self.check_url) 201 | if response.status_code == 200: 202 | return response.json()["results"][0]["text"] 203 | print(f"[失敗] KoboldCpp.check(): {response.text}") 204 | except Exception as e: 205 | pass # print(f"[例外] KoboldCpp.check(): {e}") # 害が無さそう&利用者が混乱しそう 206 | return None 207 | 208 | def abort(self): 209 | try: 210 | response = requests.post(self.abort_url, timeout=self.ctx["koboldcpp_command_timeout"]) 211 | if response.status_code == 200: 212 | return response.json()["success"] 213 | print(f"[失敗] KoboldCpp.abort(): {response.text}") 214 | except Exception as e: 215 | print(f"[例外] KoboldCpp.abort(): {e}") 216 | return None 217 | -------------------------------------------------------------------------------- /EasyNovelAssistant/src/menu/file_menu.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import os 3 | import re 4 | import shutil 5 | import time 6 | import tkinter as tk 7 | from tkinter import filedialog, messagebox 8 | 9 | from path import Path 10 | 11 | 12 | class FileMenu: 13 | def __init__(self, form, ctx): 14 | self.form = form 15 | self.ctx = ctx 16 | 17 | self.menu = tk.Menu(form.win, tearoff=False) 18 | self.form.menu_bar.add_cascade(label="ファイル", menu=self.menu) 19 | self.menu.configure(postcommand=self.on_menu_open) 20 | 21 | self.form.win.bind("", lambda e: self.new_file()) 22 | self.form.win.bind("", lambda e: self.open_file()) 23 | self.form.win.bind("", lambda e: self.open_dir()) 24 | self.form.win.bind("", lambda e: self.save_file()) 25 | self.form.win.bind("", lambda e: self.save_all_file()) 26 | self.form.win.bind("", lambda e: self.close_file()) 27 | 28 | def on_menu_open(self): 29 | self.menu.delete(0, tk.END) 30 | 31 | if len(self.ctx["recent_files"]) > 0: # 最近使ったファイル、のカスケードメニューadd_cascade 32 | recent_files = tk.Menu(self.menu, tearoff=False) 33 | self.menu.add_cascade(label="最近使ったファイル", menu=recent_files) 34 | for file_path in reversed(self.ctx["recent_files"]): 35 | if os.path.exists(file_path): 36 | recent_files.add_command(label=file_path, command=lambda fp=file_path: self._open_file(fp)) 37 | else: 38 | self.ctx["recent_files"].remove(file_path) 39 | 40 | if len(self.ctx["recent_dirs"]) > 0: # 最近使ったフォルダ、のカスケードメニューadd_cascade 41 | recent_dirs = tk.Menu(self.menu, tearoff=False) 42 | self.menu.add_cascade(label="最近開いたフォルダ", menu=recent_dirs) 43 | for dir_path in reversed(self.ctx["recent_dirs"]): 44 | if os.path.exists(dir_path): 45 | recent_dirs.add_command(label=dir_path, command=lambda dp=dir_path: self._open_dir(dp)) 46 | else: 47 | self.ctx["recent_dirs"].remove(dir_path) 48 | 49 | if (len(self.ctx["recent_files"]) > 0) or (len(self.ctx["recent_dirs"]) > 0): 50 | self.menu.add_separator() 51 | 52 | self.menu.add_command(label="新規作成 (Ctrl+N)", command=self.new_file) 53 | self.menu.add_command(label="開く (Ctrl+O)", command=self.open_file) 54 | self.menu.add_command(label="フォルダを開く (Ctrl+Shift+O)", command=self.open_dir) 55 | 56 | self.menu.add_separator() 57 | 58 | def _set_watch_file(*args): 59 | self.ctx["watch_file"] = self.watch_file_var.get() 60 | 61 | self.watch_file_var = tk.BooleanVar(value=self.ctx["watch_file"]) 62 | self.watch_file_var.trace_add("write", _set_watch_file) 63 | self.menu.add_checkbutton(label="ファイル監視", variable=self.watch_file_var) 64 | 65 | self.menu.add_separator() 66 | 67 | self.menu.add_command(label="保存 (Ctrl+S)", command=self.save_file) 68 | self.menu.add_command(label="名前を付けて保存", command=self.save_as_file) 69 | self.menu.add_command(label="すべて保存 (Ctrl+Shift+S)", command=self.save_all_file) 70 | 71 | self.menu.add_separator() 72 | 73 | self.menu.add_command(label="閉じる (Ctrl+F4)", command=self.close_file) 74 | self.menu.add_command(label="すべて閉じる", command=self.close_all_file) 75 | 76 | self.menu.add_separator() 77 | 78 | self.menu.add_command(label="終了 (Alt+F4)", command=self.ctx.finalize) 79 | 80 | def new_file(self): 81 | input_area = self.ctx.form.input_area 82 | input_area.close_unmodified_new_tab() 83 | input_area.open_tab() 84 | self.form.update_title() 85 | 86 | def open_file(self): 87 | initial_path = self.ctx.form.input_area.get_file_path() 88 | if initial_path is None: 89 | initial_dir = Path.cwd 90 | else: 91 | initial_dir = os.path.dirname(initial_path) 92 | file_paths = filedialog.askopenfilename( 93 | filetypes=[("テキストファイル", "*.txt")], initialdir=initial_dir, multiple=True 94 | ) 95 | if file_paths != "": 96 | for file_path in file_paths: 97 | self._open_file(file_path) 98 | 99 | def open_dir(self): 100 | initial_path = self.ctx.form.input_area.get_file_path() 101 | if initial_path is None: 102 | initial_dir = Path.cwd 103 | else: 104 | initial_dir = os.path.dirname(initial_path) 105 | dir_path = filedialog.askdirectory(initialdir=initial_dir) 106 | if dir_path != "": 107 | self._open_dir(dir_path) 108 | 109 | def dnd_file(self, event): 110 | file_paths = re.findall(r"\{[^}]*\}|[^ ]+", event.data) 111 | file_paths = [file_path.strip("{}") for file_path in file_paths] 112 | file_paths.sort() 113 | for file_path in file_paths: 114 | if file_path.endswith(".txt"): 115 | if os.path.exists(file_path): 116 | self._open_file(file_path) 117 | elif os.path.isdir(file_path): 118 | self._open_dir(file_path) 119 | return "break" 120 | 121 | def _open_dir(self, dir_path): 122 | txt_files = glob.glob(os.path.join(dir_path, "**", "*.txt"), recursive=True) 123 | txt_files.sort() 124 | 125 | for file_path in txt_files: 126 | if os.path.exists(file_path): 127 | self._open_file(file_path) 128 | 129 | if dir_path in self.ctx["recent_dirs"]: 130 | self.ctx["recent_dirs"].remove(dir_path) 131 | self.ctx["recent_dirs"].append(dir_path) 132 | remove_num = len(self.ctx["recent_dirs"]) - self.ctx["recents"] 133 | if remove_num > 0: 134 | self.ctx["recent_dirs"] = self.ctx["recent_dirs"][remove_num:] 135 | 136 | def _open_file(self, file_path, func=None): 137 | input_area = self.ctx.form.input_area 138 | for tab in input_area.tabs: 139 | if tab.file_path == file_path: 140 | input_area.select_tab(tab) 141 | if not self.ask_save(func): 142 | return 143 | 144 | input_area = self.ctx.form.input_area 145 | with open(file_path, "r", encoding="utf-8-sig") as f: 146 | input_text = f.read() 147 | input_area.close_unmodified_new_tab() 148 | input_area.open_tab(input_text, file_path) 149 | self.form.update_title() 150 | self._add_recent_file(file_path) 151 | 152 | def _add_recent_file(self, file_path): 153 | if file_path in self.ctx["recent_files"]: 154 | self.ctx["recent_files"].remove(file_path) 155 | self.ctx["recent_files"].append(file_path) 156 | remove_num = len(self.ctx["recent_files"]) - self.ctx["recents"] 157 | if remove_num > 0: 158 | self.ctx["recent_files"] = self.ctx["recent_files"][remove_num:] 159 | 160 | def save_file(self): 161 | input_area = self.ctx.form.input_area 162 | file_path = input_area.get_file_path() 163 | if file_path is None: 164 | return self.save_as_file() 165 | self._backup_file(file_path) 166 | input_text = input_area.get_text() 167 | with open(file_path, "w", encoding="utf-8-sig") as f: 168 | f.write(input_text) 169 | input_area.set_file(input_text, file_path) 170 | self._add_recent_file(file_path) 171 | return True 172 | 173 | def save_as_file(self): 174 | input_area = self.ctx.form.input_area 175 | file_name = f'{time.strftime("%Y%m%d_%H%M%S", time.localtime())}.txt' 176 | 177 | if input_area.get_file_path() is None: 178 | initial_dir = Path.cwd 179 | else: 180 | initial_dir = os.path.dirname(input_area.get_file_path()) 181 | file_types = [("テキストファイル", "*.txt")] 182 | file_path = filedialog.asksaveasfilename(filetypes=file_types, initialdir=initial_dir, initialfile=file_name) 183 | if file_path == "": 184 | return False 185 | if not file_path.endswith(".txt"): 186 | file_path += ".txt" 187 | 188 | self._backup_file(file_path) 189 | input_text = input_area.get_text() 190 | with open(file_path, "w", encoding="utf-8-sig") as f: 191 | f.write(input_text) 192 | input_area.set_file(input_text, file_path) 193 | self.form.update_title() 194 | self._add_recent_file(file_path) 195 | return True 196 | 197 | def _backup_file(self, file_path): 198 | if not os.path.exists(file_path): 199 | return 200 | YYYYMMDD_HHMMSS = time.strftime("%Y%m%d_%H%M%S", time.localtime()) 201 | log_file_name = f"{YYYYMMDD_HHMMSS}-{os.path.basename(file_path)}" 202 | shutil.copy2(file_path, os.path.join(Path.daily_log, log_file_name)) 203 | 204 | def save_all_file(self): 205 | input_area = self.ctx.form.input_area 206 | init_tab = input_area.tab 207 | for tab in input_area.tabs: 208 | input_area.select_tab(tab) 209 | self.save_file() 210 | input_area.select_tab(init_tab) 211 | 212 | def close_file(self): 213 | input_area = self.ctx.form.input_area 214 | if not self.ask_save(): 215 | return 216 | input_area.close_tab() 217 | self.form.update_title() 218 | 219 | def close_all_file(self): 220 | input_area = self.ctx.form.input_area 221 | 222 | for tab in reversed(input_area.tabs): 223 | input_area.select_tab(tab) 224 | self.close_file() 225 | 226 | def ask_save(self, func=None): 227 | input_area = self.ctx.form.input_area 228 | 229 | input_text = input_area.get_text() 230 | if input_area.get_file_text() == input_text: 231 | return True 232 | result = messagebox.askyesnocancel("EasyNovelAssistant", "変更内容を保存しますか?") 233 | if result is None: 234 | return False 235 | if result: 236 | if func is None: 237 | func = self.save_file 238 | return func() 239 | return True 240 | -------------------------------------------------------------------------------- /EasyNovelAssistant/src/menu/gen_menu.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | 3 | 4 | class GenMenu: 5 | def __init__(self, form, ctx): 6 | self.form = form 7 | self.ctx = ctx 8 | 9 | self.menu = tk.Menu(form.win, tearoff=False) 10 | self.form.menu_bar.add_cascade(label="生成", menu=self.menu) 11 | self.menu.configure(postcommand=self.on_menu_open) 12 | 13 | self.form.win.bind("", lambda e: self._enable()) 14 | self.form.win.bind("", lambda e: self._disable()) 15 | self.form.win.bind("", lambda e: self._set_enabled(not self.ctx.generator.enabled)) 16 | self.form.win.bind("", lambda e: self._abort()) 17 | 18 | def _set_enabled(self, enabled): 19 | self.ctx.generator.enabled = enabled 20 | self.ctx.kobold_cpp.get_model() 21 | self.ctx.form.update_title() 22 | if not enabled: 23 | self.ctx.generator.abort() 24 | 25 | def _enable(self): 26 | if not self.ctx.generator.enabled: 27 | self._set_enabled(True) 28 | 29 | def _disable(self): 30 | if self.ctx.generator.enabled: 31 | self._set_enabled(False) 32 | 33 | def _abort(self): 34 | self.ctx.generator.abort() 35 | self.ctx.style_bert_vits2.abort() 36 | 37 | def on_menu_open(self): 38 | self.menu.delete(0, tk.END) 39 | 40 | self.menu.add_command(label="生成を開始 (F3)", command=self._enable) 41 | 42 | self.menu.add_command(label="生成を終了 (F4)", command=self._disable) 43 | 44 | def set_enabled(*args): 45 | self._set_enabled(self.enabled_var.get()) 46 | 47 | self.enabled_var = tk.BooleanVar(value=self.ctx.generator.enabled) 48 | self.enabled_var.trace_add("write", set_enabled) 49 | self.menu.add_checkbutton(label="生成の開始/終了 (Shift+F5)", variable=self.enabled_var) 50 | 51 | self.menu.add_command(label="生成を中断 (F5)", command=self.ctx.generator.abort) 52 | 53 | self.menu.add_separator() 54 | 55 | def set_max_length(max_length): 56 | self.ctx["max_length"] = max_length 57 | 58 | self.max_length_menu = tk.Menu(self.menu, tearoff=False) 59 | self.menu.add_cascade(label=f'生成文の長さ: {self.ctx["max_length"]}', menu=self.max_length_menu) 60 | 61 | llm = self.ctx.llm[self.ctx["llm_name"]] 62 | max_context_length = min(llm["context_size"], self.ctx["llm_context_size"]) 63 | for max_length in self.ctx["max_lengths"]: 64 | if max_length >= max_context_length: 65 | break 66 | check_var = tk.BooleanVar(value=self.ctx["max_length"] == max_length) 67 | self.max_length_menu.add_checkbutton( 68 | label=max_length, variable=check_var, command=lambda gl=max_length, _=check_var: set_max_length(gl) 69 | ) 70 | 71 | self.temperature_menu = tk.Menu(self.menu, tearoff=False) 72 | self.menu.add_cascade( 73 | label=f'ゆらぎ度合い (Temperature): {self.ctx["temperature"]}', menu=self.temperature_menu 74 | ) 75 | 76 | def set_temperature(temperature): 77 | self.ctx["temperature"] = temperature 78 | 79 | temperatures = [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0] 80 | for temperature in temperatures: 81 | check_var = tk.BooleanVar(value=self.ctx["temperature"] == temperature) 82 | self.temperature_menu.add_checkbutton( 83 | label=temperature, variable=check_var, command=lambda t=temperature, _=check_var: set_temperature(t) 84 | ) 85 | 86 | self.menu.add_separator() 87 | 88 | def set_auto_scroll(*args): 89 | self.ctx["auto_scroll"] = self.auto_scroll_var.get() 90 | 91 | self.auto_scroll_var = tk.BooleanVar(value=self.ctx["auto_scroll"]) 92 | self.auto_scroll_var.trace_add("write", set_auto_scroll) 93 | self.menu.add_checkbutton(label="自動スクロール", variable=self.auto_scroll_var) 94 | -------------------------------------------------------------------------------- /EasyNovelAssistant/src/menu/help_menu.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | import webbrowser 3 | 4 | 5 | class HelpMenu: 6 | 7 | def __init__(self, form, ctx): 8 | self.form = form 9 | self.ctx = ctx 10 | 11 | self.menu = tk.Menu(form.win, tearoff=False) 12 | self.form.menu_bar.add_cascade(label="ヘルプ", menu=self.menu) 13 | self.menu.configure(postcommand=self.on_menu_open) 14 | 15 | def on_menu_open(self): 16 | self.menu.delete(0, tk.END) 17 | 18 | sample_menu = tk.Menu(self.menu, tearoff=False) 19 | self.menu.add_cascade(label="サンプル原典", menu=sample_menu) 20 | 21 | cmd = lambda: self._show_url("https://kakuyomu.jp/works/16818093074043995181") 22 | sample_menu.add_command(label="最新AI Claude 3で長編小説執筆支援【GPT-4を超えた!?】", command=cmd) 23 | 24 | cmd = lambda: self._show_url("https://kakuyomu.jp/works/16818093074043995181/episodes/16818093074305285059") 25 | sample_menu.add_command(label="↑ のプロンプトまとめ", command=cmd) 26 | 27 | cmd = lambda: self._show_url("https://rentry.org/gpt0721") 28 | sample_menu.add_command(label="5ch プロンプトまとめ", command=cmd) 29 | 30 | reference_menu = tk.Menu(self.menu, tearoff=False) 31 | 32 | self.menu.add_cascade(label="参照", menu=reference_menu) 33 | 34 | cmd = lambda: self._show_url("https://github.com/LostRuins/koboldcpp") 35 | reference_menu.add_command(label="LostRuins/KoboldCpp", command=cmd) 36 | reference_menu.add_separator() 37 | 38 | info_urls = [] 39 | for llm in self.ctx.llm.values(): 40 | if llm["info_url"] not in info_urls: 41 | info_urls.append(llm["info_url"]) 42 | 43 | for info_url in info_urls: 44 | cmd = lambda url=info_url: self._show_url(url) 45 | parts = info_url.split("/") 46 | label = f"{parts[-2]}/{parts[-1]}" 47 | reference_menu.add_command(label=label, command=cmd) 48 | 49 | reference_menu.add_separator() 50 | self._show_hf_url(reference_menu, "kaunista/kaunista-style-bert-vits2-models") 51 | self._show_hf_url(reference_menu, "RinneAi/Rinne_Style-Bert-VITS2") 52 | 53 | self.menu.add_separator() 54 | cmd = lambda: self._show_url("https://github.com/Zuntan03/EasyNovelAssistant") 55 | self.menu.add_command(label="EasyNovelAssistant", command=cmd) 56 | 57 | def _show_hf_url(self, menu, hf_name): 58 | cmd = lambda: self._show_url(f"https://huggingface.co/{hf_name}") 59 | menu.add_command(label=hf_name, command=cmd) 60 | 61 | def _show_url(self, url): 62 | webbrowser.open(url) 63 | -------------------------------------------------------------------------------- /EasyNovelAssistant/src/menu/model_menu.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tkinter as tk 3 | from tkinter import simpledialog 4 | 5 | from path import Path 6 | 7 | 8 | class ModelMenu: 9 | SEPALATER_NAMES = [ 10 | "LightChatAssistant-4x7B-IQ4_XS", 11 | ] 12 | 13 | def __init__(self, form, ctx): 14 | self.form = form 15 | self.ctx = ctx 16 | 17 | self.menu = tk.Menu(form.win, tearoff=False) 18 | self.form.menu_bar.add_cascade(label="モデル", menu=self.menu) 19 | self.menu.configure(postcommand=self.on_menu_open) 20 | 21 | def on_menu_open(self): 22 | self.menu.delete(0, tk.END) 23 | 24 | def context_label(context_size): 25 | llm_name = self.ctx["llm_name"] 26 | model_ctx_size = self.ctx.llm[llm_name]["context_size"] 27 | if context_size > model_ctx_size: 28 | return f"C{model_ctx_size // 1024}K: {context_size} > {model_ctx_size}({llm_name})" 29 | return f"C{context_size // 1024}K: {context_size}" 30 | 31 | self.llm_context_size_menu = tk.Menu(self.menu, tearoff=False) 32 | self.menu.add_cascade( 33 | label=f'コンテキストサイズ上限(増やすと VRAM 消費増): {context_label(self.ctx["llm_context_size"])})', 34 | menu=self.llm_context_size_menu, 35 | ) 36 | 37 | def set_llm_context_size(llm_context_size): 38 | self.ctx["llm_context_size"] = llm_context_size 39 | 40 | llm_context_sizes = [2048, 4096, 8192, 16384, 32768, 65536, 131072] 41 | for llm_context_size in llm_context_sizes: 42 | check_var = tk.BooleanVar(value=self.ctx["llm_context_size"] == llm_context_size) 43 | self.llm_context_size_menu.add_checkbutton( 44 | label=context_label(llm_context_size), 45 | variable=check_var, 46 | command=lambda v=llm_context_size, _=check_var: set_llm_context_size(v), 47 | ) 48 | 49 | self.menu.add_separator() 50 | 51 | categories = {} 52 | 53 | for llm_name in self.ctx.llm: 54 | llm = self.ctx.llm[llm_name] 55 | 56 | llm_menu = tk.Menu(self.menu, tearoff=False) 57 | name = llm_name 58 | context_size = min(llm["context_size"], self.ctx["llm_context_size"]) // 1024 59 | if "/" in llm_name: 60 | category, name = llm_name.split("/") 61 | if category not in categories: 62 | categories[category] = tk.Menu(self.menu, tearoff=False) 63 | self.menu.add_cascade(label=category, menu=categories[category]) 64 | categories[category].add_cascade(label=f"{name} C{context_size}K", menu=llm_menu) 65 | else: 66 | self.menu.add_cascade(label=f"{llm_name} C{context_size}K", menu=llm_menu) 67 | 68 | for sep_name in self.SEPALATER_NAMES: 69 | if sep_name in name: 70 | self.menu.add_separator() 71 | 72 | llm_path = os.path.join(Path.kobold_cpp, llm["file_name"]) 73 | if not os.path.exists(llm_path): 74 | llm_menu.add_command( 75 | label="ダウンロード(完了まで応答なし、コマンドプロンプトに状況表示)", 76 | command=lambda ln=llm_name: self.ctx.kobold_cpp.download_model(ln), 77 | ) 78 | continue 79 | 80 | max_gpu_layer = llm["max_gpu_layer"] 81 | for gpu_layer in self.ctx["llm_gpu_layers"]: 82 | if gpu_layer > max_gpu_layer: 83 | llm_menu.add_command( 84 | label=f"L{max_gpu_layer}", 85 | command=lambda ln=llm_name, gl=max_gpu_layer: self.select_model(ln, gl), 86 | ) 87 | break 88 | else: 89 | llm_menu.add_command( 90 | label=f"L{gpu_layer}", command=lambda ln=llm_name, gl=gpu_layer: self.select_model(ln, gl) 91 | ) 92 | 93 | def select_model(self, llm_name, gpu_layer): 94 | self.ctx["llm_name"] = llm_name 95 | self.ctx["llm_gpu_layer"] = gpu_layer 96 | result = self.ctx.kobold_cpp.launch_server() 97 | if result is not None: 98 | print(result) 99 | simpledialog.messagebox.showerror("エラー", result, parent=self.form.win) 100 | -------------------------------------------------------------------------------- /EasyNovelAssistant/src/menu/sample_menu.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import tkinter as tk 4 | import urllib.request 5 | import webbrowser 6 | from urllib.parse import quote 7 | 8 | from path import Path 9 | 10 | 11 | class SampleMenu: 12 | URL_TEMPLATE = "https://yyy.wpx.jp/EasyNovelAssistant/sample/{0}" 13 | 14 | def __init__(self, form, ctx): 15 | self.form = form 16 | self.ctx = ctx 17 | 18 | descs = [ 19 | { 20 | "label": "ユーザー", 21 | "mode": "set", 22 | "change_mode": {}, 23 | "path": "user.json", 24 | "splitter_names": [], 25 | }, 26 | { 27 | "label": "特集テーマ", 28 | "mode": "open", 29 | "change_mode": { 30 | "サンプル: ": "set", 31 | "テンプレ: ": "set", 32 | }, 33 | "path": "special.json", 34 | "splitter_names": ["ゴールシークのリポジトリ"], 35 | }, 36 | { 37 | "label": "テンプレート", 38 | "mode": "insert", 39 | "change_mode": {}, 40 | "path": "template.json", 41 | "splitter_names": [], 42 | }, 43 | {"label": "サンプル", "mode": "set", "change_mode": {}, "path": "sample.json", "splitter_names": []}, 44 | { 45 | "label": "NSFW サンプル", 46 | "mode": "set", 47 | "change_mode": {}, 48 | "path": "nsfw.json", 49 | "splitter_names": ["妄想ジェネレーター"], 50 | }, 51 | { 52 | "label": "読み上げサンプル", 53 | "mode": "set", 54 | "change_mode": {}, 55 | "path": "speech.json", 56 | "splitter_names": [], 57 | }, 58 | { 59 | "label": "作例や記事", 60 | "mode": "open", 61 | "change_mode": {}, 62 | "path": "url.json", 63 | "splitter_names": [], 64 | }, 65 | ] 66 | 67 | for desc in descs: 68 | json_path = os.path.join(Path.sample, desc["path"]) 69 | if os.path.exists(json_path): 70 | menu = tk.Menu(form.win, tearoff=False) 71 | form.menu_bar.add_cascade(label=desc["label"], menu=menu) 72 | 73 | menu.configure(postcommand=lambda mn=menu, ds=desc: self.on_menu_open(mn, ds)) 74 | 75 | def on_menu_open(self, menu, desc): 76 | menu.delete(0, tk.END) 77 | 78 | categories = {} 79 | 80 | json_path = os.path.join(Path.sample, desc["path"]) 81 | dic = None 82 | if os.path.exists(json_path): 83 | with open(json_path, "r", encoding="utf-8-sig") as f: 84 | dic = json.load(f) 85 | 86 | # if dic is None: 87 | # menu.add_command(label=f'{desc["label"]} をダウンロード', command=lambda p=desc["path"]: self._download(p)) 88 | # return 89 | 90 | change_mode = desc["change_mode"] 91 | for key in dic: 92 | item = dic[key] 93 | mode = desc["mode"] 94 | if "mode" in item: 95 | mode = item["mode"] 96 | name = key 97 | if "/" in name: 98 | category, name = name.split("/") 99 | for change_name in change_mode: 100 | if change_name in name: 101 | mode = change_mode[change_name] 102 | if category not in categories: 103 | categories[category] = tk.Menu(menu, tearoff=False) 104 | menu.add_cascade(label=category, menu=categories[category]) 105 | categories[category].add_command(label=name, command=lambda i=item, m=mode: self.on_menu_select(i, m)) 106 | else: 107 | for change_name in change_mode: 108 | if change_name in name: 109 | mode = change_mode[change_name] 110 | menu.add_command(label=name, command=lambda i=item, m=mode: self.on_menu_select(i, m)) 111 | 112 | if name in desc["splitter_names"]: 113 | menu.add_separator() 114 | 115 | # def _download(self, path): 116 | # url = self.URL_TEMPLATE.format(path) 117 | # try: 118 | # with urllib.request.urlopen(url) as res: 119 | # data = res.read() 120 | # with open(os.path.join(Path.sample, path), "wb") as f: 121 | # f.write(data) 122 | # return data 123 | # except Exception as e: 124 | # print(e) 125 | # return None 126 | 127 | def on_menu_select(self, item, mode): 128 | if (mode == "set") or (mode == "insert"): 129 | if item.startswith("http"): 130 | url = quote(item, safe=":/?=") 131 | try: 132 | with urllib.request.urlopen(url) as res: 133 | item = res.read().decode("utf-8-sig") 134 | except Exception as e: 135 | webbrowser.open(url) 136 | print(f"{e}. {url}") 137 | return 138 | if ("{char_name}" in item) or ("{user_name}" in item): 139 | item = item.format(char_name=self.ctx["char_name"], user_name=self.ctx["user_name"]) 140 | if mode == "set": 141 | self.ctx.form.input_area.set_text(item) 142 | else: 143 | self.ctx.form.input_area.insert_text(item) 144 | elif mode == "open": 145 | if item.startswith("http"): 146 | url = quote(item, safe=":/?=") 147 | webbrowser.open(url) 148 | else: 149 | print(f"SampleMenu invalid URL: {mode}, {item}") 150 | else: 151 | print(f"SampleMenu unknown mode: {mode}, {item}") 152 | -------------------------------------------------------------------------------- /EasyNovelAssistant/src/menu/setting_menu.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | import tkinter.font as font 3 | from tkinter import simpledialog 4 | 5 | 6 | class SettingMenu: 7 | 8 | def __init__(self, form, ctx): 9 | self.form = form 10 | self.ctx = ctx 11 | 12 | self.menu = tk.Menu(form.win, tearoff=False) 13 | self.form.menu_bar.add_cascade(label="設定", menu=self.menu) 14 | self.menu.configure(postcommand=self._on_menu_open) 15 | 16 | def _on_menu_open(self): 17 | self.menu.delete(0, tk.END) 18 | 19 | cmd = lambda: self._set_name("char_name", "キャラ名") 20 | self.menu.add_command(label=f'キャラ名: {self.ctx["char_name"]}', command=cmd) 21 | 22 | cmd = lambda: self._set_name("user_name", "ユーザー名") 23 | self.menu.add_command(label=f'ユーザー名: {self.ctx["user_name"]}', command=cmd) 24 | 25 | self.menu.add_separator() 26 | 27 | def _set_font(f): 28 | self.ctx["text_area_font"] = f 29 | self.form.input_area.apply_text_setting() 30 | self.form.gen_area.apply_text_setting() 31 | self.form.output_area.apply_text_setting() 32 | 33 | font_menu = tk.Menu(self.menu, tearoff=False) 34 | 35 | self.menu.add_cascade(label=f'フォント(↑↓キー利用可): {self.ctx["text_area_font"]}', menu=font_menu) 36 | for font_family in font.families(): 37 | check_var = tk.BooleanVar(value=self.ctx["text_area_font"] == font_family) 38 | 39 | font_menu.add_checkbutton( 40 | label=font_family, variable=check_var, command=lambda f=font_family, _=check_var: _set_font(f) 41 | ) 42 | 43 | def _set_font_size(fons_size): 44 | self.ctx["text_area_font_size"] = fons_size 45 | self.form.input_area.apply_text_setting() 46 | self.form.gen_area.apply_text_setting() 47 | self.form.output_area.apply_text_setting() 48 | 49 | font_size_menu = tk.Menu(self.menu, tearoff=False) 50 | self.menu.add_cascade(label=f'フォントサイズ: {self.ctx["text_area_font_size"]}', menu=font_size_menu) 51 | 52 | font_sizes = [8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72] 53 | for font_size in font_sizes: 54 | check_var = tk.BooleanVar(value=self.ctx["text_area_font_size"] == font_size) 55 | 56 | font_size_menu.add_checkbutton( 57 | label=font_size, variable=check_var, command=lambda fs=font_size, _=check_var: _set_font_size(fs) 58 | ) 59 | 60 | self.menu.add_separator() 61 | 62 | def _set_invert_color(): 63 | fg = self.ctx["foreground_color"] 64 | self.ctx["foreground_color"] = self.ctx["select_background_color"] 65 | self.ctx["select_background_color"] = fg 66 | sel_fg = self.ctx["select_foreground_color"] 67 | self.ctx["select_foreground_color"] = self.ctx["background_color"] 68 | self.ctx["background_color"] = sel_fg 69 | self.form.input_area.apply_text_setting() 70 | self.form.gen_area.apply_text_setting() 71 | self.form.output_area.apply_text_setting() 72 | 73 | self.menu.add_command(label="テーマカラーの反転", command=_set_invert_color) 74 | 75 | def _set_name(self, who, who_label): 76 | name = self.ctx[who] 77 | 78 | name = simpledialog.askstring( 79 | f"{who_label}設定", 80 | f"サンプルなどで使用する{who_label}を入力してください。", 81 | initialvalue=name, 82 | parent=self.form.win, 83 | ) 84 | if name is None: 85 | return 86 | elif name != "": 87 | self.ctx[who] = name 88 | -------------------------------------------------------------------------------- /EasyNovelAssistant/src/menu/speech_menu.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tkinter as tk 3 | 4 | from path import Path 5 | 6 | 7 | class SpeechMenu: 8 | 9 | def __init__(self, form, ctx): 10 | self.form = form 11 | self.ctx = ctx 12 | 13 | self.menu = tk.Menu(form.win, tearoff=False) 14 | self.form.menu_bar.add_cascade(label="読み上げ", menu=self.menu) 15 | self.menu.configure(postcommand=self.on_menu_open) 16 | 17 | def on_menu_open(self): 18 | self.menu.delete(0, tk.END) 19 | 20 | # 入力欄をすべて読み上げる 21 | def speech_all(): 22 | text = self.ctx.form.input_area.get_comment_removed_text() 23 | lines = text.splitlines() 24 | for line in lines: 25 | self.ctx.style_bert_vits2.generate(line, force=True) 26 | 27 | self.menu.add_command(label="入力欄を読み上げ", command=speech_all) 28 | 29 | self.menu.add_command(label="読み上げを中断 (F5)", command=self.ctx.style_bert_vits2.abort) 30 | 31 | self.menu.add_separator() 32 | 33 | models = self.ctx.style_bert_vits2.models 34 | if models is None: 35 | models = self.ctx.style_bert_vits2.get_models() 36 | 37 | if models is None: 38 | if not os.path.exists(Path.style_bert_vits2): 39 | self.menu.add_command( 40 | label="Style-Bert-VITS2 をインストール", command=self.ctx.style_bert_vits2.install 41 | ) 42 | elif not os.path.exists(Path.style_bert_vits2_config): 43 | self.menu.add_command(label="Style-Bert-VITS2 のインストール中") 44 | else: 45 | self.menu.add_command( 46 | label="読み上げサーバーを立ち上げる", command=self.ctx.style_bert_vits2.launch_server 47 | ) 48 | 49 | def set_style_bert_vits2_gpu(*args): 50 | self.ctx["style_bert_vits2_gpu"] = self.gpu_var.get() 51 | 52 | self.gpu_var = tk.BooleanVar(value=self.ctx["style_bert_vits2_gpu"]) 53 | self.gpu_var.trace_add("write", set_style_bert_vits2_gpu) 54 | self.menu.add_checkbutton(label="GPU を使用する", variable=self.gpu_var) 55 | return 56 | 57 | def set_middle_click(*args): 58 | self.ctx["middle_click_speech"] = self.middle_click_var.get() 59 | 60 | self.middle_click_var = tk.BooleanVar(value=self.ctx["middle_click_speech"]) 61 | self.middle_click_var.trace_add("write", set_middle_click) 62 | self.menu.add_checkbutton(label="中クリック読み上げ", variable=self.middle_click_var) 63 | 64 | def set_auto_speech_char(*args): 65 | self.ctx["auto_speech_char"] = self.auto_speech_char_var.get() 66 | 67 | self.auto_speech_char_var = tk.BooleanVar(value=self.ctx["auto_speech_char"]) 68 | self.auto_speech_char_var.trace_add("write", set_auto_speech_char) 69 | self.menu.add_checkbutton(label=f'{self.ctx["char_name"]} 自動読み上げ', variable=self.auto_speech_char_var) 70 | 71 | def set_auto_speech_user(*args): 72 | self.ctx["auto_speech_user"] = self.auto_speech_user_var.get() 73 | 74 | self.auto_speech_user_var = tk.BooleanVar(value=self.ctx["auto_speech_user"]) 75 | self.auto_speech_user_var.trace_add("write", set_auto_speech_user) 76 | self.menu.add_checkbutton(label=f'{self.ctx["user_name"]} 自動読み上げ', variable=self.auto_speech_user_var) 77 | 78 | def set_auto_speech_other(*args): 79 | self.ctx["auto_speech_other"] = self.auto_speech_other_var.get() 80 | 81 | self.auto_speech_other_var = tk.BooleanVar(value=self.ctx["auto_speech_other"]) 82 | self.auto_speech_other_var.trace_add("write", set_auto_speech_other) 83 | self.menu.add_checkbutton(label="その他 自動読み上げ", variable=self.auto_speech_other_var) 84 | 85 | self.menu.add_separator() 86 | 87 | self.volume_menu = tk.Menu(self.menu, tearoff=False) 88 | self.menu.add_cascade(label=f'読み上げ音量: {self.ctx["speech_volume"]}%', menu=self.volume_menu) 89 | 90 | def set_speech_volume(volume): 91 | self.ctx["speech_volume"] = volume 92 | 93 | volumes = [100, 90, 80, 70, 60, 50, 40, 30, 20, 10, 0] 94 | for volume in volumes: 95 | check_var = tk.BooleanVar(value=self.ctx["speech_volume"] == volume) 96 | self.volume_menu.add_checkbutton( 97 | label=f"{volume}%", variable=check_var, command=lambda v=volume, _=check_var: set_speech_volume(v) 98 | ) 99 | 100 | self.speed_menu = tk.Menu(self.menu, tearoff=False) 101 | self.menu.add_cascade(label=f'読み上げ速度: {self.ctx["speech_speed"]}倍', menu=self.speed_menu) 102 | 103 | def set_speech_speed(speed): 104 | self.ctx["speech_speed"] = speed 105 | 106 | speeds = [2.0, 1.75, 1.5, 1.25, 1.0, 0.75, 0.5] 107 | for speed in speeds: 108 | check_var = tk.BooleanVar(value=self.ctx["speech_speed"] == speed) 109 | self.speed_menu.add_checkbutton( 110 | label=f"{speed}倍", variable=check_var, command=lambda s=speed, _=check_var: set_speech_speed(s) 111 | ) 112 | 113 | self.interval_menu = tk.Menu(self.menu, tearoff=False) 114 | self.menu.add_cascade(label=f'読み上げ間隔: {self.ctx["speech_interval"]}秒', menu=self.interval_menu) 115 | 116 | def set_speech_interval(interval): 117 | self.ctx["speech_interval"] = interval 118 | 119 | intervals = [3.0, 2.0, 1.5, 1.0, 0.8, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1, 0.0] 120 | for interval in intervals: 121 | check_var = tk.BooleanVar(value=self.ctx["speech_interval"] == interval) 122 | self.interval_menu.add_checkbutton( 123 | label=f"{interval}秒", 124 | variable=check_var, 125 | command=lambda i=interval, _=check_var: set_speech_interval(i), 126 | ) 127 | 128 | self.menu.add_separator() 129 | 130 | def set_char_voice(voice_name): 131 | self.ctx["char_voice"] = voice_name 132 | 133 | self.char_voice_menu = tk.Menu(self.menu, tearoff=False) 134 | self.menu.add_cascade( 135 | label=f'{self.ctx["char_name"]} の声: {self.ctx["char_voice"]}', menu=self.char_voice_menu 136 | ) 137 | 138 | for voice_name in models: 139 | check_var = tk.BooleanVar(value=self.ctx["char_voice"] == voice_name) 140 | self.char_voice_menu.add_checkbutton( 141 | label=voice_name, variable=check_var, command=lambda vn=voice_name, _=check_var: set_char_voice(vn) 142 | ) 143 | 144 | def set_user_voice(voice_name): 145 | self.ctx["user_voice"] = voice_name 146 | 147 | self.user_voice_menu = tk.Menu(self.menu, tearoff=False) 148 | self.menu.add_cascade( 149 | label=f'{self.ctx["user_name"]} の声: {self.ctx["user_voice"]}', menu=self.user_voice_menu 150 | ) 151 | 152 | for voice_name in models: 153 | check_var = tk.BooleanVar(value=self.ctx["user_voice"] == voice_name) 154 | self.user_voice_menu.add_checkbutton( 155 | label=voice_name, variable=check_var, command=lambda vn=voice_name, _=check_var: set_user_voice(vn) 156 | ) 157 | 158 | def set_other_voice(voice_name): 159 | self.ctx["other_voice"] = voice_name 160 | 161 | self.other_voice_menu = tk.Menu(self.menu, tearoff=False) 162 | self.menu.add_cascade(label=f'その他の声: {self.ctx["other_voice"]}', menu=self.other_voice_menu) 163 | 164 | for voice_name in models: 165 | check_var = tk.BooleanVar(value=self.ctx["other_voice"] == voice_name) 166 | self.other_voice_menu.add_checkbutton( 167 | label=voice_name, variable=check_var, command=lambda vn=voice_name, _=check_var: set_other_voice(vn) 168 | ) 169 | -------------------------------------------------------------------------------- /EasyNovelAssistant/src/menu/tool_menu.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import tkinter as tk 4 | import webbrowser 5 | from sys import platform 6 | 7 | from path import Path 8 | 9 | 10 | class ToolMenu: 11 | 12 | def __init__(self, form, ctx): 13 | self.form = form 14 | self.ctx = ctx 15 | 16 | self.menu = tk.Menu(form.win, tearoff=False) 17 | self.form.menu_bar.add_cascade(label="ツール", menu=self.menu) 18 | self.menu.configure(postcommand=self._on_menu_open) 19 | 20 | def _on_menu_open(self): 21 | self.menu.delete(0, tk.END) 22 | 23 | self.menu.add_command(label="(New!) 動画の作成", command=self.ctx.movie_maker.make) 24 | 25 | def set_subtitles(*args): 26 | self.ctx["mov_subtitles"] = self.subtitles_var.get() 27 | 28 | self.subtitles_var = tk.BooleanVar(value=self.ctx["mov_subtitles"]) 29 | self.subtitles_var.trace_add("write", set_subtitles) 30 | self.menu.add_checkbutton(label="動画に字幕を追加", variable=self.subtitles_var) 31 | 32 | def set_resize(resize): 33 | self.ctx["mov_resize"] = resize 34 | 35 | self.resize_menu = tk.Menu(self.menu, tearoff=False) 36 | self.menu.add_cascade(label=f'動画の長辺リサイズ: {self.ctx["mov_resize"]}px', menu=self.resize_menu) 37 | 38 | for resize in [1920, 1900, 1600, 1440, 1200, 1024, 0]: 39 | check_var = tk.BooleanVar(value=self.ctx["mov_resize"] == resize) 40 | cmd = lambda rs=resize, _=check_var: set_resize(rs) 41 | self.resize_menu.add_checkbutton(label=f"{resize}px", variable=check_var, command=cmd) 42 | 43 | def set_crf(crf): 44 | self.ctx["mov_crf"] = crf 45 | 46 | self.crf_menu = tk.Menu(self.menu, tearoff=False) 47 | self.menu.add_cascade(label=f'動画の品質: CRF {self.ctx["mov_crf"]}', menu=self.crf_menu) 48 | 49 | for crf in [38, 32, 26, 20]: 50 | check_var = tk.BooleanVar(value=self.ctx["mov_crf"] == crf) 51 | cmd = lambda cr=crf, _=check_var: set_crf(cr) 52 | self.crf_menu.add_checkbutton(label=f"CRF {crf}", variable=check_var, command=cmd) 53 | 54 | def set_volume_adjust(*args): 55 | self.ctx["mov_volume_adjust"] = self.volume_adjust_var.get() 56 | 57 | self.volume_adjust_var = tk.BooleanVar(value=self.ctx["mov_volume_adjust"]) 58 | self.volume_adjust_var.trace_add("write", set_volume_adjust) 59 | self.menu.add_checkbutton(label="動画の音量を 読み上げ音量 で調整", variable=self.volume_adjust_var) 60 | 61 | def set_tempo_adjust(*args): 62 | self.ctx["mov_tempo_adjust"] = self.tempo_adjust_var.get() 63 | 64 | self.tempo_adjust_var = tk.BooleanVar(value=self.ctx["mov_tempo_adjust"]) 65 | self.tempo_adjust_var.trace_add("write", set_tempo_adjust) 66 | self.menu.add_checkbutton(label="動画の速度を 読み上げ速度 で調整", variable=self.tempo_adjust_var) 67 | 68 | self.menu.add_separator() 69 | 70 | self.menu.add_command(label="KoboldCpp", command=self._run_kobold_cpp) 71 | 72 | if os.path.exists(Path.style_bert_vits2_config): 73 | self.menu.add_separator() 74 | self.menu.add_command( 75 | label="Style-Bert-VITS2 音声生成とモデル学習", 76 | command=lambda: self._run_style_bert_vits2(Path.style_bert_vits2_app, "app.py"), 77 | ) 78 | self.menu.add_command( 79 | label="Style-Bert-VITS2 音声エディタ", 80 | command=lambda: self._run_style_bert_vits2( 81 | Path.style_bert_vits2_editor, "server_editor.py --inbrowser" 82 | ), 83 | ) 84 | 85 | self.menu.add_separator() 86 | url = "https://booth.pm/ja/search/Style-Bert-VITS2" 87 | self.menu.add_command(label="BOOTH (Style-Bert-VITS2)", command=lambda: webbrowser.open(url)) 88 | url = "https://booth.pm/ja/items/5511738" 89 | self.menu.add_command(label="黄琴まひろ (V3-JP-T)", command=lambda: webbrowser.open(url)) 90 | url = "https://booth.pm/ja/items/5566669" 91 | self.menu.add_command(label="女子大生音声モデル", command=lambda: webbrowser.open(url)) 92 | url = "https://huggingface.co/ayousanz/tsukuyomi-chan-style-bert-vits2-model" 93 | self.menu.add_command(label="つくよみちゃん 音声モデル", command=lambda: webbrowser.open(url)) 94 | 95 | def _run_kobold_cpp(self, *args): 96 | if platform == "win32": 97 | command = ["start", "cmd", "/c"] 98 | command.append(f"{Path.kobold_cpp_win} || pause") 99 | subprocess.run(command, cwd=Path.kobold_cpp, shell=True) 100 | else: 101 | subprocess.Popen(f"{Path.kobold_cpp_linux}", cwd=Path.kobold_cpp, shell=True) 102 | 103 | def _run_style_bert_vits2(self, bat, py): 104 | if platform == "win32": 105 | subprocess.run(["start", "cmd", "/c", f"{bat} || pause"], cwd=Path.style_bert_vits2, shell=True) 106 | else: 107 | python = os.path.join(Path.style_bert_vits2, "venv", "Scripts", "python") 108 | subprocess.Popen(f"{python} {py}", cwd=Path.style_bert_vits2, shell=True) 109 | -------------------------------------------------------------------------------- /EasyNovelAssistant/src/movie_maker.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import subprocess 4 | import time 5 | from tkinter import filedialog 6 | 7 | from path import Path 8 | 9 | 10 | class MovieMaker: 11 | _SERIF_REGEX = re.compile(r"^[\d_]*-(.+)") 12 | 13 | def __init__(self, ctx): 14 | self.ctx = ctx 15 | self.audio_dir = None 16 | self.image_dir = "" 17 | 18 | def make(self): 19 | audio_image_sets = self._select_audio_image_sets() 20 | if len(audio_image_sets) == 0: 21 | return False 22 | 23 | movie_path = self._select_movie_path() 24 | if movie_path is None: 25 | return False 26 | 27 | bat_path = self._prepare(audio_image_sets, movie_path) 28 | if bat_path is None: 29 | return False 30 | 31 | subprocess.run(["start", "cmd", "/c", f"{bat_path} || pause"], shell=True, cwd=os.path.dirname(bat_path)) 32 | return True 33 | 34 | def _select_audio_image_sets(self): 35 | win = self.ctx.form.win 36 | result = [] 37 | 38 | if self.audio_dir is None: 39 | if os.path.exists(Path.daily_speech): 40 | self.audio_dir = Path.daily_speech 41 | elif os.path.exists(Path.speech): 42 | self.audio_dir = Path.speech 43 | else: 44 | self.audio_dir = Path.cwd 45 | 46 | image_dir = self.ctx["mov_image_dir"] 47 | if image_dir == "": 48 | image_dir = Path.cwd 49 | 50 | while True: 51 | title = "動画にする音声ファイルを選択します。" 52 | if len(result) > 0: 53 | title += " [キャンセル] で選択を終了します。" 54 | audio_path = filedialog.askopenfilename( 55 | title=title, 56 | filetypes=[("音声ファイル", "*.wav")], 57 | initialdir=self.audio_dir, 58 | parent=win, 59 | ) 60 | if audio_path == "": 61 | break 62 | self.audio_dir = os.path.dirname(audio_path) 63 | audio_name = os.path.basename(audio_path).split(".")[0] 64 | 65 | image_path = filedialog.askopenfilename( 66 | title=f"{audio_name} の再生中に表示する画像ファイルを選択します。", 67 | filetypes=[("画像ファイル", "*.png *.webp *.jpg *.jpeg")], 68 | initialdir=image_dir, 69 | parent=win, 70 | ) 71 | if image_path == "": 72 | break 73 | image_dir = os.path.dirname(image_path) 74 | self.ctx["mov_image_dir"] = image_dir 75 | 76 | result.append({"image_path": image_path, "audio_path": audio_path}) 77 | 78 | return result 79 | 80 | def _select_movie_path(self): 81 | movie_dir = self.ctx["mov_movie_dir"] 82 | if movie_dir == "": 83 | movie_dir = Path.movie 84 | 85 | YYYYMMDD_HHMMSS = time.strftime("%Y%m%d_%H%M%S", time.localtime()) 86 | movie_path = filedialog.asksaveasfilename( 87 | title="動画ファイルを保存", 88 | filetypes=[("動画ファイル", "*.mp4")], 89 | initialdir=movie_dir, 90 | initialfile=f"{YYYYMMDD_HHMMSS}.mp4", 91 | parent=self.ctx.form.win, 92 | ) 93 | if movie_path == "": 94 | return None 95 | if not movie_path.endswith(".mp4"): 96 | movie_path += ".mp4" 97 | self.ctx["mov_movie_dir"] = os.path.dirname(movie_path) 98 | return movie_path 99 | 100 | def _prepare(self, audio_image_sets, movie_path): 101 | movie_name = os.path.basename(movie_path).split(".")[0] 102 | assets_dir = os.path.join(os.path.dirname(movie_path), movie_name) 103 | os.makedirs(assets_dir, exist_ok=True) 104 | 105 | bat = """@echo off 106 | chcp 65001 > NUL 107 | pushd %~dp0 108 | set FFMPEG={ffmpeg} 109 | set FFPLAY={ffplay} 110 | 111 | """.format( 112 | ffmpeg=Path.ffmpeg, ffplay=Path.ffplay 113 | ) 114 | 115 | subtitle_template = """1 116 | 00:00:00,000 --> 90:00:00,000 117 | {serif} 118 | """ 119 | part_paths = [] 120 | for i, audio_image_set in enumerate(audio_image_sets): 121 | audio_path = audio_image_set["audio_path"] 122 | image_path = audio_image_set["image_path"] 123 | audio_name, _ext = os.path.splitext(os.path.basename(audio_path)) 124 | serif = audio_name 125 | m = self._SERIF_REGEX.match(audio_name) 126 | if m is not None: 127 | serif = m.group(1) 128 | subtitle = subtitle_template.format(serif=serif) 129 | subtitle_path = os.path.join(assets_dir, f"{audio_name}.srt") 130 | with open(subtitle_path, "w", encoding="utf-8-sig") as f: 131 | f.write(subtitle) 132 | 133 | part_path = os.path.join(assets_dir, f"{audio_name}.mp4") 134 | part_paths.append(part_path) 135 | 136 | vf = "" 137 | mov_resize = self.ctx["mov_resize"] 138 | mov_subtitles = self.ctx["mov_subtitles"] 139 | if mov_resize > 0 or mov_subtitles: 140 | vf = '\n-vf "' 141 | if mov_resize > 0: 142 | vf += f"scale='if(gt(a,1),{mov_resize},-2)':'if(gt(a,1),-2,{mov_resize})'" 143 | if mov_subtitles: 144 | vf += ", " 145 | if mov_subtitles: 146 | vf += f"subtitles='{os.path.basename(subtitle_path)}'" 147 | vf += '" ^' 148 | 149 | af = "" 150 | volume_adjust = self.ctx["mov_volume_adjust"] 151 | tempo_adjust = self.ctx["mov_tempo_adjust"] 152 | if volume_adjust or tempo_adjust: 153 | af = '\n-af "' 154 | if volume_adjust: 155 | af += f"volume={self.ctx['speech_volume'] / 100}" 156 | if tempo_adjust: 157 | af += ", " 158 | if tempo_adjust: 159 | af += f"atempo={self.ctx['speech_speed']}" 160 | af += '" ^' 161 | 162 | bat += """echo "{i}: {serif}" 163 | %FFMPEG% -y -loglevel error ^ 164 | -i "{audio_path}" ^ 165 | -loop 1 -i "{image_path}" ^ 166 | -vcodec libx264 ^ 167 | -pix_fmt yuv420p ^ 168 | -acodec aac ^ 169 | -ab 128k ^ 170 | -ac 1 ^ 171 | -ar 44100 ^ 172 | -shortest ^{vf}{af} 173 | -crf {crf} ^ 174 | "{part_path}" 175 | if %errorlevel% neq 0 ( pause & popd & exit /b 1) 176 | 177 | """.format( 178 | i=i, 179 | serif=serif, 180 | audio_path=audio_path, 181 | image_path=image_path, 182 | vf=vf, 183 | af=af, 184 | crf=self.ctx["mov_crf"], 185 | part_path=part_path, 186 | ) 187 | 188 | file_list_path = os.path.join(assets_dir, f"{movie_name}.txt") 189 | with open(file_list_path, "w", encoding="utf-8") as f: 190 | for part_path in part_paths: 191 | f.write(f"file '{part_path}'\n") 192 | 193 | bat += """%FFMPEG% -y -loglevel error ^ 194 | -f concat ^ 195 | -safe 0 ^ 196 | -i "{file_list_path}" ^ 197 | -c copy ^ 198 | "{movie_path}" 199 | if %errorlevel% neq 0 ( pause & popd & exit /b 1) 200 | 201 | %FFPLAY% -loglevel error -autoexit -loop 3 "{movie_path}" 202 | if %errorlevel% neq 0 ( pause & popd & exit /b 1) 203 | 204 | popd 205 | """.format( 206 | file_list_path=file_list_path, movie_path=movie_path 207 | ) 208 | 209 | bat_path = os.path.join(assets_dir, f"{movie_name}.bat") 210 | with open(bat_path, "w", encoding="utf-8") as f: 211 | f.write(bat) 212 | return bat_path 213 | -------------------------------------------------------------------------------- /EasyNovelAssistant/src/output_area.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import scrolledtext 3 | 4 | from const import Const 5 | from path import Path 6 | 7 | 8 | class OutputArea: 9 | def __init__(self, parent, ctx): 10 | self.ctx = ctx 11 | self.text_area = scrolledtext.ScrolledText(parent, undo=True, maxundo=-1) 12 | self.text_area.configure(Const.TEXT_AREA_CONFIG) 13 | self.text_area.pack(fill=tk.BOTH, expand=True) 14 | self.apply_text_setting() 15 | parent.add(self.text_area, minsize=Const.AREA_MIN_SIZE, stretch="always") 16 | 17 | self.ctx_menu = tk.Menu(self.text_area, tearoff=False) 18 | self.text_area.bind("", self._on_ctx_menu) 19 | 20 | self.text_area.bind("", self._on_middle_click) 21 | 22 | self.counter = 0 23 | 24 | def apply_text_setting(self): 25 | self.text_area.configure(font=(self.ctx["text_area_font"], self.ctx["text_area_font_size"])) 26 | colors = { 27 | "fg": self.ctx["foreground_color"], 28 | "bg": self.ctx["background_color"], 29 | "selectforeground": self.ctx["select_foreground_color"], 30 | "insertbackground": self.ctx["select_foreground_color"], 31 | "selectbackground": self.ctx["select_background_color"], 32 | } 33 | self.text_area.configure(colors) 34 | 35 | def append_text(self, text): 36 | self.text_area.insert(tk.END, text) 37 | if self.ctx["auto_scroll"]: 38 | self.text_area.see(tk.END) 39 | 40 | def append_output(self, output): 41 | if output is None: 42 | return 43 | output = self.ctx["output_format"].format(self.counter, output) 44 | self.counter += 1 45 | self.append_text(output) 46 | 47 | with open(Path.output_log, "a", encoding="utf-8-sig") as f: 48 | f.write(output) 49 | 50 | def clear(self): 51 | self.text_area.delete("1.0", tk.END) 52 | self.counter = 0 53 | 54 | def _speech(self, e): 55 | line_num = self.text_area.index(f"@{e.x},{e.y}").split(".")[0] 56 | text = self.text_area.get(f"{line_num}.0", f"{line_num}.end") + "\n" 57 | self.ctx.style_bert_vits2.generate(text) 58 | 59 | def _send_to_input(self, e): 60 | text = None 61 | if self.text_area.tag_ranges(tk.SEL): 62 | text = self.text_area.get(tk.SEL_FIRST, tk.SEL_LAST) 63 | else: 64 | line_num = self.text_area.index(f"@{e.x},{e.y}").split(".")[0] 65 | text = self.text_area.get(f"{line_num}.0", f"{line_num}.end") + "\n" 66 | self.ctx.form.input_area.insert_text(text) 67 | 68 | def _on_middle_click(self, e): 69 | if self.ctx["middle_click_speech"]: 70 | self._speech(e) 71 | else: 72 | self._send_to_input(e) 73 | return "break" 74 | 75 | def _on_ctx_menu(self, event): 76 | self.ctx_menu.delete(0, tk.END) 77 | 78 | if self.ctx.style_bert_vits2.models is None: 79 | self.ctx.style_bert_vits2.get_models() 80 | 81 | if self.ctx.style_bert_vits2.models is not None: 82 | self.ctx_menu.add_command(label="読み上げる", command=lambda: self._speech(event)) 83 | 84 | self.ctx_menu.add_command(label="入力欄に送る", command=lambda: self._send_to_input(event)) 85 | 86 | self.ctx_menu.add_separator() 87 | 88 | self.ctx_menu.add_command(label="クリア", command=self.clear) 89 | 90 | self.text_area.mark_set(tk.INSERT, f"@{event.x},{event.y}") 91 | self.ctx_menu.post(event.x_root, event.y_root) 92 | -------------------------------------------------------------------------------- /EasyNovelAssistant/src/path.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import time 4 | 5 | 6 | class Path: 7 | path_regex = re.compile(r'[\n\r<>:"/\\|?* ]') 8 | 9 | cwd = os.getcwd() 10 | config = os.path.join(cwd, "config.json") 11 | llm = os.path.join(cwd, "llm.json") 12 | llm_sequence = os.path.join(cwd, "llm_sequence.json") 13 | 14 | app = os.path.join(cwd, "EasyNovelAssistant") 15 | setup = os.path.join(app, "setup") 16 | res = os.path.join(setup, "res") 17 | default_config = os.path.join(res, "default_config.json") 18 | default_llm = os.path.join(res, "default_llm.json") 19 | default_llm_sequence = os.path.join(res, "default_llm_sequence.json") 20 | 21 | kobold_cpp = os.path.join(cwd, "KoboldCpp") 22 | kobold_cpp_win = os.path.join(kobold_cpp, "koboldcpp.exe") 23 | kobold_cpp_linux = os.path.join(kobold_cpp, "koboldcpp-linux-x64-cuda1150") 24 | 25 | style_bert_vits2 = os.path.join(cwd, "Style-Bert-VITS2") 26 | style_bert_vits2_config = os.path.join(style_bert_vits2, "config.yml") 27 | style_bert_vits2_setup = os.path.join(setup, "Setup-Style-Bert-VITS2.bat") 28 | style_bert_vits2_run = os.path.join(setup, "Run-Style-Bert-VITS2.bat") 29 | style_bert_vits2_app = os.path.join(style_bert_vits2, "App.bat") 30 | style_bert_vits2_editor = os.path.join(style_bert_vits2, "Editor.bat") 31 | 32 | sample = os.path.join(cwd, "sample") 33 | 34 | YYYYMMDD = time.strftime("%Y%m%d", time.localtime()) 35 | 36 | log = os.path.join(cwd, "log") 37 | daily_log = os.path.join(log, YYYYMMDD) 38 | os.makedirs(daily_log, exist_ok=True) 39 | 40 | YYYYMMDD_HHMMSS = time.strftime("%Y%m%d_%H%M%S", time.localtime()) 41 | generate_log = os.path.join(daily_log, f"{YYYYMMDD_HHMMSS}-generate.txt") 42 | output_log = os.path.join(daily_log, f"{YYYYMMDD_HHMMSS}-output.txt") 43 | 44 | speech = os.path.join(cwd, "speech") 45 | daily_speech = os.path.join(speech, YYYYMMDD) 46 | 47 | movie = os.path.join(cwd, "movie") 48 | os.makedirs(movie, exist_ok=True) 49 | venv = os.path.join(cwd, "venv") 50 | scripts = os.path.join(venv, "Scripts") 51 | ffmpeg = os.path.join(scripts, "ffmpeg.exe") 52 | ffplay = os.path.join(scripts, "ffplay.exe") 53 | 54 | @classmethod 55 | def init(cls, ctx): 56 | pass 57 | 58 | @classmethod 59 | def get_path_name(cls, name): 60 | return cls.path_regex.sub("_", name.strip()).replace("___", "_").replace("__", "_") 61 | -------------------------------------------------------------------------------- /EasyNovelAssistant/src/style_bert_vits2.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import subprocess 4 | import time 5 | from sys import platform 6 | 7 | import numpy as np 8 | import requests 9 | from job_queue import JobQueue 10 | from path import Path 11 | from scipy.io import wavfile 12 | 13 | 14 | class StyleBertVits2: 15 | def __init__(self, ctx): 16 | self.ctx = ctx 17 | self.base_url = f'http://{ctx["style_bert_vits2_host"]}:{ctx["style_bert_vits2_port"]}' 18 | self.models_url = f"{self.base_url}/models/info" 19 | self.voice_url = f"{self.base_url}/voice" 20 | 21 | self.models = None 22 | self.gen_queue = JobQueue() 23 | self.play_queue = JobQueue() 24 | 25 | def install(self): 26 | if platform == "win32": 27 | self._run_bat(Path.style_bert_vits2_setup, "Style-Bert-VITS2 インストール") 28 | else: 29 | msg = f"{Path.style_bert_vits2} に Style-Bert-VITS2 をインストールして、" 30 | print(msg + "EasyNovelAssistant/setup/res/config.yml をインストール先にコピーしてください。") 31 | 32 | def launch_server(self): 33 | self._run_bat(Path.style_bert_vits2_run, "Style-Bert-VITS2 読み上げサーバー") 34 | 35 | def _run_bat(self, command, title): 36 | arg = "" if self.ctx["style_bert_vits2_gpu"] else " --cpu" 37 | if platform == "win32": 38 | subprocess.run(["start", title, "cmd", "/c", f"{command}{arg} || pause"], shell=True) 39 | else: 40 | python = os.path.join(Path.style_bert_vits2, "venv", "Scripts", "python") 41 | subprocess.Popen(f"{python} server_fastapi.py{arg}", cwd=Path.style_bert_vits2, shell=True) 42 | 43 | def get_models(self): 44 | try: 45 | response = requests.get(self.models_url, timeout=self.ctx["style_bert_vits2_command_timeout"]) 46 | if response.status_code == 200: 47 | data = response.json() 48 | models = None 49 | for key in data: 50 | model_id = int(key) 51 | model = data[key] 52 | model_name = model["id2spk"]["0"] 53 | model_style = "Neutral" 54 | if not model_style in model["style2id"]: 55 | model_style = list(model["style2id"].keys())[0] 56 | if models is None: 57 | models = {} 58 | models[model_name] = {"id": model_id, "style": model_style} 59 | self.models = models 60 | return self.models 61 | except Exception as e: 62 | pass 63 | self.models = None 64 | return self.models 65 | 66 | def update(self): 67 | self.gen_queue.update() 68 | self.play_queue.update() 69 | 70 | def abort(self): 71 | self.gen_queue.cancel_all() 72 | self.play_queue.cancel_all() 73 | 74 | def generate(self, text, force=False): 75 | max_speech_queue = self.ctx["max_speech_queue"] 76 | 77 | if not force: 78 | if (self.gen_queue.len() > max_speech_queue) or (self.play_queue.len() > max_speech_queue): 79 | print(f"[Info] 混み合っているので読み上げをキャンセルしました。: {text}") 80 | return False 81 | self.gen_queue.push(self._generate, text=text) 82 | return True 83 | 84 | def _generate(self, text): 85 | models = self.get_models() 86 | if models is None: 87 | return None 88 | 89 | model_id = 0 90 | model_style = "Neutral" 91 | if "「" in text: 92 | name, msg = text.split("「", 1) 93 | if msg.endswith("」"): 94 | msg = msg[:-1] 95 | if self.ctx["char_name"] in name: 96 | if self.ctx["char_voice"] in self.models: 97 | model_id = self.models[self.ctx["char_voice"]]["id"] 98 | model_style = self.models[self.ctx["char_voice"]]["style"] 99 | text = msg 100 | elif self.ctx["user_name"] in name: 101 | if self.ctx["user_voice"] in self.models: 102 | model_id = self.models[self.ctx["user_voice"]]["id"] 103 | model_style = self.models[self.ctx["user_voice"]]["style"] 104 | text = msg 105 | else: 106 | if self.ctx["other_voice"] in self.models: 107 | model_id = self.models[self.ctx["other_voice"]]["id"] 108 | model_style = self.models[self.ctx["other_voice"]]["style"] 109 | else: 110 | if self.ctx["other_voice"] in self.models: 111 | model_id = self.models[self.ctx["other_voice"]]["id"] 112 | model_style = self.models[self.ctx["other_voice"]]["style"] 113 | 114 | params = {"text": text, "model_id": model_id, "split_interval": 0.2, "style": model_style} 115 | 116 | try: 117 | start_time = time.perf_counter() 118 | response = requests.post(self.voice_url, params=params, headers={"accept": "audio/wav"}) 119 | if response.status_code == 200: 120 | os.makedirs(Path.daily_speech, exist_ok=True) 121 | YYYYMMDD_HHMMSS = time.strftime("%Y%m%d_%H%M%S", time.localtime()) 122 | wav_path = os.path.join(Path.daily_speech, f"{YYYYMMDD_HHMMSS}-{Path.get_path_name(text[:128])}.wav") 123 | with open(wav_path, "wb") as f: 124 | f.write(response.content) 125 | 126 | # 無音の付与 127 | sample_rate, data = wavfile.read(wav_path) 128 | silence = np.zeros(int(sample_rate * self.ctx["speech_interval"])) 129 | data_with_silence = np.append(data, silence) 130 | wavfile.write(wav_path, sample_rate, data_with_silence.astype(np.int16)) 131 | 132 | self.play_queue.push(self._play, wav_path=wav_path) 133 | print(f"読み上げ {time.perf_counter() - start_time:.2f}秒: {text}") 134 | return True 135 | print(f"[失敗] StyleBertVits2.generate(): {response.text}") 136 | except Exception as e: 137 | print(f"[例外] StyleBertVits2.generate(): {e}") 138 | return None 139 | 140 | def _play(self, wav_path): 141 | subprocess.Popen( 142 | [ 143 | "ffplay", 144 | "-volume", 145 | f'{self.ctx["speech_volume"]}', 146 | "-af", 147 | f'atempo={self.ctx["speech_speed"]}', 148 | "-autoexit", 149 | "-nodisp", 150 | "-loglevel", 151 | "fatal", 152 | wav_path, 153 | ], 154 | stdout=subprocess.DEVNULL, # 終了時の改行対策、stderrは残す 155 | ).wait() 156 | -------------------------------------------------------------------------------- /KoboldCpp/Launch-Ocuteus-v1-Q8_0-C16K-L0.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | chcp 65001 > NUL 3 | pushd %~dp0 4 | set CURL_CMD=C:\Windows\System32\curl.exe -k 5 | 6 | @REM 7B: 33, 35B: 41, 70B: 65 7 | set GPU_LAYERS=0 8 | 9 | @REM 2048, 4096, 8192, 16384, 32768, 65536, 131072 10 | set CONTEXT_SIZE=16384 11 | 12 | if not exist Ocuteus-v1-q8_0.gguf ( 13 | start "" https://huggingface.co/Local-Novel-LLM-project/Ocuteus-v1-gguf 14 | start "" https://huggingface.co/Local-Novel-LLM-project/Ocuteus-v1-gguf/blob/main/Modelfile-Ocuteus-v1 15 | 16 | %CURL_CMD% -LO https://huggingface.co/Local-Novel-LLM-project/Ocuteus-v1-gguf/resolve/main/Ocuteus-v1-q8_0.gguf 17 | ) 18 | 19 | if not exist Ocuteus-v1-mmproj-f16.gguf ( 20 | %CURL_CMD% -LO https://huggingface.co/Local-Novel-LLM-project/Ocuteus-v1-gguf/resolve/main/Ocuteus-v1-mmproj-f16.gguf 21 | ) 22 | 23 | koboldcpp.exe --gpulayers %GPU_LAYERS% --usecublas --contextsize %CONTEXT_SIZE% --mmproj Ocuteus-v1-mmproj-f16.gguf --launch Ocuteus-v1-q8_0.gguf 24 | if %errorlevel% neq 0 ( pause & popd & exit /b 1 ) 25 | popd 26 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Zuntan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EasyNovelAssistant 2 | 3 | 軽量で規制も検閲もない日本語ローカル LLM『[LightChatAssistant-TypeB](https://huggingface.co/Sdff-Ltba/LightChatAssistant-TypeB-2x7B-GGUF)』による、簡単なノベル生成アシスタントです。 4 | ローカル特権の永続生成 Generate forever で、当たりガチャを積み上げます。読み上げにも対応。 5 | 6 | 内部で呼び出している [KoboldCpp](https://github.com/LostRuins/koboldcpp) や [Style-Bert-VITS2](https://github.com/litagin02/Style-Bert-VITS2) を直接利用することもできますし、[EasySdxlWebUi](https://github.com/Zuntan03/EasySdxlWebUi) で画像を生成しながら利用することもできます。 7 | 8 | ## 利用者の声 9 | 10 | **記事** 11 | 12 | - 『[【検閲なし】GPUで生成するローカルAIチャット環境と小説企画+執筆用ゴールシークプロンプトで叡智小説生成最強に見える](https://note.com/kagami_kami/n/n3a321d926684)』[@kagami_kami_m](https://twitter.com/kagami_kami_m/status/1785313774620246194) 13 | - [@Emanon_14](https://twitter.com/Emanon_14/status/1787491885801783753), 14 | [@bla_tanuki](https://twitter.com/bla_tanuki/status/1786969054336700924), 15 | [@bla_tanuki](https://twitter.com/bla_tanuki/status/1786982703692382277), 16 | - 作例『[[AI試運転]スパーリング・ウィズ・ツクモドウ](https://note.com/liruk/n/nfd0bb54903cb)』と [制作の感想](https://twitter.com/liruk/status/1785596479631204420)。 17 | 18 | **動画** 19 | 20 | [EasyNovelAssistantの利用検証](https://www.nicovideo.jp/watch/sm43774612), 21 | [負けヒロインの告白](https://www.nicovideo.jp/watch/sm43754628) 22 | 23 | **つぶやき** 24 | 25 | [@AIiswonder](https://x.com/AIiswonder/status/1791854351457325319), 26 | [@umiyuki_ai](https://x.com/umiyuki_ai/status/1791360673575997553), 27 | [@dew_dew](https://x.com/dew_dew/status/1790402531459555696), 28 | [@StelsRay](https://twitter.com/StelsRay/status/1789525236557492374), 29 | [@kirimajiro](https://twitter.com/kirimajiro/status/1788173520612344283), 30 | [@Ak9TLSB3fwWnMzn](https://twitter.com/Ak9TLSB3fwWnMzn/status/1787123194991931852), 31 | [@Emanon_14](https://twitter.com/Emanon_14/status/1787317994345070865), 32 | [@liruk](https://twitter.com/liruk/status/1787318402736115994), 33 | [@maru_ai29](https://twitter.com/maru_ai29/status/1787059183621378073), 34 | [@bla_tanuki](https://twitter.com/bla_tanuki/status/1786968425430167829), 35 | [@muchkanensys](https://twitter.com/muchkanensys/status/1786991909409595529), 36 | [@shinshi78](https://twitter.com/shinshi78/status/1786991262387888451), 37 | [865](https://fate.5ch.net/test/read.cgi/liveuranus/1714702930/865), 38 | [186](https://fate.5ch.net/test/read.cgi/liveuranus/1714702930/186), 39 | [@kurayamimousou](https://twitter.com/kurayamimousou/status/1786377248033136794), 40 | [@boxheadroom](https://twitter.com/boxheadroom/status/1786031076617703640), 41 | [@luta_ai](https://twitter.com/luta_ai/status/1785933828730802214), 42 | [0026](https://mercury.bbspink.com/test/read.cgi/onatech/1714642045/26), 43 | [@liruk](https://twitter.com/liruk/status/1785596479631204420), 44 | [@kagami_kami_m](https://twitter.com/kagami_kami_m/status/1785805841410691320), 45 | [@AonekoSS](https://twitter.com/AonekoSS/status/1785327191859122446), 46 | [@maaibook](https://twitter.com/maaibook/status/1785540609627054413), 47 | [@corpsmanWelt](https://twitter.com/corpsmanWelt/status/1785878852792901738), 48 | [@kiyoshi_shin](https://twitter.com/kiyoshi_shin/status/1785363555132596593), 49 | [@AINewsDev](https://twitter.com/AINewsDev/status/1784241585183658138), 50 | [@kgmkm_inma_ai](https://twitter.com/kgmkm_inma_ai/status/1785149941448663443), 51 | [@AonekoSS](https://twitter.com/AonekoSS/status/1784650868195024996), 52 | [@StelsRay](https://twitter.com/StelsRay/status/1785338281485553757), 53 | [@mikumiku_aloha](https://twitter.com/mikumiku_aloha/status/1785300629461799372), 54 | [@kagami_kami_m](https://twitter.com/kagami_kami_m/status/1784446620916146273), 55 | [@2ewsHQJgnvkGNPr](https://twitter.com/2ewsHQJgnvkGNPr/status/1784123670451130527), 56 | [@ainiji981](https://twitter.com/ainiji981/status/1784140730094805215), 57 | [@Neve_AI](https://twitter.com/Neve_AI/status/1784207868549542307), 58 | [@WreckerAi](https://twitter.com/WreckerAi/status/1784245468798836773), 59 | [@ai_1610](https://twitter.com/ai_1610/status/1784075370330992763), 60 | [@kagami_kami_m](https://twitter.com/kagami_kami_m/status/1783113042576003282), 61 | [@kohya_tech](https://twitter.com/kohya_tech/status/1782920101328732513), 62 | [@kohya_tech](https://twitter.com/kohya_tech/status/1782563778993000538), 63 | [@G13_Yuyang](https://twitter.com/G13_Yuyang/status/1782653077683855810), 64 | [0611](https://mercury.bbspink.com/test/read.cgi/onatech/1694810015/611), 65 | [0549](https://mercury.bbspink.com/test/read.cgi/onatech/1694810015/549) 66 | 67 | ### お知らせへの反応 68 | - [読み上げ音声に画像を割り当てて、字幕付きの動画の簡単作成に対応](https://twitter.com/Zuntan03/status/1786694765997924371) 69 | - [@yuki_shikihime](https://twitter.com/yuki_shikihime/status/1786718565384790201) 70 | - [EasyNovelAssistant と EasySdxlWebUi で、絵と文章と音声をローカル PC で同時生成](https://twitter.com/Zuntan03/status/1786165587573715394) 71 | - [@StelsRay](https://twitter.com/StelsRay/status/1786289235324207593), 72 | [@hysierra](https://twitter.com/hysierra/status/1786300104338731172), 73 | [@currnya](https://twitter.com/currnya/status/1786357838492946803), 74 | [984](https://bbs.punipuni.eu/test/read.cgi/vaporeon/1712647603/984) 75 | - [EasyNovelAssistant の音声読み上げ対応](https://twitter.com/Zuntan03/status/1785252082343440723) 76 | - [@StelsRay](https://twitter.com/StelsRay/status/1785338281485553757) 77 | [@555zamagi](https://twitter.com/555zamagi/status/1785259670141374741), 78 | [879](https://mercury.bbspink.com/test/read.cgi/onatech/1702817339/879), 79 | [@kurayamimousou](https://twitter.com/kurayamimousou/status/1786379824187220016) 80 | 81 | ## インストールと更新 82 | 83 | インストールや更新で困ったことが起きたら、[こちら](https://github.com/Zuntan03/EasyNovelAssistant/wiki/%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB%E3%81%A8%E6%9B%B4%E6%96%B0) を参照してください。 84 | 85 | 1. [`Install-EasyNovelAssistant.bat`](https://github.com/Zuntan03/EasyNovelAssistant/raw/main/EasyNovelAssistant/setup/Install-EasyNovelAssistant.bat?v=2) を右クリックして `名前をつけて保存` で、インストール先フォルダ(**英数字のパスで空白や日本語を含まない**)にダウンロードして実行します。 86 | - **`WindowsによってPCが保護されました` と表示されたら、`詳細表示` から `実行` します。** 87 | - `配布元から関連ファイルをダウンロード` することに問題がなければ `y` を入力します。 88 | - `Windows セキュリティ` のネットワークへのアクセス許可は `許可` してください。 89 | 1. インストールが完了すると、自動的に EasyNovelAssistant が起動します。 90 | 91 | インストール完了後は 92 | - `Run-EasyNovelAssistant.bat` で起動します。 93 | - `Update-EasyNovelAssistant.bat` で更新します。 94 | 95 | **次のステップは [はじめての生成](https://github.com/Zuntan03/EasyNovelAssistant/wiki/%E3%81%AF%E3%81%98%E3%82%81%E3%81%A6%E3%81%AE%E7%94%9F%E6%88%90) です。** 96 | 97 | ## 最近の更新 98 | 99 | ### 2024/07/05 100 | 101 | - 『[Kagemusya-7B-v1](https://huggingface.co/Local-Novel-LLM-project/kagemusya-7B-v1)』『[Shadows-MoE](https://huggingface.co/Local-Novel-LLM-project/Shadows-MoE)』『[Ninja-V3-7B](https://huggingface.co/Local-Novel-LLM-project/Ninja-V3)』を追加しました。 102 | 103 | ### 2024/06/16 104 | 105 | - 『[Ninja-V2-7B](https://huggingface.co/Local-Novel-LLM-project/Ninja-V2-7B)』を追加しました。 106 | 107 | ### 2024/06/14 108 | 109 | - KoboldCpp を更新する `Update-KoboldCpp.bat` と、CUDA 12版の KoboldCpp に更新する `Update-KoboldCpp_CUDA12.bat` を追加しました。 110 | - CUDA 12版は最近の NVIDIA GPU でより高速に動作します。 111 | 112 | ### 2024/05/29 113 | 114 | - 『[Ninja-v1-RP-expressive-v2](https://huggingface.co/Aratako/Ninja-v1-RP-expressive-v2)』を追加しました。 115 | 116 | ### 2024/05/23 117 | 118 | - [Aratako さんの自信作な新モデル](https://twitter.com/Aratako_LM/status/1792940043813920862) 『[Ninja-v1-RP-expressive](https://huggingface.co/Aratako/Ninja-v1-RP-expressive)』を追加しました。 119 | - ロールプレイ用モデルですが、他の用途でも使えそうな感触です。 120 | - ロールプレイ(チャット)をしたい場合は [プロンプトフォーマット](https://huggingface.co/Aratako/Ninja-v1-RP-expressive#%E3%83%97%E3%83%AD%E3%83%B3%E3%83%97%E3%83%88%E3%83%95%E3%82%A9%E3%83%BC%E3%83%9E%E3%83%83%E3%83%88) を確認して、`KoboldCpp/koboldcpp.exe` を [直接ご利用ください](https://github.com/Zuntan03/EasyNovelAssistant/wiki/Tips#koboldcpp)。 121 | 122 | ### 2024/05/22 123 | 124 | - [Japanese-TextGen-Kage](https://huggingface.co/dddump/Japanese-TextGen-Kage-v0.1-2x7B-gguf) の更新に対応しました。 125 | 126 | ### 2024/05/19 127 | 128 | - `生成` メニューの `生成の開始/終了 (Shift+F5)` のトグル誤操作の対策として、`生成を開始 (F3)` と `生成を終了 (F4)` を追加しました。 129 | ![](https://raw.githubusercontent.com/wiki/Zuntan03/EasyNovelAssistant/img/ChangeLog/gen_start_stop.png) 130 | - [Japanese-TextGen-MoE-TEST-2x7B-NSFW](https://huggingface.co/dddump/Japanese-TextGen-MoE-TEST-2x7B-NSFW-gguf) と [Japanese-Chat-Evolve-TEST-NSFW](https://huggingface.co/dddump/Japanese-Chat-Evolve-TEST-7B-NSFW-gguf) の Ch200 差し替え版に対応しました。 131 | - [Japanese-Chat-Evolve-TEST-NSFW](https://huggingface.co/dddump/Japanese-Chat-Evolve-TEST-7B-NSFW-gguf) の `コンテキストサイズ上限` が `8K` から `4K` に下がっていますので、ご注意ください。 132 | 133 | ### 2024/05/17 134 | 135 | - [Japanese-TextGen-MoE-TEST-2x7B-NSFW](https://huggingface.co/dddump/Japanese-TextGen-MoE-TEST-2x7B-NSFW-gguf) の [ファイル名変更](https://huggingface.co/dddump/Japanese-TextGen-MoE-TEST-2x7B-NSFW-gguf/commit/f39f2353116283a863d86d7406375c6904007364#d2h-964057) に対応しました。 136 | 137 | ### 2024/05/16 138 | 139 | - [Japanese-TextGen-MoE-TEST-2x7B-NSFW](https://huggingface.co/dddump/Japanese-TextGen-MoE-TEST-2x7B-NSFW-gguf) 作者 [dddump さん](https://huggingface.co/dddump) の新モデル 2種を追加しました。 140 | - [Japanese-Chat-Evolve-TEST-NSFW](https://huggingface.co/dddump/Japanese-Chat-Evolve-TEST-7B-NSFW-gguf) は `コンテキストサイズ上限` を `8K` まで設定できます。 141 | - [Japanese-TextGen-Kage](https://huggingface.co/dddump/Japanese-TextGen-Kage-v0.1-2x7B-gguf) は `コンテキストサイズ上限` を `32K` まで設定できます。 142 | - Geforce RTX 3060 12GB 環境では `コンテキストサイズ上限` が `16K` だと `GPU レイヤー` を `L33` でフルロードできます。 143 | 144 | ### 2024/05/11 145 | 146 | 大規模な更新ですので、不具合がありましたらお知らせください。 147 | 148 | ![](https://raw.githubusercontent.com/wiki/Zuntan03/EasyNovelAssistant/img/ChangeLog/tab.png) 149 | 150 | - プロンプト入力欄がタブ付きになり、複数のプロンプトの比較や調整がやりやすくなりました。 151 | ![](https://raw.githubusercontent.com/wiki/Zuntan03/EasyNovelAssistant/img/ChangeLog/tabs.png) 152 | - 複数ファイルやフォルダを開けます。ドラッグ&ドロップにも対応しています。 153 | - 最近開いたフォルダや最近使ったファイルで作業状況を復元できます。 154 | - ファイル名順で読み込みますので、プロンプト順のコントロールに活用ください。 155 | ![](https://raw.githubusercontent.com/wiki/Zuntan03/EasyNovelAssistant/img/ChangeLog/recent.png) 156 | - タブに `イントロプロンプト` を指定すると、他のタブのプロンプトを生成時に付け足せます。 157 | - 世界観、キャラ設定、あらすじなどをイントロとして、各章の執筆を別タブ・別ファイルで進められます。 158 | - 入力欄の先頭に `// intro\n` があると `イントロプロンプト` として扱います。 159 | - タブを右クリックして、`イントロプロンプト` で設定してください。 160 | ![](https://raw.githubusercontent.com/wiki/Zuntan03/EasyNovelAssistant/img/ChangeLog/intro.png) 161 | - これらの章別執筆のサンプルを `sample/GoalSeek/` に用意しました([@kagami_kami_m さんの記事](https://note.com/kagami_kami/n/n3a321d926684) を元にしています)。 162 | - `GoalSeek` のフォルダをドラッグ&ドロップして、フォルダごと読み込みます。 163 | - 例えば `10-序章` タブを生成する際に、イントロプロンプトに指定した `01-執筆` が自動的に前に付け足されます。 164 | - 前章を記憶として付け足したり、執筆済みの章を要約して任意に付け足したりもできます。 165 | - 最近の個性豊かな軽量モデル公開ラッシュに対応しました。 166 | - [Japanese-TextGen-MoE-TEST-2x7B-NSFW](https://huggingface.co/dddump/Japanese-TextGen-MoE-TEST-2x7B-NSFW-gguf) 167 | - [ArrowPro-7B-RobinHood](https://huggingface.co/mmnga/DataPilot-ArrowPro-7B-RobinHood-gguf) 168 | - [ArrowPro-7B-RobinHood-toxic](https://huggingface.co/Aratako/ArrowPro-7B-RobinHood-toxic-GGUF) 169 | - [ArrowPro-7B-KUJIRA](https://huggingface.co/mmnga/DataPilot-ArrowPro-7B-KUJIRA-gguf) 170 | - [Fugaku-LLM-13B-instruct](https://huggingface.co/mmnga/Fugaku-LLM-13B-instruct-gguf) 171 | - `llm_sequence.json` のフォーマットを変更しました。 172 | - 詳細は `EasyNovelAssistant/setup/res/default_llm_sequence.json` を参照ください。 173 | - 入力欄タブのコンテキストメニューに `タブを複製` を追加しました。 174 | 175 | ### 2024/05/10 176 | 177 | - [Ocuteus-v1](https://huggingface.co/Local-Novel-LLM-project/Ocuteus-v1-gguf) を KoboldCpp で試せる `KoboldCpp/Launch-Ocuteus-v1-Q8_0-C16K-L0.bat` を追加しました。 178 | - GPU レイヤーを増やして高速化したい場合は、bat をコピーして `Launch-Ocuteus-v1-Q8_0-C16K-L33.bat` などにリネームし、`set GPU_LAYERS=0` を `set GPU_LAYERS=33` に書き換えます。 179 | 180 | ![](https://raw.githubusercontent.com/wiki/Zuntan03/EasyNovelAssistant/img/ChangeLog/Ocuteus.png) 181 | 182 | ### 2024/05/07 183 | 184 | - `設定` メニューに `フォント`、`フォントサイズ`、`テーマカラーの反転` を追加しました。 185 | - フォントの選択欄が上下にとても長くなっていますので、キーボードの上下キーで選択してください。 186 | - `config.json` の以下の項目を編集すれば、細かく色を設定することもできます。 187 | 188 | ``` 189 | "foreground_color": "#CCCCCC", 190 | "select_foreground_color": "#FFFFFF", 191 | "background_color": "#222222", 192 | "select_background_color": "#555555", 193 | ``` 194 | 195 | ![](https://raw.githubusercontent.com/wiki/Zuntan03/EasyNovelAssistant/img/ChangeLog/font_setting.png) 196 | 197 | ### 2024/05/06 198 | 199 | - `コンテキストサイズ上限` 以上の `生成文の長さ` を指定した際に、`生成文の長さ` を自動的に短縮するようにしました。 200 | - アップデート後に入力欄と関係のない文章が生成されていた方は、この対応で修正されます。 201 | - `生成文の長さ` が 4096 以上の長文を生成する方法 202 | - モデルを Vecteus(4K) からLightChatAssistant や Ninja に変更 203 | - `コンテキストサイズ上限` を 6144 以上に設定 204 | - `生成文の長さ` を 4096 以上に設定 205 | - `コンテキストサイズ上限` を増やすと VRAM 消費も増えますので、動作しない場合はモデルの GPU レイヤー数(`L33` など)を引き下げてください。 206 | - `sample/user.json` ファイルがあれば、他の `sample/*.json` と同じように `ユーザー` メニューを追加するようにしました。 207 | 208 | **[過去の更新履歴](https://github.com/Zuntan03/EasyNovelAssistant/wiki/%E6%9B%B4%E6%96%B0%E5%B1%A5%E6%AD%B4)** 209 | 210 | ## ドキュメント 211 | 212 | ### EasyNovelAssistant 213 | 214 | - [インストールと更新](https://github.com/Zuntan03/EasyNovelAssistant/wiki/%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB%E3%81%A8%E6%9B%B4%E6%96%B0) 215 | - インストールと更新の詳細説明とトラブルシューティングです。 216 | - [はじめての生成](https://github.com/Zuntan03/EasyNovelAssistant/wiki/%E3%81%AF%E3%81%98%E3%82%81%E3%81%A6%E3%81%AE%E7%94%9F%E6%88%90) 217 | - EasyNovelAssistant のチュートリアルです。 218 | - [モデルと GPU レイヤー数の選択](https://github.com/Zuntan03/EasyNovelAssistant/wiki/%E3%83%A2%E3%83%87%E3%83%AB%E3%81%A8-GPU-%E3%83%AC%E3%82%A4%E3%83%A4%E3%83%BC%E6%95%B0%E3%81%AE%E9%81%B8%E6%8A%9E) 219 | - 多様なモデルを効率的に利用する方法です。 220 | - [Tips](https://github.com/Zuntan03/EasyNovelAssistant/wiki/Tips) 221 | - ちょっとした情報です。 222 | - [動画の作成](https://github.com/Zuntan03/EasyNovelAssistant/wiki/%E5%8B%95%E7%94%BB%E3%81%AE%E4%BD%9C%E6%88%90) 223 | - 読み上げ音声に画像を割り当てて、字幕付きの動画を簡単に作成します。 224 | - [更新履歴](https://github.com/Zuntan03/EasyNovelAssistant/wiki/%E6%9B%B4%E6%96%B0%E5%B1%A5%E6%AD%B4) 225 | - 過去の更新履歴です。 226 | 227 | ## ライセンス 228 | 229 | このリポジトリの内容は以下を除き [MIT License](./LICENSE.txt) です。 230 | 231 | - インストール時に [ダウンロードするモノの一覧](https://github.com/Zuntan03/EasyNovelAssistant/blob/48350f45c838e4cda4f2a977c446e1f4141c858f/EasyNovelAssistant/setup/Install-EasyNovelAssistant.bat#L31) を表示します。 232 | - `EasyNovelAssistant/setup/res/tkinter-PythonSoftwareFoundationLicense.zip` は Python Software Foundation License です。 233 | - [Style-Bert-VITS2](https://github.com/litagin02/Style-Bert-VITS2) がダウンロードする [JVNV](https://sites.google.com/site/shinnosuketakamichi/research-topics/jvnv_corpus) 派生物は [CC BY-SA 4.0 DEED](https://creativecommons.org/licenses/by-sa/4.0/deed.ja) です。 234 | -------------------------------------------------------------------------------- /Run-EasyNovelAssistant.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | chcp 65001 > NUL 3 | set CURL_CMD=C:\Windows\System32\curl.exe -k 4 | 5 | if not exist %~dp0sample\ ( mkdir %~dp0sample ) 6 | pushd %~dp0sample 7 | 8 | echo %CURL_CMD% -sLO https://yyy.wpx.jp/EasyNovelAssistant/sample/special.json 9 | %CURL_CMD% -sLO https://yyy.wpx.jp/EasyNovelAssistant/sample/special.json 10 | if %errorlevel% neq 0 ( pause & popd & exit /b 1 ) 11 | 12 | echo %CURL_CMD% -sLO https://yyy.wpx.jp/EasyNovelAssistant/sample/template.json 13 | %CURL_CMD% -sLO https://yyy.wpx.jp/EasyNovelAssistant/sample/template.json 14 | if %errorlevel% neq 0 ( pause & popd & exit /b 1 ) 15 | 16 | echo %CURL_CMD% -sLO https://yyy.wpx.jp/EasyNovelAssistant/sample/sample.json 17 | %CURL_CMD% -sLO https://yyy.wpx.jp/EasyNovelAssistant/sample/sample.json 18 | if %errorlevel% neq 0 ( pause & popd & exit /b 1 ) 19 | 20 | echo %CURL_CMD% -sLO https://yyy.wpx.jp/EasyNovelAssistant/sample/nsfw.json 21 | %CURL_CMD% -sLO https://yyy.wpx.jp/EasyNovelAssistant/sample/nsfw.json 22 | if %errorlevel% neq 0 ( pause & popd & exit /b 1 ) 23 | 24 | echo %CURL_CMD% -sLO https://yyy.wpx.jp/EasyNovelAssistant/sample/speech.json 25 | %CURL_CMD% -sLO https://yyy.wpx.jp/EasyNovelAssistant/sample/speech.json 26 | if %errorlevel% neq 0 ( pause & popd & exit /b 1 ) 27 | 28 | popd 29 | if not exist %~dp0sample\GoalSeek\ ( mkdir %~dp0sample\GoalSeek ) 30 | pushd %~dp0sample\GoalSeek 31 | 32 | echo %CURL_CMD% -sLO https://yyy.wpx.jp/EasyNovelAssistant/sample/GoalSeek/00-企画.txt 33 | %CURL_CMD% -sLO https://yyy.wpx.jp/EasyNovelAssistant/sample/GoalSeek/00-企画.txt 34 | if %errorlevel% neq 0 ( pause & popd & exit /b 1 ) 35 | 36 | echo %CURL_CMD% -sLO https://yyy.wpx.jp/EasyNovelAssistant/sample/GoalSeek/01-執筆.txt 37 | %CURL_CMD% -sLO https://yyy.wpx.jp/EasyNovelAssistant/sample/GoalSeek/01-執筆.txt 38 | if %errorlevel% neq 0 ( pause & popd & exit /b 1 ) 39 | 40 | echo %CURL_CMD% -sLO https://yyy.wpx.jp/EasyNovelAssistant/sample/GoalSeek/10-序章.txt 41 | %CURL_CMD% -sLO https://yyy.wpx.jp/EasyNovelAssistant/sample/GoalSeek/10-序章.txt 42 | if %errorlevel% neq 0 ( pause & popd & exit /b 1 ) 43 | 44 | echo %CURL_CMD% -sLO https://yyy.wpx.jp/EasyNovelAssistant/sample/GoalSeek/20-第一章.txt 45 | %CURL_CMD% -sLO https://yyy.wpx.jp/EasyNovelAssistant/sample/GoalSeek/20-第一章.txt 46 | if %errorlevel% neq 0 ( pause & popd & exit /b 1 ) 47 | 48 | echo %CURL_CMD% -sLO https://yyy.wpx.jp/EasyNovelAssistant/sample/GoalSeek/30-第二章.txt 49 | %CURL_CMD% -sLO https://yyy.wpx.jp/EasyNovelAssistant/sample/GoalSeek/30-第二章.txt 50 | if %errorlevel% neq 0 ( pause & popd & exit /b 1 ) 51 | 52 | echo %CURL_CMD% -sLO https://yyy.wpx.jp/EasyNovelAssistant/sample/GoalSeek/40-第三章.txt 53 | %CURL_CMD% -sLO https://yyy.wpx.jp/EasyNovelAssistant/sample/GoalSeek/40-第三章.txt 54 | if %errorlevel% neq 0 ( pause & popd & exit /b 1 ) 55 | 56 | echo %CURL_CMD% -sLO https://yyy.wpx.jp/EasyNovelAssistant/sample/GoalSeek/50-終章.txt 57 | %CURL_CMD% -sLO https://yyy.wpx.jp/EasyNovelAssistant/sample/GoalSeek/50-終章.txt 58 | if %errorlevel% neq 0 ( pause & popd & exit /b 1 ) 59 | 60 | popd 61 | pushd %~dp0 62 | call EasyNovelAssistant\setup\ActivateVirtualEnvironment.bat 63 | if %errorlevel% neq 0 ( popd & exit /b 1 ) 64 | 65 | python EasyNovelAssistant\src\easy_novel_assistant.py 66 | if %errorlevel% neq 0 ( pause & popd & exit /b 1 ) 67 | popd 68 | -------------------------------------------------------------------------------- /Run-EasyNovelAssistant.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mkdir -p sample 4 | cd sample 5 | 6 | curl -LO https://yyy.wpx.jp/EasyNovelAssistant/sample/special.json 7 | if [ $? -ne 0 ]; then 8 | exit 1 9 | fi 10 | 11 | curl -LO https://yyy.wpx.jp/EasyNovelAssistant/sample/template.json 12 | if [ $? -ne 0 ]; then 13 | exit 1 14 | fi 15 | 16 | curl -LO https://yyy.wpx.jp/EasyNovelAssistant/sample/sample.json 17 | if [ $? -ne 0 ]; then 18 | exit 1 19 | fi 20 | 21 | curl -LO https://yyy.wpx.jp/EasyNovelAssistant/sample/nsfw.json 22 | if [ $? -ne 0 ]; then 23 | exit 1 24 | fi 25 | 26 | curl -LO https://yyy.wpx.jp/EasyNovelAssistant/sample/speech.json 27 | if [ $? -ne 0 ]; then 28 | exit 1 29 | fi 30 | 31 | cd - 32 | 33 | source ./venv/bin/activate 34 | python ./EasyNovelAssistant/src/easy_novel_assistant.py 35 | -------------------------------------------------------------------------------- /Update-KoboldCpp.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | chcp 65001 > NUL 3 | set CURL_CMD=C:\Windows\System32\curl.exe 4 | set KOBOLD_CPP_DIR=KoboldCpp 5 | set KOBOLD_CPP_EXE=koboldcpp.exe 6 | 7 | pushd %~dp0 8 | if not exist %KOBOLD_CPP_DIR%\ ( mkdir %KOBOLD_CPP_DIR% ) 9 | popd 10 | pushd %~dp0%KOBOLD_CPP_DIR% 11 | 12 | echo %CURL_CMD% -Lo %KOBOLD_CPP_EXE% https://github.com/LostRuins/koboldcpp/releases/latest/download/koboldcpp.exe 13 | %CURL_CMD% -Lo %KOBOLD_CPP_EXE% https://github.com/LostRuins/koboldcpp/releases/latest/download/koboldcpp.exe 14 | if %errorlevel% neq 0 ( pause & popd & exit /b 1 ) 15 | 16 | popd 17 | -------------------------------------------------------------------------------- /Update-KoboldCpp_CUDA12.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | chcp 65001 > NUL 3 | set CURL_CMD=C:\Windows\System32\curl.exe 4 | set KOBOLD_CPP_DIR=KoboldCpp 5 | set KOBOLD_CPP_EXE=koboldcpp.exe 6 | 7 | pushd %~dp0 8 | if not exist %KOBOLD_CPP_DIR%\ ( mkdir %KOBOLD_CPP_DIR% ) 9 | popd 10 | pushd %~dp0%KOBOLD_CPP_DIR% 11 | 12 | echo %CURL_CMD% -Lo %KOBOLD_CPP_EXE% https://github.com/LostRuins/koboldcpp/releases/latest/download/koboldcpp_cu12.exe 13 | %CURL_CMD% -Lo %KOBOLD_CPP_EXE% https://github.com/LostRuins/koboldcpp/releases/latest/download/koboldcpp_cu12.exe 14 | if %errorlevel% neq 0 ( pause & popd & exit /b 1 ) 15 | 16 | popd 17 | --------------------------------------------------------------------------------