Skip to content

Commit 6843802

Browse files
authored
Merge pull request #220 from Exabyte-io/feature/SOF-7438-1
Feature/SOF-7438-1
2 parents d0083d2 + 95d0e17 commit 6843802

File tree

10 files changed

+927
-0
lines changed

10 files changed

+927
-0
lines changed

other/generate_gifs/README.md

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# GIFs generation with Materials visualization
2+
3+
## 1. Setup materials
4+
5+
1.1. Generate materials in Mat3ra JSON format with [Materials Designer](https://mat3ra-materials-designer.netlify.app/) or use a script.
6+
7+
1.2. Place materials JSON files in the `structures` directory.
8+
9+
1.3. Name them with a short name that will appear at bottom left of GIF.
10+
11+
## 2. Start wave.js
12+
13+
2.1. Run wave.js (from GitHub: https://github.com/Exabyte-io/wave.js) locally (default port 3002 -- used in notebooks).
14+
15+
Or keep URL of the web deployment in the notebook.
16+
17+
## 3. Record material GIFs
18+
19+
3.1. Run `record_gifs_with_wave.ipynb` to generate GIFs.
20+
21+
This notebook will generate GIFs for all JSON materials in the `structures` directory and save them on the top level of this repo (because we can't control the downloads directory for IDE).
22+
23+
3.2. Wait until the GIFs are downloaded. Move selected ones to the `input` folder or allow the next notebook to move them automatically.
24+
25+
## 4. Add overlays and generate final GIFs
26+
27+
4.1. Store any media files (e.g. images, videos) you want to overlay on the GIFs in the `assets` directory.
28+
29+
4.2. Run `gif_processing.ipynb` to add overlays and generate final GIFs.
30+
31+
This notebook will move the GIFs from the top level to the `output` directory, removing any duplications (judging by the file name), and add overlays with the material names.

other/generate_gifs/assets/logo.png

9.96 KB
Loading
+326
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
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+
}

other/generate_gifs/input/.gitkeep

Whitespace-only changes.

other/generate_gifs/output/.gitkeep

Whitespace-only changes.

0 commit comments

Comments
 (0)