iachat/app/services/audio/transcode_service.rb
Gabriel Jablonski ce39e54308
feat: add audio transcoding support for WhatsApp Cloud API (#220)
* feat: add audio transcoding support for WhatsApp Cloud API

- Introduced `Audio::TranscodeService` to handle audio transcoding to OGG/Opus format.
- Updated `Messages::MessageBuilder` to transcode audio attachments based on `transcode_audio` parameter.
- Enhanced `WhatsappCloudService` to normalize audio content types and send voice flag for recorded audio in OGG format.
- Added utility functions for audio conversion in JavaScript.
- Updated Dockerfile to include FFmpeg for audio processing.
- Added tests for audio transcoding and WhatsApp Cloud service interactions.

* feat: enhance audio handling with transcoding support and error management

* feat: improve audio transcoding error handling and enhance audio recording features

* feat: enhance audio transcoding process and error handling for better reliability

* feat: update recorded audio handling to support boolean and array formats
2026-02-22 16:21:50 -03:00

79 lines
2.7 KiB
Ruby

class Audio::TranscodeService
SUPPORTED_FORMATS = { 'opus' => { codec: 'libopus', extension: 'ogg', content_type: 'audio/ogg' } }.freeze
def initialize(attachment, target_format, source_file: nil)
@attachment = attachment
@target_format = target_format
@source_file = source_file
end
def perform
validate_format!
return if already_in_target_format?
transcode_attachment
end
private
def already_in_target_format?
format_config = SUPPORTED_FORMATS[@target_format]
content_type = @attachment.file.content_type
return true if content_type == format_config[:content_type]
# Marcel may detect Opus-in-OGG as audio/opus; treat as already in target format
# when transcoding to Opus to avoid unnecessary re-transcoding
@target_format == 'opus' && content_type == 'audio/opus'
end
def validate_format!
return if SUPPORTED_FORMATS.key?(@target_format)
raise CustomExceptions::Audio::UnsupportedFormatError,
"Unsupported transcode format: #{@target_format}. Supported: #{SUPPORTED_FORMATS.keys.join(', ')}"
end
def transcode_attachment
format_config = SUPPORTED_FORMATS[@target_format]
input_file = nil
output_file = nil
input_file = download_to_tempfile
output_file = Tempfile.new(['transcoded', ".#{format_config[:extension]}"])
movie = FFMPEG::Movie.new(input_file.path)
raise CustomExceptions::Audio::TranscodingError, 'Invalid or unreadable audio file' unless movie.valid?
movie.transcode(output_file.path, audio_codec: format_config[:codec], custom: %w[-vn -map_metadata -1])
replace_attachment_file(output_file, format_config)
rescue FFMPEG::Error => e
raise CustomExceptions::Audio::TranscodingError, "FFmpeg transcoding failed: #{e.message}"
ensure
input_file&.close!
output_file&.close!
end
def download_to_tempfile
tempfile = Tempfile.new(['original_audio', File.extname(@attachment.file.filename.to_s)])
tempfile.binmode
if @source_file && (@source_file.respond_to?(:tempfile) || @source_file.respond_to?(:path))
source_path = @source_file.respond_to?(:tempfile) ? @source_file.tempfile.path : @source_file.path
IO.copy_stream(source_path, tempfile)
else
@attachment.file.blob.open { |file| IO.copy_stream(file, tempfile) }
end
tempfile.rewind
tempfile
end
def replace_attachment_file(output_file, format_config)
filename = "#{File.basename(@attachment.file.filename.to_s, '.*')}.#{format_config[:extension]}"
File.open(output_file.path, 'rb') do |file|
@attachment.file.attach(
io: file,
filename: filename,
content_type: format_config[:content_type]
)
end
@attachment.file_type = :audio
end
end