├── 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 | 
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 | 
149 |
150 | - プロンプト入力欄がタブ付きになり、複数のプロンプトの比較や調整がやりやすくなりました。
151 | 
152 | - 複数ファイルやフォルダを開けます。ドラッグ&ドロップにも対応しています。
153 | - 最近開いたフォルダや最近使ったファイルで作業状況を復元できます。
154 | - ファイル名順で読み込みますので、プロンプト順のコントロールに活用ください。
155 | 
156 | - タブに `イントロプロンプト` を指定すると、他のタブのプロンプトを生成時に付け足せます。
157 | - 世界観、キャラ設定、あらすじなどをイントロとして、各章の執筆を別タブ・別ファイルで進められます。
158 | - 入力欄の先頭に `// intro\n` があると `イントロプロンプト` として扱います。
159 | - タブを右クリックして、`イントロプロンプト` で設定してください。
160 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------