@xdfnet/ispeak 1.6.5 → 1.6.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Docs/ARCHITECTURE.md +1 -1
- package/Docs/HOOK_TEXT_EXTRACTION.md +241 -0
- package/README.md +17 -14
- package/clean_text.go +153 -0
- package/configs/config.example.json +1 -1
- package/configs/hook-speak.sh +173 -96
- package/main.go +70 -379
- package/npm/postinstall.js +41 -2
- package/package.json +4 -2
- package/scripts/ispeak +1 -1
package/Docs/ARCHITECTURE.md
CHANGED
|
@@ -177,7 +177,7 @@ pending -> running -> delete
|
|
|
177
177
|
├── config.json # API Key、音色配置
|
|
178
178
|
├── ispeak.sock # Unix Socket
|
|
179
179
|
├── ispeak.log # 日志(lumberjack 轮转)
|
|
180
|
-
└── hook-speak.sh # Claude/Codex
|
|
180
|
+
└── hook-speak.sh # Claude/Codex Hook
|
|
181
181
|
|
|
182
182
|
~/Library/LaunchAgents/
|
|
183
183
|
└── com.iSpeak.plist # launchd 服务配置
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# Hook 文本提取链路
|
|
2
|
+
|
|
3
|
+
本文记录 Claude Code / Codex CLI 在 Hook 中拿到“最后一条 assistant 回复”的实际方式。`hook-speak.sh` 的目标只做两件事:取最后一条 assistant 回复,发给 iSpeak socket。
|
|
4
|
+
|
|
5
|
+
## 结论
|
|
6
|
+
|
|
7
|
+
推荐优先级:
|
|
8
|
+
|
|
9
|
+
1. **Codex `notify`**:从脚本第二个参数 `$2` 读取 JSON,取 `last-assistant-message`。
|
|
10
|
+
2. **Claude / Codex Stop Hook**:从 stdin 读取 JSON,优先取 `last_assistant_message`。
|
|
11
|
+
3. **明确 transcript**:如果没有直接字段,只读取 payload 里明确传入的 `transcript_path`。
|
|
12
|
+
|
|
13
|
+
不扫描 `~/.codex/sessions`。没有 direct 字段也没有 `transcript_path` 时,本次不播报。
|
|
14
|
+
|
|
15
|
+
## Codex CLI:notify
|
|
16
|
+
|
|
17
|
+
当前本机版本:
|
|
18
|
+
|
|
19
|
+
```text
|
|
20
|
+
codex-cli 0.130.0
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Codex CLI 的 `notify = [...]` 是 legacy notify 机制。官方源码里会把通知 JSON 追加成命令的最后一个 argv 参数,不写 stdin。
|
|
24
|
+
|
|
25
|
+
配置示例:
|
|
26
|
+
|
|
27
|
+
```toml
|
|
28
|
+
notify = ["/Users/你的用户名/.config/iSpeak/hook-speak.sh", "codex"]
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
脚本实际收到:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
$1 = "codex"
|
|
35
|
+
$2 = '{"type":"agent-turn-complete",...,"last-assistant-message":"..."}'
|
|
36
|
+
stdin = empty
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
核心字段:
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"type": "agent-turn-complete",
|
|
44
|
+
"thread-id": "...",
|
|
45
|
+
"turn-id": "...",
|
|
46
|
+
"cwd": "...",
|
|
47
|
+
"input-messages": ["..."],
|
|
48
|
+
"last-assistant-message": "最后一条 assistant 回复"
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
所以 Codex `notify` 的正确读取方式是:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
input="${2:-}"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
然后解析:
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
payload["last-assistant-message"]
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
源码依据:`codex-rs/hooks/src/legacy_notify.rs`。该文件把 `last_assistant_message` 序列化为 kebab-case 的 `last-assistant-message`,并在执行命令前 `command.arg(notify_payload)`。
|
|
65
|
+
|
|
66
|
+
## Codex CLI:Stop Hook
|
|
67
|
+
|
|
68
|
+
Codex 也支持 Claude 风格 Hook。Stop Hook 的输入 JSON 写入 stdin。
|
|
69
|
+
|
|
70
|
+
配置示例:
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"hooks": {
|
|
75
|
+
"Stop": [
|
|
76
|
+
{
|
|
77
|
+
"hooks": [
|
|
78
|
+
{
|
|
79
|
+
"type": "command",
|
|
80
|
+
"command": "bash $HOME/.config/iSpeak/hook-speak.sh codex",
|
|
81
|
+
"timeout": 30
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
脚本实际收到:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
$1 = "codex"
|
|
94
|
+
$2 = empty
|
|
95
|
+
stdin = '{"hook_event_name":"Stop",...,"last_assistant_message":"..."}'
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
核心字段:
|
|
99
|
+
|
|
100
|
+
```json
|
|
101
|
+
{
|
|
102
|
+
"session_id": "...",
|
|
103
|
+
"turn_id": "...",
|
|
104
|
+
"transcript_path": "...",
|
|
105
|
+
"cwd": "...",
|
|
106
|
+
"hook_event_name": "Stop",
|
|
107
|
+
"model": "...",
|
|
108
|
+
"permission_mode": "bypassPermissions",
|
|
109
|
+
"stop_hook_active": false,
|
|
110
|
+
"last_assistant_message": "最后一条 assistant 回复"
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
源码依据:
|
|
115
|
+
|
|
116
|
+
- `codex-rs/hooks/src/events/stop.rs`:构造 `StopCommandInput`,包含 `last_assistant_message` 和 `transcript_path`。
|
|
117
|
+
- `codex-rs/hooks/schema/generated/stop.command.input.schema.json`:Stop stdin schema。
|
|
118
|
+
- `codex-rs/hooks/src/engine/command_runner.rs`:Hook 命令通过 stdin 接收 `input_json`。
|
|
119
|
+
|
|
120
|
+
## Codex Transcript
|
|
121
|
+
|
|
122
|
+
Codex 的 transcript/session 文件是 JSONL。实际 assistant 回复形态:
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{
|
|
126
|
+
"type": "response_item",
|
|
127
|
+
"payload": {
|
|
128
|
+
"type": "message",
|
|
129
|
+
"role": "assistant",
|
|
130
|
+
"content": [
|
|
131
|
+
{
|
|
132
|
+
"type": "output_text",
|
|
133
|
+
"text": "最后一条 assistant 回复"
|
|
134
|
+
}
|
|
135
|
+
]
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
提取规则:
|
|
141
|
+
|
|
142
|
+
```js
|
|
143
|
+
event.type === "response_item" &&
|
|
144
|
+
event.payload?.type === "message" &&
|
|
145
|
+
event.payload?.role === "assistant"
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
然后拼接:
|
|
149
|
+
|
|
150
|
+
```js
|
|
151
|
+
event.payload.content[].text
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Claude Code:Stop Hook
|
|
155
|
+
|
|
156
|
+
Claude Code 官方 Stop Hook 通过 stdin 传 JSON,核心字段是:
|
|
157
|
+
|
|
158
|
+
```json
|
|
159
|
+
{
|
|
160
|
+
"session_id": "...",
|
|
161
|
+
"transcript_path": "...",
|
|
162
|
+
"hook_event_name": "Stop",
|
|
163
|
+
"stop_hook_active": false
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
有些版本或场景可能直接提供:
|
|
168
|
+
|
|
169
|
+
```json
|
|
170
|
+
{
|
|
171
|
+
"last_assistant_message": "最后一条 assistant 回复"
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
所以 Claude 的读取顺序是:
|
|
176
|
+
|
|
177
|
+
1. `last_assistant_message`
|
|
178
|
+
2. `message`
|
|
179
|
+
3. `transcript_path`
|
|
180
|
+
|
|
181
|
+
Claude transcript 常见 assistant 形态:
|
|
182
|
+
|
|
183
|
+
```json
|
|
184
|
+
{"role":"assistant","content":[{"type":"text","text":"..."}]}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
或:
|
|
188
|
+
|
|
189
|
+
```json
|
|
190
|
+
{"message":{"role":"assistant","content":[{"type":"text","text":"..."}]}}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## 当前脚本策略
|
|
194
|
+
|
|
195
|
+
`configs/hook-speak.sh` 当前入口:
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
input="${2:-}"
|
|
199
|
+
if [[ -z "$input" ]]; then
|
|
200
|
+
input=$(cat)
|
|
201
|
+
fi
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
含义:
|
|
205
|
+
|
|
206
|
+
- Codex `notify`:读 `$2`
|
|
207
|
+
- Claude / Codex Stop Hook:读 stdin
|
|
208
|
+
- 如果 Codex 的 `notify` 和 `Stop` 同时启用,脚本会按 `turn_id` 去重,避免同一回合播两次
|
|
209
|
+
|
|
210
|
+
Codex 文本字段优先级:
|
|
211
|
+
|
|
212
|
+
```js
|
|
213
|
+
payload["last-assistant-message"]
|
|
214
|
+
payload.last_assistant_message
|
|
215
|
+
payload.lastAssistantMessage
|
|
216
|
+
payload.message
|
|
217
|
+
payload.lastMessage
|
|
218
|
+
payload.transcript_path
|
|
219
|
+
payload.transcriptPath
|
|
220
|
+
payload["transcript-path"]
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Claude 文本字段优先级:
|
|
224
|
+
|
|
225
|
+
```js
|
|
226
|
+
payload.last_assistant_message
|
|
227
|
+
payload.message
|
|
228
|
+
payload.transcript_path
|
|
229
|
+
payload.transcriptPath
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## 为什么不能只读 stdin
|
|
233
|
+
|
|
234
|
+
因为 Codex `notify` 不走 stdin。只读 stdin 会导致:
|
|
235
|
+
|
|
236
|
+
```text
|
|
237
|
+
TEXT_LEN: 0
|
|
238
|
+
SPOKE: SKIP
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
正确做法是先读 `$2`,再读 stdin;不扫历史 session。
|
package/README.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# iSpeak
|
|
2
2
|
|
|
3
|
-

|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
[](https://golang.org/dl/)
|
|
6
6
|

|
|
7
7
|
|
|
8
8
|
iSpeak 让 AI 编程助手开口说话。你写代码,它播结果——眼睛休息,耳朵来听。
|
|
9
9
|
|
|
10
|
-
适合 Claude Code 或 Codex 常驻后台的开发者。AI
|
|
10
|
+
适合 Claude Code 或 Codex 常驻后台的开发者。AI 完成任务后自动播报;你发新消息时,未开始的旧播报会被丢弃,不花冤枉钱。
|
|
11
11
|
|
|
12
12
|
## 效果示例
|
|
13
13
|
|
|
@@ -39,11 +39,7 @@ ispeak-codex "构建完成,耗时 12 秒"
|
|
|
39
39
|
npm i -g @xdfnet/ispeak
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
当前 npm 安装会在本机编译 `ispeakd`,需要已安装 Go
|
|
43
|
-
|
|
44
|
-
```bash
|
|
45
|
-
brew install ffmpeg
|
|
46
|
-
```
|
|
42
|
+
当前 npm 安装会在本机编译 `ispeakd`,需要已安装 Go。主播放链路使用 macOS 原生 `AVAudioEngine`,不依赖 `ffmpeg`。失败时直接记录日志并删除任务。
|
|
47
43
|
|
|
48
44
|
**源码安装:**
|
|
49
45
|
|
|
@@ -76,11 +72,11 @@ ispeak "iSpeak 准备好了"
|
|
|
76
72
|
│ │ │
|
|
77
73
|
│ ▼ │
|
|
78
74
|
│ 单 Worker 流式链路 │
|
|
79
|
-
│ (SSE
|
|
75
|
+
│ (SSE PCM chunk → AVAudioEngine) │
|
|
80
76
|
│ │ │
|
|
81
77
|
│ ▼ │
|
|
82
|
-
│
|
|
83
|
-
│
|
|
78
|
+
│ 错误处理 │
|
|
79
|
+
│ (失败时记录日志并删除任务) │
|
|
84
80
|
└─────────────────────────────────────────────────────┘
|
|
85
81
|
```
|
|
86
82
|
|
|
@@ -104,8 +100,7 @@ pending → running → delete
|
|
|
104
100
|
- Markdown 链接:只保留链接标题,不播 URL
|
|
105
101
|
- 绝对路径:简化为“路径”
|
|
106
102
|
- 长 commit hash、UUID、长 ID:不播
|
|
107
|
-
-
|
|
108
|
-
- 下载进度和终端噪声:百分比、速度、进度条、ANSI 控制符
|
|
103
|
+
- 下载进度噪声:速度、ETA、预计剩余时间、ANSI 控制符
|
|
109
104
|
|
|
110
105
|
保留优先级:结论、成功/失败状态、需要用户操作的下一步、关键错误原因。
|
|
111
106
|
|
|
@@ -131,7 +126,7 @@ ispeak-codex "消息" # Codex 专属音色
|
|
|
131
126
|
```json
|
|
132
127
|
{
|
|
133
128
|
"apiKey": "你的火山引擎 API Key",
|
|
134
|
-
"endpoint": "https://openspeech.bytedance.com/api/v3/tts/unidirectional",
|
|
129
|
+
"endpoint": "https://openspeech.bytedance.com/api/v3/tts/unidirectional/sse",
|
|
135
130
|
"defaultVoice": {
|
|
136
131
|
"voice_type": "zh_female_mizai_uranus_bigtts",
|
|
137
132
|
"resourceId": "seed-tts-2.0"
|
|
@@ -173,7 +168,13 @@ ispeak-codex "消息" # Codex 专属音色
|
|
|
173
168
|
|
|
174
169
|
### Codex
|
|
175
170
|
|
|
176
|
-
|
|
171
|
+
推荐在 `~/.codex/config.toml` 中添加回合结束通知:
|
|
172
|
+
|
|
173
|
+
```toml
|
|
174
|
+
notify = ["bash", "/Users/你的用户名/.config/iSpeak/hook-speak.sh", "codex"]
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
如果你启用了 Codex hooks,也可以在 `~/.codex/hooks.json` 中添加 Stop Hook:
|
|
177
178
|
|
|
178
179
|
```json
|
|
179
180
|
{
|
|
@@ -189,6 +190,8 @@ ispeak-codex "消息" # Codex 专属音色
|
|
|
189
190
|
}
|
|
190
191
|
```
|
|
191
192
|
|
|
193
|
+
`hook-speak.sh` 会按 `turn_id` 做一次去重,所以即使 `notify` 和 `Stop` 都启用,同一回合也只会播一次。
|
|
194
|
+
|
|
192
195
|
## 开发命令
|
|
193
196
|
|
|
194
197
|
```bash
|
package/clean_text.go
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"regexp"
|
|
5
|
+
"strings"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
var (
|
|
9
|
+
markdownLinkRe = regexp.MustCompile(`\[[^\]]+\]\(([^)]*)\)`)
|
|
10
|
+
absolutePathRe = regexp.MustCompile(`/(?:Users|private|tmp|var|opt|usr|bin|sbin|etc|Library|Applications)/\S+`)
|
|
11
|
+
commitHashRe = regexp.MustCompile(`\b[0-9a-f]{7,40}\b`)
|
|
12
|
+
uuidRe = regexp.MustCompile(`\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\b`)
|
|
13
|
+
urlRe = regexp.MustCompile(`https?://\S+`)
|
|
14
|
+
ansiEscapeRe = regexp.MustCompile(`\x1b\[[0-9;]*[A-Za-z]`)
|
|
15
|
+
multiSpaceRe = regexp.MustCompile(`\s+`)
|
|
16
|
+
markdownListRe = regexp.MustCompile(`^\s*(?:[-*+]\s+|\d+[.)]\s+)`)
|
|
17
|
+
htmlTagRe = regexp.MustCompile(`<[^>]+>`)
|
|
18
|
+
codeFenceStartRe = regexp.MustCompile("^```")
|
|
19
|
+
artifactStartRe = regexp.MustCompile(`(?i)^<artifact\b`)
|
|
20
|
+
htmlDocumentLineRe = regexp.MustCompile(`(?i)^<!doctype html|^<html\b|^<head\b|^<body\b|^<style\b|^</`)
|
|
21
|
+
speedNoiseRe = regexp.MustCompile(`(?i)\d+(?:\.\d+)?\s*(?:kb|mb|gb)/s`)
|
|
22
|
+
etaNoiseRe = regexp.MustCompile(`(?i)\bETA\b|预计剩余|剩余时间`)
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
// 过滤格式符号,保留自然朗读文本。
|
|
26
|
+
// 顺序很重要:先跳过跨行块结构,再跳过整行噪声,最后清理行内符号。
|
|
27
|
+
func cleanText(text string) string {
|
|
28
|
+
var lines []string
|
|
29
|
+
rawLines := strings.Split(text, "\n")
|
|
30
|
+
inCodeBlock := false
|
|
31
|
+
inArtifact := false
|
|
32
|
+
inMarkdownTable := false
|
|
33
|
+
for i := 0; i < len(rawLines); i++ {
|
|
34
|
+
line := rawLines[i]
|
|
35
|
+
line = strings.TrimSpace(line)
|
|
36
|
+
if line == "" {
|
|
37
|
+
inMarkdownTable = false
|
|
38
|
+
continue
|
|
39
|
+
}
|
|
40
|
+
if codeFenceStartRe.MatchString(line) {
|
|
41
|
+
inCodeBlock = !inCodeBlock
|
|
42
|
+
continue
|
|
43
|
+
}
|
|
44
|
+
if inCodeBlock {
|
|
45
|
+
continue
|
|
46
|
+
}
|
|
47
|
+
if artifactStartRe.MatchString(line) {
|
|
48
|
+
inArtifact = !strings.Contains(strings.ToLower(line), "</artifact>")
|
|
49
|
+
continue
|
|
50
|
+
}
|
|
51
|
+
if inArtifact {
|
|
52
|
+
if strings.Contains(strings.ToLower(line), "</artifact>") {
|
|
53
|
+
inArtifact = false
|
|
54
|
+
}
|
|
55
|
+
continue
|
|
56
|
+
}
|
|
57
|
+
if isMarkdownTableSeparator(line) {
|
|
58
|
+
if len(lines) > 0 && isMarkdownTableRow(strings.TrimSpace(rawLines[i-1])) {
|
|
59
|
+
lines = lines[:len(lines)-1]
|
|
60
|
+
}
|
|
61
|
+
inMarkdownTable = true
|
|
62
|
+
continue
|
|
63
|
+
}
|
|
64
|
+
if inMarkdownTable {
|
|
65
|
+
if isMarkdownTableRow(line) {
|
|
66
|
+
continue
|
|
67
|
+
}
|
|
68
|
+
inMarkdownTable = false
|
|
69
|
+
}
|
|
70
|
+
if shouldSkipSpeechLine(line) {
|
|
71
|
+
continue
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
cleaned := cleanSpeechLine(line)
|
|
75
|
+
if cleaned != "" {
|
|
76
|
+
lines = append(lines, cleaned)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return strings.Join(lines, ",")
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
func shouldSkipSpeechLine(line string) bool {
|
|
83
|
+
if isMarkdownTableSeparator(line) {
|
|
84
|
+
return true
|
|
85
|
+
}
|
|
86
|
+
if strings.HasPrefix(line, "---") && strings.Count(line, "-") > 3 {
|
|
87
|
+
return true
|
|
88
|
+
}
|
|
89
|
+
if htmlDocumentLineRe.MatchString(line) {
|
|
90
|
+
return true
|
|
91
|
+
}
|
|
92
|
+
if isProgressNoiseLine(line) {
|
|
93
|
+
return true
|
|
94
|
+
}
|
|
95
|
+
if isMostlyTableRow(line) {
|
|
96
|
+
return true
|
|
97
|
+
}
|
|
98
|
+
return false
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
func isMarkdownTableSeparator(line string) bool {
|
|
102
|
+
line = strings.TrimSpace(line)
|
|
103
|
+
return strings.Contains(line, "|") && strings.Trim(line, "|-: ") == ""
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
func isMarkdownTableRow(line string) bool {
|
|
107
|
+
line = strings.TrimSpace(line)
|
|
108
|
+
return strings.Count(line, "|") >= 2
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
func cleanSpeechLine(line string) string {
|
|
112
|
+
// Markdown 链接必须在 URL 删除前处理,否则会丢掉链接标题。
|
|
113
|
+
line = ansiEscapeRe.ReplaceAllString(line, "")
|
|
114
|
+
line = markdownListRe.ReplaceAllString(line, "")
|
|
115
|
+
line = markdownLinkRe.ReplaceAllStringFunc(line, func(match string) string {
|
|
116
|
+
if end := strings.Index(match, "]"); end > 1 {
|
|
117
|
+
return match[1:end]
|
|
118
|
+
}
|
|
119
|
+
return ""
|
|
120
|
+
})
|
|
121
|
+
line = urlRe.ReplaceAllString(line, "")
|
|
122
|
+
line = absolutePathRe.ReplaceAllString(line, " 路径 ")
|
|
123
|
+
// UUID 必须在短 hash 前处理,避免先删短片段后破坏 UUID 识别。
|
|
124
|
+
line = uuidRe.ReplaceAllString(line, "")
|
|
125
|
+
line = commitHashRe.ReplaceAllString(line, "")
|
|
126
|
+
line = htmlTagRe.ReplaceAllString(line, "")
|
|
127
|
+
line = strings.NewReplacer(
|
|
128
|
+
"**", "",
|
|
129
|
+
"*", "",
|
|
130
|
+
"`", "",
|
|
131
|
+
"#", "",
|
|
132
|
+
">", "",
|
|
133
|
+
"✅", "",
|
|
134
|
+
"❌", "",
|
|
135
|
+
"✓", "",
|
|
136
|
+
"✗", "",
|
|
137
|
+
"→", "到",
|
|
138
|
+
).Replace(line)
|
|
139
|
+
line = strings.Trim(line, " \t-:|")
|
|
140
|
+
line = multiSpaceRe.ReplaceAllString(line, " ")
|
|
141
|
+
return strings.TrimSpace(line)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
func isMostlyTableRow(line string) bool {
|
|
145
|
+
if !strings.Contains(line, "|") {
|
|
146
|
+
return false
|
|
147
|
+
}
|
|
148
|
+
return strings.Count(line, "|") >= 2 && len([]rune(line)) > 40
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
func isProgressNoiseLine(line string) bool {
|
|
152
|
+
return speedNoiseRe.MatchString(line) || etaNoiseRe.MatchString(line)
|
|
153
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"apiKey": "your-api-key",
|
|
3
|
-
"endpoint": "https://openspeech.bytedance.com/api/v3/tts/unidirectional",
|
|
3
|
+
"endpoint": "https://openspeech.bytedance.com/api/v3/tts/unidirectional/sse",
|
|
4
4
|
"defaultVoice": {
|
|
5
5
|
"voice_type": "zh_female_mizai_uranus_bigtts",
|
|
6
6
|
"resourceId": "seed-tts-2.0"
|