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.
Files changed (2) hide show
  1. package/dist/index.js +195 -118
  2. 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("Saving character files...").start();
42077
- const blueprint = buildBlueprintFromPreset(config);
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, professional studio lighting, beautiful, editorial fashion, sharp focus"
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 portrait photography.
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: under 200 words, focus on visual details only, include quality tags, match the character's aesthetic.
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, effective portrait generation prompt. Preserve all physical details.`;
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, import_voice, import_stream4, DEBUG_LOG, prism, DiscordBridge;
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 connection = (0, import_voice.getVoiceConnection)(guildId);
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 connection = (0, import_voice.getVoiceConnection)(guildId);
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: import_voice.EndBehaviorType.AfterSilence,
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 = (0, import_voice.joinVoiceChannel)({
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 (0, import_voice.entersState)(connection, import_voice.VoiceConnectionStatus.Ready, 3e4);
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 connection = (0, import_voice.getVoiceConnection)(guildId);
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 connection = (0, import_voice.getVoiceConnection)(guildId);
235086
+ const v2 = getVoiceSync();
235087
+ if (!v2) return;
235088
+ const connection = v2.getVoiceConnection(guildId);
235016
235089
  if (!connection) return;
235017
- const player = (0, import_voice.createAudioPlayer)();
235090
+ const player = v2.createAudioPlayer();
235018
235091
  const readable = import_stream4.Readable.from(audioBuffer);
235019
- const resource = (0, import_voice.createAudioResource)(readable);
235092
+ const resource = v2.createAudioResource(readable);
235020
235093
  player.play(resource);
235021
235094
  connection.subscribe(player);
235022
- await (0, import_voice.entersState)(player, import_voice.AudioPlayerStatus.Idle, 6e4);
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(voice, other, signal) {
236454
+ replyWithVoice(voice2, other, signal) {
236381
236455
  var _a3;
236382
236456
  const msg = this.msg;
236383
- return this.api.sendVoice(orThrow(this.chatId, "sendVoice"), voice, {
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, voice, other, signal) {
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, "Pick your companion");
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
- const envValues = { CHARACTER_NAME: characterName };
245129
- step(2, `Where do you want to chat with ${characterName}?`);
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 location = ((_b2 = fromMatch == null ? void 0 : fromMatch[1]) == null ? void 0 : _b2.trim()) ?? "Unknown";
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 + 60;
245523
- const tagY = 280;
245524
- const tagsSvg = data.tags.map((tag, i2) => {
245525
- const x2 = textX + i2 * 0;
245526
- const inlineX = textX;
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
- const tagRowY = tagY;
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 w2 = tag.length * 8.5 + 24;
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 - 2}" y="${tagRowY - 16}" width="${w2}" height="26" rx="13" fill="rgba(255,255,255,0.10)" />
245540
- <text x="${offsetX + 10}" y="${tagRowY}" font-family="system-ui, -apple-system, sans-serif" font-size="13" fill="#c0c0c0">${escapeXml(tag)}</text>
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 + 8;
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
- <!-- Subtle border around portrait area -->
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
- <!-- Character Name -->
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
- <!-- Age + Location -->
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
- <!-- One-line description -->
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
- <!-- Bottom bar -->
245575
- <rect x="0" y="${HEIGHT - 50}" width="${WIDTH}" height="50" fill="rgba(0,0,0,0.3)" />
245576
- <text x="40" y="${HEIGHT - 20}" font-family="system-ui, -apple-system, sans-serif" font-size="16" font-weight="bold" fill="#ff69b4">Opencrush</text>
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: 150 }).resize(WIDTH, HEIGHT);
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 = 280;
245720
+ PORTRAIT_SIZE = 260;
245644
245721
  PORTRAIT_X = 80;
245645
- PORTRAIT_Y = 100;
245722
+ PORTRAIT_Y = (HEIGHT - PORTRAIT_SIZE) / 2 - 10;
245646
245723
  }
245647
245724
  });
245648
245725
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencrush",
3
- "version": "0.3.9",
3
+ "version": "0.3.11",
4
4
  "description": "Your AI companion lives on your device. She watches dramas, listens to music, and thinks of you.",
5
5
  "bin": {
6
6
  "opencrush": "dist/index.js"