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:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user