mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Import character card from Soulkyn (#4042)
This commit is contained in:
@@ -1379,6 +1379,7 @@
|
||||
"char_import_6": "رابط PNG المباشر (راجع",
|
||||
"char_import_7": "للمضيفين المسموح بهم)",
|
||||
"char_import_8": "شخصية RisuRealm (رابط مباشر)",
|
||||
"char_import_9": "شخصية Soulkyn (رابط مباشر)",
|
||||
"Supports importing multiple characters.": "يدعم استيراد أحرف متعددة.",
|
||||
"Write each URL or ID into a new line.": "اكتب كل عنوان URL أو معرف في سطر جديد.",
|
||||
"Export for character": "تصدير للشخصية",
|
||||
|
@@ -1379,6 +1379,7 @@
|
||||
"char_import_6": "Direkter PNG-Link (siehe",
|
||||
"char_import_7": "für erlaubte Hosts)",
|
||||
"char_import_8": "RisuRealm-Charakter (Direktlink)",
|
||||
"char_import_9": "Soulkyn-Charakter (Direktlink)",
|
||||
"Supports importing multiple characters.": "Unterstützt den Import mehrerer Zeichen.",
|
||||
"Write each URL or ID into a new line.": "Schreiben Sie jede URL oder ID in eine neue Zeile.",
|
||||
"Export for character": "Export für Zeichen",
|
||||
|
@@ -1379,6 +1379,7 @@
|
||||
"char_import_6": "Enlace PNG directo (consulte",
|
||||
"char_import_7": "para hosts permitidos)",
|
||||
"char_import_8": "Personaje RisuRealm (Enlace directo)",
|
||||
"char_import_9": "Personaje Soulkyn (Enlace directo)",
|
||||
"Supports importing multiple characters.": "Admite la importación de múltiples caracteres.",
|
||||
"Write each URL or ID into a new line.": "Escriba cada URL o ID en una nueva línea.",
|
||||
"Export for character": "Exportar para personaje",
|
||||
|
@@ -1301,6 +1301,7 @@
|
||||
"char_import_6": "Lien PNG direct (voir",
|
||||
"char_import_7": "pour les hôtes autorisés)",
|
||||
"char_import_8": "Personnage de RisuRealm (lien direct)",
|
||||
"char_import_9": "Personnage de Soulkyn (lien direct)",
|
||||
"Supports importing multiple characters.": "Prend en charge l'importation de plusieurs caractères.",
|
||||
"Write each URL or ID into a new line.": "Écrivez chaque URL ou identifiant dans une nouvelle ligne.",
|
||||
"Export for character": "Exportation pour le personnage",
|
||||
|
@@ -1379,6 +1379,7 @@
|
||||
"char_import_6": "Beinn PNG hlekkur (sjá",
|
||||
"char_import_7": "fyrir leyfilega gestgjafa)",
|
||||
"char_import_8": "RisuRealm karakter (beinn hlekkur)",
|
||||
"char_import_9": "Soulkyn karakter (beinn hlekkur)",
|
||||
"Supports importing multiple characters.": "Styður innflutning á mörgum stöfum.",
|
||||
"Write each URL or ID into a new line.": "Skrifaðu hverja vefslóð eða auðkenni í nýja línu.",
|
||||
"Export for character": "Flytja út fyrir persónu",
|
||||
|
@@ -1379,6 +1379,7 @@
|
||||
"char_import_6": "Collegamento PNG diretto (fare riferimento a",
|
||||
"char_import_7": "per gli host consentiti)",
|
||||
"char_import_8": "Personaggio RisuRealm (collegamento diretto)",
|
||||
"char_import_9": "Personaggio Soulkyn (collegamento diretto)",
|
||||
"Supports importing multiple characters.": "Supporta l'importazione di più caratteri.",
|
||||
"Write each URL or ID into a new line.": "Scrivi ogni URL o ID in una nuova riga.",
|
||||
"Export for character": "Esporta per carattere",
|
||||
|
@@ -1381,6 +1381,7 @@
|
||||
"char_import_6": "直接PNGリンク(参照",
|
||||
"char_import_7": "許可されたホストの場合)",
|
||||
"char_import_8": "RisuRealm キャラクター (直接リンク)",
|
||||
"char_import_9": "Soulkyn キャラクター (直接リンク)",
|
||||
"Supports importing multiple characters.": "複数のキャラクターのインポートをサポートします。",
|
||||
"Write each URL or ID into a new line.": "各 URL または ID を新しい行に入力します。",
|
||||
"Export for character": "キャラクターのエクスポート",
|
||||
|
@@ -1398,6 +1398,7 @@
|
||||
"char_import_6": "직접 PNG 링크(참조",
|
||||
"char_import_7": "허용된 호스트의 경우)",
|
||||
"char_import_8": "RisuRealm 캐릭터 (직접링크)",
|
||||
"char_import_9": "Soulkyn 캐릭터 (직접링크)",
|
||||
"Supports importing multiple characters.": "여러 문자 가져오기를 지원합니다.",
|
||||
"Write each URL or ID into a new line.": "각 URL 또는 ID를 새 줄에 작성합니다.",
|
||||
"Export for character": "캐릭터 내보내기",
|
||||
|
@@ -1379,6 +1379,7 @@
|
||||
"char_import_6": "Directe PNG-link (zie",
|
||||
"char_import_7": "voor toegestane hosts)",
|
||||
"char_import_8": "RisuRealm-personage (directe link)",
|
||||
"char_import_9": "Soulkyn-personage (directe link)",
|
||||
"Supports importing multiple characters.": "Ondersteunt het importeren van meerdere tekens.",
|
||||
"Write each URL or ID into a new line.": "Schrijf elke URL of ID op een nieuwe regel.",
|
||||
"Export for character": "Exporteren voor karakter",
|
||||
|
@@ -1379,6 +1379,7 @@
|
||||
"char_import_6": "Link PNG direto (consulte",
|
||||
"char_import_7": "para hosts permitidos)",
|
||||
"char_import_8": "Personagem RisuRealm (link direto)",
|
||||
"char_import_9": "Personagem Soulkyn (link direto)",
|
||||
"Supports importing multiple characters.": "Suporta importação de vários caracteres.",
|
||||
"Write each URL or ID into a new line.": "Escreva cada URL ou ID em uma nova linha.",
|
||||
"Export for character": "Exportar para personagem",
|
||||
|
@@ -965,6 +965,7 @@
|
||||
"char_import_5": "Персонаж с AICharacterCards.com (прямая ссылка или ID)",
|
||||
"char_import_6": "Прямая ссылка на PNG-файл (список разрешённых хостов находится в",
|
||||
"char_import_7": ")",
|
||||
"char_import_9": "Персонаж с Soulkyn (прямая ссылка)",
|
||||
"Grammar String": "Грамматика",
|
||||
"GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF или EBNF, зависит от бэкенда. Если вы это используете, то, скорее всего, сами знаете, какой именно.",
|
||||
"Account": "Аккаунт",
|
||||
|
@@ -1379,6 +1379,7 @@
|
||||
"char_import_6": "Пряме посилання на PNG (див",
|
||||
"char_import_7": "для дозволених хостів)",
|
||||
"char_import_8": "Персонаж RisuRealm (пряме посилання)",
|
||||
"char_import_9": "Персонаж Soulkyn (пряме посилання)",
|
||||
"Supports importing multiple characters.": "Підтримується імпорт кількох символів.",
|
||||
"Write each URL or ID into a new line.": "Напишіть кожну URL-адресу або ідентифікатор у новому рядку.",
|
||||
"Export for character": "Експорт для персонажа",
|
||||
|
@@ -1379,6 +1379,7 @@
|
||||
"char_import_6": "Nhập PNG trực tiếp (tham khảo",
|
||||
"char_import_7": "đối với các máy chủ được phép)",
|
||||
"char_import_8": "RisuRealm (URL trực tiếp)",
|
||||
"char_import_9": "Soulkyn (URL trực tiếp)",
|
||||
"Supports importing multiple characters.": "Hỗ trợ nhập nhiều ký tự.",
|
||||
"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.",
|
||||
"Export for character": "Xuất cho nhân vật",
|
||||
|
@@ -1893,6 +1893,7 @@
|
||||
"char_import_6": "被允许的PNG直链(请参阅",
|
||||
"char_import_7": ")",
|
||||
"char_import_8": "RisuRealm 角色(直链)",
|
||||
"char_import_9": "Soulkyn 角色(直链)",
|
||||
"Supports importing multiple characters.": "支持导入多个角色。",
|
||||
"Write each URL or ID into a new line.": "将每个 URL 或 ID 写入新行。",
|
||||
"Enter the Git URL of the extension to install": "输入扩展程序的 Git URL 以安装",
|
||||
|
@@ -1384,6 +1384,7 @@
|
||||
"char_import_6": "直接 PNG 連結(請參閱",
|
||||
"char_import_7": "對於允許的主機)",
|
||||
"char_import_8": "RisuRealm 角色(直接連結)",
|
||||
"char_import_9": "Soulkyn 角色(直接連結)",
|
||||
"Supports importing multiple characters.": "支援匯入多個字元。",
|
||||
"Write each URL or ID into a new line.": "將每個 URL 或 ID 寫入新行。",
|
||||
"Export for character": "匯出字元",
|
||||
|
@@ -10,6 +10,7 @@
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
</ul>
|
||||
</div>
|
||||
<small>
|
||||
|
@@ -636,6 +636,187 @@ async function downloadRisuCharacter(uuid) {
|
||||
return { buffer, fileName, fileType };
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Soulkyn URL to extract the character slug.
|
||||
* @param {string} url Soulkyn character URL
|
||||
* @returns {string | null} Slug of the character
|
||||
*/
|
||||
function parseSoulkynUrl(url) {
|
||||
// Example: https://soulkyn.com/l/en-US/@kayla-marie
|
||||
const pattern = /^https:\/\/soulkyn\.com\/l\/[a-z]{2}-[A-Z]{2}\/@([\w\d-]+)/i;
|
||||
const match = url.match(pattern);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download Soulkyn character card
|
||||
* @param {string} slug Slug of the character
|
||||
* @returns {Promise<{buffer: Buffer, fileName: string, fileType: string} | null>}
|
||||
*/
|
||||
async function downloadSoulkynCharacter(slug) {
|
||||
const soulkynReplacements = [
|
||||
// https://soulkyn.com/l/en-US/help/character-backgrounds-advanced#variables-you-can-use-in-character-background-text
|
||||
{ pattern: /__USER_?NAME__/gi, replacement: '{{user}}' },
|
||||
{ pattern: /__PERSONA_?NAME__/gi, replacement: '{{char}}' },
|
||||
// ST doesn't support gender-specific pronoun macros
|
||||
{ pattern: /__U_PRONOUN_1__/gi, replacement: 'they' },
|
||||
{ pattern: /__U_PRONOUN_2__/gi, replacement: 'them' },
|
||||
{ pattern: /__U_PRONOUN_3__/gi, replacement: 'their' },
|
||||
{ pattern: /__U_PRONOUN_4__/gi, replacement: 'themselves' },
|
||||
{ pattern: /__(USER_)?PRONOUN__/gi, replacement: 'they' },
|
||||
{ pattern: /__(USER_)?CPRONOUN__/gi, replacement: 'them' },
|
||||
{ pattern: /__(USER_)?UPRONOUN__/gi, replacement: 'their' },
|
||||
// HTML tags -> Markdown syntax
|
||||
{ pattern: /<(strong|b)>/gi, replacement: '**' },
|
||||
{ pattern: /<\/(strong|b)>/gi, replacement: '**' },
|
||||
{ pattern: /<(em|i)>/gi, replacement: '*' },
|
||||
{ pattern: /<\/(em|i)>/gi, replacement: '*' },
|
||||
];
|
||||
|
||||
const normalizeContent = (str) => soulkynReplacements.reduce((acc, { pattern, replacement }) => acc.replace(pattern, replacement), str);
|
||||
|
||||
try {
|
||||
const url = `https://soulkyn.com/_special/rest/Sk/public/Persona/${slug}`;
|
||||
const result = await fetch(url, {
|
||||
headers: { 'Content-Type': 'application/json', 'User-Agent': USER_AGENT },
|
||||
});
|
||||
if (result.ok) {
|
||||
/** @type {any} */
|
||||
const soulkynCharData = await result.json();
|
||||
|
||||
if (soulkynCharData.result !== 'success') {
|
||||
console.error('Soulkyn returned error', soulkynCharData.message);
|
||||
throw new Error(`Failed to download character: ${soulkynCharData.message}`);
|
||||
}
|
||||
|
||||
// Fetch avatar
|
||||
let avatarBuffer = null;
|
||||
if (soulkynCharData.data?.Avatar?.FWSUUID) {
|
||||
const avatarUrl = `https://rub.soulkyn.com/${soulkynCharData.data.Avatar.FWSUUID}/`;
|
||||
const avatarResult = await fetch(avatarUrl, { headers: { 'User-Agent': USER_AGENT } });
|
||||
|
||||
if (avatarResult.ok) {
|
||||
const avatarContentType = avatarResult.headers.get('content-type');
|
||||
if (avatarContentType === 'image/png') {
|
||||
avatarBuffer = Buffer.from(await avatarResult.arrayBuffer());
|
||||
} else {
|
||||
console.warn(`Soulkyn character (${slug}) avatar is not PNG: ${avatarContentType}`);
|
||||
}
|
||||
} else {
|
||||
console.warn(`Soulkyn character (${slug}) avatar download failed: ${avatarResult.status}`);
|
||||
}
|
||||
} else {
|
||||
console.warn(`Soulkyn character (${slug}) does not have an avatar`);
|
||||
}
|
||||
|
||||
// Fallback to default avatar
|
||||
if (!avatarBuffer) {
|
||||
const defaultAvatarPath = path.join(serverDirectory, 'public', 'img', 'ai4.png');
|
||||
avatarBuffer = fs.readFileSync(defaultAvatarPath);
|
||||
}
|
||||
|
||||
const d = soulkynCharData.data;
|
||||
soulkynReplacements.push({ pattern: d.Username, replacement: '{{char}}' });
|
||||
|
||||
// Parse Soulkyn data into character chard
|
||||
const charData = {
|
||||
name: d.Username,
|
||||
first_mes: '',
|
||||
tags: [],
|
||||
description: '',
|
||||
creator: d.User.Username,
|
||||
creator_notes: '',
|
||||
alternate_greetings: [],
|
||||
character_version: '',
|
||||
mes_example: '',
|
||||
post_history_instructions: '',
|
||||
system_prompt: '',
|
||||
scenario: '',
|
||||
personality: '',
|
||||
extensions: {
|
||||
soulkyn_slug: slug,
|
||||
soulkyn_id: d.UUID,
|
||||
},
|
||||
};
|
||||
|
||||
if (d?.PersonaIntroText) {
|
||||
const match = d.PersonaIntroText.match(/^(?:\[Scenario:\s*([\s\S]*?)\]\s*)?([\s\S]*)$/);
|
||||
if (match) {
|
||||
if (match[1]) {
|
||||
charData.scenario = normalizeContent(match[1].trim());
|
||||
}
|
||||
charData.first_mes = normalizeContent(match[2].trim());
|
||||
}
|
||||
}
|
||||
|
||||
const descriptionArr = ['Name: {{char}}'];
|
||||
if (d?.Version?.Age) {
|
||||
descriptionArr.push(`Age: ${d.Version.Age}`);
|
||||
}
|
||||
if (d?.Version?.Gender) {
|
||||
descriptionArr.push(`Gender: ${d.Version.Gender}`);
|
||||
}
|
||||
if (d?.Version?.Race?.Name && !d.Version.Race.Name.match(/no preset/i)) {
|
||||
let race = d.Version.Race.Name;
|
||||
if (d.Version.Race?.Description) {
|
||||
race += ` (${d.Version.Race.Description})`;
|
||||
}
|
||||
descriptionArr.push(`Race: ${race}`);
|
||||
}
|
||||
if (d?.PersonalityType) {
|
||||
descriptionArr.push(`Personality type: ${d.PersonalityType}`);
|
||||
}
|
||||
if (Array.isArray(d?.Version?.PropertyPersonality)) {
|
||||
const traits = d.Version.PropertyPersonality.map((t) => t.Value).join(', ');
|
||||
descriptionArr.push(`Personality Traits: ${traits}`);
|
||||
}
|
||||
if (Array.isArray(d?.Version?.PropertyPhysical)) {
|
||||
const traits = d.Version.PropertyPhysical.map((t) => t.Value).join(', ');
|
||||
descriptionArr.push(`Physical Traits: ${traits}`);
|
||||
}
|
||||
if (Array.isArray(d?.Clothes?.Preset)) {
|
||||
descriptionArr.push(`Clothes: ${d.Clothes.Preset.join(', ')}`);
|
||||
}
|
||||
if (d?.Avatar?.Caption) {
|
||||
descriptionArr.push(`Image description featuring {{char}}: ${d.Avatar.Caption.replace(/\n+/g, ' ')}`);
|
||||
}
|
||||
if (d?.Version?.WelcomeMessage) {
|
||||
if (charData.first_mes) {
|
||||
descriptionArr.push(`{{char}}'s self-description: "${d.Version.WelcomeMessage}"`);
|
||||
} else {
|
||||
// Some characters lack `PersonaIntroText`. In that case we use `Version.WelcomeMessage` for `first_mes`
|
||||
charData.first_mes = normalizeContent(d.Version.WelcomeMessage);
|
||||
}
|
||||
}
|
||||
charData.description = normalizeContent(descriptionArr.join('\n'));
|
||||
|
||||
if (Array.isArray(d?.Version?.ChatExamplesValue)) {
|
||||
charData.mes_example = d.Version.ChatExamplesValue.map((example) => `<START>\n${normalizeContent(example)}`).join('\n');
|
||||
}
|
||||
|
||||
if (Array.isArray(d?.PersonaTags)) {
|
||||
charData.tags = d.PersonaTags.map((t) => t.Slug);
|
||||
}
|
||||
|
||||
// Character card
|
||||
const buffer = write(avatarBuffer, JSON.stringify({
|
||||
'spec': 'chara_card_v2',
|
||||
'spec_version': '2.0',
|
||||
'data': charData,
|
||||
}));
|
||||
|
||||
const fileName = `${sanitize(d.UUID)}.png`;
|
||||
const fileType = 'image/png';
|
||||
|
||||
return { buffer, fileName, fileType };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error downloading character:', error);
|
||||
throw error;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} url
|
||||
* @returns {String | null } UUID of the character
|
||||
@@ -691,6 +872,7 @@ router.post('/importURL', async (request, response) => {
|
||||
const isPygmalionContent = host.includes('pygmalion.chat');
|
||||
const isAICharacterCardsContent = host.includes('aicharactercards.com');
|
||||
const isRisu = host.includes('realm.risuai.net');
|
||||
const isSoulkyn = host.includes('soulkyn.com');
|
||||
const isGeneric = isHostWhitelisted(host);
|
||||
|
||||
if (isPygmalionContent) {
|
||||
@@ -739,6 +921,13 @@ router.post('/importURL', async (request, response) => {
|
||||
|
||||
type = 'character';
|
||||
result = await downloadRisuCharacter(uuid);
|
||||
} else if (isSoulkyn) {
|
||||
const soulkynSlug = parseSoulkynUrl(url);
|
||||
if (!soulkynSlug) {
|
||||
return response.sendStatus(404);
|
||||
}
|
||||
type = 'character';
|
||||
result = await downloadSoulkynCharacter(soulkynSlug);
|
||||
} else if (isGeneric) {
|
||||
console.info('Downloading from generic url:', url);
|
||||
type = 'character';
|
||||
|
Reference in New Issue
Block a user