diff --git a/.github/readme-zh_cn.md b/.github/readme-zh_cn.md index 3ad3a60d9..d1212b8f8 100644 --- a/.github/readme-zh_cn.md +++ b/.github/readme-zh_cn.md @@ -41,8 +41,6 @@ SillyTavern 本身并无用处,因为它只是一个用户聊天界面。你 -Termux 不支持**.Webp 字符卡的导入/导出。请使用 JSON 或 PNG 格式**。 - ## 有问题或建议? ### 我们现在有了 Discord 社区 diff --git a/.github/readme.md b/.github/readme.md index e567330ed..3ff551fbd 100644 --- a/.github/readme.md +++ b/.github/readme.md @@ -41,8 +41,6 @@ Since Tavern is only a user interface, it has tiny hardware requirements, it wil -**.webp character cards import/export is not supported in Termux. Use either JSON or PNG formats instead.** - ## Questions or suggestions? ### We now have a community Discord server @@ -71,7 +69,6 @@ Get in touch with the developers directly: * [Oobabooga's TextGen WebUI](https://github.com/oobabooga/text-generation-webui) API connection * [AI Horde](https://horde.koboldai.net/) connection * Prompt generation formatting tweaking -* webp character card interoperability (PNG is still an internal format) ## Extensions diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 000000000..fee50ace1 --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,45 @@ +# This workflow will publish a docker image for every full release to the GitHub package repository + +name: Create Docker Image on Release + +on: + release: + # Only runs on full releases not pre releases + types: [released] + +env: + # This should allow creation of docker images even in forked repositories + # Image name may not contain uppercase characters, so we can not use the repository name + # Creates a string like: ghcr.io/SillyTavern/sillytavern + image_name: ghcr.io/${{ github.repository_owner }}/sillytavern + +jobs: + + build: + + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v3 + + # Build docker image using dockerfile and tag it with branch name + # Assumes branch name is the version number + - name: Build the Docker image + run: | + docker build . --file Dockerfile --tag $image_name:${{ github.ref_name }} + + # Login into package repository as the person who created the release + - name: Login to GitHub Container Registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Assumes release is the latest and marks image as such + - name: Docker Tag and Push + run: | + docker tag $image_name:${{ github.ref_name }} $image_name:latest + docker push $image_name:${{ github.ref_name }} + docker push $image_name:latest diff --git a/Dockerfile b/Dockerfile index 100aed121..dba0ab7ce 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ ENTRYPOINT [ "tini", "--" ] WORKDIR ${APP_HOME} # Install app dependencies -COPY package*.json ./ +COPY package*.json post-install.js ./ RUN \ echo "*** Install npm packages ***" && \ npm install && npm cache clean --force diff --git a/default/settings.json b/default/settings.json index 2e520fdbb..5f5834640 100644 --- a/default/settings.json +++ b/default/settings.json @@ -75,9 +75,6 @@ "always_force_name2": true, "user_prompt_bias": "", "show_user_prompt_bias": true, - "multigen": false, - "multigen_first_chunk": 50, - "multigen_next_chunks": 30, "markdown_escape_strings": "", "fast_ui_mode": false, "avatar_style": 0, diff --git a/package-lock.json b/package-lock.json index ec1093e3d..9bb63f3aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sillytavern", - "version": "1.10.3", + "version": "1.10.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sillytavern", - "version": "1.10.3", + "version": "1.10.4", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { @@ -21,7 +21,6 @@ "cors": "^2.8.5", "csrf-csrf": "^2.2.3", "device-detector-js": "^3.0.3", - "exifreader": "^4.12.0", "express": "^4.18.2", "google-translate-api-browser": "^3.0.1", "gpt3-tokenizer": "^1.1.5", @@ -35,7 +34,6 @@ "multer": "^1.4.5-lts.1", "node-fetch": "^2.6.11", "open": "^8.4.2", - "piexifjs": "^1.0.6", "png-chunk-text": "^1.0.0", "png-chunks-encode": "^1.0.0", "png-chunks-extract": "^1.0.0", @@ -45,7 +43,6 @@ "simple-git": "^3.19.1", "uniqolor": "^1.1.0", "vectra": "^0.2.2", - "webp-converter": "2.3.2", "write-file-atomic": "^5.0.1", "ws": "^8.13.0", "yargs": "^17.7.1", @@ -968,15 +965,6 @@ "integrity": "sha512-9E61voMP4+Rze02jlTXud++Htpjyyk8vw5Hyw9FGRrmhHQg2GqbuOfwf5Klrb8vTxc2XWI3EfO7RUHMpxTj26A==", "peer": true }, - "node_modules/@xmldom/xmldom": { - "version": "0.8.9", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.9.tgz", - "integrity": "sha512-4VSbbcMoxc4KLjb1gs96SRmi7w4h1SF+fCoiK0XaQX62buCc1G5d0DC5bJ9xJBNPDSVCmIrcl8BiYxzjrqaaJA==", - "optional": true, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -1793,15 +1781,6 @@ "resolved": "https://registry.npmjs.org/exif-parser/-/exif-parser-0.1.12.tgz", "integrity": "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw==" }, - "node_modules/exifreader": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/exifreader/-/exifreader-4.13.0.tgz", - "integrity": "sha512-IhJBpyXDLbCdgzVHkthadOvrMiZOR2XS7POVp0b5JoVfScRoCJ6YazZ+stTkbDTE5TRTP44bE5RKsujckAs45Q==", - "hasInstallScript": true, - "optionalDependencies": { - "@xmldom/xmldom": "^0.8.8" - } - }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -3097,11 +3076,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/piexifjs": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/piexifjs/-/piexifjs-1.0.6.tgz", - "integrity": "sha512-0wVyH0cKohzBQ5Gi2V1BuxYpxWfxF3cSqfFXfPIpl5tl9XLS5z4ogqhUCD20AbHi0h9aJkqXNJnkVev6gwh2ag==" - }, "node_modules/pixelmatch": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-4.0.2.tgz", @@ -4183,11 +4157,6 @@ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, - "node_modules/webp-converter": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/webp-converter/-/webp-converter-2.3.2.tgz", - "integrity": "sha512-9kQ9Q/MPzUV2mye8Tv7vA6vDIPk77rI4AWWm2vSaCyGAEsxqyVZYeVU2MSJY5fLkf6u7G5K343vLxKubOxz16Q==" - }, "node_modules/whatwg-fetch": { "version": "3.6.18", "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.18.tgz", diff --git a/package.json b/package.json index 929e6dc8f..c3e855035 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,6 @@ "cors": "^2.8.5", "csrf-csrf": "^2.2.3", "device-detector-js": "^3.0.3", - "exifreader": "^4.12.0", "express": "^4.18.2", "google-translate-api-browser": "^3.0.1", "gpt3-tokenizer": "^1.1.5", @@ -25,7 +24,6 @@ "multer": "^1.4.5-lts.1", "node-fetch": "^2.6.11", "open": "^8.4.2", - "piexifjs": "^1.0.6", "png-chunk-text": "^1.0.0", "png-chunks-encode": "^1.0.0", "png-chunks-extract": "^1.0.0", @@ -35,7 +33,6 @@ "simple-git": "^3.19.1", "uniqolor": "^1.1.0", "vectra": "^0.2.2", - "webp-converter": "2.3.2", "write-file-atomic": "^5.0.1", "ws": "^8.13.0", "yargs": "^17.7.1", @@ -53,7 +50,7 @@ "type": "git", "url": "https://github.com/SillyTavern/SillyTavern.git" }, - "version": "1.10.3", + "version": "1.10.4", "scripts": { "start": "node server.js", "start-multi": "node server.js --disableCsrf", diff --git a/public/i18n.json b/public/i18n.json index 8a4f583a4..655a74732 100644 --- a/public/i18n.json +++ b/public/i18n.json @@ -158,9 +158,6 @@ "Disabled for all models": "对所有模型禁用", "Automatic (based on model name)": "自动(基于型号名称)", "Enabled for all models": "所有模型启用", - "Multigen": "Multigen", - "First chunk (tokens)": "第一个区块(Tokens)", - "Next chunks (tokens)": "接下来的区块(Tokens)", "Anchors Order": "锚点顺序", "Character then Style": "字符然后样式", "Style then Character": "样式然后字符", @@ -284,7 +281,6 @@ "Regenerate": "重新生成", "PNG": "PNG", "JSON": "JSON", - "WEBP": "WEBP", "presets": "预设", "Message Sound": "AI 消息提示音", "Author's Note": "作者注释", @@ -711,9 +707,6 @@ "Disabled for all models": "すべてのモデルで無効", "Automatic (based on model name)": "自動(モデル名に基づく)", "Enabled for all models": "すべてのモデルで有効", - "Multigen": "マルチジェン", - "First chunk (tokens)": "最初のチャンク(トークン)", - "Next chunks (tokens)": "次のチャンク(トークン)", "Anchors Order": "アンカーオーダー", "Character then Style": "キャラクター、次にスタイル", "Style then Character": "スタイル、次にキャラクター", @@ -836,7 +829,6 @@ "Regenerate": "再生成", "PNG": "PNG", "JSON": "JSON", - "WEBP": "WEBP", "presets": "プリセット", "Message Sound": "メッセージ音", "Author's Note": "作者の注記", @@ -1266,9 +1258,6 @@ "Disabled for all models": "모든 모델에 비활성화", "Automatic (based on model name)": "모델 서식 자동탐지", "Enabled for all models": "모든 모델에 활성화", - "Multigen": "다수답변 생성", - "First chunk (tokens)": "첫 말뭉치(토큰수)", - "Next chunks (tokens)": "다음 말뭉치(토큰수)", "Anchors Order": "Anchors Order", "Character then Style": "캐릭터 다음 스타일", "Style then Character": "스타일 다음 캐릭터", @@ -1392,7 +1381,6 @@ "Regenerate": "재생성", "PNG": "PNG", "JSON": "JSON", - "WEBP": "WEBP", "presets": "기본설정", "Message Sound": "메시지 효과음", "Author's Note": "글쓴이 쪽지", @@ -1876,9 +1864,6 @@ "Disabled for all models": "Выключено для всех моделей", "Automatic (based on model name)": "Автоматически (выбор по названию модели)", "Enabled for all models": "Включить для всех моделей", - "Multigen": "Мултиген", - "First chunk (tokens)": "Первый отрезок (в токенах)", - "Next chunks (tokens)": "Следующий отрезок (в токенах)", "Anchors Order": "Порядок Anchors", "Character then Style": "Персонаж после Стиля", "Style then Character": "Стиль после Персонажа", @@ -2016,7 +2001,6 @@ "Regenerate": "Повторная генерация", "PNG": "PNG", "JSON": "JSON", - "WEBP": "WEBP", "presets": "Предустановки", "Message Sound": "Звук сообщения", "Author's Note": "Авторские заметки", @@ -2454,9 +2438,6 @@ "Disabled for all models": "Disabilita per tutti i modelli", "Automatic (based on model name)": "Automatico (basato sul nome del modello)", "Enabled for all models": "Abilita per tutti i modelli", - "Multigen": "Multigen", - "First chunk (tokens)": "Primo pacchetto (in Token)", - "Next chunks (tokens)": "Pacchetto successivo (in Token)", "Anchors Order": "Anchors Order", "Character then Style": "Prima il personaggio, successivamente lo stile", "Style then Character": "Prima lo stile, successivamente il personaggio", @@ -2580,7 +2561,6 @@ "Regenerate": "Rigenera", "PNG": "PNG", "JSON": "JSON", - "WEBP": "WEBP", "presets": "preset", "Message Sound": "Suono del messaggio", "Author's Note": "Note d'autore", @@ -3133,9 +3113,6 @@ "Disabled for all models": "Uitgeschakeld voor alle modellen", "Automatic (based on model name)": "Automatisch (op basis van modelnaam)", "Enabled for all models": "Ingeschakeld voor alle modellen", - "Multigen": "Multigen", - "First chunk (tokens)": "Eerste stuk (tokens)", - "Next chunks (tokens)": "Volgende stukken (tokens)", "Anchors Order": "Ankersvolgorde", "Character then Style": "Personage dan Stijl", "Style then Character": "Stijl dan Personage", @@ -3259,7 +3236,6 @@ "Regenerate": "Regenereren", "PNG": "PNG", "JSON": "JSON", - "WEBP": "WEBP", "presets": "sjablonen", "Message Sound": "Berichtgeluid", "Author's Note": "Notitie van auteur", @@ -3696,6 +3672,6 @@ "API Key": "Clave API", "Get it here:": "Consíguela aquí:", "View my Kudos": "Ver mis Kudos", - "Models": "Modelos IA" + "Models": "Modelos IA" } } diff --git a/public/index.html b/public/index.html index 010fb8849..7eb9a8bb4 100644 --- a/public/index.html +++ b/public/index.html @@ -2474,25 +2474,27 @@

