{
  "reviewedAt": "2026-10-23",
  "sourceKind": "Production editor working copy; not claimed to be a public GitHub commit",
  "excerpts": [
    {
      "file": "app/studio/liquify.ts",
      "start": 5,
      "end": 13,
      "code": "export const liquifyTools={warp:'Forward Warp',pucker:'Pucker',bloat:'Bloat',twirl:'Twirl Clockwise',reconstruct:'Reconstruct',smooth:'Smooth',freeze:'Freeze Mask',thaw:'Thaw Mask'};\nexport type LiquifyTool=keyof typeof liquifyTools;\nexport type LiquifyMesh={version:1;size:number;dx:number[];dy:number[];freeze:number[]};\nexport type LiquifyBrush={tool:LiquifyTool;radius:number;pressure:number;density:number;aspect:number};\nexport const newLiquifyMesh=(size=129):LiquifyMesh=>({version:1,size,dx:Array(size*size).fill(0),dy:Array(size*size).fill(0),freeze:Array(size*size).fill(0)});\nexport function validateLiquifyMesh(v:unknown):asserts v is LiquifyMesh{const m=v as LiquifyMesh;if(!m||m.version!==1||!Number.isInteger(m.size)||m.size<9||m.size>129)throw Error('Invalid Liquify mesh.');for(const k of ['dx','dy','freeze'] as const)if(!Array.isArray(m[k])||m[k].length!==m.size*m.size||m[k].some(n=>!Number.isFinite(n)||(k==='freeze'?(n<0||n>1):Math.abs(n)>4)))throw Error('Invalid Liquify mesh values.');}\nconst clamp=(v:number,a=0,b=1)=>Math.max(a,Math.min(b,v));\nfunction sample(a:number[],n:number,x:number,y:number){x=clamp(x)*(n-1);y=clamp(y)*(n-1);const ix=Math.floor(x),iy=Math.floor(y),fx=x-ix,fy=y-iy,jy=Math.min(n-1,iy+1),jx=Math.min(n-1,ix+1);return a[iy*n+ix]*(1-fx)*(1-fy)+a[iy*n+jx]*fx*(1-fy)+a[jy*n+ix]*(1-fx)*fy+a[jy*n+jx]*fx*fy;}\n/** Inverse deformation composition; images are resampled once on application. */",
      "sha256": "2ba27135fce34d4f9d3ddee1f323e06ed88dbcf6903054611c7ab4b9ab337405"
    },
    {
      "file": "app/studio/face-liquify.ts",
      "start": 7,
      "end": 11,
      "code": "export async function detectLiquifyFaces(source:HTMLCanvasElement):Promise<FacePoint[][]>{\n const base=((import.meta as ImportMeta & {env?:{BASE_URL?:string}}).env?.BASE_URL||'/photoshot/editor/')+'portrait-ai/';\n detector??=(async()=>{const vision=await import('./vendor/portrait/vision_bundle.js'),files=await vision.FilesetResolver.forVisionTasks(base+'wasm');return vision.FaceLandmarker.createFromOptions(files,{baseOptions:{modelAssetPath:base+'face_landmarker.task',delegate:'CPU'},runningMode:'IMAGE',numFaces:5,minFaceDetectionConfidence:.5,minFacePresenceConfidence:.5,outputFaceBlendshapes:false,outputFacialTransformationMatrixes:false});})().catch(e=>{detector=undefined;throw e;});\n const model=await detector;await new Promise<void>(r=>setTimeout(r,0));const faces=model.detect(source).faceLandmarks as FacePoint[][];if(!faces?.length)throw Error('No face found. Use a clear front-facing portrait or the Liquify brushes.');return faces.map(f=>f.map(({x,y})=>({x,y})));\n}",
      "sha256": "a4d951919dcc4f94501a044b12db65e7ed5d9cfe3c18cbca15ece4758e288ef2"
    },
    {
      "file": "app/studio/face-liquify.ts",
      "start": 15,
      "end": 21,
      "code": "export function faceLiquifyMesh(mesh:LiquifyMesh,landmarks:FacePoint[],controls:FaceControls,aspect:number):LiquifyMesh{\n validateLiquifyMesh(mesh);if(landmarks.length<468||landmarks.some(p=>!p||!Number.isFinite(p.x)||!Number.isFinite(p.y))||!Number.isFinite(aspect)||aspect<=0||Object.keys(faceControlNames).some(k=>!Number.isFinite(controls[k as keyof FaceControls])||Math.abs(controls[k as keyof FaceControls])>100))throw Error('Invalid Face-Aware Liquify settings.');\n const point=(i:number)=>({x:landmarks[i].x*aspect,y:landmarks[i].y}),average=(a:number,b:number)=>({x:(point(a).x+point(b).x)/2,y:(point(a).y+point(b).y)/2}),left=average(33,133),right=average(362,263),eyeDistance=Math.hypot(right.x-left.x,right.y-left.y),ux=(right.x-left.x)/eyeDistance,uy=(right.y-left.y)/eyeDistance,vx=-uy,vy=ux,width=Math.hypot(point(454).x-point(234).x,point(454).y-point(234).y),height=Math.hypot(point(152).x-point(10).x,point(152).y-point(10).y);if(eyeDistance<.005||width<.01||height<.01)throw Error('Face is too small to reshape.');\n const out={...mesh,dx:[...mesh.dx],dy:[...mesh.dy],freeze:[...mesh.freeze]},n=mesh.size;\n type Region={center:FacePoint;rx:number;ry:number;sx?:number;sy?:number;mx?:number;my?:number};const regions:Region[]=[];\n for(const [eye,direction] of [[left,-1],[right,1]] as const)regions.push({center:eye,rx:width*.2,ry:height*.12,sx:controls.eyeSize*.003,sy:controls.eyeSize*.003,mx:direction*controls.eyeDistance/100*width*.07});\n regions.push({center:point(1),rx:width*.19,ry:height*.2,sx:controls.noseWidth*.003});",
      "sha256": "a4d951919dcc4f94501a044b12db65e7ed5d9cfe3c18cbca15ece4758e288ef2"
    },
    {
      "file": "app/studio/blur-gallery.ts",
      "start": 1,
      "end": 9,
      "code": "import {surface,layerSurface,type StudioDoc,type Layer} from './document.ts';import {gaussianBlur} from './gaussian.ts';import {canGeneralFilter} from './general-filters.ts';import {selectionForLayer} from './photoshop-engine.ts';import {rasterizeLayer} from './rasterize.ts';\nexport type BlurGallery={bokeh?:number;bokehColor?:number;bokehLow?:number;bokehHigh?:number;mode:'field'|'iris'|'tilt'|'path'|'spin';path?:{x:number;y:number}[];spinAngle?:number;pins:{x:number;y:number;blur:number}[];x:number;y:number;radiusX:number;radiusY:number;feather:number;angle:number;blur:number};\nexport const defaultBlurGallery=():BlurGallery=>({bokeh:0,bokehColor:50,bokehLow:192,bokehHigh:255,mode:'field',path:[{x:.2,y:.5},{x:.5,y:.25},{x:.8,y:.5}],spinAngle:20,pins:[{x:.5,y:.5,blur:0},{x:.15,y:.15,blur:15}],x:.5,y:.5,radiusX:.25,radiusY:.25,feather:.25,angle:0,blur:15});\nexport function validateBlurGallery(v:unknown):asserts v is BlurGallery{const s=v as BlurGallery;if(!s||!['field','iris','tilt','path','spin'].includes(s.mode)||!Array.isArray(s.pins)||s.pins.length<1||s.pins.length>16||s.pins.some(p=>!p||![p.x,p.y,p.blur].every(Number.isFinite)||p.x<0||p.x>1||p.y<0||p.y>1||p.blur<0||p.blur>50))throw Error('Invalid Blur Gallery settings.');for(const [key,min,max]of [['x',0,1],['y',0,1],['radiusX',.01,1],['radiusY',.01,1],['feather',.01,1],['angle',-180,180],['blur',0,50]] as const)if(!Number.isFinite(s[key])||s[key]<min||s[key]>max)throw Error('Invalid Blur Gallery control.');for(const [key,min,max,initial]of [['bokeh',0,100,0],['bokehColor',0,100,50],['bokehLow',0,254,192],['bokehHigh',1,255,255]] as const){const v=s[key]??initial;if(!Number.isFinite(v)||v<min||v>max)throw Error('Invalid Bokeh setting.');}if((s.bokehLow??192)>=(s.bokehHigh??255))throw Error('Bokeh light range must increase.');if(s.mode==='path'&&(!Array.isArray(s.path)||s.path.length!==3||s.path.some(p=>!p||![p.x,p.y].every(Number.isFinite)||p.x<0||p.x>1||p.y<0||p.y>1)))throw Error('Path Blur requires three control points.');if(s.mode==='spin'&&(!Number.isFinite(s.spinAngle)||s.spinAngle!<0||s.spinAngle!>180))throw Error('Invalid Spin Blur angle.');}\nexport function blurAt(s:BlurGallery,x:number,y:number,aspect=1){if(s.mode==='field'){let sum=0,weight=0;for(const p of s.pins){const d=((x-p.x)*aspect)**2+(y-p.y)**2;if(d<1e-10)return p.blur;const q=1/(d*d);sum+=p.blur*q;weight+=q;}return sum/weight;}const a=s.angle*Math.PI/180,dx=(x-s.x)*aspect,dy=y-s.y,px=dx*Math.cos(a)+dy*Math.sin(a),py=-dx*Math.sin(a)+dy*Math.cos(a),d=s.mode==='iris'?(Math.hypot(px/(s.radiusX*aspect),py/s.radiusY)-1)*Math.min(s.radiusX*aspect,s.radiusY):Math.abs(py)-s.radiusY,t=Math.max(0,Math.min(1,d/s.feather));return s.blur*t*t*(3-2*t);}\nasync function renderBlurBase(source:HTMLCanvasElement,s:BlurGallery,ratio=1,cancel=()=>false,coverage?:number[]|Uint8ClampedArray,preserveAlpha=false){validateBlurGallery(s);const w=source.width,h=source.height;if(w*h>4_000_000)throw Error('Blur Gallery supports up to 4 MP per layer.');if(s.mode==='path'||s.mode==='spin')return renderMotionGallery(source,s,ratio,cancel,coverage,preserveAlpha);const check=()=>{if(cancel())throw Error('Blur Gallery cancelled. No changes applied.');},src=source.getContext('2d')!.getImageData(0,0,w,h),out=surface(w,h),g=out.getContext('2d')!,result=g.createImageData(w,h),radius=new Float32Array(w*h),sum=new Float32Array(w*h*4),max=(s.mode==='field'?Math.max(...s.pins.map(p=>p.blur)):s.blur),steps=8;for(let y=0;y<h;y++)for(let x=0;x<w;x++)radius[y*w+x]=max?blurAt(s,x/Math.max(1,w-1),y/Math.max(1,h-1),w/h)/max*steps:0;\n for(let level=0;level<=steps;level++){await new Promise<void>(r=>setTimeout(r,0));check();const c=level?gaussianBlur(source,max*ratio*level/steps):source,pixels=level?c.getContext('2d')!.getImageData(0,0,w,h).data:src.data;for(let i=0;i<w*h;i++){const t=Math.max(0,1-Math.abs(radius[i]-level)),k=i*4,a=pixels[k+3]*t;sum[k+3]+=a;for(let ch=0;ch<3;ch++)sum[k+ch]+=pixels[k+ch]*a;}if(c!==source)c.width=c.height=1;}\n for(let i=0;i<w*h;i++){const k=i*4,t=coverage?coverage[i]/255:1,a=sum[k+3]*t+src.data[k+3]*(1-t);for(let ch=0;ch<3;ch++)result.data[k+ch]=a?(sum[k+ch]*t+src.data[k+ch]*src.data[k+3]*(1-t))/a:0;result.data[k+3]=preserveAlpha?src.data[k+3]:a;}check();g.putImageData(result,0,0);return out;\n}",
      "sha256": "fadb2387fa485b052241eaa92bb418e2eb6b24e3e6e1508536db157b32549ad9"
    }
  ],
  "productionUI": {
    "episode": 35,
    "at": "2026-10-24T00:47:55.190Z",
    "url": "https://slohero.com/photoshot/editor/",
    "inputKind": "AI-generated fictional adult demonstration PNG, not a real-person photograph",
    "actualFaceLandmarks": true,
    "faceControls": {
      "eyeSize": 25,
      "smile": 15
    },
    "meshSaved": true,
    "meshChanged": true,
    "undoExact": true,
    "redoExact": true,
    "passed": true,
    "errors": [],
    "imageRevision": "v3: restrained intermediate facial redesign from the original textual face descriptions; text-only generation."
  },
  "checks": [
    {
      "file": "liquify-render-results.json",
      "sha256": "dbabf21c903708452d3c9a311c75ecdf76ca6d11d26c1cbdf34c238766b87b09",
      "results": {
        "passed": true,
        "checks": [
          "Forward drag pulls source from opposite direction, local support and immutable source mesh",
          "Freeze blocks deformation and thaw releases it",
          "Pucker, Bloat, Twirl, Smooth and Reconstruct produce valid distinct deformation",
          "Rendered movement follows brush direction with premultiplied alpha",
          "Only selected area changes; placement, layer masks and original document preserved",
          "Serializable action replays identically, invalid meshes rejected, cancellation is atomic"
        ]
      },
      "freshRun": true
    },
    {
      "file": "face-liquify-render-results.json",
      "sha256": "7c38bd8178f10eaa495b5f10f72b8b4a10de7d61dc792d999e166b319760f2f8",
      "results": {
        "passed": true,
        "eightControls": true,
        "neutralIdentity": true,
        "inverseEyeGrowth": true,
        "freezePreserved": true,
        "sourceImmutable": true,
        "invalidInputs": true
      },
      "freshRun": true
    },
    {
      "file": "liquify-ui-results.json",
      "sha256": "7297eb8e572d717cbeb30868137e1c789e89cd3bbeea9c6c880e22b6ad520b27",
      "results": {
        "passed": true,
        "checks": [
          "Closing deformed preview leaves document untouched",
          "Drag, stroke undo/redo, mesh save/load, document apply/undo/redo and action recording",
          "Mobile dialog stays within viewport without horizontal overflow"
        ],
        "errors": []
      },
      "freshRun": true
    },
    {
      "file": "face-liquify-ui-results.json",
      "sha256": "29daa1c31eade178bd1fd6aa360863397c5b16bcf5ff5292a4455d0a37709b36",
      "results": {
        "passed": true,
        "actualLocalFaceModel": true,
        "eyeAndSmileControls": true,
        "meshChanges": true,
        "strokeUndoRedo": true,
        "documentUndo": true,
        "errors": []
      },
      "freshRun": true
    }
  ],
  "imageDisclosure": "AI 생성 시연 이미지 · 실제 인물 촬영 및 공식 협업 이미지가 아닙니다. 편집 화면과 결과는 실제 포토샷에서 만들었습니다.",
  "imageRevision": {
    "version": 3,
    "at": "2026-10-24T00:47:55.190Z",
    "method": "Text-only intermediate variation with two local adjustments; no reference photographs provided to generator.",
    "sourceSha256": "6b65664dc8eef4a22782951b99c52b4f8743fc612dc64ab393743f4155ea58aa",
    "previousImagesRetained": true
  }
}