验收完成40%
This commit is contained in:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user