@newgameplusinc/odyssey-audio-video-sdk-dev 1.0.341 → 1.0.342

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.
@@ -209,21 +209,17 @@ tf.serialization.registerClass(GRUCellResetAfterSupport);
209
209
  //
210
210
  // WEIGHT NAMING:
211
211
  // model.json weightsManifest uses paths like "gru_96/gru_cell/kernel".
212
- // TF.js builds variable names as `${rnnLayer.name}/${cell.name}/${varName}`.
213
- // The RNN wrapper contributes "${layerName}/" automatically, so the cell
214
- // must be named just "gru_cell" (no layer prefix).
215
- // Result: "gru_96" + "/" + "gru_cell" + "/" + "kernel" = "gru_96/gru_cell/kernel" ✅
216
- // If cell were named "${layerName}/gru_cell" (old, wrong):
217
- // "gru_96" + "/" + "gru_96/gru_cell" + "/" + "kernel" = "gru_96/gru_96/gru_cell/kernel" ❌
212
+ // TF.js builds variable names as `${layer.name}/${varName}`.
213
+ // So the cell MUST be named `${layerName}/gru_cell` (e.g. "gru_96/gru_cell")
214
+ // so that addWeight('kernel') "gru_96/gru_cell/kernel" matches manifest.
218
215
  // =============================================================================
219
216
  class GRULayerWithResetAfter {
220
217
  static fromConfig(_cls, config) {
221
218
  const layerName = config.name;
222
- // Cell named "gru_cell" only RNN wrapper prepends layerName automatically.
223
- // Final weight path: "${layerName}/gru_cell/kernel" ✅
219
+ // Create our cell — named so its weights match the manifest paths
224
220
  const cell = new GRUCellResetAfterSupport({
225
221
  ...config,
226
- name: `gru_cell`, // RNN layer adds "gru_96/" prefix → "gru_96/gru_cell/kernel" ✅
222
+ name: `${layerName}/gru_cell`, // weights: "gru_96/gru_cell/kernel" ✅
227
223
  // Normalise keys: Keras JSON is snake_case; TF.js internals are camelCase
228
224
  useBias: config.useBias ?? config.use_bias ?? true,
229
225
  recurrentActivation: config.recurrentActivation ?? config.recurrent_activation ?? "sigmoid",
@@ -351,21 +347,13 @@ class MLNoiseSuppressor {
351
347
  const nMels = this.config.n_mels || 40;
352
348
  console.log(`[MLNoiseSuppressor] Warming up model (${seqLen} × ${nMels})...`);
353
349
  const warmupInput = tf.zeros([1, seqLen, nMels]);
354
- let warmupMax = 0;
355
350
  for (let w = 0; w < 3; w++) {
356
351
  const warmupOut = this.model.predict(warmupInput);
357
- const warmupData = warmupOut.dataSync();
358
- for (let i = 0; i < warmupData.length; i++)
359
- if (warmupData[i] > warmupMax)
360
- warmupMax = warmupData[i];
352
+ warmupOut.dataSync(); // force full synchronous execution
361
353
  warmupOut.dispose();
362
354
  }
363
355
  warmupInput.dispose();
364
- // For zero input, sigmoid(bias_only) should produce non-zero output if weights loaded.
365
- // If warmupMax === 0, weights failed to load (name mismatch in weightsManifest).
366
- console.log(`[MLNoiseSuppressor] Warmup done — zero-input output max: ${warmupMax.toFixed(4)} ${warmupMax > 0
367
- ? "✅ weights loaded"
368
- : "❌ weights NOT loaded — check weight name mapping"}`);
356
+ console.log(`[MLNoiseSuppressor] Warmup done`);
369
357
  this.isInitialized = true;
370
358
  console.log(`[MLNoiseSuppressor] ✅ Ready — noise suppression is ACTIVE`);
371
359
  console.log(`[MLNoiseSuppressor] Config: ${modelSampleRate}Hz, ${this.config.n_mels} mels, n_fft=${this.config.n_fft || 2048}`);
@@ -1019,13 +1007,6 @@ class MLNoiseSuppressor {
1019
1007
  if (rawMask[m] > rawMax)
1020
1008
  rawMax = rawMask[m];
1021
1009
  }
1022
- // Model failure fallback: if model outputs all-zeros (wrong/cached model files),
1023
- // returning gains=0 suppresses all speech. Return full passthrough instead.
1024
- if (rawMax === 0) {
1025
- const passthrough = new Float32Array(bins).fill(1.0);
1026
- passthrough[0] = 1.0; // speech flag = on, keeps worklet gate open
1027
- return passthrough;
1028
- }
1029
1010
  // Threshold 0.108: fan/AC noise consistently scores rawMax=0.094-0.100.
1030
1011
  // Speech consistently scores rawMax≥0.111. Gap between 0.100 and 0.111 gives
1031
1012
  // 8% margin. Previously 0.10 caused fan frames scoring exactly 0.100 to
@@ -1084,16 +1065,6 @@ class MLNoiseSuppressor {
1084
1065
  gains[k] = Math.max(IRM_SPEECH_FLOOR, gains[k]);
1085
1066
  }
1086
1067
  }
1087
- // ── Step 6: Set DC bin as explicit speech/noise gate signal ──────────
1088
- // The worklet reads gains[0] to control its ML gate:
1089
- // gains[0] >= 0.5 → speech detected → open gate + reset 533ms holdover
1090
- // gains[0] < 0.5 → noise frame → count down holdover, then close
1091
- //
1092
- // DC bin (0 Hz) has no mel filter coverage so it always computes to 1.0
1093
- // above (no-coverage default). This means the ML gate NEVER closes via ML
1094
- // — fan noise leaks through during inter-word pauses. Override it explicitly
1095
- // so the worklet gate actually follows the ML speech decision.
1096
- gains[0] = isSpeechFrame ? 1.0 : 0.0;
1097
1068
  // ── Diagnostic log ────────────────────────────────────────────────────
1098
1069
  const now = Date.now();
1099
1070
  const lastLog = MLNoiseSuppressor._lastGainLog || new Map();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newgameplusinc/odyssey-audio-video-sdk-dev",
3
- "version": "1.0.341",
3
+ "version": "1.0.342",
4
4
  "description": "Odyssey Spatial Audio & Video SDK using MediaSoup for real-time communication",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",