Class: CallRecordProcessing::TranscriptionService
- Inherits:
-
Object
- Object
- CallRecordProcessing::TranscriptionService
- Defined in:
- app/services/call_record_processing/transcription_service.rb
Overview
Service for transcribing call recordings using AssemblyAI with speaker diarization.
Audio files (.wav, .mp3, .aac, .oga) are transcribed and formatted with speaker labels.
Constant Summary collapse
- MIN_DURATION_SECONDS =
Minimum duration seconds.
30- MIN_DURATION_SECONDS_VOICEMAIL =
Minimum duration seconds voicemail.
5- VOICEMAIL_PLACEHOLDER_PREFIX =
Voicemail placeholder prefix.
'Voicemail from'- CALLER_IDENTITY_PROMPT =
Caller identity prompt.
<<~PROMPT For voicemail caller identity, extract only details the caller explicitly says or spells out. Reassemble spelled email addresses when the transcript provides enough letters. Return null for identity fields that are not mentioned. Add this field to the top-level JSON response: "caller_identity": { "person_name": "Full caller name if stated, otherwise null", "company_name": "Company name if stated, otherwise null", "caller_type": "homeowner|business|unknown", "email": "Email address if stated or spelled, otherwise null", "phone_numbers": ["Additional phone numbers mentioned, excluding the caller ID number"], "job_title": "Caller job title if stated, otherwise null" } PROMPT
- MAX_WAIT_TIME =
Maximum wait time for transcription (most calls are short)
600
Instance Attribute Summary collapse
-
#call_record ⇒ Object
readonly
10 minutes.
-
#options ⇒ Object
readonly
10 minutes.
Instance Method Summary collapse
-
#initialize(call_record, options = {}) ⇒ TranscriptionService
constructor
A new instance of TranscriptionService.
-
#process_completed_transcript(result) ⇒ Object
Process a completed transcript (called by webhook worker or sync mode) Formats the transcript, saves it, runs LeMUR analysis, and generates embeddings.
-
#run_call_analysis(transcript_id = nil) ⇒ Object
Run call analysis on the transcript using the LLM (summary, action items, etc.) This replaces the separate CallRecordSummaryWorker.
-
#transcribe(use_webhook: true) ⇒ Hash
Main transcription workflow - now uses webhooks by default Submit transcription to AssemblyAI and exit immediately.
Constructor Details
#initialize(call_record, options = {}) ⇒ TranscriptionService
Returns a new instance of TranscriptionService.
49 50 51 52 |
# File 'app/services/call_record_processing/transcription_service.rb', line 49 def initialize(call_record, = {}) @call_record = call_record @options = .symbolize_keys end |
Instance Attribute Details
#call_record ⇒ Object (readonly)
10 minutes
44 45 46 |
# File 'app/services/call_record_processing/transcription_service.rb', line 44 def call_record @call_record end |
#options ⇒ Object (readonly)
10 minutes
44 45 46 |
# File 'app/services/call_record_processing/transcription_service.rb', line 44 def @options end |
Instance Method Details
#process_completed_transcript(result) ⇒ Object
Process a completed transcript (called by webhook worker or sync mode)
Formats the transcript, saves it, runs LeMUR analysis, and generates embeddings
104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 |
# File 'app/services/call_record_processing/transcription_service.rb', line 104 def process_completed_transcript(result) # Format the transcript with speaker diarization # This sets @agent_speaker_label or @use_role_labels for proper speaker identification formatted = format_speaker_diarization(result) # Determine the agent speaker label to save # - For name-based: The agent's actual name (legacy Slam-1 speech_understanding) # - For role-based: 'Agent' (literal label from API) # - For legacy/Universal-3 Pro A/B diarization: detected label ('A' or 'B') # - For multichannel: nil (uses channel mapping instead) detected_agent_label = if @use_name_labels agent_name # Save the actual agent name elsif @use_role_labels 'Agent' elsif @agent_speaker_label.present? @agent_speaker_label end # Save transcription results first call_record.update!( transcript: formatted[:text], structured_transcript_json: result, transcription_state: :completed, transcribed_at: Time.current, assemblyai_transcript_id: result['id'], call_direction: detect_call_direction, agent_speaker_label: detected_agent_label ) Rails.logger.info "[CallRecordTranscription] Completed transcription for CallRecord #{call_record.id}" # Run call analysis (summary, action items, etc.) if transcription succeeded run_call_analysis(result['id']) if result['id'].present? return unless call_record.voicemail? append_transcription_to_voicemail_activity notify_voicemail_recipient end |
#run_call_analysis(transcript_id = nil) ⇒ Object
Run call analysis on the transcript using the LLM (summary, action items, etc.)
This replaces the separate CallRecordSummaryWorker.
Made public so it can be called independently for re-analysis.
147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 |
# File 'app/services/call_record_processing/transcription_service.rb', line 147 def run_call_analysis(transcript_id = nil) transcript_id ||= call_record.assemblyai_transcript_id return if transcript_id.blank? Rails.logger.info "[CallRecordTranscription] Running call analysis for transcript: #{transcript_id}" begin analysis = run_analysis_with_agent(transcript_id) # Update call record with analysis results call_record.update!( ai_summary: analysis['summary'], call_outcome: map_call_outcome(analysis['call_outcome']), customer_satisfaction: analysis['customer_satisfaction'], action_items: analysis['action_items'], call_phases: analysis['call_phases'], key_topics: analysis['key_topics'], agent_performance_score: analysis.dig('agent_performance', 'score'), summarized_at: Time.current, lemur_analyzed_at: Time.current ) Rails.logger.info "[CallRecordTranscription] Call analysis completed for CallRecord #{call_record.id}" # Honor the destination employee's process_voicemails opt-out — when # disabled we don't spin off Contacts or mutate the matched party. enrich_voicemail_customer(analysis['caller_identity']) if call_record.voicemail? && call_record.voicemail_processing_enabled? # Generate embedding with the enriched content (summary, action items, etc.) EmbeddingWorker.perform_async('CallRecord', call_record.id) Rails.logger.info "[CallRecordTranscription] Queued embedding generation for CallRecord #{call_record.id}" rescue StandardError => e # Don't fail the whole transcription if analysis fails Rails.logger.error "[CallRecordTranscription] Call analysis failed for CallRecord #{call_record.id}: #{e.}" Rails.logger.error e.backtrace.first(5).join("\n") end end |
#transcribe(use_webhook: true) ⇒ Hash
Main transcription workflow - now uses webhooks by default
Submit transcription to AssemblyAI and exit immediately.
When transcription completes, AssemblyAI calls our webhook which triggers
AssemblyaiCompletionWorker to process the result.
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
# File 'app/services/call_record_processing/transcription_service.rb', line 61 def transcribe(use_webhook: true) return skip_result(:already_transcribed) if already_transcribed? && !force? return skip_result(:too_short) if too_short? return skip_result(:no_audio) unless has_audio? begin mark_processing audio_url = get_audio_url unless audio_url # With `has_audio?` now checking for a fetchable file, this is only # reachable via get_audio_url's rescue — a transient storage failure. # Mark `error` (as the rescue below does) so the nightly reconciler's # pending/error scope re-queues it, rather than leaving it stuck in # `processing` where nothing reconciles it (AppSignal #1982). call_record.update!(transcription_state: :error) return error_result(:no_audio_url) end Rails.logger.info "[CallRecordTranscription] Starting transcription for CallRecord #{call_record.id}, audio: #{audio_url}" if use_webhook # Async mode: Submit and exit immediately, webhook will handle completion transcript_id = submit_transcription_with_webhook(audio_url) Rails.logger.info "[CallRecordTranscription] Submitted async transcription for CallRecord #{call_record.id}: #{transcript_id}" { status: :submitted, transcript_id: transcript_id, mode: :webhook } else # Sync mode: Poll for completion (legacy, slower but useful for testing) result = submit_and_poll_transcription(audio_url) process_completed_transcript(result) { status: :success, transcript_id: result['id'], word_count: call_record.transcript&.split&.size || 0 } end rescue StandardError => e Rails.logger.error "[CallRecordTranscription] Failed for CallRecord #{call_record.id}: #{e.}" call_record.update!(transcription_state: :error) error_result(:transcription_failed, e.) end end |