dsh-her 0.1.0 → 0.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/lib/client.js +862 -506
- package/lib/core/character.js +1 -1
- package/lib/core/index.js +3 -1
- package/lib/core/sounds.js +31 -0
- package/lib/core/spoken-numbers.js +90 -0
- package/lib/core/voices.js +31 -1
- package/lib/dsh/api.js +53 -0
- package/lib/dsh/plugin.js +129 -14
- package/lib/types/core/character.d.ts +1 -1
- package/lib/types/core/index.d.ts +3 -1
- package/lib/types/core/sounds.d.ts +21 -0
- package/lib/types/core/spoken-numbers.d.ts +11 -0
- package/lib/types/core/voices.d.ts +18 -2
- package/lib/types/dsh/api.d.ts +37 -1
- package/lib/types/dsh/plugin.d.ts +2 -0
- package/lib/types/voice/ming-model.d.ts +62 -0
- package/lib/types/voice/ming-voices.d.ts +75 -0
- package/lib/types/voice/ming.d.ts +103 -0
- package/lib/types/voice/speech-queue.d.ts +4 -2
- package/lib/types/voice/uv.d.ts +12 -0
- package/lib/types/voice/voice.d.ts +14 -6
- package/lib/voice/edge.js +4 -48
- package/lib/voice/ming-model.js +30 -0
- package/lib/voice/ming-voices.js +181 -0
- package/lib/voice/ming.js +335 -0
- package/lib/voice/speech-queue.js +5 -3
- package/lib/voice/uv.js +60 -0
- package/lib/voice/voice.js +46 -10
- package/package.json +2 -1
- package/python/ming-requirements.in +1 -0
- package/python/ming-requirements.txt +887 -0
- package/python/ming_worker.py +77 -0
package/lib/voice/voice.js
CHANGED
|
@@ -5,20 +5,22 @@
|
|
|
5
5
|
* process, engine.ts). Its model is downloaded in the background on first
|
|
6
6
|
* use; until then she only reads what you type.
|
|
7
7
|
* - Speaking: the service of the voice she uses (core/voices.ts): Microsoft
|
|
8
|
-
* Edge voices through the edge-tts tool (edge.ts, the default),
|
|
9
|
-
* Cloud Model Studio CosyVoice (cosyvoice.ts)
|
|
8
|
+
* Edge voices through the edge-tts tool (edge.ts, the default), Alibaba
|
|
9
|
+
* Cloud Model Studio CosyVoice (cosyvoice.ts), or Ming on this computer
|
|
10
|
+
* (ming.ts; built-in voices or her own, with her moods). When the service cannot
|
|
10
11
|
* speak (tool not installed yet, no key, no quota, offline), a sentence goes
|
|
11
12
|
* out without audio and shows as text; a key added later is picked up
|
|
12
13
|
* without restarting.
|
|
13
14
|
* - Cache: short sentences ("嗯。" "好呀。") are kept, so the most common
|
|
14
15
|
* answers start at once.
|
|
15
16
|
*/
|
|
16
|
-
import { serviceOf, VOICE_MODEL } from "../core/index.js";
|
|
17
|
+
import { mingSpeech, serviceOf, spokenNumbers, VOICE_MODEL } from "../core/index.js";
|
|
17
18
|
import { LocalVoice } from "./engine.js";
|
|
18
19
|
import { CloudAccountError, CosyVoice } from "./cosyvoice.js";
|
|
19
20
|
import { EdgeVoice } from "./edge.js";
|
|
20
21
|
import { readCloudKey } from "./cloud-key.js";
|
|
21
22
|
import { fetchModel, missingModels, modelPath, MODELS } from "./models.js";
|
|
23
|
+
import { MING_STEPS } from "./ming-voices.js";
|
|
22
24
|
const CACHE_MAX_CHARS = 8;
|
|
23
25
|
const CACHE_ENTRIES = 64;
|
|
24
26
|
/** After "no key" or "no quota", try the cloud again at most this often (a key added later is picked up). */
|
|
@@ -31,18 +33,20 @@ export class Voice {
|
|
|
31
33
|
keyCheckedAt = 0;
|
|
32
34
|
cloud;
|
|
33
35
|
edge;
|
|
36
|
+
ming;
|
|
34
37
|
cache = new Map();
|
|
35
38
|
modelsDir;
|
|
36
39
|
config;
|
|
37
40
|
logger;
|
|
38
|
-
/** `edgeDir`: where dsh-her installs its own edge-tts if none is found. */
|
|
39
|
-
constructor(modelsDir, edgeDir, config, logger) {
|
|
41
|
+
/** `edgeDir`: where dsh-her installs its own edge-tts if none is found. `ming`: the local Ming voice and her own voices. */
|
|
42
|
+
constructor(modelsDir, edgeDir, config, logger, ming) {
|
|
40
43
|
this.modelsDir = modelsDir;
|
|
41
44
|
this.config = config;
|
|
42
45
|
this.logger = logger;
|
|
43
46
|
this.speakingState = config.enabled ? 'checking' : 'off';
|
|
44
47
|
this.cloud = new CosyVoice(VOICE_MODEL, readCloudKey);
|
|
45
48
|
this.edge = new EdgeVoice(edgeDir);
|
|
49
|
+
this.ming = ming;
|
|
46
50
|
}
|
|
47
51
|
/** Listening. */
|
|
48
52
|
get status() {
|
|
@@ -50,6 +54,18 @@ export class Voice {
|
|
|
50
54
|
}
|
|
51
55
|
get speaking() {
|
|
52
56
|
const service = serviceOf(this.config.voice);
|
|
57
|
+
if (service === 'ming' && this.config.enabled && this.ming !== undefined) {
|
|
58
|
+
// Ming tells its own state: installing, loading, stopped to save memory …
|
|
59
|
+
const ming = this.ming.voice.status;
|
|
60
|
+
if (ming.state === 'installing')
|
|
61
|
+
return { status: 'installing', service };
|
|
62
|
+
if (ming.state === 'loading')
|
|
63
|
+
return { status: 'checking', service };
|
|
64
|
+
if (ming.state === 'absent' || ming.state === 'unsupported')
|
|
65
|
+
return { status: 'failed', service, error: ming.error ?? '本机 Ming 还没安装' };
|
|
66
|
+
if (ming.state === 'failed')
|
|
67
|
+
return { status: 'failed', service, error: ming.error ?? '本机 Ming 出错了' };
|
|
68
|
+
}
|
|
53
69
|
return this.speakingError === undefined ? { status: this.speakingState, service } : { status: this.speakingState, service, error: this.speakingError };
|
|
54
70
|
}
|
|
55
71
|
/** Check the cloud voice and get recognition ready. Never throws; the statuses say how it went. */
|
|
@@ -79,6 +95,8 @@ export class Voice {
|
|
|
79
95
|
warm() {
|
|
80
96
|
if (serviceOf(this.config.voice) === 'cosyvoice' && (this.speakingState === 'ready' || this.speakingState === 'failed'))
|
|
81
97
|
this.cloud.warm();
|
|
98
|
+
if (serviceOf(this.config.voice) === 'ming')
|
|
99
|
+
this.ming?.voice.warm();
|
|
82
100
|
}
|
|
83
101
|
/** Change her voice and speed (from the "她" page); takes effect with the next sentence. */
|
|
84
102
|
setVoice(voice, speed) {
|
|
@@ -96,18 +114,19 @@ export class Voice {
|
|
|
96
114
|
throw new Error('她的声音已关闭(插件设置里 voice.enabled 是 false)');
|
|
97
115
|
return this.speak(text, voice, speed);
|
|
98
116
|
}
|
|
99
|
-
/** Her sentence as audio, or undefined when she cannot speak right now (it shows as text). */
|
|
100
|
-
async synthesize(text) {
|
|
117
|
+
/** Her sentence as audio, or undefined when she cannot speak right now (it shows as text). Its emotion colours a Ming voice. */
|
|
118
|
+
async synthesize(text, cues = []) {
|
|
101
119
|
if (!this.config.enabled || text === '')
|
|
102
120
|
return undefined;
|
|
103
121
|
if ((this.speakingState === 'no-key' || this.speakingState === 'no-quota') && Date.now() - this.keyCheckedAt < KEY_RECHECK_MS)
|
|
104
122
|
return undefined;
|
|
105
|
-
const
|
|
123
|
+
const emotion = serviceOf(this.config.voice) === 'ming' ? cues.find(c => c.kind === 'emotion')?.name : undefined;
|
|
124
|
+
const key = `${this.config.voice}|${this.config.speed}|${emotion ?? ''}|${text}`;
|
|
106
125
|
const cached = this.cache.get(key);
|
|
107
126
|
if (cached !== undefined)
|
|
108
127
|
return cached;
|
|
109
128
|
try {
|
|
110
|
-
const result = await this.speak(text);
|
|
129
|
+
const result = await this.speak(text, this.config.voice, this.config.speed, emotion);
|
|
111
130
|
if ([...text].length <= CACHE_MAX_CHARS) {
|
|
112
131
|
this.cache.set(key, result);
|
|
113
132
|
if (this.cache.size > CACHE_ENTRIES)
|
|
@@ -134,11 +153,28 @@ export class Voice {
|
|
|
134
153
|
this.asr?.stop();
|
|
135
154
|
this.asr = undefined;
|
|
136
155
|
this.cloud.close();
|
|
156
|
+
this.ming?.voice.close();
|
|
137
157
|
if (this.listening !== 'off')
|
|
138
158
|
this.listening = 'failed';
|
|
139
159
|
}
|
|
140
|
-
async speak(text, voice = this.config.voice, speed = this.config.speed) {
|
|
160
|
+
async speak(text, voice = this.config.voice, speed = this.config.speed, emotion) {
|
|
141
161
|
try {
|
|
162
|
+
if (serviceOf(voice) === 'ming') {
|
|
163
|
+
if (this.ming === undefined)
|
|
164
|
+
throw new Error('本机 Ming 不可用');
|
|
165
|
+
const settings = this.ming.own.settings();
|
|
166
|
+
const how = mingSpeech(voice, settings.moods ? emotion : undefined, speed);
|
|
167
|
+
const ref = how.own === undefined ? undefined : this.ming.own.ref(how.own);
|
|
168
|
+
if (how.own !== undefined && ref === undefined)
|
|
169
|
+
throw new Error('这个声音已经删掉了,换一个吧');
|
|
170
|
+
const audio = await this.ming.voice.synthesize({
|
|
171
|
+
text: spokenNumbers(text), steps: MING_STEPS[settings.quality],
|
|
172
|
+
...(how.voice === undefined ? {} : { voice: how.voice }), ...(ref === undefined ? {} : { ref }),
|
|
173
|
+
...(how.emotion === undefined ? {} : { emotion: how.emotion }), ...(how.speed === undefined ? {} : { speed: how.speed }),
|
|
174
|
+
});
|
|
175
|
+
this.mark('ready');
|
|
176
|
+
return { audio: audio.audio, durationMs: audio.durationMs, format: 'wav' };
|
|
177
|
+
}
|
|
142
178
|
if (serviceOf(voice) === 'edge') {
|
|
143
179
|
if (this.edge.status.status !== 'ready')
|
|
144
180
|
this.mark('installing');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-her",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "A desktop companion for DeepSeek Harness (dsh): she lives on your real desktop (on top of your windows or inside the wallpaper), talks with you by voice and remembers you. · DSH 桌面女友:住在你真正的桌面上,能语音聊天、记得你。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"author": "AIcivilization",
|
|
24
24
|
"files": [
|
|
25
25
|
"lib",
|
|
26
|
+
"python",
|
|
26
27
|
"cordis.patch.yml",
|
|
27
28
|
"README.md",
|
|
28
29
|
"examples"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mlx-audio==0.6.3
|