runapi-gemini-omni 0.3.5 → 0.4.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.
- checksums.yaml +4 -4
- data/README.md +13 -5
- data/lib/runapi/gemini_omni/client.rb +1 -1
- data/lib/runapi/gemini_omni/resources/create_audio.rb +0 -20
- data/lib/runapi/gemini_omni/resources/create_character.rb +3 -28
- data/lib/runapi/gemini_omni/resources/text_to_video.rb +0 -63
- data/lib/runapi/gemini_omni/types.rb +2 -5
- data/lib/runapi/gemini_omni.rb +0 -1
- metadata +4 -5
- data/lib/runapi/gemini_omni/contract_gen.rb +0 -115
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 315a15e319d647d39aeaf7e8f75e0831c1255cff05158eaf0beaed5903974f39
|
|
4
|
+
data.tar.gz: a0c715ee79057f4bde550e49b601f27a75ef2be243bbcc7ec7694c9cdd4b270a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 23dc3858605a2cba240eac1ae302287f601d9d21e3f154cf3431c7aa8cd533353e526a79e5ba5addfbbbc918b9fd4852dac4776b7c58e0eede1a4bbdf8782854
|
|
7
|
+
data.tar.gz: 465fb2003c592eeb00640d1335de475ae147bb39904c062100fb412645d0266d37ecc34e3b850a22263cac84dacb5d9a80f40ce03f7beaf7964a8e8d75ea29c4
|
data/README.md
CHANGED
|
@@ -21,17 +21,25 @@ voice = client.create_audio.run(
|
|
|
21
21
|
|
|
22
22
|
character = client.create_character.run(
|
|
23
23
|
descriptions: "A silver-haired cyberpunk guide",
|
|
24
|
-
reference_image_url: "https://cdn.runapi.ai/public/samples/
|
|
24
|
+
reference_image_url: "https://cdn.runapi.ai/public/samples/portrait.jpg",
|
|
25
|
+
body_reference_image_url: "https://cdn.runapi.ai/public/samples/image.jpg"
|
|
25
26
|
)
|
|
26
27
|
|
|
27
28
|
video = client.text_to_video.run(
|
|
28
|
-
model: "gemini-omni-flash-
|
|
29
|
-
prompt: "A paper airplane
|
|
29
|
+
model: "gemini-omni-flash-1-1",
|
|
30
|
+
prompt: "A paper airplane travels from dawn into dusk.",
|
|
31
|
+
duration_seconds: 6,
|
|
32
|
+
first_frame_image_url: "https://cdn.runapi.ai/public/samples/first-frame.jpg",
|
|
33
|
+
last_frame_image_url: "https://cdn.runapi.ai/public/samples/last-frame.jpg",
|
|
30
34
|
aspect_ratio: "16:9",
|
|
31
|
-
output_resolution: "
|
|
35
|
+
output_resolution: "360p"
|
|
32
36
|
)
|
|
33
37
|
```
|
|
34
38
|
|
|
39
|
+
`character.images` returns the portrait first and the optional full-body image second. A character created with both references consumes two of the seven reference units in a multimodal video request.
|
|
40
|
+
|
|
41
|
+
For `gemini-omni-flash-1-1`, `first_frame_image_url` cannot be combined with reference images, audio IDs, video clips, or character IDs. `last_frame_image_url` requires `first_frame_image_url`.
|
|
42
|
+
|
|
35
43
|
RunAPI-generated file URLs are temporary. Download and store generated images, videos, audio, or other files in your own durable storage within 7 days; do not treat returned URLs as long-term assets.
|
|
36
44
|
|
|
37
45
|
## Links
|
|
@@ -39,8 +47,8 @@ RunAPI-generated file URLs are temporary. Download and store generated images, v
|
|
|
39
47
|
- Model page: https://runapi.ai/models/gemini-omni
|
|
40
48
|
- SDK docs: https://runapi.ai/docs/resources/sdks
|
|
41
49
|
- Product docs: https://runapi.ai/docs/api/gemini-omni/text-to-video
|
|
50
|
+
- Flash 1.1 pricing and rate limits: https://runapi.ai/models/gemini-omni/flash-1-1
|
|
42
51
|
- Flash Preview pricing and rate limits: https://runapi.ai/models/gemini-omni/flash-preview
|
|
43
|
-
- Provider comparison: https://runapi.ai/providers/google
|
|
44
52
|
- Full catalog: https://runapi.ai/models
|
|
45
53
|
|
|
46
54
|
## License
|
|
@@ -18,7 +18,7 @@ module RunApi
|
|
|
18
18
|
class Client < RunApi::Core::Client
|
|
19
19
|
# @return [Resources::CreateAudio] Registers reusable voice presets (synchronous).
|
|
20
20
|
attr_reader :create_audio
|
|
21
|
-
# @return [Resources::CreateCharacter] Builds reusable characters
|
|
21
|
+
# @return [Resources::CreateCharacter] Builds reusable characters and follows accepted Tasks to completion.
|
|
22
22
|
attr_reader :create_character
|
|
23
23
|
# @return [Resources::TextToVideo] Generates video from prompts with optional characters and voices (async).
|
|
24
24
|
attr_reader :text_to_video
|
|
@@ -11,9 +11,6 @@ module RunApi
|
|
|
11
11
|
ENDPOINT = "/api/v1/gemini_omni/create_audio"
|
|
12
12
|
RESPONSE_CLASS = Types::CreateAudioResponse
|
|
13
13
|
MODEL = "gemini-omni-audio"
|
|
14
|
-
NAME_MAX_LENGTH = 210
|
|
15
|
-
VOICE_DESCRIPTION_MAX_LENGTH = 20_000
|
|
16
|
-
EXAMPLE_DIALOGUE_MAX_LENGTH = 120
|
|
17
14
|
|
|
18
15
|
def initialize(http)
|
|
19
16
|
@http = http
|
|
@@ -21,25 +18,8 @@ module RunApi
|
|
|
21
18
|
|
|
22
19
|
def run(options: nil, **params)
|
|
23
20
|
params = compact_params(params)
|
|
24
|
-
validate_params!(params)
|
|
25
21
|
request(:post, ENDPOINT, body: params, options: options)
|
|
26
22
|
end
|
|
27
|
-
|
|
28
|
-
private
|
|
29
|
-
|
|
30
|
-
def validate_params!(params)
|
|
31
|
-
validate_contract!(CONTRACT["create-audio"], params.merge(model: MODEL))
|
|
32
|
-
validate_length!(params, :name, NAME_MAX_LENGTH)
|
|
33
|
-
validate_length!(params, :voice_description, VOICE_DESCRIPTION_MAX_LENGTH)
|
|
34
|
-
validate_length!(params, :example_dialogue, EXAMPLE_DIALOGUE_MAX_LENGTH)
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def validate_length!(params, key, max_length)
|
|
38
|
-
value = param(params, key)
|
|
39
|
-
return if value.nil? || value.to_s.length <= max_length
|
|
40
|
-
|
|
41
|
-
raise Core::ValidationError, "#{key} must be at most #{max_length} characters"
|
|
42
|
-
end
|
|
43
23
|
end
|
|
44
24
|
end
|
|
45
25
|
end
|
|
@@ -3,17 +3,15 @@
|
|
|
3
3
|
module RunApi
|
|
4
4
|
module GeminiOmni
|
|
5
5
|
module Resources
|
|
6
|
-
# Builds a reusable character from a reference
|
|
6
|
+
# Builds a reusable character from a portrait, optional full-body reference, and description.
|
|
7
7
|
# Attach audio IDs to give the character a specific voice.
|
|
8
|
-
#
|
|
8
|
+
# +run+ returns the terminal character result, following an accepted Task when needed.
|
|
9
9
|
class CreateCharacter
|
|
10
10
|
include RunApi::Core::ResourceHelpers
|
|
11
11
|
|
|
12
12
|
ENDPOINT = "/api/v1/gemini_omni/create_character"
|
|
13
13
|
RESPONSE_CLASS = Types::CreateCharacterResponse
|
|
14
14
|
MODEL = "gemini-omni-character"
|
|
15
|
-
DESCRIPTIONS_MAX_LENGTH = 20_000
|
|
16
|
-
CHARACTER_NAME_MAX_LENGTH = 210
|
|
17
15
|
|
|
18
16
|
def initialize(http)
|
|
19
17
|
@http = http
|
|
@@ -21,30 +19,7 @@ module RunApi
|
|
|
21
19
|
|
|
22
20
|
def run(options: nil, **params)
|
|
23
21
|
params = compact_params(params)
|
|
24
|
-
|
|
25
|
-
request(:post, ENDPOINT, body: params, options: options)
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
private
|
|
29
|
-
|
|
30
|
-
def validate_params!(params)
|
|
31
|
-
validate_contract!(CONTRACT["create-character"], params.merge(model: MODEL))
|
|
32
|
-
validate_array!(params, :audio_ids) if param(params, :audio_ids)
|
|
33
|
-
validate_length!(params, :descriptions, DESCRIPTIONS_MAX_LENGTH)
|
|
34
|
-
validate_length!(params, :character_name, CHARACTER_NAME_MAX_LENGTH)
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def validate_array!(params, key)
|
|
38
|
-
return if param(params, key).is_a?(Array)
|
|
39
|
-
|
|
40
|
-
raise Core::ValidationError, "#{key} must be an array"
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def validate_length!(params, key, max_length)
|
|
44
|
-
value = param(params, key)
|
|
45
|
-
return if value.nil? || value.to_s.length <= max_length
|
|
46
|
-
|
|
47
|
-
raise Core::ValidationError, "#{key} must be at most #{max_length} characters"
|
|
22
|
+
run_hybrid(ENDPOINT, body: params, options: options, response_class: RESPONSE_CLASS)
|
|
48
23
|
end
|
|
49
24
|
end
|
|
50
25
|
end
|
|
@@ -13,10 +13,6 @@ module RunApi
|
|
|
13
13
|
RESPONSE_CLASS = Types::TextToVideoResponse
|
|
14
14
|
COMPLETED_RESPONSE_CLASS = Types::CompletedTextToVideoResponse
|
|
15
15
|
DEFAULT_MODEL = "gemini-omni-text-to-video"
|
|
16
|
-
PROMPT_MAX_LENGTH = 20_000
|
|
17
|
-
REFERENCE_UNITS_MAX = 7
|
|
18
|
-
VIDEO_REFERENCE_UNITS = 2
|
|
19
|
-
MAX_TRIM_SECONDS = 10
|
|
20
16
|
|
|
21
17
|
def initialize(http)
|
|
22
18
|
@http = http
|
|
@@ -29,71 +25,12 @@ module RunApi
|
|
|
29
25
|
|
|
30
26
|
def create(options: nil, **params)
|
|
31
27
|
params = compact_params(params)
|
|
32
|
-
validate_params!(params)
|
|
33
28
|
request(:post, ENDPOINT, body: params, options: options)
|
|
34
29
|
end
|
|
35
30
|
|
|
36
31
|
def get(id, options: nil)
|
|
37
32
|
request(:get, "#{ENDPOINT}/#{id}", options: options)
|
|
38
33
|
end
|
|
39
|
-
|
|
40
|
-
private
|
|
41
|
-
|
|
42
|
-
def validate_params!(params)
|
|
43
|
-
selected_model = param(params, :model)
|
|
44
|
-
selected_model = DEFAULT_MODEL if selected_model.nil? || selected_model.to_s.empty?
|
|
45
|
-
validate_contract!(CONTRACT["text-to-video"], params.merge(model: selected_model))
|
|
46
|
-
validate_length!(params, :prompt, PROMPT_MAX_LENGTH)
|
|
47
|
-
validate_video_list!(param(params, :video_list)) if param(params, :video_list)
|
|
48
|
-
validate_reference_units!(params)
|
|
49
|
-
validate_seed!(params)
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def validate_length!(params, key, max_length)
|
|
53
|
-
value = param(params, key)
|
|
54
|
-
return if value.nil? || value.to_s.length <= max_length
|
|
55
|
-
|
|
56
|
-
raise Core::ValidationError, "#{key} must be at most #{max_length} characters"
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def validate_video_list!(items)
|
|
60
|
-
items.each_with_index do |item, index|
|
|
61
|
-
url = param(item, :url)
|
|
62
|
-
raise Core::ValidationError, "video_list[#{index}].url is required" if url.nil? || url.to_s.empty?
|
|
63
|
-
|
|
64
|
-
start_time = numeric_param(item, :start)
|
|
65
|
-
end_time = numeric_param(item, :ends)
|
|
66
|
-
raise Core::ValidationError, "video_list[#{index}] start and ends must be numbers" unless start_time && end_time
|
|
67
|
-
raise Core::ValidationError, "video_list[#{index}].start must be 0 or greater" if start_time.negative?
|
|
68
|
-
raise Core::ValidationError, "video_list[#{index}].ends must be greater than start" unless end_time > start_time
|
|
69
|
-
if (end_time - start_time) > MAX_TRIM_SECONDS
|
|
70
|
-
raise Core::ValidationError, "video_list[#{index}] trim range must be #{MAX_TRIM_SECONDS} seconds or less"
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def validate_reference_units!(params)
|
|
76
|
-
units = Array(param(params, :reference_image_urls)).count +
|
|
77
|
-
(Array(param(params, :video_list)).count * VIDEO_REFERENCE_UNITS) +
|
|
78
|
-
Array(param(params, :character_ids)).count
|
|
79
|
-
return if units <= REFERENCE_UNITS_MAX
|
|
80
|
-
|
|
81
|
-
raise Core::ValidationError, "reference_image_urls + video_list*2 + character_ids must use #{REFERENCE_UNITS_MAX} reference units or fewer"
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def validate_seed!(params)
|
|
85
|
-
value = param(params, :seed)
|
|
86
|
-
return if value.nil?
|
|
87
|
-
|
|
88
|
-
seed = Integer(value, exception: false)
|
|
89
|
-
return if seed && Types::SEED_RANGE.cover?(seed)
|
|
90
|
-
|
|
91
|
-
raise Core::ValidationError, "seed must be an integer between #{Types::SEED_RANGE.min} and #{Types::SEED_RANGE.max}"
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def numeric_param(params, key)
|
|
95
|
-
Float(param(params, key), exception: false)
|
|
96
|
-
end
|
|
97
34
|
end
|
|
98
35
|
end
|
|
99
36
|
end
|
|
@@ -30,23 +30,20 @@ module RunApi
|
|
|
30
30
|
optional :url, String
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
# A created character
|
|
33
|
+
# A created character whose images contain the portrait first and optional full-body image second.
|
|
34
34
|
class Character < RunApi::Core::BaseModel
|
|
35
35
|
required :id, String
|
|
36
36
|
optional :name, String
|
|
37
37
|
optional :images, [-> { Image }]
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
-
#
|
|
40
|
+
# Terminal result of a create-character call.
|
|
41
41
|
class CreateCharacterResponse < RunApi::Core::TaskResponse
|
|
42
42
|
required :id, String
|
|
43
43
|
optional :character, -> { Character }
|
|
44
44
|
optional :error, String
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
-
# Valid seed range for reproducible generation.
|
|
48
|
-
SEED_RANGE = (0..2_147_483_647)
|
|
49
|
-
|
|
50
47
|
# URL to a generated video file.
|
|
51
48
|
class Video < RunApi::Core::BaseModel
|
|
52
49
|
optional :url, String
|
data/lib/runapi/gemini_omni.rb
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require "runapi/core"
|
|
4
4
|
require_relative "gemini_omni/types"
|
|
5
|
-
require_relative "gemini_omni/contract_gen"
|
|
6
5
|
require_relative "gemini_omni/resources/create_audio"
|
|
7
6
|
require_relative "gemini_omni/resources/create_character"
|
|
8
7
|
require_relative "gemini_omni/resources/text_to_video"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: runapi-gemini-omni
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- RunAPI
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - "~>"
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 0.
|
|
18
|
+
version: 0.6.0
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 0.
|
|
25
|
+
version: 0.6.0
|
|
26
26
|
description: The Gemini Omni Ruby SDK is the language-specific package for Gemini
|
|
27
27
|
Omni on RunAPI. Use this package for voice resources, character resources, and multimodal
|
|
28
28
|
video generation workflows when your application needs request bodies, task status
|
|
@@ -39,7 +39,6 @@ files:
|
|
|
39
39
|
- lib/runapi-gemini_omni.rb
|
|
40
40
|
- lib/runapi/gemini_omni.rb
|
|
41
41
|
- lib/runapi/gemini_omni/client.rb
|
|
42
|
-
- lib/runapi/gemini_omni/contract_gen.rb
|
|
43
42
|
- lib/runapi/gemini_omni/resources/create_audio.rb
|
|
44
43
|
- lib/runapi/gemini_omni/resources/create_character.rb
|
|
45
44
|
- lib/runapi/gemini_omni/resources/text_to_video.rb
|
|
@@ -68,7 +67,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
68
67
|
- !ruby/object:Gem::Version
|
|
69
68
|
version: '0'
|
|
70
69
|
requirements: []
|
|
71
|
-
rubygems_version: 4.0.
|
|
70
|
+
rubygems_version: 4.0.10
|
|
72
71
|
specification_version: 4
|
|
73
72
|
summary: Gemini Omni Ruby SDK for RunAPI
|
|
74
73
|
test_files: []
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RunApi
|
|
4
|
-
module GeminiOmni
|
|
5
|
-
CONTRACT = {
|
|
6
|
-
"create-audio" => {
|
|
7
|
-
"models" => ["gemini-omni-audio"],
|
|
8
|
-
"fields_by_model" => {
|
|
9
|
-
"gemini-omni-audio" => {
|
|
10
|
-
"audio_id" => {
|
|
11
|
-
"required" => true
|
|
12
|
-
},
|
|
13
|
-
"example_dialogue" => {
|
|
14
|
-
"max" => 120,
|
|
15
|
-
"length" => true
|
|
16
|
-
},
|
|
17
|
-
"name" => {
|
|
18
|
-
"required" => true,
|
|
19
|
-
"max" => 210,
|
|
20
|
-
"length" => true
|
|
21
|
-
},
|
|
22
|
-
"voice_description" => {
|
|
23
|
-
"max" => 20000,
|
|
24
|
-
"length" => true
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
},
|
|
29
|
-
"create-character" => {
|
|
30
|
-
"models" => ["gemini-omni-character"],
|
|
31
|
-
"fields_by_model" => {
|
|
32
|
-
"gemini-omni-character" => {
|
|
33
|
-
"character_name" => {
|
|
34
|
-
"max" => 210,
|
|
35
|
-
"length" => true
|
|
36
|
-
},
|
|
37
|
-
"descriptions" => {
|
|
38
|
-
"required" => true,
|
|
39
|
-
"max" => 20000,
|
|
40
|
-
"length" => true
|
|
41
|
-
},
|
|
42
|
-
"reference_image_url" => {
|
|
43
|
-
"required" => true
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
},
|
|
48
|
-
"text-to-video" => {
|
|
49
|
-
"models" => ["gemini-omni-flash-preview", "gemini-omni-text-to-video"],
|
|
50
|
-
"fields_by_model" => {
|
|
51
|
-
"gemini-omni-flash-preview" => {
|
|
52
|
-
"aspect_ratio" => {
|
|
53
|
-
"enum" => ["16:9", "9:16"]
|
|
54
|
-
},
|
|
55
|
-
"duration_seconds" => {
|
|
56
|
-
"type" => "integer"
|
|
57
|
-
},
|
|
58
|
-
"output_resolution" => {
|
|
59
|
-
"enum" => ["720p"]
|
|
60
|
-
},
|
|
61
|
-
"prompt" => {
|
|
62
|
-
"required" => true,
|
|
63
|
-
"max" => 20000,
|
|
64
|
-
"length" => true
|
|
65
|
-
},
|
|
66
|
-
"seed" => {
|
|
67
|
-
"type" => "integer"
|
|
68
|
-
}
|
|
69
|
-
},
|
|
70
|
-
"gemini-omni-text-to-video" => {
|
|
71
|
-
"aspect_ratio" => {
|
|
72
|
-
"enum" => ["16:9", "9:16"]
|
|
73
|
-
},
|
|
74
|
-
"audio_ids" => {
|
|
75
|
-
"max_items" => 3
|
|
76
|
-
},
|
|
77
|
-
"character_ids" => {
|
|
78
|
-
"max_items" => 3
|
|
79
|
-
},
|
|
80
|
-
"duration_seconds" => {
|
|
81
|
-
"enum" => [4, 6, 8, 10],
|
|
82
|
-
"required" => true,
|
|
83
|
-
"type" => "integer"
|
|
84
|
-
},
|
|
85
|
-
"output_resolution" => {
|
|
86
|
-
"enum" => ["720p", "1080p", "4k"]
|
|
87
|
-
},
|
|
88
|
-
"prompt" => {
|
|
89
|
-
"required" => true,
|
|
90
|
-
"max" => 20000,
|
|
91
|
-
"length" => true
|
|
92
|
-
},
|
|
93
|
-
"reference_image_urls" => {
|
|
94
|
-
"max_items" => 7
|
|
95
|
-
},
|
|
96
|
-
"seed" => {
|
|
97
|
-
"min" => 0,
|
|
98
|
-
"max" => 2147483647,
|
|
99
|
-
"type" => "integer"
|
|
100
|
-
},
|
|
101
|
-
"video_list" => {
|
|
102
|
-
"max_items" => 1
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
},
|
|
106
|
-
"rules" => [{
|
|
107
|
-
"when" => {
|
|
108
|
-
"model" => "gemini-omni-flash-preview"
|
|
109
|
-
},
|
|
110
|
-
"forbidden" => ["reference_image_urls", "audio_ids", "video_list", "character_ids", "duration_seconds", "seed"]
|
|
111
|
-
}]
|
|
112
|
-
}
|
|
113
|
-
}.freeze
|
|
114
|
-
end
|
|
115
|
-
end
|