/* Studio 76 — Studio Bench. Upload a jewellery photo → AI spec, technical blueprint, front/side/top plate, and "see it worn" on a model (Instagram-ready). Talks only to blueprint.php (keys server-side). CAD export = coming soon. */ const WEAR = [ { k: 'face', label: 'Face' }, { k: 'neck', label: 'Neckline' }, { k: 'ear', label: 'Ear' }, { k: 'finger', label: 'Hand' }, { k: 'wrist', label: 'Wrist' }, { k: 'ankle', label: 'Ankle' }, { k: 'full', label: 'Full body' }, ]; const MODELS = [ { k: 'woman', label: 'On her' }, { k: 'man', label: 'On him' }, ]; /* aspect of a generated key, for the refine (perfect-it) pass */ function aspectFor(key) { if (key === 'views') return '3:2'; if (key.slice(0, 4) === 'mag_') return '3:4'; if (key.slice(0, 2) === 'w_') return '4:5'; return '1:1'; } const LOOKS = [ { k: 'editorial', label: 'Editorial' }, { k: 'indian', label: 'Indian' }, { k: 'millennial', label: 'Millennial' }, { k: 'glam', label: 'Glam' }, { k: 'studio', label: 'Studio' }, { k: 'goldenhour', label: 'Golden hour' }, ]; const FORMS = [ { k: 'ring', label: 'As a ring' }, { k: 'earrings', label: 'As earrings' }, { k: 'necklace', label: 'As a necklace' }, { k: 'bracelet', label: 'As a bracelet' }, { k: 'pendant', label: 'As a pendant' }, { k: 'bangle', label: 'As a bangle' }, ]; async function benchApi(action, payload) { const r = await fetch('blueprint.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(Object.assign({ action }, payload)) }); let j = {}; try { j = await r.json(); } catch (e) {} if (!r.ok || j.error) throw new Error(j.error || ('Something went wrong (' + r.status + ')')); return j; } function downscale(file, max) { return new Promise((res, rej) => { const fr = new FileReader(); fr.onerror = () => rej(new Error('read')); fr.onload = () => { const img = new Image(); img.onload = () => { const s = Math.min(1, max / Math.max(img.width, img.height)); const w = Math.round(img.width * s), h = Math.round(img.height * s); const c = document.createElement('canvas'); c.width = w; c.height = h; c.getContext('2d').drawImage(img, 0, 0, w, h); res(c.toDataURL('image/jpeg', 0.85)); }; img.onerror = () => rej(new Error('img')); img.src = fr.result; }; fr.readAsDataURL(file); }); } function isUrl(x) { return typeof x === 'string' && x.slice(0, 5) === 'data:'; } function Bench() { const [stage, setStage] = React.useState('idle'); // idle | analyzing | ready const [src, setSrc] = React.useState(null); const [id, setId] = React.useState(null); const [spec, setSpec] = React.useState(null); const [out, setOut] = React.useState({}); // {key: url|'loading'} const [look, setLook] = React.useState('editorial'); const [model, setModel] = React.useState('woman'); const [shots, setShots] = React.useState([]); // [{key, placeLabel, lookLabel}] const [morphs, setMorphs] = React.useState([]); // variations + forms [{key, label}] const [fixes, setFixes] = React.useState([]); // perfected takes [{key, label}] const [fixN, setFixN] = React.useState(0); const [fixText, setFixText] = React.useState(''); const [varN, setVarN] = React.useState(0); const [varText, setVarText] = React.useState(''); const [saving, setSaving] = React.useState(false); const [savedN, setSavedN] = React.useState(''); const [savedUrl, setSavedUrl] = React.useState(''); const [drag, setDrag] = React.useState(false); const [err, setErr] = React.useState(''); const [urlText, setUrlText] = React.useState(''); const [modalSrc, setModalSrc] = React.useState(''); const [modalKey, setModalKey] = React.useState(''); // generated key behind the zoomed image ('' = not refinable) const [cad, setCad] = React.useState(null); // {status, progress, glb, stl, obj} const [cadTex, setCadTex] = React.useState(null); // {status, progress, glb} const [cad3dView, setCad3dView] = React.useState('tex'); const fileRef = React.useRef(null); const cadTaskRef = React.useRef(null); const texTaskRef = React.useRef(null); const savedSetRef = React.useRef(null); // set id from the first save — re-saving UPDATES it (no duplicates) const savingRef = React.useRef(false); // synchronous double-click guard (state updates are async) async function handleFile(file) { if (!file || !/^image\//.test(file.type)) { setErr('Please choose an image file.'); return; } setErr(''); setStage('analyzing'); setOut({}); setSpec(null); setId(null); setSavedUrl(''); savedSetRef.current = null; try { const dataUrl = await downscale(file, 1280); setSrc(dataUrl); const j = await benchApi('analyze', { image: dataUrl }); setId(j.id); setSpec(j.spec); setStage('ready'); } catch (e) { setErr(e.message || 'Could not read that image.'); setStage('idle'); } } async function handleUrl(url) { url = (url || '').trim(); if (!/^https?:\/\//i.test(url)) { setErr('Paste a valid image URL.'); return; } setErr(''); setStage('analyzing'); setOut({}); setSpec(null); setId(null); setShots([]); setMorphs([]); setVarN(0); setSavedUrl(''); savedSetRef.current = null; setSrc(url); try { const j = await benchApi('analyze', { imageUrl: url }); setId(j.id); setSpec(j.spec); setStage('ready'); } catch (e) { setErr(e.message || 'Could not bring that image in.'); setStage('idle'); } } React.useEffect(() => { const u = new URLSearchParams(window.location.search).get('img'); if (u) handleUrl(u); }, []); async function gen(key, action, payload) { if (!id) return; setOut(o => Object.assign({}, o, { [key]: 'loading' })); try { const j = await benchApi(action, Object.assign({ id }, payload)); setOut(o => Object.assign({}, o, { [key]: j.dataUrl })); } catch (e) { setOut(o => Object.assign({}, o, { [key]: null })); setErr(e.message || 'Generation failed.'); } } function genWear(place) { const w = WEAR.find(x => x.k === place), l = LOOKS.find(x => x.k === look), m = MODELS.find(x => x.k === model); const key = 'w_' + place + '_' + look + '_' + model; const lookLabel = l.label + (model === 'man' ? ' · Him' : ''); if (!shots.find(s => s.key === key)) setShots(prev => [{ key, placeLabel: w.label, lookLabel }].concat(prev)); gen(key, 'wear', { place, look, model }); } function genMagazine() { const l = LOOKS.find(x => x.k === look); const key = 'mag_' + look + '_' + model; const lookLabel = l.label + (model === 'man' ? ' · Him' : ''); if (!shots.find(s => s.key === key)) setShots(prev => [{ key, placeLabel: 'Magazine cover', lookLabel }].concat(prev)); gen(key, 'magazine', { look, model }); } const aspectsRef = React.useRef({}); // fix_N → aspect inherited from the render it perfected function genFix() { const srcKey = modalKey, base = out[srcKey]; const text = fixText.trim(); if (!srcKey || !isUrl(base) || !text) return; const aspect = aspectsRef.current[srcKey] || aspectFor(srcKey); const n = fixN + 1; setFixN(n); const key = 'fix_' + n; aspectsRef.current[key] = aspect; const label = '“' + (text.length > 42 ? text.slice(0, 42) + '…' : text) + '”'; setFixes(prev => [{ key, label }].concat(prev)); setFixText(''); setModalSrc(''); setModalKey(''); setOut(o => Object.assign({}, o, { [key]: 'loading' })); benchApi('refine', { image: base, instructions: text, aspect }) .then(j => setOut(o => Object.assign({}, o, { [key]: j.dataUrl }))) .catch(e => { setOut(o => Object.assign({}, o, { [key]: null })); setErr(e.message || 'Refine failed.'); }); } function genVar() { if (!id) return; const n = varN + 1; setVarN(n); const text = varText.trim(); const key = 'var_' + n; const label = text ? ('“' + (text.length > 42 ? text.slice(0, 42) + '…' : text) + '”') : ('Variation ' + n); setMorphs(prev => [{ key, label }].concat(prev)); gen(key, 'variation', { n, instructions: text }); setVarText(''); } function genForm(form) { const f = FORMS.find(x => x.k === form), key = 'form_' + form; if (!morphs.find(m => m.key === key)) setMorphs(prev => [{ key, label: f.label }].concat(prev)); gen(key, 'reform', { form }); } async function save() { if (!id || saving || savingRef.current) return; savingRef.current = true; setSaving(true); setSavedUrl(''); setSavedN(''); setErr(''); try { const start = await benchApi('save_start', { id, spec, original: isUrl(src) ? src : '', setid: savedSetRef.current || undefined }); savedSetRef.current = start.setid; const items = []; if (isUrl(out.blueprint)) items.push({ key: 'blueprint', label: 'Blueprint', url: out.blueprint }); if (isUrl(out.views)) items.push({ key: 'views', label: 'Front · Side · Top', url: out.views }); shots.forEach(s => { if (isUrl(out[s.key])) items.push({ key: s.key, label: s.placeLabel + ' · ' + s.lookLabel, url: out[s.key] }); }); morphs.forEach(m => { if (isUrl(out[m.key])) items.push({ key: m.key, label: m.label, url: out[m.key] }); }); fixes.forEach(f => { if (isUrl(out[f.key])) items.push({ key: f.key, label: 'Perfected · ' + f.label, url: out[f.key] }); }); let n = 0; for (const it of items) { await benchApi('save_item', { setid: start.setid, key: it.key, label: it.label, dataUrl: it.url }); n++; setSavedN(n + '/' + items.length); } try { const k = 'foreve_sets', arr = JSON.parse(localStorage.getItem(k) || '[]').filter(s => s.id !== start.setid); arr.unshift({ id: start.setid, url: start.url, title: (spec && spec.piece_type) || 'Saved set', n: items.length, created: Date.now() }); localStorage.setItem(k, JSON.stringify(arr.slice(0, 60))); } catch (e) {} const modelTask = texTaskRef.current || cadTaskRef.current; if (modelTask && cad && cad.glb) { try { await benchApi('save_model', { setid: start.setid, taskId: modelTask }); } catch (e) {} } setSavedUrl(start.url); } catch (e) { setErr(e.message || 'Save failed.'); } savingRef.current = false; setSaving(false); } function cadStart() { if (!id) return; setCad({ status: 'starting', progress: 0 }); benchApi('cad_start', { id }).then(function (r) { const tid = r.taskId; cadTaskRef.current = tid; const poll = function () { benchApi('cad_status', { taskId: tid }).then(function (s) { setCad(s); if (s.status !== 'SUCCEEDED' && s.status !== 'FAILED') setTimeout(poll, 5000); }).catch(function () { setTimeout(poll, 6000); }); }; setTimeout(poll, 4000); }).catch(function (e) { setErr(e.message || '3D conversion failed.'); setCad(null); }); } function texStart() { if (!cadTaskRef.current) return; setCadTex({ status: 'starting', progress: 0 }); benchApi('cad_texture', { taskId: cadTaskRef.current, spec }).then(function (r) { const tt = r.texTaskId; texTaskRef.current = tt; const poll = function () { benchApi('cad_texture_status', { taskId: tt }).then(function (s) { setCadTex(s); if (s.glb) setCad3dView('tex'); if (s.status !== 'SUCCEEDED' && s.status !== 'FAILED') setTimeout(poll, 5000); }).catch(function () { setTimeout(poll, 6000); }); }; setTimeout(poll, 4000); }).catch(function (e) { setErr(e.message || 'Texture pass failed.'); setCadTex(null); }); } function reset() { setStage('idle'); setSrc(null); setId(null); setSpec(null); setOut({}); setShots([]); setMorphs([]); setFixes([]); setFixN(0); setFixText(''); setModalSrc(''); setModalKey(''); aspectsRef.current = {}; setVarN(0); setSaving(false); setSavedN(''); setSavedUrl(''); setCad(null); setCadTex(null); setCad3dView('tex'); texTaskRef.current = null; savedSetRef.current = null; savingRef.current = false; setErr(''); } const heroStone = spec && spec.hero_stone ? [spec.hero_stone.carat_estimate, spec.hero_stone.cut, spec.hero_stone.color, spec.hero_stone.type].filter(Boolean).join(' ') : ''; const dims = spec && spec.estimated_dimensions_mm ? [spec.estimated_dimensions_mm.height, spec.estimated_dimensions_mm.width, spec.estimated_dimensions_mm.depth].filter(Boolean).join(' × ') : ''; const accents = spec && spec.accent_stones && spec.accent_stones.length ? spec.accent_stones.map(a => [a.count, a.cut, a.type].filter(Boolean).join(' ')).filter(Boolean).join(', ') : ''; const Row = ({ l, v }) => v ?
{l}{v}
: null; const Tile = ({ keyName, title, sub }) => { const v = out[keyName]; return (
{v === 'loading' ?
{sub || 'Working…'}
: (typeof v === 'string') ? {title} { setModalSrc(v); setModalKey(keyName); setFixText(''); }} /> :
{sub}
}
{title} {(typeof v === 'string' && v !== 'loading') && Download}
); }; return (
Studio 76 My sets✦ Studio Bench

From a photo to a blueprint.

Upload any piece of jewellery — a photo, a screenshot, a memory you saved. Our bench reads it, drafts a technical blueprint, breaks down the spec, and even shows it worn on a model, ready for the grid.

{stage === 'idle' && (
fileRef.current && fileRef.current.click()} onDragOver={e => { e.preventDefault(); setDrag(true); }} onDragLeave={() => setDrag(false)} onDrop={e => { e.preventDefault(); setDrag(false); handleFile(e.dataTransfer.files[0]); }}>

Drop a photo here, or browse

JPG / PNG · a clear, well-lit shot works best

handleFile(e.target.files[0])} />
)} {stage === 'idle' && (
setUrlText(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') handleUrl(urlText); }} /> Get the Pinterest clipper ↗
)} {err &&

{err}

} {stage !== 'idle' && (
{/* left: original + spec */} {/* right: generators */}
{savedUrl ? View / share your set ↗ : Stores the spec, blueprint & every render at a shareable link.}
✦ See it worn — Instagram-ready

Pick who wears it and a look, then tap a placement. Mix & match to build a shoot.

{MODELS.map(m => ( ))} {LOOKS.map(l => ( ))}
{WEAR.map(w => { const k = 'w_' + w.k + '_' + look + '_' + model; return ( ); })} {(() => { const k = 'mag_' + look + '_' + model; return ( ); })()}
{shots.length > 0 && (
{shots.map(s => )}
)}
✦ Make it a set — variations & forms

Tell us what to change, or just spin off a fresh take — then reimagine it as a matching ring, earrings, necklace…

setVarText(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') genVar(); }} />
{FORMS.map(f => { const k = 'form_' + f.k; return ; })}
{morphs.length > 0 && (
{morphs.map(m => )}
)}
{fixes.length > 0 && (
✦ Perfected takes

Corrected renders — zoom any image and tell the bench what to fix to make more.

{fixes.map(f => )}
)}
✦ Convert to 3D — spin it in real gold light {!cad && (

Turn your piece into a rotatable 3D model — orbit it, zoom in, even view it in AR. A high-detail concept mesh from your blueprint; open-work & cut-outs are finalised on CAD. A head start for your jeweller, not yet castable.

)} {cad && cad.status !== 'SUCCEEDED' && cad.status !== 'FAILED' && (

Sculpting your 3D model… {cad.progress || 0}%

)} {cad && cad.status === 'FAILED' &&

3D conversion didn’t complete — .

} {cad && cad.glb && (
✦ 3D · drag to spin {cadTex && cadTex.glb && (
)}
Export: .glb {cad.stl && .stl} {cad.obj && .obj} {!cadTex && } {cadTex && cadTex.status !== 'SUCCEEDED' && cadTex.status !== 'FAILED' && Texturing… {cadTex.progress || 0}%} {cadTex && cadTex.status === 'FAILED' && }
)}
)} {modalSrc && (
{ setModalSrc(''); setModalKey(''); }}> × e.stopPropagation()} /> {modalKey && isUrl(out[modalKey]) && (
e.stopPropagation()}> setFixText(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') genFix(); }} />
)}
)}
); } ReactDOM.createRoot(document.getElementById('root')).render();