Class: Lipdub::Resources::Shots

Inherits:
Base
  • Object
show all
Defined in:
lib/lipdub/resources/shots.rb

Instance Attribute Summary

Attributes inherited from Base

#client

Instance Method Summary collapse

Methods inherited from Base

#initialize

Constructor Details

This class inherits a constructor from Lipdub::Resources::Base

Instance Method Details

#actors(shot_id) ⇒ Hash

Get actors for a shot

Examples:

actors = client.shots.actors(123)

Parameters:

  • shot_id (String, Integer)

    Unique identifier of the shot

Returns:

  • (Hash)

    Response containing actors information for the shot



235
236
237
# File 'lib/lipdub/resources/shots.rb', line 235

def actors(shot_id)
  get("/v1/shots/#{shot_id}/actors")
end

#add_frame_buffer(ranges, buffer_frames: 10, fps: 30, video_duration: nil) ⇒ Array<Array>

Adds frame buffer to timecode ranges for seamless blending

Parameters:

  • ranges (Array<Array>)

    Array of [start, end] timecode pairs

  • buffer_frames (Integer) (defaults to: 10)

    Number of frames to add as buffer (default: 10)

  • fps (Integer) (defaults to: 30)

    Frames per second (default: 30)

  • video_duration (Numeric) (defaults to: nil)

    Total video duration to clamp end times (optional)

Returns:

  • (Array<Array>)

    Buffered timecode ranges



330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
# File 'lib/lipdub/resources/shots.rb', line 330

def add_frame_buffer(ranges, buffer_frames: 10, fps: 30, video_duration: nil)
  return ranges if ranges.nil? || ranges.empty?
  
  buffer_seconds = buffer_frames.to_f / fps
  
  ranges.map do |start_time, end_time|
    start_seconds = parse_timecode_to_seconds(start_time, fps: fps)
    end_seconds = parse_timecode_to_seconds(end_time, fps: fps)
    
    buffered_start = [start_seconds - buffer_seconds, 0.0].max
    buffered_end = end_seconds + buffer_seconds
    
    if video_duration
      buffered_end = [buffered_end, video_duration].min
    end
    
    [buffered_start, buffered_end]
  end
end

#download(shot_id, generate_id) ⇒ Hash

Download generated video

Examples:

download_info = client.shots.download(123, "gen_789")
# => {
#   "data" => {
#     "download_url" => "https://storage.lipdub.ai/download/gen_789?token=xyz"
#   }
# }

Parameters:

  • shot_id (String, Integer)

    Unique identifier of the shot

  • generate_id (String)

    Unique identifier of the generation request

Returns:

  • (Hash)

    Response containing download_url for the dubbed video



119
120
121
# File 'lib/lipdub/resources/shots.rb', line 119

def download(shot_id, generate_id)
  get("/v1/shots/#{shot_id}/generate/#{generate_id}/download")
end

#download_file(shot_id, generate_id, file_path) ⇒ String

Download generated video file directly to a local path

Examples:

local_path = client.shots.download_file(123, "gen_789", "output/dubbed_video.mp4")
# Downloads the file and returns "output/dubbed_video.mp4"

Parameters:

  • shot_id (String, Integer)

    Unique identifier of the shot

  • generate_id (String)

    Unique identifier of the generation request

  • file_path (String)

    Local path where the video should be saved

Returns:

  • (String)

    Path to the downloaded file

Raises:



133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/lipdub/resources/shots.rb', line 133

def download_file(shot_id, generate_id, file_path)
  download_response = download(shot_id, generate_id)
  download_url = download_response.dig("data", "download_url") if download_response.is_a?(Hash)
  
  # Handle case where response might still be a string (fallback)
  if download_response.is_a?(String)
    parsed = JSON.parse(download_response)
    download_url = parsed.dig("data", "download_url")
  end
  
  raise APIError, "Download URL not found in response" unless download_url

  download_file_from_url(download_url, file_path)
end

#generate(shot_id:, audio_id:, output_filename:, language: nil, start_frame: nil, loop_video: nil, full_resolution: nil, callback_url: nil, timecode_ranges: nil) ⇒ Hash

