@@ -252,18 +252,22 @@ def frame_num(self) -> ty.Optional[int]:
252252 stacklevel = 2 ,
253253 category = UserWarning ,
254254 )
255- # We can calculate the approx. # of frames by taking the presentation time and the
256- # time base itself.
257- (num , den ) = (self ._time .time_base * self ._time .pts ).as_integer_ratio ()
258- return num / den
255+ # Calculate approximate frame number from seconds and framerate.
256+ if self ._rate is not None :
257+ return round (self ._time .seconds * float (self ._rate ))
258+ # No framerate available - return estimate based on time.
259+ return round (self ._time .seconds )
259260 if isinstance (self ._time , _Seconds ):
260261 return self ._seconds_to_frames (self ._time .value )
261262 return self ._time .value
262263
263264 @property
264- def framerate (self ) -> float :
265+ def framerate (self ) -> ty . Optional [ float ] :
265266 """The framerate to use for distance between frames and to calculate frame numbers.
266- For a VFR video, this may just be the average framerate."""
267+ For a VFR video, this may just be the average framerate. Returns None if framerate
268+ is unknown (e.g. when working with pure Timecode representations)."""
269+ if self ._rate is None :
270+ return None
267271 return float (self ._rate )
268272
269273 @property
@@ -499,6 +503,9 @@ def __eq__(self, other: ty.Union[int, float, str, "FrameTimecode"]) -> bool:
499503 return False
500504 if _compare_as_fixed (self , other ):
501505 return self .frame_num == other .frame_num
506+ # For integer comparison, use frame numbers to avoid floating point precision issues.
507+ if isinstance (other , int ):
508+ return self .frame_num == other
502509 if isinstance (self ._time , (Timecode , _Seconds )):
503510 return self .seconds == self ._get_other_as_seconds (other )
504511 return self .frame_num == self ._get_other_as_frames (other )
@@ -508,34 +515,49 @@ def __ne__(self, other: ty.Union[int, float, str, "FrameTimecode"]) -> bool:
508515 return True
509516 if _compare_as_fixed (self , other ):
510517 return self .frame_num != other .frame_num
518+ # For integer comparison, use frame numbers to avoid floating point precision issues.
519+ if isinstance (other , int ):
520+ return self .frame_num != other
511521 if isinstance (self ._time , (Timecode , _Seconds )):
512522 return self .seconds != self ._get_other_as_seconds (other )
513523 return self .frame_num != self ._get_other_as_frames (other )
514524
515525 def __lt__ (self , other : ty .Union [int , float , str , "FrameTimecode" ]) -> bool :
516526 if _compare_as_fixed (self , other ):
517527 return self .frame_num < other .frame_num
528+ # For integer comparison, use frame numbers to avoid floating point precision issues.
529+ if isinstance (other , int ):
530+ return self .frame_num < other
518531 if isinstance (self ._time , (Timecode , _Seconds )):
519532 return self .seconds < self ._get_other_as_seconds (other )
520533 return self .frame_num < self ._get_other_as_frames (other )
521534
522535 def __le__ (self , other : ty .Union [int , float , str , "FrameTimecode" ]) -> bool :
523536 if _compare_as_fixed (self , other ):
524537 return self .frame_num <= other .frame_num
538+ # For integer comparison, use frame numbers to avoid floating point precision issues.
539+ if isinstance (other , int ):
540+ return self .frame_num <= other
525541 if isinstance (self ._time , (Timecode , _Seconds )):
526542 return self .seconds <= self ._get_other_as_seconds (other )
527543 return self .frame_num <= self ._get_other_as_frames (other )
528544
529545 def __gt__ (self , other : ty .Union [int , float , str , "FrameTimecode" ]) -> bool :
530546 if _compare_as_fixed (self , other ):
531547 return self .frame_num > other .frame_num
548+ # For integer comparison, use frame numbers to avoid floating point precision issues.
549+ if isinstance (other , int ):
550+ return self .frame_num > other
532551 if isinstance (self ._time , (Timecode , _Seconds )):
533552 return self .seconds > self ._get_other_as_seconds (other )
534553 return self .frame_num > self ._get_other_as_frames (other )
535554
536555 def __ge__ (self , other : ty .Union [int , float , str , "FrameTimecode" ]) -> bool :
537556 if _compare_as_fixed (self , other ):
538557 return self .frame_num >= other .frame_num
558+ # For integer comparison, use frame numbers to avoid floating point precision issues.
559+ if isinstance (other , int ):
560+ return self .frame_num >= other
539561 if isinstance (self ._time , (Timecode , _Seconds )):
540562 return self .seconds >= self ._get_other_as_seconds (other )
541563 return self .frame_num >= self ._get_other_as_frames (other )
@@ -565,7 +587,9 @@ def __iadd__(self, other: ty.Union[int, float, str, "FrameTimecode"]) -> "FrameT
565587 pts = max (0 , timecode .pts + round (seconds / timecode .time_base )),
566588 time_base = timecode .time_base ,
567589 )
568- self ._rate = None
590+ # Preserve rate if available from self or other.
591+ if self ._rate is None and isinstance (other , FrameTimecode ):
592+ self ._rate = other ._rate
569593 return self
570594
571595 other_is_seconds = isinstance (other , FrameTimecode ) and isinstance (other ._time , _Seconds )
@@ -610,7 +634,9 @@ def __isub__(self, other: ty.Union[int, float, str, "FrameTimecode"]) -> "FrameT
610634 pts = max (0 , timecode .pts - round (seconds / timecode .time_base )),
611635 time_base = timecode .time_base ,
612636 )
613- self ._rate = None
637+ # Preserve rate if available from self or other.
638+ if self ._rate is None and isinstance (other , FrameTimecode ):
639+ self ._rate = other ._rate
614640 return self
615641
616642 other_is_seconds = isinstance (other , FrameTimecode ) and isinstance (other ._time , _Seconds )
@@ -652,21 +678,20 @@ def __repr__(self) -> str:
652678 return f"{ self .get_timecode ()} [frame_num={ self ._time .value } , fps={ self ._rate } ]"
653679
654680 def __hash__ (self ) -> int :
655- if isinstance (self ._time , Timecode ):
656- return hash (self ._time )
681+ # Use frame_num for consistent hashing regardless of internal representation.
682+ # This ensures that FrameTimecodes representing the same frame have the same hash,
683+ # enabling proper dictionary lookups in StatsManager.
657684 return self .frame_num
658685
659686 def _get_other_as_seconds (self , other : ty .Union [int , float , str , "FrameTimecode" ]) -> float :
660687 """Get the time in seconds from `other` for arithmetic operations."""
661688 if isinstance (other , int ):
662- if isinstance (self ._time , Timecode ):
663- # TODO(https://scenedetect.com/issue/168): We need to convert every place that uses
664- # frame numbers with timestamps to convert to a non-frame based way of temporal
665- # logic and instead use seconds-based.
666- if _USE_PTS_IN_DEVELOPMENT and other == 1 :
667- return self .seconds
668- raise NotImplementedError ()
669- return float (other ) / self ._rate
689+ # Convert frame number to seconds using framerate.
690+ if self ._rate is None :
691+ raise NotImplementedError (
692+ "Cannot convert frame number to seconds without framerate"
693+ )
694+ return float (other ) / float (self ._rate )
670695 if isinstance (other , float ):
671696 return other
672697 if isinstance (other , str ):
0 commit comments