#!/usr/bin/env python3 """test_moon_composite.py — smoke-test the east-sky-backdrop composite. Uses the sample east frame (21-07-00.jpg) already in the repo and a procedurally generated moon disc (no camera timestamp, no copyright) to demonstrate the east-sky backdrop without needing a live NASA API call. Run from the sky-cam directory: python3 test_moon_composite.py Output: test_composite_out.jpg The east frame (2026-04-29 21:06:45) shows the moon through visible thin cloud cover; that sky — blurred to atmospheric haze — becomes the backdrop behind the procedural moon disc. In production the disc is replaced by the NASA SVS Dial-a-Moon render for the exact UTC hour east captured the moon. """ import pathlib import sys from datetime import datetime, timezone HERE = pathlib.Path(__file__).resolve().parent sys.path.insert(0, str(HERE)) EAST_FRAME = HERE / '21-07-00.jpg' OUT_PATH = HERE / 'test_composite_out.jpg' def _make_procedural_moon(size: int = 2048) -> 'Image': """Generate a clean grey disc that stands in for a NASA Dial-a-Moon render. Draws a base disc, a few darker ellipses for lunar maria, and a subtle limb-darkening gradient. No timestamp, no copyright, no camera artefacts. """ from PIL import Image, ImageDraw, ImageFilter img = Image.new('RGB', (size, size), (0, 0, 0)) draw = ImageDraw.Draw(img) cx = cy = size // 2 r = int(size * 0.47) # Base disc — warm grey, slightly off-white like a real moon draw.ellipse([cx - r, cy - r, cx + r, cy + r], fill=(218, 214, 200)) # A few darker patches suggesting the major maria draw.ellipse([cx - r//3, cy - r//3, cx + r//6, cy + r//5], fill=(170, 167, 154)) draw.ellipse([cx + r//8, cy - r//5, cx + r//3, cy + r//8], fill=(182, 179, 166)) draw.ellipse([cx - r//4, cy + r//6, cx + r//8, cy + r//3], fill=(175, 172, 159)) draw.ellipse([cx - r//2, cy - r//10, cx - r//5, cy + r//4], fill=(185, 182, 169)) # Smooth the hard edges so it blends naturally img = img.filter(ImageFilter.GaussianBlur(radius=size // 80)) # Subtle limb darkening: blend a radial dark vignette at the disc edge vignette = Image.new('L', (size, size), 0) vd = ImageDraw.Draw(vignette) vd.ellipse([cx - r, cy - r, cx + r, cy + r], fill=255) vignette = vignette.filter(ImageFilter.GaussianBlur(radius=size // 30)) import numpy as np arr = np.asarray(img).astype(float) vig = np.asarray(vignette).astype(float) / 255.0 # Darken towards the limb (where vig is small → near edge) darken = 0.75 + 0.25 * vig # 0.75 at edge, 1.0 at centre arr = np.clip(arr * darken[..., None], 0, 255).astype('uint8') img = Image.fromarray(arr) return img def main(): if not EAST_FRAME.exists(): print(f'ERROR: missing {EAST_FRAME}', file=sys.stderr) sys.exit(1) # Save the procedural disc to a temp file so render_phase_closeup can open it import tempfile, os with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tf: tmp_moon = tf.name try: print('=== moon composite smoke-test ===') print(f'east frame : {EAST_FRAME.name}') print(f'moon disc : procedural (NASA Dial-a-Moon stand-in)') print(f'output : {OUT_PATH.name}') print() print('generating procedural moon disc …') moon_img = _make_procedural_moon(2048) moon_img.save(tmp_moon) from moon_composite import render_phase_closeup caption = ( 'Full Moon — April 2026 — ' 'sky-cam east witnessed at 2026-04-29 21:07:00 UTC ' '(+7 min from exact full) — render: NASA SVS Dial-a-Moon' ) print('rendering composite with east sky backdrop …') render_phase_closeup( nasa_render_path=tmp_moon, out_path=str(OUT_PATH), output_size=(1920, 1080), moon_height_pct=0.88, caption=caption, east_frame_path=str(EAST_FRAME), east_sky_enabled=True, east_sky_blur=0, # auto: output_width / 6 = 320 px ) finally: os.unlink(tmp_moon) print(f'\ndone → {OUT_PATH}') print() print('What you should see:') print(' • Background: east’s April 29 night sky, blurred to soft atmospheric haze') print(' (thin cloud cover visible as a grey-blue gradient, treeline at bottom)') print(' • Moon disc filling ~88% of the 1920×1080 frame, pasted sharp on top') print(' • Caption at bottom-left with phase / date / witness text') print() print('In production the procedural disc is replaced by the NASA SVS Dial-a-Moon') print('render for that exact UTC hour — same layout, real crater detail.') if __name__ == '__main__': main()