@jxgame2020/dsh-token-quota 0.1.26 → 0.1.28
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 +505 -305
- package/lib/index.js +10 -2
- package/lib/types/client/index.js +6 -1
- package/lib/types/client/locales.js +2 -2
- package/lib/types/client/music.d.ts +64 -49
- package/lib/types/client/music.js +314 -255
- package/lib/types/client/store.js +1 -1
- package/lib/types/index.js +15 -1
- package/lib/types/types.js +1 -1
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -27,7 +27,7 @@ const TOKEN_QUOTA_EXCEEDED_CODE = "TOKEN_QUOTA_EXCEEDED";
|
|
|
27
27
|
const TOKEN_QUOTA_DEFAULT_MUSIC = {
|
|
28
28
|
enabled: false,
|
|
29
29
|
volume: .5,
|
|
30
|
-
style: "
|
|
30
|
+
style: "pentatonic",
|
|
31
31
|
onlyCurrentSession: true
|
|
32
32
|
};
|
|
33
33
|
/** Build the stable per-model key shared by the counter, settings, and snapshot. */
|
|
@@ -478,7 +478,15 @@ var TokenQuotaService = class TokenQuotaService extends Service {
|
|
|
478
478
|
});
|
|
479
479
|
this.musicClients.add(res);
|
|
480
480
|
res.write(": token-quota music stream\n\n");
|
|
481
|
+
const hb = setInterval(() => {
|
|
482
|
+
try {
|
|
483
|
+
res.write("event: hb\ndata: 1\n\n");
|
|
484
|
+
} catch {
|
|
485
|
+
clearInterval(hb);
|
|
486
|
+
}
|
|
487
|
+
}, 5e3);
|
|
481
488
|
req.on("close", () => {
|
|
489
|
+
clearInterval(hb);
|
|
482
490
|
this.musicClients.delete(res);
|
|
483
491
|
});
|
|
484
492
|
}
|
|
@@ -507,7 +515,7 @@ var TokenQuotaService = class TokenQuotaService extends Service {
|
|
|
507
515
|
return this.upgradeCheck;
|
|
508
516
|
}
|
|
509
517
|
async doRefreshUpgrade() {
|
|
510
|
-
const current = "0.1.
|
|
518
|
+
const current = "0.1.28";
|
|
511
519
|
let latest;
|
|
512
520
|
try {
|
|
513
521
|
const response = await fetch(REGISTRY_LATEST_URL, {
|
|
@@ -43,6 +43,11 @@ export function apply(ctx) {
|
|
|
43
43
|
// output is driven simultaneously.
|
|
44
44
|
const music = new TokenQuotaMusic();
|
|
45
45
|
const sse = new EventSource('/token-quota/events');
|
|
46
|
+
// Host heartbeat every 5s: proves the stream is alive while the model
|
|
47
|
+
// thinks (no actions flow then). If it stops, the engine's watchdog fades
|
|
48
|
+
// the piece out instead of droning forever.
|
|
49
|
+
sse.addEventListener('hb', () => { music.markStreamActivity(); });
|
|
50
|
+
sse.addEventListener('open', () => { music.markStreamActivity(); });
|
|
46
51
|
sse.addEventListener('action', (event) => {
|
|
47
52
|
let action;
|
|
48
53
|
try {
|
|
@@ -65,7 +70,7 @@ export function apply(ctx) {
|
|
|
65
70
|
const musicDefaults = (raw) => ({
|
|
66
71
|
enabled: raw?.enabled ?? false,
|
|
67
72
|
volume: raw?.volume ?? 0.5,
|
|
68
|
-
style: raw?.style ?? '
|
|
73
|
+
style: raw?.style ?? 'pentatonic',
|
|
69
74
|
onlyCurrentSession: raw?.onlyCurrentSession ?? true,
|
|
70
75
|
});
|
|
71
76
|
const keyOf = (selection) => {
|
|
@@ -72,7 +72,7 @@ export const zh = {
|
|
|
72
72
|
musicStyle: '风格',
|
|
73
73
|
musicStyleMajor: '大调',
|
|
74
74
|
musicStyleMinor: '小调',
|
|
75
|
-
musicStylePentatonic: '
|
|
75
|
+
musicStylePentatonic: '古风',
|
|
76
76
|
musicOnlyCurrentSessionLabel: '只播当前会话',
|
|
77
77
|
musicOnlyCurrentSessionHint: '开启后仅当前窗口的会话有音乐,后台子任务静音;关闭则所有会话都参与作曲',
|
|
78
78
|
checkUpdatesNow: '立即检查',
|
|
@@ -148,7 +148,7 @@ export const en = {
|
|
|
148
148
|
musicStyle: 'Style',
|
|
149
149
|
musicStyleMajor: 'Major',
|
|
150
150
|
musicStyleMinor: 'Minor',
|
|
151
|
-
musicStylePentatonic: '
|
|
151
|
+
musicStylePentatonic: 'Gufeng',
|
|
152
152
|
musicOnlyCurrentSessionLabel: 'Current session only',
|
|
153
153
|
musicOnlyCurrentSessionHint: 'When on, only the visible session drives the soundtrack (background tasks stay silent); when off, every session contributes.',
|
|
154
154
|
checkUpdatesNow: 'Check now',
|
|
@@ -1,28 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* tool calls, etc.). Three layers:
|
|
2
|
+
* 古风 live-composed soundtrack driven by host actions.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* Design: when a request starts the engine composes a *piece* — a guzheng-like
|
|
5
|
+
* plucked-string voice (Web Audio synthesis; the MIDI output is switched to
|
|
6
|
+
* the Koto program so hardware synths match). The piece plays phrase after
|
|
7
|
+
* phrase so it stays alive while the model thinks or streams; incoming events
|
|
8
|
+
* steer the piece instead of interrupting it: a step change rotates the mode
|
|
9
|
+
* centre and inserts a fast 过门 (fill); tool calls get a high plink; the
|
|
10
|
+
* ending gets a dizi-like breath note and a descending glissando.
|
|
10
11
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* **No sustained tones by design.** Every voice is a decaying pluck (≤1.6s)
|
|
13
|
+
* or a fixed-duration breath note — there is no drone/pad oscillator that
|
|
14
|
+
* depends on a later stop event. A lost SSE event can at worst leave the
|
|
15
|
+
* phrase chain running, and the stream watchdog below stops that too, so a
|
|
16
|
+
* hanging "呜呜" hum is physically impossible.
|
|
17
|
+
*
|
|
18
|
+
* Anti-runaway guards:
|
|
19
|
+
* - the host sends an SSE heartbeat every 5s; if it stops arriving the whole
|
|
20
|
+
* engine stops (a dead stream means no stop event will ever come),
|
|
21
|
+
* - every MIDI NoteOn is registered and swept; stops also send
|
|
22
|
+
* All Notes Off (CC 123),
|
|
23
|
+
* - every Web Audio oscillator is explicitly stopped at envelope end.
|
|
15
24
|
*
|
|
16
25
|
* @module @jxgame2020/dsh-token-quota/music
|
|
17
26
|
*/
|
|
18
27
|
import type { TokenQuotaMusicAction, TokenQuotaMusicStyle } from '../types.ts';
|
|
19
28
|
/**
|
|
20
|
-
* Live-composed
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* (the panel switch), and only produces sound while `enabled` is true AND a
|
|
24
|
-
* request is in flight (pad + bass + melody are gated by `startPad` /
|
|
25
|
-
* `stopPad`).
|
|
29
|
+
* Live-composed gufeng engine. Idle until `ensureStarted()` runs inside a
|
|
30
|
+
* user gesture; silent while `enabled` is false; the piece runs only between
|
|
31
|
+
* `request/header` (start) and `assistant/message` / `turn/end` (stop).
|
|
26
32
|
*/
|
|
27
33
|
export declare class TokenQuotaMusic {
|
|
28
34
|
private audio;
|
|
@@ -31,49 +37,58 @@ export declare class TokenQuotaMusic {
|
|
|
31
37
|
private enabled;
|
|
32
38
|
private volume;
|
|
33
39
|
private style;
|
|
34
|
-
private
|
|
35
|
-
private
|
|
36
|
-
private
|
|
37
|
-
|
|
38
|
-
private
|
|
39
|
-
private
|
|
40
|
-
private
|
|
41
|
-
private
|
|
42
|
-
private
|
|
43
|
-
|
|
44
|
-
private melodyDegree;
|
|
45
|
-
private currentRoot;
|
|
46
|
-
private currentQuality;
|
|
47
|
-
/** Base MIDI note of the key. C4 = middle C. */
|
|
40
|
+
private playing;
|
|
41
|
+
private tonicIndex;
|
|
42
|
+
private phraseIndex;
|
|
43
|
+
/** True when the next piece start should play the full intro glissando. */
|
|
44
|
+
private introPending;
|
|
45
|
+
private phraseTimer;
|
|
46
|
+
private lastStreamActivity;
|
|
47
|
+
private watchdogTimer;
|
|
48
|
+
private readonly activeMidiNotes;
|
|
49
|
+
/** Base MIDI note of the tonic. C4 = middle C. */
|
|
48
50
|
private static readonly BASE_MIDI;
|
|
49
|
-
/** Beat length in ms —
|
|
51
|
+
/** Beat length in ms — the phrase rhythm is measured in these. */
|
|
50
52
|
private static readonly BEAT_MS;
|
|
53
|
+
/** Stop everything if the SSE stream goes silent this long. */
|
|
54
|
+
private static readonly STREAM_TIMEOUT_MS;
|
|
51
55
|
/** True once the audio context has been started (needs a user gesture). */
|
|
52
56
|
get started(): boolean;
|
|
53
57
|
/**
|
|
54
|
-
* Start the audio context and
|
|
55
|
-
*
|
|
56
|
-
* AudioContext can resume. Safe to call multiple times.
|
|
58
|
+
* Start the audio context and probe for a MIDI output (once). Must be
|
|
59
|
+
* called inside a user gesture so the AudioContext can resume.
|
|
57
60
|
*/
|
|
58
61
|
ensureStarted(): Promise<boolean>;
|
|
59
62
|
setEnabled(value: boolean): void;
|
|
60
63
|
setVolume(value: number): void;
|
|
61
64
|
setStyle(value: TokenQuotaMusicStyle): void;
|
|
62
|
-
/**
|
|
65
|
+
/** Feed the engine one SSE activity timestamp (action or heartbeat). */
|
|
66
|
+
markStreamActivity(): void;
|
|
67
|
+
/** Handle one incoming host action and steer the piece. */
|
|
63
68
|
onAction(action: TokenQuotaMusicAction): void;
|
|
64
|
-
/** Start
|
|
65
|
-
private
|
|
66
|
-
/**
|
|
67
|
-
private
|
|
68
|
-
/**
|
|
69
|
-
private
|
|
70
|
-
/**
|
|
71
|
-
private
|
|
72
|
-
|
|
69
|
+
/** Start a new piece: intro run, then the phrase chain. */
|
|
70
|
+
private startPiece;
|
|
71
|
+
/** Play one phrase, then chain into the next after a one-beat rest. */
|
|
72
|
+
private playPhrase;
|
|
73
|
+
/** Insert a 过门 fill: pause the chain, play the run, resume after. */
|
|
74
|
+
private insertFill;
|
|
75
|
+
/** Stop the phrase chain. */
|
|
76
|
+
private stopMelody;
|
|
77
|
+
/** Stop the whole piece; `final` adds the closing cadence gestures. */
|
|
78
|
+
private stopPiece;
|
|
79
|
+
/** Hard stop everything (master switch off / teardown). */
|
|
73
80
|
private stopAll;
|
|
74
|
-
/**
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
81
|
+
/**
|
|
82
|
+
* Guzheng-like pluck: a triangle voice plus a soft octave partial through a
|
|
83
|
+
* sweeping lowpass, fast attack and a long exponential decay (notes
|
|
84
|
+
* overlap, which keeps the piece continuous instead of choppy).
|
|
85
|
+
*/
|
|
86
|
+
private pluck;
|
|
87
|
+
/** Dizi-like breath note: sine with vibrato and a soft envelope. */
|
|
88
|
+
private breath;
|
|
89
|
+
/** Send one MIDI note with a registered NoteOff (anti hanging note). */
|
|
90
|
+
private midiNote;
|
|
91
|
+
/** Silence every registered note and send All Notes Off. */
|
|
92
|
+
private allMidiOff;
|
|
78
93
|
}
|
|
79
94
|
//# sourceMappingURL=music.d.ts.map
|