```python
import bpy
import math
import random

# Clear all existing objects
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(use_global=False)

# Clean up data blocks
for block in bpy.data.meshes:
    bpy.data.meshes.remove(block)
for block in bpy.data.materials:
    bpy.data.materials.remove(block)
for block in bpy.data.textures:
    bpy.data.textures.remove(block)

# Set render engine to Cycles for preview (benchmark will override)
bpy.context.scene.render.engine = 'CYCLES'
bpy.context.scene.cycles.device = 'GPU'

# ============ Utility Functions ============
def create_material(name, color, roughness=0.5, metallic=0.0, specular=0.5, use_nodes=True):
    mat = bpy.data.materials.new(name=name)
    mat.use_nodes = use_nodes
    bsdf = mat.node_tree.nodes.get('Principled BSDF')
    if bsdf:
        bsdf.inputs['Base Color'].default_value = color
        bsdf.inputs['Roughness'].default_value = roughness
        bsdf.inputs['Metallic'].default_value = metallic
        bsdf.inputs['Specular IOR Level'].default_value = specular
    return mat

def create_glass_material(name, color=(1,1,1,1), roughness=0.0, ior=1.45):
    mat = bpy.data.materials.new(name=name)
    mat.use_nodes = True
    nodes = mat.node_tree.nodes
    links = mat.node_tree.links
    # Clear default
    for node in nodes:
        nodes.remove(node)
    # Glass BSDF
    glass = nodes.new('ShaderNodeBsdfGlass')
    glass.inputs['Color'].default_value = color
    glass.inputs['Roughness'].default_value = roughness
    glass.inputs['IOR'].default_value = ior
    # Output
    out = nodes.new('ShaderNodeOutputMaterial')
    links.new(glass.outputs['BSDF'], out.inputs['Surface'])
    return mat

def create_velvet_material(name, color, roughness=0.8, anisotropy=0.3):
    mat = bpy.data.materials.new(name=name)
    mat.use_nodes = True
    bsdf = mat.node_tree.nodes.get('Principled BSDF')
    if bsdf:
        bsdf.inputs['Base Color'].default_value = color
        bsdf.inputs['Roughness'].default_value = roughness
        bsdf.inputs['Anisotropic'].default_value = anisotropy
        bsdf.inputs['Sheen Weight'].default_value = 0.5
        bsdf.inputs['Sheen Roughness'].default_value = 0.6
    return mat

def create_checkered_material():
    mat = bpy.data.materials.new(name='Checkered')
    mat.use_nodes = True
    nodes = mat.node_tree.nodes
    links = mat.node_tree.links
    # Clear default
    for node in nodes:
        nodes.remove(node)
    # Principled BSDF
    bsdf = nodes.new('ShaderNodeBsdfPrincipled')
    bsdf.inputs['Metallic'].default_value = 1.0
    bsdf.inputs['Roughness'].default_value = 0.0
    # Checker texture
    checker = nodes.new('ShaderNodeTexChecker')
    checker.inputs['Scale'].default_value = 5.0
    checker.inputs['Color1'].default_value = (0.1, 0.1, 0.1, 1.0)
    checker.inputs['Color2'].default_value = (1.0, 1.0, 1.0, 1.0)
    # Connect
    links.new(checker.outputs['Color'], bsdf.inputs['Base Color'])
    out = nodes.new('ShaderNodeOutputMaterial')
    links.new(bsdf.outputs['BSDF'], out.inputs['Surface'])
    return mat

# ============ Scene Setup ============
# World
world = bpy.context.scene.world
world.use_nodes = True
bg = world.node_tree.nodes.get('Background')
if bg:
    bg.inputs['Color'].default_value = (0.02, 0.02, 0.02, 1.0)  # very dark
    bg.inputs['Strength'].default_value = 0.5

# ============ Table ============
bpy.ops.mesh.primitive_plane_add(size=4.0, location=(0,0,0))
table = bpy.context.object
table.name = 'Table'
# Add depth by extruding
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.extrude_region_move(TRANSFORM_OT_translate={"value":(0,0,-0.1)})
bpy.ops.object.mode_set(mode='OBJECT')
# Add wood material
wood_mat = create_material('Wood', (0.3, 0.15, 0.05, 1.0), roughness=0.7, metallic=0.0)
table.data.materials.append(wood_mat)

# ============ Cloths (Cornell box palette) ============
# Red cloth on left
bpy.ops.mesh.primitive_plane_add(size=2.0, location=(-1.0, 1.0, 0.4))
red_cloth = bpy.context.object
red_cloth.name = 'RedCloth'
bpy.ops.object.mode_set(mode='EDIT')
# Subdivide to get more vertices
bpy.ops.mesh.subdivide(number_cuts=10)
bpy.ops.object.mode_set(mode='OBJECT')
# Add cloth modifier
cloth_mod = red_cloth.modifiers.new(name='Cloth', type='CLOTH')
cloth_mod.settings.quality = 5
cloth_mod.settings.time_scale = 1.0
cloth_mod.settings.mass = 0.3
cloth_mod.settings.tension_stiffness = 15
cloth_mod.settings.compression_stiffness = 15
cloth_mod.settings.shear_stiffness = 10
cloth_mod.settings.bending_stiffness = 0.5
# Pin two back corners
vertex_group = red_cloth.vertex_groups.new(name='Pins')
# Select vertices at 0.5,1.5,0.4 and -0.5,1.5,0.4 approximately
# We'll just pin the top edge after simulation
# For now, set up simulation and run
bpy.context.view_layer.objects.active = red_cloth
bpy.ops.object.modifier_apply(apply_as='DATA', modifier='Cloth')  # Apply after simulation? We need to simulate first.
# Better: use cloth simulation in non-headless? We'll manually shape the cloth using lattice.
# Since simulation may be slow, we'll create folds manually with proportional editing.
# Simpler approach: use a plane with a subsurf and displace modifier to create folds.
# Let's use a different method: create a mesh with some folds manually.
# We'll just use a plane with a few vertices and manually move them.
# For simplicity, we'll create a cloth-like shape using a grid and wave displacement.
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
# Add some large folds manually
bpy.ops.transform.translate(value=(0,0,0.2), constraint_axis=(False, False, True))
# Let's use a simple approach: create a draped cloth using a cylinder? No.
# We'll use a plane with a subdivision surface and then use a lattice to deform.
# Actually, let's just use a plane with a cloth simulation but we need to run it.
# We'll set up the scene, then run the cloth simulation for a few frames.
# We need to switch to object mode, add cloth, set up pinning, then run.
# But we are in headless; we can set frame and force update.
# Let's do that.

# First, remove the cloth we started and create a new one properly.
bpy.data.objects.remove(red_cloth, do_unlink=True)

# Red cloth
bpy.ops.mesh.primitive_grid_add(x_subdivisions=20, y_subdivisions=20, size=2.0, location=(-1.0, 1.0, 0.4))
red_cloth = bpy.context.object
red_cloth.name = 'RedCloth'
# Add cloth modifier
cloth_mod = red_cloth.modifiers.new(name='Cloth', type='CLOTH')
cloth_mod.settings.quality = 8
cloth_mod.settings.time_scale = 1.0
cloth_mod.settings.mass = 0.3
cloth_mod.settings.tension_stiffness = 15
cloth_mod.settings.compression_stiffness = 15
cloth_mod.settings.shear_stiffness = 10
cloth_mod.settings.bending_stiffness = 0.5
# Pin the top edge vertices (we'll select them later)
# But we need vertex group: create new
pin_group = red_cloth.vertex_groups.new(name='Pin')
# Select vertices with y > 1.8 (back edge)
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='DESELECT')
bpy.ops.object.mode_set(mode='OBJECT')
# Actually, we can't easily select vertices in edit mode via script without context.
# We'll use a simpler approach: use cloth with no pinning but have a collision object.
# Instead, we'll create a simple draped cloth by using a plane with a lattice modifier.
# That's more controllable.

# Let's scrap the cloth simulation approach and use a manual fold with displacement modifier.
# Create a plane with many subdivisions, then add a wave modifier to create folds.
bpy.data.objects.remove(red_cloth, do_unlink=True)

# Red cloth
bpy.ops.mesh.primitive_grid_add(x_subdivisions=30, y_subdivisions=30, size=2.0, location=(-1.0, 1.0, 0.2))
red_cloth = bpy.context.object
red_cloth.name = 'RedCloth'
# Add subsurf for smoothness
subsurf = red_cloth.modifiers.new(name='Subsurf', type='SUBSURF')
subsurf.levels = 2
# Add wave modifier to simulate folds
wave = red_cloth.modifiers.new(name='Wave', type='WAVE')
wave.height = 0.15
wave.width = 0.5
wave.speed = 0.0  # static
# Add displacement modifier for more organic folds
displace = red_cloth.modifiers.new(name='Displace', type='DISPLACE')
displace.strength = 0.1
# Add red velvet material
red_velvet = create_velvet_material('RedVelvet', (0.6, 0.05, 0.05, 1.0), roughness=0.8, anisotropy=0.3)
red_cloth.data.materials.append(red_velvet)

# Green cloth on right
bpy.ops.mesh.primitive_grid_add(x_subdivisions=30, y_subdivisions=30, size=2.0, location=(1.0, 1.0, 0.2))
green_cloth = bpy.context.object
green_cloth.name = 'GreenCloth'
subsurf = green_cloth.modifiers.new(name='Subsurf', type='SUBSURF')
subsurf.levels = 2
wave = green_cloth.modifiers.new(name='Wave', type='WAVE')
wave.height = 0.12
wave.width = 0.6
wave.speed = 0.0
displace = green_cloth.modifiers.new(name='Displace', type='DISPLACE')
displace.strength = 0.08
green_velvet = create_velvet_material('GreenVelvet', (0.05, 0.4, 0.05, 1.0), roughness=0.8, anisotropy=0.3)
green_cloth.data.materials.append(green_velvet)

# White cloth in center as neutral
bpy.ops.mesh.primitive_grid_add(x_subdivisions=20, y_subdivisions=20, size=1.5, location=(0.0, 0.8, 0.1))
white_cloth = bpy.context.object
white_cloth.name = 'WhiteCloth'
subsurf = white_cloth.modifiers.new(name='Subsurf', type='SUBSURF')
subsurf.levels = 1
white_velvet = create_velvet_material('WhiteVelvet', (0.8, 0.8, 0.8, 1.0), roughness=0.9, anisotropy=0.2)
white_cloth.data.materials.append(white_velvet)

# ============ Utah Teapot ============
bpy.ops.mesh.primitive_teapot_add(location=(0.0, 0.5, 0.5))
teapot = bpy.context.object
teapot.name = 'UtahTeapot'
teapot.scale = (0.8, 0.8, 0.8)
# Silver material with patina
silver_mat = create_material('Silver', (0.85, 0.85, 0.9, 1.0), roughness=0.3, metallic=0.9, specular=0.5)
# Add some patina using a texture? We'll just use a simple silver.
teapot.data.materials.append(silver_mat)

# ============ Stanford Bunny (approximation) ============
# Create a bunny shape using spheres and cylinders
bunny = bpy.data.objects.new('Bunny', None)
bpy.context.collection.objects.link(bunny)
# We'll construct a single mesh from primitives using boolean union
# But boolean union is tricky. Instead, we'll create a single mesh using a script that builds vertices.
# For simplicity, we'll use a combination of primitives parented to an empty.
bunny_assembly = bpy.data.objects.new('Bunny', None)
bpy.context.collection.objects.link(bunny_assembly)
bunny_assembly.location = (0.5, -0.2, 0.2)

# Body: scaled sphere
bpy.ops.mesh.primitive_uv_sphere_add(radius=0.4, location=(0.5, -0.2, 0.4))
body = bpy.context.object
body.name = 'BunnyBody'
body.scale = (1.0, 0.9, 0.8)
body.parent = bunny_assembly

# Head: smaller sphere
bpy.ops.mesh.primitive_uv_sphere_add(radius=0.25, location=(0.3, -0.1, 0.5))
head = bpy.context.object
head.name = 'BunnyHead'
head.scale = (1.0, 0.8, 0.8)
head.parent = bunny_assembly

# Ears: two cylinders
bpy.ops.mesh.primitive_cylinder_add(radius=0.08, depth=0.3, location=(0.25, -0.05, 0.7))
ear_l = bpy.context.object
ear_l.name = 'BunnyEarL'
ear_l.rotation_euler = (0.2, 0.0, 0.0)
ear_l.parent = bunny_assembly

bpy.ops.mesh.primitive_cylinder_add(radius=0.08, depth=0.3, location=(0.35, -0.05, 0.7))
ear_r = bpy.context.object
ear_r.name = 'BunnyEarR'
ear_r.rotation_euler = (0.2, 0.0, 0.0)
ear_r.parent = bunny_assembly

# Tail: small sphere
bpy.ops.mesh.primitive_uv_sphere_add(radius=0.1, location=(0.7, -0.2, 0.3))
tail = bpy.context.object
tail.name = 'BunnyTail'
tail.parent = bunny_assembly

# Apply bronze material to all bunny parts
bronze_mat = create_material('Bronze', (0.4, 0.2, 0.1, 1.0), roughness=0.6, metallic=0.5)
for obj in [body, head, ear_l, ear_r, tail]:
    obj.data.materials.append(bronze_mat)

# ============ Suzanne ============
bpy.ops.mesh.primitive_monkey_add(location=(-0.5, -0.3, 0.6))
suzanne = bpy.context.object
suzanne.name = 'Suzanne'
suzanne.scale = (0.5, 0.5, 0.5)
suzanne.rotation_euler = (0.0, 0.0, 0.2)
# Marble material: white with some veins? We'll use a simple white stone.
marble_mat = create_material('Marble', (0.9, 0.85, 0.8, 1.0), roughness=0.4, metallic=0.0)
suzanne.data.materials.append(marble_mat)

# ============ Checkered Sphere ============
bpy.ops.mesh.primitive_uv_sphere_add(radius=0.3, location=(0.0, 0.0, 0.5))
sphere = bpy.context.object
sphere.name = 'CheckeredSphere'
checker_mat = create_checkered_material()
sphere.data.materials.append(checker_mat)

# ============ Wine Glass (classical prop) ============
# Create a wine glass using a curve and spin
bpy.ops.curve.primitive_bezier_curve_add(location=(0.0, -0.5, 0.2))
wine_curve = bpy.context.object
wine_curve.name = 'WineGlassCurve'
# Enter edit mode, move points to form the glass profile
bpy.ops.object.mode_set(mode='EDIT')
# We'll use a simpler method: create a lathe using a mesh arc.
# Actually, we can use a circle mesh with a screw modifier.
bpy.ops.object.mode_set(mode='OBJECT')
bpy.data.objects.remove(wine_curve, do_unlink=True)

# Create a glass profile using a cylinder and a cone? Better: use a curve then convert to mesh.
# Let's use a mesh circle with a screw modifier.
bpy.ops.mesh.primitive_circle_add(vertices=16, radius=0.2, location=(0.0, -0.5, 0.2))
circle = bpy.context.object
circle.name = 'WineGlassProfile'
# Add screw modifier
screw = circle.modifiers.new(name='Screw', type='SCREW')
screw.angle = 2*math.pi
screw.axis = 'Z'
screw.steps = 32
screw.iterations = 1
screw.screw_offset = 0.0
# Adjust profile shape by scaling vertices in edit mode
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
# Scale to make a bowl shape
bpy.ops.transform.resize(value=(1.0, 1.0, 0.0))
# Move the top vertex up
# We'll need to manually shape - too many steps. Let's use a simpler glass: a cup.
# Actually, we can use a combination of a sphere and cylinder.
# Let's create a glass using a UV sphere and a cylinder, then Boolean.
# For simplicity, we'll create a wine glass by extruding a circle.
# We'll use a scripted approach: create a mesh with vertices.
# Since time is limited, I'll create a simple glass using a cylinder and a sphere.
bpy.ops.object.mode_set(mode='OBJECT')
bpy.data.objects.remove(circle, do_unlink=True)

# Create glass using a Lathe: we'll use a curve and then convert.
bpy.ops.curve.primitive_nurbs_path_add(location=(0.0, -0.5, 0.2))
curve = bpy.context.object
curve.name = 'WineGlassCurve'
bpy.ops.object.mode_set(mode='EDIT')
# Move points to form a wine glass profile
# We'll set the points manually
# Get the curve data
curve_data = curve.data
# For a NURBS path, there are 5 points by default
# We'll adjust their positions
points = curve_data.splines[0].points
# Set points to form a profile (x is radius, y is height)
# We'll assume the curve is in the XY plane for lathe? Actually, we'll use a bezier circle and then screw.
# Better: use a mesh circle and screw, but we need to shape it.
# I'll use a simpler approach: create a glass using a cylinder with a sphere for the bowl.
# That's easy and recognizable.

# Remove the curve
bpy.data.objects.remove(curve, do_unlink=True)

# Glass bowl: UV sphere, cut off top half
bpy.ops.mesh.primitive_uv_sphere_add(radius=0.25, location=(0.0, -0.5, 0.35))
bowl = bpy.context.object
bowl.name = 'GlassBowl'
# Delete top half vertices
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.mesh.bisect(plane_co=(0,0,0.35), plane_no=(0,0,1), clear_inner=True, clear_outer=False)
bpy.ops.object.mode_set(mode='OBJECT')

# Stem: cylinder
bpy.ops.mesh.primitive_cylinder_add(radius=0.04, depth=0.25, location=(0.0, -0.5, 0.2))
stem = bpy.context.object
stem.name = 'GlassStem'

# Base: cylinder
bpy.ops.mesh.primitive_cylinder_add(radius=0.12, depth=0.05, location=(0.0, -0.5, 0.075))
base = bpy.context.object
base.name = 'GlassBase'

# Apply glass material
glass_mat = create_glass_material('Glass', color=(0.9, 0.95, 1.0, 1.0), roughness=0.0, ior=1.45)
for obj in [bowl, stem, base]:
    obj.data.materials.append(glass_mat)

# ============ Additional classical prop: Apple ============
bpy.ops.mesh.primitive_uv_sphere_add(radius=0.15, location=(0.3, 0.2, 0.35))
apple = bpy.context.object
apple.name = 'Apple'
# Slightly deform to look like an apple
apple.scale = (1.0, 0.95, 0.9)
# Add stem
bpy.ops.mesh.primitive_cylinder_add(radius=0.02, depth=0.1, location=(0.3, 0.2, 0.5))
stem = bpy.context.object
stem.name = 'AppleStem'
# Apple material: red with some bump
apple_mat = create_material('Apple', (0.8, 0.1, 0.1, 1.0), roughness=0.3, metallic=0.0)
apple.data.materials.append(apple_mat)
stem_mat = create_material('Stem', (0.3, 0.2, 0.1, 1.0), roughness=0.8)
stem.data.materials.append(stem_mat)

# ============ Lighting ============
# Single dominant warm light from left side (window shaft)
# Create a spotlight
bpy.ops.object.light_add(type='SPOT', location=(-2.0, 0.5, 2.5))
spot = bpy.context.object
spot.name = 'MainLight'
spot.data.energy = 2000
spot.data.color = (1.0, 0.8, 0.5)  # warm
spot.data.spot_size = math.radians(40)
spot.data.spot_blend = 0.5
spot.rotation_euler = (0.8, 0.0, 0.3)

# Add a subtle fill light from right to not lose all shadows
bpy.ops.object.light_add(type='AREA', location=(2.0, 0.5, 1.0))
fill = bpy.context.object
fill.name = 'FillLight'
fill.data.energy = 100
fill.data.color = (0.5, 0.6, 1.0)  # cool fill
fill.data.size = 1.0

# ============ Camera ============
bpy.ops.object.camera_add(location=(0.0, -1.0, 1.2))
camera = bpy.context.object
camera.name = 'Camera'
camera.rotation_euler = (1.1, 0.0, 0.0)
bpy.context.scene.camera = camera
# Set focal length to 50mm equivalent (assuming full frame sensor? Blender default is 35mm)
camera.data.lens = 50
camera.data.sensor_fit = 'HORIZONTAL'
# Position camera to frame the composition
# We'll adjust based on objects
# Slightly elevated, intimate angle
camera.location = (0.8, -1.5, 1.0)
camera.rotation_euler = (1.2, 0.0, 0.5)

# ============ Light compartment: Cornell box elements ============
# We already have red and green cloths. Add a white wall in background?
# The task says "one strong red and one strong green element facing each other across the composition — draped cloths, wall panels, or book covers — with neutral white/grey between them"
# We have red cloth on left, green cloth on right, white cloth in center. That's good.

# ============ Final adjustments ============
# Add a table edge in lower frame: a cube that acts as the front edge of the table
bpy.ops.mesh.primitive_cube_add(size=0.1, location=(0.0, 1.2, 0.0))
edge = bpy.context.object
edge.name = 'TableEdge'
edge.scale = (3.0, 0.1, 0.1)
# Position it at the front of the table
edge.location = (0.0, 1.0, 0.05)
# Same wood material
edge.data.materials.append(wood_mat)

# Ensure all objects are properly parented? Not necessary.

# Set sampling for preview
bpy.context.scene.cycles.samples = 64
bpy.context.scene.cycles.denoising = 'OPENIMAGEDENOISE'

# Set resolution
bpy.context.scene.render.resolution_x = 1536
bpy.context.scene.render.resolution_y = 1536

# ============ Finalize ============
print("Scene built successfully.")
```