This commit is contained in:
aven
2025-06-08 16:07:40 +08:00
commit 6840820f8d
11 changed files with 1023 additions and 0 deletions
+26
View File
@@ -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 }}
+3
View File
@@ -0,0 +1,3 @@
*.pyc
__pycache__/
.hisotry/
+63
View File
@@ -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)
+3
View File
@@ -0,0 +1,3 @@
from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
@@ -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
}
@@ -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
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

+136
View File
@@ -0,0 +1,136 @@
import jieba
import string
import json
import Levenshtein
import re
class Format2Subtitle:
def __init__(self, asr_result, ori_text):
self.asr_result = asr_result
self.ori_text = ori_text
def ms_to_srt_time(self, ms):
"""
将毫秒转换为SRT时间格式 (HH:MM:SS,mmm)
例如:150毫秒 -> 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
+220
View File
@@ -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",
}
+15
View File
@@ -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 = ""
+5
View File
@@ -0,0 +1,5 @@
sortedcontainers
simplejson
datasets==2.16.0
Levenshtein
modelscope