chore: transcription recording now working
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Twilio::RecordingController < ActionController::Base
|
||||
skip_forgery_protection
|
||||
|
||||
# POST /twilio/recording_callback
|
||||
# This endpoint is called by Twilio when a call recording is available
|
||||
def recording_callback
|
||||
conference_sid = params['conference_sid']
|
||||
call_sid = params['CallSid']
|
||||
recording_url = params['RecordingUrl']
|
||||
recording_sid = params['RecordingSid']
|
||||
account_id = params['account_id']
|
||||
Rails.logger.info("[Twilio::RecordingController] Incoming recording_callback with params: #{params.inspect}")
|
||||
unless recording_url && account_id && (conference_sid || call_sid)
|
||||
Rails.logger.warn("[Twilio::RecordingController] Missing required params. recording_url: #{recording_url}, account_id: #{account_id}, conference_sid: #{conference_sid}, call_sid: #{call_sid}")
|
||||
return head :bad_request
|
||||
end
|
||||
|
||||
# Find the account
|
||||
account = Account.find_by(id: account_id)
|
||||
unless account
|
||||
Rails.logger.warn("[Twilio::RecordingController] Account not found for id: #{account_id}")
|
||||
return head :not_found
|
||||
end
|
||||
|
||||
# Prefer lookup by conference_sid (most robust for conference recordings)
|
||||
conversation = if conference_sid
|
||||
account.conversations.find_by("additional_attributes ->> 'conference_sid' = ?", conference_sid)
|
||||
elsif call_sid
|
||||
account.conversations.find_by("additional_attributes ->> 'call_sid' = ?", call_sid)
|
||||
end
|
||||
unless conversation
|
||||
Rails.logger.warn("[Twilio::RecordingController] Conversation not found for conference_sid: #{conference_sid} or call_sid: #{call_sid}")
|
||||
return head :not_found
|
||||
end
|
||||
|
||||
# Find the original voice call message (should be unique per conference)
|
||||
message = conversation.messages.voice_call.order(:created_at).first
|
||||
unless message
|
||||
Rails.logger.warn("[Twilio::RecordingController] No voice_call message found in conversation_id: #{conversation.id}")
|
||||
return head :not_found
|
||||
end
|
||||
|
||||
# Download the recording from Twilio
|
||||
begin
|
||||
Rails.logger.info("[Twilio::RecordingController] Downloading recording from: #{recording_url}.mp3")
|
||||
file = URI.open(recording_url + '.mp3')
|
||||
rescue => e
|
||||
Rails.logger.error("[Twilio::RecordingController] Failed to download recording: #{e.message}")
|
||||
return head :internal_server_error
|
||||
end
|
||||
|
||||
# Attach the audio file to the message as an audio attachment
|
||||
begin
|
||||
att = message.attachments.create!(
|
||||
account_id: account.id,
|
||||
file: {
|
||||
io: file,
|
||||
filename: "twilio_recording_#{recording_sid}.mp3",
|
||||
content_type: 'audio/mpeg'
|
||||
},
|
||||
file_type: :audio,
|
||||
external_url: recording_url + '.mp3',
|
||||
meta: { recording_sid: recording_sid, conference_sid: conference_sid, call_sid: call_sid }
|
||||
)
|
||||
Rails.logger.info("[Twilio::RecordingController] Successfully attached recording to message_id: #{message.id}, attachment_id: #{att.id}")
|
||||
rescue => e
|
||||
Rails.logger.error("[Twilio::RecordingController] Failed to attach recording: #{e.message}")
|
||||
return head :internal_server_error
|
||||
end
|
||||
|
||||
# Optionally, update message content_attributes to indicate recording is attached
|
||||
content_attributes = message.content_attributes || {}
|
||||
content_attributes['recording_attached'] = true
|
||||
content_attributes['conference_sid'] = conference_sid if conference_sid
|
||||
message.update!(content_attributes: content_attributes)
|
||||
|
||||
head :ok
|
||||
end
|
||||
end
|
||||
@@ -86,7 +86,10 @@ class Twilio::VoiceController < ActionController::Base
|
||||
statusCallback: conference_callback_url,
|
||||
statusCallbackMethod: 'POST',
|
||||
statusCallbackEvent: 'start end join leave',
|
||||
participantLabel: "caller-#{@call_sid.last(8)}"
|
||||
participantLabel: "caller-#{@call_sid.last(8)}",
|
||||
record: 'record-from-start',
|
||||
recording_status_callback: "#{base_url}/twilio/recording_callback?account_id=#{@inbox.account_id}&conference_sid=#{conference_name}",
|
||||
recording_status_callback_method: 'POST'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -25,12 +25,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<audio
|
||||
ref="audioPlayer"
|
||||
:src="recordingUrl"
|
||||
preload="metadata"
|
||||
@ended="handlePlaybackEnd"
|
||||
/>
|
||||
<template v-if="hasAudioAttachment && recordingUrl">
|
||||
<div class="w-full m-0 p-1 min-w-[260px]">
|
||||
<audio
|
||||
ref="audioPlayer"
|
||||
:src="recordingUrl"
|
||||
preload="metadata"
|
||||
@ended="handlePlaybackEnd"
|
||||
controls
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -60,6 +66,7 @@ export default {
|
||||
isAnimating: false,
|
||||
recordingUrl: '',
|
||||
isPlaying: false,
|
||||
hasAudioAttachment: false,
|
||||
};
|
||||
},
|
||||
setup(props) {
|
||||
@@ -300,12 +307,14 @@ export default {
|
||||
message: {
|
||||
handler() {
|
||||
this.setupVoiceCall();
|
||||
this.setAudioAttachment();
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.setupVoiceCall();
|
||||
this.setAudioAttachment();
|
||||
},
|
||||
beforeUnmount() {
|
||||
// Clean up all intervals to prevent memory leaks
|
||||
@@ -398,6 +407,39 @@ export default {
|
||||
},
|
||||
handlePlaybackEnd() {
|
||||
this.isPlaying = false;
|
||||
},
|
||||
setAudioAttachment() {
|
||||
// Look for audio attachment in message.attachments or message.contentAttributes.attachments
|
||||
let attachments = [];
|
||||
if (this.message?.attachments && Array.isArray(this.message.attachments)) {
|
||||
attachments = this.message.attachments;
|
||||
} else if (this.message?.contentAttributes?.attachments && Array.isArray(this.message.contentAttributes.attachments)) {
|
||||
attachments = this.message.contentAttributes.attachments;
|
||||
}
|
||||
// Find the first audio attachment, supporting both camelCase and snake_case fields
|
||||
const audio = attachments.find(att => {
|
||||
if (!att) return false;
|
||||
// Check file_type or fileType
|
||||
if ((att.file_type && att.file_type.startsWith('audio')) ||
|
||||
(att.fileType && att.fileType.startsWith('audio'))) return true;
|
||||
// Check content_type or contentType
|
||||
if ((att.content_type && att.content_type.startsWith('audio')) ||
|
||||
(att.contentType && att.contentType.startsWith('audio'))) return true;
|
||||
// Check data_url or dataUrl
|
||||
if ((att.data_url && att.data_url.match(/\.(mp3|wav|ogg|m4a)$/i)) ||
|
||||
(att.dataUrl && att.dataUrl.match(/\.(mp3|wav|ogg|m4a)$/i))) return true;
|
||||
// Check file_url or fileUrl
|
||||
if ((att.file_url && att.file_url.match(/\.(mp3|wav|ogg|m4a)$/i)) ||
|
||||
(att.fileUrl && att.fileUrl.match(/\.(mp3|wav|ogg|m4a)$/i))) return true;
|
||||
return false;
|
||||
});
|
||||
if (audio) {
|
||||
this.recordingUrl = audio.data_url || audio.file_url || audio.dataUrl || audio.fileUrl || '';
|
||||
this.hasAudioAttachment = true;
|
||||
} else {
|
||||
this.recordingUrl = '';
|
||||
this.hasAudioAttachment = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -243,7 +243,10 @@ module Voice
|
||||
statusCallback: conference_callback_url,
|
||||
statusCallbackMethod: 'POST',
|
||||
statusCallbackEvent: 'start end join leave',
|
||||
participantLabel: "caller-#{caller_info[:call_sid].last(8)}"
|
||||
participantLabel: "caller-#{caller_info[:call_sid].last(8)}",
|
||||
record: 'record-from-start',
|
||||
recording_status_callback: "#{base_url}/twilio/recording_callback?account_id=#{account.id}&conference_sid=#{conference_name}",
|
||||
recording_status_callback_method: 'POST'
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
@@ -513,6 +513,9 @@ Rails.application.routes.draw do
|
||||
# Transcription webhook
|
||||
post :transcription_callback, to: 'transcription#transcription_callback'
|
||||
|
||||
# Recording webhook
|
||||
post :recording_callback, to: 'recording#recording_callback'
|
||||
|
||||
# Use resource scope to avoid plural/singular confusion
|
||||
resource :voice, only: [], controller: 'voice' do
|
||||
collection do
|
||||
|
||||
Reference in New Issue
Block a user