Import character card from Soulkyn (#4042)

4bec90abb5faf80e36ac5a8d70d91cd5973d11e4

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

Signed
17 files changed, +205 -0Showing whitespace changes
public/locales/ar-sa.json+1 -0
@@ -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": "تصدير للشخصية",
public/locales/de-de.json+1 -0
@@ -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",
public/locales/es-es.json+1 -0
@@ -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",
public/locales/fr-fr.json+1 -0
@@ -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",
public/locales/is-is.json+1 -0
@@ -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",
public/locales/it-it.json+1 -0
@@ -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",
public/locales/ja-jp.json+1 -0
@@ -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": "キャラクターのエクスポート",
public/locales/ko-kr.json+1 -0
@@ -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": "캐릭터 내보내기",
public/locales/nl-nl.json+1 -0
@@ -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",
public/locales/pt-pt.json+1 -0
@@ -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",
public/locales/ru-ru.json+1 -0
@@ -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": "Аккаунт",
public/locales/uk-ua.json+1 -0
@@ -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": "Експорт для персонажа",
public/locales/vi-vn.json+1 -0
@@ -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",
public/locales/zh-cn.json+1 -0
@@ -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 以安装",
public/locales/zh-tw.json+1 -0
@@ -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": "匯出字元",
public/scripts/templates/importCharacters.html+1 -0
@@ -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>
src/endpoints/content-manager.js+189 -0
@@ -637,6 +637,187 @@ async function downloadRisuCharacter(uuid) {
637}637}
638638
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 */
644function 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 */
656async 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} url821* @param {String} url
641* @returns {String | null } UUID of the character822* @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);
695877
696 if (isPygmalionContent) {878 if (isPygmalionContent) {
@@ -739,6 +921,13 @@ router.post('/importURL', async (request, response) => {
739921
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';