From 6840820f8d34ffb273e8776b8ba73a8fb0e89d17 Mon Sep 17 00:00:00 2001 From: aven Date: Sun, 8 Jun 2025 16:07:40 +0800 Subject: [PATCH] init --- .github/workflows/publish_action.yml | 26 ++ .gitignore | 3 + README.md | 63 ++++ __init__.py | 3 + .../av-audio2subtitle-with-prompt.json | 311 ++++++++++++++++++ .../av-audio2subtitle-without-prompt.json | 241 ++++++++++++++ example_workflow/workflow.webp | Bin 0 -> 25868 bytes format.py | 136 ++++++++ nodes.py | 220 +++++++++++++ pyproject.toml | 15 + requirements.txt | 5 + 11 files changed, 1023 insertions(+) create mode 100644 .github/workflows/publish_action.yml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 __init__.py create mode 100644 example_workflow/av-audio2subtitle-with-prompt.json create mode 100644 example_workflow/av-audio2subtitle-without-prompt.json create mode 100644 example_workflow/workflow.webp create mode 100644 format.py create mode 100644 nodes.py create mode 100644 pyproject.toml create mode 100644 requirements.txt diff --git a/.github/workflows/publish_action.yml b/.github/workflows/publish_action.yml new file mode 100644 index 0000000..be84400 --- /dev/null +++ b/.github/workflows/publish_action.yml @@ -0,0 +1,26 @@ +name: Publish to Comfy registry +on: + workflow_dispatch: + push: + branches: + - master + - main + paths: + - "pyproject.toml" + +permissions: + issues: write + +jobs: + publish-node: + name: Publish Custom Node to registry + runs-on: ubuntu-latest + if: ${{ github.repository_owner == 'avenstack' }} + steps: + - name: Check out code + uses: actions/checkout@v4 + - name: Publish Custom Node + uses: Comfy-Org/publish-node-action@v1 + with: + ## Add your own personal access token to your Github Repository secrets and reference it here. + personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dc98536 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.pyc +__pycache__/ +.hisotry/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..afd2575 --- /dev/null +++ b/README.md @@ -0,0 +1,63 @@ +## ComfyUI FunAsr Nodes + +使用FunAsr进行语音识别,并转为文本或字幕文件。 + +![workflow](example_workflow/workflow.webp) + +## 功能 + +- [x] 语音识别 +- [x] 语音时间戳预测 +- [x] 语音转字幕 + +## 安装 + +``` +cd ComfyUI/custom_nodes +git clone https://github.com/avenstack/ComfyUI-AV-FunASR.git +cd ComfyUI-AV-FunASR +pip install -r requirements.txt +``` + +## 模型下载 + +1. [语音识别](https://modelscope.cn/models/iic/speech_seaco_paraformer_large_asr_nat-zh-cn-16k-common-vocab8404-pytorch/files) +2. [语音端点检测](modelscope.cn/models/iic/speech_fsmn_vad_zh-cn-16k-common-pytorch/files) +3. [语音时间戳预测](modelscope.cn/models/iic/speech_timestamp_prediction-v1-16k-offline) + +模型存放目录:`models/ASR/FunASR/iic` +``` + iic + ├── speech_fsmn_vad_zh-cn-16k-common-pytorch + │   ├── README.md + │   ├── am.mvn + │   ├── config.yaml + │   ├── configuration.json + │   ├── example + │   ├── fig + │   └── model.pt + ├── speech_seaco_paraformer_large_asr_nat-zh-cn-16k-common-vocab8404-pytorch + │   ├── README.md + │   ├── am.mvn + │   ├── asr_example_hotword.wav + │   ├── config.yaml + │   ├── configuration.json + │   ├── example + │   ├── fig + │   ├── model.pt + │   ├── seg_dict + │   └── tokens.json + └── speech_timestamp_prediction-v1-16k-offline + ├── README.md + ├── am.mvn + ├── config.yaml + ├── configuration.json + ├── example + ├── model.pt + ├── seg_dict + └── tokens.json +``` + +## 鸣谢 + +- [ComfyUI](https://github.com/comfyanonymous/ComfyUI) \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..d721463 --- /dev/null +++ b/__init__.py @@ -0,0 +1,3 @@ +from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS + +__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] \ No newline at end of file diff --git a/example_workflow/av-audio2subtitle-with-prompt.json b/example_workflow/av-audio2subtitle-with-prompt.json new file mode 100644 index 0000000..01a43a2 --- /dev/null +++ b/example_workflow/av-audio2subtitle-with-prompt.json @@ -0,0 +1,311 @@ +{ + "id": "377e0192-2ad2-408a-871c-6a86cf058f73", + "revision": 0, + "last_node_id": 33, + "last_link_id": 50, + "nodes": [ + { + "id": 2, + "type": "LoadAudio", + "pos": [ + 182.18014526367188, + 389.2699890136719 + ], + "size": [ + 270, + 136 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "AUDIO", + "type": "AUDIO", + "links": [ + 24 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.32", + "Node name for S&R": "LoadAudio" + }, + "widgets_values": [ + "4_jidaohuman-megatts3_0.flac", + null, + null + ] + }, + { + "id": 25, + "type": "PrimitiveStringMultiline", + "pos": [ + 186.9199676513672, + 591.9599609375 + ], + "size": [ + 260, + 163 + ], + "flags": { + "collapsed": false + }, + "order": 1, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [ + 47, + 48 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.32", + "Node name for S&R": "PrimitiveStringMultiline" + }, + "widgets_values": [ + "直观来说,测试时试错的步骤只有当能带来信息增益的情况下才有益,然而人们并没有在RL训练中告诉模型试错和反思带来的信息增益。" + ] + }, + { + "id": 18, + "type": "AVSpeechTimestamp", + "pos": [ + 489.56988525390625, + 374.6600341796875 + ], + "size": [ + 210, + 128 + ], + "flags": { + "collapsed": false + }, + "order": 2, + "mode": 0, + "inputs": [ + { + "name": "audio", + "type": "AUDIO", + "link": 24 + }, + { + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": 47 + } + ], + "outputs": [ + { + "name": "text", + "type": "STRING", + "links": [] + }, + { + "name": "json_result", + "type": "STRING", + "links": [] + }, + { + "name": "asr_result", + "type": "ASRRESULT", + "links": [ + 43 + ] + } + ], + "properties": { + "Node name for S&R": "AVSpeechTimestamp" + }, + "widgets_values": [ + "", + false + ] + }, + { + "id": 30, + "type": "AVFormat2Subtitle", + "pos": [ + 518.5700073242188, + 561.3599853515625 + ], + "size": [ + 210, + 88 + ], + "flags": { + "collapsed": false + }, + "order": 3, + "mode": 0, + "inputs": [ + { + "name": "asr_result", + "type": "ASRRESULT", + "link": 43 + }, + { + "name": "text", + "type": "STRING", + "widget": { + "name": "text" + }, + "link": 48 + } + ], + "outputs": [ + { + "name": "subtitle", + "type": "STRING", + "links": [ + 45, + 50 + ] + } + ], + "properties": { + "Node name for S&R": "AVFormat2Subtitle" + }, + "widgets_values": [ + "" + ] + }, + { + "id": 31, + "type": "AVSaveSubtitles", + "pos": [ + 778.79541015625, + 357.02178955078125 + ], + "size": [ + 270, + 82 + ], + "flags": { + "collapsed": true + }, + "order": 4, + "mode": 0, + "inputs": [ + { + "name": "subtitles", + "type": "STRING", + "widget": { + "name": "subtitles" + }, + "link": 45 + } + ], + "outputs": [], + "properties": { + "Node name for S&R": "AVSaveSubtitles" + }, + "widgets_values": [ + "", + "subtitles" + ] + }, + { + "id": 33, + "type": "Bjornulf_ShowText", + "pos": [ + 814.5744018554688, + 427.3533935546875 + ], + "size": [ + 650.6199951171875, + 437.7900085449219 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "name": "text_value", + "type": "STRING", + "link": 50 + } + ], + "outputs": [], + "properties": { + "cnr_id": "bjornulf_custom_nodes", + "ver": "1.0.0", + "Node name for S&R": "Bjornulf_ShowText" + }, + "widgets_values": [ + "1\n00:00:00,170 --> 00:00:02,330\n直观来说 测试时试错的步骤\n\n2\n00:00:02,330 --> 00:00:04,590\n只有当能带来信息增益的情况下才有益\n\n3\n00:00:04,590 --> 00:00:06,870\n然而人们并没有在RL训练\n\n4\n00:00:06,870 --> 00:00:09,330\n中告诉模型试错和反思带来的信息增益\n\n5\n00:00:09,330 --> 00:00:09,470\n\n\n" + ] + } + ], + "links": [ + [ + 24, + 2, + 0, + 18, + 0, + "AUDIO" + ], + [ + 43, + 18, + 2, + 30, + 0, + "ASRRESULT" + ], + [ + 45, + 30, + 0, + 31, + 0, + "STRING" + ], + [ + 47, + 25, + 0, + 18, + 1, + "STRING" + ], + [ + 48, + 25, + 0, + 30, + 1, + "STRING" + ], + [ + 50, + 30, + 0, + 33, + 0, + "STRING" + ] + ], + "groups": [], + "config": {}, + "extra": { + "frontendVersion": "1.18.9", + "VHS_latentpreview": false, + "VHS_latentpreviewrate": 0, + "VHS_MetadataImage": true, + "VHS_KeepIntermediate": true + }, + "version": 0.4 +} \ No newline at end of file diff --git a/example_workflow/av-audio2subtitle-without-prompt.json b/example_workflow/av-audio2subtitle-without-prompt.json new file mode 100644 index 0000000..ba40555 --- /dev/null +++ b/example_workflow/av-audio2subtitle-without-prompt.json @@ -0,0 +1,241 @@ +{ + "id": "377e0192-2ad2-408a-871c-6a86cf058f73", + "revision": 0, + "last_node_id": 35, + "last_link_id": 53, + "nodes": [ + { + "id": 2, + "type": "LoadAudio", + "pos": [ + 187.18014526367188, + 368.2699890136719 + ], + "size": [ + 270, + 136 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "AUDIO", + "type": "AUDIO", + "links": [ + 51 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.32", + "Node name for S&R": "LoadAudio" + }, + "widgets_values": [ + "4_jidaohuman-megatts3_0.flac", + null, + null + ] + }, + { + "id": 31, + "type": "AVSaveSubtitles", + "pos": [ + 819.79541015625, + 213.02178955078125 + ], + "size": [ + 270, + 82 + ], + "flags": { + "collapsed": true + }, + "order": 3, + "mode": 0, + "inputs": [ + { + "name": "subtitles", + "type": "STRING", + "widget": { + "name": "subtitles" + }, + "link": 45 + } + ], + "outputs": [], + "properties": { + "Node name for S&R": "AVSaveSubtitles" + }, + "widgets_values": [ + "", + "subtitles" + ] + }, + { + "id": 34, + "type": "AVASRTimestamp", + "pos": [ + 467.7745361328125, + 206.14337158203125 + ], + "size": [ + 270, + 122 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "name": "audio", + "type": "AUDIO", + "link": 51 + } + ], + "outputs": [ + { + "name": "text", + "type": "STRING", + "links": null + }, + { + "name": "json_result", + "type": "STRING", + "links": null + }, + { + "name": "asr_result", + "type": "ASRRESULT", + "links": [ + 52 + ] + } + ], + "properties": { + "Node name for S&R": "AVASRTimestamp" + }, + "widgets_values": [ + 300, + false + ] + }, + { + "id": 30, + "type": "AVFormat2Subtitle", + "pos": [ + 485.57000732421875, + 415.3599853515625 + ], + "size": [ + 210, + 88 + ], + "flags": { + "collapsed": false + }, + "order": 2, + "mode": 0, + "inputs": [ + { + "name": "asr_result", + "type": "ASRRESULT", + "link": 52 + } + ], + "outputs": [ + { + "name": "subtitle", + "type": "STRING", + "links": [ + 45, + 53 + ] + } + ], + "properties": { + "Node name for S&R": "AVFormat2Subtitle" + }, + "widgets_values": [ + "" + ] + }, + { + "id": 35, + "type": "Bjornulf_ShowText", + "pos": [ + 804.8345336914062, + 269.0433654785156 + ], + "size": [ + 398, + 387 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "name": "text_value", + "type": "STRING", + "link": 53 + } + ], + "outputs": [], + "properties": { + "cnr_id": "bjornulf_custom_nodes", + "ver": "1.0.0", + "Node name for S&R": "Bjornulf_ShowText" + }, + "widgets_values": [ + "1\n00:00:00,150 --> 00:00:02,610\n直观来说测试时试错的步骤只有\n\n2\n00:00:02,610 --> 00:00:05,110\n当能带来信息增益的情况下才有益\n\n3\n00:00:05,410 --> 00:00:07,130\n然而人们并没有在all训练中\n\n4\n00:00:07,130 --> 00:00:10,225\n告诉模型试错和反思带来的信息增益\n\n" + ] + } + ], + "links": [ + [ + 45, + 30, + 0, + 31, + 0, + "STRING" + ], + [ + 51, + 2, + 0, + 34, + 0, + "AUDIO" + ], + [ + 52, + 34, + 2, + 30, + 0, + "ASRRESULT" + ], + [ + 53, + 30, + 0, + 35, + 0, + "STRING" + ] + ], + "groups": [], + "config": {}, + "extra": { + "frontendVersion": "1.18.9", + "VHS_latentpreview": false, + "VHS_latentpreviewrate": 0, + "VHS_MetadataImage": true, + "VHS_KeepIntermediate": true + }, + "version": 0.4 +} \ No newline at end of file diff --git a/example_workflow/workflow.webp b/example_workflow/workflow.webp new file mode 100644 index 0000000000000000000000000000000000000000..5055df78c23ca0d4937e83f67876aa33b0f7e80d GIT binary patch literal 25868 zcmYiNQ;;selCX`wZQFLQwr$(CZF{wC+qP}ncK2%A=D+uRGbc{nWJFzLR%9fetcp^S z6cYnA1pw4Vg%s2jI0)ZN0D$;8pd2772~cccz67xx>5`%%GUCS$k9uS%bGy%xgy?ls zTT!RQHuzdyA@iHGz7FB(ykDegzwDd0rQNvCu%c7uHzZtwGq(n0YrWsJXSfgVb(m@W zg1YOzkH3O`D!*eF_rK1se|Gra_7wGRbL0Kq`hGv-{G5L?et&-v`suIKe}DD$P30f< zKEKU+0Q|q79xiRqpPywF_&1HR2owAn z{4IVxKiBVI=L94A4}O#0K)%Lbb3X{L@?`o?eRJ;WSNi9DcOQYDyiW)p1P6Uno|wOW zKkCbIi~JqGi@#8JK;HKY3630R7p%0l$-9{+pq1jGws|f^^@N zKG|RC-@0$}Px<(|GlFfuyWhx{)8EJ6-+lg@)8D>tKScfx{-01*f#rll)Vbn>k36dW ze|_#raG%}jhVeqT&wQ!@L%z4`s;54FJvH{?oNp5LoGr80d;7c<@|WO}a3dGm@b?iy2XCXUEdmvZFeP4E_A4byyF2E( z^PQrABXLDwD^H7qEjSKX2BjEnXWHhMWBov7>d zaKK`fCEYjMA!Bs*CWsVnKck)RZ9&VuTrQJ6uYB2nE@<3;9c#aHG-OSCzoiO^5K6D8 zHWF9yP?hOKL?s(BrLcFmt6n?%7k#xH0d_PX$OFbB*{sKIX>zp!T(C{Y#*bevFr}!$ zyP`a%3+F+q3N7OD|LODPC!lc`gZO{y|B**o61wocSWTi#c3#(xbLiD~+Ef| z2h(b4_V{t!g#}FK=srHXF(>g9T#heSQusJ~m3)0tS#Uv)Ln2^DFTy&N9|`SdUc!A? zz)}~=s6t8Jz|!;F*}iOT1bz=cH>3a2q+%lnlE2>4LTmRd5Lks&tp8?&p=rqQRqu-o8)2~oCRtyRU~zhG z;cuYzzX40{kHAA!^+^akZV2^I7UV}?{M0i%H6rKFm}-Q+_7;D_>$=k_V<@{SK~f>k97jBg204VECU*gV{yx`3|o|avqX`W78E-(6ys}hml_B z|HRx{kMaXGBAn{?!UJvkdj?bvA%j8O1eeB~m*lxaB!`GpPfQhI!#6I&iKhA;57gI) z;oqnKO9PK{|IV;E$?G2!-3R3{h1Nz(v;DBf&5ESwKK6)tTt-7t<%L7f9Rb2{EB=iA z+19`B$GsEqW1&raa1&wN2?0bi*D2s!Y^J%rOn8TpQF0pc!1x+891%(ZCa;_Mnph(3 z^?bQjr`Pkp!cwI=M!sA;`JBX=8Ie;VN0zcH0<^JPi%X$>v5wQzO2Z^&v|t1r0(7E6 zpr!7?5^4Wo;m8kDb$Jk8?P69&xjsYXSrz(6|38WJzr`sp?%)*m97&2%h5(5J z@efqyi1PzykmS^U;|H~`RdHbeLTasz$T#zMkoPOT1f(Czl1hJiW!;q_iP+|vCPXkq z$UW}{of2?duIwS&QgywU=pg-Qh-m&8GE9tWE$ui)Tw=z&8vWf!DfPYWhDz6RI{o)n zR`0cH^Hl>y=u753$W{Bca~&jd96pm3M3+Kd@d`1`joreH6vb@v-#pWm2=YyOggI0W zHCNfr1!qJoTf|zQ$RDyRsHq}C5a(+Hm}>-HDwPW5KL-i_t^ns@blmPbp21U zr+KiQ@F$wgef+|^MJT=+_^ksuL#<%i7}$t z$7t{ILh|*#TEXw_+~n64+`vFis(GQ6FnJU7`0PssP^%(RzQi+#X>D^E&-3|NVCX%T zLcZ|uPvW>gYEPP^Ox=>wzRoWK;HR5C(Yk#~y!tWFQTM5?=L`_(34<9RX3KO;KkXRh z`D~K5mD6;7#O!d3WDsp^e$lU=VIRCfj>!CsFe6zsZT_XaF{XLXO{iKvA7tYfc-2*T zgyWdC%RP?RzJ?At)Y_s9KZ8HTz*fROxHV%NG25^&bKBosQ2zLd>(-8-sVD1caXHd4`3^%e zZs>%ZVtzqr;Mt}xMt@Bf%3M?f`_$dRuwNwD5dS!2OA@D+WFm*xTg>9;;*hNr5mcB| zcgf7ILrDt!!X3OXC2?;hlv8Vh(JYU^vucIeO~D7tp1M2)$fUtn77OZtGbb{a~r1L-Xps zUNAG0AFeicgJH-rHg!9-#>|N!EZ#%iu3xkr96`ZG#T&Q=kmErqur#*S4xM_Sf%xqY z6=nSOGcz+&G|Y{0(^S>@)Bt{3-0lGxIJ?&A*@4yrb#9cjy=re{6zXehe7~yG1Dc{A zC~#igsMHbc`{_wGIcXIfB2a9oDvrn;$jBN~^>JHfP=%>x?*x{ZTbgVngcab~!pKbI ze|y>&=oV0YX&BFNCLIzIPt&&yJe>!028l{D#?i*hchVv2852o{ zeJS-U1Pt1Tt|7OTYQ?mzg2*$f#|X;gAXt4_;KwjkASRv*#$F#q&zIt7w(%d4$EZM9 zP1DZUX{iDohyP&$zg=8KYbU1qPZbfH_i}P|4mMAfL<8J0P5w{(Yyi-Exilzqx{^&} z;~KF#7;@*5$UahuSbfXCMpX?wxli+=16MPP7DiAp`(B0WNZSnGy6Ww?&3`rf#C3{^ zQvvKR0$@+KVWD#u;urkh)za2#1MF*1Fe)0Z)m@{^9;TMmA(h^LihLTcX{H?14}4my zOooa@hEhRu0XUzS5{v9-=1wmmJ+c*u{q3e5Yy)De!8q*Z1m~r}zk{`>m+hlOOXh(I zc;0Y2SY=f6nk9P$w#BwP(L(Irc;W` zdouo)bE~`4DR+)Sd~a_4*G%ZOtO_5)+@d@6sGkYIesQD5t6ZH@xl0%D`@?)!!!zAP z8fW04)d*0M{{X?_FQ-qxb0=9@cKAySgJ8j=gn^4K=T2%mN(#X8HrP@^KdV0~=W?v~ ze!{$aKpnLwsaGpub{Rss34`Mo3#!ALJwT+MMY|(q7vad0cY9auQDJdISjh9gJo0~l z`v1;OWkBC;?W|?C@cXPqYra!t7t^^FBn!OqTEn5KGOmAI`hkH%0BKKr1-7p@HK0?G ziGduK^c@f+aA@dnE49V_9!9Kg0A}tgX~R}l6MrP`V&Jg%UvKRGAO8HuTnZy_>D-Ax zGL(SN<$vgH;1u*!FKwvR;OY6!rk&{%i%L5DCADccmbHD12F+LOZg1aQ7s(0ns_KyI}<8^^L;AjoI}i7IBy0 zN=EWj^tLvQd|>zF?oMC>s&!&_#|*&l6OD}(yX8qB<}SvQg6uBuX=xns#Ocm)ACf2E zn2ltZ_2x#QM+7x!Kg~;OH+N?n-ci6b?VN<0ylMus{J0Paw(6>x@5?P6%X2tT;W1+R ze8hG}J$mXkOZ}VgQ&OTD)}9OZUip-1G={E*S);(Z>+$_~)A(!u99LvKKFaLJv3Jl^ zA`SOSpn{A6FHvTgGK_%j0!_nqR^4ntJQfziGNPaEEM(h4H@#0G&${MEWN%-@b27!B z0~ge}P3+mrYK}#3+A$16Cc!3)c0+Pn5LaAUL=uDfeReV=Iy0Lw?v+P^oFZo#F8khd z{EvO((Y~6R@yS#*WG^0HO0#KJ%_T3mJm|ta203Uyq`PoR=?HCw&?j*d3noaUr+taBQZ}$2PG{c25$Vpi{%gE}hIbOtrrjJ)Y{Rbdy2kf( z_LKNfd8CFt9Tq&O;>%H!+_Pe2HBCkcXbk3nV6JSquYrFf1ao(vjNWNtFQVqh6#uJpQ@=; zR_Y94BLmunw8_Fl({QJ8p8S)VUgTp6IkSBlzFDrj4l8a<4qnB+N&@}x{n$ZbZ;XM* zOy0tcE1~f-Sbauh7Z;(=S*cp79{a41`zCz~8eYFV9vN_y_BJ zB2mykUA8VSrx)&QGi)&2+N*865;E@GX~OMzR+^SI4et85lpU_+YKRj(uO7-@UlqOl z_wgB<5K=@bJ0nVoTHE2yEp|SN)-m_u1Q^Fn(*X!e8+(YRwIy{Fv21DJBV&j?9R+$I zHnBtf?sK?!-%wu{^yNq`56}bs9vh_Zzwtv|KXJ9Sjm7WKrWoRW0j>=CzKRf7NMAL> zSs#5QTclY?DoZ2c)W27o7yKfiVV2x7>0(?-M!Aw>=P@B*t{7#E-3?%;M0CL&B3=h~ zUU=TWkC0JB3s^AOUav-`8XdZxf`7!XeT)m--POMSg81Wtq0ce~hIU(09c-Lf(?4ld@lh6IMfjWDGK30P-%yk3Nds$l zOOX(+=tCQvV=Eq31vtT)+mw=5h7BmV!R2PWBg^Mmx73?I}l*Td6>NTwv{Hc`~x!A`h%CO*A@f>&mG^s z8Z!|9BVu38{H=<(H4Hs6gtj$chzq03b2B&rWBXVu4tm^XIw}PMotnpE;1Q7=s-+2n zg2KYeWPDtO%}x_cxU(so0x5|DRpxbj`#-#HhBel zzoO;J5nv=F%TsPRO*$dL3YMC;o~|kz84VEzGg{7HP#u|Ife4XgjmL2v@!1Jndhz)a zbOM3@6`~2Io7-RXfloEZ#8z5Nj#JHsA^zGOaa!sqh@iO+u85GdfXf|t)W#IF| zs;Wt&f#thzV=O|WxliCh`jpT8b+&(pHYd3%?+{gRav2?N)!a?ZCdFQH?3)iZRD#Zm zVeb(y$Vqk+4d#GNwqj#K&8Xl*;Ng6Lks;S|la*MK5 zE^}at?oZsZxZq|kvvuQy>cGaMkfjnET6QWaH}k=A2i48<>;pUy)=2mobHIvx+vqH^ zx2p{r!->G(F+}Dg=P(07o=ip?&_UuBPb}XDep@UU$C3QmM_6$E4!JV$1GKbseMeql zA(*E$cX%9GInCbPHn!HPiJ7}UoxWQJLUpDcZrJ?!KA)&M=ew$WT1j3Br%my#62OA3 zxuy9QmBxK1=l2ALY`Ej5BcZ~R!jF%lsnfCajN08HNA?e^M?S|vPzFs+Y&JD9gOdPB zBa!VhYjGw{z5vzbvQv2I6v#zDtQ}c+%)g4sGr2X}@c7Jdkwe&7QzUS&MP2aS|2=Z~KR?^fe&0b)Tz#w;bhW9K)muGLMzb^z!hjl?abL zHW^#o)MKWMFYtu7#jm7w1;foSit84#T@|x&3Pk#eO8ZjJdTK0xkQ)XrhF?tVak$B! z=xDSaMrs291;m_9tmq2Q>LM+^BXR-x-aRQ0{t6keD4T95^4-jw57i0|Kt$71F;t=7 zoJ$!nZ%!Nt>MKG$OL;#M={67A$&DnpR$XKIeFZX zcewhp-@~kOOAa%DtLtEV13{g^nQ+AVsnrK zQy*<3tjl#w-T7OeaF8%{{}9%>n}iPW#LFS|nA%+&`bjYX{N6q- z(j7JUmyI5KXSX#uTz)cf0@x_n{LW{G`RjxX8_iHgEboK++hGC>M#@MyWh-@{*JNUk zUaDm7H0ftYS;JZXEX(0x-R7#eOd!QHAgWpV2tEz)dGgZJX`(aHD2X@Xh;eYCpYI}G zHML<{<2p}5O!-140Xc8hlNZx+ub)2q$61^cD90@_(>$cV4v%1dry?8w<0d3HJ8=on z0^I#5%E-i1Q9EvUtO2z(Ze{6Y-%SO}Ol?Fcl#>b*tN0=!Q`7tGOV@?1 zW>um|&HiPs)=i1N`W8fGk?1`hQ=EH=u4|8O|erL;bqwM>pQ zytc4|-*|S=16m?VPcE8y!0<;AUJ)` z#M}vE0vyz%??;j#m<;y+mi^E})bJ_MS8|vga+z`6N9EhuY!mfJHQyoI?`|;w8o;I1 zAW{9vAOd4UI#Uv|^>(SUS87F>wU1|{)a3mp(u%N1cY{IjO{{#u#9(K7|7n1(6>wNR zk{>^Swem={y!oDnq>7Rl%w^79d$78rTyxl1cR+z0@+SS!OZL-gH^N2f@QF&GKbnRXFCuRPmrP=zJ zbWkyPTrA)dQ)oT}iy$oreMFxbVoI6wJ4gtO92~ix{G%Ivx`nrrPJCN{^nOfl4giAl zp<8Fc*_zV5+zYhZI6utX*-TF)7_gSP5pdwDxD_-X@fR|ol2wgB zrGlyNU1r{8F+ndBurAq{C>Ckh7XpmHT4Y!8_H*%?mMu-PeC6qawAH-xVWl3=urQOs z)UnDtYscl3YDvY`8`ZhuX&UsvXc=!-{dvF8Cx4sH*~gA6TcC4zP|#Z@8k0q)Y|dznNYqRtyFA$J zdXUy3zwjOT*Z!Xfup?p%^7Q9|onlbWw1)68gp_Cz3PgwFj_~|D2nh;RirWj{w^GYB zE05Icd_#|ixESx8NRQGTKjf8dl+7>|kCYks7o)G??g*<=W1S&3TQO)^GRxNQ`1Oz&tlS!Nl z&#FVNrpCGLnR>UE4=8A|#|KtgbaD25xJ$9W!=(bTGkq-zos`DT^wjso?Q{ zCZyLH5>#T_pEtR`<2PRLfU(DJhChy9C6Bl;li9p1I)U4&3;*>FC9?Ko@z@1$7h&7A zI0Nb=2zBf`qA3;Ycxg4KmC2S%S&tjbb2EIO?LYjxl;d-v3gPwE?=J1?oHuHY><;2C zl)Bkbn1^sc)!(n~aa2qvVZV}lW@$dTCb9Xp2;mdE=CI~^d4oTn)4C_0>u)a`rb{6v zT-uFpM9v&)M>^y#v-KaqGBz4^xdn<5BXH3r30zFSC82-h=8UW)Idj;H1G+doG7RLF z#7bPz(#Np#z+rAnd!gR#G0UfVgOXAC*_SYY?L|9&{dQaf3W=WeUG$D?^(DtTzneA-sXm*bU z_6{D^lC@}#(0hSGc*k)J98C;)>h8h+^Dgn~!>peOePI)1NVm95X-&ecm7-Tj7Cnl= zA%I{1el#+^l7-YwJT_EV;8GCbY6n&iT@pRH*a$Hc67eJu-_hM`~ zS)Wy>$7&PN;ZGtK?KBzd;mOi<;lBugV=i;F2}W{+nlHEUU*2GaT=W3Ul)y%&ptH$7 zxe4sXb@GGBuUTOB46FQ9_t7I@zASqA92@$Y!uhM#%%6^$riYCa-cu!LVd+jimy}z@ zkrm26imv(gUl8C19+hY$wEoe5NP7Yd6@}o4MdCV>f@-^kDLCop#$JrmILU?Eloe46wb@(44RLO~J14m>dTMeQd<(sb76BE5eX}zElH34Us*X@@q`| z60BhK9PE+~1plNdp9Ue;Pno3}+BPdyf<`ESI)O3)#YDfbp3xXGMv+2S*2Rbnx9@E7 z2w4D$+nm8}a>g+8h5V8hK3X(-_gjrPzM(R(e_?R%RAY#2yWl?r2$D2&RtYK>xP?OJ zRA3?lYn+bq&**tpp~QAr&F(QdnH9rX8oUTNkpedzVhCBevR8{D;p1RHa#b2e{&N81 z#4CuWkoRvu^ZP_6hwEMJr}jd-MmfK?^MsdVzD z=uL_!3Sthx1IbrW+;%!-uXhA#20ZZD%n97;pY4wW0*poVTBGNBM{ZJz5tY>bwyO$3 zBbm|?=bBh@BC5K-23C$6%g$-HvSxbUg)d& zMw(o@yLkII`<9ec;X~nW2@yk6G_#fkrI!(8J=hE~yYK*!rmF9I1oAcvi4)7Q#3(z8 z&HnbC!*SqEiU|&$oR{jgxNy_6A65kX{yEZx*+6?Z1uRax15TQYh8{lGRY<4WQv%je zlHjq3w9XSM_5fv#1mTXm4qgD#d+>-f5eU-{DF%~B(ejo~A}-(}o>8h`ORIw&t+WvO z*$rym8yRY5CU#;m*Q$Kp-vgO94&j+sn#GJNc)E08Y56C|xVqa6p*$`Ymf@HbI{;?Y z(9B)fyz>s^Jg*%MPMnT&@ZgSm>!-l*M1uiUsyU4LYpboa%f-$!J!167v z%7ceUdLhs;0=T*M-zvQX+pb_w#Va$1GDacryk(+4|C%FMKAx_zOQc=n>e#FCr1bBu z(<_x$Kr$_IuQU0@iWQi_uH#VwcqbB|$}nfpemmO!OQkh1AgHDw*25wF1~XEnk?Q$Y zdT}IN8}DdOBtmFBrtIqCNk}BT?#P?5xTur-i8c)1cH7eRW_sxOh{&ZPLB1H*G5a4q zoptuZdQWnLs~pA0PigPL=T^l~{#Ax+;FMGydW%BNEuX`h?8d~q|AtLcr&oszCF9}O z1t#Nm*6?d{!(RyyA&8HZd|$@&;R=aS=G7tF3&hnk+kfSRSZ|0$zrdqXlJd?qFPnza z$!4M42|~a>z`bSu^^3%-`Tp|N_3{2cWV*|*ySrNCj8rE};HS>^TMf1eOf~@BL4XdL zMhg`*+xtJSYXkM5Af?hi$9OT`W3NA!>q0&n8cYv)&^>vyz8pz{m`!&c163H(jhAR0 z+UBD;4=JydmR{Y)8SOE90p}u6R|S3?3y;Yiwbkditv2$@`bkgg(E#p?{Fi0e7f9v# z+EZrmY3Gu1-y?Vo60^Wba$YHdj!h1I^oAMEM^}E&@+k|&RqL)%MYSi^u| zCon;wjRo1+3gquy8jzf?l8fLTe&8+$u(hVj2u5!sYYbEgSZeV9WT<8EG2!CP>)@k= zoLB&1HaEWdN=0ZMwJj={C=%XMug6FIs^RM@EOs7ig~BKTC5{psG1I_dDDh8epSo*t z92Wpc=@c^NUb{|Y6{7P!0<7$rv%zB>s<@pbra-~|3?{|wn7PaitZ5{^UtD@r(E%rm ze9t~5Q46hSVK)cYg400gz}cq{%A*jIB^B`G@2y<-45Px8q&A>?Vn0%JenYYDc;aka z@+ykUks&Q*v+h1@v2|pEkZb6zA9!t^>o3iU$|g($?O;Iwo#+$I;HnL|aVg`siO?i2 z1)CT(tM28P6u0$%2MXActb|r3FT(oc-72~0VKCybesAPpL-_Nu{mExM`W$tzB8 zno`_=?irrOa#ealAh5mznQGGm0)*(UmCW%J*^G2<6O&C2x(B9VXBWT!w$rwkh=FhT zpItg%;a?I`ffaX06~8j}|M`}KoDyEhqAU?EnpAU;iSIoA>{5h<2HPTWv7eanU7c8XJnwej;8r0qL%mtRP@hD^Z#|z{w<$llk+Gj2iv<@J???`Cwg1Nm~Lx{xw z%T&_%maKvaoG>v@$;9@6LBNIDoB=lluX}v)FSq6r8jdhL!H{SjEOpv#&!TR>0`Ei7qwN`(Rp zL$ecUcT?{TAuKhL;o~wgEXQn#M>W9Bh#ohKfM2+NuC9e%#%IP|Kh*aD95RR5C?|KqRt znTE|b$$7r??&MXhEa-WWp@BoK<#bitN;6GH-D8-D08$4Y^lt|du2R;L+A{$YLMT7X z&P_y)B?qaU>W!39Mbl)#QnH3^lhz>?@y$Zhk!j{_PVg62cHtEPb01Z=?LVLUcd?EA zJ<|)-AqOb#VKPe*U+9Xf%(OrFV<2*OGOeq#>vtvZL3;l_j^CFjHzBr~;ACFM7dxJ? zQpAZp$D_Y(KIc0iJJsTRZ9uaE4}aSlOVk~jb>WgbGuyQeN)PbOXY>1Jvt!2MS^x{m z!G6c#!p>hLY+x~Y?dw!l)M;vHu`47f)HsTI5EqoIbd1!JpjW|DFAjsuGSG1%sA%sH z3CDPsuOD}z58yv5rY?^&bNL5r*XT*Kmk=IV&J%Mkhfcc7Qq(^&D*UNLi-n`|@ZH2w zw0)=(QTjexMBDzJkkD8#yE^u$oPJB!lCuz%zhLa5I<~MuS$^P((Wc}eF?H`7OPM{v z%<5Z02dgKVAdMPHT8aE9E6+FR_&9cApWXIiWFY+YKWEG0C!P8W`9aj2=%kY~G@9dZ zjW!bJf^iA}KeT$)+W~*-TDLqqag$V=AgSW_NTgD@Y66Cm+X8H#kKzYF#LJq5Ne2sFqD zpqD`pv@YvW79k)iCa2=d%N+i{RkLne6OsndGr!YhZwlgy$|i%l25XN07`Q&g$m#Z| z2Y5##4vwx6wDn^mzJVA#&3JWtM1UA99$Yu9l6Gtu_P!e#SP2Fp+68uDfPiu#FT%ux zEABMiCl~LgN@Bhhnpudk{MF6cMc&i@`Ak2nmYrD9U{o^4FWWwU@6TKNxg4ds^2Ld) zcpG3ARlF|zV#kK+d&uy=QfQvF31c&x%c@=WpXM5tm4eJ!=ubE%PUJ`l>!rkqR0>*$ zkfXbBI>ZZ3wS<6QTy(u<(m*wVdA@-;EpWp-XwT^GpT$0TBfDQ&4a6k*XM@(^ALAy;J4Fz!}}^p$=Qp@mi${nwIo) zvKq!KKZmkt--veMxinP#l{zB@MM|&X*?kakG?R0<+sq1&=Vh3QUQqtE?d@GhSb4&c z4PvF8VVACp9NzZ%+>SAQ<}eRCvcyhigu@auRlnWE%$u%oN&)jKC6H+E?a+olb7m}J zb6I;n{5LcUoznxeAfGXQnjm3FF<@ebC|Kr`@3IK5@RjFu$hEy0lm_e@E30|v(;3l@ z_%Y*7$9W1_6}uKVq51^AX>BO9q>=PbvwdzCj%{4X=FInNfg?2_adL!0V_TnW~T7HU!9r!ejyES!N<96_wzbaE&-k|6~7P9TMAM!eEM4v&FKXu zkdn`2{a$;MEeCW9^J$<4viO0SYDuvIV1XbC40EBLstf~GH<)tvBZ=5Ac9~KEChZH) zzdVO`r~%JjR*n$e9k}oc)!BaZFxYUtdbw=@6}CX@TE2(3z=hu)PA>n>I>*eD-&a{`N-O|Cb&Q*oUe2t2dqT{1~Lmr z_o5ObDaj5~NA@v@iN68}0p7-?s=)z#x<6hnG#3C)h=KU;vd$*0a_nuwmN%uSOaZ>U z1qoC;5|Z&If4#QY(avs%?@uz5j~;Pc{|2idUBP{3tP13(9(DMu7&Zh#TYOx%!)|)% zw6|mFF8)m(#1bx;cLfb$muSrnYpF9${io&6?O$5#Xz(lsj)MG53hXD?t$PZjHCaX}=jR?=`a=+I)PhMj*iqgn zM-Q`J4|VVTcqOdV=RZwIC$`8CXDxFrM5u5%_LjyI&KPxA*1K1JIsqeYrVPk@Es`Lq zPUwd=rk!kq_;pw0JSW{;3~_f$@^M`X|NboFbcXL?F5|yO%OljNywhxD zGig$(euA9Y&&_s;#n+;+l*0Y#>~s}EoaLzpYZ+79RPYPnf{i?OqB$YMO+AA^n5i84 zM86yZFVfNF#XS*hfCTcv-GnH(kHzGV!!%6`wqq=~?Ahtu-sYKn=ySn--n%mSw~y}q zpnp%32{3a{mKfi*D`F{p7;zIE0+YO(HzR)}#}NnG3Sw=;`DY$Z-TswYL2?G0jlejc zJxJv7o>R&7z_L%qDSosBd9RBArhC`WKhDj!?+J)WjqCn9%8no6f8pbZ1uYwjx0W;)cRwu%j>w}@^Q!UpN%bC-j~PwyK;J)IUPw^lh$T@3Hm>G` zfUH~rPO_yzKL^v7fCaTAMuH+&jdA4-+*uSx*IBeP&v+jmj;y9Pkb!}4+4HLqh>Y!t z-2slEFSM9Y&n0G42%PT5(tnLQcX(jw+!QsF4A(A5(NR&(Tv8t>i)rJ3m|-}qfL=`n zWRChR3s4`0^gKizOObJZUn+0AuxWK6#pWLpRO@mq#bCI~%zH~p(UyRY=%_5AVW2ssOeji{hfgpk;2XST|$W~u!Oqg ze-QH;s2ws1N$|qzX0!L5B}?-Jn|nDW#d-<;qvO}6rL)$n5!5x`lfM&3o&Oo5t_33& zY{_qn<}ATj2L=q6no)OjzwQuF00uV8qEq^LR(Y{`x_oadTGYZZ_Q2%-H>Dw~9xzLq zLF=V}W{_A(=R-E}r)hux@3t(0Yi!Cpware#J>@}2xUT{471rffU;!YqB4;&b*zumE zlyX@~^aoa}V|ki5?dQQco^OU5yQnbZsD*AYZv!s8yF1x}Y~;+0BrzCJFO{%^cLY%qy=XO{wH>0{myhX^xK*F!2$WxMUzo}W?NEm#aB4V(x%IjB+{u$oY#p)b1GBV>WRjC{J~v` zd$IMaP2;RTKmKS}?v@oORekc!ML$Ih_xlhm)N;b%6mk1oiHz((+HhtEm$|6|1Bvx0 z`107$`~@u%D9w5nm)pucol<1l5e~+J?Wn&FxlSju?O>v#lW$m^=7nz!Tk>93>+Bfy zV`w|A83-c;(f?qJW*w{&^xNL)Equ1Tspq8sXEdCCKTA%F)%hE}k>X89aPtqZA1N^% zy>n$Ly~ZZJo#dQ49&9Vz`x;UQw5?C|oAeF_ZB_zzu0gcWq3eZu`Cab6l5XI$pCumz zZND|G+#v_L>L!NojjREqO;II1U05K8o6bw++Df256673cAQ11<+FK|QkmE#GR@Gp6qn(f^1%TnpxR@TiuGa?(; zx`un5ClG6&L_6+4e?$I2BGMa%Xt{LMaSaXENh5USxGjc zwX%;)zQ4koPJjn*$|};_Ru%u(S&-=ccr1>N8eQO(A|dP5{UuYzHiER&MqqZ*IHbbsV&LR1f)6E24mloE*CLQLj(v)$&)by z;J*t3_iFqL5?j8n6T2lOpsrzT${}QQ+v;5EaKvhD>yl_5F$BNN+}u>wMCjRo(Zz8n zd7|H7U-Hjy$L$%lo<_tUkFB%;%fG3+9V*K3lIUdgCJ{E?_~c_?-B;zH504ZNkBe)D z49-S*XPdhgZ3Uf;^n&(tJ~#BK65bL6Z}}fGObq*9cZzAat<^_mFDb?J7vqpqF(2*K zR?rFGO6#mJH)f%t$OM*AA~8GluMcW6UCEqV$4QBJ>n0an#*5dWg6ix`EC2 z+8dPWo7OK>0sCv|8Jbojb&z2M@N?8iJ{dkbO1#UKR=0SeYb;L)7A)wlYDDns;5&+t zW)Vh2YKb$5Y;rLo4udtHBj+{KXZV$J?RfriMpnkf6|YgRHGN3afjm(z>|V5xA+@h8 zD?4b8F@gs&Ag&Ce_TT$Wr{c8U(S3R@lUNQUXTNZOa+b?O(0X>}z*q-~v@ec=A6uvYH91E1iUL*#qN~KRsd!nD zyrJH)Uxn%_LAO(XaP?t&YYGx85BXNA<}O4tXMEs_D~PqFF^nZn8&|b32yybNKT?WG>P%hmNFh0;42e z!XuL0irxK`6rVpa+4hdl0}NHvF;((*A&}I^W6NBFDlHU!{4T@q!+Km2gd7@MrHOj3 z-{2Z*v$V_xj-Uw4n-XsaLSY{eto1lS!CdNrNv0JHI#y#rlNh=yiSZyp_r^A&E0IH98zOc8 z`pQ5(pZ;}GI<2`{+7aARH4_oiNJ{B;uyf7EO*>z>XS82u7f3wmI@BYkw;liFLL0QM zQa)I_TAD$spcd^WBQ_xgP8%y3hXlgf(-}7(srsFt_b#0EwDPR*;84($Zf}A{DETbE zi1EmKlYy9-mWwA{1%0bjdh?$&??@_UAc#UiM^h3VQ|f_GC8nW2gTNeh-jT`#} zqJ)>#GPVWC&(3W=jm8x!uNam7&_0!t>F(6R5sIyxY%H?eAb+_($M zJ1jw0lsK<(&$;wbejhMixXcdnmKOM+`W${*S;3q%gCu4v94V(@3e-la%pf|!dR6n7 zTQD8PCxlUB_)|p}=%O?O5ghyl*yL+>yka1l1kB_%o1IIvb}9A38*Phjfr(o4EL!v( z;fTOMM~pv`rESdOG8iihP-KJ3|wo{6dr_PO*WY&LZWoTim)oz1@2;M!tx}0FSR(c4j*&w zW4S)p7}pB#6o0vy+2(N!+qQzebrF@Yu9Ivf^&VxhN}-|H7#cIr*H+Vcb*|qmE!Urj z9_B!Ngb!*Cu|8zQizZ#DR3Z0o<}jk+Mew%>sa1Udh&86=nkC((S#3NQA^Yp!oNQah z<~blAhlu=(2x_GiF~)(6+O^=Rw2^uwv#U2+JPO6=4{=e<%LY)nhYVZR;P7(Fv%d9s2!VG%kE0P~>NFZ;pbRFCn7 zgo&A@oP93{IpsDf|5-)VC_JtCQZIiXysXB_i(kk#&NVQdNW;c0OeWsI?aqu zHUB8Q4R?uY|2$U}>#A=Yg|NL$QS8AZy0mEE<^u>osy`rGd7VqQthhSCCxqe z0&#V3C9L{cV(VZk6*IK)#dX8Zlan~_|6y206Q{b6Y0_89ddHQd=R0q0~`3;wVsDd{bN6p zD+6nf?&2tBcqC&pY6~m zPfV|fax240Lvp)Ju9W<}C!*&(P-U&z%lhk|rPw^K0hKmBTzGV||F?W3P0B2YH4o2= z@!;sgm1IL+jWRBPGXmAJzO_~w1)*d>z?#LKWM&AAPs#wc=U}hsLEtH*{pkw)D+qsl zI1;^r6>^*25sY=XNrSfz1BFy>Ov)c5Zy^5Z5OnttoPy?_vMX&kgl}v|3+^NsV^)@b{LC>%5T>^5$IR(whJIH~p-}(t+*V9h9jpU)&unvD_2*ki!PmQ1XZG z{1w)@C-8KmAH{bNusU_sVXQ!#737*u;#~~Kee1QlBCvhDHix=qqL-yOEt)vi#bT^$ z%(c-o+!P-(AoRf;JWyT9m2E)YuF4swzWf1D7zr~iC&$g&bPnzUw|k`CYORV=I9GTX zi(*S0p=jQ}3&U};tZE_ei;lVsPQq@2wZ3humAIPrxjw}D1Os#-96BQB{*ql^RN`Q1 z;x0DhhRQq`5?H2nd%B~Y>Hy>?8gz%BwsG7Q{0qUyKZGu059Ntpm4%O((w@u0W$u0H z@q9HMCr}00IlJ!bM6A~-h_1~kd9ME1`C}hFYA@mA>lLd4Ec!M)qR3r_;f@P{7CH;ODz&MLXcZ3nH_chJlkQKZLZEVvP{xr|>NIJz>ur8dDN$|5M?(l2E@ZTkC zllmv!f<^`axFW!BLSp3{=crk&=C{fop2Fi%6O)FtkpyGH7kykYx5hIXJMicS+F6d4 z+fS;*i3uccYyz|pCkPLqnBAq#B#|u$TS`9Nrus(;VkXyxwY+#L8*sade(7_kg#9Tr zbNBB=O71r?pYAW0J(;7VYP)J?YZ)a-(n2u84dRqv4cx0-tl{NX|HG5lEkop4pqQsY zG_#@;nU~=|4=s|7ATDmGFdCYcmf8fFANe%lAdiVXx9QA=@#V~4#V)Yj~AeHW% z93u@L*mW^ER!-9$USI(4NLU&KtLPF0tFKHTeW1>q$n&wkf=XVV%KNUDRG!yal!f~sIhYwhZV87!0;@{#!;>aJeh_5M) zw8pqt5Nh5Y{#9%lvwH||Wc-S!Cl=}}Cmg29Kou^t6f;)S28Cn?z(?0P6Z=CES=OmRZ=zQ zI=wzUbbQxUZp5)ixu_3eomai5GTOwwBJf$Kw&c&#{j<3~qhjM3kdPMvteD8w@Xts# zlk<>P&9U;t$Wg{vM)YKJz$X@Ee+xzl5c%=zUg^QZ743V@w8e~$)GJRxR(rPW*N~&^w-GN z=>?Q!APi1ec_fClx#;DeQAg()>=Ut+88zhmhBs-X920MqiAO zia5-z4jTp4xu*Q^b5TB|K59wlDcCr`hF3A8pKW?(U7DEK5)Wopt~M+rowg`kwqIo) z*G(W3=)WiRt^NXxpl|qcy!orTd`HW23A5iNykyqikS3b+rLndJ)v{JdMew4eDm_Ts zauWU;7Snl>yl54-%+$JIq^PxBnO7i+ZTU8PUeu5CjTSaA`ecMfMrpRQFFd zIPY*Hfz)gdB|BU0ePF@|);Ee5z@wgjlgWMesIUjBGYl6Vxu-zIy}}PFY#I)4A>~E*g$vZg zP4aZw#921VHT2lX48RVDYU;S50?kD_IM@cew`7{VRPV_rEoNZ&FaOf`4}Ev;-ZMoD z5L}gV%Pv^$&R{TwyEf55U4Jy{R_QtEW_DRgzQp}Cmkubpf4=EYjgVj7Aj?*q+f5EL z7)*NPT*C`C6A)eh;TXLzJ$pe|=Sz|uYV2Yl?)2zcGP~oqa}rjRYQr}MNVLmLIReRB zr95oJNT`Js%FTHc+>zHeyKBjt@;~d^fC^Fq!{0a`o6Hc?DeGXa?hSEY#oJIYBmX zU!1V;IHTn5goaH+(LWgCqUT(>{B$wk(zcOj^Mn<%(6esN$FkGPL_fOGZoj}^htCn` zMpQH729BOpv1j!oRq6#NB4@aQuIKMph4!Wc%GoK$kY?bwoh9lqr`;Ck*zUe(UK<+c z-lT-?kR+$xL~(N$Y8tA%Z(}3PVU7=a1D#@Jfu@;X)~mc+P{O!UA@x}8#xp?>&~Gny zrC4TP5WiNNK%ra^&61n`YxhWVU(WS>h@FWGF!M~3$Viv#i37{(DGqOfPVG-?gY3me z{{5|Lr?Wu48`-1C5H0aMD~2L8sMh#|@OUw9G6F69B?v@`>1h4Go}nd0XTef>dY`$o z86cO(rapEn%#m$L9W*$%S;)9yS^LNx))#L8OM@OohNttK4w}Cs2r|q~e**=IQcTf$ z@46MQkNd$pXGas$Z%m+NeI1C4P8kjRbT~441Z*%eo38m zs(smc*|u>VZ|Ss(vvJ?dmh@!u-{?~r>nnsOUXE?E0)uP>2J%&<~t=ZNHMl%BVwH!|uyioF?ypUOo^TBnTk zm2*@EfkS->NNWK7ZYI$3R%K}@NzbnsPV9{v8ZFFj5_R)U>>uuNtK>}_NA*UG<~UfjDU~bajb(%(h5Ze*5rlUoNb($B|Du4ZnDTW?_;z#` zD4hJr_10I25V}gZhvvSFpHda^I1+SU#{8t-M7;P~P<{?j5gid-jg)I(#+)^&x=*aO>X^>OZK$E;;spn~67s>D zifV_@BWy8HGw}(a7Py6`5~2={PLIoEevaBI#-P-lJ8<0$pa$k*C=3cQ54bRWq&zTEPyeb5_O>o?xZc8M3F^)497CF1P7`C42I zYdLbyqk#i&mj;-QZ@^eS#N(<2t-v(8n{a2{0V|J-BPYqsaE|DvnAun`#VG*UcaO|e zXWzY+jmAPDC6%sk`HdF_#mDZM=pzHM&tiSC?=b0P6C%MBcp(s`9lt{!16oQEsji+A z**^^#fN}l2z|TK7El@T;>bE-iero~_7(XGBR7^L%#X`fmNxrqu-XMQd^{@+EeBtUp zI;I!^gFQhQi8`f_PFV-9+gW1>^q49Zm6XCBZvK@mktfbW)X0_**Khy;0000045fk6 zyv;K%^U`pqE$LsC9M5nTu;(px)K-0Tq^sZfXGuBCRQ$EWgWvXsW|gUdz?M6qv;|?O zEyt=47KAKo{yyblZNkf*iQfuh7ndR@v*Mi6e{;}h#!`Q=cyM;ITJzW(h%gl@h3jCz zGN(9$yqS(eWi!Z=%%MRL9$mSONVWC$z}s7r@>2c;zoK`HVsJ;8K3BrKpNbTdJg_{yIVV8f_a!{PMj$ zuD-42iQ)I5DjJ3Uy&@zrFvjH6`;D)+nBnfgwz*j!;x*e&>a#UUjW`h_qT3chJhEG2acQ;-(M%o7T@u#uYdn$&+hL>s)VN>?_3ZW&FiZQ+~Qo<#~6p+p;+? zq||tNhY8=PNueh=+@j#Q+jdSjW@dv#J9VQ1L66C%#zn7H50H#mXXsNTk-3vhKg-< zCF`9wz#qv}-?+rHf1A5wq9Sq%vLy`-$e2787iT|_?-U;+Q`Ype<#JshinJ&j@$Fml zcQUYc!fKTd7m>FTc$!?SBPwA*;A}^o#STmX1^4?CnEHbYt16gYEN4o1eGD)IQpJRbX%Oz!x!q*v%mo|Rp({Mc*=_6fO0{=A2O zL+d*{T)z{b0^T~e{KEIW})ufaI5o?ULkt5H^yXQXP8$V}kIpQU1 z)eesI=au*8OX@9C*WXw)q&R0Z7fCVMWIBwZ&E{Qg@5t^y+80RWi2B%0Jx*R}zJ0&c z7%)7{t4dY-MTy7V_^K&GCp9){*E(BJ!YKCTz#u2QvW}_m;0%I4bZ+3&x%8_o;KuJ@0a<* zTd66i$nOhx>~Gn5pCrB-kcPB=9og1gH*u1R+otf?Yv&X!QGwtJ6SE29Rv5D>V4%l4 z5A1H34=X5zuX^eTnP8oLzhUr|(ktbjB3E=FU>@2q^w3>9_?;~I3M|F8BgAJWOVqQ- z)d4Njxr}tiNujKy(sw=T0a)!24@?n+N#Gp#GzD0ZMAe3V+fQ2w#(xe-L$1GLs##o+ zEL#Vl%WN+^x<;o+%C-UGGmTJ~R|`Z~|J7_;syU{VG%T5~^M^?YcuWoFkM14Vm3{dNT#qz> zkttR6^myz4jgmWo?Y))qnB65LXk{;=^gk|@bR@IF7Q(s*<5=C)?KO!$8d}~Oi@H=c zdJd&K%S{N{HR@J^)TGfLZSy&v-{S2FSf!IZeO>2{932rp4gun*>73}Dok{P;u|@p= za>zfpgeQkIcryJ#(bt_hF)vWPT95K(Ii@X52W{YG{I|y#*X5q7Xf(Qh9lJBZGqlKJ z9GzDrvDYDIHp(mQT-xhtUX>cW7YYE_0mZyD9I1+|8~M`GUaw8}BcI-hX!^3n6s*S5 zsQ!zjErEf}4jWDGFn$8=*3o6O*hVWR4vs}Dcc%r{hl{N>ex6?Q+?GrgC=*_>0cdR? zSmX7wsmmyhYjz4pU{EpZp(OL)ulq8nV&s(4wSFJv)p8$(&umLyVUdh-l41Yotg~n0 znFRABwPova$obNJO754hYI5j`h#Pu)!`F;|Nm$@%90F{tCW*hOylP0UkCuFfKM z91@EaO|H)p_@eH!xCbScDpP7|PYb$|q+n0rY1?sx|DI^KvJ~~NGLwUoR7i@~r*^ld z?4LgeD=@XKp&pigBTkTWB4cyCB?n2hUh*92TNg`7vq(YfEmqHOu)Y1C#Q^ckn{yBo z1&ao8OS<4`Z9C7WF~Fxi;`W(>TdTml$BldoTbM7uxLk$YH|S~iL0g1`+Hd}6stPPdPIkQMd}h3Oro0t6_KQUPg1>=> zAxnntT9?vgE2FNTS!jC%8#s_cq=h0^giQ)CS|X|l69r`BNA9WlAI3kxNo-jA1I;kaMZ$mG0TgaM?36xXFOK-Mr6 z+nSEF2>=$UsMkA`i)X$P0(`6{ffO{`i*%I02J@%)a_SBNCNZyqa?DqT6~NO?T(c!hbE; z-wrCnC47?xEYa7Lvt>ATfIlySQ2(-1-?>CdSIhIoWUOO4QcT@aE|wRdd{Q+Bfmak3}~16k@lhf29B&+6BO0EE{Sab;Y^Pfv^|zc=64 zpS2BzKC$2wQN+=I{vK2Ei6ElyR8n1%B>(tIM8MHmm}sJCVRMf^10UrM11M>d4wgaC z*R9^-n!${yKqM|$s7~NzjymN6xG%TpDnf|Aml+f**88O%J{wk~(ad0?D1qqj#f4tC zd=uu+#_@O|zZF5$Z*6nE2;JT~W(bqy3UjLOxre{@f4PF0LA00016_G$^b)t<&X*~?zv zHc;A^4&fbfb{*N^wmdu)A6L@{m0Hq$=7DUpfWd8;8d~2M@)e{axZ+AiiwGIR2~#xp zr42^N8|jaU&u9OpIV??Cxc`%;lNyTUO*H`qSO(-$s^;<=k(Y!DsEY$AQV!?r5iy=b zXY9WmE&db+jWF67!YMg)$D%}lTWq+N?1Wq-5zkIPj;-%k4>a9b?@y?D;QwgjId$dD zQLZ0?5||$8NrI;|j%)P;xWd0`!#n+|Qom!?bY|q2PKM_!b~O`n^F<9>7t%6+>|lJu z2pcSE(Bd}425Aw)#_9$34YxI74`ROky7aOK8&qUtjbX zkq0SXl)awN5kMo;sa0u7d`8!r%s_~?kfC|qCznPNzdNk6Co!AXzh=cS-FAjb5nrZ> z!W|Flor$?}*_U7_Dvg|EWJ2`c#Dx(T;7~h&Szk9gtmMJ}UH2Yvgw}MqC2n8{M(lT1 z>P2bAceVs=WuY=hSIg?8et663 zElW(&RJ%%Gg_~VxVupr=G}|8Paae{V{`M_$6Bx9twdnl}Xz z;e7szKOxp4kgwCs=r#?k4DD=RI{d<9jK>Gw+JyJFMp{Czej7iyXotE_-8WH&U$;r4 zL6d2`2%+vQ3aK*rldD_cLA7D8(6sXTNmi}!h~q_l!Ank~);%Q;ETnu3uCo;h!_uW3 zN@SI2L$6vWs6~oKrl$#pb$*SqjcC`wGI!ZPJQl*5?UP1(H2fR23WnG-VZiED4eAD< z4cASnAaf&o_2Xvwc?5US3gy)%fi~_Pi^=sWv6u2ZnH5ICnD_drGnj&eW!0a$)_K$1 z-jXW%)Ue=++f%hBWuo*m9&y(Nbi{e1>KsPc3Ra@tk^2>JVALdP2@jX6Du>l)vwq|8 zS-7bia7)lD7_lwtW~7!*8Ed= z?KSgIh2$V&`i^Dmh890&KjxV~l9)iHqUf+YH3X&niBnA?I05QB2yoqXJu;4c-91mY z2R4;LLmUt7E~!H=8tL`Tu1^t?aM85yxhTsKIY4Q8E9iEEnL7z-p)lp4Mb}N)jWno z!+@z6;cavf#N8*tJrk};s8!}Ai9%6$A>R7-ZfGY`%RRG1Xs{J{YXY;Rn-yzK#unus z-;*xTH9Q|q)y$PoXE#h zuhWGu3`lTJ>Tuy~paxifW^3$*(UG`eLJlv=?@j(P4S|x7q6sc0=xygO?p~?o2y9I;rk_(&Wd+b&O89l=_V2YO=qc{ib;TK?G zDyM4BDz#SVZ|%;V(1~TT9X_NmXnzP5)=?pG70eX4BLvi-35rAYHuA=ClOcQ zs-pEXGq5nQ0B_OE!(r%R@YBUGmvaW}#9)eob_*DEZYQ0BQN&wDPRDprJsVbf@U* zgxhig5{_TB6eIe5D|!S9`!7SMa0i1)x{6jXi-zjZURrUK+$DB|8@(4dp?06=05wOb z4>JM%j%md)+|d4-Ox9g!@A8XIBk+zrv}EAA)HDY4m*588ND#`)zZD6qU7iYom5y#$X;O!_@eQZ_tTjXjAS7^AfPudZ#ZSQbof~0Z}O{5nm64E$!xSn$O zSs8E=t1Nicoa1RkrKqR9Ye z_g}L9CWte+1P~!^2-9ENRD$zlcPeA&PeabQ`dSErC6?{|4Vb&y^YjWbj?{%sL&WR5 z`F`SgsAE#aXAm4zcsbTBlrDZeyq?Q8rV3pvSN(n zh=j@sP4{^ zz*XJ_wM4ELVdP1|H!{j(wRw3Ve7IJHUnK7nKihM|rB+WM9DOT)Np)NcuKyytGgp}9 z5rXn9)(e{7=Djpb;OMkxOpbJw%*yGfJzC&+Agqru8UfYb9!4Bc1GX>KeN*~%m&Ly( z#t3)IqYYRwfcYXn|Dp$}p-C6m!DZ6+6gFOvC#gh)!MsH0Nkg5W6DCT~G z>Ts4FN73ycj*ML4nz9v7{@CK5?Nn~2OjKG{3hGbLAS{|Bx2j;dLVkl_V14AjG|WIK zRhK+GJu>Av+C=*oB*xN&MO_Jb8jwJ|h3$1pJRh6_tu79yzx=5lq(uTOq%rs25re|z zH*E5Tx$F`kAo+o%K#nS6Ew`rD<29r-4Hs0RrNr%f(>v4cKzw538H_UJ{7>7LK9;Co zxxo(lX}pAxXGEHAYVv3EK|vgrOMfTJFLVV8evnmuI^*K4CO0KHa3Ugl_OU0#`Z2Dq z+i5u&V^z{QY)(tWCMgle5ryY%@VErwgDU+mMfx&@=?VkupUUAB2gd@mrVA_XiXEiU z5O>?aeLl$@`nesvF|_0qK5AeowEzYXc*o2KVr#lgKEU9x3$_7;?P4>IJmOUgF?hCi zdKhnb4Qp318rn1p)uywRnOaH`bD>E^xl&SZFcJUB+aQg&P?+y6@f(G8oB5xlqw})+ zA923Aph8S#e@EUtiNO#bx_NPKn?VPJ^oVUUJx(u{js*3rH2+rC9#oerWo6fo;X@5{ z?Z&HbQ^DX@T$Ynp@7NPpDDP@Ys23g)%NXp5cX z6cg^{#T)_*2QL_X(UDKpStywv=@Hb~l+$wCB4=?c&bu~gH_e6m_ibLpW9+EKztl6c zMQhB6?QAz>l1y2LCS;Gc?nB;H(>3q_5jqEu01a(+W}iZJE3JMn6>qtUB=DZY6N@L@ zc)F8Ss|>9#_?gO@7NMs#%A@GQc(f>z0x=X`K;147OgLgQSZLrlZ3IK+73KPZ>_r)v zZ+C0t?0Z9M7y(-b#DSb8ewjYWgosUO`Ey6)jFuV*Je5{mdqv;l|600056?8a0}zQ!TU1F4PI0K;Da$lgaV t 00:00:00,150 + """ + hours = ms // 3600000 # 1小时 = 3,600,000毫秒 + ms %= 3600000 + minutes = ms // 60000 # 1分钟 = 60,000毫秒 + ms %= 60000 + seconds = ms // 1000 # 1秒 = 1,000毫秒 + milliseconds = ms % 1000 + return f"{hours:02d}:{minutes:02d}:{seconds:02d},{milliseconds:03d}" + # 查询标点符号位置 + def find_punctuation_indices(self, words): + punctuation = """,。;!?!,;.?:""" + for i, word in enumerate(words): + if word in punctuation: + return i + return -1 + + # 清除句子中的断句符号 + def remove_sentence_punctuation(self, sentence): + """去除句子中的断句符号""" + punctuation = """,。;!?!,;.?:()()""" + for p in punctuation: + sentence = sentence.replace(p, ' ') + sentence = sentence.strip() + return sentence + + def align_char_timestamps(self, original_sentence, recognized_text, word_timestamps, text_length=8): + """ + 将词语级时间戳对齐到字符级时间戳 + + 参数: + original_sentence (str): 带标点的原始句子 + recognized_text (str): 空格分隔的无标点识别文本 + word_timestamps (list): 词语级时间戳列表 [[start1, end1], [start2, end2], ...] + + 返回: + list: 原始字符级时间戳列表 [[char_start, char_end], ...] + """ + if not original_sentence: + original_sentence = "".join(recognized_text.split()) + + words = jieba.lcut(original_sentence) + + # print(len(original_sentence), len(word_timestamps), len(recognized_text)) + recognized_words = recognized_text.split() + + # 4. 为每个字符分配时间戳 + char_timestamps = [] + + while True: + if len(words) < 10: + break + # 查询列表中的标点符号的位置 + punctuation_indices = self.find_punctuation_indices(words) + # print("标点符号位置:",punctuation_indices) + # 如果位置小于10,怎使用这个位置分割 + if punctuation_indices > 10 or punctuation_indices <= 2: + _words = words[:text_length] + words = words[text_length:] + else: + _words = words[:punctuation_indices] + words = words[punctuation_indices:] + + text = ''.join(_words) + # 计算词语持续时间 + best_index = -1 + best_score = 9999 + for index in range(text_length*4): + _recognized_words = recognized_words[:index] + _recognized_text = ''.join(_recognized_words) + if text == _recognized_text: + break + # 计算编辑距离 + distance = Levenshtein.distance(text, _recognized_text) + if distance < best_score: + best_index = index + best_score = distance + _recognized_words = recognized_words[:best_index] + recognized_words = recognized_words[best_index:] + + timestamp = word_timestamps[:best_index] + word_timestamps = word_timestamps[best_index:] + word_start = timestamp[0][0] + word_end = timestamp[-1][1] + + char_timestamps.append([word_start, word_end, self.remove_sentence_punctuation(text)]) + # 处理最后一段 + if len(word_timestamps) > 0: + text = ''.join(words) + timestamp = word_timestamps + word_start = timestamp[0][0] + word_end = timestamp[-1][1] + char_timestamps.append([word_start, word_end, self.remove_sentence_punctuation(text)]) + return char_timestamps + + def format_subtitle(self, char_timestamps): + """ + [230, 2210, '一个能够从音频中生成面部'] + [230, 5430, '运动系数的工具'] + 转为字幕文件 + """ + srt_content = "" + counter = 1 + for item in char_timestamps: + start_time = self.ms_to_srt_time(item[0]) + end_time = self.ms_to_srt_time(item[1]) + text = item[2] + srt_content += f"{counter}\n{start_time} --> {end_time}\n{text}\n\n" + # print(srt_content) + counter += 1 + return srt_content + + def pipeline(self, output=None): + # print(rec_result) + recognized_text = self.asr_result['text'] + word_timestamps = self.asr_result['timestamp'] + original_sentence = self.ori_text + char_timestamps = self.align_char_timestamps(original_sentence, recognized_text, word_timestamps) + subtitle_text = self.format_subtitle(char_timestamps) + if output is not None: + with open(output, 'w', encoding='utf-8') as f: + f.write(subtitle_text) + return subtitle_text \ No newline at end of file diff --git a/nodes.py b/nodes.py new file mode 100644 index 0000000..66244d6 --- /dev/null +++ b/nodes.py @@ -0,0 +1,220 @@ +import folder_paths +import os +import comfy.model_management as mm +import time +import torchaudio +import torchvision.utils as vutils +import torch +import json + +from modelscope.pipelines import pipeline +from modelscope.utils.constant import Tasks +from funasr import AutoModel +from .format import Format2Subtitle + + + +name_maps_ms = { + "paraformer": "iic/speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-pytorch", + "paraformer-zh": "iic/speech_seaco_paraformer_large_asr_nat-zh-cn-16k-common-vocab8404-pytorch", + "paraformer-en": "iic/speech_paraformer-large-vad-punc_asr_nat-en-16k-common-vocab10020", + "paraformer-en-spk": "iic/speech_paraformer-large-vad-punc_asr_nat-en-16k-common-vocab10020", + "paraformer-zh-streaming": "iic/speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-online", + "fsmn-vad": "iic/speech_fsmn_vad_zh-cn-16k-common-pytorch", + "ct-punc": "iic/punc_ct-transformer_cn-en-common-vocab471067-large", + "ct-punc-c": "iic/punc_ct-transformer_zh-cn-common-vocab272727-pytorch", + "fa-zh": "iic/speech_timestamp_prediction-v1-16k-offline", + "cam++": "iic/speech_campplus_sv_zh-cn_16k-common", + "Whisper-large-v2": "iic/speech_whisper-large_asr_multilingual", + "Whisper-large-v3": "iic/Whisper-large-v3", + "Qwen-Audio": "Qwen/Qwen-Audio", + "emotion2vec_plus_large": "iic/emotion2vec_plus_large", + "emotion2vec_plus_base": "iic/emotion2vec_plus_base", + "emotion2vec_plus_seed": "iic/emotion2vec_plus_seed", + "Whisper-large-v3-turbo": "iic/Whisper-large-v3-turbo", +} + +class AVSpeechTimestamp: + infer_ins_cache = None + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "audio": ("AUDIO",), + "text": ("STRING",), + "unload_model": ("BOOLEAN", {"default": False}), + }, + } + + RETURN_TYPES = ("STRING", "STRING", "ASRRESULT") + RETURN_NAMES = ("text", "json_result", "asr_result") + FUNCTION = "infer" + CATEGORY = "Aven/AV-FunASR" + DESCRIPTION = "get speech timestamp" + + def infer(self, audio, text, unload_model): + temp_dir = folder_paths.get_temp_directory() + os.makedirs(temp_dir, exist_ok=True) + + if AVSpeechTimestamp.infer_ins_cache is None: + model_root = os.path.join(folder_paths.models_dir, "ASR/FunASR") + model_dir = os.path.join(model_root, name_maps_ms["fa-zh"]) + + os.makedirs(model_dir, exist_ok=True) + AVSpeechTimestamp.infer_ins_cache = pipeline( + task=Tasks.speech_timestamp, + model=model_dir, + model_revision="v2.0.4", + output_dir=temp_dir + ) + # save audio + audio_save_path = os.path.join(temp_dir, f"{int(time.time())}.wav") + torchaudio.save(audio_save_path, audio['waveform'].squeeze(0), audio["sample_rate"]) + + rec_result = AVSpeechTimestamp.infer_ins_cache(input=(audio_save_path,text), data_type=("sound", "text")) + if rec_result: + rec_result = rec_result[0] + + # infer + if unload_model: + import gc + if AVSpeechTimestamp.infer_ins_cache is not None: + AVSpeechTimestamp.infer_ins_cache = None + gc.collect() + torch.cuda.empty_cache() + print("AVSpeechTimestamp memory cleanup successful") + # jr = json.dumps(rec_result, indent=4) + text = rec_result.get("text") + jr = json.dumps(rec_result, ensure_ascii=False) + print(text, jr) + return (text, jr, rec_result) + + +class AVASRTimestamp: + infer_ins_cache = None + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "audio": ("AUDIO",), + "batch_size_s": ("INT", {"default": 300, "min": 30, "max": 300, "step": 1}), + "unload_model": ("BOOLEAN", {"default": False}), + }, + } + + RETURN_TYPES = ("STRING", "STRING", "ASRRESULT") + RETURN_NAMES = ("text", "json_result", "asr_result") + FUNCTION = "infer" + CATEGORY = "Aven/AV-FunASR" + DESCRIPTION = "get speech timestamp" + + def infer(self, audio, batch_size_s, unload_model): + temp_dir = folder_paths.get_temp_directory() + os.makedirs(temp_dir, exist_ok=True) + + if AVASRTimestamp.infer_ins_cache is None: + model_root = os.path.join(folder_paths.models_dir, "ASR/FunASR") + model_dir = os.path.join(model_root, name_maps_ms["paraformer-zh"]) + vad_model = os.path.join(model_root, name_maps_ms["fsmn-vad"]) + os.makedirs(model_dir, exist_ok=True) + device = "cuda" if torch.cuda.is_available() else "cpu" + AVASRTimestamp.infer_ins_cache = AutoModel( + model=model_dir, + vad_model=vad_model, + punc_model=None, #"ct-punc", + device=device # GPU加速 + ) + # save audio + audio_save_path = os.path.join(temp_dir, f"{int(time.time())}.wav") + torchaudio.save(audio_save_path, audio['waveform'].squeeze(0), audio["sample_rate"]) + + rec_result = AVASRTimestamp.infer_ins_cache.generate(input=audio_save_path, batch_size_s=batch_size_s) + if rec_result: + rec_result = rec_result[0] + + # infer + if unload_model: + import gc + if AVSpeechTimestamp.infer_ins_cache is not None: + AVSpeechTimestamp.infer_ins_cache = None + gc.collect() + torch.cuda.empty_cache() + print("AVSpeechTimestamp memory cleanup successful") + # jr = json.dumps(rec_result, indent=4) + text = rec_result.get("text") + jr = json.dumps(rec_result, ensure_ascii=False) + print((text, jr, rec_result)) + return (text, jr, rec_result) + + +class AVFormat2Subtitle: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "asr_result": ("ASRRESULT",), + "text": ("STRING", {"default": None}), + }, + } + + RETURN_TYPES = ("STRING", ) + RETURN_NAMES = ("subtitle", ) + FUNCTION = "format_subtitle" + CATEGORY = "Aven/AV-FunASR" + DESCRIPTION = "format asr result to subtitle" + + def format_subtitle(self, asr_result, text=None): + f = Format2Subtitle(asr_result, ori_text=text) + content = f.pipeline() + + return (content, ) + + +class AVSaveSubtitles: + def __init__(self): + self.output_dir = folder_paths.get_output_directory() + self.type = "output" + self.prefix_append = "" + self.compress_level = 4 + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "subtitles": ("STRING", {"tooltip": "The subtitles to save."}), + "filename_prefix": ("STRING", {"default": "subtitles", "tooltip": "The prefix for the file to save. "}) + }, + } + + RETURN_TYPES = () + FUNCTION = "save_subtitles" + + OUTPUT_NODE = True + + CATEGORY = "Aven/AV-FunASR" + DESCRIPTION = "Saves the subtitles to a file." + + def save_subtitles(self, subtitles, filename_prefix="subtitles"): + filename = f"{filename_prefix}-{time.time()}.srt" + save_path = os.path.join(self.output_dir, filename) + with open(save_path, "w") as f: + f.write(subtitles) + + return { "ui": { "text": save_path } } + + + + +NODE_CLASS_MAPPINGS = { + "AVSpeechTimestamp": AVSpeechTimestamp, + "AVASRTimestamp": AVASRTimestamp, + "AVFormat2Subtitle": AVFormat2Subtitle, + "AVSaveSubtitles": AVSaveSubtitles, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "AVSpeechTimestamp": "AV Speech Timestamp", + "AVASRTimestamp": "AV ASR Timestamp", + "AVFormat2Subtitle": "AV Format to Subtitle", + "AVSaveSubtitles": "AV Save Subtitles", +} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3b168e1 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,15 @@ +[project] +name = "ComfyUI-AV-FunASR" +description = "FunASR wrapper for ComfyUI" +version = "1.0.0" +license = {file = "LICENSE"} +dependencies = ["modelscope", "sortedcontainers", "simplejson", "datasets==2.16.0", "Levenshtein", "soundfile"] + +[project.urls] +Repository = "https://github.com/avenstack/ComfyUI-AV-FunASR" +# Used by Comfy Registry https://comfyregistry.org + +[tool.comfy] +PublisherId = "av" +DisplayName = "ComfyUI-AV-FunASR" +Icon = "" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..afdee3f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +sortedcontainers +simplejson +datasets==2.16.0 +Levenshtein +modelscope \ No newline at end of file