fix: integrate decoded scene animation verification

This commit is contained in:
NINI
2026-10-10 00:29:41 +09:00
3 changed files with 63 additions and 8 deletions
+2
View File
@@ -37,6 +37,8 @@ def main():
frame_start=scene.frame_start, frame_end=scene.frame_end,
logical_to_blender="logical_frame + 1", external_links=False)
if job["operation"] == "render":
# frame_set과 렌더의 depsgraph가 같은 Scene을 평가하도록 활성 컨텍스트를 맞춘다.
bpy.context.window.scene = scene
profile = job["profile"]
scene.render.engine = profile["engine"]
scene.render.resolution_x = profile["width"]
+49 -6
View File
@@ -3,23 +3,54 @@ import argparse
import json
from pathlib import Path
import shutil
import subprocess
from PIL import Image
import run
from test_runner import fixture, production_fixture, endpoint_fixture, ROOT
from test_runner import write
def pixels(path):
with Image.open(path) as image:
return image.convert("RGB").tobytes()
def check_motion(evidence, ffmpeg):
frames = evidence["logical_frames"]
checked = [frames[0], frames[len(frames) // 2], frames[-1]]
media = evidence["media"]
attempt = (ROOT / media["path"]).parent
rendered_samples = [pixels(attempt / ("frame-" + str(f).zfill(8) + ".png")) for f in checked]
# H.264 프레임마다 달라질 수 있는 양자화 흔적을 원본 3D 움직임으로 오인하지 않는다.
run.require(len(set(rendered_samples)) == 3, "디코드 PNG RGB 시작/중간/끝에서 3D 움직임이 없습니다: " + evidence["scene_name"])
decoded = subprocess.run([ffmpeg, "-v", "error", "-xerror", "-i", str(ROOT / media["path"]),
"-map", "0:v:0", "-pix_fmt", "rgb24", "-c:v", "rawvideo", "-f", "image2pipe", "-"], capture_output=True)
run.require(decoded.returncode == 0, "MP4 RGB 디코드가 실패했습니다: " + decoded.stderr.decode("utf-8", errors="replace")[-1000:])
size = media["width"] * media["height"] * 3
run.require(len(decoded.stdout) == size * len(frames), "실제 RGB 디코드 프레임 수가 다릅니다")
samples = [decoded.stdout[index * size:(index + 1) * size] for index in (0, len(frames) // 2, len(frames) - 1)]
run.require(len(set(samples)) == 3, "디코드 MP4 RGB 시작/중간/끝에서 3D 움직임이 없습니다: " + evidence["scene_name"])
return checked
def main():
parser = argparse.ArgumentParser(description="설치 정보가 아닌 실제 렌더·디코드 검사")
parser.add_argument("--blender", required=True)
parser.add_argument("--ffmpeg", required=True)
parser.add_argument("--ffprobe", required=True)
parser.add_argument("--engine", choices=("CYCLES", "BLENDER_EEVEE"), default="CYCLES")
parser.add_argument("--animation-only", action="store_true", help="50fps·5프레임으로 비활성 Scene의 실제 RGB 움직임만 검사")
args = parser.parse_args()
folder, project, artifacts, job = fixture()
if args.engine == "BLENDER_EEVEE":
artifacts["08-scenes"]["payload"]["preview"]["render_profile"].update(engine="BLENDER_EEVEE", device="GPU")
write(ROOT / job["artifact"], artifacts["08-scenes"])
if args.animation_only:
artifacts["08-scenes"]["payload"]["preview"]["render_profile"]["fps"] = 50
job["end"] = 5
write(ROOT / job["artifact"], artifacts["08-scenes"])
source = ROOT / job["source"]
# fixture의 검사 전 바이트는 생성 원본이 아니다. 실패 흔적은 별도 파일로 보존한다.
source.rename(source.with_suffix(".uninspected"))
@@ -27,14 +58,26 @@ def main():
Path(__file__).with_name("smoke_fixture.py"), "--", source])
video = run.render(ROOT, job, args.blender, args.ffmpeg, args.ffprobe)
require = run.require
require(video["media"]["duration_ms"] == 1000, "검사 영상 길이 불일치")
attempt = (ROOT / video["media"]["path"]).parent
require(run.digest(attempt / "frame-00000000.png") != run.digest(attempt / "frame-00000009.png"), "3D 움직임이 없습니다")
still_job = dict(job, frame=10, start=None, end=None)
require(video["media"]["duration_ms"] == (100 if args.animation_only else 1000), "검사 영상 길이 불일치")
second_video = run.render(ROOT, dict(job, scene="SceneB", require_object=["BallB"]), args.blender, args.ffmpeg, args.ffprobe)
motion_checks, errors = [], []
for rendered in (video, second_video):
try:
motion_checks.append(check_motion(rendered, args.ffmpeg))
except ValueError as error:
errors.append(str(error))
require(not errors, "\n".join(errors))
first_motion, second_motion = motion_checks
still_job = dict(job, frame=4 if args.animation_only else 10, start=None, end=None)
first = run.render(ROOT, still_job, args.blender, args.ffmpeg, args.ffprobe)
second = run.render(ROOT, dict(still_job, scene="SceneB"), args.blender, args.ffmpeg, args.ffprobe)
require(first["identity"] != second["identity"] and run.digest(ROOT / first["media"]["path"]) != run.digest(ROOT / second["media"]["path"]),
second = run.render(ROOT, dict(still_job, scene="SceneB", require_object=["BallB"]), args.blender, args.ffmpeg, args.ffprobe)
require(first["identity"] != second["identity"] and pixels(ROOT / first["media"]["path"]) != pixels(ROOT / second["media"]["path"]),
"같은 논리 프레임의 다른 Scene 카메라가 혼입되었습니다")
if args.animation_only:
print(json.dumps(dict(first_video=video["media"], second_video=second_video["media"],
first_rgb_frames=first_motion, second_rgb_frames=second_motion, first_scene=first["media"], second_scene=second["media"],
renderer=video["render_profile"], source=video["scene_file"], result="초기 Idle Scene과 무관한 두 Scene의 디코드 RGB 움직임·5프레임·100ms·같은 시각의 Scene 구별 검사 통과"), ensure_ascii=False, indent=2))
return
for invalid in (dict(still_job, scene="Missing"), dict(still_job, require_object=["Missing"])):
try:
run.render(ROOT, invalid, args.blender, args.ffmpeg, args.ffprobe)
+12 -2
View File
@@ -1,4 +1,4 @@
"""실행 검사 전용 3D 구와 두 카메라를 만든다. 제작 내용과 무관하다."""
"""실행 검사 전용 두 애니메이션 Scene과 초기 Idle Scene을 만든다."""
import sys
import bpy
@@ -34,7 +34,10 @@ scene.camera = camera
second = bpy.data.scenes.new("SceneB")
second.frame_start, second.frame_end = 1, 21
second.world = scene.world
second.collection.objects.link(ball)
second_ball = ball.copy()
second_ball.data = ball.data.copy()
second_ball.name = "BallB"
second.collection.objects.link(second_ball)
second.collection.objects.link(light)
other = camera.copy()
other.data = camera.data.copy()
@@ -42,6 +45,13 @@ other.location = (5, -7, 1)
other.rotation_euler = (-other.location).to_track_quat("-Z", "Y").to_euler()
second.collection.objects.link(other)
second.camera = other
idle = bpy.data.scenes.new("Idle")
idle.world = scene.world
idle_camera = camera.copy()
idle_camera.data = camera.data.copy()
idle.collection.objects.link(idle_camera)
idle.camera = idle_camera
bpy.context.window.scene = idle
if "--external-image" in sys.argv:
bpy.data.images.load(sys.argv[sys.argv.index("--external-image") + 1]).use_fake_user = True
if "--short-scene" in sys.argv: