{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# GIF Animation Generator using LangGraph and DALL-E\n", "\n", "## Overview\n", "This project demonstrates the creation of a GIF animation generator that leverages the power of large language models (LLMs) and image generation AI. By combining LangGraph for workflow management, GPT-4 for text generation, and DALL-E for image creation, we've developed a system that can produce custom GIF animations based on user prompts.\n", "\n", "## Motivation\n", "In the era of AI-driven content creation, there's a growing demand for tools that can automate and simplify complex creative processes. This project aims to showcase how various AI technologies can be integrated to create a seamless workflow that transforms a simple text prompt into a dynamic visual story. By doing so, we're exploring the potential of AI in creative fields and providing a tool that could be valuable for content creators, educators, and enthusiasts alike.\n", "\n", "## Key Components\n", "1. **LangGraph**: Orchestrates the overall workflow, managing the flow of data between different stages of the process.\n", "2. **GPT-4 (via LangChain)**: Generates detailed descriptions, plots, and image prompts based on the initial user query.\n", "3. **DALL-E 3**: Creates high-quality images based on the generated prompts.\n", "4. **Python Imaging Library (PIL)**: Assembles the individual images into a GIF animation.\n", "5. **Asynchronous Programming**: Utilizes `asyncio` and `aiohttp` for efficient parallel processing of image generation and retrieval.\n", "\n", "## Method\n", "The GIF generation process follows these high-level steps:\n", "\n", "1. **Character/Scene Description**: Based on the user's input query, the system generates a detailed description of the main character or scene.\n", "\n", "2. **Plot Generation**: Using the character description and initial query, a 5-step plot is created, outlining the progression of the animation.\n", "\n", "3. **Image Prompt Creation**: For each step of the plot, a specific image prompt is generated, ensuring consistency across the frames.\n", "\n", "4. **Image Generation**: DALL-E 3 is used to create images based on each prompt.\n", "\n", "5. **GIF Assembly**: The generated images are compiled into a GIF animation.\n", "\n", "Throughout this process, LangGraph manages the flow of information between steps, ensuring that the output of each stage is appropriately fed into the next. The use of asynchronous programming allows for efficient parallel processing, particularly during the image generation and retrieval phases.\n", "\n", "## Conclusion\n", "This GIF Animation Generator demonstrates the potential of combining different AI technologies to create a powerful, user-friendly tool for content creation. By automating the process from text prompt to visual animation, it opens up new possibilities for storytelling, education, and entertainment. \n", "\n", "The modular nature of the system, facilitated by LangGraph, allows for easy updates or replacements of individual components. This makes the project adaptable to future advancements in language models or image generation technologies.\n", "\n", "While the current implementation focuses on creating simple 5-frame GIFs, the concept could be extended to create longer animations, incorporate user feedback at intermediate stages, or even integrate with other media types. As AI continues to evolve, tools like this will play an increasingly important role in bridging the gap between human creativity and machine capabilities." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Setup and Imports\n", "\n", "Import necessary libraries and set up the environment." ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [], "source": [ "import os\n", "from typing import TypedDict, Annotated, Sequence, List\n", "from langgraph.graph import Graph, END\n", "from langchain_openai import ChatOpenAI\n", "from langchain_core.messages import HumanMessage, AIMessage\n", "from openai import OpenAI\n", "from PIL import Image\n", "import io\n", "from IPython.display import display, Image as IPImage\n", "\n", "from langchain_core.runnables.graph import MermaidDrawMethod\n", "\n", "import asyncio\n", "import aiohttp\n", "from dotenv import load_dotenv\n", "\n", "# Load environment variables\n", "load_dotenv()\n", "\n", "# Set OpenAI API key\n", "os.environ[\"OPENAI_API_KEY\"] = os.getenv('OPENAI_API_KEY')\n", "\n", "# Initialize OpenAI client\n", "client = OpenAI()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Define Data Structures\n", "\n", "Define the structure for the graph state using TypedDict." ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [], "source": [ "class GraphState(TypedDict):\n", " messages: Annotated[Sequence[HumanMessage | AIMessage], \"The messages in the conversation\"]\n", " query: Annotated[str, \"Input query describing the character and scene\"]\n", " plot: Annotated[str, \"Generated plot for the GIF\"]\n", " character_description: Annotated[str, \"Detailed description of the main character or object\"]\n", " image_prompts: Annotated[List[str], \"List of prompts for each frame\"]\n", " image_urls: Annotated[List[str], \"List of URLs for generated images\"]\n", " gif_data: Annotated[bytes, \"GIF data in bytes\"]\n", "\n", "# Initialize the language model\n", "llm = ChatOpenAI(model=\"gpt-4\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Define Graph Functions\n", "\n", "Define the functions that will be used in the LangGraph workflow." ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "async def get_image_data(session, url: str):\n", " \"\"\"Fetch image data from a given URL.\"\"\"\n", " async with session.get(url) as response:\n", " if response.status == 200:\n", " return await response.read()\n", " return None\n", "\n", "def generate_character_description(state: GraphState) -> GraphState:\n", " \"\"\"Generate a detailed description of the main character or scene.\"\"\"\n", " query = state[\"query\"]\n", " response = llm.invoke([HumanMessage(content=f\"Based on the query '{query}', create a detailed description of the main character, object, or scene. Include specific details about appearance, characteristics, and any unique features. This description will be used to maintain consistency across multiple images.\")])\n", " state[\"character_description\"] = response.content\n", " return state\n", "\n", "def generate_plot(state: GraphState) -> GraphState:\n", " \"\"\"Generate a 5-step plot for the GIF animation.\"\"\"\n", " query = state[\"query\"]\n", " character_description = state[\"character_description\"]\n", " response = llm.invoke([HumanMessage(content=f\"Create a short, 5-step plot for a GIF based on this query: '{query}' and featuring this description: {character_description}. Each step should be a brief description of a single frame, maintaining consistency throughout. Keep it family-friendly and avoid any sensitive themes.\")])\n", " state[\"plot\"] = response.content\n", " return state\n", "\n", "def generate_image_prompts(state: GraphState) -> GraphState:\n", " \"\"\"Generate specific image prompts for each frame of the GIF.\"\"\"\n", " plot = state[\"plot\"]\n", " character_description = state[\"character_description\"]\n", " response = llm.invoke([HumanMessage(content=f\"\"\"Based on this plot: '{plot}' and featuring this description: {character_description}, generate 5 specific, family-friendly image prompts, one for each step. Each prompt should be detailed enough for image generation, maintaining consistency, and suitable for DALL-E. \n", "\n", "Always include the following in EVERY prompt to maintain consistency:\n", "1. A brief reminder of the main character or object's key features\n", "2. The specific action or scene described in the plot step\n", "3. Any relevant background or environmental details\n", "\n", "Format each prompt as a numbered list item, like this:\n", "1. [Your prompt here]\n", "2. [Your prompt here]\n", "... and so on.\"\"\")])\n", " \n", " prompts = []\n", " for line in response.content.split('\\n'):\n", " if line.strip().startswith(('1.', '2.', '3.', '4.', '5.')):\n", " prompt = line.split('.', 1)[1].strip()\n", " prompts.append(f\"Create a detailed, photorealistic image of the following scene: {prompt}\")\n", " \n", " if len(prompts) != 5:\n", " raise ValueError(f\"Expected 5 prompts, but got {len(prompts)}. Please try again.\")\n", " \n", " state[\"image_prompts\"] = prompts\n", " return state\n", "\n", "async def create_image(prompt: str, retries: int = 3):\n", " \"\"\"Generate an image using DALL-E based on the given prompt.\"\"\"\n", " for attempt in range(retries):\n", " try:\n", " response = await asyncio.to_thread(\n", " client.images.generate,\n", " model=\"dall-e-3\",\n", " prompt=prompt,\n", " size=\"1024x1024\",\n", " quality=\"standard\",\n", " n=1,\n", " )\n", " return response.data[0].url\n", " except Exception as e:\n", " if attempt == retries - 1:\n", " print(f\"Failed to generate image for prompt: {prompt}\")\n", " print(f\"Error: {str(e)}\")\n", " return None\n", " await asyncio.sleep(2) # Wait before retrying\n", "\n", "async def create_images(state: GraphState) -> GraphState:\n", " \"\"\"Generate images for all prompts in parallel.\"\"\"\n", " image_prompts = state[\"image_prompts\"]\n", " tasks = [create_image(prompt) for prompt in image_prompts]\n", " image_urls = await asyncio.gather(*tasks)\n", " state[\"image_urls\"] = image_urls\n", " return state\n", "\n", "async def create_gif(state: GraphState) -> GraphState:\n", " \"\"\"Create a GIF from the generated images.\"\"\"\n", " image_urls = state[\"image_urls\"]\n", " images = []\n", " async with aiohttp.ClientSession() as session:\n", " tasks = [get_image_data(session, url) for url in image_urls if url]\n", " image_data_list = await asyncio.gather(*tasks)\n", " \n", " for img_data in image_data_list:\n", " if img_data:\n", " images.append(Image.open(io.BytesIO(img_data)))\n", " \n", " if images:\n", " gif_buffer = io.BytesIO()\n", " images[0].save(gif_buffer, format='GIF', save_all=True, append_images=images[1:], duration=1000, loop=0)\n", " state[\"gif_data\"] = gif_buffer.getvalue()\n", " else:\n", " state[\"gif_data\"] = None\n", " return state" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Set Up LangGraph Workflow\n", "\n", "Define the LangGraph workflow by adding nodes and edges." ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [], "source": [ "workflow = Graph()\n", "\n", "workflow.add_node(\"generate_character_description\", generate_character_description)\n", "workflow.add_node(\"generate_plot\", generate_plot)\n", "workflow.add_node(\"generate_image_prompts\", generate_image_prompts)\n", "workflow.add_node(\"create_images\", create_images)\n", "workflow.add_node(\"create_gif\", create_gif)\n", "\n", "workflow.add_edge(\"generate_character_description\", \"generate_plot\")\n", "workflow.add_edge(\"generate_plot\", \"generate_image_prompts\")\n", "workflow.add_edge(\"generate_image_prompts\", \"create_images\")\n", "workflow.add_edge(\"create_images\", \"create_gif\")\n", "workflow.add_edge(\"create_gif\", END)\n", "\n", "workflow.set_entry_point(\"generate_character_description\")\n", "\n", "app = workflow.compile()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Display Graph Structure" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "data": { "image/jpeg": "/9j/4AAQSkZJRgABAQAAAQABAAD/4gHYSUNDX1BST0ZJTEUAAQEAAAHIAAAAAAQwAABtbnRyUkdCIFhZWiAH4AABAAEAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAACRyWFlaAAABFAAAABRnWFlaAAABKAAAABRiWFlaAAABPAAAABR3dHB0AAABUAAAABRyVFJDAAABZAAAAChnVFJDAAABZAAAAChiVFJDAAABZAAAAChjcHJ0AAABjAAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAAgAAAAcAHMAUgBHAEJYWVogAAAAAAAAb6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAAACSgAAAPhAAAts9YWVogAAAAAAAA9tYAAQAAAADTLXBhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABtbHVjAAAAAAAAAAEAAAAMZW5VUwAAACAAAAAcAEcAbwBvAGcAbABlACAASQBuAGMALgAgADIAMAAxADb/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/wAARCAJ2ARQDASIAAhEBAxEB/8QAHQABAAMBAQEBAQEAAAAAAAAAAAUGBwQIAwIBCf/EAFoQAAEDBAACBQYHCQsKAwkBAAEAAgMEBQYRBxITFSExlAgXIkFW0xQWMlFUVdI2N2F0dZOz0dQjJjNEcYGRlbK0wyQ0NUJSc4KhsfAJJXIYJ0NFRlNiZJKD/8QAGwEBAQADAQEBAAAAAAAAAAAAAAECAwQFBgf/xAA4EQEAAQICBggFAgYDAQAAAAAAAQIRAxIUITFRUpEEE0FicZKh0TNhscHSMvAFIkJDgbIVI8Lh/9oADAMBAAIRAxEAPwD/AFTREQEREBERAREQEREBERARF8qqqhoqaaoqJGwwQsMkkjzprWgbJJ+YBNuqB9Vx1d4oKB3LVV1NTO+aaZrD/wAyoJturcwaJ7hLV2y0v0YrbE50E8rf9qd4PM3fqjaRofL2SWt7aTB8doGclPYrdENaJFKzZ7d9p1s9vb2royYdOqudfy9/34raI2uj41WX64oPEs/Wnxqsv1xQeJZ+tPirZfqeg8Mz9SfFWy/U9B4Zn6k/6fn6Lqf1uUWZ7gG3ahcT3AVLP1qRjkZKwPY4PYe0OadgqMdidje0tdZre5p7waVmj/yUe/ArbSvM9mDsfq9g9JbgGMdr1Pi1yOHz7G/mIPalsGdkzH7/AHvTUsqKGsd5nqp5rdcomU11pgHPEe+injPdLHvt5T3Fp7Wu2DscrnTK01UzRNpQREWIIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgKr5bq5XiwWR2jBUzvq6lh/14oAHBv88rod+ogOB71aFWL8Pgma4xWu30cjaq371sB0jWSt38w/ycjfzkD1rowP13+U/SbLG1Z1m9J5RfDu4Z3WYdS5JHVZFRvminpIKWd7WSQsc+WPpQwxl7WtcS0OLhojW1pC8jY3R5LiflNGm4eY9mlpxm63ysqMro79bwLI8kHdbR1BOw6QgENae3YBAA0udF94HeWBinGC0ZPWTGSzy2M1lVNC6mqXtbb4HAfCDIYmt5iDzGMemO7XYSrdiXlLcNc5suRXWyZPHV0mPUr626B1LPFNTQNYXukMT42yObytJ21p33DtIXnbFK7idw74T8XMFxnDMjo86hulyu1rvJt4db6mCSqjP7hMTyvmMTpHNZonbfn7FUrLheS1mZ8RrpR47xNrLZeOFtztENbm1PLNW1NwBDxEGjZjBB0xhA5nc/KDtBv2b+W5w9tPDDIMsxauflUlsghkjp2UdXBDK+YkRtMzoeVvc/Y7wWEHRWs8L+Jdl4s4hS5BYp5KikkPRvMlLNTlsoAL2hsrGuIBPfrR9RKxXJeGt8vnkAUmH26zzxZE7D6GM2p8Rim+EMiifJGWO0RIXNeC09vN2d61vgpmsuc4DRVc+NX7FZ6UMopKLIqE0k7nMiZzPawkks24gO7Nlp7EErnGra213tmmzUFXHG93bswTPbHI3+Tta/Xzxt+ZWdVjiI34Tj8dA3ZlrqympmADffK0uP8zGvP8ys66KteFTM7dfLV95lewREXOgiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAo++2aK/WyWjlc6IktkjmZ8qKRrg5j2/ha4A/zKQRZU1TTMVRtgQdmyLpqkWu6dFR3trdmEEhlQAO2SEntc35x2lvcfUTOLiu1lob9SGluNJFWQcweGTNDuVw7nD5nD1Edo9ShfiIyEctJfb5RxgaEba4yho/AZQ8/81uth1675Z9P3/j/ACupZ0VX+JNR7VX789D7pVPhzb7rlNBepq7KbwH0d6r6CLoZYQOihncxm/3M+lygb/D6gnV4fH6SWje1Rc1wuVJaaR9TW1MVLTs1zSTPDWjfcNn1n5lAfEic9jspvzh6x08Q/wCYj2uq3YVa7fVsrHsnuFcztZU3CofUPYda2znJDOz/AGQO8/OUy4Ubar+Ee/8A9NT5Wummv93ivdZA+lpqdrmW6lnaWyjmGnzSNPyXOHY1p7WtJ5tF5Yyxoi1V155+RIiIsEEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAWe8FiDaMn0SR8Z7t3/jcn4T/wB/MtCWe8F99U5PvX3T3b5Ovpcnzf8AfzoNCREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBZ5wVGrRlHpB376Lv8AJGv43J2LQ1nnBXXVGUa2f30XfvGv43Ig0NERAREQEREBERAREQEREBERAREQEREBF/HODGlziGtA2ST2AKlHML3dgKiy2yhNtf2w1FwqXxyTN9TxG2M8rT3jZ2R3gLdh4VWLfL7La67IqR17mH0Cx+Lm92nXuYfQLH4ub3a3aLXvjnBZd0VI69zD6BY/Fze7Tr3MPoFj8XN7tNFr3xzgsu6Kkde5h9Asfi5vdp17mH0Cx+Lm92mi1745wWXdFSOvcw+gWPxc3u069zD6BY/Fze7TRa98c4LLuipHXuYfQLH4ub3ade5h9Asfi5vdpote+OcFl3RUjr3MPoFj8XN7tOvcw+gWPxc3u00WvfHOCy7oqR17mH0Cx+Lm92nXuYfQLH4ub3aaLXvjnBZ9eMWe1nC7hnf8sobHJkc9pgFSbbFP0LpYw5vSHn5Xa5WFz+475ddm9rzJ5GXlc1fGfNrti1BgslFRy1dde626OuTXtpGTSuexnIIW85L3Nb3js271aPpKpumV1lPLBPa7DNBK0skjkqZnNe0jRBBi7QQsp8nvgRV+TnQZBTWCjtNS68V7quSaoqpQ+OIb6KAaj7WsBPb6ySU0WvfHOCz0iipHXuYfQLH4ub3ade5h9Asfi5vdpote+OcFl3RUjr3MPoFj8XN7tOvcw+gWPxc3u00WvfHOCy7oqR17mH0Cx+Lm92nXuYfQLH4ub3aaLXvjnBZd0VI69zD6BY/Fze7Tr3MPoFj8XN7tNFr3xzgsu6Kkde5h9Asfi5vdp17mH0Cx+Lm92mi1745wWXdFSOvcw+gWPxc3u069zD6BY/Fze7TRa98c4LLuipHXuYfQLH4ub3ade5h9Asfi5vdpote+OcFl3RUkX3Lwdm32Qj5hWTDf8/RdinseyEXttRFLAaO4Urgyopi7mDd9rXNdoczHDtB0O4ggEEDXXgV0RmnZ8puWTCIi50ReUEtxm7kHRFHMQf8AgKr2MgDG7UAAAKSLQH/oCsOVfcxePxOb+wVXsa+5y1fikX9gL0cH4M+P2XsSSIiyQREQEREBEXDbL5b70+tbQVsFa6iqHUlSIJA/oZmhpdG7Xc4BzdjvG0HciIgIih8Sy6051j9Ne7HV/DrXUl4in6N8fMWPcx3ovAcNOa4do9XzKCYREVBERARcNnvlvyGi+GWutguFJ0skPT00geznjeWPbsdm2ua5p+YgruQEREBFDwZdaanLKvGY6vmvdJRxV81L0bxyQSPexj+bXKdujeNA7Gu0doUwoCIioIom7ZVa7FdrNba6pMNbeJn09DF0b3dK9kbpHDYBDdMa47cQOzXf2KWUBFw0N8t9zrrhR0lbBU1VvkbFVwxSBzqd7mh7WvA+SS1zXaPqIPrXPi+VWvM7SLlZ6k1dF00tP0pjfH6cUjo5Bp4B7HscN60dbGx2oJZERUFG42f/AHi30erqqhP8v7tVqSUbjf3xr7+SqH9NVrL+1ieH3hY7V1REXlIi8q+5i8fic39gqvY19zlq/FIv7AVhyr7mLx+Jzf2Cq9jX3OWr8Ui/sBejg/Bnx+y9jsrXTso53UrGyVIjcYmPOmufrsBPzb0vHPC7JL9e8hwG8Ut+y/Ib/TQ3KrzO0Vs9SyipKiOnlayPo9COMichjI27Dh6RBLQ4ey3bIOtb/CvNvDjydsvxHN7FXsqbRjVotlQ6SeOxXq61La+Hkc1tOaWpkMMLNua70S4jkHLpY1RN4RW+D9Hxbzy14XntJdQ/rSogrrhNPlcs1JNSuf8Au8Dbd8EEcTmt5mt5X8zXNG3u7d/FlxyC3cNrjxCGX5FPdrZnUlDFSTXKR1G6jN5+DGndD8l7eSQ6c4FzdNDXBrQBvtk4B4FjmVNyK2WBtFc2Tvqo+iqpxTxzPBa+RlPz9ExxDnAlrAe0qRk4SYnLi1VjjrVuzVVwN1mpvhMvpVRqRUmTm5+YfuwDuUHl9WtdimWR54mdxR4v5NxDrcfrpqGeyXyqs1sMeVS2+Gh6ANEb5aJtJIycP2JCZHHmD+UcoCuuIWe+5nx5zOmyHJbxBBY6CxVHVVpuc0FGat8crpXANIJYXRkFnY14d6QJDdaBkvALAsuyaXILpYGy3WfkFRLDVTwNqeT5HTRxvayXWgBzh3YAFabdiNptOSXi/wBLSdFdruyCOtqOkeelbCHCIcpPK3lD3fJA3vt32KxTPaPLduzLITnuFZpYKzI/ifkeVOtIkvuQGoirYJOnH7nQdHywMa6PbHh4fpg5mnm2pHh5SHhniXH7M7TVXavu1lvF6FPSVlzqJ6ZxZBDK174XPLXP2Bt5HNy9m9LYYvJw4dQXRlwjxwMqoqwXCnLayoDKWoEgk54GdJywkvGyIw0O7QQQSFN0/CTE6TNK7K4LV0N7rmltVKyolENRtnIXPg5+ic4t7OYt3+FSKZGL8IcY4pSXbDcmN0NRZ66NtTdpa3LJblFXwSwlwdDTGkjZA4PLHN6NwaAC0gg7XpOoldBTyyNYZXMaXBje9xA7gs8x/gLiOA1VTc8PtEFnvQgmjopKiaoqKWlc/tPJTmUNYwnW2x8mwNbC6KW18Um1UJqcmxCSnDwZGRY7VMe5u+0Nca4gHXcSD/IVlF4FE8nayV/ELEca4mXnMshrbzdBJWTW+C4uZbIwXPaKYUo9Dlj7Bs+mXNJLvUsr4W0NzwjhZwqy23ZPfOmuGWMtNTapKwm3upaivmhcwU+uUEb5w/5XNvt1oD0dbeAeB2fLfjJQ2EUl1+EurB0NVO2nE7gQ6UU4f0QednbgzZ2VI0/CTE6TGrNj8Vq5LRZ6+O6UNP8ACZT0NSyYzNfzF/M7Uji7TiR261rsUyyPOWS5nkIzWgzbF6zI2427NKexz1F0yAupKpjqsU08UNuEZaIw4vDZC5rwW70VdsP6Wrzji/k2RZVkAs2LXt7qa3wXGZlNTxMoIZZCY2n90aebYjdtoIJDduJN8uPk4cOrtcq2vqscD6irqvhz+WsqGMZU84eZ4mNkDYpS4bMkYa47OydndttmD2O0SX99Nb2N6/qHVVzbI90jamQxNiJLXEgAsY1vKAB2d3aUimR5k4eZRl+J8Q8Sqycilx3KbJca6GgyPIes6mpMMUc0MnRCMNpnkO0WRuc0h+tAtWhcAcVq83wrGOIV4zfI7pd71RmtqqWG6PjtzTKxwMLKdvoMEXNoFunczNl3eFcca8nvAMQu9sulqsTqe42xxNFUvrqmV9O0scwxsL5DqLle4dEPQ7jy7AX1sfATA8ZyluQ2uwihuTJ5KmMQ1U4p45XhzXvZT8/RMcQ5wJawd5SKZjaPOWI1VZwg8k7Icox253Lria8VVt6S4XSaanomuvEkBmDJC9kTg1/MX8h270nB3bu2y4txRwPHszudRcaijsLMWuTpGVOWVF4qW1bYS6GogfJTROhI0/Ya7l7WkAFq2aj4H4RQ3DIKuKwx7v7JWXOmknlfS1AlIdITTueYmlxaCXNaCT6+0r4Y5wDwXFLZeLfbbNJHS3aidbqts1fUzufTEEGJrpJHOjbpx0GFut9mlMsj48CcZmtOA2W7V19vN+ut3tdHUVk91r5J2dJ0XMTHG48kQ9Mg8oBOm8xJG1w+UXk11sOK4/b7TcpLJNkWQUNjmu0OulooZnHnkjJ2A8hvI0kdheD3gK1Xiw5DbbPabbhVfZ7NS0UQpzHd6CauHRta1sbWFtRERoA7Li7fZ3euKfgV6zS0XKx8Rp8cybH6yINNFQWmekdzhwcHF76mXu1scoaQQCD2LK02sPO2f1Fx4AZZxYuGP3i63W4U2IWh1PW36tdWS0xlr54XP6R4ceVgc6T0uYA77Ndiu2PcOuMNtqK7q+9C20dbZqyAzXXKpr4RWOj/AMmqYhJSR9Fyv+UGksId2M9Eb1TGuA2C4k66ut9iDjdaNtvr/h1XPWfCadpdqN/TPfzD03Dt7daHcAB88c8n7AsTo7nS22xOjp7lRPttRHPXVE4NK75ULOkkd0bD/ss0FjlkYG3PLxiGE1GJG75TYM3nvVmtF6qL/chcer4auQsdWUk7uZvJIGvA3rldrbWkBXrjZiNw4W8Fr07Hs0yoV9Zc7RFHXXG7SVU1KXV8MbjG5/aA4PIc3fKR2a0SDotp4BYBZsfvdkhxyGe3XprGXFldPLVvqWs/gw6SV7n6Z/qgH0T2jRXzt3k/YHa7JXWmCzTOoq2alnqG1FxqppJHU0glg/dHyl4DHjYaDrvGtEhXLIpWQ41Nw/4ycIKa35Jk1VTXK4XKGtguN7qamKpAt80g543vLTp7Q4DWmkdgCzKnr79YeAfnPgzLJJ8mo8hqIoqOsu0s1HVxdbvphSmneS0gx9gIHM0604AAD1ddsRtN9vdju9dSdPcbJLLPb5uke3oXyROieeUEB22PcPSB1vY7e1ZNwn8lzHcSpKC4ZHbKa6ZPSXSsuMdRHWVEtMx8lVLJDI2F5EYkbG9g5uTYLewnQKk0zfUIrg/gNFLxm41VRul9bKy8si6Nt5qRHyzUELi4s5+UuaXkMcRtga0NIDRqk8ObzkGY2jhThdblV+pbddKnJJq25xXKUXCsFHXPZBT/AAokyABr9nR2WxgAgAr0e/hTi78+Gai2uiyUtax9ZBVTRtlDWFjTJE14jkIa4tBe0kDuPYFwVvAvB7hitFjk1k/8qoquaupWx1c7JqeeWR8kkkc7XiRhc6V57HDsdru0FcsjAGZBkdbkFqwj43319vt/EOewi7Q1pbV1NGbZJOaeWUdr3Mc4t5z6Q5WkEOaHDX+B1XX2/K+JmJ1N1r7xQY/eIGUE90qXVNQyKajhnMbpXkueGue7RcSdHW+xWm1cHMNsdDYKOgskVLT2KtfcaBscsgMdS5j2Ple7m3I4tkeCZC7e994Gpu0YjabDer5dqGk6C4XuaOor5uke7pnxxNiYdEkN0xjRpoA7Nnt7VYpmBMKNxv7419/JVD+mq1JKNxv7419/JVD+mq1t/tYnh94WO1dURF5SIvKvuYvH4nN/YKr2Nfc5avxSL+wFcaiCOqgkhlbzxSNLHNPrBGiFQ4aW/wCM08NubZJr5T07GxQ1lJUQtc9gGm9I2V7NP0O3RIPf2b5R6HR5iaJovab31zb6so1xZOooTra/exl18VRe/Tra/exl18VRe/W/J3o80e5ZNooTra/exl18VRe/Tra/exl18VRe/TJ3o80e5ZNooTra/exl18VRe/Tra/exl18VRe/TJ3o80e5ZNooTra/exl18VRe/Tra/exl18VRe/TJ3o80e5ZNooTra/exl18VRe/Tra/exl18VRe/TJ3o80e5ZNooTra/exl18VRe/Tra/exl18VRe/TJ3o80e5ZNooTra/exl18VRe/Tra/exl18VRe/TJ3o80e5ZNooTra/exl18VRe/UdY83r8jhqpbdil1qI6Wqmopj09I3lmieWSN9KYb04EbHYfUSmTvR5o9yy2IoTra/exl18VRe/Tra/exl18VRe/TJ3o80e5ZNooTra/exl18VRe/Tra/exl18VRe/TJ3o80e5ZNooTra/exl18VRe/Tra/exl18VRe/TJ3o80e5ZNooTra/exl18VRe/Tra/exl18VRe/TJ3o80e5ZNooTra/exl18VRe/Tra/exl18VRe/TJ3o80e5ZNooTra/exl18VRe/Tra/exl18VRe/TJ3o80e5ZNooTra/exl18VRe/Tra/exl18VRe/TJ3o80e5ZNqNxv7419/JVD+mq1ztul+cdfE65tOuwvqqPX8+pif8AkpvFbHV0dTXXS48jK+tEcfQROLmQRM5ixm/9Z23vJIAG3aG+XZxrtRh1RMxri2qYntiezwNixIiLymIiIgIiICIiAiIgIiICIiAiIgIiICz/AINDVqybs1++a6+rX8af+Af9+s960BZ7wWby2jJ+wjeT3Y9o1/G5EGhIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAs84KkG0ZRo7/AH0Xf1a/jci0NZ9wYDhacn5i8n4z3b5Y0dfC36/m+ZBoKIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiKHvGYWLHpmw3S80Fvmc3nEdTUsjcW71vRO9b7NrKmmqubUxeTamEVW86WHe1No8bH+tPOlh3tTaPGx/rW7R8bgnlLLLO5aUVW86WHe1No8bH+tPOlh3tTaPGx/rTR8bgnlJlnctKKredLDvam0eNj/WnnSw72ptHjY/1po+NwTykyzuTd7vttxm2TXK8XGktVuh5elq66dsMMfM4NbzPcQBtxAGz3kBZTwH4jYjdRfrXQZRZay5VWR3WaCip7hC+aZhqJH87GB5LgWgu2PUN9ykeKlw4fcV+HWQYjdMotHwO7UroC/4bHuN/ex49Lva8Nd/wryL/wCHzwhs3CrI8py3M7ra6G8U00lptkc9VGD0YP7rUM2fkv8ARa1w7xzfOmj43BPKTLO5/oaiq3nSw72ptHjY/wBaedLDvam0eNj/AFpo+NwTykyzuWlFVvOlh3tTaPGx/rTzpYd7U2jxsf600fG4J5SZZ3LSiq3nSw72ptHjY/1p50sO9qbR42P9aaPjcE8pMs7lpRVhvFDD3u0Mos+/x2P9askUrJ4mSxPbJG9oc17DsOB7iD6wtdeHXh/rpmPFJiY2v2iItaCIiAiIgIiICIiAiIgIiICIiAiIg/E8nQwySa3yNLtfPoKgYKxsuLW24P1JWXGnjrKqoI9KaV7A5zj2n59Ab9EANHYAr3W/5nP/ALt3/RUXAPuDxv8AJtN+iavQ6P8ACqn5x92XYnkRFmxEREBERAREQEREBERAREQCAQQRsH1FcGDuFFfsgtUPoUcHQVMUI+TGZQ/nDfmBLObQ0NuPzld6jcR+7nJ/xai/x1Z14VfhH1hlGyV1REXlsRERAREQEREBERAREQEREBERAREQfCt/zOf/AHbv+iouAfcHjf5Npv0TVeq3/M5/927/AKKi4B9weN/k2m/RNXoYHwavGPpLLsTpIaCSdAd5WE435Sl1yW84RIzCxRYlmFdLS2u9VF0Blexkcr2ufTtiJYXiIlo5j2fKLVu68O8GbzR45xVxq0xNtuXtgu1TT0Nttd0ry7HxMZOknbQzUzWwxtaS080ji0OPKTvtVTaYYtQPls4865tqI6e0y4w6vFAKtuR0vWZBl6Lpxb/4To+bt+Vz8npcmlO1PlKXSkp73eJcILcTsmRS4/cLp1qwzNLasU4njg6P02bcwuBc0jZA5gOY/ThhwrzvhW2hxWhfitxweirHvp66sZOLmykdI6ToCwN6Nz28xaJOcdgBLV8btwIv9fwj4gYtHWW0XDIMmqb1Syulk6JkMlcyoa2Q8mw/kYQQARvXbrtWP8w5eIHld2nD8ov9roqSz18OPv6K4OuGSUtuqZJQwPeylp5NumLQ4DZLAXbaCSCp+3cerlluciwYjibbzSus9vvgulXcvgkQpqrnI5m9E9weA0ENG+b0tlvKN8XmtzzCcxyuqwuTFq+yZJXm6yR5E2cTUFU9jWSlnRtIlY7ka4NLmaOxv1q5Y9gNfaOMWV5XJLS9WXW1W6hp4YnO6Vj6d1QXlzeXQaRM3Wie47A7N2MwqF18o2XGeKlvxK+2G30NNcbmLXS1EGQU9RXczyRDLJRNAeyN5A9LmJHM3YC5sD4qZeeI3FduS0dBDh+OVx3W9YbfQwMo45gGxCAdIHA9I4l+2l5aOYNBNZh8nPN6CG3W6mlxR9Ha8sZk7brN0/WF0IqzNyVDuTUbgx5bzgyb5GDTRvV1qeEOQvzfiDB0tpqsFzqP/wAyEkssdwpXGiFK9sQDCx4dyMdtzmkbcNHsU/mFfwnywbTleUY7b5qG1U1BkNQKa3vosjpa2uje5pdGKqkj9KHmA0dOfyuIDtbXoZY5w1xriBw9t9ut2SnFq/GrDRuiNzttPUvudZHFHqN3QBmmv00FwaZC47DQCVYoeOuLTzMjbBkvM9waObE7q0bPzk02h/KVlE6tYorvKZvFJYrplVdgohwm1XiotVddIbw2SohbFUmnNQKcxDmj2ASOfmHboOA2Vk4snBIONV+vlRX3ajs+TspaGhbKZX7kpaRsVPCHHTQ6WXuGgC8n51n+DYBm/FXh3lGJRVNht2C3LLLs2vrC6Z1yMLblI6SKOPl6Pby0t5y7sDvkkja0PI/J/vl7fxMt8V0t9NaMlrqS+2yp6N5qaK4wCDlD2fJdFumYdgh2nOGvWsf5p1iRl405RE+/2C7YfFjmXx2Ka9WqHrZtVTVUcZDXgzCIcj2OczbSxwPMNEhTfk6ZVlOZ8I8Zu+V0dNDWVdtpZ46uCs6d1a18LXGaRvRRiJziSSwcwG/lKIsnC/LspzqfKc+qbLTTQ2Sex0NBj75pY2Nnc10073ytaeY8jAGgEAb7SVO8CsUy7AsIoMXyd9lqKay0sFvt1XapJS+ohjaWB8zHsAY7lDOxpcN83b3LKL3GjKNxH7ucn/FqL/HUko3Efu5yf8Wov8dbZ+FieH/qGUbJXVEReUxEREBERAREQEREBERAREQEREBERB8K3/M5/wDdu/6Ki4B9weN/k2m/RNV+nj6aGSPeudpbv5thZ/gsjIcZt1ueRHXW2njo6qmLtvhkYwNII7Do6BB1pzS1w7CF6HR/hVR84+7LsWBERZsRERAREQEREBERAREQEREBRuI/dzk/4tRf46kXODWlziAANkn1Lhwdorb7f7tD6dFUdBTQzD5MpiD+Yt+cAv5djsJafmVnVhV+EfWGUbJXJEReWxEREBERAREQEREBERAREQEREBERAUTecRseRSNkutmoLlI1vIH1dMyVwbvetuB7N9ulLIsqaqqJvTNpNireavDPZOyf1fF9lPNXhnsnZP6vi+yrSi3aRjcc85W871W81eGeydk/q+L7KeavDPZOyf1fF9lWlE0jG455yXneq3mrwz2Tsn9XxfZTzV4Z7J2T+r4vsq0omkY3HPOS871W81eGeydk/q+L7Ko3CTh3i9xteROq8etVY6LIrnDG6ajieWRtqXhjB2HTWjQA9QGtBbEs+4MEm05PzO5j8Z7t29vd8Lf86aRjcc85LzvTHmrwz2Tsn9XxfZTzV4Z7J2T+r4vsq0omkY3HPOS871W81eGeydk/q+L7KeavDPZOyf1fF9lWlE0jG455yXneq3mrwz2Tsn9XxfZTzV4Z7J2T+r4vsq0omkY3HPOS871Yj4X4dE8Obilla4dxFvi+yrJFEyGNkcbGxxsAa1jRoNA7gAv2i114leJ+uqZ8S8yIiLWgiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAs94LNLbTk+2cn757sddvb/lcnb2/P3rQlnnBVpZaMoBa5u8ou5078bk7UGhoiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAs94LgC05PrQ/fPdvk7+lyfP/AN/MpPi5d8nsHDXIbnhlJRV+TUVK6oo6W4RvfDMWEOcwtY5riSwODdEekW/yLyd5A3lAcSOM+UZLSXC1WGixWlqKi5V1TTUs7ZzV1MrniFjnTFrRsvPa0nlbreztB7gREQEREBERAREQEREBERAREQEREBERAREQFy3S4w2e2VdfUc3QUsL55OUbPK1pcdD59BdSrPE772uWfkir/QvW3CpivEppntmFjXKHY3I7xC2qnv1RZnygPFHb4IHNhHqaXSxvLjojZ7NkdgA7E6nvvtpePDUP7OpmL+CZ/IF+l6We2ymOUexdCdT3320vHhqH9nTqe++2l48NQ/s6m0TrO7Hlp9i6E6nvvtpePDUP7OnU999tLx4ah/Z1NonWd2PLT7F0J1PffbS8eGof2dOp777aXjw1D+zqbROs7seWn2LoTqe++2l48NQ/s6dT3320vHhqH9nU2idZ3Y8tPsXQnU999tLx4ah/Z06nvvtpePDUP7OptE6zux5afYuhOp777aXjw1D+zqsYHwbpeGNHcaXF77cbPT3CskuFTHDBRkSTv1zO9KA6HYNNGmj1ALQkTrO7Hlp9i6E6nvvtpePDUP7OnU999tLx4ah/Z1NonWd2PLT7F0J1PffbS8eGof2dOp777aXjw1D+zqbROs7seWn2LoTqe++2l48NQ/s6dT3320vHhqH9nU2idZ3Y8tPsXQnU999tLx4ah/Z06nvvtpePDUP7OptE6zux5afYuhOp777aXjw1D+zp1PffbS8eGof2dTaJ1ndjy0+xdCdT3320vHhqH9nTqi+j/wCtLuf5aah1/d1NonWd2PLT7F34xe91j7jU2e5yMqauCFlRHVxs5BPE5zm+k3uD2lujrsOwRrehZlR7T98t/wCSP8ZXhcXSaYpr1dsRJIiIuVBERAREQFWeJ33tcs/JFX+herMqzxO+9rln5Iq/0L10dH+NR4x9VjbD+xfwTP5Av0vzF/BM/kCh83bXOwy/ttdXHQXM2+oFLVzODWQS9G7ke4nuDXaJP4F1yiZLmhwaSAT3Dfev6vD/AAzx+SohtWYYDi1Xar7ZMLuLrpV3BzJn3O6vgYIHxjneZXGRsrumAAc14bvt5RO8BOHsN1qeHmWWvO8UhutSGVlX1fT1Aut2BiJqKeqdJWvEjtkl24/RcwEBoGlriq/YPYaLxVguKWuxcJ+D+aUNOYMpqMzp6Ga6CR5mkppa+aB8BcT/AAXR6HJ8ka3ra4rPhdZxTqsrul3zbFsZzaLI6mhFZcqao63tcjaktpY4JPhsbQws6PkaI+Vwdoh5JJZ/kPcSLzvgOD2K68duMWQ3q3tu9fZ7rbpaPpGl4p5GW2neZIWE6bITr0h2+i0bWVcL6mjpeLHCLLLM3HccgzGet6Wz2qrnnrpaZ1LLI34ZI+UtlcHtj/8Ahgtf2bKuYew8YzC1Zgy5vtVQallur57ZUuMbmctRC7lkaOYDej2bHYddmwppeLbPa7Vw74RcerlhNvt9qzugvF5o4ZKCNja2CiE7HAMA9INZF6bddg5QR3K78DuGVDac9x6+49muHupX0E01TbcZpqiKa7U72ANln6Wtm5iyR0bukLebZIJ9JSKpkem1E41kHxkoqioNsuFq6GqmpehuUHRSP6N5Z0jRs7jdrma71tIOgpKop46unlgmY2WGVpY9jhsOaRog/wAy8XYRiNouTOFWNT0MZsLs5yimdb2EsidDG2tLYiARtnoNBaewgaII7FlM2HtVF4gufD+w4zwv4s3+2UPwO8YpmvQWKrjlfz22Fs1I4RQbd+5xkzS7Y3QPOdju1+PKMuNHcq/iLmlthsGNXfErnS0UVzramc3ipqYxA7cAErWQxFrwAOV4eA8kDe1jnsPcLnBjS5xDWgbJJ0AF/V4v4843Dm3EXifYK+y1GS5JcKOhhxSthqmCC0h8IDmSOLwID0nPI4EEyMcO8EBfbK8XfnHGLOrPlV8xG2tsFJQstkGS0k/JDSGmaX1FJ0dXA2P916TmeAXNLWjmAACZvkPZSKucOLRW2DArBbrje/jLW0tFFFJdy3RrNNAEp9J2y4aO9nffvtWL8Ssfw3JfKjpKTNobbUWwYVJIyK6ytZEXitHbpxAJDS4/g7+zvWUzaB6LReKcOtFNxAqeDdquU1RdsU+M+SwWp9RM9xq7XFFP8Ha55O3xlrQ3RJDmDR2CrZdsO4f13FHiLZs8FHbrFjVmt7cbop6g00VBQmB5lnpmhwAeJmuaXt24cjBsevHNceqkXjbhbi0XF/KMdHEW3uvVTLwwt9RNDXucQ+T4XUhkz27G5OQ7Dj6QL3EEElf3hDj9Fj9k8m/KaGOSHIL9LJR3W4GZ75a+F1DUP5JnEkvDXRsLQexvKOXSZh7IULZMwtWQ3i+2ugqDNWWSoZS1zDG5oikfE2VrQSNO9B7TsbHbrvBXizF7didk4FYPkOPy0lPxUkySOnopKSp/yyoc66OZLBI0O26LoC/bCOUDt0tMwzB8Lg4q8eWyOteMZE6rIpbsOjiq6KGotsb5Z4ySCBzGWQkHWw4k96RVceoUXnnyVae04pcMiw2ntdnivNto6CeqvWP1rqiku0TxKIp3NJJimPI8vadk8wPM4a16GWUTeLiKtP3y3/kj/GV4VHtP3y3/AJI/xleFq6V+qPCFkREXGgiIgIiICrPE772uWfkir/QvVmVa4lsMnDjK2tG3OtNWAP8A/F66Oj/Go8Y+qxtgi/gmfyBfOupzV0VRAHiMyxuYHlgfy7Gt8p7D/IexfuEh0LCCCC0aIX7XXKMF4b+SzHhGfWjKKq72iae1MnbBHYcZprO6oMsZjJqXwk9KACSGgNHNo67Frds4f4vZb5UXq3Y3aKC8VJJmuNLQRR1EpPfzSNaHHf4Sp9FjERGwQ8eHWCG20VujsdtZb6KobV0tI2kjEVPM15e2SNmtNeHkuDgAQST3r4V3D/F7nkEN+rMbtFXfIddFc56CJ9SzXdyylvMNerRU+ithx0dmt9vrK6rpaGmpquue2SrnhhaySoe1oY10jgNvIa1rQTvQAHcFD0nDTELfVSVNLilkpqmSqbXPmht0LHuqGklsxIbsvBJ07vGz2qyIgh48NsEORTX+Ox21l9mj6KW6NpIxVPZoDldLrmI0ANE+pRcHDKwWSmuRxe20GHXOubqS6WW3U0VRve+Y80bmuP8A62uCtiJYUS2cP8lobjS1FRxNyK4QRStfJST0VsbHM0HZY4spGuAI7CWuB7ewgqx02HWCjlpJKex22CSkqJqunfHSRtMM0vN0srCB6L387+Zw7Xcx3vZUwiWEPNh1gqKG4UUtjtstFcZ/hVbTPpIzHVTeiekkaRp7/QZ6Ttn0W/MFz3Lh5it5u011r8Zs9dc5oDTS1tTQRSTSRFvKY3PLS4tIJHKTrR0rAiWGEcWPJfj4nXypqBcMfobbUUjKLoKnEqSrqqaNrOX/ACeqcWvjOu7YcGnuAWmT8LMUuVrstFeLDb8i6op46ekqbzSRVczAxoaHc72k8x0CSNbKtaKWgUq+YJfrldJqi38Qr5YaNwaI7dQ0VufDCA0DTTLSvf2kE9rj2k60NAVv/wBnugvGdU+Q5fcY88jhtDrWKXILVSybcagTNl9BjYwWjbQBHvt3zLWUS0CO+Ldp6W2S9V0XS2sOFA/4OzmpA5nI4RHXobb6J5dbHZ3LkyLBMay+elmv2PWq9zUh5qeS40UVQ6E/OwvaeX+ZTiKjiFktwur7mKClFyfTikdWdC3pnQhxcIi/WywOc48u9bJPrXLTYfYaOmtNNBZLdBT2h3Pboo6SNrKI8pbuEAajPK5zfR12OI9al0QZrwi4D45wqsluibb7XcsgpOnDsgFsjhq5GySvfov9JwAD+X5R2B6u5W64YNjd3vTLxXY/aq27xxOgZX1FFHJUNjIIcwSFpcGkOcCN60T86nESIiNQhsawzH8Lp5oMesVtsUEz+klitlHHTtkd/tODAAT+EqZREEVafvlv/JH+MrwqRaBviVIR6rR2/g3N2f8AQ/0K7rT0r9UeELIiIuNBERAREQF+JoY6iF8UrGyRPaWvY8ba4HsII9YX7RBSvijfrXG2mtV3o30MYDYWXKlklljaO5pkbIOcAaAJHNoekXEkr+dQ5h9Z2PwM3vldkXXpWJ225Qt1J6hzD6zsfgZvfJ1DmH1nY/Aze+V2RXSsTdHKC6k9Q5h9Z2PwM3vk6hzD6zsfgZvfK7ImlYm6OUF1J6hzD6zsfgZvfJ1DmH1nY/Aze+V2RNKxN0coLqT1DmH1nY/Aze+TqHMPrOx+Bm98rsiaVibo5QXUnqHMPrOx+Bm98nUOYfWdj8DN75XZE0rE3RygupPUOYfWdj8DN75V/DK7LsvpLnO2pstL8CudXbS11JM7nMEro+f+FGg7l3r1bWrLPeCzg60ZOQNfvouw9X0uT5k0rE3Rygu7Oocw+s7H4Gb3ydQ5h9Z2PwM3vldkTSsTdHKC6k9Q5h9Z2PwM3vk6hzD6zsfgZvfK7ImlYm6OUF1J6hzD6zsfgZvfJ1DmH1nY/Aze+V2RNKxN0coLqT1DmH1nY/Aze+TqHMPrOx+Bm98rsiaVibo5QXUnqHMPrOx+Bm98nUOYfWdj8DN75XZE0rE3RygupPUOYfWdj8DN75OoMwP/AMzsg/D8AmP+MrsiaVibo5QXQmO44bM6eqqqn4fc6kNE1TydG0NbvlYxmzytGydbJJJJJU2iLmrrqrnNVtTaIiLAEREBERAREQEREBERAREQEREBERAREQFnvBdxdacn2/n/AHz3Yb2Tr/K5Ozt+buWhLPeCzzJaMnJ9WUXcd5PdVyBBoSIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgLPOCuuqMo1ofvou/yd/S5PnVjz7PbFwwxG4ZPkta63WOgDHVNU2CScxhz2sB5I2ucRzOb3A67zoAlYh5M/lK8Os+vF4xixZFJc75XXm53KCnbb6toNM6d8jXukfEGMHKR2OIOyB3kBB6PREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQfiWVkET5JHtjjYC5z3HQaB3kn1BVR3EaGT06OyXi4U5+RUQQMax49Rb0j2kg+o67V++KDy3DKhv+rLU0kLx87H1MTHD+driP510LuwcOjq+sqi95mOVvdl2XcXnEk9lr7+bp/fJ5xJPZa+/m6f3y7UW3LhcHrKXjc4vOJJ7LX383T++TziSey19/N0/vl2omXC4PWS8bnF5xJPZa+/m6f3yecST2Wvv5un98u1Ey4XB6yXjcr+TZDR5fjtzsd1w++VVtuNPJS1ELo6f0o3tLXD+G7Do9/qXnnyNuA8vk1OyquulhudzvNyqTT0tTTRwno6Fp2wHco0557XDtA5G6JXqZEy4XB6yXjc4vOJJ7LX383T++TziSey19/N0/vl2omXC4PWS8bnF5xJPZa+/m6f3yecST2Wvv5un98u1Ey4XB6yXjc4vOJJ7LX383T++TziSey19/N0/vl2omXC4PWS8bnEOIcm/uXvv5uD3ymrDk1JkAlbCyemqoddLSVcZjljB+SdHsLTo6c0kbBG9ggcSiHOMXEHHy3sMlNWRuI9bf3J2v6WgqThYdcTFMWm0zyi66pXpEReaxEREBERAREQEREBERAREQEREBERBUuKX3Hv/HqH+9wrqXLxS+49/wCPUP8Ae4V1L08P4EeM/Sll2CKq8VM/puFnDy/ZXV076yK10xmFNGdOmfsNYwH1czi0b9W9rO8qzfitw84X5bluRRYi+S32eSupaKghqSYahuiI5XOk1KzXNtzeQ7A0NFYzNmLbkWB5vxozPh3ZMeju8FmkyPKanlt1PQ26tqYrdEyEyzGZsJfLUub2NHRsjBLtnlAJXxs/HvJqrBsvq7my0WKusk1K2mv15tlxt9srY5j28kEzWzmRpDm9G0u5nFmnel2TNA9BIvJWX8csrzfyeuL7I6mltWRYzGxjrnQUlZRtnp5YmyB8UUxZNDJoub6RI7NjmBCvvELjHlXDKjxPHquS1XHL74KiY19HZa+ejpqaEM240sDpZnuJkY3scG95JboAs0DeEWbcEOJF64iWi79e2l9vrLbWfBmVbaCqoqevjLGvbNFFUsbK35RaWnei06JBCi/Ktr6u18Eb1VwUlsuNLHJT/DaK6MmdHPAZmtLWmKWNzX8xY4O5iNNI0d9lvquNdRefM/4z5/aLnxZksVNjYtOAxwVbm3GKofPWxuomVL4wWSNaxw2/T9OB20Fo0XGbxnirl8Oa0VlyiLH44L1js9+t9RQ9NE2kMTog+Goc9x52gTNPStDPku9AJmgbQi838OvKZu1yzGa032Sy32gls1Xd6W443Q1tPETTlnPG11SOWoBa/bZIjrY0QNhfnHuP+asuWAXTJosdp8Uyu01t8bFa4ZpaukggpenEcj3ScrjyvZt7WgczS3l9IOUzQPSSLzNgflMZdlN5xaqksDKqyZBUwxGgorBdY6i3Qzfwc0lZJEKeZrdt5y3lGiS1zgNm4cBuLeS8Trrdm32THra+jM0c+NwMnju1tkbLyxicSO09rmAnna1oJI1vt0iqJG0qHm++BjX+5rP7MamFDzffAxr/AHNZ/ZjW6jt8KvpKwvaIi8lBERAREQEREBERAREQEREBERAREQVLil9x7/x6h/vcK6ly8UvuPf8Aj1D/AHuFdS9PD+BHjP0pZdiAz7CLbxIwy8YxeGvdbbpTup5jE7le0Huc0+pzSAR39oCzi6cFM1yjA8ixXJOJYvNDc7VJbIZOoo4XxOdy6nlIl3K8AEaBY08xJG9a2ZFJiJYs/wCJPCc51R47UW+9TY9kuO1Hwq13iGBs3ROMZjkY+JxAfG9hIc3YPd2qIyDg9kWXYhR0V7zdtbkdvvMF7t92ZaI46enliADIzTB/ps+WTzSc23bDhoa1dFLQMXg8naouFBxIpskyyW9uzmjigrpYqBlMaeWON0TXwgOIDQwx6Y7mO2bLncxX0uXBDJ7xS43c6rPmjOcdmm+AX+nszGROp5Y2MkgnpjKRIHcgcSHtIOiNaWyImWBQY7rlHD+zUdNc6K88SblPJLJNXWaloqNkI2OVhjlqI9DR0NF59E8x7txGX22v48YHfsTrMfvmDNqmQltfdo6SdpLZmP01kFS8k+hrtLR2+vuWqolhlt94H9dw8XI+uuh+P1Kym38E5vgHLRCl5vljpe7n16PzfhX5yvgNS5hW291bdpGUdPi9djE0MMPK+VlS2JrpWv5vQLREdN07fN39nbqiJaBi+O8DMhs+U4vkF2zht5kxy3VFrp6OnssdNG+mkia3sAkcRLzRxEuJLSGcoY3ZKxvgJhF4x/NaG1QYnU1lnuMFTRXypveIS2eSkpnsc4iOc1UkbueTlBigaGHmJHKANezEUywMr4Z8Jcp4cSWq1s4gS3PDbSx0NHaJ7VEKjoQwtiikqeYlzY9t0WsaTyAEkbB/uI8Hr3buJMOZZNl7MlrqO3zWyjbDaY6FzYpZGPd0zmOd0pHRtA7GgbcdbK1NFbQCh5vvgY1/uaz+zGphQ833wMa/3NZ/ZjW2jt8KvpKwvaIi8lBERAREQEREBERAREQEREBERAREQQeaWWbIMaq6OmLBVbjmhEh0x0kcjZGNcdHQLmAE6Ogd6PcqzJm1tpXdHWNrKCoHY+Coo5Q5p9Y2GkO/laSD6iVoSLqwsaKKclUXjbtt9p3Lfezv4/2P6VL4Wb7CfH+x/SpfCzfYWiIt2kYXBPOPxXUzv4/2P6VL4Wb7CfH+x/SpfCzfYWiImkYXBPOPxNTO/j/Y/pUvhZvsJ8f7H9Kl8LN9haIiaRhcE84/E1M7+P8AY/pUvhZvsL5U/EnHatr3QXAzBj3RuMdPK7lc06c06b2EHsIWkrPOCgAtGUa391F37/xuRNIwuCecfian8+P9j+lS+Fm+wnx/sf0qXws32FoiJpGFwTzj8TUzv4/2P6VL4Wb7CfH+x/SpfCzfYWiImkYXBPOPxNTO/j/Y/pUvhZvsJ8f7H9Kl8LN9haIiaRhcE84/E1M7Gf2Mn/OpfCzfYXZYYpMjyajvEUM0NtoYJY45KiJ0Tp5JOT5LXAO5Whp7SBsuGt6KvCLGrpFNpiim0zvm/wBoS8dgiIuFBERAREQEREBERAREQEREBERAREQEREBERAREQEREBZ7wX/0Tk/YB++e7dxB/jcnzLQlnvBYg2jJ9En98927zv+NyfgQaEiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICzzgprqjKNa+6i792/pci0NZ5wVAFoyjXtRd/Xv+NyINDREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBFX84vFTZ7Iw0bxFV1VTBRxyubzdF0kjWl4B7CWtLiAezYG+xVx2BWWU809PLVyn5U1RUySPcfnJLl14eBFVOeubR4X+8Lbe0NFnXm+x/wCrm/nX/aTzfY/9XN/Ov+0tuj4XHPKPyXU0VFnXm+x/6ub+df8AaTzfY/8AVzfzr/tJo+Fxzyj8jUkONGI3jO+FmSWPH7zW2C+1VKTQ3C31L6aaKdhD4wJGkFoc5oa7R+S5wXjH/wAOHG+I2SZbk2T5blOTzWWzzzUTLZX3OodDUXB5JmfJG5xa8sBJPMN80jT3hevvN9j/ANXN/Ov+0vnT8NMZpQ8QWiKEPe6Rwjc5vM4nZcdHtJPeU0fC455R+RqaSizrzfY/9XN/Ov8AtJ5vsf8Aq5v51/2k0fC455R+RqaKizrzfY/9XN/Ov+0nm+x/6ub+df8AaTR8LjnlH5GpoqLOvN9j/wBXN/Ov+0ui2bxPJLRRUkkptt0kkp3U0srpBFI2J8rXsLiS0ERvBb3fJI1o7xno9MxOSq8xr1xbZ/mS0di+oiLhYiIiAiIgIiICIiAiIgIiICIiAiIgIiICIiCo8Sv9F2n8rUf6ULrXJxK/0XafytR/pQutenR8Gnxn7L2CLjvF4ocetVXc7nVw0FvpInTT1VQ8MjiY0bLnOPYAAqBSeUdw8rLBdb2y/SRWu1shkq6mot1VCGMleI43gPiBe1ziBzNBHr3obUvEI0tFQZ+O+EU2OC+zXaaG2vqvgULpLdUtkqpeQPAgiMfPOC08wdG1zSNkHsK/ruOuDjDzlDb4JbOKr4C58NLPJM2o/wDsugawyh+u3lLN67dJeBfUWQ535SmNY5winzyxyOyOibWxW9kUMM7XNmdK1j2ytEZfEWAl2ntbshre97d2W68bMPseO2q9XC41NJS3R72UUEttqm1k5YSH8tL0fTdmtk8nYCD3EEy8C8oobEcxsueWOG8WC4RXK3Sucxs0WxpzTpzXNIBa4EEFrgCD3hfLNM6sXDyzi6ZBcG2+jdMynjPRvkklld8mOONgL3vOjprQT2Hs7Fb9onkWPXzyjscrsXp7njF5pyZr5T2UVF0tVf8AB+mc+LpIjyRczH8knKC/TRJpriNEL5Y9xyqcz4/XjCLSIKa02FgZXOrLXWfCKqblfzCKXTYomtcGaL+bpASWbGnKZoGzKEu/3W4Z+UZv7lUqbUJd/utwz8ozf3KpW6j+rwq/1lYX5EReQgiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgqPEr/AEXafytR/pQutcnEr/Rdp/K1H+lC616dHwafGfsvYzHyk8QuuccHrxbLLRi517ZqSsFuc8NFYyCpimfBs9nptjc0b7CSAVROMuX1PGXgllNoteD5bTVGqBxp7rZZIHS/5bCXxsYdueWta4uLQW67dleiUWMxdGBeUZiNfU53w/yo2/IrtjtpZXUlwgxSpnhuFP07Y+jnjbA9sj2gxlrmtO9O3oqAutmnsGG0Nzw+z59ZrTfMga/J5ZPhNVf5KZkLmMkY2R8kzWucyFpLdSBncAvTiKZe0ePKXBcgqeDXGe1W/GslbPVZFSXy20t6EklXW0zfgchIkkc4ySEU0m2lxeDytcATpWPiRFPlfEbFOIcuO567FDaaqzz0tmZW2+60M5mZI2V8ELmTOieGlpABG2tJHYF6gRMooXBbGrNj+ISz2a03yzR3atmuFRT5HPLLXPmcQwySGV73AubG12id6I2ASQqp5TtLdaW14bkdhoK25Xmw36Oqgp6a3y10Za6GWOQyxwgyBvK4gOY1xDizs0SRoOWcMMQzypgqckxi032ogYY4pbjRxzujaTsgFwOhtQdX5PXDiqs81rjxC226jmnjqXttTDQvMsYeGP54CxwLRI8Ag/6x+dJibWGE0tB8Z+FOQ9V/D7vlVZnttvF9trLPU0clDI6qpHFop5W9II2wRsdznvHM4kdw2bh5ZrhRccuLdfUUNTT0Nc60/BaqWFzYqjkpS1/RuI07lPYdb0ewq34Pw6x3hvbp6LHba23w1EvTzvMj5pZ5NAc0kkjnPedADbiewKyJFIKEu/3W4Z+UZv7lUqbUJd/utwz8ozf3KpW+j+rwq/1lYX5EReQgiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgqXEsctlt8x7I4bpRvkd6mt6Zrdn8GyF1KfqaeKsp5YJ4mTQStLJIpGhzXtI0QQe8EepVV3DinaeWnvd6pIR8mJlWHtaPmBe1zv6Su7CxaMkUVzayutFxebke0d98RH7tPNyPaO++Ij92tubC4/SVtG92ouLzcj2jvviI/dp5uR7R33xEfu0zYXH6SWje7UXF5uR7R33xEfu1VOHNgrMpoL1NXZFdw+kvVfQRdDPGB0UM7o2b9A+lytG/wAPqCZsLj9JLRvXhFxebke0d98RH7tPNyPaO++Ij92mbC4/SS0b3ai4vNyPaO++Ij92nm5HtHffER+7TNhcfpJaN7tULcm9NmOIRs7Xx1k87mjvDBSzMLv5OaRg/wCILt83I9o774iP3amLFi1HYJJZonz1VXKA19VVymSQtBJDQT3N2SdAAdqTi4dETNM3m0xs3xY1QmERF5jEREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQFnvBbQtGT8p3++i7f0/C5PwlaEs94Lb6pyffL9092+Tr6XJ83/fzoNCREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBZ5wUAFoyjTg799F37vxuRaGs74Ka6oyjR3++i7+r/APbkQaIiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIua43GltFFNWVs8dNSwt5nyyu01oWY3fjs3pC2yWWSsi9VTXSmma78IZyuf/AP0GldeB0TH6T8Km/wBOcrZZeMWe1nC/hnkGV0FjkySe00/wp1tin6F0kYc3pHB/K7XIznf3HfLrs3teZfIx8rir4z5rd8XoMGkoaOSqrr5WXR9yD20rJpXPYzkEI53F72t7x2czvVo65UcZ75VwSwTWS0ywStLHxyTSOa5pGiCC3tBCyryfsci8nWhyCmx61UM7rxXuq5Jqid/OyMb6KAab2tYC7R7yXErv/wCH6Zwxzgs9iosYg453mJwNRYaKdnrENY5jv5tsIP8ASFfcP4j2jMnOp4HSUdxY3nfQVYDZeUHRc3RIe3u7Wk62N6J0ubH/AIf0no9OeujV8rT9Cy1IiLzkEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBEXLdJ301sq5ohuWOF72j8IaSFYi82GC8QstkzHIJo2vJtFvndFSxg+jJI30Xykes83M1vzAbGuYqurisjQ2zUAB2OgZ2/P6I7VmnH/iVd8FpMettibKy53ysNOKqCiNZLBG1vM90cI/hH9o0D2d6/TbYfQsHLGyn9+spO1rCLzBcuMHEPH+GueVVULmJrS2jmtd+uth6vfMJJ2MljdC5vISASNt9R32HWrnHm+VcOeIFRZ8kvTMqoJcfqLyxzKJlLJDJCfSjbyd7SN65tneu359UdNomY1T2btV5mN/y7Lo1y75BbbAaIXGtiozW1LKOmErtGWZ/yWN+cnR/oKkA6SOSOaCaSmqYnc8U8R0+N3qc0/zn8BBIOwSF5SvFVmWUUXCPLMhyKnqqC75VQTwWSloWRspOYvLCJt87tNBBDvW78Hb6tWzBxuvmq9No1bfnBsb/AMO8uOZ43HVytZHXQPNNVxs+S2VoBJHzBzS14HqDwrOsd4CyvF4ymDm3CI6Obl+Z7uma4/0MZ/QtiXwXT8Gno/Sa8OnZ7xf7s5ERF56CIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiIC/jmhwIIBB7CD61/UQeYLpYpcTvNZZJg4GkduBzjsyU7ieif+HsBaf/yY4epVDiBw5tPEi109Hc3VNPLSztqqStoZjDUU0o7nxvHcf5QR/QF6qzbBqLNaGNkz3UtbBs01bE0F8ZPe0g/KY7Q2316BBBAIxi8YDlFhlc2azyXGEd1TbHCVrv8AgJDwfwaIHzn1/e9E/iGD0vDyY0xFXbE9vzgmL64ZBWcErddOH94xG5X/ACC60l1lZLPW19a2aqBa5jgGuczlaP3NvZy/P6+1Tty4e2u7ZrR5NUunkrKagltwpyWmB8Uh27maW7J9Xfr8Ctht12Hfj18/qyf7KdX3b2evn9Vz/YXoxTgRu7O3ds+qZZ3Mdtvky47aa20SU98yT4DaLjHcqG1SXASUkEjHFwa1jmH0dk+vm7T2jZ3rj3tjaXOIa0DZJOgAuynsN/rXhlPjd3ke7uElKYB//UnKB/OVf8M4OzvqIq7JhCY2EPZa4j0jSfUZXdztf7A7PnLh2Lmr6R0ToVEzEx4Rt/fotp7U1wVx2W1Y5UXKpjdFUXWXp2sf3shDQ2MH+UAv/wCPXqWhIi+Cx8arpGLVi1bZWRERc6CIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiIP/2Q==", "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "display(\n", " IPImage(\n", " app.get_graph().draw_mermaid_png(\n", " draw_method=MermaidDrawMethod.API,\n", " )\n", " )\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Run Workflow Function\n", "\n", "Define a function to run the workflow and display results." ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [], "source": [ "async def run_workflow(query: str):\n", " \"\"\"Run the LangGraph workflow and display results.\"\"\"\n", " initial_state = {\n", " \"messages\": [],\n", " \"query\": query,\n", " \"plot\": \"\",\n", " \"character_description\": \"\",\n", " \"image_prompts\": [],\n", " \"image_urls\": [],\n", " \"gif_data\": None\n", " }\n", "\n", " try:\n", " result = await app.ainvoke(initial_state)\n", "\n", " print(\"Character/Scene Description:\")\n", " print(result[\"character_description\"])\n", "\n", " print(\"\\nGenerated Plot:\")\n", " print(result[\"plot\"])\n", "\n", " print(\"\\nImage Prompts:\")\n", " for i, prompt in enumerate(result[\"image_prompts\"], 1):\n", " print(f\"{i}. {prompt}\")\n", "\n", " print(\"\\nGenerated Image URLs:\")\n", " for i, url in enumerate(result[\"image_urls\"], 1):\n", " print(f\"{i}. {url}\")\n", "\n", " if result[\"gif_data\"]:\n", " print(\"\\nGIF generated successfully. Use the next cell to display or save it.\")\n", " else:\n", " print(\"\\nFailed to generate GIF.\")\n", " \n", " return result\n", " except Exception as e:\n", " print(f\"An error occurred: {str(e)}\")\n", " return None" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Execute Workflow\n", "\n", "Run the workflow with a sample query." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "query = \"A cat wearing a top hat and monocle, sitting at a desk and writing a letter with a quill pen.\"\n", "result = await run_workflow(query)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Display and Save GIF\n", "\n", "Display the generated GIF and provide an option to save it." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "if result and result[\"gif_data\"]:\n", " # Display the GIF\n", " display(IPImage(data=result[\"gif_data\"], format='gif'))\n", " \n", " # Ask if the user wants to save the GIF\n", " save_gif = input(\"Do you want to save the GIF? (yes/no): \").lower().strip()\n", " if save_gif == 'yes':\n", " filename = input(\"Enter the filename to save the GIF (e.g., output.gif): \").strip()\n", " if not filename.endswith('.gif'):\n", " filename += '.gif'\n", " with open(filename, 'wb') as f:\n", " f.write(result[\"gif_data\"])\n", " print(f\"GIF saved as {filename}\")\n", " else:\n", " print(\"GIF not saved.\")\n", "else:\n", " print(\"No GIF data available to display or save.\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "![Cat_GIF_agent](../images/langgraph_agent_cat_animation.gif)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "![](https://europe-west1-genai-agents-views-tracker.cloudfunctions.net/genai-agents-tracker?notebook=all-agents-tutorials--gif-animation-generator-langgraph)" ] } ], "metadata": { "kernelspec": { "display_name": ".venv", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.0" } }, "nbformat": 4, "nbformat_minor": 2 }