验收完成40%

This commit is contained in:
2026-07-17 08:08:31 +08:00
parent 23cfd365a1
commit 887481a4ab
172 changed files with 855 additions and 577 deletions
@@ -1,7 +1,9 @@
{
"schemaVersion": 1,
"page": "G01",
"status": "candidates-pending-user-selection",
"status": "long-flagship-selected-for-genealogy-module",
"selectedCandidateId": "g01-list-background-long-flagship",
"runtimeOutput": "static/assets/modules/genealogy/opaque/genealogy-page-background-long.png",
"generatedDate": "2026-07-16",
"referenceAssets": [
"static/assets/foundation/transparent/footer-mountain-bamboo.png",
@@ -44,6 +46,17 @@
"processingMode": "opaque-paper",
"sourcePixels": { "width": 1024, "height": 1536 },
"prompt": "1024x1536 纵向 G01 列表背景独立资产;暖宣纸底,顶部 5% 即出现云气、枝叶和淡金竹影,左侧约 40% 高度放置亭台,山水从中部连续延伸到底部,中央低对比度;无文字、无 UI、无卡片、无标志、无红色。"
},
{
"id": "g01-list-background-long-flagship",
"title": "旗舰长屏连续云竹亭台宣纸景",
"imageGenSource": "docs/design/assets/g01-background/masters/genealogy-page-background-long-flagship-imagegen-source.png",
"master": "docs/design/assets/g01-background/masters/genealogy-page-background-long-flagship-master.png",
"generatedOutput": "design-pipeline/generated/g01-background/genealogy-page-background-long-flagship.png",
"processingMode": "opaque-paper-resize",
"sourcePixels": { "width": 1536, "height": 3840 },
"outputPixels": { "width": 1440, "height": 3600 },
"prompt": "1536x3840 纵向 G 类型共享连续长背景;延续 C 方向的暖灰宣纸、淡金竹影和灰褐水墨,顶部疏竹淡云作为短屏安全裁切区,中段低对比留给页面内容,下半部连续布置亭台、湖面、远山和岸边竹影;无文字、无 Logo、无 UI、无卡片、无按钮、无红色元素、无横向接缝、无重复图案。"
}
]
}
@@ -10,8 +10,8 @@ const manifestPath = path.join(pipelineDirectory, 'manifests', 'g01-background-c
const reportPath = path.join(pipelineDirectory, 'generated', 'g01-background', 'build-report.json')
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
if (manifest.schemaVersion !== 1 || manifest.page !== 'G01' || manifest.candidates?.length !== 3) {
throw new Error('G01 background manifest must declare exactly three schema v1 candidates')
if (manifest.schemaVersion !== 1 || manifest.page !== 'G01' || manifest.candidates?.length !== 4) {
throw new Error('G01 background manifest must declare exactly four schema v1 candidates')
}
for (const candidate of manifest.candidates) {
@@ -35,9 +35,11 @@ def process_chroma_image(source: Image.Image, config: dict[str, Any]) -> Image.I
return output
def process_opaque_image(source: Image.Image) -> Image.Image:
def process_opaque_image(source: Image.Image, output_size: tuple[int, int] | None = None) -> Image.Image:
"""Normalize a paper-backed master to deterministic opaque RGBA."""
output = source.convert("RGBA")
if output_size is not None and output.size != output_size:
output = output.resize(output_size, Image.Resampling.LANCZOS)
output.putalpha(255)
return output
@@ -83,8 +85,12 @@ def build(manifest_path: Path, workspace: Path, report_path: Path) -> list[dict[
)
if candidate["processingMode"] == "chroma-key":
output = process_chroma_image(opened, chroma)
elif candidate["processingMode"] == "opaque-paper":
output = process_opaque_image(opened)
elif candidate["processingMode"] in {"opaque-paper", "opaque-paper-resize"}:
output_size = None
if candidate["processingMode"] == "opaque-paper-resize":
target = candidate["outputPixels"]
output_size = (target["width"], target["height"])
output = process_opaque_image(opened, output_size)
else:
raise ValueError(f"unsupported processingMode: {candidate['processingMode']}")
@@ -9,20 +9,22 @@ const workspace = path.resolve(testDirectory, '..', '..')
const manifestPath = path.join(workspace, 'design-pipeline', 'manifests', 'g01-background-candidates.json')
const wrapperPath = path.join(workspace, 'design-pipeline', 'scripts', 'build-g01-backgrounds.mjs')
test('G01 background candidate manifest preserves three portable masters', async () => {
test('G01 background candidate manifest preserves four portable masters and selects the approved long background', async () => {
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'))
assert.equal(manifest.schemaVersion, 1)
assert.equal(manifest.page, 'G01')
assert.equal(manifest.status, 'candidates-pending-user-selection')
assert.equal(manifest.candidates.length, 3)
assert.equal(manifest.status, 'long-flagship-selected-for-genealogy-module')
assert.equal(manifest.selectedCandidateId, 'g01-list-background-long-flagship')
assert.equal(manifest.runtimeOutput, 'static/assets/modules/genealogy/opaque/genealogy-page-background-long.png')
assert.equal(manifest.candidates.length, 4)
const ids = new Set()
for (const candidate of manifest.candidates) {
assert.ok(!ids.has(candidate.id), `duplicate candidate id: ${candidate.id}`)
ids.add(candidate.id)
assert.match(candidate.prompt, /1024x1536/)
assert.deepEqual(candidate.sourcePixels, { width: 1024, height: 1536 })
assert.ok(['chroma-key', 'opaque-paper'].includes(candidate.processingMode))
assert.match(candidate.prompt, /\d+x\d+/)
assert.ok(candidate.sourcePixels.width > 0 && candidate.sourcePixels.height > 0)
assert.ok(['chroma-key', 'opaque-paper', 'opaque-paper-resize'].includes(candidate.processingMode))
for (const key of ['master', 'generatedOutput']) {
const absolute = path.resolve(workspace, candidate[key])
@@ -30,10 +32,16 @@ test('G01 background candidate manifest preserves three portable masters', async
assert.ok(!relative.startsWith('..') && !path.isAbsolute(relative), `${key} escapes workspace`)
}
}
const selected = manifest.candidates.find(({ id }) => id === manifest.selectedCandidateId)
assert.deepEqual(selected.sourcePixels, { width: 1536, height: 3840 })
assert.deepEqual(selected.outputPixels, { width: 1440, height: 3600 })
assert.equal(selected.processingMode, 'opaque-paper-resize')
})
test('G01 Node wrapper delegates pixel decoding to locked Pillow without Sharp', async () => {
const wrapper = await readFile(wrapperPath, 'utf8')
assert.doesNotMatch(wrapper, /from ['"]sharp['"]/)
assert.match(wrapper, /build_g01_backgrounds\.py/)
assert.match(wrapper, /candidates\?\.length !== 4/)
})
@@ -43,6 +43,13 @@ class G01BackgroundBuilderTests(unittest.TestCase):
self.assertEqual("RGBA", output.mode)
self.assertEqual((240, 235, 220, 255), output.getpixel((0, 0)))
def test_opaque_master_can_be_resized_to_locked_runtime_dimensions(self):
source = Image.new("RGB", (2, 3), (240, 235, 220))
output = process_opaque_image(source, (4, 6))
self.assertEqual((4, 6), output.size)
self.assertEqual("RGBA", output.mode)
self.assertEqual((240, 235, 220, 255), output.getpixel((3, 5)))
def test_saved_png_declares_srgb_color_profile(self):
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "asset.png"