flowspeech 0.2.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 77ffd1136c8cf4e44cffce0e60f46a55a785d3011f228eb894fa12674a5b0f3a
4
- data.tar.gz: cbcf965a998720defa2dce9286b132b22cde88bf4e5c243bbae19e1e62579e27
3
+ metadata.gz: a3a94e1d6e6d165af4cbffe0b09add95d1cbe1a1e0bfef60a486f522b19eab09
4
+ data.tar.gz: fbb53b24d39482a7043052d7da00c7f79f33277f6e9ea860e109e90f80872d3c
5
5
  SHA512:
6
- metadata.gz: 6af7176948a2a08e459f9ccc6afdb9efe51f6384086b54fa0806c3333cac25217174b69a3c6a30a1f0e4e3421b7b003ddc7f53ac9a280c0a54acb1fd78426ac5
7
- data.tar.gz: 514fa7b5655ffa6ae3a4f84d486437939988f3bfbac8526c7faa8921b5edee280c67b5d6278c93782c049255b4de262f7a7e29db6b0b5bf4c961bc9c9e9772c9
6
+ metadata.gz: efb94ab0287fa9b3038ad490e109902542cacfea5884fcd950b29f32b2d4016a8c21aa3ca0738d3d239ab301c7c864791d7aa91fbeb2e981537fcf6c29d82d08
7
+ data.tar.gz: 5bf7e1ea264d6388d395b96e60f391132f6548911eba5cddbe8b7b1f45d87d9d12a4b0a47f5e7911cbd2d4786ead246e918e66e9c615ec01f9a4f75ff46944ef
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.1 - 2026-10-27
4
+
5
+ - Suppress raw download transport causes so full_message does not reveal private result URLs; preserve recovery IDs.
6
+ - Use generic API error messages and filter reflected key/URL values from error codes and traces, including terminal job errors.
7
+ - Do not retry a known permanent HTTP error or redirect when its body is interrupted; audio downloads remain single-attempt operations.
8
+ - Preserve received HTTP status, Retry-After and trace metadata when a response body is interrupted; retries keep the same UUID and bytes. Previously published 0.2.0 artifacts remain unchanged.
9
+
3
10
  ## 0.2.0
4
11
 
5
12
  - Validate RIFF/WAVE chunk sizes, 24000 Hz mono 16-bit PCM format and complete aligned nonempty audio data; preserve valid files byte-for-byte and reject malformed downloads without resubmitting.
