← back to Dwjs Consolidation 2026 04 23

spin_detect_delete.js

107 lines

// Parallel header-detection on Hollywood spin.gifs.
// 16 concurrent downloads → first-frame extract via Python helper → variance check → delete via Shopify API.
// Resume-safe: writes /tmp/hw_spin_state.ndjson with verdict per product.
const fs = require('fs');
const path = require('path');
const { execSync, spawn } = require('child_process');
const TOKEN = (process.env.SHOPIFY_ADMIN_TOKEN || '');
const SHOP = 'designer-laboratory-sandbox.myshopify.com';
const API = `https://${SHOP}/admin/api/2024-10/graphql.json`;
const STATE = '/tmp/hw_spin_state.ndjson';
const SAMPLE_DIR = '/tmp/hw_spin_frames';
fs.mkdirSync(SAMPLE_DIR, { recursive: true });

const targets = JSON.parse(fs.readFileSync('/tmp/hw_spin_targets.json','utf8'));
console.log(`Targets: ${targets.length}`);

// Skip already-processed
const done = new Set();
if (fs.existsSync(STATE)) {
  fs.readFileSync(STATE,'utf8').trim().split('\n').forEach(l=>{ try { const r=JSON.parse(l); done.add(r.id); } catch{} });
}
const todo = targets.filter(t=>!done.has(t.id));
console.log(`Already processed: ${done.size}, Remaining: ${todo.length}\n`);

const stateStream = fs.createWriteStream(STATE,{flags:'a'});
const CONC = 16;

async function gql(q,v) {
  const r = await fetch(API,{method:'POST',headers:{'X-Shopify-Access-Token':TOKEN,'Content-Type':'application/json'},body:JSON.stringify({query:q,variables:v})});
  return r.json();
}

let processed=0, hasHeader=0, clean=0, downloadFail=0, deleteFail=0;
const start = Date.now();

async function downloadAndDetect(url, idx) {
  const localGif = path.join(SAMPLE_DIR, `s_${idx}.gif`);
  const localPng = path.join(SAMPLE_DIR, `s_${idx}.png`);
  try {
    // Download with redirect follow
    const buf = await fetch(url).then(r=>r.arrayBuffer());
    fs.writeFileSync(localGif, Buffer.from(buf));
    // Extract frame 0 + score in one Python call
    const out = execSync(`python3 -c "
from PIL import Image
import numpy as np
img = Image.open('${localGif}').convert('RGB')
img.seek(0)
arr = np.array(img)
h, w = arr.shape[:2]
strip = arr[5:30, int(w*0.15):int(w*0.85), :].mean(axis=2)
std = float(strip.std())
rng = int(strip.max() - strip.min())
print(f'{std:.1f},{rng}')
"`, { timeout: 10000 }).toString().trim();
    const [stdStr, rngStr] = out.split(',');
    const std = parseFloat(stdStr);
    const rng = parseInt(rngStr);
    fs.unlinkSync(localGif);
    return { std, rng, hasHeader: std > 60 && rng > 180 };
  } catch (e) {
    return { error: e.message };
  }
}

async function deleteMedia(productId, mediaIds) {
  // productDeleteMedia(productId, mediaIds: [ID!]!)
  const q = `mutation($p:ID!,$ids:[ID!]!){productDeleteMedia(productId:$p,mediaIds:$ids){deletedMediaIds mediaUserErrors{field message}}}`;
  const j = await gql(q, { p: productId, ids: mediaIds });
  return { ok: !j.errors && !j.data?.productDeleteMedia?.mediaUserErrors?.length, raw: j };
}

async function worker(workerId, queue) {
  while (queue.length) {
    const t = queue.shift();
    if (!t) break;
    const idx = `${workerId}_${Date.now()}`;
    // Use the first spin URL (most products have only one)
    const result = await downloadAndDetect(t.spinUrls[0], idx);
    if (result.error) {
      downloadFail++;
      stateStream.write(JSON.stringify({ id:t.id, status:t.status, dlError:result.error, ts:new Date().toISOString() })+'\n');
    } else if (result.hasHeader) {
      // DETECT-ONLY: log verdict, save sample frame, do not delete
      hasHeader++;
      stateStream.write(JSON.stringify({ id:t.id, status:t.status, std:result.std, rng:result.rng, hasHeader:true, spinUrls:t.spinUrls, spinIds:t.spinIds, ts:new Date().toISOString() })+'\n');
    } else {
      clean++;
      stateStream.write(JSON.stringify({ id:t.id, status:t.status, std:result.std, rng:result.rng, hasHeader:false, ts:new Date().toISOString() })+'\n');
    }
    processed++;
    if (processed % 50 === 0) {
      const elapsed = (Date.now() - start)/1000;
      const rate = (processed/elapsed).toFixed(1);
      const remaining = queue.length;
      console.log(`  processed=${processed} header=${hasHeader} clean=${clean} dlFail=${downloadFail} delFail=${deleteFail} | rate=${rate}/s | rem=${remaining}`);
    }
  }
}

(async () => {
  const queue = [...todo];
  await Promise.all(Array.from({length:CONC},(_,i)=>worker(i,queue)));
  stateStream.end();
  console.log(`\nDONE. processed=${processed} header(deleted)=${hasHeader} clean=${clean} dlFail=${downloadFail} delFail=${deleteFail}`);
})();