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.
@@ -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), or Alibaba
9
- * Cloud Model Studio CosyVoice (cosyvoice.ts). When the service cannot
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 key = `${this.config.voice}|${this.config.speed}|${text}`;
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.1.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