Drop Soulkyn import (#4372) * Fix Soulkyn import Generate and supply necessary headers to Soulkyn API request. Fixes #4323 * Drop soulkyn import --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>

0e5b509dc321661489907e2c188230b9e67cbe8b

buzz <buzz@users.noreply.github.com>

Signed
17 files changed, +3 -207Ignore whitespace
public/locales/ar-sa.json+0 -1
@@ -1376,7 +1376,6 @@
1376 "char_import_6": "رابط PNG المباشر (راجع",1376 "char_import_6": "رابط PNG المباشر (راجع",
1377 "char_import_7": "للمضيفين المسموح بهم)",1377 "char_import_7": "للمضيفين المسموح بهم)",
1378 "char_import_8": "شخصية RisuRealm (رابط مباشر)",1378 "char_import_8": "شخصية RisuRealm (رابط مباشر)",
1379 "char_import_9": "شخصية Soulkyn (رابط مباشر)",
1380 "char_import_10": "شخصية Perchance (رابط مباشر أو UUID + .gz)",1379 "char_import_10": "شخصية Perchance (رابط مباشر أو UUID + .gz)",
1381 "Supports importing multiple characters.": "يدعم استيراد أحرف متعددة.",1380 "Supports importing multiple characters.": "يدعم استيراد أحرف متعددة.",
1382 "Write each URL or ID into a new line.": "اكتب كل عنوان URL أو معرف في سطر جديد.",1381 "Write each URL or ID into a new line.": "اكتب كل عنوان URL أو معرف في سطر جديد.",
public/locales/de-de.json+0 -1
@@ -1376,7 +1376,6 @@
1376 "char_import_6": "Direkter PNG-Link (siehe",1376 "char_import_6": "Direkter PNG-Link (siehe",
1377 "char_import_7": "für erlaubte Hosts)",1377 "char_import_7": "für erlaubte Hosts)",
1378 "char_import_8": "RisuRealm-Charakter (Direktlink)",1378 "char_import_8": "RisuRealm-Charakter (Direktlink)",
1379 "char_import_9": "Soulkyn-Charakter (Direktlink)",
1380 "char_import_10": "Perchance-Charakter (Direktlink oder UUID + .gz)",1379 "char_import_10": "Perchance-Charakter (Direktlink oder UUID + .gz)",
1381 "Supports importing multiple characters.": "Unterstützt den Import mehrerer Zeichen.",1380 "Supports importing multiple characters.": "Unterstützt den Import mehrerer Zeichen.",
1382 "Write each URL or ID into a new line.": "Schreiben Sie jede URL oder ID in eine neue Zeile.",1381 "Write each URL or ID into a new line.": "Schreiben Sie jede URL oder ID in eine neue Zeile.",
public/locales/es-es.json+0 -1
@@ -1376,7 +1376,6 @@
1376 "char_import_6": "Enlace PNG directo (consulte",1376 "char_import_6": "Enlace PNG directo (consulte",
1377 "char_import_7": "para hosts permitidos)",1377 "char_import_7": "para hosts permitidos)",
1378 "char_import_8": "Personaje RisuRealm (Enlace directo)",1378 "char_import_8": "Personaje RisuRealm (Enlace directo)",
1379 "char_import_9": "Personaje Soulkyn (Enlace directo)",
1380 "char_import_10": "Personaje Perchance (enlace directo o UUID + .gz)",1379 "char_import_10": "Personaje Perchance (enlace directo o UUID + .gz)",
1381 "Supports importing multiple characters.": "Admite la importación de múltiples caracteres.",1380 "Supports importing multiple characters.": "Admite la importación de múltiples caracteres.",
1382 "Write each URL or ID into a new line.": "Escriba cada URL o ID en una nueva línea.",1381 "Write each URL or ID into a new line.": "Escriba cada URL o ID en una nueva línea.",
public/locales/fr-fr.json+0 -1
@@ -1298,7 +1298,6 @@
1298 "char_import_6": "Lien PNG direct (voir",1298 "char_import_6": "Lien PNG direct (voir",
1299 "char_import_7": "pour les hôtes autorisés)",1299 "char_import_7": "pour les hôtes autorisés)",
1300 "char_import_8": "Personnage de RisuRealm (lien direct)",1300 "char_import_8": "Personnage de RisuRealm (lien direct)",
1301 "char_import_9": "Personnage de Soulkyn (lien direct)",
1302 "char_import_10": "Personnage de Perchance (lien direct ou UUID + .gz)",1301 "char_import_10": "Personnage de Perchance (lien direct ou UUID + .gz)",
1303 "Supports importing multiple characters.": "Prend en charge l'importation de plusieurs caractères.",1302 "Supports importing multiple characters.": "Prend en charge l'importation de plusieurs caractères.",
1304 "Write each URL or ID into a new line.": "Écrivez chaque URL ou identifiant dans une nouvelle ligne.",1303 "Write each URL or ID into a new line.": "Écrivez chaque URL ou identifiant dans une nouvelle ligne.",
public/locales/is-is.json+0 -1
@@ -1376,7 +1376,6 @@
1376 "char_import_6": "Beinn PNG hlekkur (sjá",1376 "char_import_6": "Beinn PNG hlekkur (sjá",
1377 "char_import_7": "fyrir leyfilega gestgjafa)",1377 "char_import_7": "fyrir leyfilega gestgjafa)",
1378 "char_import_8": "RisuRealm karakter (beinn hlekkur)",1378 "char_import_8": "RisuRealm karakter (beinn hlekkur)",
1379 "char_import_9": "Soulkyn karakter (beinn hlekkur)",
1380 "char_import_10": "Perchance karakter (beinn hlekkur eða UUID + .gz)",1379 "char_import_10": "Perchance karakter (beinn hlekkur eða UUID + .gz)",
1381 "Supports importing multiple characters.": "Styður innflutning á mörgum stöfum.",1380 "Supports importing multiple characters.": "Styður innflutning á mörgum stöfum.",
1382 "Write each URL or ID into a new line.": "Skrifaðu hverja vefslóð eða auðkenni í nýja línu.",1381 "Write each URL or ID into a new line.": "Skrifaðu hverja vefslóð eða auðkenni í nýja línu.",
public/locales/it-it.json+0 -1
@@ -1376,7 +1376,6 @@
1376 "char_import_6": "Collegamento PNG diretto (fare riferimento a",1376 "char_import_6": "Collegamento PNG diretto (fare riferimento a",
1377 "char_import_7": "per gli host consentiti)",1377 "char_import_7": "per gli host consentiti)",
1378 "char_import_8": "Personaggio RisuRealm (collegamento diretto)",1378 "char_import_8": "Personaggio RisuRealm (collegamento diretto)",
1379 "char_import_9": "Personaggio Soulkyn (collegamento diretto)",
1380 "char_import_10": "Perchance Personaggio (collegamento diretto o UUID + .gz)",1379 "char_import_10": "Perchance Personaggio (collegamento diretto o UUID + .gz)",
1381 "Supports importing multiple characters.": "Supporta l'importazione di più caratteri.",1380 "Supports importing multiple characters.": "Supporta l'importazione di più caratteri.",
1382 "Write each URL or ID into a new line.": "Scrivi ogni URL o ID in una nuova riga.",1381 "Write each URL or ID into a new line.": "Scrivi ogni URL o ID in una nuova riga.",
public/locales/ja-jp.json+0 -1
@@ -1378,7 +1378,6 @@
1378 "char_import_6": "直接PNGリンク(参照",1378 "char_import_6": "直接PNGリンク(参照",
1379 "char_import_7": "許可されたホストの場合)",1379 "char_import_7": "許可されたホストの場合)",
1380 "char_import_8": "RisuRealm キャラクター (直接リンク)",1380 "char_import_8": "RisuRealm キャラクター (直接リンク)",
1381 "char_import_9": "Soulkyn キャラクター (直接リンク)",
1382 "char_import_10": "Perchance キャラクター (直接リンクまたは UUID + .gz)",1381 "char_import_10": "Perchance キャラクター (直接リンクまたは UUID + .gz)",
1383 "Supports importing multiple characters.": "複数のキャラクターのインポートをサポートします。",1382 "Supports importing multiple characters.": "複数のキャラクターのインポートをサポートします。",
1384 "Write each URL or ID into a new line.": "各 URL または ID を新しい行に入力します。",1383 "Write each URL or ID into a new line.": "各 URL または ID を新しい行に入力します。",
public/locales/ko-kr.json+0 -1
@@ -1395,7 +1395,6 @@
1395 "char_import_6": "직접 PNG 링크(참조",1395 "char_import_6": "직접 PNG 링크(참조",
1396 "char_import_7": "허용된 호스트의 경우)",1396 "char_import_7": "허용된 호스트의 경우)",
1397 "char_import_8": "RisuRealm 캐릭터 (직접링크)",1397 "char_import_8": "RisuRealm 캐릭터 (직접링크)",
1398 "char_import_9": "Soulkyn 캐릭터 (직접링크)",
1399 "char_import_10": "Perchance 캐릭터 (직접 링크 또는 UUID + .gz)",1398 "char_import_10": "Perchance 캐릭터 (직접 링크 또는 UUID + .gz)",
1400 "Supports importing multiple characters.": "여러 문자 가져오기를 지원합니다.",1399 "Supports importing multiple characters.": "여러 문자 가져오기를 지원합니다.",
1401 "Write each URL or ID into a new line.": "각 URL 또는 ID를 새 줄에 작성합니다.",1400 "Write each URL or ID into a new line.": "각 URL 또는 ID를 새 줄에 작성합니다.",
public/locales/nl-nl.json+0 -1
@@ -1376,7 +1376,6 @@
1376 "char_import_6": "Directe PNG-link (zie",1376 "char_import_6": "Directe PNG-link (zie",
1377 "char_import_7": "voor toegestane hosts)",1377 "char_import_7": "voor toegestane hosts)",
1378 "char_import_8": "RisuRealm-personage (directe link)",1378 "char_import_8": "RisuRealm-personage (directe link)",
1379 "char_import_9": "Soulkyn-personage (directe link)",
1380 "char_import_10": "Perchance-personage (directe link of UUID + .gz)",1379 "char_import_10": "Perchance-personage (directe link of UUID + .gz)",
1381 "Supports importing multiple characters.": "Ondersteunt het importeren van meerdere tekens.",1380 "Supports importing multiple characters.": "Ondersteunt het importeren van meerdere tekens.",
1382 "Write each URL or ID into a new line.": "Schrijf elke URL of ID op een nieuwe regel.",1381 "Write each URL or ID into a new line.": "Schrijf elke URL of ID op een nieuwe regel.",
public/locales/pt-pt.json+0 -1
@@ -1376,7 +1376,6 @@
1376 "char_import_6": "Link PNG direto (consulte",1376 "char_import_6": "Link PNG direto (consulte",
1377 "char_import_7": "para hosts permitidos)",1377 "char_import_7": "para hosts permitidos)",
1378 "char_import_8": "Personagem RisuRealm (link direto)",1378 "char_import_8": "Personagem RisuRealm (link direto)",
1379 "char_import_9": "Personagem Soulkyn (link direto)",
1380 "char_import_10": "Personagem Perchance (Link Direto ou UUID + .gz)",1379 "char_import_10": "Personagem Perchance (Link Direto ou UUID + .gz)",
1381 "Supports importing multiple characters.": "Suporta importação de vários caracteres.",1380 "Supports importing multiple characters.": "Suporta importação de vários caracteres.",
1382 "Write each URL or ID into a new line.": "Escreva cada URL ou ID em uma nova linha.",1381 "Write each URL or ID into a new line.": "Escreva cada URL ou ID em uma nova linha.",
public/locales/ru-ru.json+0 -1
@@ -963,7 +963,6 @@
963 "char_import_5": "Персонаж с AICharacterCards.com (прямая ссылка или ID)",963 "char_import_5": "Персонаж с AICharacterCards.com (прямая ссылка или ID)",
964 "char_import_6": "Прямая ссылка на PNG-файл (список разрешённых хостов находится в",964 "char_import_6": "Прямая ссылка на PNG-файл (список разрешённых хостов находится в",
965 "char_import_7": ")",965 "char_import_7": ")",
966 "char_import_9": "Персонаж с Soulkyn (прямая ссылка)",
967 "char_import_10": "Персонаж с Perchance (прямая ссылка или UUID + .gz)",966 "char_import_10": "Персонаж с Perchance (прямая ссылка или UUID + .gz)",
968 "Grammar String": "Грамматика",967 "Grammar String": "Грамматика",
969 "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF или EBNF, зависит от бэкенда. Если вы это используете, то, скорее всего, сами знаете, какой именно.",968 "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF или EBNF, зависит от бэкенда. Если вы это используете, то, скорее всего, сами знаете, какой именно.",
public/locales/uk-ua.json+0 -1
@@ -1376,7 +1376,6 @@
1376 "char_import_6": "Пряме посилання на PNG (див",1376 "char_import_6": "Пряме посилання на PNG (див",
1377 "char_import_7": "для дозволених хостів)",1377 "char_import_7": "для дозволених хостів)",
1378 "char_import_8": "Персонаж RisuRealm (пряме посилання)",1378 "char_import_8": "Персонаж RisuRealm (пряме посилання)",
1379 "char_import_9": "Персонаж Soulkyn (пряме посилання)",
1380 "char_import_10": "Персонаж Perchance (пряме посилання або UUID + .gz)",1379 "char_import_10": "Персонаж Perchance (пряме посилання або UUID + .gz)",
1381 "Supports importing multiple characters.": "Підтримується імпорт кількох символів.",1380 "Supports importing multiple characters.": "Підтримується імпорт кількох символів.",
1382 "Write each URL or ID into a new line.": "Напишіть кожну URL-адресу або ідентифікатор у новому рядку.",1381 "Write each URL or ID into a new line.": "Напишіть кожну URL-адресу або ідентифікатор у новому рядку.",
public/locales/vi-vn.json+0 -1
@@ -1376,7 +1376,6 @@
1376 "char_import_6": "Nhập PNG trực tiếp (tham khảo",1376 "char_import_6": "Nhập PNG trực tiếp (tham khảo",
1377 "char_import_7": "đối với các máy chủ được phép)",1377 "char_import_7": "đối với các máy chủ được phép)",
1378 "char_import_8": "RisuRealm (URL trực tiếp)",1378 "char_import_8": "RisuRealm (URL trực tiếp)",
1379 "char_import_9": "Soulkyn (URL trực tiếp)",
1380 "char_import_10": "Perchance (Nhập URL trực tiếp hoặc UUID + .gz)",1379 "char_import_10": "Perchance (Nhập URL trực tiếp hoặc UUID + .gz)",
1381 "Supports importing multiple characters.": "Hỗ trợ nhập nhiều ký tự.",1380 "Supports importing multiple characters.": "Hỗ trợ nhập nhiều ký tự.",
1382 "Write each URL or ID into a new line.": "Viết mỗi URL hoặc ID vào một dòng mới.",1381 "Write each URL or ID into a new line.": "Viết mỗi URL hoặc ID vào một dòng mới.",
public/locales/zh-cn.json+0 -1
@@ -1888,7 +1888,6 @@
1888 "char_import_6": "被允许的PNG直链(请参阅",1888 "char_import_6": "被允许的PNG直链(请参阅",
1889 "char_import_7": ")",1889 "char_import_7": ")",
1890 "char_import_8": "RisuRealm 角色(直链)",1890 "char_import_8": "RisuRealm 角色(直链)",
1891 "char_import_9": "Soulkyn 角色(直链)",
1892 "char_import_10": "Perchance 角色(直链或UUID + .gz)",1891 "char_import_10": "Perchance 角色(直链或UUID + .gz)",
1893 "Supports importing multiple characters.": "支持导入多个角色。",1892 "Supports importing multiple characters.": "支持导入多个角色。",
1894 "Write each URL or ID into a new line.": "将每个 URL 或 ID 写入新行。",1893 "Write each URL or ID into a new line.": "将每个 URL 或 ID 写入新行。",
public/locales/zh-tw.json+0 -1
@@ -1381,7 +1381,6 @@
1381 "char_import_6": "直接 PNG 連結(請參閱",1381 "char_import_6": "直接 PNG 連結(請參閱",
1382 "char_import_7": "對於允許的主機)",1382 "char_import_7": "對於允許的主機)",
1383 "char_import_8": "RisuRealm 角色(直接連結)",1383 "char_import_8": "RisuRealm 角色(直接連結)",
1384 "char_import_9": "Soulkyn 角色(直接連結)",
1385 "char_import_10": "Perchance 角色(直接連結或 ID + .gz)",1384 "char_import_10": "Perchance 角色(直接連結或 ID + .gz)",
1386 "Supports importing multiple characters.": "支援匯入多個字元。",1385 "Supports importing multiple characters.": "支援匯入多個字元。",
1387 "Write each URL or ID into a new line.": "將每個 URL 或 ID 寫入新行。",1386 "Write each URL or ID into a new line.": "將每個 URL 或 ID 寫入新行。",
public/scripts/templates/importCharacters.html+0 -1
@@ -10,7 +10,6 @@
10 <li><span data-i18n="char_import_5">AICharacterCards.com Character (Direct Link or ID)</span><br><span data-i18n="char_import_example">Example:</span> <tt>AICC/aicharcards/the-game-master</tt></li>10 <li><span data-i18n="char_import_5">AICharacterCards.com Character (Direct Link or ID)</span><br><span data-i18n="char_import_example">Example:</span> <tt>AICC/aicharcards/the-game-master</tt></li>
11 <li><span data-i18n="char_import_6">Direct PNG Link (refer to</span> <code>config.yaml</code><span data-i18n="char_import_7"> for allowed hosts)</span><br><span data-i18n="char_import_example">Example:</span> <tt>https://files.catbox.moe/notarealfile.png</tt></li>11 <li><span data-i18n="char_import_6">Direct PNG Link (refer to</span> <code>config.yaml</code><span data-i18n="char_import_7"> for allowed hosts)</span><br><span data-i18n="char_import_example">Example:</span> <tt>https://files.catbox.moe/notarealfile.png</tt></li>
12 <li><span data-i18n="char_import_8">RisuRealm Character (Direct Link)</span><br><span data-i18n="char_import_example">Example:</span> <tt>https://realm.risuai.net/character/3ca54c71-6efe-46a2-b9d0-4f62df23d712</tt></li>12 <li><span data-i18n="char_import_8">RisuRealm Character (Direct Link)</span><br><span data-i18n="char_import_example">Example:</span> <tt>https://realm.risuai.net/character/3ca54c71-6efe-46a2-b9d0-4f62df23d712</tt></li>
13 <li><span data-i18n="char_import_9">Soulkyn Character (Direct Link)</span><br><span data-i18n="char_import_example">Example:</span> <tt>https://soulkyn.com/l/en-US/@haruka-509al</tt></li>
14 <li><span data-i18n="char_import_10">Perchance Character (Direct Link or UUID + .gz)</span><br><span data-i18n="char_import_example">Example:</span> <tt>https://perchance.org/ai-character-chat?data=Loreena~cb3a1b531477378db7cad0148ba62d71.gz</tt><br><span data-i18n="char_import_example">Example:</span> <tt>Loreena~cb3a1b531477378db7cad0148ba62d71.gz</tt></li>13 <li><span data-i18n="char_import_10">Perchance Character (Direct Link or UUID + .gz)</span><br><span data-i18n="char_import_example">Example:</span> <tt>https://perchance.org/ai-character-chat?data=Loreena~cb3a1b531477378db7cad0148ba62d71.gz</tt><br><span data-i18n="char_import_example">Example:</span> <tt>Loreena~cb3a1b531477378db7cad0148ba62d71.gz</tt></li>
15 </ul>14 </ul>
16</div>15</div>
src/endpoints/content-manager.js+3 -191
@@ -639,187 +639,6 @@ async function downloadRisuCharacter(uuid) {
639 return { buffer, fileName, fileType };639 return { buffer, fileName, fileType };
640}640}
641641
642/**
643 * Parse Soulkyn URL to extract the character slug.
644 * @param {string} url Soulkyn character URL
645 * @returns {string | null} Slug of the character
646 */
647function parseSoulkynUrl(url) {
648 // Example: https://soulkyn.com/l/en-US/@kayla-marie
649 const pattern = /^https:\/\/soulkyn\.com\/l\/[a-z]{2}-[A-Z]{2}\/@([\w\d-]+)/i;
650 const match = url.match(pattern);
651 return match ? match[1] : null;
652}
653
654/**
655 * Download Soulkyn character card
656 * @param {string} slug Slug of the character
657 * @returns {Promise<{buffer: Buffer, fileName: string, fileType: string} | null>}
658 */
659async function downloadSoulkynCharacter(slug) {
660 const soulkynReplacements = [
661 // https://soulkyn.com/l/en-US/help/character-backgrounds-advanced#variables-you-can-use-in-character-background-text
662 { pattern: /__USER_?NAME__/gi, replacement: '{{user}}' },
663 { pattern: /__PERSONA_?NAME__/gi, replacement: '{{char}}' },
664 // ST doesn't support gender-specific pronoun macros
665 { pattern: /__U_PRONOUN_1__/gi, replacement: 'they' },
666 { pattern: /__U_PRONOUN_2__/gi, replacement: 'them' },
667 { pattern: /__U_PRONOUN_3__/gi, replacement: 'their' },
668 { pattern: /__U_PRONOUN_4__/gi, replacement: 'themselves' },
669 { pattern: /__(USER_)?PRONOUN__/gi, replacement: 'they' },
670 { pattern: /__(USER_)?CPRONOUN__/gi, replacement: 'them' },
671 { pattern: /__(USER_)?UPRONOUN__/gi, replacement: 'their' },
672 // HTML tags -> Markdown syntax
673 { pattern: /<(strong|b)>/gi, replacement: '**' },
674 { pattern: /<\/(strong|b)>/gi, replacement: '**' },
675 { pattern: /<(em|i)>/gi, replacement: '*' },
676 { pattern: /<\/(em|i)>/gi, replacement: '*' },
677 ];
678
679 const normalizeContent = (str) => soulkynReplacements.reduce((acc, { pattern, replacement }) => acc.replace(pattern, replacement), str);
680
681 try {
682 const url = `https://soulkyn.com/_special/rest/Sk/public/Persona/${slug}`;
683 const result = await fetch(url, {
684 headers: { 'Content-Type': 'application/json', 'User-Agent': USER_AGENT },
685 });
686 if (result.ok) {
687 /** @type {any} */
688 const soulkynCharData = await result.json();
689
690 if (soulkynCharData.result !== 'success') {
691 console.error('Soulkyn returned error', soulkynCharData.message);
692 throw new Error(`Failed to download character: ${soulkynCharData.message}`);
693 }
694
695 // Fetch avatar
696 let avatarBuffer = null;
697 if (soulkynCharData.data?.Avatar?.FWSUUID) {
698 const avatarUrl = `https://rub.soulkyn.com/${soulkynCharData.data.Avatar.FWSUUID}/`;
699 const avatarResult = await fetch(avatarUrl, { headers: { 'User-Agent': USER_AGENT } });
700
701 if (avatarResult.ok) {
702 const avatarContentType = avatarResult.headers.get('content-type');
703 if (avatarContentType === 'image/png') {
704 avatarBuffer = Buffer.from(await avatarResult.arrayBuffer());
705 } else {
706 console.warn(`Soulkyn character (${slug}) avatar is not PNG: ${avatarContentType}`);
707 }
708 } else {
709 console.warn(`Soulkyn character (${slug}) avatar download failed: ${avatarResult.status}`);
710 }
711 } else {
712 console.warn(`Soulkyn character (${slug}) does not have an avatar`);
713 }
714
715 // Fallback to default avatar
716 if (!avatarBuffer) {
717 const defaultAvatarPath = path.join(serverDirectory, DEFAULT_AVATAR_PATH);
718 avatarBuffer = fs.readFileSync(defaultAvatarPath);
719 }
720
721 const d = soulkynCharData.data;
722 soulkynReplacements.push({ pattern: d.Username, replacement: '{{char}}' });
723
724 // Parse Soulkyn data into character chard
725 const charData = {
726 name: d.Username,
727 first_mes: '',
728 tags: [],
729 description: '',
730 creator: d.User.Username,
731 creator_notes: '',
732 alternate_greetings: [],
733 character_version: '',
734 mes_example: '',
735 post_history_instructions: '',
736 system_prompt: '',
737 scenario: '',
738 personality: '',
739 extensions: {
740 soulkyn_slug: slug,
741 soulkyn_id: d.UUID,
742 },
743 };
744
745 if (d?.PersonaIntroText) {
746 const match = d.PersonaIntroText.match(/^(?:\[Scenario:\s*([\s\S]*?)\]\s*)?([\s\S]*)$/);
747 if (match) {
748 if (match[1]) {
749 charData.scenario = normalizeContent(match[1].trim());
750 }
751 charData.first_mes = normalizeContent(match[2].trim());
752 }
753 }
754
755 const descriptionArr = ['Name: {{char}}'];
756 if (d?.Version?.Age) {
757 descriptionArr.push(`Age: ${d.Version.Age}`);
758 }
759 if (d?.Version?.Gender) {
760 descriptionArr.push(`Gender: ${d.Version.Gender}`);
761 }
762 if (d?.Version?.Race?.Name && !d.Version.Race.Name.match(/no preset/i)) {
763 let race = d.Version.Race.Name;
764 if (d.Version.Race?.Description) {
765 race += ` (${d.Version.Race.Description})`;
766 }
767 descriptionArr.push(`Race: ${race}`);
768 }
769 if (d?.PersonalityType) {
770 descriptionArr.push(`Personality type: ${d.PersonalityType}`);
771 }
772 if (Array.isArray(d?.Version?.PropertyPersonality)) {
773 const traits = d.Version.PropertyPersonality.map((t) => t.Value).join(', ');
774 descriptionArr.push(`Personality Traits: ${traits}`);
775 }
776 if (Array.isArray(d?.Version?.PropertyPhysical)) {
777 const traits = d.Version.PropertyPhysical.map((t) => t.Value).join(', ');
778 descriptionArr.push(`Physical Traits: ${traits}`);
779 }
780 if (Array.isArray(d?.Clothes?.Preset)) {
781 descriptionArr.push(`Clothes: ${d.Clothes.Preset.join(', ')}`);
782 }
783 if (d?.Avatar?.Caption) {
784 descriptionArr.push(`Image description featuring {{char}}: ${d.Avatar.Caption.replace(/\n+/g, ' ')}`);
785 }
786 if (d?.Version?.WelcomeMessage) {
787 if (charData.first_mes) {
788 descriptionArr.push(`{{char}}'s self-description: "${d.Version.WelcomeMessage}"`);
789 } else {
790 // Some characters lack `PersonaIntroText`. In that case we use `Version.WelcomeMessage` for `first_mes`
791 charData.first_mes = normalizeContent(d.Version.WelcomeMessage);
792 }
793 }
794 charData.description = normalizeContent(descriptionArr.join('\n'));
795
796 if (Array.isArray(d?.Version?.ChatExamplesValue)) {
797 charData.mes_example = d.Version.ChatExamplesValue.map((example) => `<START>\n${normalizeContent(example)}`).join('\n');
798 }
799
800 if (Array.isArray(d?.PersonaTags)) {
801 charData.tags = d.PersonaTags.map((t) => t.Slug);
802 }
803
804 // Character card
805 const buffer = write(avatarBuffer, JSON.stringify({
806 'spec': 'chara_card_v2',
807 'spec_version': '2.0',
808 'data': charData,
809 }));
810
811 const fileName = `${sanitize(d.UUID)}.png`;
812 const fileType = 'image/png';
813
814 return { buffer, fileName, fileType };
815 }
816 } catch (error) {
817 console.error('Error downloading character:', error);
818 throw error;
819 }
820 return null;
821}
822
823/** * Check if the given string is a valid Perchance UUID.642/** * Check if the given string is a valid Perchance UUID.
824 * @param {string} uuid UUID string to check643 * @param {string} uuid UUID string to check
825 * @returns {boolean} True if the UUID is valid, false otherwise644 * @returns {boolean} True if the UUID is valid, false otherwise
@@ -921,13 +740,14 @@ async function downloadPerchanceCharacter(slug) {
921 return null;740 return null;
922}741}
923742
924/** * Extracts Perchance character data from a gzipped response.743/**
744 * Extracts Perchance character data from a gzipped response.
925 * @param {import('node-fetch').Response} result Fetch response containing gzipped character data745 * @param {import('node-fetch').Response} result Fetch response containing gzipped character data
926 * @returns {Promise<Object>} Parsed Perchance character data746 * @returns {Promise<Object>} Parsed Perchance character data
927 * @throws {Error} If the character data is invalid or missing required fields747 * @throws {Error} If the character data is invalid or missing required fields
928 */748 */
929async function extractPerchanceCharacterFromGz(result) {749async function extractPerchanceCharacterFromGz(result) {
930 const compressedBuffer = Buffer.from(await result.arrayBuffer());750 const compressedBuffer = await result.arrayBuffer();
931 const decompressedBuffer = zlib.gunzipSync(compressedBuffer);751 const decompressedBuffer = zlib.gunzipSync(compressedBuffer);
932752
933 // inside the gz file, there is a file of the same name without extensions, but it is a json file753 // inside the gz file, there is a file of the same name without extensions, but it is a json file
@@ -1062,7 +882,6 @@ router.post('/importURL', async (request, response) => {
1062 const isPygmalionContent = host.includes('pygmalion.chat');882 const isPygmalionContent = host.includes('pygmalion.chat');
1063 const isAICharacterCardsContent = host.includes('aicharactercards.com');883 const isAICharacterCardsContent = host.includes('aicharactercards.com');
1064 const isRisu = host.includes('realm.risuai.net');884 const isRisu = host.includes('realm.risuai.net');
1065 const isSoulkyn = host.includes('soulkyn.com');
1066 const isPerchance = host.includes('perchance.org');885 const isPerchance = host.includes('perchance.org');
1067 const isGeneric = isHostWhitelisted(host);886 const isGeneric = isHostWhitelisted(host);
1068887
@@ -1112,13 +931,6 @@ router.post('/importURL', async (request, response) => {
1112931
1113 type = 'character';932 type = 'character';
1114 result = await downloadRisuCharacter(uuid);933 result = await downloadRisuCharacter(uuid);
1115 } else if (isSoulkyn) {
1116 const soulkynSlug = parseSoulkynUrl(url);
1117 if (!soulkynSlug) {
1118 return response.sendStatus(404);
1119 }
1120 type = 'character';
1121 result = await downloadSoulkynCharacter(soulkynSlug);
1122 } else if (isPerchance) {934 } else if (isPerchance) {
1123 const perchanceSlug = parsePerchanceSlug(url);935 const perchanceSlug = parsePerchanceSlug(url);
1124 if (!perchanceSlug) {936 if (!perchanceSlug) {