const { useState, useEffect, useRef } = React; const cx = (...a) => a.filter(Boolean).join(' '); const q = (k) => new URLSearchParams(location.search).get(k); const S2 = q('s') === '2'; // 评审:直接进结果 const G = q('g') === '1'; // 评审:冻结在合成中 const REDUCE = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); function Btn({ v = 'glass', size, icon, block, children, ...rest }) { return ( ); } function Phone({ children }) { return (
{children}
); } /* ============ AI 客厅合成 ============ */ const MODELS = [ { id: 'e8s', name: '海信 85 E8S', short: '海信 E8S', panel: 'RGB-MiniLED' }, { id: 'g5', name: 'LG 65 G5', short: 'LG G5', panel: 'OLED evo' }, { id: 'sny', name: 'Sony BRAVIA 8 II', short: 'Sony B8 II', panel: 'QD-OLED' }, { id: 'tcl', name: 'TCL 85 Q10K', short: 'TCL Q10K', panel: 'Mini LED' }, ]; const REF = 85; // 预览样片里那台电视的实际尺寸(英寸) const DISTS = [240, 280, 320]; // 沙发距离候选(cm),默认 280 // 电视在照片卡中的位置(卡与照片同为 4:3,坐标即百分比) const TVRECT = { wall: { cx: 62, cy: 40.5, w: 33.5, h: 28 }, stand: { cx: 61.5, cy: 45.5, w: 34, h: 28 } }; const viewAngle = (size, dist) => Math.round(2 * Math.atan((size * 2.54 * 0.8716 / 2) / dist) * 180 / Math.PI); const fitLabel = (a) => a < 28 ? { t: '偏小', cls: 'warn' } : a <= 40 ? { t: '推荐', cls: 'ok' } : { t: '偏大', cls: 'warn' }; const T = { toResult: 1300 }; // 需 ≥ 所有合成动画收尾(bloom .3+.9=1.2s / scan .1+1.15=1.25s);reduced 时快进 function ComposeApp({ onBack }) { const [phase, setPhase] = useState(G ? 'gen' : S2 ? 'result' : 'capture'); // capture | gen | result const [size, setSize] = useState(q('sz') ? +q('sz') : REF); const [midx, setMidx] = useState(0); const [mount, setMount] = useState(q('m') === 'stand' ? 'stand' : 'wall'); const [dist, setDist] = useState(280); const timers = useRef([]); const clear = () => { timers.current.forEach(clearTimeout); timers.current = []; }; useEffect(() => clear, []); const shoot = () => { if (phase !== 'capture') return; setPhase('gen'); timers.current.push(setTimeout(() => setPhase('result'), REDUCE ? 320 : T.toResult)); }; const reset = () => { clear(); setSize(REF); setPhase('capture'); }; const model = MODELS[midx]; const angle = viewAngle(size, dist), fit = fitLabel(angle); const wCm = Math.round(size * 2.54 * 0.8716), hCm = Math.round(wCm * 9 / 16); const shown = phase !== 'capture'; const resultSrc = mount === 'wall' ? IMG.wall : IMG.stand; // AR 尺寸框:以样片实际尺寸(REF)为基准,按所选尺寸等比缩放,套在真实电视位置上 const tvr = TVRECT[mount], k = size / REF; const fp = { left: tvr.cx - tvr.w * k / 2, top: tvr.cy - tvr.h * k / 2, width: tvr.w * k, height: tvr.h * k }; // 回传 Agent 的结构化载荷(chip 只是它的可视摘要) const payload = { size_in: size, width_cm: wCm, mount, model_id: model.id, panel: model.panel, distance_cm: dist, view_angle: angle, fit: fit.t }; const chips = [`${size}″`, model.panel, mount === 'wall' ? '挂墙' : '底座', `${(dist / 100).toFixed(1)}m`, `视角 ${angle}°`, fit.t]; return (
{/* 顶栏 */}
{onBack && }
AI 客厅合成
{phase === 'capture' ? '拍下你家电视墙' : phase === 'gen' ? '正在生成合成预览…' : '等比预览 · 尺寸/视角为精算值'}
{phase === 'capture' ? '拍摄' : phase === 'gen' ? '合成中' : '预览'}
{/* 照片卡 */}
客厅原图 挂墙 底座 {phase === 'capture' && (
对准电视墙 · 保持水平
)} {phase === 'gen' && ( )} {phase === 'result' && ( {/* AR 尺寸框:等比套住电视,随尺寸缩放 → 所选即所见 */}
{size}″ · {wCm}×{hCm}cm
{size}″ ≈ 视角 {angle}° {fit.t}
)}
正在生成 {model.short} 的合成预览
{/* 拍摄条 */}
拍下电视墙,AI 生成等比合成预览;尺寸 / 视角为精算值
{/* 调节面板 */}
{[['wall', '挂墙', 'monitor'], ['stand', '底座', 'layers']].map(([kk, l, ic]) => ( ))}
{MODELS.map((m, i) => ( ))}
目标尺寸{size}″ · {wCm}×{hCm}cm
setSize(+e.target.value)} />
沙发距离
{DISTS.map((d) => )}
视角 {angle}° {fit.t}
回传 Agent
{chips.map((c) => {c})}
重拍 满意 · 存入清单
); } /* ============ 采光 · 测光(前台工具 #3) ============ 两轴分离:照度轴 = 锁曝光 EV 反推 lux(快测=估算档,补齐三时段=可信档); 反光轴 = AI 识别光源位置(对屏/侧向/顶部)。两轴合成抗反与亮度建议。 */ const LMIN = 30, LMAX = 3000; const logT = (v) => Math.min(1, Math.max(0, (Math.log(v) - Math.log(LMIN)) / (Math.log(LMAX) - Math.log(LMIN)))); const lux = (ev) => Math.round(2.5 * Math.pow(2, ev)); // ISO 2720:E ≈ 2.5·2^EV(ISO100) const SLOTS = [ { k: 'day', l: '白天', ic: 'sun', ev: 7.0 }, { k: 'dusk', l: '傍晚', ic: 'sunset', ev: 4.5 }, { k: 'night', l: '夜晚', ic: 'moon', ev: 3.8 }, ]; /* agent 识别出的反光源(box 为照片内百分比,按真实样片标注) */ const LSRC = [ { id: 'win2', ph: 2, n: '对屏侧窗', risk: 'hi', box: { x: 66, y: 10, w: 27, h: 52 } }, { id: 'lamp2', ph: 2, n: '落地灯', risk: 'mid', box: { x: 1, y: 25, w: 11.5, h: 38 } }, { id: 'spot2', ph: 2, n: '筒灯×2', risk: 'mid', box: { x: 12.5, y: 1.5, w: 11.5, h: 8.5 } }, { id: 'win1', ph: 1, n: '侧窗', risk: 'mid', box: { x: 0.6, y: 8, w: 20, h: 50 } }, { id: 'spot1', ph: 1, n: '筒灯×3', risk: 'mid', box: { x: 43, y: 1.5, w: 21, h: 11.5 } }, { id: 'cove1', ph: 1, n: '灯带', risk: 'lo', box: { x: 26, y: 6.5, w: 71, h: 6 } }, ]; const RISKL = { hi: '高', mid: '中', lo: '低' }; function ABox({ s, i }) { const b = s.box; return (
{s.n} · {RISKL[s.risk]}
); } function LightApp({ onBack }) { const LPS = ['turn', 'cap2', 'ana', 'result']; const lp0 = LPS.includes(q('lp')) ? q('lp') : 'cap1'; const [phase, setPhase] = useState(lp0); // cap1 | m1 | turn | cap2 | m2 | ana | result const [ev, setEv] = useState(null); const [pv, setPv] = useState(q('pv') === '1' ? 1 : 2); // 结果页当前照片 const [busySlot, setBusySlot] = useState(null); const [meas, setMeas] = useState(() => { const m = { day: null, dusk: null, night: null }; if (lp0 !== 'cap1') m.day = lux(SLOTS[0].ev); if (q('slots') === '3') SLOTS.forEach((s) => { m[s.k] = lux(s.ev); }); return m; }); const timers = useRef([]); const T2 = (fn, ms) => timers.current.push(setTimeout(fn, REDUCE ? Math.min(ms, 240) : ms)); useEffect(() => () => timers.current.forEach(clearTimeout), []); // 锁曝光测光:EV 读数 ease-out 逼近目标,落点回调(clearTimeout 亦可清 interval) const runMeter = (target, done) => { let i = 0; const N = REDUCE ? 4 : 18; setEv(Math.round((target - 3.6) * 10) / 10); const id = setInterval(() => { i++; const k = 1 - i / N; setEv(Math.round((target - 3.6 * k * k) * 10) / 10); if (i >= N) { clearInterval(id); done(); } }, 46); timers.current.push(id); }; const shoot1 = () => { if (phase !== 'cap1') return; setPhase('m1'); runMeter(SLOTS[0].ev, () => { setMeas((m) => ({ ...m, day: lux(SLOTS[0].ev) })); T2(() => { setEv(null); setPhase('turn'); }, 460); }); }; const shoot2 = () => { if (phase !== 'cap2') return; setPhase('m2'); runMeter(7.4, () => T2(() => { setEv(null); setPhase('ana'); T2(() => setPhase('result'), REDUCE ? 320 : 1450); }, 400)); }; const reset = () => { timers.current.forEach(clearTimeout); timers.current = []; setMeas({ day: null, dusk: null, night: null }); setEv(null); setPv(2); setBusySlot(null); setPhase('cap1'); }; const fillSlot = (k) => { if (meas[k] != null || busySlot) return; setBusySlot(k); T2(() => { setMeas((m) => ({ ...m, [k]: lux(SLOTS.find((s) => s.k === k).ev) })); setBusySlot(null); }, 900); }; const filled = SLOTS.filter((s) => meas[s.k] != null); const cred = filled.length === 3; // 置信阶梯:3 时段 → 可信档 const dayLux = meas.day ?? lux(SLOTS[0].ev); const band = [Math.round(dayLux / 1.414), Math.round(dayLux * 1.414)]; // ±半档 EV const vmin = Math.min(...filled.map((s) => meas[s.k])), vmax = Math.max(...filled.map((s) => meas[s.k])); // 二维结论:反光轴(光源位置)× 照度轴(白天上限) const hiG = LSRC.some((s) => s.risk === 'hi'), midG = LSRC.some((s) => s.risk === 'mid'); const arV = hiG ? '必要' : midG ? '建议' : '非必需'; const brV = dayLux > 200 ? '峰值 ≥1000 nits' : dayLux > 100 ? '峰值 ≥600 nits' : '无高亮需求'; const chips = [cred ? `全天 ${vmin}–${vmax}lx·可信档` : `白天 ${dayLux}lx·估算档`, '对屏侧窗·高', `抗反·${arV}`, dayLux > 200 ? '≥1000nits' : brV]; const capturing = ['cap1', 'm1', 'turn', 'cap2', 'm2'].includes(phase); const img2on = phase === 'result' ? pv === 2 : ['cap2', 'm2', 'ana'].includes(phase); const bg = img2on ? (IMG.rev || IMG.orig) : IMG.orig; const wrapCls = phase === 'result' ? 'p-lresult' : phase === 'ana' ? 'p-gen' : 'p-capture'; const subT = { cap1: '第 1 拍:朝电视墙,锁曝光测照度', m1: '锁曝光 · EV 反推照度…', turn: '转身 180°,拍屏幕对面', cap2: '第 2 拍:对屏方向', m2: '锁曝光 · EV 反推照度…', ana: 'AI 正在识别反光源…', result: cred ? '三时段齐 · 可信档结论' : '快测结论 · 补测时段可升可信档' }[phase]; const stepT = { cap1: '测光①', m1: '测光①', turn: '转身', cap2: '测光②', m2: '测光②', ana: '识别中', result: '结果' }[phase]; return (
{onBack && }
采光 · 测光
{subT}
{stepT}
朝电视墙
{IMG.rev ? 朝沙发方向 :
「朝沙发方向」样片待生成prompts/01-scene-reverse-sofa.md
生成后放 aiimage/朝沙发.png,跑 python _mkimg.py
}
{(phase === 'cap1' || phase === 'cap2') && (
{phase === 'cap1' ? '① 朝电视墙 · 含吸顶灯' : '② 对屏方向 · 含窗与灯'}
)} {(phase === 'm1' || phase === 'm2') && (
AE-L · EV {(ev ?? 0).toFixed(1)} ≈ {lux(ev ?? 0)} lx
)} {phase === 'turn' && (
setPhase('cap2')}>
转身 180°
屏幕反射的是它对面的东西——窗、落地灯都在你身后这一侧,拍下来交给 AI 认
{ e.stopPropagation(); setPhase('cap2'); }}>已转身 · 拍对屏方向
)} {phase === 'ana' && ( )} {phase === 'result' && LSRC.filter((s) => s.ph === pv).map((s, i) => )} {phase === 'result' && (
{[[1, '① 电视墙'], [2, '② 对屏']].map(([n, l]) => ( ))}
)}
AI 正在识别反光源与位置…
{/* 拍摄条 */}
{phase === 'turn' ? '转身后拍第 2 张:屏幕会反射对面的窗和灯' : phase === 'cap2' || phase === 'm2' ? 'AI 识别对屏反光源(窗 / 落地灯 / 顶灯)' : '锁定曝光,由 EV 反推环境照度(估算档 ±半档)'}
{/* 结果面板 */}
{cred ? `${vmin}–${vmax} lx` : `${dayLux} lx`}
{cred ? '全天照度跨度 · 三时段实测' : `白天快测 · 约 ${band[0]}–${band[1]} lx(±半档 EV)`}
{cred ? '可信档 · 3/3' : `估算档 · ${filled.length}/3 时段`}
{!cred &&
} {[50, 200].map((v) => )} {[['暗', 40], ['中', 100], ['亮', 700]].map(([l, v]) => {l} {l === '暗' ? '<50' : l === '中' ? '50–200' : '>200'})} {filled.map((s) => )}
{SLOTS.map((s) => ( ))}
反光源 · AI 识别 {LSRC.length} 处
{LSRC.map((s) => ( {s.n}照片{s.ph === 1 ? '①' : '②'}·{RISKL[s.risk]} ))}
抗反涂层{arV}
亮度需求{brV}
白天 >200 lx,且对屏方向有大面积侧窗直射入屏——光面屏会出现清晰镜面反射{cred ? `;夜晚 ${meas.night} lx 属暗房,OLED 黑位受益` : ''}
海信 E8S · 高峰值亮度 LG G5 · 抗反膜 Sony B8 II · 光面屏慎选
照度=锁曝光 EV 反推(估算,非照度计);反光=光源位置判定,非实测眩光
回传 Agent
{chips.map((c) => {c})}
重测 满意 · 存入清单
); } /* ============ stage ============ */ const APPMETA = { compose: { t: 'AI 客厅合成 · 移动端', p: '拍下电视墙,AI 生成等比合成预览;尺寸、视角为精算值。可切挂墙 / 底座、机型、距离,结构化结果回传 Agent。', foot: '评审深链:?s=2 结果 · ?g=1 冻结合成中 · ?m=stand 底座 · ?sz=72 指定尺寸', }, light: { t: '采光 · 测光 · 移动端', p: '两拍测两轴:朝电视墙锁曝光反推照度(快测=估算档,补齐傍晚 / 夜晚升可信档);转身拍对屏方向,AI 识别反光源。合成抗反与亮度建议,回传 Agent。', foot: '评审深链:?app=light&lp=result(&slots=3 补满时段 · &pv=1 电视墙标注)', }, }; function Stage() { const [app, setApp] = useState(q('app') === 'light' ? 'light' : 'compose'); const pick = (a) => { if (a === app) return; const u = new URL(location); ['app', 'lp', 'slots', 'pv', 's', 'g', 'm', 'sz'].forEach((k) => u.searchParams.delete(k)); if (a === 'light') u.searchParams.set('app', 'light'); history.replaceState(null, '', u); setApp(a); }; const M = APPMETA[app]; return (
{q('from') === 'home' && ( 主页 )}

{M.t}

{M.p}

Liquid Glass 一镜到底动效 前台交互工具
{app === 'light' ? : }
{M.foot}
); } /* 供 电视数据库主页(home.jsx) 壳内嵌入调用;__TV_EMBED 时不自挂载 */ Object.assign(window, { ComposeApp, LightApp }); if (!window.__TV_EMBED) ReactDOM.createRoot(document.getElementById('root')).render();