- Multigen - - ? - + Auto-Continue

- -
-
@@ -3499,7 +3501,7 @@ @@ -4462,7 +4464,6 @@
PNG
JSON
-
WEBP
diff --git a/public/script.js b/public/script.js index a82ae592f..30577a2ef 100644 --- a/public/script.js +++ b/public/script.js @@ -322,7 +322,6 @@ let safetychat = [ { name: systemUserName, is_user: false, - is_name: true, create_date: 0, mes: "You deleted a character/chat and arrived back here for safety reasons! Pick another character!", }, @@ -398,7 +397,6 @@ function getSystemMessages() { force_avatar: system_avatar, is_user: false, is_system: true, - is_name: true, mes: renderTemplate("help"), }, slash_commands: { @@ -406,7 +404,6 @@ function getSystemMessages() { force_avatar: system_avatar, is_user: false, is_system: true, - is_name: true, mes: '', }, hotkeys: { @@ -414,7 +411,6 @@ function getSystemMessages() { force_avatar: system_avatar, is_user: false, is_system: true, - is_name: true, mes: renderTemplate("hotkeys"), }, formatting: { @@ -422,7 +418,6 @@ function getSystemMessages() { force_avatar: system_avatar, is_user: false, is_system: true, - is_name: true, mes: renderTemplate("formatting"), }, macros: { @@ -430,7 +425,6 @@ function getSystemMessages() { force_avatar: system_avatar, is_user: false, is_system: true, - is_name: true, mes: renderTemplate("macros"), }, welcome: @@ -439,7 +433,6 @@ function getSystemMessages() { force_avatar: system_avatar, is_user: false, is_system: true, - is_name: true, mes: renderTemplate("welcome"), }, group: { @@ -447,7 +440,6 @@ function getSystemMessages() { force_avatar: system_avatar, is_user: false, is_system: true, - is_name: true, is_group: true, mes: "Group chat created. Say 'Hi' to lovely people!", }, @@ -456,7 +448,6 @@ function getSystemMessages() { force_avatar: system_avatar, is_user: false, is_system: true, - is_name: true, mes: "No one hears you. Hint: add more members to the group!", }, generic: { @@ -464,7 +455,6 @@ function getSystemMessages() { force_avatar: system_avatar, is_user: false, is_system: true, - is_name: true, mes: "Generic system message. User `text` parameter to override the contents", }, bookmark_created: { @@ -472,7 +462,6 @@ function getSystemMessages() { force_avatar: system_avatar, is_user: false, is_system: true, - is_name: true, mes: `Bookmark created! Click here to open the bookmark chat: {1}`, }, bookmark_back: { @@ -480,7 +469,6 @@ function getSystemMessages() { force_avatar: system_avatar, is_user: false, is_system: true, - is_name: true, mes: `Click here to return to the previous chat: Return`, }, }; @@ -662,9 +650,7 @@ export let user_avatar = "you.png"; export var amount_gen = 80; //default max length of AI generated responses var max_context = 2048; -var tokens_already_generated = 0; var message_already_generated = ""; -var cycle_count_generation = 0; var swipes = true; let extension_prompts = {}; @@ -2060,8 +2046,7 @@ function isStreamingEnabled() { return ((main_api == 'openai' && oai_settings.stream_openai && oai_settings.chat_completion_source !== chat_completion_sources.SCALE && oai_settings.chat_completion_source !== chat_completion_sources.AI21) || (main_api == 'kobold' && kai_settings.streaming_kobold && kai_flags.can_use_streaming) || (main_api == 'novel' && nai_settings.streaming_novel) - || (main_api == 'textgenerationwebui' && textgenerationwebui_settings.streaming)) - && !isMultigenEnabled(); // Multigen has a quasi-streaming mode which breaks the real streaming + || (main_api == 'textgenerationwebui' && textgenerationwebui_settings.streaming)); } function showStopButton() { @@ -2128,9 +2113,6 @@ class StreamingProcessor { const isContinue = this.type == "continue"; text = this.removePrefix(text); let processedText = cleanUpMessage(text, isImpersonate, isContinue, !isFinal); - let result = extractNameFromMessage(processedText, this.force_name2, isImpersonate); - let isName = result.this_mes_is_name; - processedText = result.getMessage; // Predict unbalanced asterisks / quotes during streaming const charsToBalance = ['*', '"']; @@ -2149,7 +2131,6 @@ class StreamingProcessor { // Don't waste time calculating token count for streaming let currentTokenCount = isFinal && power_user.message_token_count_enabled ? getTokenCount(processedText, 0) : 0; const timePassed = formatGenerationTimer(this.timeStarted, currentTime, currentTokenCount); - chat[messageId]['is_name'] = isName; chat[messageId]['mes'] = processedText; chat[messageId]['gen_started'] = this.timeStarted; chat[messageId]['gen_finished'] = currentTime; @@ -2311,7 +2292,6 @@ class StreamingProcessor { async function Generate(type, { automatic_trigger, force_name2, resolve, reject, quiet_prompt, force_chid, signal } = {}, dryRun = false) { //console.log('Generate entered'); setGenerationProgress(0); - tokens_already_generated = 0; generation_started = new Date(); // Don't recreate abort controller if signal is passed @@ -2324,17 +2304,6 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, const isImpersonate = type == "impersonate"; message_already_generated = isImpersonate ? `${name1}: ` : `${name2}: `; - // Name for the multigen prefix - const magName = isImpersonate ? name1 : name2; - - if (isInstruct) { - message_already_generated = formatInstructModePrompt(magName, isImpersonate, '', name1, name2); - } else { - message_already_generated = `${magName}: `; - } - - // To trim after multigen ended - const magFirst = message_already_generated; const interruptedByCommand = processCommands($("#send_textarea").val(), type); @@ -2356,12 +2325,6 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, return; } - if (main_api == 'kobold' && kai_settings.streaming_kobold && power_user.multigen) { - toastr.error('Multigen is not supported with Kobold streaming enabled. Disable streaming in "AI Response Configuration" or multigen in "Advanced Formatting" to proceed.', undefined, { timeOut: 10000, preventDuplicates: true, }); - is_send_press = false; - return; - } - if (isHordeGenerationNotAllowed()) { is_send_press = false; return; @@ -2523,7 +2486,7 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, console.log(`Core/all messages: ${coreChat.length}/${chat.length}`); // kingbri MARK: - Make sure the prompt bias isn't the same as the user bias - if ((promptBias && !isUserPromptBias) || power_user.always_force_name2) { + if ((promptBias && !isUserPromptBias) || power_user.always_force_name2 || main_api == 'novel') { force_name2 = true; } @@ -2605,7 +2568,7 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, const storyString = renderStoryString(storyStringParams); if (main_api === 'openai') { - message_already_generated = ''; // OpenAI doesn't have multigen + message_already_generated = ''; setOpenAIMessages(coreChat); setOpenAIMessageExamples(mesExamplesArray); } @@ -2699,10 +2662,8 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, // Save reply does add cycle text to the prompt, so it's not needed here streamingProcessor && (streamingProcessor.firstMessageText = ''); message_already_generated = continue_mag; - tokens_already_generated = 1; // Multigen copium } - // Multigen rewrites the type and I don't know why const originalType = type; runGenerate(cyclePrompt); @@ -2771,13 +2732,13 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, } // Get instruct mode line - if (isInstruct && tokens_already_generated === 0) { + if (isInstruct && !isContinue) { const name = isImpersonate ? name1 : name2; lastMesString += formatInstructModePrompt(name, isImpersonate, promptBias, name1, name2); } // Get non-instruct impersonation line - if (!isInstruct && isImpersonate && tokens_already_generated === 0) { + if (!isInstruct && isImpersonate && !isContinue) { const name = name1; if (!lastMesString.endsWith('\n')) { lastMesString += '\n'; @@ -2787,7 +2748,7 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, // Add character's name // Force name append on continue - if (!isInstruct && force_name2 && (tokens_already_generated === 0 || isContinue)) { + if (!isInstruct && force_name2) { if (!lastMesString.endsWith('\n')) { lastMesString += '\n'; } @@ -2900,14 +2861,12 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, }); // TODO: Move zero-depth anchor append to work like CFG and bias appends - if (zeroDepthAnchor && zeroDepthAnchor.length) { - if (!isMultigenEnabled() || tokens_already_generated == 0) { - console.log(/\s/.test(finalMesSend[finalMesSend.length - 1].message.slice(-1))) - finalMesSend[finalMesSend.length - 1].message += - /\s/.test(finalMesSend[finalMesSend.length - 1].message.slice(-1)) - ? zeroDepthAnchor - : `${zeroDepthAnchor}`; - } + if (zeroDepthAnchor?.length && !isContinue) { + console.log(/\s/.test(finalMesSend[finalMesSend.length - 1].message.slice(-1))) + finalMesSend[finalMesSend.length - 1].message += + /\s/.test(finalMesSend[finalMesSend.length - 1].message.slice(-1)) + ? zeroDepthAnchor + : `${zeroDepthAnchor}`; } let cfgPrompt = {}; @@ -2929,7 +2888,7 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, // Add prompt bias after everything else // Always run with continue - if (!isInstruct && !isImpersonate && (tokens_already_generated === 0 || isContinue)) { + if (!isInstruct && !isImpersonate) { if (promptBias.trim().length !== 0) { finalMesSend[finalMesSend.length - 1].message += /\s/.test(finalMesSend[finalMesSend.length - 1].message.slice(-1)) @@ -2978,11 +2937,6 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, let this_amount_gen = Number(amount_gen); // how many tokens the AI will be requested to generate let this_settings = koboldai_settings[koboldai_setting_names[preset_settings]]; - if (isMultigenEnabled() && type !== 'quiet') { - // if nothing has been generated yet.. - this_amount_gen = getMultigenAmount(); - } - let thisPromptBits = []; // TODO: Make this a switch @@ -3133,6 +3087,7 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, if (isStreamingEnabled() && type !== 'quiet') { hideSwipeButtons(); let getMessage = await streamingProcessor.generate(); + let messageChunk = cleanUpMessage(getMessage, isImpersonate, isContinue, false); if (isContinue) { getMessage = continue_mag + getMessage; @@ -3141,10 +3096,13 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, if (streamingProcessor && !streamingProcessor.isStopped && streamingProcessor.isFinished) { await streamingProcessor.onFinishStreaming(streamingProcessor.messageId, getMessage); streamingProcessor = null; + triggerAutoContinue(messageChunk, isImpersonate); } } async function onSuccess(data) { + let messageChunk = ''; + if (data.error == 'dryRun') { generatedPromptCache = ''; resolve(); @@ -3157,48 +3115,7 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, let title = extractTitleFromData(data); kobold_horde_model = title; - // to make it continue generating so long as it's under max_amount and hasn't signaled - // an end to the character's response via typing "You:" or adding "" - if (isMultigenEnabled() && type !== 'quiet') { - message_already_generated += getMessage; - promptBias = ''; - - let this_mes_is_name; - ({ this_mes_is_name, getMessage } = extractNameFromMessage(getMessage, force_name2, isImpersonate)); - - if (!isImpersonate) { - if (tokens_already_generated == 0) { - console.debug("New message"); - ({ type, getMessage } = await saveReply(type, getMessage, this_mes_is_name, title)); - } - else { - console.debug("Should append message"); - ({ type, getMessage } = await saveReply('append', getMessage, this_mes_is_name, title)); - } - } else { - let chunk = cleanUpMessage(message_already_generated, true, isContinue, true); - let extract = extractNameFromMessage(chunk, force_name2, isImpersonate); - $('#send_textarea').val(extract.getMessage).trigger('input'); - } - - if (shouldContinueMultigen(getMessage, isImpersonate, isInstruct)) { - hideSwipeButtons(); - tokens_already_generated += this_amount_gen; // add new gen amt to any prev gen counter.. - getMessage = message_already_generated; - - // if any tokens left to generate - if (getMultigenAmount() > 0) { - runGenerate(getMessage); - console.debug('returning to make generate again'); - return; - } - } - - tokens_already_generated = 0; - generatedPromptCache = ""; - const substringStart = originalType !== 'continue' ? magFirst.length : 0; - getMessage = message_already_generated.substring(substringStart); - } + messageChunk = cleanUpMessage(getMessage, isImpersonate, isContinue, false); if (isContinue) { getMessage = continue_mag + getMessage; @@ -3208,8 +3125,6 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, const displayIncomplete = type == 'quiet'; getMessage = cleanUpMessage(getMessage, isImpersonate, isContinue, displayIncomplete); - let this_mes_is_name; - ({ this_mes_is_name, getMessage } = extractNameFromMessage(getMessage, force_name2, isImpersonate)); if (getMessage.length > 0) { if (isImpersonate) { $('#send_textarea').val(getMessage).trigger('input'); @@ -3220,12 +3135,12 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, resolve(getMessage); } else { - // Without streaming we'll be having a full message on continuation. Treat it as a multigen last chunk. - if (!isMultigenEnabled() && originalType !== 'continue') { - ({ type, getMessage } = await saveReply(type, getMessage, this_mes_is_name, title)); + // Without streaming we'll be having a full message on continuation. Treat it as a last chunk. + if (originalType !== 'continue') { + ({ type, getMessage } = await saveReply(type, getMessage, true, title)); } else { - ({ type, getMessage } = await saveReply('appendFinal', getMessage, this_mes_is_name, title)); + ({ type, getMessage } = await saveReply('appendFinal', getMessage, true, title)); } } activateSendButtons(); @@ -3298,6 +3213,7 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, setGenerationProgress(0); if (type !== 'quiet') { + triggerAutoContinue(messageChunk, isImpersonate); resolve(); } }; @@ -3330,6 +3246,61 @@ function getNextMessageId(type) { return type == 'swipe' ? Number(count_view_mes - 1) : Number(count_view_mes); } +/** + * + * @param {string} messageChunk + * @param {boolean} isImpersonate + * @returns {void} + */ +export function triggerAutoContinue(messageChunk, isImpersonate) { + if (selected_group) { + console.log('Auto-continue is disabled for group chat'); + return; + } + + if (power_user.auto_continue.enabled && !is_send_press) { + if (power_user.auto_continue.target_length <= 0) { + console.log('Auto-continue target length is 0, not triggering auto-continue'); + return; + } + + if (main_api === 'openai' && !power_user.auto_continue.allow_chat_completions) { + console.log('Auto-continue for OpenAI is disabled by user.'); + return; + } + + if (isImpersonate) { + console.log('Continue for impersonation is not implemented yet'); + return; + } + + const textareaText = String($('#send_textarea').val()); + const USABLE_LENGTH = 5; + + if (textareaText.length > 0) { + console.log('Not triggering auto-continue because user input is not empty'); + return; + } + + if (messageChunk.trim().length > USABLE_LENGTH && chat.length) { + const lastMessage = chat[chat.length - 1]; + const messageLength = getTokenCount(lastMessage.mes); + const shouldAutoContinue = messageLength < power_user.auto_continue.target_length; + + if (shouldAutoContinue) { + console.log(`Triggering auto-continue. Message tokens: ${messageLength}. Target tokens: ${power_user.auto_continue.target_length}. Message chunk: ${messageChunk}`); + $("#option_continue").trigger('click'); + } else { + console.log(`Not triggering auto-continue. Message tokens: ${messageLength}. Target tokens: ${power_user.auto_continue.target_length}`); + return; + } + } else { + console.log('Last generated chunk was empty, not triggering auto-continue'); + return; + } + } +} + export function getBiasStrings(textareaText, type) { if (type == 'impersonate' || type == 'continue') { return { messageBias: '', promptBias: '', isUserPromptBias: false }; @@ -3373,7 +3344,7 @@ function formatMessageHistoryItem(chatItem, isInstruct, forceOutputSequence) { const isNarratorType = chatItem?.extra?.type === system_message_types.NARRATOR; const characterName = (selected_group || chatItem.force_avatar) ? chatItem.name : name2; const itemName = chatItem.is_user ? chatItem['name'] : characterName; - const shouldPrependName = (chatItem.is_name || chatItem.force_avatar || selected_group) && !isNarratorType; + const shouldPrependName = !isNarratorType; let textResult = shouldPrependName ? `${itemName}: ${chatItem.mes}\n` : `${chatItem.mes}\n`; @@ -3396,7 +3367,6 @@ export async function sendMessageAsUser(textareaText, messageBias) { chat[chat.length] = {}; chat[chat.length - 1]['name'] = name1; chat[chat.length - 1]['is_user'] = true; - chat[chat.length - 1]['is_name'] = true; chat[chat.length - 1]['send_date'] = getMessageTimeStamp(); chat[chat.length - 1]['mes'] = substituteParams(textareaText); chat[chat.length - 1]['extra'] = {}; @@ -3507,35 +3477,6 @@ function appendZeroDepthAnchor(force_name2, zeroDepthAnchor, finalPrompt) { return finalPrompt; } -function getMultigenAmount() { - let this_amount_gen = Number(amount_gen); - - if (tokens_already_generated === 0) { - // if the max gen setting is > 50...( - if (Number(amount_gen) >= power_user.multigen_first_chunk) { - // then only try to make 50 this cycle.. - this_amount_gen = power_user.multigen_first_chunk; - } - else { - // otherwise, make as much as the max amount request. - this_amount_gen = Number(amount_gen); - } - } - // if we already received some generated text... - else { - // if the remaining tokens to be made is less than next potential cycle count - if (Number(amount_gen) - tokens_already_generated < power_user.multigen_next_chunks) { - // subtract already generated amount from the desired max gen amount - this_amount_gen = Number(amount_gen) - tokens_already_generated; - } - else { - // otherwise make the standard cycle amount (first 50, and 30 after that) - this_amount_gen = power_user.multigen_next_chunks; - } - } - return this_amount_gen; -} - async function DupeChar() { if (!this_chid) { toastr.warning('You must first select a character to duplicate!') @@ -3754,50 +3695,11 @@ function getGenerateUrl() { } else if (main_api == 'textgenerationwebui') { generate_url = '/generate_textgenerationwebui'; } else if (main_api == 'novel') { - generate_url = '/generate_novelai'; + generate_url = '/api/novelai/generate'; } return generate_url; } -function shouldContinueMultigen(getMessage, isImpersonate, isInstruct) { - if (isInstruct && power_user.instruct.stop_sequence) { - if (message_already_generated.indexOf(power_user.instruct.stop_sequence) !== -1) { - return false; - } - } - - // stopping name string - const nameString = isImpersonate ? `${name2}:` : `${name1}:`; - // if there is no 'You:' in the response msg - const doesNotContainName = message_already_generated.indexOf(nameString) === -1; - //if there is no stamp in the response msg - const isNotEndOfText = message_already_generated.indexOf('<|endoftext|>') === -1; - //if the gen'd msg is less than the max response length.. - const notReachedMax = tokens_already_generated < Number(amount_gen); - //if we actually have gen'd text at all... - const msgHasText = getMessage.length > 0; - return doesNotContainName && isNotEndOfText && notReachedMax && msgHasText; -} - -function extractNameFromMessage(getMessage, force_name2, isImpersonate) { - const nameToTrim = isImpersonate ? name1 : name2; - let this_mes_is_name = true; - if (getMessage.startsWith(nameToTrim + ":")) { - getMessage = getMessage.replace(nameToTrim + ':', ''); - getMessage = getMessage.trimStart(); - } else { - this_mes_is_name = false; - } - if (force_name2 || power_user.instruct.enabled) - this_mes_is_name = true; - - if (isImpersonate) { - getMessage = getMessage.trim(); - } - - return { this_mes_is_name, getMessage }; -} - function throwCircuitBreakerError() { callPopup(`Could not extract reply in ${MAX_GENERATION_LOOPS} attempts. Try generating again`, 'text'); generate_loop_counter = 0; @@ -3876,7 +3778,7 @@ function cleanUpMessage(getMessage, isImpersonate, isContinue, displayIncomplete if (nameToTrim && getMessage.indexOf(`${nameToTrim}:`) == 0) { getMessage = getMessage.substr(0, getMessage.indexOf(`${nameToTrim}:`)); } - if (nameToTrim && getMessage.indexOf(`\n${nameToTrim}:`) > 0) { + if (nameToTrim && getMessage.indexOf(`\n${nameToTrim}:`) >= 0) { getMessage = getMessage.substr(0, getMessage.indexOf(`\n${nameToTrim}:`)); } if (getMessage.indexOf('<|endoftext|>') != -1) { @@ -3947,10 +3849,22 @@ function cleanUpMessage(getMessage, isImpersonate, isContinue, displayIncomplete if (power_user.auto_fix_generated_markdown) { getMessage = fixMarkdown(getMessage, false); } + + const nameToTrim2 = isImpersonate ? name1 : name2; + + if (getMessage.startsWith(nameToTrim2 + ":")) { + getMessage = getMessage.replace(nameToTrim2 + ':', ''); + getMessage = getMessage.trimStart(); + } + + if (isImpersonate) { + getMessage = getMessage.trim(); + } + return getMessage; } -async function saveReply(type, getMessage, this_mes_is_name, title) { +async function saveReply(type, getMessage, _, title) { if (type != 'append' && type != 'continue' && type != 'appendFinal' && chat.length && (chat[chat.length - 1]['swipe_id'] === undefined || chat[chat.length - 1]['is_user'])) { type = 'normal'; @@ -4023,7 +3937,6 @@ async function saveReply(type, getMessage, this_mes_is_name, title) { chat[chat.length - 1]['extra'] = {}; chat[chat.length - 1]['name'] = name2; chat[chat.length - 1]['is_user'] = false; - chat[chat.length - 1]['is_name'] = this_mes_is_name; chat[chat.length - 1]['send_date'] = getMessageTimeStamp(); chat[chat.length - 1]["extra"]["api"] = getGeneratingApi(); chat[chat.length - 1]["extra"]["model"] = getGeneratingModel(); @@ -4045,7 +3958,6 @@ async function saveReply(type, getMessage, this_mes_is_name, title) { if (characters[this_chid].avatar != 'none') { avatarImg = getThumbnailUrl('avatar', characters[this_chid].avatar); } - chat[chat.length - 1]['is_name'] = true; chat[chat.length - 1]['force_avatar'] = avatarImg; chat[chat.length - 1]['original_avatar'] = characters[this_chid].avatar; chat[chat.length - 1]['extra']['gen_id'] = group_generation_id; @@ -4135,10 +4047,6 @@ function extractImageFromMessage(getMessage) { return { getMessage, image, title }; } -export function isMultigenEnabled() { - return power_user.multigen && (main_api == 'textgenerationwebui' || main_api == 'kobold' || main_api == 'koboldhorde' || main_api == 'novel'); -} - export function activateSendButtons() { is_send_press = false; $("#send_but").removeClass("displayNone"); @@ -4537,7 +4445,6 @@ function getFirstMessage() { name: name2, is_user: false, is_system: false, - is_name: true, send_date: getMessageTimeStamp(), mes: getRegexedString(firstMes, regex_placement.AI_OUTPUT), extra: {}, @@ -5120,11 +5027,11 @@ function updateMessage(div) { const mes = chat[this_edit_mes_id]; let regexPlacement; - if (mes.is_name && mes.is_user) { + if (mes.is_user) { regexPlacement = regex_placement.USER_INPUT; - } else if (mes.is_name && mes.name === name2) { + } else if (mes.name === name2) { regexPlacement = regex_placement.AI_OUTPUT; - } else if (mes.is_name && mes.name !== name2 || mes.extra?.type === "narrator") { + } else if (mes.name !== name2 || mes.extra?.type === "narrator") { regexPlacement = regex_placement.SLASH_COMMAND; } @@ -5476,7 +5383,6 @@ function select_rm_info(type, charId, previousCharId = null) { $('#rm_print_characters_pagination').pagination('go', page); waitUntilCondition(() => document.querySelector(selector) !== null).then(() => { - const parent = $('#rm_print_characters_block'); const element = $(selector).parent(); if (element.length === 0) { @@ -5484,7 +5390,8 @@ function select_rm_info(type, charId, previousCharId = null) { return; } - parent.scrollTop(element.position().top + parent.scrollTop()); + const scrollOffset = element.offset().top - element.parent().offset().top; + element.parent().scrollTop(scrollOffset); element.addClass('flash animated'); setTimeout(function () { element.removeClass('flash animated'); @@ -5508,12 +5415,12 @@ function select_rm_info(type, charId, previousCharId = null) { const perPage = Number(localStorage.getItem('Characters_PerPage')); const page = Math.floor(charIndex / perPage) + 1; $('#rm_print_characters_pagination').pagination('go', page); - const parent = $('#rm_print_characters_block'); const selector = `#rm_print_characters_block [grid="${charId}"]`; try { waitUntilCondition(() => document.querySelector(selector) !== null).then(() => { const element = $(selector); - parent.scrollTop(element.position().top + parent.scrollTop()); + const scrollOffset = element.offset().top - element.parent().offset().top; + element.parent().scrollTop(scrollOffset); $(element).addClass('flash animated'); setTimeout(function () { $(element).removeClass('flash animated'); @@ -6573,13 +6480,6 @@ const swipe_right = () => { return; } - // if (chat.length == 1) { - // if (chat[0]['swipe_id'] !== undefined && chat[0]['swipe_id'] == chat[0]['swipes'].length - 1) { - // toastr.info('Add more alternative greetings to swipe through', 'That\'s all for now'); - // return; - // } - // } - const swipe_duration = 200; const swipe_range = 700; //console.log(swipe_range); @@ -6829,7 +6729,6 @@ export function processDroppedFiles(files) { const allowedMimeTypes = [ 'application/json', 'image/png', - 'image/webp', ]; for (const file of files) { @@ -6845,7 +6744,7 @@ function importCharacter(file) { const ext = file.name.match(/\.(\w+)$/); if ( !ext || - (ext[1].toLowerCase() != "json" && ext[1].toLowerCase() != "png" && ext[1] != "webp") + (ext[1].toLowerCase() != "json" && ext[1].toLowerCase() != "png") ) { return; } diff --git a/public/scripts/bookmarks.js b/public/scripts/bookmarks.js index 294f0266e..e22b135fd 100644 --- a/public/scripts/bookmarks.js +++ b/public/scripts/bookmarks.js @@ -280,7 +280,6 @@ async function convertSoloToGroupChat() { message.name = character.name; message.original_avatar = character.avatar; message.force_avatar = getThumbnailUrl('avatar', character.avatar); - message.is_name = true; // Allow regens of a single message in group if (typeof message.extra !== 'object') { diff --git a/public/scripts/extensions/caption/index.js b/public/scripts/extensions/caption/index.js index c199c3c2a..5a610033b 100644 --- a/public/scripts/extensions/caption/index.js +++ b/public/scripts/extensions/caption/index.js @@ -53,7 +53,6 @@ async function sendCaptionedMessage(caption, image) { const message = { name: context.name1, is_user: true, - is_name: true, send_date: getMessageTimeStamp(), mes: messageText, extra: { diff --git a/public/scripts/extensions/hypebot/index.js b/public/scripts/extensions/hypebot/index.js index 250ed97ce..cd853544a 100644 --- a/public/scripts/extensions/hypebot/index.js +++ b/public/scripts/extensions/hypebot/index.js @@ -159,7 +159,7 @@ async function generateHypeBot() { abortController = new AbortController(); - const response = await fetch('/generate_novelai', { + const response = await fetch('/api/novelai/generate', { headers: getRequestHeaders(), body: JSON.stringify(parameters), method: 'POST', diff --git a/public/scripts/extensions/infinity-context/index.js b/public/scripts/extensions/infinity-context/index.js index 303b2eee0..d1c761925 100644 --- a/public/scripts/extensions/infinity-context/index.js +++ b/public/scripts/extensions/infinity-context/index.js @@ -508,7 +508,6 @@ async function onSelectInjectFile(e) { meta: JSON.stringify({ name: file.name, is_user: false, - is_name: false, is_system: false, send_date: humanizedDateTime(), mes: m, @@ -686,7 +685,6 @@ window.chromadb_interceptGeneration = async (chat, maxContext) => { const charname = context.name2; newChat.push( { - is_name: false, is_user: false, mes: `[Use these past chat exchanges to inform ${charname}'s next response:`, name: "system", @@ -696,7 +694,6 @@ window.chromadb_interceptGeneration = async (chat, maxContext) => { newChat.push(...queriedMessages.map(m => m.meta).filter(onlyUnique).map(JSON.parse)); newChat.push( { - is_name: false, is_user: false, mes: `]\n`, name: "system", @@ -752,7 +749,6 @@ window.chromadb_interceptGeneration = async (chat, maxContext) => { newChat.push( { - is_name: false, is_user: false, mes: recallStart, name: "system", @@ -762,7 +758,6 @@ window.chromadb_interceptGeneration = async (chat, maxContext) => { newChat.push(...queriedMessages.map(m => m.meta).filter(onlyUnique).map(JSON.parse)); newChat.push( { - is_name: false, is_user: false, mes: recallEnd + `\n`, name: "system", diff --git a/public/scripts/extensions/speech-recognition/index.js b/public/scripts/extensions/speech-recognition/index.js index 1ac098f36..3ac3df229 100644 --- a/public/scripts/extensions/speech-recognition/index.js +++ b/public/scripts/extensions/speech-recognition/index.js @@ -165,7 +165,6 @@ async function processTranscript(transcript) { const message = { name: context.name1, is_user: true, - is_name: true, send_date: getMessageTimeStamp(), mes: messageText, }; diff --git a/public/scripts/extensions/stable-diffusion/index.js b/public/scripts/extensions/stable-diffusion/index.js index 886cb8141..a03b71e60 100644 --- a/public/scripts/extensions/stable-diffusion/index.js +++ b/public/scripts/extensions/stable-diffusion/index.js @@ -1246,7 +1246,6 @@ async function sendMessage(prompt, image) { name: context.groupId ? systemUserName : context.name2, is_user: false, is_system: true, - is_name: true, send_date: getMessageTimeStamp(), mes: context.groupId ? p(messageText) : messageText, extra: { diff --git a/public/scripts/extensions/tts/index.js b/public/scripts/extensions/tts/index.js index 0a6a70002..3a3becd74 100644 --- a/public/scripts/extensions/tts/index.js +++ b/public/scripts/extensions/tts/index.js @@ -1,4 +1,4 @@ -import { callPopup, cancelTtsPlay, eventSource, event_types, isMultigenEnabled, is_send_press, saveSettingsDebounced } from '../../../script.js' +import { callPopup, cancelTtsPlay, eventSource, event_types, saveSettingsDebounced } from '../../../script.js' import { ModuleWorkerWrapper, doExtrasFetch, extension_settings, getApiUrl, getContext, modules } from '../../extensions.js' import { escapeRegex, getStringHash } from '../../utils.js' import { EdgeTtsProvider } from './edge.js' @@ -117,11 +117,6 @@ async function moduleWorker() { return } - // Multigen message is currently being generated - if (is_send_press && isMultigenEnabled()) { - return; - } - // Chat changed if ( context.chatId !== lastChatId diff --git a/public/scripts/extensions/tts/novel.js b/public/scripts/extensions/tts/novel.js index fb58ffffb..02ccd9ad5 100644 --- a/public/scripts/extensions/tts/novel.js +++ b/public/scripts/extensions/tts/novel.js @@ -170,7 +170,7 @@ class NovelTtsProvider { async fetchTtsGeneration(inputText, voiceId) { console.info(`Generating new TTS for voice_id ${voiceId}`) - const response = await fetch(`/novel_tts`, + const response = await fetch(`/api/novelai/generate-voice`, { method: 'POST', headers: getRequestHeaders(), diff --git a/public/scripts/group-chats.js b/public/scripts/group-chats.js index ddac1ef48..7b43eb17a 100644 --- a/public/scripts/group-chats.js +++ b/public/scripts/group-chats.js @@ -51,7 +51,6 @@ import { menu_type, select_selected_character, cancelTtsPlay, - isMultigenEnabled, displayPastChats, sendMessageAsUser, getBiasStrings, @@ -206,7 +205,6 @@ function getFirstCharacterMessage(character) { mes["is_user"] = false; mes["is_system"] = false; mes["name"] = character.name; - mes["is_name"] = true; mes["send_date"] = getMessageTimeStamp(); mes["original_avatar"] = character.avatar; mes["extra"] = { "gen_id": Date.now() * Math.random() * 1000000 }; @@ -577,7 +575,7 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) { await Generate(generateType, { automatic_trigger: by_auto_mode, ...(params || {}) }); - if (type !== "swipe" && type !== "impersonate" && !isMultigenEnabled() && !isStreamingEnabled()) { + if (type !== "swipe" && type !== "impersonate" && !isStreamingEnabled()) { // update indicator and scroll down typingIndicator .find(".typing_indicator_name") @@ -593,7 +591,7 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) { } // if not swipe - check if message generated already - if (generateType === "group_chat" && !isMultigenEnabled() && chat.length == messagesBefore) { + if (generateType === "group_chat" && chat.length == messagesBefore) { await delay(100); } // if swipe - see if message changed @@ -606,13 +604,6 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) { break; } } - else if (isMultigenEnabled()) { - if (isGenerationDone) { - break; - } else { - await delay(100); - } - } else { if (lastMessageText === chat[chat.length - 1].mes) { await delay(100); @@ -631,13 +622,6 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) { break; } } - else if (isMultigenEnabled()) { - if (isGenerationDone) { - break; - } else { - await delay(100); - } - } else { if (!$("#send_textarea").val() || $("#send_textarea").val() == userInput) { await delay(100); @@ -654,14 +638,6 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) { await delay(100); } } - else if (isMultigenEnabled()) { - if (isGenerationDone) { - messagesBefore++; - break; - } else { - await delay(100); - } - } else if (isStreamingEnabled()) { if (streamingProcessor && !streamingProcessor.isFinished) { await delay(100); diff --git a/public/scripts/nai-settings.js b/public/scripts/nai-settings.js index 468793582..dba24b4ea 100644 --- a/public/scripts/nai-settings.js +++ b/public/scripts/nai-settings.js @@ -11,6 +11,7 @@ import { getTextTokens, tokenizers } from "./tokenizers.js"; import { getSortableDelay, getStringHash, + onlyUnique, uuidv4, } from "./utils.js"; @@ -87,7 +88,7 @@ export function getNovelUnlimitedImageGeneration() { } export async function loadNovelSubscriptionData() { - const result = await fetch('/getstatus_novelai', { + const result = await fetch('/api/novelai/status', { method: 'POST', headers: getRequestHeaders(), }); @@ -402,7 +403,7 @@ function getBadWordPermutations(text) { // Ditto + leading space result.push(` ${text.toLowerCase()}`); - return result; + return result.filter(onlyUnique); } export function getNovelGenerationData(finalPrompt, this_settings, this_amount_gen, isImpersonate, cfgValues) { @@ -679,7 +680,7 @@ function tryParseStreamingError(decoded) { export async function generateNovelWithStreaming(generate_data, signal) { generate_data.streaming = nai_settings.streaming_novel; - const response = await fetch('/generate_novelai', { + const response = await fetch('/api/novelai/generate', { headers: getRequestHeaders(), body: JSON.stringify(generate_data), method: 'POST', diff --git a/public/scripts/openai.js b/public/scripts/openai.js index 9794884db..73da02abc 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -123,6 +123,32 @@ const j2_max_pres = 5.0; const openrouter_website_model = 'OR_Website'; const openai_max_stop_strings = 4; +const textCompletionModels = [ + "text-davinci-003", + "text-davinci-002", + "text-davinci-001", + "text-curie-001", + "text-babbage-001", + "text-ada-001", + "code-davinci-002", + "code-davinci-001", + "code-cushman-002", + "code-cushman-001", + "text-davinci-edit-001", + "code-davinci-edit-001", + "text-embedding-ada-002", + "text-similarity-davinci-001", + "text-similarity-curie-001", + "text-similarity-babbage-001", + "text-similarity-ada-001", + "text-search-davinci-doc-001", + "text-search-curie-doc-001", + "text-search-babbage-doc-001", + "text-search-ada-doc-001", + "code-search-babbage-code-001", + "code-search-ada-code-001", +]; + let biasCache = undefined; let model_list = []; @@ -1123,7 +1149,7 @@ async function sendOpenAIRequest(type, openai_msgs_tosend, signal) { const isOpenRouter = oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER; const isScale = oai_settings.chat_completion_source == chat_completion_sources.SCALE; const isAI21 = oai_settings.chat_completion_source == chat_completion_sources.AI21; - const isTextCompletion = oai_settings.chat_completion_source == chat_completion_sources.OPENAI && (oai_settings.openai_model.startsWith('text-') || oai_settings.openai_model.startsWith('code-')); + const isTextCompletion = oai_settings.chat_completion_source == chat_completion_sources.OPENAI && textCompletionModels.includes(oai_settings.openai_model); const isQuiet = type === 'quiet'; const stream = oai_settings.stream_openai && !isQuiet && !isScale && !isAI21; diff --git a/public/scripts/power-user.js b/public/scripts/power-user.js index 320ab92b8..57918ef30 100644 --- a/public/scripts/power-user.js +++ b/public/scripts/power-user.js @@ -93,9 +93,11 @@ let power_user = { always_force_name2: false, user_prompt_bias: '', show_user_prompt_bias: true, - multigen: false, - multigen_first_chunk: 50, - multigen_next_chunks: 30, + auto_continue: { + enabled: false, + allow_chat_completions: false, + target_length: 400, + }, markdown_escape_strings: '', ui_mode: ui_mode.POWER, @@ -848,9 +850,9 @@ function loadPowerUserSettings(settings, data) { $("#noShadowsmode").prop("checked", power_user.noShadows); $("#start_reply_with").val(power_user.user_prompt_bias); $("#chat-show-reply-prefix-checkbox").prop("checked", power_user.show_user_prompt_bias); - $("#multigen").prop("checked", power_user.multigen); - $("#multigen_first_chunk").val(power_user.multigen_first_chunk); - $("#multigen_next_chunks").val(power_user.multigen_next_chunks); + $("#auto_continue_enabled").prop("checked", power_user.auto_continue.enabled); + $("#auto_continue_allow_chat_completions").prop("checked", power_user.auto_continue.allow_chat_completions); + $("#auto_continue_target_length").val(power_user.auto_continue.target_length); $("#play_message_sound").prop("checked", power_user.play_message_sound); $("#play_sound_unfocused").prop("checked", power_user.play_sound_unfocused); $("#never_resize_avatars").prop("checked", power_user.never_resize_avatars); @@ -1816,8 +1818,18 @@ $(document).ready(() => { saveSettingsDebounced(); }) - $("#multigen").change(function () { - power_user.multigen = $(this).prop("checked"); + $("#auto_continue_enabled").on('change', function () { + power_user.auto_continue.enabled = $(this).prop("checked"); + saveSettingsDebounced(); + }); + + $("#auto_continue_allow_chat_completions").on('change', function () { + power_user.auto_continue.allow_chat_completions = !!$(this).prop('checked'); + saveSettingsDebounced(); + }); + + $("#auto_continue_target_length").on('input', function () { + power_user.auto_continue.target_length = Number($(this).val()); saveSettingsDebounced(); }); @@ -1986,16 +1998,6 @@ $(document).ready(() => { saveSettingsDebounced(); }); - $("#multigen_first_chunk").on('input', function () { - power_user.multigen_first_chunk = Number($(this).val()); - saveSettingsDebounced(); - }); - - $("#multigen_next_chunks").on('input', function () { - power_user.multigen_next_chunks = Number($(this).val()); - saveSettingsDebounced(); - }); - $('#auto_swipe').on('input', function () { power_user.auto_swipe = !!$(this).prop('checked'); saveSettingsDebounced(); diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 5d189f56d..9abe252fb 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -325,7 +325,6 @@ async function sendMessageAs(_, text) { const message = { name: name, is_user: false, - is_name: true, is_system: isSystem, send_date: getMessageTimeStamp(), mes: substituteParams(mesText), @@ -357,7 +356,6 @@ async function sendNarratorMessage(_, text) { const message = { name: name, is_user: false, - is_name: false, is_system: isSystem, send_date: getMessageTimeStamp(), mes: substituteParams(text.trim()), @@ -384,7 +382,6 @@ async function sendCommentMessage(_, text) { const message = { name: COMMENT_NAME_DEFAULT, is_user: false, - is_name: true, is_system: true, send_date: getMessageTimeStamp(), mes: substituteParams(text.trim()), diff --git a/public/style.css b/public/style.css index 342b13a61..209591f4c 100644 --- a/public/style.css +++ b/public/style.css @@ -2436,18 +2436,18 @@ input[type="range"]::-webkit-slider-thumb { #anchor_checkbox label, #power-user-option-checkboxes label, .checkbox_label, -.multigen_settings_block { +.auto_continue_settings_block { display: flex; flex-direction: row; column-gap: 5px; align-items: center; } -.multigen_settings_block { +.auto_continue_settings_block { margin-top: 10px; } -.multigen_settings_block label { +.auto_continue_settings_block label { flex: 1; display: flex; flex-direction: column; diff --git a/server.js b/server.js index ab064dfb6..832601f9d 100644 --- a/server.js +++ b/server.js @@ -10,7 +10,7 @@ const readline = require('readline'); const util = require('util'); const { Readable } = require('stream'); const { finished } = require('stream/promises'); -const { TextEncoder, TextDecoder } = require('util'); +const { TextDecoder } = require('util'); // cli/fs related library imports const open = require('open'); @@ -40,14 +40,11 @@ const json5 = require('json5'); const WebSocket = require('ws'); // image processing related library imports -const exif = require('piexifjs'); const encode = require('png-chunks-encode'); const extract = require('png-chunks-extract'); const jimp = require('jimp'); const mime = require('mime-types'); const PNGtext = require('png-chunk-text'); -const webp = require('webp-converter'); -const yauzl = require('yauzl'); // tokenizing related library imports const { SentencePieceProcessor } = require("@agnai/sentencepiece-js"); @@ -65,10 +62,9 @@ util.inspect.defaultOptions.maxStringLength = null; const basicAuthMiddleware = require('./src/middleware/basicAuthMiddleware'); const characterCardParser = require('./src/character-card-parser.js'); const contentManager = require('./src/content-manager'); -const novelai = require('./src/novelai'); const statsHelpers = require('./statsHelpers.js'); const { writeSecret, readSecret, readSecretState, migrateSecrets, SECRET_KEYS, getAllSecrets } = require('./src/secrets'); -const { delay, getVersion } = require('./src/util'); +const { delay, getVersion, getImageBuffers } = require('./src/util'); // Work around a node v20.0.0, v20.1.0, and v20.2.0 bug. The issue was fixed in v20.3.0. // https://github.com/nodejs/node/issues/47822#issuecomment-1564708870 @@ -109,8 +105,6 @@ const app = express(); app.use(compression()); app.use(responseTime()); -const utf8Encode = new TextEncoder(); - // impoort from statsHelpers.js const config = require(path.join(process.cwd(), './config.conf')); @@ -133,7 +127,6 @@ const enableExtensions = config.enableExtensions; const listen = config.listen; const allowKeysExposure = config.allowKeysExposure; -const API_NOVELAI = "https://api.novelai.net"; const API_OPENAI = "https://api.openai.com/v1"; const API_CLAUDE = "https://api.anthropic.com/v1"; @@ -242,6 +235,35 @@ function countClaudeTokens(tokenizer, messages) { const tokenizersCache = {}; +/** + * @type {import('@dqbd/tiktoken').TiktokenModel[]} + */ +const textCompletionModels = [ + "text-davinci-003", + "text-davinci-002", + "text-davinci-001", + "text-curie-001", + "text-babbage-001", + "text-ada-001", + "code-davinci-002", + "code-davinci-001", + "code-cushman-002", + "code-cushman-001", + "text-davinci-edit-001", + "code-davinci-edit-001", + "text-embedding-ada-002", + "text-similarity-davinci-001", + "text-similarity-curie-001", + "text-similarity-babbage-001", + "text-similarity-ada-001", + "text-search-davinci-doc-001", + "text-search-curie-doc-001", + "text-search-babbage-doc-001", + "text-search-ada-doc-001", + "code-search-babbage-code-001", + "code-search-ada-code-001", +]; + function getTokenizerModel(requestModel) { if (requestModel.includes('claude')) { return 'claude'; @@ -259,7 +281,7 @@ function getTokenizerModel(requestModel) { return 'gpt-3.5-turbo'; } - if (requestModel.startsWith('text-') || requestModel.startsWith('code-')) { + if (textCompletionModels.includes(requestModel)) { return requestModel; } @@ -1097,7 +1119,7 @@ app.post("/renamecharacter", jsonParser, async function (request, response) { try { // Read old file, replace name int it const rawOldData = await charaRead(oldAvatarPath); - if (rawOldData === false || rawOldData === undefined) throw new Error("Failed to read character file"); + if (rawOldData === undefined) throw new Error("Failed to read character file"); const oldData = getCharaCardV2(json5.parse(rawOldData)); _.set(oldData, 'data.name', newName); @@ -1344,7 +1366,7 @@ const calculateDataSize = (data) => { const processCharacter = async (item, i) => { try { const img_data = await charaRead(charactersPath + item); - if (img_data === false || img_data === undefined) throw new Error("Failed to read character file"); + if (img_data === undefined) throw new Error("Failed to read character file"); let jsonObject = getCharaCardV2(json5.parse(img_data)); jsonObject.avatar = item; @@ -1853,162 +1875,6 @@ function getImages(path) { .sort(Intl.Collator().compare); } -//***********Novel.ai API - -app.post("/getstatus_novelai", jsonParser, async function (request, response_getstatus_novel) { - if (!request.body) return response_getstatus_novel.sendStatus(400); - const api_key_novel = readSecret(SECRET_KEYS.NOVEL); - - if (!api_key_novel) { - return response_getstatus_novel.sendStatus(401); - } - - try { - const response = await fetch(API_NOVELAI + "/user/subscription", { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'Authorization': "Bearer " + api_key_novel, - }, - }); - - if (response.ok) { - const data = await response.json(); - return response_getstatus_novel.send(data); - } else if (response.status == 401) { - console.log('NovelAI Access Token is incorrect.'); - return response_getstatus_novel.send({ error: true }); - } - else { - console.log('NovelAI returned an error:', response.statusText); - return response_getstatus_novel.send({ error: true }); - } - } catch (error) { - console.log(error); - return response_getstatus_novel.send({ error: true }); - } -}); - -app.post("/generate_novelai", jsonParser, async function (request, response_generate_novel) { - if (!request.body) return response_generate_novel.sendStatus(400); - - const api_key_novel = readSecret(SECRET_KEYS.NOVEL); - - if (!api_key_novel) { - return response_generate_novel.sendStatus(401); - } - - const controller = new AbortController(); - request.socket.removeAllListeners('close'); - request.socket.on('close', function () { - controller.abort(); - }); - - const isNewModel = (request.body.model.includes('clio') || request.body.model.includes('kayra')); - const badWordsList = novelai.getBadWordsList(request.body.model); - - // Add customized bad words for Clio and Kayra - if (isNewModel && Array.isArray(request.body.bad_words_ids)) { - for (const badWord of request.body.bad_words_ids) { - if (Array.isArray(badWord) && badWord.every(x => Number.isInteger(x))) { - badWordsList.push(badWord); - } - } - } - - // Add default biases for dinkus and asterism - const logit_bias_exp = isNewModel ? novelai.logitBiasExp.slice() : []; - - if (Array.isArray(logit_bias_exp) && Array.isArray(request.body.logit_bias_exp)) { - logit_bias_exp.push(...request.body.logit_bias_exp); - } - - const data = { - "input": request.body.input, - "model": request.body.model, - "parameters": { - "use_string": request.body.use_string ?? true, - "temperature": request.body.temperature, - "max_length": request.body.max_length, - "min_length": request.body.min_length, - "tail_free_sampling": request.body.tail_free_sampling, - "repetition_penalty": request.body.repetition_penalty, - "repetition_penalty_range": request.body.repetition_penalty_range, - "repetition_penalty_slope": request.body.repetition_penalty_slope, - "repetition_penalty_frequency": request.body.repetition_penalty_frequency, - "repetition_penalty_presence": request.body.repetition_penalty_presence, - "repetition_penalty_whitelist": isNewModel ? novelai.repPenaltyAllowList : null, - "top_a": request.body.top_a, - "top_p": request.body.top_p, - "top_k": request.body.top_k, - "typical_p": request.body.typical_p, - "mirostat_lr": request.body.mirostat_lr, - "mirostat_tau": request.body.mirostat_tau, - "cfg_scale": request.body.cfg_scale, - "cfg_uc": request.body.cfg_uc, - "phrase_rep_pen": request.body.phrase_rep_pen, - "stop_sequences": request.body.stop_sequences, - "bad_words_ids": badWordsList, - "logit_bias_exp": logit_bias_exp, - "generate_until_sentence": request.body.generate_until_sentence, - "use_cache": request.body.use_cache, - "return_full_text": request.body.return_full_text, - "prefix": request.body.prefix, - "order": request.body.order - } - }; - - console.log(util.inspect(data, { depth: 4 })) - - const args = { - body: JSON.stringify(data), - headers: { "Content-Type": "application/json", "Authorization": "Bearer " + api_key_novel }, - signal: controller.signal, - }; - - try { - const url = request.body.streaming ? `${API_NOVELAI}/ai/generate-stream` : `${API_NOVELAI}/ai/generate`; - const response = await fetch(url, { method: 'POST', timeout: 0, ...args }); - - if (request.body.streaming) { - // Pipe remote SSE stream to Express response - response.body.pipe(response_generate_novel); - - request.socket.on('close', function () { - if (response.body instanceof Readable) response.body.destroy(); // Close the remote stream - response_generate_novel.end(); // End the Express response - }); - - response.body.on('end', function () { - console.log("Streaming request finished"); - response_generate_novel.end(); - }); - } else { - if (!response.ok) { - const text = await response.text(); - let message = text; - console.log(`Novel API returned error: ${response.status} ${response.statusText} ${text}`); - - try { - const data = JSON.parse(text); - message = data.message; - } - catch { - // ignore - } - - return response_generate_novel.status(response.status).send({ error: { message } }); - } - - const data = await response.json(); - console.log(data); - return response_generate_novel.send(data); - } - } catch (error) { - return response_generate_novel.send({ error: true }); - } -}); - app.post("/getallchatsofcharacter", jsonParser, function (request, response) { if (!request.body) return response.sendStatus(400); @@ -2182,26 +2048,13 @@ app.post("/importcharacter", urlencodedParser, async function (request, response } else { try { var img_data = await charaRead(uploadPath, format); - if (img_data === false || img_data === undefined) throw new Error('Failed to read character data'); + if (img_data === undefined) throw new Error('Failed to read character data'); let jsonData = json5.parse(img_data); jsonData.name = sanitize(jsonData.data?.name || jsonData.name); png_name = getPngName(jsonData.name); - if (format == 'webp') { - try { - let convertedPath = path.join(UPLOADS_PATH, path.basename(uploadPath, ".webp") + ".png") - await webp.dwebp(uploadPath, convertedPath, "-o"); - fs.unlinkSync(uploadPath); - uploadPath = convertedPath; - } - catch { - console.error('WEBP image conversion failed. Using the default character image.'); - uploadPath = defaultAvatarPath; - } - } - if (jsonData.spec !== undefined) { console.log('Found a v2 character file.'); importRisuSprites(jsonData); @@ -2381,7 +2234,7 @@ app.post("/exportcharacter", jsonParser, async function (request, response) { case 'json': { try { let json = await charaRead(filename); - if (json === false || json === undefined) return response.sendStatus(400); + if (json === undefined) return response.sendStatus(400); let jsonObject = getCharaCardV2(json5.parse(json)); return response.type('json').send(jsonObject) } @@ -2389,39 +2242,6 @@ app.post("/exportcharacter", jsonParser, async function (request, response) { return response.sendStatus(400); } } - case 'webp': { - try { - let json = await charaRead(filename); - if (json === false || json === undefined) return response.sendStatus(400); - let stringByteArray = utf8Encode.encode(json).toString(); - let inputWebpPath = path.join(UPLOADS_PATH, `${Date.now()}_input.webp`); - let outputWebpPath = path.join(UPLOADS_PATH, `${Date.now()}_output.webp`); - let metadataPath = path.join(UPLOADS_PATH, `${Date.now()}_metadata.exif`); - let metadata = - { - "Exif": { - [exif.ExifIFD.UserComment]: stringByteArray, - }, - }; - const exifString = exif.dump(metadata); - writeFileAtomicSync(metadataPath, exifString, 'binary'); - - await webp.cwebp(filename, inputWebpPath, '-q 95'); - await webp.webpmux_add(inputWebpPath, outputWebpPath, metadataPath, 'exif'); - - response.sendFile(outputWebpPath, { root: process.cwd() }, () => { - fs.rmSync(inputWebpPath); - fs.rmSync(metadataPath); - fs.rmSync(outputWebpPath); - }); - - return; - } - catch (err) { - console.log(err); - return response.sendStatus(400); - } - } } return response.sendStatus(400); @@ -2479,7 +2299,6 @@ app.post("/importchat", urlencodedParser, function (request, response) { (message) => ({ name: message.src.is_human ? user_name : ch_name, is_user: message.src.is_human, - is_name: true, send_date: humanizedISO8601DateTime(), mes: message.text, }) @@ -2524,7 +2343,6 @@ app.post("/importchat", urlencodedParser, function (request, response) { const userMessage = { name: user_name, is_user: true, - is_name: true, send_date: humanizedISO8601DateTime(), mes: arr[0], }; @@ -2534,7 +2352,6 @@ app.post("/importchat", urlencodedParser, function (request, response) { const charMessage = { name: ch_name, is_user: false, - is_name: true, send_date: humanizedISO8601DateTime(), mes: arr[1], }; @@ -3598,7 +3415,7 @@ app.post("/generate_openai", jsonParser, function (request, response_generate_op bodyParams['stop'] = request.body.stop; } - const isTextCompletion = Boolean(request.body.model && (request.body.model.startsWith('text-') || request.body.model.startsWith('code-'))); + const isTextCompletion = Boolean(request.body.model && textCompletionModels.includes(request.body.model)); const textPrompt = isTextCompletion ? convertChatMLPrompt(request.body.messages) : ''; const endpointUrl = isTextCompletion ? `${api_url}/completions` : `${api_url}/chat/completions`; @@ -3627,7 +3444,7 @@ app.post("/generate_openai", jsonParser, function (request, response_generate_op "frequency_penalty": request.body.frequency_penalty, "top_p": request.body.top_p, "top_k": request.body.top_k, - "stop": request.body.stop, + "stop": isTextCompletion === false ? request.body.stop : undefined, "logit_bias": request.body.logit_bias, ...bodyParams, }), @@ -4080,8 +3897,6 @@ const setupTasks = async function () { contentManager.checkForNewContent(); cleanUploads(); - await convertWebp(); - [spp_llama, spp_nerd, spp_nerd_v2, claude_tokenizer] = await Promise.all([ loadSentencepieceTokenizer('src/sentencepiece/tokenizer.model'), loadSentencepieceTokenizer('src/sentencepiece/nerdstash.model'), @@ -4141,47 +3956,6 @@ if (true === cliArguments.ssl) { ); } -async function convertWebp() { - const files = fs.readdirSync(directories.characters).filter(e => e.endsWith(".webp")); - - if (!files.length) { - return; - } - - console.log(`${files.length} WEBP files will be automatically converted.`); - - for (const file of files) { - try { - const source = path.join(directories.characters, file); - const dest = path.join(directories.characters, path.basename(file, ".webp") + ".png"); - - if (fs.existsSync(dest)) { - console.log(`${dest} already exists. Delete ${source} manually`); - continue; - } - - console.log(`Read... ${source}`); - const data = await charaRead(source); - - console.log(`Convert... ${source} -> ${dest}`); - await webp.dwebp(source, dest, "-o"); - - console.log(`Write... ${dest}`); - const success = await charaWrite(dest, data, path.parse(dest).name); - - if (!success) { - console.log(`Failure on ${source} -> ${dest}`); - continue; - } - - console.log(`Remove... ${source}`); - fs.rmSync(source); - } catch (err) { - console.log(err); - } - } -} - function backupSettings() { const MAX_BACKUPS = 25; @@ -4264,136 +4038,6 @@ app.post('/viewsecrets', jsonParser, async (_, response) => { } }); -app.post('/api/novelai/generate-image', jsonParser, async (request, response) => { - if (!request.body) { - return response.sendStatus(400); - } - - const key = readSecret(SECRET_KEYS.NOVEL); - - if (!key) { - return response.sendStatus(401); - } - - try { - console.log('NAI Diffusion request:', request.body); - const generateUrl = `${API_NOVELAI}/ai/generate-image`; - const generateResult = await fetch(generateUrl, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${key}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - action: 'generate', - input: request.body.prompt, - model: request.body.model ?? 'nai-diffusion', - parameters: { - negative_prompt: request.body.negative_prompt ?? '', - height: request.body.height ?? 512, - width: request.body.width ?? 512, - scale: request.body.scale ?? 9, - seed: Math.floor(Math.random() * 9999999999), - sampler: request.body.sampler ?? 'k_dpmpp_2m', - steps: request.body.steps ?? 28, - n_samples: 1, - // NAI handholding for prompts - ucPreset: 0, - qualityToggle: false, - }, - }), - }); - - if (!generateResult.ok) { - console.log('NovelAI returned an error.', generateResult.statusText); - return response.sendStatus(500); - } - - const archiveBuffer = await generateResult.arrayBuffer(); - const imageBuffer = await extractFileFromZipBuffer(archiveBuffer, '.png'); - const originalBase64 = imageBuffer.toString('base64'); - - // No upscaling - if (isNaN(request.body.upscale_ratio) || request.body.upscale_ratio <= 1) { - return response.send(originalBase64); - } - - try { - console.debug('Upscaling image...'); - const upscaleUrl = `${API_NOVELAI}/ai/upscale`; - const upscaleResult = await fetch(upscaleUrl, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${key}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - image: originalBase64, - height: request.body.height, - width: request.body.width, - scale: request.body.upscale_ratio, - }), - }); - - if (!upscaleResult.ok) { - throw new Error('NovelAI returned an error.'); - } - - const upscaledArchiveBuffer = await upscaleResult.arrayBuffer(); - const upscaledImageBuffer = await extractFileFromZipBuffer(upscaledArchiveBuffer, '.png'); - const upscaledBase64 = upscaledImageBuffer.toString('base64'); - - return response.send(upscaledBase64); - } catch (error) { - console.warn('NovelAI generated an image, but upscaling failed. Returning original image.'); - return response.send(originalBase64) - } - } catch (error) { - console.log(error); - return response.sendStatus(500); - } -}); - -app.post('/novel_tts', jsonParser, async (request, response) => { - const token = readSecret(SECRET_KEYS.NOVEL); - - if (!token) { - return response.sendStatus(401); - } - - const text = request.body.text; - const voice = request.body.voice; - - if (!text || !voice) { - return response.sendStatus(400); - } - - try { - const url = `${API_NOVELAI}/ai/generate-voice?text=${encodeURIComponent(text)}&voice=-1&seed=${encodeURIComponent(voice)}&opus=false&version=v2`; - const result = await fetch(url, { - method: 'GET', - headers: { - 'Authorization': `Bearer ${token}`, - 'Accept': 'audio/mpeg', - }, - timeout: 0, - }); - - if (!result.ok) { - return response.sendStatus(result.status); - } - - const chunks = await readAllChunks(result.body); - const buffer = Buffer.concat(chunks); - response.setHeader('Content-Type', 'audio/mpeg'); - return response.send(buffer); - } - catch (error) { - console.error(error); - return response.sendStatus(500); - } -}); - app.post('/delete_sprite', jsonParser, async (request, response) => { const label = request.body.label; const name = request.body.name; @@ -4551,45 +4195,6 @@ app.post('/import_custom', jsonParser, async (request, response) => { } }); -/** - * Extracts a file with given extension from an ArrayBuffer containing a ZIP archive. - * @param {ArrayBuffer} archiveBuffer Buffer containing a ZIP archive - * @param {string} fileExtension File extension to look for - * @returns {Promise} Buffer containing the extracted file - */ -async function extractFileFromZipBuffer(archiveBuffer, fileExtension) { - return await new Promise((resolve, reject) => yauzl.fromBuffer(Buffer.from(archiveBuffer), { lazyEntries: true }, (err, zipfile) => { - if (err) { - reject(err); - } - - zipfile.readEntry(); - zipfile.on('entry', (entry) => { - if (entry.fileName.endsWith(fileExtension)) { - console.log(`Extracting ${entry.fileName}`); - zipfile.openReadStream(entry, (err, readStream) => { - if (err) { - reject(err); - } else { - const chunks = []; - readStream.on('data', (chunk) => { - chunks.push(chunk); - }); - - readStream.on('end', () => { - const buffer = Buffer.concat(chunks); - resolve(buffer); - zipfile.readEntry(); // Continue to the next entry - }); - } - }); - } else { - zipfile.readEntry(); - } - }); - })); -} - async function downloadChubLorebook(id) { const result = await fetch('https://api.chub.ai/api/lorebooks/download', { method: 'POST', @@ -4737,78 +4342,6 @@ function importRisuSprites(data) { } } - -async function readAllChunks(readableStream) { - return new Promise((resolve, reject) => { - // Consume the readable stream - const chunks = []; - readableStream.on('data', (chunk) => { - chunks.push(chunk); - }); - - readableStream.on('end', () => { - //console.log('Finished reading the stream.'); - resolve(chunks); - }); - - readableStream.on('error', (error) => { - console.error('Error while reading the stream:', error); - reject(); - }); - }); -} - -async function getImageBuffers(zipFilePath) { - return new Promise((resolve, reject) => { - // Check if the zip file exists - if (!fs.existsSync(zipFilePath)) { - reject(new Error('File not found')); - return; - } - - const imageBuffers = []; - - yauzl.open(zipFilePath, { lazyEntries: true }, (err, zipfile) => { - if (err) { - reject(err); - } else { - zipfile.readEntry(); - zipfile.on('entry', (entry) => { - const mimeType = mime.lookup(entry.fileName); - if (mimeType && mimeType.startsWith('image/') && !entry.fileName.startsWith('__MACOSX')) { - console.log(`Extracting ${entry.fileName}`); - zipfile.openReadStream(entry, (err, readStream) => { - if (err) { - reject(err); - } else { - const chunks = []; - readStream.on('data', (chunk) => { - chunks.push(chunk); - }); - - readStream.on('end', () => { - imageBuffers.push([path.parse(entry.fileName).base, Buffer.concat(chunks)]); - zipfile.readEntry(); // Continue to the next entry - }); - } - }); - } else { - zipfile.readEntry(); // Continue to the next entry - } - }); - - zipfile.on('end', () => { - resolve(imageBuffers); - }); - - zipfile.on('error', (err) => { - reject(err); - }); - } - }); - }); -} - /** * This function extracts the extension information from the manifest file. * @param {string} extensionPath - The path of the extension folder @@ -5248,6 +4781,9 @@ app.post('/get_character_assets_list', jsonParser, async (request, response) => } }); +// NovelAI generation +require('./src/novelai').registerEndpoints(app, jsonParser); + // Stable Diffusion generation require('./src/stable-diffusion').registerEndpoints(app, jsonParser); diff --git a/src/character-card-parser.js b/src/character-card-parser.js index 6f2e73479..adbdca6c7 100644 --- a/src/character-card-parser.js +++ b/src/character-card-parser.js @@ -1,56 +1,12 @@ const fs = require('fs'); -const json5 = require('json5'); -const ExifReader = require('exifreader'); const extract = require('png-chunks-extract'); const PNGtext = require('png-chunk-text'); -const utf8Decode = new TextDecoder('utf-8', { ignoreBOM: true }); - const parse = async (cardUrl, format) => { - let fileFormat; - if (format === undefined) { - if (cardUrl.indexOf('.webp') !== -1) - fileFormat = 'webp'; - else - fileFormat = 'png'; - } - else - fileFormat = format; + let fileFormat = format === undefined ? 'png' : format; switch (fileFormat) { - case 'webp': - try { - const exif_data = await ExifReader.load(fs.readFileSync(cardUrl)); - let char_data; - - if (exif_data['UserComment']['description']) { - let description = exif_data['UserComment']['description']; - if (description === 'Undefined' && exif_data['UserComment'].value && exif_data['UserComment'].value.length === 1) { - description = exif_data['UserComment'].value[0]; - } - - try { - json5.parse(description); - char_data = description; - } catch { - const byteArr = description.split(",").map(Number); - const uint8Array = new Uint8Array(byteArr); - const char_data_string = utf8Decode.decode(uint8Array); - char_data = char_data_string; - } - } - else { - console.log('No description found in EXIF data.'); - return false; - } - - return char_data; - } - catch (err) { - console.log(err); - return false; - } case 'png': const buffer = fs.readFileSync(cardUrl); const chunks = extract(buffer); diff --git a/src/novelai.js b/src/novelai.js index ab6b3a6c8..75728bf67 100644 --- a/src/novelai.js +++ b/src/novelai.js @@ -1,6 +1,14 @@ +const fetch = require('node-fetch').default; +const util = require('util'); +const { Readable } = require('stream'); +const { readSecret, SECRET_KEYS } = require('./secrets'); +const { readAllChunks, extractFileFromZipBuffer } = require('./util'); + +const API_NOVELAI = "https://api.novelai.net"; + // Ban bracket generation, plus defaults const badWordsList = [ - [3], [49356], [1431], [31715], [34387], [20765], [30702], [10691], [49333], [1266], + [3], [49356], [1431], [31715], [34387], [20765], [30702], [10691], [49333], [1266], [19438], [43145], [26523], [41471], [2936], [85, 85], [49332], [7286], [1115] ] @@ -38,7 +46,7 @@ const logitBiasExp = [ ] const hypeBotLogitBiasExp = [ - { "sequence": [8162], "bias": -0.12, "ensure_sequence_finish": false, "generate_once": false}, + { "sequence": [8162], "bias": -0.12, "ensure_sequence_finish": false, "generate_once": false }, { "sequence": [46256, 224], "bias": -0.12, "ensure_sequence_finish": false, "generate_once": false } ]; @@ -57,11 +65,297 @@ function getBadWordsList(model) { return list.slice(); } +/** + * Registers NovelAI API endpoints. + * @param {import('express').Express} app - Express app + * @param {any} jsonParser - JSON parser middleware + */ +function registerEndpoints(app, jsonParser) { + app.post("/api/novelai/status", jsonParser, async function (req, res) { + if (!req.body) return res.sendStatus(400); + const api_key_novel = readSecret(SECRET_KEYS.NOVEL); + + if (!api_key_novel) { + return res.sendStatus(401); + } + + try { + const response = await fetch(API_NOVELAI + "/user/subscription", { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Authorization': "Bearer " + api_key_novel, + }, + }); + + if (response.ok) { + const data = await response.json(); + return res.send(data); + } else if (response.status == 401) { + console.log('NovelAI Access Token is incorrect.'); + return res.send({ error: true }); + } + else { + console.log('NovelAI returned an error:', response.statusText); + return res.send({ error: true }); + } + } catch (error) { + console.log(error); + return res.send({ error: true }); + } + }); + + app.post("/api/novelai/generate", jsonParser, async function (req, res) { + if (!req.body) return res.sendStatus(400); + + const api_key_novel = readSecret(SECRET_KEYS.NOVEL); + + if (!api_key_novel) { + return res.sendStatus(401); + } + + const controller = new AbortController(); + req.socket.removeAllListeners('close'); + req.socket.on('close', function () { + controller.abort(); + }); + + const isNewModel = (req.body.model.includes('clio') || req.body.model.includes('kayra')); + const badWordsList = getBadWordsList(req.body.model); + + // Add customized bad words for Clio and Kayra + if (isNewModel && Array.isArray(req.body.bad_words_ids)) { + for (const badWord of req.body.bad_words_ids) { + if (Array.isArray(badWord) && badWord.every(x => Number.isInteger(x))) { + badWordsList.push(badWord); + } + } + } + + // Add default biases for dinkus and asterism + const logit_bias_exp = isNewModel ? logitBiasExp.slice() : []; + + if (Array.isArray(logit_bias_exp) && Array.isArray(req.body.logit_bias_exp)) { + logit_bias_exp.push(...req.body.logit_bias_exp); + } + + const data = { + "input": req.body.input, + "model": req.body.model, + "parameters": { + "use_string": req.body.use_string ?? true, + "temperature": req.body.temperature, + "max_length": req.body.max_length, + "min_length": req.body.min_length, + "tail_free_sampling": req.body.tail_free_sampling, + "repetition_penalty": req.body.repetition_penalty, + "repetition_penalty_range": req.body.repetition_penalty_range, + "repetition_penalty_slope": req.body.repetition_penalty_slope, + "repetition_penalty_frequency": req.body.repetition_penalty_frequency, + "repetition_penalty_presence": req.body.repetition_penalty_presence, + "repetition_penalty_whitelist": isNewModel ? repPenaltyAllowList : null, + "top_a": req.body.top_a, + "top_p": req.body.top_p, + "top_k": req.body.top_k, + "typical_p": req.body.typical_p, + "mirostat_lr": req.body.mirostat_lr, + "mirostat_tau": req.body.mirostat_tau, + "cfg_scale": req.body.cfg_scale, + "cfg_uc": req.body.cfg_uc, + "phrase_rep_pen": req.body.phrase_rep_pen, + "stop_sequences": req.body.stop_sequences, + "bad_words_ids": badWordsList, + "logit_bias_exp": logit_bias_exp, + "generate_until_sentence": req.body.generate_until_sentence, + "use_cache": req.body.use_cache, + "return_full_text": req.body.return_full_text, + "prefix": req.body.prefix, + "order": req.body.order + } + }; + + console.log(util.inspect(data, { depth: 4 })) + + const args = { + body: JSON.stringify(data), + headers: { "Content-Type": "application/json", "Authorization": "Bearer " + api_key_novel }, + signal: controller.signal, + }; + + try { + const url = req.body.streaming ? `${API_NOVELAI}/ai/generate-stream` : `${API_NOVELAI}/ai/generate`; + const response = await fetch(url, { method: 'POST', timeout: 0, ...args }); + + if (req.body.streaming) { + // Pipe remote SSE stream to Express response + response.body.pipe(res); + + req.socket.on('close', function () { + if (response.body instanceof Readable) response.body.destroy(); // Close the remote stream + res.end(); // End the Express response + }); + + response.body.on('end', function () { + console.log("Streaming request finished"); + res.end(); + }); + } else { + if (!response.ok) { + const text = await response.text(); + let message = text; + console.log(`Novel API returned error: ${response.status} ${response.statusText} ${text}`); + + try { + const data = JSON.parse(text); + message = data.message; + } + catch { + // ignore + } + + return res.status(response.status).send({ error: { message } }); + } + + const data = await response.json(); + console.log(data); + return res.send(data); + } + } catch (error) { + return res.send({ error: true }); + } + }); + + app.post('/api/novelai/generate-image', jsonParser, async (request, response) => { + if (!request.body) { + return response.sendStatus(400); + } + + const key = readSecret(SECRET_KEYS.NOVEL); + + if (!key) { + return response.sendStatus(401); + } + + try { + console.log('NAI Diffusion request:', request.body); + const generateUrl = `${API_NOVELAI}/ai/generate-image`; + const generateResult = await fetch(generateUrl, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${key}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + action: 'generate', + input: request.body.prompt, + model: request.body.model ?? 'nai-diffusion', + parameters: { + negative_prompt: request.body.negative_prompt ?? '', + height: request.body.height ?? 512, + width: request.body.width ?? 512, + scale: request.body.scale ?? 9, + seed: Math.floor(Math.random() * 9999999999), + sampler: request.body.sampler ?? 'k_dpmpp_2m', + steps: request.body.steps ?? 28, + n_samples: 1, + // NAI handholding for prompts + ucPreset: 0, + qualityToggle: false, + }, + }), + }); + + if (!generateResult.ok) { + console.log('NovelAI returned an error.', generateResult.statusText); + return response.sendStatus(500); + } + + const archiveBuffer = await generateResult.arrayBuffer(); + const imageBuffer = await extractFileFromZipBuffer(archiveBuffer, '.png'); + const originalBase64 = imageBuffer.toString('base64'); + + // No upscaling + if (isNaN(request.body.upscale_ratio) || request.body.upscale_ratio <= 1) { + return response.send(originalBase64); + } + + try { + console.debug('Upscaling image...'); + const upscaleUrl = `${API_NOVELAI}/ai/upscale`; + const upscaleResult = await fetch(upscaleUrl, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${key}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + image: originalBase64, + height: request.body.height, + width: request.body.width, + scale: request.body.upscale_ratio, + }), + }); + + if (!upscaleResult.ok) { + throw new Error('NovelAI returned an error.'); + } + + const upscaledArchiveBuffer = await upscaleResult.arrayBuffer(); + const upscaledImageBuffer = await extractFileFromZipBuffer(upscaledArchiveBuffer, '.png'); + const upscaledBase64 = upscaledImageBuffer.toString('base64'); + + return response.send(upscaledBase64); + } catch (error) { + console.warn('NovelAI generated an image, but upscaling failed. Returning original image.'); + return response.send(originalBase64) + } + } catch (error) { + console.log(error); + return response.sendStatus(500); + } + }); + + app.post('/api/novelai/generate-voice', jsonParser, async (request, response) => { + const token = readSecret(SECRET_KEYS.NOVEL); + + if (!token) { + return response.sendStatus(401); + } + + const text = request.body.text; + const voice = request.body.voice; + + if (!text || !voice) { + return response.sendStatus(400); + } + + try { + const url = `${API_NOVELAI}/ai/generate-voice?text=${encodeURIComponent(text)}&voice=-1&seed=${encodeURIComponent(voice)}&opus=false&version=v2`; + const result = await fetch(url, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}`, + 'Accept': 'audio/mpeg', + }, + timeout: 0, + }); + + if (!result.ok) { + return response.sendStatus(result.status); + } + + const chunks = await readAllChunks(result.body); + const buffer = Buffer.concat(chunks); + response.setHeader('Content-Type', 'audio/mpeg'); + return response.send(buffer); + } + catch (error) { + console.error(error); + return response.sendStatus(500); + } + }); +} + module.exports = { - badWordsList, - repPenaltyAllowList, - logitBiasExp, - hypeBotBadWordsList, - hypeBotLogitBiasExp, - getBadWordsList, + registerEndpoints, }; diff --git a/src/util.js b/src/util.js index 3350548fc..ed2ee4c97 100644 --- a/src/util.js +++ b/src/util.js @@ -1,7 +1,10 @@ const path = require('path'); +const fs = require('fs'); const child_process = require('child_process'); const commandExistsSync = require('command-exists').sync; const _ = require('lodash'); +const yauzl = require('yauzl'); +const mime = require('mime-types'); /** * Returns the config object from the config.conf file. @@ -77,10 +80,133 @@ function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } +/** + * Extracts a file with given extension from an ArrayBuffer containing a ZIP archive. + * @param {ArrayBuffer} archiveBuffer Buffer containing a ZIP archive + * @param {string} fileExtension File extension to look for + * @returns {Promise} Buffer containing the extracted file + */ +async function extractFileFromZipBuffer(archiveBuffer, fileExtension) { + return await new Promise((resolve, reject) => yauzl.fromBuffer(Buffer.from(archiveBuffer), { lazyEntries: true }, (err, zipfile) => { + if (err) { + reject(err); + } + + zipfile.readEntry(); + zipfile.on('entry', (entry) => { + if (entry.fileName.endsWith(fileExtension)) { + console.log(`Extracting ${entry.fileName}`); + zipfile.openReadStream(entry, (err, readStream) => { + if (err) { + reject(err); + } else { + const chunks = []; + readStream.on('data', (chunk) => { + chunks.push(chunk); + }); + + readStream.on('end', () => { + const buffer = Buffer.concat(chunks); + resolve(buffer); + zipfile.readEntry(); // Continue to the next entry + }); + } + }); + } else { + zipfile.readEntry(); + } + }); + })); +} + +/** + * Extracts all images from a ZIP archive. + * @param {string} zipFilePath Path to the ZIP archive + * @returns {Promise<[string, Buffer][]>} Array of image buffers + */ +async function getImageBuffers(zipFilePath) { + return new Promise((resolve, reject) => { + // Check if the zip file exists + if (!fs.existsSync(zipFilePath)) { + reject(new Error('File not found')); + return; + } + + const imageBuffers = []; + + yauzl.open(zipFilePath, { lazyEntries: true }, (err, zipfile) => { + if (err) { + reject(err); + } else { + zipfile.readEntry(); + zipfile.on('entry', (entry) => { + const mimeType = mime.lookup(entry.fileName); + if (mimeType && mimeType.startsWith('image/') && !entry.fileName.startsWith('__MACOSX')) { + console.log(`Extracting ${entry.fileName}`); + zipfile.openReadStream(entry, (err, readStream) => { + if (err) { + reject(err); + } else { + const chunks = []; + readStream.on('data', (chunk) => { + chunks.push(chunk); + }); + + readStream.on('end', () => { + imageBuffers.push([path.parse(entry.fileName).base, Buffer.concat(chunks)]); + zipfile.readEntry(); // Continue to the next entry + }); + } + }); + } else { + zipfile.readEntry(); // Continue to the next entry + } + }); + + zipfile.on('end', () => { + resolve(imageBuffers); + }); + + zipfile.on('error', (err) => { + reject(err); + }); + } + }); + }); +} + +/** + * Gets all chunks of data from the given readable stream. + * @param {any} readableStream Readable stream to read from + * @returns {Promise} Array of chunks + */ +async function readAllChunks(readableStream) { + return new Promise((resolve, reject) => { + // Consume the readable stream + const chunks = []; + readableStream.on('data', (chunk) => { + chunks.push(chunk); + }); + + readableStream.on('end', () => { + //console.log('Finished reading the stream.'); + resolve(chunks); + }); + + readableStream.on('error', (error) => { + console.error('Error while reading the stream:', error); + reject(); + }); + }); +} + module.exports = { getConfig, getConfigValue, getVersion, getBasicAuthHeader, + extractFileFromZipBuffer, + getImageBuffers, + readAllChunks, delay, }; diff --git a/tools/charaverter/main.mjs b/tools/charaverter/main.mjs deleted file mode 100644 index da7f3123d..000000000 --- a/tools/charaverter/main.mjs +++ /dev/null @@ -1,117 +0,0 @@ -import fs from 'fs'; -import jimp from 'jimp'; -import extract from 'png-chunks-extract'; -import encode from 'png-chunks-encode'; -import PNGtext from 'png-chunk-text'; -import ExifReader from 'exifreader'; -import webp from 'webp-converter'; -import path from 'path'; - -async function charaRead(img_url, input_format){ - let format; - if(input_format === undefined){ - if(img_url.indexOf('.webp') !== -1){ - format = 'webp'; - }else{ - format = 'png'; - } - }else{ - format = input_format; - } - - switch(format){ - case 'webp': - const exif_data = await ExifReader.load(fs.readFileSync(img_url)); - const char_data = exif_data['UserComment']['description']; - if (char_data === 'Undefined' && exif_data['UserComment'].value && exif_data['UserComment'].value.length === 1) { - return exif_data['UserComment'].value[0]; - } - return char_data; - case 'png': - const buffer = fs.readFileSync(img_url); - const chunks = extract(buffer); - - const textChunks = chunks.filter(function (chunk) { - return chunk.name === 'tEXt'; - }).map(function (chunk) { - //console.log(text.decode(chunk.data)); - return PNGtext.decode(chunk.data); - }); - var base64DecodedData = Buffer.from(textChunks[0].text, 'base64').toString('utf8'); - return base64DecodedData;//textChunks[0].text; - //console.log(textChunks[0].keyword); // 'hello' - //console.log(textChunks[0].text); // 'world' - default: - break; - } - -} - -async function charaWrite(img_url, data, target_img, response = undefined, mes = 'ok') { - try { - // Read the image, resize, and save it as a PNG into the buffer - - webp - - const rawImg = await jimp.read(img_url); - const image = await rawImg.cover(400, 600).getBufferAsync(jimp.MIME_PNG); - - // Get the chunks - const chunks = extract(image); - const tEXtChunks = chunks.filter(chunk => chunk.create_date === 'tEXt'); - - // Remove all existing tEXt chunks - for (let tEXtChunk of tEXtChunks) { - chunks.splice(chunks.indexOf(tEXtChunk), 1); - } - // Add new chunks before the IEND chunk - const base64EncodedData = Buffer.from(data, 'utf8').toString('base64'); - chunks.splice(-1, 0, PNGtext.encode('chara', base64EncodedData)); - //chunks.splice(-1, 0, text.encode('lorem', 'ipsum')); - - fs.writeFileSync(target_img, new Buffer.from(encode(chunks))); - if (response !== undefined) response.send(mes); - return true; - - - } catch (err) { - console.log(err); - if (response !== undefined) response.status(500).send(err); - return false; - } -} - - -(async function() { - const spath = process.argv[2] - const dpath = process.argv[3] || spath - const files = fs.readdirSync(spath).filter(e => e.endsWith(".webp")) - if (!files.length) { - console.log("Nothing to convert.") - return - } - - try { fs.mkdirSync(dpath) } catch {} - - for(const f of files) { - const source = path.join(spath, f), - dest = path.join(dpath, path.basename(f, ".webp") + ".png") - - console.log(`Read... ${source}`) - const data = await charaRead(source) - - console.log(`Convert... ${source} -> ${dest}`) - await webp.dwebp(source, dest, "-o") - - console.log(`Write... ${dest}`) - const success = await charaWrite(dest, data, path.parse(dest).name); - - if (!success) { - console.log(`Failure on ${source} -> ${dest}`); - continue; - } - - console.log(`Remove... ${source}`) - fs.rmSync(source) - } -})() \ No newline at end of file diff --git a/tools/charaverter/package.json b/tools/charaverter/package.json deleted file mode 100644 index 5f4db5583..000000000 --- a/tools/charaverter/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "dependencies": { - "exifreader": "^4.12.0", - "jimp": "^0.22.7", - "png-chunk-text": "^1.0.0", - "png-chunks-encode": "^1.0.0", - "png-chunks-extract": "^1.0.0", - "webp-converter": "^2.3.3" - } -}