opencrush 0.3.9 → 0.3.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +195 -118
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -42073,10 +42073,19 @@ async function createFromPreset(apiKey, provider) {
|
|
|
42073
42073
|
personality,
|
|
42074
42074
|
backstory
|
|
42075
42075
|
};
|
|
42076
|
-
const spinner = ora("
|
|
42077
|
-
|
|
42076
|
+
const spinner = ora("Building character files...").start();
|
|
42077
|
+
let blueprint = buildBlueprintFromPreset(config);
|
|
42078
|
+
if (apiKey && provider) {
|
|
42079
|
+
try {
|
|
42080
|
+
blueprint = await enrichBlueprintWithLLM(blueprint, config, apiKey, provider);
|
|
42081
|
+
spinner.succeed(source_default.green(`${displayName} created!`));
|
|
42082
|
+
} catch {
|
|
42083
|
+
spinner.succeed(source_default.green(`${displayName} created! (basic \u2014 add API key for richer files)`));
|
|
42084
|
+
}
|
|
42085
|
+
} else {
|
|
42086
|
+
spinner.succeed(source_default.green(`${displayName} created!`));
|
|
42087
|
+
}
|
|
42078
42088
|
writeCharacterFiles(folderName, blueprint);
|
|
42079
|
-
spinner.succeed(source_default.green(`${displayName} created!`));
|
|
42080
42089
|
const { hasPortrait, falKey } = await craftPortrait(config, folderName, apiKey, provider);
|
|
42081
42090
|
printCreationSuccess(folderName, displayName);
|
|
42082
42091
|
return { folderName, displayName, gender: preset.gender, hasPortrait, falKey };
|
|
@@ -42301,20 +42310,24 @@ function buildPortraitPrompt(config) {
|
|
|
42301
42310
|
`wearing ${appearance.fashionStyle} style`,
|
|
42302
42311
|
archetype.portraitBase,
|
|
42303
42312
|
vibeHints,
|
|
42304
|
-
"cinematic photography, ultra-detailed, 8k resolution,
|
|
42313
|
+
"cinematic photography, ultra-detailed, 8k resolution, beautiful, editorial fashion, sharp focus, atmospheric background matching character aesthetic, environmental storytelling, moody scene lighting"
|
|
42305
42314
|
];
|
|
42306
42315
|
return parts.filter(Boolean).join(", ");
|
|
42307
42316
|
}
|
|
42308
42317
|
async function optimizePortraitPrompt(basePrompt, config, apiKey, provider) {
|
|
42309
|
-
const system = `You are an expert at writing image generation prompts for
|
|
42318
|
+
const system = `You are an expert at writing image generation prompts for character portraits.
|
|
42310
42319
|
Given a character description and base prompt, write an optimized FLUX/Stable Diffusion prompt.
|
|
42311
|
-
Rules:
|
|
42320
|
+
Rules:
|
|
42321
|
+
- Under 200 words, focus on visual details, include quality tags
|
|
42322
|
+
- ALWAYS include a scene-appropriate background that matches the character's vibe and lifestyle (e.g. neon cityscape for cyberpunk, cozy caf\xE9 for warm characters, art studio for artists, rain-soaked street for moody characters)
|
|
42323
|
+
- NEVER use plain white, studio, or blank backgrounds
|
|
42324
|
+
- The background should tell a story about who this character is
|
|
42312
42325
|
Return ONLY the prompt text \u2014 no explanation, no quotes.`;
|
|
42313
42326
|
const userMsg = `Character: ${config.name}, ${config.gender}
|
|
42314
42327
|
Archetype: ${config.archetype.label}
|
|
42315
42328
|
Base prompt: ${basePrompt}
|
|
42316
42329
|
|
|
42317
|
-
Optimize this into a vivid
|
|
42330
|
+
Optimize this into a vivid portrait prompt with an atmospheric background that fits the character's world. Preserve all physical details.`;
|
|
42318
42331
|
try {
|
|
42319
42332
|
const result = await callLLMDirect(provider, apiKey, system, [{ role: "user", content: userMsg }], 300);
|
|
42320
42333
|
return result.trim() || basePrompt;
|
|
@@ -42358,6 +42371,38 @@ function openImage(filePath) {
|
|
|
42358
42371
|
(0, import_child_process2.exec)(cmd, () => {
|
|
42359
42372
|
});
|
|
42360
42373
|
}
|
|
42374
|
+
async function enrichBlueprintWithLLM(blueprint, config, apiKey, provider) {
|
|
42375
|
+
const system = `You are a character writer creating rich companion profiles. Write in second person removed \u2014 describe the character as they are, not to them. Be specific, vivid, contradictory. Real people have texture.
|
|
42376
|
+
|
|
42377
|
+
RULES:
|
|
42378
|
+
- Expand appearance into 2-3 paragraphs of vivid prose (not bullet points)
|
|
42379
|
+
- Add a ## Background section (2-3 paragraphs about their history, how they got here)
|
|
42380
|
+
- Flesh out the SOUL with specific examples, not generic traits
|
|
42381
|
+
- Keep the markdown structure and frontmatter exactly as given
|
|
42382
|
+
- Write in English only
|
|
42383
|
+
- Do NOT add any sections that aren't in the original
|
|
42384
|
+
- Return the FULL expanded file content, not just the additions`;
|
|
42385
|
+
const identityPrompt = `Expand this IDENTITY.md into a rich, detailed character file. The appearance section should be vivid prose paragraphs (like a novel character introduction), not a list. Add a ## Background section at the end with 2-3 paragraphs of backstory.
|
|
42386
|
+
|
|
42387
|
+
Current file:
|
|
42388
|
+
${blueprint.identity}
|
|
42389
|
+
|
|
42390
|
+
Character context: ${config.archetype.label} archetype, personality traits: ${config.personality.join(", ")}${config.backstory ? ", backstory moment: " + config.backstory : ""}`;
|
|
42391
|
+
const soulPrompt = `Expand this SOUL.md into a richer version. Add specific examples to each section. The "Voice & Vibe" section should be 2+ paragraphs. "Loves" and "Dislikes" should have 5-7 items each with colorful detail. Add a "## Things She Does" section with 6-8 specific behavioral habits. Keep the same structure.
|
|
42392
|
+
|
|
42393
|
+
Current file:
|
|
42394
|
+
${blueprint.soul}`;
|
|
42395
|
+
const [enrichedIdentity, enrichedSoul] = await Promise.all([
|
|
42396
|
+
callLLMDirect(provider, apiKey, system, [{ role: "user", content: identityPrompt }], 2e3).catch(() => blueprint.identity),
|
|
42397
|
+
callLLMDirect(provider, apiKey, system, [{ role: "user", content: soulPrompt }], 2e3).catch(() => blueprint.soul)
|
|
42398
|
+
]);
|
|
42399
|
+
return {
|
|
42400
|
+
identity: enrichedIdentity || blueprint.identity,
|
|
42401
|
+
soul: enrichedSoul || blueprint.soul,
|
|
42402
|
+
user: blueprint.user,
|
|
42403
|
+
memory: blueprint.memory
|
|
42404
|
+
};
|
|
42405
|
+
}
|
|
42361
42406
|
function buildBlueprintFromPreset(config) {
|
|
42362
42407
|
const { archetype, name, appearance, personality, backstory } = config;
|
|
42363
42408
|
const renameIn = (text) => text.replace(new RegExp(`# ${escapeRegex(archetype.id.charAt(0).toUpperCase() + archetype.id.slice(1))}`, "g"), `# ${name}`).replace(new RegExp(`\\b${escapeRegex(archetype.id)}\\b`, "gi"), name).replace(new RegExp(`\\b(Yuna|Valentina|Hana|Nyx|Hu Lan|Riot|Kai|Eli)\\b`, "g"), name);
|
|
@@ -234588,6 +234633,21 @@ function debugLog3(msg) {
|
|
|
234588
234633
|
} catch {
|
|
234589
234634
|
}
|
|
234590
234635
|
}
|
|
234636
|
+
async function loadVoice() {
|
|
234637
|
+
if (voice) return voice;
|
|
234638
|
+
if (voiceLoadError) return null;
|
|
234639
|
+
try {
|
|
234640
|
+
voice = await import("@discordjs/voice");
|
|
234641
|
+
return voice;
|
|
234642
|
+
} catch (err) {
|
|
234643
|
+
voiceLoadError = err instanceof Error ? err.message : String(err);
|
|
234644
|
+
console.warn(`[Discord] @discordjs/voice not available \u2014 voice features disabled (${voiceLoadError})`);
|
|
234645
|
+
return null;
|
|
234646
|
+
}
|
|
234647
|
+
}
|
|
234648
|
+
function getVoiceSync() {
|
|
234649
|
+
return voice;
|
|
234650
|
+
}
|
|
234591
234651
|
async function loadPrism() {
|
|
234592
234652
|
if (!prism) {
|
|
234593
234653
|
prism = await import("prism-media");
|
|
@@ -234610,15 +234670,16 @@ function splitMessage(text, maxLength = 1900) {
|
|
|
234610
234670
|
if (current) chunks.push(current.trim());
|
|
234611
234671
|
return chunks;
|
|
234612
234672
|
}
|
|
234613
|
-
var import_discord, import_fs15,
|
|
234673
|
+
var import_discord, import_fs15, import_stream4, DEBUG_LOG, voice, voiceLoadError, prism, DiscordBridge;
|
|
234614
234674
|
var init_dist5 = __esm({
|
|
234615
234675
|
"../bridges/discord/dist/index.mjs"() {
|
|
234616
234676
|
"use strict";
|
|
234617
234677
|
import_discord = __toESM(require_src3(), 1);
|
|
234618
234678
|
import_fs15 = require("fs");
|
|
234619
|
-
import_voice = require("@discordjs/voice");
|
|
234620
234679
|
import_stream4 = require("stream");
|
|
234621
234680
|
DEBUG_LOG = "/tmp/opencrush-debug.log";
|
|
234681
|
+
voice = null;
|
|
234682
|
+
voiceLoadError = null;
|
|
234622
234683
|
prism = null;
|
|
234623
234684
|
DiscordBridge = class {
|
|
234624
234685
|
client;
|
|
@@ -234839,6 +234900,8 @@ var init_dist5 = __esm({
|
|
|
234839
234900
|
}
|
|
234840
234901
|
async handleVoiceState(oldState, newState) {
|
|
234841
234902
|
var _a3;
|
|
234903
|
+
const v2 = await loadVoice();
|
|
234904
|
+
if (!v2) return;
|
|
234842
234905
|
if (((_a3 = newState.member) == null ? void 0 : _a3.id) !== this.config.ownerId) return;
|
|
234843
234906
|
const userJoinedVoice = !oldState.channelId && newState.channelId;
|
|
234844
234907
|
const userLeftVoice = oldState.channelId && !newState.channelId;
|
|
@@ -234868,7 +234931,9 @@ var init_dist5 = __esm({
|
|
|
234868
234931
|
* Creates a loop: listen → transcribe → LLM → TTS → speak → repeat.
|
|
234869
234932
|
*/
|
|
234870
234933
|
startVoiceConversation(guildId) {
|
|
234871
|
-
const
|
|
234934
|
+
const v2 = getVoiceSync();
|
|
234935
|
+
if (!v2) return;
|
|
234936
|
+
const connection = v2.getVoiceConnection(guildId);
|
|
234872
234937
|
if (!connection) return;
|
|
234873
234938
|
this.voiceConversationActive.set(guildId, true);
|
|
234874
234939
|
console.log(`[Discord/Voice] Starting voice conversation in guild ${guildId}`);
|
|
@@ -234881,7 +234946,9 @@ var init_dist5 = __esm({
|
|
|
234881
234946
|
}
|
|
234882
234947
|
async listenForNextUtterance(guildId) {
|
|
234883
234948
|
if (!this.voiceConversationActive.get(guildId)) return;
|
|
234884
|
-
const
|
|
234949
|
+
const v2 = getVoiceSync();
|
|
234950
|
+
if (!v2) return;
|
|
234951
|
+
const connection = v2.getVoiceConnection(guildId);
|
|
234885
234952
|
if (!connection) return;
|
|
234886
234953
|
if (this.isSpeaking.get(guildId)) {
|
|
234887
234954
|
setTimeout(() => this.listenForNextUtterance(guildId), 500);
|
|
@@ -234891,7 +234958,7 @@ var init_dist5 = __esm({
|
|
|
234891
234958
|
const receiver = connection.receiver;
|
|
234892
234959
|
const opusStream = receiver.subscribe(this.config.ownerId, {
|
|
234893
234960
|
end: {
|
|
234894
|
-
behavior:
|
|
234961
|
+
behavior: v2.EndBehaviorType.AfterSilence,
|
|
234895
234962
|
duration: 1500
|
|
234896
234963
|
}
|
|
234897
234964
|
});
|
|
@@ -234990,36 +235057,42 @@ var init_dist5 = __esm({
|
|
|
234990
235057
|
}
|
|
234991
235058
|
// ── Voice Channel Management ───────────────────────────────
|
|
234992
235059
|
async joinVoiceChannel(channelId, guildId, adapterCreator) {
|
|
235060
|
+
const v2 = getVoiceSync();
|
|
235061
|
+
if (!v2) return;
|
|
234993
235062
|
try {
|
|
234994
|
-
const connection =
|
|
235063
|
+
const connection = v2.joinVoiceChannel({
|
|
234995
235064
|
channelId,
|
|
234996
235065
|
guildId,
|
|
234997
235066
|
adapterCreator,
|
|
234998
235067
|
selfDeaf: false,
|
|
234999
235068
|
selfMute: false
|
|
235000
235069
|
});
|
|
235001
|
-
await
|
|
235070
|
+
await v2.entersState(connection, v2.VoiceConnectionStatus.Ready, 3e4);
|
|
235002
235071
|
console.log(`[Discord] Joined voice channel ${channelId}`);
|
|
235003
235072
|
} catch (err) {
|
|
235004
235073
|
console.error("[Discord] Failed to join voice channel:", err);
|
|
235005
235074
|
}
|
|
235006
235075
|
}
|
|
235007
235076
|
leaveVoiceChannel(guildId) {
|
|
235008
|
-
const
|
|
235077
|
+
const v2 = getVoiceSync();
|
|
235078
|
+
if (!v2) return;
|
|
235079
|
+
const connection = v2.getVoiceConnection(guildId);
|
|
235009
235080
|
if (connection) {
|
|
235010
235081
|
connection.destroy();
|
|
235011
235082
|
console.log(`[Discord] Left voice channel in guild ${guildId}`);
|
|
235012
235083
|
}
|
|
235013
235084
|
}
|
|
235014
235085
|
async playAudio(guildId, audioBuffer) {
|
|
235015
|
-
const
|
|
235086
|
+
const v2 = getVoiceSync();
|
|
235087
|
+
if (!v2) return;
|
|
235088
|
+
const connection = v2.getVoiceConnection(guildId);
|
|
235016
235089
|
if (!connection) return;
|
|
235017
|
-
const player =
|
|
235090
|
+
const player = v2.createAudioPlayer();
|
|
235018
235091
|
const readable = import_stream4.Readable.from(audioBuffer);
|
|
235019
|
-
const resource =
|
|
235092
|
+
const resource = v2.createAudioResource(readable);
|
|
235020
235093
|
player.play(resource);
|
|
235021
235094
|
connection.subscribe(player);
|
|
235022
|
-
await
|
|
235095
|
+
await v2.entersState(player, v2.AudioPlayerStatus.Idle, 6e4);
|
|
235023
235096
|
}
|
|
235024
235097
|
// ── Message Handling ───────────────────────────────────────
|
|
235025
235098
|
/**
|
|
@@ -235215,6 +235288,7 @@ var init_dist5 = __esm({
|
|
|
235215
235288
|
}
|
|
235216
235289
|
}
|
|
235217
235290
|
async start() {
|
|
235291
|
+
await loadVoice();
|
|
235218
235292
|
await this.client.login(this.config.token);
|
|
235219
235293
|
}
|
|
235220
235294
|
async stop() {
|
|
@@ -236377,10 +236451,10 @@ var require_context = __commonJS({
|
|
|
236377
236451
|
*
|
|
236378
236452
|
* **Official reference:** https://core.telegram.org/bots/api#sendvoice
|
|
236379
236453
|
*/
|
|
236380
|
-
replyWithVoice(
|
|
236454
|
+
replyWithVoice(voice2, other, signal) {
|
|
236381
236455
|
var _a3;
|
|
236382
236456
|
const msg = this.msg;
|
|
236383
|
-
return this.api.sendVoice(orThrow(this.chatId, "sendVoice"),
|
|
236457
|
+
return this.api.sendVoice(orThrow(this.chatId, "sendVoice"), voice2, {
|
|
236384
236458
|
business_connection_id: this.businessConnectionId,
|
|
236385
236459
|
...(msg === null || msg === void 0 ? void 0 : msg.is_topic_message) ? { message_thread_id: msg.message_thread_id } : {},
|
|
236386
236460
|
direct_messages_topic_id: (_a3 = msg === null || msg === void 0 ? void 0 : msg.direct_messages_topic) === null || _a3 === void 0 ? void 0 : _a3.topic_id,
|
|
@@ -239463,8 +239537,8 @@ var require_api3 = __commonJS({
|
|
|
239463
239537
|
*
|
|
239464
239538
|
* **Official reference:** https://core.telegram.org/bots/api#sendvoice
|
|
239465
239539
|
*/
|
|
239466
|
-
sendVoice(chat_id,
|
|
239467
|
-
return this.raw.sendVoice({ chat_id, voice, ...other }, signal);
|
|
239540
|
+
sendVoice(chat_id, voice2, other, signal) {
|
|
239541
|
+
return this.raw.sendVoice({ chat_id, voice: voice2, ...other }, signal);
|
|
239468
239542
|
}
|
|
239469
239543
|
/**
|
|
239470
239544
|
* Use this method to send video messages. On success, the sent Message is returned.
|
|
@@ -245081,7 +245155,64 @@ async function runSetupWizard() {
|
|
|
245081
245155
|
const TOTAL = 4;
|
|
245082
245156
|
const step = (n2, label) => console.log(source_default.cyan(`
|
|
245083
245157
|
[${n2}/${TOTAL}] ${label}`));
|
|
245084
|
-
step(1, "
|
|
245158
|
+
step(1, "Choose your AI brain");
|
|
245159
|
+
console.log(source_default.gray(" Pick whichever you already have a key for.\n"));
|
|
245160
|
+
const envValues = {};
|
|
245161
|
+
const providerChoices = [
|
|
245162
|
+
PROVIDER_INFO.find((p2) => p2.id === "anthropic"),
|
|
245163
|
+
PROVIDER_INFO.find((p2) => p2.id === "openai"),
|
|
245164
|
+
PROVIDER_INFO.find((p2) => p2.id === "deepseek"),
|
|
245165
|
+
PROVIDER_INFO.find((p2) => p2.isLocal)
|
|
245166
|
+
].filter(Boolean);
|
|
245167
|
+
const { llmProvider } = await esm_default12.prompt([{
|
|
245168
|
+
type: "list",
|
|
245169
|
+
name: "llmProvider",
|
|
245170
|
+
message: "Which AI provider?",
|
|
245171
|
+
choices: providerChoices.map((p2) => ({
|
|
245172
|
+
name: `${p2.emoji} ${p2.name}
|
|
245173
|
+
${source_default.gray(p2.tagline)}`,
|
|
245174
|
+
value: p2.id,
|
|
245175
|
+
short: p2.name
|
|
245176
|
+
}))
|
|
245177
|
+
}]);
|
|
245178
|
+
envValues.LLM_PROVIDER = llmProvider;
|
|
245179
|
+
const providerInfo = getProviderInfo(llmProvider);
|
|
245180
|
+
let collectedApiKey;
|
|
245181
|
+
let collectedProvider;
|
|
245182
|
+
if (llmProvider !== "ollama") {
|
|
245183
|
+
console.log(source_default.yellow(`
|
|
245184
|
+
Get your API key: ${providerInfo.keyUrl}`));
|
|
245185
|
+
await openBrowser(providerInfo.keyUrl);
|
|
245186
|
+
if (llmProvider === "anthropic") {
|
|
245187
|
+
console.log(source_default.gray(' Sign up \u2192 API Keys \u2192 Create Key \u2192 copy the key (starts with "sk-ant-")\n'));
|
|
245188
|
+
} else if (llmProvider === "openai") {
|
|
245189
|
+
console.log(source_default.gray(" Sign up \u2192 API Keys \u2192 Create new secret key\n"));
|
|
245190
|
+
} else if (llmProvider === "deepseek") {
|
|
245191
|
+
console.log(source_default.gray(" Sign up \u2192 API Keys \u2192 Create key (new users get free credits)\n"));
|
|
245192
|
+
}
|
|
245193
|
+
const { apiKey } = await esm_default12.prompt([{
|
|
245194
|
+
type: "password",
|
|
245195
|
+
name: "apiKey",
|
|
245196
|
+
message: `Paste your ${providerInfo.name} API key:`,
|
|
245197
|
+
mask: "*",
|
|
245198
|
+
validate: (v2) => {
|
|
245199
|
+
if (!v2.trim()) return "API key required";
|
|
245200
|
+
if (providerInfo.keyPrefix && !v2.startsWith(providerInfo.keyPrefix)) {
|
|
245201
|
+
return `Should start with "${providerInfo.keyPrefix}"`;
|
|
245202
|
+
}
|
|
245203
|
+
return true;
|
|
245204
|
+
}
|
|
245205
|
+
}]);
|
|
245206
|
+
envValues[providerInfo.envKey] = apiKey;
|
|
245207
|
+
collectedApiKey = apiKey;
|
|
245208
|
+
collectedProvider = llmProvider;
|
|
245209
|
+
} else {
|
|
245210
|
+
console.log(source_default.yellow("\n Make sure Ollama is running: https://ollama.ai"));
|
|
245211
|
+
console.log(source_default.gray(" Run: ollama pull qwen2.5:7b\n"));
|
|
245212
|
+
envValues.OLLAMA_BASE_URL = "http://localhost:11434";
|
|
245213
|
+
envValues.OLLAMA_MODEL = "qwen2.5:7b";
|
|
245214
|
+
}
|
|
245215
|
+
step(2, "Pick your companion");
|
|
245085
245216
|
const characters = getExistingCharacters();
|
|
245086
245217
|
let characterName;
|
|
245087
245218
|
let characterCreatedNew = false;
|
|
@@ -245108,7 +245239,7 @@ async function runSetupWizard() {
|
|
|
245108
245239
|
choices
|
|
245109
245240
|
}]);
|
|
245110
245241
|
if (pick === "__new__") {
|
|
245111
|
-
const created = await createCharacterFlow();
|
|
245242
|
+
const created = await createCharacterFlow(collectedApiKey, collectedProvider);
|
|
245112
245243
|
characterName = created.folderName;
|
|
245113
245244
|
characterCreatedNew = true;
|
|
245114
245245
|
gender = created.gender;
|
|
@@ -245119,14 +245250,17 @@ async function runSetupWizard() {
|
|
|
245119
245250
|
}
|
|
245120
245251
|
} else {
|
|
245121
245252
|
console.log(source_default.gray(" No companions found yet \u2014 let's create one!\n"));
|
|
245122
|
-
const created = await createCharacterFlow();
|
|
245253
|
+
const created = await createCharacterFlow(collectedApiKey, collectedProvider);
|
|
245123
245254
|
characterName = created.folderName;
|
|
245124
245255
|
characterCreatedNew = true;
|
|
245125
245256
|
gender = created.gender;
|
|
245126
245257
|
}
|
|
245258
|
+
envValues.CHARACTER_NAME = characterName;
|
|
245127
245259
|
const pro = pronouns(gender);
|
|
245128
|
-
|
|
245129
|
-
|
|
245260
|
+
if (characterCreatedNew && collectedApiKey) {
|
|
245261
|
+
await runTestChat(characterName, collectedApiKey, collectedProvider ?? "anthropic");
|
|
245262
|
+
}
|
|
245263
|
+
step(3, `Where do you want to chat with ${characterName}?`);
|
|
245130
245264
|
const { platforms } = await esm_default12.prompt([{
|
|
245131
245265
|
type: "checkbox",
|
|
245132
245266
|
name: "platforms",
|
|
@@ -245192,61 +245326,6 @@ async function runSetupWizard() {
|
|
|
245192
245326
|
WhatsApp needs no setup! A QR code will appear when ${characterName} starts.`));
|
|
245193
245327
|
console.log(source_default.gray(" Scan it with WhatsApp \u2192 Linked Devices on your phone."));
|
|
245194
245328
|
}
|
|
245195
|
-
step(3, `Give ${characterName} a brain`);
|
|
245196
|
-
console.log(source_default.gray(" Pick whichever you already have a key for.\n"));
|
|
245197
|
-
const providerChoices = [
|
|
245198
|
-
PROVIDER_INFO.find((p2) => p2.id === "anthropic"),
|
|
245199
|
-
PROVIDER_INFO.find((p2) => p2.id === "openai"),
|
|
245200
|
-
PROVIDER_INFO.find((p2) => p2.id === "deepseek"),
|
|
245201
|
-
PROVIDER_INFO.find((p2) => p2.isLocal)
|
|
245202
|
-
].filter(Boolean);
|
|
245203
|
-
const { llmProvider } = await esm_default12.prompt([{
|
|
245204
|
-
type: "list",
|
|
245205
|
-
name: "llmProvider",
|
|
245206
|
-
message: "Which AI provider?",
|
|
245207
|
-
choices: providerChoices.map((p2) => ({
|
|
245208
|
-
name: `${p2.emoji} ${p2.name}
|
|
245209
|
-
${source_default.gray(p2.tagline)}`,
|
|
245210
|
-
value: p2.id,
|
|
245211
|
-
short: p2.name
|
|
245212
|
-
}))
|
|
245213
|
-
}]);
|
|
245214
|
-
envValues.LLM_PROVIDER = llmProvider;
|
|
245215
|
-
const providerInfo = getProviderInfo(llmProvider);
|
|
245216
|
-
if (llmProvider !== "ollama") {
|
|
245217
|
-
console.log(source_default.yellow(`
|
|
245218
|
-
Get your API key: ${providerInfo.keyUrl}`));
|
|
245219
|
-
await openBrowser(providerInfo.keyUrl);
|
|
245220
|
-
if (llmProvider === "anthropic") {
|
|
245221
|
-
console.log(source_default.gray(' Sign up \u2192 API Keys \u2192 Create Key \u2192 copy the key (starts with "sk-ant-")\n'));
|
|
245222
|
-
} else if (llmProvider === "openai") {
|
|
245223
|
-
console.log(source_default.gray(" Sign up \u2192 API Keys \u2192 Create new secret key\n"));
|
|
245224
|
-
} else if (llmProvider === "deepseek") {
|
|
245225
|
-
console.log(source_default.gray(" Sign up \u2192 API Keys \u2192 Create key (new users get free credits)\n"));
|
|
245226
|
-
}
|
|
245227
|
-
const { apiKey } = await esm_default12.prompt([{
|
|
245228
|
-
type: "password",
|
|
245229
|
-
name: "apiKey",
|
|
245230
|
-
message: `Paste your ${providerInfo.name} API key:`,
|
|
245231
|
-
mask: "*",
|
|
245232
|
-
validate: (v2) => {
|
|
245233
|
-
if (!v2.trim()) return "API key required";
|
|
245234
|
-
if (providerInfo.keyPrefix && !v2.startsWith(providerInfo.keyPrefix)) {
|
|
245235
|
-
return `Should start with "${providerInfo.keyPrefix}"`;
|
|
245236
|
-
}
|
|
245237
|
-
return true;
|
|
245238
|
-
}
|
|
245239
|
-
}]);
|
|
245240
|
-
envValues[providerInfo.envKey] = apiKey;
|
|
245241
|
-
if (characterCreatedNew) {
|
|
245242
|
-
await runTestChat(characterName, apiKey, llmProvider);
|
|
245243
|
-
}
|
|
245244
|
-
} else {
|
|
245245
|
-
console.log(source_default.yellow("\n Make sure Ollama is running: https://ollama.ai"));
|
|
245246
|
-
console.log(source_default.gray(" Run: ollama pull qwen2.5:7b\n"));
|
|
245247
|
-
envValues.OLLAMA_BASE_URL = "http://localhost:11434";
|
|
245248
|
-
envValues.OLLAMA_MODEL = "qwen2.5:7b";
|
|
245249
|
-
}
|
|
245250
245329
|
step(4, "Extras");
|
|
245251
245330
|
console.log(source_default.gray(" All optional \u2014 skip everything by pressing Enter.\n"));
|
|
245252
245331
|
const quickChoices = [
|
|
@@ -245490,7 +245569,8 @@ function parseIdentity(identityPath) {
|
|
|
245490
245569
|
const ageMatch = content.match(/\*\*Age:\*\*\s*(\d+)/i);
|
|
245491
245570
|
const age = (ageMatch == null ? void 0 : ageMatch[1]) ?? "??";
|
|
245492
245571
|
const fromMatch = content.match(/\*\*From:\*\*\s*(.+)/i);
|
|
245493
|
-
const
|
|
245572
|
+
const locationRaw = ((_b2 = fromMatch == null ? void 0 : fromMatch[1]) == null ? void 0 : _b2.trim()) ?? "Unknown";
|
|
245573
|
+
const location = locationRaw.replace(/\s*\(.*\)/, "").split(" \u2014 ")[0].trim();
|
|
245494
245574
|
const hobbiesMatch = content.match(/\*\*Hobbies:\*\*\s*(.+)/i);
|
|
245495
245575
|
const hobbiesRaw = (hobbiesMatch == null ? void 0 : hobbiesMatch[1]) ?? "";
|
|
245496
245576
|
const tags = hobbiesRaw.split(",").map((t2) => t2.trim()).filter(Boolean).slice(0, 5);
|
|
@@ -245519,29 +245599,33 @@ function pickGradient(name) {
|
|
|
245519
245599
|
return gradients[hash % gradients.length];
|
|
245520
245600
|
}
|
|
245521
245601
|
function createSvgOverlay(data, gradient) {
|
|
245522
|
-
const textX = PORTRAIT_X + PORTRAIT_SIZE +
|
|
245523
|
-
const
|
|
245524
|
-
const
|
|
245525
|
-
|
|
245526
|
-
|
|
245527
|
-
const inlineY = tagY + i2 * 36;
|
|
245528
|
-
return `
|
|
245529
|
-
<rect x="${inlineX - 4}" y="${inlineY - 18}" width="${tag.length * 10 + 24}" height="28" rx="14" fill="rgba(255,255,255,0.12)" />
|
|
245530
|
-
<text x="${inlineX + 8}" y="${inlineY}" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#e0e0e0">${escapeXml(tag)}</text>
|
|
245531
|
-
`;
|
|
245532
|
-
}).join("\n");
|
|
245602
|
+
const textX = PORTRAIT_X + PORTRAIT_SIZE + 50;
|
|
245603
|
+
const maxTagX = WIDTH - 30;
|
|
245604
|
+
const nameY = 210;
|
|
245605
|
+
const metaY = nameY + 45;
|
|
245606
|
+
const tagStartY = metaY + 50;
|
|
245533
245607
|
let inlineTags = "";
|
|
245534
245608
|
let offsetX = textX;
|
|
245535
|
-
|
|
245609
|
+
let rowY = tagStartY;
|
|
245610
|
+
const rowHeight = 30;
|
|
245611
|
+
const maxRows = 2;
|
|
245612
|
+
let currentRow = 1;
|
|
245536
245613
|
for (const tag of data.tags) {
|
|
245537
|
-
const
|
|
245614
|
+
const label = tag.length > 18 ? tag.slice(0, 17) + "\u2026" : tag;
|
|
245615
|
+
const w2 = label.length * 7.5 + 16;
|
|
245616
|
+
if (offsetX + w2 > maxTagX) {
|
|
245617
|
+
if (currentRow >= maxRows) break;
|
|
245618
|
+
currentRow++;
|
|
245619
|
+
offsetX = textX;
|
|
245620
|
+
rowY += rowHeight;
|
|
245621
|
+
}
|
|
245538
245622
|
inlineTags += `
|
|
245539
|
-
<rect x="${offsetX
|
|
245540
|
-
<text x="${offsetX +
|
|
245623
|
+
<rect x="${offsetX}" y="${rowY - 14}" width="${w2}" height="22" rx="11" fill="rgba(255,255,255,0.10)" />
|
|
245624
|
+
<text x="${offsetX + 8}" y="${rowY + 1}" font-family="system-ui, -apple-system, sans-serif" font-size="12" fill="#c0c0c0">${escapeXml(label)}</text>
|
|
245541
245625
|
`;
|
|
245542
|
-
offsetX += w2 +
|
|
245543
|
-
if (offsetX > WIDTH - 40) break;
|
|
245626
|
+
offsetX += w2 + 6;
|
|
245544
245627
|
}
|
|
245628
|
+
const descriptionY = tagStartY + currentRow * rowHeight + 14;
|
|
245545
245629
|
return `<svg width="${WIDTH}" height="${HEIGHT}" xmlns="http://www.w3.org/2000/svg">
|
|
245546
245630
|
<defs>
|
|
245547
245631
|
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
|
@@ -245553,28 +245637,21 @@ function createSvgOverlay(data, gradient) {
|
|
|
245553
245637
|
</clipPath>
|
|
245554
245638
|
</defs>
|
|
245555
245639
|
|
|
245556
|
-
<!-- Background -->
|
|
245557
245640
|
<rect width="${WIDTH}" height="${HEIGHT}" fill="url(#bg)" />
|
|
245558
245641
|
|
|
245559
|
-
|
|
245560
|
-
<circle cx="${PORTRAIT_X + PORTRAIT_SIZE / 2}" cy="${PORTRAIT_Y + PORTRAIT_SIZE / 2}" r="${PORTRAIT_SIZE / 2 + 3}" fill="none" stroke="rgba(255,255,255,0.15)" stroke-width="2" />
|
|
245642
|
+
<circle cx="${PORTRAIT_X + PORTRAIT_SIZE / 2}" cy="${PORTRAIT_Y + PORTRAIT_SIZE / 2}" r="${PORTRAIT_SIZE / 2 + 2}" fill="none" stroke="rgba(255,255,255,0.12)" stroke-width="2" />
|
|
245561
245643
|
|
|
245562
|
-
|
|
245563
|
-
<text x="${textX}" y="170" font-family="system-ui, -apple-system, sans-serif" font-size="48" font-weight="bold" fill="white">${escapeXml(data.name)}</text>
|
|
245644
|
+
<text x="${textX}" y="${nameY}" font-family="system-ui, -apple-system, sans-serif" font-size="44" font-weight="bold" fill="white">${escapeXml(data.name)}</text>
|
|
245564
245645
|
|
|
245565
|
-
|
|
245566
|
-
<text x="${textX}" y="215" font-family="system-ui, -apple-system, sans-serif" font-size="18" fill="#a0a0a0">${escapeXml(data.age)} \xB7 ${escapeXml(truncate(data.location, 60))}</text>
|
|
245646
|
+
<text x="${textX}" y="${metaY}" font-family="system-ui, -apple-system, sans-serif" font-size="16" fill="#a0a0a0">${escapeXml(data.age)} \xB7 ${escapeXml(truncate(data.location, 35))}</text>
|
|
245567
245647
|
|
|
245568
|
-
<!-- Personality Tags (inline) -->
|
|
245569
245648
|
${inlineTags}
|
|
245570
245649
|
|
|
245571
|
-
|
|
245572
|
-
<text x="${textX}" y="${tagRowY + 50}" font-family="system-ui, -apple-system, sans-serif" font-size="15" fill="#b0b0b0" font-style="italic">${escapeXml(truncate(data.description, 70))}</text>
|
|
245650
|
+
<text x="${textX}" y="${descriptionY}" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#b0b0b0" font-style="italic">${escapeXml(truncate(data.description, 60))}</text>
|
|
245573
245651
|
|
|
245574
|
-
|
|
245575
|
-
<
|
|
245576
|
-
<text x="
|
|
245577
|
-
<text x="${WIDTH - 40}" y="${HEIGHT - 20}" font-family="system-ui, -apple-system, sans-serif" font-size="13" fill="#888" text-anchor="end">github.com/Hollandchirs/Opencrush</text>
|
|
245652
|
+
<rect x="0" y="${HEIGHT - 44}" width="${WIDTH}" height="44" fill="rgba(0,0,0,0.3)" />
|
|
245653
|
+
<text x="36" y="${HEIGHT - 17}" font-family="system-ui, -apple-system, sans-serif" font-size="14" font-weight="bold" fill="#ff69b4">Opencrush</text>
|
|
245654
|
+
<text x="${WIDTH - 36}" y="${HEIGHT - 17}" font-family="system-ui, -apple-system, sans-serif" font-size="12" fill="#888" text-anchor="end">github.com/Hollandchirs/Opencrush</text>
|
|
245578
245655
|
</svg>`;
|
|
245579
245656
|
}
|
|
245580
245657
|
function escapeXml(str2) {
|
|
@@ -245607,7 +245684,7 @@ async function generateCard(characterName) {
|
|
|
245607
245684
|
}
|
|
245608
245685
|
const svgOverlay = createSvgOverlay(data, gradient);
|
|
245609
245686
|
const svgBuffer = Buffer.from(svgOverlay);
|
|
245610
|
-
let card = sharp(svgBuffer, { density:
|
|
245687
|
+
let card = sharp(svgBuffer, { density: 300 }).resize(WIDTH, HEIGHT);
|
|
245611
245688
|
if (refImagePath) {
|
|
245612
245689
|
const circleMask = Buffer.from(
|
|
245613
245690
|
`<svg width="${PORTRAIT_SIZE}" height="${PORTRAIT_SIZE}">
|
|
@@ -245640,9 +245717,9 @@ var init_card = __esm({
|
|
|
245640
245717
|
init_source();
|
|
245641
245718
|
WIDTH = 1200;
|
|
245642
245719
|
HEIGHT = 630;
|
|
245643
|
-
PORTRAIT_SIZE =
|
|
245720
|
+
PORTRAIT_SIZE = 260;
|
|
245644
245721
|
PORTRAIT_X = 80;
|
|
245645
|
-
PORTRAIT_Y =
|
|
245722
|
+
PORTRAIT_Y = (HEIGHT - PORTRAIT_SIZE) / 2 - 10;
|
|
245646
245723
|
}
|
|
245647
245724
|
});
|
|
245648
245725
|
|