550 lines
17 KiB
Python
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}")
|