Add Legendary Moses card and production guidance

This commit is contained in:
2026-09-17 09:26:45 -07:00
parent ee0113442d
commit 5e49435127
514 changed files with 9974 additions and 13 deletions

View File

@@ -0,0 +1,33 @@
# Moses — suspended tent textile and coherent entrance
This provisional Normal candidate preserves the v4 nameplate and cloud crown. Only the lower composition and near entrance structure are revised.
- [Full preview](composed-preview-1000.png)
- [500 px preview](composed-preview-500.png)
- [V4 / V5 full comparison](v4-v5-comparison.png)
- [V4 / V5 lower junction comparison](v4-v5-junction-comparison.png)
- [Editable composition](composed-preview.svg)
- [Builder](build-preview.py)
- [Validation](preview-validation.json)
## Lower textile
The rigid plaque becomes a suspended warm linen field. Its shallow sag reveals more of Moses's robe; its left edge curves inward behind the cloud instead of running to the curtain pole as a wide blank rectangle. A fine woven hem replaces the metal plaque rules. The right corners bind to the existing frame. Its upper-left corner extends behind the cloud to a binding on the near entrance post, so the field belongs to the tent structure.
The v4 cloud contour is retained, but the narrower backing changes the relationship: the cloud occupies and follows the textile edge rather than dangling over an expansive blank field. The text retains the exact words, font faces, sizes and horizontal alignment; verse baselines move down 55 master pixels and the reference moves down 40 pixels. Measured spacing is 66 pixels above the verse and 71 pixels between verse and reference ink.
## Near entrance post
A separate authored foreground jamb follows the existing curtain-side socket and vertical. Its dark outline, warm gold face and segmented joints match the structural language of the far post. It stays visible in front of the cloud and continues to the ground. Its slight lean and stronger near-side width make the entrance read as a pair of supports. A small binding ties the textile's concealed left corner to this post.
This is an SVG-native foreground adaptation; `../../source/art-master.png` is unchanged. No donor image or source repaint is used. The approved nameplate/crown region and title glyph pixels must remain identical to v4, verified programmatically.
## Verification and scope
The builder verifies the source hash, exact text, shared font hashes and glyph coverage, safe glyph bounds, opaque backing beneath lettering, zero foreground/text intersection, and preservation of the nameplate/crown. Compositing remains at 2× resolution with premultiplied-alpha Lanczos downsampling. No glass-blurring filter is used. Static review includes the full card and matched lower-junction crops.
Rebuild from the repository:
`python3 in-progress/cards/BP-001-moses/revisions/v03/review/layout-preview-astra-v5/build-preview.py`
Requires Inkscape, Fontconfig, Pillow and NumPy. Earlier candidates remain intact. This is a review candidate only: no production layers, printing exports, finish masks, harness fixture, GPU review or promotion.

View File

