@memef1f1y/dsh-plugin-tts 0.6.5 → 0.6.7

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.
Files changed (37) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.yml +51 -51
  2. package/.github/ISSUE_TEMPLATE/config.yml +2 -2
  3. package/.github/ISSUE_TEMPLATE/feature_request.yml +28 -28
  4. package/.github/pull_request_template.md +7 -7
  5. package/.github/workflows/contrib-triage.yml +66 -66
  6. package/.github/workflows/linked-issue.yml +21 -21
  7. package/.github/workflows/publish.yml +63 -0
  8. package/.github/workflows/stale.yml +30 -30
  9. package/.github/workflows/test.yml +33 -33
  10. package/.github/workflows/welcome.yml +22 -22
  11. package/ANALYSIS.md +205 -205
  12. package/PR-REVIEW.md +74 -74
  13. package/README.md +335 -335
  14. package/TODO.md +34 -34
  15. package/docs/HOST-CONTRACT.md +36 -36
  16. package/docs/RVC-GUIDE.md +390 -390
  17. package/docs/USER-GUIDE.md +389 -389
  18. package/docs/acceptance-checklist.md +147 -147
  19. package/docs/adaptive-chunked-playback.md +194 -194
  20. package/docs/macos-test-prompt.md +230 -230
  21. package/docs/windows-gpu-verify-prompt.md +151 -151
  22. package/lib/client.js +7164 -7164
  23. package/lib/index.mjs +3374 -3374
  24. package/package.json +51 -51
  25. package/rvc-server.py +564 -564
  26. package/tests/README.md +45 -45
  27. package/tests/client-load.mjs +406 -406
  28. package/tests/e2e-compact-index.mjs +86 -86
  29. package/tests/e2e-index-ab.mjs +138 -138
  30. package/tests/e2e-real-rvc.mjs +122 -122
  31. package/tests/i18n-keys.mjs +82 -82
  32. package/tests/package-runtime-patch.mjs +88 -88
  33. package/tests/rvc-server-live.mjs +230 -230
  34. package/tests/smoke.mjs +1284 -1284
  35. package/tools/gen-ui-preview.mjs +174 -174
  36. package/tools/mac-verify.sh +158 -158
  37. package/tools/package-runtime.py +214 -214
