Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,12 @@ download
labellerr/__pycache__/
env.*
claude.md
labellerr/notebooks/Labellerr_datasets/354681d3-034a-4d66-b070-365f4bd11d8a/2a8d96ca-9161-4dee-ad3b-a5faf301bc6c.mp4
labellerr/notebooks/Labellerr_datasets/354681d3-034a-4d66-b070-365f4bd11d8a/7db3f60c-f6e5-4d3d-a63b-cb38530ee265.mp4
labellerr/notebooks/PyScene_detects/354681d3-034a-4d66-b070-365f4bd11d8a/2a8d96ca-9161-4dee-ad3b-a5faf301bc6c/2a8d96ca-9161-4dee-ad3b-a5faf301bc6c_mapping.json
labellerr/notebooks/PyScene_detects/354681d3-034a-4d66-b070-365f4bd11d8a/2a8d96ca-9161-4dee-ad3b-a5faf301bc6c/frames/316.jpg
labellerr/notebooks/PyScene_detects/354681d3-034a-4d66-b070-365f4bd11d8a/7db3f60c-f6e5-4d3d-a63b-cb38530ee265/7db3f60c-f6e5-4d3d-a63b-cb38530ee265_mapping.json
labellerr/notebooks/PyScene_detects/354681d3-034a-4d66-b070-365f4bd11d8a/2a8d96ca-9161-4dee-ad3b-a5faf301bc6c/frames/1406.jpg
labellerr/notebooks/PyScene_detects/354681d3-034a-4d66-b070-365f4bd11d8a/2a8d96ca-9161-4dee-ad3b-a5faf301bc6c/frames/1064.jpg
labellerr/notebooks/PyScene_detects/354681d3-034a-4d66-b070-365f4bd11d8a/2a8d96ca-9161-4dee-ad3b-a5faf301bc6c/frames/760.jpg
labellerr/notebooks/dev.env
4 changes: 2 additions & 2 deletions labellerr/core/constants.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
BASE_URL = "https://api.labellerr.com"
BASE_URL = "https://api-gateway-qcb3iv2gaa-uc.a.run.app"
ALLOWED_ORIGINS = "https://pro.labellerr.com"


Expand All @@ -7,7 +7,7 @@
TOTAL_FILES_SIZE_LIMIT_PER_DATASET = 2.5 * 1024 * 1024 * 1024 # 2.5GB
TOTAL_FILES_COUNT_LIMIT_PER_DATASET = 2500

ANNOTATION_FORMAT = ["json", "coco_json", "csv", "png"]
ANNOTATION_FORMAT = ["json", "coco_json", "csv", "png", "video_json"]
LOCAL_EXPORT_FORMAT = ["json", "coco_json", "csv", "png"]
LOCAL_EXPORT_STATUS = [
"review",
Expand Down
4 changes: 4 additions & 0 deletions labellerr/core/files/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,7 @@ def dataset_id(self):
@property
def metadata(self):
return self.__file_data.get("file_metadata", {})

@property
def file_name(self):
return self.__file_data.get("file_name", "")
73 changes: 52 additions & 21 deletions labellerr/core/files/video_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ def get_frames(self, frame_start: int = 0, frame_end: int | None = None):
"frame_end": frame_end,
"project_id": self.project_id,
"uuid": unique_id,
"client_id": self.client.client_id,
}

