@goodandready/dsh-tts 0.4.17 → 0.4.19
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/CHANGELOG.md +15 -0
- package/lib/client-src/60-card-dock.js +19 -0
- package/lib/client.js +19 -0
- package/lib/engines/kokoro.js +0 -23
- package/lib/routes.js +8 -0
- package/package.json +1 -1
- package/scripts/publish-github.sh +184 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
Notable changes to `@goodandready/dsh-tts`.
|
|
4
4
|
|
|
5
|
+
## 0.4.19
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- **Integration and cache stats reads require a trusted settings request.** `GET /dsh-tts/integrations` and `GET /dsh-tts/stats` now return 403 for a cross-site caller, the same rule as the other settings routes.
|
|
9
|
+
- **Removed the unused Kokoro WAV encoder.** Local Kokoro inference is not bundled, and nothing called `pcmFloat32ToWav`.
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **Settings reachable again on the plugin's own page**: the current DSH core
|
|
14
|
+
(0.1.6-alpha.2) renders a plugin's configuration page only for entries registered
|
|
15
|
+
in the plugin-list seat `plugins.item`. `TtsRowConfig` is now registered there too
|
|
16
|
+
(`id: 'dsh-tts'`, order 60, static label); the row seat and the legacy
|
|
17
|
+
`settings.plugin.item` card stay as fallbacks. Sources edited in `lib/client-src`,
|
|
18
|
+
`lib/client.js` rebuilt.
|
|
19
|
+
|
|
5
20
|
## 0.4.17
|
|
6
21
|
|
|
7
22
|
### Added
|
|
@@ -173,6 +173,25 @@
|
|
|
173
173
|
} catch (rowSeatUnavailable) {
|
|
174
174
|
console.warn('[dsh-tts] row seat plugins.row.config not available:', rowSeatUnavailable && rowSeatUnavailable.message)
|
|
175
175
|
}
|
|
176
|
+
// Plugin-list seat (`plugins.item`): the seat the current core (0.1.6-alpha.2)
|
|
177
|
+
// renders as the plugin's own page with its configuration. The label is a static
|
|
178
|
+
// string on purpose — it is resolved while the page renders, and a locale lookup
|
|
179
|
+
// there would take the whole client batch down with it.
|
|
180
|
+
try {
|
|
181
|
+
ctx.slots.inject('plugins.item', () => ctx.slots.register(
|
|
182
|
+
{
|
|
183
|
+
name: 'plugins.item',
|
|
184
|
+
id: ROW_ID,
|
|
185
|
+
order: 60,
|
|
186
|
+
label: () => 'Text to Speech',
|
|
187
|
+
locale: NS,
|
|
188
|
+
inject: () => ({ ctx: ctx }),
|
|
189
|
+
},
|
|
190
|
+
TtsRowConfig,
|
|
191
|
+
))
|
|
192
|
+
} catch (itemSeatUnavailable) {
|
|
193
|
+
console.warn('[dsh-tts] list seat plugins.item not available:', itemSeatUnavailable && itemSeatUnavailable.message)
|
|
194
|
+
}
|
|
176
195
|
// Fallback for hosts that render the older plugin-settings tab card. The tab
|
|
177
196
|
// looks up a slot by entryKey equal to the namespace name: key must equal NS
|
|
178
197
|
// or the card never appears, with no log error.
|
package/lib/client.js
CHANGED
|
@@ -1936,6 +1936,25 @@ async function pollPending() {
|
|
|
1936
1936
|
} catch (rowSeatUnavailable) {
|
|
1937
1937
|
console.warn('[dsh-tts] row seat plugins.row.config not available:', rowSeatUnavailable && rowSeatUnavailable.message)
|
|
1938
1938
|
}
|
|
1939
|
+
// Plugin-list seat (`plugins.item`): the seat the current core (0.1.6-alpha.2)
|
|
1940
|
+
// renders as the plugin's own page with its configuration. The label is a static
|
|
1941
|
+
// string on purpose — it is resolved while the page renders, and a locale lookup
|
|
1942
|
+
// there would take the whole client batch down with it.
|
|
1943
|
+
try {
|
|
1944
|
+
ctx.slots.inject('plugins.item', () => ctx.slots.register(
|
|
1945
|
+
{
|
|
1946
|
+
name: 'plugins.item',
|
|
1947
|
+
id: ROW_ID,
|
|
1948
|
+
order: 60,
|
|
1949
|
+
label: () => 'Text to Speech',
|
|
1950
|
+
locale: NS,
|
|
1951
|
+
inject: () => ({ ctx: ctx }),
|
|
1952
|
+
},
|
|
1953
|
+
TtsRowConfig,
|
|
1954
|
+
))
|
|
1955
|
+
} catch (itemSeatUnavailable) {
|
|
1956
|
+
console.warn('[dsh-tts] list seat plugins.item not available:', itemSeatUnavailable && itemSeatUnavailable.message)
|
|
1957
|
+
}
|
|
1939
1958
|
// Fallback for hosts that render the older plugin-settings tab card. The tab
|
|
1940
1959
|
// looks up a slot by entryKey equal to the namespace name: key must equal NS
|
|
1941
1960
|
// or the card never appears, with no log error.
|
package/lib/engines/kokoro.js
CHANGED
|
@@ -37,26 +37,3 @@ export class KokoroEngine {
|
|
|
37
37
|
throw new Error('Kokoro synthesis unavailable')
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
-
|
|
41
|
-
export function pcmFloat32ToWav(samples, sampleRate = 24000) {
|
|
42
|
-
const numSamples = samples.length
|
|
43
|
-
const buffer = Buffer.alloc(44 + numSamples * 2)
|
|
44
|
-
buffer.write('RIFF', 0)
|
|
45
|
-
buffer.writeUInt32LE(36 + numSamples * 2, 4)
|
|
46
|
-
buffer.write('WAVE', 8)
|
|
47
|
-
buffer.write('fmt ', 12)
|
|
48
|
-
buffer.writeUInt32LE(16, 16)
|
|
49
|
-
buffer.writeUInt16LE(1, 20)
|
|
50
|
-
buffer.writeUInt16LE(1, 22)
|
|
51
|
-
buffer.writeUInt32LE(sampleRate, 24)
|
|
52
|
-
buffer.writeUInt32LE(sampleRate * 2, 28)
|
|
53
|
-
buffer.writeUInt16LE(2, 32)
|
|
54
|
-
buffer.writeUInt16LE(16, 34)
|
|
55
|
-
buffer.write('data', 36)
|
|
56
|
-
buffer.writeUInt32LE(numSamples * 2, 40)
|
|
57
|
-
for (let i = 0; i < numSamples; i++) {
|
|
58
|
-
const s = Math.max(-1, Math.min(1, samples[i]))
|
|
59
|
-
buffer.writeInt16LE(s < 0 ? s * 0x8000 : s * 0x7fff, 44 + i * 2)
|
|
60
|
-
}
|
|
61
|
-
return buffer
|
|
62
|
-
}
|
package/lib/routes.js
CHANGED
|
@@ -161,6 +161,10 @@ export function registerHttpRoutes(ctx, api) {
|
|
|
161
161
|
path: '/dsh-tts/integrations',
|
|
162
162
|
handler: async (req, res) => {
|
|
163
163
|
if (req.method !== 'GET') { writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET only' } }); return }
|
|
164
|
+
if (!isTrustedSettingsRequest(req)) {
|
|
165
|
+
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
166
|
+
return
|
|
167
|
+
}
|
|
164
168
|
let voiceInstalled = false
|
|
165
169
|
let messengerInstalled = false
|
|
166
170
|
|
|
@@ -248,6 +252,10 @@ export function registerHttpRoutes(ctx, api) {
|
|
|
248
252
|
path: '/dsh-tts/stats',
|
|
249
253
|
handler: async (req, res) => {
|
|
250
254
|
if (req.method === 'GET') {
|
|
255
|
+
if (!isTrustedSettingsRequest(req)) {
|
|
256
|
+
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
257
|
+
return
|
|
258
|
+
}
|
|
251
259
|
writeJson(res, 200, { ok: true, total: stats.total, cacheHits: stats.cacheHits, errors: stats.errors, providers: stats.providers, breaker: typeof breakerSnapshot === 'function' ? breakerSnapshot() : [] })
|
|
252
260
|
return
|
|
253
261
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-tts",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.19",
|
|
4
4
|
"description": "Text-to-speech for DeepSeek Harness: speak agent replies in the Web UI with a provider fallback chain (OpenAI, ElevenLabs, Google, Azure, Groq, Deepgram, OpenRouter, Edge, Piper, eSpeak).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Sanitized publication to a mirror remote (GitHub).
|
|
4
|
+
#
|
|
5
|
+
# Gitea is the source of truth: branches, tags, issues, PRs and the full history,
|
|
6
|
+
# including build sources and internal tooling. A mirror is a *product* channel, so
|
|
7
|
+
# the published tree carries only product files — never agent instructions, plans,
|
|
8
|
+
# build sources, caches or infrastructure documents.
|
|
9
|
+
#
|
|
10
|
+
# Usage:
|
|
11
|
+
# ./scripts/publish-github.sh --check # read-only: what would go
|
|
12
|
+
# ./scripts/publish-github.sh --push <gitea-ref> # publish a sanitized tree
|
|
13
|
+
#
|
|
14
|
+
# The allowlist is the package's own `files` field (i.e. exactly what npm ships)
|
|
15
|
+
# plus the repository files a reader needs: .gitignore, LICENSE, README*.md,
|
|
16
|
+
# CHANGELOG.md, package.json, cordis.patch.yml.
|
|
17
|
+
#
|
|
18
|
+
# What --push does:
|
|
19
|
+
# 1. builds that tree from <gitea-ref>;
|
|
20
|
+
# 2. refuses to publish when a forbidden path or a required product file is missing;
|
|
21
|
+
# 3. creates ONE commit on top of the mirror branch carrying the tree and pushes it
|
|
22
|
+
# as a normal fast-forward — never a force;
|
|
23
|
+
# 4. prints the Gitea SHA -> mirror SHA correspondence for the release notes.
|
|
24
|
+
#
|
|
25
|
+
# Rewriting mirror history is a destructive operation on a published channel and is
|
|
26
|
+
# deliberately not implemented. Removing a file from *past* mirror history requires
|
|
27
|
+
# recreating the mirror repository and an explicit owner decision.
|
|
28
|
+
#
|
|
29
|
+
# Environment: MIRROR_REMOTE (default github), MIRROR_BRANCH (default main).
|
|
30
|
+
set -euo pipefail
|
|
31
|
+
|
|
32
|
+
MIRROR_REMOTE="${MIRROR_REMOTE:-github}"
|
|
33
|
+
MIRROR_BRANCH="${MIRROR_BRANCH:-main}"
|
|
34
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
35
|
+
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
36
|
+
|
|
37
|
+
# Repository files a reader needs, on top of the package's npm contents.
|
|
38
|
+
EXTRA_ALLOW=(
|
|
39
|
+
".gitignore"
|
|
40
|
+
"LICENSE"
|
|
41
|
+
"README.md"
|
|
42
|
+
"README.ru.md"
|
|
43
|
+
"README.zh.md"
|
|
44
|
+
"CHANGELOG.md"
|
|
45
|
+
"package.json"
|
|
46
|
+
"cordis.patch.yml"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Never publish these, even if they sneak into the allowlist.
|
|
50
|
+
FORBIDDEN=(
|
|
51
|
+
"AGENTS.md"
|
|
52
|
+
"index.md"
|
|
53
|
+
"deploy.sh"
|
|
54
|
+
".gitea"
|
|
55
|
+
".worktrees"
|
|
56
|
+
".planning"
|
|
57
|
+
"__pycache__"
|
|
58
|
+
".ruff_cache"
|
|
59
|
+
".venv"
|
|
60
|
+
"node_modules"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
mode=""
|
|
64
|
+
gitea_ref=""
|
|
65
|
+
for arg in "$@"; do
|
|
66
|
+
case "$arg" in
|
|
67
|
+
--check) mode="check" ;;
|
|
68
|
+
--push) mode="push" ;;
|
|
69
|
+
-*) echo "unknown option: $arg" >&2; exit 2 ;;
|
|
70
|
+
*) gitea_ref="$arg" ;;
|
|
71
|
+
esac
|
|
72
|
+
done
|
|
73
|
+
|
|
74
|
+
if [ -z "$mode" ]; then
|
|
75
|
+
echo "usage: ./scripts/publish-github.sh --check | --push <gitea-ref>" >&2
|
|
76
|
+
exit 2
|
|
77
|
+
fi
|
|
78
|
+
if [ "$mode" = "push" ] && [ -z "$gitea_ref" ]; then
|
|
79
|
+
echo "usage: ./scripts/publish-github.sh --push <gitea-ref>" >&2
|
|
80
|
+
exit 2
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
cd "$REPO_DIR"
|
|
84
|
+
|
|
85
|
+
# The allowlist comes from the package manifest of the ref being published.
|
|
86
|
+
ALLOW=("${EXTRA_ALLOW[@]}")
|
|
87
|
+
while IFS= read -r entry; do
|
|
88
|
+
[ -n "$entry" ] && ALLOW+=("${entry%/}" )
|
|
89
|
+
done < <(git show "${gitea_ref:-origin/main}:package.json" | node -e '
|
|
90
|
+
let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{
|
|
91
|
+
try{const p=JSON.parse(s);for(const f of (p.files||[])) console.log(f.replace(/\/\*\*$/,""));}catch(e){}
|
|
92
|
+
});')
|
|
93
|
+
|
|
94
|
+
is_allowed() {
|
|
95
|
+
local path="$1" entry
|
|
96
|
+
for entry in "${ALLOW[@]}"; do
|
|
97
|
+
if [ "$path" = "$entry" ]; then return 0; fi
|
|
98
|
+
case "$path" in "$entry"/*) return 0 ;; esac
|
|
99
|
+
done
|
|
100
|
+
return 1
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
is_forbidden() {
|
|
104
|
+
local path="$1" entry
|
|
105
|
+
for entry in "${FORBIDDEN[@]}"; do
|
|
106
|
+
if [ "$path" = "$entry" ]; then return 0; fi
|
|
107
|
+
case "$path" in "$entry"/*) return 0 ;; esac
|
|
108
|
+
done
|
|
109
|
+
return 1
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
list_tree() {
|
|
113
|
+
local ref="$1" path
|
|
114
|
+
git ls-tree -r --name-only "$ref" | while IFS= read -r path; do
|
|
115
|
+
if is_forbidden "$path"; then continue; fi
|
|
116
|
+
if is_allowed "$path"; then echo "$path"; fi
|
|
117
|
+
done | LC_ALL=C sort
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
echo "repository : $REPO_DIR"
|
|
121
|
+
echo "mirror : $MIRROR_REMOTE/$MIRROR_BRANCH"
|
|
122
|
+
echo
|
|
123
|
+
|
|
124
|
+
if [ "$mode" = "check" ]; then
|
|
125
|
+
ref="${gitea_ref:-origin/main}"
|
|
126
|
+
echo "WILL BE PUBLISHED (sanitized tree of $ref):"
|
|
127
|
+
list_tree "$ref" | sed 's/^/ /'
|
|
128
|
+
echo
|
|
129
|
+
echo "WILL BE DROPPED (not product):"
|
|
130
|
+
git ls-tree -r --name-only "$ref" | while IFS= read -r path; do
|
|
131
|
+
if is_allowed "$path" && ! is_forbidden "$path"; then continue; fi
|
|
132
|
+
echo " $path"
|
|
133
|
+
done
|
|
134
|
+
exit 0
|
|
135
|
+
fi
|
|
136
|
+
|
|
137
|
+
source_sha="$(git rev-parse "$gitea_ref")"
|
|
138
|
+
mirror_sha="$(git rev-parse "$MIRROR_REMOTE/$MIRROR_BRANCH")"
|
|
139
|
+
echo "gitea ref : $gitea_ref = $source_sha"
|
|
140
|
+
echo "mirror head: $MIRROR_REMOTE/$MIRROR_BRANCH = $mirror_sha"
|
|
141
|
+
echo
|
|
142
|
+
|
|
143
|
+
sanitized_list="$(mktemp)"
|
|
144
|
+
tmp_index="$(mktemp)"
|
|
145
|
+
trap 'rm -f "$sanitized_list" "$tmp_index"' EXIT
|
|
146
|
+
list_tree "$gitea_ref" > "$sanitized_list"
|
|
147
|
+
|
|
148
|
+
if list_tree "$gitea_ref" | while IFS= read -r path; do
|
|
149
|
+
if is_forbidden "$path"; then echo "FORBIDDEN $path"; fi
|
|
150
|
+
done | grep -q FORBIDDEN; then
|
|
151
|
+
echo "refusing to publish: the sanitized list contains a forbidden path" >&2
|
|
152
|
+
exit 3
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
for required in package.json README.md; do
|
|
156
|
+
if ! grep -qx "$required" "$sanitized_list"; then
|
|
157
|
+
echo "refusing to publish: required product file missing: $required" >&2
|
|
158
|
+
exit 4
|
|
159
|
+
fi
|
|
160
|
+
done
|
|
161
|
+
|
|
162
|
+
echo "files to publish: $(wc -l < "$sanitized_list")"
|
|
163
|
+
sed 's/^/ /' "$sanitized_list"
|
|
164
|
+
echo
|
|
165
|
+
|
|
166
|
+
# A temporary index is used instead of `git mktree`: mktree accepts only single-level
|
|
167
|
+
# entries and refuses nested paths such as lib/client.js, while `update-index
|
|
168
|
+
# --cacheinfo` builds the intermediate trees for us.
|
|
169
|
+
GIT_INDEX_FILE="$tmp_index" git read-tree --empty
|
|
170
|
+
while IFS= read -r path; do
|
|
171
|
+
blob="$(git rev-parse "$gitea_ref:$path")"
|
|
172
|
+
GIT_INDEX_FILE="$tmp_index" git update-index --add --cacheinfo "100644,$blob,$path"
|
|
173
|
+
done < "$sanitized_list"
|
|
174
|
+
new_tree="$(GIT_INDEX_FILE="$tmp_index" git write-tree)"
|
|
175
|
+
|
|
176
|
+
new_commit="$(git commit-tree "$new_tree" -p "$mirror_sha" -m "chore(publish): sanitized tree from ${source_sha:0:8}")"
|
|
177
|
+
echo "new mirror commit: $new_commit"
|
|
178
|
+
|
|
179
|
+
git push "$MIRROR_REMOTE" "$new_commit:refs/heads/$MIRROR_BRANCH"
|
|
180
|
+
|
|
181
|
+
echo
|
|
182
|
+
echo "correspondence to record in the release notes:"
|
|
183
|
+
echo " gitea $source_sha"
|
|
184
|
+
echo " mirror $new_commit"
|