@naersen/tts-rich-text
v0.8.3
Published
Reusable Vue 3 rich-text editor for TTS workflows
Maintainers
Readme
@naersen/tts-rich-text
面向 Vue 3 项目的 TTS 富文本编辑器。包内维护统一的 Tiptap 文档协议、TTS 标签、分句和数据转换;各业务项目通过 features、services 和 slots 注入差异,不再复制组件源码。
环境要求
- Vue 3.4+
- Node.js 18+
- pnpm 9+
本地开发
pnpm install
pnpm dev开发预览默认运行在 Vite 输出的本地地址。提交前执行:
pnpm build
pnpm test:e2ebuild 会依次完成类型检查、单元测试和组件库构建。test:e2e 使用本机 Chrome 验证拼音标注、停顿插入、原主题样式和窄屏工具栏收纳。
安装
通过 npm 在业务项目中安装:
pnpm add @naersen/tts-rich-textVue、Tiptap 和 Element Plus 是 peer dependencies。业务项目没有安装时,需要同时添加 peer packages,并在应用入口加载 Element Plus 样式。
引入全局样式
使用默认的 TtsRichTextEditor 时,在业务项目的 src/main.ts 中全局引入 Element Plus 和组件库样式:
import { createApp } from 'vue'
import 'element-plus/dist/index.css'
import '@naersen/tts-rich-text/styles.css'
import App from './App.vue'
createApp(App).mount('#app')如果项目已有 main.ts 初始化逻辑,只需要把下面两行加入现有入口,不需要重复创建 Vue 应用:
import 'element-plus/dist/index.css'
import '@naersen/tts-rich-text/styles.css'必须先引入 Element Plus,再引入组件库样式,因为 styles.css 包含组件基础样式、配套图标和对 Element Plus 的主题覆盖。请保持全局引入,不要放在某个页面组件的 scoped 样式中,否则弹窗、下拉菜单等挂载到页面其他位置的内容可能无法获得完整样式。
基础使用
<template>
<tts-rich-text-editor
v-model="documentValue"
:voice-version="voiceVersion"
:features="features"
:services="services"
:get-preview-params="getPreviewParams"
:get-playback-speed="getPlaybackSpeed"
/>
</template>
<script setup lang="ts">
import type { JSONContent } from '@tiptap/core'
import { ref } from 'vue'
import type {
TtsEditorFeatures,
TtsRichTextServices
} from '@naersen/tts-rich-text'
import {
TtsRichTextEditor,
getInitContent
} from '@naersen/tts-rich-text'
const documentValue = ref<JSONContent>(getInitContent())
const voiceVersion = ref(2)
const features: TtsEditorFeatures = {
tools: ['stop', 'customStop', 'pinyin', 'replace', 'language', 'ignore'],
enablePreview: true
}
const services: TtsRichTextServices = {
parseDocument: async (file, signal) => {
return api.parseDocument(file, { signal })
},
readFormula: async (formula, { signal, onChunk }) => {
return api.readFormula(formula, { signal, onChunk })
},
synthesizePreview: async (request, signal) => {
return api.synthesizePreview(request, { signal })
},
notify: (type, message) => messageService[type](message)
}
const getPreviewParams = () => ({
timberId: selectedVoice.value.id,
speed: selectedVoice.value.speed
})
// 仅用于预估时长,固定以 1 表示 1 倍速。
const getPlaybackSpeed = () => selectedVoice.value.speed / 50
</script>上例只负责使用编辑器组件,样式应按前面的说明在 main.ts 中统一引入,不需要在每个业务组件中重复加载。
上例中的 api、messageService 和 selectedVoice 由业务项目提供,组件包不会读取业务项目的环境变量、请求实例或状态管理。
差异扩展
功能配置
features 可以覆盖工具列表、语言列表、拼音最大选区长度、数字最大长度和试听开关。包内提供 V1/V2 默认能力配置,项目配置拥有最高优先级。V2 默认提供全部工具;V1 也支持 ignore,但默认不提供 stop 和 continue。
拼音标注统一输出数字声调格式(如 chong2 qing4),不再根据 voiceVersion 转换为带声调符号的拼音。ü 会规范化为 v,轻声统一使用 5 表示。业务接口应按该格式处理拼音标签的 payload.text。
ignore 对应“忽略读音”工具,只能标记纯文本选区。convertToTtsFormat() 会保留句子原文,并为忽略区域输出替换指令:
{
action: 'replace',
model: 'replace',
payload: ' ',
position: 0,
text: '需要忽略的文本',
actionCode: 'ignore'
}合成端应按 position 和 text 将该区域替换为空格,从而保留编辑器可见文本但不合成读音。
分句策略
convertToTtsFormat() 使用 splitTextToSentences() 生成 sentences。默认按视觉长度计算,中文、全角字符计 2,其他字符计 1;每段最小目标长度为 32,最大目标长度为 64。
超长内容优先在分号处分割,其次是逗号或冒号、空格,顿号仅在没有其他自然切点时使用。数字中的小数点和千分位逗号不会被当作分句点,过短尾段会自动重平衡。
需要自定义长度时,可以从核心入口调用:
import { splitTextToSentences } from '@naersen/tts-rich-text/core'
const sentences = splitTextToSentences(text, {
minSegmentLength: 32,
maxSegmentLength: 64,
rebalanceRatio: 0.4
})服务适配
以下能力只有传入对应 service 时才可用;未提供时保留原工具位置并显示为禁用态:
parseDocument:上传并解析 TXT/DOCX。generateDraft:AI 写稿,支持通过onChunk增量输出。readFormula:公式转读法,支持通过onChunk增量输出。synthesizePreview:选区试听,返回音频 URL 或 Blob。synthesizePreviewStream:保持原编辑器的流式分片试听,逐条调用onMessage传入后端音频消息。confirmVoiceChange:接管音色版本冲突确认框;未提供时使用 Element Plus MessageBox。notify:接入项目自己的消息组件。
所有耗时服务都会收到 AbortSignal,项目请求层必须将取消信号继续传给实际请求。
Slots
toolbar-beforetoolbar-aftereditor-after,参数为{ editor }footer-before,参数为{ editor, textLength }footer-after,参数为{ editor, textLength }
项目专属工具可以通过 slots 放在默认布局中;页面结构差异较大时,使用下方组合式布局。
组合式布局
TtsEditorRoot 维护编辑器实例、初始内容、服务和上下文,不输出固定布局,并默认挂载选区试听菜单。它的后代可以按项目需要组合 TtsEditorContent、工具栏和底部栏:
<template>
<tts-editor-root
ref="editorRef"
:initial-content="documentValue"
:services="services"
@change="handleEditorChange"
>
<section class="project-editor">
<header class="project-editor-header">
<project-title />
<tts-editor-toolbar />
<button type="button" @click="editorRef?.clear()">
清空
</button>
</header>
<main class="project-editor-content">
<tts-editor-content />
<project-side-panel :editor="editorRef?.editor" />
</main>
<footer class="project-editor-footer">
<span>{{ editorRef?.textLength ?? 0 }} 字</span>
<tts-editor-footer />
</footer>
</section>
</tts-editor-root>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type {
TtsEditorRootExpose,
TtsRichTextServices
} from '@naersen/tts-rich-text'
import {
TtsEditorContent,
TtsEditorFooter,
TtsEditorRoot,
TtsEditorToolbar,
getInitContent
} from '@naersen/tts-rich-text'
import '@naersen/tts-rich-text/base.css'
const editorRef = ref<TtsEditorRootExpose>()
const documentValue = ref(getInitContent())
const services: TtsRichTextServices = {}
const handleEditorChange = () => {
const value = editorRef.value?.getJson()
if (value) documentValue.value = value
}
</script>initialContent 只在编辑器初始化时读取,后续变化不会反向覆盖编辑器。需要主动替换内容时,调用 editorRef.value?.setContent(content)。Root 默认插槽只负责组合布局,不提供作用域参数。Root 内的后代组件通过 useEditorContext() 访问编辑器上下文;外层业务组件通过 TtsEditorRootExpose 和组件 ref 访问编辑器状态与命令。
Root 会自动挂载 TtsSelectionMenu,不用在默认插槽中重复引入。项目需要关闭选区试听或替换为自定义菜单时,传入 :show-selection-menu="false";TtsSelectionMenu 仍保留独立导出,供高级组合场景使用。
组合模式只需要引入 base.css。需要复用原项目的 Element Plus 全局主题时,再在它后面引入:
import 'element-plus/dist/index.css'
import '@naersen/tts-rich-text/base.css'
import '@naersen/tts-rich-text/theme-default.css'styles.css 等价于依次引入 base.css 和 theme-default.css,继续用于默认成品组件。
原子组件
对编辑器外壳差异较大的项目,可以直接组合包内原始组件:
TtsEditorToolbarTtsEditorRootTtsEditorContentTtsEditorFooterTtsSelectionMenuTtsMarkPinyinModalTtsTagNodeViewTtsAiRewriteModalTtsFormulaModalTtsReplaceTextModalTtsUploadModal
这些组件共享 TtsEditorRoot 提供的编辑器上下文,不能脱离 Root 单独使用。布局接近原项目时使用完整的 TtsRichTextEditor;页面结构差异较大时再组合原子组件,业务接口仍统一通过 services 注入。
组合式 API
自定义工具栏、试听面板或业务弹窗时,可以从包根入口引入:
import {
useGlobalAudioControl,
useSSETool,
useSynthesizeAudioTool,
useTestAudioTool,
useVoiceChangeConfirm
} from '@naersen/tts-rich-text'useSynthesizeAudioTool 和 useTestAudioTool 会读取编辑器 services,需要在 TtsEditorRoot 的后代组件中调用。useGlobalAudioControl 和 useSSETool 可以独立使用。
对外方法
通过组件 ref 可以调用:
clear()focus()getJson()getTtsData()replaceText(text)handleChangeVoice(currentModel, nextModel):返回Promise<boolean>;存在版本冲突时先请求确认,确认后清空冲突内容,取消时返回false。
核心转换函数也可以独立导入:
import { convertToTtsFormat } from '@naersen/tts-rich-text/core'预估语音时长
estimateTtsDuration() 可独立计算文本的预估语音时长,返回值单位为秒。计算时会去除所有空白字符;汉字按 0.28 秒/字、其余字符按 0.14 秒/字符计算,再除以播放倍速。
import { estimateTtsDuration } from '@naersen/tts-rich-text'
const duration = estimateTtsDuration('你好 hello', 1.5)第二个参数为播放倍速,默认为 1;传入 0、负数或无效值时也会按 1 倍速处理。
版本管理
各版本的功能变化和升级注意事项见 CHANGELOG.md。
遵循 SemVer:
- 修复兼容问题:patch,例如
0.1.1。 - 增加向后兼容能力:minor,例如
0.2.0。 - 修改文档协议、TTS 输出或删除公开 API:major。
项目应依赖明确版本,通过升级依赖完成同步,不要把 src 目录复制回业务项目。