@@ -1,174 +1,174 @@
1
- #!/usr/bin/env node
2
- // Generates a standalone UI preview page (tools/ui-preview.html) that loads the
3
- // REAL lib/client.js in a browser, renders the injected components into actual
4
- // DOM using a lightweight React-agnostic renderer, and lets you screenshot it
5
- // for visual verification of the plugin UI (P2-P5).
6
- //
7
- // node tools/gen-ui-preview.mjs [out.html]
8
- // Then open out.html in a browser (or headless screenshot):
9
- // npx playwright screenshot tools/ui-preview.html shot.png (example)
10
- //
11
- // Note: this is a visual-verification aid only, NOT shipped into the plugin.
12
- import { readFileSync, writeFileSync } from 'node:fs';
13
- import { fileURLToPath } from 'node:url';
14
- import path from 'node:path';
15
-
16
- const here = path.dirname(fileURLToPath(import.meta.url));
17
- const clientSrc = readFileSync(path.resolve(here, '..', 'lib', 'client.js'), 'utf8')
18
- .replace(/<\/script/gi, '<\\/script');
19
-
20
- const out = path.resolve(here, process.argv[2] || 'ui-preview.html');
21
-
22
- const html = `<!doctype html>
23
- <html lang="zh-CN"><head><meta charset="utf-8"><title>dsh-plugin-tts UI preview</title>
24
- <style>
25
- :root{
26
- /* minimal dsh web theme tokens so --dsw-* styles resolve */
27
- --dsw-alias-label-primary:#eceff3;
28
- --dsw-alias-label-secondary:#c3c9d1;
29
- --dsw-alias-label-tertiary:#7f8894;
30
- --dsw-alias-label-dimmed:#5a6472;
31
- --dsw-alias-label-inverse:#0f1115;
32
- --dsw-alias-label-error:#e5484d;
33
- --dsw-alias-interactive-bg-hover:rgba(255,255,255,.06);
34
- --dsw-alias-interactive-bg-primary:#3b82f6;
35
- --dsw-alias-bg-layer-2:#161a20;
36
- --dsw-alias-bg-layer-3:#1c2128;
37
- --dsw-alias-border-secondary:rgba(255,255,255,.08);
38
- --dsw-alias-border-l2:#2a313b;
39
- --dsw-alias-brand-primary:#3b82f6;
40
- }
41
- body{background:#0d0f13;color:var(--dsw-alias-label-primary);font-family:system-ui,-apple-system,'PingFang SC','Microsoft YaHei',sans-serif;margin:0;padding:24px}
42
- h2{font-size:13px;color:var(--dsw-alias-label-tertiary);font-weight:600;margin:18px 0 8px;text-transform:uppercase;letter-spacing:.5px}
43
- .card{background:#14181d;border:1px solid var(--dsw-alias-border-secondary);border-radius:12px;padding:14px;margin-bottom:6px;max-width:640px}
44
- .toolrow{display:flex;align-items:center;gap:6px;padding:6px 8px;background:#15191f;border:1px solid var(--dsw-alias-border-secondary);border-radius:10px;margin-bottom:8px}
45
- .toolrow::before{content:"▍";color:var(--dsw-alias-label-dimmed);font-size:13px}
46
- pre.tip{color:var(--dsw-alias-label-tertiary);font-size:11px;margin:2px 0 0}
47
- </style></head>
48
- <body>
49
- <h2>1) auto-read pill (off)</h2>
50
- <div class="toolrow"><div class="slot-input" data-state="off"></div></div>
51
- <h2>2) auto-read pill (on)</h2>
52
- <div class="toolrow"><div class="slot-input" data-state="on"></div></div>
53
- <h2>3) voice settings panel — Edge provider</h2>
54
- <div class="card"><div class="slot-settings" data-provider="edge"></div></div>
55
- <h2>4) voice settings panel — RVC provider (onboarding)</h2>
56
- <div class="card"><div class="slot-settings" data-provider="rvc"></div></div>
57
- <h2>5) selection-read chip (demo)</h2>
58
- <div class="card"><button type="button" class="dsh-tts-sel-btn">朗读选中文本</button> <span style="color:var(--dsw-alias-label-tertiary);font-size:11px">← 悬浮在选择旁的“朗读选中”chip(真实交互中由 mouseup 触发)</span></div>
59
-
60
- <script>
61
- /* ---------- minimal React-agnostic shim + hooks + DOM renderer ---------- */
62
- let activeHooks = null;
63
- const react = {
64
- createElement(type, props, ...children){
65
- const flat=[];
66
- const push=(x)=>{ if(x==null||x===false||x===true)return; if(Array.isArray(x)){x.forEach(push);return;} flat.push(x); };
67
- children.forEach(push);
68
- return {type, props: props||{}, children: flat};
69
- },
70
- Fragment: Symbol('frag'),
71
- useState:i=>activeHooks.useState(i),
72
- useEffect:(f,d)=>activeHooks.effect(f,d),
73
- useRef:i=>activeHooks.ref(i),
74
- useMemo:f=>activeHooks.memo(f),
75
- };
76
- function makeHooks(){
77
- let c=0; const mem=[];
78
- return {
79
- useState(init){ const i=c++; if(!(i in mem)) mem[i]=typeof init==='function'?init():init; return [mem[i],()=>{}]; },
80
- effect(){ c++; /* do not run component effects (no interaction/nav) */ },
81
- ref(init){ c++; return {current:init}; },
82
- memo(f){ c++; try{return f();}catch(e){return undefined;} },
83
- };
84
- }
85
- function mount(v){
86
- if(v==null||typeof v==='boolean') return null;
87
- if(typeof v==='string'||typeof v==='number') return document.createTextNode(String(v));
88
- if(v.type===react.Fragment){ const f=document.createDocumentFragment(); (v.children||[]).forEach(c=>{const n=mount(c);if(n)f.appendChild(n);}); return f; }
89
- if(typeof v.type==='function'){ activeHooks=makeHooks(); const n=mount(v.type(v.props) ); activeHooks=null; return n; }
90
- const el=document.createElement(v.type);
91
- const p=v.props||{};
92
- for(const k in p){
93
- const val=p[k];
94
- if(k==='children'||k==='key'||k==='ref') continue;
95
- if(typeof val==='function') continue; // handlers ignored for static render
96
- if(k==='className'){ el.setAttribute('class', val); }
97
- else if(k==='style'&&val&&typeof val==='object'){ for(const sk in val){ try{ el.style[sk]=val[sk]; }catch(e){} } }
98
- else if(k==='value'){ try{ el.value=val; }catch(e){ el.setAttribute('value',val); } }
99
- else if(k==='disabled'){ if(val) el.setAttribute('disabled',''); }
100
- else { try{ el.setAttribute(k, val); }catch(e){} }
101
- }
102
- (v.children||[]).forEach(c=>{ const n=mount(c); if(n)el.appendChild(n); });
103
- return el;
104
- }
105
- /* ---------- slots / ctx (mirrors client-load) ---------- */
106
- const injectedComponents=[];
107
- const slots={
108
- inject(slot,fn){ injectedComponents.push({slot,fn}); },
109
- register(spec,component){ return component; },
110
- };
111
- const ctx={ get(n){ return n==='slots'?slots:undefined; }, effect(fn){ try{ fn(); }catch(e){} } };
112
-
113
- /* ---------- load client bundle once; we'll re-eval per state ---------- */
114
- const CLIENT = ${JSON.stringify(clientSrc)};
115
- function evalClient(){
116
- injectedComponents.length=0;
117
- window.__ModuleLoader__={
118
- load({factory}){
119
- const require_=name=>{ if(name==='react') return react; throw new Error('unknown require '+name); };
120
- const mod=factory(require_);
121
- if(!mod||!mod.apply) throw new Error('no apply');
122
- mod.apply(ctx);
123
- },
124
- };
125
- const fn=new Function('window','navigator','document','Audio', CLIENT + '\\n;return window.__ModuleLoader__;');
126
- fn(window,navigator,document,Audio);
127
- }
128
- function seedSettings(obj){
129
- if(obj) localStorage.setItem('dsh-tts-settings', JSON.stringify(obj));
130
- else localStorage.removeItem('dsh-tts-settings');
131
- }
132
- function renderSlot(slotName, container){
133
- try{
134
- const c=injectedComponents.find(x=>x.slot===slotName);
135
- if(!c) throw new Error('no component for '+slotName);
136
- const comp=c.fn();
137
- if(typeof comp!=='function') throw new Error(slotName+' fn did not return component');
138
- activeHooks=makeHooks();
139
- const node=comp({ session:null, sessionId:'s1', useSession:s=>s({nodes:[]}), messageId:'m1' });
140
- const dom=mount(node);
141
- activeHooks=null;
142
- if(dom) container.appendChild(dom);
143
- }catch(e){
144
- logErr(slotName+': '+ (e&&e.stack||e));
145
- }
146
- }
147
- function logErr(msg){
148
- const el=document.createElement('pre');
149
- el.style.cssText='color:#e5484d;font-size:11px;white-space:pre-wrap';
150
- el.textContent=msg;
151
- document.body.appendChild(el);
152
- }
153
-
154
- /* off */
155
- seedSettings({autoRead:false, provider:'edge-tts', voice:'zh-CN-XiaoxuanNeural'});
156
- evalClient(); renderSlot('conversation.input.left', document.querySelector('.slot-input[data-state=off]'));
157
-
158
- /* on */
159
- seedSettings({autoRead:true, provider:'edge-tts', voice:'zh-CN-YunyangNeural'});
160
- evalClient(); renderSlot('conversation.input.left', document.querySelector('.slot-input[data-state=on]'));
161
-
162
- /* settings edge */
163
- seedSettings({autoRead:false, provider:'edge-tts', voice:'zh-CN-XiaoxuanNeural'});
164
- evalClient(); renderSlot('settings.plugins.tab', document.querySelector('.slot-settings[data-provider=edge]'));
165
-
166
- /* settings rvc (shows onboarding) */
167
- seedSettings({autoRead:false, provider:'rvc', voice:'zh-CN-XiaoxuanNeural', rvc:{model:'C:/rvc/model.pth',index:'C:/rvc/model.index',f0UpKey:-2,indexRate:0.75,protect:0.33}});
168
- evalClient(); renderSlot('settings.plugins.tab', document.querySelector('.slot-settings[data-provider=rvc]'));
169
- </script>
170
- </body></html>
171
- `;
172
-
173
- writeFileSync(out, html, 'utf8');
174
- console.log('wrote', out);
1
+ #!/usr/bin/env node
2
+ // Generates a standalone UI preview page (tools/ui-preview.html) that loads the
3
+ // REAL lib/client.js in a browser, renders the injected components into actual
4
+ // DOM using a lightweight React-agnostic renderer, and lets you screenshot it
5
+ // for visual verification of the plugin UI (P2-P5).
6
+ //
7
+ // node tools/gen-ui-preview.mjs [out.html]
8
+ // Then open out.html in a browser (or headless screenshot):
9
+ // npx playwright screenshot tools/ui-preview.html shot.png (example)
10
+ //
11
+ // Note: this is a visual-verification aid only, NOT shipped into the plugin.
12
+ import { readFileSync, writeFileSync } from 'node:fs';
13
+ import { fileURLToPath } from 'node:url';
14
+ import path from 'node:path';
15
+
16
+ const here = path.dirname(fileURLToPath(import.meta.url));
17
+ const clientSrc = readFileSync(path.resolve(here, '..', 'lib', 'client.js'), 'utf8')
18
+ .replace(/<\/script/gi, '<\\/script');
19
+
20
+ const out = path.resolve(here, process.argv[2] || 'ui-preview.html');
21
+
22
+ const html = `<!doctype html>
23
+ <html lang="zh-CN"><head><meta charset="utf-8"><title>dsh-plugin-tts UI preview</title>
24
+ <style>
25
+ :root{
26
+ /* minimal dsh web theme tokens so --dsw-* styles resolve */
27
+ --dsw-alias-label-primary:#eceff3;
28
+ --dsw-alias-label-secondary:#c3c9d1;
29
+ --dsw-alias-label-tertiary:#7f8894;
30
+ --dsw-alias-label-dimmed:#5a6472;
31
+ --dsw-alias-label-inverse:#0f1115;
32
+ --dsw-alias-label-error:#e5484d;
33
+ --dsw-alias-interactive-bg-hover:rgba(255,255,255,.06);
34
+ --dsw-alias-interactive-bg-primary:#3b82f6;
35
+ --dsw-alias-bg-layer-2:#161a20;
36
+ --dsw-alias-bg-layer-3:#1c2128;
37
+ --dsw-alias-border-secondary:rgba(255,255,255,.08);
38
+ --dsw-alias-border-l2:#2a313b;
39
+ --dsw-alias-brand-primary:#3b82f6;
40
+ }
41
+ body{background:#0d0f13;color:var(--dsw-alias-label-primary);font-family:system-ui,-apple-system,'PingFang SC','Microsoft YaHei',sans-serif;margin:0;padding:24px}
42
+ h2{font-size:13px;color:var(--dsw-alias-label-tertiary);font-weight:600;margin:18px 0 8px;text-transform:uppercase;letter-spacing:.5px}
43
+ .card{background:#14181d;border:1px solid var(--dsw-alias-border-secondary);border-radius:12px;padding:14px;margin-bottom:6px;max-width:640px}
44
+ .toolrow{display:flex;align-items:center;gap:6px;padding:6px 8px;background:#15191f;border:1px solid var(--dsw-alias-border-secondary);border-radius:10px;margin-bottom:8px}
45
+ .toolrow::before{content:"▍";color:var(--dsw-alias-label-dimmed);font-size:13px}
46
+ pre.tip{color:var(--dsw-alias-label-tertiary);font-size:11px;margin:2px 0 0}
47
+ </style></head>
48
+ <body>
49
+ <h2>1) auto-read pill (off)</h2>
50
+ <div class="toolrow"><div class="slot-input" data-state="off"></div></div>
51
+ <h2>2) auto-read pill (on)</h2>
52
+ <div class="toolrow"><div class="slot-input" data-state="on"></div></div>
53
+ <h2>3) voice settings panel — Edge provider</h2>
54
+ <div class="card"><div class="slot-settings" data-provider="edge"></div></div>
55
+ <h2>4) voice settings panel — RVC provider (onboarding)</h2>
56
+ <div class="card"><div class="slot-settings" data-provider="rvc"></div></div>
57
+ <h2>5) selection-read chip (demo)</h2>
58
+ <div class="card"><button type="button" class="dsh-tts-sel-btn">朗读选中文本</button> <span style="color:var(--dsw-alias-label-tertiary);font-size:11px">← 悬浮在选择旁的“朗读选中”chip(真实交互中由 mouseup 触发)</span></div>
59
+
60
+ <script>
61
+ /* ---------- minimal React-agnostic shim + hooks + DOM renderer ---------- */
62
+ let activeHooks = null;
63
+ const react = {
64
+ createElement(type, props, ...children){
65
+ const flat=[];
66
+ const push=(x)=>{ if(x==null||x===false||x===true)return; if(Array.isArray(x)){x.forEach(push);return;} flat.push(x); };
67
+ children.forEach(push);
68
+ return {type, props: props||{}, children: flat};
69
+ },
70
+ Fragment: Symbol('frag'),
71
+ useState:i=>activeHooks.useState(i),
72
+ useEffect:(f,d)=>activeHooks.effect(f,d),
73
+ useRef:i=>activeHooks.ref(i),
74
+ useMemo:f=>activeHooks.memo(f),
75
+ };
76
+ function makeHooks(){
77
+ let c=0; const mem=[];
78
+ return {
79
+ useState(init){ const i=c++; if(!(i in mem)) mem[i]=typeof init==='function'?init():init; return [mem[i],()=>{}]; },
80
+ effect(){ c++; /* do not run component effects (no interaction/nav) */ },
81
+ ref(init){ c++; return {current:init}; },
82
+ memo(f){ c++; try{return f();}catch(e){return undefined;} },
83
+ };
84
+ }
85
+ function mount(v){
86
+ if(v==null||typeof v==='boolean') return null;
87
+ if(typeof v==='string'||typeof v==='number') return document.createTextNode(String(v));
88
+ if(v.type===react.Fragment){ const f=document.createDocumentFragment(); (v.children||[]).forEach(c=>{const n=mount(c);if(n)f.appendChild(n);}); return f; }
89
+ if(typeof v.type==='function'){ activeHooks=makeHooks(); const n=mount(v.type(v.props) ); activeHooks=null; return n; }
90
+ const el=document.createElement(v.type);
91
+ const p=v.props||{};
92
+ for(const k in p){
93
+ const val=p[k];
94
+ if(k==='children'||k==='key'||k==='ref') continue;
95
+ if(typeof val==='function') continue; // handlers ignored for static render
96
+ if(k==='className'){ el.setAttribute('class', val); }
97
+ else if(k==='style'&&val&&typeof val==='object'){ for(const sk in val){ try{ el.style[sk]=val[sk]; }catch(e){} } }
98
+ else if(k==='value'){ try{ el.value=val; }catch(e){ el.setAttribute('value',val); } }
99
+ else if(k==='disabled'){ if(val) el.setAttribute('disabled',''); }
100
+ else { try{ el.setAttribute(k, val); }catch(e){} }
101
+ }
102
+ (v.children||[]).forEach(c=>{ const n=mount(c); if(n)el.appendChild(n); });
103
+ return el;
104
+ }
105
+ /* ---------- slots / ctx (mirrors client-load) ---------- */
106
+ const injectedComponents=[];
107
+ const slots={
108
+ inject(slot,fn){ injectedComponents.push({slot,fn}); },
109
+ register(spec,component){ return component; },
110
+ };
111
+ const ctx={ get(n){ return n==='slots'?slots:undefined; }, effect(fn){ try{ fn(); }catch(e){} } };
112
+
113
+ /* ---------- load client bundle once; we'll re-eval per state ---------- */
114
+ const CLIENT = ${JSON.stringify(clientSrc)};
115
+ function evalClient(){
116
+ injectedComponents.length=0;
117
+ window.__ModuleLoader__={
118
+ load({factory}){
119
+ const require_=name=>{ if(name==='react') return react; throw new Error('unknown require '+name); };
120
+ const mod=factory(require_);
121
+ if(!mod||!mod.apply) throw new Error('no apply');
122
+ mod.apply(ctx);
123
+ },
124
+ };
125
+ const fn=new Function('window','navigator','document','Audio', CLIENT + '\\n;return window.__ModuleLoader__;');
126
+ fn(window,navigator,document,Audio);
127
+ }
128
+ function seedSettings(obj){
129
+ if(obj) localStorage.setItem('dsh-tts-settings', JSON.stringify(obj));
130
+ else localStorage.removeItem('dsh-tts-settings');
131
+ }
132
+ function renderSlot(slotName, container){
133
+ try{
134
+ const c=injectedComponents.find(x=>x.slot===slotName);
135
+ if(!c) throw new Error('no component for '+slotName);
136
+ const comp=c.fn();
137
+ if(typeof comp!=='function') throw new Error(slotName+' fn did not return component');
138
+ activeHooks=makeHooks();
139
+ const node=comp({ session:null, sessionId:'s1', useSession:s=>s({nodes:[]}), messageId:'m1' });
140
+ const dom=mount(node);
141
+ activeHooks=null;
142
+ if(dom) container.appendChild(dom);
143
+ }catch(e){
144
+ logErr(slotName+': '+ (e&&e.stack||e));
145
+ }
146
+ }
147
+ function logErr(msg){
148
+ const el=document.createElement('pre');
149
+ el.style.cssText='color:#e5484d;font-size:11px;white-space:pre-wrap';
150
+ el.textContent=msg;
151
+ document.body.appendChild(el);
152
+ }
153
+
154
+ /* off */
155
+ seedSettings({autoRead:false, provider:'edge-tts', voice:'zh-CN-XiaoxuanNeural'});
156
+ evalClient(); renderSlot('conversation.input.left', document.querySelector('.slot-input[data-state=off]'));
157
+
158
+ /* on */
159
+ seedSettings({autoRead:true, provider:'edge-tts', voice:'zh-CN-YunyangNeural'});
160
+ evalClient(); renderSlot('conversation.input.left', document.querySelector('.slot-input[data-state=on]'));
161
+
162
+ /* settings edge */
163
+ seedSettings({autoRead:false, provider:'edge-tts', voice:'zh-CN-XiaoxuanNeural'});
164
+ evalClient(); renderSlot('settings.plugins.tab', document.querySelector('.slot-settings[data-provider=edge]'));
165
+
166
+ /* settings rvc (shows onboarding) */
167
+ seedSettings({autoRead:false, provider:'rvc', voice:'zh-CN-XiaoxuanNeural', rvc:{model:'C:/rvc/model.pth',index:'C:/rvc/model.index',f0UpKey:-2,indexRate:0.75,protect:0.33}});
168
+ evalClient(); renderSlot('settings.plugins.tab', document.querySelector('.slot-settings[data-provider=rvc]'));
169
+ </script>
170
+ </body></html>
171
+ `;
172
+
173
+ writeFileSync(out, html, 'utf8');
174
+ console.log('wrote', out);