120 lines
3.6 KiB
Python
120 lines
3.6 KiB
Python
import numpy as np
|
|
from PIL import Image, ImageDraw, ImageFont
|
|
import os
|
|
|
|
# ---------- Configuration (smaller size) ----------
|
|
W, H = 480, 210 # lower resolution
|
|
TOTAL_DURATION = 3.0
|
|
FPS = 15 # lower fps
|
|
TEXT = "dLLM"
|
|
# TEXT_COLOR = (235, 235, 235)
|
|
TEXT_COLOR = (0, 0, 0)
|
|
OUTPUT = "logo.gif"
|
|
LAST_FRAME_PNG = "logo.png"
|
|
|
|
DIFFUSION_PORTION = 0.3 # fewer diffusion frames
|
|
SEED = 8
|
|
|
|
|
|
# ---------- Auto font size ----------
|
|
def load_font_auto_size(text, w, h, target_width_ratio=0.95, target_height_ratio=0.95):
|
|
lo, hi = 10, 2000
|
|
best_font, best_size = None, lo
|
|
while lo <= hi:
|
|
mid = (lo + hi) // 2
|
|
try:
|
|
font = ImageFont.truetype(
|
|
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", size=mid
|
|
)
|
|
except:
|
|
font = ImageFont.load_default()
|
|
dummy = Image.new("L", (w, h), 0)
|
|
d = ImageDraw.Draw(dummy)
|
|
bbox = d.textbbox((0, 0), text, font=font)
|
|
tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1]
|
|
|
|
width_ok = tw <= w * target_width_ratio
|
|
height_ok = th <= h * target_height_ratio
|
|
|
|
if width_ok and height_ok:
|
|
best_font, best_size = font, mid
|
|
lo = mid + 1
|
|
else:
|
|
hi = mid - 1
|
|
|
|
return best_font if best_font is not None else font
|
|
|
|
|
|
# ---------- Text rendering ----------
|
|
def render_text_mask(w, h, text, font):
|
|
img = Image.new("L", (w, h), 0)
|
|
d = ImageDraw.Draw(img)
|
|
bbox = d.textbbox((0, 0), text, font=font)
|
|
tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1]
|
|
x = (w - tw) // 2 - bbox[0]
|
|
y = (h - th) // 2 - bbox[1]
|
|
d.text((x, y), text, font=font, fill=255)
|
|
return np.asarray(img, np.float32) / 255.0
|
|
|
|
|
|
# ---------- Initialization ----------
|
|
font = load_font_auto_size(TEXT, W, H)
|
|
mask = render_text_mask(W, H, TEXT, font)
|
|
|
|
num_frames = int(TOTAL_DURATION * FPS)
|
|
diffusion_frames = max(1, int(num_frames * DIFFUSION_PORTION))
|
|
hold_ms = int((TOTAL_DURATION - diffusion_frames / FPS) * 1000)
|
|
|
|
rng = np.random.default_rng(SEED)
|
|
frames = []
|
|
|
|
# ---------- Diffusion stage ----------
|
|
for i in range(diffusion_frames):
|
|
t = i / max(1, diffusion_frames - 1)
|
|
progress = t**0.9
|
|
noise_sigma = (1.0 - progress) ** 2.2
|
|
|
|
noise = rng.standard_normal((H, W, 1)).astype(np.float32)
|
|
noise_img = 1.0 - noise_sigma * 0.5 * np.abs(noise)
|
|
np.clip(noise_img, 0.0, 1.0, out=noise_img)
|
|
|
|
alpha = progress**2.0
|
|
alpha_map = (mask * alpha).astype(np.float32)[..., None]
|
|
|
|
text_rgb = np.zeros((H, W, 3), dtype=np.float32)
|
|
for c in range(3):
|
|
text_rgb[..., c] = (mask > 0).astype(np.float32) * (TEXT_COLOR[c] / 255.0)
|
|
|
|
frame = (1.0 - alpha_map) * noise_img + alpha_map * text_rgb
|
|
frame = (np.clip(frame, 0.0, 1.0) * 255).astype(np.uint8)
|
|
frames.append(Image.fromarray(frame, mode="RGB"))
|
|
|
|
# ---------- Last frame ----------
|
|
final_frame = frames[-1]
|
|
|
|
# ---------- Save last frame as PNG ----------
|
|
final_frame.save(LAST_FRAME_PNG)
|
|
print(f"🖼️ Last frame saved as: {LAST_FRAME_PNG}")
|
|
|
|
# ---------- Quantization (reduce size) ----------
|
|
pal_frames = [f.convert("P", palette=Image.ADAPTIVE, colors=64) for f in frames]
|
|
pal_final = final_frame.convert("P", palette=Image.ADAPTIVE, colors=64)
|
|
|
|
# ---------- Save GIF ----------
|
|
normal_ms = int(1000 / FPS)
|
|
durations = [normal_ms] * len(pal_frames) + [hold_ms]
|
|
|
|
pal_frames[0].save(
|
|
OUTPUT,
|
|
save_all=True,
|
|
append_images=pal_frames[1:] + [pal_final],
|
|
duration=durations,
|
|
loop=0,
|
|
optimize=True,
|
|
)
|
|
|
|
print(f"✅ GIF saved: {OUTPUT}")
|
|
print(
|
|
f"Frames (diffusion only): {len(pal_frames)} at {FPS} FPS, final hold {hold_ms} ms, resolution {W}x{H}"
|
|
)
|