fix: ASRX duplication, TKG edges, trace ingest, and add pipeline progress publishing

- ASRX handler no longer stores duplicate 'asr' pre_chunks
- Pre_chunks storage made idempotent (delete-before-insert)
- Rule 1 + trace_ingest changed to query 'asrx' not 'asr'
- Trace chunks removed (dynamic from TKG/Qdrant)
- TKG scroll_face_points fixed: trace_id >= 1 (not == 1)
- TKG AsrxSegmentEntry: start/end -> start_time/end_time (match ASRX JSON)
- Unregister error handling: log instead of silent discard
- Add publish_pipeline_progress calls at each pipeline stage
  (processors, rule1, face_trace, identity_agent, TKG, rule2, completion)
This commit is contained in:
Accusys
2026-07-02 10:43:46 +08:00
parent d791d138f2
commit 3eabd45882
65 changed files with 9481 additions and 3856 deletions

View File

@@ -35,7 +35,7 @@ from redis_publisher import RedisPublisher
from qdrant_faces import push_face_embeddings_batch
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
SWIFT_BIN = os.path.join(SCRIPT_DIR, "swift_processors", ".build", "debug", "swift_face_pose")
SWIFT_BIN = os.path.join(SCRIPT_DIR, "swift_processors", ".build", "release", "swift_face_pose")
FACENET_PATH = os.path.join(SCRIPT_DIR, "..", "models", "facenet512.mlpackage")
# Pose angle classification from roll/yaw
@@ -84,7 +84,12 @@ class FaceProcessorVision:
self.total_frames = int(self.video.get(cv2.CAP_PROP_FRAME_COUNT))
self.width = int(self.video.get(cv2.CAP_PROP_FRAME_WIDTH))
self.height = int(self.video.get(cv2.CAP_PROP_FRAME_HEIGHT))
# Calculate 8Hz sample interval based on FPS
self.sample_interval = max(1, round(self.fps / 8))
print(f"[FACE_V2] Video: {self.width}x{self.height}, {self.fps:.1f}fps, {self.total_frames}f")
print(f"[FACE_V2] 8Hz sample interval: {self.fps:.1f}/8 = {self.sample_interval}")
def extract_face_embedding(self, face_img: np.ndarray) -> Optional[list]:
"""Run CoreML FaceNet on cropped face"""
@@ -126,11 +131,15 @@ class FaceProcessorVision:
output_basename = os.path.basename(self.output_path)
pose_basename = output_basename.replace("face", "pose")
swift_pose_out = os.path.join(output_dir, pose_basename)
# Appearance output: same directory, but replace "face" with "appearance" in filename
appearance_basename = output_basename.replace("face", "appearance")
swift_appearance_out = os.path.join(output_dir, appearance_basename)
cmd = [
SWIFT_BIN,
self.video_path,
swift_face_out,
swift_pose_out,
swift_appearance_out,
"--sample-interval", str(self.sample_interval),
]
if self.uuid:
@@ -286,17 +295,28 @@ class FaceProcessorVision:
# Convert dict frames to list for Rust FaceResult format
frames_list = []
total_faces = 0
for fnum_str, fdata in sorted(face_data["frames"].items(), key=lambda x: int(x[0])):
faces = fdata["faces"]
total_faces += len(faces)
frames_list.append({
"frame": int(fnum_str),
"timestamp": fdata["time_seconds"],
"faces": fdata["faces"],
"faces": faces,
})
# Determine status based on face count
if total_faces > 0:
status = "has_faces"
else:
status = "no_faces"
output = {
"status": status,
"frame_count": len(frames_list),
"fps": self.fps,
"frames": frames_list,
"total_faces": total_faces,
}
with open(self.output_path, "w") as f:
@@ -339,6 +359,9 @@ def main():
args.uuid, args.sample_interval, publisher
)
# Open video to get FPS and calculate sample_interval
processor.open_video()
# Step 1: Vision detection (bbox + pose via ANE)
try:
detection = processor.process_with_swift()