2026-07-16 01:47:11 +00:00
package handler
import (
"context"
"database/sql"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"floorvisualizer/internal/openrouter"
"floorvisualizer/internal/repository"
)
// ── Floor options ──────────────────────────────────────────
type FloorOption struct {
2026-07-22 07:23:40 +00:00
ID string ` json:"id" `
Name string ` json:"name" `
Category string ` json:"category" `
Material string ` json:"material" `
ColorTone string ` json:"color_tone" `
Finish string ` json:"finish" `
SizeLabel string ` json:"size_label" `
WidthIn float64 ` json:"width_in" `
LengthIn float64 ` json:"length_in" `
ImageURL string ` json:"image_url" `
SKU string ` json:"sku" `
Description string ` json:"description" `
Variants [ ] FloorOption ` json:"variants,omitempty" `
2026-07-16 01:47:11 +00:00
}
type PatternOption struct {
Code int ` json:"code" `
Name string ` json:"name" `
Pattern string ` json:"pattern" `
Category string ` json:"category" `
}
type RoomOption struct {
Code int ` json:"code" `
Name string ` json:"name" `
}
var woodPatterns = [ ] PatternOption {
2026-07-22 07:23:40 +00:00
{ Code : 1 , Name : "直铺( 工字拼) Straight Lay" , Category : "wood" , Pattern : "planks running from bottom-left toward top-right of the image, joints staggered randomly, at least 6 inch offset between rows" } ,
{ Code : 2 , Name : "横铺 Horizontal Lay" , Category : "wood" , Pattern : "planks running horizontally left to right, parallel to the bottom edge of the image, joints staggered randomly, at least 6 inch offset" } ,
{ Code : 3 , Name : "人字拼 Herringbone" , Category : "wood" , Pattern : "planks at 90° forming continuous zigzag V-lines from bottom-left to top-right of the image" } ,
{ Code : 4 , Name : "鱼骨拼 Chevron" , Category : "wood" , Pattern : "planks with 45° mitered ends forming continuous V-points aligned from bottom-left to top-right of the image" } ,
2026-07-16 01:47:11 +00:00
}
var tilePatterns = [ ] PatternOption {
2026-07-22 07:23:40 +00:00
{ Code : 5 , Name : "直铺( 网格对缝) Straight Grid" , Category : "tile" , Pattern : "tiles aligned in rows (parallel to bottom edge) and columns (perpendicular to bottom edge), all grout lines continuous, no offset" } ,
{ Code : 6 , Name : "工字铺( 1/2错缝) Running Bond" , Category : "tile" , Pattern : "rows parallel to bottom edge of image, each row offset 50% from previous, horizontal grout lines continuous, vertical grout lines staggered" } ,
{ Code : 7 , Name : "三七错铺 1/3 Offset" , Category : "tile" , Pattern : "rows parallel to bottom edge of image, each row offset exactly 33% of tile length, every third row aligns" } ,
{ Code : 8 , Name : "六边形 Hexagonal" , Category : "tile" , Pattern : "six-sided tiles interlocked in honeycomb mesh, flat edges horizontal (parallel to bottom edge of image), grout follows hexagonal edges" } ,
{ Code : 9 , Name : "斜铺( 60度对角) Diagonal" , Category : "tile" , Pattern : "tiles rotated 60° to the image frame, grout lines run diagonally at 60° and 150° to the bottom edge, no lines parallel to image edges" } ,
2026-07-16 01:47:11 +00:00
}
var roomOptions = [ ] RoomOption {
2026-07-22 07:23:40 +00:00
{ Code : 0 , Name : "自动识别" } ,
{ Code : 1 , Name : "客厅" } ,
{ Code : 2 , Name : "卧室" } ,
{ Code : 3 , Name : "厨房" } ,
{ Code : 4 , Name : "餐厅" } ,
{ Code : 5 , Name : "浴室" } ,
{ Code : 6 , Name : "书房" } ,
{ Code : 7 , Name : "走廊" } ,
2026-07-16 01:47:11 +00:00
}
func findPatternByCode ( code int ) * PatternOption {
for _ , p := range append ( woodPatterns , tilePatterns ... ) {
if p . Code == code {
return & p
}
}
return nil
}
func findRoomByCode ( code int ) * RoomOption {
if code == 0 {
return nil
}
for _ , r := range roomOptions {
if r . Code == code {
return & r
}
}
return nil
}
2026-07-22 07:23:40 +00:00
// loadFloorOptions loads all products, deduplicated by brand+style_name. Variants
// (different sizes of the same style) are attached to the representative entry.
func loadFloorOptions ( db * sql . DB ) ( [ ] FloorOption , error ) {
// Use raw query without DISTINCT ON so all size variants are returned
rows , err := db . Query ( ` SELECT brand , group_name , sku , series_name , style_name ,
category , material , color_tone , finish , price_per_sqft , price_tier , price_source ,
is_official_price , main_image_url , room_image_url , source_url , description , status ,
COALESCE ( coverage_sqft_per_box , 0 ) , COALESCE ( wood_species , ' ' ) ,
size_label , COALESCE ( width_in , 0 ) , COALESCE ( length_in , 0 )
FROM products ORDER BY brand , style_name , id LIMIT 3000 ` )
2026-07-16 01:47:11 +00:00
if err != nil {
return nil , err
}
2026-07-22 07:23:40 +00:00
defer rows . Close ( )
prods , err := repository . ScanProducts ( rows )
if err != nil {
return nil , err
}
type key struct { brand , style string }
seen := map [ key ] int { } // key → index in out slice
2026-07-16 01:47:11 +00:00
var out [ ] FloorOption
for _ , p := range prods {
2026-07-22 07:23:40 +00:00
k := key { p . Brand , p . StyleName }
if idx , ok := seen [ k ] ; ok {
// Add as variant to the representative entry
out [ idx ] . Variants = append ( out [ idx ] . Variants , FloorOption {
SizeLabel : p . SizeLabel , WidthIn : p . WidthIn , LengthIn : p . LengthIn ,
SKU : p . SKU ,
} )
} else {
seen [ k ] = len ( out )
out = append ( out , FloorOption {
ID : p . SKU , Name : p . StyleName , Category : p . Category ,
Material : p . Material , ColorTone : p . ColorTone , Finish : p . Finish ,
SizeLabel : p . SizeLabel , WidthIn : p . WidthIn , LengthIn : p . LengthIn ,
ImageURL : p . MainImageURL , SKU : p . SKU , Description : p . Description ,
} )
2026-07-16 01:47:11 +00:00
}
}
return out , nil
}
2026-07-22 07:23:40 +00:00
func isTileProduct ( p FloorOption ) bool {
m := strings . ToLower ( p . Material )
return strings . Contains ( m , "porcelain" ) || strings . Contains ( m , "ceramic" ) ||
strings . Contains ( m , "stone look" ) || m == "glass"
}
func isVinylProduct ( p FloorOption ) bool {
m := strings . ToLower ( p . Material )
return m == "luxury vinyl" || strings . Contains ( m , "rigid core" ) || m == "vinyl"
}
func isLaminateProduct ( p FloorOption ) bool {
return strings . EqualFold ( p . Material , "laminate" )
}
func isWoodProduct ( p FloorOption ) bool {
return ! isTileProduct ( p ) && ! isVinylProduct ( p ) && ! isLaminateProduct ( p )
}
2026-07-16 01:47:11 +00:00
func findFloorBySKU ( db * sql . DB , sku string ) * FloorOption {
p , err := repository . GetProductBySKU ( db , sku )
if err != nil {
return nil
}
2026-07-22 07:23:40 +00:00
f := & FloorOption {
2026-07-16 01:47:11 +00:00
ID : p . SKU , Name : p . StyleName , Category : p . Category ,
Material : p . Material , ColorTone : p . ColorTone , Finish : p . Finish ,
2026-07-22 07:23:40 +00:00
SizeLabel : p . SizeLabel , WidthIn : p . WidthIn , LengthIn : p . LengthIn ,
ImageURL : p . MainImageURL , SKU : p . SKU , Description : p . Description ,
}
// Fetch size variants (same style, different dimensions)
variants , _ := repository . GetVariantsByStyle ( db , sku )
for _ , vp := range variants {
f . Variants = append ( f . Variants , FloorOption {
SizeLabel : vp . SizeLabel , WidthIn : vp . WidthIn , LengthIn : vp . LengthIn ,
SKU : vp . SKU ,
} )
2026-07-16 01:47:11 +00:00
}
2026-07-22 07:23:40 +00:00
// Canonical reference image = smallest size variant (by width)
// All sizes of the same style use the same reference image to prevent color drift
canonicalSKU , canonicalImage , canonicalWidth , canonicalLength := p . SKU , p . MainImageURL , f . WidthIn , f . LengthIn
rows , _ := db . Query ( ` SELECT sku , main_image_url , COALESCE ( width_in , 0 ) , COALESCE ( length_in , 0 ) FROM products
WHERE style_name = $ 1 AND brand = $ 2
ORDER BY COALESCE ( width_in , 999 ) , COALESCE ( length_in , 999 ) LIMIT 1 ` ,
p . StyleName , p . Brand )
if rows != nil {
defer rows . Close ( )
if rows . Next ( ) {
rows . Scan ( & canonicalSKU , & canonicalImage , & canonicalWidth , & canonicalLength )
}
}
f . ImageURL = canonicalImage
// Build dynamic size instruction if selected size differs from reference image size
if ( canonicalWidth > 0 && canonicalLength > 0 ) && ( f . WidthIn > 0 && f . LengthIn > 0 ) {
if canonicalWidth != f . WidthIn || canonicalLength != f . LengthIn {
f . Description = fmt . Sprintf (
` SIZE MAPPING: The reference image shows %.1f"× %.1f" planks. Generate %.1f"× %.1f" planks — same exact material, only scale the board size proportionally. | %s ` ,
canonicalWidth , canonicalLength , f . WidthIn , f . LengthIn , f . Description ,
)
}
}
return f
2026-07-16 01:47:11 +00:00
}
// ── Public accessors for worker ────────────────────────────
func FindFloorBySKUPublic ( db * sql . DB , sku string ) * FloorOption { return findFloorBySKU ( db , sku ) }
func FindPatternByCodePublic ( code string ) * PatternOption {
2026-07-16 02:29:11 +00:00
c := 0
fmt . Sscanf ( code , "%d" , & c )
return findPatternByCode ( c )
2026-07-16 01:47:11 +00:00
}
func FindRoomByCodePublic ( code string ) * RoomOption {
2026-07-16 02:29:11 +00:00
c := 0
fmt . Sscanf ( code , "%d" , & c )
return findRoomByCode ( c )
}
func BuildMaskPromptPublic ( room * RoomOption ) string { return buildMaskPrompt ( room ) }
func BuildFloorPromptPublic ( f FloorOption , p PatternOption , room * RoomOption ) string {
return buildFloorPrompt ( f , p , room )
2026-07-16 01:47:11 +00:00
}
2026-07-22 07:23:40 +00:00
// BuildCombinedFloorPrompt builds a single prompt that identifies the floor area AND replaces it.
// No separate mask generation step — the AI does both in one pass.
func BuildCombinedFloorPrompt ( f FloorOption , p PatternOption , room * RoomOption ) string {
floorPrompt := buildFloorPrompt ( f , p , room )
roomCtx := ""
if room != nil && room . Code != 0 {
roomCtx = fmt . Sprintf ( " This is a %s." , room . Name )
}
return fmt . Sprintf (
` FIRST — identify the floor : Look at this room photo . % s
The floor is the flat horizontal walking surface — NOT walls , NOT baseboards , NOT furniture , NOT stairs .
You will replace ONLY the floor area . Every other pixel must remain bit - identical to the input photo .
% s
Return ONLY the completed image . No text . ` ,
roomCtx ,
floorPrompt ,
)
}
// buildPatternConstraint returns installation rules specific to each pattern code.
func buildPatternConstraint ( code int ) string {
switch code {
case 1 :
return ` PATTERN RULES — Straight Lay (wood): Planks run from the bottom-left of the image toward the top-right, at roughly 45° to the image frame. Long edges of each plank point toward the upper-right corner. Joints staggered 6" minimum — random pattern. 1/8" expansion gap. `
case 2 :
return ` PATTERN RULES — Horizontal Lay (wood): Planks run horizontally — left to right, parallel to the bottom edge of the image. Long edges of each plank are horizontal across the photo. Joints staggered 6" minimum — random pattern. 1/8" expansion gap. `
case 3 :
return ` PATTERN RULES — Herringbone (wood): Planks at 90° forming continuous zigzag. Each plank end meets neighbor at 90°. V-lines run from bottom-left to top-right of the image. Joints tight — continuous unbroken zigzag. `
case 4 :
return ` PATTERN RULES — Chevron (wood): Planks with 45° mitered ends form continuous V. V-points aligned in a straight line running bottom-left to top-right across the image. Joint lines are straight and continuous. NOT herringbone — mitered joints, not staggered. `
case 5 :
return ` PATTERN RULES — Straight Grid (tile): Tiles aligned in rows and columns. Grout lines run horizontally (parallel to bottom edge of image) and vertically (perpendicular to bottom edge). NO offset between rows. Grout ~1/8". `
case 6 :
return ` PATTERN RULES — Running Bond (tile): Each row offset 50% from previous — classic brickwork. Grout lines parallel to bottom edge of image are continuous; grout lines perpendicular to bottom edge are staggered. Grout ~1/8". `
case 7 :
return ` PATTERN RULES — 1/3 Offset (tile): Each row offset exactly 33% of tile length. Every third row aligns. Rows run parallel to bottom edge of image. Grout ~1/8". NOT 50% bond — offset is exactly 1/3. `
case 8 :
return ` PATTERN RULES — Hexagonal (tile): Six-sided tiles interlocked in honeycomb mesh. Each tile touches 6 neighbors. Flat edges of hexagons are horizontal (parallel to bottom edge of image). Grout follows hexagonal edges — network pattern, not grid. `
case 9 :
return ` PATTERN RULES — Diagonal (tile): Tiles rotated 60° relative to the image frame — all tiles at a steep diagonal, diamond orientation. Grout lines run diagonally across the photo at 60° and 150° to the bottom edge. None parallel or perpendicular to image edges. Triangle cuts at all walls. Grout ~1/8". `
default :
return ""
}
}
2026-07-16 01:47:11 +00:00
// ── Floor Generate Handler (Redis queue) ──────────────────
func SetQueue ( enqueue func ( ctx context . Context , jobID string , payload map [ string ] string ) error ) {
generateQueue = enqueue
}
var generateQueue func ( ctx context . Context , jobID string , payload map [ string ] string ) error
var queryStatus func ( ctx context . Context , jobID string ) ( * JobStatusResp , error )
func SetStatusQuery ( fn func ( ctx context . Context , jobID string ) ( * JobStatusResp , error ) ) {
queryStatus = fn
}
type JobStatusResp struct {
JobID string ` json:"job_id" `
Status string ` json:"status" `
Step string ` json:"step" `
Message string ` json:"message" `
Result string ` json:"result,omitempty" `
Error string ` json:"error,omitempty" `
}
2026-07-16 02:29:11 +00:00
func FloorGenerate ( client * openrouter . Client , db * sql . DB ) http . HandlerFunc {
2026-07-16 01:47:11 +00:00
return func ( w http . ResponseWriter , r * http . Request ) {
if r . Method != http . MethodPost {
writeErr ( w , 405 , "Method not allowed" )
return
}
if err := r . ParseMultipartForm ( 20 << 20 ) ; err != nil {
writeErr ( w , 400 , err . Error ( ) )
return
}
file , _ , err := r . FormFile ( "image" )
if err != nil {
writeErr ( w , 400 , "No image" )
return
}
defer file . Close ( )
floor := findFloorBySKU ( db , r . FormValue ( "floor_id" ) )
patternCode := 0
fmt . Sscanf ( r . FormValue ( "pattern_code" ) , "%d" , & patternCode )
pattern := findPatternByCode ( patternCode )
if floor == nil || pattern == nil {
writeErr ( w , 400 , "Invalid options" )
return
}
os . MkdirAll ( "uploads" , 0755 )
jobID := fmt . Sprintf ( "%d" , time . Now ( ) . UnixNano ( ) )
inputPath := filepath . Join ( "uploads" , fmt . Sprintf ( "input_%s.png" , jobID ) )
dst , err := os . Create ( inputPath )
if err != nil {
writeErr ( w , 500 , "Failed to save image" )
return
}
if _ , err := io . Copy ( dst , file ) ; err != nil {
dst . Close ( )
writeErr ( w , 500 , "Failed to write image" )
return
}
dst . Close ( )
os . MkdirAll ( "outputs" , 0755 )
if generateQueue == nil {
writeErr ( w , 500 , "Queue not initialized" )
return
}
// Store job payload then enqueue
payload := map [ string ] string {
"input_path" : inputPath ,
"floor_id" : r . FormValue ( "floor_id" ) ,
"pattern_code" : r . FormValue ( "pattern_code" ) ,
"room_code" : r . FormValue ( "room_code" ) ,
}
if err := generateQueue ( r . Context ( ) , jobID , payload ) ; err != nil {
writeErr ( w , 500 , "Failed to enqueue job" )
return
}
_ = payload // payload stored inside generateQueue wrapper
writeJSON ( w , 200 , map [ string ] string { "job_id" : jobID } )
}
}
2026-07-16 02:29:11 +00:00
2026-07-16 01:47:11 +00:00
// ── Progress ───────────────────────────────────────────────
// ── Floor Status Query ─────────────────────────────────
2026-07-16 02:29:11 +00:00
func FloorStatus ( w http . ResponseWriter , r * http . Request ) {
2026-07-16 01:47:11 +00:00
jobID := r . URL . Query ( ) . Get ( "job_id" )
if jobID == "" {
writeErr ( w , 400 , "job_id is required" )
return
}
if queryStatus == nil {
writeErr ( w , 500 , "status query not initialized" )
return
}
js , err := queryStatus ( r . Context ( ) , jobID )
if err != nil {
writeJSON ( w , 404 , map [ string ] string { "error" : "job not found" } )
return
}
writeJSON ( w , 200 , js )
}
2026-07-16 02:29:11 +00:00
2026-07-16 01:47:11 +00:00
// ── Options ────────────────────────────────────────────────
2026-07-16 02:29:11 +00:00
func FloorOptions ( db * sql . DB ) http . HandlerFunc {
2026-07-16 01:47:11 +00:00
return func ( w http . ResponseWriter , r * http . Request ) {
2026-07-22 07:23:40 +00:00
all , _ := loadFloorOptions ( db )
var allWood , allTile , vinylFloors [ ] FloorOption
for _ , f := range all {
switch {
case isVinylProduct ( f ) :
vinylFloors = append ( vinylFloors , f )
case isTileProduct ( f ) || isLaminateProduct ( f ) :
allTile = append ( allTile , f )
default :
allWood = append ( allWood , f )
}
}
2026-07-16 01:47:11 +00:00
writeJSON ( w , 200 , map [ string ] any {
"wood_floors" : allWood ,
"wood_patterns" : woodPatterns ,
"tile_floors" : allTile ,
"tile_patterns" : tilePatterns ,
"vinyl_floors" : vinylFloors ,
"rooms" : roomOptions ,
} )
}
}
// ── Prompts ────────────────────────────────────────────────
func buildMaskPrompt ( room * RoomOption ) string {
2026-07-22 07:23:40 +00:00
base := ` You are a precise floor segmentation tool . Create a pure black - and - white mask for this room photo .
STRICT DEFINITIONS :
- WHITE ( # FFFFFF ) : ONLY the flat , horizontal walking surface ( the floor ) . Include the entire visible floor area up to the exact edge where it meets walls , baseboards , cabinets , or stairs .
- BLACK ( # 000000 ) : EVERYTHING that is not flat horizontal floor — walls , baseboards / skirting boards , stairs / stair risers ( they are NOT flat floor ) , furniture and furniture legs , doors and door frames , windows , cabinets , appliances , rugs / carpets , ceiling , decorations , people , pets .
EDGE RULES :
- At the wall - floor boundary : cut exactly along the junction . The baseboard / skirting board is BLACK , the floor is WHITE .
- Furniture legs resting on the floor : the legs are BLACK . The floor visible BETWEEN and AROUND legs is WHITE .
- Stairs : each step ' s horizontal tread AND vertical riser are BLACK ( stairs are not a continuous flat plane ) .
- Rugs / carpets on the floor : BLACK ( they are not the permanent floor ) .
SELF - CHECK before output :
- Did you include any wall area ? → fix it .
- Did you miss any floor area between furniture legs ? → fill it WHITE .
- Did you paint any stair surface WHITE ? → make it BLACK .
- Are all edges sharp and precise at boundaries ? → verify .
Output ONLY the mask image . No text , no explanation . `
2026-07-16 01:47:11 +00:00
if room != nil && room . Code != 0 {
base += fmt . Sprintf ( "\nContext: This is a %s." , room . Name )
}
return base
}
func buildFloorPrompt ( floor FloorOption , pattern PatternOption , room * RoomOption ) string {
var roomCtx string
if room != nil && room . Code != 0 {
roomCtx = fmt . Sprintf ( "\nRoom context: This is a %s." , room . Name )
}
matType := "wood plank"
2026-07-22 07:23:40 +00:00
if isTileProduct ( floor ) || isLaminateProduct ( floor ) {
2026-07-16 01:47:11 +00:00
matType = "tile"
}
materialDesc := fmt . Sprintf ( "%s material, %s tone, %s finish" , floor . Material , floor . ColorTone , floor . Finish )
if floor . SizeLabel != "" {
materialDesc += fmt . Sprintf ( ", %s size" , floor . SizeLabel )
}
2026-07-22 07:23:40 +00:00
// Include product description if available for richer visual detail
var descriptionText string
if floor . Description != "" {
descriptionText = fmt . Sprintf ( "\nVisual appearance: %s" , floor . Description )
}
productLock := ""
if floor . ImageURL != "" {
productLock = fmt . Sprintf (
` The reference image is the EXACT product material — not style inspiration , not a mood board .
LOCK THESE PROPERTIES from the reference image : base color , color temperature , grain / vein pattern ,
knot density , surface texture , gloss level , material character .
FORBIDDEN : color shift , different wood species or stone type , new grain pattern , different stain ,
gloss change , "luxury upgrade" or aesthetic reinterpretation . ` ,
)
}
// Size instruction: only change board proportion, never the material itself
sizeInstruction := ""
if len ( floor . Variants ) > 0 {
var variantList [ ] string
for _ , v := range floor . Variants {
marker := ""
if v . SKU == floor . SKU {
marker = " ← SELECTED"
}
if v . SizeLabel != "" {
variantList = append ( variantList , v . SizeLabel + marker )
} else if v . WidthIn > 0 && v . LengthIn > 0 {
variantList = append ( variantList , fmt . Sprintf ( ` %.1f"× %.1f" ` , v . WidthIn , v . LengthIn ) + marker )
}
}
if len ( variantList ) > 0 {
sizeInstruction = fmt . Sprintf (
"\nSIZE: This product comes in multiple sizes: %s.\nONLY change board width and seam spacing. The material texture, color, and grain are from the reference image — identical in every size. Wider boards use the SAME texture source, just with fewer seams. Do NOT reinterpret the wood to look \"more premium\" for wider planks." ,
strings . Join ( variantList , ", " ) ,
)
}
} else if floor . SizeLabel != "" {
sizeInstruction = fmt . Sprintf (
"\nSIZE: Each piece is %s. Only adjust board proportion and seam spacing. The material texture is from the reference image — do not change it." ,
floor . SizeLabel ,
)
} else if floor . WidthIn > 0 && floor . LengthIn > 0 {
sizeInstruction = fmt . Sprintf (
` \nSIZE: Each piece is %.1f" × %.1f". Only adjust board proportion and seam spacing. The material texture is from the reference image — do not change it. ` ,
floor . WidthIn , floor . LengthIn ,
)
}
patternInstruction := buildPatternConstraint ( pattern . Code )
2026-07-16 01:47:11 +00:00
return fmt . Sprintf (
2026-07-22 07:23:40 +00:00
` You are a flooring product visualization renderer for a commercial catalog . Your sole function is to replace the floor surface in interior photos with a specific flooring SKU . This is product - accurate catalog photography — NOT interior design or artistic interpretation .
STRICT OUTPUT RULES :
1. Respond with ONLY the completed PNG image . No text , no explanation , no JSON wrapper .
2. The output must have the exact same pixel dimensions as the input image .
Do not crop , pad , upscale , downscale , rotate , or alter the image geometry in any way .
3. Do not apply global filtering , sharpening , blurring , tone - mapping , contrast adjustment ,
or style transfer . The only permitted change is the floor surface .
FLOOR SURFACE DEFINITION :
- "Floor" — the flat horizontal walking surface . The visible ground plane inside the room .
DO NOT bleed onto : walls , baseboards / skirting boards , stairs , stair risers , furniture ,
chair legs , table legs , cabinet bases , doors , door frames , window glass or frames ,
trim , pipes , conduit , rugs , carpets , ceiling , appliances , decorations .
- Stairs are NOT floor — each tread and riser must remain completely unchanged .
- If a surface is not the flat horizontal floor , PRESERVE IT EXACTLY .
FLOOR REPLACEMENT RULES :
4. Apply the new floor ONLY to the floor surface . Every pixel outside must be bit - identical to input .
5. Preserve the original room lighting , shadows , and highlights ON the new floor .
6. ADD realistic contact shadows : chair wheels , table legs , cabinet bases MUST cast tight dark shadows
directly at the contact point — sharpest there , softening outward . This shows physical weight .
7. Maintain clean but physically installed - looking floor edges — not a Photoshop cutout .
8. The new floor must look physically installed and photographed — not rendered , not composited .
FIDELITY RULES :
9. The output must be a photorealistic edit of the input photograph .
Do not generate new content , synthesise areas , invent objects , or alter composition .
10. Preserve camera sensor noise , natural vignette , and original white balance on all non - floor surfaces .
AMBIGUITY RULE :
11. If a pixel could be floor OR furniture / wall / baseboard — preserve the original unchanged .
Precision over completeness .
FLOOR SPECIFICATION :
- Product : % s ( % s % s )
- Pattern : % s — % s
The pattern changes layout only . The material comes from the reference image — identical regardless of pattern .
% s
- Material details : % s % s % s % s
% s
TEXTURE FIDELITY :
- Every plank / tile must have UNIQUE grain , vein , or tonal variation — no visible texture repetition .
- Natural imperfections : minor scratches , micro - wear near chair wheels , colour variance between pieces .
- Seams and joints : subtle dust in gaps , micro - shadow at edges , slight edge wear .
- Polished surfaces : reflections UNEVEN and angle - dependent — not uniform glossy sheen .
- Do NOT invent sunbeams , light rays , or window reflections not in the original .
NEGATIVE CONSTRAINTS — the floor must NOT contain :
CGI look , 3 D render , plastic texture , repeating patterns , uniform gloss , invented lights ,
HDR tone - mapping , "luxury showroom" aesthetic , sterile clean seams , floating furniture ,
color shift , different wood species , new grain , different stain , aesthetic reinterpretation .
REQUIREMENTS SUMMARY :
✓ Only the flat horizontal floor changes — all other pixels preserved bit - identical .
✓ Contact shadows under all furniture — sharp at contact , softening outward .
✓ Original lighting and shadows match on new floor .
✓ Output geometry identical to input — same dimensions , no cropping .
✓ Scene composition unchanged — no new objects , no synthesised content .
✓ Material color , grain , gloss , species match the reference image exactly .
Return ONLY the completed image . No text . ` ,
floor . Name , matType , materialDesc ,
pattern . Name , pattern . Pattern , patternInstruction ,
materialDesc ,
roomCtx , descriptionText , sizeInstruction , productLock ,
2026-07-16 01:47:11 +00:00
)
}
// ── Content Check ─────────────────────────────────────────
func validateContent ( client * openrouter . Client , ctx context . Context , imagePath string ) ( bool , int ) {
imgData , err := os . ReadFile ( imagePath )
if err != nil {
return false , 0
}
dataURI := "data:image/png;base64," + b64enc ( imgData )
resp , err := client . Chat ( ctx , openrouter . ChatRequest {
Model : "google/gemini-2.5-flash" ,
Messages : [ ] openrouter . Message { { Role : "user" , Content : [ ] openrouter . ContentPart {
{ Type : "image_url" , ImageURL : & openrouter . ImageURL { URL : dataURI } } ,
{ Type : "text" , Text : ` Score this image 1 - 10 for indoor floor replacement suitability .
8 - 10 : Clear indoor room , floor visible . 5 - 7 : Indoor but poor angle . 1 - 4 : Outdoor / pets / objects / no floor .
Return ONLY a number ( 1 - 10 ) . ` } ,
} } } ,
} )
if err != nil || len ( resp . Choices ) == 0 {
return true , 0
}
score := parseScore ( resp . Choices [ 0 ] . Message . Content )
return score >= 6 , score
}
func parseScore ( raw string ) int {
for _ , s := range [ ] string { "10" , "9" , "8" , "7" , "6" , "5" , "4" , "3" , "2" , "1" } {
if len ( raw ) >= len ( s ) && raw [ : len ( s ) ] == s {
v := 0
fmt . Sscanf ( s , "%d" , & v )
return v
}
}
return 0
}
func b64enc ( data [ ] byte ) string {
const tbl = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
var b strings . Builder
for i := 0 ; i < len ( data ) ; i += 3 {
b0 , b1 , b2 := data [ i ] , byte ( 0 ) , byte ( 0 )
if i + 1 < len ( data ) {
b1 = data [ i + 1 ]
}
if i + 2 < len ( data ) {
b2 = data [ i + 2 ]
}
b . WriteByte ( tbl [ b0 >> 2 ] )
b . WriteByte ( tbl [ ( ( b0 & 3 ) << 4 ) | ( b1 >> 4 ) ] )
if i + 1 < len ( data ) {
b . WriteByte ( tbl [ ( ( b1 & 15 ) << 2 ) | ( b2 >> 6 ) ] )
} else {
b . WriteByte ( '=' )
}
if i + 2 < len ( data ) {
b . WriteByte ( tbl [ b2 & 63 ] )
} else {
b . WriteByte ( '=' )
}
}
return b . String ( )
}