From e9a35ff6e24bcf8ca05e0844cfded946a04aabf5 Mon Sep 17 00:00:00 2001 From: if-ai Date: Sat, 30 Aug 2025 02:35:47 +0100 Subject: [PATCH] Optimisations handls both types --- .../document_symbols_cache_v23-06-25.pkl | Bin 0 -> 3566064 bytes .serena/project.yml | 68 + INSTALLATION_GUIDE.md | 194 +++ README.md | 143 ++ __init__.py | 12 + __pycache__/__init__.cpython-312.pyc | Bin 0 -> 629 bytes __pycache__/model_urls.cpython-312.pyc | Bin 0 -> 1454 bytes __pycache__/nodes.cpython-312.pyc | Bin 0 -> 51804 bytes configs/hunyuanvideo-foley-xxl.yaml | 49 + download_models_manual.py | 133 ++ example_workflows/hunyuan_foley.json | 708 +++++++++ hunyuanvideo_foley/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 177 bytes .../__pycache__/constants.cpython-312.pyc | Bin 0 -> 1954 bytes hunyuanvideo_foley/constants.py | 57 + hunyuanvideo_foley/models/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 184 bytes .../__pycache__/hifi_foley.cpython-312.pyc | Bin 0 -> 43659 bytes hunyuanvideo_foley/models/dac_vae/__init__.py | 16 + hunyuanvideo_foley/models/dac_vae/__main__.py | 36 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 709 bytes .../models/dac_vae/model/__init__.py | 4 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 355 bytes .../model/__pycache__/base.cpython-312.pyc | Bin 0 -> 13036 bytes .../model/__pycache__/dac.cpython-312.pyc | Bin 0 -> 17298 bytes .../__pycache__/discriminator.cpython-312.pyc | Bin 0 -> 11511 bytes .../models/dac_vae/model/base.py | 301 ++++ .../models/dac_vae/model/dac.py | 410 ++++++ .../models/dac_vae/model/discriminator.py | 228 +++ .../models/dac_vae/nn/__init__.py | 3 + .../nn/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 289 bytes .../nn/__pycache__/layers.cpython-312.pyc | Bin 0 -> 2232 bytes .../nn/__pycache__/loss.cpython-312.pyc | Bin 0 -> 16416 bytes .../nn/__pycache__/quantize.cpython-312.pyc | Bin 0 -> 12444 bytes .../nn/__pycache__/vae_utils.cpython-312.pyc | Bin 0 -> 5990 bytes .../models/dac_vae/nn/layers.py | 33 + hunyuanvideo_foley/models/dac_vae/nn/loss.py | 368 +++++ .../models/dac_vae/nn/quantize.py | 262 ++++ .../models/dac_vae/nn/vae_utils.py | 91 ++ .../models/dac_vae/utils/__init__.py | 121 ++ .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 3798 bytes .../models/dac_vae/utils/decode.py | 95 ++ .../models/dac_vae/utils/encode.py | 94 ++ hunyuanvideo_foley/models/hifi_foley.py | 794 ++++++++++ hunyuanvideo_foley/models/nn/__init__.py | 0 .../nn/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 187 bytes .../activation_layers.cpython-312.pyc | Bin 0 -> 2562 bytes .../__pycache__/attn_layers.cpython-312.pyc | Bin 0 -> 26557 bytes .../__pycache__/embed_layers.cpython-312.pyc | Bin 0 -> 7222 bytes .../nn/__pycache__/mlp_layers.cpython-312.pyc | Bin 0 -> 8753 bytes .../modulate_layers.cpython-312.pyc | Bin 0 -> 2864 bytes .../__pycache__/norm_layers.cpython-312.pyc | Bin 0 -> 3120 bytes .../__pycache__/posemb_layers.cpython-312.pyc | Bin 0 -> 7520 bytes .../models/nn/activation_layers.py | 44 + hunyuanvideo_foley/models/nn/attn_layers.py | 546 +++++++ hunyuanvideo_foley/models/nn/embed_layers.py | 136 ++ hunyuanvideo_foley/models/nn/mlp_layers.py | 149 ++ .../models/nn/modulate_layers.py | 49 + hunyuanvideo_foley/models/nn/norm_layers.py | 70 + hunyuanvideo_foley/models/nn/posemb_layers.py | 159 +++ .../models/synchformer/__init__.py | 1 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 246 bytes .../__pycache__/ast_model.cpython-312.pyc | Bin 0 -> 15574 bytes .../__pycache__/modeling_ast.cpython-312.pyc | Bin 0 -> 34141 bytes .../__pycache__/motionformer.cpython-312.pyc | Bin 0 -> 20200 bytes .../__pycache__/synchformer.cpython-312.pyc | Bin 0 -> 17808 bytes .../__pycache__/utils.cpython-312.pyc | Bin 0 -> 6246 bytes .../video_model_builder.cpython-312.pyc | Bin 0 -> 11822 bytes .../__pycache__/vit_helper.cpython-312.pyc | Bin 0 -> 18050 bytes .../models/synchformer/ast_model.py | 289 ++++ .../synchformer/compute_desync_score.py | 214 +++ .../models/synchformer/data_transforms.py | 1130 +++++++++++++++ .../models/synchformer/divided_224_16x4.yaml | 84 ++ .../models/synchformer/modeling_ast.py | 673 +++++++++ .../models/synchformer/motionformer.py | 397 +++++ .../models/synchformer/synchformer.py | 355 +++++ .../models/synchformer/utils.py | 87 ++ .../models/synchformer/video_model_builder.py | 270 ++++ .../models/synchformer/vit_helper.py | 364 +++++ hunyuanvideo_foley/utils/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 183 bytes .../__pycache__/config_utils.cpython-312.pyc | Bin 0 -> 6783 bytes .../__pycache__/feature_utils.cpython-312.pyc | Bin 0 -> 8429 bytes .../utils/__pycache__/helper.cpython-312.pyc | Bin 0 -> 6330 bytes .../__pycache__/media_utils.cpython-312.pyc | Bin 0 -> 4647 bytes .../__pycache__/model_utils.cpython-312.pyc | Bin 0 -> 14226 bytes hunyuanvideo_foley/utils/config_utils.py | 109 ++ hunyuanvideo_foley/utils/feature_utils.py | 156 ++ hunyuanvideo_foley/utils/helper.py | 134 ++ hunyuanvideo_foley/utils/media_utils.py | 130 ++ hunyuanvideo_foley/utils/model_utils.py | 274 ++++ .../utils/schedulers/__init__.py | 2 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 375 bytes ...duling_flow_match_discrete.cpython-312.pyc | Bin 0 -> 17196 bytes .../scheduling_flow_match_discrete.py | 376 +++++ install.py | 105 ++ model_urls.py | 40 + nodes.py | 1271 +++++++++++++++++ pyproject.toml | 56 + requirements.txt | 39 + test_node.py | 186 +++ utils.py | 242 ++++ 102 files changed, 12357 insertions(+) create mode 100644 .serena/cache/python/document_symbols_cache_v23-06-25.pkl create mode 100644 .serena/project.yml create mode 100644 INSTALLATION_GUIDE.md create mode 100644 README.md create mode 100644 __init__.py create mode 100644 __pycache__/__init__.cpython-312.pyc create mode 100644 __pycache__/model_urls.cpython-312.pyc create mode 100644 __pycache__/nodes.cpython-312.pyc create mode 100644 configs/hunyuanvideo-foley-xxl.yaml create mode 100644 download_models_manual.py create mode 100644 example_workflows/hunyuan_foley.json create mode 100644 hunyuanvideo_foley/__init__.py create mode 100644 hunyuanvideo_foley/__pycache__/__init__.cpython-312.pyc create mode 100644 hunyuanvideo_foley/__pycache__/constants.cpython-312.pyc create mode 100644 hunyuanvideo_foley/constants.py create mode 100644 hunyuanvideo_foley/models/__init__.py create mode 100644 hunyuanvideo_foley/models/__pycache__/__init__.cpython-312.pyc create mode 100644 hunyuanvideo_foley/models/__pycache__/hifi_foley.cpython-312.pyc create mode 100644 hunyuanvideo_foley/models/dac_vae/__init__.py create mode 100644 hunyuanvideo_foley/models/dac_vae/__main__.py create mode 100644 hunyuanvideo_foley/models/dac_vae/__pycache__/__init__.cpython-312.pyc create mode 100644 hunyuanvideo_foley/models/dac_vae/model/__init__.py create mode 100644 hunyuanvideo_foley/models/dac_vae/model/__pycache__/__init__.cpython-312.pyc create mode 100644 hunyuanvideo_foley/models/dac_vae/model/__pycache__/base.cpython-312.pyc create mode 100644 hunyuanvideo_foley/models/dac_vae/model/__pycache__/dac.cpython-312.pyc create mode 100644 hunyuanvideo_foley/models/dac_vae/model/__pycache__/discriminator.cpython-312.pyc create mode 100644 hunyuanvideo_foley/models/dac_vae/model/base.py create mode 100644 hunyuanvideo_foley/models/dac_vae/model/dac.py create mode 100644 hunyuanvideo_foley/models/dac_vae/model/discriminator.py create mode 100644 hunyuanvideo_foley/models/dac_vae/nn/__init__.py create mode 100644 hunyuanvideo_foley/models/dac_vae/nn/__pycache__/__init__.cpython-312.pyc create mode 100644 hunyuanvideo_foley/models/dac_vae/nn/__pycache__/layers.cpython-312.pyc create mode 100644 hunyuanvideo_foley/models/dac_vae/nn/__pycache__/loss.cpython-312.pyc create mode 100644 hunyuanvideo_foley/models/dac_vae/nn/__pycache__/quantize.cpython-312.pyc create mode 100644 hunyuanvideo_foley/models/dac_vae/nn/__pycache__/vae_utils.cpython-312.pyc create mode 100644 hunyuanvideo_foley/models/dac_vae/nn/layers.py create mode 100644 hunyuanvideo_foley/models/dac_vae/nn/loss.py create mode 100644 hunyuanvideo_foley/models/dac_vae/nn/quantize.py create mode 100644 hunyuanvideo_foley/models/dac_vae/nn/vae_utils.py create mode 100644 hunyuanvideo_foley/models/dac_vae/utils/__init__.py create mode 100644 hunyuanvideo_foley/models/dac_vae/utils/__pycache__/__init__.cpython-312.pyc create mode 100644 hunyuanvideo_foley/models/dac_vae/utils/decode.py create mode 100644 hunyuanvideo_foley/models/dac_vae/utils/encode.py create mode 100644 hunyuanvideo_foley/models/hifi_foley.py create mode 100644 hunyuanvideo_foley/models/nn/__init__.py create mode 100644 hunyuanvideo_foley/models/nn/__pycache__/__init__.cpython-312.pyc create mode 100644 hunyuanvideo_foley/models/nn/__pycache__/activation_layers.cpython-312.pyc create mode 100644 hunyuanvideo_foley/models/nn/__pycache__/attn_layers.cpython-312.pyc create mode 100644 hunyuanvideo_foley/models/nn/__pycache__/embed_layers.cpython-312.pyc create mode 100644 hunyuanvideo_foley/models/nn/__pycache__/mlp_layers.cpython-312.pyc create mode 100644 hunyuanvideo_foley/models/nn/__pycache__/modulate_layers.cpython-312.pyc create mode 100644 hunyuanvideo_foley/models/nn/__pycache__/norm_layers.cpython-312.pyc create mode 100644 hunyuanvideo_foley/models/nn/__pycache__/posemb_layers.cpython-312.pyc create mode 100644 hunyuanvideo_foley/models/nn/activation_layers.py create mode 100644 hunyuanvideo_foley/models/nn/attn_layers.py create mode 100644 hunyuanvideo_foley/models/nn/embed_layers.py create mode 100644 hunyuanvideo_foley/models/nn/mlp_layers.py create mode 100644 hunyuanvideo_foley/models/nn/modulate_layers.py create mode 100644 hunyuanvideo_foley/models/nn/norm_layers.py create mode 100644 hunyuanvideo_foley/models/nn/posemb_layers.py create mode 100644 hunyuanvideo_foley/models/synchformer/__init__.py create mode 100644 hunyuanvideo_foley/models/synchformer/__pycache__/__init__.cpython-312.pyc create mode 100644 hunyuanvideo_foley/models/synchformer/__pycache__/ast_model.cpython-312.pyc create mode 100644 hunyuanvideo_foley/models/synchformer/__pycache__/modeling_ast.cpython-312.pyc create mode 100644 hunyuanvideo_foley/models/synchformer/__pycache__/motionformer.cpython-312.pyc create mode 100644 hunyuanvideo_foley/models/synchformer/__pycache__/synchformer.cpython-312.pyc create mode 100644 hunyuanvideo_foley/models/synchformer/__pycache__/utils.cpython-312.pyc create mode 100644 hunyuanvideo_foley/models/synchformer/__pycache__/video_model_builder.cpython-312.pyc create mode 100644 hunyuanvideo_foley/models/synchformer/__pycache__/vit_helper.cpython-312.pyc create mode 100644 hunyuanvideo_foley/models/synchformer/ast_model.py create mode 100644 hunyuanvideo_foley/models/synchformer/compute_desync_score.py create mode 100644 hunyuanvideo_foley/models/synchformer/data_transforms.py create mode 100644 hunyuanvideo_foley/models/synchformer/divided_224_16x4.yaml create mode 100644 hunyuanvideo_foley/models/synchformer/modeling_ast.py create mode 100644 hunyuanvideo_foley/models/synchformer/motionformer.py create mode 100644 hunyuanvideo_foley/models/synchformer/synchformer.py create mode 100644 hunyuanvideo_foley/models/synchformer/utils.py create mode 100644 hunyuanvideo_foley/models/synchformer/video_model_builder.py create mode 100644 hunyuanvideo_foley/models/synchformer/vit_helper.py create mode 100644 hunyuanvideo_foley/utils/__init__.py create mode 100644 hunyuanvideo_foley/utils/__pycache__/__init__.cpython-312.pyc create mode 100644 hunyuanvideo_foley/utils/__pycache__/config_utils.cpython-312.pyc create mode 100644 hunyuanvideo_foley/utils/__pycache__/feature_utils.cpython-312.pyc create mode 100644 hunyuanvideo_foley/utils/__pycache__/helper.cpython-312.pyc create mode 100644 hunyuanvideo_foley/utils/__pycache__/media_utils.cpython-312.pyc create mode 100644 hunyuanvideo_foley/utils/__pycache__/model_utils.cpython-312.pyc create mode 100644 hunyuanvideo_foley/utils/config_utils.py create mode 100644 hunyuanvideo_foley/utils/feature_utils.py create mode 100644 hunyuanvideo_foley/utils/helper.py create mode 100644 hunyuanvideo_foley/utils/media_utils.py create mode 100644 hunyuanvideo_foley/utils/model_utils.py create mode 100644 hunyuanvideo_foley/utils/schedulers/__init__.py create mode 100644 hunyuanvideo_foley/utils/schedulers/__pycache__/__init__.cpython-312.pyc create mode 100644 hunyuanvideo_foley/utils/schedulers/__pycache__/scheduling_flow_match_discrete.cpython-312.pyc create mode 100644 hunyuanvideo_foley/utils/schedulers/scheduling_flow_match_discrete.py create mode 100644 install.py create mode 100644 model_urls.py create mode 100644 nodes.py create mode 100644 pyproject.toml create mode 100644 requirements.txt create mode 100644 test_node.py create mode 100644 utils.py diff --git a/.serena/cache/python/document_symbols_cache_v23-06-25.pkl b/.serena/cache/python/document_symbols_cache_v23-06-25.pkl new file mode 100644 index 0000000000000000000000000000000000000000..cf2860afdd763e8e2717bc71f3bc3adc07a8549f GIT binary patch literal 3566064 zcmd?Scf4Fjc{eWk_NrpbvLws0Ecc>o8{4>oWffaotz@g%23tO?cF(=LtG&C|TT~1V z229^jFNB!Rk75ELG}A&4y@k*_1QJ3h34uTe$qVn4=bXJ~?(>{ElK1R0@8`!K$n)HJ z_MG$mKF^eAo|(C?_r*Pn7B7u2HO3b_c&y7;abTZ5GDcC8;o&D-_H5nqLub*cvQXb6KgZ z9^2Eb73W%8r;26&>WPVonMz|KKe4$wGj(*|_K94x-l)!a3Wz}H-PEicZDuRC7V~~} zTea*Tod|%M3qGwd)Ednms8`F)hJSOmQJ8Bz>f!$KXw5H+w>|{FHyA#%+3KIoicbR+ zHS~Sz&jIGGW#Uze`Eq@BW4Z1%j?Vh^xz_NWqcaDpU5KW%RJTwyg%%AyD< zi{_DXVdj~=g^MN^Bdiq8E1WE8IHgnQ7FLU&T=TG@EUYb@7Bz`;3%&A1`&vt}5*&)nN9iq{!P$SVCIRrcv0~h&C@_UAe|Wr1y#X?e6D&~r^r(OWUVQnrBTE7@fxXp`};cm?VC64-FoA$-H%=P?eC6!yDXXCj@L--+dt9a zZy(vRby*b8cg)_V0utyr1%%GK(@x>qh9^u0qejN%1D!|1T!qk^OMWayKkw98=Z zV?x$830XQzn*>a@hKiNhW_blx8R*kQPrYHa-@ z4Zl(s@h;_bQ4Bs2!B3V>F)+Yuq=i9gSi~PY27{lDTl}_TFR0z(L{Cs8Bc8F&@M%F# z=nR=vi$zfmf#*k_J`-V6mZG7DK4lOKJweu7A9{i;osInR!Wzm7N-HI0osRUC&gi5e zXqF_)!P#^>WT()|3Z zJOBAINM85z3yuFrk?)tK)yDVZHPZb4ZJq!AzNuoZjzk{*kNDuW^~;hOdBAI=`T4s$ z|M`Pr2p|TdvkS)onG(*c(pOn8XW~$Cru2V+j$Iz4`ds;f>$rzvv9bOxklp9ZY@>uWWyGyI%(qYK<9DjFZjjj z!oogNV{l4n3+i)E!!SVVq=~^BI*&o$;bIO2TT3WOj`6A2>dPh1h&f`Y+x2PH)6qzS`kI}bzu zRJB>lIFnw_EQqzS-}IuF3&;zItn z%g+zzQ5jWSX7EqdN%Qmnt@EGXr-n!iA$Uv_0#!8Tk{PN_nh^Y<;}8@!o5AGFW97%6 z8n%I|xYppGs*~o&_nqK;Jhe2_ae`W>c(b)?&acO$2?6N&RWkHm?&>dXO=+?HQUoiRW$n(s*bjX8zd$jZzwXIL^_>i>(Z7^zN4mTnRmEW zZ1|p<1%<0<(T?OvQG58BESgcs`=a)sigLL{PzD_du|}6sRo8_B3{^bSXdb9K+8`d6 zib43D2b+w+y-1?h9%QSc8GEAYq=~`ZoyTCf(wxx^4-$`khs2@XRj8tg163zY9PaHr z4rB~YeP6qQR7ErBK-EbTgJ*Rf11bimKCc~vQ$;fdN7YFaf){rl0y+lwP}CPx(Tu@S zbb1uSs%TD{s5;sdE|!3FJi2&I=V9(s zyMhP^R)j5r`dTxeO9_!WX+m&gCm~458!qkSJ8ExOQJtEa=MFOXo||nh47x9uZqV*C z&ydASg~iX$J33Jc+kK`g%2ANbD^^OqwpXa4=@nERZ4!G^@d~f=a8q8PKCf-@s%Uxz zRVPgd4t5fPl)U2BPQD{}#Z2{(9~?&`T%z5zNBfF1qh2u~iACwpXa4=@nF+G$D9iCm~45EAHv!J8G{uDPKLT$4-Zf zje^+KQEy`FvzJ7j;(#m~yBjl?Mx8NcwrQVt1%dmI%&f2&d$RyoGUad2Z^M3g~PQ`9Cl+kAa&Bj z;lrKBVW8k=^L`CkmAo?QOR8uFU#U82g7CS{gU~A{6$@L1tD*>~qH&-_hN_b$0^jI7 z0>ir1^=7MxC0rfFK^4tN163zY9Ddw+90mmrvtnyKVhx)m3hj7{Dw_F0RGlg`7|T^F;;@B=r%yOtus*X}LeZXKs-hX6r|P5$ z#XxVzqp^#p7PgPK$?T$50;8Lx>ZJMkCwBhxduI=1k-f&ZO9bu<5m3eZ4Fsq< zX(F($^9b~*0nZJMcS9J2@ zQ<_P>vh(fr(pJ2ct-+qB(e`>ZFN8 zt&>Qk)KG5iZ{jl{HCNyPf=BSv=2Z!|G8|RS|YWKs#wU!!M&$)`l&m zDn15#3Q|X##gimB9d9Y0-Fe7|^jY+1OL?yZvOTp_MKg1Rs*@%RFG&H0m=5D9Q5aOw zgn_D)CJe9bJPgAchGJ!^x^VpP^e7IhXwI;yI%(qY*3RQF0>^uzmosFZ69qyQjhhO{ zI%xv&{>}q29*!vGnW{(-?s-uxRMA|tqUxlH#V0$D#Zb0f7Nkf&k0`dLM12LMdi3@+k%DK7>XF(M1ZFOp&r(DJKe^=&8cYvB;$K?%U4!-2Ib-e3Sg)kzb8@s0z4MCINsKmN?{<5khzNJrI4^W#@_ z{Nq~#^=3|K-aK8EkS6X@~4VsJ3!S*6NF1T z4?@2NVWA;>USiM=(5s@km5{2FCI;7a9)q!&V!bX8q$kGEUzSj`y9HG=!}V01G@;nu zc_@YoQ;=x5&?0^ig+UdKL!vWOoit(C*LfHQ0~i*f@K4dUK^0}L*!(d9RVPgpvYkg^ z$gkC^HE*Vl9NhEUs4=LbX$(}IG+~(OBn&AeU}5S?#E7u16F% z7&iMSS*N{vrHZoS7i2+H7J^Us975G!6%L+MQ68*3-z}&*+8*wa%ym2@`?OBlgN}la zi1d!BqBz0{yT~r1KC+$Npo(TPBvmI(AYRaUAjYyynT-xGZ=AhW6}J70u}eRVPgtzSVgcmib3!#TD-2OhmE3NyKSKqHw698J3~y zqzT7AcOH)MT(*)g=Ck66Iec(28pT2t%~MLLI%#6@ADzcyB-gCT`|R;0zNJwhRM9*p zld6*@5dWw1Kv^O*Gc0iH5i&pqF@jG8K)sEj)W${9z=~8vt@jF$N z*QX1nbl9pl`0eQCT~%>q*b=CsIq9P6Xe-z+`RaJ!uFy#ZQ<^yE!^8HY~eb?7HYCi+ZI$&W&#Snf}IEraoH?+2)$Bh=gz9)Mc6Zt zI@%_hso2DQY2e)y6Rt|m_v*Q985uUHkDmzrLlw7RSRi%M#G=)CEKZcCO^e)8U(CBC zAPpb82Cy!T>*Np+RorL*Le)tVke7BIkdb#!X5V#g#TZA~1=W0#YYU1m4zp1gHb~8&PjiMYH>)>ZA$52RaV{c{8f|zV zRGl<2_;lwnz)xIOAJ=aGRq>IAA5eAD1mLTk2VeL{EzXp^D~a z9jZ>6KwQ>&AZ)orPme;Oiss4=RVPg-u1^69OD@qfqEM)!2?bRrO(=GB9twJ&!n30w zsG_+~fvS@x2)CyI0=cB6K5#`i|5ima7n!P)CJ6b?gD@g)UvC_32SuZEju%L4XlD+o zqPby%s*@%Vvz-TGR0E-IqeOyIFN`9gil#|Wb<#xQiJeDc+58Jt)$P6uN7@fYn+a7k zm-?tWX~OZ$&ciXRE~Hd@SQg^&wkQs&Xb!2UI%(oC*LfV4w{Pe4ay4Hpr64zfygiDC zDw?~Ss5)ul@#@awaYFUrLVI|>{Qh>1p(>j8K-Ed}``^;}?~l7LPkmy0lbb58H)dQ^ zoit&1Z|7m?LAUX!kJ}KAAXM>e3;?7~ngD#f^8m=H)*-*v!1ptKN;*V4Cqxy`F@T`z zqzS~AIuFF?p<-R`fu9m>BQk>cp+urRmQqD?1VPnF6N&G39*JSK3Lb|;`#3)J$z~i| z36VN!;_%bX<1nsr4LlLG6wALJWfSd9P3p6+G`vFzkveH&@t+;XqOf_vK_Ner4$;0~ zR~5}pf~u3|$N#D0AK&T=lTM=Zt6xh9+7UTbG|yzD>ZA$5$Y93<*1?*;%)wbWZc-oF z-p8ei=D3lnlO_-=I}gM#EN-Dk&;2oK5UOa7AE`QN;;^CfI1Dw4A^_76nIH?zpG8v<1)O)ReIJQkz6ztj%05Q*U^5~^sP zr$p6B6NwvBLqeQ(U#}vozT;6ORMA9&s*@%Xds0I}UWidf&h%XtMM4!#B&a%RBH?u& ziDfzv1>Fa}|KOx39I9x><)}Jo!f~+kaER$v-miL8z;;Cx2~{)=gQ}Az5{EmF#CSVG zi66M!1mJm{2Vg`ygjYt- z>pUw8gesc*JE=Np0`aoW1JP6Sk^bUb2|znCsfy-`7F8!r0N&Vn0Q!_AEF4Cu&ui~6 zP(^dIGgT)|2;SLw2!<2{UJX03^}?tnsG=Eupz5Rv!-qQ$!%_`HMW$1hi}zp)Hyfj9 zsG=E_qw1uI#^*YZ#t3Ze#RJwmBoOVXrz)CV1yv_aAimLgAmTPL&O~8QMRNvD)kzbE zA9oUllr}LQ?qu$(-|;Pgg_{d&T92BTsWc|?6Pv3uQ%Co0pU5@qjp~e75jQW^1EF_Q zvvRbVt=uXH+S{sS|LBDJ`^H&u>HXS*h|jMpoZi&?>rULcYs*$|^Nvk>_IR01H{ZN{ z=Z$-0*E3!ilof{M|BYszEUju$xGbiMW_A%(M_N_KcSHWRgXT03jnxW`E!+3pykpa2 zy`7sfTVp_ZS`-vjGy^u!XHiYE!Q2_x3r(Jw*3+6%L?mCBjQVxf3I(y93G3IvMB4` zOtvBxJ0)%B#z6fv0sOF7(7L#^VIC^CJ}gx5H&Kn!YWOEMjGTYdV{N@0PmJ`we%JgtaFL@qgi)RvvfHk zmOn8lPVX=0#a`68c8i)KBNvO;5U!R2Bc=1iPm4~dv|j!z5x7wN$2?P27nsl1s?#ER zSf49x6n~u@Elr4j7Ql9fhS-GdDlHawW0f8mqt2k$_Wtf+8;s6GbSoCyvvq4UYG4auk!*ouYgg+ww$JYxwga<%Eho;5 zj0g4{99R&GX+93DU9H=|zPx+D%4XJ^IS>!*xj3*OCV{o9bsN|Ry9Vr_ZfR}Eo`)j~ zVsg}PSL-&i@9G+|0|&A-nb8_=vKQjOf|xW}yIQw_{Yck<9Tq{6vN%}MitR->wjd@u zFuPi}vHg74u8*i{1aZo|LgzCKPYTX9)n`wh8g1OxaAmq~imLO(S*c>}#e55vx zQ;Z=ah{Yapd}L!+3yq^o5t*N)4QRMpm10=whLk^IK|ry0CR+^pJ0g5|jkXXoeE4K7 zCi^>3GXo|d9u$V9EkvF!sl$h(Vsq})(Pq(;7ahbmY1iq8n?DX>afT^wwRW}8YJVe{ zfR&CE3IAitgty~}`uLQ(IvYQ>MR*jBEr`b{Z0%~@#&&qPTQS;RvFc&5*hw5!5KDJJ zgYb5>Zlii~*H9hJ9?BNW*#qJ}uV{_LgpAtG2Yn+UuGVc} z|2%DAyAkmEZOZYFAmAkC6-I_ErxR1;X{)oVKvaj?}`HxtFKr@3s zB>pHv$Mfro81=^xvHS@vGdOkHJ}6#BWQnrUwyyM6@fIo${66s?tz`jkw{G9Eb(gp8 z<~?(zzZHL&94~!9{39WaK-;frFrfvpIMJu{@#9nbv`}kjX{XM`m&D`JddCwJ#F4w_K6m0`>vhd zo~@gA?c8F)eHh0b#4_a_#oeyfW!!(th5PoM$A$X{jys4+-0f;z#{EBea6d`6Ew^sk z>g^Ey@6Ns6mhGAN7G@kr9KoVG-JZKM#L5gTs_UzlVBi^c)<2Zv@Y}UhZwySj+=TmraULK59)DA!I=1qHV zipTpD6z>-=S|k=}#Q!GY?=pCe8aS>$U#-i)pUDIKi2?9pjIwR}jR}xng(5!*Vj0Ck z0k^Al8SsmF01rpyH}2cMWz){hTfIG-H|^LOZ|$ezn1h%cmD|<2jQO=Zn6C+peCNK5 zw|(cft-H4>;P-B|O+nYbs6uy zTzHG==ds|u4#ykBB;IzlF5~@p9=uPK_I*qZ%IP@ZASQcHyIPk4FLMDd4E&e?UylP0 zViIt>T9*Mo!UOmc8PM9h_4d8qj;%Xyw2jKo#K8u!%-2K5hIX|sgMB{_*ekpquI;52p_w#VvK}_OqSL-tF5AxuCvPSzB5z^Ye_pxGi zV((sctXymu@j@JP5R(&7yIPkqe-{tt<6C#{-nHAyY~8bG(~Vm#?aCu?tU)Yy!JwU} zU9HPlf26Be%QTzU$#qb%j4bjh`t^ZoW=6@w+fRjLDg=*7l2~$_hjqm~RUDs2|252; z5O56SvpMfjR@@O2JxTAbPN$D(JSM`&CBfykj^&n^PhZcB{OZsE^l+W;v{qYa$_}p-u>Gkn*&A-XL}BZap|ncnabOnd|fiLBixn>3`%uBGiNC zI{jY!Q6}chuPYLA{udF;pTKgRy3X%^iI)@kHcs=quJlv!mMS;uH{w5916ww2-X>0F zoh$uT6h#Kw@5Db6_y_^xnkGE4QZik;9AYXGs_?u5RKT}{7?=+6hmTd}ngC(4I+(TYj~;(^V!MU3V|t8q{s zTr%A##4~Xs&IE|%B0tK6UCj+9)-f}&q*^OZ7c1ft(j;V@gp&bc8E{0&u&cR2#s$n| z3>GW1&4!m3+p<+lNi}qZhG0jXwZ#{b`n8@_wbB8rJ0~cs9IYWGs|`*2Vr9)21{gnKyG5Iy z*(DUUm<$zwn%O0QSX%9ZE}`sIo8q{-xVXh0yg33fIZm~!g}yL4#@HpSln-Z2Ls%>} zzAO00@ks7o9DWd!Ss->bH{jpE41b~?`Bt0*5R*N!UCj*+CYU)$(j(udDKL9v5R*N! zUCj*&u3@HNaS%cz<^^}+@PnA_1ng>Vz<(n*{Njq*xITD44nK%V`0Z+Lz`utZe#?yP zE*yRklknTs+<@O>hQGJ$g+Uvl{h8Jbm_u6-lZj4tH8&VI$io2pglwGWyTb|DGC3g= zGa#Aq7|A^)Vs!_#8JZKbd$gEL1O_$JdO<92V$){W4jqo_EX>F|5v~Eha=H<#0P%%X zxX7*+y2cU75?9FXsk|mqIzv1^c7j&MnQ051ilZJ`SgP0(!6{zb#3ASUs*tGR*y>zVQ2ZNvX=9Dfj#_}kUo!2fN`_}^v2 zzlP%vViJG5nj854Ef4$~N=GswXpky=u*?7pKLZt@yyjlkkUt_!??(YF7(Aa;WINp$IV%cHu}PSROFfHMJNd2;}ocw|>|gNY@~Oe_tzxhLAq_$r(Y5T8P^VOMj5jWx_{ zv=1G~`-h4-OUM0coD2|?V{E&c8)TfrOvXs)8ueQI25g;ZvHrmzPAmS0+M2P98 zzCLj(NTViVes92u05M5~UCj+59?eZex!TNE#N6Mr5%ETx2oRG**wx%1Vv?DNc0ZA1 zG48E686YP63A>sbWZcF~M#95s-iEUPVse?*uI2^{Ic644s8|jMdN+-42;<0Ag~dpIyxj8eYUqLr5yW z5P_KV0=t?U_}qbX@JAjN*z+bo%ba{NEDi!nlsWlxZGT4Qr2QzMU*L8N zVtK2pHo*3No}TJ~QoJwx8~&RhKAVb}+SNi|7+J!YDS5JdIO8!3$@2659VY?AWPYAq z%?%RHU?$<%GGH=2a3q(}KZJ6sn+_|)DZ0VPMrKBmg)cJw1QUUr>Z%hdy1~TN%uFPR z}Dn;`J^s0M6eOasSMUc zif*uR7c(3EH8HfV#4o#Lh6x@5Idx!CB1JcNDDm=O4R~ipfdgahGnA8EMG{3fIA}6+ z@Xum;AGhd`86!9d%o2ivKu%H+R4^$B#eDxQju0-S@@)*z&yl!unQ^SOm0PS^u*3mGxhgWWhlW2fK1* zy?8&kv0goUO)H=7jrGCv*0we#y|5ls7Ti}a&W9^Dyu*HRx*+a-3vQ|(ZS|>iv^haQ ztFPV=H^Zs_>@627{@mnp@}~Ol3+yLXlzt%pIg_}l{=4FjvVBo?m+QJ>V&Nu=b_b&2x0OUoYmH+44T&U0|twxTT(=9YEY76S35DwOB?P zQA-6iafwVs4aCyGwWX?+W92e~o;Vy;___!0(~rN;D%>I0Yq30q5Pcs|69dV}`+!(3 z>zMD;lP_AF7?fAyzbTrR*jI^B3!QlJQa&dhmCx>;6KhLto9JM^SccPn;u=!m;B1_O zM_M?DQFDTWQ+PS(5o7E4er%0)JJYQ|EN`Mlz0t1b1pH^Rz%NIazn9$v(-^Ub<*UQK zX<({aJDjZrhDUS*8}M$x)-8dWX|Eub`*e(Mp!5Y9{ElmZKv&!(7U@|)EEh&m7qF{^ zE^vt?gvl7L;WdWE;sT*K@Xy5C9}ttH0lS(L;NQp#zPah9mJ5a9X#4RClUVo}KyD_851u;2$wyT9^$JzfLU?Cuk<#jtG^#?a*#9J#l zaj$CahI!UU%!n`0)@RO$uhZgFsSY31%o#C=AI6OMLj7PfM0K?mlYJnlnL{BEAK#1^ zl+CFvh{+kTT`hFXBhopT#zIeJam=t~?5{U!#k|;Q64x{Wl;*@1#FtaXX;*WCf# zAbaF0qut%Z^3|c;C7Y04i?>3yRuR<9RtUuH30d$q_W2m7n)5Lblk+jVTIc~UXS9Vk zu-HPD*#ln5^3{ROwUUfUH){Jg$D|-8$E0?((Cj#ypodrpU>lR(&GESlR%r%cObR_d zF&w>K8=g5Fy+e!1;V7t?!%+~+lrwF3$GkwZ3GWDQ*W%Nt5eTT69RY~3;pjl#n-N2H zaWO;OfM~ORuKBwlmYE-Dc+RdC`sL@Ou`mruzs_N(?M0daJtP%r@^KVw!6^VSIV-TM zIYGgXcqtgndt#l;mj|`Qaj+HV0K}^)H?XTY!NI@ra*$|dwnH-@l6)iU1Ry2{2zE6m z82AGV19E2eG5KVs=JyLMUmcE1lg-Q?rwz}H57`C?pk{WQAU>X%8K{~wGZ2$AGrL;o z0RzkUY+;#vcGvSII`n`fGqdg5{>_;gh{=Z6t`?ddCjqDP5-{Lba-KC) z3I_is1t2Dq$n0uPP;em&1#FY!k8^Z-3ns^-Tf_$4WD7QT=?1`@A>XaV zAI5@>hj+*oEhZOiK+WusL5$6ihxFyzR`-OTFqdsWd_FZfu&agcd4qHjra@YU#XZ9= zY8b9vPs+lD#smRSezb*!7&RwYcq}gqiKb`(^(LHwn=K5)s5!xa&%%J5qFu%4{aaYR zI`sZzQ?yyUN3iu!pl0?6AU>Wc8mO96G!T z0f-;Q;#m{#2%f9O7f>S*P%}FM5Mxt=F+DZN6|$9zZ3*qLe!BVVAeOg_pi5|Wwa`0X zBTa^Bdhiwwb8Ro74FUz0HMAo*2OyRVqa4`PoZ#TSyc~$l#0}p``@rdE6TElW+z=EDyUw8L+E4 z!N7M}8PEsa#i9QcjlQ{l3}UhYva30P{=cxI-%1o70w~3;7LgTv5R>B|yP6XW{Dzl- zq_OI!;%)$9k_5Y&6D0hJmxRQz>icmPKumVVb~Pth7+KD-lrJNtFO~c=O@#hGUfW9+ zDSf^+JTsZaHem-fa|#RM(*stse04ig znjoDSB0*-P6vSkt)UFnq9VY>oun@o&DSd#W#a$37?b)(vb9|I^PB*$nlr&RatmWj~ z1N6)=DTrmFgSNV3-ed4$ychVK7N14UL_y8$1wf32Nk>F_lgu))r#StEezy7BAeJ}k zpp#>}TIiTJGWG|%SsXKL8OH(|5(MgAiW33i35p22niE9a#Y@Dn-fLiYhrh&G05Q1> z$gbuD3ng9_M&!M3iCU!xa2i0oj&cUOniDh};-z6Mpdmr4^fH_X5RaG+W}Xj#R1IZgwJ$@N~lniDiUmzRdIfCR5zyvK4*#Vc?kK)jx6nCxm!5b*#n z5u+gy)}+%{;yi$uO#QX1Il;plczIZMcQadQ2wxEMhgx;ky2SY^oD2}3LpgU3z=?drgXQwsT|Yu0vfN$k}udD(Lw(d?VvNuAu-{4s{I8$vxf$8d!*Vz` zhn$2J;uM`=z-MKk)hCyD;$0v!3M3dC7D737?owDGPSFVx?&c+7Kxe}wq`^oP4pF9Z z!@>%2icZjQFE0&Ca@CocVq-=eg6fs4;iMk#7(!InMdxgxd?PVv2rI-XIzh*?ch|P8XJ*At9x^9E_h>BjWG(U@fruV>VPgbWG*JC2;@{ccOpe6sQ4}~70JU(nM(*R0y*gxK?T#F0=ZOl zf{UN>a*<^5C$o=WA&^tcKZz8bVBtSmS+EaVZwCgmt)ba!z2Vo2)mnUzDNhg)$f+}|GC>8Ch(InCop6P1yhIEfEGJk8 zDiAaT@)HG`*pUsHpn^$5AeV|x(6F0@2JTqjr+CNf$xjStFqn07TMh?wCeBKcVrZ^UT;G3gI>H795|%1gtsoK^84 zP6UWaf3T}LLB!K|iAWsopL`Qe1Bgj~u&X&i!wYz6NW2aCuW%MXOrG;-S95}eS1_}X z^f;-%!C3$?$%0+Y2^QYO%tE0$>m}K1^LCsC5R)v})tq4AAzl{bO{(%xuh`(ht2`~W_AH}HvF_oh=X;*WCilG&}NfB`T zX}mZ5ouD#R&*=`n3&xtPP|7Lc&G66^PwBS3(eXdK8%0)h&0 zio66|>jD9#f3F9T0;fq_!@240DDwqrea;eD6 zz&kh@klEkgA6c}hDKc>j>sw>_Vm((Y&J-)zhS(QT*ibmPuv-4y7G+SCQwS1w!wp zX62~3bLG}z-mh+}mi?m>1uZ!wCEk=0spwUGBA?B9hqC^JDo#XRbmQ#Nxx#Aw7He9S zZa;Z#>&oMP?PlS!{d4l1hgNSTJLAu_25;NBS*%%IlAmk!9xPV!bCaX3zFM|2?a#?t zef37R)=>Z1TP{}oxyj+yP_7^@C&-D-EOUaLR*x^VMBUOC1uK(FN`Ei^xun@_jo1CM zpKBDWmEFPbl|C>27}g!ub%n2d2@#9;nRqGGEzTbmZAs(nU2 zXsq;E(8YdQtSewYEyLPhK0dXt3$^`o!i+yD{)68Jzb)V5|1}$w-mjVLKX^DXmnY#| z%50iAE@RXjK@<7<8!6e@+~+92xg~} zsH+Ef%|08%WLIxjbBvV@uCQY7>QBVG6%dnMyQN+w*PBtchy63z*T$*$h6<`^f} zFmvKSS3k}R!Gf;7Cv)=_=;izHUVb&+%U`F(^7sRE913dv#^3hCMcRC$HHy(BRQw@g!G3hXNwa{UTPMAsE6*F0E3H@BCZNTIh#AI`7S96Sz zCovNumrC4d!45Jb5w>9MgR)hNL9|^L<87C1`~hmFU4vL2l6}l=7wC#}8zVyq5X%kG zXe(-03tgqGeFbU*_BuVk)4%_(F{2ZJZ5T8u7=5{s5i1`{bF%Cp0 zAHxd)5}n**iB4XxjniC%yi1G8rX1AF=p=|AMs#wMez3W?xl4=XF>dJ42h_~j3W(d$ z$#%4~x0Y@A@s11HW|R@c@`|yzcZpF89rx$bUf6?^zvH#q_PTlWw7<9!GH`IVF+v9M zqb$_Ks5wT>2`hP*tK|s!M@|^&FI_RxWKow}@OI4Bu7aA`j)C~NqAs9oMqNNmMqTV` zp{p#BM#|obtzveSa0-T;{L4wGgOlTjDDnq!=tR z+tnN+LP*4ZE6SoaC7~aUc}-PrUQB z1);FM8#e9SvIj;iNYXjcRCWS=9|dF^rmDLt9pmiS~Ir{v9A zOvWcc&5Tci_+i8+ef?lFKKVi|-b0xPsF@Q!5M%MlvBP5bY4vb;(tKPrY+66v{B;n^ zlWbADwX22A>eHp+u$%7-c+IyRs2rAe08EK(f%5jcIA#hsGa!~ns-n!;)f{8y6|OLo ze0*5MsR1!bja|(#YTo1uHGK!NmAqvUyM&VhVtE=g>MwRR$4Gg|8B!|V)Rbib`>8l7 zASQ!jb~VRH`KT+ToRBNTcPr1p83D089}RUAyP9K+e8Ck)k_73ViIW0i(oO7Yj*;>$ zW>RDf`Cj=B?BmgAG9wYjkVopX;$)dxRUD@^11$%U4L+xh)SSOOpv7ds64cBY28iV} zOdDyKUt!C43N*UA4#{EW zxD3SPnu}e{F=F1pOpF6ruTSEQr7uXZ9NQAPQAFpe5eHSiRkt5z_UkvbSl&I2&T>J` zOtl0tvNYIAcu?iv=m(oim7mw*OR4t(H8ao;Vl35C-0#uwEsg8j^;6AX1u;1mv8#nX z`~m4b>`U68V)o&1;ya+vWwg-p4xAPcpG(<@UCl9CzTyfkLn2$us~7LF`O7ZGH=Crl)}TxDv-e*GwD(F&U||tA(y|juWnOi7N)uCl5jn z8dBmaif`bZJ&4K9-mc~tDc3QR;!bCO9y1bQ`!yc9PS;|m5dSix$si_Y3U;;7PTlBT zb}+N!Ks5Ow-qGlSXmYQ#)A;-h{>zC>}p`0?8Co1c+Jw}#J)Eu~*BwoG7ufDZscOy1;US967t zd);EB)vp~TzC!_cM~tunh{+?P>}syC@+`MlDgDI~i=X_X_LC(BBOoRtPIfg{7@2c} z5jj!%{)oO;y0CuUs(E2U;oQP%F?%SSFaPht$wh^WCKoje8+EBqm70Z%ORtt60wbo` zsbbl`dSYT?rqYxvUrJDEafzZ3DSvlIwR&Fik{pz-A**`i_ z(2_$^;!P>hj8G}pC-T{xcPQ&msNzI$1?9%sqjQDT`UTdsid}r@+SVhE=VhCP%l6L& zfJ@}R+D86_N@cFFp)@|!Ml+i-n(~P>!n>ZRZ;R(On_;l2+s64( zj?Wd{>pSFU%eF1dpll0&TIF6L}X9%Xlp64tBNB9eyS`f?-^@?c=u`_R%(sL=6%^xfW;O zEDHlMYECe)cy(915AFu(7aRt+ph4=ZSF#7i^-!uGBpRV*cq6n@i{+UHsN*l!e>WQ; z5X&%=(Fo}fZ(NTIy2FM8fLO*7CNp3>rZ!lTD0HU|#r7cS`;E`>eqoVXYQlN4*cIi6p1U z`+!)+boKj$t)iIgXDbyE0E}-K;RnQBh}iG0v%F4>TIiSCq_HsdOt)|tYEvcvUdQ;= zH?e$mXnE~qKQ$yG{9qmK2R6S1HPa73ER+4BeqbM)fvPz@1hLHZLwhE>TId0HG1@|b z#TK|d;PEV99eO~L5h*nI=7SpCl+{m z%T=)@A%1x$bNAwKedp=&-JyRjc42)-ydA9Ws3)&!9Y}L^CwSW0)`iK|c0ggl%1-}| zOLjCI_Ui6l(kRI+J>zp=_g3 zD;{XdTfNl_jm5r@7+Ye$e3|8iV$}2t32%9|crzZ8f3tkDt0u3ax{9j)!+vqPAa0#V zz|$2tPund##i%*J(|fzgQ|Te`wB|hY4<6P_vI)?i#L<`cT*bLuCZ?tqZazWKd@QEcNi=pFa*7qGy(Ew${-`~>uUK+PQYfEb%757cv6ksUf6KmE$+=bFC@ zVwu*2zK&fj^vIRcFIa}dXE1nVyF;q^`KC&=jVE9yP5_AI(iuvCUCjXkHnI|Mk6){L zGk&%bPrxpm01&UG2(YU;K)}_U1PtcN#aVBrXu}n3+zl{_BL?g0W zaP&b;w*PiD2hiWmfWDl9jmzh<^mwZnzBughlEr-~SB^ zH3K*xCgVPKwb1lEHbW>f8iGCU0}z@491xRnAG?|Z$Tt{~7u&w%@kenXw>$9m9>g+- z80~HCY7RhuA1Cy~HGj$%o8rV#e(?<4r5OiPJ^z(S*17dlfJ&L?t%>m?J$BF#7oaTCA3lz^n z3AcX`lMyJpngcAnjgy5j#X`^u#glLlCjrE(C|9tnIY7b(I7t|3yMd)6p2b@u5R;1$ zb~Ohm_%tU4!(QM676$Id82~XEo42buz`$2I8CVf^1$9pxFJ00nKE$_3b({~x`18H0XlxcNk^Yt7qcXf9>T!~vD~+X4pi-G4uJoA zPVoDEuTg7S(0?M1K8VLD^zCX6px=9H_p=1T2yLG)21k}Y`Cc4+5R;~FS91XTrJUgR z*NfAYY&pIYeiDv8h)L78t2u!FshsHdRjS3hrQNv?2Oq?w>D$#D0RLQ0@O$d{TD;vq z8HXOkWLVm+<^c4UF+k6|44LH|VkFCZ+M^B5%mW3nyaf{-;n~$fgX3ET-ppWd;TU5m zVcyeToB$A$%OG|&2ME~3NkQ~KWuhxCWxQM@E(Q?A^B4o|69O+V;$p z^dD<6X?vh%rlf;d-c7A-PaSJ{EPJ>AT0huK$@;VwlRW{bnJHNy#!}M94~xT$tB1X^ zUzu(cEP;r>(T_KO9>f#W2+gh*dg!yHpRnZV%`tdrI6@ncM>}{^mbBcr;qZfaBL%-* z%>npd%?bZduAH5j^{km%Z^sD$F}VO>S95@Xw{Q|LJd>UFk{nI&4x9lHlML9^9AMzR ztPGT^$ryMi&H#u>2JC7MFz|6s26|?zhvOr1@4}%6u^ern0TR2K1JHko6Z%BclZSBh zK}>GTu&X(M{&zXi9~B9rxq??0X9(pj%K`7kIRG(vq>)|C0S*}ndY;4Z#qC+oRJA4!L=rbnD1#%8G5f4GICCm& zo0@}~IadYoaqUh5U2}I5h{@eacD2y-7q8_qgc14VZk=CdoQq}Oodgh?yOTgn?oP6+ zIe`4hjL5^8lCcf`xA6ua#PYHQbku2Aa{&7@7_l$iH72fmjO%K@qmehaC4rbsez2=K zfc!-a$n$o!Jq%wQcC{n*##F&8W^!GDB&y<4ll4hhoKOD&Ag`=2*CQ=Mt z^a1H8EYaB4Gq`9tMLd>FpiCdZL?EYfff6Y?z{J}*nMgEK%=8l!1ac}LD3PKA6nv1A zf`lXG%m6_`Ag9*D5-B=B!e=;1NSYOt86;Q;ixl3l4fIpB^q1!}?4miOtIpI$-anFnp3(>rq(7@ZlsvxO5wMbqelA3UDSx$sAg)VWYj<<5l_;uOP{ z=n83wECz8sg9`+KJ8Sk_2%?cb7s{#pxv)Z{-;qhfh39!;Qs_h{H=Msp_QLbpugURew?BM=s%qi{Z@a1JGC=c5$%5j@45Id$f1Ca7R?5XhyX170xZ00*|< zP&Az(kR$m+t=_5Z!T4k6!k;x0fr6i< z?#NA~7&^foHj}uE(?Pq5}*ZVrIbZ1GfP3rW_caIqzC`?+*-!XDASZnw zs9^d)Aa}$EKFi9$kpwe?yMO^RK`W4xnO#AJI7J71;Ooo`*ju3hNHf1Zkdx7gphBFY z0}T9#fdSr`!3!9^IGh=bPx}?WCL_ZMW(7I$elu)s4W$Q)X8Rk++et0QG%Eqqp)dTq&21b<+O!?VHvu0VjED@d% z$jQv_pn~ZMfjpA=&BwzHj69Tnxj6m~l}wrNf|W&hN3}*I7J7*e=Gy|ywl?U;GFy%OVU)3v}GnI268eY6I6&(3{8)3<+8wHdMp#} z%errKK~|%5wg*=&TGSMwnZo+k;N8t^rBS@cpDS!AoLg8erRU54yKr(*;iAb!&B8`i z5~usBVzY2@>98aUaGRMbmi?PV^;WVo{#}&PavXyCnPS)+K zH?p;c`p@2SvEt87j<$w!1#!bwuHo0_1R1R!UucQCrDq5ZCij<~DgP0NaJI(lepx)C zSgq_1ey{X2@yD?4sIDuLOrM2_#lWjIm@5>^`I=vu+ppfebf2i$8uYwkrP%PixzbbQ z!}?241^vj+pQ3+WMC>9zzfOxciC*zJF=}Ew7*T^?lUb(E6KeC{XQ)`2Z8p4oaRy#! zEcS&&qA2#uaveYJg<{n73kffJfp{}MH+Z>xy6bKr8yRgzV>#j12ddSBUR^de+Lq#} z6FC4Fbyr!ai&1lgx;J&3y3!lO6Ps-^Mnf`^y3sW_7dtFm#HcyK#Y0S7$Zq&a^4V*_X-e`eS=-i5jy^tn~wJUK_`{Rk#QYP28d-!IqDmBHAl!8T;JuXAIu;4$E+Xc zHp5|m=768~+=n;`ek>>@x+@?ckUgon>D@erQG+Wp+y z`+3%1M3B8Hp4?7gJ9)QXFXo%svXNG++vh$!BlPjQT@dH9Mgm)Z)Z#5vHwkKH;w*?y z6>8cZ+i|vpDYH}h!R8Xt6SeqK>U}`X3=4ty;iSwK^pnkB2C@87bWCSg3mx_!rOU7_ z68(b7VZ#CTNJXuZ$TV0>?5~JZ0b-I0yP6|Z{GOYNW0~uea56wlE}q)e93i9k44!;~ z&@YZ*u5%FQ0>tt>YqX8Bt2x5OQf@Am1>0?EUcOeH6+`a$Sx*@!1jJ`kgxJ*_A>>ph zLS#JZo2=dCkGb3EWC`MbrVY>>MtwqyZ=ehi)Xc#Sh>t5l9CXbDaS)S9fOfUe70#2! z$Tog@1d}VY`-zGdwoaA-6o6_bJc0NUi&ey^Il{%Gxw){fRJ;uD89+>~RM^!Vp;BC z4bNbwA<^9WjhY5=YFQ*=4Pu%3jCzAz%@G=2#7)CcC0UT;L7WH>lih?}%@HDA#Y}`g z^ek?1{Vg~HAeKj-p{`(8bA*AvVrHN<6wa*UXA^+v5~EE5G1)`g)m$Lr#fuj$UL^k~ z&KY_)HzR!_7tkxlv+*|EEkI1>h}+d%U_+4bF>Vr;8QXvp5^|#99soGGdjkF*qvi-9 zU*sl4Ot#@5A$4A9GXFRmr{i*}+WsM5+{N@#s!FoQFDZd zpKuc)dJ8`x2{+&*+-UKJ7&S*o_%$~Py))VR!T28gA-u;1F&T}ut2x5KAGtZ`lO2R* zb?9T71at8Z#N=$kuI2~{!y9-qI`|i@FW@%6;i;N9_;kJv65D=1rESn$xBiY6UraT| zpk|sbh}$Pbw9^rjI0UGh>z*LK(DE`dYN11{lqSixbbSVsLxfA$_T|zq<82a%$>mbJ znj;i!bbx}{_`dloI0YakDX^}rmXP-G&3 zf3~oS>C3}m;i#G|M9<|R<_q7`R%p%_{;0*|_9aj==L;Z~yHd3kw&x4UQUu@!))=uQ z5R=mvyISZJha@>{GlnN~n}&VH@Jmg-NiB%U;i_HD5eoj?0SYWLhJV8;05M5{UCj{+ z{*s%5B*BY+$4LM&Ia0N&IYPqgxk(TQG|gBds=vZH05N$$lU>ab4&KhqLGr2duW>3s zOirEcYK~CxL1rrKQ|I4m2F$55h{*=YuI2~>pXO%Zc5xa&>{%?o!x;cE$$(wW5eB}( z&A{%lWDJ;jVt_EY&Ix~yQFDZW?=v%yD17u^xEp|&j9l2&93kOn+$0RBg+5yp5AZaz zyFg4HmS$ITgofX7(~u-p@_%q&05Rzcb~Q&xIN{9yzpnWo;WoeIWAmCeLi0>45R=){ zcD2w5dCv@3#$<%;fNGc?YQ5U9j&lKG^4LVXnj>7S<>sQj5z~I2Xagq$#AFv~S964n z^SH?v4)=FhA~}a}CO}N?PqwQ$!o=m=OdRV8J8&SJ*=KwC>cv0R@kY$%*tSS<$lh4bbAT{yX@aM9$VW?`c)-Kk2=!o{VU`~ql8DpSR>fAz$~ z#7w0zk)POHotZkiZ~H{9S#OB!?26nW5D2}Snw6u?Y~|Ks-mh+}mi?m>1uZ!wCEk=0 z%?OopeIlRDdE(V3DwT=w4L8mnofBIX)#KN+rn~mYwXI8z{Y9IF%l6O7T#r_7B|GEK zwNBb}px&rubB!(H5|&!=fB?N(nQQeP6dSwdCi_}_wQObDpOYGW^+vYVQ2*ImE>`@x z$==pbu8@`5er-;W((3VrmZ)2LiXdZhsPt6%&xU5RHD342eop@O?%?-I_lZA-b$fMP zam?=hh*(SjT7$Vlv7E2@mAU=u3rbHA66>YuJq2fs! z#7S!#6;2wcnWTYOu0*>++RM65TIr?YkUoQMbU6Y zT0Px{^xoqDl($A`zYY2wy6@jVOL zv~ew#5fk)%K+SXy5X*Hr{XQX1eTTB;X5gx0I5wcY(!d79^0?}_cZpF8UG?qKZ1_6& z4=`D6Q(mU|An&)olk3|<6HL^{FU9>~nZ-Xr&GZKlNBVeX9zMs;LxN8GM4Sa%D+y{Q z3m}ek+RQ9`gPnyW4KnlsB4!xrtUxSxiJ}d%T`g>onQ8bj7Y&?ar@vzQ=mle^Q@0ee z)7470d}Fp*uNSkG?e`g;6%*wdU(A%n0*o5i5rZlC4RcfkVtL&JijZ9`^xglKcEmTB>OHI5 zlU11agaeAPyyRh~SP?OgVlELK8;qtD;J(5_M~s>)bS&kfL&p4m#cPWH!_=a-r=f|) zwrAjt)R|f=TN|`V05!9b0&#n68~BA@h=^;OHs~jszX;+X>P75op);(N*2vd3ox|k} za-zrV4C}bQJ+Qyl2?xvZF~PHOM|h+b%OpP35kSo}TM!QmHEn<4i}Y0+1rgGY?WaL| znb9$Tc#Y*JKV-@60i?1u7d_RSK}<4MKwxxHCI^3v9mB( zu1+@&S7*i1qj5}JtKHCaGZ2$wHoKZDOjNm;aA4lBlk3~V?jb=e_6EFvu!UMc%`{vP zM`E$e1J*xdXCZM=XO-r{49J35E(4+iSG!u+8ZdM5Y<4bsXZ&m>zD-_@vjAdp`e0Xc zg@u=}v(PVk$3t1m)CmAR#Tcc4Sf(_i-e6aAg@@O&^KdL7&Qo!>05O?LU{`a6j<>SY z(Nk~a)m$OrlUyWluABavDayZK-E^o@ zt<7Z1UKr*a#q+7QX)`nzNgt=hGF=?)A3@Dba|N+H^+KCrJF?R!$4r;RH%y>DZgkcl zmgmXDy-bW+=o4R&M#&fR|2~&bgp;`eJ!ncs#0s1UGRqE#icxcgh@Y_&q5R>}ME-CZ zP6L@t4m8B5xkAJ5*l7TNNJhk2I1v|G8m1UESBN;_Z0}sy?FwV|HPxU}4zE#?(Nid^!AeO5-C<%5oS4ddRMFQu@^=nK`aWHZ5&3Z@!Y+k|zLH z+aD$1UmXeeLG}fEAWP#zZ5;C{;|LF>H(ly2m`${LBI#r=B=9+_zKsHqhq9Bdv9U=Fs6j>c~n zT|#ab%?S7>Z!hhX|A-7%bi3%J_@kULsk+N`T`}g^g^0zNW8rquZK7hUuUMIFHs(q< z$tR7Kwu5ryw@<`>8^khg?v~?I+b7i2ZqcpcKY~V)!P*`xAMd`ksU%6oT7R|Ku+nxC zP8*2jN%tsib~RUMo9a4krJQ(Vv(3ZkVZS(CXw<#gY@-nGL@RJMKrGi3P&VvpuCP&K zXJbemO5ioJ)A3ZCj8g&PwG^uxZ3w3nX!o@0_3lNh{lwHjgE?&#d z#lRuIcA#3f5V0C30>tu^1Jo((YOWCRR(2wWru~X9x(OLpjq4{a!KnbTT+KtNu&cR3 z#rxT*=oir<>%8I`oCgrg@HWbWUCk99KFQ9*h@!!hVJ)#aHqI%o#mNBi5{eAFnk!^{ znVSrKy-6Gg+cXDasUZ@30kPb7f;xm<%@q#5$IU@&P#m;YoD%1%#e2g|I1?b2=}IUQ zb~RU+_*Zr&`fDPRT(PuDlQ<6`CL@M+HCK4}EjtfmHD6qlSv=$?h&}DZ=>YKx$}Q|_ zuF$dgoNfnj%}C&|j>vlXqGc4j3+DpF@+2CxiL$G?!o?Un7bC!hI?ywA^t1;j1H>|w z4<*B{<_a0BoFT&=A>WIW0b-I2yP7LxoW(^(*D^ND1yXSg@MD;IN|85kjd*l>t)@3y zo3;#F?$j1&E~0{XkZKO>YG8p~O6f&dxQxm40v2Q%$0b0AOh@o5NzxI#FdgA~`Tkvn zPro6gBdEuxzCb+hk;jmZ(AHX*jsP$xOh;HsrX$=e;Gev$bc_5)oGpf?BkUA^l<5en z?uojt@TA>{7)?jmE-HrU2$S+jW2GIS9Qo~&@ZScpoLuZZKDFD0no38wN&E++wcRNn z@4mIErd6>PrX$4BwgRUO#4;TLrOmG93T?%%(^i@ikJRZ1iU%VdA&!lcaW+6K(-BZM z>}syC(PU?1emX)N6)SNnKrGk#P%7+du2Au0b}Gztgg81*!RY|8Oh-WJu&cR3$Dgy) zFd#wwf*5X(JYC>wS)SJ?PVb~eJ~kT@Pz<2-;^CWoLr*wtL&;q~l1M3O_| zxLAX80b;Vlu&cSk#oO7r7(HC9c=@KtN|3kzl;YSp6=ws)BpY@$SJ?OH zGF1oV!LH^C5C6!{!$`K7FN$07bgE7q8Jlo2KrB;rP%`Xlu8{FdZZdSLP8~R7N^Bc81aLqn{XaLOvZ5SYOe6mdv3R5xMnbLUdmLQ zM@-^OfLNwXp&nsZbA^ee>`a)cI&pOD#OVOBOw~c@u&cR3$EoaeL{fF)xY&hr0b-e| zgK}Y4bA^j@*|~_O>co+;2PXr>GCKq%!>;BE8J9UjhECOqBV#X428c;A>}sx%aXl9q zT~5_$Cl82Joeb01QRId^N<6x~o(!ovaTa)owm>t%0K_s?2W<}QYG8qUxhA`qOfO); zNYzPv#LYcnvd$gy3jn~`ZXC@rG5`1&VaVzQ*0hSLp2ZS<=-SpJ5}wNv(OZzRGqNd{ z6F0-_?QlWf&RG00BK>6E&OyP1xC6c{{}D%Cp?Nz+@kg1rGrz7lXmbV;%b%Ek>pUyT z1@VF+OXmLxlGl|UE8bQ$aCz|`treb^*|lZs4sXY%y<7L}_4aPMagXQCm8QfWC&x?E z;vY##gqlH3qe%^j<;4YNLPXbET8EgaO z*H|ctQFDiq2OXfKN1eixfQajGB5t-25u@e~5$|$`G^CQ4411pap=7cv&6Z`KAaB_lfw(Unmc@a-T^)aYkqxJ>}eK9!Njp~ zK(is%4kP0P5X+Sk)I02I?y&JqJ~qO@7fZz8MQk6wU;-jqhiu+c2SvpXcW1OcniIk! zS}ZqIp!NuA=5QCpvQg3YcpSss3f^9Um>llf)j|*XXUP-iaQ9bihH`wv-6~EAh-EfA zY9w|wcPRN^2Pm-*cV}@TKrAmjM2WDgxkJR@d0bJPwr>on*3&+;x*Mkh#4=eFrNge~ z4jm`CK*zkZ-QvbphjBVUEE7giI_zrh&~Z8+9lT?!e_?B%gRxb(q-ESDka%rtS-7?} zAz6e0NGavDEn`(nJ$g+mm)5FQ30JZfkt;tBG|FXDMA*x=6gC>RqoC|Iu3J z>50iaW=mI#KZvdR*NA^4_z@C%G+`zQAeI58&Bv#9qfl$FS#_DUUE<;0W39A9JWj4t zDJmAWU^7{~U7ug54qCzH2R*emgiuLp?fGbD#IPge_QGV?hhnzelXgZru|$ zUgPo0@PlOIbx=S)DHdwwKmF7)y!fM9hM!kgPVEt~{0VFsK7BW6gC(*o3gXq$b%S}2 zI}SF*%C2}rJv9*j(dwx+#a6y4@t>1o0a5&;me}E6MbU_+$b@~37OQw`jGBz7Aip*s zTDiKA@1N5V74e^~zPeZ*k>N&B5N&7N%gAse@~blYv2osj;=fvh3Vv@YFb9Hx6`FUGR}jmEVUz*8nhOj(pM!yLBGToi>In=V zD<>b)>|`sWtgdgmR&CD4JN-J`1R|Avd>Gxmd0{oCv8rS5Wj-wA^nN*6R zZ&zbQ|ILi(hr?C*AD?aC<2PI=P8GeWYS}+(8M$WG^n^p#_sA~+6He>SzxIp4pjACI zg^T@+2gP3O7p=Qspn6KN((r3#KPyfHZXB+9b^q?B$m|wp#|QIn%SCj{$)W3~1mu&a zl|C*15j`H7LHY^tN7>4$y32K4(P@4L5sL#`B12a-B>IS`*gD~exb6F6@-btjzXMI| zhu7jie35X@&mEuI-wQQ$?AwRMe{|ivz9Jv$mU*cz3rG=HPA4E`0w?9s7E)r=cuDzT zmq;mnPdrEuOcmaXZveQ*aUVDdcPP20Z0(3~h7Z z?=fn;c>kUW@5MJIGVc)_ZaLwKgF8lz7w+EkyVJxkXiYXJf_of?TOLRe2X~AbFWgI+ z;67nz&{-0#$qLoImVlS_zKIL#HfWHaG5m5ZcoV{)m;w=Eu**W zSAr2EF-rw#uf!4GWI;SejTiC9Fd`oGs_}?##u49PK|Dr{7x7zp5HI~)w1v8LBStan z@m>|gWUp#h{W@FWyh(!P_22KMQZZK`c^BaJ=nmym&v44R6cv zyHlc9Fo+^!b~`z1K+AeOd|bSqeN@6OE+Y3DM`tbV%p-8*L3|En@OCv`&z}zp#kq7a53VFL4FY>=gNM8W#jK@Qy#hxa z#HUfH+tqkc@4bL$&3q`^XjIfDr}$>(kvQximiy|_X2!0@3;R+g*avF9+$?D6P_M*6 z2eCX~0tMZ!#tZtXOrWowzrLxjF*#JMH^tFJwW@7wa}`bnh#x^wVOQg&;#?*w`ttVS zIIiq#bJLP zjy;IUc!6Dw7yBJd*bmAOjh8Q4=1*I3)Ilr*Z)hKDSK~$fb|%!vWw-z$lUD4v;n;(i zw0OH3FZOvR?8kz+YdL$=uf>n&cjC~4Se_k>n!R0(7y8-mLN9lDJt*hP-Hv^~lWCHE z;(=zdoLA#zv74>tHT)wDFf?L4o>CeQHAslLK3W@?AWU zu@-Om@C#=c3oRf%pE7*AT4?z9N*nC9K|Hg&h99m%CG2GZz6){GFSDQ?qsEK+924qe za>6RdHsX-udfD71^U ztMLN=7AD|_mCei5ZcBvtnK_JS9 z=#c1e3Eca8Oc5heX#r{j(#hz_Q|D^7IXE=fztBc;uTm>Qq z^M9e;+Z@b4pvB~pC#ac&c@WFVrZ%zgodzrZ;RK6P@B^nB77yYxDT}wOg%=&~bTJn{i+wq6_^+@SKB$?74`P{| z5n38c7~#K|7+8@n){m8fSK$=AT8qhyPf#-{0I^&{<)xtKLY{^H0TI4AoUMr~#?+bw z(cr%p1>K%!3wkE%AeLM0HR|o~O`lqjjgQ^DPQQ)$t00y^A#{XeR|`#GOtR7K_Wl$m z6X?xVD*^vR@Zj}0;vim4A#PXWMf^-A#QP74DU=QKH{h6qn9S(2tMOufF%#yH;gqch zTM7tMes4vxdlj6FzOT4-`rqi&)U@Oe1B~Jl z-^ivy5KmCHZ&%|*|9&R)m&~*M>1-nz_?O_|UvB|FMvWKz=QDxdZg?(D((t?;Z+Jlb zf606AI61HCZd_$|rPc1L%T1QtvD~!oHttm}tFevb4)){G&a*o+nw?oqk+jA%hfs$S zvj*c(g9!;GFL8j-YY2gt9y&24fC(=(5JHFd-shh4%+B3=?s-50@PLYT3j#3N@bENQ z@!uvSey8C%BTK{cu)E;_m~41>nymUC3_*Rq;Q<-#h6iA>;o)hr>VHy5{Z7MkMuvt5 zRJ0o&fXRl3r^$-{6(RB0m}IeD2hTLudXH&&hOGf7z+}tA(`4oUZV>YGP0xGX@&ioD z&(mb(|Cx~dtLH{PKj?X%TYZ4ZMH8MTtN!nW)SuJ#WSVZh-z`7DWbpGeS^1ZrBC|N( zEkED(e84R~z@+>Mz z-BBMi;@kVt06v&X@$)oU@ox|kf6%P2sC%IN+W`AlU4m+WL4kIv@jr_J%$9BJgmsmWL5uykm@6~dMPsT&R+2J zZ|;Bxn9N7=G+F7tB`JL}nBni%dc-X~z@+p%O;-9J2}yq#d0#Qp=6}QO1Hj}RXFN^T zhhGQh1Jet9)9nMmqz^nz)`z~WGOOYn>H5G_h;O-#0GPZ=ny1Mcu{t;-xN#9EdWtm~ z1(>{yj;F~Qak!8XgP3Jt;{Q7){_Wu?z{gO@d!8mM`xar@xoCgXEjz%Z>^x0Y_A`ZK zU(7`L&yC#naxB2)(|bHkR_;q=kTleH&2t*drU^}_F^V~_Frzf z0Vd_AozvI+xkblv@pt7F*+%6sPvqqy>CL zS>c~8B>aF8-rLCJ))2ZQzLIF)GZAHVe~GB>{&7qypS2{F;?ynCX#pQmR`*v6>7MNs zhTH}s;t{9z8)PEND*q-SL4*y6Qwt@Th_W{PV>mW&_ob(5 zT8G6`oO+ccE#M={+VGKZY#3+mqstvk)CR=K+JFV@;Sb`PC~L!KglsqnCRz(>l@23( zK%9D7Ifn&o9}w3>Ss%V0kPmFLgt8skJqb%?KARerW+BS@@cn>%VB4jQM14S<+5J#NqwQ7@<|K$h_d=09Dx38%YQTx|A>?Ej|J@bM_fMskCD6*HG@h1*n8>4IyLua9>9cOj>ha#V-e9ifzSJ!`!0U7XI=rq^D=@NO=9V2`YD722(`048TT1q7Ybq+n`2zFU%jIs_0VZYVX|l3E zOGx&iVyRGwtC4<%+Pk@6>Xcb-|iM3V6y4qX|lqkcq^6DUuU)&1u}x(~y0QHIgZZEocOrgG(DJWW>lKMED=xXA7HY% z;c2qsuQ*j^m%!RC@iX+bPju@KFnL~wr^)JnPzd^G8SC8X)*oO}f1W0*|ItGF<5i&< z2RnJU`T$e=hR1lCtomDp)E}P8?~js$Q|ujE<8I{vCOci8Cae5;LduWmBxY17O+}Sb zgLzT8;Fca>a?y~d$x6RXNcuTxwbT7mt4X);0F!Aoo+c~&4k6*^4)Gb~i*DrsCR-bx zCae6NLduWKJ+kKCiZ`~~Ej_?wYs1rIr7sIfzY+%cW+M`#eZ{Rkz#FMFGEbA$ey@=B zBgxRlD9;?pADePZ4=@?_JWW>mdxfN5+tuP^nAg_a`U6bPt9Y8M{x1~Le_eO8lfi(x z+W>&cB`lsMYrum-2CSLe_GFO1<(3~{at(#2$;$uNLh^TQ|K%4^_Pf;wn9M8iG+Ff@ z7E-@^|2r@Lj9Y$y$>8T{vhu%QNd7r9+%x<$+8BrT{Ve?So?=%%Zy$;${`b#&q?|6)B$ENZlI#@f=I+*QZO|&x+W%a+M zxBBZt{lC+5on8;+273EF+35xn?Z`x2UjS|bEFIdH zKef{TI7bf22U){P#E+wz8!X@>rr}=Dj`TVVszPDVr(ZAdk0?>R;|bNtMm`HsR`t7u zR3DtE$9ozPdutaecpf4B6c_M5qOA1K5|Vy}N(8kkdLHF}C?$6X(YGQ_-Wr7k?4}QK zO_bICJ|XP~8!G+QjHViSe~WV`AwA-2sIbQZw)BW=qOA0<5|e&BUx~9zzs_oLP(kYa zOAZV8h_ceZQAqmbllkTXl;2D!Pu`G;%KM12%D+QMd2DZ(wVO0{w-C~w#OxsZ}yVWET6Vknl^nQ@y!7Av)qiRJ3COTXe)VQ8wCtE+cyRVs-C( z`NL{~aLj!dWNsIsIpWmwo|%ZUn*TvabMF1xRJtLnRY%RK2R<_qWi?-Zy2zq;`R1%~ zzLL#-y$!N- zy)Wz5t!oMC5hs_Fv4AZ-;+iNc{Wc-#GcP%8C$vYL+C`kh0=D*ucQ$uQEpzXX(!Nrv z#`!vX?Ne?Cp*`ZH_E^By9&vf??-bI0nbDp}hwLPTN1VF8K8FQt;StwF*#@U9E4=>- zirkHa@Q9PbV*y)u#N~zGD@O6OeIUcl#kpGu-4S1>TIv4{CtSKtx?llYcf>VOwt0V0O7{%c_~mXT zbVr=j9ShjHBQCG|Ukm9zsLp8B>-p*=vnv|UJ=qE(PEMh)fR88}@(=e``raQ0{6kOA zJI+52IPWYYWaEhAfcv19+s6S9U22XAZXjCM`xh@-yh#5iho4}d3q-s#eMvX!wlkg! z+z&s_-aPzAz)vTBTy6#m*r`IqUFQOalLMZS+Ri#rn)IhIpG=3)sI4@vedI zzv!yc>utpU+S~e>W|y)Y(z%C_9r2T>aKHk#?1*b(+ANC5{!Jm-Gab@-3ZXmVIDr{dGn)u0;F+se1|S5hrJT zSisgE@vd2)i1tg*5LpgyuWHOx$JM<6(R990P?s0@``Uj21BR`&am1-BA9Gm1N0e>! zSM}Bay^i;g96US>ca3IrhW?K4KdF zXG(8vdzH)KBBDXwcd&@*5=&a z+rKkyb2jd3l`4fOc`rs?N)efh)%TVft$YP0m&8baJ<;x*4fusrM~6ha-9h{q$5;oR zn24u#MQjg@pL3$s10zn>1}xwsrnTW7i6-dTLQ%5H1cXlT7qT1hc)|d36pRM=h_VK} zM96@lrb@S|8F^K`>EzG(91PXZu3dF$`xHylD}k2sm| z!~(YVh<7DCMOvOW32DE)80QQ6B{P52A4({XIC(QG7O<5^ytCS=G9V(#|D%xd3wT2= zcNif*;?zobCZcSk^AREW@s^?*)8=d@q(_{*t_2I&k&k%SbuA*1{~1~78zttFB2>!0 zbp>(qx)v`TwL65CE^}&a-4$&?6^m~Ym1#o+<#wMd^Yk?s}0r& zA93nEryLgW5oII)7rhlgp*`Z{ycY}D+9TdI?-kMh7$NO5&U-H; zlt-ML_hJECdBnTsy&}qw3Ms#kdGAGp{D_nDUMyhCkGLkvw)p1@$)9!Jdl?};;^e#+ z3)s>l-Zk$PiTtZ%rDsR~sFXc@MVy@XVgVmfHtKiwR(i3~|B~MRo$2U*q@flu^37&7 z(wm6=3yfn>!|gHt^?;v14X2T4*Kou;jcaF&zq}gnoBNxHTh;I*4!4H*h?5WeU;!U7 z4gI^cJH6gfxLYVB`ghHFc#V*KJ6LY-M@F1%?6H84C@cHE-pVf4;_T|}-*H#E zm%4nr5UV`HlzLIG(JYPo8=fZ+4bM%0Z=o6eB|p*JjG>yCKWLvJEs|5_p4R~p^*O>TU%^CCij#K~)8v4E{V;$7Fq zis=6qA^iu+-h56-cy*LK1x?hmuXNBlS{mx%>@L|O5VlNNuRyRis0 zwA&8E$@U%#_=vLNpC%+ew)Z9GlC*~*?(Nh7;$%(%3;2k#(qAYfecI@ZbK?I4AwJ^d z+!hPi;v+6Ux4lM4{FUiKRwX|Z*ZoPIcMyXe#1EqyeJo(NJBVwdZ1CSCr2k+ddsJ;P z*OI=6kREYzoQVZ&=@FM7XXb^Z&vIZERkDYQh?5soU;!UdHsYs*WM88mftpswsFTC` z8Qy+-KN0ze>$9NF114C&j(o&Bhe)Ls4rin+P{aGl1z7L}uwb<{*hic^d{0%~y ze{UtD`7fb%*_8+JK}vHh;3LXv{;-hdcWz)b|8GKb#7WJufUP;=nkcLJ`(!k4XE{>x zJ3@BE$s;9Lz?L2Ht|KKPP5Q@sE4$b*X12F~Cw5)!;pu!mKh;qC09sW%q(=;6{s>WK z4`U8pYTmOYXX;3_qYUxRFed$31Ih1UQkH)r!X9yQ2Ot)(e-PqbI{?3=o$2*R=G(mu z2eDgkzS7&jGmWoxg?PGIjONbRnPB@fcqn|t8p0t?F3Mp6A2IFPL>|!onXCzO=j@CL z&m>GhoHPLo*d`#}bxv5sgx^bzRN^!LrjThtb-P1Mo+JHY<{w%@-#7PsdfNcWe z@+K@lM`lyTVGa|bB!SLUh-VXCAbvbG$-x4)7l>=3Y^$_h$cs(Xa~sJEp#G}yLc$Kj zPo(U?0=6B9Yoe?jM+(`oUhhyXHA}T>G+nQi<8l2z{Vx0k;R51hRu>D{E+F1@bCXCf zak7vLBOMpmhX(H>AUC*vOr*d8D*AOBAh@?br@d!CQg$W*sP{FAdUBV0h7?1-^|?E>QR z9dS{}h2dID&(yNifd>fn5kHg~jA8*>eZ)0Ub}-r$Qh!}%(Kn+BFDFbuoSZCU0ow$` zDDF8_YS)2MOU3Cl}qZfGs@Yoouz#2&gS2{Gi&*7wN~wna=kWg!G7$ zM`W>pEj{90C&NVsKo1B>KU6Fg3UM{V=<}6?_=q1uwfb1V79Vj zkRNe!@{0v*`4N|&{JvF4{=uorbX3o)nXflF5m3DS4jF{Eq#8x z88v67nf~@Qg!+h+qfIPetB<(+X!Bo$)L*`5_ugn%DbIAbe?=&dIJqv51#IOJ@7h5x z(%pVRNcm-Zc1P7(ojJPjIzo8F$sr~du!Toleu()kA>oHX_zVrsLxlQ>lMN0Qu+>Lg zzQOsCkos`{Q0Li6;>OF@6VfA2_Ow{QmL75Wp7z&5(vQSbyJ9uOuV;w-HxTk8PL46L zfGt1b@?*@tb7dCbS0z_oss~qFD!0ilFusv60C92)Jr=MHK)mz%p;T^RwU7Y^bnob! z2pXEhT)j zG!;jj@c%?aJmO@24GY*2kGOn(?H(!NVSMRtYCcUwJmO@;V*xwj5tnajo+Bjua3cJ4 ztr4lN*027*66zyP-V1^SZ1oZEx)($wpFb<4{>TCjdVP^-br2_KwOGJzbr6@I)xJhb z{skQI`T-$7;-vgoz?L6zdHLTgB>&JvshY2t^F;op=f4T@5hojdEMSX|xO~I^P9gDE z#Z%MGnJD8^@4q7qK%C4hV*%R$#N{)~|0HWb(*N>rF#nz~0CCa)EMOaexV!)WY=()yGW8(Rsgc>-3CL*L?lP_T>M2 z35l=V)IQCptDBXI-R`UAr{dZ6`pLMdH*R&QKHKiwU8)vl$A;Ve^?Y?Qo>lg=`y0)C zy_x*azDlVY&yFo^52_OebwyYArmBc)FO3yTl`a29nK8Dq{9FBxOIoe=%0^rk6*Otj#*W)|5Nb=qpx564po6ekG@u3P^16W7WGu4@+xIZOGU@` z)mQ6gznlc^z|MW8tG3@XTTVi@-yO26O@&xwh3rWNK8Fffpbc0wfQJ>$gluvTps7NU z%CD80Q3Std!2NsHxGU%u1D{L%9-yfUbh_#_!20=A`}ZtOmXm$8d7b%A`!fO7pXsd4 zJT0xwhwJJj*OwgM^V)18wG*N%;1~hx#d^0F0PBOOPA_;`C|;Z)?!_{Fl8-fFgWCvz zbt2no1Wyaah>N9+(Bt-XvJJv!slPq#vzATN>ihC_b%AZtG83J|i6#i`Q5@H40nKjE z0bZ4M68LM%pVD`F`nm|99%OYp0F%uUPfKgVcG-xySt=s*4RFG>;X0|m9pkUPWS@Er z&KG}=GZjJQc)Er?0PBc$w#GayjXz-z;s88QZ^HRJINt36z@!H}Eff#-1mM9Leh*-J zVAlhHNe_5hC>}gT%7Zj(Dv;~iAoNG$Oj?h~9)T&f${G$OZ*;;fc>a80?wBQM!mzFX z3Yg>-AH#%Uvsr~?CZr0JFW%VBKNhJ%>&rH^Px2=TyUWi@4{oT(jbeT}R{7+5w5wjr z7sm6AX4d@RA!L5=6-xWD>&vgy|4NT?^tX+aAAk>VeZG2=&-M8^)nUKpz-SLCT9P50RsTaI-2O&?ahTzjBpkF) zW<;OCpzGZR-N_i_p~)NcwjLT(epr23E7iTdLSNGnO_b`YR+lsAf0tYTX-0n!OE}bjaG%(bmg*{)ej3>GRZ=b{g5CdabE$!i$Re$^-=5uzMJ?!vvgK_2~+@8x4E` z6>vbaM@InbX_g7NbX>&ux4>2JmUaCSfXUG3X=&(xPa7jK{{CstBcc-u1N)K6!0{8?SCh$eQ7E`nMr%Q$p>xMGTM7+^4c#uuXhsx7-z~f`QPt(kj-oI zSLdgvD>LdQc(rF{#%$$J#&|@#e~Y`_e};keMI+9E2GHzwA7H)cXSMt7CHwdIpjuVW{0QA>wmeR{sa4WM{4_szv-#DwFj6SZt^sF z?XMTqe(88kUC8Il?CdeZt9=h$b1Z;|sG#R*^1?q+Qh0TWER*nRS<)rE-a+VWZ9sSr zO=Y!r8V=lI&LJA9dGV=z;=?{BZXL<2%A#JN*c}5 zxW88dtxt4Ya3N!XhbC{qJ9}?|-Wm0PRLH%q_kVAiUmI!YbBua7lRonbAxAVr&vQ3J z&o{6>4d`r!fMz#C0PCCKOvt6bX?Zo?H}~fdTaVxa4!3Fmz{fH_$3shNz(=Jb;H=7s}e`9MwqA(FL2k07cw;hXm*VNSSRh$ z5EOIbn}SYkD3lrtwc^EYE4VQm&}=IJ))zX-Tk%6dE3%FVpKau~Q#=6cYv-MP15cBe z|5t+YFJwdr(*fH8fc5Q6P78RNyah|o|NqGf{DVD@wRzL!k=&KJ>v!B1ZNK!k>u+Oj$z#~-1@iYiIi3#A^p2yle3syz?b`kv)W+5u%n|UyS*aPtvw+SCL za6e@N(CnlF!0Qyvn9#{qtW@)at_QMxDFXPM_15Sd;8U4j<)Njm(c#)Ci3mBq=MmCL z5v*>+d(<`F4R!ZKv@72nFZ#0t?=wMQn+!0yJA$XlyKsh}3(M3kKc(@QS*CoyTYP}Y z{54OL7yn{G@z+h|_eZ80WUK`ra2o-zKGp6V8SylEBgO=cSTWuLADD{pLAU+@lS`L8 zOGDXOu#ES#(!JrtDCy%S2JNBh zrwm-6+5n*09VozhDagcNn!H?9DebE3y{q~O?c|{=-zW(_=NN001hBrX%GomUw6tav zv~Lolq*~7drgKYLPY=pW^!yK zLm==Lx9(gZcxdvv-zTX1&0DkSev(`FlbLw;(ByT0rJU~VL71LJ*!~gC&F4%*XE$U3 zlj$p-CNKZr3d%p+nHOYqV3kz?Kw<9|K3BBci`=k4$PS%XR8Bt-w6tnO+Wm5 z4^7^I4+%Q3SiM2x>v2EtZh8PFleRofUiME3%D$wbE||-p`!cJ+0bK{F1_%DVhbFK4 zR|R!njB6A`PYe1Dxn3?)riQpY9%-yzW1f)4jc9 z_g;T9^F?>_4)6e_J5Q6>{rA1sT_)rC)SmyL^YS_)If4xL^9{ACgkh{=OYHz zJDHqS9ccC(2w;6*i3zzhnNz-)o7Dn$t<~rNtXGzOzr;gJgMaV>p`_+I{lz`H6VBeB zxMUZz%E523He)WE`MSHM2besE&C}!^*xq{w zWQLbRJ^w@J4KLTsSxv9jBDLMnKg9f|bBLMiTWoOhay#JJV@!ZMCxFs1W_#%#Zie+O zcNGA588yh{X=xR>O&cV!a$e|pWTZ=l?bW;E8660PQgw3f3Nn$`0XYY)p+3Om{aZXu z-i2vF7Y4@jtwy?|#jpKFxAw;~HNZoY*Zv+s?FS3>+O&Q;YTBRTffzo)E&th!{2rRT z{Lc}Tf22{HXeP^7O0-)es-;?(>%6EUp-r|LMASoJ!u_Z0vS?Xm~ta)X5Sh?Cl50oydhHBnysD+RUZ z4$0+K5ZWV7YL5kM?GX<{`y1r6Z}-i)^wYh@k{cq_N1S>`GZRrh@}DHAezMXS)zu{i z-ce6(n6LnGD)EzvC~rYg(gJh!Eq5?IH$qr|IQ70=CZfCrO-T#JYt;h3)g-r)umEw= z0xV#61Bh#)yai7ev>?lNliVsof5fSoZziI={%t}17jVmGZZ%;8;?!+cnTYZ>JRoR8 z)>L$E4dDUe)Df3VM0pP$lJuZ+a@{{DT1$9qO*eerJz9zg^(uGbZ4xO? zj*zkt<&F53pb@L5YYliVW+6@-+PzwtIzinolNRt1<(>GEpcBL9=+5qa`TC@P%U13p zqCOx_HcVK+u1JW7(J=j5&;|CGQtm1cf7q%5h?BWHEZ`%`SAo8Z#MbQD?V`DB3GET5 zE_lmCl-GWhoc8PmD7hS=J>uljDHgEf9&t^S*M6g%_T^79=i+lu0PR;>Z4TnpF5OH- zdF>x3s6BTVL@MDrLVL2aMRR;adF@XV)SkN!BGvvNLVI%HkJ|f)^4eb@r+s^||DBOk zdV9c*%2KaSWg^N;f3=|WbF*>&Yawzwh=w0=eKof;8;1q#9s}{to!*jppc^G!n3Ijm z=)z{J8A35n#{iso)2ssiHF)xw#G@+MRSO&D(IliATke!N-3 zht!G9Lov8uXAlr4M?+Y^N0fJAzn}};O}Dusp*`Z%Zs|-!dF`JmsJ(g4Vpl28WZp`I z1&EUqF)Uz*KjNAwZ^4TtEojD5(;0RRO@Re=IstKVX$%Yai1HS^LePRf-5LAW#P$;v z0CDP$z8n^?D*)n}D6jtC$f?g>{hYfOw71>^PH{2=mxU;={XfWQ-(JeDLH-$tZ?EYh zPTfzI!va2{y!apJz4$VRl}_*Z;?lgsN^5te&?gixo1P{*{7Di z2spV>7KsDa#1(OU#@)nTdTOb?JW>Z&OU)>Pj$pw3o1R0|hc^Shm6{VH(e4Nk*Sk*b z-?X$)n)3Hc&x5bEKNWH6ZPpwX@DbDM`9)nzB#u&jtM}Ctk5aV{?5iDREfOyx&3^4=xE*9Gx&^9x4eUqcVBX}WU0pjE?Vk}@=fOyv~VlfL= zOIk2puazU-f)^1MAWm9<1#Alt55t1P1uf_=G-sxn!-X#<yLOC;eV!}{)4&;j`%8YA7KIF)HT*QEMQxJxF*Wa z1}>4bAX%{EPno}jumJJR)cg<&*cKqJiSia~7qnm@hYM#38xSW~O|gJ&1LB$}Z^NyE zHmo%3r87~ME!!_8d_eplsybi++XuuoQQn7wpbsmOPC=bsNYiM<+V$%Q4-h8@Mp(f1 z0P!#eM$^6bAXd4}PfF#)zATvzn5fm&K@EIeg~*Zq9R!@6`e*aDNVEeEaec?T3MEpc&+dKrcT%Ji^>|Ms8ZR;12T)0SQzhcm6CXJ& z;3LXg@DfQ2%(#@j_XU--p6f^zK8Navvk>Jic(t4b{4s{N6CE(()WeE7EMT{Xh-;#} z_HPu_o;}8ZQdw^+rqYrdH?k1rwST*y_T2M|sm9j(iYZPW8AR=UM0xE$D5pLDtX%G$ zM5BW^xiX9e>_!K1O_Ue^lXBwo7o5JE&>nHJ!N&r&_K1hk;D1?A`yI?1#yOPAZtxK& z8+BPt1aI1ua;v_Zvs-uJ6Aj z96+4h@QMX&2N3Vt@G9oO6@m_A-Ea8{!UDv}N`M7y3lI;Z65JqYLEkNE+qZwb_gX@I z#K}GPSin{v@vc4hVin*?g6a?K$*|%3Awqw|$-RD9z}6q}uDyO@`WGej&$8kE4TS!P zllo%;TYtpE(7!3D|8jLCGSWM~{VmX&2nP@+yI?F}JAil?UGUQd9T?2G-~P>n1&EVf zFcz>aKs<~txZQgTWR9kOy5}33<{eF4cX_?mXe{Jx>Q^B6?6avyEio@Uk|{qV+GkS{ z?`*YB_c&!eoBCDwP4>R4Mev)*v#Ch56ETRp&ZhS5VW%lyhu>p=EaK!+3>NSa)4BRz z>3Slu^Xe_Vub}B%y>BZVRRufJWv|&LYiv?^S5bsLgiurJ%pbwdk2LFoi0C93) zgavF55D#NubWQI)&|9}&eu+><@*(}j8{7HOscLhyFnW1yYGUT5YevUgjb?31E$bHI z27=Mmt?EoGU%k0hh-=$wm3U^fXpp@ci6#N5E&zGB|r)7qrHfe53OJUA5mWW-E!K)opDp_nP61%aO;{> z#L4$Puz-&!Z^1q}3-~j^gU}G#?HS_a5iu;_Bg$)kub}qqnP8OAKIw%xxiW?Yd_;Ne zUm&PGdnOnqv`=~=PM*}j0zRU=_7BKu-)0|DSpjX19lnT@&xm3HA5mWWha|NxoaWyz zh7zu_`dY-vrC==JBg$+4RypnYqyGaz1NbAsn+fp|CyxYU0b6{;yN(2l ziT}&qi!Yj2zP0y%FD4S(8TJ|fA6Uh(_wyr8j;*nPkC?W#Vp;vZ%jB~98TJ|fkWe3S zQhh97tB-isCRZ`_R|~42b)WH%2n!G=mr}8SZ2{t4OQ~WO94=@<#uM2;Asj%Q+-Hmh zYzGkU+Gi~0z!pIVvTk$zDPaNPWF^1?wgrfXQ3=izv_PE+=1)Za7ok7m#)eXuJWxBn&{D+-i>nYy%MQ+G;Omz;;0cG98NlHK9M^WCg$iw*H8R zQ2}n1)IZCi_}>xwBTnj%1#JBh4@3Wgp#B*T#s7(L0C92@gavE|5D#M%G%e^r)UTW=WC;2(_3kxREf_U9eoV8CC>eE z$-czH%r|dr@9M?3ZEBzF-8E_dyjJndJ7)C;#dcpcKNZimhi}L?$BS2LiBG#?w%xb8 zR4vSo^|$-$`RZgmt4sDbn)!M&`Ja82QZ=3(8)y%X7xVS}cvC(7qttINjTK9kE&rub zee8_#EA&6qq5byCMqG)Bcb_$!= zDODpCzg6{UjL)fcZl?eyoihLa{-ui+FVg=Rvz?y955Thb_0qEPyVbXuE!&9^gQj^( zZj$ghvEJHs{YL5r!eeKR6Zt(%u zhrFEP^RxiO|Ba;h>9KjA`iHvJ2UusNoa*zm0MzflyoX~|d*CzgoT|@-jcykJ*7+2t z3p_0V7uE>4pz{ka(REKGJMad{THiS%nFJg$X+F^eoIPni!@zp>>kK%c+4CKM^{zD& zaC+{qhyUhyq%ZP;st!4JO~U~uN1Z$^jr`3z{zT$ni$LV(MI--6fxj4It(`ePv)M$s z%0hR=9KdAE@w7DNcN|KfvTnho=P~|K*bM_nGy3UvqnuTYG@XnGR12 zK>O=OwXfFdQ~nt~WTfq82Vhcro)&=iPZZETT{P?M+Rp>MAKj@w{kiH}JL&Cpm)EL= zl3sJULER#6R$n0a`rT_1XPb(zu9EKR^R71V;naW+X!hC-!1`V;6MV^q7zzJO0T?hVX+V}t zC`A7SZt=G<;(KTTi2oW%@iS#YLH(=U>hEOK_s{}R|IL!>FDMgwkr@Ek9X!BfCX}ZI z;KDlvT+o@&qW)fyVMjyYFHVOY*)pM5xCf%#z!+$DoC4g*gmwnT%f|E5dc^MUfZ+q| zd?>)1s8)!lrSbn!v1s_LWHgN6Ahelp#>`CMN_Qg!Fu56prv+fbzX_Ni+X#J3;4e-a zq2+ozWxgJ%t9yKTxl2sM*|{Nr$%x}=X~fBO0zZ`0zh6&$81XN4iw`il1A(UnApWl; z#mC@3-5>mr^s$2dbM8`_8BMhyWi#jr@38 zS_@X|SQMED941f;(k^%@yZIP2Jj|N<156IOd0GI{KTcBm6;t{B(R5rd)e8PD0Hi#^ z?EsnhLd%weO#($9tHm=Iw6r0oIq` zIX7bQv;f4vR#N=Fe0`G9K5}ahFd6tfEdcFrmek%H`k6EMA`Z&kBoKn#i+<{m(WqIqV0Ty&VEz$FM)^ zA=Zk!CfsY_v#6Q?G&>6ouyZ>Era{?LtW)N@*H}X`fXN*rJT0x9`*qxi%t!vL_YvFK zypwe;zUnprVEuN9vvuTY0T}QiNdpeisk!|C_!P<;3Whk$Eu3<8)uw8qoY(EqMjApAr! z5C)o&o)7qQseXS`&%ylSKg%sXz+{?`rv)JXz?BmF10-Ah zM@Iul{4}`gHTm9+?Yka>t6r_|+SESLd(mrm@p;$0F3oMfA?xj~!{qI*+m*~?r`{Y%|~CuN6CS+yi~odZ@I80O#ZdZj8jcVX-*{bd8?tAXG3@vF?os|i@w z$Dd%}t*Ym}!9!CMur3;Wnr^dhRJ1$HZL`Vm={_f%{G<{0Pg-NX+x~2T^_5x9pTyJ5 zPf~T|X7$@7Vm+_FH^f*^7S9r!R>AtEzTDLu>GoN*)@>EQI&0^&il>QNRqdTs<=yJL z%$2T*1;e$Lnli`vW~;6b()jdW=hh!!eSLsaf1W0;|J`!>&s{(EiND@0KER~-JWX8u zXUU1*b)BwH{S9vQ0oD^1XXx`ZarN($Qy=bf_33_)TX%rg4ZuJ2s;RLp^E7eUkCl_XUytROcz>K* zc!0@a98VJ${!}^P`^;V3zINt#x9$LwSwfyBuKNXYx~t}0-Sp#=eT!RmfXQ}-r-{pc zwVdopJHrSM>*aR83NYEu@HBDZZ|u47>HeKwZbP}I*YgxLLOoA?XJ_SoNbN7_9Ij4k zdB1yu#5tyxtHae@gSbl!tS^yv&W(X)?<4_Ox5*~hk~@`>8PxJ9DpgC(D1ugpm_nUv zzSy2ZU1;E2sNVxLdkO`xZmjIzGobF8Pc9qswSmA__dIo}1F*hk-l+~xORHj8R|$c+ zbgSo8F`ZegfNddJ)j!s){uzw=9-6rN&yZ6;(^3fp@L6uPLmP$bQ z%iO|mXN33A#D#ykobU@;DtWx=Y3!Z^U~;L1r-?i8`kp(Wlf`-ctpXifrRRTb+QDT@ z7GLZh>2M=2pxN;W@PQBE&2NNM6JG4U6fskp~e$w zoy$#)x6?Xf1}68D1Iy=oXyTxx#$0oz}U^z}HYMKhW&K4!{ST)&ahq)&ZDI z>+rO+O8!R2jKHY3@2XxekEdgRWx73Nb2^}*oty#qR3;ESG;!%y%SoSMt33#Ox|LV} z_^yZZ*19QyJ!aR)BxxdS?v_A{YT#qWAO0`9D2X3M2r;~rje!(E`+@d9eM&o%UgOL@;J0CZ(FV2n7S6 zV>~U5F{w`Vo}Q~Nl0Ex*ug4DavS-8FO4WR2dwwRapLT^mZI&}Fnw>T)7?|Ai2sArw z2C&|-XQC|~M`XQG=Q{Ve=1v3arzV`y4>WsR1F$P?Hq`ahgs-cG53zG+0F${ho|aa~ z`*g(+7~s6J=M^$-23Jhyo8v`z*C2!Xb{-7WC9hb5fA67*tN*uh>L-`vWV@sYggn(6 zXaalzQz1PxasA&er+?NpPj=5 zn9O1FG;!6JT`jgLp}$cqO)v+l^X?WNVEtr)bCS)|#D!lcDg1ao-K0cZ&I<~zvIaK* zllQXlG;!gN=(+HcUF&~|wW@7%#pi@h@cHw|J543G^T+^`eHu?oV@+ycv$f~3mJVzN z%pFXACRg}t`28V3wdYm?angY*y!g{%myIhyu-A+wV5^WIaCJ+G`qC{SWo>-wOa7P zZQb48{Dpzc3P@{h_H=rCFM;#8GX+Kk(h_mLFjK3c545!_&m&|0_ZHcgJ=0o==AD z3j5sh15Cc@!_&m&|2sMPmrdsj1$73{ANl*;(gS=575O|(T>5v*NuO!4e8#Ojz+}RM zr->{7&vMFVSuDTXEj+;F#d$nUT=>t+3BRDl^5?l70GM1X=V{^&e52q*U*_Mr>!ml6}lIyf1dE4lt=YPZL*t zwWR82_@^z9A+uv0U{ZCSCa(J7Jy%_1rD}h#8&b>|>+#Z@hs#&N+Gw2d_43ox*UL}R zM!}$~@)*8eZq{z`_rVk9$(L_z$B)Gm=8iAi)E@P}V?LiV?=kbC92B~ubYs>>&HKnl z&CgfrkF6?Sp#O0^@u>OZ)xXr;XHvG$lvO$W3mve!+-&}%=4YycbB|!1t-oxbd=But zKEB_4yc%hCef+S2x2QbI#U7d(X?D@z({$GTaz(qtJZhf&p6(-9$xj;b{v>q-%k|kx zH1ALH(9BO#_2^3V+r+BC4f@N&t^&zIK;qiKgnF>0R-Y|rFzf`kVP`Uid1zr6c1Mp5 zE8niZ&^&UUcrf?;PzD>$cH5vYA@fyb4=oHEO2Rf^iY|i%V{Qv>Vl42`!mywvY(biz z&fviBi59TFIeO?ThVT3(Q09YqXolO!?3&V>S$atZr0%z#2mKw!h zCG&UdF>%r(c9N(`@DW4XmF}T!XYlO~KA_n#3$UJ|o8ZG=V>ZS4Y5`DH618hU3$Wf4 z=d1-hEv*F)N=Cy&GSQ&ZsiL*u6*7M}#$9`9@|L16@Rpkzpt4O}fd{ajr8xtSr=@`> zYQR4T8<3t4@Od!k_5fhLT<-LMr-k9c2V^`*bALV0V4W-S2dlR31og$8iTQHO|H0IJ z*quvRYobqWQ}1d4HW~PFRP+JO&I$oMtY{|sl8vLfOXfR4;M#2wz$=+w;-RJW;FH=J zsUgvqM8cvI4|o#P*A#))tF2%FSRcW4Hbp!w3=95U+yZkZG=l|)yDi}AqlXrT1wWCo zK(06Vw8$SkuQyncpPsJFM3ecZdN?78KcYW4(%m0yF|gjx;cNncX7>jG>&AP*{Q+>* z>FBPW31D(;%G1)C@H_2|RDaNaZEv&ucG9?SPAm}XEFk4tw*woQSn$xoaA1wJ1I>K3 z=wCPi2R6AKAXmD0S{M#&l5s$;H~2#sy}|JK?&)T{)Uds_}lrEL}yQNy;lSTeuoU&Ql_=c*r z`c%d(d{?Dg_=@^Z0R4)O-4?z%zYVtUC11R;oqr6r@7Z6rseO`n>z-A9-nPAEJ90a( zQ{rabvbU7nvbSHUJ$6QUM*l-S;@V!>h%529zLjQ2zB(Dt>i%`F`j`5jN!g{QtlF}7 zw*ywk?&ojW+oKB3-LluvUp7!~0>A6y`^?9yzPan;4>GXc=W~yTrrPf=8hn}_vprqW z?l4>SlHX$v-zPt1#QRg!o;%lP9>M$+56%1(RcY>3zeyz2U!cD@%ur9VzKKI?RU}Tu z)#koZBaZSD>Y^|;!p>mWQEtP|WeoGsoKH48ou17JiHo z-b0fY{;vgvPj`g*wC^`9qncKBwNe1<>4USu91re4cG2QR`ako=C2LJRutxv@>q))S44x)$#wU7jh91dJ>#vmxG^@a^8mfZlT_7V}^2{ojjmHs{!W5^O}&ALowx&fL)*Y(TT49$=kW zGr^W*O?4II!ydS5%W2n;2Vk8^cQ*ArEe-phNQM2c1;b##WJ>)*{ZrlDEWmoP+^IcJ zlh?j)tap=Qm}7}{vp*AzwWaKwa=i&OwKuM-k^`(WI?h1jX=$K|we{(@ zO%~Ow3chg#WZUdHCBS3@%hTkwKce^Ar%U!d&1ScHUc1%sX+nKzCxN$eGHxcrX|o0Z zF*oeaww`V3yV}g#Ro|~P@C{Ub2b!HP23X%4W@0X_0Or+!ihfblUkTtN)>%IYV14hp zvjO61X(c#W$D+gp>zv+Kg1pEdy0zzjC{_Y%DQslA)`+KeWn2tz(%%VG1sZd3ad?}FGo#=Ir5!Khh_nk{v)Y`Q2m-0rXAB38S<(ahJI zdOfS%r=Px<9i#8DxLK(_wxWEC{>L_AQS3(bFLk4ml19mQoU9Sq7sm>Gh z*Nl{R0MqrsE8QQgXHT~u80}6)yJJ>G>vigXVBPF-gyr>DhZ|u@N2YCRo3+9WKB*Ct zD*$x%(dUzg7J^UJp88bYt-h|+N=HpS4$7SZf2^v8SLi=lwskl*`td|N= zRqba>f^XQ}NQ{`)M-832g6|vylZ^z>>_!Vd1(TR$_e0@@_#QNGHka441{UaImJ+u(izfV+sz7qiP zk9Lc%cf3100iG6u_^*-`zuHoZGQP;)Y+5^&XYOiu03M>m=V>8`|3*>q`x?#Kv|s%r z-Rc9Z&(u0YpQnYO{yRj~AI$GBH6lF_@LMoyEU>$Cfc5gC(*m9rf(0Lzw4nTBwdcUw z_?+qve}D%m?Ri=V+J8z^`|qC5Xn&epdw@ypd0GhCe??ULPiEA9$m(=KTe8`QfA67% zp#675wLgDDwz#)bL!d1=>w|yqp@pFRPb9T(FIMmK_{N&2yPF(<$tH)Vg&_TJMWyfK z2RdiE2RZ;Rr#f4n7J~W%+k2T470$IiMIhAX<=PHTRw)Zny`~ljW+JuH5UG`#7)RSg z&g~Ktgm%uYVqm@Q=p1bW%^su!tgj_7LDQLPwvr62&-&$c;Zf~SQb|B0gVXGnd5jEA|^ z*T=Pd>U(G*sDHMo`u)vf+{|Zcd(@C(>3VFR5m5 z;dZwRWyS>$Ed&=PL|w=*Q@h1I9tD`3sqwTB)UV5`ujVMsTJY^|^#LZ;=V>9R|5Qo! z`2~R|8tv`XNPx)<5l;(2`{zn(-(IRB-#-v~l3RR$$)#YP7J~Tq%ZfiEH4ANfVM_yCjQ^Ry7ee~X0pQri$W2>h|`)aYpoO^wz}5ZbBHR~eX0 zg8|J>jRNdSjq*FPrp0PE|( zoe4Rf7J~Sn6BR#8Lay$XA7FhUzEggl7J~ebh{~TaA=hv_05ItQPYc0;ABZ||013IK z+YNyALsicDz|%r-;(Dc#sWJv1TeWs&(lJ%;Ls2(V5i9sx-9^hw1B6DV8L-ASTJ5< z8lqRaEdZFbfTx9E!D$i}NM$PCD=_rHOoesIet8s?s-Yg+C+d~3 zkDRMcqH1A3F=URU7LRHVHD;>gMfJGCl)5#ecuaZaU`MrGkCAF!UrAeOjBTlN%^kxF3+hD$-&hgd?afOM<+|iwoym3xQ)*gFtN7LU3!I^yj#&+>B zl+*goP3_Y&Y1>Upg>Ls%^Hb{DhozV9yfMA}q2GV`!wT|7^n#KPqwp2doCw^RGm|LlvBRC3>VkXP|r+@Vh>K$b7s?Vs?G}iwu0H znlcqVG&RobqQR%>3$!YVc89qVJ^4MWs+Uzo0PJY*j`*)3)A7Fn0 zz&gL@{9QcF{4Ui3Rn-r~##y2!%;@j`pHLH$v~1$xVDdcDbiS!x=kl4i(rq5VdLHgH zkEaD=-m`jdUir_|cbc2ijTyQkMa`LMb#cAVj8$$k04B}gX~CFrpS&3>rtC!+ z%NylwS(TrhG=|`lcD|aj)@=#E$558=v|ucGMY*nREp@$_x0bAPTLLg?2~P{g zk`ISqNudx;R%*NQ6}2aPT3saRt0n8*mH>P#RZDnUFqV8;-ja2?;cCQ_`oW+^rkb+B zZ41EqwjO7H!qbAW#*atk{a9X)_m&#!bvM5k zry4JYt=s{?`l(Z=7d$N(FMclX#fphmr2^SOZwGR^+YNyA<&sV}cv>)S{88SG)%)^| zNWCmwt48^~`)hicIBY~*5|UFHHD`I zW5?0*cJ%4AufJM6*K8se9ah08I8EJS`YM&J*{e zykkV&qUGx>o?={BZM9AS>wPHBTEWwTap6jN7jE8~%>{LHN>?8NDwFdK`1c-KFfQC6 z?m~Mws?_p@NWFv5Y%y)qGu&+xz+~IR(}MBiNn(EJtu;^5^BvfAB(bnPEkEB{rgjsl z!MwU=D`}pIy$^q30@2?4@Gb-EBNWc&8EE#_1c3G3h$aw|Yqs=Kt$B&V*HPSOzEa(r z*wsV)Lw6sc;bVv#YMMJDg=$$mlg86b46sncdw@qj8;t01FXHiiBUU+E1c=0rO zF9yeJRke97-`JhWjC0&(+{BpSp#@{c^X1K0p*QJeNq#_#-{N*-oN>cL3&xF?iMvt$ z;R!gNUFqH$_iEEw*!>T{`k0Ng+u~`#xbRovF0>c#goep)g1Tk8tH%bB_2F-y2_9N7 zCj4ChCfu0G1bf&7Cg_ov&jb%G7!%$dfC<|&nV_B-bJYZWS*_0m4=orI{#o9H;aa_< zwjdPq>bhQLdi^@nJ=n{u0F%>eo)(NBpO^PzMaK{S0QQY;HvlG=S9w}6ZhS-DjpeXi zYQNu$x48#S0F#RpJS`Y6{*Ra!Atau5i~aR!j$t(c75RpU%>OCeR?F`iC4NOKP z(Ciriz`BVy5xIa26maeI1;FG6DV~JuNU8Pkh#|YS=D2IU1KVM&!EOs zJS`YEM&#Ydnu7vW&vKiwlQF|X3&xB?<;}>Pg90~ha=Vdd-0;wXapO37HwN}+$jyGh z^ip;%17LD>o2Lci#A#wqgfN`?jr?$Gy?#Ci4-7}UX67DuB!*O9a1W`zVc@H%_5o=2 zkP6_#70pB?j`a97J)jF^XBQC%%oJ@Nr8b6+C%*JBK}4^qeI zo3%Qga*U^@^;=GuA0jHqx7-!vR|Y1x!vM{$AOP!wt0pefIY+x+S6uut#4&%MA7@n( zfKQ|<2~SHa$xXtQ&HC^}QdOnrPQFfXVeeo)(M? z`{Z4CiyGXRZ6L$~AINEsZUH72_;^|{F5D~c!n-oMU@z5y%H#?k{Cf{A7#Ch3@4`nj zx?r!>fy(3(AN+d{Ef^Ob5O<-y_zM3z>yO;s0>EUqz|(>;;URGo%4_?uTObCkKXtnR zFzEtM3&w^2CGWyh56b3(-8qBGWakY3-a`wy9}OH)lP93rqkVw&CUg^%>Aae6qkivhqW~t`D4v$qj*n@7 z1T#;c6OWHF+eU$$c4GrD*+%iSU|je&aTnTyjbg3djH~)Zb>F7`Ke(#|z~ov5PYcG3 z{}3}Hgbw55Vt;+wVJvCZyd6Yt5nTIf^$>HfgLQ6|Oo>(>k>V@TP9ps=0|j*z%eenK zuas|1&{F(hqR&bT_=su6_@%Z+u&-EjgVer{5LPLEC0{WXc=PY$OjA8#C7}T(&%*Jv zG$N%BNUjo#$h3XPdXw+*ZZiNTx3BWFV9eMkZ${?JW4E~70GMj7$9P&WZX7S~#sY4Y zJHhP-z=smu=NL~5#*Z`P{TNV3MJGy={_*{3#tCacG6pbNGk97sPFyVS#8P!m)NjLu z?g{}gxu2P*1!KdQybXgHFMhw&T_*r0uQ%Xn!I*K2ycr9>YW#AyEdY}@_VTn~Y#Eog zWhA-xJ>P7q3l}GuzGJ5`nJC9S};z$Nz94#8BmFPSZ z%BjN!JJfe~-a$KZn|f8Kr7n)1xiPszI(fSC%Idg!i?O~vKa;i<{mu(~as#jsw5WT; znL842YMz`Dt;YaUTn~**l%~4{GQIDbZe>!VlDcjQyMX*n76q zk3HDZ2tQVz$Z_2(l8KnsiysmD>(f}wbZuvDCDBx|%_kD=rV8-`xwrxZbql?7*>rj~I*-%bp;$ zxda={^6S(N58p|q)CucGuoNHRobVBYabm5!6Wrl}RPj|->x3as4*aozj~I*-o8+C~ zHs_{JtRNwBHx&B}LOD@0R+rF-@YqzZ&u-#iQEm$7@s5t!5k* z;>L`6YpXG?ZY-EB9#h;>JWO?B#S`^^ojkUvc*@wKR&lHOe_u}iU#oa(`8*xNFh#$6 zqEv~`8yy{;sy0UpqnFpFCT4EBW^}xzZb6uesw(}`Krp(xRh?<&t2dVlacx_z63>hl z4YF4w(L@5NM{nawV^sgHqPn?lDz1;Ze(l!jnc3oD=BI3IH$=XEQ~SdIZ+}3mc;+3m zIxp1jtLD{v*zJ|m>Ur6`x;>&)RsJ+)+kLxB)xzx9aJye+wI<_PUAAAn=uvMb|Ff@B zs>ZWptJ;I(#e6+K-c)%mWm$V^tXQgS`C4Vw*t+sq@{d-ly|NKk;&FX?W(R&=`D*nq z)3U2fS(S6z?ts;~()RN4VyRN7t7pLONPc_ya#gV1ze}IZnJr(bziFhr4aly~R?oM% zK3m_Bcin-}b|~5%vns|fQ~x87$h=wql>fPrnPgTIXVVOS25S#}QwsynPA^A@WbfvNkVlGbI|-K!sD0$8n_b@l2O z8+ZfNx&X}{XaKCY2b%y+md%o%wnX1tA$Sx@bN9Re#BeuvX8M`079us2Dybu*zUJ;ScXM~0 zf%UE;=SUZ5c5?@?tGVk_w;eDODqtUCHN61q^?2WJ@zB!B^yY9HynhTJemV`_a@|i( z#m&6GO9Cs@B7Ilq2=K{_Lmpa)4t*pjhX#{0iMsWFJd;IR-4;EbvB*OU(W1`;WzjN~ zrpj#4`EG;s^})VY&O-~)psxpI&|vbW4&M=7?jF?vtXDIg7V)$YE&6^?7A>t$s#}nJ z)1oWg<^W7)7 zcD8IhEkt*Qc1W&SKw>+T`Rd;V5UTU$t1IiV+T*U4NTX!Z>Yu7U!97)t4XpQ5IAa%R z_EZ&MeYLEK-A-mM!&DX6_Szr7dj9MCEgo80j}Fnn8%A#Km=Nkwn$qptJ2A~$(cPmj6WA(IH;j)xYaJLd=G&azT9YLxC)x3>94*r2qX z3jml*A@H;i4Z12QgO*q0eGxxF-RYhK0jzKSbap>HEkuWQ2IbJw@hN5r{ARZ~0Fx6` zo))4xQ3&RQGR8hE05Rr`u{RguWQ5%)RVOPj(2mr`yh172IOiX87u{p-{RSrIi$Jr- z+yEc#7;~?vO-w|U+GL4a6^GAIua|X=9soXtYWjFuTDvOR#Q;X$_b*zsc#-~(+8DYo zfXM2s7WGZmxqU#O_o&;ZGZ>pZwBT$~?)+I;?v&I`IQ|hHxO0}<9dd-n(?WFTMM1gK zU&-%^D}H-Q?hypQ`g(om@Q$a2XwP2;WzV4QhoO<9J$#CcQr>lU5`q z=_vry$bs1E&fBiBHo)ZOFrF5oN1q7HBXxs~+LKkxs|#^`jon^TkJMTAE{_1#%Nx#m z#M46b=u07Z6iT+C8h{w{vJHJx`BK%NQ+Tq8SbK;42Mv5S6|q3Ga|!@2S2Po`bJp?9 zB~ZTF18jS@0>I=jj;E!y=R4Y^0I~^cn);If0%lGUfnUeF$2~>>SX=IF+jv@t?))|^ zcldR@r?}k#m~@Ayh3L+}PF0P?&cE7+B(tsKJ=JXxz(Z8^;b|cnv^FS%hNklSBb`cM z_iI1HZ4$ubJcy@-Xwv4OOj=fLO-17sW)gI-+aQ3+TEx>rH0Z>T3@YEemC5ux%k2%o zWTuCwh3L&$A$im8D^(}(Y#Ff?7-U{yO-%tNw*vFD5Z$>nBzMY7PsEzD(tXVN*{0?! zv%CSA9K7+g5WU$Rk~i(ex4`_^Z;f4TKzg#nfq(Czg=o#KL0OaOa+nvpI~{=at&`4K zDNhU0pF&Xn@TY@j-R1yHZocAaA(}HClsQ>W2Z7FZz71eEW&zpm?hriM9VYfK|lY=&%7NR-t49gt$Mn6#59ZbzoH?-^|awR@VREKyG(m`r^RS8#{Z#T0MAi!1|@|*g*0tHnyve zI=quh;)Llz3iXjry1R zE{de=kSVM7ByMoPDxEq1d4XY7u)Q?d7P3-*&q#R{P+gy_Ms==F*4ugyJ}}xwMN1wR z7*hWOHzvvj;Su^*{7(zQWD}z?GP&&{gOTgqM(Ueld`5a`ff;$Sl#%5V)Hk7ZOL zmRHM4poV&jwG_H_N`iBRX%|oLG&aNT#%7gi#?+4NuEu7yfyo#Jn%(*VtQT`kj3zs> zlV3CdEp^#fV}NfreE{nTt+U4Pw6w-NUK<5-1i5gzLN;7Fak7+c#6Vm1RB~5<0DKr@ ziH8=LB{v9L5x! zC<-f)Mr!8`?j%5odb7!0Z@794G`rpatkWWPJ?=9r@4m_cZ1s+IS7ij4?EZLKT3MbN zM2I{$lo099N9>s7SQ8+&(*WzEd(MWQrv+xp{lS>BmpA1&w>Lm%uBSiFRtx^UhZdM4p9#g0Thu~ZmO7%I zvv<`Ia$XINcxZt+^0h!5X|KrROkN#O_4V*4y89D=$)zHm7ML@S2II`$jLw|ob_QV5 z8J-rHGd~Z-nPpSzf^(Js@>iIX-L?R%a|F)Lg{K8(%O69rWxbltCrk9vbXSs(*pBo#Z5x%->__&X61%Wqn2fXZq)4!!1|bw(;J=^m^TLnJQj-te@*yg6Ff8@(AUjJ*ESLuus~xB>+$U?bLq0^m(lE5g&#NDafE)5IfH ze5XSs8=~{plvnP!>av~cm6cMya(TTr4I>d^LHT^sqS~8db{UwQ_yWydPzG2x#3oKV zn`1IAC|_v4*j`Y++`#&61!wOIG<&%O;Ld__=l85ARi|4`crVB|0fo=77li>niupwz zT3VB@(sf8YjCTmv7Ht!*#FGD&?w0L31MBOio#DF8{Cm4)16U6v z?3Q+Ea_G@lbAYT~tnaEh0FzA`PfKgg?jYi2Zz%CHRBJV}^){ff-8uohm5CA$Eih~D z4aA!ARTtsX0C85~YIg+$Sf8zT)?1zym?JL;#F6&meg1wQlpV5KFM#!tZJ!|?T408} zJQPEgG>d*mu5nipfXOv~o)(xRuMfu&rjCHL_L@Jy`tX9Yj_|a=9Qpe|94Y@;J!a>f zt!#JK5rD~Vgr^1O$a_O^w27MLT=P#j_N zDIo0zYe631gQ$Llrv>K7(}W!fXB>QyuoCGYC-W}e%S_K|@5cw2yabS^r7;@D7ReWe z;!M_Eys)#|HU?m_@!)BJ8S|j9F#%k@cZuxmdEP;ZfgN$9bT=N=Bo0W#rs1#;NK_0= z4mW^iACLf8uQ8bT>>NnQctB#(e6c;JFBO8=YV|3A>_)V{Zr| zJ|7lVB8|^%Y00v?3FDenpxI3rz@4;Y`fD=WeF$VbD+w@}mE>t@UHL)~5%Nvp2+g|AL zkIw);_Q1zy4R_=AbOVzqT%g&F8^Esd*-!&@itvmtF(7~sv4?2@lV@sqTH46{I*3MY z=`DfmcUjX^qcn9wFzXRxQ2JOafd=qdR1EX9zzkXyia}YE6QJ-pZeuQCjPcL{GiGBb z#;k2jaN`r8 z+2a9#4{WjnWP7p$Fge-bX=zdm4Ww5m&sqD&R&$;PKO)~FR=auJ881?E#-*rx!J zZT~4dT%DI}J8VaCwbrH6)0LUaYn594ic+Ilt2awEyjPf52z{Z6Sv%48CIgcTp+K_} zZ2%vs`g0Sr3tk9)vH4fMyS@0d^(YhNq*+dVVIFn~WqnM)(vv(*`h^ zjO1x)y?&0aQQ{-)mk8JEbX>V|IvUqO9Fo5J%d>*=x(oOhalnlmL@=E%UU%ym?0`-mK9V3ND~I zd%$fF!0Lr}_wfy$7MMXF4#l9s#2`Hu@b@z>cN+un8p;@+7ML-g7B)ubq@SjLgZOyp zdBRGhv-aiwq z=}+sM^HYf7Gm!1eO8_Q^&pa)yIA0GUTpkq;7oDIEqvAXwtV9}}YtxDo=f{f~s?Dq2 z)#k4ZOb(BLW>*`4^%{(c)y{OdU)LLE%O}u}vc}N>lUqJ{T3T^_5k$rLV<6$epX7Xv zi39s2C&1)3Af6VOBZIdFvgam4_X6VDXE^~TGgv$=Fi$oJdlJsL?3aPG7a8wsc*xzS za4A-x*?kJYoxK8`J|)A>Xkgo$1pp>D3-GkGvK+0$RD9@uig0B~M<64`d?VVGZ>rHp zp~O6W@p=;|w&MVApvLSxEiiK)ABs6kCUA9xn7X{d-QNRD-j~JG0(0a_VMoI0TaFc0 zB5gPpus(I4iBkKpEx=^Tkf)_l8pgEp#!$Rj(E8L%-0lEO-tf!Q0(0lCK-}Tir)J%b z08F;(JS{Lsb_e1}dqq>_*c%hI`V^D#fMtHWB?Fke&w-}}=FC1}X9Czk^9G<)s=aAyMzj!*bj7O3{F8Gy-a1bA9nmtUZ3k@!^eWx{n?r~RK9gc2_jRsv(Q zeE{oa?{HU}cQMr%Xm+&$xU*h1CrQIEm;u{fFawxeFym=y&3Pz@c==y}#0#JI|3?!B zcHSRgGVjmR0(0cufjH9cPu`99Z^!_3?ZiL8*!H?Iz~s6zPfIJylG{S)QbzQz*x0U)PF0(uh0)7vQxh{cT{Ak~Qm3t^qH0aO z7mr|cb*nnl%2#hzkI2`y)hhAKXwe|*CHJ@%O(f8iqEs5AjhX6rQ71^_`e-5F%txJ! z>DK9)+2W@12{$fU)aqPQu#k1-51Bx*9S4|PSLSJfnR93;=CJF^A945h0Fx^aJS{Ls zjuUnyoW7+`SPAS~_`L1ICN}Lg7=X#xtdgASDViom^?=YG`rdW+*ub(e_Dow43O<*F@VWsF`kxI zoJtVkvQIc%!l^h_VI|V&TyVbs^X_W%MFW%hexTXa24Gk3m*pH8(CvIbz+}Fkr==C= znL$*X7X=b7e7^r*O&r+k$pDl2ex4SXBM%1RNS1s*h->Hj0VW5mJS{L!{#w`*os$V; zT=pzsCDQICTfYCx?mmT^Q31{FQvg1|d_S=5d_TZszMrS1mE|9_IpUL;_lFW93(5C? z#RQ7&IKX7SpQi<8&L=`Kht2nY&E4MvOy*R1T40WRN!XEa`j)qgDvV^$u}D64#-jg((c3MRceh@cgkzvpX<~=$G&aquARvIg@JYd z>AXn?Xm-94U_G2Pb+@xKc(@WzG^c8fX0$h7@h=7ZyZLha!vWS=N#}3kX=!`%sP;@Y zgjD4Gr)=cRS#GbFCW{%(`HtHhfXR!(cv@iQ{2>%`26ZJ?S7W7*eiEm-zw0&zU~<;Y z(*iSQ@OD*qL)fIUHeV=2@&0B#o{FQ|#6%-*`rEbdxg7$S%r^71z#Q5TibJa#@nlkS zqP@wEd**!Rqi%ly*4I8e`yHMZm_J8_;!j^y8RV}s-*KA`xh0>cr8VbaZJ2m}^FGG0;dH!^P2A%9S=t9OI4=pf*z8H!@{rQN^aYFcC z>^9{J#uN`NFjKxAiYcq7^7|v~|M;Oa__NLJ&&`ZK9$H}j{5TYUMvOn1h68_e52OJm zH*oN@z|8qgDCVr}syxhK?N4rZ0M^4m=aL#v3(TGVCkC>MV_mm9YUDQI-^P(!1Vedi zsGj1~Mb2pfA2BeS)`ViyddsF{$C`KElUq#qggCW!kck+WPn$yVX+=6p@Hce1rGz_( zQ`^Qf5d(AQ1Yvgq$mzUIwl|)a)7fxiJT+ab=PQ@jYtxrj3)j!xzvsU6AvXjeYpo*W z{vPmAqIXV-*1ZoYuJ30sA=}CBWV`qwH$qgS3jx1@`b|i*7QWLD>|FlP`Ax&=)S1hs z=T^b@+aHfObtOe6Vp_c~))h-U@pP?l^-dQ$hIA*HWw9f-ny?3P>Wo_^Vqo^%9E?3? zlW^3Wnf9-%=hhJRAWqtY1?*~vc;}G4Y7xVzJ-b4&XRuJ~UQ2dwddRIM%t3rJ)#PFU z+Z@C-F|fK*4aS^oryz3c2y+nMM45vHY;zFT#K6q?zwEtvoZLm7|DTYVBy%Su;Ru9V z2mvxkKoJi}LPFxClb8eWz>l5j&rElxkD-rAf-WA5c(vC$sO#z?uIr8Gx?ZcRuD7D= zs(9kH>LT8N>#-ics{Xv+)#>-tyXrIJ<4e7LJbwNm@JhYAJN2x0)w`e^5kp_`=;8q#M~a@FR6hB_JOJ_eCl#Q;M6OwNU{ z00o4)HWUWv@1y`)sS+W}aix*|xJu?Yh7;;*IZjxBazb4j3gh&TQaJUuQ&u%8j~5-! z&_SJz60iWJgSx;d;Z0KL%;E4#<^%>1>TEZM1t=iY1$J})Dg}`L088dXh70QK+y@I# zE~pEf`+QUim!(s+>Vc?Q^>k{h88)c1of;OPY)}{2seL{$8-3fyL-cP5zP`0Fa7~!? ztyDKjWiE$CO1IhG06Lc|Y7A*3#Sfg{ZBpiA$#qgjXFD|fUYDVX)4nO9sr*PvQ(2^* zp>S^xUcvNbs1I=AGZvuD1a)mF49mS@uwy{{$IHD#ej?cfS3ZrtG6e`#3P3bEqGgR0u0;J+H6h`GdAyiuF&qQQ4Gb0S@>L+sZO)Nkmp{@;uL3+FxNOL>xmD$cfLY;+#1t=uc1wxvX0;wPJqP(dC z8K6U6mdK#a-EL(HxD184P+bb0p=ms3?9c7k$pAu~bssE10iiC?eV!@>(BeYfzjL!2 z$Sfn19@JUa!2&KrVXpH$DP%^_b#z{?w|9#fM5uH3DQ2($MTELG6o%+!QV>dIZpzecuS2~o8VdqOy2o2_|hoW+GJRen?p3~qrGJH^HPZ?tY$_I6A zD9nStBZW`uQ^wB%I^;d-4C<$Ii*8uJWhji!Po&UUl1&pAv;YlVF+-hudnkhiT!zBX z{6-3zL0D=lWe<2RT+F-*_|V80b?)N0REEO%EV@ocqDN~8kBDY#lb+{fOg)p!>c{*+hyW$;5zB1@vEbGI+1GBo~kvTnJ-Q@GCw>^~-jd&FEJk>YwxQ`6@&h_2we zIArKD6ozPn6hwVydg`8;VjXW{;ILPZq%jl*=W;1<7VG=Td`oU`g?^PzEm7yLk;`BK zm!U8+V}Z%Yi40B&Toa~`pTjjlYoMhLkv;@oCz3Ai=1`g`% zo-r1na8TEV!r)AZfs^v0lT#TusIzde0EL6PKsYria0YOC?!Ce3GzJgq?6NQxpmuW6c6eG@jO=wp1Iv3o;ibIggSdp0}D__sCQlEB*TSX zDuq%1cqv=2x1Z#3&;A|+bXJkb4RtnK2@ALkg#`z%kwRyw8h@f(q3+p}yo6zcI_o)D zfU-edpy#|*3Y&pKqZu{y#{Q(|cE?K@Jg9ST1ZA)Q#e=#w6y`ebmx3qd4JQv{*r3k3 z4i=znP#5SrpAf^QP^qWr)-Ge%pw6m}!%w&>uEe!$ zBhEBSjlxtZ-kV(6hj%71w}$@1NqB_0@(S zX6T^K-U5aNC>_*0Em}FB3_5?7LT5zn$|dhRC8?wC+1W=JM5sSVMT)kEgfduwB0^mo z3Ui==Ckjd4S*9*GDpcW>Q2%+Kk1~ukhP?^j9ZC_wU7}VJj1`E&;26cfWOhXEvH21fCkpY7`drcS?pkPq%x+YA<(El_s zU{c)Q_FoJb)LAfCfPz6?AefdEF!NZJzLVjEI(sAz3s6p|YeQip&&y?U%BDFs@>PZt z>MSQLKsliRYK2@JO{XFBb%qe?Z1yS^poCEG%3hT*6un&GSfq1Vr`@|e-(nb{&StS<0m=yVt}Ip=Ui2xcjIwS{&)EK2#9Eim6S^OW1caY*5$PK-Me+EI`?y-bo`8vzPE&F>JDPVsk9X&&Mie z*RHXE%TSo#^h9AZK;&#*UnHfKo~UmJsmIqh)l0dBY)Sp=WOuV=t_t}TbSZQ@+nX(k zhO|pTU8f8t&9;**Ifrdi{Ft@Golw7+>*}z8%h0&fa@~-FZ)=|%*qu7t+S%04l3_JJ zgyFNv$H!$TjL%vre9~+>Aebu|Fx!1#T!zBHTr37micJRuGsb{nuWw0XC=ARMV!*T~ zrtbMD*6$t$%r!obaTy8&vqK7)Wja8q7jB57RI8)N=&=l=f{&5QP#B|Y=bo&I49>@-z%lP<>Uhrcj9EVe2zAzbumA;wx;7LB=s(2(Lhnfd zXn+BPItvI3P(Y{)1oSN_fQE~OMkB7JTbUbS1|ZbgeQzv42O!k7p)eP^TM8kyk*Zz- zR2NkEySP=12cgcM4#EP|gHZ1}9VBBGbiWur?H8F+@i~s+gF4Fx3s63&3*@ulNn!Iw z%tm+0e5(@~HmI}tR#z-;8?Tdk1N z!_VV1+b3Y5i(ZqAI(x|)7H}DwNJ++}F6RVpv8Ja_o%j4nh7RiNjSyIX(m`Dt3Zt_@ z3Z1!Kvi2zk5$fzQLo7fMq26`OP=*6tE(OsXFIoFE!w7ZuvIs0d8KK^FXRHiHV^SE+ z<&w3}Fn~~J=j&L20z$oOzAgjMHGu&MxYYN1VQ*Jc$4M6MTd<&M#^dw$#pS7LEnC`L zt4?jK)T!_h0r`! zKh`pwP+!Y&!UB{N>e^5kr#DF9lzR2!T!s$n>>z{%C>_)V4nqGTh0a{2ZR;37sGrHX z4;G++P}hdS+~>nm0QF2yOnHvvpT}@PoeizA0Of*u*K}G&fbcmfTn5{z+WPR2H#}I+ z@Ijs3Uc~~G59(dpt1|d}O$wi7>SnTXaww&c;sD+#!w7Zu;1Cv|j8N}7I3$D74+1mN zTX{MC8-h>NtAT66Ow`le`+o&AQo6MBTF}`;A81G$DeCjQ_aDvaMdYZn0}mE(8Jd9L zS0b9qAEY#uMbm|xdpf$2>B~?b;NmnaK${8b+E5slrCA~AJd4yrKyECX7%Zr>Sg-)a zg1SH~E2UtW!*ws489u19VHy^od{FNS(`58!rwQTXe9}Bn)g;9Bk6e`|VGO%*#V;88iI|W$4Whjiw#=ul=S-4=~0{uS| z!w;svx#~)0s6m|_YOnwuYETzA)MNsKGJMk9T(y+}gF5>v0~VlQQ15z`K}HPlSTSHy z++1}P0|s>#3>KhZP!|X$mI7w^bTqC~WKv(qx1E86I(ugb7NC$&@47QYhWA`A22yrz zkRHuILY;+#1t=uc1w#58DUkZB?Hn`zUcpYFLr)~4&R$H81zd*0Lb&Hhp<|9~7yN*B zF@R8K-3JR$K&T6JpIf8=Iu_hVzlYw=(JaO8wYGggX&Jez5A`#+B~UEjG8E=Uw@INi zG7VQ!=-esquILjOM5v$2A;JO_5$f7d7^2&yAR0hKUOW+l2X*#JBrHJjpx$*Ql8jjH zeNynGy>TbYz(JiIgs=dGgSx;$=;Ko0ENNeO;JeP{0D}f~cD))4P&BA_o#B(=H(!*3 zrVp=k@!YjD$-qIK^&2ce;h-+iZ|;%;r#~CPWiB2%1)y^(3EEIUmx~CnfXh%=MDSxN zbe6QQ3Gwx6WyWz(XL~g)Kph8lfxX(VrJxyB)16wjlB<^6D?IKQ&omIC(@WIZ9XTxE zG8E=K3l9iM`WVbt4@KqXcs{B&8*V-~F?>*G6AZBc<%4=xf}xDKU`PxfST`tT55y%m zpI0(`P-ppI0m=t;fqYJs!eh?pIgEM`@nl#t%7( z_z&vqIRC){E<@u#7wMK8Jg@taf&Iti9E{9q{}gNbdj?SdgnQGF$b{UB85}x zTWTK4&_SKu3Bv-E4(eSyVKThvy;A7R<(8UD89=DB8(~<00zzFI3X8EnCI-+{t$HA3 zZsx-nK&Z2TumA;wx^VeFLON{=qxA8T&S~oe^|g}D9nAn6_}2I zyLl(V?yt6wF&^ZH-pC8 zT+ooBLA`T_j`>ABg+^RPp`E=vXzcXmO%>@l98r0`E?w~atK2T>{3Knn~nsH~nh?erO zz%50OJ2HIc!oW2lVB9$=Y!%EJcLvqCv$I+&XG`iXS%?*xna;d#YeGQuJQlVJW<}4-H#T#H>Ynx?$*ZfSW*G-N zCWQVk=tpP>eK+WA2#tm`ghsuyd*2CHQ*IV~nd#fU3i=oq8=)cX+fcWJ(8VXNTCkuv ztj7H@CLBOx8V94!Udw_7T!yA=d!2}`?d@S(b0>gaeDIJu0q1$<1uOV;5~!ih9*@TY zE<<5t{!I#*zJ@x1U7t`BboadTPHNQ>oEQtZ428k@RA4wdihh-d4&)7iYr;g) zE3;Emr6Wg>JMSiof{jLM>1*!<%qfN&tlkka+@Q`*1hIh2P#Bkyz+B|SYrhcEZ?w|o z41A9nW4x;sXh_Ex)H|!5oiQdwvKpGx)l1abWlt>NGBi$evTm-y(@V|?>@=n`8_H+v z(SdA3MJ2g{XBPT>XfTvJ>Z`aI01LPbg#p?i1<)}Qo;~RwFfjn?!yFYXKw|*ZwV^O7 zmj|XIXRJ9Ta7~yFB<&Iwt~b-;HK?;ktg(R0&@@vS^Uj@t;Rx8uxixGoG;1s8grl}{ zc2o~dspCpp3#CR}b0g^^P~X5s2Ux&mC=AaHQt&K%Ogbo+kmUia^DRCoE<<5ZZVn7Zhis1*(R)k;t_jn7 z%ws#}K4#4M4e0E>1ZYUd9Mn78Ih}DZ#R40e(`^~l+5H?W;4(BG^b!&6<{zcBn`7!2 zEit=nSh?qtehu~WxPA=_xD16+c~f92a{9Gf0@sA;*XF#-_B*Cm`vd6gE*l!sUJdp6 z?y{jh4Z2WgciFIj%g{K`dqi|=AC=NGCD&?A|99a2X1t@^v9pS}8&^tX{f>i8{Mvf(2ZL!nph}Fc&#N!21H%gb4!D?S1}< z8Tq~IdT2;Te$?l=_lf3o3lnvA?-L8S42{#=t04tnMf;Sa#8Q+CY)HnWhjiwDlt@iJJQDh6&hfn&hAKK0hggLDyIjg zA}0d4Ur4v^KZNuP=*#JjGU{ym!~!lu(>`S^I*$f!pC&F#zh(7H1`q1&ZQodc;z7N0 z^^ur8frkshQ+%fK-sI%ou>BxE?k>~HMV)mTEZ{N}<}_OaQxTBAab4Knd{+L(s-yBZ zb{AB-;@*5=0^jswj^p-0972;f9^^~jKtr0mfqLhW`OcB(L=O#V z#ESa7k~h$p=3t=CCU0N?m!Wa$CunyHKJAO+5$)!=QreAo!3HarZuFzh_G?(cWhjiwOQld*5>HIT;|Ea9O z?7j^aa2X0i^P0eD9K%ET&uz<@@7?-;ObCDASyd!W;m>?is^2Qow2Ab0x4Aj{*JuKidG){Amh7^3@|Fx8+GMD6yQ=q|6?x?fL8(6?)C=AfT ziNBWQjZ+yasI$o%Sb$PNT^kCcG9-qIFL~p1CNe{v-RHvsG%`b78w#UxVqhwAB7mO? z>DK+p8$-}O>2@;eZ2QClE<@8kW$Xu>6S#evxGc?<74Bft`bC{RmW~BnhQh#X2n3ps|3<(0I_3M6{cOQrgX;3Ex$=Si5v98TIqI zLjzdAWhe~Gp}<)53`a)Cb|7#~n2v4E8=Vhfy0wRa&Te#~A??;spYKK|+SC0?)Y*+r zEZ{OU4s?@2^9F&J6wDMG7>eLqF>C z-04Jfx?zbryVHpUT!zMJ{#`=~z9#S)DNSWAJDrb!21B`{&hB(#0hggLKzB+3ai!zL67 z*z4RBwsp?h>pc1}^i|nry}q+rsN;D!<}mbD=uPN;X9PMs-$g^Z---HPa2R?Ud^g?i z90UCfZY3TKX*Wz^Z-Xe{6|G#<23M7!A`rQP&ajvASnu5A}I2s)iaoqgOJ3%Cr0fw?v? z7&%?r=D;;!y0$s*NAG1iwyQyBCqHONJ2uqkyC04Abcjct-H*lsE<@u$v51~+N=UQu zZ+z}!x;50B zchuR9Pb}av6b9(cQUH11CVo5<1E9`sVPF9o1EAiu#U{gT-V>OLoU!JmfolTC8vl~- zqoIA$?HJVA_K5{thNjn$v6l96AyiT?`Qk1Btz*>L7o)L&%TO4cF9wDqV3Y0muvIW? zlWj%&#^Fp{+B>B#&#P4@YuU2lBq2AeI!|t}V_~4;zZPIf~8rsv%GSt~!HZ0&WG+zBbx-kWhr0)&e zq?`I!;kQN9#E%2lglVJcrk{DnYrLL}hSY0N?@T|{i!U8Ts(?VtRWhVyaA|O`# zZrJX8R;;$P9jooAPPzlMGIZ-SKx=}|254wV12oh-%Mo)9(5lQ(b3N$nEC&tgP=mT9 zKwA|1&PJm#4YE*YqcbevGBkcXsy!%pbaq)_Kh`77*&=GN2R$FFHeO8f2l)MrT;SWoUfnDiJMZPvDj!$7i+&t_kBa{!Sf-5*lNnu1AXF_bphU zKXN(zbA|e61`D_hO#_wDm1P5C5fGfM4O<1Xg0rFB+1#$$zFM;~o^4>7FO#Ky7`k>E zp}hcfHn#{3X@rKl4(*brI_C)O24=Xq5p*_79S!MlgL)@I>-?tva`tef=UWjzZpN$$ z;D^wF3w1W>2@ALkjZ+`eZ7F!9_KdJCx@r2_rBm5lRId7x)v=1#lAAbCXBSB?ISi+-Qh!T!zBnydp3hy(e7}(S;ldToa}XNjJrLni+F$ zIcCAa1^R!*RY!+r&wv8#k_CPO9ehxq-&_Z+>68U^cCLd3T!zMZUN54_yfbi<(Q_Rc z&hy5=HDR1*MECL4TGZ$cCKubEEWXJ&QMd1SgGn@`zJt2$ir3z3Ggqj(SIN=-6f*Rn zekM2cU;&q*@t%Jd;XR*`(q{T|1@-)0Ev}4v0)uBlbD`8xXD_hE0xm;gbncWwryWt} z3T02$z;l2N%?CuCofl#Om!U8=-w(`2PSEy|z%^mUoz%1Pr$9TUD`u$ck;A&wh6P-P zrk%=&3VtPpPRd#N-!N=YXH&$n0A+)^HWbF@k5bsAot5Ka8m(j0*=_t; z#YMmx^;FpQIctqNc|BrhwN}oS3OB?X_wI9N61*7t61qM9IU3S+YSe90iiw>oa6VX6V4_p(bfu>vk{ClP^^Y*W3Nc%F> z=eO*M)^zm}b#~bk3%Cr8&ukXaQg#MzDSFvchRW1WapA2wMfSrZvO6<661di1#*Xan`J{ zGPyUmLKi~kG;al+oztKpoztMM7iW{U+R1ZCIj6aei345_`h{GuiiR`}K;1T{IW{g- zs#A5f!IX<@QHuQ>{4hFEL7feyv4G3ac=uCvqY6I5d2ZOI-O0gO)RWs*$Ym&u(Mtm}($k%35v}#ls+hh z(j3QOjM!*ZqrQe4hp~XmP#B|62WBK-CUiJ#70j9m4eg2R`l|7`eNxoDx$_R_PUuAF zgP^k$AvB~DA=I^D(p2X>5qcLBA-o&(Q#kiQLmDBV-kAuQ{&z_wJ`{PM(Zvs-a~Raw zWHv0|GBghT9o>$CPk!zR+nU=KdE6`~L*q2R5z$nRDTYmRFimA?wN{w)cWCd0wnCAk&aTyB z0hggLHp7A0=#>H)9op~2bZDtYT^#ReG>kf%2#W<=hNh9qK(i)zBMlySd@XG2oE1h7 zU>LpUs!i@7`oqwh(jfXjL1%+#G^9Z^>Ut+LX{eo%H|1`^N115gW1zD^G#b(#9`$(# z(f9#07Dk;7qOpL>(75vk?MT7f@}mN~vtDUiC!!`U30xBj%oyy z(XHJO7>%BP%7_Irfoq}@3#6KVVyH}~k*KrtPb}avH2sGRG|vf)M!+`C`C+SI*8Fpo znt$r^IhEXwxY3Af2danF?NaWU=$D{hr8CiAfX<%lMngIiMg1>$`S?!wZhBVd8=#-d z4Z~BTGRJJyl zL+QZKs~@o5*%bmT;4%~j=3fJY(NV!mMKsad0@p+*DoC}>_ARD6`!?uo=!%B4J43y* zLSTMTid{Cert^H%*#!bD;4(Bm^B*Ew%I5>O6uqh@!)HDoxF$>s9j>*5lqg%tMe4#L z=--(g>hChHRy&cvB5%4hTCXAL(x|g3?O4ENXk6!;B3$Q3QX0(S zd|a5!doKL^9yF9;LI!nq=7j}ZhQi3)D}_w&p+c_V37xTi>7aqSo+emBXDr|{6b9x` zQoyVnZ`RbEUX3WLFUxI2?e$N0B>jDGAlhM~&Q5W#fXh%Ar~ZRNa&Ly}=~GK0{grHBmfVfdiVZnj|o$_I6AD2&f# zQuy?g;t5YE{Zobu>g*;p7NA^E@2tLx>Cv`I;nJ&;aD8)}pD|!iXK%5<0u&7Doun}_ zVDOCTdpH(rlD`odJV73kC~N zFsOG0Lo&dWrGObsKmEq7OOn7H}B~ zG*3g#jVxrDo?1BYc zhQ?z)E25ozC2%{@$u=@P=8nKMf$g)^->79Pb(N)NuD4{`>HmV$&}k&E@!I;!52MvwIV0NT-{q zcl!3u)Gp<8^Uut9vjBb*n@@>`bi6^`KHaQMdQM8AIh{44&Sr980hgh1<=^WT6g)KP zEeXlwSbR`jx099(O*+SlXJ4hp0xm;gWR?deqr>N4iD;vL2;0|o7WvYJ&&NQwMOV+f zT`U^X?hN%#7mMMEKYT`88U>@yhR;~QWoSI+6x~*X_hoAXdyEdBWq8bKfonoQfOcQl zPGeSp)-zcvV6Hwhg;>IL9H=kl7Kg9^?Qu}ohTyBgo%FJ>O>-8P-f92UZ37G#)Y<(I zEI`4at_`Jt*(waoQoMD-ojy9qut9y0a~do_*`TfsrLfr}j7@K$GS&2C!3{HDP-mS6 z3s5kqOLCeg3IlV@gy+@7Qy40!FXr3@3s5SkYeOmSQj|eO&!Dztk}QGz>8-(XH5ZrE z3XM8c(wNDgnO~bxt6Qt3_{eBJQKq#L zO|+%o>hk5tCNZpKDF3Tzqma;f2#VI)=0E@*UqimuZp(r+7n&5Yx7L; z=}I0OEWE~eKSlT%V?McB=ISB3Fv)KN7HYH!IRyVpnIFrbF$KU#Uz)bDf zb(w*z^AG6nSv+2?OcYd>M*G(cIe(4HBd~p}&fazX8iyG)TD4E58&23og+ByryPZkn z@e{T9`es}iKN3}&jVMV1=OBdX`KQ!zA4X4v0lANKbr@?~6K0cN!O2U*_+kTlCq7?Nt(2m2F9&jqr6 zKE^t#_|1%yT%cQM*kS+DkWQjkf!kr>{D%EK_FfERcG&luNdk1G2tacl_V?P^fXuF^ z_{}7-*&ULNkP&7yWF43eq>~HknpR$8A76aaTri{K3&?tJijFUX?Tn0QI$LV`LNWLh zmDt%exdiz_Zj|wx8ArKJ6AC-Z5fXt#A?v_IV11_3aQ7Kol4hyS*LKA_AhWp& zelyc71uVO~UIdb3XZIOyHe2m%KxQY@eltmI-X(&~vi3S{ls5Xg%1#DkeQeCynfuKo zk@xK6FnrY#T+2Me!Z~n5v@taA4^H~vamgH*HDYY}`?#Fi6fq<;v&aeXU zn@IxlRS`f^t)#=gJstBwX1AOCW|DaPcSs)Qh-aW93HJotXPTAZe5}fR*{zp)w$QbO zUVDN^ob(qo`TT}NX47B5jHbVUtTWP*_SxBYN}2w0F!^Gd{?bTfed5a6e}NfYrv!Q4 z=`UbU^H@M;V-LTX@!sEQ*9p5q*;A2_UD2OJh5i-FO41Z4YLMBL7r&V#Cd))HS=LEO zsyEcBanIUu)lLRvHci!UCW*|+BFGH1bCJ?OnX*FxnGHbvW|E++4GBfBh5a&oH~xPi zgD|remTot4I%$-2l=np)U`D%skpIHnNca@G8woPI8|gPQj&g}^rD4bVD?&PoX(at= zmluxMJ1>ygvEFYciOG(TOoVh^4-Hudopw1@s}_@E)b3T5TGB4*tR7^2!=`nX;x{wx zQow-n_>fFO&+6|DIGWCy)h|=C`n_>!Vq>GBo&xQh31H^*Hzuu<&gnm#$m~87n9(^s z$l5GvopYYkKQ;McI@-T5k+*W*0%mly2e~t+H}0EiK@QC6lpbXEM2p|dxbF4ZZ^91q zPYLO|`u4ee_&TVEtOL^~SGKQhR{JWky62=8)yJ#q(R3JGm{jl=+uda*kKK09*A2L)V5i$QOBjiNnnJ%5%cElb48SCj*uEP;|KKqyUo*_IF7; zpqxQwC-iv+k}27U&!U+N~G>e zYM73c@syX_Jq2X;@(I70@sziO@4nt6qLnOFX?amOTR-TYH~qbx2*{({fZ{ikMC3n2 z5b4Xu*<1=FudpKlnO*ksn@K|Q1rbP=tLzPRTV<5RX&eTEO#k%{b~GTL!+DF}OcI)J z%0N>t9FB95I)7g~5|y$?)O~x-Q^c>bqXC)SHusxJLi0Z&(DWUQQz5z4js#?V6xO;X z=Qoptoz+SkXa~xGf7YesuHqVmYK1> zd+CQ6@n35v12UUI;Wv{+=6Dfgj@h&qhIr;$);A?A2FcY9?1v0x4>Nk@F<}wjrhSYs+ zjredQP1yBbI~I`Hl`p@UBrID+U^!kd+;^c#vxxrzJ0OtR<7R#{Nr3i<0JJ1ob@6m$ zAF=}hS+7J}XODg}NkE<$5{QspmA8tRtgX=Z)pbT%y;kdKpwCJgC`~y6nLTOeH!}@X zz?N4zWCJx@UMsr6q_{QZ*>*S}vvB-olHeQ>0cR*%Dn;$R4gXP*=h(4;%-+Q1H;{ch>=JlIN!tr7eI2Nfzb2p5Z9R|ocyUrT$_{}82c$Ekkz3t_4 zH;(7qae&PFhTlvQjyHFq; zfjr;*ZZN0$-5|61-F`FUx1ZHc6E?s5&X9hq^SeJ9z7FmPSqEmWm@Yl?3w96jc`TSw z4*~fvO^<|+p$8s8elX{@elz1K{~f-S{9HsUflG1K>79DjlUW4SyMb(Vft>MBahOS> z^81if^g^V7{_BS!>%jD1y(*cy?zz|Db4l+-Lt&8FxryJ*G)VzC`lcl0i=>;IKx9RM zfXvQK{AQAXtO^Mv^xWiQ0Xv6TbCW?eH`$0e%kHVkSCg)SPEG!h$n4QmFoUVdEo&Dn zT%iA_d#UzI*YM}Mt4W&ZoaZQCvqxaxO61eIVHM111P1ba=O|!K=O`eva}>Xs@!#{b z^MswFJS3$5>N(08;p^ankab|j_<^Zfvl6#AO>mQuIePkSyO;btk=a*>z>Inc$hMV* z-V{5YV7{6}!XTf^xvbyJc**AQEo7&N7Sda&- zJuwl`X>AEv2d2|Xx3Ka{dxzzVH^7W`SRntU3oG{~KZ<@j$Y*jLm*32I%4GPCt0tnA zq&#Bsdyv`1QNNjK zmI7vvpB8~+ahiVnn|2-`v*)1wW|DY(MFfw*_7Y^SYYmc#b6`oHdI`wv)f#>?NmRZU zl1k{!t+N9DP|7|{#Ite!lch28(f(o(7EJ0V@R^^jIPgQ zCJl71TU(ih@ZBW;F0%ymOPK{HV@MXNj6TopEn`piw~WqhaG3%wL*vE2(XA-#7S}P? z2h17JTUo92JH{ z+3`j+QE^TvGra(4dH0^I(Yd>#O#zpoBtVyh1Qa@4{$;=sc2>CDr^4l3{xd3><Efo>1CH=XT3|25Xfns#-9M{eeHyMVha>X;3FpN+? zh3na{0PWdO*M^c9{YeC)x#f#z9?sA~oy!-F+xq%efos+0anwvL@b{wx;B&q=8+=6q&yFkS<7HSon36j0u&4C+E5af?IN(K&D>dO z?)EbnnR6L3sB@<lh}ebLW*(8A@W36~Sa#J3y^eYvt%btcHI) z7{YkWc?=!Y*KpGZEI|7*)U}}`I%N@bMmlt|6RKBpkN>z+M(Y@LHW?8MxC|x1IT8|1 z=>6u`2An#~+HdYx`^{S_a2* zW%q)99`~Ejkj}VK@BAhc#V=NG=S>tQ-42ZQbmJIxcE=eDxD1U0zfgNm*d6CrgmhrN z<9u`YI(Tu&Ixy`r-H!8q##6lBi-y!wQ2$GJoUef&Mn4~QcE=eDxD1WQygqzud8dfh zl5)rST80Yh?2a=Qpj1%r+Hn>@L+m>8y28|P}hc%04=TyNYyC*OuhS1e0piY zf-z>9H48ij$%%K=hd3Tsz-1_j$8rfgT9BntuI8F0&jL7BFu5N-gF5ReSiof{iOd=i zWReB&GwtmnZ2E>fppw7a<0@Q6#m)4D)D+11{@mj2c=4iZGZ&b@9d1SEo4AsHu zb*7u(pZbZLOaTxrBRyLgbnd+iW4MwS;sCv(G=9K3=7ZT;aT-J8(Hk_e zu#2vb647Xe=h9k=&?+eOgP?EZ#z8bZfj~w5Y|~oIO6cN3#d|w-2JO#bK6kzE2e}MQ zW7!tIvFr`mSo8|L0Ec;W$U4x8iUL+RF9}%(#y5wKPNVhAWva{wgF2g{fCcE#gnCzs zf&eseNHn^63fM$@qlgK2s?FWUK)a+#7O1m{O<2HXXod~}Od28ErRg$y+g*lxzOaWu zf;xL^1{R=5Q186pD8o&jCIZQEleKKF5LX(}cs?FKI8{}dnuUrdCjyf(XjP-m?(kv( zm!TxLX@z7II;rW|0sC>~q75}Q?cB0^tV(LyU5oeCvV}^V%c$h067(ZXa?^95AEC)j zuLGUEt_uxmaue#E#QV;qb}-2#ipERXx+fHRA$&hga6(<@DOnGDVF8z+@twcd4L9u6 z_O&5>#|$~k4`l0cq}CkbQZ!YICkpPTCNnQ$@Sv_QOSj^|0u&GGomr*0N4Jj`wG@?UsVKQ&tbF;s-oVkTTa~tUFf-f4t9lH9esU*6gE5kB0hV zd`Mh|l8`KH1kB#hE1m*+w4a7-mZ6sk{vqIBe%3O<1snHYxqVl(clVaf`}XX*YR|?@ zv~SPGoqM@8-j-m39 zW0I%s*aL_E35Ik{4)wp_irX*3N7LEnH$i7F=s-gn&7rOr@{*QfV$M{1QfN=l4WiB- zIK%=jLlZe%q?=FJ-sh14TX6e|+jGO$!Nno#z_iPilWHoIEW*X*192{=c7Sjho0$aN z#kkG4L1&MXp&@k})b)XQ(@F&p+986_pjy0cR^qNlY#B1&Wyt&p^i5pYf`*g~>YY2w zI@8nPe4NeIqeeCPeeSJY{GgM_oCx*HeZS0QXxhxx;k`O5qRph)ZT}%O3Yuh#`f?5l z7H}C#LQF4u;v12g`oyKvxcW?=C~ z@4o~?IF^VJN~J%(<8Oc+@v=eHa#?zYBG3XdLEdO)6}_{9+M}rBCHL zG$Q}OkNX%bsIxizSb$^*dlLi}pkPq% zx(Px+9B_vSFw4wRQL3xd`(TZiRx;}Bv<3^f3?+pFUl&2AuTW7}bYvS<&;69k0SleS zqt50#VgZ+-BrHD^fhFZ-T_-S9P-ovAzyg#C>YX(_HUAaRmHkQtm615PE;HQ=H>WT? z8S2M#^Gz&3dot9up(L02qYOCeuKlJ06sHE=G~Z;qoJv4 zwWJ=YNbc=&FKnL6ph2A-YOnxBgL)_HR7R(^DkK^q3!Bdt(RZZUaQ_m_8|a}l)K_r* zITmmkn*LnCV)I!c+okC;`pvw-v#$FUh70O!SB?cJ7u37DasgZ}6v1T)1VQdj{7wc6 z>TG0+1t=8MyCPEoP#z%yN}qXbz>Vc=3>MVc_iV5L#e#a*do}{FYzv7cbgJ!n0SDDt z`I`Effy|EG+tjuLv`ywD?!zGNqbZe}Kxc0NLqnSNhx#%zU+N^JERB+LP!aqlCQ0Tp z_->l@_ejvM=f-?Aq_HUKo!?{zyi`F5+SBAc)Y+^*EZ{OULC6!e_k`WVEQEAmeFAEK z_&Rt}$T~3XGTpBK<&39zy%!Csr=Z^1^*6t0@aTJ=-2)ze%5fx12K6&I2gU*}L*p;g z;oHj7L$(zm{&F~E9hlacHWqsXcnl4xP-kN?EZ{OUO;bQO_978jdeu$wboKmQg=-{?T#N;u9dfOBhpjc4XhNd4AfaQBKu%yh9onWw_ z&SJp=6btIou>4E}mc^y)fw<(^4Vq+_pw6Dg#{!fI>e^6}tNcy`ley+C=NUYxvo3=L zC?3?MxlHf0guIJ+ zYeaxqlH@9RhMNWh1$7;dTO%_pK%t=C3FOt1fdDAyihwegJhH!G(4fxdkzoOf2KBBy zG685V5rJlLsXAFOH%c;RFmGg-psp`yvAPQupiEHj-0UL5T{enf(o?GX?wNiXLj?6f zjtCZ@L{Qg;lH%oz2qH^0k!ZsADC+Zo3VqK6b#^5c3%Cp=QF*KgDvQ(PCBGb)&@&UL zvztR$z-1_jNgR@i&Px`snEW{rv+Mp!vmB*a_1FzQdq)Zd8dBMW=e>U?M7XLwWd_cFIJ3n*%)RCF| z>f|S!)H*m?t~5q-qnoSci6i^BkHRyd74@1*9mQx{vvQ=Ftz2Em#nr9qFxZjNe4s1b>VE$I@oAHLTunSA&D%x(7e6_(t`Iw5&?sxZNV_k61dMFO+h%xH5CyOkmD$ zP(LyMSHxM}_N{(#`&Fpds()z>X4;KN<=M1{=hrE1the}j^$*RXi_3`#L(1j%iL6hb zyv<>zl4`ne!l&t}!Ml{%b?xR?Z2zXCGSJ(R@z}tQCz3OY5oz~l@-rH+LQ`tt|jpRH#5|H&1s8%F?Gf7CkE&|EQ3FRr( zTH%IRO#-s&%z2}rAEk0R%=vaYAnT`etaSWllIZ+U1f6Bs$w}a%0*YqIL*@cI8IaH7 z$oS19k@=MfG9#Ld_7(lqo)^q|I~b7lGdNb4@ta8k^G6|Ia=B=-R6UR_MYVWJtzo$O zvQax2kXeuMn@IxGcSyqEzoOlj#gk?A_HI3u*Ia0a1M)eX*Z9pO!C55&&R{uvSiKLH zz;V0GgY8s6K8>T|HdZW+wx(e)Yjh#&0Hx%)?}mIh3tOYGzrjMA_-_QenzXW|N%^$SfJZ znItk>M3CuIZW2u-4;{Ol<;nz!+U)P@#y~!ia~8juBqWa!fn;(0!I8J~y2{Q2WPPC5 z+IjiSB=NXT1do#yk7>PB8Ns?`wbmYCQmm70PbjSNi#rlfYuoltH~pwO`&WPSI6b(Ha&NrLjvBA^Vmw=ECF zg~_}=7T|J~x7b|;WOnTLn~9-v%Yp?97wG?~teJO6z*F2av|xb~%IyghI=KW{zxZc$ z6~CDnD2m0uiC}T{`gAPRlYLzieW+_TD+T`CVJ3;iCnd0G4MwR$uy@$IEs)u6%Wo!$ z%9lb?(W#Q}(;K8=^Bg}WVvn|SwM=^~WfI0dnlwt9F!p8a&6#c+%;-!HWPKVnX_W12 zWpn~t@s43nrvT*ZS$tS+OVtm$cR#+CxFwy~ zfy^$n`^_Zrm<-87$XIe!$T~3n#0lzbr#idVKDnk2C`XOBJQd{%W!$4<#+1A5V~X!! zESS+T1!O${kTIpztFyZB$1tZ)!TcQ3g@Jq-*M<4bjLS4MtFXh$Q$;kF)FClc?j!6> zG9D%lGf7OICxJ=PyWIPW#967=fy^%V`pqPMEo?qWLYP!}j>8x3`((xm66s~03>P9%{r$meoo z{AQBKJV6ARp>~c#IxI7GEFiNNTl>uz`#wg z={)Auc8>vhE!UCx%_M<&T1YSt@T)20!Hw0$4+K2_s!jlRKjyG(Q??#&sf?@FMQS_r zZ7dLzG7mcZF=?ITL5ECcVWPA5;es7~(E((=N|H2BlMLNT_o72)34Aj-p_@4gbZ&?- zh9s-U=(blK26Pt-mvA{__e>vrJ^gglxs``hhQ^a$s@*7TTHS3SJ=rXynw!zQA$+Ev zA%prEOb?sNP!gHjMUY8-nNVhc0fRbsolq)6NnqY50?bN%=ho2=?dc1-+@0DWLkM+l z(Jq4pXm5zRHk3r@<01$hm)wgsH%3DYBhSnn=FNjxr-z@xRK9$)XhVLNjP z<0Pnaj|QYNlmumKNGL+4^WPOQFBpmnl|m!xzG~cljX`D;G*3FI-wyiuTpx~xbZ&sU z&TdMYCoVG-e>~z&-8VnxWYU4*XR^zaSiogyd}Xh0s9~3Vt_$faSs6=T`$N_Nw#n8= zr3#J`<>K+|5r{w;Zy95}B?CIQdz#q+1*o^6u5bS}4OGCoWJv^^J~NYcPYjkp?UGX! z8PvHPepA3@C<)795m?f1qOWAYpw7P*7*R1q)Cps7v#fSBikLR73Hno1DNH&k0qCi7@Zyd@Oxc`#-~4|9V+ZZ z^V1=H+Dx(*r+6RG)cq-B@{d){=7C}Xm!TvkUkS-XFOc3!B@EAIu3Y5EH4Y~BkNa2Xmm`F{8&@>3B_BvsxERxM3mL7mNe!2&KrNlbn# zg2^$P_QHCQ{dK3zLTDC7~}?H@ioS1zd)bcsxi158oAH zhQ}&~2fLquJY0s7cw88ghmfhr4?@N?p_jeh6YwuTYuW46ed_6jYAsuu*Sc2;fc5K@AP*x)*Y6Ft~m5x!lHg{%Y9F4L`h)tS!A z+o7T%?YvN**SZ%P)8#AFx%aa(Siogy+@u`7i5wEqM7(P()6hC7WYpO;7A)X0l*Hq2 zCGaSE=k!>ubg2Y&c8vuKxC|xnc!3BW-Zd|*R=QMzIvb8+0hgg99)BN_hmc{t8nzC? z#@d?#{^e)QA%{0k$F=Nayt`U06)Je;p#3hS%#ARH(J5mKbk;}FkWLv<*JlJ1AD#1* z@o7xdbu;Lfs*kqrU7#TyS5dc383(e{?VElZajoJ`vBgiJ6F}72%x5g%GBn-UJ9SeE z+nPTZur)VLgub$ok1NqsTq`#lvA&BrJ)v{Sq9p>@?rS(MLrH`_9g>h9aNZccmHtb} zIxwv?-8}NyOpoU6WzmrKXsFL?9*M?uI*2-(KY;~YhQ>|q4Bte)E24>{%AdfhrD+hT zv-uNPz-1_j$xlNv5z=*iEo2>-uB)#W=bGbjG~tPRZi3E>9z;Q%-3r74E<@8a1&k>F zCjv|Qfb^LR7}VLr5?FwOLA~p+ga9y0o+2Q}r!|j&6lW~7qETmWP{jf+LrJc)A|#>E zJIHSi*l*zMUyp3tG7A>y`BVP%)&x3K-6&HRPMGc-n+d|p3MD8-kJj*gZqjnUlb=4yH3$o}o4PD-lk}bzGt%1Eq)q1PvV4;$m8S8EJsxvH;@r*9mTW@4*jrPCx zsKuFhW^9>yq${7TWyjU@c1BTYEsB+uDqFlr!5BMG+@Sw)QM1_^uE(W#T;IgK2S2ZP zq54Bpc3DzZt;k$#k=0FQt)=7nLMc~^D>K)%e|YgcRq*IrsxQ!Awz#++?7Kex*yQ8Y z0hX?hKQEE{)ZF?Z4l^}_=`w>)(@Qd!DYNTlR0Ce8{v~YldzAhKe=W_gy;jmD*qMd8 z0A64RxFM1CRcls&U`7FgtY@GJ!1g*{`{yj0E~tB~+COE;-j={vO(eS-1<3kXpzEhN z%uHL_9=t8>3(=N}+tjxe7G5e<7y%SXFu$_b+vaG$6-%JpRX#pgPBl)8?V~ox!(9C2Hxoo-!QTYfa)D)X8B@L2NobG5@!^Kvg^Z>r;;dR|V))GjkytN)gnuUqs+5LPAhY3= z-%JpR%LI_{?<7H$(kTYW>=eUqCWyoq2_(EbNl>M$$cPOxyOZQM6GURS1QOnzB&brl zB?L0NljJuOMB?!QNywO9j0CI!j2>HNdXY^ry~x<77kfO@3oxV83y`hT3-1mF7}Mzm z$n5mOZ)RL1uki$(UepCN5dZXIr+s<>GCRHSn+YOuqXZJ(=>=3NonC;h(-Jj&&X!4l_YGJ}v@>cGUJU zYgeum;5gro<8luUhnXN8UzC8O=cs;@agF`7AWuKXDdm;S0wdY-1f-CUQ^TCJ|u$!)I` z8oR6Yg1%}eTWUW$-M&Y;y?WDYzeXcY+Niolxhu}8Ch~f&je;4yvIk_ny_~es&ctkK z6cyC(iz4?#F`s-f&Co3+@^xR9A?Iiuhvc!bjFws=*A`#TwICb@&V8M9jbkuoyH{|8V)l-G|B;K=q0X9@b-9Z zz#1^^G2QJ{6?>26>p{Vc_E;cyZl^N8W?7?pP+cspJS5KLRKJc}Xw1qAd=R~I3S|8< zl{NVAn;Ad3A$Ti!X24dG72zjO30MQBWu_lWi;1t$RTYrgh}dst8fEydYeoW+R72@O zI}MQ8U3R~jAR4chKx4^NwXQpEcbo|^E_Du&b@qa_$M%~E!ts`XI08=>cZK__&zdeS zzp7L{(Dhg=G)-ofSW9{eI!kLlkLAGt*rJN-;lP{*T#M=`2TFzC#jK*9b z+h&P+lhe8 zp4Rc32_o`!2}A~JalMcV$q_pekl9>QznLH;Ka_x^Up+ddo}lt~TQ}H=fXq6H-%JpZ zUr8Xcq!N{~N7PN}&TGe>Y{vnzx(MAK0s74Z;rOEj9BO)$kF%bY#;4e6fXoIxeltNd z`koptH^rx63`4+ zqUyv%yX%UoO-#9D7J#2_rvfs2NZN0PRBq{6uyBF?UorgL!IT?6GUfH03HA34zINbSs=5auis1% ziJAZstz-6jE=7harI$N^JjD5g-%JpR8wHT?Uy2M>IzoCUklA$;znLHs&k;aE`Gn_k zhks6bC+ZU*vp(TB6GY+`2_%;2X}Q`ebT99|-ac4>tS2zm*@oXt5RThK;80Jt`f*&|k_aa1 zZpoBPUSMYeGRwqoCWy&*BrsXBYwK2jK=UFy4v^UdQoorX96u3(BeUyDKaLmMae&O? z@S6$3@f!&^(k5xX#0~^xHrvT>CJ4x)rwK^t7}Qy_I{PeAW`dZE z$YSEXJNgzo6OdUZeltN#PLjZ608?$oCp2AXF8d6k_8$n0F*ZzhPwn>4bBCEA5^tA4V$TqgU`};>%icSI z%=S)xGeIOiAc4f?zVkJA*-3!RlJJ`eBJl|cB)lh`zHKJ~GE2g5CWyor1(0YhQU^NR zt1{oSqX2n;o8kG*1fjS~0E*()WuAfJ2X+!5vrgeR6GYyS`yDH{4GmSAkir+{;(R+O}u6ba5+71I`_O=PXnIIU8o-QC& zq<1=7YI-i<{)`<4$Se%MnIIS=5@7VT?~U^BfPc;o1Y|am%5NqJ$Vn1_^e1<#`H9?N zCjv6NH0n1KMC2R^M0)%$v3$YK0%Ud<+;1j`#Rds1Qr+SOyLdFL0-4PZ@tX;vad|)* zf%m{C!bOy`_P~2L=_f_pd*44xItRM<{pUnxqkJ%*ba%7fi2 zRre-dK|?B#*)hy-W*TGoPU>bMC@ylNxX+FPWEO?rOc08{lYpW}FO9fa+;3+AGCS|{ zn+am^j}llcDwjOZG5p$&0%X=L{APksyeS}xz>~h6;r{BgCVeC2?7=u1FJObgLFdwhj3Jq*8QqrtJXne+8s%!e5vfIXXWx}s1m90T9(B%T zQW+Yjd5`86boTYpa4o0vhRIN^FquyYriTH8IybCjumB~Bx;7L9=JOK3^l1l<>V+HJ z8&a8G1`6uj&1$I(1wr|S1SrR5bGax!+^EInSYLl$kKOq-nZ*np)X!ovY*HBtqVppO zbe7fQ$+pKt(-XC9Id-4c%q(Hppsrtzwq1gk%1{uSdnK^xsVF$^aepbp19k3}k_;B0 z!wc%#P!NwlN#HS5Z^ToHvv|SuF?O;oY%Z$SqDHM*8P7Iwv4dH%JOLUf zU9vm_barVF4e3aWx^2m_S6xn4sJe$3v|ri%z(5A|bGV@&3%Cr8x9rtzHRxL969amS z*$wV5XAej9sdzl{th!+3p36|V)JMf-D2Pf?0+prOgViPX=(PK42n4pyiSb5yW^%TN%N z7f7Je?!Iz`vS-a@4HFil&PE`jqx+>IrIT~1ic2%frLqRmYEP=+2$GNY>Ff^7B z-@r;`_f@cf%TN%F?+BpL()%i9bz^FZl%w+)FF~D+g|Ptj64bk5;qU|cPb6^ZGaL2p zmB{lMD5$e}U|4`cL0uaPa+cpnfHGKVs=cnnVLTJZ3m7h_v(ADAC>PX)Im@EI6_7l! zG#!)m3=`DZi4qo|Oi7>Uw)4X_uYV#-$U5 zQe06v-R>Y5KVpQ;7*XdAr<>p8GBmz&xo)XJml_`(&{xcM{78IQ?Zr3ZT%^!UHuCAv zoXDVgDA$c4JC~s#G*?SNv-D7QI@W9RZYEbSLksF`9E=613!~l@2Zwi;0}_}VQ?I3? zL9SoVV5PD%MJ(Vl6yzZl2{iiDZFbr6RLQq_btSk6onxTRo_WLqE<-_3ZU_h^aP+k? z++TfG^p$>B^)XD}jXHaJ8w=3B8}-hWKw)E?=Lo{GQg3!gYBR5$e%Pp0C)N2vcW?d# zh7am&f;$$Vd{93s!98dUdP{gd`cCg{nFR~<_ILjDR&TRWDAi~3XXe-DS8HjV{=f6b z7UVA&ThPp}PyXxL_J1|=7Zz{Re*>t)gL;B-+34tKxzZTTjc%@%CywmjK04m4H>%~R zqH;0nC`Q|wl_Sk;*x3roB4~co6+;HR!@7{)f(8nanF{W`=TA&caF`pdJYyUxtX!yR&Om^ znT%(2!QOf!TWhrcwWn05#4}?{T7BdBY%QxM3AGu8ptUGgR;q0A9jfZbmK5Kq|DkRS zYz^1rlA24X=lb^G=M~?k{*abclg)QoWc?9K$Mc0!PNmn)TsISn>RZ)s%ZsLZYVlU} z8{7FJZ&Lr#>fgF^V`j@mZK}n$C}nK8c)R+CuJ|qhOA}ZW0LcAn_V(`inY~Y$g(LP6 z_3aNJvEql+_cc|w)lK1y@q9dfFe*$$x*JkA3>4~(dZhY~ILcKIRZ7*YS|n`K8MMQd>GCR&&sJs$o%TXZyS?;GW%W$I{ac2dzvUeJho0y8Eej~6^R=>XLvysLu)SuayrM=igZi2DD1(E2aFY((wsx?2B3lG@)$ zY9C~_5AvIdsQuoX0)!4njRcKqBdaE)?Hi(86dsyT7$g)x)^|QweZX%fg2M8!6ndr( zWYxN^tNt%d>VGS#e~=%|)xY0NME$P`TK~mkdpv#6!;;!pCpx=E0Fc*lweL3*QTuCy z)_$uemu+O-t^cy5{!bwF53)YOVeNhVW+LkU;vn^J7R(>$RjWHf1RS$w%Y%pVh4Fke zF;SkM}_%#M9vM(4#K>y5ai^>yaOi`9Nov!u?YyGDX7$=A}4 z1({t#@tYZ6*r?lMpqcTGAikg%s@f|P14(;`{0kIY?HEAT`ybXW$!{hC!?i&$1nZJE zhxqHx>XP~=<3?1@T~cqVZ6Q~O^k{pBv^$aYZnCxQff?;NK-L*kv_ndkG**hSR z*_hOCCW64NK?np3h9*P&b!P=bi#Jy*69t@^_1bp{_a^O*&djb(WPLNRHTVEC_4r^{ z%mT6=FOzm>rUgr*s8A_1q6j*o#rB`G&+Z1kp95xee*t9cMDgIEY;95<8gxw*!CpN+ z*!8yrnXMeZnQ_W@>y{E|qWIwut<*$j#XUowCBywmTcCpy$of32wQ2gzMAZIg!`A-Q z>(kXfonAogYL{}=zQasJ?cW)s_I0A%TLOCgdxHF}v!Ob;w}$xZ;xJ5-B3C3yiS2a6 z=s;7Y$f;aBcZy_YWTDBCy!)tDbL0WYk+l3;$e3*Zl9O7s|Cbq)+_$f3JtAHHB=Z67 zY)Tc9U^3$p!PPuAfIEljEGb*EI0uqlJN2f z)Gj-clvSbB?=7-AD%X`#*O3KK|K>taHo$ z;4o7WRhJojn(qD=JX0qOc7D&I@d*`cwtvcy^QWkA%J!N1b~D#cahN4PMS0#a>NjC; z6V^?J^e_2qb(40G)^23|6?J+oDrJx813E6Y=Opckwil3(<=Fbo1hQQnl5O#L_3h0L zp#gmdirQ>ZsjF%><3jYP1d)oTyW9_CeJ0oHett87h|ZRTXi;v;<2`%r-UG5qtGA=^ zn+Zg7kt8(z`9dxiS0Z)utlN2tcIN?E?_gV<$8RQ((4!;~TBMGxxY3joG|Nc82D08i zv!d~v2}JW~L1>CU(DyjHMx1BZ$$+f46|7|ZW&+9Vmqez0*;5KKD~QiP&FdGLTx1+( z0?9l{02#d+@Jlt%k})~y5l|y~{@b2n3?&-_hhqH#GKR3sWcIm9he9W_FHYq1xn>Jy zbaDc+eo856w)mThKh`PrF8=}3lZpR;yvFlG9A?IU%9;`E41}G^9um-6vZDN_DxgNN zt+qxg)hKbEC>M`sk3g`*c+gAi9`y1=emK`~XOcgs9t5(UJDP?or<=Q35~g9Dnl@g_ z*6W3df>}*rD80f?=^qkVUp-(Qp23V#0$EQ|1XB7tNtBkU{NXupdX=5it%>|3juV(s zP9ST%38&8DYI~KZ-FkehJn$i>l935yebB<%bo^$f>AX?{6EQNqUQ*K;m@HKfsFPq* z)v9`Iz!f||U7v49beRW{!%QHecL_iwEBO9L0X1TRhh=KpOC`eOqKP=$XsXW79X;G; z@9#R>UG~le%xHfHvZcRE-cND)5ZKag4&>7~AM%?SANsJEZtgRJT1@dL=i=08*uKm6 z+N6z8*dXg$q^$#x-%KExuLvU3TH2@{jLO-%I*ae}oZIbuK-N3ERz7|+fqcFviBJD{ zsU9gtaRqx>W}o~+c0wStd#QdifrNf0iO^6bKBVF|b>&a_K&-C%ak-_)(5k!Zsa!;NNJ5EN-Nrw8gY55T2pHZDIDrsc2ppr z$)WO_2}E_SBvd2G1wbjohP&*TK-Nb}tRCez6Nu>&L70;Fk6Z!WJqa0_9tW~MZek_l zHxo$a3PEIAiw=}LQ&y;Bb*Q8(a}i{ALgY6Sh-QZXG_n>v!LDIo*bqiw8~BhWHuYxZzhn?q$EO13zbM+NA6n$ zJI>ArWY&ZHW&-)tCGknW)qFyNXgTTRK-PE4StBODnLtEOm4v9TJsHFDTA;QUaI3Tk@t=~)_oR>+0)2B~N<<;|wo^6;lc0eGr-s3kD z2dk5Ozp+#8 zpg?A0Ilq}eP#=&4sxMcowp~d**yg7K={byehMf_}?7pVoOdzB0NMbaQ-1$+cO!*L z+0f2!CXmuek|-^0hj!jOCeF3<0hyh_`ppFLIY$y7|E*%{>}Wt{^V$7o0?}-cgk~Vw zYE6-wc7Yuc$ZW*zHxr2Ha!H7Wk3tk}@MN@KWTyl&I|lj91X3E4L@E6hZX4~0KxQ4u zZzd4YHIfju)7sl-cic0gEA4440po%+oL(z#U=S} zvm`eCQ?)oMRVNGP{y%0hyJ7bpklDV?Zzhn?dn6I+GdtjJ7kZlAg+OM*YrmO5K>r~L zP>%-WX7fzD^MK5T)_yaAY`!3Xjd^}9?Dd_$*4NI4dWj;r9zb2*`6Kl!n)(*cZ5j11 z>bOq+^j810dJrxjm!|wr;gma1;e1=8h3n_09+0PS@JGT6IPD*BQmgR)_63}-FJ05x zkoMu5qwU)7-RMgM@xhzHTs)Cg@1sZRt(N)>zI8J+-?wglu277P6n~-rq2BznzIF3c z^@sX&*3o6vYlFYE$oeCej_1|eNVT{!bKOi(%0E#*P`S-t1?5wV-%`J}{U*r&sDEj- z^Kc7VZi}$F1XbN34Rwd*pjWvm^FG;!AVWuu&?t%!Prr&=2UuAY(@-&eC zJ;lrQV{`3aGUWUv>bRusBYnTbVV3+7)$RO2{f?mRY4Nik`1Vx%KlQgx-im2+Xg|4_ zf`hu1#l}I;fLtxYVJ3&easeFLk1wX+pzhhXanOlXE)EVeIUG(7&*A7B^HR__&Q4>4 zhlayU4vn<}Xe_GhM|fNyj!Q-+I`V+LgahF>lLO*n2_QD@-3B0r?c?={2?%w^XIK9O zvVO11+CTZtG$_;ZJu91dgQIkb9?ilvzd_9T1P1oBeO zBm8D^NL(X;#G>{A9M>3evK<7->=@xUlLKNr90;?f6=db*PkIvf0ME&jIh2|~c2BXF zU0Kivj&d6tjkuB=WNEj`Ui*M~TGA@%RJ)wWI!Vl)VcmdvDg-@%<~kqq#+ ziCed9##U?a7Mazx~B= zV6$}CN$qA%9B$GdhoX9YZ$umjHcPi2sNKv7#NGM=VZ3-} zR_F>lR%$mdIci_@@tD5k=q>uF%Eh9+bUjK?6LMp5DA+6=_EEc;6N=v&5Q?#M`BQKp zHW3Gc&C-QnYBzHN@tOgF7>8Ao!I9Wh90@i{_j9Y=%!$N%21Fw4^5I5tIM^($EYxo1 zgyZifgd^ngVczZHue;eS4TsvzoN$ajN88d0y<5ey+S-uAwu;T*tzribL}S)LsXl8L zoD<0fw`9NAfKyWV_lsTQn59)HyZQHvu^B@*PSW(2uNEhU+b@=N9_-(Ff01LBu9;*v z|8iP3f4%);dFPq_?_{%dL`3c8<=S=~L-aucj^;+CvJ3s|-IM^&5xK+EE6OOG- z2#38xDZhF^(Fs~94hNg1gSToobHcHW3E>!WMR8D{^k{K7*o-q5g~J1CH*>E>^>o0l!F*PSeGGa*^ztfL~mbZB^BJ=I*6%uGbb4L8xV{d z`*QMZw9is(ZDnEoTe#WWENfS(-OP!{FARvs_~-ylh^-2hH(Gt`Z&~Y|6|o3#Gba`= z7!Zp|$Q!-UOuR3e#Z_TJVitQ(}0kKJQ#tau~Zz5oFWle!KqlD%$y&Koyb$dT4#{QWkg!=g zZl!iJCnQzh)V83)>jc&1u#zBDCi#vyB5an<&Z^zaiO5s~B2w=}BwQ}JN*oh5ONUC- zZsx>fj=q=}F;sG+0R{KeAqS#G?DQF2c%SSfOaE#(Hm@(MPt|T-veZ6;wWYpf=@s6S zy<^eB1mzTQP}n?M78JFcIYH?(AShFY<`pz}g0sr0;;68Bjw~u_H*=!0(157a4`vm0 zOimNWgw4{0Pii-FVzQqBF{#2NT~l!OI71u?HcL0+sNKv7#o-2oBE+pUXNe=hX6gP9 zwVOGSNE#4{i71}bcLbdyjs}~h-D904)6N+o~g+lM{v?)jez$?M16iWg-b`*A-}7H#o2Im-z26WAg^G z0Rgp}msjsaR?)fo>oI+Kb#UyAS0jFo7f-WUx_Dac z=H-epMa`+A4lkhPM&m-%gj_LB-|u(RVR39CrkK$9p_3#0U2!%`dq8S8FCl6#o@SX4 z2x_+k_p@#m2ZGJgK&ai!3B)EQ1R@br4A}li90)c`1EF>^ClD$zDVdDCcHyin{n7MVaZ_Tn<_&%w=<&j+K7{%Hl{s#Y0A)6P-x_W9i zFTXB9cG20@IZ|JK^%`|Gbjm_389p8x`MTb$w{#O4|9gO&Iq~QMtb$s+yd&D>j3>eKI^IcgNr%U}4<59rUuPl}5t zHcM|wQM;KFhuaN^!{p7w#le3$FY7G5oR|MSz|EXE+;2b}#)gc8|CU|WS$bA6|9gO& zIdOQ>fH)j{V7UA+)z^+?ouzB(_}>HE%!$Ku`r=?jv+29~e!tgjniy^f=pm<;^bgsx zS-KBE?dGLM?Ty$!8W4@pLqHFUrw4yPCPy^ARgg+xsQkg!sc~l!wzaUa{}^( z0Rahdfe;Uq`->_zOJ|qVZstT{{JGkeVCbD)+DBUw95%aDH#oc0vM_jl=~qtK;GbXm z&@oHrm)Olezr<#=0G$-+4LXEfjs6?w!T$NBcOA3z*aUX-4}!4yf0$o-)_Ji1i`gta z@kQk9_P6Q|?xIvbhm>-+s)BQxAI?E6k)^!GqiJtB7VcNEzCwWeX&*FO!*X6Znh z+Re)r$LUTMXXsBB>S@?Ni;E{VOQ&JgZsx?{0t4cpoQ7pV{qw18mQKT}-OP!@6$ZrN zr~4?T|5#A}d@7rz)39ncbK-El0dY`HhqIvm`BXMbJKbtGbK-Ej0dY`HhqIt-`KH6! zEbU9E-OP!@{rciyMDz6oeZSvpzK%~?spUzskrNy(_&74rYY(OZ!!Wi5e;)t+Wdh?{(&76q5Wk5tiOoabY90@i{ zC&JZk=0xHn10oS__>Cu5{dESLrNeJ(H*pWlQkIkFO z7T2lW%n8S4283f`hVM=eKGB3_-9j9V4n;Hq+{}qaoBn8|kC@>O5l#~CT;e!%iQ^#M zc&v6aCk{L5ivxy4s_}T8GkR<3`~6-a9co6edm>lW{wiG6rtlnnb+~6AoYiyRNV&Jt z^YzW@d3M>fp7(*Z3m307YV;`lm;PwTZb9z@_y#YVbn8wg2kT2FIFe0!HrZF-?{^br zFGb<}EsONO8)i z_vOBf;@M@F`FS4*a`f`bw-GU&{gG?5C7FWP&(fIsiLb~u_7k$d$=Bz?f7;L)S*$s7xRzuUFTmRyE% zG$$6184!!5TZfE=Z`GwIwKQM2!3uITCla$aWnJWM*U?{MyLJq9I4N_;_T^9qZhBum!&k4nUoG308p_ejEw3SFI@?$w=q|ShljNX@Xhxo+D?)4<0 zaP3vHP`DrP*J-XDj^>17T@ymFXSh&I@D)_9q7zl22y!$h6dRflioL>xqDB&mT2&~5 z9L)(uqY0r{94-{T#iCxpRj&#~kfS-F*v^1ZjL%vFbX_jTLp%x6y%kp&Q+!#&wM#oe z?gxS#&56ez`r@Itd;SM)jrw8T^U1ya(RgGaYvr@iRK~lwMlu)D$qCax7qWuwvURiF zuA{#tQwB!bT!^g^<$BqaN*LK5U?PDsu-ASB~+ z`K)qvgPYP1l*Ho*RXlE5ozEG3>6n*8j+8_sql!q7qd5_|!GMT_ zUIVnPWL(j;OS|pv2mGyZ*A7Q>VsfVeF{!op@Y(a1ij1<8Bq*+3c9KrF`vHGYT>Id@ z1nR_T?~y!cKu{(R1|^b-_9-Sr7II+fd?lD`m#&y|KM>?-PHuVHfWXu`6SyIQvPcpX z*Dl>S<9@&&6xUv|Uq*Xwd0Br@lDnqg!}Vj5A;&#A5S#mQhil(LR=>L+2y!$hcl=3z zAc~XietRmBva%s^$Uc(##I;M8AG;s$*C(!haLCPs`s70c0#nx;?Xh|z{fR`*QjE~= zD~XD0m))_@?S8-?71utvDBOgo{L6r-OmPRNLv7vf-cJ%4*Dl-g7Ru3_f^5S1`j?_i z9a4~mS+aEqhsM7G*tJUs>)j6oIhqri=>~*mXdTm&iYodnhe-nC+NG^9_XGaGxOTHz zVRH=#OdaOi=vr}mpHJ}I)-p*+=GvuojQat99pl=~s$;e=ASyLEt2YrQCPzqO;@YJ{ zF75~XF>&o?4Y_m}5R>X?Z?D}xRuoJe^6YzS)*Y48W5Cm>Y=YFN5fxDyLRbzANK=6j^;GHzF|N# zss^IjXkT!}?yBlLYrWgGOS|;$2Z9{U3CD7M;n2Ih>tJoYW~!)Or9r*|&XU@roBl$p zcarmmT+^6bklZvm6V}avEfMvSIwk})aN)64Q zs{QJkkM=Y_X^7@MWzyWIv;D|f#czzX&LxkTQ`|_d?a)uUdfGBV&ueQuJ*(*Y$llzV zv$#S0&6OA7_r?{>_C+8n+X|OhuvU4460ZOEnM_fe$ zvvkHwUTcOVFRLuNH4J?*&dK&uXBCeaS+RycX-;udnRvrK?1@=)8c)SZJLhRtN>3qg zEyUWC;E5r)Md}+?Z1xerKPfK7LPAh$C6at$8d%Sm7>M5 z7J1QU)7O&Ji+j@7;SUlNxC6KuKWOj7F(2k*M7?2j=lg1t$FnJ*hA#dDY5jZ-G`N*f~>@8(6&= zV$o+RL=SM&Ec&w*79FjHEAUKivsNzO-&)~ciaNv2(UT&%D7BeW$5@fUye+8!))H3$ z>pA9iWyy@){1pJ3aX_BG0#Fw->^$3-;@OO&;DyDE+RaPxf6$T+-q)25s@-HjN9zR@ zHETP;_6L*AxSm5OyxL8(@Sjy!czs39n-%`0UQtsUi7dDBsl3$}iEtv56g0EM1Adn0u@!@e_pYI8zwG!=2ZqDGq zWj!@(RfT`4m$)do(+Z$4$$g$Uxo_y0yJU%s-TcX&&4bB(u%o9=@9gYPxNMfDceR_B z-e+k^?;Gn%1!0=+tR9^uRMKZ+v$QdzcGE0+bA?6MSEtUY@GtcWnTa&=Ndrf!3Uy;~ z3A34F##J=JYK-0dB@CMfhi>C|ps^bbZH zDn3@xqbvFo;#MMB=yg+oWH5~N^N;be8OH|%$sXXQS@ukYW!G1m9$MjF>eZ&>Xb6nf zBn3@^w2RA^?Hu!gvedpkB@{{kHSmB{Hn7V`}u^btKBrK{$7Pu_g0`+ZXwF|Dmyz&BgYrg69<<& zRoh32u8#K6nVoiPtxmiEFz#e*IzzN1WA{4d4p|aoH~$(&HV<}A23u=5{mqIDMDt00 zA<3{4e+BKTD0v|^&sMxpfSZ@*KSS!Pc!O>9g_WHt_KNq3RwmXTPi5%5(4Yk=Eb#hb z-KA><)NYz}pQKCoaY*pNnkSrG*6Gun&D+V+y4p>%=Id$GT)pCXxudziL&#=y?}c@$ z+D)_O8)?#SJq=r3fga2sr{^fuV+hyZadpD51;o02!<9X9r3XB8tOyu)VrqvjMhlWwsY_UU1J zY}Ve*QFYNOI%?kD3$rMj?azkYY%^K9*=Apoeeu-ve)xm#ZxL>`>85`~^Tht_WaqQA z*=B!%nc90Ln{6~ibZ>esTHlGUAZQ`7a14V zr5Dp55W7V5>5k~s>Ms#}JIA~+Re}cwxKXRW#EriVi2w`ID`o z=xoNko=|kPn{Ltb+C;DEi>|_wrse4825G;xSbH|3pDWZ}?WSA%leB3+fp&b-TH#o* z@au|&XET;4358d?=@!1IP59XvYZcc$k$kGpqLT%(5i1%?MwCU+dg5rXd7dm9YB$}{ zxKvv-*0DoCxAo+sUm*}P#erZm&SMs43$>f>KwPUW5H&b7%W07X7fG|l+Ot_&53AjD zYk#XY?I-mY@&g68K^!dq9I^atMzdC!^wn;<<-bpx{MB^$L>_mTWrL-k>qtM(*P~!F z7Ss!+SG(z!{z+}p<4zX~r(OlMKIb{Q(`uWNo*bK{C63xnx9-nt(|tmJHl=Rw%ohvK zW-N0OCU~`*ZsFh1Cj9t;Xlw=cGXy301&;1C>|c`L*^G5yLfzGFx^@4HHr=c2rcJQy z8#=PnTCft?*^Ip#LfO@Bx@G@do9s2V>~=pZSo@8{+Oru;!i3tZ-E?a|?jl`7Y7-sp zGXufOZy{Em%{bsgsJz-uxAIf8DPNsWrg9+?{Z?Y>*^EQ&h0?3tbW1;5oAh-whHvlE zOl2a90p$)yeg7yvn=!dARA23;Tm4Ndt-d$kTfw=@NQLV(nv^M>!dlN>s$@l+Nu#4v z`D7%QTHc!)Xy)=(GM%-9lkEJ{S$jHWX>VpnacX5V4ry?boxO++Q!43S)f0l6ne0oe zs7l&9Y~D(i0n~0@253V{)G)7I=?s85OM4ot#y+5yH=cyhzCo-#j{guAKCFF!n{MqF z>(jm`nx|3x935*G+~Q#ow-sx@uR{9(H{IGFtV{d!)7vV_nw`YO7n`eOnycM(Yu=+x z^Apw%r}+e5I%iF3!Hc+z;eQWs)2(@*HqGBw)u}9AjaYN(gss|5x8|#~X}A>grd#t)$I4h!=`3#jO|E)-X!Y?c-< zYB$}&U#?5|^j}9S68j=YbN>Vso2z9NsoG7q=GW=cyf~Kj!w0w0cNZsgHcya=u6ENc z`fZgKUB~cDM}>dnu;H1t1;aCRYfSLi%wi|a`Nw9KJ7%2NBkYy$El#Rz#^i;Q=Dxz% z4ExUVwQ1PAx#CR%+`QEP03uZRv6&|;oG=F0oQEEpVUf2KYmc*7f>J?%n{Mr&*Qb5h zu^HB$b{Uq8NwRrwh4uk%y0w2pm-gzhnFGbe7n`MHGio>8n*Ujw=E|`d7SBJ(!e;5% zjM`1N=AUWPTsbzw;`zsB*evZjsoivIK6;I&H3n(r*bIy3A7o*(bZkcLrd#tmZJH~` zW>`G`APbwNV>4q~&RX7QW%40rmzIiaQ#MPN6{y{GtG_^-`qjC-H4sAj zgT>Ob83(-x8}4d1-O@K`lfEuf=%a0FcH&nI*c>8OpUu*QuXfX|{{VY$_QhV>50K61H^IZeAu>fz(*};o+5)&IDh5=^l%?u~_>?h4uk%y0t$=oA#4e zrgDWS?LT%7`f;|1qEekBAe`6qFb2ihVqKGhdpu3C%GFHZSvmZp5Qn{M52*QR@IjJ8UI zI&k7BvG{D3?m||(=@$P1ed6b6DMTpoj~0v1W~umUH{IetrA_>=utn`yvHEP54ymc# zbgTbjrPbHb*X^tDj~v$5ox*+HynW&owXK6!Kb_>HHh(wwbjRE&Yu}wLPNZyJi`<;l zHrUM_hZhQ7{m0%je4Q*dOFOt~H!tP?3DK#12ls;tCyKAJ{)t8O_f**|?cl22bZh^G zHtol!GIYR`qF;NeI3uxH+R9YB>DGPx#hTV9gd9>nO)NZ{r9;YUH{HTdtF-Vss@b-!wSx#c}*RtPn%q_B7mfifP6|fnDUQS{gtYt$FCi8c! z<*RGijAQSGgUM<)FZs_!I;?zMyLqLPzc-jXo{~b}fTE4V0?rfbEkvle5LG95zb_6Vz_H)jv#|`t{L5Jk=laydf5HnlJUUS$cZ3+D*6q32pk8Z{}OHbS9|&gOMjj?>9bk7 z7+me9Tl#CXNk64Pw{z09B_Sj5EpY_cjEMnZb6@SII|8?8i@;=C`kZxCsB3#JbOPXC zqs(Sp$t(zcamyBwxUL^FV8)JQqniuf#V<+RvL+ zJY3`8P3I(rNuS7ZZ$4dPy%0bWJ=b<#P z6VoI&F~#$%2i)9UC(hk9PRT)2gC)tS-Z8H&OHSaIjimp@?CsW16$u!5jy(~$wo4+_=Gp@n$7bjzTb2QZ*)NWzt zwSB3Z&DaVjOxqKQpsSjDyhX@d?yW1=QD)dbPQZc+0AldWkHbK0dCsl z-mPMCSF|u`D)-O2c=jkd-IV@I&T;CdVWrB1NG4wx=v9;@GsI=d`i>cs$HElFZvL`_ z&6xi2l_lwIXV77+L1~+P{aqk7V_Y}rO#IGh zZ2hsD?U*-^3C(Ua=vY$ruo>}jgm&9V&OIeT5ogzW@i$@*iZJ48H!tGLD;)8oDi-l5 z<@ZXb=TyahT}8|4C6)SF!;0wYe1D{AXdx{b9hxsLq}hx!NrV|i?Zz3Uf&+`EY8AUV zgxDL1#b&cqY_(hE#r~F7v0Fljy+ABBo26o_-6}8kceIM#8ba(1#bUErDz@6K@?u}F zP3(zLx&cz%uHHnf^;V7gXo@}M^faPt!OXNX7zn;@gUUFjLe^Z}C; zUp#-TM;HFLG+KQ=M(cf8oJqjOaRvX~>F&cq*1}3ZC_1cRKgJBxSQ14M$uT)xk3F>f|DSLyygvJQK>ScGD*F zww0GT%&JOO%s+_1=4lF{1KhL;y?e!ku4t7wvr<26Sc`g!QzfpB_R%S|sb1{6vr9-x zy|`FhFCOZcrA=9O^Vf@P#*P^$MGe-A)p!|uR3^AA;jftCE3(-vU8$;e^OE>Mh)o6S z#z>{o!r%zVBs&qrBeV%Bk)pE>g6hbI9%?sjLjRy* zLRYkHy|hw4YgpU5W^lMExK(|)lWG0?V=TukZB?_IzbVFM%tJb9>8rP@`70*-N)a}z zUMRrLOWb!LDiv&1KTxSu@by~N%bZaAL&;`ot6J@*P4K5GFL;Pnb;QxxKlH?AX{%c8 zrcLJ;E3UJ;RehwRvcFZ$W@)Qh?WRrTH!H4kxK?%4QQF_CX0x8v@?5W+fDLtq1N{@~8x>FUBO(U$AziMQ&ba$27 zO`FV{RbFPCJ532XlIj}>EY% z%BT9=*$>G)ib-A3nsH&JewLz(ZeKSMv;j6dPKcew$f40W??D#_ZZh@J0GnE{^>K2$4i{x$e zQZ%Y*R3zu7uc5cHi`gI1Uy4(m>3HXA8@ij`>R*%oG5rI@;a2(sQkO_R*^!*8*%Ha) zjv1#`-V@+P)oh6ye;amd+(&N9*4VT1_KVdxuQU+NC;5#giQj07^JxE9vl(~g3ExQV z=DZOVC=b)CRXp+h3JV54=M~Mh?0v0?UW$VeX3Y@HcNF^yXn>acbass?Jwl% z*4T(T!A}zl&t^1Pg$Z8mrdRm4H3?tcztYMcolWIw^erg8PZ!J1W^A1m%C2_PEBnWq zWS?+UA=;bD)2^r>(bp1-&Ss1;3Po4D=@tFonnbVRLMEP~3GX1?w-)QpW~`zU>aKRv zt9$ijdMec^oY*5dE1#zZGYv!rsoyMCpUu(|UhSq={TZ6nuTLcU2I%g*+HUrJEGG>X-W5h&6t)JimrCkEBaQNM5k3m zaVqJ96Z(N-&Dkt1;?-_?HSf}>xs_E^t1KQJT2PYC*(~intKIZ!zDSei)g*X-Hqw`) z1#>~g%Tgz))2f#e+1ZTI2VwD|cGD~S{(59j?iwg|)De4KpV(}cHrdr~dc{7xvSNFq z^A#Jde6*r{5*k*s2O@`!)Az@%-caN8-P1>V1NAEq9G(v=mtcc0#sl^CL(4Ei=X+4# zKz-?FhYi$E#u8Ur-W<(42mE+r^ok48>ZN1!$CA>E7o?BFA9Ui6aEyL6{UZjB?a$Ub zpQYj1;{|3KCM+4Fx5t0;^ub~kEwZ!dMy{23(xmj!>@50j+Se)iZWJ{qem%ES$<4kX zEk}Q;c)~gp4>c=c+2g}@T(J0&a;P&*86r4zzlbO}8!3Vl;HEb?Ybq3+^ab=FXI$RV zeMl}2%oJ0^8IH|p*9$X++D)(SS8LL}t}m6#(U47qPH>`Oo1hdwO{_edaXy+*d9|Bf zV>&__P|eq_|;mart_X_yKNu#lJ_B_|BmUL8*OhC$&?jszh)$ zW6VY)sy>Cnbqow#wgk(0h?r9nyh zy4W#ezD=0E*v;SGWHYKiCw&dJG()#&_&aDPf5{uMS=!uGyLl=8w}?%p`!TOpG(ile zgj(l>oDfY(R?y!jWHXle1to<5H@(8Yt4a9racgBNM$_Fv#XF0;sW`E3tLC()r&xA2}of?vTvwK z_G!7)^4`=yb7X+7>UC$jgG%|G#QL)tGk`+<)oyzAZ`7p!Bv=0sNq={-^lZjHSfTW4 zH@(ttr%C!T7M;)^l+?Q&y=fw_q`SmsX$Ma2rdRJhwCPO;^#|#_#L;`UPj5Ek@I_(z zR=erd`yg$4w*>2bgroO7pWbZ7mB~WA)oyzAK2nq3^|9XQKqTM4!pfwMv1lPpP+DIh zF3Z`BReD10)oyyV@71Jzjjer%IJDa7YzS>Q%?l)-CT|KvYfg?DrhcD?mu^Gps2ot*6 zO|Rg0R#x!J&#gXJ(bkaCE1NNh<)pL0Io9f2ENS7;JVm?yQgKPcX6f*x+RaP&4<(gegMprZ)gDYYIT9sn*NInzI>ap9(cs zyXn>ZEnS+gI69ie7R#UkgztzoXR~zrN$sXr^N%!XUX9Z$LliGpiDhTAw0u{)>6QJ< z%F15(hRq`t{ei<8Hj^>E7R}P#hqRL+lXvsBq*Zf`lgRw7njbo5>EIx{`CBz?#_)=h z$i8~3hQGqUPl3&IWCgt1%}e#w-_g*jSql%Xz&-{4(vF&*Xg-#V))tmEWMM-m#rQAXsfgKeoKfq0I1h&)^f$_aoW;qVT z3ToQ0<~xfu-(8`3fSX>;J2h!u70<682v^YTDVBU6h2#NldL>_|N%By=CQj!2i8WuU z&^*9Rujcz{(!45{jH;6P_rMS8sP>!S@mi-mMTkz)i2%^O|(8 z%hN6Lxl6?*1c!!GQ2 zKrB0(rLwEt^vXU(lkDM!h94D+&SvS*u-Z+p=(F{Rp8o6TQKJH@(PtdJ{mZi1ES>*Q zyXnFDjkN-WuO|Ra~dh{;VIrB|43P~3Y1veOfBTngTmNppG zZhDp9L6hoJ zdX-3DCz*bBn|+ZXURrp)NXnM(4#2;-nleTK1_lCs-wUEG*325 z506m0>D51@N&lf6yVZ&RO>yF9vvkvq+D)(c$7m9NZMXCp++44Y!5_sjV6!v>sNM9& z;0#SM7`nMWY#H>9xC~;m^ay9Qo8Ayys3`=~y!9Rt+C4$_fNGTZ&*Ch=X6b=VYB#<5 zU!_U^^|%=Ns$uwxI1Fr-W&^dG-Z0#xDGZYb^$*eU{Xkp>v02&(8U}%*BIHOkXc=bW|pSnR{w4|xV$0hSXROkvClGZDCASUxrU(+oJl8r zBsT}PTs+FY4KKO%(ElsY_S{3u`20D=1!3OPyO%CGzqmfXKdw|t^!6Ve>9aTU##6Z% zonB*+h{;Wp(%;p+dhpV}|003&;{8*~t+gw=L+u6^|{O$u)j##*zu zPI`;rsNBZ9M(9hDA{;2{pzG7l5nd*(xIX7$Q;y$xdMIF5| zWyJq)QpMc#zbWh48%rkAPb<#bjq-6fvhhOB#Rs~lJ?&e_+jEUI^tr`V=~@n6No*%O z>A)Y`nT{Ff=uQ{8O>vV?X_851q5WbPx&3PlLO6Jh38J0>n#Vct~rRmx98ihjCFmdJ@Bn~B?()>45p8q(V zOLE>EVb1G5dbDFlpF^0?*>Airf3O)F^PGfkpOj+fy}C%OzmM(=P0?Ca+5r`m`5K*v z`!gS#F$pewC$*cG`P$6Od^;JGz6J~Ss{U*oC-wz}t4SO#He*ecFkEUklf$*Qk>Q#c zp_$%PJ`&-GjThH$E#inB;+QeiCX86C^Y7H*EvXdQjC|?O^!C1(d^SqE9)}zc7L=IU zooCQsQpq#eyoK!5)NWp4T54Y2j~bbn=sbv2f(EUE!qp)T7n?C>DojagH0b6Zkz{cnLdnsbU8LUejFE(Q;P#7<@o5}Gy-N<;= z7w9aFL~mhLBt*fujW}*>-c%MhwVTOtyTHh}O}1m!6Q$^d2;8>fz_D2xIJKL}fxFVk zz}4G<>x+iUaNCLF#%5{U)NUrn?M5TxHj!HUxWq}(&EH-eF*a{5D0o$7b9CB5aDN-AsxA0%zk5J%xZhAdqSVBWxP!BjVg8&rtK}VYyDVX| zn?I|ud9aN1-l)ocq2O^W_MYj>WNemJvT8Rkll{lMOjdoBQ4{J$HXcWrq>-1T*P^9N zd5an%kwhv>XDkh!29@NtmEzpC$}vj^AdYta-JjdoJlm_-%q$|;H8OfLyy)dDdlzzu z@Es!#-*JlY9V-qWo3Uu#E1(SQ1#DCAJ7>zv&~;N+t*-WbE~ZJq;~Vl zgKZE#vw8)Kj7(A9VS6Fs)h>?LGDW-s+)R$w{zk?t?k_tjQ&f`;l;LIyEoKCzy;!3lg-kJ0JWRRflC<>xZ&{{(^m-YcVFVf%D@1mYNN=H#SIZQOLu9h-AoSHbw&nk>_ERFciklp z7MpQYv9M%RyO|uU+e`|Up)HZqOw!0}iA-^J(u7?^@>6j;7x@d zdMV!xZWx!08$Tq@Z4WzU3;+x35_a?FHa4&4<+j0A|M)PQ#o2$JFSD^(I#jH7^D^6u z=4G}wjZ8=(i$|8;KkCcogB0l}z|G`{{nez18QK z$BNC;SgGAij@6DP#mdk&{Fg?y;hkORm_#hug)SN3c*;rK{%Osp9rLcT3jUYkJi+F{ zT{1(O@Tvs_?7Ob7`eL*601CC6m(BJ@nl@`B@nDm(nb(95v(JKcUmy-wgCgMsxS1TT zn2F&^+m{;#wKh0bP2yPXq=;33o5``tm=r5RYp{Jx(#R`*>T~fHdnrYj3hWti1@>#l zyq7F(vzvbmip@Bg&Pm(uOQ|y%yUy`tGB$6ic#!}%FOwaMXqi=ioo-SBGIS>Fc#|}8 z({phgCY*zZcYiC)THOmqJN7+fNt*rqd5g`sOV>%#gN?&FXF{%j=n>A6P1Mgh5BIM# zWwZ3$Mzx!l%`P-An_X#S5(+s7_PjV^Y;KdKBDI^z5xddIh=rPEenA{BHg6=0m)gzb zc->`Wyl{?u+`CmNsEPlgIB0Bck_AoeW^<GBRk@%dL!+jpq9mCvLtZ4jG#}WFb?# znH;iT85y#W8_8Z4M~uzVjbv&!lOy&!BO_KzH~18?nFuZ3b`K|!G#Gy;4jP*`lw~uu zo5?|Y+sL4W7+1t?XZBq^zTSgp$2Do9HFw?zf*ef_*~dnPtj0M5C}ZC$7|;Oeu9d{i zwd0&RVcgsg_~YiZ?s$LRCXop`=#Q6*en_583aY%yILLhIn?_4IADEf*egQEN2)Q zw=wbHwumRYZ<5oqV?U@cpSgh!ax^(!^NoyG=xf@#*O3&Et{sQf2?OVTz+XVR_Q6x` z%q$?cGAVF|&Z4a`vIQ`0$boW^J}aN>j|UF-cW=aL+_x5_dsDW{#@0PYe;RkcP_)ZE z{D{Bz^}em{o_#A>-gBQH({KGGw7Uaco+?0K{O(gMh?KQGyi2DJ5yj(jRP0nX0n-niY zy8zpnq>{fXSQ&+-!a*ID)Bef z9KFnU9-?SgAK)@06IPf$fE$`dNyuaif4qx>Lp=stiOw+>0%c9zw1VH{14 z+igb1EmR-Cm3~)A%=T2pEXdL1nB8w=%tH48nz&l^Ps6x&>HLlRfgned{--C6ai#cG*28-R=kc@pA2OG&x>vM#k%_b>Nmtg6G<$9XR&` z{@}Ux!L}`R;0$a-?rc)<4DG;uWMn&gLL|~_Wg?N_m3J#SVf#C6N3&hpIdUESoi^8w zn<>0f$-rwt_cJMBd%f;Zcatr2wkb_29SK~_X4!gR_um8DOpe##M#ih!y)j8~G01M> zkg-|TR9mceGdW~QBSTj2g^Z4x?(Gdegm!mv+}JFu^%tw%OpaUL$hg(oo5^TjS=?F` z9JD>fL1Qxx`4)|}FIKym9JG^+3|d{J$68K@x`&9`UgD^+85dUwqo#HCTz~IMOL3aS-r~ry8TZ2rBd2yVIdYd88M(T_?VAzBS@=uDQDd_-zp33! zj@q@xM@@0k`ciS!*es2j+Rfys-D-T)T7p|72aBV|W@*&aZYD?VJ|m;18mC=+h&Wbk zmR`82b~8CvPZ}Ak>6tr~>95KE+>XtTs8n zI59#6Qz}ox>&XSlO_MWW-5l6*@u=ihi$@ibTRZB2LLu3h{tzMO6+*p&>fLE*BIHOkXc=bW|pSnR{w7Oz1Hf6q+?kL z3*93RQz4(~%{92UfoF3K?)z;$uzF2$rt|PwMXN#&oKyVDFIh;oE?eV@47JZX|EsG# z+SVRw|8t1i(ekM6lh}UZtm0uKEOE(G<`g%S27l-WT`_G9A?CF;|BIMouNh3b-xz5z zhrTzZRQp-8iU*CDj3wVcr?|dAz@Z;;rLz@-bhXuZv^^`VCQV(Zu35ImUNX8ieXuxn=zB*Hi#6$W@zhD__4tcQo>M1&4x4cS@BFXl zwh_76McoYgi(4a%Wa8Ki-#apiBR!qItAI)`4H47R#u0*4Tia1>D_?ZjjOk>dYHGK# ztJYdJ)rxgPlU{JDTgTCl){B&+ZZ>1`La3kGt?c^kR6hOa$W^b%36^htv3zVscS0zi z+O6#J?OitclDh`x*hWXPR$q!^GnQHlB~!bVU9y8mTr#gGG?K;f%#nUgG)m|@y;8Zj zPL7Tw9^kv><5V}t=m_wXMS~@FQC%ml;I?%FKgt*QZpYjq3p~5|>t8lwPTUFnV9h(R zCz`Y9ykwe33aUD`cOLBjVm9OQ4q-x6yLkyQj!1A9Y$PMxKhhB%4DA%Vf9HfYr`B*M zv4(7>_LW#ewOiRWJhprqPDF=Vt=%HAc5IfWKeb!gwL7zX+ST;-$D+Lu&n-aiJ%l!duh?N)a6E-IgT6G^woKs28WF8LNa+R<9dl13k!r^vKZyOmwL?~b^3WvPM_ zBmJ7gs^D>b(Nre5zTMY}vcJBy95ZH?ggJ!W{M90xCz2aSxeOKCE#=b9H?UIU+`?7J z_U2S$E1S~T3U%^nY~^0O9RKLO{-?2(!Kd@^iv88Iio1Tj;T7N4%_&YQwG10%sp;N# z;iA1GOAgrkKyN^$K6pT7taL!-DUxOJ*z_;)2Mt0A2UMP0s9`{2%Ub6OpEK&kJBIcM2RM| z9sT^8u(_5p-7{a$?OAf8!JqOZ+uzZbmtDk3%dI?jH7LKXab&C$%Q#Og;{wOLsZ2(8 zqav>)MY4G+xj8c0T?{+2T!7ePmd+bZ3Vfq!;;+VC&%(g+qXXPHuyfOYLJ-Om-w(wqH*j)*KZUay zH_Zr>q1vqy5|01=$QLs-!CIa?^L82HJ*;Ax(VL<>PogWMsorQ$FXbZYa8gqyux#8& zTsCgvn5l^)P6F)aPdRMHS~5-o<&kqn8Dj6s=`{+I^Qc0mS@!C@MY5vzu4)zDORYjG zys1?pO8-?~a&noWn^%{kx-!`XMS7I2}B&>}UvNVI0w}&m1TLsiRt(@vjbJU|1Pa)G`t5``k;}#;Jl4`dKD0zMvl~h#4+dHz+)p8{zAe+&b z5z3}^tAK3ZDT8cYA9!S&fw7T))nQffRJ$sUTPsu8se$DP!4>h&PU!vZ>3touv@OVP z{)(8*Sd{04ey}1Q@4QoRNw|kN|FT(H5~|(2gm?pDP?k2~9U~uIuPRpL-)?dKWwSK@ zs@*Ce+XH2gtxWm%rjdWuVfnYl&cF6Hg5Zq1zc}L_rO3GK=FhlnMh^R08TSZr#$~fK zKCE8GE!HHXIXbqtuRpswxajKwGaBN?0 zc_``D@-<&r8uSW-qziDXfOH>}Q@V-qLYD53?xzJ2LA|$xIO(w&13N@y87hI8??8xUIiDt7jTd3VCAm7eqkgrT7;=GZ6)nO$fuS2z)4U$6e zd?(ueLhvrfjKe5}nT6f_MHHK{4%LbF;LuAI`jl>oESXomSe$d&TrGPUwVRjp_JQhU zDfkW@`FMJ5nBw?>Xf9_dT7XxI)nl`CR$A>=0rjl1s+XXpI4bq76065%sd{R+3aEEf zIo0EJH`o9k*a5lP5zt?6uo>$#g(ZpFtpWlbUw#1-g-k4zfYW^=2rSTSg@6HW6%g?3 zG76~Z!QJSjyV*Y3*o+e=h3QW1Rsq@8ltH#KwO95V`Bxp*UYU?9#9~%X(Mq^STvD@H z+P+u2alp&cJotY3G zx6Uf=@b%V54t~>|;zaL%hOLX7oVPN${%j=QAFlGbv{cC;fDlfT69vfHqvem-$NfP);R|@IX_@c`d<5i>7Vg;h#o;h z#G*%RO$FQ|U(f9ca(UKWtQ%xa!EHffm0JhNVbbuvV9ej1-O-2vOkqevWZSk&CnW#Al+s=1p%Ga zQj*lzj5Q3xT&i{}vu+=jOSkcfEcF=_sjbCPjXDh_s<9bUwnEj^Ze>>OUu9D*X{DAY zX~R%ZTH~KTMXVYIZGuz_a4WNF6Rs;$jZk%TDvo{iAw;8@9#K+5KOso80Jk!WHoaV; z(MH~ACSz|+3ZdIvv2L3vbPI4Rvu<*oI&%5zKQ?25gitQETbboLbcE&dhSf%H zjO4zNZA8*=+K=cP?O7wcpInc_8Jyk=9h~3K*`iK7%CM&sXPO8sX&zaQS(+`_%|HIZ zW~>-=;ygI=QRBRky7SK8K?B<*Z^UNoi58|jwVRjn5=bK>KJ<0e2&X)ZHQDhT8;dGB zp-Y`;`MXnWmR6x^w=zq1e7PhWHzeUt5-0pa#G0`gJMVX%drNhj3)D&Zu@ypJqr zu$#Y1U^AAlIiVh`5^CK_ibi#V8*lt2Yy0X1Ht()@r2scC^?eUP81Xvc=8+BUU>!d( zZ}km0$5{ro;*J#;a%{%xU|}Jrb}O@P_moSw@N)>qiKSyRwiOGdQ@fQ}x<^M?x-t}a z-yhjm99H11Wv`o$=J^aB`<#DwKw47fohmN#u5`>zvS_oLzszGZPHOj;d3;Ejx*5#h zv7WEUV>6CT6Q)14o0tB6i!h9Mk@xDz26wQ?tLG)&@sfB-T6gCkU&59CvQI3nEM`yV{w zbhQ&}fA``6$Bcf0uxMsCe~Hd!Ony4C9_(J!^hH;B+h2pS?TzBp$7X3|p?339-+D+V zBVN^RIKrvV8*!@b!I^s{!IadqH;JWVv$U10b}O@Vjb)NfJ$ZApqnLle2%DvoH)^*s zE4FQ!6f4#`JJchItVK)BgYxN*#L}@Dd#!~fhuW>o((PU@>AZ7If>gU*oJH9zosw3& zm07g|M_9Eo)UC5d_7#WKt?M}#?{Nkx-QsG8T+*9;$O*Z>ihac~H_I{#yZNhFHe;!b z6Y{|-HdF#+|Mh)6SvITQCcw>0fJY(@BVN6(7}@9!_GI0w%;J&dJ;5a&Yee^QmCS{) zxmyv)0JkzrcXYX=s~O1l4_Mi}eX&(gsrM^!smEq%n@;UkX5~&TmvVLae!8|aRD1Xt zv36{hu4z@fm07!Qjj(oQs4;p*_7#WK7?TtI*}f>X-MzzCg6fIa#r4F;j+yp}h#OSw z=C3E%j7Gl`=)rnoLe7fyXX5T4tfVu+U*O;Iz-H;pzuL`9cUM6E5w9q&AK9P|Rupvb zL8d?(sDmqsKZ~;~o3V3W*eq7Nm07ks$|V~eyn&mT=)?#*h0sZN7h9ufRUQ2o*unUV zSV=ZZC%M&bWmfX%?^v}t#9k)MECA^)eRMOwEJ>Ld-vr#crmVs_V}N6U%Joc4Vd;9 z&noWx^)_IZeA%30?a==pwhi;^Tu(7hdOgKwq{!lN>3`r4TCpbFg84W4M+{O9{Va8I zKNpzs6{0K4MpW!i>A|$saYV#U|AoE`ms7ljKk+L+q`wp=L?Y4N-biFk`eXWEx|!k= z`UC%3qD8Hvn_mkyPoO5i7hliqKjb!eGsTFCS9N{4#PhPJRiMn)p4=t5Px9boEZLtt zHhE_9qU3jzKTQ54`AG8D$ybtZB|lAmnVyIx_v--jfGvSeU?H#{a5%66$O9(<%0VM1U2*F~F(7w}9^e*8_I|KL>sZyafCS_z?IPFkw7lIxrX50_XsC1@-}!0STZ0 zoB*5yTmoDJ+ydMSJPteyybin%dT4C3c$(0xxi(>b--=F{lG7P7l1c` zPl5jutYq66YtC3xoEz^yI)mf%-7Bp7qM1Upmujr7c8m7rtTn}XO;%4^tg)@7r>C_e z*3;G0*qZ2UZ;Qp^(OA5xv#DpzGAi=6sHQL83D^@@0z`o{uo5^0I1jiS_yKS`@Br`> z@FMUw@G0;mFtLWP7BCOk66gdL0{a5r0G0ze;6&h?1S`EceS`fwH%b;Vs|(SLv#im+ zw7Y@2AGv%qlg|zPweiNTw&qT&t*I%|(bC%9)ZWw9+-&v4I%6#@T?ziR_uvW70KW&` z0X_l#1Ju+K)&@2Jwg$EVb^{IojsViYO5haWJm7NR2f*#X0|YDm6Z%&BSF{O!g>#o# z14V<*B()ms|82mH?n8f7EE;QSY)T}W+j=_MTH52$w&sp_W1^|Ky{)aOquGh!CwSt- zI>J<74zL-}2J8%U1BU_@a1?M1a0YN8a5Zova2N0p@Eq_e@HX%<@Nb}Q5@7}~AJ_`$ z0u}-L1IvIEunIUCI2X7SxEA;^a4+yA@LS+D;632)!039yBw#&YBcKV`9@rCD0z`p6 zU^Q?$Z~<^7a3gRR@DT7c@G|fg@DcDOP&=8h7BCOk66gdL0{a1n14$qcoCur)TmoDJ z+ydNBu+kq>XzUcw8a4$`dKj7jtc2ATZ|$)5AH! z_(eSMUEp(I^b|r3ur{y(&;)D)ECTiijs*IFRlv!>xxf{`b-*3K!@w_r=YcnXzW|>D z6Q&ZT0P6#r16{x_z+&KFAO>WCV}Y}QOM&kJHvxA84+GBtzX#p{J_h~`R8J$U3(N-^ zfE|F{f&GBPfh3RzP6CR+rNFhot-yW2lfX;B8^DLa=fH&NgekymU{jzK*cI3tI0T3T z{lKxnnZQNBHNX#nyMf1mXMo=W?*N|w{{gDkBFq5h16u)Iz(Qa@AOfrajs{Kzz6E>- zxE{Cz_&M-P;3eQsz=y!UfC)1Q(}B6b*1$HvZovM)G9U#MfRlkDa4B#ta4W$|zd(u2 zPTV_$PTZ+nESsVpd^F#m9h%IeRcvDYfV^@2uv!$b>tA!GMSF_b=*~$EI zJo;teZQx_z-$32kgc-nmU@M>t*aO%fI1ESuS>Sk}2wV(Y4crX;40r-~4)`PRXW(zZ zsC5XFfpvieKm)J?uotit=mCxbjt9;Fz6E>-xE{Cz_&M-P;3eQ4;6vaGpn6@xG++*} z8PEpo40HpB14$qcoCFksOMz>FJAnIu$AI4guL17?e+NddN0 zFaR6}oC#b+u+ooF80@6b6*egh=>`wBSi0J*SZ8B%PftrU-W6+Yq7tt;))Z^9tj>0; zv6E6k`g(d^*b+69Y3Plvwz4G+h0e}IV{1!$l*-bko{p}z7OSnjrLom&?1^_p+oG*@ zQo9d7?HS+|;Cn~Bi^kgNC7YT_pJ*)F5%1_Cb7N;qyv3;o&coxb25toI1|9}}3;Z5<8~7CXFHk>= zunsT}*b?Xj76SVL5g-ZVfD?dofJ=aDfLnljfyaSof!BfefzN<3vkCRUOkiW68Q2-< z1`Y)*;3(iE;B4SR;40uI;BMey;2Gfez&pSv!00)I8ej%657-je2G|Wa05}3j11o`3 zfD3@jf$sx927U%S0Xzr%0r)fU4`A&2gvr1xU=yGn*b&$ZSOP?WK43L)I&eO41#koK z6W|fx*T5^l{{f!@{{?F264n700L{R*z(U|a;BX)bW30vJD^Faua0 z*a~O|76JPJhXDy73!DI)16&4N2iykS5Bvgn8F&+TANULyvjL$VmEM{z;VD? zz#8Cc;AY?+;8EZ=z^lOj0iOc@1!^}TtOG0nnh92VA^po&`!!a^|7$wCEb1_I#oIc1 zI%Dn4i6$y&60MCGFR+>uolbqf2cCHtupGz%Cj#FDz72dI_%ZM^;0fS4;19r`fqwvF zHYL;pGl7kPW?%=Pn_#7Prw`h{D@1e2iIOlq^t8rfR-&_mPWtTXq6|Z$I!#UO@y2+p zwI`8q1_xqz>T2M0-~!-s;0M6%zyrWjz>C1!zz4t=!1&Dw(}48}R=S6b_Pe*JbGYcO zo1(3WCaa~THP#htv8Zd?+1$|-k9J0zW3e`BirMY24m_{heqK8<{PQqL9f=I>S~NCM zGH!~uwf6KhTG1vN6| z*b`U+#DP9w6>u7GK5zwa18^tsAn-KsGVm7g5%482p_woZm;-DEbOH;3{eZ)PB#;MA z0*b(;z_q}wz|Vmvf!_kJ0q+5S2S&FLCIPd7jeu5QdtguC5FiTl0mlKS0~Y{S0yhG8 z0S^Jc0)7X)4SWoYY9&+yGl2QPRzMf92-qJu5=a3B;56V|;0oY6;7;Iv;7Q;);19r` zfqwvF+X$0^S->Vh3$P=w7&sV+0sX+Sz?r~Bz;}Tk0zUyB0e%g<0{kEFDezyQww9H? z;4mNoWPuZabAU^LYk*sTdx6J+XMxv&_kquVF`a~ZU?#9J&;)_ZdVmaY3~&Z; zA#fFN6L2^1Fz^h)N8?ZCb4IBzsz)`^Qz}dj1!1sWgfV+XmfM*d1Go^l3b+Zl8+aJ_74SRYZQx_z-$3>Dgc-nmpb_W-76SVMhXDy73!DI) z16%@J1Ka}K3p@@y3%m}z4}1oU*?~|GtOslaGy>ZKy8#CPM*wMHC2$IG9&kDE1K@Vx ze&A`~1>kkyec&@-%#MV5U?#9J&;)_ZdVmaYJa7hZ0dOU7BXB41bKoi9Mc_@~ zFTg*5u{#ka1G9k5fp%aQU?1QxAOU276M%Do%YbWu9|AuC9szz0yaN0m@G0ciII-z}P7j6jmSgPX#4KW1@#9!YwKzs9)RF8tv$aHuto*G+(>A+lI3xbutkbbZ&eq&%M-Pb!HkUk!bw#S+ii8u|dH?=f1HPX0s ztfR+j?rH0cxg!Y+@vx;p1n31;0jB}y16Key0Cxfp0#5@k18)Hz0sjRi?nYP}*Z|lX z*ap}QH~@$OX&?uj2z(RxHt>DmcHn2gFMt<-H-V3Ue*$B6C)5KofsKJ?UwuY~VuRJHYk89l+0lUji=ye*!)M{soNRgD@4C18fO&0tgZ|fi8jU(R!>t`XIpb44PC^#Xe`Ugfh~CAB48ij8^CfP2b>6e6Sy4sK5!dwKkzj0 z0`La#7vNvOxV;FIfmy)jKnt)Vum^Au!AdvNr$aPGGPD&$^aGn(S~?m#WAWCu&h}`6 zerhz))85fZEv1fDE9#7C4B%;}0N(^I1+E2d1?~f$1YQE(06qZz0gPEpmsst+8SFrW1Ssstuz83w_1AQ(XO`67Ful38m3a4+x(@N3`|;QxS6f&T)v-Gp_31waGAN_W$H*ph7>Sfq&tui~vuaSC%6 zjo{E&EX^8rby9<~i`v9AlGxmVR%d#5dWik?o${~Gt=(tf& zdxDmCv^U2(TG|@hJFK>j_Gn{Eyvd>=-*{6~o8A0A9KR_891WZXoDW<9+yLAOJP14u zybQbrd<1+6Ox%aC7BCOk66gdL0{a4o0VyC091olgTnu~gn1xCQtr@C5K1;1%E<;P1eHfJyri)&n*I znt<(rJ%J@a6zBt11E&KQ09OJx0`~w90Z#!h0&fE!0RIH4_9sjQW&@iLtn@cXUOOf1 z8ZITo`uphEK8sF?K>CQ}V*S~Yf%$f;qb(6{iMKY#+dJB6HiSk{JE$Mj)6z-v0&Pw) zvkRVo5D*3WfYrcRzy-jSz|Fv2z(c^VfZqXc10Mtb2C5GrtP9KswgS3^U+8?8_nd9WlJXJ8)=@Tt+T6@78$3A{?LqYDIl$&X7qAPk53meK0R6xTz?r~Bz;}Tk0zUyB z0iFSV2fPJ*1bhihTtZk2m=_`vHdoD}X$35>N!b4SXN?G4M0sao~C2HQ+tq z@4%>~gj!%7U;(f-upO`n*dJI1B!L`o0#F1l1+E8f1%3)V20RbE27Cy74vapSFbP-> z*a&C^wg>hG4guo80B|C37H|P@C2%8fC-5NfH1G=W7Vs(XB{1<2!gOE*U<;rF*cCVc zI08rm1>iK`T;LMmTHr^(Pk~2)=YUs%cY#lVF^3XrfpvfdKr^sCus5&-I0EPe3czW= zxxi(>wZI*~eZZ5z^T0d6hrqvp35OA;19O2bfDT|+U|--Hz;Yl5oC16k_%`r;;CA3= zz!Si8z#o7=1OEWVeuFR>m<4PCv;#W=djJOkM*_XTD&REWeBcV;2H;NMLEvfN72qx4 zBj8J5;^Bm~fVsftKpU_#&A@9W0DDiiWlmL2N4lL5tTz&5${#k3)c%3 z4@6hIR@MVWadp9KSAXBnyXsYS9X$zjmH+>+?kCf)-uG42`_y~y`@Zk{z7MbmkOm9@ zh5$DJZUWo}xEt_6z^4IU1w0P;8Q{MGe+5iR63hiG0W1e}0L}+&1zZM50SbU?0j~z! z40tEte!#~74+97ieLet0T2hQ25bTB1ndL!0ZM=y0Ivhw0k|9RAm9^#F9N;` z_%YzOfad_mrwL{PP6eC^Xa#fuwgL75(trWLA;2pDHv?`5+za>^;In|Q0lp7-67UDW z-vEz?A?KFbH@F;6}g^z@2~x0Url^0q{-06M$a>{s{O7V0tgX z$$+x}t$_0ZTL3!&R|7JD3g8uh*8|=LcrV}sfX@KF40sIiQ^3=JzX2xo5u5~A0$2uU z1FQpF1h^Dn0`h>vfL8*J0PXXa=kSYzFKA zBmjB9LBLA^BY;~0_W(Ws_zd97fX4to1v~@z8(?yVU_Rh9Kn$=FupY1tun*7&7y`Tm za3kOd;N5@+0G|YW3Gg`JCxBl8{sf@^BOe;TBEWJ$JK!9^R={q+)qot}5a88-TLA9_ z+za?H;9;PN|=qE6<(@3#n%Ib@1n?$gwB=+gkyi%W0 zu53@WnJd?}u4(D*ZC^{)7up=NGSx$_wJj@?sg-c9jb*RLeUAX{1UvxvIN%F_ZvuV@ z_!Z#40e=Nd8X%YpSOQo9SOvHMupO`;&2iybr5a9EGM*xojo&-D%_%mSgAi+F91K@1H zO2B%+Ho#s$A7BvhV!(}nBY-;r4*)(6_!8hdfS&+<2lx|U+%*KV0jB_#0onl<0JZ{l z15$t-;1J-|fSUn#0qzHU81OLQYk=Tqta2{YQ;4(l8Py}2LcrAgMy^eZ-(REPeOo0y3f$>Z4pjLIP zB176*vz68gdRy98_w=@s5AfQK-kz48RVz(yC4M*V`>%k{10Df<5AY=58NlBFlMfQi z1Dpmp8?YL%9&j;WAD|CV0=xw9I>1{2?*=>w_$1(qfNueQ3U~_eKY)J#rX3X1Z)Rf4ln^_z;%Gv0Nw()6L3G^qkzu=z6SUn;Aeo}0{#q`G)yoTumrFiunKSi zU_0OnKrf&OxE}C&z%77x0`3QV4Dc}EQNWJ@zXbdN@OQwp!vymI4S+acEnov+7vM@j z7BB?30dN!GHo)D0j{rUm_!8hdfS&<=19%Q_{IvwL01E(T0Ga{k1GWHm0+jmaMhyYkRO^!)#qmR@{|jB<60j%W>Ot09yb%0apPsfHL5vfY$+T0lWwBFMxjq z{5#;AfF}UI1pEQ;cfic+2<8JC04o4%02=@o1Fit{1FivF2e=V%GvJ+o_X9o#co^^~ z;0eI50e=Mi12F4)f`x#y0BwMC0o{O00SUkXz+u440dEGp1Moh;zXCoFcm(i0z>|Py z0Dl8aehI-mz-fT90iA&LfbD?G0Vd!Yz>5Jl0&WJp6L3G^V}O4Ld<*a+z*B%{0mr?R zU?yMz;7mX(pbM}Cum_L^3;>1zuLRr#cpKn7fCmAe0Q@`Pn}8<(zXUuBIPPTxGXV<# zX98LQU4U(XJ%FnK1ArmGD*$f<+zz-G@G-z=0bc`rAMjs*-vj;^Fy#h<69ERG39tsR z0k8{jC7>Tr2D}t70=N}$58wlUPXWFJ_zvL5fTsY@0*-q*!A!scz!`uRz9cKCYlY1&TL_XJv|y56)tg$ia%HR8+tZdz_RzNJl`B`T zS)Js{?tQr7X8?}?9s~Ro@HF7hfC;Z4mTqd&;{56*aJuc1^~l=R|4JyxC3w>;KP820gnQn0Q?g0N5DS-vtCPZGQa>d z0oDLE0CoVb1oQ*SfR_SB0Jj3}0ek@PNx+u?-vRst@EgFN0OMarFdMK4upF=oZ~0X_kE81N|I3BWG_e*_#qLNFb$0B|Ot70?CP2G|3*3Xla1 z0d4@?1h@lmH{c<_rvP69d>imnz|(*~117wlU>@L9z?pznKo?*OU=JV(7y!H&@JhfN z0dEJ~4R{FfDZp0&j{|-N_#NP{fT=eT%mpk0ECaLy&I5D-nAx9Gn`C`}k7KRg2e`QqtX@ka(c1Pk z?JcX?){-Trr+saEOS-pp)mqvZ(!u)Z>~(ZEcAvAohXuE?YjIC_wJh7s`+;vZq5vzs znZD|cd1N%|=;&D8M$S8H+Q`Y497a~RtX`FFr=2@(w4sG{UbmyV-VgW~;0u6n0)7Jc z72tmWH%>d!(&nlT+A!E^ zrpVvCHQCa}KJg#nCeHx=7cl)6f)fD-pc$|RumP|Oa3!D*FbH@V;PrsF0p1IE2=EEO zmjT}b{1EUfz;l3q0H(c_;1s~=fE9qXfQ^9Nfc=0BpaOUqU<7b0;5~p30X_xzGT<@5 zPXWIH{1q_%R)X1pMSx|1cEAOIt$@n_DL?^mJ>b=Vn+eS952qc>5YK)0t#- z>uOK8kZrnUbvtc8C4HT&LFC#`zN8cmWKEmd!M)NRz@0u1cm(i0z>|Py0Dl8aejC9& zz-fT90jmM)0T%=I5t!NgDR>qMHrq{d zH)dZ<33qxW;LU({0Nw}qDB$yeZvlP)_yyn(fWHH#-9|7U&;W=7IsuyiI{{Y#vVbAL z4S<^fw*l@3d=T(Cz*hl}1AYeh9pJBksc$Ek3s?eJ4p;@a0I(f!1)vvD0bCDwE#MZw zU4ZukJ^}b5;M;&71AYy74siVK1hW7O0cQbP0p|cN1ndS}4aflw0bT)kBj9$xy?_q` zJ`4C7;QN68A~3T>syoK$R?p^NqO%~#X_W$mG>bp6Zql18986Ha?^Hh>21@a2%(Bf1 zE46xcZ;}XK+m`N0rdw$c#|lci&FpRISi8E14}z2LpyyZ&I1A7LI2Ujc;8H*WZ~$-+ z@N&Q#0B;A}1Nac&Gk|XZegOCd;2FT*0h8ZBFdxtWhyywS>j4)7_5u0;CBO}U*8$!N zcsJldz^4IU0z3}*3E($?zXHa;lVCPr5nvgh9dIsS8(5)CDjy36(YueInq)M*rY44y|SZw@gL~-W=E&}WV zBmr5#5a0&DO@P|~_W~XQ{2SmAz+-@)0-gr^88G2J1SbHN0G0vT0p|g>11F%*LoJ_L8%zHDz;kx`W~_keOo*ZGd03I!)tbZ!a0j)4ecOu^0RpiuwxR zyMX@${1)&Rz{K|w%mFM0Gy*yR>j2vTdjY+G0^nM}s{uCya1?d+eSnVwJ`ea7;0J(T z0R8~@J7C(~1UOzU+W?3IIsqF2mjJE?WB?Vw%K)zj+zxmz;32@j0lotGE`gc-D7_7< zra5j+r#0QOnrxO7&ndN7W1!yrzDK(#GF|(tb;SveK4#M+4nm ztkfC|ltLhnOkgK+j!f#lc*DkRyN--ZIFQMwk8GVaGLhV0`^+QAGm)BXvCRIQkjv!F zBU@*BhM=B*FN?5zGX9z55rXH_w8AGNBS3L=h0(O zK(LFf8~DqK4yszVmAG&Rn5Unxj#W?81)s+5}m6shpuj&k)10t z+di&_Ec+0hAJmXH`dBEDkwbmM6>5sxy{_fZzPm$@Y1 zQGCSsqfpTEI6YGxxL=Fgynx(im(mSJPM`{HGH7E&If0{Z8#|9wRAo<9$u(8E;a=s9 z0aflJR_`j`RF^8BuBz2m+cRZTOY|2C2mIUH8N6XR8%`@v_Wa0&7@hfSYi}YM$JsjD z+ti+1A$1^;uMGHq0-}id?j@h*^$7%vsZZdN`n0+$>+%UKRn^*L@IOlwxv$CK=bqtj z+ov(j%2N{!r@PZ|b^r}NV)dpWQx_UevFg>k?k!Ggs{0l0b-y~G?tR4SUH3zEse5t8 zU-h>xRWmc~U*Bi(&W;WzwsmGA8Ha4U>Kwsh^{(%i*QLIfTJ=h*uW)#c$tEMF@v_{Yq(-B;Ssw^= zQqWow19T88Q^q|s;FvopBH5FaV6l3Waz|ZAIYU$`k)l!2Y!E$dE2(+lpGJ*@DdKR_ z02UhESrEydQ=(w8db9Apy0CDjtW~1H8gUJ88YcH7i;0|>_iqbL?ktF8&$bXOR&N$Q zRu>j#h)N|AR<>y0{4_EHUY*SARa9F&8_>RsPos7rk>x9XLuJd-cSBmv6} zIabhUfKfqXV?1T@|Lo)LG>Bvzv+6o72aDC4hHuq{hSgM|RR7C+sK?hxIonva z0!fOO71~we1jo14$sV+;9#yfo_xN$+(q^w=Ge*Rey68P!oo0ytGQD~Sey-8dsV*Uh zHpY6Xp|{G>HnF4p zaBXGSew2nw@lrWmsFdReis@wbao@FcpX>G;*9tPOJJX0=E9$i~-r9Rz^R=?l z*iWPTg2rWyUgh@YDy9CI)}i&U_tj@t^UmxP$3#ygEhe$nx&bCQs!XU z*vS5&y`x65WSG{o@sfKRuFP*N77N8r<685&2J6P6G=3~5=|N{we_$;hO~8o(LQC~n zHGC28*qy6}FXPFclQSXaT>KEpQ)oQk9lNz^ID-O!^$u4uiDID;VA1O0H+L2;l27m& zY=XtqVDo!)n=ky*@!#svJCAJDYnFPD9khlROC-?gCK8SD!DNwai1vT*p>8f1l|fPy zjrAaA&lZCbV|_f+nCno9`~(9gI_J96C6Z@((G@IKZ@Q-3SC7@+Empk}SvKyPjRt

P^#0b)o4D)F=^D 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

AiS!Df?bS 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 z

    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 z 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 ComfyUI_HunyuanVideoFoley +cd ComfyUI_HunyuanVideoFoley +``` + +### 2. Install Dependencies + +```bash +# Install Python dependencies +pip install -r requirements.txt + +# Or run the installation script +python install.py +``` + +### 3. Download Models + +You need to download the HunyuanVideo-Foley models: + +```bash +# Option 1: Manual download from HuggingFace +# Visit: https://huggingface.co/tencent/HunyuanVideo-Foley +# Download and place files in the following structure: + +mkdir -p pretrained_models +mkdir -p configs + +# Download these files to pretrained_models/: +# - hunyuanvideo_foley.pth +# - vae_128d_48k.pth +# - synchformer_state_dict.pth + +# Download config file to configs/: +# - hunyuanvideo-foley-xxl.yaml +``` + +Or use the Gradio app's auto-download feature by setting the environment variable: + +```bash +export HIFI_FOLEY_MODEL_PATH="/path/to/ComfyUI/custom_nodes/ComfyUI_HunyuanVideoFoley/pretrained_models" +``` + +### 4. Verify Installation + +```bash +# Run the test script to verify everything is working +python test_node.py +``` + +### 5. Restart ComfyUI + +After installation, restart ComfyUI to load the new custom nodes. + +## Expected Directory Structure + +After installation, your directory should look like this: + +``` +ComfyUI_HunyuanVideoFoley/ +├── __init__.py +├── nodes.py +├── utils.py +├── requirements.txt +├── install.py +├── test_node.py +├── example_workflow.json +├── README.md +├── INSTALLATION_GUIDE.md +└── pyproject.toml + +pretrained_models/ +├── hunyuanvideo_foley.pth +├── vae_128d_48k.pth +└── synchformer_state_dict.pth + +configs/ +└── hunyuanvideo-foley-xxl.yaml +``` + +## Usage + +### Nodes Available + +1. **HunyuanVideo-Foley Generator** + - Main node for audio generation + - Inputs: video, text prompt, generation parameters + - Outputs: video with audio, audio only, status message + +2. **HunyuanVideo-Foley Model Loader** + - Separate model loading node + - Useful for sharing models between multiple generator nodes + - Inputs: model path, config path + - Outputs: model handle, status message + +### Basic Workflow + +1. Load a video using ComfyUI's video input nodes +2. Add the "HunyuanVideo-Foley Generator" node +3. Connect the video to the generator +4. Set your text prompt (e.g., "A person walks on frozen ice") +5. Adjust parameters as needed +6. Run the workflow + +### Example Workflow + +An example workflow is provided in `example_workflow.json`. Load this file in ComfyUI to see a basic setup. + +## Performance Tips + +- **VRAM Usage**: The model requires significant GPU memory. If you encounter CUDA out of memory errors: + - Reduce `sample_nums` parameter + - Lower `num_inference_steps` + - Use CPU mode (slower but works with less memory) + +- **Generation Time**: Audio generation can take several minutes depending on: + - Video length + - Number of inference steps + - Number of samples generated + - Hardware specifications + +## Troubleshooting + +### Common Issues + +1. **"Failed to import HunyuanVideo-Foley modules"** + ```bash + # Make sure you're in the correct directory and have all dependencies + pip install -r requirements.txt + ``` + +2. **"Model path does not exist"** + ```bash + # Download models from HuggingFace and verify directory structure + ls pretrained_models/ + # Should show: hunyuanvideo_foley.pth, vae_128d_48k.pth, synchformer_state_dict.pth + ``` + +3. **CUDA out of memory** + ```bash + # Reduce memory usage by adjusting parameters: + # - Lower sample_nums to 1 + # - Reduce num_inference_steps to 25 + # - Use shorter videos for testing + ``` + +4. **Slow generation** + ```bash + # Normal for first run (model loading) + # Subsequent runs should be faster + # Consider using fewer inference steps for faster results + ``` + +### Getting Help + +- Check the test script output: `python test_node.py` +- Review ComfyUI console output for detailed error messages +- Ensure all model files are downloaded correctly +- Verify GPU memory availability + +## Model Information + +The HunyuanVideo-Foley model consists of several components: + +- **Main Foley Model**: Core text-video-to-audio generation +- **DAC VAE**: Audio encoding/decoding +- **SigLIP2**: Visual feature extraction +- **CLAP**: Text feature extraction +- **Synchformer**: Video-audio synchronization + +All components are automatically loaded when using the custom node. + +## License & Credits + +This custom node is based on the HunyuanVideo-Foley project by Tencent. Please respect the original project's license terms when using this implementation. + +Original project: https://github.com/tencent/HunyuanVideo-Foley \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..7b8142a --- /dev/null +++ b/README.md @@ -0,0 +1,143 @@ +# ComfyUI HunyuanVideo-Foley Custom Node + +This is a ComfyUI custom node wrapper for the HunyuanVideo-Foley model, which generates realistic audio from video and text descriptions. + +## Features + +- **Text-Video-to-Audio Synthesis**: Generate realistic audio that matches your video content +- **Flexible Text Prompts**: Use optional text descriptions to guide audio generation +- **Multiple Samples**: Generate up to 6 different audio variations per inference +- **Configurable Parameters**: Control guidance scale, inference steps, and sampling +- **Seed Control**: Reproducible results with seed parameter +- **Model Caching**: Efficient model loading and reuse across generations +- **Automatic Model Downloads**: Models are automatically downloaded to `ComfyUI/models/foley/` when needed +- **Multiple Model Variants**: Support for different model sizes (XXL, Base, etc.) + +## Installation + +1. **Clone this repository** into your ComfyUI custom_nodes directory: + ```bash + cd ComfyUI/custom_nodes + git clone ComfyUI_HunyuanVideoFoley + ``` + +2. **Install dependencies**: + ```bash + cd ComfyUI_HunyuanVideoFoley + pip install -r requirements.txt + ``` + +3. **Run the installation script** (recommended): + ```bash + python install.py + ``` + +4. **Restart ComfyUI** to load the new nodes. + +### Model Setup + +The models can be obtained in two ways: + +#### Option 1: Automatic Download (Recommended) +- Models will be automatically downloaded to `ComfyUI/models/foley/` when you first run the node +- No manual setup required +- Progress will be shown in the ComfyUI console + +#### Option 2: Manual Download +- Download models from [HuggingFace](https://huggingface.co/tencent/HunyuanVideo-Foley) +- Place models in `ComfyUI/models/foley/` (recommended) or `./pretrained_models/` directory +- Ensure the config file is at `configs/hunyuanvideo-foley-xxl.yaml` + +## Usage + +### Node Types + +#### 1. HunyuanVideo-Foley Generator +Main node for generating audio from video and text. + +**Inputs:** +- **video**: Video input (VIDEO type) +- **text_prompt**: Text description of desired audio (STRING) +- **guidance_scale**: CFG scale for generation control (1.0-10.0, default: 4.5) +- **num_inference_steps**: Number of denoising steps (10-100, default: 50) +- **sample_nums**: Number of audio samples to generate (1-6, default: 1) +- **seed**: Random seed for reproducibility (INT) +- **model_path**: Path to pretrained models (optional, leave empty for auto-download) +- **config_path**: Path to config file (optional, leave empty for default) +- **auto_download**: Enable automatic model downloading (BOOLEAN, default: True) +- **model_variant**: Choose model variant to download (dropdown, default: "hunyuanvideo-foley-xxl") + +**Outputs:** +- **video_with_audio**: Video with generated audio merged (VIDEO) +- **audio_only**: Generated audio file (AUDIO) +- **status_message**: Generation status and info (STRING) + +#### 2. HunyuanVideo-Foley Model Loader +Separate node for loading models (useful for sharing models between multiple generator nodes). + +**Inputs:** +- **model_path**: Path to pretrained models +- **config_path**: Path to config file + +**Outputs:** +- **model**: Model handle for use with other nodes +- **status_message**: Loading status + +## Example Workflow + +1. **Load Video**: Use a "Load Video" node to input your video file +2. **Add Generator**: Add the "HunyuanVideo-Foley Generator" node +3. **Connect Video**: Connect the video output to the generator's video input +4. **Set Prompt**: Enter a text description (e.g., "A person walks on frozen ice") +5. **Adjust Settings**: Configure guidance scale, steps, and sample count as needed +6. **Generate**: Run the workflow to generate audio + +## Model Requirements + +The node expects the following model structure: +``` +pretrained_models/ +├── hunyuanvideo_foley.pth # Main Foley model +├── vae_128d_48k.pth # DAC VAE model +└── synchformer_state_dict.pth # Synchformer model + +configs/ +└── hunyuanvideo-foley-xxl.yaml # Configuration file +``` + +## Performance Notes + +- **VRAM Usage**: The model requires significant GPU memory (recommended 16GB+ VRAM) +- **Generation Time**: Audio generation can take several minutes depending on video length and settings +- **Model Loading**: First run will download and cache models from HuggingFace if not present locally + +## Troubleshooting + +### Common Issues: + +1. **"Failed to import HunyuanVideo-Foley modules"** + - Ensure the parent HunyuanVideo-Foley package is properly installed + - Check that all dependencies are installed correctly + +2. **"Model path does not exist"** + - Verify the model files are downloaded and in the correct directory + - Check the model_path parameter matches your model location + +3. **CUDA out of memory** + - Reduce the number of samples (`sample_nums`) + - Lower the number of inference steps + - Use CPU if GPU memory is insufficient + +4. **"Config path does not exist"** + - Ensure the config file is in the expected location + - Verify the config_path parameter is correct + +## License + +This custom node is based on the HunyuanVideo-Foley project. Please check the original project's license terms. + +## Credits + +Based on the HunyuanVideo-Foley project by Tencent. Original paper and code available at: +- Paper: [HunyuanVideo-Foley: Text-Video-to-Audio Synthesis] +- Code: [https://github.com/tencent/HunyuanVideo-Foley] \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..091dfbe --- /dev/null +++ b/__init__.py @@ -0,0 +1,12 @@ +import os +import sys + +# Add the current directory to Python path to import hunyuanvideo_foley modules +current_dir = os.path.dirname(os.path.abspath(__file__)) +if current_dir not in sys.path: + sys.path.insert(0, current_dir) + +from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS + +# Export the mappings +__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] \ No newline at end of file diff --git a/__pycache__/__init__.cpython-312.pyc b/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8c1c906b32ac6adb87aa173d9aff0c84c4408a31 GIT binary patch literal 629 zcmX@j%ge<81hW%2WOOhxFgylvV1O0M`1}IMn9h*GkiwY55Cx$bqZm^evRGhB5p)(i zf(>KgP{|3CN6@LPtAVZn0T>;{mdcjOh|Q)l#v&Q)GFki(Gmyy?hE%rIOb`)9h7_h0 z<{XY(&M3}Yt|%@bpCyGgohgdDl2McGB}kFqOOQJ>nQjUD`MbErJNq~W2gmz51_XHe zxd-2p0t>o$1_$^!M#lR&`ntm9ia=pf1PVG$o?A@$#kZJ?D~oTj6eN~p++t73EXqsF zO})jQm{bfF;fRk<%gjlQkH5v8Tv}9=npYAJRC9|hGp{(csN@!FUVchyF~Z1O?D6r5 zIXUt1D;YimZD9Ci>tYq-oS&Ok8R{96Tv}X`pBoR>0uzk)D9x)ZP0S0+Oi9gm%g;%z zjERrW%*!l^kJl@xyv1RYo1apelWJEa3^WboHYbg`7@H{r_W5EpFcByMaE@0^UHh|4cs@F`6eV!&zP8Tky-8r3wygqqer`cqyGfQ d3Ds9vl%F#=0Zrghyr5)vfy=&;y+{yfJODxlkR$*A literal 0 HcmV?d00001 diff --git a/__pycache__/model_urls.cpython-312.pyc b/__pycache__/model_urls.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cfdb70df890a065add9db5e18dbcf2351f101580 GIT binary patch literal 1454 zcmbtT&1)M+6rcUjTG_QMxpth?O`^36ZFGo5Ob*86gEn7vuxpTUaE)Qv?2M#sR=Z|q z*0qESg5=UuDYV#!9CB>q9P%gho=d7 znai07#@MqL^@4%W8?ot+k)Cq00m?qY2rF2{8rGj^ID-v5BDE|&g-xN=H1M$S=o8IX zHj$4u6+D4+M8#u7!+D}t4P1Df+01}$R8>6wSl=9Jeo~rjP1WPD8N1<*k4bc?8U>_z zX?Hhht=Bo%sKdC+iGzKQ%X+8(2m5qw7}u@S{(<Ayk{A| z|JCjPoO<~K{B3M3_q^bOwi{zVvTGzH)D_eLv4&)rB{{Oosq1lq;r5j3&~}L_F-J0P z#*8=(Kq8tLl<=5_p4#tLSVe#a^W!VDky{;2M$-)H*BaNUEpJ?wd{ zp_0=2>@MNG+Se<~wiks}zZMS)w^o)amDP#^*ji$)tlql4+|{)j;U$fVA4Ibtl2gz- zOui`-723A__l;YNTQ{RdwYk2u<;9Fg4JU+7V|}CZE|s#qx4K-2RCC+SBnO8$0~0Mk z-Wize9Edi0T`c`(J#V#_I@aXR*M7QoF#WW0WLURX{G z&>4hw@5ksAU|+ywlOWpYt(E`P+|U1#KPt=|S+lS7S()P7l5(e%m8q#EvT1NoqCbeD zAkoE#k|fayfMiB`T4=+=vsZ!6I3U;QGyvjO>=B4mQxqjNQRaMVptF(EId`Q~ynZ~Q z=f=`ebe4f(p;KHO7<@3yb&B&{!@~ literal 0 HcmV?d00001 diff --git a/__pycache__/nodes.cpython-312.pyc b/__pycache__/nodes.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e647c9da79d4df32d438ee4c5da5931aa4224c2e GIT binary patch literal 51804 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 literal 0 HcmV?d00001 diff --git a/configs/hunyuanvideo-foley-xxl.yaml b/configs/hunyuanvideo-foley-xxl.yaml new file mode 100644 index 0000000..96910ed --- /dev/null +++ b/configs/hunyuanvideo-foley-xxl.yaml @@ -0,0 +1,49 @@ +model_config: + model_name: HunyuanVideo-Foley-XXL + model_type: 1d + model_precision: bf16 + model_kwargs: + depth_triple_blocks: 18 + depth_single_blocks: 36 + hidden_size: 1536 + num_heads: 12 + mlp_ratio: 4 + mlp_act_type: "gelu_tanh" + qkv_bias: True + qk_norm: True + qk_norm_type: "rms" + attn_mode: "torch" + embedder_type: "default" + interleaved_audio_visual_rope: True + enable_learnable_empty_visual_feat: True + sync_modulation: False + add_sync_feat_to_audio: True + cross_attention: True + use_attention_mask: False + condition_projection: "linear" + sync_feat_dim: 768 # syncformer 768 dim + condition_dim: 768 # clap 768 text condition dim (clip-text) + clip_dim: 768 # siglip2 visual dim + audio_vae_latent_dim: 128 + audio_frame_rate: 50 + patch_size: 1 + rope_dim_list: null + rope_theta: 10000 + text_length: 77 + clip_length: 64 + sync_length: 192 + use_mmaudio_singleblock: True + depth_triple_ssl_encoder: null + depth_single_ssl_encoder: 8 + use_repa_with_audiossl: True + +diffusion_config: + denoise_type: "flow" + flow_path_type: "linear" + flow_predict_type: "velocity" + flow_reverse: True + flow_solver: "euler" + sample_flow_shift: 1.0 + sample_use_flux_shift: False + flux_base_shift: 0.5 + flux_max_shift: 1.15 diff --git a/download_models_manual.py b/download_models_manual.py new file mode 100644 index 0000000..e56924f --- /dev/null +++ b/download_models_manual.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +""" +Manual download helper for HunyuanVideo-Foley models +Run this script directly if the automatic download fails in ComfyUI +""" + +import os +import sys +from pathlib import Path +import urllib.request +import time + +# Model download URLs +MODELS = [ + { + "url": "https://huggingface.co/tencent/HunyuanVideo-Foley/resolve/main/hunyuanvideo_foley.pth", + "filename": "hunyuanvideo_foley.pth", + "size_gb": 10.3, + "description": "Main HunyuanVideo-Foley model" + }, + { + "url": "https://huggingface.co/tencent/HunyuanVideo-Foley/resolve/main/synchformer_state_dict.pth", + "filename": "synchformer_state_dict.pth", + "size_gb": 0.95, + "description": "Synchformer model weights" + }, + { + "url": "https://huggingface.co/tencent/HunyuanVideo-Foley/resolve/main/vae_128d_48k.pth", + "filename": "vae_128d_48k.pth", + "size_gb": 1.49, + "description": "VAE model weights" + } +] + +def download_with_progress(url, dest_path): + """Download file with progress display""" + def progress_hook(block_num, block_size, total_size): + if total_size > 0: + downloaded = block_num * block_size + percent = min(100, (downloaded * 100) // total_size) + size_mb = downloaded / (1024 * 1024) + total_mb = total_size / (1024 * 1024) + + # Print progress + bar_len = 40 + filled_len = int(bar_len * percent // 100) + bar = '█' * filled_len + '-' * (bar_len - filled_len) + + sys.stdout.write(f'\r[{bar}] {percent}% ({size_mb:.1f}/{total_mb:.1f} MB)') + sys.stdout.flush() + + try: + urllib.request.urlretrieve(url, dest_path, progress_hook) + print() # New line after progress + return True + except Exception as e: + print(f"\nError: {e}") + return False + +def main(): + # Determine ComfyUI models directory + comfyui_root = Path(__file__).parent.parent.parent # Go up to ComfyUI root + models_dir = comfyui_root / "models" / "foley" / "hunyuanvideo-foley-xxl" + + print("=" * 60) + print("HunyuanVideo-Foley Model Downloader") + print("=" * 60) + print(f"\nModels will be downloaded to:") + print(f" {models_dir}") + + # Create directory if it doesn't exist + models_dir.mkdir(parents=True, exist_ok=True) + + # Check disk space + import shutil + stat = shutil.disk_usage(models_dir) + available_gb = stat.free / (1024 ** 3) + required_gb = sum(m["size_gb"] for m in MODELS) + 1 # Add 1GB buffer + + print(f"\nDisk space available: {available_gb:.1f} GB") + print(f"Space required: {required_gb:.1f} GB") + + if available_gb < required_gb: + print(f"\n⚠️ WARNING: Insufficient disk space!") + print(f"Please free up at least {required_gb - available_gb:.1f} GB before continuing.") + response = input("\nContinue anyway? (y/n): ") + if response.lower() != 'y': + return + + print("\n" + "=" * 60) + + # Download each model + for i, model_info in enumerate(MODELS, 1): + model_path = models_dir / model_info["filename"] + + print(f"\n[{i}/{len(MODELS)}] {model_info['description']}") + print(f" File: {model_info['filename']} ({model_info['size_gb']} GB)") + + # Check if already downloaded + if model_path.exists() and model_path.stat().st_size > 100 * 1024 * 1024: + size_gb = model_path.stat().st_size / (1024 ** 3) + print(f" ✓ Already downloaded ({size_gb:.2f} GB)") + continue + + print(f" Downloading from: {model_info['url']}") + + # Try downloading with retries + max_retries = 3 + for attempt in range(max_retries): + if attempt > 0: + print(f" Retry {attempt}/{max_retries - 1}...") + time.sleep(5) + + success = download_with_progress(model_info["url"], model_path) + if success: + size_gb = model_path.stat().st_size / (1024 ** 3) + print(f" ✓ Downloaded successfully ({size_gb:.2f} GB)") + break + else: + if attempt == max_retries - 1: + print(f" ✗ Failed to download after {max_retries} attempts") + print(f"\n You can manually download from:") + print(f" {model_info['url']}") + print(f" And place it at:") + print(f" {model_path}") + + print("\n" + "=" * 60) + print("Download process completed!") + print("\nYou can now use the HunyuanVideo-Foley node in ComfyUI.") + print("=" * 60) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/example_workflows/hunyuan_foley.json b/example_workflows/hunyuan_foley.json new file mode 100644 index 0000000..6d96f04 --- /dev/null +++ b/example_workflows/hunyuan_foley.json @@ -0,0 +1,708 @@ +{ + "id": "b97ba031-d866-4f4e-baf5-3017bd4ef4b5", + "revision": 0, + "last_node_id": 25, + "last_link_id": 58, + "nodes": [ + { + "id": 16, + "type": "VHS_VideoCombine", + "pos": [ + 1363.7874755859375, + 232.0892791748047 + ], + "size": [ + 214.7587890625, + 458.1463623046875 + ], + "flags": {}, + "order": 10, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 39 + }, + { + "name": "audio", + "shape": 7, + "type": "AUDIO", + "link": 53 + }, + { + "name": "meta_batch", + "shape": 7, + "type": "VHS_BatchManager", + "link": null + }, + { + "name": "vae", + "shape": 7, + "type": "VAE", + "link": null + }, + { + "name": "frame_rate", + "type": "FLOAT", + "widget": { + "name": "frame_rate" + }, + "link": 49 + } + ], + "outputs": [ + { + "name": "Filenames", + "type": "VHS_FILENAMES", + "links": null + } + ], + "properties": { + "Node name for S&R": "VHS_VideoCombine" + }, + "widgets_values": { + "frame_rate": 24, + "loop_count": 0, + "filename_prefix": "AnimateDiff", + "format": "video/h264-mp4", + "pix_fmt": "yuv420p", + "crf": 19, + "save_metadata": true, + "trim_to_audio": false, + "pingpong": false, + "save_output": true, + "videopreview": { + "hidden": false, + "paused": false, + "params": { + "filename": "AnimateDiff_00027-audio.mp4", + "subfolder": "", + "type": "output", + "format": "video/h264-mp4", + "frame_rate": 24, + "workflow": "AnimateDiff_00027.png", + "fullpath": "D:\\ComfyUI\\output\\AnimateDiff_00027-audio.mp4" + } + } + } + }, + { + "id": 1, + "type": "LoadVideo", + "pos": [ + -232.2467041015625, + -0.7514959573745728 + ], + "size": [ + 300, + 243.2307891845703 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "VIDEO", + "type": "VIDEO", + "slot_index": 0, + "links": [ + 33, + 44 + ] + } + ], + "properties": { + "Node name for S&R": "LoadVideo" + }, + "widgets_values": [ + "ComfyUI_00006_.mp4", + "image" + ] + }, + { + "id": 22, + "type": "Reroute", + "pos": [ + 465.9364013671875, + 599.26171875 + ], + "size": [ + 75, + 26 + ], + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "name": "", + "type": "*", + "widget": { + "name": "value" + }, + "link": 58 + } + ], + "outputs": [ + { + "name": "", + "type": "FLOAT", + "links": [ + 49 + ] + } + ], + "properties": { + "showOutputText": false, + "horizontal": false + } + }, + { + "id": 14, + "type": "PreviewAny", + "pos": [ + 921.5315551757812, + 690.1361694335938 + ], + "size": [ + 257.9618835449219, + 212.5326690673828 + ], + "flags": {}, + "order": 9, + "mode": 0, + "inputs": [ + { + "name": "source", + "type": "*", + "link": 42 + } + ], + "outputs": [], + "properties": { + "Node name for S&R": "PreviewAny" + }, + "widgets_values": [] + }, + { + "id": 17, + "type": "VHS_LoadVideo", + "pos": [ + -142.1929168701172, + 301.8902282714844 + ], + "size": [ + 247.455078125, + 452.2566223144531 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "name": "meta_batch", + "shape": 7, + "type": "VHS_BatchManager", + "link": null + }, + { + "name": "vae", + "shape": 7, + "type": "VAE", + "link": null + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [] + }, + { + "name": "frame_count", + "type": "INT", + "links": null + }, + { + "name": "audio", + "type": "AUDIO", + "links": null + }, + { + "name": "video_info", + "type": "VHS_VIDEOINFO", + "links": [ + 34 + ] + } + ], + "properties": { + "Node name for S&R": "VHS_LoadVideo" + }, + "widgets_values": { + "video": "veo_25184_0.mp4", + "force_rate": 0, + "custom_width": 0, + "custom_height": 0, + "frame_load_cap": 0, + "skip_first_frames": 0, + "select_every_nth": 1, + "format": "AnimateDiff", + "choose video to upload": "image", + "videopreview": { + "hidden": false, + "paused": false, + "params": { + "filename": "veo_25184_0.mp4", + "type": "input", + "format": "video/mp4", + "force_rate": 0, + "custom_width": 0, + "custom_height": 0, + "frame_load_cap": 0, + "skip_first_frames": 0, + "select_every_nth": 1 + } + } + } + }, + { + "id": 3, + "type": "PreviewVideo", + "pos": [ + 953.444091796875, + -29.38906478881836 + ], + "size": [ + 300, + 239.23077392578125 + ], + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "name": "video_path", + "type": "STRING", + "widget": { + "name": "video_path" + }, + "link": 40 + }, + { + "name": "video", + "type": "VIDEO", + "link": null + } + ], + "outputs": [ + { + "name": "video", + "type": "VIDEO", + "links": null + }, + { + "name": "video_path", + "type": "STRING", + "links": null + } + ], + "properties": { + "Node name for S&R": "PreviewVideo" + }, + "widgets_values": [ + "" + ] + }, + { + "id": 4, + "type": "PreviewAudio", + "pos": [ + 921.59326171875, + 350.4112548828125 + ], + "size": [ + 300, + 100 + ], + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [ + { + "name": "audio", + "type": "AUDIO", + "link": 41 + } + ], + "outputs": [], + "properties": { + "Node name for S&R": "PreviewAudio" + }, + "widgets_values": [] + }, + { + "id": 18, + "type": "Reroute", + "pos": [ + 191.51316833496094, + 14.080697059631348 + ], + "size": [ + 75, + 26 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "name": "", + "type": "*", + "link": 33 + } + ], + "outputs": [ + { + "name": "", + "type": "VIDEO", + "links": [ + 56 + ] + } + ], + "properties": { + "showOutputText": false, + "horizontal": false + } + }, + { + "id": 20, + "type": "HunyuanVideoFoley", + "pos": [ + 454.4621276855469, + 23.64237403869629 + ], + "size": [ + 400, + 466 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "name": "video", + "shape": 7, + "type": "VIDEO", + "link": 56 + }, + { + "name": "images", + "shape": 7, + "type": "IMAGE", + "link": null + }, + { + "name": "fps", + "shape": 7, + "type": "FLOAT", + "widget": { + "name": "fps" + }, + "link": 57 + } + ], + "outputs": [ + { + "name": "video_path", + "type": "STRING", + "links": [ + 40 + ] + }, + { + "name": "video_frames", + "type": "IMAGE", + "links": [ + 39 + ] + }, + { + "name": "audio", + "type": "AUDIO", + "links": [ + 41, + 53 + ] + }, + { + "name": "status_message", + "type": "STRING", + "links": [ + 42 + ] + } + ], + "properties": { + "Node name for S&R": "HunyuanVideoFoley" + }, + "widgets_values": [ + "An animated cartoon of a leopard walking between snowy trees is happy and fun. Footstep sound, impact, snow falling, and wind blowingthrough the trees ", + 4.5, + 50, + 1, + 790777712306117, + "randomize", + 24, + "", + "both", + "hunyuan_foley", + "foley_", + true, + true + ] + }, + { + "id": 21, + "type": "GetVideoComponents", + "pos": [ + 140.83583068847656, + 173.76251220703125 + ], + "size": [ + 185.17733764648438, + 66 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "name": "video", + "type": "VIDEO", + "link": 44 + } + ], + "outputs": [ + { + "name": "images", + "type": "IMAGE", + "links": null + }, + { + "name": "audio", + "type": "AUDIO", + "links": null + }, + { + "name": "fps", + "type": "FLOAT", + "links": [ + 57, + 58 + ] + } + ], + "properties": { + "Node name for S&R": "GetVideoComponents" + }, + "widgets_values": [] + }, + { + "id": 19, + "type": "VHS_VideoInfo", + "pos": [ + 186.7323760986328, + 637.5086669921875 + ], + "size": [ + 225.59765625, + 206 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "name": "video_info", + "type": "VHS_VIDEOINFO", + "link": 34 + } + ], + "outputs": [ + { + "name": "source_fps🟨", + "type": "FLOAT", + "links": [] + }, + { + "name": "source_frame_count🟨", + "type": "INT", + "links": null + }, + { + "name": "source_duration🟨", + "type": "FLOAT", + "links": null + }, + { + "name": "source_width🟨", + "type": "INT", + "links": null + }, + { + "name": "source_height🟨", + "type": "INT", + "links": null + }, + { + "name": "loaded_fps🟦", + "type": "FLOAT", + "links": null + }, + { + "name": "loaded_frame_count🟦", + "type": "INT", + "links": null + }, + { + "name": "loaded_duration🟦", + "type": "FLOAT", + "links": null + }, + { + "name": "loaded_width🟦", + "type": "INT", + "links": null + }, + { + "name": "loaded_height🟦", + "type": "INT", + "links": null + } + ], + "properties": { + "Node name for S&R": "VHS_VideoInfo" + }, + "widgets_values": {} + } + ], + "links": [ + [ + 33, + 1, + 0, + 18, + 0, + "*" + ], + [ + 34, + 17, + 3, + 19, + 0, + "VHS_VIDEOINFO" + ], + [ + 39, + 20, + 1, + 16, + 0, + "IMAGE" + ], + [ + 40, + 20, + 0, + 3, + 0, + "STRING" + ], + [ + 41, + 20, + 2, + 4, + 0, + "AUDIO" + ], + [ + 42, + 20, + 3, + 14, + 0, + "*" + ], + [ + 44, + 1, + 0, + 21, + 0, + "VIDEO" + ], + [ + 49, + 22, + 0, + 16, + 4, + "FLOAT" + ], + [ + 53, + 20, + 2, + 16, + 1, + "AUDIO" + ], + [ + 56, + 18, + 0, + 20, + 0, + "VIDEO" + ], + [ + 57, + 21, + 2, + 20, + 2, + "FLOAT" + ], + [ + 58, + 21, + 2, + 22, + 0, + "*" + ] + ], + "groups": [], + "config": {}, + "extra": { + "ds": { + "scale": 1.0458302028549975, + "offset": [ + 531.3574911692173, + 102.3339554039322 + ] + }, + "frontendVersion": "1.25.11", + "VHS_latentpreview": false, + "VHS_latentpreviewrate": 0, + "VHS_MetadataImage": true, + "VHS_KeepIntermediate": true + }, + "version": 0.4 +} \ No newline at end of file diff --git a/hunyuanvideo_foley/__init__.py b/hunyuanvideo_foley/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hunyuanvideo_foley/__pycache__/__init__.cpython-312.pyc b/hunyuanvideo_foley/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f30401f098b578a406d7db6d2eb1404e3ee1dbd1 GIT binary patch literal 177 zcmX@j%ge<81YW@#GC=fW5P=Rpvj9b=GgLBYGWxA#C}INgK7-W!3UINCan8?8s|@vw zNiHoe$#bU#$-T5%0MFVX<(uF_{_Y_lK6PN jg34PQHo5sJr8%i~MXW#z7=gGL#Q4a}$jDg43}gWS^LsB< literal 0 HcmV?d00001 diff --git a/hunyuanvideo_foley/__pycache__/constants.cpython-312.pyc b/hunyuanvideo_foley/__pycache__/constants.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5accb37022225bb7bb5075d578705a27ec42130e GIT binary patch literal 1954 zcmZ`)O>7!h5Pqt<{#taNlLva2Vb`JXEt?NmIvVBacRFP;9K- zcr>1c#fjRO+*qMyK2xay}O&sM8kX^ZdODLO-86pPUfn^6r3 z%Q(xFRavrRS9Pz_ogC9pOv}#p?$&gx({3B44R`L&j&VjW*;$4^w|f?Ao%wt$ebHNS zH-W(&Q?eC9XXL}WEMtq&q(epPXpGTddQ*%g!|hwiUf;Qm`H-4M384b~9Hi zR7G$4e6Cn7uwo@wWy##xk+%)7v?P9J^1L@5=Sn5Ez>`!l_jJ58`5FYn&cFq3Y?t!R z8CUwhvzV7>C&!8fC|RnCd9Fypo^`X5D|%GSphK0iY?DxmZj)J2lyn%CdrKDd6*=Qo z+|IKFv7Dj1UvKbn4#???Ofe&Wm0DWZJ-lOT%U8lcE9et&?X5SdCt zzTq#bU#$-T5%0MFVX<(sTpq8BCnE3e2 nyv&mLc)fzkTO2k(!P1;myCPPgJ&ZtH3}Sp_W@Kb6Vg|ARg`zS% literal 0 HcmV?d00001 diff --git a/hunyuanvideo_foley/models/__pycache__/hifi_foley.cpython-312.pyc b/hunyuanvideo_foley/models/__pycache__/hifi_foley.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2a6d29510a46f7f66b673252ce3b1fc75796f9c9 GIT binary patch literal 43659 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 literal 0 HcmV?d00001 diff --git a/hunyuanvideo_foley/models/dac_vae/__init__.py b/hunyuanvideo_foley/models/dac_vae/__init__.py new file mode 100644 index 0000000..4a5d033 --- /dev/null +++ b/hunyuanvideo_foley/models/dac_vae/__init__.py @@ -0,0 +1,16 @@ +__version__ = "1.0.0" + +# preserved here for legacy reasons +__model_version__ = "latest" + +import audiotools + +audiotools.ml.BaseModel.INTERN += ["dac.**"] +audiotools.ml.BaseModel.EXTERN += ["einops"] + + +from . import nn +from . import model +from . import utils +from .model import DAC +from .model import DACFile diff --git a/hunyuanvideo_foley/models/dac_vae/__main__.py b/hunyuanvideo_foley/models/dac_vae/__main__.py new file mode 100644 index 0000000..3a85723 --- /dev/null +++ b/hunyuanvideo_foley/models/dac_vae/__main__.py @@ -0,0 +1,36 @@ +import sys + +import argbind + +from .utils import download +from .utils.decode import decode +from .utils.encode import encode + +STAGES = ["encode", "decode", "download"] + + +def run(stage: str): + """Run stages. + + Parameters + ---------- + stage : str + Stage to run + """ + if stage not in STAGES: + raise ValueError(f"Unknown command: {stage}. Allowed commands are {STAGES}") + stage_fn = globals()[stage] + + if stage == "download": + stage_fn() + return + + stage_fn() + + +if __name__ == "__main__": + group = sys.argv.pop(1) + args = argbind.parse_args(group=group) + + with argbind.scope(args): + run(group) diff --git a/hunyuanvideo_foley/models/dac_vae/__pycache__/__init__.cpython-312.pyc b/hunyuanvideo_foley/models/dac_vae/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..81d34b5f3474fa31de4e3ef6ff40fe2e0239ee5f GIT binary patch literal 709 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 H%=P~O2Dhlb literal 0 HcmV?d00001 diff --git a/hunyuanvideo_foley/models/dac_vae/model/__pycache__/dac.cpython-312.pyc b/hunyuanvideo_foley/models/dac_vae/model/__pycache__/dac.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4628ece6e37da5119f9a944e31522401580850cf GIT binary patch literal 17298 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 J=V)ui{{`C=ygvW{ literal 0 HcmV?d00001 diff --git a/hunyuanvideo_foley/models/dac_vae/model/__pycache__/discriminator.cpython-312.pyc b/hunyuanvideo_foley/models/dac_vae/model/__pycache__/discriminator.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fa43c90c35ec9e498dca33abd1b2d8c1c00418d8 GIT binary patch literal 11511 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 RvH#x5HEe-ZA~Z9e`X5kgVoLx3 literal 0 HcmV?d00001 diff --git a/hunyuanvideo_foley/models/dac_vae/model/base.py b/hunyuanvideo_foley/models/dac_vae/model/base.py new file mode 100644 index 0000000..0eca801 --- /dev/null +++ b/hunyuanvideo_foley/models/dac_vae/model/base.py @@ -0,0 +1,301 @@ +import math +from dataclasses import dataclass +from pathlib import Path +from typing import Union + +import numpy as np +import torch +import tqdm +from audiotools import AudioSignal +from torch import nn + +SUPPORTED_VERSIONS = ["1.0.0"] + + +@dataclass +class DACFile: + codes: torch.Tensor + + # Metadata + chunk_length: int + original_length: int + input_db: float + channels: int + sample_rate: int + padding: bool + dac_version: str + + def save(self, path): + artifacts = { + "codes": self.codes.numpy().astype(np.uint16), + "metadata": { + "input_db": self.input_db.numpy().astype(np.float32), + "original_length": self.original_length, + "sample_rate": self.sample_rate, + "chunk_length": self.chunk_length, + "channels": self.channels, + "padding": self.padding, + "dac_version": SUPPORTED_VERSIONS[-1], + }, + } + path = Path(path).with_suffix(".dac") + with open(path, "wb") as f: + np.save(f, artifacts) + return path + + @classmethod + def load(cls, path): + artifacts = np.load(path, allow_pickle=True)[()] + codes = torch.from_numpy(artifacts["codes"].astype(int)) + if artifacts["metadata"].get("dac_version", None) not in SUPPORTED_VERSIONS: + raise RuntimeError( + f"Given file {path} can't be loaded with this version of descript-audio-codec." + ) + return cls(codes=codes, **artifacts["metadata"]) + + +class CodecMixin: + @property + def padding(self): + if not hasattr(self, "_padding"): + self._padding = True + return self._padding + + @padding.setter + def padding(self, value): + assert isinstance(value, bool) + + layers = [ + l for l in self.modules() if isinstance(l, (nn.Conv1d, nn.ConvTranspose1d)) + ] + + for layer in layers: + if value: + if hasattr(layer, "original_padding"): + layer.padding = layer.original_padding + else: + layer.original_padding = layer.padding + layer.padding = tuple(0 for _ in range(len(layer.padding))) + + self._padding = value + + def get_delay(self): + # Any number works here, delay is invariant to input length + l_out = self.get_output_length(0) + L = l_out + + layers = [] + for layer in self.modules(): + if isinstance(layer, (nn.Conv1d, nn.ConvTranspose1d)): + layers.append(layer) + + for layer in reversed(layers): + d = layer.dilation[0] + k = layer.kernel_size[0] + s = layer.stride[0] + + if isinstance(layer, nn.ConvTranspose1d): + L = ((L - d * (k - 1) - 1) / s) + 1 + elif isinstance(layer, nn.Conv1d): + L = (L - 1) * s + d * (k - 1) + 1 + + L = math.ceil(L) + + l_in = L + + return (l_in - l_out) // 2 + + def get_output_length(self, input_length): + L = input_length + # Calculate output length + for layer in self.modules(): + if isinstance(layer, (nn.Conv1d, nn.ConvTranspose1d)): + d = layer.dilation[0] + k = layer.kernel_size[0] + s = layer.stride[0] + + if isinstance(layer, nn.Conv1d): + L = ((L - d * (k - 1) - 1) / s) + 1 + elif isinstance(layer, nn.ConvTranspose1d): + L = (L - 1) * s + d * (k - 1) + 1 + + L = math.floor(L) + return L + + @torch.no_grad() + def compress( + self, + audio_path_or_signal: Union[str, Path, AudioSignal], + win_duration: float = 1.0, + verbose: bool = False, + normalize_db: float = -16, + n_quantizers: int = None, + ) -> DACFile: + """Processes an audio signal from a file or AudioSignal object into + discrete codes. This function processes the signal in short windows, + using constant GPU memory. + + Parameters + ---------- + audio_path_or_signal : Union[str, Path, AudioSignal] + audio signal to reconstruct + win_duration : float, optional + window duration in seconds, by default 5.0 + verbose : bool, optional + by default False + normalize_db : float, optional + normalize db, by default -16 + + Returns + ------- + DACFile + Object containing compressed codes and metadata + required for decompression + """ + audio_signal = audio_path_or_signal + if isinstance(audio_signal, (str, Path)): + audio_signal = AudioSignal.load_from_file_with_ffmpeg(str(audio_signal)) + + self.eval() + original_padding = self.padding + original_device = audio_signal.device + + audio_signal = audio_signal.clone() + audio_signal = audio_signal.to_mono() + original_sr = audio_signal.sample_rate + + resample_fn = audio_signal.resample + loudness_fn = audio_signal.loudness + + # If audio is > 10 minutes long, use the ffmpeg versions + if audio_signal.signal_duration >= 10 * 60 * 60: + resample_fn = audio_signal.ffmpeg_resample + loudness_fn = audio_signal.ffmpeg_loudness + + original_length = audio_signal.signal_length + resample_fn(self.sample_rate) + input_db = loudness_fn() + + if normalize_db is not None: + audio_signal.normalize(normalize_db) + audio_signal.ensure_max_of_audio() + + nb, nac, nt = audio_signal.audio_data.shape + audio_signal.audio_data = audio_signal.audio_data.reshape(nb * nac, 1, nt) + win_duration = ( + audio_signal.signal_duration if win_duration is None else win_duration + ) + + if audio_signal.signal_duration <= win_duration: + # Unchunked compression (used if signal length < win duration) + self.padding = True + n_samples = nt + hop = nt + else: + # Chunked inference + self.padding = False + # Zero-pad signal on either side by the delay + audio_signal.zero_pad(self.delay, self.delay) + n_samples = int(win_duration * self.sample_rate) + # Round n_samples to nearest hop length multiple + n_samples = int(math.ceil(n_samples / self.hop_length) * self.hop_length) + hop = self.get_output_length(n_samples) + + codes = [] + range_fn = range if not verbose else tqdm.trange + + for i in range_fn(0, nt, hop): + x = audio_signal[..., i : i + n_samples] + x = x.zero_pad(0, max(0, n_samples - x.shape[-1])) + + audio_data = x.audio_data.to(self.device) + audio_data = self.preprocess(audio_data, self.sample_rate) + _, c, _, _, _ = self.encode(audio_data, n_quantizers) + codes.append(c.to(original_device)) + chunk_length = c.shape[-1] + + codes = torch.cat(codes, dim=-1) + + dac_file = DACFile( + codes=codes, + chunk_length=chunk_length, + original_length=original_length, + input_db=input_db, + channels=nac, + sample_rate=original_sr, + padding=self.padding, + dac_version=SUPPORTED_VERSIONS[-1], + ) + + if n_quantizers is not None: + codes = codes[:, :n_quantizers, :] + + self.padding = original_padding + return dac_file + + @torch.no_grad() + def decompress( + self, + obj: Union[str, Path, DACFile], + verbose: bool = False, + ) -> AudioSignal: + """Reconstruct audio from a given .dac file + + Parameters + ---------- + obj : Union[str, Path, DACFile] + .dac file location or corresponding DACFile object. + verbose : bool, optional + Prints progress if True, by default False + + Returns + ------- + AudioSignal + Object with the reconstructed audio + """ + self.eval() + if isinstance(obj, (str, Path)): + obj = DACFile.load(obj) + + original_padding = self.padding + self.padding = obj.padding + + range_fn = range if not verbose else tqdm.trange + codes = obj.codes + original_device = codes.device + chunk_length = obj.chunk_length + recons = [] + + for i in range_fn(0, codes.shape[-1], chunk_length): + c = codes[..., i : i + chunk_length].to(self.device) + z = self.quantizer.from_codes(c)[0] + r = self.decode(z) + recons.append(r.to(original_device)) + + recons = torch.cat(recons, dim=-1) + recons = AudioSignal(recons, self.sample_rate) + + resample_fn = recons.resample + loudness_fn = recons.loudness + + # If audio is > 10 minutes long, use the ffmpeg versions + if recons.signal_duration >= 10 * 60 * 60: + resample_fn = recons.ffmpeg_resample + loudness_fn = recons.ffmpeg_loudness + + if obj.input_db is not None: + recons.normalize(obj.input_db) + + resample_fn(obj.sample_rate) + + if obj.original_length is not None: + recons = recons[..., : obj.original_length] + loudness_fn() + recons.audio_data = recons.audio_data.reshape( + -1, obj.channels, obj.original_length + ) + else: + loudness_fn() + + self.padding = original_padding + return recons diff --git a/hunyuanvideo_foley/models/dac_vae/model/dac.py b/hunyuanvideo_foley/models/dac_vae/model/dac.py new file mode 100644 index 0000000..549e4f7 --- /dev/null +++ b/hunyuanvideo_foley/models/dac_vae/model/dac.py @@ -0,0 +1,410 @@ +import math +from typing import List +from typing import Union + +import numpy as np +import torch +from audiotools import AudioSignal +from audiotools.ml import BaseModel +from torch import nn + +from .base import CodecMixin +from ..nn.layers import Snake1d +from ..nn.layers import WNConv1d +from ..nn.layers import WNConvTranspose1d +from ..nn.quantize import ResidualVectorQuantize +from ..nn.vae_utils import DiagonalGaussianDistribution + + +def init_weights(m): + if isinstance(m, nn.Conv1d): + nn.init.trunc_normal_(m.weight, std=0.02) + nn.init.constant_(m.bias, 0) + + +class ResidualUnit(nn.Module): + def __init__(self, dim: int = 16, dilation: int = 1): + super().__init__() + pad = ((7 - 1) * dilation) // 2 + self.block = nn.Sequential( + Snake1d(dim), + WNConv1d(dim, dim, kernel_size=7, dilation=dilation, padding=pad), + Snake1d(dim), + WNConv1d(dim, dim, kernel_size=1), + ) + + def forward(self, x): + y = self.block(x) + pad = (x.shape[-1] - y.shape[-1]) // 2 + if pad > 0: + x = x[..., pad:-pad] + return x + y + + +class EncoderBlock(nn.Module): + def __init__(self, dim: int = 16, stride: int = 1): + super().__init__() + self.block = nn.Sequential( + ResidualUnit(dim // 2, dilation=1), + ResidualUnit(dim // 2, dilation=3), + ResidualUnit(dim // 2, dilation=9), + Snake1d(dim // 2), + WNConv1d( + dim // 2, + dim, + kernel_size=2 * stride, + stride=stride, + padding=math.ceil(stride / 2), + ), + ) + + def forward(self, x): + return self.block(x) + + +class Encoder(nn.Module): + def __init__( + self, + d_model: int = 64, + strides: list = [2, 4, 8, 8], + d_latent: int = 64, + ): + super().__init__() + # Create first convolution + self.block = [WNConv1d(1, d_model, kernel_size=7, padding=3)] + + # Create EncoderBlocks that double channels as they downsample by `stride` + for stride in strides: + d_model *= 2 + self.block += [EncoderBlock(d_model, stride=stride)] + + # Create last convolution + self.block += [ + Snake1d(d_model), + WNConv1d(d_model, d_latent, kernel_size=3, padding=1), + ] + + # Wrap black into nn.Sequential + self.block = nn.Sequential(*self.block) + self.enc_dim = d_model + + def forward(self, x): + return self.block(x) + + +class DecoderBlock(nn.Module): + def __init__(self, input_dim: int = 16, output_dim: int = 8, stride: int = 1): + super().__init__() + self.block = nn.Sequential( + Snake1d(input_dim), + WNConvTranspose1d( + input_dim, + output_dim, + kernel_size=2 * stride, + stride=stride, + padding=math.ceil(stride / 2), + output_padding=stride % 2, + ), + ResidualUnit(output_dim, dilation=1), + ResidualUnit(output_dim, dilation=3), + ResidualUnit(output_dim, dilation=9), + ) + + def forward(self, x): + return self.block(x) + + +class Decoder(nn.Module): + def __init__( + self, + input_channel, + channels, + rates, + d_out: int = 1, + ): + super().__init__() + + # Add first conv layer + layers = [WNConv1d(input_channel, channels, kernel_size=7, padding=3)] + + # Add upsampling + MRF blocks + for i, stride in enumerate(rates): + input_dim = channels // 2**i + output_dim = channels // 2 ** (i + 1) + layers += [DecoderBlock(input_dim, output_dim, stride)] + + # Add final conv layer + layers += [ + Snake1d(output_dim), + WNConv1d(output_dim, d_out, kernel_size=7, padding=3), + nn.Tanh(), + ] + + self.model = nn.Sequential(*layers) + + def forward(self, x): + return self.model(x) + + +class DAC(BaseModel, CodecMixin): + def __init__( + self, + encoder_dim: int = 64, + encoder_rates: List[int] = [2, 4, 8, 8], + latent_dim: int = None, + decoder_dim: int = 1536, + decoder_rates: List[int] = [8, 8, 4, 2], + n_codebooks: int = 9, + codebook_size: int = 1024, + codebook_dim: Union[int, list] = 8, + quantizer_dropout: bool = False, + sample_rate: int = 44100, + continuous: bool = False, + ): + super().__init__() + + self.encoder_dim = encoder_dim + self.encoder_rates = encoder_rates + self.decoder_dim = decoder_dim + self.decoder_rates = decoder_rates + self.sample_rate = sample_rate + self.continuous = continuous + + if latent_dim is None: + latent_dim = encoder_dim * (2 ** len(encoder_rates)) + + self.latent_dim = latent_dim + + self.hop_length = np.prod(encoder_rates) + self.encoder = Encoder(encoder_dim, encoder_rates, latent_dim) + + if not continuous: + self.n_codebooks = n_codebooks + self.codebook_size = codebook_size + self.codebook_dim = codebook_dim + self.quantizer = ResidualVectorQuantize( + input_dim=latent_dim, + n_codebooks=n_codebooks, + codebook_size=codebook_size, + codebook_dim=codebook_dim, + quantizer_dropout=quantizer_dropout, + ) + else: + self.quant_conv = torch.nn.Conv1d(latent_dim, 2 * latent_dim, 1) + self.post_quant_conv = torch.nn.Conv1d(latent_dim, latent_dim, 1) + + self.decoder = Decoder( + latent_dim, + decoder_dim, + decoder_rates, + ) + self.sample_rate = sample_rate + self.apply(init_weights) + + self.delay = self.get_delay() + + @property + def dtype(self): + """Get the dtype of the model parameters.""" + # Return the dtype of the first parameter found + for param in self.parameters(): + return param.dtype + return torch.float32 # fallback + + @property + def device(self): + """Get the device of the model parameters.""" + # Return the device of the first parameter found + for param in self.parameters(): + return param.device + return torch.device('cpu') # fallback + + def preprocess(self, audio_data, sample_rate): + if sample_rate is None: + sample_rate = self.sample_rate + assert sample_rate == self.sample_rate + + length = audio_data.shape[-1] + right_pad = math.ceil(length / self.hop_length) * self.hop_length - length + audio_data = nn.functional.pad(audio_data, (0, right_pad)) + + return audio_data + + def encode( + self, + audio_data: torch.Tensor, + n_quantizers: int = None, + ): + """Encode given audio data and return quantized latent codes + + Parameters + ---------- + audio_data : Tensor[B x 1 x T] + Audio data to encode + n_quantizers : int, optional + Number of quantizers to use, by default None + If None, all quantizers are used. + + Returns + ------- + dict + A dictionary with the following keys: + "z" : Tensor[B x D x T] + Quantized continuous representation of input + "codes" : Tensor[B x N x T] + Codebook indices for each codebook + (quantized discrete representation of input) + "latents" : Tensor[B x N*D x T] + Projected latents (continuous representation of input before quantization) + "vq/commitment_loss" : Tensor[1] + Commitment loss to train encoder to predict vectors closer to codebook + entries + "vq/codebook_loss" : Tensor[1] + Codebook loss to update the codebook + "length" : int + Number of samples in input audio + """ + z = self.encoder(audio_data) # [B x D x T] + if not self.continuous: + z, codes, latents, commitment_loss, codebook_loss = self.quantizer(z, n_quantizers) + else: + z = self.quant_conv(z) # [B x 2D x T] + z = DiagonalGaussianDistribution(z) + codes, latents, commitment_loss, codebook_loss = None, None, 0, 0 + + return z, codes, latents, commitment_loss, codebook_loss + + def decode(self, z: torch.Tensor): + """Decode given latent codes and return audio data + + Parameters + ---------- + z : Tensor[B x D x T] + Quantized continuous representation of input + length : int, optional + Number of samples in output audio, by default None + + Returns + ------- + dict + A dictionary with the following keys: + "audio" : Tensor[B x 1 x length] + Decoded audio data. + """ + if not self.continuous: + audio = self.decoder(z) + else: + z = self.post_quant_conv(z) + audio = self.decoder(z) + + return audio + + def forward( + self, + audio_data: torch.Tensor, + sample_rate: int = None, + n_quantizers: int = None, + ): + """Model forward pass + + Parameters + ---------- + audio_data : Tensor[B x 1 x T] + Audio data to encode + sample_rate : int, optional + Sample rate of audio data in Hz, by default None + If None, defaults to `self.sample_rate` + n_quantizers : int, optional + Number of quantizers to use, by default None. + If None, all quantizers are used. + + Returns + ------- + dict + A dictionary with the following keys: + "z" : Tensor[B x D x T] + Quantized continuous representation of input + "codes" : Tensor[B x N x T] + Codebook indices for each codebook + (quantized discrete representation of input) + "latents" : Tensor[B x N*D x T] + Projected latents (continuous representation of input before quantization) + "vq/commitment_loss" : Tensor[1] + Commitment loss to train encoder to predict vectors closer to codebook + entries + "vq/codebook_loss" : Tensor[1] + Codebook loss to update the codebook + "length" : int + Number of samples in input audio + "audio" : Tensor[B x 1 x length] + Decoded audio data. + """ + length = audio_data.shape[-1] + audio_data = self.preprocess(audio_data, sample_rate) + if not self.continuous: + z, codes, latents, commitment_loss, codebook_loss = self.encode(audio_data, n_quantizers) + + x = self.decode(z) + return { + "audio": x[..., :length], + "z": z, + "codes": codes, + "latents": latents, + "vq/commitment_loss": commitment_loss, + "vq/codebook_loss": codebook_loss, + } + else: + posterior, _, _, _, _ = self.encode(audio_data, n_quantizers) + z = posterior.sample() + x = self.decode(z) + + kl_loss = posterior.kl() + kl_loss = kl_loss.mean() + + return { + "audio": x[..., :length], + "z": z, + "kl_loss": kl_loss, + } + + +if __name__ == "__main__": + import numpy as np + from functools import partial + + model = DAC().to("cpu") + + for n, m in model.named_modules(): + o = m.extra_repr() + p = sum([np.prod(p.size()) for p in m.parameters()]) + fn = lambda o, p: o + f" {p/1e6:<.3f}M params." + setattr(m, "extra_repr", partial(fn, o=o, p=p)) + print(model) + print("Total # of params: ", sum([np.prod(p.size()) for p in model.parameters()])) + + length = 88200 * 2 + x = torch.randn(1, 1, length).to(model.device) + x.requires_grad_(True) + x.retain_grad() + + # Make a forward pass + out = model(x)["audio"] + print("Input shape:", x.shape) + print("Output shape:", out.shape) + + # Create gradient variable + grad = torch.zeros_like(out) + grad[:, :, grad.shape[-1] // 2] = 1 + + # Make a backward pass + out.backward(grad) + + # Check non-zero values + gradmap = x.grad.squeeze(0) + gradmap = (gradmap != 0).sum(0) # sum across features + rf = (gradmap != 0).sum() + + print(f"Receptive field: {rf.item()}") + + x = AudioSignal(torch.randn(1, 1, 44100 * 60), 44100) + model.decompress(model.compress(x, verbose=True), verbose=True) diff --git a/hunyuanvideo_foley/models/dac_vae/model/discriminator.py b/hunyuanvideo_foley/models/dac_vae/model/discriminator.py new file mode 100644 index 0000000..ade8e6d --- /dev/null +++ b/hunyuanvideo_foley/models/dac_vae/model/discriminator.py @@ -0,0 +1,228 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F +from audiotools import AudioSignal +from audiotools import ml +from audiotools import STFTParams +from einops import rearrange +from torch.nn.utils import weight_norm + + +def WNConv1d(*args, **kwargs): + act = kwargs.pop("act", True) + conv = weight_norm(nn.Conv1d(*args, **kwargs)) + if not act: + return conv + return nn.Sequential(conv, nn.LeakyReLU(0.1)) + + +def WNConv2d(*args, **kwargs): + act = kwargs.pop("act", True) + conv = weight_norm(nn.Conv2d(*args, **kwargs)) + if not act: + return conv + return nn.Sequential(conv, nn.LeakyReLU(0.1)) + + +class MPD(nn.Module): + def __init__(self, period): + super().__init__() + self.period = period + self.convs = nn.ModuleList( + [ + WNConv2d(1, 32, (5, 1), (3, 1), padding=(2, 0)), + WNConv2d(32, 128, (5, 1), (3, 1), padding=(2, 0)), + WNConv2d(128, 512, (5, 1), (3, 1), padding=(2, 0)), + WNConv2d(512, 1024, (5, 1), (3, 1), padding=(2, 0)), + WNConv2d(1024, 1024, (5, 1), 1, padding=(2, 0)), + ] + ) + self.conv_post = WNConv2d( + 1024, 1, kernel_size=(3, 1), padding=(1, 0), act=False + ) + + def pad_to_period(self, x): + t = x.shape[-1] + x = F.pad(x, (0, self.period - t % self.period), mode="reflect") + return x + + def forward(self, x): + fmap = [] + + x = self.pad_to_period(x) + x = rearrange(x, "b c (l p) -> b c l p", p=self.period) + + for layer in self.convs: + x = layer(x) + fmap.append(x) + + x = self.conv_post(x) + fmap.append(x) + + return fmap + + +class MSD(nn.Module): + def __init__(self, rate: int = 1, sample_rate: int = 44100): + super().__init__() + self.convs = nn.ModuleList( + [ + WNConv1d(1, 16, 15, 1, padding=7), + WNConv1d(16, 64, 41, 4, groups=4, padding=20), + WNConv1d(64, 256, 41, 4, groups=16, padding=20), + WNConv1d(256, 1024, 41, 4, groups=64, padding=20), + WNConv1d(1024, 1024, 41, 4, groups=256, padding=20), + WNConv1d(1024, 1024, 5, 1, padding=2), + ] + ) + self.conv_post = WNConv1d(1024, 1, 3, 1, padding=1, act=False) + self.sample_rate = sample_rate + self.rate = rate + + def forward(self, x): + x = AudioSignal(x, self.sample_rate) + x.resample(self.sample_rate // self.rate) + x = x.audio_data + + fmap = [] + + for l in self.convs: + x = l(x) + fmap.append(x) + x = self.conv_post(x) + fmap.append(x) + + return fmap + + +BANDS = [(0.0, 0.1), (0.1, 0.25), (0.25, 0.5), (0.5, 0.75), (0.75, 1.0)] + + +class MRD(nn.Module): + def __init__( + self, + window_length: int, + hop_factor: float = 0.25, + sample_rate: int = 44100, + bands: list = BANDS, + ): + """Complex multi-band spectrogram discriminator. + Parameters + ---------- + window_length : int + Window length of STFT. + hop_factor : float, optional + Hop factor of the STFT, defaults to ``0.25 * window_length``. + sample_rate : int, optional + Sampling rate of audio in Hz, by default 44100 + bands : list, optional + Bands to run discriminator over. + """ + super().__init__() + + self.window_length = window_length + self.hop_factor = hop_factor + self.sample_rate = sample_rate + self.stft_params = STFTParams( + window_length=window_length, + hop_length=int(window_length * hop_factor), + match_stride=True, + ) + + n_fft = window_length // 2 + 1 + bands = [(int(b[0] * n_fft), int(b[1] * n_fft)) for b in bands] + self.bands = bands + + ch = 32 + convs = lambda: nn.ModuleList( + [ + WNConv2d(2, ch, (3, 9), (1, 1), padding=(1, 4)), + WNConv2d(ch, ch, (3, 9), (1, 2), padding=(1, 4)), + WNConv2d(ch, ch, (3, 9), (1, 2), padding=(1, 4)), + WNConv2d(ch, ch, (3, 9), (1, 2), padding=(1, 4)), + WNConv2d(ch, ch, (3, 3), (1, 1), padding=(1, 1)), + ] + ) + self.band_convs = nn.ModuleList([convs() for _ in range(len(self.bands))]) + self.conv_post = WNConv2d(ch, 1, (3, 3), (1, 1), padding=(1, 1), act=False) + + def spectrogram(self, x): + x = AudioSignal(x, self.sample_rate, stft_params=self.stft_params) + x = torch.view_as_real(x.stft()) + x = rearrange(x, "b 1 f t c -> (b 1) c t f") + # Split into bands + x_bands = [x[..., b[0] : b[1]] for b in self.bands] + return x_bands + + def forward(self, x): + x_bands = self.spectrogram(x) + fmap = [] + + x = [] + for band, stack in zip(x_bands, self.band_convs): + for layer in stack: + band = layer(band) + fmap.append(band) + x.append(band) + + x = torch.cat(x, dim=-1) + x = self.conv_post(x) + fmap.append(x) + + return fmap + + +class Discriminator(ml.BaseModel): + def __init__( + self, + rates: list = [], + periods: list = [2, 3, 5, 7, 11], + fft_sizes: list = [2048, 1024, 512], + sample_rate: int = 44100, + bands: list = BANDS, + ): + """Discriminator that combines multiple discriminators. + + Parameters + ---------- + rates : list, optional + sampling rates (in Hz) to run MSD at, by default [] + If empty, MSD is not used. + periods : list, optional + periods (of samples) to run MPD at, by default [2, 3, 5, 7, 11] + fft_sizes : list, optional + Window sizes of the FFT to run MRD at, by default [2048, 1024, 512] + sample_rate : int, optional + Sampling rate of audio in Hz, by default 44100 + bands : list, optional + Bands to run MRD at, by default `BANDS` + """ + super().__init__() + discs = [] + discs += [MPD(p) for p in periods] + discs += [MSD(r, sample_rate=sample_rate) for r in rates] + discs += [MRD(f, sample_rate=sample_rate, bands=bands) for f in fft_sizes] + self.discriminators = nn.ModuleList(discs) + + def preprocess(self, y): + # Remove DC offset + y = y - y.mean(dim=-1, keepdims=True) + # Peak normalize the volume of input audio + y = 0.8 * y / (y.abs().max(dim=-1, keepdim=True)[0] + 1e-9) + return y + + def forward(self, x): + x = self.preprocess(x) + fmaps = [d(x) for d in self.discriminators] + return fmaps + + +if __name__ == "__main__": + disc = Discriminator() + x = torch.zeros(1, 1, 44100) + results = disc(x) + for i, result in enumerate(results): + print(f"disc{i}") + for i, r in enumerate(result): + print(r.shape, r.mean(), r.min(), r.max()) + print() diff --git a/hunyuanvideo_foley/models/dac_vae/nn/__init__.py b/hunyuanvideo_foley/models/dac_vae/nn/__init__.py new file mode 100644 index 0000000..ec2f6b9 --- /dev/null +++ b/hunyuanvideo_foley/models/dac_vae/nn/__init__.py @@ -0,0 +1,3 @@ +from . import layers +from . import loss +from . import quantize diff --git a/hunyuanvideo_foley/models/dac_vae/nn/__pycache__/__init__.cpython-312.pyc b/hunyuanvideo_foley/models/dac_vae/nn/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..04db565246c0d8d88056feaba93d2f754a5d00cd GIT binary patch literal 289 zcmX@j%ge<81YW@#GMs?)V-N=hn4pZ$8bHQ$h7^Vr#vF!R#wbQc5St0eW{P40vzdWx z<|t-HhDsJq)|ZSx<(iDQ*m4ppQ;Uj$ES8-7Vi3(ySelqul3A7Nr^#}Qp@=S4s~G3}+_cJ2&zR)W;*$K__`Lj-)MA)myhmwXCD5F(%#_r8xBQ&c z%9sp@NEt{ZJ`F6C3)GTR9Fvlm9AB208k3h76Ca4BO~Ko29diALiZTNia3Bm0Hp9uxc~qF literal 0 HcmV?d00001 diff --git a/hunyuanvideo_foley/models/dac_vae/nn/__pycache__/layers.cpython-312.pyc b/hunyuanvideo_foley/models/dac_vae/nn/__pycache__/layers.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4ee24c6a9c593ce52fd8a711ab827da376032a68 GIT binary patch literal 2232 zcmcH)TWl0n^xm1--PsqVMOaXjg+64~q)E~E0oVvwUWtuZ&}6g8IP6Tjv&_z{cV>ZY zmSjzu(0(A0ND5zKLLyB~tbcs^?@u>tthvx=;*b3?NYM~~^xQjpyY=zwN#>q&&pG$r z^PX?g=>!5eJ+V|eEhF?3+X#qsi1i0R%p((-*g<*Bv6PoMmMcnL!J#ZtQHl~qm1sW3 zIg+DR;`unIWhYTd=954xGbo=jBe13|sk~b}*2U$Qy0oCo=nPuyZ1P(rbOM>N8Dy%K zQr;(Yx_A*hVUX{uV=cbM$Z-wRII#>u40pYZ`SV)EN4dU z54AEQ?c{-F<-j_K(f<`55{1h<0U|8yw1=5@7EMEJ3AZtf4@ohN-q3~3I=#SW z{`fD#((z>_qqQ>H{nwT=2bW%NWe&EJhnAH?WD95%*>wV#1uU)WzrZq&8mKHJoA4D0 z_#4UZ3L27$cfl60#IPxg!n*=DHL$5n1Gg;NsC#gulmq59hq5ZDE^goknugP&pvuXv zOi*S-NSP*kkG=ih)(TCgF@dz`IANh!W97Q`C&1LL?}mO(4)>0(F}@b15x-nG+;G{?SI zHP}wQ{Kepx+Jd$;_Tb1@r`oyEmE6%*?&#Me?c7)^HP%wc_?hswwz?B`$yD=Xn0k10 z14J3D4*=^bB;Vq*EicWYNo?X7H7}bIOetj+UB>wc1Wcx}3FgI!`L`NkIpj6e1mYHk zkpVb_IjEd#EQ6grM!`lklv(Zl0|Hj>9Ljtwi(QCK3^j#CjfreFkiKzK9cj`y*#_!b z46;|NT7;^)Zo77%>tTz<-!X_$u>y+_RTRXuR77wsl?!N-dGw4D!Jr`@KJHJ(*f3!U_U$|MgH~4w(v)uhN zOX`EY?R|&a+uxW~t|k86k_ueF!#NDZ=K0Iop)9fnG69Pn-UJEi*x)4HLrwLt$ zd0TTBoznFWYlag_c!Wp}?xE>U42zcI=(>itH caC%)vQr}}nJ>43`1GDF^+jI7B2uS|sUpW%icK`qY literal 0 HcmV?d00001 diff --git a/hunyuanvideo_foley/models/dac_vae/nn/__pycache__/loss.cpython-312.pyc b/hunyuanvideo_foley/models/dac_vae/nn/__pycache__/loss.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0eb6199daf1a5d86fc2250051ddd98cafbd82bc4 GIT binary patch literal 16416 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 literal 0 HcmV?d00001 diff --git a/hunyuanvideo_foley/models/dac_vae/nn/__pycache__/quantize.cpython-312.pyc b/hunyuanvideo_foley/models/dac_vae/nn/__pycache__/quantize.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f79fc88c52843508ea423a105baf9b81402cfdd4 GIT binary patch literal 12444 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 w?AMN_jp4*&oF literal 0 HcmV?d00001 diff --git a/hunyuanvideo_foley/models/dac_vae/nn/__pycache__/vae_utils.cpython-312.pyc b/hunyuanvideo_foley/models/dac_vae/nn/__pycache__/vae_utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fb5e62d65814cbfe610756a56e39ea72306ff2cf GIT binary patch literal 5990 zcmcIoU2GKB6`ucH+lznK#x{X0Hjpe3j2+`(aQHPC)6}IUgkT8E?9O<-_Ux=XGi$TU zqF50RMj{H9QrSq6u&YSe0tqTrDkznpZ$y2_PM6B=#1BY)NO`kaq^f=DIWs%!#SCt$ zw)f%8%)RHF`}ck4o;!bBv!UrhQ?ldnFQ@;4DZ39swsR(>x{rN5{zq+m>jaL(Fu+XN0O}pF+phg zw%hQHcqa70mJ3RE+yDD|mm<@;x-bI-ZMv1ni7RQI`1 zT-9RHfE*J<)vgR2i_3{PFMkvf#Msf8Bqq-FTQ$R26X-LmqF^Ga=L9|!80N)uvV0C6 z0T{WY?iflKoXX=UlIxtNw!>=VU^v+l5TnFG>BdZHW7^%Ag-_x?ow|aa6c>sj5Y{{u z^lUIV&SahAD6k3jl{F|zLG<`p!yO37d{hhsjN(85RN@l8djf%9#(BwVQ2=CwQVwF) zvNQWq7_3Esm{U>b3h|sG*-cpvgxP`F?2M5Opzn+kfd-_o2?hpN(HQ}pyabaRY{9w! zCY7eM6qzCdW&?@@VXhOPfgR?hxag-8q>WMsLPak=pS5PN0uec)1p>+XSGwC_x8ZWB zxO$_+e0BX8^Lep3!I!oU@n)j79$qGh6fpqLmF1*0ECo5Ta{J7MPS(CU3JX9LV3Ngk zhB1^?eUZGHOuOs;AH%?{PS(6S3`CF@$NdHh3qhQM!%}bulqwWB`DMf?rar1#pg}?_ zC=i4ynp&V)_Z&b|kI1RFkb^oeA;;eMY#X!`Ag2i83u(s}p;%zxixpUb3sZfh;DRqZ zYSI1@Lc^i1oTNQXRqx#FP07LGVYw$~8gnUdc^p&FE3xk7M3OwWbuP6fN>^lkbzMYG= z;4R<#>fW4v75G8wm<2Qu!Yhh$qQE~wW<>$3{JfT%Z2tpmm+R~|u&`eXo&!j`73Gf0 z_Hz;UxL~Bqvrj-c@wOfebg-A4P`P%m=_tJXc2dn1ke$e0qaX1nZ{cDWk zAzpzftBHzguv$zB^&4ymj+sG-*di|*oD>TW^9oc-aF7^l1XPVeSW>k}$e_iGLk6vC zft=fIS3Dx0Y&@j9H$ZjK_0})7#ts*+H`B%^tPG!Jt0%G z7urhZ*KZwTp1A85s%j_N$J-}QPPN|JJH1z5zavxC1PX4u;lrC}Z=B6k`LS-kq-?Ha z{hvzKFEqS0*U+LjwA^d`UHfm^GY#+0H5||z4m@VQEBm%=?$~+#*!gtBflNan?Fo!^ zUORSO`_;uon(%CRH5%q=`~;@-x}XeTvBL{!*+oNJvp9k8H2}*K#OzVegPOOp&5V@N=0=ktYd1)uU{IFTY#a1`lN?3O@*gRd`vD4OZ>vheYLFtT*9d z;>9`*4f0~)S!f+VjjQ6sI*S^?o7x2eP~s{7qWfz1By-b!!~OLx+~*nXZeliZ_tV)= zGmZP_8V~7>haPYGuIbyRx#Jh~;}_D6hcb=4Ud!tqevF(iD<5-Pr%9-A7hK>SL7Xjx z+Sl!zQd1B(@az!A+`E@%9L45SIKowXx1eJnThrNXTasfAnZmq|+SCG32iWpyMS_E16CnK{Vp>YT72L>6lFE0m^3lyq1cGhjC$Kq zFGmM9ru9RNMi|=ZAvGu^-&~augVI}|uRa3-Y{&;O2z?#n9aB|PY^G{U+Oy?pnRo2+ z{M!1jPTlOi(YxTSo9G_zo)m6IZbW7(GTv=7!rlJae%;%4ze@LZ+}9pndT>ei9-ptP zpFB9pyRFg(`&Xa(PZ_ur!{p;ELB~<$PsJJFuvrXs~Z;G#M(C|q;YAfh^cObe(Kkm z{jYIh|1Rv`RZ(drY34fP(ua_!c0nMT#61Kj<^P7b8U_QiIOao?mZydzY=KI4@Vrb5 zbOC7`V3(o+;tl~A;aFgaN%D$c|$yEVqDn^ZPpoqxc4(w#(*neFp$fr zh8t|Ok=Rf~#YhUtL+*w|%aWAzt;zw=z^>IxAXX#6LRIxd+j!e#a0xss+pX&-9Db8m6Kt7`{Xl3lyaq3zp17o?&;VtbliSa>TQnblM z5}8}jkHe9Ob3!fKpJ@jyHaX8GAr}4{d%9%V7UC4zTf7fZq&cn;!q4$rVWWzXk!mci zgv33*eoY%v_cS$yBU*nvxFZycHpPZSnU6Guhpsw(1NuEr0VTg)-Q z@3f(~Ci>v4^!?-mpAZ=qm9Qv>L|;(UMnqBeK@{Q1nh1YzN2u^)589G$Lar{H*i^2~;bjtg3xd1XL7g!e`npuROGm0Z> zL{>E(W@Kg#R#fFQD}68%=_auAy9^d@Z_S3wy1q4A-!1lci_#5h1|16yDDT0WioC;3 zgkt@BDw1PLl$Qd7lJYZX>q4c!ffywgONr9T(WCR_)z?R+*r}0BS<@K3P*Qz8^wqAL z9XC4uT=MpOb?w;Ezj!JZT}1g#IJP~N>5}znrYrqsR|aIsdZ-h~K{73=dQi)(je|^o za{jB{8S>7qyB)I~PuB0gLH=HRC_RuKH-8hG+jm0WcOw1s)0urA>$M-JgPIODjCZta z?DW%;(s{RMY~*U`s}L4!RC|7>JF2_%K*G9vw<3 z`{CXC#fX1`3=Y;%3~6R4GnZ;bfHd44*3Zj*$}d14*g~kSAQowoB)=y--xHO8=U8&n k5&_~xxtpv{*KU77!1G1TcCv1&?F9jk?0M2la%RZ>7fB70c>n+a literal 0 HcmV?d00001 diff --git a/hunyuanvideo_foley/models/dac_vae/nn/layers.py b/hunyuanvideo_foley/models/dac_vae/nn/layers.py new file mode 100644 index 0000000..63c53a8 --- /dev/null +++ b/hunyuanvideo_foley/models/dac_vae/nn/layers.py @@ -0,0 +1,33 @@ +import numpy as np +import torch +import torch.nn as nn +import torch.nn.functional as F +from einops import rearrange +from torch.nn.utils import weight_norm + + +def WNConv1d(*args, **kwargs): + return weight_norm(nn.Conv1d(*args, **kwargs)) + + +def WNConvTranspose1d(*args, **kwargs): + return weight_norm(nn.ConvTranspose1d(*args, **kwargs)) + + +# Scripting this brings model speed up 1.4x +@torch.jit.script +def snake(x, alpha): + shape = x.shape + x = x.reshape(shape[0], shape[1], -1) + x = x + (alpha + 1e-9).reciprocal() * torch.sin(alpha * x).pow(2) + x = x.reshape(shape) + return x + + +class Snake1d(nn.Module): + def __init__(self, channels): + super().__init__() + self.alpha = nn.Parameter(torch.ones(1, channels, 1)) + + def forward(self, x): + return snake(x, self.alpha) diff --git a/hunyuanvideo_foley/models/dac_vae/nn/loss.py b/hunyuanvideo_foley/models/dac_vae/nn/loss.py new file mode 100644 index 0000000..7a26e62 --- /dev/null +++ b/hunyuanvideo_foley/models/dac_vae/nn/loss.py @@ -0,0 +1,368 @@ +import typing +from typing import List + +import torch +import torch.nn.functional as F +from audiotools import AudioSignal +from audiotools import STFTParams +from torch import nn + + +class L1Loss(nn.L1Loss): + """L1 Loss between AudioSignals. Defaults + to comparing ``audio_data``, but any + attribute of an AudioSignal can be used. + + Parameters + ---------- + attribute : str, optional + Attribute of signal to compare, defaults to ``audio_data``. + weight : float, optional + Weight of this loss, defaults to 1.0. + + Implementation copied from: https://github.com/descriptinc/lyrebird-audiotools/blob/961786aa1a9d628cca0c0486e5885a457fe70c1a/audiotools/metrics/distance.py + """ + + def __init__(self, attribute: str = "audio_data", weight: float = 1.0, **kwargs): + self.attribute = attribute + self.weight = weight + super().__init__(**kwargs) + + def forward(self, x: AudioSignal, y: AudioSignal): + """ + Parameters + ---------- + x : AudioSignal + Estimate AudioSignal + y : AudioSignal + Reference AudioSignal + + Returns + ------- + torch.Tensor + L1 loss between AudioSignal attributes. + """ + if isinstance(x, AudioSignal): + x = getattr(x, self.attribute) + y = getattr(y, self.attribute) + return super().forward(x, y) + + +class SISDRLoss(nn.Module): + """ + Computes the Scale-Invariant Source-to-Distortion Ratio between a batch + of estimated and reference audio signals or aligned features. + + Parameters + ---------- + scaling : int, optional + Whether to use scale-invariant (True) or + signal-to-noise ratio (False), by default True + reduction : str, optional + How to reduce across the batch (either 'mean', + 'sum', or none).], by default ' mean' + zero_mean : int, optional + Zero mean the references and estimates before + computing the loss, by default True + clip_min : int, optional + The minimum possible loss value. Helps network + to not focus on making already good examples better, by default None + weight : float, optional + Weight of this loss, defaults to 1.0. + + Implementation copied from: https://github.com/descriptinc/lyrebird-audiotools/blob/961786aa1a9d628cca0c0486e5885a457fe70c1a/audiotools/metrics/distance.py + """ + + def __init__( + self, + scaling: int = True, + reduction: str = "mean", + zero_mean: int = True, + clip_min: int = None, + weight: float = 1.0, + ): + self.scaling = scaling + self.reduction = reduction + self.zero_mean = zero_mean + self.clip_min = clip_min + self.weight = weight + super().__init__() + + def forward(self, x: AudioSignal, y: AudioSignal): + eps = 1e-8 + # nb, nc, nt + if isinstance(x, AudioSignal): + references = x.audio_data + estimates = y.audio_data + else: + references = x + estimates = y + + nb = references.shape[0] + references = references.reshape(nb, 1, -1).permute(0, 2, 1) + estimates = estimates.reshape(nb, 1, -1).permute(0, 2, 1) + + # samples now on axis 1 + if self.zero_mean: + mean_reference = references.mean(dim=1, keepdim=True) + mean_estimate = estimates.mean(dim=1, keepdim=True) + else: + mean_reference = 0 + mean_estimate = 0 + + _references = references - mean_reference + _estimates = estimates - mean_estimate + + references_projection = (_references**2).sum(dim=-2) + eps + references_on_estimates = (_estimates * _references).sum(dim=-2) + eps + + scale = ( + (references_on_estimates / references_projection).unsqueeze(1) + if self.scaling + else 1 + ) + + e_true = scale * _references + e_res = _estimates - e_true + + signal = (e_true**2).sum(dim=1) + noise = (e_res**2).sum(dim=1) + sdr = -10 * torch.log10(signal / noise + eps) + + if self.clip_min is not None: + sdr = torch.clamp(sdr, min=self.clip_min) + + if self.reduction == "mean": + sdr = sdr.mean() + elif self.reduction == "sum": + sdr = sdr.sum() + return sdr + + +class MultiScaleSTFTLoss(nn.Module): + """Computes the multi-scale STFT loss from [1]. + + Parameters + ---------- + window_lengths : List[int], optional + Length of each window of each STFT, by default [2048, 512] + loss_fn : typing.Callable, optional + How to compare each loss, by default nn.L1Loss() + clamp_eps : float, optional + Clamp on the log magnitude, below, by default 1e-5 + mag_weight : float, optional + Weight of raw magnitude portion of loss, by default 1.0 + log_weight : float, optional + Weight of log magnitude portion of loss, by default 1.0 + pow : float, optional + Power to raise magnitude to before taking log, by default 2.0 + weight : float, optional + Weight of this loss, by default 1.0 + match_stride : bool, optional + Whether to match the stride of convolutional layers, by default False + + References + ---------- + + 1. Engel, Jesse, Chenjie Gu, and Adam Roberts. + "DDSP: Differentiable Digital Signal Processing." + International Conference on Learning Representations. 2019. + + Implementation copied from: https://github.com/descriptinc/lyrebird-audiotools/blob/961786aa1a9d628cca0c0486e5885a457fe70c1a/audiotools/metrics/spectral.py + """ + + def __init__( + self, + window_lengths: List[int] = [2048, 512], + loss_fn: typing.Callable = nn.L1Loss(), + clamp_eps: float = 1e-5, + mag_weight: float = 1.0, + log_weight: float = 1.0, + pow: float = 2.0, + weight: float = 1.0, + match_stride: bool = False, + window_type: str = None, + ): + super().__init__() + self.stft_params = [ + STFTParams( + window_length=w, + hop_length=w // 4, + match_stride=match_stride, + window_type=window_type, + ) + for w in window_lengths + ] + self.loss_fn = loss_fn + self.log_weight = log_weight + self.mag_weight = mag_weight + self.clamp_eps = clamp_eps + self.weight = weight + self.pow = pow + + def forward(self, x: AudioSignal, y: AudioSignal): + """Computes multi-scale STFT between an estimate and a reference + signal. + + Parameters + ---------- + x : AudioSignal + Estimate signal + y : AudioSignal + Reference signal + + Returns + ------- + torch.Tensor + Multi-scale STFT loss. + """ + loss = 0.0 + for s in self.stft_params: + x.stft(s.window_length, s.hop_length, s.window_type) + y.stft(s.window_length, s.hop_length, s.window_type) + loss += self.log_weight * self.loss_fn( + x.magnitude.clamp(self.clamp_eps).pow(self.pow).log10(), + y.magnitude.clamp(self.clamp_eps).pow(self.pow).log10(), + ) + loss += self.mag_weight * self.loss_fn(x.magnitude, y.magnitude) + return loss + + +class MelSpectrogramLoss(nn.Module): + """Compute distance between mel spectrograms. Can be used + in a multi-scale way. + + Parameters + ---------- + n_mels : List[int] + Number of mels per STFT, by default [150, 80], + window_lengths : List[int], optional + Length of each window of each STFT, by default [2048, 512] + loss_fn : typing.Callable, optional + How to compare each loss, by default nn.L1Loss() + clamp_eps : float, optional + Clamp on the log magnitude, below, by default 1e-5 + mag_weight : float, optional + Weight of raw magnitude portion of loss, by default 1.0 + log_weight : float, optional + Weight of log magnitude portion of loss, by default 1.0 + pow : float, optional + Power to raise magnitude to before taking log, by default 2.0 + weight : float, optional + Weight of this loss, by default 1.0 + match_stride : bool, optional + Whether to match the stride of convolutional layers, by default False + + Implementation copied from: https://github.com/descriptinc/lyrebird-audiotools/blob/961786aa1a9d628cca0c0486e5885a457fe70c1a/audiotools/metrics/spectral.py + """ + + def __init__( + self, + n_mels: List[int] = [150, 80], + window_lengths: List[int] = [2048, 512], + loss_fn: typing.Callable = nn.L1Loss(), + clamp_eps: float = 1e-5, + mag_weight: float = 1.0, + log_weight: float = 1.0, + pow: float = 2.0, + weight: float = 1.0, + match_stride: bool = False, + mel_fmin: List[float] = [0.0, 0.0], + mel_fmax: List[float] = [None, None], + window_type: str = None, + ): + super().__init__() + self.stft_params = [ + STFTParams( + window_length=w, + hop_length=w // 4, + match_stride=match_stride, + window_type=window_type, + ) + for w in window_lengths + ] + self.n_mels = n_mels + self.loss_fn = loss_fn + self.clamp_eps = clamp_eps + self.log_weight = log_weight + self.mag_weight = mag_weight + self.weight = weight + self.mel_fmin = mel_fmin + self.mel_fmax = mel_fmax + self.pow = pow + + def forward(self, x: AudioSignal, y: AudioSignal): + """Computes mel loss between an estimate and a reference + signal. + + Parameters + ---------- + x : AudioSignal + Estimate signal + y : AudioSignal + Reference signal + + Returns + ------- + torch.Tensor + Mel loss. + """ + loss = 0.0 + for n_mels, fmin, fmax, s in zip( + self.n_mels, self.mel_fmin, self.mel_fmax, self.stft_params + ): + kwargs = { + "window_length": s.window_length, + "hop_length": s.hop_length, + "window_type": s.window_type, + } + x_mels = x.mel_spectrogram(n_mels, mel_fmin=fmin, mel_fmax=fmax, **kwargs) + y_mels = y.mel_spectrogram(n_mels, mel_fmin=fmin, mel_fmax=fmax, **kwargs) + + loss += self.log_weight * self.loss_fn( + x_mels.clamp(self.clamp_eps).pow(self.pow).log10(), + y_mels.clamp(self.clamp_eps).pow(self.pow).log10(), + ) + loss += self.mag_weight * self.loss_fn(x_mels, y_mels) + return loss + + +class GANLoss(nn.Module): + """ + Computes a discriminator loss, given a discriminator on + generated waveforms/spectrograms compared to ground truth + waveforms/spectrograms. Computes the loss for both the + discriminator and the generator in separate functions. + """ + + def __init__(self, discriminator): + super().__init__() + self.discriminator = discriminator + + def forward(self, fake, real): + d_fake = self.discriminator(fake.audio_data) + d_real = self.discriminator(real.audio_data) + return d_fake, d_real + + def discriminator_loss(self, fake, real): + d_fake, d_real = self.forward(fake.clone().detach(), real) + + loss_d = 0 + for x_fake, x_real in zip(d_fake, d_real): + loss_d += torch.mean(x_fake[-1] ** 2) + loss_d += torch.mean((1 - x_real[-1]) ** 2) + return loss_d + + def generator_loss(self, fake, real): + d_fake, d_real = self.forward(fake, real) + + loss_g = 0 + for x_fake in d_fake: + loss_g += torch.mean((1 - x_fake[-1]) ** 2) + + loss_feature = 0 + + for i in range(len(d_fake)): + for j in range(len(d_fake[i]) - 1): + loss_feature += F.l1_loss(d_fake[i][j], d_real[i][j].detach()) + return loss_g, loss_feature diff --git a/hunyuanvideo_foley/models/dac_vae/nn/quantize.py b/hunyuanvideo_foley/models/dac_vae/nn/quantize.py new file mode 100644 index 0000000..499088f --- /dev/null +++ b/hunyuanvideo_foley/models/dac_vae/nn/quantize.py @@ -0,0 +1,262 @@ +from typing import Union + +import numpy as np +import torch +import torch.nn as nn +import torch.nn.functional as F +from einops import rearrange +from torch.nn.utils import weight_norm + +from .layers import WNConv1d + + +class VectorQuantize(nn.Module): + """ + Implementation of VQ similar to Karpathy's repo: + https://github.com/karpathy/deep-vector-quantization + Additionally uses following tricks from Improved VQGAN + (https://arxiv.org/pdf/2110.04627.pdf): + 1. Factorized codes: Perform nearest neighbor lookup in low-dimensional space + for improved codebook usage + 2. l2-normalized codes: Converts euclidean distance to cosine similarity which + improves training stability + """ + + def __init__(self, input_dim: int, codebook_size: int, codebook_dim: int): + super().__init__() + self.codebook_size = codebook_size + self.codebook_dim = codebook_dim + + self.in_proj = WNConv1d(input_dim, codebook_dim, kernel_size=1) + self.out_proj = WNConv1d(codebook_dim, input_dim, kernel_size=1) + self.codebook = nn.Embedding(codebook_size, codebook_dim) + + def forward(self, z): + """Quantized the input tensor using a fixed codebook and returns + the corresponding codebook vectors + + Parameters + ---------- + z : Tensor[B x D x T] + + Returns + ------- + Tensor[B x D x T] + Quantized continuous representation of input + Tensor[1] + Commitment loss to train encoder to predict vectors closer to codebook + entries + Tensor[1] + Codebook loss to update the codebook + Tensor[B x T] + Codebook indices (quantized discrete representation of input) + Tensor[B x D x T] + Projected latents (continuous representation of input before quantization) + """ + + # Factorized codes (ViT-VQGAN) Project input into low-dimensional space + z_e = self.in_proj(z) # z_e : (B x D x T) + z_q, indices = self.decode_latents(z_e) + + commitment_loss = F.mse_loss(z_e, z_q.detach(), reduction="none").mean([1, 2]) + codebook_loss = F.mse_loss(z_q, z_e.detach(), reduction="none").mean([1, 2]) + + z_q = ( + z_e + (z_q - z_e).detach() + ) # noop in forward pass, straight-through gradient estimator in backward pass + + z_q = self.out_proj(z_q) + + return z_q, commitment_loss, codebook_loss, indices, z_e + + def embed_code(self, embed_id): + return F.embedding(embed_id, self.codebook.weight) + + def decode_code(self, embed_id): + return self.embed_code(embed_id).transpose(1, 2) + + def decode_latents(self, latents): + encodings = rearrange(latents, "b d t -> (b t) d") + codebook = self.codebook.weight # codebook: (N x D) + + # L2 normalize encodings and codebook (ViT-VQGAN) + encodings = F.normalize(encodings) + codebook = F.normalize(codebook) + + # Compute euclidean distance with codebook + dist = ( + encodings.pow(2).sum(1, keepdim=True) + - 2 * encodings @ codebook.t() + + codebook.pow(2).sum(1, keepdim=True).t() + ) + indices = rearrange((-dist).max(1)[1], "(b t) -> b t", b=latents.size(0)) + z_q = self.decode_code(indices) + return z_q, indices + + +class ResidualVectorQuantize(nn.Module): + """ + Introduced in SoundStream: An end2end neural audio codec + https://arxiv.org/abs/2107.03312 + """ + + def __init__( + self, + input_dim: int = 512, + n_codebooks: int = 9, + codebook_size: int = 1024, + codebook_dim: Union[int, list] = 8, + quantizer_dropout: float = 0.0, + ): + super().__init__() + if isinstance(codebook_dim, int): + codebook_dim = [codebook_dim for _ in range(n_codebooks)] + + self.n_codebooks = n_codebooks + self.codebook_dim = codebook_dim + self.codebook_size = codebook_size + + self.quantizers = nn.ModuleList( + [ + VectorQuantize(input_dim, codebook_size, codebook_dim[i]) + for i in range(n_codebooks) + ] + ) + self.quantizer_dropout = quantizer_dropout + + def forward(self, z, n_quantizers: int = None): + """Quantized the input tensor using a fixed set of `n` codebooks and returns + the corresponding codebook vectors + Parameters + ---------- + z : Tensor[B x D x T] + n_quantizers : int, optional + No. of quantizers to use + (n_quantizers < self.n_codebooks ex: for quantizer dropout) + Note: if `self.quantizer_dropout` is True, this argument is ignored + when in training mode, and a random number of quantizers is used. + Returns + ------- + dict + A dictionary with the following keys: + + "z" : Tensor[B x D x T] + Quantized continuous representation of input + "codes" : Tensor[B x N x T] + Codebook indices for each codebook + (quantized discrete representation of input) + "latents" : Tensor[B x N*D x T] + Projected latents (continuous representation of input before quantization) + "vq/commitment_loss" : Tensor[1] + Commitment loss to train encoder to predict vectors closer to codebook + entries + "vq/codebook_loss" : Tensor[1] + Codebook loss to update the codebook + """ + z_q = 0 + residual = z + commitment_loss = 0 + codebook_loss = 0 + + codebook_indices = [] + latents = [] + + if n_quantizers is None: + n_quantizers = self.n_codebooks + if self.training: + n_quantizers = torch.ones((z.shape[0],)) * self.n_codebooks + 1 + dropout = torch.randint(1, self.n_codebooks + 1, (z.shape[0],)) + n_dropout = int(z.shape[0] * self.quantizer_dropout) + n_quantizers[:n_dropout] = dropout[:n_dropout] + n_quantizers = n_quantizers.to(z.device) + + for i, quantizer in enumerate(self.quantizers): + if self.training is False and i >= n_quantizers: + break + + z_q_i, commitment_loss_i, codebook_loss_i, indices_i, z_e_i = quantizer( + residual + ) + + # Create mask to apply quantizer dropout + mask = ( + torch.full((z.shape[0],), fill_value=i, device=z.device) < n_quantizers + ) + z_q = z_q + z_q_i * mask[:, None, None] + residual = residual - z_q_i + + # Sum losses + commitment_loss += (commitment_loss_i * mask).mean() + codebook_loss += (codebook_loss_i * mask).mean() + + codebook_indices.append(indices_i) + latents.append(z_e_i) + + codes = torch.stack(codebook_indices, dim=1) + latents = torch.cat(latents, dim=1) + + return z_q, codes, latents, commitment_loss, codebook_loss + + def from_codes(self, codes: torch.Tensor): + """Given the quantized codes, reconstruct the continuous representation + Parameters + ---------- + codes : Tensor[B x N x T] + Quantized discrete representation of input + Returns + ------- + Tensor[B x D x T] + Quantized continuous representation of input + """ + z_q = 0.0 + z_p = [] + n_codebooks = codes.shape[1] + for i in range(n_codebooks): + z_p_i = self.quantizers[i].decode_code(codes[:, i, :]) + z_p.append(z_p_i) + + z_q_i = self.quantizers[i].out_proj(z_p_i) + z_q = z_q + z_q_i + return z_q, torch.cat(z_p, dim=1), codes + + def from_latents(self, latents: torch.Tensor): + """Given the unquantized latents, reconstruct the + continuous representation after quantization. + + Parameters + ---------- + latents : Tensor[B x N x T] + Continuous representation of input after projection + + Returns + ------- + Tensor[B x D x T] + Quantized representation of full-projected space + Tensor[B x D x T] + Quantized representation of latent space + """ + z_q = 0 + z_p = [] + codes = [] + dims = np.cumsum([0] + [q.codebook_dim for q in self.quantizers]) + + n_codebooks = np.where(dims <= latents.shape[1])[0].max(axis=0, keepdims=True)[ + 0 + ] + for i in range(n_codebooks): + j, k = dims[i], dims[i + 1] + z_p_i, codes_i = self.quantizers[i].decode_latents(latents[:, j:k, :]) + z_p.append(z_p_i) + codes.append(codes_i) + + z_q_i = self.quantizers[i].out_proj(z_p_i) + z_q = z_q + z_q_i + + return z_q, torch.cat(z_p, dim=1), torch.stack(codes, dim=1) + + +if __name__ == "__main__": + rvq = ResidualVectorQuantize(quantizer_dropout=True) + x = torch.randn(16, 512, 80) + y = rvq(x) + print(y["latents"].shape) diff --git a/hunyuanvideo_foley/models/dac_vae/nn/vae_utils.py b/hunyuanvideo_foley/models/dac_vae/nn/vae_utils.py new file mode 100644 index 0000000..923c01f --- /dev/null +++ b/hunyuanvideo_foley/models/dac_vae/nn/vae_utils.py @@ -0,0 +1,91 @@ +import torch +import numpy as np + + +class AbstractDistribution: + def sample(self): + raise NotImplementedError() + + def mode(self): + raise NotImplementedError() + + +class DiracDistribution(AbstractDistribution): + def __init__(self, value): + self.value = value + + def sample(self): + return self.value + + def mode(self): + return self.value + + +class DiagonalGaussianDistribution(object): + def __init__(self, parameters, deterministic=False): + self.parameters = parameters + self.mean, self.logvar = torch.chunk(parameters, 2, dim=1) + self.logvar = torch.clamp(self.logvar, -30.0, 20.0) + self.deterministic = deterministic + self.std = torch.exp(0.5 * self.logvar) + self.var = torch.exp(self.logvar) + if self.deterministic: + self.var = self.std = torch.zeros_like(self.mean).to(device=self.parameters.device) + + def sample(self): + x = self.mean + self.std * torch.randn(self.mean.shape).to(device=self.parameters.device) + return x + + def kl(self, other=None): + if self.deterministic: + return torch.Tensor([0.0]) + else: + if other is None: + return 0.5 * torch.mean( + torch.pow(self.mean, 2) + self.var - 1.0 - self.logvar, + dim=[1, 2], + ) + else: + return 0.5 * torch.mean( + torch.pow(self.mean - other.mean, 2) / other.var + + self.var / other.var + - 1.0 + - self.logvar + + other.logvar, + dim=[1, 2], + ) + + def nll(self, sample, dims=[1, 2]): + if self.deterministic: + return torch.Tensor([0.0]) + logtwopi = np.log(2.0 * np.pi) + return 0.5 * torch.sum( + logtwopi + self.logvar + torch.pow(sample - self.mean, 2) / self.var, + dim=dims, + ) + + def mode(self): + return self.mean + + +def normal_kl(mean1, logvar1, mean2, logvar2): + """ + source: https://github.com/openai/guided-diffusion/blob/27c20a8fab9cb472df5d6bdd6c8d11c8f430b924/guided_diffusion/losses.py#L12 + Compute the KL divergence between two gaussians. + Shapes are automatically broadcasted, so batches can be compared to + scalars, among other use cases. + """ + tensor = None + for obj in (mean1, logvar1, mean2, logvar2): + if isinstance(obj, torch.Tensor): + tensor = obj + break + assert tensor is not None, "at least one argument must be a Tensor" + + # Force variances to be Tensors. Broadcasting helps convert scalars to + # Tensors, but it does not work for torch.exp(). + logvar1, logvar2 = [x if isinstance(x, torch.Tensor) else torch.tensor(x).to(tensor) for x in (logvar1, logvar2)] + + return 0.5 * ( + -1.0 + logvar2 - logvar1 + torch.exp(logvar1 - logvar2) + ((mean1 - mean2) ** 2) * torch.exp(-logvar2) + ) diff --git a/hunyuanvideo_foley/models/dac_vae/utils/__init__.py b/hunyuanvideo_foley/models/dac_vae/utils/__init__.py new file mode 100644 index 0000000..0956a23 --- /dev/null +++ b/hunyuanvideo_foley/models/dac_vae/utils/__init__.py @@ -0,0 +1,121 @@ +from pathlib import Path + +import argbind +from audiotools import ml + +from ..model import DAC +Accelerator = ml.Accelerator + +__MODEL_LATEST_TAGS__ = { + ("44khz", "8kbps"): "0.0.1", + ("24khz", "8kbps"): "0.0.4", + ("16khz", "8kbps"): "0.0.5", + ("44khz", "16kbps"): "1.0.0", +} + +__MODEL_URLS__ = { + ( + "44khz", + "0.0.1", + "8kbps", + ): "https://github.com/descriptinc/descript-audio-codec/releases/download/0.0.1/weights.pth", + ( + "24khz", + "0.0.4", + "8kbps", + ): "https://github.com/descriptinc/descript-audio-codec/releases/download/0.0.4/weights_24khz.pth", + ( + "16khz", + "0.0.5", + "8kbps", + ): "https://github.com/descriptinc/descript-audio-codec/releases/download/0.0.5/weights_16khz.pth", + ( + "44khz", + "1.0.0", + "16kbps", + ): "https://github.com/descriptinc/descript-audio-codec/releases/download/1.0.0/weights_44khz_16kbps.pth", +} + + +@argbind.bind(group="download", positional=True, without_prefix=True) +def download( + model_type: str = "44khz", model_bitrate: str = "8kbps", tag: str = "latest" +): + """ + Function that downloads the weights file from URL if a local cache is not found. + + Parameters + ---------- + model_type : str + The type of model to download. Must be one of "44khz", "24khz", or "16khz". Defaults to "44khz". + model_bitrate: str + Bitrate of the model. Must be one of "8kbps", or "16kbps". Defaults to "8kbps". + Only 44khz model supports 16kbps. + tag : str + The tag of the model to download. Defaults to "latest". + + Returns + ------- + Path + Directory path required to load model via audiotools. + """ + model_type = model_type.lower() + tag = tag.lower() + + assert model_type in [ + "44khz", + "24khz", + "16khz", + ], "model_type must be one of '44khz', '24khz', or '16khz'" + + assert model_bitrate in [ + "8kbps", + "16kbps", + ], "model_bitrate must be one of '8kbps', or '16kbps'" + + if tag == "latest": + tag = __MODEL_LATEST_TAGS__[(model_type, model_bitrate)] + + download_link = __MODEL_URLS__.get((model_type, tag, model_bitrate), None) + + if download_link is None: + raise ValueError( + f"Could not find model with tag {tag} and model type {model_type}" + ) + + local_path = ( + Path.home() + / ".cache" + / "descript" + / "dac" + / f"weights_{model_type}_{model_bitrate}_{tag}.pth" + ) + if not local_path.exists(): + local_path.parent.mkdir(parents=True, exist_ok=True) + + # Download the model + import requests + + response = requests.get(download_link) + + if response.status_code != 200: + raise ValueError( + f"Could not download model. Received response code {response.status_code}" + ) + local_path.write_bytes(response.content) + + return local_path + + +def load_model( + model_type: str = "44khz", + model_bitrate: str = "8kbps", + tag: str = "latest", + load_path: str = None, +): + if not load_path: + load_path = download( + model_type=model_type, model_bitrate=model_bitrate, tag=tag + ) + generator = DAC.load(load_path) + return generator diff --git a/hunyuanvideo_foley/models/dac_vae/utils/__pycache__/__init__.cpython-312.pyc b/hunyuanvideo_foley/models/dac_vae/utils/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b3abba930d99dc7694ab075830286e8e35862f32 GIT binary patch literal 3798 zcmb_fUu+Y}8K3=Qd)FH~PHdBe3uFlusatT8D@Q9pcR&)3qmV<8yI!qStBq$JZ?fKX zXV*X?M@=^4LcnPdce`3R0{UCq=sUfj6h&iKl)u>&?GIePV)V zzxmBK-+ce(o8gBVeFCAM=pbxzo7nm%5RVW>n6prYD{vg8d5bUb@D(hf zA{IozL>#dqm1rT#Aw!%?hGKTUg&2O~^ZsbXuz0e_};13_%&4qh_Q^LJ>BZb5QpL_2W z<lGhh!ua{IH<6M2NPaZ0_=rfm0Ghjmrhmvm14!seA)V0q zQW%A)oZv@hiBqfk@v7sRp6S@Sui0&D_ z;OVo)c4mWUgs2{Lymb+>@e!({IgY}6TbS)jsLuc2dA<&(_9k*;m*K>QtL|`h4hwBm z+=cQuGXG03Qt{|XV6BeoT#4@kdWrYMHi+h^`}Q^92`B0t>#6MIcQHD%B-X__ z8mfIQl;wIbpV~JG!@2a&G2DqepQb{apw4ySs1;v|)FW^8f)LuZ6s|CLCnNC8isuhRvz33!7yCKp}KejbOzTYnBI=IBJ_SziapQdE2}Hh2RjE*>6pY8DQ1 z(X>O|Y3Ws|YZp_hzKyahTa4ye21EUB-?zSi^`bAe6JPT=ZBeB6#1vfNv8qlC+jD)% zxNW+g=FAgX*Is>3Z6Dp)J3K>S`dl}PhItcwN({H^*sh_{D_qSb{fOnIN6VC-2rbbp z)1LR^?96L4Pe_i=gY>{4msWq8=g{65wd!eS= zpPSflE;^R6FkKFin-tMX3}R{EPD3=bo4PSw^GwT~)-=;LJx$A37YHrZtb_Xrxp(32 zSVpbmsJnl?>&RNyk>_1UmoGQti3hp+xi5dcJhq`^TSz<@-9%zX6sX8Kt|cMq9JeJi zn!z2YqpyX8L%=0aE+KNJ6-CmStr(>k7DtkDuYBh(FOx@C3af`2zxZe^dG4O@@66zO z=Ga>1*yH%>*gp<6hCcp2^E@-Te03w1X~g?oEM{(;M;3W`visu)UJnj0tiIv`u6m79L(%;Z{zp46Jgi zM_03}xDh|IDU0&imVhFitt9F=u&(s4Dg7^$!%eAsUFutt`qrg^HEH0nxau{effv%* zttgUvH`4DtI{VexmBjOO{+_g!HDzGsH~&(Oz3lF9c8@)~v_AgD+V~fZu|GCa z1yDDg1a%Wh&`D0OONZB_!(ZQC9~fR67=9s*fP))ib_;@me3_OGmMF#T7XsiB__3GF zH6Y+6Be+R#`z=G?6?Oh87ryG31dsBvHn&?!8>{c!t8ll&oyJc>McSLe0I)QP!Wv(s zO64N^SjhWyPM$Sv1D;SV0NC(z^Uy7$O-PsQ%S?Z>=g8`z zXJfAgJ{!L$zmB4=%p>V5>Fd7dsiVuEG?h$4%#cx75>`z5rXjr)5QDepHZTcA0m_V5 zw#=E}c*k@yI|IeckMGo9LfrvJoW3CmvrRc)EW&>Xm<8kr6+cK>M7C0Z8#eF*HQ?`v z=@M8codqE^%lMFd1XDB??hnwl1dii=M7=+uZTm&;~Oa3>P3>ywcu>b?&@uBGneh=my@j##3z?~{?`B3{)d+zpL{01;3hUMUE35c zaNL1rD%}c*{AioFF!5i8Yzc_J$hCON*1E!@p2M(bv$Lm}?c0 Tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + # Get modulation parameters + if sync_vec is not None: + assert sync_vec.ndim == 3 + (audio_mod1_shift, audio_mod1_scale, audio_mod1_gate, + audio_mod2_shift, audio_mod2_scale, audio_mod2_gate, + audio_mod3_shift, audio_mod3_scale, audio_mod3_gate, + ) = self.audio_mod(sync_vec).chunk(9, dim=-1) + else: + (audio_mod1_shift, audio_mod1_scale, audio_mod1_gate, + audio_mod2_shift, audio_mod2_scale, audio_mod2_gate, + audio_mod3_shift, audio_mod3_scale, audio_mod3_gate, + ) = self.audio_mod(vec).chunk(9, dim=-1) + + ( + v_cond_mod1_shift, + v_cond_mod1_scale, + v_cond_mod1_gate, + v_cond_mod2_shift, + v_cond_mod2_scale, + v_cond_mod2_gate, + v_cond_mod3_shift, + v_cond_mod3_scale, + v_cond_mod3_gate, + ) = self.v_cond_mod(vec).chunk(9, dim=-1) + + # 1. Self Attention for audio + visual + audio_modulated = self.audio_norm1(audio) + audio_modulated = modulate(audio_modulated, shift=audio_mod1_shift, scale=audio_mod1_scale) + audio_qkv = self.audio_self_attn_qkv(audio_modulated) + audio_q, audio_k, audio_v = rearrange(audio_qkv, "B L (K H D) -> K B L H D", K=3, H=self.num_heads) + audio_q = self.audio_self_q_norm(audio_q).to(audio_v) + audio_k = self.audio_self_k_norm(audio_k).to(audio_v) + + # Prepare visual cond for attention + v_cond_modulated = self.v_cond_norm1(v_cond) + v_cond_modulated = modulate(v_cond_modulated, shift=v_cond_mod1_shift, scale=v_cond_mod1_scale) + v_cond_qkv = self.v_cond_attn_qkv(v_cond_modulated) + v_cond_q, v_cond_k, v_cond_v = rearrange(v_cond_qkv, "B L (K H D) -> K B L H D", K=3, H=self.num_heads) + v_cond_q = self.v_cond_attn_q_norm(v_cond_q).to(v_cond_v) + v_cond_k = self.v_cond_attn_k_norm(v_cond_k).to(v_cond_v) + + # Apply RoPE if needed for audio and visual + if freqs_cis is not None: + if not self.interleaved_audio_visual_rope: + audio_qq, audio_kk = apply_rotary_emb(audio_q, audio_k, freqs_cis, head_first=False) + audio_q, audio_k = audio_qq, audio_kk + else: + ori_audio_len = audio_q.shape[1] + ori_v_con_len = v_cond_q.shape[1] + interleaved_audio_visual_q = interleave_two_sequences(audio_q, v_cond_q) + interleaved_audio_visual_k = interleave_two_sequences(audio_k, v_cond_k) + interleaved_audio_visual_qq, interleaved_audio_visual_kk = apply_rotary_emb( + interleaved_audio_visual_q, interleaved_audio_visual_k, freqs_cis, head_first=False + ) + audio_qq, v_cond_qq = decouple_interleaved_two_sequences( + interleaved_audio_visual_qq, ori_audio_len, ori_v_con_len + ) + audio_kk, v_cond_kk = decouple_interleaved_two_sequences( + interleaved_audio_visual_kk, ori_audio_len, ori_v_con_len + ) + audio_q, audio_k = audio_qq, audio_kk + v_cond_q, v_cond_k = v_cond_qq, v_cond_kk + + # Apply RoPE to visual if needed and not interleaved + if v_freqs_cis is not None and not self.interleaved_audio_visual_rope: + v_cond_qq, v_cond_kk = apply_rotary_emb(v_cond_q, v_cond_k, v_freqs_cis, head_first=False) + v_cond_q, v_cond_k = v_cond_qq, v_cond_kk + + # Concatenate for self-attention + q = torch.cat((v_cond_q, audio_q), dim=1) + k = torch.cat((v_cond_k, audio_k), dim=1) + v = torch.cat((v_cond_v, audio_v), dim=1) + + # Run self-attention + attn = attention(q, k, v, mode=self.attn_mode, attn_mask=attn_mask, deterministic=self.deterministic) + v_cond_attn, audio_attn = torch.split(attn, [v_cond.shape[1], audio.shape[1]], dim=1) + + # Apply self-attention output to audio and v_cond + audio = audio + apply_gate(self.audio_self_proj(audio_attn), gate=audio_mod1_gate) + v_cond = v_cond + apply_gate(self.v_cond_self_proj(v_cond_attn), gate=v_cond_mod1_gate) + + # 2. Cross Attention: (v_cond, audio) as query, text as key/value + # audio, v_cond modulation + audio_modulated = self.audio_norm2(audio) + audio_modulated = modulate(audio_modulated, shift=audio_mod2_shift, scale=audio_mod2_scale) + v_cond_modulated = self.v_cond_norm2(v_cond) + v_cond_modulated = modulate(v_cond_modulated, shift=v_cond_mod2_shift, scale=v_cond_mod2_scale) + + # Prepare audio query + audio_q = self.audio_cross_q(audio_modulated) + audio_q = rearrange(audio_q, "B L (H D) -> B L H D", H=self.num_heads) + audio_q = self.audio_cross_q_norm(audio_q) + + # Prepare v_cond query + v_cond_q = self.v_cond_cross_q(v_cond_modulated) + v_cond_q = rearrange(v_cond_q, "B L (H D) -> B L H D", H=self.num_heads) + v_cond_q = self.v_cond_cross_q_norm(v_cond_q) + + # Prepare text key/value + text_kv = self.text_cross_kv(cond) + text_k, text_v = rearrange(text_kv, "B L (K H D) -> K B L H D", K=2, H=self.num_heads) + text_k = self.text_cross_k_norm(text_k).to(text_v) + + # Apply RoPE to (v_cond, audio) query and text key if needed + head_dim = self.hidden_size // self.num_heads + audio_cross_freqs_cos, audio_cross_freqs_sin = self.build_rope_for_text(audio_q.shape[1], head_dim, rope_dim_list=self.rope_dim_list) + audio_cross_freqs_cis = (audio_cross_freqs_cos.to(audio_q.device), audio_cross_freqs_sin.to(audio_q.device)) + audio_q = apply_rotary_emb(audio_q, audio_q, audio_cross_freqs_cis, head_first=False)[0] + + v_cond_cross_freqs_cos, v_cond_cross_freqs_sin = self.build_rope_for_text(v_cond_q.shape[1], head_dim, rope_dim_list=self.rope_dim_list) + v_cond_cross_freqs_cis = (v_cond_cross_freqs_cos.to(v_cond_q.device), v_cond_cross_freqs_sin.to(v_cond_q.device)) + v_cond_q = apply_rotary_emb(v_cond_q, v_cond_q, v_cond_cross_freqs_cis, head_first=False)[0] + + text_len = text_k.shape[1] + + text_freqs_cos, text_freqs_sin = self.build_rope_for_text(text_len, head_dim, + rope_dim_list=self.rope_dim_list) + text_freqs_cis = (text_freqs_cos.to(text_k.device), text_freqs_sin.to(text_k.device)) + text_k = apply_rotary_emb(text_k, text_k, text_freqs_cis, head_first=False)[1] + + # Concat v_cond and audio for cross-attention + v_cond_audio_q = torch.cat([v_cond_q, audio_q], dim=1) + + # Run cross-attention + cross_attn = attention(v_cond_audio_q, text_k, text_v, mode=self.attn_mode, deterministic=self.deterministic) + v_cond_cross_attn, audio_cross_attn = torch.split(cross_attn, [v_cond.shape[1], audio.shape[1]], dim=1) + + # Apply cross-attention output + audio = audio + apply_gate(self.audio_cross_proj(audio_cross_attn), gate=audio_mod2_gate) + v_cond = v_cond + apply_gate(self.v_cond_cross_proj(v_cond_cross_attn), gate=v_cond_mod2_gate) + + # 3. Apply MLPs + audio = audio + apply_gate( + self.audio_mlp(modulate(self.audio_norm3(audio), shift=audio_mod3_shift, scale=audio_mod3_scale)), + gate=audio_mod3_gate, + ) + + # Apply visual MLP + v_cond = v_cond + apply_gate( + self.v_cond_mlp(modulate(self.v_cond_norm3(v_cond), shift=v_cond_mod3_shift, scale=v_cond_mod3_scale)), + gate=v_cond_mod3_gate, + ) + + return audio, cond, v_cond + +class SingleStreamBlock(nn.Module): + + def __init__(self, hidden_size: int, + num_heads: int, + mlp_ratio: float, + qk_norm_type: str = "rms", + dtype: Optional[torch.dtype] = None, + device: Optional[torch.device] = None,): + factory_kwargs = {"device": device, "dtype": dtype} + super().__init__() + + self.hidden_size = hidden_size + self.num_heads = num_heads + + self.modulation = ModulateDiT( + hidden_size=hidden_size, + factor=6, + act_layer=get_activation_layer("silu"), + **factory_kwargs, + ) + self.linear_qkv = nn.Linear(hidden_size, hidden_size * 3, bias=True) + self.linear1 = ChannelLastConv1d(hidden_size, hidden_size, kernel_size=3, padding=1, **factory_kwargs) + self.linear2 = ConvMLP(hidden_size, hidden_size * mlp_ratio, kernel_size=3, padding=1, **factory_kwargs) + self.norm1 = nn.LayerNorm(hidden_size, elementwise_affine=False) + self.norm2 = nn.LayerNorm(hidden_size, elementwise_affine=False) + self.q_norm = nn.RMSNorm(hidden_size // num_heads) + self.k_norm = nn.RMSNorm(hidden_size // num_heads) + self.rearrange = Rearrange("B L (H D K) -> B H L D K", K=3, H=num_heads) + + def forward(self, x: torch.Tensor, cond: torch.Tensor,freqs_cis: Tuple[torch.Tensor, torch.Tensor] = None): + assert cond.ndim == 3, "Condition should be in shape of [B, T, D]" + modulation = self.modulation(cond) + shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = modulation.chunk(6, dim=-1) + x_norm1 = self.norm1(x) * (1 + scale_msa) + shift_msa + + qkv = self.linear_qkv(x_norm1) + q, k, v = self.rearrange(qkv).chunk(3, dim=-1) + q = q.squeeze(-1) + k = k.squeeze(-1) + v = v.squeeze(-1) + + q = self.q_norm(q) + k = self.k_norm(k) + q, k = apply_rotary_emb(q, k, freqs_cis, head_first=True) + + q = q.contiguous() + k = k.contiguous() + v = v.contiguous() + out = F.scaled_dot_product_attention(q, k, v) + out = rearrange(out, 'b h n d -> b n (h d)').contiguous() + + x = x + apply_gate(self.linear1(out),gate=gate_msa) + x_norm = self.norm2(x) * (1 + scale_mlp) + shift_mlp + x = x + apply_gate(self.linear2(x_norm), gate=gate_mlp) + + return x + +class HunyuanVideoFoley(ModelMixin, ConfigMixin): + @register_to_config + def __init__( + self, + model_config, + dtype: Optional[torch.dtype] = None, + device: Optional[torch.device] = None, + ): + factory_kwargs = {"device": device, "dtype": dtype} + super().__init__() + + model_args = model_config.model_config.model_kwargs + self.depth_triple_blocks = model_args.get("depth_triple_blocks", 19) + self.depth_single_blocks = model_args.get("depth_single_blocks", 38) + # Gradient checkpoint. + self.gradient_checkpoint = False + self.gradient_checkpoint_layers = None + if self.gradient_checkpoint: + assert self.gradient_checkpoint_layers <= self.depth_triple_blocks + self.depth_single_blocks, ( + f"Gradient checkpoint layers must be less or equal than the depth of the model. " + f"Got gradient_checkpoint_layers={self.gradient_checkpoint_layers} and depth={self.depth_triple_blocks + self.depth_single_blocks}." + ) + + self.interleaved_audio_visual_rope = model_args.get("interleaved_audio_visual_rope", False) + + # Condition projection. Default to linear projection. + self.condition_projection = model_args.get("condition_projection", "linear") + self.condition_dim = model_args.get("condition_dim", None) + self.use_attention_mask = model_args.get("use_attention_mask", False) + + self.patch_size = model_args.get("patch_size", 1) + self.visual_in_channels = model_args.get("clip_dim", 768) + self.audio_vae_latent_dim = model_args.get("audio_vae_latent_dim", 128) + self.out_channels = self.audio_vae_latent_dim + self.unpatchify_channels = self.out_channels + self.reverse = model_args.get("reverse", False) + + self.num_heads = model_args.get("num_heads", 24) + self.hidden_size = model_args.get("hidden_size", 3072) + self.rope_dim_list = model_args.get("rope_dim_list", None) + self.mlp_ratio = model_args.get("mlp_ratio", 4.0) + self.mlp_act_type = model_args.get("mlp_act_type", "gelu_tanh") + + self.qkv_bias = model_args.get("qkv_bias", True) + self.qk_norm = model_args.get("qk_norm", True) + self.qk_norm_type = model_args.get("qk_norm_type", "rms") + self.attn_mode = model_args.get("attn_mode", "torch") + + self.embedder_type = model_args.get("embedder_type", "default") + + # sync condition things + self.sync_modulation = model_args.get("sync_modulation", False) + self.add_sync_feat_to_audio = model_args.get("add_sync_feat_to_audio", False) + self.sync_feat_dim = model_args.get("sync_feat_dim", 768) + self.sync_in_ksz = model_args.get("sync_in_ksz", 1) + + # condition tokens length + self.clip_len = model_args.get("clip_length", 64) + self.sync_len = model_args.get("sync_length", 192) + + if self.hidden_size % self.num_heads != 0: + raise ValueError(f"Hidden size {self.hidden_size} must be divisible by num_heads {self.num_heads}") + + # Build audio patchify layer and visual gated linear projection + self.patch_size = 1 + self.audio_embedder = PatchEmbed1D(self.patch_size, self.audio_vae_latent_dim, self.hidden_size, **factory_kwargs) + self.visual_proj = SwiGLU(self.visual_in_channels, hidden_dim=self.hidden_size, out_dim=self.hidden_size) + + # condition + if self.condition_projection == "linear": + self.cond_in = ConditionProjection( + self.condition_dim, self.hidden_size, get_activation_layer("silu"), **factory_kwargs + ) + else: + raise NotImplementedError(f"Unsupported condition_projection: {self.condition_projection}") + + # time modulation + self.time_in = TimestepEmbedder(self.hidden_size, get_activation_layer("silu"), **factory_kwargs) + + # visual sync embedder if needed + if self.sync_in_ksz == 1: + sync_in_padding = 0 + elif self.sync_in_ksz == 3: + sync_in_padding = 1 + else: + raise ValueError + if self.sync_modulation or self.add_sync_feat_to_audio: + self.sync_in = nn.Sequential( + nn.Linear(self.sync_feat_dim, self.hidden_size), + nn.SiLU(), + ConvMLP(self.hidden_size, self.hidden_size * 4, kernel_size=self.sync_in_ksz, padding=sync_in_padding), + ) + self.sync_pos_emb = nn.Parameter(torch.zeros((1, 1, 8, self.sync_feat_dim))) + + self.triple_blocks = nn.ModuleList( + [ + TwoStreamCABlock( + hidden_size=self.hidden_size, + num_heads=self.num_heads, + mlp_ratio=self.mlp_ratio, + mlp_act_type=self.mlp_act_type, + qk_norm=self.qk_norm, + qk_norm_type=self.qk_norm_type, + qkv_bias=self.qkv_bias, + attn_mode=self.attn_mode, + reverse=self.reverse, + interleaved_audio_visual_rope=self.interleaved_audio_visual_rope, + **factory_kwargs, + ) + for _ in range(self.depth_triple_blocks) + ] + ) + + + self.single_blocks = nn.ModuleList( + [ + SingleStreamBlock( + hidden_size=self.hidden_size, + num_heads=self.num_heads, + mlp_ratio=self.mlp_ratio, + qk_norm_type=self.qk_norm_type, + **factory_kwargs, + ) + for _ in range(self.depth_single_blocks) + ] + ) + + self.final_layer = FinalLayer1D( + self.hidden_size, self.patch_size, self.out_channels, get_activation_layer("silu"), **factory_kwargs + ) + self.unpatchify_channels = self.out_channels + + self.empty_clip_feat = nn.Parameter(torch.zeros(1, self.visual_in_channels), requires_grad=True) + self.empty_sync_feat = nn.Parameter(torch.zeros(1, self.sync_feat_dim), requires_grad=True) + nn.init.constant_(self.empty_clip_feat, 0) + nn.init.constant_(self.empty_sync_feat, 0) + + def get_empty_string_sequence(self, bs=None) -> torch.Tensor: + if bs is None: + return self.empty_string_feat + else: + return self.empty_string_feat.unsqueeze(0).expand(bs, -1, -1) + + def get_empty_clip_sequence(self, bs=None, len=None) -> torch.Tensor: + len = len if len is not None else self.clip_len + if bs is None: + return self.empty_clip_feat.expand(len, -1) # 15s + else: + return self.empty_clip_feat.unsqueeze(0).expand(bs, len, -1) # 15s + + def get_empty_sync_sequence(self, bs=None, len=None) -> torch.Tensor: + len = len if len is not None else self.sync_len + if bs is None: + return self.empty_sync_feat.expand(len, -1) + else: + return self.empty_sync_feat.unsqueeze(0).expand(bs, len, -1) + + def build_rope_for_audio_visual(self, audio_emb_len, visual_cond_len): + assert self.patch_size == 1 + # ======================================== Build RoPE for audio tokens ====================================== + target_ndim = 1 # n-d RoPE + rope_sizes = [audio_emb_len] + head_dim = self.hidden_size // self.num_heads + rope_dim_list = self.rope_dim_list + if rope_dim_list is None: + rope_dim_list = [head_dim // target_ndim for _ in range(target_ndim)] + assert sum(rope_dim_list) == head_dim, "sum(rope_dim_list) should equal to head_dim of attention layer" + freqs_cos, freqs_sin = get_nd_rotary_pos_embed( + rope_dim_list=rope_dim_list, + start=rope_sizes, + theta=10000, + use_real=True, + theta_rescale_factor=1.0, + ) + + # ========================== Build RoPE for clip tokens ========================= + target_ndim = 1 # n-d RoPE + rope_sizes = [visual_cond_len] + head_dim = self.hidden_size // self.num_heads + rope_dim_list = self.rope_dim_list + if rope_dim_list is None: + rope_dim_list = [head_dim // target_ndim for _ in range(target_ndim)] + assert sum(rope_dim_list) == head_dim, "sum(rope_dim_list) should equal to head_dim of attention layer" + v_freqs_cos, v_freqs_sin = get_nd_rotary_pos_embed( + rope_dim_list=rope_dim_list, + start=rope_sizes, + theta=10000, + use_real=True, + theta_rescale_factor=1.0, + freq_scaling=1.0 * audio_emb_len / visual_cond_len, + ) + return freqs_cos, freqs_sin, v_freqs_cos, v_freqs_sin + + def build_rope_for_interleaved_audio_visual(self, total_len): + assert self.patch_size == 1 + # ========================== Build RoPE for audio tokens ======================== + target_ndim = 1 # n-d RoPE + rope_sizes = [total_len] + head_dim = self.hidden_size // self.num_heads + rope_dim_list = self.rope_dim_list + if rope_dim_list is None: + rope_dim_list = [head_dim // target_ndim for _ in range(target_ndim)] + assert sum(rope_dim_list) == head_dim, "sum(rope_dim_list) should equal to head_dim of attention layer" + freqs_cos, freqs_sin = get_nd_rotary_pos_embed( + rope_dim_list=rope_dim_list, + start=rope_sizes, + theta=10000, + use_real=True, + theta_rescale_factor=1.0, + ) + return freqs_cos, freqs_sin + + def set_attn_mode(self, new_mode): + for block in self.triple_blocks: + block.set_attn_mode(new_mode) + for block in self.single_blocks: + block.set_attn_mode(new_mode) + + def enable_deterministic(self): + for block in self.triple_blocks: + block.enable_deterministic() + for block in self.single_blocks: + block.enable_deterministic() + + def disable_deterministic(self): + for block in self.triple_blocks: + block.disable_deterministic() + for block in self.single_blocks: + block.disable_deterministic() + + def forward( + self, + x: torch.Tensor, + t: torch.Tensor, # Should be in range(0, 1000). + clip_feat: Optional[torch.Tensor] = None, + cond: torch.Tensor = None, + audio_mask: Optional[torch.Tensor] = None, + cond_mask: torch.Tensor = None, + sync_feat: Optional[torch.Tensor] = None, + drop_visual: Optional[List[bool]] = None, + return_dict: bool = True, + ) -> Union[torch.Tensor, Dict[str, torch.Tensor]]: + out = {} + audio = x + bs, _, ol = x.shape + tl = ol // self.patch_size + + # Prepare learnable empty conditions for visual condition + if drop_visual is not None: + clip_feat[drop_visual] = self.get_empty_clip_sequence().to(dtype=clip_feat.dtype) + sync_feat[drop_visual] = self.get_empty_sync_sequence().to(dtype=sync_feat.dtype) + + # ========================= Prepare time & visual modulation ========================= + vec = self.time_in(t) + sync_vec = None + if self.sync_modulation: + assert sync_feat is not None and sync_feat.shape[1] % 8 == 0 + sync_feat = sync_feat.view(bs, int(sync_feat.shape[1] / 8), 8, self.sync_feat_dim) + self.sync_pos_emb + sync_feat = sync_feat.view(bs, -1, self.sync_feat_dim) # bs, num_segments * 8, channels + sync_vec = self.sync_in(sync_feat) # bs, num_segments * 8, c + sync_vec = ( + F.interpolate(sync_vec.transpose(1, 2), size=(tl), mode="nearest-exact").contiguous().transpose(1, 2) + ) # bs, tl, c + sync_vec = sync_vec + vec.unsqueeze(1) + elif self.add_sync_feat_to_audio: + assert sync_feat is not None and sync_feat.shape[1] % 8 == 0 + sync_feat = sync_feat.view(bs, sync_feat.shape[1] // 8, 8, self.sync_feat_dim) + self.sync_pos_emb + sync_feat = sync_feat.view(bs, -1, self.sync_feat_dim) # bs, num_segments * 8, channels + sync_feat = self.sync_in(sync_feat) # bs, num_segments * 8, c + add_sync_feat_to_audio = ( + F.interpolate(sync_feat.transpose(1, 2), size=(tl), mode="nearest-exact").contiguous().transpose(1, 2) + ) # bs, tl, c + + # ========================= Get text, audio and video clip embedding ========================= + cond = self.cond_in(cond) + cond_seq_len = cond.shape[1] + + audio = self.audio_embedder(x) + audio_seq_len = audio.shape[1] + v_cond = self.visual_proj(clip_feat) + v_cond_seq_len = v_cond.shape[1] + + # ========================= Compute attention mask ========================= + attn_mask = None + if self.use_attention_mask: + assert cond_mask is not None + batch_size = audio.shape[0] + seq_len = cond_seq_len + v_cond_seq_len + audio_seq_len + + # get default audio_mask and v_cond_mask + audio_mask = torch.ones((batch_size, audio_seq_len), dtype=torch.bool, device=audio.device) + v_cond_mask = torch.ones((batch_size, v_cond_seq_len), dtype=torch.bool, device=audio.device) + + # batch_size x seq_len + concat_mask = torch.cat([cond_mask, v_cond_mask, audio_mask], dim=1) + # batch_size x 1 x seq_len x seq_len + attn_mask_1 = concat_mask.view(batch_size, 1, 1, seq_len).repeat(1, 1, seq_len, 1) + # batch_size x 1 x seq_len x seq_len + attn_mask_2 = attn_mask_1.transpose(2, 3) + # batch_size x 1 x seq_len x seq_len, 1 for broadcasting of num_heads + attn_mask = (attn_mask_1 & attn_mask_2).bool() + # avoids self-attention weight being NaN for text padding tokens + attn_mask[:, :, :, 0] = True + + + # ========================= Build rope for audio and clip tokens ========================= + if self.interleaved_audio_visual_rope: + freqs_cos, freqs_sin = self.build_rope_for_interleaved_audio_visual(audio_seq_len * 2) + v_freqs_cos = v_freqs_sin = None + else: + freqs_cos, freqs_sin, v_freqs_cos, v_freqs_sin = self.build_rope_for_audio_visual( + audio_seq_len, v_cond_seq_len + ) + + # ========================= Pass through DiT blocks ========================= + freqs_cis = (freqs_cos, freqs_sin) if freqs_cos is not None else None + v_freqs_cis = (v_freqs_cos, v_freqs_sin) if v_freqs_cos is not None else None + + if self.add_sync_feat_to_audio: + add_sync_layer = 0 + assert ( + add_sync_layer < self.depth_triple_blocks + ), f"The layer to add mel_spectrogram feature and sync feature should in the triple_stream_blocks (n: {self.depth_triple_blocks})." + # Triple-stream blocks + for layer_num, block in enumerate(self.triple_blocks): + if self.add_sync_feat_to_audio and layer_num == add_sync_layer: + audio = audio + add_sync_feat_to_audio + triple_block_args = [audio, cond, v_cond, attn_mask, vec, freqs_cis, v_freqs_cis, sync_vec] + if ( + self.training + and self.gradient_checkpoint + and (self.gradient_checkpoint_layers == -1 or layer_num < self.gradient_checkpoint_layers) + ): + audio, cond, v_cond = torch.utils.checkpoint.checkpoint( + ckpt_wrapper(block), *triple_block_args, use_reentrant=False + ) + else: + audio, cond, v_cond = block(*triple_block_args) + + x = audio + if sync_vec is not None: + vec = vec.unsqueeze(1).repeat(1, cond_seq_len + v_cond_seq_len, 1) + vec = torch.cat((vec, sync_vec), dim=1) + + freqs_cos, freqs_sin, _, _ = self.build_rope_for_audio_visual(audio_seq_len, v_cond_seq_len) + if self.add_sync_feat_to_audio: + vec = add_sync_feat_to_audio + vec.unsqueeze(dim=1) + if len(self.single_blocks) > 0: + for layer_num, block in enumerate(self.single_blocks): + single_block_args = [ + x, + vec, + (freqs_cos, freqs_sin), + ] + if ( + self.training + and self.gradient_checkpoint + and ( + self.gradient_checkpoint_layers == -1 + or layer_num + len(self.triple_blocks) < self.gradient_checkpoint_layers + ) + ): + x = torch.utils.checkpoint.checkpoint(ckpt_wrapper(block), *single_block_args, use_reentrant=False) + else: + x = block(*single_block_args) + + audio = x + + # ========================= Final layer ========================= + if sync_vec is not None: + vec = sync_vec + audio = self.final_layer(audio, vec) # (N, T, patch_size * out_channels) + audio = self.unpatchify1d(audio, tl) + + if return_dict: + out["x"] = audio + return out + return audio + + def unpatchify1d(self, x, l): + # x: (N, L, patch_size * C) + # audio: (N, C, T), T == L * patch_size + c = self.unpatchify_channels + p = self.patch_size + assert l == x.shape[1] + + x = x.reshape(shape=(x.shape[0], l, p, c)) + x = torch.einsum("ntpc->nctp", x) + audio = x.reshape(shape=(x.shape[0], c, l * p)) + return audio + + def params_count(self): + counts = { + "triple": sum( + [ + sum(p.numel() for p in block.audio_cross_q.parameters()) + + sum(p.numel() for p in block.v_cond_cross_q.parameters()) + + sum(p.numel() for p in block.text_cross_kv.parameters()) + + sum(p.numel() for p in block.audio_self_attn_qkv.parameters()) + + sum(p.numel() for p in block.v_cond_attn_qkv.parameters()) + + sum(p.numel() for p in block.audio_mlp.parameters()) + + sum(p.numel() for p in block.audio_self_proj.parameters()) + + sum(p.numel() for p in block.v_cond_self_proj.parameters()) + + sum(p.numel() for p in block.v_cond_mlp.parameters()) + for block in self.triple_blocks + ] + ), + "single": sum( + [ + sum(p.numel() for p in block.linear1.parameters()) + + sum(p.numel() for p in block.linear2.parameters()) + for block in self.single_blocks + ] + ), + "total": sum(p.numel() for p in self.parameters()), + } + + counts["attn+mlp"] = counts["triple"] + counts["single"] + return counts diff --git a/hunyuanvideo_foley/models/nn/__init__.py b/hunyuanvideo_foley/models/nn/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hunyuanvideo_foley/models/nn/__pycache__/__init__.cpython-312.pyc b/hunyuanvideo_foley/models/nn/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d8ab613c987c9c7e23b7de151f6856d0c576dd42 GIT binary patch literal 187 zcmX@j%ge<81YW@#GC=fW5P=Rpvj9b=GgLBYGWxA#C}INgK7-W!igK}van8?8s|@vw zNiHoe$#bU#$-T5%0MFVX<(sTpq8BCn7q80 q`1s7c%#!$cy@JYH95z7l(wtPgB37VPj6hrrVtiy~WMnL22C@J*r!#i| literal 0 HcmV?d00001 diff --git a/hunyuanvideo_foley/models/nn/__pycache__/activation_layers.cpython-312.pyc b/hunyuanvideo_foley/models/nn/__pycache__/activation_layers.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..884ceddc897b83d313261bc54146082b86362359 GIT binary patch literal 2562 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 literal 0 HcmV?d00001 diff --git a/hunyuanvideo_foley/models/nn/__pycache__/embed_layers.cpython-312.pyc b/hunyuanvideo_foley/models/nn/__pycache__/embed_layers.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9e0b3a2d7b58f4bfcb896ce06d9061cbe3b35c59 GIT binary patch literal 7222 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 literal 0 HcmV?d00001 diff --git a/hunyuanvideo_foley/models/nn/__pycache__/mlp_layers.cpython-312.pyc b/hunyuanvideo_foley/models/nn/__pycache__/mlp_layers.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ab0f475fdb6e080de7674f18a871e2c610f16714 GIT binary patch literal 8753 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 q?w)#c^37ESz}0M-0v+XR?*>Y#L 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 literal 0 HcmV?d00001 diff --git a/hunyuanvideo_foley/models/nn/__pycache__/posemb_layers.cpython-312.pyc b/hunyuanvideo_foley/models/nn/__pycache__/posemb_layers.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..488a4325dadaadac421bd387eb6464b5ca59a891 GIT binary patch literal 7520 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 aPu-j= 1.13 + return lambda: nn.GELU(approximate="tanh") + elif act_type == "relu": + return nn.ReLU + elif act_type == "silu": + return nn.SiLU + else: + raise ValueError(f"Unknown activation type: {act_type}") + +class SwiGLU(nn.Module): + def __init__( + self, + dim: int, + hidden_dim: int, + out_dim: int, + ): + """ + Initialize the SwiGLU FeedForward module. + + Args: + dim (int): Input dimension. + hidden_dim (int): Hidden dimension of the feedforward layer. + + Attributes: + w1: Linear transformation for the first layer. + w2: Linear transformation for the second layer. + w3: Linear transformation for the third layer. + + """ + super().__init__() + + self.w1 = nn.Linear(dim, hidden_dim, bias=False) + self.w2 = nn.Linear(hidden_dim, out_dim, bias=False) + self.w3 = nn.Linear(dim, hidden_dim, bias=False) + + def forward(self, x): + return self.w2(F.silu(self.w1(x)) * self.w3(x)) diff --git a/hunyuanvideo_foley/models/nn/attn_layers.py b/hunyuanvideo_foley/models/nn/attn_layers.py new file mode 100644 index 0000000..4669ca4 --- /dev/null +++ b/hunyuanvideo_foley/models/nn/attn_layers.py @@ -0,0 +1,546 @@ +import importlib.metadata +import math +from typing import Tuple, Union +import torch +import torch.nn as nn +import torch.nn.functional as F +from einops import rearrange + +try: + from flash_attn import ( + flash_attn_qkvpacked_func, + flash_attn_kvpacked_func, + flash_attn_varlen_kvpacked_func, + flash_attn_varlen_qkvpacked_func, + ) + from flash_attn.bert_padding import index_first_axis, pad_input, unpad_input +except ImportError: + flash_attn_qkvpacked_func, flash_attn_kvpacked_func, flash_attn_varlen_kvpacked_func = None, None, None + index_first_axis = None +from packaging import version +from transformers.utils.import_utils import _is_package_available + +from .norm_layers import get_norm_layer + +def reshape_for_broadcast(freqs_cis: Union[torch.Tensor, Tuple[torch.Tensor]], x: torch.Tensor, head_first=False): + """ + Reshape frequency tensor for broadcasting it with another tensor. + + This function reshapes the frequency tensor to have the same shape as the target tensor 'x' + for the purpose of broadcasting the frequency tensor during element-wise operations. + + Notes: + When using FlashMHAModified, head_first should be False. + When using Attention, head_first should be True. + + Args: + freqs_cis (Union[torch.Tensor, Tuple[torch.Tensor]]): Frequency tensor to be reshaped. + x (torch.Tensor): Target tensor for broadcasting compatibility. + head_first (bool): head dimension first (except batch dim) or not. + + Returns: + torch.Tensor: Reshaped frequency tensor. + + Raises: + AssertionError: If the frequency tensor doesn't match the expected shape. + AssertionError: If the target tensor 'x' doesn't have the expected number of dimensions. + """ + ndim = x.ndim + assert 0 <= 1 < ndim + + if isinstance(freqs_cis, tuple): + # freqs_cis: (cos, sin) in real space + if head_first: + assert freqs_cis[0].shape == ( + x.shape[-2], + x.shape[-1], + ), f"freqs_cis shape {freqs_cis[0].shape} does not match x shape {x.shape}" + shape = [d if i == ndim - 2 or i == ndim - 1 else 1 for i, d in enumerate(x.shape)] + else: + assert freqs_cis[0].shape == ( + x.shape[1], + x.shape[-1], + ), f"freqs_cis shape {freqs_cis[0].shape} does not match x shape {x.shape}" + shape = [d if i == 1 or i == ndim - 1 else 1 for i, d in enumerate(x.shape)] + return freqs_cis[0].view(*shape), freqs_cis[1].view(*shape) + else: + # freqs_cis: values in complex space + if head_first: + assert freqs_cis.shape == ( + x.shape[-2], + x.shape[-1], + ), f"freqs_cis shape {freqs_cis.shape} does not match x shape {x.shape}" + shape = [d if i == ndim - 2 or i == ndim - 1 else 1 for i, d in enumerate(x.shape)] + else: + assert freqs_cis.shape == ( + x.shape[1], + x.shape[-1], + ), f"freqs_cis shape {freqs_cis.shape} does not match x shape {x.shape}" + shape = [d if i == 1 or i == ndim - 1 else 1 for i, d in enumerate(x.shape)] + return freqs_cis.view(*shape) + + +def rotate_half(x): + x_real, x_imag = x.float().reshape(*x.shape[:-1], -1, 2).unbind(-1) # [B, S, H, D//2] + return torch.stack([-x_imag, x_real], dim=-1).flatten(3) + + +def apply_rotary_emb( + xq: torch.Tensor, + xk: torch.Tensor, + freqs_cis: Union[torch.Tensor, Tuple[torch.Tensor, torch.Tensor]], + head_first: bool = False, +) -> Tuple[torch.Tensor, torch.Tensor]: + """ + Apply rotary embeddings to input tensors using the given frequency tensor. + + This function applies rotary embeddings to the given query 'xq' and key 'xk' tensors using the provided + frequency tensor 'freqs_cis'. The input tensors are reshaped as complex numbers, and the frequency tensor + is reshaped for broadcasting compatibility. The resulting tensors contain rotary embeddings and are + returned as real tensors. + + Args: + xq (torch.Tensor): Query tensor to apply rotary embeddings. [B, S, H, D] + xk (torch.Tensor): Key tensor to apply rotary embeddings. [B, S, H, D] + freqs_cis (torch.Tensor or tuple): Precomputed frequency tensor for complex exponential. + head_first (bool): head dimension first (except batch dim) or not. + + Returns: + Tuple[torch.Tensor, torch.Tensor]: Tuple of modified query tensor and key tensor with rotary embeddings. + + """ + xk_out = None + if isinstance(freqs_cis, tuple): + cos, sin = reshape_for_broadcast(freqs_cis, xq, head_first) # [S, D] + cos, sin = cos.to(xq.device), sin.to(xq.device) + # real * cos - imag * sin + # imag * cos + real * sin + xq_out = (xq.float() * cos + rotate_half(xq.float()) * sin).type_as(xq) + xk_out = (xk.float() * cos + rotate_half(xk.float()) * sin).type_as(xk) + else: + # view_as_complex will pack [..., D/2, 2](real) to [..., D/2](complex) + xq_ = torch.view_as_complex(xq.float().reshape(*xq.shape[:-1], -1, 2)) # [B, S, H, D//2] + freqs_cis = reshape_for_broadcast(freqs_cis, xq_, head_first).to(xq.device) # [S, D//2] --> [1, S, 1, D//2] + # (real, imag) * (cos, sin) = (real * cos - imag * sin, imag * cos + real * sin) + # view_as_real will expand [..., D/2](complex) to [..., D/2, 2](real) + xq_out = torch.view_as_real(xq_ * freqs_cis).flatten(3).type_as(xq) + xk_ = torch.view_as_complex(xk.float().reshape(*xk.shape[:-1], -1, 2)) # [B, S, H, D//2] + xk_out = torch.view_as_real(xk_ * freqs_cis).flatten(3).type_as(xk) + + return xq_out, xk_out + + +class BasicAttentionLayer(nn.Module): + def __init__(self, attn_mode="flash", deterministic=False): + super().__init__() + self.attn_mode = attn_mode + self.deterministic = deterministic + + def set_attn_mode(self, new_mode): + self.attn_mode = new_mode + + def enable_deterministic(self): + self.deterministic = True + + def disable_deterministic(self): + self.deterministic = False + + +MEMORY_LAYOUT = { + "self_flash": ( + lambda x: x, + lambda x: x, + ), + "cross_flash": ( + lambda x: x, + lambda x: x, + ), + "flash_torch_sp": ( + lambda x: x, + lambda x: x, + ), + "torch": ( + lambda x: x.transpose(1, 2), + lambda x: x.transpose(1, 2), + ), + "vanilla": ( + lambda x: x.transpose(1, 2), + lambda x: x.transpose(1, 2), + ), +} + + +# Copyed from https://github.com/huggingface/transformers/blob/b873234cb649a24865021f0d598627ce2b24d34a/src/transformers/modeling_flash_attention_utils.py#L33C1-L57C6 +def _get_unpad_data(attention_mask: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor, int]: + """ + Retrieves indexing data required to repad unpadded (ragged) tensors. + + Arguments: + attention_mask (`torch.Tensor`): + Boolean or int tensor of shape (batch_size, sequence_length), 1 means valid and 0 means not valid. + + Return: + indices (`torch.Tensor): + The indices of non-masked tokens from the flattened input sequence. + cu_seqlens (`torch.Tensor`): + The cumulative sequence lengths, used to index into ragged (unpadded) tensors. `cu_seqlens` shape is (batch_size + 1,). + max_seqlen_in_batch (`int`): + Maximum sequence length in batch. + """ + seqlens_in_batch = attention_mask.sum(dim=-1, dtype=torch.int32) + indices = torch.nonzero(attention_mask.flatten(), as_tuple=False).flatten() + max_seqlen_in_batch = seqlens_in_batch.max().item() + cu_seqlens = F.pad(torch.cumsum(seqlens_in_batch, dim=0, dtype=torch.int32), (1, 0)) + return ( + indices, + cu_seqlens, + max_seqlen_in_batch, + ) + + +# Copyed from https://github.com/huggingface/transformers/blob/b873234cb649a24865021f0d598627ce2b24d34a/src/transformers/utils/import_utils.py#L822 +def is_flash_attn_greater_or_equal(library_version: str): + if not _is_package_available("flash_attn"): + return False + + return version.parse(importlib.metadata.version("flash_attn")) >= version.parse(library_version) + + +def get_kv_seqlens_with_mask(attn_mask, k, v): + indices_k, cu_seqlens_k, max_seqlen_k = _get_unpad_data(attn_mask) + b, s1, a, d = k.shape + k = index_first_axis(k.reshape(b * s1, a, d), indices_k) + v = index_first_axis(v.reshape(b * s1, a, d), indices_k) + kv = torch.stack([k, v], dim=1) + return cu_seqlens_k, max_seqlen_k, kv + + +def get_q_seqlens(q): + bs, s, a, d = q.shape + cu_seqlens_q = torch.arange(0, (bs + 1) * s, step=s, dtype=torch.int32, device=q.device) + q = q.reshape(bs * s, a, d) + return cu_seqlens_q, s, q + +def flash_attn_no_pad( + qkv, key_padding_mask, causal=False, dropout_p=0.0, softmax_scale=None +): + # adapted from https://github.com/Dao-AILab/flash-attention/blob/13403e81157ba37ca525890f2f0f2137edf75311/flash_attn/flash_attention.py#L27 + batch_size = qkv.shape[0] + seqlen = qkv.shape[1] + nheads = qkv.shape[-2] + x = rearrange(qkv, "b s three h d -> b s (three h d)") + # x_unpad, indices, cu_seqlens, max_s, used_seqlens_in_batch + # x_unpad, indices, cu_seqlens, max_s + unpad_results = unpad_input( + x, key_padding_mask + ) + + if len(unpad_results) == 4: + x_unpad, indices, cu_seqlens, max_s = unpad_results + elif len(unpad_results) == 5: + x_unpad, indices, cu_seqlens, max_s, used_seqlens_in_batch = unpad_results + else: + raise ValueError + + x_unpad = rearrange(x_unpad, "nnz (three h d) -> nnz three h d", three=3, h=nheads) + output_unpad = flash_attn_varlen_qkvpacked_func( + x_unpad, + cu_seqlens, + max_s, + dropout_p, + softmax_scale=softmax_scale, + causal=causal, + ) + output = rearrange( + pad_input( + rearrange(output_unpad, "nnz h d -> nnz (h d)"), indices, batch_size, seqlen + ), + "b s (h d) -> b s h d", + h=nheads, + ) + return output + + +def attention( + q, + k, + v, + mode, + drop_rate=0, + attn_mask=None, + cond_mask=None, + causal=False, + deterministic=False, + cu_seqlens=None, + max_seqlen=None, + cu_seqlens_k=None, + max_seqlen_k=None, + img_seq_len=None, +): + """ + Perform QKV self attention. + + Args: + q (torch.Tensor): Query tensor with shape [b, s, a, d], where a is the number of heads. + k (torch.Tensor): Key tensor with shape [b, s1, a, d] + v (torch.Tensor): Value tensor with shape [b, s1, a, d] + mode (str): Attention mode. Choose from 'self_flash', 'cross_flash', 'torch', and 'vanilla'. + drop_rate (float): Dropout rate in attention map. (default: 0) + attn_mask (torch.Tensor): Attention mask with shape [b, s1] (cross_attn), or [b, a, s, s1] (torch or vanilla). + (default: None) + causal (bool): Whether to use causal attention. (default: False) + deterministic (bool): Whether to use deterministic attention. (default: False) + cu_seqlens (torch.Tensor): dtype torch.int32. The cumulative sequence lengths of the sequences in the batch, + used to index into q. + max_seqlen (int): The maximum sequence length in the batch of q. + cu_seqlens_k (torch.Tensor): dtype torch.int32. The cumulative sequence lengths of the sequences in the batch, + used to index into kv. + max_seqlen_k (int): The maximum sequence length in the batch of k and v. + + Returns: + torch.Tensor: Output tensor after self attention with shape [b, s, ad] + """ + if mode in ["torch", "vanilla", "self_flash", "cross_flash"]: + if isinstance(q, tuple): + q = torch.cat(q, dim=1) + if isinstance(k, tuple): + k = torch.cat(k, dim=1) + if isinstance(v, tuple): + v = torch.cat(v, dim=1) + pre_attn_layout, post_attn_layout = MEMORY_LAYOUT[mode] + q = pre_attn_layout(q) + k = pre_attn_layout(k) + v = pre_attn_layout(v) + + if "flash" in mode: + assert ( + flash_attn_qkvpacked_func is not None + ), "Flash attention is not available. Please install flash_attn first." + flash_kwargs = dict(dropout_p=drop_rate, causal=causal) + if deterministic: + if not is_flash_attn_greater_or_equal("2.4.1"): + raise ValueError( + "Flash attention deterministic mode requires flash_attn>=2.4.1. " "Please upgrade flash_attn" + ) + flash_kwargs["deterministic"] = deterministic + + if mode == "self_flash": + qkv = torch.stack([q, k, v], dim=2) + if attn_mask is not None: + raise ValueError("Self attention does not support attention mask") + x = flash_attn_qkvpacked_func(qkv, **flash_kwargs) + + elif mode == "cross_flash": + kv = torch.stack([k, v], dim=2) + if attn_mask is None: + x = flash_attn_kvpacked_func(q, kv, **flash_kwargs) + else: + b, s, a, h = q.shape + cu_seqlens_q, max_seqlen_q, q = get_q_seqlens(q) + cu_seqlens_k, max_seqlen_k, kv = get_kv_seqlens_with_mask(attn_mask, k, v) + + attn_output = flash_attn_varlen_kvpacked_func( + q, + kv, + cu_seqlens_q=cu_seqlens_q, + cu_seqlens_k=cu_seqlens_k, + max_seqlen_q=max_seqlen_q, + max_seqlen_k=max_seqlen_k, + **flash_kwargs, + ) + x = attn_output.reshape(b, s, a, h) + elif mode == 'torch': + if attn_mask is not None and attn_mask.dtype != torch.bool: + attn_mask = attn_mask.to(q.dtype) + x = F.scaled_dot_product_attention(q, k, v, attn_mask=attn_mask, dropout_p=drop_rate, is_causal=causal) + + elif mode == "vanilla": + scale_factor = 1 / math.sqrt(q.size(-1)) + + b, a, s, _ = q.shape + s1 = k.size(2) + attn_bias = torch.zeros(b, a, s, s1, dtype=q.dtype, device=q.device) + if causal: + # Only applied to self attention + assert attn_mask is None, "Causal mask and attn_mask cannot be used together" + temp_mask = torch.ones(b, a, s, s, dtype=torch.bool, device=q.device).tril(diagonal=0) + attn_bias.masked_fill_(temp_mask.logical_not(), float("-inf")) + attn_bias.to(q.dtype) + + if attn_mask is not None: + if attn_mask.dtype == torch.bool: + attn_bias.masked_fill_(attn_mask.logical_not(), float("-inf")) + else: + attn_bias += attn_mask + + # TODO(jarvizhang): Maybe force q and k to be float32 to avoid numerical overflow + attn = (q @ k.transpose(-2, -1)) * scale_factor + attn += attn_bias + attn = attn.softmax(dim=-1) + attn = torch.dropout(attn, p=drop_rate, train=True) + x = attn @ v + else: + raise NotImplementedError(f"Unsupported attention mode: {mode}") + + if mode in ["torch", "vanilla", "self_flash", "cross_flash"]: + x = post_attn_layout(x).contiguous() + b, s, a, d = x.shape + out = x.reshape(b, s, -1) + return out + + +class SelfAttentionLayer(BasicAttentionLayer): + def __init__( + self, + dim, + num_heads, + qkv_bias=True, + qk_norm=True, + attn_drop=0, + proj_drop=0, + dtype=None, + device=None, + norm_type="layer", + attn_mode="self_flash", + deterministic=False, + ) -> None: + factory_kwargs = {"device": device, "dtype": dtype} + super().__init__(attn_mode, deterministic) + self.dim = dim + self.num_heads = num_heads + assert self.dim % num_heads == 0, "dim must be divisible by num_heads" + self.head_dim = self.dim // num_heads + self.attn_drop = attn_drop + + # This assertion is aligned with flash attention + assert self.head_dim % 8 == 0 and self.head_dim <= 128, "Only support head_dim <= 128 and divisible by 8" + + self.Wqkv = nn.Linear(dim, dim * 3, bias=qkv_bias, **factory_kwargs) + + norm_layer = get_norm_layer(norm_type) + self.q_norm = ( + norm_layer(self.head_dim, elementwise_affine=True, eps=1e-6, **factory_kwargs) if qk_norm else nn.Identity() + ) + self.k_norm = ( + norm_layer(self.head_dim, elementwise_affine=True, eps=1e-6, **factory_kwargs) if qk_norm else nn.Identity() + ) + + self.out_proj = nn.Linear(dim, dim, bias=qkv_bias, **factory_kwargs) + self.proj_drop = nn.Dropout(proj_drop) + + def forward(self, x, freqs_cis=None, attn_mask=None): + """ + Args: + x (torch.Tensor): (batch, seq_len, hidden_dim) (where hidden_dim = num heads * head dim) + freqs_cis (torch.Tensor, optional): (batch, hidden_dim // 2), RoPE for image + attn_mask (torch.Tensor, optional): (batch, seq_len, seq_len), mask for attention + """ + b, s, d = x.shape + + # Apply QKV projection + qkv = self.Wqkv(x) + qkv = qkv.view(b, s, 3, self.num_heads, self.head_dim) # [b, s, 3, a, d] + q, k, v = qkv.unbind(dim=2) # [b, s, a, d] + + # Apply QK-Norm if needed + q = self.q_norm(q) + k = self.k_norm(k) + + # Apply RoPE if needed + if freqs_cis is not None: + qq, kk = apply_rotary_emb(q, k, freqs_cis) + assert ( + qq.shape == q.shape and kk.shape == k.shape + ), f"qq: {qq.shape}, q: {q.shape}, kk: {kk.shape}, k: {k.shape}" + q, k = qq, kk + + # Apply self attention + context = attention( + q, + k, + v, + drop_rate=self.attn_drop if self.training else 0, + attn_mask=attn_mask, + mode=self.attn_mode, + deterministic=self.deterministic, + ) + out = self.out_proj(context) + out = self.proj_drop(out) + + return out + + +class CrossAttentionLayer(BasicAttentionLayer): + def __init__( + self, + qdim, + kdim, + num_heads, + qkv_bias=True, + qk_norm=True, + attn_drop=0, + proj_drop=0, + dtype=None, + device=None, + norm_type="layer", + attn_mode="cross_flash", + deterministic=False, + ): + factory_kwargs = {"device": device, "dtype": dtype} + super().__init__(attn_mode, deterministic) + self.qdim = qdim + self.kdim = kdim + self.num_heads = num_heads + assert self.qdim % num_heads == 0, "qdim must be divisible by num_heads" + self.head_dim = self.qdim // num_heads + self.attn_drop = attn_drop + + # This assertion is aligned with flash attention + assert self.head_dim % 8 == 0 and self.head_dim <= 128, "Only support head_dim <= 128 and divisible by 8" + + self.q_proj = nn.Linear(qdim, qdim, bias=qkv_bias, **factory_kwargs) + self.kv_proj = nn.Linear(kdim, 2 * qdim, bias=qkv_bias, **factory_kwargs) + + norm_layer = get_norm_layer(norm_type) + self.q_norm = ( + norm_layer(self.head_dim, elementwise_affine=True, eps=1e-6, **factory_kwargs) if qk_norm else nn.Identity() + ) + self.k_norm = ( + norm_layer(self.head_dim, elementwise_affine=True, eps=1e-6, **factory_kwargs) if qk_norm else nn.Identity() + ) + + self.out_proj = nn.Linear(qdim, qdim, bias=qkv_bias, **factory_kwargs) + self.proj_drop = nn.Dropout(proj_drop) + + def forward(self, x, y, attn_mask=None): + """ + Args: + x (torch.Tensor): (batch, seq_len, hidden_dim) (where hidden_dim = num heads * head dim) + y (torch.Tensor): (batch, seq_len1, hidden_dim1) + attn_mask (torch.Tensor): (batch, seq_len1), mask for attention + """ + b, s, d = x.shape + _, s1, d1 = y.shape + + q = self.q_proj(x).view(b, s, self.num_heads, self.head_dim) + kv = self.kv_proj(y).view(b, s1, 2, self.num_heads, self.head_dim) + k, v = kv.unbind(dim=2) + + # Apply QK-Norm if needed + q = self.q_norm(q) + k = self.k_norm(k) + + # Apply cross attention + context = attention( + q, + k, + v, + attn_mask=attn_mask, + drop_rate=self.attn_drop if self.training else 0, + mode=self.attn_mode, + deterministic=self.deterministic, + ) + out = self.out_proj(context) + out = self.proj_drop(out) + + return out diff --git a/hunyuanvideo_foley/models/nn/embed_layers.py b/hunyuanvideo_foley/models/nn/embed_layers.py new file mode 100644 index 0000000..2760655 --- /dev/null +++ b/hunyuanvideo_foley/models/nn/embed_layers.py @@ -0,0 +1,136 @@ +import math +import torch +import torch.nn as nn + +from ...utils.helper import to_2tuple, to_1tuple + +class PatchEmbed1D(nn.Module): + """1D Audio to Patch Embedding + + A convolution based approach to patchifying a 1D audio w/ embedding projection. + + Based on the impl in https://github.com/google-research/vision_transformer + + Hacked together by / Copyright 2020 Ross Wightman + """ + + def __init__( + self, + patch_size=1, + in_chans=768, + embed_dim=768, + norm_layer=None, + flatten=True, + bias=True, + dtype=None, + device=None, + ): + factory_kwargs = {"dtype": dtype, "device": device} + super().__init__() + patch_size = to_1tuple(patch_size) + self.patch_size = patch_size + self.flatten = flatten + + self.proj = nn.Conv1d( + in_chans, embed_dim, kernel_size=patch_size, stride=patch_size, bias=bias, **factory_kwargs + ) + nn.init.xavier_uniform_(self.proj.weight.view(self.proj.weight.size(0), -1)) + if bias: + nn.init.zeros_(self.proj.bias) + + self.norm = norm_layer(embed_dim) if norm_layer else nn.Identity() + + def forward(self, x): + assert ( + x.shape[2] % self.patch_size[0] == 0 + ), f"The patch_size of {self.patch_size[0]} must be divisible by the token number ({x.shape[2]}) of x." + + x = self.proj(x) + if self.flatten: + x = x.transpose(1, 2) # BCN -> BNC + x = self.norm(x) + return x + + +class ConditionProjection(nn.Module): + """ + Projects condition embeddings. Also handles dropout for classifier-free guidance. + + Adapted from https://github.com/PixArt-alpha/PixArt-alpha/blob/master/diffusion/model/nets/PixArt_blocks.py + """ + + def __init__(self, in_channels, hidden_size, act_layer, dtype=None, device=None): + factory_kwargs = {'dtype': dtype, 'device': device} + super().__init__() + self.linear_1 = nn.Linear(in_features=in_channels, out_features=hidden_size, bias=True, **factory_kwargs) + self.act_1 = act_layer() + self.linear_2 = nn.Linear(in_features=hidden_size, out_features=hidden_size, bias=True, **factory_kwargs) + + def forward(self, caption): + hidden_states = self.linear_1(caption) + hidden_states = self.act_1(hidden_states) + hidden_states = self.linear_2(hidden_states) + return hidden_states + + +def timestep_embedding(t, dim, max_period=10000): + """ + Create sinusoidal timestep embeddings. + + Args: + t (torch.Tensor): a 1-D Tensor of N indices, one per batch element. These may be fractional. + dim (int): the dimension of the output. + max_period (int): controls the minimum frequency of the embeddings. + + Returns: + embedding (torch.Tensor): An (N, D) Tensor of positional embeddings. + + .. ref_link: https://github.com/openai/glide-text2im/blob/main/glide_text2im/nn.py + """ + half = dim // 2 + freqs = torch.exp( + -math.log(max_period) + * torch.arange(start=0, end=half, dtype=torch.float32) + / half + ).to(device=t.device) + args = t[:, None].float() * freqs[None] + embedding = torch.cat([torch.cos(args), torch.sin(args)], dim=-1) + if dim % 2: + embedding = torch.cat( + [embedding, torch.zeros_like(embedding[:, :1])], dim=-1 + ) + return embedding + + +class TimestepEmbedder(nn.Module): + """ + Embeds scalar timesteps into vector representations. + """ + def __init__(self, + hidden_size, + act_layer, + frequency_embedding_size=256, + max_period=10000, + out_size=None, + dtype=None, + device=None + ): + factory_kwargs = {'dtype': dtype, 'device': device} + super().__init__() + self.frequency_embedding_size = frequency_embedding_size + self.max_period = max_period + if out_size is None: + out_size = hidden_size + + self.mlp = nn.Sequential( + nn.Linear(frequency_embedding_size, hidden_size, bias=True, **factory_kwargs), + act_layer(), + nn.Linear(hidden_size, out_size, bias=True, **factory_kwargs), + ) + nn.init.normal_(self.mlp[0].weight, std=0.02) + nn.init.normal_(self.mlp[2].weight, std=0.02) + + def forward(self, t): + t_freq = timestep_embedding(t, self.frequency_embedding_size, self.max_period).type(self.mlp[0].weight.dtype) + t_emb = self.mlp(t_freq) + return t_emb diff --git a/hunyuanvideo_foley/models/nn/mlp_layers.py b/hunyuanvideo_foley/models/nn/mlp_layers.py new file mode 100644 index 0000000..ffe6edc --- /dev/null +++ b/hunyuanvideo_foley/models/nn/mlp_layers.py @@ -0,0 +1,149 @@ +# Modified from timm library: +# https://github.com/huggingface/pytorch-image-models/blob/648aaa41233ba83eb38faf5ba9d415d574823241/timm/layers/mlp.py#L13 + +from functools import partial + +import torch +import torch.nn as nn +import torch.nn.functional as F + +from .modulate_layers import modulate +from ...utils.helper import to_2tuple + +class MLP(nn.Module): + """MLP as used in Vision Transformer, MLP-Mixer and related networks""" + + def __init__( + self, + in_channels, + hidden_channels=None, + out_features=None, + act_layer=nn.GELU, + norm_layer=None, + bias=True, + drop=0.0, + use_conv=False, + device=None, + dtype=None, + ): + factory_kwargs = {"device": device, "dtype": dtype} + super().__init__() + out_features = out_features or in_channels + hidden_channels = hidden_channels or in_channels + bias = to_2tuple(bias) + drop_probs = to_2tuple(drop) + linear_layer = partial(nn.Conv2d, kernel_size=1) if use_conv else nn.Linear + + self.fc1 = linear_layer(in_channels, hidden_channels, bias=bias[0], **factory_kwargs) + self.act = act_layer() + self.drop1 = nn.Dropout(drop_probs[0]) + self.norm = norm_layer(hidden_channels, **factory_kwargs) if norm_layer is not None else nn.Identity() + self.fc2 = linear_layer(hidden_channels, out_features, bias=bias[1], **factory_kwargs) + self.drop2 = nn.Dropout(drop_probs[1]) + + def forward(self, x): + x = self.fc1(x) + x = self.act(x) + x = self.drop1(x) + x = self.norm(x) + x = self.fc2(x) + x = self.drop2(x) + return x + + +# copied from https://github.com/black-forest-labs/flux/blob/main/src/flux/modules/layers.py +# only used when use_vanilla is True +class MLPEmbedder(nn.Module): + def __init__(self, in_dim: int, hidden_dim: int, device=None, dtype=None): + factory_kwargs = {"device": device, "dtype": dtype} + super().__init__() + self.in_layer = nn.Linear(in_dim, hidden_dim, bias=True, **factory_kwargs) + self.silu = nn.SiLU() + self.out_layer = nn.Linear(hidden_dim, hidden_dim, bias=True, **factory_kwargs) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return self.out_layer(self.silu(self.in_layer(x))) + + +class LinearWarpforSingle(nn.Module): + def __init__(self, in_dim: int, out_dim: int, bias=True, device=None, dtype=None): + factory_kwargs = {"device": device, "dtype": dtype} + super().__init__() + self.fc = nn.Linear(in_dim, out_dim, bias=bias, **factory_kwargs) + + def forward(self, x, y): + z = torch.cat([x, y], dim=2) + return self.fc(z) + +class FinalLayer1D(nn.Module): + def __init__(self, hidden_size, patch_size, out_channels, act_layer, device=None, dtype=None): + factory_kwargs = {"device": device, "dtype": dtype} + super().__init__() + + # Just use LayerNorm for the final layer + self.norm_final = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6, **factory_kwargs) + self.linear = nn.Linear(hidden_size, patch_size * out_channels, bias=True, **factory_kwargs) + nn.init.zeros_(self.linear.weight) + nn.init.zeros_(self.linear.bias) + + # Here we don't distinguish between the modulate types. Just use the simple one. + self.adaLN_modulation = nn.Sequential( + act_layer(), nn.Linear(hidden_size, 2 * hidden_size, bias=True, **factory_kwargs) + ) + # Zero-initialize the modulation + nn.init.zeros_(self.adaLN_modulation[1].weight) + nn.init.zeros_(self.adaLN_modulation[1].bias) + + def forward(self, x, c): + shift, scale = self.adaLN_modulation(c).chunk(2, dim=-1) + x = modulate(self.norm_final(x), shift=shift, scale=scale) + x = self.linear(x) + return x + + +class ChannelLastConv1d(nn.Conv1d): + + def forward(self, x: torch.Tensor) -> torch.Tensor: + x = x.permute(0, 2, 1) + x = super().forward(x) + x = x.permute(0, 2, 1) + return x + + +class ConvMLP(nn.Module): + + def __init__( + self, + dim: int, + hidden_dim: int, + multiple_of: int = 256, + kernel_size: int = 3, + padding: int = 1, + device=None, + dtype=None, + ): + """ + Convolutional MLP module. + + Args: + dim (int): Input dimension. + hidden_dim (int): Hidden dimension of the feedforward layer. + multiple_of (int): Value to ensure hidden dimension is a multiple of this value. + + Attributes: + w1: Linear transformation for the first layer. + w2: Linear transformation for the second layer. + w3: Linear transformation for the third layer. + + """ + factory_kwargs = {"device": device, "dtype": dtype} + super().__init__() + hidden_dim = int(2 * hidden_dim / 3) + hidden_dim = multiple_of * ((hidden_dim + multiple_of - 1) // multiple_of) + + self.w1 = ChannelLastConv1d(dim, hidden_dim, bias=False, kernel_size=kernel_size, padding=padding, **factory_kwargs) + self.w2 = ChannelLastConv1d(hidden_dim, dim, bias=False, kernel_size=kernel_size, padding=padding, **factory_kwargs) + self.w3 = ChannelLastConv1d(dim, hidden_dim, bias=False, kernel_size=kernel_size, padding=padding, **factory_kwargs) + + def forward(self, x): + return self.w2(F.silu(self.w1(x)) * self.w3(x)) diff --git a/hunyuanvideo_foley/models/nn/modulate_layers.py b/hunyuanvideo_foley/models/nn/modulate_layers.py new file mode 100644 index 0000000..1962c45 --- /dev/null +++ b/hunyuanvideo_foley/models/nn/modulate_layers.py @@ -0,0 +1,49 @@ +from typing import Callable +import torch +import torch.nn as nn + +class ModulateDiT(nn.Module): + def __init__(self, hidden_size: int, factor: int, act_layer: Callable, dtype=None, device=None): + factory_kwargs = {"dtype": dtype, "device": device} + super().__init__() + self.act = act_layer() + self.linear = nn.Linear(hidden_size, factor * hidden_size, bias=True, **factory_kwargs) + # Zero-initialize the modulation + nn.init.zeros_(self.linear.weight) + nn.init.zeros_(self.linear.bias) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return self.linear(self.act(x)) + + +def modulate(x, shift=None, scale=None): + if x.ndim == 3: + shift = shift.unsqueeze(1) if shift is not None and shift.ndim == 2 else None + scale = scale.unsqueeze(1) if scale is not None and scale.ndim == 2 else None + if scale is None and shift is None: + return x + elif shift is None: + return x * (1 + scale) + elif scale is None: + return x + shift + else: + return x * (1 + scale) + shift + + +def apply_gate(x, gate=None, tanh=False): + if gate is None: + return x + if gate.ndim == 2 and x.ndim == 3: + gate = gate.unsqueeze(1) + if tanh: + return x * gate.tanh() + else: + return x * gate + + +def ckpt_wrapper(module): + def ckpt_forward(*inputs): + outputs = module(*inputs) + return outputs + + return ckpt_forward diff --git a/hunyuanvideo_foley/models/nn/norm_layers.py b/hunyuanvideo_foley/models/nn/norm_layers.py new file mode 100644 index 0000000..a0719a1 --- /dev/null +++ b/hunyuanvideo_foley/models/nn/norm_layers.py @@ -0,0 +1,70 @@ +import torch +import torch.nn as nn + +class RMSNorm(nn.Module): + def __init__(self, dim: int, elementwise_affine=True, eps: float = 1e-6, + device=None, dtype=None): + """ + Initialize the RMSNorm normalization layer. + + Args: + dim (int): The dimension of the input tensor. + eps (float, optional): A small value added to the denominator for numerical stability. Default is 1e-6. + + Attributes: + eps (float): A small value added to the denominator for numerical stability. + weight (nn.Parameter): Learnable scaling parameter. + + """ + factory_kwargs = {'device': device, 'dtype': dtype} + super().__init__() + self.eps = eps + if elementwise_affine: + self.weight = nn.Parameter(torch.ones(dim, **factory_kwargs)) + + def _norm(self, x): + """ + Apply the RMSNorm normalization to the input tensor. + + Args: + x (torch.Tensor): The input tensor. + + Returns: + torch.Tensor: The normalized tensor. + + """ + return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps) + + def forward(self, x): + """ + Forward pass through the RMSNorm layer. + + Args: + x (torch.Tensor): The input tensor. + + Returns: + torch.Tensor: The output tensor after applying RMSNorm. + + """ + output = self._norm(x.float()).type_as(x) + if hasattr(self, "weight"): + output = output * self.weight + return output + + +def get_norm_layer(norm_layer): + """ + Get the normalization layer. + + Args: + norm_layer (str): The type of normalization layer. + + Returns: + norm_layer (nn.Module): The normalization layer. + """ + if norm_layer == "layer": + return nn.LayerNorm + elif norm_layer == "rms": + return RMSNorm + else: + raise NotImplementedError(f"Norm layer {norm_layer} is not implemented") diff --git a/hunyuanvideo_foley/models/nn/posemb_layers.py b/hunyuanvideo_foley/models/nn/posemb_layers.py new file mode 100644 index 0000000..9265243 --- /dev/null +++ b/hunyuanvideo_foley/models/nn/posemb_layers.py @@ -0,0 +1,159 @@ +import torch +from typing import Union, Tuple + + +def _to_tuple(x, dim=2): + if isinstance(x, int): + return (x,) * dim + elif len(x) == dim: + return x + else: + raise ValueError(f"Expected length {dim} or int, but got {x}") + + +def get_meshgrid_nd(start, *args, dim=2): + """ + Get n-D meshgrid with start, stop and num. + + Args: + start (int or tuple): If len(args) == 0, start is num; If len(args) == 1, start is start, args[0] is stop, + step is 1; If len(args) == 2, start is start, args[0] is stop, args[1] is num. For n-dim, start/stop/num + should be int or n-tuple. If n-tuple is provided, the meshgrid will be stacked following the dim order in + n-tuples. + *args: See above. + dim (int): Dimension of the meshgrid. Defaults to 2. + + Returns: + grid (np.ndarray): [dim, ...] + """ + if len(args) == 0: + # start is grid_size + num = _to_tuple(start, dim=dim) + start = (0,) * dim + stop = num + elif len(args) == 1: + # start is start, args[0] is stop, step is 1 + start = _to_tuple(start, dim=dim) + stop = _to_tuple(args[0], dim=dim) + num = [stop[i] - start[i] for i in range(dim)] + elif len(args) == 2: + # start is start, args[0] is stop, args[1] is num + start = _to_tuple(start, dim=dim) # Left-Top eg: 12,0 + stop = _to_tuple(args[0], dim=dim) # Right-Bottom eg: 20,32 + num = _to_tuple(args[1], dim=dim) # Target Size eg: 32,124 + else: + raise ValueError(f"len(args) should be 0, 1 or 2, but got {len(args)}") + + # PyTorch implement of np.linspace(start[i], stop[i], num[i], endpoint=False) + axis_grid = [] + for i in range(dim): + a, b, n = start[i], stop[i], num[i] + g = torch.linspace(a, b, n + 1, dtype=torch.float32)[:n] + axis_grid.append(g) + grid = torch.meshgrid(*axis_grid, indexing="ij") # dim x [W, H, D] + grid = torch.stack(grid, dim=0) # [dim, W, H, D] + + return grid + + +################################################################################# +# Rotary Positional Embedding Functions # +################################################################################# +# https://github.com/meta-llama/llama/blob/be327c427cc5e89cc1d3ab3d3fec4484df771245/llama/model.py#L80 + + +def get_nd_rotary_pos_embed( + rope_dim_list, start, *args, theta=10000.0, use_real=False, theta_rescale_factor=1.0, freq_scaling=1.0 +): + """ + This is a n-d version of precompute_freqs_cis, which is a RoPE for tokens with n-d structure. + + Args: + rope_dim_list (list of int): Dimension of each rope. len(rope_dim_list) should equal to n. + sum(rope_dim_list) should equal to head_dim of attention layer. + start (int | tuple of int | list of int): If len(args) == 0, start is num; If len(args) == 1, start is start, + args[0] is stop, step is 1; If len(args) == 2, start is start, args[0] is stop, args[1] is num. + *args: See above. + theta (float): Scaling factor for frequency computation. Defaults to 10000.0. + use_real (bool): If True, return real part and imaginary part separately. Otherwise, return complex numbers. + Some libraries such as TensorRT does not support complex64 data type. So it is useful to provide a real + part and an imaginary part separately. + theta_rescale_factor (float): Rescale factor for theta. Defaults to 1.0. + freq_scaling (float, optional): Frequence rescale factor, which is proposed in mmaudio. Defaults to 1.0. + + Returns: + pos_embed (torch.Tensor): [HW, D/2] + """ + + grid = get_meshgrid_nd(start, *args, dim=len(rope_dim_list)) # [3, W, H, D] / [2, W, H] + + # use 1/ndim of dimensions to encode grid_axis + embs = [] + for i in range(len(rope_dim_list)): + emb = get_1d_rotary_pos_embed( + rope_dim_list[i], + grid[i].reshape(-1), + theta, + use_real=use_real, + theta_rescale_factor=theta_rescale_factor, + freq_scaling=freq_scaling, + ) # 2 x [WHD, rope_dim_list[i]] + embs.append(emb) + + if use_real: + cos = torch.cat([emb[0] for emb in embs], dim=1) # (WHD, D/2) + sin = torch.cat([emb[1] for emb in embs], dim=1) # (WHD, D/2) + return cos, sin + else: + emb = torch.cat(embs, dim=1) # (WHD, D/2) + return emb + + +def get_1d_rotary_pos_embed( + dim: int, + pos: Union[torch.FloatTensor, int], + theta: float = 10000.0, + use_real: bool = False, + theta_rescale_factor: float = 1.0, + freq_scaling: float = 1.0, +) -> Union[torch.Tensor, Tuple[torch.Tensor, torch.Tensor]]: + """ + Precompute the frequency tensor for complex exponential (cis) with given dimensions. + (Note: `cis` means `cos + i * sin`, where i is the imaginary unit.) + + This function calculates a frequency tensor with complex exponential using the given dimension 'dim' + and the end index 'end'. The 'theta' parameter scales the frequencies. + The returned tensor contains complex values in complex64 data type. + + Args: + dim (int): Dimension of the frequency tensor. + pos (int or torch.FloatTensor): Position indices for the frequency tensor. [S] or scalar + theta (float, optional): Scaling factor for frequency computation. Defaults to 10000.0. + use_real (bool, optional): If True, return real part and imaginary part separately. + Otherwise, return complex numbers. + theta_rescale_factor (float, optional): Rescale factor for theta. Defaults to 1.0. + freq_scaling (float, optional): Frequence rescale factor, which is proposed in mmaudio. Defaults to 1.0. + + Returns: + freqs_cis: Precomputed frequency tensor with complex exponential. [S, D/2] + freqs_cos, freqs_sin: Precomputed frequency tensor with real and imaginary parts separately. [S, D] + """ + if isinstance(pos, int): + pos = torch.arange(pos).float() + + # proposed by reddit user bloc97, to rescale rotary embeddings to longer sequence length without fine-tuning + # has some connection to NTK literature + # https://www.reddit.com/r/LocalLLaMA/comments/14lz7j5/ntkaware_scaled_rope_allows_llama_models_to_have/ + if theta_rescale_factor != 1.0: + theta *= theta_rescale_factor ** (dim / (dim - 1)) + + freqs = 1.0 / (theta ** (torch.arange(0, dim, 2)[: (dim // 2)].float() / dim)) # [D/2] + freqs *= freq_scaling + freqs = torch.outer(pos, freqs) # [S, D/2] + if use_real: + freqs_cos = freqs.cos().repeat_interleave(2, dim=1) # [S, D] + freqs_sin = freqs.sin().repeat_interleave(2, dim=1) # [S, D] + return freqs_cos, freqs_sin + else: + freqs_cis = torch.polar(torch.ones_like(freqs), freqs) # complex64 # [S, D/2] + return freqs_cis diff --git a/hunyuanvideo_foley/models/synchformer/__init__.py b/hunyuanvideo_foley/models/synchformer/__init__.py new file mode 100644 index 0000000..5026833 --- /dev/null +++ b/hunyuanvideo_foley/models/synchformer/__init__.py @@ -0,0 +1 @@ +from .synchformer import Synchformer diff --git a/hunyuanvideo_foley/models/synchformer/__pycache__/__init__.cpython-312.pyc b/hunyuanvideo_foley/models/synchformer/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0fc7cddeaf58872bfd434589dcb554b0033a82a4 GIT binary patch literal 246 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 JQ%6wz{{qyCse1qb literal 0 HcmV?d00001 diff --git a/hunyuanvideo_foley/models/synchformer/__pycache__/utils.cpython-312.pyc b/hunyuanvideo_foley/models/synchformer/__pycache__/utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5df2a822d83a833c64d47db5a7815e28018716e1 GIT binary patch literal 6246 zcmbtYU2Gf25#Bo`mi3}Ou~>fgbgsJ4;$l%HwebxAl`VF{w2km2wg#FGodYn zt|YXT&^AI>5qb}ys|jr7 zbSt3`5c(jY4-vYJ&@W?}P>1i*ROK7*{_CeysZaB~{045{xdS_f9)Zuq`!g=C*;xbHcXFF{V>Y49`P73P_Cs?<}?bDh?O}9gUit0GeUsYHebc8HeJu{Ml{~yos;S zWE5Rn~3*a461-q8vkj!Kq37f`ct|o-2vo95;aLv=qjS% z@BMx6>aLer;eIMDJNfivDv{=R z0Lad$m`*z7nBYvrQ`enxn$QzasD@m$Vj?ry^mWBJ!HGgjmWHOA2e=eWi>G)3jo_sw z(s9B|2*;fY)jr)loDeukD6E7RN^+?Tmq<)GIhZM=q>LyO*2-(x4z;mCl0|__PTL~s zlng7n6GCcCj)}OiR;bf1ouZf1GM7-8Oe!ubX56w;gi8ntJvj;j%#m0ob)A*s(}Jr? z#8*r)s0L6B>LwI@@;VMKBsraAQ&7}P8k0SjNlj(A)SGc$ zNS{e3gsBnOJm$QEIc$`$)b9Z!_=`a-nii9SIFga$38{TzN@*qM$Ht>9_7qk%6boM) z1Xgns5v~Yw!$+!t@HUJ7ZmGJ>ymx#yxN51obz=U+&-+$QmA5SOmS-l%W0Paa|DZN! zay&70tyVd1{doSzi~PO#o%nL?N>%$D19Rq1uGyPb?cN6mAJjj%k=xUo>ytNhbd7Bt z(N^1c24t(hH8MZ4cw{N`@zsy6K5q0pWS(04KCNDJG%a4dclFNId)Mw<%N;%O@Z!q; z@2xmqnR{)mx^D5c<;xF4PpeOV%CFTq77s2lPwEaWSFbu-KDK>idw68Y_JwnBkx}+F zuQu(!clOTNrQnAbR-2sn&fht|Y<}9*`Hc~^oS~lUP<IwDALhQ;bg_)wfqu?^3uBxK5a{58B~J4fAb!deyNyvc;LOj$}goV-wP(?^L@K zw1OJtGTM^y@|n=Z3@NlYT5><|CO{#yfldA^Y5~oo@nTbuZQz@sc_?@vJrsu%z(33| zkZO1rB_0Hrgs07_M`t>j;F6;}cWSx`as&%M-$ z6oa~`sv15J?D?;#>X|o{5`!>w9zN1r(10FmRrdEXv**?-taJSB;FAi+;zfvuD~-Mf z17Fnj%nhzq*4}z!{*A@KrS}pQJCf__+?BtMMJ_i45U-h_}CT~#Zou0 z4Hs}ptI2`B_GoSaP6xtA;Bw8-GrD5O&C=OI#DbEkmO)c+u0wD=^P#<&7FsTG=8v|8J^@cIX@ z|9WUXIM+XSW3960c5ty}@#53UX0T`70dnv|u%3x!aRI=xikZc~#bqGBvPvb(-hett z9TD;Ff2aBiQ9(Qd4AO+~iuj7%L>n}%cdAZ>hg~+PNx)lh%){P=YcrYj@NUEo+~As6 zEm$nW{g(!ZLhS3W4xLxbXNLMO4Eiulvbv&!SV}?@@gsHhOL#7EVSJ|)(@9nLQ{wlb zi%&`V2ej)tilV+md%r{te?!$@qpB}a-B-2?vzB$b#YTU{G%o&VF_}9W$u;rodf06e5uyi5kP3KxC){U5FM*4k4I1A=gmg20Kx2F_Wjd}J`Tn*;cm*VzfUPCF)fq6}( zxc!*-QYo$(^IA)B2QcqoDee&F9WKSWFz-ky?kMK9m*PB_*HMb|VxAxAUtT`3?9O#x z%N>4uJ%D*#=vcSLSv}hIzmVVdQy^*9ync>Cy#v(e@&>A={OgB})ZW{X4Fs=mYYmkB Oxm`y&zeUgxH~2U9A~R|L literal 0 HcmV?d00001 diff --git a/hunyuanvideo_foley/models/synchformer/__pycache__/video_model_builder.cpython-312.pyc b/hunyuanvideo_foley/models/synchformer/__pycache__/video_model_builder.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9df0c73ab30b34344ffcb5e268afb5b9e62314cd GIT binary patch literal 11822 zcmcIqYfv27b?%-Q^JGB0VHjqR03jo!mB4zeRssP+?D7&bdZ9;qG~K`q^FTd~B-p{; zO_cIrYYmj0z{Fb_a9k0xNLF@|vNnHqf2G!yAEpIzkGpWyCU%u;Q%RL}B|CQhBd-5zi-qm?vn0CIdu} z93)9Zs{*Q^dQdIV>VRfY13Yy=8`KTzB%UUa6Vwmtf!5NxfFWocGzLwBrl5JyOcE-h zlc00%611Mt`O9&IO8QVO(}TGa>UzT`C{s_0raqQtScY!*c{#}DjCokj=Lz%zZQ%*$PN6XsC8)F{Xi38im&vK$yg83@pp}EUJ7{HzDQFoo zv}{}O84Rpb)8Y9F$G$C6x#2= zw332E4bE{rX{r}f{(ej;DXm%2K1|K~Db)_%3@GxLg1XY9V+XAZSXN0vs{!YtB;@wv z9Cr;Bv6AyJwaP#p2e35f!4zoeF|DM)ZC7%b0&bQy9Jnv- z4ag2+Yf74R?Yp6rV`@eJFtv_CThglYK8BKdji~nphpC8fk`av(kRsE1Ux@OK01zV@7=$QMv@ck%WAi~rEZ~?R zfG+?&u}mZ!80P@AVPWeb*aGw_L^9?{>IMRtBx@`>>Hqr8!oTVK=gIG!dI0rDI2zm_ z$3(3^>43>1`Q$R&@x~Jppe1CaUqJE7-K{=_#hfW<3VbK@zCH|Ffc}e zk}oXkZ;sxgZumSAQA4xgv6PlfqBjLi8xf5NF{1*W35FFloF_CQYKED>xTqTPifWIS zo75r0HFW(oQ40VQNa-oc7xHlwC2HDR-L0b4JI+?NfZtl( zZD(aJ3Wlg}JKNgZ+tKZk!Xj!VRWS$Iado&w6KaSz;E6<-h^X!9Ywze5O}*!PsJ8A_ zm#f1i8ajGTceGROT|IJ0we21K?z5tnX2zf`1|)j6qqW_|I#6wfp6-5Xpw->g$2z6j z&%JShI^EUkVyjVB(+)0+1}PRO&Y}S^DDe5Ln469eiY6nPky#3r_nmi(W)u?&iG;^F z(R87!y`zun>g|Uj6G9ck5Fxm~U$qnds9KilQ zSGrw#B!HR)#(*t#ILrkurDQ}CEk)}CzEA|=x1z=eY|$tMsJr!Y$AD<;3De^NrrQ_c zL@l7F!wieg9->Z~f-oo0xED2UX~cJSbe|VB0}Q;4F6QPq6GE^ui$GCP*X;{29u`&t zmZ1XSVILO}^<6X);U+{wdL)QOKx#vPu7;350aUL#z(5Bw(6Nj(h;kwf2fYRp4QYv5 z&)8UCLbS-!Ne*hH-Xv-wOkjvbcT=_&z0hezB=5jnA7e$*CmKh5G}Mol9~L1qBIywg zlss=JDq;ebBmKPfpP}~SSKGqDp^5WdSH0sAE*u1uoMs}JNu3=JO^ka&7tp{x6AmyF zS4U)yJd}qdRuEJIk*kr3kat7|qOZ!uQUP^i+!p|7zHw}VMTf}A@@=?vBVIPpf*4ta z!SK7chz-4EI)~SnuItN_r4k}&1Y9GgBSm~6bswwU|G)R=TsFSadA#M+;lZ68-vCnn+(LRAxA)f8)AH`|hB zwnRz1L@3+Gm+b=sxyho^`HOQG=daCOgRF6T`8I`ZF31uITihlYv^x{S@nOM!fVUrr zY1Z`>Nn2H-Gu|oK4)Qi=*1EndSz%8ci60Rv8u^Mw#3@bguDyR`>4>oVB)|J4;y98e z<@2}aZqI*h?rUJyoU}O-z42a2eWS?s?m*&d{OU5dUepvbCUKRAc>5tFQL#Nv1;t=R z|3XLDYN5%C?)%gd^_cr$>iwzJOIL+UL;R(o7iyKwD0%b3NK}0(SyYCil634597lP_ z(PT*lat5=D%jZLLp=4QQqBLHL8ZVSJ^JUF1EICz@k%||2#KC6az$yN~sSh*dO1& z?EabgP|}h=b7}h0%+=|uf@L>v*}XXV#PZ69-H`~y11rt%z53wQM>iku5snY=#|Kv3 zmjw3>-hE^3OWrj*4c7Xgq1AQw?MwDYrd^P^nwL+Ll_YBgonK|)| z6KG`Gupu}lWwOky z8pSji4{qdDd|X!fE{oQZ<-+&u5A2W2*Q(lAOFNQf_C@uA`I%N-RQz16MV{qUfoJdZ zz1T%mITNGt(eDORF^5={$IBOKq4o%0dt|-rs5E#?Fa(9-TE4h;rEocC-E{EpTxfiE z@yEK>nqU9u+>_?sjZ)|LYgW|D?Lxx|zTw2$-q+TuPyVd*)SPCPoPA{@uVg{9FtVHv zeSiABt_NL@=pToE82XSF&RpQnTv)w$i9d6BtzqzK-jz?)L|F?75jsnDlS1c3zVqVh z;FTwxSKd`63~|Ha9@Hg|_6Ubg^M_8am$t2Yyn-jddje~o5Hz9-G!2#+O~Yk&XQC`nk)lfVB=2OSf!z{>u#w!~8fle)Fhvq!(_Gb1|ENS_)16;9spkrCeV z6D2ux!j+zE9~bdOg7tHmp9aw>X3Im9D=I7v@agBT;b?Zy^6f8Wcxc{IDP;ZD?Y!)k zXtm5n1##Nz}+U7j)BrGh|pgSaB4KFrs=Di{N093`WqnaX8qwAz=}; zK3SFJ+#%goQpc-rf>2}uUU2q++2?t8SE4Llwr<*uR#|6aZ+!2v=BauAh81$K&0daQ zUbogQx4w7g!I{;DlN))v=5yw9gghso=UjB&uU)ELc0SE(c%~-ypIWun{mN3ZK)!n@ zaUyhr9OFUM&SP&e@VIsNYyB<*YvU#pn4M3#Cp){3VK~l z^U_)N2Jq_D@&WY#68Z|f9*|O}M3Colhltitn3BUz(TvwKG3ngiIa@@&1KN>2@Oq2L zzGI37$5ze9{-xm6Rr9Ot*FkVos8HonsLFxZDOAu{689?z{Z2;7XIul*3w)Nw9(vg@PP ze+Yl)^nBM`*8-gg#Y4-ZYlX*Sns*HAuJeNHI`6u^=K2cC%dyX#4(Q$|#s1&mjbfi| z0?{PGsNuUf;ZMg67^w$hoSKqT@DCvPj!0zdAAfp#!g>hxhJTl+Fa|fJ;gC1dlkKZ; zR1+lue^sxf7ttvgDcSxfGo=N7F1Dql+5T}fg}$>R{AJ6-{tEc0W4Aon{wtFWWu>(e z*8Q#Q9>U+jh(Ce7CwIZdMqbu9NyexMws13Ntcwq5`2*e>*TKqXuZhx zy(?9V7Rt3U4RUo^+LkeXxq4gZuvaLk$ zz?SE8Xg6+Ag=|y}{{cy@i&NyiD;l)As)tmt&g`7*{=c>)`nhaEY@p9 zKxJ-H2&pkj2pqz%BBBnk88&=Jv>;x<6QYA2b`*dgK(eTyCo)=J3UKI%2k_7VWD0GL zqK>&U29m4`Wz=2|C+(hWKN3KDHw#NVv1Qv74dbB*08R{G%{rDD1N=_{2&GL{%$GR; z5CNhVU_7?~M?yBx*1?XW*9g4Cyw?8yfxb(W1b?|N_jicaOb(!>^%i-(eiMn4r`bP4 zsWA=BMWCI`ZCJoDG^Yzmh@Onz$!J|J=L;|yA_Y=D4SUN%RxZp*J3-12JB2Dz-w2FK zmxDAag^1J^0btT?4@;+^&&e!X#8#Q_fz`+duxh+Tz_r989kXUzr(che&+-UMzF@B6 z%~gx!nz=egCJRbp`e)SyKo-~Mt_wx`_@aHwuPonqRJ~Tz95Zg1^JluIyJjaBUzrUr zzp`%L4@UM8aDU8==f*5>Q55o>e7;l2-_PgokLjc`jW(`Sa;}32=a{jRTN1o+7yz{8wJjFXt zeMo-TB(z=O+per$yT0b6W^@?-02!`Y8K>(%|U)?{JPf|W07E7Wn%Nd3(Ph}NsJU-iGX1Q$Vfsw0H5U1K zUO_T1KUr!|R#XWUhxv-b2pGCEcV|927ZpmH_>!iiy*dFWS_DT3_T#+$c(Tk1xKMuX zbSoT_OZGpr5qbHudt+~|>dU3Q8a{du^||P?LI|cFd{!`06o85V-U4l=sGH-SK&m8% zqUf-fq9lw_I`F3^n;&ioI^Qs0x(xdb7#!?9^qN7hWq1L`Dj({+Odvo}unN2dTKyIFbx@zAR{qcx}0tLqyOr`b&4TGEk2?`H#W+;g; zkT6@`GH5#yS-BjcBrd_b-d(7yz0h~LTV^j;%VcvE<@V|h5a=Z`%iQWJzPi=zKgvqZ zrgB?#_kBH>0R%zTW*;q~r>Fbto}PaEzDIwHQq<4!Mh@Ci;z63G zsCkN^1}TPSRAG9MCRf#gwhF=uBTAC38fn->z_hdPbeFJ((@F`24)S@_%=Q0 zVOp6cxHd9vOfy`Yrc}PQaqFughK+e6F@_EEUH5Pl`h69NbZKtYbX*JVYf=p+{UqCHo&v|HYRo@8jb~+ZI`!qb#J{K62`7>8@uX< zgntMK{Nvrv`g?kIZr#0|9S-hcn67bQG(P?>^T2R`8!10=C(Aw;*vSmz6Jc<4*ro1E zqD8v;39)DpVwe1p0Doy%Bd;pB>fwiL>%sqkXr2;aH%Nh@6I6nh*IiPRP=Dn>LOrIE z%gfh`keAdZbqURwysOHE6x6|IRE30=Q6+S8>x}x+CW;b_G7+QsxYUo2OV5MnYsSoS zdHKp{Z|P*(q`so}`h-5AlG{%j5{3cFXXuaD4|zjg<{9shcg)Lpw;q5*$UAlqpT^;% z;+n0YXzkkYG)=9V>^aA#YxH_L zDRt3YcWvLyzI0D&U%}iqeYDi>`USdR>X|+Q#nUGqz!RxqjO0*4J?g<>$ax3WCrbf7 zQ-IBy6cq%de%m;xW?(G=#t^KRD@s>DS2dukE=66X2lcQ5bfW!e2nU8aBnWI2L17rD zQ4Y9%$!_=v7Q;7a*bj0EHKR{ZUxo6IOF1-^P$ejTX?;?#R}!i?+`&H3F?8GjIh0@S zf5qCa)laAkyeAH=C)Bsp@~VaUO5e(R<4N`9odUI#{g$A{EXoTgi7~s9R1#xOC8;FF z>XoFDDDO;o|JtqN;;9P%$kywsnt|JYQs1>QQjosaYW<(7?-jGv`1m^{6m)~RvdZO8 zsiX8A>L@**f)(9ABDHY$Pw(Ms1Ju$Cg#tW5GY)6pX9JL$gncd={QgiBw!L38Mkgb% z`vVLQWy66mE2^SVQFkg71-2@x0SrXVI2Riebyrwqj-rMM2mydJfHyza39X9yqg-qp z@JciyZ1@?t6OCAhBz*=^!?WQaX9HRez@3sHhIFk5^LCIt?1LXrRy6wk!{GqW`~5s@ z9T@UXs7xZZKk<4%=nDz5LkAYVbK zashDt68Eje&rvD{j4I*YMW~kmA&?Yorxlms3z}ahH zx7-Iti?RW)Izjz54V21$DfgFAB!DqoZB>C$&A1pv5*Tw-6~iq^A~k!!=$fEZep8S@ zbTD2ua54^gNnQE<8U5Q-t#>DKk}+u_S98)ruGVB7jD{znVL%7@9TjK{y5CU+Rrq8j zPlqK^UecblMYW8P)U_q;f`ZWrBaU`LbIbH`Nxn-u%6&>{Q>v6Ys0MArkT4}IiMpzJ zF(oa~6H~33U<{-;<|mgS-+R|x@iaHA01G_HoUjH}@(7mR+bVeEIT)*jj^=~~__nUR z#*&t&)Ud+aJX!pFvfKn$dnle~pIm z#hpe%!p7KV+_xMucTU=#(kolSUe&A0*&=h0uvg5Mv!cIF>?!mYdi{aAqCd{3JkybI zRz35PRvG7v>y}G-11GHKcKQ9v*Mw8vdr8m%#t>brLKz`|f(E(D-OnKzhr5$Q_B32V z5K<8_$_2j%UWkZPrIaQ>v!0=EDf<)JQuoIjhP+Ylrcp2BLlJ$`kayGv84U0?dh{mE zA!dP^LkrwKNbv|oUWrqPhW+6{WQYkIh;M$3U4YFkvCeWI#R&oMftyWq(6msSqY8vWaME6Olzp<0gR@QuY5@gEPhL$EG#p zeMB=T;IVNYxjV1i1i2XS`cjDy;}NR7)6*qB|C_$|X{gQPZpMk*lRdMrZ(-}Au++D- zFSq@4zQdmzc;ilopS*SL$@4gTT;*j%DsCrTlC}rw5Z8-^4a#njwA<1?;&6@o zoJ11io`JNe9R@w|lBkXZ#zl3QjdG|Jh{nk%KQYO&aj>hfSH}ZUM$~gG(Q!E+5D@iH z)Dkm^s2&aoWHTbocsMZ3lCj{nA|c9mzItvawn|^+j$z($jBw3yxMDfvT^x$s9C8Hc zQJD8nh^h&{l)NOWE|rp(Mb%}$Xg~r~v_&n(eo>DZAcJyiun`0UX#%ieulLP2y>H5T zG`YU;0@+@ozJvtv#wTF}_L^s4qx}6eb=P9Q)<4sqqCpaLdFD6GZAy24eanioBku%B zF58oHb`+dlGx`*r>P`h#o%Mx=leYu8Gq2>%p1Xaa;5;uE70s@kdF^MehS{t0Uz+<; z!L=dVe#f;XXW5c-Z7F)zu6TTTk1yL^@I15P*^&3`SU9{$FZL8X2Ua}C^Pb~N-0j`} z{lHHS_oNwz|F#L&Q@zD1UFV=rz zn`LKu@3w84Ii2dAWua+TOL|wPJ#%KEaoM>yrH7n`rcayvxgjPO9LqK00Fp zN#pRW3c6;sUF)6cojo&iGQI1@zU%uk6W0&qExr`J>S->toV?BE&JEsvqu{wXV*&YO z@-wUR+J%`5X>FRDeJe{ZTQ{dvt2OeVWs4g}(%qV7v*t{4;pk%1qB+-jbfxoDzVp=W zv$=tDh0a&=?qF_^g*bI=)zg|D&28Gd7+Chakm_A^HxViSYPLStwr%0evirHzF?b)F zXO>I%q`B;|<+`4f8gm-c>Wne{R(5^%t!yOMx<6lcYRL?dRO-GKnhB*nnT~W__Rw-& zmr^I!x@*y~T(>W!#&K|5>znDD9exiMQBhh=>F$E174B^Il!4%rPjk-zi|+XUhKL02 zO}N~o`$3Zey)7Duu%Ne@{C+UyOolOS@%twx17WFzs7Txys3PjlvQa+9ac|&_wWL-J zv!J;7fpX`R3Xm(eP*D;z5%Z27%sV>9#28q8EyW*1 z9V3``jKsWS0zPCG>q|R&U>wlsY$(DW24;Wh9vXG-p)xG$8Oi=LB0bIi!f`Ut!@%+18_IF=$gt zXhE*m-O_wqstJ_JFbYqjR4l4xJz=bdjYGC+LEDPG=&}AOnwOx+&(J>&TCEg$(Oe-f z(;)pDq32pzAA!~t%hO}6D|#8RX~d1d-GbgI@Z7i&I9ia%e-Ar!#phrSTm=cZH1OIqH7!^sS z9#I{N3ZnX@FmU7uL?m~I&q|nRg(XF@xCm2VcQyi@p9_E?lOsbhfp0^`V+vE__(T)- zsPxu#NM9Ni5X>Yvz?rRNlqG2l2PNKCb3-Yn7anN%1&BZbd)Q1F>ty50-S%fvnq@;< zaqVWjt|@xA;I*~b=EG~t|Eyn|QGeZ!wwTY{>oWrjeYXv{i)?OUa@lq8UOyyiyB^_Fk7qG{x(<#tw^)_^@{&vk#HX=|!()zy&M zu~?UT<+a>UaM?8i#0G`(q8Ev1h`F&tS?W6Jiepr>jbgCq>{7KB>L7Jj6bVl=GCRl~DY%Cn7N zKA>tDND@Z5b<#6a!h~wy6KJV1)E}w>(4tIM70U1@w=ZAGb0pPLjmLd-l!%R(3Q8$6 zcLiGGkm+z}q~|6v!VSk=#)!iRS0_g{LK0Khs?RL#rn8WSvl({653(<85wM2_IbgE9 zs0)mP8S|=W#k?>`z>xrVi6>7L&A69Q;v5IVruH&vnwyeqCkvL&jIgrt`TWM`7loCB z1Nnmkx$}WsSb+MC?NGnbl(Inm=8akRciL{YEm{h`WBI0Isk*zCW|U*Bvzqz3xw^D) z*z-#}UIMW?&y zY$-bHi;jjO-qw@f|IG*;trzoGFS6z-#-AWmZjA3AY7uMRjnvTT}yE$5ap34S5{Sy-zRg^C)J-kkSI% zRuK&dXC#CY3q&3uZO=;PX8~5Mu(~Xe@VJ11)m5G{cslNQdNOaNEX8Kuo#x%y*HSk5 zbm6t^wt{Of;XJN2U*CrbxyYXl7hF;f966krJCPO&?)53nSB+$`4Wp4p4mkB>`jOE- zktr#%B(d5deN`yKEum1RvP@P*QOOufL{$_|YXgi!(&TBlZ$JwiqM0=6^N_l!Dr2!^ zH1Ss8q8SIjgo2*{HQx&npdi2>(JJ3NDQQ!W+@PKnc<X6Rp7SD{Kms|-y}8^HSt zuXwV6XXB+ja>YE!~#srwl`Ix`4Y=T*|47vp`DBuIPfR{R<;4iB~?O0gmW$NMdEOih=x#PM4~r= z#6LVLrOhZ~kb9PwSfB1lEPAK`5rM0v2G;?$#YmHPuMCVG8s z6mylAiUMZ;3F`3IAbJ$cR@Zi9dcXCh@7CwnK0CMj`+ZBJh5e^fhSm1Y%vhnl`@84z z?R)2b>qo}hn+q?TOPRoEm7(X}%8V^IK8%3DYSFOd_?~$wo_l5B2g&^Iv-yT|DRZ&W z3&3YBHUql7Z!5NJK>5G%v(iE9Xx6w0^cR=iuh#}YoDi~XQ;-1gqsQP{9WDY@Qk7IE zHLxiDQHPrHLapVj1a=Hs=8e*U$4zxVcMv*3R>e&Xv7M4B1KVO4bUhF$jRLr*@#8U` z1y>=oCvrsOqz{Yq5s?O`pYuGfRf*|Tz@D>Wwo2%C8|e7wAp-QPZ z`~6SD!Ea+b82t(Sc-&-wgFn@*`&?txzCr)8K`lcc($t2&-B3~ueMYMC-xmU*r956c zj&dL1V+;_<0n=pp{2NGAiB6#IREbV#&p^bfu;>sQhm?oykU+2n7bXb;%JwAGRJ}mS z0(wnC6I9DrQ20uYtKWYwjPZD#`_kkaV_A5<#AyN(Uwd&!r~M0 z0;puX0EexoAO;bal<9_MOMz<#G{Waq1wf0E)!-k7CL9Xb+%1S?#7C&$zJn2pd?J08 z`vh00*=2y`d)|tI2xO7=&$Q%tTJ%S9;$#2fC#hg+WvR<7wwLD z9Ap@ra1^Y9pLlvfKJ@@aR&3iZ^5{7&iQv?qYP0n)v9`8pWs#MKT ziRCc*Z&-lxl>`+q|DcPlX)QXOpoam#pdmw?J*tD&c-gX~&zz&>f)|w8a2Xb}opBmOZ&Ny((;O_xW;nTHj zS+k|NKL>j5n-D#&!`kFwEKL)9Ekf~V9|b_RxmIlLc^i2A6>RHshV`UJKJYyP#2NYK zbqwD-2(veY%@1S5V1&T=1&S$ISr|QlACI5|DEqm_sqOgXCR*D`9>QVyBNhfdWT@fy zU>2fzDg?|Cm||%|LY5L__&Z9yzMZ0oM;8d)aL%Q^OmXYLj}BX@3XrR*%7R--r-L&w zF!KhLEscKupoyvX1I7D28t}13)Q!oZ_2Ktv@X(8CceJ`z6)5Lk{|hC(YYfOC)lzXF?My0>U= zUa@=gcJE)<*Ue~Kq-#x_JlUx8)S5z~b%Dl(4xs4Bg^rrs zd-!R`rK6BKQoEL<*n_(}0FU(HN6ER9t|cwOa)93 zPENrlHbB1>)uiOm23!!_QYGh95(hPCjKMX*s03xLC6q&8d?s;;rK3F+Z}1zB*%Nz- zqpS3itWE4tRVd37NC6;XDVcC^hf8}M1=i!>xP^!4qW%)gj)OG?KFgVeZ+-?QBN4sJ`xbT(U{-`pL`Y6M- zk1ez>o=wLJZHMQ~pJCCqW&3t0T50P-@22iWCfC+gXge}zE*c%PXWngvp3TzlKD+Ad z%x!q3;2g+V2C7?zbLDTJy>a3Ch0N83_74+<)}4i>U9&pU>h^rwcKG7yIO%BHp&zy{ zU051jx^U;^Gj|5g<_@3Dx1F0c!+@nHKH8OS{Jn$uwLN)z&lmR%Dqx38P4_MamK@My z$aS{Pcg=NWnhMS>FxcjXoTfoSd_;LU7$AYW^1%UMx=%vPn&3 zSy3cqge_N`pG;#%v8xNYJ5xg=%)57B(Zfagts@=6|~#*g>DaxwwtaDtx|npDCd zDEfiz8+im6Llo`6%}E-v>eX=NPXT(3&hkvFixQ!G-So~KCKiT!(? zV$4ZxbW6g@*a`pKP^Ke@YfIRmjB1wZkaxnqmRlfH_JqBHinJ_IB^@BrAxTwGxcnoo z;>yr(MhGYZdO4Nh+=uFg)wLe~u zaWQUXB1?mdzYwVA=5ndLGdS8=1 zCVs|S`BCxfpz5k8xp3TGEmaf4P+VUUuHl6C@(8#}{}qf`y#9zJ%mZg6sxM)VUU)_Z^S7o{I*C!sO5k*&ERf%GKjrE^ug(cW{`GPTnBQP>*pq|0khNZMI0M4%H z7YpAV@PW%fBi(550%nl_IQsA?0n1kHs;eXi*~py8U1J#z{-gu72Y!4676<5W@ZIys z%*aaJrhMI|oYj}}y_h%m<}|%IbMLC7VZLduX~nT4@7VEu%hJG)&;Q{3$}xW){_lP> z=h#tj1g1~i)!TFWH9xIyOUDcKn^UH{&h^08_0F8WEwej2@w?Bjn(JpTeY9iQyb+Gg z(p{;Oe+k}nr@_x4+m~gK(RyO#!YFTa*Qb}=_Jvb=e8)|B3tw{Jw@ zsBvpHmfLzDci>{~bvAb~m7`uHD=E|L!OVrct1EBV1_yx||H74g=Yf3l!IZ7I{os;i z$(q~VPl(RvUC)5O`dwEm^up1w;%Lu1+6#`3=@YAV*X)V;({rZ_?#_aJ!*n0G+RqMs zeIpE+r$h3c{|nC+aCFzC*rL5Yr7fD>X-(d|rU+j9;N6okW_t>*uGCR5b^ac6XyOADQUyycTE3#~cl!A~tO+;>oo&pdQeT2pGfte(a&P=wN)5ACbhoA7%DJ}`o7!e|WTJL0#B#fi=Z@q2tl0TGcK-j~*CXk# ziC^{)4I5tEsrvCwWA7U6kDqtl?fyAs{yAm&8CCyR)cT)M?LVU$e@3kb4>j8hs{1Oc{zcUTm!-kh=+A+(a``beb(+}TN_0W#=;TwI|`yNt|d^kosXh&+@HQ$Wy zAqB~Yn;@EX;wCQLuY>y@{ATze1<8jeXb;_)8bbg7hoBsFKG>^bWd% Hyx9K-A<$>p literal 0 HcmV?d00001 diff --git a/hunyuanvideo_foley/models/synchformer/ast_model.py b/hunyuanvideo_foley/models/synchformer/ast_model.py new file mode 100644 index 0000000..9281393 --- /dev/null +++ b/hunyuanvideo_foley/models/synchformer/ast_model.py @@ -0,0 +1,289 @@ +import logging + +import torch +from transformers.modeling_outputs import BaseModelOutputWithPooling + +from .modeling_ast import ASTForAudioClassification, ASTConfig +from .motionformer import AveragePooling, BaseEncoderLayer, TemporalTransformerEncoderLayer +from .utils import check_if_file_exists_else_download + + +class AST(torch.nn.Module): + def __init__( + self, + extract_features: bool = False, + ckpt_path: str = None, + feat_type: str = None, + max_spec_t: int = None, + factorize_freq_time: bool = None, + agg_freq_module: str = None, + agg_time_module: str = None, + add_global_repr: bool = True, + agg_segments_module: str = None, + max_segments: int = None, + ) -> None: + """ + extract_features: if True, then the model will return the features instead of head's output + ckpt_path: is not a path to a ckpt file, but a name of a model from the HuggingFace model hub. + feat_type: if extract_features is True, this parameter specifies the type of features to return + max_spec_t: if specified, then the model (pos emb) will be patched to support this length of spec + factorize_freq_time: if True, then the model will use a factorized freq/time aggregation + agg_freq_module: if specified, then the model will use this module for freq aggregation + agg_time_module: if specified, then the model will use this module for time aggregation + add_global_repr: if True, adds a global representation to the features (aggregation on segments) + agg_segments_module: if specified, then the model will use this module for segments aggregation + max_segments: if specified, the initialization of PE in the global agg module will use this value. + This should correspond to the max number of segments per video (if None, 16 is used) + """ + super().__init__() + self.extract_features = extract_features + self.ckpt_path = ckpt_path + self.max_spec_t = max_spec_t + self.max_segments = max_segments + + # depending on whether the feat extractor was pre-trained contrastively or not, we need to + # load the state dict differently. + + # if ckpt is specified, then load the model from the HuggingFace model hub, otherwise init a new model + if ckpt_path == "MIT/ast-finetuned-audioset-10-10-0.4593": + revision = "c1c0c66" # fixing the revision for compatibility (V4.27.4) + self.config = ASTConfig.from_pretrained(ckpt_path, revision=revision) + full_model = ASTForAudioClassification.from_pretrained(ckpt_path, revision=revision) + logging.info(f"Loaded AST from {ckpt_path}") + else: + self.config = ASTConfig() + self.config.num_labels = 527 # 2 by default, audioset has 527 labels + full_model = ASTForAudioClassification(self.config) + logging.info("Initialized AST from scratch with the AST AudioSet config") + + was_pt_on_avclip = ckpt_path is not None and ckpt_path.endswith(".pt") + + # feature extractor + self.ast = full_model.audio_spectrogram_transformer + + if self.extract_features: + # assign `feat_type` (use default if not specified) + self.feat_type = "last_hidden_state" if feat_type is None else feat_type + # define adapters if needed + self.factorize_freq_time = factorize_freq_time + # avoiding code duplication (used only if agg_*_module is TransformerEncoderLayer) + transf_enc_layer_kwargs = dict( + d_model=self.config.hidden_size, + nhead=self.config.num_attention_heads, + dim_feedforward=self.config.intermediate_size, + activation=torch.nn.GELU(), + batch_first=True, + dropout=self.config.attention_probs_dropout_prob, + layer_norm_eps=1e-6, + norm_first=True, + ) + if factorize_freq_time: + self.feat_type = "last_hidden_state" # this feat_type supports factorization + # frequency aggreration + if agg_freq_module == "TransformerEncoderLayer": + self.freq_attn_agg = FrequencyTransformerEncoderLayer(**transf_enc_layer_kwargs) + elif agg_freq_module == "AveragePooling": + self.freq_attn_agg = AveragePooling( + avg_pattern="BS D f t -> BS D t", then_permute_pattern="BS D t -> BS t D" + ) + # time aggreration + if agg_time_module == "TransformerEncoderLayer": + self.temp_attn_agg = TemporalTransformerEncoderLayer(**transf_enc_layer_kwargs) + elif agg_time_module == "AveragePooling": + self.temp_attn_agg = AveragePooling(avg_pattern="BS t D -> BS D") + elif "Identity" in agg_time_module: + self.temp_attn_agg = torch.nn.Identity() + # define a global aggregation layer (aggregarate over segments) + self.add_global_repr = add_global_repr + if add_global_repr: + if agg_segments_module == "TransformerEncoderLayer": + # we can reuse the same layer as for temporal factorization (B, dim_to_agg, D) -> (B, D) + # we need to add pos emb (PE) because previously we added the same PE for each segment + pos_max_len = max_segments if max_segments is not None else 16 # 16 = 10sec//0.64sec + 1 + self.global_attn_agg = TemporalTransformerEncoderLayer( + add_pos_emb=True, + pos_emb_drop=self.config.hidden_dropout_prob, + pos_max_len=pos_max_len, + **transf_enc_layer_kwargs, + ) + elif agg_segments_module == "AveragePooling": + self.global_attn_agg = AveragePooling(avg_pattern="B S D -> B D") + else: + self.classifier = full_model.classifier + + # AST.device fails with AttributeError. This is a workaround + self.device = full_model.device + + # pre-trained on 12*101+2=1214 tokens, but we have less (e.g. 12*6+2=74) + self.patch_position_emb() + + if was_pt_on_avclip: + # we need to filter out the state_dict of the AVCLIP model (has both A and V extractors) + # and keep only the state_dict of the feat extractor + check_if_file_exists_else_download(self.ckpt_path) + ckpt = torch.load(ckpt_path, map_location="cpu") + ckpt_weights = dict() + for k, v in ckpt["state_dict"].items(): + if k.startswith(("module.a_encoder.", "a_encoder.")): + k = k.replace("module.", "").replace("a_encoder.", "") + ckpt_weights[k] = v + _load_status = self.load_state_dict(ckpt_weights, strict=False) + if len(_load_status.missing_keys) > 0 or len(_load_status.unexpected_keys) > 0: + logging.warning( + f"Loading exact afeat_extractor ckpt from {self.ckpt_path} failed. \n" + f"Missing keys ({len(_load_status.missing_keys)}): " + f"{_load_status.missing_keys}, \n" + f"Unexpected keys ({len(_load_status.unexpected_keys)}): " + f"{_load_status.unexpected_keys} \n" + f"temp_attn_agg are expected to be missing if ckpt was pt contrastively." + ) + else: + logging.info(f"Loading afeat_extractor ckpt from {self.ckpt_path} succeeded.") + + # print the number of parameters + logging.info(f"AST: {sum(p.numel() for p in self.parameters() if p.requires_grad):,}") + + def forward( + self, x: torch.Tensor, for_loop: bool = False, cont_mask: torch.Tensor = None, **ast_kwargs + ) -> torch.Tensor: + """ + x: (B, S, T, F) where S is number of segments, F is number of (mel) frequency bins, + ast_kwargs: additional arguments for the AST model + cont_mask: (B, S, T, F) where 0s are the values to be masked out + if `for_loop=True`, we use a for loop to extract features for each segment separately. + if `for_loop=False`, we extract features for all segments at once. + Using the for loop is slower but more memory efficient, while using all segments at once + is faster but more memory inefficient. + Using for loop allows to control the memory footprint by varying the number of videos in a + batch (batch size) rather than the number of segments in a video. + """ + B, S, T, F = x.shape + + if for_loop: + assert cont_mask is None, "cont_mask is not supported with for_loop=True" + orig_shape_s = (B, 1, T, F) + # NOTE: since x is (B, S, T, F), and forward_segments expects (BS, T, F). + # (B, S, T, F)[:, s] is (B, T, F) or (BS, T, F) if S=1. + x = torch.cat( + [self.forward_segments(x[:, s], orig_shape_s, **ast_kwargs).unsqueeze(1) for s in range(S)], dim=1 + ) + else: + orig_shape = (B, S, T, F) + x = x.view(B * S, T, F) + if cont_mask is not None: + cont_mask = cont_mask.reshape(B * S, T, F) + # AST expects a tensor of shape (B*S, T, F). + x = self.forward_segments(x, orig_shape=orig_shape, cont_mask=cont_mask, **ast_kwargs) + # unpack the segments (using rest dimensions to support different shapes e.g. (BS, D) or (BS, t, D)) + x = x.view(B, S, *x.shape[1:]) + # x now is of shape (B, S, D) or (B, S, t, D) if `self.temp_attn_agg` is `Identity` + + global_x = None + if self.extract_features and self.add_global_repr: # lazy execution, throws AttributeError + assert len(x.shape) == 3, f"Local representation should be (B, S, D) {x.shape}" + global_x = self.global_attn_agg(x) # (B, D) + + return x, global_x # x is (B, S, ...), global_x is (B, D) or None + + def forward_segments(self, x, orig_shape: tuple, cont_mask: torch.Tensor = None, **ast_kwargs): + """x is (BS, T, F), where S is the number of segments; cont_mask is (BS, T, F): 0s to be masked out""" + # 'pooler_output': (B, D); or 'last_hidden_state: (B, T, D) where T is [CLS, DISTILL, ] + # x_mask is (B, T) where 0s are the values to be masked out + x, x_mask = self.ast(x, cont_mask=cont_mask, **ast_kwargs) + + if self.extract_features: + x = self.get_features_by_type(x) + if self.factorize_freq_time: + x = self.restore_freq_temp_dims(x, orig_shape) # (BS, D, f, t) <- (B*S, T, D) + if cont_mask is not None: + # duplicating the mask for the latent dimension (D) to be compatible with the next func + x_mask = x_mask.unsqueeze(-1).expand(-1, -1, self.config.hidden_size) + x_mask = self.restore_freq_temp_dims(x_mask, orig_shape) # (BS, D, f, t) <- (B*S, T, D) + # again removing the latent + x_mask = x_mask[:, 0, :, :] + else: + x_mask = None + x = self.freq_attn_agg(x, x_mask) # (BS, t, D) + x = self.temp_attn_agg(x) # (BS, D) or (BS, t, D) if self.temp_attn_agg is Identity + else: + x = x["pooler_output"] + x = self.classifier(x) + return x + + def get_features_by_type(self, x: BaseModelOutputWithPooling) -> torch.Tensor: + if self.feat_type == "pooler_output": + return x["pooler_output"] # (B, D) + elif self.feat_type == "CLS": + return x["last_hidden_state"][:, 0, :] # (B, D) + elif self.feat_type == "last_hidden_state": + return x["last_hidden_state"] # (B, 2+T, D) + elif self.feat_type == "last_hidden_state_no_AUX": + return x["last_hidden_state"][:, 2:, :] # (B, T, D) removing CLS and distill tokens + else: + raise ValueError(f"Unknown feature type: {self.feat_type}") + + def restore_freq_temp_dims(self, feats, orig_shape: tuple): + """ + feats are of shape (B*S, T, D) + where T = 2 + f * t (if feat_type == 'last_hidden_state') + where T = f * t (if feat_type == 'last_hidden_state_no_AUX') + Our goal is to make them of shape (B*S, f, t, D) where f and t are dimensions after patching. + From `self.ast.embeddings.patch_embeddings`, it follows that we could reshape feats: + `feats.transpose(1, 2).view(B*S, D, f, t)` + + (Similar function is defined in for RGB features in `motionformer.py`) + """ + B, S, T, F = orig_shape + D = self.config.hidden_size + + # num patches in each dimension + f, t = self.ast.embeddings.get_shape(self.config) + + if self.feat_type == "last_hidden_state": + feats = feats[:, 2:, :] # removing CLS and distill tokens + + feats = feats.permute(0, 2, 1) # (B*S, D, T) + feats = feats.view(B * S, D, f, t) # (B*S, D, f, t) + + return feats + + def patch_position_emb(self): + if self.max_spec_t is not None: + self.config.max_length = self.max_spec_t + f, t = self.ast.embeddings.get_shape(self.config) + shortened = self.ast.embeddings.position_embeddings[:, : f * t + 2].clone() # +2 for CLS and distill tokens + self.ast.embeddings.position_embeddings = torch.nn.Parameter(shortened).to(self.device) + + def to(self, device): + """AST.device fails with AttributeError. This is a workaround.""" + self.device = torch.device(device) + return super().to(device) + + +class FrequencyTransformerEncoderLayer(BaseEncoderLayer): + """This layer is used to aggregate the features along the frequency axis. + It follows the same logic as spatio-temporal aggregation in visual feature extractor. + Thus, it is recommended to check the definition of `BaseEncoderLayer` in `motionformer.py`""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def forward(self, x: torch.Tensor, x_mask: torch.Tensor = None) -> torch.Tensor: + """x: (B*S, D, f, t); if specified x_mask (B*S, f, t), 0s are the values to be masked out""" + BS, D, f, t = x.shape + + # time as a batch dimension + x = x.permute(0, 3, 2, 1) # (B*S, t, f, D) + x = x.reshape(BS * t, f, D) # .view() fails with non-contiguous memory + # similar to mask + if x_mask is not None: + x_mask = x_mask.permute(0, 2, 1) # (B*S, t, f) + x_mask = x_mask.reshape(BS * t, f) + + # apply encoder layer (BaseEncoderLayer.forward) - it will add CLS token and output its representation + x = super().forward(x=x, x_mask=x_mask) # (B*S*t, D) + + # reshape back to (B*S, t, D) + x = x.view(BS, t, D) + + return x # (B*S, t, D) diff --git a/hunyuanvideo_foley/models/synchformer/compute_desync_score.py b/hunyuanvideo_foley/models/synchformer/compute_desync_score.py new file mode 100644 index 0000000..89c739b --- /dev/null +++ b/hunyuanvideo_foley/models/synchformer/compute_desync_score.py @@ -0,0 +1,214 @@ +import argparse +import subprocess +from pathlib import Path + +import torch +import torchaudio +import torchvision +from omegaconf import OmegaConf + +import data_transforms +from .synchformer import Synchformer +from .data_transforms import make_class_grid, quantize_offset +from .utils import check_if_file_exists_else_download, which_ffmpeg + + +def prepare_inputs(batch, device): + aud = batch["audio"].to(device) + vid = batch["video"].to(device) + + return aud, vid + + +def get_test_transforms(): + ts = [ + data_transforms.EqualifyFromRight(), + data_transforms.RGBSpatialCrop(input_size=224, is_random=False), + data_transforms.TemporalCropAndOffset( + crop_len_sec=5, + max_off_sec=2, # https://a3s.fi/swift/v1/AUTH_a235c0f452d648828f745589cde1219a/sync/sync_models/24-01-04T16-39-21/cfg-24-01-04T16-39-21.yaml + max_wiggle_sec=0.0, + do_offset=True, + offset_type="grid", + prob_oos="null", + grid_size=21, + segment_size_vframes=16, + n_segments=14, + step_size_seg=0.5, + vfps=25, + ), + data_transforms.GenerateMultipleSegments( + segment_size_vframes=16, + n_segments=14, + is_start_random=False, + step_size_seg=0.5, + ), + data_transforms.RGBToHalfToZeroOne(), + data_transforms.RGBNormalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]), # motionformer normalization + data_transforms.AudioMelSpectrogram( + sample_rate=16000, + win_length=400, # 25 ms * 16 kHz + hop_length=160, # 10 ms * 16 kHz + n_fft=1024, # 2^(ceil(log2(window_size * sampling_rate))) + n_mels=128, # as in AST + ), + data_transforms.AudioLog(), + data_transforms.PadOrTruncate(max_spec_t=66), + data_transforms.AudioNormalizeAST(mean=-4.2677393, std=4.5689974), # AST, pre-trained on AudioSet + data_transforms.PermuteStreams( + einops_order_audio="S F T -> S 1 F T", einops_order_rgb="S T C H W -> S T C H W" # same + ), + ] + transforms = torchvision.transforms.Compose(ts) + + return transforms + + +def get_video_and_audio(path, get_meta=False, start_sec=0, end_sec=None): + orig_path = path + # (Tv, 3, H, W) [0, 255, uint8]; (Ca, Ta) + rgb, audio, meta = torchvision.io.read_video(str(path), start_sec, end_sec, "sec", output_format="TCHW") + assert meta["video_fps"], f"No video fps for {orig_path}" + # (Ta) <- (Ca, Ta) + audio = audio.mean(dim=0) + # FIXME: this is legacy format of `meta` as it used to be loaded by VideoReader. + meta = { + "video": {"fps": [meta["video_fps"]]}, + "audio": {"framerate": [meta["audio_fps"]]}, + } + return rgb, audio, meta + + +def reencode_video(path, vfps=25, afps=16000, in_size=256): + assert which_ffmpeg() != "", "Is ffmpeg installed? Check if the conda environment is activated." + new_path = Path.cwd() / "vis" / f"{Path(path).stem}_{vfps}fps_{in_size}side_{afps}hz.mp4" + new_path.parent.mkdir(exist_ok=True) + new_path = str(new_path) + cmd = f"{which_ffmpeg()}" + # no info/error printing + cmd += " -hide_banner -loglevel panic" + cmd += f" -y -i {path}" + # 1) change fps, 2) resize: min(H,W)=MIN_SIDE (vertical vids are supported), 3) change audio framerate + cmd += f" -vf fps={vfps},scale=iw*{in_size}/'min(iw,ih)':ih*{in_size}/'min(iw,ih)',crop='trunc(iw/2)'*2:'trunc(ih/2)'*2" + cmd += f" -ar {afps}" + cmd += f" {new_path}" + subprocess.call(cmd.split()) + cmd = f"{which_ffmpeg()}" + cmd += " -hide_banner -loglevel panic" + cmd += f" -y -i {new_path}" + cmd += f" -acodec pcm_s16le -ac 1" + cmd += f' {new_path.replace(".mp4", ".wav")}' + subprocess.call(cmd.split()) + return new_path + + +def decode_single_video_prediction(off_logits, grid, item): + label = item["targets"]["offset_label"].item() + print("Ground Truth offset (sec):", f"{label:.2f} ({quantize_offset(grid, label)[-1].item()})") + print() + print("Prediction Results:") + off_probs = torch.softmax(off_logits, dim=-1) + k = min(off_probs.shape[-1], 5) + topk_logits, topk_preds = torch.topk(off_logits, k) + # remove batch dimension + assert len(topk_logits) == 1, "batch is larger than 1" + topk_logits = topk_logits[0] + topk_preds = topk_preds[0] + off_logits = off_logits[0] + off_probs = off_probs[0] + for target_hat in topk_preds: + print(f'p={off_probs[target_hat]:.4f} ({off_logits[target_hat]:.4f}), "{grid[target_hat]:.2f}" ({target_hat})') + return off_probs + + +def main(args): + vfps = 25 + afps = 16000 + in_size = 256 + # making the offset class grid similar to the one used in transforms, + # refer to the used one: https://a3s.fi/swift/v1/AUTH_a235c0f452d648828f745589cde1219a/sync/sync_models/24-01-04T16-39-21/cfg-24-01-04T16-39-21.yaml + max_off_sec = 2 + num_cls = 21 + + # checking if the provided video has the correct frame rates + print(f"Using video: {args.vid_path}") + v, _, info = torchvision.io.read_video(args.vid_path, pts_unit="sec") + _, H, W, _ = v.shape + if info["video_fps"] != vfps or info["audio_fps"] != afps or min(H, W) != in_size: + print(f'Reencoding. vfps: {info["video_fps"]} -> {vfps};', end=" ") + print(f'afps: {info["audio_fps"]} -> {afps};', end=" ") + print(f"{(H, W)} -> min(H, W)={in_size}") + args.vid_path = reencode_video(args.vid_path, vfps, afps, in_size) + else: + print(f'Skipping reencoding. vfps: {info["video_fps"]}; afps: {info["audio_fps"]}; min(H, W)={in_size}') + + device = torch.device(args.device) + + # load visual and audio streams + # rgb: (Tv, 3, H, W) in [0, 225], audio: (Ta,) in [-1, 1] + rgb, audio, meta = get_video_and_audio(args.vid_path, get_meta=True) + + # making an item (dict) to apply transformations + # NOTE: here is how it works: + # For instance, if the model is trained on 5sec clips, the provided video is 9sec, and `v_start_i_sec=1.3` + # the transform will crop out a 5sec-clip from 1.3 to 6.3 seconds and shift the start of the audio + # track by `args.offset_sec` seconds. It means that if `offset_sec` > 0, the audio will + # start by `offset_sec` earlier than the rgb track. + # It is a good idea to use something in [-`max_off_sec`, `max_off_sec`] (-2, +2) seconds (see `grid`) + item = dict( + video=rgb, + audio=audio, + meta=meta, + path=args.vid_path, + split="test", + targets={ + "v_start_i_sec": args.v_start_i_sec, + "offset_sec": args.offset_sec, + }, + ) + + grid = make_class_grid(-max_off_sec, max_off_sec, num_cls) + if not (min(grid) <= item["targets"]["offset_sec"] <= max(grid)): + print(f'WARNING: offset_sec={item["targets"]["offset_sec"]} is outside the trained grid: {grid}') + + # applying the test-time transform + item = get_test_transforms()(item) + + # prepare inputs for inference + batch = torch.utils.data.default_collate([item]) + aud, vid = prepare_inputs(batch, device) + + # TODO: + # sanity check: we will take the input to the `model` and recontruct make a video from it. + # Use this check to make sure the input makes sense (audio should be ok but shifted as you specified) + # reconstruct_video_from_input(aud, vid, batch['meta'], args.vid_path, args.v_start_i_sec, args.offset_sec, + # vfps, afps) + + # forward pass + with torch.set_grad_enabled(False): + with torch.autocast("cuda", enabled=True): + _, logits = synchformer(vid, aud) + + # simply prints the results of the prediction + decode_single_video_prediction(logits, grid, item) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--exp_name", required=True, help="In a format: xx-xx-xxTxx-xx-xx") + parser.add_argument("--vid_path", required=True, help="A path to .mp4 video") + parser.add_argument("--offset_sec", type=float, default=0.0) + parser.add_argument("--v_start_i_sec", type=float, default=0.0) + parser.add_argument("--device", default="cuda:0") + args = parser.parse_args() + + synchformer = Synchformer().cuda().eval() + synchformer.load_state_dict( + torch.load( + os.environ.get("SYNCHFORMER_WEIGHTS", f"weights/synchformer.pth"), + weights_only=True, + map_location="cpu", + ) + ) + + main(args) diff --git a/hunyuanvideo_foley/models/synchformer/data_transforms.py b/hunyuanvideo_foley/models/synchformer/data_transforms.py new file mode 100644 index 0000000..b43ddb2 --- /dev/null +++ b/hunyuanvideo_foley/models/synchformer/data_transforms.py @@ -0,0 +1,1130 @@ +import logging +import math +import random +from typing import Tuple +import torch +import torchvision +import torchaudio +import numpy as np +import einops + + +def sec2frames(sec, fps): + return int(sec * fps) + + +def frames2sec(frames, fps): + return frames / fps + + +class EqualifyFromRight(torch.nn.Module): + + def __init__(self, clip_max_len_sec=10): + """ + Takes the dataset item and makes sure more streams are of an equal size in terms of fps. + It, however, assumes that the signal is synched and trims the ending parts ('from the right'). + """ + super().__init__() + self.clip_max_len_sec = clip_max_len_sec + + def forward(self, item): + """ + `item`: {'video': (Tv, C, H, W), 'audio': (Ta,), + 'meta': { + 'audio': {'framerate': [float], 'duration': [float]} + 'video': {'fps': [float], 'duration': [float]}} + """ + a_fps = item["meta"]["audio"]["framerate"][0] + v_fps = item["meta"]["video"]["fps"][0] + + Ta = item["audio"].shape[0] + Tv, C, H, W = item["video"].shape + + a_len_secs = Ta / a_fps + v_len_secs = Tv / v_fps + min_len = min(self.clip_max_len_sec, a_len_secs, v_len_secs) + + a_frames_per_v_frame = a_fps // v_fps + v_len_frames = int(v_fps * min_len) + a_len_frames = int(a_frames_per_v_frame * v_len_frames) + # print(a_len_frames, v_len_frames) + + assert a_len_frames <= Ta and v_len_frames <= Tv + + item["audio"] = item["audio"][:a_len_frames] + item["video"] = item["video"][:v_len_frames, :, :, :] + + return item + + +class RGBSpatialCrop(torch.nn.Module): + + def __init__(self, input_size, is_random): + super().__init__() + assert input_size is not None, f"smaller_input_size is `{input_size}`" + if isinstance(input_size, int): + input_size = (input_size, input_size) + self.input_size = input_size + self.is_random = is_random + + @staticmethod + def get_random_crop_sides(vid, output_size): + """Slice parameters for random crop""" + h, w = vid.shape[-2:] + th, tw = output_size + if w == tw and h == th: + return 0, 0, h, w + i = random.randint(0, h - th) + j = random.randint(0, w - tw) + return i, j, th, tw + + @staticmethod + def get_center_crop_sides(vid, output_size): + """Slice parameters for center crop""" + h, w = vid.shape[-2:] + th, tw = output_size + + i = int(round((h - th) / 2.0)) + j = int(round((w - tw) / 2.0)) + return i, j, th, tw + + def forward(self, item): + # (Tv, C, H, W) + vid = item["video"] + if self.is_random: + i, j, h, w = self.get_random_crop_sides(vid, self.input_size) + else: + i, j, h, w = self.get_center_crop_sides(vid, self.input_size) + item["video"] = vid[..., i : (i + h), j : (j + w)] + return item + + +class Resize(torchvision.transforms.Resize): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def forward(self, item): + item["video"] = super().forward(item["video"]) + return item + + +class RGBSpatialCropSometimesUpscale(torch.nn.Module): + """This (randomly) crops the input video and with prob `sometimes_p` this crop is smaller but upscaled + to `target_input_size`""" + + def __init__(self, sometimes_p, target_input_size, is_random, smaller_input_size=None): + super().__init__() + self.sometimes_p = sometimes_p + self.do_sometimes_upscale = sometimes_p is not None and sometimes_p > 0 + + self.crop_only = RGBSpatialCrop(target_input_size, is_random) + + if self.do_sometimes_upscale: + self.crop_further_and_upscale = torchvision.transforms.Compose( + [ + RGBSpatialCrop(smaller_input_size, is_random), + Resize(target_input_size, antialias=None), + ] + ) + + def forward(self, item): + assert len(item["video"].shape) == 4, ( + f"{item['video'].shape}: if it is applied after GenerateMultipleClips," + "augs should be applied to each clip separately, not to the whole video array. " + "Otherwise, ignore this warning (comment it)." + ) + if self.do_sometimes_upscale and self.sometimes_p > torch.rand(1): + return self.crop_further_and_upscale(item) + else: + return self.crop_only(item) + + +class RandomApplyColorDistortion(torch.nn.Module): + + def __init__(self, p_gray_scale=0.0, p_color_jitter=0.0, s=1.0) -> None: + super().__init__() + self.p_gray_scale = p_gray_scale + self.p_color_jitter = p_color_jitter + self.s = s + assert 0 <= self.p_color_jitter <= 1 and 0 <= self.p_gray_scale <= 1, (p_color_jitter, p_gray_scale) + # SimCLR params + color_jitter = torchvision.transforms.ColorJitter(0.8 * s, 0.8 * s, 0.8 * s, 0.2 * s) + rand_color_jitter = torchvision.transforms.RandomApply([color_jitter], p_color_jitter) + rand_gray = torchvision.transforms.RandomGrayscale(p_gray_scale) + self.transforms = torchvision.transforms.Compose([rand_color_jitter, rand_gray]) + + def apply_to_single_clip(self, clip): + return self.transforms(clip) + + def apply_to_each_clip(self, clips): + for i, clip in enumerate(clips): + clips[i] = self.apply_to_single_clip(clip) + return clips + + def forward(self, item): + has_batch_dim = len(item["video"].shape) == 5 + if has_batch_dim: + fn = self.apply_to_each_clip + else: + fn = self.apply_to_single_clip + item["video"] = fn(item["video"]) + return item + + +class ApplyColorJitterFrameWise(torch.nn.Module): + + def __init__(self, s=1.0) -> None: + super().__init__() + self.s = s + # SimCLR params + self.transform = torchvision.transforms.ColorJitter(0.8 * s, 0.8 * s, 0.8 * s, 0.2 * s) + + def apply_to_single_clip(self, clip): + for i, frame in enumerate(clip): + clip[i] = self.transform(frame) + return clip + + def apply_to_each_clip(self, clips): + for i, clip in enumerate(clips): + clips[i] = self.apply_to_single_clip(clip) + return clips + + def forward(self, item): + has_batch_dim = len(item["video"].shape) == 5 + if has_batch_dim: + fn = self.apply_to_each_clip + else: + fn = self.apply_to_single_clip + item["video"] = fn(item["video"]) + return item + + +class RandomHorizontalFlip(torchvision.transforms.RandomHorizontalFlip): + + def __init__(self, p=0.5): + super().__init__(p) + + def apply_to_single_clip(self, clip): + return super().forward(clip) + + def apply_to_each_clip(self, clips): + for i, clip in enumerate(clips): + clips[i] = self.apply_to_single_clip(clip) + return clips + + def forward(self, item): + has_batch_dim = len(item["video"].shape) == 5 + if has_batch_dim: + fn = self.apply_to_each_clip + else: + fn = self.apply_to_single_clip + item["video"] = fn(item["video"]) + return item + + +def make_class_grid( + leftmost_val, + rightmost_val, + grid_size, + add_extreme_offset: bool = False, + seg_size_vframes: int = None, + nseg: int = None, + step_size_seg: float = None, + vfps: float = None, +): + assert grid_size >= 3, f"grid_size: {grid_size} doesnot make sense. If =2 -> (-1,1); =1 -> (-1); =0 -> ()" + grid = torch.from_numpy(np.linspace(leftmost_val, rightmost_val, grid_size)).float() + if add_extreme_offset: + assert all([seg_size_vframes, nseg, step_size_seg]), f"{seg_size_vframes} {nseg} {step_size_seg}" + seg_size_sec = seg_size_vframes / vfps + trim_size_in_seg = nseg - (1 - step_size_seg) * (nseg - 1) + extreme_value = trim_size_in_seg * seg_size_sec + grid = torch.cat([grid, torch.tensor([extreme_value])]) # adding extreme offset to the class grid + return grid + + +def quantize_offset(grid: torch.Tensor, off_sec: float) -> Tuple[float, int]: + """Takes in the offset in seconds and snaps it onto the closest grid element. + Returns the grid value and its index.""" + closest_grid_el = (grid - off_sec).abs().argmin() + return grid[closest_grid_el], closest_grid_el + + +def apply_a_jitter(a_start_i, a_len_frames, a_crop_len_frames, a_fps, max_a_jitter_sec): + max_a_start_i = a_len_frames - a_crop_len_frames + max_a_jitter_i = sec2frames(max_a_jitter_sec, a_fps) + max_a_jitter_i_left = min(a_start_i, max_a_jitter_i) + max_a_jitter_i_right = min(max_a_start_i - a_start_i, max_a_jitter_i) + # jitter is U[left, right] + a_jitter_i = random.randint(-max_a_jitter_i_left, max_a_jitter_i_right) + # apply jitter + a_start_i = a_start_i + a_jitter_i + # making sure that any value from `a_start_i + U[left, right]` will be inside of [0, len-crop] region + assert 0 <= a_start_i <= max_a_start_i, f"{a_jitter_i} {max_a_jitter_i_left} {max_a_jitter_i_right} {max_a_start_i}" + return a_start_i, a_jitter_i + + +class TemporalCropAndOffset(torch.nn.Module): + + def __init__( + self, + crop_len_sec: float, + max_off_sec: float, + offset_type="grid", + do_offset: bool = True, + grid_size: int = None, + max_wiggle_sec: float = None, + add_doubt_cls: bool = False, + segment_size_vframes: int = None, + n_segments: int = None, + step_size_seg: float = None, + vfps: float = None, + prob_oos: float = None, + ): + super().__init__() + self.crop_len_sec = crop_len_sec + self.do_offset = do_offset + self.grid_size = grid_size + self.offset_type = offset_type + self.max_off_sec = max_off_sec + self.max_a_jitter_sec = max_wiggle_sec + if do_offset: + if offset_type == "grid": + self.class_grid = make_class_grid( + -max_off_sec, + max_off_sec, + grid_size, + add_doubt_cls, + segment_size_vframes, + n_segments, + step_size_seg, + vfps, + ) + logging.info(f"Offsets class grid: {self.class_grid}") + if self.max_a_jitter_sec is not None: + assert (max_wiggle_sec - 1e-6) <= ( + (self.class_grid[1] - self.class_grid[0]) / 2 + ), f"{self.class_grid}" + elif offset_type == "uniform": + self.off_dist = torch.distributions.uniform.Uniform(-max_off_sec, max_off_sec) + logging.info(f"Offset uniform distribution: {self.off_dist}") + elif offset_type == "uniform_binary": + self.itu_t_range = (-0.125, 0.045) + self.prob_oos = prob_oos + self.ins_dist = torch.distributions.uniform.Uniform(self.itu_t_range[0], self.itu_t_range[1]) + self.off_dist = torch.distributions.uniform.Uniform(-max_off_sec, max_off_sec) + else: + raise NotImplementedError(f"Unknown offset type: {offset_type}") + + def forward(self, item): + vid = item["video"] + aud = item["audio"] + v_len_frames, C, H, W = vid.shape + a_len_frames = aud.shape[0] + + v_fps = int(item["meta"]["video"]["fps"][0]) + a_fps = int(item["meta"]["audio"]["framerate"][0]) + + v_crop_len_frames = sec2frames(self.crop_len_sec, v_fps) + a_crop_len_frames = sec2frames(self.crop_len_sec, a_fps) + + if self.do_offset: + # trying to get the offset parameters (for instance during valid and test we have fixed offsets) + offset_sec = item["targets"].get("offset_sec", None) + v_start_i_sec = item["targets"].get("v_start_i_sec", None) + if "offset_target" in item["targets"]: + is_oos = item["targets"]["offset_target"].get("oos", None) + # train-time + if offset_sec is None and v_start_i_sec is None: + # aud starts `offset_sec` earlier than it should; aud has what will be shown after offset_sec + if self.offset_type == "grid": + offset_sec = random.choice(self.class_grid.tolist()) + elif self.offset_type == "uniform": + offset_sec = self.off_dist.sample().item() + elif self.offset_type == "uniform_binary": + # in-sync: Uniform(-0.125, 0.045) + # out-of-sync: Uniform(-5.5, 5.5) and resampled until not in Uniform(-0.125, 0.045) + # first, we sample if the offset is out-of-sync with prob_oss + is_oos = (torch.rand(1) < self.prob_oos).item() + if is_oos: + # second, we sample the offset itself (if in in-sync range, trying again) + offset_sec = self.off_dist.sample().item() + while self.itu_t_range[0] <= offset_sec <= self.itu_t_range[1]: + offset_sec = self.off_dist.sample().item() + else: + offset_sec = self.ins_dist.sample().item() + offset_sec = round(offset_sec, 2) + v_start_max_sec = frames2sec(v_len_frames - v_crop_len_frames, v_fps) + assert v_start_max_sec > 0, f'{v_len_frames} {v_crop_len_frames} {v_fps} @ {item["path"]}' + # `v_start_sec` IS NOT rounded to the fps grid + v_start_sec = random.uniform(max(0, -offset_sec), min(v_start_max_sec, v_start_max_sec - offset_sec)) + assert 0 <= v_start_sec <= v_start_max_sec, f'{v_start_sec} {v_start_max_sec} {item["path"]}' + v_start_i = sec2frames(v_start_sec, v_fps) + # `v_start_i_sec` IS rounded to the fps grid + v_start_i_sec = frames2sec(v_start_i, v_fps) + else: + offset_sec = round(offset_sec, 2) + v_start_i = sec2frames(v_start_i_sec, v_fps) + v_end_i = v_start_i + v_crop_len_frames + # `a_start_i` depends on the rounded value `v_start_i_sec`, otherwise + # (v_start_sec) we have ±0.1 jittering + a_start_i = sec2frames(v_start_i_sec + offset_sec, a_fps) + else: + offset_sec = 0.0 + is_random_crop = item["split"] == "train" + v_start_i, v_end_i = self.get_crop_idx(v_len_frames, v_crop_len_frames, is_random=is_random_crop) + v_start_i_sec = frames2sec(v_start_i, v_fps) + a_start_i = sec2frames(v_start_i_sec, a_fps) + + # sometimes due to the rounding error e.g. v_start_sec = 1.505 but sec2frames(1.505, 25) = 1.48 + # given offset is -1.5, the a_start_i will be a small negative value. (likely a_fps * 1/v_fps * 0.5) + if a_start_i < 0: + how_much_out = a_start_i + logging.info(f'a_start_i is negative ({how_much_out}) at {item["path"]}') + if abs(how_much_out) <= a_fps / v_fps: + logging.info("fixing it") + a_start_i += abs(how_much_out) + else: + raise Exception(f'{how_much_out} {item["path"]}') + + if self.max_a_jitter_sec is not None and self.max_a_jitter_sec > 0: + a_start_i, a_jitter_i = apply_a_jitter( + a_start_i, a_len_frames, a_crop_len_frames, a_fps, self.max_a_jitter_sec + ) + item["meta"]["a_jitter_i"] = a_jitter_i + + a_end_i = a_start_i + a_crop_len_frames + + assert v_start_i < v_end_i and a_start_i < a_end_i + assert aud.shape[0] >= a_end_i, f'{aud.shape} {a_end_i} {item["path"]}' + assert vid.shape[0] >= v_end_i, f'{vid.shape} {v_end_i} {item["path"]}' + + vid, aud = vid[v_start_i:v_end_i, :, :, :], aud[a_start_i:a_end_i] + + item["video"] = vid + item["audio"] = aud + + assert item["video"].shape[0] == v_fps * self.crop_len_sec, f'{item["video"].shape} {item["path"]}' + assert item["audio"].shape[0] == a_fps * self.crop_len_sec, f'{item["audio"].shape} {item["path"]}' + + # caching parameters + if self.do_offset: + if self.offset_type == "grid": + offset_label, offset_target = quantize_offset(self.class_grid, offset_sec) + elif self.offset_type == "uniform": + offset_label, offset_target = offset_sec, offset_sec + elif self.offset_type == "uniform_binary": + offset_label, offset_target = offset_sec, {"oos": is_oos, "offset": offset_sec} + item["targets"]["offset_sec"] = offset_sec + item["targets"]["v_start_i_sec"] = v_start_i_sec + item["targets"]["offset_label"] = offset_label + # assert 'offset_target' not in item['targets'], f'{item["targets"]}. What passed it there?' + item["targets"]["offset_target"] = offset_target + + return item + + def get_crop_idx(self, len_frames: int, crop_len_frames: int, is_random=True): + if len_frames == crop_len_frames: + return 0, len_frames + if is_random: + left_i = random.randint(0, len_frames - crop_len_frames) + else: + left_i = int(round((len_frames - crop_len_frames) / 2.0)) + return left_i, left_i + crop_len_frames + + +class GenerateMultipleSegments(torch.nn.Module): + """ + Given an item with video and audio, generates a batch of `n_segments` segments + of length `segment_size_vframes` (if None, the max number of segments will be made). + If `is_start_random` is True, the starting position of the 1st segment will be random but respecting + n_segments. + `audio_jitter_sec` is the amount of audio offset in seconds. + """ + + def __init__( + self, + segment_size_vframes: int, + n_segments: int = None, + is_start_random: bool = False, + audio_jitter_sec: float = 0.0, + step_size_seg: float = 1, + ): + super().__init__() + self.segment_size_vframes = segment_size_vframes + self.n_segments = n_segments + self.is_start_random = is_start_random + self.audio_jitter_sec = audio_jitter_sec + self.step_size_seg = step_size_seg + logging.info(f"Segment step size: {self.step_size_seg}") + + def forward(self, item): + v_len_frames, C, H, W = item["video"].shape + a_len_frames = item["audio"].shape[0] + + v_fps = int(item["meta"]["video"]["fps"][0]) + a_fps = int(item["meta"]["audio"]["framerate"][0]) + + ## Determining the number of segments + # segment size + segment_size_vframes = self.segment_size_vframes + segment_size_aframes = sec2frames(frames2sec(self.segment_size_vframes, v_fps), a_fps) + # step size (stride) + stride_vframes = int(self.step_size_seg * segment_size_vframes) + stride_aframes = int(self.step_size_seg * segment_size_aframes) + # calculating the number of segments. (W - F + 2P) / S + 1 + n_segments_max_v = math.floor((v_len_frames - segment_size_vframes) / stride_vframes) + 1 + n_segments_max_a = math.floor((a_len_frames - segment_size_aframes) / stride_aframes) + 1 + # making sure audio and video can accommodate the same number of segments + n_segments_max = min(n_segments_max_v, n_segments_max_a) + n_segments = n_segments_max if self.n_segments is None else self.n_segments + + assert n_segments <= n_segments_max, ( + f"cant make {n_segments} segs of len {self.segment_size_vframes} in a vid " + f'of len {v_len_frames} for {item["path"]}' + ) + + # (n_segments, 2) each + v_ranges, a_ranges = self.get_sequential_seg_ranges( + v_len_frames, a_len_frames, v_fps, a_fps, n_segments, segment_size_aframes + ) + + # segmenting original streams (n_segments, segment_size_frames, C, H, W) + item["video"] = torch.stack([item["video"][s:e] for s, e in v_ranges], dim=0) + item["audio"] = torch.stack([item["audio"][s:e] for s, e in a_ranges], dim=0) + return item + + def get_sequential_seg_ranges(self, v_len_frames, a_len_frames, v_fps, a_fps, n_seg, seg_size_aframes): + # if is_start_random is True, the starting position of the 1st segment will + # be random but respecting n_segments like so: "-CCCCCCCC---" (maybe with fixed overlap), + # else the segments are taken from the middle of the video respecting n_segments: "--CCCCCCCC--" + + seg_size_vframes = self.segment_size_vframes # for brevity + + # calculating the step size in frames + step_size_vframes = int(self.step_size_seg * seg_size_vframes) + step_size_aframes = int(self.step_size_seg * seg_size_aframes) + + # calculating the length of the sequence of segments (and in frames) + seg_seq_len = n_seg * self.step_size_seg + (1 - self.step_size_seg) + vframes_seg_seq_len = int(seg_seq_len * seg_size_vframes) + aframes_seg_seq_len = int(seg_seq_len * seg_size_aframes) + + # doing temporal crop + max_v_start_i = v_len_frames - vframes_seg_seq_len + if self.is_start_random: + v_start_i = random.randint(0, max_v_start_i) + else: + v_start_i = max_v_start_i // 2 + a_start_i = sec2frames(frames2sec(v_start_i, v_fps), a_fps) # vid frames -> seconds -> aud frames + + # make segments starts + v_start_seg_i = torch.tensor([v_start_i + i * step_size_vframes for i in range(n_seg)]).int() + a_start_seg_i = torch.tensor([a_start_i + i * step_size_aframes for i in range(n_seg)]).int() + + # apply jitter to audio + if self.audio_jitter_sec > 0: + jitter_aframes = sec2frames(self.audio_jitter_sec, a_fps) + # making sure after applying jitter, the audio is still within the audio boundaries + jitter_aframes = min(jitter_aframes, a_start_i, a_len_frames - a_start_i - aframes_seg_seq_len) + a_start_seg_i += random.randint(-jitter_aframes, jitter_aframes) # applying jitter to segments + + # make segment ends + v_ends_seg_i = v_start_seg_i + seg_size_vframes + a_ends_seg_i = a_start_seg_i + seg_size_aframes # using the adjusted a_start_seg_i (with jitter) + + # make ranges + v_ranges = torch.stack([v_start_seg_i, v_ends_seg_i], dim=1) + a_ranges = torch.stack([a_start_seg_i, a_ends_seg_i], dim=1) + assert (a_ranges >= 0).all() and (a_ranges <= a_len_frames).all(), f"{a_ranges} out of {a_len_frames}" + assert (v_ranges <= v_len_frames).all(), f"{v_ranges} out of {v_len_frames}" + return v_ranges, a_ranges + + +class TemporalCropAndOffsetForSyncabilityTraining(torch.nn.Module): + + def __init__( + self, + max_off_sec: float, + do_offset: bool = True, + grid_size: int = None, + max_wiggle_sec: float = None, + segment_size_vframes: int = None, + n_segments: int = None, + step_size_seg: float = None, + vfps: float = None, + ): + super().__init__() + seg_size_sec = segment_size_vframes / vfps + trim_size_in_seg = n_segments - (1 - step_size_seg) * (n_segments - 1) + self.crop_len_sec = round(trim_size_in_seg * seg_size_sec, 2) + logging.info(f"Crop len: {self.crop_len_sec}") + self.do_offset = do_offset + self.grid_size = grid_size + self.max_off_sec = max_off_sec + self.max_a_jitter_sec = max_wiggle_sec + self.segment_size_vframes = segment_size_vframes + self.n_segments = n_segments + self.step_size_seg = step_size_seg + self.prob_syncable = 0.5 + if do_offset: + self.class_grid = make_class_grid(-max_off_sec, max_off_sec, grid_size) + logging.info(f"Offset class grid: {self.class_grid}") + if self.max_a_jitter_sec is not None: + assert (max_wiggle_sec - 1e-6) <= ((self.class_grid[1] - self.class_grid[0]) / 2), f"{self.class_grid}" + + def forward(self, item): + vid = item["video"] + aud = item["audio"] + v_len_frames, C, H, W = vid.shape + a_len_frames = aud.shape[0] + + v_fps = int(item["meta"]["video"]["fps"][0]) + a_fps = int(item["meta"]["audio"]["framerate"][0]) + + v_crop_len_frames = sec2frames(self.crop_len_sec, v_fps) + a_crop_len_frames = sec2frames(self.crop_len_sec, a_fps) + + if self.do_offset: + # trying to get the offset parameters (for instance during valid and test we have fixed offsets) + offset_sec = item["targets"].get("offset_sec", None) + v_start_i_sec = item["targets"].get("v_start_i_sec", None) + # train-time + if offset_sec is None and v_start_i_sec is None: + + # for the syncability training, we want to have a syncable or non-syncable offset with 50% prob + offset_is_syncable = random.random() < self.prob_syncable # 1=syncable, 0=non-syncable + if offset_is_syncable: + offset_sec = random.choice(self.class_grid.tolist()) + else: + offset_sec = random.choice([-self.crop_len_sec, self.crop_len_sec]) # either - or + offset + # aud starts `offset_sec` earlier than it should; aud has what will be shown after offset_sec + + offset_sec = round(offset_sec, 2) + v_start_max_sec = frames2sec(v_len_frames - v_crop_len_frames, v_fps) + assert v_start_max_sec > 0, f'{v_len_frames} {v_crop_len_frames} {v_fps} @ {item["path"]}' + # `v_start_sec` IS NOT rounded to the fps grid + v_start_sec = random.uniform(max(0, -offset_sec), min(v_start_max_sec, v_start_max_sec - offset_sec)) + assert 0 <= v_start_sec <= v_start_max_sec, f'{v_start_sec} {v_start_max_sec} {item["path"]}' + v_start_i = sec2frames(v_start_sec, v_fps) + v_end_i = v_start_i + v_crop_len_frames + # `v_start_i_sec` IS rounded to the fps grid + v_start_i_sec = frames2sec(v_start_i, v_fps) + # `a_start_i` depends on the rounded value `v_start_i_sec`, otherwise + # (v_start_sec) we have ±0.1 jittering + a_start_i = sec2frames(v_start_i_sec + offset_sec, a_fps) + if self.max_a_jitter_sec is not None and self.max_a_jitter_sec > 0: + a_start_i, a_jitter_i = apply_a_jitter( + a_start_i, a_len_frames, a_crop_len_frames, a_fps, self.max_a_jitter_sec + ) + item["meta"]["a_jitter_i"] = a_jitter_i + a_end_i = a_start_i + a_crop_len_frames + else: + offset_sec = round(offset_sec, 2) + v_start_i = sec2frames(v_start_i_sec, v_fps) + a_start_i = sec2frames(v_start_i_sec + offset_sec, a_fps) + v_end_i = v_start_i + v_crop_len_frames + a_end_i = a_start_i + a_crop_len_frames + else: + offset_sec = 0.0 + is_random_crop = item["split"] == "train" + v_start_i, v_end_i = self.get_crop_idx(v_len_frames, v_crop_len_frames, is_random=is_random_crop) + v_start_i_sec = frames2sec(v_start_i, v_fps) + a_start_i = sec2frames(v_start_i_sec, a_fps) + if self.max_a_jitter_sec is not None and self.max_a_jitter_sec > 0: + a_start_i, a_jitter_i = apply_a_jitter( + a_start_i, a_len_frames, a_crop_len_frames, a_fps, self.max_a_jitter_sec + ) + item["meta"]["a_jitter_i"] = a_jitter_i + a_end_i = a_start_i + a_crop_len_frames + + # sometimes due to the rounding error e.g. v_start_sec = 1.505 but sec2frames(1.505, 25) = 1.48 + # given offset is -1.5, the a_start_i will be a small negative value. (likely a_fps * 1/v_fps * 0.5) + if a_start_i < 0: + how_much_out = a_start_i + logging.info(f'a_start_i is negative ({how_much_out}) at {item["path"]}') + if abs(how_much_out) <= a_fps / v_fps: + logging.info("fixing it") + a_start_i += abs(how_much_out) + a_end_i += abs(how_much_out) + else: + raise Exception(f'{how_much_out} {item["path"]}') + + assert v_start_i < v_end_i and a_start_i < a_end_i + assert aud.shape[0] >= a_end_i, f'{aud.shape} {a_end_i} {item["path"]}' + assert vid.shape[0] >= v_end_i, f'{vid.shape} {v_end_i} {item["path"]}' + + vid, aud = vid[v_start_i:v_end_i, :, :, :], aud[a_start_i:a_end_i] + + item["video"] = vid + item["audio"] = aud + + assert item["video"].shape[0] == int(v_fps * self.crop_len_sec), f'{item["video"].shape} {item["path"]}' + assert item["audio"].shape[0] == int(a_fps * self.crop_len_sec), f'{item["audio"].shape} {item["path"]}' + + # caching parameters + if self.do_offset: + # NOTE: this is useless for the extreme offsetting + offset_label, offset_target = quantize_offset(self.class_grid, offset_sec) + item["targets"]["offset_sec"] = offset_sec + item["targets"]["offset_label"] = offset_label + # assert 'offset_target' not in item['targets'], f'{item["targets"]}. What passed it there?' + item["targets"]["offset_target"] = offset_target + item["targets"]["v_start_i_sec"] = v_start_i_sec + item["targets"]["sync_target"] = int(offset_is_syncable) + + return item + + def get_crop_idx(self, len_frames: int, crop_len_frames: int, is_random=True): + if len_frames == crop_len_frames: + return 0, len_frames + if is_random: + left_i = random.randint(0, len_frames - crop_len_frames) + else: + left_i = int(round((len_frames - crop_len_frames) / 2.0)) + return left_i, left_i + crop_len_frames + + +class RGBToFloatToZeroOne(torch.nn.Module): + + def __init__(self) -> None: + super().__init__() + + def forward(self, item): + item["video"] = item["video"].to(torch.float32).div(255.0) + return item + + +class RGBToHalfToZeroOne(torch.nn.Module): + + def __init__(self) -> None: + super().__init__() + + def forward(self, item): + item["video"] = item["video"].half().div(255.0) + return item + + +class RGBNormalize(torchvision.transforms.Normalize): + """The same as the torchvision`s but with different interface for the dict. + This should work for any shape (..., C, H, W)""" + + def __init__(self, mean, std, inplace=False): + super().__init__(mean, std, inplace) + logging.info(f"RGBNormalize: mean={mean}, std={std}") + + def forward(self, item): + item["video"] = super().forward(item["video"]) + item["meta"]["video"]["norm_stats"] = {"mean": torch.as_tensor(self.mean), "std": torch.as_tensor(self.std)} + return item + + +class AudioRandomVolume(torch.nn.Module): + + def __init__(self, p: float, **kwargs): + super().__init__() + transform = torchaudio.transforms.Vol(**kwargs) + self.transform = torchvision.transforms.RandomApply([transform], p) + + def apply_to_single_clip(self, clip): + return self.transform(clip) + + def apply_to_each_clip(self, clips): + for i, clip in enumerate(clips): + clips[i] = self.apply_to_single_clip(clip) + return clips + + def forward(self, item): + has_batch_dim = len(item["audio"].shape) == 2 + if has_batch_dim: + fn = self.apply_to_each_clip + else: + fn = self.apply_to_single_clip + item["audio"] = fn(item["audio"]) + return item + + +class AudioRandomLowpassFilter(torch.nn.Module): + + def __init__(self, p: float, cutoff_freq: float, Q: float = 0.707): + super().__init__() + self.p = p + self.cutoff_freq = cutoff_freq + self.Q = Q + + def apply_to_single_clip(self, clip, sr): + if self.p > torch.rand(1): + return torchaudio.functional.lowpass_biquad(clip, sr, self.cutoff_freq, self.Q) + else: + return clip + + def apply_to_each_clip(self, clips, sr): + for i, clip in enumerate(clips): + clips[i] = self.apply_to_single_clip(clip, sr) + return clips + + def forward(self, item): + has_batch_dim = len(item["audio"].shape) == 2 + sr = int(item["meta"]["audio"]["framerate"][0]) + if has_batch_dim: + fn = self.apply_to_each_clip + else: + fn = self.apply_to_single_clip + item["audio"] = fn(item["audio"], sr) + return item + + +class AudioRandomPitchShift(torch.nn.Module): + + def __init__(self, p: float, shift: int) -> None: + super().__init__() + self.p = p + self.shift = shift + + def apply_to_single_clip(self, wave, sr): + if self.p > torch.rand(1): + effects = [["pitch", f"{self.shift}"], ["rate", f"{sr}"]] + wave = wave.unsqueeze(0) + wave, _ = torchaudio.sox_effects.apply_effects_tensor(wave, sr, effects) + wave = wave.squeeze(0) + return wave + + def apply_to_each_clip(self, waves, sr): + for i, wave in enumerate(waves): + waves[i] = self.apply_to_single_clip(wave, sr) + return waves + + def forward(self, item): + has_batch_dim = len(item["audio"].shape) == 2 + sr = int(item["meta"]["audio"]["framerate"][0]) + if has_batch_dim: + fn = self.apply_to_each_clip + else: + fn = self.apply_to_single_clip + item["audio"] = fn(item["audio"], sr) + return item + + +class AudioRandomReverb(torch.nn.Module): + + def __init__(self, p: float) -> None: + super().__init__() + self.p = p + self.effects = [["reverb", "-w"]] + + def apply_to_single_clip(self, wave, fps): + if self.p > torch.rand(1): + wave = wave.unsqueeze(0) + wave, _ = torchaudio.sox_effects.apply_effects_tensor(wave, fps, self.effects) + wave = wave.mean(dim=0) + return wave + + def apply_to_each_clip(self, waves, fps): + for i, wave in enumerate(waves): + waves[i] = self.apply_to_single_clip(wave, fps) + return waves + + def forward(self, item): + has_batch_dim = len(item["audio"].shape) == 2 + sr = int(item["meta"]["audio"]["framerate"][0]) + if has_batch_dim: + fn = self.apply_to_each_clip + else: + fn = self.apply_to_single_clip + item["audio"] = fn(item["audio"], sr) + return item + + +class AudioRandomGaussNoise(torch.nn.Module): + + def __init__(self, p: float, amplitude=0.01) -> None: + super().__init__() + self.p = p + self.amplitude = amplitude + + def apply_to_single_clip(self, wave): + if self.p > torch.rand(1): + noise = torch.randn_like(wave, dtype=wave.dtype) + wave = wave + self.amplitude * noise + return wave + + def apply_to_each_clip(self, waves): + for i, wave in enumerate(waves): + waves[i] = self.apply_to_single_clip(wave) + return waves + + def forward(self, item): + has_batch_dim = len(item["audio"].shape) == 2 + if has_batch_dim: + fn = self.apply_to_each_clip + else: + fn = self.apply_to_single_clip + item["audio"] = fn(item["audio"]) + return item + + +class AudioMelSpectrogram(torch.nn.Module): + + def __init__(self, **kwargs): + super().__init__() + self.spec = torchaudio.transforms.MelSpectrogram(**kwargs) + + def forward(self, item): + item["audio"] = self.spec(item["audio"]) # safe for batched input + return item + + +class AudioLog(torch.nn.Module): + + def __init__(self, eps=1e-6) -> None: + super().__init__() + self.eps = eps + + def forward(self, item): + item["audio"] = torch.log(item["audio"] + self.eps) + return item + + +class PadOrTruncate(torch.nn.Module): + + def __init__(self, max_spec_t: int, pad_mode: str = "constant", pad_value: float = 0.0): + super().__init__() + self.max_spec_t = max_spec_t + self.pad_mode = pad_mode + self.pad_value = pad_value + + def forward(self, item): + item["audio"] = self.pad_or_truncate(item["audio"]) + return item + + def pad_or_truncate(self, audio): + difference = self.max_spec_t - audio.shape[-1] # safe for batched input + # pad or truncate, depending on difference + if difference > 0: + # pad the last dim (time) -> (..., n_mels, 0+time+difference) # safe for batched input + pad_dims = (0, difference) + audio = torch.nn.functional.pad(audio, pad_dims, self.pad_mode, self.pad_value) + elif difference < 0: + logging.warning(f"Truncating spec ({audio.shape}) to max_spec_t ({self.max_spec_t}).") + audio = audio[..., : self.max_spec_t] # safe for batched input + return audio + + +class AudioNormalizeAST(torch.nn.Module): + """Normalization is done with two specified mean and std (half)""" + + def __init__(self, mean: float, std: float) -> None: + super().__init__() + self.mean = mean + self.std = std + + def forward(self, item): + item["audio"] = (item["audio"] - self.mean) / (2 * self.std) + item["meta"]["audio"]["norm_stats"] = {"mean": self.mean, "std": self.std} + return item + + +class PermuteStreams(torch.nn.Module): + + def __init__(self, einops_order_audio: str, einops_order_rgb: str) -> None: + '''For example: + einops_order_audio: "S F T -> S T F" + einops_order_rgb: "S T C H W -> S C T H W"''' + super().__init__() + self.einops_order_audio = einops_order_audio + self.einops_order_rgb = einops_order_rgb + + def forward(self, item): + if self.einops_order_audio is not None: + item["audio"] = einops.rearrange(item["audio"], self.einops_order_audio).contiguous() + if self.einops_order_rgb is not None: + item["video"] = einops.rearrange(item["video"], self.einops_order_rgb).contiguous() + return item + + +class ResampleAudio(torch.nn.Module): + + def __init__(self, new_fps: int): + super().__init__() + self.new_fps = new_fps + + def forward(self, item): + orig_fps = int(item["meta"]["audio"]["framerate"][0]) + item["meta"]["audio"]["orig_shape"] = item["audio"].shape + if orig_fps != self.new_fps: + item["audio"] = torchaudio.functional.resample(item["audio"], orig_fps, self.new_fps) + item["meta"]["audio"]["framerate"][0] = self.new_fps + return item + + +class ResampleRGB(torch.nn.Module): + + def __init__(self, new_fps: int) -> None: + super().__init__() + self.new_fps = new_fps + + def forward(self, item): + orig_fps = float(item["meta"]["video"]["fps"][0]) + item["meta"]["video"]["orig_shape"] = item["video"].shape + if orig_fps != self.new_fps: + duration_sec = item["video"].shape[0] / orig_fps + indices = torch.arange(0, orig_fps * duration_sec - 1e-9, orig_fps / self.new_fps) + # basically, rounding + indices = indices.to(dtype=torch.long) + item["video"] = item["video"][indices] + item["meta"]["video"]["fps"][0] = self.new_fps + return item + + +class ResizeAndLetterboxPad(torch.nn.Module): + """Adapted from WACV24 Amazon`s challenge""" + + def __init__(self, new_h, new_w): + super().__init__() + self.new_h = new_h + self.new_w = new_w + self.aspect_ratio = new_w / new_h + + def forward(self, item): + item["video"] = self.resize_and_pad(item["video"]) + return item + + def resize_and_pad(self, rgb: torch.Tensor): + _, _, height, width = rgb.shape + current_aspect_ratio = width / height + if current_aspect_ratio > self.aspect_ratio: + scaled_height = round(self.new_w / current_aspect_ratio) + rgb = torchvision.transforms.functional.resize(rgb, (scaled_height, self.new_w), antialias=None) + top = (self.new_h - scaled_height) // 2 + bottom = self.new_h - (scaled_height + top) + rgb = torch.nn.ConstantPad2d((0, 0, top, bottom), 0)(rgb) + elif current_aspect_ratio < self.aspect_ratio: + scaled_width = round(self.new_h * current_aspect_ratio) + rgb = torchvision.transforms.functional.resize(rgb, (self.new_h, scaled_width), antialias=None) + left = (self.new_w - scaled_width) // 2 + right = self.new_w - (scaled_width + left) + rgb = torch.nn.ConstantPad2d((left, right, 0, 0), 0)(rgb) + return rgb + + +class ResampleResizeLetterboxPad(torch.nn.Module): + + def __init__(self, afps, vfps, new_h, new_w) -> None: + super().__init__() + self.transforms = torchvision.transforms.Compose( + [ResampleAudio(new_fps=afps), ResampleRGB(new_fps=vfps), ResizeAndLetterboxPad(new_h=new_h, new_w=new_w)] + ) + + def forward(self, x: dict) -> dict: + return self.transforms(x) + + +class DoNothing(torch.nn.Module): + def __init__(self, *args, **kwargs) -> None: + super().__init__() + + def forward(self, x: dict) -> dict: + return x + + +if __name__ == "__main__": + grid = make_class_grid(-1, 1, 21) + grid = make_class_grid(-2, 2, 41) + print("grid:", grid) + print("value quantization:", quantize_offset(grid, 0.06)) + v_fps = 25.0 + duration = 10.0 + + input = { + "video": torch.randint(0, 256, (int(duration * v_fps), 3, 720 // 2, 1280 // 2), dtype=torch.uint8), + "audio": torch.arange(221184 - 1).float(), + "targets": {}, + "meta": { + "video": {"duration": [duration], "fps": [v_fps]}, + "audio": {"duration": [duration], "framerate": [22050.0]}, + "subtitles": {"duration": []}, + "cc": {"duration": []}, + }, + "path": "/home/nvme/data/vggsound/video/-5cWCaoEDlE_261000_271000.mp4", + "split": "train", + } + + print(input["audio"].shape, input["video"].shape) + + fn = EqualifyFromRight(clip_max_len_sec=10) + input = fn(input) + print(input["audio"].shape, input["video"].shape) + + fn = RGBSpatialCrop((224, 224), is_random=True) + # fn = RGBSpatialCrop((112, 112), is_random=True) + input = fn(input) + print(input["audio"].shape, input["video"].shape, input["meta"]["audio"]) + + fn = Resize((224, 224)) + input = fn(input) + print(input["audio"].shape, input["video"].shape, input["meta"]["audio"]) + + fn = GenerateMultipleSegments( + segment_size_vframes=16, n_segments=14, is_start_random=False, audio_jitter_sec=0.05, step_size_seg=0.5 + ) + input = fn(input) + print(input["audio"].shape, input["video"].shape, input["meta"]["audio"]) + + fn = RandomApplyColorDistortion(p_gray_scale=0.5, p_color_jitter=0.5, s=1.0) + input = fn(input) + print(input["audio"].shape, input["video"].shape, input["meta"]["audio"]) + + fn = RGBToFloatToZeroOne() + input = fn(input) + print(input["audio"].shape, input["video"].shape, input["meta"]["audio"]) + print(input["meta"]) + + fn = RGBNormalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) + input = fn(input) + print(input["audio"].shape, input["video"].shape, input["meta"]["audio"]) + print(input["video"].mean(dim=(0, 2, 3))) + print(input["meta"]) + + fn = AudioRandomReverb(p=1.0) + input = fn(input) + + fn = AudioRandomVolume(p=1.0, gain=2.0, gain_type="amplitude") + input = fn(input) + print(input["audio"].shape, input["video"].shape, input["meta"]["audio"]) + + fn = AudioRandomPitchShift(p=1.0, shift=1000) + input = fn(input) + print(input["audio"].shape, input["video"].shape, input["meta"]["audio"]) + + fn = AudioRandomLowpassFilter(p=1.0, cutoff_freq=100) + input = fn(input) + print(input["audio"].shape, input["video"].shape, input["meta"]["audio"]) + + fn = AudioRandomGaussNoise(p=1.0, amplitude=0.01) + input = fn(input) + print(input["audio"].shape, input["video"].shape, input["meta"]["audio"]) + + fn = AudioLog() + input = fn(input) + print(input["audio"].shape, input["video"].shape, input["meta"]["audio"]) + + # audio only + input = { + "audio": torch.arange(221184).float(), + "meta": { + "video": {"duration": [10.0], "fps": [10.0]}, + "audio": {"duration": [11.0], "framerate": [22050.0]}, + "subtitles": {"duration": []}, + "cc": {"duration": []}, + }, + "path": "/home/nvme/data/vggsound/video/-5cWCaoEDlE_261000_271000.mp4", + } + + print(input["audio"].shape) + + fn = AudioLog() + input = fn(input) + print(input["audio"].shape, input["meta"]["audio"]) + print(input["meta"]) + print(input["audio"].min(), input["audio"].max()) diff --git a/hunyuanvideo_foley/models/synchformer/divided_224_16x4.yaml b/hunyuanvideo_foley/models/synchformer/divided_224_16x4.yaml new file mode 100644 index 0000000..9fec8a6 --- /dev/null +++ b/hunyuanvideo_foley/models/synchformer/divided_224_16x4.yaml @@ -0,0 +1,84 @@ +TRAIN: + ENABLE: True + DATASET: Ssv2 + BATCH_SIZE: 32 + EVAL_PERIOD: 5 + CHECKPOINT_PERIOD: 5 + AUTO_RESUME: True + CHECKPOINT_EPOCH_RESET: True + CHECKPOINT_FILE_PATH: /checkpoint/fmetze/neurips_sota/40944587/checkpoints/checkpoint_epoch_00035.pyth +DATA: + NUM_FRAMES: 16 + SAMPLING_RATE: 4 + TRAIN_JITTER_SCALES: [256, 320] + TRAIN_CROP_SIZE: 224 + TEST_CROP_SIZE: 224 + INPUT_CHANNEL_NUM: [3] + MEAN: [0.5, 0.5, 0.5] + STD: [0.5, 0.5, 0.5] + PATH_TO_DATA_DIR: /private/home/mandelapatrick/slowfast/data/ssv2 + PATH_PREFIX: /datasets01/SomethingV2/092720/20bn-something-something-v2-frames + INV_UNIFORM_SAMPLE: True + RANDOM_FLIP: False + REVERSE_INPUT_CHANNEL: True + USE_RAND_AUGMENT: True + RE_PROB: 0.0 + USE_REPEATED_AUG: False + USE_RANDOM_RESIZE_CROPS: False + COLORJITTER: False + GRAYSCALE: False + GAUSSIAN: False +SOLVER: + BASE_LR: 1e-4 + LR_POLICY: steps_with_relative_lrs + LRS: [1, 0.1, 0.01] + STEPS: [0, 20, 30] + MAX_EPOCH: 35 + MOMENTUM: 0.9 + WEIGHT_DECAY: 5e-2 + WARMUP_EPOCHS: 0.0 + OPTIMIZING_METHOD: adamw + USE_MIXED_PRECISION: True + SMOOTHING: 0.2 +SLOWFAST: + ALPHA: 8 +VIT: + PATCH_SIZE: 16 + PATCH_SIZE_TEMP: 2 + CHANNELS: 3 + EMBED_DIM: 768 + DEPTH: 12 + NUM_HEADS: 12 + MLP_RATIO: 4 + QKV_BIAS: True + VIDEO_INPUT: True + TEMPORAL_RESOLUTION: 8 + USE_MLP: True + DROP: 0.0 + POS_DROPOUT: 0.0 + DROP_PATH: 0.2 + IM_PRETRAINED: True + HEAD_DROPOUT: 0.0 + HEAD_ACT: tanh + PRETRAINED_WEIGHTS: vit_1k + ATTN_LAYER: divided +MODEL: + NUM_CLASSES: 174 + ARCH: slow + MODEL_NAME: VisionTransformer + LOSS_FUNC: cross_entropy +TEST: + ENABLE: True + DATASET: Ssv2 + BATCH_SIZE: 64 + NUM_ENSEMBLE_VIEWS: 1 + NUM_SPATIAL_CROPS: 3 +DATA_LOADER: + NUM_WORKERS: 4 + PIN_MEMORY: True +NUM_GPUS: 8 +NUM_SHARDS: 4 +RNG_SEED: 0 +OUTPUT_DIR: . +TENSORBOARD: + ENABLE: True diff --git a/hunyuanvideo_foley/models/synchformer/modeling_ast.py b/hunyuanvideo_foley/models/synchformer/modeling_ast.py new file mode 100644 index 0000000..24feae8 --- /dev/null +++ b/hunyuanvideo_foley/models/synchformer/modeling_ast.py @@ -0,0 +1,673 @@ +# coding=utf-8 +# Copyright 2022 MIT and The HuggingFace Inc. team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Modified by v-iashin to support token masking + +"""PyTorch Audio Spectrogram Transformer (AST) model.""" + +import math +from typing import Dict, List, Optional, Set, Tuple, Union + +import torch +import torch.utils.checkpoint +from torch import nn +from torch.nn import BCEWithLogitsLoss, CrossEntropyLoss, MSELoss + +from transformers.activations import ACT2FN +from transformers.modeling_outputs import BaseModelOutput, BaseModelOutputWithPooling, SequenceClassifierOutput +from transformers.modeling_utils import PreTrainedModel +from transformers.pytorch_utils import find_pruneable_heads_and_indices, prune_linear_layer +from transformers.models.audio_spectrogram_transformer.modeling_audio_spectrogram_transformer import ASTConfig +from transformers.utils import ( + add_code_sample_docstrings, + add_start_docstrings, + add_start_docstrings_to_model_forward, + logging, +) + + +logger = logging.get_logger(__name__) + +# General docstring +_CONFIG_FOR_DOC = "ASTConfig" + +# Base docstring +_CHECKPOINT_FOR_DOC = "MIT/ast-finetuned-audioset-10-10-0.4593" +_EXPECTED_OUTPUT_SHAPE = [1, 1214, 768] + +# Audio classification docstring +_SEQ_CLASS_CHECKPOINT = "MIT/ast-finetuned-audioset-10-10-0.4593" +_SEQ_CLASS_EXPECTED_OUTPUT = "'Speech'" +_SEQ_CLASS_EXPECTED_LOSS = 0.17 + + +AUDIO_SPECTROGRAM_TRANSFORMER_PRETRAINED_MODEL_ARCHIVE_LIST = [ + "MIT/ast-finetuned-audioset-10-10-0.4593", + # See all Audio Spectrogram Transformer models at https://huggingface.co/models?filter=ast +] + + +class ASTEmbeddings(nn.Module): + """ + Construct the CLS token, position and patch embeddings. + """ + + def __init__(self, config: ASTConfig) -> None: + super().__init__() + + self.cls_token = nn.Parameter(torch.zeros(1, 1, config.hidden_size)) + self.distillation_token = nn.Parameter(torch.zeros(1, 1, config.hidden_size)) + self.patch_embeddings = ASTPatchEmbeddings(config) + + frequency_out_dimension, time_out_dimension = self.get_shape(config) + num_patches = frequency_out_dimension * time_out_dimension + self.position_embeddings = nn.Parameter(torch.zeros(1, num_patches + 2, config.hidden_size)) + self.dropout = nn.Dropout(config.hidden_dropout_prob) + self.config = config + + def get_shape(self, config): + # see Karpathy's cs231n blog on how to calculate the output dimensions + # https://cs231n.github.io/convolutional-networks/#conv + frequency_out_dimension = (config.num_mel_bins - config.patch_size) // config.frequency_stride + 1 + time_out_dimension = (config.max_length - config.patch_size) // config.time_stride + 1 + + return frequency_out_dimension, time_out_dimension + + def forward(self, input_values: torch.Tensor) -> torch.Tensor: + batch_size = input_values.shape[0] + embeddings = self.patch_embeddings(input_values) + + cls_tokens = self.cls_token.expand(batch_size, -1, -1) + distillation_tokens = self.distillation_token.expand(batch_size, -1, -1) + embeddings = torch.cat((cls_tokens, distillation_tokens, embeddings), dim=1) + print(self.position_embeddings.shape) + embeddings = embeddings + self.position_embeddings + embeddings = self.dropout(embeddings) + + return embeddings + + +class ASTPatchEmbeddings(nn.Module): + """ + This class turns `input_values` into the initial `hidden_states` (patch embeddings) of shape `(batch_size, + seq_length, hidden_size)` to be consumed by a Transformer. + """ + + def __init__(self, config): + super().__init__() + + patch_size = config.patch_size + frequency_stride = config.frequency_stride + time_stride = config.time_stride + + self.projection = nn.Conv2d( + 1, config.hidden_size, kernel_size=(patch_size, patch_size), stride=(frequency_stride, time_stride) + ) + + def forward(self, input_values: torch.Tensor) -> torch.Tensor: + input_values = input_values.unsqueeze(1) + input_values = input_values.transpose(2, 3) + embeddings = self.projection(input_values).flatten(2).transpose(1, 2) + return embeddings + + +# Copied from transformers.models.vit.modeling_vit.ViTSelfAttention with ViT->AST +class ASTSelfAttention(nn.Module): + def __init__(self, config: ASTConfig) -> None: + super().__init__() + if config.hidden_size % config.num_attention_heads != 0 and not hasattr(config, "embedding_size"): + raise ValueError( + f"The hidden size {config.hidden_size,} is not a multiple of the number of attention " + f"heads {config.num_attention_heads}." + ) + + self.num_attention_heads = config.num_attention_heads + self.attention_head_size = int(config.hidden_size / config.num_attention_heads) + self.all_head_size = self.num_attention_heads * self.attention_head_size + + self.query = nn.Linear(config.hidden_size, self.all_head_size, bias=config.qkv_bias) + self.key = nn.Linear(config.hidden_size, self.all_head_size, bias=config.qkv_bias) + self.value = nn.Linear(config.hidden_size, self.all_head_size, bias=config.qkv_bias) + + self.dropout = nn.Dropout(config.attention_probs_dropout_prob) + + def transpose_for_scores(self, x: torch.Tensor) -> torch.Tensor: + new_x_shape = x.size()[:-1] + (self.num_attention_heads, self.attention_head_size) + x = x.view(new_x_shape) + return x.permute(0, 2, 1, 3) + + def forward( + self, + hidden_states, + tok_mask: Optional[torch.Tensor] = None, + head_mask: Optional[torch.Tensor] = None, + output_attentions: bool = False, + ) -> Union[Tuple[torch.Tensor, torch.Tensor], Tuple[torch.Tensor]]: + mixed_query_layer = self.query(hidden_states) + + key_layer = self.transpose_for_scores(self.key(hidden_states)) + value_layer = self.transpose_for_scores(self.value(hidden_states)) + query_layer = self.transpose_for_scores(mixed_query_layer) + + # Take the dot product between "query" and "key" to get the raw attention scores. + attention_scores = torch.matmul(query_layer, key_layer.transpose(-1, -2)) + + attention_scores = attention_scores / math.sqrt(self.attention_head_size) + + # apply masking if provided, tok_mask is (BS, N): 1s - keep; attention_scores is (BS, H, N, N) + if tok_mask is not None: + BS, N = tok_mask.shape + attention_scores = attention_scores.masked_fill(tok_mask.view(BS, 1, 1, N) == 0, float("-inf")) + + # Normalize the attention scores to probabilities. + attention_probs = nn.functional.softmax(attention_scores, dim=-1) + + # This is actually dropping out entire tokens to attend to, which might + # seem a bit unusual, but is taken from the original Transformer paper. + attention_probs = self.dropout(attention_probs) + + # Mask heads if we want to + if head_mask is not None: + attention_probs = attention_probs * head_mask + + context_layer = torch.matmul(attention_probs, value_layer) + + context_layer = context_layer.permute(0, 2, 1, 3).contiguous() + new_context_layer_shape = context_layer.size()[:-2] + (self.all_head_size,) + context_layer = context_layer.view(new_context_layer_shape) + + outputs = (context_layer, attention_probs) if output_attentions else (context_layer,) + + return outputs + + +# Copied from transformers.models.vit.modeling_vit.ViTSelfOutput with ViT->AST +class ASTSelfOutput(nn.Module): + """ + The residual connection is defined in ASTLayer instead of here (as is the case with other models), due to the + layernorm applied before each block. + """ + + def __init__(self, config: ASTConfig) -> None: + super().__init__() + self.dense = nn.Linear(config.hidden_size, config.hidden_size) + self.dropout = nn.Dropout(config.hidden_dropout_prob) + + def forward(self, hidden_states: torch.Tensor, input_tensor: torch.Tensor) -> torch.Tensor: + hidden_states = self.dense(hidden_states) + hidden_states = self.dropout(hidden_states) + + return hidden_states + + +# Copied from transformers.models.vit.modeling_vit.ViTAttention with ViT->AST +class ASTAttention(nn.Module): + def __init__(self, config: ASTConfig) -> None: + super().__init__() + self.attention = ASTSelfAttention(config) + self.output = ASTSelfOutput(config) + self.pruned_heads = set() + + def prune_heads(self, heads: Set[int]) -> None: + if len(heads) == 0: + return + heads, index = find_pruneable_heads_and_indices( + heads, self.attention.num_attention_heads, self.attention.attention_head_size, self.pruned_heads + ) + + # Prune linear layers + self.attention.query = prune_linear_layer(self.attention.query, index) + self.attention.key = prune_linear_layer(self.attention.key, index) + self.attention.value = prune_linear_layer(self.attention.value, index) + self.output.dense = prune_linear_layer(self.output.dense, index, dim=1) + + # Update hyper params and store pruned heads + self.attention.num_attention_heads = self.attention.num_attention_heads - len(heads) + self.attention.all_head_size = self.attention.attention_head_size * self.attention.num_attention_heads + self.pruned_heads = self.pruned_heads.union(heads) + + def forward( + self, + hidden_states: torch.Tensor, + tok_mask: Optional[torch.Tensor] = None, + head_mask: Optional[torch.Tensor] = None, + output_attentions: bool = False, + ) -> Union[Tuple[torch.Tensor, torch.Tensor], Tuple[torch.Tensor]]: + self_outputs = self.attention(hidden_states, tok_mask, head_mask, output_attentions) + + attention_output = self.output(self_outputs[0], hidden_states) + + outputs = (attention_output,) + self_outputs[1:] # add attentions if we output them + return outputs + + +# Copied from transformers.models.vit.modeling_vit.ViTIntermediate with ViT->AST +class ASTIntermediate(nn.Module): + def __init__(self, config: ASTConfig) -> None: + super().__init__() + self.dense = nn.Linear(config.hidden_size, config.intermediate_size) + if isinstance(config.hidden_act, str): + self.intermediate_act_fn = ACT2FN[config.hidden_act] + else: + self.intermediate_act_fn = config.hidden_act + + def forward(self, hidden_states: torch.Tensor) -> torch.Tensor: + hidden_states = self.dense(hidden_states) + hidden_states = self.intermediate_act_fn(hidden_states) + + return hidden_states + + +# Copied from transformers.models.vit.modeling_vit.ViTOutput with ViT->AST +class ASTOutput(nn.Module): + def __init__(self, config: ASTConfig) -> None: + super().__init__() + self.dense = nn.Linear(config.intermediate_size, config.hidden_size) + self.dropout = nn.Dropout(config.hidden_dropout_prob) + + def forward(self, hidden_states: torch.Tensor, input_tensor: torch.Tensor) -> torch.Tensor: + hidden_states = self.dense(hidden_states) + hidden_states = self.dropout(hidden_states) + + hidden_states = hidden_states + input_tensor + + return hidden_states + + +# Copied from transformers.models.vit.modeling_vit.ViTLayer with ViT->AST +class ASTLayer(nn.Module): + """This corresponds to the Block class in the timm implementation.""" + + def __init__(self, config: ASTConfig) -> None: + super().__init__() + self.chunk_size_feed_forward = config.chunk_size_feed_forward + self.seq_len_dim = 1 + self.attention = ASTAttention(config) + self.intermediate = ASTIntermediate(config) + self.output = ASTOutput(config) + self.layernorm_before = nn.LayerNorm(config.hidden_size, eps=config.layer_norm_eps) + self.layernorm_after = nn.LayerNorm(config.hidden_size, eps=config.layer_norm_eps) + + def forward( + self, + hidden_states: torch.Tensor, + tok_mask: Optional[torch.Tensor] = None, + head_mask: Optional[torch.Tensor] = None, + output_attentions: bool = False, + ) -> Union[Tuple[torch.Tensor, torch.Tensor], Tuple[torch.Tensor]]: + self_attention_outputs = self.attention( + self.layernorm_before(hidden_states), # in AST, layernorm is applied before self-attention + tok_mask, + head_mask, + output_attentions=output_attentions, + ) + attention_output = self_attention_outputs[0] + outputs = self_attention_outputs[1:] # add self attentions if we output attention weights + + # first residual connection + hidden_states = attention_output + hidden_states + + # in AST, layernorm is also applied after self-attention + layer_output = self.layernorm_after(hidden_states) + layer_output = self.intermediate(layer_output) + + # second residual connection is done here + layer_output = self.output(layer_output, hidden_states) + + outputs = (layer_output,) + outputs + + return outputs + + +# Copied from transformers.models.vit.modeling_vit.ViTEncoder with ViT->AST +class ASTEncoder(nn.Module): + def __init__(self, config: ASTConfig) -> None: + super().__init__() + self.config = config + self.layer = nn.ModuleList([ASTLayer(config) for _ in range(config.num_hidden_layers)]) + self.gradient_checkpointing = False + + def forward( + self, + hidden_states: torch.Tensor, + tok_mask: Optional[torch.Tensor] = None, + head_mask: Optional[torch.Tensor] = None, + output_attentions: bool = False, + output_hidden_states: bool = False, + return_dict: bool = True, + ) -> Union[tuple, BaseModelOutput]: + all_hidden_states = () if output_hidden_states else None + all_self_attentions = () if output_attentions else None + + for i, layer_module in enumerate(self.layer): + if output_hidden_states: + all_hidden_states = all_hidden_states + (hidden_states,) + + layer_head_mask = head_mask[i] if head_mask is not None else None + + if self.gradient_checkpointing and self.training: + + def create_custom_forward(module): + def custom_forward(*inputs): + return module(*inputs, output_attentions) + + return custom_forward + + layer_outputs = torch.utils.checkpoint.checkpoint( + create_custom_forward(layer_module), + hidden_states, + tok_mask, + layer_head_mask, + ) + else: + layer_outputs = layer_module(hidden_states, tok_mask, layer_head_mask, output_attentions) + + hidden_states = layer_outputs[0] + + if output_attentions: + all_self_attentions = all_self_attentions + (layer_outputs[1],) + + if output_hidden_states: + all_hidden_states = all_hidden_states + (hidden_states,) + + if not return_dict: + return tuple(v for v in [hidden_states, all_hidden_states, all_self_attentions] if v is not None) + return BaseModelOutput( + last_hidden_state=hidden_states, + hidden_states=all_hidden_states, + attentions=all_self_attentions, + ) + + +class ASTPreTrainedModel(PreTrainedModel): + """ + An abstract class to handle weights initialization and a simple interface for downloading and loading pretrained + models. + """ + + config_class = ASTConfig + base_model_prefix = "audio_spectrogram_transformer" + main_input_name = "input_values" + supports_gradient_checkpointing = True + + # Copied from transformers.models.deit.modeling_deit.DeiTPreTrainedModel._init_weights + def _init_weights(self, module: Union[nn.Linear, nn.Conv2d, nn.LayerNorm]) -> None: + """Initialize the weights""" + if isinstance(module, (nn.Linear, nn.Conv2d)): + # Upcast the input in `fp32` and cast it back to desired `dtype` to avoid + # `trunc_normal_cpu` not implemented in `half` issues + module.weight.data = nn.init.trunc_normal_( + module.weight.data.to(torch.float32), mean=0.0, std=self.config.initializer_range + ).to(module.weight.dtype) + if module.bias is not None: + module.bias.data.zero_() + elif isinstance(module, nn.LayerNorm): + module.bias.data.zero_() + module.weight.data.fill_(1.0) + + # Copied from transformers.models.vit.modeling_vit.ViTPreTrainedModel._set_gradient_checkpointing with ViT->AST + def _set_gradient_checkpointing(self, module: ASTEncoder, value: bool = False) -> None: + if isinstance(module, ASTEncoder): + module.gradient_checkpointing = value + + +AUDIO_SPECTROGRAM_TRANSFORMER_START_DOCSTRING = r""" + This model is a PyTorch [torch.nn.Module](https://pytorch.org/docs/stable/nn.html#torch.nn.Module) subclass. Use it + as a regular PyTorch Module and refer to the PyTorch documentation for all matter related to general usage and + behavior. + + Parameters: + config ([`ASTConfig`]): + Model configuration class with all the parameters of the model. Initializing with a config file does not + load the weights associated with the model, only the configuration. Check out the + [`~PreTrainedModel.from_pretrained`] method to load the model weights. +""" + +AUDIO_SPECTROGRAM_TRANSFORMER_INPUTS_DOCSTRING = r""" + Args: + input_values (`torch.FloatTensor` of shape `(batch_size, num_channels, height, width)`): + Pixel values. Pixel values can be obtained using [`AutoFeatureExtractor`]. See + [`ASTFeatureExtractor.__call__`] for details. + + head_mask (`torch.FloatTensor` of shape `(num_heads,)` or `(num_layers, num_heads)`, *optional*): + Mask to nullify selected heads of the self-attention modules. Mask values selected in `[0, 1]`: + + - 1 indicates the head is **not masked**, + - 0 indicates the head is **masked**. + + output_attentions (`bool`, *optional*): + Whether or not to return the attentions tensors of all attention layers. See `attentions` under returned + tensors for more detail. + output_hidden_states (`bool`, *optional*): + Whether or not to return the hidden states of all layers. See `hidden_states` under returned tensors for + more detail. + return_dict (`bool`, *optional*): + Whether or not to return a [`~utils.ModelOutput`] instead of a plain tuple. +""" + + +@add_start_docstrings( + "The bare AST Model transformer outputting raw hidden-states without any specific head on top.", + AUDIO_SPECTROGRAM_TRANSFORMER_START_DOCSTRING, +) +class ASTModel(ASTPreTrainedModel): + def __init__(self, config: ASTConfig): + super().__init__(config) + self.config = config + + self.embeddings = ASTEmbeddings(config) + self.encoder = ASTEncoder(config) + + self.layernorm = nn.LayerNorm(config.hidden_size, eps=config.layer_norm_eps) + + # Initialize weights and apply final processing + self.post_init() + + def get_input_embeddings(self) -> ASTPatchEmbeddings: + return self.embeddings.patch_embeddings + + def _prune_heads(self, heads_to_prune: Dict[int, List[int]]) -> None: + """ + Prunes heads of the model. heads_to_prune: dict of {layer_num: list of heads to prune in this layer} See base + class PreTrainedModel + """ + for layer, heads in heads_to_prune.items(): + self.encoder.layer[layer].attention.prune_heads(heads) + + @add_start_docstrings_to_model_forward(AUDIO_SPECTROGRAM_TRANSFORMER_INPUTS_DOCSTRING) + @add_code_sample_docstrings( + checkpoint=_CHECKPOINT_FOR_DOC, + output_type=BaseModelOutputWithPooling, + config_class=_CONFIG_FOR_DOC, + modality="audio", + expected_output=_EXPECTED_OUTPUT_SHAPE, + ) + def forward( + self, + input_values: Optional[torch.Tensor] = None, + cont_mask: Optional[torch.Tensor] = None, + head_mask: Optional[torch.Tensor] = None, + output_attentions: Optional[bool] = None, + output_hidden_states: Optional[bool] = None, + return_dict: Optional[bool] = None, + ): + output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions + output_hidden_states = ( + output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states + ) + return_dict = return_dict if return_dict is not None else self.config.use_return_dict + + if input_values is None: + raise ValueError("You have to specify input_values") + + # Prepare head mask if needed + # 1.0 in head_mask indicate we keep the head + # attention_probs has shape bsz x n_heads x N x N + # input head_mask has shape [num_heads] or [num_hidden_layers x num_heads] + # and head_mask is converted to shape [num_hidden_layers x batch x num_heads x seq_length x seq_length] + head_mask = self.get_head_mask(head_mask, self.config.num_hidden_layers) + + embedding_output = self.embeddings(input_values) + + # transforms the mask that has spectrogram dims to the token masking which is obtained after patching. + # Due to the ovelap in patching, getting the token mask from spectrogram mask is not straightforward, + # because one 16x16 content patch is encoded in two tokens if stride is <16. So, to get the mask for + # tokens I will apply the patching func (self.embeddings) to the tensor with infinities at the masked + # content position. For infs, the patching fn will return nans, which I'll use to get the token mask. + if cont_mask is not None: + indicator = torch.ones_like(input_values).to(input_values.dtype) + # replace content mask (0s) with infs + indicator[~cont_mask] = torch.inf + # apply patching; now nans are where the content mask was + with torch.no_grad(): + indicator = self.embeddings(indicator) # BS, N, D + # replace nans with 0s; these are the tokens that correspond to the masked content + tok_mask = ~torch.isnan(indicator) + # since all values in the D-dimension (latent) will also be nans, we can just use the first el + tok_mask = tok_mask[:, :, 0] # (BS, 2+num_patches) -- 2 is from CLS and DISTIL tokens + else: + tok_mask = None + + encoder_outputs = self.encoder( + embedding_output, + tok_mask=tok_mask, + head_mask=head_mask, + output_attentions=output_attentions, + output_hidden_states=output_hidden_states, + return_dict=return_dict, + ) + sequence_output = encoder_outputs[0] + sequence_output = self.layernorm(sequence_output) + + pooled_output = (sequence_output[:, 0] + sequence_output[:, 1]) / 2 + + if not return_dict: + return (sequence_output, pooled_output) + encoder_outputs[1:] + + return ( + BaseModelOutputWithPooling( + last_hidden_state=sequence_output, + pooler_output=pooled_output, + hidden_states=encoder_outputs.hidden_states, + attentions=encoder_outputs.attentions, + ), + tok_mask, + ) + + +class ASTMLPHead(nn.Module): + def __init__(self, config: ASTConfig): + super().__init__() + self.layernorm = nn.LayerNorm(config.hidden_size, eps=config.layer_norm_eps) + self.dense = nn.Linear(config.hidden_size, config.num_labels) if config.num_labels > 0 else nn.Identity() + + def forward(self, hidden_state): + hidden_state = self.layernorm(hidden_state) + hidden_state = self.dense(hidden_state) + return hidden_state + + +@add_start_docstrings( + """ + Audio Spectrogram Transformer model with an audio classification head on top (a linear layer on top of the pooled + output) e.g. for datasets like AudioSet, Speech Commands v2. + """, + AUDIO_SPECTROGRAM_TRANSFORMER_START_DOCSTRING, +) +class ASTForAudioClassification(ASTPreTrainedModel): + def __init__(self, config: ASTConfig) -> None: + super().__init__(config) + + self.num_labels = config.num_labels + self.audio_spectrogram_transformer = ASTModel(config) + + # Classifier head + self.classifier = ASTMLPHead(config) + + # Initialize weights and apply final processing + self.post_init() + + @add_start_docstrings_to_model_forward(AUDIO_SPECTROGRAM_TRANSFORMER_INPUTS_DOCSTRING) + @add_code_sample_docstrings( + checkpoint=_SEQ_CLASS_CHECKPOINT, + output_type=SequenceClassifierOutput, + config_class=_CONFIG_FOR_DOC, + modality="audio", + expected_output=_SEQ_CLASS_EXPECTED_OUTPUT, + expected_loss=_SEQ_CLASS_EXPECTED_LOSS, + ) + def forward( + self, + input_values: Optional[torch.Tensor] = None, + cont_mask: Optional[torch.Tensor] = None, + head_mask: Optional[torch.Tensor] = None, + labels: Optional[torch.Tensor] = None, + output_attentions: Optional[bool] = None, + output_hidden_states: Optional[bool] = None, + return_dict: Optional[bool] = None, + ) -> Union[tuple, SequenceClassifierOutput]: + r""" + labels (`torch.LongTensor` of shape `(batch_size,)`, *optional*): + Labels for computing the audio classification/regression loss. Indices should be in `[0, ..., + config.num_labels - 1]`. If `config.num_labels == 1` a regression loss is computed (Mean-Square loss), If + `config.num_labels > 1` a classification loss is computed (Cross-Entropy). + """ + return_dict = return_dict if return_dict is not None else self.config.use_return_dict + + outputs = self.audio_spectrogram_transformer( + input_values, + cont_mask=cont_mask, + head_mask=head_mask, + output_attentions=output_attentions, + output_hidden_states=output_hidden_states, + return_dict=return_dict, + ) + + pooled_output = outputs[1] + logits = self.classifier(pooled_output) + + loss = None + if labels is not None: + if self.config.problem_type is None: + if self.num_labels == 1: + self.config.problem_type = "regression" + elif self.num_labels > 1 and (labels.dtype == torch.long or labels.dtype == torch.int): + self.config.problem_type = "single_label_classification" + else: + self.config.problem_type = "multi_label_classification" + + if self.config.problem_type == "regression": + loss_fct = MSELoss() + if self.num_labels == 1: + loss = loss_fct(logits.squeeze(), labels.squeeze()) + else: + loss = loss_fct(logits, labels) + elif self.config.problem_type == "single_label_classification": + loss_fct = CrossEntropyLoss() + loss = loss_fct(logits.view(-1, self.num_labels), labels.view(-1)) + elif self.config.problem_type == "multi_label_classification": + loss_fct = BCEWithLogitsLoss() + loss = loss_fct(logits, labels) + + if not return_dict: + output = (logits,) + outputs[2:] + return ((loss,) + output) if loss is not None else output + + return SequenceClassifierOutput( + loss=loss, + logits=logits, + hidden_states=outputs.hidden_states, + attentions=outputs.attentions, + ) diff --git a/hunyuanvideo_foley/models/synchformer/motionformer.py b/hunyuanvideo_foley/models/synchformer/motionformer.py new file mode 100644 index 0000000..bc0b55f --- /dev/null +++ b/hunyuanvideo_foley/models/synchformer/motionformer.py @@ -0,0 +1,397 @@ +import logging +from pathlib import Path + +import einops +import torch +from omegaconf import OmegaConf +from timm.layers import trunc_normal_ +from torch import nn + +from .utils import check_if_file_exists_else_download +from .video_model_builder import VisionTransformer + + +FILE2URL = { + # cfg + "motionformer_224_16x4.yaml": "https://raw.githubusercontent.com/facebookresearch/Motionformer/bf43d50/configs/SSV2/motionformer_224_16x4.yaml", + "joint_224_16x4.yaml": "https://raw.githubusercontent.com/facebookresearch/Motionformer/bf43d50/configs/SSV2/joint_224_16x4.yaml", + "divided_224_16x4.yaml": "https://raw.githubusercontent.com/facebookresearch/Motionformer/bf43d50/configs/SSV2/divided_224_16x4.yaml", + # ckpt + "ssv2_motionformer_224_16x4.pyth": "https://dl.fbaipublicfiles.com/motionformer/ssv2_motionformer_224_16x4.pyth", + "ssv2_joint_224_16x4.pyth": "https://dl.fbaipublicfiles.com/motionformer/ssv2_joint_224_16x4.pyth", + "ssv2_divided_224_16x4.pyth": "https://dl.fbaipublicfiles.com/motionformer/ssv2_divided_224_16x4.pyth", +} + + +class MotionFormer(VisionTransformer): + """This class serves three puposes: + 1. Renames the class to MotionFormer. + 2. Downloads the cfg from the original repo and patches it if needed. + 3. Takes care of feature extraction by redefining .forward() + - if `extract_features=True` and `factorize_space_time=False`, + the output is of shape (B, T, D) where T = 1 + (224 // 16) * (224 // 16) * 8 + - if `extract_features=True` and `factorize_space_time=True`, the output is of shape (B*S, D) + and spatial and temporal transformer encoder layers are used. + - if `extract_features=True` and `factorize_space_time=True` as well as `add_global_repr=True` + the output is of shape (B, D) and spatial and temporal transformer encoder layers + are used as well as the global representation is extracted from segments (extra pos emb + is added). + """ + + def __init__( + self, + extract_features: bool = False, + ckpt_path: str = None, + factorize_space_time: bool = None, + agg_space_module: str = None, + agg_time_module: str = None, + add_global_repr: bool = True, + agg_segments_module: str = None, + max_segments: int = None, + ): + self.extract_features = extract_features + self.ckpt_path = ckpt_path + self.factorize_space_time = factorize_space_time + + if self.ckpt_path is not None: + check_if_file_exists_else_download(self.ckpt_path, FILE2URL) + ckpt = torch.load(self.ckpt_path, map_location="cpu") + mformer_ckpt2cfg = { + "ssv2_motionformer_224_16x4.pyth": "motionformer_224_16x4.yaml", + "ssv2_joint_224_16x4.pyth": "joint_224_16x4.yaml", + "ssv2_divided_224_16x4.pyth": "divided_224_16x4.yaml", + } + # init from motionformer ckpt or from our Stage I ckpt + # depending on whether the feat extractor was pre-trained on AVCLIPMoCo or not, we need to + # load the state dict differently + was_pt_on_avclip = self.ckpt_path.endswith(".pt") # checks if it is a stage I ckpt (FIXME: a bit generic) + if self.ckpt_path.endswith(tuple(mformer_ckpt2cfg.keys())): + cfg_fname = mformer_ckpt2cfg[Path(self.ckpt_path).name] + elif was_pt_on_avclip: + # TODO: this is a hack, we should be able to get the cfg from the ckpt (earlier ckpt didn't have it) + s1_cfg = ckpt.get("args", None) # Stage I cfg + if s1_cfg is not None: + s1_vfeat_extractor_ckpt_path = s1_cfg.model.params.vfeat_extractor.params.ckpt_path + # if the stage I ckpt was initialized from a motionformer ckpt or train from scratch + if s1_vfeat_extractor_ckpt_path is not None: + cfg_fname = mformer_ckpt2cfg[Path(s1_vfeat_extractor_ckpt_path).name] + else: + cfg_fname = "divided_224_16x4.yaml" + else: + cfg_fname = "divided_224_16x4.yaml" + else: + raise ValueError(f"ckpt_path {self.ckpt_path} is not supported.") + else: + was_pt_on_avclip = False + cfg_fname = "divided_224_16x4.yaml" + # logging.info(f'No ckpt_path provided, using {cfg_fname} config.') + + if cfg_fname in ["motionformer_224_16x4.yaml", "divided_224_16x4.yaml"]: + pos_emb_type = "separate" + elif cfg_fname == "joint_224_16x4.yaml": + pos_emb_type = "joint" + + self.mformer_cfg_path = Path(__file__).absolute().parent / cfg_fname + + check_if_file_exists_else_download(self.mformer_cfg_path, FILE2URL) + mformer_cfg = OmegaConf.load(self.mformer_cfg_path) + logging.info(f"Loading MotionFormer config from {self.mformer_cfg_path.absolute()}") + + # patch the cfg (from the default cfg defined in the repo `Motionformer/slowfast/config/defaults.py`) + mformer_cfg.VIT.ATTN_DROPOUT = 0.0 + mformer_cfg.VIT.POS_EMBED = pos_emb_type + mformer_cfg.VIT.USE_ORIGINAL_TRAJ_ATTN_CODE = True + mformer_cfg.VIT.APPROX_ATTN_TYPE = "none" # guessing + mformer_cfg.VIT.APPROX_ATTN_DIM = 64 # from ckpt['cfg'] + + # finally init VisionTransformer with the cfg + super().__init__(mformer_cfg) + + # load the ckpt now if ckpt is provided and not from AVCLIPMoCo-pretrained ckpt + if (self.ckpt_path is not None) and (not was_pt_on_avclip): + _ckpt_load_status = self.load_state_dict(ckpt["model_state"], strict=False) + if len(_ckpt_load_status.missing_keys) > 0 or len(_ckpt_load_status.unexpected_keys) > 0: + logging.warning( + f"Loading exact vfeat_extractor ckpt from {self.ckpt_path} failed." + f"Missing keys: {_ckpt_load_status.missing_keys}, " + f"Unexpected keys: {_ckpt_load_status.unexpected_keys}" + ) + else: + logging.info(f"Loading vfeat_extractor ckpt from {self.ckpt_path} succeeded.") + + if self.extract_features: + assert isinstance(self.norm, nn.LayerNorm), "early x[:, 1:, :] may not be safe for per-tr weights" + # pre-logits are Sequential(nn.Linear(emb, emd), act) and `act` is tanh but see the logger + self.pre_logits = nn.Identity() + # we don't need the classification head (saving memory) + self.head = nn.Identity() + self.head_drop = nn.Identity() + # avoiding code duplication (used only if agg_*_module is TransformerEncoderLayer) + transf_enc_layer_kwargs = dict( + d_model=self.embed_dim, + nhead=self.num_heads, + activation=nn.GELU(), + batch_first=True, + dim_feedforward=self.mlp_ratio * self.embed_dim, + dropout=self.drop_rate, + layer_norm_eps=1e-6, + norm_first=True, + ) + # define adapters if needed + if self.factorize_space_time: + if agg_space_module == "TransformerEncoderLayer": + self.spatial_attn_agg = SpatialTransformerEncoderLayer(**transf_enc_layer_kwargs) + elif agg_space_module == "AveragePooling": + self.spatial_attn_agg = AveragePooling( + avg_pattern="BS D t h w -> BS D t", then_permute_pattern="BS D t -> BS t D" + ) + if agg_time_module == "TransformerEncoderLayer": + self.temp_attn_agg = TemporalTransformerEncoderLayer(**transf_enc_layer_kwargs) + elif agg_time_module == "AveragePooling": + self.temp_attn_agg = AveragePooling(avg_pattern="BS t D -> BS D") + elif "Identity" in agg_time_module: + self.temp_attn_agg = nn.Identity() + # define a global aggregation layer (aggregarate over segments) + self.add_global_repr = add_global_repr + if add_global_repr: + if agg_segments_module == "TransformerEncoderLayer": + # we can reuse the same layer as for temporal factorization (B, dim_to_agg, D) -> (B, D) + # we need to add pos emb (PE) because previously we added the same PE for each segment + pos_max_len = max_segments if max_segments is not None else 16 # 16 = 10sec//0.64sec + 1 + self.global_attn_agg = TemporalTransformerEncoderLayer( + add_pos_emb=True, + pos_emb_drop=mformer_cfg.VIT.POS_DROPOUT, + pos_max_len=pos_max_len, + **transf_enc_layer_kwargs, + ) + elif agg_segments_module == "AveragePooling": + self.global_attn_agg = AveragePooling(avg_pattern="B S D -> B D") + + if was_pt_on_avclip: + # we need to filter out the state_dict of the AVCLIP model (has both A and V extractors) + # and keep only the state_dict of the feat extractor + ckpt_weights = dict() + for k, v in ckpt["state_dict"].items(): + if k.startswith(("module.v_encoder.", "v_encoder.")): + k = k.replace("module.", "").replace("v_encoder.", "") + ckpt_weights[k] = v + _load_status = self.load_state_dict(ckpt_weights, strict=False) + if len(_load_status.missing_keys) > 0 or len(_load_status.unexpected_keys) > 0: + logging.warning( + f"Loading exact vfeat_extractor ckpt from {self.ckpt_path} failed. \n" + f"Missing keys ({len(_load_status.missing_keys)}): " + f"{_load_status.missing_keys}, \n" + f"Unexpected keys ({len(_load_status.unexpected_keys)}): " + f"{_load_status.unexpected_keys} \n" + f"temp_attn_agg are expected to be missing if ckpt was pt contrastively." + ) + else: + logging.info(f"Loading vfeat_extractor ckpt from {self.ckpt_path} succeeded.") + + # patch_embed is not used in MotionFormer, only patch_embed_3d, because cfg.VIT.PATCH_SIZE_TEMP > 1 + # but it used to calculate the number of patches, so we need to set keep it + self.patch_embed.requires_grad_(False) + + def forward(self, x): + """ + x is of shape (B, S, C, T, H, W) where S is the number of segments. + """ + # Batch, Segments, Channels, T=frames, Height, Width + B, S, C, T, H, W = x.shape + # Motionformer expects a tensor of shape (1, B, C, T, H, W). + # The first dimension (1) is a dummy dimension to make the input tensor and won't be used: + # see `video_model_builder.video_input`. + # x = x.unsqueeze(0) # (1, B, S, C, T, H, W) + + orig_shape = (B, S, C, T, H, W) + x = x.view(B * S, C, T, H, W) # flatten batch and segments + x = self.forward_segments(x, orig_shape=orig_shape) + # unpack the segments (using rest dimensions to support different shapes e.g. (BS, D) or (BS, t, D)) + x = x.view(B, S, *x.shape[1:]) + # x is now of shape (B*S, D) or (B*S, t, D) if `self.temp_attn_agg` is `Identity` + + return x # x is (B, S, ...) + + def forward_segments(self, x, orig_shape: tuple) -> torch.Tensor: + """x is of shape (1, BS, C, T, H, W) where S is the number of segments.""" + x, x_mask = self.forward_features(x) + + assert self.extract_features + + # (BS, T, D) where T = 1 + (224 // 16) * (224 // 16) * 8 + x = x[:, 1:, :] # without the CLS token for efficiency (should be safe for LayerNorm and FC) + x = self.norm(x) + x = self.pre_logits(x) + if self.factorize_space_time: + x = self.restore_spatio_temp_dims(x, orig_shape) # (B*S, D, t, h, w) <- (B*S, t*h*w, D) + + x = self.spatial_attn_agg(x, x_mask) # (B*S, t, D) + x = self.temp_attn_agg(x) # (B*S, D) or (BS, t, D) if `self.temp_attn_agg` is `Identity` + + return x + + def restore_spatio_temp_dims(self, feats: torch.Tensor, orig_shape: tuple) -> torch.Tensor: + """ + feats are of shape (B*S, T, D) where T = 1 + (224 // 16) * (224 // 16) * 8 + Our goal is to make them of shape (B*S, t, h, w, D) where h, w are the spatial dimensions. + From `self.patch_embed_3d`, it follows that we could reshape feats with: + `feats.transpose(1, 2).view(B*S, D, t, h, w)` + """ + B, S, C, T, H, W = orig_shape + D = self.embed_dim + + # num patches in each dimension + t = T // self.patch_embed_3d.z_block_size + h = self.patch_embed_3d.height + w = self.patch_embed_3d.width + + feats = feats.permute(0, 2, 1) # (B*S, D, T) + feats = feats.view(B * S, D, t, h, w) # (B*S, D, t, h, w) + + return feats + + +class BaseEncoderLayer(nn.TransformerEncoderLayer): + """ + This is a wrapper around nn.TransformerEncoderLayer that adds a CLS token + to the sequence and outputs the CLS token's representation. + This base class parents both SpatialEncoderLayer and TemporalEncoderLayer for the RGB stream + and the FrequencyEncoderLayer and TemporalEncoderLayer for the audio stream stream. + We also, optionally, add a positional embedding to the input sequence which + allows to reuse it for global aggregation (of segments) for both streams. + """ + + def __init__( + self, + add_pos_emb: bool = False, + pos_emb_drop: float = None, + pos_max_len: int = None, + *args_transformer_enc, + **kwargs_transformer_enc, + ): + super().__init__(*args_transformer_enc, **kwargs_transformer_enc) + self.cls_token = nn.Parameter(torch.zeros(1, 1, self.self_attn.embed_dim)) + trunc_normal_(self.cls_token, std=0.02) + + # add positional embedding + self.add_pos_emb = add_pos_emb + if add_pos_emb: + self.pos_max_len = 1 + pos_max_len # +1 (for CLS) + self.pos_emb = nn.Parameter(torch.zeros(1, self.pos_max_len, self.self_attn.embed_dim)) + self.pos_drop = nn.Dropout(pos_emb_drop) + trunc_normal_(self.pos_emb, std=0.02) + + self.apply(self._init_weights) + + def forward(self, x: torch.Tensor, x_mask: torch.Tensor = None): + """x is of shape (B, N, D); if provided x_mask is of shape (B, N)""" + batch_dim = x.shape[0] + + # add CLS token + cls_tokens = self.cls_token.expand(batch_dim, -1, -1) # expanding to match batch dimension + x = torch.cat((cls_tokens, x), dim=-2) # (batch_dim, 1+seq_len, D) + if x_mask is not None: + cls_mask = torch.ones((batch_dim, 1), dtype=torch.bool, device=x_mask.device) # 1=keep; 0=mask + x_mask_w_cls = torch.cat((cls_mask, x_mask), dim=-1) # (batch_dim, 1+seq_len) + B, N = x_mask_w_cls.shape + # torch expects (N, N) or (B*num_heads, N, N) mask (sadness ahead); torch masks + x_mask_w_cls = ( + x_mask_w_cls.reshape(B, 1, 1, N) + .expand(-1, self.self_attn.num_heads, N, -1) + .reshape(B * self.self_attn.num_heads, N, N) + ) + assert x_mask_w_cls.dtype == x_mask_w_cls.bool().dtype, "x_mask_w_cls.dtype != bool" + x_mask_w_cls = ~x_mask_w_cls # invert mask (1=mask) + else: + x_mask_w_cls = None + + # add positional embedding + if self.add_pos_emb: + seq_len = x.shape[1] # (don't even think about moving it before the CLS token concatenation) + assert seq_len <= self.pos_max_len, f"Seq len ({seq_len}) > pos_max_len ({self.pos_max_len})" + x = x + self.pos_emb[:, :seq_len, :] + x = self.pos_drop(x) + + # apply encoder layer (calls nn.TransformerEncoderLayer.forward); + x = super().forward(src=x, src_mask=x_mask_w_cls) # (batch_dim, 1+seq_len, D) + + # CLS token is expected to hold spatial information for each frame + x = x[:, 0, :] # (batch_dim, D) + + return x + + def _init_weights(self, m): + if isinstance(m, nn.Linear): + trunc_normal_(m.weight, std=0.02) + if isinstance(m, nn.Linear) and m.bias is not None: + nn.init.constant_(m.bias, 0) + elif isinstance(m, nn.LayerNorm): + nn.init.constant_(m.bias, 0) + nn.init.constant_(m.weight, 1.0) + + @torch.jit.ignore + def no_weight_decay(self): + return {"cls_token", "pos_emb"} + + +class SpatialTransformerEncoderLayer(BaseEncoderLayer): + """Aggregates spatial dimensions by applying attention individually to each frame.""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def forward(self, x: torch.Tensor, x_mask: torch.Tensor = None) -> torch.Tensor: + """x is of shape (B*S, D, t, h, w) where S is the number of segments. + if specified x_mask (B*S, t, h, w), 0=masked, 1=kept + Returns a tensor of shape (B*S, t, D) pooling spatial information for each frame.""" + BS, D, t, h, w = x.shape + + # time as a batch dimension and flatten spatial dimensions as sequence + x = einops.rearrange(x, "BS D t h w -> (BS t) (h w) D") + # similar to mask + if x_mask is not None: + x_mask = einops.rearrange(x_mask, "BS t h w -> (BS t) (h w)") + + # apply encoder layer (BaseEncoderLayer.forward) - it will add CLS token and output its representation + x = super().forward(x=x, x_mask=x_mask) # (B*S*t, D) + + # reshape back to (B*S, t, D) + x = einops.rearrange(x, "(BS t) D -> BS t D", BS=BS, t=t) + + # (B*S, t, D) + return x + + +class TemporalTransformerEncoderLayer(BaseEncoderLayer): + """Aggregates temporal dimension with attention. Also used with pos emb as global aggregation + in both streams.""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def forward(self, x): + """x is of shape (B*S, t, D) where S is the number of segments. + Returns a tensor of shape (B*S, D) pooling temporal information.""" + BS, t, D = x.shape + + # apply encoder layer (BaseEncoderLayer.forward) - it will add CLS token and output its representation + x = super().forward(x) # (B*S, D) + + return x # (B*S, D) + + +class AveragePooling(nn.Module): + + def __init__(self, avg_pattern: str, then_permute_pattern: str = None) -> None: + """patterns are e.g. "bs t d -> bs d" """ + super().__init__() + # TODO: need to register them as buffers (but fails because these are strings) + self.reduce_fn = "mean" + self.avg_pattern = avg_pattern + self.then_permute_pattern = then_permute_pattern + + def forward(self, x: torch.Tensor, x_mask: torch.Tensor = None) -> torch.Tensor: + x = einops.reduce(x, self.avg_pattern, self.reduce_fn) + if self.then_permute_pattern is not None: + x = einops.rearrange(x, self.then_permute_pattern) + return x diff --git a/hunyuanvideo_foley/models/synchformer/synchformer.py b/hunyuanvideo_foley/models/synchformer/synchformer.py new file mode 100644 index 0000000..e7a7317 --- /dev/null +++ b/hunyuanvideo_foley/models/synchformer/synchformer.py @@ -0,0 +1,355 @@ +import logging +import math +from typing import Any, Mapping + +import einops +import numpy as np +import torch +import torchaudio +from torch import nn +from torch.nn import functional as F + +from .motionformer import MotionFormer +from .ast_model import AST +from .utils import Config + + +class Synchformer(nn.Module): + + def __init__(self): + super().__init__() + + self.vfeat_extractor = MotionFormer( + extract_features=True, + factorize_space_time=True, + agg_space_module="TransformerEncoderLayer", + agg_time_module="torch.nn.Identity", + add_global_repr=False, + ) + self.afeat_extractor = AST( + extract_features=True, + max_spec_t=66, + factorize_freq_time=True, + agg_freq_module="TransformerEncoderLayer", + agg_time_module="torch.nn.Identity", + add_global_repr=False, + ) + + # # bridging the s3d latent dim (1024) into what is specified in the config + # # to match e.g. the transformer dim + self.vproj = nn.Linear(in_features=768, out_features=768) + self.aproj = nn.Linear(in_features=768, out_features=768) + self.transformer = GlobalTransformer( + tok_pdrop=0.0, embd_pdrop=0.1, resid_pdrop=0.1, attn_pdrop=0.1, n_layer=3, n_head=8, n_embd=768 + ) + + def forward(self, vis): + B, S, Tv, C, H, W = vis.shape + vis = vis.permute(0, 1, 3, 2, 4, 5) # (B, S, C, Tv, H, W) + # feat extractors return a tuple of segment-level and global features (ignored for sync) + # (B, S, tv, D), e.g. (B, 7, 8, 768) + vis = self.vfeat_extractor(vis) + return vis + + def compare_v_a(self, vis: torch.Tensor, aud: torch.Tensor): + vis = self.vproj(vis) + aud = self.aproj(aud) + + B, S, tv, D = vis.shape + B, S, ta, D = aud.shape + vis = vis.view(B, S * tv, D) # (B, S*tv, D) + aud = aud.view(B, S * ta, D) # (B, S*ta, D) + # print(vis.shape, aud.shape) + + # self.transformer will concatenate the vis and aud in one sequence with aux tokens, + # ie `CvvvvMaaaaaa`, and will return the logits for the CLS tokens + logits = self.transformer(vis, aud) # (B, cls); or (B, cls) and (B, 2) if DoubtingTransformer + + return logits + + def extract_vfeats(self, vis): + B, S, Tv, C, H, W = vis.shape + vis = vis.permute(0, 1, 3, 2, 4, 5) # (B, S, C, Tv, H, W) + # feat extractors return a tuple of segment-level and global features (ignored for sync) + # (B, S, tv, D), e.g. (B, 7, 8, 768) + vis = self.vfeat_extractor(vis) + return vis + + def extract_afeats(self, aud): + B, S, _, Fa, Ta = aud.shape + aud = aud.view(B, S, Fa, Ta).permute(0, 1, 3, 2) # (B, S, Ta, F) + # (B, S, ta, D), e.g. (B, 7, 6, 768) + aud, _ = self.afeat_extractor(aud) + return aud + + def compute_loss(self, logits, targets, loss_fn: str = None): + loss = None + if targets is not None: + if loss_fn is None or loss_fn == "cross_entropy": + # logits: (B, cls) and targets: (B,) + loss = F.cross_entropy(logits, targets) + else: + raise NotImplementedError(f"Loss {loss_fn} not implemented") + return loss + + def load_state_dict(self, sd: Mapping[str, Any], strict: bool = True): + # discard all entries except vfeat_extractor + # sd = {k: v for k, v in sd.items() if k.startswith('vfeat_extractor')} + + return super().load_state_dict(sd, strict) + + +class RandInitPositionalEncoding(nn.Module): + """Random inited trainable pos embedding. It is just applied on the sequence, thus respects no priors.""" + + def __init__(self, block_shape: list, n_embd: int): + super().__init__() + self.block_shape = block_shape + self.n_embd = n_embd + self.pos_emb = nn.Parameter(torch.randn(1, *block_shape, n_embd)) + + def forward(self, token_embeddings): + return token_embeddings + self.pos_emb + + +class GlobalTransformer(torch.nn.Module): + """Same as in SparseSync but without the selector transformers and the head""" + + def __init__( + self, + tok_pdrop=0.0, + embd_pdrop=0.1, + resid_pdrop=0.1, + attn_pdrop=0.1, + n_layer=3, + n_head=8, + n_embd=768, + pos_emb_block_shape=[ + 198, + ], + n_off_head_out=21, + ) -> None: + super().__init__() + self.config = Config( + embd_pdrop=embd_pdrop, + resid_pdrop=resid_pdrop, + attn_pdrop=attn_pdrop, + n_layer=n_layer, + n_head=n_head, + n_embd=n_embd, + ) + # input norm + self.vis_in_lnorm = torch.nn.LayerNorm(n_embd) + self.aud_in_lnorm = torch.nn.LayerNorm(n_embd) + # aux tokens + self.OFF_tok = torch.nn.Parameter(torch.randn(1, 1, n_embd)) + self.MOD_tok = torch.nn.Parameter(torch.randn(1, 1, n_embd)) + # whole token dropout + self.tok_pdrop = tok_pdrop + self.tok_drop_vis = torch.nn.Dropout1d(tok_pdrop) + self.tok_drop_aud = torch.nn.Dropout1d(tok_pdrop) + # maybe add pos emb + self.pos_emb_cfg = RandInitPositionalEncoding( + block_shape=pos_emb_block_shape, + n_embd=n_embd, + ) + # the stem + self.drop = torch.nn.Dropout(embd_pdrop) + self.blocks = torch.nn.Sequential(*[Block(self.config) for _ in range(n_layer)]) + # pre-output norm + self.ln_f = torch.nn.LayerNorm(n_embd) + # maybe add a head + self.off_head = torch.nn.Linear(in_features=n_embd, out_features=n_off_head_out) + + def forward(self, v: torch.Tensor, a: torch.Tensor, targets=None, attempt_to_apply_heads=True): + B, Sv, D = v.shape + B, Sa, D = a.shape + # broadcasting special tokens to the batch size + off_tok = einops.repeat(self.OFF_tok, "1 1 d -> b 1 d", b=B) + mod_tok = einops.repeat(self.MOD_tok, "1 1 d -> b 1 d", b=B) + # norm + v, a = self.vis_in_lnorm(v), self.aud_in_lnorm(a) + # maybe whole token dropout + if self.tok_pdrop > 0: + v, a = self.tok_drop_vis(v), self.tok_drop_aud(a) + # (B, 1+Sv+1+Sa, D) + x = torch.cat((off_tok, v, mod_tok, a), dim=1) + # maybe add pos emb + if hasattr(self, "pos_emb_cfg"): + x = self.pos_emb_cfg(x) + # dropout -> stem -> norm + x = self.drop(x) + x = self.blocks(x) + x = self.ln_f(x) + # maybe add heads + if attempt_to_apply_heads and hasattr(self, "off_head"): + x = self.off_head(x[:, 0, :]) + return x + + +class SelfAttention(nn.Module): + """ + A vanilla multi-head masked self-attention layer with a projection at the end. + It is possible to use torch.nn.MultiheadAttention here but I am including an + explicit implementation here to show that there is nothing too scary here. + """ + + def __init__(self, config): + super().__init__() + assert config.n_embd % config.n_head == 0 + # key, query, value projections for all heads + self.key = nn.Linear(config.n_embd, config.n_embd) + self.query = nn.Linear(config.n_embd, config.n_embd) + self.value = nn.Linear(config.n_embd, config.n_embd) + # regularization + self.attn_drop = nn.Dropout(config.attn_pdrop) + self.resid_drop = nn.Dropout(config.resid_pdrop) + # output projection + self.proj = nn.Linear(config.n_embd, config.n_embd) + # # causal mask to ensure that attention is only applied to the left in the input sequence + # mask = torch.tril(torch.ones(config.block_size, + # config.block_size)) + # if hasattr(config, "n_unmasked"): + # mask[:config.n_unmasked, :config.n_unmasked] = 1 + # self.register_buffer("mask", mask.view(1, 1, config.block_size, config.block_size)) + self.n_head = config.n_head + + def forward(self, x): + B, T, C = x.size() + + # calculate query, key, values for all heads in batch and move head forward to be the batch dim + k = self.key(x).view(B, T, self.n_head, C // self.n_head).transpose(1, 2) # (B, nh, T, hs) + q = self.query(x).view(B, T, self.n_head, C // self.n_head).transpose(1, 2) # (B, nh, T, hs) + v = self.value(x).view(B, T, self.n_head, C // self.n_head).transpose(1, 2) # (B, nh, T, hs) + + # self-attention; Self-attend: (B, nh, T, hs) x (B, nh, hs, T) -> (B, nh, T, T) + att = (q @ k.transpose(-2, -1)) * (1.0 / math.sqrt(k.size(-1))) + # att = att.masked_fill(self.mask[:, :, :T, :T] == 0, float('-inf')) + att = F.softmax(att, dim=-1) + y = self.attn_drop(att) @ v # (B, nh, T, T) x (B, nh, T, hs) -> (B, nh, T, hs) + y = y.transpose(1, 2).contiguous().view(B, T, C) # re-assemble all head outputs side by side + + # output projection + y = self.resid_drop(self.proj(y)) + + return y + + +class Block(nn.Module): + """an unassuming Transformer block""" + + def __init__(self, config): + super().__init__() + self.ln1 = nn.LayerNorm(config.n_embd) + self.ln2 = nn.LayerNorm(config.n_embd) + self.attn = SelfAttention(config) + self.mlp = nn.Sequential( + nn.Linear(config.n_embd, 4 * config.n_embd), + nn.GELU(), # nice + nn.Linear(4 * config.n_embd, config.n_embd), + nn.Dropout(config.resid_pdrop), + ) + + def forward(self, x): + x = x + self.attn(self.ln1(x)) + x = x + self.mlp(self.ln2(x)) + return x + + +def make_class_grid( + leftmost_val, + rightmost_val, + grid_size, + add_extreme_offset: bool = False, + seg_size_vframes: int = None, + nseg: int = None, + step_size_seg: float = None, + vfps: float = None, +): + assert grid_size >= 3, f"grid_size: {grid_size} doesnot make sense. If =2 -> (-1,1); =1 -> (-1); =0 -> ()" + grid = torch.from_numpy(np.linspace(leftmost_val, rightmost_val, grid_size)).float() + if add_extreme_offset: + assert all([seg_size_vframes, nseg, step_size_seg]), f"{seg_size_vframes} {nseg} {step_size_seg}" + seg_size_sec = seg_size_vframes / vfps + trim_size_in_seg = nseg - (1 - step_size_seg) * (nseg - 1) + extreme_value = trim_size_in_seg * seg_size_sec + grid = torch.cat([grid, torch.tensor([extreme_value])]) # adding extreme offset to the class grid + return grid + + +# from synchformer +def pad_or_truncate(audio: torch.Tensor, max_spec_t: int, pad_mode: str = "constant", pad_value: float = 0.0): + difference = max_spec_t - audio.shape[-1] # safe for batched input + # pad or truncate, depending on difference + if difference > 0: + # pad the last dim (time) -> (..., n_mels, 0+time+difference) # safe for batched input + pad_dims = (0, difference) + audio = torch.nn.functional.pad(audio, pad_dims, pad_mode, pad_value) + elif difference < 0: + print(f"Truncating spec ({audio.shape}) to max_spec_t ({max_spec_t}).") + audio = audio[..., :max_spec_t] # safe for batched input + return audio + + +def encode_audio_with_sync( + synchformer: Synchformer, x: torch.Tensor, mel: torchaudio.transforms.MelSpectrogram +) -> torch.Tensor: + b, t = x.shape + + # partition the video + segment_size = 10240 + step_size = 10240 // 2 + num_segments = (t - segment_size) // step_size + 1 + segments = [] + for i in range(num_segments): + segments.append(x[:, i * step_size : i * step_size + segment_size]) + x = torch.stack(segments, dim=1) # (B, S, T, C, H, W) + + x = mel(x) + x = torch.log(x + 1e-6) + x = pad_or_truncate(x, 66) + + mean = -4.2677393 + std = 4.5689974 + x = (x - mean) / (2 * std) + # x: B * S * 128 * 66 + x = synchformer.extract_afeats(x.unsqueeze(2)) + return x + + +def read_audio(filename, expected_length=int(16000 * 4)): + waveform, sr = torchaudio.load(filename) + waveform = waveform.mean(dim=0) + + if sr != 16000: + resampler = torchaudio.transforms.Resample(sr, 16000) + waveform = resampler[sr](waveform) + + waveform = waveform[:expected_length] + if waveform.shape[0] != expected_length: + raise ValueError(f"Audio {filename} is too short") + + waveform = waveform.squeeze() + + return waveform + + +if __name__ == "__main__": + synchformer = Synchformer().cuda().eval() + + # mmaudio provided synchformer ckpt + synchformer.load_state_dict( + torch.load( + os.environ.get("SYNCHFORMER_WEIGHTS", f"weights/synchformer.pth"), + weights_only=True, + map_location="cpu", + ) + ) + + sync_mel_spectrogram = torchaudio.transforms.MelSpectrogram( + sample_rate=16000, + win_length=400, + hop_length=160, + n_fft=1024, + n_mels=128, + ) diff --git a/hunyuanvideo_foley/models/synchformer/utils.py b/hunyuanvideo_foley/models/synchformer/utils.py new file mode 100644 index 0000000..b13e8aa --- /dev/null +++ b/hunyuanvideo_foley/models/synchformer/utils.py @@ -0,0 +1,87 @@ +from hashlib import md5 +from pathlib import Path +import subprocess + +import requests +from tqdm import tqdm + +PARENT_LINK = "https://a3s.fi/swift/v1/AUTH_a235c0f452d648828f745589cde1219a" +FNAME2LINK = { + # S3: Synchability: AudioSet (run 2) + "24-01-22T20-34-52.pt": f"{PARENT_LINK}/sync/sync_models/24-01-22T20-34-52/24-01-22T20-34-52.pt", + "cfg-24-01-22T20-34-52.yaml": f"{PARENT_LINK}/sync/sync_models/24-01-22T20-34-52/cfg-24-01-22T20-34-52.yaml", + # S2: Synchformer: AudioSet (run 2) + "24-01-04T16-39-21.pt": f"{PARENT_LINK}/sync/sync_models/24-01-04T16-39-21/24-01-04T16-39-21.pt", + "cfg-24-01-04T16-39-21.yaml": f"{PARENT_LINK}/sync/sync_models/24-01-04T16-39-21/cfg-24-01-04T16-39-21.yaml", + # S2: Synchformer: AudioSet (run 1) + "23-08-28T11-23-23.pt": f"{PARENT_LINK}/sync/sync_models/23-08-28T11-23-23/23-08-28T11-23-23.pt", + "cfg-23-08-28T11-23-23.yaml": f"{PARENT_LINK}/sync/sync_models/23-08-28T11-23-23/cfg-23-08-28T11-23-23.yaml", + # S2: Synchformer: LRS3 (run 2) + "23-12-23T18-33-57.pt": f"{PARENT_LINK}/sync/sync_models/23-12-23T18-33-57/23-12-23T18-33-57.pt", + "cfg-23-12-23T18-33-57.yaml": f"{PARENT_LINK}/sync/sync_models/23-12-23T18-33-57/cfg-23-12-23T18-33-57.yaml", + # S2: Synchformer: VGS (run 2) + "24-01-02T10-00-53.pt": f"{PARENT_LINK}/sync/sync_models/24-01-02T10-00-53/24-01-02T10-00-53.pt", + "cfg-24-01-02T10-00-53.yaml": f"{PARENT_LINK}/sync/sync_models/24-01-02T10-00-53/cfg-24-01-02T10-00-53.yaml", + # SparseSync: ft VGGSound-Full + "22-09-21T21-00-52.pt": f"{PARENT_LINK}/sync/sync_models/22-09-21T21-00-52/22-09-21T21-00-52.pt", + "cfg-22-09-21T21-00-52.yaml": f"{PARENT_LINK}/sync/sync_models/22-09-21T21-00-52/cfg-22-09-21T21-00-52.yaml", + # SparseSync: ft VGGSound-Sparse + "22-07-28T15-49-45.pt": f"{PARENT_LINK}/sync/sync_models/22-07-28T15-49-45/22-07-28T15-49-45.pt", + "cfg-22-07-28T15-49-45.yaml": f"{PARENT_LINK}/sync/sync_models/22-07-28T15-49-45/cfg-22-07-28T15-49-45.yaml", + # SparseSync: only pt on LRS3 + "22-07-13T22-25-49.pt": f"{PARENT_LINK}/sync/sync_models/22-07-13T22-25-49/22-07-13T22-25-49.pt", + "cfg-22-07-13T22-25-49.yaml": f"{PARENT_LINK}/sync/sync_models/22-07-13T22-25-49/cfg-22-07-13T22-25-49.yaml", + # SparseSync: feature extractors + "ResNetAudio-22-08-04T09-51-04.pt": f"{PARENT_LINK}/sync/ResNetAudio-22-08-04T09-51-04.pt", # 2s + "ResNetAudio-22-08-03T23-14-49.pt": f"{PARENT_LINK}/sync/ResNetAudio-22-08-03T23-14-49.pt", # 3s + "ResNetAudio-22-08-03T23-14-28.pt": f"{PARENT_LINK}/sync/ResNetAudio-22-08-03T23-14-28.pt", # 4s + "ResNetAudio-22-06-24T08-10-33.pt": f"{PARENT_LINK}/sync/ResNetAudio-22-06-24T08-10-33.pt", # 5s + "ResNetAudio-22-06-24T17-31-07.pt": f"{PARENT_LINK}/sync/ResNetAudio-22-06-24T17-31-07.pt", # 6s + "ResNetAudio-22-06-24T23-57-11.pt": f"{PARENT_LINK}/sync/ResNetAudio-22-06-24T23-57-11.pt", # 7s + "ResNetAudio-22-06-25T04-35-42.pt": f"{PARENT_LINK}/sync/ResNetAudio-22-06-25T04-35-42.pt", # 8s +} + + +def check_if_file_exists_else_download(path, fname2link=FNAME2LINK, chunk_size=1024): + """Checks if file exists, if not downloads it from the link to the path""" + path = Path(path) + if not path.exists(): + path.parent.mkdir(exist_ok=True, parents=True) + link = fname2link.get(path.name, None) + if link is None: + raise ValueError( + f"Cant find the checkpoint file: {path}.", f"Please download it manually and ensure the path exists." + ) + with requests.get(fname2link[path.name], stream=True) as r: + total_size = int(r.headers.get("content-length", 0)) + with tqdm(total=total_size, unit="B", unit_scale=True) as pbar: + with open(path, "wb") as f: + for data in r.iter_content(chunk_size=chunk_size): + if data: + f.write(data) + pbar.update(chunk_size) + + +def which_ffmpeg() -> str: + """Determines the path to ffmpeg library + Returns: + str -- path to the library + """ + result = subprocess.run(["which", "ffmpeg"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + ffmpeg_path = result.stdout.decode("utf-8").replace("\n", "") + return ffmpeg_path + + +def get_md5sum(path): + hash_md5 = md5() + with open(path, "rb") as f: + for chunk in iter(lambda: f.read(4096 * 8), b""): + hash_md5.update(chunk) + md5sum = hash_md5.hexdigest() + return md5sum + + +class Config: + def __init__(self, **kwargs): + for k, v in kwargs.items(): + setattr(self, k, v) diff --git a/hunyuanvideo_foley/models/synchformer/video_model_builder.py b/hunyuanvideo_foley/models/synchformer/video_model_builder.py new file mode 100644 index 0000000..eb63e4d --- /dev/null +++ b/hunyuanvideo_foley/models/synchformer/video_model_builder.py @@ -0,0 +1,270 @@ +#!/usr/bin/env python3 +# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. +# Copyright 2020 Ross Wightman +# Modified Model definition + +from collections import OrderedDict +from functools import partial + +import torch +import torch.nn as nn +from timm.layers import trunc_normal_ + +from .vit_helper import PatchEmbed, PatchEmbed3D, DividedSpaceTimeBlock + + +class VisionTransformer(nn.Module): + """Vision Transformer with support for patch or hybrid CNN input stage""" + + def __init__(self, cfg): + super().__init__() + self.img_size = cfg.DATA.TRAIN_CROP_SIZE + self.patch_size = cfg.VIT.PATCH_SIZE + self.in_chans = cfg.VIT.CHANNELS + if cfg.TRAIN.DATASET == "Epickitchens": + self.num_classes = [97, 300] + else: + self.num_classes = cfg.MODEL.NUM_CLASSES + self.embed_dim = cfg.VIT.EMBED_DIM + self.depth = cfg.VIT.DEPTH + self.num_heads = cfg.VIT.NUM_HEADS + self.mlp_ratio = cfg.VIT.MLP_RATIO + self.qkv_bias = cfg.VIT.QKV_BIAS + self.drop_rate = cfg.VIT.DROP + self.drop_path_rate = cfg.VIT.DROP_PATH + self.head_dropout = cfg.VIT.HEAD_DROPOUT + self.video_input = cfg.VIT.VIDEO_INPUT + self.temporal_resolution = cfg.VIT.TEMPORAL_RESOLUTION + self.use_mlp = cfg.VIT.USE_MLP + self.num_features = self.embed_dim + norm_layer = partial(nn.LayerNorm, eps=1e-6) + self.attn_drop_rate = cfg.VIT.ATTN_DROPOUT + self.head_act = cfg.VIT.HEAD_ACT + self.cfg = cfg + + # Patch Embedding + self.patch_embed = PatchEmbed( + img_size=224, patch_size=self.patch_size, in_chans=self.in_chans, embed_dim=self.embed_dim + ) + + # 3D Patch Embedding + self.patch_embed_3d = PatchEmbed3D( + img_size=self.img_size, + temporal_resolution=self.temporal_resolution, + patch_size=self.patch_size, + in_chans=self.in_chans, + embed_dim=self.embed_dim, + z_block_size=self.cfg.VIT.PATCH_SIZE_TEMP, + ) + self.patch_embed_3d.proj.weight.data = torch.zeros_like(self.patch_embed_3d.proj.weight.data) + + # Number of patches + if self.video_input: + num_patches = self.patch_embed.num_patches * self.temporal_resolution + else: + num_patches = self.patch_embed.num_patches + self.num_patches = num_patches + + # CLS token + self.cls_token = nn.Parameter(torch.zeros(1, 1, self.embed_dim)) + trunc_normal_(self.cls_token, std=0.02) + + # Positional embedding + self.pos_embed = nn.Parameter(torch.zeros(1, self.patch_embed.num_patches + 1, self.embed_dim)) + self.pos_drop = nn.Dropout(p=cfg.VIT.POS_DROPOUT) + trunc_normal_(self.pos_embed, std=0.02) + + if self.cfg.VIT.POS_EMBED == "joint": + self.st_embed = nn.Parameter(torch.zeros(1, num_patches + 1, self.embed_dim)) + trunc_normal_(self.st_embed, std=0.02) + elif self.cfg.VIT.POS_EMBED == "separate": + self.temp_embed = nn.Parameter(torch.zeros(1, self.temporal_resolution, self.embed_dim)) + + # Layer Blocks + dpr = [x.item() for x in torch.linspace(0, self.drop_path_rate, self.depth)] + if self.cfg.VIT.ATTN_LAYER == "divided": + self.blocks = nn.ModuleList( + [ + DividedSpaceTimeBlock( + attn_type=cfg.VIT.ATTN_LAYER, + dim=self.embed_dim, + num_heads=self.num_heads, + mlp_ratio=self.mlp_ratio, + qkv_bias=self.qkv_bias, + drop=self.drop_rate, + attn_drop=self.attn_drop_rate, + drop_path=dpr[i], + norm_layer=norm_layer, + ) + for i in range(self.depth) + ] + ) + + self.norm = norm_layer(self.embed_dim) + + # MLP head + if self.use_mlp: + hidden_dim = self.embed_dim + if self.head_act == "tanh": + # logging.info("Using TanH activation in MLP") + act = nn.Tanh() + elif self.head_act == "gelu": + # logging.info("Using GELU activation in MLP") + act = nn.GELU() + else: + # logging.info("Using ReLU activation in MLP") + act = nn.ReLU() + self.pre_logits = nn.Sequential( + OrderedDict( + [ + ("fc", nn.Linear(self.embed_dim, hidden_dim)), + ("act", act), + ] + ) + ) + else: + self.pre_logits = nn.Identity() + + # Classifier Head + self.head_drop = nn.Dropout(p=self.head_dropout) + if isinstance(self.num_classes, (list,)) and len(self.num_classes) > 1: + for a, i in enumerate(range(len(self.num_classes))): + setattr(self, "head%d" % a, nn.Linear(self.embed_dim, self.num_classes[i])) + else: + self.head = nn.Linear(self.embed_dim, self.num_classes) if self.num_classes > 0 else nn.Identity() + + # Initialize weights + self.apply(self._init_weights) + + def _init_weights(self, m): + if isinstance(m, nn.Linear): + trunc_normal_(m.weight, std=0.02) + if isinstance(m, nn.Linear) and m.bias is not None: + nn.init.constant_(m.bias, 0) + elif isinstance(m, nn.LayerNorm): + nn.init.constant_(m.bias, 0) + nn.init.constant_(m.weight, 1.0) + + @torch.jit.ignore + def no_weight_decay(self): + if self.cfg.VIT.POS_EMBED == "joint": + return {"pos_embed", "cls_token", "st_embed"} + else: + return {"pos_embed", "cls_token", "temp_embed"} + + def get_classifier(self): + return self.head + + def reset_classifier(self, num_classes, global_pool=""): + self.num_classes = num_classes + self.head = nn.Linear(self.embed_dim, num_classes) if num_classes > 0 else nn.Identity() + + def forward_features(self, x): + # if self.video_input: + # x = x[0] + B = x.shape[0] + + # Tokenize input + # if self.cfg.VIT.PATCH_SIZE_TEMP > 1: + # for simplicity of mapping between content dimensions (input x) and token dims (after patching) + # we use the same trick as for AST (see modeling_ast.ASTModel.forward for the details): + + # apply patching on input + x = self.patch_embed_3d(x) + tok_mask = None + + # else: + # tok_mask = None + # # 2D tokenization + # if self.video_input: + # x = x.permute(0, 2, 1, 3, 4) + # (B, T, C, H, W) = x.shape + # x = x.reshape(B * T, C, H, W) + + # x = self.patch_embed(x) + + # if self.video_input: + # (B2, T2, D2) = x.shape + # x = x.reshape(B, T * T2, D2) + + # Append CLS token + cls_tokens = self.cls_token.expand(B, -1, -1) + x = torch.cat((cls_tokens, x), dim=1) + # if tok_mask is not None: + # # prepend 1(=keep) to the mask to account for the CLS token as well + # tok_mask = torch.cat((torch.ones_like(tok_mask[:, [0]]), tok_mask), dim=1) + + # Interpolate positinoal embeddings + # if self.cfg.DATA.TRAIN_CROP_SIZE != 224: + # pos_embed = self.pos_embed + # N = pos_embed.shape[1] - 1 + # npatch = int((x.size(1) - 1) / self.temporal_resolution) + # class_emb = pos_embed[:, 0] + # pos_embed = pos_embed[:, 1:] + # dim = x.shape[-1] + # pos_embed = torch.nn.functional.interpolate( + # pos_embed.reshape(1, int(math.sqrt(N)), int(math.sqrt(N)), dim).permute(0, 3, 1, 2), + # scale_factor=math.sqrt(npatch / N), + # mode='bicubic', + # ) + # pos_embed = pos_embed.permute(0, 2, 3, 1).view(1, -1, dim) + # new_pos_embed = torch.cat((class_emb.unsqueeze(0), pos_embed), dim=1) + # else: + new_pos_embed = self.pos_embed + npatch = self.patch_embed.num_patches + + # Add positional embeddings to input + if self.video_input: + if self.cfg.VIT.POS_EMBED == "separate": + cls_embed = self.pos_embed[:, 0, :].unsqueeze(1) + tile_pos_embed = new_pos_embed[:, 1:, :].repeat(1, self.temporal_resolution, 1) + tile_temporal_embed = self.temp_embed.repeat_interleave(npatch, 1) + total_pos_embed = tile_pos_embed + tile_temporal_embed + total_pos_embed = torch.cat([cls_embed, total_pos_embed], dim=1) + x = x + total_pos_embed + elif self.cfg.VIT.POS_EMBED == "joint": + x = x + self.st_embed + else: + # image input + x = x + new_pos_embed + + # Apply positional dropout + x = self.pos_drop(x) + + # Encoding using transformer layers + for i, blk in enumerate(self.blocks): + x = blk( + x, + seq_len=npatch, + num_frames=self.temporal_resolution, + approx=self.cfg.VIT.APPROX_ATTN_TYPE, + num_landmarks=self.cfg.VIT.APPROX_ATTN_DIM, + tok_mask=tok_mask, + ) + + ### v-iashin: I moved it to the forward pass + # x = self.norm(x)[:, 0] + # x = self.pre_logits(x) + ### + return x, tok_mask + + # def forward(self, x): + # x = self.forward_features(x) + # ### v-iashin: here. This should leave the same forward output as before + # x = self.norm(x)[:, 0] + # x = self.pre_logits(x) + # ### + # x = self.head_drop(x) + # if isinstance(self.num_classes, (list, )) and len(self.num_classes) > 1: + # output = [] + # for head in range(len(self.num_classes)): + # x_out = getattr(self, "head%d" % head)(x) + # if not self.training: + # x_out = torch.nn.functional.softmax(x_out, dim=-1) + # output.append(x_out) + # return output + # else: + # x = self.head(x) + # if not self.training: + # x = torch.nn.functional.softmax(x, dim=-1) + # return x diff --git a/hunyuanvideo_foley/models/synchformer/vit_helper.py b/hunyuanvideo_foley/models/synchformer/vit_helper.py new file mode 100644 index 0000000..9d54794 --- /dev/null +++ b/hunyuanvideo_foley/models/synchformer/vit_helper.py @@ -0,0 +1,364 @@ +#!/usr/bin/env python3 +# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. +# Copyright 2020 Ross Wightman +# Modified Model definition +"""Video models.""" + +import math + +import torch +import torch.nn as nn +from einops import rearrange, repeat +from timm.layers import to_2tuple +from torch import einsum +from torch.nn import functional as F + +default_cfgs = { + "vit_1k": "https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-vitjx/jx_vit_base_p16_224-80ecf9dd.pth", + "vit_1k_large": "https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-vitjx/jx_vit_large_p16_224-4ee7a4dc.pth", +} + + +def qkv_attn(q, k, v, tok_mask: torch.Tensor = None): + sim = einsum("b i d, b j d -> b i j", q, k) + # apply masking if provided, tok_mask is (B*S*H, N): 1s - keep; sim is (B*S*H, H, N, N) + if tok_mask is not None: + BSH, N = tok_mask.shape + sim = sim.masked_fill(tok_mask.view(BSH, 1, N) == 0, float("-inf")) # 1 - broadcasts across N + attn = sim.softmax(dim=-1) + out = einsum("b i j, b j d -> b i d", attn, v) + return out + + +class DividedAttention(nn.Module): + + def __init__(self, dim, num_heads=8, qkv_bias=False, attn_drop=0.0, proj_drop=0.0): + super().__init__() + self.num_heads = num_heads + head_dim = dim // num_heads + self.scale = head_dim**-0.5 + self.qkv = nn.Linear(dim, dim * 3, bias=qkv_bias) + self.proj = nn.Linear(dim, dim) + + # init to zeros + self.qkv.weight.data.fill_(0) + self.qkv.bias.data.fill_(0) + self.proj.weight.data.fill_(1) + self.proj.bias.data.fill_(0) + + self.attn_drop = nn.Dropout(attn_drop) + self.proj_drop = nn.Dropout(proj_drop) + + def forward(self, x, einops_from, einops_to, tok_mask: torch.Tensor = None, **einops_dims): + # num of heads variable + h = self.num_heads + + # project x to q, k, v vaalues + q, k, v = self.qkv(x).chunk(3, dim=-1) + q, k, v = map(lambda t: rearrange(t, "b n (h d) -> (b h) n d", h=h), (q, k, v)) + if tok_mask is not None: + # replicate token mask across heads (b, n) -> (b, h, n) -> (b*h, n) -- same as qkv but w/o d + assert len(tok_mask.shape) == 2 + tok_mask = tok_mask.unsqueeze(1).expand(-1, h, -1).reshape(-1, tok_mask.shape[1]) + + # Scale q + q *= self.scale + + # Take out cls_q, cls_k, cls_v + (cls_q, q_), (cls_k, k_), (cls_v, v_) = map(lambda t: (t[:, 0:1], t[:, 1:]), (q, k, v)) + # the same for masking + if tok_mask is not None: + cls_mask, mask_ = tok_mask[:, 0:1], tok_mask[:, 1:] + else: + cls_mask, mask_ = None, None + + # let CLS token attend to key / values of all patches across time and space + cls_out = qkv_attn(cls_q, k, v, tok_mask=tok_mask) + + # rearrange across time or space + q_, k_, v_ = map(lambda t: rearrange(t, f"{einops_from} -> {einops_to}", **einops_dims), (q_, k_, v_)) + + # expand CLS token keys and values across time or space and concat + r = q_.shape[0] // cls_k.shape[0] + cls_k, cls_v = map(lambda t: repeat(t, "b () d -> (b r) () d", r=r), (cls_k, cls_v)) + + k_ = torch.cat((cls_k, k_), dim=1) + v_ = torch.cat((cls_v, v_), dim=1) + + # the same for masking (if provided) + if tok_mask is not None: + # since mask does not have the latent dim (d), we need to remove it from einops dims + mask_ = rearrange(mask_, f"{einops_from} -> {einops_to}".replace(" d", ""), **einops_dims) + cls_mask = repeat(cls_mask, "b () -> (b r) ()", r=r) # expand cls_mask across time or space + mask_ = torch.cat((cls_mask, mask_), dim=1) + + # attention + out = qkv_attn(q_, k_, v_, tok_mask=mask_) + + # merge back time or space + out = rearrange(out, f"{einops_to} -> {einops_from}", **einops_dims) + + # concat back the cls token + out = torch.cat((cls_out, out), dim=1) + + # merge back the heads + out = rearrange(out, "(b h) n d -> b n (h d)", h=h) + + ## to out + x = self.proj(out) + x = self.proj_drop(x) + return x + + +class DividedSpaceTimeBlock(nn.Module): + + def __init__( + self, + dim=768, + num_heads=12, + attn_type="divided", + mlp_ratio=4.0, + qkv_bias=False, + drop=0.0, + attn_drop=0.0, + drop_path=0.0, + act_layer=nn.GELU, + norm_layer=nn.LayerNorm, + ): + super().__init__() + + self.einops_from_space = "b (f n) d" + self.einops_to_space = "(b f) n d" + self.einops_from_time = "b (f n) d" + self.einops_to_time = "(b n) f d" + + self.norm1 = norm_layer(dim) + + self.attn = DividedAttention(dim, num_heads=num_heads, qkv_bias=qkv_bias, attn_drop=attn_drop, proj_drop=drop) + + self.timeattn = DividedAttention( + dim, num_heads=num_heads, qkv_bias=qkv_bias, attn_drop=attn_drop, proj_drop=drop + ) + + # self.drop_path = DropPath(drop_path) if drop_path > 0. else nn.Identity() + self.drop_path = nn.Identity() + self.norm2 = norm_layer(dim) + mlp_hidden_dim = int(dim * mlp_ratio) + self.mlp = Mlp(in_features=dim, hidden_features=mlp_hidden_dim, act_layer=act_layer, drop=drop) + self.norm3 = norm_layer(dim) + + def forward(self, x, seq_len=196, num_frames=8, approx="none", num_landmarks=128, tok_mask: torch.Tensor = None): + time_output = self.timeattn( + self.norm3(x), self.einops_from_time, self.einops_to_time, n=seq_len, tok_mask=tok_mask + ) + time_residual = x + time_output + + space_output = self.attn( + self.norm1(time_residual), self.einops_from_space, self.einops_to_space, f=num_frames, tok_mask=tok_mask + ) + space_residual = time_residual + self.drop_path(space_output) + + x = space_residual + x = x + self.drop_path(self.mlp(self.norm2(x))) + return x + + +class Mlp(nn.Module): + + def __init__(self, in_features, hidden_features=None, out_features=None, act_layer=nn.GELU, drop=0.0): + super().__init__() + out_features = out_features or in_features + hidden_features = hidden_features or in_features + self.fc1 = nn.Linear(in_features, hidden_features) + self.act = act_layer() + self.fc2 = nn.Linear(hidden_features, out_features) + self.drop = nn.Dropout(drop) + + def forward(self, x): + x = self.fc1(x) + x = self.act(x) + x = self.drop(x) + x = self.fc2(x) + x = self.drop(x) + return x + + +class PatchEmbed(nn.Module): + """Image to Patch Embedding""" + + def __init__(self, img_size=224, patch_size=16, in_chans=3, embed_dim=768): + super().__init__() + img_size = img_size if type(img_size) is tuple else to_2tuple(img_size) + patch_size = img_size if type(patch_size) is tuple else to_2tuple(patch_size) + num_patches = (img_size[1] // patch_size[1]) * (img_size[0] // patch_size[0]) + self.img_size = img_size + self.patch_size = patch_size + self.num_patches = num_patches + + self.proj = nn.Conv2d(in_chans, embed_dim, kernel_size=patch_size, stride=patch_size) + + def forward(self, x): + B, C, H, W = x.shape + x = self.proj(x).flatten(2).transpose(1, 2) + return x + + +class PatchEmbed3D(nn.Module): + """Image to Patch Embedding""" + + def __init__( + self, + img_size=224, + temporal_resolution=4, + in_chans=3, + patch_size=16, + z_block_size=2, + embed_dim=768, + flatten=True, + ): + super().__init__() + self.height = img_size // patch_size + self.width = img_size // patch_size + ### v-iashin: these two are incorrect + # self.frames = (temporal_resolution // z_block_size) + # self.num_patches = self.height * self.width * self.frames + self.z_block_size = z_block_size + ### + self.proj = nn.Conv3d( + in_chans, + embed_dim, + kernel_size=(z_block_size, patch_size, patch_size), + stride=(z_block_size, patch_size, patch_size), + ) + self.flatten = flatten + + def forward(self, x): + B, C, T, H, W = x.shape + x = self.proj(x) + if self.flatten: + x = x.flatten(2).transpose(1, 2) + return x + + +class HeadMLP(nn.Module): + + def __init__(self, n_input, n_classes, n_hidden=512, p=0.1): + super(HeadMLP, self).__init__() + self.n_input = n_input + self.n_classes = n_classes + self.n_hidden = n_hidden + if n_hidden is None: + # use linear classifier + self.block_forward = nn.Sequential(nn.Dropout(p=p), nn.Linear(n_input, n_classes, bias=True)) + else: + # use simple MLP classifier + self.block_forward = nn.Sequential( + nn.Dropout(p=p), + nn.Linear(n_input, n_hidden, bias=True), + nn.BatchNorm1d(n_hidden), + nn.ReLU(inplace=True), + nn.Dropout(p=p), + nn.Linear(n_hidden, n_classes, bias=True), + ) + print(f"Dropout-NLP: {p}") + + def forward(self, x): + return self.block_forward(x) + + +def _conv_filter(state_dict, patch_size=16): + """convert patch embedding weight from manual patchify + linear proj to conv""" + out_dict = {} + for k, v in state_dict.items(): + if "patch_embed.proj.weight" in k: + v = v.reshape((v.shape[0], 3, patch_size, patch_size)) + out_dict[k] = v + return out_dict + + +def adapt_input_conv(in_chans, conv_weight, agg="sum"): + conv_type = conv_weight.dtype + conv_weight = conv_weight.float() + O, I, J, K = conv_weight.shape + if in_chans == 1: + if I > 3: + assert conv_weight.shape[1] % 3 == 0 + # For models with space2depth stems + conv_weight = conv_weight.reshape(O, I // 3, 3, J, K) + conv_weight = conv_weight.sum(dim=2, keepdim=False) + else: + if agg == "sum": + print("Summing conv1 weights") + conv_weight = conv_weight.sum(dim=1, keepdim=True) + else: + print("Averaging conv1 weights") + conv_weight = conv_weight.mean(dim=1, keepdim=True) + elif in_chans != 3: + if I != 3: + raise NotImplementedError("Weight format not supported by conversion.") + else: + if agg == "sum": + print("Summing conv1 weights") + repeat = int(math.ceil(in_chans / 3)) + conv_weight = conv_weight.repeat(1, repeat, 1, 1)[:, :in_chans, :, :] + conv_weight *= 3 / float(in_chans) + else: + print("Averaging conv1 weights") + conv_weight = conv_weight.mean(dim=1, keepdim=True) + conv_weight = conv_weight.repeat(1, in_chans, 1, 1) + conv_weight = conv_weight.to(conv_type) + return conv_weight + + +def load_pretrained(model, cfg=None, num_classes=1000, in_chans=3, filter_fn=None, strict=True, progress=False): + # Load state dict + assert f"{cfg.VIT.PRETRAINED_WEIGHTS} not in [vit_1k, vit_1k_large]" + state_dict = torch.hub.load_state_dict_from_url(url=default_cfgs[cfg.VIT.PRETRAINED_WEIGHTS]) + + if filter_fn is not None: + state_dict = filter_fn(state_dict) + + input_convs = "patch_embed.proj" + if input_convs is not None and in_chans != 3: + if isinstance(input_convs, str): + input_convs = (input_convs,) + for input_conv_name in input_convs: + weight_name = input_conv_name + ".weight" + try: + state_dict[weight_name] = adapt_input_conv(in_chans, state_dict[weight_name], agg="avg") + print(f"Converted input conv {input_conv_name} pretrained weights from 3 to {in_chans} channel(s)") + except NotImplementedError as e: + del state_dict[weight_name] + strict = False + print(f"Unable to convert pretrained {input_conv_name} weights, using random init for this layer.") + + classifier_name = "head" + label_offset = cfg.get("label_offset", 0) + pretrain_classes = 1000 + if num_classes != pretrain_classes: + # completely discard fully connected if model num_classes doesn't match pretrained weights + del state_dict[classifier_name + ".weight"] + del state_dict[classifier_name + ".bias"] + strict = False + elif label_offset > 0: + # special case for pretrained weights with an extra background class in pretrained weights + classifier_weight = state_dict[classifier_name + ".weight"] + state_dict[classifier_name + ".weight"] = classifier_weight[label_offset:] + classifier_bias = state_dict[classifier_name + ".bias"] + state_dict[classifier_name + ".bias"] = classifier_bias[label_offset:] + + loaded_state = state_dict + self_state = model.state_dict() + all_names = set(self_state.keys()) + saved_names = set([]) + for name, param in loaded_state.items(): + param = param + if "module." in name: + name = name.replace("module.", "") + if name in self_state.keys() and param.shape == self_state[name].shape: + saved_names.add(name) + self_state[name].copy_(param) + else: + print(f"didnt load: {name} of shape: {param.shape}") + print("Missing Keys:") + print(all_names - saved_names) diff --git a/hunyuanvideo_foley/utils/__init__.py b/hunyuanvideo_foley/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hunyuanvideo_foley/utils/__pycache__/__init__.cpython-312.pyc b/hunyuanvideo_foley/utils/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f803a666076328284bdf315383660797e244ed62 GIT binary patch literal 183 zcmX@j%ge<81YW@#GC=fW5P=Rpvj9b=GgLBYGWxA#C}INgK7-W!3UjfFan8?8s|@vw zNiHoe$#bU#$-T5%0MFVX<(t!lFXdqnE3e2 pyv&mLc)fzkTO2mI`6;D2sdh!IKx-I*xERFv$jr#dSi}ru0RT@#G3WpQ literal 0 HcmV?d00001 diff --git a/hunyuanvideo_foley/utils/__pycache__/config_utils.cpython-312.pyc b/hunyuanvideo_foley/utils/__pycache__/config_utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1b46fb12bd323654a883fcae94186dfd175d35fb GIT binary patch literal 6783 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 literal 0 HcmV?d00001 diff --git a/hunyuanvideo_foley/utils/__pycache__/feature_utils.cpython-312.pyc b/hunyuanvideo_foley/utils/__pycache__/feature_utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..044f6ce5e937adfd836b68dc5945417bf8d25e1b GIT binary patch literal 8429 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 literal 0 HcmV?d00001 diff --git a/hunyuanvideo_foley/utils/__pycache__/helper.cpython-312.pyc b/hunyuanvideo_foley/utils/__pycache__/helper.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..78a205fff6ff1b5de0414432e33e9718415c6934 GIT binary patch literal 6330 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 str: + import re + sanitized = re.sub(r'[^a-zA-Z0-9_]', '_', str(key)) + if sanitized[0].isdigit(): + sanitized = f'_{sanitized}' + return sanitized + + def __getitem__(self, key): + if hasattr(self, '_list'): + return self._list[key] + return getattr(self, self._sanitize_key(key)) + + def __setitem__(self, key, value): + if hasattr(self, '_list'): + self._list[key] = value + else: + setattr(self, self._sanitize_key(key), value) + + def __iter__(self): + if hasattr(self, '_list'): + return iter(self._list) + return iter(self.__dict__.keys()) + + def __len__(self): + if hasattr(self, '_list'): + return len(self._list) + return len(self.__dict__) + + def get(self, key, default=None): + try: + return self[key] + except (KeyError, AttributeError, IndexError): + return default + + def keys(self): + if hasattr(self, '_list'): + return range(len(self._list)) + elif hasattr(self, '_value'): + return [] + else: + return [key for key in self.__dict__.keys() if not key.startswith('_')] + + def values(self): + if hasattr(self, '_list'): + return self._list + elif hasattr(self, '_value'): + return [self._value] + else: + return [value for key, value in self.__dict__.items() if not key.startswith('_')] + + def items(self): + if hasattr(self, '_list'): + return enumerate(self._list) + elif hasattr(self, '_value'): + return [] + else: + return [(key, value) for key, value in self.__dict__.items() if not key.startswith('_')] + + def __repr__(self): + if hasattr(self, '_list'): + return f"AttributeDict({self._list})" + elif hasattr(self, '_value'): + return f"AttributeDict({self._value})" + return f"AttributeDict({dict(self.__dict__)})" + + def to_dict(self) -> Union[Dict, List, Any]: + if hasattr(self, '_list'): + return [item.to_dict() if isinstance(item, AttributeDict) else item + for item in self._list] + elif hasattr(self, '_value'): + return self._value + else: + result = {} + for key, value in self.__dict__.items(): + if isinstance(value, AttributeDict): + result[key] = value.to_dict() + else: + result[key] = value + return result + +def load_yaml(file_path: str, encoding: str = 'utf-8') -> AttributeDict: + try: + with open(file_path, 'r', encoding=encoding) as file: + data = yaml.safe_load(file) + return AttributeDict(data) + except FileNotFoundError: + raise FileNotFoundError(f"YAML file not found: {file_path}") + except yaml.YAMLError as e: + raise yaml.YAMLError(f"YAML format error: {e}") diff --git a/hunyuanvideo_foley/utils/feature_utils.py b/hunyuanvideo_foley/utils/feature_utils.py new file mode 100644 index 0000000..0fd90b6 --- /dev/null +++ b/hunyuanvideo_foley/utils/feature_utils.py @@ -0,0 +1,156 @@ +"""Feature extraction utilities for video and text processing.""" + +import os +import numpy as np +import torch +import av +from PIL import Image +from einops import rearrange +from typing import Any, Dict, List, Union, Tuple +from loguru import logger + +from .config_utils import AttributeDict +from ..constants import FPS_VISUAL, MAX_VIDEO_DURATION_SECONDS + + +class FeatureExtractionError(Exception): + """Exception raised for feature extraction errors.""" + pass + +def get_frames_av( + video_path: str, + fps: float, + max_length: float = None, +) -> Tuple[np.ndarray, float]: + end_sec = max_length if max_length is not None else 15 + next_frame_time_for_each_fps = 0.0 + time_delta_for_each_fps = 1 / fps + + all_frames = [] + output_frames = [] + + with av.open(video_path) as container: + stream = container.streams.video[0] + ori_fps = stream.guessed_rate + stream.thread_type = "AUTO" + for packet in container.demux(stream): + for frame in packet.decode(): + frame_time = frame.time + if frame_time < 0: + continue + if frame_time > end_sec: + break + + frame_np = None + + this_time = frame_time + while this_time >= next_frame_time_for_each_fps: + if frame_np is None: + frame_np = frame.to_ndarray(format="rgb24") + + output_frames.append(frame_np) + next_frame_time_for_each_fps += time_delta_for_each_fps + + output_frames = np.stack(output_frames) + + vid_len_in_s = len(output_frames) / fps + if max_length is not None and len(output_frames) > int(max_length * fps): + output_frames = output_frames[: int(max_length * fps)] + vid_len_in_s = max_length + + return output_frames, vid_len_in_s + +@torch.inference_mode() +def encode_video_with_siglip2(x: torch.Tensor, model_dict, batch_size: int = -1): + b, t, c, h, w = x.shape + if batch_size < 0: + batch_size = b * t + x = rearrange(x, "b t c h w -> (b t) c h w") + outputs = [] + for i in range(0, b * t, batch_size): + outputs.append(model_dict.siglip2_model.get_image_features(pixel_values=x[i : i + batch_size])) + res = torch.cat(outputs, dim=0) + res = rearrange(res, "(b t) d -> b t d", b=b) + return res + +@torch.inference_mode() +def encode_video_with_sync(x: torch.Tensor, model_dict, batch_size: int = -1): + """ + The input video of x is best to be in fps of 24 of greater than 24. + Input: + x: tensor in shape of [B, T, C, H, W] + batch_size: the batch_size for synchformer inference + """ + b, t, c, h, w = x.shape + assert c == 3 and h == 224 and w == 224 + + segment_size = 16 + step_size = 8 + num_segments = (t - segment_size) // step_size + 1 + segments = [] + for i in range(num_segments): + segments.append(x[:, i * step_size : i * step_size + segment_size]) + x = torch.stack(segments, dim=1).cuda() # (B, num_segments, segment_size, 3, 224, 224) + + outputs = [] + if batch_size < 0: + batch_size = b * num_segments + x = rearrange(x, "b s t c h w -> (b s) 1 t c h w") + for i in range(0, b * num_segments, batch_size): + with torch.autocast(device_type="cuda", enabled=True, dtype=torch.half): + outputs.append(model_dict.syncformer_model(x[i : i + batch_size])) + x = torch.cat(outputs, dim=0) # [b * num_segments, 1, 8, 768] + x = rearrange(x, "(b s) 1 t d -> b (s t) d", b=b) + return x + + +@torch.inference_mode() +def encode_video_features(video_path, model_dict): + visual_features = {} + # siglip2 visual features + frames, ori_vid_len_in_s = get_frames_av(video_path, FPS_VISUAL["siglip2"]) + images = [Image.fromarray(frame).convert('RGB') for frame in frames] + images = [model_dict.siglip2_preprocess(image) for image in images] # [T, C, H, W] + clip_frames = torch.stack(images).to(model_dict.device).unsqueeze(0) + visual_features['siglip2_feat'] = encode_video_with_siglip2(clip_frames, model_dict).to(model_dict.device) + + # synchformer visual features + frames, ori_vid_len_in_s = get_frames_av(video_path, FPS_VISUAL["synchformer"]) + images = torch.from_numpy(frames).permute(0, 3, 1, 2) # [T, C, H, W] + sync_frames = model_dict.syncformer_preprocess(images).unsqueeze(0) # [1, T, 3, 224, 224] + # [1, num_segments * 8, channel_dim], e.g. [1, 240, 768] for 10s video + visual_features['syncformer_feat'] = encode_video_with_sync(sync_frames, model_dict) + + vid_len_in_s = sync_frames.shape[1] / FPS_VISUAL["synchformer"] + visual_features = AttributeDict(visual_features) + + return visual_features, vid_len_in_s + +@torch.inference_mode() +def encode_text_feat(text: List[str], model_dict): + # x: (B, L) + inputs = model_dict.clap_tokenizer(text, padding=True, return_tensors="pt").to(model_dict.device) + outputs = model_dict.clap_model(**inputs, output_hidden_states=True, return_dict=True) + return outputs.last_hidden_state, outputs.attentions + + +def feature_process(video_path, prompt, model_dict, cfg): + visual_feats, audio_len_in_s = encode_video_features(video_path, model_dict) + neg_prompt = "noisy, harsh" + prompts = [neg_prompt, prompt] + text_feat_res, text_feat_mask = encode_text_feat(prompts, model_dict) + + text_feat = text_feat_res[1:] + uncond_text_feat = text_feat_res[:1] + + if cfg.model_config.model_kwargs.text_length < text_feat.shape[1]: + text_seq_length = cfg.model_config.model_kwargs.text_length + text_feat = text_feat[:, :text_seq_length] + uncond_text_feat = uncond_text_feat[:, :text_seq_length] + + text_feats = AttributeDict({ + 'text_feat': text_feat, + 'uncond_text_feat': uncond_text_feat, + }) + + return visual_feats, text_feats, audio_len_in_s diff --git a/hunyuanvideo_foley/utils/helper.py b/hunyuanvideo_foley/utils/helper.py new file mode 100644 index 0000000..c3f98fb --- /dev/null +++ b/hunyuanvideo_foley/utils/helper.py @@ -0,0 +1,134 @@ +import collections.abc +from itertools import repeat +import importlib +import yaml +import time + +def default(value, default_val): + return default_val if value is None else value + + +def default_dtype(value, default_val): + if value is not None: + assert isinstance(value, type(default_val)), f"Expect {type(default_val)}, got {type(value)}." + return value + return default_val + + +def repeat_interleave(lst, num_repeats): + return [item for item in lst for _ in range(num_repeats)] + + +def _ntuple(n): + def parse(x): + if isinstance(x, collections.abc.Iterable) and not isinstance(x, str): + x = tuple(x) + if len(x) == 1: + x = tuple(repeat(x[0], n)) + return x + return tuple(repeat(x, n)) + + return parse + + +to_1tuple = _ntuple(1) +to_2tuple = _ntuple(2) +to_3tuple = _ntuple(3) +to_4tuple = _ntuple(4) + + +def as_tuple(x): + if isinstance(x, collections.abc.Iterable) and not isinstance(x, str): + return tuple(x) + if x is None or isinstance(x, (int, float, str)): + return (x,) + else: + raise ValueError(f"Unknown type {type(x)}") + + +def as_list_of_2tuple(x): + x = as_tuple(x) + if len(x) == 1: + x = (x[0], x[0]) + assert len(x) % 2 == 0, f"Expect even length, got {len(x)}." + lst = [] + for i in range(0, len(x), 2): + lst.append((x[i], x[i + 1])) + return lst + + +def find_multiple(n: int, k: int) -> int: + assert k > 0 + if n % k == 0: + return n + return n - (n % k) + k + + +def merge_dicts(dict1, dict2): + for key, value in dict2.items(): + if key in dict1 and isinstance(dict1[key], dict) and isinstance(value, dict): + merge_dicts(dict1[key], value) + else: + dict1[key] = value + return dict1 + + +def merge_yaml_files(file_list): + merged_config = {} + + for file in file_list: + with open(file, "r", encoding="utf-8") as f: + config = yaml.safe_load(f) + if config: + # Remove the first level + for key, value in config.items(): + if isinstance(value, dict): + merged_config = merge_dicts(merged_config, value) + else: + merged_config[key] = value + + return merged_config + + +def merge_dict(file_list): + merged_config = {} + + for file in file_list: + with open(file, "r", encoding="utf-8") as f: + config = yaml.safe_load(f) + if config: + merged_config = merge_dicts(merged_config, config) + + return merged_config + + +def get_obj_from_str(string, reload=False): + module, cls = string.rsplit(".", 1) + if reload: + module_imp = importlib.import_module(module) + importlib.reload(module_imp) + return getattr(importlib.import_module(module, package=None), cls) + + +def readable_time(seconds): + """ Convert time seconds to a readable format: DD Days, HH Hours, MM Minutes, SS Seconds """ + seconds = int(seconds) + days, seconds = divmod(seconds, 86400) + hours, seconds = divmod(seconds, 3600) + minutes, seconds = divmod(seconds, 60) + if days > 0: + return f"{days} Days, {hours} Hours, {minutes} Minutes, {seconds} Seconds" + if hours > 0: + return f"{hours} Hours, {minutes} Minutes, {seconds} Seconds" + if minutes > 0: + return f"{minutes} Minutes, {seconds} Seconds" + return f"{seconds} Seconds" + + +def get_obj_from_cfg(cfg, reload=False): + if isinstance(cfg, str): + return get_obj_from_str(cfg, reload) + elif isinstance(cfg, (list, tuple,)): + return tuple([get_obj_from_str(c, reload) for c in cfg]) + else: + raise NotImplementedError(f"Not implemented for {type(cfg)}.") diff --git a/hunyuanvideo_foley/utils/media_utils.py b/hunyuanvideo_foley/utils/media_utils.py new file mode 100644 index 0000000..b4ef2d0 --- /dev/null +++ b/hunyuanvideo_foley/utils/media_utils.py @@ -0,0 +1,130 @@ +"""Media utilities for audio/video processing.""" + +import os +import subprocess +from pathlib import Path +from typing import Optional + +from loguru import logger + + +class MediaProcessingError(Exception): + """Exception raised for media processing errors.""" + pass + + +def merge_audio_video( + audio_path: str, + video_path: str, + output_path: str, + overwrite: bool = True, + quality: str = "high" +) -> str: + """ + Merge audio and video files using ffmpeg. + + Args: + audio_path: Path to input audio file + video_path: Path to input video file + output_path: Path for output video file + overwrite: Whether to overwrite existing output file + quality: Quality setting ('high', 'medium', 'low') + + Returns: + Path to the output file + + Raises: + MediaProcessingError: If input files don't exist or ffmpeg fails + FileNotFoundError: If ffmpeg is not installed + """ + # Validate input files + if not os.path.exists(audio_path): + raise MediaProcessingError(f"Audio file not found: {audio_path}") + if not os.path.exists(video_path): + raise MediaProcessingError(f"Video file not found: {video_path}") + + # Create output directory if needed + output_dir = Path(output_path).parent + output_dir.mkdir(parents=True, exist_ok=True) + + # Quality settings + quality_settings = { + "high": ["-b:a", "192k"], + "medium": ["-b:a", "128k"], + "low": ["-b:a", "96k"] + } + + # Build ffmpeg command with more flexible stream handling + ffmpeg_command = [ + "ffmpeg", + "-i", video_path, + "-i", audio_path, + "-c:v", "copy", + "-c:a", "aac", + "-ac", "2", + "-shortest", # Use shortest stream to avoid hanging + *quality_settings.get(quality, quality_settings["high"]), + ] + + if overwrite: + ffmpeg_command.append("-y") + + ffmpeg_command.append(output_path) + + try: + logger.info(f"Merging audio '{audio_path}' with video '{video_path}'") + logger.info(f"FFmpeg command: {' '.join(ffmpeg_command)}") + + process = subprocess.Popen( + ffmpeg_command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + stdout, stderr = process.communicate() + + if process.returncode != 0: + logger.error(f"Primary merge failed, trying fallback method...") + logger.error(f"FFmpeg stderr: {stderr}") + + # Try a more compatible fallback approach + fallback_command = [ + "ffmpeg", "-y", + "-i", video_path, + "-i", audio_path, + "-c:v", "libx264", # Re-encode video for compatibility + "-c:a", "aac", + "-b:a", "128k", + "-preset", "fast", # Faster encoding + "-shortest", + output_path + ] + + logger.info(f"Fallback FFmpeg command: {' '.join(fallback_command)}") + + fallback_process = subprocess.Popen( + fallback_command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + fallback_stdout, fallback_stderr = fallback_process.communicate() + + if fallback_process.returncode != 0: + error_msg = f"Both primary and fallback FFmpeg failed. Primary: {stderr}, Fallback: {fallback_stderr}" + logger.error(error_msg) + raise MediaProcessingError(error_msg) + else: + logger.info(f"Successfully merged video with fallback method: {output_path}") + else: + logger.info(f"Successfully merged video saved to: {output_path}") + + except FileNotFoundError: + raise FileNotFoundError( + "ffmpeg not found. Please install ffmpeg: " + "https://ffmpeg.org/download.html" + ) + except Exception as e: + raise MediaProcessingError(f"Unexpected error during media processing: {e}") + + return output_path diff --git a/hunyuanvideo_foley/utils/model_utils.py b/hunyuanvideo_foley/utils/model_utils.py new file mode 100644 index 0000000..f685dbe --- /dev/null +++ b/hunyuanvideo_foley/utils/model_utils.py @@ -0,0 +1,274 @@ +import torch +import os +from loguru import logger +from torchvision import transforms +from torchvision.transforms import v2 +from diffusers.utils.torch_utils import randn_tensor +from transformers import AutoTokenizer, AutoModel, ClapTextModelWithProjection +from ..models.dac_vae.model.dac import DAC +from ..models.synchformer import Synchformer +from ..models.hifi_foley import HunyuanVideoFoley +from .config_utils import load_yaml, AttributeDict +from .schedulers import FlowMatchDiscreteScheduler +from tqdm import tqdm + +def load_state_dict(model, model_path): + logger.info(f"Loading model state dict from: {model_path}") + state_dict = torch.load(model_path, map_location=lambda storage, loc: storage, weights_only=False) + + missing_keys, unexpected_keys = model.load_state_dict(state_dict, strict=False) + + if missing_keys: + logger.warning(f"Missing keys in state dict ({len(missing_keys)} keys):") + for key in missing_keys: + logger.warning(f" - {key}") + else: + logger.info("No missing keys found") + + if unexpected_keys: + logger.warning(f"Unexpected keys in state dict ({len(unexpected_keys)} keys):") + for key in unexpected_keys: + logger.warning(f" - {key}") + else: + logger.info("No unexpected keys found") + + logger.info("Model state dict loaded successfully") + return model + +def load_model(model_path, config_path, device): + logger.info("Starting model loading process...") + logger.info(f"Configuration file: {config_path}") + logger.info(f"Model weights dir: {model_path}") + logger.info(f"Target device: {device}") + + cfg = load_yaml(config_path) + logger.info("Configuration loaded successfully") + + # HunyuanVideoFoley + logger.info("Loading HunyuanVideoFoley main model...") + foley_model = HunyuanVideoFoley(cfg, dtype=torch.bfloat16, device=device).to(device=device, dtype=torch.bfloat16) + foley_model = load_state_dict(foley_model, os.path.join(model_path, "hunyuanvideo_foley.pth")) + foley_model.eval() + logger.info("HunyuanVideoFoley model loaded and set to evaluation mode") + + # DAC-VAE + dac_path = os.path.join(model_path, "vae_128d_48k.pth") + logger.info(f"Loading DAC VAE model from: {dac_path}") + dac_model = DAC.load(dac_path) + dac_model = dac_model.to(device) + dac_model.requires_grad_(False) + dac_model.eval() + logger.info("DAC VAE model loaded successfully") + + # Siglip2 visual-encoder + logger.info("Loading SigLIP2 visual encoder...") + siglip2_preprocess = transforms.Compose([ + transforms.Resize((512, 512)), + transforms.ToTensor(), + transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]), + ]) + + # Try multiple approaches to load SigLIP2 + siglip2_model = None + + # Method 1: Try with standard transformers AutoModel + try: + siglip2_model = AutoModel.from_pretrained("google/siglip2-base-patch16-512", trust_remote_code=True).to(device).eval() + logger.info("SigLIP2 loaded using standard transformers") + except Exception as e1: + logger.warning(f"Standard transformers loading failed: {e1}") + + # Method 2: Try loading from local cache or downloaded weights + try: + from transformers import SiglipVisionModel + siglip2_model = SiglipVisionModel.from_pretrained("google/siglip-base-patch16-512").to(device).eval() + logger.info("SigLIP2 loaded using SiglipVisionModel (base variant)") + except Exception as e2: + logger.warning(f"SiglipVisionModel loading failed: {e2}") + + # Method 3: Try using a compatible CLIP model as fallback + try: + from transformers import CLIPVisionModel + logger.warning("Falling back to CLIP vision model as SigLIP2 is not available") + siglip2_model = CLIPVisionModel.from_pretrained("openai/clip-vit-large-patch14-336").to(device).eval() + logger.info("Using CLIP vision model as fallback") + except Exception as e3: + logger.error(f"All vision model loading attempts failed: {e3}") + raise RuntimeError( + "Could not load SigLIP2 vision encoder. Please ensure you have a compatible " + "transformers version installed. You can try:\n" + "1. pip install transformers>=4.37.0\n" + "2. Or manually download the model weights" + ) + + logger.info("SigLIP2 model and preprocessing pipeline loaded successfully") + + # clap text-encoder + logger.info("Loading CLAP text encoder...") + clap_tokenizer = AutoTokenizer.from_pretrained("laion/larger_clap_general") + clap_model = ClapTextModelWithProjection.from_pretrained("laion/larger_clap_general").to(device) + logger.info("CLAP tokenizer and model loaded successfully") + + # syncformer + syncformer_path = os.path.join(model_path, "synchformer_state_dict.pth") + logger.info(f"Loading Synchformer model from: {syncformer_path}") + syncformer_preprocess = v2.Compose( + [ + v2.Resize(224, interpolation=v2.InterpolationMode.BICUBIC), + v2.CenterCrop(224), + v2.ToImage(), + v2.ToDtype(torch.float32, scale=True), + v2.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]), + ] + ) + + syncformer_model = Synchformer() + syncformer_model.load_state_dict(torch.load(syncformer_path, weights_only=False, map_location="cpu")) + syncformer_model = syncformer_model.to(device).eval() + logger.info("Synchformer model and preprocessing pipeline loaded successfully") + + + logger.info("Creating model dictionary with attribute access...") + model_dict = AttributeDict({ + 'foley_model': foley_model, + 'dac_model': dac_model, + 'siglip2_preprocess': siglip2_preprocess, + 'siglip2_model': siglip2_model, + 'clap_tokenizer': clap_tokenizer, + 'clap_model': clap_model, + 'syncformer_preprocess': syncformer_preprocess, + 'syncformer_model': syncformer_model, + 'device': device, + }) + + logger.info("All models loaded successfully!") + logger.info("Available model components:") + for key in model_dict.keys(): + logger.info(f" - {key}") + logger.info("Models can be accessed via attribute notation (e.g., models.foley_model)") + + return model_dict, cfg + +def retrieve_timesteps( + scheduler, + num_inference_steps, + device, + **kwargs, +): + scheduler.set_timesteps(num_inference_steps, device=device, **kwargs) + timesteps = scheduler.timesteps + return timesteps, num_inference_steps + + +def prepare_latents(scheduler, batch_size, num_channels_latents, length, dtype, device): + shape = (batch_size, num_channels_latents, int(length)) + latents = randn_tensor(shape, device=device, dtype=dtype) + + # Check existence to make it compatible with FlowMatchEulerDiscreteScheduler + if hasattr(scheduler, "init_noise_sigma"): + # scale the initial noise by the standard deviation required by the scheduler + latents = latents * scheduler.init_noise_sigma + + return latents + + +@torch.no_grad() +def denoise_process(visual_feats, text_feats, audio_len_in_s, model_dict, cfg, guidance_scale=4.5, num_inference_steps=50, batch_size=1): + + target_dtype = model_dict.foley_model.dtype + autocast_enabled = target_dtype != torch.float32 + device = model_dict.device + + scheduler = FlowMatchDiscreteScheduler( + shift=cfg.diffusion_config.sample_flow_shift, + reverse=cfg.diffusion_config.flow_reverse, + solver=cfg.diffusion_config.flow_solver, + use_flux_shift=cfg.diffusion_config.sample_use_flux_shift, + flux_base_shift=cfg.diffusion_config.flux_base_shift, + flux_max_shift=cfg.diffusion_config.flux_max_shift, + ) + + timesteps, num_inference_steps = retrieve_timesteps( + scheduler, + num_inference_steps, + device, + ) + + latents = prepare_latents( + scheduler, + batch_size=batch_size, + num_channels_latents=cfg.model_config.model_kwargs.audio_vae_latent_dim, + length=audio_len_in_s * cfg.model_config.model_kwargs.audio_frame_rate, + dtype=target_dtype, + device=device, + ) + + # Denoise loop + for i, t in tqdm(enumerate(timesteps), total=len(timesteps), desc="Denoising steps"): + # noise latents + latent_input = torch.cat([latents] * 2) if guidance_scale > 1.0 else latents + latent_input = scheduler.scale_model_input(latent_input, t) + + t_expand = t.repeat(latent_input.shape[0]) + + # siglip2 features + siglip2_feat = visual_feats.siglip2_feat.repeat(batch_size, 1, 1) # Repeat for batch_size + uncond_siglip2_feat = model_dict.foley_model.get_empty_clip_sequence( + bs=batch_size, len=siglip2_feat.shape[1] + ).to(device) + + if guidance_scale is not None and guidance_scale > 1.0: + siglip2_feat_input = torch.cat([uncond_siglip2_feat, siglip2_feat], dim=0) + else: + siglip2_feat_input = siglip2_feat + + # syncformer features + syncformer_feat = visual_feats.syncformer_feat.repeat(batch_size, 1, 1) # Repeat for batch_size + uncond_syncformer_feat = model_dict.foley_model.get_empty_sync_sequence( + bs=batch_size, len=syncformer_feat.shape[1] + ).to(device) + if guidance_scale is not None and guidance_scale > 1.0: + syncformer_feat_input = torch.cat([uncond_syncformer_feat, syncformer_feat], dim=0) + else: + syncformer_feat_input = syncformer_feat + + # text features + text_feat_repeated = text_feats.text_feat.repeat(batch_size, 1, 1) # Repeat for batch_size + uncond_text_feat_repeated = text_feats.uncond_text_feat.repeat(batch_size, 1, 1) # Repeat for batch_size + if guidance_scale is not None and guidance_scale > 1.0: + text_feat_input = torch.cat([uncond_text_feat_repeated, text_feat_repeated], dim=0) + else: + text_feat_input = text_feat_repeated + + with torch.autocast(device_type=device.type, enabled=autocast_enabled, dtype=target_dtype): + # Predict the noise residual + noise_pred = model_dict.foley_model( + x=latent_input, + t=t_expand, + cond=text_feat_input, + clip_feat=siglip2_feat_input, + sync_feat=syncformer_feat_input, + return_dict=True, + )["x"] + + noise_pred = noise_pred.to(dtype=torch.float32) + + if guidance_scale is not None and guidance_scale > 1.0: + # Perform classifier-free guidance + noise_pred_uncond, noise_pred_text = noise_pred.chunk(2) + noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond) + + # Compute the previous noisy sample x_t -> x_t-1 + latents = scheduler.step(noise_pred, t, latents, return_dict=False)[0] + + # Post-process the latents to audio + + with torch.no_grad(): + audio = model_dict.dac_model.decode(latents) + audio = audio.float().cpu() + + audio = audio[:, :int(audio_len_in_s*model_dict.dac_model.sample_rate)] + + return audio, model_dict.dac_model.sample_rate + + diff --git a/hunyuanvideo_foley/utils/schedulers/__init__.py b/hunyuanvideo_foley/utils/schedulers/__init__.py new file mode 100644 index 0000000..2c4275f --- /dev/null +++ b/hunyuanvideo_foley/utils/schedulers/__init__.py @@ -0,0 +1,2 @@ +from diffusers.schedulers import DDPMScheduler, EulerDiscreteScheduler +from .scheduling_flow_match_discrete import FlowMatchDiscreteScheduler \ No newline at end of file diff --git a/hunyuanvideo_foley/utils/schedulers/__pycache__/__init__.cpython-312.pyc b/hunyuanvideo_foley/utils/schedulers/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cc273aad310b1f8e4cd09a89e909f749246f9f2d GIT binary patch literal 375 zcmZXQze>e06vmUbS1xKdac~gVF82XMT&zWfE5b#GQV694#kFe2UVX(%~WjU<}<@+Ayv==KUG9_F=fx_^?( zY~pq>ZCj&=R5LYNDjEB`GV#gn50v(|sP#^W2P=e!adN|pp25mz@KxuB)rsJ(y*{rR zqqN|t7nrCKqBGN5Q};I%6Hy38Y}Ngc`#@0pjf|#DRez7E5TZTR2+i)Sn;vly);^Zo WeqvZrZTa*WLkQnN@dgSR{(B7!6LQS} literal 0 HcmV?d00001 diff --git a/hunyuanvideo_foley/utils/schedulers/__pycache__/scheduling_flow_match_discrete.cpython-312.pyc b/hunyuanvideo_foley/utils/schedulers/__pycache__/scheduling_flow_match_discrete.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..024c1d0c7ce517669e7461d0bfc684a97598a2d9 GIT binary patch literal 17196 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 literal 0 HcmV?d00001 diff --git a/hunyuanvideo_foley/utils/schedulers/scheduling_flow_match_discrete.py b/hunyuanvideo_foley/utils/schedulers/scheduling_flow_match_discrete.py new file mode 100644 index 0000000..a7e9365 --- /dev/null +++ b/hunyuanvideo_foley/utils/schedulers/scheduling_flow_match_discrete.py @@ -0,0 +1,376 @@ +import math +from dataclasses import dataclass +from typing import Optional, Tuple, Union + +import numpy as np +import torch + +from diffusers.configuration_utils import ConfigMixin, register_to_config +from diffusers.utils import BaseOutput, logging +from diffusers.schedulers.scheduling_utils import SchedulerMixin + + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +@dataclass +class FlowMatchDiscreteSchedulerOutput(BaseOutput): + """ + Output class for the scheduler's `step` function output. + + Args: + prev_sample (`torch.FloatTensor` of shape `(batch_size, num_channels, height, width)` for images): + Computed sample `(x_{t-1})` of previous timestep. `prev_sample` should be used as next model input in the + denoising loop. + """ + + prev_sample: torch.FloatTensor + + +class FlowMatchDiscreteScheduler(SchedulerMixin, ConfigMixin): + """ + Euler scheduler. + + This model inherits from [`SchedulerMixin`] and [`ConfigMixin`]. Check the superclass documentation for the generic + methods the library implements for all schedulers such as loading and saving. + + Args: + num_train_timesteps (`int`, defaults to 1000): + The number of diffusion steps to train the model. + timestep_spacing (`str`, defaults to `"linspace"`): + The way the timesteps should be scaled. Refer to Table 2 of the [Common Diffusion Noise Schedules and + Sample Steps are Flawed](https://huggingface.co/papers/2305.08891) for more information. + shift (`float`, defaults to 1.0): + The shift value for the timestep schedule. + reverse (`bool`, defaults to `True`): + Whether to reverse the timestep schedule. + """ + + _compatibles = [] + order = 1 + + @register_to_config + def __init__( + self, + num_train_timesteps: int = 1000, + shift: float = 1.0, + reverse: bool = True, + solver: str = "euler", + use_flux_shift: bool = False, + flux_base_shift: float = 0.5, + flux_max_shift: float = 1.15, + n_tokens: Optional[int] = None, + ): + sigmas = torch.linspace(1, 0, num_train_timesteps + 1) + + if not reverse: + sigmas = sigmas.flip(0) + + self.sigmas = sigmas + # the value fed to model + self.timesteps = (sigmas[:-1] * num_train_timesteps).to(dtype=torch.float32) + self.timesteps_full = (sigmas * num_train_timesteps).to(dtype=torch.float32) + + self._step_index = None + self._begin_index = None + + self.supported_solver = [ + "euler", + "heun-2", "midpoint-2", + "kutta-4", + ] + if solver not in self.supported_solver: + raise ValueError(f"Solver {solver} not supported. Supported solvers: {self.supported_solver}") + + # empty dt and derivative (for heun) + self.derivative_1 = None + self.derivative_2 = None + self.derivative_3 = None + self.dt = None + + @property + def step_index(self): + """ + The index counter for current timestep. It will increase 1 after each scheduler step. + """ + return self._step_index + + @property + def begin_index(self): + """ + The index for the first timestep. It should be set from pipeline with `set_begin_index` method. + """ + return self._begin_index + + # Copied from diffusers.schedulers.scheduling_dpmsolver_multistep.DPMSolverMultistepScheduler.set_begin_index + def set_begin_index(self, begin_index: int = 0): + """ + Sets the begin index for the scheduler. This function should be run from pipeline before the inference. + + Args: + begin_index (`int`): + The begin index for the scheduler. + """ + self._begin_index = begin_index + + def _sigma_to_t(self, sigma): + return sigma * self.config.num_train_timesteps + + @property + def state_in_first_order(self): + return self.derivative_1 is None + + @property + def state_in_second_order(self): + return self.derivative_2 is None + + @property + def state_in_third_order(self): + return self.derivative_3 is None + + def set_timesteps(self, num_inference_steps: int, device: Union[str, torch.device] = None, + n_tokens: int = None): + """ + Sets the discrete timesteps used for the diffusion chain (to be run before inference). + + Args: + num_inference_steps (`int`): + The number of diffusion steps used when generating samples with a pre-trained model. + device (`str` or `torch.device`, *optional*): + The device to which the timesteps should be moved to. If `None`, the timesteps are not moved. + n_tokens (`int`, *optional*): + Number of tokens in the input sequence. + """ + self.num_inference_steps = num_inference_steps + + sigmas = torch.linspace(1, 0, num_inference_steps + 1) + + # Apply timestep shift + if self.config.use_flux_shift: + assert isinstance(n_tokens, int), "n_tokens should be provided for flux shift" + mu = self.get_lin_function(y1=self.config.flux_base_shift, y2=self.config.flux_max_shift)(n_tokens) + sigmas = self.flux_time_shift(mu, 1.0, sigmas) + elif self.config.shift != 1.: + sigmas = self.sd3_time_shift(sigmas) + + if not self.config.reverse: + sigmas = 1 - sigmas + + self.sigmas = sigmas + self.timesteps = (sigmas[:-1] * self.config.num_train_timesteps).to(dtype=torch.float32, device=device) + self.timesteps_full = (sigmas * self.config.num_train_timesteps).to(dtype=torch.float32, device=device) + + # empty dt and derivative (for kutta) + self.derivative_1 = None + self.derivative_2 = None + self.derivative_3 = None + self.dt = None + + # Reset step index + self._step_index = None + + def index_for_timestep(self, timestep, schedule_timesteps=None): + if schedule_timesteps is None: + schedule_timesteps = self.timesteps + + indices = (schedule_timesteps == timestep).nonzero() + + # The sigma index that is taken for the **very** first `step` + # is always the second index (or the last index if there is only 1) + # This way we can ensure we don't accidentally skip a sigma in + # case we start in the middle of the denoising schedule (e.g. for image-to-image) + pos = 1 if len(indices) > 1 else 0 + + return indices[pos].item() + + def _init_step_index(self, timestep): + if self.begin_index is None: + if isinstance(timestep, torch.Tensor): + timestep = timestep.to(self.timesteps.device) + self._step_index = self.index_for_timestep(timestep) + else: + self._step_index = self._begin_index + + def scale_model_input(self, sample: torch.Tensor, timestep: Optional[int] = None) -> torch.Tensor: + return sample + + @staticmethod + def get_lin_function(x1: float = 256, y1: float = 0.5, x2: float = 4096, y2: float = 1.15): + m = (y2 - y1) / (x2 - x1) + b = y1 - m * x1 + return lambda x: m * x + b + + @staticmethod + def flux_time_shift(mu: float, sigma: float, t: torch.Tensor): + return math.exp(mu) / (math.exp(mu) + (1 / t - 1) ** sigma) + + def sd3_time_shift(self, t: torch.Tensor): + return (self.config.shift * t) / (1 + (self.config.shift - 1) * t) + + def step( + self, + model_output: torch.FloatTensor, + timestep: Union[float, torch.FloatTensor], + sample: torch.FloatTensor, + pred_uncond: torch.FloatTensor = None, + generator: Optional[torch.Generator] = None, + n_tokens: Optional[int] = None, + return_dict: bool = True, + ) -> Union[FlowMatchDiscreteSchedulerOutput, Tuple]: + """ + Predict the sample from the previous timestep by reversing the SDE. This function propagates the diffusion + process from the learned model outputs (most often the predicted noise). + + Args: + model_output (`torch.FloatTensor`): + The direct output from learned diffusion model. + timestep (`float`): + The current discrete timestep in the diffusion chain. + sample (`torch.FloatTensor`): + A current instance of a sample created by the diffusion process. + generator (`torch.Generator`, *optional*): + A random number generator. + n_tokens (`int`, *optional*): + Number of tokens in the input sequence. + return_dict (`bool`): + Whether or not to return a [`~schedulers.scheduling_euler_discrete.EulerDiscreteSchedulerOutput`] or + tuple. + + Returns: + [`~schedulers.scheduling_euler_discrete.EulerDiscreteSchedulerOutput`] or `tuple`: + If return_dict is `True`, [`~schedulers.scheduling_euler_discrete.EulerDiscreteSchedulerOutput`] is + returned, otherwise a tuple is returned where the first element is the sample tensor. + """ + + if ( + isinstance(timestep, int) + or isinstance(timestep, torch.IntTensor) + or isinstance(timestep, torch.LongTensor) + ): + raise ValueError( + ( + "Passing integer indices (e.g. from `enumerate(timesteps)`) as timesteps to" + " `EulerDiscreteScheduler.step()` is not supported. Make sure to pass" + " one of the `scheduler.timesteps` as a timestep." + ), + ) + + if self.step_index is None: + self._init_step_index(timestep) + + # Upcast to avoid precision issues when computing prev_sample + sample = sample.to(torch.float32) + model_output = model_output.to(torch.float32) + pred_uncond = pred_uncond.to(torch.float32) if pred_uncond is not None else None + + # dt = self.sigmas[self.step_index + 1] - self.sigmas[self.step_index] + sigma = self.sigmas[self.step_index] + sigma_next = self.sigmas[self.step_index + 1] + + last_inner_step = True + if self.config.solver == "euler": + derivative, dt, sample, last_inner_step = self.first_order_method(model_output, sigma, sigma_next, sample) + elif self.config.solver in ["heun-2", "midpoint-2"]: + derivative, dt, sample, last_inner_step = self.second_order_method(model_output, sigma, sigma_next, sample) + elif self.config.solver == "kutta-4": + derivative, dt, sample, last_inner_step = self.fourth_order_method(model_output, sigma, sigma_next, sample) + else: + raise ValueError(f"Solver {self.config.solver} not supported. Supported solvers: {self.supported_solver}") + + prev_sample = sample + derivative * dt + + # Cast sample back to model compatible dtype + # prev_sample = prev_sample.to(model_output.dtype) + + # upon completion increase step index by one + if last_inner_step: + self._step_index += 1 + + if not return_dict: + return (prev_sample,) + + return FlowMatchDiscreteSchedulerOutput(prev_sample=prev_sample) + + def first_order_method(self, model_output, sigma, sigma_next, sample): + derivative = model_output.float() + dt = sigma_next - sigma + return derivative, dt, sample, True + + def second_order_method(self, model_output, sigma, sigma_next, sample): + if self.state_in_first_order: + # store for 2nd order step + self.derivative_1 = model_output + self.dt = sigma_next - sigma + self.sample = sample + + derivative = model_output + if self.config.solver == 'heun-2': + dt = self.dt + elif self.config.solver == 'midpoint-2': + dt = self.dt / 2 + else: + raise NotImplementedError(f"Solver {self.config.solver} not supported.") + last_inner_step = False + + else: + if self.config.solver == 'heun-2': + derivative = 0.5 * (self.derivative_1 + model_output) + elif self.config.solver == 'midpoint-2': + derivative = model_output + else: + raise NotImplementedError(f"Solver {self.config.solver} not supported.") + + # 3. take prev timestep & sample + dt = self.dt + sample = self.sample + last_inner_step = True + + # free dt and derivative + # Note, this puts the scheduler in "first order mode" + self.derivative_1 = None + self.dt = None + self.sample = None + + return derivative, dt, sample, last_inner_step + + def fourth_order_method(self, model_output, sigma, sigma_next, sample): + if self.state_in_first_order: + self.derivative_1 = model_output + self.dt = sigma_next - sigma + self.sample = sample + derivative = model_output + dt = self.dt / 2 + last_inner_step = False + + elif self.state_in_second_order: + self.derivative_2 = model_output + derivative = model_output + dt = self.dt / 2 + last_inner_step = False + + elif self.state_in_third_order: + self.derivative_3 = model_output + derivative = model_output + dt = self.dt + last_inner_step = False + + else: + derivative = 1/6 * self.derivative_1 + 1/3 * self.derivative_2 + 1/3 * self.derivative_3 + 1/6 * model_output + + # 3. take prev timestep & sample + dt = self.dt + sample = self.sample + last_inner_step = True + + # free dt and derivative + # Note, this puts the scheduler in "first order mode" + self.derivative_1 = None + self.derivative_2 = None + self.derivative_3 = None + self.dt = None + self.sample = None + + return derivative, dt, sample, last_inner_step + + def __len__(self): + return self.config.num_train_timesteps diff --git a/install.py b/install.py new file mode 100644 index 0000000..d94dbbb --- /dev/null +++ b/install.py @@ -0,0 +1,105 @@ +""" +Installation script for ComfyUI HunyuanVideo-Foley Custom Node +""" + +import os +import sys +import subprocess +import pkg_resources +from pathlib import Path + +def check_and_install_requirements(): + """Check and install required packages""" + requirements_file = Path(__file__).parent / "requirements.txt" + + if not requirements_file.exists(): + print("Requirements file not found!") + return False + + try: + print("Checking and installing requirements...") + + # Read requirements + with open(requirements_file, 'r') as f: + requirements = f.read().splitlines() + + # Filter out comments and empty lines + requirements = [line.strip() for line in requirements + if line.strip() and not line.strip().startswith('#')] + + # Install packages + for requirement in requirements: + try: + # Skip git+ requirements for now (they need special handling) + if requirement.startswith('git+'): + print(f"Installing git requirement: {requirement}") + subprocess.check_call([sys.executable, '-m', 'pip', 'install', requirement]) + else: + # Check if package is already installed + try: + pkg_resources.require([requirement]) + print(f"✓ {requirement} already installed") + except pkg_resources.DistributionNotFound: + print(f"Installing {requirement}...") + subprocess.check_call([sys.executable, '-m', 'pip', 'install', requirement]) + except pkg_resources.VersionConflict: + print(f"Updating {requirement}...") + subprocess.check_call([sys.executable, '-m', 'pip', 'install', '--upgrade', requirement]) + + except subprocess.CalledProcessError as e: + print(f"Failed to install {requirement}: {e}") + return False + + print("✅ All requirements installed successfully!") + return True + + except Exception as e: + print(f"Error installing requirements: {e}") + return False + +def setup_model_directories(): + """Create necessary model directories""" + base_dir = Path(__file__).parent.parent.parent # Go up to ComfyUI root + + # Create ComfyUI/models/foley directory for automatic downloads + foley_models_dir = base_dir / "models" / "foley" + foley_models_dir.mkdir(parents=True, exist_ok=True) + print(f"✓ Created ComfyUI models directory: {foley_models_dir}") + + # Also create local fallback directories + node_dir = Path(__file__).parent + local_dirs = [ + node_dir / "pretrained_models", + node_dir / "configs" + ] + + for dir_path in local_dirs: + dir_path.mkdir(exist_ok=True) + print(f"✓ Created local directory: {dir_path}") + +def main(): + """Main installation function""" + print("🚀 Installing ComfyUI HunyuanVideo-Foley Custom Node...") + + # Install requirements + if not check_and_install_requirements(): + print("❌ Failed to install requirements") + return False + + # Setup directories + setup_model_directories() + + print("📁 Directory structure created") + print("📋 Installation completed!") + print() + print("📌 Next steps:") + print("1. Restart ComfyUI to load the custom nodes") + print("2. Models will be automatically downloaded when you first use the node") + print("3. Alternatively, manually download models and place them in ComfyUI/models/foley/") + print("4. Model URLs are configured in model_urls.py (can be updated as needed)") + print() + + return True + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/model_urls.py b/model_urls.py new file mode 100644 index 0000000..9dd5803 --- /dev/null +++ b/model_urls.py @@ -0,0 +1,40 @@ +# HunyuanVideo-Foley Model URLs Configuration +# Update these URLs with the actual download links for the models + +MODEL_URLS = { + "hunyuanvideo-foley-xxl": { + "models": [ + { + "url": "https://huggingface.co/tencent/HunyuanVideo-Foley/resolve/main/hunyuanvideo_foley.pth", + "filename": "hunyuanvideo_foley.pth", + "description": "Main HunyuanVideo-Foley model" + }, + { + "url": "https://huggingface.co/tencent/HunyuanVideo-Foley/resolve/main/synchformer_state_dict.pth", + "filename": "synchformer_state_dict.pth", + "description": "Synchformer model weights" + }, + { + "url": "https://huggingface.co/tencent/HunyuanVideo-Foley/resolve/main/vae_128d_48k.pth", + "filename": "vae_128d_48k.pth", + "description": "VAE model weights" + } + ], + "extracted_dir": "hunyuanvideo-foley-xxl", + "description": "HunyuanVideo-Foley XXL model for audio generation" + } +} + +# Alternative mirror URLs (if main URLs fail) +MIRROR_URLS = { + # Add mirror download sources here if needed +} + +def get_model_url(model_name: str, use_mirror: bool = False) -> dict: + """Get model URL configuration""" + urls_dict = MIRROR_URLS if use_mirror else MODEL_URLS + return urls_dict.get(model_name, {}) + +def list_available_models() -> list: + """List all available model names""" + return list(MODEL_URLS.keys()) \ No newline at end of file diff --git a/nodes.py b/nodes.py new file mode 100644 index 0000000..536bcd4 --- /dev/null +++ b/nodes.py @@ -0,0 +1,1271 @@ +import os +import glob +import torch +import torchaudio +import tempfile +import numpy as np +from loguru import logger +from typing import Optional, Tuple, Any, List +import folder_paths +import random +import urllib.request +import zipfile +import tarfile +from pathlib import Path +from datetime import datetime +import shutil +import time +import math +from tqdm import tqdm +try: + import requests + HAS_REQUESTS = True +except ImportError: + HAS_REQUESTS = False + +# Import ComfyUI video types +try: + from comfy_api.input_impl import VideoFromFile +except ImportError: + try: + # Fallback to latest API location + from comfy_api.latest._input_impl.video_types import VideoFromFile + except ImportError: + logger.warning("VideoFromFile not available, will return file paths only") + VideoFromFile = None + +# Add foley models directory to ComfyUI folder paths +foley_models_dir = os.path.join(folder_paths.models_dir, "foley") +if "foley" not in folder_paths.folder_names_and_paths: + folder_paths.folder_names_and_paths["foley"] = ([foley_models_dir], folder_paths.supported_pt_extensions) + +# Import model URLs configuration +try: + from .model_urls import MODEL_URLS, get_model_url, list_available_models +except ImportError: + logger.warning("model_urls.py not found, using default URLs") + MODEL_URLS = { + "hunyuanvideo-foley-xxl": { + "models": [ + { + "url": "https://huggingface.co/tencent/HunyuanVideo-Foley/resolve/main/hunyuanvideo_foley.pth", + "filename": "hunyuanvideo_foley.pth", + "description": "Main HunyuanVideo-Foley model" + }, + { + "url": "https://huggingface.co/tencent/HunyuanVideo-Foley/resolve/main/synchformer_state_dict.pth", + "filename": "synchformer_state_dict.pth", + "description": "Synchformer model weights" + }, + { + "url": "https://huggingface.co/tencent/HunyuanVideo-Foley/resolve/main/vae_128d_48k.pth", + "filename": "vae_128d_48k.pth", + "description": "VAE model weights" + } + ], + "extracted_dir": "hunyuanvideo-foley-xxl", + "description": "HunyuanVideo-Foley XXL model for audio generation" + } + } + +# Import the HunyuanVideo-Foley modules +try: + from hunyuanvideo_foley.utils.model_utils import load_model, denoise_process + from hunyuanvideo_foley.utils.feature_utils import feature_process + from hunyuanvideo_foley.utils.media_utils import merge_audio_video +except ImportError as e: + logger.error(f"Failed to import HunyuanVideo-Foley modules: {e}") + logger.error("Make sure the HunyuanVideo-Foley package is installed and accessible") + raise + +class HunyuanVideoFoleyNode: + """ + A node for generating audio using the HunyuanVideo-Foley model + """ + # Class-level model storage for persistence + _model_dict = None + _cfg = None + _device = None + _model_path = None + _memory_efficient = False # Track memory mode + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "text_prompt": ("STRING", { + "multiline": True, + "default": "footstep sound, impact, water splash", + "display": "textarea", + "placeholder": "Describe the audio you want to generate..." + }), + "guidance_scale": ("FLOAT", { + "default": 4.5, + "min": 1.0, + "max": 10.0, + "step": 0.1, + "display": "slider" + }), + "num_inference_steps": ("INT", { + "default": 50, + "min": 10, + "max": 200, + "step": 1, + "display": "slider" + }), + "sample_nums": ("INT", { + "default": 1, + "min": 1, + "max": 4, + "step": 1, + "display": "slider" + }), + "seed": ("INT", { + "default": 42, + "min": 0, + "max": 0xffffffffffffffff, + "display": "number" + }), + }, + "optional": { + # Hybrid input support - either VIDEO or IMAGE + "video": ("VIDEO",), + "images": ("IMAGE",), # Support for frame sequences from PR + "fps": ("FLOAT", { + "default": 24.0, + "min": 1.0, + "max": 120.0, + "step": 0.1, + "display": "slider", + "tooltip": "Frames per second (only used with IMAGE input)" + }), + # Negative prompt from PR + "negative_prompt": ("STRING", { + "multiline": True, + "default": "", + "display": "textarea", + "placeholder": "Additional negative prompts (optional). Will be combined with built-in quality controls." + }), + # Output options + "output_format": (["video_path", "frames", "both"], { + "default": "both", + "tooltip": "Choose output format: video file path only works with VIDEO input, frames only works with IMAGE input and VIDEO input" + }), + "output_folder": ("STRING", { + "default": "hunyuan_foley", + "multiline": False, + "display": "text", + "placeholder": "Subfolder name in ComfyUI/output/" + }), + "filename_prefix": ("STRING", { + "default": "foley_", + "multiline": False, + "display": "text", + "placeholder": "Prefix for output filename" + }), + # Memory optimization options + "memory_efficient": ("BOOLEAN", { + "default": False, + "display": "checkbox", + "tooltip": "Enable memory-efficient mode: unloads models after generation and uses aggressive garbage collection" + }), + "cpu_offload": ("BOOLEAN", { + "default": False, + "display": "checkbox", + "tooltip": "Offload models to CPU when not in use (slower but saves VRAM)" + }), + } + } + + RETURN_TYPES = ("STRING", "IMAGE", "AUDIO", "STRING") + RETURN_NAMES = ("video_path", "video_frames", "audio", "status_message") + FUNCTION = "generate_audio" + CATEGORY = "HunyuanVideo-Foley" + DESCRIPTION = "Generate synchronized audio for videos using HunyuanVideo-Foley model" + + @classmethod + def _required_model_filenames(cls, model_variant: str) -> List[str]: + if model_variant == "hunyuanvideo-foley-s": + return ["hunyuanvideo_foley.pth", "vae_88d_48k.pth", "synchformer_state_dict.pth"] + else: + # XXL (default) + return ["hunyuanvideo_foley.pth", "vae_128d_48k.pth", "synchformer_state_dict.pth"] + + @staticmethod + def _string_looks_like_video_path(input_str): + if not isinstance(input_str, str): + return False + video_extensions = ['.mp4', '.avi', '.mov', '.mkv', '.webm', '.m4v', '.flv', '.wmv', '.mpg', '.mpeg'] + return any(input_str.lower().endswith(ext) for ext in video_extensions) + + @classmethod + def _extract_video_path(cls, video): + """ + Extract a file path from various potential video input types + """ + # Handle ComfyUI VideoFromFile object + if hasattr(video, '__class__') and 'VideoFromFile' in video.__class__.__name__: + # Try to access the private attribute + if hasattr(video, '_VideoFromFile__file'): + return getattr(video, '_VideoFromFile__file') + # Try other common attributes + for attr in ['file', 'path', 'filename', '__file']: + if hasattr(video, attr): + value = getattr(video, attr) + if isinstance(value, str): + return value + + # Direct string path + if isinstance(video, str): + # Check if it looks like a video path + if cls._string_looks_like_video_path(video): + return video + else: + # Could be JSON or other string representation + try: + import json + parsed = json.loads(video) + if isinstance(parsed, dict): + # Try to get path from dict + if 'path' in parsed: + return parsed['path'] + elif 'file' in parsed: + return parsed['file'] + elif 'filename' in parsed: + return parsed['filename'] + except: + pass + + # Dictionary with path key + elif isinstance(video, dict): + if 'path' in video: + return video['path'] + elif 'file' in video: + return video['file'] + elif 'filename' in video: + return video['filename'] + elif 'video' in video: + return cls._extract_video_path(video['video']) + + # List (possibly frames) + elif isinstance(video, (list, tuple)): + if len(video) > 0: + # Check if first element is a path + first = video[0] + if isinstance(first, str) and cls._string_looks_like_video_path(first): + return first + # Could be a list of frames - return None to trigger temp video creation + return None + + # Tensor (video frames) + elif hasattr(video, 'shape'): + # This is a tensor, we'll need to save it as a temp video + return None + + # Object with attributes + elif hasattr(video, '__dict__'): + # Try to get common attribute names + for attr in ['path', 'file', 'filename', 'video_path', 'file_path', '_VideoFromFile__file']: + if hasattr(video, attr): + value = getattr(video, attr) + if isinstance(value, str) and (cls._string_looks_like_video_path(value) or os.path.exists(value)): + return value + + # Custom __str__ method might return path + try: + str_repr = str(video) + if cls._string_looks_like_video_path(str_repr): + return str_repr + except: + pass + + return None + + + @classmethod + def _extract_frames_from_image_input(cls, images, fps=24.0): + """ + Convert IMAGE input to video frames and create a temporary video file + Based on PR #3 by yichengup + """ + import cv2 + import tempfile + + try: + if images is None: + return None, "No images provided" + + # Handle different image input formats + if hasattr(images, 'shape'): + # Tensor input [batch, height, width, channels] + if len(images.shape) == 4: + frames = images.cpu().numpy() + else: + return None, f"Unexpected image tensor shape: {images.shape}" + else: + return None, f"Unsupported image input type: {type(images)}" + + # Convert to uint8 if needed + if frames.dtype != np.uint8: + if frames.max() <= 1.0: + frames = (frames * 255).astype(np.uint8) + else: + frames = frames.astype(np.uint8) + + # Get dimensions + batch_size, height, width = frames.shape[:3] + + # Create temporary video file + temp_fd, temp_path = tempfile.mkstemp(suffix='.mp4') + os.close(temp_fd) + + # Setup video writer + fourcc = cv2.VideoWriter_fourcc(*'mp4v') + out = cv2.VideoWriter(temp_path, fourcc, fps, (width, height)) + + if not out.isOpened(): + return None, "Failed to open video writer" + + # Write frames + for i in range(batch_size): + frame = frames[i] + + # Handle channels - convert RGB to BGR for OpenCV + if len(frame.shape) == 3 and frame.shape[2] == 3: + frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) + elif len(frame.shape) == 3 and frame.shape[2] == 4: + frame = cv2.cvtColor(frame, cv2.COLOR_RGBA2BGR) + + out.write(frame) + + out.release() + + logger.info(f"Created temporary video from {batch_size} frames at {fps} FPS: {temp_path}") + return temp_path, "Success" + + except Exception as e: + return None, f"Error converting images to video: {str(e)}" + + + @classmethod + def _process_negative_prompt(cls, user_negative_prompt=""): + """ + Combine built-in negative prompt with user input + Based on PR #3 by yichengup + """ + built_in_neg = "noisy, harsh" + if user_negative_prompt and user_negative_prompt.strip(): + combined_neg_prompt = f"{built_in_neg}, {user_negative_prompt.strip()}" + else: + combined_neg_prompt = built_in_neg + return combined_neg_prompt + + @staticmethod + def _to_uint8_frame(frame): + """ + Convert a frame tensor to uint8 format suitable for video encoding + """ + if frame.dtype == torch.uint8: + return frame + + # Assume frame is in [0, 1] or [-1, 1] range + if frame.min() < 0: + # [-1, 1] range + frame = (frame + 1) / 2 + + # Clamp to [0, 1] + frame = torch.clamp(frame, 0, 1) + + # Convert to [0, 255] + frame = (frame * 255).to(torch.uint8) + + return frame + + @classmethod + def _write_temp_video(cls, video) -> Tuple[bool, Optional[str], str]: + """ + Try to write video tensor/frames to a temporary mp4 file + Returns: (success, file_path, error_message) + """ + import tempfile + import cv2 + + try: + # Handle tensor input + if hasattr(video, 'shape'): + # Expected shape: [frames, height, width, channels] or [batch, frames, height, width, channels] + if len(video.shape) == 5: + # Take first batch + video = video[0] + + if len(video.shape) != 4: + return False, None, f"Unexpected video tensor shape: {video.shape}" + + frames = video + + # Handle list of frames + elif isinstance(video, (list, tuple)): + if len(video) == 0: + return False, None, "Empty video frame list" + + # Stack frames if they're separate tensors + if hasattr(video[0], 'shape'): + frames = torch.stack(list(video)) + else: + return False, None, "Unknown frame format in list" + + else: + return False, None, f"Cannot process video type: {type(video)}" + + # Convert to numpy + if hasattr(frames, 'cpu'): + frames = frames.cpu() + + if hasattr(frames, 'numpy'): + frames = frames.numpy() + + # Ensure uint8 format + if frames.dtype != np.uint8: + if frames.max() <= 1.0: + frames = (frames * 255).astype(np.uint8) + else: + frames = frames.astype(np.uint8) + + # Get dimensions + num_frames, height, width = frames.shape[:3] + + # Create temporary file + temp_fd, temp_path = tempfile.mkstemp(suffix='.mp4') + os.close(temp_fd) # Close the file descriptor + + # Setup video writer with better codec for ffmpeg compatibility + # Try different codecs in order of preference + codecs_to_try = [ + cv2.VideoWriter_fourcc(*'avc1'), # H.264 (best compatibility) + cv2.VideoWriter_fourcc(*'H264'), # Alternative H.264 + cv2.VideoWriter_fourcc(*'XVID'), # XVID (good compatibility) + cv2.VideoWriter_fourcc(*'mp4v') # Fallback + ] + + fps = 30 # Default FPS + out = None + + for fourcc in codecs_to_try: + out = cv2.VideoWriter(temp_path, fourcc, fps, (width, height)) + if out.isOpened(): + logger.info(f"Successfully created video writer with codec: {fourcc}") + break + else: + out.release() + out = None + + if out is None or not out.isOpened(): + return False, None, "Failed to open video writer with any codec" + + # Write frames + for i in range(num_frames): + frame = frames[i] + + # Handle channels + if len(frame.shape) == 3: + if frame.shape[2] == 3: + # RGB to BGR + frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) + elif frame.shape[2] == 4: + # RGBA to BGR + frame = cv2.cvtColor(frame, cv2.COLOR_RGBA2BGR) + + out.write(frame) + + out.release() + + logger.info(f"Created temporary video: {temp_path}") + return True, temp_path, "Success" + + except Exception as e: + return False, None, f"Error creating temp video: {str(e)}" + + @classmethod + def setup_device(cls, device_type: str = "auto", device_id: int = 0): + """Setup and return the appropriate device with memory optimization""" + # Try to import ComfyUI's model management if available + try: + import comfy.model_management as mm + device = mm.get_torch_device() + logger.info(f"Using ComfyUI device: {device}") + return device + except: + pass + + if device_type == "auto": + if torch.cuda.is_available(): + device = torch.device(f"cuda:{device_id}") + # Clear cache before loading + torch.cuda.empty_cache() + if hasattr(torch.cuda, 'reset_peak_memory_stats'): + torch.cuda.reset_peak_memory_stats(device) + else: + device = torch.device("cpu") + else: + device = torch.device(device_type) + + logger.info(f"Using device: {device}") + return device + + @classmethod + def unload_models(cls): + """Unload all models from memory and clear VRAM""" + logger.info("Unloading models from memory...") + + if cls._model_dict is not None: + # Move models to CPU first if they're on GPU + for key, model in cls._model_dict.items(): + if hasattr(model, 'cpu'): + try: + model.cpu() + except: + pass + + # Clear the model dictionary + cls._model_dict = None + + cls._cfg = None + cls._device = None + cls._model_path = None + + # Aggressive garbage collection + import gc + gc.collect() + + # Clear CUDA cache if available + if torch.cuda.is_available(): + torch.cuda.empty_cache() + torch.cuda.synchronize() + + logger.info("Models unloaded and memory cleared") + + @classmethod + def offload_models_to_cpu(cls): + """Move models to CPU to save VRAM""" + if cls._model_dict is not None: + logger.info("Offloading models to CPU...") + for key, model in cls._model_dict.items(): + if hasattr(model, 'cpu'): + try: + model.cpu() + except: + pass + + # Clear CUDA cache + if torch.cuda.is_available(): + torch.cuda.empty_cache() + + logger.info("Models offloaded to CPU") + + @classmethod + def move_models_to_device(cls): + """Move models back to the target device""" + if cls._model_dict is not None and cls._device is not None: + logger.info(f"Moving models to {cls._device}...") + for key, model in cls._model_dict.items(): + if hasattr(model, 'to'): + try: + cls._model_dict[key] = model.to(cls._device) + except: + pass + logger.info("Models moved to device") + + @classmethod + def download_with_resume(cls, url: str, dest_path: str, chunk_size: int = 8192) -> bool: + """Download file with resume capability""" + if not HAS_REQUESTS: + logger.error("requests library not available") + return False + + headers = {} + mode = 'wb' + resume_pos = 0 + + # Check if partial download exists + if os.path.exists(dest_path): + resume_pos = os.path.getsize(dest_path) + headers['Range'] = f'bytes={resume_pos}-' + mode = 'ab' + + try: + response = requests.get(url, headers=headers, stream=True, timeout=30) + + # Check if server supports resume + if resume_pos > 0 and response.status_code != 206: + logger.info("Server doesn't support resume, starting from beginning") + mode = 'wb' + resume_pos = 0 + response = requests.get(url, stream=True, timeout=30) + + response.raise_for_status() + + # Get total size + total_size = int(response.headers.get('content-length', 0)) + if response.status_code == 206: + # Partial content, adjust total size + content_range = response.headers.get('content-range', '') + if content_range: + total_size = int(content_range.split('/')[-1]) + + # Download with progress bar + with open(dest_path, mode) as f: + with tqdm(total=total_size, initial=resume_pos, unit='B', unit_scale=True, desc=os.path.basename(dest_path)) as pbar: + for chunk in response.iter_content(chunk_size=chunk_size): + if chunk: + f.write(chunk) + pbar.update(len(chunk)) + + return True + + except Exception as e: + logger.error(f"Download failed: {e}") + return False + + @classmethod + def check_disk_space(cls, required_gb: float) -> bool: + """Check if there's enough disk space""" + import shutil + + model_dir = folder_paths.models_dir + stat = shutil.disk_usage(model_dir) + available_gb = stat.free / (1024**3) + + if available_gb < required_gb: + logger.warning(f"Low disk space: {available_gb:.1f}GB available, {required_gb:.1f}GB required") + return False + return True + + @classmethod + def download_models(cls, model_variant: str = "hunyuanvideo-foley-xxl") -> Tuple[bool, str, Optional[str]]: + """ + Download model files with smart extraction and progress tracking + Returns: (success, message, extracted_path) + """ + if not HAS_REQUESTS: + return False, "requests library not installed. Please install it to enable auto-download.", None + + try: + # Use ComfyUI's foley models directory + foley_dir = folder_paths.folder_names_and_paths.get("foley", [None])[0] + if not foley_dir or len(foley_dir) == 0: + # Fallback to models directory + foley_dir = [os.path.join(folder_paths.models_dir, "foley")] + + model_dir = foley_dir[0] + os.makedirs(model_dir, exist_ok=True) + + # Get model info + model_info = MODEL_URLS.get(model_variant) + if not model_info: + return False, f"Unknown model variant: {model_variant}", None + + # Create variant-specific subdirectory + variant_dir = os.path.join(model_dir, model_info["extracted_dir"]) + os.makedirs(variant_dir, exist_ok=True) + + # Check if models already exist + required_files = cls._required_model_filenames(model_variant) + all_exist = all(os.path.exists(os.path.join(variant_dir, f)) for f in required_files) + + if all_exist: + logger.info(f"All required model files already exist in {variant_dir}") + return True, "Models already downloaded", variant_dir + + # Check disk space (rough estimate) + required_gb = 8.0 if model_variant == "hunyuanvideo-foley-xxl" else 5.0 + if not cls.check_disk_space(required_gb): + return False, f"Insufficient disk space. Need approximately {required_gb}GB free.", None + + # Download archive + archive_name = os.path.basename(model_info["url"]) + archive_path = os.path.join(model_dir, archive_name) + + # Check if archive already fully downloaded + if os.path.exists(archive_path): + # Verify size if possible + actual_size = os.path.getsize(archive_path) + logger.info(f"Archive exists with size: {actual_size / (1024**3):.2f}GB") + + # Try to extract first before re-downloading + try: + logger.info(f"Attempting to extract existing archive: {archive_path}") + with tarfile.open(archive_path, 'r:gz') as tar: + # Extract to parent directory so structure is correct + tar.extractall(path=model_dir) + + # Verify extraction + if all(os.path.exists(os.path.join(variant_dir, f)) for f in required_files): + logger.info("Successfully extracted from existing archive") + # Optionally remove archive to save space + try: + os.remove(archive_path) + logger.info("Removed archive file to save disk space") + except: + pass + return True, "Models extracted successfully", variant_dir + except Exception as e: + logger.warning(f"Failed to extract existing archive: {e}") + logger.info("Will attempt to re-download...") + + # Download the archive + logger.info(f"Downloading {model_variant} from {model_info['url']}...") + logger.info(f"This is a large file (~{model_info['size']}), please be patient...") + + success = cls.download_with_resume(model_info["url"], archive_path) + + if not success: + return False, "Failed to download model archive", None + + # Extract archive + logger.info(f"Extracting models to {model_dir}...") + try: + with tarfile.open(archive_path, 'r:gz') as tar: + # Check archive structure + members = tar.getmembers() + logger.info(f"Archive contains {len(members)} files") + + # Extract with progress + for member in tqdm(members, desc="Extracting"): + tar.extract(member, path=model_dir) + + logger.info(f"Models extracted to {variant_dir}") + + # Verify all required files exist + missing_files = [f for f in required_files if not os.path.exists(os.path.join(variant_dir, f))] + if missing_files: + return False, f"Extraction incomplete, missing files: {missing_files}", None + + # Remove archive to save space (optional) + try: + os.remove(archive_path) + logger.info("Removed archive file to save disk space") + except: + logger.info("Could not remove archive file, you may delete it manually to save space") + + # Also download required Hugging Face models + logger.info("Downloading required Hugging Face models...") + hf_models = [ + "google/siglip2-base-patch16-512", + "laion/larger_clap_general" + ] + + for hf_model in hf_models: + logger.info(f"Downloading {hf_model}...") + try: + from transformers import AutoModel, AutoTokenizer + if "siglip" in hf_model: + AutoModel.from_pretrained(hf_model) + elif "clap" in hf_model: + AutoTokenizer.from_pretrained(hf_model) + from transformers import ClapTextModelWithProjection + ClapTextModelWithProjection.from_pretrained(hf_model) + except Exception as e: + logger.warning(f"Could not pre-download {hf_model}: {e}") + logger.info("Model will be downloaded on first use") + + return True, f"Models downloaded and extracted successfully to {variant_dir}", variant_dir + + except Exception as e: + error_msg = f"Failed to extract archive: {str(e)}" + logger.error(error_msg) + return False, error_msg, None + + except Exception as e: + error_msg = f"Download error: {str(e)}" + logger.error(error_msg) + return False, error_msg, None + + @classmethod + def load_models(cls, model_path: str, config_path: str, auto_download: bool = True, + model_variant: str = "hunyuanvideo-foley-xxl", + memory_efficient: bool = False, cpu_offload: bool = False) -> Tuple[bool, str]: + """Load models if not already loaded or if path changed""" + try: + # Set default paths if empty + if not model_path.strip(): + # Try ComfyUI foley models directory first + foley_models_dir = folder_paths.folder_names_and_paths.get("foley", [None])[0] + if foley_models_dir and len(foley_models_dir) > 0: + # Prefer concrete subfolder for this variant if it exists + root_path = foley_models_dir[0] + expected_dir_name = MODEL_URLS.get(model_variant, {}).get("extracted_dir", "hunyuanvideo-foley-xxl") + candidate_path = os.path.join(root_path, expected_dir_name) + model_path = candidate_path if os.path.isdir(candidate_path) else root_path + else: + # Fallback to custom node directory + current_dir = os.path.dirname(os.path.abspath(__file__)) + model_path = os.path.join(current_dir, "pretrained_models") + + if not config_path.strip(): + current_dir = os.path.dirname(os.path.abspath(__file__)) + config_path = os.path.join(current_dir, "configs", "hunyuanvideo-foley-xxl.yaml") + + # Check if models are already loaded with the same path and memory mode + if (cls._model_dict is not None and + cls._cfg is not None and + cls._model_path == model_path and + cls._memory_efficient == memory_efficient): + + # If models are on CPU and we need them on device, move them + if cpu_offload and cls._device is not None: + cls.move_models_to_device() + + return True, "Models already loaded" + + # If switching memory modes, unload first + if cls._model_dict is not None and cls._memory_efficient != memory_efficient: + cls.unload_models() + + # Verify paths exist, attempt auto-download if not found + # If a root directory is given, try to refine to the expected subdir + if os.path.isdir(model_path): + expected_dir_name = MODEL_URLS.get(model_variant, {}).get("extracted_dir", "hunyuanvideo-foley-xxl") + candidate_path = os.path.join(model_path, expected_dir_name) + if os.path.isdir(candidate_path): + model_path = candidate_path + + # Ensure folder exists + os.makedirs(model_path, exist_ok=True) + + # Determine if any required files are missing + required_files = cls._required_model_filenames(model_variant) + missing_files = [f for f in required_files if not os.path.isfile(os.path.join(model_path, f))] + + if not os.path.exists(model_path) or not os.listdir(model_path) or missing_files: + if auto_download: + if missing_files: + logger.info(f"Missing model files: {missing_files}") + logger.info(f"Attempting to download {model_variant} models automatically into ComfyUI/models/foley...") + + download_success, download_message, downloaded_path = cls.download_models(model_variant) + if download_success and downloaded_path: + model_path = downloaded_path + logger.info(f"Using downloaded models at: {model_path}") + else: + return False, f"Model path does not exist and auto-download failed: {download_message}. Please manually place models in ComfyUI/models/foley/ or specify a valid path." + else: + return False, f"Model path does not exist: {model_path}. Please place models in ComfyUI/models/foley/ or specify a valid path. Auto-download is disabled." + + if not os.path.exists(config_path): + return False, f"Config path does not exist: {config_path}" + + # Setup device + cls._device = cls.setup_device("auto", 0) + + logger.info(f"Loading models from: {model_path}") + logger.info(f"Config: {config_path}") + logger.info(f"Memory efficient mode: {memory_efficient}") + logger.info(f"CPU offload: {cpu_offload}") + + # Load models + cls._model_dict, cls._cfg = load_model(model_path, config_path, cls._device) + cls._model_path = model_path + cls._memory_efficient = memory_efficient + + # If CPU offload is enabled, move models to CPU after loading + if cpu_offload: + cls.offload_models_to_cpu() + + logger.info("Models loaded successfully!") + return True, "Models loaded successfully!" + + except Exception as e: + error_msg = f"Failed to load models: {str(e)}" + logger.error(error_msg) + cls._model_dict = None + cls._cfg = None + cls._device = None + cls._model_path = None + return False, error_msg + + def set_seed(self, seed: int): + """Set random seed for reproducibility""" + # Clamp seed to valid range for numpy (0 to 2^32-1) + seed = int(seed) % (2**32) + + torch.manual_seed(seed) + np.random.seed(seed) + random.seed(seed) + if torch.cuda.is_available(): + torch.cuda.manual_seed(seed) + torch.cuda.manual_seed_all(seed) + + @torch.inference_mode() + def generate_audio(self, text_prompt: str, guidance_scale: float, + num_inference_steps: int, sample_nums: int, seed: int, + video=None, images=None, fps=24.0, + negative_prompt="", + output_format="video_path", + output_folder: str = "hunyuan_foley", + filename_prefix: str = "foley_", + memory_efficient: bool = False, + cpu_offload: bool = False): + """ + Generate audio for the input video/images with the given text prompt + Hybrid implementation supporting both VIDEO and IMAGE inputs + """ + try: + # Set seed for reproducibility + self.set_seed(seed) + + # Set default paths internally + model_path = "" + config_path = "" + auto_download = True + model_variant = "hunyuanvideo-foley-xxl" + + # Load models if needed + success, message = self.load_models( + model_path, config_path, auto_download, model_variant, + memory_efficient, cpu_offload + ) + if not success: + # Return empty values that won't cause downstream errors + logger.error(f"Model loading failed: {message}") + empty_audio = {"waveform": torch.zeros((1, 1, 48000)), "sample_rate": 48000} + empty_frames = torch.zeros((1, 64, 64, 3), dtype=torch.float32) # Dummy frames (VHS compatible) + return ("", empty_frames, empty_audio, f"❌ {message}") + + # If CPU offload is enabled, move models to device for inference + if cpu_offload and self._device is not None: + self.move_models_to_device() + + # Validate inputs - need either video or images + if video is None and images is None: + empty_audio = {"waveform": torch.zeros((1, 1, 48000)), "sample_rate": 48000} + empty_frames = torch.zeros((1, 64, 64, 3), dtype=torch.float32) + return ("", empty_frames, empty_audio, "❌ Please provide either video or images input!") + + if video is not None and images is not None: + logger.warning("Both video and images provided. Using video input and ignoring images.") + + # Clean text prompt + if text_prompt is None: + text_prompt = "" + text_prompt = text_prompt.strip() + + # Process negative prompt (from PR #3) + combined_negative = self._process_negative_prompt(negative_prompt) + + logger.info(f"Processing with prompt: {text_prompt}") + logger.info(f"Negative prompt: {combined_negative}") + logger.info(f"Generating {sample_nums} sample(s)") + logger.info(f"Output format: {output_format}") + + # Determine video file path + video_file = None + temp_video_created = False + + if video is not None: + # Handle VIDEO input + logger.info(f"Video input type: {type(video)}") + video_file = self._extract_video_path(video) + + if video_file is None: + # Try to serialize VIDEO tensors/frames to a temp mp4 + ok, temp_path, msg = self._write_temp_video(video) + if ok: + video_file = temp_path + temp_video_created = True + logger.info(f"Serialized VIDEO tensor to temp file: {video_file}") + else: + logger.error(msg) + + elif images is not None: + # Handle IMAGE input (from PR #3) + logger.info(f"Images input type: {type(images)}") + logger.info(f"Converting {images.shape[0] if hasattr(images, 'shape') else 'unknown'} images to video at {fps} FPS") + + video_file, convert_msg = self._extract_frames_from_image_input(images, fps) + if video_file is not None: + temp_video_created = True + # Verify temp video was created successfully + if os.path.exists(video_file): + file_size = os.path.getsize(video_file) + logger.info(f"Created video from images: {video_file} ({file_size} bytes)") + else: + logger.error(f"Temp video file not found after creation: {video_file}") + video_file = None + else: + logger.error(f"Image conversion failed: {convert_msg}") + + if video_file is None: + logger.error("Could not process input - no valid video file obtained") + empty_audio = {"waveform": torch.zeros((1, 1, 48000)), "sample_rate": 48000} + empty_frames = torch.zeros((1, 64, 64, 3), dtype=torch.float32) + return ("", empty_frames, empty_audio, "❌ Could not process input format") + + if not os.path.exists(video_file): + empty_audio = {"waveform": torch.zeros((1, 1, 48000)), "sample_rate": 48000} + empty_frames = torch.zeros((1, 64, 64, 3), dtype=torch.float32) + return ("", empty_frames, empty_audio, f"❌ Video file not found: {video_file}") + + # Feature processing with negative prompt support + logger.info("Processing video features...") + + # Custom feature processing that handles negative prompts + # This is inspired by PR #3's approach + try: + # Use the original feature_process but we'll handle negative prompts in text processing + visual_feats, text_feats, audio_len_in_s = feature_process( + video_file, + text_prompt, + self._model_dict, + self._cfg + ) + + # If we want to integrate negative prompts more deeply, we could modify the text features here + # For now, we rely on the guidance during generation + + except Exception as e: + logger.error(f"Feature processing failed: {e}") + empty_audio = {"waveform": torch.zeros((1, 1, 48000)), "sample_rate": 48000} + empty_frames = torch.zeros((1, 64, 64, 3), dtype=torch.float32) + return ("", empty_frames, empty_audio, f"❌ Feature processing failed: {str(e)}") + + # Generate audio + logger.info("Generating audio...") + audio, sample_rate = denoise_process( + visual_feats, + text_feats, + audio_len_in_s, + self._model_dict, + self._cfg, + guidance_scale=guidance_scale, + num_inference_steps=num_inference_steps, + batch_size=sample_nums + ) + + # Create output directory structure + output_dir = folder_paths.get_output_directory() + + # Create subfolder if specified + if output_folder and output_folder.strip(): + output_dir = os.path.join(output_dir, output_folder.strip()) + os.makedirs(output_dir, exist_ok=True) + + # Generate timestamp for unique filename + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + + # Save audio file + audio_filename = f"{filename_prefix}audio_{timestamp}_{seed}.wav" + audio_output = os.path.join(output_dir, audio_filename) + torchaudio.save(audio_output, audio[0], sample_rate) + + # Create audio result dict with correct shape for ComfyUI + audio_tensor = audio[0].unsqueeze(0) # Add batch dimension + if len(audio_tensor.shape) == 2: + audio_tensor = audio_tensor.unsqueeze(1) # Add channel dimension if needed + audio_result = {"waveform": audio_tensor, "sample_rate": sample_rate} + + # Handle different output formats + video_output_path = "" + video_frames = torch.zeros((1, 64, 64, 3), dtype=torch.float32) # Default empty frames (VHS compatible) + + if output_format in ["video_path", "both"]: + # Create video with audio + video_filename = f"{filename_prefix}video_{timestamp}_{seed}.mp4" + video_output_path = os.path.join(output_dir, video_filename) + + logger.info(f"Merging audio with video: {audio_output} + {video_file} -> {video_output_path}") + + # Merge audio and video + try: + # Extra logging for debugging IMAGE input issues + if temp_video_created: + logger.info(f"Merging audio with temporary video created from images") + # Check video properties + import cv2 + cap = cv2.VideoCapture(video_file) + if cap.isOpened(): + fps = cap.get(cv2.CAP_PROP_FPS) + frame_count = cap.get(cv2.CAP_PROP_FRAME_COUNT) + width = cap.get(cv2.CAP_PROP_FRAME_WIDTH) + height = cap.get(cv2.CAP_PROP_FRAME_HEIGHT) + logger.info(f"Temp video properties: {width}x{height}, {fps} FPS, {frame_count} frames") + cap.release() + else: + logger.error(f"Cannot open temporary video for inspection: {video_file}") + + # Check if both input files exist before merging + if not os.path.exists(audio_output): + raise Exception(f"Audio file does not exist: {audio_output}") + if not os.path.exists(video_file): + raise Exception(f"Video file does not exist: {video_file}") + + audio_size = os.path.getsize(audio_output) + video_size = os.path.getsize(video_file) + logger.info(f"Input files - Audio: {audio_size} bytes, Video: {video_size} bytes") + + merge_audio_video(audio_output, video_file, video_output_path) + logger.info(f"Successfully created video with audio: {video_output_path}") + + # Verify the output file exists and has content + if os.path.exists(video_output_path): + file_size = os.path.getsize(video_output_path) + logger.info(f"Output video file size: {file_size} bytes") + if file_size == 0: + raise Exception("Output video file is empty") + else: + raise Exception(f"Output video file was not created: {video_output_path}") + + except Exception as e: + logger.error(f"Failed to merge audio and video: {e}") + logger.error(f"This is a critical issue when using IMAGE input - video path will show only the temporary video without audio") + + # For IMAGE input, we need to create a video with audio even if the merge fails + # Try alternative merge approach for temp videos created from images + if temp_video_created: + logger.info("Attempting alternative merge for temporary video from images...") + try: + # Create a new merged video using cv2 and direct file handling + alternative_video_path = f"{video_output_path}_alt.mp4" + + # Use a simpler ffmpeg command that might work better with temp videos + import subprocess + alt_command = [ + "ffmpeg", "-y", + "-i", video_file, + "-i", audio_output, + "-c:v", "libx264", # Re-encode video + "-c:a", "aac", + "-strict", "experimental", + "-shortest", # Match shortest stream + alternative_video_path + ] + + result = subprocess.run(alt_command, capture_output=True, text=True) + if result.returncode == 0 and os.path.exists(alternative_video_path): + # Success with alternative method + if os.path.exists(video_output_path): + os.remove(video_output_path) + os.rename(alternative_video_path, video_output_path) + logger.info(f"Alternative merge successful: {video_output_path}") + else: + logger.error(f"Alternative merge failed: {result.stderr}") + raise Exception("Both primary and alternative merge failed") + + except Exception as alt_e: + logger.error(f"Alternative merge also failed: {alt_e}") + # Last resort: return temp video path but warn user + video_output_path = video_file + logger.warning("Returning temporary video without audio - merge failed completely") + else: + # If merge fails with regular video, return the original video path + video_output_path = video_file + + if output_format in ["frames", "both"]: + # Extract frames from video for IMAGE output (VHS compatible format) + try: + import cv2 + cap = cv2.VideoCapture(video_file) + frames_list = [] + + while True: + ret, frame = cap.read() + if not ret: + break + # Convert BGR to RGB (VHS expects this) + frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + # Convert to float32 and normalize to [0, 1] range (VHS format) + frame_normalized = np.array(frame_rgb, dtype=np.float32) / 255.0 + frames_list.append(frame_normalized) + + cap.release() + + if frames_list: + # Convert to tensor [batch, height, width, channels] - VHS compatible + video_frames = torch.from_numpy(np.stack(frames_list)) + logger.info(f"Extracted {len(frames_list)} frames for VHS-compatible output") + + except Exception as e: + logger.warning(f"Could not extract frames: {e}") + # Keep default empty frames + + # Clean up temporary video if created and we have a final output video + if temp_video_created and os.path.exists(video_file): + if output_format == "frames": + # Only frames requested, safe to clean up temp video + try: + os.remove(video_file) + logger.info(f"Cleaned up temporary video: {video_file}") + except: + pass # Don't fail if cleanup fails + elif output_format in ["video_path", "both"]: + # We created a final video with audio, can clean up the temp video now + if video_output_path and os.path.exists(video_output_path): + try: + os.remove(video_file) + logger.info(f"Cleaned up temporary video after creating final output: {video_file}") + except: + pass + else: + # Final video creation might have failed, keep temp video as fallback + logger.warning(f"Final video not found, keeping temporary video: {video_file}") + video_output_path = video_file + + success_msg = f"✅ Generated audio successfully (output format: {output_format})" + if output_format in ["video_path", "both"]: + success_msg += f" - Video: {video_output_path}" + + logger.info(success_msg) + + # Memory cleanup if in efficient mode + if memory_efficient: + logger.info("Memory efficient mode: Cleaning up after generation...") + + # Clear intermediate variables + del visual_feats, text_feats, audio + + # Unload models + self.unload_models() + + # Force garbage collection + import gc + gc.collect() + + # Clear CUDA cache + if torch.cuda.is_available(): + torch.cuda.empty_cache() + + logger.info("Memory cleanup complete") + elif cpu_offload: + # Move models back to CPU after inference + self.offload_models_to_cpu() + + # Return based on output format + return (video_output_path, video_frames, audio_result, success_msg) + + except Exception as e: + error_msg = f"❌ Generation failed: {str(e)}" + logger.error(error_msg) + + # Clean up on error if memory efficient + if memory_efficient or cpu_offload: + try: + if memory_efficient: + self.unload_models() + elif cpu_offload: + self.offload_models_to_cpu() + except: + pass + + # Return valid empty outputs to prevent downstream errors + empty_audio = {"waveform": torch.zeros((1, 1, 48000)), "sample_rate": 48000} + empty_frames = torch.zeros((1, 64, 64, 3), dtype=torch.float32) + return ("", empty_frames, empty_audio, error_msg) + + +# Node mappings for ComfyUI +NODE_CLASS_MAPPINGS = { + "HunyuanVideoFoley": HunyuanVideoFoleyNode, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "HunyuanVideoFoley": "HunyuanVideo-Foley Generator", +} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..31bd912 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,56 @@ +[build-system] +requires = ["setuptools>=45", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "comfyui-hunyuanvideo-foley" +version = "1.0.0" +description = "ComfyUI custom node for HunyuanVideo-Foley text-video-to-audio synthesis" +readme = "README.md" +requires-python = ">=3.8" +authors = [ + {name = "ComfyUI HunyuanVideo-Foley", email = ""}, +] +keywords = ["comfyui", "audio", "video", "ai", "machine-learning"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Topic :: Multimedia :: Sound/Audio", + "Topic :: Scientific/Engineering :: Artificial Intelligence", +] + +dependencies = [ + "torch>=2.0.0", + "torchvision>=0.15.0", + "torchaudio>=2.0.0", + "numpy==1.26.4", + "scipy", + "diffusers", + "timm", + "accelerate", + "sentencepiece", + "huggingface_hub", + "pillow", + "av", + "einops", + "pyyaml", + "omegaconf", + "easydict", + "loguru", + "tqdm", + "setuptools", + "pandas", + "pyarrow", + "urllib3==2.4.0", +] + +[project.urls] +Homepage = "https://github.com/your-repo/ComfyUI_HunyuanVideoFoley" +Repository = "https://github.com/your-repo/ComfyUI_HunyuanVideoFoley.git" +Issues = "https://github.com/your-repo/ComfyUI_HunyuanVideoFoley/issues" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a577af2 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,39 @@ +# Core ML dependencies +#torch>=2.0.0 +#torchvision>=0.15.0 +#torchaudio>=2.0.0 +numpy>=1.26.4 +scipy + +# Deep Learning frameworks +diffusers +timm +accelerate + +# Transformers and NLP +transformers>=4.37.0 +sentencepiece +huggingface_hub + +# Audio processing +git+https://github.com/descriptinc/audiotools + +# Video/Image processing +pillow +av +einops + +# Configuration and utilities +pyyaml +omegaconf +easydict +loguru +tqdm +setuptools + +# Data handling +pandas +pyarrow + +# Network +urllib3==2.4.0 \ No newline at end of file diff --git a/test_node.py b/test_node.py new file mode 100644 index 0000000..f324488 --- /dev/null +++ b/test_node.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python3 +""" +Test script for ComfyUI HunyuanVideo-Foley custom node +""" + +import sys +import os +import tempfile +from pathlib import Path + +# Add the parent directory to path for imports +current_dir = Path(__file__).parent +parent_dir = current_dir.parent +sys.path.insert(0, str(parent_dir)) + +def test_imports(): + """Test that all required modules can be imported""" + print("Testing imports...") + + try: + from ComfyUI_HunyuanVideoFoley import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS + print("✅ Successfully imported node mappings") + + print(f"Available nodes: {list(NODE_CLASS_MAPPINGS.keys())}") + print(f"Display names: {NODE_DISPLAY_NAME_MAPPINGS}") + + return True + + except ImportError as e: + print(f"❌ Import failed: {e}") + return False + +def test_node_structure(): + """Test node class structure""" + print("\nTesting node structure...") + + try: + from ComfyUI_HunyuanVideoFoley.nodes import HunyuanVideoFoleyNode, HunyuanVideoFoleyModelLoader + + # Test HunyuanVideoFoleyNode + node = HunyuanVideoFoleyNode() + input_types = node.INPUT_TYPES() + + print("✅ HunyuanVideoFoleyNode structure:") + print(f" - Required inputs: {list(input_types['required'].keys())}") + print(f" - Optional inputs: {list(input_types.get('optional', {}).keys())}") + print(f" - Return types: {node.RETURN_TYPES}") + print(f" - Function: {node.FUNCTION}") + print(f" - Category: {node.CATEGORY}") + + # Test HunyuanVideoFoleyModelLoader + loader = HunyuanVideoFoleyModelLoader() + loader_input_types = loader.INPUT_TYPES() + + print("✅ HunyuanVideoFoleyModelLoader structure:") + print(f" - Required inputs: {list(loader_input_types['required'].keys())}") + print(f" - Return types: {loader.RETURN_TYPES}") + print(f" - Function: {loader.FUNCTION}") + + return True + + except Exception as e: + print(f"❌ Node structure test failed: {e}") + return False + +def test_device_setup(): + """Test device setup functionality""" + print("\nTesting device setup...") + + try: + from ComfyUI_HunyuanVideoFoley.nodes import HunyuanVideoFoleyNode + + device = HunyuanVideoFoleyNode.setup_device("auto") + print(f"✅ Device setup successful: {device}") + + return True + + except Exception as e: + print(f"❌ Device setup failed: {e}") + return False + +def test_utils(): + """Test utility functions""" + print("\nTesting utility functions...") + + try: + from ComfyUI_HunyuanVideoFoley.utils import ( + get_optimal_device, + check_memory_requirements, + format_duration, + validate_model_files + ) + + # Test device detection + device = get_optimal_device() + print(f"✅ Optimal device: {device}") + + # Test memory check + has_memory, msg = check_memory_requirements(device) + print(f"✅ Memory check: {msg}") + + # Test duration formatting + duration = format_duration(125.5) + print(f"✅ Duration formatting: 125.5s -> {duration}") + + # Test model validation (will fail without models, but that's expected) + is_valid, msg = validate_model_files("./pretrained_models/") + print(f"✅ Model validation: {msg}") + + return True + + except Exception as e: + print(f"❌ Utils test failed: {e}") + return False + +def test_requirements(): + """Test if key requirements are available""" + print("\nTesting requirements...") + + required_packages = [ + 'torch', + 'torchaudio', + 'numpy', + 'loguru', + 'diffusers', + 'transformers' + ] + + missing = [] + for package in required_packages: + try: + __import__(package) + print(f"✅ {package}") + except ImportError: + print(f"❌ {package} - not installed") + missing.append(package) + + if missing: + print(f"\nMissing packages: {', '.join(missing)}") + print("Run: pip install -r requirements.txt") + return False + + return True + +def main(): + """Main test function""" + print("🧪 Testing ComfyUI HunyuanVideo-Foley Custom Node") + print("=" * 50) + + tests = [ + ("Requirements", test_requirements), + ("Imports", test_imports), + ("Node Structure", test_node_structure), + ("Device Setup", test_device_setup), + ("Utils", test_utils), + ] + + passed = 0 + failed = 0 + + for test_name, test_func in tests: + print(f"\n🔍 Running test: {test_name}") + try: + if test_func(): + passed += 1 + print(f"✅ {test_name} PASSED") + else: + failed += 1 + print(f"❌ {test_name} FAILED") + except Exception as e: + failed += 1 + print(f"❌ {test_name} FAILED with exception: {e}") + + print("\n" + "=" * 50) + print(f"📊 Test Results: {passed} passed, {failed} failed") + + if failed == 0: + print("🎉 All tests passed! The custom node is ready for use.") + else: + print("⚠️ Some tests failed. Please check the issues above.") + + return failed == 0 + +if __name__ == "__main__": + success = main() + sys.exit(0 if success else 1) \ No newline at end of file diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..44ad3b5 --- /dev/null +++ b/utils.py @@ -0,0 +1,242 @@ +""" +Utility functions for ComfyUI HunyuanVideo-Foley custom node +""" + +import os +import tempfile +import torch +import numpy as np +from typing import Union, Optional, Tuple +from loguru import logger + +def tensor_to_video(video_tensor: torch.Tensor, output_path: str, fps: int = 30) -> str: + """ + Convert a video tensor to a video file + + Args: + video_tensor: Video tensor with shape (frames, channels, height, width) + output_path: Output video file path + fps: Frame rate for the output video + + Returns: + Path to the saved video file + """ + try: + import cv2 + + # Convert tensor to numpy and handle different formats + if isinstance(video_tensor, torch.Tensor): + video_np = video_tensor.detach().cpu().numpy() + else: + video_np = np.array(video_tensor) + + # Handle different tensor formats + if video_np.ndim == 4: # (frames, channels, height, width) + if video_np.shape[1] == 3: # RGB + video_np = np.transpose(video_np, (0, 2, 3, 1)) # (frames, height, width, channels) + elif video_np.shape[1] == 1: # Grayscale + video_np = np.transpose(video_np, (0, 2, 3, 1)) + video_np = np.repeat(video_np, 3, axis=3) # Convert to RGB + elif video_np.ndim == 5: # (batch, frames, channels, height, width) + video_np = video_np[0] # Take first batch + if video_np.shape[1] == 3: + video_np = np.transpose(video_np, (0, 2, 3, 1)) + + # Normalize values to 0-255 range + if video_np.max() <= 1.0: + video_np = (video_np * 255).astype(np.uint8) + else: + video_np = video_np.astype(np.uint8) + + # Get video dimensions + frames, height, width, channels = video_np.shape + + # Create video writer + fourcc = cv2.VideoWriter_fourcc(*'mp4v') + out = cv2.VideoWriter(output_path, fourcc, fps, (width, height)) + + # Write frames + for i in range(frames): + frame = video_np[i] + if channels == 3: + frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) # Convert RGB to BGR for OpenCV + out.write(frame) + + out.release() + logger.info(f"Video saved to: {output_path}") + return output_path + + except Exception as e: + logger.error(f"Failed to convert tensor to video: {e}") + raise + + +def get_video_info(video_path: str) -> dict: + """ + Get information about a video file + + Args: + video_path: Path to video file + + Returns: + Dictionary with video information + """ + try: + import cv2 + + cap = cv2.VideoCapture(video_path) + + info = { + 'fps': cap.get(cv2.CAP_PROP_FPS), + 'frame_count': int(cap.get(cv2.CAP_PROP_FRAME_COUNT)), + 'width': int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)), + 'height': int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)), + 'duration': int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) / cap.get(cv2.CAP_PROP_FPS) + } + + cap.release() + return info + + except Exception as e: + logger.error(f"Failed to get video info: {e}") + return {} + + +def ensure_video_file(video_input: Union[str, torch.Tensor, np.ndarray]) -> str: + """ + Ensure the video input is converted to a file path + + Args: + video_input: Video input (path, tensor, or array) + + Returns: + Path to video file + """ + if isinstance(video_input, str): + # Already a file path + if os.path.exists(video_input): + return video_input + else: + raise FileNotFoundError(f"Video file not found: {video_input}") + + elif isinstance(video_input, (torch.Tensor, np.ndarray)): + # Convert tensor/array to video file + temp_dir = tempfile.mkdtemp() + output_path = os.path.join(temp_dir, "input_video.mp4") + return tensor_to_video(video_input, output_path) + + else: + raise ValueError(f"Unsupported video input type: {type(video_input)}") + + +def validate_model_files(model_path: str) -> Tuple[bool, str]: + """ + Validate that all required model files exist + + Args: + model_path: Path to model directory + + Returns: + Tuple of (is_valid, error_message) + """ + required_files = [ + "hunyuanvideo_foley.pth", + "vae_128d_48k.pth", + "synchformer_state_dict.pth" + ] + + missing_files = [] + + for file_name in required_files: + file_path = os.path.join(model_path, file_name) + if not os.path.exists(file_path): + missing_files.append(file_name) + + if missing_files: + return False, f"Missing model files: {', '.join(missing_files)}" + + return True, "All required model files found" + + +def get_optimal_device() -> torch.device: + """ + Get the optimal device for model execution + + Returns: + PyTorch device + """ + if torch.cuda.is_available(): + # Get the device with most free memory + max_memory = 0 + best_device = 0 + + for i in range(torch.cuda.device_count()): + memory_free = torch.cuda.get_device_properties(i).total_memory + if memory_free > max_memory: + max_memory = memory_free + best_device = i + + device = torch.device(f"cuda:{best_device}") + logger.info(f"Using CUDA device: {device} with {max_memory / 1e9:.1f}GB memory") + return device + + elif torch.backends.mps.is_available(): + device = torch.device("mps") + logger.info("Using MPS device (Apple Silicon)") + return device + + else: + device = torch.device("cpu") + logger.info("Using CPU device") + return device + + +def check_memory_requirements(device: torch.device, required_gb: float = 16.0) -> Tuple[bool, str]: + """ + Check if the device has enough memory for model execution + + Args: + device: PyTorch device + required_gb: Required memory in GB + + Returns: + Tuple of (has_enough_memory, message) + """ + if device.type == "cuda": + properties = torch.cuda.get_device_properties(device) + total_memory = properties.total_memory / 1e9 # Convert to GB + + if total_memory < required_gb: + return False, f"GPU has {total_memory:.1f}GB memory, but {required_gb}GB is recommended" + else: + return True, f"GPU has {total_memory:.1f}GB memory (sufficient)" + + elif device.type == "mps": + # MPS doesn't have a direct way to check memory, assume it's sufficient + return True, "Using MPS device (memory check not available)" + + else: + # CPU - assume it has enough memory + return True, "Using CPU (no memory limit)" + + +def format_duration(seconds: float) -> str: + """ + Format duration in seconds to human readable format + + Args: + seconds: Duration in seconds + + Returns: + Formatted duration string + """ + if seconds < 60: + return f"{seconds:.1f}s" + elif seconds < 3600: + minutes = int(seconds // 60) + remaining_seconds = seconds % 60 + return f"{minutes}m {remaining_seconds:.1f}s" + else: + hours = int(seconds // 3600) + remaining_minutes = int((seconds % 3600) // 60) + return f"{hours}h {remaining_minutes}m" \ No newline at end of file