dsh-speak 1.0.0 → 1.2.0
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/LICENSE +21 -21
- package/README.md +289 -186
- package/README.zh-CN.md +274 -177
- package/adapters/dsh/install.ps1 +81 -81
- package/adapters/dsh/speech-hook.js +142 -133
- package/docs/DESIGN.md +270 -245
- package/docs/DESIGN.zh-CN.md +253 -232
- package/engine/speak.ps1 +76 -76
- package/engine/speak.sh +89 -0
- package/engine/speech-prompt.ps1 +22 -22
- package/engine/speech-summary.ps1 +27 -27
- package/package.json +46 -43
package/engine/speak.ps1
CHANGED
|
@@ -1,76 +1,76 @@
|
|
|
1
|
-
# speak.ps1 — Harness-agnostic speech engine (Windows SAPI5 + NaturalVoiceSAPIAdapter)
|
|
2
|
-
# ====================================================================================
|
|
3
|
-
# Reads text (inline or from a UTF-8 file), cleans it for speech synthesis, and reads
|
|
4
|
-
# it aloud through Windows SAPI5, preferring natural voices registered by
|
|
5
|
-
# NaturalVoiceSAPIAdapter (https://github.com/gexgd0419/NaturalVoiceSAPIAdapter).
|
|
6
|
-
#
|
|
7
|
-
# This script knows NOTHING about any harness (DSH, Claude Code, ...). Any process
|
|
8
|
-
# can call it:
|
|
9
|
-
#
|
|
10
|
-
# powershell.exe -NoProfile -ExecutionPolicy Bypass -File speak.ps1 -Text "hello"
|
|
11
|
-
# powershell.exe -NoProfile -ExecutionPolicy Bypass -File speak.ps1 -File C:\tmp\msg.txt
|
|
12
|
-
#
|
|
13
|
-
# It is best-effort by design: it never throws, never blocks the caller for longer
|
|
14
|
-
# than the utterance itself, and exits 0 even if something failed.
|
|
15
|
-
#
|
|
16
|
-
# Design notes (see docs/DESIGN.md for full rationale):
|
|
17
|
-
# * Markdown symbols, URLs and emoji are stripped before speaking — SAPI5 Speak()
|
|
18
|
-
# silently fails (produces no audio, no error) when it hits emoji/surrogates.
|
|
19
|
-
# * NaturalVoiceSAPIAdapter has a per-Speak character ceiling (~375-470 chars);
|
|
20
|
-
# beyond that it silently speaks nothing. Text longer than $MaxChars is replaced
|
|
21
|
-
# with $LongTextMessage instead.
|
|
22
|
-
# * Adapter-registered voices often have plain names ("Microsoft Xiaoxiao") that do
|
|
23
|
-
# not contain the word "Natural", so matching checks Name + Description.
|
|
24
|
-
# ====================================================================================
|
|
25
|
-
|
|
26
|
-
param(
|
|
27
|
-
[string]$Text = '',
|
|
28
|
-
[string]$File = '',
|
|
29
|
-
[int]$Volume = 50,
|
|
30
|
-
[int]$Rate = 1,
|
|
31
|
-
[int]$MaxChars = 300,
|
|
32
|
-
[string]$LongTextMessage = '本次播报内容较长,请自行阅读。'
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
# ---------- input: pick text source ----------
|
|
36
|
-
if ($File) {
|
|
37
|
-
if (-not (Test-Path $File)) { exit 0 }
|
|
38
|
-
$text = [System.IO.File]::ReadAllText($File, [System.Text.Encoding]::UTF8)
|
|
39
|
-
} else {
|
|
40
|
-
$text = [string]$Text
|
|
41
|
-
}
|
|
42
|
-
if (-not $text -or -not $text.Trim()) { exit 0 }
|
|
43
|
-
|
|
44
|
-
# ---------- clean: markdown -> plain speech text ----------
|
|
45
|
-
# code blocks, inline code, markdown links, bare URLs, emphasis/marker chars
|
|
46
|
-
$text = $text -replace '```[\s\S]*?```', ' '
|
|
47
|
-
$text = $text -replace '`[^`]*`', ' '
|
|
48
|
-
$text = $text -replace '\[([^\]]*)\]\([^\)]*\)', '$1'
|
|
49
|
-
$text = $text -replace 'https?://\S+', ' '
|
|
50
|
-
$text = $text -replace '[-#*_~|>+]+', ' '
|
|
51
|
-
# emoji / special symbols (Speak() fails silently on them): keep CJK, CJK punct,
|
|
52
|
-
# full-width ranges, ASCII printable
|
|
53
|
-
$text = [regex]::Replace($text, '[^一-龥 -〿- - -~]', '')
|
|
54
|
-
$text = $text -replace '\s+', ' '
|
|
55
|
-
$text = $text.Trim()
|
|
56
|
-
|
|
57
|
-
# ---------- length guard: adapter per-Speak ceiling ----------
|
|
58
|
-
if ($text.Length -gt $MaxChars) { $text = $LongTextMessage }
|
|
59
|
-
|
|
60
|
-
# ---------- speak ----------
|
|
61
|
-
Add-Type -AssemblyName System.Speech
|
|
62
|
-
$synth = New-Object System.Speech.Synthesis.SpeechSynthesizer
|
|
63
|
-
$synth.Volume = $Volume
|
|
64
|
-
|
|
65
|
-
# prefer a zh natural voice (NaturalVoiceSAPIAdapter-registered), fall back to any zh
|
|
66
|
-
$voices = $synth.GetInstalledVoices()
|
|
67
|
-
$voice = $voices | Where-Object {
|
|
68
|
-
$_.VoiceInfo.Culture.Name -like 'zh*' -and
|
|
69
|
-
($_.VoiceInfo.Name + ' ' + $_.VoiceInfo.Description) -match 'Natural|Online'
|
|
70
|
-
} | Select-Object -First 1
|
|
71
|
-
if (-not $voice) { $voice = $voices | Where-Object { $_.VoiceInfo.Culture.Name -like 'zh*' } | Select-Object -First 1 }
|
|
72
|
-
if ($voice) { $synth.SelectVoice($voice.VoiceInfo.Name) }
|
|
73
|
-
|
|
74
|
-
$synth.Rate = $Rate
|
|
75
|
-
$synth.Speak($text)
|
|
76
|
-
exit 0
|
|
1
|
+
# speak.ps1 — Harness-agnostic speech engine (Windows SAPI5 + NaturalVoiceSAPIAdapter)
|
|
2
|
+
# ====================================================================================
|
|
3
|
+
# Reads text (inline or from a UTF-8 file), cleans it for speech synthesis, and reads
|
|
4
|
+
# it aloud through Windows SAPI5, preferring natural voices registered by
|
|
5
|
+
# NaturalVoiceSAPIAdapter (https://github.com/gexgd0419/NaturalVoiceSAPIAdapter).
|
|
6
|
+
#
|
|
7
|
+
# This script knows NOTHING about any harness (DSH, Claude Code, ...). Any process
|
|
8
|
+
# can call it:
|
|
9
|
+
#
|
|
10
|
+
# powershell.exe -NoProfile -ExecutionPolicy Bypass -File speak.ps1 -Text "hello"
|
|
11
|
+
# powershell.exe -NoProfile -ExecutionPolicy Bypass -File speak.ps1 -File C:\tmp\msg.txt
|
|
12
|
+
#
|
|
13
|
+
# It is best-effort by design: it never throws, never blocks the caller for longer
|
|
14
|
+
# than the utterance itself, and exits 0 even if something failed.
|
|
15
|
+
#
|
|
16
|
+
# Design notes (see docs/DESIGN.md for full rationale):
|
|
17
|
+
# * Markdown symbols, URLs and emoji are stripped before speaking — SAPI5 Speak()
|
|
18
|
+
# silently fails (produces no audio, no error) when it hits emoji/surrogates.
|
|
19
|
+
# * NaturalVoiceSAPIAdapter has a per-Speak character ceiling (~375-470 chars);
|
|
20
|
+
# beyond that it silently speaks nothing. Text longer than $MaxChars is replaced
|
|
21
|
+
# with $LongTextMessage instead.
|
|
22
|
+
# * Adapter-registered voices often have plain names ("Microsoft Xiaoxiao") that do
|
|
23
|
+
# not contain the word "Natural", so matching checks Name + Description.
|
|
24
|
+
# ====================================================================================
|
|
25
|
+
|
|
26
|
+
param(
|
|
27
|
+
[string]$Text = '',
|
|
28
|
+
[string]$File = '',
|
|
29
|
+
[int]$Volume = 50,
|
|
30
|
+
[int]$Rate = 1,
|
|
31
|
+
[int]$MaxChars = 300,
|
|
32
|
+
[string]$LongTextMessage = '本次播报内容较长,请自行阅读。'
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# ---------- input: pick text source ----------
|
|
36
|
+
if ($File) {
|
|
37
|
+
if (-not (Test-Path $File)) { exit 0 }
|
|
38
|
+
$text = [System.IO.File]::ReadAllText($File, [System.Text.Encoding]::UTF8)
|
|
39
|
+
} else {
|
|
40
|
+
$text = [string]$Text
|
|
41
|
+
}
|
|
42
|
+
if (-not $text -or -not $text.Trim()) { exit 0 }
|
|
43
|
+
|
|
44
|
+
# ---------- clean: markdown -> plain speech text ----------
|
|
45
|
+
# code blocks, inline code, markdown links, bare URLs, emphasis/marker chars
|
|
46
|
+
$text = $text -replace '```[\s\S]*?```', ' '
|
|
47
|
+
$text = $text -replace '`[^`]*`', ' '
|
|
48
|
+
$text = $text -replace '\[([^\]]*)\]\([^\)]*\)', '$1'
|
|
49
|
+
$text = $text -replace 'https?://\S+', ' '
|
|
50
|
+
$text = $text -replace '[-#*_~|>+]+', ' '
|
|
51
|
+
# emoji / special symbols (Speak() fails silently on them): keep CJK, CJK punct,
|
|
52
|
+
# full-width ranges, ASCII printable
|
|
53
|
+
$text = [regex]::Replace($text, '[^一-龥 -〿- - -~]', '')
|
|
54
|
+
$text = $text -replace '\s+', ' '
|
|
55
|
+
$text = $text.Trim()
|
|
56
|
+
|
|
57
|
+
# ---------- length guard: adapter per-Speak ceiling ----------
|
|
58
|
+
if ($text.Length -gt $MaxChars) { $text = $LongTextMessage }
|
|
59
|
+
|
|
60
|
+
# ---------- speak ----------
|
|
61
|
+
Add-Type -AssemblyName System.Speech
|
|
62
|
+
$synth = New-Object System.Speech.Synthesis.SpeechSynthesizer
|
|
63
|
+
$synth.Volume = $Volume
|
|
64
|
+
|
|
65
|
+
# prefer a zh natural voice (NaturalVoiceSAPIAdapter-registered), fall back to any zh
|
|
66
|
+
$voices = $synth.GetInstalledVoices()
|
|
67
|
+
$voice = $voices | Where-Object {
|
|
68
|
+
$_.VoiceInfo.Culture.Name -like 'zh*' -and
|
|
69
|
+
($_.VoiceInfo.Name + ' ' + $_.VoiceInfo.Description) -match 'Natural|Online'
|
|
70
|
+
} | Select-Object -First 1
|
|
71
|
+
if (-not $voice) { $voice = $voices | Where-Object { $_.VoiceInfo.Culture.Name -like 'zh*' } | Select-Object -First 1 }
|
|
72
|
+
if ($voice) { $synth.SelectVoice($voice.VoiceInfo.Name) }
|
|
73
|
+
|
|
74
|
+
$synth.Rate = $Rate
|
|
75
|
+
$synth.Speak($text)
|
|
76
|
+
exit 0
|
package/engine/speak.sh
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# speak.sh — macOS speech engine (uses the built-in `say` command)
|
|
3
|
+
# ==============================================================================
|
|
4
|
+
# The macOS counterpart of engine/speak.ps1. Reads text (inline or UTF-8 file),
|
|
5
|
+
# cleans it for speech synthesis, and reads it aloud via the system `say`
|
|
6
|
+
# command. Any process can call it:
|
|
7
|
+
#
|
|
8
|
+
# ./speak.sh -t "你好,构建完成"
|
|
9
|
+
# ./speak.sh -f /tmp/msg.txt -v Eddy -r 200
|
|
10
|
+
#
|
|
11
|
+
# Best-effort by design: never throws, exits 0 even if something failed.
|
|
12
|
+
#
|
|
13
|
+
# Voice selection:
|
|
14
|
+
# * no -v (default): follow the system voice — on recent macOS this is the
|
|
15
|
+
# Siri voice chosen in Settings > Siri > Voice (e.g. "声音 1"), on older
|
|
16
|
+
# versions the Spoken Content voice. This is the least surprising default.
|
|
17
|
+
# * -v <name>: force a voice by name (e.g. Eddy, Flo, Tingting — see
|
|
18
|
+
# `say -v '?'`).
|
|
19
|
+
# * NOTE: the Siri voices ("声音 1-4") are NOT exposed to `say` — they do not
|
|
20
|
+
# appear in `say -v '?'` and cannot be selected by name; they only work as
|
|
21
|
+
# the system default.
|
|
22
|
+
#
|
|
23
|
+
# Notes:
|
|
24
|
+
# * LC_ALL is pinned to a UTF-8 locale so the perl cleaning pipeline and bash
|
|
25
|
+
# character counting behave identically regardless of the caller's locale
|
|
26
|
+
# (a C/POSIX locale would silently strip all CJK text).
|
|
27
|
+
# * The cleaning pipeline mirrors speak.ps1 (markdown/URL/emoji stripped).
|
|
28
|
+
# * `say` has no volume flag — volume is controlled by the system output.
|
|
29
|
+
# * Length guard: text over $MAX_CHARS is replaced with $LONG_MSG.
|
|
30
|
+
# ==============================================================================
|
|
31
|
+
|
|
32
|
+
export LC_ALL="${LC_ALL:-en_US.UTF-8}"
|
|
33
|
+
|
|
34
|
+
TEXT=""
|
|
35
|
+
FILE=""
|
|
36
|
+
VOICE=""
|
|
37
|
+
RATE=175 # words per minute (say default)
|
|
38
|
+
MAX_CHARS=300
|
|
39
|
+
LONG_MSG="本次播报内容较长,请自行阅读。"
|
|
40
|
+
|
|
41
|
+
usage() {
|
|
42
|
+
echo "usage: speak.sh [-t text | -f file] [-v voice] [-r wpm] [-m maxchars] [-l longmsg]" >&2
|
|
43
|
+
exit 1
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
while getopts "t:f:v:r:m:l:h" opt; do
|
|
47
|
+
case "$opt" in
|
|
48
|
+
t) TEXT="$OPTARG" ;;
|
|
49
|
+
f) FILE="$OPTARG" ;;
|
|
50
|
+
v) VOICE="$OPTARG" ;;
|
|
51
|
+
r) RATE="$OPTARG" ;;
|
|
52
|
+
m) MAX_CHARS="$OPTARG" ;;
|
|
53
|
+
l) LONG_MSG="$OPTARG" ;;
|
|
54
|
+
h) usage ;;
|
|
55
|
+
*) usage ;;
|
|
56
|
+
esac
|
|
57
|
+
done
|
|
58
|
+
|
|
59
|
+
# ---------- input ----------
|
|
60
|
+
if [ -n "$FILE" ]; then
|
|
61
|
+
[ -f "$FILE" ] || exit 0
|
|
62
|
+
TEXT=$(/usr/bin/perl -CSD -e 'print <>' "$FILE")
|
|
63
|
+
fi
|
|
64
|
+
if [ -z "$TEXT" ]; then exit 0; fi
|
|
65
|
+
|
|
66
|
+
# ---------- clean (mirrors speak.ps1) ----------
|
|
67
|
+
TEXT=$(printf '%s' "$TEXT" | /usr/bin/perl -CSD -pe '
|
|
68
|
+
s/```[\s\S]*?```/ /g; # code blocks
|
|
69
|
+
s/`[^`]*`/ /g; # inline code
|
|
70
|
+
s/\[([^\]]*)\]\([^\)]*\)/$1/g; # markdown links
|
|
71
|
+
s|https?://\S+| |g; # bare URLs
|
|
72
|
+
s/[-#*_~|>+]+/ /g; # emphasis / marker chars
|
|
73
|
+
s/[^\p{Han}\x{3000}-\x{303F}\x{FF00}-\x{FFEF}\x{2000}-\x{206F}\x{20}-\x{7E}]//g; # emoji / specials
|
|
74
|
+
s/\s+/ /g; # collapse whitespace
|
|
75
|
+
')
|
|
76
|
+
TEXT=$(printf '%s' "$TEXT" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
|
|
77
|
+
|
|
78
|
+
# ---------- length guard ----------
|
|
79
|
+
if [ "${#TEXT}" -gt "$MAX_CHARS" ]; then
|
|
80
|
+
TEXT="$LONG_MSG"
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
# ---------- speak (no -v => system default voice) ----------
|
|
84
|
+
if [ -n "$VOICE" ]; then
|
|
85
|
+
say -v "$VOICE" -r "$RATE" "$TEXT"
|
|
86
|
+
else
|
|
87
|
+
say -r "$RATE" "$TEXT"
|
|
88
|
+
fi
|
|
89
|
+
exit 0
|
package/engine/speech-prompt.ps1
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
# speech-prompt.ps1 — Short prompt announcement (synchronous, blocking)
|
|
2
|
-
# Use when a harness/agent needs the user's attention (a question, an approval
|
|
3
|
-
# request). Reads the text through engine/speak.ps1 and waits until it finishes,
|
|
4
|
-
# so the caller knows the announcement was actually spoken.
|
|
5
|
-
#
|
|
6
|
-
# powershell.exe -NoProfile -ExecutionPolicy Bypass -File speech-prompt.ps1 -Text "请做出选择"
|
|
7
|
-
|
|
8
|
-
param([string]$Text = '请做出选择')
|
|
9
|
-
|
|
10
|
-
if (-not $Text) { exit 0 }
|
|
11
|
-
|
|
12
|
-
$speak = Join-Path $PSScriptRoot 'speak.ps1'
|
|
13
|
-
if (-not (Test-Path $speak)) { exit 0 }
|
|
14
|
-
|
|
15
|
-
$tmp = Join-Path $env:TEMP ('speech-prompt-' + [guid]::NewGuid().ToString('N') + '.txt')
|
|
16
|
-
try {
|
|
17
|
-
[System.IO.File]::WriteAllText($tmp, $Text, [System.Text.UTF8Encoding]::new($false))
|
|
18
|
-
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $speak -File $tmp
|
|
19
|
-
exit $LASTEXITCODE
|
|
20
|
-
} finally {
|
|
21
|
-
if (Test-Path $tmp) { Remove-Item $tmp -Force -ErrorAction SilentlyContinue }
|
|
22
|
-
}
|
|
1
|
+
# speech-prompt.ps1 — Short prompt announcement (synchronous, blocking)
|
|
2
|
+
# Use when a harness/agent needs the user's attention (a question, an approval
|
|
3
|
+
# request). Reads the text through engine/speak.ps1 and waits until it finishes,
|
|
4
|
+
# so the caller knows the announcement was actually spoken.
|
|
5
|
+
#
|
|
6
|
+
# powershell.exe -NoProfile -ExecutionPolicy Bypass -File speech-prompt.ps1 -Text "请做出选择"
|
|
7
|
+
|
|
8
|
+
param([string]$Text = '请做出选择')
|
|
9
|
+
|
|
10
|
+
if (-not $Text) { exit 0 }
|
|
11
|
+
|
|
12
|
+
$speak = Join-Path $PSScriptRoot 'speak.ps1'
|
|
13
|
+
if (-not (Test-Path $speak)) { exit 0 }
|
|
14
|
+
|
|
15
|
+
$tmp = Join-Path $env:TEMP ('speech-prompt-' + [guid]::NewGuid().ToString('N') + '.txt')
|
|
16
|
+
try {
|
|
17
|
+
[System.IO.File]::WriteAllText($tmp, $Text, [System.Text.UTF8Encoding]::new($false))
|
|
18
|
+
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $speak -File $tmp
|
|
19
|
+
exit $LASTEXITCODE
|
|
20
|
+
} finally {
|
|
21
|
+
if (Test-Path $tmp) { Remove-Item $tmp -Force -ErrorAction SilentlyContinue }
|
|
22
|
+
}
|
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
# speech-summary.ps1 — Reply summary announcement (synchronous, blocking)
|
|
2
|
-
# For harnesses with no "reply finished" event (e.g. DSH has no Stop hook): the
|
|
3
|
-
# agent calls this at the end of its final reply.
|
|
4
|
-
#
|
|
5
|
-
# powershell.exe -NoProfile -ExecutionPolicy Bypass -File speech-summary.ps1 -Text "总结文本"
|
|
6
|
-
#
|
|
7
|
-
# NOTE: keep this SYNCHRONOUS. An earlier version spawned the inner powershell
|
|
8
|
-
# asynchronously with Start-Process; DSH's sandbox blocks nested sub-process
|
|
9
|
-
# spawning, so it silently produced no audio. The synchronous call chain
|
|
10
|
-
# (summary -> speak.ps1) is the reliable path (cost: caller waits for the
|
|
11
|
-
# utterance to finish).
|
|
12
|
-
|
|
13
|
-
param([string]$Text = '')
|
|
14
|
-
|
|
15
|
-
if (-not $Text) { exit 0 }
|
|
16
|
-
|
|
17
|
-
$speak = Join-Path $PSScriptRoot 'speak.ps1'
|
|
18
|
-
if (-not (Test-Path $speak)) { exit 0 }
|
|
19
|
-
|
|
20
|
-
$tmp = Join-Path $env:TEMP ('speech-summary-' + [guid]::NewGuid().ToString('N') + '.txt')
|
|
21
|
-
try {
|
|
22
|
-
[System.IO.File]::WriteAllText($tmp, $Text, [System.Text.UTF8Encoding]::new($false))
|
|
23
|
-
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $speak -File $tmp
|
|
24
|
-
exit $LASTEXITCODE
|
|
25
|
-
} finally {
|
|
26
|
-
if (Test-Path $tmp) { Remove-Item $tmp -Force -ErrorAction SilentlyContinue }
|
|
27
|
-
}
|
|
1
|
+
# speech-summary.ps1 — Reply summary announcement (synchronous, blocking)
|
|
2
|
+
# For harnesses with no "reply finished" event (e.g. DSH has no Stop hook): the
|
|
3
|
+
# agent calls this at the end of its final reply.
|
|
4
|
+
#
|
|
5
|
+
# powershell.exe -NoProfile -ExecutionPolicy Bypass -File speech-summary.ps1 -Text "总结文本"
|
|
6
|
+
#
|
|
7
|
+
# NOTE: keep this SYNCHRONOUS. An earlier version spawned the inner powershell
|
|
8
|
+
# asynchronously with Start-Process; DSH's sandbox blocks nested sub-process
|
|
9
|
+
# spawning, so it silently produced no audio. The synchronous call chain
|
|
10
|
+
# (summary -> speak.ps1) is the reliable path (cost: caller waits for the
|
|
11
|
+
# utterance to finish).
|
|
12
|
+
|
|
13
|
+
param([string]$Text = '')
|
|
14
|
+
|
|
15
|
+
if (-not $Text) { exit 0 }
|
|
16
|
+
|
|
17
|
+
$speak = Join-Path $PSScriptRoot 'speak.ps1'
|
|
18
|
+
if (-not (Test-Path $speak)) { exit 0 }
|
|
19
|
+
|
|
20
|
+
$tmp = Join-Path $env:TEMP ('speech-summary-' + [guid]::NewGuid().ToString('N') + '.txt')
|
|
21
|
+
try {
|
|
22
|
+
[System.IO.File]::WriteAllText($tmp, $Text, [System.Text.UTF8Encoding]::new($false))
|
|
23
|
+
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $speak -File $tmp
|
|
24
|
+
exit $LASTEXITCODE
|
|
25
|
+
} finally {
|
|
26
|
+
if (Test-Path $tmp) { Remove-Item $tmp -Force -ErrorAction SilentlyContinue }
|
|
27
|
+
}
|
package/package.json
CHANGED
|
@@ -1,43 +1,46 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "dsh-speak",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Make your AI harness speak — verified voice announcements for DSH and other AI coding harnesses (Windows SAPI5 +
|
|
5
|
-
"main": "adapters/dsh/speech-hook.js",
|
|
6
|
-
"files": [
|
|
7
|
-
"adapters/dsh/speech-hook.js",
|
|
8
|
-
"adapters/dsh/install.ps1",
|
|
9
|
-
"engine/",
|
|
10
|
-
"docs/",
|
|
11
|
-
"README.md",
|
|
12
|
-
"README.zh-CN.md",
|
|
13
|
-
"LICENSE"
|
|
14
|
-
],
|
|
15
|
-
"keywords": [
|
|
16
|
-
"dsh",
|
|
17
|
-
"dsh-plugin",
|
|
18
|
-
"deepseek-harness",
|
|
19
|
-
"tts",
|
|
20
|
-
"text-to-speech",
|
|
21
|
-
"voice",
|
|
22
|
-
"speech",
|
|
23
|
-
"voice-announcement",
|
|
24
|
-
"sapi5",
|
|
25
|
-
"windows",
|
|
26
|
-
"powershell"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
},
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-speak",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "Make your AI harness speak — verified voice announcements for DSH and other AI coding harnesses (Windows SAPI5 + macOS system voices)",
|
|
5
|
+
"main": "adapters/dsh/speech-hook.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"adapters/dsh/speech-hook.js",
|
|
8
|
+
"adapters/dsh/install.ps1",
|
|
9
|
+
"engine/",
|
|
10
|
+
"docs/",
|
|
11
|
+
"README.md",
|
|
12
|
+
"README.zh-CN.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"keywords": [
|
|
16
|
+
"dsh",
|
|
17
|
+
"dsh-plugin",
|
|
18
|
+
"deepseek-harness",
|
|
19
|
+
"tts",
|
|
20
|
+
"text-to-speech",
|
|
21
|
+
"voice",
|
|
22
|
+
"speech",
|
|
23
|
+
"voice-announcement",
|
|
24
|
+
"sapi5",
|
|
25
|
+
"windows",
|
|
26
|
+
"powershell",
|
|
27
|
+
"macos",
|
|
28
|
+
"darwin",
|
|
29
|
+
"say"
|
|
30
|
+
],
|
|
31
|
+
"scripts": {
|
|
32
|
+
"prepublishOnly": "node --check adapters/dsh/speech-hook.js"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18"
|
|
36
|
+
},
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/Alan2Z/dsh-speak.git"
|
|
40
|
+
},
|
|
41
|
+
"author": "Alan2Z",
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"registry": "https://registry.npmjs.org"
|
|
45
|
+
}
|
|
46
|
+
}
|