diff --git a/public/script.js b/public/script.js
index 31344684a..7c3f66167 100644
--- a/public/script.js
+++ b/public/script.js
@@ -10552,6 +10552,7 @@ jQuery(async function () {
Chub Lorebook (Direct Link or ID)
Example: lorebooks/bartleby/example-lorebook
JanitorAI Character (Direct Link or UUID)
Example: ddd1498a-a370-4136-b138-a8cd9461fdfe_character-aqua-the-useless-goddess
Pygmalion.chat Character (Direct Link or UUID)
Example: a7ca95a1-0c88-4e23-91b3-149db1e78ab9
+ AICharacterCard.com Character (Direct Link or ID)
Example: AICC/aicharcards/the-game-master
More coming soon...
`;
const input = await callPopup(html, 'input', '', { okButton: 'Import', rows: 4 });
diff --git a/src/endpoints/content-manager.js b/src/endpoints/content-manager.js
index bbb444faf..76ef3f157 100644
--- a/src/endpoints/content-manager.js
+++ b/src/endpoints/content-manager.js
@@ -345,6 +345,40 @@ async function downloadJannyCharacter(uuid) {
throw new Error('Failed to download character');
}
+//Download Character Cards from AICharactersCards.com (AICC) API.
+async function downloadAICCCharacter(id) {
+ const apiURL = `https://aicharactercards.com/wp-json/pngapi/v1/image/${id}`;
+ try {
+ const response = await fetch(apiURL);
+ if (!response.ok) {
+ throw new Error(`Failed to download character: ${response.statusText}`);
+ }
+
+ const contentType = response.headers.get('content-type') || 'image/png'; // Default to 'image/png' if header is missing
+ const buffer = await response.buffer();
+ const fileName = `${sanitize(id)}.png`; // Assuming PNG, but adjust based on actual content or headers
+
+ return {
+ buffer: buffer,
+ fileName: fileName,
+ fileType: contentType
+ };
+ } catch (error) {
+ console.error('Error downloading character:', error);
+ throw error;
+ }
+}
+
+function parseAICC(url) {
+ const pattern = /^https?:\/\/aicharactercards\.com\/character-cards\/([^\/]+)\/([^\/]+)\/?$|([^\/]+)\/([^\/]+)$/;
+ const match = url.match(pattern);
+ if (match) {
+ // Match group 1 & 2 for full URL, 3 & 4 for relative path
+ return match[1] && match[2] ? `${match[1]}/${match[2]}` : `${match[3]}/${match[4]}`;
+ }
+ return null;
+}
+
/**
* @param {String} url
* @returns {String | null } UUID of the character
@@ -373,6 +407,7 @@ router.post('/importURL', jsonParser, async (request, response) => {
const isJannnyContent = url.includes('janitorai');
const isPygmalionContent = url.includes('pygmalion.chat');
+ const isAICharacterCardsContent = url.includes('aicharactercards.com');
if (isPygmalionContent) {
const uuid = getUuidFromUrl(url);
@@ -390,6 +425,13 @@ router.post('/importURL', jsonParser, async (request, response) => {
type = 'character';
result = await downloadJannyCharacter(uuid);
+ } else if (isAICharacterCardsContent) {
+ const AICCParsed = parseAICC(url);
+ if (!AICCParsed) {
+ return response.sendStatus(404);
+ }
+ type = 'character';
+ result = await downloadAICCCharacter(AICCParsed);
} else {
const chubParsed = parseChubUrl(url);
type = chubParsed?.type;
@@ -428,6 +470,7 @@ router.post('/importUUID', jsonParser, async (request, response) => {
const isJannny = uuid.includes('_character');
const isPygmalion = (!isJannny && uuid.length == 36);
+ const isAICC = uuid.startsWith('AICC/');
const uuidType = uuid.includes('lorebook') ? 'lorebook' : 'character';
if (isPygmalion) {
@@ -436,6 +479,10 @@ router.post('/importUUID', jsonParser, async (request, response) => {
} else if (isJannny) {
console.log('Downloading Janitor character:', uuid.split('_')[0]);
result = await downloadJannyCharacter(uuid.split('_')[0]);
+ } else if (isAICC) {
+ const [, author, card] = uuid.split('/');
+ console.log('Downloading AICC character:', `${author}/${card}`);
+ result = await downloadAICCCharacter(`${author}/${card}`);
} else {
if (uuidType === 'character') {
console.log('Downloading chub character:', uuid);