@xdfnet/ispeak 1.6.1 → 1.6.2
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/README.md +4 -3
- package/configs/hook-speak.sh +78 -23
- package/main.go +39 -2
- package/package.json +1 -1
- package/scripts/ispeak +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# iSpeak
|
|
2
2
|
|
|
3
|
-

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

|
|
@@ -51,11 +51,12 @@ brew install ffmpeg
|
|
|
51
51
|
git clone https://github.com/xdfnet/iSpeak.git && cd iSpeak && make install
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
安装后编辑 API Key,然后验证:
|
|
55
55
|
|
|
56
56
|
```bash
|
|
57
|
+
open ~/.config/iSpeak/config.json
|
|
57
58
|
ispeak status
|
|
58
|
-
ispeak
|
|
59
|
+
ispeak "iSpeak 准备好了"
|
|
59
60
|
```
|
|
60
61
|
|
|
61
62
|
## 工作原理
|
package/configs/hook-speak.sh
CHANGED
|
@@ -11,52 +11,107 @@ LOG="$HOME/.config/iSpeak/hook.log"
|
|
|
11
11
|
|
|
12
12
|
input=$(cat)
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
json_value() {
|
|
15
|
+
local key="$1"
|
|
16
|
+
if command -v node >/dev/null 2>&1; then
|
|
17
|
+
printf "%s" "$input" | node -e '
|
|
18
|
+
const key = process.argv[1];
|
|
19
|
+
let input = "";
|
|
20
|
+
process.stdin.setEncoding("utf8");
|
|
21
|
+
process.stdin.on("data", chunk => input += chunk);
|
|
22
|
+
process.stdin.on("end", () => {
|
|
23
|
+
try {
|
|
24
|
+
const value = JSON.parse(input)[key];
|
|
25
|
+
if (typeof value === "string") process.stdout.write(value);
|
|
26
|
+
} catch (_) {}
|
|
27
|
+
});
|
|
28
|
+
' "$key"
|
|
29
|
+
return
|
|
30
|
+
fi
|
|
18
31
|
|
|
19
|
-
|
|
32
|
+
printf "%s" "$input" | sed -n "s/.*\"$key\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*/\1/p"
|
|
33
|
+
}
|
|
20
34
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
cutoff=$(($(date +%s) * 1000 - 30000))
|
|
35
|
+
extract_recent_assistant_text() {
|
|
36
|
+
local transcript="$1"
|
|
37
|
+
local cutoff="$2"
|
|
25
38
|
|
|
26
|
-
|
|
27
|
-
|
|
39
|
+
if command -v node >/dev/null 2>&1; then
|
|
40
|
+
node -e '
|
|
41
|
+
const fs = require("fs");
|
|
42
|
+
const file = process.argv[1];
|
|
43
|
+
const cutoff = Number(process.argv[2]);
|
|
44
|
+
const out = [];
|
|
45
|
+
|
|
46
|
+
function collectText(content) {
|
|
47
|
+
if (typeof content === "string") {
|
|
48
|
+
out.push(content);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (!Array.isArray(content)) return;
|
|
52
|
+
for (const item of content) {
|
|
53
|
+
if (item && typeof item.text === "string") out.push(item.text);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for (const line of fs.readFileSync(file, "utf8").split(/\r?\n/)) {
|
|
58
|
+
if (!line.trim()) continue;
|
|
59
|
+
try {
|
|
60
|
+
const event = JSON.parse(line);
|
|
61
|
+
if (typeof event.timestamp === "number" && event.timestamp < cutoff) continue;
|
|
62
|
+
if (event.role === "assistant") collectText(event.content);
|
|
63
|
+
if (event.message && event.message.role === "assistant") collectText(event.message.content);
|
|
64
|
+
} catch (_) {}
|
|
65
|
+
}
|
|
66
|
+
process.stdout.write([...new Set(out.filter(Boolean))].join(" "));
|
|
67
|
+
' "$transcript" "$cutoff" 2>/dev/null
|
|
68
|
+
return
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
awk -v cutoff="$cutoff" '
|
|
28
72
|
{
|
|
29
|
-
|
|
30
|
-
if (match($0, /"timestamp"\s*:\s*[0-9]+/)) {
|
|
73
|
+
if (match($0, /"timestamp"[[:space:]]*:[[:space:]]*[0-9]+/)) {
|
|
31
74
|
ts = substr($0, RSTART, RLENGTH)
|
|
32
75
|
gsub(/[^0-9]/, "", ts)
|
|
33
76
|
ts = int(ts)
|
|
34
77
|
if (ts < cutoff) next
|
|
35
78
|
}
|
|
36
79
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
# 提取 content(可能是字符串或数组)
|
|
40
|
-
if (match($0, /"content"\s*:\s*\[/)) {
|
|
41
|
-
# 数组形式,提取所有 text 字段
|
|
80
|
+
if (match($0, /"role"[[:space:]]*:[[:space:]]*"assistant"/)) {
|
|
81
|
+
if (match($0, /"content"[[:space:]]*:[[:space:]]*\[/)) {
|
|
42
82
|
gsub(/[^{]*\[/, "", $0)
|
|
43
83
|
gsub(/\].*/, "", $0)
|
|
44
|
-
while (match($0, /"text"
|
|
84
|
+
while (match($0, /"text"[[:space:]]*:[[:space:]]*"[^"]*"/)) {
|
|
45
85
|
t = substr($0, RSTART, RLENGTH)
|
|
46
|
-
gsub(/"text"
|
|
86
|
+
gsub(/"text"[[:space:]]*:[[:space:]]*"/, "", t)
|
|
47
87
|
gsub(/"$/, "", t)
|
|
48
88
|
if (t != "") print t
|
|
49
89
|
$0 = substr($0, RSTART + RLENGTH)
|
|
50
90
|
}
|
|
51
|
-
} else if (match($0, /"content"
|
|
91
|
+
} else if (match($0, /"content"[[:space:]]*:[[:space:]]*"[^"]*"/)) {
|
|
52
92
|
t = substr($0, RSTART, RLENGTH)
|
|
53
|
-
gsub(/"content"
|
|
93
|
+
gsub(/"content"[[:space:]]*:[[:space:]]*"/, "", t)
|
|
54
94
|
gsub(/"$/, "", t)
|
|
55
95
|
if (t != "") print t
|
|
56
96
|
}
|
|
57
97
|
}
|
|
58
98
|
}
|
|
59
|
-
' "$transcript" 2>/dev/null | sort -u | tr '\n' ' '
|
|
99
|
+
' "$transcript" 2>/dev/null | sort -u | tr '\n' ' '
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
# 从 stdin JSON 提取 transcript 路径和最后一条消息
|
|
103
|
+
transcript=$(json_value "transcript_path")
|
|
104
|
+
last_msg=$(json_value "last_assistant_message")
|
|
105
|
+
|
|
106
|
+
all_text="$last_msg"
|
|
107
|
+
|
|
108
|
+
# 如果有 transcript 文件,提取最近 30 秒内的所有 assistant 消息
|
|
109
|
+
if [[ -n "$transcript" && -f "$transcript" ]]; then
|
|
110
|
+
# 计算 30 秒前的时间戳(毫秒)
|
|
111
|
+
cutoff=$(($(date +%s) * 1000 - 30000))
|
|
112
|
+
|
|
113
|
+
# 优先用 JSON parser,Node 不存在时回退到简易 awk。
|
|
114
|
+
extra=$(extract_recent_assistant_text "$transcript" "$cutoff")
|
|
60
115
|
|
|
61
116
|
if [[ -n "$extra" ]]; then
|
|
62
117
|
all_text="$extra"
|
package/main.go
CHANGED
|
@@ -387,6 +387,17 @@ func loadConfig() Config {
|
|
|
387
387
|
}
|
|
388
388
|
var cfg Config
|
|
389
389
|
if json.Unmarshal(data, &cfg) == nil && cfg.APIKey != "" {
|
|
390
|
+
if err := validateConfig(cfg); err != nil {
|
|
391
|
+
log.Printf("配置文件无效: %s err=%v", p, err)
|
|
392
|
+
configCacheMu.Lock()
|
|
393
|
+
if configCacheValid {
|
|
394
|
+
cached := configCache
|
|
395
|
+
configCacheMu.Unlock()
|
|
396
|
+
return cached
|
|
397
|
+
}
|
|
398
|
+
configCacheMu.Unlock()
|
|
399
|
+
return cfg
|
|
400
|
+
}
|
|
390
401
|
log.Printf("配置文件: %s", p)
|
|
391
402
|
if cfg.DefaultVoice != nil {
|
|
392
403
|
log.Printf("默认音色: %s (%s)", cfg.DefaultVoice.VoiceType, cfg.DefaultVoice.ResourceID)
|
|
@@ -779,8 +790,26 @@ func validateConfig(cfg Config) error {
|
|
|
779
790
|
if cfg.Endpoint == "" {
|
|
780
791
|
return fmt.Errorf("endpoint 未设置")
|
|
781
792
|
}
|
|
782
|
-
if
|
|
783
|
-
return
|
|
793
|
+
if err := validateVoiceInfo("defaultVoice", cfg.DefaultVoice); err != nil {
|
|
794
|
+
return err
|
|
795
|
+
}
|
|
796
|
+
for source, voice := range cfg.SourceVoices {
|
|
797
|
+
if err := validateVoiceInfo(fmt.Sprintf("sourceVoices.%s", source), voice); err != nil {
|
|
798
|
+
return err
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
return nil
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
func validateVoiceInfo(name string, voice *VoiceInfo) error {
|
|
805
|
+
if voice == nil {
|
|
806
|
+
return fmt.Errorf("%s 未设置", name)
|
|
807
|
+
}
|
|
808
|
+
if voice.VoiceType == "" {
|
|
809
|
+
return fmt.Errorf("%s.voice_type 未设置", name)
|
|
810
|
+
}
|
|
811
|
+
if voice.ResourceID == "" {
|
|
812
|
+
return fmt.Errorf("%s.resourceId 未设置", name)
|
|
784
813
|
}
|
|
785
814
|
return nil
|
|
786
815
|
}
|
|
@@ -809,6 +838,10 @@ func handleConnection(conn net.Conn, engine *TaskEngine) {
|
|
|
809
838
|
defer conn.Close()
|
|
810
839
|
|
|
811
840
|
cfg := loadConfig()
|
|
841
|
+
if err := validateConfig(cfg); err != nil {
|
|
842
|
+
log.Printf("配置错误,跳过本次播报: %v", err)
|
|
843
|
+
return
|
|
844
|
+
}
|
|
812
845
|
|
|
813
846
|
var sb strings.Builder
|
|
814
847
|
scanner := bufio.NewScanner(conn)
|
|
@@ -816,6 +849,10 @@ func handleConnection(conn net.Conn, engine *TaskEngine) {
|
|
|
816
849
|
for scanner.Scan() {
|
|
817
850
|
sb.WriteString(scanner.Text())
|
|
818
851
|
}
|
|
852
|
+
if err := scanner.Err(); err != nil {
|
|
853
|
+
log.Printf("读取 socket 消息失败: %v", err)
|
|
854
|
+
return
|
|
855
|
+
}
|
|
819
856
|
|
|
820
857
|
text := strings.TrimSpace(sb.String())
|
|
821
858
|
if text == "" {
|
package/package.json
CHANGED