import spaces import numpy as np import gradio as gr from PIL import Image, ImageEnhance, ImageFilter, ImageDraw, ImageFont def get_font(size: int): candidates = [ "DejaVuSans-Bold.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", "Arial.ttf", ] for path in candidates: try: return ImageFont.truetype(path, size) except Exception: continue try: return ImageFont.load_default(size=size) except Exception: return ImageFont.load_default() def crop_to_ratio(img: Image.Image, ratio_key: str, max_size: int = 1024) -> Image.Image: ratios = { "1:1": (1, 1), "4:3": (4, 3), "3:4": (3, 4), "16:9": (16, 9), "9:16": (9, 16), } tw, th = ratios.get(ratio_key, (1, 1)) target_ratio = tw / th img = img.convert("RGB") w, h = img.size current_ratio = w / h if current_ratio > target_ratio: new_w = int(h * target_ratio) left = (w - new_w) // 2 img = img.crop((left, 0, left + new_w, h)) else: new_h = int(w / target_ratio) top = (h - new_h) // 2 img = img.crop((0, top, w, top + new_h)) w, h = img.size scale = max_size / max(w, h) if scale < 1: img = img.resize((int(w * scale), int(h * scale)), Image.LANCZOS) return img def add_grain(arr: np.ndarray, amount: float = 15) -> np.ndarray: noise = np.random.normal(0, amount, arr.shape) out = arr.astype(np.float32) + noise return np.clip(out, 0, 255).astype(np.uint8) def color_grade(arr: np.ndarray, shadow_tint, highlight_tint, shadow_strength=0.3, highlight_strength=0.3) -> np.ndarray: arr = arr.astype(np.float32) luminance = arr.mean(axis=2, keepdims=True) / 255.0 shadow_mask = (1 - luminance) * shadow_strength highlight_mask = luminance * highlight_strength for i in range(3): arr[:, :, i] += shadow_tint[i] * shadow_mask[:, :, 0] arr[:, :, i] += highlight_tint[i] * highlight_mask[:, :, 0] return np.clip(arr, 0, 255).astype(np.uint8) def channel_shift(arr: np.ndarray, shift_x: int = 3) -> np.ndarray: r, g, b = arr[:, :, 0], arr[:, :, 1], arr[:, :, 2] return np.dstack([np.roll(r, shift_x, axis=1), g, np.roll(b, -shift_x, axis=1)]) def add_scanlines(arr: np.ndarray, gap: int = 4, darken: int = 25) -> np.ndarray: arr = arr.copy() arr[::gap, :, :] = np.clip( arr[::gap, :, :].astype(int) - darken, 0, 255 ).astype(np.uint8) return arr def apply_vignette(img: Image.Image, strength: float = 0.5) -> Image.Image: w, h = img.size y, x = np.ogrid[:h, :w] cx, cy = w / 2, h / 2 max_dist = np.sqrt(cx**2 + cy**2) dist = np.sqrt((x - cx)**2 + (y - cy)**2) / max_dist mask = np.clip(1 - strength * dist**2, 0, 1)[:, :, None] arr = np.array(img).astype(np.float32) * mask return Image.fromarray(np.clip(arr, 0, 255).astype(np.uint8)) def add_bloom(img: Image.Image, threshold=200, blur_radius=10, intensity=0.5) -> Image.Image: arr = np.array(img).astype(np.float32) luminance = arr.mean(axis=2) mask = np.clip((luminance - threshold) / max(255 - threshold, 1), 0, 1) bright = (arr * mask[:, :, None]).astype(np.uint8) bright_img = Image.fromarray(bright).filter(ImageFilter.GaussianBlur(blur_radius)) bright_arr = np.array(bright_img).astype(np.float32) out = 255 - (255 - arr) * (255 - bright_arr * intensity) / 255 return Image.fromarray(np.clip(out, 0, 255).astype(np.uint8)) def add_light_leak(img: Image.Image, color=(255, 120, 60), strength=0.3) -> Image.Image: w, h = img.size xv, yv = np.meshgrid(np.linspace(0, 1, w), np.linspace(0, 1, h)) gradient = np.clip(1 - np.abs((xv + yv) / 2 - 0.75) * 3, 0, 1) arr = np.array(img).astype(np.float32) for i in range(3): arr[:, :, i] += gradient * color[i] * strength return Image.fromarray(np.clip(arr, 0, 255).astype(np.uint8)) def draw_caption(img: Image.Image, text: str, prefix: str = "", text_color=(255, 255, 255, 255), bg_color=(0, 0, 0, 130), position: str = "bottom") -> Image.Image: img = img.convert("RGBA") overlay = Image.new("RGBA", img.size, (0, 0, 0, 0)) draw = ImageDraw.Draw(overlay) w, h = img.size full_text = f"{prefix} {text}".strip() if prefix else text font_size = max(16, w // 30) font = get_font(font_size) bbox = draw.textbbox((0, 0), full_text, font=font) text_w = bbox[2] - bbox[0] text_h = bbox[3] - bbox[1] pad = 12 if position == "bottom": box = (0, h - text_h - pad * 2, w, h) else: box = (0, 0, w, text_h + pad * 2) draw.rectangle(box, fill=bg_color) draw.text(((w - text_w) // 2, box[1] + pad), full_text, font=font, fill=text_color) return Image.alpha_composite(img, overlay).convert("RGB") def add_polaroid_border(img: Image.Image, border=20, bottom_extra=70) -> Image.Image: w, h = img.size canvas = Image.new("RGB", (w + border * 2, h + border * 2 + bottom_extra), (250, 250, 245)) canvas.paste(img, (border, border)) return canvas def apply_1980s(img: Image.Image, text: str) -> Image.Image: out = ImageEnhance.Color(img).enhance(1.4) out = ImageEnhance.Contrast(out).enhance(1.15) out = Image.fromarray(color_grade(np.array(out), shadow_tint=(20, -10, 60), highlight_tint=(255, 60, 180), shadow_strength=0.5, highlight_strength=0.35)) out = add_bloom(out, threshold=180, blur_radius=12, intensity=0.6) out = add_light_leak(out, color=(255, 80, 180), strength=0.3) out = Image.fromarray(add_grain(np.array(out), amount=10)) out = draw_caption(out, text, prefix="◆ 1980s ◆", text_color=(255, 0, 200, 255)) return out def apply_1990s(img: Image.Image, text: str) -> Image.Image: out = ImageEnhance.Color(img).enhance(0.85) out = ImageEnhance.Contrast(out).enhance(0.9) out = ImageEnhance.Brightness(out).enhance(1.05) arr = color_grade(np.array(out), shadow_tint=(10, 5, -10), highlight_tint=(255, 220, 150), shadow_strength=0.3, highlight_strength=0.4) arr = channel_shift(arr, shift_x=3) arr = add_scanlines(arr, gap=4, darken=25) arr = add_grain(arr, amount=18) out = Image.fromarray(arr).filter(ImageFilter.GaussianBlur(0.6)) out = draw_caption(out, text, prefix="● REC", text_color=(255, 60, 60, 255), position="top") return out def apply_2000s(img: Image.Image, text: str) -> Image.Image: out = ImageEnhance.Brightness(img).enhance(1.25) out = ImageEnhance.Contrast(out).enhance(1.1) out = ImageEnhance.Sharpness(out).enhance(1.5) out = Image.fromarray(color_grade(np.array(out), shadow_tint=(-10, 0, 40), highlight_tint=(30, 30, 60), shadow_strength=0.3, highlight_strength=0.2)) out = apply_vignette(out, strength=0.55) out = Image.fromarray(add_grain(np.array(out), amount=14)) out = draw_caption(out, text, prefix="📷", text_color=(255, 140, 0, 255)) return out def apply_2010s(img: Image.Image, text: str) -> Image.Image: out = ImageEnhance.Color(img).enhance(1.1) out = ImageEnhance.Contrast(out).enhance(0.92) arr = np.clip(np.array(out).astype(np.float32) * 0.9 + 25, 0, 255).astype(np.uint8) arr = color_grade(arr, shadow_tint=(30, 10, -10), highlight_tint=(255, 200, 120), shadow_strength=0.35, highlight_strength=0.3) out = apply_vignette(Image.fromarray(arr), strength=0.3) out = Image.fromarray(add_grain(np.array(out), amount=6)) out = add_polaroid_border(out) out = draw_caption(out, text, prefix="📸", text_color=(60, 60, 60, 255), bg_color=(0, 0, 0, 0), position="bottom") return out ERA_FUNCS = { "1980-е 🕺": apply_1980s, "1990-е 📼": apply_1990s, "2000-е 💿": apply_2000s, "2010-е 📱": apply_2010s, } @spaces.GPU(duration=10) def generate_all_eras(selfie, scene_text, aspect_ratio): if selfie is None: raise gr.Error("Пожалуйста, загрузите ваше селфи! 🤳") scene_text = (scene_text or "").strip() or "путешествие во времени" base = crop_to_ratio(selfie, aspect_ratio) results = [] for era_name, era_func in ERA_FUNCS.items(): results.append((era_func(base.copy(), scene_text), era_name)) return results CUSTOM_CSS = """ #title { text-align: center; font-family: 'Georgia', serif; } .gradio-container { background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%); } #generate-btn { background: linear-gradient(90deg, #f7971e, #ffd200) !important; font-weight: bold; font-size: 18px !important; } """ with gr.Blocks(css=CUSTOM_CSS, theme=gr.themes.Soft(primary_hue="orange")) as demo: gr.Markdown( "
" "Загрузи селфи — получи себя в стиле 1980-х, 1990-х, 2000-х и 2010-х! ✨" "
" ) with gr.Row(): with gr.Column(scale=1): selfie_input = gr.Image( type="pil", label="📸 Загрузите ваше селфи", ) scene_input = gr.Textbox( label="✍️ Опишите сцену / момент", placeholder="Например: сижу на капоте машины на закате", lines=2, ) aspect_input = gr.Radio( choices=["1:1", "4:3", "3:4", "16:9", "9:16"], value="1:1", label="📐 Формат изображения", ) generate_btn = gr.Button( "Отправиться сквозь время 🚀", elem_id="generate-btn", variant="primary", ) with gr.Column(scale=1): gallery_output = gr.Gallery( label="🖼️ Результаты путешествия во времени", columns=2, rows=2, height="auto", object_fit="contain", preview=True, ) generate_btn.click( fn=generate_all_eras, inputs=[selfie_input, scene_input, aspect_input], outputs=gallery_output, ) gr.Markdown( "" "CPU-фильтры · ZeroGPU · Hugging Face Spaces · Бесплатно для всех" "
" ) if __name__ == "__main__": demo.queue().launch()