mirror of
https://github.com/severian-dev/sucker.severian.dev.git
synced 2026-02-22 00:42:05 +00:00
attempt at metadata grabbing
This commit is contained in:
@@ -2,4 +2,4 @@
|
||||
|
||||
# Disable Discord community banner (optional)
|
||||
# Uncomment the line below to hide the Discord banner
|
||||
# NEXT_PUBLIC_DISABLE_DISCORD_BANNER=true
|
||||
NEXT_PUBLIC_DISABLE_DISCORD_BANNER=true
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,4 +1,9 @@
|
||||
.idea/
|
||||
bun.lock
|
||||
.env
|
||||
|
||||
node_modules
|
||||
.next
|
||||
.env.local
|
||||
.env*.local
|
||||
.env
|
||||
|
||||
125
src/app/page.tsx
125
src/app/page.tsx
@@ -89,6 +89,9 @@ export default function Home() {
|
||||
"https://sucker.severian.dev/api/proxy"
|
||||
);
|
||||
|
||||
const [pageSource, setPageSource] = useState("");
|
||||
const [metadataJson, setMetadataJson] = useState<any>(null);
|
||||
|
||||
const fetchCards = async () => {
|
||||
try {
|
||||
setIsRefreshing(true);
|
||||
@@ -233,6 +236,15 @@ export default function Home() {
|
||||
|
||||
const arrayBuffer = await pngBlob.arrayBuffer();
|
||||
|
||||
// Fallback if metadataJson hasn't been fetched for this card
|
||||
const safeMetadata = metadataJson || {};
|
||||
const creator = (safeMetadata.creator_name || (card.data as any).creator || "Unknown") + (safeMetadata.creator_verified ? " ✅" : "");
|
||||
|
||||
// Tag Parsing
|
||||
const normalTags = (safeMetadata.tags || (card.data as any).tags || []).map((t: any) => t.name);
|
||||
const customTags = safeMetadata.custom_tags || [];
|
||||
const allTagsArray = [...normalTags, ...customTags];
|
||||
|
||||
// Use initial version for PNG embedding, or current version if no initial version available
|
||||
const chosen = card.initialVersion || card.data;
|
||||
const pngData = {
|
||||
@@ -244,13 +256,13 @@ export default function Home() {
|
||||
personality: chosen.personality,
|
||||
mes_example: chosen.mes_example,
|
||||
scenario: chosen.scenario,
|
||||
creator: (chosen as any).creator || "",
|
||||
creator_notes: (chosen as any).creator_notes || "",
|
||||
creator: creator,
|
||||
creator_notes: safeMetadata.description || (chosen as any).creator_notes || "",
|
||||
system_prompt: (chosen as any).system_prompt || "",
|
||||
post_history_instructions:
|
||||
(chosen as any).post_history_instructions || "",
|
||||
tags: (chosen as any).tags || [],
|
||||
character_version: (chosen as any).character_version || "1",
|
||||
tags: allTagsArray,
|
||||
character_version: safeMetadata.name || (chosen as any).character_version || "1",
|
||||
extensions: (chosen as any).extensions || {},
|
||||
},
|
||||
spec: card.spec || "chara_card_v2",
|
||||
@@ -295,39 +307,9 @@ export default function Home() {
|
||||
const isCharacterUrl = /janitorai\.com\/characters\//.test(characterUrl);
|
||||
|
||||
if (isCharacterUrl) {
|
||||
// Extract character ID and open metadata page, then show second input
|
||||
const match = characterUrl.match(/characters\/([\w-]+)/);
|
||||
if (match && match[1]) {
|
||||
const characterId = match[1].split("_")[0];
|
||||
window.open(
|
||||
`https://janitorai.com/hampter/characters/${characterId}`,
|
||||
"_blank"
|
||||
);
|
||||
// Open html source, then show second input
|
||||
window.open(`view-source:${characterUrl}`, "_blank");
|
||||
setIsMetadataOpen(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the input is a direct image link (webp filename or full image URL)
|
||||
const isImagePath = /\.(webp|png|jpg|jpeg|gif)(\?.*)?$/i.test(characterUrl);
|
||||
const isFullImageUrl = (characterUrl.startsWith("http://") || characterUrl.startsWith("https://")) && isImagePath;
|
||||
const isWebpFilename = /^[\w-]+\.(webp|png|jpg|jpeg|gif)$/i.test(characterUrl);
|
||||
|
||||
if (isFullImageUrl || isWebpFilename) {
|
||||
// Directly set the avatar URL without opening metadata
|
||||
if (selectedCardIndex === null) return;
|
||||
|
||||
const avatarUrl = isFullImageUrl
|
||||
? characterUrl
|
||||
: `https://ella.janitorai.com/bot-avatars/${characterUrl}`;
|
||||
|
||||
const updatedCards = [...cards];
|
||||
updatedCards[selectedCardIndex] = {
|
||||
...updatedCards[selectedCardIndex],
|
||||
avatarUrl,
|
||||
};
|
||||
setCards(updatedCards);
|
||||
setDialogOpen(false);
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -336,7 +318,42 @@ export default function Home() {
|
||||
if (selectedCardIndex === null) return;
|
||||
|
||||
try {
|
||||
const avatarUrl = `https://ella.janitorai.com/bot-avatars/${avatarPath}`;
|
||||
const storeKey = "Sk--a:a-a--characterStore";
|
||||
|
||||
// Define the anchor points we are looking for
|
||||
const prefix = 'window.mbxM.push(JSON.parse("';
|
||||
const suffix = '"));';
|
||||
|
||||
// Locate the script content
|
||||
const startIndex = pageSource.indexOf(prefix);
|
||||
if (startIndex === -1) {
|
||||
throw new Error("Could not find character data in the page source.");
|
||||
}
|
||||
|
||||
// Move index to the start of the actual JSON string
|
||||
const jsonStartIndex = startIndex + prefix.length;
|
||||
|
||||
// Find the end of the statement
|
||||
const jsonEndIndex = pageSource.indexOf(suffix, jsonStartIndex);
|
||||
if (jsonEndIndex === -1) {
|
||||
throw new Error("Could not find closing tag for JSON content.");
|
||||
}
|
||||
|
||||
// Extract the escaped string
|
||||
const escapedJsonString = pageSource.substring(jsonStartIndex, jsonEndIndex);
|
||||
|
||||
// Wrap it in quotes so it gets treated as a string containing JSON
|
||||
const rawJsonString = JSON.parse(`"${escapedJsonString}"`);
|
||||
console.log(rawJsonString);
|
||||
|
||||
// Parse the actual JSON data into an object
|
||||
const data = JSON.parse(rawJsonString);
|
||||
|
||||
// Return the specific character data
|
||||
const char = data[storeKey].character;
|
||||
setMetadataJson(char);
|
||||
|
||||
const avatarUrl = `https://ella.janitorai.com/bot-avatars/${char.avatar}`;
|
||||
const updatedCards = [...cards];
|
||||
updatedCards[selectedCardIndex] = {
|
||||
...updatedCards[selectedCardIndex],
|
||||
@@ -742,7 +759,7 @@ export default function Home() {
|
||||
onClick={() => handleOpenDialog(index)}
|
||||
variant="outline"
|
||||
>
|
||||
Fetch Avatar (required for PNG)
|
||||
Fetch Metadata
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
@@ -750,7 +767,7 @@ export default function Home() {
|
||||
variant="default"
|
||||
disabled={!card.avatarUrl}
|
||||
>
|
||||
Download PNG
|
||||
Download Complete Card
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -993,43 +1010,25 @@ export default function Home() {
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{isMetadataOpen ? "Enter Avatar Path" : "Fetch Avatar"}
|
||||
{isMetadataOpen ? "Enter Page Source Code" : "Fetch Metadata"}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{isMetadataOpen
|
||||
? "Look for the avatar field in the opened tab and paste the value here."
|
||||
: "Enter a character URL (janitorai.com/characters/...) to open metadata, or paste an image filename (id.webp) or full image URL directly."}
|
||||
{isMetadataOpen ? "Paste the entire source code here." : "Enter a character URL (janitorai.com/characters/...) to open page source."}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{isMetadataOpen ? (
|
||||
<div className="space-y-4">
|
||||
<Input
|
||||
placeholder="id.webp"
|
||||
value={avatarPath}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setAvatarPath(e.target.value)
|
||||
}
|
||||
/>
|
||||
<Input placeholder="<!DOCTYPE html><html..." value={pageSource} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setPageSource(e.target.value)} />
|
||||
<Button onClick={handleFetchAvatar} className="w-full">
|
||||
Fetch Avatar
|
||||
Fetch Metadata
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<Input
|
||||
placeholder="URL or id.webp"
|
||||
value={characterUrl}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setCharacterUrl(e.target.value)
|
||||
}
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
For character URLs, a new tab will open with metadata. For image
|
||||
filenames or full image URLs, the avatar will be set directly.
|
||||
</p>
|
||||
<Input placeholder="URL" value={characterUrl} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setCharacterUrl(e.target.value)} />
|
||||
<Button onClick={handleOpenMetadata} className="w-full">
|
||||
Fetch Avatar
|
||||
Fetch Metadata
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user