Generate lip-dubbed video

Examples:

response = client.shots.generate(
  shot_id: 123,
  audio_id: "audio_456",
  output_filename: "dubbed_video.mp4",
  language: "en-US",
  start_frame: 0,
  loop_video: false,
  full_resolution: true,
  callback_url: "https://example.com/webhook"
)
# => {
#   "generate_id" => 456
# }

Parameters:

  • shot_id (String, Integer)

    Unique identifier of the shot

  • audio_id (String)

    Unique identifier of the audio file

  • output_filename (String)

    Name for the output file

  • language (String, nil) (defaults to: nil)

    Optional language specification (ISO 639-1)

  • start_frame (Integer, nil) (defaults to: nil)

    Frame number to start the lip-sync from (defaults to 0)

  • loop_video (Boolean, nil) (defaults to: nil)

    Whether to loop the video during rendering (defaults to false)

  • full_resolution (Boolean, nil) (defaults to: nil)

    Whether to use full resolution (defaults to true)

  • callback_url (String, nil) (defaults to: nil)

    Optional HTTPS URL for completion callback

  • timecode_ranges (Array, nil) (defaults to: nil)

    Optional list of timecode ranges to render

Returns:

  • (Hash)

    Response containing generation details and generate_id



77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/lipdub/resources/shots.rb', line 77

def generate(shot_id:, audio_id:, output_filename:, language: nil, start_frame: nil, 
             loop_video: nil, full_resolution: nil, callback_url: nil, timecode_ranges: nil)
  body = {
    audio_id: audio_id,
    output_filename: output_filename
  }
  
  body[:language] = language if language
  body[:start_frame] = start_frame if start_frame
  body[:loop_video] = loop_video unless loop_video.nil?
  body[:full_resolution] = full_resolution unless full_resolution.nil?
  body[:callback_url] = callback_url if callback_url
  body[:timecode_ranges] = timecode_ranges if timecode_ranges

  post("/v1/shots/#{shot_id}/generate", body)
end

#generate_and_wait(shot_id:, audio_id:, output_filename:, language: nil, start_frame: nil, loop_video: nil, full_resolution: nil, callback_url: nil, timecode_ranges: nil, polling_interval: 10, max_wait_time: 1800) ⇒ Hash

Complete generation workflow: generate and wait for completion

Examples:

result = client.shots.generate_and_wait(
  shot_id: 123,
  audio_id: "audio_456",
  output_filename: "dubbed_video.mp4",
  polling_interval: 15,
  max_wait_time: 3600
)

Parameters:

  • shot_id (String, Integer)

    Unique identifier of the shot

  • audio_id (String)

    Unique identifier of the audio file

  • output_filename (String)

    Name for the output file

  • language (String, nil) (defaults to: nil)

    Optional language specification (ISO 639-1)

  • start_frame (Integer, nil) (defaults to: nil)

    Frame number to start the lip-sync from (defaults to 0)

  • loop_video (Boolean, nil) (defaults to: nil)

    Whether to loop the video during rendering (defaults to false)

  • full_resolution (Boolean, nil) (defaults to: nil)

    Whether to use full resolution (defaults to true)

  • callback_url (String, nil) (defaults to: nil)

    Optional HTTPS URL for completion callback

  • timecode_ranges (Array, nil) (defaults to: nil)

    Optional list of timecode ranges to render

  • polling_interval (Integer) (defaults to: 10)

    Seconds to wait between status checks (default: 10)

  • max_wait_time (Integer) (defaults to: 1800)

    Maximum seconds to wait for completion (default: 1800)

Returns:

  • (Hash)

    Response containing final generation status

Raises:



171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
# File 'lib/lipdub/resources/shots.rb', line 171

