ruby-gemini-api 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,223 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "base64"
5
+
6
+ module Gemini
7
+ class Live
8
+ # Live API session manager
9
+ class Session
10
+ attr_reader :configuration, :last_resumption_token, :usage_metadata
11
+
12
+ def initialize(api_key:, configuration:)
13
+ @api_key = api_key
14
+ @configuration = configuration
15
+ @event_handlers = Hash.new { |h, k| h[k] = [] }
16
+ @connected = false
17
+ @setup_complete = false
18
+ @last_resumption_token = nil
19
+ @usage_metadata = nil
20
+ @connection = nil
21
+
22
+ setup_connection
23
+ end
24
+
25
+ # Register event handler
26
+ # Supported events:
27
+ # :setup_complete - Session setup completed
28
+ # :text - Text response received (text)
29
+ # :audio - Audio data received (base64_data, mime_type)
30
+ # :data - Other inline data received (base64_data, mime_type)
31
+ # :tool_call - Tool call requested (function_calls)
32
+ # :interrupted - User interrupted the model
33
+ # :turn_complete - Model turn completed
34
+ # :generation_complete - Generation completed
35
+ # :usage_metadata - Token usage info received (metadata)
36
+ # :session_resumption - Session resumption token updated (update)
37
+ # :go_away - Connection will close soon (info)
38
+ # :error - Error occurred (error)
39
+ # :close - Connection closed (code, reason)
40
+ def on(event, &block)
41
+ @event_handlers[event.to_sym] << block
42
+ self
43
+ end
44
+
45
+ # Send text message via clientContent.turns. This is the legacy form
46
+ # used by native-audio Live models. Newer models such as
47
+ # gemini-3.1-flash-live-preview reject this payload — use
48
+ # #send_realtime_text instead, which works on every Live model.
49
+ def send_text(text, turn_complete: true)
50
+ ensure_setup_complete!
51
+ message = MessageBuilder.client_content(
52
+ text: text,
53
+ turn_complete: turn_complete
54
+ )
55
+ @connection.send(message)
56
+ end
57
+
58
+ # Send text input via realtimeInput.text (universal form).
59
+ # Works with every currently-deployed Live model, including
60
+ # gemini-3.1-flash-live-preview and native-audio variants.
61
+ def send_realtime_text(text)
62
+ ensure_setup_complete!
63
+ @connection.send(MessageBuilder.realtime_text(text))
64
+ end
65
+
66
+ # Send audio data (Base64 encoded PCM)
67
+ def send_audio(audio_data, mime_type: "audio/pcm;rate=16000")
68
+ ensure_setup_complete!
69
+ encoded_data = audio_data.is_a?(String) && audio_data.encoding == Encoding::BINARY ?
70
+ Base64.strict_encode64(audio_data) : audio_data
71
+ message = MessageBuilder.realtime_input(
72
+ audio_data: encoded_data,
73
+ mime_type: mime_type
74
+ )
75
+ @connection.send(message)
76
+ end
77
+
78
+ # Send video/image data (Base64 encoded)
79
+ def send_video(image_data, mime_type: "image/jpeg")
80
+ ensure_setup_complete!
81
+ encoded_data = image_data.is_a?(String) && image_data.encoding == Encoding::BINARY ?
82
+ Base64.strict_encode64(image_data) : image_data
83
+ message = MessageBuilder.realtime_input(
84
+ video_data: encoded_data,
85
+ mime_type: mime_type
86
+ )
87
+ @connection.send(message)
88
+ end
89
+
90
+ # Send tool response
91
+ def send_tool_response(function_responses)
92
+ ensure_setup_complete!
93
+ message = MessageBuilder.tool_response(function_responses)
94
+ @connection.send(message)
95
+ end
96
+
97
+ # Manual VAD control - signal activity start
98
+ def activity_start
99
+ ensure_setup_complete!
100
+ @connection.send(MessageBuilder.activity_start)
101
+ end
102
+
103
+ # Manual VAD control - signal activity end
104
+ def activity_end
105
+ ensure_setup_complete!
106
+ @connection.send(MessageBuilder.activity_end)
107
+ end
108
+
109
+ # Close the session
110
+ def close
111
+ @connection&.close
112
+ @connected = false
113
+ @setup_complete = false
114
+ end
115
+
116
+ def connected?
117
+ @connected && @connection&.connected?
118
+ end
119
+
120
+ def setup_complete?
121
+ @setup_complete
122
+ end
123
+
124
+ private
125
+
126
+ def setup_connection
127
+ @connection = Connection.new(
128
+ api_key: @api_key,
129
+ on_message: method(:handle_message),
130
+ on_open: method(:handle_open),
131
+ on_error: method(:handle_error),
132
+ on_close: method(:handle_close)
133
+ )
134
+ @connection.connect
135
+ @connected = true
136
+ end
137
+
138
+ def handle_open
139
+ # Send setup message immediately after connection opens
140
+ setup_message = MessageBuilder.setup(@configuration)
141
+ @connection.send(setup_message)
142
+ end
143
+
144
+ def handle_message(data)
145
+ parsed = JSON.parse(data, symbolize_names: true)
146
+
147
+ if parsed[:setupComplete]
148
+ @setup_complete = true
149
+ emit(:setup_complete)
150
+ elsif parsed[:serverContent]
151
+ handle_server_content(parsed[:serverContent])
152
+ elsif parsed[:toolCall]
153
+ emit(:tool_call, parsed[:toolCall][:functionCalls])
154
+ elsif parsed[:usageMetadata]
155
+ @usage_metadata = parsed[:usageMetadata]
156
+ emit(:usage_metadata, parsed[:usageMetadata])
157
+ elsif parsed[:sessionResumptionUpdate]
158
+ handle_session_resumption(parsed[:sessionResumptionUpdate])
159
+ elsif parsed[:goAway]
160
+ emit(:go_away, parsed[:goAway])
161
+ end
162
+ rescue JSON::ParserError => e
163
+ emit(:error, e)
164
+ end
165
+
166
+ def handle_server_content(content)
167
+ # Check for interruption
168
+ if content[:interrupted]
169
+ emit(:interrupted)
170
+ return
171
+ end
172
+
173
+ # Check for generation complete
174
+ if content[:generationComplete]
175
+ emit(:generation_complete)
176
+ end
177
+
178
+ # Process model turn
179
+ model_turn = content[:modelTurn]
180
+ if model_turn
181
+ model_turn[:parts]&.each do |part|
182
+ if part[:text]
183
+ emit(:text, part[:text])
184
+ elsif part[:inlineData]
185
+ inline = part[:inlineData]
186
+ if inline[:mimeType]&.start_with?("audio/")
187
+ emit(:audio, inline[:data], inline[:mimeType])
188
+ else
189
+ emit(:data, inline[:data], inline[:mimeType])
190
+ end
191
+ end
192
+ end
193
+ end
194
+
195
+ # Check for turn complete
196
+ emit(:turn_complete) if content[:turnComplete]
197
+ end
198
+
199
+ def handle_session_resumption(update)
200
+ @last_resumption_token = update[:newHandle]
201
+ emit(:session_resumption, update)
202
+ end
203
+
204
+ def handle_error(error)
205
+ emit(:error, error)
206
+ end
207
+
208
+ def handle_close(code, reason)
209
+ @connected = false
210
+ @setup_complete = false
211
+ emit(:close, code, reason)
212
+ end
213
+
214
+ def emit(event, *args)
215
+ @event_handlers[event].each { |handler| handler.call(*args) }
216
+ end
217
+
218
+ def ensure_setup_complete!
219
+ raise Gemini::Error, "Session setup not complete. Wait for :setup_complete event." unless @setup_complete
220
+ end
221
+ end
222
+ end
223
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "live/configuration"
4
+ require_relative "live/message_builder"
5
+ require_relative "live/connection"
6
+ require_relative "live/session"
7
+
8
+ module Gemini
9
+ # Live API client for real-time audio/video/text interactions
10
+ #
11
+ # @example Basic text conversation
12
+ # client = Gemini::Client.new(api_key)
13
+ # session = client.live.connect(model: "gemini-2.5-flash-live-preview")
14
+ #
15
+ # session.on(:setup_complete) { puts "Connected!" }
16
+ # session.on(:text) { |text| puts "AI: #{text}" }
17
+ # session.on(:error) { |e| puts "Error: #{e}" }
18
+ #
19
+ # session.send_text("Hello!")
20
+ # sleep 5
21
+ # session.close
22
+ #
23
+ # @example Audio conversation
24
+ # session = client.live.connect(
25
+ # model: "gemini-2.5-flash-live-preview",
26
+ # response_modality: "AUDIO",
27
+ # voice_name: "Puck"
28
+ # )
29
+ #
30
+ # session.on(:audio) { |data, mime| play_audio(data) }
31
+ # session.send_audio(pcm_data) # 16-bit PCM, 16kHz, mono
32
+ #
33
+ # @example With block (auto-close)
34
+ # client.live.connect(model: "gemini-2.5-flash-live-preview") do |session|
35
+ # session.on(:text) { |text| puts text }
36
+ # session.send_text("Hello!")
37
+ # sleep 5
38
+ # end # session.close called automatically
39
+ #
40
+ class Live
41
+ def initialize(client:)
42
+ @client = client
43
+ end
44
+
45
+ # Establish a WebSocket connection and return a session
46
+ #
47
+ # @param model [String] Model to use (default: "gemini-2.5-flash-live-preview")
48
+ # @param response_modality [String] "TEXT" or "AUDIO" (default: "TEXT")
49
+ # @param voice_name [String] Voice for audio responses (Puck, Charon, Kore, etc.)
50
+ # @param system_instruction [String] System prompt
51
+ # @param tools [Array] Tool definitions for function calling
52
+ # @param context_window_compression [Hash] Compression settings for long sessions
53
+ # @param session_resumption [Hash] Session resumption settings
54
+ # @param automatic_activity_detection [Boolean] Enable/disable automatic VAD (default: true)
55
+ # @param media_resolution [String] Media resolution setting
56
+ # @param output_audio_transcription [Boolean] Enable audio transcription (default: false)
57
+ # @yield [session] If block given, yields the session and closes it when block returns
58
+ # @return [Gemini::Live::Session] The live session
59
+ #
60
+ def connect(
61
+ model: Configuration::DEFAULT_MODEL,
62
+ response_modality: "TEXT",
63
+ voice_name: nil,
64
+ system_instruction: nil,
65
+ tools: nil,
66
+ context_window_compression: nil,
67
+ session_resumption: nil,
68
+ automatic_activity_detection: true,
69
+ media_resolution: nil,
70
+ output_audio_transcription: false,
71
+ &block
72
+ )
73
+ config = Configuration.new(
74
+ model: model,
75
+ response_modality: response_modality,
76
+ voice_name: voice_name,
77
+ system_instruction: system_instruction,
78
+ tools: tools,
79
+ context_window_compression: context_window_compression,
80
+ session_resumption: session_resumption,
81
+ automatic_activity_detection: automatic_activity_detection,
82
+ media_resolution: media_resolution,
83
+ output_audio_transcription: output_audio_transcription
84
+ )
85
+
86
+ session = Session.new(
87
+ api_key: @client.api_key,
88
+ configuration: config
89
+ )
90
+
91
+ if block_given?
92
+ begin
93
+ yield session
94
+ ensure
95
+ session.close
96
+ end
97
+ else
98
+ session
99
+ end
100
+ end
101
+ end
102
+ end
@@ -41,9 +41,83 @@ module Gemini
41
41
  # Get image parts (if any)
