Solar-Prince commited on
Commit
f145864
·
verified ·
1 Parent(s): 94cea84

Upload 2 files

Browse files
Files changed (2) hide show
  1. README.md +3 -0
  2. index.html +30 -20
README.md CHANGED
@@ -24,3 +24,6 @@ SentAI is a browser-based live camera application for face detection, expression
24
  ## Notes
25
 
26
  The app estimates visible facial expression and apparent age from camera frames. It cannot know a person's true internal feeling. Lighting, pose, camera quality, glasses, occlusion, and model bias can affect results.
 
 
 
 
24
  ## Notes
25
 
26
  The app estimates visible facial expression and apparent age from camera frames. It cannot know a person's true internal feeling. Lighting, pose, camera quality, glasses, occlusion, and model bias can affect results.
27
+
28
+
29
+ Phase 3B fixes the accuracy-pack loader by using the Transformers.js package version that the selected ONNX emotion model was published for.
index.html CHANGED
@@ -485,10 +485,11 @@
485
  <div id="toast" class="toast" role="status" aria-live="polite"></div>
486
 
487
  <script type="module">
 
488
  const FACE_API_MODEL_URL = "https://cdn.jsdelivr.net/gh/justadudewhohacks/face-api.js@0.22.2/weights";
489
- const TRANSFORMERS_CDN = "https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.2.0";
490
  const EMOTION_MODEL_ID = "Xenova/facial_emotions_image_detection";
491
- const AGE_GENDER_MODEL_ID = "onnx-community/age-gender-prediction-ONNX";
492
 
493
  const els = {
494
  coreDot: document.getElementById("coreDot"),
@@ -630,7 +631,7 @@
630
  };
631
 
632
  const nonNeutralTop = Math.max(raw.happy, raw.sad, raw.fearful, raw.angry, raw.disgusted, raw.surprised);
633
- const uncertainty = clamp01((1 - nonNeutralTop) * 0.28 + raw.surprised * 0.54 + raw.neutral * 0.16);
634
 
635
  return normalizeScores({
636
  Happy: Math.pow(raw.happy, 1.08),
@@ -638,7 +639,7 @@
638
  Fear: Math.max(Math.pow(raw.fearful, 1.04), raw.surprised * raw.fearful * 0.55),
639
  Anger: Math.pow(raw.angry, 1.03),
640
  Disgust: Math.pow(raw.disgusted, 1.02),
641
- Confused: Math.min(0.62, uncertainty),
642
  });
643
  }
644
 
@@ -655,8 +656,8 @@
655
  else if (label.includes("angry") || label.includes("anger")) scores.Anger = Math.max(scores.Anger, score);
656
  else if (label.includes("disgust")) scores.Disgust = Math.max(scores.Disgust, score);
657
  else if (label.includes("surprise") || label.includes("neutral")) {
658
- const scaled = label.includes("neutral") ? score * 0.30 : score * 0.72;
659
- scores.Confused = Math.max(scores.Confused, Math.min(0.68, scaled));
660
  }
661
  }
662
  return normalizeScores(scores);
@@ -689,9 +690,13 @@
689
  const second = sorted[1] || ["", 0];
690
  let label = top[0];
691
  let score = top[1];
692
- if (score < 0.22 || (score - second[1] < 0.055 && label !== "Confused")) {
693
  label = "Confused";
694
- score = Math.max(scores.Confused || 0, 0.24);
 
 
 
 
695
  }
696
  return { label, score: clamp01(score) };
697
  }
@@ -994,26 +999,31 @@
994
  }
995
 
996
  async function getTransformersModule() {
997
- const mod = await import(TRANSFORMERS_CDN);
 
 
998
  if (mod.env) {
999
  mod.env.allowLocalModels = false;
1000
- if (mod.env.backends && mod.env.backends.onnx && mod.env.backends.onnx.wasm) {
1001
- mod.env.backends.onnx.wasm.numThreads = Math.max(1, Math.min(4, navigator.hardwareConcurrency || 2));
1002
- }
 
 
1003
  }
1004
  return mod;
1005
  }
1006
 
