25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

181 lines
6.5 KiB

  1. class SourceMedia():
  2. """Source media used by eg feature extractors. This is a list of Source objects.
  3. JSON type schema:
  4. [{
  5. "source": "/path/to/video.mp4",
  6. "path": "/path/to/video.mp4",
  7. "provider": "FileInputJSON"
  8. },
  9. {
  10. "source": "http://example.com/video.mp4",
  11. "path": "/path/to/downloaded_video.mp4",
  12. "provider": "InputYAML"
  13. }]
  14. It should be possible to combine/merge/aggregate multiple SourceMedia into one
  15. TODO: consider if we actually want that or if we just loop over a list of >0 SourceMedia
  16. Iterating over a SourceMedia object should return a list of Source objects.
  17. """
  18. def __init__(self, sources=[]):
  19. self.sources = sources
  20. def __iter__(self):
  21. return iter(self.sources)
  22. class Source():
  23. """A Source is a single media file (eg), used to populate SourceMedia objects.
  24. JSON type schema:
  25. {
  26. "source": "/path/to/video.mp4",
  27. "path": "/path/to/video.mp4",
  28. "provider": "FileInputJSON"
  29. }
  30. def __init__(self, source, path, provider):
  31. if not source:
  32. raise ValueError("Source must be provided") # TODO: #API -- decide if this is necessary
  33. self.source = source
  34. if not path:
  35. # we need a file to work on for the rest of the pipeline
  36. raise ValueError("Path must be provided")
  37. self.path = path
  38. if not provider:
  39. raise ValueError("Provider must be provided") # TODO: #API -- decide if this is necessary
  40. self.provider = provider
  41. def __str__(self):
  42. """See: 'accessing the object should return the path to the media file'"""
  43. return self.path
  44. def __repr__(self):
  45. return f"Source({self.source}, {self.path}, {self.provider})"
  46. def duration(self):
  47. """Return the duration of the media file at self.path (result is cached)"""
  48. return self._duration or self._get_duration(self.path)
  49. def _get_duration(self, file):
  50. """Use ffprobe to get the duration of the media file at self.path and cache result (_duration)
  51. usage: ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 <file>
  52. """
  53. # test if file exists
  54. try:
  55. with open(file) as _:
  56. pass
  57. except FileNotFoundError:
  58. raise FileNotFoundError(f"File not found: {file}")
  59. # cache the result
  60. self._duration = 0.0 or float(subprocess.check_output(["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", file]))
  61. return self._duration
  62. class Interval():
  63. """An interval of time in a media file
  64. This can be defined by a start and end time, a start time and a duration, or an end time and a duration.
  65. Instance variables:
  66. start -- the start time of the interval
  67. end -- the end time of the interval
  68. duration -- the duration of the interval (end - start)
  69. Notes:
  70. Sorts by start time, then end time
  71. """
  72. # TODO: decide if ABC or will be used directly
  73. # TODO: have default duration for intervals set by config
  74. # TODO: consider if we want to permit adjusting intervals (eg, start time, end time, duration) [probably yes]
  75. # NOTE: if we have more ways of defining, we could consider multipledispatch?
  76. DEFAULT_DURATION = 5 # seconds
  77. DEFAUT_PRECISION = 3 # decimal places
  78. def __init__(self, start=None, end=None, duration=None):
  79. if start is None and end is None and duration is None:
  80. raise ValueError("Two of start, end, or duration must be provided")
  81. if start is not None and end is not None and duration is not None:
  82. raise ValueError("Only two of start, end, or duration may be provided")
  83. # start and end
  84. if start is not None and end is not None:
  85. # some trivial validation
  86. if start > end:
  87. raise ValueError("Start time must be before end time")
  88. self.start = start
  89. self.end = end
  90. self.duration = end - start
  91. # start and duration
  92. elif start is not None and duration is not None:
  93. if duration < 0:
  94. raise ValueError("Duration must be positive")
  95. self.start = start
  96. self.duration = duration
  97. self.end = start + duration
  98. # end and duration
  99. elif end is not None and duration is not None:
  100. if duration < 0:
  101. raise ValueError("Duration must be positive")
  102. self.end = end
  103. self.duration = duration
  104. self.start = end - duration
  105. # set precision
  106. self.start = round(self.start, self.DEFAUT_PRECISION)
  107. self.end = round(self.end, self.DEFAUT_PRECISION)
  108. self.duration = round(self.duration, self.DEFAUT_PRECISION)
  109. @classmethod
  110. def from_start(cls, start=None):
  111. """Create an interval from a start time using the default duration"""
  112. return cls(start=start, duration=cls.DEFAULT_DURATION)
  113. @classmethod
  114. def from_end(cls, end=None):
  115. """Create an interval from an end time using the default duration"""
  116. return cls(end=end, duration=cls.DEFAULT_DURATION)
  117. def __repr__(self):
  118. return f"Interval({self.start}, {self.end}, {self.duration})"
  119. def __lt__(self, other):
  120. if self.start == other.start:
  121. return self.end < other.end
  122. return self.start < other.start
  123. # --------------------------------------------------------------
  124. # TODO: handle bad cases, eg negative duration, start > end, etc
  125. # --------------------------------------------------------------
  126. def move_start(self, new_start: float | int, relative: bool = False):
  127. """Update start time of Interval, keeping end time constant (& so modify duration)"""
  128. if relative:
  129. self.start += new_start
  130. else:
  131. self.start = new_start
  132. self.duration = self.end - self.start
  133. def move_end(self, new_end: float | int, relative: bool = False):
  134. """Update end time of Interval, keeping start time constant (& so modify duration)"""
  135. if relative:
  136. self.end += new_end
  137. else:
  138. self.end = new_end
  139. self.duration = self.end - self.start
  140. def update_duration(self, new_duration: float | int, relative: bool = False):
  141. """Update duration of Interval, keeping start time constant (& so modify end time)"""
  142. if relative:
  143. self.duration += new_duration
  144. else:
  145. self.duration = new_duration
  146. self.end = self.start + self.duration