42
42
  def image_parts
43
43
  return [] unless valid?
44
-
44
+
45
45
  parts.select { |part| part.key?("inline_data") && part["inline_data"]["mime_type"].start_with?("image/") }
46
46
  end
47
+
48
+ # Get the first audio inlineData part (TTS responses use camelCase "inlineData")
49
+ def audio_part
50
+ return nil unless valid?
51
+
52
+ parts.find do |part|
53
+ data_key = part["inlineData"] || part["inline_data"]
54
+ next false unless data_key
55
+ mt = data_key["mimeType"] || data_key["mime_type"]
56
+ mt.is_a?(String) && mt.start_with?("audio/")
57
+ end
58
+ end
59
+
60
+ # Base64-encoded audio data from a TTS response
61
+ def audio_data
62
+ part = audio_part
63
+ return nil unless part
64
+ data_key = part["inlineData"] || part["inline_data"]
65
+ data_key["data"]
66
+ end
67
+
68
+ # MIME type of the audio payload (e.g. "audio/L16;codec=pcm;rate=24000")
69
+ def audio_mime_type
70
+ part = audio_part
71
+ return nil unless part
72
+ data_key = part["inlineData"] || part["inline_data"]
73
+ data_key["mimeType"] || data_key["mime_type"]
74
+ end
75
+
76
+ # True if the response contains audio inlineData
77
+ def audio_response?
78
+ !audio_part.nil?
79
+ end
80
+
81
+ # Save audio to a file. PCM (L16) payloads are wrapped in a WAV header so
82
+ # the result is directly playable; other audio MIME types are written as-is.
83
+ # Returns the written file path or nil if no audio is present.
84
+ def save_audio(filepath)
85
+ data_b64 = audio_data
86
+ return nil unless data_b64
87
+
88
+ require 'base64'
89
+ raw = Base64.strict_decode64(data_b64)
90
+ mime = audio_mime_type.to_s
91
+
92
+ if mime.include?("L16") || mime.include?("pcm")
93
+ rate = mime[/rate=(\d+)/, 1]&.to_i || 24000
94
+ channels = 1
95
+ bits_per_sample = 16
96
+ byte_rate = rate * channels * bits_per_sample / 8
97
+ block_align = channels * bits_per_sample / 8
98
+ data_size = raw.bytesize
99
+
100
+ header = +""
101
+ header << "RIFF"
102
+ header << [36 + data_size].pack("V")
103
+ header << "WAVE"
104
+ header << "fmt "
105
+ header << [16].pack("V")
106
+ header << [1].pack("v")
107
+ header << [channels].pack("v")
108
+ header << [rate].pack("V")
109
+ header << [byte_rate].pack("V")
110
+ header << [block_align].pack("v")
111
+ header << [bits_per_sample].pack("v")
112
+ header << "data"
113
+ header << [data_size].pack("V")
114
+
115
+ File.binwrite(filepath, header + raw)
116
+ else
117
+ File.binwrite(filepath, raw)
118
+ end
119
+ filepath
120
+ end
47
121
 
