DataFlowing / processor /app /models.py
stouchinglhh
collector: 同步采集工程 v1.2.1(全部 LFS 模式文件指针化,与 Space 现有文件合并)
d646dd7
Raw History Blame Contribute Delete
12.3 kB
"""SQLAlchemy ORM models — PostgreSQL."""
import uuid
from datetime import datetime, timezone
from sqlalchemy import (
Column, String, Integer, Float, Text, Boolean, DateTime,
ForeignKey, JSON, BigInteger, ARRAY
)
from sqlalchemy.dialects.postgresql import UUID as PUUID
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import relationship
from app.database import Base
def utcnow():
return datetime.now(timezone.utc)
class Session(Base):
__tablename__ = "sessions"
id = Column(PUUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
name = Column(String(256), nullable=True)
project_id = Column(PUUID(as_uuid=True), ForeignKey("projects.id", ondelete="SET NULL"), nullable=True, index=True)
codebase_version = Column(String(16), nullable=True)
info = Column(JSONB, nullable=True)
episode_count = Column(Integer, default=0)
original_archive = Column(String(512), nullable=True)
created_at = Column(DateTime(timezone=True), default=utcnow)
episodes = relationship("Episode", back_populates="session", cascade="all, delete-orphan")
class Episode(Base):
__tablename__ = "episodes"
id = Column(PUUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
session_id = Column(PUUID(as_uuid=True), ForeignKey("sessions.id", ondelete="SET NULL"), nullable=True, index=True)
name = Column(String(256), nullable=True)
task_description = Column(Text, nullable=True)
task_id = Column(Integer, nullable=True)
# Status: collecting | completed | reviewed (legacy)
# received | processing | to_review | approved | rejected | failed (new pipeline)
status = Column(String(16), default="collecting", index=True)
frame_count = Column(Integer, default=0)
fps = Column(Integer, default=30)
camera_names = Column(JSONB, nullable=True) # ["cam_front", "cam_wrist"]
meta = Column(JSONB, nullable=True)
# Pipeline timestamps
received_at = Column(DateTime(timezone=True), nullable=True, index=True) # 上传完成时间
processing_started_at = Column(DateTime(timezone=True), nullable=True) # 开始处理
review_ready_at = Column(DateTime(timezone=True), nullable=True) # 处理完成,待审核
approved_at = Column(DateTime(timezone=True), nullable=True) # 审核通过
rejected_at = Column(DateTime(timezone=True), nullable=True) # 审核拒绝
# Cleaning / validation report
cleaning_report = Column(JSONB, nullable=True) # {passed, checks: [{name, status, detail}], summary}
created_at = Column(DateTime(timezone=True), default=utcnow, index=True)
updated_at = Column(DateTime(timezone=True), default=utcnow, onupdate=utcnow)
deleted_at = Column(DateTime(timezone=True), nullable=True) # 软删除时间戳
session = relationship("Session", back_populates="episodes")
frames = relationship("Frame", back_populates="episode", cascade="all, delete-orphan")
annotations = relationship("AnnotationSegment", back_populates="episode", cascade="all, delete-orphan")
class Frame(Base):
__tablename__ = "frames"
id = Column(BigInteger, primary_key=True, autoincrement=True)
episode_id = Column(PUUID(as_uuid=True), ForeignKey("episodes.id", ondelete="CASCADE"), nullable=False, index=True)
frame_index = Column(Integer, nullable=False)
timestamp = Column(Float, nullable=True)
observation = Column(JSONB, nullable=True)
action = Column(JSONB, nullable=True)
reward = Column(Float, nullable=True)
is_terminal = Column(Boolean, default=False)
is_truncated = Column(Boolean, default=False)
image_paths = Column(JSONB, nullable=True)
created_at = Column(DateTime(timezone=True), default=utcnow)
episode = relationship("Episode", back_populates="frames")
class ExportJob(Base):
__tablename__ = "export_jobs"
id = Column(PUUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
dataset_name = Column(String(128), nullable=False)
status = Column(String(16), default="pending") # pending | running | completed | failed
episode_ids = Column(ARRAY(PUUID(as_uuid=True)), nullable=True)
split_ratio = Column(Float, default=0.9)
progress = Column(Float, default=0.0)
output_dir = Column(String(512), nullable=True)
export_format = Column(String(16), default="lerobot_v3")
error = Column(Text, nullable=True)
created_at = Column(DateTime(timezone=True), default=utcnow)
finished_at = Column(DateTime(timezone=True), nullable=True)
class AnnotationSegment(Base):
"""Frame-precise annotation segment — a labeled range of frames within an episode."""
__tablename__ = "annotation_segments"
id = Column(PUUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
episode_id = Column(PUUID(as_uuid=True), ForeignKey("episodes.id", ondelete="CASCADE"), nullable=False, index=True)
label = Column(String(256), nullable=False)
start_frame_index = Column(Integer, nullable=False)
end_frame_index = Column(Integer, nullable=False)
color = Column(String(7), nullable=True, default="#3B82F6") # hex color
sort_order = Column(Integer, default=0)
notes = Column(Text, nullable=True)
created_at = Column(DateTime(timezone=True), default=utcnow)
updated_at = Column(DateTime(timezone=True), default=utcnow, onupdate=utcnow)
episode = relationship("Episode", back_populates="annotations")
keyframes = relationship("AnnotationKeyframe", back_populates="segment", cascade="all, delete-orphan")
class AnnotationKeyframe(Base):
"""A single keyframe marker within an annotation segment — marks a critical moment."""
__tablename__ = "annotation_keyframes"
id = Column(PUUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
segment_id = Column(PUUID(as_uuid=True), ForeignKey("annotation_segments.id", ondelete="CASCADE"), nullable=True, index=True)
episode_id = Column(PUUID(as_uuid=True), ForeignKey("episodes.id", ondelete="CASCADE"), nullable=False, index=True)
frame_index = Column(Integer, nullable=False)
event = Column(String(256), nullable=True) # e.g. "手指接触杯身"
created_at = Column(DateTime(timezone=True), default=utcnow)
segment = relationship("AnnotationSegment", back_populates="keyframes")
class TaskDefinition(Base):
"""Pre-defined collection task with targets. Matches uploaded episodes by task_description."""
__tablename__ = "task_definitions"
id = Column(PUUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
name = Column(String(256), nullable=False, unique=True, index=True) # matches episode.task_description
description = Column(Text, nullable=True)
claimer = Column(String(128), nullable=True, index=True) # who claims this task (e.g. HandTest_001)
target_episodes = Column(Integer, default=0) # target number of batches (1 zip upload = 1 batch)
target_frames = Column(Integer, default=0) # (deprecated, kept for compat)
target_duration_sec = Column(Integer, default=0) # (deprecated, kept for compat)
fps = Column(Integer, default=30)
params = Column(JSONB, nullable=True) # extra params, e.g. {"object":"cylinder","hand":"right"}
status = Column(String(16), default="active") # active | paused | completed
created_at = Column(DateTime(timezone=True), default=utcnow)
updated_at = Column(DateTime(timezone=True), default=utcnow, onupdate=utcnow)
class Device(Base):
"""Registered data-collection device (robot / test rig / DAQ client)."""
__tablename__ = "devices"
id = Column(PUUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
name = Column(String(128), nullable=False, unique=True, index=True)
meta = Column(JSONB, nullable=True)
last_seen_at = Column(DateTime(timezone=True), default=utcnow)
first_seen_at = Column(DateTime(timezone=True), default=utcnow)
status = Column(String(16), default="online") # online | offline
created_at = Column(DateTime(timezone=True), default=utcnow)
class User(Base):
"""Platform user accounts."""
__tablename__ = "users"
id = Column(PUUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
username = Column(String(64), unique=True, nullable=False, index=True)
password_hash = Column(String(256), nullable=False)
email = Column(String(128), nullable=True)
role = Column(String(16), default="engineer") # admin | reviewer | engineer
status = Column(String(16), default="active") # active | disabled
expires_at = Column(DateTime(timezone=True), nullable=True, index=True)
last_login_at = Column(DateTime(timezone=True), nullable=True)
created_at = Column(DateTime(timezone=True), default=utcnow)
# ── Workflow Studio ─────────────────────────────────────
class Project(Base):
"""A collection project — binds one workflow to a device/task flow.
Example: "鱼眼相机采集项目" binds the fisheye pipeline (fisheye_camera
→ mediapipe_hand → export) so uploads under this project automatically
run its workflow. Calibration params follow the LeRobot dataset layout
(calibration/<camera>.json), so the worker reads them without extra input.
"""
__tablename__ = "projects"
id = Column(PUUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
name = Column(String(256), nullable=False, unique=True, index=True)
description = Column(Text, nullable=True)
workflow_id = Column(PUUID(as_uuid=True), ForeignKey("workflows.id", ondelete="SET NULL"), nullable=True, index=True)
device_type = Column(String(64), nullable=True) # e.g. "fisheye" | "stereo" | "glove"
params = Column(JSONB, nullable=True) # project-level defaults, e.g. {"position": "head"}
status = Column(String(16), default="active", index=True) # active | paused | archived
created_at = Column(DateTime(timezone=True), default=utcnow)
updated_at = Column(DateTime(timezone=True), default=utcnow, onupdate=utcnow)
workflow = relationship("Workflow")
class Workflow(Base):
"""A saved workflow pipeline — stores the React Flow graph as JSON."""
__tablename__ = "workflows"
id = Column(PUUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
name = Column(String(256), nullable=False)
description = Column(Text, nullable=True)
graph = Column(JSONB, nullable=False, default=dict) # {nodes: [...], edges: [...]}
node_configs = Column(JSONB, nullable=False, default=dict) # per-node config overrides
status = Column(String(16), default="draft", index=True) # draft | active | archived
created_at = Column(DateTime(timezone=True), default=utcnow)
updated_at = Column(DateTime(timezone=True), default=utcnow, onupdate=utcnow)
runs = relationship("WorkflowRun", back_populates="workflow", cascade="all, delete-orphan")
class WorkflowRun(Base):
"""A single execution run of a workflow."""
__tablename__ = "workflow_runs"
id = Column(PUUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
workflow_id = Column(PUUID(as_uuid=True), ForeignKey("workflows.id", ondelete="CASCADE"),
nullable=False, index=True)
episode_id = Column(PUUID(as_uuid=True), ForeignKey("episodes.id", ondelete="SET NULL"),
nullable=True, index=True)
status = Column(String(16), default="queued", index=True) # queued | running | completed | failed
started_at = Column(DateTime(timezone=True), nullable=True)
finished_at = Column(DateTime(timezone=True), nullable=True)
node_states = Column(JSONB, nullable=False, default=dict) # {node_id: {status, progress}}
error_log = Column(Text, nullable=True)
worker_id = Column(String(128), nullable=True, index=True)
lease_until = Column(DateTime(timezone=True), nullable=True, index=True)
heartbeat_at = Column(DateTime(timezone=True), nullable=True)
attempt = Column(Integer, nullable=False, default=0)
progress = Column(Float, nullable=False, default=0.0)
outputs = Column(JSONB, nullable=False, default=dict)
created_at = Column(DateTime(timezone=True), default=utcnow)
workflow = relationship("Workflow", back_populates="runs")