feat: integrate invitation access, world NPCs, and deployment
This commit is contained in:
@@ -17,6 +17,7 @@ import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from scipy import ndimage
|
||||
from pathlib import Path
|
||||
from typing import Iterable
|
||||
|
||||
@@ -33,15 +34,22 @@ DEFAULT_CUTOUT_SCRIPT = SCRIPT_DIR / "tools" / "birefnet_cutout.py"
|
||||
DEFAULT_EXPAND_POSE_TRIPLET_SCRIPT = SCRIPT_DIR / "tools" / "expand_pose_triplet.py"
|
||||
DEFAULT_REFERENCE_STRIPS_DIR = SCRIPT_DIR / "references"
|
||||
DEFAULT_IDENTITY_REFERENCE_IMAGE = (
|
||||
DEFAULT_REFERENCE_STRIPS_DIR / "whaleboy_reference_down.png"
|
||||
DEFAULT_REFERENCE_STRIPS_DIR / "whaleboy_identity_single.png"
|
||||
)
|
||||
FRAME_SIZE = 160
|
||||
SPRITESHEET_COLUMNS = 8
|
||||
SPRITESHEET_ROWS = 4
|
||||
# Official style strips keep their source canvas for stable cell extraction.
|
||||
# The generated identity master is intentionally square and much smaller.
|
||||
REFERENCE_CANVAS_SIZE = (1536, 1024)
|
||||
IDENTITY_GENERATION_SIZE = "1024x1024"
|
||||
SINGLE_POSE_GENERATION_SIZE = "832x832"
|
||||
SINGLE_POSE_REFERENCE_SIZE = 1024
|
||||
SINGLE_POSE_REFERENCE_BODY_HEIGHT = 700
|
||||
POSE_LAYOUT_MARGIN_RATIO = 0.05
|
||||
POSE_LAYOUT_MAX_BODY_HEIGHT_RATIO = 0.88
|
||||
POSE_LAYOUT_MAX_BODY_WIDTH_RATIO = 0.68
|
||||
POSE_LAYOUT_BOTTOM_RATIO = 0.92
|
||||
LOWER_BODY_EDIT_START_RATIO = 0.68
|
||||
LOWER_BODY_EDIT_FEATHER_RATIO = 0.025
|
||||
LOWER_BODY_EDIT_TOP_HALF_WIDTH_RATIO = 0.18
|
||||
@@ -342,6 +350,15 @@ def _run_with_retries(
|
||||
raise last_error or RuntimeError("Novamailio command failed")
|
||||
|
||||
|
||||
SINGLE_POSE_LAYOUT_RULES = """
|
||||
Current generation-canvas placement (mandatory, before later 160x160 assembly):
|
||||
- Treat the full current image canvas as the frame. Keep exactly one complete character inside it; never crop any hair, hand, shoe, or shadow edge.
|
||||
- Leave a clean empty margin on every side: at least 5% of the current canvas width on the left and right, and 5% of the current canvas height at the top and bottom.
|
||||
- Place the visible character bounding-box center on the canvas centerline. The bounding-box center may deviate no more than 8% of the current canvas width.
|
||||
- Keep the character at the same scale and occupancy as the supplied pose reference. Do not enlarge it until it touches an edge, and do not shrink it into a tiny figure.
|
||||
""".strip()
|
||||
|
||||
|
||||
def _direction_prompt(direction: str) -> str:
|
||||
if direction in POSE_TRIPLET_DIRECTIONS:
|
||||
return _pose_triplet_prompt(direction)
|
||||
@@ -459,28 +476,44 @@ def _single_pose_prompt(direction: str, pose_index: int) -> str:
|
||||
reference_row = DIRECTION_REFERENCE_ROWS[direction]
|
||||
reference_column = (1, 2, 4)[pose_index]
|
||||
action = POSE_SINGLE_ACTIONS[direction][pose_index]
|
||||
if pose_index == 0:
|
||||
motion_rules = """
|
||||
- This is the neutral anchor pose. Both legs and shoes remain still, compact, symmetrical where the viewing direction permits, and on the same baseline.
|
||||
- Do not invent a lifted, forward, backward, or active leg. Do not create a walking step in this neutral pose.
|
||||
- Keep the head, shoulders, torso, hips, arms, and hands in a neutral upright standing pose. Both arms hang straight down at the sides.
|
||||
""".strip()
|
||||
identity_input = "Image 1 is the approved identity master. Preserve its identity and rendering exactly while converting it to the requested viewing direction."
|
||||
reference_match_rule = "Match Image 2's compact neutral stance, leg spacing, shoe placement, and viewing direction exactly."
|
||||
else:
|
||||
motion_rules = """
|
||||
- Keep the head, hair, shoulders, torso, hips, and both arms in the accepted neutral pose. Do not swing or bend an arm, rotate the torso, lean the body, or create a running pose.
|
||||
- Only the active leg and shoe may differ from the neutral pose. Both feet must remain visible: one active foot and one grounded foot.
|
||||
- The active leg must be recognizable from the outer silhouette, not only from color or shading.
|
||||
""".strip()
|
||||
identity_input = "Image 1 is the already accepted neutral pose and the immutable appearance/upper-body master."
|
||||
reference_match_rule = "Match Image 2's compact stride and actual leg/shoe geometry. Do not widen the stance or move a foot toward a canvas edge."
|
||||
return f"""
|
||||
Task:
|
||||
Create one single canonical {direction.upper()} pose for a WhaleTown walking animation.
|
||||
|
||||
Inputs:
|
||||
- Image 1 is appearance identity only. Preserve its hair, face or back-head design, outfit, colors, proportions, outline, and rendering style. Ignore all poses in Image 1.
|
||||
- {identity_input}
|
||||
- Image 2 is an enlarged single-pose crop extracted from column {reference_column} of the official WhaleTown {reference_row} motion row. Copy the complete body, leg, ankle, shoe, spacing, and perspective geometry visible in Image 2.
|
||||
- Image 3 is a layout-only construction guide. Use its safe box, centerline, and baseline to place the character. Never copy its lines, colors, background, or guide marks into the output.
|
||||
|
||||
Output:
|
||||
- Exactly ONE full-body character on the entire canvas. Do not create a row, sequence, comparison, duplicate, or additional character.
|
||||
- Center the character on a flat pure magenta #FF00FF background.
|
||||
{SINGLE_POSE_LAYOUT_RULES}
|
||||
- No text, labels, dividers, UI, watermark, props, or shadows.
|
||||
- Target assembled character height is {spec["body_height"]} px, width about {spec["body_width"]} px, and foot baseline y={spec["foot_y"]} px in a 160x160 frame.
|
||||
- These pixel targets describe the later 160x160 assembled frame, not the current generation canvas. On the current canvas, match Image 2's character occupancy and scale; do not draw a tiny 116px character.
|
||||
|
||||
Required pose:
|
||||
- {action}
|
||||
- {DIRECTION_LOCKS[direction]}
|
||||
- Keep the head, hair, shoulders, torso, hips, and both arms in a perfectly neutral upright standing pose. Both arms hang straight down at the sides. Do not swing or bend an arm, rotate the torso, lean the body, or create a running pose.
|
||||
- Only the active leg and shoe may differ from a neutral standing pose. The motion must remain compact enough for a calm game walk cycle.
|
||||
- Both feet must remain visible: one active foot and one grounded/neutral foot where applicable.
|
||||
- Match Image 2's compact stride and actual leg/shoe geometry. Do not widen the stance or move a foot toward a canvas edge.
|
||||
- The active leg must be recognizable from the outer silhouette, not only from color or shading.
|
||||
{motion_rules}
|
||||
- {reference_match_rule}
|
||||
|
||||
Identity locks:
|
||||
- Image 1 controls identity and all neutral upper-body geometry; Image 2 controls the requested leg and shoe pose only.
|
||||
@@ -510,6 +543,7 @@ Create one single canonical {direction.upper()} action pose for a WhaleTown walk
|
||||
Inputs:
|
||||
- Image 1 is the neutral pose A and the immutable appearance master.
|
||||
- Image 2 is the already accepted opposite-leg action pose of the SAME character. It shows this source action: {source_action}
|
||||
- Image 3 is a layout-only construction guide. Use its safe box, centerline, and baseline to place the character. Never copy its lines, colors, background, or guide marks into the output.
|
||||
- Produce the opposite action shown below. Use Image 2 only to match stride size, forward-depth perspective, and motion strength; switch the active leg exactly as requested.
|
||||
|
||||
Required target action:
|
||||
@@ -527,8 +561,10 @@ Immutable appearance contract:
|
||||
|
||||
Output:
|
||||
- Exactly ONE full-body character, centered on flat pure magenta #FF00FF.
|
||||
{SINGLE_POSE_LAYOUT_RULES}
|
||||
- No row, sequence, duplicate, text, labels, dividers, UI, watermark, props, or shadows.
|
||||
- Target assembled character height is {spec["body_height"]} px, width about {spec["body_width"]} px, and foot baseline y={spec["foot_y"]} px in a 160x160 frame.
|
||||
- These pixel targets describe the later 160x160 assembled frame, not the current generation canvas. Match Image 1's full-body scale and occupancy exactly; do not draw a tiny 116px character.
|
||||
""".strip()
|
||||
|
||||
|
||||
@@ -539,16 +575,18 @@ Create one locked front-facing identity master for a WhaleTown V2 player skin.
|
||||
|
||||
Input images:
|
||||
- Image 1 is the player's uploaded character reference. Preserve its main identity: face impression, hairstyle or head silhouette, outfit idea, dominant colors, and overall character feeling.
|
||||
- Image 2 is the official WhaleTown DOWN/front 8-frame reference row. Use it for sprite structure: body height, body width, baseline, frame spacing, simple rounded proportions, and clean 2D game rendering.
|
||||
- Image 2 is one official front-facing Sea Breeze Boy style reference. Use it only for WhaleTown sprite structure: body proportions, scale, baseline, dark outline, simple cel shading, and clean 2D game rendering.
|
||||
- Do not copy Image 2's face, hair, whale hood, outfit, colors, accessories, or character identity.
|
||||
|
||||
Output:
|
||||
- Exactly 8 equal columns x 1 row.
|
||||
- Same character identity in every column, front-facing, full-body, centered, same size and baseline.
|
||||
- Exactly ONE front-facing full-body character, centered on the canvas.
|
||||
- This is a single identity master, not a row, spritesheet, sequence, turnaround, or animation.
|
||||
- Human character only. If the uploaded image is animal-like, mascot-like, realistic, or non-human, adapt it into a human WhaleTown player character while preserving the visual inspiration.
|
||||
- Do not invent new accessories, props, hats, tails, ears, weapons, or costume details that are not visible in the uploaded image.
|
||||
- If the uploaded image and resulting identity have no hat or hood, keep the head uncovered in every frame. Do not add a hat, hood, cap, helmet, animal ears, hair accessory, or new head accessory.
|
||||
- If the uploaded image has no hat or hood, keep the head uncovered. Do not add a hat, hood, cap, helmet, animal ears, hair accessory, or new head accessory.
|
||||
- Match WhaleTown style: compact rounded 2D game sprite, clean dark outline, simple cel shading, low texture density.
|
||||
- Target assembled frame size is 160x160 px; visible character box about 73 px wide, 116 px high, bottom baseline y=137.
|
||||
- These pixel targets describe later assembly, not the current identity-generation canvas. Match Image 2's single-character occupancy and scale; do not draw a tiny 73x116 character.
|
||||
- Flat pure magenta #FF00FF background.
|
||||
- No text, labels, dividers, UI, watermark, props, shadows, green, or magenta/pink on the character.
|
||||
""".strip()
|
||||
@@ -561,7 +599,11 @@ def _parse_args(argv: Iterable[str]) -> argparse.Namespace:
|
||||
parser.add_argument("--name", default="")
|
||||
parser.add_argument("--result-json", type=Path, required=True)
|
||||
parser.add_argument("--status-json", type=Path, required=True)
|
||||
parser.add_argument("--size", default="1536x1024")
|
||||
parser.add_argument(
|
||||
"--size",
|
||||
default=IDENTITY_GENERATION_SIZE,
|
||||
help="Identity-master generation canvas; the official style reference keeps its own 1536x1024 source canvas.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--quality", default=os.getenv("SKIN_GENERATION_QUALITY", "medium")
|
||||
)
|
||||
@@ -571,6 +613,14 @@ def _parse_args(argv: Iterable[str]) -> argparse.Namespace:
|
||||
)
|
||||
parser.add_argument("--assemble-script", type=Path, default=DEFAULT_ASSEMBLE_SCRIPT)
|
||||
parser.add_argument("--reference-strips-dir", type=Path, default=None)
|
||||
parser.add_argument("--phase", choices=("all", "identity", "actions", "pose"), default="all")
|
||||
parser.add_argument("--direction", choices=DIRECTIONS)
|
||||
parser.add_argument("--pose", choices=POSE_NAMES)
|
||||
parser.add_argument(
|
||||
"--qa-feedback",
|
||||
default="",
|
||||
help="Previous pose QA failure to incorporate into a targeted repair attempt.",
|
||||
)
|
||||
return parser.parse_args(list(argv))
|
||||
|
||||
|
||||
@@ -998,7 +1048,7 @@ def _resolve_identity_reference_path(reference_strips_dir: Path | None) -> Path:
|
||||
source_dir = reference_strips_dir
|
||||
if not source_dir.is_absolute():
|
||||
source_dir = SCRIPT_DIR / source_dir
|
||||
path = source_dir / "whaleboy_reference_down.png"
|
||||
path = source_dir / "whaleboy_identity_single.png"
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(f"Identity reference image is missing: {path}")
|
||||
with Image.open(path) as image:
|
||||
@@ -1047,14 +1097,93 @@ def _create_single_pose_reference(
|
||||
(SINGLE_POSE_REFERENCE_SIZE, SINGLE_POSE_REFERENCE_SIZE),
|
||||
(255, 0, 255),
|
||||
)
|
||||
canvas.paste(
|
||||
resized,
|
||||
((canvas.width - resized.width) // 2, (canvas.height - resized.height) // 2),
|
||||
)
|
||||
# Keep the official pose inside a stable, invisible layout box. The model
|
||||
# sees the same margins for every direction, while the box itself is not
|
||||
# drawn and therefore cannot leak into the generated sprite.
|
||||
box_left = round(canvas.width * POSE_LAYOUT_MARGIN_RATIO)
|
||||
box_right = canvas.width - box_left
|
||||
box_bottom = round(canvas.height * (1 - POSE_LAYOUT_MARGIN_RATIO))
|
||||
x = (box_left + box_right - resized.width) // 2
|
||||
y = box_bottom - resized.height
|
||||
canvas.paste(resized, (x, y))
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
canvas.save(output_path)
|
||||
|
||||
|
||||
def _create_single_pose_layout_guide(output_path: Path) -> None:
|
||||
"""Create a layout-only construction guide for the image model."""
|
||||
size = SINGLE_POSE_REFERENCE_SIZE
|
||||
margin = round(size * POSE_LAYOUT_MARGIN_RATIO)
|
||||
image = Image.new("RGB", (size, size), "#f7f7f7")
|
||||
draw = ImageDraw.Draw(image)
|
||||
draw.rectangle((margin, margin, size - margin - 1, size - margin - 1), outline="#2f80ed", width=4)
|
||||
center_x = size // 2
|
||||
draw.line((center_x, margin, center_x, size - margin), fill="#9aa5ad", width=3)
|
||||
baseline = round(size * POSE_LAYOUT_BOTTOM_RATIO)
|
||||
draw.line((margin, baseline, size - margin, baseline), fill="#9aa5ad", width=3)
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
image.save(output_path)
|
||||
|
||||
|
||||
def _normalize_single_pose_cutout(path: Path) -> dict[str, float | int]:
|
||||
"""Fit a generated cutout into the shared safe layout before QA/assembly."""
|
||||
with Image.open(path).convert("RGBA") as source:
|
||||
image = source.copy()
|
||||
array = np.asarray(image).copy()
|
||||
alpha = array[:, :, 3] > 16
|
||||
ys, xs = np.where(alpha)
|
||||
if not xs.size:
|
||||
raise RuntimeError("逐格QA失败:抠图中没有角色")
|
||||
width, height = image.size
|
||||
# Keep the character's meaningful connected regions and discard isolated
|
||||
# specks before measuring its box. Small separated regions such as shoes
|
||||
# remain because the area threshold is relative to the main component.
|
||||
labels, count = ndimage.label(alpha, structure=ndimage.generate_binary_structure(2, 1))
|
||||
if count:
|
||||
sizes = np.bincount(labels.ravel())
|
||||
largest = int(sizes[1:].max()) if sizes.size > 1 else 0
|
||||
min_component_area = max(12, round(largest * 0.002))
|
||||
keep = (labels > 0) & (sizes[labels] >= min_component_area)
|
||||
array[:, :, 3] = np.where(keep, array[:, :, 3], 0).astype(np.uint8)
|
||||
alpha = keep
|
||||
edge_counts = (int(alpha[0].sum()), int(alpha[-1].sum()), int(alpha[:, 0].sum()), int(alpha[:, -1].sum()))
|
||||
edge_noise_limit = max(4, round(min(width, height) * 0.005))
|
||||
if max(edge_counts) > edge_noise_limit:
|
||||
raise RuntimeError("逐格QA失败:角色主体贴近画布边缘,无法安全裁切")
|
||||
# Remove isolated one-pixel edge residue before calculating the crop box.
|
||||
if edge_counts[0] <= edge_noise_limit:
|
||||
array[0, :, 3] = 0
|
||||
if edge_counts[1] <= edge_noise_limit:
|
||||
array[-1, :, 3] = 0
|
||||
if edge_counts[2] <= edge_noise_limit:
|
||||
array[:, 0, 3] = 0
|
||||
if edge_counts[3] <= edge_noise_limit:
|
||||
array[:, -1, 3] = 0
|
||||
image = Image.fromarray(array, "RGBA")
|
||||
alpha = np.asarray(image)[:, :, 3] > 16
|
||||
ys, xs = np.where(alpha)
|
||||
if not xs.size:
|
||||
raise RuntimeError("逐格QA失败:去除边缘噪点后没有角色")
|
||||
bbox = (int(xs.min()), int(ys.min()), int(xs.max()) + 1, int(ys.max()) + 1)
|
||||
body_w, body_h = bbox[2] - bbox[0], bbox[3] - bbox[1]
|
||||
max_w = max(1, round(width * POSE_LAYOUT_MAX_BODY_WIDTH_RATIO))
|
||||
max_h = max(1, round(height * POSE_LAYOUT_MAX_BODY_HEIGHT_RATIO))
|
||||
scale = min(1.0, max_w / body_w, max_h / body_h)
|
||||
cropped = image.crop(bbox)
|
||||
if scale < 1.0:
|
||||
cropped = cropped.resize(
|
||||
(max(1, round(cropped.width * scale)), max(1, round(cropped.height * scale))),
|
||||
Image.Resampling.LANCZOS,
|
||||
)
|
||||
canvas = Image.new("RGBA", (width, height), (0, 0, 0, 0))
|
||||
x = round((width - cropped.width) / 2)
|
||||
y = round(height * POSE_LAYOUT_BOTTOM_RATIO) - cropped.height
|
||||
y = max(round(height * POSE_LAYOUT_MARGIN_RATIO), min(height - round(height * POSE_LAYOUT_MARGIN_RATIO) - cropped.height, y))
|
||||
canvas.alpha_composite(cropped, (x, y))
|
||||
canvas.save(path)
|
||||
return {"scale": round(scale, 4), "body_width": cropped.width, "body_height": cropped.height, "center_error": 0.0, "components": int(count)}
|
||||
|
||||
|
||||
def _create_lower_body_edit_mask(neutral_pose_path: Path, output_path: Path) -> None:
|
||||
"""Protect a neutral pose except for a compact, character-relative leg region."""
|
||||
with Image.open(neutral_pose_path) as source:
|
||||
@@ -1097,6 +1226,46 @@ def _create_lower_body_edit_mask(neutral_pose_path: Path, output_path: Path) ->
|
||||
mask.save(output_path)
|
||||
|
||||
|
||||
def _validate_single_pose_cutout(path: Path, pose_name: str, neutral_path: Path | None) -> dict[str, object]:
|
||||
image = Image.open(path).convert("RGBA")
|
||||
alpha = np.asarray(image)[:, :, 3] > 16
|
||||
ys, xs = np.where(alpha)
|
||||
if not xs.size:
|
||||
raise RuntimeError("逐格QA失败:抠图中没有角色")
|
||||
width, height = image.size
|
||||
bbox = (int(xs.min()), int(ys.min()), int(xs.max()) + 1, int(ys.max()) + 1)
|
||||
body_w, body_h = bbox[2] - bbox[0], bbox[3] - bbox[1]
|
||||
center_error = abs((bbox[0] + bbox[2]) / 2 - width / 2) / width
|
||||
failures = []
|
||||
warnings = []
|
||||
if not 0.35 <= body_h / height <= 0.95: failures.append(f"角色高度占比异常 {body_h / height:.2f}")
|
||||
if not 0.08 <= body_w / width <= 0.70: failures.append(f"角色宽度占比异常 {body_w / width:.2f}")
|
||||
if center_error > 0.18:
|
||||
if center_error <= 0.24:
|
||||
warnings.append(f"角色轻微水平偏心 {center_error:.2f}")
|
||||
else:
|
||||
failures.append(f"角色水平偏心 {center_error:.2f}")
|
||||
edge_margin = max(2, round(min(width, height) * 0.01))
|
||||
edge_pixels = int(alpha[:edge_margin].sum() + alpha[-edge_margin:].sum() + alpha[:, :edge_margin].sum() + alpha[:, -edge_margin:].sum())
|
||||
if edge_pixels > 0:
|
||||
warnings.append(f"边缘有少量残留像素 {edge_pixels}")
|
||||
motion_xor = None
|
||||
upper_xor = None
|
||||
if pose_name != "neutral" and neutral_path and neutral_path.exists():
|
||||
neutral = np.asarray(Image.open(neutral_path).convert("RGBA"))[:, :, 3] > 16
|
||||
if neutral.shape == alpha.shape:
|
||||
split = max(1, round(bbox[1] + body_h * 0.64))
|
||||
upper_union = np.logical_or(neutral[:split], alpha[:split])
|
||||
upper_xor = float(np.logical_xor(neutral[:split], alpha[:split]).sum() / max(1, upper_union.sum()))
|
||||
lower_union = np.logical_or(neutral[split:], alpha[split:])
|
||||
motion_xor = float(np.logical_xor(neutral[split:], alpha[split:]).sum() / max(1, lower_union.sum()))
|
||||
if upper_xor > 0.12: failures.append(f"上半身漂移过大 {upper_xor:.3f}")
|
||||
if motion_xor < 0.012: failures.append(f"腿脚动作不足 {motion_xor:.3f}")
|
||||
qa = {"passed": not failures, "bbox": bbox, "canvas": [width, height], "body_width": body_w, "body_height": body_h, "center_error": round(center_error, 4), "upper_xor": upper_xor, "motion_xor": motion_xor, "failures": failures, "warnings": warnings}
|
||||
if failures: raise RuntimeError("逐格QA失败:" + ";".join(failures))
|
||||
return qa
|
||||
|
||||
|
||||
def main(argv: Iterable[str]) -> int:
|
||||
args = _parse_args(argv)
|
||||
out_dir = args.out_dir.resolve()
|
||||
@@ -1159,12 +1328,13 @@ def main(argv: Iterable[str]) -> int:
|
||||
identity_prompt_path = prompt_dir / f"{skin_name}_identity.txt"
|
||||
identity_path = identity_dir / f"{skin_name}_identity_reference.png"
|
||||
identity_prompt_path.write_text(_identity_prompt(), encoding="utf-8")
|
||||
_status(
|
||||
args.status_json,
|
||||
"identity",
|
||||
"正在基于上传图片和whaleboy参考条生成角色身份母版",
|
||||
)
|
||||
_run_with_retries(
|
||||
if args.phase != "actions":
|
||||
_status(
|
||||
args.status_json,
|
||||
"identity",
|
||||
"正在基于上传图片和海风少年单格参考生成角色身份母版",
|
||||
)
|
||||
_run_with_retries(
|
||||
[
|
||||
sys.executable,
|
||||
str(args.novamailio_script),
|
||||
@@ -1191,26 +1361,45 @@ def main(argv: Iterable[str]) -> int:
|
||||
log_path=log_path,
|
||||
env=child_env,
|
||||
attempts=3,
|
||||
retry_delay=10.0,
|
||||
)
|
||||
retry_delay=10.0,
|
||||
)
|
||||
elif not identity_path.exists():
|
||||
raise FileNotFoundError("已确认的身份母版不存在,请先完成身份任务")
|
||||
|
||||
if args.phase == "identity":
|
||||
result = {"ok": True, "phase": "identity", "identity_path": str(identity_path), "identity_prompt_path": str(identity_prompt_path), "log_path": str(log_path)}
|
||||
args.result_json.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
_status(args.status_json, "identity_done", "身份母版已生成,请确认后继续动作")
|
||||
return 0
|
||||
|
||||
_status(args.status_json, "prompt", "正在规划四方向8帧动作")
|
||||
canonical_front_identity_path: Path | None = None
|
||||
for direction in DIRECTIONS:
|
||||
active_directions = (args.direction,) if args.phase == "pose" and args.direction else DIRECTIONS
|
||||
for direction in active_directions:
|
||||
pose_cutouts: dict[int, Path] = {}
|
||||
pose_raw_paths: dict[int, Path] = {}
|
||||
neutral_pose_path: Path | None = None
|
||||
for existing_index, existing_name in enumerate(POSE_NAMES):
|
||||
existing_raw = raw_dir / f"{skin_name}_{direction}_{existing_name}_source.png"
|
||||
existing_cutout = cutout_dir / f"{skin_name}_{direction}_{existing_name}_cutout.png"
|
||||
if existing_raw.exists(): pose_raw_paths[existing_index] = existing_raw
|
||||
if existing_cutout.exists(): pose_cutouts[existing_index] = existing_cutout
|
||||
neutral_pose_path: Path | None = pose_raw_paths.get(0)
|
||||
lower_body_mask_path = (
|
||||
pose_reference_dir / f"{direction}_lower_body_edit_mask.png"
|
||||
)
|
||||
for generation_step, pose_index in enumerate(
|
||||
POSE_GENERATION_ORDER, start=1
|
||||
):
|
||||
if args.phase == "pose" and POSE_NAMES[pose_index] != args.pose:
|
||||
continue
|
||||
pose_name = POSE_NAMES[pose_index]
|
||||
prompt_path = prompt_dir / f"{skin_name}_{direction}_{pose_name}.txt"
|
||||
pose_reference_path = (
|
||||
pose_reference_dir / f"{direction}_{pose_name}_reference.png"
|
||||
)
|
||||
layout_guide_path = (
|
||||
pose_reference_dir / f"{direction}_{pose_name}_layout_guide.png"
|
||||
)
|
||||
raw_path = raw_dir / f"{skin_name}_{direction}_{pose_name}_source.png"
|
||||
cutout_path = (
|
||||
cutout_dir / f"{skin_name}_{direction}_{pose_name}_cutout.png"
|
||||
@@ -1224,22 +1413,26 @@ def main(argv: Iterable[str]) -> int:
|
||||
(0, 1, 3)[pose_index],
|
||||
pose_reference_path,
|
||||
)
|
||||
_create_single_pose_layout_guide(layout_guide_path)
|
||||
if pose_index == 1:
|
||||
sibling_pose_path = pose_raw_paths.get(2)
|
||||
if sibling_pose_path is None:
|
||||
raise RuntimeError(
|
||||
f"{direction} opposite-leg sibling pose was not generated first"
|
||||
)
|
||||
prompt_path.write_text(
|
||||
_single_pose_from_sibling_prompt(direction, pose_index),
|
||||
encoding="utf-8",
|
||||
)
|
||||
prompt_text = _single_pose_from_sibling_prompt(direction, pose_index)
|
||||
motion_reference_path = sibling_pose_path
|
||||
else:
|
||||
prompt_path.write_text(
|
||||
_single_pose_prompt(direction, pose_index), encoding="utf-8"
|
||||
)
|
||||
prompt_text = _single_pose_prompt(direction, pose_index)
|
||||
motion_reference_path = pose_reference_path
|
||||
if args.qa_feedback:
|
||||
prompt_text += (
|
||||
"\n\nTargeted repair feedback from the previous candidate:\n"
|
||||
"- The previous candidate failed this QA check: "
|
||||
+ args.qa_feedback.strip()
|
||||
+ "\n- Correct only the reported layout or motion issue. Preserve the approved identity and requested action exactly.\n"
|
||||
)
|
||||
prompt_path.write_text(prompt_text, encoding="utf-8")
|
||||
|
||||
_status(
|
||||
args.status_json,
|
||||
@@ -1267,6 +1460,8 @@ def main(argv: Iterable[str]) -> int:
|
||||
),
|
||||
"--image",
|
||||
str(motion_reference_path),
|
||||
"--image",
|
||||
str(layout_guide_path),
|
||||
"--prompt-file",
|
||||
str(prompt_path),
|
||||
"--size",
|
||||
@@ -1328,6 +1523,15 @@ def main(argv: Iterable[str]) -> int:
|
||||
env=child_env,
|
||||
)
|
||||
pose_cutouts[pose_index] = cutout_path
|
||||
layout_qa = _normalize_single_pose_cutout(cutout_path)
|
||||
pose_qa = _validate_single_pose_cutout(cutout_path, pose_name, pose_cutouts.get(0))
|
||||
pose_qa["layout"] = layout_qa
|
||||
|
||||
if args.phase == "pose":
|
||||
result = {"ok": True, "phase": "pose", "direction": direction, "pose": pose_name, "raw_path": str(raw_path), "cutout_path": str(cutout_path), "preview_path": str(preview_path), "prompt_path": str(prompt_path), "layout_guide_path": str(layout_guide_path), "pose_reference_path": str(pose_reference_path), "qa": pose_qa}
|
||||
args.result_json.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
_status(args.status_json, "pose_done", f"{direction} {pose_name} 动作已生成", direction=direction, pose=pose_name)
|
||||
return 0
|
||||
|
||||
expanded_path = expanded_dir / f"{skin_name}_{direction}_8frame_cutout.png"
|
||||
expand_command = [
|
||||
|
||||
Reference in New Issue
Block a user