diff --git a/.gitattributes b/.gitattributes
index a6344aac8c09253b3b630fb776ae94478aa0275b..44a82c40f8139faa925d995356e021db0f7fc114 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -33,3 +33,9 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
*.zip filter=lfs diff=lfs merge=lfs -text
*.zst filter=lfs diff=lfs merge=lfs -text
*tfevents* filter=lfs diff=lfs merge=lfs -text
+external_models/depth-fm/assets/dog.png filter=lfs diff=lfs merge=lfs -text
+external_models/depth-fm/assets/figures/dfm-cover.png filter=lfs diff=lfs merge=lfs -text
+external_models/depth-fm/assets/figures/radio.png filter=lfs diff=lfs merge=lfs -text
+external_models/TangoFlux/assets/tangoflux.png filter=lfs diff=lfs merge=lfs -text
+external_models/TangoFlux/assets/tf_opener.png filter=lfs diff=lfs merge=lfs -text
+external_models/TangoFlux/assets/tf_teaser.png filter=lfs diff=lfs merge=lfs -text
diff --git a/DepthEstimator.py b/DepthEstimator.py
new file mode 100644
index 0000000000000000000000000000000000000000..69f58725c2512e0389433e73b8c5177dbc5cfa3c
--- /dev/null
+++ b/DepthEstimator.py
@@ -0,0 +1,67 @@
+import torch
+from accelerate.test_utils.testing import get_backend
+from PIL import Image
+import os
+import sys
+from config import LOGS_DIR, DEPTH_FM_CHECKPOINT, DEPTH_FM_DIR
+sys.path.append(DEPTH_FM_DIR + '/depthfm')
+from dfm import DepthFM
+from unet import UNetModel
+import einops
+import numpy as np
+from torchvision import transforms
+
+
+class DepthEstimator:
+ def __init__(self, image_dir = LOGS_DIR):
+ self.device,_,_ = get_backend()
+ self.image_dir = image_dir
+ self.model = None
+
+ def _load_model(self):
+ if self.model is None:
+ self.model = DepthFM(DEPTH_FM_CHECKPOINT).to(self.device).eval()
+ else:
+ self.model = self.model.to(self.device).eval()
+
+ def _unload_model(self):
+ if self.model is not None:
+ self.model = self.model.to("cpu")
+ torch.cuda.empty_cache()
+
+
+ def estimate_depth(self, image_path : str) -> list:
+ print("Estimating depth...")
+ predictions_list = []
+ self._load_model()
+ for img in os.listdir(image_path):
+ if img.endswith(".jpg") or img.endswith(".jpeg") or img.endswith(".png"):
+ image = Image.open(os.path.join(image_path, img))
+ x = np.array(image)
+ x = einops.rearrange(x, 'h w c -> c h w')
+ x = x / 127.5 - 1
+ x = torch.tensor(x, dtype=torch.float32)[None]
+ with torch.no_grad():
+ depth = self.model.predict_depth(x.to(self.device), num_steps=2, ensemble_size=4) # returns a tensor
+ depth.cpu()
+ to_pil = transforms.ToPILImage()
+ PIL_image = to_pil(depth.squeeze())
+ predictions_list.append({"depth": PIL_image})
+ del x, depth
+ torch.cuda.empty_cache()
+ self._unload_model()
+ print("Depth estimation complete.")
+ return predictions_list
+
+ def visualize(self, predictions_list : list) -> None:
+ for (i, prediction) in enumerate(predictions_list):
+ prediction["depth"].save(f"depth_{i}.png")
+
+
+# Estimator = DepthEstimator()
+# predictions = Estimator.estimate_depth(Estimator.image_dir)
+# Estimator.visualize(predictions)
+
+
+
+
diff --git a/GenerateAudio.py b/GenerateAudio.py
new file mode 100644
index 0000000000000000000000000000000000000000..5c9babc47dfdcbbcbbd1d4ab739e4ec9d0606d4b
--- /dev/null
+++ b/GenerateAudio.py
@@ -0,0 +1,184 @@
+import torchaudio
+import sys
+import torch
+import random
+from config import TANGO_FLUX_DIR
+sys.path.append(TANGO_FLUX_DIR)
+from tangoflux import TangoFluxInference
+from transformers import AutoTokenizer, T5EncoderModel
+from collections import Counter
+
+class GenerateAudio():
+ def __init__(self):
+ self.device = "cuda"
+ self.model = None
+ self.text_encoder = None
+
+ # Basic categories for object classification
+ self.categories = {
+ 'vehicle': ['car', 'bus', 'truck', 'motorcycle', 'bicycle', 'train', 'vehicle'],
+ 'nature': ['tree', 'bird', 'water', 'river', 'lake', 'ocean', 'rain', 'wind', 'forest'],
+ 'urban': ['traffic', 'building', 'street', 'signal', 'construction'],
+ 'animal': ['dog', 'cat', 'bird', 'insect', 'frog', 'squirrel'],
+ 'human': ['person', 'people', 'crowd', 'child', 'footstep', 'voice'],
+ 'indoor': ['door', 'window', 'chair', 'table', 'fan', 'appliance', 'tv', 'radio']
+ }
+
+ # Suffixes and prefixes for pattern matching
+ self.suffixes = {
+ 'tree': 'nature',
+ 'bird': 'animal',
+ 'car': 'vehicle',
+ 'truck': 'vehicle',
+ 'signal': 'urban'
+ }
+
+ def _load_model(self):
+ if self.model is None:
+ self.model = TangoFluxInference(name='declare-lab/TangoFlux')
+ if self.text_encoder is None:
+ self.text_encoder = T5EncoderModel.from_pretrained("google/flan-t5-large").to(self.device).eval()
+ else:
+ self.text_encoder = self.text_encoder.to(self.device)
+
+ def generate_sound(self, prompt, steps=25, duration=10, guidance_scale=4.5, disable_progress=True):
+ self._load_model()
+ with torch.no_grad():
+ latents = self.model.model.inference_flow(
+ prompt,
+ duration=duration,
+ num_inference_steps=steps,
+ guidance_scale=guidance_scale,
+ disable_progress=disable_progress
+ )
+ wave = self.model.vae.decode(latents.transpose(2, 1)).sample.cpu()[0]
+ waveform_end = int(duration * self.model.vae.config.sampling_rate)
+ wave = wave[:, :waveform_end]
+
+ return wave
+
+ def _categorize_object(self, object_name):
+ """Categorize an object based on keywords or patterns"""
+ object_lower = object_name.lower()
+
+ # Check if the object contains any category keywords
+ for category, keywords in self.categories.items():
+ for keyword in keywords:
+ if keyword in object_lower:
+ return category
+
+ # Check suffix/prefix patterns
+ words = object_lower.split()
+ for word in words:
+ for suffix, category in self.suffixes.items():
+ if word.endswith(suffix):
+ return category
+
+ return "unknown"
+
+ def _describe_object_sound(self, object_name, zone):
+ """Generate an appropriate sound description based on object type and distance"""
+ category = self._categorize_object(object_name)
+
+ # Volume descriptor based on zone
+ volume_descriptors = {
+ "near": ["prominent", "clear", "loud", "distinct"],
+ "medium": ["moderate", "audible", "present"],
+ "far": ["subtle", "distant", "faint", "soft"]
+ }
+
+ volume = random.choice(volume_descriptors[zone])
+
+ # Sound descriptors based on category
+ sound_templates = {
+ "vehicle": [
+ "{volume} engine sounds from the {object}",
+ "{volume} mechanical noise of the {object}",
+ "the {object} creating {volume} road noise",
+ "{volume} sounds of the {object} in motion"
+ ],
+ "nature": [
+ "{volume} rustling of the {object}",
+ "the {object} making {volume} natural sounds",
+ "{volume} environmental sounds from the {object}",
+ "the {object} with {volume} movement in the wind"
+ ],
+ "urban": [
+ "{volume} urban sounds around the {object}",
+ "the {object} with {volume} city ambience",
+ "{volume} noise from the {object}",
+ "the {object} contributing to {volume} street sounds"
+ ],
+ "animal": [
+ "{volume} calls from the {object}",
+ "the {object} making {volume} animal sounds",
+ "{volume} sounds of the {object}",
+ "the {object} with its {volume} presence"
+ ],
+ "human": [
+ "{volume} voices from the {object}",
+ "the {object} creating {volume} human sounds",
+ "{volume} movement sounds from the {object}",
+ "the {object} with {volume} activity"
+ ],
+ "indoor": [
+ "{volume} ambient sounds around the {object}",
+ "the {object} making {volume} indoor noises",
+ "{volume} mechanical sounds from the {object}",
+ "the {object} with its {volume} presence"
+ ],
+ "unknown": [
+ "{volume} sounds from the {object}",
+ "the {object} creating {volume} audio",
+ "{volume} noises associated with the {object}",
+ "the {object} with its {volume} acoustic presence"
+ ]
+ }
+
+ # Select a template for this category
+ templates = sound_templates.get(category, sound_templates["unknown"])
+ template = random.choice(templates)
+
+ # Fill in the template
+ description = template.format(volume=volume, object=object_name)
+ return description
+
+ def create_audio_prompt(self, object_depths):
+ if not object_depths:
+ return "Environmental ambient sounds."
+
+ for obj in object_depths:
+ if obj.get("sound_description") and len(obj["sound_description"]) > 5:
+ return obj["sound_description"]
+ return f"Sounds of {object_depths[0]['original_label']}."
+
+ def process_and_generate_audio(self, object_depths, output_path=None, duration=10, steps=25, guidance_scale=4.5):
+ self._load_model()
+
+ if not object_depths:
+ prompt = "Environmental ambient sounds."
+ else:
+ # Sort objects by depth to prioritize closer objects
+ sorted_objects = sorted(object_depths, key=lambda x: x["mean_depth"])
+ prompt = self.create_audio_prompt(sorted_objects)
+
+ print(f"Generated audio prompt: {prompt}")
+
+ wave = self.generate_sound(
+ prompt,
+ steps=steps,
+ duration=duration,
+ guidance_scale=guidance_scale
+ )
+
+ sample_rate = self.model.vae.config.sampling_rate
+
+ if output_path:
+ torchaudio.save(
+ output_path,
+ wave.unsqueeze(0),
+ sample_rate
+ )
+ print(f"Audio saved to: {output_path}")
+
+ return wave, sample_rate
\ No newline at end of file
diff --git a/GenerateCaptions.py b/GenerateCaptions.py
new file mode 100644
index 0000000000000000000000000000000000000000..15716d3c0f91bf09fecd8de29199d39229431861
--- /dev/null
+++ b/GenerateCaptions.py
@@ -0,0 +1,494 @@
+#!/usr/bin/env python3
+"""
+streetsoundtext.py - A pipeline that downloads Google Street View panoramas,
+extracts perspective views, and analyzes them for sound information.
+"""
+
+import os
+import requests
+import argparse
+import numpy as np
+import torch
+import time
+from PIL import Image
+from io import BytesIO
+from config import LOGS_DIR
+import torchvision.transforms as T
+from torchvision.transforms.functional import InterpolationMode
+from transformers import AutoModel, AutoTokenizer
+from utils import sample_perspective_img
+import cv2
+
+log_dir = LOGS_DIR
+os.makedirs(log_dir, exist_ok=True) # Creates the directory if it doesn't exist
+
+# soundscape_query = "\nWhat can we expect to hear from the location captured in this image? Name the around five nouns. Avoid speculation and provide a concise response including sound sources visible in the image."
+soundscape_query = """
+Identify 5 potential sound sources visible in this image. For each source, provide both the noun and a brief description of its typical sound.
+
+Format your response exactly like these examples (do not include the word "Noun:" in your response):
+Car: engine humming with occasional honking.
+River: gentle flowing water with subtle splashing sounds.
+Trees: rustling leaves moved by the wind.
+"""
+# Constants
+IMAGENET_MEAN = (0.485, 0.456, 0.406)
+IMAGENET_STD = (0.229, 0.224, 0.225)
+
+# Model Leaderboard Paths
+MODEL_LEADERBOARD = {
+ "intern_2_5-8B": "OpenGVLab/InternVL2_5-8B-MPO",
+ "intern_2_5-4B": "OpenGVLab/InternVL2_5-4B-MPO",
+}
+
+class StreetViewDownloader:
+ """Downloads panoramic images from Google Street View"""
+
+ def __init__(self):
+ # URLs for API requests
+ # https://www.google.ca/maps/rpc/photo/listentityphotos?authuser=0&hl=en&gl=us&pb=!1e3!5m45!2m2!1i203!2i100!3m3!2i4!3sCAEIBAgFCAYgAQ!5b1!7m33!1m3!1e1!2b0!3e3!1m3!1e2!2b1!3e2!1m3!1e2!2b0!3e3!1m3!1e8!2b0!3e3!1m3!1e10!2b0!3e3!1m3!1e10!2b1!3e2!1m3!1e10!2b0!3e4!1m3!1e9!2b1!3e2!2b1!8m0!9b0!11m1!4b1!6m3!1sI63QZ8b4BcSli-gPvPHf-Qc!7e81!15i11021!9m2!2d-90.30324219145255!3d38.636242944711036!10d91.37627840655999
+ #self.panoid_req = 'https://www.google.com/maps/preview/reveal?authuser=0&hl=en&gl=us&pb=!2m9!1m3!1d82597.14038230096!2d{}!3d{}!2m0!3m2!1i1523!2i1272!4f13.1!3m2!2d{}!3d{}!4m2!1syPETZOjwLvCIptQPiJum-AQ!7e81!5m5!2m4!1i96!2i64!3i1!4i8'
+ self.panoid_req = 'https://www.google.ca/maps/rpc/photo/listentityphotos?authuser=0&hl=en&gl=us&pb=!1e3!5m45!2m2!1i203!2i100!3m3!2i4!3sCAEIBAgFCAYgAQ!5b1!7m33!1m3!1e1!2b0!3e3!1m3!1e2!2b1!3e2!1m3!1e2!2b0!3e3!1m3!1e8!2b0!3e3!1m3!1e10!2b0!3e3!1m3!1e10!2b1!3e2!1m3!1e10!2b0!3e4!1m3!1e9!2b1!3e2!2b1!8m0!9b0!11m1!4b1!6m3!1sI63QZ8b4BcSli-gPvPHf-Qc!7e81!15i11021!9m2!2d{}!3d{}!10d25'
+ # https://www.google.com/maps/photometa/v1?authuser=0&hl=en&gl=us&pb=!1m4!1smaps_sv.tactile!11m2!2m1!1b1!2m2!1sen!2sus!3m3!1m2!1e2!2s{}!4m61!1e1!1e2!1e3!1e4!1e5!1e6!1e8!1e12!1e17!2m1!1e1!4m1!1i48!5m1!1e1!5m1!1e2!6m1!1e1!6m1!1e2!9m36!1m3!1e2!2b1!3e2!1m3!1e2!2b0!3e3!1m3!1e3!2b1!3e2!1m3!1e3!2b0!3e3!1m3!1e8!2b0!3e3!1m3!1e1!2b0!3e3!1m3!1e4!2b0!3e3!1m3!1e10!2b1!3e2!1m3!1e10!2b0!3e3!11m2!3m1!4b1 # vmSzE7zkK2eETwAP_r8UdQ
+ # https://www.google.ca/maps/photometa/v1?authuser=0&hl=en&gl=us&pb=!1m4!1smaps_sv.tactile!11m2!2m1!1b1!2m2!1sen!2sus!3m3!1m2!1e2!2s{}!4m61!1e1!1e2!1e3!1e4!1e5!1e6!1e8!1e12!1e17!2m1!1e1!4m1!1i48!5m1!1e1!5m1!1e2!6m1!1e1!6m1!1e2!9m36!1m3!1e2!2b1!3e2!1m3!1e2!2b0!3e3!1m3!1e3!2b1!3e2!1m3!1e3!2b0!3e3!1m3!1e8!2b0!3e3!1m3!1e1!2b0!3e3!1m3!1e4!2b0!3e3!1m3!1e10!2b1!3e2!1m3!1e10!2b0!3e3!11m2!3m1!4b1 # -9HfuNFUDOw_IP5SA5IspA
+ self.photometa_req = 'https://www.google.com/maps/photometa/v1?authuser=0&hl=en&gl=us&pb=!1m4!1smaps_sv.tactile!11m2!2m1!1b1!2m2!1sen!2sus!3m5!1m2!1e2!2s{}!2m1!5s0x87d8b49f53fc92e9:0x6ecb6e520c6f4d9f!4m57!1e1!1e2!1e3!1e4!1e5!1e6!1e8!1e12!2m1!1e1!4m1!1i48!5m1!1e1!5m1!1e2!6m1!1e1!6m1!1e2!9m36!1m3!1e2!2b1!3e2!1m3!1e2!2b0!3e3!1m3!1e3!2b1!3e2!1m3!1e3!2b0!3e3!1m3!1e8!2b0!3e3!1m3!1e1!2b0!3e3!1m3!1e4!2b0!3e3!1m3!1e10!2b1!3e2!1m3!1e10!2b0!3e3'
+ self.panimg_req = 'https://streetviewpixels-pa.googleapis.com/v1/tile?cb_client=maps_sv.tactile&panoid={}&x={}&y={}&zoom={}'
+ def get_image_id(self, lat, lon):
+ """Get Street View panorama ID for given coordinates"""
+ null = None
+ pr_response = requests.get(self.panoid_req.format(lon, lat, lon, lat))
+ if pr_response.status_code != 200:
+ error_message = f"Error fetching panorama ID: HTTP {pr_response.status_code}"
+ if pr_response.status_code == 400:
+ error_message += " - Bad request. Check coordinates format."
+ elif pr_response.status_code == 401 or pr_response.status_code == 403:
+ error_message += " - Authentication error. Check API key and permissions."
+ elif pr_response.status_code == 404:
+ error_message += " - No panorama found at these coordinates."
+ elif pr_response.status_code == 429:
+ error_message += " - Rate limit exceeded. Try again later."
+ elif pr_response.status_code >= 500:
+ error_message += " - Server error. Try again later."
+ return None
+
+ pr = BytesIO(pr_response.content).getvalue().decode('utf-8')
+ pr = eval(pr[pr.index('\n'):])
+ try:
+ panoid = pr[0][0][0]
+ except:
+ return None
+
+ return panoid
+
+ def download_image(self, lat, lon, zoom=1):
+ """Download Street View panorama and metadata"""
+ null = None
+ panoid = self.get_image_id(lat, lon)
+ if panoid is None:
+ raise ValueError(f"get_image_id failed() at coordinates: {lat}, {lon}")
+
+ # Get metadata
+ pm_response = requests.get(self.photometa_req.format(panoid))
+ pm = BytesIO(pm_response.content).getvalue().decode('utf-8')
+ pm = eval(pm[pm.index('\n'):])
+ pan_list = pm[1][0][5][0][3][0]
+
+ # Extract relevant info
+ pid = pan_list[0][0][1]
+ plat = pan_list[0][2][0][2]
+ plon = pan_list[0][2][0][3]
+ p_orient = pan_list[0][2][2][0]
+
+ # Download image tiles and assemble panorama
+ img_part_inds = [(x, y) for x in range(2**zoom) for y in range(2**(zoom-1))]
+ img = np.zeros((512*(2**(zoom-1)), 512*(2**zoom), 3), dtype=np.uint8)
+
+ for x, y in img_part_inds:
+ sub_img_response = requests.get(self.panimg_req.format(pid, x, y, zoom))
+ sub_img = np.array(Image.open(BytesIO(sub_img_response.content)))
+ img[512*y:512*(y+1), 512*x:512*(x+1)] = sub_img
+
+ if (img[-1] == 0).all():
+ # raise ValueError("Failed to download complete panorama")
+ print("Failed to download complete panorama")
+
+ return img, pid, plat, plon, p_orient
+
+
+class PerspectiveExtractor:
+ """Extracts perspective views from panoramic images"""
+
+ def __init__(self, output_shape=(256, 256), fov=(90, 90)):
+ self.output_shape = output_shape
+ self.fov = fov
+
+ def extract_views(self, pano_img, face_size=512):
+ """Extract front, back, left, and right views based on orientation"""
+ # orientations = {
+ # "front": (0, p_orient, 0), # Align front with real orientation
+ # "back": (0, p_orient + 180, 0), # Behind
+ # "left": (0, p_orient - 90, 0), # Left side
+ # "right": (0, p_orient + 90, 0), # Right side
+ # }
+
+ # cutouts = {}
+ # for view, rot in orientations.items():
+ # cutout, fov, applied_rot = sample_perspective_img(
+ # pano_img, self.output_shape, fov=self.fov, rot=rot
+ # )
+ # cutouts[view] = cutout
+
+ # return cutouts
+ """
+ Convert ERP panorama to four cubic faces: Front, Left, Back, Right.
+ Args:
+ erp_img (numpy.ndarray): The input equirectangular image.
+ face_size (int): The size of each cubic face.
+ Returns:
+ dict: A dictionary with the four cube faces.
+ """
+ # Get ERP dimensions
+ h_erp, w_erp, _ = pano_img.shape
+ # Define cube face directions (yaw, pitch, roll)
+ cube_faces = {
+ "front": (0, 0),
+ "left": (90, 0),
+ "back": (180, 0),
+ "right": (-90, 0),
+ }
+ # Output faces
+ faces = {}
+ # Generate each face
+ for face_name, (yaw, pitch) in cube_faces.items():
+ # Create a perspective transformation matrix
+ fov = 90 # Field of view
+ K = np.array([
+ [face_size / (2 * np.tan(np.radians(fov / 2))), 0, face_size / 2],
+ [0, face_size / (2 * np.tan(np.radians(fov / 2))), face_size / 2],
+ [0, 0, 1]
+ ])
+ # Generate 3D world coordinates for the cube face
+ x, y = np.meshgrid(np.linspace(-1, 1, face_size), np.linspace(-1, 1, face_size))
+ z = np.ones_like(x)
+ # Normalize 3D points
+ points_3d = np.stack((x, y, z), axis=-1) # Shape: (H, W, 3)
+ points_3d /= np.linalg.norm(points_3d, axis=-1, keepdims=True)
+ # Apply rotation to align with the cube face
+ yaw_rad, pitch_rad = np.radians(yaw), np.radians(pitch)
+ Ry = np.array([[np.cos(yaw_rad), 0, np.sin(yaw_rad)], [0, 1, 0], [-np.sin(yaw_rad), 0, np.cos(yaw_rad)]])
+ Rx = np.array([[1, 0, 0], [0, np.cos(pitch_rad), -np.sin(pitch_rad)], [0, np.sin(pitch_rad), np.cos(pitch_rad)]])
+ R = Ry @ Rx
+ # Rotate points
+ points_3d_rot = np.einsum('ij,hwj->hwi', R, points_3d)
+ # Convert 3D to spherical coordinates
+ lon = np.arctan2(points_3d_rot[..., 0], points_3d_rot[..., 2])
+ lat = np.arcsin(points_3d_rot[..., 1])
+ # Map spherical coordinates to ERP image coordinates
+ x_erp = (w_erp * (lon / (2 * np.pi) + 0.5)).astype(np.float32)
+ y_erp = (h_erp * (0.5 - lat / np.pi)).astype(np.float32)
+ # Sample pixels from ERP image
+ face_img = cv2.remap(pano_img, x_erp, y_erp, interpolation=cv2.INTER_LINEAR, borderMode=cv2.BORDER_WRAP)
+ cv2.rotate(face_img, cv2.ROTATE_180, face_img)
+ faces[face_name] = face_img
+ return faces
+
+
+class ImageAnalyzer:
+ """Analyzes images using Vision-Language Models"""
+
+ def __init__(self, model_name="intern_2_5-4B", use_cuda=True):
+ self.model_name = model_name
+ self.use_cuda = use_cuda and torch.cuda.is_available()
+ self.model, self.tokenizer, self.device = self._load_model()
+
+ def _load_model(self):
+ """Load selected Vision-Language Model"""
+ if self.model_name not in MODEL_LEADERBOARD:
+ raise ValueError(f"Model '{self.model_name}' not found. Choose from: {list(MODEL_LEADERBOARD.keys())}")
+
+ model_path = MODEL_LEADERBOARD[self.model_name]
+
+ # Configure device and parameters
+ if self.use_cuda:
+ device = torch.device("cuda")
+ torch_dtype = torch.bfloat16
+ use_flash_attn = True
+ else:
+ device = torch.device("cpu")
+ torch_dtype = torch.float32
+ use_flash_attn = False
+
+ # Load model and tokenizer
+ model = AutoModel.from_pretrained(
+ model_path,
+ torch_dtype=torch_dtype,
+ load_in_8bit=False,
+ low_cpu_mem_usage=True,
+ use_flash_attn=use_flash_attn,
+ trust_remote_code=True,
+ ).eval().to(device)
+
+ tokenizer = AutoTokenizer.from_pretrained(
+ model_path,
+ trust_remote_code=True,
+ use_fast=False
+ )
+
+ return model, tokenizer, device
+
+ def _build_transform(self, input_size=448):
+ """Create image transformation pipeline"""
+ transform = T.Compose([
+ T.Lambda(lambda img: img.convert('RGB') if img.mode != 'RGB' else img),
+ T.Resize((input_size, input_size), interpolation=InterpolationMode.BICUBIC),
+ T.ToTensor(),
+ T.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD)
+ ])
+ return transform
+
+ def _find_closest_aspect_ratio(self, aspect_ratio, target_ratios, width, height, image_size):
+ """Find closest aspect ratio for image tiling"""
+ best_ratio_diff = float('inf')
+ best_ratio = (1, 1)
+ area = width * height
+ for ratio in target_ratios:
+ target_aspect_ratio = ratio[0] / ratio[1]
+ ratio_diff = abs(aspect_ratio - target_aspect_ratio)
+ if ratio_diff < best_ratio_diff:
+ best_ratio_diff = ratio_diff
+ best_ratio = ratio
+ elif ratio_diff == best_ratio_diff:
+ if area > 0.5 * image_size * image_size * ratio[0] * ratio[1]:
+ best_ratio = ratio
+ return best_ratio
+
+ def _preprocess_image(self, image, min_num=1, max_num=12, image_size=448, use_thumbnail=False):
+ """Preprocess image for model input"""
+ orig_width, orig_height = image.size
+ aspect_ratio = orig_width / orig_height
+
+ # Calculate possible image aspect ratios
+ target_ratios = set(
+ (i, j) for n in range(min_num, max_num + 1)
+ for i in range(1, n + 1)
+ for j in range(1, n + 1)
+ if i * j <= max_num and i * j >= min_num
+ )
+ target_ratios = sorted(target_ratios, key=lambda x: x[0] * x[1])
+
+ # Find closest aspect ratio
+ target_aspect_ratio = self._find_closest_aspect_ratio(
+ aspect_ratio, target_ratios, orig_width, orig_height, image_size
+ )
+
+ # Calculate target dimensions
+ target_width = image_size * target_aspect_ratio[0]
+ target_height = image_size * target_aspect_ratio[1]
+ blocks = target_aspect_ratio[0] * target_aspect_ratio[1]
+
+ # Resize and split image
+ resized_img = image.resize((target_width, target_height))
+ processed_images = []
+ for i in range(blocks):
+ box = (
+ (i % (target_width // image_size)) * image_size,
+ (i // (target_width // image_size)) * image_size,
+ ((i % (target_width // image_size)) + 1) * image_size,
+ ((i // (target_width // image_size)) + 1) * image_size
+ )
+ split_img = resized_img.crop(box)
+ processed_images.append(split_img)
+
+ assert len(processed_images) == blocks
+ if use_thumbnail and len(processed_images) != 1:
+ thumbnail_img = image.resize((image_size, image_size))
+ processed_images.append(thumbnail_img)
+
+ return processed_images
+
+ def load_image(self, image_path, input_size=448, max_num=12):
+ """Load and process image for analysis"""
+ image = Image.open(image_path).convert('RGB')
+ transform = self._build_transform(input_size)
+ images = self._preprocess_image(image, image_size=input_size, use_thumbnail=True, max_num=max_num)
+ pixel_values = [transform(image) for image in images]
+ pixel_values = torch.stack(pixel_values)
+ return pixel_values
+
+ def analyze_image(self, image_path, max_num=12):
+ """Analyze image for expected sounds"""
+ # Load and process image
+ pixel_values = self.load_image(image_path, max_num=max_num)
+
+ # Move to device with appropriate dtype
+ if self.device.type == "cuda":
+ pixel_values = pixel_values.to(torch.bfloat16).to(self.device)
+ else:
+ pixel_values = pixel_values.to(torch.float32).to(self.device)
+
+ # Create sound-focused query
+ query = soundscape_query
+
+ # Generate response
+ generation_config = dict(max_new_tokens=1024, do_sample=True)
+ response = self.model.chat(self.tokenizer, pixel_values, query, generation_config)
+
+ return response
+
+
+class StreetSoundTextPipeline:
+ """Complete pipeline for Street View sound analysis"""
+
+ def __init__(self, log_dir="logs", model_name="intern_2_5-4B", use_cuda=True):
+ # Create log directory if it doesn't exist
+ self.log_dir = log_dir
+ os.makedirs(log_dir, exist_ok=True)
+
+ # Initialize components
+ self.downloader = StreetViewDownloader()
+ self.extractor = PerspectiveExtractor()
+ # self.analyzer = ImageAnalyzer(model_name=model_name, use_cuda=use_cuda)
+ self.analyzer = None
+ self.model_name = model_name
+ self.use_cuda = use_cuda
+
+ def _load_analyzer(self):
+ if self.analyzer is None:
+ self.analyzer = ImageAnalyzer(model_name=self.model_name, use_cuda=self.use_cuda)
+
+ def _unload_analyzer(self):
+ if self.analyzer is not None:
+ if hasattr(self.analyzer, 'model') and self.analyzer.model is not None:
+ self.analyzer.model = self.analyzer.model.to("cpu")
+ del self.analyzer.model
+ self.analyzer.model = None
+ torch.cuda.empty_cache()
+ self.analyzer = None
+
+ def process(self, lat, lon, view, panoramic=False):
+ """
+ Process a location to generate sound description for specified view or all views
+
+ Args:
+ lat (float): Latitude
+ lon (float): Longitude
+ view (str): Perspective view ('front', 'back', 'left', 'right')
+ panoramic (bool): If True, process all views instead of just the specified one
+
+ Returns:
+ dict or list: Results including panorama info and sound description(s)
+ """
+ if view not in ["front", "back", "left", "right"]:
+ raise ValueError(f"Invalid view: {view}. Choose from: front, back, left, right")
+
+ # Step 1: Download panoramic image
+ print(f"Downloading Street View panorama for coordinates: {lat}, {lon}")
+
+ pano_path = os.path.join(self.log_dir, "panorama.jpg")
+ pano_img, pid, plat, plon, p_orient = self.downloader.download_image(lat, lon)
+ Image.fromarray(pano_img).save(pano_path)
+
+ # Step 2: Extract perspective views
+ print(f"Extracting perspective views with orientation: {p_orient}°")
+ cutouts = self.extractor.extract_views(pano_img, 512)
+
+ # Save all views
+ for v, img in cutouts.items():
+ view_path = os.path.join(self.log_dir, f"{v}.jpg")
+ Image.fromarray(img).save(view_path)
+
+ self._load_analyzer()
+ print("\n[DEBUG] Current soundscape query:")
+ print(soundscape_query)
+ print("-" * 50)
+ if panoramic:
+ # Process all views
+ print(f"Analyzing all views for sound information")
+ results = []
+
+ for current_view in ["front", "back", "left", "right"]:
+ view_path = os.path.join(self.log_dir, f"{current_view}.jpg")
+ sound_description = self.analyzer.analyze_image(view_path)
+
+ view_result = {
+ "panorama_id": pid,
+ "coordinates": {"lat": plat, "lon": plon},
+ "orientation": p_orient,
+ "view": current_view,
+ "sound_description": sound_description,
+ "files": {
+ "panorama": pano_path,
+ "view_path": view_path
+ }
+ }
+ results.append(view_result)
+
+ self._unload_analyzer()
+ return results
+ else:
+ # Process only the selected view
+ view_path = os.path.join(self.log_dir, f"{view}.jpg")
+ print(f"Analyzing {view} view for sound information")
+ sound_description = self.analyzer.analyze_image(view_path)
+
+ self._unload_analyzer()
+
+ # Prepare results
+ results = {
+ "panorama_id": pid,
+ "coordinates": {"lat": plat, "lon": plon},
+ "orientation": p_orient,
+ "view": view,
+ "sound_description": sound_description,
+ "files": {
+ "panorama": pano_path,
+ "views": {v: os.path.join(self.log_dir, f"{v}.jpg") for v in cutouts.keys()}
+ }
+ }
+
+ return results
+
+
+def parse_location(location_str):
+ """Parse location string in format 'lat,lon' into float tuple"""
+ try:
+ lat, lon = map(float, location_str.split(','))
+ return lat, lon
+ except ValueError:
+ raise argparse.ArgumentTypeError("Location must be in format 'latitude,longitude'")
+
+
+def generate_caption(lat, lon, view="front", model="intern_2_5-4B", cpu_only=False, panoramic=False):
+ """
+ Generate sound captions for one or all views of a street view location
+
+ Args:
+ lat (float/str): Latitude
+ lon (float/str): Longitude
+ view (str): Perspective view ('front', 'back', 'left', 'right')
+ model (str): Model name to use for analysis
+ cpu_only (bool): Whether to force CPU usage
+ panoramic (bool): If True, process all views instead of just the specified one
+
+ Returns:
+ dict or list: Results with sound descriptions
+ """
+ pipeline = StreetSoundTextPipeline(
+ log_dir=log_dir,
+ model_name=model,
+ use_cuda=not cpu_only
+ )
+
+ try:
+ results = pipeline.process(lat, lon, view, panoramic=panoramic)
+
+ if panoramic:
+ # Process results for all views
+ print(f"Generated captions for all views at location: {lat}, {lon}")
+ else:
+ print(f"Generated caption for {view} view at location: {lat}, {lon}")
+
+ return results
+ except Exception as e:
+ print(f"Error: {str(e)}")
+ return None
\ No newline at end of file
diff --git a/README.md b/README.md
index fb11e4bd036d7561e7f740151d219359c217d97b..e8cdba62c74283a2c0dc85a5d7277b241b9d8eea 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,50 @@
----
-title: SoundingStreet
-emoji: 🏢
-colorFrom: gray
-colorTo: yellow
-sdk: gradio
-sdk_version: 5.26.0
-app_file: app.py
-pinned: false
-license: apache-2.0
----
-
-Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
+**A training-free pipeline utilizing pre-trained generative models to synthesize sound for any street on Earth with available Street View panoramic images.**
+
+1. Change to this directory:
+ ```
+ cd SoundingStreet
+ ```
+
+2. Create the conda environment:
+ ```
+ conda env create -f environment.yml
+ conda activate geosynthsound
+ ```
+
+3. Make sure to create necessary directories:
+ ```
+ mkdir -p logs output
+ ```
+
+4. Download checkpoint for depth estimator model:
+ ```
+ wget https://ommer-lab.com/files/depthfm/depthfm-v1.ckpt -P external_models/depth-fm/checkpoints/
+ ```
+
+5. Run the `SoundingStreet` demo:
+ ```
+ python main.py --panoramic --location "52.3436723,4.8529625"
+ ```
+ Intermediate files such as the downloaded panoramic image and perspective cut-outs can be found in `./logs/`, and output audios for each view as well as the composite audio for the location are saved as `./output/panoramic_composition.wav`
+
+
+## Acknowledgements
+
+- **InternVL2.5-8B-MPO**
+ For vision-language modeling, we employ InternVL2.5-8B-MPO, which is released under the MIT License.
+ GitHub: https://github.com/OpenGVLab/InternVL
+
+- **Grounding DINO**
+ We use Grounding DINO for open-set object detection. Grounding DINO is released under the Apache 2.0 License.
+ GitHub: https://github.com/IDEA-Research/GroundingDINO
+
+- **DepthFM**
+ We utilize the DepthFM model for monocular depth estimation. DepthFM is released under the MIT License.
+ GitHub: https://github.com/CompVis/depth-fm
+
+- **TangoFlux**
+ We incorporate TangoFlux for text-to-audio generation. TangoFlux is available for non-commercial research use only and is subject to the Stability AI Community License, WavCaps license, and the original licenses of the datasets used in training.
+ GitHub: https://github.com/declare-lab/TangoFlux
+
+
+Our repository's license and usage terms adhere to the respective licenses of these models.
\ No newline at end of file
diff --git a/SoundMapper.py b/SoundMapper.py
new file mode 100644
index 0000000000000000000000000000000000000000..72a59c41d18533e55b5cd2ad0402bf8e5b699008
--- /dev/null
+++ b/SoundMapper.py
@@ -0,0 +1,438 @@
+from DepthEstimator import DepthEstimator
+import numpy as np
+from PIL import Image
+import os
+from GenerateCaptions import generate_caption
+import re
+from config import LOGS_DIR
+from transformers import AutoProcessor, AutoModelForZeroShotObjectDetection
+import torch
+from PIL import Image, ImageDraw, ImageFont
+import spacy
+import gc
+
+class SoundMapper:
+ def __init__(self):
+ self.depth_estimator = DepthEstimator()
+ # List of depth maps in dict["predicted_depth" ,"depth"] in (tensor, PIL.Image) format
+ self.device = "cuda"
+ # self.map_list = self.depth_estimator.estimate_depth(self.depth_estimator.image_dir)
+ self.map_list = None
+ self.image_dir = self.depth_estimator.image_dir
+ # self.nlp = spacy.load("en_core_web_sm")
+ self.nlp = None
+ self.dino = None
+ self.dino_processor = None
+ # self.dino = AutoModelForZeroShotObjectDetection.from_pretrained("IDEA-Research/grounding-dino-tiny").to(self.device)
+ # self.dino_processor = AutoProcessor.from_pretrained("IDEA-Research/grounding-dino-tiny")
+
+ def _load_nlp(self):
+ if self.nlp is None:
+ self.nlp = spacy.load("en_core_web_sm")
+ return self.nlp
+
+ def _load_depth_maps(self):
+ if self.map_list is None:
+ self.map_list = self.depth_estimator.estimate_depth(self.depth_estimator.image_dir)
+ return self.map_list
+
+ def process_depth_maps(self) -> list:
+ depth_maps = self._load_depth_maps()
+ processed_maps = []
+ for item in depth_maps:
+ depth_map = item["depth"]
+ depth_array = np.array(depth_map)
+ normalization = depth_array / 255.0
+ processed_maps.append({
+ "original": depth_map,
+ "normalization": normalization
+ })
+ return processed_maps
+
+ # def create_depth_zone(self, processed_maps : list, num_zones = 3):
+ # zones_data = []
+ # for depth_data in processed_maps:
+ # normalized = depth_data["normalization"]
+ # thresholds = np.linspace(0, 1, num_zones+1)
+ # zones = []
+ # for i in range(num_zones):
+ # zone_mask = (normalized >= thresholds[i]) & (normalized < thresholds[i+1])
+ # zone_percentage = zone_mask.sum() / zone_mask.size
+ # zones.append({
+ # "range": (thresholds[i], thresholds[i+1]),
+ # "percentage": zone_percentage,
+ # "mask": zone_mask
+ # })
+ # zones_data.append(zones)
+ # return zones_data
+
+ def detect_sound_sources(self, caption_text: str) -> dict:
+ """
+ Extract nouns and their sound descriptions from caption text.
+ Returns a dictionary mapping nouns to their descriptions.
+ """
+ sound_sources = {}
+ nlp = self._load_nlp()
+
+ print(f"\n[DEBUG] Beginning sound source detection")
+ print(f"Raw caption text length: {len(caption_text)}")
+ print(f"First 100 chars: {caption_text[:100]}...")
+
+ # Split the caption by newlines to separate entries
+ lines = caption_text.strip().split('\n')
+ print(f"Found {len(lines)} lines after splitting")
+
+ for i, line in enumerate(lines):
+ # Skip empty lines
+ if not line.strip():
+ continue
+
+ print(f"Processing line {i}: {line[:50]}{'...' if len(line) > 50 else ''}")
+
+ # Check if line matches the expected format (Noun: description)
+ if ':' in line:
+ parts = line.split(':', 1) # Split only on the first colon
+
+ # Clean up the noun part - remove numbers and leading/trailing whitespace
+ noun_part = parts[0].strip().lower()
+ # Remove list numbering (e.g., "1. ", "2. ", etc.)
+ noun_part = re.sub(r'^\d+\.\s*', '', noun_part)
+
+ description = parts[1].strip()
+
+ # Clean any markdown formatting
+ noun = re.sub(r'[*()]', '', noun_part).strip()
+ description = re.sub(r'[*()]', '', description).strip()
+
+ # Separate the description at em dash if present
+ if ' — ' in description:
+ description = description.split(' — ', 1)[0].strip()
+ elif ' - ' in description:
+ description = description.split(' - ', 1)[0].strip()
+
+ print(f" - Found potential noun: '{noun}' with description: '{description[:30]}...'")
+
+ # Skip if noun contains invalid characters or is too short
+ if '##' not in noun and len(noun) > 1 and noun[0].isalpha():
+ sound_sources[noun] = description
+ print(f" √ Added to sound sources")
+ else:
+ print(f" × Skipped (invalid format)")
+
+ # If no structured format found, try to extract nouns from the text
+ if not sound_sources:
+ print("No structured format found, falling back to noun extraction")
+ all_nouns = []
+ doc = nlp(caption_text)
+ for token in doc:
+ if token.pos_ == "NOUN" and len(token.text) > 1:
+ if token.text[0].isalpha():
+ all_nouns.append(token.text.lower())
+ print(f" - Extracted noun: '{token.text.lower()}'")
+
+ for noun in all_nouns:
+ sound_sources[noun] = "" # Empty description
+
+ print(f"[DEBUG] Final detected sound sources: {list(sound_sources.keys())}")
+ return sound_sources
+
+ def map_bbox_to_depth_zone(self, bbox, depth_map, num_zones=3):
+ x1, y1, x2, y2 = [int(coord) for coord in bbox]
+
+ height, width = depth_map.shape
+ x1, y1 = max(0, x1), max(0, y1)
+ x2, y2 = min(width, x2), min(height, y2)
+
+ depth_roi = depth_map[y1:y2, x1:x2]
+
+ if depth_roi.size == 0:
+ return num_zones - 1
+
+ mean_depth = np.mean(depth_roi)
+
+ thresholds = self.create_histogram_depth_zones(depth_map, num_zones)
+ for i in range(num_zones):
+ if thresholds[i] <= mean_depth < thresholds[i+1]:
+ return i
+ return num_zones - 1
+
+ def detect_objects(self, nouns : list, image: Image):
+ filtered_nouns = []
+ for noun in nouns:
+ if '##' not in noun and len(noun) > 1 and noun[0].isalpha():
+ filtered_nouns.append(noun)
+
+ print(f"Detecting objects for nouns: {filtered_nouns}")
+
+ if self.dino is None:
+ self.dino = AutoModelForZeroShotObjectDetection.from_pretrained("IDEA-Research/grounding-dino-base").to(self.device)
+ self.dino_processor = AutoProcessor.from_pretrained("IDEA-Research/grounding-dino-base")
+ else:
+ self.dino = self.dino.to(self.device)
+
+ text_prompt = " . ".join(filtered_nouns)
+ inputs = self.dino_processor(images=image, text=text_prompt, return_tensors="pt").to(self.device)
+
+ with torch.no_grad():
+ outputs = self.dino(**inputs)
+ results = self.dino_processor.post_process_grounded_object_detection(
+ outputs,
+ inputs.input_ids,
+ box_threshold=0.25,
+ text_threshold=0.25,
+ target_sizes=[image.size[::-1]]
+ )
+
+ result = results[0]
+ labels = result["labels"]
+ bboxes = result["boxes"]
+
+ clean_labels = []
+ for label in labels:
+ clean_label = re.sub(r'##\w+', '', label)
+ clean_label = self._split_combined_words(clean_label, filtered_nouns)
+ clean_labels.append(clean_label)
+
+ self.dino = self.dino.to("cpu")
+ torch.cuda.empty_cache()
+ del inputs, outputs, results
+
+ print(f"Detected objects: {clean_labels}")
+
+ return (clean_labels, bboxes)
+
+ def _split_combined_words(self, text, nouns=None):
+ nlp = self._load_nlp()
+ if nouns is None:
+ known_words = set()
+ doc = nlp(text)
+ for token in doc:
+ if token.pos_ == "NOUN" and len(token.text) > 1:
+ known_words.add(token.text.lower())
+ else:
+ known_words = set(nouns)
+
+ result = []
+ for word in text.split():
+ if word in known_words:
+ result.append(word)
+ continue
+
+ found = False
+ for known in known_words:
+ if known in word and len(known) > 2:
+ result.append(known)
+ found = True
+
+ if not found:
+ result.append(word)
+
+ return " ".join(result)
+
+ def process_dino_labels(self, labels):
+ processed_labels = []
+ nlp = self._load_nlp()
+
+ for label in labels:
+ if label.startswith('##'):
+ continue
+ label = re.sub(r'[*()]', '', label).strip()
+
+ parts = label.split()
+ for part in parts:
+ if part.startswith('##'):
+ continue
+ doc = nlp(part)
+ for token in doc:
+ if token.pos_ == "NOUN" and len(token.text) > 1:
+ processed_labels.append(token.text.lower())
+
+ unique_labels = []
+ for label in processed_labels:
+ if label not in unique_labels:
+ unique_labels.append(label)
+
+ return unique_labels
+
+
+ def create_histogram_depth_zones(self, depth_map, num_zones = 3):
+ # using 50 bins because it is faster
+ hist, bin_edge = np.histogram(depth_map.flatten(), bins=50, range=(0, 1))
+ cumulative = np.cumsum(hist) / np.sum(hist)
+ thresholds = [0.0]
+ for i in range(1, num_zones):
+ target = i / num_zones
+ idx = np.argmin(np.abs(cumulative - target))
+ thresholds.append(bin_edge[idx + 1])
+ thresholds.append(1.0)
+
+ return thresholds
+
+
+ def analyze_object_depths(self, image_path, depth_map, lat, lon, caption_data=None, all_objects=False):
+ image = Image.open(image_path)
+
+ if caption_data is None:
+ caption = generate_caption(lat, lon)
+ if not caption:
+ print(f"Failed to generate caption for {image_path}")
+ return []
+ caption_text = caption.get("sound_description", "")
+ else:
+ caption_text = caption_data.get("sound_description", "")
+
+ # Debug: Print the raw caption text
+ print(f"\n[DEBUG] Raw caption text for {os.path.basename(image_path)}:")
+ print(caption_text)
+ print("-" * 50)
+
+ if not caption_text:
+ print(f"No caption text available for {image_path}")
+ return []
+
+ # Extract nouns and their sound descriptions
+ sound_sources = self.detect_sound_sources(caption_text)
+
+ # Debug: Print the extracted sound sources
+ print(f"[DEBUG] Extracted sound sources:")
+ for noun, desc in sound_sources.items():
+ print(f" - {noun}: {desc}")
+ print("-" * 50)
+
+ if not sound_sources:
+ print(f"No sound sources detected in caption for {image_path}")
+ return []
+
+ # Get list of nouns only for object detection
+ nouns = list(sound_sources.keys())
+
+ # Debug: Print the list of nouns being used for detection
+ print(f"[DEBUG] Nouns for object detection: {nouns}")
+ print("-" * 50)
+
+ labels, bboxes = self.detect_objects(nouns, image)
+ if len(labels) == 0 or len(bboxes) == 0:
+ print(f"No objects detected in {image_path}")
+ return []
+
+ object_data = []
+ known_objects = set(nouns) if nouns else set()
+
+ for i, (label, bbox) in enumerate(zip(labels, bboxes)):
+ if '##' in label:
+ continue
+
+ x1, y1, x2, y2 = [int(coord) for coord in bbox]
+ height, width = depth_map.shape
+ x1, y1 = max(0, x1), max(0, y1)
+ x2, y2 = min(width, x2), min(height, y2)
+
+ depth_roi = depth_map[y1:y2, x1:x2]
+ if depth_roi.size == 0:
+ continue
+
+ mean_depth = np.mean(depth_roi)
+
+ matched_noun = None
+ matched_desc = None
+
+ for word in label.split():
+ word = word.lower()
+ if word in sound_sources:
+ matched_noun = word
+ matched_desc = sound_sources[word]
+ break
+ if matched_noun is None:
+ for noun in sound_sources:
+ if noun in label.lower():
+ matched_noun = noun
+ matched_desc = sound_sources[noun]
+ break
+ if matched_noun is None:
+ for word in label.split():
+ if len(word) > 1 and word[0].isalpha() and '##' not in word:
+ matched_noun = word.lower()
+ matched_desc = "" # No description available
+ break
+
+ if matched_noun:
+ thresholds = self.create_histogram_depth_zones(depth_map, num_zones=3)
+ zone = 0 # The default is 0 which is the closest zone
+ for i in range(3):
+ if thresholds[i] <= mean_depth < thresholds[i+1]:
+ zone = i
+ break
+
+ object_data.append({
+ "original_label": matched_noun,
+ "bbox": bbox.tolist(),
+ "depth_zone": zone,
+ "zone_description": ["near", "medium", "far"][zone],
+ "mean_depth": mean_depth,
+ "weight": 1.0 - mean_depth,
+ "sound_description": matched_desc
+ })
+ if all_objects:
+ object_data.sort(key=lambda x: x["mean_depth"])
+ return object_data
+ else:
+ if not object_data:
+ return []
+ closest_object = min(object_data, key=lambda x: x["mean_depth"])
+ return [closest_object]
+
+ def cleanup(self):
+ if hasattr(self, 'depth_estimator') and self.depth_estimator is not None:
+ del self.depth_estimator
+ self.depth_estimator = None
+
+ if self.map_list is not None:
+ del self.map_list
+ self.map_list = None
+
+ if self.dino is not None:
+ self.dino = self.dino.to("cpu")
+ del self.dino
+ self.dino = None
+ del self.dino_processor
+ self.dino_processor = None
+
+ if self.nlp is not None:
+ del self.nlp
+ self.nlp = None
+ torch.cuda.empty_cache()
+ gc.collect()
+
+ def test_object_depth_analysis(self):
+ """
+ Test the object depth analysis on all images in the directory.
+ """
+ # Process depth maps first
+ processed_maps = self.process_depth_maps()
+
+ # Get list of original image paths
+ image_dir = self.depth_estimator.image_dir
+ image_paths = [os.path.join(image_dir, f) for f in os.listdir(image_dir) if f.endswith(".jpg")]
+
+ results = []
+
+ # For each image and its corresponding depth map
+ for i, (image_path, processed_map) in enumerate(zip(image_paths, processed_maps)):
+ # Extract the normalized depth map
+ depth_map = processed_map["normalization"]
+
+ # Analyze objects and their depths
+ object_depths = self.analyze_object_depths(image_path, depth_map)
+
+ # Store results
+ results.append({
+ "image_path": image_path,
+ "object_depths": object_depths
+ })
+
+ # Print some information for debugging
+ print(f"Analyzed {image_path}:")
+ for obj in object_depths:
+ print(f" - {obj['original_label']} (Zone: {obj['zone_description']})")
+
+ return results
diff --git a/app.py b/app.py
new file mode 100644
index 0000000000000000000000000000000000000000..5255710a986fa998db82cb4a7da73c82b733e2d3
--- /dev/null
+++ b/app.py
@@ -0,0 +1,182 @@
+import os
+import gc
+from pathlib import Path
+
+import gradio as gr
+import torch
+import torchaudio
+
+from config import LOGS_DIR, OUTPUT_DIR
+from SoundMapper import SoundMapper
+from GenerateAudio import GenerateAudio
+from GenerateCaptions import generate_caption
+from audio_mixer import compose_audio
+
+# Ensure required directories exist
+os.makedirs(LOGS_DIR, exist_ok=True)
+os.makedirs(OUTPUT_DIR, exist_ok=True)
+# Prepare external model dir and download checkpoint if missing
+from pathlib import Path
+depthfm_ckpt = Path('external_models/depth-fm/checkpoints/depthfm-v1.ckpt')
+if not depthfm_ckpt.exists():
+ depthfm_ckpt.parent.mkdir(parents=True, exist_ok=True)
+ os.system('wget https://ommer-lab.com/files/depthfm/depthfm-v1.ckpt -P external_models/depth-fm/checkpoints/')
+
+
+# Clear CUDA cache between runs
+def clear_cuda():
+ torch.cuda.empty_cache()
+ gc.collect()
+
+
+def process_images(
+ image_dir: str,
+ output_dir: str,
+ panoramic: bool,
+ view: str,
+ model: str,
+ location: str,
+ audio_duration: int,
+ cpu_only: bool
+) -> None:
+ # Existing processing logic, generates files in OUTPUT_DIR
+ lat, lon = location.split(",")
+ os.makedirs(output_dir, exist_ok=True)
+ sound_mapper = SoundMapper()
+ audio_generator = GenerateAudio()
+
+ if panoramic:
+ # Panoramic: generate per-view audio then composition
+ view_results = generate_caption(lat, lon, view=view, model=model,
+ cpu_only=cpu_only, panoramic=True)
+ processed_maps = sound_mapper.process_depth_maps()
+ image_paths = sorted(Path(image_dir).glob("*.jpg"))
+ audios = {}
+ for vr in view_results:
+ cv = vr["view"]
+ img_file = Path(image_dir) / f"{cv}.jpg"
+ if not img_file.exists():
+ continue
+ idx = [i for i, p in enumerate(image_paths) if p.name == img_file.name]
+ if not idx:
+ continue
+ depth_map = processed_maps[idx[0]]["normalization"]
+ obj_depths = sound_mapper.analyze_object_depths(
+ str(img_file), depth_map, lat, lon,
+ caption_data=vr, all_objects=False
+ )
+ if not obj_depths:
+ continue
+ out_wav = Path(output_dir) / f"sound_{cv}.wav"
+ audio, sr = audio_generator.process_and_generate_audio(
+ obj_depths, duration=audio_duration
+ )
+ if audio.dim() == 3:
+ audio = audio.squeeze(0)
+ elif audio.dim() == 1:
+ audio = audio.unsqueeze(0)
+ torchaudio.save(str(out_wav), audio, sr)
+ audios[cv] = str(out_wav)
+ # final panoramic composition
+ comp = Path(output_dir) / "panoramic_composition.wav"
+ compose_audio(list(audios.values()), [1.0]*len(audios), str(comp))
+ audios['panorama'] = str(comp)
+ clear_cuda()
+ return
+
+ # Single-view: generate one audio
+ vr = generate_caption(lat, lon, view=view, model=model,
+ cpu_only=cpu_only, panoramic=False)
+ img_file = Path(image_dir) / f"{view}.jpg"
+ processed_maps = sound_mapper.process_depth_maps()
+ image_paths = sorted(Path(image_dir).glob("*.jpg"))
+ idx = [i for i, p in enumerate(image_paths) if p.name == img_file.name]
+ depth_map = processed_maps[idx[0]]["normalization"]
+ obj_depths = sound_mapper.analyze_object_depths(
+ str(img_file), depth_map, lat, lon,
+ caption_data=vr, all_objects=True
+ )
+ out_wav = Path(output_dir) / f"sound_{view}.wav"
+ audio, sr = audio_generator.process_and_generate_audio(obj_depths, duration=audio_duration)
+ if audio.dim() == 3:
+ audio = audio.squeeze(0)
+ elif audio.dim() == 1:
+ audio = audio.unsqueeze(0)
+ torchaudio.save(str(out_wav), audio, sr)
+ clear_cuda()
+
+# Gradio UI
+demo = gr.Blocks(title="Panoramic Audio Generator")
+with demo:
+ gr.Markdown("""
+ # Panoramic Audio Generator
+
+ Displays each view with its audio side by side.
+ """
+ )
+
+ with gr.Row():
+ panoramic = gr.Checkbox(label="Panoramic (multi-view)", value=False)
+ view = gr.Dropdown(["front", "back", "left", "right"], value="front", label="View")
+ location = gr.Textbox(value="52.3436723,4.8529625", label="Location (lat,lon)")
+ # model = gr.Textbox(value="intern_2_5-4B", label="Vision-Language Model")
+ model = "intern_2_5-4B"
+ audio_duration = gr.Slider(1, 60, value=10, step=1, label="Audio Duration (sec)")
+ cpu_only = gr.Checkbox(label="CPU Only", value=False)
+ btn = gr.Button("Generate")
+
+ # Output layout: two rows of two
+ with gr.Row():
+ with gr.Column():
+ img_front = gr.Image(label="Front View", type="filepath")
+ aud_front = gr.Audio(label="Front Audio", type="filepath")
+ with gr.Column():
+ img_back = gr.Image(label="Back View", type="filepath")
+ aud_back = gr.Audio(label="Back Audio", type="filepath")
+ with gr.Row():
+ with gr.Column():
+ img_left = gr.Image(label="Left View", type="filepath")
+ aud_left = gr.Audio(label="Left Audio", type="filepath")
+ with gr.Column():
+ img_right = gr.Image(label="Right View", type="filepath")
+ aud_right = gr.Audio(label="Right Audio", type="filepath")
+ # Panorama at bottom
+ img_pan = gr.Image(label="Panorama View", type="filepath")
+ aud_pan = gr.Audio(label="Panoramic Audio", type="filepath")
+
+ # Preview update
+ def run_all(pan, vw, loc, mdl, dur, cpu):
+ # generate files
+ process_images(LOGS_DIR, OUTPUT_DIR, pan, vw, mdl, loc, dur, cpu)
+ # collect files
+ views = ["front", "back", "left", "right", "panorama"]
+ paths = {}
+ for v in views:
+ img = Path(LOGS_DIR) / f"{v}.jpg"
+ audio = Path(OUTPUT_DIR) / ("panoramic_composition.wav" if v == "panorama" else f"sound_{v}.wav")
+ paths[v] = {
+ 'img': str(img) if img.exists() else None,
+ 'aud': str(audio) if audio.exists() else None
+ }
+ return (
+ paths['front']['img'], paths['front']['aud'],
+ paths['back']['img'], paths['back']['aud'],
+ paths['left']['img'], paths['left']['aud'],
+ paths['right']['img'], paths['right']['aud'],
+ paths['panorama']['img'], paths['panorama']['aud']
+ )
+
+ btn.click(
+ fn=run_all,
+ inputs=[panoramic, view, location, model, audio_duration, cpu_only],
+ outputs=[
+ img_front, aud_front,
+ img_back, aud_back,
+ img_left, aud_left,
+ img_right, aud_right,
+ img_pan, aud_pan
+ ]
+ )
+
+if __name__ == "__main__":
+ demo.launch(share=True)
diff --git a/audio_mixer.py b/audio_mixer.py
new file mode 100644
index 0000000000000000000000000000000000000000..7590a429ba54487311c1c846b704a277d88654d0
--- /dev/null
+++ b/audio_mixer.py
@@ -0,0 +1,428 @@
+import numpy as np
+import torch
+import torchaudio
+import torchaudio.transforms as T
+import matplotlib.pyplot as plt
+import os
+from typing import List, Tuple
+from config import LOGS_DIR
+
+
+
+##Some utils:
+def load_audio_files(file_paths: List[str]) -> List[Tuple[torch.Tensor, int]]:
+ """
+ Load multiple audio files and ensure they have the same length.
+
+ Args:
+ file_paths: List of paths to audio files
+
+ Returns:
+ List of tuples containing audio data and sample rate
+ """
+ audio_data = []
+
+ for path in file_paths:
+ # Load audio file
+ waveform, sample_rate = torchaudio.load(path)
+ # Convert to mono if stereo
+ if waveform.shape[0] > 1:
+ waveform = torch.mean(waveform, dim=0, keepdim=True)
+ audio_data.append((waveform.squeeze(), sample_rate))
+
+ # Verify all audio files have the same length and sample rate
+ lengths = [len(audio) for audio, _ in audio_data]
+ sample_rates = [sr for _, sr in audio_data]
+
+ if len(set(lengths)) > 1:
+ raise ValueError(f"Audio files have different lengths: {lengths}")
+ if len(set(sample_rates)) > 1:
+ raise ValueError(f"Audio files have different sample rates: {sample_rates}")
+
+ return audio_data
+
+
+def normalize_audio_volumes(audio_data: List[Tuple[torch.Tensor, int]]) -> List[Tuple[torch.Tensor, int]]:
+ """
+ Normalize the volume of each audio file to have the same energy level.
+
+ Args:
+ audio_data: List of tuples containing audio data and sample rate
+
+ Returns:
+ List of tuples containing normalized audio data and sample rate
+ """
+ normalized_data = []
+
+ # Calculate RMS (Root Mean Square) for each audio
+ rms_values = []
+ for audio, sr in audio_data:
+ # Calculate energy (squared amplitude)
+ energy = torch.mean(audio ** 2)
+ # Calculate RMS (square root of mean energy)
+ rms = torch.sqrt(energy)
+ rms_values.append(rms)
+
+ # Find the target RMS (we'll use the median to avoid outliers)
+ target_rms = torch.median(torch.tensor(rms_values))
+
+ # Normalize each audio to the target RMS
+ for (audio, sr), rms in zip(audio_data, rms_values):
+ if rms > 0: # Avoid division by zero
+ # Calculate scaling factor
+ scaling_factor = target_rms / rms
+ # Apply scaling
+ normalized_audio = audio * scaling_factor
+ else:
+ normalized_audio = audio
+
+ normalized_data.append((normalized_audio, sr))
+
+ return normalized_data
+
+def plot_energy_comparison(original_metrics: List[dict], normalized_metrics: List[dict], file_names: List[str], output_path: str = "./logs/energy_comparison.png") -> None:
+ """
+ Plot a comparison of energy metrics before and after normalization.
+
+ Args:
+ original_metrics: List of dictionaries containing metrics for original audio
+ normalized_metrics: List of dictionaries containing metrics for normalized audio
+ file_names: List of audio file names
+ output_path: Path to save the plot
+ """
+ fig, axs = plt.subplots(2, 2, figsize=(14, 10))
+
+ # Extract metrics
+ orig_rms = [m['rms'] for m in original_metrics]
+ norm_rms = [m['rms'] for m in normalized_metrics]
+
+ orig_peak = [m['peak'] for m in original_metrics]
+ norm_peak = [m['peak'] for m in normalized_metrics]
+
+ orig_dr = [m['dynamic_range_db'] for m in original_metrics]
+ norm_dr = [m['dynamic_range_db'] for m in normalized_metrics]
+
+ orig_cf = [m['crest_factor'] for m in original_metrics]
+ norm_cf = [m['crest_factor'] for m in normalized_metrics]
+
+ # Prepare x-axis
+ x = np.arange(len(file_names))
+ width = 0.35
+
+ # Plot RMS (volume)
+ axs[0, 0].bar(x - width/2, orig_rms, width, label='Original')
+ axs[0, 0].bar(x + width/2, norm_rms, width, label='Normalized')
+ axs[0, 0].set_title('RMS Energy (Volume)')
+ axs[0, 0].set_xticks(x)
+ axs[0, 0].set_xticklabels(file_names, rotation=45, ha='right')
+ axs[0, 0].set_ylabel('RMS Value')
+ axs[0, 0].legend()
+
+ # Plot Peak Amplitude
+ axs[0, 1].bar(x - width/2, orig_peak, width, label='Original')
+ axs[0, 1].bar(x + width/2, norm_peak, width, label='Normalized')
+ axs[0, 1].set_title('Peak Amplitude')
+ axs[0, 1].set_xticks(x)
+ axs[0, 1].set_xticklabels(file_names, rotation=45, ha='right')
+ axs[0, 1].set_ylabel('Peak Value')
+ axs[0, 1].legend()
+
+ # Plot Dynamic Range
+ axs[1, 0].bar(x - width/2, orig_dr, width, label='Original')
+ axs[1, 0].bar(x + width/2, norm_dr, width, label='Normalized')
+ axs[1, 0].set_title('Dynamic Range (dB)')
+ axs[1, 0].set_xticks(x)
+ axs[1, 0].set_xticklabels(file_names, rotation=45, ha='right')
+ axs[1, 0].set_ylabel('dB')
+ axs[1, 0].legend()
+
+ # Plot Crest Factor
+ axs[1, 1].bar(x - width/2, orig_cf, width, label='Original')
+ axs[1, 1].bar(x + width/2, norm_cf, width, label='Normalized')
+ axs[1, 1].set_title('Crest Factor (Peak-to-RMS Ratio)')
+ axs[1, 1].set_xticks(x)
+ axs[1, 1].set_xticklabels(file_names, rotation=45, ha='right')
+ axs[1, 1].set_ylabel('Ratio')
+ axs[1, 1].legend()
+
+ plt.tight_layout()
+
+ # Create directory if it doesn't exist
+ os.makedirs(os.path.dirname(output_path) or '.', exist_ok=True)
+
+ # Save the plot
+ plt.savefig(output_path)
+ plt.close()
+
+def calculate_audio_metrics(audio_data: List[Tuple[torch.Tensor, int]]) -> List[dict]:
+ """
+ Calculate various audio metrics for each audio file.
+
+ Args:
+ audio_data: List of tuples containing audio data and sample rate
+
+ Returns:
+ List of dictionaries containing metrics
+ """
+ metrics = []
+
+ for audio, sr in audio_data:
+ # Calculate RMS (Root Mean Square)
+ energy = torch.mean(audio ** 2)
+ rms = torch.sqrt(energy)
+
+ # Calculate peak amplitude
+ peak = torch.max(torch.abs(audio))
+
+ # Calculate dynamic range
+ if torch.min(torch.abs(audio[audio != 0])) > 0:
+ min_non_zero = torch.min(torch.abs(audio[audio != 0]))
+ dynamic_range_db = 20 * torch.log10(peak / min_non_zero)
+ else:
+ dynamic_range_db = torch.tensor(float('inf'))
+
+ # Calculate crest factor (peak to RMS ratio)
+ crest_factor = peak / rms if rms > 0 else torch.tensor(float('inf'))
+
+ metrics.append({
+ 'rms': rms.item(),
+ 'peak': peak.item(),
+ 'dynamic_range_db': dynamic_range_db.item() if not torch.isinf(dynamic_range_db) else float('inf'),
+ 'crest_factor': crest_factor.item() if not torch.isinf(crest_factor) else float('inf')
+ })
+
+ return metrics
+
+
+def create_weighted_composite(
+ audio_data: List[Tuple[torch.Tensor, int]],
+ weights: List[float]
+) -> torch.Tensor:
+ """
+ Create a weighted composite of multiple audio files.
+
+ Args:
+ audio_data: List of tuples containing audio data and sample rate
+ weights: List of weights for each audio file
+
+ Returns:
+ Weighted composite audio data
+ """
+ if len(audio_data) != len(weights):
+ raise ValueError("Number of audio files and weights must match")
+
+ # Normalize weights to sum to 1
+ weights = torch.tensor(weights) / sum(weights)
+
+ # Initialize composite audio with zeros
+ composite = torch.zeros_like(audio_data[0][0])
+
+ # Add weighted audio data
+ for (audio, _), weight in zip(audio_data, weights):
+ composite += audio * weight
+
+ # Normalize to prevent clipping
+ max_val = torch.max(torch.abs(composite))
+ if max_val > 1.0:
+ composite = composite / max_val
+
+ return composite
+
+
+def create_melspectrograms(
+ audio_data: List[Tuple[torch.Tensor, int]],
+ composite: torch.Tensor,
+ sr: int
+) -> List[torch.Tensor]:
+ """
+ Create melspectrograms for individual audio files and the composite.
+
+ Args:
+ audio_data: List of tuples containing audio data and sample rate
+ composite: Composite audio data
+ sr: Sample rate
+
+ Returns:
+ List of melspectrogram data
+ """
+ specs = []
+
+ # Create mel spectrogram transform
+ mel_transform = T.MelSpectrogram(
+ sample_rate=sr,
+ n_fft=2048,
+ win_length=2048,
+ hop_length=512,
+ n_mels=128,
+ f_max=8000
+ )
+
+ # Generate spectrograms for individual audio files
+ for audio, _ in audio_data:
+ melspec = mel_transform(audio)
+ specs.append(melspec)
+
+ # Generate spectrogram for composite audio
+ composite_melspec = mel_transform(composite)
+ specs.append(composite_melspec)
+
+ return specs
+
+
+def plot_melspectrograms(
+ specs: List[torch.Tensor],
+ sr: int,
+ file_names: List[str],
+ weights: List[float],
+ output_path: str = "melspectrograms.png"
+) -> None:
+ """
+ Plot melspectrograms for individual audio files and the composite.
+
+ Args:
+ specs: List of melspectrogram data
+ sr: Sample rate
+ file_names: List of audio file names
+ weights: List of weights for each audio file
+ output_path: Path to save the plot
+ """
+ fig, axs = plt.subplots(len(specs), 1, figsize=(12, 4 * len(specs)))
+
+ # Create labels for the plots
+ labels = [f"{name} (weight: {weight:.2f})" for name, weight in zip(file_names, weights)]
+ labels.append("Composite.wav")
+
+ # Convert to dB scale (similar to librosa's power_to_db)
+ def power_to_db(spec):
+ return 10 * torch.log10(spec + 1e-10)
+
+ # Plot each melspectrogram
+ for i, (spec, label) in enumerate(zip(specs, labels)):
+ spec_db = power_to_db(spec).numpy().squeeze()
+
+ # For single subplot case
+ if len(specs) == 1:
+ ax = axs
+ else:
+ ax = axs[i]
+
+ img = ax.imshow(
+ spec_db,
+ aspect='auto',
+ origin='lower',
+ interpolation='none',
+ extent=[0, spec_db.shape[1], 0, sr/2]
+ )
+ ax.set_title(label)
+ ax.set_ylabel('Frequency (Hz)')
+ ax.set_xlabel('Time Frames')
+
+ # No colorbar as requested
+
+ plt.tight_layout()
+
+ # Create directory if it doesn't exist
+ os.makedirs(os.path.dirname(output_path) or '.', exist_ok=True)
+ # Save the plot
+ plt.savefig(output_path,dpi=300)
+ plt.close()
+
+
+def compose_audio(
+ file_paths: List[str],
+ weights: List[float],
+ output_audio_path: str = os.path.join(LOGS_DIR, "composite.wav"),
+ output_plot_path: str = os.path.join(LOGS_DIR, "plot/melspectrograms.png"),
+ energy_plot_path: str = os.path.join(LOGS_DIR, "plot/energy_comparison.png")
+) -> None:
+ """
+ Main function to process audio files and create visualizations.
+
+ Args:
+ file_paths: List of paths to audio files (supports 4 audio files)
+ weights: List of weights for each audio file
+ output_audio_path: Path to save the composite audio
+ output_plot_path: Path to save the melspectrogram plot
+ energy_plot_path: Path to save the energy comparison plot
+ """
+ # Load audio files
+ audio_data = load_audio_files(file_paths)
+
+ # # Calculate metrics for original audio
+ print("Calculating metrics for original audio...")
+ original_metrics = calculate_audio_metrics(audio_data)
+
+ # Normalize audio volumes to have same energy level
+ print("Normalizing audio volumes...")
+ normalized_audio_data = normalize_audio_volumes(audio_data)
+
+ # Calculate metrics for normalized audio
+ print("Calculating metrics for normalized audio...")
+ normalized_metrics = calculate_audio_metrics(normalized_audio_data)
+
+ # Print energy comparison
+ print("\nAudio Energy Comparison (RMS values):")
+ print("-" * 50)
+ print(f"{'File':<20} {'Original':<15} {'Normalized':<15} {'Scaling Factor':<15}")
+ print("-" * 50)
+ for i, path in enumerate(file_paths):
+ file_name = path.split("/")[-1]
+ orig_rms = original_metrics[i]['rms']
+ norm_rms = normalized_metrics[i]['rms']
+ scaling = norm_rms / orig_rms if orig_rms > 0 else float('inf')
+ print(f"{file_name[:20]:<20} {orig_rms:<15.6f} {norm_rms:<15.6f} {scaling:<15.6f}")
+
+ # Create energy comparison plot
+ print("\nCreating energy comparison plot...")
+ file_names = [path.split("/")[-1] for path in file_paths]
+ plot_energy_comparison(original_metrics, normalized_metrics, file_names, energy_plot_path)
+
+ # Get sample rate (all files have the same sample rate)
+ sr = normalized_audio_data[0][1]
+
+ # Create weighted composite
+ print("\nCreating weighted composite...")
+ composite = create_weighted_composite(normalized_audio_data, weights)
+
+ # Create directory if it doesn't exist
+ os.makedirs(os.path.dirname(output_audio_path) or '.', exist_ok=True)
+
+ # Save composite audio
+ print("Saving composite audio...")
+ torchaudio.save(output_audio_path, composite.unsqueeze(0), sr)
+
+ # Create melspectrograms for normalized audio (not original)
+ print("Creating melspectrograms for normalized audio...")
+ specs = create_melspectrograms(normalized_audio_data, composite, sr)
+
+ # Get file names without path
+ labeled_file_names = [path.split("/")[-1] for path in file_paths]
+
+ # Plot melspectrograms
+ print("Plotting melspectrograms...")
+ plot_melspectrograms(specs, sr, labeled_file_names, weights, output_plot_path)
+
+ print(f"\nComposite audio saved to {output_audio_path}")
+ print(f"Melspectrograms saved to {output_plot_path}")
+ print(f"Energy comparison saved to {energy_plot_path}")
+
+ print(f"Composite audio saved to {output_audio_path}")
+ print(f"Melspectrograms saved to {output_plot_path}")
+
+
+# if __name__ == "__main__":
+# import argparse
+
+# parser = argparse.ArgumentParser(description="Mix audio files with weights and create melspectrograms")
+# parser.add_argument("--files", nargs="+", required=True, help="Paths to audio files")
+# parser.add_argument("--weights", nargs="+", type=float, required=True, help="Weights for each audio file")
+# parser.add_argument("--output-audio", default="./logs/composite.wav", help="Path to save the composite audio")
+# parser.add_argument("--output-plot", default="./logs/melspectrograms.png", help="Path to save the melspectrogram plot")
+
+# args = parser.parse_args()
+# os.makedirs("./logs", exist_ok=True)
+# main(args.files, args.weights, args.output_audio, args.output_plot)
+
+
+# Example usage:
+# python audio_mixer.py --files audio1.wav audio2.wav audio3.wav audio4.wav --weights 0.4 0.3 0.2 0.1
\ No newline at end of file
diff --git a/config.py b/config.py
new file mode 100644
index 0000000000000000000000000000000000000000..2e0a618fff2e4184e00cc5cccd59ff15a5151b29
--- /dev/null
+++ b/config.py
@@ -0,0 +1,16 @@
+import os
+
+# Base directories
+BASE_DIR = os.path.dirname(os.path.abspath(__file__))
+LOGS_DIR = os.path.join(BASE_DIR, "logs")
+OUTPUT_DIR = os.path.join(BASE_DIR, "output")
+
+# Model paths
+EXTERNAL_MODELS_DIR = os.path.join(BASE_DIR, "external_models")
+DEPTH_FM_DIR = os.path.join(EXTERNAL_MODELS_DIR, "depth-fm")
+DEPTH_FM_CHECKPOINT = os.path.join(DEPTH_FM_DIR, "checkpoints/depthfm-v1.ckpt") # You will need to download the checkpoint manually. Here is the link: https://github.com/CompVis/depth-fm/tree/main/checkpoints
+TANGO_FLUX_DIR = os.path.join(EXTERNAL_MODELS_DIR, "TangoFlux")
+
+# Create required directories
+os.makedirs(LOGS_DIR, exist_ok=True)
+os.makedirs(OUTPUT_DIR, exist_ok=True)
diff --git a/environment.yml b/environment.yml
new file mode 100644
index 0000000000000000000000000000000000000000..be76c5954b6b3e78e58abc5a76cfd06f2b14b46c
--- /dev/null
+++ b/environment.yml
@@ -0,0 +1,8 @@
+name: geosynthsound
+channels:
+- conda-forge
+- defaults
+dependencies:
+- python=3.10
+- pip:
+ - -r requirements.txt
\ No newline at end of file
diff --git a/external_models/TangoFlux/.gitignore b/external_models/TangoFlux/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..c1ab9941a4cd4600a04713bee5f232aaff330332
--- /dev/null
+++ b/external_models/TangoFlux/.gitignore
@@ -0,0 +1,175 @@
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# UV
+# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+#uv.lock
+
+# poetry
+# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+# in version control.
+# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
+.pdm.toml
+.pdm-python
+.pdm-build/
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+# and can be added to the global gitignore or merged into this file. For a more nuclear
+# option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
+
+# PyPI configuration file
+.pypirc
+
+
+.DS_Store
+
+*.wav
diff --git a/external_models/TangoFlux/Demo.ipynb b/external_models/TangoFlux/Demo.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..0cae42c69476a35ce7de593bc7d0ab430b35861d
--- /dev/null
+++ b/external_models/TangoFlux/Demo.ipynb
@@ -0,0 +1,117 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "view-in-github",
+ "colab_type": "text"
+ },
+ "source": [
+ "
"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": true,
+ "id": "xiaRzuzPOP4H"
+ },
+ "outputs": [],
+ "source": [
+ "!pip install git+https://github.com/declare-lab/TangoFlux.git"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": true,
+ "id": "Hfu3zXTDOP4J"
+ },
+ "outputs": [],
+ "source": [
+ "import IPython\n",
+ "import torchaudio\n",
+ "from tangoflux import TangoFluxInference\n",
+ "from IPython.display import Audio\n",
+ "\n",
+ "model = TangoFluxInference(name='declare-lab/TangoFlux')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "id": "oFiak5QIOP4K"
+ },
+ "outputs": [],
+ "source": [
+ "# @title Generate Audio\n",
+ "\n",
+ "prompt = 'a futuristic space craft with unique engine sound' # @param {type:\"string\"}\n",
+ "duration = 10 # @param {type:\"number\"}\n",
+ "steps = 50 # @param {type:\"number\"}\n",
+ "\n",
+ "audio = model.generate(prompt, steps=steps, duration=duration)\n",
+ "\n",
+ "Audio(data=audio, rate=44100)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "import IPython\n",
+ "import torchaudio\n",
+ "from tangoflux import TangoFluxInference\n",
+ "from IPython.display import Audio\n",
+ "\n",
+ "model = TangoFluxInference(name='declare-lab/TangoFlux')\n",
+ "\n",
+ "# @title Generate Audio\n",
+ "prompt = 'Melodic human whistling harmonizing with natural birdsong' # @param {type:\"string\"}\n",
+ "duration = 10 # @param {type:\"number\"}\n",
+ "steps = 50 # @param {type:\"number\"}\n",
+ "\n",
+ "# Generate the audio\n",
+ "audio = model.generate(prompt, steps=steps, duration=duration)\n",
+ "\n",
+ "# Ensure audio is in the correct format (2D Tensor: [channels, samples])\n",
+ "if len(audio.shape) == 1: # If mono audio (1D tensor)\n",
+ " audio_tensor = audio.unsqueeze(0) # Add channel dimension to make it [1, samples]\n",
+ "elif len(audio.shape) == 2: # Stereo audio (2D tensor)\n",
+ " audio_tensor = audio # Already in correct format\n",
+ "else:\n",
+ " raise ValueError(f\"Unexpected audio tensor shape: {audio.shape}\")\n",
+ "\n",
+ "# Save the audio as a .wav file\n",
+ "torchaudio.save('generated_audio.wav', audio_tensor, sample_rate=44100)\n",
+ "\n",
+ "# Optionally play the audio in the notebook\n",
+ "Audio(data=audio.numpy(), rate=44100)\n"
+ ],
+ "metadata": {
+ "id": "_Z8elHyOHOQ1"
+ },
+ "execution_count": null,
+ "outputs": []
+ }
+ ],
+ "metadata": {
+ "language_info": {
+ "name": "python"
+ },
+ "colab": {
+ "provenance": [],
+ "machine_shape": "hm",
+ "private_outputs": true,
+ "include_colab_link": true
+ },
+ "kernelspec": {
+ "name": "python3",
+ "display_name": "Python 3"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 0
+}
\ No newline at end of file
diff --git a/external_models/TangoFlux/Inference.ipynb b/external_models/TangoFlux/Inference.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..856cf1ce71ee2ffab12a166ebdff79d2eb0c6ab6
--- /dev/null
+++ b/external_models/TangoFlux/Inference.ipynb
@@ -0,0 +1,79 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "Fetching 5 files: 100%|██████████| 5/5 [00:00<00:00, 76818.75it/s]\n",
+ " 0%| | 0/50 [01:05, ?it/s]\n"
+ ]
+ },
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ " \n",
+ " "
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 2,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "import torchaudio\n",
+ "from tangoflux import TangoFluxInference\n",
+ "from IPython.display import Audio\n",
+ "\n",
+ "model = TangoFluxInference(name=\"declare-lab/TangoFlux\")\n",
+ "\n",
+ "\n",
+ "audio = model.generate(\"Hammer slowly hitting the wooden table\", steps=50, duration=10)\n",
+ "\n",
+ "Audio(data=audio, rate=44100)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 16,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "torchaudio.save(\"temp.wav\", audio, sample_rate=44100)"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "flux",
+ "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.8"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/external_models/TangoFlux/LICENSE.md b/external_models/TangoFlux/LICENSE.md
new file mode 100644
index 0000000000000000000000000000000000000000..99f081548119074e570ea03364d5141159f9f81a
--- /dev/null
+++ b/external_models/TangoFlux/LICENSE.md
@@ -0,0 +1,51 @@
+# LICENSE
+
+## 1. Model & License Summary
+
+This repository contains **TangoFlux** (the “Model”) created for **non-commercial, research-only** purposes under the **UK data copyright exemption**. The Model is subject to:
+
+1. The **Stability AI Community License Agreement**, provided in the file ```STABILITY_AI_COMMUNITY_LICENSE.md```.
+2. The **WavCaps** license requirement: **only academic uses** are permitted for data sourced from WavCaps.
+3. The **original licenses** of the datasets used in training.
+
+By using or distributing this Model, you **agree** to adhere to all applicable licenses and restrictions, as summarized below.
+
+---
+
+## 2. Stability AI Community License Requirements
+
+- You must comply with the **Stability AI Community License Agreement** (the “Agreement”) for any usage, distribution, or modification of this Model.
+- **Non-Commercial Use**: This Model is for research and academic purposes only. Any commercial usage requires registering with Stability AI or obtaining a separate commercial license.
+- **Attribution & Notice**:
+ - Retain the notice:
+ ```
+ This Stability AI Model is licensed under the Stability AI Community License, Copyright © Stability AI Ltd. All Rights Reserved.
+ ```
+ - Clearly display “Powered by Stability AI” if you build upon or showcase this Model.
+- **Disclaimer & Liability**: This Model is provided **“AS IS”** with **no warranties**. Neither we nor Stability AI will be liable for any claim or damages related to Model use.
+
+See ```STABILITY_AI_COMMUNITY_LICENSE.md``` for the full text.
+
+---
+
+## 3. WavCaps & Dataset Usage
+
+- **Academic-Only for WavCaps**: By accessing any WavCaps-sourced data (including audio clips via provided links), you agree to use them **strictly for non-commercial, academic research** in accordance with WavCaps’ terms.
+- **WavCaps Audio**: Each WavCaps audio subset has its own license terms. **You** are responsible for reviewing and complying with those licenses, including attribution requirements on your end.
+
+---
+
+## 4. UK Data Copyright Exemption
+
+This Model was developed under the **UK data copyright exemption for non-commercial research**. Distribution or use outside these bounds must **not** violate that exemption or infringe on any underlying dataset’s license.
+
+---
+
+## 5. Further Information
+
+- **Stability AI License Terms**:
+- **WavCaps License**:
+
+---
+
+**End of License**.
diff --git a/external_models/TangoFlux/Notice b/external_models/TangoFlux/Notice
new file mode 100644
index 0000000000000000000000000000000000000000..127cddaba55c9e2fc0d3d01ff027ffd15a5d8b04
--- /dev/null
+++ b/external_models/TangoFlux/Notice
@@ -0,0 +1 @@
+This Stability AI Model is licensed under the Stability AI Community License, Copyright © Stability AI Ltd. All Rights Reserved
diff --git a/external_models/TangoFlux/README.md b/external_models/TangoFlux/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..f91838188793a6635e939ec0d45f22f3c8c103ea
--- /dev/null
+++ b/external_models/TangoFlux/README.md
@@ -0,0 +1,188 @@
+
+

+
+
+
+ [](https://arxiv.org/abs/2412.21037) [](https://huggingface.co/declare-lab/TangoFlux) [](https://tangoflux.github.io/) [](https://huggingface.co/spaces/declare-lab/TangoFlux) [](https://huggingface.co/datasets/declare-lab/CRPO) [](https://replicate.com/declare-lab/tangoflux)
+
+

+
+
+
+
+* Powered by **Stability AI**
+## News
+> 📣 1/3/25: We have released CRPO dataset as well as the script to perform CRPO dataset generation!
+
+## Demos
+
+[](https://huggingface.co/spaces/declare-lab/TangoFlux)
+
+[](https://colab.research.google.com/github/declare-lab/TangoFlux/blob/main/Demo.ipynb)
+
+## Overall Pipeline
+
+TangoFlux consists of FluxTransformer blocks, which are Diffusion Transformers (DiT) and Multimodal Diffusion Transformers (MMDiT) conditioned on a textual prompt and a duration embedding to generate a 44.1kHz audio up to 30 seconds long. TangoFlux learns a rectified flow trajectory to an audio latent representation encoded by a variational autoencoder (VAE). TangoFlux training pipeline consists of three stages: pre-training, fine-tuning, and preference optimization with CRPO. CRPO, particularly, iteratively generates new synthetic data and constructs preference pairs for preference optimization using DPO loss for flow matching.
+
+
+
+🚀 **TangoFlux can generate 44.1kHz stereo audio up to 30 seconds in ~3 seconds on a single A40 GPU.**
+
+## Installation
+
+```bash
+pip install git+https://github.com/declare-lab/TangoFlux
+```
+
+## Inference
+
+TangoFlux can generate audio up to 30 seconds long. You must pass a duration to the `model.generate` function when using the Python API. Please note that duration should be between 1 and 30.
+
+### Web Interface
+
+Run the following command to start the web interface:
+
+```bash
+tangoflux-demo
+```
+
+### CLI
+
+Use the CLI to generate audio from text.
+
+```bash
+tangoflux "Hammer slowly hitting the wooden table" output.wav --duration 10 --steps 50
+```
+
+### Python API
+
+```python
+import torchaudio
+from tangoflux import TangoFluxInference
+
+model = TangoFluxInference(name='declare-lab/TangoFlux')
+audio = model.generate('Hammer slowly hitting the wooden table', steps=50, duration=10)
+
+torchaudio.save('output.wav', audio, 44100)
+```
+
+### [ComfyUI](https://github.com/comfyanonymous/ComfyUI)
+
+> This ui will let you design and execute advanced stable diffusion pipelines using a graph/nodes/flowchart based interface.
+
+Check [this](https://github.com/LucipherDev/ComfyUI-TangoFlux) repo for the TangoFlux custom node for *ComfyUI*. (Thanks to [LucipherDev](https://github.com/LucipherDev))
+
+Our evaluation shows that inference with 50 steps yields the best results. A CFG scale of 3.5, 4, and 4.5 yield similar quality output. Inference with 25 steps yields similar audio quality at a faster speed.
+
+## Training
+
+We use the `accelerate` package from Hugging Face for multi-GPU training. Run `accelerate config` to setup your run configuration. The default accelerate config is in the `configs` folder. Please specify the path to your training files in the `configs/tangoflux_config.yaml`. Samples of `train.json` and `val.json` have been provided. Replace them with your own audio.
+
+`tangoflux_config.yaml` defines the training file paths and model hyperparameters:
+
+```bash
+CUDA_VISIBLE_DEVICES=0,1 accelerate launch --config_file='configs/accelerator_config.yaml' tangoflux/train.py --checkpointing_steps="best" --save_every=5 --config='configs/tangoflux_config.yaml'
+```
+
+To perform DPO training, modify the training files such that each data point contains "chosen", "reject", "caption" and "duration" fields. Please specify the path to your training files in `configs/tangoflux_config.yaml`. An example has been provided in `train_dpo.json`. Replace it with your own audio.
+
+```bash
+CUDA_VISIBLE_DEVICES=0,1 accelerate launch --config_file='configs/accelerator_config.yaml' tangoflux/train_dpo.py --checkpointing_steps="best" --save_every=5 --config='configs/tangoflux_config.yaml'
+```
+
+## Evaluation
+
+### TangoFlux vs. Other Audio Generation Models
+
+This key comparison metrics include:
+
+- **Output Length**: Represents the duration of the generated audio.
+- **FD**openl3: Fréchet Distance.
+- **KL**passt: KL divergence.
+- **CLAP**score: Alignment score.
+
+
+All the inference times are observed on the same A40 GPU. The counts of trainable parameters are reported in the **\#Params** column.
+
+| Model | Params | Duration | Steps | FDopenl3 ↓ | KLpasst ↓ | CLAPscore ↑ | IS ↑ | Inference Time (s) |
+|---|---|---|---|---|---|---|---|---|
+| **AudioLDM 2 (Large)** | 712M | 10 sec | 200 | 108.3 | 1.81 | 0.419 | 7.9 | 24.8 |
+| **Stable Audio Open** | 1056M | 47 sec | 100 | 89.2 | 2.58 | 0.291 | 9.9 | 8.6 |
+| **Tango 2** | 866M | 10 sec | 200 | 108.4 | 1.11 | 0.447 | 9.0 | 22.8 |
+| **TangoFlux (Base)** | 515M | 30 sec | 50 | 80.2 | 1.22 | 0.431 | 11.7 | 3.7 |
+| **TangoFlux** | 515M | 30 sec | 50 | 75.1 | 1.15 | 0.480 | 12.2 | 3.7 |
+
+## CRPO dataset generation
+
+There are 2 py files for CRPO dataset generation.
+tangoflux/generate_crpo.py generates the crpo dataset by providing path to prompt bank and model weights. You can specify the sample size as well as number of samples per prompt for crpo in the arguments.
+tangoflux/label_crpo.py labels the generated audio and construct preference pairs. This will also create a train.json in the output dir that can be passed into train_dpo.py
+
+You can follow the example in crpo.sh which will generate crpo dataset, then perform reward labelling to generate the train.json
+
+To run CRPO for multiple iteration, you can simply repeat the above the process multiple time through setting the correct model weight.
+## Citation
+
+```bibtex
+@misc{hung2024tangofluxsuperfastfaithful,
+ title={TangoFlux: Super Fast and Faithful Text to Audio Generation with Flow Matching and Clap-Ranked Preference Optimization},
+ author={Chia-Yu Hung and Navonil Majumder and Zhifeng Kong and Ambuj Mehrish and Amir Zadeh and Chuan Li and Rafael Valle and Bryan Catanzaro and Soujanya Poria},
+ year={2024},
+ eprint={2412.21037},
+ archivePrefix={arXiv},
+ primaryClass={cs.SD},
+ url={https://arxiv.org/abs/2412.21037},
+}
+```
+
+## LICENSE
+
+### 1. Model & License Summary
+
+This repository contains **TangoFlux** (the “Model”) created for **non-commercial, research-only** purposes under the **UK data copyright exemption**. The Model is subject to:
+
+1. The **Stability AI Community License Agreement**, provided in the file ```STABILITY_AI_COMMUNITY_LICENSE.md```.
+2. The **WavCaps** license requirement: **only academic uses** are permitted for data sourced from WavCaps.
+3. The **original licenses** of the datasets used in training.
+
+By using or distributing this Model, you **agree** to adhere to all applicable licenses and restrictions, as summarized below.
+
+---
+
+### 2. Stability AI Community License Requirements
+
+- You must comply with the **Stability AI Community License Agreement** (the “Agreement”) for any usage, distribution, or modification of this Model.
+- **Non-Commercial Use**: This Model is for research and academic purposes only. Any commercial usage requires registering with Stability AI or obtaining a separate commercial license.
+- **Attribution & Notice**:
+ - Retain the notice:
+ ```
+ This Stability AI Model is licensed under the Stability AI Community License, Copyright © Stability AI Ltd. All Rights Reserved.
+ ```
+ - Clearly display “Powered by Stability AI” if you build upon or showcase this Model.
+- **Disclaimer & Liability**: This Model is provided **“AS IS”** with **no warranties**. Neither we nor Stability AI will be liable for any claim or damages related to Model use.
+
+See ```STABILITY_AI_COMMUNITY_LICENSE.md``` for the full text.
+
+---
+
+### 3. WavCaps & Dataset Usage
+
+- **Academic-Only for WavCaps**: By accessing any WavCaps-sourced data (including audio clips via provided links), you agree to use them **strictly for non-commercial, academic research** in accordance with WavCaps’ terms.
+- **WavCaps Audio**: Each WavCaps audio subset has its own license terms. **You** are responsible for reviewing and complying with those licenses, including attribution requirements on your end.
+
+---
+
+### 4. UK Data Copyright Exemption
+
+This Model was developed under the **UK data copyright exemption for non-commercial research**. Distribution or use outside these bounds must **not** violate that exemption or infringe on any underlying dataset’s license.
+
+---
+
+### 5. Further Information
+
+- **Stability AI License Terms**:
+- **WavCaps License**:
+
+---
+
+**End of License**.
diff --git a/external_models/TangoFlux/STABILITY_AI_COMMUNITY_LICENSE.md b/external_models/TangoFlux/STABILITY_AI_COMMUNITY_LICENSE.md
new file mode 100644
index 0000000000000000000000000000000000000000..6afe787e9fb75c882d8b869d3dbbdb5d69a96fe2
--- /dev/null
+++ b/external_models/TangoFlux/STABILITY_AI_COMMUNITY_LICENSE.md
@@ -0,0 +1,57 @@
+STABILITY AI COMMUNITY LICENSE AGREEMENT
+
+Last Updated: July 5, 2024
+1. INTRODUCTION
+
+This Agreement applies to any individual person or entity (“You”, “Your” or “Licensee”) that uses or distributes any portion or element of the Stability AI Materials or Derivative Works thereof for any Research & Non-Commercial or Commercial purpose. Capitalized terms not otherwise defined herein are defined in Section V below.
+
+This Agreement is intended to allow research, non-commercial, and limited commercial uses of the Models free of charge. In order to ensure that certain limited commercial uses of the Models continue to be allowed, this Agreement preserves free access to the Models for people or organizations generating annual revenue of less than US $1,000,000 (or local currency equivalent).
+
+By clicking “I Accept” or by using or distributing or using any portion or element of the Stability Materials or Derivative Works, You agree that You have read, understood and are bound by the terms of this Agreement. If You are acting on behalf of a company, organization or other entity, then “You” includes you and that entity, and You agree that You: (i) are an authorized representative of such entity with the authority to bind such entity to this Agreement, and (ii) You agree to the terms of this Agreement on that entity’s behalf.
+
+2. RESEARCH & NON-COMMERCIAL USE LICENSE
+
+Subject to the terms of this Agreement, Stability AI grants You a non-exclusive, worldwide, non-transferable, non-sublicensable, revocable and royalty-free limited license under Stability AI’s intellectual property or other rights owned by Stability AI embodied in the Stability AI Materials to use, reproduce, distribute, and create Derivative Works of, and make modifications to, the Stability AI Materials for any Research or Non-Commercial Purpose. “Research Purpose” means academic or scientific advancement, and in each case, is not primarily intended for commercial advantage or monetary compensation to You or others. “Non-Commercial Purpose” means any purpose other than a Research Purpose that is not primarily intended for commercial advantage or monetary compensation to You or others, such as personal use (i.e., hobbyist) or evaluation and testing.
+
+3. COMMERCIAL USE LICENSE
+
+Subject to the terms of this Agreement (including the remainder of this Section III), Stability AI grants You a non-exclusive, worldwide, non-transferable, non-sublicensable, revocable and royalty-free limited license under Stability AI’s intellectual property or other rights owned by Stability AI embodied in the Stability AI Materials to use, reproduce, distribute, and create Derivative Works of, and make modifications to, the Stability AI Materials for any Commercial Purpose. “Commercial Purpose” means any purpose other than a Research Purpose or Non-Commercial Purpose that is primarily intended for commercial advantage or monetary compensation to You or others, including but not limited to, (i) creating, modifying, or distributing Your product or service, including via a hosted service or application programming interface, and (ii) for Your business’s or organization’s internal operations.
+If You are using or distributing the Stability AI Materials for a Commercial Purpose, You must register with Stability AI at (https://stability.ai/community-license). If at any time You or Your Affiliate(s), either individually or in aggregate, generate more than USD $1,000,000 in annual revenue (or the equivalent thereof in Your local currency), regardless of whether that revenue is generated directly or indirectly from the Stability AI Materials or Derivative Works, any licenses granted to You under this Agreement shall terminate as of such date. You must request a license from Stability AI at (https://stability.ai/enterprise) , which Stability AI may grant to You in its sole discretion. If you receive Stability AI Materials, or any Derivative Works thereof, from a Licensee as part of an integrated end user product, then Section III of this Agreement will not apply to you.
+
+4. GENERAL TERMS
+
+Your Research, Non-Commercial, and Commercial License(s) under this Agreement are subject to the following terms.
+a. Distribution & Attribution. If You distribute or make available the Stability AI Materials or a Derivative Work to a third party, or a product or service that uses any portion of them, You shall: (i) provide a copy of this Agreement to that third party, (ii) retain the following attribution notice within a "Notice" text file distributed as a part of such copies: "This Stability AI Model is licensed under the Stability AI Community License, Copyright © Stability AI Ltd. All Rights Reserved”, and (iii) prominently display “Powered by Stability AI” on a related website, user interface, blogpost, about page, or product documentation. If You create a Derivative Work, You may add your own attribution notice(s) to the “Notice” text file included with that Derivative Work, provided that You clearly indicate which attributions apply to the Stability AI Materials and state in the “Notice” text file that You changed the Stability AI Materials and how it was modified.
+b. Use Restrictions. Your use of the Stability AI Materials and Derivative Works, including any output or results of the Stability AI Materials or Derivative Works, must comply with applicable laws and regulations (including Trade Control Laws and equivalent regulations) and adhere to the Documentation and Stability AI’s AUP, which is hereby incorporated by reference. Furthermore, You will not use the Stability AI Materials or Derivative Works, or any output or results of the Stability AI Materials or Derivative Works, to create or improve any foundational generative AI model (excluding the Models or Derivative Works).
+c. Intellectual Property.
+(i) Trademark License. No trademark licenses are granted under this Agreement, and in connection with the Stability AI Materials or Derivative Works, You may not use any name or mark owned by or associated with Stability AI or any of its Affiliates, except as required under Section IV(a) herein.
+(ii) Ownership of Derivative Works. As between You and Stability AI, You are the owner of Derivative Works You create, subject to Stability AI’s ownership of the Stability AI Materials and any Derivative Works made by or for Stability AI.
+(iii) Ownership of Outputs. As between You and Stability AI, You own any outputs generated from the Models or Derivative Works to the extent permitted by applicable law.
+(iv) Disputes. If You or Your Affiliate(s) institute litigation or other proceedings against Stability AI (including a cross-claim or counterclaim in a lawsuit) alleging that the Stability AI Materials, Derivative Works or associated outputs or results, or any portion of any of the foregoing, constitutes infringement of intellectual property or other rights owned or licensable by You, then any licenses granted to You under this Agreement shall terminate as of the date such litigation or claim is filed or instituted. You will indemnify and hold harmless Stability AI from and against any claim by any third party arising out of or related to Your use or distribution of the Stability AI Materials or Derivative Works in violation of this Agreement.
+(v) Feedback. From time to time, You may provide Stability AI with verbal and/or written suggestions, comments or other feedback related to Stability AI’s existing or prospective technology, products or services (collectively, “Feedback”). You are not obligated to provide Stability AI with Feedback, but to the extent that You do, You hereby grant Stability AI a perpetual, irrevocable, royalty-free, fully-paid, sub-licensable, transferable, non-exclusive, worldwide right and license to exploit the Feedback in any manner without restriction. Your Feedback is provided “AS IS” and You make no warranties whatsoever about any Feedback.
+d. Disclaimer Of Warranty. UNLESS REQUIRED BY APPLICABLE LAW, THE STABILITY AI MATERIALS AND ANY OUTPUT AND RESULTS THEREFROM ARE PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE. YOU ARE SOLELY RESPONSIBLE FOR DETERMINING THE APPROPRIATENESS OR LAWFULNESS OF USING OR REDISTRIBUTING THE STABILITY AI MATERIALS, DERIVATIVE WORKS OR ANY OUTPUT OR RESULTS AND ASSUME ANY RISKS ASSOCIATED WITH YOUR USE OF THE STABILITY AI MATERIALS, DERIVATIVE WORKS AND ANY OUTPUT AND RESULTS.
+e. Limitation Of Liability. IN NO EVENT WILL STABILITY AI OR ITS AFFILIATES BE LIABLE UNDER ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, TORT, NEGLIGENCE, PRODUCTS LIABILITY, OR OTHERWISE, ARISING OUT OF THIS AGREEMENT, FOR ANY LOST PROFITS OR ANY DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL, EXEMPLARY OR PUNITIVE DAMAGES, EVEN IF STABILITY AI OR ITS AFFILIATES HAVE BEEN ADVISED OF THE POSSIBILITY OF ANY OF THE FOREGOING.
+f. Term And Termination. The term of this Agreement will commence upon Your acceptance of this Agreement or access to the Stability AI Materials and will continue in full force and effect until terminated in accordance with the terms and conditions herein. Stability AI may terminate this Agreement if You are in breach of any term or condition of this Agreement. Upon termination of this Agreement, You shall delete and cease use of any Stability AI Materials or Derivative Works. Section IV(d), (e), and (g) shall survive the termination of this Agreement.
+g. Governing Law. This Agreement will be governed by and constructed in accordance with the laws of the United States and the State of California without regard to choice of law principles, and the UN Convention on Contracts for International Sale of Goods does not apply to this Agreement.
+
+5. DEFINITIONS
+
+“Affiliate(s)” means any entity that directly or indirectly controls, is controlled by, or is under common control with the subject entity; for purposes of this definition, “control” means direct or indirect ownership or control of more than 50% of the voting interests of the subject entity.
+
+"Agreement" means this Stability AI Community License Agreement.
+
+“AUP” means the Stability AI Acceptable Use Policy available at (https://stability.ai/use-policy), as may be updated from time to time.
+
+"Derivative Work(s)” means (a) any derivative work of the Stability AI Materials as recognized by U.S. copyright laws and (b) any modifications to a Model, and any other model created which is based on or derived from the Model or the Model’s output, including “fine tune” and “low-rank adaptation” models derived from a Model or a Model’s output, but do not include the output of any Model.
+
+“Documentation” means any specifications, manuals, documentation, and other written information provided by Stability AI related to the Software or Models.
+
+“Model(s)" means, collectively, Stability AI’s proprietary models and algorithms, including machine-learning models, trained model weights and other elements of the foregoing listed on Stability’s Core Models Webpage available at (https://stability.ai/core-models), as may be updated from time to time.
+
+"Stability AI" or "we" means Stability AI Ltd. and its Affiliates.
+
+"Software" means Stability AI’s proprietary software made available under this Agreement now or in the future.
+
+“Stability AI Materials” means, collectively, Stability’s proprietary Models, Software and Documentation (and any portion or combination thereof) made available under this Agreement.
+
+“Trade Control Laws” means any applicable U.S. and non-U.S. export control and trade sanctions laws and regulations.
diff --git a/external_models/TangoFlux/__init__.py b/external_models/TangoFlux/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..8ad7ff69d0d7d9cce0aa567c85725272f55a6d4c
--- /dev/null
+++ b/external_models/TangoFlux/__init__.py
@@ -0,0 +1,4 @@
+try:
+ from .comfyui import *
+except:
+ pass
\ No newline at end of file
diff --git a/external_models/TangoFlux/assets/tangoflux.png b/external_models/TangoFlux/assets/tangoflux.png
new file mode 100644
index 0000000000000000000000000000000000000000..1ca8c862049089e270ae5b292f83a21ddb4f836d
--- /dev/null
+++ b/external_models/TangoFlux/assets/tangoflux.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e8e19e12b3c2c991a29987d7fceaed80aa8ed306827cfaa0894d666b5c250702
+size 304299
diff --git a/external_models/TangoFlux/assets/tf_opener.png b/external_models/TangoFlux/assets/tf_opener.png
new file mode 100644
index 0000000000000000000000000000000000000000..e0f423cb33b45200d15a9114e7de7ea85b1750fc
--- /dev/null
+++ b/external_models/TangoFlux/assets/tf_opener.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:58934ca2300804d67bc73c7116c3a0d956d770e0bd6e816aa9dbe9034f5b32fe
+size 464900
diff --git a/external_models/TangoFlux/assets/tf_teaser.png b/external_models/TangoFlux/assets/tf_teaser.png
new file mode 100644
index 0000000000000000000000000000000000000000..d830fb5cb03a7d691edc519b9119660e1d4450d1
--- /dev/null
+++ b/external_models/TangoFlux/assets/tf_teaser.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:475a101c58ee8cb7481172d24763fddcc1da59f578aaeccf9d8052f5a86401b6
+size 778285
diff --git a/external_models/TangoFlux/comfyui/README.md b/external_models/TangoFlux/comfyui/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..ebacef63c23db0a966454916866ec03cc51241f5
--- /dev/null
+++ b/external_models/TangoFlux/comfyui/README.md
@@ -0,0 +1,78 @@
+# ComfyUI-TangoFlux
+ComfyUI Custom Nodes for ["TangoFlux: Super Fast and Faithful Text to Audio Generation with Flow Matching"](https://arxiv.org/abs/2412.21037). These nodes, adapted from [the official implementations](https://github.com/declare-lab/TangoFlux/), generates high-quality 44.1kHz audio up to 30 seconds using just a text promptproduction.
+
+## Installation
+
+1. Navigate to your ComfyUI's custom_nodes directory:
+```bash
+cd ComfyUI/custom_nodes
+```
+
+2. Clone this repository:
+```bash
+git clone https://github.com/declare-lab/TangoFlux ComfyUI-TangoFlux
+```
+
+3. Install requirements:
+```bash
+cd ComfyUI-TangoFlux/comfyui
+python install.py
+```
+
+### Or Install via ComfyUI Manager
+
+#### Check out some demos from [the official demo page](https://tangoflux.github.io/)
+
+## Example Workflow
+
+
+
+## Usage
+
+**All the necessary models should be automatically downloaded when the TangoFluxLoader node is used for the first time.**
+
+**Models can also be downloaded using the `install.py` script**
+
+
+
+**Manual Download:**
+- Download TangoFlux from [here](https://huggingface.co/declare-lab/TangoFlux/tree/main) into `models/tangoflux`
+- Download text encoders from [here](https://huggingface.co/google/flan-t5-large/tree/main) into `models/text_encoders/google-flan-t5-large`
+
+*(Include Everything as shown in the screenshot above. Do Not Rename Anything)*
+
+The nodes can be found in "TangoFlux" category as `TangoFluxLoader`, `TangoFluxSampler`, `TangoFluxVAEDecodeAndPlay`.
+
+
+
+> [TeaCache](https://github.com/LiewFeng/TeaCache) can speedup TangoFlux 2x without much audio quality degradation, in a training-free manner.
+>
+>
+> ## 📈 Inference Latency Comparisons on a Single A800
+>
+>
+> | TangoFlux | TeaCache (0.25) | TeaCache (0.4) |
+> |:-------------------:|:----------------------------:|:--------------------:|
+> | ~4.08 s | ~2.42 s | ~1.95 s |
+
+## Citation
+
+```bibtex
+@misc{hung2024tangofluxsuperfastfaithful,
+ title={TangoFlux: Super Fast and Faithful Text to Audio Generation with Flow Matching and Clap-Ranked Preference Optimization},
+ author={Chia-Yu Hung and Navonil Majumder and Zhifeng Kong and Ambuj Mehrish and Rafael Valle and Bryan Catanzaro and Soujanya Poria},
+ year={2024},
+ eprint={2412.21037},
+ archivePrefix={arXiv},
+ primaryClass={cs.SD},
+ url={https://arxiv.org/abs/2412.21037},
+}
+```
+```
+@article{liu2024timestep,
+ title={Timestep Embedding Tells: It's Time to Cache for Video Diffusion Model},
+ author={Liu, Feng and Zhang, Shiwei and Wang, Xiaofeng and Wei, Yujie and Qiu, Haonan and Zhao, Yuzhong and Zhang, Yingya and Ye, Qixiang and Wan, Fang},
+ journal={arXiv preprint arXiv:2411.19108},
+ year={2024}
+}
+```
diff --git a/external_models/TangoFlux/comfyui/__init__.py b/external_models/TangoFlux/comfyui/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..458b6c411ad33604d35b71188e2906d3fc915490
--- /dev/null
+++ b/external_models/TangoFlux/comfyui/__init__.py
@@ -0,0 +1,6 @@
+from .nodes import NODE_CLASS_MAPPINGS
+from .server import *
+
+WEB_DIRECTORY = "./comfyui/web"
+
+__all__ = ["NODE_CLASS_MAPPINGS", "WEB_DIRECTORY"]
diff --git a/external_models/TangoFlux/comfyui/example_workflow.json b/external_models/TangoFlux/comfyui/example_workflow.json
new file mode 100644
index 0000000000000000000000000000000000000000..aceed958775e2138a6cb31b57ff32cde5ff86c96
--- /dev/null
+++ b/external_models/TangoFlux/comfyui/example_workflow.json
@@ -0,0 +1,168 @@
+{
+ "last_node_id": 13,
+ "last_link_id": 15,
+ "nodes": [
+ {
+ "id": 10,
+ "type": "TangoFluxLoader",
+ "pos": [
+ 380,
+ 320
+ ],
+ "size": [
+ 210,
+ 102
+ ],
+ "flags": {},
+ "order": 0,
+ "mode": 0,
+ "inputs": [],
+ "outputs": [
+ {
+ "name": "model",
+ "type": "TANGOFLUX_MODEL",
+ "links": [
+ 11
+ ],
+ "slot_index": 0
+ },
+ {
+ "name": "vae",
+ "type": "TANGOFLUX_VAE",
+ "links": [
+ 15
+ ],
+ "slot_index": 1
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "TangoFluxLoader"
+ },
+ "widgets_values": [
+ false,
+ 0.25
+ ]
+ },
+ {
+ "id": 13,
+ "type": "TangoFluxVAEDecodeAndPlay",
+ "pos": [
+ 1060,
+ 320
+ ],
+ "size": [
+ 315,
+ 126
+ ],
+ "flags": {},
+ "order": 2,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "vae",
+ "type": "TANGOFLUX_VAE",
+ "link": 15
+ },
+ {
+ "name": "latents",
+ "type": "TANGOFLUX_LATENTS",
+ "link": 14
+ }
+ ],
+ "outputs": [],
+ "properties": {
+ "Node name for S&R": "TangoFluxVAEDecodeAndPlay"
+ },
+ "widgets_values": [
+ "TangoFlux",
+ "wav",
+ true
+ ]
+ },
+ {
+ "id": 11,
+ "type": "TangoFluxSampler",
+ "pos": [
+ 620,
+ 320
+ ],
+ "size": [
+ 400,
+ 220
+ ],
+ "flags": {},
+ "order": 1,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "model",
+ "type": "TANGOFLUX_MODEL",
+ "link": 11
+ }
+ ],
+ "outputs": [
+ {
+ "name": "latents",
+ "type": "TANGOFLUX_LATENTS",
+ "links": [
+ 14
+ ],
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "TangoFluxSampler"
+ },
+ "widgets_values": [
+ "A dog barking near the ocean, ocean waves crashing.",
+ 50,
+ 3,
+ 10,
+ 106139285587780,
+ "randomize",
+ 1
+ ]
+ }
+ ],
+ "links": [
+ [
+ 11,
+ 10,
+ 0,
+ 11,
+ 0,
+ "TANGOFLUX_MODEL"
+ ],
+ [
+ 14,
+ 11,
+ 0,
+ 13,
+ 1,
+ "TANGOFLUX_LATENTS"
+ ],
+ [
+ 15,
+ 10,
+ 1,
+ 13,
+ 0,
+ "TANGOFLUX_VAE"
+ ]
+ ],
+ "groups": [],
+ "config": {},
+ "extra": {
+ "ds": {
+ "scale": 0.9480295566502464,
+ "offset": [
+ -200.83333333333337,
+ -102.2460379319304
+ ]
+ },
+ "node_versions": {
+ "comfyui-tangoflux": "1.0.4"
+ }
+ },
+ "version": 0.4
+}
\ No newline at end of file
diff --git a/external_models/TangoFlux/comfyui/install.py b/external_models/TangoFlux/comfyui/install.py
new file mode 100644
index 0000000000000000000000000000000000000000..6358026308929ee72af31e36f138c83feb94885a
--- /dev/null
+++ b/external_models/TangoFlux/comfyui/install.py
@@ -0,0 +1,79 @@
+import sys
+import os
+import logging
+import subprocess
+import traceback
+import json
+import re
+
+log = logging.getLogger("TangoFlux")
+
+download_models = True
+
+EXT_PATH = os.path.dirname(os.path.abspath(__file__))
+
+try:
+ folder_paths_path = os.path.abspath(os.path.join(EXT_PATH, "..", "..", "..", "folder_paths.py"))
+
+ sys.path.append(os.path.dirname(folder_paths_path))
+
+ import folder_paths
+
+ TANGOFLUX_DIR = os.path.join(folder_paths.models_dir, "tangoflux")
+ TEXT_ENCODER_DIR = os.path.join(folder_paths.models_dir, "text_encoders")
+except:
+ download_models = False
+
+try:
+ log.info("Installing requirements")
+ subprocess.check_call([sys.executable, "-m", "pip", "install", "-r", f"{EXT_PATH}/requirements.txt", "--no-warn-script-location"])
+
+ if download_models:
+ from huggingface_hub import snapshot_download
+
+ log.info("Downloading Necessary models")
+
+ try:
+ log.info(f"Downloading TangoFlux models to: {TANGOFLUX_DIR}")
+ snapshot_download(
+ repo_id="declare-lab/TangoFlux",
+ allow_patterns=["*.json", "*.safetensors"],
+ local_dir=TANGOFLUX_DIR,
+ local_dir_use_symlinks=False,
+ )
+ except Exception:
+ traceback.print_exc()
+ log.error("Failed to download TangoFlux models")
+
+ log.info("Loading config")
+
+ with open(os.path.join(TANGOFLUX_DIR, "config.json"), "r") as f:
+ config = json.load(f)
+
+ try:
+ text_encoder = re.sub(r'[<>:"/\\|?*]', '-', config.get("text_encoder_name", "google/flan-t5-large"))
+ text_encoder_path = os.path.join(TEXT_ENCODER_DIR, text_encoder)
+
+ log.info(f"Downloading text encoders to: {text_encoder_path}")
+ snapshot_download(
+ repo_id=config.get("text_encoder_name", "google/flan-t5-large"),
+ allow_patterns=["*.json", "*.safetensors", "*.model"],
+ local_dir=text_encoder_path,
+ local_dir_use_symlinks=False,
+ )
+ except Exception:
+ traceback.print_exc()
+ log.error("Failed to download text encoders")
+
+ try:
+ log.info("Installing TangoFlux module")
+ subprocess.check_call([sys.executable, "-m", "pip", "install", os.path.join(EXT_PATH, "..")])
+ except Exception:
+ traceback.print_exc()
+ log.error("Failed to install TangoFlux module")
+
+ log.info("TangoFlux Installation completed")
+
+except Exception:
+ traceback.print_exc()
+ log.error("TangoFlux Installation failed")
\ No newline at end of file
diff --git a/external_models/TangoFlux/comfyui/nodes.py b/external_models/TangoFlux/comfyui/nodes.py
new file mode 100644
index 0000000000000000000000000000000000000000..9d463877f4f80ed8e1f934bbf3722bda28c42f70
--- /dev/null
+++ b/external_models/TangoFlux/comfyui/nodes.py
@@ -0,0 +1,328 @@
+import os
+import logging
+import json
+import random
+import torch
+import torchaudio
+import re
+
+from diffusers import AutoencoderOobleck, FluxTransformer2DModel
+from huggingface_hub import snapshot_download
+
+from comfy.utils import load_torch_file, ProgressBar
+import folder_paths
+
+from tangoflux.model import TangoFlux
+from .teacache import teacache_forward
+
+log = logging.getLogger("TangoFlux")
+
+TANGOFLUX_DIR = os.path.join(folder_paths.models_dir, "tangoflux")
+if "tangoflux" not in folder_paths.folder_names_and_paths:
+ current_paths = [TANGOFLUX_DIR]
+else:
+ current_paths, _ = folder_paths.folder_names_and_paths["tangoflux"]
+folder_paths.folder_names_and_paths["tangoflux"] = (
+ current_paths,
+ folder_paths.supported_pt_extensions,
+)
+TEXT_ENCODER_DIR = os.path.join(folder_paths.models_dir, "text_encoders")
+
+
+class TangoFluxLoader:
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {
+ "required": {
+ "enable_teacache": ("BOOLEAN", {"default": False}),
+ "rel_l1_thresh": (
+ "FLOAT",
+ {"default": 0.25, "min": 0.0, "max": 10.0, "step": 0.01},
+ ),
+ },
+ }
+
+ RETURN_TYPES = ("TANGOFLUX_MODEL", "TANGOFLUX_VAE")
+ RETURN_NAMES = ("model", "vae")
+ OUTPUT_TOOLTIPS = ("TangoFlux Model", "TangoFlux Vae")
+
+ CATEGORY = "TangoFlux"
+ FUNCTION = "load_tangoflux"
+ DESCRIPTION = "Load TangoFlux model"
+
+ def __init__(self):
+ self.model = None
+ self.vae = None
+ self.enable_teacache = False
+ self.rel_l1_thresh = 0.25
+ self.original_forward = FluxTransformer2DModel.forward
+
+ def load_tangoflux(
+ self,
+ enable_teacache=False,
+ rel_l1_thresh=0.25,
+ tangoflux_path=TANGOFLUX_DIR,
+ text_encoder_path=TEXT_ENCODER_DIR,
+ device="cuda",
+ ):
+ if self.model is None or self.enable_teacache != enable_teacache:
+
+ pbar = ProgressBar(6)
+
+ snapshot_download(
+ repo_id="declare-lab/TangoFlux",
+ allow_patterns=["*.json", "*.safetensors"],
+ local_dir=tangoflux_path,
+ local_dir_use_symlinks=False,
+ )
+
+ pbar.update(1)
+
+ log.info("Loading config")
+
+ with open(os.path.join(tangoflux_path, "config.json"), "r") as f:
+ config = json.load(f)
+
+ pbar.update(1)
+
+ text_encoder = re.sub(
+ r'[<>:"/\\|?*]',
+ "-",
+ config.get("text_encoder_name", "google/flan-t5-large"),
+ )
+ text_encoder_path = os.path.join(text_encoder_path, text_encoder)
+
+ snapshot_download(
+ repo_id=config.get("text_encoder_name", "google/flan-t5-large"),
+ allow_patterns=["*.json", "*.safetensors", "*.model"],
+ local_dir=text_encoder_path,
+ local_dir_use_symlinks=False,
+ )
+
+ pbar.update(1)
+
+ log.info("Loading TangoFlux models")
+
+ del self.model
+ self.model = None
+
+ model_weights = load_torch_file(
+ os.path.join(tangoflux_path, "tangoflux.safetensors"),
+ device=torch.device(device),
+ )
+
+ pbar.update(1)
+
+ if enable_teacache:
+ log.info("Enabling TeaCache")
+ FluxTransformer2DModel.forward = teacache_forward
+ else:
+ log.info("Disabling TeaCache")
+ FluxTransformer2DModel.forward = self.original_forward
+
+ model = TangoFlux(config=config, text_encoder_dir=text_encoder_path)
+
+ model.load_state_dict(model_weights, strict=False)
+ model.to(device)
+
+ if enable_teacache:
+ model.transformer.__class__.enable_teacache = True
+ model.transformer.__class__.cnt = 0
+ model.transformer.__class__.rel_l1_thresh = rel_l1_thresh
+ model.transformer.__class__.accumulated_rel_l1_distance = 0
+ model.transformer.__class__.previous_modulated_input = None
+ model.transformer.__class__.previous_residual = None
+
+ pbar.update(1)
+
+ self.model = model
+ del model
+ self.enable_teacache = enable_teacache
+ self.rel_l1_thresh = rel_l1_thresh
+
+ if self.vae is None:
+ log.info("Loading TangoFlux VAE")
+
+ vae_weights = load_torch_file(
+ os.path.join(tangoflux_path, "vae.safetensors")
+ )
+ self.vae = AutoencoderOobleck()
+ self.vae.load_state_dict(vae_weights)
+ self.vae.to(device)
+
+ pbar.update(1)
+
+ if self.enable_teacache == True and self.rel_l1_thresh != rel_l1_thresh:
+ self.model.transformer.__class__.rel_l1_thresh = rel_l1_thresh
+
+ self.rel_l1_thresh = rel_l1_thresh
+
+ return (self.model, self.vae)
+
+
+class TangoFluxSampler:
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {
+ "required": {
+ "model": ("TANGOFLUX_MODEL",),
+ "prompt": ("STRING", {"multiline": True, "dynamicPrompts": True}),
+ "steps": ("INT", {"default": 50, "min": 1, "max": 10000, "step": 1}),
+ "guidance_scale": (
+ "FLOAT",
+ {"default": 3, "min": 1, "max": 100, "step": 1},
+ ),
+ "duration": ("INT", {"default": 10, "min": 1, "max": 30, "step": 1}),
+ "seed": ("INT", {"default": 0, "min": 0, "max": 0xFFFFFFFFFFFFFFFF}),
+ "batch_size": ("INT", {"default": 1, "min": 1, "max": 4096}),
+ },
+ }
+
+ RETURN_TYPES = ("TANGOFLUX_LATENTS",)
+ RETURN_NAMES = ("latents",)
+ OUTPUT_TOOLTIPS = "TangoFlux Sample"
+
+ CATEGORY = "TangoFlux"
+ FUNCTION = "sample"
+ DESCRIPTION = "Sampler for TangoFlux"
+
+ def sample(
+ self,
+ model,
+ prompt,
+ steps=50,
+ guidance_scale=3,
+ duration=10,
+ seed=0,
+ batch_size=1,
+ device="cuda",
+ ):
+ pbar = ProgressBar(steps)
+
+ with torch.no_grad():
+ model.to(device)
+
+ try:
+ if model.transformer.__class__.enable_teacache:
+ model.transformer.__class__.num_steps = steps
+ except:
+ pass
+
+ log.info("Generating latents with TangoFlux")
+
+ latents = model.inference_flow(
+ prompt,
+ duration=duration,
+ num_inference_steps=steps,
+ guidance_scale=guidance_scale,
+ seed=seed,
+ num_samples_per_prompt=batch_size,
+ callback_on_step_end=lambda: pbar.update(1),
+ )
+
+ return ({"latents": latents, "duration": duration},)
+
+
+class TangoFluxVAEDecodeAndPlay:
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {
+ "required": {
+ "vae": ("TANGOFLUX_VAE",),
+ "latents": ("TANGOFLUX_LATENTS",),
+ "filename_prefix": ("STRING", {"default": "TangoFlux"}),
+ "format": (
+ ["wav", "mp3", "flac", "aac", "wma"],
+ {"default": "wav"},
+ ),
+ "save_output": ("BOOLEAN", {"default": True}),
+ },
+ }
+
+ RETURN_TYPES = ()
+ OUTPUT_NODE = True
+
+ CATEGORY = "TangoFlux"
+ FUNCTION = "play"
+ DESCRIPTION = "Decoder and Player for TangoFlux"
+
+ def decode(self, vae, latents):
+ results = []
+
+ for latent in latents:
+ decoded = vae.decode(latent.unsqueeze(0).transpose(2, 1)).sample.cpu()
+ results.append(decoded)
+
+ results = torch.cat(results, dim=0)
+
+ return results
+
+ def play(
+ self,
+ vae,
+ latents,
+ filename_prefix="TangoFlux",
+ format="wav",
+ save_output=True,
+ device="cuda",
+ ):
+ audios = []
+ pbar = ProgressBar(len(latents) + 2)
+
+ if save_output:
+ output_dir = folder_paths.get_output_directory()
+ prefix_append = ""
+ type = "output"
+ else:
+ output_dir = folder_paths.get_temp_directory()
+ prefix_append = "_temp_" + "".join(
+ random.choice("abcdefghijklmnopqrstupvxyz") for _ in range(5)
+ )
+ type = "temp"
+
+ filename_prefix += prefix_append
+ full_output_folder, filename, counter, subfolder, _ = (
+ folder_paths.get_save_image_path(filename_prefix, output_dir)
+ )
+
+ os.makedirs(full_output_folder, exist_ok=True)
+
+ pbar.update(1)
+
+ duration = latents["duration"]
+ latents = latents["latents"]
+
+ vae.to(device)
+
+ log.info("Decoding Tangoflux latents")
+
+ waves = self.decode(vae, latents)
+
+ pbar.update(1)
+
+ for wave in waves:
+ waveform_end = int(duration * vae.config.sampling_rate)
+ wave = wave[:, :waveform_end]
+
+ file = f"{filename}_{counter:05}_.{format}"
+
+ torchaudio.save(
+ os.path.join(full_output_folder, file), wave, sample_rate=44100
+ )
+
+ counter += 1
+
+ audios.append({"filename": file, "subfolder": subfolder, "type": type})
+
+ pbar.update(1)
+
+ return {
+ "ui": {"audios": audios},
+ }
+
+
+NODE_CLASS_MAPPINGS = {
+ "TangoFluxLoader": TangoFluxLoader,
+ "TangoFluxSampler": TangoFluxSampler,
+ "TangoFluxVAEDecodeAndPlay": TangoFluxVAEDecodeAndPlay,
+}
diff --git a/external_models/TangoFlux/comfyui/requirements.txt b/external_models/TangoFlux/comfyui/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..94fe2aa1c556ae35fd47fdb28c12c0057b21664d
--- /dev/null
+++ b/external_models/TangoFlux/comfyui/requirements.txt
@@ -0,0 +1,9 @@
+torchaudio
+torchlibrosa
+torchvision
+diffusers
+accelerate
+datasets
+librosa
+wandb
+tqdm
\ No newline at end of file
diff --git a/external_models/TangoFlux/comfyui/server.py b/external_models/TangoFlux/comfyui/server.py
new file mode 100644
index 0000000000000000000000000000000000000000..18ea81cecaf80ca0695d92b19c460da977ba636f
--- /dev/null
+++ b/external_models/TangoFlux/comfyui/server.py
@@ -0,0 +1,64 @@
+import os
+import server
+import folder_paths
+
+web = server.web
+
+
+@server.PromptServer.instance.routes.get("/tangoflux/playaudio")
+async def play_audio(request):
+ query = request.rel_url.query
+
+ filename = query.get("filename", None)
+
+ if filename is None:
+ return web.Response(status=404)
+
+ if filename[0] == "/" or ".." in filename:
+ return web.Response(status=403)
+
+ filename, output_dir = folder_paths.annotated_filepath(filename)
+
+ if not output_dir:
+ file_type = query.get("type", "output")
+ output_dir = folder_paths.get_directory_by_type(file_type)
+
+ if output_dir is None:
+ return web.Response(status=400)
+
+ subfolder = query.get("subfolder", None)
+ if subfolder:
+ full_output_dir = os.path.join(output_dir, subfolder)
+ if os.path.commonpath((os.path.abspath(full_output_dir), output_dir)) != output_dir:
+ return web.Response(status=403)
+ output_dir = full_output_dir
+
+ filename = os.path.basename(filename)
+ file_path = os.path.join(output_dir, filename)
+
+ if not os.path.isfile(file_path):
+ return web.Response(status=404)
+
+ _, ext = os.path.splitext(filename)
+ ext = ext.lower()
+
+ content_types = {
+ ".wav": "audio/wav",
+ ".mp3": "audio/mpeg",
+ ".flac": "audio/flac",
+ ".aac": "audio/aac",
+ ".wma": "audio/x-ms-wma",
+ }
+
+ content_type = content_types.get(ext, None)
+
+ if content_type is None:
+ return web.Response(status=400)
+
+ try:
+ with open(file_path, "rb") as file:
+ data = file.read()
+ except:
+ return web.Response(status=500)
+
+ return web.Response(body=data, content_type=content_type)
diff --git a/external_models/TangoFlux/comfyui/teacache.py b/external_models/TangoFlux/comfyui/teacache.py
new file mode 100644
index 0000000000000000000000000000000000000000..96ed7bd4d2b8c134b722690a8761d9dcd43aff39
--- /dev/null
+++ b/external_models/TangoFlux/comfyui/teacache.py
@@ -0,0 +1,283 @@
+# Code from https://github.com/ali-vilab/TeaCache/blob/main/TeaCache4TangoFlux/teacache_tango_flux.py
+
+from typing import Any, Dict, Optional, Union
+from diffusers.models.modeling_outputs import Transformer2DModelOutput
+from diffusers.utils import (
+ USE_PEFT_BACKEND,
+ is_torch_version,
+ logging,
+ scale_lora_layers,
+ unscale_lora_layers,
+)
+import torch
+import numpy as np
+
+
+logger = logging.get_logger(__name__) # pylint: disable=invalid-name
+
+
+def teacache_forward(
+ self,
+ hidden_states: torch.Tensor,
+ encoder_hidden_states: torch.Tensor = None,
+ pooled_projections: torch.Tensor = None,
+ timestep: torch.LongTensor = None,
+ img_ids: torch.Tensor = None,
+ txt_ids: torch.Tensor = None,
+ guidance: torch.Tensor = None,
+ joint_attention_kwargs: Optional[Dict[str, Any]] = None,
+ return_dict: bool = True,
+) -> Union[torch.FloatTensor, Transformer2DModelOutput]:
+ """
+ The [`FluxTransformer2DModel`] forward method.
+
+ Args:
+ hidden_states (`torch.FloatTensor` of shape `(batch size, channel, height, width)`):
+ Input `hidden_states`.
+ encoder_hidden_states (`torch.FloatTensor` of shape `(batch size, sequence_len, embed_dims)`):
+ Conditional embeddings (embeddings computed from the input conditions such as prompts) to use.
+ pooled_projections (`torch.FloatTensor` of shape `(batch_size, projection_dim)`): Embeddings projected
+ from the embeddings of input conditions.
+ timestep ( `torch.LongTensor`):
+ Used to indicate denoising step.
+ block_controlnet_hidden_states: (`list` of `torch.Tensor`):
+ A list of tensors that if specified are added to the residuals of transformer blocks.
+ joint_attention_kwargs (`dict`, *optional*):
+ A kwargs dictionary that if specified is passed along to the `AttentionProcessor` as defined under
+ `self.processor` in
+ [diffusers.models.attention_processor](https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/attention_processor.py).
+ return_dict (`bool`, *optional*, defaults to `True`):
+ Whether or not to return a [`~models.transformer_2d.Transformer2DModelOutput`] instead of a plain
+ tuple.
+
+ Returns:
+ If `return_dict` is True, an [`~models.transformer_2d.Transformer2DModelOutput`] is returned, otherwise a
+ `tuple` where the first element is the sample tensor.
+ """
+ if joint_attention_kwargs is not None:
+ joint_attention_kwargs = joint_attention_kwargs.copy()
+ lora_scale = joint_attention_kwargs.pop("scale", 1.0)
+ else:
+ lora_scale = 1.0
+
+ if USE_PEFT_BACKEND:
+ # weight the lora layers by setting `lora_scale` for each PEFT layer
+ scale_lora_layers(self, lora_scale)
+ else:
+ if (
+ joint_attention_kwargs is not None
+ and joint_attention_kwargs.get("scale", None) is not None
+ ):
+ logger.warning(
+ "Passing `scale` via `joint_attention_kwargs` when not using the PEFT backend is ineffective."
+ )
+ hidden_states = self.x_embedder(hidden_states)
+
+ timestep = timestep.to(hidden_states.dtype) * 1000
+ if guidance is not None:
+ guidance = guidance.to(hidden_states.dtype) * 1000
+ else:
+ guidance = None
+ temb = (
+ self.time_text_embed(timestep, pooled_projections)
+ if guidance is None
+ else self.time_text_embed(timestep, guidance, pooled_projections)
+ )
+ encoder_hidden_states = self.context_embedder(encoder_hidden_states)
+
+ ids = torch.cat((txt_ids, img_ids), dim=1)
+ image_rotary_emb = self.pos_embed(ids)
+
+ if self.enable_teacache:
+ inp = hidden_states.clone()
+ temb_ = temb.clone()
+ modulated_inp, gate_msa, shift_mlp, scale_mlp, gate_mlp = (
+ self.transformer_blocks[0].norm1(inp, emb=temb_)
+ )
+ if self.cnt == 0 or self.cnt == self.num_steps - 1:
+ should_calc = True
+ self.accumulated_rel_l1_distance = 0
+ else:
+ coefficients = [
+ 4.98651651e02,
+ -2.83781631e02,
+ 5.58554382e01,
+ -3.82021401e00,
+ 2.64230861e-01,
+ ]
+ rescale_func = np.poly1d(coefficients)
+ self.accumulated_rel_l1_distance += rescale_func(
+ (
+ (modulated_inp - self.previous_modulated_input).abs().mean()
+ / self.previous_modulated_input.abs().mean()
+ )
+ .cpu()
+ .item()
+ )
+ if self.accumulated_rel_l1_distance < self.rel_l1_thresh:
+ should_calc = False
+ else:
+ should_calc = True
+ self.accumulated_rel_l1_distance = 0
+ self.previous_modulated_input = modulated_inp
+ self.cnt += 1
+ if self.cnt == self.num_steps:
+ self.cnt = 0
+
+ if self.enable_teacache:
+ if not should_calc:
+ hidden_states += self.previous_residual
+ else:
+ ori_hidden_states = hidden_states.clone()
+ for index_block, block in enumerate(self.transformer_blocks):
+ if self.training and self.gradient_checkpointing:
+
+ def create_custom_forward(module, return_dict=None):
+ def custom_forward(*inputs):
+ if return_dict is not None:
+ return module(*inputs, return_dict=return_dict)
+ else:
+ return module(*inputs)
+
+ return custom_forward
+
+ ckpt_kwargs: Dict[str, Any] = (
+ {"use_reentrant": False}
+ if is_torch_version(">=", "1.11.0")
+ else {}
+ )
+ encoder_hidden_states, hidden_states = (
+ torch.utils.checkpoint.checkpoint(
+ create_custom_forward(block),
+ hidden_states,
+ encoder_hidden_states,
+ temb,
+ image_rotary_emb,
+ **ckpt_kwargs,
+ )
+ )
+
+ else:
+ encoder_hidden_states, hidden_states = block(
+ hidden_states=hidden_states,
+ encoder_hidden_states=encoder_hidden_states,
+ temb=temb,
+ image_rotary_emb=image_rotary_emb,
+ )
+
+ hidden_states = torch.cat([encoder_hidden_states, hidden_states], dim=1)
+
+ for index_block, block in enumerate(self.single_transformer_blocks):
+ if self.training and self.gradient_checkpointing:
+
+ def create_custom_forward(module, return_dict=None):
+ def custom_forward(*inputs):
+ if return_dict is not None:
+ return module(*inputs, return_dict=return_dict)
+ else:
+ return module(*inputs)
+
+ return custom_forward
+
+ ckpt_kwargs: Dict[str, Any] = (
+ {"use_reentrant": False}
+ if is_torch_version(">=", "1.11.0")
+ else {}
+ )
+ hidden_states = torch.utils.checkpoint.checkpoint(
+ create_custom_forward(block),
+ hidden_states,
+ temb,
+ image_rotary_emb,
+ **ckpt_kwargs,
+ )
+
+ else:
+ hidden_states = block(
+ hidden_states=hidden_states,
+ temb=temb,
+ image_rotary_emb=image_rotary_emb,
+ )
+
+ hidden_states = hidden_states[:, encoder_hidden_states.shape[1] :, ...]
+ self.previous_residual = hidden_states - ori_hidden_states
+ else:
+ for index_block, block in enumerate(self.transformer_blocks):
+ if self.training and self.gradient_checkpointing:
+
+ def create_custom_forward(module, return_dict=None):
+ def custom_forward(*inputs):
+ if return_dict is not None:
+ return module(*inputs, return_dict=return_dict)
+ else:
+ return module(*inputs)
+
+ return custom_forward
+
+ ckpt_kwargs: Dict[str, Any] = (
+ {"use_reentrant": False} if is_torch_version(">=", "1.11.0") else {}
+ )
+ encoder_hidden_states, hidden_states = (
+ torch.utils.checkpoint.checkpoint(
+ create_custom_forward(block),
+ hidden_states,
+ encoder_hidden_states,
+ temb,
+ image_rotary_emb,
+ **ckpt_kwargs,
+ )
+ )
+
+ else:
+ encoder_hidden_states, hidden_states = block(
+ hidden_states=hidden_states,
+ encoder_hidden_states=encoder_hidden_states,
+ temb=temb,
+ image_rotary_emb=image_rotary_emb,
+ )
+
+ hidden_states = torch.cat([encoder_hidden_states, hidden_states], dim=1)
+
+ for index_block, block in enumerate(self.single_transformer_blocks):
+ if self.training and self.gradient_checkpointing:
+
+ def create_custom_forward(module, return_dict=None):
+ def custom_forward(*inputs):
+ if return_dict is not None:
+ return module(*inputs, return_dict=return_dict)
+ else:
+ return module(*inputs)
+
+ return custom_forward
+
+ ckpt_kwargs: Dict[str, Any] = (
+ {"use_reentrant": False} if is_torch_version(">=", "1.11.0") else {}
+ )
+ hidden_states = torch.utils.checkpoint.checkpoint(
+ create_custom_forward(block),
+ hidden_states,
+ temb,
+ image_rotary_emb,
+ **ckpt_kwargs,
+ )
+
+ else:
+ hidden_states = block(
+ hidden_states=hidden_states,
+ temb=temb,
+ image_rotary_emb=image_rotary_emb,
+ )
+
+ hidden_states = hidden_states[:, encoder_hidden_states.shape[1] :, ...]
+
+ hidden_states = self.norm_out(hidden_states, temb)
+ output = self.proj_out(hidden_states)
+
+ if USE_PEFT_BACKEND:
+ # remove `lora_scale` from each PEFT layer
+ unscale_lora_layers(self, lora_scale)
+
+ if not return_dict:
+ return (output,)
+
+ return Transformer2DModelOutput(sample=output)
diff --git a/external_models/TangoFlux/comfyui/web/js/playAudio.js b/external_models/TangoFlux/comfyui/web/js/playAudio.js
new file mode 100644
index 0000000000000000000000000000000000000000..24aa3791943a613f9229ff443bf47387015724ee
--- /dev/null
+++ b/external_models/TangoFlux/comfyui/web/js/playAudio.js
@@ -0,0 +1,59 @@
+import { app } from "../../../scripts/app.js";
+import { api } from "../../../scripts/api.js";
+
+app.registerExtension({
+ name: "TangoFlux.playAudio",
+ async beforeRegisterNodeDef(nodeType, nodeData, app) {
+ if (nodeData.name === "TangoFluxVAEDecodeAndPlay") {
+ const originalNodeCreated = nodeType.prototype.onNodeCreated;
+
+ nodeType.prototype.onNodeCreated = async function () {
+ originalNodeCreated?.apply(this, arguments);
+ this.widgets_count = this.widgets?.length || 0;
+
+ this.addAudioWidgets = (audios) => {
+ if (this.widgets) {
+ for (let i = 0; i < this.widgets.length; i++) {
+ if (this.widgets[i].name.startsWith("_playaudio")) {
+ this.widgets[i].onRemove?.();
+ }
+ }
+ this.widgets.length = this.widgets_count;
+ }
+
+ let index = 0
+ for (const params of audios) {
+ const audioElement = document.createElement("audio");
+ audioElement.controls = true;
+
+ this.addDOMWidget("_playaudio" + index, "playaudio", audioElement, {
+ serialize: false,
+ hideOnZoom: false,
+ });
+ audioElement.src = api.apiURL(
+ `/tangoflux/playaudio?${new URLSearchParams(params)}`
+ );
+ index++
+ }
+
+ requestAnimationFrame(() => {
+ const newSize = this.computeSize();
+ newSize[0] = Math.max(newSize[0], this.size[0]);
+ newSize[1] = Math.max(newSize[1], this.size[1]);
+ this.onResize?.(newSize);
+ app.graph.setDirtyCanvas(true, false);
+ });
+ };
+ };
+
+ const originalNodeExecuted = nodeType.prototype.onExecuted;
+
+ nodeType.prototype.onExecuted = async function (message) {
+ originalNodeExecuted?.apply(this, arguments);
+ if (message?.audios) {
+ this.addAudioWidgets(message.audios);
+ }
+ };
+ }
+ },
+});
diff --git a/external_models/TangoFlux/configs/__init__.py b/external_models/TangoFlux/configs/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/external_models/TangoFlux/configs/accelerator_config.yaml b/external_models/TangoFlux/configs/accelerator_config.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..d9e14b2dc4bbdbf18f8c03a0e71962237dc4cc53
--- /dev/null
+++ b/external_models/TangoFlux/configs/accelerator_config.yaml
@@ -0,0 +1,17 @@
+{
+ "compute_environment": "LOCAL_MACHINE",
+ "distributed_type": "MULTI_GPU",
+ "main_process_port": 29512,
+ "downcast_bf16": false,
+ "machine_rank": 0,
+ "gpu_ids": "0,1",
+ "main_training_function": "main",
+ "mixed_precision": "no",
+ "num_machines": 1,
+ "num_processes": 2,
+ "rdzv_backend": "static",
+ "same_network": true,
+ "tpu_use_cluster": false,
+ "tpu_use_sudo": false,
+ "use_cpu": false
+}
\ No newline at end of file
diff --git a/external_models/TangoFlux/configs/tangoflux_config.yaml b/external_models/TangoFlux/configs/tangoflux_config.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..a77f6c1ebd03e7d05c1d34590bfec85a23372092
--- /dev/null
+++ b/external_models/TangoFlux/configs/tangoflux_config.yaml
@@ -0,0 +1,36 @@
+
+# Absolute paths for different resources
+paths:
+ train_file: "data/train.json"
+ val_file: "data/val.json"
+ test_file: "data/val.json"
+ resume_from_checkpoint: ""
+ output_dir: "outputs/"
+
+# Training-related parameters
+training:
+ per_device_batch_size: 4
+ learning_rate: 5e-4
+ gradient_accumulation_steps: 1
+ num_train_epochs: 80
+ num_warmup_steps: 1000
+ max_audio_duration: 30
+
+
+# Model and optimizer parameters,
+model:
+ num_layers: 6
+ num_single_layers: 18
+ in_channels: 64
+ attention_head_dim: 128
+ joint_attention_dim: 1024
+ num_attention_heads: 8
+ audio_seq_len: 645
+ max_duration: 30
+ uncondition: false
+ text_encoder_name: "google/flan-t5-large"
+
+
+
+
+
diff --git a/external_models/TangoFlux/crpo.sh b/external_models/TangoFlux/crpo.sh
new file mode 100644
index 0000000000000000000000000000000000000000..122fecffeb12c7fdc60509b542b2e47c740eff00
--- /dev/null
+++ b/external_models/TangoFlux/crpo.sh
@@ -0,0 +1,2 @@
+python3 tangoflux/generate_crpo.py --json_path='path_to_prompt_bank.json' --sample_size=50 --model='path_to_tangoflux.safetensors' --num_samples=5 --output_dir='outputs'
+python3 tangoflux/label_crpo.py --json_path='outputs/results.json' --output_dir='outputs/crpo_iteration1' --num_samples=5
\ No newline at end of file
diff --git a/external_models/TangoFlux/inference.py b/external_models/TangoFlux/inference.py
new file mode 100644
index 0000000000000000000000000000000000000000..2c3f4a53fb72d402293711db3336f2108a6af851
--- /dev/null
+++ b/external_models/TangoFlux/inference.py
@@ -0,0 +1,7 @@
+import torchaudio
+from tangoflux import TangoFluxInference
+
+model = TangoFluxInference(name="declare-lab/TangoFlux")
+audio = model.generate("Hammer slowly hitting the wooden table", steps=50, duration=10)
+
+torchaudio.save("output.wav", audio, sample_rate=44100)
diff --git a/external_models/TangoFlux/replicate_demo/cog.yaml b/external_models/TangoFlux/replicate_demo/cog.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..c5cb4ca032a57a14def21075ac9436486695d188
--- /dev/null
+++ b/external_models/TangoFlux/replicate_demo/cog.yaml
@@ -0,0 +1,31 @@
+# Configuration for Cog ⚙️
+# Reference: https://cog.run/yaml
+
+build:
+ # set to true if your model requires a GPU
+ gpu: true
+
+ # a list of ubuntu apt packages to install
+ system_packages:
+ - "libgl1-mesa-glx"
+ - "libglib2.0-0"
+
+ # python version in the form '3.11' or '3.11.4'
+ python_version: "3.11"
+
+ # a list of packages in the format ==
+ python_packages:
+ - torch==2.4.0
+ - torchaudio==2.4.0
+ - torchlibrosa==0.1.0
+ - torchvision==0.19.0
+ - transformers==4.44.0
+ - diffusers==0.30.0
+ - accelerate==0.34.2
+ - datasets==2.21.0
+ - librosa
+ - ipython
+
+ run:
+ - curl -o /usr/local/bin/pget -L "https://github.com/replicate/pget/releases/download/v0.6.0/pget_linux_x86_64" && chmod +x /usr/local/bin/pget
+predict: "predict.py:Predictor"
diff --git a/external_models/TangoFlux/replicate_demo/predict.py b/external_models/TangoFlux/replicate_demo/predict.py
new file mode 100644
index 0000000000000000000000000000000000000000..8dcf5e473c5c9fb7f1b8d79b3419637993573c47
--- /dev/null
+++ b/external_models/TangoFlux/replicate_demo/predict.py
@@ -0,0 +1,92 @@
+# Prediction interface for Cog ⚙️
+# https://cog.run/python
+
+import os
+import subprocess
+import time
+import json
+from cog import BasePredictor, Input, Path
+from diffusers import AutoencoderOobleck
+import soundfile as sf
+from safetensors.torch import load_file
+from huggingface_hub import snapshot_download
+from tangoflux.model import TangoFlux
+from tangoflux import TangoFluxInference
+
+MODEL_CACHE = "model_cache"
+MODEL_URL = (
+ "https://weights.replicate.delivery/default/declare-lab/TangoFlux/model_cache.tar"
+)
+
+
+class CachedTangoFluxInference(TangoFluxInference):
+ ## load the weights from replicate.delivery for faster booting
+ def __init__(self, name="declare-lab/TangoFlux", device="cuda", cached_paths=None):
+ if cached_paths:
+ paths = cached_paths
+ else:
+ paths = snapshot_download(repo_id=name)
+
+ self.vae = AutoencoderOobleck()
+ vae_weights = load_file(f"{paths}/vae.safetensors")
+ self.vae.load_state_dict(vae_weights)
+ weights = load_file(f"{paths}/tangoflux.safetensors")
+
+ with open(f"{paths}/config.json", "r") as f:
+ config = json.load(f)
+ self.model = TangoFlux(config)
+ self.model.load_state_dict(weights, strict=False)
+ self.vae.to(device)
+ self.model.to(device)
+
+
+def download_weights(url, dest):
+ start = time.time()
+ print("downloading url: ", url)
+ print("downloading to: ", dest)
+ subprocess.check_call(["pget", "-x", url, dest], close_fds=False)
+ print("downloading took: ", time.time() - start)
+
+
+class Predictor(BasePredictor):
+ def setup(self) -> None:
+ """Load the model into memory to make running multiple predictions efficient"""
+
+ if not os.path.exists(MODEL_CACHE):
+ print("downloading")
+ download_weights(MODEL_URL, MODEL_CACHE)
+
+ self.model = CachedTangoFluxInference(
+ cached_paths=f"{MODEL_CACHE}/declare-lab/TangoFlux"
+ )
+
+ def predict(
+ self,
+ prompt: str = Input(
+ description="Input prompt", default="Hammer slowly hitting the wooden table"
+ ),
+ duration: int = Input(
+ description="Duration of the output audio in seconds", default=10
+ ),
+ steps: int = Input(
+ description="Number of inference steps", ge=1, le=200, default=25
+ ),
+ guidance_scale: float = Input(
+ description="Scale for classifier-free guidance", ge=1, le=20, default=4.5
+ ),
+ ) -> Path:
+ """Run a single prediction on the model"""
+
+ audio = self.model.generate(
+ prompt,
+ steps=steps,
+ guidance_scale=guidance_scale,
+ duration=duration,
+ )
+ audio_numpy = audio.numpy()
+ out_path = "/tmp/out.wav"
+
+ sf.write(
+ out_path, audio_numpy.T, samplerate=self.model.vae.config.sampling_rate
+ )
+ return Path(out_path)
diff --git a/external_models/TangoFlux/requirements.txt b/external_models/TangoFlux/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..d1edc250660e03a58923f7f8eaf6898a8b08cf17
--- /dev/null
+++ b/external_models/TangoFlux/requirements.txt
@@ -0,0 +1,12 @@
+torch==2.4.0
+torchaudio==2.4.0
+torchlibrosa==0.1.0
+torchvision==0.19.0
+transformers==4.44.0
+diffusers==0.30.0
+accelerate==0.34.2
+datasets==2.21.0
+librosa
+tqdm
+wandb
+
diff --git a/external_models/TangoFlux/setup.py b/external_models/TangoFlux/setup.py
new file mode 100644
index 0000000000000000000000000000000000000000..6ae8914365641f29810fef5fe9e57ff3f20e6348
--- /dev/null
+++ b/external_models/TangoFlux/setup.py
@@ -0,0 +1,30 @@
+from setuptools import setup
+
+setup(
+ name="tangoflux",
+ description="TangoFlux: Super Fast and Faithful Text to Audio Generation with Flow Matching",
+ version="0.1.0",
+ packages=["tangoflux"],
+ install_requires=[
+ "torch==2.4.0",
+ "torchaudio==2.4.0",
+ "torchlibrosa==0.1.0",
+ "torchvision==0.19.0",
+ "transformers==4.44.0",
+ "diffusers==0.30.0",
+ "accelerate==0.34.2",
+ "datasets==2.21.0",
+ "librosa",
+ "tqdm",
+ "wandb",
+ "click",
+ "gradio",
+ "torchaudio",
+ ],
+ entry_points={
+ "console_scripts": [
+ "tangoflux=tangoflux.cli:main",
+ "tangoflux-demo=tangoflux.demo:main",
+ ],
+ },
+)
diff --git a/external_models/TangoFlux/tangoflux/__init__.py b/external_models/TangoFlux/tangoflux/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..58424baf29135f473be583b1c8e13f85e52f9037
--- /dev/null
+++ b/external_models/TangoFlux/tangoflux/__init__.py
@@ -0,0 +1,60 @@
+from diffusers import AutoencoderOobleck
+import torch
+from transformers import T5EncoderModel, T5TokenizerFast
+from diffusers import FluxTransformer2DModel
+from torch import nn
+from typing import List
+from diffusers import FlowMatchEulerDiscreteScheduler
+from diffusers.training_utils import compute_density_for_timestep_sampling
+import copy
+import torch.nn.functional as F
+import numpy as np
+from tangoflux.model import TangoFlux
+from huggingface_hub import snapshot_download
+from tqdm import tqdm
+from typing import Optional, Union, List
+from datasets import load_dataset, Audio
+from math import pi
+import json
+import inspect
+import yaml
+from safetensors.torch import load_file
+
+
+class TangoFluxInference:
+
+ def __init__(
+ self,
+ name="declare-lab/TangoFlux",
+ device="cuda" if torch.cuda.is_available() else "cpu",
+ ):
+
+ self.vae = AutoencoderOobleck()
+
+ paths = snapshot_download(repo_id=name)
+ vae_weights = load_file("{}/vae.safetensors".format(paths))
+ self.vae.load_state_dict(vae_weights)
+ weights = load_file("{}/tangoflux.safetensors".format(paths))
+
+ with open("{}/config.json".format(paths), "r") as f:
+ config = json.load(f)
+ self.model = TangoFlux(config)
+ self.model.load_state_dict(weights, strict=False)
+ # _IncompatibleKeys(missing_keys=['text_encoder.encoder.embed_tokens.weight'], unexpected_keys=[]) this behaviour is expected
+ self.vae.to(device)
+ self.model.to(device)
+
+ def generate(self, prompt, steps=25, duration=10, guidance_scale=4.5):
+
+ with torch.no_grad():
+ latents = self.model.inference_flow(
+ prompt,
+ duration=duration,
+ num_inference_steps=steps,
+ guidance_scale=guidance_scale,
+ )
+
+ wave = self.vae.decode(latents.transpose(2, 1)).sample.cpu()[0]
+ waveform_end = int(duration * self.vae.config.sampling_rate)
+ wave = wave[:, :waveform_end]
+ return wave
diff --git a/external_models/TangoFlux/tangoflux/cli.py b/external_models/TangoFlux/tangoflux/cli.py
new file mode 100644
index 0000000000000000000000000000000000000000..ad26adfd077e4fc1feb8ea76c88412a80c13704e
--- /dev/null
+++ b/external_models/TangoFlux/tangoflux/cli.py
@@ -0,0 +1,29 @@
+import click
+import torchaudio
+from tangoflux import TangoFluxInference
+
+@click.command()
+@click.argument('prompt')
+@click.argument('output_file')
+@click.option('--duration', default=10, type=int, help='Duration in seconds (1-30)')
+@click.option('--steps', default=50, type=int, help='Number of inference steps (10-100)')
+def main(prompt: str, output_file: str, duration: int, steps: int):
+ """Generate audio from text using TangoFlux.
+
+ Args:
+ prompt: Text description of the audio to generate
+ output_file: Path to save the generated audio file
+ duration: Duration of generated audio in seconds (default: 10)
+ steps: Number of inference steps (default: 50)
+ """
+ if not 1 <= duration <= 30:
+ raise click.BadParameter('Duration must be between 1 and 30 seconds')
+ if not 10 <= steps <= 100:
+ raise click.BadParameter('Steps must be between 10 and 100')
+
+ model = TangoFluxInference(name="declare-lab/TangoFlux")
+ audio = model.generate(prompt, steps=steps, duration=duration)
+ torchaudio.save(output_file, audio, sample_rate=44100)
+
+if __name__ == '__main__':
+ main()
diff --git a/external_models/TangoFlux/tangoflux/demo.py b/external_models/TangoFlux/tangoflux/demo.py
new file mode 100644
index 0000000000000000000000000000000000000000..aaa36ead512bcd7bca8d1788fe5ddfa4ff879fce
--- /dev/null
+++ b/external_models/TangoFlux/tangoflux/demo.py
@@ -0,0 +1,63 @@
+import gradio as gr
+import torchaudio
+import click
+import tempfile
+from tangoflux import TangoFluxInference
+
+model = TangoFluxInference(name="declare-lab/TangoFlux")
+
+
+def generate_audio(prompt, duration, steps):
+ audio = model.generate(prompt, steps=steps, duration=duration)
+ with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
+ torchaudio.save(f.name, audio, sample_rate=44100)
+ return f.name
+
+
+examples = [
+ ["Hammer slowly hitting the wooden table", 10, 50],
+ ["Gentle rain falling on a tin roof", 15, 50],
+ ["Wind chimes tinkling in a light breeze", 10, 50],
+ ["Rhythmic wooden table tapping overlaid with steady water pouring sound", 10, 50],
+]
+
+with gr.Blocks(title="TangoFlux Text-to-Audio Generation") as demo:
+ gr.Markdown("# TangoFlux Text-to-Audio Generation")
+ gr.Markdown("Generate audio from text descriptions using TangoFlux")
+
+ with gr.Row():
+ with gr.Column():
+ prompt = gr.Textbox(
+ label="Text Prompt", placeholder="Enter your audio description..."
+ )
+ duration = gr.Slider(
+ minimum=1, maximum=30, value=10, step=1, label="Duration (seconds)"
+ )
+ steps = gr.Slider(
+ minimum=10, maximum=100, value=50, step=10, label="Number of Steps"
+ )
+ generate_btn = gr.Button("Generate Audio")
+
+ with gr.Column():
+ audio_output = gr.Audio(label="Generated Audio")
+
+ generate_btn.click(
+ fn=generate_audio, inputs=[prompt, duration, steps], outputs=audio_output
+ )
+
+ gr.Examples(
+ examples=examples,
+ inputs=[prompt, duration, steps],
+ outputs=audio_output,
+ fn=generate_audio,
+ )
+
+@click.command()
+@click.option('--host', default='127.0.0.1', help='Host to bind to')
+@click.option('--port', default=None, help='Port to bind to')
+@click.option('--share', is_flag=True, help='Enable sharing via Gradio')
+def main(host, port, share):
+ demo.queue().launch(server_name=host, server_port=port, share=share)
+
+if __name__ == "__main__":
+ main()
diff --git a/external_models/TangoFlux/tangoflux/generate_crpo_dataset.py b/external_models/TangoFlux/tangoflux/generate_crpo_dataset.py
new file mode 100644
index 0000000000000000000000000000000000000000..d089c33a9cd60afc20608ce11ef5a1bbb7197641
--- /dev/null
+++ b/external_models/TangoFlux/tangoflux/generate_crpo_dataset.py
@@ -0,0 +1,204 @@
+import os
+import json
+import time
+import torch
+import argparse
+import multiprocessing
+from tqdm import tqdm
+from safetensors.torch import load_file
+from diffusers import AutoencoderOobleck
+import soundfile as sf
+from model import TangoFlux
+import random
+
+
+
+
+def generate_audio_chunk(args, chunk, gpu_id, output_dir, samplerate, return_dict, process_id):
+ """
+ Function to generate audio for a chunk of text prompts on a specific GPU.
+ """
+ try:
+ device = f"cuda:{gpu_id}"
+ torch.cuda.set_device(device)
+ print(f"Process {process_id}: Using device {device}")
+
+ # Initialize model
+ config = {
+ 'num_layers': 6,
+ 'num_single_layers': 18,
+ 'in_channels': 64,
+ 'attention_head_dim': 128,
+ 'joint_attention_dim': 1024,
+ 'num_attention_heads': 8,
+ 'audio_seq_len': 645,
+ 'max_duration': 30,
+ 'uncondition': False,
+ 'text_encoder_name': "google/flan-t5-large"
+ }
+
+ model = TangoFlux(config)
+ print(f"Process {process_id}: Loading model from {args.model} on {device}")
+ w1 = load_file(args.model)
+ model.load_state_dict(w1, strict=False)
+ model = model.to(device)
+ model.eval()
+
+ # Initialize VAE
+ vae = AutoencoderOobleck.from_pretrained("stabilityai/stable-audio-open-1.0", subfolder='vae')
+ vae = vae.to(device)
+ vae.eval()
+
+ outputs = []
+
+ # Corrected loop using enumerate properly with tqdm
+ for idx, item in tqdm(enumerate(chunk), total=len(chunk), desc=f"GPU {gpu_id}"):
+ text = item['captions']
+
+
+ if os.path.exists(os.path.join(output_dir, f"id_{item['id']}_sample1.wav")):
+ print("Exist! Skipping!")
+ continue
+ with torch.no_grad():
+ latent = model.inference_flow(
+ text,
+ num_inference_steps=args.num_steps,
+ guidance_scale=args.guidance_scale,
+ duration=10,
+ num_samples_per_prompt=args.num_samples
+ )
+
+ #waveform_end = int(duration * vae.config.sampling_rate)
+ latent = latent[:, :220, :] ## 220 correspond to the latent length of audiocaps encoded with this vae. You can modify this
+ wave = vae.decode(latent.transpose(2, 1)).sample.cpu()
+
+ for i in range(args.num_samples):
+ filename = f"id_{item['id']}_sample{i+1}.wav"
+ filepath = os.path.join(output_dir, filename)
+
+ sf.write(filepath, wave[i].T, samplerate)
+ outputs.append({
+ "id": item['id'],
+ "sample": i + 1,
+ "path": filepath,
+ "captions": text
+ })
+
+ return_dict[process_id] = outputs
+ print(f"Process {process_id}: Completed processing on GPU {gpu_id}")
+
+ except Exception as e:
+ print(f"Process {process_id}: Error on GPU {gpu_id}: {e}")
+ return_dict[process_id] = []
+
+def split_into_chunks(data, num_chunks):
+ """
+ Splits data into num_chunks approximately equal parts.
+ """
+ avg = len(data) // num_chunks
+ chunks = []
+ for i in range(num_chunks):
+ start = i * avg
+ # Ensure the last chunk takes the remainder
+ end = (i + 1) * avg if i != num_chunks - 1 else len(data)
+ chunks.append(data[start:end])
+ return chunks
+
+def main():
+ parser = argparse.ArgumentParser(description="Generate audio using multiple GPUs")
+ parser.add_argument('--num_steps', type=int, default=50, help='Number of inference steps')
+ parser.add_argument('--model', type=str, required=True, help='Path to tangoflux weights')
+ parser.add_argument('--num_samples', type=int, default=5, help='Number of samples per prompt')
+ parser.add_argument('--output_dir', type=str, default='output', help='Directory to save outputs')
+ parser.add_argument('--json_path', type=str, required=True, help='Path to input JSON file')
+ parser.add_argument('--sample_size', type=int, default=20000, help='Number of prompts to sample for CRPO')
+ parser.add_argument('--guidance_scale', type=float, default=4.5, help='Guidance scale used for generation')
+ args = parser.parse_args()
+
+ # Check GPU availability
+ num_gpus = torch.cuda.device_count()
+ sample_size = args.sample_size
+
+
+ # Load JSON data
+ import json
+ try:
+ with open(args.json_path, 'r') as f:
+ data = json.load(f)
+
+ except Exception as e:
+ print(f"Error loading JSON file {args.json_path}: {e}")
+ return
+
+ if not isinstance(data, list):
+ print("Error: JSON data is not a list.")
+ return
+
+ if len(data) < sample_size:
+ print(f"Warning: JSON data contains only {len(data)} items. Sampling all available data.")
+ sampled = data
+ else:
+ sampled = random.sample(data, sample_size)
+
+ # Split data into chunks based on available GPUs
+ random.shuffle(sampled)
+ chunks = split_into_chunks(sampled, num_gpus)
+
+ # Prepare output directory
+ os.makedirs(args.output_dir, exist_ok=True)
+ samplerate = 44100
+
+ # Manager for inter-process communication
+ manager = multiprocessing.Manager()
+ return_dict = manager.dict()
+
+ processes = []
+ for i in range(num_gpus):
+ p = multiprocessing.Process(
+ target=generate_audio_chunk,
+ args=(
+ args,
+ chunks[i],
+ i, # GPU ID
+ args.output_dir,
+ samplerate,
+ return_dict,
+ i, # Process ID
+
+ )
+ )
+ processes.append(p)
+ p.start()
+ print(f"Started process {i} on GPU {i}")
+
+ for p in processes:
+ p.join()
+ print(f"Process {p.pid} has finished.")
+
+ # Aggregate results
+
+
+
+
+
+
+ audio_info_list = [
+ [{
+ "path": f"{args.output_dir}/id_{sampled[j]['id']}_sample{i}.wav",
+ "duration": sampled[j]["duration"],
+ "captions": sampled[j]["captions"]
+ }
+ for i in range(1, args.num_samples+1) ] for j in range(sample_size)
+ ]
+
+ #print(audio_info_list)
+
+ with open(f'{args.output_dir}/results.json','w') as f:
+ json.dump(audio_info_list,f)
+
+ print(f"All audio samples have been generated and saved to {args.output_dir}")
+
+
+if __name__ == "__main__":
+ multiprocessing.set_start_method('spawn')
+ main()
\ No newline at end of file
diff --git a/external_models/TangoFlux/tangoflux/label_crpo.py b/external_models/TangoFlux/tangoflux/label_crpo.py
new file mode 100644
index 0000000000000000000000000000000000000000..eb04f5ff24c4b78e543254d8989be6428546338a
--- /dev/null
+++ b/external_models/TangoFlux/tangoflux/label_crpo.py
@@ -0,0 +1,153 @@
+import os
+import json
+import argparse
+import torch
+import laion_clap
+import numpy as np
+import multiprocessing
+from tqdm import tqdm
+
+def parse_args():
+ parser = argparse.ArgumentParser(
+ description="Labelling clap score for crpo dataset"
+ )
+ parser.add_argument(
+ "--num_samples", type=int, default=5,
+ help="Number of audio samples per prompt"
+ )
+ parser.add_argument(
+ "--json_path", type=str, required=True,
+ help="Path to input JSON file"
+ )
+ parser.add_argument(
+ "--output_dir", type=str, required=True,
+ help="Directory to save the final JSON with CLAP scores"
+ )
+ return parser.parse_args()
+
+#python3 label_clap.py --json_path=/mnt/data/chiayu/crpo/crpo_iteration1/results.json --output_dir=/mnt/data/chiayu/crpo/crpo_iteration1
+@torch.no_grad()
+def compute_clap(model, audio_files, text_data):
+ # Compute audio and text embeddings, then compute the dot product (CLAP score)
+ audio_embed = model.get_audio_embedding_from_filelist(x=audio_files, use_tensor=True)
+ text_embed = model.get_text_embedding(text_data, use_tensor=True)
+ return audio_embed @ text_embed.T
+
+def process_chunk(args, chunk, gpu_id, return_dict, process_id):
+ """
+ Process a chunk of the data on a specific GPU.
+ Loads the CLAP model on the designated device, then for each item in the chunk,
+ computes the CLAP scores and attaches them to the data.
+ """
+ try:
+ device = f"cuda:{gpu_id}"
+ torch.cuda.set_device(device)
+ print(f"Process {process_id}: Using device {device}")
+
+ # Initialize the CLAP model on this GPU
+ model = laion_clap.CLAP_Module(enable_fusion=False)
+ model.to(device)
+ model.load_ckpt()
+ model.eval()
+
+ for j, item in enumerate(tqdm(chunk, desc=f"GPU {gpu_id}")):
+ # Each item is assumed to be a list of samples.
+ # Skip if already computed.
+ if 'clap_score' in item[0]:
+ continue
+
+ # Collect audio file paths and text data (using the first caption)
+ audio_files = [item[i]['path'] for i in range(args.num_samples)]
+ text_data = [item[0]['captions']]
+
+ try:
+ clap_scores = compute_clap(model, audio_files, text_data)
+ except Exception as e:
+ print(f"Error processing item index {j} on GPU {gpu_id}: {e}")
+ continue
+
+ # Attach the computed score to each sample in the item
+ for k in range(args.num_samples):
+ item[k]['clap_score'] = np.round(clap_scores[k].item(), 3)
+
+ return_dict[process_id] = chunk
+ print(f"Process {process_id}: Completed processing on GPU {gpu_id}")
+ except Exception as e:
+ print(f"Process {process_id}: Error on GPU {gpu_id}: {e}")
+ return_dict[process_id] = []
+
+def split_into_chunks(data, num_chunks):
+ """
+ Splits data into num_chunks approximately equal parts.
+ """
+ avg = len(data) // num_chunks
+ chunks = []
+ for i in range(num_chunks):
+ start = i * avg
+ # Ensure the last chunk takes the remainder of the data
+ end = (i + 1) * avg if i != num_chunks - 1 else len(data)
+ chunks.append(data[start:end])
+ return chunks
+
+def main():
+ args = parse_args()
+
+ # Load data from JSON and slice by start/end if provided
+ with open(args.json_path, 'r') as f:
+ data = json.load(f)
+
+ # Check GPU availability and split data accordingly
+ num_gpus = torch.cuda.device_count()
+
+ print(f"Found {num_gpus} GPUs. Splitting data into {num_gpus} chunks.")
+ chunks = split_into_chunks(data, num_gpus)
+
+ # Prepare output directory
+ os.makedirs(args.output_dir, exist_ok=True)
+
+ # Create a manager dict to collect results from all processes
+ manager = multiprocessing.Manager()
+ return_dict = manager.dict()
+ processes = []
+
+ for i in range(num_gpus):
+ p = multiprocessing.Process(
+ target=process_chunk,
+ args=(args, chunks[i], i, return_dict, i)
+ )
+ processes.append(p)
+ p.start()
+ print(f"Started process {i} on GPU {i}")
+
+ for p in processes:
+ p.join()
+ print(f"Process {p.pid} has finished.")
+
+ # Aggregate all chunks back into a single list
+ combined_data = []
+ for i in range(num_gpus):
+ combined_data.extend(return_dict[i])
+
+ # Save the combined results to a single JSON file
+ output_file = f"{args.output_dir}/clap_scores.json"
+ with open(output_file, 'w') as f:
+ json.dump(combined_data, f)
+ print(f"All CLAP scores have been computed and saved to {output_file}")
+
+ max_item = [max(x, key=lambda item: item['clap_score']) for x in combined_data]
+ min_item = [min(x, key=lambda item: item['clap_score']) for x in combined_data]
+
+ crpo_dataset = []
+ for chosen,reject in zip(max_item,min_item):
+ crpo_dataset.append({"captions": chosen['captions'],
+ "duration": chosen['duration'],
+ "chosen": chosen['path'],
+ "reject": reject['path']})
+
+ with open(f"{args.output_dir}/train.json",'w') as f:
+ json.dump(crpo_dataset,f)
+
+
+if __name__ == '__main__':
+ multiprocessing.set_start_method('spawn')
+ main()
diff --git a/external_models/TangoFlux/tangoflux/model.py b/external_models/TangoFlux/tangoflux/model.py
new file mode 100644
index 0000000000000000000000000000000000000000..8a137067061d7fdd5bf7508118d1da09ed4c8719
--- /dev/null
+++ b/external_models/TangoFlux/tangoflux/model.py
@@ -0,0 +1,556 @@
+from transformers import T5EncoderModel, T5TokenizerFast
+import torch
+from diffusers import FluxTransformer2DModel
+from torch import nn
+import random
+from typing import List
+from diffusers import FlowMatchEulerDiscreteScheduler
+from diffusers.training_utils import compute_density_for_timestep_sampling
+import copy
+import torch.nn.functional as F
+import numpy as np
+from tqdm import tqdm
+
+from typing import Optional, Union, List
+from datasets import load_dataset, Audio
+from math import pi
+import inspect
+import yaml
+
+
+class StableAudioPositionalEmbedding(nn.Module):
+ """Used for continuous time
+ Adapted from Stable Audio Open.
+ """
+
+ def __init__(self, dim: int):
+ super().__init__()
+ assert (dim % 2) == 0
+ half_dim = dim // 2
+ self.weights = nn.Parameter(torch.randn(half_dim))
+
+ def forward(self, times: torch.Tensor) -> torch.Tensor:
+ times = times[..., None]
+ freqs = times * self.weights[None] * 2 * pi
+ fouriered = torch.cat((freqs.sin(), freqs.cos()), dim=-1)
+ fouriered = torch.cat((times, fouriered), dim=-1)
+ return fouriered
+
+
+class DurationEmbedder(nn.Module):
+ """
+ A simple linear projection model to map numbers to a latent space.
+
+ Code is adapted from
+ https://github.com/Stability-AI/stable-audio-tools
+
+ Args:
+ number_embedding_dim (`int`):
+ Dimensionality of the number embeddings.
+ min_value (`int`):
+ The minimum value of the seconds number conditioning modules.
+ max_value (`int`):
+ The maximum value of the seconds number conditioning modules
+ internal_dim (`int`):
+ Dimensionality of the intermediate number hidden states.
+ """
+
+ def __init__(
+ self,
+ number_embedding_dim,
+ min_value,
+ max_value,
+ internal_dim: Optional[int] = 256,
+ ):
+ super().__init__()
+ self.time_positional_embedding = nn.Sequential(
+ StableAudioPositionalEmbedding(internal_dim),
+ nn.Linear(in_features=internal_dim + 1, out_features=number_embedding_dim),
+ )
+
+ self.number_embedding_dim = number_embedding_dim
+ self.min_value = min_value
+ self.max_value = max_value
+ self.dtype = torch.float32
+
+ def forward(
+ self,
+ floats: torch.Tensor,
+ ):
+ floats = floats.clamp(self.min_value, self.max_value)
+
+ normalized_floats = (floats - self.min_value) / (
+ self.max_value - self.min_value
+ )
+
+ # Cast floats to same type as embedder
+ embedder_dtype = next(self.time_positional_embedding.parameters()).dtype
+ normalized_floats = normalized_floats.to(embedder_dtype)
+
+ embedding = self.time_positional_embedding(normalized_floats)
+ float_embeds = embedding.view(-1, 1, self.number_embedding_dim)
+
+ return float_embeds
+
+
+def retrieve_timesteps(
+ scheduler,
+ num_inference_steps: Optional[int] = None,
+ device: Optional[Union[str, torch.device]] = None,
+ timesteps: Optional[List[int]] = None,
+ sigmas: Optional[List[float]] = None,
+ **kwargs,
+):
+
+ if timesteps is not None and sigmas is not None:
+ raise ValueError(
+ "Only one of `timesteps` or `sigmas` can be passed. Please choose one to set custom values"
+ )
+ if timesteps is not None:
+ accepts_timesteps = "timesteps" in set(
+ inspect.signature(scheduler.set_timesteps).parameters.keys()
+ )
+ if not accepts_timesteps:
+ raise ValueError(
+ f"The current scheduler class {scheduler.__class__}'s `set_timesteps` does not support custom"
+ f" timestep schedules. Please check whether you are using the correct scheduler."
+ )
+ scheduler.set_timesteps(timesteps=timesteps, device=device, **kwargs)
+ timesteps = scheduler.timesteps
+ num_inference_steps = len(timesteps)
+ elif sigmas is not None:
+ accept_sigmas = "sigmas" in set(
+ inspect.signature(scheduler.set_timesteps).parameters.keys()
+ )
+ if not accept_sigmas:
+ raise ValueError(
+ f"The current scheduler class {scheduler.__class__}'s `set_timesteps` does not support custom"
+ f" sigmas schedules. Please check whether you are using the correct scheduler."
+ )
+ scheduler.set_timesteps(sigmas=sigmas, device=device, **kwargs)
+ timesteps = scheduler.timesteps
+ num_inference_steps = len(timesteps)
+ else:
+ scheduler.set_timesteps(num_inference_steps, device=device, **kwargs)
+ timesteps = scheduler.timesteps
+ return timesteps, num_inference_steps
+
+
+class TangoFlux(nn.Module):
+
+ def __init__(self, config, text_encoder_dir=None, initialize_reference_model=False,):
+
+ super().__init__()
+
+ self.num_layers = config.get("num_layers", 6)
+ self.num_single_layers = config.get("num_single_layers", 18)
+ self.in_channels = config.get("in_channels", 64)
+ self.attention_head_dim = config.get("attention_head_dim", 128)
+ self.joint_attention_dim = config.get("joint_attention_dim", 1024)
+ self.num_attention_heads = config.get("num_attention_heads", 8)
+ self.audio_seq_len = config.get("audio_seq_len", 645)
+ self.max_duration = config.get("max_duration", 30)
+ self.uncondition = config.get("uncondition", False)
+ self.text_encoder_name = config.get("text_encoder_name", "google/flan-t5-large")
+
+ self.noise_scheduler = FlowMatchEulerDiscreteScheduler(num_train_timesteps=1000)
+ self.noise_scheduler_copy = copy.deepcopy(self.noise_scheduler)
+ self.max_text_seq_len = 64
+ self.text_encoder = T5EncoderModel.from_pretrained(
+ text_encoder_dir if text_encoder_dir is not None else self.text_encoder_name
+ )
+ self.tokenizer = T5TokenizerFast.from_pretrained(
+ text_encoder_dir if text_encoder_dir is not None else self.text_encoder_name
+ )
+ self.text_embedding_dim = self.text_encoder.config.d_model
+
+ self.fc = nn.Sequential(
+ nn.Linear(self.text_embedding_dim, self.joint_attention_dim), nn.ReLU()
+ )
+ self.duration_emebdder = DurationEmbedder(
+ self.text_embedding_dim, min_value=0, max_value=self.max_duration
+ )
+
+ self.transformer = FluxTransformer2DModel(
+ in_channels=self.in_channels,
+ num_layers=self.num_layers,
+ num_single_layers=self.num_single_layers,
+ attention_head_dim=self.attention_head_dim,
+ num_attention_heads=self.num_attention_heads,
+ joint_attention_dim=self.joint_attention_dim,
+ pooled_projection_dim=self.text_embedding_dim,
+ guidance_embeds=False,
+ )
+
+ self.beta_dpo = 2000 ## this is used for dpo training
+
+ def get_sigmas(self, timesteps, n_dim=3, dtype=torch.float32):
+ device = self.text_encoder.device
+ sigmas = self.noise_scheduler_copy.sigmas.to(device=device, dtype=dtype)
+
+ schedule_timesteps = self.noise_scheduler_copy.timesteps.to(device)
+ timesteps = timesteps.to(device)
+ step_indices = [(schedule_timesteps == t).nonzero().item() for t in timesteps]
+
+ sigma = sigmas[step_indices].flatten()
+ while len(sigma.shape) < n_dim:
+ sigma = sigma.unsqueeze(-1)
+ return sigma
+
+ def encode_text_classifier_free(self, prompt: List[str], num_samples_per_prompt=1):
+ device = self.text_encoder.device
+ batch = self.tokenizer(
+ prompt,
+ max_length=self.tokenizer.model_max_length,
+ padding=True,
+ truncation=True,
+ return_tensors="pt",
+ )
+ input_ids, attention_mask = batch.input_ids.to(device), batch.attention_mask.to(
+ device
+ )
+
+ with torch.no_grad():
+ prompt_embeds = self.text_encoder(
+ input_ids=input_ids, attention_mask=attention_mask
+ )[0]
+
+ prompt_embeds = prompt_embeds.repeat_interleave(num_samples_per_prompt, 0)
+ attention_mask = attention_mask.repeat_interleave(num_samples_per_prompt, 0)
+
+ # get unconditional embeddings for classifier free guidance
+ uncond_tokens = [""]
+
+ max_length = prompt_embeds.shape[1]
+ uncond_batch = self.tokenizer(
+ uncond_tokens,
+ max_length=max_length,
+ padding="max_length",
+ truncation=True,
+ return_tensors="pt",
+ )
+ uncond_input_ids = uncond_batch.input_ids.to(device)
+ uncond_attention_mask = uncond_batch.attention_mask.to(device)
+
+ with torch.no_grad():
+ negative_prompt_embeds = self.text_encoder(
+ input_ids=uncond_input_ids, attention_mask=uncond_attention_mask
+ )[0]
+
+ negative_prompt_embeds = negative_prompt_embeds.repeat_interleave(
+ num_samples_per_prompt, 0
+ )
+ uncond_attention_mask = uncond_attention_mask.repeat_interleave(
+ num_samples_per_prompt, 0
+ )
+
+ # For classifier free guidance, we need to do two forward passes.
+ # We concatenate the unconditional and text embeddings into a single batch to avoid doing two forward passes
+
+ prompt_embeds = torch.cat([negative_prompt_embeds, prompt_embeds])
+ prompt_mask = torch.cat([uncond_attention_mask, attention_mask])
+ boolean_prompt_mask = (prompt_mask == 1).to(device)
+
+ return prompt_embeds, boolean_prompt_mask
+
+ @torch.no_grad()
+ def encode_text(self, prompt):
+ device = self.text_encoder.device
+ batch = self.tokenizer(
+ prompt,
+ max_length=self.max_text_seq_len,
+ padding=True,
+ truncation=True,
+ return_tensors="pt",
+ )
+ input_ids, attention_mask = batch.input_ids.to(device), batch.attention_mask.to(
+ device
+ )
+
+ encoder_hidden_states = self.text_encoder(
+ input_ids=input_ids, attention_mask=attention_mask
+ )[0]
+
+ boolean_encoder_mask = (attention_mask == 1).to(device)
+
+ return encoder_hidden_states, boolean_encoder_mask
+
+ def encode_duration(self, duration):
+ return self.duration_emebdder(duration)
+
+ @torch.no_grad()
+ def inference_flow(
+ self,
+ prompt,
+ num_inference_steps=50,
+ timesteps=None,
+ guidance_scale=3,
+ duration=10,
+ seed=0,
+ disable_progress=False,
+ num_samples_per_prompt=1,
+ callback_on_step_end=None,
+ ):
+ """Only tested for single inference. Haven't test for batch inference"""
+
+ torch.manual_seed(seed)
+ if torch.cuda.is_available():
+ torch.cuda.manual_seed(seed)
+ torch.cuda.manual_seed_all(seed)
+ torch.backends.cudnn.deterministic = True
+
+ bsz = num_samples_per_prompt
+ device = self.transformer.device
+ scheduler = self.noise_scheduler
+
+ if not isinstance(prompt, list):
+ prompt = [prompt]
+ if not isinstance(duration, torch.Tensor):
+ duration = torch.tensor([duration], device=device)
+ classifier_free_guidance = guidance_scale > 1.0
+ duration_hidden_states = self.encode_duration(duration)
+ if classifier_free_guidance:
+ bsz = 2 * num_samples_per_prompt
+
+ encoder_hidden_states, boolean_encoder_mask = (
+ self.encode_text_classifier_free(
+ prompt, num_samples_per_prompt=num_samples_per_prompt
+ )
+ )
+ duration_hidden_states = duration_hidden_states.repeat(bsz, 1, 1)
+
+ else:
+
+ encoder_hidden_states, boolean_encoder_mask = self.encode_text(
+ prompt, num_samples_per_prompt=num_samples_per_prompt
+ )
+
+ mask_expanded = boolean_encoder_mask.unsqueeze(-1).expand_as(
+ encoder_hidden_states
+ )
+ masked_data = torch.where(
+ mask_expanded, encoder_hidden_states, torch.tensor(float("nan"))
+ )
+
+ pooled = torch.nanmean(masked_data, dim=1)
+ pooled_projection = self.fc(pooled)
+
+ encoder_hidden_states = torch.cat(
+ [encoder_hidden_states, duration_hidden_states], dim=1
+ ) ## (bs,seq_len,dim)
+
+ sigmas = np.linspace(1.0, 1 / num_inference_steps, num_inference_steps)
+ timesteps, num_inference_steps = retrieve_timesteps(
+ scheduler, num_inference_steps, device, timesteps, sigmas
+ )
+
+ latents = torch.randn(num_samples_per_prompt, self.audio_seq_len, 64)
+ weight_dtype = latents.dtype
+
+ progress_bar = tqdm(range(num_inference_steps), disable=disable_progress)
+
+ txt_ids = torch.zeros(bsz, encoder_hidden_states.shape[1], 3).to(device)
+ audio_ids = (
+ torch.arange(self.audio_seq_len)
+ .unsqueeze(0)
+ .unsqueeze(-1)
+ .repeat(bsz, 1, 3)
+ .to(device)
+ )
+
+ timesteps = timesteps.to(device)
+ latents = latents.to(device)
+ encoder_hidden_states = encoder_hidden_states.to(device)
+
+ for i, t in enumerate(timesteps):
+
+ latents_input = (
+ torch.cat([latents] * 2) if classifier_free_guidance else latents
+ )
+
+ noise_pred = self.transformer(
+ hidden_states=latents_input,
+ # YiYi notes: divide it by 1000 for now because we scale it by 1000 in the transforme rmodel (we should not keep it but I want to keep the inputs same for the model for testing)
+ timestep=torch.tensor([t / 1000], device=device),
+ guidance=None,
+ pooled_projections=pooled_projection,
+ encoder_hidden_states=encoder_hidden_states,
+ txt_ids=txt_ids,
+ img_ids=audio_ids,
+ return_dict=False,
+ )[0]
+
+ if classifier_free_guidance:
+ noise_pred_uncond, noise_pred_text = noise_pred.chunk(2)
+ noise_pred = noise_pred_uncond + guidance_scale * (
+ noise_pred_text - noise_pred_uncond
+ )
+
+ latents = scheduler.step(noise_pred, t, latents).prev_sample
+
+ progress_bar.update(1)
+
+ if callback_on_step_end is not None:
+ callback_on_step_end()
+
+ return latents
+
+ def forward(self, latents, prompt, duration=torch.tensor([10]), sft=True):
+
+ device = latents.device
+ audio_seq_length = self.audio_seq_len
+ bsz = latents.shape[0]
+
+ encoder_hidden_states, boolean_encoder_mask = self.encode_text(prompt)
+ duration_hidden_states = self.encode_duration(duration)
+
+ mask_expanded = boolean_encoder_mask.unsqueeze(-1).expand_as(
+ encoder_hidden_states
+ )
+ masked_data = torch.where(
+ mask_expanded, encoder_hidden_states, torch.tensor(float("nan"))
+ )
+ pooled = torch.nanmean(masked_data, dim=1)
+ pooled_projection = self.fc(pooled)
+
+ ## Add duration hidden states to encoder hidden states
+ encoder_hidden_states = torch.cat(
+ [encoder_hidden_states, duration_hidden_states], dim=1
+ ) ## (bs,seq_len,dim)
+
+ txt_ids = torch.zeros(bsz, encoder_hidden_states.shape[1], 3).to(device)
+ audio_ids = (
+ torch.arange(audio_seq_length)
+ .unsqueeze(0)
+ .unsqueeze(-1)
+ .repeat(bsz, 1, 3)
+ .to(device)
+ )
+
+ if sft:
+
+ if self.uncondition:
+ mask_indices = [k for k in range(len(prompt)) if random.random() < 0.1]
+ if len(mask_indices) > 0:
+ encoder_hidden_states[mask_indices] = 0
+
+ noise = torch.randn_like(latents)
+
+ u = compute_density_for_timestep_sampling(
+ weighting_scheme="logit_normal",
+ batch_size=bsz,
+ logit_mean=0,
+ logit_std=1,
+ mode_scale=None,
+ )
+
+ indices = (u * self.noise_scheduler_copy.config.num_train_timesteps).long()
+ timesteps = self.noise_scheduler_copy.timesteps[indices].to(
+ device=latents.device
+ )
+ sigmas = self.get_sigmas(timesteps, n_dim=latents.ndim, dtype=latents.dtype)
+
+ noisy_model_input = (1.0 - sigmas) * latents + sigmas * noise
+
+ model_pred = self.transformer(
+ hidden_states=noisy_model_input,
+ encoder_hidden_states=encoder_hidden_states,
+ pooled_projections=pooled_projection,
+ img_ids=audio_ids,
+ txt_ids=txt_ids,
+ guidance=None,
+ # YiYi notes: divide it by 1000 for now because we scale it by 1000 in the transforme rmodel (we should not keep it but I want to keep the inputs same for the model for testing)
+ timestep=timesteps / 1000,
+ return_dict=False,
+ )[0]
+
+ target = noise - latents
+ loss = torch.mean(
+ ((model_pred.float() - target.float()) ** 2).reshape(
+ target.shape[0], -1
+ ),
+ 1,
+ )
+ loss = loss.mean()
+ raw_model_loss, raw_ref_loss, implicit_acc = (
+ 0,
+ 0,
+ 0,
+ ) ## default this to 0 if doing sft
+
+ else:
+ encoder_hidden_states = encoder_hidden_states.repeat(2, 1, 1)
+ pooled_projection = pooled_projection.repeat(2, 1)
+ noise = (
+ torch.randn_like(latents).chunk(2)[0].repeat(2, 1, 1)
+ ) ## Have to sample same noise for preferred and rejected
+ u = compute_density_for_timestep_sampling(
+ weighting_scheme="logit_normal",
+ batch_size=bsz // 2,
+ logit_mean=0,
+ logit_std=1,
+ mode_scale=None,
+ )
+
+ indices = (u * self.noise_scheduler_copy.config.num_train_timesteps).long()
+ timesteps = self.noise_scheduler_copy.timesteps[indices].to(
+ device=latents.device
+ )
+ timesteps = timesteps.repeat(2)
+ sigmas = self.get_sigmas(timesteps, n_dim=latents.ndim, dtype=latents.dtype)
+
+ noisy_model_input = (1.0 - sigmas) * latents + sigmas * noise
+
+ model_pred = self.transformer(
+ hidden_states=noisy_model_input,
+ encoder_hidden_states=encoder_hidden_states,
+ pooled_projections=pooled_projection,
+ img_ids=audio_ids,
+ txt_ids=txt_ids,
+ guidance=None,
+ # YiYi notes: divide it by 1000 for now because we scale it by 1000 in the transforme rmodel (we should not keep it but I want to keep the inputs same for the model for testing)
+ timestep=timesteps / 1000,
+ return_dict=False,
+ )[0]
+ target = noise - latents
+
+ model_losses = F.mse_loss(
+ model_pred.float(), target.float(), reduction="none"
+ )
+ model_losses = model_losses.mean(
+ dim=list(range(1, len(model_losses.shape)))
+ )
+ model_losses_w, model_losses_l = model_losses.chunk(2)
+ model_diff = model_losses_w - model_losses_l
+ raw_model_loss = 0.5 * (model_losses_w.mean() + model_losses_l.mean())
+
+ with torch.no_grad():
+ ref_preds = self.ref_transformer(
+ hidden_states=noisy_model_input,
+ encoder_hidden_states=encoder_hidden_states,
+ pooled_projections=pooled_projection,
+ img_ids=audio_ids,
+ txt_ids=txt_ids,
+ guidance=None,
+ timestep=timesteps / 1000,
+ return_dict=False,
+ )[0]
+
+ ref_loss = F.mse_loss(
+ ref_preds.float(), target.float(), reduction="none"
+ )
+ ref_loss = ref_loss.mean(dim=list(range(1, len(ref_loss.shape))))
+
+ ref_losses_w, ref_losses_l = ref_loss.chunk(2)
+ ref_diff = ref_losses_w - ref_losses_l
+ raw_ref_loss = ref_loss.mean()
+
+ scale_term = -0.5 * self.beta_dpo
+ inside_term = scale_term * (model_diff - ref_diff)
+ implicit_acc = (
+ scale_term * (model_diff - ref_diff) > 0
+ ).sum().float() / inside_term.size(0)
+ loss = -1 * F.logsigmoid(inside_term).mean() + model_losses_w.mean()
+
+ ## raw_model_loss, raw_ref_loss, implicit_acc is used to help to analyze dpo behaviour.
+ return loss, raw_model_loss, raw_ref_loss, implicit_acc
diff --git a/external_models/TangoFlux/tangoflux/train.py b/external_models/TangoFlux/tangoflux/train.py
new file mode 100644
index 0000000000000000000000000000000000000000..293ef1c87a786fc086dc4122449b3ee93dd91f81
--- /dev/null
+++ b/external_models/TangoFlux/tangoflux/train.py
@@ -0,0 +1,588 @@
+import time
+import argparse
+import json
+import logging
+import math
+import os
+import yaml
+from pathlib import Path
+import diffusers
+import datasets
+import numpy as np
+import pandas as pd
+import wandb
+import transformers
+import torch
+from accelerate import Accelerator
+from accelerate.logging import get_logger
+from accelerate.utils import set_seed
+from datasets import load_dataset
+from torch.utils.data import Dataset, DataLoader
+from tqdm.auto import tqdm
+from transformers import SchedulerType, get_scheduler
+from model import TangoFlux
+from datasets import load_dataset, Audio
+from utils import Text2AudioDataset, read_wav_file, pad_wav
+
+from diffusers import AutoencoderOobleck
+import torchaudio
+
+logger = get_logger(__name__)
+
+
+def parse_args():
+ parser = argparse.ArgumentParser(
+ description="Rectified flow for text to audio generation task."
+ )
+
+ parser.add_argument(
+ "--num_examples",
+ type=int,
+ default=-1,
+ help="How many examples to use for training and validation.",
+ )
+
+ parser.add_argument(
+ "--text_column",
+ type=str,
+ default="captions",
+ help="The name of the column in the datasets containing the input texts.",
+ )
+ parser.add_argument(
+ "--audio_column",
+ type=str,
+ default="location",
+ help="The name of the column in the datasets containing the audio paths.",
+ )
+ parser.add_argument(
+ "--adam_beta1",
+ type=float,
+ default=0.9,
+ help="The beta1 parameter for the Adam optimizer.",
+ )
+ parser.add_argument(
+ "--adam_beta2",
+ type=float,
+ default=0.95,
+ help="The beta2 parameter for the Adam optimizer.",
+ )
+ parser.add_argument(
+ "--config",
+ type=str,
+ default="tangoflux_config.yaml",
+ help="Config file defining the model size as well as other hyper parameter.",
+ )
+ parser.add_argument(
+ "--prefix",
+ type=str,
+ default="",
+ help="Add prefix in text prompts.",
+ )
+
+ parser.add_argument(
+ "--learning_rate",
+ type=float,
+ default=3e-5,
+ help="Initial learning rate (after the potential warmup period) to use.",
+ )
+ parser.add_argument(
+ "--weight_decay", type=float, default=1e-8, help="Weight decay to use."
+ )
+
+ parser.add_argument(
+ "--max_train_steps",
+ type=int,
+ default=None,
+ help="Total number of training steps to perform. If provided, overrides num_train_epochs.",
+ )
+
+ parser.add_argument(
+ "--lr_scheduler_type",
+ type=SchedulerType,
+ default="linear",
+ help="The scheduler type to use.",
+ choices=[
+ "linear",
+ "cosine",
+ "cosine_with_restarts",
+ "polynomial",
+ "constant",
+ "constant_with_warmup",
+ ],
+ )
+ parser.add_argument(
+ "--num_warmup_steps",
+ type=int,
+ default=0,
+ help="Number of steps for the warmup in the lr scheduler.",
+ )
+ parser.add_argument(
+ "--adam_epsilon",
+ type=float,
+ default=1e-08,
+ help="Epsilon value for the Adam optimizer",
+ )
+ parser.add_argument(
+ "--adam_weight_decay",
+ type=float,
+ default=1e-2,
+ help="Epsilon value for the Adam optimizer",
+ )
+ parser.add_argument(
+ "--seed", type=int, default=None, help="A seed for reproducible training."
+ )
+ parser.add_argument(
+ "--checkpointing_steps",
+ type=str,
+ default="best",
+ help="Whether the various states should be saved at the end of every 'epoch' or 'best' whenever validation loss decreases.",
+ )
+ parser.add_argument(
+ "--save_every",
+ type=int,
+ default=5,
+ help="Save model after every how many epochs when checkpointing_steps is set to best.",
+ )
+
+ parser.add_argument(
+ "--resume_from_checkpoint",
+ type=str,
+ default=None,
+ help="If the training should continue from a local checkpoint folder.",
+ )
+
+ parser.add_argument(
+ "--load_from_checkpoint",
+ type=str,
+ default=None,
+ help="Whether to continue training from a model weight",
+ )
+
+ args = parser.parse_args()
+
+ return args
+
+
+def main():
+ args = parse_args()
+ accelerator_log_kwargs = {}
+
+ def load_config(config_path):
+ with open(config_path, "r") as file:
+ return yaml.safe_load(file)
+
+ config = load_config(args.config)
+
+ learning_rate = float(config["training"]["learning_rate"])
+ num_train_epochs = int(config["training"]["num_train_epochs"])
+ num_warmup_steps = int(config["training"]["num_warmup_steps"])
+ per_device_batch_size = int(config["training"]["per_device_batch_size"])
+ gradient_accumulation_steps = int(config["training"]["gradient_accumulation_steps"])
+
+ output_dir = config["paths"]["output_dir"]
+
+ accelerator = Accelerator(
+ gradient_accumulation_steps=gradient_accumulation_steps,
+ **accelerator_log_kwargs,
+ )
+
+ # Make one log on every process with the configuration for debugging.
+ logging.basicConfig(
+ format="%(asctime)s - %(levelname)s - %(name)s - %(message)s",
+ datefmt="%m/%d/%Y %H:%M:%S",
+ level=logging.INFO,
+ )
+ logger.info(accelerator.state, main_process_only=False)
+
+ datasets.utils.logging.set_verbosity_error()
+ diffusers.utils.logging.set_verbosity_error()
+ transformers.utils.logging.set_verbosity_error()
+
+ # If passed along, set the training seed now.
+ if args.seed is not None:
+ set_seed(args.seed)
+
+ # Handle output directory creation and wandb tracking
+ if accelerator.is_main_process:
+ if output_dir is None or output_dir == "":
+ output_dir = "saved/" + str(int(time.time()))
+
+ if not os.path.exists("saved"):
+ os.makedirs("saved")
+
+ os.makedirs(output_dir, exist_ok=True)
+
+ elif output_dir is not None:
+ os.makedirs(output_dir, exist_ok=True)
+
+ os.makedirs("{}/{}".format(output_dir, "outputs"), exist_ok=True)
+ with open("{}/summary.jsonl".format(output_dir), "a") as f:
+ f.write(json.dumps(dict(vars(args))) + "\n\n")
+
+ accelerator.project_configuration.automatic_checkpoint_naming = False
+
+ wandb.init(
+ project="Text to Audio Flow matching",
+ settings=wandb.Settings(_disable_stats=True),
+ )
+
+ accelerator.wait_for_everyone()
+
+ # Get the datasets
+ data_files = {}
+ # if args.train_file is not None:
+ if config["paths"]["train_file"] != "":
+ data_files["train"] = config["paths"]["train_file"]
+ # if args.validation_file is not None:
+ if config["paths"]["val_file"] != "":
+ data_files["validation"] = config["paths"]["val_file"]
+ if config["paths"]["test_file"] != "":
+ data_files["test"] = config["paths"]["test_file"]
+ else:
+ data_files["test"] = config["paths"]["val_file"]
+
+ extension = "json"
+ raw_datasets = load_dataset(extension, data_files=data_files)
+ text_column, audio_column = args.text_column, args.audio_column
+
+ model = TangoFlux(config=config["model"])
+ vae = AutoencoderOobleck.from_pretrained(
+ "stabilityai/stable-audio-open-1.0", subfolder="vae"
+ )
+
+ ## Freeze vae
+ for param in vae.parameters():
+ vae.requires_grad = False
+ vae.eval()
+
+ ## Freeze text encoder param
+ for param in model.text_encoder.parameters():
+ param.requires_grad = False
+ model.text_encoder.eval()
+
+ prefix = args.prefix
+
+ with accelerator.main_process_first():
+ train_dataset = Text2AudioDataset(
+ raw_datasets["train"],
+ prefix,
+ text_column,
+ audio_column,
+ "duration",
+ args.num_examples,
+ )
+ eval_dataset = Text2AudioDataset(
+ raw_datasets["validation"],
+ prefix,
+ text_column,
+ audio_column,
+ "duration",
+ args.num_examples,
+ )
+ test_dataset = Text2AudioDataset(
+ raw_datasets["test"],
+ prefix,
+ text_column,
+ audio_column,
+ "duration",
+ args.num_examples,
+ )
+
+ accelerator.print(
+ "Num instances in train: {}, validation: {}, test: {}".format(
+ train_dataset.get_num_instances(),
+ eval_dataset.get_num_instances(),
+ test_dataset.get_num_instances(),
+ )
+ )
+
+ train_dataloader = DataLoader(
+ train_dataset,
+ shuffle=True,
+ batch_size=config["training"]["per_device_batch_size"],
+ collate_fn=train_dataset.collate_fn,
+ )
+ eval_dataloader = DataLoader(
+ eval_dataset,
+ shuffle=True,
+ batch_size=config["training"]["per_device_batch_size"],
+ collate_fn=eval_dataset.collate_fn,
+ )
+ test_dataloader = DataLoader(
+ test_dataset,
+ shuffle=False,
+ batch_size=config["training"]["per_device_batch_size"],
+ collate_fn=test_dataset.collate_fn,
+ )
+
+ # Optimizer
+
+ optimizer_parameters = list(model.transformer.parameters()) + list(
+ model.fc.parameters()
+ )
+ num_trainable_parameters = sum(
+ p.numel() for p in model.parameters() if p.requires_grad
+ )
+ accelerator.print("Num trainable parameters: {}".format(num_trainable_parameters))
+
+ if args.load_from_checkpoint:
+ from safetensors.torch import load_file
+
+ w1 = load_file(args.load_from_checkpoint)
+ model.load_state_dict(w1, strict=False)
+ logger.info("Weights loaded from{}".format(args.load_from_checkpoint))
+
+ optimizer = torch.optim.AdamW(
+ optimizer_parameters,
+ lr=learning_rate,
+ betas=(args.adam_beta1, args.adam_beta2),
+ weight_decay=args.adam_weight_decay,
+ eps=args.adam_epsilon,
+ )
+
+ # Scheduler and math around the number of training steps.
+ overrode_max_train_steps = False
+ num_update_steps_per_epoch = math.ceil(
+ len(train_dataloader) / gradient_accumulation_steps
+ )
+ if args.max_train_steps is None:
+ args.max_train_steps = num_train_epochs * num_update_steps_per_epoch
+ overrode_max_train_steps = True
+
+ lr_scheduler = get_scheduler(
+ name=args.lr_scheduler_type,
+ optimizer=optimizer,
+ num_warmup_steps=num_warmup_steps
+ * gradient_accumulation_steps
+ * accelerator.num_processes,
+ num_training_steps=args.max_train_steps * gradient_accumulation_steps,
+ )
+
+ # Prepare everything with our `accelerator`.
+ vae, model, optimizer, lr_scheduler = accelerator.prepare(
+ vae, model, optimizer, lr_scheduler
+ )
+
+ train_dataloader, eval_dataloader, test_dataloader = accelerator.prepare(
+ train_dataloader, eval_dataloader, test_dataloader
+ )
+
+ # We need to recalculate our total training steps as the size of the training dataloader may have changed.
+ num_update_steps_per_epoch = math.ceil(
+ len(train_dataloader) / gradient_accumulation_steps
+ )
+ if overrode_max_train_steps:
+ args.max_train_steps = num_train_epochs * num_update_steps_per_epoch
+ # Afterwards we recalculate our number of training epochs
+ num_train_epochs = math.ceil(args.max_train_steps / num_update_steps_per_epoch)
+
+ # Figure out how many steps we should save the Accelerator states
+ checkpointing_steps = args.checkpointing_steps
+ if checkpointing_steps is not None and checkpointing_steps.isdigit():
+ checkpointing_steps = int(checkpointing_steps)
+
+ # We need to initialize the trackers we use, and also store our configuration.
+ # The trackers initializes automatically on the main process.
+
+ # Train!
+ total_batch_size = (
+ per_device_batch_size * accelerator.num_processes * gradient_accumulation_steps
+ )
+
+ logger.info("***** Running training *****")
+ logger.info(f" Num examples = {len(train_dataset)}")
+ logger.info(f" Num Epochs = {num_train_epochs}")
+ logger.info(f" Instantaneous batch size per device = {per_device_batch_size}")
+ logger.info(
+ f" Total train batch size (w. parallel, distributed & accumulation) = {total_batch_size}"
+ )
+ logger.info(f" Gradient Accumulation steps = {gradient_accumulation_steps}")
+ logger.info(f" Total optimization steps = {args.max_train_steps}")
+
+ # Only show the progress bar once on each machine.
+ progress_bar = tqdm(
+ range(args.max_train_steps), disable=not accelerator.is_local_main_process
+ )
+
+ completed_steps = 0
+ starting_epoch = 0
+ # Potentially load in the weights and states from a previous save
+ resume_from_checkpoint = config["paths"]["resume_from_checkpoint"]
+ if resume_from_checkpoint != "":
+ accelerator.load_state(resume_from_checkpoint)
+ accelerator.print(f"Resumed from local checkpoint: {resume_from_checkpoint}")
+
+ # Duration of the audio clips in seconds
+ best_loss = np.inf
+ length = config["training"]["max_audio_duration"]
+
+ for epoch in range(starting_epoch, num_train_epochs):
+ model.train()
+ total_loss, total_val_loss = 0, 0
+ for step, batch in enumerate(train_dataloader):
+
+ with accelerator.accumulate(model):
+ optimizer.zero_grad()
+ device = model.device
+ text, audios, duration, _ = batch
+
+ with torch.no_grad():
+ audio_list = []
+
+ for audio_path in audios:
+
+ wav = read_wav_file(
+ audio_path, length
+ ) ## Only read the first 30 seconds of audio
+ if (
+ wav.shape[0] == 1
+ ): ## If this audio is mono, we repeat the channel so it become "fake stereo"
+ wav = wav.repeat(2, 1)
+ audio_list.append(wav)
+
+ audio_input = torch.stack(audio_list, dim=0)
+ audio_input = audio_input.to(device)
+ unwrapped_vae = accelerator.unwrap_model(vae)
+
+ duration = torch.tensor(duration, device=device)
+ duration = torch.clamp(
+ duration, max=length
+ ) ## clamp duration to max audio length
+
+ audio_latent = unwrapped_vae.encode(
+ audio_input
+ ).latent_dist.sample()
+ audio_latent = audio_latent.transpose(
+ 1, 2
+ ) ## Tranpose to (bsz, seq_len, channel)
+
+ loss, _, _, _ = model(audio_latent, text, duration=duration)
+ total_loss += loss.detach().float()
+ accelerator.backward(loss)
+
+ if accelerator.sync_gradients:
+ progress_bar.update(1)
+ completed_steps += 1
+
+ optimizer.step()
+ lr_scheduler.step()
+
+ if completed_steps % 10 == 0 and accelerator.is_main_process:
+
+ total_norm = 0.0
+ for p in model.parameters():
+ if p.grad is not None:
+ param_norm = p.grad.data.norm(2)
+ total_norm += param_norm.item() ** 2
+
+ total_norm = total_norm**0.5
+ logger.info(
+ f"Step {completed_steps}, Loss: {loss.item()}, Grad Norm: {total_norm}"
+ )
+
+ lr = lr_scheduler.get_last_lr()[0]
+ result = {
+ "train_loss": loss.item(),
+ "grad_norm": total_norm,
+ "learning_rate": lr,
+ }
+
+ # result["val_loss"] = round(total_val_loss.item()/len(eval_dataloader), 4)
+ wandb.log(result, step=completed_steps)
+
+ # Checks if the accelerator has performed an optimization step behind the scenes
+
+ if isinstance(checkpointing_steps, int):
+ if completed_steps % checkpointing_steps == 0:
+ output_dir = f"step_{completed_steps }"
+ if output_dir is not None:
+ output_dir = os.path.join(output_dir, output_dir)
+ accelerator.save_state(output_dir)
+
+ if completed_steps >= args.max_train_steps:
+ break
+
+ model.eval()
+ eval_progress_bar = tqdm(
+ range(len(eval_dataloader)), disable=not accelerator.is_local_main_process
+ )
+ for step, batch in enumerate(eval_dataloader):
+ with accelerator.accumulate(model) and torch.no_grad():
+ device = model.device
+ text, audios, duration, _ = batch
+
+ audio_list = []
+ for audio_path in audios:
+
+ wav = read_wav_file(
+ audio_path, length
+ ) ## make sure none of audio exceed 30 sec
+ if (
+ wav.shape[0] == 1
+ ): ## If this audio is mono, we repeat the channel so it become "fake stereo"
+ wav = wav.repeat(2, 1)
+ audio_list.append(wav)
+
+ audio_input = torch.stack(audio_list, dim=0)
+ audio_input = audio_input.to(device)
+ duration = torch.tensor(duration, device=device)
+ unwrapped_vae = accelerator.unwrap_model(vae)
+ audio_latent = unwrapped_vae.encode(audio_input).latent_dist.sample()
+ audio_latent = audio_latent.transpose(
+ 1, 2
+ ) ## Tranpose to (bsz, seq_len, channel)
+
+ val_loss, _, _, _ = model(audio_latent, text, duration=duration)
+
+ total_val_loss += val_loss.detach().float()
+ eval_progress_bar.update(1)
+
+ if accelerator.is_main_process:
+
+ result = {}
+ result["epoch"] = float(epoch + 1)
+
+ result["epoch/train_loss"] = round(
+ total_loss.item() / len(train_dataloader), 4
+ )
+ result["epoch/val_loss"] = round(
+ total_val_loss.item() / len(eval_dataloader), 4
+ )
+
+ wandb.log(result, step=completed_steps)
+
+ result_string = "Epoch: {}, Loss Train: {}, Val: {}\n".format(
+ epoch, result["epoch/train_loss"], result["epoch/val_loss"]
+ )
+
+ accelerator.print(result_string)
+
+ with open("{}/summary.jsonl".format(output_dir), "a") as f:
+ f.write(json.dumps(result) + "\n\n")
+
+ logger.info(result)
+
+ if result["epoch/val_loss"] < best_loss:
+ best_loss = result["epoch/val_loss"]
+ save_checkpoint = True
+ else:
+ save_checkpoint = False
+
+ accelerator.wait_for_everyone()
+ if accelerator.is_main_process and args.checkpointing_steps == "best":
+ if save_checkpoint:
+ accelerator.save_state("{}/{}".format(output_dir, "best"))
+
+ if (epoch + 1) % args.save_every == 0:
+ accelerator.save_state(
+ "{}/{}".format(output_dir, "epoch_" + str(epoch + 1))
+ )
+
+ if accelerator.is_main_process and args.checkpointing_steps == "epoch":
+ accelerator.save_state(
+ "{}/{}".format(output_dir, "epoch_" + str(epoch + 1))
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/external_models/TangoFlux/tangoflux/train_dpo.py b/external_models/TangoFlux/tangoflux/train_dpo.py
new file mode 100644
index 0000000000000000000000000000000000000000..22c589cbdca84275f6b865f2649f91fe3f969449
--- /dev/null
+++ b/external_models/TangoFlux/tangoflux/train_dpo.py
@@ -0,0 +1,608 @@
+import time
+import argparse
+import json
+import logging
+import math
+import os
+import yaml
+
+# from tqdm import tqdm
+import copy
+from pathlib import Path
+import diffusers
+import datasets
+import numpy as np
+import pandas as pd
+import wandb
+import transformers
+import torch
+from accelerate import Accelerator
+from accelerate.logging import get_logger
+from accelerate.utils import set_seed
+from datasets import load_dataset
+from torch.utils.data import Dataset, DataLoader
+from tqdm.auto import tqdm
+from transformers import SchedulerType, get_scheduler
+from tangoflux.model import TangoFlux
+from datasets import load_dataset, Audio
+from tangoflux.utils import Text2AudioDataset, read_wav_file, DPOText2AudioDataset
+
+from diffusers import AutoencoderOobleck
+import torchaudio
+
+logger = get_logger(__name__)
+
+
+def parse_args():
+ parser = argparse.ArgumentParser(
+ description="Rectified flow for text to audio generation task."
+ )
+
+ parser.add_argument(
+ "--num_examples",
+ type=int,
+ default=-1,
+ help="How many examples to use for training and validation.",
+ )
+
+ parser.add_argument(
+ "--text_column",
+ type=str,
+ default="captions",
+ help="The name of the column in the datasets containing the input texts.",
+ )
+ parser.add_argument(
+ "--audio_column",
+ type=str,
+ default="location",
+ help="The name of the column in the datasets containing the audio paths.",
+ )
+ parser.add_argument(
+ "--adam_beta1",
+ type=float,
+ default=0.9,
+ help="The beta1 parameter for the Adam optimizer.",
+ )
+ parser.add_argument(
+ "--adam_beta2",
+ type=float,
+ default=0.95,
+ help="The beta2 parameter for the Adam optimizer.",
+ )
+ parser.add_argument(
+ "--config",
+ type=str,
+ default="tangoflux_config.yaml",
+ help="Config file defining the model size.",
+ )
+
+ parser.add_argument(
+ "--weight_decay", type=float, default=1e-8, help="Weight decay to use."
+ )
+
+ parser.add_argument(
+ "--max_train_steps",
+ type=int,
+ default=None,
+ help="Total number of training steps to perform. If provided, overrides num_train_epochs.",
+ )
+
+ parser.add_argument(
+ "--lr_scheduler_type",
+ type=SchedulerType,
+ default="linear",
+ help="The scheduler type to use.",
+ choices=[
+ "linear",
+ "cosine",
+ "cosine_with_restarts",
+ "polynomial",
+ "constant",
+ "constant_with_warmup",
+ ],
+ )
+
+ parser.add_argument(
+ "--adam_epsilon",
+ type=float,
+ default=1e-08,
+ help="Epsilon value for the Adam optimizer",
+ )
+ parser.add_argument(
+ "--adam_weight_decay",
+ type=float,
+ default=1e-2,
+ help="Epsilon value for the Adam optimizer",
+ )
+ parser.add_argument(
+ "--seed", type=int, default=None, help="A seed for reproducible training."
+ )
+ parser.add_argument(
+ "--checkpointing_steps",
+ type=str,
+ default="best",
+ help="Whether the various states should be saved at the end of every 'epoch' or 'best' whenever validation loss decreases.",
+ )
+ parser.add_argument(
+ "--save_every",
+ type=int,
+ default=5,
+ help="Save model after every how many epochs when checkpointing_steps is set to best.",
+ )
+
+
+
+ parser.add_argument(
+ "--load_from_checkpoint",
+ type=str,
+ default=None,
+ help="Whether to continue training from a model weight",
+ )
+
+
+ args = parser.parse_args()
+
+ # Sanity checks
+ # if args.train_file is None and args.validation_file is None:
+ # raise ValueError("Need a training/validation file.")
+ # else:
+ # if args.train_file is not None:
+ # extension = args.train_file.split(".")[-1]
+ # assert extension in ["csv", "json"], "`train_file` should be a csv or a json file."
+ # if args.validation_file is not None:
+ # extension = args.validation_file.split(".")[-1]
+ # assert extension in ["csv", "json"], "`validation_file` should be a csv or a json file."
+
+ return args
+
+
+def main():
+ args = parse_args()
+ accelerator_log_kwargs = {}
+
+ def load_config(config_path):
+ with open(config_path, "r") as file:
+ return yaml.safe_load(file)
+
+ config = load_config(args.config)
+
+ learning_rate = float(config["training"]["learning_rate"])
+ num_train_epochs = int(config["training"]["num_train_epochs"])
+ num_warmup_steps = int(config["training"]["num_warmup_steps"])
+ per_device_batch_size = int(config["training"]["per_device_batch_size"])
+ gradient_accumulation_steps = int(config["training"]["gradient_accumulation_steps"])
+
+ output_dir = config["paths"]["output_dir"]
+
+ accelerator = Accelerator(
+ gradient_accumulation_steps=gradient_accumulation_steps,
+ **accelerator_log_kwargs,
+ )
+
+ # Make one log on every process with the configuration for debugging.
+ logging.basicConfig(
+ format="%(asctime)s - %(levelname)s - %(name)s - %(message)s",
+ datefmt="%m/%d/%Y %H:%M:%S",
+ level=logging.INFO,
+ )
+ logger.info(accelerator.state, main_process_only=False)
+
+ datasets.utils.logging.set_verbosity_error()
+ diffusers.utils.logging.set_verbosity_error()
+ transformers.utils.logging.set_verbosity_error()
+
+ # If passed along, set the training seed now.
+ if args.seed is not None:
+ set_seed(args.seed)
+
+ # Handle output directory creation and wandb tracking
+ if accelerator.is_main_process:
+ if output_dir is None or output_dir == "":
+ output_dir = "saved/" + str(int(time.time()))
+
+ if not os.path.exists("saved"):
+ os.makedirs("saved")
+
+ os.makedirs(output_dir, exist_ok=True)
+
+ elif output_dir is not None:
+ os.makedirs(output_dir, exist_ok=True)
+
+ os.makedirs("{}/{}".format(output_dir, "outputs"), exist_ok=True)
+ with open("{}/summary.jsonl".format(output_dir), "a") as f:
+ f.write(json.dumps(dict(vars(args))) + "\n\n")
+
+ accelerator.project_configuration.automatic_checkpoint_naming = False
+
+ wandb.init(
+ project="Text to Audio Flow matching DPO",
+ settings=wandb.Settings(_disable_stats=True),
+ )
+
+ accelerator.wait_for_everyone()
+
+ # Get the datasets
+ data_files = {}
+ # if args.train_file is not None:
+ if config["paths"]["train_file"] != "":
+ data_files["train"] = config["paths"]["train_file"]
+ # if args.validation_file is not None:
+ if config["paths"]["val_file"] != "":
+ data_files["validation"] = config["paths"]["val_file"]
+ if config["paths"]["test_file"] != "":
+ data_files["test"] = config["paths"]["test_file"]
+ else:
+ data_files["test"] = config["paths"]["val_file"]
+
+ extension = "json"
+ train_dataset = load_dataset(extension, data_files=data_files["train"])
+ data_files.pop("train")
+ raw_datasets = load_dataset(extension, data_files=data_files)
+ text_column, audio_column = args.text_column, args.audio_column
+
+ model = TangoFlux(config=config["model"], initialize_reference_model=True)
+ vae = AutoencoderOobleck.from_pretrained(
+ "stabilityai/stable-audio-open-1.0", subfolder="vae"
+ )
+
+ ## Freeze vae
+ for param in vae.parameters():
+ vae.requires_grad = False
+ vae.eval()
+
+ ## Freeze text encoder param
+ for param in model.text_encoder.parameters():
+ param.requires_grad = False
+ model.text_encoder.eval()
+
+ prefix = ""
+
+ with accelerator.main_process_first():
+ train_dataset = DPOText2AudioDataset(
+ train_dataset["train"],
+ prefix,
+ text_column,
+ "chosen",
+ "reject",
+ "duration",
+ args.num_examples,
+ )
+ eval_dataset = Text2AudioDataset(
+ raw_datasets["validation"],
+ prefix,
+ text_column,
+ audio_column,
+ "duration",
+ args.num_examples,
+ )
+ test_dataset = Text2AudioDataset(
+ raw_datasets["test"],
+ prefix,
+ text_column,
+ audio_column,
+ "duration",
+ args.num_examples,
+ )
+
+ accelerator.print(
+ "Num instances in train: {}, validation: {}, test: {}".format(
+ train_dataset.get_num_instances(),
+ eval_dataset.get_num_instances(),
+ test_dataset.get_num_instances(),
+ )
+ )
+
+ train_dataloader = DataLoader(
+ train_dataset,
+ shuffle=True,
+ batch_size=config["training"]["per_device_batch_size"],
+ collate_fn=train_dataset.collate_fn,
+ )
+ eval_dataloader = DataLoader(
+ eval_dataset,
+ shuffle=True,
+ batch_size=config["training"]["per_device_batch_size"],
+ collate_fn=eval_dataset.collate_fn,
+ )
+ test_dataloader = DataLoader(
+ test_dataset,
+ shuffle=False,
+ batch_size=config["training"]["per_device_batch_size"],
+ collate_fn=test_dataset.collate_fn,
+ )
+
+ # Optimizer
+
+ optimizer_parameters = list(model.transformer.parameters()) + list(
+ model.fc.parameters()
+ )
+ num_trainable_parameters = sum(
+ p.numel() for p in model.parameters() if p.requires_grad
+ )
+ accelerator.print("Num trainable parameters: {}".format(num_trainable_parameters))
+
+ if args.load_from_checkpoint:
+ from safetensors.torch import load_file
+
+ w1 = load_file(args.load_from_checkpoint)
+ model.load_state_dict(w1, strict=False)
+ logger.info("Weights loaded from{}".format(args.load_from_checkpoint))
+
+ import copy
+
+ model.ref_transformer = copy.deepcopy(model.transformer)
+ model.ref_transformer.requires_grad_ = False
+ model.ref_transformer.eval()
+ for param in model.ref_transformer.parameters():
+ param.requires_grad = False
+
+
+
+
+ optimizer = torch.optim.AdamW(
+ optimizer_parameters,
+ lr=learning_rate,
+ betas=(args.adam_beta1, args.adam_beta2),
+ weight_decay=args.adam_weight_decay,
+ eps=args.adam_epsilon,
+ )
+
+ # Scheduler and math around the number of training steps.
+ overrode_max_train_steps = False
+ num_update_steps_per_epoch = math.ceil(
+ len(train_dataloader) / gradient_accumulation_steps
+ )
+ if args.max_train_steps is None:
+ args.max_train_steps = num_train_epochs * num_update_steps_per_epoch
+ overrode_max_train_steps = True
+
+ lr_scheduler = get_scheduler(
+ name=args.lr_scheduler_type,
+ optimizer=optimizer,
+ num_warmup_steps=num_warmup_steps
+ * gradient_accumulation_steps
+ * accelerator.num_processes,
+ num_training_steps=args.max_train_steps * gradient_accumulation_steps,
+ )
+
+ # Prepare everything with our `accelerator`.
+ vae, model, optimizer, lr_scheduler = accelerator.prepare(
+ vae, model, optimizer, lr_scheduler
+ )
+
+ train_dataloader, eval_dataloader, test_dataloader = accelerator.prepare(
+ train_dataloader, eval_dataloader, test_dataloader
+ )
+
+ # We need to recalculate our total training steps as the size of the training dataloader may have changed.
+ num_update_steps_per_epoch = math.ceil(
+ len(train_dataloader) / gradient_accumulation_steps
+ )
+ if overrode_max_train_steps:
+ args.max_train_steps = num_train_epochs * num_update_steps_per_epoch
+ # Afterwards we recalculate our number of training epochs
+ num_train_epochs = math.ceil(args.max_train_steps / num_update_steps_per_epoch)
+
+ # Figure out how many steps we should save the Accelerator states
+ checkpointing_steps = args.checkpointing_steps
+ if checkpointing_steps is not None and checkpointing_steps.isdigit():
+ checkpointing_steps = int(checkpointing_steps)
+
+
+
+ # Train!
+ total_batch_size = (
+ per_device_batch_size * accelerator.num_processes * gradient_accumulation_steps
+ )
+
+ logger.info("***** Running training *****")
+ logger.info(f" Num examples = {len(train_dataset)}")
+ logger.info(f" Num Epochs = {num_train_epochs}")
+ logger.info(f" Instantaneous batch size per device = {per_device_batch_size}")
+ logger.info(
+ f" Total train batch size (w. parallel, distributed & accumulation) = {total_batch_size}"
+ )
+ logger.info(f" Gradient Accumulation steps = {gradient_accumulation_steps}")
+ logger.info(f" Total optimization steps = {args.max_train_steps}")
+
+ # Only show the progress bar once on each machine.
+ progress_bar = tqdm(
+ range(args.max_train_steps), disable=not accelerator.is_local_main_process
+ )
+
+ completed_steps = 0
+ starting_epoch = 0
+ # Potentially load in the weights and states from a previous save
+ resume_from_checkpoint = config["paths"]["resume_from_checkpoint"]
+ if resume_from_checkpoint != "":
+ accelerator.load_state(resume_from_checkpoint)
+ accelerator.print(f"Resumed from local checkpoint: {resume_from_checkpoint}")
+
+ # Duration of the audio clips in seconds
+ best_loss = np.inf
+ length = config["training"]["max_audio_duration"]
+
+ for epoch in range(starting_epoch, num_train_epochs):
+ model.train()
+ total_loss, total_val_loss = 0, 0
+
+ for step, batch in enumerate(train_dataloader):
+ optimizer.zero_grad()
+ with accelerator.accumulate(model):
+ optimizer.zero_grad()
+ device = accelerator.device
+ text, audio_w, audio_l, duration, _ = batch
+
+ with torch.no_grad():
+ audio_list_w = []
+ audio_list_l = []
+ for audio_path in audio_w:
+
+ wav = read_wav_file(
+ audio_path, length
+ ) ## Only read the first 30 seconds of audio
+ if (
+ wav.shape[0] == 1
+ ): ## If this audio is mono, we repeat the channel so it become "fake stereo"
+ wav = wav.repeat(2, 1)
+ audio_list_w.append(wav)
+
+ for audio_path in audio_l:
+ wav = read_wav_file(
+ audio_path, length
+ ) ## Only read the first 30 seconds of audio
+ if (
+ wav.shape[0] == 1
+ ): ## If this audio is mono, we repeat the channel so it become "fake stereo"
+ wav = wav.repeat(2, 1)
+ audio_list_l.append(wav)
+
+ audio_input_w = torch.stack(audio_list_w, dim=0).to(device)
+ audio_input_l = torch.stack(audio_list_l, dim=0).to(device)
+ # audio_input_ = audio_input.to(device)
+ unwrapped_vae = accelerator.unwrap_model(vae)
+
+ duration = torch.tensor(duration, device=device)
+ duration = torch.clamp(
+ duration, max=length
+ ) ## max duration is 30 sec
+
+ audio_latent_w = unwrapped_vae.encode(
+ audio_input_w
+ ).latent_dist.sample()
+ audio_latent_l = unwrapped_vae.encode(
+ audio_input_l
+ ).latent_dist.sample()
+ audio_latent = torch.cat((audio_latent_w, audio_latent_l), dim=0)
+ audio_latent = audio_latent.transpose(
+ 1, 2
+ ) ## Tranpose to (bsz, seq_len, channel)
+
+ loss, raw_model_loss, raw_ref_loss, implicit_acc = model(
+ audio_latent, text, duration=duration, sft=False
+ )
+
+ total_loss += loss.detach().float()
+ accelerator.backward(loss)
+ optimizer.step()
+ lr_scheduler.step()
+ # if accelerator.sync_gradients:
+ if accelerator.sync_gradients:
+ # accelerator.clip_grad_value_(model.parameters(),1.0)
+ progress_bar.update(1)
+ completed_steps += 1
+
+ if completed_steps % 10 == 0 and accelerator.is_main_process:
+
+ total_norm = 0.0
+ for p in model.parameters():
+ if p.grad is not None:
+ param_norm = p.grad.data.norm(2)
+ total_norm += param_norm.item() ** 2
+
+ total_norm = total_norm**0.5
+ logger.info(
+ f"Step {completed_steps}, Loss: {loss.item()}, Grad Norm: {total_norm}"
+ )
+
+ lr = lr_scheduler.get_last_lr()[0]
+
+ result = {
+ "train_loss": loss.item(),
+ "grad_norm": total_norm,
+ "learning_rate": lr,
+ "raw_model_loss": raw_model_loss,
+ "raw_ref_loss": raw_ref_loss,
+ "implicit_acc": implicit_acc,
+ }
+
+ # result["val_loss"] = round(total_val_loss.item()/len(eval_dataloader), 4)
+ wandb.log(result, step=completed_steps)
+
+ # Checks if the accelerator has performed an optimization step behind the scenes
+
+ if isinstance(checkpointing_steps, int):
+ if completed_steps % checkpointing_steps == 0:
+ output_dir = f"step_{completed_steps }"
+ if output_dir is not None:
+ output_dir = os.path.join(output_dir, output_dir)
+ accelerator.save_state(output_dir)
+
+ if completed_steps >= args.max_train_steps:
+ break
+
+ model.eval()
+ eval_progress_bar = tqdm(
+ range(len(eval_dataloader)), disable=not accelerator.is_local_main_process
+ )
+ for step, batch in enumerate(eval_dataloader):
+ with accelerator.accumulate(model) and torch.no_grad():
+ device = model.device
+ text, audios, duration, _ = batch
+
+ audio_list = []
+ for audio_path in audios:
+
+ wav = read_wav_file(
+ audio_path, length
+ ) ## Only read the first 30 seconds of audio
+ if (
+ wav.shape[0] == 1
+ ): ## If this audio is mono, we repeat the channel so it become "fake stereo"
+ wav = wav.repeat(2, 1)
+ audio_list.append(wav)
+
+ audio_input = torch.stack(audio_list, dim=0)
+ audio_input = audio_input.to(device)
+ duration = torch.tensor(duration, device=device)
+ unwrapped_vae = accelerator.unwrap_model(vae)
+ audio_latent = unwrapped_vae.encode(audio_input).latent_dist.sample()
+ audio_latent = audio_latent.transpose(
+ 1, 2
+ ) ## Tranpose to (bsz, seq_len, channel)
+
+ val_loss, _, _, _ = model(
+ audio_latent, text, duration=duration, sft=True
+ )
+
+ total_val_loss += val_loss.detach().float()
+ eval_progress_bar.update(1)
+
+ if accelerator.is_main_process:
+
+ result = {}
+ result["epoch"] = float(epoch + 1)
+
+ result["epoch/train_loss"] = round(
+ total_loss.item() / len(train_dataloader), 4
+ )
+ result["epoch/val_loss"] = round(
+ total_val_loss.item() / len(eval_dataloader), 4
+ )
+
+ wandb.log(result, step=completed_steps)
+
+ with open("{}/summary.jsonl".format(output_dir), "a") as f:
+ f.write(json.dumps(result) + "\n\n")
+
+ logger.info(result)
+
+ save_checkpoint = True
+ accelerator.wait_for_everyone()
+ if accelerator.is_main_process and args.checkpointing_steps == "best":
+ if save_checkpoint:
+ accelerator.save_state("{}/{}".format(output_dir, "best"))
+
+ if (epoch + 1) % args.save_every == 0:
+ accelerator.save_state(
+ "{}/{}".format(output_dir, "epoch_" + str(epoch + 1))
+ )
+
+ if accelerator.is_main_process and args.checkpointing_steps == "epoch":
+ accelerator.save_state(
+ "{}/{}".format(output_dir, "epoch_" + str(epoch + 1))
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/external_models/TangoFlux/tangoflux/utils.py b/external_models/TangoFlux/tangoflux/utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..c6456a604c4c1d8124b731d591de4a0919d15fad
--- /dev/null
+++ b/external_models/TangoFlux/tangoflux/utils.py
@@ -0,0 +1,159 @@
+import torch
+from torch.utils.data import Dataset, DataLoader
+import numpy as np
+import pandas as pd
+
+import torchaudio
+import random
+import itertools
+import numpy as np
+
+
+import numpy as np
+
+
+def normalize_wav(waveform):
+ waveform = waveform - torch.mean(waveform)
+ waveform = waveform / (torch.max(torch.abs(waveform)) + 1e-8)
+ return waveform * 0.5
+
+
+def pad_wav(waveform, segment_length):
+ waveform_length = len(waveform)
+
+ if segment_length is None or waveform_length == segment_length:
+ return waveform
+ elif waveform_length > segment_length:
+ return waveform[:segment_length]
+ else:
+ padded_wav = torch.zeros(segment_length - waveform_length).to(waveform.device)
+ waveform = torch.cat([waveform, padded_wav])
+ return waveform
+
+
+def read_wav_file(filename, duration_sec):
+ info = torchaudio.info(filename)
+ sample_rate = info.sample_rate
+
+ # Calculate the number of frames corresponding to the desired duration
+ num_frames = int(sample_rate * duration_sec)
+
+ waveform, sr = torchaudio.load(filename, num_frames=num_frames) # Faster!!!
+
+ if waveform.shape[0] == 2: ## Stereo audio
+ resampler = torchaudio.transforms.Resample(orig_freq=sr, new_freq=44100)
+ resampled_waveform = resampler(waveform)
+ # print(resampled_waveform.shape)
+ padded_left = pad_wav(
+ resampled_waveform[0], int(44100 * duration_sec)
+ ) ## We pad left and right seperately
+ padded_right = pad_wav(resampled_waveform[1], int(44100 * duration_sec))
+
+ return torch.stack([padded_left, padded_right])
+ else:
+ waveform = torchaudio.functional.resample(
+ waveform, orig_freq=sr, new_freq=44100
+ )[0]
+ waveform = pad_wav(waveform, int(44100 * duration_sec)).unsqueeze(0)
+
+ return waveform
+
+
+class DPOText2AudioDataset(Dataset):
+ def __init__(
+ self,
+ dataset,
+ prefix,
+ text_column,
+ audio_w_column,
+ audio_l_column,
+ duration,
+ num_examples=-1,
+ ):
+
+ inputs = list(dataset[text_column])
+ self.inputs = [prefix + inp for inp in inputs]
+ self.audios_w = list(dataset[audio_w_column])
+ self.audios_l = list(dataset[audio_l_column])
+ self.durations = list(dataset[duration])
+ self.indices = list(range(len(self.inputs)))
+
+ self.mapper = {}
+ for index, audio_w, audio_l, duration, text in zip(
+ self.indices, self.audios_w, self.audios_l, self.durations, inputs
+ ):
+ self.mapper[index] = [audio_w, audio_l, duration, text]
+
+ if num_examples != -1:
+ self.inputs, self.audios_w, self.audios_l, self.durations = (
+ self.inputs[:num_examples],
+ self.audios_w[:num_examples],
+ self.audios_l[:num_examples],
+ self.durations[:num_examples],
+ )
+ self.indices = self.indices[:num_examples]
+
+ def __len__(self):
+ return len(self.inputs)
+
+ def get_num_instances(self):
+ return len(self.inputs)
+
+ def __getitem__(self, index):
+ s1, s2, s3, s4, s5 = (
+ self.inputs[index],
+ self.audios_w[index],
+ self.audios_l[index],
+ self.durations[index],
+ self.indices[index],
+ )
+ return s1, s2, s3, s4, s5
+
+ def collate_fn(self, data):
+ dat = pd.DataFrame(data)
+ return [dat[i].tolist() for i in dat]
+
+
+class Text2AudioDataset(Dataset):
+ def __init__(
+ self, dataset, prefix, text_column, audio_column, duration, num_examples=-1
+ ):
+
+ inputs = list(dataset[text_column])
+ self.inputs = [prefix + inp for inp in inputs]
+ self.audios = list(dataset[audio_column])
+ self.durations = list(dataset[duration])
+ self.indices = list(range(len(self.inputs)))
+
+ self.mapper = {}
+ for index, audio, duration, text in zip(
+ self.indices, self.audios, self.durations, inputs
+ ):
+ self.mapper[index] = [audio, text, duration]
+
+ if num_examples != -1:
+ self.inputs, self.audios, self.durations = (
+ self.inputs[:num_examples],
+ self.audios[:num_examples],
+ self.durations[:num_examples],
+ )
+ self.indices = self.indices[:num_examples]
+
+ def __len__(self):
+ return len(self.inputs)
+
+ def get_num_instances(self):
+ return len(self.inputs)
+
+ def __getitem__(self, index):
+ s1, s2, s3, s4 = (
+ self.inputs[index],
+ self.audios[index],
+ self.durations[index],
+ self.indices[index],
+ )
+ return s1, s2, s3, s4
+
+ def collate_fn(self, data):
+ dat = pd.DataFrame(data)
+ return [dat[i].tolist() for i in dat]
diff --git a/external_models/TangoFlux/train.sh b/external_models/TangoFlux/train.sh
new file mode 100644
index 0000000000000000000000000000000000000000..d2657b39ef58517c76d83b173bda62aad4e3e73b
--- /dev/null
+++ b/external_models/TangoFlux/train.sh
@@ -0,0 +1,2 @@
+
+CUDA_VISISBLE_DEVICES=0,1 accelerate launch --config_file='configs/accelerator_config.yaml' tangoflux/train.py --checkpointing_steps="best" --save_every=5 --config='configs/tangoflux_config.yaml'
\ No newline at end of file
diff --git a/external_models/depth-fm/.gitignore b/external_models/depth-fm/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..2e6006cf223954e5589a5d74893d268e454691dc
--- /dev/null
+++ b/external_models/depth-fm/.gitignore
@@ -0,0 +1,5 @@
+*__pycache__*
+sandbox
+*.ckpt
+*-depth.png
+evaluation
\ No newline at end of file
diff --git a/external_models/depth-fm/LICENSE b/external_models/depth-fm/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..dc54d0241c4e4cd1a5397f2e134fea4aa017414b
--- /dev/null
+++ b/external_models/depth-fm/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 CompVis - Computer Vision and Learning LMU Munich
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/external_models/depth-fm/README.md b/external_models/depth-fm/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..f6b5a75faee737dedde08c993babdc9c78de9f5b
--- /dev/null
+++ b/external_models/depth-fm/README.md
@@ -0,0 +1,108 @@
+
+
+
DepthFM: Fast Monocular Depth Estimation with Flow Matching
+
+ Ming Gui* · Johannes Schusterbauer* · Ulrich Prestel · Pingchuan Ma
+
+ Dmytro Kotovenko · Olga Grebenkova · Stefan A. Baumann · Vincent Tao Hu · Björn Ommer
+
+
+ CompVis Group @ LMU Munich
+
+
+ AAAI 2025
+
+ * equal contribution
+
+
+
+
+[](https://depthfm.github.io)
+[](https://arxiv.org/abs/2403.13788)
+
+
+
+
+
+## 📻 Overview
+
+We present **DepthFM**, a state-of-the-art, versatile, and fast monocular depth estimation model. DepthFM is efficient and can synthesize realistic depth maps within *a single inference* step. Beyond conventional depth estimation tasks, DepthFM also demonstrates state-of-the-art capabilities in downstream tasks such as depth inpainting and depth conditional synthesis.
+
+With our work we demonstrate the successful transfer of strong image priors from a foundation image synthesis diffusion model (Stable Diffusion v2-1) to a flow matching model. Instead of starting from noise, we directly map from input image to depth map.
+
+
+## 🛠️ Setup
+
+This setup was tested with `Ubuntu 22.04.4 LTS`, `CUDA Version: 12.4`, and `Python 3.10.12`.
+
+First, clone the github repo...
+
+```bash
+git clone git@github.com:CompVis/depth-fm.git
+cd depth-fm
+```
+
+Then download the weights via
+
+```bash
+wget https://ommer-lab.com/files/depthfm/depthfm-v1.ckpt -P checkpoints/
+```
+
+Now you have either the option to setup a virtual environment and install all required packages with `pip`
+
+```bash
+pip install -r requirements.txt
+```
+
+or if you prefer to use `conda` create the conda environment via
+
+```bash
+conda env create -f environment.yml
+```
+
+Now you should be able to listen to DepthFM! 📻 🎶
+
+
+## 🚀 Usage
+
+You can either use the notebook `inference.ipynb` or just run the python script `inference.py` as follows
+
+```bash
+python inference.py \
+ --num_steps 2 \
+ --ensemble_size 4 \
+ --img assets/dog.png \
+ --ckpt checkpoints/depthfm-v1.ckpt
+```
+
+The argument `--num_steps` allows you to set the number of function evaluations. We find that our model already gives very good results with as few as one or two steps. Ensembling also improves performance, so you can set it via the `--ensemble_size` argument. Currently, the inference code only supports a batch size of one for ensembling.
+
+## 📈 Results
+
+Our quantitative analysis shows that despite being substantially more efficient, our DepthFM performs on-par or even outperforms the current state-of-the-art generative depth estimator Marigold **zero-shot** on a range of benchmark datasets. Below you can find a quantitative comparison of DepthFM against other affine-invariant depth estimators on several benchmarks.
+
+
+
+
+
+## Trend
+
+[](https://star-history.com/#CompVis/depth-fm&Date)
+
+
+
+
+## 🎓 Citation
+
+Please cite our paper:
+
+```bibtex
+@misc{gui2024depthfm,
+ title={DepthFM: Fast Monocular Depth Estimation with Flow Matching},
+ author={Ming Gui and Johannes Schusterbauer and Ulrich Prestel and Pingchuan Ma and Dmytro Kotovenko and Olga Grebenkova and Stefan Andreas Baumann and Vincent Tao Hu and Björn Ommer},
+ year={2024},
+ eprint={2403.13788},
+ archivePrefix={arXiv},
+ primaryClass={cs.CV}
+}
+```
diff --git a/external_models/depth-fm/assets/dog.png b/external_models/depth-fm/assets/dog.png
new file mode 100644
index 0000000000000000000000000000000000000000..94a10e7dce9a1ee2cba811754b89c5007e16b38d
--- /dev/null
+++ b/external_models/depth-fm/assets/dog.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:89c61df823ceb1302262c5da4856007589723791ae7dc24d594ece6df8e4eaeb
+size 336739
diff --git a/external_models/depth-fm/assets/figures/badge-website.svg b/external_models/depth-fm/assets/figures/badge-website.svg
new file mode 100644
index 0000000000000000000000000000000000000000..7231a99946f500887b2f8197b64cd7e30f995742
--- /dev/null
+++ b/external_models/depth-fm/assets/figures/badge-website.svg
@@ -0,0 +1,129 @@
+
+
diff --git a/external_models/depth-fm/assets/figures/dfm-cover.png b/external_models/depth-fm/assets/figures/dfm-cover.png
new file mode 100644
index 0000000000000000000000000000000000000000..0f004d94a5c434436447a5477198a3bb1a9dc6b9
--- /dev/null
+++ b/external_models/depth-fm/assets/figures/dfm-cover.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:176897efdf08240716ecab620c4c18c53769696e2f47b1752f0d0c5ddc044af5
+size 3188460
diff --git a/external_models/depth-fm/assets/figures/radio.png b/external_models/depth-fm/assets/figures/radio.png
new file mode 100644
index 0000000000000000000000000000000000000000..ccb98fb3195d32d26f065ebd43b509268439eb46
--- /dev/null
+++ b/external_models/depth-fm/assets/figures/radio.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:61420ab8bffcff6fa148e9571cc22b1df41270f5f30b77194f04e45d921a2282
+size 311620
diff --git a/external_models/depth-fm/checkpoints/README.md b/external_models/depth-fm/checkpoints/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..a9f55ed7a6548b8e5713e7afef17aefa83df0f4f
--- /dev/null
+++ b/external_models/depth-fm/checkpoints/README.md
@@ -0,0 +1,5 @@
+Download the weights in this specific folder via
+
+```bash
+wget https://ommer-lab.com/files/depthfm/depthfm-v1.ckpt
+```
\ No newline at end of file
diff --git a/external_models/depth-fm/depthfm/__init__.py b/external_models/depth-fm/depthfm/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..f2a14c291a9c6ea66ecb04a689080a2022227755
--- /dev/null
+++ b/external_models/depth-fm/depthfm/__init__.py
@@ -0,0 +1,5 @@
+import os
+import sys
+sys.path.append(os.path.dirname(os.path.abspath(__file__)))
+from dfm import DepthFM
+from unet import UNetModel
diff --git a/external_models/depth-fm/depthfm/dfm.py b/external_models/depth-fm/depthfm/dfm.py
new file mode 100644
index 0000000000000000000000000000000000000000..481465042a81a98a4833dcc8212a17d096bc288d
--- /dev/null
+++ b/external_models/depth-fm/depthfm/dfm.py
@@ -0,0 +1,157 @@
+import torch
+import einops
+import numpy as np
+import torch.nn as nn
+from torch import Tensor
+from functools import partial
+from torchdiffeq import odeint
+
+from unet import UNetModel
+from diffusers import AutoencoderKL
+
+
+def exists(val):
+ return val is not None
+
+
+class DepthFM(nn.Module):
+ def __init__(self, ckpt_path: str):
+ super().__init__()
+ vae_id = "runwayml/stable-diffusion-v1-5"
+ self.vae = AutoencoderKL.from_pretrained(vae_id, subfolder="vae")
+ self.scale_factor = 0.18215
+
+ # set with checkpoint
+ ckpt = torch.load(ckpt_path, map_location="cpu")
+ self.noising_step = ckpt['noising_step']
+ self.empty_text_embed = ckpt['empty_text_embedding']
+ self.model = UNetModel(**ckpt['ldm_hparams'])
+ self.model.load_state_dict(ckpt['state_dict'])
+
+ def ode_fn(self, t: Tensor, x: Tensor, **kwargs):
+ if t.numel() == 1:
+ t = t.expand(x.size(0))
+ return self.model(x=x, t=t, **kwargs)
+
+ def generate(self, z: Tensor, num_steps: int = 4, n_intermediates: int = 0, **kwargs):
+ """
+ ODE solving from z0 (ims) to z1 (depth).
+ """
+ ode_kwargs = dict(method="euler", rtol=1e-5, atol=1e-5, options=dict(step_size=1.0 / num_steps))
+
+ # t specifies which intermediate times should the solver return
+ # e.g. t = [0, 0.5, 1] means return the solution at t=0, t=0.5 and t=1
+ # but it also specifies the number of steps for fixed step size methods
+ t = torch.linspace(0, 1, n_intermediates + 2, device=z.device, dtype=z.dtype)
+ # t = torch.tensor([0., 1.], device=z.device, dtype=z.dtype)
+
+ # allow conditioning information for model
+ ode_fn = partial(self.ode_fn, **kwargs)
+
+ ode_results = odeint(ode_fn, z, t, **ode_kwargs)
+
+ if n_intermediates > 0:
+ return ode_results
+ return ode_results[-1]
+
+ def forward(self, ims: Tensor, num_steps: int = 4, ensemble_size: int = 1):
+ """
+ Args:
+ ims: Tensor of shape (b, 3, h, w) in range [-1, 1]
+ Returns:
+ depth: Tensor of shape (b, 1, h, w) in range [0, 1]
+ """
+ if ensemble_size > 1:
+ assert ims.shape[0] == 1, "Ensemble mode only supported with batch size 1"
+ ims = ims.repeat(ensemble_size, 1, 1, 1)
+
+ bs, dev = ims.shape[0], ims.device
+
+ ims_z = self.encode(ims, sample_posterior=False)
+
+ conditioning = torch.tensor(self.empty_text_embed).to(dev).repeat(bs, 1, 1)
+ context = ims_z
+
+ x_source = ims_z
+
+ if self.noising_step > 0:
+ x_source = q_sample(x_source, self.noising_step)
+
+ # solve ODE
+ depth_z = self.generate(x_source, num_steps=num_steps, context=context, context_ca=conditioning)
+
+ depth = self.decode(depth_z)
+ depth = depth.mean(dim=1, keepdim=True)
+
+ if ensemble_size > 1:
+ depth = depth.mean(dim=0, keepdim=True)
+
+ # normalize depth maps to range [-1, 1]
+ depth = per_sample_min_max_normalization(depth.exp())
+
+ return depth
+
+ @torch.no_grad()
+ def predict_depth(self, ims: Tensor, num_steps: int = 4, ensemble_size: int = 1):
+ """ Inference method for DepthFM. """
+ return self.forward(ims, num_steps, ensemble_size)
+
+ @torch.no_grad()
+ def encode(self, x: Tensor, sample_posterior: bool = True):
+ posterior = self.vae.encode(x)
+ if sample_posterior:
+ z = posterior.latent_dist.sample()
+ else:
+ z = posterior.latent_dist.mode()
+ # normalize latent code
+ z = z * self.scale_factor
+ return z
+
+ @torch.no_grad()
+ def decode(self, z: Tensor):
+ z = 1.0 / self.scale_factor * z
+ return self.vae.decode(z).sample
+
+
+def sigmoid(x):
+ return 1 / (1 + np.exp(-x))
+
+
+def cosine_log_snr(t, eps=0.00001):
+ """
+ Returns log Signal-to-Noise ratio for time step t and image size 64
+ eps: avoid division by zero
+ """
+ return -2 * np.log(np.tan((np.pi * t) / 2) + eps)
+
+
+def cosine_alpha_bar(t):
+ return sigmoid(cosine_log_snr(t))
+
+
+def q_sample(x_start: torch.Tensor, t: int, noise: torch.Tensor = None, n_diffusion_timesteps: int = 1000):
+ """
+ Diffuse the data for a given number of diffusion steps. In other
+ words sample from q(x_t | x_0).
+ """
+ dev = x_start.device
+ dtype = x_start.dtype
+
+ if noise is None:
+ noise = torch.randn_like(x_start)
+
+ alpha_bar_t = cosine_alpha_bar(t / n_diffusion_timesteps)
+ alpha_bar_t = torch.tensor(alpha_bar_t).to(dev).to(dtype)
+
+ return torch.sqrt(alpha_bar_t) * x_start + torch.sqrt(1 - alpha_bar_t) * noise
+
+
+def per_sample_min_max_normalization(x):
+ """ Normalize each sample in a batch independently
+ with min-max normalization to [0, 1] """
+ bs, *shape = x.shape
+ x_ = einops.rearrange(x, "b ... -> b (...)")
+ min_val = einops.reduce(x_, "b ... -> b", "min")[..., None]
+ max_val = einops.reduce(x_, "b ... -> b", "max")[..., None]
+ x_ = (x_ - min_val) / (max_val - min_val)
+ return x_.reshape(bs, *shape)
diff --git a/external_models/depth-fm/depthfm/unet/__init__.py b/external_models/depth-fm/depthfm/unet/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..b3d1ebb76c9940d37ea3b36bce633a4f79bad988
--- /dev/null
+++ b/external_models/depth-fm/depthfm/unet/__init__.py
@@ -0,0 +1,4 @@
+import os
+import sys
+sys.path.append(os.path.dirname(os.path.abspath(__file__)))
+from openaimodel import UNetModel
\ No newline at end of file
diff --git a/external_models/depth-fm/depthfm/unet/attention.py b/external_models/depth-fm/depthfm/unet/attention.py
new file mode 100644
index 0000000000000000000000000000000000000000..6a30ab12de8378274bb4bb429c774b8511ae8cdb
--- /dev/null
+++ b/external_models/depth-fm/depthfm/unet/attention.py
@@ -0,0 +1,374 @@
+import math
+import torch
+from torch import nn
+from einops import rearrange
+from inspect import isfunction
+import torch.nn.functional as F
+from typing import Optional, Any
+
+from util import checkpoint
+
+
+try:
+ import xformers
+ import xformers.ops
+ XFORMERS_IS_AVAILBLE = True
+except:
+ print("WARNING: xformers is not available, inference might be slow.")
+ XFORMERS_IS_AVAILBLE = False
+
+# CrossAttn precision handling
+import os
+
+_ATTN_PRECISION = os.environ.get("ATTN_PRECISION", "fp32")
+
+
+def exists(val):
+ return val is not None
+
+
+def uniq(arr):
+ return {el: True for el in arr}.keys()
+
+
+def default(val, d):
+ if exists(val):
+ return val
+ return d() if isfunction(d) else d
+
+
+def max_neg_value(t):
+ return -torch.finfo(t.dtype).max
+
+
+def init_(tensor):
+ dim = tensor.shape[-1]
+ std = 1 / math.sqrt(dim)
+ tensor.uniform_(-std, std)
+ return tensor
+
+
+# feedforward
+class GEGLU(nn.Module):
+ def __init__(self, dim_in, dim_out):
+ super().__init__()
+ self.proj = nn.Linear(dim_in, dim_out * 2)
+
+ def forward(self, x):
+ x, gate = self.proj(x).chunk(2, dim=-1)
+ return x * F.gelu(gate)
+
+
+class FeedForward(nn.Module):
+ def __init__(self, dim, dim_out=None, mult=4, glu=False, dropout=0.0):
+ super().__init__()
+ inner_dim = int(dim * mult)
+ dim_out = default(dim_out, dim)
+ project_in = (
+ nn.Sequential(nn.Linear(dim, inner_dim), nn.GELU())
+ if not glu
+ else GEGLU(dim, inner_dim)
+ )
+
+ self.net = nn.Sequential(
+ project_in, nn.Dropout(dropout), nn.Linear(inner_dim, dim_out)
+ )
+
+ def forward(self, x):
+ return self.net(x)
+
+
+def zero_module(module):
+ """
+ Zero out the parameters of a module and return it.
+ """
+ for p in module.parameters():
+ p.detach().zero_()
+ return module
+
+
+def Normalize(in_channels):
+ return torch.nn.GroupNorm(
+ num_groups=32, num_channels=in_channels, eps=1e-6, affine=True
+ )
+
+
+class SpatialSelfAttention(nn.Module):
+ def __init__(self, in_channels):
+ super().__init__()
+ self.in_channels = in_channels
+
+ self.norm = Normalize(in_channels)
+ self.q = torch.nn.Conv2d(
+ in_channels, in_channels, kernel_size=1, stride=1, padding=0
+ )
+ self.k = torch.nn.Conv2d(
+ in_channels, in_channels, kernel_size=1, stride=1, padding=0
+ )
+ self.v = torch.nn.Conv2d(
+ in_channels, in_channels, kernel_size=1, stride=1, padding=0
+ )
+ self.proj_out = torch.nn.Conv2d(
+ in_channels, in_channels, kernel_size=1, stride=1, padding=0
+ )
+
+ def forward(self, x):
+ h_ = x
+ h_ = self.norm(h_)
+ q = self.q(h_)
+ k = self.k(h_)
+ v = self.v(h_)
+
+ # compute attention
+ b, c, h, w = q.shape
+ q = rearrange(q, "b c h w -> b (h w) c")
+ k = rearrange(k, "b c h w -> b c (h w)")
+ w_ = torch.einsum("bij,bjk->bik", q, k)
+
+ w_ = w_ * (int(c) ** (-0.5))
+ w_ = torch.nn.functional.softmax(w_, dim=2)
+
+ # attend to values
+ v = rearrange(v, "b c h w -> b c (h w)")
+ w_ = rearrange(w_, "b i j -> b j i")
+ h_ = torch.einsum("bij,bjk->bik", v, w_)
+ h_ = rearrange(h_, "b c (h w) -> b c h w", h=h)
+ h_ = self.proj_out(h_)
+
+ return x + h_
+
+
+class CrossAttention(nn.Module):
+ def __init__(self, query_dim, context_dim=None, heads=8, dim_head=64, dropout=0.0):
+ super().__init__()
+ inner_dim = dim_head * heads
+ context_dim = default(context_dim, query_dim)
+
+ self.dim_head = dim_head
+
+ self.scale = dim_head**-0.5
+ self.heads = heads
+
+ self.to_q = nn.Linear(query_dim, inner_dim, bias=False)
+ self.to_k = nn.Linear(context_dim, inner_dim, bias=False)
+ self.to_v = nn.Linear(context_dim, inner_dim, bias=False)
+
+ self.to_out = nn.Sequential(
+ nn.Linear(inner_dim, query_dim), nn.Dropout(dropout)
+ )
+
+ def forward(self, x, context=None, mask=None, rescale_attention=True):
+
+ is_self_attention = context is None
+
+ n_tokens = x.shape[1]
+
+ h = self.heads
+
+ q = self.to_q(x)
+ context = default(context, x)
+ k = self.to_k(context)
+ v = self.to_v(context)
+
+ q, k, v = map(lambda t: rearrange(t, "b n (h d) -> (b h) n d", h=h), (q, k, v))
+
+ if rescale_attention:
+ out = F.scaled_dot_product_attention(q, k, v, scale=(math.log(n_tokens) / math.log(n_tokens*4) / self.dim_head)**0.5 if is_self_attention else None)
+ else:
+ out = F.scaled_dot_product_attention(q, k, v)
+
+ out = rearrange(out, "(b h) n d -> b n (h d)", h=h)
+ return self.to_out(out)
+
+
+class MemoryEfficientCrossAttention(nn.Module):
+ # https://github.com/MatthieuTPHR/diffusers/blob/d80b531ff8060ec1ea982b65a1b8df70f73aa67c/src/diffusers/models/attention.py#L223
+ def __init__(self, query_dim, context_dim=None, heads=8, dim_head=64, dropout=0.0):
+ super().__init__()
+ # print(
+ # f"Setting up {self.__class__.__name__}. Query dim is {query_dim}, context_dim is {context_dim} and using "
+ # f"{heads} heads."
+ # )
+ inner_dim = dim_head * heads
+ context_dim = default(context_dim, query_dim)
+
+ self.heads = heads
+ self.dim_head = dim_head
+
+ self.to_q = nn.Linear(query_dim, inner_dim, bias=False)
+ self.to_k = nn.Linear(context_dim, inner_dim, bias=False)
+ self.to_v = nn.Linear(context_dim, inner_dim, bias=False)
+
+ self.to_out = nn.Sequential(
+ nn.Linear(inner_dim, query_dim), nn.Dropout(dropout)
+ )
+ self.attention_op: Optional[Any] = None
+
+ def forward(self, x, context=None, mask=None):
+ q = self.to_q(x)
+ context = default(context, x)
+ k = self.to_k(context)
+ v = self.to_v(context)
+
+ b, _, _ = q.shape
+ q, k, v = map(
+ lambda t: t.unsqueeze(3)
+ .reshape(b, t.shape[1], self.heads, self.dim_head)
+ .permute(0, 2, 1, 3)
+ .reshape(b * self.heads, t.shape[1], self.dim_head)
+ .contiguous(),
+ (q, k, v),
+ )
+
+ # actually compute the attention, what we cannot get enough of
+ out = xformers.ops.memory_efficient_attention(
+ q, k, v, attn_bias=None, op=self.attention_op
+ )
+
+ if exists(mask):
+ raise NotImplementedError
+ out = (
+ out.unsqueeze(0)
+ .reshape(b, self.heads, out.shape[1], self.dim_head)
+ .permute(0, 2, 1, 3)
+ .reshape(b, out.shape[1], self.heads * self.dim_head)
+ )
+ return self.to_out(out)
+
+
+class BasicTransformerBlock(nn.Module):
+ ATTENTION_MODES = {
+ "softmax": CrossAttention, # vanilla attention
+ "softmax-xformers": MemoryEfficientCrossAttention,
+ }
+
+ def __init__(
+ self,
+ dim,
+ n_heads,
+ d_head,
+ dropout=0.0,
+ context_dim=None,
+ gated_ff=True,
+ checkpoint=True,
+ disable_self_attn=False,
+ ):
+ super().__init__()
+ attn_mode = "softmax-xformers" if XFORMERS_IS_AVAILBLE else "softmax"
+ assert attn_mode in self.ATTENTION_MODES
+ attn_cls = self.ATTENTION_MODES[attn_mode]
+ self.disable_self_attn = disable_self_attn
+ self.attn1 = attn_cls(
+ query_dim=dim,
+ heads=n_heads,
+ dim_head=d_head,
+ dropout=dropout,
+ context_dim=context_dim if self.disable_self_attn else None,
+ ) # is a self-attention if not self.disable_self_attn
+ self.ff = FeedForward(dim, dropout=dropout, glu=gated_ff)
+ self.attn2 = attn_cls(
+ query_dim=dim,
+ context_dim=context_dim,
+ heads=n_heads,
+ dim_head=d_head,
+ dropout=dropout,
+ ) # is self-attn if context is none
+ self.norm1 = nn.LayerNorm(dim)
+ self.norm2 = nn.LayerNorm(dim)
+ self.norm3 = nn.LayerNorm(dim)
+ self.checkpoint = checkpoint
+
+ def forward(self, x, context=None):
+ return checkpoint(
+ self._forward, (x, context), self.parameters(), self.checkpoint
+ )
+
+ def _forward(self, x, context=None):
+ x = (
+ self.attn1(
+ self.norm1(x), context=context if self.disable_self_attn else None
+ )
+ + x
+ )
+ x = self.attn2(self.norm2(x), context=context) + x
+ x = self.ff(self.norm3(x)) + x
+ return x
+
+
+class SpatialTransformer(nn.Module):
+ """
+ Transformer block for image-like data.
+ First, project the input (aka embedding)
+ and reshape to b, t, d.
+ Then apply standard transformer action.
+ Finally, reshape to image
+ NEW: use_linear for more efficiency instead of the 1x1 convs
+ """
+
+ def __init__(
+ self,
+ in_channels,
+ n_heads,
+ d_head,
+ depth=1,
+ dropout=0.0,
+ context_dim=None,
+ disable_self_attn=False,
+ use_linear=False,
+ use_checkpoint=True,
+ ):
+ super().__init__()
+ if exists(context_dim) and not isinstance(context_dim, list):
+ context_dim = [context_dim]
+ self.in_channels = in_channels
+ inner_dim = n_heads * d_head
+ self.norm = Normalize(in_channels)
+ if not use_linear:
+ self.proj_in = nn.Conv2d(
+ in_channels, inner_dim, kernel_size=1, stride=1, padding=0
+ )
+ else:
+ self.proj_in = nn.Linear(in_channels, inner_dim)
+
+ self.transformer_blocks = nn.ModuleList(
+ [
+ BasicTransformerBlock(
+ inner_dim,
+ n_heads,
+ d_head,
+ dropout=dropout,
+ context_dim=context_dim[d],
+ disable_self_attn=disable_self_attn,
+ checkpoint=use_checkpoint,
+ )
+ for d in range(depth)
+ ]
+ )
+ if not use_linear:
+ self.proj_out = zero_module(
+ nn.Conv2d(inner_dim, in_channels, kernel_size=1, stride=1, padding=0)
+ )
+ else:
+ self.proj_out = zero_module(nn.Linear(in_channels, inner_dim))
+ self.use_linear = use_linear
+
+ def forward(self, x, context=None):
+ # note: if no context is given, cross-attention defaults to self-attention
+ if not isinstance(context, list):
+ context = [context]
+ b, c, h, w = x.shape
+ x_in = x
+ x = self.norm(x)
+ if not self.use_linear:
+ x = self.proj_in(x)
+ x = rearrange(x, "b c h w -> b (h w) c").contiguous()
+ if self.use_linear:
+ x = self.proj_in(x)
+ for i, block in enumerate(self.transformer_blocks):
+ x = block(x, context=context[i])
+ if self.use_linear:
+ x = self.proj_out(x)
+ x = rearrange(x, "b (h w) c -> b c h w", h=h, w=w).contiguous()
+ if not self.use_linear:
+ x = self.proj_out(x)
+ return x + x_in
diff --git a/external_models/depth-fm/depthfm/unet/openaimodel.py b/external_models/depth-fm/depthfm/unet/openaimodel.py
new file mode 100644
index 0000000000000000000000000000000000000000..69eadf1c97acb6241a7f057f02c818ce4c1058a3
--- /dev/null
+++ b/external_models/depth-fm/depthfm/unet/openaimodel.py
@@ -0,0 +1,894 @@
+import math
+import numpy as np
+import torch as th
+import torch.nn as nn
+from abc import abstractmethod
+import torch.nn.functional as F
+
+from util import (
+ checkpoint,
+ conv_nd,
+ linear,
+ avg_pool_nd,
+ zero_module,
+ normalization,
+ timestep_embedding,
+)
+from attention import SpatialTransformer
+
+
+def exists(x):
+ return x is not None
+
+# dummy replace
+def convert_module_to_f16(x):
+ pass
+
+def convert_module_to_f32(x):
+ pass
+
+
+## go
+class AttentionPool2d(nn.Module):
+ """
+ Adapted from CLIP: https://github.com/openai/CLIP/blob/main/clip/model.py
+ """
+
+ def __init__(
+ self,
+ spacial_dim: int,
+ embed_dim: int,
+ num_heads_channels: int,
+ output_dim: int = None,
+ ):
+ super().__init__()
+ self.positional_embedding = nn.Parameter(th.randn(embed_dim, spacial_dim ** 2 + 1) / embed_dim ** 0.5)
+ self.qkv_proj = conv_nd(1, embed_dim, 3 * embed_dim, 1)
+ self.c_proj = conv_nd(1, embed_dim, output_dim or embed_dim, 1)
+ self.num_heads = embed_dim // num_heads_channels
+ self.attention = QKVAttention(self.num_heads)
+
+ def forward(self, x):
+ b, c, *_spatial = x.shape
+ x = x.reshape(b, c, -1) # NC(HW)
+ x = th.cat([x.mean(dim=-1, keepdim=True), x], dim=-1) # NC(HW+1)
+ x = x + self.positional_embedding[None, :, :].to(x.dtype) # NC(HW+1)
+ x = self.qkv_proj(x)
+ x = self.attention(x)
+ x = self.c_proj(x)
+ return x[:, :, 0]
+
+
+class TimestepBlock(nn.Module):
+ """
+ Any module where forward() takes timestep embeddings as a second argument.
+ """
+
+ @abstractmethod
+ def forward(self, x, emb):
+ """
+ Apply the module to `x` given `emb` timestep embeddings.
+ """
+
+
+class TimestepEmbedSequential(nn.Sequential, TimestepBlock):
+ """
+ A sequential module that passes timestep embeddings to the children that
+ support it as an extra input.
+ """
+
+ def forward(self, x, emb, context=None):
+ for layer in self:
+ if isinstance(layer, TimestepBlock):
+ x = layer(x, emb)
+ elif isinstance(layer, SpatialTransformer):
+ x = layer(x, context)
+ else:
+ x = layer(x)
+ return x
+
+
+class Upsample(nn.Module):
+ """
+ An upsampling layer with an optional convolution.
+ :param channels: channels in the inputs and outputs.
+ :param use_conv: a bool determining if a convolution is applied.
+ :param dims: determines if the signal is 1D, 2D, or 3D. If 3D, then
+ upsampling occurs in the inner-two dimensions.
+ """
+
+ def __init__(self, channels, use_conv, dims=2, out_channels=None, padding=1):
+ super().__init__()
+ self.channels = channels
+ self.out_channels = out_channels or channels
+ self.use_conv = use_conv
+ self.dims = dims
+ if use_conv:
+ self.conv = conv_nd(dims, self.channels, self.out_channels, 3, padding=padding)
+
+ def forward(self, x):
+ assert x.shape[1] == self.channels
+ if self.dims == 3:
+ x = F.interpolate(
+ x, (x.shape[2], x.shape[3] * 2, x.shape[4] * 2), mode="nearest"
+ )
+ else:
+ x = F.interpolate(x, scale_factor=2, mode="nearest")
+ if self.use_conv:
+ x = self.conv(x)
+ return x
+
+class TransposedUpsample(nn.Module):
+ 'Learned 2x upsampling without padding'
+ def __init__(self, channels, out_channels=None, ks=5):
+ super().__init__()
+ self.channels = channels
+ self.out_channels = out_channels or channels
+
+ self.up = nn.ConvTranspose2d(self.channels,self.out_channels,kernel_size=ks,stride=2)
+
+ def forward(self,x):
+ return self.up(x)
+
+
+class Downsample(nn.Module):
+ """
+ A downsampling layer with an optional convolution.
+ :param channels: channels in the inputs and outputs.
+ :param use_conv: a bool determining if a convolution is applied.
+ :param dims: determines if the signal is 1D, 2D, or 3D. If 3D, then
+ downsampling occurs in the inner-two dimensions.
+ """
+
+ def __init__(self, channels, use_conv, dims=2, out_channels=None,padding=1):
+ super().__init__()
+ self.channels = channels
+ self.out_channels = out_channels or channels
+ self.use_conv = use_conv
+ self.dims = dims
+ stride = 2 if dims != 3 else (1, 2, 2)
+ if use_conv:
+ self.op = conv_nd(
+ dims, self.channels, self.out_channels, 3, stride=stride, padding=padding
+ )
+ else:
+ assert self.channels == self.out_channels
+ self.op = avg_pool_nd(dims, kernel_size=stride, stride=stride)
+
+ def forward(self, x):
+ assert x.shape[1] == self.channels
+ return self.op(x)
+
+
+class ResBlock(TimestepBlock):
+ """
+ A residual block that can optionally change the number of channels.
+ :param channels: the number of input channels.
+ :param emb_channels: the number of timestep embedding channels.
+ :param dropout: the rate of dropout.
+ :param out_channels: if specified, the number of out channels.
+ :param use_conv: if True and out_channels is specified, use a spatial
+ convolution instead of a smaller 1x1 convolution to change the
+ channels in the skip connection.
+ :param dims: determines if the signal is 1D, 2D, or 3D.
+ :param use_checkpoint: if True, use gradient checkpointing on this module.
+ :param up: if True, use this block for upsampling.
+ :param down: if True, use this block for downsampling.
+ """
+
+ def __init__(
+ self,
+ channels,
+ emb_channels,
+ dropout,
+ out_channels=None,
+ use_conv=False,
+ use_scale_shift_norm=False,
+ dims=2,
+ use_checkpoint=False,
+ up=False,
+ down=False,
+ ):
+ super().__init__()
+ self.channels = channels
+ self.emb_channels = emb_channels
+ self.dropout = dropout
+ self.out_channels = out_channels or channels
+ self.use_conv = use_conv
+ self.use_checkpoint = use_checkpoint
+ self.use_scale_shift_norm = use_scale_shift_norm
+
+ self.in_layers = nn.Sequential(
+ normalization(channels),
+ nn.SiLU(),
+ conv_nd(dims, channels, self.out_channels, 3, padding=1),
+ )
+
+ self.updown = up or down
+
+ if up:
+ self.h_upd = Upsample(channels, False, dims)
+ self.x_upd = Upsample(channels, False, dims)
+ elif down:
+ self.h_upd = Downsample(channels, False, dims)
+ self.x_upd = Downsample(channels, False, dims)
+ else:
+ self.h_upd = self.x_upd = nn.Identity()
+
+ self.emb_layers = nn.Sequential(
+ nn.SiLU(),
+ linear(
+ emb_channels,
+ 2 * self.out_channels if use_scale_shift_norm else self.out_channels,
+ ),
+ )
+ self.out_layers = nn.Sequential(
+ normalization(self.out_channels),
+ nn.SiLU(),
+ nn.Dropout(p=dropout),
+ zero_module(
+ conv_nd(dims, self.out_channels, self.out_channels, 3, padding=1)
+ ),
+ )
+
+ if self.out_channels == channels:
+ self.skip_connection = nn.Identity()
+ elif use_conv:
+ self.skip_connection = conv_nd(
+ dims, channels, self.out_channels, 3, padding=1
+ )
+ else:
+ self.skip_connection = conv_nd(dims, channels, self.out_channels, 1)
+
+ def forward(self, x, emb):
+ """
+ Apply the block to a Tensor, conditioned on a timestep embedding.
+ :param x: an [N x C x ...] Tensor of features.
+ :param emb: an [N x emb_channels] Tensor of timestep embeddings.
+ :return: an [N x C x ...] Tensor of outputs.
+ """
+ return checkpoint(
+ self._forward, (x, emb), self.parameters(), self.use_checkpoint
+ )
+
+
+ def _forward(self, x, emb):
+ if self.updown:
+ in_rest, in_conv = self.in_layers[:-1], self.in_layers[-1]
+ h = in_rest(x)
+ h = self.h_upd(h)
+ x = self.x_upd(x)
+ h = in_conv(h)
+ else:
+ h = self.in_layers(x)
+ emb_out = self.emb_layers(emb).type(h.dtype)
+ while len(emb_out.shape) < len(h.shape):
+ emb_out = emb_out[..., None]
+ if self.use_scale_shift_norm:
+ out_norm, out_rest = self.out_layers[0], self.out_layers[1:]
+ scale, shift = th.chunk(emb_out, 2, dim=1)
+ h = out_norm(h) * (1 + scale) + shift
+ h = out_rest(h)
+ else:
+ h = h + emb_out
+ h = self.out_layers(h)
+ return self.skip_connection(x) + h
+
+
+class AttentionBlock(nn.Module):
+ """
+ An attention block that allows spatial positions to attend to each other.
+ Originally ported from here, but adapted to the N-d case.
+ https://github.com/hojonathanho/diffusion/blob/1e0dceb3b3495bbe19116a5e1b3596cd0706c543/diffusion_tf/models/unet.py#L66.
+ """
+
+ def __init__(
+ self,
+ channels,
+ num_heads=1,
+ num_head_channels=-1,
+ use_checkpoint=False,
+ use_new_attention_order=False,
+ ):
+ super().__init__()
+ self.channels = channels
+ if num_head_channels == -1:
+ self.num_heads = num_heads
+ else:
+ assert (
+ channels % num_head_channels == 0
+ ), f"q,k,v channels {channels} is not divisible by num_head_channels {num_head_channels}"
+ self.num_heads = channels // num_head_channels
+ self.use_checkpoint = use_checkpoint
+ self.norm = normalization(channels)
+ self.qkv = conv_nd(1, channels, channels * 3, 1)
+ if use_new_attention_order:
+ # split qkv before split heads
+ self.attention = QKVAttention(self.num_heads)
+ else:
+ # split heads before split qkv
+ self.attention = QKVAttentionLegacy(self.num_heads)
+
+ self.proj_out = zero_module(conv_nd(1, channels, channels, 1))
+
+ def forward(self, x):
+ return checkpoint(self._forward, (x,), self.parameters(), True) # TODO: check checkpoint usage, is True # TODO: fix the .half call!!!
+ #return pt_checkpoint(self._forward, x) # pytorch
+
+ def _forward(self, x):
+ b, c, *spatial = x.shape
+ x = x.reshape(b, c, -1)
+ qkv = self.qkv(self.norm(x))
+ h = self.attention(qkv)
+ h = self.proj_out(h)
+ return (x + h).reshape(b, c, *spatial)
+
+
+def count_flops_attn(model, _x, y):
+ """
+ A counter for the `thop` package to count the operations in an
+ attention operation.
+ Meant to be used like:
+ macs, params = thop.profile(
+ model,
+ inputs=(inputs, timestamps),
+ custom_ops={QKVAttention: QKVAttention.count_flops},
+ )
+ """
+ b, c, *spatial = y[0].shape
+ num_spatial = int(np.prod(spatial))
+ # We perform two matmuls with the same number of ops.
+ # The first computes the weight matrix, the second computes
+ # the combination of the value vectors.
+ matmul_ops = 2 * b * (num_spatial ** 2) * c
+ model.total_ops += th.DoubleTensor([matmul_ops])
+
+
+class QKVAttentionLegacy(nn.Module):
+ """
+ A module which performs QKV attention. Matches legacy QKVAttention + input/ouput heads shaping
+ """
+
+ def __init__(self, n_heads):
+ super().__init__()
+ self.n_heads = n_heads
+
+ def forward(self, qkv):
+ """
+ Apply QKV attention.
+ :param qkv: an [N x (H * 3 * C) x T] tensor of Qs, Ks, and Vs.
+ :return: an [N x (H * C) x T] tensor after attention.
+ """
+ bs, width, length = qkv.shape
+ assert width % (3 * self.n_heads) == 0
+ ch = width // (3 * self.n_heads)
+ q, k, v = qkv.reshape(bs * self.n_heads, ch * 3, length).split(ch, dim=1)
+ scale = 1 / math.sqrt(math.sqrt(ch))
+ weight = th.einsum(
+ "bct,bcs->bts", q * scale, k * scale
+ ) # More stable with f16 than dividing afterwards
+ weight = th.softmax(weight.float(), dim=-1).type(weight.dtype)
+ a = th.einsum("bts,bcs->bct", weight, v)
+ return a.reshape(bs, -1, length)
+
+ @staticmethod
+ def count_flops(model, _x, y):
+ return count_flops_attn(model, _x, y)
+
+
+class QKVAttention(nn.Module):
+ """
+ A module which performs QKV attention and splits in a different order.
+ """
+
+ def __init__(self, n_heads):
+ super().__init__()
+ self.n_heads = n_heads
+
+ def forward(self, qkv):
+ """
+ Apply QKV attention.
+ :param qkv: an [N x (3 * H * C) x T] tensor of Qs, Ks, and Vs.
+ :return: an [N x (H * C) x T] tensor after attention.
+ """
+ bs, width, length = qkv.shape
+ assert width % (3 * self.n_heads) == 0
+ ch = width // (3 * self.n_heads)
+ q, k, v = qkv.chunk(3, dim=1)
+ scale = 1 / math.sqrt(math.sqrt(ch))
+ weight = th.einsum(
+ "bct,bcs->bts",
+ (q * scale).view(bs * self.n_heads, ch, length),
+ (k * scale).view(bs * self.n_heads, ch, length),
+ ) # More stable with f16 than dividing afterwards
+ weight = th.softmax(weight.float(), dim=-1).type(weight.dtype)
+ a = th.einsum("bts,bcs->bct", weight, v.reshape(bs * self.n_heads, ch, length))
+ return a.reshape(bs, -1, length)
+
+ @staticmethod
+ def count_flops(model, _x, y):
+ return count_flops_attn(model, _x, y)
+
+
+class Timestep(nn.Module):
+ def __init__(self, dim):
+ super().__init__()
+ self.dim = dim
+
+ def forward(self, t):
+ return timestep_embedding(t, self.dim)
+
+
+class UNetModel(nn.Module):
+ """
+ The full UNet model with attention and timestep embedding.
+ :param in_channels: channels in the input Tensor.
+ :param model_channels: base channel count for the model.
+ :param out_channels: channels in the output Tensor.
+ :param num_res_blocks: number of residual blocks per downsample.
+ :param attention_resolutions: a collection of downsample rates at which
+ attention will take place. May be a set, list, or tuple.
+ For example, if this contains 4, then at 4x downsampling, attention
+ will be used.
+ :param dropout: the dropout probability.
+ :param channel_mult: channel multiplier for each level of the UNet.
+ :param conv_resample: if True, use learned convolutions for upsampling and
+ downsampling.
+ :param dims: determines if the signal is 1D, 2D, or 3D.
+ :param num_classes: if specified (as an int), then this model will be
+ class-conditional with `num_classes` classes.
+ :param use_checkpoint: use gradient checkpointing to reduce memory usage.
+ :param num_heads: the number of attention heads in each attention layer.
+ :param num_heads_channels: if specified, ignore num_heads and instead use
+ a fixed channel width per attention head.
+ :param num_heads_upsample: works with num_heads to set a different number
+ of heads for upsampling. Deprecated.
+ :param use_scale_shift_norm: use a FiLM-like conditioning mechanism.
+ :param resblock_updown: use residual blocks for up/downsampling.
+ :param use_new_attention_order: use a different attention pattern for potentially
+ increased efficiency.
+ """
+
+ def __init__(
+ self,
+ image_size,
+ in_channels,
+ model_channels,
+ out_channels,
+ num_res_blocks,
+ attention_resolutions,
+ dropout=0,
+ channel_mult=(1, 2, 4, 8),
+ conv_resample=True,
+ dims=2,
+ num_classes=None,
+ use_checkpoint=False,
+ use_fp16=False,
+ use_bf16=False,
+ num_heads=-1,
+ num_head_channels=-1,
+ num_heads_upsample=-1,
+ use_scale_shift_norm=False,
+ resblock_updown=False,
+ use_new_attention_order=False,
+ use_spatial_transformer=False, # custom transformer support
+ transformer_depth=1, # custom transformer support
+ context_dim=None, # custom transformer support
+ n_embed=None, # custom support for prediction of discrete ids into codebook of first stage vq model
+ legacy=True,
+ disable_self_attentions=None,
+ num_attention_blocks=None,
+ disable_middle_self_attn=False,
+ use_linear_in_transformer=False,
+ adm_in_channels=None,
+ load_from_ckpt=None,
+ ):
+ super().__init__()
+ if use_spatial_transformer:
+ assert context_dim is not None, 'Fool!! You forgot to include the dimension of your cross-attention conditioning...'
+
+ if context_dim is not None:
+ assert use_spatial_transformer, 'Fool!! You forgot to use the spatial transformer for your cross-attention conditioning...'
+ from omegaconf.listconfig import ListConfig
+ if type(context_dim) == ListConfig:
+ context_dim = list(context_dim)
+
+ if num_heads_upsample == -1:
+ num_heads_upsample = num_heads
+
+ if num_heads == -1:
+ assert num_head_channels != -1, 'Either num_heads or num_head_channels has to be set'
+
+ if num_head_channels == -1:
+ assert num_heads != -1, 'Either num_heads or num_head_channels has to be set'
+
+ self.image_size = image_size
+ self.in_channels = in_channels
+ self.model_channels = model_channels
+ self.out_channels = out_channels
+ if isinstance(num_res_blocks, int):
+ self.num_res_blocks = len(channel_mult) * [num_res_blocks]
+ else:
+ if len(num_res_blocks) != len(channel_mult):
+ raise ValueError("provide num_res_blocks either as an int (globally constant) or "
+ "as a list/tuple (per-level) with the same length as channel_mult")
+ self.num_res_blocks = num_res_blocks
+ if disable_self_attentions is not None:
+ # should be a list of booleans, indicating whether to disable self-attention in TransformerBlocks or not
+ assert len(disable_self_attentions) == len(channel_mult)
+ if num_attention_blocks is not None:
+ assert len(num_attention_blocks) == len(self.num_res_blocks)
+ assert all(map(lambda i: self.num_res_blocks[i] >= num_attention_blocks[i], range(len(num_attention_blocks))))
+ print(f"Constructor of UNetModel received num_attention_blocks={num_attention_blocks}. "
+ f"This option has LESS priority than attention_resolutions {attention_resolutions}, "
+ f"i.e., in cases where num_attention_blocks[i] > 0 but 2**i not in attention_resolutions, "
+ f"attention will still not be set.")
+
+ self.attention_resolutions = attention_resolutions
+ self.dropout = dropout
+ self.channel_mult = channel_mult
+ self.conv_resample = conv_resample
+ self.num_classes = num_classes
+ self.use_checkpoint = use_checkpoint
+ self.dtype = th.float16 if use_fp16 else th.float32
+ self.dtype = th.bfloat16 if use_bf16 else self.dtype
+ self.num_heads = num_heads
+ self.num_head_channels = num_head_channels
+ self.num_heads_upsample = num_heads_upsample
+ self.predict_codebook_ids = n_embed is not None
+
+ time_embed_dim = model_channels * 4
+ self.time_embed = nn.Sequential(
+ linear(model_channels, time_embed_dim),
+ nn.SiLU(),
+ linear(time_embed_dim, time_embed_dim),
+ )
+
+ if self.num_classes is not None:
+ if isinstance(self.num_classes, int):
+ self.label_emb = nn.Embedding(num_classes, time_embed_dim)
+ elif self.num_classes == "continuous":
+ print("setting up linear c_adm embedding layer")
+ self.label_emb = nn.Linear(1, time_embed_dim)
+ elif self.num_classes == "sequential":
+ assert adm_in_channels is not None
+ self.label_emb = nn.Sequential(
+ nn.Sequential(
+ linear(adm_in_channels, time_embed_dim),
+ nn.SiLU(),
+ linear(time_embed_dim, time_embed_dim),
+ )
+ )
+ else:
+ raise ValueError()
+
+ self.input_blocks = nn.ModuleList(
+ [
+ TimestepEmbedSequential(
+ conv_nd(dims, in_channels, model_channels, 3, padding=1)
+ )
+ ]
+ )
+ self._feature_size = model_channels
+ input_block_chans = [model_channels]
+ ch = model_channels
+ ds = 1
+ for level, mult in enumerate(channel_mult):
+ for nr in range(self.num_res_blocks[level]):
+ layers = [
+ ResBlock(
+ ch,
+ time_embed_dim,
+ dropout,
+ out_channels=mult * model_channels,
+ dims=dims,
+ use_checkpoint=use_checkpoint,
+ use_scale_shift_norm=use_scale_shift_norm,
+ )
+ ]
+ ch = mult * model_channels
+ if ds in attention_resolutions:
+ if num_head_channels == -1:
+ dim_head = ch // num_heads
+ else:
+ num_heads = ch // num_head_channels
+ dim_head = num_head_channels
+ if legacy:
+ #num_heads = 1
+ dim_head = ch // num_heads if use_spatial_transformer else num_head_channels
+ if exists(disable_self_attentions):
+ disabled_sa = disable_self_attentions[level]
+ else:
+ disabled_sa = False
+
+ if not exists(num_attention_blocks) or nr < num_attention_blocks[level]:
+ layers.append(
+ AttentionBlock(
+ ch,
+ use_checkpoint=use_checkpoint,
+ num_heads=num_heads,
+ num_head_channels=dim_head,
+ use_new_attention_order=use_new_attention_order,
+ ) if not use_spatial_transformer else SpatialTransformer(
+ ch, num_heads, dim_head, depth=transformer_depth, context_dim=context_dim,
+ disable_self_attn=disabled_sa, use_linear=use_linear_in_transformer,
+ use_checkpoint=use_checkpoint
+ )
+ )
+ self.input_blocks.append(TimestepEmbedSequential(*layers))
+ self._feature_size += ch
+ input_block_chans.append(ch)
+ if level != len(channel_mult) - 1:
+ out_ch = ch
+ self.input_blocks.append(
+ TimestepEmbedSequential(
+ ResBlock(
+ ch,
+ time_embed_dim,
+ dropout,
+ out_channels=out_ch,
+ dims=dims,
+ use_checkpoint=use_checkpoint,
+ use_scale_shift_norm=use_scale_shift_norm,
+ down=True,
+ )
+ if resblock_updown
+ else Downsample(
+ ch, conv_resample, dims=dims, out_channels=out_ch
+ )
+ )
+ )
+ ch = out_ch
+ input_block_chans.append(ch)
+ ds *= 2
+ self._feature_size += ch
+
+ if num_head_channels == -1:
+ dim_head = ch // num_heads
+ else:
+ num_heads = ch // num_head_channels
+ dim_head = num_head_channels
+ if legacy:
+ #num_heads = 1
+ dim_head = ch // num_heads if use_spatial_transformer else num_head_channels
+ self.middle_block = TimestepEmbedSequential(
+ ResBlock(
+ ch,
+ time_embed_dim,
+ dropout,
+ dims=dims,
+ use_checkpoint=use_checkpoint,
+ use_scale_shift_norm=use_scale_shift_norm,
+ ),
+ AttentionBlock(
+ ch,
+ use_checkpoint=use_checkpoint,
+ num_heads=num_heads,
+ num_head_channels=dim_head,
+ use_new_attention_order=use_new_attention_order,
+ ) if not use_spatial_transformer else SpatialTransformer( # always uses a self-attn
+ ch, num_heads, dim_head, depth=transformer_depth, context_dim=context_dim,
+ disable_self_attn=disable_middle_self_attn, use_linear=use_linear_in_transformer,
+ use_checkpoint=use_checkpoint
+ ),
+ ResBlock(
+ ch,
+ time_embed_dim,
+ dropout,
+ dims=dims,
+ use_checkpoint=use_checkpoint,
+ use_scale_shift_norm=use_scale_shift_norm,
+ ),
+ )
+ self._feature_size += ch
+
+ self.output_blocks = nn.ModuleList([])
+ for level, mult in list(enumerate(channel_mult))[::-1]:
+ for i in range(self.num_res_blocks[level] + 1):
+ ich = input_block_chans.pop()
+ layers = [
+ ResBlock(
+ ch + ich,
+ time_embed_dim,
+ dropout,
+ out_channels=model_channels * mult,
+ dims=dims,
+ use_checkpoint=use_checkpoint,
+ use_scale_shift_norm=use_scale_shift_norm,
+ )
+ ]
+ ch = model_channels * mult
+ if ds in attention_resolutions:
+ if num_head_channels == -1:
+ dim_head = ch // num_heads
+ else:
+ num_heads = ch // num_head_channels
+ dim_head = num_head_channels
+ if legacy:
+ #num_heads = 1
+ dim_head = ch // num_heads if use_spatial_transformer else num_head_channels
+ if exists(disable_self_attentions):
+ disabled_sa = disable_self_attentions[level]
+ else:
+ disabled_sa = False
+
+ if not exists(num_attention_blocks) or i < num_attention_blocks[level]:
+ layers.append(
+ AttentionBlock(
+ ch,
+ use_checkpoint=use_checkpoint,
+ num_heads=num_heads_upsample,
+ num_head_channels=dim_head,
+ use_new_attention_order=use_new_attention_order,
+ ) if not use_spatial_transformer else SpatialTransformer(
+ ch, num_heads, dim_head, depth=transformer_depth, context_dim=context_dim,
+ disable_self_attn=disabled_sa, use_linear=use_linear_in_transformer,
+ use_checkpoint=use_checkpoint
+ )
+ )
+ if level and i == self.num_res_blocks[level]:
+ out_ch = ch
+ layers.append(
+ ResBlock(
+ ch,
+ time_embed_dim,
+ dropout,
+ out_channels=out_ch,
+ dims=dims,
+ use_checkpoint=use_checkpoint,
+ use_scale_shift_norm=use_scale_shift_norm,
+ up=True,
+ )
+ if resblock_updown
+ else Upsample(ch, conv_resample, dims=dims, out_channels=out_ch)
+ )
+ ds //= 2
+ self.output_blocks.append(TimestepEmbedSequential(*layers))
+ self._feature_size += ch
+
+ self.out = nn.Sequential(
+ normalization(ch),
+ nn.SiLU(),
+ zero_module(conv_nd(dims, model_channels, out_channels, 3, padding=1)),
+ )
+ if self.predict_codebook_ids:
+ self.id_predictor = nn.Sequential(
+ normalization(ch),
+ conv_nd(dims, model_channels, n_embed, 1),
+ #nn.LogSoftmax(dim=1) # change to cross_entropy and produce non-normalized logits
+ )
+
+ if load_from_ckpt is not None:
+ self.load_from_ckpt(load_from_ckpt)
+
+ def load_from_ckpt(self, ckpt_path):
+ input_ch = self.state_dict()["input_blocks.0.0.weight"].shape[1]
+ assert input_ch >= 4 and input_ch // 4 * 4 == input_ch, "Input channels must be at a multiplier 4 to load from SD ckpt"
+ output_ch = self.state_dict()["out.2.weight"].shape[0]
+ assert output_ch >= 4 and output_ch // 4 * 4 == output_ch, "Output channels must be at a multiplier 4 to load from SD ckpt"
+ sd = th.load(ckpt_path)
+ sd_ = {}
+ for k,v in sd["state_dict"].items():
+ if k.startswith("model.diffusion_model"):
+ sd_[k.replace("model.diffusion_model.", "")] = v
+
+ if input_ch > 4:
+ # Scaling for input channels so that the gradients are not too large
+ scale = input_ch // 4
+ sd_["input_blocks.0.0.weight"] = sd_["input_blocks.0.0.weight"] / scale
+ sd_["input_blocks.0.0.weight"] = sd_["input_blocks.0.0.weight"].repeat(1, scale, 1, 1)
+
+ if output_ch > 4:
+ # No scaling for output channels
+ scale = output_ch // 4
+ sd_["out.2.weight"] = sd_["out.2.weight"].repeat(scale, 1, 1, 1)
+ sd_["out.2.bias"] = sd_["out.2.bias"].repeat(scale)
+
+ missing, unexpected = self.load_state_dict(sd_, strict=False)
+
+ if len(missing) > 0:
+ print(f"Load model weights - missing keys: {len(missing)}")
+ print(missing)
+ if len(unexpected) > 0:
+ print(f"Load model weights - unexpected keys: {len(unexpected)}")
+ print(unexpected)
+
+
+ def convert_to_fp16(self):
+ """
+ Convert the torso of the model to float16.
+ """
+ self.input_blocks.apply(convert_module_to_f16)
+ self.middle_block.apply(convert_module_to_f16)
+ self.output_blocks.apply(convert_module_to_f16)
+
+ def convert_to_fp32(self):
+ """
+ Convert the torso of the model to float32.
+ """
+ self.input_blocks.apply(convert_module_to_f32)
+ self.middle_block.apply(convert_module_to_f32)
+ self.output_blocks.apply(convert_module_to_f32)
+
+ def forward(self, x, t=None, context=None, context_ca=None, y=None,**kwargs):
+ """
+ Apply the model to an input batch.
+ :param x: an [N x C x ...] Tensor of inputs.
+ :param t: a 1-D batch of timesteps.
+ :param context: conditioning plugged in via crossattn
+ :param y: an [N] Tensor of labels, if class-conditional.
+ :return: an [N x C x ...] Tensor of outputs.
+ """
+ assert (y is not None) == (
+ self.num_classes is not None
+ ), "must specify y if and only if the model is class-conditional"
+ hs = []
+ t_emb = timestep_embedding(t, self.model_channels, repeat_only=False)
+ emb = self.time_embed(t_emb)
+
+ if self.num_classes is not None:
+ assert y.shape[0] == x.shape[0]
+ emb = emb + self.label_emb(y)
+
+ h = x.type(self.dtype)
+ if context is not None:
+ h = th.cat([h, context], dim=1)
+ for module in self.input_blocks:
+ h = module(h, emb, context_ca)
+ hs.append(h)
+ h = self.middle_block(h, emb, context_ca)
+ for module in self.output_blocks:
+ h = th.cat([h, hs.pop()], dim=1)
+ h = module(h, emb, context_ca)
+ h = h.type(x.dtype)
+ if self.predict_codebook_ids:
+ return self.id_predictor(h)
+ else:
+ return self.out(h)
+
+ def get_midblock_features(self, x, t=None, context=None, context_ca=None, y=None, **kwargs):
+ """
+ Apply the model to an input batch and return the features from the middle block.
+ :param x: an [N x C x ...] Tensor of inputs.
+ :param t: a 1-D batch of timesteps.
+ :param context: conditioning plugged in via crossattn
+ :param y: an [N] Tensor of labels, if class-conditional
+ """
+ assert (y is not None) == (
+ self.num_classes is not None
+ ), "must specify y if and only if the model is class-conditional"
+ hs = []
+ t_emb = timestep_embedding(t, self.model_channels, repeat_only=False)
+ emb = self.time_embed(t_emb)
+
+ if self.num_classes is not None:
+ assert y.shape[0] == x.shape[0]
+ emb = emb + self.label_emb(y)
+
+ h = x.type(self.dtype)
+ if context is not None:
+ h = th.cat([h, context], dim=1)
+ for module in self.input_blocks:
+ h = module(h, emb, context_ca)
+ hs.append(h)
+ h = self.middle_block(h, emb, context_ca)
+ return h
+
+if __name__ == "__main__":
+ unet = UNetModel(
+ image_size=32,
+ in_channels=8,
+ model_channels=320,
+ out_channels=4,
+ num_res_blocks=2,
+ attention_resolutions=(4,2,1),
+ dropout=0.0,
+ channel_mult=(1, 2, 4, 4),
+ num_heads=8,
+ use_spatial_transformer=True,
+ context_dim=768,
+ transformer_depth=1,
+ legacy=False,
+ load_from_ckpt="/export/scratch/ra97ram/checkpoints/sd/v1-5-pruned.ckpt"
+ )
+ print(f"UNetModel has {sum(p.numel() for p in unet.parameters())} parameters")
diff --git a/external_models/depth-fm/depthfm/unet/util.py b/external_models/depth-fm/depthfm/unet/util.py
new file mode 100644
index 0000000000000000000000000000000000000000..56690f756ad58caf6e7fce8d3052c2ea59c1a85c
--- /dev/null
+++ b/external_models/depth-fm/depthfm/unet/util.py
@@ -0,0 +1,175 @@
+# adopted from
+# https://github.com/openai/improved-diffusion/blob/main/improved_diffusion/gaussian_diffusion.py
+# and
+# https://github.com/lucidrains/denoising-diffusion-pytorch/blob/7706bdfc6f527f58d33f84b7b522e61e6e3164b3/denoising_diffusion_pytorch/denoising_diffusion_pytorch.py
+# and
+# https://github.com/openai/guided-diffusion/blob/0ba878e517b276c45d1195eb29f6f5f72659a05b/guided_diffusion/nn.py
+#
+# thanks!
+
+
+import os
+import math
+import torch
+import torch.nn as nn
+import numpy as np
+from einops import repeat
+
+
+def extract_into_tensor(a, t, x_shape):
+ b, *_ = t.shape
+ out = a.gather(-1, t)
+ return out.reshape(b, *((1,) * (len(x_shape) - 1)))
+
+
+def checkpoint(func, inputs, params, flag):
+ """
+ Evaluate a function without caching intermediate activations, allowing for
+ reduced memory at the expense of extra compute in the backward pass.
+ :param func: the function to evaluate.
+ :param inputs: the argument sequence to pass to `func`.
+ :param params: a sequence of parameters `func` depends on but does not
+ explicitly take as arguments.
+ :param flag: if False, disable gradient checkpointing.
+ """
+ if flag:
+ args = tuple(inputs) + tuple(params)
+ return CheckpointFunction.apply(func, len(inputs), *args)
+ else:
+ return func(*inputs)
+
+
+class CheckpointFunction(torch.autograd.Function):
+ @staticmethod
+ def forward(ctx, run_function, length, *args):
+ ctx.run_function = run_function
+ ctx.input_tensors = list(args[:length])
+ ctx.input_params = list(args[length:])
+ ctx.gpu_autocast_kwargs = {"enabled": torch.is_autocast_enabled(),
+ "dtype": torch.get_autocast_gpu_dtype(),
+ "cache_enabled": torch.is_autocast_cache_enabled()}
+ with torch.no_grad():
+ output_tensors = ctx.run_function(*ctx.input_tensors)
+ return output_tensors
+
+ @staticmethod
+ def backward(ctx, *output_grads):
+ ctx.input_tensors = [x.detach().requires_grad_(True) for x in ctx.input_tensors]
+ with torch.enable_grad(), \
+ torch.cuda.amp.autocast(**ctx.gpu_autocast_kwargs):
+ # Fixes a bug where the first op in run_function modifies the
+ # Tensor storage in place, which is not allowed for detach()'d
+ # Tensors.
+ shallow_copies = [x.view_as(x) for x in ctx.input_tensors]
+ output_tensors = ctx.run_function(*shallow_copies)
+ input_grads = torch.autograd.grad(
+ output_tensors,
+ ctx.input_tensors + ctx.input_params,
+ output_grads,
+ allow_unused=True,
+ )
+ del ctx.input_tensors
+ del ctx.input_params
+ del output_tensors
+ return (None, None) + input_grads
+
+
+def timestep_embedding(timesteps, dim, max_period=10000, repeat_only=False):
+ """
+ Create sinusoidal timestep embeddings.
+ :param timesteps: a 1-D Tensor of N indices, one per batch element.
+ These may be fractional.
+ :param dim: the dimension of the output.
+ :param max_period: controls the minimum frequency of the embeddings.
+ :return: an [N x dim] Tensor of positional embeddings.
+ """
+ if not repeat_only:
+ half = dim // 2
+ freqs = torch.exp(
+ -math.log(max_period) * torch.arange(start=0, end=half, dtype=torch.float32) / half
+ ).to(device=timesteps.device)
+ args = timesteps[:, None].float() * freqs[None]
+ embedding = torch.cat([torch.cos(args), torch.sin(args)], dim=-1)
+ if dim % 2:
+ embedding = torch.cat([embedding, torch.zeros_like(embedding[:, :1])], dim=-1)
+ else:
+ embedding = repeat(timesteps, 'b -> b d', d=dim)
+ return embedding
+
+
+def zero_module(module):
+ """
+ Zero out the parameters of a module and return it.
+ """
+ for p in module.parameters():
+ p.detach().zero_()
+ return module
+
+
+def scale_module(module, scale):
+ """
+ Scale the parameters of a module and return it.
+ """
+ for p in module.parameters():
+ p.detach().mul_(scale)
+ return module
+
+
+def mean_flat(tensor):
+ """
+ Take the mean over all non-batch dimensions.
+ """
+ return tensor.mean(dim=list(range(1, len(tensor.shape))))
+
+
+def normalization(channels):
+ """
+ Make a standard normalization layer.
+ :param channels: number of input channels.
+ :return: an nn.Module for normalization.
+ """
+ return GroupNorm32(32, channels)
+
+
+# PyTorch 1.7 has SiLU, but we support PyTorch 1.5.
+class SiLU(nn.Module):
+ def forward(self, x):
+ return x * torch.sigmoid(x)
+
+
+class GroupNorm32(nn.GroupNorm):
+ def forward(self, x):
+ return super().forward(x.float()).type(x.dtype)
+
+
+def conv_nd(dims, *args, **kwargs):
+ """
+ Create a 1D, 2D, or 3D convolution module.
+ """
+ if dims == 1:
+ return nn.Conv1d(*args, **kwargs)
+ elif dims == 2:
+ return nn.Conv2d(*args, **kwargs)
+ elif dims == 3:
+ return nn.Conv3d(*args, **kwargs)
+ raise ValueError(f"unsupported dimensions: {dims}")
+
+
+def linear(*args, **kwargs):
+ """
+ Create a linear module.
+ """
+ return nn.Linear(*args, **kwargs)
+
+
+def avg_pool_nd(dims, *args, **kwargs):
+ """
+ Create a 1D, 2D, or 3D average pooling module.
+ """
+ if dims == 1:
+ return nn.AvgPool1d(*args, **kwargs)
+ elif dims == 2:
+ return nn.AvgPool2d(*args, **kwargs)
+ elif dims == 3:
+ return nn.AvgPool3d(*args, **kwargs)
+ raise ValueError(f"unsupported dimensions: {dims}")
diff --git a/external_models/depth-fm/environment.yml b/external_models/depth-fm/environment.yml
new file mode 100644
index 0000000000000000000000000000000000000000..38491ea5697947dc488f20e3523bdd384107547b
--- /dev/null
+++ b/external_models/depth-fm/environment.yml
@@ -0,0 +1,20 @@
+name: dfm
+channels:
+ - pytorch
+ - nvidia
+ - conda-forge
+ - defaults
+dependencies:
+ - omegaconf>=2.3.0
+ - accelerate>=0.27.2
+ - diffusers=0.24.0
+ - matplotlib>=3.8.1
+ - python=3.11.5
+ - pytorch=2.1.0
+ - pytorch-cuda=12.1
+ - pip=23.3
+ - pip:
+ - transformers==4.35.0
+ - einops==0.7.0
+ - torchdiffeq==0.2.3
+ - xformers==0.0.22.post7
\ No newline at end of file
diff --git a/external_models/depth-fm/inference.ipynb b/external_models/depth-fm/inference.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..b5486fb08fd6dd31aa598e4a31327777d76f4f8e
--- /dev/null
+++ b/external_models/depth-fm/inference.ipynb
@@ -0,0 +1,183 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "\n",
+ "
📻 DepthFM: Fast Monocular Depth Estimation with Flow Matching
\n",
+ " \n",
+ " Ming Gui* · Johannes Schusterbauer* · Ulrich Prestel · Pingchuan Ma\n",
+ "
\n",
+ " Dmytro Kotovenko · Olga Grebenkova · Stefan A. Baumann · Vincent Tao Hu · Björn Ommer\n",
+ "
\n",
+ " \n",
+ " CompVis Group, LMU Munich\n",
+ "
\n",
+ " * equal contribution
\n",
+ "\n",
+ "\n",
+ " "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import torch\n",
+ "import einops\n",
+ "import numpy as np\n",
+ "from PIL import Image\n",
+ "import matplotlib.pyplot as plt"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Load Model"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from depthfm import DepthFM\n",
+ "\n",
+ "model = DepthFM('checkpoints/depthfm-v1.ckpt')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Load Image"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Shape : torch.Size([1, 3, 512, 512])\n",
+ "dtype : torch.float32\n"
+ ]
+ },
+ {
+ "data": {
+ "image/jpeg": "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",
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAIAAADTED8xAAAKMWlDQ1BJQ0MgUHJvZmlsZQAAeJydlndUU9kWh8+9N71QkhCKlNBraFICSA29SJEuKjEJEErAkAAiNkRUcERRkaYIMijggKNDkbEiioUBUbHrBBlE1HFwFBuWSWStGd+8ee/Nm98f935rn73P3Wfvfda6AJD8gwXCTFgJgAyhWBTh58WIjYtnYAcBDPAAA2wA4HCzs0IW+EYCmQJ82IxsmRP4F726DiD5+yrTP4zBAP+flLlZIjEAUJiM5/L42VwZF8k4PVecJbdPyZi2NE3OMErOIlmCMlaTc/IsW3z2mWUPOfMyhDwZy3PO4mXw5Nwn4405Er6MkWAZF+cI+LkyviZjg3RJhkDGb+SxGXxONgAoktwu5nNTZGwtY5IoMoIt43kA4EjJX/DSL1jMzxPLD8XOzFouEiSniBkmXFOGjZMTi+HPz03ni8XMMA43jSPiMdiZGVkc4XIAZs/8WRR5bRmyIjvYODk4MG0tbb4o1H9d/JuS93aWXoR/7hlEH/jD9ld+mQ0AsKZltdn6h21pFQBd6wFQu/2HzWAvAIqyvnUOfXEeunxeUsTiLGcrq9zcXEsBn2spL+jv+p8Of0NffM9Svt3v5WF485M4knQxQ143bmZ6pkTEyM7icPkM5p+H+B8H/nUeFhH8JL6IL5RFRMumTCBMlrVbyBOIBZlChkD4n5r4D8P+pNm5lona+BHQllgCpSEaQH4eACgqESAJe2Qr0O99C8ZHA/nNi9GZmJ37z4L+fVe4TP7IFiR/jmNHRDK4ElHO7Jr8WgI0IABFQAPqQBvoAxPABLbAEbgAD+ADAkEoiARxYDHgghSQAUQgFxSAtaAYlIKtYCeoBnWgETSDNnAYdIFj4DQ4By6By2AE3AFSMA6egCnwCsxAEISFyBAVUod0IEPIHLKFWJAb5AMFQxFQHJQIJUNCSAIVQOugUqgcqobqoWboW+godBq6AA1Dt6BRaBL6FXoHIzAJpsFasBFsBbNgTzgIjoQXwcnwMjgfLoK3wJVwA3wQ7oRPw5fgEVgKP4GnEYAQETqiizARFsJGQpF4JAkRIauQEqQCaUDakB6kH7mKSJGnyFsUBkVFMVBMlAvKHxWF4qKWoVahNqOqUQdQnag+1FXUKGoK9RFNRmuizdHO6AB0LDoZnYsuRlegm9Ad6LPoEfQ4+hUGg6FjjDGOGH9MHCYVswKzGbMb0445hRnGjGGmsVisOtYc64oNxXKwYmwxtgp7EHsSewU7jn2DI+J0cLY4X1w8TogrxFXgWnAncFdwE7gZvBLeEO+MD8Xz8MvxZfhGfA9+CD+OnyEoE4wJroRIQiphLaGS0EY4S7hLeEEkEvWITsRwooC4hlhJPEQ8TxwlviVRSGYkNimBJCFtIe0nnSLdIr0gk8lGZA9yPFlM3kJuJp8h3ye/UaAqWCoEKPAUVivUKHQqXFF4pohXNFT0VFysmK9YoXhEcUjxqRJeyUiJrcRRWqVUo3RU6YbStDJV2UY5VDlDebNyi/IF5UcULMWI4kPhUYoo+yhnKGNUhKpPZVO51HXURupZ6jgNQzOmBdBSaaW0b2iDtCkVioqdSrRKnkqNynEVKR2hG9ED6On0Mvph+nX6O1UtVU9Vvuom1TbVK6qv1eaoeajx1UrU2tVG1N6pM9R91NPUt6l3qd/TQGmYaYRr5Grs0Tir8XQObY7LHO6ckjmH59zWhDXNNCM0V2ju0xzQnNbS1vLTytKq0jqj9VSbru2hnaq9Q/uE9qQOVcdNR6CzQ+ekzmOGCsOTkc6oZPQxpnQ1df11Jbr1uoO6M3rGelF6hXrtevf0Cfos/ST9Hfq9+lMGOgYhBgUGrQa3DfGGLMMUw12G/YavjYyNYow2GHUZPTJWMw4wzjduNb5rQjZxN1lm0mByzRRjyjJNM91tetkMNrM3SzGrMRsyh80dzAXmu82HLdAWThZCiwaLG0wS05OZw2xljlrSLYMtCy27LJ9ZGVjFW22z6rf6aG1vnW7daH3HhmITaFNo02Pzq62ZLde2xvbaXPJc37mr53bPfW5nbse322N3055qH2K/wb7X/oODo4PIoc1h0tHAMdGx1vEGi8YKY21mnXdCO3k5rXY65vTW2cFZ7HzY+RcXpkuaS4vLo3nG8/jzGueNueq5clzrXaVuDLdEt71uUnddd457g/sDD30PnkeTx4SnqWeq50HPZ17WXiKvDq/XbGf2SvYpb8Tbz7vEe9CH4hPlU+1z31fPN9m31XfKz95vhd8pf7R/kP82/xsBWgHcgOaAqUDHwJWBfUGkoAVB1UEPgs2CRcE9IXBIYMj2kLvzDecL53eFgtCA0O2h98KMw5aFfR+OCQ8Lrwl/GGETURDRv4C6YMmClgWvIr0iyyLvRJlESaJ6oxWjE6Kbo1/HeMeUx0hjrWJXxl6K04gTxHXHY+Oj45vipxf6LNy5cDzBPqE44foi40V5iy4s1licvvj4EsUlnCVHEtGJMYktie85oZwGzvTSgKW1S6e4bO4u7hOeB28Hb5Lvyi/nTyS5JpUnPUp2Td6ePJninlKR8lTAFlQLnqf6p9alvk4LTduf9ik9Jr09A5eRmHFUSBGmCfsytTPzMoezzLOKs6TLnJftXDYlChI1ZUPZi7K7xTTZz9SAxESyXjKa45ZTk/MmNzr3SJ5ynjBvYLnZ8k3LJ/J9879egVrBXdFboFuwtmB0pefK+lXQqqWrelfrry5aPb7Gb82BtYS1aWt/KLQuLC98uS5mXU+RVtGaorH1futbixWKRcU3NrhsqNuI2ijYOLhp7qaqTR9LeCUXS61LK0rfb+ZuvviVzVeVX33akrRlsMyhbM9WzFbh1uvb3LcdKFcuzy8f2x6yvXMHY0fJjpc7l+y8UGFXUbeLsEuyS1oZXNldZVC1tep9dUr1SI1XTXutZu2m2te7ebuv7PHY01anVVda926vYO/Ner/6zgajhop9mH05+x42Rjf2f836urlJo6m06cN+4X7pgYgDfc2Ozc0tmi1lrXCrpHXyYMLBy994f9Pdxmyrb6e3lx4ChySHHn+b+O31w0GHe4+wjrR9Z/hdbQe1o6QT6lzeOdWV0iXtjusePhp4tLfHpafje8vv9x/TPVZzXOV42QnCiaITn07mn5w+lXXq6enk02O9S3rvnIk9c60vvG/wbNDZ8+d8z53p9+w/ed71/LELzheOXmRd7LrkcKlzwH6g4wf7HzoGHQY7hxyHui87Xe4Znjd84or7ldNXva+euxZw7dLI/JHh61HXb95IuCG9ybv56Fb6ree3c27P3FlzF3235J7SvYr7mvcbfjT9sV3qID0+6j068GDBgztj3LEnP2X/9H686CH5YcWEzkTzI9tHxyZ9Jy8/Xvh4/EnWk5mnxT8r/1z7zOTZd794/DIwFTs1/lz0/NOvm1+ov9j/0u5l73TY9P1XGa9mXpe8UX9z4C3rbf+7mHcTM7nvse8rP5h+6PkY9PHup4xPn34D94Tz+6TMXDkAAQAASURBVHgBdP1Hsy3NkqaH7aXF1vuoT966t251F7u60SgABgwIckAazWCcUM74K/jDOKFxTk5oIAQNtGaD3Wh1q6/45BFbLq34PK9nrr3PVxd59skVGeHh7uHh4eEhMrLzf/o//xeHzsnJCf8P3Y6h3eHgc6fT5deI5iKGWBM7J11u3a6JZA4McaQEFbH7E0G4e5HRq8K5dzu9JAAEzWOikAUfqIB0ionKfYxuAgXPwzFwhDBbQ9O4FvEv8QDj1fJt8HBI1GeQxBwxJ8AzBTw58C90zIUgzH0EJEs9cBesfWwDZKZ0h8ivxV/5f0HuBVLyVnbue+KPSTsQVcW1bIFc4UGVeP46DTfkPF7H7AGzJIeTvcUoiJfJiflFxC8e5exwoNY7ssaP/ynZS7CX4b9PhNS6ihp3HtsiE1ajiIl8Sg5EqJNGkpRsgWmAjaDoIknOwqbUm6tfHLaP/lYzUExUcQG21UNkx0gZEIArP0mHA5oEaRBREVKzgqrjBVw5wRsJg0M8uSr9GRDEx0xN3uan4ksKfx/mZcwxfAwcMbUxrRyaGpfNJLX8JkMBE0WJDJOpeFNT2pI08jhQWkoZGZm59KERUyoCu9FUh5kLAXdFIva0wJB9vhEZVMVtmZWGw0SZu6krqpaUJBIZFIKg1PwA02QzgYjPyQFjrb0AsrBGcFmdYGzZK8BKer5T2+a25pM1wmkxPIN9HjribKItbTRaDsHTMKViKqQon6DFmAU6HHYASjXxIdiwbUSnscQiPgqP9Pz1Q62yNsJJGaRcmZVjMhZKlNd/lfoSqlFrocRANTdSa9CKLSmVNa0CNM2V1Ge0z49Wbj1xD/KXjFUhTBePd6EBe5mryS5A6q1IJrZwJb845Dh4KtxkbH5oyfRyMUJJ1lKaJQ8VSJDIxCalY18XvLGM1Ws26doh1cpScT3nqudf3hvGAtrYQkvaMCErhbDN1yqzikGtYZT3aE+IlVBpD2Y/0lXHjShdT6Kpra6rewEgRUjiLeozQLICFRJgAjFBH/0vmaQU4l/eTaqcpoA4hXmOIS/K+xkCIcwEEAGYoXaISC9RSCrZsBcJMU+GFcSeLCd9Ho7FMqWuVFRYEPHxSqmen1vZKYcw98xfPVdncswuKf+ay77hxWOQmFRoG+QNuMwnW/PMT6M5cAl1nlXdpFrJFdUQsqIqfwBauBfirIzSLrhgSOSxSAnsWy7Q/uYioCoIXFa4QZ/KKKjCUdwd8SV7lZ+4zwQhY0f8Mi+uUldrO6JumY+SlYYBGRUWQnEQ6/885Ndg8DYQL6gkWLDmVYkko5404sxjosDVkjKyuQhEEY+PRCiUxNfvMYkAGFIs41oc1CjNtFF0ApKRWDFnHilEVEc5mBow8ciwCEEt14UqgSRaeNOtCZGKjgYgkaqc5K30YAmtJn/g8PsbmBdQRVJyL67IKJRky6JAVHkcoStAtLFeLyJaIKOVgs8Wzd/ofYWlSCNKemEXLmAiLYYakZA3hETRJlUgAAd7tuaSYGrd52Os5SgMubfAgTlS0xOpi2KVI1yPlF+qVYEtCL/R0NCw8io5j/GOAi80ukLbE4XMpWDC1lXKIKbneiStVWIzYfstSUGay8LoN/gXeg02uSBC88gtNFppqF7WZUM/WMzlX+IFDy49OinE5pqDFHW7aFlK/hgyVuaWvPDJDzYv85GTUrcPcEJBKI3oKzl34oNKAuRLqiT4I3ekajiw3C2eKKN7/SqwsjVjc5H8C700d4BakPo9Rgnf8tki4RcyXA1iagT9aKlQVt2g5rEJ+dMCPHMsCuNl2iJ5hRily4N36szC+o9Y1KXRRWXTwgU4ma3gijad3Lu4mMkTbNyCrM0itAIltgr1nGAoXB27KWP+LBhITDE9QdluwgQMg74KIaQwRJYqHzOa4NXgibZViy3sommwGQAsT0bXRQhI2mfgI6jSquQrmkCgolJWoGLIfyshkTwe5UCgoQxogAOe9CZvSVn1PRY8sFWrxO2pPjEXoHwWZMONEVLkESDhWjxJyYNEyRWy4ac4BrMZo58BCecIWehOP7rRNM/gIj4o8pCSVa48NxDRE9JgPwwHe/iLIvgYdiHbqDz0gG1TQUOw6iYMEEEOWWovEVTYrNp5W38xl7yFrEAaSJipIUpVyDMq8llzxbGSPgIQLc6GElBWC6k4Z1ypzkC0YfAXw5XrGG6Yj+QKW+V9RgwGi8wF+UaHQsL4qvawKYQ8NFHRwYiggJtUMkUcQoK0U0YRAs+YRZQLSQWIh5QqXBSn8uMI0qvYjpHl6VkqlRTIBuQI3KI1Iq2lG9afwUjwij5ErlZDxflTQpY7YomoMqsnpKQkQjdMJ2MlBT4Z2tTP2C0CKULVQvA1sRa06Cq6Qx8IG0N0KgxIl6th8/ibyGIliWgIykDOlgXwyHbIhrfi1XwF9tL8N/j1+I5OzNHNfSYussJaAhK6oaKsVJyGzxSq1FGYlETSllYhUKoqYg1LxBsywRLAlDSFJw49spGXajYkAiWeQPJk+dtwi+JZh8ViLG1SVDF+LbeJr9Ri3FqIavioUIO4yngEJq2ZxogCqFWFsITDY9gXkbi5pUIaINF4ibkx7UL4r+AjkoKRibDXPCbfc4Tkf5Euz/xljNQA8lhXGyhu5OAXFwDFVRMPcqKQmuVpKaWwWEHjCuMRD4EKG281NylN/FGD0g6fVYbkTgbBoFYKZDQmPWLCDTuhR5LVWJd0wgeVmwuBQiWFIK2hHy4Npx55qt9Ek8tfs6ROwdMtm2eNJI1kEXoli0Jp8yY25Chv6AL0LF9DykGwMEOhm6w8tqEgESKB5A63xqgTjbhatAUO0ZYrwcKSjDUqJa8NZRAAKXviokWFk8IiC8YLDpqkFBthpwkGoG6Cgy7wgFsaECZjgOWpcjV5wSPqSqy6bWCIk+FiJDiCzmerkxzmBKAyBy5MJFSPx6A8yU5KjcuUzsRIiQfJETSQPDWU4eKFQkVKR1AChdeYNsmYQLSoxdQgS6BJbQVh1sBXfIJgsFQWM7HMnzoLxKU0vSxME24JU4Wp56psqafEqXTDkDmS0Ca2EyVBB/ty4XgQuMoczx/sakpd5N/pwXthegMIVoZEVlV4TfYk2EcTCWgTZ4mLTBCYkCokuqBMbYQduCO7omiklJJU/vAR3kgtgJbRhkCDINIkKY8VpyyO8mjAdmE2WQkVa0dyldcCNLjzo1iPIJVQ9UJ0AMmAH1UFDPo2u5HAwINqGdEprYhfROJ9wbKAwpuUq+WjcraQiA0UEj8WLqEwYFVWhRSloAi0wmhRF4zEjhf12FQL0OJrU/lN/uRJrI+mmhIu+CFK7PWnPU6uIDoWiHrXw2uyRS8Mi8MfbiyEJaO8G0IDxRUois1FCjrtym2VVAUFtNSU/gJQcCgEQy+u9tFqCPvmErYBLHJ2JXCZ7CUtIkwqdsgIOUk2mCFkapM5sm8fjAWMBkzqEd58ypfGaWQbX+gaNM1D0U8GcAkpX/6Q0gRJiDYQG+F8ji95KkvuIDEq/HCLbXmJS34skAAQCDuRxedoTWxj6peZEPGEiDdD9dSSIU9Tk88JckK2liuClsL8dYGBp0ou+aeMMmepFYJEpGyowMuilvNTRWjrU6TFlJmSz2frFFwKGIG4tBKsyUvKsaSSbZQhiMAgDNmLNr8RgrKoXNLIMKg4C8MywUVS/kMAbSqKxHQZAyimVEGMh8C/uBRZq5UpfDN/X9EWiSvcVfCz7OHsgI7LAbnTikJQAficmBQlEBEOIXFZOY2oeGSPBsAKIVlDUmlVHI+EzdXeGwRGODqO2ERQl3QLIfCfCT0ElHGLLGUDSWWsWMJNIGANo6lVUiLPgAcoIv48X5CZtRhCQpk3LRrOZxUxeTMUjMY1rFYy96gh8Q18CmWGYBB9MCiiI31pNuBCtldhForEX2QIDNbLgjRXofOuVvKPykmS4iuqLai/Vpk/AXfFKsD8NA5HUnm0PMGg3UyuKiEPRQtAEyRomaSKOhdpoq3TBpc/5qnLpIT0uUsc8tQ3czPn0YIWGKVoSsSzWaMtTTGZSMlSr8Xgz2SAQ6FyPatUagIgzX/LZyg838heTJOXcCPJpiTG5LLARSLa0GQplNUfNZy0iLsvVi3C45FOi/G5gMY885z0piAQLbDwUaQFTo4ApuiynahGzC17EYzwER+xZqzSvgwUQlOVVIpqHjEK/DmjbWxQNVSN4zn4W86NI8YZd0GDLXF/9qbwG4qSaxgG9GgYLUKhAhaQIkajADaewBFvCsETJMVT8f6CgbhMH7TA0n3Jm8WHj5KE2SMBMZlDJLREIjPxBVxYaSiEQBXDjC8uHhMjZ14pCDFshSjhNuAlfCMrGkh13kxtf0Q4j7mTwj+eLRa/RSTZEzR7AZrLfMR4GUq4AnUHycu0ihQULBbHfPyvkjXASa6YBunfQyse665hwMcwDDxMJr6pWOLFL53manJVNRhNjspSNMlgHpmpHIL40CTbwybYJEdCUBCRFVmkhBDAuoOiPOzp2IFp2ACy9DCzWBV7pEG+VHpIHHk3ILNhR5SlGCI01kxmkGICJry4Yj4C2KQ2rDR4lGe0Ub5tA6CrMsQHEftL7lvE6UXim5kl2cKIQgKFroIGmqTw3WY7sio1mfenJdcAVaGCsylgm/vlb4NW2iHMOgBXU6UGE3vUlJeiIVyPFK0C/BS+Un0zvzC6qSrQKWEYDqEGf0OlRXhk3eQXV0OlqS0Tiu9j4X3MfyD5rdSWtyQkqokBWCh/AhvGDCF9Y6zBXAm0D01cmwQKkHxWIJOO0EGEQlCR8gPpkhHSUFaxBq1AdChERx7zJ2toU+KWZ3D4ZGrD/DM1cUc4RBn7+WMTozJYalE0JUxEQ6gEIhCpLwBkiCvuvGnkfkFKXAUQ9ITlM39Ei6x+CoYEW0ObIw8NXYGVflu64kNi9ltkD+oEWooRY2UqrAqWS/k0OYqsWAwhYZNL9E1SVSDRzgIFKhwXeCqvAfTHpFY0FU1MIk1ITZdsrONfXpJtYCPgFIPbS4Qvw+SHrQIVV0CV0p+7QNwWqwCk1Fzq6UsqgW3T+M1zcpU9TtZgg6RZW0T5DdsVU+VpxPYLMCHkOJCZ5ysKPEcBaGwWzRoTTioBaLE1D8lunjYewGhEwxT5GxKt6IAMupZPa7yu/NbjMdEZgWhLm11BVbjN2GYvJluG6hc8DUL5A7IFfhmQATI3SREsuYmFdXSmELQZ1SKQ7rvOwRCQgLSCvYh+dpcrAVIiZdEUtyVIWdqCAASW53RBW0GAoU/uVFiyildwYio6QinJkOSlXQesbkGUTOT67JKDhv2iRoRX8ia/3Jv1F1fLahudxtAUtClFJYWgKAzUUF6U8F+sEBJ/Sb7KlIwRl2zQWwkLAYwNz7oYha+scjHXIisqFVcMECbSTM2zQQttzFEex0CAUn4BuAQ0+NlPPbeJz2nibcpjnpQMBJJs8iuJpmojQ+JDof0lreJlTuLmFY9XJZHu80sxm0sMopJnA9zsu5qr4UUAgBOZcDBVRskFc2D0l9KlKPvi/4gwz8RKNsAGj9eR3SSFq4YgKYSox7hPLa2GKJPwQSEXlqLBl7LQAIqFgrDmgpdHgrkacH+kEkrtQ9KeOTymJR5gyiG5xFci9xS+KAXuxe0lAmvjyGyDsOVdnJKlROEYQJPCSUpfzUL6gvK/4BOQdMOzhqeoNLIJywUFiGapwdQgN4kLjLJnCLAjSJJsVMerSNdjRScmjBXnpKUmRBnEBVxgIW80KtRazaSk4CH0ArDlUWEA0LBh9gao4cOyqgyBOiaWPpiJ/PVQATO3McGgxBpoiIDFIjdeXztiBg+RJf9kkmBxZBIYiW23c3zGnvEC5D8/Id/m5ZmIYv7IFRByJD4nlZig8SE58+uLXvLIQ5A19UUegPrNgPxFpgILBiG4CpRA3Prj03N8oD67BUhWrWbmaNqr1M3Iz6/PZ2xMKzLPxIxLxRloria1kWyLNLjDagv34reUARArJ5BZipbeL6GUs3ElzUq2SEahj5TNcapjPsXf4CJRmFbYPj1fFVuwz/FtGRutCnhoGkog6I41GAAJv0Se4sAOUE6PqKKQ5n/iDGpQxGacN4JSTCEbK2Bcoz0h8uIGHmG9aIqZgSEoIiJ1WxodlURJQ0SVIbnkyRyJEvz4r5Qk9kQsxzxlYoS0FEbLLFl5YtjGg0Yhf8SZnBWj1Ez0TmpBZ007pjB7sB5p1PsAYSzstdgg0TxHWsrOiBbtywxN6Y4/gRTAiSr1hDCE5TpheGlRpyjHjBUQzEueK1Q/MP+cAEYF0qQffwuDZW6xVEzdE4fIiipFiRQiUA3H8bKsNp9GSMVG85AfyEq5EUZGQWRvmAmbR9ZF+oIXpA9c4QLG6jmmvggTbOgluxXHfzLIrNlDK4KKGI7QkbM0ubQ6PsuA2lLgst1wamTKEEAjw1rDeyK16wXd4JGPMJGlUYUmVylU0Qp4sDaqbgR5gGyucEPYgouKJJJBERhuFVdiy0JYOPBmSlqCowi1S+mR1+yiSrp39C5zb6bzWAwQ4p8RxU5x5TRow1n7I0y9uQSmpJq1AsRUq3uRq5Lk70WkyDIIlB2dbHkRRv5+eTVJBVBt/RcgUatkpuSkKadnRMlvedq4igiOIxSBphhVfmCUfVS/gMwFo2gOuFLNuaVqrGUaXWrJMkQKqbJw0whFrj7j4ZfFqMTDSa/kkMeG1jEjeXiPzOKEDx7two2tem6pGsH/NkWhKALikgI0CqqO/r0rUMnX9s2VK2IVmso6ZhIuzBhj8WCmZv1bRqjfwLwkRxqAoGedSWwlc1FwHZ8VaAogZa9W6slACmyQElFIpc1oTQNqVn8qr9n9xyPZYel5Hjk8Ex0JyZhZwlin2Qt0RFGBZ5TSrwwUI26Fmvwi3QxyH51Ibsk8AxTw38siUnO1qF4+ktCUrlBFgtWcUtjPhBmSRbcl2uBsH49UoKGGPNcugEqx8FUu743yF+ZipgikWLVKS7T4TYVZa4Kr7sbnshBtML/UizDQaFpSWFGgLxREEHPKCX/V11fpiX5JCJxYlqIRQpYuEJao/hr6Jd6UNBicjcLIxUzKEOoihqKsanyWz4fnOOERWtwe6TWwOLERHIDHVteiSWleYiDCx0gbDOFejhN4FpsRJQcxNelV1IAid2VPTASrOA2QCdiyvzJo5vBsQY5XGM406DFKKNGTvak8A6DQlbGVNxwmlKdinVzMFxMBbX/CHEmNWI1pOEhqwkWpkrjDZcU0GM1gdHtPqX1QuPVAQOmkeA1eo0xtAFIYYkxVqoXNUhopswZ8KvnxEFh/czV4jo/Pz0EpRq4iHixiq8c2TyW3nBxjFWuu1nAcUZdNfIGkkRT8vohsEJW0eDCpBbSwuY44BVBORDQ4JK9IGpCKBUE9fyZVIXPtXZtr7EVTdPCRJVVQDChVYYoBf56ztHkrrYnnpzCUnlaa2eBUzPbHLZfHxDYgu6ERcSqgiigm2iQLb46GYpu5YvI+wJFAyho0hOoqGyN3jXDaBH4FkvsSyrEyiH3JM8QbbBb1z13H+Eb7w26biThycSO2EloUQfaMvSVzpFFJdSe/CEyTYfE1+m9c0w+3iNvfth4FIV/pUISevWjg+x+/zFIqq4ofxYclKfoyE7RiaW1gg46ERs/CcdEBuGwQj8cYAuGnyVjxqRODx1YG547UI4EQFW8D3GT1sUkiJrDEBAtPgU1HUeBgtlBckE9ii8bfwvNZdCCf8Re0VFrdNFuToxG70ADQ6LSuRB4RHklWTBtPBoP1owYqRilx+4x05qwUMmnq9/ES/DPgeo5a6ivz14KT94icSFIpC4hKhSUYPoCpP5OkQ5nbCZ8GFY9EmsbVoq8nsUVjkvAyDfRmKfE1QmxzA1d/DdLwBaI8on/pl1D5PFchWnItinpW6i1RZR6ZJiIMk7+hEHAT6lknK7onhEUIt4bIZ5zCkY1cREQABdREClTInqGSK1C2hJSiJRjKoSbDxXM4iCRSioYw6GSiRX4sX8hYv8diCJNMJrW8JO5IWh/f6pVyEAT5L1oz1MPAszDzGJVuMjVklQoxxfgRH3H2+lUoCMBhGzZfE+bHkPnbqi6WKrZRQlAXRKpTvtgLlEzm85JA/heoY6Sm+Tca06AIuYa4yEQSL0j8zRVaFZYL/rd+lLRaqOcyVAz0C13VRcoYkag/gPDfQDB6e3kpjheofWwyAA9ivFTVsK7CIV9trgY6yWUk2sJH7MkIdH4bNhtk9dOyI0wKSAS/EipigiW7qdRqZau7VGGYhyOH5k5iI+hovbjIq+0kh8khK2TKgXYmIY8i/fw6ioeA5IpA4RSSvPwhJx/kXTySMiK6AUDypsIbfosN6QqVvEeeWxLiKQDulqIyBSloYuWl7QyrOFpgJ1DMR4biJLmJpKSNgF5gljXiIyXoqTkRqm0o8bk3uIGyAXgducyTz8BUUhsTKOJM4H+Dg1inY1ObljzkA/p5bovxWUyDluzP1dA8NEl/DlUKpizEFSOawjeS+WUpgiiMAo/tN9i0APIjn2cxpzxhMKoPoNKpvGQyo9iSJbXuAwgVuClNLRkqybT6XYnEcwVFbmYufoAmpiXaQBUKo4/4Eq4KNTJt4AW+wlrI2vDzGNeYF1oiFa4oR9tW1aciSwFa5gsqTcW0bNj21/iGs7SGhFW1NinJAXq+vawdtFy1iW6K6eSwVVdF7cDcZRYF1NAoeiqKoMqdgC5J01wMHC+Bq16DUdBc1pa5TfSneWpmgXiyfZH4ks3QAtJa9iYKeTC/If+FT3mX6JFaAH5xSwF/EVePFu05NbykGMGawiQU4i8ZFCiRyKJ48vlY0w1OMAd5+DfUgFb7sbLl3cL70xRERET7xyV/eU5WxUBcoWqQGZPsxPrPq5KCWNQVaWyDwBVK4ajySguHqdfwcyyIQOZrePQhYm9IkFlSRxmKTNCWDo9cIrd4XlXWxOWxoFWBJBdIG2wiHbxwwTtoWl3k2bhgauCUS9graZpHzl6gLizRprL39jipQsjbNPwRq9xA70XuZ/5CFTrVGqRSV0MHlhoJRKjNbu02e8OaFNoGQL5GTySborYYiYi4+TFKCVhio/MjLSMSk2VnIhRRVW/QROItws+TKrbgj1mCmfwtI0WpQcAP4NwFDxEm4Hwb9Uj0iEcgQQqPd/O9uEgDlX9WEWLPrMML+CYHiVVjDQJRlh63KEOCm2IgKRzLkrhCtHgupo1odSKuYyvSKk8YbATYZgseH4pQyl1VkNqpLJZDgDDXCB06oU9sk8n9Cj0McCbpXwijAHQtajmZn7KbL4a/woiOFO4RX4M9uYuUIA7eXAQAqpwVkZHgnbK3G3RI5pIzUgrT80MiWEWycloqADdgyZjsz2l5LHmHT1KaLlqW2qooqID1y/xEarBgiYQENoVIbYKjyJCeWontDDLZSqIlK9aOsJYjV35LL1rKkUXz8Pd+yGhOucmdnyhJAb4URWLCUo3mTeOxBN5Qb7lXwEeWwJu8wOCcaX1SagrP7zFjwdS9spPJLA0nLaCcvggX0830Au4hdMFfscWxFR5BI2zmZ2BaFA2hsNaGi5LpzzGG+A9GkJDXR59TY4k31mijkhQ0xATMVl4Vy29hFkswlYhI1j8JLmKKXuELJjF3eWPBB1GlmTTYKQYlEk9zkxEi8hOWuIW/iExxF68WqIWSc7Mloypa0qqo5h5+K9ygMKYytqWGaFs3Df3PUPDQbIduy1juX0O7Ybt9UijsVuWtdeuWWOsVgvWQGEstyV9c1n6L5RdJf+6xhGWmKnmL8YhCiq2ACJb8TMVJDTsy5h9x8Nzke5GlqLbxebIi/CvmvcPGL7JU9TxTbjIWtspIuEGLUpQ4KuJYtQlYRPC0a0YSr3/SjWUupHkCtkGenwof8clzUKk9UDyWocVw/CWlSYyFAJ5Hs0TgLVp/k4Bgj3X7QixBkoy8+7FvNlrCoVG5SlfbojUU28QWJnzyUKrbqHf4T8GKGdIJHBnSsMVaWAwvi33krGWhqTVZ56r0l+FE1000NoCgsw4oheVI3qr+5FRApecFaf+Y3Q1hQVyKklw0ESvBGK7CEPRNVARtUgndWOvLixiuCn92h4NEe6tyNJ5zA5VcIq5AM9SVEXMGZRI/Q8pDS0uwhueIUKtmXHNZiAaN5MnF33NyoHxsxH5E+zmQOVOAWiQlDC05jNrkByotyWf8dk1HYsfAEe5I/QVXwfKcSRDIIQjpPaOoUN1JIr3KxV0MGMJm3V9OK/KYpdob9Y3cuJnZ0DP2I6pnksEfftubZF5c5pEHR+7BJDnZatqnfCgOS1OcmNmHQPNbS9PyL2fGFn8W7yjcFxQr6CxQkgteNpJggIxVQUFpmNhcJLkoSBg6za+Ldk6ftTAFCftEfR5XKeQtFO3j8RcOyIBQuVL+FzhlRaoN0gZzw4so0xCDQRDwEEYc/yOkGjIhJTkNmiUSnKLxh9BlxmgMEPiNlrEmOkGzIuf8JELZpeZERDbS8lt6ImCAm7tPgLRMVpI1aiikCwVPVSYDLy/x1xXDK6k/f7VgSU0Zw1qYJC7FDm9NTBEnF4HCWhhyl2UEIpetWJ5hgvzPM1GxSlM0AkZA1l0V+RfZ4Kp6mlaIVqdO8bHUhKVMTGkNwZYT4iQUu5YyhOvwHzL9JodslIonp2Vqhv0iiDiiWymwKiLPibYM0lYI0QrDirJJbYFCrrklb1AEsmIrkruPz/l5CHLAC47jgooOZKuYFe+D2ap/J64tgJpbOBrAwIWQmOU+AhIogrN/M8rUiuIe6g1zpPSKtlx9VsLaj94StEwFmJwKW+NKCYLuF2w1lQXVsFLcSjoJVdbaj9Am+RtpmJhcxjS4DdZV5tkaSamJLGTqTeT60n5XpO05ZAEO12KqXNXUm8jQskaCmafiwmjpcSWupWsEF49WVaUHAEbK9j9HFmjuiWyYFHVMVVFq5JuBTYZb4dIMDU1Cri1YrPASXKSVvvtCDJdp/E/QJAMqRRNXOUtLWsYb9knS9jdsWK8EQyPYECPU1cDmKoi01BqCVbxEk6vNa2nbmCNvDT+2VBchKY//2iwGvIKxytUgP1ZGw8QxmoBVR5ZSsrCvmJrZOIPVCTTSt6QRjyYl8iGQBpasssQfEIUyrhioKbWYirXcmxaQMqSklFewwAnLRUywNT9GJHiEJ1k6Ustv4Tfr51cjSSk0HkUDmZxgNaElJz5KBC/ImXCYya14M0g6P/xR+kpiZNh1gNg+AlTcVqGrWpqYz5mrp0J3ZOMXIC3bRlcFwx3/Itbsug9dkiiYolBQaWMCHQXUsHpEznMdjNXgbZWnpFPjyaIXopHWc+ZUA8kFLeGmUgtbRROuotWjHISjJrXN3WAtASVSSB4NNxSMqcputQlM4El8g6D5qWjyiiGZjuk+cymgYyjhWoIJOgXL1eIGC0/0JzLSMNNQDVwMg8jyZE5CzgL6U5ENVZNSR8Ynh7fnK8w+CyH8h7iieI6X/ZZWEVDydZAETDY4259nEiWQFKxoVlJlSBuwfIks38+W7XOotRQtkeEANnahQVewLWBQmbWKXDAvS0FMHuG++tujyGXjuYzJ2TAmadkqHpofCJSESwUbBiKU8OlMH+iqZFVQHqwDdoOmHGI1EI0RDnIpTXTaBOGaBKm3V5IK2lSu5GzCPiZXEiDCXzBiYWFAJZFKXRa4eTRU1ASH1xZ3A9pkqnGIaUeiEiiixXkgjySEBOLFBXblYn1/nhCq5k508RWhtbx8huTPRMK0lUiKOvDZpexf5CjkdXckJ4s5TLLNhGQUDhetNIVp5lhFAgXhKQjp3v0VWTIUhFnrCpXCFkRN7FEspIuKv8+SBSt8oK48ABHTRDZoUmAiRdKU+gguSKhXxpJOsmt6nqVUMIWwRVJPlcUSyQK5GpmEofDd8FTJjcKbFyZoNDsro+WB5+cvxDSZ1TPJ+8/0tni/qK/CGKoGlcHxgexi+PuXlVTCAnuFBayocCiWiD7ZuTXpVRk+S8u8hP48kbSUI/kGRfHyC1GKKsSLgarywix2SbgPysvqUe3qsYmspPaOsMCTJG5RnYhRTRBDk6nlIY/JQDHayMJlE0s1iU2QNi/JgXx+NiS0OBJbLEo9RZNKZTemUUepEEmWI0KxJFrKPuRRzD6G+chVbw5JfHYV/pfIgf8znq1lEq0lyl9QFyexueiEw9oGv/ylYGZ5wXlow7r88Nfr8I0P0kXWIJT9Kj2xFUhxgq3hQGCCh3rpy3eCTcCkKPzjJsWGYwHFHYpKQ1j+C1oZffbit4IVON7F2yQS12qVOLisreBzd1cCEArCPFSpFGiAW86TSQaLKx7VAkD0OnLx2LAtMkWUmMJXIISJLRZI3GVtg+eG2UIIUPUMDd7K+uKeeIfd1kmDPmwVmqiRRI5ZqnzaF+OKyhG5UfSKJlXxDBXMEUFSICdAgZUUQzCwhc5JqyPi5A5Eqpn4z5MaXJLnf2P+nkGKVoY/6pu1JlyuRlARNxHPmcK5qZF+Ay1EA2KtePmcMlMkXZVEEtti4rclJmmjk5RIy3iMgVQzFVqlbMAqnbxRpJA1k1pFwXglknuINoakQQj+RAsGVVisKypMItgqW5ub5zamAW0eW/ar6MVOFCZgPKfgPETOyXQsJ2WSspENNZqoBe00bxVSkIbTon7kIYGGg+anYQtCkX0QlhiUd2aPjiAhKmqgpdFWsJClD63YKksii8WW0STIhlj8T07tY5CmSEJI2d+CEjsZQg044A2Sq538MXOrxElvaonoZ+qG+F+ECIdKfg1G3wiox80VlQ4jbfOT2SOCwtWgk8W0gYCABiYLkxVjL+GjXDfIK7XKEZQpoTwBLowY49n5rntZYhGYpuj9RxDUmPwEgjrEaAAvKQEJO8KT0dw8koMrPDYY67lYcQzQaFj4jT4FuGW/xgZkTeWFg9KVZ5RkyhVKwV70m+fCFHCJ8lhMFUG1KZIj5ShKcppqY7MeUqCqsJImPPsHiMIEQ0gV8gTJR5x0ipaRxYFRSqcqmFSyIyUxHAF8EquzobR94VPrREJVgbWkrHCtQ3HQ9FXiESVYijuUQjY61KCUZSEqVASbqg5NJCBgmLHyLHyMouY3TAUBSeEgyMK9qQWgsEIW8IZAFS/ZK8kSSwrYXA1Rfho8saUhKauUr3hpECqNzMwnX4YrcpIyNhik3PjoRQIhFTFquRgkvigIIKZQasMyJzlWZlomI1WBQ4g0sitHS0F+sotZPqi3BqotYlGncQKTpAYHx6LIhNnMDM+FrupbYqIOXiEapUsG8zxfBSXqxKIjL/1FQFXyBl2BNI+F5jNkv4h6pvMcKtFBypKGYn5fAIRWKLZMCh2AqnabnUIDQ8Oq9SwugAiYt/AR1+ZVxMFcSHna91S4JjL9Up7NIIKAH1g0sHpErCrlN6miyTM3AzyGLUxhsBZ/svOiRswZPOYWpTfDuQjVY+xKWANCOTXFM9DmkM1KAsYrd1nULFYZEiBZ9wJtDDiADQfgTR5v+Z8ncmaI2aaSJtLIF8nuNRWFSIkDheEHI+d/+wuRQsU9eXx6TipWjeNqf2Q5QqbmUkth1WkS6YQPQPkVk2izkoYLhLyDo2Si9OtC5A0bYVe2pBZolnxtC8FKLC1OYQd/idqCFoDIUgmanFAKD8GVlCqNYHUBHPjjowEl33AvB77PXfnaEySNjXanbMdCVN4wJpamDOJrLqNgNVaGIhSLlYqYwFgC0ZYZG2iL5lN0tF0QM7GS+U1lChIJlebzQG7Cnp6a+FQp+aoskC77EiYFEU9qE3tWX16xoUbqqWnTJUpk3QwEXQW8Aw1ZQKSde8Emk7mINNoSyZ2p0RijYxdMcgrRMhnJZZ6SJdHBHVYbSlUs0mUyZQ0eMYeIGIyOGMHVsBVeygRRk3pE0kxxwpWZm7iAVr4QEWPTYlU8nTCLLaN81iJYindkT4JpsKTYIdHvWUJiAXkhzAgDHMbaA0pBGopB1OQ2ZE5bnooSdMZZsmeqxAOVq2TYPKbcpr0AOAJGbmp0AddduoFGKfWkfTYEr8Llsc3Q/LYIyfasC+bisvIEq6ywoxxeXLaHPAJwdD6LAfPb6iMCGAFHmJUpn47UlVyepOUXszMCEL6KL/7I3/x2OiZl2FM+MTxaX8Ryp6w+FXEiZD0cmkXGEiNKLugm/VmGAT0+PjNJlPAtJhFZxxC0FoyGckiEbx6Iy19bzOIqQCEtgjBNITo7/oOuOAw2Y4smakhA0oHX/TCCC9IEpS8qHlJ+ISMDQRoIYZVRcSmBk55WKz6aGUQSqXm3cTtMaUZiYGcvkMSKI6HNY4nDmGFrLcTkyFAu4PR+gX7WG3kXEbkIJU+gYcBokz+7RPRZBA8Ny+Hh87SQD5YmUyKEKWKF64iSgAUntSUifMEGceIFSOQvGcmzGtDCis3IRHCzF0zWytlQB78sGtf+GkyMsUSS10oobEmxemC1fWFUFOnZkDCDB9OKsOUh3OaVvKjksuVUQoIVRVOLF7Jlo1Z4lo0qRsOjzCAuaXFJ338ih34gVeFmaUJuRapdlwzZhOSPf5KrKiSxTgonK5fYuInNsJecgiH4yUTAKmvwNCCat8rkbyLDUJcSNz6M67X8yY6slbqaScz8514ZK6Qj1yOYBPKwG9RMxaIETCCG/wq1yNoCclnrAQ+A/Uuin3MbEQBvYIikxG7ZKg15QCNC9A6Chj9gxFZ1EbwyRkBYfuGnIBJHsJ4CEDxNnoL2ocFY8Q2WPESbS0CRZ8teQVbOYoZwICpbozhNHQcCIvSr0hdfJBXOjIDBJAnIQ+TFr9Xclrotg+Ct8CSptHSX/CpA1QIOMnXdjAmtpVZuBNSToA2lBg8aQH0VKcmkvKQBWv5bkyRrJRA0trSKFKDgwWwNjg5HN0eZjRYA1KIK2zyLLS2xKXV+CoNQ1rpYMc0JNfDkRpcBq9IHDzfVFFAg5TwZ0waRimwZQVr+BSRKLbe5TO8eemQHOGMRIoxsssmV/2w0WQn2WYMGDMgrQHoZubArg5QhOJRkaPFYrIRbhQhIhh2WNKmJSvJz9iaxYoPxiFkOddOMJdJLTEW4LV6blKwNZJW9AMmgnEpgIkrxLJYPKWKjMT5bA5a7IVJAEoyAw6R1E2mURhSacNNwJ7SojnFpqzxFbGU+SLUkyVxLMCHFDeom+fhMgJxyZVUFxHmk6EjW5ODYP7Om0eVBiyuuJk9DjxZDRrNXncGNMTTGZjM/BGTLfp4kcwUbUAxVrYyGiFKUoeDb0VTCmTomKm9kLiGXwEkAm31IQebOQ/QdNKEafAk3+JVHKSO/+ccjaQokRAg1BU8VmRbGiIcH2QiE7DR1WNHGHzMKSsujjC6EURZgi78QT3KDTuxa33pMaXluWBM1hSRN3vSUOdPvyImppldqEClgBRK2cydVdYlNCloRik68RFMzluP5SrwJuWCdi/SqGOJ072SvcOQOR1yBz90bz/5TxaTHXdAiRELqINVaGBr8lakwBUsw1URpYr2FljjQEZjnN1WXaB4DcQROIEnQVp1IpwDk0EMOS9ZbZKTN3acf4TE6HT8kKKK/5E12SVTB7O5Bgr6SN0VEGAypQaAqpq4gGU59sm6CD/+W5OBR60XS+BmOIJnPxcCaXurMcyOhym4V+D9KUW0ECQMLWhhzUoG/kgiYvYJN8nkGSARKUTpGVzyxPommEPBMoFXtIDIrcS1HyWhEZG+ReAiW/UmPhTCbcqg3mYsDHiojiYA0AOCANYVdESVCHpFvi0Xm5LHFEA2THSKAdxcQgQAUhYIthBCQnzQ4EdVjUAnQkDUHj3DKb+K1YCkx96SZ5F+e2sgULbnkBjkkIXjJFKmRpWoWgGPpQt9SSYKERpT8GOKf9yIWig1Rk57T1CcBg4zfKryPdQndGBrrFiWDFk5PcsGb3EjcfOLlKeyQr8wAgTBXLceyWcAwYZai2BTSwhoUU7CRjG/StL/QL64CQLzwOkEYBSpb+sEUIJshnoMRkRGPNM/Q94cOLw8my1NNDvEgo5bKPxgNN5K2hJWf37YEApB4vCyX2MxbfPMQFGLVVww1y93GK8/KIzFVMOcCpYhiUi2BlsM8Fklgk8sbMnyh/YAGnjz8Ih9+n6PkVHxijBWvAopFKPluwpFCqJbVIT3Kwh1zF0dMAYoowpHTgNi/GqkEEifmoOUecH6rLVGgkIvhFCYtNtByArAH0qaoARSh8f4U6cb+CQs33K1ZMYUgM/0Ba/hJxiAV3KIGMr8Ek0fUhKpSA2WFEFX5BDJW3QvFRKg2BcIjoX3XxbWY90aBIs0ySMf6Dkpu4gND1uOkXozJhHWXCcFSEhmTEX8lmebXGATZSt4wyEMtEAivD0ZEsUyIrJKwQEoRuYX+seQ11jcnTinANK9ILjHcvOQSVGblJ/2eIfF6NfBiF9ibbEd0PKHlyW40gVSYdWtuxMOX4gsNMWorVBRvkv0JYSACVJCiS1LdZKVpFG20k0OVN3d5MCIsytKxQGhQg6mhCYhVZH7LRSbzlvDzaMmSx2gunC7FppSaP2OTWDfSyluVbVAGjLutwrxeoUSQ8is/oYI2icUKUSiBbYbUYlph2bjESVzLnswnnF95y7/WRAltBrOJLaW1VBVlWlV4hRpIoMKZkeWXGwrdHO5gGGxcQdzwZKupyBcESgsyUjNd8YG9VDecK0+UsXnZt7DqdoFbWDWYe1MLKWR2lVUlCCLNVgrg1wUzQxhEDJEeMcRJeE+EueAFzaMZNAIwSxgJRZmsssTNtp7S64iGnE3Z5TH8KXIfWlWTozChQOSEh4bO8Z1gkkpcgshae4Bh5WhqXawFRkgU/lUEFCopvBa3RayBkNn2CpFW+40UvvKAJGqFOgrQUKviq2hECBhsSUy2ylskCEcjgzHQZIreymxzDsxLZuzU6ln5FS9BmIqDGKmhW/ouSjEK0nKQhhrIIh0h8Jx+phiynsVdVZus1qMxjSSSGOTFjiohX1ZXiErgWc6at7Y06lDI1y3h4jA5iZXTyiuqdBmSTb7c2uowGXAejaZfVxNFqGWSkaQbCoGSVyVGLoFsgELTsFCRZiNrUNFvWCsiRep7JzaxMtFmuLNXQdwiA86kKl/jZxCFRQpwkwAkl8giC4VDdyECURAgGNAUo5ABSw/ALdkCSlTJsnKaGSFEYISd4RFMnopuA2b7+vOXAKQl2cw+g6XwJEuFlZ2PPGlN23RzwJAstrFtWjA1mEVLgXU9CVQzCZIA5QYHrcfSqJ3MtMSbCifCVU9iAa+0hnnpNzWSeJ+azOHVDJVFarDlT+KMPYaJoTIjzwYxkmvypn4AJaFwiCeCLmkEmzQj7YZ40bBC4PCYYH5iUndmEClxRHlv0AsUkwwc4aDnJ2EerIY2X3LYDBojl6wUxCuRBaiB4NFYIcClZwUislk6sSaXPhVNwKyWxo3NAIYAKMyc/GQwIVeKYwFNppIsS+UPIW/5I1AIVIVQtSVpaMgb3sxI3fEE7nwnWDhieRKGklfTC2uyRVxQHXkR0nhxhENxJu+RX/OkZgvAglhShRD+khwgkBUFUqK7jTUFTG78L3nwFKrmniQSKVzYEKCfpe8yHYlsORZBsORXzCVB2SIlf5mgaJ5Ea47UmUDNY3IBX+yELeCecTfMkk5pSc6zgYJqZAQ2E3KUV6WFN+P4D00vy6NfnKsymhpWKzJ0QzuhRJKJq0kvPhVvCSnPclZYiS9gVaWYhZ41SUUExrccCQguaTMgAVXYYOa+zEmavgvZ6p/qDh70O9zKjvABJAkU+278ZMuZg2zgEN2o3UNCACwD5kwGYmQiEYYEiVKDuEAtQ3iuTAVaJW3YMEeQiCcFpSHt9/ShxFqmv39FlLDOBRIoWYYwnZ7972eomCppQM0EW0EULJ/lSfmaPABpP5qyScs/IarUTcaK4m7ZfADCjq75ZxxOJ1Wla1FXcCJhI+QmYsq0YMQRuYgw8rP2xZl7cpBFNTR8vHggWhYJcCsZHZON5b+YvQTgHrqGw4EAuVIE0oMuWdI87ODzVFlhsFSxlLXN/Dlb8hK8hZOcijRYwmcTKs5ICkekqLwCM54WRAaRBnaQGIL7/Wa737YkhYw0FXtsZaBJjiRSDkLig0LahUwJFOYoJ0m0Hv/FEUL5SbS5RC9lARi5szAUWO1PbDA0oYouxCUU2kJYUTlLoY+gqSG5cCgXqIYGC2G2aAmZKlmTqz8wSl1q0VhAQQrWO1cBv8iSslda7i9KVc8tuXoKOp08aUoB1TTlJbFAvrxBrgUhEFCetTL+gaRlmScVPzw10UkXWVOSBlYZ2i+S87n4gnGJLRT5DQV/+e/1y+IapzSC3x/DxWGTpR4zmyEwl4IvwipBxfkLTzKUpFA8FqtgzCp2yZBOFgD4yZYvDbUJGtd4htKJTskA82vdwbA3IXnYG6Lkm91uNJicTi5JK5KD3rDfH+53++1msd1vluvlZrtcbeboUMwH1nO73S0AjkjlgZD/U37URtZSNOyRDAoqF7mOIcmFVXoA+RewQE1oRCly2C5QJSKokFGFNAAzIlVnWQBO3mALnDdRmytIHF7BDmeDFk3TZVig/E8EwUAbL34T62pwHR9fJFXcC7QpdSv6FknqxkpqrqKAZbByIMqzLAJuISsQQRe8wXpU2taZPIpUQSWcvCSpQ16iTHI9IqMGrZEiMTVZ+f3zFzAASKbKW/fANqhRNx99am4USPw+FeEYACL8EwzAYgUOU+awZXxUw3yggGb4Nf6zqyGVH4n45+yiOSJDNKLbZ72z1+2Nh2e9Hno/mE4uRv0J935/gB+glLp9DP6wP+rQD5TMpSdn0cvDbrfdbjeb7Wrn0Q97Hteb1ae7P85X98vl0/7AC7clYYST7tdy88eFsmmGbPMpsgU0YGlTdAIItXSSPGFcWdgkqtRx+6VQWTx+olF+84ZDcAYPmC26j6HmMzFtVDl3JrPHFqerpkFJrSoKYGhXceSFtMptLq/URROlwqbezC97BdBoSP0ECZijC6YHrGpVnSKUf+4QQH8AbLMXtoJJEYQ1+zNDiSkUx1xl9SPJYGhuwfD39Uj0CknM/nIVQwR4ikY39VAx0LFIAQp4ZUk+sqrhitwf74IAzMRa81BTP4krhOKzqiFvkL+Kr06J5u1gsmYgwqRggjSXYVSDH2lKPBfPvV5/0B1OhpPzyfX1xZvz0+s+dr03UO/NJHuY+P1+11f1ccPZkW8CKkV8iOSFh66Yhr3+yWiqiOSH4cHu9dUXs8XDw9PHu6efV+v5YvWYloDIGh4ADcLqAsxnDDwWeVuFAgKaRdlnvs1DgYJHBPATsxi8FNWGEETAga3ZXdQgFY3SKMrJQnFascbBjEZTIcRnHk2pVRsNO9z8B5ctq8T4mFtSRR+Iim0KlIK1am4hjle0X6gGhZAEudQvfrwHmmA9Ns/H1MA0IAVKMWWCptXwkrqPwgVdQhFRkETOQdBikRB/SKo1y+arlhrG0LyYhFRTCqQgPV4Ak1pXwyxkisHcFWZdJYSmTFZb2TmqAZ5Ba814+QMTapZS56niC6kRpVXyiovzmXCTRSSWX/YTNAMW/ZvXv72+/PJ0fIqrI7nDDggUPboHiMafWZgDB/Rvyb53BaDQ77f+dk626wXdQrc/0usvBhFKzPyg1788u7k4vX776lfL9eL24f3Huz/Nl5+2dggSS2FgxzYctSUcpHH6A5BKo+Qy1JQ72WCGv8hnH0mloshvI4pYlFVdkat4lZs11GCQAcMhGmyuLdg8Ukg7Ec8FqlxAWQNiAIFQSlRg2Q/RAJqMCNMkK29QKFalwy9gVcqEjGobhaxwCSCSVvvL709Mk72QCNbgMFtdL2JUTjMURe/BLLMYhRS8iTJentqrsjQZjUxG+LE58ahm5FIjnrMlu7IpCQU0cVXAMsPqH1eB2f8D39CmOsGep5J0QZoqWCU1nANpjFfDp7krCAmYiv4cIZKdG6i4U2kX06s3F1+8e/UX0/EpDW6/YyBLLhKjaUqjYY4eABcnqTg36H0z373f7mylNI71vLNddwfj8BT0ko9kUqKRPcvZ2fj8Ynr56eHHjw8/zVdP5IOcBhuenOaHPD2MjR8Oor6mVcmrHAot/6RqkhwTYSDUFJRRiak8dW9FUtDmhc26lIhGMpfkmqSgbtcBSEx0U6eAhxeik1Nk0eMGa/UY5jGiqpxwBVJNoRISLcrmF3hR1fIPAXSnqrqymZzL6L93/SLyZRb4rFSk/FnOF3gUS/vYBsI2kfD6XACexEGcosydVP+M5w8LwoaCKjLCLfEWAMmi01ABqjvuVQSUZuoGFEFciWL0glRVOnNZMkN0ROr2NcI1ZRe0ySAZ4ZMT28asje5+dzo8u5je4J9cnN6Mh9MTHfgNLYAiY+1jp5qMmHYt/m7DgLaLh4Nh36936yVhX0ZATW0MThcetgvn17qDbncAHrV/x3CAFkJ6sY+r37s6uz47vbo6f/fT7Z8+PPy43S5tBhQrlhvxq8YlUmQh85U3haiSPMekaMkLmGUNgCMMI1VxosBCBDwkRmaMl6AiLNhq8UYcJWalSckXYqoEqXL1g6yULspcLEo8uiEh/rccBoG3RMb8ib+RiKwJ/Gcu4FV7M+b+Z0D+XJSwVYjIMJmLAvfCRKDK7nPhCIMlvBKg0Yi+BYATlQxZkVPgiJL8urOaqqCMTLPhR9iQI0BGws5hKzX+W2T7WMAVBSDPF5Aw553/xSV3SfI/F6mQNbdVB3KlFEh3W5Kt7KJYRIEuUQpQoBKWot/tX0yu3lx9+Uqf53wwGAEVi47b1Oc/Kotnz5QPuABHfTuUUTRKg2EtRSaH857bJSDAOCoggmlQ72tY6vaH3d4g4usyRwT3NKpiiJ6CQXa/O7y5fDUen/Z6wx8+/h244J3GhDhULX+5fGrLHZ1LpDdhhbTsAKmD5rCCbJQ+BokgggeYO/B58sd65KcRt6MoU8mmxAQTBxXlK5E2kqAyQ2XTmBgZToH3r2CMzVMhCSUwVqp3U+uC3UooNOnxmqRgpspaxEY/55PW8emYowLB6K1BnZzKJhxwiw7BfEu7AaCoYSCiLg6RQFPMoKas/a42btjrLbGIiQzCdpUn0kg01JWPEdKrSQ8pRk5RWUMQtV3kagtUeYwip46owTCkTY1zgj6xxwESYEgxqrqBhxbwsgY6108RsWa3y7j2coKxH786f3d9+XYyOqVJABlfn6KX7pKtx6TmfruEMNkw/4fNbr+LWrMvGHK5Or3Rycka5dis5pLTBVrsNkvaJOQ7zBUNJ753CHNggBE46A0tva4nJywtuyfD0+Hkq9d/sd6u3j/8sNou4EHcKl4aHO1JGURmEWTkQGli34ErqRAhAZuCJZcC/8la9dMAidUkBalMSkDeo9DWkXEBiFhUzSSnBzAMamPARK4kRsZgqIwKH0Y0NCZEvPwUrWQNwoZI+H9OJWeaW8ujpM0jIctW+QuzGMEOhuIkBZezl5e0lEuDBySEw2qD6gXwcb3yRVxEynOUTDrX43PWerqdwWTQn/a7t4sFjvBmv2Z+BNcBF0J6JYtwrsDh13L6j+bAkwylYZAhIYUV/gFWjkDWf8NcouqWSTMoCrKoVcQ7Ng3SiIcCyoeZJEHA0tfz5fTV28svL05fTUYTRr00B8223Rcy1LlHxbfLp836qT+cor74QzDdw2xTutVsv9vgO23WcNDiRA33G6w+Wovq9wYjLP1mtdBt7Y14nYDoDp3AbtdlNgl+sKNDJpHwjhxSMvfUUZid8+nZX37517hMP939cbme7RwSUADHxyHFLTwaW+WiRORn751isbw8eakMXNSl4kr4eGuYJraAlblSjHSEihiP4Mf8QNsMsDQBanI32Ks2RE3VpU+WGUH57z8NYDScmhGBN8CLrm6jtZRqN7VyGeBK9gKt5wZbcZ3aJabhX6rRIfkDuxeByKAwm4GrHiREGiw3lIr9FNo89nbhih8cgGF3cDN9RZab6cXl6aud5r+738zPxzNs5tN69oSCrObM0K12VJtGTFSSp4gDfjM/gforD8vbyL64F5ro/BVDFDv1JS9NmyBP0rjrxCM5pgSi/dUALDj/QQWk7oikKWajCSxXjXrD6fB0OpzgAsXar5nY1OmvV7poDZvlbr3Yrubb1WIwuWIsqy0HhIaxWYJ3t7a17Lar7fLhsF9ll22+oJ62tN+s+6NT9Hi9vO/2d/3h6W697g3xf6qNOYW2ZWNHr9/vT/Sv8JH6dIuUpHc2Of/69a8WqxldAXToKbC56QsgiDybkXekQaWV3CxbZOJNiTciajxOwfifGjctqSpUtZfEm5OAMvNLHy/xRYNEKgzcVPWYy+pKDWGuebJGhDC/warm/PKY7NZEajTccPOhmgQY0mGJobnIop7UJTAh/ldJEhua4jTFW/FAyECrEOGSnMKpFwRdVQ9H3BokyZN0y5VEUbrCz0oPrurZ6Iy/Ly7fjgZjqmU8OgMjLvLJ6Hy6W20287P95Wo9W68XdP9PNIPD7n6Dm8xxlLwr2MPQYl3lM3vsQkBO+JMpWTtKGbqWxv/PAUPoqHG5KE6j9zSA4BRJi1dC7tgveaMh7lAeDgb9nm8xMEQVWbQB87zbLDaLR/0f8sStR7vJDzGUnkrAZaJfcIl3TdNe7TDPtizaDStdj4fdijUxuKDC8OMx9khjMDzvDc+BPPS2nf7Y0u3W4NalYcTsS7ls8Fln7Ihq79B+eqLeYHw2Pb84vVptZk+rBzpV6Dqx5nUsewRjSau0LxoABiXVXU2ihFviyl0uKBfFRFBANvmbkPJr0xoI/S/yNHDiaBpA4lSluqQn4uZSuUg0whuCaRKBwlqljShqczWs2F6inKqDuXl4QdbEqDSYTDaj2P0r8DxKJtbdVDE1vYXMNBnM0obNA4j/ChFSNqqNpr8bXEzOvrjQSz6fXrnaj8PdczgID2rMdkkD6VEofIldZzS9Oplc6UaoJqsfHz7dL+nKkWKn3+tvTjo41PJCa28Ih1sJcsFX2ffm2ahm+o/gMdKAoGp9OdZE1EWsSU3NQkbb5KQ0BCOLLgu6g/5I1rebDnpISXZrNi7QXLHo+/V8t5md7FbYd/L0hhfDC4z0Ttdus2Btl4EE41hUcqfqL2gDNCvgaA94P8z8nOzvMP84TmTq9sc4Udvdysa37293zPAo7g6NCrfHMQkJh+1mddjPkOtuuxxNrugxbs7fPDx9eFzSwpxjBehF8fP0LI16tAKtPItqOFeGDU0YHdT6NE8JoGD+Sw4SlD0P0Q2Dkgh/+W1EmgbQIrEK9b7J6+3zq7Iy4LOSpExEKqcsczjxGSKmxZ8pIsQVrAhljwtI/uUyTdtnZK5wTMjK52oyBLO5WRN0viY0gigAFhyU/hePBU52eg2SBp3+q/O316c3rGWO+tp+rH4s3Mlus8L0Eaa+9yj5Zgm77BWwHExZs3hE5fcGzHtsd9vR+Pp+djtfz+er2fnkYrHZPK6etKn40xLusGcG1VGO/pd3ZEBdyaORTXlAneSwTlwkgGBj4qvgJCEAoXIVDqXqo07HyaA3OB9fnNKSJxcTZ+hRbFQf/weLbkHoB5y3QfV3q+3qHi++f3roTd+w5AroavaJrQ2YeEa4NJXN6lEfaflIA+rhwwwmGG+aOX3UejnrDX17dtCF5ojeRm4YSDC6OLCEPKV1aUFkzGLSFEmGwe3JqrN6GvVGZ5ML2sBys2T4vd4pYYGfr+Oj+dVVr0jITovaJgeR/lUiYqCCW0iAK0voqwh1tb8mA6s6xoltkit7VoKbGAHBylNpz4vosBOmWqz5tY4qwt/CaCCtTzzEkVWQhkVw1PMxigQV2mh/jkSTJTu326gMDYCwWVcpzRHryy9eEI/Eox1OztkEMEP9s+HZl1dfvb14Ox4MFVvoY9S75cIy1t0e6AUy3N3VAJQ7yoSvrAOgH7zdrp56vd75aHw+/TWOMy4B+rZczS+G/RWm0WFrZ7HdzlbLjDpdQ6VPQEcohTymWGHWx4iF39hCS0VuIOS/HM62xJXTtFxBAC6adJdplrPLyeXN+Ssmrygteo8rFP+HvmqGgUfJie3sFyfbefewxcavFw+99apD37V6WC+fev0xU/6b9SMjXUbJWm6Xw06I7GxWQ3YKDaYYfoceiycNwZCugNmfAXrP3nN6zt1yduizLMBs0nrfX3YcXeBeMWhm7h+TwrzqCfrf644uTi8X68V8M1dJrBx+9JSalqOYqnTeE65HwraBFJ/uTYm1kIlrgUnBjKb+q+415cpUxdPlso8mOY/EJ1EM/ULX6nF012QRN5Ey7J+b7ROfmuLWVkx+xYiXWcUAafAGRZAFZeUnZ9CowwBQJg2aFIghv+EQKhJEpFUmgY7WvJigBiGPri1G+7NNEWPOZA47W2g7aP/48ttXv8JGdvZrRGnNxc0QPz4qc4I73qrt4S6gH/3BCBBS9EYObIWcInFKxVwH3jn+MR4FnhGTLePRdLOej3AORhNHgpRiv1tud6/OT+arOTMeeNMPqydHzpaIm3ymcARSFUlQSBYoie3UV4ofYSoeCxwlAChB8HQ6Q3zr8RnTLph5DbZ6qX5g+GEyM5uAI851Z32/W3xaLxcbtr5Pz4eMENYPm+UjlDer9wTMtWWMOt8sF0Q6kT88ZU1BA3+ywNHfbved4bSHt+ykpx0nLpQOP1e3v10vkdQJg4rNChtBMtuKcKS22zWDh5y8pnfHSvF0dHo2uoDaakfXhMLCIPEdFyfiF0VQ3torOtQUn/hGUhUbqVcM8qmsaR4tmHp1RFbpqQuNRQkyGGkAKliNDYgBkvrkl9I19GXHHCly9eimtlwaKPyEzJmLUCKpw4a94BYg8CYqAX9AnEFf/GMTcoEgALi8FYfqEuCPeTczWr8FxCPOsYNb3Vl2e11Mzi+nV5PR2fn4jJkRBrFUGlpOEK9mMJqgKY6pqQGxcGmu8HNAr3wYMnZ7uLB4FHj8mLr+6Hw4mu4PFzgM+82CzmE0Po0mIzqaAE1rP4XEfsM05HIwWGxWmUecMSBVokelp2hQs9hcKgHliFmivjhG0NFFzAFPJpqxATBfMp+M+sPLyfnri5sxFleG7XVQREaoW2z/ir2Zime3W3T3jAGY/8H7WC61AOPew48IjsktxKXVX+HHrclNefujS3w+fkaTs+FwTAe2Wz8xqQPdbv+003PgS9OiPmO16AQwCwMEotmLcrDIAB7WyEFOwbJ1Qucw3DEo35JA0vBkvIMFF4mpBQBAzNVqSlSIqqlqhzqXyf6LSEoKMBNrZZJC4FJWiC3aWfUqXq6kBklDyhjjdfJa7NglY7zUsDYMSjAGGhPTDABMTVXJLGS55M+LpNwE4T+ykUI0QMT6NM1EXiDlWHioq4tcISXeBldpgg9EObkGQzyRHAiehr2xku10cYjXbFnpdK5Or99cvsN3Z9FntXhgEtB8ZKB+2AepA9XZ7J5oNHb3NL8eHXoXo0X/TXPZ7tfKkeJm4pGeYXPyxAQfyoFnrLSzMsqaKPw6Zc3iD5ZVR2g/HU0nw9MJq0udHsygRI4x5N0S8efNB2N4gH9atmMgIpSD9WAkZiosB8yduxEmcZR2OB2MBlhPmaQTw+9nCIs2P+HBg8x2y2geErjci6f5/f3T3XyxOHQvDuvtYXr1CisMJK2F6gExVcgIAfsyPru4uPl6ND3H898u7zq7HpMBBzItH+fMkG4uxpNTBAV/+JZIjcVm1knsfpBsF0Vf4xf2D+dIBWtEp9nRQbJumK2KxmojGClRxmF/zHiaSSEKZtU0OhcpKAyuCEWxkWzdRxRGGlPxuVdLSIzPQNC/K034aJWwiS69ErmtBTSMcrI9Q/GLl9joYeGvCAClHUWHi9QWKYK4MGFKdFKoxAdz3WRcGJWAdIdP0WTuhsNuCJoznBsoTgxxyWYCzhKCo9YjtUOYGga1o/FgOh4w0jpnrMrWX2An43MmxTGKWbJh6Et5N/vVcr167A0Zkr2mvqC6XTymZ9gMx6foA4Z+Pb/Di8AOBsAlHtoG4zYaA7YxXcEUS9mDSodBRLoRZ/WoRrp9mqdjU8TBGIKhOkumzLesDytMtB2F5oDSRB4vBI5I1HaXT6w2QYBEkY0pOEgkunMyGYyuT69PR9OO84n2XI5313Q5TzRmUKPcmHbccYq4Xz7Nbz/cf3h8uF2tD72bLy77pxeL2e1hy2R/1/G6NdBlaz+KPr14e/nm15PJ+X6LO4TVmCOTDQtgd7frleOe9eXF/up6ND3TJxzTNibWyHq+Xc8Gw1P8SWpjs/y0Y/0YiOnNSXfd646tcVbKcEw7h2l/sBlMZ6vZYodJcjGONNS3LGxBok/+WVxArH3+9DPVAkXDFYj8yr/Q3vnvj080PrMrR+/krdaj8N3+QUQaAOMhRdZQE66w0Qgom2D+E7Oal4ATdUSBlq+W5NcW0zCgFbcKgwdUIM/lhHSYs9P3v5NYolfFYLKZW5KOi311iap4D3kj4cJI6KmLnR7O/c3Zq7PxBQaGLQwYRXQIs4RXi3YChnVkRAg9VARcvdEpXu/uw2x0eoOW4wAwY6hN2sxBDfHt/FZ75XtCzGEPXN1kBgRfKPPoeNv95Wh0hlZfMIK0DVgWVsOYStxQ0rhyJwxAV6snck5HE6bZOyQxflDMFF34pnwWTgHFjFBjiAT/h7nMFDyPxJQMGKYzCXM+Ors5vbwYn43ctYHdXzH63Czvacmb5R0TZK5jMG6JMHbzj+uH95sZSxknD/Pd5OZ8fHmBm7SePzrdxCuJ+Oi69b3R+Ko/Pju9eD0ajdfLu+XDT7vVwjmCLYhXH97Pbm/vqf6bRePODYbMpZ32++MOlXA4WTqhxApAh6ER4lrc/p7xwOTqG1pUd3yBejCHcNjM+4c162PsoGaQgUWgm0XyEbtKQDmjbPZ01LDaVqZAxYuYTDjCKcOololc9GFkT46oo9MPsKYk41KC7QhHPWCMSEFBOizLBVewyYUXMa3njb5KVMcFUt5Vax/0EFAB1MVKlZkqA4jRHrWpgMr4IWYKJaT8af7JYHmkzn/Pt5ElcxsPNj1NIIkhGnzklZcSCy30aqqfczY+ZcMjb3joD5jvhNVNzKGze7iwDu9mWCZ0d429l2JnNb/dbeb4M47hVo/0wvj4NBUNjbOsmKol5XK83LWmlAZFR93Fvt7d/pGJjuH5F4PJtUy6foTLpDSYUEK36CnYhqkXsmWVQL3PqEtTQ1ksZUlMs08EMSko2am2MiERlDMykhYEvIzrkSGDSHszItB0Srq438xvt9x5GaXDnNWAlO7Jej+f7Wcft4v5an3yyGTtbN2/6iznT7vVAz7j8PQUZroD9vwwdauPjwnH7189vZ8//LSYPW5mK6a+fv/dz//2dz8+LLaj0eDmbNTpMzW0oAeYnF6zJwIvh+Ey/iFKv5p/YpiLkHsXXyxnt8uP/2L19KnzzX/UG98MxpaeBjcaThh7zd0b57TbjhU1J09jQ7QNVl50yJt1XrXtj9XmzQt2DQf6ePeZ6/kmNiPU/qDzWSBoFMLoX2frHF9MjmlUSICBbbQbnVLWaB0chQBNLSjTRJFi6tQaZQAqD2CXUpodWakuJhMUgWTAj9xrDif8pfOzo0fZrXo7/Wo5LO5aam/8J4/wombKB9tzMT5nBhCnnxEhxtDeyBlodXH19HH9+KOTNpnhlkV0Ep8VYTOEBaX+yHbXecTg4vPwiGHFFq4X90xo7A5PaP9weunEol4+VmJE7+L0Nk4DZaEP5TSB8XI/dAuxDxvWRO1haGndPvtt4H3PrgrG4o+rGQ4KUx0KOa0hUkQelJmIyFuRpTVoFrZKm1IKoDxTcDpkxx9no9H5aIjdZsUKTwPtXy/uNnP+7nFX2KWz2z7iEnYGw+36sUfV8g4v4/Dtnqmr4cWUidD9oT8+PWeAS9MSPTs6BriQV0Sunn6af/pusVgunhg3rP67f/Gn3//40J+e4c38/PHD/SPTv/vJZHz1Gs8e/woP3+bNQGh8dmW9YCF61Mb4/NVf4mXdfv/Hzfd/POmP6UiznoCLxEB8zho1k6F4jIBz2Z6jW0jM+m3NaBWbNBJLVQ0RFkodA94khuVqqEHlpSJiMlQZeKOA0SFYa0w28PwFkejI2PflncqryEPEQHQe3QlqnrCMEgh/hQ0qAdy7Dk7no4oaA2FQVx/DZiw0RpYFhVM8Y8nCUKrXFhIkmD5CYVg3wKslTVC2HXmRycs7yyv84Vky6aDi64FQI3S2s8X996uHH1kBRTbyZOoW54dpHGynbgqrWqusj3a6KAhKjMvrYqd+Lm8/7cenF7sdnYBj3P12dtg+dQdnaAztB8+YheLB6RWed2eFttHpM7wbMdh1xMksZKbSnYJl4ojxIlzhYtnmIojUEmitwpj3tlAU3zIqpjAdSVZ5AbefpbZYukJlWNhin+Zu+UhXtpl9WD/RDJ7Wy8N6uZy+uhlMJtvlzEbKjBBj3vQWvAU8GLLjbXN2/aaDEOiQnYDcOMPFpDENavHh6Yd/8+Offvz+/Ryl/v/+7sMTDRxU+/2YKdFu92m9fX8//+JudlitqQf6DAZCSBLTcMJIl5UTph80NF3aw+tf/Uc/fPfxv/m//7++/epf/8/+i//12Rd/3euxDHM4LGbrdGGU0eGZ5cVYxHJGJkSg0Agg+oKGqR4UvJpHVb0KpEZYOY4f1LW6IjnaJDO2z5oSvVM7pWmz0cCGmM3kwIYWFbTxMUwjqwoqCW02l7VIgilCEvKRIBGUYGtHrnGTqjgJFKgoKgqw4jKcuw+RkkOHGcd0C5gDUYE/jQT61U9UJvFGKmpHpaAQ2EB2fA0GU5Kd7MO8ORl4v5p9WDx9ogdXuM6KDKC2nc9oCeQisjNgwatPbwGN9WrJ7DVvPFENtCZs56A/YMTX606YuaAwdCm71T0zFvTmeE1WBTFoliPOeyZe0E28JqqfoYI+0/ojqCav/lLDezicjs4e14v1Bu9ZsUY4JSRqneLTWSAtRMETOmEHyMVjIlIRdAeKRgmhbpvNUhnN78r2rx5uZ5/u7j4+wMbFzdX44mpHiZgB6JzM79jRtGe3x+Jph/qhrGdXN0pku2Usw7oFiPFMOofNyebh8cOffvjdH3//3SOzM6yisb+i3z9hKpd2tH56OJ+M3l5MX01HX7y7PLs6h0Omy/ADM2s6gTcmxxB+FADu9pPTq1//4//sdz88/fN/++//J3/7/fnbv0I4w+4Js9J3y/lii0AAVldUKZWEIqNFUVAUyWfEgrBVCQUiFPFCGsmDSoh5zW8BACQcjQalqn/YJhU26iNaHkNTXQ3CbM0zWgsOB9ZGVNxA0UsmgvIVI13xVUUpBBFmbZqW8A0tFE8qZmg99/BHCWimJNgGkhM9kGPkbYmKtvoSZSBvhKEzwzEeXXYynE+G4+w5ZLj1aDb0L3sgWeGf371fzVi5ZG/itDc5pYWwhI95XM3xkuGO8di9q5Xb3XByRgNBIVBZdoAxFwQgqLr7Ef6tBhoWeuP1w+93i8fhFfBvTgaTjVuFT9Aj/utzY9zRIvoHnKTHW9BPrn+1X92zzYaBAu7RcQOMxFNV8BCRUVZcIypJOrkQF6LkSq2VIPFsYG63pLtbbXoTZqWw94xTl7Pl48en97ePn2agm7y+GN/cbJfzgbP4g+Wn7/C7+4PB+5/uF9uT16eT6fk5E0Sb9UqqmAW6gB7NYbNfbmafHmcfPwz6nb/5D74and/se6PlbPXh59v7Rw5EOYzHg4vp4Obi4vRsev36fHJ6OWBbNXuhBr5t70jaMTWaoI2ipvo7zlhZffHNX//tfzrv7hdMkjIrwCiFMcJkwDh+9LQeMrFKCavQtP8MRCm7V4pv+QmpE8iijY8qoYfVFKJc6r8RKor/A05t5DlZ1dtodsC4qZ1EBjMrwVodBE1eTyoyTbr8BalwZCg1tdoIF4chZQLJzDxA0LbFcwsBkiqPXQzWDZ6KD/TcXPgi2B8JybsRKUmVQELhsH6KqJht9ejDBq3Bv2DShwlsp8DxwllWWS+WDx9Rdd5l1bxudgO3x1N+Rlyd4Xiynj+gpRBbPdxRYJQAC9hj0YcRw0oXAhRs9O0OBpve/cnJKfMp7I5Gsw94O3ThA3aDnWNUIcRM4mDC2PeKVVJmnFhaWs/veYVkMOgtb/89Yhxd/cVo8vqLm6/J+LC4u5vfz5hNqoFSI0L73ojT+ojQuaVCjSaslXBCHQvbHbyeXlwzxlhyDsNP69ktEzXrpyecMvoxmtr06pqd0RSJv9mHHzq4cJ3O4oGJ4JPh2Xh6fobrQf3SYnlpEouBvNnkg9nOC/Hr/mR884pNUlf9yUWPOc3d4e2XC9aGYXDAFhKawdl594RNHzRYBrsD3EC6XZYSIsaeVUx/a3Gii5if0eg/+I/+p+fnZ/3dA6SZdej3T9lTwWsJVHu/N9rQBVlGVEcdYiqtVTSlozJ4xWQlBDCRZICSja1ER5SyM5o8SjHBJAoPBvRabUYHSEo6aJJPVp0GVQfj5QMmqN0imQDn1qCWPKWjdiQjlRh80u2ffUxK2JKsHFQGfryi9LIKBXFZU3JkeSq3YbMRIzDUU6AWE3mcPWBH4ph1L1ZqmP5jUXa/uN2sGNrC8gkj2sXj7XpBH75DrYfsf3j8MBx+RdgJB4b7WPsFHgJOL3NzbIFx3/9+zoBMV51qYpA2ZHM0bZR3oAb3dgcP953Nkomh3ejp5Hw5GL4enb1ezO6YH4/H9cCGCgrFEA8HCSqbxYxm1Z++Zv219/Qj23UuL9+uzq8+PPz0h/sP79mDQE2USKwOLpVHCRhWmJbTMNKhHbuJ4PX0zdvTa+Z6t0+3q9n7zeKBbgf1Wy6YuQKkNzid9qenDEhg8+nD9/3ulnmexw+fMDC70Wg4GqGAJ/thl3cH+n2atENH1kAH9JA6b13mM+nqhoMTVw98x4Wh1enFTfemdk/REK02CkmIzae0McgzqGUkYW3xmRV2ldJooul0xalnzMvot3/zn7CdnH1HtMP96vH88svuw4c1Yyr1gHVDahhThvOhYtEC/FelVyKRBLJVOKXNpiVeCsoN/asuM/nQXaOTUHh4KmUironhR1NMtAToAUCiwJE96NV7yVErcPb5JXNOtnCVr2Im+w2tQDhLTVqHKjCE7BTFam9FgIJKhQchinGRyZxstTyahTji4xxbTqMEYRLpsBszTt0uNo/384f3aMGhM2BLuhmwhwj6QB0xErs62cyw9/j3kNjgsjMtuZozwZHZGgrOmNLJGcuAnUAojjJ3+xk2tbNdLpnPZ5pxNkOl15Nx7x3rPhfLwfkJ0ybnnALCxAvuhExl5nTAfjO0lYaAqq1PlrfMNvJ+yaL/fnT5xW5Ax2DZ+Y//ZDVFIJSyCqbOGVQylt2rZoHZvTC6HHQnh/mWCf0ZU/sf8arQ//kD/R7z/i6HwQSYdQd/+I5p9tH15eLhFivQPzs9rF2tY/bddyAmDNnZpOkZPDSA8fiKToPVBBYHaMxYCLSC5Sl22jG5yVQbis5Ff6sD7EYG/D0WNjbl+aDtrLg5GUA90bCydYqdQ6w54x46Xu/3xuPJdHoxmF6jWaunn89Pb9iLfrd4omJQEGqAwsI9GFLHWmOEZFWrDIqi1J27YKXdkY5yJ1cTIzzqotSa1ARU7Oi1oPE/wKyi8aPbSr27FcJU/hPL2qMQVhXtomkBJktNOLmCEF22PTUBNVniKitcpP2Iikum8ht6PjcR/opRdU5csSaAPAmnJOyEoEIZLWXoywWxLu3SQIfj3XC6eGAK/AGfnk4XR2i3Zotv9+zm1em4t7q/Zc/6hgnpySn1x6IOBh7DmZd/3Tu/xn+gkJmFYG3HTlxLRt0xiqJ6ATjcsr9m0+9fni9OJuz87M3nJ8PlmCWu05vlCdMvDy60qShssVCWNDkmZR1QISneFfEtwcNss/zDA6ux7JfDZlvwSFypl3AiCauTilF0WNMYhiEb+zosZdzOt8P+erV5/MgyFpOzi7t7R+rstsiGO5DQGldPd3B+enMz+/QeD3/PFM+YjRs7tXiIk3SWuRre2JqgreOzN+PpFdYa88yQn96PAtNI3AWt0iCXsq6YWAy8DWT1dEslM0W25vXIrnM+Dp9cj2dPnptMd72zv/u73//p+x/ZFoKHOhpPmVGYsLZ8cfnuzbdD/MvHny+uvn5zdvF4+xMqVDoUVaG4/FXt+0OV2xaiZCqRqZQyF9FGICjdJ/MkRTUxobkarfGpMtZdhbJoQY0eqklQsbpaTAJ4hUoqS9ucyPCjxSGNCCuSjMGY+qJ5oJ/VAQSFOYUMN8Zo4Br4auFGVjNA3r5hmHYDjQxISCxS5GEo2381HH85HgxWvJu1PTC78vAJT3a3Ygf8psNUJNdmw1YWxm2HDWthyx1bge5nfV6dGjOwY6cmds7VXeqUeQxe/abeqXQqkkbPkBFNZvIc95gA5Nbr9XzTA/WHR6YO73GX2YaJRsDqeHrJKYP7J0anzMB09zgG9EIwz/hhyZsot9Ort4PpKzy1zdNHpk5+c3nD+tqP8xU9UVMmi05QGRKI5auIErDtlAWIBxbyDmdnNKenT5h9jO3sDgdsPZ5OZncsZVgLbgVin8dyPr7AItxOT8cMEJQzCwK7x+nppD+eOP3jrqFlZ3g2Gt4w4lcJePmL1dnBaHfClCiOjFs+KQiz+zQRfCe2+ezwGh8+LB4/uVFufIGvmBVwvUYajIsNq/ns7v1itfnjT0//6l//fkWPwt92c3p6xiJAutb9F19/+0/+0W+/vdyyBe/d+avldv3j0x2vykeVVL5SDdWpkQ661aiZadpvfwXTODc6L0TGkahQ5aMGAqpqqqINtmRW8VqrDukYIV/QJDEOkTMzjgoBkxF7Ax6iw0SgyEJq6TGZgSnIROZZYnVJnSh4s4Qv4ExIGgF7DcDDgDD2B17GQzcZASZAMUjG2D2sTlbbPUdaXjKZ6WGV2bm20hAxPgUEXT+9viHP093d7Y/3q8WWd5V296vBiHl6quwwYMVzignEZWIrO2YSP/vQH/E2OducR/BDu2KKdbNguMZs5+Fh1//Dh/v3d08owleXw//sr3/+J//kV93t02HzZd+d7l23mG59SUqBMfXE64L9DTsp6Y7oUNgbnRemhmyh/mIyocHRYS18RUvDgctenh4yogqgrlLyHykknXcWJ93uaXd5YIGBQfZ+v3x4nD88nl2cr5ZLhzoUxN0Ph+XjrD+ZLBary/OpC2BrhgHjB16MQUib1fLpnuVu2ulgSPfJal3feVuQshkBntlAO2QrOJ3odMBJioPxcDTBLjDhSxe3oNuZ3douho7+mTpz2pfX3k9wEmfr2d3T3c8//Omnn2edP/388LRc0a8+Pc2XK6abfu5jZhhaHE5+/OnHH/74u//Nf/E/+2qKDvoywdT3Krusi1FWJYFC+oseqGc+tApnsFGtBEph1BTllCiUvtHOYDDtz1za4micKlZhj0cvYnpkpKpsudJ0WhWVSi7VlCYNaAOWhPAAxpZ4QVdsGTaLBQL+G0pFp22k9wjmEFYn4AdVSpPPvKf5bBtoJdPfWWeiDk+Z2Z72uqv19tMtld2xr8cP7o0mExaGZ/cfP314+Piwf7hf8dYjlvl8iu/enZ5PeYmK11Tx73gdlgEwNUQrspLYaICK4MjTXbBsMxrefbh7XPXePy5/vJ9dvHp7cX394cfv/i//9Xf/wx9u//N/+v7LX315/uYbBIREaFlZQVt2WZSgc2GPJB7J/LE/xTEY3P/4d1tGw1/9aoKwdwsWL9Altge11adQGhEoXioH8VJe5ru2vJr1CraZjFy5IW/xcMeU5eT6CiVj/xuKi5ww3IsZ2yrdqjEa0HbpG1nc6NOs2ZMvauqKSmNWYMxyGC8/v6J7q80gbMuxoWn7meyZjtlEyJyPG5g3i6e75dOH9ewTQavl0H16/Hj2+ldMQ7AMR5YNc6WLB4p5f/t497D9d3/6NGPdoHNye/tAd4RlQSW3fd+1gMRsNvv4889fvr783/8f/rej/fLN+duH1R9YUEEUsbSqZKQQl10jCCaVNepAASwnkNqNAMYmEl2ig71GlaJBlBglA1Yt5UYyueqPR1Pp+NLk0A0eweL/ZCDdfD5qhRWPbBCOygZh8Wp05UuIfHIjnWpneZYhUPCDR2/Vkio/0iuKocRjS9TC+IdVAEB40AUJg0ze1cOAqXOY2OXT7R1Tnig+xxuhH66cMwW9YPPj/PZ+92l+uF8yMt6c0wZ63bOr8eXrC17dwJgxfcFsJ3PZ++FwxUgAvGMmNxgYMThm9ZMJz/HZqzeje3axPzCOe3y4I9PlhH3IV7972Az/5af/eLX9ar2Y3tyc9M9OGDWSTLXhH7gSzHItRwmuO3ffd0eXlHrx8HT+ejka33w5fnq/WrBHSIFEEhbO/5TJWor0LbRSQYA48ot7XtnnAS9n+fTEhE8VnmEqYIjp6dPTcsG+fuYr1xe/ejN/ehyzeMsghglLnJDhB5BuV+vheMr0JQvUDGpZOA8Bpmt0URi/ZlzbR1m3nILOOz3zT6vZ7X4154UY/MXBeILvdP7m1+Oz1yi9rtSaYZLVuWMGgiXhyfbq1cn99z/f3z6s147zVa/oQb0VRGdFp/j4MBuML7dPHDK3e3t2xfj94+KO0lrWqu1Gva3xVL236EIEVjJRNLQ0imghjPOP/2pVYmxFkaFJCZEdCZNaIORn9ohUewDxZdjJbC6WNisxIgzWaKHYvELFVkFzJdx6S0lreLESK5hQ6EmzSIeWqt1kkbR881htJDpQABILc2biEs44eu1Bb9xfLR++/34xY05zhJvKCLC70Xc9HCb4pJv54of3j//mJzz0k7PxcD04nJ12r17d4ApjwNyKRTPo9nCa6Ys5RI1tlYr5hDUrttcfmOGDNnPnX73bXH03/LDYMPX0ePdxNxxcXV3+k99c/uPf3Hzx5Rfj6Zh2yI6AQZfxNxMpnIvGYQq8cDh3zE1Derjfd2f4nevN4e73//L1X/7N9enbv+qf/+7uJ95SiRhKLhEAxWMOC5cz5VQBeIFlPzvlqLXu6ICLvVr34H8wZnuRM+l9hp5semCU44rvdrE4++YVLcGpNlZYmLYdj/Z9Nr3ReSyz9MsIadvlTQCoYQFp/K5YsdHt3JccWBnOyzTY9fXygWWN2v3Cxk/+KNfpm1/TRS7YeMcbMCe95XyGSq3YeDKfzx55WWJ3NhleX18yM6sygdfdK1YbVJE3lUgxvv6LX49Or9mUPvJ1+w1uFm8zP3HwqHXsX2sGzclFlDpQJlg9UY+iFagvSpjLzApLwakm+R+F09y2OCokApceHdZjXUh3EKxuofihbxvwTAvaUJpRdBW8lV9/2TZVXBAM0+FALpw+sl2K8mVGqD43JJkMuoBJ22nZtgcM5kDoBlhk/keSIuXVDF7R2vXZb/yH7/aL7ZDBLrP7siwCXCOP9bl9+O/++x/+Pz/McOvHk8nFV282H39+/fWr8dkZ8yLO2LH9aziBKDU6GF8w2mO63+3vbI3GpWYSB8f0gKuyvHl7/o//4uZyeooHDtPjYe/15eTX3968/fqbIa+GKCGmgcnCqi97AfKKiYeFYGGZcuV1W+ZYNncf7w/b/evXnJn5A0sTuOxng1OccwoY4SsQhBq5WY/8J4HJo8nJ/stxfzq6OizXHHPCiGQ4ma7Y37leM4xhYw+vnS2elmveuqEmeKFn5TaJ88sJXhBnmbMflvcR1JStc6aTs3MJ4dbvGIe7GYQk7PGOFsX22N3K6SDaAMdesDo2mDCDieJOKCajhMGYPmE5u2dzG5QYOrPiwa7POc7+DB/o8Wm+2Rx4U+eEA3KtjD3Ni0kfyphydQ7XN5dnZ2evv/oVnLKVireJzya7x/WHYWfbO1nQScJbozkIoK1xVUSJlK771EhJXdUcGlNAtDqVGMkRrZaXGgZCQGJUTVulOWCDPgDFoQeQdmZvyEQ0jKhMQDe9Dg+isWJElMxtSwUEfEgy/bh6rrcCFDexGBIkEeZsinlkUCBLYqak5xfA2Cnz8R/K4IdnHUGc1u6nn5nr724Yr3KCDXq0Z5bPxrffPL1//NMf7n+4W3NkzsAVT4an+7PL8c15f7lYjq5eMdqjT3c/s684Mql3gzFEc7eMAp1Sxzdg09uCrdQ4wjhLv/0H35yffrz7+QMzvmytuH4zvLiauvmIuVd2443OcTSYXGdekWrbjM5IGJ7uT/rTzeHDfn3PjOHD/WL+MFvd3eE1//qfTl9fvvp+caccqpopYRxCCmmcUigx8U4+5wz2ZvvtYP20vv+Ab8Y+n9VsNhyN6eWoMxaFGQShjCzOYnKxvmgq804gPrA/b8Vb6vRL7pm4eHXtwJeecuvBbUgUBXYnGBumH339jZdaGPDit3ANJpe8X1QvR7LJAeuO/8JgmlfbgVnOHvCjVqynPD7OZ0/LOQNu3H3ep2NCGN+GHaOj20+s/ooLWqgRLWIxm52fjl+9fkur9WyV1eMpmy5OcRWxWuOHxcdsjiAL5UcK/KiATcNAt1FuoxWQ6uIDXKM9KkVsZftsKhe6nZCaRkA9Sq7GTRIZio4ZEDgogxMGgtocTSaRmblpAFFHqwp9s4VE5RlAiqayGCe4hXmBBBWmAyFNdsFisxavsC6O8psgP2GWpKQ3PxVHsbqrRefi1XixefjwHdoPHmwZDOhBbHcPd/Pl5uTyfLicY6Z4GWX36f2Hf/IXN5PTs8nl9eTiejBid5DvsjApzmwGAwEnRhj3DoZOZrsmwB7ZPqM3JheRDybvm7Oz69dTXizENWI7DfMeiJfDAA9jXA1rmEfKQLMaTi9woph24VTmk8F0d/h+c8siKG7AyWK5u/vp06ff/9sv/un1V5MRToai1izZwWtcbOCE+O9CN+Va7jv3m/315n7AWt6ElbST5R17Ud2uhGiXTwyLD/M5Zr6PC0QXyOQjmXHBKdVuxBxXnxdC6bomV5fnr1+xDGyT4zRQXnKnFnBRRrwOkbeWQxzR4grShFwCgy9mh6HE9pADzWOR4p2xFMEIafbp58X9HeVisItIXGTfESbEMqUvf52fny6ZDnILoOqH2Yop+Qd8lIbNSMwyQHe+vp0v7pbLOYuXl5MrusuH1a39Y8wBuUoWwRAJRRtEpwol3RjhYh9tDEpQtSvlcxwBBKmUlzyGhce2KmkdjI49AFEiitpFYaPcqRczEBWVBYhGqYJwEQY9l5KygEEuAwbI4j9HC008RJjYN2du/ChzgBJhgY6FMg1myuQHkdlszkDjGPCS6ejxcfHTR7pf3s1whZX3kmwCbi3qT8dvhiM8jKu79dPSgw0mo/5ffvtqdHHO+gDeDqs/uLzit477Vp1MOJDu4qAwd6HJY5DGHJFbg1BRvqZ1+upNH5faV8Pw7jnwRi/cHpS9OuTGGefclINn6TBZaFtkdXrIXCJrQWyI76NxnDb6tOrcfni4vP341bu/+Gm1+QSSEo9SI1SitmIgip4xvhzw8jgKN2aEvut+uMOVoh61i5o9zIZvOpCXcTzixheiKCwNczrRgI3M9Gzd7uWrS5vCkBeG6A6f6OsgRHZU0tfeGaLSnFzIR3V9IRPqTGAhAktlT8dUKYaSPVecgsRoG5PPBDHjEZyrAROneHsQpCdCFgiHHXar7XrAttqzyWY7pJOAOMelv337+utvv8mO3d0Jc6zsFT9wnARbUXaz5dPj8m4yODsfXj7ydlu1AbVAJVG71PpeegfrqpWWwQAlUvFRcxbOS3ESQnMVJuUNsPIWa8QOVmLTA5ijiAGozkaPbTTENyotjGn8c0RjF5Xr+JtcYG/4A4dkdcoMyoih3A2RHSMXTSdZ/rxaPvKkdyMzgpNAHO/bbk5nnwbbwUc2Ga9243OUmFVMLA8ToGPs92Cy6u9OJmfMiw7u75d3j+t3X1xd37CDl4EcI0hcfzYF0GBoN5RD0WD2mPbn7Spk4ABhOOYVkP7wbPn0kb0MzniwoMtLZ2waul1SheSEG7ryDruG2Uij70UrYCTuaR8MynEeAGA3xvnNl2gJGy6f7mfL+fb+adX/aT/8V//y2/Prr89ePe0+sNcYgURCirQKCk+qeVT8w4YV637/sHrdOWGwzxYGpl9QCfohRqfz2ZLe63GOCqq6+Oo4JqyCsWQ4u3+cXuGZsrjX4wVNtJwBAyuDTg51TjDDkI3JQAg45TiQNGe3WDCuZasHQydykIvde2qtnSLvsLOvghgmqR1JMbZmusFZV1pkyoBeoLLjEfuLaAnsVxk+zeZnp+NL8OEn4pG6oMzUEz7j+GTFWwTuqqOCMVSPq7tRd8L5BhzhQZdpp5pa13IiXI1vWr76ZZMITeipMvlPVIyqyqlqhRebQ55VHyALVv6DnAhMamW3jfinQ6o/Y8VE3QnHQotLFQEROYxVcdVaMTSxhAUrqvUjInCKLI0gBQsQ5GTLmpa67coSoQZgtFTSaq+EGQU/sQL2+3/784efH8aXU6qAZS1OSGCXC8tAbMZkmtuDPbecZ8Ci3uH1xfDLr65gE5FjbJAqr8zwR/3ZJKgGjsdhOwPu9WbOBjiMIuNH3pDsjaas9W4Gw86STZwPbMMbn13TKhaPHxhcUkwsffaNoe28OYD2uAddM4n5zDIGrdvJ+CwnTUb4xpwntZ8ttrc/3d788Purb/4KhZ7ps5TAFGcE5R2xIkI6W+cpTee9JSYrDx2WPtxg7U5Qtyww0GTS3zl39/0zvKVVds57GGVmiiaX54wZNss5doN2g74ylQpiJ389+YV3Zdiug8nmnFCOhGDMz+iac/K6LJk4SqZ4A16wVK08EJfJMVax7QAZjbO1iqZLHjdPgW0BRfaYYBzZtpqWxXsVDGB4JWias1dfvb66OL90mo638JbLxf1PrL3QqBnA9LsjGEc35rs5U6pUNE2KQjO9kMoHvZYUxYg9JI7ECIhBApHKx5iK9MkIlAoVQtlQrwgZDawAqaA3Dk3zVAgvJI4zK1aCicntRbBISEqJpAkBTq3lAroQqcOGnWOKQStD33JslPFpJ3GfindNcYhxA7mc5lEuEvaGqWd/1sfvblniQkdwedmChGEZnrFJnd0+1MCB48vcDq0u7E45o55ZiemQDpSXCBiwovqb9YxtPMx+MH0EKyglu8LQX/Rrz/7mx/fz2+/YQ8moYHz+1eD8Xe+MeTrfwGKpjA30F6++YSUBR5npSN+lGow9C0S/gtGmTjClY6YFlcxa8JadyQ8fmTrExrHjgCViVkl3P/6bv/t6Or6ZXD2xbyjlLlk9F5rixu/UMGuIehz2suJMksnWE/4fPcuc/QaQQwiCcj4nnzTQvHP6OTM6bNxgPx/jDPw1DzFfzTEcEXFeuUIxsS6sWZCXOR92V4GNboE5K17v+vjze+hOz3j3lykg5gvGG9Y0tFNsUXLGGSNOLItyjKkRCqvd8aU4RgAlYluhlhTCjyvctwMndmSLCdQZkSEGpqR+XHz6u8HlFU1j2J8wwYVKuYZDdezZmmGRqRdEGfuq8vPWBsJRxWAiak1pVMlwVbHqilcUJwI0vdQnCciRPNUzEAITlxuDVS7xGoL1CMo48db/giGDubXmXDHJwHgBR7Eb6IoTYZA+R6dFp6iVi7uIUCx/DHDhjNoDhm+ZzCUiaqlzMn1arO45qP6Erpgd+BfnY9o4XqaNWvvEwcXsY+GPV3Lxjvtn1xeYofHptSc9sUUevT57w5IQW+GZz17OH5Zz3k/lXBB6ku7N6y9HF+8OglEL3TXbhpe3/TGHDH359PGPtAH6Zd5GdoyIT82RWyxB5FspKBHDYTjcrO4YDLB0imqC3oHgnA7BI/EZjTCJy7oTNm/2MFv8/NPFu6GjVTvrEhl3/nw0xnq3knDV2GP61L1k3uv0dMkbW7y1P9idUHyEheiYzkQBESJsg4E2jt2nA3y6Z/8ejR+7v+FDKBzYolfDlBFq2h0wxrAO6fHlVYuBe4ML+Hi/IEzfRetAW5lIpmXwDgEDDEb5lOnpka2cvBe5e3xy1oi9Hw6MrSC6DVlmaRfbvtkdPtzqVdJqmFtGJsyxIhP6D3Ubz3D5uDw8sTo9Gp7OFx/sna1tDacl9wexaQBojUqGaNOpaY7GVkKRmjdAuYGVpCY6UUCr3tEss4qBPkNouxiZxVUjVJjIWmjJFULciFH1eI6WJx4galsGKj5hNcaZBa4AF1Pem8zGB7/pacT6zTQcasMhbNgLTqslaMjbMOcjG9GYrf/x0+xhwfoWR+2wiI4RdPsadgVTSpe6RtCb3nTCeQP0B6NztsgPGByws4uDT/Ynm+H0Gvjv//S7j3cPzqNv9swZsWKK7ozHw7unf//Fuzc4HfvVw9ARM8C+FswU5+T8zfzxJxRlxYYclWzONgNOvdT24/ywfdrdAZ5EtGTLGiozm9HPY1Dns4XLalo1x5r09Itd5/5hffHj7Zurd9/evPvD7AGEiLnkiUpTUmIUtbPWSoP9a+y84U2bxejqhEOp3bfU6X143G5vcewQOsN1LC4HRTPzgpfBUBkTwjEEvPDKqi6tdUcs37TmUFG2eYzYqUHYsTo0sCDIj2Et0vj4afb+03K+RKqd68vN2XQ1nT5xujndBiuGrO3NHQEzFeons1l/YQL07oFzHtDdwz0aTiC1/NdfnT3ONiTRaL/78ZZvDox624+cpsdgfr94+tM/u/7yL7pDzpH8mU1DfU6p6DGrx8GMCAHdaP5R9ziWDmExuG28Gkkx6O0iMsiZwn9VREmgUfXYKn5yCC6OWHgK7zwCZLgYAwBfzhI/9Q/PFFC13OLEfW80Mcwl1kYpKVHTRDNsgLaOa3NRjxYlBBoOYd3n2IvkTx9XzEnJZNqRXYUyFThMGH3ojD6+f3zPdisdRr04W46Nhx0Iroa5IX7NlhcO7vGdPXZc8bIvG294dZUJysElJ4LQyf/04V/983/13cOC0SozGGwkxfZzJMLFefeMRvX4+Lt3V531T7/nc4fn774ZTl/BBV4+R3zzAs7i4f1q9gBOeoDF0wcOQIinQ4+BFYbGKePF+f3D/O4jbYpV57Oba23qw4Jto76evvFsQBjnIMJPP9/3B//66vr1/eiMt5jhluJQGMhppRoR2sv6Brtm4mTXPVtoDwfTQff87a/2lx+Hp98/fbx9+MT+OFaDGYDsGALwZmOk7rcOugdWux2namYwBWO/p+HcEmsdvreAKvDSvJ4kisZE7Xc/z3/4wGuQO95dXHpU7v0prwb0O9MRZxz58qcmtt97YiTjdjfGEfrDvm6MEuzp4ehVHEHeLTa8v/PlF6/++P3Tw9PqD3+COae13nz5drb8OPv+d2dnw93w202nd9Znw/ljXkCl8vmXOrfWmQGxOakGVnabxrg++mJbaS7SyPZSbiaUSO3lotTBHn0iJ0Qk5B/SyqXqcWWggB66GNa4pzFDJqXfgCfo09ei9KhetJDqiWars8RYjrAOZVmztyFUzq7c8AxMhiSg8F8AQj6QblSCB50Bi0ZLY08Y725/fLzl+DNGY7iKm72ng8ATTi5fNZxyjjHePEfC3fAyh6aDb7tgXFEItvXyIu/iw8c//d2uO/6X3z3+i999xDrjpFMSqhTy7BoYT6evX90wrfPzH2Zvxjs8nfX8iWExJ5wxWhidfcneIEzlmoX/Dz/APb4vhngwPqUo2WNDq12CE0fCvalMmPCy8uEJFUNULBYhBfQ7vgK7bjt8tpcGu3r/++HNN5x34jqIte7N2lZuxlh8fqhGJTbcdSdsZ7s+e/f26jedV9v7s99PX3/X+Xf/ik86id9NbHSHbPUQ1YZDaH3zPe9qRG3ACBp6JdoGys/wF8mSExpQu3tYfWKaS451JMmIaB/pY09O5mym6ixZGQbxcrtf7A4UlSEYfIZJp2XPGYUzSWH2k8enzX/yj199+1ff/vDd3Z/++PNivru+GX719avN3Y9IgUwcSdbbrd/POVyeN1SlidvK1l6VCxYjKwe4uWplliBiID3yoIBeiqlVMBSK4bc6o7IxFEE/eJQekMlnDhk2I//9V9OgEIyg6RdVc1Kd0/BXJ9OINBsDnzEAMonJh2wFfxlvKYUoSSRXYvgwlgBg9EP96mcA0R1tSpVUy0nA7J3Oaa97+tPP79/P6LvRFYrD6ThsZPn44eGCo5kAZeVqyJD3Cl0kE2dZLnlRZrNm+xubAlaPPz59+v79dz//+LD93afd+x/vMneBSyIJ+o+TO3H+/MP3X31xsxgPr76e9KYXhyEDDD3k3fqRI7b4ugQrCa+//qvZ2fX88RNJvJIDCnnktQqGHdQrszBYs+l0OaOlsjiAq0BLA8vJ02x5ynCQgQljwRO29rC5aP6r3/TH8Nub3K1WVbkpcMqt+OGq6tvOgSE47YAd+adn75hjHXEwv0dbs5i1XMzYivDAchsyAz/54IvZIdSZTeL60O4S3PdxFZe8yyLLGjjcLY+PsOz4QfMZVpqaIpGxA74SE7twij5Q7zlEmVoYD/fs9PQoXO2j5kMV86IWXo0H72kf+/1vv6X7fHfx+recoDXc/pfIBXd0eb++/enTzRdXNNHt08P2+gHNul1y/IxfHaNx7diJ6FbC4oe2mY2ieVQQahRE1VCUzjIQ4QVxec5PCm8UTEX7iW8YLIUWeVY88jIa/q0IMB1mSG+hLAhG9iquOoBUoWmVGwMKAvwQBtgQl9M+5bfIDdkVjbhM5C4of8gyEXIc4+S3fpqCVAKPDXGe7T2ovVMGlz+7q1Gc3Q7n9mNSudi0f/tpNvCUy935mzfY5w1vPHZ67Bfg5UcsNJscjeTET3znbv97fKi7JfsJwAueVoJOWVCWxWI/X6zfvHqzZALczTIMdnlBhLPDONbgjj6Fd2/p7TmOERvLFgSAVo+3q6fHwfRizBu0biRlWzSbBfiGBhvtmCZnUXg/5yOJS3obzPPhZOBOBE5xdMCyOXAWw/jy0lVoFpVS09aOxW48yUg4YkQ/kB2V2uWYNhaz+TR1nwMaVrM7tnxevLpC9nfvmUEvLbECmZfsjGoEqaQzn8RdA4EY0X1kSaOijEz3s5uOxsncpZ9LgBPaGm2L1IHbxVF05EXjPR2ziM4giReBnK2sTkAcqFv35GIynHdWNxf9f/q3v2K6jENUP/ybf/34050HdbJ/lKPal+vx/ePppOfLNyuGcvRD7gCk7DgR2tponrryrIWlOESU5pTeE/bxpTbxqEFDZKX0prOEZ59AqFIAIYtapF2HrXwkr6RcQA3eZDAmyghg09GEP9iVVTVTOB+jzILxTx0v5rwHecWIjKj8ccujxaf9YZXCZYpuEQRE6jJLbzy4u+X9DSb6slzbxX9B9MidzQ5Ps9XFKdsW2CfMlt0+5/9gldh+zFjgjAPERyO2AqN37JThlV1OiWDKBHap0WZ6GJHkUr0YcA3GTFIwM8FkP7utqSeqhL6F2meEcdg9cLgOYcvPMHu9/vm7H+9+vuXVv5t3r08vr1k85ST+Fa+lsLK/3mKXaQO4J761785jjm10soIvMXU5e6TfZ9fqaHbX5ZCX3tTC0vPSfanEcGPjR66IhKqi+SGWMYsdbFPjnRUWHzBMcM6WjFN8oi9w+jDY7M1BpzDkFIdjW3g9QEyIE9vJj5ouoBkZCRipeePmRPGIDVJ8IhadOGFIwU6dq6uL119e/w///O9Wc0/UI9cEeVIJNBKqf7VlexGTRbQiJp04ufGGr65dXb1507t599X06qvZ/cPdT3+kf2Vwsr9n3dfVXBhj8/XhisV6XgjrYdJhTiXJUNOqRxEsdeIcFCGF9lKBeP4sijT5D5w6hCLlXzQKSFHnHzeIRHftE2xu5HJEkVz+6ImCTii5oj05t2zYyqi/aD2P5BfOWG1/LtnDyjVPQVsJIheNl9jyawyTX2xW7zkWpyeotgRHuKd0f0ylsU8L27jpPOn8wBxG1LEXs2+8DGPUCS4G2kad8wK773nxcjBb3K4upmevLq/fchQB41JmMNk9RsFwhzB4VCTEqDb9ZVk5mU55RQZz6AtiIBoNJrDImJV9M3w1DMdlNLnonl8zB+oc0W7HUjEzjw+ffvj++7tPnxbvrreTKRPtvE8+nD+wmd7OgXkSOJytdw9zBwCMPykXNQA2SkIR0Ax6g5GnV3Q53wLXAfOuvMMSoFZFIzmf2Hd0PuGNhre8Wk4xmFVEl5njd0VicjG+fByxM5RtGoeT+RO7mE4urnj/XW8/grfF+iLAllfknCFGSuCmjmkLiALuzqaDs/GAQ0GZOqP7ohV/+/UbJimpUNAy9mVvB5cjZ0/wJRuTQKwi+Hogg4lJp8M5rV9+/fX4asUWa/qJp08fZ7f3tNn9yepxtnJ2etR/vFu8/uKc7eYcZsSHZdjXEq1MmXWrQAZzeVR5dXV8oAwIgycf1bMGwp+CTpvRKAeFT+a1jITUKTPyWDIlzgPSDhs0j5SGIiDsa2qR49TkJV1I2hLqoq3RKhmjQopxg/FpEnBOPtRZQBsvlEKLkB1oIhu+NW6WRngwOXvtxWN+hKaRYFZ4uZwAq/OYhcmr84fH24Uz3uRgEkPtBxMR8/nmhFfH19sJn/OiXfOCIufOcP6r3+xiJv6RyX72EN/f8/EIWhy7+jF+HosSsoqTCVHns4lia1lvzGzm7GHfv7liWOGSu+ec5IR19opxJktv0j2sevTo/dEnXrJHsXacyMIi9JCpQYa5rJth57CzFJPuSPXXuDDRQuNxLghDi/jlfLai13g9HjNn+5HvOlIXClshxuiQuxSCeNo8X7i7vD57zanrztxIV8+KsrISvuZNxeGn06tTui5qh3U/tPTA0jYnWAepPQ/aj2ODJab9u8QV0+2R7t3OsHt1NXm72D3M/MgFLH68n/3404cv+6/fffmq27vFjJCLF2o2fEJvt+ekOXaU+L0ZFmDWaxYF+VI2Lev1my/Hr3yd6Ild4D//kaLPPj2wlMYYYPa06K85aLG3wlCQkUrE6qmP+Gm80IRTYbOyUim+utrothHKQbVqWkRpCuVqlM0UwjwTwopE+9v8xqV/QYgx2oBCiVjQsRIcSgAHnl6hgtyRnA560Io6gfCmRP3vjU4NrmDRyqur+RVxkFulxBVAoIixFcCH+O1ebUqy1FzoKY4L2263i85hfXI55q0JlunZ/MWoheVPHFV0iKV9ZhB4xYOOmS84srrOxMZuwduxY1b22Wm+ZXPM0yOGngyeoMUp+VEEl6VSsCKHKrF4i23GNDIg4G1bNtktnh7Z2jvkFUHcJgwdU+wHzl9xszGDXXYDvP/4AMt8oReVomzpPDihkY2ksob/vT8ZMLteJKgDPAGMK2NTdlCyANkdd2/fP5398OH04g1z4Zydz6viSlk5IS9Ki1CRj64OX2a6GF1eTa78nCs2hXbHHKRLucgMjWV6n9Y65j1gRkGe3Dzts9DBF+1I1h3SVFvpWHv1yZ0CbsFhBZAojBhJrNdeX43ePo1vH7dTzpY76fz7H1jDPtAPfP3NF//s//0v8CfHznoNOGSUl9psfLikdiAD3ozmvbnpFYuM59PpsNtZ/vTv/vn9jx9spbw38zhnZ8nV6/OHj7xh1zmcTmdMG7BUiE+nAjhJi+CZfLJZhz8YN2+EoByQp05LVET5PGuKyswjeBSYAKpR1a76GVeI+CT6aPGrIRDp6KXBpdaKWa9RcKPppQuf2gnA0ekxjQ7CCfuAwZ48CpIYbvay9QAK6jEW23ZwvNICSKGZyW+AzdO2LadPMHvsPFhuPNiVg21ZagESIYGd7JguOLzj4Nbt/pxZ9v7t2c0Zms9e4fNXbvVZze7p91FKVzlt83uGeui5276w7Ja1YRjdH49wbj1RfLvlhAn21NGs9F/t5Rm9nTK1f8XMD02Twn73hz989+OT75O7B5MYdZ7P9vKiAo0DhbNMGn6uWBt7PBawPX6CXoIWyPGYfDuOZdrJ3lEA60muhsYicCtRpyYUycX46i9e/ebVxVsoMgPJ+gPqwR8XVMCJDZ1eMcHihyLZGCV56XeYY2UXkOXNGptEOnve8qJuGabYH3AMJPlptLTCwe7tK95mWNw+rhhlz3f77z7csV/hNS+O1UukjDm0mTRfPr/nZhIawHR6xs5ELMWIrzVcf8F08HLxw/0ff3f/84xD6fDz+Pv06YlG++pqMuZlGOxSZ4xYWKDmOCX4RCvTABxZaisdC53g/roDwpVgBEw6f1ExdUzzyaXa+ah+Utq6KDeQll6lskFletSH6KhoXEtT2T0Bk0DdlHnJnefyRpskUSZRKn4mmgeWNnnvA5ywENRQDTMvfLjQkEsQhOHgky8RoegwnmI1DMh2lcgyYN3zekofP2TOWqTNkSkznQl7cHSON67Yb8ILeYfulHFY927Bso3d6YE9bAtWgalkDJSDFDalTPoXkz5vKm4ZjUZAKI/HQeFd4FWAUc9uzxne7LZmTxZmltbANgv2sow4VkRt2/JlOcT5x9/923/2L/7Im1C/+eJqNOH8REblfkaIcvnKGj7ykCnHA0OKzHuWNGXe4lHgHUPyw91sf/Xp8Zt/+BXdGGf370a92bbHilFTIQggl7rNK1ksStPRYK0RmmqPO0Oz5ggqt7gB6Kh2NDksD/R+Vp7NS53hxBgEMuLc8iFkqXd9SyTI4EZnCPffL41QjzQDJlZtC8w1sJ2Kc0EnDAY223/773/8mdflUyMk2wTcPsCuJOSVOmdINLk8LBZ4exwozWtkt/vJu3/09X7x+y0lZujb6/F5Ac6v+eLtZPrtFw/s8TvpLA/srgKP+kMfFI3AJKEBDV70jLkrT8rxIl0tK631ucRkY6F2LReSLUgalNGiKjACBNtMYAJztiGSgJ9oGv+DKHQIJoc6liZhcpgtoln2Uqkga+tE3hCCI6ssmfhtO6+g5xFMbRAsICRHGoagKKwRMIV2EOIWjkzizOXDgBMQf7pf4oam+wIXBouxr742Uwn9Pm9VwCnHnDExOj3nDNDF7U8/Tca8poQBR1doNJg9Dux3y7teU/aQcWogp6xgSHWGwYhdwTLzrsmGDe6bNIndyXTSHTGaxG7TAbGf4jB/Wv33/+oPf/fHj79+dzHpbi+mY045QUnRbIayYAEXJPDFIe2XuCmOf1YsXhYdCGWjqbGBjjfI57ePh28Od+wuoBnSpE1kmgg2FRNXBsd+3ti6YMLBtS43neLjubfHrTUur2nC0WP0mXEqvgOznhvm3xnb0Oxw2PUoOBoP4Wo8lXj0habSBJht81OaeObMqn397uIPP9xz1sSYidHuyYyXLTuHnzh599MHPsvAiIHVaATFPCYaQBPia8UrptQ5affu7u1f/lXn7D/87vDwq87+4fv3E07HGp9xiMfBRshJX/SttGlOe2ESwqMdqXLYQyTs4UBIaJOeD2PDTIxqK2Os1QtEwv9Sx6id6qDASjYkmUYJkTGA2vmYdFBQSrvg5rLl2blgqhAFP1F4Kym6LxFqiEssioiuI2lOmkXdGy5oBenDdIcaSYYEagRW2iDIrUsyw5FtJtgKczhE6yyENeOfq6RWisxhSvFTZP3A3ASP0Ej7Ql3t0+kXcYHshHt80KE7HbBvnVNHTjpz3Ivb97fX767pDnBgQIK5JICbyziMPWgcLc2OSZChH2w2wPo7MZIyovr3j50pRxnTSx+6y/vZFS72mBcJBkxxPr7/+F/+1//yDx+Wk7PTswGnSLPexAGxGESVFsHjm9DeKI510GFArKuOp4ycPY8dI+xLWTrvHMd+vzg8PK4vUdsTTi9FkcmX6o4Qc6Pe4C6c8lmK+kYBMwDOA5Oj6g76gNCZBzjrux6EyqsLvNXF+hIJfLfdEbOar+Tc6cxOvKyj8y1UtuXTZnEveWueGSFmKc96mzen3/08Y3cDy3cnQ3P+cP/AxNW7V19Ot7uLk/no3Zc/z7qzp0cW1NhuNbmZan54O3g2f/32m9XqbziR/s3NX7/ff08fvO6fzvlCFXtUVh+WnJICC8wT8bqdYwKPSURhaKQqpoXBBKAJNA2VQVWMgqJVFMHiqCG+MEkgGkZef31UZ+IhOaeErqqD+tJon3rMZdW44zr+h4fjQjJYUv8gkJXMSkoWNkKwcY5sreGAm7jwSshGGPS0KZ7oQ0UkVllRr5nutF7JqM4JYarZ/F8E4CdNne4SzvQSQElO9q9j5NhC1UOr9f7BzFwOrAEBp0TRNgotU9K+EAh25nN2J0/3c2Y2mJHYzTgOtjxxUty/ozuhH0fz7TKRxFIkw0gYTU3gWe2ZDOGgnPHOF+Fxb9hXwbmG3/3w6f/3r3/4/nbBfp9vv7j+4uZyPZvb/lAvxoQsFbDayq7V6ZhvTsA+0yOQ6z34QgkNlflC9s3zYQoYx/S5ga97+Hg7f8dKQ2fjib7KL7IDPIXnacx60viaT6HxYgmRbkGgcJQ6sghmosmHxdRdYi3ccUFUGS8ChWbkjdQcX/A6hDO/zIAhDRaJ6wgg9IFdznZQLvOdnVLe/mH+9oJziM9+Zm/cKp9kcqSAa7R4vPt+//hAp/LNN1+9+/ZXP/77P1CxnKI3Pr/kS8sMBOijHt//3PlnP15vz3q/ubzf327orjf9DVv6tmx5GKxOmFpF5B69wdSoCn9CG0AP0HbLrWKgy6qHTwSipcTHQUgyBaZlUO/KRItv3QFrSfklzGWlq2EInD6RADGi96ZDjfbgAgVW7VToXtJGsyTBA0zZCs2XS0lKwuYZOsTG2EENXOC0NyYj6Ly04TggYKfGZJldiIXJH+u7ihlNNkHGlE+KRJn7vO3dOXn6wA4U/QGc26Z00WnYABR95l1sTDtKyD5QkXZO7t9/4nU8TLuDS00kzoiyYkp+4peNTlixRWURBX4/RhWmgdgySNt07/EW6CtAyezRFYepLL77MPsfvufYm6cxA7nT0V99ccress6WxVP9JzZdUlayxw2hELyBgu9wePN6yrHodxzJiPmyD9YYUAv0aygx+2Du2Vb2cP90esF6s91/5KyMc+G9X01e/ebNb99cfkMt0AXh3uDX6eRQPZgSSszbkqg0tcHr+XjcW04P860rkqm5OPp0U3lzzRqllVqjVAZriwiXXoJ6gRrqYO1QGnxymv16czXtcmAQve/vf549zmmzHd7h/X/+3R84b/GffvP69XJ2et07P7tg+ZtNdvRyw4tLdhbS5NZM+9/PxpcTvt33iXUKK00aKMIM55B3T9UV1YXDzuJRI3mrLJc+hWGl5Y/6hlaY3RjkoIbIsGpGMFrGzVx6sYTIHGwa7CAFidmjOgGgqTCVntOhgbaXSSJ30APpOCLjCh/Nyd2MAEqKe7hQ1dRZrp3+NbVBWuZ3ZcKlLlSNDFgefmBDb1lGzQJzlCEIMf/oCDB4ohRRfiAm0+wE7ix2b99MfvyRZQCQxw1WoUTEBRZfeT05sMWtiw7wlXfMHjvvF5yl8Hh6MYIvaW8PfJ6KvQmcCcFMPisy2H7WnygYSDJ4dQcVeyg7fOQc52k8ZXdMZ704u7lhFuS7j5/unua0NOZSf/PV9a/endOz88lnDlFkqZo3u5WazejAK680CRIp8+R6ymsvd5xMSJFSKNQP5SNMzwFXj2jh/K47PWV2HBdE0SpdxcJmh8vR1ZfnX/DOOPaXrU280Zt7zQJpUhSTk0FoPLNeDGLXiId5fIrLu2PYeqrZCmDuB5kwywNyeERkbmamtxzYCdryaS0YL4B5eY127CN7maZj3Pvh7eLw6fGWuFO2mnO+9Hr943L9h+9/+Aejt9PR+MCwG9vPt8noI2jW7HDu9c7/8red+f1id/vYGa4cKqo1VPIG81/Tb5bTSNtqjCyKQWmiGXCCmfCxYJLXuqbSI0bK7UVMZRCPkkC0qlugBDajFWO6dlFV1X+IVio+OiCSgbGBEvLmfBkch1xyEmsrFIm4oQIn+Mc+2SSdBYe6VjZDASpe5RWfWq+R5VfDB1zYo+2KiZuXVJsLOhBIQel11JKT1WFwcTngkEPW46lPNhfALLhwnFORKApb3PuvLtlXhu2zA7ImcELwlJd8BXL3M3k5Qne95U3i29nmL795fXE2eryd08gRErD+OLmpM0M1oBCcHMTZrn/64/dMHLHXjY0/6DZsogG8cfbrLy/PLs9WvOyCKCy+s7y0JU6YhhN0n5U4JlsRFAuNX/7Fu7/7/S0tu8ZVlD8VfWCN9dWQ+c3hw/e3w9dfucphVUVqkQltkG8KTUZTlrqwsrzviN/PWp0vqmfsq9DpWmTMymbSDEc/UkaaFIMxMdv1EArGx6VnRjy8YIn/lf4j2uOhcfpTKh/bGewZZ374kSlRtkSPOUKmRzp02C8riMdiSuaH2X4x//6yN6lvoY7OrxyCcDDM/T3i77OqPB2v7v9031kAn40g5E7Xw55WnXAVA9VA82GMnw3bpHLeqAzilyoHPdRyJBCLl/LhP4X1ooFYbMvBLyIrBSOb6kTlatbT66JtwKk0UT+1kicBsxmOH7DQ8Yg109WqFnhj3WG18MuwbHEZShHMiaWhDESisLac1CJwQBLLD+lWCzk8SrtB0GApXJQq6KyktKMUtYY4FJN1184ff5gxOYOS4QWR1wUYKGFM+BbzqH/qhnXXf3wpA3ePDtXJN1hjh66jAvT+9z8/1SwSybzUeDYesaMOWHpnNIgTZqk5D6nhdPVe990N5ydPgPz2H/01Kv7x9ife4mIl6Pp0/NU3V//w1zdYLc6YgmsHr3x1nu+QssvNd9NYv2M9DmdCS8FI84u/+s3Hj6t/8c9/RyeAA4axof5pxMxsvn41/fVvX4/f3DAHRKvLNrTI56Qz5jXnwVRXhz+NCFbenQdcrMmx58hfvCa1y063+VOOEEn1xgwxJsBs8AojCw4OVOCLvRgMMZ1QxgljNokeo2c3q7+kluAa+SIRA2NWbFnc2O6//fJyNFx8uKcDhb4UaHTfb7f/zXd//Fu+hbNcsAVrd3nJ7jy+AalP6uHQj4vJfM9rqSsUw+ZHLhDGO0Dhox8qgQYBtlRV30/UOqie1q29vBqm3qnsRrcuQ7SeXO1FahlpQRvdU++jWrYGw3VT9e0FoIVtIzLNxwhbErLCfiZbEZOn9ADmx5bbVsQEVwjLJDl0WRDfReHYJIUFAn0Guw1Ei0TVq5dcDYRQATNV/sjOHZ5SMrMYqZFw3wmmlTbGCFUMsG2bVV4Uh2qFT/4y+NSG814fn8Cmjtl3cHU+/svB4GGx+fjIrOfu/afHr68u375+s97+3FOJDiwDc4gzNc6S/mmvxwuUb65fzbcblvcvTzk/i50Aa6qETZing863X53zzZinJ44g5VQPOAHHgRGIb5emycObkyzTMdJhFDEan/9n/6v/5fvvfuKELNwrSohc8NieFuvH7hk+UvfNNYjsJxVCyYqp2DkHKUz6N6eswKEKTmMxpPb9Y8y/2k8zwGOhKpVvZNLUIQ2Z0Tvm07UDpkKRAlNYbDxg/RoB43MzRCbAwUm+rJnxgPXW66LJcDd/4hjTAQe68E14Og3Gqa9fc8p//+pyTO/LN+5YWJstlmxD/LvHh+EPh1+f37MJZf141j+9ghCD/5OHn8Zvzi7/5h8vH35kXy5NQD1AQXDMqlKpOGteLYSitYw66d/TMHU3NM/qmXBERc7qTYDVilbHSn+IEFUajGnRJoGloT+lQyJEaIaosMTZAHI1aksmeU0alZs1WtONykXloe0EbaCwJwFyWAaR182f8Bxs8JPROmUBTksQ7okTY3L520oGLOABUm5purRHtvi8Oh/y5ZFMBDm3AS7sFx03Lgf+zOkIGHEhBGB4G1znvuhaoO71ef/rmwmVzBtlzJmyn4GtBxcYWTbc8Zo2s0As//LC8KB/2h1cTc4uLl8vPv3EgIKZbE6Hvn16Yq8vDe41r7Kf9haPT3echcyolUrVHGq/YRkXCHuNdtIEhsP+xde/7U/OeCPn+s3X/4v/4//uv/2//t84fe6p2//t3/zFqHfonZ/2vvligaIdVgMctGx8AgsX0uP9ravJzVdXXwxde9brc63KqR5Fnj1udF1qOXt5oO7GAoewvA3KxAYbHDDrznH7OgIwdpX9E44AZXGAtwRobNp/D7HC2PMxPOwHhZmwLYghACcRzdb3t4vLa77ocjUYsYa1ZhLubHJyMmP1dje97F9enH+6693fz//d4xMt8jcXZ2xxZhmO42lY4UO43/6T//ww7a8//Z66dFhmVVv7WnrYsrEbRxVb7yqBGklXbGJqkmdqLhc/JqMvgpmDYLQnGIyOmVYgTYxZIJjs9gQQJQeSU6tsKQ36agABMzO5oqz8YNnEYNtJXrkkk4SNkE74bttAQxhUoYAUEbC2OP1JGIQRMtqAnG8oKy8+Ek2nejXq1LQM0/JhGUpO2bGxf9x5fGRfMYs+5I20aABsaljv3n+Y9W8mXbZ/2SQ9NBG15EVcUOMLwQUaysTkFYuRg6uLy/X7Dx/uHx7fXl1z7AGfMNQjZuKosz7jdHAmqnuDy+s3Fzdvbme3N2/e/f7n+6fu4ZRFgc2Sw7V+/dUFuvbwwDH9OCFwBi9qjHKjZHzLiONI2R2EguOebxavf/0fn169A+wf/advee34x//2//HNb7568w//EY30vjP4frb9wKfEPO4HxuE+gyvnBjrX42u+C+YOUi+ERFmRmMeZcKp+ZKagdZACAgm+Yk+YIjvzozfhbodYUBYEbS1WM/MDg11PyTFO4V06p6p4xdE5nCGT+htOBCUTjYXxAMWcL96//upqgn/Z23MAHS/Z06PSzh4elxdTjM54vtz+NF/ef1i9PlucDQaMrl5dvnr7N38z5jT5u+8wEWoN7ZQqUVA1No+DA/eql9yXDpRKWkirjtKpbgo2MgYuikIp+HMLAlmbro+EZBIgFwU1LhehZOWnIuAnCgwKegD0jECandpP9+mP+icOHsgVSYNE3A0lSyWAJaCFizppRAEBkkibX+NpdfygtVowW4GYbepSqn9QAhgDFv1XDyCNTzHAZSXIluHr8Wi27fIeBtYLfPG+3HKz5VD83umkMxpzZi5UVSW4Yko0c6Ycm8PcPz7S4eZs8P5p8frt1/d3vEO7GX95wYQhWx8YKLCbkyxnfhSFPWd8jfHN8Oxyen7BRrrt+fTt+fmnH3/gC4+//e1X55Pe++85KZTvjjkIoahYUkfeHEvd7+AHsSuSvQKcRDu5vplef8lwYzQ5d6vpZv7lX//t8OZy9/jDexdWe4/MSNE0/WCvjgd16QwhheVczz47O7TbCozxDpLg/SzKxQS+auMmSsai9gy4ThmGeLYXCBjOuNaC7jMW4nIKiIaqeLHtblBdM7AZnroXiLODrCwM/2j4eL9krz87IFKBLi4ilEc+a81GbrYTdTu8h8/gvrNwTWP2NGPgf3nKu9KdC5z8M95P4nviq+5kv3416n47Hb5CdGykW5+OTuliFxwZ5qgnzdwWqyY5dLEw0TW1JrHIkcKqVcoWgy2DQgEXzSnFtEBCUNPi9YpWBEuADWUyxNZTCi8IwdI/EXDZBwrhgzRqOkx0PJNABoLEq7MEbXVy5+VAQbD8M7c8IWjx0ZJ8AtRYc4iHmiGVZL0y7kCq5MJEmbD5LhIQQQbAwFFtgMWdm7+45JMwH241crZfEKBzmOtu782ryde/vnaNIPWnBkUiYEANGCQyj4F15SPPl5P93cMdZwVdnvMx3elwWN4AY4ADGydPR9Oz4XR6eoH6nt/wcYBPv/uv/qu//kd//enHH9kh+ttvL379asyUxsdPHIRD+4MJJ/4pCzWDHC/oPljvnwxGLCP7yZmz08svzq7ecRI1sy9Y8BEfUZ3efOJboUwksRMNV16NLu1XIhSaZoCNwMamvpnKYkOe9ieOFotgEHbeSeUgkgHsjvccUCW1itIyUGZPpSbHbdquDXDcFW/ljM9x5+moaC0YF9wjszOrxMsVMH15Ppg9urGEGoEhDmmA6NMTX9djj/QQwc7uZhevLzgmiC3f83vOMvGlNhfmqYne4WyEpvp9qbPz8eUrOkveLjjwkZne4DU7kXwpyPkrFcCiEIjo0gukzFa1FzWv9YqqJCI51BcvHYbokrkFtKtQc5zrBb1FEwVX3WxbqpsolSv5gLDVRZtVPS56ABVRnXMQSYirdDg1Ai5yRIfBS7VAw6KAzTtV7xyvJkpWND4YDwk416Eeyj5HtJmHi2TV3nKHHHdAicPgkUXj7rjcnJLycuPV+qR3dnM++Dgf9R9IAYjToE8ng1/96oKPQVy9u2QZZvW48FX5rYvBmP+IusvROcycwxPGk0/1fvvuav6HW96Lffvr3+IKsLNyNHTVwfU5Jn94Md5vup2enl7yfZV/+O0/+PXrd5/++Acs7aur6V++GnGgD0fdzNhnzVnk2B48cV6+6Z0wWQgDyLu24lEiPBMPBuKr3Y8feXGeSXK27F9evGMahAXd+Yo3hH1d7HH5+Li6581h5eCAh5tdsmNet0XwnDkm7vzqtXPgeKTo2jFTmnyq1ZR0itQxU1lMEXgcVRTCraxuy2BkyuntfLtyxdwxO2E5Tc6T3BA5nQPH6+IvsWTO0Nb6yduNaz/KB6yDU14V4FWx9cOS3enj8ys/tzx74vwXhuWMsdGVuLodDiC7fvv6/NVvzm++Ob/+ChcLXtAx58lom/Jt2SyS3VSUCEERGx0wlAcstKI0HLWMflBIVIGcKlCUrxAAJzAJ9n7gjjIDSryaRh7xF/24IqCoZiIFwPD9SuuFkpNggEhaGE6LrYoHTB3tg2KIPDG2GcGOxFRsLSJ0nZIBrcUUbeiTyqXfIIZ6AoYIo8zMnTlVWBZv/hvnLh1UAl/o7N3Z//w/737/u7svfsPbXpx9woIWL4lcsmGN75F2O7d0mE75ebwr237wfDinkkV4PyuGL8wW3+lk/u0F1m/y+vKqx/e1+F78znGfk58n95wAwuQfW7acY4WN/fbd6fTfPX1Ap7ocg3V+xmlUHz/c4tfqVlAYeMbtx7VwsoTtNfnYVgoEJrYPjaaXvkrGcqtiAoA28PZ0crlYPc35oPHsjvaAKz7nGN/NYrnn4B5lgHQwB6dsVGD4i9HmU3/85HIThGthfqtU0bgMzHKHaeRhrcthRPb04PCQlWoYTadM/kT29Cy+1uXZdOwtx23rds/Rez4d+Oh7QBgU9ghSz9SZtUaAQvIxCV7B4KhJ91EhGI6HZHbo0t4Hx8YOBykc8PNOz19dvPmrs8tvhsNTvD6K5qvY6dhYTMC1cr9Yxm4QoMVGNdVespd9D9mgi9ZEXy2lylBwahxBnemUSHdGFfSBe+xvGEI06JJWWT2GFJngpfCCryIlrOFCwWSi2g/gVmzagWKAV0esRUGM/vFrixQQGKcirAr1PdjowdOqjCDVNm1K+ARxYbOy8TW9JwUiXERIEZ5DiHftsLY2P4R/+vby1evx69fDd19fY2z5UiOnIrCyuNwzep2u8Wh3jxgzP30HUbyAHe+mdK7PR2zs4dVEPhL5w0+Hb7+4nKLmLqN5LiSjQQ5OYQPBZvWEnUaTocwRnoczDjI4f/fm3XvO4u3w7SD0iq3XHCmCOrKfDO7RDDLvaS4sRJydMlnCpH+VBYUMBN0X9s9+wuqhqBhV9iuklHg/vMXJoQhsR2a5TGGmypEYx9qxcfWUL5siLak56cnmZ/WeMBVJdZgFX0h3iK0n6FRVL8rmOe94QMx18h4z5zSzOI2mEklLGKLmoyGNCO9q+bRiupOxE6Wjq0BwblalLdGa2M7A1iRqzEnUPd8ZdlZpMKHlQHV4esHBYQ53YMLuBT4dIEzO3/n+BoXs+yrF6fTy8vTVx8cPqEh0Ra4pniqjXnBl0d/q50nNU2PUQ5MKAsjojbmiJ3QnqKsaq8CSAwAEAcrYU2cD1aXkCyq1SX8tdCCAAhrv/2yCjEJi6t2bZbtRK8HtzpTmlRdyhgtygDrtCdVS9mABD0IhXmZkC3ySRxUtjk0wP4WDBJW88okVwQUrTcgReIqOYUWB0PuK9LQzDBx98nAwHb1BPdidToHJynAQTkZUE+Cs3XAiK2djcQDP4smdxSDEtk15+wTP25rkUKrO1SVniox4udhNAZSaqRXAfEFXF8+DBNGyx8fBjjd9T9lN/+rtW85IPB2ffHhoNvywBZl/bMx4xZlD7C2l9TPlz7ZJZylTf4gPLVs+rpePvLWO/SSdJNIQF10kx2JyKiDrzag7E+sYdlol6ZyNM+mz1xgrej4aonCqPlafI8tpMMqXDK4J4MgwfZUN5FQb+x4w/Du+EuVamz6JtemyCb2V3gjaz7iDfRS3m8np2FE7VdZj2meJq4NgETR5aryMeabVWhkcR8e2QSCmpzSR5ScPJKYSfHUL0fESxfgcLUDpKTbdKAMkXlxjbzSVSPfKlpTTycWrszcML/jmeKkHqkBLrYvyos6NDkiNLrzRaQBIiSwtNOJWMzSyqiaGCz0yQTCe0GkNpcplLjUQ8F9ciQTeKqiKANrVG4XFaRucHsmmFtZLXJLkED/dXAaluiXQUzXrP+gl4GRYGmJhhCKxMuR/W5vGkLAtGELYKAAtJY/p/4BMjGqXYV26fkqSwYeNBHh1vMubrGREQZUEnx/nABmqhhcOWdxnOv1kx8HonL42PTm/oKVv+EwVkoNulsNAM/HwHQ7L5GA4z/T78t2XaCwDURYowc+UKQpghq1tRvcGj3mC8q1fX7+jCPv1/G//w384Wv+EyrDsMFsu4Ayo0xEvn3jyPqo3zpKiJiNv5OApuDvfVSo+Vb9AU5yelBpCgpj1NcZtWDPivsDdv13g3LhfbXQyvJxefHP17QSnC1vu98rUfqZSaAZM62AgGSFQNpiBphNHbMBhYlnNcFGCFsOuZiStQtMykB2R7ErFVHDe29Oc94H4dhPd0vSUl8s8uoI+CMYQmHWVaqat0bDRfsygRr3Px4/5YDLvWPsWzqhzDjxfeeGQLZQWATLE4oziyflbWvthdOYsJXJ3puiGouKZ/v49Qx4GMPZTdUFI5cjFj7rlPQWB8Xg5JkdNKuAgEfUwF4WjvVg5/FVu+j7DwQkeEdqZ2Hj4tWjH4jXcEY1nkZnJcY9lzu2U74CznsFBCPsObyfNdmOOAUZZnXWTO/nU8FcDwJCjyuFGXrzgD09PXQ81+ZWLZJFrTbJK4OIj9URTQElLy9FzNVbcmFEg/ZdSpqpxbXgEh/0SIeuTw050HjjN44qDHWr7t/aSNWNQ8QYjIpmj53TlHebyzvcDJq1h53R00WVuiLd1EU2mCBU8lpMGwOZruoDF7HB+jidyfXb1l998c/nV66tp5+7DI59idA3JJrOdDPqcocDh97z4UuJmVxyv5cIW5pQ/trFSBHWR6mQSkATKozzSL/lC+fp8PH2c+7YK235WvkewYRYMl+l8enlz+Q7xcXwsTj2ZqEXe1sQ/cbuGK1z8c5ZBmdmgxIEd9Oz3Nf0Ajc1ZIBoh2WKOaHxqBwz4OjEysafg3GC2pjkdi/XEhNaO7tJFJINdgMbsae4eIZhwwY25VIS2HQyuN4tPTpcChF83OB+fvr64+XY4uSKGMRcbRfSET9inyHIitU4RWWxEGgrbuqQtxPKihQpKRdJUQidK7IyNjThCM9I+XxVQ6ZGozgnRyYjSybSzI+pU1YepdYElk0CJAGclmKHGAJjTs8Hmor87751M0E9eiXJJh7M7EDnn3amj0TqdJzxYfGyMMfaFl5gYNVkJNlyYkj17DCngXTSPqR+ySpo39wRm94IbFSPAyAOpKUkya7cgCAW8IFDbuMKybvWuy0x5CYjSOkPBhkzqjRrnhb7e8Hx4xjfqTma3twwNyYdTW21pqKMMYVZ8d+PexfZ+jleAPuBOuPeF7pvXGnm7npdOsHS8sgjOm8N4P/rbv/kP7tZ/WHFo8q778WHx6Z4p7RMmOVkOcg7EPSAHvBlqV/lnJg7NY8DARCSt1Za+5c3NBQsC+MYUnxJSd/3DkG8obdjvAO+2+e6It3Q6g19dffPNq7+4wpl21tTzrkHAEUUcvsskKCVyu4kLw9FlVDoyQq5EcGfVhOPCmHjntWAkSHZbH/8ZtCMuxkx49QuGwBwZdMIbdg6YOCPVeRoRIUbq0ZoCHe9dLLacAP/p04ykm1cXUOMIGt67p1OCKSTOJCzn4HVZD5i+Hk5eYdO267nf7XQYwOa5MrqcJnSGRtC1qpm0V1RFO5ApyUzLSl7XXL1BATxmD7b9b9UHD4zbGix30wpQenouMmEgrF0w8FUIMKALqWoaUDTfOBUUZbJF5klyGg6XF3uT3vq0tz3r7s/YB459x8XDnvBjG+AzW3ydQSZgfdzbnQ229BJpJl2MFl9fWGFwaQ6SkDmJJRjmtXZGUGxrHj2hBcEWYmbKglNitWCoCSDE09Xwz5bN4K9h1fzYoQ1jupDBCUBbe5wRoa294pASy4Kl4cAC+v/NvD9Z4uTyhq5vDNJih0yMYlzZBU0/0L2+fs061Xr5CZ1w0MnEIixj1XBg8bHjTeIf7Jbj/eqcCh3uV7v5D5i0h/sZn7eANY4HpArVL79AuuWcFKa98SXq7XJ6NkysMzOOVvHUF73tsrfj80qcL07VWLZUgSPF09M3N9v1/fzTgtdveGGqP3h7+cWriy/HvGKrhXYgFF9fW84X89wFpH8uCqcmXIaDBDHoDAuGvgjE3gZ8Dg72yc42h76YebJT97gxe9a6kDpvpeGzTPoM65EBr7GAD+FhwRgwYMgwHFSBJ0BzNE2v6+dIJoOL6yvGBKuHewjzahyjfubbDtv+Cc6nDYZKxApYjWg/7do6V4d4qYjvh1P4Uj86ZL8+phDKJfKcsVJt2h7Z0T5mznlxknQlwE+JTNEZZZeOtTE/NKTInIYm2v9REpuLLUITBE5q1wGevU7cb3KZTcvTn/Q3aP+ks2OLPId7qfegj7KSmY0f9A9YRmwHgSs7CsBIRmcPa0606u74ysN6x4H1IMXcQiKaW03Zbgs25ERnnhZXGg8M2PVqLDms0YZgWm4VIRwgFDXMlsZX3fkMFrWOU+LLpOTAC0bCV0yNgg9N4oCv1cKPXFCr3c2CrVkDzu/A1sz5vrRvcuDj0z8y6TieXO2Y5FmuQR5TiqoiLGfeHT/jtqPduFjzx/3ZxQ5qfKmr252tGTOrIhp9NqW6m4NXqmTUXfXOD+r4sU5lF0kDQil5Z5ePB2+m7jyzZfrVdjUCMxQLRWkJX5y9+YJJ3NUDa0XXk3OWTqlLpIiuy1Lt6PCNS4fmFoThKoKhndESUFUmYSi+LZcMvHNMB3u26XGyL29HcGA5C2csb9E6nC1l2xP1j5yRHaKn66HIYKXc1hBI6RlOB6g9J0Gw04lCUSKYRXR3H58YDZ9dXXFI2Boz7xvT2uzddsZHMthcS5jJUFb9mItA2SHKH3VAa7g6f/2rN7/5/uMf7ZvY9XTC58vVEv9bDVQAkqReyaX9JQU5c6HkVBL/iYvSmQlo/DBVxBeStZwoDH/+15jZBpAzsmFvqxYrhIi1yJrbGFuUzgx0YL3tsOv+KT6jw0zIIPlpWnTTFqnPsfJ8+ZaWzlcPOhdMoVNaaoB0v1jBd4I6rANyyC/H3nMgE7vCkCVtA9QgAcZJO3WPJ1Ve/yYsoDKacsm5T4FyURL+ZFeDHnMRp0lavOhhdawnHkKGOzk44RPQPU7qZHzGaYd04pg1JY7t2fU4PJ/pU1zhGL79gPEldKHiP+a6l/c2MEg7WFRCiBIm6RLQC97r2VLl7KJc8rojAyAUef/+45z9YUz5o3coMCrMt01AgHrRw1hvJLG+dNhiF3ktnYi8uY7STPk0QW/jezqRCEMDvA4ApMwYl6EtKw9noyk7zW5Or9kFzdsNziCtF8ycWP2Ot9muajeMjvJE62BFjAbAhIzjAS5YpxWjPdpyOwQ+B47QoMXkLSKmPTKKGp2N1x22HmHSQOtAzEnjUY+RLbNNtl5IZHSASQVpaocjcjlPgGmf/Xq24IC+NbsP3+AOMdhGRxhFnA+mrwbjK76oAz2oRmUNAEJNUiuM0iYDD6wnqfZEYHqdWbGyVSbrnBrXBUO51C5ki4ZpsN1KjcunNa5GQnxEAXe2HJUmxhwAVU79F4W/NL+UQTT4CZIBfSa8mKEGnDHfFa+zqpX2GFQ3BoS8dErpz9D1Hl9WYbMYNW0jccc4nSktGwIciexh9IiKOz3nfMu3ECUFEcpDAHJ0iu6UkVlGBVVsU7UOAbN5QJw4hQF1mDRsOYIq4nBK9LTfOafGROn7hJwVwuZl3oVEAexQKFCyg1f0VLimyDfeQWdvgNJjvjd8CNp36a1qX+SNPBUGCGCE+pcyQ8O9p1DsHpYfv19+4FUy5MNEPwNBjCxComjAccYcj8CzcutADwtDXv6TCgX0jnawnCkGkGeSprf3eFo/T5aSYi+Z73938QWt9Obiq/HkFbhZp2XkgHfjtA81wXtbJxymT6Wx2ZiJXLooY9Fgmqm6wQPa0+swxsf9RpdsMIcxJ3khTYY31DAcwgPHinrGSp89VZ6cRROk6mgASi89g4g4M91FAPb/6MdTAt5Fom/BU9LXYrqRd+vunkanlx6/x+krvAcR/dbnxJXabyijaskl906JMuebJxbDMtCoYWtMQpRE1cfqsyhjTVE+5UdlULFwRiegQlhHxCA+NFdNp5q001ggjZAp5Iw2B8oYfSWAvYMSHqIcIqOEZO+fd/xioBGomC4cFlAZ6tLgWJIfrVHBaEUMoRx8M8HBnAHxGEPQRV1pILYiRlOYakQuZZCqvCIOv9oE2mSUx6ZIGnzT3GTOf+FXYFAmM+UlyZ36nctB5wZVIdUWg+ad9YcXaA9PsYAYSMug4SC77XTA6bQyYdmsDUwaTWG9eTx0TzFMVHI5G3EWUQ65xUtFarFh+C1LFot/fPw4w5FQUUCTzQXbHVaeDsCa5Ox8Nvkzfcn2H9+DQYL0mHxCw22YqD3anw9nMHOqC+Ka0Qm23spjLwbFc5i7WdJZUzOcgIudxTLSaBho8i4+Jj+iQ/Gw8p6SpO3UK7AqlSC1G1OCHCgs30NlAL45WQ/dfDFAdREYOy0WTzNtAj4PlcjZAXzBbsciMStitNHO6HTMOIkv9uL6s4sbF4j+gF1uTPJCdMFxw5wZ4GfFmFviM4WbDkcUoCO8OnFzMTx7xcyBPWjceuoCxqho1B7JoxUuYbLuQctj0AqrCGiPHdSQ0YVo4jClzL9rfJnjRud9vQPhWY8aDlCoZhFahBFjZy2pWroy9PRu9LPdCIAckBoqCACIVCIF7n/wpPPXThlD66cS0G40A2uphyS6JFJqX+DCbDfEKSN2hYajG0qCRdGyxsY4p8OchQ05c5S2YWQCjNaP5lNIkI+nkcALGNJ0aEeorVKQfd2baJqc65hwG/f4Plbvqs+ByA5S4A+v9qTHvjSsLt4sxkYi6A8lwYjwSVMG5wwt6dRREyJ1V2HeXfsYT16+49wslocYSzNMZYFGWeuNiQX7BqtIRldguZvf8mkAWgecIFZONuaNR1wxvsbOh4o6HU7OATNnnXs0JsbDzzbyyol7+lxN4yyo9Ywz1NVWdmO42jpi0ODkDBU3xP4Sx2QUQ83deOTXx5CMg2hXtZz/gUErWv1ySE13QHHwZeiYaQYqPbXoNkDiaeGKACeNnpEyY/Q1P509p1XriY85VYWJe97YQoqs1CJFvHZWUNb4bAzwOESIutSX2+15UWI05pNMGxw8v6qEr5U3yHj5neE+2abXV7QoiLL8Mp6eg5/JVEZzvAaB8FxY3s95mZWc9HW4eNMJp02f9w4PbHziWHQasAdEWBoOsNwMT5bpmHAUQUISLcSX0ag2uFUjaOWUV5VVZpoAL9q/GmaQgKIQxLAmlbt7Aog03gqM4mldkydgOLIK0UbKHxpgGyAzGbesXlIw35G2H3LCL0uRbumANp0D5SCJJkMPCiIasXWKzQkhScqJjQqi1JaqljPlUVNMdLk6sGZTiDVjpgMbii7nEW1l0nPHh744dmbE21o49xTezh31WJ0Mzv3OlFKwMaIrJMEZyJlww1PhQNYNkyGo9KDPKx3wQ/vkwOS7+YfXhwuooGW0HjLDJF4cJpKhAwXBeFJxaMv98hZZoL3sfaC6KbeDW0TGBLAHzVhCd/MMccTpWxwNsiEPHtiBg6IgPEBU5ZhzvnNKwClrvwTBkLWcbDtcvm3jfqSOm39MwsHH/cELYtGX0SiF1lHF+aF6qnE5BrDgVozqT33pc/PBDaeDCHIoWwc/LoMWpgamo7MzRIRDDWoaJJaITUbsfBqMB3zTAGf2dMrZvbgr3e3TsubuGTPM7x5UPGSKPH3tnekh133ByGs+SCyqwCwAtclEBo3tAeR2rXZS2BOmRDn/mRn1VXf76XT/CXPGDixOcGVJhhloJi/Gne3pyZ4G4LEyHL506LL30cnsAyuMGCzVxmJaTZ9daDFGE4GlBeieKAYuTQPAWj0CaTHAomPE6JSJTzAvagJOUWS1mmdw4rGUpYldZVhMY4jNzVoJbJFXz06E9lggQCXQVNDyh6VAVzCoTBS6EK/ea8vTAgHgwSjewdLYVpY0Ht5aZTzr6xaImb4pjYxdmcyd4/mjj57JrNEBwYBP4e37uBMWBXTWM2zatGEEXdnxQncnHwbw1Fu+g4qC8N1Q/LN+73YzO+WT8jnhGJfUsYIzm1idnYNKe2p7wafd4sftvUN+ikHLYmc8ig4iplw5/5U2ethzMi4RKDrLT5wnTsF5NxIrivIObAOr9eJRo8K3cHD6O2dMk2OwmXujqTCARK3id2I5eRHtjDkFCkJe3G4DNIAV5ztHgL7Z6KyxzYk7VPlT8wliEfw6IJUYp4w3dZhB4lwW5i+cekdXKRZmnoUFVp9Bj9gwz4unWw5z98NhuCC8/0k3QKlw2P2wIBugeov7+Ygz0cd8VM/Fcl6Z4fwr2sGedw/0xzqj6amfWVs+0vVLgh2IDofcBZpWkJpEnLRwNpxrLDyHCjM2wZekv+wz+7Lj0I6hL4JTEIcp1NCGVXte0+v0OJimdBmVscBeysNf6pvWwWSDe/5oCVFsFZAEMgGF+WesqG9CZpQVi0BbxodB9ORAyUHrd53irkifP254cSZqWj3aEaPLNDfVDxmspsOJDOG1uTzTDmiCAjCbxLdbAGM2CRh8QJr1jjki3Rux29LYxMhKkkRRt6zTo8/oEl+kQCjMYjKkhD/HQzCAVYCwDjxjL1omJpzxOdxzoCtuAHMpbONP4dFQuyiaOOZ25jiNMzppFebWSMg/gQ1ne7IDvv9+9fQrtnj2T7dDNHi/ZybbsjI6pt75ZMpyt1v+afdpxXcsXNBjyKFM6DHARABCdCbUA/sxdB8M8z4+k+5+acaFUfSUz++N+Uz8ev14u1/ccboI+ul86Jq2gBiYs+VkXO0B7cHGwOQPFeKcKcrPvMycP+Z/6GupURTOlwHs2mzm8Awh3B3bO+68+djRMMbfQdLE8I0c4Pn6H/NjDAYowmrFe2S96dkV0GwIX4+208sLdoZQCvoSpoH4aAcz21dvRhdXE8pItY3G+DOKh16F5U82dKA349MJe45QBeapiGE0RNeEviB5Po5p143uj6/4iDJNCV75R1fPRurx4Jyv7nF4zehkP6UU9LK4itQqGky50Gywo1Rdasm6hwU+bO90MyzYDwCgjdMzctwjY+TQKiCRJrmMP0Kl4p0ac+jKE6mBEZM2WK4UM5964KObG1fROwxkhlhJQWiIaKAVDA5VUMbggay6LSTBhIhoVrqefL/O6QZmR07RPGacIeFysHsocDyHaTj27bQSTgv0i52bme+CwKZ1g5xGJ6tRlx6AflAJMtjFmCEl7C81waE4ENXZZ3jGRIEdCV6sjRaNYtUrG+VRHz9aBLcwy9k1ygSi7HvxVH4caNoO4w0qYzjfrz71lm/W8OyADCPOvAzlpGQDurST9R92dx/5GgCmkVzWKqOlHi+fQI0yQ6B8ZRSJ8Zfzmm6ypGUCfjJ7XCiikTqqunB+rccxsLH4Dm5QU9SV1VOaCKZRMUee1EdqhIUtDC5fI+VrBnzuCQbZlWeHACAI6cfcskHF0gSteWI0T1JEBe0GPLcHoXBGla4JQx2+RvH0RMMcn51CEeOLD8R4d3B+Bgf4+CBZPzxirnnR/fHjI/vXMEL0N5vZEsQUDIZRdYbYVACm4fTqFQwwAbVa3OMoHna88cPCNt0nFajSUxMKn1ry0mqzPsBBMxxFBpeoNlbRP5SJJiAgIy6bP2abmWmemc3FquCpwVuUP5IFdQa+tFQbgH4Ew1f0KqNrXRXkLxa2AQRAC1guLlZWRYIlEqAIF3E1+6vecMOHG5xmo7vAJGH/QZ2BOHyqo/BDCtrBX7jiTnXa8DRH9sicJAZaHryxckwnmEVEFiNIoyTWWCCAY/DJh5c7hxkzDHDBpxtx9HE/cCz9eBSkmZ+WfxQKX8jFf/aR2er5BS3m/3zfO0WM1AILpWgSDYFKQgGUMkzjTC5nDAAcsdlMGWozXKMDQjdcvkUOD/Sunfn1cjvym2PMvqF2KNyOtnHXXz+ytibX2hL1jCnT/Z4Nz5RBGXBSy2rHcdM4BcygW5MMLF0QoM/qLdkux46J0YZvg22Xc7qI1DYTMvf6UhOEwo5lxsccysm5PYyqGUFTjXDNZlAsPa4a06DMVDJmPuWzvJh3putxGilg9zBELs5lI1IaPHPUlM0ziJgHs5JxxkDFXmU8MTgn4/zxAW3lPR/GV4s532/fsmWN1Aw2dF7GF5f3K1c5UDo+k0atTHizEbbOuvRp+HJoALNKNa+DprLAN5yeOSHCzBE7Ppi8Go66WBO+4Dq+5K1S/T0HiTSt8syZ3hmeXv+Dx6fveosf8ZuZQ1STFKRthcqml0NTGaJYWdo+ZAlZ/AB+VGcCZSvQTpBqjY30RrGziRPrbbwHt6OWaSUgr2Zhy9Uio7FCgS291g5FxAZjZoI143pgYu8x5W6GckgLFSIzx5fsWEHMlmyAEaVgPgvhAanIT5h/5KNFNFYn7+kq9aCqm6MsjrVHzJvge5+x2wSPgE9b7+h8mIbi4BMGm+jlOiMqyOMkUUA9pmqefCnm0Dnb999wpD5mm1rnzyLiGWMXcRVwiGMm2LCuM8oBEOBX+9N/sKLH1+2W+rscW/vpsLvvLE87i+HyHuGt7XwPC00qDHIyKKuhTGrjk+j92tswzMVmUVDM/HpJGLTIFZXTy6DsmfU7O5/i/POERmcHBPsmerxMgKfvG859NilMdPR1oxmi0TjR6LhXbnNmApQG8IRxRfZQZvhIg1blIevG2BFeMgVmfp7qx6/DiGcYgOSxLkgdRw73l+YBET/djYjx0hCCeoGPzzwdJtbX9rvrGRbGfeSoLJ79mC2AUz6LrZnGweQV/9WP79m47belGNDv5nwkiQOC+T4Gsz2MirEZdlziwzmd9kaXnIMICZQExYOT9JQoHL0vjfnCr4YsZ1hYeltXzxkf2wNTPNZraAPac/QILcLvANEQLxsCYrOLw7VmeoYexwF+5M0dNKMeb1Cg3uRXPaOEnr7KRjVUl2aWNgZ6jLXagWjkEFD32Gcj9CVuK64LaN2wZRpDFFe46JBSQCdpMQ/J5XKuesa2E1o5bUHjDhlsjjtH7c7qSCpPoMdH5fhZa2XPLJutGgL41LxExBw+bg/VC+O8v8UaXJf98A5G4RKcWFbaA60MCdBmmWRC+893h0tmI/F8aRgIH2mUJbNI1AOrwuiwH11kJZj+gJ1CNCTavb4yBgmzyESlb39joPBqBmyycHpRktCGOdb/V5wVRQ9NTVEl6js1Cb980QgibCPgYhc0n4+h/dHo8Z/7+Oq8WTNlVwIGrMestzNJrB7yYWrUG+OK/uKTqlts4KHds8PZLTQunvZGO04S9ew3ZpKWvJrDsUOEfVMHH9SSWlWyp7XSBIORPzTZBk8CnrOqg9rYAtB8tZDhlqeqEkcsXJ0w1U8PMD2/QcLEU2a6Di6MyPB0uryd8QE+JpExdev5A/zTzaArZBydYu9ZnNj1eA/vMIZ/2jMD4tHZJQ2MxoLHz0cC0EY9FAwDF7LTdvJDCRjr6ASwGZRzWTizEnPCm/96X3SGChTrxxLDEs+SrwfanTLY4GvK9BQDPUW4JRI9QHVBzRyXDhTNn1dDeP/YBmAjIZX0viw7s2OxMQqQJmxObTT/ZI9E1Zh2eMLok5lxWqLNCdeIsS/SRbS0ShwZen+NB39s+EHKDnodqBhFAZnxQD9ommkd3Jjn9FPodjEOQtxFwfw3rYJ2K8d6N3QXWWRLumXmRFomBNAO2hV7kpEagSxt2HghgxsKTfoVDtmmJVB1HPIzpVgAowZUv52UniRxZETbna6kK6OC0ykQzU4w+oM95hw2fE2E9YK5/QPy5aJzQspxe1QQNT9bZfTAQKGTIn70BSheEKc3w4zjaDPNQz9xcPkWX5mBLyXde7oic0X42iNqFDPvS5N7/g8mCMB5zjWn2PI+LoMZtZnnzFNh/meOalCazpb+M7se6E9QLCoZ7x1byxsRenHIibEJfQHMZUIJO6O249VQ5aul70XgnVMfeFZsKSV19fSEhT29eoN8cEvwv0DMl48Q4HaFBnO0NT4km6iZ3JlNz0/v0aXdZjSZLNnugjxtMXiM6P8CQZ9fveE4aHVKmSg+qKseaEYUNFrGJAo2xBfbWFTrzZd8CormdGC92IlC3Bx4wFDajnSOHPchYy36oLO/YE3W0qDH9Jb6SLy9yszknDlUZxpPaACZEqEW1RF8KHLSqN2kg9E1I4bMPhNdQZUQqLVsbTLLyjuDjFoxCKgsWol5tBdgegqz7acM0UadHTwxXntAvUrXKS1zeagdFd4frvgiFOqixq+HbBADH22Mb2nRdwDpyWtsDNaKosvgRDnjz+gC2j/AFWYtg2kUF5IWnWc4R7ulRcOguM4aUxkYEOzEtntmwyMyoocEC7C11R295zwc242fQacfPOmPh0x3MJLEq2GhhmLi2qA+1DrCR3twPagzxnipP/0KWbMzpWVzWvoGW8V3A2zQkuPFWucKl2vemfE8Oc6QRsPmvLO13XMuMl0ZVeJOUGdVB3DpIhR1QR2yLxqizP9Y3wyc6ORpwNRw7etk4zRqivxQDLxLBlDUqhVg3bkJAtkye8DLuK54YOM959BlVZoalYZKEAX7XeZAKeCQeSGeWAbhpLAV00/wPVzc309OLzBR6AQujZsp8mVY3XqUIws/zFktHu2bp1eXnA3P1A3zUdRIRL7C/WERy5nWBbviJnS0sOJMvx6Bqqaesbyt4rgUhVipYObIlB6KtKwJtd3JkAO5MG74w66F4iJqZugqHe6hHntWRlB9K0WLrFNPc6V6mN9+7O4eAO2ccBI967LMQGIXFRY7VDucSAmYXhKEzZnBBRsDOUyPD0i4aW3H7jLHx4z2qZgoHCIkEzSgrcdlfdErNH/A0Gx3XTQOM4+G0FH4jcb9etkd8bVvSsl2uiFr5jg5GjzeRmU4m8oFhRVO0yRE+7Ap6VlB1m2VartiooS2cpZM9QVZGYGQ8FQq5tC+CC+CgmkQPF0fcdrnIR/9BCfz3DJJO8E14mDEBepJ9ddcXs4+oIfXtjkhhPcKyRM+KuPqDxbCDdNwTUXQa6JDVqm9i4OYleN2tGqxdqRIdib/KA8eviPg+ZodbKRixThA8ZLv+XF+NDJ2bbHLK+fM4vTYMcosiie7U8801QUb0ygJ8ydOm+jd00E6nkGJvVMo+igmRmkGNAh9JsrLFA8mglkiD4PAreqPpiRBjpMasAV0DgDDOeaedjXiu5H2y7qrULHhbNZ8wujpCTf+aXpxgfvkURHgZ0Meu9lysieVzDqvY/ezSzuofFuA9xncr4G1RFgnO4YVDJFoo/Sz7LbNep9TW4BZ0xaS2WS7Yb7+RJuwy7eu+R26ALObb+9nLBVhZWluvIV1YGOJckARcKKys6y7Zb3NgVfaky3IwmMqtPBDNvb2OuwLxm658ZH/vBqCkY7RhR6mzB6TTlyjjnK6mw2NX3Z3fCrCVQzM3wnVxygbXnH6WB3AdLtCQk2jzDLjdl9Uk57YAQDKQC/mMZqoiPMjuGIkdXFKuk973k7cT/HbWMzq7CddlvoYqJCoLRUbHandIJkxymoOhzLYrlVxxGMLB4y2ARSoGRIpO/oyWjNSYVGf8Tba4Ce7eAGSVoTdYm8ozVH9t6miGu47mNm9MoXCvgxlRrXwFgez3U6HQ8ueD1djteH7SHTnVCoAMMG8y2K5OT0jEsGB1KNDtCeaErqIPS9AskZBNTN2wOFBaal4dkMgDVdlQYjFG3BeC3v0mCNnukl1p87ZQob7g1ajTIA7CGXPtupCXWxdF0WNHNDrtvG/en+XBWJW8y487wPgR7DGhJdF887MDztxtZTMMLIvn4JhePiquzsdbNhENss1tC8cr5Eulb05DtSYeSEjeWuG4nd4eeBxyMe+//9E/VezJGuyJualzoxInUuV2Kr1zJkBBhiQBEGARmXGS97yjv+Ov4G3pNFgRiMJm5lzcFSf7t6qdlUtlTpUSj4e1SSrd++9KldmZMT3+efi9dfdk5TKqLIdn06LJf0srJsIOPoN0w5XrO+wJ3GXJmjIDAg5/EPmIM0HXCN2+stxClMrGR5+GeeaVqaoInCk/G00myBnwvuU7wJsXXozkX3KqkfURcItv3fTsJ7NkZYOI3UB9YoOKSjYE6XZHLRivZG1kH8pZAeD5Mb5rM0gV95vAc+iJoeAO+CwOLC1RiaMjUrYgNngDqX8+i22BmwdVtV7g/tACFmCiBlCFDwHeLwyI8tJCt/Aq55EyrqRXZvb83VPkqVH24ep4YHuihMV9jqqRSMnGH+zgrYojJQAkFJ1iXCL+ehEMhR7RC3e7+bobTeLtMldQ1tL6cOYqcCdjaMd/nPEhhY0bIjFrXXb/pSv5E2dgto4OOFUJgGLA0DHfRFeZSJh4Sj8CMBIagTxgi4XoYroK8tO5buy+XBUiG5R5JFM11pNJ6Leru6mxgjQ7g63zxPC+reBg3uJQCBRmtJSMdTBN5Ddl8pmp8XXcdjKPWkLXcV5CImnuSMbGQ9jIdwePe1tgj8HP9R9hASWMTQuM0PZhRsUP8ddxbbHBts20h94keuGZFgWCxnuBKmTyQpFHFeQDRhf8s1mt1z2R2EhB6ORSud8vx7O33Kt9L04pSlhpv7LLEsTdG7axALK9AjDHAaBYy/M5RelTlHSNTEmMAf0sE6UBNk4IQYGcSiiFxCAO4UymbzQbuXN1v5EU8JzMREUCJjAZHYYnC0sGE1pcYiifYrHcqk4Nh45/ql9F+hYWGhBqxUQPKhF8d54YF9COVuUWEd+lUUJ/EFqwuUin2t1+9S80Nr31N5/Z6ytLHN5OfYCA/TGeHPcRyxW7aF7oQ4vOfql+ezc0FiJIAtYmM2lsTqBgB2zgcCh3cz5bQIZ8bLxWQ6x56+vGIBO3JhtiHsVBoBrafkTeQbBx7Gm2DhBnBbnhXVuXLdCFW1iZctoSNXnfc1bxb6jS2N6vgwpPd5CPKtDz8EPF8IV2N3Q+bI0zkJ8X5DDkG1gD1We6X0pWdMeagpXVhHrk1h9FDtNHXYzQ6AhghA1tbkCXMBUG5ASX+DBqCAd3xrDyNf6jM/5j/iXpITN1msk4vjmQdDvIUi7D0vyRj1+hOR15wlHLgajO5RHZ5Up0UAhUCHkF7IiQFeLaoH4eDGdXbThRUBqbL+vDGFwxvQBYBvqG68v7SjFhsSXckK5KBHz1lIYYbe7Fw90U9wnpzPOgCMBH+M4K5c+SZUgJfQ17a2EB2Kkcvf52ry3/XVBTymdZrmsc6R12UlBj6SYfL0FCh4HxR6xWB3kRTVFlP9bGq/7YkJK8cTXegCyiWly7A0PzYfWcdwtpYECJpRqV4cNYhfZE4Fwgb/IQfgZFsv+hQNMndXBY2BGYU1qveML45jEWtR3Euvkf1/OQjwv2aslOQICH/P0bJbvofqFrPwUhRO8bcMMbbmFjmPGMY11cnjC5ooRcJT4JLqAmOcDxHbyiHiskLQ754eicx1nl3Ho7E/tYQTH4krq3JK5i9CTXqjh+lDwcV88F/l0x1W+FiLJOeZa8aJ0RWCywg7CKKSiAj7syI9BHjAlSU9bEl0qnQKo79J2WrLY0uhhYKksVngUlISLUFgC1aP4LN4TFoeeJ+40sDcG4hqxsI5omA6+FWE3CAHxY526EqJaQU5fhCvhTNtmZ4lv55BQCsGQDBsTd+n0xYDK6IzS5jnE3ntwT0oTDsJVoBUDDTzwthPb57nCZcu3sTOxnd4Q+xYK7K+ur+NFoOO0hCidKBl6IH5UQeBZ7GxIBgFhbOmkWhy/2CQ746oOTGgEkhKyaMAMJpWv5dO2RrcPTkVpmIWBN1nhuvyOslD8PvSF1W45ufnGNLBYrss52+xa0yEuYgC1QYONK7NMcQbEPQ5GbEd4CrXohaDay3gWf2QkQdLWTlIm/6XX2aNEXJNjc9g4JKei4ut2Tz2JnUlAgXFG6MTC0oQWjbaQXBtejSRttAvwLHadK2dR4ttC0mMLgKn+Y9MYjjC7fkPVc5kcSC/VVlDQVBtYz+ckkP9YSfft9mkgRVLq1OCPAWx6GEaInEf/DEly5gxnttHcNNqra++iUx5xCFApvK2w06GWiDVZ8qBtHSUibucC4PsGFsztd1tnVyAHdVY3HN5QRKejqXFN7eML+abYxNOAJlNLxR65oLAG5hWGzLbFWef3+OL4bl8WhsU3fvn3SZkLGrxPym+VNlf2KvbDMaFYRbJ0b2TEQm3ViBjJsEbByPPHV0UfKE4RiI37MUD/gnNT783LfNLbZ+EEh7RHqMui4a9EHUx9mEMbxShIue9WU9PQodKAXl2hVisfeVkPwH+oU7M+EdaASDIB4QGidvtw1Jx5bmdAFIEbxpRZ/2gtWO+0pw494tmJVuxtRHcR1FpPXyKjy2RFMBRqKaTTiy4g+8ZjcVmnvSoyt6iLC2pSMpp2ZQlXL8Snb8KX41LsHXs202FghLLPT7Pbr6G3vCOFBNanyPNxOqYBgmTKR+G1NJr6RA5SKUVbFH5InMm4xTjPtt8teUZ222OxaRwqwbOQi37j8VjGU8cXXjc6wyM1imeCIUYQD7ipoR+9g7Hj5IT0V6Q0rA21jYsu8/iFO0RoYJ0CAD48a6siVTeftugwNpbshYawWtQECxlulDtzOEL2rVTIFRQ5PAdRRnltV67hhUCUPAHRASiY1k3xF83mutHbXfuGp1v6OqChPxmR2BvWi1zH7sTRVEBcA/USFByDCxMS14r0JgGWNiZH3NRA8bhIclantlpelBPrFhH+ETldh7coZQ2cgxg4LIQ5nGU/eHCaUAjmrEYOiBIINefYu6Br+K44mtYlHH/r4EPEKlxaj+4djo6rsP51HAwVHVAzIWB0Cs/SL8kNo3/MA9+U6AVxQhHzTGmB9J0bv6SpqMs96XZFKwknon7KN5Nnj5VlfAkcCTIR3xumRv7BEGks/sgWfSFZ2RmqVMmldwEoHaBju6/d+MADuE2L7HNxt+4s0rrxXCFU4ZvxXiIe0KDN03tsgudAxsO7D3IWSqH+QLhS7lTeQzbQIYxZaY6BGb+8cw/JXWkKfk3RdOrBAvuCeIZi6nUECR3jANy/Va4x5WK/Vf9Va05al8Xl21g02xKeBZ3gy3yrHXKTRsH7i5fbDHn026s5EeKBw7bRFi7b1tATytC3rUFx6VJdthEAXDF0pyOUgwKUG+WOQA+cY5LgIlwC1wwmeuwt9VBR2HExsUQD3ETTgQhC9rk4cerozfhALELcToSZVuevy2mVYKzCWyln/dT48UUoPd1iv4iiYLTVhVrvmq31tV0EZdU6h7a3GWSMhrXtlkFfNI9Ih3Fg7bl7dAUJBg5DJNUsjlgwnCDOa7fOUYk2vAVl8dznMB/qc+NChmfqs63NRHgLgg3C3XX2OA8QroplFFQL9bSyYVX5OXw2yxyPY/sBleovEHkRlDunDvY5BeTSxzRB8/MmYW5gncBA5wKaoltWTQeIyDfiRk4/+Q2OgNyseCwWW4lXMDXY4xoaMhwSnMplUiYS2i4OFfkImxpmhHRIelcno1NGw6j6FWirMQ/xIJgF4ONsCCkFY1tsiXqbcMgtI8cr4J+SRmjLK4HVw+BF8OpPLVJsMJUF9gljVtvDOOAuUe+uoxvZXA/M5NG1vA6yx0J7Q9gOBqN+nCBCOf1EQg5+NDHlxl9lvhLzjA6v3SH3EjhTtuGng4G79Yf9hBqh5dUEuyNqdNxAXI/uSMMiBR0zsIRQCUT/i3ViRi/y8ZRX+CNec1RAcyB69o12omKi31y3dWP8VNXOyb8cp2MY+G0URmgkNvYM1kEcG1ERkWYT4qfIYh5R3Rnw8M3di9MBEaBwrTO3nfpQOxoOhJX+ogMY2Dg7oSZiTa1b+PVekSPq3DBYzSQ7tncV+FRQIjggXmAUvC5D2BWPAmXi3RziuGJsu2uQT+LSaQ6533R6nEhHkP2wCS7uCiADQq6HjOSDs0L9hXaNgAJq6P0HU5zpeWJgSfjmsFjPlAseuJZyDugFOyfGFCovRZRiN68mHvVUC8VGRszkwF01Aum5nmAx8AdbE/YjCKp9QVaoJccK9KaOS2sS1fI1pqattCXzGlaqqiZ1j+HkhNiRJ4gTNeM3zm2wJ9kBT+yQRIhcAMKjm1o/iDEaLcRZYK08QTA/av6WL2r0rWLgVeFKhQTUN8IUHJwL/pM+bVRyBAJ1//CjQ62tEYSFCIfGijgrvA0/kyR6FFeQRSeTbjM2wArEMrCncWLjGEfSLRYkZCVGJkcjLfUutdYKQ1r/1Zro9B8UIz6MC6eLm3Sy2K6WpKtjPAZxsszWTWTY0/brSXzPiMZSB11030sVM4gdgrEiY9Bqj77w9sLgkeo+ssQwYl83RGTiW0kbnVyKM2tmDYsL/A2XhpPgogKmqmrJDoNbr9F30VQxzvUZ91q+xsqGikJWCHfAkHN/sQzR3wmAFFAJxRqOvPsRIYQpoevtsmeI5fUYoSO/7BJNGfJCoiwyC+adQqeUo9VpDigYcM7aDWGGAL69iVJUoRMxWX3ZsK3UjkuEHXFVl/A9kUaPR2UNRLERZUYOLPj4upU797jK3E8qlJcsJAyfUakPn5v6CHsNqvWQTJ2MMrMHroowx50yCmLSjpz73v3GSUCc798rHPHMYfzpP89oNZyoUktE2hhJJaqvAjyJZ/YF9f+5VkTdgeQn+uGcDt11GAOAZojp5YTpaVVo4gB/4o9gsU01Q+jqaOIKPmI7iDgSqK/1MQfEUtTLGXaW2vFdfQIWndctTK00gnzPENlu2ixUhiOixaEDFrO06ImArMIqQQK9DWPBVeK24xltUBjdsN5gkBC5+Aoy5dz4xRfZ+qLUAqtxspHwwouhvtm/HO3C3diqfLeOIxQ3FmwlsH25WyEseV70BOWKHP7s9dUUjJaZaFFRg/6g74YI/sVo7GQ8xk3ytTE7PLxpjVd4s6mECp0ZrWjqMwoDqgsbsIhixCSZ8NQhLfKS20/d/LmtJsECWzxLR6FLrTQP2oFZK2JKjFjwEGWxXLShEVicQZThbSn/oFrkl1gAEbFqaRJEelj0cD2sT4A81o5lCN0e/0Q9nP105XpFLar3BEYvBg1FEo4Mv7UirrwokWobykji163j64lbbt1kfqKfB9Uumewq/vjO2rck/67oV44p/yzOhtt0xOya7XPywFzt6NhPGbrP2K4EMONM0YCNnoQ0iFiPDprR4WBaeFAxrwLGokA25ISrexo2DzxFMfSu0czDB0zh6raw/sejwTb2NpRPI0zwNzsXMh+/jgDAfyJiDw0SdtuiQfbtSprG4K0sQH05o/BSGAQZH3cfwhdiHSwDHqUrEUKJmEvuDARbwqGn+33cIHjXhLhScXbTt0UqgPxRZiVVYFEInbv1/FGF6M1xGxSHAx9gRuhMx88xszWWW6gAkmf4Xc+dfHkQ8h2OBjlo4T5xnnwgqJO1K8IYhIS5JaVnEb6PbwKhkWlmNoGVdVuUMtu5hdAXtj5BAVAJ2WeXIjndvW5fnnvdxHFyHcdPSMCg75efW/fvRsO38sEvHz9y+W2G/RU4kCb+T2We3umkvamlo2ul82xr/YDWLtafDmOL1RA7hBHBXrSAhKJH8xxrGdssESbIiTAqFoFuigUKb0pSNTKJERhwMVCV9N/GTxNCSKrEXta+BZ0RfB3nEUWsp5sAb95COTb8lVBQTYgt7oO5fkTMTZo4KPF+ivjdmraDhangqHPYPBJxZs+Z5tqqaKnPMY170h7wJPALgYgH8p8wK34TjYJopdDTQsdMDXU4oycJah5DJfStZQgUQTSREyDemc+ervMemp0Hs5J8k+7hctz7uVb3rBEPpjrxwqjJSO7F/5DfT2gWknZSYapFLSRtwb47aqGDnfB4Gv8TMknLWior6GSRdaoerSjOAOclTuWXIxvNEiLH5WtaOF6egqpEWEjSUJ3iv7DDKAxFORwNrTG31LI4MEEq9j00gVUbtY0tcyjB4p4gdi9SGJaJVxZZNpqndvJiY/tpDE+nlIETjFAcBDkfuEH0EBCWeEvQs+KgcpCN84j+BGhWmKfhiXFQY/HDN4uzwCv3vKSNzg3fht2uskOxF7APb94E1SU0Yyi3cJDAA5am1xUeZC9LO9i8hScnelUUSmTgdJ3B6uNjOpq4H4wpivlQlaPJrNxvt08fktHs5uGrp59+2G9WgKy64idiBrfhQLgYCxq+H9ZqrDIrblOgfxF18Gg9nTNQO72hNoIdEV489AdzKwppYgRxxDV17owaJFWBCriMRb3OO815kD2lAA9dUzwxhcsysiOKSVr9vNHdh68tRAaZBNLHorsL3003OBaMQaj/mBUabtKsd7npntPI0oaG9n8w/F5SryofUadlBprG4s7GDqxLhOnTQ7IlLotH5MTYHJJHgzlPpJEYuoancHUGEwLVP/NfQr9RXq7eb57SyzG8CmldbWjAhipZUCRjyxKMvxr51lTusgMcMW5KoSOMbd7USFlBaYb9CrvGhDtdnc6wJ2vXn6SDqWj4FEfdeeB0xzEjU8QZ7O8O4/hF3iTiNMEhYycAs/Q8R1xeZpLq2u/dl5XsAm28y1HSAtbB0AkEJkAlu54X7XmxL6KLuVjH0WROtb1P0qosQSJ2i8tVyyXxdzN23BkJu9/WnpANp6gdgeJIDEJzeZhwEcl5eJA+EZHlKfoPkSU6IfqE1pkKH6NF4zwEw4eCcq7FAITDh+NI80lVjHmAeOF0GIwWCn+ZBqVkdUWBuCtOGrl3D1UmLWAg9rGbdLVrD0hC8rks9KFzH+iru/XreLFA8RObR2kzog/AtMy3Lx8X7//w9rvffPjj3wbBqaAsquG06qXSeS4p+V3jUY29ktY4c+I8MbG8BAV0BimTee5LAAzX0btT9nVzt5J/1bOF3qg1Upg5bmkolvCAQqHC613pvnt+Y/YzjP96EI4oxjvnRZGpEw0Yqxz0N83+NjIHwB+xJnog3ewr9WwnXXjMzLnjxRHhWfNHW1rfTppV4mqcZyaCpiOJ3Iz2UAu8AAxgEW2OaGsYdBuNW1vSFa0Sq7Z52dl6ukfP9PDEc5Jt5R2I2DQ8B2IvvjlUds+W2X1L4/cyRm07G3vagimMTucJJwLkizjeTmjpQDsbx13VXOmxqDLTw0eIr1mVTwT3EkAKqNN/S7JYsdZ4fHM7eSOGo2Zqmx5iHefQASFqQlv/hJ4nP5Y0CvS4zXSmY4B/Q8NR9rEmHKXwIiIwcHP+oMSFVEWvT8SyaApBrlyRwCIVU6Vhy5PBfsMRQ8knkJ3Nq/g8WED0nRMSci/S9eDBOHJLMX8mpD9wWI9Swwjha4bxCYn4q8Foikw06CSRCEbC7fBM6jA6+F1f7C5BFsnKgof2AZEDJqPZqSfstydx66OZeFohAdfeLgW+TDEia0QcGxmMCOarmpSGhufEZwWgSVmQLmHJZDq5XexXazYhHQ3RNDqDmErvCQbGwMjK7tfzu7ePH/68f3rhsTPfCNVWkMEJVyIUQiyHP84OhdLqSdLF4bDtDrBDKwFCLzQmb7MnJH+uYYtN5xSgeNVQDE12zZ1oGit0J8/mNGo23rS7s6icourkkUr/CApYZ3zyXbO/bvTXl47CPl8kjUwavAszzDAnFfbuqTxnDkCoX3fXaSXNy+h6BGb1yqLF0WXrVXGmYk/RyqR/HpjKECvaHBx11Yw6Wb5hQ1KwlWDMXQ+jDjrsBTlnEwtatg8Z+hClqC+eo265v2R9o5iRmgt0qK6Pb9bcj1AB0cDWpSgc2DWsttUyWXlkZ2qcOXSVhNjB366trW4siEeoMK1zcdS4N9GZRvNwDbg1w0oGE83DjCpysyTLTkAz/EDh2QCnLTBq/4q5E14US0VEZEvizZxXdLSo6rK0Cmfl53DXwt9gsqBBiACOgyiaKo4KMofnFI8ZyqE6Asz7Q174RTrJLV07+XbD7Q6j7VskC0iC00Wb0PeRvNUJqA5C9GKg3CI8kMRGI7VnHQAbK8sachCYRo4cJyECKhLMnAceFXfl93FoOvr+cu2EbeHyOmksG15adNcNJlz4S1F04cqOJ4NHTbmc48BuMBWuSEYk0/nwPm8LvTm8sbKwSioAdPys8nw4TxXLg+rsp6U7KhAbjLLtY2cw0ay31VoyPfWxjIHbriKqjnmrbth1lCBHjyHKgTsXGle5jEfzbXGMiXtv2Bz/Kn/8H+iCPf6YoW/hQFD5l921pdKGxADTbRh9oUlzAmHC2CFKElzBJSOTFsLMq27R0+BRW3nAUcUbDqzHDXOpuN8BZwRg+uXcOwA8LjFB0js68byd6/YQboZYEkYjrOJVSEKEFhxHr31eQ2DQ3M5I2DpJQSWxonErsUXnrAkDDDpeH6RtlGzkOK1r5Hf5L0ID9+NYoXdx7WDe5J42lTQh2h1Wot6rRr4hhbr2JK3BTV/FARKxyRVfYndt8juaVlEugeIwBxh5XUTM2Wx4OxvdG6wSfoiFIyxOGxPQ7jkGth65kexjP9gQ+ptiDazVEoRjGdVe3hO/FF2pYDRkD+8zJIbZCOXtQsyCZxMg1mIde+M0mXe62+UulGX5eDYpc2g1qW8rMHc8UNOLTO9g3rB/kUDfFj5XCC4PhAeqap5pdy5xIC37AawUqt13MUR0Z1g6gCD/O7I5Yc3Cp4qsbkB08YARF7pG+NA4sE4DyfNNfMMwMpaCWiZoqE7ZLtusPZ7fRW4BzWyErN8BxelTBKAOryuEDgWjKPY5gOu0zvrJcXL7NjwpQyyTYbF+TWZjbyz2G0iS7j8kMplMRtOFSMDdui0CEoGJ7BdmGSOI3ailB5Hhi1Kf4cqEJhSo1IbUkyl27aRv/vP9x789rP5R2nnJyZC+ChZHaxcoZt0UycOSMidbUXisSrgSzKEHD2NCH0fdLBeleYOE1u3o78EoRnTFnQrPKdS0wx3VCfVO4ObQYOE4auOu4z2nkeZ0o5YBRMEPG+hsvjnE0gxHYqVwXX1vKE/5NCxerBtSSxKpFwY/bsORGZmDc4UAyOXNNUYV6ZCdM5J3HWGA3Pud86LbmEjrNxOHK/IykTeWkHJO0MIKbq/y7H5/jjp5320fBhdFxCEWIgixTJAnLCONCX2ifPN5+v5m/GY6vrdAlWYZlohGCFpL3FTYfedBhoH7qY1lVUbEGWrQFkAZPEQ4IrbEv+0bHJSIaEcbnebE0yLHA3MpBULY4uy5Jvm0qPIRyWS43aokJK+XSP5gB2wz3lFkOHyvaCfpRTIBChGdQuJwsgZf7FK9hTZOyYdo1jJZWvIZhtx4SpLozQ75saC0TEbCJgxOAHtI+P2Se+c+Q9+HVEXJmIfop7MIop0Sz+xA84oddQfLkcqxaOgdwLy+h7RDuAwWJ13MCQEjKeNLWp1bvieFHQHlsbKQjjSpdn5Gs/td9Tl/WSa6QGMo+ILqgA1WZJvR/HazXDloDD7N5n4kyLizVslD5asduZnezkVQluVQbsXSPVkCZi30sI0nPyjHt43ODYKUFdhLELW6/m9MIWa/lmAUQ+wH0+haEIOomoDPBwc7aPGB6PDAr73jQQXv+35j2tHQ3yvHzaklcc+7AoBHMij6FjsPdLvqyI5V2wXoznur+UDhnbPuViQMR6e543izbS2pUXsX7rZTDrJxQB0HhuXMuYQvYg/49v48TW5A4TQljlp3ynYQRjtE1DpMwLmcanCizPIC9NSBKABS6FVsln+cuWBp8O7Lw37XwL+cLqT9xpzsKK10HPWD4DkESuDkUF8kWkf0hLnoyVMSjHpBQ+/Za9tKb0VNqqKnQIUOYhZ6iHBGG6nY7fCAPWzoFiAjj0kj12SI+C5KjuPDc3KAPEC460DiOiUceGio7eAWaW8xnE+y1bY36HxJM3TSpFIaQ+itNxvL5OsNEQpd2rKlbngAYPKQ9b6T/NAbNoRSjwNWM8PMNy2ceJsQoZSV2e/K2Hh9eDg/lB25D10h9O/rEBEMkiaAXGoZmSLhnLgzxGVlKyY8ynvo/wN3786HHHfbSpFJUXkwB0OwixrFgpD8g3HYKA855oWKRxiJL2ccWDyIYbPKVYqNDIE8FBnsfzi7hVdTu6+Pj+7FEU3n09dPL/YAojG6vWNFXTJfr+kPsaAjocNcp7v1ADJIVW6aWEBHUW5/HVJnfk4Wvzpkn5JW9abVyZHvEDCwx7wtuA+1FqMswv4RgOAAhVvr0MtddDuFnKwqN7FN48ABnXe7N0lTQQM85N5Q9Otldbj8cD7uhbQsvMQjE2M1A0PRf4SDHZaZuYDDBLZv5f05YeNngmaY1Vl+n9S19EY4FcHs7OvAwRX3L3pZCBXxF+GsJd3BZ0NkD9oxgtQBBdDy7Cl54bP7K2Q3kCHqrQ+zbs3iW9jiSMramfx4+FwYktuabk53Y+RYNb4arqkvZ22sAM3lW7UZEAJTZTyfXrQbIBNhKVzOlpCP8A2YF28PuJXQq8wIsZB4odUA9fQ6qaVFBWUSUkAXNGNOiLWQcS2DK+GiZCaENIIyeiiUFtlxKhAzwiaFIA6wAEOyyEaIM4xNAreuFuBASnvR7qyA7yS6mTwxuk349NYm1IyT7yMRJAdlhkkIVefkcJzCU6OPHTxJl5D+uLiT6WRZYacojrlF4djgXfex2ej7DPTp8eyJ8wAC6g8nPCIDiXmjPu4LWEJfIb0Vhp3obF/d5nA8caP75v71Q5bvq6FcOL2QE/jcOTa71VRjD8PTJoC71Wo0d+VxOuwbrhY7IiOu2F9QphtWmsTRDVsqt9YeDIcemeEMKN7yQa9Kc5vlFwd9dyWjCmMSsz/8vipWveOzAnhrbc/qKCkqGN2AnaavrBxgYCNrH13JvEovxTZJV+WBbl8GB1ofNx8JlNA4Dy1yYrNoXuf1n51m3mZcMIJzqx42SeKlFkZsgwY5SsivNXWrrKQOCDZpV56HEa9cj3u5W/8J8BKmjzuJ08IvYIcZokAXLlgtQAwFzL6l3TvAoYJNLg+g9KCrPwJuRwk6rSVK2IvW4dtUCYE6PUio2qCW5Nd8eTw9r6p1mf12ehwnZ8wF68J7dtPhSljI1nz6dtgdW1omjNyHWiU+Aa8o8vKm2ozFGfNKoRzsSK3wByBgYPfIaEaY7EBFxobAyVVBAAIZ1itKs3t1HvE/f9WQnzIilraZKDMddtyX8VAqZY3iMzET3jzPnrav3xBaXVq3jjQcILcWvpg/8SIQxurHfVseUXPYwvi7IyFvEKUX3uYjziTKNiMaXptMM5fdKlE91EkNMfWFubF5fNLYA7rbX/lFlIJ/B2hMYr94e2gUrJIc1U76ds/NwnEI1zCI/gk2G5kQN4xnY60AToeVfReaOxI0dzzXpZFv9zp5BUdDsj7aOu5o7dFskW2XVlqy/FjSqSI8R6J0i+7fCQnvzvEacJkwc5irVMEnxRS37R3h/lDQNkPyoN+bvi9el8HvC/ovWWd8lDsFSTBSxBH7XUhw6RHpOj6F04Sp6TBcI+fQHpj2EMiPt7Sr+rOdPjwHBQyCTwMV/eOPR/Ti0GWRjQANKVQkzdaI9rHVZN9uaNfuDuhB3ixpCx71eB8+jwroJlQOdabRyQTInJJ2U/vsJHKHaNeGa2VM0YEfFaZLHZk321dPArmpDQbE6tzYkw63CaflD8E+PEf7mrLksSBV95pxGyhiKnt3bJWX6Zg3mgsTaSx4oC8j3eSXFnx3+xszdsg9tW3rv6j/EDTODw3vO0QoNHYYATLoobxQGwOyr7Qq7k/Ubbn9qZvjSwxF9yjXD48YCwTEyQEl+oSOFIbRCE9E/OEHLjLhjHnA/Ac76Cq+qXbAwrmiRfhWHtX6ODWOkLMrTQpSAJ6STx9kP0MtOng+55MO2pc/qrakjZHiERTqjYknDA4KsxM6ID5p40JOawqx4nbFWf1hJw1nyf9ikYIJEsAl01dsl5oiqn33pECe3enZUwuae6YOG/mqBCbL7MlsPso29CljGL6LdYpbO12kxqm9IQoQJDGTJ43bHc4WBEaQgSW+XW3dOJ+EAexRKsDAZMhPI7cBCUauJpIIEA7RUmQcPUBEqFxJJyGMaq8/2obtzV4DT6CC7ENbBBgmNHwJDjhwfFAiX511R9pfqygA5vrwdiGAvGJ2K3UYjllHDWumiKVq3MLsrHJEp9+A4BqHPwmHo5uJbmvFoJ2n0cNBnNHlabHHognyT35BC9R3rUn1NM4zCQfKTVyAgxHNP+gzDsCQQeywAHJBLhJ7ri5ih7zS5avbLiEZKakZXRdy6vvbAmtIzBdAjSYJUxHxB0tvFlxDrDHlctV+fnaSvVX/7hYdSFLpTChRTJJI47M07s9rsdMWMmTE7deLFoxS0kIAkC1ofe2fxCoRaVlleugAVA/SQHy3Am7ITygkzndejfseLpQ0Ycv2gRaL6ELHY4r4Loox5JPn4dHCgyRovB+ywF3xpOGpu7QjJZ4mvtyb2l8JzR+nlEqxdmHJBiDWEEUOjAuzfnH8gm4UATfV6yvCnyPeEFh5eCaCNxfK3xf6YrdLdg7GuDvG9GE4Q93+iGvORPhf+LCe+VC5stUwyc+7AbjZLvcAwfRu6e1ll8CswMPUTTiRr7+8kMPR1Iz4fUAesaxhVD11RuUBr/lVo+n4dr5fr7PNxgp6FqhMmKXahUMBIhkm4cRyCe+NbQhaMOEeWr3uYCRpggExGKTimoCeaCnHSzwFDR/Ort3p0/KHJ80EpNhZFdoFsy1qPCw9V8HDCHz7cTDiukrMWluNkgDP+oS1O4teDINOrhmclelsrJlg2b0+wNzJHDUaXyX9fWP3IWfnT8PWmbglyi95j5GdDlJPDGpqFXBt+jBmiMShsAIkEhmTy4R8FCktWK8YoXuSj3MmSuiOKLiVnHjTzVJqhVD7BlaqCzp0QmlZXkd7cLh0IxS/dpj2yET5Kc42/0Ofodhh24pgIYYnmxMdsnrtEeKU7yaRatNOs25nGlxl86VvG+l3vH9rZ+8tf+jJ+p9QHJRfjX+DC04RUJWcF2BDaOP4kjh0Icg1E5iUczYYaxqRa+Ri9pWCms3S2gzwZTleMUDXBzkWLhFOkH/zmpi+qDcP1yjKou15v8vz8VfSXxpLas0C2gh96VxAjJJue+wMYxY6ZqTZrVh4qfHw40LICUTUGYW5iFiPsPtqKxF+EjGhNEgbSsVJL9Hw0Jyr2tOwNYEJevwa11HQGGkQOqzY6SVaRI0l8YnBLZHNbfVTl6sDBjerM0VCEgdJ8vGn57u3N7woHpdgRfLXLrkXj1mV5+0qA5QMxkYYT4vtDm1CttiJrrOHoSw4FloJYZvS9nFyPEN9kFg/m8Rl9HrMEauJpdyv2I86POMVaCs0Xvz6uvqYnIE1Fb84MhVhu9l86B//BxAS2xYONZBd1zAaw0ZoZBmFhpgRAVuLPzkqarjUk6D7bFxqfhCe4Hhheo3bnVlHt4Y4tRw9a1d1EgSp81UjAoy8y3mZLw4YkEaA0MBgSyoGK28od8obJR2OHj+tkGU6cYaCNyEbxw3mPg1QYI0m+oJvnDMDz5yJ0JuVLms8tzbWOzGGwbpvCJPAQxVJIK6ZB/RFF4gVPXM1iPFsxmaqAH1Ite6DDa5JKLsaNbxsdrMzOpgAEKM/7TGJjQgvJCi0asiQ1Q8tGCcxVBQX1ymCipgGQMsy69SpXzOqtBCzQArcqhPixVDbWFZIPmISjEDfaX+ce2G6sde8zji7FEN0U1QEQ2HTYpYTKcCvwyBEgMFiRGWZJyX07o5tcK7El063AROUAiXljyL70Nj10av1O4uIY3ihJbKoQYsKcWfAYYBoAFKtr7MehsRE4XCfHSf9A1h8B82B4SkUqtbdc1wdfBF+zliNS9yKIFjvkAEXhetkYGAv36+sCztJEhdv7mj2fJ9P5nKdYbzcJMtF+v3WYacgUKEskVH2rI35GvgwUC9JZGefdbMugiAKkb2vsn2YOcIO+SkzIa9rlfsVs8CFia5g6aTGQ9WASXXaOqeDBElJCimDcy4oY4hwqwMh96jRoMB3hL8kQeIXumANrmVJQMOcx3LGOmNziCLik4Mte55fBhreG/KX7w/7sjVtzyNCi06DZbtf8EZMVLGoPn64rIF+h8two1m83ocPY94lNzDQAp4wfrtBX1yJkaHlncZue319NS+3UTdq5wjhqVkjax4J1/BTacLoMEpMi+bA+Vp3rodxd8x7UWqj4FqfGS5q0i6S3pNMPI3WGgpJCo5fpJGvdP0kGdv4SrSPjtFULyovS18HH925DPVIxENZfrEDsel+5h2E7fz/dgqBw/LeSSy/JaSHLvUX9r8WepeIMxonqVHoSEq0IwalwnHdAFf62KpRjCDdP+bhmaJOlTrCVo4Dg4UVSdxYf7eBaMKDCxqzrXBTYVZ8Tpgr0ggwixvPNbIkboM7HQ6LP0QkDkxIpf9Hbo748QwcmLi1eEvIvIPqLshB2GVSduRtp5Y78FC+bDBIylOBIS76InnQRRGneJQd1k1QNBpqIfLb8PTIlDkJ7JVB7bkDqNED5Xr37n75+Snu2h5qVGHHHVGeIZ3BFkt+s8Zw3oB9eCBNPb+EIy8vm7uH2zBgMPWw7u0Kkb916GrsHSzRaARqRVFEoYkEmutE+aDheWefdyRkpF3dpw66g2mrWh6PeyccogRyMSg20sCBxMcoFEFP+EJUHuDxXILdQ0h1tKeAeSWUEmCH82Jw7yLBHOPiMptFEi5zY5818N1NdLMLDZElj6S1jTa2adCGTtkrb6bZutV4S3+X9t0kYBBEJ/tLiz+XjSyss9xc0+xLdqlY1TpKf5KIRgK2iH0jHXKcNLo4QKuh+BX/rHBk2pdMJkhxRZy4bmRqGB4Jt17yXHU3Oi0obEfspnBiBEipVjiq3cjyqZxV2aKF8iYC6p+bw0P3hjjEqoRoBM3XilDYVplTYM+cAnIeKZvRxdaSV7sV1yNfFEjXgQ1o1zv5+S4hpCBYf31L0B8Y5jgLoSp8FoW49grsfJHzvGl0nxGDRjsTh51ginK9KHHNGCBvh5dTV8MRV2lbz+Q9cWO100LiRZmOkBuLl1wsKks8Dz/N+1gt+22xg0rtJIQVIAThdzk9gY87y+6OcNsg4svfjvBd33ZjLWIedbRnDJOJVRkxaA28ODUBUUenKgahRmabg3SWZ2tPYUfQ/tzOeD6x5AyEz7rj/R5FrSPh4sZpHBm2SAp2QWDGDA9Xn19FFCABHNLJJAnby0HPlUuRX7ekrJ6C1Mol+NUexXNAM8SrbilWv15hbhwr7FeMGpGDhauIlVhLOzOuPAFG5cPJujWrvd0t4eqx08B87GjK6Og0E0pFZIpu8gNPqVly31JJY8QTRethFateVwFbz1FnRgQERNo8DjYtgJ5w2cFGq+qambvROj62tQBTBi6AdoesUQqMvx6gNLxhTI+8aA/S5mDSHB9P2729a6WdY8oU2nmKhfAf1OyIokvLxJByvxlvHbNbuAxt4dUYgBAy2FclvMeouICEk5tLG8gqGQP31j4HccN5is6+EQGBl6Goh2ZRXfvTc+sNbDdEgvtnQZm+UD52P2diqUEG0R+OD5dcFlN+J6iqEe2GTETYHB+L00qtWfSQdO5joLpUruWIv7q00KF256irMBDkMlDk8LPDaHz5t3dSjbXIRlDBm+cOBVbjI6Hg41SQWJljMIKf4T9hE+w8TRhxcGhlpyngoDqe9rMru0IdCvhVnEzCGggdXRoEUDAbKSH5QkEeTvjrEQa7SD+VXXL5uKI/4TOEtDII9dEjZi5cu7XimZ46HsSTtJ+Ml5tVQBLNdqYz3FCgGZkaD4XvzSjpGOc0DUEE0V8joHxrGyglqi9yqMxqdIcPH4U9pF7knh3wMAcOZDSKiy+NbfLclqpeL3XH8RdnWHMhQV9UQdivQTq8+/jaez1sWPApEKTR53L1W9W7UeNd0pLZ/UCtRtQflf/N7oSrT8M4GVxLLjsIgoNvm6NeWA+2K+QVlfNcta+75kD/EHiCMqQJWk5tozUUdbL5ZMGVVXCjKqJ7/uB2QHSNegYiNLJLlQ8Th0Jht5q5EC/uwXDUmY5h1Dn/MB2USWtP5SrfC5evxXPnpOwGnTwcIB8g/XQXh6i3PXrhOBdjdIZbVRBN7TlxkpI0GTWraP4qfOkLdLnoQc3MWLZmZ5IPZu3ipVhthhfw8oR0hSYNEMMqhljHaYjSpAgJvBZBMMW43/hIQGqh/EQ6IXdk2d6E5nYiCEj9fqsfiXZulv2pWcYhdMQZjQT4CuupHc+QR+qZANT+fUShrkzcY1kPkFBPLzj8a6qllmz21pq5LK3p9IXHH4aLcuWSuVPHIUI3X1HPeQnRJX4Bqrf5gsIJR8nXhYKsVXI4xjgO8djEijcOUmQyPaJLMIbRnF1EFAeQ9y9cZh4FtT7rEVg6/65lMY6r0wESTYfzYrh52X2qitzbdrv9dD5h12qT2OQwWCtrISeWpEIjEAvy2Jfm6pGK9hUy6FFxdzCsgElEM45dCZQg7lmLgJofSr6Dkhkn3mlxe3DtuOfQq7EOkRNocSxboy6Ww6AwqhXIaFjHoJx1SkGoxjhWkTJRQA7f5WFj5/Z7o/DCrKJvpVugCyfwTL/UZpFIWOWYmuCwYMCmwk/FN8DlZyZQMMu8mOvMbT0X+1NxADFxTYlds/1JbLDOgxns72ieGv2AIjytvoGXUbDDGVfkx84xObRP225XTZZ6wWuPf27hADd4/50toKo9uhHbnDZV+IYseeRoVPtHBBwpTluHTi8V+wq8kBRTGqYcgvOTwnQx5697AU2nPbq2B1krPzW2OmUPxA94HBF5B/IAJK3XmcvC/SJq4V/61RH4k2VWhECELoxG/fY0BKf2kvgRka+p/ZcwCeHdsohxAup/SDG3xBviuFjiwOpcLUyaoUM87lB6Ia4+Qq6EFIBUn6U53Yr8TWxHAEceMvRzjdW4AD0bIXvwROKoUabOYFh/XlLNgAsSnlc4OQo/WX/1NrqqsEVcL6fJQYmHrJuKOHgenttUyxwHJHpD+AJ3xhRYnDhNEW8EQPVlZdwRhew+eXD+7bKRozK6Acizybk6cP/9LhuNIjngTphqd0PqyO52s99nh9nNotafwcDPy8rQA8ceDGYN/aFdMKM84Je/uqVIn9WCTuuYE0wsIzMT+AXPjY63zvY+vEoPIyV2IyVHpbd6udGV2kB1lP4BK2zNbZIubrkv521zuzKBIwJ6i8ttZhyvp0KKpd4Mc0NVfKL+4xxCrMf95nRw/VRe1LaLotmF4ixakE6GcMAt1SJEdooopeIkLklU3nc6zzYHyu8E23lgeiv1P93rMB+UQFh6JbDN6RA79lVWTE5ZGkA4QiPiHg36T1dM5sFIB3m84lbabyXREkSA4ivliDGMdJkstwxqNKbtdcEPw9NxARlKtd72aO1Zqz1rHH4+7TCdhs3uYvZmePPu2l1ooWzBQkhC5cX62fmoTI3MbrwU61u75J7CKvNoWQtKIzR/bBFpI2uBnAZkGWlHEkmO7JlLgWNoSVFpoBqh/OpEhUiAIaBtyYFvdsK8O8AKjn4Id3ivWK+lKt5I+satUWauSmprExDJZHsV+tdNhzdWe0j1wQg/jjDWbAXBGOmmRZw6mUYuMJPir7oxaNXH5aVlnQPf4ApxeJwA1uPMS/E6t9YBcCnEGc8Roh+WhXzpHaYTB5wqEmfsQBzPeGjaodFkfz0uSw3PmS1S6GccMBBKedLtPW4F2Us6AhAFWt/ughsslaAzdnnKs1wizDPVNiO0vmuGFmTa2NA6/AglFG5h3Ky3OThG70kc2hD3yljUx4Wb6P4VhoyFnH1NvQ5rsOuxdeyfKNjQKFJomnNI75w7p2UEOJIuPFgsCZsT05ttYvRp1QWkOx5e83edzV3vID8AfUzauz+S+7BPzqrz72aU5n6pbOk8jPpv+2yWoOX0eup8yBudZRDPWtPmddbBeo6m/wpy4/C2Ltvo23EM9js6R8bwDw6nnawp7WKm3bAORXcRLZE0OTTfFOmyQXS7qFWqCRCKnDUS+biqvE/3iK9nmo6MqnKqGBflc9R7jE6/h1nnOG50J73eQ5Ikg2FjMKk6MyvvLmqV4Eli/+tl5bS7O2SHfQigqIDoAhvLKDWlngOcj8+EDNbAKQEgaZA+HycqhI3IBFqqL6DL+q3t0xm3zov5Es9RuzvW12L7DHBaeFpPlnaaCLdfk2M6ME7Slz9hs+M3fvFF9nlDLh6uQpywgImdFVrsy1GOxK75oHV61DfaK++nbPCqWFB9RV3Lu8OMEdv6UuHMoXm74V4S3UO/NIqTrxSOBSdKik1dYpgzpy7aDLg3f4kbivPlIEXBGYGOgoHM1XgEfpfvC92zPQutEgRVolEwqMDWWCsCTdChQSOEuUuYjpFAsX7o2iaF2XTKHGYXJfNugyPaSVXf84JieWQD3INDBsr21e4vfMHLaTAYH/s3n1c/9qsVAG7Tm4B05A6gsA2VppcP0T0h386rYt5uLo/tHD7okRANQsMIj9jqyEYidP92kr9BXDYiI/CiU3C2FIextOJUmQIlEUFKQ5M3a73xpnWcW8lrNexVs0HUY3beCMPoJ47FseEgHnDggpgZTCtDQKO7p7xxP0/k3y2PrG24NNWw3b7pjnA734iISEWjMYmD4wgly1Znq92J9FvbMEClvmys8txrvs67i35nMJUAjEo/i3Pt7g9RCUl7fpc+3I++kV/wwUoNdCya5a19lXoprWat29V/yk0oPMWPR5qAUcjnEfWoro26lLCY4LvYUd4IEaLINXTgrUXWOvwc6WaGjZ/YEczVTjVLRFxCZvwQgUQ4G9w3shQCRX+Eoq1RUT9Qe3ECiK39dAJdKvKjfrTc8U+c1uhfEHFwlBSQSD/V0hgnh8EJxe8i7lAdXHSJcUp4aWoj4T5fvtc1AEOe0i9q+EtTJB2K2ocaRBsXAACncklEQVRy7yv4GzwTO+VZON+yvCSS8vR0djxYCXYpjo77oS9pSpFucugNprNJtt9R8LgMg6SHCadIj50IixZxbaCujp03EGsPK65wghw4x8FKx5eCrZivACRCJ9VHLv4TD+DQ1+Cu3zritaZgbA/E0UPEcXYYow6AmqXjh587yamv65tWxBNx0PNxyeWfK/fdZXvAaXHtn1pzqV4dlI7CWPNcJ5aR8Y6MVpA9z++G268HBSClCLsQgyP0uU07rfVByHwJ5pZS336KbHU5bkfLFwuU8WGu1+FDOlocfju+dOaldofKma+fT9dfwgHG3OGJkJWjqbCi6evucN3l3BdqfzJs3SXF4GD2AVIgybOzCHnSd1o3ksERLEmfXSu3C4kJMF4Z/OEWWNU8r7Rmbo6bnZd2dzdJE80o2ym886vB/Te93n3vHnlGnZ4lx1QiKKGjhHrOu+Xjp3GHIi335RUkpPRUbMP0WFHdB2jqKJmyLJGj5nrZd5refjj99fBVyIySSOC9FZSfiFQrLUeZojM4CKTWd9EVXo4DYkfD6SJBfhNHhr/qeIQDEIAsCMgS2XG77BO23yflHcPBigAgVLISIMrFIsXpcKs2g1ZmgN10HDYNWEJxOgBxZOK6lim+PQqrfGN4kOF51Q5bZEVTEukicfnwbfyN68XzdjUyFrGm12iHtswA3waJVYxS3wy/R2bYoLrpYrF8eXb+GMp4yhodQMSIM8mLrE2iy9LrERrww0N9DA7GyR+PQ3YgcGaLoL9qLcc+Hk/+ZSHc58m4TG4q7zqWXQrPJCHdP039iErRsEmCQsvpnum4Q3sCfD12UniX3lifzwlXfW2FIE5coE5jTKZD0CMofjuZpb3RXnCrTsCtX1pp73if8CCAO6WSt6AFt66T/nHWOj4XEMkTZe3uHDYaMeDenw1GPoyVgfDedUYWoE4EtfvGINdRsZXPFAFzoVpJN0mig/uScUVi4Fqec90Zq/b8NB2kd/1CdK4LHXVj165FzpZH+W86l/tiaQfHvSpzikfpGTzLZNpLdxoU+f5+HxXniCM6PURV/7DZ/FeTyfvkZqgR5253UZog9DEAT/bebvBtWCUKlVCEXIXpjAAqPDvcU9l7bg0MwQEgKs0DBgKZomdqOIY/VIeX9GitetjBiGjBog4KzD4SZCF2EUiH5opt9IPu4fWm1ntE+yIhxsd9u3d7YCi8d/oobMnr4U/5dJyd2k45CcIaeZzo2U+Qwu3xji/utR/cW/3mOA++u74BoheXdXjFHmHlQueHOxFJNd8pHJNIInch3g6gUIRYh6nxgm2VWo1Dwox4ekdCWMzI8JcC7XJvQHYKmDLkAaXj2e1k+pm7mOccqkhxuIf6NMXxMTu+hshY9LAJjDd76BX8InfJvXSI61GZ1iB2xbL7iYxZeZoqvj7+Bsou1DeG9ePaVTvZBikpJAyHMu6VlJiwJv+pz9O5qHCoAwNorq5jEzb3OhLIjXaGcL2xCjj8T1Ps8fZQhWNwsm+Q0D4AQ2/7e6A9ikuQa90ocD07P7banxv9/VGKhGKGs9A/So4No2djgDPNETHYXfI/b1i2aFwfQ5ByJzqA1puzXkf4eGUU6BTuK+Y3gn1CAUb5dasPrthmjWVuwEBr1sUSDboc4jYh47wxem4zVANuncxIHL9pOuAv4SKdZ8kafhRl5muJYITRb9PJ7ybfQYjzp++P+w2Y9ToyZjCNu6Bug+UCCYGFk7Xa/6mZPzbCP6f98lisyVPINO3EfawT7REWezfTF6IWsUBQCYOqCQgGNBIyADmD7ozFxpNBIstzqe82VJlti/xt5Iy+gMg+TXLpQnsd214DenLs2maHHxQvhhzEy35N1BwAgsf40NI+FHLy13jR+XI+gwvkFW0eIg2HI4Sn5LxHWiBUm7/GsExmIuAL0ALHJ74qADFlpbbBgQhukm0HrqlBCqoVoYkUaoBgcUskSjWjO/N4cak4E7AXkoy/PN6u1/wmN+H+qBoPW99ohEhxG8EWgVRGXs8/1CdPkc/taEa4S/TqSR+xxqS8Nla1EojVA5MyVdEwK/w1jjj2EbPG/qgKoB9ZaT62fhYSJRE1RnvAs/az0CJ3CXnDpq/GMwewmRWX7bFxq65GUu96eNnu65Rm3XDjeh4NqndQUCD+AVY4uqBnbouP1/Z/6AzBCIFZXLSOC4rabLC/650m+2JXUB6WW4chUZ274VDSUHhtmYRuo1s64eDVRlasQUKKImad002QdxDmWgogy3YTLeSKC/3jIYtO5dlESKAeZl+cXvGiqvZXgZOpQ9XIxaSRh97xoSdUOGKFOR+8PvTlD0X7c4Cs51Hz/G3CqKXL9c+Pf/knzQhG3dsmYom2Fp2x7lcORlBfCKscS51yAoGpBSFYlwqeuwvlKxEcnkMAI4BX6IxNsi9Ul2MQwkUR1Co3dDmpCZ+cJYCaO7LkJySDaQlNVVsZSizsQC2j9aXJUm3ga9ffV1E0dBBt7Y9z5r8uxE0hdP4aIFvMG4zY0TO7wS/hbzxABEpBXqHD/AnrVX+q/phfMQf0cpwEawidCpENASMkbiiSfWoQNS1xn4RMW0M3Sp7NWYH8tPQ/ZdBZEOugkJqTwPzxpixQbb8CCvBDZAicIEGqICJk15Grz7Xm6Wh6QSggO4Jzb3eblqSAgMq1NC/jERiU+gir5HW35nG9z6OEVYoWG5gakAvd5kpUuQil4mt03o040q98iFPk2Skge4NxaM+L4w4Bb2PCmglbUe8qK9Ac0sn4SRFcJQH5hWGBWrpbGqDAExh1Gw+91UQYvNOW/lyZubY9NZ/PXI0/mN/QiGmTRbu7kZA5H9+DebShyMsPOZAhmvD0T8oYPE3LsCRFL041f/SaKVUenE06DhVrRBShUhoxSq5w2o3zBCP1LoUr7cYieT03ny7HPjTteEysUF6N8vJuPrFrndHNpN3+TWf30D6Ks1vP0b/mnFxHs/MU24QVsQOt3k3jNGk/HY4K2dbNm14ukakoW2eAbH8d7M+DCdtTay/fHPSDsPCanwF/iEBIBONVI6HWiRhGqMlQhGbhOnmNkY2EarjKUSxAzfqdpfRCvFK7GYSuloBwHeoTEP/xcwh0YKMkhF535ZDaOG6+ilSFzouf/ZpejC+w8X/FgBz02mHy7tjwkPvYb1JVRczqncyVk+HPF/Xv3iPzW9sKViXI0pE78Erw0sIxov3rLC/JIW1ujRRYGf/AcOhYKd7IAtStsog358cO9oezSOV63kgXcE4DwdXu2Dv76aj2cLQ2iUJ7Zy7OvBMbGht9VQ7W/QnQmVBM6vpb+fJ4QSp0HSML6HkijSfkqe1h+IdMTlTq6JZuteplI/d+AC7h5Bk5Exn4OISxeIC7wh0LAV8ZKG0C6zxKdEuxmk71ScuTYtRpzxMa2doDNVsjYUfzsj1kjiTb2TOcEem4vOavx2J67OXuVJ6meYPXWOpJ3hECB2ajvve52hvesG3IcS+1Y282Fu3mzV091kCOkBOdz3tyDGXanqetr01Ih/OfZw3J4/YpMRVoeNo2L9kxKo5fr6d01Lv/XXBhk+r0oag+XZvD29Fk2J29HkASnEF00/mgXJS5suemvRFxZvK+3eScyWeIfr/Snk5KuKWBjJ4K29212i76q+xocI+5sOm1NRKUO2tf3HAcwHoLQxOGyqaMeF1dbbBIKC2L3Fo7RWQzMue4nIFjONyE3k7V/Bz+us+qWGAq6r2JI8OyR+QXLnccmNih+o9f+St9ZeFDUusdYxFtehwAv3PY4t/1GxQcBdwSViKOi1e//Bbg4du5wUiQ9J1fBOv4IJQlqXmuYs2TkIX6C+J7a/+HwKE9U3rRboTMRcgZLNKIOjyUxvdArqgn4Nn4qphLrPA8eDXeQnauh2JNU6AAxY1GhtRDfVm2Wr7DCaHFRSi97WarEMg3/BU1qw2TE2v4MVm3NHHma2XPn+OHsU882XjEeMpQE34ZJ8XPX6wA6IyKVJDAZ5aqUxLuVjFWrCy7G/0nI2CI9RaoOORQFpIQ5ecXpFP1V7KFlksUJhJJWtm/H5/fjnt/zHofikQRft97dAiHW6gkLhRM8k2socEOp0aR+c/zqPsnnb3P19n1en8532aocboxtI+veWsT3bV+f9N5bFxfstNIHfuo2xqhi186Rb+9H7aH/evdsHkz7I48EsURKpNApTqc0GDz2ZS3hAL4xPaepMo1Rh5MJ41Z2V5uD1u1QpPGBEs6bKBIJYokW3sSNuiOx90ZY7cFs5xfHIjW8aEvS7jmc/dm48Hb1mF1KH74WVkJVta4xa7fdkU9nCj4ETcA+8Veu5VwEPiCOHaHYhs+NVEmQzx2Ol8hpvpcGdpCfoCbFB9UwhahJPSD42O5xTiELhwN8WVsIlFTSRGmIPbUSsZmuv/Y1Pr/dVhX+0kRd4Y1iZ2OWDzELQSgVpBMpZ8p50hHfXmVMHKzovEgFV6D+mFDHJWOMxCQZF0KSPPGMRdlhgyFBXNZt0RiQmhEJaAG8bTTIKI3UCPa4cdxrX8rN+p/4HnKudKz1gNpYuXhuskYTw5iRhYH6TTuEsklaOlxhgUJzAzdHygDh4cMWpOAgcMq1g8H7wPjU0ERugMUpNMsS22EQa7h9blUPGmsUzyqO7DUBvfEulpl0duBRScjY+6QkCDOIuRQdA4TgOnZtbgQGGc2p14NWeRENJuqyASHujQrM/xDs/jPNaY37aTTymDb7T5TADfQIYwnvL8cXqreu25MEsCEhJUd2o2f2p1Pih2A47gV7WrsNrPrtMjxH6rl4ZI0+jfJ+3Gv/Vp6k8i2MTWZTyP9pF8kjUmnnB62nRMgaNDYVTJucM66/RQDymhcZybf9vp700OKxo0e35pKjwYTiZVpYi5KO9+EsY2mX7J557H+EzIIvVR8qV1KhAAYg0uXPe/H0/7bznTiQGet4z/0quwb5Ohm+/GQcn0qPmAPBVUrSLV/kSsF6sWWh0YvceDsidLvWu1EsMa5qI20E+KPYxEkJQ3WYk9NO9Bo1tbVv7C7RLLmrdQ7aOciRfFX7MJm2knvjB31tPHbeAPprNVWGLaIAgkIaeIaeTU8ojgQ4dLHT3UoHAeEj+A2hH1BOPELFoned0kuvtvZ79UjRZU8m+BzvsI3Rsoi1K53QfsZAWEA/Sae7uj84HFqTA9uKuLgVAh260AvtDKfK94LMvZb2RpSXhYgDlLM94gjQ60GYhYHPHx35Gq31E+cBCkOv4+DV69APLnMg9dDF1i3eqFpmFgTYGAUlIVfX5/AWvrjY/FOT8FFjbRGVUQoAuPO1760qx8EqzKcO6Jhya2fb3Px8MoYhes+/7zKtoY+3g9793p0QWlkwwySWpUK8I6Tavbt5W+Gp4/nzj9uuwukucFohxbXOmyPl+WhuQiQKnqsrkc9LEzkXxU0VO/ydJg02w9PO2WO25fokMvZ234ugLB6vTsxxZ6mWVsEZDWYpY4pwNLLblti2511w9Tjh8BbvozSoASSE5y+dfz9aMDJ+OPR2Th904IDXV+MsmuM5mrSzJFHuRokilrC5A8XitC0lzyvnqUIrgpSDUgZtnp3g2En724+6IiKmWph04fRN/3ulh8ZKlbqeCiNf8GVxc+LbBcvmx0PDERgzYENu1erGs4PH8IehRYE2sBXw7EW/DB3Foau8fGo7QrZjM/U/rnjQYLDfNs4QvlFMr7o/hCE2KFAceozEL6N0FEymGCGJDnS8am4h9h4ohzK84tw2d64mYgUI0YMVaipjBuJM+FIBGrrCIa3EwRfIJer1KfIpcK5r6PJ6ClCI0eTNK36asc/wFFyVvvzoY+jFoefShT5FaG6o89hwLzwOj1TIEXRJtBN6D4gLyYUdAc8fYqGiHPKB4mk0IrssgZUtkdwky7liWj9sEDR8ixWwYskykpKW8YxiFfigeMHJyTuyuNY5HClwhqFCWCj9LknDWsfVMIcfOmgRiN0aaQFBQpN5ch2LrubVsnnWoz7D6dsvMqCmOHxMP4nKdmJntnQxcb5NXc9HlWkF/uN8nfNrK8lXHbV58Wcyd69jq39KQdBXNNsT+QuDo3ksej8lG1mU+kkvZaN30Dap0i4A5c0KHzHJ418VSt2T6CdyoUHyR4COV0cJ63L51VTXakIbCQSkGburieagJ1u26O37eume7s7VE/N4/7cX9KSgJvBbXNXaZFXTaJ25/LL8jyfHhYSEEc1Ba1krL/FaTJsT6XuT4112doW14/H5v58NhKiWVymBihxTZmN4ypfjXsjSXkLEcwfGifWl87UcwYXxYrXnkx0YfHN4W7Wx4G3WZJRksT94GyE1JO7Gnup9U2XS+21OEuOCy1XO+Cxq/b1r4Lvx9Dx4ju7aI+9HDtC6iMzGDFWyC8NEmhgiKUre1cIRfwhYYG50srxMm0dv0PyCx/JAQiqVKe13BQ8+fgmHwinP4QnsmD1iWMDfCGN7yRFHoPS/auOr+H/OFhOsy/08AHyhAMVNxnVEeBhQCe/yLdDjWgAjpDG5TRHTDeyncSJwjwdEd0+ffzoDoOjWJ+heFgLV+uLuK+Is4BhEURYRp4LYLC+Rw9tbWI1IEC0QbhrjnGsp7uOjzhUYnEU35722tEx1oiVKAxD4nBv7iEuKng75uNWnkw612TMJels1kZBNEdGrcewQ9t7uBstz+1/WnZfg/ChLcKOz5cci1mZDUFBp/PwUjGsioEPy/zu2h5OzxJQ80vnwXdXjcEqKtDOPFRf6UuVcQawCwUlU7F8HgTC1TnNm59XAU/h4xW9c2p46mh0fCiTPTUwKN682x+LVQmrf6y0Tr19GKJtVEd7+Is0Was9HEzn3cuoOeuV+97D3aeeHIVOYsdy/xgFl4uhxo/IhAMsi8aR53nd7k7rU/JLcV6dFYORvaYhK2KNxomyMglA2MLxJ6J6/bnvLqpyc0q+zD4KaaNiKG/74xH8E7vGWY01DckQDNgG/kOw4AKgsSteFkEK/CAhdVVr7K89DHeIONuL2MWQ1fonl6y9Gr+0S17iXYQ0x7eF61MnBOJnV3Ciaqco5Nwfr9akoLirUNKgM20yyCElKpkd+l90BFYwDhV+7cZ8v2fSXiiuEAcgkl++1wEjX8yFuhw3qYch6QvdXJ81gupZ4obdhPuMq8QHiKmHUtenVRDzADKuR8lz3HkJgSm5Fe1MmFafQYzTDsgyhpKOED8e0C0ENBRQmWPsBhnleEM8dgQpbs0txNf6E8IX665orb4NDh9iGNWDLmHYoaDL/6GtiKuKXaq87haKBYWJSaUc2rsP/f2/9HWBbw6jXcniptl+OX9+qZbV2lz6cXf39uapOVuGoO+mZKw3mlAPymaP/UaxW5zLlI4QLc76213++ufVYLj73X00MbpgiO6PZTo9bmOes8SKnAQ9oc+homHLsFXMvc17887g/X1nPOzfnJu3A1McZbwbjAFkW7bk9Zy/yI2t1r1lVQ64GvtdOtjvqn87vvlta/O9usfBgw7q9xyhZtdJrNKBHibLbNlnt2+nz+U2qfLvjFm+YPu0u9VSfaRhY309q2x8FqITy22pUwez2SuzSX+YRRvmOYjncMlqoQpFZJH9z2rTcWF4Q06IWoB0bES13SBi2GAWOF6m8yk1ajPIuoTH7kALhAqwkX4Ur0WwGToh3POQ2BDq+p1e/3KubGkcDb/wzvh3qGS/cvo4A3Fi/M7nXcEFasv/V0UYIuSp6tf1RONoigd7x1Y6xf9Fqgk/5LCV3YxjAQble9hTmgbwUgfkjlukHzBSXNrBg+3V2cCc1JlQhqTvvqJnFMMC6ZMCJYG0OoUWgokO4C9tPd5orI4RL3EZR4NsonCrX7Kz2PWDosit0JeAOAwlzfNFicTFIgwIBWJZQofYI45JvTJWy0moH/DLc8bSWTW6InSRGw6KwLkPCGGufUrom9Hd7sn78ZQk4CXsApzQv2e9wbAM9pt+sEXZunl/vKyLn7bbl/OSM9/r7ZSr93u/G15mGT2on0jKMV42oq75d73DA3RRWdRKhqE9mQ1LYOKOL5AXUy3iL9ddawN7MltIi5pB+0APbXUrbKme1BNaPWG0273vZdrvbAz+DXLpAKft47GHbnt3bazP3WW23ncvaXl4/3N2m/QPt5PlsLkryr87/vxN9/IrctYdGRjXz1aSas+d5nOMGOgsG525tR8fd2NoT/L9a6G+Ubp42jWco9leNa88JTOn4DQWI6ypLtGz9ri/aFXv3f4gebnqiU20yF6YILIcHkhIn3UOB5x0CgfIZIipgQB2zCLaioi0A1WhHYNEgOap0iEcXNfhcfIXwjuKyjsWoNa49F8t564UjkfYg1BvoVv9nmkn9+STQPtdrSCD7Rhp4dhsb4na6Wg7mURL7jhLjiT/nSZG/+bwhBfOzwxkLE6ce1J+pb+QGwNqOkJhU+Lf/lAFtQcX/hBL4bu5s8CzUM0RB4sggNVoWsTZEshhKuC0Pj5oA90x8FKNRwevNVw/naKi5SajTxDDXeSgyRWS9nQ4IfvQw4g0PHGU7AjwXSPsgDeEHiHp/lO7kF6xALHOof/jeIeqqHfIh6meuELYjFg1S8Dj4oLiX9X7da7yVfSmtR2VEV/9XjqPSs5in7QXRPJ4/ctlt2q8lG3pIpygv3mz+9Pysu8MxS3H9lI1L2U2nBsPZEbAHp9GPdfBhPZ0s1fryy0/gzjVzo9xWqHEImfHNmtWihme97uk97k8fJKDjjRCs2VAIXRQ0/+XYnxuJFX7Y6VkpnP5tDLobqiNyh7DQCbYUzrZ3dEl3zmC3d9PDA04tUad/GU6aq8Ox/8gy9tqTT9udeMq78f55YB7pNfDhI4dM0/ATL4gt+5yfFlv57O/VMtFszXvp9+l+N/oUQarEUlGvXHZ59WnzSF9i5dESQmSTwOdr+WA6/4hNjBWnOhY2Fprcw6iJ1wEfAHj+Z3EEoCffMKW7Q0tToN6e7Qt0MEhxNG+c6kw4CSGyXdoVrBdmJSArnym/lz89f/3Q3wxNV//Pf7lb37P6Q1nya3E33xLnKUvP9fWg0xhtkLDiST1rAwFFZJEYRXqquAMUP1Oq8xByJTrhswQtviXvzkecTIJmLPhMeL2/AGImuYtWNdupM0OgFlcmb9Oi9fH1q2EavBPmD8XJ45x/nV+xrn3li9TrRiBeI9uhaPxWJxkLmrcW7yX1DgM/gkxrl/4cgTCsnkPhypuURQRZ8YFPQrRc0bCk0YdsVb+79/e7lcWOQIPFq/SY13StY256KwcVFUi6on25LrTY/H5h9PzXxoFmcN6ezzsW2tlaNStJNF4tEsSM37kMpaXHS9oeBiX6/VDt5p1u+ls8OdP2ek2vX1zP/7hx8n6oDlqmQBWsHPSZjrYmGm9Ktb95tP1uhn1x07v2wmYBvduoyvvtL+WbhhEz9COvook56OeKdeTBK0C0O2pvDE7cg4jqt70G+m9gSja72btDIcD01Bu+Li5VP9yLBfHrH9MuZdZdRz1R+FF9YPe2+tOhD/rYmsgM5P7dvRWZPTz4fx5kn89OU7p4k422ubh0Y66u8Sj4xNFbcfpnKK4A+iDCERkiWAtNETOsocOZhG4lUhVjIC8Wy1ExMEOhR7VVjJqVS6NpM4EuSB2x8Xc1dJJppUZCwqTHxI2pv5je/nNX2BUV68DOV9kg5ymsO2h60JM4wcbW1t2+8+zijPgluIQ1t/NjSGvqoEYAchuMI2NXardHW+oNWvI0xfdGu/UG50ZcXJrv4ILFPFwWLjwr8SZ8bWUF6pcDEhXCnXtbHntl0aqj6pb8784aLR7cNECS40j4eFQ4WQbGAEyDYn3Kzo66tw5NyGpVz2rwit23mwXYDSA6pBgr9S3EIfAsZBC8YKowJ8o3PcnViIuEQdAeBkECkYkTK6zxVjH0kWHxiOjomihNoM6P9HtYYRCdVgbcV63s9v+ePz+T+3tHof9dD9rjBqP+fnP22MymiSKzEedZQtyIw0o4f16B4cBwb0cXnuA+sPCYCGGDs2gaexG+7fLcnJqJA+L42jY/LTOhpfTruxMkuEUfFJ2RwP+Z52FMRioUUxATvNcW7rD6zRZdP7j83ky+frnV7ZiPx/OYJVs9sNh/dYRVidbGmFyGdwk3TnAAsKXMp/VNRrA3bzt6xSrHtd5ZGuhp7vDZXO46JumZ92NCLvRq27efdXafYewN/83uDvr7MU25qdsUDwyA4rqjqNprii6N5xJuvm+K4gaCzE7lXvKpqMMPyhxQQ8MtURR0XnR01uy4WgSHX3VG09tEtmMVn5cZmyUydB+RKRIuoKWEwo1pvXZOZzEWq7jX2H/Q+mR5pDucOvjRzsZViFcgpCyOooOzelX8dsaQo13E4LItROAMClEQ7GsylpiEvca+cOgOH6JGfyde+ZgoCOQUp8lCyHxccUvJztuI/IAEoCeXH+yOIFeDKZenDwRTkO6kxhT6AjTQqMgI7g/DqElwh8GZcQhEl/iI/DZvN/U9VaQhSxXLaI4MuyHZjtQKcTxwI7iEzVQQo3U90DIWdOwPSxJBBjcGTReGiZWA7gZXAknJjR9sAx9OFIwcZMAjFgRmKnBU6AfY1ciN8zsSVD4xJczYO2gAPl//LtekZ1vEs0X0MWkeB5dBlTR7BqUu9T1qNvMJEcxpPfV6nXbeA+SjIz8ed/6MBruksNs2GMTNttcyPjN8bgc5Bu9AtRV6unfv6TfaZfYISnmDOnxnrQR4D2sllQC1mL9/PjeyDq7MVxtm+XP/f44M3TqdNLZvHOpXv7hdXIjJWvKI42Ebph/9hCDAfhfCaoY6eXYnAz7usU15bkaraF+d2cTnHY2zSiiUa+1B+JGA93dT8vVUM/VZTt9+HW6K6rPH8GjzdtFTJEzYqMf7ZTmLz8poT8nk+NohnECO7NhCpPy/UY2J53faGnGFtSeJ6EI7yjGEno+PmWfKd2V+40tb5tVxF+j45V7/P8npdbiGALsq6RfoQLhLsRFan0YIhzGJy4fAhHCSc9K/XpLRMAhTA4ZNVrbCros3hu60Avh2PsraabY/IrAOKTEnSiYAD9g2SLWj15Xvt19+VrCcd6JtqKLlvtxqfCm/mpxAiyCVQeDK8TPaQzN606jGfsOZbZr7gjZdJcWShDqKSxEBPrE83LmIfESzTUIJyXwXNkr/piWZdj9iAmUiW7qC8ZC5bEddwNRKxSJOKtSr0D811eEKGNNxCq452jw295vtvWRiWVyWOt30cwBHJNvlud4LbEs3Zgv1BpR5ZflOxz27LbvgFk4QTFTAE4pfTvqFbffrp3iw/4jDpKOOIPeGjFeyXIkNGUqs9te66nR+Gkx5svOB9XwJZ/1kmI0fhpGzzEtU56L4za/jL5eKEn5/PS0GjYmit3OzYkKg4mKGvORuKDd2fTmsHvVYWPa09BlE6y0wfB1v+08NYenfQFhC1AArr7fFlgcC6LUPm4P2ms76dlz/pJM1oO74SV72a1UDByuUtNNvfM+bSCtZhbRmf2qkQ+tWZFpp/p8at2MJrft05+vh/8w+/pf9Q+vP/33p1banCaVnudT3Yjah/3P/WO23Gaz/3Env1u9HW4uy/bNXeih+EfZYtQ69icz288jiqCKMArswrgr9A8utHmGVbbhBGil3ovZEEf0IriQ7WckIO627ssW+iTfJIrAQmOJm2sjQPpxAUPXkuV6X0N5h3CHcyzYjRcdgDDxrhAC7+/QG38jtmHZ4wAQIzl8HyO12kVJB7BANcbPDUO+UD4fqU+c7SwX2DX4BoY7hoCH/Nfqn5Ynd18OW/RdisiSV8O6xcbUnhUv6/ElA3e6Hz4B3evBPR013E+0bRIDB98Y3Q0CaXUsmi91UsJd+nLm3XIPbHIzHk+fPi/j7FM2Wsab9hMKoX5MbxUTxKETygEOw9FPjAaMtfeA1HCEKgxRGKwomWBmI8QGu+jYKuh2JSnhQ7aJ+sHemL7XI0hb3PhUzXHspNNNr/0yGFdtHofM3eLj7qV7KRxNaJdZ9bplMR3I2/PR7Nn+OuGXNEurw/hwfvPQ3eoGu/3+X7Zv3w3Kt73Vn/IfZ5epri3YCmYeLYEI7fOo9dLcS6aMOa6+utVd6pRy6aT2dzweD9JSfxl9gQ6jsZz9QLu0fWY4KHNZHC+L2aD6JXtdHo93+lUwKkhyTdS8x93BQDHLPOZTK9GUd2l1sxjCEFMGUquoy3zGqelvi+p+1hoXz2zS/izIKX5sVDqVmjs5HE9fYf+X8aFxuzJeqjNsvekOyua218yb57FFrTtSE0BMdVpYhiwUBmZE7DulxjoTM4pXQ0zZjirmfkanYdosr7aZ7JvDw2LZroiZG5iDSWwsaY4uQDwROxkwDo+ViNfJSyId+szhAMDE/rqJwI9qP6TWi2Sj3vYg8NSCWx+IcLMiEnE13xACFILj4vQ4n1tdm4PI5ui+xrEItIqwbnbR1CRcISfPI7kNNxSHPG7Dn9rh8VNEF67msf3Pt/iKvDhs96V2CJFy4H7EbSEAISLFopBlH2CTFcbjj7mazzgGvizeFpN3ZLSiJ+bbr7/58YefFYVGQB+JM4z882CIPkxNWzVKIk7kl7ABfzdWg0Fgn2vbSsUSZcdc3M+Y+BYK1Bwi5zVuI86ezpDlVYEKbzIZYvJqbtLiCFl7a9Xp7RdvPlSr6/6xMRjf9W/SnkbQRYPKu55yw4e4bIh5rfauXHlzINBR5ZckXy+eciMlxo7t6KX419NG893w+8F1ueg9GxlYFreCllmi4VbkvMRZsgK1Z/m8XmWn3iiNlinSG5Nkdiw3wgQ1LCIo6qT/cM5BaT+b32ja+Or8w1M4zWNrW6I6JJ30vPj4/R9WXUr+z+klH/ZWVb7c00bBR9qI8ZuN+57a27YupL+slquqsdyu/5PG/O10clz+P7LWuDNb6x16Otxut9PFnWSabW517gw5yR7woNUxtJVrp8OZ7gPHIlGN7w8KFKeWBxtiH+oqJCW0j45Nh8w6k1Fhhe0PpAUrULKNxxcEMjBMMEAoZWb+iwRYfJJKNEPfx0ejQDH2mp1jB0SEwdgBlNpUoSVRrEOIkFJ/4l/hjviwn0L04jWnw02RVNFBKOPoi8hZqoWYERCVeBtPFs3NjYWD1BqOBytm21fGhVzzr5fz1hDZUKxOZjyqG41jL+NTf5HrizQZFoeKePpBLs3XR9TN6yjNuTCyKUWUcBMWyXQ9dtRbHRSXDIiHHtH6Q4/lm4fbh7v97ofwlLSAYtBVC6n61Z0ytAvNgOIftJGujhEYLqSTFzycWDxhQGSh48kiJ+zZlZc4AHyW+Fic7Q7ybVxGjzeeORWrq7JCwHbv2Cj6KAjdQTX9pnreKwH0ddvzeZiagNjSL8rUBkQ0jZqdIQ3fdGUT/ubko4cpxKXBrHj+tKs2jENv9Y6vd+jcJrS7vmyXcb5Xq+N+4JmNX//2H9e7WdvcC65MrpvAcNg9tApR0AN6hRUZ6i+mSf+5nA2EGu648dxrLffFu7T/umK1Tu+Gpuw2tt2+uhWT1HUF6reK3ub8X27PebL627T1wUSqBO7g30Pk3cZhuTuOkLV6F0Q3zsahqZvE+TTGdjg0Vnrytro3p2r2MFsKSa+FodzrTy/399/evv893TLA5yx+EDnJM3PxITdkiyLNN8vh7IYHwR7XqR+biJuqEO5w1NZYPF7XBIrZQ6rDXeaCU7xxWNhbuQCWrv5NiJQfwnGI/fPLWumG1xG+Mnn0vFpr/BffDf72hzIsPq1JX/hVCGZorvhDZ9RMANIvVpVk//JKrSPDBfLmWqLJbXxUWGyP4c3O1pekEq05GiphqSQI6vMUAUZYkjrQCZVP3fHBahg07klunB3OgozE59AWnx1gGF3W6TEQxE1pks7bkfCO84CAoH1T9NS4tjAEDllQcUh3MJRJf3RA8ciT0Sj6BSl7tGLhBQphQddlQOOop2I0+Ew0zm3ty91QLmhzhuco39WSijWUffS9X84WYFY2LTAHeWuhp8tZhHgSRyBCYWkqfoiHdbBRg6hKSU1bMCZ77f6qwmIYYuuodFT8jnGMroBOUFyLG88oa6Ghf7Berqu8eLtYjI6N9eunP9y1WypEKo0cjv3Z8Jq99rkwbi0731Gpp0+/HjBUt2lefBr2pbawCtSEaT5jkNKlsU2Hb5YxNfUohjASW1lu0/F6kyS3l/Zr5yy1kwDqkvbjrvqn9eXvt+nhRb8MbQTb7dnw8in/+h+X/1YdQROlKXPK+UUwnF5oHtOPLkMiFsPP1Gxfu4Nbbbp4q4Pz+fbhYdfp7Ius2+za/XTy8OY3/0444q5MVVCGBwAFUW1fnnYvT9l2Y8NscOgtqroqACuhBXUQKctyrzosclugz2y9ORRVvs2zrcoIHcJIWkxx6w/7/SS62RByBJwIgWsJDqUblpr6D4AykrJ2M4zD5dd3nf/jfz36d7fqxGlWHjZfPIAOep9qCaXtpbgIX4xyZpdCV4cijh+imRKtyQmLkxNeTUi/8xjvp4LDNSJSuMv8hyCZ+bereqm+r9o74JH2mzczWKdQAbBDAqVliFYcPAJJ+nkuHC7OnEquQF2DRB2gkBsqTCkzF8xYv4NE5b4q1NYaKColKhiVUAsehCtRJ6FSwm+KpvARC9O3p/N6qx26mYHeozQlmplkeb7J9k4F99j0bAsq8WRBXNCq+B/7Vrtx0Y/CVRHdgq8apWHR16zK1o5kmW0PZe7gqf0S2vooUy8B0O/rBSpJdek3j6uYGI6sMOGSyHYbo+SDvtUkJEUUaqE0HZRD/LTPXg47WxmVba7APA3HaKKa9yx7qfp7uG3gES/5vzvs7yFhYwyt47ArayXDHJH2vrquj6eXfAdjtmLOQLXZyKf1553xr9KZ+QrnmaEzbTMuft6ePp+v/y8NPX955Gc/tk4bnPtt0R8NNsuq9/1mu97G0ESZzWgilI4HM6Y+SlkxUw0tbNwNWmm2fVok12/n6S0wpzN6Wn0y0NDwD0yOt9/9m2y3fvzw9/nmY7b8CR8+370gavzy/Z+Wzysbvt/m0dXKUtNnAA3pm5I+2sSpCNlraNdqujU53iv5DFQlAlx4hQRPX/giCFNUPegSE7bbRlM8dWxaW4pw62PQgc/ZUJt9N+z8r//NsPqYfRvXFjMAN0XffuVeaM2QsQgA4k9ExpOJ0DuEOM5A6HtUQEznODNecwT+ekiiybO1dju8fV5DdD4IP8PH6pMT335pANZmo96tWXdJ9/23U+dNpsO/ubKwnhC1OsqwHrr3RCTzxShFQR8AiGNk9p5iEWpCjTQtHM7PAdm4/rWRbNVhS0nXTwNl4b4HnhZjW5G0s6IzMOrv5P4QlkTYXyapSTMaCLTXNQlRvXu+n2bdFqaBgyqA8tz+Hz6av+NBQJ1MNow8oCG/9XMp3nSXxW6DE+FeKnQF47HKnWMQjf9dyCJrfH0p9sUnu4zMHJUqShpi1LII0Dz3nn5xkupbDKpLORoMt/ned99Nbx7Ltg9E/xp+p4YnveuKGhX7NZvl+nxan9WXJNn687mDaQ8Yyk9W7Wga5mR0d2j0NuCIYhNuBkXGf5U4oQ3XL69v+Av93i+LdJWVGVj20njTv/5ay9Jr71Pa+PZ8Tc+nfb+5+te3K+Nat+U7KGUn2ezRl49GDFBGKKStqjmEVlNRh5OcdC+Z4rAb1x6HsJOsNCgetvuzN6vVDz99+r55PbxJZhpGEOSX183tZPK4Ln719ddg0M2mwB6zt1Qp+an2a6vJiNu/eiSEua+AvWhBA7RRO9BNpdDR4XR0jC5Kod6CGxfkUKYEkhhygi5BQANAjK5E0rNfJJj//t9+lfx60DhMOr9uNe82h0d6U0Iq4r9awWt/NB7STQiCjs7tTToaDT59XkEeiDthYEBkuOVjeez1NVHTYq6EMNEB4F77q19GcB1Fb2fQ4p5xrCMTr715m6zWxhhdp4sU1QWzkxgRC3XgHDuyiDprOyKx7cDnRyNz3V1o4YDI4gZUeNDB4daTyECi/CASIqEsBMov9Mw1IzTmnsMGHJjgBIW+uKYjmHL7Mo40rcKZSPV1tNEtkVgbCc8azGI3Gq+wQVKWDBSXB9zgWeuo/axxcnSTQtPUZQcptcdB1UaKwQv6hqABErBbBvwGbNSj9piN6m5Znzcv1tcZO14p6PJVr6juOI0Rr1ENSJVqVmHyAJJNlu3v1LX0z5y3znX4jx833+Wr3wC9caLG/f3pPO01FLmU4D6mzoJ3tfgxBqopQ7Wv1iMycd0Yh/laVO9H00/rjSITs05G52q5Lq9vB2IRoz9Py7yaAKO/ul3/9CFfTH8/7/93AtTOcflh89Whd71Jv0dIardXp8aGtVteXisUNj027LTuoudvp3eb6oldNzDyUi3xoV4KM2rKsZDWE+brKnP6B7PJV4d8+z/+5R+eLpc7k8Gz9RYDzowAqei1dqP9T8/bx6cn/TGIP3lipAWD1Y5IBslns3ol1gK+Xqp+gAdnKKEexZHS5FZf0xAGPDVbSRLZU8rG8df/RUQdqr4NHohG+GKg8GRDYq+GB37/WNii6W87d9+2/7Nfuo9/793hnER/W8t6ubx/N396doO5sWn3t+nHT3E/lByh4urYSIeKvPkTB0E9F4mvlaQAJQAoZyDAn8gf6dVFUr0hkA8O0qB5mXS6RWuXRb3RcNp/9818s9FI4KLXnnvuD7VEj0QWSYXnsDTjlKMaX0q2amKzht+afwQ6zsl2yIWy/JFw96InAKFM61DVCRHyciHUDOoiCATgqofrQgi5hQ4PkaJNLM8A20VbHsHu+vHPl+NKW9dDqQI2nYy5mjqtC5V05NB0i6V02uLKahmYFtGFIxTaiBXNJUtlALj7prtW2bKTzLflstUffFwBJ2HVMXi+q2NbseZ4FwxfO7lNu/tssy3243T6/TKLm5TSOh/Y9W25QajUWFaMQjHyPag8o2sNRwqlURdDW7eqN8572MkHSyplpMrRvsi6DtXTlMddVU4ksXFJBTaaa2HHEql80Hl9OWEx3ny1WL5dTKvsv+6bGd/cl8hKJyT+CLe+nSE3XV62u0OUZXBD1YutKD+hqwyOcPjShvqKoAfDxctpLf3MRk1UtRlldMhfi6Pew1Ueuvx5V2kaKjz+pXFe4yg+HSaz3tPjR30Y893q4WEoYp5P5zQSAaTnLCvmoLGNZtbGlsGbw9+Nev0ICKGcxRH78GDkpKHRpJ3MceWlCyoLFNVlAQN9sc6qZ8y+lrSSDwU0SLq3m3+3Of/+c+O/4UKer//Lv+n/86fjDy9OUaR9yJkiONmN3fePkLRvfzNe77LXVTYySkOcRjQ7rds30yu2At1FYN1w1CWjZUClw6PoSsOxR85KoFThJrmDICs5hI3GYN4XvwASdexZLRvvfjdM79VNHN797qv0bVK8VMOHxNGOgfCbbDTBbtJQtKFXRDDZjL41oZYHCZILmI9oSDwRa/MdAgEjFNiL8rGgo745WtHky9GCIrnR82Se7taZVeFLtgJGiQAmSLUdjzw2YWm2fP1UTV5v7prFq9+znhQCWwbogdQE7S3ayvoJ/H9o9I0TKm0NLRZjkPb7Rlci4stqMFBRG5+X+1+2T9z1Yv+S9scGXoiWFnY9puu2siLTJcq8j5eiSAfNX14f++3+fNB9KcrXLMcKjbigWN1KpFbnbHvaJu1fcoP3et9yFFCwE1GUPlEntY+PZrpnK+kIi7PKP38zmqjQBw//sN8yqm3w7265OXYm0OOP5+M7OEG/8W/+Zv4ffswa22LxcPPbct1uFK8fK2hP+rvxPz825u/GWqZPTtk6xePuPUwfnjFvwRI8w6S52n28ZVSarReUPQUQ5qHN7pqbNe3ffM5SCfBFd13k0wQZ9vk139YTykIzZLlsSGd625jMpiCDU6YvXXtxNxvNZ0FvqjWsHSUATPfkfhJNodF+ACJGxAls6uGNKm65MainBJbRpRwCGxQ7sFBASNIvMGgJwYMYEKiFAyM1bnm4JGhX5KvV+vl8/fdGgP7lNPuq/X/6347+z//X7PvncFZ1/f3ut/evL1stMu9/NVEC9/nnvZnp41FfdpbXbcTmYGbejeAHY+E4G+qJRBtoLHM0Zo1gaaOfFY1NzurgiukGrONrK4ETHzv888G4u9/o8Rd0GlEr1T69H7d6s0rSoHl+/OPH2cNYqeRueSCaw/EQQMzN530NUgQqY5BsAgBbrRnxpfMJmt4Dw0AOmrpKcRqlB3iFlzx/Hs++8R7joYSto2HHaKw8K4VVBvj0ZxNiyIuY34/Drz/k3/ALFpOn6f2k1yvW+apA7r/OZkMRNKOi5UcTkgSFjIbVMVTdbJwIJJQ+NU+ba+uWROv886VblvZ1Tj5KXK83GSx2648xOwYzIbAyg7Oui9k7KkyLNARS0+7vJ/P54rsfHz+OG9mon3yMLhjaugWg8l9hE5c7x+qSdBmdddvpu+y70S56Yj5Ab6Sb3cdm9tMWImstXO0yTx/4WOsIIUQwJ/ydobhnPO9uIj0kAUeFVJ/2ndFD8qtfTzEFwemP6V3j9DI/FwD+bJbczHuLXmO/NozzNNOR8X7aSua9Z5VgbG53zNM+ddkRvbA6h6Uqmk21I4u6uU6rqrEzrKNVPmqv23qm5aqGvhaIQ3L0g2u/lCTNzHjr9cbtf734Zt4aNYxShrfpUuCpoojnOGitmt2Mc5mJ9M7SBu1mml4aY3hBlVWIBnBBJ+q1US7OQUVu9zXe5v1IQjW42lr9aT+920R4YOfmN6PN6+6wUYXZYMdpZjWqo5EeBJV+TM5c49PlTbv5v/93g//Lf7ws3t893N+M7wY//8un9N/cde4700P67m/MWTt897tfvX78vF1vUu6CoYeonqfjt4vG+3d+Pmp8JtHSSgxVNPTlqinwP37fWWUheUZ42hXciNvZwCHlpIOlOgOJlMYi7QkXb++STz8+luudpZ0thrpvtE5GWwTGFLbLOCv/UVQEZeKymovZVp4a7Q563SHplx8MaAgM0InhcXpUBS3WDJLjdrP5YTb/zd3dG5cVijgKzV7Gi+K5JEDMCJxUs6PI63Zz+HS9fPWv/t3s3MyXnzS3hOc58Nc+e1bamPZ4MoDrVxdacEu4S3PEGpPpzAhKzh58MOn15g07oduHlDzr53B2B0Yk8fOanfvFQ7X7wJuS3lcU9Xm1vJv9erfZC0ZGCZQ2CCA304flp7+TyvVXuTBmYyg4XjaTl8Pj++mNCRk5ePO8GnbWVtFgyL98HH41f572l6aCl1nn2p8Z8CE/Nnq33fzEDm7pxfNhcqWvy86vZtP/5zrc5u3hCr5USvovL5f/+e110h6Xp82n5uL1sPhvF4BWWTbjw/Yrgwmvp23KUdTEpzhnrxa8GuLHnTVOlBYkSU+rNQmDlnWplGP+KwDUrrkHaqXXPL/c300/F4Yn6oKtTz3D0W7sLzIN7Vn/pbrOm8Xv3i76F5TANEND4g7KMXdX485renkBanFZBNAHKbwahcDu+OmDEQOKfdRYn7d9lBBzxdpnTUr3xgqavNHar/YUvQUMiy9BrEjo2nj33Tfrl7+NGlIVEjH1NpKp3359c9hsf66KP/SaZdkofjr96tvW/+F/9VXyzf9CfPPh6Yf/2X/zP/kff/673739w7CXrtavUkfTYXI7725fxzo6cD6anep3v5p8PcW725zyfXuSxlzyQEwigdRvHd4+9PY/6cbU6EyEc9hVwUX7+rvZjoKl3na68SGXtCfzBbU4MO5kkXAdv/nujrOc4zFADacmLPJlfTag+nBKufi+e2gTdkF8SZTGC2sALLqsAFVm4ZLRCNE83VDC+6JcrrafZW/ev3/7+rpWtb5svTwXVTLSkEFjwDALxfH8pji+9No/Z+fxpTU9V5v1+s3Du5enV+J3HnBBOeXlTCDeHT19/+MljVZrQTuK2nmBDpeQ2hcOHuaKQ4TChqY731HFisUY6QKDjK+j+2uHqZGU5Zilr6tPhhHPxjdF/vm1wM683Wer2ejmkr75ef86HQzmk3efsj/2W+nzZPr1Zv8SQ1W6t+++rZ5/+ths3e2373//326+f/xcHZ5G0cHp7XzytCRtw2QQbDd6BqrDm6V7Iics8/ERLOBm9dYEho/T5Npani5/XJ6/SoNor1/cuTN9bgxPq+1e76CXavNL3n+fVqPex3P3G/kUuWy1/5fqnnpsHfZFruQBMphdjiwsx4v85rvjX/5hmY7ad/d3o2/uIFZr2SKz4i+K9xwsoQJftu17J+Gcdv74px/uDBybP+TIPcXmYX6ct55inAaCjC3da/JxAH9ZQtZDLPYwTk4X9TbUS7vsn9lIu46YL7DVLFB7CEoL3+HXv//t4+7TAYTsmJkROui9fXv3U/XLeJbqwSSmXJcXHY46087HP3/6g9hXdNht7k7pr5PX7PrLz8uMQ/b3H//0MH/3bvGr7x//5aeieDcYZaXKof77X/22d1jp/oiC+P7rb4x/klE6Z5/r+psD0Y8hE59216QFmmcKZVPAIjFVoaZ/QrEpjEGSrncv3/3+1mB7OVVZ09ii6eD5T8uH384MXHBHN4tkMZ+VhqhBpDU2Tjh1IDdz5oyRmXXWGzsbAXsE9Y2NlktdkR7fh8fAFRDaCIoMvbz9/un7RvNF6Z9KXO48TPCrr2+DR3fSwHu+Xz5+U53zm5Gsyu188fHTx9dDNZ6ORmmzHCXPy1fW4d1cKLg69dKn9VanTKH4MMY48cEHjA5EHFYBwvi57vrP+MAp1Gr2R3N+tsTAt/e/el3+DFcHP05Qo0SjJfagoDkbj98a1gVB/LT5tFmukEtfqgNg1gQyR9f0jQ/r7VAD22+/O+3WP7Wvr6fGM0r26fBxMno3bBW/ffNP3e1dcvu6fL1tdd/fpB+ec0Kj7Qr3AXcPrtTXv7HVnmhFu8r2UAp+GiiCjYwiE0vVReDu7GW2rs1y0P3navi2le8Ox3cdzVta27EGucNdVTzmygCONzC0wL3RU2/wFQ1J6mixi/SnJ0TuWxov2wP87f2/fWsKJdYrkHXEbeskeXBjevcTHNYu3gQL+DbpL6MV8O0vKsfXv0Cc/qc3O7N09tW4uc0khxvFsb0/77C9sqxtjgGAPJx5kiR0Uw8hTGZMtOPrwIbxW7TDjP4o6WDcOGlunLbm2b64mY0XeH2X4+zdV+jZ1O34fvy3//QpgsdmJcY6vkl2xUmkZVbaC7ZWeZyu/z5pf7MU9O5eHyb3f/z5b398+tgazbW9fswhAslNf/fu/oqGvjPmZf05/2ETiV0uWadSNx3o2lKnjWiStdkjpyDfxG+Nsz3UcCcQ3hPgizZukx4M1GBDRRjn9mg+2GTSuijo4hM4ZmM6SY1uTFq75fY6uhuZHWe3hLNaYmPj6ebNP4Iu2ZD1eR/NIaIFp0qgyEvwnVebj6PRjTh4Ohz+svy8PBWw2LtF8u6bsRQEas/z63Exnd6f9hzMLYJ08zKdDV7/+HPehE+8I2S/vhn+ojGrbEB3njV7p235/PzYTdsLw3EN6pLsHCTZuYI4sgcjTeqz9i6AV0M2ECFyru54dB805HTGmZODkOwY9rEFIKYIXYc892iv3739XVGsgUg6qH0qttsy/3Y2b11UT625mIAmcNPmOpAxUKD3YfUo+aSrGuLk3/345+Ziss4yw7H5fo/r8uubwZtp+8N6NzHOrjf/5fVTUeYjo5RVGrSknq8idSx22pPvCr/qvh8PVMaDFdrdFN2+3Lyaa/2nqNJrzXBautAinmuEWUWjlxwvqBMvrLhprIcd27cyvuycLzrNyaoQm/6M5rEYv/1mzCzoJHTePa/LPddHHz1oXaTxeiNBFA1Av0z605d8vVq+5p+3/en0v7zvGBj1Ut2eu4uk/CcMh8ZlALhZ8jyWu/ObEXDqLwN/sXh6qWGjS71KX6ZX3QEj6x69AE/5QYVAdzT5+fkxJhJOe/9WvVxeLADxaW/1Zvq8XiteuX0zlBH9boG3N/hTZ/v9sPnm0MGtFgY+N++PyeR5uX7Ji+/GNz/98s92bnNo8z+L82FbnX/XXyXff19ujtQOAsfhP/5HYtm/jZ4lnfuU1b1+ysEunoX2esOD1o2ve8FEM3b7nHJZGmyzXh7q4G/e34SeJxQ+3FKi3Uhax3dvRrKBBsHpJKLY9u3ddPXLc9XpjJL+553oJx9MjQi4gxLDQ1n26f3bX15/WVfbG8WEwWeNvrOg2rLa/vT0l387ebCVZbl5g2bbHjx39ViuBrPu/ljdjtM3ACvR3yRlkbrbp/vZLfLisvtBtiVM1tmhvf62c8nbu49qE7v95fK5qoq7+/cIdufydVu3HD1x/wwaA3b35zcajJ6b2vEvD1m5XSeTe24pVtFewyo5wabakqqmbO4j3RsJP2299j89/hkK9F3z/DdF7793bgwMD1exu11/noxR2FSp03bnsjdVRGzx5PPPxosdj9jUM6H0ZJw7bNxOnX20UsTSpAl1CutPDcmVTh5PHPA4lB2WBmaMc2mM8slWGPelI9Hlsrp2lfTp9mxejo3BhErc382wkTSGhoXVhVp3nRacAnrDvkuYSFmw+Krb9AibNnqjwUXlRQnjn88WrZmqZanUd4tv7/iA+GaN42MGejB7fng/vOHvQzPLbKWF0virbw/jfXe8mAxXy8P9wtvTocKhs9B2cFMOprvrSYvq6HCz37/MZ2/FU8xts7MLl3esHsnp4pAaIRW53OgQfNrkhh1f+uPJ7bj379uth1/9vjMSsZhG0t6N7/6y/PxJhpjCm9xGHL0t/ngs5rNZ+8OqfMmeD8vnVu+HVTbWAfiwS49ZdzYzYwFu8vNmrb/2XNNSsdFTtekk18WQcNyk0nrq3Qavz6ef/uF5mbT+i0XrFvxWCawbd9PeU3P341mb7qZyd2EdZFE5e1Z25DWS81K/q3DnIIfHfDafAQpe8mw+efvj54/NYbo/bKRwVLOALoLvCNriuPDwz8dZt52X2Z8//YmKBSxmDS3JIdV+GVVzH9afJSUjD3HYC4X1yB9W17LbVNadiCSGbcn5XuNgJtwWOWa9QrwzVGGfZeNbUBynZnS+7p8anb1C7rzsT0qGS8fWYb87G43Pxw0GvuZJt/0jigzfST4ixgB12msof3+4ziD6I9lCKd2kP/3549/3BlgJeRrxAvjeuLrHuM1ooJN+zhHuGn8s9g/L3uAWU6thWFjNwrmhFsat00dZHfm+aN6bTcc3WoVrQD1uNzZw0Da6RXOLRo4N3h5s7dm+8vgGuOcoIZeD5aahJqqXWgt8c+JxmplH4Eh0sS7w4zLU3exyHfdHq3KpnlBampuDUHIZDNPBXUYbqbquCsPGNvKSUb4KfYo0B9eHwpEsePmwXO+rlBGathojKHNhxtK6okKOWoTyv2G1YWeanexQbsunQDCg5bz3cBu6eeP4MBz+WDR/3X3uXtd/2u1Gk4WeAFUyKBuDIml9mBnqQDqSY7qYq/4YHH+4li9lY9qEr53XSuMcYgjhftucjdfQunZjhqAyGl22y0a7uKBJgbCbx8m8N2oOHt79F6/Fxmg+lvmX509TmYRj+ovhCBLs+Vn9Z54+c9ItlR2XZIOgau/05lpMGjqUFb09NdLamjh4Lfr9+fNiCrzsEND9+eeN3KOM5MOf8u3gUp4GMRAovd50bt/97k3/zx//suOKtcba4Qhv9qbSptNduY28m+z1Lnubb4pB+vD2N+2XnxLS9eb+0knWn576uEHTQQzba1zXBmVpMXEpfyyuY7OGL9c///Lz12/fqvHTFfk2nR+aeQ+5vDX44+MP0/GUu7TbfK5M/dIxbZery6v6N52br0eN/OdNMQLoRq4dZNOmL0eDxePmcSZn225uiheJJmmI8d3bw2Ez3Oyug6PqpuH87emyvx53nE9YeH49gIN4XEVL56jXu053e2r8tH6WFn2ALf30lzw77ubr5+Wf8B7ejCdPW2gNRLsy2cZQdcnCdPZ+2FhvtG3pD/6H9mqHOqiL+LGU9uPeaZX3Zj5jv6uYra5CyPy+2cE+kbvesG8GcAOkBT6RGz2/m7cRMeHFnI4or79E9TpP5Hc3D6/7Z4n6zlsZpRNJbQrQjGg7V9mlP5Qzuh5WLresJKl4iT1u/gCNVnkq2DB0DbAl6BoiAfIA+b+dJYyldm+79To3w1K7ot15L/8li47Ymfa3zQGulTvLTsWb1vWl1IUIPgj80ilOdu8UyCNs+7DlFg5Go+gk0zK1afrxcPx42DzM7oxzebn0lpfmfPP5ebqQ49n2WvNj72Ozupu92e+1v27+ftrvBVkerWOPWKHlEghatmh+f9sbTMD8g8bTadrKrztQC3odvyD7/Nxe/HZ+//Zm89GkkZ9NbbtepuDXdnP1++Tyed3ZHOfDhfZ1t60Co2MyGurM8U/F7r8igY3zXy5DefBT79B7339/l+iuCrmJFM7r4Xk+ep00V0PTGapfGqPHVxj/acaQjY/T0UwZ0W+mwzwrlPNIBsnmDbVXpTPNcLjuVtVhNp1qerr88OHPl9ZXsxspxNnoTlxW9YsTvKZV9bPdVgZ5OP4lq1LYUVPReft153hPNocs7XZ15s4WaHHb03U9Tu5f8u23t7f/73/8vyfp8Nyha0Zgpc1+d1ztv7qdj4a33fxJY02xgti8rttBUgwe8PvBYYT8u2xuoB72Ln+V6Lx92YzHu795k7y005cdBdEZdZn9+yXBCvpRdEaScbmTyDlWs/3u91k1+MvTyrJOxp8//DRZ/TT5/f9m/aowxpBgeNdgIdGdfR6lY6lHg1kE5FT1h2lv1E3fzt9+evleQMKai5+ey4ORuyr5025vtdsXVXk3+/pl/SfhDg9NgzRjVb+wZyTveUqL4cJ/8Cf1KEFJ7l0zXQY1rI2iyfvpbQqf2m7uLt01F6LdXMdMABr5uisybfRuuD68Jgwns5SDgJz3yUYQanprjjanDceM6z3uAcJv0+kt6ul21YDOJdf19bIf9AZVIyacoClcO+B/rVv1aMzODGWlaFn0UZq2WhUOyupQMdmB6MFGL6VmW0/Bf0mGDFQzeVLezthcSv5FI+39VBzGjd675vUvGgIj0Z06+hT58Ko1Xh+E+Qm01kCEYtBC5f363e8e17+snz9E3mWVFfmnxz9vO3ptMJYyl9lfsvVKUzv8DRRKaaD3rdbNXp8NWEpLLH7T636DDDjqoaV8uhar13w9GH/YFp/ge8P+u6Q9++rd5F5MO2jmu3eX/WnSaszHZTrq/nFzHY6kJrJj+dgdPnQNU+vuYtpJ9iJtl7xtNNaDgoAOpZIc+B2en5wQtKSlMHCYj/Jrvudn5/n+X91/fdwV25fPigxmaH5Hk61Po8Vkl0nrt+/eT55XH+SP1ATgk5M/MLyc01N1nPWGP20/zlvT8Wj200qDs+rt6P50QXkpjYwRJe+fVzpZjZvHm7T7ck6l1vHHgFir7fO3X4/+7bD6brC79IrBZPB9magT4TbEjN6HiXw+p27Z2slJmyAh662Qm7MRFTLXCxGjdGHZf9is5jt8nmv5uN/864VCnfX3f/kmubu7+VV+/NTaP2GMQmkMRTDtF7UTnRUEzEQvkslsMlmtCRpQIyGEvH2cCeSQnH5tt1JtTVqS789KWdQYcq43BxrEoKNgp8AMYqScjjIx86u6SwerXMdmmEH5VGwlqLiRncGqFG3keSPfvr7mZW/27qgp7lkRWPRe6PTGSLHokzrQ5Bcge4y60aZIGGB6k0y6ATBm4LjfzzzT6vXtCBEqv+8br93bXrePl+Nra3SDjAlR0TFJxH0tRX0q16CMjwCD03nSrSI5f+1DUF5XL28eHgZqpglIEIuUDRZ3uIHdqYwGUgzG3VepMtv0E/sgjNYwb9DvFcW/aLXWaDHHOy025PPpCcPRBsVnc9G0/pp/g31JW9xMu+gEwuLl8/ZpU765mwhyBsp93ty1ZzdyYru9DMdytH/sjmZZaz/a8/tH5+5hHU1Wd9v2Q/L6+nmiufvgCVttulB89YfR5WH268E5Y12a3XFzPoWhna8/NE7FQ2Pb+93gn/ed70+L7PQ65fOBrOY3s9H0SW1p8VQ07lJnQEeldt9Qht1uCQ7vD99OFjfl/nW5eum2jr1x+m2GtFaWq9X9N7/H2Pi7v3yPtTafTTT+7m4PbE7r/f39ZLF6NvfHIb7k211rDgY/6yGPQizb9/SKs/tyv3j3sv3EJN9P0u8//4DTNL39djG9O2cZH+w/W+wXzfX/rUwL0SaMQ8jXLfuHZ10vTRninH5t/Fez9ZqbZxzzoT+cL/fj8VDvs+VqhGkj4wia7lHfzlTUMhz7x+mgN9+u3/+4fuk1P08GnTe9/Lb3+viXuckwiwWc+jff/puXf3gUxeaHHG6zmNx+eH6GQw578mRKX4d3wxsjkTTbEQIttx+ChIGaZUaggEu/tcMFmY2Tsld72NKUYISxg9+MoIMapD+uxwATgwDGA62QlLjrB9zc8l8rnW3ZmVOnWC1Hl77u2uNTZ3QZoSmfW1pad0YD4AzS5u7QWQzGb1DounRJf/QKpoXa6r11zcwl7qfzrH2en3bPJ+PNwF7nYStdVsYDq7Zs/EoZC1/Y4PQOovZq1mk7MMbm7S7ltN9foY8goiALnS2bpND6fj5cZtsztzIZH9rnJ92fVJoazxo8eL2wo+paKfKpfcSQs0DQfVVhwObN6fKgR+N4sYVZYk1S/9dym5u+cX47v1P6cyxexeDlSR1Gtek1R5y5YdfYt87Xs2r9dF1+OI7HP+93enSPW7MH3FbZkmQ2mBNxLSz+It+001bMCIeBnpN8h9Yc3/aweZPeTUaLiySO0hw8n3aBNNwYzx3vY/bC4Rnz5pFq92AuBSUriFA1nethqoPtI/eltbw7Ps7mv65ao7EuOJfLDQ7W7W+yci+ploxvj4fXpAn7Nj2V+1SmQqNvvtNt8gXnptW9zPrIoze94pw0NmgzozQCkkP29o79679sdR3YtzoPfHVEoEV/BAVfzL6S+/3x45+GaSqw3UkOnPNv724WI+Hp81fdozG6x0Z/0i6+G+ZYDNPGJkqc6A2Cd758212vkt6fL/hP1y3j2O6h+fvFot0RfRpKpCZ5mk77aI3nDJND00MubLk+7N/3H28GawQt1qU6Dq+9X8p8sPw5v36SoOd56ImPv2m0C5egOsoKAmpv/vkvH8ubnbnbncNqOvYl3+X5i4oLp0XKQfocNJQA/YwU3sWs7PzMy5bq4tsfwlezX2FMADX9UW/4tF0JsbaXHB1mRCidGpjZ4yJFCZ13h53X5QTz+XR90+o/tbWda5yxnPT1KjMd+XinZLk/ms0GnfSYjzJJzdYwCk2Og+H0V53qnzfFz0d0rq1e2EKgjeTFuVg0W7+qmpk+GZo39DVpopcbm2tbG3RYdHOgJkArxmjpBsaTQ4FnspMYMUnvgmImgSOjCZ7LotHsuXvN5sNxXpbfNldfjRt5iw92VoC/22k3/TxS0SdN2srVv3x+/dHgHE0t7m++7esfqYsMpstltK4Oq/YEGeZXqbH0zcKU1Usxb+XoaJf1D4tmdwcmmU95Y+ZUSgSti8wwkRGKErnPC3NVj5OHcodsfvnN4m4eEwSPj8CyqnMnvd/OCbsapOgpZkZsA859flltssFDa7B938mS+/RRNl2Gm78vVisO/7s/dP/w5u4/vbbW5rcZdSryTSdwPQVvEZ/hSqOrtm9391n+88vuZfe0/ulTNmtO356e/5QfTAIY9lpTOO0AM1k7hn76VaP8w1etu2N/l53+oZMPF7P7/nV5OI6+mmmO+pz9cmmNZg//WfOyKxvbxfDuuHk5nqk6KPzi8+Hy0Nzfd7W67/93o+OvlJHgqfJFGqbMR6V0vsqqbrprLTRMQKTZda6bUtP7UgZbMcqlXGPFD7ucW+VSO02QEVTxXgZZe5X2n6aj5+YwRzu8dt4ul5c3bx2z1eZzEWCZjhOpLqSqlTk5k7SX7yF3PaNO06Fp3oadmZHAIf8lSb/pDB/2F7Xq+2gXYHLdcLHTRbpxuhnS/RpjXxbDWWaMEIdKI8KiHGj5nK0WkaIfirsS+S88oKPYdd1R1aOYulJRC+uaTLbZstgtF+PZ3aX9zqHbKPgtngZdhObm/vMbgcJghAb4Rqm6+qCs1FusVXYMaJ1Mrqlidt2wWoZeH9dVgeuGFIYS9PFy3GFSFaWB4IPJjD63/6w5cY+0gvSIrv/nw006jvGm8KRyhxJYmTAVBNeT9hRGv5qKE+3dlRl1jDdIjuWawcSikPpqop9sP9w2Ww+dsrv9OHC0g/VcTpv5Y396aM82gpfrRs2/RTExeWRmLObRobOjAzSkOFxu+xdEkM3jZdZef3WTnh9unq7H11LjJsE97VGuqv3d7FuA6QFkiiyl//A4kSgR8qXXQ755Xp9+uGtNqtayqfcBKMxTpeJR+xkse6d7na25jdvT5W48a/ZPmjuJDiXn/9278b//7dfPl2+VQUzL4sOTvpIDXIC7yR1WsVLV02FlJOilN+5N5u1Z+a7XfOkNPy2f+tlp1r1MF7fHxmS9/BdO8914ZEyJpgfzQf8P05HkzPGnp+/uh1eddJrbx37jw9UNPMq66MSf7T92kFbU1OV82v2pN1YBcTZkszxlGli2TmL9Bw2QdYA0F3W/08c1+0s1AsnMevshhCfi7uK4no9kfk6GgYrZ+MM7kprEkdDYHvVsMn7QT39kZOpN//P7m6fJBFYOYpzl5YRzO3OKEl+B+sbRnU1me/OPFeTROO32VzdfaTCFNz8ftzElI0Wrvux0yYuPSXLP5G55xsdMyxlsWxHrJOiMw/NBUvnyuqZgteIKxAjbAiM80YQGUQxHfpCsdk9Pm62IOR1+87L5cVtgCkFGgXrVuDO5ybYr1Yjb4eDdcDTXuWLVbGVVMzkk4GE95HADth/7FeYR34NrjVrXOz/vFWUVk3aejJw53ZnwpUCDgKN5D2K++aXR2l6O75Yl6uh5NkY71UVDAk/qpijXbu2W49IZr447aCwaBWVwbnV2iBJ6nLbaBYKKp8FcB2kPosWdNNzfH3t3r6XHe2k1jFf4/Ww4zY7SZyaRcVxOvWSv68sITzLp7PezwZDPT6QYhBsTPc7b0XQEvN3z6psJwssErf7NzyeI5/V0c5vM0IpOycv4zb+8/FDQ4Q3JJyRiZdaFNLYpt2/H95tM1dw5O2A1lN8lxV276ErrvOaqkKSrmilwifxLhmmlpJKi5JGUzcEyWk607s47o7AGYyYw+U/LWRFWBOMp1SoUO/PaZhjymsHskIi1zLDS6m/S+HbymK+BA/ok48HPJm+b/Qka5WjY3+mE1Rx0GqtWs9Ix4R863T8YmM7H0g1ZIv/pCUMZpvBW549Jb9Pur7NXhXvi1NO5ABpcy1+u0wfIeHf/6fOl/c8Ch1ZrY86F0zJWmJJ39md9M5PF5DIf/OPm8sN6vZhC/k5ItwofF3r7nBvLougm3Arbjtusxj2q3njhaaPaPgyWnZu0jV5M8WmsZmIo9KkxNW99v09T87mOu3wl7tRnblCsh7M5GdgelrKBuuoTqUxBZ3Wi2kWux+ZyPvwKwirqBZcoDpyMoxutarGiqviZOPXGYGLxUD/4X+B3E/Uc1E8vv0wmc7nJmxRpYqeyV+sLOjiof1ywVrXS6hwHqntgfqpfZDVG7E9rDVnML9NTD/s3Ws93cRWnKpF2Pz5fn08uSOHRjFB6wIORNMhaQiHdrTlGJuTAMuUm+WqfmlL+WR9ciJYow2lmgcw9gLLTv7/9Gr2uWVRq9OXrGA4j0TB7dM2WTMnwSTngUfKSS5iYu8CfBxmYQJx2h/h1miv8UA0W6lP66Z466M0nw9mRS3jYJe3TTap/dnddab/e2z3/0r/9bnM69/mpvcnjcI0ldZc83M9/2+7sBodNjBbJfmlL9PWHlXgPEn8eQAPb1b5bPlYHRamXN8ovDs+BNGeqNTf3CCkyErrCrNE0rqYOXsoL+2L7mbvGyAyT24dpeyRKOZ6fTq1ptRk95zEifPax86uv16ZJXdTsMq73Q1Q2GU9VBOCHZnM61NHPVLCjOUJFeRholjG9LXYf78c36kZQhxw4Xya/PhENMThAjW5LOfkf10UnTb+95fP0wl1+0fSprTsBisiD0Gv1zzhHRWdybczWrD9UtVVG7rw//2Nz8HyqtBxMJ8kP3RtKr3fof3O40TEimzb++Xz85VFT6XQx7uC2ceVknNR+0iwYFBgZ6viL03Vqbfuodz2Ub63yrCGSx6gfCn4yvMFNLbjWskiC8+t1MFXW3j1lK8bOWEij19fGozQtJ0L5FYzbNabIMeio8xOioCcW2f6130Zqmcpk6/PehOiFTX63LB55RHjOykdmyUSKQO3TUEd2ZLBDofu6ce97lJYh/La/D+O36nZ1ZdgROn2sJWZpOeyjTAoGJ0J1Dxx6MHuQnCozBQOAH2gnPHGChUhbNPqb1cfN6bWa3g76AFDJC/PmL0rFKnSQNjyyCP7teDwfHDmSB7TMXME7XFVxTWA0dORlgPyIQJGk4gWiLnqJZpP5ie2ZpLN5MlnmJntjPh/tN1FmCpRSzXShu7yU9TRVvS25LQzY+kotps3TS+u0FFCn49vPW7Po1qCZaT+H/+zy06bqfD163y0+YQoC3aazd6MbpISDdMzjab447scCMK3mG+fnThfw4VTS/MPBeMHfEIhft1L6g/aYoUZ0dEAE31+NwJHlZVuZpRnkENVUedTbBJNeDMbFo+CS2wE8tXXeVa/qx/DF/At01zWjM1EXzlu4iRLGKCoWrvGuLgaewcp0y/hx+b2+IK7MF0NynzQd+aylBUIyrfY/b/SUT8fGC/68fKTnFN/CweaH7ONlcm6O3x9eeSYFJvTg+uv7NlL16z5C77vt6p/vd8fFr5u9Wb95QLAw7cF2PvnWZve2n9ufX9rzTvFsxFWrq0J5V6TXVcl9HY5OS6HaNO29ygAfG6U2ZKpDkReQ/zQqIEBlk9g5DY6evOx89g1V/bp72R9K/QmsS5TBBaYZFUOSd+xQzDW5xDQWibfRhHeJd/AyJpimUrIb8gGt0wo4E6U/2lqcXte/DAd32KU0+UhRgFA3Gjjora2bbZQEBkzUaOrtxnzrEsVa3Wl0hZooDsSZuuLxV3Xj02Rozgxmq8/oSxeT0xgDADL4aaDJUVt1kLLR5jHXXS1uHbE4WjkYcZ03+pwct33eQFyUWLUP02ZbVLDT+0PoZFCRTkFH00EsF2o/E661ixq5Y0dcpz4SbkVNRa9mDaSfRNrn4xYbAIFNWS/HbTS56be6eq3MzRmvjpp+LcxWBQB3x1oXSOtgu7Nx53by9UwK6eVxv2zoEzO4JXjb/XoopYhrDwOOXEnyMABFXXuXuW4TSg4UWghZ34/HhO+H18+jcz7FcQcWb/Sn7pyGA7StfM/fuDxMbibJ7fvjbtG7DsfNP7aSsj1O5dSV9AKBNa9R4s3vf+IKEfvouIPTD3/XY0b9XN65LG+Prem0aZoPBMoyHDvXCV95ro5OUcx1/2Nn8YaVXJtfdcjNXFuMpmtdHLJPD93uXQzIWUmgbAcjLY0NTjP3q+qaFzrT/gdNYT6g8htZ7vbIqrS1hRoWJxXo61/S9+W16F7Lm1/f3N0Anqtimf9iPMNiKGsQM6Q3Pzan/+ph8fvL7o+24HT8RWrJETS+YZVLzB3uo3V3Xt68/XY+ujn8+Zq82Vwnq/1GoQXJ070A+SPO+fGsZwOvQZRstMmn1eauG+Rj7X2E8Sf020Z7jxauaXBLvEdxHHvJhMzm5Ws0NEX9S+5k0qdDxEUEwZFyk4mGxr1hIUOqK4LJwBcUOkauuYgZrKPy3MyytVoI7ts9d6enNG8t9zBKfc6Rb8GIbkWj5fEJxbd5vkvVgCWtk2RL9OoeqpU5SaRrYCt3L9bj5jZRPUusEQpBxUS0ggG+9LraLezVmdFdbByGrWOrgQSH65yjunLI2/1uPmg/YR20Gw/KoY6XuSYw1/1LG4QiaKHPTHdpHAIs2Jn5zcY19pHhO007OtEZla6PrMSTakGArLw+cPMAQAxgzX5G8gyRZazGB3tKZkPjie7i3B4TIFkzGWmFwjGqD+7eMXeAWR4YrtEoXxCfuoBzs1Q1kRUTITvcTTrbVVkJ+hNT9PBGpZZgZtpyYWPc6az6ca8muvG2hQp/1+s8ZUvFIUl6n5yLN+M7FkAh6ovSk8N+dO4fZGh1rN8jGwQU19ipU3ZD+J/cblAGergnuCL9ebbheYMdDYSdde6L7KOcV2v6Lnp+nEw90Z1TRuKYpu/eL/DmMmk+quxd53CrpPuAH97ST0EWVhkHOqjWZSqHO2eNS/C1OJFd+Ld9mA5neoWMW91pr7tMhzu989v779sTRNqvZhEKKTM4iAnbY2mOzfz0phPNTjrZj3taubHITe01vwe5TQY0ajkFlhV3nXYfHj60kvtd8hsNZ75//Gc78ybtA7GUJAXgaAACvggXRWYUYsFbLHJVdoPh8G4ypTop7Vf8D5VSnTbMjaMWreGREVIJapVlZSMdm6G17Sz1lg3tHWoE810lb3Q+u/ZG4nmuhXo2rvPtIHW0VoXCh9lWW7bNJyt9yT+7387oTv2ftK+L36Wj67F8ztdayin9UPTlbCIEZc5ipHCrlI+U7zisc4EEam3MmdRrSdkYLwX8R735aJgFhsmQHnbwjOOPFgG2bCTtQt+AjtQ/lDlIlwWzfwDOn4aVESD5YNwcpfIBar3129E6IEd+P9wkx766ZMer1QonpyqwnoBnXZ19o+ragUMHP+mKKjwQ/hyLEqui2iqjaUtqSjI6hwoHr8/yo3wux4pVwsoElU4mb2W3todXbWiUa4/T7jwZf26aNyKa4FgDiAAkBdhoNJy/bF+zcqmw/Hju4FT96pLfrk8t3GLtX5A1d43mcqU9xcLZUclSbt/AfkdvTnk7y/6sZgLVYXfZL6EHHZm27ilFdd6iN15ztDvEwWgOeKjbWuWLweu9CW3nYvXL4JCmXbjrSKjIdNMluIpF9sTbGSS3/JpLteoozDY95ajVkoyoCT3lzbULHt+dj7qNUM6azDjK+2rbMwkdxoVKey2lu6/JXIF/71S9n/Tfa0V/aP3dTy8IPNUgPfban1fFLDCpzkHFKuqJ6FX5Q+OI9amofbv+ER8HyjAK5B4NI9iPiqLg95JTkZAHFr18vozfbKMn5rCBgF+WH/MoWKTdHrAceuZP8GXRQBuq9SpVTq0F5EREN+ronhzmTL1dk+LoDxATUtkVDorO6d3pNH3QTpPG4GtK30STg9axP1KvA3myklapOh4yeX84we6EoJc1u3OYKzqAEHY0mmpFqnmWfIdWXnSmAIoG1Ji3jElFLfGGzgMlRQdcJ5CFqWRIo3g5iYabo8Fspko1pUj5ZFIFdLwKf5cORp5hUnp+7bgxHCm9WfQ4Ye/UYQitmrBInbX09L9iBDk9zM2Z1sVNVAa6VsZ4KVX7mgGGNM0TQYe7GnbQ6rycqnH3NLyAdKTqNAJtmigmV+U9cHfNHEHI40Ey1c5WJ6Vj9rR7NaAbx/1Q7K2nDWkeNyqnYwYbX7KExAQ+yXXX2AOPEjtf2bmhiGk3vZs2t0C2S2Oz24x+3qgsmX5zM54cbjptfMj2ZsV15GLN9jZaLzHQXl8RTnN+aQit1ZvDBQxIbg8Wg9n+5ceX5798OCyNJ8EoPPcnN+dRu8i1z272F/m1M7qiwjYrIzdRyrlmm+Np1t//7qHS+yCXjYuKwd8CxRIikigS3m6ftsVnIUKMztW7gOd0WSkLQYDU8ZQHpXPxKxj4eB65tvBCVlxjNHj9gY0zkVeFF92W4XMOsKA7i7x8GZ0OU7NfjBhpc9tHmaRzu6vxwYoaoIEMDNalmWI58isU7UX745GIbisv9GGuJ/j5+nwQyvI/NbnpJDxM2amDydDXh/Nuctx15v+5OhWN6wkijaggLzmVYxcddl4yzD/jXwTHFNpgs7V827e99m1LyvhUJZcnMyEvTc36TflI5GeazU0O25AXapdskxkWWki1mpmKbhM9muR7F9lPXWzOWhpGOb+Tj4NfnahlYqorQgH+j/QcMj8sBXuLU3WKXaMUsZLPWj/0ZgCZdf7p+dDAmWSZR/qeX3gI0USD9zJVJXTaq1yWe6KvTa8Bf+nsKwUDplG3z4s5iMkV21WY9TqqRvQGN841IeDBirhpQY6yAFeNk24i6Pg5G3M835h7rJ959DMRIPR9PFUF2rzysbUHixassgRr0wByA1eNziO0eKOLQesVWCnyD/glQCW8ZvUVElvhUmupOVyU1UZH3D4Ur9Xa7ZdwALM4sQh5d5JTwUm9FIhAApqGWSSttpJg+dbebcJzHO3RQDcQiLv9STOQ07yLqnuWgeHFzBBDee4tzi/pN6+5dG7K41c39yUF/vjza6fUymV0QHVUiwaUhRUUqOTO6asY3xbwiqFVo74ceOe4K2fpafH1Jdu8HvebY2Pen2yubL1FqZ42LzHMo+AW9yLffNybc303lZBwt8owSRhHhEvXeWke36pAqYA7K1kiZdC4UKpuFT4O+3fD8bwQ3qIwMsONY9bmo+1vt7K949vJQpk4RoE6qJtgBWsVc9Ki+MoruKjb4gTeGGAVqce83GCLzIcZ1yOI8dHAgspDXdF+TqmB+V5qjnbnzurx+8bgDi2tsf1p0SN2QdiWsRa0TpMpXJIaTzupkvhWu1wYjAIZUtLpiKLRalOo+kPTJjCUTkMxJCrnpyjU28rRSNAoXO2pYxZIDvclIAJmA0qrxsNonCioBZ2gH5FeJzh8XRKh13wGBLt+00Z56K4a5U4M1NIZ0nCohubDbu98XMPKWXq1TX0lPsHHQeFURdt3tobXrUGYxtMlXuVui4H5Xie7qIxEqzoEIDJYN1KhGdFUdSeLymDZIHSoKLywMp7BQAFnsBVIpjA2CiK5wjIR0agBmqX61n0DldG74HUIfjnK0n43BrC7Gv2nqshwPnlW3XA7raFy8WjfFm0oJ46f04Y/Izg/hfIRZ0I6BRFYRebOaXmwXf1UHTUB6+p51TjkspJNfbdFRWVLeQOVo00HyPpsigAPFeDbOuDGmQyoY2J52aMF6tAn3LiOoq6sef87TQTau5+mhejr+iyH9em1MMHy/e1D1hg2ukyFSi05mXbHSsC9x0oQrnqLS3RFAVlrz0I+dI8KQK/pI8TieOqhhBsVoTdjJX7YDY6b7ASWlPU78k/MEWrPJVcMlyv0fBKeyO6I/ygp3YgUb+dNIBYGSJln+1FT359OQzers2Ov9sQOyD0LOk6bS+dvX+js1XezDJP8pl2M9wdzet6QbP3LdhuVaHZwEOV3ppecOtn+PCh3qilR2JFwm4Bp1Zmnqm+i0SC837M+9n002NNxt8FZ3W7zTy+jrwyIixlPFgnWQ3JZZkiRAuJokNPQpRPTjyZKgqq2dIglXM0w6edN1XIsaFoZMCR0pkapPcYmqjRxS8S/AMoxJJPI4U5eDjtmiZcmwoa5NaQ6Orp81QP2rkqKL9NUJ+bepcxbz1tIUv++WWog2x4p715vHwc8WNCmpG06DbgIJp4ONa7C3mNPZF58qfoSk5GgpDpZcHyuvCW8Bi0jKOO0iyUeLb6guaxL6txwuFUcIVvWgR6nBSpG7sX+Cs3ZSwedqOLnkMsDcr9ieB/gKx6MITabSfO04UT6ejgtmb/q0h8N4XrlXhedllqLQNCjcz37z9tna8JlU7AA4BIfwEwPum0UhdjD6qxUdRyLKUvdH5fl02dVwv0HsX6DgwTdTaf5ebXfv2b2GULn3twlAA1662ieTSTBH0kRXBQBBzVBozAwwI0OrM3herOYo7B0vnoz/urY/Wl/+nDYRTuJUiuDUbn9jN0zVtmug3YkJow0E3W1G5KCVMEJP+q60llCOxShFu+DJeSztIf8/XZDdJoOcZaOp0/8xlJWEbeoDTYHW/CRrXB1NsBTujRR7962cEckjWEvWSBKAT2VXey1nIosJIh9f3VL0QLhwoxsSrwWWlifodNxvfn9jTm6F1GOLrgWzL2u95fsoH/LxALutbqh7HUR5C2M2t0HBQ7G8+xVN+YRBEMj4a4076zfAadeMwhho30zHP/6m3vn/xPqFm6ZTGtEPTTeFE1I359BgwvZzsmQ9lp6XnQlN/k6l0kpMGsvUh0yEFUbzcmDdok6KauPg5pSbs6SdBAPkKSXlZoRNsT7h5B/vRSHLZ1wm+uz+hSKOtjNvEZ3p7sc6dutyu9/Lr66wfs82eyy4BfBLsZ8PWpykExAKVmuIKeQz9RDFOgfRDgel7Yo2fHwtAMunPJLpSubgzxAp3K+uUhgMi0Og+9JZgSD6gw5bCjOEqZRakgPOfveCcb5wrZjxc5Sv9RFtSPLtp7JMrg4Bot4G78zdCqcQNmYnJoZksodQXkEOlT/2bRU30TUJc7EMoA6StE8MuXk+HkRNzYgXywR783i5XqhIIVrG3ceivDpLgkKdeHACe4AnwCWy/9sq/GlKaPWLmbFqo3dmKhx3t9fTpPx4txJi0H36a0aoKP2He5nr2FqHyBKoXUX4/nbUTdL8qfV6qAdyTTNb6cUUUoNWwPYf9rad3fd5mQXfbXK8Vk5zEA5hdLKHlWjabM6jGYy4TBftNSMOtgt/rCdEJKdTws9YdKBoV9KwU86kqUjhwrKrLblpFPTSUepHWB6KpmcLCAqZfUYYwUPKmzPPY0Sujf7/PUEvhAiHlsDaWOiXp7+Jaac9OeiZYp9J1t2Bu19doTNO1f168PH/dCumsVWMc16sNke5vZOmaLSPuS2C4PdHopyCUGnJS3A+23d330jenx8/FHaoNtH7FQdSXtI3AQNNnBCgezmF96JXkYOEEF2VygwsPcUsj9IuAyzTthwnvpNkn7VuyxNcwB+XoagBZpTBzcVqCakGpuqUJbm67ZL5sCWmtIEbqEhxcxjZzTaznk4lcBHVeoF7zkdYuwSXKE7MRfGQFTJgZ3x81GZ+KnShUmNXVPN4gnZzHHTEInPgQel/r3dWFfXSSfI1pHc1V38wFjBBEic+xBky0mnQ+QNeTX5C+VUwTcXe+j4RMMGB8wCsQqyALqUDDkzIpeOMV9okEdpEiWuegeIFqJpmUI+zhGAXzKgo4yIpOrzIy0qgwcAMqA1OlcJi7kBzo4sXPhj0UdNwYK4pUMrXCJPuueDyRa2Tju1dqHfqWG94sBtnWh7hHQThhQ6c0Z9olCvqq6CZuFUCVAQMhrn/Xgcbf3yvSa6eWcoF6p3szx7MC4hD630fv5+eXna56vRZKJyi8I6J7P1diNBL6Ol9Fp1avRzZ3U7g7DLmr+IaxsiYY1zB1LvTivCvL5vkSSYz5P8VTpXHLMZocuz40dlh3oaslYM++HYwq/xCNKLgPzCaAnzIHQhwFk5N+4nb6FipfZnOCO7z42LSnk1SokhyAnXA7A8SH7qDLZ4o5rkXVqfj83P+WWPRCWglq+iONF5dCYw2lXf7piAJL4jZ/xgSJ4izP12XxjpSQtY4353KhF57J6W++W1fIYyGKkubmmOxuph1f+v82W8kVIYTgq9xyixZiLVKoRPMI6Gw9Xj7jwxqCwG5Y2ksUI/9m96525h+Otp4zReqx71z4XVihb1xxpqMCgjwXvsptqoSSkAk2PKurU/6TtlMgsAQTuna3d+q0HSiQFjR7QU1YmkuZccY1OPKksbybStEqwbKU0RgZCRWVC8LLo/XrbUJF1M6bbVZWmzfDhvsx1fAEYCr3WCBtFpkd9MWPkpnP0jf127JNeQYlPuIxQE4UhuBDQUHS+JnQ+BVYm6vOi5fzxhA4gyLTZ2g6lR+2YDJaZs9kQhdMHEWZVHPgcCSPubIrHab9unw1iUbK4jdwU9JkKXqEyW0Qd3Ambl+vEOnUoE74iypa51woueS8bBKggbmYcGF6JO2lpqe1WzRrnuvrwgHZBSJ5VeGqIMrmsl2Q1qEPkAbcEQQ02xDGEhfksmtFXdTW5061ARy2mhGMg/wK6BUTcYayjRuibAtf5xxfJ6RjfawY+PB9kPAiYd9zsDEYTwg0oBBcLDaNfbbWP8ehGQ5LraWzCnHXgqrlQmATmnStDi9Fmym53mckPKe9ee86xdfaOTInrsLYNVYsdBIsoCyUf0vMsq6pNyX+k61Gq82UD/ep9H3Y+7YkSUxpqaIPJN4fGvZaWihplUmFGVLwMh/Fm7ChVAwsIRQanOG1VH8tZa6jd1umDEMSgi4aWtHaQ812sa6sZEFAW/rJkmeO5zLCrYPArstdWbjwdv335rDBEYXc9zyOPcqTsWG50TzpkA6qksX5HDPdE5LJKmUSJPp0snrO7gHeb7WevRstzt8azp/DaeTzgnwpWLXtWwMp2J3zQg8dsV8dvDHIWfWuw7ATmAxLYGG1FwdXf/pp0jOuQpzFSPlr6ypEnPsKVzOb6H5Ue87DiFNaTxIjfBzWwH1y2obbqaQBrJIPo0K3I2wF28nGDLsNXqVVQc0PzXYwvkjtelFALjVB9XTjw8Bd+NN4EvTHIne0Fkm38L1pKUszQ0LofTeUsGQ2GN5gLUBlb9lTZLgQNcrCiBg67wMCgrCr191C0E34B3y40TpgoaLp5BtFIySgAFbSZ8ierkTpDttNjrdVL9OmRXRKMR5yjEFwINJ4Aqbp5orFfmqah4uGCbxEa8OOG03I1OWRKDOnobaym+1fYrGo1qvntteYsOQgMlkX6+7qedyyzFQlALnpwTDZgu6XXOo0JuoCN4QoJTtETUJ12Akn2hQYJmhdtpr9R3g1/cofhnGkBsIc/qWcyYcKuNQ4qZpaJqs9rt/nz/fvFmOgjyZfnaPew6BqOUiaRNhlcrZYbW25uAro150CtdAAdI/5yZU3Le4PfpHNmCo0pZYFblm1N340x3qnO3odMtlZ8KepzsahtzOPozTAIMAkVV0RunZc0UcxSbVpFJfEUXUm3NoW0mWAlYchvXGd6AncVFHEhhOXnhP5jpzZbbm8G7WxxogYpCdJ7/Wkvadk8NpLzLQR27UJ/PG8GuKKy1vHYzNZ/gHDThcqurqZLa2JDTHiVOwhR2rPkdWnojoqegpEznYypY2byMASeqfTG78ah1JA2IQ6U7PK0O1TV6EUZ7SmZKe8b6YWljfzW/GmaiWRegnx7X0o44gbM9JTMTwI4dhLRGdEw7MTG8EIQhng2MRw+wKM90HsITQyBm+shdWwkVL1Rf7fZOg4hGoTDGqwhBLxulId3FJBmnkhRawPAHJUxHWiNKwjtYcvmDKnqKsQZa+kd6SR8gg0KMi40gqafh0jG7FDgdVU8+WvyiOjlmswVQCimplNqc2IQL3KQokVT3JX+JxY2qGgakEYeav3XYwI9ifw3sbt9Fw/tLftwvyQlIRLgRWLTGVfzuY86/VgeB6207QVECKbjZBhp7LqXKAFbWdtpVe6xuBMaFuCJJ3XEYGMcSz4XuDGwF6JmK7SbHS7JRmXRdvulXM98+7IqiERksvLa1GvsFxqcw86DPS2ui2aSWNKbsCGCwxYd+ZYZRRGrnhItPragvjadgfFBz5/NTrpaA1zVVqvqnBGCvngdtOYG+R1G38Z7b5+12xVEO6gY5OxfkvT2SuCxtGv4ZNyt6reOwhHvUkUHeFidNM6PDGNdWcxAzErvT8toHadM1irf4qmIB7Xyw1usO60oqddgpdk1d+KN3KVN3PhXP7alWaHr86764xIsIlAvKj43ZGPab9z0pz87zCXaK87YnYEFHcD2NBzWR5y+bAOTIASHtZGsSVNZirdwcfq8BI0QaGHnfbqRZ/oBO5SRHGwjeKVQWRxOeIM1K2/TGKmWbu+hZOWhWw2FHRimgHpeGgkWUHvwDnhyUAggHIpUTDESeNyx0DTXG3Ze34rK7SVVJfB+fQa6IP2rRE/pO45XXfXOdR/eIMV2pfVExSnK0BN+huGiKm3K+ZgrwhSkq+QNM1FHYeIZo/x1xiP4g4RVZg5YuCZqvanES510c3JKpkxhogSZEvdolC8wz+3axdRDBbUxiuh2y1r3DdtY9LsbXRIe+MG28jsRoGkZKv5QEioO0qifbcFgp3NNp1exlxgaBqa+m8YoJQhyFZroOHTpnPVt6ksrRUWxTlc9EE6nYgCj5IpNnjlepHIkwzfBYS06fXtqF1ActAQcfTFM22PS31hbRu1oPu88d83AAQHxP+UPJbqdUp1sOjSplLKEM5NdsTy7ndeDH+tqo8qbmL0EZoKS7yCBX9chdpz6aXitI02hG1b+yrwa02cIMz9p+HaHMBNaNDPZepqrO1cig1f6bnMHdfUI315T4SB8guSslDDHKxBJpKn7pn3f58uUll41BXji1U/RaDFFTeIP1JEo/81RFOT3tq/RYRdPB5Q2oh/cfTQG3VdD7dApRb0icKjJch4IHSVLNkORfycBRE85q2GXWu8vGAJt8ECOgMOF5C2PGtkIi7k7syOX0almQKFHnpmOxlqJ4fSU1MFF+25oHJJi89zT6/pDmCqVclVNM4lGUvJXhS4apUoNYrvNztY2xA4OZ4JK+k1VWHEnWWDXKXw1HzKbhVAg8tZEVDUG6QweoW+OToiBpLBysvlBSHP9ImznNzEZx1v+udRQd7g8auu/U8gyHlOZVldwCXCZrVjcdOHLxzKB51p4qaEkyz9R3VlTYGntapocJ2x7i39NwLJ72DcdqfyrgGXp+lBxhgVa0d5HT0DfnIGshL8AUcLvOPdhLlMoms7EpE4OH3mU2JdEekMN/XR2PT8Jd9lMcrb9lrshww2NqREO2ORMhupZ3Dlar57+mRLlTvUDrNOblFjaRO6BS14MivFTfQv17ZSYjBUnnEGazM1UhpMymoFwzmylqNyJ0b7i7dg/5qnFaHTS/er8gqxKueo9zTFEcREu+2arDBYyJdvw5QdqeCOrAzck4AlX7gT7UvUgi4TSiUaUjrNjGINs9yT63OtppwQEtV/TZwvrSYHCQyEbBwpyp4VIkWxTTkRnDRvgUVI9cjNphgK475MvqFiWRyiDb3Wl/Qu0GmpnvT7tM3QdOhvPthhQCZIHRKVknstG36PZyvBHnXzunSGgjlrZzzdT4Zrz4oN5wERpaDCKxVKdNREpd0Q3tx6dQZsLGMxONpcSQ4U7Rvkoa4iAdoAZwz4NHJT4ejXaccCc9p94LGqDTAeVqnOBE9E/n7c1l/3DWULO9MptLdKuxvlAZvsgC5PIdl63mF40R/MY6X1RDIUqXPPqRIwd0k4ZhszW5ptlNyaaMAma2AzRfp3kYNlo3IhNm+ooLgnFQe7WNQ1eemCcNi4oKXMIvjIipqRJgIiTdjnZuWC4eUUUmXv/vZICxSnRogU6Zj48tzBATSZZxBMmtTtJbfkKk6oJ4zAPcGxJvD7hDkW07NYpsLRvCBJn7y0AkjhXIItdAn3GQQfO8sXuM8o2w8lqqqZyab6/T0Ul2NRiawhWsDqd8L/biV/IAYqaMb0Dr4XVAl+2A7B5DL2sarXk8g5IFXbfamh8zb8Cvo9TmIKUQIG5ILFKnQAVcJcGMu3P/NjzaLp1HRblITIp9a1q0kT0Uw6GT7kfy00H2EToQUuzCwOOkZmJKE68bosWXieIcJXUARfPY1AmSOIqwPGbYpBNpVPqca9GWSIh5ebJEXX1MUL6CNQWhBsVncYgMxlLUej5oqiWuUiYxHk1dfJerV6VWO8NxegQJGzXjC9TtXLXNuYyiJeDIKZeWhqZQswHyuUseEU3hxpRZSTnQRJ32m/P1BjVNSksSBArS62x0l5TapxPPytFGnGY9Jih+/oQACssA1IcfQ27itEbHLuZk8gIG0MpCL7dkcC9acoS0jqA1ThkvAOLu88SAjYXwJAxhnD3Zz3TWPD9o4cQC9xu7VvunaFLl7fLBeWFJ7YjWD/0FEmnwnLmL9AviqtKLtmMPGWkAZnmGqMZCzkrHO8i/FpZwFZXntpjcKDfDtpOVCWFpVFUSjJe2olY5PKo3GBpyTJT7RSfKKaBA81AIld4Ng85WkNrS/zFSSVcMzGMGUjDGDeDDoBokPu0NubcRwAh6DPVhjh1y5o9sgQOgDO7HEhA+KCff1EJbVF0kYu7EiaDJnva1V9QD9CRj25wlnbcjlcHCqgyxLjIEhlVie4NsB8gahLlD/rSsElndjYM6Jz105G2B/SwNodSdQ1MM5qaVk74JFBw5RVsw5lbDDEVwjEQUC4QrSlo6fWoRuUGughBbK+ovOHqbYp8cnpQzjpUMyLQ29io7ov4BCRuacb2uqwo7BVhEk7HjwQb3+eDpKNwXXHYEH4VFD6BzICEAxEM+FBrBnVGX7YtkWHB3FDBfMviJWFrae19pHwu9lWCB9ff00eDYiPFlbfVB0UdNzw0TZyQsWXf3scduaTU1TdA2jQ4fGF5ijmMMX2D/8dL7gkfUGsjspjovWsc/DBoTuUqqQCqtbI9wvDTp3bAzzS4geDgo5ukzUqs2raWhG0is3F6YuyfUA8S8wDL6pzAfMTtB6TqZRDieHA1UJcrQSZ1llfbwC3iNVpW2YbA9uBSAUL5R0O96HQpBNKNjle0nHGDs743LR/IM1QEpcdLw//jTFAMEIG7WU+oMgutOEXPGxGfOBMsRYH6ttUAozi27dfHk+tiqNDluIJZBX9Adjuo0BFwrej1SdNmuz5QDB9gXgYauD8fJU0U3RlEEW8GNlx5vXgo58obvlAosujqv0MKM6G7bGhXX27DuAXRK93a6GXqnoAL99QomYbBk/l0qssvUBlizBhrDTaAeYZb484CqSCXgOChfOHTDLUBOh85GO5gu9uCFO705RUeWhEPdUmwKLNnXM/ZwvoJd3zmyrgfkHg4W55uthju2G/fd0w2EVwyPytUxTYzEtLaOcISIOCrBCuH5SL3Q33BXWQqnTcoZjGn7PxCR6/5u/3FY7Zl3sab+p3Iveev0ekXhPbwKoOmrQDNJAQUSg2IonVzzmyY8igPIJUFiZfLkEOQrm/tj028XsAa4yFUZ8NruScj4n75PiocwywR0GlRJp/QwXiiMRkvjNBhCXqzU4NpyRyUAja5Bh3J0N+O5Qro9bJVOe8WSnUxE8YedtFDVp0loA0ZLvf1VZWzxXfNwG61GVej2IvCkPg1A2B4a+9CBjkIyLMfd82jcepVbRF/vDC0RResRsWRIXJewRO9Ax1W8xWM3GLhKtUwU9fkqergWBqEpRUETko3jJfPaEHnv3BqBuk7lqzTiARrfVdC1wi5gVbinoaBZGJGpFWwfeSMcfRoSFVaNJVoOO+JYskE2FM7EhJ+PpkY1tTcLqJVLEfabL+QO+ycnLypKmsuTtFeTntahK+xFeDvRVUxxTFT99UpwvDVh0YK4IjfY7R70NwAfobypSWW7tCeC59QWL1Q1R0GZakBix1Nn30hX5/4ULsH9inkvzI44x4J5kKghEpzQcs5HdAaJ4VFRMAnwltKK3oXu2MKNEeWxDozO5EgfT3vtZk8N+YjAfyKNqBfL1biolQePnIBjpfNYB2TIBT/4Qm6Jox1Pmaetg9OCwGBC6QCKGt0afS9X/aQ8kcuvG189piEAGcgwNRbazwIiA1IyYihN5TUY5GKiBjTzsY5GQs9GY6ZSTSM/M/haBxUbpekempqSIMGmREFLthBV0w0GEQHEyYfh6bAPrk6rmZyoeV5MqjV7lI6OpucsXaTH7Zd8r6och5EwABuUPZlw0G2ZLT0hfmxs9NIUzofxNjCKXBlJJG0y0GFb5ziED71slB4ORybEQIaX25NpdQoanYLOrHO+73eH6pojCMXTFxPATqgY9gp1zhlpaqwPS+mej+NdxQ4/IsnLTwjlCUfUeFIpWCwQIRUHFR6xScTQRDbZzCf4qUpF30vrifoiHyehosqc6w33C7FVO6XrWzMZzLbZhjG0ZdpikC5PNSBtAmcYVL851jhe9hAFU0wDVW+Y3NhTk41pfjjlJJT8qBG1eNwvcXMnEe35UsvNdfT2yL3i+4gqOE7iCB1swuZDYOgZQAuHHNhC+UpuRAewiKwRQGQqDIKi7x0KuGaIg4jGods6gyC+iGYNiGFNolG3SDrwErTw8P3ES0A9VNXI51GoWhb6TBmxblAObUtQ3EyZj3kPzFG8YRieUfflmtpzDV7l4Y1b0C+GjbBgWx/sdoKXQtapyCEafJPqZWDE+rTr3NEVgijVDhPK6Fh5LbYkcPgYTkzIUIi9MAGsGMgQwBHpg8M12JxkBo2xCPpaTU5xWHVbsleRjrRUcutWs05o6RaqFE6LlzMAn8oi8ZwCQjHjt3SGtIlYKooVLtf9VaMkWj1o+YrHbRBwDfBGbLQMw7tkfwT7wk8Xh0rL8vDwSXtAIYYOB5MZmUOgaI+CVKv2IzRXgOjqIrFjG7kWZlbcw7U6m92+p5JBKWMjKfvzywyRYsdCWEQpIdsVDYZbHfjL3IASiAdyi0PCLCepiUcn5ehGNMUYJCwV1CBlAbq1MmbH5NxA5LgdjTdKuCQ6oCm0YUwnRPK4GN0sc2cBTwp/+NZ2Odxz62ySNeHjH5g2ScCiAJKpNQnIXnAEt9kade7YHmi1d2gla7Xe2kQIFo/nDw6Aj7STCWWvdIMx6g+3BPusXy3/9zAwlKk7wDOVGKBNSf8BBKwndX/SkVC0ZfX+ocES3FDe7qGpoQUsFhufYqVRwI60PzsBNZKGVCQZW8Rfj15bjDV9JaBFxqdvBa1RjcApamvNGcqc5A6cL/IbZkowL5r136Aecy0iOtasxQARkteFz8vXCwjkTWTEQfMS/UNo3sXsCxDXhU/LEYBq+pa2upAVuBo+3TFUJ6q/pISRCgSR1J7WEvQ3zSI2I3y9ZuLwIp+hEctyA7SN8tNX3izTQL9pBkbXuXNonPpAiLn7ITDsKDdJgM+fpJZMDXGWhO/iLb4QHxPV3qEN3/6SO+YEj2yC2gklSAv1RVZLw1nq1rPiZkWEhrNEmYaTyW9Ma7nmJ0CXaRNJGFaPfx+q4no2MSA4QkC6EXJpvCpYp0q5nGqzNB0Mj6aOviMxE4ieW2LfUGCjgfSFrsK9JIVd7FIxTbObf5kEFQVKEhBJIxlyU7SYrXbr1fb1Gff02v6q1b7jWGJ662LZPnIWpPMb0rPZwfTmYuB1M2KCnktVRF8mbVSSAXpI2roulCIPp3QhAryoxl3lleJWVZrBkXR7AShLtpD3SCvZiwC9Qy49dryupJHYg1/8kdqJdo0rRRttjo2VRgp0Cxnn09phR31qaSgixIfUKaiSUnK6ArrRLs46QiqDBRQOJpUYDOWo+agvHblR2WXQvlbmdsK5izbmXOTW/iSXG0gumNKlxH2EIf7xks/gYztvQYyhFXjDXBd6NzJTkAe7aacjbKeswkvVoJcaCGECVUSNgTDKIbc77CMvP/Q094boygsJfCjkxv+npftQbvNI1jCMSBBgULJd3irf/73tliRmEhn7vEPraH1EEvz/mZ7ur3MP9mVGI7nfKLiuApdxZ72mReh10fuzudL0xYs3nXapslyjgC7v2u9kMMm+ApVrKsZYbRUGrQ36TUicrhwyPLwPhFHbwGxhRvSHscEKtFMOTcFgwjvpPKX5WGm0B7Nkwqi3Eos/31//IPCL45scepc3YUg7kSmhMqQ4g5SjMOYXl6ZVBbE2EEUJiHglcoq17t9oKanH5XNxWisq16Hp1DmZ78KrdGykZXzZ2tSTEidvHHfFg1UN5TSLLm0uOF2B9evQdmrZkKl0NU2myDKV2CKAQFtSISilBM4ZmSai6W5lyz6AYVW3QSgD9eW0jAH+KjP2oUt7r1RdRo1Vvnt8Z9TqXz9/MY5Csul4WJbSNE/8yQ2w7ugWxDV51/wlsRw9T9p5cIpbflPmzSnRmmnfUFBTiEN2k5BDkb9frW6EermTNCIuK2NBbLgqClbd6mTsD4hJOCTXlTwX9SV6xKcqMdb01JRlUAaN2QTewk48CEJROLbN+3czkGJS18wwMZiQ8I6v9qrkR+uIWn/TTuWnjNdQn6Dehfh087toVh4yfVgnTtPZqIfYA/hQunJ4K8OJJGwqGcIwpCD/JhZIBvC+D3esihQKNlXmHkw6FxGRWtqV0ILdYsWZYayjtFG6Hxrkiea1YGiyzriSWh3BIqAjAWYah9iXAuuLRKlvSH/taCQ0gx/EhPKii9QekHb0jPqZwnx0fe23iJGjachJHooNF8er9pUop+Zwf/+2srupMiaSwA2Iv+W+cwPme6SbTO4cs7Gp1j55J2p4JXiouqRkIvTgYDDtFN7KSeoNL8bKqi1aFKXc16fbTnrVZIf15YUzz2AxxgRjbRYqFUQQ9NnRm4owSDqOzehhwPse6HMQVq3YKoRB4NbOgrV+O5AelzdUT8aMEuNy7rtYkF2qgrKWGpXWkrhr3xSh7JD4sNpuxOx5W8ohJM01t10Zc8FHkqMzek633uzqu7IpgNjDVANclNUc5lSM0tX5z4/J49YYTvdT6fSYPTzDD8s0JkELlmsY8Z1eiK3ksaZxY5ZMMMR/0rOj11kYh/BnLzFkLCjEoursmuUdZIxtC+M7m4GIl5XbKW0njBzzlJxgDM6uJc6oz5zwcvklTo8dGd6a3UEM2IeejyMYLf4Dkc+qIcinwlABAxKR0ijwkprF+HCIlUCrILEg6XnVHDt+q0Y47Kv884AjFXcrzhRloUsxLuY2oJc/gfoxhGuqRu+FrRCsWB0z1YfEKBEJ0Z1tp0UgcDp28yFyhMTIwqGGaphDkyiaypK+qCNKCr2G8DheCK0qUVil48cJvisvy3GhRol79g0ShtACiXTO5NYVF4bIM3JEfwQQ3Dk/UJ/EpOrAKTVFgQgkwPbmDF8+5lP5cCVfZiBboo28brXguDcgYSEFmK731wiKPExtu3cr1KI6R3dSSE+igGsYrYHONTxHkGSy4befTk+n47OpS20niCH5b/ptoAUy0LNqA404Tk6NWoIRzHNqE8J4Gdyz9oOiZYOC+RcWlr7Ve0nTAAfsWregHhSR6yBzKhx1y2RYrC2Zu947BdJ0bolSmsOAMvoDV+sXPqB9TKfuZb9XR7OWZhMgiA9LhCGqg5SClDSa3ezXu1+7d7qV//hykvh4UWiUVCIER1UAunoxWCFTt8aNH4cXIW5OAF7U/LB910eHTTk2mBCsYWCndVARLe6uNtnhE4TgDOPm3dQVKroc1eNhbUIv1fcJPFIqisB5Dnuq4AXiC0OVOvLkEryWKCJHI+aQE/pqujN5caskIvLDoSAv1AM/CMpg4MGo9MAZXDHlx1/wkTUyMUW1hMxpQoXXa0brjFzzvv0rmBT2qjKbHNtnJdhqmDmq1lN+Q0ETAa2qjg1rOCTxTOljCBzFhcPWZd5y5uKksDVTHRNf3g2sA2nCSNQWfgqD6Zo4kGDApwA4DaS80UbQiDrz+8qdZIyxth8fOOnqsxnk+gWpJX/LgDnxxJwxlNFT+lENuRZhQVW3mEhCoyghj/m0JOxe3x49i77G7g6bavU2hin1SJqti0CjAzAeg0Jgjxk2GURqmQQwTIjpVq8YlSLSA6PBW67JxalgR7I9ecSgDo+Glg5mafnB6oqbIwLsNChMNSQ4SUDF9oSMyBZYE8PjIEmdGF6hQn1HBXkLlAHt6YlsOyuTcnW3znVS1j2axnTK/qtiDSMNOONZfV+dvq72d93GJ//XcgqeBdTpk2wbjKCkdjZ5AiPHayVDz69PiohMjfngzpx333RKc/JMeYStQuT03nm1w3baPQW5hA35soJhrv90dYqQbqZGyKiQnFlii5pnstUZdSGzkoIuesb8igYDjOrWykmFHQw8E1PqtFUEzMImJ2c11gxopeCWz0XrAjmuTf7AYfGsXd6RwRyxDJKhwB2blVBgF1QhJH1HAlhg2MErHR1jU15K9S9OCMe0C6vYNqTESiWnrlbmh2nKVHlSjJB9Sm7y6sUkiYB9yG7hU/kWEUvJbqbUURhPoiQ+x738B2dfmbmqep9zsAVqPrcs0sfQyr9g64O/tCR5zSmECjRbeoNREFwwEYmNj1AvnqTrKstdLhbcbQgzQX+5HB+EusikFXmudQNWdIxJ0MXXMFtrx3Z29XoRpmROyevFLcQML76926u2jsLNQ9ukcIR8R5k+gmuuBca4Jd9WeVlZjAKY8IN4WXOE4LBQmCO6gCHWlQkU+TRKOfXbbhJuDyteIRtdeAAEZn11tky5lINzOmzLtlF+ospdU5x3j0HxPQIQCp6qA3QDhqAhU5nqlv6DMtYnN8l6FAjVBMJr2m2fCPifN8c/XfS6OFEadGGGXZbwwGdrVknDrkyw3vkRyx9rswu3z3vTn5ylWldt41yDVyzl1aleS76Tn1GS000CS+UBukEa44aam4KmtkF1FR6ihNFeMbkDVFVOaWBCikCMXF7QcadqKSAsBfWxL3qZ6yADBBZZ3XUCqeETB7c+kYDUglwUVnbC3GE60wWiohlCeN3fjSmz9j1pou6DMU0EkdX+BE1BaZXqQ4UxTkQz2T9078k1Adqi+WuZg1sRdJW90/f55OHq8lxsSp2YQlMCDfTTW1CZxVOEBfA4sIInyD96mYrGiKq0khCVjexCGscekFkSgcwW9NTQuXyGO+9ETSrAtEcay2f6UcsM3Qg1dgV1ci4MOtvHmzLvqCxRseH7Ph0vvw+HZx/U2VzWAFensbAaJhL1FNjj+ykTWF2rred7UAfy8jodLQQHY2yoIzADyLyYTtMRq4GJ+tWZ7aWE+V38dkCuZWFrnGp5CphRBFev7AmlnTdNN1AKj2Be6sJ5EwssVgS67fTOodtsT6ic/VynKqmzCNzIZ1AaDZCvDSvLgONE0cZOkphYHp6QGBdpUQilU4NBeNB7m9Balj5kp68QBQ4JeLEUDrvv19tvOrDqVOJqmPtWRw6/jJEVz4c3lLZdeD6VCBT8/DS6fd15ABKFjjlLS+41vWNEkzJD6vz94xVq2L27e8HX2XVSLZV/CNucsNnhIGTUXtEt2mK6HnMF7BuGxetpmcpwedPe1r+deacBoeVbmWYoinVGlpdXg5KdknCBed8ODEEdgP/BOZaRr5kz+LuTlD/D706ujDxmZcR7FFhs7QTYcfhgwVWGv6gG8927auBg5xKXvWru6yPXWzWqFNLpQSnShYzU7nO4mKUj0ht+sF7VPOEzydStmrZISjJZIYzYgk7lsbm/AJt1e2TKKfqTeRHHcuke+KOgRci1V7YUPpNBxNQEJI41GTKiIL4g6CKVWIFvY41ShaJsWF38WvX908fp9+tJVxfy+OV1wULGp1MkkCK3vjO7VQYpJaICYvbswi1U0UerH59uyvRmH4FMg7q+BIyIY1CKmm6y7kXKH+SIhBjgpljx4mJMlerRrB8QgOuhrjCZluvZldo0/OXMc/yIsXRHgmwpNQJZTktMBjKAqDw33YpzwlEwycRzvBiJ7DGAMCu0peLbhIRrRVknUx/XiubVo2qfJ1pFmxioO4HYhKfQip+a7lYcgsslkazZyNR1D7QcQnN+bXymQNCAsizIwRj5KL7A/CpBRTPnnBiujLtcx63hCAEQbHTHaHBAUE8YG+cym5vIoygCzIV4dL7Mg+AjM4Ez6pRvpqf78wFUiTmamKnJgUpQE9A9SVnBCWFcmrUZgAvU+WbYl1ZKk3PT1IbKqRFERkXML1TvBfaUwBRjhXuOG+9jI2Q0tRbNPIWcUaIo68N0R4EZfBQe4UhWWfYCYwn32CG3NHTyY4aYyp8rVWnz013BhVDyl8Jdtf4NLudvZbPdzNV/X24yVE+Pp9OD9lI8ibgOC/1My6EWjo2eUOmVwpJ9ozQCniZWSAHlBPpObcJmvdg758kWCEAVBRxWNkbB0Exb1giD0LAFPv1BErqqXy0Ab+yL8w5Lik+op9HYvxHXMKY9h729qq+4YZzJ5s6fXQjCZmRpB6gQ3qnTEkYs3X27/1tduHEsRn1ggNEqLV7lYJIJEUMLgJWb2dm88NsGdi95TswueRa+p7WlCpyO4U9YHDu76gtkRu8lBocOw0BJAljkTOISBPKebkFzuLiXmwV8MaE3pWStkzeDbIovVWftXNv4TS9frHt6UpB1sZ2d6UK9LC/CIoMGggMRsQXXk5HnMbEIJgEqnPPX4/RDkRSX0K+xv8gXhgQw/hNfVC1sQJwbjq2OPnQEYjImjFQs0aS1uA+o1mlY/JSZqf0HGbAx+95/yNLd9VTx6fpyujvtv5o5RBz1Tpkz6j6OmTuolYpBBxJD29uz97Zo+IAEhRzLMdAYXgX4Amyck7HQR7xEFBsnE5Om6EiQZS37jJ33GH+DezkGW4NQbFqpX3+QqQekZYa0exb6WBtmfCUdGfUq4YCK31GKUxaJ4eNjTKv78X73cBJWEWTcypDNY3adImY5nb/v3x8nhwc3mdaDArrk1LPhKV6mtknWuSdoJUZEq7cscf1CY44gwmvUnBz4N2xb5o7VsO3K9OchyrA4HuKrQo+bQAJi3pML/JgihiTfzxj/ptYAGT6HoJHf4Urjbfxt9k3hvNni19mNWYEDeoEP21Q1SlR4O3c3P+5v/0TkD8Ov9i+hRL+uSEvDxzgtWV31YPrjdE8KvwzH3EowGpqiTf7GRSS5GbXQAdvwlomfA8ar4DTeVn29dAODvQsM0IuCMyoI9m/q/Maoi84aYEHQeMn5kmBL0BBy+rq8/KGQhqBNdeGxyKbYyCgiCG+1Qj3haCAtCOnR+Xv+J4pnhcwkFncKxhxGJbLUZniNW2FjtMTn8gmDawoyyH4fMykIYUcj1OWJOlJ4HQxL5Q+25GDpMAMMbgooSfUUmYPyDOm/woj5zUJN+1nTLUVK0iztTdYSgC0qBVei93E4PMHEjjrHFI+gFhfTccF9qB+HowuGxI6sCXGVUXZn82lPYNo2c2zpO+siQYGM8xgyENz6Bg1h4BaQAqNZezbU64JwDO7JPpTxIJYUbxWQamIiL4QkIo+YCj+Hl31xXZVmb04kE0TnlR3r/FdkJUivK0I0yZQhhoLOZtN2FPpJKjMxVK7CNI7vrdhxzbjUDmGE6OE6641ywrMzfpYwZE4A9zC3yel3SP3BLzxIWmH7EWMSkNP+zbqly+Tjbm5cXCeD9WCS3mT7Vfv6sl40tuaAA0DnLoLpr+P84WDo0OV2flEZLa9Q7JaYqF0RAS1oHFPqbSWCUSSFTqJwtkoYHpgMhSI5sepPj3ZPLNkYw2cr/xG0BKTYhBEByQBHxoL9eJAfOcXb1fEPPcFebr7TcWqI4TurJMPU2dMEtlsVAZERa4FN+XyQzjV7s/0XNaJqocLDhERgRCkwC1fRFYtF+bEliCwyYK2ZrahBCIYoR3WECjTUd4FonXs8BikMO2PDYBZqX0y11RMAclXVGUhkorO5fF/4Qe7I9pfKqb1caX3zOYsFfORqX21utakpvNA+7rCPp38Ol39cUBV+BF7qFfhabixUrarQnPpTsmiuvewHKw4f/E9WziOZYAMJMycdPohDR/+M0S0sYrJa2Q+kVDCDDqwsFRywwwN0ASaZ5f4xkCWLathECQvKCPI4H6/IQMJh8b0D9iHW0RB85lDtIKQ/8SEZUu6YgLZ1IkibdSRn432YncJ1qFxnLKxd9LGUPzWjxhS2yW1ZJcNK4hMJxTekybC8Hkb2ruiLVIUzZs6jd24bFsM20qJsg3zEEa+3NGE+C0CHSl6cGJtIXl2wEX7UT3W1KyRgCYhhMdWe/eZezyXzzZzxfE5IUMmAeThNfu6XmjxIEdebcASsNk6VD60oOP2xeyBqBsmI0cWy1cH6TK1yyDDVSKB4SSGGe0MmVyDds11ct8XiWRNSD/gyJybumytMqt6k/CBDAqLKBSwu6tm/yGE6WRxo4Wzgy+F7VQy2edhOTw8CEBJ/pnLYCOAW2i3IogaWPeDhJMeCIeKKIbRhWM/0STW/HVNmGcntWusYy9cw3+OWZnbQr/oht5oUXZYsfMg75bioycIiNUghP6rzh7BjCTm/Ys+4RiKgjCcnxMk6WvcByAOatcnjRhcrMVRT3mjrHHld32bHH5fzX6r0hYcZbf7S8mCMXUEKVUfgDOHhuck9ns7PSccg3H8HtfFlhEwrRXJXtMEI6AV0ipk76fyh1FSOKMrWH8ViDiEwFpbCe7E5lZLMeNo4GwyMamXTHcX4wgdSKeRgfCaEyULLOIFvDp3YOR1bph6jUn2lU0WapKUbPMhGhnMfHLreHdO2KoPvzJW+YbEQJVREWMamtp7ZBrUEMZNuM7uYr5aAUZYlcUGdNe6wDcOlQM6kESPMFovAKYjUSeQ4FDERQDuLJanMNgRIYEsZS9JmtfjDQb4N30pvsuaSOIM9yfzau71QoCx42+ucagar1CEa1MEConiZc5cD2obuQ2EZ6KtDEJHFXC4igHuGBxDPxcp2d/cHKJFKgyJ4TkwG8+u4MP0ZAypicfmfcUrx1ztKzJoK5j66u7wZZAMpJRxhrGnfhE48CwQg/n13FlLZmjwkR06iKNZzNzE+P4cOX6JUpwcz1PNoE+73IYg3VAJVtWUWLH9gZQ8KDXjAzsRMhZ9PdMB0rQ3CHXGS+c6esyCcBvISAiZQdOwFBQybo3I2KveoFwN7eLWGfQ3y7INwm1KAiswFTFr3myZpd20etxsCRtsQ7VBNFBQdS/3nQJnU713+IlT1lIZuVqD/H6p47UpfCzdrdg+5SLNn5wnMLk8ng8BYxEoVOHYWqLFEjQVmMHF68F9NSxXqBO3ogPVhgDf6gwvDqej26d44kBRkZZIkHBV8PwO9Zg2E8FliMOyWsieDdYSvc6OrNUOgZs2yjcBfLr3vS/ZYppjKo1gV6QKNhFu40zqwdlYYJ0Lxl5DxzXDw6ImVjqF0VvjdgTqFYrakxcGZc1q5XqfNGCb+pMm9I6klJlY2Ipkhq8TbBuN7i0ni/WFkgO3LUUaYtOhXBQpoh2SvcjW0LJSuKPBKUS5J6CFOGmpiZw6sbxR5Q2KWtH2gATGQ+NJuSHcwvrswzuiBaplkiAXPpepTuQbpatRuaphV+HK2/xCa4UhTVqZIcmA2Zjk74lEM7GScsRkKwp2QP5yk54UslQ1NJt8tWJpYYwMnDnaoA5sc1/IzFmNlWMMOHLgCqunm7/Xyq0LsXLKuLhentQI0AROuglYSN5+8b3QUmQT96/BrP93MFl+gUWn9SF8Rr3O3cT4Kwc7EcchsVDYZtezE4oPBtPVapFInchaEgwxqMxRcCAjx0XpPw8pIFTXIQj9eFKARG1yAsZYa7gyMEVMDSuG2N+BChY8NK8E5OgFwPhtL+MHvOV3HS3RwlbPtTjRDd9hsxGL77tpxMquUUeif/exXvFKkJR/Rr41fxhJtEDUK1QHq/pmIkCsf8+9YChdTnMQiZdiaAkirZ/AhTHJEu/F4vE1WRsdgDTBVPEQ9T3d46AQwgkHjCYAlnC+lg5OKQJcV9TKTrFJm9SyMyI86ukmFHPRENl0jTEYtHP7OV0Q9sG0n/Cq/hyXBWZFjYji8hjAPa1q+RmRbyQi0JF8LtyKGm+M9F4L5dNagIxbPQlfUYkWw48BMx+5PuwgyPIMUEubcFAYLBQXALKywW4vxx6l7DegIXWDdCOxkduoQucMyBa7MBHjfmtkvw6gMiB00k7YbYCDGTgl7FNFhAakOYBgUXRVnjgzpFEGIkqRazGVLyA/esKXKW8hMvQCW6LJmI2kM/mQXlY2Zm4BrCItAl8a7aKU47PyRAzQ/miKxUrrxv/P5t4Fz06svvAWmZe58YbAMRgKsCQXYZOLkildnD0cGrjghZ+YPnEOoTN8UjB1tzs+3Mb8IuCp2Y/zMARMtAfuxMmwQJ0DypGhpEhC2Mwio+N1aRlchE7ojvHIKyudw2FC+3XIBmQtaQRWkHkR3wZ8aOoWhqRJ8y/U2boubypU2yci8GQnwStKIbuydJei/RAODf5o8/kUchjxA4nGE4kZQAA+1z6QmeUCK3I/BTvFfEoz+SfCwCIU8Mkp8CsPbHDKJluGq2LnAOny0K4DgO4x5qqJiPj5etcacDa0qfgO6g5sSKxx8XjJBSlfCsdri82wsnkSxHP3NFxTnrigDPTwLpYq9tDf7GoU0lgy0PxeGAHyhouvWYTmtyrwdL0jdaawrxu1XccBQyMkb8iRQVeTGiISKk9MhkgUgUFDKx7NoKGWQ4/MINaTG9x3J2aAS/c3C88yOUuJa15cZJ0kOIocyuCpDhQCsqYhSXUbSe6/HOoLUp5fgO51lcvcsCJPVqlAQohu8KOAp3CFf0T6lSE1znLptyPyFBX8cewrhLdwyrm+OZWVU7zp5ZaUZDnY0T5v9swLdMmSSBXVU5EU2eRYLUIQLqUqEhT8oIbbKNUelq0Kn3eqS/2kZ2KLzzdRURPgmtSMkaFWX6Zvvi+JltvBhwIczt8GsedqCEFmW9AT2pcal3DtAfg4d58r1TIVBVKqbx9KFcVHEbFFO+yZv11OQ8qif4V501YRHLkzBA6mu2O6VBuhYhtDEzlE24I6sMSWQyr+EWaSTdCSCNG/MbkuW4//5Y7l4r5MdOBo3dOZOLinoJ/kHjhHR0EeOYLCFz5MBzOqBRU29NC50slgOw14ZEyuSSZn4ZmychQP92JFOerLWVoAvMJ+VsZ7TUdSQmiXlXaC6XQ3bTmCUIKWpyeunSkszVJhUk8NIuNoU4gbeVFXRBS8EwJ5TSDbHyc9jW0AzHkFcw/Yk3bejwOeTfV8APNmHE/mKNl4kp8SF5xOPdkEaZ+pvOAp4XxJAibDkBBPQLoP96mFYlmiNmDn2Bp1yWM2hEC4bcGF7uRmhj8EO0nNa0HxYjVcWnZXXk+GHeK8vcM14ksIwxYWXxVY5mDC9PJko+/blMPvFOpqt/4C68isuC2yGjE2YJ6sNDuVAV7Wp2ELFl6ShoJCnL6Qz0RiTZAY04zcnytXlHH2Vhg1nyxMteS8Ao0ztKH/hsU4DOvC/dIWh3V741NGKBVN3rskRWMFzeVQeSNdpSds7rRJQn5noOB7DIqgh/GQBFp7fOHLsaq4JBsEJacmegq6NxWQFixeluKUFyCKyRBgC489gFRT3JcI6V+zuuMb/DYUU0hPywfqDvbOlIEWkRW8/9UNhhOGJUykdsRV2gv0khRF6Y8veUqlP0uZxEVkmOCWA5WIVrWZ5vb4NHMWC3BWgg/lNHM3Ufn6n6jZ+VkS0RfzeovJbAa81FSeK0T6JIOFqWBApxAe4kiz7zSH0ONw+PtVWGg1TDiFDYkUaKW/fSeBbRqeV9GamO6ribL4WP7G1TPBOwoL6MrJYc/3vhQZ9t58IRPitQdpwwdKxDV6iqEQzSJyK7lc7lCTKWaRsxC3SA4XdU80VRbjzzV+s5zCHCmWIgFjPscXkNk0Ce7VZyvJyDuBPKdjOQFQnnEBoJaH77q7yZHktw94+uCiULbeBoCkkGEzDM3BojDGowZ7WzmpjsiJCFgpEYBkjulyPpB2zjFpzyiLuLLWmxtXUYouggvaEK9RGyCJISysLdIsBURplJ1sXw5WDh8gNJqPSxSdFkMpVZZ3mK5sBL3SRnTq0XejhBSkIXjU+ZlRooYD6zEk1BBAW1VmDz97Gk2tE7uXGQmoUrmYDDTg/TYph76ZnmTmBdWKM2GHL+OP1gZnzSiDaYtQelgfqDMz0G8Fa7Iw/MFU8UNddOWaLGzJiA2y+dpjUwjW878P/AirFUBA2pUF27arBOLaISyBMkTJj0jT0NqOqkXzCRTjM/ymjIVV+AXbh/gSuv3wj72nRbBZGjFhSjF6kDiTX1VCYhpHAVoYPuOxTi/lQtrqvHEoPsFmmVwVawKoj9HSnNVImUSMdYRd5VMoofQht7B42+Vcqh6y1X49KHHuOdw3HIPCSU3K3GEKIzZ1rJigRB1I1L9aa4dEBVtakZ2RCgLfUNQUhFtYrel/NPkChEwwCRrA+b0uNCaEZU8/EcDwnWazkW+5eMtLQs9V5uzKLWQhOBxdGEunlfum8EDAOkPgKFuOUcYzw3VDRETqbZSn0RFIZMfrZhjqGa4ysYJMSZ/eAPrEKcuVYbNkGMBIaltoe5m5FDXABElqnp2Y6xhf+2AtDN1/NNlPbjHUYz6hADga0/ryCPaGIzzAcnVjlGzi52Aoetpo+79BY+cTDHQqP9hIcNJFNlIxUMDVxvkfHH+N049vcPF8nQ+Hc+Ee7GsfuB86QEdwBer61OfxiJr6EmVjfv5wZYmcSeF7uMiKOrRGPoT0GW6QXAarDG1ick5jX5NvBeDyDY8uTb+fH59VuZ4hRk2tirt5quPEgq4Oi0BxPjOLP0Pg+w2rMY9CWXkiSNVEpkTJOeVavAa69AYsTPWdjR46Lj+6lYSl+0zxRMtEzxnZ9bAg2RmLaVt0ZViUjMAEc2ip9mKlgO6jhf20Wt/aPXhQPeTq2z2pCJcZoMM+MydGxDdPktW7KTdgaR1fwCqgLmOM5SzYOiN7Dqa0jeIjnBzugtFgMWiobbP527T34nOcbK8d8bYxKshNnBSpq+GA+k2Yf4gas2eu4Dug2TSPTQM2qVDzIyqPKCMDSGsYQBaF5z15MBvYKtKkIxwXVOUXJh+686An1PR6Khzt43y6giAeNxZcYJkKff2yksyhYy8jyMA93mmxP1LX4YCXu95lsU6TtgOCaG+cIhvLC6otjQ7aj8+jE/CKRTeyp2rkkyU8u0xCtOJ+lUjizwsTs4N7fIpCVYGHZmAlBvLK8jz8D5T85wINB7tAVMaPVxO1ZbQQnbe137cFZdfI9uI+M7YESD/flODuklepHgrjdX6HeuDg9/fmY0nj+JZIduneG7MfW+C+wSiSb4pB5H9+jjIcVzmO3YFt44sF0SkkvB5Yw1B9G46MIEaPSUax29fP2VtCwIFd1dqITanVq1POpwikxWvGqQfpIAfucJAL7pnc7deseSgungcW2XeDLrp276jRIk0LvEamd+jW8nRTQumVZuqBHZUHtD35T4n+EdiTRabMh8ahge7ag6BpCZYpDNuacQ7R/i2JVGCEzU0/HK5VKLLammoe6plaqBhcbK4HAQC8Um2nIhRSfrvxJqZ/iu2CBzytEbdtUWk07Awzt31/BB7COo/is9uFOSYb+nHWEc9MLQY1pVpkJPl0Cpvn3RkfF984fLUQ2kCrEQtVPzApvOjjn29n7xaht152z1+OfXo/ORMeTB2v6RpogPvVh0b/YyqlmeNmpvyAGWfm8kXeAtU63IjmePCJFTnHA7eT/7kd2AC0MOGwAAAAASUVORK5CYII=",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "# set image filepath\n",
+ "im_fp = 'assets/dog.png'\n",
+ "\n",
+ "# open the image\n",
+ "im = Image.open(im_fp).convert('RGB')\n",
+ "\n",
+ "# convert to tensor and normalize to [-1, 1] range\n",
+ "x = np.array(im)\n",
+ "x = einops.rearrange(x, 'h w c -> c h w')\n",
+ "x = x / 127.5 - 1\n",
+ "x = torch.tensor(x, dtype=torch.float32)[None]\n",
+ "\n",
+ "print(f\"{'Shape':<10}: {x.shape}\")\n",
+ "print(f\"{'dtype':<10}: {x.dtype}\")\n",
+ "\n",
+ "display(im.resize((256, 256)))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Inference"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Depth : torch.Size([1, 1, 512, 512])\n"
+ ]
+ }
+ ],
+ "source": [
+ "dev = 'cuda:4'\n",
+ "model = model.to(dev)\n",
+ "depth = model.predict_depth(x.to(dev), num_steps=2, ensemble_size=4)\n",
+ "\n",
+ "print(f\"{'Depth':<10}: {depth.shape}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Visualize Result"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "plt.imshow(depth.squeeze().cpu().numpy(), cmap='magma')\n",
+ "plt.show()"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "jenv",
+ "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.11.5"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/external_models/depth-fm/inference.py b/external_models/depth-fm/inference.py
new file mode 100644
index 0000000000000000000000000000000000000000..3a603342b26ef132fce567a09e0592277aed5248
--- /dev/null
+++ b/external_models/depth-fm/inference.py
@@ -0,0 +1,113 @@
+import os
+import torch
+import einops
+import argparse
+import numpy as np
+from PIL import Image
+from PIL.Image import Resampling
+from depthfm import DepthFM
+import matplotlib.pyplot as plt
+
+def get_dtype_from_str(dtype_str):
+ return {"fp32": torch.float32, "fp16": torch.float16, "bf16": torch.bfloat16}[dtype_str]
+
+def resize_max_res(
+ img: Image.Image, max_edge_resolution: int, resample_method=Resampling.BILINEAR
+) -> Image.Image:
+ """
+ Resize image to limit maximum edge length while keeping aspect ratio.
+
+ Args:
+ img (`Image.Image`):
+ Image to be resized.
+ max_edge_resolution (`int`):
+ Maximum edge length (pixel).
+ resample_method (`PIL.Image.Resampling`):
+ Resampling method used to resize images.
+
+ Returns:
+ `Image.Image`: Resized image.
+ """
+ original_width, original_height = img.size
+ downscale_factor = min( max_edge_resolution / original_width, max_edge_resolution / original_height)
+
+ new_width = int(original_width * downscale_factor)
+ new_height = int(original_height * downscale_factor)
+
+ new_width = round(new_width / 64) * 64
+ new_height = round(new_height / 64) * 64
+
+ print(f"Resizing image from {original_width}x{original_height} to {new_width}x{new_height}")
+
+ resized_img = img.resize((new_width, new_height), resample=resample_method)
+ return resized_img, (original_width, original_height)
+
+def load_im(fp, processing_res=-1):
+ assert os.path.exists(fp), f"File not found: {fp}"
+ im = Image.open(fp).convert('RGB')
+ if processing_res < 0:
+ processing_res = max(im.size)
+ im, orig_res = resize_max_res(im, processing_res)
+ x = np.array(im)
+ x = einops.rearrange(x, 'h w c -> c h w')
+ x = x / 127.5 - 1
+ x = torch.tensor(x, dtype=torch.float32)[None]
+ return x, orig_res
+
+
+def main(args):
+ print(f"{'Input':<10}: {args.img}")
+ print(f"{'Steps':<10}: {args.num_steps}")
+ print(f"{'Ensemble':<10}: {args.ensemble_size}")
+
+ # Load the model
+ model = DepthFM(args.ckpt)
+ model.cuda(args.device).eval()
+
+ # Load an image
+ im, orig_res = load_im(args.img, args.processing_res)
+ im = im.cuda(args.device)
+
+ # Generate depth
+ dtype = get_dtype_from_str(args.dtype)
+ model.model.dtype = dtype
+ with torch.autocast(device_type="cuda", dtype=dtype):
+ depth = model.predict_depth(im, num_steps=args.num_steps, ensemble_size=args.ensemble_size)
+ depth = depth.squeeze(0).squeeze(0).cpu().numpy() # (h, w) in [0, 1]
+
+ # Convert depth to [0, 255] range
+ if args.no_color:
+ depth = (depth * 255).astype(np.uint8)
+ else:
+ depth = plt.get_cmap('magma')(depth, bytes=True)[..., :3]
+
+ # Save the depth map
+ depth_fp = args.img + '_depth.png'
+ depth_img = Image.fromarray(depth)
+ if depth_img.size != orig_res:
+ depth_img = depth_img.resize(orig_res, Resampling.BILINEAR)
+ depth_img.save(depth_fp)
+ print(f"==> Saved depth map to {depth_fp}")
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser("DepthFM Inference")
+ parser.add_argument("--img", type=str, default="assets/dog.png",
+ help="Path to the input image")
+ parser.add_argument("--ckpt", type=str, default="checkpoints/depthfm-v1.ckpt",
+ help="Path to the model checkpoint")
+ parser.add_argument("--num_steps", type=int, default=2,
+ help="Number of steps for ODE solver")
+ parser.add_argument("--ensemble_size", type=int, default=4,
+ help="Number of ensemble members")
+ parser.add_argument("--no_color", action="store_true",
+ help="If set, the depth map will be grayscale")
+ parser.add_argument("--device", type=int, default=0,
+ help="GPU to use")
+ parser.add_argument("--processing_res", type=int, default=-1,
+ help="Longer edge of the image will be resized to this resolution. -1 to disable resizing.")
+ parser.add_argument("--dtype", type=str, choices=["fp32", "bf16", "fp16"], default="fp16",
+ help="Run with specific precision. Speeds up inference with subtle loss")
+ args = parser.parse_args()
+
+ main(args)
diff --git a/external_models/depth-fm/requirements.txt b/external_models/depth-fm/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..d60d5f832d00fef8134a08df902e87615de7ce9e
--- /dev/null
+++ b/external_models/depth-fm/requirements.txt
@@ -0,0 +1,10 @@
+numpy==1.26.0
+einops
+omegaconf
+matplotlib
+accelerate>=0.22.0
+torch==2.1.0
+torchdiffeq>=0.2.3
+diffusers==0.26.3
+huggingface_hub==0.25.0
+xformers
\ No newline at end of file
diff --git a/main.py b/main.py
new file mode 100644
index 0000000000000000000000000000000000000000..b851dd806dd7d0800b398d22c7f9dfb439da746a
--- /dev/null
+++ b/main.py
@@ -0,0 +1,192 @@
+import os
+import argparse
+from PIL import Image
+import numpy as np
+import torch
+import torchaudio
+import gc
+from config import LOGS_DIR, OUTPUT_DIR
+from DepthEstimator import DepthEstimator
+from SoundMapper import SoundMapper
+from GenerateAudio import GenerateAudio
+from GenerateCaptions import generate_caption
+from audio_mixer import compose_audio
+
+def main():
+ parser = argparse.ArgumentParser(description="Generate sound from panoramic images")
+ parser.add_argument("--image_dir", type=str, default=LOGS_DIR, help="Directory containing input images")
+ parser.add_argument("--output_dir", type=str, default=OUTPUT_DIR, help="Directory for output files")
+ parser.add_argument("--audio_duration", type=int, default=10, help="Duration of generated audio in seconds")
+ parser.add_argument("--location", type=str, default="52.3436723,4.8529625", help='Location in format "latitude,longitude" (e.g., "40.7128,-74.0060")')
+ parser.add_argument("--view", type=str, default="front", choices=["front", "back", "left", "right"], help="Perspective view to analyze")
+ parser.add_argument("--model", type=str, default="intern_2_5-4B", help="Vision-language model to use for analysis")
+ parser.add_argument("--cpu_only", action="store_true", help="Force CPU usage even if CUDA is available")
+ parser.add_argument("--panoramic", action="store_true", default=False,
+ help="Process panoramic images instead of a single image")
+ args = parser.parse_args()
+
+ lat, lon = args.location.split(",")
+ os.makedirs(args.output_dir, exist_ok=True)
+
+ if args.panoramic:
+ print("-----------Processing panoramic images-----------")
+ # Generate captions for all views at once with panoramic=True
+ view_results = generate_caption(lat, lon, view=args.view, model=args.model,
+ cpu_only=args.cpu_only, panoramic=True)
+ if not view_results:
+ print("Failed to generate captions for panoramic views")
+ return
+
+ sound_mapper = SoundMapper()
+ processed_maps = sound_mapper.process_depth_maps()
+ image_paths = [os.path.join(args.image_dir, f) for f in os.listdir(args.image_dir) if f.endswith(".jpg")]
+
+ # Create audio generator
+ audio_generator = GenerateAudio()
+ sound_tracks_dict = {} # keep track of sound tracks and their weight
+
+ # Process each view
+ for i, view_result in enumerate(view_results):
+ current_view = view_result["view"]
+ print(f"Processing {current_view} view ({i+1}/{len(view_results)})")
+
+ # Find corresponding image path for this view
+ image_path = os.path.join(args.image_dir, f"{current_view}.jpg")
+ if not os.path.exists(image_path):
+ print(f"Warning: Image file {image_path} not found")
+ continue
+
+ image_index = [idx for idx, path in enumerate(image_paths)
+ if os.path.basename(path) == f"{current_view}.jpg"]
+
+ if not image_index:
+ print(f"Could not find processed map for {current_view} view")
+ continue
+
+ depth_map = processed_maps[image_index[0]]["normalization"]
+
+ object_depths = sound_mapper.analyze_object_depths(
+ image_path, depth_map, lat, lon,
+ caption_data=view_result,
+ all_objects=False
+ )
+
+ if not object_depths:
+ print(f"No objects detected in the {current_view} view")
+ continue
+
+ # Generate audio for this view
+ output_path = os.path.join(args.output_dir, f"sound_{current_view}.wav")
+ print(f"Generating audio for {current_view} view...")
+
+ audio, sample_rate = audio_generator.process_and_generate_audio(
+ object_depths,
+ duration=args.audio_duration
+ )
+
+ if audio.dim() == 3:
+ audio = audio.squeeze(0)
+ elif audio.dim() == 1:
+ audio = audio.unsqueeze(0)
+
+ if audio.dim() != 2:
+ raise ValueError(f"Could not convert audio tensor of shape {audio.shape} to 2D")
+
+ torchaudio.save(
+ output_path,
+ audio,
+ sample_rate
+ )
+
+ if object_depths:
+ sound_tracks_dict[output_path] = object_depths[0]['weight']
+
+ print(f"Generated audio saved to: {output_path}")
+ print("-" * 50)
+
+ if sound_tracks_dict:
+ print("Composing final audio from all views...")
+ compose_audio(
+ list(sound_tracks_dict.keys()),
+ list(sound_tracks_dict.values()),
+ os.path.join(args.output_dir, "panoramic_composition.wav")
+ )
+ print(f"Final audio composition saved to: {os.path.join(args.output_dir, 'panoramic_composition.wav')}")
+
+ torch.cuda.empty_cache()
+ gc.collect()
+ del sound_mapper, audio_generator
+ gc.collect()
+ torch.cuda.empty_cache()
+
+ else:
+ print("Processing single image...")
+ view_result = generate_caption(lat, lon, view=args.view, model=args.model,
+ cpu_only=args.cpu_only, panoramic=False)
+ if not view_result:
+ print("Failed to generate caption for the view")
+ return
+ image_path = os.path.join(args.image_dir, f"{args.view}.jpg")
+ if not os.path.exists(image_path):
+ print(f"Error: Image file {image_path} not found")
+ return
+ print(f"Processing image: {image_path}")
+
+ sound_mapper = SoundMapper()
+ processed_maps = sound_mapper.process_depth_maps()
+ image_paths = [os.path.join(args.image_dir, f) for f in os.listdir(args.image_dir) if f.endswith(".jpg")]
+ image_basename = os.path.basename(image_path)
+ image_index = [i for i, path in enumerate(image_paths) if os.path.basename(path) == image_basename]
+
+ if not image_index:
+ print(f"Could not find processed map for {image_basename}")
+ return
+
+ depth_map = processed_maps[image_index[0]]["normalization"]
+
+ print("Detecting objects and their depths...")
+ object_depths = sound_mapper.analyze_object_depths(
+ image_path, depth_map, lat, lon,
+ caption_data=view_result,
+ all_objects=True
+ )
+
+ if not object_depths:
+ print("No objects detected in the image.")
+ return
+
+ print(f"Detected {len(object_depths)} objects:")
+ for obj in object_depths:
+ print(f" - {obj['original_label']} (Zone: {obj['zone_description']}, Depth: {obj['mean_depth']:.4f})")
+
+ print("Generating audio...")
+ audio_generator = GenerateAudio()
+
+ audio, sample_rate = audio_generator.process_and_generate_audio(
+ object_depths,
+ duration=args.audio_duration
+ )
+
+ if audio.dim() == 3:
+ audio = audio.squeeze(0)
+ elif audio.dim() == 1:
+ audio = audio.unsqueeze(0)
+
+ if audio.dim() != 2:
+ raise ValueError(f"Could not convert audio tensor of shape {audio.shape} to 2D")
+
+ output_path = os.path.join(args.output_dir, f"sound_{args.view}.wav")
+ torchaudio.save(
+ output_path,
+ audio,
+ sample_rate
+ )
+
+ print(f"Generated audio saved to: {output_path}")
+
+
+if __name__ == "__main__":
+ main()
+ # Usage:
+ #(For single image): python main.py --view front
+ #(For panoramic images): python main.py --panoramic
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..a07136981b5a4ccb68425187fea888406d2a2170
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,22 @@
+numpy==1.26.0
+einops
+omegaconf
+matplotlib
+accelerate==0.34.2
+torch==2.4.0
+torchdiffeq>=0.2.3
+diffusers==0.30.0
+huggingface_hub==0.25.0
+xformers
+torchaudio==2.4.0
+torchlibrosa==0.1.0
+torchvision==0.19.0
+transformers==4.44.0
+datasets==2.21.0
+librosa
+tqdm
+wandb
+opencv-python
+spacy
+timm==1.0.14
+en_core_web_sm @ https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.8.0/en_core_web_sm-3.8.0-py3-none-any.whl
diff --git a/utils.py b/utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..b56431a4712b5cde34ad7d6dfda3ab2b743c10cd
--- /dev/null
+++ b/utils.py
@@ -0,0 +1,132 @@
+import torch
+import numpy as np
+from einops import rearrange
+
+def sample_img_rays(x, img_fov=45):
+ """
+ Samples a unit ray for each pixel in image
+
+ Args:
+ x: images (...,h,w)
+ img_fov: assumed image fov for ray calculation; int or tuple(h,w)
+
+ Returns:
+ img_rays (h,w,3) 3:
+ """
+ h, w, dtype, device = *x.shape[-2:], x.dtype, x.device
+ hf_rad = 2*torch.pi*torch.tensor(img_fov)/2/360
+ axis_mag = (1/hf_rad.cos()).expand(2) # [y,x]
+ axis_max_coord = (axis_mag**2-1)**.5 # [y,x]
+ y_coords = torch.linspace(-axis_max_coord[0],axis_max_coord[0],h, dtype=dtype, device=device)
+ x_coords = torch.linspace(-axis_max_coord[1],axis_max_coord[1],w, dtype=dtype, device=device)
+ y, x = torch.meshgrid(y_coords, x_coords, indexing = 'ij')
+ xyz = torch.stack([x, y, torch.ones_like(x)], dim=-1) # (h,w,)
+ img_rays = xyz / xyz.norm(dim=-1).unsqueeze(-1)
+ return img_rays
+
+def gen_rotation_matrix(angles):
+ """
+ Generate rotation matrix from angles
+
+ Args:
+ angles: axis-wise rotations in [0,360] (...,3)
+
+ Returns:
+ rot_mat (...,3,3)
+ """
+ dims = angles.shape[:-1]
+ angles = 2*torch.pi*angles/360 # [0,1] -> [0,2pi]
+ angles = rearrange(angles, '... a -> a ...') # (3,...)
+ cos = angles.cos()
+ sin = angles.sin()
+ rot_mat = torch.stack([
+ cos[1]*cos[2], sin[0]*sin[1]*cos[2]-cos[0]*sin[2], cos[0]*sin[1]*cos[2]+sin[0]*sin[2],
+ cos[1]*sin[2], sin[0]*sin[1]*sin[2]+cos[0]*cos[2], cos[0]*sin[1]*sin[2]-sin[0]*cos[2],
+ -sin[1], sin[0]*cos[1], cos[0]*cos[1]
+ ], dim=-1).reshape(*dims,3,3) # (...,9) -> (...,3,3)
+ return rot_mat
+
+def cart_2_spherical(pts):
+ """
+ Convert Cartesian to spherical coordinates
+
+ Args:
+ pts: input pts (...,)
+
+ Returns:
+ ret (...,) () (radians)
+ """
+ x,y,z = pts.moveaxis(-1,0)
+ r = pts.norm(dim=-1)
+ phi = torch.arcsin(y/r)
+ theta = x.sign()*torch.arccos(z/(x**2+z**2)**.5)
+ ret = torch.stack([theta,phi,r],dim=-1)
+ return ret
+
+def sample_pano_img(img, pts, h_fov_ratio=1, w_fov_ratio=1):
+ """
+ Sample points from panoramic image
+
+ Args:
+ img: pano-image (...,3:,h,w)
+ pts: spherical points to sample from img (...,h,w,3:)
+ *_fov_ratio: ratio of full fov for pano
+
+ Returns:
+ sampled_img (...,3:,h,w)
+ """
+ h, w = img.shape[-2:]
+ sh, sw = pts.shape[-3:-1]
+ h_conv, w_conv = h/h_fov_ratio, w/w_fov_ratio
+ img = rearrange(img, '... c h w -> ... (h w) c') # (...,n,3)
+ pts = rearrange(pts, '... h w c -> ... (h w) c') # (...,m,3)
+ # convert (pts) radians to indices
+ h_inds = (((pts[...,1] + torch.pi/2) / torch.pi) % 1) * h_conv # azimuth (-pi/2,+pi/2)
+ w_inds = (((pts[...,0] + torch.pi) / (2*torch.pi)) % 1) * w_conv # azimuth (-pi,+pi)
+ # get inds for bilin interp
+ h_l, w_l = h_inds.to(torch.int).clamp(0,h-1), w_inds.to(torch.int).clamp(0,w-1)
+ h_r, w_r = (h_l+1).clamp(0,h-1), (w_l+1).clamp(0,w-1)
+ # get weights
+ h_p_r, w_p_r = h_inds-h_l, w_inds-w_l
+ h_p_l, w_p_l = 1-h_p_r, 1-w_p_r
+ # linearize inds,weights
+ inds = (torch.stack([w*h_l, w*h_r],dim=-1)[...,:,None] + torch.stack([w_l, w_r],dim=-1)[...,None,:]).flatten(-2).moveaxis(-1,0).to(torch.long) # (4,...)
+ weights = (torch.stack([h_p_l, h_p_r],dim=-1)[...,:,None] * torch.stack([w_p_l, w_p_r],dim=-1)[...,None,:]).flatten(-2).moveaxis(-1,0) # (4,...)
+ # do bilin interp
+ img_extract = img[None,:].expand(4,*(len(img.shape)*[-1])).gather(-2, inds[...,None].expand(*(len(inds.shape)*[-1]),3))
+ sampled_img = (weights[...,None]*img_extract).sum(0) # (4,...,m,3) -> (...,m,3)
+ sampled_img = rearrange(sampled_img, '... (h w) c -> ... c h w', h=sh, w=sw)
+ return sampled_img
+
+def sample_perspective_img(pano_img, output_shape, fov=None, rot=None):
+ """
+ Sample perspective image from panoramic
+
+ Args:
+ pano_img: pano-image numpy.array (h,w,3:)
+ output_shape: output image dimensions tuple(h,w)
+ fov: desired perspective image fov; int or tuple(vertical,horizontal) in degrees [0,180)
+ rot: axis-wise rotations; tuple(pitch,yaw,roll) in degrees [0,360]
+
+ Returns:
+ sampled_img numpy.array (h,w,3:), fov, rot
+ """
+ if fov is None:
+ fov = torch.tensor([30,30]) + torch.tensor([60,60])*torch.rand(2) # (v-fov,h-fov)
+ fov = (fov[0].item(), fov[1].item())
+ if rot is None:
+ rot = (-torch.tensor([10,135,20]) + torch.tensor([20,225,40])*torch.rand(3)) # rot w.r.t (x,y,z) aka (pitch,yaw,roll)
+ else:
+ rot = torch.tensor(rot)
+ pano_img = torch.tensor(pano_img, dtype=torch.uint8).moveaxis(-1,0)
+ out_dtype = pano_img.dtype
+ pano_img = pano_img.to(torch.float)
+
+ img_rays = sample_img_rays(torch.empty(output_shape, dtype=pano_img.dtype, device=pano_img.device), img_fov=fov)
+ rot_mat = gen_rotation_matrix(rot.to(pano_img.dtype))[None,None,:] # (3,3) -> (1,1,3,3)
+ rot_img_rays = torch.matmul(rot_mat, img_rays.unsqueeze(-1)).squeeze(-1)
+ spher_rot_img_rays = cart_2_spherical(rot_img_rays) # (h,w,3)
+ # sample img
+ pano_img = sample_pano_img(pano_img, spher_rot_img_rays)
+
+ return pano_img.moveaxis(0,-1).to(out_dtype).numpy(), fov, rot.numpy()
\ No newline at end of file
diff --git a/visualize.py b/visualize.py
new file mode 100644
index 0000000000000000000000000000000000000000..3dfabfb1c90f2feb12164aee385bb270fdcc9542
--- /dev/null
+++ b/visualize.py
@@ -0,0 +1,523 @@
+import os
+import sys
+import torch
+import numpy as np
+from PIL import Image, ImageDraw, ImageFont
+import matplotlib.pyplot as plt
+import matplotlib.cm as cm
+from transformers import AutoProcessor, AutoModelForZeroShotObjectDetection
+import re
+import spacy
+from config import LOGS_DIR, OUTPUT_DIR
+from DepthEstimator import DepthEstimator
+from SoundMapper import SoundMapper
+from GenerateCaptions import generate_caption
+from GenerateCaptions import StreetSoundTextPipeline, ImageAnalyzer
+
+
+class ProcessVisualizer:
+ def __init__(self, image_dir=LOGS_DIR, output_dir=None):
+ self.image_dir = image_dir
+ self.output_dir = output_dir if output_dir else os.path.join(OUTPUT_DIR, "visualizations")
+ os.makedirs(self.output_dir, exist_ok=True)
+
+ # Initialize components (but don't load models yet)
+ self.depth_estimator = DepthEstimator(image_dir=self.image_dir)
+ self.sound_mapper = SoundMapper()
+ self.device = "cuda" if torch.cuda.is_available() else "cpu"
+ self.dino = None
+ self.dino_processor = None
+ self.nlp = None
+
+ # Create subdirectories for different visualization types
+ self.dirs = {
+ "bbox_original": os.path.join(self.output_dir, "bbox_original"),
+ "bbox_depth": os.path.join(self.output_dir, "bbox_depth"),
+ "depth_maps": os.path.join(self.output_dir, "depth_maps"),
+ "combined": os.path.join(self.output_dir, "combined")
+ }
+
+ for dir_path in self.dirs.values():
+ os.makedirs(dir_path, exist_ok=True)
+
+ def _load_nlp(self):
+ if self.nlp is None:
+ self.nlp = spacy.load("en_core_web_sm")
+ return self.nlp
+
+ def _load_dino(self):
+ if self.dino is None:
+ print("Loading DINO model...")
+ self.dino = AutoModelForZeroShotObjectDetection.from_pretrained("IDEA-Research/grounding-dino-base").to(self.device)
+ self.dino_processor = AutoProcessor.from_pretrained("IDEA-Research/grounding-dino-base")
+ else:
+ self.dino = self.dino.to(self.device)
+ return self.dino, self.dino_processor
+
+ def _unload_dino(self):
+ if self.dino is not None:
+ self.dino = self.dino.to("cpu")
+ torch.cuda.empty_cache()
+
+ def detect_nouns(self, caption_text):
+ """Extract nouns from caption text for object detection"""
+ print("Detecting nouns in caption...")
+ nlp = self._load_nlp()
+ all_nouns = []
+
+ # Extract nouns from sound source descriptions
+ pattern = r'\d+\.\s+\*\*([^:]+)\*\*:'
+ sources = re.findall(pattern, caption_text)
+ for source in sources:
+ clean_source = re.sub(r'sounds?|noise[s]?', '', source, flags=re.IGNORECASE).strip()
+ if clean_source:
+ source_doc = nlp(clean_source)
+ for token in source_doc:
+ if token.pos_ == "NOUN" and len(token.text) > 1:
+ all_nouns.append(token.text.lower())
+
+ # Extract nouns from general text
+ clean_caption = re.sub(r'[*()]', '', caption_text).strip()
+ clean_caption = re.sub(r'##\w+', '', clean_caption)
+ clean_caption = re.sub(r'\s+', ' ', clean_caption).strip()
+ doc = nlp(clean_caption)
+ for token in doc:
+ if token.pos_ == "NOUN" and len(token.text) > 1:
+ if token.text[0].isalpha():
+ all_nouns.append(token.text.lower())
+
+ matches = sorted(set(all_nouns))
+ print(f"Detected nouns: {matches}")
+ return matches
+
+ def detect_objects(self, image_path, caption_text):
+ """Detect objects in image based on nouns from caption"""
+ print(f"Processing image: {image_path}")
+
+ # Extract nouns from caption
+ nouns = self.detect_nouns(caption_text)
+ if not nouns:
+ print("No nouns detected in caption.")
+ return None, None
+
+ # Load image
+ image = Image.open(image_path)
+
+ # Load DINO model
+ self.dino, self.dino_processor = self._load_dino()
+
+ # Filter nouns
+ filtered_nouns = []
+ for noun in nouns:
+ if '##' not in noun and len(noun) > 1 and noun[0].isalpha():
+ filtered_nouns.append(noun)
+
+ # Create text prompt for DINO
+ text_prompt = " . ".join(filtered_nouns)
+ print(f"Using text prompt for DINO: {text_prompt}")
+
+ # Process image with DINO
+ inputs = self.dino_processor(images=image, text=text_prompt, return_tensors="pt").to(self.device)
+
+ with torch.no_grad():
+ outputs = self.dino(**inputs)
+ results = self.dino_processor.post_process_grounded_object_detection(
+ outputs,
+ inputs.input_ids,
+ box_threshold=0.25,
+ text_threshold=0.25,
+ target_sizes=[image.size[::-1]]
+ )
+
+ # Clean up to save memory
+ self._unload_dino()
+ del inputs, outputs
+ torch.cuda.empty_cache()
+
+ # Process results
+ result = results[0]
+ labels = result["labels"]
+ scores = result["scores"]
+ bboxes = result["boxes"]
+
+ # Clean labels
+ clean_labels = []
+ for label in labels:
+ clean_label = re.sub(r'##\w+', '', label)
+ clean_labels.append(clean_label)
+
+ print(f"Detected {len(clean_labels)} objects: {list(zip(clean_labels, scores.tolist()))}")
+
+ return clean_labels, bboxes
+
+ def estimate_depth(self):
+ """Generate depth maps for all images in the directory"""
+ print("Estimating depth for all images...")
+ depth_maps = self.depth_estimator.estimate_depth(self.image_dir)
+
+ # Convert depth maps to normalized grayscale for visualization
+ normalized_maps = []
+ img_paths = [os.path.join(self.image_dir, f) for f in os.listdir(self.image_dir)
+ if f.endswith(('.jpg', '.jpeg', '.png'))]
+
+ for i, item in enumerate(depth_maps):
+ depth_map = item["depth"]
+ depth_array = np.array(depth_map)
+ normalization = depth_array / 255.0
+
+ # Associate source path with depth map
+ source_path = img_paths[i] if i < len(img_paths) else f"depth_{i}.jpg"
+ filename = os.path.basename(source_path)
+
+ # Save grayscale depth map
+ depth_path = os.path.join(self.dirs["depth_maps"], f"depth_{filename}")
+ depth_map.save(depth_path)
+
+ normalized_maps.append({
+ "original": depth_map,
+ "normalization": normalization,
+ "path": depth_path,
+ "source_path": source_path
+ })
+
+ return normalized_maps
+
+ def create_histogram_depth_zones(self, depth_map, num_zones=3):
+ """Create depth zones based on histogram of depth values"""
+ hist, bin_edge = np.histogram(depth_map.flatten(), bins=50, range=(0, 1))
+ cumulative = np.cumsum(hist) / np.sum(hist)
+ thresholds = [0.0]
+ for i in range(1, num_zones):
+ target = i / num_zones
+ idx = np.argmin(np.abs(cumulative - target))
+ thresholds.append(bin_edge[idx + 1])
+ thresholds.append(1.0)
+ return thresholds
+
+ def get_depth_zone(self, bbox, depth_map, num_zones=3):
+ """Determine depth zone for a given bounding box"""
+ x1, y1, x2, y2 = [int(coord) for coord in bbox]
+
+ # Adjust for image dimensions
+ height, width = depth_map.shape
+ x1, y1 = max(0, x1), max(0, y1)
+ x2, y2 = min(width, x2), min(height, y2)
+
+ # Extract depth ROI
+ depth_roi = depth_map[y1:y2, x1:x2]
+ if depth_roi.size == 0:
+ return num_zones - 1, 1.0 # Default to farthest zone
+
+ # Calculate mean depth
+ mean_depth = np.mean(depth_roi)
+
+ # Determine zone
+ thresholds = self.create_histogram_depth_zones(depth_map, num_zones)
+ zone = 0
+ for i in range(num_zones):
+ if thresholds[i] <= mean_depth < thresholds[i+1]:
+ zone = i
+ break
+
+ weight = 1.0 - mean_depth # Higher weight for closer objects
+ return zone, mean_depth
+
+ def draw_bounding_boxes(self, image, labels, bboxes, scores=None, depth_zones=None):
+ """Draw bounding boxes on image with depth zone information"""
+ draw = ImageDraw.Draw(image)
+
+ # Try to get a font, fallback to default if not available
+ try:
+ font = ImageFont.truetype("arial.ttf", 16)
+ except IOError:
+ try:
+ font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 16)
+ except:
+ font = ImageFont.load_default()
+
+ # Store colors as a class attribute for access in modified versions
+ self.zone_colors = {
+ 0: (255, 50, 50), # Bright red for near
+ 1: (255, 180, 0), # Orange for medium
+ 2: (50, 255, 50) # Bright green for far
+ }
+
+ for i, (label, bbox) in enumerate(zip(labels, bboxes)):
+ x1, y1, x2, y2 = [int(coord) for coord in bbox]
+
+ # Get color based on depth zone if available
+ if depth_zones is not None and i < len(depth_zones):
+ zone, depth = depth_zones[i]
+ color = self.zone_colors.get(zone, (0, 0, 255))
+ zone_text = ["near", "medium", "far"][zone]
+ label_text = f"{depth:.2f}"
+ else:
+ color = (255, 50, 50) # Default bright red
+ label_text = label
+
+ # Add score if available
+ if scores is not None and i < len(scores):
+ label_text += f" {scores[i]:.2f}"
+
+ # Draw bounding box with thick border for better visibility
+ draw.rectangle([x1, y1, x2, y2], outline=color, width=3)
+
+ # Calculate text size more reliably
+ if hasattr(draw, 'textsize'):
+ text_size = draw.textsize(label_text, font=font)
+ else:
+ # Fallback sizing when textsize is not available
+ text_width = len(label_text) * 8 # Approximate 8 pixels per character
+ text_height = 20 # Approximate height for readability
+ text_size = (text_width, text_height)
+
+ # Draw label background with margin
+ margin = 2
+ text_box = [
+ x1 - margin,
+ y1 - text_size[1] - margin,
+ x1 + text_size[0] + margin,
+ y1 + margin
+ ]
+ draw.rectangle(text_box, fill=color)
+
+ # Draw label text
+ draw.text((x1, y1 - text_size[1]), label_text, fill=(255, 255, 255), font=font)
+
+ return image
+
+ def create_depth_map_visualization(self, depth_map, use_grayscale=True):
+ """Create a visualization of the depth map
+
+ Args:
+ depth_map: Normalized depth map array
+ use_grayscale: If True, creates grayscale image; otherwise, uses colored heatmap
+
+ Returns:
+ PIL Image with depth visualization
+ """
+ # Normalize depth map to [0, 1]
+ normalized_depth = depth_map.copy()
+
+ if use_grayscale:
+ # Convert to grayscale (multiplying by 255 for better visibility)
+ grayscale = (normalized_depth * 255).astype(np.uint8)
+ # Convert to RGB for consistent processing with bounding box drawing
+ depth_img = Image.fromarray(grayscale).convert('RGB')
+ else:
+ # Apply colormap (jet)
+ colored_depth = (cm.jet(normalized_depth) * 255).astype(np.uint8)
+ # Convert to PIL Image (RGB)
+ depth_img = Image.fromarray(colored_depth[:, :, :3])
+
+ return depth_img
+
+ def process_images(self, lat=None, lon=None, single_view=None, save_with_heatmap=False):
+ """
+ Process all images in the directory or a single view
+
+ Args:
+ lat: Latitude for caption generation
+ lon: Longitude for caption generation
+ single_view: Process only specified view if provided
+ save_with_heatmap: If True, also saves depth maps as colored heatmaps
+ """
+ # Get image paths
+ if single_view:
+ image_paths = [os.path.join(self.image_dir, f"{single_view}.jpg")]
+ else:
+ image_paths = [os.path.join(self.image_dir, f) for f in os.listdir(self.image_dir)
+ if f.endswith(('.jpg', '.jpeg', '.png'))]
+
+ if not image_paths:
+ print(f"No images found in {self.image_dir}")
+ return
+
+ # Generate depth maps
+ depth_maps = self.estimate_depth()
+
+ # Process each image
+ for i, image_path in enumerate(image_paths):
+ image_basename = os.path.basename(image_path)
+ view_name = os.path.splitext(image_basename)[0]
+ print(f"\nProcessing {view_name} view ({i+1}/{len(image_paths)})...")
+
+ # Generate caption if coordinates are provided
+ caption_text = None
+ analyzer = ImageAnalyzer()
+ caption_text = analyzer.analyze_image(image_path)
+
+ if lat and lon:
+ view_result = generate_caption(lat, lon, view=view_name, panoramic=False)
+
+ if view_result:
+ caption_text = view_result.get("sound_description", "")
+ print(f"Generated caption: {caption_text}")
+
+ # Skip if no caption and lat/lon were provided
+ if lat and lon and not caption_text:
+ print(f"Failed to generate caption for {image_path}, skipping.")
+ continue
+
+ # Detect objects based on caption
+ if caption_text:
+ labels, bboxes = self.detect_objects(image_path, caption_text)
+ else:
+ # If no caption provided, use generic object detection
+ print("No caption provided, using predefined nouns for detection...")
+ generic_nouns = ["car", "person", "tree", "building", "road", "sign", "window", "door"]
+ labels, bboxes = self.detect_objects(image_path, " ".join(generic_nouns))
+
+ if len(labels) == 0 or len(bboxes)==0:
+ print(f"No objects detected in {image_path}, skipping.")
+ continue
+
+ # Find matching depth map
+ depth_map_idx = next((idx for idx, data in enumerate(depth_maps)
+ if os.path.basename(image_path) == os.path.basename(data.get("source_path", ""))), i % len(depth_maps))
+ depth_map = depth_maps[depth_map_idx]["normalization"]
+
+ # Get depth zones for each detected object
+ depth_zones = []
+ for bbox in bboxes:
+ zone, mean_depth = self.get_depth_zone(bbox, depth_map)
+ depth_zones.append((zone, mean_depth))
+
+ # Load and process original image
+ original_img = Image.open(image_path).convert("RGB")
+ bbox_img = original_img.copy()
+
+ # Draw bounding boxes on original image
+ bbox_img = self.draw_bounding_boxes(bbox_img, labels, bboxes, depth_zones=depth_zones)
+
+ # Save image with bounding boxes
+ bbox_path = os.path.join(self.dirs["bbox_original"], f"bbox_{image_basename}")
+ bbox_img.save(bbox_path)
+ print(f"Saved bounding boxes on original image: {bbox_path}")
+
+ # Create grayscale depth map for better visibility of bounding boxes
+ depth_vis = self.create_depth_map_visualization(depth_map, use_grayscale=True)
+
+ # Draw bounding boxes on depth map visualization
+ depth_bbox_img = depth_vis.copy()
+ depth_bbox_img = self.draw_bounding_boxes(depth_bbox_img, labels, bboxes, depth_zones=depth_zones)
+
+ # Draw bounding boxes directly on the original depth map
+ # Load the saved grayscale depth map
+ original_depth_path = depth_maps[depth_map_idx]["path"]
+ original_depth_img = Image.open(original_depth_path).convert('RGB')
+
+ # Draw boxes on the original depth map
+ original_depth_bbox = original_depth_img.copy()
+ original_depth_bbox = self.draw_bounding_boxes(original_depth_bbox, labels, bboxes, depth_zones=depth_zones)
+
+ # Save the original depth map with bounding boxes
+ original_depth_bbox_path = os.path.join(self.dirs["bbox_depth"], f"orig_depth_bbox_{image_basename}")
+ original_depth_bbox.save(original_depth_bbox_path)
+ print(f"Saved bounding boxes on original depth map: {original_depth_bbox_path}")
+
+ # Save depth map with bounding boxes
+ depth_bbox_path = os.path.join(self.dirs["bbox_depth"], f"depth_bbox_{image_basename}")
+ depth_bbox_img.save(depth_bbox_path)
+ print(f"Saved bounding boxes on depth map: {depth_bbox_path}")
+
+ # Also save colored heatmap version if requested
+ if save_with_heatmap:
+ # Create a heatmap depth visualization
+ depth_heatmap = self.create_depth_map_visualization(depth_map, use_grayscale=False)
+ depth_heatmap_bbox = depth_heatmap.copy()
+ depth_heatmap_bbox = self.draw_bounding_boxes(depth_heatmap_bbox, labels, bboxes, depth_zones=depth_zones)
+
+ # Save heatmap version
+ heatmap_path = os.path.join(self.dirs["bbox_depth"], f"heatmap_bbox_{image_basename}")
+ depth_heatmap_bbox.save(heatmap_path)
+ print(f"Saved bounding boxes on depth heatmap: {heatmap_path}")
+
+ # Create combined visualization
+ # Create a 2x1 grid showing original with bboxes and original depth with bboxes
+ combined_width = original_img.width * 2
+ combined_height = original_img.height
+ combined_img = Image.new('RGB', (combined_width, combined_height))
+
+ # Paste images
+ combined_img.paste(bbox_img, (0, 0))
+ combined_img.paste(original_depth_bbox, (original_img.width, 0))
+
+ # Save combined image
+ combined_path = os.path.join(self.dirs["combined"], f"combined_{image_basename}")
+ combined_img.save(combined_path)
+ print(f"Saved combined visualization: {combined_path}")
+
+ print("\nVisualization process complete!")
+ print(f"Results saved in {self.output_dir}")
+
+ def cleanup(self):
+ """Clean up resources"""
+ if hasattr(self, 'depth_estimator'):
+ self.depth_estimator._unload_model()
+
+ if self.dino is not None:
+ self.dino = self.dino.to("cpu")
+ del self.dino
+ self.dino = None
+
+ if self.nlp is not None:
+ del self.nlp
+ self.nlp = None
+
+ torch.cuda.empty_cache()
+
+
+def main():
+ import argparse
+
+ parser = argparse.ArgumentParser(description="Visualize intermediate steps of the Street Sound Pipeline")
+ parser.add_argument("--image_dir", type=str, default=LOGS_DIR, help="Directory containing input images")
+ parser.add_argument("--output_dir", type=str, default=None, help="Directory for output visualizations")
+ parser.add_argument("--location", type=str, default=None, help='Location in format "latitude,longitude" (e.g., "40.7128,-74.0060")')
+ parser.add_argument("--view", type=str, default=None, choices=["front", "back", "left", "right"], help="Process only the specified view")
+ parser.add_argument("--skip_caption", action="store_true", help="Skip caption generation and use generic noun list")
+ parser.add_argument("--save_heatmap", action="store_true", help="Also save depth maps as colored heatmaps with bounding boxes")
+ parser.add_argument("--box_width", type=int, default=3, help="Width of bounding box lines")
+
+ args = parser.parse_args()
+
+ # Parse location if provided
+ lat, lon = None, None
+ if args.location and not args.skip_caption:
+ try:
+ lat, lon = map(float, args.location.split(","))
+ except ValueError:
+ print("Error: Location must be in format 'latitude,longitude'")
+ return
+
+ # Initialize visualizer
+ visualizer = ProcessVisualizer(image_dir=args.image_dir, output_dir=args.output_dir)
+
+ # Set box width if provided
+ if args.box_width != 3:
+ draw_bounding_boxes_orig = visualizer.draw_bounding_boxes
+ def draw_bounding_boxes_with_width(*args, **kwargs):
+ draw = ImageDraw.Draw(args[0])
+ for i, (label, bbox) in enumerate(zip(args[1], args[2])):
+ x1, y1, x2, y2 = [int(coord) for coord in bbox]
+ depth_zones = kwargs.get('depth_zones')
+ if depth_zones is not None and i < len(depth_zones):
+ zone, depth = depth_zones[i]
+ color = draw_bounding_boxes_orig.zone_colors.get(zone, (0, 0, 255))
+ else:
+ color = (255, 0, 0)
+ draw.rectangle([x1, y1, x2, y2], outline=color, width=args.box_width)
+ return draw_bounding_boxes_orig(*args, **kwargs)
+ visualizer.draw_bounding_boxes = draw_bounding_boxes_with_width
+
+ try:
+ # Process images
+ visualizer.process_images(lat=lat, lon=lon, single_view=args.view, save_with_heatmap=args.save_heatmap)
+ finally:
+ # Clean up resources
+ visualizer.cleanup()
+
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file