init
This commit is contained in:
@@ -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 }}
|
||||
@@ -0,0 +1,3 @@
|
||||
*.pyc
|
||||
__pycache__/
|
||||
.hisotry/
|
||||
@@ -0,0 +1,63 @@
|
||||
## ComfyUI FunAsr Nodes
|
||||
|
||||
使用FunAsr进行语音识别,并转为文本或字幕文件。
|
||||
|
||||

|
||||
|
||||
## 功能
|
||||
|
||||
- [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)
|
||||
@@ -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 |
@@ -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
|
||||
@@ -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",
|
||||
}
|
||||
@@ -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 = ""
|
||||
@@ -0,0 +1,5 @@
|
||||
sortedcontainers
|
||||
simplejson
|
||||
datasets==2.16.0
|
||||
Levenshtein
|
||||
modelscope
|
||||
Reference in New Issue
Block a user