@jxgame2020/dsh-token-quota 0.1.27 → 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 CHANGED
@@ -95,7 +95,7 @@ window.__ModuleLoader__.load({
95
95
  d.musicOnlyCurrentSession = only;
96
96
  },
97
97
  setUpgrade: (d, upgrade) => {
98
- const normalized = upgrade !== null && upgrade.latestVersion !== "" && upgrade.latestVersion !== "0.1.27" ? upgrade : null;
98
+ const normalized = upgrade !== null && upgrade.latestVersion !== "" && upgrade.latestVersion !== "0.1.28" ? upgrade : null;
99
99
  if (d.upgrade === null || d.upgrade.latestVersion !== normalized?.latestVersion) d.upgradeDismissed = false;
100
100
  d.upgrade = normalized;
101
101
  },
@@ -201,92 +201,92 @@ window.__ModuleLoader__.load({
201
201
  document.head.appendChild(tag);
202
202
  }
203
203
  var TokenQuotaPanel_module_css_default = {
204
- "bar": "NPLLIq_bar",
205
- "tab": "NPLLIq_tab",
206
- "rowHeader": "NPLLIq_rowHeader",
207
- "logTable": "NPLLIq_logTable",
208
- "logPagerInfo": "NPLLIq_logPagerInfo",
209
- "copyBtn": "NPLLIq_copyBtn",
210
- "dialogBody": "NPLLIq_dialogBody",
211
- "logClearDialog": "NPLLIq_logClearDialog",
212
- "input": "NPLLIq_input",
204
+ "gearBtn": "NPLLIq_gearBtn",
205
+ "title": "NPLLIq_title",
206
+ "checkErrorHint": "NPLLIq_checkErrorHint",
207
+ "collapse": "NPLLIq_collapse",
208
+ "dialogTitle": "NPLLIq_dialogTitle",
209
+ "logDayCol": "NPLLIq_logDayCol",
210
+ "upgradeBannerText": "NPLLIq_upgradeBannerText",
211
+ "fillOver": "NPLLIq_fillOver",
213
212
  "monitorHint": "NPLLIq_monitorHint",
213
+ "logClearDialog": "NPLLIq_logClearDialog",
214
+ "dialogClose": "NPLLIq_dialogClose",
214
215
  "monitorName": "NPLLIq_monitorName",
215
- "checkErrorHint": "NPLLIq_checkErrorHint",
216
- "fillWarn": "NPLLIq_fillWarn",
217
- "dialogBtn": "NPLLIq_dialogBtn",
218
- "logModelCol": "NPLLIq_logModelCol",
219
- "fullNotice": "NPLLIq_fullNotice",
220
- "logPagerPages": "NPLLIq_logPagerPages",
221
- "subtitle": "NPLLIq_subtitle",
222
- "upgradeClose": "NPLLIq_upgradeClose",
223
- "logClearDate": "NPLLIq_logClearDate",
224
- "row": "NPLLIq_row",
225
- "logHeader": "NPLLIq_logHeader",
226
- "upgradeCommandRow": "NPLLIq_upgradeCommandRow",
227
216
  "dialog": "NPLLIq_dialog",
228
- "upgradeHint": "NPLLIq_upgradeHint",
229
- "dialogHeader": "NPLLIq_dialogHeader",
230
- "rowMeta": "NPLLIq_rowMeta",
231
- "upgradeBannerText": "NPLLIq_upgradeBannerText",
232
- "checkUpdatesLabel": "NPLLIq_checkUpdatesLabel",
233
- "dialogLabel": "NPLLIq_dialogLabel",
234
- "logPagerBtn": "NPLLIq_logPagerBtn",
235
217
  "logClearBtn": "NPLLIq_logClearBtn",
236
- "noticeError": "NPLLIq_noticeError",
237
- "logUsedCol": "NPLLIq_logUsedCol",
238
- "panelDim": "NPLLIq_panelDim",
239
- "body": "NPLLIq_body",
240
- "dialogClose": "NPLLIq_dialogClose",
218
+ "rowMeta": "NPLLIq_rowMeta",
219
+ "logSearch": "NPLLIq_logSearch",
241
220
  "selectBtn": "NPLLIq_selectBtn",
242
- "dialogOverlay": "NPLLIq_dialogOverlay",
243
- "header": "NPLLIq_header",
221
+ "panelDim": "NPLLIq_panelDim",
222
+ "upgradeBanner": "NPLLIq_upgradeBanner",
223
+ "rowHeader": "NPLLIq_rowHeader",
224
+ "checkBtn": "NPLLIq_checkBtn",
225
+ "dialogHint": "NPLLIq_dialogHint",
226
+ "logPagerInfo": "NPLLIq_logPagerInfo",
227
+ "logModelCol": "NPLLIq_logModelCol",
228
+ "upgradeClose": "NPLLIq_upgradeClose",
229
+ "resetRow": "NPLLIq_resetRow",
244
230
  "dialogActions": "NPLLIq_dialogActions",
245
- "logDayCol": "NPLLIq_logDayCol",
246
- "dialogTitle": "NPLLIq_dialogTitle",
247
- "headerText": "NPLLIq_headerText",
248
- "collapse": "NPLLIq_collapse",
231
+ "upgradeCommandRow": "NPLLIq_upgradeCommandRow",
249
232
  "currentBadge": "NPLLIq_currentBadge",
250
- "panel": "NPLLIq_panel",
251
- "fillOver": "NPLLIq_fillOver",
252
- "settingsBtn": "NPLLIq_settingsBtn",
253
- "gearBtn": "NPLLIq_gearBtn",
254
- "logPageSizeSelect": "NPLLIq_logPageSizeSelect",
255
- "dialogHint": "NPLLIq_dialogHint",
233
+ "upgradeHint": "NPLLIq_upgradeHint",
234
+ "dialogHeader": "NPLLIq_dialogHeader",
235
+ "logScroll": "NPLLIq_logScroll",
256
236
  "versionNew": "NPLLIq_versionNew",
257
- "save": "NPLLIq_save",
258
- "upgradeBannerFlash": "NPLLIq_upgradeBannerFlash",
259
- "switch": "NPLLIq_switch",
237
+ "header": "NPLLIq_header",
260
238
  "logToolbar": "NPLLIq_logToolbar",
261
- "tabIcon": "NPLLIq_tabIcon",
262
- "radioRow": "NPLLIq_radioRow",
263
- "settingsBtnActive": "NPLLIq_settingsBtnActive",
264
- "copyBtnDone": "NPLLIq_copyBtnDone",
265
- "headerActions": "NPLLIq_headerActions",
266
- "dialogSection": "NPLLIq_dialogSection",
267
- "upgradeBanner": "NPLLIq_upgradeBanner",
268
- "logScroll": "NPLLIq_logScroll",
269
- "resetSelect": "NPLLIq_resetSelect",
270
- "logSearch": "NPLLIq_logSearch",
271
- "checkBtn": "NPLLIq_checkBtn",
272
- "panelDimStrong": "NPLLIq_panelDimStrong",
239
+ "logPageSizeSelect": "NPLLIq_logPageSizeSelect",
273
240
  "monitorRow": "NPLLIq_monitorRow",
241
+ "dialogSection": "NPLLIq_dialogSection",
242
+ "logUsedCol": "NPLLIq_logUsedCol",
243
+ "subtitle": "NPLLIq_subtitle",
244
+ "tabLabel": "NPLLIq_tabLabel",
245
+ "dialogLabel": "NPLLIq_dialogLabel",
246
+ "row": "NPLLIq_row",
247
+ "dialogBtn": "NPLLIq_dialogBtn",
248
+ "dialogBody": "NPLLIq_dialogBody",
249
+ "version": "NPLLIq_version",
250
+ "checkUpdatesLabel": "NPLLIq_checkUpdatesLabel",
251
+ "copyBtnDone": "NPLLIq_copyBtnDone",
252
+ "logHeader": "NPLLIq_logHeader",
253
+ "headerText": "NPLLIq_headerText",
254
+ "fillWarn": "NPLLIq_fillWarn",
255
+ "upgradeBannerFlash": "NPLLIq_upgradeBannerFlash",
256
+ "fullNotice": "NPLLIq_fullNotice",
257
+ "upgradeCommand": "NPLLIq_upgradeCommand",
274
258
  "dialogBtnPrimary": "NPLLIq_dialogBtnPrimary",
259
+ "logPagerPages": "NPLLIq_logPagerPages",
275
260
  "upgradeCommands": "NPLLIq_upgradeCommands",
276
- "checkUpdatesRow": "NPLLIq_checkUpdatesRow",
261
+ "save": "NPLLIq_save",
262
+ "logClearDate": "NPLLIq_logClearDate",
263
+ "settingsBtn": "NPLLIq_settingsBtn",
277
264
  "logPager": "NPLLIq_logPager",
278
- "logPagerControls": "NPLLIq_logPagerControls",
265
+ "dialogOverlay": "NPLLIq_dialogOverlay",
279
266
  "logDialog": "NPLLIq_logDialog",
280
- "rowName": "NPLLIq_rowName",
267
+ "headerActions": "NPLLIq_headerActions",
281
268
  "monitorList": "NPLLIq_monitorList",
282
- "resetRow": "NPLLIq_resetRow",
283
- "title": "NPLLIq_title",
284
- "version": "NPLLIq_version",
285
- "tabLabel": "NPLLIq_tabLabel",
286
- "notice": "NPLLIq_notice",
287
- "upgradeCommand": "NPLLIq_upgradeCommand",
269
+ "bar": "NPLLIq_bar",
270
+ "input": "NPLLIq_input",
271
+ "noticeError": "NPLLIq_noticeError",
288
272
  "fillIdle": "NPLLIq_fillIdle",
289
- "controls": "NPLLIq_controls"
273
+ "copyBtn": "NPLLIq_copyBtn",
274
+ "logPagerControls": "NPLLIq_logPagerControls",
275
+ "logPagerBtn": "NPLLIq_logPagerBtn",
276
+ "radioRow": "NPLLIq_radioRow",
277
+ "rowName": "NPLLIq_rowName",
278
+ "controls": "NPLLIq_controls",
279
+ "resetSelect": "NPLLIq_resetSelect",
280
+ "tab": "NPLLIq_tab",
281
+ "notice": "NPLLIq_notice",
282
+ "body": "NPLLIq_body",
283
+ "tabIcon": "NPLLIq_tabIcon",
284
+ "panelDimStrong": "NPLLIq_panelDimStrong",
285
+ "switch": "NPLLIq_switch",
286
+ "settingsBtnActive": "NPLLIq_settingsBtnActive",
287
+ "panel": "NPLLIq_panel",
288
+ "checkUpdatesRow": "NPLLIq_checkUpdatesRow",
289
+ "logTable": "NPLLIq_logTable"
290
290
  };
291
291
  //#endregion
292
292
  //#region lib/types/client/TokenQuotaPanel.js
@@ -592,13 +592,13 @@ window.__ModuleLoader__.load({
592
592
  title: t("upgradeAvailableHint"),
593
593
  children: [
594
594
  "v",
595
- "0.1.27",
595
+ "0.1.28",
596
596
  " → v",
597
597
  upgrade.latestVersion
598
598
  ]
599
599
  }) : (0, react_jsx_runtime.jsxs)("span", {
600
600
  className: TokenQuotaPanel_module_css_default.version,
601
- children: ["v", "0.1.27"]
601
+ children: ["v", "0.1.28"]
602
602
  })]
603
603
  }), (0, react_jsx_runtime.jsx)("div", {
604
604
  className: TokenQuotaPanel_module_css_default.subtitle,
@@ -1235,21 +1235,25 @@ window.__ModuleLoader__.load({
1235
1235
  * 古风 live-composed soundtrack driven by host actions.
1236
1236
  *
1237
1237
  * Design: when a request starts the engine composes a *piece* — a guzheng-like
1238
- * plucked-string voice over a soft open-fifth drone (Web Audio synthesis; the
1239
- * MIDI output is switched to the Koto program so hardware synths match). The
1240
- * piece plays phrase after phrase so it never goes quiet while the model
1241
- * thinks or streams. Incoming events steer the piece instead of interrupting
1242
- * it: a step change glides the drone to the next mode centre and inserts a
1243
- * fast 过门 (fill); tool calls get a high plink; the ending gets a flute-like
1244
- * breath note and a descending glissando.
1238
+ * plucked-string voice (Web Audio synthesis; the MIDI output is switched to
1239
+ * the Koto program so hardware synths match). The piece plays phrase after
1240
+ * phrase so it stays alive while the model thinks or streams; incoming events
1241
+ * steer the piece instead of interrupting it: a step change rotates the mode
1242
+ * centre and inserts a fast 过门 (fill); tool calls get a high plink; the
1243
+ * ending gets a dizi-like breath note and a descending glissando.
1244
+ *
1245
+ * **No sustained tones by design.** Every voice is a decaying pluck (≤1.6s)
1246
+ * or a fixed-duration breath note — there is no drone/pad oscillator that
1247
+ * depends on a later stop event. A lost SSE event can at worst leave the
1248
+ * phrase chain running, and the stream watchdog below stops that too, so a
1249
+ * hanging "呜呜" hum is physically impossible.
1245
1250
  *
1246
- * Anti-runaway guards (the "endless drone" / hanging-MIDI-note problem):
1251
+ * Anti-runaway guards:
1247
1252
  * - the host sends an SSE heartbeat every 5s; if it stops arriving the whole
1248
- * engine fades out (a dead stream means no stop event will ever come),
1249
- * - every MIDI NoteOn is registered and swept; `stopAll` also sends
1253
+ * engine stops (a dead stream means no stop event will ever come),
1254
+ * - every MIDI NoteOn is registered and swept; stops also send
1250
1255
  * All Notes Off (CC 123),
1251
- * - Web Audio ramps never target zero exponentially and every oscillator is
1252
- * explicitly stopped.
1256
+ * - every Web Audio oscillator is explicitly stopped at envelope end.
1253
1257
  *
1254
1258
  * @module @jxgame2020/dsh-token-quota/music
1255
1259
  */
@@ -1489,11 +1493,9 @@ window.__ModuleLoader__.load({
1489
1493
  playing = false;
1490
1494
  tonicIndex = 0;
1491
1495
  phraseIndex = 0;
1496
+ /** True when the next piece start should play the full intro glissando. */
1497
+ introPending = true;
1492
1498
  phraseTimer;
1493
- droneOscs = [];
1494
- droneGain = null;
1495
- droneFilter = null;
1496
- droneActive = false;
1497
1499
  lastStreamActivity = 0;
1498
1500
  watchdogTimer;
1499
1501
  activeMidiNotes = /* @__PURE__ */ new Set();
@@ -1567,6 +1569,7 @@ window.__ModuleLoader__.load({
1567
1569
  this.stopPiece(false);
1568
1570
  this.tonicIndex = 0;
1569
1571
  this.phraseIndex = 0;
1572
+ this.introPending = true;
1570
1573
  break;
1571
1574
  case "request/header":
1572
1575
  if (!this.playing) this.startPiece();
@@ -1575,9 +1578,10 @@ window.__ModuleLoader__.load({
1575
1578
  case "step/start": {
1576
1579
  const prog = PROGRESSION[this.style];
1577
1580
  this.tonicIndex = (this.tonicIndex + 1) % prog.length;
1578
- const newRoot = base + (prog[this.tonicIndex] ?? 0);
1579
- this.glideDrone(newRoot);
1580
- if (this.playing) this.insertFill(newRoot);
1581
+ if (this.playing) {
1582
+ const newRoot = base + (prog[this.tonicIndex] ?? 0);
1583
+ this.insertFill(newRoot);
1584
+ }
1581
1585
  break;
1582
1586
  }
1583
1587
  case "tool/call":
@@ -1600,6 +1604,7 @@ window.__ModuleLoader__.load({
1600
1604
  break;
1601
1605
  case "assistant/message":
1602
1606
  this.stopMelody();
1607
+ this.playing = false;
1603
1608
  this.breath(action.interrupted ? base + 6 : base + 12, 1.6);
1604
1609
  break;
1605
1610
  case "turn/end":
@@ -1607,29 +1612,36 @@ window.__ModuleLoader__.load({
1607
1612
  break;
1608
1613
  }
1609
1614
  }
1610
- /** Start a new piece: drone + ascending glissando + phrase chain. */
1615
+ /** Start a new piece: intro run, then the phrase chain. */
1611
1616
  startPiece() {
1612
1617
  if (!this.audio || this.playing) return;
1613
1618
  this.playing = true;
1614
- this.tonicIndex = 0;
1615
- this.phraseIndex = 0;
1616
- const root = TokenQuotaMusic.BASE_MIDI + (PROGRESSION[this.style][0] ?? 0);
1617
- this.startDrone(root);
1619
+ const root = TokenQuotaMusic.BASE_MIDI + (PROGRESSION[this.style][this.tonicIndex] ?? 0);
1618
1620
  const scale = SCALES[this.style];
1619
- let t = 0;
1620
- for (let i = 0; i < 8; i++) {
1621
- const deg = i % scale.length;
1622
- const note = root + 12 * Math.floor(i / scale.length) + (scale[deg] ?? 0);
1623
- setTimeout(() => this.pluck(note, .3), t);
1624
- t += 150;
1621
+ if (this.introPending) {
1622
+ this.introPending = false;
1623
+ let t = 0;
1624
+ for (let i = 0; i < 8; i++) {
1625
+ const note = root + 12 * Math.floor(i / scale.length) + (scale[i % scale.length] ?? 0);
1626
+ setTimeout(() => {
1627
+ if (this.playing) this.pluck(note, .3);
1628
+ }, t);
1629
+ t += 150;
1630
+ }
1631
+ this.phraseTimer = setTimeout(() => this.playPhrase(), t + 100);
1632
+ } else {
1633
+ for (let i = 0; i < 3; i++) setTimeout(() => {
1634
+ if (this.playing) this.pluck(root + 12 + (scale[i] ?? 0), .3);
1635
+ }, i * 140);
1636
+ this.phraseTimer = setTimeout(() => this.playPhrase(), 480);
1625
1637
  }
1626
- this.phraseTimer = setTimeout(() => this.playPhrase(), t + 100);
1627
1638
  }
1628
1639
  /** Play one phrase, then chain into the next after a one-beat rest. */
1629
1640
  playPhrase() {
1630
1641
  if (!this.playing || !this.audio) return;
1631
1642
  const root = TokenQuotaMusic.BASE_MIDI + (PROGRESSION[this.style][this.tonicIndex] ?? 0);
1632
1643
  const scale = SCALES[this.style];
1644
+ this.pluck(root - 12, .28, 1.6);
1633
1645
  const phrase = PHRASES[this.phraseIndex % PHRASES.length];
1634
1646
  this.phraseIndex += 1;
1635
1647
  let t = 0;
@@ -1657,7 +1669,7 @@ window.__ModuleLoader__.load({
1657
1669
  }
1658
1670
  this.phraseTimer = setTimeout(() => this.playPhrase(), t + 60);
1659
1671
  }
1660
- /** Stop the melody chain (drone keeps fading naturally). */
1672
+ /** Stop the phrase chain. */
1661
1673
  stopMelody() {
1662
1674
  if (this.phraseTimer !== void 0) {
1663
1675
  clearTimeout(this.phraseTimer);
@@ -1673,8 +1685,7 @@ window.__ModuleLoader__.load({
1673
1685
  const scale = SCALES[this.style];
1674
1686
  let t = 0;
1675
1687
  for (let i = 7; i >= 0; i--) {
1676
- const deg = i % scale.length;
1677
- const note = base + 12 * Math.floor(i / scale.length) + (scale[deg] ?? 0);
1688
+ const note = base + 12 * Math.floor(i / scale.length) + (scale[i % scale.length] ?? 0);
1678
1689
  setTimeout(() => this.pluck(note, .3), t);
1679
1690
  t += 120;
1680
1691
  }
@@ -1685,86 +1696,16 @@ window.__ModuleLoader__.load({
1685
1696
  this.pluck(base - 12, .5);
1686
1697
  }, t + 150);
1687
1698
  }
1688
- this.fadeDrone(final ? 1.2 : .8);
1689
1699
  this.allMidiOff();
1690
1700
  }
1691
1701
  /** Hard stop everything (master switch off / teardown). */
1692
1702
  stopAll() {
1693
1703
  this.stopPiece(false);
1694
1704
  }
1695
- /** Start the sustained open fifth (root + fifth below octave root). */
1696
- startDrone(root) {
1697
- if (!this.audio || this.droneActive) return;
1698
- const now = this.audio.currentTime;
1699
- const gain = this.audio.createGain();
1700
- gain.gain.setValueAtTime(0, now);
1701
- const filter = this.audio.createBiquadFilter();
1702
- filter.type = "lowpass";
1703
- filter.frequency.value = 620;
1704
- filter.Q.value = .4;
1705
- gain.connect(filter);
1706
- filter.connect(this.audio.destination);
1707
- const oscs = [];
1708
- for (const interval of [-12, -5]) {
1709
- const osc = this.audio.createOscillator();
1710
- osc.type = "sine";
1711
- osc.frequency.value = midiToFreq(root + interval);
1712
- osc.connect(gain);
1713
- osc.start(now);
1714
- oscs.push(osc);
1715
- }
1716
- this.droneOscs = oscs;
1717
- this.droneGain = gain;
1718
- this.droneFilter = filter;
1719
- this.droneActive = true;
1720
- gain.gain.linearRampToValueAtTime(this.volume * .07, now + 1.4);
1721
- }
1722
- /** Glide the drone to a new tonic (mode rotation). */
1723
- glideDrone(root) {
1724
- if (!this.audio || !this.droneActive) return;
1725
- const now = this.audio.currentTime;
1726
- const targets = [root - 12, root - 5];
1727
- this.droneOscs.forEach((osc, i) => {
1728
- const target = targets[i] ?? root;
1729
- osc.frequency.setValueAtTime(osc.frequency.value, now);
1730
- osc.frequency.exponentialRampToValueAtTime(midiToFreq(target), now + .9);
1731
- });
1732
- }
1733
- /** Fade the drone out and stop its oscillators. */
1734
- fadeDrone(seconds) {
1735
- if (!this.audio || !this.droneActive || this.droneGain === null) return;
1736
- const now = this.audio.currentTime;
1737
- const gain = this.droneGain;
1738
- gain.gain.cancelScheduledValues(now);
1739
- gain.gain.setValueAtTime(Math.max(gain.gain.value, 1e-4), now);
1740
- gain.gain.exponentialRampToValueAtTime(1e-4, now + seconds);
1741
- const oscs = this.droneOscs;
1742
- const filter = this.droneFilter;
1743
- setTimeout(() => {
1744
- for (const osc of oscs) {
1745
- try {
1746
- osc.stop();
1747
- } catch {}
1748
- try {
1749
- osc.disconnect();
1750
- } catch {}
1751
- }
1752
- try {
1753
- filter?.disconnect();
1754
- } catch {}
1755
- try {
1756
- gain.disconnect();
1757
- } catch {}
1758
- }, seconds * 1e3 + 120);
1759
- this.droneActive = false;
1760
- this.droneOscs = [];
1761
- this.droneGain = null;
1762
- this.droneFilter = null;
1763
- }
1764
1705
  /**
1765
- * Guzheng-like pluck: two slightly detuned oscillators through a sweeping
1766
- * lowpass, fast attack and a long exponential decay (notes overlap, which
1767
- * keeps the piece continuous instead of choppy).
1706
+ * Guzheng-like pluck: a triangle voice plus a soft octave partial through a
1707
+ * sweeping lowpass, fast attack and a long exponential decay (notes
1708
+ * overlap, which keeps the piece continuous instead of choppy).
1768
1709
  */
1769
1710
  pluck(midi, velocity, decaySec = 1.3) {
1770
1711
  if (!this.audio) return;
@@ -1788,9 +1729,9 @@ window.__ModuleLoader__.load({
1788
1729
  osc1.frequency.value = freq;
1789
1730
  const osc2 = this.audio.createOscillator();
1790
1731
  osc2.type = "sine";
1791
- osc2.frequency.value = freq * 2.001;
1732
+ osc2.frequency.value = freq * 2;
1792
1733
  const octGain = this.audio.createGain();
1793
- octGain.gain.value = .35;
1734
+ octGain.gain.value = .25;
1794
1735
  osc1.connect(filter);
1795
1736
  osc2.connect(octGain);
1796
1737
  octGain.connect(filter);
package/lib/index.js CHANGED
@@ -515,7 +515,7 @@ var TokenQuotaService = class TokenQuotaService extends Service {
515
515
  return this.upgradeCheck;
516
516
  }
517
517
  async doRefreshUpgrade() {
518
- const current = "0.1.27";
518
+ const current = "0.1.28";
519
519
  let latest;
520
520
  try {
521
521
  const response = await fetch(REGISTRY_LATEST_URL, {
@@ -2,21 +2,25 @@
2
2
  * 古风 live-composed soundtrack driven by host actions.
3
3
  *
4
4
  * Design: when a request starts the engine composes a *piece* — a guzheng-like
5
- * plucked-string voice over a soft open-fifth drone (Web Audio synthesis; the
6
- * MIDI output is switched to the Koto program so hardware synths match). The
7
- * piece plays phrase after phrase so it never goes quiet while the model
8
- * thinks or streams. Incoming events steer the piece instead of interrupting
9
- * it: a step change glides the drone to the next mode centre and inserts a
10
- * fast 过门 (fill); tool calls get a high plink; the ending gets a flute-like
11
- * breath note and a descending glissando.
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.
12
11
  *
13
- * Anti-runaway guards (the "endless drone" / hanging-MIDI-note problem):
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:
14
19
  * - the host sends an SSE heartbeat every 5s; if it stops arriving the whole
15
- * engine fades out (a dead stream means no stop event will ever come),
16
- * - every MIDI NoteOn is registered and swept; `stopAll` also sends
20
+ * engine stops (a dead stream means no stop event will ever come),
21
+ * - every MIDI NoteOn is registered and swept; stops also send
17
22
  * All Notes Off (CC 123),
18
- * - Web Audio ramps never target zero exponentially and every oscillator is
19
- * explicitly stopped.
23
+ * - every Web Audio oscillator is explicitly stopped at envelope end.
20
24
  *
21
25
  * @module @jxgame2020/dsh-token-quota/music
22
26
  */
@@ -36,11 +40,9 @@ export declare class TokenQuotaMusic {
36
40
  private playing;
37
41
  private tonicIndex;
38
42
  private phraseIndex;
43
+ /** True when the next piece start should play the full intro glissando. */
44
+ private introPending;
39
45
  private phraseTimer;
40
- private droneOscs;
41
- private droneGain;
42
- private droneFilter;
43
- private droneActive;
44
46
  private lastStreamActivity;
45
47
  private watchdogTimer;
46
48
  private readonly activeMidiNotes;
@@ -64,28 +66,22 @@ export declare class TokenQuotaMusic {
64
66
  markStreamActivity(): void;
65
67
  /** Handle one incoming host action and steer the piece. */
66
68
  onAction(action: TokenQuotaMusicAction): void;
67
- /** Start a new piece: drone + ascending glissando + phrase chain. */
69
+ /** Start a new piece: intro run, then the phrase chain. */
68
70
  private startPiece;
69
71
  /** Play one phrase, then chain into the next after a one-beat rest. */
70
72
  private playPhrase;
71
73
  /** Insert a 过门 fill: pause the chain, play the run, resume after. */
72
74
  private insertFill;
73
- /** Stop the melody chain (drone keeps fading naturally). */
75
+ /** Stop the phrase chain. */
74
76
  private stopMelody;
75
77
  /** Stop the whole piece; `final` adds the closing cadence gestures. */
76
78
  private stopPiece;
77
79
  /** Hard stop everything (master switch off / teardown). */
78
80
  private stopAll;
79
- /** Start the sustained open fifth (root + fifth below octave root). */
80
- private startDrone;
81
- /** Glide the drone to a new tonic (mode rotation). */
82
- private glideDrone;
83
- /** Fade the drone out and stop its oscillators. */
84
- private fadeDrone;
85
81
  /**
86
- * Guzheng-like pluck: two slightly detuned oscillators through a sweeping
87
- * lowpass, fast attack and a long exponential decay (notes overlap, which
88
- * keeps the piece continuous instead of choppy).
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).
89
85
  */
90
86
  private pluck;
91
87
  /** Dizi-like breath note: sine with vibrato and a soft envelope. */
@@ -2,21 +2,25 @@
2
2
  * 古风 live-composed soundtrack driven by host actions.
3
3
  *
4
4
  * Design: when a request starts the engine composes a *piece* — a guzheng-like
5
- * plucked-string voice over a soft open-fifth drone (Web Audio synthesis; the
6
- * MIDI output is switched to the Koto program so hardware synths match). The
7
- * piece plays phrase after phrase so it never goes quiet while the model
8
- * thinks or streams. Incoming events steer the piece instead of interrupting
9
- * it: a step change glides the drone to the next mode centre and inserts a
10
- * fast 过门 (fill); tool calls get a high plink; the ending gets a flute-like
11
- * breath note and a descending glissando.
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.
12
11
  *
13
- * Anti-runaway guards (the "endless drone" / hanging-MIDI-note problem):
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:
14
19
  * - the host sends an SSE heartbeat every 5s; if it stops arriving the whole
15
- * engine fades out (a dead stream means no stop event will ever come),
16
- * - every MIDI NoteOn is registered and swept; `stopAll` also sends
20
+ * engine stops (a dead stream means no stop event will ever come),
21
+ * - every MIDI NoteOn is registered and swept; stops also send
17
22
  * All Notes Off (CC 123),
18
- * - Web Audio ramps never target zero exponentially and every oscillator is
19
- * explicitly stopped.
23
+ * - every Web Audio oscillator is explicitly stopped at envelope end.
20
24
  *
21
25
  * @module @jxgame2020/dsh-token-quota/music
22
26
  */
@@ -74,12 +78,9 @@ export class TokenQuotaMusic {
74
78
  playing = false;
75
79
  tonicIndex = 0;
76
80
  phraseIndex = 0;
81
+ /** True when the next piece start should play the full intro glissando. */
82
+ introPending = true;
77
83
  phraseTimer;
78
- // Drone layer (sustained open fifth under the plucks)
79
- droneOscs = [];
80
- droneGain = null;
81
- droneFilter = null;
82
- droneActive = false;
83
84
  // Watchdog: host heartbeat freshness (ms timestamp)
84
85
  lastStreamActivity = 0;
85
86
  watchdogTimer;
@@ -138,7 +139,7 @@ export class TokenQuotaMusic {
138
139
  this.lastStreamActivity = Date.now();
139
140
  this.watchdogTimer = setInterval(() => {
140
141
  if (this.playing && Date.now() - this.lastStreamActivity > TokenQuotaMusic.STREAM_TIMEOUT_MS) {
141
- // Dead stream: no stop event will ever arrive — fade out.
142
+ // Dead stream: no stop event will ever arrive — stop the piece.
142
143
  this.stopPiece(false);
143
144
  }
144
145
  }, 4_000);
@@ -170,6 +171,7 @@ export class TokenQuotaMusic {
170
171
  this.stopPiece(false);
171
172
  this.tonicIndex = 0;
172
173
  this.phraseIndex = 0;
174
+ this.introPending = true;
173
175
  break;
174
176
  case 'request/header': {
175
177
  if (!this.playing)
@@ -179,13 +181,13 @@ export class TokenQuotaMusic {
179
181
  break;
180
182
  }
181
183
  case 'step/start': {
182
- // New phase: rotate the tonic, glide the drone, insert a 过门 fill.
184
+ // New phase: rotate the mode centre and insert a 过门 fill.
183
185
  const prog = PROGRESSION[this.style];
184
186
  this.tonicIndex = (this.tonicIndex + 1) % prog.length;
185
- const newRoot = base + (prog[this.tonicIndex] ?? 0);
186
- this.glideDrone(newRoot);
187
- if (this.playing)
187
+ if (this.playing) {
188
+ const newRoot = base + (prog[this.tonicIndex] ?? 0);
188
189
  this.insertFill(newRoot);
190
+ }
189
191
  break;
190
192
  }
191
193
  case 'tool/call':
@@ -211,9 +213,10 @@ export class TokenQuotaMusic {
211
213
  setTimeout(() => this.pluck(base + 5, 0.4, 0.8), 220);
212
214
  break;
213
215
  case 'assistant/message':
214
- // Phrase ends; the turn may continue (more steps) — keep the drone
215
- // breathing but stop the melody, ending with a breath note.
216
+ // Sentence end: stop the phrase chain and breathe. The next
217
+ // request/header (next step or next turn) starts a fresh phrase.
216
218
  this.stopMelody();
219
+ this.playing = false;
217
220
  this.breath(action.interrupted ? base + 6 : base + 12, 1.6);
218
221
  break;
219
222
  case 'turn/end':
@@ -222,27 +225,37 @@ export class TokenQuotaMusic {
222
225
  }
223
226
  }
224
227
  // ── piece lifecycle ───────────────────────────────────────────────────
225
- /** Start a new piece: drone + ascending glissando + phrase chain. */
228
+ /** Start a new piece: intro run, then the phrase chain. */
226
229
  startPiece() {
227
230
  if (!this.audio || this.playing)
228
231
  return;
229
232
  this.playing = true;
230
- this.tonicIndex = 0;
231
- this.phraseIndex = 0;
232
- const root = TokenQuotaMusic.BASE_MIDI + (PROGRESSION[this.style][0] ?? 0);
233
- this.startDrone(root);
234
- // Intro glissando — the guzheng sweep that announces the piece.
233
+ const root = TokenQuotaMusic.BASE_MIDI + (PROGRESSION[this.style][this.tonicIndex] ?? 0);
235
234
  const scale = SCALES[this.style];
236
- let t = 0;
237
- for (let i = 0; i < 8; i++) {
238
- const deg = i % scale.length;
239
- const oct = Math.floor(i / scale.length);
240
- const note = root + 12 * oct + (scale[deg] ?? 0);
241
- setTimeout(() => this.pluck(note, 0.3), t);
242
- t += 150;
235
+ if (this.introPending) {
236
+ // Full intro: the ascending guzheng glissando that announces the piece.
237
+ this.introPending = false;
238
+ let t = 0;
239
+ for (let i = 0; i < 8; i++) {
240
+ const note = root + 12 * Math.floor(i / scale.length) + (scale[i % scale.length] ?? 0);
241
+ setTimeout(() => {
242
+ if (this.playing)
243
+ this.pluck(note, 0.3);
244
+ }, t);
245
+ t += 150;
246
+ }
247
+ this.phraseTimer = setTimeout(() => this.playPhrase(), t + 100);
248
+ }
249
+ else {
250
+ // Restart within the same turn: a short three-note pickup, no glissando.
251
+ for (let i = 0; i < 3; i++) {
252
+ setTimeout(() => {
253
+ if (this.playing)
254
+ this.pluck(root + 12 + (scale[i] ?? 0), 0.3);
255
+ }, i * 140);
256
+ }
257
+ this.phraseTimer = setTimeout(() => this.playPhrase(), 480);
243
258
  }
244
- // First phrase starts right after the glissando.
245
- this.phraseTimer = setTimeout(() => this.playPhrase(), t + 100);
246
259
  }
247
260
  /** Play one phrase, then chain into the next after a one-beat rest. */
248
261
  playPhrase() {
@@ -250,6 +263,9 @@ export class TokenQuotaMusic {
250
263
  return;
251
264
  const root = TokenQuotaMusic.BASE_MIDI + (PROGRESSION[this.style][this.tonicIndex] ?? 0);
252
265
  const scale = SCALES[this.style];
266
+ // Soft low root pluck under each phrase — a foundation that decays away
267
+ // instead of a sustained drone (nothing here can hang).
268
+ this.pluck(root - 12, 0.28, 1.6);
253
269
  const phrase = PHRASES[this.phraseIndex % PHRASES.length];
254
270
  this.phraseIndex += 1;
255
271
  let t = 0;
@@ -282,7 +298,7 @@ export class TokenQuotaMusic {
282
298
  }
283
299
  this.phraseTimer = setTimeout(() => this.playPhrase(), t + 60);
284
300
  }
285
- /** Stop the melody chain (drone keeps fading naturally). */
301
+ /** Stop the phrase chain. */
286
302
  stopMelody() {
287
303
  if (this.phraseTimer !== undefined) {
288
304
  clearTimeout(this.phraseTimer);
@@ -299,9 +315,7 @@ export class TokenQuotaMusic {
299
315
  const scale = SCALES[this.style];
300
316
  let t = 0;
301
317
  for (let i = 7; i >= 0; i--) {
302
- const deg = i % scale.length;
303
- const oct = Math.floor(i / scale.length);
304
- const note = base + 12 * oct + (scale[deg] ?? 0);
318
+ const note = base + 12 * Math.floor(i / scale.length) + (scale[i % scale.length] ?? 0);
305
319
  setTimeout(() => this.pluck(note, 0.3), t);
306
320
  t += 120;
307
321
  }
@@ -312,95 +326,17 @@ export class TokenQuotaMusic {
312
326
  this.pluck(base - 12, 0.5);
313
327
  }, t + 150);
314
328
  }
315
- this.fadeDrone(final ? 1.2 : 0.8);
316
329
  this.allMidiOff();
317
330
  }
318
331
  /** Hard stop everything (master switch off / teardown). */
319
332
  stopAll() {
320
333
  this.stopPiece(false);
321
334
  }
322
- // ── drone layer ───────────────────────────────────────────────────────
323
- /** Start the sustained open fifth (root + fifth below octave root). */
324
- startDrone(root) {
325
- if (!this.audio || this.droneActive)
326
- return;
327
- const now = this.audio.currentTime;
328
- const gain = this.audio.createGain();
329
- gain.gain.setValueAtTime(0, now);
330
- const filter = this.audio.createBiquadFilter();
331
- filter.type = 'lowpass';
332
- filter.frequency.value = 620;
333
- filter.Q.value = 0.4;
334
- gain.connect(filter);
335
- filter.connect(this.audio.destination);
336
- const oscs = [];
337
- for (const interval of [-12, -5]) {
338
- const osc = this.audio.createOscillator();
339
- osc.type = 'sine';
340
- osc.frequency.value = midiToFreq(root + interval);
341
- osc.connect(gain);
342
- osc.start(now);
343
- oscs.push(osc);
344
- }
345
- this.droneOscs = oscs;
346
- this.droneGain = gain;
347
- this.droneFilter = filter;
348
- this.droneActive = true;
349
- gain.gain.linearRampToValueAtTime(this.volume * 0.07, now + 1.4);
350
- }
351
- /** Glide the drone to a new tonic (mode rotation). */
352
- glideDrone(root) {
353
- if (!this.audio || !this.droneActive)
354
- return;
355
- const now = this.audio.currentTime;
356
- const targets = [root - 12, root - 5];
357
- this.droneOscs.forEach((osc, i) => {
358
- const target = targets[i] ?? root;
359
- osc.frequency.setValueAtTime(osc.frequency.value, now);
360
- osc.frequency.exponentialRampToValueAtTime(midiToFreq(target), now + 0.9);
361
- });
362
- }
363
- /** Fade the drone out and stop its oscillators. */
364
- fadeDrone(seconds) {
365
- if (!this.audio || !this.droneActive || this.droneGain === null)
366
- return;
367
- const now = this.audio.currentTime;
368
- const gain = this.droneGain;
369
- gain.gain.cancelScheduledValues(now);
370
- gain.gain.setValueAtTime(Math.max(gain.gain.value, 0.0001), now);
371
- gain.gain.exponentialRampToValueAtTime(0.0001, now + seconds);
372
- const oscs = this.droneOscs;
373
- const filter = this.droneFilter;
374
- setTimeout(() => {
375
- for (const osc of oscs) {
376
- try {
377
- osc.stop();
378
- }
379
- catch { /* ignore */ }
380
- try {
381
- osc.disconnect();
382
- }
383
- catch { /* ignore */ }
384
- }
385
- try {
386
- filter?.disconnect();
387
- }
388
- catch { /* ignore */ }
389
- try {
390
- gain.disconnect();
391
- }
392
- catch { /* ignore */ }
393
- }, seconds * 1000 + 120);
394
- this.droneActive = false;
395
- this.droneOscs = [];
396
- this.droneGain = null;
397
- this.droneFilter = null;
398
- }
399
335
  // ── voices ────────────────────────────────────────────────────────────
400
336
  /**
401
- * Guzheng-like pluck: two slightly detuned oscillators through a sweeping
402
- * lowpass, fast attack and a long exponential decay (notes overlap, which
403
- * keeps the piece continuous instead of choppy).
337
+ * Guzheng-like pluck: a triangle voice plus a soft octave partial through a
338
+ * sweeping lowpass, fast attack and a long exponential decay (notes
339
+ * overlap, which keeps the piece continuous instead of choppy).
404
340
  */
405
341
  pluck(midi, velocity, decaySec = 1.3) {
406
342
  if (!this.audio)
@@ -426,9 +362,9 @@ export class TokenQuotaMusic {
426
362
  osc1.frequency.value = freq;
427
363
  const osc2 = this.audio.createOscillator();
428
364
  osc2.type = 'sine';
429
- osc2.frequency.value = freq * 2.001; // shimmering octave partial
365
+ osc2.frequency.value = freq * 2; // octave partial (no detune beating)
430
366
  const octGain = this.audio.createGain();
431
- octGain.gain.value = 0.35;
367
+ octGain.gain.value = 0.25;
432
368
  osc1.connect(filter);
433
369
  osc2.connect(octGain);
434
370
  octGain.connect(filter);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@jxgame2020/dsh-token-quota",
3
3
  "description": "Daily per-model token quota accounting, enforcement, and a floating quota panel for the DeepSeek Harness — a fully self-contained plugin (no core-Harness wiring, no generated RPC contract)",
4
- "version": "0.1.27",
4
+ "version": "0.1.28",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },