rmi-backend/app/card_generator.py

550 lines
17 KiB
Python

"""
RMI Card Generator - Template-based card generation with text overlay.
Pre-designed graphics with dynamic text overlay.
Fast, consistent, professional trading platform aesthetic.
"""
import hashlib
import logging
import os
from datetime import UTC, datetime
from PIL import Image, ImageDraw, ImageFont
logger = logging.getLogger(__name__)
# ── Paths ────────────────────────────────────────────────────
TEMPLATE_DIR = "/root/backend/assets/card_templates"
GENERATED_DIR = "/root/backend/assets/generated_cards"
# Ensure directories exist
os.makedirs(TEMPLATE_DIR, exist_ok=True)
os.makedirs(GENERATED_DIR, exist_ok=True)
# ── Card Templates Config ────────────────────────────────────
CARD_TEMPLATES = {
"big_win": {
"name": "Big Win Alert",
"template_file": "big_win_template.png",
"size": (1200, 675), # Twitter optimized
"text_fields": {
"wallet_address": {
"position": (80, 150),
"font_size": 32,
"color": "#FFFFFF",
"format": "0x{first8}...{last6}",
},
"token_symbol": {
"position": (600, 280),
"font_size": 72,
"color": "#00FF88",
"format": "{symbol}",
},
"pnl_usd": {
"position": (600, 400),
"font_size": 96,
"color": "#00FF88",
"format": "+${amount:,.0f}",
},
"pnl_pct": {
"position": (600, 500),
"font_size": 64,
"color": "#00FF88",
"format": "({pct:.1f}%)",
},
},
"colors": {
"primary": "#00FF88", # Neon green
"accent": "#FFD700",
},
},
"big_loss": {
"name": "Loss Porn Alert",
"template_file": "big_loss_template.png",
"size": (1200, 675),
"text_fields": {
"wallet_address": {
"position": (80, 150),
"font_size": 32,
"color": "#FFFFFF",
"format": "0x{first8}...{last6}",
},
"token_symbol": {
"position": (600, 280),
"font_size": 72,
"color": "#FF4444",
"format": "{symbol}",
},
"pnl_usd": {
"position": (600, 400),
"font_size": 96,
"color": "#FF4444",
"format": "-${amount:,.0f}",
},
"pnl_pct": {
"position": (600, 500),
"font_size": 64,
"color": "#FF4444",
"format": "({pct:.1f}%)",
},
},
"colors": {
"primary": "#FF4444", # Neon red
"accent": "#FF6B6B",
},
},
"kol_scorecard": {
"name": "KOL Scorecard",
"template_file": "kol_scorecard_template.png",
"size": (1200, 1200), # Square
"text_fields": {
"kol_handle": {
"position": (600, 200),
"font_size": 56,
"color": "#FFFFFF",
"format": "@{handle}",
},
"tier": {
"position": (1000, 150),
"font_size": 48,
"color": "#A855F7",
"format": "{tier} TIER",
},
"win_rate": {
"position": (300, 400),
"font_size": 64,
"color": "#00FF88",
"format": "{rate:.1f}%",
},
"total_calls": {
"position": (300, 500),
"font_size": 48,
"color": "#FFFFFF",
"format": "{count}",
},
"avg_pnl": {
"position": (300, 600),
"font_size": 48,
"color": "#FFD700",
"format": "{pnl:.1f}%",
},
"followers": {
"position": (300, 700),
"font_size": 48,
"color": "#FFFFFF",
"format": "{count:,}",
},
},
"colors": {
"primary": "#A855F7", # Purple
"accent": "#F472B6",
},
},
"rug_call": {
"name": "Rugpull Alert",
"template_file": "rugpull_template.png",
"size": (1200, 675),
"text_fields": {
"token_symbol": {
"position": (600, 200),
"font_size": 72,
"color": "#FF8C00",
"format": "{symbol}",
},
"amount_stolen": {
"position": (600, 350),
"font_size": 96,
"color": "#FF4500",
"format": "${amount:,.0f}",
},
"victims": {
"position": (400, 500),
"font_size": 48,
"color": "#FFFFFF",
"format": "{count} victims",
},
"rug_type": {
"position": (800, 500),
"font_size": 48,
"color": "#FFFFFF",
"format": "{type}",
},
},
"colors": {
"primary": "#FF8C00", # Orange
"accent": "#FF4500",
},
},
"whale_move": {
"name": "Whale Movement",
"template_file": "whale_template.png",
"size": (1200, 675),
"text_fields": {
"wallet_label": {
"position": (80, 150),
"font_size": 48,
"color": "#3B82F6",
"format": "{label}",
},
"action": {
"position": (600, 300),
"font_size": 72,
"color": "#3B82F6",
"format": "{action}",
},
"amount": {
"position": (600, 400),
"font_size": 96,
"color": "#60A5FA",
"format": "{amount:,.0f} {symbol}",
},
"usd_value": {
"position": (600, 500),
"font_size": 64,
"color": "#FFFFFF",
"format": "${value:,.0f}",
},
},
"colors": {
"primary": "#3B82F6", # Blue
"accent": "#60A5FA",
},
},
"smart_money": {
"name": "Smart Money Pick",
"template_file": "smart_money_template.png",
"size": (1200, 675),
"text_fields": {
"wallet_label": {
"position": (80, 150),
"font_size": 48,
"color": "#FFD700",
"format": "{label}",
},
"token_symbol": {
"position": (600, 280),
"font_size": 72,
"color": "#FFD700",
"format": "{symbol}",
},
"position_size": {
"position": (600, 400),
"font_size": 96,
"color": "#FFEC8B",
"format": "${amount:,.0f}",
},
"pnl": {
"position": (600, 500),
"font_size": 64,
"color": "#00FF88",
"format": "+{pnl:.1f}%",
},
},
"colors": {
"primary": "#FFD700", # Gold
"accent": "#FFEC8B",
},
},
}
# ── Social Media Accounts ────────────────────────────────────
SOCIAL_ACCOUNTS = {
"twitter": {
"handle": "@cryptorugmunch",
"name": "Rug Munch Intelligence",
"purpose": "Main posting account - alerts, wins, losses, KOL scorecards",
},
"telegram_bot": {
"handle": "@rugmunchbot",
"name": "Rug Munch Bot",
"purpose": "Interactive bot - respond to questions, commands, alerts",
"commands": [
"/start - Welcome + features",
"/trending - Top meme tokens",
"/whales - Recent whale moves",
"/kol [handle] - KOL scorecard",
"/wallet [address] - Wallet analysis",
"/alerts - Manage notifications",
"/premium - Upgrade info",
"/help - Command list",
],
},
}
# ── Card Generation Functions ────────────────────────────────
def format_wallet_short(address: str) -> str:
"""Format wallet address for display."""
if len(address) >= 14:
return f"{address[:8]}...{address[-6:]}"
return address
def get_or_create_template(card_type: str) -> Image.Image:
"""Get template image or create placeholder if doesn't exist."""
template_config = CARD_TEMPLATES.get(card_type)
if not template_config:
raise ValueError(f"Unknown card type: {card_type}")
template_path = os.path.join(TEMPLATE_DIR, template_config["template_file"])
if os.path.exists(template_path):
return Image.open(template_path)
else:
# Create placeholder template
logger.warning(f"Template not found: {template_path}, creating placeholder")
return create_placeholder_template(card_type, template_config)
def create_placeholder_template(card_type: str, config: dict) -> Image.Image:
"""Create a placeholder template if none exists."""
img = Image.new("RGB", config["size"], color=config["colors"].get("background", "#0A0A0F"))
draw = ImageDraw.Draw(img)
# Add title
title = config["name"]
bbox = draw.textbbox((0, 0), title, font_size=72)
title_width = bbox[2] - bbox[0]
draw.text(
((config["size"][0] - title_width) / 2, 50),
title,
fill=config["colors"]["primary"],
font_size=72,
)
# Add watermark
draw.text(
(config["size"][0] - 200, config["size"][1] - 50),
"@cryptorugmunch",
fill="#FFFFFF",
font_size=32,
)
# Save as template
template_path = os.path.join(TEMPLATE_DIR, config["template_file"])
img.save(template_path)
logger.info(f"Created placeholder template: {template_path}")
return img
def generate_card(card_type: str, data: dict) -> dict:
"""
Generate card by overlaying text on template.
Returns image path and metadata.
"""
try:
config = CARD_TEMPLATES.get(card_type)
if not config:
return {"error": f"Unknown card type: {card_type}"}
# Get or create template
template = get_or_create_template(card_type)
draw = ImageDraw.Draw(template)
# Try to load font (use default if not available)
try:
font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
base_font = ImageFont.truetype(font_path, 32)
except Exception:
base_font = ImageFont.load_default()
# Overlay text fields
for field_name, field_config in config["text_fields"].items():
if field_name in data:
value = data[field_name]
# Format value
if "format" in field_config:
try:
# Handle wallet formatting
if field_name == "wallet_address":
value = format_wallet_short(str(value))
else:
value = field_config["format"].format(**{field_name: value, **data})
except Exception:
pass
# Draw text
position = field_config["position"]
font_size = field_config.get("font_size", 32)
color = field_config.get("color", "#FFFFFF")
try:
font = ImageFont.truetype(font_path, font_size)
except Exception:
font = base_font
draw.text(position, str(value), fill=color, font=font)
# Add watermark
draw.text(
(config["size"][0] - 250, config["size"][1] - 60),
"@cryptorugmunch",
fill="#FFFFFF",
font_size=36,
)
# Generate unique filename
data_hash = hashlib.md5(str(sorted(data.items())).encode()).hexdigest()[:12]
timestamp = datetime.now(UTC).strftime("%Y%m%d_%H%M%S")
filename = f"{card_type}_{timestamp}_{data_hash}.png"
output_path = os.path.join(GENERATED_DIR, filename)
# Save image
template.save(output_path, "PNG")
return {
"status": "success",
"card_type": card_type,
"image_path": output_path,
"image_url": f"/assets/generated_cards/{filename}",
"filename": filename,
"data": data,
"generated_at": datetime.now(UTC).isoformat(),
"share_urls": {
"twitter": f"https://rugmunch.io/share/{card_type}/{data_hash}",
"telegram": f"https://rugmunch.io/share/{card_type}/{data_hash}",
},
}
except Exception as e:
logger.error(f"Card generation failed: {e}")
return {"status": "error", "error": str(e)}
# ── Convenience Functions ────────────────────────────────────
async def generate_win_card(
wallet_address: str,
token_symbol: str,
pnl_usd: float,
pnl_pct: float,
entry_price: float,
exit_price: float,
hold_time: str,
chain: str = "solana",
) -> dict:
"""Generate big win alert card."""
data = {
"wallet_address": wallet_address,
"token_symbol": token_symbol.upper(),
"pnl_usd": pnl_usd,
"pnl_pct": pnl_pct,
"entry_price": entry_price,
"exit_price": exit_price,
"hold_time": hold_time,
"chain": chain,
}
return generate_card("big_win", data)
async def generate_loss_card(
wallet_address: str,
token_symbol: str,
pnl_usd: float,
pnl_pct: float,
entry_price: float,
exit_price: float,
hold_time: str,
chain: str = "solana",
) -> dict:
"""Generate loss porn card."""
data = {
"wallet_address": wallet_address,
"token_symbol": token_symbol.upper(),
"pnl_usd": abs(pnl_usd),
"pnl_pct": abs(pnl_pct),
"entry_price": entry_price,
"exit_price": exit_price,
"hold_time": hold_time,
"chain": chain,
}
return generate_card("big_loss", data)
async def generate_kol_scorecard(
kol_handle: str,
win_rate: float,
total_calls: int,
avg_pnl: float,
followers: int,
tier: str = "A",
) -> dict:
"""Generate KOL scorecard."""
data = {
"kol_handle": kol_handle,
"win_rate": win_rate,
"total_calls": total_calls,
"avg_pnl": avg_pnl,
"followers": followers,
"tier": tier,
}
return generate_card("kol_scorecard", data)
async def generate_rug_call_card(token_symbol: str, amount_stolen: float, victims: int, rug_type: str) -> dict:
"""Generate rugpull alert card."""
data = {
"token_symbol": token_symbol.upper(),
"amount_stolen": amount_stolen,
"victims": victims,
"rug_type": rug_type,
}
return generate_card("rug_call", data)
async def generate_whale_alert_card(
wallet_label: str, action: str, amount: float, symbol: str, usd_value: float
) -> dict:
"""Generate whale movement card."""
data = {
"wallet_label": wallet_label,
"action": action,
"amount": amount,
"symbol": symbol.upper(),
"usd_value": usd_value,
}
return generate_card("whale_move", data)
async def generate_smart_money_card(wallet_label: str, token_symbol: str, position_size: float, pnl: float) -> dict:
"""Generate smart money pick card."""
data = {
"wallet_label": wallet_label,
"token_symbol": token_symbol.upper(),
"position_size": position_size,
"pnl": pnl,
}
return generate_card("smart_money", data)
# ── Template Management ──────────────────────────────────────
def list_templates() -> list[dict]:
"""List available card templates."""
return [
{
"id": key,
"name": config["name"],
"size": config["size"],
"file": config["template_file"],
"exists": os.path.exists(os.path.join(TEMPLATE_DIR, config["template_file"])),
}
for key, config in CARD_TEMPLATES.items()
]
def create_all_templates():
"""Create all placeholder templates."""
for card_type, _config in CARD_TEMPLATES.items():
try:
get_or_create_template(card_type)
except Exception as e:
logger.error(f"Failed to create template for {card_type}: {e}")