{"id":31779585,"url":"https://github.com/vbrazo/lipdub","last_synced_at":"2025-10-10T07:52:09.983Z","repository":{"id":316837549,"uuid":"1064969190","full_name":"vbrazo/lipdub","owner":"vbrazo","description":"Ruby library that uploads videos, processes them, and generating dubbed content with LibDub.ai","archived":false,"fork":false,"pushed_at":"2025-09-27T01:37:32.000Z","size":45,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-09-27T03:25:12.595Z","etag":null,"topics":["ai","dubbing"],"latest_commit_sha":null,"homepage":"https://lipdub.readme.io/","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/vbrazo.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-09-26T20:59:49.000Z","updated_at":"2025-09-27T01:37:34.000Z","dependencies_parsed_at":"2025-09-27T03:25:16.496Z","dependency_job_id":"b0720fc3-30e3-4975-88b3-2856d15859d0","html_url":"https://github.com/vbrazo/lipdub","commit_stats":null,"previous_names":["vbrazo/lipdub"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/vbrazo/lipdub","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vbrazo%2Flipdub","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vbrazo%2Flipdub/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vbrazo%2Flipdub/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vbrazo%2Flipdub/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/vbrazo","download_url":"https://codeload.github.com/vbrazo/lipdub/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vbrazo%2Flipdub/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279003173,"owners_count":26083533,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-10T02:00:06.843Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ai","dubbing"],"created_at":"2025-10-10T07:52:01.146Z","updated_at":"2025-10-10T07:52:09.968Z","avatar_url":"https://github.com/vbrazo.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Lipdub Ruby Client\n\nA comprehensive Ruby client library for the [Lipdub.ai API](https://lipdub.ai), providing easy access to AI-powered lip-dubbing functionality.\n\n[![Gem Version](https://badge.fury.io/rb/lipdub.svg)](https://badge.fury.io/rb/lipdub)\n[![Build Status](https://github.com/vbrazo/lipdub/workflows/CI/badge.svg)](https://github.com/upriser/lipdub-ruby/actions)\n[![Security](https://img.shields.io/badge/security-bundler--audit-blue.svg)](https://github.com/rubysec/bundler-audit)\n\n## Table of Contents\n\n- [Features](#features)\n- [Installation](#installation)\n- [Configuration](#configuration)\n- [Quick Start](#quick-start)\n- [API Reference](#api-reference)\n  - [Videos](#videos)\n  - [Audios](#audios)\n  - [Shots](#shots)\n  - [Projects](#projects)\n- [Usage Examples](#usage-examples)\n  - [Video Upload](#video-upload)\n  - [Audio Upload](#audio-upload)\n  - [Shot Management](#shot-management)\n  - [Shot Generation](#shot-generation)\n  - [Project Management](#project-management)\n- [Complete Workflow Examples](#complete-workflow-examples)\n  - [Basic Lip-dubbing Workflow](#basic-lip-dubbing-workflow)\n  - [Selective Lip-dubbing Workflow](#selective-lip-dubbing-workflow)\n- [Supported File Formats](#supported-file-formats)\n- [Error Handling](#error-handling)\n- [Rate Limits and Best Practices](#rate-limits-and-best-practices)\n- [Troubleshooting](#troubleshooting)\n- [Development](#development)\n- [Contributing](#contributing)\n- [License](#license)\n\n## Features\n\n- **Video Upload**: Upload and process videos for lip-dubbing\n- **Audio Upload**: Upload audio files for voice replacement\n- **Generation**: Generate lip-dubbed videos with AI\n- **Status Monitoring**: Track processing and generation progress\n- **File Management**: Handle file uploads and downloads seamlessly\n- **Error Handling**: Comprehensive error handling with custom exceptions\n- **Test Coverage**: Full test suite with RSpec\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem 'lipdub'\n```\n\nAnd then execute:\n\n    $ bundle install\n\nOr install it yourself as:\n\n    $ gem install lipdub\n\n## Configuration\n\nConfigure the client with your API key:\n\n```ruby\nrequire 'lipdub'\n\n# Global configuration\nLipdub.configure do |config|\n  config.api_key = \"your_api_key_here\"\n  config.base_url = \"https://api.lipdub.ai\"  # Optional, this is the default\n  config.timeout = 30                        # Optional, default is 30 seconds\n  config.open_timeout = 10                   # Optional, default is 10 seconds\nend\n\n# Or create a client with specific configuration\nconfig = Lipdub::Configuration.new\nconfig.api_key = \"your_api_key_here\"\nclient = Lipdub::Client.new(config)\n```\n\n## Quick Start\n\nHere's a minimal example to get you started with lip-dubbing:\n\n```ruby\nrequire 'lipdub'\n\n# Configure the client\nLipdub.configure do |config|\n  config.api_key = \"your_api_key_here\"\nend\n\nclient = Lipdub.client\n\n# Upload video and audio, then generate lip-dubbed video\nvideo_response = client.videos.upload_complete(\"input/video.mp4\")\nshot_id = video_response.dig(\"data\", \"shot_id\")\n\naudio_response = client.audios.upload_complete(\"input/audio.mp3\")\naudio_id = audio_response.dig(\"data\", \"audio_id\")\n\n# Generate and download the result\nresult = client.shots.generate_and_wait(\n  shot_id: shot_id,\n  audio_id: audio_id,\n  output_filename: \"dubbed_video.mp4\"\n)\n\ngenerate_id = result.dig(\"data\", \"generate_id\")\nclient.shots.download_file(shot_id, generate_id, \"output/result.mp4\")\n```\n\n## API Reference\n\nThe Lipdub Ruby client provides four main resource classes for interacting with the API:\n\n### Videos\n\nThe Videos resource handles video file uploads and processing.\n\n#### Methods\n\n| Method | Description | Parameters | Returns |\n|--------|-------------|------------|---------|\n| `upload(size_bytes:, file_name:, content_type:, video_source_url: nil)` | Initiate video upload process | `size_bytes` (Integer), `file_name` (String), `content_type` (String), `video_source_url` (String, optional) | Hash with `video_id`, `upload_url`, `success_url`, `failure_url` |\n| `upload_file(upload_url, file_content, content_type)` | Upload video file to provided URL | `upload_url` (String), `file_content` (String/IO), `content_type` (String) | Hash |\n| `upload_complete(file_path, content_type: nil)` | Complete upload workflow from file path | `file_path` (String), `content_type` (String, optional) | Hash with `shot_id` and `asset_type` |\n| `success(video_id)` | Mark video upload as successful | `video_id` (String) | Hash with `shot_id` and `asset_type` |\n| `failure(video_id)` | Mark video upload as failed | `video_id` (String) | Hash |\n| `status(video_id)` | Get video processing status | `video_id` (String) | Hash with status information |\n\n#### Endpoints\n\n- `POST /v1/video` - Initiate video upload\n- `POST /v1/video/success/{video_id}` - Mark upload successful\n- `POST /v1/video/failure/{video_id}` - Mark upload failed\n- `GET /v1/video/status/{video_id}` - Get processing status\n\n### Audios\n\nThe Audios resource handles audio file uploads and management.\n\n#### Methods\n\n| Method | Description | Parameters | Returns |\n|--------|-------------|------------|---------|\n| `upload(size_bytes:, file_name:, content_type:, audio_source_url: nil)` | Initiate audio upload process | `size_bytes` (Integer, 1-104857600), `file_name` (String), `content_type` (String), `audio_source_url` (String, optional) | Hash with `audio_id`, `upload_url`, `success_url`, `failure_url` |\n| `upload_file(upload_url, file_content, content_type)` | Upload audio file to provided URL | `upload_url` (String), `file_content` (String/IO), `content_type` (String) | Hash |\n| `upload_complete(file_path, content_type: nil)` | Complete upload workflow from file path | `file_path` (String), `content_type` (String, optional) | Hash with upload result |\n| `success(audio_id)` | Mark audio upload as successful | `audio_id` (String) | Hash |\n| `failure(audio_id)` | Mark audio upload as failed | `audio_id` (String) | Hash |\n| `status(audio_id)` | Get audio processing status | `audio_id` (String) | Hash with status information |\n| `list(page: 1, page_size: 10)` | List all audio files | `page` (Integer), `page_size` (Integer) | Hash with audio list and pagination |\n\n#### Endpoints\n\n- `POST /v1/audio` - Initiate audio upload\n- `POST /v1/audio/success/{audio_id}` - Mark upload successful\n- `POST /v1/audio/failure/{audio_id}` - Mark upload failed\n- `GET /v1/audio/status/{audio_id}` - Get processing status\n- `GET /v1/audio` - List audio files\n\n#### Supported Audio Formats\n\n- `audio/mpeg` (MP3)\n- `audio/wav` (WAV)\n- `audio/mp4` (MP4/M4A)\n\n### Shots\n\nThe Shots resource handles lip-dubbing generation, translation, and shot management.\n\n#### Methods\n\n| Method | Description | Parameters | Returns |\n|--------|-------------|------------|---------|\n| `list(page: 1, per_page: 20)` | List available shots | `page` (Integer), `per_page` (Integer, max 100) | Hash with shots list and count |\n| `status(shot_id)` | Get shot processing status | `shot_id` (String/Integer) | Hash with status information |\n| `generate(shot_id:, audio_id:, output_filename:, **options)` | Generate lip-dubbed video | See generation options below | Hash with `generate_id` |\n| `generation_status(shot_id, generate_id)` | Get generation progress | `shot_id` (String/Integer), `generate_id` (String) | Hash with progress and status |\n| `download(shot_id, generate_id)` | Get download URL for generated video | `shot_id` (String/Integer), `generate_id` (String) | Hash with `download_url` |\n| `download_file(shot_id, generate_id, file_path)` | Download generated video to local path | `shot_id` (String/Integer), `generate_id` (String), `file_path` (String) | String (file path) |\n| `generate_and_wait(shot_id:, audio_id:, output_filename:, **options)` | Generate and wait for completion | Same as generate + `polling_interval` (Integer), `max_wait_time` (Integer) | Hash with final status |\n| `actors(shot_id)` | Get actors for a shot | `shot_id` (String/Integer) | Hash with actors information |\n| `translate(shot_id:, source_language:, target_language:, full_resolution: nil)` | Translate a shot | `shot_id` (String/Integer), `source_language` (String), `target_language` (String), `full_resolution` (Boolean, optional) | Hash with translation details |\n| `generate_multi_actor(shot_id:, **params)` | Generate multi-actor lip-dub | `shot_id` (String/Integer), `params` (Hash) | Hash with generation details |\n\n#### Generation Options\n\n| Parameter | Type | Description | Default |\n|-----------|------|-------------|---------|\n| `shot_id` | String/Integer | **Required.** Unique identifier of the shot | - |\n| `audio_id` | String | **Required.** Unique identifier of the audio file | - |\n| `output_filename` | String | **Required.** Name for the output file | - |\n| `language` | String | Language specification (ISO 639-1) | `nil` |\n| `start_frame` | Integer | Frame number to start lip-sync from | `0` |\n| `loop_video` | Boolean | Whether to loop video during rendering | `false` |\n| `full_resolution` | Boolean | Whether to use full resolution | `true` |\n| `callback_url` | String | HTTPS URL for completion callback | `nil` |\n| `timecode_ranges` | Array | List of `[start, end]` timecode pairs for selective lip-dubbing | `nil` |\n\n#### Timecode Ranges for Selective Lip-dubbing\n\nTimecode ranges allow you to lip-dub only specific parts of a video:\n\n```ruby\n# Using seconds (float)\ntimecode_ranges: [[2.5, 4.2], [10.0, 12.5]]\n\n# Using SMPTE format (HH:MM:SS:FF)\ntimecode_ranges: [[\"00:00:02:15\", \"00:00:04:06\"], [\"00:00:10:00\", \"00:00:12:15\"]]\n```\n\n#### Helper Methods\n\n| Method | Description | Parameters | Returns |\n|--------|-------------|------------|---------|\n| `validate_timecode_ranges(ranges, video_duration: nil)` | Validate timecode ranges | `ranges` (Array), `video_duration` (Numeric, optional) | Boolean or raises ArgumentError |\n| `add_frame_buffer(ranges, buffer_frames: 10, fps: 30, video_duration: nil)` | Add frame buffer to ranges | `ranges` (Array), `buffer_frames` (Integer), `fps` (Integer), `video_duration` (Numeric, optional) | Array of buffered ranges |\n| `parse_timecode_to_seconds(timecode, fps: 30)` | Convert timecode to seconds | `timecode` (String/Numeric), `fps` (Integer) | Float |\n\n#### Endpoints\n\n- `GET /v1/shots` - List shots\n- `GET /v1/shots/{shot_id}/status` - Get shot status\n- `POST /v1/shots/{shot_id}/generate` - Generate lip-dubbed video\n- `GET /v1/shots/{shot_id}/generate/{generate_id}` - Get generation status\n- `GET /v1/shots/{shot_id}/generate/{generate_id}/download` - Get download URL\n- `GET /v1/shots/{shot_id}/actors` - Get shot actors\n- `POST /v1/shots/{shot_id}/translate` - Translate shot\n- `POST /v1/shots/{shot_id}/generate-multi-actor` - Multi-actor generation\n\n### Projects\n\nThe Projects resource handles project management and listing.\n\n#### Methods\n\n| Method | Description | Parameters | Returns |\n|--------|-------------|------------|---------|\n| `list(page: 1, per_page: 20)` | List all projects | `page` (Integer), `per_page` (Integer, max 100) | Hash with projects list and count |\n\n#### Endpoints\n\n- `GET /v1/projects` - List projects\n\n## Usage Examples\n\n### Video Upload\n\n#### Simple Video Upload\n\n```ruby\nclient = Lipdub.client\n\n# Upload a video file (handles the entire workflow)\nresponse = client.videos.upload_complete(\"path/to/your/video.mp4\")\n# =\u003e {\n#   \"data\" =\u003e {\n#     \"shot_id\" =\u003e 123,\n#     \"asset_type\" =\u003e \"dubbing-video\"\n#   }\n# }\n\nshot_id = response.dig(\"data\", \"shot_id\")\n```\n\n#### Manual Video Upload Process\n\n```ruby\n# Step 1: Initiate upload\nupload_response = client.videos.upload(\n  size_bytes: File.size(\"video.mp4\"),\n  file_name: \"my_video.mp4\",\n  content_type: \"video/mp4\"\n)\n\nvideo_id = upload_response.dig(\"data\", \"video_id\")\nupload_url = upload_response.dig(\"data\", \"upload_url\")\n\n# Step 2: Upload the file\nfile_content = File.read(\"video.mp4\")\nclient.videos.upload_file(upload_url, file_content, \"video/mp4\")\n\n# Step 3: Mark as successful\nsuccess_response = client.videos.success(video_id)\nshot_id = success_response.dig(\"data\", \"shot_id\")\n```\n\n#### Check Video Status\n\n```ruby\nstatus = client.videos.status(video_id)\n# =\u003e {\n#   \"data\" =\u003e {\n#     \"status\" =\u003e \"processing\",\n#     \"progress\" =\u003e 50\n#   }\n# }\n```\n\n### Audio Upload\n\n#### Simple Audio Upload\n\n```ruby\n# Upload an audio file (handles the entire workflow)\nresponse = client.audios.upload_complete(\"path/to/your/audio.mp3\")\n\n# Get the audio_id for generation\naudio_id = response.dig(\"data\", \"audio_id\")\n```\n\n#### Manual Audio Upload Process\n\n```ruby\n# Step 1: Initiate upload\nupload_response = client.audios.upload(\n  size_bytes: File.size(\"audio.mp3\"),\n  file_name: \"voiceover.mp3\",\n  content_type: \"audio/mpeg\"\n)\n\naudio_id = upload_response.dig(\"data\", \"audio_id\")\nupload_url = upload_response.dig(\"data\", \"upload_url\")\n\n# Step 2: Upload the file\nfile_content = File.read(\"audio.mp3\")\nclient.audios.upload_file(upload_url, file_content, \"audio/mpeg\")\n\n# Step 3: Mark as successful\nclient.audios.success(audio_id)\n```\n\n#### List Audio Files\n\n```ruby\n# List all audio files\naudios = client.audios.list(page: 1, page_size: 20)\n# =\u003e {\n#   \"data\" =\u003e [\n#     { \"audio_id\" =\u003e \"audio_1\", \"file_name\" =\u003e \"voice1.mp3\" },\n#     { \"audio_id\" =\u003e \"audio_2\", \"file_name\" =\u003e \"voice2.wav\" }\n#   ],\n#   \"pagination\" =\u003e { \"page\" =\u003e 1, \"page_size\" =\u003e 20, \"total\" =\u003e 50 }\n# }\n```\n\n### Shot Management\n\n#### List Available Shots\n\n```ruby\n# List all shots\nshots = client.shots.list(page: 1, per_page: 50)\n# =\u003e {\n#   \"data\" =\u003e [\n#     {\n#       \"shot_id\" =\u003e 99,\n#       \"shot_label\" =\u003e \"api-full-test-new.mp4\",\n#       \"shot_project_id\" =\u003e 37,\n#       \"shot_scene_id\" =\u003e 37,\n#       \"shot_project_name\" =\u003e \"Lee Studios\",\n#       \"shot_scene_name\" =\u003e \"Under the tent\"\n#     }\n#   ],\n#   \"count\" =\u003e 1\n# }\n```\n\n#### Get Shot Actors\n\n```ruby\n# Get actors for a specific shot\nactors = client.shots.actors(shot_id)\n```\n\n### Shot Generation\n\n#### Generate Lip-Dubbed Video\n\n```ruby\n# Start generation with all available options\ngenerate_response = client.shots.generate(\n  shot_id: shot_id,\n  audio_id: audio_id,\n  output_filename: \"final_dubbed_video.mp4\",\n  language: \"en-US\",                 # Optional (ISO 639-1)\n  start_frame: 0,                    # Optional\n  loop_video: false,                 # Optional\n  full_resolution: true,             # Optional\n  callback_url: \"https://example.com/webhook\", # Optional\n  timecode_ranges: [[0, 100], [200, 300]]     # Optional\n)\n\ngenerate_id = generate_response[\"generate_id\"]\n```\n\n#### Selective Lip-dubbing for Single Actors\n\nFor scenarios where you only want to lip-dub specific parts of a video (e.g., personalization where only a name needs to be replaced), you can use selective lip-dubbing with `timecode_ranges`:\n\n```ruby\n# Basic selective lip-dubbing with time ranges in seconds\nresponse = client.shots.generate(\n  shot_id: 123,\n  audio_id: \"audio_abc123\",\n  output_filename: \"personalized_video.mp4\",\n  timecode_ranges: [[0, 10], [20, 30]] # Replace seconds 0-10 and 20-30\n)\n\n# With SMPTE timecode format (be consistent with format)\nresponse = client.shots.generate(\n  shot_id: 123,\n  audio_id: \"audio_abc123\", \n  output_filename: \"personalized_video.mp4\",\n  timecode_ranges: [[\"00:00:00:00\", \"00:00:10:00\"], [\"00:00:20:00\", \"00:00:30:00\"]]\n)\n\n# Example: Replace a name greeting with proper buffering\n# Calculate 10-frame buffer (assuming 30fps: 10/30 = 0.33 seconds)\nname_start = 2.5 - 0.33  # Start 10 frames before\nname_end = 4.2 + 0.33    # End 10 frames after\n\nresponse = client.shots.generate(\n  shot_id: 123,\n  audio_id: \"audio_with_new_name\",\n  output_filename: \"personalized_greeting.mp4\", \n  timecode_ranges: [[name_start, name_end]],\n  language: \"en-US\"\n)\n```\n\n##### Best Practices for Selective Lip-dubbing\n\n1. **Match Original Region Length**: Ensure replaced audio regions match the original region length to maintain sync\n2. **Add Frame Buffer**: Include a 10-frame buffer around start/end timecodes for seamless blending  \n3. **Normalize Audio**: Normalize audio levels and isolate vocals from background noise for best results\n4. **Audio Duration**: The total audio duration must match the video duration\n5. **Consistent Timecode Format**: Use either seconds (float) or SMPTE format consistently\n6. **Non-overlapping Ranges**: Ensure timecode ranges don't overlap each other\n\n#### Translation\n\n```ruby\n# Translate a shot to different language\ntranslation = client.shots.translate(\n  shot_id: shot_id,\n  source_language: \"English\",\n  target_language: \"Spanish\",\n  full_resolution: true              # Optional\n)\n```\n\n#### Multi-Actor Generation\n\n```ruby\n# Generate with multiple actors\nmulti_result = client.shots.generate_multi_actor(\n  shot_id: shot_id,\n  actors: [\n    { actor_id: 1, voice_id: \"voice_1\" },\n    { actor_id: 2, voice_id: \"voice_2\" }\n  ]\n)\n```\n\n#### Monitor Generation Progress\n\n```ruby\n# Check generation status\nstatus = client.shots.generation_status(shot_id, generate_id)\n# =\u003e {\n#   \"data\" =\u003e {\n#     \"generate_id\" =\u003e \"gen_789\",\n#     \"status\" =\u003e \"processing\",\n#     \"progress\" =\u003e 75\n#   }\n# }\n```\n\n#### Generate and Wait for Completion\n\n```ruby\n# Generate and automatically wait for completion\nresult = client.shots.generate_and_wait(\n  shot_id: shot_id,\n  audio_id: audio_id,\n  output_filename: \"dubbed_video.mp4\",\n  polling_interval: 10,    # Check every 10 seconds\n  max_wait_time: 1800     # Wait up to 30 minutes\n)\n# =\u003e Returns when generation is complete or raises an error\n```\n\n#### Download Generated Video\n\n```ruby\n# Get download URL\ndownload_info = client.shots.download(shot_id, generate_id)\ndownload_url = download_info.dig(\"data\", \"download_url\")\n\n# Or download directly to a file\nlocal_path = client.shots.download_file(\n  shot_id, \n  generate_id, \n  \"output/my_dubbed_video.mp4\"\n)\n```\n\n### Project Management\n\n#### List Projects\n\n```ruby\n# List all projects\nprojects = client.projects.list(page: 1, per_page: 20)\n# =\u003e {\n#   \"data\" =\u003e [\n#     {\n#       \"project_id\" =\u003e 123,\n#       \"projects_tenant_id\" =\u003e 1,\n#       \"projects_user_id\" =\u003e 47,\n#       \"project_name\" =\u003e \"My Sample Project\",\n#       \"user_email\" =\u003e \"user@example.com\",\n#       \"created_at\" =\u003e \"2024-01-15T10:30:00Z\",\n#       \"updated_at\" =\u003e nil,\n#       \"source_language\" =\u003e {\n#         \"language_id\" =\u003e 1,\n#         \"name\" =\u003e \"English\",\n#         \"supported\" =\u003e true\n#       },\n#       \"project_identity_type\" =\u003e \"single_identity\",\n#       \"language_project_links\" =\u003e []\n#     }\n#   ],\n#   \"count\" =\u003e 1\n# }\n```\n\n### Complete Workflow Examples\n\n#### Basic Lip-dubbing Workflow\n\nHere's a complete example that uploads a video and audio, generates a lip-dubbed video, and downloads the result:\n\n```ruby\nrequire 'lipdub'\n\n# Configure the client\nLipdub.configure do |config|\n  config.api_key = \"your_api_key_here\"\nend\n\nclient = Lipdub.client\n\nbegin\n  # 0. List existing projects and shots (optional)\n  puts \"Listing projects...\"\n  projects = client.projects.list\n  puts \"Found #{projects['count']} projects\"\n  \n  puts \"Listing available shots...\"\n  shots = client.shots.list\n  puts \"Found #{shots['count']} shots\"\n\n  # 1. Upload video\n  puts \"Uploading video...\"\n  video_response = client.videos.upload_complete(\"input/original_video.mp4\")\n  shot_id = video_response.dig(\"data\", \"shot_id\")\n  puts \"Video uploaded, shot_id: #{shot_id}\"\n\n  # 2. Upload audio\n  puts \"Uploading audio...\"\n  audio_response = client.audios.upload_complete(\"input/new_voice.mp3\")\n  audio_id = audio_response.dig(\"data\", \"audio_id\") || \"audio_from_upload\"\n  puts \"Audio uploaded, audio_id: #{audio_id}\"\n\n  # 3. Generate lip-dubbed video\n  puts \"Starting generation...\"\n  result = client.shots.generate_and_wait(\n    shot_id: shot_id,\n    audio_id: audio_id,\n    output_filename: \"dubbed_output.mp4\",\n    language: \"en\",\n    maintain_expression: true,\n    polling_interval: 15,\n    max_wait_time: 3600  # 1 hour\n  )\n\n  generate_id = result.dig(\"data\", \"generate_id\")\n  puts \"Generation complete, generate_id: #{generate_id}\"\n\n  # 4. Download the result\n  puts \"Downloading result...\"\n  output_path = client.shots.download_file(\n    shot_id,\n    generate_id,\n    \"output/final_dubbed_video.mp4\"\n  )\n  puts \"Video downloaded to: #{output_path}\"\n```\n\n#### Selective Lip-dubbing Workflow\n\nHere's an example showing how to use selective lip-dubbing for personalization (e.g., replacing just a name in a greeting):\n\n```ruby\nrequire 'lipdub'\n\n# Configure the client\nLipdub.configure do |config|\n  config.api_key = \"your_api_key_here\"\nend\n\nclient = Lipdub.client\n\nbegin\n  # 1. Upload original video (done once)\n  video_response = client.videos.upload_complete(\n    file_path: \"./original_greeting.mp4\",\n    content_type: \"video/mp4\"\n  )\n  video_id = video_response.dig(\"data\", \"video_id\")\n\n  # 2. Upload personalized audio (replace original audio with new name)\n  # NOTE: Audio duration must match the video duration exactly\n  personalized_audio = client.audios.upload_complete(\n    file_path: \"./personalized_greeting_audio.mp3\", # Contains new name\n    content_type: \"audio/mp3\"\n  )\n  audio_id = personalized_audio.dig(\"data\", \"audio_id\")\n\n  # 3. Wait for video processing and get shot_id\n  loop do\n    status = client.videos.status(video_id: video_id)\n    if status.dig(\"data\", \"status\") == \"success\"\n      shot_id = status.dig(\"data\", \"shot_id\")\n      break\n    elsif status.dig(\"data\", \"status\") == \"failed\"\n      raise \"Video processing failed\"\n    end\n    sleep 5\n  end\n\n  # 4. Define timecode ranges for selective replacement\n  # Example: Replace name at 2.5-4.2 seconds with 10-frame buffer\n  name_start = 2.5\n  name_end = 4.2\n\n  # Use helper method to add frame buffer (recommended)\n  timecode_ranges = client.shots.add_frame_buffer(\n    [[name_start, name_end]], \n    buffer_frames: 10, \n    fps: 30\n  )\n\n  # Validate ranges (optional but recommended)\n  client.shots.validate_timecode_ranges(\n    timecode_ranges, \n    video_duration: 30.0 # Your video duration\n  )\n\n  # 5. Generate selective lip-dub\n  generation = client.shots.generate(\n    shot_id: shot_id,\n    audio_id: audio_id,\n    output_filename: \"personalized_greeting.mp4\",\n    timecode_ranges: timecode_ranges, # Only lip-dub the name part\n    language: \"en-US\"\n  )\n\n  generate_id = generation[\"generate_id\"]\n\n  # 6. Wait for generation to complete and download\n  client.shots.download_file(\n    shot_id,\n    generate_id,\n    \"output/personalized_greeting.mp4\"\n  )\n\n  puts \"Personalized video with selective lip-dubbing saved!\"\n\n  # Alternative: Multiple selective ranges (e.g., name + closing)\n  multiple_ranges = [\n    [2.5, 4.2],   # Name replacement\n    [25.0, 27.5]  # Closing replacement\n  ]\n\n  buffered_ranges = client.shots.add_frame_buffer(\n    multiple_ranges,\n    buffer_frames: 10,\n    fps: 30,\n    video_duration: 30.0\n  )\n\n  # Generate with multiple selective ranges\n  multi_selective = client.shots.generate(\n    shot_id: shot_id,\n    audio_id: audio_id,\n    output_filename: \"multi_personalized.mp4\",\n    timecode_ranges: buffered_ranges\n  )\n\nrescue Lipdub::AuthenticationError =\u003e e\n  puts \"Authentication failed: #{e.message}\"\nrescue Lipdub::ValidationError =\u003e e\n  puts \"Validation error: #{e.message}\"\nrescue Lipdub::TimeoutError =\u003e e\n  puts \"Request timed out: #{e.message}\"\nrescue Lipdub::APIError =\u003e e\n  puts \"API error (#{e.status_code}): #{e.message}\"\nrescue =\u003e e\n  puts \"Unexpected error: #{e.message}\"\nend\n```\n\n## Supported File Formats\n\n### Video Formats\n- **MP4** (recommended: 1080p HD, 23.976 fps, H.264 codec) - `video/mp4`\n- **MOV** (QuickTime) - `video/quicktime`\n- **AVI** (Audio Video Interleave) - `video/x-msvideo`\n- **WebM** (Web Media) - `video/webm`\n- **MKV** (Matroska Video) - `video/x-matroska`\n\n### Audio Formats\n- **MP3** (MPEG Audio Layer III) - `audio/mpeg` (1 byte to 100MB)\n- **WAV** (Waveform Audio File Format) - `audio/wav` (1 byte to 100MB)\n- **MP4/M4A** (MPEG-4 Audio) - `audio/mp4` (1 byte to 100MB)\n\n### Recommendations\n- **Video**: Use MP4 with H.264 codec for best compatibility and processing speed\n- **Audio**: Use MP3 or WAV for optimal lip-sync results\n- **Resolution**: 1080p HD recommended for best quality output\n- **Frame Rate**: 23.976 fps or 30 fps for smooth lip-sync\n- **Audio Quality**: 44.1kHz sample rate, 16-bit depth minimum\n\n## Error Handling\n\nThe gem provides comprehensive error handling with specific exception types:\n\n```ruby\nbegin\n  client.videos.upload_complete(\"video.mp4\")\nrescue Lipdub::AuthenticationError =\u003e e\n  # API key is invalid or missing\n  puts \"Authentication failed: #{e.message}\"\nrescue Lipdub::ValidationError =\u003e e\n  # Request parameters are invalid\n  puts \"Validation error: #{e.message}\"\n  puts \"Status code: #{e.status_code}\"\n  puts \"Response body: #{e.response_body}\"\nrescue Lipdub::NotFoundError =\u003e e\n  # Resource not found\n  puts \"Resource not found: #{e.message}\"\nrescue Lipdub::RateLimitError =\u003e e\n  # Rate limit exceeded\n  puts \"Rate limit exceeded: #{e.message}\"\nrescue Lipdub::ServerError =\u003e e\n  # Server error (5xx)\n  puts \"Server error: #{e.message}\"\nrescue Lipdub::TimeoutError =\u003e e\n  # Request timed out\n  puts \"Request timed out: #{e.message}\"\nrescue Lipdub::ConnectionError =\u003e e\n  # Connection failed\n  puts \"Connection failed: #{e.message}\"\nrescue Lipdub::APIError =\u003e e\n  # Generic API error\n  puts \"API error: #{e.message}\"\nrescue Lipdub::ConfigurationError =\u003e e\n  # Configuration is invalid\n  puts \"Configuration error: #{e.message}\"\nend\n```\n\n## Rate Limits and Best Practices\n\n### Rate Limits\nThe Lipdub API implements rate limiting to ensure fair usage:\n- **Upload endpoints**: 10 requests per minute\n- **Generation endpoints**: 5 requests per minute  \n- **Status/List endpoints**: 100 requests per minute\n\n### Best Practices\n\n#### Performance Optimization\n- **Batch operations**: Upload multiple files before starting generation\n- **Polling intervals**: Use appropriate intervals (10-30 seconds) when polling for status\n- **File optimization**: Compress videos and normalize audio before upload\n- **Concurrent uploads**: Upload video and audio files in parallel when possible\n\n#### Error Handling\n- **Retry logic**: Implement exponential backoff for transient errors\n- **Validation**: Validate file formats and sizes before upload\n- **Monitoring**: Log API responses for debugging and monitoring\n- **Graceful degradation**: Handle API failures gracefully in production\n\n#### Security\n- **API key protection**: Store API keys securely (environment variables, secrets management)\n- **HTTPS only**: All API communications use HTTPS\n- **File validation**: Validate uploaded files on your end before sending to API\n- **Webhook security**: Verify webhook signatures if using callback URLs\n\n#### Resource Management\n- **Cleanup**: Remove temporary files after processing\n- **Storage**: Monitor storage usage for large video files\n- **Timeouts**: Set appropriate timeouts for long-running operations\n- **Memory**: Stream large files instead of loading entirely into memory\n\n## Troubleshooting\n\n### Common Issues\n\n#### Upload Failures\n```ruby\n# Issue: File upload fails with timeout\n# Solution: Increase timeout settings\nLipdub.configure do |config|\n  config.timeout = 120        # 2 minutes for large files\n  config.open_timeout = 30    # 30 seconds to establish connection\nend\n```\n\n#### Generation Errors\n```ruby\n# Issue: Generation fails with validation error\n# Solution: Validate inputs before generation\nbegin\n  # Ensure audio duration matches video duration\n  client.shots.validate_timecode_ranges(ranges, video_duration: 30.0)\n  \n  result = client.shots.generate(\n    shot_id: shot_id,\n    audio_id: audio_id,\n    output_filename: \"output.mp4\"\n  )\nrescue Lipdub::ValidationError =\u003e e\n  puts \"Validation failed: #{e.message}\"\n  # Handle validation error\nend\n```\n\n#### Network Issues\n```ruby\n# Issue: Connection timeouts or failures\n# Solution: Implement retry logic with exponential backoff\ndef upload_with_retry(file_path, max_retries: 3)\n  retries = 0\n  begin\n    client.videos.upload_complete(file_path)\n  rescue Lipdub::TimeoutError, Lipdub::ConnectionError =\u003e e\n    retries += 1\n    if retries \u003c= max_retries\n      sleep(2 ** retries) # Exponential backoff\n      retry\n    else\n      raise e\n    end\n  end\nend\n```\n\n### Debug Mode\n\nEnable debug logging to troubleshoot issues:\n\n```ruby\n# Enable debug logging (if supported by your HTTP client)\nLipdub.configure do |config|\n  config.api_key = \"your_api_key\"\n  config.debug = true  # Enable debug mode\nend\n\n# Or use a custom logger\nrequire 'logger'\nlogger = Logger.new(STDOUT)\nlogger.level = Logger::DEBUG\n\n# Log API requests and responses\nclient = Lipdub.client\n# Add logging middleware to your HTTP client if needed\n```\n\n### Getting Help\n\n- **Documentation**: Check this README and inline code documentation\n- **API Status**: Monitor [Lipdub API status page](https://status.lipdub.ai) for service issues\n- **Support**: Contact support@lipdub.ai for API-related issues\n- **Issues**: Report bugs on [GitHub Issues](https://github.com/upriser/lipdub-ruby/issues)\n\n## Development\n\nAfter checking out the repo, run:\n\n```bash\nbin/setup\n```\n\nTo install dependencies. Then, run:\n\n```bash\nrake spec\n```\n\nTo run the tests. You can also run:\n\n```bash\nbin/console\n```\n\nFor an interactive prompt that will allow you to experiment.\n\n### Running Tests\n\n```bash\n# Run all tests\nbundle exec rspec\n\n# Run specific test file\nbundle exec rspec spec/lipdub/client_spec.rb\n\n# Run tests with coverage\nbundle exec rspec --format documentation\n\n# Run security audit\nbundle exec rake audit\n\n# Run all CI checks (tests + security audit)\nbundle exec rake ci\n\n# Optional: Run rubocop for linting (not included in CI)\nbundle exec rubocop\n```\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at https://github.com/upriser/lipdub-ruby.\n\n1. Fork it\n2. Create your feature branch (`git checkout -b my-new-feature`)\n3. Make your changes and add tests\n4. Run the test suite (`bundle exec rake ci`)\n5. Ensure security audit passes (`bundle exec rake audit`)\n6. Commit your changes (`git commit -am 'Add some feature'`)\n7. Push to the branch (`git push origin my-new-feature`)\n8. Create new Pull Request\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvbrazo%2Flipdub","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fvbrazo%2Flipdub","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvbrazo%2Flipdub/lists"}