sublinear 0.2.0__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sublinear
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Python SDK for Sublinear foundation reward models for robotics.
5
5
  Author: Sublinear
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sublinear"
7
- version = "0.2.0"
7
+ version = "0.2.2"
8
8
  description = "Python SDK for Sublinear foundation reward models for robotics."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -397,6 +397,298 @@ class Sublinear:
397
397
  verbose: bool = True,
398
398
  **input_json: Any,
399
399
  ) -> SimpleNamespace | list[SimpleNamespace]:
400
- print('Thank you for trying out Sublinear! We are still in development and will be launching the API soon.')
401
- return
400
+ # print('Thank you for trying out Sublinear! We are still in development and will be launching the API soon.')
401
+ # return
402
+ """
403
+ Create one inference job for each video.
404
+ All keyword arguments captured by **input_json are passed
405
+ to every inference job. Each job also receives the view_type
406
+ associated with that video.
407
+ """
408
+ if "downsample_to" not in input_json:
409
+ input_json["downsample_to"] = 10
410
+ if "task_description" not in input_json:
411
+ raise ValueError("task_description must be provided")
412
+ if "verbose" not in input_json:
413
+ input_json["verbose"] = True
414
+ #
415
+ if isinstance(video_paths, str):
416
+ video_paths = [video_paths]
417
+ if isinstance(video_frames, np.ndarray):
418
+ video_frames = list(video_frames)
419
+ if isinstance(video_frames_external_view, np.ndarray):
420
+ video_frames_external_view = list(video_frames_external_view)
421
+ if isinstance(video_frames_wrist_view, np.ndarray):
422
+ video_frames_wrist_view = list(video_frames_wrist_view)
423
+ #
424
+ videos, final_video_paths, final_video_paths_external_view, final_video_paths_wrist_view, view_type_per_video, single_video, video_count = get_final_video_frames(
425
+ model=model,
426
+ video_paths=video_paths,
427
+ video_path=video_path,
428
+ video_paths_external_view=(video_paths_external_view),
429
+ video_paths_wrist_view=(video_paths_wrist_view),
430
+ video_path_external_view=(video_path_external_view),
431
+ video_path_wrist_view=(video_path_wrist_view),
432
+ video_frames=video_frames,
433
+ video_frames_external_view=(video_frames_external_view),
434
+ video_frames_wrist_view=(video_frames_wrist_view),
435
+ view_type_per_video=view_type_per_video,
436
+ view_type=view_type,
437
+ verbose=verbose,
438
+ )
439
+ #
440
+ if video_count < 1 or video_count > 100:
441
+ raise ValueError(f"Number of videos must be between 1 and 100, got {video_count}")
442
+ #
443
+ videos_iterable = (enumerate(videos) if videos is not None else repeat(None, video_count))
444
+ final_video_paths_iterable = (enumerate(final_video_paths) if final_video_paths is not None else repeat(None, video_count))
445
+ final_video_paths_external_view_iterable = (enumerate(final_video_paths_external_view) if final_video_paths_external_view is not None else repeat(None, video_count))
446
+ final_video_paths_wrist_view_iterable = (enumerate(final_video_paths_wrist_view) if final_video_paths_wrist_view is not None else repeat(None, video_count))
447
+ model = model.lower()
448
+ # if "dm" not in model and model not in ["sole-r1", "robometer", "roboreward", "topreward"]:
449
+ # raise ValueError(
450
+ # f"Model '{model}' is not supported. "
451
+ # "Supported models are: 'sole-r1', 'robometer', 'roboreward', 'topreward'."
452
+ # )
453
+ active_job_ids: set[str] = set()
454
+ active_job_ids_lock = threading.Lock()
455
+ cancel_requested = threading.Event()
456
+ def register_job(job_id: str) -> None:
457
+ with active_job_ids_lock:
458
+ active_job_ids.add(job_id)
459
+ # Handles a race where Ctrl+C occurs just before registration.
460
+ if cancel_requested.is_set():
461
+ try:
462
+ self.cancel_job(job_id)
463
+ except requests.RequestException:
464
+ pass
465
+ def unregister_job(job_id: str) -> None:
466
+ with active_job_ids_lock:
467
+ active_job_ids.discard(job_id)
468
+ def submit_video(
469
+ indexed_video: tuple[int, list[np.ndarray]],
470
+ indexed_video_path: tuple[int, str] | None = None,
471
+ indexed_video_path_external_view: tuple[int, str] | None = None,
472
+ indexed_video_path_wrist_view: tuple[int, str] | None = None,
473
+ ) -> SimpleNamespace:
474
+ if indexed_video is None and (indexed_video_path_external_view is None or indexed_video_path_wrist_view is None):
475
+ if not indexed_video_path is None:
476
+ video_index, video_path = indexed_video_path
477
+ # frames = load_video_frames(video_path)
478
+ elif not indexed_video_path_external_view is None and indexed_video_path_wrist_view is None:
479
+ video_index, video_path = indexed_video_path_external_view
480
+ # frames = load_video_frames(video_path_external_view)
481
+ elif indexed_video_path_external_view is None and not indexed_video_path_wrist_view is None:
482
+ video_index, video_path_wrist_view = indexed_video_path_wrist_view
483
+ # frames = load_video_frames(video_path_wrist_view)
484
+ #
485
+ source_path = Path(video_path)
486
+ job_input_json = dict(input_json)
487
+ frame_width, frame_height = get_video_dimensions(
488
+ source_path
489
+ )
490
+ job_input_json["_frame_width"] = frame_width
491
+ job_input_json["_frame_height"] = frame_height
492
+ job_input_json["view_type"] = view_type_per_video[video_index]
493
+ with source_path.open("rb") as video_file:
494
+ created_job = self.create_job(
495
+ job_input_json,
496
+ video=video_file,
497
+ filename=source_path.name,
498
+ model=model,
499
+ on_job_created=register_job,
500
+ )
501
+ else:
502
+ if not indexed_video is None:
503
+ video_index, frames = indexed_video
504
+ elif not indexed_video_path_external_view is None and not indexed_video_path_wrist_view is None:
505
+ video_index, video_path_external_view = indexed_video_path_external_view
506
+ video_index, video_path_wrist_view = indexed_video_path_wrist_view
507
+ frames_external = load_video_frames(video_path_external_view)
508
+ frames_wrist = load_video_frames(video_path_wrist_view)
509
+ if not len(frames_external) == len(frames_wrist):
510
+ raise ValueError(f"Number of frames in external view video {len(frames_external)} does not match number of frames in wrist view video {len(frames_wrist)} for video_idx {video_index}")
511
+ frames = [np.concatenate((frames_external[i], frames_wrist[i]), axis=1) for i in range(len(frames_external))]
512
+ else:
513
+ raise ValueError("No video data provided for submission.")
514
+ #
515
+ if not frames:
516
+ raise ValueError(
517
+ f"Video {video_index} has no frames."
518
+ )
519
+ first_frame = np.asarray(frames[0])
520
+ if (
521
+ first_frame.ndim != 3
522
+ or first_frame.shape[2] != 3
523
+ ):
524
+ raise ValueError(
525
+ f"Video {video_index} frames must have "
526
+ "shape (height, width, 3)."
527
+ )
528
+ height, width = first_frame.shape[:2]
529
+ filename = f"video_{video_index}_{time.time_ns()}.mp4"
530
+ job_input_json = dict(input_json)
531
+ job_input_json["_frame_width"] = width
532
+ job_input_json["_frame_height"] = height
533
+ job_input_json["view_type"] = (view_type_per_video[video_index])
534
+ with tempfile.TemporaryDirectory() as temp_directory:
535
+ encoded_video_path = (
536
+ Path(temp_directory) / filename
537
+ )
538
+ writer = cv2.VideoWriter(
539
+ str(encoded_video_path),
540
+ cv2.VideoWriter_fourcc(*"mp4v"),
541
+ 30.0,
542
+ (width, height),
543
+ )
544
+ if not writer.isOpened():
545
+ raise RuntimeError(
546
+ f"Unable to initialize MP4 encoder "
547
+ f"for video {video_index}."
548
+ )
549
+ try:
550
+ for frame_index, frame in enumerate(frames):
551
+ frame = np.asarray(frame)
552
+ if (
553
+ frame.ndim != 3
554
+ or frame.shape != first_frame.shape
555
+ ):
556
+ raise ValueError(
557
+ f"Frame {frame_index} of video "
558
+ f"{video_index} has shape "
559
+ f"{frame.shape}; expected "
560
+ f"{first_frame.shape}."
561
+ )
562
+ if frame.dtype != np.uint8:
563
+ if (
564
+ np.issubdtype(
565
+ frame.dtype,
566
+ np.floating,
567
+ )
568
+ and frame.size > 0
569
+ and frame.min() >= 0
570
+ and frame.max() <= 1
571
+ ):
572
+ frame = frame * 255
573
+ frame = np.clip(
574
+ frame,
575
+ 0,
576
+ 255,
577
+ ).astype(np.uint8)
578
+ # Input frames are RGB; OpenCV expects BGR.
579
+ bgr_frame = cv2.cvtColor(
580
+ frame,
581
+ cv2.COLOR_RGB2BGR,
582
+ )
583
+ writer.write(bgr_frame)
584
+ finally:
585
+ writer.release()
586
+ if (
587
+ not encoded_video_path.is_file()
588
+ or encoded_video_path.stat().st_size == 0
589
+ ):
590
+ raise RuntimeError(
591
+ f"Encoding video {video_index} failed."
592
+ )
593
+ with encoded_video_path.open("rb") as video_file:
594
+ created_job = self.create_job(
595
+ job_input_json,
596
+ video=video_file,
597
+ filename=filename,
598
+ model=model,
599
+ on_job_created=register_job,
600
+ )
601
+ #
602
+ job_id = created_job["job_id"]
603
+ try:
604
+ finished_job = self.wait_for_job(job_id)
605
+ finally:
606
+ # completed, failed, or canceled are all terminal.
607
+ unregister_job(job_id)
608
+ result = finished_job.get("result")
609
+ if not isinstance(result, dict):
610
+ raise RuntimeError(
611
+ f"Video {video_index} returned an "
612
+ "invalid inference result."
613
+ )
614
+ return SimpleNamespace(**result)
615
+ #
616
+ worker_count = min(
617
+ self.max_parallel_jobs,
618
+ # len(videos),
619
+ video_count,
620
+ )
621
+ executor = ThreadPoolExecutor(max_workers=worker_count)
622
+ try:
623
+ results = list(executor.map(submit_video, videos_iterable, final_video_paths_iterable, final_video_paths_external_view_iterable, final_video_paths_wrist_view_iterable,))
624
+ except KeyboardInterrupt:
625
+ cancel_requested.set()
626
+ with active_job_ids_lock:
627
+ jobs_to_cancel = list(active_job_ids)
628
+ for job_id in jobs_to_cancel:
629
+ try:
630
+ self.cancel_job(job_id)
631
+ except requests.RequestException as error:
632
+ print(
633
+ f"Could not cancel job {job_id}: {error}",
634
+ flush=True,
635
+ )
636
+ # Do not wait for queued worker functions to start.
637
+ executor.shutdown(
638
+ wait=False,
639
+ cancel_futures=True,
640
+ )
641
+ raise
642
+ except Exception:
643
+ # One submission failed, so cancel the remaining batch.
644
+ cancel_requested.set()
645
+ with active_job_ids_lock:
646
+ jobs_to_cancel = list(active_job_ids)
647
+ for job_id in jobs_to_cancel:
648
+ try:
649
+ self.cancel_job(job_id)
650
+ except requests.RequestException as cancel_error:
651
+ print(
652
+ f"Could not cancel job {job_id}: {cancel_error}",
653
+ flush=True,
654
+ )
655
+ executor.shutdown(wait=False, cancel_futures=True)
656
+ raise
657
+ else:
658
+ executor.shutdown(wait=True)
659
+ # with ThreadPoolExecutor(
660
+ # max_workers=worker_count
661
+ # ) as executor:
662
+ # try:
663
+ # results = list(executor.map(submit_video, videos_iterable, final_video_paths_iterable, final_video_paths_external_view_iterable, final_video_paths_wrist_view_iterable,))
664
+ # except KeyboardInterrupt:
665
+ # for job_id in active_job_ids:
666
+ # try:
667
+ # self.cancel_job(job_id)
668
+ # except requests.RequestException:
669
+ # pass
670
+ # executor.shutdown(wait=False, cancel_futures=True)
671
+ # raise
672
+ if single_video:
673
+ return results[0]
674
+ else:
675
+ if len(results)==1:
676
+ return results[0]
677
+ else:
678
+ results_rewards = [result.rewards[0] for result in results]
679
+ # results_output_text = [result.output_text[0] for result in results if result.output_text is not None else None]
680
+ results_output_text = [result.output_text[0] if result.output_text is not None else None for result in results]
681
+ results_success_probs = [result.success_probs[0] if result.success_probs else None for result in results]
682
+ results_success = [result.success[0] if result.success else None for result in results]
683
+ results = SimpleNamespace(
684
+ model=model,
685
+ rewards=results_rewards,
686
+ output_text=results_output_text,
687
+ success_probs=results_success_probs,
688
+ success=results_success,
689
+ )
690
+ return results
691
+
692
+
693
+
402
694
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sublinear
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Python SDK for Sublinear foundation reward models for robotics.
5
5
  Author: Sublinear
6
6
  License-Expression: MIT
File without changes
File without changes