Module: RubyLLM::Protocols::ChatCompletions::Transcription

Defined in:
lib/ruby_llm/protocols/chat_completions/transcription.rb

Overview

Audio transcription methods for the OpenAI API integration

Class Method Summary collapse

Class Method Details

.build_streamed_transcription(chunks, model:) ⇒ Object



88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/ruby_llm/protocols/chat_completions/transcription.rb', line 88

def build_streamed_transcription(chunks, model:)
  final = chunks.reverse.find(&:done?)
  data = final&.raw || {}
  usage = data['usage'] || {}

  RubyLLM::Transcription.new(
    text: final&.text || streamed_transcript_text(chunks),
    model: model,
    language: data['language'],
    duration: transcription_duration(usage),
    segments: streamed_transcription_segments(chunks, data),
    reported_cost: reported_cost(usage),
    **transcription_tokens(usage)
  )
end

.build_transcription_chunk(data) ⇒ Object



76
77
78
79
80
81
82
83
84
85
86
# File 'lib/ruby_llm/protocols/chat_completions/transcription.rb', line 76

def build_transcription_chunk(data)
  type = data['type']

  RubyLLM::TranscriptionChunk.new(
    type: type,
    delta: data['delta'],
    text: (data['text'] if type == RubyLLM::TranscriptionChunk::DONE),
    segment: (data.except('type') if type == RubyLLM::TranscriptionChunk::SEGMENT),
    raw: data
  )
end

.default_response_format(model) ⇒ Object

Diarization models return plain text with no segments unless the response format asks for them.



57
58
59
# File 'lib/ruby_llm/protocols/chat_completions/transcription.rb', line 57

def default_response_format(model)
  'diarized_json' if model.include?('diarize')
end

.encode_speaker_references(references) ⇒ Object



43
44
45
46
47
48
49
# File 'lib/ruby_llm/protocols/chat_completions/transcription.rb', line 43

def encode_speaker_references(references)
  return nil unless references

  references.map do |ref|
    Attachment.new(ref, config: @config).for_llm
  end
end

.parse_transcription_response(response, model:) ⇒ Object



118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/ruby_llm/protocols/chat_completions/transcription.rb', line 118

def parse_transcription_response(response, model:)
  data = response.body

  return RubyLLM::Transcription.new(text: data, model: model) if data.is_a?(String)

  usage = data['usage'] || {}

  RubyLLM::Transcription.new(
    text: data['text'],
    model: model,
    language: data['language'],
    duration: data['duration'] || transcription_duration(usage),
    segments: data['segments'],
    words: data['words'],
    reported_cost: reported_cost(usage),
    **transcription_tokens(usage)
  )
end

.render_transcription_options(timestamps:, format:, streaming:) ⇒ Object



14
15
16
17
18
19
20
21
22
23
24
25
26
# File 'lib/ruby_llm/protocols/chat_completions/transcription.rb', line 14

def render_transcription_options(timestamps:, format:, streaming:)
  return {} if timestamps.nil?

  values = Array(timestamps).map(&:to_s)
  unless values.any? && (values - %w[word segment]).empty?
    raise ArgumentError, 'Transcription timestamps must be word or segment'
  end
  if streaming || (format && format != 'verbose_json')
    raise ArgumentError, 'Transcription timestamps require a non-streaming verbose_json response'
  end

  { response_format: 'verbose_json', timestamp_granularities: values }
end

.render_transcription_payload(file_part, model:, language:, format: nil, speaker_names: nil, speaker_references: nil, provider_options: {}, prompt: nil, temperature: nil) ⇒ Object



28
29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/ruby_llm/protocols/chat_completions/transcription.rb', line 28

def render_transcription_payload(file_part, model:, language:, format: nil, speaker_names: nil,
                                 speaker_references: nil, provider_options: {}, prompt: nil,
                                 temperature: nil)
  {
    model: model,
    file: file_part,
    language: language,
    response_format: format || default_response_format(model),
    prompt: prompt,
    temperature: temperature,
    known_speaker_names: speaker_names,
    known_speaker_references: encode_speaker_references(speaker_references)
  }.compact.merge(provider_options)
end

.reported_cost(_usage) ⇒ Object



51
52
53
# File 'lib/ruby_llm/protocols/chat_completions/transcription.rb', line 51

def reported_cost(_usage)
  nil
end

.stream_transcription(payload, model:, &block) ⇒ Object

OpenAI streams transcriptions as server-sent events carrying text deltas, completed segments on diarization models, and a final event with the whole transcript and its usage.



64
65
66
67
68
69
70
71
72
73
74
# File 'lib/ruby_llm/protocols/chat_completions/transcription.rb', line 64

def stream_transcription(payload, model:, &block)
  chunks = []

  stream_events(transcription_url, payload.merge(stream: 'true')) do |data|
    chunk = build_transcription_chunk(data)
    chunks << chunk
    block.call chunk
  end

  build_streamed_transcription(chunks, model: model)
end

.streamed_transcript_text(chunks) ⇒ Object

Diarization models stream segments instead of deltas, so the transcript is rebuilt from whichever the provider sent.



106
107
108
109
110
111
# File 'lib/ruby_llm/protocols/chat_completions/transcription.rb', line 106

def streamed_transcript_text(chunks)
  deltas = chunks.filter_map(&:delta)
  return deltas.join if deltas.any?

  chunks.filter_map { |chunk| chunk.segment&.fetch('text', nil) }.join(' ')
end

.streamed_transcription_segments(chunks, data) ⇒ Object



113
114
115
116
# File 'lib/ruby_llm/protocols/chat_completions/transcription.rb', line 113

def streamed_transcription_segments(chunks, data)
  segments = data['segments'] || chunks.filter_map(&:segment)
  segments.empty? ? nil : segments
end

.transcription_duration(usage) ⇒ Object



144
145
146
# File 'lib/ruby_llm/protocols/chat_completions/transcription.rb', line 144

def transcription_duration(usage)
  usage['seconds']
end

.transcription_tokens(usage) ⇒ Object



137
138
139
140
141
142
# File 'lib/ruby_llm/protocols/chat_completions/transcription.rb', line 137

def transcription_tokens(usage)
  {
    input_tokens: usage['input_tokens'] || usage['prompt_tokens'],
    output_tokens: usage['output_tokens'] || usage['completion_tokens']
  }
end

.transcription_urlObject



10
11
12
# File 'lib/ruby_llm/protocols/chat_completions/transcription.rb', line 10

def transcription_url
  'audio/transcriptions'
end