| @@ -1379,6 +1379,7 @@ | |||
| 1379 | "char_import_6": "رابط PNG المباشر (راجع", | 1379 | "char_import_6": "رابط PNG المباشر (راجع", |
| 1380 | "char_import_7": "للمضيفين المسموح بهم)", | 1380 | "char_import_7": "للمضيفين المسموح بهم)", |
| 1381 | "char_import_8": "شخصية RisuRealm (رابط مباشر)", | 1381 | "char_import_8": "شخصية RisuRealm (رابط مباشر)", |
| 1382 | "char_import_9": "شخصية Soulkyn (رابط مباشر)", | ||
| 1382 | "Supports importing multiple characters.": "يدعم استيراد أحرف متعددة.", | 1383 | "Supports importing multiple characters.": "يدعم استيراد أحرف متعددة.", |
| 1383 | "Write each URL or ID into a new line.": "اكتب كل عنوان URL أو معرف في سطر جديد.", | 1384 | "Write each URL or ID into a new line.": "اكتب كل عنوان URL أو معرف في سطر جديد.", |
| 1384 | "Export for character": "تصدير للشخصية", | 1385 | "Export for character": "تصدير للشخصية", |
| @@ -1379,6 +1379,7 @@ | |||
| 1379 | "char_import_6": "Direkter PNG-Link (siehe", | 1379 | "char_import_6": "Direkter PNG-Link (siehe", |
| 1380 | "char_import_7": "für erlaubte Hosts)", | 1380 | "char_import_7": "für erlaubte Hosts)", |
| 1381 | "char_import_8": "RisuRealm-Charakter (Direktlink)", | 1381 | "char_import_8": "RisuRealm-Charakter (Direktlink)", |
| 1382 | "char_import_9": "Soulkyn-Charakter (Direktlink)", | ||
| 1382 | "Supports importing multiple characters.": "Unterstützt den Import mehrerer Zeichen.", | 1383 | "Supports importing multiple characters.": "Unterstützt den Import mehrerer Zeichen.", |
| 1383 | "Write each URL or ID into a new line.": "Schreiben Sie jede URL oder ID in eine neue Zeile.", | 1384 | "Write each URL or ID into a new line.": "Schreiben Sie jede URL oder ID in eine neue Zeile.", |
| 1384 | "Export for character": "Export für Zeichen", | 1385 | "Export for character": "Export für Zeichen", |
| @@ -1379,6 +1379,7 @@ | |||
| 1379 | "char_import_6": "Enlace PNG directo (consulte", | 1379 | "char_import_6": "Enlace PNG directo (consulte", |
| 1380 | "char_import_7": "para hosts permitidos)", | 1380 | "char_import_7": "para hosts permitidos)", |
| 1381 | "char_import_8": "Personaje RisuRealm (Enlace directo)", | 1381 | "char_import_8": "Personaje RisuRealm (Enlace directo)", |
| 1382 | "char_import_9": "Personaje Soulkyn (Enlace directo)", | ||
| 1382 | "Supports importing multiple characters.": "Admite la importación de múltiples caracteres.", | 1383 | "Supports importing multiple characters.": "Admite la importación de múltiples caracteres.", |
| 1383 | "Write each URL or ID into a new line.": "Escriba cada URL o ID en una nueva línea.", | 1384 | "Write each URL or ID into a new line.": "Escriba cada URL o ID en una nueva línea.", |
| 1384 | "Export for character": "Exportar para personaje", | 1385 | "Export for character": "Exportar para personaje", |
| @@ -1301,6 +1301,7 @@ | |||
| 1301 | "char_import_6": "Lien PNG direct (voir", | 1301 | "char_import_6": "Lien PNG direct (voir", |
| 1302 | "char_import_7": "pour les hôtes autorisés)", | 1302 | "char_import_7": "pour les hôtes autorisés)", |
| 1303 | "char_import_8": "Personnage de RisuRealm (lien direct)", | 1303 | "char_import_8": "Personnage de RisuRealm (lien direct)", |
| 1304 | "char_import_9": "Personnage de Soulkyn (lien direct)", | ||
| 1304 | "Supports importing multiple characters.": "Prend en charge l'importation de plusieurs caractères.", | 1305 | "Supports importing multiple characters.": "Prend en charge l'importation de plusieurs caractères.", |
| 1305 | "Write each URL or ID into a new line.": "Écrivez chaque URL ou identifiant dans une nouvelle ligne.", | 1306 | "Write each URL or ID into a new line.": "Écrivez chaque URL ou identifiant dans une nouvelle ligne.", |
| 1306 | "Export for character": "Exportation pour le personnage", | 1307 | "Export for character": "Exportation pour le personnage", |
| @@ -1379,6 +1379,7 @@ | |||
| 1379 | "char_import_6": "Beinn PNG hlekkur (sjá", | 1379 | "char_import_6": "Beinn PNG hlekkur (sjá", |
| 1380 | "char_import_7": "fyrir leyfilega gestgjafa)", | 1380 | "char_import_7": "fyrir leyfilega gestgjafa)", |
| 1381 | "char_import_8": "RisuRealm karakter (beinn hlekkur)", | 1381 | "char_import_8": "RisuRealm karakter (beinn hlekkur)", |
| 1382 | "char_import_9": "Soulkyn karakter (beinn hlekkur)", | ||
| 1382 | "Supports importing multiple characters.": "Styður innflutning á mörgum stöfum.", | 1383 | "Supports importing multiple characters.": "Styður innflutning á mörgum stöfum.", |
| 1383 | "Write each URL or ID into a new line.": "Skrifaðu hverja vefslóð eða auðkenni í nýja línu.", | 1384 | "Write each URL or ID into a new line.": "Skrifaðu hverja vefslóð eða auðkenni í nýja línu.", |
| 1384 | "Export for character": "Flytja út fyrir persónu", | 1385 | "Export for character": "Flytja út fyrir persónu", |
| @@ -1379,6 +1379,7 @@ | |||
| 1379 | "char_import_6": "Collegamento PNG diretto (fare riferimento a", | 1379 | "char_import_6": "Collegamento PNG diretto (fare riferimento a", |
| 1380 | "char_import_7": "per gli host consentiti)", | 1380 | "char_import_7": "per gli host consentiti)", |
| 1381 | "char_import_8": "Personaggio RisuRealm (collegamento diretto)", | 1381 | "char_import_8": "Personaggio RisuRealm (collegamento diretto)", |
| 1382 | "char_import_9": "Personaggio Soulkyn (collegamento diretto)", | ||
| 1382 | "Supports importing multiple characters.": "Supporta l'importazione di più caratteri.", | 1383 | "Supports importing multiple characters.": "Supporta l'importazione di più caratteri.", |
| 1383 | "Write each URL or ID into a new line.": "Scrivi ogni URL o ID in una nuova riga.", | 1384 | "Write each URL or ID into a new line.": "Scrivi ogni URL o ID in una nuova riga.", |
| 1384 | "Export for character": "Esporta per carattere", | 1385 | "Export for character": "Esporta per carattere", |
| @@ -1381,6 +1381,7 @@ | |||
| 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 キャラクター (直接リンク)", | ||
| 1384 | "Supports importing multiple characters.": "複数のキャラクターのインポートをサポートします。", | 1385 | "Supports importing multiple characters.": "複数のキャラクターのインポートをサポートします。", |
| 1385 | "Write each URL or ID into a new line.": "各 URL または ID を新しい行に入力します。", | 1386 | "Write each URL or ID into a new line.": "各 URL または ID を新しい行に入力します。", |
| 1386 | "Export for character": "キャラクターのエクスポート", | 1387 | "Export for character": "キャラクターのエクスポート", |
| @@ -1398,6 +1398,7 @@ | |||
| 1398 | "char_import_6": "직접 PNG 링크(참조", | 1398 | "char_import_6": "직접 PNG 링크(참조", |
| 1399 | "char_import_7": "허용된 호스트의 경우)", | 1399 | "char_import_7": "허용된 호스트의 경우)", |
| 1400 | "char_import_8": "RisuRealm 캐릭터 (직접링크)", | 1400 | "char_import_8": "RisuRealm 캐릭터 (직접링크)", |
| 1401 | "char_import_9": "Soulkyn 캐릭터 (직접링크)", | ||
| 1401 | "Supports importing multiple characters.": "여러 문자 가져오기를 지원합니다.", | 1402 | "Supports importing multiple characters.": "여러 문자 가져오기를 지원합니다.", |
| 1402 | "Write each URL or ID into a new line.": "각 URL 또는 ID를 새 줄에 작성합니다.", | 1403 | "Write each URL or ID into a new line.": "각 URL 또는 ID를 새 줄에 작성합니다.", |
| 1403 | "Export for character": "캐릭터 내보내기", | 1404 | "Export for character": "캐릭터 내보내기", |
| @@ -1379,6 +1379,7 @@ | |||
| 1379 | "char_import_6": "Directe PNG-link (zie", | 1379 | "char_import_6": "Directe PNG-link (zie", |
| 1380 | "char_import_7": "voor toegestane hosts)", | 1380 | "char_import_7": "voor toegestane hosts)", |
| 1381 | "char_import_8": "RisuRealm-personage (directe link)", | 1381 | "char_import_8": "RisuRealm-personage (directe link)", |
| 1382 | "char_import_9": "Soulkyn-personage (directe link)", | ||
| 1382 | "Supports importing multiple characters.": "Ondersteunt het importeren van meerdere tekens.", | 1383 | "Supports importing multiple characters.": "Ondersteunt het importeren van meerdere tekens.", |
| 1383 | "Write each URL or ID into a new line.": "Schrijf elke URL of ID op een nieuwe regel.", | 1384 | "Write each URL or ID into a new line.": "Schrijf elke URL of ID op een nieuwe regel.", |
| 1384 | "Export for character": "Exporteren voor karakter", | 1385 | "Export for character": "Exporteren voor karakter", |
| @@ -1379,6 +1379,7 @@ | |||
| 1379 | "char_import_6": "Link PNG direto (consulte", | 1379 | "char_import_6": "Link PNG direto (consulte", |
| 1380 | "char_import_7": "para hosts permitidos)", | 1380 | "char_import_7": "para hosts permitidos)", |
| 1381 | "char_import_8": "Personagem RisuRealm (link direto)", | 1381 | "char_import_8": "Personagem RisuRealm (link direto)", |
| 1382 | "char_import_9": "Personagem Soulkyn (link direto)", | ||
| 1382 | "Supports importing multiple characters.": "Suporta importação de vários caracteres.", | 1383 | "Supports importing multiple characters.": "Suporta importação de vários caracteres.", |
| 1383 | "Write each URL or ID into a new line.": "Escreva cada URL ou ID em uma nova linha.", | 1384 | "Write each URL or ID into a new line.": "Escreva cada URL ou ID em uma nova linha.", |
| 1384 | "Export for character": "Exportar para personagem", | 1385 | "Export for character": "Exportar para personagem", |
| @@ -965,6 +965,7 @@ | |||
| 965 | "char_import_5": "Персонаж с AICharacterCards.com (прямая ссылка или ID)", | 965 | "char_import_5": "Персонаж с AICharacterCards.com (прямая ссылка или ID)", |
| 966 | "char_import_6": "Прямая ссылка на PNG-файл (список разрешённых хостов находится в", | 966 | "char_import_6": "Прямая ссылка на PNG-файл (список разрешённых хостов находится в", |
| 967 | "char_import_7": ")", | 967 | "char_import_7": ")", |
| 968 | "char_import_9": "Персонаж с Soulkyn (прямая ссылка)", | ||
| 968 | "Grammar String": "Грамматика", | 969 | "Grammar String": "Грамматика", |
| 969 | "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF или EBNF, зависит от бэкенда. Если вы это используете, то, скорее всего, сами знаете, какой именно.", | 970 | "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF или EBNF, зависит от бэкенда. Если вы это используете, то, скорее всего, сами знаете, какой именно.", |
| 970 | "Account": "Аккаунт", | 971 | "Account": "Аккаунт", |
| @@ -1379,6 +1379,7 @@ | |||
| 1379 | "char_import_6": "Пряме посилання на PNG (див", | 1379 | "char_import_6": "Пряме посилання на PNG (див", |
| 1380 | "char_import_7": "для дозволених хостів)", | 1380 | "char_import_7": "для дозволених хостів)", |
| 1381 | "char_import_8": "Персонаж RisuRealm (пряме посилання)", | 1381 | "char_import_8": "Персонаж RisuRealm (пряме посилання)", |
| 1382 | "char_import_9": "Персонаж Soulkyn (пряме посилання)", | ||
| 1382 | "Supports importing multiple characters.": "Підтримується імпорт кількох символів.", | 1383 | "Supports importing multiple characters.": "Підтримується імпорт кількох символів.", |
| 1383 | "Write each URL or ID into a new line.": "Напишіть кожну URL-адресу або ідентифікатор у новому рядку.", | 1384 | "Write each URL or ID into a new line.": "Напишіть кожну URL-адресу або ідентифікатор у новому рядку.", |
| 1384 | "Export for character": "Експорт для персонажа", | 1385 | "Export for character": "Експорт для персонажа", |
| @@ -1379,6 +1379,7 @@ | |||
| 1379 | "char_import_6": "Nhập PNG trực tiếp (tham khảo", | 1379 | "char_import_6": "Nhập PNG trực tiếp (tham khảo", |
| 1380 | "char_import_7": "đối với các máy chủ được phép)", | 1380 | "char_import_7": "đối với các máy chủ được phép)", |
| 1381 | "char_import_8": "RisuRealm (URL trực tiếp)", | 1381 | "char_import_8": "RisuRealm (URL trực tiếp)", |
| 1382 | "char_import_9": "Soulkyn (URL trực tiếp)", | ||
| 1382 | "Supports importing multiple characters.": "Hỗ trợ nhập nhiều ký tự.", | 1383 | "Supports importing multiple characters.": "Hỗ trợ nhập nhiều ký tự.", |
| 1383 | "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.", | 1384 | "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.", |
| 1384 | "Export for character": "Xuất cho nhân vật", | 1385 | "Export for character": "Xuất cho nhân vật", |
| @@ -1893,6 +1893,7 @@ | |||
| 1893 | "char_import_6": "被允许的PNG直链(请参阅", | 1893 | "char_import_6": "被允许的PNG直链(请参阅", |
| 1894 | "char_import_7": ")", | 1894 | "char_import_7": ")", |
| 1895 | "char_import_8": "RisuRealm 角色(直链)", | 1895 | "char_import_8": "RisuRealm 角色(直链)", |
| 1896 | "char_import_9": "Soulkyn 角色(直链)", | ||
| 1896 | "Supports importing multiple characters.": "支持导入多个角色。", | 1897 | "Supports importing multiple characters.": "支持导入多个角色。", |
| 1897 | "Write each URL or ID into a new line.": "将每个 URL 或 ID 写入新行。", | 1898 | "Write each URL or ID into a new line.": "将每个 URL 或 ID 写入新行。", |
| 1898 | "Enter the Git URL of the extension to install": "输入扩展程序的 Git URL 以安装", | 1899 | "Enter the Git URL of the extension to install": "输入扩展程序的 Git URL 以安装", |
| @@ -1384,6 +1384,7 @@ | |||
| 1384 | "char_import_6": "直接 PNG 連結(請參閱", | 1384 | "char_import_6": "直接 PNG 連結(請參閱", |
| 1385 | "char_import_7": "對於允許的主機)", | 1385 | "char_import_7": "對於允許的主機)", |
| 1386 | "char_import_8": "RisuRealm 角色(直接連結)", | 1386 | "char_import_8": "RisuRealm 角色(直接連結)", |
| 1387 | "char_import_9": "Soulkyn 角色(直接連結)", | ||
| 1387 | "Supports importing multiple characters.": "支援匯入多個字元。", | 1388 | "Supports importing multiple characters.": "支援匯入多個字元。", |
| 1388 | "Write each URL or ID into a new line.": "將每個 URL 或 ID 寫入新行。", | 1389 | "Write each URL or ID into a new line.": "將每個 URL 或 ID 寫入新行。", |
| 1389 | "Export for character": "匯出字元", | 1390 | "Export for character": "匯出字元", |
| @@ -10,6 +10,7 @@ | |||
| 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> | ||
| 13 | </ul> | 14 | </ul> |
| 14 | </div> | 15 | </div> |
| 15 | <small> | 16 | <small> |
| @@ -637,6 +637,187 @@ async function downloadRisuCharacter(uuid) { | |||
| 637 | } | 637 | } |
| 638 | 638 | ||
| 639 | /** | 639 | /** |
| 640 | * Parse Soulkyn URL to extract the character slug. | ||
| 641 | * @param {string} url Soulkyn character URL | ||
| 642 | * @returns {string | null} Slug of the character | ||
| 643 | */ | ||
| 644 | function parseSoulkynUrl(url) { | ||
| 645 | // Example: https://soulkyn.com/l/en-US/@kayla-marie | ||
| 646 | const pattern = /^https:\/\/soulkyn\.com\/l\/[a-z]{2}-[A-Z]{2}\/@([\w\d-]+)/i; | ||
| 647 | const match = url.match(pattern); | ||
| 648 | return match ? match[1] : null; | ||
| 649 | } | ||
| 650 | |||
| 651 | /** | ||
| 652 | * Download Soulkyn character card | ||
| 653 | * @param {string} slug Slug of the character | ||
| 654 | * @returns {Promise<{buffer: Buffer, fileName: string, fileType: string} | null>} | ||
| 655 | */ | ||
| 656 | async function downloadSoulkynCharacter(slug) { | ||
| 657 | const soulkynReplacements = [ | ||
| 658 | // https://soulkyn.com/l/en-US/help/character-backgrounds-advanced#variables-you-can-use-in-character-background-text | ||
| 659 | { pattern: /__USER_?NAME__/gi, replacement: '{{user}}' }, | ||
| 660 | { pattern: /__PERSONA_?NAME__/gi, replacement: '{{char}}' }, | ||
| 661 | // ST doesn't support gender-specific pronoun macros | ||
| 662 | { pattern: /__U_PRONOUN_1__/gi, replacement: 'they' }, | ||
| 663 | { pattern: /__U_PRONOUN_2__/gi, replacement: 'them' }, | ||
| 664 | { pattern: /__U_PRONOUN_3__/gi, replacement: 'their' }, | ||
| 665 | { pattern: /__U_PRONOUN_4__/gi, replacement: 'themselves' }, | ||
| 666 | { pattern: /__(USER_)?PRONOUN__/gi, replacement: 'they' }, | ||
| 667 | { pattern: /__(USER_)?CPRONOUN__/gi, replacement: 'them' }, | ||
| 668 | { pattern: /__(USER_)?UPRONOUN__/gi, replacement: 'their' }, | ||
| 669 | // HTML tags -> Markdown syntax | ||
| 670 | { pattern: /<(strong|b)>/gi, replacement: '**' }, | ||
| 671 | { pattern: /<\/(strong|b)>/gi, replacement: '**' }, | ||
| 672 | { pattern: /<(em|i)>/gi, replacement: '*' }, | ||
| 673 | { pattern: /<\/(em|i)>/gi, replacement: '*' }, | ||
| 674 | ]; | ||
| 675 | |||
| 676 | const normalizeContent = (str) => soulkynReplacements.reduce((acc, { pattern, replacement }) => acc.replace(pattern, replacement), str); | ||
| 677 | |||
| 678 | try { | ||
| 679 | const url = `https://soulkyn.com/_special/rest/Sk/public/Persona/${slug}`; | ||
| 680 | const result = await fetch(url, { | ||
| 681 | headers: { 'Content-Type': 'application/json', 'User-Agent': USER_AGENT }, | ||
| 682 | }); | ||
| 683 | if (result.ok) { | ||
| 684 | /** @type {any} */ | ||
| 685 | const soulkynCharData = await result.json(); | ||
| 686 | |||
| 687 | if (soulkynCharData.result !== 'success') { | ||
| 688 | console.error('Soulkyn returned error', soulkynCharData.message); | ||
| 689 | throw new Error(`Failed to download character: ${soulkynCharData.message}`); | ||
| 690 | } | ||
| 691 | |||
| 692 | // Fetch avatar | ||
| 693 | let avatarBuffer = null; | ||
| 694 | if (soulkynCharData.data?.Avatar?.FWSUUID) { | ||
| 695 | const avatarUrl = `https://rub.soulkyn.com/${soulkynCharData.data.Avatar.FWSUUID}/`; | ||
| 696 | const avatarResult = await fetch(avatarUrl, { headers: { 'User-Agent': USER_AGENT } }); | ||
| 697 | |||
| 698 | if (avatarResult.ok) { | ||
| 699 | const avatarContentType = avatarResult.headers.get('content-type'); | ||
| 700 | if (avatarContentType === 'image/png') { | ||
| 701 | avatarBuffer = Buffer.from(await avatarResult.arrayBuffer()); | ||
| 702 | } else { | ||
| 703 | console.warn(`Soulkyn character (${slug}) avatar is not PNG: ${avatarContentType}`); | ||
| 704 | } | ||
| 705 | } else { | ||
| 706 | console.warn(`Soulkyn character (${slug}) avatar download failed: ${avatarResult.status}`); | ||
| 707 | } | ||
| 708 | } else { | ||
| 709 | console.warn(`Soulkyn character (${slug}) does not have an avatar`); | ||
| 710 | } | ||
| 711 | |||
| 712 | // Fallback to default avatar | ||
| 713 | if (!avatarBuffer) { | ||
| 714 | const defaultAvatarPath = path.join(serverDirectory, 'public', 'img', 'ai4.png'); | ||
| 715 | avatarBuffer = fs.readFileSync(defaultAvatarPath); | ||
| 716 | } | ||
| 717 | |||
| 718 | const d = soulkynCharData.data; | ||
| 719 | soulkynReplacements.push({ pattern: d.Username, replacement: '{{char}}' }); | ||
| 720 | |||
| 721 | // Parse Soulkyn data into character chard | ||
| 722 | const charData = { | ||
| 723 | name: d.Username, | ||
| 724 | first_mes: '', | ||
| 725 | tags: [], | ||
| 726 | description: '', | ||
| 727 | creator: d.User.Username, | ||
| 728 | creator_notes: '', | ||
| 729 | alternate_greetings: [], | ||
| 730 | character_version: '', | ||
| 731 | mes_example: '', | ||
| 732 | post_history_instructions: '', | ||
| 733 | system_prompt: '', | ||
| 734 | scenario: '', | ||
| 735 | personality: '', | ||
| 736 | extensions: { | ||
| 737 | soulkyn_slug: slug, | ||
| 738 | soulkyn_id: d.UUID, | ||
| 739 | }, | ||
| 740 | }; | ||
| 741 | |||
| 742 | if (d?.PersonaIntroText) { | ||
| 743 | const match = d.PersonaIntroText.match(/^(?:\[Scenario:\s*([\s\S]*?)\]\s*)?([\s\S]*)$/); | ||
| 744 | if (match) { | ||
| 745 | if (match[1]) { | ||
| 746 | charData.scenario = normalizeContent(match[1].trim()); | ||
| 747 | } | ||
| 748 | charData.first_mes = normalizeContent(match[2].trim()); | ||
| 749 | } | ||
| 750 | } | ||
| 751 | |||
| 752 | const descriptionArr = ['Name: {{char}}']; | ||
| 753 | if (d?.Version?.Age) { | ||
| 754 | descriptionArr.push(`Age: ${d.Version.Age}`); | ||
| 755 | } | ||
| 756 | if (d?.Version?.Gender) { | ||
| 757 | descriptionArr.push(`Gender: ${d.Version.Gender}`); | ||
| 758 | } | ||
| 759 | if (d?.Version?.Race?.Name && !d.Version.Race.Name.match(/no preset/i)) { | ||
| 760 | let race = d.Version.Race.Name; | ||
| 761 | if (d.Version.Race?.Description) { | ||
| 762 | race += ` (${d.Version.Race.Description})`; | ||
| 763 | } | ||
| 764 | descriptionArr.push(`Race: ${race}`); | ||
| 765 | } | ||
| 766 | if (d?.PersonalityType) { | ||
| 767 | descriptionArr.push(`Personality type: ${d.PersonalityType}`); | ||
| 768 | } | ||
| 769 | if (Array.isArray(d?.Version?.PropertyPersonality)) { | ||
| 770 | const traits = d.Version.PropertyPersonality.map((t) => t.Value).join(', '); | ||
| 771 | descriptionArr.push(`Personality Traits: ${traits}`); | ||
| 772 | } | ||
| 773 | if (Array.isArray(d?.Version?.PropertyPhysical)) { | ||
| 774 | const traits = d.Version.PropertyPhysical.map((t) => t.Value).join(', '); | ||
| 775 | descriptionArr.push(`Physical Traits: ${traits}`); | ||
| 776 | } | ||
| 777 | if (Array.isArray(d?.Clothes?.Preset)) { | ||
| 778 | descriptionArr.push(`Clothes: ${d.Clothes.Preset.join(', ')}`); | ||
| 779 | } | ||
| 780 | if (d?.Avatar?.Caption) { | ||
| 781 | descriptionArr.push(`Image description featuring {{char}}: ${d.Avatar.Caption.replace(/\n+/g, ' ')}`); | ||
| 782 | } | ||
| 783 | if (d?.Version?.WelcomeMessage) { | ||
| 784 | if (charData.first_mes) { | ||
| 785 | descriptionArr.push(`{{char}}'s self-description: "${d.Version.WelcomeMessage}"`); | ||
| 786 | } else { | ||
| 787 | // Some characters lack `PersonaIntroText`. In that case we use `Version.WelcomeMessage` for `first_mes` | ||
| 788 | charData.first_mes = normalizeContent(d.Version.WelcomeMessage); | ||
| 789 | } | ||
| 790 | } | ||
| 791 | charData.description = normalizeContent(descriptionArr.join('\n')); | ||
| 792 | |||
| 793 | if (Array.isArray(d?.Version?.ChatExamplesValue)) { | ||
| 794 | charData.mes_example = d.Version.ChatExamplesValue.map((example) => `<START>\n${normalizeContent(example)}`).join('\n'); | ||
| 795 | } | ||
| 796 | |||
| 797 | if (Array.isArray(d?.PersonaTags)) { | ||
| 798 | charData.tags = d.PersonaTags.map((t) => t.Slug); | ||
| 799 | } | ||
| 800 | |||
| 801 | // Character card | ||
| 802 | const buffer = write(avatarBuffer, JSON.stringify({ | ||
| 803 | 'spec': 'chara_card_v2', | ||
| 804 | 'spec_version': '2.0', | ||
| 805 | 'data': charData, | ||
| 806 | })); | ||
| 807 | |||
| 808 | const fileName = `${sanitize(d.UUID)}.png`; | ||
| 809 | const fileType = 'image/png'; | ||
| 810 | |||
| 811 | return { buffer, fileName, fileType }; | ||
| 812 | } | ||
| 813 | } catch (error) { | ||
| 814 | console.error('Error downloading character:', error); | ||
| 815 | throw error; | ||
| 816 | } | ||
| 817 | return null; | ||
| 818 | } | ||
| 819 | |||
| 820 | /** | ||
| 640 | * @param {String} url | 821 | * @param {String} url |
| 641 | * @returns {String | null } UUID of the character | 822 | * @returns {String | null } UUID of the character |
| 642 | */ | 823 | */ |
| @@ -691,6 +872,7 @@ router.post('/importURL', async (request, response) => { | |||
| 691 | const isPygmalionContent = host.includes('pygmalion.chat'); | 872 | const isPygmalionContent = host.includes('pygmalion.chat'); |
| 692 | const isAICharacterCardsContent = host.includes('aicharactercards.com'); | 873 | const isAICharacterCardsContent = host.includes('aicharactercards.com'); |
| 693 | const isRisu = host.includes('realm.risuai.net'); | 874 | const isRisu = host.includes('realm.risuai.net'); |
| 875 | const isSoulkyn = host.includes('soulkyn.com'); | ||
| 694 | const isGeneric = isHostWhitelisted(host); | 876 | const isGeneric = isHostWhitelisted(host); |
| 695 | 877 | ||
| 696 | if (isPygmalionContent) { | 878 | if (isPygmalionContent) { |
| @@ -739,6 +921,13 @@ router.post('/importURL', async (request, response) => { | |||
| 739 | 921 | ||
| 740 | type = 'character'; | 922 | type = 'character'; |
| 741 | result = await downloadRisuCharacter(uuid); | 923 | result = await downloadRisuCharacter(uuid); |
| 924 | } else if (isSoulkyn) { | ||
| 925 | const soulkynSlug = parseSoulkynUrl(url); | ||
| 926 | if (!soulkynSlug) { | ||
| 927 | return response.sendStatus(404); | ||
| 928 | } | ||
| 929 | type = 'character'; | ||
| 930 | result = await downloadSoulkynCharacter(soulkynSlug); | ||
| 742 | } else if (isGeneric) { | 931 | } else if (isGeneric) { |
| 743 | console.info('Downloading from generic url:', url); | 932 | console.info('Downloading from generic url:', url); |
| 744 | type = 'character'; | 933 | type = 'character'; |