From b9ad0258ae20054f6c8fc178a31f0da701005d46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=BA=90=E6=96=87=E9=9B=A8?= <41315874+fumiama@users.noreply.github.com> Date: Sun, 2 Jun 2024 22:47:52 +0900 Subject: [PATCH] feat(infer): add model hash identification and optimize infer-web ui --- infer-web.py | 154 +++++++++++++---------------- infer/lib/train/process_ckpt.py | 35 ++++--- infer/modules/train/train.py | 6 +- infer/modules/vc/__init__.py | 2 + infer/modules/vc/hash.py | 170 ++++++++++++++++++++++++++++++++ infer/modules/vc/info.py | 50 ++++++++++ infer/modules/vc/lgdsng.mp3 | Bin 0 -> 24534 bytes infer/modules/vc/modules.py | 10 +- requirements-amd.txt | 1 + requirements-dml.txt | 1 + requirements-ipex.txt | 1 + requirements-py311.txt | 1 + requirements.txt | 1 + 13 files changed, 327 insertions(+), 105 deletions(-) create mode 100644 infer/modules/vc/hash.py create mode 100644 infer/modules/vc/info.py create mode 100644 infer/modules/vc/lgdsng.mp3 diff --git a/infer-web.py b/infer-web.py index 0a47e07..4905990 100644 --- a/infer-web.py +++ b/infer-web.py @@ -10,13 +10,12 @@ load_dotenv("sha256.env") if sys.platform == "darwin": os.environ["PYTORCH_ENABLE_MPS_FALLBACK"] = "1" -from infer.modules.vc import VC +from infer.modules.vc import VC, show_info from infer.modules.uvr5.modules import uvr from infer.lib.train.process_ckpt import ( change_info, extract_small_model, merge, - show_info, ) from i18n.i18n import I18nAuto from configs.config import Config @@ -838,6 +837,7 @@ with gr.Blocks(title="RVC WebUI") as app: clean_button.click( fn=clean, inputs=[], outputs=[sid0], api_name="infer_clean" ) + modelinfo = gr.Textbox(label=i18n("模型信息")) with gr.TabItem(i18n("单次推理")): with gr.Group(): with gr.Row(): @@ -846,24 +846,23 @@ with gr.Blocks(title="RVC WebUI") as app: label=i18n("变调(整数, 半音数量, 升八度12降八度-12)"), value=0, ) - input_audio0 = gr.Textbox( + input_audio0 = gr.File( label=i18n( - "输入待处理音频文件路径(默认是正确格式示例)" + "待处理音频文件" ), - placeholder="C:\\Users\\Desktop\\audio_example.wav", - ) - file_index1 = gr.Textbox( - label=i18n( - "特征检索库文件路径,为空则使用下拉的选择结果" - ), - placeholder="C:\\Users\\Desktop\\model_example.index", - interactive=True, + file_types=["audio"] ) file_index2 = gr.Dropdown( label=i18n("自动检测index路径,下拉式选择(dropdown)"), choices=sorted(index_paths), interactive=True, ) + file_index1 = gr.File( + label=i18n( + "特征检索库文件路径,为空则使用下拉的选择结果" + ), + ) + with gr.Column(): f0method0 = gr.Radio( label=i18n( "选择音高提取算法,输入歌声可用pm提速,harvest低音好但巨慢无比,crepe效果好但吃GPU,rmvpe效果最好且微吃GPU" @@ -876,8 +875,6 @@ with gr.Blocks(title="RVC WebUI") as app: value="rmvpe", interactive=True, ) - - with gr.Column(): resample_sr0 = gr.Slider( minimum=0, maximum=48000, @@ -928,6 +925,10 @@ with gr.Blocks(title="RVC WebUI") as app: ), visible=False, ) + but0 = gr.Button(i18n("转换"), variant="primary") + vc_output2 = gr.Audio( + label=i18n("输出音频(右下角三个点,点了可以下载)") + ) refresh_button.click( fn=change_choices, @@ -935,19 +936,8 @@ with gr.Blocks(title="RVC WebUI") as app: outputs=[sid0, file_index2], api_name="infer_refresh", ) - # file_big_npy1 = gr.Textbox( - # label=i18n("特征文件路径"), - # value="E:\\codes\py39\\vits_vc_gpu_train\\logs\\mi-test-1key\\total_fea.npy", - # interactive=True, - # ) with gr.Group(): - with gr.Column(): - but0 = gr.Button(i18n("转换"), variant="primary") - with gr.Row(): - vc_output1 = gr.Textbox(label=i18n("输出信息")) - vc_output2 = gr.Audio( - label=i18n("输出音频(右下角三个点,点了可以下载)") - ) + vc_output1 = gr.Textbox(label=i18n("输出信息")) but0.click( vc.vc_single, @@ -981,36 +971,28 @@ with gr.Blocks(title="RVC WebUI") as app: label=i18n("变调(整数, 半音数量, 升八度12降八度-12)"), value=0, ) + dir_input = gr.Textbox( + label=i18n( + "输入待处理音频文件夹路径(去文件管理器地址栏拷就行了)" + ), + placeholder="C:\\Users\\Desktop\\input_vocal_dir", + ) + inputs = gr.File( + file_count="multiple", + label=i18n("也可批量输入音频文件, 二选一, 优先读文件夹"), + ) opt_input = gr.Textbox( label=i18n("指定输出文件夹"), value="opt" ) - file_index3 = gr.Textbox( - label=i18n("特征检索库文件路径,为空则使用下拉的选择结果"), - value="", - interactive=True, - ) file_index4 = gr.Dropdown( label=i18n("自动检测index路径,下拉式选择(dropdown)"), choices=sorted(index_paths), interactive=True, ) - f0method1 = gr.Radio( + file_index3 = gr.File( label=i18n( - "选择音高提取算法,输入歌声可用pm提速,harvest低音好但巨慢无比,crepe效果好但吃GPU,rmvpe效果最好且微吃GPU" + "特征检索库文件路径,为空则使用下拉的选择结果" ), - choices=( - ["pm", "harvest", "crepe", "rmvpe"] - if config.dml == False - else ["pm", "harvest", "rmvpe"] - ), - value="rmvpe", - interactive=True, - ) - format1 = gr.Radio( - label=i18n("导出文件格式"), - choices=["wav", "flac", "mp3", "m4a"], - value="wav", - interactive=True, ) refresh_button.click( @@ -1026,6 +1008,18 @@ with gr.Blocks(title="RVC WebUI") as app: # ) with gr.Column(): + f0method1 = gr.Radio( + label=i18n( + "选择音高提取算法,输入歌声可用pm提速,harvest低音好但巨慢无比,crepe效果好但吃GPU,rmvpe效果最好且微吃GPU" + ), + choices=( + ["pm", "harvest", "crepe", "rmvpe"] + if config.dml == False + else ["pm", "harvest", "rmvpe"] + ), + value="rmvpe", + interactive=True, + ) resample_sr1 = gr.Slider( minimum=0, maximum=48000, @@ -1070,48 +1064,42 @@ with gr.Blocks(title="RVC WebUI") as app: value=1, interactive=True, ) - with gr.Row(): - dir_input = gr.Textbox( - label=i18n( - "输入待处理音频文件夹路径(去文件管理器地址栏拷就行了)" - ), - placeholder="C:\\Users\\Desktop\\input_vocal_dir", - ) - inputs = gr.File( - file_count="multiple", - label=i18n("也可批量输入音频文件, 二选一, 优先读文件夹"), - ) + format1 = gr.Radio( + label=i18n("导出文件格式"), + choices=["wav", "flac", "mp3", "m4a"], + value="wav", + interactive=True, + ) + but1 = gr.Button(i18n("转换"), variant="primary") - with gr.Row(): - but1 = gr.Button(i18n("转换"), variant="primary") - vc_output3 = gr.Textbox(label=i18n("输出信息")) + vc_output3 = gr.Textbox(label=i18n("输出信息")) - but1.click( - vc.vc_multi, - [ - spk_item, - dir_input, - opt_input, - inputs, - vc_transform1, - f0method1, - file_index3, - file_index4, - # file_big_npy2, - index_rate2, - filter_radius1, - resample_sr1, - rms_mix_rate1, - protect1, - format1, - ], - [vc_output3], - api_name="infer_convert_batch", - ) + but1.click( + vc.vc_multi, + [ + spk_item, + dir_input, + opt_input, + inputs, + vc_transform1, + f0method1, + file_index3, + file_index4, + # file_big_npy2, + index_rate2, + filter_radius1, + resample_sr1, + rms_mix_rate1, + protect1, + format1, + ], + [vc_output3], + api_name="infer_convert_batch", + ) sid0.change( fn=vc.get_vc, inputs=[sid0, protect0, protect1], - outputs=[spk_item, protect0, protect1, file_index2, file_index4], + outputs=[spk_item, protect0, protect1, file_index2, file_index4, modelinfo], api_name="infer_change_voice", ) with gr.TabItem(i18n("伴奏人声分离&去混响&去回声")): diff --git a/infer/lib/train/process_ckpt.py b/infer/lib/train/process_ckpt.py index 2529ccf..23377f6 100644 --- a/infer/lib/train/process_ckpt.py +++ b/infer/lib/train/process_ckpt.py @@ -1,16 +1,17 @@ import os -import sys import traceback from collections import OrderedDict +from time import time import torch from i18n.i18n import I18nAuto +from infer.modules.vc import model_hash_ckpt, hash_id i18n = I18nAuto() - -def savee(ckpt, sr, if_f0, name, epoch, version, hps): +# add author sign +def save_small_model(ckpt, sr, if_f0, name, epoch, version, hps): try: opt = OrderedDict() opt["weight"] = {} @@ -39,28 +40,20 @@ def savee(ckpt, sr, if_f0, name, epoch, version, hps): hps.data.sampling_rate, ] opt["info"] = "%sepoch" % epoch + opt["name"] = name + opt["timestamp"] = int(time()) opt["sr"] = sr opt["f0"] = if_f0 opt["version"] = version + h = model_hash_ckpt(opt) + opt["hash"] = h + opt["id"] = hash_id(h) torch.save(opt, "assets/weights/%s.pth" % name) return "Success." except: return traceback.format_exc() -def show_info(path): - try: - a = torch.load(path, map_location="cpu") - return "模型信息:%s\n采样率:%s\n模型是否输入音高引导:%s\n版本:%s" % ( - a.get("info", "None"), - a.get("sr", "None"), - a.get("f0", "None"), - a.get("version", "None"), - ) - except: - return traceback.format_exc() - - def extract_small_model(path, name, sr, if_f0, info, version): try: ckpt = torch.load(path, map_location="cpu") @@ -182,9 +175,14 @@ def extract_small_model(path, name, sr, if_f0, info, version): if info == "": info = "Extracted model." opt["info"] = info + opt["name"] = name + opt["timestamp"] = int(time()) opt["version"] = version opt["sr"] = sr opt["f0"] = int(if_f0) + h = model_hash_ckpt(opt) + opt["hash"] = h + opt["id"] = hash_id(h) torch.save(opt, "assets/weights/%s.pth" % name) return "Success." except: @@ -251,10 +249,15 @@ def merge(path1, path2, alpha1, sr, f0, info, name, version): elif(sr=="48k"):opt["config"] = [1025, 32, 192, 192, 768, 2, 6, 3, 0, "1", [3, 7, 11], [[1, 3, 5], [1, 3, 5], [1, 3, 5]], [10,6,2,2,2], 512, [16, 16, 4, 4], 109, 256, 48000] elif(sr=="32k"):opt["config"] = [513, 32, 192, 192, 768, 2, 6, 3, 0, "1", [3, 7, 11], [[1, 3, 5], [1, 3, 5], [1, 3, 5]], [10, 4, 2, 2, 2], 512, [16, 16, 4, 4,4], 109, 256, 32000] """ + opt["name"] = name + opt["timestamp"] = int(time()) opt["sr"] = sr opt["f0"] = 1 if f0 == i18n("是") else 0 opt["version"] = version opt["info"] = info + h = model_hash_ckpt(opt) + opt["hash"] = h + opt["id"] = hash_id(h) torch.save(opt, "assets/weights/%s.pth" % name) return "Success." except: diff --git a/infer/modules/train/train.py b/infer/modules/train/train.py index 38a5678..48c1f57 100644 --- a/infer/modules/train/train.py +++ b/infer/modules/train/train.py @@ -74,7 +74,7 @@ from infer.lib.train.losses import ( kl_loss, ) from infer.lib.train.mel_processing import mel_spectrogram_torch, spec_to_mel_torch -from infer.lib.train.process_ckpt import savee +from infer.lib.train.process_ckpt import save_small_model global_step = 0 @@ -602,7 +602,7 @@ def train_and_evaluate( % ( hps.name, epoch, - savee( + save_small_model( ckpt, hps.sample_rate, hps.if_f0, @@ -626,7 +626,7 @@ def train_and_evaluate( logger.info( "saving final ckpt:%s" % ( - savee( + save_small_model( ckpt, hps.sample_rate, hps.if_f0, hps.name, epoch, hps.version, hps ) ) diff --git a/infer/modules/vc/__init__.py b/infer/modules/vc/__init__.py index 98a4fb7..01141ef 100644 --- a/infer/modules/vc/__init__.py +++ b/infer/modules/vc/__init__.py @@ -1,3 +1,5 @@ from .pipeline import Pipeline from .modules import VC from .utils import get_index_path_from_model, load_hubert +from .info import show_info +from .hash import model_hash_ckpt, hash_id diff --git a/infer/modules/vc/hash.py b/infer/modules/vc/hash.py new file mode 100644 index 0000000..8e8334d --- /dev/null +++ b/infer/modules/vc/hash.py @@ -0,0 +1,170 @@ +import numpy as np +import torch +import hashlib +import pathlib +from scipy.fft import fft +from pybase16384 import encode_to_string, decode_from_string + +if __name__ == "__main__": + import os, sys + now_dir = os.getcwd() + sys.path.append(now_dir) + +from configs.config import Config, singleton_variable + +from .pipeline import Pipeline +from .utils import load_hubert + +from infer.lib.audio import load_audio + +class TorchSeedContext: + def __init__(self, seed): + self.seed = seed + self.state = None + + def __enter__(self): + self.state = torch.random.get_rng_state() + torch.manual_seed(self.seed) + + def __exit__(self, type, value, traceback): + torch.random.set_rng_state(self.state) + +half_hash_len = 512 +expand_factor = 65536*8 + +@singleton_variable +def original_audio_time_minus(): + __original_audio = load_audio(str(pathlib.Path(__file__).parent / "lgdsng.mp3"), 16000) + np.divide(__original_audio, np.abs(__original_audio).max(), __original_audio) + return -__original_audio + +@singleton_variable +def original_audio_freq_minus(): + __original_audio = load_audio(str(pathlib.Path(__file__).parent / "lgdsng.mp3"), 16000) + np.divide(__original_audio, np.abs(__original_audio).max(), __original_audio) + __original_audio = fft(__original_audio) + return -__original_audio + +def _cut_u16(n): + if n > 16384: n = 16384 + 16384*(1-np.exp((16384-n)/expand_factor)) + elif n < -16384: n = -16384 - 16384*(1-np.exp((n+16384)/expand_factor)) + return n + +# wave_hash will change time_field, use carefully +def wave_hash(time_field): + np.divide(time_field, np.abs(time_field).max(), time_field) + if len(time_field) != 48000: + raise Exception("time not hashable") + freq_field = fft(time_field) + if len(freq_field) != 48000: + raise Exception("freq not hashable") + np.add(time_field, original_audio_time_minus(), out=time_field) + np.add(freq_field, original_audio_freq_minus(), out=freq_field) + hash = np.zeros(half_hash_len//2*2, dtype='>i2') + d = 375 * 512 // half_hash_len + for i in range(half_hash_len//4): + a = i*2 + b = a+1 + x = a + half_hash_len//2 + y = x+1 + s = np.average(freq_field[i*d:(i+1)*d]) + hash[a] = np.int16(_cut_u16(round(32768*np.real(s)))) + hash[b] = np.int16(_cut_u16(round(32768*np.imag(s)))) + hash[x] = np.int16(_cut_u16(round(32768*np.sum(time_field[i*d:i*d+d//2])))) + hash[y] = np.int16(_cut_u16(round(32768*np.sum(time_field[i*d+d//2:(i+1)*d])))) + return encode_to_string(hash.tobytes()) + +def audio_hash(file): + return wave_hash(load_audio(file, 16000)) + +def model_hash(config, tgt_sr, net_g, if_f0, version): + pipeline = Pipeline(tgt_sr, config) + audio = load_audio(str(pathlib.Path(__file__).parent / "lgdsng.mp3"), 16000) + audio_max = np.abs(audio).max() / 0.95 + if audio_max > 1: + np.divide(audio, audio_max, audio) + audio_opt = pipeline.pipeline(load_hubert(config.device, config.is_half), net_g, 0, audio, + [0, 0, 0], 6, "rmvpe", "", 0, if_f0, 3, tgt_sr, 16000, 0.25, + version, 0.33) + opt_len = len(audio_opt) + diff = 48000 - opt_len + n = diff//2 + if n > 0: + audio_opt = np.pad(audio_opt, (n, n)) + elif n < 0: + n = -n + audio_opt = audio_opt[n:-n] + h = wave_hash(audio_opt) + del pipeline, audio, audio_opt + return h + +def model_hash_ckpt(cpt): + from infer.lib.infer_pack.models import ( + SynthesizerTrnMs256NSFsid, + SynthesizerTrnMs256NSFsid_nono, + SynthesizerTrnMs768NSFsid, + SynthesizerTrnMs768NSFsid_nono, + ) + config = Config() + with TorchSeedContext(114514): + tgt_sr = cpt["config"][-1] + if_f0 = cpt.get("f0", 1) + version = cpt.get("version", "v1") + synthesizer_class = { + ("v1", 1): SynthesizerTrnMs256NSFsid, + ("v1", 0): SynthesizerTrnMs256NSFsid_nono, + ("v2", 1): SynthesizerTrnMs768NSFsid, + ("v2", 0): SynthesizerTrnMs768NSFsid_nono, + } + net_g = synthesizer_class.get( + (version, if_f0), SynthesizerTrnMs256NSFsid + )(*cpt["config"], is_half=config.is_half) + + del net_g.enc_q + + net_g.load_state_dict(cpt["weight"], strict=False) + net_g.eval().to(config.device) + if config.is_half: + net_g = net_g.half() + else: + net_g = net_g.float() + + h = model_hash(config, tgt_sr, net_g, if_f0, version) + + del net_g + + return h + +def model_hash_from(path): + cpt = torch.load(path, map_location="cpu") + h = model_hash_ckpt(cpt) + del cpt + return h + +def _extend_difference(n, a, b): + if n < a: n = a + elif n > b: n = b + n -= a + n /= (b-a) + return n + +def hash_similarity(h1: str, h2: str) -> int: + h1b, h2b = decode_from_string(h1), decode_from_string(h2) + if len(h1b) != half_hash_len*2 or len(h2b) != half_hash_len*2: + raise Exception("invalid hash length") + h1n, h2n = np.frombuffer(h1b, dtype='>i2'), np.frombuffer(h2b, dtype='>i2') + d = 0 + for i in range(half_hash_len//4): + a = i*2 + b = a+1 + ax = complex(h1n[a], h1n[b]) + bx = complex(h2n[a], h2n[b]) + if abs(ax) == 0 or abs(bx) == 0: continue + d += np.abs(ax - bx) + frac = (np.linalg.norm(h1n) * np.linalg.norm(h2n)) + cosine = np.dot(h1n.astype(np.float32), h2n.astype(np.float32)) / frac if frac != 0 else 1.0 + distance = _extend_difference(np.exp(-d/expand_factor), 0.5, 1.0) + return round((abs(cosine) + distance) / 2, 6) + +def hash_id(h: str) -> str: + return encode_to_string(hashlib.md5(decode_from_string(h)).digest())[:-1] diff --git a/infer/modules/vc/info.py b/infer/modules/vc/info.py new file mode 100644 index 0000000..f2bf320 --- /dev/null +++ b/infer/modules/vc/info.py @@ -0,0 +1,50 @@ +import traceback +from i18n.i18n import I18nAuto +from datetime import datetime +import torch + +from .hash import model_hash_ckpt, hash_id + +i18n = I18nAuto() + +def show_model_info(cpt, show_long_id=False): + try: + h = model_hash_ckpt(cpt) + id = hash_id(h) + idread = cpt.get("id", "None") + hread = cpt.get("hash", "None") + if id != idread: + id += "("+i18n("实际计算")+"), "+idread+"("+i18n("从模型中读取")+")" + if not show_long_id: h = i18n("不显示") + elif h != hread: + h += "("+i18n("实际计算")+"), "+hread+"("+i18n("从模型中读取")+")" + txt = f"""{i18n("模型名")}: %s +{i18n("封装时间")}: %s +{i18n("信息")}: %s +{i18n("采样率")}: %s +{i18n("音高引导(f0)")}: %s +{i18n("版本")}: %s +{i18n("ID(短)")}: %s +{i18n("ID(长)")}: %s""" % ( + cpt.get("name", "None"), + datetime.fromtimestamp(float(cpt.get("timestamp", 0))), + cpt.get("info", "None"), + cpt.get("sr", "None"), + i18n("有") if cpt.get("f0", 0) == 1 else i18n("无"), + cpt.get("version", "None"), + id, h + ) + except: + txt = traceback.format_exc() + + return txt + +def show_info(path): + try: + a = torch.load(path, map_location="cpu") + txt = show_model_info(a, show_long_id=True) + del a + except: + txt = traceback.format_exc() + + return txt diff --git a/infer/modules/vc/lgdsng.mp3 b/infer/modules/vc/lgdsng.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..35a37c52091a47442fd79cd4a1a01dc5f513a600 GIT binary patch literal 24534 zcmX`y1z1yG+&J(HjMy06ArhlT$mo{t?rx+z4PO$P?Q!BlunfpBvp`75cY@P z`@a9X=h^Odp1XaX?{n_i`F`)YstN)i;BLc!8km@v-0hG80G5ZXtCz2}ov*E@1K{H4 zB?SC$8_9i}i|1Wiprfy!in@G;9N(2=nnGgn1EseD{q1-O$$t{r|WB|6f%-pLpH9xjPPc z2>{kI01h66keHO5nwFk{8OFxJ&CACxEGjN3Bd4IErlF;0U}R!pWn=fm+11_K*FW&t ziy&LX&E_r1w~~QRW%K$=GM;cp4aaNhTe~Vn3|beSX%qCzPYozfB5U<^!)Gj z&HazxA?@#w0*L!J|F^*~W&UT^TIS1}F8lxc_Gj+WNh^=*aZvC`}OW?1s7iO!g-@Fe^1=_K?`BM%`?=J|UTzLm+;nXP9^d698a_#8DaPd2+|c z@(Kt@*F%28bj-i8`4OkS9>Q6*3)Q2}cHW+T0tt8{56wy#BMPX)2h>Y`qqA4AE624j znP!gA^)IMG%u1e@wkeWDCbaLVyY>+SL)1Z>KLhDMxP#@HluYn3Q(+8OWcl}g0s!LO zz=fOZA#0q%T6zHuHYjcv3yR4`RdiWFO23L%<^J#-u8t>^4CW{td!Lj-K9CUWD8uMT z$&mA-)F^zujmO?M;l~)EbKTpw&LC7_G;&k`Yk>GzEFoMMmb*XB3ST2%l2v};rvrx$ zauag@V^GAW^nx?yKa1tyR-h1L2HOpsIH3lH!Nu?a8@fS0V&7e!t(k_f2=aEB^oif` zSqnPA_&;CRr)r7R&9erHS?EU|&Lv90bV}|2{78Ig-kskI!yuK7?}C6su#Z#)#f=IU zYT;Ll6-ou<&c+kjUDs(7Byf~Op`_tll3^W`qQr@%Dkp}~HHx{tb~Qi65uS2Tq6S&g zA0kZ7S;WrcuN>JY%fe-gb7M<4zp-p%s(xT!?h)b9>VnKHlW?S^$x+ne6nNI=!KOr5 zaOiZZhLlXn%Lw})DL`FBWM##1Cf~$%YRwSh3=BUYE$>z1(*poKYP{)0rl@h<4xXgG zz!Gb%5}tcM6o~&<#pRUisTy49sB=oN=d(O?u=ldGlO8TVBNP4Y7rQc~mX$hMrSbdU zU(HOq4$^GS@h~`Pf#J#t^^B81m9#j<<@)U!`kfTzvkND$Gc{znOoQkb@kCq5Lyjm{ zo;tIM)F<|_ufk=fw8#}cI?v5n9TD|PCuhVjX@+$C#~;81&E z7?U`bo+XtX<5E5>O-=yEi0G+em#TtX1oW@IPQMvWi&9CAp^^Vcr4bnQkR7fl%-LQv zIo*?^j(+qmuysFF^-*Z%Uxz~U_G+Tsk;i13-ALb)eo)nXXuTfSxE;iT9H+4Ev}bQ` zIx@4KtZav?@TJz?TdVf$pM1!&BN>BQ2~9o9PXbhy{&GEYSAXk>WYt$|6VJYsASQ1w zZ@<=tkG9ple`oc3l_HN05Y~M_?*fE7%mqUQe@|$N+GfR^$lJv-JD!b= z5fT^kensocOmWhe`Dg(mtlR@lnU>gACvCflkv~r#{<@yDSM5 zzys*7kGG6JmT-n^0b!~7ioIUuhJAJDANAAGNxEfCFGDmkJmxtqtS9_yZ6Z0?S6e-4iP& zPNAC2Kb33dUZxgDc-jz!Ok&d|4+vD)6Li&Pbui0>!cK7Xu#eh6PKT^4Hir;dJy&D+ zSpRgQ8GR|~;unfWpYq0Fp0q?-$Xwf@ulg~jXxue3p(-PZz)UWu>k)m}B9twK|6^6h z95+K{RD#Sg%=Ed&+9MF(snO!eRp!tz$aP65aV-9$QY&d~vXOpl=pnHVBl5S@^q1Sd zcefl5$cX5=)BwDrq$&?xga+sT`m4aEjj*~jgVM%-XOvH1%!28g$btA8KO>QW~Tr-)79>$4{^cZKYGD`@T&FdheFOV_dgP>xv_jU_;WY=lWNf z{`U*w-Tq)#cAoMKv(vm$&O_$ziPCR4EvNjg#fxb4-{E+;pXG=vfOA<|d9E_BLGhm- z6VivJ+X&MhS7SU7%FH~Wm=5fy`dX#IZp9A$z(QW)>Uy>uvs0N9a2lTt5 z8yl_f{v|d)3E|!rGYS2U96ZN>_2gEGb!!%|;;7yGaRA6UjqPr(3zJd>uV5-s3Z+&N z>9>t3Rvi9*e?a$2B_eEpOoTM84nH-`b(s4O(*&`?Kac3Jb|kM zJ=UMI7KI*X7ZFVL$0NhXdHmem6zFkRRp|$sY_x>6ez^3I%VCj}g(&&GJA4%SO4TeI zK!nM83SvfQy)c%Ety3N`P|;<#UU!)2T+LbETzMUq3f7gE~-Y}P53KRNX#hW^OV39Afh2@qGo|NgH zi!`U)+SE`c=W?>C7A2D*qMwkW$;YOw(X~(g&EXEMiRc5ZO0gxR)Meb(pl!ynlLCbrl&m9`-ws z3yJJ=uU~9qyXB$#v+o$gqP|(wvU~AeqV=nv@>~az;)^LP5c6@3Wy*z|-}^xT5iS(MmMSa)r`+&V_eP{x}|Myoa81Q%^7tSZ0);EinlL_9@pE(NFpAGqbsE2{=@KZ4@E(kec-NaW#-H?;_OfB zzcl*=v|S>y<-%X;i!YX3r%roFcf*k?PJxQdw5bn)`3@2Ezcf8YznC~B zjx{R-a(0Hf8rPXu&*{Tnop=ywW%Wy|zV3QfwZ-&e&D0R8x4TB<|A5ycM8?TQBIqh@ z;dQhuJNjDSLXT0`M*g7-N8#>dOzFGrvQ+?f>QP##${MD@ql@)F{U7||+@+>vcSbZB z_2z>5%=A^V7M(jc1?iBxc~?&0$h1Rrv%uD26+eV(;Y{-CiTHN2L9WYWDEgFc?+|^3 zUQs1%domhMaPw1->`A=e~?$@&gsYkgrjdl(e?JFL+E z!_9=`v$!Gc;I2Pkx?ir+hhCf2<9#_a(U&QNYgka}db3w&2#hVr`Vs=<_OgHhBbiBy z*a1Y15bwQR;>ED&S{x?6Ugd{3_kJP(>>go_f~n5%6?1iF#)%qgOybg6?Mp0g?HUn( z{~}M4T9?-}_QR8d`60U~dW#56jg3Lx-Tr~4IPTRPfMM7|44otnhBJH9hENa2C~epI zEpf#$*?$;&g;&9BfQ^AY&I&PFieKK>i;8R+W6h|b-$7^Rm!QY3(P;Ejd2$kd1n%%o zGx|0jz%fSx7qL}_YYr7Sl}!DZ=TE{+9}M%?cJYmc2&MD2%mt_J(u+ayI5}kd%|h!f z@BP$BFjCvLvC>JFC+Tp%8oIaTBZ_3U5yUc+39eaRb~Q6$E^YB9WTd4+?*m0L*sC&h z4Q%z%TrM9dbFc=nxj=;M8qa1%AqYC!xxCU=J0j#BvHW!!{nc^?VS4sS87r==3D=jb ziSB0}1js!$h2aPWKR5jw3KhIH%`Bl-u}|-}KCTiQZcR7Ht1igI&^t%xGJRJ0Jm>lW z>$Vci+$_%}{yw%FeSK3R+7{agc}6)B({N+(uRp*Yk#+B92Jku>{Ayb2bUCO|9A}va zXjjac$|(yKn;=DD2nL+KKMOYpM2=VEv`9hmG2DIX5h?Aj^5XB`Xro!8h%oZ}S4-qS zIfRW#68x6apv3u9d;j^&~x2 zz(}vEQLns62*>Aio~?4Z_dh~{8N;kKT5)yZ#N6l6G=v|pj|-jownjBte$UyY^OLXr zbJ3PCUtpXU)Dx=2fW3s(k0)G2 zH4rMy%(VRtQ1hNSKSESp*Lk+{G}PC?Lh8>Pv%aJ^qbN;UPwkdThD#J&qVu_6VvoOMv*tJ6h-E$;v&C!=PlN^+nn7!RZ%ev#E0*YLb<=Irn}E z0IU#TuUv&U!&m0UaTX_CmzAyG-a4OS9$F<{vu-K4gg#tcm5lMFRzqx-PDkh013zn{ z=ophSkTuw{O(v?~`ou>7%llkI3>b!M(d=UF?5UNt2tkg9Z#_wCtAZ lVA3q!iJ+ zcoy)id(xkQ!P%q*ka%E|9XmsM6Fn^yhjfbokz{2;lkankrY0`S3yNg`%nF3G6M{ z{fmGq8O+aJ1QEIFr0Ec9Lb*?s2}4@au?}_@prp8&k-GJjXW38HoVzB!na3H}byQF+ zeyI7x+DX32MgB!2+uc>}vafqZp6zW#?G<*I4VWE7LtCCK$8M(>$xG0Og%OB{!=?Qs zs`ad`cBG!`;RO3b!W|mtJ#V+&L@V5$R;;%hClsW%7>1?|DF<#^)SILek7{d;|NI+yPz z@axiQMy@$FKDM$IxejFp-{ybFV=sm$5mT0!v&nWiSa2aPcoTkID^9nO<(P8&j;+zj z&++1@ZH#lJj@6wzrW2@Oe%*X+wvwMos`QhyC8bc%P(X}RJ%sk-YqTat+{j}3LejHT zG3SiHoy1=OE(h1Xh5*PRj=mT?EyF}I_u_boK|q%^qE>}E9P%MGQDScx?5t+h{OS)QTY>^ zE<5Pasytuipg`h5KGi&@e=w|)i_kA2AM*2-b+MqDA&XL7uU)Y*Fk}bh@TfpT9(yl) zB>{Y0LCiQ(Wu`y2TGKMSwHwFA^r}8#)R%MAYWAdpwKb2vWm$yg{kF@O^5W`tcHTUE z{108eTmz~!mrpQc4sFPMuQ9fP)y`?a^02zv?cVC`N}Sc{xN+UcIKG+ zAM4$=l1#fg{?K1OLB=w26B29o4He?$zfGgmy>8yYBW=f;eKHO!n$br;Q-dVth@ptT zV)#vAITDPsb;4Ab?XeHxBE2kdo71H{$u_Fr}WG#i^j=HLw5 zOd&BRgoJCm<6&F6!uA0+%$F2{ICz-hq2KOzx`0;}QOX`(Z@X7Bs+b+HXdOdu*K5XX z<;xz>{+;|}jlMOMt9$%o%2q9UAWYI6>$=n+j+u2>Ax^^0qqO3(%_L*^?RvMX2dQf8 z^h=|!KlV)DtO@)I?xQq(+tr%ntIn;Var5c1fn|mm%DSUJ7S;%L*gspld3NvD0Kh9; zF3P!@CnhEC`%!W4CS&*-zZ`_F%JDQ&f)wypQRJx9vT%h(PI+FY57qXY;}mcomA?sa zV`^M?G_+a4?v+3Gu`okH^J+fm`bEgMlIY4;PolkMV(>WByO}F=RRW!!t~@?NqgB;V zj-XUbStK-BiB0EwzkDgC5;y6`r{zCmEFR#cpq}YS`B4#y8l~Yr zD5c;$YEd{ZPuox$+{SD!;>;VH46|d8Vfu}Ve-)(;`(jP=Jr9Nd4ADl72NWP#d_}2d z!i9i{R-%H~*Hh&aR{)FhumQf*OR%(>(K)XFJHd*Ay_0I-RV;@$r* z249jhXBTv%ha5X09+77okx5cV{4$j!->0RtCHQ#8AkSGcs#Xn)0u#bWng6^=`DXn* zK`V^uvtB7lo2HxZ*fI^xqc~GquEd4#!YnY(tK3IJqPk5Zb$M8C+%mT9)X#W#QQpsn zxR)zGa{qnQwQOY6D+nq#DG9pRMQplHtg<0x^5>ln~ zeB(}KzUm?n&d;Jk>c;R}pHR=4&kix!ZpUyl95UY2+w@w1(zmvkqv7r1ntLKb%{qWt zjw^>^dX+lDy{D(0J%f+BGR&~mlv>VvJ_XgcIxhAFb?Lf9TnR^h{+5n~&mG(_tI(=j z_q6yd^xp4r=XbKKX>mMZ1=k5>=7G>|d41BQK>7z3U*R;;^lD|3Ss5t1{J(hmBsE3( zq3lU*O8U~aHe8kQ^V!}&{j{u&(O6P4Bo;RBVU1!p$VgcO9D|eMilEh+seeVwjz&dWWjgmtM-o*Oz zm_%uf7heL)_c<&*(()dvUE`Wrp5E4&rWl+3SV6Opi?ux`;l?4PiTz0BS3jNj zI zsUf+6M=-YUU^QsSeG4Czvr|dV-+wBY-%zBk?{zK~^lxJW;9TNL7nY@kGnEOe&|*f^ zcVE~+H}ynwoWAs=5oWPr?ce*u0kC9=ZB2`PD_>fJP-M~*wqhk$G?Q+wZEky;JQW5oZe8RQYzb=}a-u-D_DXg(i_$cy5M#=yN z8CPyli}LNtn24Jb3c`;VJ5yCz?W`vJ!1~8s4k<-T2<0 zh`j4RwvE*8Cy(;n1H$4i6oWorKHhq9cZo^NP%cder5x*AC}RE(L(!=@J~$BD{n2#} zPvS><={Ui#P3kf~A?Gpf*{2vgq3Q%T>fJO+zxVIbs?e*?waL`aczmf#3&aGuN=>dJ z37Gm!TZoZ%%5xmfYdMC5S5rLNFD^8R2Pt+p^<2As%+^_-dE`ILu9pFmWt(5B+46lJ zKBrS#yxBMK%&-Wa*e>@u1G_<6@(J~W(E;3LR=>EQCZJ-*C5&WoYgS3Cr;v!Im*4q(WN8$Z={a2kM6mA z&0mADA8OZ}GvJVK5{mk04`jn_oemH?@sRctTyZzUwJmPuBSv+cV7hMVHtrWo2e-Cg z38jOHjLiL#pS&)*i)ruuZg=NzgkvL26~m#-Eub<>RA`PvDa>pT2U`{Fau>=;nRlup zlNLjgpDzQ$hKDP&jCZ2m6Mp=Ya21Q)R{nHd&09l?aYCy{RpyLMQ=A{p_T;xCLrZa4 zaG9`jibdbk?4pYejpyN6$E&_Qr$X(;LHn+od24C~X4Kd%uXz+g(deFEG@&b(|4z@L zY+7nE8%uTEj5H>B;K#deGNAY>7A_3LLpXr1UWp&ZVTu!x!gcn+5NE?bsD|@{6__89U<~479;XEHxg>wAZsg&Z3tLOG( z=`nimCr7!2tJk?a!rb{)o63Nb(?M*-W6m+bn!sG(NLrnCTA#vHHDN$hJw1O|bu}wq zaL6&PM3bv(V0~_gzYOBdCkGEytVrm2ypw;v;BA8i5j`_rM>EQT#5)b%Ex8it(T9JC zA&$n*bq`f_WW)#6kzFauCQLiGexd-Ui8cxrZQuJHkx-w9mMHGlG!+K7fJQuTt?%AT zbC^}D!Qsa-U&Y??Jk!X#etdf~#(7fGHSF}J_ut4WLScphQG)(ATk#%WUVi&$`{d2p z8sBe6G1~$z)avWqm$HhIvn z00p(niPEUjzkkc0ee!E=DT0U4E_l#CGN?Uy|L*olQWL`Y)A5y{{>G$-9($vCbEe%q5BK^^OR7IqV)zs90$xmLBmm@+IkbZOB_( zW0f%Uiyk)aH|f{6d`em_WEE6TePgzxm_AsecWr$}dGxh!N3Uomyf*+2WUY_v4B#Yg zV>}&=$*M|%$b=A`Fsk^e`$Ly$aI$$--%LSA#!4aX9fJzn8jAeyh1+XtoLg~EMPjdh z`It#6{z>h@o$o+jSMIv3p>O-FoFIz)=5~1ZejOwv^{-z|X>+R<6DlB@B9L)tmVoKT zc>w3eU6v!KH<-gRf{yyzQOyyePV@cye8D~9v+9$k+Eqn2OZ#VC8Bms2ZN7DFFqo-e{Ab~TKNd1J zk^}bdhu-yHYX%l(uRhD{WU`I>H)Y}Nw3|^RA+~-bv-LUF=jvspdJC^+L0H3SF3cuC(z&~k^0A| z8!u{-4vtZbi6GF~m)?NKhLk3ZbF0!8gw!t(haikJ*_LJ<3HYI?O(lSXQdTgK>)K}g zdqf(edekwYza_w|@st6RJDt1wZRvpS=T{4zslBJ{eCA#C>#pKlTsUcv?-FXE3!z!b zp6fTCZIxNh96PJ>u#tcQzo77E!!i0vOhJL;XGti~t|uhMq=dPmclq9L3V;P;qIN5_ zTDgk23?s8PUUA8F(wCX_Ow>142IGfbE{LYmLYM~{x+?qH%9>3~BdHl}ax8K$GANjL z1u06Anf{%2F0J(caEP~z3t`!qoV^(cz^b zdcG{G#UjyAQhF6bFJW;`6cfm{F0rsQd-N}X|e^B{68=cjR#zcm#%k{D0`wz@Vg<*^+EU#sxW zmTP}VC~&>`u=8{BC&o)=&yHEi!TI0EH4v6HBa#^Hnl{cg1?CEix0*NT*AwRs=uI_7 z!i%S*|J8pykYJ5{$C_M?69y8O^Jp+*s8%f|psFrh`oN|BgT;ACcSAly|rFo|zjq`N(@3Ve# z%G?U~*476|Dpw_&SuL2!Wc6$w;|(4qWeNYq;TK94QDEP%yj)79b}TtzhU6TTC9f6p zi&qwNSJnnReMnsAe?Wgi8e&y~LISq4hRJNv-|VSO8^Z`lQ|9jD|LpGncVD&fE`R4H zap|~#HvS}%bqfT|@^<=um8qx7B4s=}V9rVKVP@VcfYPUCU8Uwx~H zLy7aMs?kmcV|T2Mg;W;&hC*Yn)gy+5m2mlIrb(5%#EP!(_)Xgi7YU}xVFjuK+C+{S zr3k`M(P#@(&?ugaUpDzy^HljL_TC?PmwzQ_*0j`ba^O4P&7b}Ep~M*2i{#STiL|tA zI)B{9hKrsL*1mR67le$Ey#7|A`S=+;wR*erJ&OlZ+cOU2wSlR*oQS_DWsD;e=bMo4 z`Rxx6<1ht3zka5drjdejz2U^KuM-yEPy44Yz`Xt_DL>kE&_=~3ji}dZ_nChaXruj- zwQU54PHP@9`67KuPU8?qZabgah0g_UijN*t&g$BYOow7d5rVXk&!P{&Kq@ra=mA@gfft}z zN&?D&w%`D!M>DDQ@Bw!rrJXLTDZ6BMCBq#L>GHeeHzuL+39)xSV>#h#J6qrUd3$@0 zCu1fCi=&~ucEr2ucq4rbNEH+{oasK74sNs^$hou#yM@s_q74$EOA?np!UbM z!LvMa3?n9`M-h^6oLC4*9}nD(a0&hkR^10FW~KGS`_i#~<72NRnQzg^RFjw6vA5Y^ z^XGlKNx|w=6Ztd1?ZH|P=b_U1N}c9nIYhS-UpHW2IYxxu(e9{L69gU`$Fv|15Fj$F z*lhTn5nr`Ee z*D2Z+tI*7T3Q5t%K9p~0fi3P~GRIH@6o>=x-m~gK;}zr|JQUKwniB4-EY=gNz$ce3@|tCJF@BzDq&LMZdjY z{~bj>9$+vjL;Sw%-8PwW$OnJFfD+rI7k}9ttk<)s@2>S540i$GNC6RK%RtxzhVQl! zVDDG#qN#P$$(qK0ZJ-+zZb)j3yYq^$;{yg>wOv~Vt{5=;_fK30W;yGtgsglVhhy8F z$%5PlkGBSkdl(7P2H(I6(jL5|_y$(V{94_dWnFmW>sR#vAlOB0?wPIfP>-&Bi9~f3 zA5JUlv?aPB6^!V0AuCc|nRqVF!tFDh>47y6ldke(xVZf+Ou8qhMpUd?apn`R_xrSeq(F+5GQOH6PtrmVZ&$ zVbPv76@O+)$vs1=^HYJQzTdl54cl1~Q5Ih=`tTa2F*8}F_r z-uWkxV7{5V^Vj}_9m38!#m&f@>%*=Wq2s;5YcLmkw$cmLCadHbm4}xw418ly;C|Yk z1K0gTZ)%o75gamad5iK}0I0b2R@{^Mj-Z^9CuzMpL#!Sv?g0xCHm;f`#l*NVws@}+ z?9-JL6WKDOUYT+kKh(1j)PAo^;lVTY!fGjCY2<{;(l9_k0)5$NC^RM8KYyZ&=&!{M zpQe65O9Xf`k7C&k%}r%@qR~G?<6sgd011p!)xUS;zxv@K0M-!E{9!X-%br+|$jSBe z=!gJly*Y#5H*inp$y&MS$e|3`)*brQFShdGJ)f;#{^=v-Xf;4=4aMrUqu}ZIF9j1X zdQJ6HAkX`wYH$nEe0t7Pp1CcWkAz~snwIPCTRp2jX%4N++Z;7|nEW^teNxhQwNfh2u?70M-V@=9W;~S@cD@X9o2HzoJf&G$1FLeIL+ASM^2x%#tn9Wcj4_m=vuo+9`uB9Hv@G}wc z2$AaKM!l4wAdE=0>dW9V@Z`uL!8%xg|8-`_pyAhgv+(G&Vff9>6a%Cr%NP3J{9zYK za4u!r842)nvDRPmA7lEkbtVy49l)n+9gggn8F%mjb4>sb6rIT$zL{45v1?r^?+3qh zJVp{P+ozh~X~j9eotoLr_R_85O9TwDD<8?5rP*hGkjX^T+{gbNl0@^NaZe7CCV<=}r~?Ul(lIpl zPmU>_C9Ov1p+H*z3ybTAo}VS=0S`*Nm;&eh?w(e@O?+~sKQsJ=-me1yp-IAZdvp4!?e=Fzg|l-o zIaUKwd+Yi`8*0&sMK8}@uGSvGAmpD7tRfg;xn??EB;S#cATO+NMzq%n*Lz?3snqGf zSY=`cB2MVw-d_d~(D#TLjv_I+s_KLIjxqlP5!m4pWl(ZSUbL?*wSC`FT!Y%xiK7b+ z3jTT7Ue=LX_!4!0zg%y-C6t#Y{W|oc`PS83{}cLJJ3r;4sXWu5LL}>$6<9>#x>R@! zl2}ighVQ8Jde(U6pHV+if2l5hzkdl+R2zp9sl9>RAbu)V5M?__Av|Lz!yzeyx%z!k z-p5?VymECtxZxE3cU+A$KP5-Ru9F_P*9FneziUbk*MnQVJg*}!`mg@;0!d=)Lj70z)D3bUr7uxm2 zUqvZO{6Gi;85X4|82^}@PbZYRi?l91^Cr*Nsvs*Z(68L^8tyDkye;i6^+WW#2JeZ@k>r!`W?Fn{i4M0;Smt3V0N~3A?-a;$nU?l{V$KvH!(`k` z)t1YiYudJFlA*g)x<$rOs#p-1_Bh|iUlj@6yD{RC^uowyIXcfEB%Zg)t(=&T4pUh) zU|OhWmVUmBF^f-~ zki;6sQ}=#RB*c`}uO?S#Q#-x+uKq9FlB_tL?|2Q1J+{i+{k6CPmBW{{<$!-xP;@pT z?+=f#ne|$c;02O=NqFz_l_@zp z{@@(q>tu=}U#;-gL+Vil_1?0?Uu`q5u^ZeQn{vDk=dLaf(YH5-Gt0x2EQX=KUW8L9 z8iVZ}o$JfbFkgnTn2d8H-rF-{Ce|QcY+hkXtn0YBu z8D)G0Ga2A3hz@pPE}$&P@FLFj;MoT(l64Yi?JOzI;nw(&^5#nVkw<5J}!|Ik^J;^waghOO2!4{umNFRc43SQgtA4 z>;y~=N-A932JfAo1IvSja~7Gcd;i_~fzKg9>@BXXti;XUXK@U}L&O>bLWU?z#cXz1 z854q!Wo#|(v|?tw-rp#*b^2R?DMlHM-wW{H!_gC4S#Ulu zz;?>H8~oAei+x2&nsARMeivj*u~NEe~aD(DDKvN2`N?I=Z<*dX)AZBZemm zxTTJ{zhhXY-7Oog=9JQO(tCVPO%jUEg`jt}rhow#Z^I-tY|IFs>Tj`SOQ}wElqf%i zd;eYiB;-M8O{w;GhzotZ ze)S~29}t@lU>6IMwD<~-Osi(G$HDKV49lu0m2Bni{gy~DC&H(uQtgC|q@Ex%u7zhd z{E2J~31RK0UJcZ4Iq!C~;g}THmWVBf>RCtigmTU0m3g!6EQ@&kubo-+>1F8W8u9NL=;|?mms(O0 z7MU1Oy%~lW5#^rfAk9sX(1y~n*QTXLa~L`^1#b!*)QTBoEX*7`x^r*t+q9&n`)z&D>wEqE5x(4I_kh3VS3JQq_}BydUDx6usx|(lAL%WeEK!*`1!4q z?m)ryIMh#^EZAK)%}EJjeMXRA2m*b6n#bQJ8$VFy=fK2Jrbgq?_9GGKGR?S2H>H%ToWO+ z9b;)EUShQBpqs7Z_7%5poC6hm4BviMopK!A`@@l7Q(TvtO4U}*EVs^R0yc?2d;SGC zBfVn(@0W|8yf=6v=9FUHbh`OXca9NuUWDb(nx|u!3Mc^37ymqXGDcXN5p2S)u}4WP zBf7^Qkyb-e{hN3n*3K?9AnpejPc6vX%6qUNwA$F439`(wluP&Kd(N;*DTt)-bZh8IFZ#B6Az1&}Fv!|mW z4S`|@=S|vH-usgPaBPNKRg3%y2WbOqW?Z&{owq1CzvGwr&oO||UE%~wR(*K25Azut zlt~|4RU;+GrpWmq)Xb5h(T|BW0^KFF)XI8ez^sC~nmgdv5?AMa4QL%-$DFvCVj z(($cWw}2cgS8__(<-PrT@=F2npXHtW8aA#DIzg%-*qrsUBL0~LM&X{HWUrEl$}7_+ z)6iFVRC5}yEId?s3eeYV1!cFrN-DXej23N zq(?6pxz6c5k|{r0zaM1(b_X1WqR&a}1DP>I9evbf0L)bj{jFEc8t%K+ih~|6o-=-O zR@&@Fh?W-rTBL4daZhuu{tJeYU%n;2_g4ZWwqCY9(MZxqJZ?dJsTkc820OX=CHl)9 zgVNG^oQnAFlEZ^r_RykWawgG8q*7u&j_@)uhW#WSII&A5IUQzD5+cdxsGQDd43sj~|WceVgxPt|kXjuo2l5WE4R=v8jT@w8_dR!X`E8Cs@%m zykabO5 zgSq9hmvD{$Nol^3M8*5miCO1JL)PJsWbjs|L)dngd~p2MpfztOV~C>*P-plKN!>L~tv+uG~J&Cv$Zv+Cz zR`2d(^}mllHUQyy9B?;&>e|#by*Y`8Z;_JG5GaSi&yHrlm9gu2NYpZ7&mNrtx`dL4PZ&E`8n{bfJY)=#;&AS&P(K8PnWxI+ zca1Jv=AXd=SD!rml}~<$Za=A+x@o>y`gL(`%KuU?NM@%tHa5u7+|4y!d-tbavcUae>H-cK5aw<#3%9i&Uc82jkWD0C}MxF{q<0uz>^ghsof z)!e>*Lhl>DEfJvdYj^-*CE94O&Wp`y6OF~#{&oVh5sMx$)fAB7gHtW^XFX3Ue5JHf z=Ssg9h0y^n<_jRbd=M&q@(P)>;vE+S~S9MF;rJ(cb}MX#HHSAO8?RS`OxT36lU9K57vV&0m&y#lRM(*DzsJO_(d5oZq&hse zmHx0PsX)jK&4)J25Ue+zUeW%ty7Pr#eY@J;8tu*j`lur36n}$l@pK;T8jz3e`qk-) z^AoQw2Y|XR<8}N;>h>$uie-*{O6+1KB`y#Mg9<8`okLLPWK{O=fOJ`V2Hl=T^EGFt z&<9hnWI8?uC`l$+)p*ZhG!FA1#zl3s5b5fJTId9*bMO7!NW5G+w;!9#vUK>U&O)ef zesR1?UX0WCtyY=`>@e=SDWrT$IDb^Nw=QuDarE++aIsnctKw(-5Cilly_)OL^`$Z(_aoB)0{>wd9*l#TSZPLgbp@eMxQI^9`|jldiUJN zUkL!?$+}cY+RpHh{t!HibDb)KTgy@ISIL4hv!vV|GPIzQf7)lj;{^U7x2gehp=oCq zSy~MTrpMpLcR?Z7pN}S*x7!ytqhjdW9zEcN2{%_c%7vz0li1{zzEk>6P^0$B86$r1 zGZM)bjh9xW#uF{wV}AB7315=W9z(}%SjY9c2q|mx!KlN0Az5;Z)$9i|zk{*LbncE_ zeEaQ=ATQQ4{BZN>Y?{8sQu-_4N8}-vW9y z>qzAZyo4$n08~>S2-RYdKq!c#L=VZqW~oUkTm*ilrJu*EbUt|dcKj%f0BIFF+4a;J zp-khulnUma>Fiz>aU7k^U3n_5qu=x_%Pd_Jg^Ksu_6T-K@FXz-lekgi}qfS{g57{oVQv>MeX#5wtLrCZPM}#J05c-rIaI z-C{`bqgb8w4Y@K#1BP3JF#s@#9H+-76g)qM3(H)m()4*9mnJIPb(iCF^laz=d$`MV z70P45qfRe~r|G!))^D_$Y&#S9J@f;$6rvvNJn2-+6)5Dbot z3DUSQ>x5px-zYXaoQHF@Yz?5wn%`r(ocAQsN7urFnmpA{U#G4NPy z$v^$g2SGdIS7x4$!>& zD=K8^_rnxtGPJigMt2nPgWz9fz+pud zAY<%)n$Jc{SkZ*QW7oBzQvARZ5NZQ;o@344-jtA{M-=`j5dl%UldE#kM-B4a^CTnr>|iBQ3&;{(&rY!o zi9vNfFoYbU8ln6|QSRUT#e{+pC9NM&#!P4}tsJgMSvb(Nz1?HzpbH;QVH6CoT+08s zQB-ri5FF;+ryJIsAJ3NsY-$s7H_I=2KV$bxHatA!@p<}Pb8-Q8c16|eR?f^=Me-e2 zic!8jnk(ccl_9=Ny^3nA3gp>uB~qSuGg&U}{KbCm^1>s=V(XIe@XY+P;)`A0*|^t7 zWg6Joyt>G{#=|G7`lb$0Uzx-?xiQxH!u0G zKtZQ{Way8BYs6&vpt4a52*cPk#Y~@Vs;~ZdBT4}Bg|<7u`dODjstJWBW|zNszW=D+7)Wa*R(%)?jz?^u*#Wgzqf9=eUV~Zp$8&Je-$?)+e(!^lQqnh+=shm!ctixeD-GO zMf(%`XV*I58?GQPg@c|?z7?`&-%Rbn*HE14{ne`9sy)-ce+uQo*1WwX)*k3^ z<%dA6G@rR!VbxC;Dh+cX-8t{4Pf5%2VHjXy1j`VHuS|9{@|`*da*MFtjt_=do;&iw-j4t9cgUPh`R&-IS z!Xgjzhb-xljGyBSuCSXp+ZED$78Bd{X|z(Z0RMg?QiL1J4GdIe_cwCiZUoZlJ}oXdpM|?9LpSb~2%{*tej1kT`j#Xe0Qo(|n`7Qi(s!P2SLhOtBh)XJ=cc#u z+rP}4Z)M8_3s(2XcLEH;UQr~lX$9|nF%}fyZrLz+UXmU2;Ti^8@D(9H_z)0+{>;0Z`uHm%URHDGY%036GKb;@yf(WJ`bHVHn*(uTV@RE-ZmY6*`E#+4;odTlpPO? zW369B1NDb;|Mo`bc6}`-_3=3asRI%{TA$u%8~Qests&44MzJPexkkQM&Bhe3m6QT_ zW9BBxk7>E)w%(OourTHIpqg&x*2&BK`0FVKnlYF7c}V_8v-2jej)MU-P+Hci#Dg4t!`R*xP(UUe7NM{~) zTGje7x5LU;PJCmp+iFsScrK~Jc-g26p{Yh_$|(kq9lI^WAQ7Vn#!FvIW`>j zG?z2$tHZRA;9B+akD|ZA)a$=Z_-|{!MvgP}k&mqAg;JV+i)*QFUO~o@7}r9D1rE9U*7Y3kW(JNgzk%OW=8f#2*I82+j8i! zw){CfAFgItIX*B=e=)~4vIE`{p_b8AV9eEwAsg!n)rA)RowlLMIN?tupF=Oq<|1p*=Y^;jzg zFA;ka#?E-S@cUC4;eFG2+#Mpo(;mzrkpT5A3Wp@m>a1e$S3iy2cQA;9ETk&4d(k&( z9Cxjz$g!qC$V(mM$zsy<`ql-A54db#9#+L=Ik39vV1Opo$5PWzYOKVI!5<$D3l`vX zGGt)cs5cTu|MWLf9EW$4Uj+`fD9$9Q@GE0JlScm%r7kUfcY(HZ5n|U6ckcNrF+BMR|`aRrnS52?S9h#>t6-Z{$7Rs zwu}#%G#6ep9>uLtwh?;H@>m+XkN)xuC>sV!Q!2(i@eF$JnZglIccqiO*BGcoJnaZ| zYYgnIMqFX_yP(m2{X$zHgkrzaf-{k~!kEcMro&Ake7SEySLAJ#0BxaN`Kei}U9~C; zdlZdg$XaZBlk_ouY&a$d+4jifH@Hi)*H~?^p*a&kNdqmfck-BZ*I~`#milM}(GQ5< zz7xiZdHD#>m?3U5M+__94i|f^p!!A98AlJ_HD$pXpps^X>2RBexyjHnV`Wb%Uv8Mw zMH?Irf4p0s99kl?c({G83;PyP;8-}}<>-nVc!85TBqS$~rM&Pc>TLguc9 z6q7`AQu^{}fm^79Ke~aMp`_kdFD%Q#J=6M^f&TDd0`2P|`-tJ;Kqdn6HB`PHFDf=*ijtG+P;PYMbDG5avn-5LgKocFvWiOlV(G!S?~Z2+>Pp?Jr8e z+C08B;%0&1D1II+MY(bV82W(Ss^Y!mbXc`Pv>&y?JAY{1j$xyHs9zfq9rMAm69Axk zJn806Pp9Byhr||p+}hxtRnnpReTm-!LA{)jRDm#LB1FW9I| z9_H}<_o~>@SLT3lA4HWq($q*_%?>Gf%7>A^vQ42d4M!(t8MxTrHKFoV#fY&ojP4bz zHT7QNC;8X@j(JGnO$Z={`g{Aix)seloCCB8J7osVkon1%!P6kZCrycfv_=m7kXq6cP!ivLl(xsE$-Q`P|y<3Vz%YB6#;V=sxr+MMNlXUlq zS(N^?ui5n3)Jr09gZQ{qb)Z@_Cc?WMkm(IDxM$KdSE|w_(ZTjm1>SAM+ZO<*_9Hhk zRe%jzWgP4tPxCb%&8+vTtU)aK-NfswP3#YuWa@Nn=(PXo=Oq7&bMymJ{oxRyIuidE zn22KNSgVw-GpY>%a~6OXxSi#{Ogo)XGh+t2-tK#U94+@@&yhCvcP*1~EUsb$J}2~? zL!}##9MvCmkP~IJqI6LBa*RsC%dQG6d77Ti%^a}}1hqRNJ9mLa-;^-I=_`fPB2tUKx06v8p>ObY$1vSZ~E zN0AuLB}w7zT46zyR31buAT75NJPm|L@-u>~g0^OR=f*mWFnq6AW(gjXy#R7B`5c;{ zYIsoJ_?`&^XAUb|e3tH!=p~U+Z<#VNXZ?z~)O*;xePFA5`F2RzOn2(ZvRhBBOZB$( ze8qRGDat)aAl@)AmFH2B*=vvok!eUMjvwz zk3wAAYXdeQE%HIH%_3fu^8eQ{+wyqhe859I3%}~IaaI~054mPJ27^sB^kxNfgLv^0 z>!Pb*)@Pey_B0;ttWU8#(GT1cN zJi@f5?X;)g;D@C}_>P$v#UP3T)C*tQr1 zZU08OOMTq~Xr?h2A9x?;z;c{ZyL}h=Uq$Fh#ge5Q_cnL9xq>Tq}SSfoe!~Z{z!7dyO;hsSUSg5+vPBBxR|f!Hvas3 zZBOCbG%<*OXZ?Y%y#xCf1Xwv6-7?BsGH~6nOi)$34l4(1$gGxx9fhXv=WT5;0NE- zRpq!d#r`eM{fjxd7|MJ;wo~-OwG5-MAx(pKOrXs<+SkH zRik7Impyh*e7!lbk!&bx^qKj634z>((vC*UnJe<^S@g@3kB+stKN4GK?~7j_*8>1T zJZ|WcnuxP@slWBw=2H>r9)=lN3zdPeOZ=rI{@Zy~lwFk-P1^c(f)h@IL+ z;wM%=UgEC<&}i3^{HI>QV#(`A^k79QBg)xv>3IIBXHlOQr}o{W&--_P(ySS5I-a_w z0GXq2X~@!F7wv3)vz-MCa>B-`RB!APWwSU4vP;Y_LPo}&!*mQ1G@z{cI-TUyy_|5M zCOBQK-@RiH3))2$hzK?vUZ>fuEhsUCxBSq~6mt={^2d_WwWXgU{vqI#wS1>7p+_d@ zFh-19Q?5P{ zKYe74u&^c|kI9U-v6i>5SC!E#ZcIN~OSCqN`4ba$KJto)eU^S`p-WF*B`e0@U{)?D zq@^(GLIXiJ5Y;v!_NZ#YCroJ-d5OP^#2*c_r1a5X;bf7ryaHG@8S8TaZ$D__mp>sh zNv~#W6`}bTz4tXM9AU~=S?S1i5}xnW6yw9ASs=0ZQsO>XwtNl0CZ5sARb=5C!8{(= z%&Q|1V7~6ad6jL{w+bdtCWn?W(xl3ZOk8$A^J<<~pRHj#UnzI?m8d=p9-R+V++t%> zy{V_7<7;{3VYyio{W^e6GQ&a%z*#8K72mfKkWxf&OSF+d+^cX=0msO@Kei=T&A0+X zr(-Yi4`taUt)4LznpL!PES-~`umkO3H6s`c`;yC^8_; z68s~t+8BWGkdz?s4O!EhK^vAc)DxOhgjk@y%K6EA;{{JOkgB;>82`?8?)wS=VD3d^M4b{D-cwu_`9ynnDFSOyv`m zwk!!Nj*d?bQf67FI`4m!&iv|jS^pmh8h=1Om1Os`&*ez061-b`r3Y?{E{r`m;gAaE21gTkSNhxOoh^d;{cV4H*(37 z2xe55_*Y5!w;t+A^F4%+i;6cFny@QU&vt$y%JiRsEy6>mIoV9PqAoEk;(r3mmj-|pi|gaN-7D|u&*xP z+fVf_-$FKUCpI+-Gv@i0<79!d7EffeAE#XvT?~`IQ~qOArEnHzgcj_GFsk zBB%`TLFzdw+>`I>!=HM9u^TWrVi!B%%NOP`MkoXop)2b^y~&wFF_Ke-s~lJ|3fZ!@*`|8G zhNO90oB3{0f8ME5K))^YWD2>BP_wi(r#85Pb4MmO0N?7*g;o35I zf(F{P~1 zso#azoJBRy`G@4H@Pp8 zDB%xzSZ2)GaWlq#$*F+Lt-Xh4HK^S8b&(umx1aQ|Fi4=e}D9Kr0MAMo%=1.0.0 av torchfcpe +pybase16384 diff --git a/requirements-dml.txt b/requirements-dml.txt index 2dc1b67..7a6106e 100644 --- a/requirements-dml.txt +++ b/requirements-dml.txt @@ -45,3 +45,4 @@ ffmpy==0.3.1 python-dotenv>=1.0.0 av torchfcpe +pybase16384 diff --git a/requirements-ipex.txt b/requirements-ipex.txt index 48b0712..cac27bf 100644 --- a/requirements-ipex.txt +++ b/requirements-ipex.txt @@ -53,3 +53,4 @@ av FreeSimpleGUI sounddevice torchfcpe +pybase16384 diff --git a/requirements-py311.txt b/requirements-py311.txt index e002fbd..bb3ff29 100644 --- a/requirements-py311.txt +++ b/requirements-py311.txt @@ -46,3 +46,4 @@ torchfcpe ffmpy==0.3.1 python-dotenv>=1.0.0 av +pybase16384 diff --git a/requirements.txt b/requirements.txt index 7f2268d..de4dac1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -46,3 +46,4 @@ torchfcpe ffmpy==0.3.1 python-dotenv>=1.0.0 av +pybase16384