diff --git a/.memory/worklog.json b/.memory/worklog.json index 5e56eaa..4de0a0c 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -1,12 +1,5 @@ { "entries": [ - { - "files_changed": 1, - "hash": "a072d49", - "message": "auto-save 2026-05-13 06:51 (~1)", - "ts": "2026-05-13T06:51:56+08:00", - "type": "commit" - }, { "files_changed": 1, "hash": "3417408", @@ -3283,6 +3276,13 @@ "type": "session-heartbeat", "message": "Codex 会话活跃 · 最近命令:codex · 1 项未提交变更 · 最近提交:auto-save 2026-05-14 12:53 (~2)", "files_changed": 1 + }, + { + "ts": "2026-05-14T12:59:37+08:00", + "type": "commit", + "message": "auto-save 2026-05-14 12:59 (~1)", + "hash": "887c9a0", + "files_changed": 1 } ] } diff --git a/api/character_library/skg-characters/images/character-01-back.png b/api/character_library/skg-characters/images/character-01-back.png new file mode 100644 index 0000000..ca927cb Binary files /dev/null and b/api/character_library/skg-characters/images/character-01-back.png differ diff --git a/api/character_library/skg-characters/images/character-01-back_detail.png b/api/character_library/skg-characters/images/character-01-back_detail.png new file mode 100644 index 0000000..c5b80fe Binary files /dev/null and b/api/character_library/skg-characters/images/character-01-back_detail.png differ diff --git a/api/character_library/skg-characters/images/character-01-bust.png b/api/character_library/skg-characters/images/character-01-bust.png new file mode 100644 index 0000000..c84c376 Binary files /dev/null and b/api/character_library/skg-characters/images/character-01-bust.png differ diff --git a/api/character_library/skg-characters/images/character-01-front.png b/api/character_library/skg-characters/images/character-01-front.png new file mode 100644 index 0000000..39a2ce4 Binary files /dev/null and b/api/character_library/skg-characters/images/character-01-front.png differ diff --git a/api/character_library/skg-characters/images/character-01-left_45.png b/api/character_library/skg-characters/images/character-01-left_45.png new file mode 100644 index 0000000..53c845a Binary files /dev/null and b/api/character_library/skg-characters/images/character-01-left_45.png differ diff --git a/api/character_library/skg-characters/images/character-01-right_45.png b/api/character_library/skg-characters/images/character-01-right_45.png new file mode 100644 index 0000000..b2fe78e Binary files /dev/null and b/api/character_library/skg-characters/images/character-01-right_45.png differ diff --git a/api/character_library/skg-characters/images/character-01-side.png b/api/character_library/skg-characters/images/character-01-side.png new file mode 100644 index 0000000..020f53e Binary files /dev/null and b/api/character_library/skg-characters/images/character-01-side.png differ diff --git a/api/character_library/skg-characters/images/character-02-back.png b/api/character_library/skg-characters/images/character-02-back.png new file mode 100644 index 0000000..b90894e Binary files /dev/null and b/api/character_library/skg-characters/images/character-02-back.png differ diff --git a/api/character_library/skg-characters/images/character-02-back_detail.png b/api/character_library/skg-characters/images/character-02-back_detail.png new file mode 100644 index 0000000..fce0a91 Binary files /dev/null and b/api/character_library/skg-characters/images/character-02-back_detail.png differ diff --git a/api/character_library/skg-characters/images/character-02-bust.png b/api/character_library/skg-characters/images/character-02-bust.png new file mode 100644 index 0000000..ad1227b Binary files /dev/null and b/api/character_library/skg-characters/images/character-02-bust.png differ diff --git a/api/character_library/skg-characters/images/character-02-front.png b/api/character_library/skg-characters/images/character-02-front.png new file mode 100644 index 0000000..650d51a Binary files /dev/null and b/api/character_library/skg-characters/images/character-02-front.png differ diff --git a/api/character_library/skg-characters/images/character-02-left_45.png b/api/character_library/skg-characters/images/character-02-left_45.png new file mode 100644 index 0000000..88ed799 Binary files /dev/null and b/api/character_library/skg-characters/images/character-02-left_45.png differ diff --git a/api/character_library/skg-characters/images/character-02-right_45.png b/api/character_library/skg-characters/images/character-02-right_45.png new file mode 100644 index 0000000..79818b8 Binary files /dev/null and b/api/character_library/skg-characters/images/character-02-right_45.png differ diff --git a/api/character_library/skg-characters/images/character-02-side.png b/api/character_library/skg-characters/images/character-02-side.png new file mode 100644 index 0000000..93e98e6 Binary files /dev/null and b/api/character_library/skg-characters/images/character-02-side.png differ diff --git a/api/character_library/skg-characters/images/character-03-back.png b/api/character_library/skg-characters/images/character-03-back.png new file mode 100644 index 0000000..cdf215d Binary files /dev/null and b/api/character_library/skg-characters/images/character-03-back.png differ diff --git a/api/character_library/skg-characters/images/character-03-back_detail.png b/api/character_library/skg-characters/images/character-03-back_detail.png new file mode 100644 index 0000000..ed94ad9 Binary files /dev/null and b/api/character_library/skg-characters/images/character-03-back_detail.png differ diff --git a/api/character_library/skg-characters/images/character-03-bust.png b/api/character_library/skg-characters/images/character-03-bust.png new file mode 100644 index 0000000..a673929 Binary files /dev/null and b/api/character_library/skg-characters/images/character-03-bust.png differ diff --git a/api/character_library/skg-characters/images/character-03-front.png b/api/character_library/skg-characters/images/character-03-front.png new file mode 100644 index 0000000..ee1ecab Binary files /dev/null and b/api/character_library/skg-characters/images/character-03-front.png differ diff --git a/api/character_library/skg-characters/images/character-03-left_45.png b/api/character_library/skg-characters/images/character-03-left_45.png new file mode 100644 index 0000000..70402c8 Binary files /dev/null and b/api/character_library/skg-characters/images/character-03-left_45.png differ diff --git a/api/character_library/skg-characters/images/character-03-right_45.png b/api/character_library/skg-characters/images/character-03-right_45.png new file mode 100644 index 0000000..c7c7a11 Binary files /dev/null and b/api/character_library/skg-characters/images/character-03-right_45.png differ diff --git a/api/character_library/skg-characters/images/character-03-side.png b/api/character_library/skg-characters/images/character-03-side.png new file mode 100644 index 0000000..a09ed5f Binary files /dev/null and b/api/character_library/skg-characters/images/character-03-side.png differ diff --git a/api/character_library/skg-characters/images/character-04-back.png b/api/character_library/skg-characters/images/character-04-back.png new file mode 100644 index 0000000..b814b3c Binary files /dev/null and b/api/character_library/skg-characters/images/character-04-back.png differ diff --git a/api/character_library/skg-characters/images/character-04-back_detail.png b/api/character_library/skg-characters/images/character-04-back_detail.png new file mode 100644 index 0000000..82960fd Binary files /dev/null and b/api/character_library/skg-characters/images/character-04-back_detail.png differ diff --git a/api/character_library/skg-characters/images/character-04-bust.png b/api/character_library/skg-characters/images/character-04-bust.png new file mode 100644 index 0000000..24b557d Binary files /dev/null and b/api/character_library/skg-characters/images/character-04-bust.png differ diff --git a/api/character_library/skg-characters/images/character-04-front.png b/api/character_library/skg-characters/images/character-04-front.png new file mode 100644 index 0000000..a6c9dbb Binary files /dev/null and b/api/character_library/skg-characters/images/character-04-front.png differ diff --git a/api/character_library/skg-characters/images/character-04-left_45.png b/api/character_library/skg-characters/images/character-04-left_45.png new file mode 100644 index 0000000..876a4b5 Binary files /dev/null and b/api/character_library/skg-characters/images/character-04-left_45.png differ diff --git a/api/character_library/skg-characters/images/character-04-right_45.png b/api/character_library/skg-characters/images/character-04-right_45.png new file mode 100644 index 0000000..9e402b0 Binary files /dev/null and b/api/character_library/skg-characters/images/character-04-right_45.png differ diff --git a/api/character_library/skg-characters/images/character-04-side.png b/api/character_library/skg-characters/images/character-04-side.png new file mode 100644 index 0000000..a1337ee Binary files /dev/null and b/api/character_library/skg-characters/images/character-04-side.png differ diff --git a/api/character_library/skg-characters/images/character-05-back.png b/api/character_library/skg-characters/images/character-05-back.png new file mode 100644 index 0000000..e7942ef Binary files /dev/null and b/api/character_library/skg-characters/images/character-05-back.png differ diff --git a/api/character_library/skg-characters/images/character-05-back_detail.png b/api/character_library/skg-characters/images/character-05-back_detail.png new file mode 100644 index 0000000..42b8ccb Binary files /dev/null and b/api/character_library/skg-characters/images/character-05-back_detail.png differ diff --git a/api/character_library/skg-characters/images/character-05-bust.png b/api/character_library/skg-characters/images/character-05-bust.png new file mode 100644 index 0000000..58ce060 Binary files /dev/null and b/api/character_library/skg-characters/images/character-05-bust.png differ diff --git a/api/character_library/skg-characters/images/character-05-front.png b/api/character_library/skg-characters/images/character-05-front.png new file mode 100644 index 0000000..2e22077 Binary files /dev/null and b/api/character_library/skg-characters/images/character-05-front.png differ diff --git a/api/character_library/skg-characters/images/character-05-left_45.png b/api/character_library/skg-characters/images/character-05-left_45.png new file mode 100644 index 0000000..65536de Binary files /dev/null and b/api/character_library/skg-characters/images/character-05-left_45.png differ diff --git a/api/character_library/skg-characters/images/character-05-right_45.png b/api/character_library/skg-characters/images/character-05-right_45.png new file mode 100644 index 0000000..07b04eb Binary files /dev/null and b/api/character_library/skg-characters/images/character-05-right_45.png differ diff --git a/api/character_library/skg-characters/images/character-05-side.png b/api/character_library/skg-characters/images/character-05-side.png new file mode 100644 index 0000000..b4178ec Binary files /dev/null and b/api/character_library/skg-characters/images/character-05-side.png differ diff --git a/api/character_library/skg-characters/manifest.json b/api/character_library/skg-characters/manifest.json new file mode 100644 index 0000000..1905b50 --- /dev/null +++ b/api/character_library/skg-characters/manifest.json @@ -0,0 +1,367 @@ +{ + "source": "/Users/kangwan/Desktop/skg_anatomy_characters_20260514_120852", + "model": "gpt-image-2", + "quality": "high", + "characters": [ + { + "id": "character-01", + "name": "运动阳光男", + "folder": "01_运动阳光男", + "description": "运动阳光男透明骨架人角色,含正面、左右45度、侧面、背面、半身近景和背部特写参考。", + "primary_image": "character-01-front", + "images": [ + { + "id": "character-01-front", + "view": "front", + "label": "正面", + "filename": "images/character-01-front.png", + "width": 1536, + "height": 2048, + "source_path": "/Users/kangwan/Desktop/skg_anatomy_characters_20260514_120852/01_运动阳光男/01_正面.png" + }, + { + "id": "character-01-left_45", + "view": "left_45", + "label": "左45度", + "filename": "images/character-01-left_45.png", + "width": 1536, + "height": 2048, + "source_path": "/Users/kangwan/Desktop/skg_anatomy_characters_20260514_120852/01_运动阳光男/02_左45度.png" + }, + { + "id": "character-01-right_45", + "view": "right_45", + "label": "右45度", + "filename": "images/character-01-right_45.png", + "width": 1536, + "height": 2048, + "source_path": "/Users/kangwan/Desktop/skg_anatomy_characters_20260514_120852/01_运动阳光男/03_右45度.png" + }, + { + "id": "character-01-side", + "view": "side", + "label": "侧面", + "filename": "images/character-01-side.png", + "width": 1536, + "height": 2048, + "source_path": "/Users/kangwan/Desktop/skg_anatomy_characters_20260514_120852/01_运动阳光男/04_侧面.png" + }, + { + "id": "character-01-back", + "view": "back", + "label": "背面", + "filename": "images/character-01-back.png", + "width": 1536, + "height": 2048, + "source_path": "/Users/kangwan/Desktop/skg_anatomy_characters_20260514_120852/01_运动阳光男/05_背面.png" + }, + { + "id": "character-01-bust", + "view": "bust", + "label": "半身近景", + "filename": "images/character-01-bust.png", + "width": 2048, + "height": 2048, + "source_path": "/Users/kangwan/Desktop/skg_anatomy_characters_20260514_120852/01_运动阳光男/06_半身近景.png" + }, + { + "id": "character-01-back_detail", + "view": "back_detail", + "label": "背部特写", + "filename": "images/character-01-back_detail.png", + "width": 2048, + "height": 2048, + "source_path": "/Users/kangwan/Desktop/skg_anatomy_characters_20260514_120852/01_运动阳光男/07_背部特写.png" + } + ] + }, + { + "id": "character-02", + "name": "都市型男", + "folder": "02_都市型男", + "description": "都市型男透明骨架人角色,含正面、左右45度、侧面、背面、半身近景和背部特写参考。", + "primary_image": "character-02-front", + "images": [ + { + "id": "character-02-front", + "view": "front", + "label": "正面", + "filename": "images/character-02-front.png", + "width": 1536, + "height": 2048, + "source_path": "/Users/kangwan/Desktop/skg_anatomy_characters_20260514_120852/02_都市型男/01_正面.png" + }, + { + "id": "character-02-left_45", + "view": "left_45", + "label": "左45度", + "filename": "images/character-02-left_45.png", + "width": 1536, + "height": 2048, + "source_path": "/Users/kangwan/Desktop/skg_anatomy_characters_20260514_120852/02_都市型男/02_左45度.png" + }, + { + "id": "character-02-right_45", + "view": "right_45", + "label": "右45度", + "filename": "images/character-02-right_45.png", + "width": 1536, + "height": 2048, + "source_path": "/Users/kangwan/Desktop/skg_anatomy_characters_20260514_120852/02_都市型男/03_右45度.png" + }, + { + "id": "character-02-side", + "view": "side", + "label": "侧面", + "filename": "images/character-02-side.png", + "width": 1536, + "height": 2048, + "source_path": "/Users/kangwan/Desktop/skg_anatomy_characters_20260514_120852/02_都市型男/04_侧面.png" + }, + { + "id": "character-02-back", + "view": "back", + "label": "背面", + "filename": "images/character-02-back.png", + "width": 1536, + "height": 2048, + "source_path": "/Users/kangwan/Desktop/skg_anatomy_characters_20260514_120852/02_都市型男/05_背面.png" + }, + { + "id": "character-02-bust", + "view": "bust", + "label": "半身近景", + "filename": "images/character-02-bust.png", + "width": 2048, + "height": 2048, + "source_path": "/Users/kangwan/Desktop/skg_anatomy_characters_20260514_120852/02_都市型男/06_半身近景.png" + }, + { + "id": "character-02-back_detail", + "view": "back_detail", + "label": "背部特写", + "filename": "images/character-02-back_detail.png", + "width": 2048, + "height": 2048, + "source_path": "/Users/kangwan/Desktop/skg_anatomy_characters_20260514_120852/02_都市型男/07_背部特写.png" + } + ] + }, + { + "id": "character-03", + "name": "优雅白领女", + "folder": "03_优雅白领女", + "description": "优雅白领女透明骨架人角色,含正面、左右45度、侧面、背面、半身近景和背部特写参考。", + "primary_image": "character-03-front", + "images": [ + { + "id": "character-03-front", + "view": "front", + "label": "正面", + "filename": "images/character-03-front.png", + "width": 1536, + "height": 2048, + "source_path": "/Users/kangwan/Desktop/skg_anatomy_characters_20260514_120852/03_优雅白领女/01_正面.png" + }, + { + "id": "character-03-left_45", + "view": "left_45", + "label": "左45度", + "filename": "images/character-03-left_45.png", + "width": 1536, + "height": 2048, + "source_path": "/Users/kangwan/Desktop/skg_anatomy_characters_20260514_120852/03_优雅白领女/02_左45度.png" + }, + { + "id": "character-03-right_45", + "view": "right_45", + "label": "右45度", + "filename": "images/character-03-right_45.png", + "width": 1536, + "height": 2048, + "source_path": "/Users/kangwan/Desktop/skg_anatomy_characters_20260514_120852/03_优雅白领女/03_右45度.png" + }, + { + "id": "character-03-side", + "view": "side", + "label": "侧面", + "filename": "images/character-03-side.png", + "width": 1536, + "height": 2048, + "source_path": "/Users/kangwan/Desktop/skg_anatomy_characters_20260514_120852/03_优雅白领女/04_侧面.png" + }, + { + "id": "character-03-back", + "view": "back", + "label": "背面", + "filename": "images/character-03-back.png", + "width": 1536, + "height": 2048, + "source_path": "/Users/kangwan/Desktop/skg_anatomy_characters_20260514_120852/03_优雅白领女/05_背面.png" + }, + { + "id": "character-03-bust", + "view": "bust", + "label": "半身近景", + "filename": "images/character-03-bust.png", + "width": 2048, + "height": 2048, + "source_path": "/Users/kangwan/Desktop/skg_anatomy_characters_20260514_120852/03_优雅白领女/06_半身近景.png" + }, + { + "id": "character-03-back_detail", + "view": "back_detail", + "label": "背部特写", + "filename": "images/character-03-back_detail.png", + "width": 2048, + "height": 2048, + "source_path": "/Users/kangwan/Desktop/skg_anatomy_characters_20260514_120852/03_优雅白领女/07_背部特写.png" + } + ] + }, + { + "id": "character-04", + "name": "运动辣妹", + "folder": "04_运动辣妹", + "description": "运动辣妹透明骨架人角色,含正面、左右45度、侧面、背面、半身近景和背部特写参考。", + "primary_image": "character-04-front", + "images": [ + { + "id": "character-04-front", + "view": "front", + "label": "正面", + "filename": "images/character-04-front.png", + "width": 1536, + "height": 2048, + "source_path": "/Users/kangwan/Desktop/skg_anatomy_characters_20260514_120852/04_运动辣妹/01_正面.png" + }, + { + "id": "character-04-left_45", + "view": "left_45", + "label": "左45度", + "filename": "images/character-04-left_45.png", + "width": 1536, + "height": 2048, + "source_path": "/Users/kangwan/Desktop/skg_anatomy_characters_20260514_120852/04_运动辣妹/02_左45度.png" + }, + { + "id": "character-04-right_45", + "view": "right_45", + "label": "右45度", + "filename": "images/character-04-right_45.png", + "width": 1536, + "height": 2048, + "source_path": "/Users/kangwan/Desktop/skg_anatomy_characters_20260514_120852/04_运动辣妹/03_右45度.png" + }, + { + "id": "character-04-side", + "view": "side", + "label": "侧面", + "filename": "images/character-04-side.png", + "width": 1536, + "height": 2048, + "source_path": "/Users/kangwan/Desktop/skg_anatomy_characters_20260514_120852/04_运动辣妹/04_侧面.png" + }, + { + "id": "character-04-back", + "view": "back", + "label": "背面", + "filename": "images/character-04-back.png", + "width": 1536, + "height": 2048, + "source_path": "/Users/kangwan/Desktop/skg_anatomy_characters_20260514_120852/04_运动辣妹/05_背面.png" + }, + { + "id": "character-04-bust", + "view": "bust", + "label": "半身近景", + "filename": "images/character-04-bust.png", + "width": 2048, + "height": 2048, + "source_path": "/Users/kangwan/Desktop/skg_anatomy_characters_20260514_120852/04_运动辣妹/06_半身近景.png" + }, + { + "id": "character-04-back_detail", + "view": "back_detail", + "label": "背部特写", + "filename": "images/character-04-back_detail.png", + "width": 2048, + "height": 2048, + "source_path": "/Users/kangwan/Desktop/skg_anatomy_characters_20260514_120852/04_运动辣妹/07_背部特写.png" + } + ] + }, + { + "id": "character-05", + "name": "绅士大叔", + "folder": "05_绅士大叔", + "description": "绅士大叔透明骨架人角色,含正面、左右45度、侧面、背面、半身近景和背部特写参考。", + "primary_image": "character-05-front", + "images": [ + { + "id": "character-05-front", + "view": "front", + "label": "正面", + "filename": "images/character-05-front.png", + "width": 1536, + "height": 2048, + "source_path": "/Users/kangwan/Desktop/skg_anatomy_characters_20260514_120852/05_绅士大叔/01_正面.png" + }, + { + "id": "character-05-left_45", + "view": "left_45", + "label": "左45度", + "filename": "images/character-05-left_45.png", + "width": 1536, + "height": 2048, + "source_path": "/Users/kangwan/Desktop/skg_anatomy_characters_20260514_120852/05_绅士大叔/02_左45度.png" + }, + { + "id": "character-05-right_45", + "view": "right_45", + "label": "右45度", + "filename": "images/character-05-right_45.png", + "width": 1536, + "height": 2048, + "source_path": "/Users/kangwan/Desktop/skg_anatomy_characters_20260514_120852/05_绅士大叔/03_右45度.png" + }, + { + "id": "character-05-side", + "view": "side", + "label": "侧面", + "filename": "images/character-05-side.png", + "width": 1536, + "height": 2048, + "source_path": "/Users/kangwan/Desktop/skg_anatomy_characters_20260514_120852/05_绅士大叔/04_侧面.png" + }, + { + "id": "character-05-back", + "view": "back", + "label": "背面", + "filename": "images/character-05-back.png", + "width": 1536, + "height": 2048, + "source_path": "/Users/kangwan/Desktop/skg_anatomy_characters_20260514_120852/05_绅士大叔/05_背面.png" + }, + { + "id": "character-05-bust", + "view": "bust", + "label": "半身近景", + "filename": "images/character-05-bust.png", + "width": 2048, + "height": 2048, + "source_path": "/Users/kangwan/Desktop/skg_anatomy_characters_20260514_120852/05_绅士大叔/06_半身近景.png" + }, + { + "id": "character-05-back_detail", + "view": "back_detail", + "label": "背部特写", + "filename": "images/character-05-back_detail.png", + "width": 2048, + "height": 2048, + "source_path": "/Users/kangwan/Desktop/skg_anatomy_characters_20260514_120852/05_绅士大叔/07_背部特写.png" + } + ] + } + ] +} \ No newline at end of file diff --git a/api/main.py b/api/main.py index f859fae..32ec6b1 100644 --- a/api/main.py +++ b/api/main.py @@ -30,6 +30,10 @@ PRODUCT_LIBRARY_DIR = Path( os.getenv("PRODUCT_LIBRARY_DIR", Path(__file__).resolve().parent / "product_library" / "skg-products") ).resolve() PRODUCT_LIBRARY_MANIFEST = PRODUCT_LIBRARY_DIR / "manifest.json" +CHARACTER_LIBRARY_DIR = Path( + os.getenv("CHARACTER_LIBRARY_DIR", Path(__file__).resolve().parent / "character_library" / "skg-characters") +).resolve() +CHARACTER_LIBRARY_MANIFEST = CHARACTER_LIBRARY_DIR / "manifest.json" LLM_BASE_URL = os.getenv("LLM_BASE_URL", "").strip() LLM_API_KEY = os.getenv("LLM_API_KEY", "").strip() @@ -306,6 +310,26 @@ class ProductLibraryItem(BaseModel): tags: list[str] = Field(default_factory=list) +class CharacterLibraryImage(BaseModel): + id: str + view: str + label: str + filename: str + width: int = 0 + height: int = 0 + source_path: str = "" + url: str = "" + + +class CharacterLibraryItem(BaseModel): + id: str + name: str + folder: str = "" + description: str = "" + primary_image: str = "" + images: list[CharacterLibraryImage] = Field(default_factory=list) + + class ProductFusionRegion(BaseModel): x: float = 0 y: float = 0 @@ -319,6 +343,10 @@ class ProductFusionShot(BaseModel): last_image: dict | None = None product_images: list[dict] = Field(default_factory=list) product_image: dict | None = None + character_id: str = "" + character_name: str = "" + subject_image: dict | None = None + subject_images: list[dict] = Field(default_factory=list) person_image: dict | None = None product_region: ProductFusionRegion | None = None scene_image: dict | None = None @@ -541,6 +569,41 @@ def product_library_file(item: ProductLibraryItem) -> Path: return p +def load_character_library_items() -> list[CharacterLibraryItem]: + if not CHARACTER_LIBRARY_MANIFEST.exists(): + return [] + try: + data = json.loads(CHARACTER_LIBRARY_MANIFEST.read_text(encoding="utf-8")) + items: list[CharacterLibraryItem] = [] + for raw in data.get("characters", []): + item = CharacterLibraryItem(**raw) + for image in item.images: + image.url = f"/character-library/skg/images/{image.filename}" + items.append(item) + return items + except Exception as e: + raise HTTPException(500, f"character library manifest invalid: {e}") + + +def find_character_library_item(character_id: str) -> CharacterLibraryItem: + character_id = character_id.strip() + for item in load_character_library_items(): + if item.id == character_id: + return item + raise HTTPException(404, "character library item not found") + + +def character_library_file(filename: str) -> Path: + p = (CHARACTER_LIBRARY_DIR / filename).resolve() + try: + p.relative_to(CHARACTER_LIBRARY_DIR) + except ValueError: + raise HTTPException(400, "invalid character library path") + if not p.exists(): + raise HTTPException(404, "character library image missing") + return p + + def storyboard_ref_url(job_id: str, ref: dict | None) -> str: if not ref: return "" @@ -3310,6 +3373,7 @@ class GenerateStoryboardVideoReq(BaseModel): last_image: dict | None = None product_images: list[dict] = Field(default_factory=list) subject_image: dict | None = None + subject_images: list[dict] = Field(default_factory=list) scene_image: dict | None = None product_image: dict | None = None action_image: dict | None = None @@ -3433,6 +3497,7 @@ def submit_video_create( source_ref: VideoSourceRef | None = None, last_img: Path | None = None, product_imgs: list[Path] | None = None, + primary_role: str = "first_frame", ): if video_uses_ark(): content = [{"type": "text", "text": payload["prompt"]}] @@ -3448,7 +3513,7 @@ def submit_video_create( { "type": "image_url", "image_url": {"url": ark_reference_data_url(ref_img)}, - "role": "first_frame", + "role": primary_role, } ) if last_img and last_img.exists(): @@ -3505,6 +3570,7 @@ def render_storyboard_video( source_ref: VideoSourceRef | None = None, last_ref_path: Path | None = None, product_ref_paths: list[Path] | None = None, + primary_role: str = "first_frame", ) -> None: import httpx @@ -3534,16 +3600,16 @@ def render_storyboard_video( create = None create_errors: list[str] = [] for create_path in VIDEO_CREATE_PATHS: - resp = submit_video_create(client, f"{base}{video_path(create_path)}", headers, ref_img, payload, source_ref, prepared_last_img, prepared_product_imgs) + resp = submit_video_create(client, f"{base}{video_path(create_path)}", headers, ref_img, payload, source_ref, prepared_last_img, prepared_product_imgs, primary_role) if video_uses_ark() and source_ref and resp.status_code in {400, 422}: create_errors.append(f"{video_path(create_path)} + reference_video -> HTTP {resp.status_code}: {resp.text[:160]}") - resp = submit_video_create(client, f"{base}{video_path(create_path)}", headers, ref_img, payload, None, prepared_last_img, prepared_product_imgs) + resp = submit_video_create(client, f"{base}{video_path(create_path)}", headers, ref_img, payload, None, prepared_last_img, prepared_product_imgs, primary_role) if video_uses_ark() and prepared_last_img and resp.status_code in {400, 422}: create_errors.append(f"{video_path(create_path)} + last_frame -> HTTP {resp.status_code}: {resp.text[:160]}") - resp = submit_video_create(client, f"{base}{video_path(create_path)}", headers, ref_img, payload, None, None, prepared_product_imgs) + resp = submit_video_create(client, f"{base}{video_path(create_path)}", headers, ref_img, payload, None, None, prepared_product_imgs, primary_role) if video_uses_ark() and prepared_product_imgs and resp.status_code in {400, 422}: create_errors.append(f"{video_path(create_path)} + product_reference -> HTTP {resp.status_code}: {resp.text[:160]}") - resp = submit_video_create(client, f"{base}{video_path(create_path)}", headers, ref_img, payload, None, prepared_last_img, None) + resp = submit_video_create(client, f"{base}{video_path(create_path)}", headers, ref_img, payload, None, prepared_last_img, None, primary_role) if resp.status_code < 400: create = resp break @@ -3601,6 +3667,7 @@ def generate_storyboard_video(job_id: str, idx: int, req: GenerateStoryboardVide raise HTTPException(400, "prompt required") ref = req.first_image or req.subject_image or req.product_image or req.scene_image or req.action_image + primary_role = "first_frame" if req.first_image else "reference_image" ref_path = storyboard_ref_path(job_id, ref) or (job_dir(job_id) / "frames" / f"{idx:03d}.jpg") if not ref_path.exists(): raise HTTPException(404, "reference image missing") @@ -3608,6 +3675,14 @@ def generate_storyboard_video(job_id: str, idx: int, req: GenerateStoryboardVide last_ref_path = storyboard_ref_path(job_id, req.last_image) raw_product_refs = req.product_images[:6] if req.product_images else ([req.product_image] if req.product_image else []) product_ref_paths = [p for p in (storyboard_ref_path(job_id, r) for r in raw_product_refs) if p] + subject_ref_paths = [p for p in (storyboard_ref_path(job_id, r) for r in req.subject_images[:8]) if p] + reference_ref_paths = [] + seen_ref_paths: set[str] = set() + for p in [*subject_ref_paths, *product_ref_paths]: + key = str(p) + if key not in seen_ref_paths: + reference_ref_paths.append(p) + seen_ref_paths.add(key) local_id = uuid.uuid4().hex[:12] model = resolve_video_model(req.model) @@ -3629,7 +3704,7 @@ def generate_storyboard_video(job_id: str, idx: int, req: GenerateStoryboardVide source_ref = req.source_ref if source_ref and source_ref.kind == "source_video" and not source_ref.url: source_ref = None - bg.add_task(render_storyboard_video, job_id, local_id, "", ref_path, prompt, model, seconds, req.size, source_ref, last_ref_path, product_ref_paths) + bg.add_task(render_storyboard_video, job_id, local_id, "", ref_path, prompt, model, seconds, req.size, source_ref, last_ref_path, reference_ref_paths, primary_role) return job @@ -3645,6 +3720,10 @@ class CopyProductLibraryAssetReq(BaseModel): product_id: str +class CopyCharacterLibraryAssetReq(BaseModel): + character_id: str + + @app.get("/product-library/skg", response_model=list[ProductLibraryItem]) def list_skg_product_library() -> list[ProductLibraryItem]: """内置 SKG 白底产品图库。来源是本地筛选后的产品图 manifest。""" @@ -3660,6 +3739,19 @@ def get_skg_product_library_image(filename: str): return FileResponse(product_library_file(item), media_type="image/jpeg") +@app.get("/character-library/skg", response_model=list[CharacterLibraryItem]) +def list_skg_character_library() -> list[CharacterLibraryItem]: + """内置透明骨架人角色库。来源是桌面生成的 5 个角色参考组。""" + return load_character_library_items() + + +@app.get("/character-library/skg/images/{filename:path}") +def get_skg_character_library_image(filename: str): + p = character_library_file(filename) + media_type = "image/png" if p.suffix.lower() == ".png" else "image/jpeg" + return FileResponse(p, media_type=media_type) + + @app.post("/jobs/{job_id}/assets") async def upload_storyboard_asset(job_id: str, file: UploadFile = File(...)) -> dict: if job_id not in JOBS: @@ -3716,6 +3808,38 @@ def copy_product_library_asset(job_id: str, req: CopyProductLibraryAssetReq) -> } +@app.post("/jobs/{job_id}/assets/character-library") +def copy_character_library_assets(job_id: str, req: CopyCharacterLibraryAssetReq) -> dict: + if job_id not in JOBS: + raise HTTPException(404, "job not found") + character = find_character_library_item(req.character_id) + out_dir = job_dir(job_id) / "assets" + out_dir.mkdir(parents=True, exist_ok=True) + refs = [] + for image in character.images: + src = character_library_file(image.filename) + asset_id = uuid.uuid4().hex[:12] + out = out_dir / f"{asset_id}.jpg" + try: + img = Image.open(src).convert("RGB") + img.thumbnail((1600, 1600), Image.Resampling.LANCZOS) + img.save(out, "JPEG", quality=94) + except Exception as e: + raise HTTPException(400, f"character library copy failed: {e}") + refs.append({ + "kind": "asset", + "frame_idx": -1, + "element_id": asset_id, + "cutout_id": asset_id, + "label": f"角色 · {character.name} · {image.label}", + }) + return { + "character_id": character.id, + "character_name": character.name, + "images": refs, + } + + def product_image_alpha(img: Image.Image) -> Image.Image: rgba = img.convert("RGBA") rgb = rgba.convert("RGB") diff --git a/web/lib/api.ts b/web/lib/api.ts index 8cd08cb..6ddcfbc 100644 --- a/web/lib/api.ts +++ b/web/lib/api.ts @@ -71,6 +71,10 @@ export interface ProductFusionShot { last_image?: ImageRef | null product_images?: ImageRef[] product_image?: ImageRef | null + character_id?: string + character_name?: string + subject_image?: ImageRef | null + subject_images?: ImageRef[] person_image?: ImageRef | null product_region?: ProductFusionRegion | null scene_image?: ImageRef | null @@ -165,6 +169,32 @@ export async function copyProductLibraryAsset(jobId: string, productId: string): return res.json() } +export async function listCharacterLibrary(): Promise { + const res = await fetch(`${API_BASE}/character-library/skg`) + if (!res.ok) { + const txt = await res.text().catch(() => "") + throw new Error(`listCharacterLibrary ${res.status} ${txt.slice(0, 300)}`) + } + return res.json() +} + +export function characterLibraryImageUrl(filename: string): string { + return `${API_BASE}/character-library/skg/images/${filename}` +} + +export async function copyCharacterLibraryAssets(jobId: string, characterId: string): Promise<{ character_id: string; character_name: string; images: ImageRef[] }> { + const res = await fetch(`${API_BASE}/jobs/${jobId}/assets/character-library`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ character_id: characterId }), + }) + if (!res.ok) { + const txt = await res.text().catch(() => "") + throw new Error(`copyCharacterLibraryAssets ${res.status} ${txt.slice(0, 300)}`) + } + return res.json() +} + export async function createProductFusionGuide( jobId: string, body: ProductFusionShot, @@ -293,6 +323,26 @@ export interface ProductLibraryItem { tags: string[] } +export interface CharacterLibraryImage { + id: string + view: string + label: string + filename: string + url: string + width: number + height: number + source_path: string +} + +export interface CharacterLibraryItem { + id: string + name: string + folder: string + description: string + primary_image: string + images: CharacterLibraryImage[] +} + export interface TranscriptSegment { index: number start: number @@ -632,6 +682,7 @@ export async function generateStoryboardVideo( first_image?: ImageRef | null last_image?: ImageRef | null product_images?: ImageRef[] + subject_images?: ImageRef[] subject_image?: ImageRef | null scene_image?: ImageRef | null product_image?: ImageRef | null