@@ -0,0 +1,242 @@
#!/usr/bin/env python3
"""Provisional BP-001 Moses Legendary composition; no production assembly."""
from pathlib import Path
import hashlib,json,os,subprocess
from xml.sax.saxutils import escape
import numpy as np
from PIL import Image,ImageDraw,ImageFont
P=Path(__file__).resolve().parent
REV=P.parents[1]
REPO=next(p for p in P.parents if (p/'docs/card-layer-pipeline.md').is_file())
ART=REV/'source/art-master.png'
FONTS=REPO/'fonts'
sha=lambda p:hashlib.sha256(p.read_bytes()).hexdigest()
art_hash=sha(ART)
font_manifest=json.loads((FONTS/'manifest.json').read_text())
font_files={'title':'P052-Bold.otf','verse':'SanctificationP052-Medium.otf','reference':'SanctificationP052-Medium.otf'}
for name in set(font_files.values()):
assert sha(FONTS/name)==font_manifest['files'][name]['sha256']
(P/'font-cache').mkdir(exist_ok=True)
(P/'fonts.conf').write_text(f'<fontconfig><dir>{FONTS}</dir><cachedir>{P/"font-cache"}</cachedir></fontconfig>')
env=dict(os.environ,FONTCONFIG_FILE=str(P/'fonts.conf'))
def ink(*args):return subprocess.check_output(['inkscape',*map(str,args)],env=env,text=True,stderr=subprocess.PIPE).strip()
def render(src,dst):
# Supersample only geometry-heavy composite/foreground outputs; text measurements
# remain native. LANCZOS uses premultiplied RGBA via Pillow's RGBa path.
factor=2 if src.stem in {'foreground-curtain','composed-preview','without-curtain'} else 1
ink(src,'--export-type=png',f'--export-filename={dst}',f'--export-width={2000*factor}',f'--export-height={2800*factor}')
if factor>1:
im=Image.open(dst).convert('RGBA')
im.convert('RGBa').resize((2000,2800),Image.Resampling.LANCZOS).convert('RGBA').save(dst)
def resolve(family,style):return Path(subprocess.check_output(['fc-match','-f','%{file}',f'{family}:style={style}'],env=env,text=True).strip()).resolve()
assert resolve('P052','Bold')==(FONTS/'P052-Bold.otf').resolve()
assert resolve('Sanctification P052','Medium')==(FONTS/'SanctificationP052-Medium.otf').resolve()
head='<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="2000" height="2800" viewBox="0 0 2000 2800">'
defs='''<defs>
<linearGradient id="paper" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#fbf3df"/><stop offset=".5" stop-color="#f4ead4"/><stop offset="1" stop-color="#e8d4b0"/></linearGradient>
<linearGradient id="gold" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#f0d47d"/><stop offset=".48" stop-color="#bd8b31"/><stop offset="1" stop-color="#71501e"/></linearGradient>
<pattern id="weave" width="44" height="44" patternUnits="userSpaceOnUse"><path d="M0 11H44 M0 33H44 M11 0V44 M33 0V44" stroke="#9d783d" stroke-width="1" opacity=".045"/><path d="M22 7l7 15-7 15-7-15Z" fill="none" stroke="#806438" stroke-width="1" opacity=".07"/></pattern>
<pattern id="lapis-band" width="34" height="34" patternUnits="userSpaceOnUse"><path d="M17 2L32 17 17 32 2 17Z" fill="#173e59" stroke="#d7b35a" stroke-width="3"/><circle cx="17" cy="17" r="4" fill="url(#gold)"/></pattern>
<clipPath id="curtain-clip"><path d="M-10-10 H770 C734 204 694 395 650 585 C610 770 566 972 522 1170 C474 1388 426 1604 378 1818 C338 2000 302 2190 286 2380 C276 2510 304 2640 412 2810 H-10Z"/></clipPath>
</defs>'''
base='<image xlink:href="../../source/art-master.png" width="2000" height="2800"/>'
frame='''<g id="legendary-frame">
<rect x="14" y="14" width="1972" height="2772" rx="34" fill="none" stroke="#2c2115" stroke-width="28"/>
<rect x="25" y="25" width="1950" height="2750" rx="27" fill="none" stroke="url(#gold)" stroke-width="14"/>
<rect x="43" y="43" width="1914" height="2714" rx="20" fill="none" stroke="#173e59" stroke-width="10"/>
<rect x="55" y="55" width="1890" height="2690" rx="15" fill="none" stroke="#d7b35a" stroke-width="4"/>
<path d="M68 260V68H260 M1740 68H1932V260 M68 2540V2732H260 M1740 2732H1932V2540" fill="none" stroke="#9b6d26" stroke-width="6"/>
<g fill="url(#gold)" stroke="#533810" stroke-width="3"><path d="M68 68l14 14-14 14-14-14Z"/><path d="M1932 68l14 14-14 14-14-14Z"/><path d="M68 2732l14 14-14 14-14-14Z"/><path d="M1932 2732l14 14-14 14-14-14Z"/></g>
<rect x="1906" y="430" width="27" height="255" fill="url(#lapis-band)" opacity=".95"/><rect x="1906" y="2115" width="27" height="255" fill="url(#lapis-band)" opacity=".95"/>
</g>'''
# The panels tuck behind the original tent and the pillar of cloud.
# No alteration, retouch, rescaling or regeneration of the selected source.
title_path='M700 62 H1900 Q1940 62 1940 102 V342 H700Z'
verse_path='M340 2106 C445 2114 550 2118 655 2140 C960 2230 1435 2260 1960 2095 L1960 2720 C1535 2782 1045 2728 580 2750 C488 2755 437 2684 453 2592 C462 2506 520 2470 574 2404 C625 2341 641 2230 655 2140 L340 2106Z'
title_inner='<path d="M710 82 H1898 Q1920 82 1920 104 V320 H710" fill="none" stroke="#bb9040" stroke-width="3"/><path d="M1070 301H1870" stroke="#c5a257" stroke-width="2"/>'
verse_inner='''<path d="M690 2170 C1000 2250 1470 2250 1925 2138 M490 2677 C516 2726 551 2728 591 2725 C1060 2702 1530 2757 1925 2696" fill="none" stroke="#a17533" stroke-width="3"/><path d="M688 2182 C1000 2262 1470 2262 1923 2150 M489 2665 C516 2714 551 2716 591 2713 C1060 2690 1530 2745 1925 2684" fill="none" stroke="#8c3d3c" stroke-width="2" opacity=".65"/><path d="M1925 2140V2695" fill="none" stroke="#b7924a" stroke-width="3" stroke-dasharray="7 9"/>'''
def panel(path,inner,ident):
if ident=='verse-backing':
# A linen field with a woven seam, rather than a metallic plaque.
return f'<g id="{ident}"><path d="{path}" transform="translate(3 6)" fill="#211b15" opacity=".6"/><path d="{path}" fill="url(#linen)" stroke="#79552b" stroke-width="4"/><path d="{path}" fill="url(#weave)"/>{inner}</g>'
return f'<g id="{ident}"><path d="{path}" fill="url(#paper)" stroke="#62421e" stroke-width="9"/><path d="{path}" fill="url(#weave)" stroke="url(#gold)" stroke-width="4"/>{inner}</g>'
defs=defs.replace('</defs>','<linearGradient id="linen" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#f7edd4"/><stop offset=".55" stop-color="#f2e7cd"/><stop offset="1" stop-color="#e8d7b6"/></linearGradient></defs>')
# Ties sit at the existing right rail and visibly support the hanging field.
ties='<g fill="none" stroke-linecap="round">'+''.join(f'<path d="M1940 {y+9} C1950 {y-4} 1964 {y-5} 1977 {y-1}" stroke="#362719" stroke-width="13"/><path d="M1940 {y+9} C1950 {y-4} 1964 {y-5} 1977 {y-1}" stroke="#d6b260" stroke-width="7"/>' for y in [2110,2700])+'</g>'
panels=panel(title_path,title_inner,'title-backing')+panel(verse_path,verse_inner,'verse-backing')+ties
# Trace the visible source silhouette, including the whole cloud lower lobe.
# Upper crown is separately registered; both layers stay at source coordinates.
# Cubic contours follow the source lead, not the rough v3 clipping guesses.
# The lower closing arc ends above the old terrain occlusion.
top_edge='M714 0 C741 25 750 55 746 82 C787 58 814 43 846 45 C926 50 980 112 952 191 C1008 238 1002 319 925 369'
lower_edge='M736 2080 C728 2148 722 2213 710 2262 C704 2290 684 2310 660 2323 C650 2348 629 2367 608 2374 C599 2432 541 2477 480 2479 C435 2481 375 2418 354 2350'
cloud_top=top_edge+' L730 475 H0 V0Z'
cloud_low='M0 400H352 V1780H765 C765 1920 748 2010 736 2080 '+lower_edge.removeprefix('M736 2080 ')+' L350 2633 Q286 2659 205 2635 Q101 2583 0 2612Z'
defs=defs.replace('</defs>',f'<clipPath id="cloud-top"><path d="{cloud_top}"/></clipPath><clipPath id="cloud-low"><path d="{cloud_low}"/></clipPath></defs>')
# Quiet fine-gold enclosure: motifs reside in the original curtain and cloud.
frame='''<g id="legendary-frame"><rect x="19" y="19" width="1962" height="2762" rx="28" fill="none" stroke="#251c15" stroke-width="21"/><rect x="23" y="23" width="1954" height="2754" rx="23" fill="none" stroke="url(#gold)" stroke-width="11"/><rect x="43" y="43" width="1914" height="2714" rx="15" fill="none" stroke="#e2bf6c" stroke-width="3"/></g>'''
# A narrow opaque lead edge removes blue/terrain fringe without softening glass.
lead=f'<g fill="none" stroke="#2b2116" stroke-width="8" stroke-linejoin="round" stroke-linecap="round"><path d="{top_edge}"/><path d="{lower_edge}"/></g>'
curtain='<g id="presence-foreground">'+''.join(f'<g clip-path="url(#{clip})"><image xlink:href="../../source/art-master.png" width="2000" height="2800"/></g>' for clip in ['cloud-top','cloud-low'])+lead+'</g>'
# Near entrance jamb: aligned to the source curtain-side upper socket.
# The cloud sits behind this continuous dark/gold glass structure.
defs=defs.replace('</defs>','<linearGradient id="post-gold" x1="0" y1="0" x2="1" y2="0"><stop offset="0" stop-color="#654016"/><stop offset=".25" stop-color="#b78829"/><stop offset=".52" stop-color="#f5dc75"/><stop offset=".72" stop-color="#d8ae45"/><stop offset="1" stop-color="#80601c"/></linearGradient></defs>')
post_path='M326 250 L360 250 L350 2640 L316 2637Z'
post=f'<g id="near-entrance-post"><path d="{post_path}" fill="url(#post-gold)" stroke="#241c12" stroke-width="7" stroke-linejoin="round"/>'
# Uneven joints follow the counterpart post's segmented construction.
for y in [310,493,720,944,1186,1432,1669,1915,2167,2406,2590]:
x=326-(y-250)*10/2390
post+=f'<path d="M{x} {y}l34 1v15l-34-1Z" fill="#372916" stroke="#17170f" stroke-width="3"/><path d="M{x+7} {y+22}l0 101" fill="none" stroke="#f4d373" stroke-width="3" opacity=".5"/>'
post+='<path d="M326 254Q342 242 359 254 M319 2625Q333 2633 350 2629" fill="none" stroke="#edcf70" stroke-width="4"/>'
# A small woven binding holds the concealed left textile corner to the jamb.
post+='<path d="M317 2108 C324 2101 345 2101 357 2110 L355 2124 C341 2114 326 2115 317 2121Z" fill="#b88b3f" stroke="#2b2216" stroke-width="4"/><path d="M319 2113 C333 2109 344 2112 355 2117" fill="none" stroke="#f2d484" stroke-width="3"/></g>'
curtain+=post
(P/'foreground-curtain.svg').write_text(head+defs+curtain+'</svg>');render(P/'foreground-curtain.svg',P/'foreground-curtain.png')
fg=Image.open(P/'foreground-curtain.png').convert('RGBA')
fa=np.asarray(fg.getchannel('A'))
assert fa.max()==255 and fa.min()==0
rows=[
('title','MOSES',1458,244,142,'P052',700),
('verse-0','And there has not arisen a',1320,2360,74,'Sanctification P052',500),
('verse-1','prophet since in Israel like Moses,',1320,2468,74,'Sanctification P052',500),
('verse-2','whom the LORD knew face to face',1320,2576,74,'Sanctification P052',500),
('reference','Deuteronomy 34:10 • ESV',1320,2690,57,'Sanctification P052',500),
]
for ident,content,*_ in rows:
fontfile=font_files['title' if ident=='title' else 'reference' if ident=='reference' else 'verse']
charset=subprocess.check_output(['fc-query','--format=%{charset}',str(FONTS/fontfile)],text=True).split()
ranges=[tuple(int(v,16) for v in token.split('-')) for token in charset]
assert all(any(r[0]<=ord(char)<=r[-1] for r in ranges) for char in content), (ident, 'missing glyph')
texts=''.join(f'<text id="{ident}" x="{x}" y="{y}" font-family="{family}" font-weight="{weight}" font-size="{size}" text-anchor="middle" fill="#263f50">{escape(text)}</text>' for ident,text,x,y,size,family,weight in rows)
(P/'text-preview.svg').write_text(head+texts+'</svg>');render(P/'text-preview.svg',P/'text-preview.png')
text_image=Image.open(P/'text-preview.png').convert('RGBA');ta=np.asarray(text_image.getchannel('A'))
assert not np.any((ta>0)&(fa>0))
(P/'border-backing-preview.svg').write_text(head+defs+frame+panels+'</svg>');render(P/'border-backing-preview.svg',P/'border-backing-preview.png')
backing=np.asarray(Image.open(P/'border-backing-preview.png').convert('RGBA').getchannel('A'))
checks=[];verse_bounds=[]
for ident,text,x,y,size,family,weight in rows:
node=next(v for v in texts.split('</text>') if f'id="{ident}"' in v)+'</text>'
src=P/(ident+'-ink.svg');dst=P/(ident+'-ink.png');src.write_text(head+node+'</svg>');render(src,dst)
alpha=np.asarray(Image.open(dst).convert('RGBA').getchannel('A'));ys,xs=np.where(alpha>0);assert len(xs)
bounds=[int(xs.min()),int(ys.min()),int(xs.max()+1),int(ys.max()+1)]
safe=[1050,90,1905,330] if ident=='title' else [730,2275,1900,2730]
assert safe[0]<=bounds[0] and safe[1]<=bounds[1] and bounds[2]<=safe[2] and bounds[3]<=safe[3],(ident,bounds,safe)
assert np.all(backing[alpha>0]==255),ident
assert not np.any((alpha>0)&(fa>0)),ident
checks.append({'id':ident,'text':text,'baseline':y,'font':family,'weight':weight,'size':size,'inkBounds':bounds,'safeBounds':safe,'glyphPixelsOutsideOpaqueBacking':0,'foregroundIntersectionPixels':0})
if ident.startswith('verse-'):verse_bounds.append(bounds)
assert ' '.join(c['text'] for c in checks if c['id'].startswith('verse-'))==json.loads((REV/'card.json').read_text())['excerpt']
assert checks[-1]['text']==json.loads((REV/'card.json').read_text())['referenceDisplay']
union=[min(b[0] for b in verse_bounds),min(b[1] for b in verse_bounds),max(b[2] for b in verse_bounds),max(b[3] for b in verse_bounds)]
upper=2240;ref_top=checks[-1]['inkBounds'][1];above=union[1]-upper;below=ref_top-union[3]
assert min(above,below)>=35 and abs(above-below)<=20,(above,below)
for name,include_curtain in [('composed-preview',True),('without-curtain',False)]:
body=base+frame+panels+(curtain if include_curtain else '')+texts
src=P/(name+'.svg');src.write_text(head+defs+body+'</svg>');render(src,P/(name+'-2000.png'))
master=Image.open(P/(name+'-2000.png')).convert('RGB')
master.resize((1000,1400),Image.Resampling.LANCZOS).save(P/(name+'-1000.png'))
master.resize((500,700),Image.Resampling.LANCZOS).save(P/(name+'-500.png'))
# Compare the structural effect of the attached curtain.
board=Image.new('RGB',(1000,750),'#11161c');draw=ImageDraw.Draw(board)
for i,(label,path) in enumerate([('Panels before foreground',P/'without-curtain-1000.png'),('Pillar of cloud over panels',P/'composed-preview-1000.png')]):
draw.text((i*500+12,12),label,fill='#f4ead4')
image=Image.open(path).convert('RGB');image.thumbnail((480,672));board.paste(image,(i*500+10,45))
board.save(P/'curtain-comparison.png')
# A focused junction comparison at master crop.
detail=Image.new('RGB',(1200,620),'#11161c');draw=ImageDraw.Draw(detail)
for i,(label,path) in enumerate([('Without foreground',P/'without-curtain-2000.png'),('Cloud and curtain over backing',P/'composed-preview-2000.png')]):
draw.text((i*600+12,12),label,fill='#f4ead4')
image=Image.open(path).convert('RGB').crop((0,1780,900,2800));image.thumbnail((575,545));detail.paste(image,(i*600+12,52))
detail.save(P/'curtain-junction-detail.png')
frame_alpha=np.asarray(Image.open(P/'border-backing-preview.png').convert('RGBA').getchannel('A'))
foreground_frame_overlap=int(np.count_nonzero((fa>0)&(frame_alpha>0)))
report={
'status':'passed',
'scope':'Provisional Legendary Normal — suspended tent textile',
'canvas':[2000,2800],
'sourceArtwork':{'path':str(ART.relative_to(REPO)),'sha256':art_hash,'unchanged':sha(ART)==art_hash},
'template':{
'governingIdea':'Nameplate unchanged; a suspended linen field tucks behind the pillar of cloud and ties to the right frame',
'titleBackingPath':title_path,
'verseBackingPath':verse_path,
'compositionOrder':['base-art','frame-and-backings','attached-curtain-foreground','deterministic-text'],
'curtainContour':'foreground-curtain.svg',
'cloudUpperContour':cloud_top,
'cloudLowerContour':cloud_low,
'curtainAlphaBounds':list(Image.fromarray(fa).getbbox()),
'curtainFrameOrBackingOverlapPixels':foreground_frame_overlap,
},
'typography':{
'fontManifestSHA256':sha(FONTS/'manifest.json'),
'checks':checks,
'verseInkUnion':union,
'upperFlourishToVerseInk':above,
'verseInkToReferenceLineBox':below,
'centeringDifference':abs(above-below),
'lineBaselineGap':108,
},
'structuralChecks':{
'curtainTextIntersectionPixels':int(np.count_nonzero((ta>0)&(fa>0))),
'glyphPixelsOutsideOpaqueBacking':0,
'artworkSHA256Unchanged':True,
'fontGlyphCoverage':'passed',
'fontManifestHashes':'passed',
},
'notPerformed':['Borderless/Textless/Boundless composition','Production finish masks','Production text masks','Harness fixture installation','GPU moving-light validation','Final card approval'],
'staticReview':'Inspected at card size and enlarged cloud joins; provisional, pending user review',
'edgeRendering':{'geometry':'source-traced cubic Bezier paths','leadWidthMasterPixels':8,'renderScale':2,'downsample':'premultiplied RGBA LANCZOS','blur':False},
'foregroundAdaptation':'V4 cloud and nameplate preserved. Lower linen field now curves behind the cloud along its side; shallow sag exposes more robe. Lower text moves down with exact words, faces, sizes and horizontal alignment retained. A continuous near entrance post is authored over the cloud, with the textile left corner bound to it behind the cloud.'
}
(P/'preview-validation.json').write_text(json.dumps(report,indent=2)+'\n')
assert sha(ART)==art_hash
print(json.dumps({'status':'passed','preview':str((P/'composed-preview-1000.png').relative_to(REPO)),'curtainOverlapPixels':foreground_frame_overlap,'spacing':[above,below]},indent=2))
# Reproducible v3/v4 presentation and focused interaction review.
label_font=ImageFont.truetype(str(FONTS/'P052-Bold.otf'),26)
board=Image.new('RGB',(1500,1100),'#11161c');d=ImageDraw.Draw(board)
for i,(label,path) in enumerate([('Astra v4 — rigid lower plaque',P.parent/'layout-preview-astra-v4/composed-preview-1000.png'),('Astra v5 — suspended textile',P/'composed-preview-1000.png')]):
d.text((i*750+24,18),label,font=label_font,fill='#f4ead4')
im=Image.open(path).convert('RGB');im.thumbnail((730,1022));board.paste(im,(i*750+10,63))
board.save(P/'v4-v5-comparison.png')
master=Image.open(P/'composed-preview-2000.png').convert('RGB')
detail=Image.new('RGB',(1500,1050),'#11161c');d=ImageDraw.Draw(detail)
for label,box,origin,size in [('Cloud crowns the name',(570,0,1140,470),(22,65),(650,536)),('Pillar enters the verse field',(220,1960,850,2580),(795,65),(650,640))]:
d.text((origin[0],20),label,font=label_font,fill='#f4ead4')
im=master.crop(box);im.thumbnail(size);detail.paste(im,origin)
im=master.crop((250,2080,1980,2780));im.thumbnail((1450,320));detail.paste(im,(25,722))
detail.save(P/'cloud-interaction-detail.png')
# The approved nameplate and cloud crown remain pixel-identical.
previous=P.parent/'layout-preview-astra-v4'
current_image=np.asarray(Image.open(P/'composed-preview-2000.png'))
previous_image=np.asarray(Image.open(previous/'composed-preview-2000.png'))
assert np.array_equal(current_image[:200],previous_image[:200])
assert np.array_equal(current_image[:500,600:],previous_image[:500,600:])
assert np.array_equal(np.asarray(Image.open(P/'title-ink.png')),np.asarray(Image.open(previous/'title-ink.png')))
report['structuralChecks']['titleAndCloudCrownRegionIdenticalToV4']=True
report['structuralChecks']['titlePixelsIdenticalToV4']=True
report['structuralChecks']['nearEntrancePostAuthored']=True
report['template']['nearEntrancePostPath']=post_path
report['template']['leftTextileAnchor']=[340,2106]
report['edgeRendering']['alphaEdge']='Premultiplied before downsampling; no transparent-RGB halo'
(P/'preview-validation.json').write_text(json.dumps(report,indent=2)+'\n')
# Matched lower card crops show the textile silhouette and interaction.
board=Image.new('RGB',(1480,930),'#11161c');d=ImageDraw.Draw(board)
for col,(label,folder) in enumerate([('V4 — rigid plaque',previous),('V5 — suspended textile',P)]):
d.text((col*740+22,18),label,font=label_font,fill='#f4ead4')
im=Image.open(folder/'composed-preview-2000.png').convert('RGB')
crop=im.crop((190,1940,2000,2800));crop.thumbnail((710,850));board.paste(crop,(col*740+15,75))
detail=im.crop((340,2050,830,2790));detail.thumbnail((380,485));board.paste(detail,(col*740+185,425))
board.save(P/'v4-v5-junction-comparison.png')

