From 11600ae70f7864f84c30456740111871608ce81e Mon Sep 17 00:00:00 2001 From: chickenflyshigh Date: Wed, 6 Nov 2024 22:51:03 +1100 Subject: [PATCH] Batched asynchronous API requests. Added additional draw options. --- api_models.json | 23 ++- app.py | 83 --------- config.py | 22 ++- create_overlay.py | 305 ------------------------------- data.py | 59 ++++++ database/translations.db | Bin 0 -> 327680 bytes draw.py | 222 ++++++++++++++++------ helpers/batching.py | 384 +++++++++++++++++++++++++++++---------- helpers/ocr.py | 21 ++- helpers/translation.py | 70 +++++-- helpers/utils.py | 27 ++- logging_config.py | 7 +- main.py | 100 ++++++++++ qtapp.py | 115 ------------ templates/index.html | 2 +- view_buffer_app.py | 52 ++++++ web.py => web_app.py | 7 +- 17 files changed, 788 insertions(+), 711 deletions(-) delete mode 100644 app.py delete mode 100644 create_overlay.py create mode 100644 data.py create mode 100644 database/translations.db create mode 100644 main.py delete mode 100644 qtapp.py create mode 100644 view_buffer_app.py rename web.py => web_app.py (85%) diff --git a/api_models.json b/api_models.json index 1d04faf..3424a65 100644 --- a/api_models.json +++ b/api_models.json @@ -1,13 +1,20 @@ { "Gemini": { - "gemini-1.5-pro": 2, - "gemini-1.5-flash": 15, - "gemini-1.5-flash-8b": 8, - "gemini-1.0-pro": 15 + "gemini-1.5-pro": { "rpmin": 2, "rpd": 50 }, + "gemini-1.5-flash": { "rpmin": 15, "rpd": 1500 }, + "gemini-1.5-flash-8b": { "rpmin": 15, "rpd": 1500 }, + "gemini-1.0-pro": { "rpmin": 15, "rpd": 1500 } }, - "Groqq": { - "llama-3.2-90b-text-preview": 30, - "llama3-70b-8192": 30, - "mixtral-8x7b-32768": 30 + "Groq": { + "llama-3.2-90b-text-preview": { "rpmin": 30, "rpd": 7000 }, + "llama3-70b-8192": { "rpmin": 30, "rpd": 14400 }, + "mixtral-8x7b-32768": { "rpmin": 30, "rpd": 14400 }, + "llama-3.1-70b-versatile": { "rpmin": 30, "rpd": 14400 }, + "gemma2-9b-it": { "rpmin": 30, "rpd": 14400 }, + "llama3-groq-8b-8192-tool-use-preview": { "rpmin": 30, "rpd": 14400 }, + "llama3-groq-70b-8192-tool-use-preview": { "rpmin": 30, "rpd": 14400 }, + "llama-3.2-90b-vision-preview": { "rpmin": 15, "rpd": 3500 }, + "llama-3.2-11b-text-preview": { "rpmin": 30, "rpd": 7000 }, + "llama-3.2-11b-vision-preview": { "rpmin": 30, "rpd": 7000 } } } diff --git a/app.py b/app.py deleted file mode 100644 index 5309916..0000000 --- a/app.py +++ /dev/null @@ -1,83 +0,0 @@ -################################################################################### -##### IMPORT LIBRARIES ##### -import os, time, sys - -sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'helpers')) - -from translation import translate_Seq_LLM, translate_API_LLM, init_API_LLM, init_Seq_LLM -from utils import printsc, convert_image_to_bytes, bytes_to_image -from ocr import get_words, init_OCR, id_keep_source_lang -from logging_config import logger -from draw import modify_image_bytes -from config import ADD_OVERLAY, SOURCE_LANG, TARGET_LANG, OCR_MODEL, OCR_USE_GPU, LOCAL_FILES_ONLY, REGION, INTERVAL, MAX_TRANSLATE, TRANSLATION_MODEL -################################################################################### - -ADD_OVERLAY = False - -latest_image = None - -def main(): - global latest_image - - ##### Initialize the OCR ##### - OCR_LANGUAGES = [SOURCE_LANG, TARGET_LANG, 'en'] - ocr = init_OCR(model=OCR_MODEL, easy_languages = OCR_LANGUAGES, use_GPU=OCR_USE_GPU) - - ##### Initialize the translation ##### - # model, tokenizer = init_Seq_LLM(TRANSLATION_MODEL, from_lang =SOURCE_LANG , target_lang = TARGET_LANG) - models = init_API_LLM(SOURCE_LANG, TARGET_LANG) - ################################################################################### - runs = 0 - app.exec() - while True: - if ADD_OVERLAY: - overlay.clear_all_text() - - untranslated_image = printsc(REGION) - - if ADD_OVERLAY: - overlay.text_entries = overlay.text_entries_copy - overlay.update() - overlay.text_entries.clear() - - byte_image = convert_image_to_bytes(untranslated_image) - ocr_output = id_keep_source_lang(ocr, byte_image, SOURCE_LANG) # keep only phrases containing the source language - - if runs == 0: - logger.info('Initial run') - prev_words = set() - else: - logger.info(f'Run number: {runs}.') - runs += 1 - - curr_words = set(get_words(ocr_output)) - - ### If the OCR detects different words, translate screen -> to ensure that the screen is not refreshing constantly and to save GPU power - if prev_words != curr_words: - logger.info('Translating') - - to_translate = [entry[1] for entry in ocr_output][:MAX_TRANSLATE] - # translation = translate_Seq_LLM(to_translate, model_type = TRANSLATION_MODEL, model = model, tokenizer = tokenizer, from_lang = SOURCE_LANG, target_lang = TARGET_LANG) - translation = translate_API_LLM(to_translate, models) - logger.info(f'Translation from {to_translate} to\n {translation}') - translated_image = modify_image_bytes(byte_image, ocr_output, translation) - latest_image = bytes_to_image(translated_image) - # latest_image.show() # for debugging - - prev_words = curr_words - else: - logger.info("No new words to translate. Output will not refresh.") - - logger.info(f'Sleeping for {INTERVAL} seconds') - time.sleep(INTERVAL) - # if ADD_OVERLAY: - # sys.exit(app.exec()) - -################### TODO ################## -# 3. Quantising/finetuning larger LLMs. Consider using Aya-23-8B, Gemma, llama3.2 models. -# 5. Maybe refreshing issue of flask app. Also get webpage to update only if the image changes. -# Create a way for it to just replace the text and provide only the translation on-screen. Qt6 - -if __name__ == "__main__": - sys.exit(main()) - \ No newline at end of file diff --git a/config.py b/config.py index d0998e3..0b05eb8 100644 --- a/config.py +++ b/config.py @@ -10,22 +10,31 @@ load_dotenv(override=True) INTERVAL = int(os.getenv('INTERVAL')) ### OCR +IMAGE_CHANGE_THRESHOLD = float(os.getenv('IMAGE_CHANGE_THRESHOLD', 0.75)) # higher values mean more sensitivity to changes in the screen, too high and the screen will constantly refresh OCR_MODEL = os.getenv('OCR_MODEL', 'easy') # 'easy', 'paddle', 'rapid' ### easy is the most accurate, paddle is the fastest with CUDA and rapid is the fastest with CPU. Rapid has only between Chinese and English unless you add more languages OCR_USE_GPU = ast.literal_eval(os.getenv('OCR_USE_GPU', 'True')) + ### Drawing/Overlay Config -ADD_OVERLAY = ast.literal_eval(os.getenv('ADD_OVERLAY', 'True')) FILL_COLOUR = os.getenv('FILL_COLOUR', 'white') FONT_FILE = os.getenv('FONT_FILE') -FONT_SIZE = int(os.getenv('FONT_SIZE', 16)) +FONT_SIZE_MAX = int(os.getenv('FONT_SIZE_MAX', 20)) +FONT_SIZE_MIN = int(os.getenv('FONT_SIZE_MIN', 8)) LINE_SPACING = int(os.getenv('LINE_SPACING', 3)) REGION = ast.literal_eval(os.getenv('REGION','(0,0,2560,1440)')) +DRAW_TRANSLATIONS_MODE = os.getenv('DRAW_TRANSLATIONS_MODE', 'add') +""" +`learn': adds translated text, original text (should be added so when texts get moved around the translation of which it references is understood) and (optionally with the other TO_ROMANIZE option) romanized text above the original text. Texts can overlap if squished into a corner. Works well for games where texts are sparser +'learn_cover': same as above but covers the original text with the translated text. Can help with readability and is less cluttered but with sufficiently dense text the texts can still overlap +'translation_only_cover': cover the original text with the translated text - will not show the original text at all but not affected by overlapping texts +""" + FONT_COLOUR = os.getenv('FONT_COLOUR', "#ff0000") TO_ROMANIZE = ast.literal_eval(os.getenv('TO_ROMANIZE', 'True')) # API KEYS https://github.com/cheahjs/free-llm-api-resources?tab=readme-ov-file -GEMINI_API_KEY = os.getenv('GEMINI_KEY') +GEMINI_API_KEY = os.getenv('GEMINI_API_KEY') GROQ_API_KEY = os.getenv('GROQ_API_KEY') # # MISTRAL_API_KEY = os.getenv('MISTRAL_API_KEY') # https://console.mistral.ai/api-keys/ slow asf @@ -44,12 +53,13 @@ BATCH_SIZE = int(os.getenv('BATCH_SIZE', 6)) LOCAL_FILES_ONLY = ast.literal_eval(os.getenv('LOCAL_FILES_ONLY', 'False')) ################################################################################################### +## Filepaths +API_MODELS_FILEPATH = os.path.join(os.path.dirname(__file__), 'api_models.json') + +FONT_SIZE = int((FONT_SIZE_MAX + FONT_SIZE_MIN)/2) LINE_HEIGHT = FONT_SIZE - - - if TRANSLATION_USE_GPU is False: device = torch.device("cpu") else: diff --git a/create_overlay.py b/create_overlay.py deleted file mode 100644 index 91c8556..0000000 --- a/create_overlay.py +++ /dev/null @@ -1,305 +0,0 @@ -from PySide6.QtCore import Qt, QPoint, QRect, QTimer, QBuffer -from PySide6.QtGui import (QKeySequence, QShortcut, QAction, QPainter, QFont, QScreen, QIcon) -from PySide6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, - QLabel, QSystemTrayIcon, QMenu) -import sys, io, os, signal, time, platform -from PIL import Image -from dataclasses import dataclass -from typing import List, Optional -from config import ADD_OVERLAY, FONT_FILE, FONT_SIZE -from logging_config import logger - - -def qpixmap_to_bytes(qpixmap): - qimage = qpixmap.toImage() - buffer = QBuffer() - buffer.open(QBuffer.ReadWrite) - qimage.save(buffer, "PNG") - return qimage - -@dataclass -class TextEntry: - text: str - x: int - y: int - font: QFont = QFont('Arial', FONT_SIZE) - visible: bool = True - text_color: Qt.GlobalColor = Qt.GlobalColor.red - background_color: Optional[Qt.GlobalColor] = None - padding: int = 1 - -class TranslationOverlay(QMainWindow): - def __init__(self): - super().__init__() - self.setWindowTitle("Translation Overlay") - self.is_passthrough = True - self.text_entries: List[TextEntry] = [] - self.setup_window_attributes() - self.setup_shortcuts() - self.closeEvent = lambda event: QApplication.quit() - self.default_font = QFont('Arial', FONT_SIZE) - self.text_entries_copy: List[TextEntry] = [] - self.next_text_entries: List[TextEntry] = [] - #self.show_background = True - self.background_opacity = 0.5 - # self.setup_tray() - - def prepare_for_capture(self): - """Preserve current state and clear overlay""" - if ADD_OVERLAY: - self.text_entries_copy = self.text_entries.copy() - self.clear_all_text() - self.update() - - def restore_after_capture(self): - """Restore overlay state after capture""" - if ADD_OVERLAY: - logger.debug(f'Text entries copy during initial phase of restore_after_capture: {self.text_entries_copy}') - self.text_entries = self.text_entries_copy.copy() - logger.debug(f"Restored text entries: {self.text_entries}") - self.update() - - def add_next_text_at_position_no_update(self, x: int, y: int, text: str, - font: Optional[QFont] = None, text_color: Qt.GlobalColor = Qt.GlobalColor.red): - """Add new text without triggering update""" - entry = TextEntry( - text=text, - x=x, - y=y, - font=font or self.default_font, - text_color=text_color - ) - self.next_text_entries.append(entry) - - def update_translation(self, ocr_output, translation): - # Update your overlay with new translations here - # You'll need to implement the logic to display the translations - self.clear_all_text() - self.text_entries = self.next_text_entries.copy() - self.next_text_entries.clear() - self.update() - - def capture_behind(self, x=None, y=None, width=None, height=None): - """ - Capture the screen area behind the overlay. - If no coordinates provided, captures the area under the window. - """ - # Temporarily hide the window - self.hide() - - # Get screen - screen = QScreen.grabWindow( - self.screen(), - 0, - x if x is not None else self.x(), - y if y is not None else self.y(), - width if width is not None else self.width(), - height if height is not None else self.height() - ) - - # Show the window again - self.show() - screen_bytes = qpixmap_to_bytes(screen) - return screen_bytes - - def clear_all_text(self): - """Clear all text entries""" - self.text_entries.clear() - self.update() - - def setup_window_attributes(self): - # Set window flags for overlay behavior - self.setWindowFlags( - Qt.WindowType.FramelessWindowHint | - Qt.WindowType.WindowStaysOnTopHint | - Qt.WindowType.Tool - ) - - # Set attributes for transparency - self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) - - # Make the window cover the entire screen - self.setGeometry(QApplication.primaryScreen().geometry()) - - # Special handling for Wayland - if platform.system() == "Linux": - if "WAYLAND_DISPLAY" in os.environ: - self.setAttribute(Qt.WidgetAttribute.WA_X11NetWmWindowTypeCombo) - self.setAttribute(Qt.WidgetAttribute.WA_DontCreateNativeAncestors) - - def setup_shortcuts(self): - # Toggle visibility (Alt+Shift+T) - self.toggle_visibility_shortcut = QShortcut(QKeySequence("Alt+Shift+T"), self) - self.toggle_visibility_shortcut.activated.connect(self.toggle_visibility) - - # Toggle passthrough mode (Alt+Shift+P) - self.toggle_passthrough_shortcut = QShortcut(QKeySequence("Alt+Shift+P"), self) - self.toggle_passthrough_shortcut.activated.connect(self.toggle_passthrough) - - # Quick hide (Escape) - self.hide_shortcut = QShortcut(QKeySequence("Esc"), self) - self.hide_shortcut.activated.connect(self.hide) - - # Clear all text (Alt+Shift+C) - self.clear_shortcut = QShortcut(QKeySequence("Alt+Shift+C"), self) - self.clear_shortcut.activated.connect(self.clear_all_text) - - # Toggle background - self.toggle_background_shortcut = QShortcut(QKeySequence("Alt+Shift+B"), self) - self.toggle_background_shortcut.activated.connect(self.toggle_background) - - def toggle_visibility(self): - if self.isVisible(): - self.hide() - else: - self.show() - - def toggle_passthrough(self): - self.is_passthrough = not self.is_passthrough - if self.is_passthrough: - self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents) - if platform.system() == "Linux" and "WAYLAND_DISPLAY" not in os.environ: - self.setWindowFlags(self.windowFlags() | Qt.WindowType.X11BypassWindowManagerHint) - else: - self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, False) - if platform.system() == "Linux" and "WAYLAND_DISPLAY" not in os.environ: - self.setWindowFlags(self.windowFlags() & ~Qt.WindowType.X11BypassWindowManagerHint) - - self.hide() - self.show() - - def toggle_background(self): - """Toggle background visibility""" - self.show_background = not self.show_background - self.update() - - def set_background_opacity(self, opacity: float): - """Set background opacity (0.0 to 1.0)""" - self.background_opacity = max(0.0, min(1.0, opacity)) - self.update() - - - - def add_text_at_position(self, x: int, y: int, text: str): - """Add new text at specific coordinates""" - entry = TextEntry(text, x, y) - self.text_entries.append(entry) - self.update() - - def update_text_at_position(self, x: int, y: int, text: str): - """Update text at specific coordinates, or add if none exists""" - # Look for existing text entry near these coordinates (within 5 pixels) - for entry in self.text_entries: - if abs(entry.x - x) <= 1 and abs(entry.y - y) <= 1: - entry.text = text - self.update() - return - - # If no existing entry found, add new one - self.add_text_at_position(x, y, text) - - def setup_tray(self): - self.tray_icon = QSystemTrayIcon(self) - self.tray_icon.setIcon(QIcon.fromTheme("applications-system")) - - tray_menu = QMenu() - - toggle_action = tray_menu.addAction("Show/Hide Overlay") - toggle_action.triggered.connect(self.toggle_visibility) - - toggle_passthrough = tray_menu.addAction("Toggle Passthrough") - toggle_passthrough.triggered.connect(self.toggle_passthrough) - - # Add background toggle to tray menu - toggle_background = tray_menu.addAction("Toggle Background") - toggle_background.triggered.connect(self.toggle_background) - - clear_action = tray_menu.addAction("Clear All Text") - clear_action.triggered.connect(self.clear_all_text) - - tray_menu.addSeparator() - - quit_action = tray_menu.addAction("Quit") - quit_action.triggered.connect(self.clean_exit) - - self.tray_icon.setToolTip("Translation Overlay") - self.tray_icon.setContextMenu(tray_menu) - self.tray_icon.show() - self.tray_icon.activated.connect(self.tray_activated) - - def remove_text_at_position(self, x: int, y: int): - """Remove text entry near specified coordinates""" - self.text_entries = [ - entry for entry in self.text_entries - if abs(entry.x - x) > 1 or abs(entry.y - y) > 1 - ] - self.update() - - def paintEvent(self, event): - painter = QPainter(self) - painter.setRenderHint(QPainter.RenderHint.Antialiasing) - - # Draw each text entry - for entry in self.text_entries: - if not entry.visible: - continue - - # Set the font for this specific entry - painter.setFont(entry.font) - text_metrics = painter.fontMetrics() - - # Get the bounding rectangles for text - text_bounds = text_metrics.boundingRect( - entry.text - ) - total_width = text_bounds.width() - total_height = text_bounds.height() - - # Create rectangles for text placement - text_rect = QRect(entry.x, entry.y, total_width, total_height) - # Calculate background rectangle that encompasses both texts - if entry.background_color is not None: - bg_rect = QRect(entry.x - entry.padding, - entry.y - entry.padding, - total_width + (2 * entry.padding), - total_height + (2 * entry.padding)) - - painter.setPen(Qt.PenStyle.NoPen) - painter.setBrush(entry.background_color) - painter.drawRect(bg_rect) - - # Draw the texts - painter.setPen(entry.text_color) - painter.drawText(text_rect, Qt.AlignmentFlag.AlignLeft, entry.text) - - - -def handle_exit(signum, frame): - QApplication.quit() - -def start_overlay(): - app = QApplication(sys.argv) - - # Enable Wayland support if available - if platform.system() == "Linux" and "WAYLAND_DISPLAY" in os.environ: - app.setProperty("platform", "wayland") - - overlay = TranslationOverlay() - - overlay.show() - signal.signal(signal.SIGINT, handle_exit) # Handle Ctrl+C (KeyboardInterrupt) - signal.signal(signal.SIGTERM, handle_exit) - return (app, overlay) - # sys.exit(app.exec()) - -if ADD_OVERLAY: - app, overlay = start_overlay() - -if __name__ == "__main__": - ADD_OVERLAY = True - if not ADD_OVERLAY: - app, overlay = start_overlay() - overlay.add_text_at_position(600, 100, "Hello World I AM A BIG FAAT FOROGGGGGGGGGG") - capture = overlay.capture_behind() - capture.save("capture.png") - sys.exit(app.exec()) \ No newline at end of file diff --git a/data.py b/data.py new file mode 100644 index 0000000..1dd78b1 --- /dev/null +++ b/data.py @@ -0,0 +1,59 @@ +from sqlalchemy import create_engine, Column, Index, Integer, String, MetaData, Table, DateTime, ForeignKey, Boolean +from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.orm import relationship, declarative_base, sessionmaker +import logging +from logging_config import logger +import os +# Set up the database connection +data_dir = os.path.join(os.path.dirname(__file__), 'database') +os.makedirs(data_dir, exist_ok=True) +database_file = os.path.join(os.path.dirname(__file__), data_dir, 'translations.db') +engine = create_engine(f'sqlite:///{database_file}', echo=False) + +Session = sessionmaker(bind=engine) +session = Session() + + +Base = declarative_base() + +logging.basicConfig() +logging.getLogger('sqlalchemy.engine').setLevel(logging.WARNING) + +class Api(Base): + __tablename__ = 'api' + id = Column(Integer, primary_key=True, autoincrement=True) + model_name = Column(String, nullable=False) + site = Column(Integer, nullable=False) + rpmin = Column(Integer) # rate per minute + rph = Column(Integer) # rate per hour + rpd = Column(Integer) # rate per day + rpw = Column(Integer) # rate per week + rpmth = Column(Integer) # rate per month + rpy = Column(Integer) # rate per year + + translations = relationship("Translations", back_populates="api") + +class Translations(Base): + __tablename__ = 'translations' + id = Column(Integer, primary_key=True, autoincrement=True) + model_id = Column(Integer, ForeignKey('api.id'), nullable=False) + source_texts = Column(String, nullable=False) # as a json string + translated_texts = Column(String, nullable=False) # as a json string + source_lang = Column(String, nullable=False) + target_lang = Column(String, nullable=False) + timestamp = Column(DateTime, nullable=False) + translation_mismatch = Column(Boolean, nullable=False) + api = relationship("Api", back_populates="translations") + __table_args__ = ( + Index('idx_timestamp', 'timestamp'), + ) + + +def create_tables(): + if not os.path.exists(database_file): + Base.metadata.create_all(engine) + logger.info(f"Database created at {database_file}") + else: + logger.info(f"Using Pre-existing Database at {database_file}.") + + diff --git a/database/translations.db b/database/translations.db new file mode 100644 index 0000000000000000000000000000000000000000..3c31f9a4cd538a6cc3c7fdae5ce0733428f25a34 GIT binary patch literal 327680 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

GYgC_~!S$CEOqSBpB2h~%|d&^9O z6C^gu4~-taLgEzwU1|KA{XXSsZmK21Ficbi@j~CaLl>}ZEjPXV?{s%9Lr$IVO0`nY z|Je(}Qw;f^>-?#=HTf-cP5z%5LBb9x2_gV-v8!aU8Kft<1o$o;)l%VRudfGyK!Mj9 zK#^CY&fDFMU0H#4M*&plpI2~H*9%hwZdZ%l4bQIL<%Xx5fn;!TO}!(NlL^gtsy*ITP+YVR zf)S~6hBG4PLKzI=k%zgS=ILEX6^?{N04rw* ztEM`q;58Ghz@b^Jjm3(Bj!^V4(6^6viG}x zf2FL!NxpWslOZ*xyY8k44!Kh|-TnLdPT-YMBMkd#2`GWrFCSsGb}^3iup)M9U?OdN zVW{$%=WGx{)}ck_86+``i z53tXj^u9v?S&z1Y^Y~YoQ@#{@c~za=7?xoO=6P=~&r!?6md-&gF*)Sb%wr_8bagFLxVuN8)G4qxKShi?zRE}O!W8`Cso z$(IN29k#1V(Q8?$4_a)3wZ*6&(L^5v03%s6yZmPAKSf_0y^drfc=Xjx$AQdrg=*Iy zmzuJj3?J1boo2&c#m8@Lx|O}dx98{bRno(2!`|?czP&XxR&QgDmeTG+63UMjWn%=3 z0yV`jWZJMr?vTC&J%n!z%8=(a7v;Hi+^94hf2_R#uL0Yq4&eB2E)q~ea^fFc9IPum zv_@3QzBra)I+?n3@;W^%%U@6xpHaD3Em!NeD#MF>?CsAvtE|O;&?A|fusQ_a^KMe8 zgVxDIH4j8G*(i$|BSZ%ai_CtKImbGUcMw#J3_^N8@B5Ca^dqcypJ^OeKjiB380y0`j6v<;Qviq#|$)1Zc ziGB142yz`;%(~n3C_C-u#*%Ft-+X(4G!FROER_*q9pW^bOM{igBiS(;22QiIRxRbf zJ$#Y$;loLKWnwMJ`=ktq?4hT?Xo*BQ%0_%;c!8n6_4Sbw`rU7k=$aTLT_rNgsPHLz z2f?=Eq9(VgmU}ilCvQVsP*#)1Z}OC~QCT(JLcLO!edo$lvwB25wFLmFmStJ$Fih z@|B)ty6mtC<*BI*p-!=S>-g{mP0(-OGQP*1HlbX!6H4yLMI<9hEnx5_YEw+vb;!w+ z1EpxD926U!%z_k}%ZLuEzk?bW)Ew(F|J_ol$D)8l;9x^!P}vTlepc z?`kwdi#dWLTX%Kbx?1SB(0|pZv|cH~DA3;gjBL`%e5Hna5{~J@s5E?@Q5?NGd?^+) z(R1o)!A=H0_k$^SLVybxl)l7=lZQUh@Ainpd+lH?3Xv~Cmb!Xlb8^s+oH;HU((XZenmm?6)1EgWbXDW zii%Iynw`i^&{e)Fl}u=RgsNQ(EkJ_PqQFhpDRW@DY3m&mKy zG-^cT+)3j{B6_~QmAiP8|L9t{OmGsze(wGoSsIt{*XQtPJ7>ZLndMvXDnXReUk8Z= zM9K*nx1Bk$PT;^AFnP~~PTRHEvjiV-0LzzsX&G81U45m011Kd8v=_@-1Zj)w>64sG zp*>_M9`3x!PJn8`7};}U3KzZPlum}o&1C_2br6n9l};iC6S;XaB{;Q*8*>zJt(MDw z`sv}ztkysK)!*M>wH`3NW49|#Z)9*6g{9s;a}!=SaUuD&>8K>XGq+tMaZQD!+1j&E z_&??}fCblDxU>Z%*u#um{Li@!nF}J#Q#kHbqf>0z{~M5VpIxocFgS>&e66 zTnbHxZ4tr}z+y^Mw{M3+armno$>EQY%Q&{z>TP^=jgE)~!lnk6Q%A%IT<(hbgS)t< z+)=^afWB$jGFlW>SsH#4>CS~`=;lzIB8rjk78ejhh`izCkmw~&CWPCACO~dzy{6q5 z5R0yB$jrwl9D*Xg6OlKsuZj^(p(nC;A{Smevh$%t6B{;8OYJer*hc7|P-?JK(kq0@ zcBOsbq<+;4T@Tl9iA9Py7VXZdZnpRS{=A)a9SH}6eT$Iol9z5 zwBPRdjq|y)9Vo~I77nc-X?o2f;%bhba|H+94cnf|=@wm8xnnguHlovo`d;j>6MxSI zlg0erOFiQ4SUnaGHn(@GbCVrduUr5%N4Wr@sA-kX-R;rHl-r1!K&eE6ql9%OJ#u5s z!?Lr{Ylmy~uH=3c0$`cZW7O?D1VLcf1j@Q|F`xyw7_EzrMI>#9Tjm}n;7yD`-ZJ7+ zHkI%^tYX}SPPeMV96-}y+HB@<`16<0-0l8~8F@MC6m>dKJkb+P&=NP{VeNEQgd?`*GM^)r1?03j6}Uf2oeNAN`q`+xsTZ|2dz@yLIA@N@hB!QKlGe4Rh<{l8l;4!Pzr@J2Zl;mDfe_n&h2c6XJfutpR1P}er}m# zK}|1-4-go4l&aKmxT}J`^|k=8HP8|Kbd%h8fH5ObtndOzsZY6--G+#PvC?US>(3~W z9Fa~cORNl`p`l*6RT3#TQj0{T>O8ETB#BHcly zaxZLG)sq|!8X(99ME2*@@*+?YW;2(Y`*x5e83E`8XjW{sA2X6`EYP)HYanFX^$vmJ z9yLLUJ;z>Y`Ktx`asZTOgyoY$8{cbTYHm^kMC7h5=W#u*Fl85PdY5?hG8h4)Y?W_v zgH2a^FhnU$r6zeoYP+|=Q~+*b`?r`xEneP&#&m5;oSGRy5U)@{KEMBk;j3~Sed%3` zV|-%A5vL+jiX2h^6Q z`F#`|2@a}phmj%HWzq1ODIxcJ?J#m>BTBqD*6n%&m!u7@Uf1K@6Xg4~TtO?WWMHYpQ7PQPTC|uJBN0hk-81GLB2R?G z5?VY}57Wt5L&)?JHB{#pj`LEZ_THi4_gI?m?R(FnQ=T~D^vZj-AJ7)YBv0TYw z(4J6NIS8lVPMQ@YYVuUIHmi6~)Xv}_vh1z~ZV?{eurH5kd(&<93L+2@B3`ne>))2X zF8gajADDhf6`!^osN2ZQuRVDQwY#T`Q;dOWl;_u+Ssq0lN-8cVVoy-8<~Lb+C`h?k zS~hQ_#c!}t%uRoqBTPF2o4&k?StzbptO#@p_LnUvS#`gjE7Y&-gKtlN7UltHRjkE? zUCy7T`#~rHBBq8x1aaM=Q7^L@ZF|V{_v-R=oe+ktsV;q8L{39!10*Mkq5tK88t`pqv0SVfQA|+F7i%uxW34E3>J)ToaCe(GKPiWr4W@k;B zOW&sL6>vjSgc(K40IcRQ0&~OfFhZlo@IpL731CE>mSAEFIUy%bMK5nHU&G=E&JBy!Mtt6DI820FRPEmC`Xt4&;a zZAxU|vs)ZcQp2pU#I@6cwq?2ZmN5u0m04X>KSk?C4wEpmNU1H-LN_+81usKAASp-L z9p$2s%AnmGD)9Br2B&x#Sr@MJo5rq0l?e7C%^edFa4JrxHN15Ez1p1mVHt?6`aJpn zjYbLowe0_g_wJc_{E^50(xayj{-XmQ?*CU0{>6R&+1{%hy#Mch58Us8KkPm5((pP? z*_tIlU>9Ve5aQSc}><8nPmS!7IBWuvn)s=83^OzJtg16)G?Qf;E-mbE9r-e_j<|tw?NF zr6?S4uCC&j*RSrEm7j&excT{F)ShUk&ov|4kbS0S>d>^!UEQ0XYhV)P>%$wv4&6K4 z7+OUm_oim2?BrCp-ITydZDxB~(pd76X*W->xw92#`mwiZH{ngGo;me~(vm`8;dIBk zNsW1eAk^v&d+H9KTcQwmqgc+DMwfdvUPmY@p2StpPM`MDdZuA0^|UdTPKDs z>4*D9XS6~caQ~@0hcAVPD3F>WQFc#wgP?QoA-%@Z^OSuub`$ zQgGwzBJI0A)f>Ov5q~w620Q6_m*nWLc6;k9Qv|L2Iuyz)a8bk9zh!BQ=7djU zKJbrYffAhx_??8_A`-%6YPla=ioDe$@I)Oc>>TmJ1C6?zs9iBhdPz;r%7Ne1@mgJ# zPdI{41?V_GT}vMakVWVYu8%1;(jJ%ev(ha{`{4b#3Dk%8rPu#j$}TcDiaCsE z*v_J0UWVS!YJp%&_DbNEMkOju2@FFIWt9FO4$xI|;-zb#l zC<{rF#puegsT`xXU63jWmkxdYEOupWgPv%!i`1_>Z_CFBO+dVpL{LMP*Lh_oxqM8L zVHL4A$RRak*pQQ^SXP(e@i4@*a=NnBoqP#q%@Q7((Y4`g+Mu>Z`y@;~6c%qabaL!i z;H`J`8BMwuRLyEKTX-_lDWN0r%^p*|eAvGKI9?7i@&bNoGM6cHlhO>|!83Ghxo_ez z6^FKbrHIweqL+I2MTqy-qgI@Bz|H06;v$}H6w2!ZSCOVgsT=bZvee7<22_c|9~_uD zaPfpXcc;X-0`9mZ!)oj^>{Ny=+?X%d$zH-6R3EMl|2V5-*dI3T^rBeHm{h77i=}`p z&e9N=32k8-?jo z?#%w*+w&jIzQQl}>fdl{ctcBK|FEFW4F0FCHV|BH=_R42ve8U&ppGww>`qNhVxE<0 zz)YxLdS^;cO(oK&Cbjin)&nzhIm?Cl0{S_usfEJr7l%LO z7~~(%25P%>>WYHGgMLwQ0X9hAsx(~eFd?0h&p^w>m5pR#zY@; z$Prce(?The$d8x0j%?upIcEyo3I6^ZX^{@vqQ)n`D5f5H=(+<6;h|3?QG4kQ#*AYP zF>Bfa>nr3Dt2)iJox9SQE7l1OVUHR0lULtMEWSh!@>z5@IT;}oP7M&29!er} zrLN43FhUl1uNGBz3tuZU3uHG-*8|H?l{2@(mB!dd__`}+y2sb=_)sprON9Cz!~Ay! z0_>D+5m?}>Z$<+(Sx8m>QTf;8i>JA6K5Xhbv`Q017d zukCWN1F+Bv%7!2~j1wZPjbh@Mop{2T>ftoc!092XsWwx5#d)7P3E=j+18%JrPXdIU zxJ!*&>0?YS<0vNt`oqr6&a!I@Qx134`Zkx6M7M4`Ar_Ry{KK0#b-fR_j-Ml($rG13 z*otyRSi&K(M4iz}cz@eTHJwJS5&LO`piw(1dqnOiy1f3d^bU0!08tTh(FLBxAL zaS+s^gPg8uiYZsA&Q+9fTmDn850_9s{><^Cq@-P;NfZEOamN5O< zQh9gTQs!}opctc{b}OLAvYd(C-R7N~FxTaM=so`M%`D|cdGiHEKS zM&Vn~24a?r0RC!A$Wpi@%`eH@u%dd;7F~$RKCsMt`7?blJwCZ1!$W+XjSM9JR*nLAfk!yX+}Kbq2}&%+ zphyr&$&izh{n}x}4m)c?Uu4-Ecrjs?x8|PJMyg$1AcNDky_)+Z#=3b|M<2w63mw?@ zEZD7%NfI^nt*QMq^u4ENJi9$P%T6gjjBWubObF~sm>LHTA5JR zYr+PmvNCp?EK1kC((Bw*e@xBDz#|Au1ds^gO!5DHGaECHKm6E#b>NjfFY@cB{qNnY z!!`7r_cm@THfVZI8kmyUsrm?sMqxM7SGe`+^^4QT)4-Cn2RY^$!}C%vh+CFtV$FEw zdGfxr{T!0!QrG!DkigMpT^sLit1UQ-(^eU_);;822g$dAz!Gf?Xu&t0SEhQX?_pt= zG)t~mVe%b`YTq&%e6vVl(_xafbU)c{jhbx}Mg>#p3et=Qz}NcQ$!KAtayn_pud}~} zO~^5v^{$c;z2vX=kctKG09ubEAM(fSS`C%C%LR@JX0I)>n*{%2Ll}Sy&=rGv1?-D0 zr_<~Gw*bd1QcSe(kajmWNp%Co?ja=TE%bn{Y)IiH1)||tWr6Yp2 zwLXu|b(c}g7nflqwVL%&?gT4edq_+V!=gqgkbf4QL|z0I{YGzj%Rc*rY#dJMu!#aW z)xMpn!WZUCY#P;az0~-Vdxw3j^gp@vyD!V><$j;L0-*c64LUr!v#)_k1>28~+y6xM zq@21doWaaj+l4|zVcw_~(Jb8g#E>&J5;Ezq%Pifq-$k2tV0hTkudu^?<6Z;WcDG#qVS?;35m$c6 zPaz+mIz2VlB>Q-LFv{VR1OL2Aq-l>{?A+{;vMY{jz;Wi))*fnUqe4~aJ0sO?P3oNv zc(#FlV@)762G`??J498lqqM-pXx_M7v_j4{*DXQzGGTQJ`{-U*f>7VctP}%sh60z? zP~`?7!RERSxq^W$d zw9iGSr{CyUVY{Y}UPSv_n<)5Oo}`K}PcxM}Nw+TcvE*_bEF$`Xn>!>=fRrS;I(TAC zJ&Lh3mS>?zUcD4KaP+LP{0h4xJT`8OuA0bb)se`Xv$HFQeSoaW2G)N4cW(?=<*i-# zlSa2`;EHi+Ey=ih2Ay3C+`Evt`quk0H(}!$m$_6C`gDZibfK3@3hA`306V_+} zyKYZuNS!x3lXc+&ZK=1Gl2tq2c!3%LOFLeXmMuaq=WYT?kW`i^mI#1Q$4V16Q<87T zCmth)vLfLdOPd`pPYc1o+kYJvpsgKm3U-)_z#Xrgkc}o3cf8>)n}1A%2^&yX2S4?W z&pz2(TlOq5U?^)lUV3-Wb$|Ab4{X#TG0BE2sXhTq-0^0Ysykj{vKFrUEsLYc%EPH|GVD<_j}-e58Us8`#mt(13%e69LTUa^uBG9=HN!Ss)L z8HJq?&j1Jfn|6nvO$Z1qX`e8`t}13!^3_&#r?e(te^$2%QUq_4;cF8(epI08B~9$_ z_U!%XG8a*ui(?&s(+<`PsK|1mgxNOQ7_Ku1Z+&s}xg_2)4R@c9x@w}}gqQ&22#c`E zyUq-o#M~2$6mTPEVaTQnxFdu)0kFpy{Lu6OVy;Kv{HZl5;9;OBg(ikqNYHQRR#paH zaOyNv9ZRzEIz4p-2hjI_i?Eh&0hx_@X7&VDfa&g^TmM`v@79DOAB$jrfy4t{v>gM;rJeC^=TgSmq<4}bLVhYx@7@H-E` z_V`DSfB5(ZkH7QyYmXm&Joos_V;?>C;bR{>_ReFkJ$Cf5-0aM5|9xf8u?MJZQ$AcM z9L`r^%9$^h=JWMAz`6B&X||AYzBsSMRRmPNFq_XjPeryu9`5ea?2(M~DviZ?3ajO3 z=eD0GdqeM6(p`D>aK?E^Knn0}LO4{LeJvho=#n`*=M((FM37{Woa6X+277S zPer*>Azv@dJ~Q=vu|@-CpUyl_oFv~MZ6QB~CeC7vz@P^Rr*gK3|%nb%kFysC48`KNuqIsLpmpR-@iI8R}r%3O_dd)3P9Z)BV=Ykn3B@Pd_R zzm##lIM4Ty2vRFGW`BMA`2y@A+J9^1Vqx}+ndjAh>iL!Pv%i*kp7~p%;#svY`-SZD z`8fz8Yvt=9%F7teW_7&LO>$86> z`#kfPl8NQo>@R1Yhf1nZDpgrOzm$DG&+}128?%pRoHxE)-Cu0XK9+sHNZui=ZuQwm zGtWcOS*lYyFhBc9=J`D1TVVk;W)EhcXZ})2r7AmFLnauNy57M<- zsm=VG?DNQPW>>yE^Z#X@XMNS^{d{BQ|IIv~FU~dUz>z95|4+vG;yn5@`$x4=o%z?9 z=gW`;)bgcTZRY>ZKA$Ihuw2YnX8u*?dE`rl`CBW@{9l>pi$(BKB}$^t{L9Sql&vgR zD=hGtf01!MMKdz<_p{GOJQ*|pOXm5QSYqbyWuA{o5@!Bx=J`1Le&&D9J|E><&-|Ut z^Kstr%-`OAp8Y+}d!6|x^L(7KI`cnepO3OTXZ}{^`Iy3Y=6}pS9}&9F{12JuV^+zT zznOVHrqY}F?ozDcer6=siRp6B~2RK{cdAI&~5?G^Ha>g=~O&le?si}`$Q z_FI|f$%m&LZVCDN&5ZNNw+8!XwOlFAek1!l>#M^1YqMX^JTE+8qg*MJX6G}`mqnor zc>@~UY$N+T-v?zwxiVYNJkS1FL_8qBYT4(B?xCR2f7Oih$T#GBIbSJOW-FQJ>3{a; z2H#)KJkS1%_JpHiwv>II{RtvP#-})OUh+BRLz?;Lndf6i$jm>>IA75Cxcxu#PczTQ zhU(1!nSDO8erEnj=J^=UIrD#HpO0`pGygdId=!|T`A3=OU-ygG1CIay=Q9sI@!((E z_qS$$f99Y6;(fziw|Mv~!?$pD41e|ek^74DbU?6j3<@YOParApfab@@in<+$IKWj$ ziIrnsft z#IXs49B1#;yIV>;py;2n8kICB@}OLOO#uEd`TnP_e0ys`8U<^LaIr&hbNcD}aja)gmL@JpfxL#ge?ze7m!}c+Gp|h1! zbsXLm{xdzh3l%G}&xXz6n+TNQ>%%WdpadS01~acwRgF#;1b70Ysq+a)Sf-)bHeh^a zJ`tpCw%?#bpcL0F|M+!8Xj!dU{ z;kZL4HmD$q+)foIMJ^*vsWFq{KrpM#?81~GB67nhqY?pz~i%tJlls<~?HBa5BhkxXnQF2Kk{|F>Tq zzCrK)nJ@p|59r+kR-63pP#bh5@@cXrMYScVgp*eiwy2V;0!@ZKAsENe-PDat=n2x9 zfVGyYK#3Ui*{BB7FG^v_yX)IKp;?&zKO@S4pC|6+7n^thN4hkYk_v7Fnt`GUS)^Ij zi8bj6Y>ct2DTJCz(!47Az0y|7XOLW0g2M}UxNM^I1XtGbQN6`T!Wmn8icX4UB)L`8 zCJ{-gp99N)WbmiVGVltBgh3lAOCG?+08Xe4)Q{`UmPK;onl9&7H&j+wf{ElN%jUPT zj5=_k`DQL%{qfZ4gk2zH$bta6U=@`p80n(u3jB_?8^+ZrmCBFsR$^Ny40$Oj44DfV zQ0dx`$+I)6L*(I{;dDDZw%r^rttf^8EBe@9 zgSK9o^xjeHeR}h%6B?iG*k2c#>o;`r*gBg!@FP_)G2S7Sf0UL-jvTP3J`9|uAdHo8 z%q?6xNgt5}MjCEZ=F#3|`6!{hqw^W$)G1@vs;n#ILZF_6siIIqkF9bR_rNsf{cXIY46`t55^>Ix;WK?1PSrycs+5-I!z`ZG?JKwD;aM`6~{ zJLmu7fr~usvj6wb{H2+P|Jp-;Zr^C{f4Apv?D;8we#ZX2`-9OeO2~V!-!`0&aRwx2 zjtMG6Uv*I?DOvf5(@LW))z6B1AjYKFjTx8-&Dd;;?nh(G(2R3Ow%-fW09TSd%ujOo zP1lGjvQV20A??|HE_y444(-C7Yde{4zq;H$iSLT_{ zZ{X|)m9EiTmn#7lhMQVOq^Yyjs?;iQd9*KZD72-FRx#h7;6ID{oO-v#=XfPxA~5=t z9bcP#3gtPux?YLvr4>xKD|GUQWq6(lwTZ4hd9q2Cy^3F=oo?VFx%MPjL`eXj(_X@e zT1Sz&fW^crJrayTUl}q_Bm5_nmut<BX`%_^BCAuL7l$5) zgW>P6WJU*vPX(*!6_t>#H)@glir*Mq%pEeuh|nz^gcY@40)Jj`mZ+ixd(Z}zsD zKeYWJWho-(vNM`Wim7-^b@<%yPdtpx`_$X|ZnIaimeV(A|Be%Tay@;^CUG>c)b?+r zCyoSuG83*{i{=8iWez+USRUhj@0W)^W}x1my=`DRZ1jsX&8Vzv6zgS3N-83S7s`5r zzZ(@gzuKx*mF?T^1Vgf3w0e`BhE%6=Cs_0tRhU^ms*P$la%_E)Q8lIj-iEN4WEXcv z6qrWTz^avSOBM2=Ex6sPUIF%cTJlWEG`g$#L$-dbt+{H*IjA7q#Nm zJ}O+i9&#^*1pzWX_uXVc1Yj)>$tEwqj4Hd{R#|Ax%s|P4Ho9>GN5BT;C~-y6MbzI- zj-VGAW6Bsg6vuHYz&+40_DCaS5iNOV6&WPI0+nc6R{BjEQB;)(*Ee3T=CZl95R=R1 zG7RkJJ1pWD*l_#l;g47px0|=DEXjUBOn0KS|D!tbvb;yhlsK`C&z`YdhY<_ml#D-n z8S^zv44VbxQzMxfaxeKj(43l_CpdIP$%7`U2?9B;5wqW6g4kC1@*1QZZQq|+>f1bG znF2Hhx%_Tp_%?Ip_g?-2oaRw>AJFi(U5_yuZs8&)LTC@gf#y}zY*{D}@lt)4Da`dKm} zB9M~ayXB6mFvGg29xZmv85`+9>~nl?xucz>(o!HU`fAaAS}#i|H~)r0gkw z7+zti^&P82SXKTP_u3#mfBwoj_HA5NdJk@D-EjE|hicuT_IYAK{tVs0Gni(ffn#x; z7na^=5?^TMPJ-$;IeP(FFXqGZL5+p!TBdFhZFs3^hOTLP$5Fnd@>0^y&a8=`%RR4I zN_Eh>?F}zBd+>5?4*FEOl~-BqIt^EsM1Lw$b7rtLz@4!v{wz)hrSE)?!;0vv4V-N^ zeanlTn=~?HylYzBO2B=B{+MOlLeX=>3oHG2({TVr0c8U;z5OD;c z?Ei;n{_4!*4?XI?tbYMxDZuQof!$w?aKC3O7<{dIf*fMx=;Fs@#?DT*>he3R1Rs zNWE3AcD|}8tPa#pJOfcYb1SC_cLBocL%EAx2qaa9@xm*53+q?H*I1~+qtqVmFGzv3 z=auz59o@zAxUt(Vf|@h z*&b`nNnd)$Z&8+!lX2ely>`RmdY{FzKCR^wn>xtALRH9JM<(B%A3ZDuuyFf;#ug~V zvqvR0JF*Kjw#V~7S~$CW;#}sKgdxdU?UB?#q$Y-s!Sm;KoF1vJ^XqxJRf^R}`tLS9 zc0HW;Zg$Aph$i3IqzqHhzM7~Xq8ZODs<|_0X-%Y#M&EyS0596s_T_OcnLFVwJ#dkl zWA=lO_rrAKo#xZ?%~kPzX_=(j7AA>5vcuZg-SZ}G>M(mE-1=z$=m4{4bYS>`W{>$( zCf2`Iir3B_fhRP;E!nN+KXl4w*QA_%uhytbVVuz45_m^ZVnQjUCT)`EYK^LUQ=Mz? zmh*rm4C^ai9d-*b&^pLXR~H$e7mvMoQe3rHPF=ZtZoxmdQgsd5 zMs{gTbkd)V+AijY-Ri3>x}O(fZsZFcP1CH#rDZP{qfZ7|d1bq-%$->7t!Xd_ODyHS z+aIXq+rNc)&*pku|2KVY`ghkR?)b6{8OP8>*%8C_hpx9eVg(kh{N%%uz~ zA$rcnHh&otI-L{p%4i?YJ!Mse%)93VxgmeoO4L;fRVtL?a)vGNhNkK%N>r>*d2pOe zGCDZgtE}rILvzAfR_laMG#4xtc~Lbv3yL%pB0K4m_)1wNH81YnmnBfiB!<$gL;aAy zRFn5RJs7wxrUohaDn)2?P@dDoOXNwwapCa(i>_Bsb-|=^XMXy_(H^?v_s+a;T(;_t z1Oum_2*3gvdBtYb)?YC6VOrczeoL|pQQHWNAls)!mnL_FESs&2#CX#q`b<-qV)1K&;d0_c{o1{JlFM4+&}UlKP?y@x&!cDi(up zJk(w-3Vcn23rKb;n@gg7zN@A z`P>B*j`iTCO+Dh2aC&;y1cotsCfLPo4|QHgDKtc-HwIE>FqoM+gw}g_2(Xd(SlRy@ zj`T~vPvSXuh5)=?n&>(Xot(#qvF&{da;G~$0Lj!7$9@HJm>n)d+C`aw!_CnH68@j_ zy*Nwwt8Gm8IDqyu68Ao)9ymKgfJ}c&$Xj2XSdwOW-VW|w%BM0MW&b}qJDhp^E02Bs z(SLF9?>+qGht53s2m5aA+q3uI-}BxBvz)sB@6+soACGqYpmQ>AH3$)b;`-E zwtH(^tBW`WQPq>UQz7>n5pX2d_A8&J@MgF7<}-($v#I(dG?2P9LkX>8B7_v}TeG7t z$cggJ(ZN^=1(%Pj7YC_rB_AVFYAa1P_OGVCDi5{J3`r;7|0;%WG`{gF3iVr$ovBozbtdGG4V>tX@&~jvhRgMTYk) zYXzi81b#gF6^hGlT~N<^oN)pek=e!LT_Z%p8V+oq4#x+8$?W5&erHWd?oLuL>Wn_8 z75Mnb+?d!j8K&?+UPH2xTA0G)KCfuVGp9>HvwtenV+TXUKHy4|0(>~_nGw`0S{^Kp zneeR(ng^OvHzA_74J_$4IHFi%@m9BOBE^fWpWDfz(JG^(BesXp)zKfbRW|x|SY;pE zr-U$kx%2$O`4?V%@z{wM&gU+l zFeg;A-OkC!4k(W|baG3ZYa7qE`z>DBZjyqttlz=!g6IM(DeY88Zgkd{flv^Lp@1W` zp*C85rSWNG7Z%Ruo@M*3w+n6Y;&t#P5bU91tG3_E0*l_^<1W5>F7=SuAF{C zBrafvlvch-*I4V-HA|bcd;Z7~wRWznmJc=#)A+-7fLQ&J^e5;tKH*682}dTr;n|$N zLh<+wDc?MQ{5!HoHKI4Ub#nAejOy)2N5`~aOgfS*goQ}P8_nD^K~u=~vNXz8r`9k!s+pu$apNaI zG|Bg(R+bC7<9gZ-I>fQ1>&+EQQ2I_!u{+z$zHySW6vTzLR&a|yy{yzWfHC3*%(Vwr zI*egkBS51`=N~86%?-O5QMbwMdauFXOlNTTn~tS4i|2a-7~6WGjTaX?0Zx0Olp=q ztsKC^<`C$#pULSFHY5BOwnUo+c=b9yNkF(RWp09j>Ak})c(d=mz#UYMe8f9+eDoNq zl@sQ>&Qn^{66iK+l5JB6^EIndL<_$KHSBUmsjO@b?~Cd+?v^dHsPk4&DFv z^XP%$tD`4yL=8K`FKWh_BdTk;x-AhBC~U_hrud(JT9F!qJ+Xx$wH=j~@Iv%cHEv zbD$c*_p(iz#+|)NhhWI70jDU%Sww@JsG2h>)Z5j5ZCtv9RQpEruLv zJZi$!Pqz?O`oY_EDs=#%X`=*u??O@@LJf_)0ubd+J%Ac39Si)Ee8ClY2OBnl)(1~py;ujf<5+-Xc>MKHr)do?HbXQp zwT74|!Xq-(^7FZ|$##r7nQti5PhgEVLW$PP&0af4653#6$rJGOf=ehB5G*L?M62+y zrYXuhWqHG5JiE1GK(<8c2TFmwa|z^11(jwRY&tG**Uvi-**aF>CO{Bgm;>N#pt#fc zY-kEeZHsI{6vS;U5wVqtD8D)lj@or)-SBJv%1^v3(Duh{ADK-?U%L{0VRiy<|0(~sG201|IFzP8R&!(PQ_3Y@67cg5q0<+yA znme|!(FgHiv77Z&T9mhHLbpBRKEKlg5*HQh!Jr>Z>3+GJjnQ=U`smBn{mvS1RhBZl z_!qzXH5;yy1Qrj&3(c)%N3g~{|E;7;$NK)na%cTUW~ZNUw^-ci7w*vMqKUn=Z}c1L z^cP0^H0O<1$aA88UUN6wS@{WU_;Giv=6KT6OSWBJLP6PLpWoO#q95qc8lZ2alD4eB zL;s@MAg9ejml@V3ox%S2-V&=P{1%g}lhS>IH6uJ`#^#^m@b*s&WVJw&L7y8kKg`goTkPCi6UZS;G+F7XES1-YE#_=hgl6dy+0Y-tQ^@ z&>cEH&EPHj|H0XRH1o)R@zB5C|2um>-18Iuxc~1DX%F1`{OFJ?0*7Kn!1)X6<*3#? zfh~+E&zW_IoIq+aYLuf^f#r{-SeD^gtuz%1=m;`KjvPubHoOeD(jvdZDI=YIF+hk6~{q;|x{!5(sI} z`nd{MNABXvYJUS(t@TA2I2$V%Bh*mouaeE9no2fEs<($1ui!`nRq(9i1%x=lj3>vB!?UfKl<rxst2&`wKE5o!@OowCdE2qoFEPULoS$uaBOh-`=a--XFVDvimJ^ ztps|u9eXZoj;rOLRUYpknw;+}56sd( zX%oe90G&5TiCfAsW7HGhTOtcF!Mza&$vv&(#~}0}jB8Ij1`E$R35&hATraeo<<%up z30J|IC*f$>!*Kgt$HPHtq^bffW@J7jzpO@I`$=Z@r;`Pa zpaI|LUd%MI!+_>D(&xwrhdZ{KD!J;HfAczb^yj)vs@Q!d4wxjrI z?0B_&H9nWX{kG*}*K1GtUDMOCUsdMyCr-v*38~l1A@#aYJZ;WE3|hrW?wDPzR$ib` z`$~U(jb(st;px;me!SCb_Xh}}OK{KV!1#M!4d4frQKM8^`{`*SP065tR%tUDM=R(m>&KbR#9{x(nO$)x#R9=vGi9}(Gi@<-5fWex!Pg~?Hm?jow%o|U z`!RwY%uZ+{^jzbL=*T}ytqYE5clI}<&N6>1T@gQ~smIxN+1Hu*()h?{(4E@p(}4`v z%vni#LQzt{$+~=}clI&ArdAh2g>#GXl^#8mPS9<#l4}L|kE>SvFz67qs#2BFB*H#c zSrogs7qhRmJljWcq$Hh$%Z)x_vIcM}xx%{IwR&n=NL^i25-(j{@y_s%fVj*CV?3uQ z6dct@gUM30-0TINgKlhP?i+F&%10 zu^3=(K^d~B2jlTio9--v&iX*={t_rG+w^lVE8JAV0<_Lipkgj}PV^^exb56lf1n0* z=9Fa!d*CV?6`LAOv0ae&bek(E9ncO+%Wx{f6YP!4Kpb5EfIY`KlZn3GkS3i=O6y+k zSV9B+DqtBaXA^Iywe<`;CwZA6iDk#-6ID2q{r}KxbLP?4AO1TB_V5319{i=f|IeOv z4&48DZ+Zai)e+>=yR#$lCx=wzOq0&znS>S1&~xFTEpVD3AhC*V(?gU&&38;!IWIL| z*ey;sWH-9j?hmSWtFjZ@R?NV%vTB{S9#j)QA=*6hCeZ&`34|bTj0(`9dhpUCD?Rd5 z>4<_J5rM1{KNe3gnN-P(q|8|mA&RMeJFN9jWX-cYH*Lc>(I3F9~qLOa~rElB$ayDY`-CzDkJ@5)spLkeG#o$sIB^fMiQ&M!Lmg`ZFZH2B)LF+YREo# zB)mk{c2*s_{u&S@eaYEC?iWmbGi!IS^r?9h*L=M7;Al>+&QqhuH6hH^nLQtD1E^Zf z1Rn(EhLkTOn1sU2WOGYdgk4vlBCv!HhhIU|T-%$wWb#cZVNM-MR<>lZ&o)=J3Oq4@ zF3XxcdaI8cn_0AVO|U%f?0=C!4MZ^u{dEG%^td7`GM5l>Himiwp=UIl>n6X55x_}Sw40M|uQgYT72QgE6R5mx&VClkeTh$oqJk|$6^ z>be1)Li4fPHuIt`<%WLNaZ^@0IzBe9nW0aFzMtsvkLw~l+ga*7Cx|!1Nr(?4(7&mv zx_W5SXDzc=6BmdS4#Z6u(4dm(m`i%3@edglpJJ2khLJDT>+?pT zC3#|`jZ-%Y9JtwCeh()3Idmp~_;c{vpltEKNilrLbewTH9M5_BsRcc^TDM#GYbjL- z_tw?XZ_|Ibe>JsBZ=3d?700-Qbe*M9uM0#4R~bJU0Q+5@ch~iC2N%Nv`MBQmYr%i+ zl)EtULT`C-MR>VYccRBWbQmt>^JPjyX2QrJ!DA14pMe3jionJM)VM;gv4-e}IZ{Jx z4)!OaSDYC{zp)b9mcYo6^6J4RyxN<+R&+Wn4F1e=^Q3ARzk6`>3`6_w?Cn>z-#&29 zn1qbY2|At1$D)`DyilJJHX-#fYQ&wmrapddQbtN!s_Qb6_GnS+9!+z_c|IQRl3>%4 z4C4>{v?#eUARtV~sAM+N@#)lb#9whYow@_D)&1$XBPG~)s$6O&eAj1{h_RxT{6}Lt zT7`DA{~vg9=CS@GuOIj~`~Twyf3)|tJPVA zNChK&CagKe3MEp^*Kx+u&o(0)dnq_Onvo8sVQNjsHK+8~eJHs-W47;;i z_AuDqk$UPrG_z~TvMp>bZoRn^sp)!ijmcUkVatgdR!E(>e|Nyx(H69gfI zYY7mtkN}B*1eRPILJk-b(6S^U7l9lQ5(4@Dp6CC*^;Y#;eMs7w@vQWjDObP$x8CFb zfBw(?{GM6jsX({8g()8x?&lVX^yWiG0av4&2Ft2CYc@glH8S>YfJ($RkZ!)Zc4}5Z zR6Nm&p>964AraV3J2q(ahEIBI-Wf zf3jcFj`{4m^qd&H=@R_W*75B!g{UVcf>*Ir0Hg6<+BRFpLxy42zxD;w%sttKuy!oT= z9HC5~GM*Hb$5>g7tJ}2#9wISqz!;(NmTd6nmX$mudm8_-09f4q2ys!<{cI<-a)J{1rdto!ak{bvyFMZ1C%vhAAH59iSD$?LV2f$ z(~k5KdeW?An*@Zi^mJP8;Gpi{BU+NlOMD9V4;4M^Xm!uR(W&!RdWPr9G2L9IaVt=d ztY&h~+8E6$Gp8#RPYB!AdF(vQWPRH)doC&m0=7j1_l@O#K~z;7bVOC|Hxhax*JIuz zM*TqO%gD>!fz4^rQ+bTWgbzP~#-;;}`E%Vbg}v>UDQ`())j|V@7y?4E()%K@XAOOl z8Ku1kev{>QXlJA+cupRZFSM6uI`-!k(xPH=JBh<^RZJb=&9=bXnK9C&Vsgt)XV0h8 z+sf(b)7ju6ekrCqxz*ALue~g$WfQ^)RJRKq9&#f+Y7zLlhFUsM|Bzi%leFvYyWnF*Dyj=PI)M(p($7mF$PWzI*%BScHAG(?Z#W1fiI^eO%l9M!2TjPpsq)0yHn$@9ylx|EHwLh)1~nxI#h%pGvU>j#1w zb3{J?ea*B|(%8sQwM>f^U4`kmGT+u>ioJMqs+D(pR1%60t+3Ltp%oPw5-m|wn^dd@ zgLbl%f>p+^w*eX>LY;p76=3(5uIr_?(T~j~r}MU!VkpVfX41rRaJ-*m^#|qtr^Ro) zugg7hnE)=*E=Ukrax%51thz8nw4!Rv5u^pNf~QPP91~4a_e`zx;KVSz87Ocp)nXY`Mv~&B;+u~n7o@8zognHy zAYhS^ZTMWbV*#NZZ%1pM48<37dD;I*rhezt@t-*M_mBSW!@u>=&mH`82cFtDo%vlH z{BHP1!T4DyrvK6PA;qNg3)d1t1jFi!mGBBBIFD%7B*pSkrSTtKH)+bGLPZ;;NAcelvN zZG73{(eW031`LxPVCZ!khAJR<5|wcD$aMDX;tHJzm84)Ap37D{KsT*5(tC7RQ4?a0 zv!2WVn?~MC)x#dA*AlRUSYsAb!Ju1ObL;OU+`Bv9xNekpL-t5~Ym2QbJ%|g4wO9<3 zfTTywNk>EoXljxKxgGni$OtJH3x!;rWR|BSyc_)whU#Lf#YPtlZMUW&2Rm~%!^{nW zq4h2rQ>I9%ZKPq{Fe^nBObg*R=FcvfUGHO&p(3yYAc{b5)|Lq>8WOkNGQ>BHGRt%X zjcyZCS(TPDbRN{4Wmp9Zxc_>36D<#ln#>ucxt5+4Sp$}%26xZSvS!LSN8J*`r{U}z zr7EoAqEa^l)rhgESFp-s%4cYq0)-cltB|u{m~a2nS5pqfAFh zXrcPVRNxB?T%QsLu18gKGqX|XcuWP_TJd>#4ON7MD@)QkJO3!@l~@<@Q_bF2~OtS(S*N>KzjAmPdv?%`i0YXRJ^2HsmXMPs83fbl~TE|aisrQ@!5+5 zD^0$yZB?chs6?sl9sf)kzIaAO43gzuulU@rSA9)Di;mlW?3g= z5cjpeC>p5LOXYlV{Y1a69>K@2NNPC<)qTl`^vBW*Y_;P{!eOgaOVwI&W3^vnjc;8T zSOV8*oJ6|FF5J7yMLa$Ic%&Np-t(P|&CHI)ON>9m$;8__c)ee}(u|XNMLxddld{1) z|JaLzgBRIUapo7(!TCXw)3{0D+{K^3pDgJM_H%?oC<4qwsQ8ib|Acx(Bh)IYoRKHw zs_$kCbK%VR4s2c~ZP#!G-LN#!B|R1+@UQ zv?jU#V}61QM#XFB?@Ep0BBt(n1wo6dHR*yvgCzz7BT4v$DKThguK|dWZLAoK49Uj} zw3{=>obE2&LQ9lX=657%pgR;eIDe@u`~UdVPfeY;b^K#TFCPAdLx1Ja&m8>o2mZwV zZ|(cbd(ZEwGxTlzH+a1N61!k<+;MNjo*?M-KIDyY!N&LV}BPHF}t#gdBHijag`tO%}U)+ry=9lGM79?jsS zC8Lrdi%N}j?}%{7IF*V*zmzn318g%6Ea|AELi~iGG4QVS&JtDBi3D0KhhN;u4$fJ-yBYgE2Fyqj@@X? zMq#>&Y%Ju#l6g^t`{w%BV>>9_OI4nkmA2GwiL%Tc$s90Cnu)AYVN>G;xj7;pDzF7N zrDF|^UwB(0+QKtR(_GWFW^sR1TH=@_U~`8s_jtX;^>t0x_H~^HG1vIPX50@ZT?uEU zWq~etZbV8Ia7m=}nuJ%l6lxXF!gBk4RzLvBa*wFv#Tbe1nx#<@2%bLnP`S|G+kZg_ znjfrR(pr4+F}<`+#t314+26l3tFS)vt(j}OslX3Lv;j;9ZSuEwD`dQ1m+phx-duw1 z_pdir>5NW{UgavGy=*s*qd#GL^KDwfZWYzDa`x2dI@3^`E&}F}uhCuZ^N=J!qGp<# zjWli+dO-vIogtxM`ZX2m5V866+-t~u2)rS?H!A@~5)NC z(oCnlLdk%xVJsYNz#RiONSvPI-sT!uL8+iECzZ2qvdr%`%f>(rb9rQ2b^Vq8^IGdK zt`F_S3GAn+M3{P|QV$0HD5dX~!V?PpGjeNm?W!YOb$5|MI@3VFIh48L>Hc{@svk9^ zH0y{`ZB>iHH|)xUNXspuql)N*-&WUG`p>}x=m7euhzX1nf6!S&8+U1(?iZx2U6l|e zOHYDlIxXDaJSlm=41%S^dGQ`1ThNa5DI1q*0jcA+v8LR(rs&6 zZa2^h2v8zdnAoV^($pm?p4f(zuCxSvR;>O?M~^p6~UPigv~0nW(eWSIu?gLX}GJ6!47K zr6Ax$k+1rOon}6AGH8N}9Oc)nDcC03ooWbiW_zRNVSFQ1#SP>#366=CES53HiHa8c z@AS`UAAQ_=@_0k0)If(y04)WI2}Xq=kxI&!$IbFu;j(=?js;7{7IR5k$;-H)I$z3N z9H6$SSfm@9PS?(rvyq7#qhQ_ZN+kF-DI!Z+i;ImlG_wV{W6Z5Z%HW!0lEJFXw!v{+ zo`VuED8fY@+%=Rtdz`uZOcJGgOntggD3D>%ztMkMv;F;j!#qz_Rf4b4qCO+e{#c-C z8Y_iI$p*y|l)meJ?Xgv3%US(wxu|Fn)}As3W(B4&iSC2;C+gWO9?O+k=}U#E5y~X^ zG1G$NCLlX-#w4XsYiz=q&_}Z`_TW)6{iG0+e4|bWS&}+hG?6^%xCAYa`oC(;M)WkU zEb1F-lt-L9O;UwIiS$I=F$f*}tRiYuy(0VnVCIukC-xlrvj=|TzHjaMOPPPnpAY{H zXJGwG|8=Nyy|w-ksS*JvNIrofXkMjRt{b~{;)-^-td2@9FVhT;kWN9{L{x?VcIHCr z7=bKVMV*qj7d&G-B7hxe(Pi2x*QQY$%jMkqrT%MrwTtT?+s-y)fDqS>QY%1`6=tduJo`}>!9rHz*Nx-q{*wJB_oYGqb?Jtxb%ikq9wY*>TU z*yIs!@Q6k&cyyFj;IFw!j!)t%j^aP7cw6vh)(i5v8jo>*huFRNl6-az3dt1VXc>xem(^xSQMhdHE95f`$7V0zhYO&jyXVJRcpNKD7lWR(r{K;}e-5_nHQ$6ei$qG~ZfDWx~~IUw@C8B{QAe zaf!nZWr0n|;`!Ec?cJ|gCfXWb-wG1wQO5az9>I3RUU<-lpM`UY~J`Cg~(lo*cj10h>*ImxDi zQdY`Ep>CF|DuHs&?l-Gc^2P9Ii9l|WTTmn=jkj1e_Ih08=IsE+r!>VsN<`Ttzz9hc3KMbkoI*shqH}3e-Q%HNze(&l`D^Z{bY9JmyR^% zl|43VwY-J0bMlYZE8?+b2!c<``xGXbUT*B_SdwzQ5%XUhy#o-nOR#dLT#HbdF)Gn6 zFVl^P&w@9Qk-^{%V`>#kXDG_xNUJaUkbg0?gQ3yu{NPuUJ^6)tb^ICGgb* z27DDs62u1occFs!pYW%jF91+Q2e5sRHMF23R})R8{^Js}y8hlEOUKH>qpp&CbMb(t$oyV~}qyD3yR0al&dv zTcTR6Fh$POu+@2EEugrf;7t5@aGh+@uDvUjd+5Wj5>;XT+oVjEq1nGkD1QN`vNxY#Ax;9R2%w8}i#$LPHsJ5`v2+DYtiSHCTsFih`_l&#?0 z9MTC75G{ZD>gSaSQz@2gR zTY1fzg1B_46*9+*CNcA~(2OSo2RdmQBBTS=nbIA~JxB2E%-jk2O)h$kx7EnX(&fa_ zy@u2cAdaxa_TVkx3#uBWJ&N&fTs@YuEyt&a zi+anwm0n}KxUD0Vf#~OAkHNvy%SMguwY2kv4P@dX1YQ>qbx4L#`v#HnQ!;|pKIy@(?*>%gHn9F2x7UqlIt0Q=fW z*Sn21v~&F$h2Jba%svs~N}HPSH98(pojPMD49N&FmY|;Tl#z+*I&@UbP1g&=h$>W^ zei0sVt1&RpJqyo!cGn0`4v`M!ntJU>dDgaUz16hBYE6G)uxR2XXz7l0J7n-?rH2(A zlPi97#K!_*dKQuMk(L}nD5_|EJP2H)(ozFM7rkDS;&x{cTqP?(x!+3Kot@2fV4q1QvAprhbg+@RkkFz(1q}{dJvw88eU$RhdRiALHb~q zk$PsLg|s3{Jm+j$pJ8;!fMnT28;LR$J4vJ(WpP@Pl1Nql8lnlNsY-cmIX;9+TkI~# zuu#I>$UZJig2#2T8;dtk_=OkdDLT-g)zp}D8=*ZjYrW~;@WoO){y}kt5-x6>+@=28 z{U3vK4Sr~S&fN-EwO0)Zv)GDK18lKF{h!Mk-|5YINPLB*z%8mpK`OKCeCLS7M6;mu zYD_OEq+GQEMDrh2>oce}a4!AK=)pf}s3|7npHCxap7^*lLsWs3*;86 zvQs8GrGg@oCW*{ud22yr%@bZ=ne0$7?g3-7Sq(lleuOx2q%~b3tYVt4jVaDor>nS7 zONEV3^xu@=|6q{O6uYl2b*tf3Gi#}36PVb+@%D~(hMUlvmn34uO4V>h0nHnBjErKw zeODn7OwklX>JAsq?B`u9oZ_omI2Jc%J3HB!NRln}No_6@-pWF^U8?A4a-&Kg@TC^3 zwwj&=z|+BzmhXm|ZA`0Z!Re`a5>QI*bVkB{X3Y^0+=rai_ZDXK-SP`pz&C*9d&!aB zWfW^M>D3!&{gVg;u3v=pRX3*kZ^*TG-U-BVvQp_Ia-nK#h1jSCsQtL>XVsE$(L`|8|>N9m`|N$3>8<5Q3Nl!!8<9|*5l8#s`R2(v=%KbJPN-Ed>)1_pv0 zIeiiWrXCd>aZ7g$&zfD+ss#RW@47%>k@Ag3|X?V4yy?7PGB4RQWF*Ea(Fhv{8UbO!qA2 zSnEnb9APAs?D+TAPr{cjBu>^7ULBMXC}cAwxCV%2Dpl933=5-MeEB!BM{l33{ zQR`=swO<+QXFI?o$!728rR9zJ~F&+PjOhaUcWI0N@I0~?R^ zXC>@E@ao0M!hVRS4pa6>sX1d@vyS9!P5N~t)s@N^+(uTft=we|Tb80nS^`SGVYzs0 z-4O@eO&~K$;@$(~mJC2q6jH(K(1V5nq8Oswq(X&SrTG4KQL<-c6V9q15|7FgxgSEh zXSxc=S(`Zr3Kn8xTV*i7=i^XR42{bFS$I6%vKrR-L?Ky#lm!i-8lezfUjkZWMPl95 z2Kc@rb0uG^7HSUjCWCCt$Z zytZ+!-@EzGoa}Knk2(F(LLVh*1!J~Q-lSAc`F>x6! z>+u-1M-4%u;M)-4D9Ip@Od=*oHAgx7C4SYktC@$&2{ZAslg)ob(v^<6u&y|uKm#NS zi>25)#uf&CCKD|hG)q53VIs5Kyv-Kuz2`3}UE(okWThGIx!>^1m6#?AbR|*EHp7IL)ysDo9K*Y1+)ZA zPO`J#lTQ=I5lSG;c{FkXc?cGtyb7tu}wA?1RSPx8R*pAc=dt$h8=MUEW09Jw@j%$o9;ec$@Z)_baqb>-~K<0qa8Gvd=U zX6GgUlvf$~%eZzxmk=nlSO6*X3eQ>b#UwWOtZlIS6{RTNdT6=Xy;4U$Z#-$%33&1T)JOYeO|eNo@&?Qo2PvgWGS`7d z*hC^F!Uc_1;AHkPg?2O(WS=4RC1s>Gu@X$b|2!GuBA$lK(kFr~#KFwn5kOQu)5+2< zlBtRj*NfaBP6ziF1bJ@JTwh2iO8{Yxv68#09BTxwLff>}HG-umE@Q8`3ND_x$rJ~Z zb4%W_0IcL{P$EBe}OntxGJ zz;}uv4C+s&eJl2}BlWOwIu8%3=W|T$$NO)?gZfXce|Bpg6lETmj@VS{RPY;`vraA= zOU&L(_g;*F8Xu(S15ncCypcwZ`;`C*xJ$TjVJ)~+Np#f)8Mn?kv}0kFKV8mE7r{}? zmCpCSOljOHL&UR$jnT4$w<1QL-W^3@-STv)Mz(Z8_WzNozdm*RPab>h=)ObWI<)V= zd;2f%{YQKL7zaP}{|2x2uZznb?6*WjxUAIp;ft2#RYol*q9@l~t5j+Ca-V_kHf#CM zpS$p-?Bkc@9fkR1FLt7!LQ*I-OaMq#v=o9yE`X-g%IdD?V;%IN)pqIrN8m!|O|7YOJ*)9;AuezyO8-^Hsc$DoMqWogb7$KrtJLv*HS zt1apnnH6#c%Rp_B^yiSk!wGyZUKgO-17I*wQdT4z2n+Q^1&{9;pHU$28x!Uc;IJwo zltR6KxxcJ={ObC^;*uVb4g}YP3U>4 zYbXk62`fGC06R+6+0QvOUk1dz-ThGwk{jQ-(O956o0o&O-(^)a@XW$u&m=;k81xd> zsomLXK{t_S4ov&gMOq$Eq*^TYf2hAC$#ri1TqL;=Eiw>Xt1Ad(N2#wcied?mgI5&P z@-<`^OomacLb(xuu*Fh!e&X-#i7*}%MWUfFlLnznlTK$Bs=riUH%=#Qh4&L!AZq4X1#9Bv6C&DT49}6zP<5gCqS#C~xq^b;FS~V{2rNO6pqb zbHs7k?UqRtYA?(_mt;@Ql+IV4m#az1Qf^&gr_-ma$mis)Nmb!;4>Q1rbE<48TMlchHQ&#xWFqtlkkaIp;2~>E-NW5NmI7>=urNf`I?w<;z5Bv59m-=_ zCM%A$L!=pM_H{jH4l90htUW7@p0FA@$}dTpD%ICt?O%gnLDRG7y<8}~$dF(k;9>-< zx6w&4B9vDOok?UhK0i{Qs7Rf$A4e|*`EG8s#Wu{L*}&V%<&9t0e+L@7^@)B@+b|Ut z(R0!2<-B~z?h+1qH$H(!wU0{5=7$hvA$c8Dyqh8-aW@hyWvv4&}HRZPQzBc}*z1&I}^!PkSZKfiu^H>KtlT z*o{#aOy~Qv(r60^&STVwFF2-QzmH~LpPT-I^fyx7SLPSjqFcIyBl=4QT=ZSabo4zq zJNPq=Qs_|wlu^C{vGjjUe?bg8?^q&RxDd@Ib0f|fgcpgq5tn2K95H9HxfzW&yTfCc zPI}K70eDf#1J&G_l}12d1^)3CNO|yuw;nS#paO@88H)|=+Qwye=n$T-muvOGNBi^4 z&fvU5wrh6AOJNW8Ppg;RRS-5vGrYpQ&MP(NV;OZ5kYafq+fn#v;;2iI<} zio6*sp2=0%MAZKT#e1Bh`NciCiH1J5D@uz5M#9L{S?}Y~oIZY1zy&iM!t|{UNftD= zR2aqRePJ2L(Gm_zUl~pR&>^=&`)_h4CGB%qn0gg$VdHFnP7JO%Fo2&e3ZFs`XvgCGJ+*O6S*=T_EpnQ{)%A%&Mou`mx5Q4p~-rwCiqxPy{*@hUU}O!%q< zyd-}>e1lDrsq6UN3-pKSbQfr*G?eiX5jv)Kr@0b=OO0giZRP4Mo-UN}VAW{IapcIE zsUO>OXX=r=kKB3W_9HhRx%$YNN3xGhow$4A&WYP6Zl1V$;>?NciK*jvkKZ|d`}ob{ zSC5}Lo;^Nw?C!BU$8I0HdF<-3Ge@#VrVigdeCP1(!#59KJ$&YH_VCo9yNB)^x_#*8 zp{s|^9LgS=I(YZsorAXz-aL5q;F*KjgHs3Y9=LPh_JNxRt{ymZAbVhH|K0s}_TS!r zbN|)-XZC0JPwl(A@6Nv4`)=;Ly6?=s?7peJclX}edwcKAy;t|1*_+)vwdd}hJ6k=| zvFx#_qj!(qIePo(&7)V3o;jL5I(6jkkvm6jAGvwtDlh%;-}l4}{LtRBd$5~Jr}Ftz zxk|QBAn*5dK~QjY9A%k*IenZCWHn$)bD5t>9ml_0s}?KyYUa16j~A!0%&Mh)=C`Ge z6XT|$xl$`+emZ5`I-pevfph~i!MQDwkX)^1 z{)N=>Jopd!TrpS8{PXGKf;Fs`ilxkNNf{?lMD`FmbE%y9&8g$0{DHw$=lcI#>Nxio zU{bl1`DfF|3)4Azj#UbopGqAE<*Uj9vcA75eO&PL)l$8f`Hd;#IFqP;DkDL2nct8; zUZ7n~4m&mTld0nX*Hlp)x&Ntx#ej)NLHQJ+>_0M9qrJf5p7uUmF{p+GmwvHgV;n(ITK zxdJ}t%zDZ={UY+HC~Say=4Q%xNx(n&BG|Q=4^qa7l0m<@N;#Lyd?R(dP?)YEyr7@2 zr;fA#*c|mzA@jAAab5u&`C1Mj`OH^0kCzPAGhd-xrI`6j>bMXKD&)_VGHdDM@>DVb z#mxJu-b$DZ2E_0)0R&pnWt&iMGKD=o8}IxhM) zKT>8Xbv*WLWERuMBOgKLTFQ9BGR(Y_J|5XdnT3?`go>V-PaTi7ugqNPc$_1ZnN1l_ zB)??ZspD}{LuMv@JPPz@TB+l4lseN)9gpLmna0%Ap6>}g;^9l&+YF@BM-;@(Tum8I z#4$5(r;f+b$;_A2$D^Xu%$HKf<6^_iTdCu5X;|iqspHVM*EMB+EPXsGYstKsGM*@T z$h?t09;MW0uB48~+1Z&dq>jhUdmzp1G7Vo&Xuoyp%p3!ER?RrjEx@(3uz0$0Lm7%nPaGG0Je}^XcQc zX~*5oTu2>{A#5|xr;f)cwVCrN;|Vy|%yX&ZG2m$C+0^kE#xnCv>Ua#im^r8T|NhM1 znL7T5j{U(SO9x-z>%)KFvor9MZ}xk*41e<-(hIxk{FU2P!6E z>h^?82zR4dN<%*Kz9teGhVkLYvtQS_uhTrcvAnWKN-$2*i8%{?Sv`p2C_G5E{dy<+ zwUDc!c=_jbigNKQv*z0^ojuDR1m|>oc;Vp#OuV~NfJyc6Tb5^LX0ng#J7A>pBXH5? zOvR4SM1?%9@+bM{-?I7|{hN4we3)a2Aa*sM=fvyz1L&RO_w#e~sgiTg1i7U1CcXH` zL+ixH6K^+ZR(SvTt#`OSZ^y4tFWW3kn2^mc5S~7o@d@`ET{eZWT2)}LqBIHxr`G^Z z4b|w|(C3Rd?8%I9`iv7)NT*n9cz7uFA%@v0${+ODY>24p9W0Zhu6$@>S|VmAP&*uv z=eb5FYjg#13Y)`%=FuwH{dJ|(yXT1*gkq0#^F3nnNt%4F_YTSOPLo&2Vz3p6zidm8 zTfCWFGws#lSB~DzPSVV*c4m9Lypd(<3kRA^K6K*kHcdz3byH_%tEaaNtHhcI?IoYK!V`=5D90U_4bGWJs6c9_(aIy@xyVW!vkZCoH^ zb+6=1-NTimNq&_8y`=&s`Bs=>Z&H*eR^RdY$&nHK>@r;)qsI2REHGHh={qFhj@tNa ze}x^gae1(i*dh0wjyX!nELviR?3{o%?VD(7#<$95;hwc^F+5+gbw~6{RB|>8KZrD% zB;97SubqvhUsfy(+;9>o;JiIHxz=7}Ho`)p2x=7V91=t<1Cwc;_SnW8-=%?qUHbJ) z+0PhV#WWnf*6>=JE)kt#d_25(c7DNR-zW{)1M?gU2F3grDtwXPM z7OdHJy!WCuj zB_jGy9{JXhYlpvfsD1DU_W$vHKd|TbF!1o-=nVAV9c0*ogZ+JDLY6c=N{E}~gOjT# zJ%vobVp;jRW+c{%GfLxYDJ8}$2SWOaf!8?35V01{kJvgq=-vwnO~bMDC^p5AsSim< z8h(<+>*>k$rKpOW)6Qahr9pLo;Bzgtg`}rYF(B}nU^PYU=0^&7xs1~Dbh#)wg|D6w zm7-cFUOwTWdVKBoQ0)H z@{`$FtJlh_lF_S($w^k-@2mh^84XT_)ci9h{H@2{6o@+TEaW&&yDfT|LN!$lHRiB0 z%nEq1(_9#1HP;iY1_N|HG4CfqZ0gcl`(2F2RYQEjM5SXcMyl#-Eu->LP04unS=y{8 zqPK&O^*>PN-=qE4HS3la?3ILLlO95%JE)@Io}GtO>?>oL9yJOht~Bpa_YGd|yz5t}y2iR^>^v;D7YlHXjv5p$hy z5BC@+F6Cg=?bjxi+A-K7u{<0BI* zO4I0nU;k^G$VU^+8b>EGrHsg`bi`z>iOvJ;F*jN}^x{H~(l?VzF)a=EF8e*Iwo)^V zD~LZU7y3MDpzqqZXBOUHSo4BDD;UbYXosnTSxwWun3gy@W&K+JtD2U>>sH5Rs)Rfi z$Q}AKqB=KK9Ry7CNMC{nSyDAR6=!F7ThOK3*;fIt7R^|Z)J_Vd&P{sVM6%6d|0@)& zzIh>-{w!^AHnap*WdeVPCWAuWFXt;|Gpy9O1SzJcIkzZ`WYS~heeI^rZZdbL|3ZIF zvs>;PpabH|+;LpGT`hIK_O)}T7N5+H*lmwztxd`zGV|gWvZt~LiPw5mLsJ9#6%f1{ z;9ekCJMZgR3anDO6v2l|rs*hmFeN}h2Ibj&2h$#vpl;Qqug$kF;v{<9?!g;f{6b`v zJh4WB&sI+)DL&ze0D6m`2;4YojrAO>w8u$4&I+2@whyz}^kh2YRHyY~|9z%&{rlFf zM;oD|Y$5k4UYPc1j?k-E1>eCDc>cmA-kf(s1K~6#@?tQ&ISIF9`Jlp7k z7h)BX{22ehbA*esUmRJW@N?DvV|`On0>lwlNq_7p1M6DmCG-a92a=bAgB zxj3Ke)^z_pCh69rx6D2dGzS*1Y}q&U;yaJ8fl#Y870MavX7qEa;)Om->pa<0f_V3y~>g0%Y8V(hMp*F>zAj zFa^Ge$tOiIqT!Rir;b?yzX(XcBY&stk7S0S?Egc1et7D{`mw)uw0`7chkomUzja`W z0}uZ_oPm)U`0-ZQ0(a-f7J%aPaU; zS~h0)F&u7t) zYXK|vtCA(bFGE!pDDi$2T@k-Toy>La(o%1$S}Sf~n3%nCy8+B??o44IByB~i|v&aps7)cy~}mJ zK)v~MK$HVZTcm9K)lO$g*LhlI2LOW|j^%Tl^X&D76^GA z-dJtT^9vgNSg7T>PM0F7xx|rY+AA%TcdI&WzqhbRJ?g~}%hgq1Tv#LK@oM{eZ`Ch4 zyw*#!Jw%~Z8$vpot1*k+;tB~Y-R$LSYuV5Dmgl_v;W-0ke2tC^+1DuT?k@YEv?fd3wHDMg8SfoHKK)^Lmi8GmY#QjDzd19N4di`i$F&3@C*F3$tdW-9x+`A%oW=IzBcPqbM-!Y%{3J*r?>zdAU?u3leTeGPD7T6WA%GS=F8f^hZzy{Lv_vWaq$V#6R*?bd`F$5R@_BVQO~Hn8AL- zOUzTjCa9_>Rk>K#l?@FLWi#HHs@MjpUbqfch>4{aI7FI>`-X0juCOU+H;c^j8}cSwqqDow=T^2bLjwAm~@mwAq_$**A2RSJbmO>qqqW^XPNM>F zeDLegQfG|0p7Sz2R9Fv+i4&`lbSAujuYF;rW2POXAJP6<%+baSog4vb1Vhm|lFz?N zkiCeQ>%-*KV@52Vt>kb|Qe2K%tXnC@#Gk{NP}pn9BLS;ggwn{N82=Y8<1{G==~jR>cpT zkBap;J=bR2I@-3dM5PreO%IxKSH(d_sHz=vkB9qbXS|A>e7#Og;~!zN3Ss}mv=-nhrX z5+86-H8ps|d-5z>``?pjYI6kc-4lQUM?+jq)TU@E3DOJ+Ybo|YZxKx$595iiC!6+! z=&JMoP4+(y=tPC5Q>vGya5I5g$@dd@g)02%47jN2haC%>Haf=UnQxywB8sxv40uf zbvHW77W3kyI)apT8Ba4zn3-bl4)y7g>2l_cyE=QR2cTS_+24L-s=ZoDvfD)!5czefZSV=b4xf2Ug>nCr$8ka`%a((Nn&@6f~E zBG66q2yv&3QLgYm)ME{~gGEap0Q1gcmB*|K zD9dEB&{{hegr0;Q=Kx+~Twt6^^eC*rSa!^$dEb88yKQ49 z25qlN_g8IUuIpRMtM1}OW0Zu!*9VViJDl&YX*=wZ3{0>$d!=!%=r0<*zI91RY!W;` z5Gc4ikfyiZB`ZV0nS%L;@WOEi5EL}3j&pN>~1)wN| zKC&gE7Q5Wh4cA(;Vs~srD(MavW!udVF{{g9>nzNg0eq#c-UCo4j+O<|@I?9|P=LpJ z?%fIW+{NES>#Ih$2@DzSSB2u#UIp-*ow61+yvmZw2Qc5p&+<>LC;m>+KCQ5h$WQTt)C41GXy1$j%#lHp>- z9vL*rCDR=f+U4iyiM)az%_R7<mS-uwwNn<$xAs=+ zzw;M~_6oPjm7DDJTzLku$0tsZ9B$y@_olh>hKf{hwY}O|8CkRmZyX`6tiL!osyBXR z{kYc15z%qxZ~s#FzFL>vB{e~}Nvz&;?Zw_o=sB|03MLR@e*Ig6BfQUV`1p?-&Dr~u zX5|^Aojkk_92_YkF|uO4R+aWTs}6X-aj;;1yZNVY*;5J5HxKj4a&~isYHFE6;GL+Roq43 zoIVc~;{?EMGUh!Zlke@ox~P)em6?#++FE=DSy|Sc*1YKtje7L-))d+rNP2docNJ2c zD9pR%k@^fI-@uVN%hrX;yDfNAY8ta?o$ZQYR&5Tv_wX>6)su$PV@pFS4STPs#v~Qc z6BU@b(pbALTwje5plEk{#79tGdP|+9&Pw)qB&0)76RN<|D>3;V#uKP@Eo;|;zoyaZ z(&)v+A}&p0?KpByVpl9Kc5ayNnI&n@O6LgH;D<9a;YDer*tyRybbB)v|9B0tdcEDb zj_04_60-5tLc?>)EiGdfM>RdESJhXD%PkO0;V`|=5!Ym{tMhk`k@PMjo{2EzdTVXi?GVI*zZM+FpB)TozP;Xo~Fl)E5A2-_6zK4p;Lh zQcEtHavGjw3+=wQi~&X;Sf)*fc^JizxP+@AXG!{o2A% z+3>dr;Y#-Z!98zHJyJgLJC6R_{wxMnW!Uigle+Y9TO)8A%|{s&5N@++=_i ziae*AYMwfPfZ=?9yU>`<-jVb$-W_2Tl!S3Rmghy_jV1%X!ELtmM#d`oWlVf^X@lL( zfvaq-*i?3~+?M6P>|NI_2^jO%lBVHmJPo}D+v+0(+^tTJnoh0KV-Ty!+H>K%pA+|9 zCUV~B(u4q8**Rd8+dcFTkVu^9Bz($(hFjT5WW7glj2kP1kE4v;x;BuV6j}zxc-$`L z9@3H*q>R$ic@aJ^*>oDJ23;_Xn6{^-uDPMS{Sx-KS~JCjB+3w(Nbr(!*TsI8sFT!9 zqbSkT4Qu=-va+_6617fN+4lP=9;8q#XV24wr9nKp*~q@R@Dxp1Ea#$O#74}Bv;inj zIsQ(r0LVk$W}}}UQr(lHj(MD!`mdZc`tLSm7omJ^yfgS`jE@aI^^lK!`1x4Gz?$YJ zlD;)0wz-DUT2u74U_NrGaHOHorEr;kP7(98UnRNSm``Yw$$4hP*3=eb#%`P&e9Wch zm944mq3oP2J5%`GsMy?|(*^&BOU-dsNB4rrj7Vc`n}ns)TEoo@QCyOC9NK%yn?#;R zmbNUDYm|nz@1I7}QvzD}*3*N38d>?xk8Ko1Mve|YCM$;plemGbg!02AJzd^^r**PX zvf8;e_ExXiY_>2v$ph+9>_EsJBUW@4_sNx5Bq5SR=w~Bh^}^#zmdGPB+s;}h8gC+y z4v5Qhb|F{Uu-L6lgp@q_xNSTObb+yC0+Aw&!n9l^GD zjvt)AmiXf~v^RA3_Au_qEjLvvK)BOA1pMBey6#-wEcdt(=gabz?QY$2&6nWmzU2@M zmBk;d_OWj5yw#@WP2R)1T(?k50-&Hwelh3ApZ-?k=9wGy_bOd3&5JRM6)%&4|7iBb zPUo6jm6&T1F^|%j^l|eTUZie_4(@6g)LN8xm6OjdBX2tMCmoakfpn2x+v?Qh4Z5Te zN8Q^cCeF@xd)Y6jKynU?u0duy_TOf^pWb`3ry>h%XCcz^=>i$SG%CpD$Wy0-hmrxT z{Lmt4<86qN4VXkU=@W~>5TXF1le?Wp`0|=3@hipXQC)f{-tH$4pnN)Co~{@2q=WY_ z41S1rr1Pz%#e`_#sKcyWmOKcA>YbVEHOP{73P$Q8;7Z<7_h+gW5wQ$+b3_f}2IIuG z#Y_+@`086!%)P(*)?>69joM5ZvggRa*{6t@hNX|3L{CxD35l3ne;Krk&A;+VOdFBT zkak?gyN;&?B1?icraKbN2{yhs_(As-eMIcYYd^>I-w%_vYhknxrQgw=MN(7U@)r9L zBEy(YJJD5y{yE8>B2;vJO*{xc(OhTAjrVTjoklR(6(@q_NkN^UmIhbk&iGMDDj=OR z^5x3L^}#1ZUJHXC5qUjyZc9WPdds~!x3}OyrU{R>xQ@NrZn-**nPd`a#g1xOPMb2^ zXg-*9NhVDu8BGeadA~P2iQNVb&&jbfZzRbq{19>rMh^i(x0GT#sgeCWEoB>>HClU; zV7XZeCM}^Ine?U8MY>&7s?~aF@MD7?5Y63K-wVwhcDrMYrG5C!_zy2*8&v|#!_4yT zlB2}Esy4cV@7L<~24!&+w{;(`u7ZINFkH5smKXDt^bkkvj@gc>BqC>9#5vS7l2+ew z=7Yr8sq>vvig%oPx^e0(eB^1cLF5_=JST+L#vCl!CzFgtgt@i1nGQ7G0nP0HslPXM z{5KuV9Qv7q$M-k({ycwv@BX`WdGG|=?3;D>i>B&@WlIbi+MN)MDcPmARBBI_tysg# zNF6r|wW-2%x&&k6RubRCb-Yl;%HGSAKSbx0t>b3$nx1S#eYR{-Yc$OjINxK?p8Qp7 ztJD-z24jFv^g^=WIX|3fg)kvMy`U0vPv5G}&hnkUTAc4s<|auOy;4}!&fz?n)A4mF1z;5iD+^Pc&KJRQs12SN{5mbn zyL}5DAGIu)581+qEYcPQXo#W{)wn}pR9O$MBc00fWS16JLRK5Pdb~Kq+iY#lQ`Eeg zO|B68b98y6T3ZA*(OK65uvE7=s6v-)#r6%|Fs#$WVmQ{SRGThWX*Zc492tBH7v|tf zf4??STEbGuPBpBO+Kg56x2x=!$en5a7i0AS8Kl*acg8YyE3G=|I~B_wcAds|2?<4F z(RNK|#pRMPNW8N=}~sOpUUZcUd>ZW zrP4-o@M|Th_YMwfHGJp9%qY;^eR`CcH5PN#|0F1Q2b z3}enOEzHfYsz|s&xEnLRYlge*2(sd{vuEeRRBo&0ytL&l4+DxKtFNDvyTwaFbA z=Qc^z+G^v~PUi}=?k?7_HQIw|t=*6GFAvu)-7a*`kr~lwEX-;>C*3aOJ0y(paf8}s zp4E|F7_`7bQKtKBnN<%fT`jX_7#Z^GRAs1#3YYj4X9?-~>zFy+gX4j<&ju+0_;D)2uve zi3%x{PDo%5eB@QE7?o`)({5&gsw%>fZLZ0Gp@_M8vdCrU%yu1_aJb@;in){Q@#<-M z7#C}_kgu~zPidjQFVQe$JXyr2De|JUz(;yGI!W=Qw3cqjN`Vs<-x? z+37CJxppCo-$k3W0ki}Q(xou4)w6QCXyvHdms1B>nOuFvc;zDiCxxq=T-ADRXnC~%}EjB5e~(UuWj>s^?=0(R+H884M%W8 zD?1Gb#@QP7YP4Ex$Q#`SN<){maJE6OFTh^JaEHs7zKEL~rD^`r>gi%(x?auKN~OWs z!KX3vHa;A zMMQ8ZBN<@gqB~r~LT$QOtrqfyTaONYnC% zLNq5`PN&n;xQXN?VG7g8xf=;+;xxEx@YHINJImX~XvEGF=f|!^oIb=+60sObe+$@P z%!B8P16j4v7dWZjahOwp_n*0*vlL*c@OS65~$E^WI&UA)*K+O(N5gNa6yO zu2Cd0rs$YLT@x7w63zTD(ljT!B1tHi@>ynAY1aY4Dwwz}nHt^iB7FEpb|gNvHMQod zbd;>ui+S1q2Qq(g>i8cx`l-X$4*aQo=lJ8{zYpIGY`ixp!**_cFtB>BW2uhpavOy{ zvi243^_=@CB&^DHHw=QoU|xX8dRIu;w{6J)R_c`|mf*eB!6(y|-43m`1e`Mr`|`bA z$J80A><gZ%~rJJ3M$?0`DQ^JwVEXvZ38kLARYoFY;S3my}0#@o}u7n7sp@jR1Af zbs!Z|R_KS&owgD`6!C>-W5G}HLVSo%ZnIIHtVNS=z$FE{<0buZE={1dpsYb`isq9o zYv?(%M#Md@$sd|?G3D(lcMZZT5+JTsD%D$`9u&p$mNq^k!b`1}U;<%z-k8AMpXHp| zl0=M5-W2aX=4>wM<_t{G@TG#5Hd7YUafg?=Giitf+e#)%(FjH7%(P>liu-BG`8N0R zM2c$Odju$hMs2cqp0Y5q=%0k!6?8@Uc-#S(aLoDh)GIZeL+S;DQC7E(Qd`JQU~s|s zcnkfuDCaINBxT|>?Yq52z?drK<-%*$xx44@h|IP!0fhJHK4Jz!GWVso5O+&jqK=`Ns9@cni?m44Jxt z*OpxE78ppRcnR-FXcVMpbpZpWfS>!5sKC*o=Z{26Hy2h&V$k!E@x!A4V{Iuv@*Kl1 z`OgY)Qk4CR$6jl!1xxn51rQ}Q5y5(GTVs%|RIQp^5V?9OPbN@hP=H(q7y5>&pPJ9` z?IhQ&2sND;cF%{_bfG6yeJ1r+l1>XP|0*P+{JMr#2xi0r@W&z`SZ3}G>EwbFNdw=! zS{xX)-~?O^{7jS}nh0gio~*)EAq?Gm$Ze7yz#aJ=YNrd;>1wT1E!A)B8RS*k`0n6~ zVl<{&rFsFw{tarA_8r)8s7*8^l5%71QIbIzbQtzBZsItc5|tV%=ZC!v5Ai)4{Izj# zhih)i#q%?%g5#Riij=uZvB5JW!vgN6cg4sREWdKCIfE;}IZWh-7B8yg1)Ai^sFku( zOyM?g)S$FARmzx5UUg+F@)Prv_wFWU`D_mAio_}i5WA_&)W5cH-7K%`L{8-~l#~&X zMJ#C60X#jz4`rRY-nr54&h{2#-<1+Smr=;N;J?BOq9{O8*HRR~?y}P9b(Jol%6mZ} zn>E+LO=&3joB};eQj3t@AXE-ELvHyex@skhzEYV zIm_RHX{&YXx8>I{3s^9!AsxzfEIob-P!89+Pki&($3QnOvr!fhtztsR0>H#=*V1=~gk<`!mGVYmXLTOdgrtUN2KiPg4jO~c!CyC?>Z?WZ;R%l05Aj-Wmd5wB)VTCJ9lFioOw}5h zMsB|ONekTVEjMn!4(B^vA-aZDZoe<}4b44}fmU4^pQ6rg9Kci(Dv9lVHaN7|aent3^KBe?_X?gz}#c@go0nZ0KPLP8OKnhI&P$>}QN7Dfsvr zPI4OHytb*s24A5SqjV-)-NaU&p0-5Z^NCpf{cUH|rBbm{B=6$#;Iwb&Q{S5_{eZW# zcGUB2QoF4HBV~P)%Ng-&wTFp!vhiz?Kw#6phleIua$)0?sU2}?x`F^OUnu0tmBH$u z?i+YX%bJR7R(>dvN7GkLB0n38;wy~>s@@jI1hQL7CV^q&Fzc+ib;@2w8mDb#5zslR z?Qt)zkUu~R4O`^os_#}h7fu6Im9OS1rTXC7pr&;%_vf_ksm1Rfrga~68p`FR`P;mw z@6qRCD?WKAH(39X{cL#VTGi&76fr90D8|^t#V4%%_(tDa51UwuRirgpd74s|VllV= zF}mZzSugX1g4S`-RQyHsZXMAL>!M4IuEl(a1JSv-%{FRt zs^O=MC9yoMULX_Ay7K9e#!@Q`E)JfRcJOJhgh(QrA59OcZ|CTs#BTIbT`X7Cs7nks7~QIE93P5oMR5|MtqIx z1)G*;d}U(ij4$maVp=4`S*$lIy%bYIGYh66jSI0q-MjW^UL=>ML2?H^6S+)mmDHbs?B>!({2ZIRq0da>5abXGj8cR#oOg4jr^mJPu` zNZz;$QV=P|HEI!8ujGVyQ)-tGu@EZfOzzS$iJW%M1zx*B)Mt&dX8KDF(dd*mF$Vjz zN&kC8xuvtGmO8S~x1T2>+@)fTwzY$=49;kCKh^(gtWbPU#W`5u2ej*BNovlr5azJ_ zx)h%ox{CmYg1gMEYV)wjcF@t_6Gyzp#&+xw*E#Ml)wjs0rd1s>_DvM9V^fsAI)&+C z32^fI;Jv|9TKyN?w=$uw;zR5dXZ71^1ogma;7y_*m+^@s2xc}f|Co2;F>U<^p&Za)AvayCAs{XO ze|YN2sS_8E{rV$+`0#Hych5e|+18#D)(L@CeqpOu90=DTq^lAg?DCGxRc zt}%^Cvoly;W4S6(5>dI?RQlpYXhkEy1UMX8%A77W17n#1BS;y8oPAnvz zP#&QI;6c$*)HCRf>V746(h7WGwM&)=)hNT3M0CGW)v`BQk4!XaNKO!z+S|xNZQTXZ z=a3WKMYEuXqG899pei_s5*lE+)Snx?B(v+Ieer`-b;0!Gm06C#MI_t(C?j z^f84d(az_3xchc?>_G`#rkb5s%~O+l>$Sl}Hq^R`NHUl_qgzQ-B# zo$G4cO8xYzwO}MWOiBAKJTOsSVuVseH6%H+YFbZSrMqfG&L^`t(ZVxaPy@b@zTEJ} zKtLcaCeDbSGMcF&+ep$Es|fmgfrU7=t!(aISd3%r% zS~jCX*fdR^B{Fdw(*(dwq?N{O8{=5J0d`IzrHmu?3dmm^d9Sg|{+I<=($W$ZJ2P_# z-|S4!B&(ffN&J$mxIZJrC8`1_9Z*>&|21eyN`q1J+Dq4?&Rj7n{7&rf9m`&j=s*fB z74x-f|Ixwcp{{;;-4J0?eb`Aqgs&rQ$}Qxn7UW51eq7}@!Dn;deDHZDMC?gy%DZ1v zE79tCp{uI@pBi)Fq%xKrrGJ1|C7j!GwOSgyKDfXnZCvP^)J!#1c{NDh-RsHkkT8eDofjpK;1UwyTb+-48M9h&RuaXT_FSDn1vbuVgt;7ZkTbM4@iltm(aAxql7WRd8^FXEMcO(}x z`TcuwVc*z}S&pr&)-Tu-Vq;jKOG?a~A)Za3Kn5vZ3Z+>B)e1~!d#$~*O3?SWUqkvc zgQZf-=Leq|oM#OOU+SNaMgIhpm4rVva@b4ih0STTz~yR9Jk8X-WQmCf7Ls7P&;pye zR#4?{*$Ul!^ZV6ymJY%-Le?|X#J1esHcLn@S!ReJL!KVX0!UVt^j3NV@z-oONnptx zIE)xLOtr)5j^&!fZ1lKo-WAFuWk+=ITQD7<;VSUkmHhU??DK((6l@9Tp;vuUP#B<-hZJBCLI^E_B*?}hEn_&BQfL2Iy$&|CGdQ!^=ftF z*x+>)Y<$pfhyv2ms_u~h+Jd~}w=fy6sdW+bopn&{oB+mI+Rh1rgFyE2j%B&gE|H8@vWbxb@MEy67QQtG-8q__lj2gQB_vzvX7K@ZV124&%fD ziEkOh9qIh{49)~>#6|fOh{>mP`ExI*_IS)hWHyA7-WjSqDEzf{8y#wdpf(lHIXxL? zNuwrwp$UGng7LTJjEbXgnMbDiq`sy+j+0f|h{!DMxa_&K<3YLAP)hY?5jv#NPQI8c z);2yhxGXw6I3-;NrxdsY_S9Vyj~EeGfxKLo0_!SnHKo$kXLfJ z3#QE2)^;v;XIj#qF`p6y7kl0#el;e&B8IgiKzb2M4n!sdSbpPI4_;MX@ZP=^IBZ8J zN!jn;zuWYf9#}(0)?!Np91JjASga*_TGS1 z)d?;x_5X!ht?u~$Q%_8tIDhmP4?lC@Z|>{wxx$~D{%w4E@Fhv5Ykh;4OOr~Dyq27r zG9eu-ZTlPs>8L^9oP@nw7&)Skgy(A&1^(TEqLrI4B9E_+zcL zo;!-%hNIsD=iup8qIOkoq{t~Hk1^-q5E{NfatWeQ&cOp5U(@}%mL@pKqq{o?Q|#@# zjLZ=XiItC#DLG?lqpt7>KqKXd9qa243?gJZ&d5|*XSH+m(BlpjWfb}RB zv8QqyUmpCJSW#o}$%m{cXf6+iymronTz$fYh_?{R5!I|T7C=JwI;5y{@_ERty8!+v z6_g`{X1|zew~%(GY0FhKMr{^75kDKDGEW~k>Ghi1kyxTSeJ30YO}mDy%T;cDeDEe5 z?3>32*F<5S$z?FKn{|eyN|aQUg2uh6r0Kg~FWUSTn~&Tpz3F#SN_Oj>e9PutQko_hLolxMkOT+5!scJzf8_% zXO&I{_%fFx(%XC=g6>^%RyOHeTrhcjjO)dbzQ~ufNT<@%Zn=UJ77qKrnlI+7N3isl_net}<$8H|G zdhE=x?6Ik%caPpVdi&_jqgRifIhs8>b>!}mJ4bFGxq0O3kuyiKN2U(nJ$&cz?ZY<@ zUp;*0aQ5)jp}U9f9J+nz=Ao;H&K$}fnmTy*;GKiF58ga@_28L<*@IID?jE>v;P!!= z2d*ACb0B+QYX9B+clO`je{=uU{b%-P_fPG+yYJ4v+xu?rySnepzU;oKy?6KC*?W8M z&AnImp4pq-JGJNTo;!PP@430>>Yg)G5C8qL&A<=sJ-Y`jr*tZxKb5Ow3m`Qid_b1~ zTe4c&b2MeVdOBC1F4b%KT7J)w^zqU(weh)fdC%dL@yh99ei{UAvS{}lN*O1+r6Sg7EL(#P|{XepQLd-kV{TTL{k1#YB0`_jiPsV$$Y?b(|;UaC$P z^7VYVvS&~FIL}umJEyiMlRjRY2Fk9QtL~Xf9|tT8=U%m%`6sF4l*g8fb;3=Vf1El_ z3+_rTUn}P`|6l5Oj`CVvAfNe1DdRvY=BH}~7Pyf4e^bYyk8*{a*Fxt1NgvM}=u^F% z`G=|Fh01iLSgGgong2U=99RhGr&`Zv{z1xkK|$GCy}$xy{(kDX-oIP~g&^~PrH-q7 zuAVF8%bEW(eVkrE#cHlr%={lI<9XH8mntyda^`E6X;_cYE#?ZD zzneY|eNgyas%8GS)Nvrmg?L#iX8umMc@)M|7*&4POTq!`a-du`Cn4Uq3S4=&idoL&hdPIJAFJqUCz-Xv5@&6Qpe#B zjF(vd|2}=37W9=uyNxw4)RtPklDV5cUSfVKm0~&bH&Vv&y}^IW1};vE`ikR{D5kyk!1r>UgYnX8ub0c%j?G%zvIb9>?%9e<^)Dir!@Yv()jpoGElri zTIN4Z8Bf&mWd4)X@wgx+^B<>-CrTPJ|556AoXwy457Wn^r0~pNOdXFCjx#@>J|1Pa zX8uCzcnlkq`Sa=H5eiM_KS&u*V4`IH{q*q&dL#4aQpaO>ip-x)9gkrdGJhs@JTBbN z+({je%eyoGUdni)`Zx3MrjEyu$eBN#Iv%6xX8u&_c#OfC`I9N*2{6yhzmqy1gFa^d z?bPuYB{1_R(#Io=z04m^8Bd^}Wqxk+cme(xwKmH9Tj}Fb2b#ErBwLvPRg;neZC$#mx5NEuJ`CeHk!^zo?wZRTfF$K$T5nLn61 z9yjdF{DIW*xL0E4_ot4B{aMUseqYKs{7Kb7U~AHU+{#-zPtL8GlD}7w^ z4=`9R^E*?=(GNiOp&B-GJ7paHg#0J+QK)8qN9wrrbF`mwCG)R1{{P9T<3D}$j~@Pq z2Y>T{pW62i_x@n!uH}Bw-bYWI1f*qpM3;6`)e2vVL(7DAr$plEb1e9H9Az*nA4pxLqQl&97 z>Nn*e2OQCLqGs-l`kA#^Yrt2na!25yn(hZ1ufZS9TG)96=zK#ibGffZx}>Z&u2GWY zDdQH41`?BsPBjdZm%T&XXII?>UhOSAn1=V&jC+(3IaK74>I5)aaj?_QNzc7GZ+t8} z=G^4$m{RVv_P^=>gDZm>Vuu@s=v$ zBx5P6)mb4^SnaRpK9);LZi(8Gsfib0En1cYE|u3sdAEq#tnddbCH0cP%Szgbh$wvl z%}31??KY$`ba7g?KnoOjt6%a4TBIte=XczwgnEE?I#im00e=6HN2Z4xefQ-&i5Pk zbZjrLe=umWDE*JFTSq_d&M*cW<7Dj{$a$HH(l<3WVJGYm@N3(7dqMRt*8Jx+Xp?wL z(vLsNZZ?JNc9#Cl6htn8jhR7%DY*62;F9QaUs|S}zx+^8_>}nHJ{C}IX4MfDKR+UA z2!vg0c}uc3qNv<#s2L*ODn3mlY(^Y#PO};Mqg=^z1s7mo@4(5tGLzzF?iv6(U}79E zw&F9Rn#?MK$E!3wX)Y2-;*_3(7r{JQOGIzEm{a>j?l_@Hde_3qD+sQT7S5__7FF)1 z++S#1T`ug{6*>y8cIXADk`>!17Kx)inp`Q%TmgOuG=iB0#1N1kXS=-!R^TZb)p0p7 zIqxsH)bK`lU4A^XGM#<)J(wMt#236w7hFU#iYb1vNV6b2RMs1HR+;Ij9D9`DA{Mw) zI*VOPMC`62+6E18gkofty3;&mKM(-lm-%h zzo}=PlTQ)^!|Up+<&uhdOM+&swA5v;+-^X|#2O_C9mlvWU?+)8-vB$>@MFE?lz0T| zqfVr4*otqm#_B;7U|O`&hT%69ZLIJcy`=@pAXUiL0FQ0O%9T)AR)LFXcN?lW$@Z4X znQYCIuR<9>Xh=yr%612|xX~p{@(iV*q($uNr(>TVKwNK23(fc66oYUeu3fC0X3R9e z@A#@EnNm=KcF3}~D^Cmg*gnrvQ=ZD61%euE(Ux-cSwb!Ba&M_AKvp`s#fl37qZXy7P9Aiy6tMzuV@}UM+X`KxRgll zL_lj+Ir7{h2EHsY&=?qKYbwsgFrXd@q-0cmn0usf`-d>GX!rur>q&Pt@`c^pd#d%e z@;+8|LHNv4;(=^PCaPh>ct+&v8p*RFB*0FD`q!9M(xfR&QM}$tmQQ9AscVRqIr>I+ zJF^QEph?igyxzb$lKDV(b>xF&$kwSsyAuP62!PrDQ-5sg_%9thdgL32t{s@!cYe>) z9QZE&H#jm_Vs8u@>uchGR^&epXq+y!kQ})p@~KvD$)a|Amjudmq)%35AzPIIaZo8X zB>}kyS!F=(QMYC5k=j!A0Dy!@CaxeOtS;zy2hc5>H9l0o#--G#(y|?A8-gdlYE8D$ zFbtv{K_rCf7<{_Zo14#`QW_D+q%x7)?^B#j#kR0YIT`5vnK82}q9J9OiDPYLBjZDj zVuTx+!J=&B5BdhNJXw)QIypk`;fUSGDnpoJYg@}ya@-vr(ZVzk_`dU9*DTj7g0yqH zxU6QnmA0DH`qUF4<_iAlcClZ-HdBz@J*(CQu9#E}w5K*xLeX%|)%z;C4jKS_nnKm` zUxjBduDa8u-Ini!nncaL)DY`2LOZ2iS2?p-)R*vvbXFQCeZglfUz$cuod(U5-7mr! zXqzl>=YNdDmIw~tgke%M+0Q->CNOEH!fv6}kNdnszC*mlH1Hl^~A6%1C z@}e3{wYi-%IhqK`0dd4x02_M;?}#|w_KN0X4_O2dwx$&Bh$Vy;122KMX0}S$z)8*! zsjN5V9imHaRXN?9Bx2^!m}mb(O0*R+%JBM)1XXl9Ev#O%Ur>ZGU~t(s7EwL~%V4}B zS#P)#L6K83m?wK|JTh1i8N4|-`XS*F;>2>EKR9XOR~%OX9tel%Ew5pePG$^&3eMtF zUs)J(2Ls8R11~X_3`5=^opL0VN;)5g`|+Nm zA`{|UFi)I^<3TPsxCwfH< z9rwPjdXUI8N^Vm3M!OCF-M7yE)VnjiVz&V3%4zE=DLH3h&XE|WG1y48lPkg2JJRfS z9qLq^a|^>Q=D=HT`Zxp9td!=;sgogZ-b}dnVKRE;j!^Y>zg0bI{ZN z`*hzLeQseJ;q#V#YoBhZ`i>%I!tp3VB6oTKYscrncS8UU4)z(r5Y!twe`uG2l8#MW zt()uIQVJXu8XRZA8_ZU?Vz^(sDGcaBk>k3eV6_@c9k60Cm4_D`GECn{nCGERkIP^r z?`~t6NNlex=vvQHhG(u<^RhawFlEQf9bu*L1x(tmk^e;+$;*`xciJp> zD_&%rtiHl?(oF;6y}TDEguocu!f=NU(>Vb`0~eem zgMUv?fl$&;S|FWflAxg+E`0c$SU7RUPNoxJK#AQBNJ#txB>n|ATye!c{{}92 z-nI7r{MoV7Oqoi+xEE<+f5)}=+Iy{cy=$$K-6&x1a#R4_lj*Epp46wpoPbM-M!k{b zKiV7(HN;oFrYQTT+6uH*P${Z}05D~L&*;e9S>W`Ka-qO^F%+t%Vtk&mfwEl?9Dwo# zz&S+qq{+F}l>|d9yW!R=7CH^+q_cjiECz&o3CF@80Vs1N_xpPP&swLY*qvdW0c0!c z`a(Qiw=;v!h*~_;kSW~DeFZkr7~DjYe+J74O_ST+;lxH#J*N%Iw$sg({XVWlmurx} zXb2bn>ai4E1`@j|5Qz zg$@K-e#WU>ADbb~>UsqZBh#bY>U@oX4RxuqV=7r&4WjIa=|EFGO%r6nKu?V!R?zVo>JDC?1EoCxkMsJSZK}kpNpCwv{-Ff*h z)=}53TKl8d(2efVkzv00j5GxaH8u$!@n<@C0aIE$cq9?Scu>7F#kYu%lI9)=e2k`^ zep)}hj1YxD!f7ngU;x7wJcm~HtH$V8T{% zg*g)sFG!6S40f8xMq7uz2K)18KmDIjUv2#>D<6##KViJ*4=t9`Ma!@4vor*Nx)jEA zdTZ5_n;)7%{WH6*Yco-2+W0@jg1wxXs^8GeG74zjWo`%BP#3s=Qi!<@CjWUHj(?|GfG) zK6(cK9=|pE4Ql?ucOSj3*RddzC$KpEzxO`f#B|4&$k8H0_ zMd8?1TXt|Fo;=;BWbc;Mi!8XqiP&glV&c@&BRW{Lp+ifj%=yz?{Jh7PW!mctbptFH zR)lz#>ajW1Q{T3fh{i_KGg)0pg9pO|4>WrWbNm*Z2Z^F2`T^Wx}56hjOik!Ou%d7|pivz#rJJ3U}8lT)a*}YDujYZojh9=HPNt z<7Ubn=C~ErxfJIvPbSKB-D{0&6lzGaQkE|<88?3ryb7`O2+jq{UzVaXF_#;v56GzS(x z&D~8fv#rs}TJAQ7ixqlgFS14ec;Vyqrj4wjS}8h{f{93e1+nn#{Ugt6kUjpH&-5(5 zWBBm--<6iueX2GHUoE!6CRQ!lGTTxsu`2yb%oC@zq!u_zadt7gS)0V-P48qcmMKsQo((ic*${;ewh1v`ws*Hds)_nS)*I3ZJKlvbf8Fy# zYT=UE6gIyVX|9^}Zzd0OJ0X2<`=PRp?sPk5IasUJ%P7OGUZ17#e#b1eTRm_iIjuW% zrLm;I4QfjchbFqHAxK8|Wcz-I->SE`PbZr00j*M5fIvOob5Yz)27SDtHz1HJa|1-< zJzJm8El%(Gds=Q3=i0mdlBA)T5=lj|V!Q;->5b9J%d|_)57W)trx-b;G62i0D1?74tHg1QF(^IGJ@{2`hCv*^+Xk><_AP`x~EE!;FlWLNeplyMv)m9pCtdj99w! zF@$$d_6ZiTC+(Amab(Nooi3y}uq}ZoK}K_z&Z4LiUU%s1o1=#k!GqK9rc0+&PHV+l z*Z_$4U~f-I>|)_t3NP*r`jq|MVfjj4Bx5{JTWge}QKv+Xtvk}zva&m8bH1h{H9zL` z3)}XjA?%RyWUYalqOk{{o)Wymd-|PDZrJNT?1}p>;b#;3E<^h&-b`ySjlMW~7g^C1 z@GFv)BeQd3BNGYoy5};5JdfIgpiy87YkKZb#d=kHzv<8*zM!mW#bay^nIP#-L~uyH zqWdt}LC8dIA}!gbhi&z7Q>W+@G8SZRi|WPFHnU3yn%IS@kSI2gt)HfwQ2AepM05-= z`jz2$eUMhNXGQxxxslrrLlP+=GsBSvGYw_qUY8l2Oa(0Y|5snwx$^4YU-~`&E&nYg zu#~`30!s-jCGgY|c$^G)U6Ndx5y=HD(R?PC}FO7nvlCl zSyB~!jAb>L+2?&_9Otff8gi-fvg<39^W%4Gc1+<^Y?gAf%(v$ZzSJh>=$5;w%E_K` z?}`amtQDKWbjfk^H!in3cGVf1dV<;_?=9J!su=(XEYGhgl8K7h_$8*ns1)MGQPTH^ zgJh`8XPMF=H#)Va`C8VGr&90Y*vUEvQ#~eOjYx5dPm$k*CDamnrAxlq9lYB(Xei4b zT(vD!q>)?4Fy$6^`fUeTQ*_lSMV2iOD@<`n!%iN0O4L3sC7nibGp(~%7miqm*vk+{ zV_L}3H;^$VcWP=WE>zn;%U?c0bJyW`K_1w5{BSE*B4!A zGovdRx07$=#2R|rIR=E73*Ztx&`}dtS65U`f~=ls?q@V5Mj&n|*kE;-qH;vG zTXVRk?7{#z)YP9=m#nt<@4EW~oJO=b=h~r?2SGcevbxiZCh450XQn?}mglICx2Tzc z{@kAE&qZi%DV9QhTpayY`t#Z&tLI~G=0tyd)3HT3gch@4mFtzT2R#5sb($>p0v=ci z=LW^JR3|!y4j>$33vPX-re(eaqA{G5@;ZxCQt0=Td#CLL?t7xX}|Y z05%owWsY`%YTJMn>CYgCdYdgH4WF_dO+vHtpFhe@&sd4KxM)ksL=A)J=SC5;A&)HR z7BRv&|3Wz<4|q^l#mFlJ6QK>;G|ZlBQMaRpcq)3`TgQWewy)Vk?7P`u9bUZE_6XyU zdit{`)1S`|4Dtqpq@hw)(*C>M1nvwu+H7sC4l%y7@Pa|Qv$~SeBiE>TU>@5JK@h7D zF8}}9mA_tj>CZ3z>G_kZqfh+fxxe%0|Hi+w7soFG;GcctG_O6=$R!ySo5c3Rbao~z zDuaSDLH*NGBAiutkpDe7i=$e32jG&TmVzlNz=lrhU>~WFU%0_!a{!Kv6H0Z=^!;kv ziLZvW?(~UW>C=sbzUq)+4|0Ts_a~vZ4Xb~qBf^l*8^}y4{IhH0Ypj3B!U~3X>8$@h zVJr-YXR7|6zCM0IkowoHJVd~!&8l@XeRkC5P77}vIhhvae5Iv0#q6l=z&#e@O(UgU z*G1NZXFzDt&aS$% zuQ)Yv-&8Czyx+HsgrPUj#9mjwMH{6BtOedWwpRGvSI5uu*1vo3ft7mm#EE>J{ia?q zm#k}b=#rJ#aj3jP7NC9#iBe0S4|qx;jxN}<*F0qbOkm4EOiW7r(Zqi zQ31lQPRND7Rj17V4tsyA7$anr4!b>DWz_V{&RAafUAUG7YN&^gT~MLwtL{?{&sbY& z0ZEtXHQ>ii+_>;ul!@hg?hvk(lBL4;8HrKG(;U)Pc;;$di3Lg#@new;3!NbHaBf|! zBCj}Eu~J%8)Q!gyi{`#Y1)5k<;k;&D*M*>j1ncH57mG0>S+ILr3st8ODJ~w)E2>6Z z9UhkdT`yPV_!cc;h)9|4*U?fu`z%uwh@Y{JiXKCrI;@9-UwB{bpUIkRf~-$Mbn%O~ z`vWMg8(09n_|f)W|1kc(*-*uuZQ|vas>Ar{x)*4Rf3CU-p~U#~kTvmu8iSK~39^waY9dn2DcJ~_TBOBu({bC}HjikMs4C3jV z1M6%a|NMah3XN0*xBAy7GT5dr7J2?orHCgGNA23DNOu4D04`VjW*hnmR?0fX0@9~g zCxfGj+Q(xyrM?C8pHQB;_zv5EESOHg zT@x&y*(K&Ys4z=za2%|>QuO6Rin>w_3kALDwav6tXr~J3rR!@Msx}gi%i;As}i={ENjS3+S`v-4wNFm7$y3^){0M85Ot(yE=6WIoU{dDe1*Vw17Ud)}y!w za7xp?+`Y!3T+I6ikZ0Ga;z6Y!=qzhAvGlr>muB1C>$b0B{PIXQbJO?Ore?FqGoZE! zWkLqI_Hl|hbprMG+jOuyQh72^BjtaoU==vger|BA`Uh&X8NuT~L6{~5MRu-8joZh& zjRD+_$Sb_x=H0fPHWmSb>9cR+&nv62oyzPEWyW*IhiSf4z_g9nX!TgB5ea*UN}n2l zZE+8ksd@)gS|3bk$@2CV(UNZ?Z!V^1hO2bk2B15c8B#+j0$3)@Sq!ECP@Es=AF(St zp9i)Q$#r5nh3l<)v8UmJ9X9PiX7T@*Ygaz~msdCWZ~1R2fu#hN5?D%LDS@Q~mJ(P> zU@3vQ5*UAO{0i{x`1s5~_6s_7IGf!I{yMqYIk{78UrPxt*Okr?px70q7lL8wYQbkU6vY!9y!$|S z{)|T$IXq(TRKQoW3Eb0!Y@Es&#&8CJCzxQBj~t(dr0;cKdr60!{ChYXx!e6gm&iE< zIW4E&`>_zQl;aW`fQxQJIr_$c+RjG`Dk+b@1=JQkle4kcz**@LiX;n+f@7i^J2?iA z;b2J2Q4I`?vctt@2*#0D91;F`h+U@R^SC{JnR)T3JbECw%8Sh{V0#Qx`SXTsK$|#*zS_L8Hu7zHGVRVxonco1K$t-CyF;Scjd}Bk zFnXLwtw?_|m-P}eDW$4=)QO4pVe4*XI)|U#9>2sq8$EYwMb)fwD1i!sG__Ja1{`MQ zo!!?^Cu7`IRXW^xdXAk2ttT=Z@w;+KXku6yEL@`e4k1=v4?gxmd6L#&Jerf5?(hhX zPQI!_9M1X|Qkbu!-%|2#Ydtuza-Um(8)Pe+;tGD)guDZspRMr literal 0 HcmV?d00001 diff --git a/draw.py b/draw.py index 9cf231e..a166ec7 100644 --- a/draw.py +++ b/draw.py @@ -1,14 +1,14 @@ -from PIL import Image, ImageDraw, ImageFont +from PIL import Image, ImageDraw, ImageFont, ImageFilter import os, io, sys, numpy as np sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'helpers')) from utils import romanize, intercepts, add_furigana from logging_config import logger -from config import ADD_OVERLAY, SOURCE_LANG, MAX_TRANSLATE, FONT_FILE, FONT_SIZE, LINE_SPACING, FONT_COLOUR, LINE_HEIGHT, TO_ROMANIZE, FILL_COLOUR, REGION +from config import SOURCE_LANG, MAX_TRANSLATE, FONT_FILE, FONT_SIZE_MAX,FONT_SIZE_MIN, FONT_SIZE, LINE_SPACING, FONT_COLOUR, LINE_HEIGHT, TO_ROMANIZE, FILL_COLOUR, REGION, DRAW_TRANSLATIONS_MODE -from PySide6.QtGui import QFont font = ImageFont.truetype(FONT_FILE, FONT_SIZE) +#### CREATE A CLASS LATER so it doesn't have to inherit the same arguments all the way too confusing :| its so ass like this man i had no foresight def modify_image_bytes(image_bytes: io.BytesIO, ocr_output, translation: list) -> bytes: """Modify the image bytes with the translated text and return the modified image bytes""" @@ -24,45 +24,36 @@ def modify_image_bytes(image_bytes: io.BytesIO, ocr_output, translation: list) - modified_image_bytes = byte_stream.getvalue() return modified_image_bytes -def draw_on_image(draw: ImageDraw, translation: list, ocr_output: list, max_translate: int, replace = False) -> ImageDraw: +def draw_on_image(draw: ImageDraw, translation: list, ocr_output: list, max_translate: int, draw_mode: str = DRAW_TRANSLATIONS_MODE) -> ImageDraw: """Draw the original, translated and optionally the romanisation of the texts on the image""" translated_number = 0 bounding_boxes = [] - logger.debug(f"Translations: {len(translation)} {translation}") - logger.debug(f"OCR output: {len(ocr_output)} {ocr_output}") for i, (position, untranslated_phrase, confidence) in enumerate(ocr_output): - logger.debug(f"Untranslated phrase: {untranslated_phrase}") - if translated_number >= max_translate - 1: + if translated_number >= len(translation): # note if using api llm some issues may cause it to return less translations than expected break - if replace: - draw = draw_one_phrase_replace(draw, translation[i], position, bounding_boxes, untranslated_phrase) - else: - draw_one_phrase_add(draw, translation[i], position, bounding_boxes, untranslated_phrase) + if draw_mode == 'learn': + draw_one_phrase_learn(draw, translation[i], position, bounding_boxes, untranslated_phrase) + elif draw_mode == 'translation_only': + draw_one_phrase_translation_only(draw, translation[i], position, bounding_boxes, untranslated_phrase) + elif draw_mode == 'learn_cover': + draw_one_phrase_learn_cover(draw, translation[i], position, bounding_boxes, untranslated_phrase) + elif draw_mode == 'translation_only_cover': + draw_one_phrase_translation_only_cover(draw, translation[i], position, bounding_boxes, untranslated_phrase) translated_number += 1 - return draw -def draw_one_phrase_add(draw: ImageDraw, +def draw_one_phrase_learn(draw: ImageDraw, translated_phrase: str, position: tuple, bounding_boxes: list, untranslated_phrase: str) -> ImageDraw: """Draw the bounding box rectangle and text on the image above the original text""" - if SOURCE_LANG == 'ja': - untranslated_phrase = add_furigana(untranslated_phrase) - romanized_phrase = romanize(untranslated_phrase, 'ja') - else: - romanized_phrase = romanize(untranslated_phrase, SOURCE_LANG) - if TO_ROMANIZE: - text_content = f"{translated_phrase}\n{romanized_phrase}\n{untranslated_phrase}" - else: - text_content = f"{translated_phrase}\n{untranslated_phrase}" - - lines = text_content.split('\n') - + lines = get_lines(untranslated_phrase, translated_phrase) # Draw the bounding box - top_left, _, _, _ = position - max_width = get_max_width(lines, FONT_FILE, FONT_SIZE) - total_height = get_max_height(lines, FONT_SIZE, LINE_SPACING) + top_left, _, bottom_right,_ = position + font_size = get_font_size(top_left[1], bottom_right[1], FONT_SIZE_MAX, FONT_SIZE_MIN) + max_width = get_max_width(lines, FONT_FILE, font_size) + total_height = get_max_height(lines, font_size, LINE_SPACING) + font = ImageFont.truetype(FONT_FILE, font_size) right_edge = REGION[2] # Ensure the text is within the screen. P.S. Text on the edge may still be squished together if there are too many to translate @@ -75,58 +66,142 @@ def draw_one_phrase_add(draw: ImageDraw, adjusted_x, adjusted_y, adjusted_max_x, adjusted_max_y, _ = bounding_boxes[-1] draw.rectangle([(adjusted_x,adjusted_y), (adjusted_max_x, adjusted_max_y)], outline="black", width=1) position = (adjusted_x,adjusted_y) + + for line in lines: - draw.text(position, line, fill= FONT_COLOUR, font=font) - if ADD_OVERLAY: - overlay.add_next_text_at_position_no_update(position[0], position[1], line, text_color=FONT_COLOUR) - adjusted_y += FONT_SIZE + LINE_SPACING + if FONT_COLOUR == 'rainbow': + rainbow_text(draw, line, *position, font) + else: + draw.text(position, line, fill= FONT_COLOUR, font=font) + adjusted_y += font_size + LINE_SPACING position = (adjusted_x,adjusted_y) ### Only support for horizontal text atm, vertical text is on the todo list -def draw_one_phrase_replace(draw: ImageDraw, +def draw_one_phrase_translation_only_cover(draw: ImageDraw, translated_phrase: str, position: tuple, bounding_boxes: list, untranslated_phrase: str) -> ImageDraw: - """Cover up old text and add translation directly on top""" - # Draw the bounding box - top_left, _, _, bottom_right = position - max_width = bottom_right[0] - top_left[0] - font_size = bottom_right[1] - top_left[1] - draw.rectangle([top_left, bottom_right], fill=FILL_COLOUR) - while True: - font = ImageFont.truetype(FONT_FILE, font_size) - if font.get_max_width < max_width: - draw.text(top_left, translated_phrase, fill= FONT_COLOUR, font=font) - break - elif font_size <= 1: - break + """Cover up old text and add translation directly on top""" + # Draw the bounding box + top_left, _, bottom_right, _ = position + bounding_boxes.append((top_left[0], top_left[1], bottom_right[0], bottom_right[1], untranslated_phrase)) # Debugging purposes + max_width = bottom_right[0] - top_left[0] + font_size = get_font_size(top_left[1], bottom_right[1], FONT_SIZE_MAX, FONT_SIZE_MIN) + while True: + font = ImageFont.truetype(FONT_FILE, font_size) + phrase_width = get_max_width(translated_phrase, FONT_FILE, font_size) + rectangle = get_rectangle_coordinates(translated_phrase, top_left, FONT_FILE, font_size, LINE_SPACING) + + if phrase_width < max_width: + draw.rectangle(rectangle, fill=FILL_COLOUR) + if FONT_COLOUR == 'rainbow': + rainbow_text(draw, translated_phrase, *top_left, font) else: - font_size -= 1 - -def get_max_width(lines: list, font_path, font_size) -> int: + draw.text(top_left, translated_phrase, fill= FONT_COLOUR, font=font) + + break + elif font_size <= FONT_SIZE_MIN: + break + else: + font_size -= 1 + +def draw_one_phrase_learn_cover(draw: ImageDraw, + translated_phrase: str, + position: tuple, bounding_boxes: list, + untranslated_phrase: str) -> ImageDraw: + """Cover up old text and add translation directly on top""" + lines = get_lines(untranslated_phrase, translated_phrase) + # Draw the bounding box + top_left, _, bottom_right,_ = position + font_size = get_font_size(top_left[1], bottom_right[1], FONT_SIZE_MAX, FONT_SIZE_MIN) + max_width = get_max_width(lines, FONT_FILE, font_size) + total_height = get_max_height(lines, font_size, LINE_SPACING) + font = ImageFont.truetype(FONT_FILE, font_size) + right_edge = REGION[2] + + # Ensure the text is within the screen. P.S. Text on the edge may still be squished together if there are too many to translate + x_onscreen = top_left[0] if top_left[0] + max_width <= right_edge else right_edge - max_width + y_onscreen = max(top_left[1] - int(total_height/3), 0) + bounding_box = (x_onscreen, y_onscreen, x_onscreen + max_width, y_onscreen + total_height, untranslated_phrase) + + adjust_if_intersects(x_onscreen, y_onscreen, bounding_box, bounding_boxes, untranslated_phrase, max_width, total_height) + + adjusted_x, adjusted_y, adjusted_max_x, adjusted_max_y, _ = bounding_boxes[-1] + draw.rounded_rectangle([(adjusted_x,adjusted_y), (adjusted_max_x, adjusted_max_y)], fill=FILL_COLOUR,outline="black", width=2, radius=5) + position = (adjusted_x,adjusted_y) + + + for line in lines: + if FONT_COLOUR == 'rainbow': # easter egg yay + rainbow_text(draw, line, *position, font) + else: + draw.text(position, line, fill= FONT_COLOUR, font=font) + adjusted_y += font_size + LINE_SPACING + position = (adjusted_x,adjusted_y) + +def draw_one_phrase_translation_only(draw: ImageDraw, + translated_phrase: str, + position: tuple, bounding_boxes: list, + untranslated_phrase: str) -> ImageDraw: + """Cover up old text and add translation directly on top""" + # Draw the bounding box + pass + +def get_rectangle_coordinates(lines: list | str, top_left: tuple | list, font_path, font_size, line_spacing, padding: int = 1) -> list: + + """Get the coordinates of the rectangle surrounding the text""" + + text_width = get_max_width(lines, font_path, font_size) + text_height = get_max_height(lines, font_size, line_spacing) + x1 = top_left[0] - padding + y1 = top_left[1] - padding + x2 = top_left[0] + text_width + padding + y2 = top_left[1] + text_height + padding + return [(x1,y1), (x2,y2)] + +def get_max_width(lines: list | str, font_path, font_size) -> int: """Get the maximum width of the text lines""" font = ImageFont.truetype(font_path, font_size) max_width = 0 dummy_image = Image.new("RGB", (1, 1)) draw = ImageDraw.Draw(dummy_image) - for line in lines: - bbox = draw.textbbox((0,0), line, font=font) - line_width = bbox[2] - bbox[0] - max_width = max(max_width, line_width) + if isinstance(lines, list): + for line in lines: + bbox = draw.textbbox((0,0), line, font=font) + line_width = bbox[2] - bbox[0] + max_width = max(max_width, line_width) + else: + bbox = draw.textbbox((0,0), lines, font=font) + max_width = bbox[2] - bbox[0] return max_width -def get_max_height(lines: list, font_size, line_spacing) -> int: +def get_max_height(lines: list | str, font_size, line_spacing) -> int: """Get the maximum height of the text lines""" - return len(lines) * (font_size + line_spacing) + no_of_lines = len(lines) if isinstance(lines, list) else 1 + return no_of_lines * (font_size + line_spacing) + +def get_lines(untranslated_phrase: str, translated_phrase: str) -> list: + """Get the translated. untranslated and optionally the romanised text as a list""" + if SOURCE_LANG == 'ja': + untranslated_phrase = add_furigana(untranslated_phrase) + romanized_phrase = romanize(untranslated_phrase, 'ja') + else: + romanized_phrase = romanize(untranslated_phrase, SOURCE_LANG) + if TO_ROMANIZE: + text_content = f"{translated_phrase}\n{romanized_phrase}\n{untranslated_phrase}" + else: + text_content = f"{translated_phrase}\n{untranslated_phrase}" + return text_content.split('\n') + def adjust_if_intersects(x: int, y: int, bounding_box: tuple, bounding_boxes: list, untranslated_phrase: str, max_width: int, total_height: int) -> tuple: - """Adjust the y coordinate if the bounding box intersects with any other bounding box""" + """Adjust the y coordinate every time the bounding box intersects with any previous bounding boxes. OCR returns results from top to bottom so it works.""" y = np.max([y,0]) if len(bounding_boxes) > 0: for box in bounding_boxes: @@ -136,3 +211,36 @@ def adjust_if_intersects(x: int, y: int, bounding_boxes.append(adjusted_bounding_box) return adjusted_bounding_box + +def get_font_size(y_1, y_2, font_size_max: int, font_size_min: int) -> int: + """Get the average of the maximum and minimum font sizes""" + if font_size_min > font_size_max: + raise ValueError("Minimum font size cannot be greater than maximum font size") + font_size = min( + max(int(abs(2/3*(y_2-y_1))), font_size_min), + font_size_max) + return font_size + + + + +def rainbow_text(draw,text,x,y,font): + for i, letter in enumerate(text): + # Calculate hue for rainbow effect + # Convert HSV to RGB (using full saturation and value) + rgb = tuple(np.random.randint(50,255,3)) + # Get the width of this letter + + letter_bbox = draw.textbbox((x, y), letter, font=font) + letter_width = letter_bbox[2] - letter_bbox[0] + + # Draw the letter + draw.text((x, y), letter, fill=rgb, font=font) + + # Move x position for next letter + x += letter_width + + +if __name__ == "__main__": + pass + \ No newline at end of file diff --git a/helpers/batching.py b/helpers/batching.py index 5c49437..abee2ba 100644 --- a/helpers/batching.py +++ b/helpers/batching.py @@ -1,142 +1,326 @@ from torch.utils.data import Dataset, DataLoader from typing import List, Dict +from datetime import datetime, timedelta from dotenv import load_dotenv -import os , sys, torch, time, ast +import os , sys, torch, time, ast, json, pytz from werkzeug.exceptions import TooManyRequests from multiprocessing import Process, Event, Value load_dotenv() sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) -from config import device, GEMINI_API_KEY, GROQ_API_KEY +from config import device, GEMINI_API_KEY, GROQ_API_KEY, MAX_TRANSLATE from logging_config import logger -from groq import Groq +from groq import Groq as Groqq import google.generativeai as genai +from google.api_core.exceptions import ResourceExhausted import asyncio +import aiohttp from functools import wraps - +from data import session, Api, Translations +from typing import Optional class ApiModel(): - def __init__(self, model, # model name - rate, # rate of calls per minute - api_key, # api key for the model wrt the site + def __init__(self, model, # model name as defined by the API + site, # site of the model; # to be precise, use the name as defined precisely by the class names in this script, i.e. Groqq and Gemini + api_key: Optional[str] = None, # api key for the model wrt the site + rpmin: Optional[int] = None, # rate of calls per minute + rph: Optional[int] = None, # rate of calls per hour + rpd: Optional[int] = None, # rate of calls per day + rpw: Optional[int] = None, # rate of calls per week + rpmth: Optional[int] = None, # rate of calls per month + rpy: Optional[int] = None # rate of calls per year ): - self.model = model - self.rate = rate self.api_key = api_key - self.curr_calls = Value('i', 0) - self.time = Value('i', 0) - self.process = None - self.stop_event = Event() - self.site = None + self.model = model + self.rpmin = rpmin + self.rph = rph + self.rpd = rpd + self.rpw = rpw + self.rpmth = rpmth + self.rpy = rpy + self.site = site self.from_lang = None self.target_lang = None - self.request = None # request response from API + self.db_table = None + self.session_calls = 0 + self._id = None + self._set_db_model_id() if self._get_db_model_id() else self.update_db() + # Create the table if it does not already exist + def __repr__(self): - return f'{self.site} Model: {self.model}; Rate: {self.rate}; Current_Calls: {self.curr_calls.value} calls; Time Passed: {self.time.value} seconds.' + return f'{self.site} Model: {self.model}; Total calls this session: {self.session_calls}; rpmin: {self.rpmin}; rph: {self.rph}; rpd: {self.rpd}; rpw: {self.rpw}; rpmth: {self.rpmth}; rpy: {self.rpy}' def __str__(self): return self.model + async def __aenter__(self): + self.session = aiohttp.ClientSession() + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + if self.session: + await self.session.close() + def _get_db_model_id(self): + model = session.query(Api).filter_by(model_name = self.model, site = self.site).first() + if model: + return model.id + else: + return None + + def _set_db_model_id(self): + self._id = self._get_db_model_id() + + @staticmethod + def _get_time(): + return datetime.now(tz=pytz.timezone('Australia/Sydney')) + def set_lang(self, from_lang, target_lang): self.from_lang = from_lang self.target_lang = target_lang - ### CHECK MINUTELY API RATES. For working with hourly rates and monthly will need to create another file. Also just unlikely those rates will be hit - async def api_rate_check(self): - # Background task to manage the rate of calls to the API - while not self.stop_event.is_set(): - start_time = time.monotonic() - self.time.value += 5 - if self.time.value >= 60: - self.time.value = 0 - self.curr_calls.value = 0 - elapsed = time.monotonic() - start_time - # Sleep for exactly 5 seconds minus the elapsed time - sleep_time = max(0, 5 - elapsed) - await asyncio.sleep(sleep_time) + def set_db_table(self, db_table): + self.db_table = db_table + + def update_db(self): + api = session.query(Api).filter_by(model_name = self.model, site = self.site).first() + if not api: + api = Api(model_name = self.model, + site = self.site, + rpmin = self.rpmin, + rph = self.rph, + rpd = self.rpd, + rpw = self.rpw, + rpmth = self.rpmth, + rpy = self.rpy) + session.add(api) + session.commit() + self._set_db_model_id() + else: + api.rpmin = self.rpmin + api.rph = self.rph + api.rpd = self.rpd + api.rpw = self.rpw + api.rpmth = self.rpmth + api.rpy = self.rpy + session.commit() + + def _db_add_translation(self, text: list | str, translation: list, mismatch = False): + text = json.dumps(text) if isinstance(text, list) else json.dumps([text]) + translation = json.dumps(translation) + translation = Translations(source_texts = text, translated_texts = translation, + model_id = self._id, source_lang = self.from_lang, target_lang = self.target_lang, + timestamp = datetime.now(tz=pytz.timezone('Australia/Sydney')), + translation_mismatch = mismatch) + session.add(translation) + session.commit() + + @staticmethod + def _single_period_calls_check(max_calls, call_count): + if not max_calls: + return True + if max_calls <= call_count: + return False + else: + return True + + def _are_rates_good(self): + curr_time = self._get_time() + min_ago = curr_time - timedelta(minutes=1) + hour_ago = curr_time - timedelta(hours=1) + day_ago = curr_time - timedelta(days=1) + week_ago = curr_time - timedelta(weeks=1) + month_ago = curr_time - timedelta(days=30) + year_ago = curr_time - timedelta(days=365) + min_calls = session.query(Translations).join(Api). \ + filter(Api.id==self._id, + Translations.timestamp >= min_ago + ).count() + hour_calls = session.query(Translations).join(Api). \ + filter(Api.id==self._id, + Translations.timestamp >= hour_ago + ).count() + day_calls = session.query(Translations).join(Api). \ + filter(Api.id==self._id, + Translations.timestamp >= day_ago + ).count() + week_calls = session.query(Translations).join(Api). \ + filter(Api.id==self._id, + Translations.timestamp >= week_ago + ).count() + month_calls = session.query(Translations).join(Api). \ + filter(Api.id==self._id, + Translations.timestamp >= month_ago + ).count() + year_calls = session.query(Translations).join(Api). \ + filter(Api.id==self._id, + Translations.timestamp >= year_ago + ).count() + if self._single_period_calls_check(self.rpmin, min_calls) \ + and self._single_period_calls_check(self.rph, hour_calls) \ + and self._single_period_calls_check(self.rpd, day_calls) \ + and self._single_period_calls_check(self.rpw, week_calls) \ + and self._single_period_calls_check(self.rpmth, month_calls) \ + and self._single_period_calls_check(self.rpy, year_calls): + return True + else: + logger.warning(f"Rate limit reached for {self.model} from {self.site}. Current calls: {min_calls} in the last minute; {hour_calls} in the last hour; {day_calls} in the last day; {week_calls} in the last week; {month_calls} in the last month; {year_calls} in the last year.") + return False + + + # async def request_func(request): + # @wraps(request) + # async def wrapper(self, text, *args, **kwargs): + # if await self._are_rates_good(): + # try: + # self.session_calls += 1 + # response = await request(self, text, *args, **kwargs) + # return response + # except Exception as e: + # logger.error(f"Error with model {self.model} from {self.site}. Error: {e}") + # else: + # logger.error(f"Rate limit reached for this model.") + # raise TooManyRequests('Rate limit reached for this model.') + # return wrapper - def background_task(self): - asyncio.run(self.api_rate_check()) + # @request_func + async def translate(self, texts_to_translate, store = False): + if isinstance(texts_to_translate, str): + texts_to_translate = [texts_to_translate] + if len(texts_to_translate) == 0: + return [] + #prompt = f"Without any additional remarks, and without any code, translate the following items of the Python list from {self.from_lang} into {self.target_lang} and output as a Python list ensuring proper escaping of characters and ensuring the length of the list given is exactly equal to the length of the list you provide. Do not output in any other language other than the specified target language: {texts_to_translate}" + prompt = f"""INSTRUCTIONS: +- Provide ONE and ONLY ONE translation to each text provided in the JSON array given. +- The translations must preserve the original order. +- Each translation must be from the Source language to the Target language +- Source language: {self.from_lang} +- Target language: {self.target_lang} +- Texts are provided in JSON array syntax. +- Respond using ONLY valid JSON array syntax. +- Do not include explanations or additional text +- Escape special characters properly - def start(self): - # Start the background task - self.process = Process(target=self.background_task) - self.process.daemon = True - self.process.start() - logger.info(f"Background process started with PID: {self.process.pid}") +Input texts: +{texts_to_translate} - def stop(self): - # Stop the background task - logger.info(f"Stopping background process with PID: {self.process.pid}") - self.stop_event.set() - if self.process: - self.process.join(timeout=5) - if self.process.is_alive(): - self.process.terminate() +Expected format: +["translation1", "translation2", ...] - def request_func(request): - @wraps(request) - def wrapper(self, text, *args, **kwargs): - if self.curr_calls.value < self.rate: - # try: - response = request(self, text, *args, **kwargs) - self.curr_calls.value += 1 - return response - # except Exception as e: - #logger.error(f"Error with model {self.model} from {self.site}. Error: {e}") - else: - logger.error(f"Rate limit reached for this model. Please wait for the rate to reset in {60 - self.time} seconds.") - raise TooManyRequests('Rate limit reached for this model.') - return wrapper +Translation:""" + response = await self._request(prompt) + response_list = ast.literal_eval(response.strip()) + logger.debug(repr(self)) + logger.info(f'{self.model} translated texts from: {texts_to_translate} to {response_list}.') + + if len(response_list) != len(texts_to_translate) and len(texts_to_translate) <= MAX_TRANSLATE: + logger.error(f"{self.model} model failed to translate all the texts. Number of translations to make: {len(texts_to_translate)}; Number of translated texts: {len(response_list)}.") + if store: + self._db_add_translation(texts_to_translate, response_list, mismatch=True) + else: + if store: + self._db_add_translation(texts_to_translate, response_list) + print(response_list) + return response_list + +class Groq(ApiModel): + def __init__(self, # model name as defined by the API + model, + api_key = GROQ_API_KEY, # api key for the model wrt the site + **kwargs): + super().__init__(model, + api_key = api_key, + site = 'Groq', **kwargs) + self.client = Groqq() - @request_func - def translate(self, request_fn, texts_to_translate): - if len(texts_to_translate) == 0: - return [] - prompt = f"Without any additional remarks, and without any code, translate the following items of the Python list from {self.from_lang} into {self.target_lang} and output as a Python list ensuring proper escaping of characters: {texts_to_translate}" - response = request_fn(self, prompt) - return ast.literal_eval(response.strip()) - -class Groqq(ApiModel): - def __init__(self, model, rate, api_key = GROQ_API_KEY): - super().__init__(model, rate, api_key) - self.site = "Groq" - - def request(self, content): - client = Groq() - chat_completion = client.chat.completions.create( - messages=[ - { - "role": "user", - "content": content, + async def _request(self, content: str) -> str: + async with aiohttp.ClientSession() as session: + async with session.post( + "https://api.groq.com/openai/v1/chat/completions", + headers={ + "Authorization": f"Bearer {GROQ_API_KEY}", + "Content-Type": "application/json" + }, + json={ + "messages": [{"role": "user", "content": content}], + "model": self.model } - ], - model=self.model - ) - return chat_completion.choices[0].message.content + ) as response: + response_json = await response.json() + return response_json["choices"][0]["message"]["content"] +# https://console.groq.com/settings/limits for limits + # def request(self, content): + # chat_completion = self.client.chat.completions.create( + # messages=[ + # { + # "role": "user", + # "content": content, + # } + # ], + # model=self.model + # ) + # return chat_completion.choices[0].message.content - def translate(self, texts_to_translate): - return super().translate(Groqq.request, texts_to_translate) + # async def translate(self, texts_to_translate): + # return super().translate(self.request, texts_to_translate) class Gemini(ApiModel): - def __init__(self, model, rate, api_key = GEMINI_API_KEY): - super().__init__(model, rate, api_key) - self.site = "Gemini" + def __init__(self, # model name as defined by the API + model, + api_key = GEMINI_API_KEY, # api key for the model wrt the site + **kwargs): + super().__init__(model, + api_key = api_key, + site = 'Google', + **kwargs) - def request(self, content): - genai.configure(api_key=self.api_key) - safety_settings = { - "HARM_CATEGORY_HARASSMENT": "BLOCK_NONE", - "HARM_CATEGORY_HATE_SPEECH": "BLOCK_NONE", - "HARM_CATEGORY_SEXUALLY_EXPLICIT": "BLOCK_NONE", - "HARM_CATEGORY_DANGEROUS_CONTENT": "BLOCK_NONE"} - response = genai.GenerativeModel(self.model).generate_content(content, safety_settings=safety_settings) - return response.text.strip() + # def request(self, content): + # genai.configure(api_key=self.api_key) + # safety_settings = { + # "HARM_CATEGORY_HARASSMENT": "BLOCK_NONE", + # "HARM_CATEGORY_HATE_SPEECH": "BLOCK_NONE", + # "HARM_CATEGORY_SEXUALLY_EXPLICIT": "BLOCK_NONE", + # "HARM_CATEGORY_DANGEROUS_CONTENT": "BLOCK_NONE"} + # try: + # response = genai.GenerativeModel(self.model).generate_content(content, safety_settings=safety_settings) + # except ResourceExhausted as e: + # logger.error(f"Rate limited with {self.model}. Error: {e}") + # raise ResourceExhausted("Rate limited.") + # return response.text.strip() - def translate(self, texts_to_translate): - return super().translate(Gemini.request, texts_to_translate) - + async def _request(self, content): + async with aiohttp.ClientSession() as session: + async with session.post( + f"https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key={self.api_key}", + headers={ + "Content-Type": "application/json" + }, + json={ + "contents": [{"parts": [{"text": content}]}], + "safetySettings": [ + {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE", + "category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE", + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_NONE", + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE" + } + ] + } + ) as response: + response_json = await response.json() + return response_json['candidates'][0]['content']['parts'][0]['text'] + + # async def translate(self, texts_to_translate): + # return super().translate(self.request, texts_to_translate) + + + + + + +################################################################################################### + +### LOCAL LLM TRANSLATION + class TranslationDataset(Dataset): def __init__(self, texts: List[str], tokenizer, max_length: int = 512): """ diff --git a/helpers/ocr.py b/helpers/ocr.py index 55c60bd..b4af7ae 100644 --- a/helpers/ocr.py +++ b/helpers/ocr.py @@ -21,7 +21,6 @@ def _paddle_init(paddle_lang, use_angle_cls=False, use_GPU=True, **kwargs): def _paddle_ocr(ocr, image) -> list: - ### return a list containing the bounding box, text and confidence of the detected text result = ocr.ocr(image, cls=False)[0] if not isinstance(result, list): @@ -32,28 +31,29 @@ def _paddle_ocr(ocr, image) -> list: # EasyOCR has support for many languages def _easy_init(easy_languages: list, use_GPU=True, **kwargs): - langs = [] - for lang in easy_languages: - langs.append(standardize_lang(lang)['easyocr_lang']) - return easyocr.Reader(langs, gpu=use_GPU, **kwargs) + return easyocr.Reader(easy_languages, gpu=use_GPU, **kwargs) def _easy_ocr(ocr,image) -> list: return ocr.readtext(image) # RapidOCR mostly for mandarin and some other asian languages - +# default only supports chinese and english def _rapid_init(use_GPU=True, **kwargs): return RapidOCR(use_gpu=use_GPU, **kwargs) def _rapid_ocr(ocr, image) -> list: - return ocr(image) + return ocr(image)[0] ### Initialize the OCR model -def init_OCR(model='paddle', easy_languages: Optional[list] = ['ch_sim','en'], paddle_lang: Optional[str] = 'ch', use_GPU=True, **kwargs): +def init_OCR(model='paddle', easy_languages: Optional[list] = ['ch_sim','en'], paddle_lang: Optional[str] = 'ch_sim', use_GPU=True): if model == 'paddle': + paddle_lang = standardize_lang(paddle_lang)['paddleocr_lang'] return _paddle_init(paddle_lang=paddle_lang, use_GPU=use_GPU) elif model == 'easy': - return _easy_init(easy_languages=easy_languages, use_GPU=use_GPU) + langs = [] + for lang in easy_languages: + langs.append(standardize_lang(lang)['easyocr_lang']) + return _easy_init(easy_languages=langs, use_GPU=use_GPU) elif model == 'rapid': return _rapid_init(use_GPU=use_GPU) @@ -82,10 +82,11 @@ def _id_filtered(ocr, image, lang) -> list: return results_no_eng -# ch_sim, ch_tra, ja, ko, en +# ch_sim, ch_tra, ja, ko, en input def _id_lang(ocr, image, lang) -> list: result = _identify(ocr, image) lang = standardize_lang(lang)['id_model_lang'] + print(result) try: filtered = [entry for entry in result if contains_lang(entry[1], lang)] except: diff --git a/helpers/translation.py b/helpers/translation.py index cbb2a4a..4feff4b 100644 --- a/helpers/translation.py +++ b/helpers/translation.py @@ -1,17 +1,16 @@ from transformers import M2M100Tokenizer, M2M100ForConditionalGeneration, AutoTokenizer, AutoModelForSeq2SeqLM, GPTQConfig, AutoModelForCausalLM import google.generativeai as genai -import torch, os, sys, ast, json +import torch, os, sys, ast, json, asyncio, batching, random +from typing import List, Optional, Set from utils import standardize_lang from functools import wraps -import random -import batching -from batching import generate_text, Gemini, Groq +from batching import generate_text, Gemini, Groq, ApiModel from logging_config import logger -from multiprocessing import Process,Event +from asyncio import Task # root dir sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) -from config import LOCAL_FILES_ONLY, available_langs, curr_models, BATCH_SIZE, device, GEMINI_API_KEY, MAX_INPUT_TOKENS, MAX_OUTPUT_TOKENS, seq_llm_models, api_llm_models, causal_llm_models +from config import LOCAL_FILES_ONLY, available_langs, curr_models, BATCH_SIZE, device, GEMINI_API_KEY, MAX_INPUT_TOKENS, MAX_OUTPUT_TOKENS, seq_llm_models, api_llm_models, causal_llm_models, API_MODELS_FILEPATH ############################## # translation decorator @@ -32,27 +31,66 @@ def translate(translation_func): def init_API_LLM(from_lang, target_lang): from_lang = standardize_lang(from_lang)['translation_model_lang'] target_lang = standardize_lang(target_lang)['translation_model_lang'] - with open('api_models.json', 'r') as f: + with open(API_MODELS_FILEPATH, 'r') as f: models_and_rates = json.load(f) models = [] for class_type, class_models in models_and_rates.items(): cls = getattr(batching, class_type) - instantiated_objects = [ cls(model, rate) for model, rate in class_models.items()] + instantiated_objects = [ cls(model = model, **rates) for model, rates in class_models.items()] models.extend(instantiated_objects) - for model in models: - model.start() + model.update_db() model.set_lang(from_lang, target_lang) return models -def translate_API_LLM(text, models): - random.shuffle(models) - for model in models: +async def translate_API_LLM(texts_to_translate: List[str], + models: List[ApiModel], + call_size: int = 2, + stagger_delay: int = 2) -> List[str]: + async def try_translate(model: ApiModel) -> Optional[List[str]]: try: - return model.translate(text) - except: - continue + result = await model.translate(texts_to_translate, store=True) + logger.debug(f'Try_translate result: {result}') + return result + except Exception as e: + logger.error(f"Translation failed for {model.model} from {model.site}: {e}") + return None + random.shuffle(models) + groups = [models[i:i+call_size] for i in range(0, len(models), call_size)] + + for group in groups: + tasks = set(asyncio.create_task(try_translate(model)) for model in group) + while tasks: + done, pending = await asyncio.wait(tasks, + return_when=asyncio.FIRST_COMPLETED + ) + logger.debug(f"Tasks done: {done}") + logger.debug(f"Tasks remaining: {pending}") + for task in done: + result = await task + logger.debug(f'Result: {result}') + if result is not None: + # Cancel remaining tasks + for t in pending: + t.cancel() + return result logger.error("All models have failed to translate the text.") + raise TypeError("Models have likely all outputted garbage translations or rate limited.") +# def translate_API_LLM(text, models): +# random.shuffle(models) +# logger.debug(f"All Models Available: {models}") +# for model in models: +# logger.info(f"Attempting translation with model {model}.") +# try: +# translation = model.translate(text) +# logger.debug(f"Translation obtained: {translation}") +# if translation or translation == []: +# return translation +# except Exception as e: +# logger.error(f"Error with model {repr(model)}. Error: {e}") +# continue +# logger.error("All models have failed to translate the text.") +# raise TypeError("Models have likely all outputted garbage translations or rate limited.") ############################### # Best model by far. Aya-23-8B. Gemma is relatively good. If I get the time to quantize either gemma or aya those will be good to use. llama3.2 is really good as well. diff --git a/helpers/utils.py b/helpers/utils.py index f97bc5a..367c2c3 100644 --- a/helpers/utils.py +++ b/helpers/utils.py @@ -4,7 +4,9 @@ import pyscreenshot as ImageGrab # wayland tings not sure if it will work on oth import mss, io, os from PIL import Image import jaconv, MeCab, unidic, pykakasi - +from sklearn.metrics.pairwise import cosine_similarity +from sklearn.feature_extraction.text import TfidfVectorizer +import numpy as np # for creating furigana mecab = MeCab.Tagger('-d "{}"'.format(unidic.DICDIR)) uroman = ur.Uroman() @@ -95,7 +97,7 @@ def contains_katakana(text): # use kakasi to romanize japanese text def romanize(text, lang): - if lang == 'zh': + if lang in ['zh','ch_sim','ch_tra']: return ' '.join([ py[0] for py in pinyin(text, heteronym=True)]) if lang == 'ja': return kks.convert(text)[0]['hepburn'] @@ -131,13 +133,13 @@ def standardize_lang(lang): id_model_lang = 'zh' elif lang == 'ja': easyocr_lang = 'ja' - paddleocr_lang = 'ja' + paddleocr_lang = 'japan' rapidocr_lang = 'ja' translation_model_lang = 'ja' id_model_lang = 'ja' elif lang == 'ko': easyocr_lang = 'korean' - paddleocr_lang = 'ko' + paddleocr_lang = 'korean' rapidocr_lang = 'ko' translation_model_lang = 'ko' id_model_lang = 'ko' @@ -165,6 +167,23 @@ def which_ocr_lang(model): else: raise ValueError("Invalid OCR model. Please use one of 'easy', 'paddle', or 'rapid'.") +def similar_tfidf(list1,list2,threshold) -> float: + """Calculate cosine similarity using TF-IDF vectors.""" + if not list1 or not list2: + return 0.0 + + vectorizer = TfidfVectorizer() + all_texts = list1 + list2 + tfidf_matrix = vectorizer.fit_transform(all_texts) + + # Calculate average vectors for each list + vec1 = np.mean(tfidf_matrix[:len(list1)].toarray(), axis=0).reshape(1, -1) + vec2 = np.mean(tfidf_matrix[len(list1):].toarray(), axis=0).reshape(1, -1) + + return float(cosine_similarity(vec1, vec2)[0, 0]) > threshold + + + if __name__ == "__main__": # Example usage diff --git a/logging_config.py b/logging_config.py index c1dce08..8d3488e 100644 --- a/logging_config.py +++ b/logging_config.py @@ -48,8 +48,8 @@ def setup_logger( # Create a formatter and set it for both handlers formatter = logging.Formatter( - '%(asctime)s - %(name)s - [%(levelname)s] %(message)s', - datefmt='%Y-%m-%d %H:%M:%S' + '%(asctime)s.%(msecs)03d - %(name)s - [%(levelname)s] %(message)s', + datefmt="%Y-%m-%d %H:%M:%S" ) file_handler.setFormatter(formatter) console_handler.setFormatter(formatter) @@ -64,4 +64,5 @@ def setup_logger( print(f"Failed to setup logger: {e}") return None -logger = setup_logger('on_screen_translator', log_file='translate.log', level=logging.DEBUG) \ No newline at end of file +logger = setup_logger('on_screen_translator', log_file='translate.log', level=logging.DEBUG) + diff --git a/main.py b/main.py new file mode 100644 index 0000000..ee1ebfe --- /dev/null +++ b/main.py @@ -0,0 +1,100 @@ +################################################################################### +##### IMPORT LIBRARIES ##### +import os, time, sys, threading, subprocess + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'helpers')) + +from translation import translate_Seq_LLM, translate_API_LLM, init_API_LLM, init_Seq_LLM +from utils import printsc, convert_image_to_bytes, bytes_to_image, similar_tfidf +from ocr import get_words, init_OCR, id_keep_source_lang +from data import Base, engine, create_tables +from draw import modify_image_bytes +import config, asyncio +from config import SOURCE_LANG, TARGET_LANG, OCR_MODEL, OCR_USE_GPU, LOCAL_FILES_ONLY, REGION, INTERVAL, MAX_TRANSLATE, TRANSLATION_MODEL, IMAGE_CHANGE_THRESHOLD +from logging_config import logger +import web_app +import view_buffer_app +################################################################################### + + +async def main(): + ################################################################################### + + # Initialisation + ##### Create the database if not present ##### + create_tables() + + ##### Initialize the OCR ##### + OCR_LANGUAGES = [SOURCE_LANG, TARGET_LANG, 'en'] + ocr = init_OCR(model=OCR_MODEL, paddle_lang= SOURCE_LANG, easy_languages = OCR_LANGUAGES, use_GPU=OCR_USE_GPU) + + ##### Initialize the translation ##### + # model, tokenizer = init_Seq_LLM(TRANSLATION_MODEL, from_lang =SOURCE_LANG , target_lang = TARGET_LANG) + models = init_API_LLM(SOURCE_LANG, TARGET_LANG) + ################################################################################### + + runs = 0 + + # label, app = view_buffer_app.create_viewer() + + # try: + while True: + logger.debug("Capturing screen") + untranslated_image = printsc(REGION) + logger.debug(f"Screen Captured. Proceeding to perform OCR.") + byte_image = convert_image_to_bytes(untranslated_image) + ocr_output = id_keep_source_lang(ocr, byte_image, SOURCE_LANG) # keep only phrases containing the source language + logger.debug(f"OCR completed. Detected {len(ocr_output)} phrases.") + if runs == 0: + logger.info('Initial run') + prev_words = set() + else: + logger.info(f'Run number: {runs}.') + runs += 1 + + curr_words = set(get_words(ocr_output)) + logger.debug(f'Current words: {curr_words} Previous words: {prev_words}') + ### If the OCR detects different words, translate screen -> to ensure that the screen is not refreshing constantly and to save GPU power + if not similar_tfidf(list(curr_words), list(prev_words), threshold = IMAGE_CHANGE_THRESHOLD) and prev_words != curr_words: + logger.info('Beginning Translation') + + to_translate = [entry[1] for entry in ocr_output][:MAX_TRANSLATE] + # translation = translate_Seq_LLM(to_translate, model_type = TRANSLATION_MODEL, model = model, tokenizer = tokenizer, from_lang = SOURCE_LANG, target_lang = TARGET_LANG) + try: + translation = await translate_API_LLM(to_translate, models, call_size = 3) + except TypeError as e: + logger.error(f"Failed to translate using API models. Error: {e}. Sleeping for 30 seconds.") + time.sleep(30) + continue + logger.debug('Translation complete. Modifying image.') + translated_image = modify_image_bytes(byte_image, ocr_output, translation) + # view_buffer_app.show_buffer_image(translated_image, label) + web_app.latest_image = bytes_to_image(translated_image) + logger.debug("Image modified. Saving image.") + # web_app.latest_image.save('/home/James/Pictures/translated.png') # home use + # logger.debug("Image saved.") + prev_words = curr_words + else: + logger.info("Skipping translation. No significant change in the screen detected.") + logger.debug("Continuing to next iteration.") + # logger.debug(f'Sleeping for {INTERVAL} seconds') + asyncio.sleep(INTERVAL) + # finally: + # label.close() + # app.quit() +################### TODO ################## +# 3. Quantising/finetuning larger LLMs. Consider using Aya-23-8B, Gemma, llama3.2 models. +# 5. Maybe refreshing issue of flask app. Also get webpage to update only if the image changes. + +if __name__ == "__main__": + # subprocess.Popen(['feh','--auto-reload', '/home/James/Pictures/translated.png']) + # asyncio.run(main()) + # Start the image updating thread + logger.info('Configuration:') + for i in dir(config): + if not callable(getattr(config, i)) and not i.startswith("__"): + logger.info(f'{i}: {getattr(config, i)}') + threading.Thread(target=asyncio.run, args=(main(),), daemon=True).start() + + # Start the Flask web server + web_app.app.run(host='0.0.0.0', port=5000, debug=False) diff --git a/qtapp.py b/qtapp.py deleted file mode 100644 index 2d72364..0000000 --- a/qtapp.py +++ /dev/null @@ -1,115 +0,0 @@ -################################################################################### -##### IMPORT LIBRARIES ##### -import os, time, sys - -sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'helpers')) -from translation import translate_Seq_LLM, translate_API_LLM, init_API_LLM, init_Seq_LLM -from utils import printsc, convert_image_to_bytes, bytes_to_image -from ocr import get_words, init_OCR, id_keep_source_lang -from logging_config import logger -from draw import modify_image_bytes -from config import ADD_OVERLAY, SOURCE_LANG, TARGET_LANG, OCR_MODEL, OCR_USE_GPU, LOCAL_FILES_ONLY, REGION, INTERVAL, MAX_TRANSLATE, TRANSLATION_MODEL, FONT_SIZE, FONT_FILE, FONT_COLOUR -from create_overlay import app, overlay -from typing import Optional, List -################################################################################### -from PySide6.QtCore import Qt, QPoint, QRect, QTimer, QThread, Signal -from PySide6.QtGui import (QKeySequence, QShortcut, QAction, QPainter, QFont, - QColor, QIcon, QImage, QPixmap) -from PySide6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, - QLabel, QSystemTrayIcon, QMenu) -from dataclasses import dataclass - - -class TranslationThread(QThread): - translation_ready = Signal(list, list) # Signal to send translation results - start_capture = Signal() - end_capture = Signal() - screen_capture = Signal(int, int, int, int) - def __init__(self, ocr, models, source_lang, target_lang, interval): - super().__init__() - self.ocr = ocr - self.models = models - self.source_lang = source_lang - self.target_lang = target_lang - self.interval = interval - self.running = True - self.prev_words = set() - self.runs = 0 - - def run(self): - while self.running: - self.start_capture.emit() - untranslated_image = printsc(REGION) - self.end_capture.emit() - byte_image = convert_image_to_bytes(untranslated_image) - ocr_output = id_keep_source_lang(self.ocr, byte_image, self.source_lang) - - if self.runs == 0: - logger.info('Initial run') - else: - logger.info(f'Run number: {self.runs}.') - self.runs += 1 - - curr_words = set(get_words(ocr_output)) - - if self.prev_words != curr_words: - logger.info('Translating') - to_translate = [entry[1] for entry in ocr_output][:MAX_TRANSLATE] - translation = translate_API_LLM(to_translate, self.models) - logger.info(f'Translation from {to_translate} to\n {translation}') - - # Emit the translation results - modify_image_bytes(byte_image, ocr_output, translation) - self.translation_ready.emit(ocr_output, translation) - - self.prev_words = curr_words - else: - logger.info("No new words to translate. Output will not refresh.") - - logger.info(f'Sleeping for {self.interval} seconds') - time.sleep(self.interval) - - def stop(self): - self.running = False - - - -def main(): - - # Initialize OCR - OCR_LANGUAGES = [SOURCE_LANG, TARGET_LANG, 'en'] - ocr = init_OCR(model=OCR_MODEL, easy_languages=OCR_LANGUAGES, use_GPU=OCR_USE_GPU) - - # Initialize translation - models = init_API_LLM(SOURCE_LANG, TARGET_LANG) - - - # Create and start translation thread - translation_thread = TranslationThread( - ocr=ocr, - models=models, - source_lang=SOURCE_LANG, - target_lang=TARGET_LANG, - interval=INTERVAL - ) - - # Connect translation results to overlay update - translation_thread.start_capture.connect(overlay.prepare_for_capture) - translation_thread.end_capture.connect(overlay.restore_after_capture) - translation_thread.translation_ready.connect(overlay.update_translation) - translation_thread.screen_capture.connect(overlay.capture_behind) - # Start the translation thread - translation_thread.start() - - - # Start Qt event loop - result = app.exec() - - # Cleanup - translation_thread.stop() - translation_thread.wait() - - return result - -if __name__ == "__main__": - sys.exit(main()) \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 7035033..7e55270 100644 --- a/templates/index.html +++ b/templates/index.html @@ -17,7 +17,7 @@ setInterval(function () { document.getElementById("live-image").src = "/image?" + new Date().getTime(); - }, 3500); // Update every 2 seconds + }, 2500); // Update every 2.5 seconds. Beware that if the image fails to reload on time, the browser will continuously refresh without being able to display the images. diff --git a/view_buffer_app.py b/view_buffer_app.py new file mode 100644 index 0000000..b772cff --- /dev/null +++ b/view_buffer_app.py @@ -0,0 +1,52 @@ + +#### Same thread as main.py so it will be relatively unresponsive. Just for use locally for a faster image display from buffer. + + +from PySide6.QtWidgets import QApplication, QLabel +from PySide6.QtCore import Qt +from PySide6.QtGui import QImage, QPixmap +import sys +def create_viewer(): + """Create and return a QLabel widget for displaying images""" + app = QApplication.instance() + if app is None: + app = QApplication(sys.argv) + + label = QLabel() + label.setWindowTitle("Image Viewer") + label.setMinimumSize(640, 480) + # Enable mouse tracking for potential future interactivity + label.setMouseTracking(True) + # Better scaling quality + label.setScaledContents(True) + label.show() + + return label, app + +def show_buffer_image(buffer, label): + """ + Display an image from buffer using PySide6 + + Parameters: + buffer: bytes + Raw image data in memory + label: QLabel + Qt label widget to display the image + """ + # Convert buffer to QImage + qimg = QImage.fromData(buffer) + + # Convert to QPixmap and set to label + pixmap = QPixmap.fromImage(qimg) + + # Scale with better quality + scaled_pixmap = pixmap.scaled( + label.size(), + Qt.KeepAspectRatio, + Qt.SmoothTransformation + ) + + label.setPixmap(scaled_pixmap) + + # Process Qt events to update the display + QApplication.processEvents() \ No newline at end of file diff --git a/web.py b/web_app.py similarity index 85% rename from web.py rename to web_app.py index 0f7275e..b6f23d6 100644 --- a/web.py +++ b/web_app.py @@ -1,12 +1,12 @@ from flask import Flask, Response, render_template import threading import io -import app app = Flask(__name__) +latest_image = None # Global variable to hold the current image def curr_image(): - return app.latest_image + return latest_image @app.route('/') def index(): @@ -29,7 +29,8 @@ def stream_image(): if __name__ == '__main__': # Start the image updating thread - threading.Thread(target=app.main, daemon=True).start() + import main, asyncio + threading.Thread(target=asyncio.run, args=(main(),), daemon=True).start() # Start the Flask web server app.run(host='0.0.0.0', port=5000, debug=True)