def generate_and_wait(shot_id:, audio_id:, output_filename:, language: nil, 
                     start_frame: nil, loop_video: nil, full_resolution: nil, 
                     callback_url: nil, timecode_ranges: nil, polling_interval: 10, max_wait_time: 1800)
  # Start generation
  generate_response = generate(
    shot_id: shot_id,
    audio_id: audio_id,
    output_filename: output_filename,
    language: language,
    start_frame: start_frame,
    loop_video: loop_video,
    full_resolution: full_resolution,
    callback_url: callback_url,
    timecode_ranges: timecode_ranges
  )

  generate_id = nil
  if generate_response.is_a?(Hash)
    generate_id = generate_response["generate_id"] || generate_response.dig("data", "generate_id")
  elsif generate_response.is_a?(String)
    parsed = JSON.parse(generate_response)
    generate_id = parsed["generate_id"] || parsed.dig("data", "generate_id")
  end
  
  raise APIError, "Generate ID not found in response" unless generate_id

  # Poll for completion
  start_time = Time.now
  loop do
    status_response = generation_status(shot_id, generate_id)
    
    # Handle both Hash and String responses
    status = nil
    if status_response.is_a?(Hash)
      status = status_response.dig("data", "status") || status_response["status"]
    elsif status_response.is_a?(String)
      parsed = JSON.parse(status_response)
      status = parsed.dig("data", "status") || parsed["status"]
      status_response = parsed # Use parsed version for return
    end

    case status
    when "completed", "success"
      return status_response
    when "failed", "error"
      raise APIError, "Generation failed: #{status_response}"
    end

    # Check timeout
    if Time.now - start_time > max_wait_time
      raise TimeoutError, "Generation did not complete within #{max_wait_time} seconds"
    end

    sleep(polling_interval)
  end
end

#generate_multi_actor(shot_id:, **params) ⇒ Hash

Generate multi-actor LipDub for a shot

Examples:

response = client.shots.generate_multi_actor(
  shot_id: 123,
  params: { actors: [...] }
)

Parameters:

  • shot_id (String, Integer)

    Unique identifier of the shot

  • params (Hash)

    Multi-actor generation parameters

Returns:

  • (Hash)

    Response containing multi-actor generation details



275
276
277
# File 'lib/lipdub/resources/shots.rb', line 275

def generate_multi_actor(shot_id:, **params)
  post("/v1/shots/#{shot_id}/generate-multi-actor", params)
end

#generation_status(shot_id, generate_id) ⇒ Hash

Get generation status

Examples:

status = client.shots.generation_status(123, "gen_789")

Parameters:

  • shot_id (String, Integer)

    Unique identifier of the shot

  • generate_id (String)

    Unique identifier of the generation request

Returns:

  • (Hash)

    Response containing generation progress and status



102
103
104
# File 'lib/lipdub/resources/shots.rb', line 102

def generation_status(shot_id, generate_id)
  get("/v1/shots/#{shot_id}/generate/#{generate_id}")
end

#list(page: 1, per_page: 20) ⇒ Hash

List all available shots

Examples:

shots = client.shots.list(page: 1, per_page: 50)
# => {
#   "data" => [
#     {
#       "shot_id" => 99,
#       "shot_label" => "api-full-test-new.mp4",
#       "shot_project_id" => 37,
#       "shot_scene_id" => 37,
#       "shot_project_name" => "Lee Studios",
#       "shot_scene_name" => "Under the tent"
#     }
#   ],
#   "count" => 1
# }

Parameters:

  • page (Integer) (defaults to: 1)

    Page number for pagination (defaults to 1)

  • per_page (Integer) (defaults to: 20)

    Number of items per page, max 100 (defaults to 20)

Returns:

  • (Hash)

    Response containing list of shots and count



29
30
31
32
33
34
35
36
37
# File 'lib/lipdub/resources/shots.rb', line 29

def list(page: 1, per_page: 20)
  validate_pagination_params!(page, per_page)
  
  params = {
    page: page,
    per_page: per_page
  }
  get("/v1/shots", params)
end

#parse_timecode_to_seconds(timecode, fps: 30) ⇒ Float

Converts timecode to seconds

Parameters:

  • timecode (String, Numeric)

    Either numeric seconds or SMPTE format “HH:MM:SS:FF”

  • fps (Integer) (defaults to: 30)

    Frames per second for SMPTE conversion (default: 30)

