|
| 1 | +{ |
| 2 | + "cells": [ |
| 3 | + { |
| 4 | + "metadata": {}, |
| 5 | + "cell_type": "markdown", |
| 6 | + "source": [ |
| 7 | + "# Batch GIF Processing Notebook\n", |
| 8 | + "\n", |
| 9 | + "This notebook provides functionality to:\n", |
| 10 | + "1. Process multiple GIFs from an input folder\n", |
| 11 | + "2. Resize all GIFs to specified square dimensions\n", |
| 12 | + "3. Add text overlay with the GIF filename and standard text\n", |
| 13 | + "4. Add logo overlay at selected positions\n", |
| 14 | + "5. Compress the resulting GIFs\n", |
| 15 | + "\n", |
| 16 | + "# Usage\n", |
| 17 | + "- Place your GIFs in the `input` directory, or keep them at the root of the `api-examples` folder if they were downloaded with `record_gifs_with_wave.ipynb`.\n", |
| 18 | + "- Run All Cells to process the GIFs and save the results in the `output` directory.\n", |
| 19 | + "- You can select available fonts for text overlays by running the `font_manager.list_fonts()` command in the 3.2. cell and setting `FONT_NAME` to one of the listed fonts.\n", |
| 20 | + "- You can customize the logo, and GIF quality settings in the 1. Input/Output Settings cell.\n", |
| 21 | + "- You can specify the text overlay content in the 2.3. cell.\n" |
| 22 | + ], |
| 23 | + "id": "28f8a295c4c03fa5" |
| 24 | + }, |
| 25 | + { |
| 26 | + "metadata": {}, |
| 27 | + "cell_type": "markdown", |
| 28 | + "source": "## 1. Input/Output Settings", |
| 29 | + "id": "c4119fd75a2937e5" |
| 30 | + }, |
| 31 | + { |
| 32 | + "metadata": {}, |
| 33 | + "cell_type": "code", |
| 34 | + "source": [ |
| 35 | + "# Directory settings\n", |
| 36 | + "INPUT_DIR = \"input\"\n", |
| 37 | + "OUTPUT_DIR = \"output\"\n", |
| 38 | + "ASSETS_DIR = \"assets\"\n", |
| 39 | + "\n", |
| 40 | + "# GIF Settings\n", |
| 41 | + "GIF_SIZE = 600 # Size for square output\n", |
| 42 | + "QUALITY = 30 # GIF quality (1-100)\n", |
| 43 | + "\n", |
| 44 | + "# Logo Settings\n", |
| 45 | + "LOGO_FILE = \"logo.png\" # Place your logo in files/assets/\n", |
| 46 | + "LOGO_POSITION = (15, 10)\n", |
| 47 | + "\n", |
| 48 | + "# Text Overlay Settings\n", |
| 49 | + "FONT_SIZE = 16\n", |
| 50 | + "TEXT_COLOR = (255, 255, 255)\n", |
| 51 | + "STROKE_COLOR = (0, 0, 0)\n", |
| 52 | + "FONT_NAME = \"lucida-grande\" # Use `font_manager.list_fonts()` below in 3.2. to see available fonts\n", |
| 53 | + "\n", |
| 54 | + "TEXT_2 = \"Available in our materials bank\"" |
| 55 | + ], |
| 56 | + "id": "ff83c26628e7ec46", |
| 57 | + "outputs": [], |
| 58 | + "execution_count": null |
| 59 | + }, |
| 60 | + { |
| 61 | + "metadata": {}, |
| 62 | + "cell_type": "markdown", |
| 63 | + "source": "## 2. Utility Functions", |
| 64 | + "id": "96b41f843ed53919" |
| 65 | + }, |
| 66 | + { |
| 67 | + "metadata": {}, |
| 68 | + "cell_type": "code", |
| 69 | + "source": [ |
| 70 | + "import os\n", |
| 71 | + "import shutil\n", |
| 72 | + "import re\n", |
| 73 | + "\n", |
| 74 | + "\n", |
| 75 | + "def copy_and_clean_gifs(source_folder, target_folder=\"input\"):\n", |
| 76 | + " \"\"\"\n", |
| 77 | + " Copy GIF files from source folder to target folder,\n", |
| 78 | + " removing numbered duplicates (e.g., removes 'xxx1.gif' if 'xxx.gif' exists)\n", |
| 79 | + "\n", |
| 80 | + " Args:\n", |
| 81 | + " source_folder (str): Path to source folder containing GIFs\n", |
| 82 | + " target_folder (str): Path to target folder (defaults to 'input')\n", |
| 83 | + " \"\"\"\n", |
| 84 | + "\n", |
| 85 | + " # Ensure target folder exists\n", |
| 86 | + " os.makedirs(target_folder, exist_ok=True)\n", |
| 87 | + "\n", |
| 88 | + " # Get all GIF files from source\n", |
| 89 | + " gif_files = [f for f in os.listdir(source_folder) if f.lower().endswith('.gif')]\n", |
| 90 | + "\n", |
| 91 | + " # Dictionary to store base names and their variations\n", |
| 92 | + " file_groups = {}\n", |
| 93 | + "\n", |
| 94 | + " # Group files by their base names\n", |
| 95 | + " for file in gif_files:\n", |
| 96 | + " # Remove .gif extension\n", |
| 97 | + " base = file[:-4]\n", |
| 98 | + " # Check if the filename ends with a number\n", |
| 99 | + " match = re.match(r'(.*?)\\d+$', base)\n", |
| 100 | + "\n", |
| 101 | + " if match:\n", |
| 102 | + " # If it has a number, use the part before the number as key\n", |
| 103 | + " key = match.group(1).rstrip()\n", |
| 104 | + " else:\n", |
| 105 | + " # If no number, use the whole base as key\n", |
| 106 | + " key = base\n", |
| 107 | + "\n", |
| 108 | + " if key not in file_groups:\n", |
| 109 | + " file_groups[key] = []\n", |
| 110 | + " file_groups[key].append(file)\n", |
| 111 | + "\n", |
| 112 | + " # Copy files, skipping numbered versions if base version exists\n", |
| 113 | + " copied_count = 0\n", |
| 114 | + " skipped_count = 0\n", |
| 115 | + "\n", |
| 116 | + " for base_name, variations in file_groups.items():\n", |
| 117 | + " # Sort variations to ensure base version (without number) comes first if it exists\n", |
| 118 | + " variations.sort(key=lambda x: (len(x), x))\n", |
| 119 | + "\n", |
| 120 | + " # Copy the first variation (usually the base version)\n", |
| 121 | + " source_path = os.path.join(source_folder, variations[0])\n", |
| 122 | + " target_path = os.path.join(target_folder, variations[0])\n", |
| 123 | + " shutil.copy2(source_path, target_path)\n", |
| 124 | + " copied_count += 1\n", |
| 125 | + "\n", |
| 126 | + " # Count skipped variations\n", |
| 127 | + " skipped_count += len(variations) - 1\n", |
| 128 | + "\n", |
| 129 | + " print(f\"Copied {copied_count} files\")\n", |
| 130 | + " if skipped_count > 0:\n", |
| 131 | + " print(f\"Skipped {skipped_count} numbered variations\")\n", |
| 132 | + "\n", |
| 133 | + "# Example usage:\n", |
| 134 | + "# copy_and_clean_gifs(\"/path/to/source/folder\")\n", |
| 135 | + "# Or with custom target: copy_and_clean_gifs(\"/path/to/source\", \"custom_input\")" |
| 136 | + ], |
| 137 | + "id": "beafdf7911dee370", |
| 138 | + "outputs": [], |
| 139 | + "execution_count": null |
| 140 | + }, |
| 141 | + { |
| 142 | + "metadata": {}, |
| 143 | + "cell_type": "markdown", |
| 144 | + "source": "## 2.1. Copy and Clean GIFs", |
| 145 | + "id": "839f510115d46896" |
| 146 | + }, |
| 147 | + { |
| 148 | + "metadata": {}, |
| 149 | + "cell_type": "code", |
| 150 | + "source": [ |
| 151 | + "import json\n", |
| 152 | + "\n", |
| 153 | + "# GIFs generated by `record_gifs_with_wave.ipynb` are downloaded to the root of `api-examples`\n", |
| 154 | + "# They need to be copied to input directory and pruned from duplications due to possible bugs.\n", |
| 155 | + "current_dir = os.getcwd()\n", |
| 156 | + "print(current_dir)\n", |
| 157 | + "parent_dir = os.path.abspath(os.path.join(os.getcwd(), os.pardir, os.pardir))\n", |
| 158 | + "print(parent_dir)\n", |
| 159 | + "copy_and_clean_gifs(parent_dir, INPUT_DIR)\n", |
| 160 | + "\n", |
| 161 | + "# Some symbols needed to be encoded and decoded\n", |
| 162 | + "# Load the symbols from the JSON file\n", |
| 163 | + "with open('symbols_map.json', 'r') as file:\n", |
| 164 | + " symbols = json.load(file)\n", |
| 165 | + "\n", |
| 166 | + "# Extract the \"OVER\" symbol\n", |
| 167 | + "SLASH_SYMBOL = symbols[\"/\"]\n", |
| 168 | + "\n", |
| 169 | + "# Create directories if they don't exist\n", |
| 170 | + "for directory in [INPUT_DIR, OUTPUT_DIR, ASSETS_DIR]:\n", |
| 171 | + " os.makedirs(directory, exist_ok=True)" |
| 172 | + ], |
| 173 | + "id": "4ac1cc7071a612a2", |
| 174 | + "outputs": [], |
| 175 | + "execution_count": null |
| 176 | + }, |
| 177 | + { |
| 178 | + "metadata": {}, |
| 179 | + "cell_type": "markdown", |
| 180 | + "source": "## 2.2. List fonts", |
| 181 | + "id": "57108e5b06ef8cce" |
| 182 | + }, |
| 183 | + { |
| 184 | + "metadata": {}, |
| 185 | + "cell_type": "code", |
| 186 | + "source": [ |
| 187 | + "from other.media_generation.utils.font_manager import FontManager\n", |
| 188 | + "\n", |
| 189 | + "# Initialize font manager and list available fonts\n", |
| 190 | + "font_manager = FontManager()\n", |
| 191 | + "print(\"Available fonts:\")\n", |
| 192 | + "# print(font_manager.list_fonts())\n" |
| 193 | + ], |
| 194 | + "id": "77d8a123f4796db9", |
| 195 | + "outputs": [], |
| 196 | + "execution_count": null |
| 197 | + }, |
| 198 | + { |
| 199 | + "metadata": {}, |
| 200 | + "cell_type": "markdown", |
| 201 | + "source": "## 2.3. Define Text Overlays", |
| 202 | + "id": "6758710ce94551ef" |
| 203 | + }, |
| 204 | + { |
| 205 | + "metadata": {}, |
| 206 | + "cell_type": "code", |
| 207 | + "source": [ |
| 208 | + "def create_text_overlays(filename):\n", |
| 209 | + " \"\"\"Create text overlays using the GIF filename as text_1\"\"\"\n", |
| 210 | + " # Clean up filename by removing extension and replacing underscores/hyphens with spaces\n", |
| 211 | + " clean_name = os.path.splitext(filename)[0].replace(SLASH_SYMBOL, \"/\")\n", |
| 212 | + "\n", |
| 213 | + " return [\n", |
| 214 | + " {\n", |
| 215 | + " \"text\": clean_name,\n", |
| 216 | + " \"position\": (10, GIF_SIZE - 10 - FONT_SIZE), # Bottom left\n", |
| 217 | + " \"font\": FONT_NAME,\n", |
| 218 | + " \"color\": TEXT_COLOR,\n", |
| 219 | + " \"stroke_width\": 2,\n", |
| 220 | + " \"stroke_fill\": STROKE_COLOR\n", |
| 221 | + " },\n", |
| 222 | + " {\n", |
| 223 | + " \"text\": TEXT_2,\n", |
| 224 | + " \"position\": (GIF_SIZE // 2 + 50, GIF_SIZE - 10 - FONT_SIZE), # Bottom right\n", |
| 225 | + " \"font\": FONT_NAME,\n", |
| 226 | + " \"color\": TEXT_COLOR,\n", |
| 227 | + " \"stroke_width\": 2,\n", |
| 228 | + " \"stroke_fill\": STROKE_COLOR\n", |
| 229 | + " }\n", |
| 230 | + " ]\n" |
| 231 | + ], |
| 232 | + "id": "4340871c4e59cc92", |
| 233 | + "outputs": [], |
| 234 | + "execution_count": null |
| 235 | + }, |
| 236 | + { |
| 237 | + "metadata": {}, |
| 238 | + "cell_type": "markdown", |
| 239 | + "source": "## 3. Process All GIFs", |
| 240 | + "id": "348711dac2da3b96" |
| 241 | + }, |
| 242 | + { |
| 243 | + "metadata": {}, |
| 244 | + "cell_type": "code", |
| 245 | + "source": [ |
| 246 | + "from other.media_generation.utils.gif_processor import GIFProcessor\n", |
| 247 | + "\n", |
| 248 | + "\n", |
| 249 | + "def process_all_gifs():\n", |
| 250 | + " \"\"\"Process all GIFs in the input directory\"\"\"\n", |
| 251 | + " # Get logo path\n", |
| 252 | + " logo_path = os.path.join(ASSETS_DIR, LOGO_FILE)\n", |
| 253 | + " if not os.path.exists(logo_path):\n", |
| 254 | + " print(f\"Warning: Logo file not found at {logo_path}\")\n", |
| 255 | + " return\n", |
| 256 | + "\n", |
| 257 | + " # Get all GIF files from input directory\n", |
| 258 | + " gif_files = [f for f in os.listdir(INPUT_DIR) if f.lower().endswith('.gif')]\n", |
| 259 | + "\n", |
| 260 | + " if not gif_files:\n", |
| 261 | + " print(\"No GIF files found in input directory\")\n", |
| 262 | + " return\n", |
| 263 | + "\n", |
| 264 | + " print(f\"Found {len(gif_files)} GIF files to process\")\n", |
| 265 | + "\n", |
| 266 | + " # Process each GIF\n", |
| 267 | + " for gif_file in gif_files:\n", |
| 268 | + " try:\n", |
| 269 | + " print(f\"\\nProcessing: {gif_file}\")\n", |
| 270 | + "\n", |
| 271 | + " input_path = os.path.join(INPUT_DIR, gif_file)\n", |
| 272 | + " output_path = os.path.join(OUTPUT_DIR, f\"{gif_file}\")\n", |
| 273 | + "\n", |
| 274 | + " # Create GIF processor\n", |
| 275 | + " gif_processor = GIFProcessor(input_path)\n", |
| 276 | + "\n", |
| 277 | + " # Make square and resize\n", |
| 278 | + " gif_processor.make_square(size=GIF_SIZE)\n", |
| 279 | + "\n", |
| 280 | + " # Add text overlays\n", |
| 281 | + " text_overlays = create_text_overlays(gif_file)\n", |
| 282 | + " for overlay in text_overlays:\n", |
| 283 | + " gif_processor.add_text(\n", |
| 284 | + " text=overlay[\"text\"],\n", |
| 285 | + " position=overlay[\"position\"],\n", |
| 286 | + " font_path=overlay[\"font\"],\n", |
| 287 | + " font_size=FONT_SIZE,\n", |
| 288 | + " color=overlay[\"color\"],\n", |
| 289 | + " stroke_width=overlay[\"stroke_width\"],\n", |
| 290 | + " stroke_fill=overlay[\"stroke_fill\"]\n", |
| 291 | + " )\n", |
| 292 | + "\n", |
| 293 | + " # Add logo\n", |
| 294 | + " gif_processor.add_image_overlay(logo_path, position=LOGO_POSITION)\n", |
| 295 | + "\n", |
| 296 | + " # Optimize and save\n", |
| 297 | + " gif_processor.optimize(quality=QUALITY)\n", |
| 298 | + " gif_processor.save(output_path, optimize=False, quality=QUALITY)\n", |
| 299 | + "\n", |
| 300 | + " filename = text_overlays[0][\"text\"]\n", |
| 301 | + " print(f\"Filename: {filename}\")\n", |
| 302 | + " print(f\"Successfully processed: {gif_file}\")\n", |
| 303 | + "\n", |
| 304 | + " except Exception as e:\n", |
| 305 | + " print(f\"Error processing {gif_file}: {str(e)}\")\n", |
| 306 | + " continue\n", |
| 307 | + "\n", |
| 308 | + "\n", |
| 309 | + "# Run the batch processing\n", |
| 310 | + "process_all_gifs()" |
| 311 | + ], |
| 312 | + "id": "228b136f3a8379d4", |
| 313 | + "outputs": [], |
| 314 | + "execution_count": null |
| 315 | + } |
| 316 | + ], |
| 317 | + "metadata": { |
| 318 | + "kernelspec": { |
| 319 | + "name": "python3", |
| 320 | + "language": "python", |
| 321 | + "display_name": "Python 3 (ipykernel)" |
| 322 | + } |
| 323 | + }, |
| 324 | + "nbformat": 5, |
| 325 | + "nbformat_minor": 9 |
| 326 | +} |
0 commit comments