npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@naersen/tts-rich-text

v0.8.3

Published

Reusable Vue 3 rich-text editor for TTS workflows

Readme

@naersen/tts-rich-text

面向 Vue 3 项目的 TTS 富文本编辑器。包内维护统一的 Tiptap 文档协议、TTS 标签、分句和数据转换;各业务项目通过 featuresservices 和 slots 注入差异,不再复制组件源码。

环境要求

  • Vue 3.4+
  • Node.js 18+
  • pnpm 9+

本地开发

pnpm install
pnpm dev

开发预览默认运行在 Vite 输出的本地地址。提交前执行:

pnpm build
pnpm test:e2e

build 会依次完成类型检查、单元测试和组件库构建。test:e2e 使用本机 Chrome 验证拼音标注、停顿插入、原主题样式和窄屏工具栏收纳。

安装

通过 npm 在业务项目中安装:

pnpm add @naersen/tts-rich-text

Vue、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 中统一引入,不需要在每个业务组件中重复加载。

上例中的 apimessageServiceselectedVoice 由业务项目提供,组件包不会读取业务项目的环境变量、请求实例或状态管理。

差异扩展

功能配置

features 可以覆盖工具列表、语言列表、拼音最大选区长度、数字最大长度和试听开关。包内提供 V1/V2 默认能力配置,项目配置拥有最高优先级。V2 默认提供全部工具;V1 也支持 ignore,但默认不提供 stopcontinue

拼音标注统一输出数字声调格式(如 chong2 qing4),不再根据 voiceVersion 转换为带声调符号的拼音。ü 会规范化为 v,轻声统一使用 5 表示。业务接口应按该格式处理拼音标签的 payload.text

ignore 对应“忽略读音”工具,只能标记纯文本选区。convertToTtsFormat() 会保留句子原文,并为忽略区域输出替换指令:

{
	action: 'replace',
	model: 'replace',
	payload: ' ',
	position: 0,
	text: '需要忽略的文本',
	actionCode: 'ignore'
}

合成端应按 positiontext 将该区域替换为空格,从而保留编辑器可见文本但不合成读音。

分句策略

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-before
  • toolbar-after
  • editor-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.csstheme-default.css,继续用于默认成品组件。

原子组件

对编辑器外壳差异较大的项目,可以直接组合包内原始组件:

  • TtsEditorToolbar
  • TtsEditorRoot
  • TtsEditorContent
  • TtsEditorFooter
  • TtsSelectionMenu
  • TtsMarkPinyinModal
  • TtsTagNodeView
  • TtsAiRewriteModal
  • TtsFormulaModal
  • TtsReplaceTextModal
  • TtsUploadModal

这些组件共享 TtsEditorRoot 提供的编辑器上下文,不能脱离 Root 单独使用。布局接近原项目时使用完整的 TtsRichTextEditor;页面结构差异较大时再组合原子组件,业务接口仍统一通过 services 注入。

组合式 API

自定义工具栏、试听面板或业务弹窗时,可以从包根入口引入:

import {
	useGlobalAudioControl,
	useSSETool,
	useSynthesizeAudioTool,
	useTestAudioTool,
	useVoiceChangeConfirm
} from '@naersen/tts-rich-text'

useSynthesizeAudioTooluseTestAudioTool 会读取编辑器 services,需要在 TtsEditorRoot 的后代组件中调用。useGlobalAudioControluseSSETool 可以独立使用。

对外方法

通过组件 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 目录复制回业务项目。