data/README.md CHANGED
@@ -5,12 +5,21 @@ using only the standard library at runtime. Requires an active paid Basic, Pro
5
5
  or Scale subscription (not a trial) and a key from [API settings](https://flowspeech.io/settings/apikeys).
6
6
 
7
7
  ```ruby
8
- gem "flowspeech", "~> 0.2.0"
8
+ gem "flowspeech", "~> 0.2.1"
9
9
  ```
10
10
 
11
11
  Set `FLOWSPEECH_API_KEY` through your environment or secret manager. Use this gem
12
12
  on your server, never in a public client. Generation consumes account credits.
13
13
 
14
+ Version 0.2.1 includes the safety fixes below. Previously published 0.2.0
15
+ artifacts remain unchanged.
16
+ API and terminal-job errors use local messages and filter reflected keys and
17
+ private URLs. Download errors suppress raw causes, including `full_message`
18
+ cause chains, while preserving recovery IDs. Interrupted responses retain HTTP
19
+ status and cooldown; permanent failures do not retry. Do not log raw request
20
+ objects or result URLs. Injected transports and application logging remain
21
+ trusted boundaries.
22
+
14
23
  ## Generate WAV Audio
15
24
 
16
25
  ```ruby
@@ -170,6 +170,15 @@ module FlowSpeech
170
170
 
171
171
  private
172
172
 
173
+ def error_token(value, fallback = nil)
174
+ value.is_a?(String) && value.match?(/\A[A-Za-z0-9][A-Za-z0-9_.-]{0,127}\z/) && !value.include?(@api_key) ? value : fallback
175
+ end
176
+
177
+ def job_error(job)
178
+ code = job["error"].is_a?(Hash) ? job["error"]["code"] : nil
179
+ JobError.new(job.merge("error" => { "code" => error_token(code, "job_failed") }))
180
+ end
181
+
173
182
  def normalize_input(text, speakers, voice, segments, original)
174
183
  raise ArgumentError, "original_text is unsupported by v1" unless original.nil?
175
184
  unless segments.nil?
@@ -328,20 +337,20 @@ module FlowSpeech
328
337
  elsif job["status"] == "success" && job["resultDeleted"] != true
329
338
  return job
330
339
  else
331
- raise JobError, job
340
+ raise job_error(job)
332
341
  end
333
342
  end
334
343
  end
335
344
 
336
345
  def download_bytes(job, deadline)
337
- raise JobError, job unless job["status"] == "success" && job["resultDeleted"] != true
346
+ raise job_error(job) unless job["status"] == "success" && job["resultDeleted"] != true
338
347
  uri = safe_url(job.fetch("result").fetch("url"))
339
348
  req = Net::HTTP::Get.new(uri)
340
349
  response = exchange(uri, req, deadline, @max_download_bytes, @download_transport)
341
350
  raise Error, "Download HTTP #{response.code}; retry download with the same job" unless response.code.to_i == 200
342
351
  Wav.validate!(response.body.to_s) { remaining(deadline) }
343
352
  rescue IOError, SystemCallError, SocketError, Timeout::Error, OpenSSL::SSL::SSLError
344
- raise Error, "Audio download failed; retry download with the same job"
353
+ raise Error, "Audio download failed; retry download with the same job", cause: nil
345
354
  end
346
355
 
347
356
  def request(method, path, body: nil, id: nil, deadline:)
@@ -349,6 +358,8 @@ module FlowSpeech
349
358
  attempts = 0
350
359
  loop do
351
360
  retry_after = nil
361
+ status = nil
362
+ trace_id = nil
352
363
  begin
353
364
  req = (method == :get ? Net::HTTP::Get : Net::HTTP::Post).new(uri)
354
365
  req["Authorization"] = "Bearer #{@api_key}"
@@ -358,7 +369,11 @@ module FlowSpeech
358
369
  req["Content-Type"] = "application/json"
359
370
  req.body = body
360
371
  end
361
- response = exchange(uri, req, deadline, 1024 * 1024, @transport)
372
+ response = exchange(uri, req, deadline, 1024 * 1024, @transport) do |received|
373
+ status = received.code.to_i
374
+ retry_after = received["Retry-After"]
375
+ trace_id = error_token(received["X-Request-Id"])
376
+ end
362
377
  status = response.code.to_i
363
378
  retry_after = response["Retry-After"]
364
379
  data = begin
@@ -371,14 +386,17 @@ module FlowSpeech
371
386
  return [data, retry_after]
372
387
  end
373
388
  detail = data.is_a?(Hash) && data["error"].is_a?(Hash) ? data["error"] : {}
374
- error = APIError.new(detail["message"] || "HTTP #{status}", status: status,
375
- code: detail["code"], retryable: detail["retryable"] == true, retry_after: retry_after,
376
- trace_id: data.is_a?(Hash) ? data["traceId"] || response["X-Request-Id"] : response["X-Request-Id"])
377
- raise error unless status == 429 || (status >= 500 && error.retryable)
389
+ retryable = status == 429 || (status >= 500 && detail["retryable"] == true)
390
+ error = APIError.new("FlowSpeech API returned HTTP #{status}", status: status,
391
+ code: error_token(detail["code"], "api_error"), retryable: retryable, retry_after: retry_after,
392
+ trace_id: error_token(data.is_a?(Hash) ? data["traceId"] || response["X-Request-Id"] : response["X-Request-Id"]))
393
+ raise error unless error.retryable
378
394
  rescue IOError, SystemCallError, SocketError, Timeout::Error, OpenSSL::SSL::SSLError
379
- error = Error.new("API transport failed")
395
+ retryable = status.nil? || (200..299).cover?(status) || status == 429 || (500..599).cover?(status)
396
+ error = APIError.new("API transport failed", status: status || 0,
397
+ error_code: "transport_error", retryable: retryable, retry_after: retry_after, trace_id: trace_id)
380
398
  end
381
- raise error if attempts >= @max_retries
399
+ raise error if !error.retryable || attempts >= @max_retries
382
400
  attempts += 1
383
401
  pause(retry_after, deadline)
384
402
  end
@@ -395,6 +413,7 @@ module FlowSpeech
395
413
  http.max_retries = 0 if http.respond_to?(:max_retries=)
396
414
  result = nil
397
415
  http.request(request) do |res|
416
+ yield res if block_given?
398
417
  bytes = String.new(encoding: Encoding::BINARY)
399
418
  res.read_body do |chunk|
400
419
  remaining(deadline)
@@ -406,6 +425,7 @@ module FlowSpeech
406
425
  result
407
426
  end
408
427
  end
428
+ yield response if transport && block_given?
409
429
  remaining(deadline)
410
430
  raise Error, "Response exceeds byte limit" if response.body.to_s.bytesize > limit
411
431
  response
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FlowSpeech
4
- VERSION = "0.2.0"
4
+ VERSION = "0.2.1"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: flowspeech
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - FlowSpeech
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-10-26 00:00:00.000000000 Z
11
+ date: 2026-10-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake