Spaces:
Sleeping
Sleeping
Download app.py from AtesiT/BackTo80Photos: direct link, hf CLI and curl.
- Browser
- Download file 11.1 kB
-
https://huggingface.co/spaces/AtesiT/BackTo80Photos/resolve/main/app.py
- Command line
-
hf download hf://spaces/AtesiT/BackTo80Photos/app.py
-
curl -L -o app.py https://huggingface.co/spaces/AtesiT/BackTo80Photos/resolve/main/app.py
11.1 kB
| 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, | |
| } | |
| 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( | |
| "<h1 id='title'>⏳ Eras Time Machine AI</h1>" | |
| "<p style='text-align:center;'>" | |
| "Загрузи селфи — получи себя в стиле 1980-х, 1990-х, 2000-х и 2010-х! ✨" | |
| "</p>" | |
| ) | |
| 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( | |
| "<p style='text-align:center; opacity:0.6; font-size:12px;'>" | |
| "CPU-фильтры · ZeroGPU · Hugging Face Spaces · Бесплатно для всех" | |
| "</p>" | |
| ) | |
| if __name__ == "__main__": | |
| demo.queue().launch() |