|
|
@@ -29,7 +29,13 @@ from ..contracts.production_assembly_models import (
|
|
|
ProductionAssemblyDelivery,
|
|
|
ProductionAssemblyPackage,
|
|
|
)
|
|
|
+from ..contracts.document_identity import exact_document_sha256
|
|
|
from ..contracts.production_execution_models import ImmutableJsonRef
|
|
|
+from ..contracts.production_evaluation import build_production_input_context
|
|
|
+from ..contracts.production_models import (
|
|
|
+ ProductionInputCatalog,
|
|
|
+ SegmentValidationReport,
|
|
|
+)
|
|
|
from ..contracts.production_planning_models import ProductionPlan
|
|
|
from ..contracts.production_validation_evaluation import (
|
|
|
evaluate_production_validator_candidate,
|
|
|
@@ -74,9 +80,12 @@ def _system_prompt() -> str:
|
|
|
return (
|
|
|
"你是独立 Production Validator。代码已经完成身份、Artifact 内容、"
|
|
|
"输出规格、时间线、音频、字幕、血缘和全局覆盖的确定性就绪检查;不得"
|
|
|
- "否认或覆盖这些结果。你必须根据最终视频的真实 ASR、ASS 和抽帧,"
|
|
|
- "按固定五项语义 criteria 以及全部相邻 Segment boundary 逐项验收。"
|
|
|
- "evidence 必须引用输入中的具体事实或时间点。只输出完整 "
|
|
|
+ "否认或覆盖这些结果。你必须根据完整 ProductionInput 原值、未分配到 "
|
|
|
+ "Shot 的正式要求、各 Segment 的 PASS 摘要,以及最终视频的真实 "
|
|
|
+ "ASR、ASS、边界和 Shot 中点抽帧,按固定五项语义 criteria 以及全部"
|
|
|
+ "相邻 Segment boundary 逐项验收。Segment PASS 只代表局部历史结论,"
|
|
|
+ "不得代替整片独立判断。evidence 必须引用输入 ID、来源路径、报告事实"
|
|
|
+ "或具体时间点。只输出完整 "
|
|
|
"ProductionValidatorCandidate JSON,不输出总体 verdict 或 markdown。"
|
|
|
"候选合同 JSON Schema:"
|
|
|
+ json.dumps(
|
|
|
@@ -101,8 +110,129 @@ def _subtitle_uri(delivery: ProductionAssemblyDelivery) -> str:
|
|
|
)
|
|
|
|
|
|
|
|
|
+def _frame_sampling_plan(
|
|
|
+ plan: ProductionPlan,
|
|
|
+ assembly: ProductionAssemblyPackage,
|
|
|
+ *,
|
|
|
+ duration_sec: float,
|
|
|
+) -> list[dict[str, Any]]:
|
|
|
+ """派生首尾、Segment 边界和每个 Shot 中点的稳定抽帧计划。"""
|
|
|
+
|
|
|
+ upper_bound = max(0.0, duration_sec - 0.05)
|
|
|
+ purposes_by_timestamp: dict[float, list[dict[str, str]]] = {}
|
|
|
+
|
|
|
+ def add(timestamp_sec: float, **purpose: str) -> None:
|
|
|
+ timestamp = min(upper_bound, max(0.0, timestamp_sec))
|
|
|
+ purposes_by_timestamp.setdefault(timestamp, []).append(purpose)
|
|
|
+
|
|
|
+ add(0.0, kind="production_start")
|
|
|
+ add(upper_bound, kind="production_end")
|
|
|
+ for previous, following in zip(
|
|
|
+ assembly.timeline,
|
|
|
+ assembly.timeline[1:],
|
|
|
+ ):
|
|
|
+ boundary = previous.end_ms / 1000
|
|
|
+ add(
|
|
|
+ boundary - 0.1,
|
|
|
+ kind="segment_boundary_before",
|
|
|
+ previous_segment_id=previous.segment_id,
|
|
|
+ next_segment_id=following.segment_id,
|
|
|
+ )
|
|
|
+ add(
|
|
|
+ boundary + 0.1,
|
|
|
+ kind="segment_boundary_after",
|
|
|
+ previous_segment_id=previous.segment_id,
|
|
|
+ next_segment_id=following.segment_id,
|
|
|
+ )
|
|
|
+
|
|
|
+ timeline_by_segment = {
|
|
|
+ item.segment_id: item for item in assembly.timeline
|
|
|
+ }
|
|
|
+ for segment in plan.segments:
|
|
|
+ timeline = timeline_by_segment.get(segment.segment_id)
|
|
|
+ if timeline is None:
|
|
|
+ raise ValidatorEvidenceError(
|
|
|
+ f"Assembly 缺少 {segment.segment_id} 时间线"
|
|
|
+ )
|
|
|
+ for shot in segment.shots:
|
|
|
+ add(
|
|
|
+ (
|
|
|
+ timeline.start_ms
|
|
|
+ + (shot.start_ms + shot.end_ms) / 2
|
|
|
+ )
|
|
|
+ / 1000,
|
|
|
+ kind="shot_midpoint",
|
|
|
+ segment_id=segment.segment_id,
|
|
|
+ shot_id=shot.shot_id,
|
|
|
+ )
|
|
|
+
|
|
|
+ return [
|
|
|
+ {
|
|
|
+ "timestamp_sec": timestamp,
|
|
|
+ "purposes": purposes_by_timestamp[timestamp],
|
|
|
+ }
|
|
|
+ for timestamp in sorted(purposes_by_timestamp)
|
|
|
+ ]
|
|
|
+
|
|
|
+
|
|
|
+def _segment_validation_summaries(
|
|
|
+ assembly: ProductionAssemblyPackage,
|
|
|
+) -> list[dict[str, Any]]:
|
|
|
+ """从 Accepted 引用读取并校验正式 PASS Report 的紧凑摘要。"""
|
|
|
+
|
|
|
+ summaries: list[dict[str, Any]] = []
|
|
|
+ for accepted in assembly.accepted_segments:
|
|
|
+ ref = accepted.validation_report_ref
|
|
|
+ path = Path(ref.uri).resolve()
|
|
|
+ if not path.is_file():
|
|
|
+ raise ValidatorEvidenceError(
|
|
|
+ f"SegmentValidationReport 不存在:{path}"
|
|
|
+ )
|
|
|
+ content = path.read_bytes()
|
|
|
+ if exact_document_sha256(content) != ref.document_sha256:
|
|
|
+ raise ValidatorEvidenceError(
|
|
|
+ f"SegmentValidationReport 不可变引用已变化:{path}"
|
|
|
+ )
|
|
|
+ try:
|
|
|
+ report = SegmentValidationReport.model_validate_json(content)
|
|
|
+ except ValueError as exc:
|
|
|
+ raise ValidatorEvidenceError(
|
|
|
+ f"SegmentValidationReport 合同无效:{path}: {exc}"
|
|
|
+ ) from exc
|
|
|
+ if (
|
|
|
+ report.run_id != accepted.source_run_id
|
|
|
+ or report.plan_id != accepted.source_plan_id
|
|
|
+ or report.plan_version != accepted.source_plan_version
|
|
|
+ or report.segment_id != accepted.segment_id
|
|
|
+ or report.verdict != "PASS"
|
|
|
+ ):
|
|
|
+ raise ValidatorEvidenceError(
|
|
|
+ "Accepted Segment 的 ValidationReport 身份或 verdict 不一致:"
|
|
|
+ f"{accepted.segment_id}"
|
|
|
+ )
|
|
|
+ summaries.append(
|
|
|
+ {
|
|
|
+ "segment_id": report.segment_id,
|
|
|
+ "source_plan_version": report.plan_version,
|
|
|
+ "verdict": report.verdict,
|
|
|
+ "criterion_results": [
|
|
|
+ {
|
|
|
+ "criterion": item.criterion,
|
|
|
+ "verdict": item.verdict,
|
|
|
+ "reason": item.reason,
|
|
|
+ }
|
|
|
+ for item in report.criterion_results
|
|
|
+ ],
|
|
|
+ "summary": report.summary,
|
|
|
+ "validation_report_ref": ref.model_dump(mode="json"),
|
|
|
+ }
|
|
|
+ )
|
|
|
+ return summaries
|
|
|
+
|
|
|
+
|
|
|
def _collect_evidence(
|
|
|
plan: ProductionPlan,
|
|
|
+ production_inputs: ProductionInputCatalog,
|
|
|
assembly: ProductionAssemblyPackage,
|
|
|
delivery: ProductionAssemblyDelivery,
|
|
|
inspection: ProductionMediaInspection,
|
|
|
@@ -133,32 +263,36 @@ def _collect_evidence(
|
|
|
{"subtitle": _subtitle_uri(delivery)},
|
|
|
)
|
|
|
duration_sec = inspection.duration_ms / 1000
|
|
|
- timestamps = {0.0, max(0.0, duration_sec - 0.05)}
|
|
|
- for boundary_ms in (
|
|
|
- item.end_ms for item in assembly.timeline[:-1]
|
|
|
- ):
|
|
|
- boundary = boundary_ms / 1000
|
|
|
- timestamps.add(max(0.0, boundary - 0.1))
|
|
|
- timestamps.add(min(duration_sec - 0.05, boundary + 0.1))
|
|
|
+ sampling_plan = _frame_sampling_plan(
|
|
|
+ plan,
|
|
|
+ assembly,
|
|
|
+ duration_sec=duration_sec,
|
|
|
+ )
|
|
|
+ timestamps = [item["timestamp_sec"] for item in sampling_plan]
|
|
|
frames_result = invoke_evidence_tool(
|
|
|
tools["extract_frames"],
|
|
|
- {"video": primary_uri, "timestamps": sorted(timestamps)},
|
|
|
+ {"video": primary_uri, "timestamps": timestamps},
|
|
|
)
|
|
|
frames = [
|
|
|
{
|
|
|
- "timestamp_sec": item.get("timestamp_sec"),
|
|
|
+ "requested_timestamp_sec": sample["timestamp_sec"],
|
|
|
+ "observed_timestamp_sec": item.get("timestamp_sec"),
|
|
|
+ "purposes": sample["purposes"],
|
|
|
"source": item.get("local_path") or item.get("url"),
|
|
|
}
|
|
|
- for item in frames_result.get("frames") or []
|
|
|
+ for sample, item in zip(
|
|
|
+ sampling_plan,
|
|
|
+ frames_result.get("frames") or [],
|
|
|
+ )
|
|
|
if isinstance(item, dict)
|
|
|
and isinstance(item.get("local_path") or item.get("url"), str)
|
|
|
]
|
|
|
if len(frames) != len(timestamps):
|
|
|
- raise ValidatorEvidenceError("最终成片边界抽帧证据不完整")
|
|
|
+ raise ValidatorEvidenceError("最终成片时间线抽帧证据不完整")
|
|
|
image_blocks = view_image_evidence(
|
|
|
tools["view_images"],
|
|
|
[str(item["source"]) for item in frames],
|
|
|
- incomplete_prefix="最终成片抽帧证据不完整",
|
|
|
+ incomplete_prefix="最终成片时间线抽帧证据不完整",
|
|
|
)
|
|
|
return (
|
|
|
{
|
|
|
@@ -173,6 +307,13 @@ def _collect_evidence(
|
|
|
preflight_report_ref.model_dump(mode="json")
|
|
|
),
|
|
|
},
|
|
|
+ "production_input_context": build_production_input_context(
|
|
|
+ production_inputs,
|
|
|
+ plan,
|
|
|
+ ),
|
|
|
+ "segment_validation_summaries": (
|
|
|
+ _segment_validation_summaries(assembly)
|
|
|
+ ),
|
|
|
"production_plan": plan.model_dump(mode="json"),
|
|
|
"assembly_package": assembly.model_dump(mode="json"),
|
|
|
"assembly_delivery": delivery.model_dump(mode="json"),
|
|
|
@@ -181,7 +322,7 @@ def _collect_evidence(
|
|
|
"semantic_evidence": {
|
|
|
"final_video_asr": transcript,
|
|
|
"final_subtitles": subtitles,
|
|
|
- "boundary_frames": frames,
|
|
|
+ "timeline_frames": frames,
|
|
|
},
|
|
|
},
|
|
|
image_blocks,
|
|
|
@@ -304,6 +445,7 @@ def _candidate_from_agent(
|
|
|
|
|
|
def run_production_validator(
|
|
|
plan: ProductionPlan,
|
|
|
+ production_inputs: ProductionInputCatalog,
|
|
|
assembly: ProductionAssemblyPackage,
|
|
|
delivery: ProductionAssemblyDelivery,
|
|
|
inspection: ProductionMediaInspection,
|
|
|
@@ -339,6 +481,7 @@ def run_production_validator(
|
|
|
)
|
|
|
evidence, image_blocks = _collect_evidence(
|
|
|
plan,
|
|
|
+ production_inputs,
|
|
|
assembly,
|
|
|
delivery,
|
|
|
inspection,
|