← back to Big Red
snapshot: 4 file(s) changed, +2 new, ~2 modified
95b5ea08bd8c9b701ef88117487a4099afcfc6cd · 2026-05-13 08:57:46 -0700 · Steve
Files touched
A .selected-modelA data/price-verifications.jsonM public/desktop.htmlM public/phone.html
Diff
commit 95b5ea08bd8c9b701ef88117487a4099afcfc6cd
Author: Steve <steve@designerwallcoverings.com>
Date: Wed May 13 08:57:46 2026 -0700
snapshot: 4 file(s) changed, +2 new, ~2 modified
---
.selected-model | 1 +
data/price-verifications.json | 17 +++++++
public/desktop.html | 106 ++++++++++++++++++++++++++++++++++++++++--
public/phone.html | 2 +-
4 files changed, 122 insertions(+), 4 deletions(-)
diff --git a/.selected-model b/.selected-model
new file mode 100644
index 0000000..6a20541
--- /dev/null
+++ b/.selected-model
@@ -0,0 +1 @@
+hermes3:8b
diff --git a/data/price-verifications.json b/data/price-verifications.json
new file mode 100644
index 0000000..30c143c
--- /dev/null
+++ b/data/price-verifications.json
@@ -0,0 +1,17 @@
+{
+ "DWKK-150018": {
+ "price": 154.35,
+ "source": "shopify-admin-live",
+ "verified_at": "2026-05-12T13:39:06.515Z"
+ },
+ "DWSC-700068": {
+ "price": 425.41,
+ "source": "shopify-admin-live",
+ "verified_at": "2026-05-12T13:39:06.871Z"
+ },
+ "DWSC-700105": {
+ "price": 474.35,
+ "source": "shopify-admin-live",
+ "verified_at": "2026-05-12T13:39:07.177Z"
+ }
+}
\ No newline at end of file
diff --git a/public/desktop.html b/public/desktop.html
index 08105e7..5dc5f4c 100644
--- a/public/desktop.html
+++ b/public/desktop.html
@@ -87,6 +87,11 @@
transform: scaleY(calc(0.15 + var(--speak, 0) * 1));
transition: transform 80ms ease-out;
}
+ /* Photo-avatar mouth overlay uses its own (smaller) viewBox-centered origin */
+ #photoMouthOverlay .mouth-inner {
+ transform-origin: 60px 25px;
+ transform: scaleY(calc(0.15 + var(--speak, 0) * 1.6));
+ }
/* Subtle idle bob */
@keyframes bob { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-6px); } }
.avatar { animation: bob 6s ease-in-out infinite; }
@@ -257,8 +262,19 @@
<span class="pill" id="pillPhone"><span class="dot" id="dotPhone"></span>phone</span>
</div>
- <div class="avatar-wrap">
- <svg class="avatar" viewBox="0 0 500 525" xmlns="http://www.w3.org/2000/svg">
+ <div class="avatar-wrap" style="position:relative">
+ <!-- Photo avatar (shown when /api/avatar exists, hidden otherwise) -->
+ <img id="photoAvatar" alt="Steve" style="display:none;width:100%;max-width:500px;aspect-ratio:1/1.05;object-fit:cover;object-position:center 30%;border-radius:12px;box-shadow:0 0 60px rgba(232,65,71,.35);"/>
+ <!-- Animated mouth overlay (positioned over the photo's mouth area, scales with --speak) -->
+ <div id="photoMouthOverlay" style="display:none;position:absolute;left:50%;top:72%;transform:translate(-50%,-50%);pointer-events:none">
+ <svg width="120" height="50" viewBox="0 0 120 50">
+ <ellipse cx="60" cy="25" rx="42" ry="6" fill="rgba(58,10,12,.0)" stroke="rgba(255,255,255,0)" stroke-width="0"/>
+ <ellipse class="mouth-inner" cx="60" cy="25" rx="38" ry="9" fill="#3a0a0c" opacity=".75"
+ style="filter:drop-shadow(0 2px 6px rgba(0,0,0,.5))"/>
+ </svg>
+ </div>
+ <!-- Default SVG avatar (hidden when photo avatar is set) -->
+ <svg class="avatar" id="svgAvatar" viewBox="0 0 500 525" xmlns="http://www.w3.org/2000/svg">
<defs>
<radialGradient id="skinGrad" cx="50%" cy="40%" r="60%">
<stop offset="0%" stop-color="#fbe0c8"/>
@@ -390,6 +406,12 @@
<button class="btn" id="analyzeBtn" disabled>Analyze frame</button>
</div>
<div class="analyze-result" id="analyzeResult">No analysis yet.</div>
+
+ <div class="analyze-row" style="margin-top:8px">
+ <button class="btn" id="captureAvatarBtn" disabled style="background:#a02320;flex:1">📸 Make Me the Avatar</button>
+ <button class="btn" id="resetAvatarBtn" style="background:#444;flex:0 0 auto">↺ Reset</button>
+ </div>
+ <div class="analyze-result" id="avatarStatus" style="font-size:11px">Pair phone, then click "Make Me the Avatar" to capture your face.</div>
</div>
</div>
</aside>
@@ -441,6 +463,7 @@
src.connect(analyser);
analyser.connect(audioCtx.destination);
const data = new Uint8Array(analyser.frequencyBinCount);
+ let sineFallbackUsed = 0;
function tick() {
analyser.getByteTimeDomainData(data);
let sum = 0;
@@ -449,7 +472,17 @@
sum += v * v;
}
const rms = Math.sqrt(sum / data.length);
- setSpeak(rms * 4);
+ // Fallback: if analyser RMS is silent (some browsers/CORS edge cases) but
+ // audio is playing, drive the mouth with a sine wave so it still animates.
+ if (rms < 0.005 && !audioEl.paused && !audioEl.ended && audioEl.currentTime > 0.05) {
+ sineFallbackUsed++;
+ const t = audioEl.currentTime;
+ // Layered sines + smooth random envelope = natural-looking talk shape
+ const env = 0.55 + 0.4 * Math.abs(Math.sin(t * 7.3) * Math.cos(t * 3.1) + Math.sin(t * 11.7) * 0.4);
+ setSpeak(env);
+ } else {
+ setSpeak(rms * 4);
+ }
speakRaf = requestAnimationFrame(tick);
}
if (speakRaf) cancelAnimationFrame(speakRaf);
@@ -616,6 +649,7 @@
phoneVideo.srcObject = phoneStream;
videoEmpty.style.display = 'none';
analyzeBtn.disabled = false;
+ if (typeof captureAvatarBtn !== 'undefined' && captureAvatarBtn) captureAvatarBtn.disabled = false;
dotPhone.classList.add('ok');
});
pc.addEventListener('connectionstatechange', () => {
@@ -632,9 +666,75 @@
phoneVideo.srcObject = null;
videoEmpty.style.display = 'flex';
analyzeBtn.disabled = true;
+ if (typeof captureAvatarBtn !== 'undefined' && captureAvatarBtn) captureAvatarBtn.disabled = true;
dotPhone.classList.remove('ok');
}
+ // -------- avatar capture (use phone-cam frame as Steve's face) --------
+ const photoAvatar = $('photoAvatar');
+ const photoMouthOverlay = $('photoMouthOverlay');
+ const svgAvatar = $('svgAvatar');
+ const captureAvatarBtn = $('captureAvatarBtn');
+ const resetAvatarBtn = $('resetAvatarBtn');
+ const avatarStatus = $('avatarStatus');
+
+ function showPhotoAvatar(url) {
+ photoAvatar.src = url;
+ photoAvatar.style.display = '';
+ photoMouthOverlay.style.display = '';
+ svgAvatar.style.display = 'none';
+ }
+ function showSvgAvatar() {
+ photoAvatar.style.display = 'none';
+ photoMouthOverlay.style.display = 'none';
+ svgAvatar.style.display = '';
+ }
+
+ // Load any existing avatar on boot
+ fetch('/api/avatar').then(r => r.json()).then(j => {
+ if (j.exists) { showPhotoAvatar(j.url); avatarStatus.textContent = 'Photo avatar active. Click Reset to go back to red-haired Steve.'; }
+ }).catch(() => {});
+
+ // Enable the capture button only when phone is paired and streaming
+ function refreshAvatarBtn() {
+ captureAvatarBtn.disabled = !phoneStream;
+ }
+
+ captureAvatarBtn.addEventListener('click', async () => {
+ if (!phoneStream) { avatarStatus.textContent = 'Pair your phone first.'; return; }
+ const v = phoneVideo;
+ captureCanvas.width = v.videoWidth || 1280;
+ captureCanvas.height = v.videoHeight || 720;
+ const ctx = captureCanvas.getContext('2d');
+ ctx.drawImage(v, 0, 0, captureCanvas.width, captureCanvas.height);
+ const dataUrl = captureCanvas.toDataURL('image/jpeg', 0.92);
+ captureAvatarBtn.disabled = true;
+ avatarStatus.textContent = 'Saving your face as the avatar…';
+ try {
+ const r = await fetch('/api/avatar/capture', {
+ method: 'POST', headers: { 'content-type': 'application/json' },
+ body: JSON.stringify({ dataUrl })
+ });
+ const j = await r.json();
+ if (j.ok) {
+ showPhotoAvatar(j.url);
+ avatarStatus.textContent = 'Avatar set. Mouth will animate when speaking.';
+ } else {
+ avatarStatus.textContent = 'Failed: ' + (j.error || 'unknown');
+ }
+ } catch (e) {
+ avatarStatus.textContent = 'Error: ' + e.message;
+ } finally {
+ captureAvatarBtn.disabled = !phoneStream;
+ }
+ });
+
+ resetAvatarBtn.addEventListener('click', async () => {
+ await fetch('/api/avatar', { method: 'DELETE' });
+ showSvgAvatar();
+ avatarStatus.textContent = 'Reset — using default red-haired SVG.';
+ });
+
// -------- frame analysis --------
analyzeBtn.addEventListener('click', async () => {
if (!phoneStream) return;
diff --git a/public/phone.html b/public/phone.html
index b994118..d0a8b54 100644
--- a/public/phone.html
+++ b/public/phone.html
@@ -98,7 +98,7 @@
try { await getStream(); }
catch (e) {
errEl.style.display = 'block';
- errEl.textContent = 'Camera error: ' + e.message + '. Make sure you are on http://' + location.host + ' (not https), and grant camera access.';
+ errEl.textContent = 'Camera error: ' + e.message + '. iOS Safari requires HTTPS — open the https:// cloudflared URL on your phone, not http://192.168.x.x. Then grant camera access.';
return;
}
setStatus('connecting', false);
← 6688ef1 feat(big-red/tick3): SKU resolver fallback into sister catal
·
back to Big Red
·
big-red: lower-left launcher + woman-avatar fallback + retai 2335ec4 →