response = self.client.make_request(
Expand Down Expand Up @@ -115,8 +116,15 @@ def download_frames(
:return: Dictionary with download statistics
"""
try:
# Use file_id as folder name
folder_name = self.file_id
# Use [Dataset_id]+[File_id]+[File_name] as folder name
if self.dataset_id and self.file_name:
# Remove extension from file_name if present
base_name = os.path.splitext(self.file_name)[0]
folder_name = f"{self.dataset_id}+{self.file_id}+{base_name}"
elif self.dataset_id:
folder_name = f"{self.dataset_id}+{self.file_id}"
else:
folder_name = self.file_id

# Set output path
if output_folder:
Expand Down Expand Up @@ -207,7 +215,13 @@ def create_video(

input_pattern = os.path.join(frames_folder, pattern)
if output_file is None:
output_file = f"{self.file_id}.mp4"
# Use [Dataset_id]+[File_id]+[File_name] as default output filename
if self.dataset_id and self.file_name:
output_file = f"{self.dataset_id}+{self.file_id}+{self.file_name}"
elif self.dataset_id:
output_file = f"{self.dataset_id}+{self.file_id}.mp4"
else:
output_file = f"{self.file_id}.mp4"

# FFmpeg command
command = [
Expand Down Expand Up @@ -235,7 +249,7 @@ def create_video(
raise LabellerrError(f"Error while joining frames: {str(e)}")

def download_create_video_auto_cleanup(
self, output_folder: str = "./Labellerr_datastets"
self, output_folder: str = "./Labellerr_datasets"
):
"""
Download frames, create video, and automatically clean up temporary frames.
Expand All @@ -258,36 +272,50 @@ def download_create_video_auto_cleanup(
print(f"\n[1/4] Fetching frame data from API (0 to {total_frames})...")
frames_data = self.get_frames(frame_start=0, frame_end=total_frames)

# print(frames_data)

if not frames_data:
raise LabellerrError("No frame data retrieved from API")

print(f"Retrieved {len(frames_data)} frames")

# Step 2: Create dataset folder structure
# Step 2: Create output folder structure
print("\n[2/4] Setting up output folders...")
if self.dataset_id is None:
dataset_folder = output_folder
# Videos will be saved directly in output_folder (labellerr_datasets)
os.makedirs(output_folder, exist_ok=True)

# Define actual frames folder path using [Dataset_id]+[File_id]+[File_name] naming
# Frames will be temporarily stored in a subfolder for organization
if self.dataset_id and self.file_name:
base_name = os.path.splitext(self.file_name)[0]
folder_name = f"{self.dataset_id}+{self.file_id}+{base_name}"
elif self.dataset_id:
folder_name = f"{self.dataset_id}+{self.file_id}"
else:
dataset_folder = os.path.join(output_folder, self.dataset_id)
os.makedirs(dataset_folder, exist_ok=True)

# Define actual frames folder path
actual_frames_folder = os.path.join(dataset_folder, self.file_id)
folder_name = self.file_id
actual_frames_folder = os.path.join(output_folder, folder_name)

# Step 3: Download frames
print("\n[3/4] Downloading frames...")
download_result = self.download_frames(
frames_data=frames_data, output_folder=dataset_folder
frames_data=frames_data, output_folder=output_folder
)

if download_result["failed_downloads"] > 0:
print(
f"\nWarning: {download_result['failed_downloads']} frames failed to download"
)

# Step 4: Create video from downloaded frames
# Step 4: Create video from downloaded frames using [Dataset_id]+[File_id]+[File_name] naming
# Save video directly in output_folder (labellerr_datasets)
print("\n[4/4] Creating video from frames...")
video_output_path = os.path.join(dataset_folder, f"{self.file_id}.mp4")
if self.dataset_id and self.file_name:
video_filename = f"{self.dataset_id}+{self.file_id}+{self.file_name}"
elif self.dataset_id:
video_filename = f"{self.dataset_id}+{self.file_id}.mp4"
else:
video_filename = f"{self.file_id}.mp4"
video_output_path = os.path.join(output_folder, video_filename)

self.create_video(
frames_folder=actual_frames_folder, output_file=video_output_path
Expand All @@ -304,7 +332,7 @@ def download_create_video_auto_cleanup(
"file_id": self.file_id,
"dataset_id": self.dataset_id,
"video_path": video_output_path,
"output_folder": dataset_folder,
"output_folder": output_folder,
"frames_downloaded": download_result["successful_downloads"],
"frames_failed": download_result["failed_downloads"],
"failed_frames_info": download_result["failed_frames"],
Expand All @@ -313,19 +341,22 @@ def download_create_video_auto_cleanup(
print(f"\n{'='*60}")
print("Processing complete!")
print(f"Video saved to: {video_output_path}")
print("{'='*60}\n")
print(f"{'='*60}\n")

return result

except Exception as e:
# Attempt cleanup on error
# Get the frames folder path
# Get the frames folder path using [Dataset_id]+[File_id]+[File_name] naming
if self.dataset_id is None:
cleanup_folder = os.path.join(output_folder, self.file_id)
else:
cleanup_folder = os.path.join(
output_folder, self.dataset_id, self.file_id
)
if self.file_name:
base_name = os.path.splitext(self.file_name)[0]
folder_name = f"{self.dataset_id}+{self.file_id}+{base_name}"
else:
folder_name = f"{self.dataset_id}+{self.file_id}"
cleanup_folder = os.path.join(output_folder, folder_name)

if os.path.exists(cleanup_folder):
shutil.rmtree(cleanup_folder)
Expand Down
54 changes: 53 additions & 1 deletion labellerr/core/projects/video_project.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os
import uuid
from typing import List
from typing import Any, Dict, List

from .. import constants
from ..exceptions import LabellerrError
Expand Down Expand Up @@ -91,5 +92,56 @@ def delete_keyframes(self, file_id: str, keyframes: List[int]):
except Exception as e:
raise LabellerrError(f"Failed to delete key frames: {str(e)}")

def upload_keyframe_preannotations(self, video_json_file_path: str = None) -> Any:
"""
Uploads pre-annotations for video project.

Supports both the parent signature and a video-specific signature for backward compatibility.

:param annotation_format: (Deprecated) The format of the preannotation data
:param annotation_file: (Deprecated) The file path of the preannotation data
:param conf_bucket: (Deprecated) Confidence bucket [low, medium, high]
:param _async: (Deprecated) Whether to return a future object
:param video_json_file_path: Path to the video JSON file containing pre-annotations
:return: Response from the API
"""
# Support both old and new signatures
file_path = video_json_file_path

# Parameter validation
if not isinstance(file_path, str):
raise LabellerrError("file_path must be a str")

try:
# Validate if the file exists
if not os.path.exists(file_path):
raise LabellerrError(f"File not found: {file_path}")

unique_id = str(uuid.uuid4())
url = f"{constants.BASE_URL}/actions/upload_answers?project_id={self.project_id}&answer_format=video_json&client_id={self.client.client_id}&uuid={unique_id}"

# Get file name from path
file_name = os.path.basename(file_path)

# Open file and prepare multipart form data
with open(file_path, "rb") as f:
files = [("file", (file_name, f, "application/json"))]
payload: Dict[Any, Any] = {}

response = self.client.make_request(
"POST",
url,
request_id=unique_id,
handle_response=False,
data=payload,
files=files,
)

return self.client.handle_upload_response(response, unique_id)
except LabellerrError:
raise
except Exception as e:
raise LabellerrError(f"Failed to upload pre-annotations: {str(e)}")


LabellerrProjectMeta._register(DatasetDataType.video, VideoProject)
40 changes: 18 additions & 22 deletions labellerr/core/schemas/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,29 @@
# Import from autolabel.typings for backward compatibility
from labellerr.core.autolabel.typings import * # noqa: F403, F401

# Export annotation templates
from labellerr.core.schemas.annotation_templates import (
AnnotationQuestion,
CreateTemplateParams,
Option,
QuestionType,
)

# Autolabel schemas
from labellerr.core.schemas.autolabel import Hyperparameters, KeyFrame, TrainingRequest

# Base custom types
from labellerr.core.schemas.base import DirPathStr, FilePathStr, NonEmptyStr

# Connection schemas
from labellerr.core.schemas.connectors import (
AWSConnectionParams,
DatasetDataType,
DeleteConnectionParams,
GCSConnectionParams,
AWSConnectionTestParams,
ConnectionType,
ConnectorType,
DatasetDataType,
DeleteConnectionParams,
GCSConnectionParams,
GCSConnectionTestParams,
)

Expand All @@ -43,6 +54,9 @@
UploadFilesParams,
)

# Export schemas
from labellerr.core.schemas.exports import CreateExportParams, ExportDestination

# File operation schemas
from labellerr.core.schemas.files import BulkAssignFilesParams, ListFileParams

Expand All @@ -65,25 +79,6 @@
UpdateUserRoleParams,
)

# Autolabel schemas
from labellerr.core.schemas.autolabel import (
Hyperparameters,
KeyFrame,
TrainingRequest,
)

# Export schemas
from labellerr.core.schemas.exports import CreateExportParams, ExportDestination


# Export annotation templates
from labellerr.core.schemas.annotation_templates import (
AnnotationQuestion,
Option,
QuestionType,
)


__all__ = [
# Base types
"NonEmptyStr",
Expand Down Expand Up @@ -136,4 +131,5 @@
"AnnotationQuestion",
"Option",
"QuestionType",
"CreateTemplateParams",
]
10 changes: 6 additions & 4 deletions labellerr/core/schemas/annotation_templates.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from pydantic import BaseModel, Field
from typing import List, Optional
from enum import Enum
from ..schemas import DatasetDataType
import uuid
from enum import Enum
from typing import List, Optional

from pydantic import BaseModel, Field

from .base import DatasetDataType


class QuestionType(str, Enum):
Expand Down
1 change: 0 additions & 1 deletion labellerr/core/schemas/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ class CreateProjectParams(BaseModel):
class CreateTemplateParams(BaseModel):
"""Parameters for creating an annotation template."""

client_id: str = Field(min_length=1)
data_type: Literal["image", "video", "audio", "document", "text"]
template_name: str = Field(min_length=1)
questions: List[Question] = Field(min_length=1)
Expand Down
Loading
Loading