48
122
  # Get all content with string representation
49
123
  def full_content
@@ -70,9 +144,50 @@ module Gemini
70
144
 
71
145
  # Check if response is valid
72
146
  def valid?
73
- !@raw_data.nil? &&
74
- ((@raw_data.key?("candidates") && !@raw_data["candidates"].empty?) ||
75
- (@raw_data.key?("predictions") && !@raw_data["predictions"].empty?))
147
+ !@raw_data.nil? &&
148
+ ((@raw_data.key?("candidates") && !@raw_data["candidates"].empty?) ||
149
+ (@raw_data.key?("predictions") && !@raw_data["predictions"].empty?) ||
150
+ embedding_response? ||
151
+ count_tokens_response?)
152
+ end
153
+
154
+ # Check if the raw response contains embedding data
155
+ def embedding_response?
156
+ return false if @raw_data.nil?
157
+ (@raw_data.key?("embedding") && !@raw_data["embedding"].nil?) ||
158
+ (@raw_data.key?("embeddings") && @raw_data["embeddings"].is_a?(Array) && !@raw_data["embeddings"].empty?)
159
+ end
160
+
161
+ # Get the embedding values as an Array of Floats.
162
+ # For single embedContent responses returns the values array.
163
+ # For batchEmbedContents responses returns the first embedding's values.
164
+ def embedding
165
+ return nil unless @raw_data
166
+ if @raw_data["embedding"].is_a?(Hash)
167
+ @raw_data["embedding"]["values"]
168
+ elsif @raw_data["embeddings"].is_a?(Array) && @raw_data["embeddings"].first.is_a?(Hash)
169
+ @raw_data["embeddings"].first["values"]
170
+ end
171
+ end
172
+
173
+ # Get all embedding value arrays for batch responses.
174
+ # Returns an Array of Arrays of Floats.
175
+ # For single embedContent responses, returns a single-element array.
176
+ def embeddings
177
+ return [] unless @raw_data
178
+ if @raw_data["embeddings"].is_a?(Array)
179
+ @raw_data["embeddings"].map { |e| e["values"] }.compact
180
+ elsif @raw_data["embedding"].is_a?(Hash) && @raw_data["embedding"]["values"]
181
+ [@raw_data["embedding"]["values"]]
182
+ else
183
+ []
184
+ end
185
+ end
186
+
187
+ # Get the dimensionality (length) of the first embedding vector
188
+ def embedding_dimension
189
+ values = embedding
190
+ values.is_a?(Array) ? values.length : 0
76
191
  end