View File

@@ -0,0 +1 @@
<fontconfig><dir>/home/dkzver/docker/sanctification/fonts</dir><cachedir>/home/dkzver/docker/sanctification/in-progress/cards/BP-001-moses/revisions/v03/review/layout-preview-astra-v5/font-cache</cachedir></fontconfig>

View File

@@ -0,0 +1,192 @@
{
"status": "passed",
"scope": "Provisional Legendary Normal \u2014 suspended tent textile",
"canvas": [
2000,
2800
],
"sourceArtwork": {
"path": "in-progress/cards/BP-001-moses/revisions/v03/source/art-master.png",
"sha256": "73f933cf34719dfda09b719376948bcf072bec504a88329b5a22e4ac1ed76d85",
"unchanged": true
},
"template": {
"governingIdea": "Nameplate unchanged; a suspended linen field tucks behind the pillar of cloud and ties to the right frame",
"titleBackingPath": "M700 62 H1900 Q1940 62 1940 102 V342 H700Z",
"verseBackingPath": "M340 2106 C445 2114 550 2118 655 2140 C960 2230 1435 2260 1960 2095 L1960 2720 C1535 2782 1045 2728 580 2750 C488 2755 437 2684 453 2592 C462 2506 520 2470 574 2404 C625 2341 641 2230 655 2140 L340 2106Z",
"compositionOrder": [
"base-art",
"frame-and-backings",
"attached-curtain-foreground",
"deterministic-text"
],
"curtainContour": "foreground-curtain.svg",
"cloudUpperContour": "M714 0 C741 25 750 55 746 82 C787 58 814 43 846 45 C926 50 980 112 952 191 C1008 238 1002 319 925 369 L730 475 H0 V0Z",
"cloudLowerContour": "M0 400H352 V1780H765 C765 1920 748 2010 736 2080 C728 2148 722 2213 710 2262 C704 2290 684 2310 660 2323 C650 2348 629 2367 608 2374 C599 2432 541 2477 480 2479 C435 2481 375 2418 354 2350 L350 2633 Q286 2659 205 2635 Q101 2583 0 2612Z",
"curtainAlphaBounds": [
0,
0,
996,
2714
],
"curtainFrameOrBackingOverlapPixels": 189298,
"nearEntrancePostPath": "M326 250 L360 250 L350 2640 L316 2637Z",
"leftTextileAnchor": [
340,
2106
]
},
"typography": {
"fontManifestSHA256": "ef68ee6393981d54ca068a950f21c53b374d20e6e46c21c788f36380d3aae178",
"checks": [
{
"id": "title",
"text": "MOSES",
"baseline": 244,
"font": "P052",
"weight": 700,
"size": 142,
"inkBounds": [
1202,
145,
1711,
247
],
"safeBounds": [
1050,
90,
1905,
330
],
"glyphPixelsOutsideOpaqueBacking": 0,
"foregroundIntersectionPixels": 0
},
{
"id": "verse-0",
"text": "And there has not arisen a",
"baseline": 2360,
"font": "Sanctification P052",
"weight": 500,
"size": 74,
"inkBounds": [
890,
2306,
1749,
2362
],
"safeBounds": [
730,
2275,
1900,
2730
],
"glyphPixelsOutsideOpaqueBacking": 0,
"foregroundIntersectionPixels": 0
},
{
"id": "verse-1",
"text": "prophet since in Israel like Moses,",
"baseline": 2468,
"font": "Sanctification P052",
"weight": 500,
"size": 74,
"inkBounds": [
769,
2414,
1869,
2490
],
"safeBounds": [
730,
2275,
1900,
2730
],
"glyphPixelsOutsideOpaqueBacking": 0,
"foregroundIntersectionPixels": 0
},
{
"id": "verse-2",
"text": "whom the LORD knew face to face",
"baseline": 2576,
"font": "Sanctification P052",
"weight": 500,
"size": 74,
"inkBounds": [
752,
2521,
1886,
2578
],
"safeBounds": [
730,
2275,
1900,
2730
],
"glyphPixelsOutsideOpaqueBacking": 0,
"foregroundIntersectionPixels": 0
},
{
"id": "reference",
"text": "Deuteronomy 34:10 \u2022 ESV",
"baseline": 2690,
"font": "Sanctification P052",
"weight": 500,
"size": 57,
"inkBounds": [
991,
2649,
1649,
2707
],
"safeBounds": [
730,
2275,
1900,
2730
],
"glyphPixelsOutsideOpaqueBacking": 0,
"foregroundIntersectionPixels": 0
}
],
"verseInkUnion": [
752,
2306,
1886,
2578
],
"upperFlourishToVerseInk": 66,
"verseInkToReferenceLineBox": 71,
"centeringDifference": 5,
"lineBaselineGap": 108
},
"structuralChecks": {
"curtainTextIntersectionPixels": 0,
"glyphPixelsOutsideOpaqueBacking": 0,
"artworkSHA256Unchanged": true,
"fontGlyphCoverage": "passed",
"fontManifestHashes": "passed",
"titleAndCloudCrownRegionIdenticalToV4": true,
"titlePixelsIdenticalToV4": true,
"nearEntrancePostAuthored": true
},
"notPerformed": [
"Borderless/Textless/Boundless composition",
"Production finish masks",
"Production text masks",
"Harness fixture installation",
"GPU moving-light validation",
"Final card approval"
],
"staticReview": "Inspected at card size and enlarged cloud joins; provisional, pending user review",
"edgeRendering": {
"geometry": "source-traced cubic Bezier paths",
"leadWidthMasterPixels": 8,
"renderScale": 2,
"downsample": "premultiplied RGBA LANCZOS",
"blur": false,
"alphaEdge": "Premultiplied before downsampling; no transparent-RGB halo"
},
"foregroundAdaptation": "V4 cloud and nameplate preserved. Lower linen field now curves behind the cloud along its side; shallow sag exposes more robe. Lower text moves down with exact words, faces, sizes and horizontal alignment retained. A continuous near entrance post is authored over the cloud, with the textile left corner bound to it behind the cloud."
}