1007
  async function loadPipelineWithFallback(pipeline, task, modelId) {
1008
- const preferWebGpu = !!navigator.gpu;
1009
- const attempts = preferWebGpu
1010
- ? [{ device: "webgpu", dtype: "q8" }, { device: "wasm", dtype: "q8" }, { device: "wasm" }]
1011
- : [{ device: "wasm", dtype: "q8" }, { device: "wasm" }];
 
1012
  let lastErr = null;
1013
  for (const opts of attempts) {
1014
  try {
1015
  const pipe = await pipeline(task, modelId, opts);
1016
- pro.device = opts.device || "wasm";
1017
  return pipe;
1018
  } catch (err) {
1019
  lastErr = err;
@@ -1050,7 +1060,7 @@
1050
  pro.loading = true;
1051
  pro.tried = true;
1052
  els.accuracyBtn.disabled = true;
1053
- setPill(els.proDot, els.proStatus, auto ? "Accuracy pack: loading in background..." : "Accuracy pack: loading...", "loading");
1054
  if (!auto) showToast("Loading higher-accuracy transformer models. First load can take time.");
1055
 
1056
  try {
@@ -1064,7 +1074,7 @@
1064
  }
1065
 
1066
  try {
1067
- if (mod.AutoModel && mod.AutoProcessor && (mod.load_image || mod.RawImage?.fromURL)) {
1068
  const pair = await loadAgeGenderWithFallback(mod);
1069
  pro.ageGenderModel = pair.model;
1070
  pro.ageGenderProcessor = pair.processor;
@@ -1084,7 +1094,7 @@
1084
  }
1085
  } catch (err) {
1086
  console.error(err);
1087
- setPill(els.proDot, els.proStatus, "Accuracy pack unavailable; using core model", "error");
1088
  if (!auto) showToast("Accuracy pack could not load. The app will keep using the core model.");
1089
  } finally {
1090
  pro.loading = false;
 
485
  <div id="toast" class="toast" role="status" aria-live="polite"></div>
486
 
487
  <script type="module">
488
+ import { pipeline as xenovaPipeline, env as xenovaEnv } from "https://cdn.jsdelivr.net/npm/@xenova/transformers@2.17.2";
489
  const FACE_API_MODEL_URL = "https://cdn.jsdelivr.net/gh/justadudewhohacks/face-api.js@0.22.2/weights";
490
+ const TRANSFORMERS_CDN = "https://cdn.jsdelivr.net/npm/@xenova/transformers@2.17.2";
491
  const EMOTION_MODEL_ID = "Xenova/facial_emotions_image_detection";
492
+ const AGE_GENDER_MODEL_ID = null; // Phase 3B keeps age on the stable face-api model and improves it with smoothing + calibration.
493
 
494
  const els = {
495
  coreDot: document.getElementById("coreDot"),
 
631
  };
632
 
633
  const nonNeutralTop = Math.max(raw.happy, raw.sad, raw.fearful, raw.angry, raw.disgusted, raw.surprised);
634
+ const uncertainty = clamp01((1 - nonNeutralTop) * 0.10 + raw.surprised * 0.42 + raw.neutral * 0.05);
635
 
636
  return normalizeScores({
637
  Happy: Math.pow(raw.happy, 1.08),
 
639
  Fear: Math.max(Math.pow(raw.fearful, 1.04), raw.surprised * raw.fearful * 0.55),
640
  Anger: Math.pow(raw.angry, 1.03),
641
  Disgust: Math.pow(raw.disgusted, 1.02),
642
+ Confused: Math.min(0.42, uncertainty),
643
  });
644
  }
645
 
 
656
  else if (label.includes("angry") || label.includes("anger")) scores.Anger = Math.max(scores.Anger, score);
657
  else if (label.includes("disgust")) scores.Disgust = Math.max(scores.Disgust, score);
658
  else if (label.includes("surprise") || label.includes("neutral")) {
659
+ const scaled = label.includes("neutral") ? score * 0.10 : score * 0.55;
660
+ scores.Confused = Math.max(scores.Confused, Math.min(0.48, scaled));
661
  }
662
  }
663
  return normalizeScores(scores);
 
690
  const second = sorted[1] || ["", 0];
691
  let label = top[0];
692
  let score = top[1];
693
+ if (score < 0.18) {
694
  label = "Confused";
695
+ score = Math.max(scores.Confused || 0, 0.18);
696
+ } else if (score - second[1] < 0.035 && label !== "Confused") {
697
+ // When the visible expression is ambiguous, mark it as confused but keep confidence modest.
698
+ label = "Confused";
699
+ score = Math.max(Math.min(scores.Confused || 0, 0.34), 0.22);
700
  }
701
  return { label, score: clamp01(score) };
702
  }
 
999
  }
1000
 
1001
  async function getTransformersModule() {
1002
+ // Use the legacy Xenova build because the selected emotion model was published for it.
1003
+ // This avoids the Phase 3A failure where the newer package could import but could not initialize the model.
1004
+ const mod = { pipeline: xenovaPipeline, env: xenovaEnv };
1005
  if (mod.env) {
1006
  mod.env.allowLocalModels = false;
1007
+ mod.env.useBrowserCache = true;
1008
+ mod.env.backends ??= {};
1009
+ mod.env.backends.onnx ??= {};
1010
+ mod.env.backends.onnx.wasm ??= {};
1011
+ mod.env.backends.onnx.wasm.numThreads = Math.max(1, Math.min(4, navigator.hardwareConcurrency || 2));
1012
  }
1013
  return mod;
1014
  }
1015
 
1016
  async function loadPipelineWithFallback(pipeline, task, modelId) {
1017
+ const attempts = [
1018
+ { quantized: true, progress_callback: p => { if (p?.status) console.log("accuracy pack", p.status, p.file || ""); } },
1019
+ { quantized: true },
1020
+ {},
1021
+ ];
1022
  let lastErr = null;
1023
  for (const opts of attempts) {
1024
  try {
1025
  const pipe = await pipeline(task, modelId, opts);
1026
+ pro.device = "wasm";
1027
  return pipe;
1028
  } catch (err) {
1029
  lastErr = err;
 
1060
  pro.loading = true;
1061
  pro.tried = true;
1062
  els.accuracyBtn.disabled = true;
1063
+ setPill(els.proDot, els.proStatus, auto ? "Emotion accuracy pack: loading in background..." : "Emotion accuracy pack: loading...", "loading");
1064
  if (!auto) showToast("Loading higher-accuracy transformer models. First load can take time.");
1065
 
1066
  try {
 
1074
  }
1075
 
1076
  try {
1077
+ if (AGE_GENDER_MODEL_ID && mod.AutoModel && mod.AutoProcessor && (mod.load_image || mod.RawImage?.fromURL)) {
1078
  const pair = await loadAgeGenderWithFallback(mod);
1079
  pro.ageGenderModel = pair.model;
1080
  pro.ageGenderProcessor = pair.processor;
 
1094
  }
1095
  } catch (err) {
1096
  console.error(err);
1097
+ setPill(els.proDot, els.proStatus, "Emotion accuracy pack unavailable; using core model", "error");
1098
  if (!auto) showToast("Accuracy pack could not load. The app will keep using the core model.");
1099
  } finally {
1100
  pro.loading = false;