77
192
 
78
193
  # Get error message if any
@@ -191,6 +306,28 @@ module Gemini
191
306
  def total_tokens
192
307
  usage&.dig("totalTokens") || 0
193
308
  end
309
+
310
+ # Check whether this response is a countTokens API result
311
+ def count_tokens_response?
312
+ !@raw_data.nil? && @raw_data.key?("totalTokens") &&
313
+ !@raw_data.key?("candidates") && !@raw_data.key?("predictions") &&
314
+ !embedding_response?
315
+ end
316
+
317
+ # Total tokens reported by the countTokens API (top-level totalTokens)
318
+ def count_tokens
319
+ @raw_data&.dig("totalTokens")
320
+ end
321
+
322
+ # Cached content token count reported by countTokens
323
+ def cached_content_token_count
324
+ @raw_data&.dig("cachedContentTokenCount") || 0
325
+ end
326
+
327
+ # Per-modality token breakdown reported by countTokens
328
+ def prompt_tokens_details
329
+ @raw_data&.dig("promptTokensDetails") || []
330
+ end
194
331
 
195
332
  # Process chunks for streaming responses
196
333
  def stream_chunks
@@ -0,0 +1,77 @@
1
+ module Gemini
2
+ class Tokens
3
+ DEFAULT_MODEL = "gemini-2.5-flash".freeze
4
+
5
+ def initialize(client:)
6
+ @client = client
7
+ end
8
+
9
+ # Count tokens for the given input.
10
+ #
11
+ # input: String, Array of parts/contents, or Hash. Optional when `contents:` is given.
12
+ # contents: full Array of Content objects (overrides input).
13
+ # system_instruction: String or Content hash.
14
+ # tools: Array of tool definitions (passed via generateContentRequest form).
15
+ # generation_config: Hash forwarded as generationConfig.
16
+ # cached_content: cachedContents/* resource name.
17
+ def count(input = nil, model: DEFAULT_MODEL, contents: nil, system_instruction: nil,
18
+ tools: nil, generation_config: nil, cached_content: nil, **parameters)
19
+ normalized_model = normalize_model(model)
20
+
21
+ payload = build_payload(
22
+ model: normalized_model,
23
+ input: input,
24
+ contents: contents,
25
+ system_instruction: system_instruction,
26
+ tools: tools,
27
+ generation_config: generation_config,
28
+ cached_content: cached_content
29
+ ).merge(parameters)
30
+
31
+ response = @client.json_post(
32
+ path: "models/#{normalized_model}:countTokens",
33
+ parameters: payload
34
+ )
35
+ Gemini::Response.new(response)
36
+ end
37
+
38
+ private
39
+
40
+ def build_payload(model:, input:, contents:, system_instruction:, tools:, generation_config:, cached_content:)
41
+ resolved_contents = contents || [format_content(input)]
42
+
43
+ # Use generateContentRequest form when extra request fields are present
44
+ if system_instruction || tools || generation_config || cached_content
45
+ # model is required inside the nested GenerateContentRequest
46
+ gc_request = { model: "models/#{model}", contents: resolved_contents }
47
+ gc_request[:systemInstruction] = format_content(system_instruction) if system_instruction
48
+ gc_request[:tools] = tools if tools
49
+ gc_request[:generationConfig] = generation_config if generation_config
50
+ gc_request[:cachedContent] = cached_content if cached_content
51
+ { generateContentRequest: gc_request }
52
+ else
53
+ { contents: resolved_contents }
54
+ end
55
+ end
56
+
57
+ def format_content(input)
58
+ case input
59
+ when nil
60
+ raise ArgumentError, "input or contents parameter is required"
61
+ when String
62
+ { parts: [{ text: input }] }
63
+ when Array
64
+ { parts: input.map { |part| part.is_a?(String) ? { text: part } : part } }
65
+ when Hash
66
+ input.key?(:parts) || input.key?("parts") ? input : { parts: [input] }
67
+ else
68
+ { parts: [{ text: input.to_s }] }
69
+ end
70
+ end
71
+
72
+ def normalize_model(model)
73
+ model_str = model.to_s
74
+ model_str.start_with?("models/") ? model_str.delete_prefix("models/") : model_str
75
+ end
76
+ end
77
+ end
data/lib/gemini/tts.rb ADDED
@@ -0,0 +1,83 @@
1
+ module Gemini
2
+ class TTS
3
+ DEFAULT_MODEL = "gemini-2.5-flash-preview-tts".freeze
4
+
5
+ # 30 prebuilt voice names available for the prebuiltVoiceConfig
6
+ VOICES = %w[
7
+ Zephyr Puck Charon Kore Fenrir Leda Orus Aoede Callirrhoe Autonoe
8
+ Enceladus Iapetus Umbriel Algieba Despina Erinome Algenib Rasalgethi
9
+ Laomedeia Achernar Alnilam Schedar Gacrux Pulcherrima Achird
10
+ Zubenelgenubi Vindemiatrix Sadachbia Sadaltager Sulafat
11
+ ].freeze
12
+
13
+ def initialize(client:)
14
+ @client = client
15
+ end
16
+
17
+ # Generate speech audio from text.
18
+ #
19
+ # text: prompt String (use style cues / bracket tags like [excited] for control,
20
+ # or "Speaker 1: ... Speaker 2: ..." for multi-speaker).
21
+ # voice: a single voice name (prebuiltVoiceConfig). Mutually exclusive with multi_speaker.
22
+ # multi_speaker: Array of { speaker:, voice: } Hashes for multi-speaker output.
23
+ # model: TTS preview model name. Defaults to gemini-2.5-flash-preview-tts.
24
+ # speech_config: raw speechConfig Hash override (skips voice/multi_speaker handling).
25
+ def generate(text, voice: nil, multi_speaker: nil, model: DEFAULT_MODEL,
26
+ speech_config: nil, **parameters)
27
+ raise ArgumentError, "text is required" if text.nil? || text.to_s.empty?
28
+ if voice && multi_speaker
29
+ raise ArgumentError, "voice and multi_speaker are mutually exclusive"
30
+ end
31
+
32
+ resolved_speech_config = speech_config || build_speech_config(voice: voice, multi_speaker: multi_speaker)
33
+ raise ArgumentError, "voice, multi_speaker, or speech_config is required" unless resolved_speech_config
34
+
35
+ payload = {
36
+ contents: [{ parts: [{ text: text }] }],
37
+ generationConfig: {
38
+ responseModalities: ["AUDIO"],
39
+ speechConfig: resolved_speech_config
40
+ }
41
+ }
42
+
43
+ payload.merge!(parameters) if parameters && !parameters.empty?
44
+
45
+ response = @client.json_post(
46
+ path: "models/#{normalize_model(model)}:generateContent",
47
+ parameters: payload
48
+ )
49
+ Gemini::Response.new(response)
50
+ end
51
+
52
+ private
53
+
54
+ def build_speech_config(voice:, multi_speaker:)
55
+ if multi_speaker
56
+ speaker_voice_configs = multi_speaker.map do |entry|
57
+ speaker = entry[:speaker] || entry["speaker"]
58
+ v = entry[:voice] || entry["voice"]
59
+ raise ArgumentError, "multi_speaker entries require :speaker and :voice" unless speaker && v
60
+ validate_voice!(v)
61
+ {
62
+ speaker: speaker,
63
+ voiceConfig: { prebuiltVoiceConfig: { voiceName: v } }
64
+ }
65
+ end
66
+ { multiSpeakerVoiceConfig: { speakerVoiceConfigs: speaker_voice_configs } }
67
+ elsif voice
68
+ validate_voice!(voice)
69
+ { voiceConfig: { prebuiltVoiceConfig: { voiceName: voice } } }
70
+ end
71
+ end
72
+
73
+ def validate_voice!(voice)
74
+ return if VOICES.include?(voice.to_s)
75
+ raise ArgumentError, "Unknown voice '#{voice}'. Available voices: #{VOICES.join(', ')}"
76
+ end
77
+
78
+ def normalize_model(model)
79
+ model_str = model.to_s
80
+ model_str.start_with?("models/") ? model_str.delete_prefix("models/") : model_str
81
+ end
82
+ end
83
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Gemini
4
- VERSION = "1.0.0"
4
+ VERSION = "1.2.0"
5
5
  end
data/lib/gemini.rb CHANGED
@@ -12,6 +12,8 @@ require_relative "gemini/threads"
12
12
  require_relative "gemini/messages"
13
13
  require_relative "gemini/runs"
14
14
  require_relative "gemini/embeddings"
15
+ require_relative "gemini/tokens"
16
+ require_relative "gemini/tts"
15
17
  require_relative "gemini/audio"
16
18
  require_relative "gemini/files"
17
19
  require_relative "gemini/images"
@@ -20,6 +22,7 @@ require_relative "gemini/function_calling_helper"
20
22
  require_relative "gemini/documents"
21
23
  require_relative "gemini/cached_content"
22
24
  require_relative "gemini/video"
25
+ require_relative "gemini/live"
23
26
 
24
27
  module Gemini
25
28
  class Error < StandardError; end