Returns:

  • (Float)

    Time in seconds



354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
# File 'lib/lipdub/resources/shots.rb', line 354

def parse_timecode_to_seconds(timecode, fps: 30)
  case timecode
  when Numeric
    timecode.to_f
  when String
    if timecode.match?(/^\d{2}:\d{2}:\d{2}:\d{2}$/)
      # SMPTE format: HH:MM:SS:FF
      hours, minutes, seconds, frames = timecode.split(':').map(&:to_i)
      hours * 3600 + minutes * 60 + seconds + frames.to_f / fps
    else
      # Try parsing as float string
      timecode.to_f
    end
  else
    raise ArgumentError, "Invalid timecode format: #{timecode}. Use numeric seconds or SMPTE format (HH:MM:SS:FF)"
  end
end

#status(shot_id) ⇒ Hash

Get shot processing status

Examples:

status = client.shots.status(123)

Parameters:

  • shot_id (String, Integer)

    Unique identifier of the shot

Returns:

  • (Hash)

    Response containing shot status and processing information



46
47
48
# File 'lib/lipdub/resources/shots.rb', line 46

def status(shot_id)
  get("/v1/shots/#{shot_id}/status")
end

#translate(shot_id:, source_language:, target_language:, full_resolution: nil) ⇒ Hash

Translate a LipDub for a shot

Examples:

response = client.shots.translate(
  shot_id: 123,
  source_language: "English",
  target_language: "Spanish",
  full_resolution: true
)

Parameters:

  • shot_id (String, Integer)

    Unique identifier of the shot

  • source_language (String)

    Source language code

  • target_language (String)

    Target language code

  • full_resolution (Boolean, nil) (defaults to: nil)

    Whether to render in full resolution (defaults to true)

Returns:

  • (Hash)

    Response containing translation details



254
255
256
257
258
259
260
261
262
# File 'lib/lipdub/resources/shots.rb', line 254

def translate(shot_id:, source_language:, target_language:, full_resolution: nil)
  body = {
    source_language: source_language,
    target_language: target_language
  }
  body[:full_resolution] = full_resolution unless full_resolution.nil?

  post("/v1/shots/#{shot_id}/translate", body)
end

#validate_timecode_ranges(ranges, video_duration: nil) ⇒ Boolean

Validates timecode ranges for selective lip-dubbing

Parameters:

  • ranges (Array<Array>)

    Array of [start, end] timecode pairs

  • video_duration (Numeric) (defaults to: nil)

    Total video duration in seconds (optional)

Returns:

  • (Boolean)

    true if valid

Raises:

  • (ArgumentError)

    if ranges are invalid



286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
# File 'lib/lipdub/resources/shots.rb', line 286

def validate_timecode_ranges(ranges, video_duration: nil)
  return true if ranges.nil? || ranges.empty?

  unless ranges.is_a?(Array)
    raise ArgumentError, "timecode_ranges must be an array"
  end

  ranges.each_with_index do |range, index|
    unless range.is_a?(Array) && range.length == 2
      raise ArgumentError, "Each timecode range must be an array of [start, end] at index #{index}"
    end

    start_time, end_time = range
    start_seconds = parse_timecode_to_seconds(start_time)
    end_seconds = parse_timecode_to_seconds(end_time)

    if start_seconds >= end_seconds
      raise ArgumentError, "Start time must be before end time in range #{index}: #{range}"
    end

    if video_duration && end_seconds > video_duration
      raise ArgumentError, "End time #{end_time} exceeds video duration #{video_duration} in range #{index}"
    end
  end

  # Check for overlapping ranges
  sorted_ranges = ranges.map { |r| [parse_timecode_to_seconds(r[0]), parse_timecode_to_seconds(r[1])] }
                       .sort_by(&:first)
  
  sorted_ranges.each_cons(2) do |(prev_start, prev_end), (curr_start, curr_end)|
    if curr_start < prev_end
      raise ArgumentError, "Overlapping timecode ranges detected: [#{prev_start}, #{prev_end}] and [#{curr_start}, #{curr_end}]"
    end
  end

  true
end