feat: add AIIA Text Splitter node (v1.13.0)

New node that splits single-speaker text into dialogue_json format:
- 3 split modes: auto, by_sentence, by_line
- Short sentence merging (min_chars)
- Long sentence splitting at commas/semicolons (max_chars)
- Output compatible with Emotion Annotator and Dialogue TTS

Workflow: Text → Text Splitter → Emotion Annotator → TTS
This commit is contained in:
Hawk Lee
2026-02-16 11:03:31 +08:00
parent 449183c800
commit 40bc5627cb
3 changed files with 276 additions and 10 deletions
+41 -10
View File
@@ -1179,11 +1179,16 @@ Script Parser → Emotion Annotator → Dialogue TTS / Qwen Dialogue TTS
##### 典型工作流
**基础流程**(适合大多数场景):
**对话流程**(适合大多数场景):
```
Script Parser → Emotion Annotator → Dialogue TTS → Video Combine
```
**单人旁白流程**(配合 Text Splitter):
```
长文本 → Text Splitter → Emotion Annotator → Qwen3 TTS / CosyVoice
```
**高级拆分流程**(多引擎混合):
```
Script Parser → Emotion Annotator → Podcast Splitter → TTS_A (CosyVoice)
@@ -1191,7 +1196,33 @@ Script Parser → Emotion Annotator → Podcast Splitter → TTS_A (CosyVoice)
→ Podcast Stitcher
```
#### 4.3 AIIA Dialogue TTS (对话生成引擎)
#### 4.3 AIIA Text Splitter (文本拆分器)
**[v1.13.0 New]** 将单人长文本按标点拆分为标准 `dialogue_json`,使其可直接接入 Emotion Annotator → TTS 管线。
##### 参数说明
| 参数 | 说明 |
|------|------|
| `text` | 待拆分文本(多行/长段落) |
| `speaker_name` | 说话人名称,默认 `Narrator` |
| `split_mode` | `auto`:智能拆分(句末标点 + 短句合并 + 长句再拆);`by_sentence`:仅按句号/问号/感叹号拆分;`by_line`:按换行拆分 |
| `min_chars` | 最小字符数,短于此的句子合并到前一句。默认 `4` |
| `max_chars` | 最大字符数,超长句子在逗号/分号处强制拆分。默认 `100` |
##### 拆分行为 (`auto` 模式)
1. **按换行分段** → 保留段落结构
2. **段内按句末标点拆分** → `。!?!?…` 和省略号 `……` / `...`
3. **短句合并** → 短于 `min_chars` 的句子追加到前一句
4. **长句拆分** → 超过 `max_chars` 的句子在逗号/分号处再切
##### 输出
- `dialogue_json`:标准格式,与 Script Parser 输出兼容,可直接接入 Emotion Annotator / Dialogue TTS
- `sentence_count`:拆分后的句子数
#### 4.4 AIIA Dialogue TTS (对话生成引擎)
核心调度与生成节点,支持自动角色切换和长音频拼接。
@@ -1214,7 +1245,7 @@ Script Parser → Emotion Annotator → Podcast Splitter → TTS_A (CosyVoice)
- **Emotion Safeguard (New!)**:
- **智能检测**: 系统会自动嗅探加载模型的元数据。如果你使用 CosyVoice SFT/Base 或 VibeVoice 等不支持 `Instruct` 功能的模型,系统将自动跳过 `[Emotion]` 标签插入,防止模型读出方括号。
#### 4.4 AIIA Qwen Dialogue TTS (Qwen 旗舰对话节点)
#### 4.5 AIIA Qwen Dialogue TTS (Qwen 旗舰对话节点)
**[v1.11.0 New]** 深度集成 Qwen3-TTS 的多模式特性,支持复杂的混合角色场景。
@@ -1235,7 +1266,7 @@ Script Parser → Emotion Annotator → Podcast Splitter → TTS_A (CosyVoice)
- **Ref Audio**: 当模式为 Clone 时,连接参考音频。
- **特点**: 相对于通用对话节点,此节点能根据每个人的模式自动路由到最合适的 Qwen 引擎,且支持在 UI 直接输入设计描述。
#### 4.5 AIIA Subtitle Gen (字幕生成器)
#### 4.6 AIIA Subtitle Gen (字幕生成器)
**[v1.7.0 New]** 无需 STT,直接从生成过程中提取精准时间轴。
@@ -1250,7 +1281,7 @@ Script Parser → Emotion Annotator → Podcast Splitter → TTS_A (CosyVoice)
- **VibeVoice**: 使用**智能插值算法 (Smart Interpolation)**,根据字符长度自动计算长音频段内的单句时间轴。
- **Qwen3-TTS**: 基于生成的音频振幅精准断句,支持多角色时间轴导出。
#### 4.6 AIIA Subtitle to Segments (字幕转分段)
#### 4.7 AIIA Subtitle to Segments (字幕转分段)
**[v1.10.3 New]** 将现有的 SRT/ASS 字幕文件转换为 `segments_info` 格式,以便进行时间轴重新校准。
@@ -1261,7 +1292,7 @@ Script Parser → Emotion Annotator → Podcast Splitter → TTS_A (CosyVoice)
- `segments_info`: 标准化的 JSON 字符串,可直接输入到 `AIIA Subtitle Gen`。
- **用途**: 结合 `Subtitle Gen` 的 `calibration_info` 输入,可以将**旧的、不准的字幕**自动对齐到**新的、精准的音轨**上。
#### 4.7 AIIA Subtitle Preview (字幕预览)
#### 4.8 AIIA Subtitle Preview (字幕预览)
**[v1.7.1 New]** 实时校验音画同步效果。
@@ -1272,7 +1303,7 @@ Script Parser → Emotion Annotator → Podcast Splitter → TTS_A (CosyVoice)
- **交互式界面**: 提供 Web 播放器,按时间轴滚动显示字幕。
- **ASS 样式渲染**: 尝试还原 ASS 字幕的字体颜色、大小和描边效果。
#### 4.8 Interactive Teaching (Web Export) (互动式教学导出)
#### 4.9 Interactive Teaching (Web Export) (互动式教学导出)
**[v1.8.1 New]** 将播客升级为视听同步的互动网页。支持“读写分离”的缓存优化,修改 Visual 标签无需重跑 TTS。
@@ -1290,7 +1321,7 @@ Script Parser → Emotion Annotator → Podcast Splitter → TTS_A (CosyVoice)
- 支持绝对 URL: `(Visual: https://example.com)`
- 支持相对路径: `(Visual: ./slides/01.jpg)` (相对于导出 HTML 的位置)
#### 4.9 AIIA Podcast Splitter (对话拆分器)
#### 4.10 AIIA Podcast Splitter (对话拆分器)
**[v1.12.0 New]** 将对话 JSON 按说话人拆分为独立文本列表,用于"**拆分→生成→拼接**"的高级流程。
@@ -1301,7 +1332,7 @@ Script Parser → Emotion Annotator → Podcast Splitter → TTS_A (CosyVoice)
- `text_A`, `text_B`: 分别为说话人 A、B 的纯文本列表(每行一句),可直接送入各自的 TTS 节点独立生成。
- **用途**: 实现对不同说话人使用不同 TTS 引擎/参数生成音频,再通过 Stitcher 精确拼接的高级工作流。
#### 4.10 AIIA ASR Node (语音识别)
#### 4.11 AIIA ASR Node (语音识别)
**[v1.12.0 New]** 基于 FunASR 的语音识别节点,输出带词级时间戳的识别结果,为 Stitcher 提供精确对齐依据。
@@ -1309,7 +1340,7 @@ Script Parser → Emotion Annotator → Podcast Splitter → TTS_A (CosyVoice)
- **Output**: `ASR_RESULT`(包含词级时间戳的识别结果)。
- **依赖**: 需要安装 `funasr` 库。模型首次运行时自动下载。
#### 4.11 AIIA Podcast Stitcher (精确拼接器)
#### 4.12 AIIA Podcast Stitcher (精确拼接器)
**[v1.12.0 New]** 将分轨生成的多角色音频按原始对话顺序精确拼接,还原自然对话节奏。
+3
View File
@@ -151,6 +151,9 @@ else:
# 30. 处理 aiia_emotion_annotator.py (LLM 情感标注)
_load_nodes_from_module(".aiia_emotion_annotator", "aiia_emotion_annotator")
# 31. 处理 aiia_text_splitter.py (单人文本拆分)
_load_nodes_from_module(".aiia_text_splitter", "aiia_text_splitter")
# 告诉 ComfyUI 这个节点包有一个包含网页资源的 'js' 目录
WEB_DIRECTORY = "js"
+232
View File
@@ -0,0 +1,232 @@
"""
AIIA Text Splitter — 单人文本按标点拆分为 dialogue_json
[v1.13.0 New]
将长文本按句号、问号、感叹号等标点拆分为标准 dialogue_json 格式,
可直接接入 Emotion Annotator → TTS 管线。
支持短句合并(避免碎片)和长句拆分(避免 TTS 单句过长)。
"""
import json
import re
class AIIA_Text_Splitter:
"""
将纯文本按标点拆分为 dialogue_json 格式。
支持三种拆分模式:
- auto: 中英文自动,按句末标点拆分 + 短句合并 + 长句拆分
- by_sentence: 仅按句号/问号/感叹号拆分
- by_line: 按换行拆分
"""
NODE_NAME = "AIIA Text Splitter"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"text": ("STRING", {
"multiline": True,
"default": "",
"tooltip": "待拆分的文本。支持多段落、多行。"
}),
"speaker_name": ("STRING", {
"default": "Narrator",
"tooltip": "说话人名称,写入 dialogue_json 的 speaker 字段"
}),
"split_mode": (["auto", "by_sentence", "by_line"], {
"default": "auto",
"tooltip": "拆分模式:\n"
" auto: 按句末标点拆分 + 短句合并 + 长句再拆\n"
" by_sentence: 仅按句号/问号/感叹号拆分\n"
" by_line: 按换行拆分"
}),
},
"optional": {
"min_chars": ("INT", {
"default": 4,
"min": 1,
"max": 50,
"tooltip": "最小字符数。短于此的句子合并到前一句。"
}),
"max_chars": ("INT", {
"default": 100,
"min": 20,
"max": 500,
"tooltip": "最大字符数。超长句子在逗号/分号处强制拆分。"
}),
}
}
RETURN_TYPES = ("STRING", "INT")
RETURN_NAMES = ("dialogue_json", "sentence_count")
FUNCTION = "split_text"
CATEGORY = "AIIA/Podcast"
def split_text(self, text, speaker_name="Narrator", split_mode="auto",
min_chars=4, max_chars=100):
"""拆分文本为 dialogue_json 格式。"""
if not text or not text.strip():
empty = json.dumps([], ensure_ascii=False)
return (empty, 0)
text = text.strip()
if split_mode == "by_line":
raw_sentences = self._split_by_line(text)
elif split_mode == "by_sentence":
raw_sentences = self._split_by_sentence(text)
else: # auto
raw_sentences = self._split_auto(text, min_chars, max_chars)
# 构建 dialogue_json
dialogue = []
for sent in raw_sentences:
sent = sent.strip()
if not sent:
continue
dialogue.append({
"type": "speech",
"speaker": speaker_name,
"text": sent,
"emotion": None
})
result = json.dumps(dialogue, ensure_ascii=False, indent=2)
return (result, len(dialogue))
def _split_by_line(self, text):
"""按换行拆分,空行跳过。"""
return [line.strip() for line in text.split("\n") if line.strip()]
def _split_by_sentence(self, text):
"""仅按句末标点拆分(句号、问号、感叹号、省略号)。"""
# 先按换行分段,再段内按标点拆分
paragraphs = [p.strip() for p in text.split("\n") if p.strip()]
sentences = []
for para in paragraphs:
parts = self._split_at_sentence_end(para)
sentences.extend(parts)
return sentences
def _split_auto(self, text, min_chars, max_chars):
"""
智能拆分:
1. 按换行分段
2. 段内按句末标点拆分
3. 短句合并
4. 长句在逗号/分号处再拆
"""
paragraphs = [p.strip() for p in text.split("\n") if p.strip()]
all_sentences = []
for para in paragraphs:
# Step 1: 按句末标点拆分
raw = self._split_at_sentence_end(para)
# Step 2: 短句合并
merged = self._merge_short(raw, min_chars)
# Step 3: 长句拆分
final = []
for sent in merged:
if len(sent) > max_chars:
final.extend(self._split_long(sent, max_chars))
else:
final.append(sent)
all_sentences.extend(final)
return all_sentences
def _split_at_sentence_end(self, text):
"""
在句末标点处拆分,保留标点在前一句末尾。
支持:。!?!? 以及省略号 …… / ...
"""
# 按句末标点拆分,保留分隔符
# 匹配:句号/问号/感叹号(中英文),以及省略号
parts = re.split(r'((?:\.{3}|…{1,2}|[。!?!?]))', text)
sentences = []
buffer = ""
for i, part in enumerate(parts):
if i % 2 == 0:
# 正文部分
buffer += part
else:
# 标点部分,附加到 buffer
buffer += part
if buffer.strip():
sentences.append(buffer.strip())
buffer = ""
# 处理末尾没有标点的残余
if buffer.strip():
sentences.append(buffer.strip())
return sentences
def _merge_short(self, sentences, min_chars):
"""将短于 min_chars 的句子合并到前一句。"""
if not sentences:
return sentences
merged = [sentences[0]]
for sent in sentences[1:]:
if len(sent) < min_chars and merged:
# 合并到前一句
merged[-1] = merged[-1] + sent
else:
merged.append(sent)
return merged
def _split_long(self, text, max_chars):
"""
将超长句子在逗号/分号/顿号处拆分。
尽量靠近 max_chars 的位置切,避免太碎。
"""
# 可选拆分点:逗号、分号、顿号(中英文)
split_points = []
for i, ch in enumerate(text):
if ch in ',,;;、':
split_points.append(i)
if not split_points:
# 没有可拆分点,原样返回
return [text]
result = []
start = 0
for point in split_points:
segment = text[start:point + 1]
if len(segment) >= max_chars // 2:
# 够长了,切一刀
result.append(segment.strip())
start = point + 1
# 追加剩余部分
remainder = text[start:].strip()
if remainder:
if result and len(remainder) < max_chars // 4:
# 残余太短,合并到最后一段
result[-1] = result[-1] + remainder
else:
result.append(remainder)
return result if result else [text]
# === ComfyUI Registration ===
NODE_CLASS_MAPPINGS = {
"AIIA_Text_Splitter": AIIA_Text_Splitter,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"AIIA_Text_Splitter": "💬 AIIA Text Splitter",
}