flowspeech 0.1.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4aa33771a20fb9cb3f38ed4af5e46f47f6d884823a83c54e8d96c10fbe92f601
4
+ data.tar.gz: 3daec29ae0da53cf1931c3c4866847b717d068f443cc1ac056552b2ef661a616
5
+ SHA512:
6
+ metadata.gz: 22522217c54774ae015a720d88a73a8f9fe18d8c465464d3fe08a2f1d65e7f77b8fae598567f6b6bd11efdf88561d2af317cb1b885c526f46c4ae2ea2d3d7284
7
+ data.tar.gz: b268e634167156062af46985c4af5df99aa2bf829152c84d2e8d86e41a84547f4a1b4d3df2a8463d637cffb2fff99240649c7ad08536983ff3a830a8f1df9121
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 FlowSpeech
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
data/README.md ADDED
@@ -0,0 +1,102 @@
1
+ # FlowSpeech Ruby
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/flowspeech.svg)](https://rubygems.org/gems/flowspeech)
4
+ [![Tests](https://github.com/waeckerlinfederowicz66-sketch/flowspeech-ruby/actions/workflows/test.yml/badge.svg)](https://github.com/waeckerlinfederowicz66-sketch/flowspeech-ruby/actions/workflows/test.yml)
5
+
6
+ A small Ruby client for the [FlowSpeech text to speech API](https://flowspeech.io/). It supports context-aware synthesis, multi-speaker voice assignments, quota checks, audio metadata, and structured API errors.
7
+
8
+ The gem uses only the Ruby standard library at runtime.
9
+
10
+ ## Installation
11
+
12
+ Install the gem:
13
+
14
+ ```bash
15
+ gem install flowspeech
16
+ ```
17
+
18
+ Or add it to your Gemfile:
19
+
20
+ ```ruby
21
+ gem "flowspeech", "~> 0.1"
22
+ ```
23
+
24
+ Create a FlowSpeech API key, then keep it outside source control:
25
+
26
+ ```bash
27
+ export FLOWSPEECH_API_KEY="your_api_key"
28
+ ```
29
+
30
+ ## Generate speech
31
+
32
+ ```ruby
33
+ require "flowspeech"
34
+
35
+ client = FlowSpeech::Client.new
36
+ audio = client.synthesize(
37
+ text: "Welcome. [cheerfully] Let's make this sound natural.",
38
+ speakers: [{ voice_name: "Kore" }]
39
+ )
40
+
41
+ audio.write("speech.pcm")
42
+ puts "#{audio.sample_rate} Hz, #{audio.num_channels} channel"
43
+ ```
44
+
45
+ The API currently returns base64-encoded linear PCM with format metadata. The
46
+ `FlowSpeech::Audio#bytes` method returns the decoded bytes without changing the encoding.
47
+
48
+ ## Multi-speaker dialogue
49
+
50
+ ```ruby
51
+ audio = client.synthesize(
52
+ text: "Host: Welcome to the show.\nGuest: Thanks for having me.",
53
+ speakers: [
54
+ { speaker: "Host", voice_name: "Charon" },
55
+ { speaker: "Guest", voice_name: "Kore" }
56
+ ]
57
+ )
58
+ ```
59
+
60
+ ## Check quota
61
+
62
+ ```ruby
63
+ quota = client.quota
64
+ puts "#{quota.fetch("remaining")} characters remaining"
65
+ ```
66
+
67
+ ## Error handling
68
+
69
+ ```ruby
70
+ begin
71
+ client.synthesize(text: "Hello")
72
+ rescue FlowSpeech::APIError => error
73
+ warn "#{error.status}: #{error.message}"
74
+ warn "Retry after #{error.retry_after}s" if error.retryable
75
+ end
76
+ ```
77
+
78
+ ## Development
79
+
80
+ Run the tests without real credentials or network requests:
81
+
82
+ ```bash
83
+ ruby -Ilib test/client_test.rb
84
+ ```
85
+
86
+ Build the gem locally:
87
+
88
+ ```bash
89
+ gem build flowspeech.gemspec
90
+ ```
91
+
92
+ ## Security
93
+
94
+ - Never commit API keys.
95
+ - Load `FLOWSPEECH_API_KEY` from the environment or a secret manager.
96
+ - The client sends the key only in the `Authorization: Bearer` header.
97
+ - Tests use an injected fake transport and contain no credentials.
98
+
99
+ ## License
100
+
101
+ MIT
102
+
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+ require "json"
5
+ require "net/http"
6
+ require "uri"
7
+
8
+ module FlowSpeech
9
+ class Error < StandardError; end
10
+
11
+ class APIError < Error
12
+ attr_reader :status, :code, :error_code, :retryable, :retry_after
13
+
14
+ def initialize(message, status:, code: nil, error_code: nil, retryable: false, retry_after: nil)
15
+ super(message)
16
+ @status = status
17
+ @code = code
18
+ @error_code = error_code
19
+ @retryable = retryable
20
+ @retry_after = retry_after
21
+ end
22
+ end
23
+
24
+ class Audio
25
+ attr_reader :mime_type, :audio_base64, :quota, :sample_rate, :num_channels, :bits_per_sample
26
+
27
+ def initialize(data)
28
+ @mime_type = data.fetch("mimeType")
29
+ @audio_base64 = data.fetch("audioBase64")
30
+ @quota = data["quota"]
31
+ @sample_rate = data.fetch("sampleRate")
32
+ @num_channels = data.fetch("numChannels")
33
+ @bits_per_sample = data.fetch("bitsPerSample")
34
+ end
35
+
36
+ def bytes
37
+ Base64.strict_decode64(audio_base64)
38
+ rescue ArgumentError => e
39
+ raise Error, "FlowSpeech returned invalid base64 audio: #{e.message}"
40
+ end
41
+
42
+ def write(path)
43
+ File.binwrite(path, bytes)
44
+ path
45
+ end
46
+ end
47
+
48
+ class Client
49
+ DEFAULT_BASE_URL = "https://flowspeech.io"
50
+
51
+ def initialize(api_key: ENV["FLOWSPEECH_API_KEY"], base_url: DEFAULT_BASE_URL,
52
+ open_timeout: 10, read_timeout: 90, transport: nil)
53
+ @api_key = api_key.to_s.strip
54
+ @base_url = URI(base_url)
55
+ @open_timeout = open_timeout
56
+ @read_timeout = read_timeout
57
+ @transport = transport
58
+
59
+ raise ArgumentError, "api_key is required" if @api_key.empty?
60
+ unless @base_url.is_a?(URI::HTTP) && @base_url.host
61
+ raise ArgumentError, "base_url must be an HTTP or HTTPS URL"
62
+ end
63
+ end
64
+
65
+ def quota
66
+ request(:get, "/api/ai/text-to-speech/quota").fetch("quota")
67
+ end
68
+
69
+ def synthesize(text:, speakers: [{ voice_name: "Kore" }], original_text: nil)
70
+ raise ArgumentError, "text is required" if text.to_s.strip.empty?
71
+
72
+ normalized_speakers = Array(speakers).map { |speaker| normalize_speaker(speaker) }
73
+ raise ArgumentError, "at least one speaker is required" if normalized_speakers.empty?
74
+
75
+ data = request(
76
+ :post,
77
+ "/api/ai/text-to-speech",
78
+ text: text,
79
+ originalText: original_text || text,
80
+ speakers: normalized_speakers
81
+ )
82
+ raise Error, "FlowSpeech response did not include audio" if data["audioBase64"].to_s.empty?
83
+
84
+ Audio.new(data)
85
+ end
86
+
87
+ private
88
+
89
+ def normalize_speaker(speaker)
90
+ input = speaker.respond_to?(:to_h) ? speaker.to_h : {}
91
+ voice_name = input[:voice_name] || input["voice_name"] || input[:voiceName] || input["voiceName"]
92
+ raise ArgumentError, "every speaker requires voice_name" if voice_name.to_s.strip.empty?
93
+
94
+ result = { voiceName: voice_name }
95
+ label = input[:speaker] || input["speaker"]
96
+ result[:speaker] = label unless label.to_s.strip.empty?
97
+ result
98
+ end
99
+
100
+ def request(method, path, payload = nil)
101
+ uri = @base_url + path
102
+ http_request = build_request(method, uri, payload)
103
+ response = @transport ? @transport.call(uri, http_request) : perform(uri, http_request)
104
+ parsed = parse_json(response.body)
105
+
106
+ unless response.code.to_i.between?(200, 299)
107
+ error_data = parsed["data"].is_a?(Hash) ? parsed["data"] : {}
108
+ message = parsed["message"].to_s.strip
109
+ message = "FlowSpeech request failed with HTTP #{response.code}" if message.empty?
110
+ raise APIError.new(
111
+ message,
112
+ status: response.code.to_i,
113
+ code: parsed["code"],
114
+ error_code: error_data["errorCode"],
115
+ retryable: error_data["retryable"] == true,
116
+ retry_after: response["Retry-After"]
117
+ )
118
+ end
119
+
120
+ unless parsed["code"] == 0 && parsed["data"].is_a?(Hash)
121
+ raise Error, parsed["message"].to_s.empty? ? "Unexpected FlowSpeech response" : parsed["message"]
122
+ end
123
+
124
+ parsed["data"]
125
+ end
126
+
127
+ def build_request(method, uri, payload)
128
+ request_class = method == :get ? Net::HTTP::Get : Net::HTTP::Post
129
+ request = request_class.new(uri)
130
+ request["Authorization"] = "Bearer #{@api_key}"
131
+ request["Accept"] = "application/json"
132
+ if payload
133
+ request["Content-Type"] = "application/json"
134
+ request.body = JSON.generate(payload)
135
+ end
136
+ request
137
+ end
138
+
139
+ def perform(uri, request)
140
+ Net::HTTP.start(
141
+ uri.host,
142
+ uri.port,
143
+ use_ssl: uri.scheme == "https",
144
+ open_timeout: @open_timeout,
145
+ read_timeout: @read_timeout
146
+ ) { |http| http.request(request) }
147
+ end
148
+
149
+ def parse_json(body)
150
+ JSON.parse(body.to_s)
151
+ rescue JSON::ParserError => e
152
+ raise Error, "FlowSpeech returned invalid JSON: #{e.message}"
153
+ end
154
+ end
155
+ end
156
+
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlowSpeech
4
+ VERSION = "0.1.1"
5
+ end
data/lib/flowspeech.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "flowspeech/version"
4
+ require_relative "flowspeech/client"
5
+
metadata ADDED
@@ -0,0 +1,73 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: flowspeech
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - FlowSpeech
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-08-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '12.3'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '14'
23
+ type: :development
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '12.3'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '14'
33
+ description: Generate context-aware speech, multi-speaker dialogue, and audio metadata
34
+ with FlowSpeech.
35
+ email:
36
+ - support@flowspeech.io
37
+ executables: []
38
+ extensions: []
39
+ extra_rdoc_files: []
40
+ files:
41
+ - LICENSE
42
+ - README.md
43
+ - lib/flowspeech.rb
44
+ - lib/flowspeech/client.rb
45
+ - lib/flowspeech/version.rb
46
+ homepage: https://flowspeech.io/
47
+ licenses:
48
+ - MIT
49
+ metadata:
50
+ homepage_uri: https://flowspeech.io/
51
+ source_code_uri: https://github.com/waeckerlinfederowicz66-sketch/flowspeech-ruby
52
+ changelog_uri: https://github.com/waeckerlinfederowicz66-sketch/flowspeech-ruby/releases
53
+ rubygems_mfa_required: 'true'
54
+ post_install_message:
55
+ rdoc_options: []
56
+ require_paths:
57
+ - lib
58
+ required_ruby_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: 2.6.0
63
+ required_rubygems_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ requirements: []
69
+ rubygems_version: 3.5.22
70
+ signing_key:
71
+ specification_version: 4
72
+ summary: Ruby client for the FlowSpeech text-to-speech API
73
+ test_files: []