freeimagetovideo 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5c87a66298c7dbe96206cb7407322cb19c9ded78aa11092095fa650a98417779
4
+ data.tar.gz: cbbb8bfe7f7f011eb003d9fd1685c70c7fe8a80faf362164bb64cbe4f2e5c3f3
5
+ SHA512:
6
+ metadata.gz: 7a56d066febdd9dcc675bbd3a115261ea3715d8bc18986a1c87b8a0aa499747eefc7f40f29beb5ede3c0fef21a7bdc0383157df7a6a7c18692a2154b7496c716
7
+ data.tar.gz: 6edff9ce9055fd918135668fbeb32ed309d155f498ccc82060c34688aac259fe7ab8a0313b624034beeba62800b95865f66c8f26ddb11aca859eea875d00d502
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Free Image to Video (https://freeimagetovideo.ai)
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.
data/README.md ADDED
@@ -0,0 +1,67 @@
1
+ # Free Image to Video: Ruby SDK
2
+
3
+ Free image to video AI with no daily cap: start with no sign-up, keep going on a free account. Every video comes with sound and no watermark. This is the official Ruby SDK for [Free Image to Video](https://freeimagetovideo.ai/?utm_source=rubygems&utm_medium=package): turn a prompt or a photo into a video clip from your Ruby code.
4
+
5
+ ```ruby
6
+ require "freeimagetovideo"
7
+
8
+ clip = FreeImageToVideo.generate_video("a paper lantern drifting over a night river")
9
+ moved = FreeImageToVideo.generate_video("the camera slowly orbits the statue",
10
+ image_url: "https://example.com/statue.jpg",
11
+ duration: 6, aspect_ratio: "9:16")
12
+ ```
13
+
14
+ Each call submits the run, waits for it and returns the URL of the finished clip. With `image_url` it is [image to video AI with sound](https://freeimagetovideo.ai/video/image-to-video-ai-with-sound): your photo is the first frame. Without it, it is [free text to video AI](https://freeimagetovideo.ai/video/free-text-to-video-ai) from the prompt alone.
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ gem install freeimagetovideo
20
+ ```
21
+
22
+ Standard library only (`net/http`, `json`).
23
+
24
+ ## API key and plans
25
+
26
+ Create a key at [freeimagetovideo.ai/settings/apikeys](https://freeimagetovideo.ai/settings/apikeys), then export it:
27
+
28
+ ```bash
29
+ export FREEIMAGETOVIDEO_API_KEY=sk-...
30
+ ```
31
+
32
+ API runs need a paid plan: each run draws on the plan's full-speed allowance, then waits in the relaxed queue. See [pricing](https://freeimagetovideo.ai/pricing). To make clips without a key, use the generator on [freeimagetovideo.ai](https://freeimagetovideo.ai/?utm_source=rubygems&utm_medium=package): start with no sign-up, keep going on a free account.
33
+
34
+ ## Options
35
+
36
+ | option | values |
37
+ |---|---|
38
+ | `duration` | 4, 5, 6, 8, 10, 15 (seconds) |
39
+ | `resolution` | `480p` (default), `768p` |
40
+ | `aspect_ratio` | `16:9` (default), `9:16`, `1:1` |
41
+
42
+ Model: MiniMax H3 (`minimax-h3`). Every clip comes with sound.
43
+
44
+ ## Lower level
45
+
46
+ ```ruby
47
+ client = FreeImageToVideo::Client.new # reads FREEIMAGETOVIDEO_API_KEY
48
+ id = client.submit("a koi pond in the rain")
49
+ run = client.status(id) # run["status"]: pending | processing | success | failed | canceled
50
+ url = client.wait(id) # polls every 10 s, gives up after 30 minutes
51
+ left = client.credits # credits left on the account
52
+ ```
53
+
54
+ ## Errors worth catching
55
+
56
+ All inherit from `FreeImageToVideo::Error`:
57
+
58
+ - `AuthError`: no key, or the key is wrong or deleted
59
+ - `QuotaError`: not enough credits for this run
60
+ - `PlanError`: API runs need a paid plan
61
+ - `BusyError`: too many runs generating at once
62
+ - `RejectedError`: the content filter refused the prompt, or a cooldown is active
63
+ - `GenerationError`: the run failed or was canceled; `#error_message` says why
64
+ - `TimeoutError`: still generating at the deadline; check the run later with `status`
65
+ - `APIError`: any other failed request
66
+
67
+ MIT licensed.
@@ -0,0 +1,3 @@
1
+ module FreeImageToVideo
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,209 @@
1
+ # Official Ruby SDK for Free Image to Video (https://freeimagetovideo.ai).
2
+ # Free image to video AI with no daily cap: start with no sign-up, keep going on
3
+ # a free account. Every video comes with sound and no watermark.
4
+ #
5
+ # require "freeimagetovideo"
6
+ # clip = FreeImageToVideo.generate_video("a paper lantern drifting over a night river")
7
+ # moved = FreeImageToVideo.generate_video("the camera slowly orbits the statue",
8
+ # image_url: "https://example.com/statue.jpg")
9
+ #
10
+ # Set FREEIMAGETOVIDEO_API_KEY (create a key at https://freeimagetovideo.ai/settings/apikeys).
11
+ require "json"
12
+ require "net/http"
13
+ require "uri"
14
+ require_relative "freeimagetovideo/version"
15
+
16
+ module FreeImageToVideo
17
+ BASE_URL = "https://freeimagetovideo.ai".freeze
18
+ KEYS_URL = "https://freeimagetovideo.ai/settings/apikeys".freeze
19
+ DEFAULT_MODEL = "minimax-h3".freeze
20
+ MODELS = ["minimax-h3"].freeze
21
+
22
+ # Base of every error this gem raises.
23
+ class Error < StandardError
24
+ attr_reader :status, :code, :reason, :task_id, :error_message, :failure_kind
25
+ def initialize(msg = nil, status: nil, code: nil, reason: nil, task_id: nil, error_message: nil, failure_kind: nil)
26
+ super(msg)
27
+ @status = status
28
+ @code = code
29
+ @reason = reason
30
+ @task_id = task_id
31
+ @error_message = error_message
32
+ @failure_kind = failure_kind
33
+ end
34
+ end
35
+ # No API key, or the key is invalid or deleted.
36
+ class AuthError < Error; end
37
+ # Not enough credits for this run.
38
+ class QuotaError < Error; end
39
+ # API runs need a paid plan.
40
+ class PlanError < Error; end
41
+ # Too many runs generating at once on this account.
42
+ class BusyError < Error; end
43
+ # The content filter refused the prompt, or a cooldown is active.
44
+ class RejectedError < Error; end
45
+ # The run ended as failed or canceled; #error_message says why.
46
+ class GenerationError < Error; end
47
+ # Still generating when the deadline passed; check the run later.
48
+ class TimeoutError < Error; end
49
+ # Any other failed request.
50
+ class APIError < Error; end
51
+
52
+ # Map an HTTP status and envelope to the error that says what to do next.
53
+ def self.classify(status, env)
54
+ env = {} unless env.is_a?(Hash)
55
+ msg = env["message"].to_s
56
+ msg = "request failed (HTTP #{status})" if msg.empty?
57
+ code = env["code"].is_a?(Integer) ? env["code"] : 0
58
+ reason = env["data"].is_a?(Hash) ? env["data"]["reason"].to_s : ""
59
+ klass =
60
+ if status == 401 then AuthError
61
+ elsif code == -2 || code == -3 then RejectedError
62
+ elsif reason == "insufficient_credits" then QuotaError
63
+ elsif reason == "plan_required" then PlanError
64
+ elsif reason == "concurrency" then BusyError
65
+ elsif msg.downcase.include?("no auth") then AuthError
66
+ else APIError
67
+ end
68
+ klass.new(msg, status: status, code: code, reason: reason.empty? ? nil : reason)
69
+ end
70
+
71
+ # Read one answer: the envelope's data on code 0, else raise the classified error.
72
+ # Refusals usually arrive as HTTP 200 with a non-zero code, so the status alone is not enough.
73
+ def self.parse_envelope(status, raw)
74
+ env = {}
75
+ unless raw.to_s.strip.empty?
76
+ begin
77
+ env = JSON.parse(raw)
78
+ rescue JSON::ParserError
79
+ raise classify(401, {}) if status == 401
80
+ raise APIError.new("freeimagetovideo.ai returned a non-JSON response (HTTP #{status})", status: status)
81
+ end
82
+ end
83
+ return env["data"] if status < 400 && env.is_a?(Hash) && env["code"] == 0
84
+
85
+ raise classify(status, env)
86
+ end
87
+
88
+ # Request body for POST /api/ai/generate.
89
+ def self.body(prompt, image_url: nil, model: nil, **options)
90
+ opts = options.each_with_object({}) { |(k, v), o| o[k.to_s] = v }
91
+ opts["sound"] = true
92
+ scene = "text-to-video"
93
+ if image_url && !image_url.to_s.empty?
94
+ opts["image_input"] = [image_url.to_s]
95
+ scene = "image-to-video"
96
+ end
97
+ { "provider" => "auto", "mediaType" => "video", "model" => (model || DEFAULT_MODEL).to_s,
98
+ "scene" => scene, "prompt" => prompt, "options" => opts }
99
+ end
100
+
101
+ # A client bound to one API key. transport: is a callable (url, body_hash_or_nil, key) -> [status, raw_body], for tests.
102
+ class Client
103
+ attr_accessor :poll_every, :timeout
104
+
105
+ def initialize(api_key = nil, base_url: nil, poll_every: 10, timeout: 1800, transport: nil)
106
+ @key = (api_key || ENV["FREEIMAGETOVIDEO_API_KEY"]).to_s.strip
107
+ base = (base_url || ENV["FREEIMAGETOVIDEO_BASE_URL"]).to_s.strip
108
+ @base = (base.empty? ? BASE_URL : base).chomp("/")
109
+ @poll_every = poll_every
110
+ @timeout = timeout
111
+ @transport = transport || method(:http)
112
+ end
113
+
114
+ # Start a run and return its task id without waiting.
115
+ # Options: duration (4/5/6/8/10/15), resolution ("480p"/"768p"), aspect_ratio ("16:9"/"9:16"/"1:1").
116
+ def submit(prompt, image_url: nil, model: nil, **options)
117
+ raise APIError, "prompt is required" if prompt.to_s.strip.empty?
118
+
119
+ data = post("/api/ai/generate", FreeImageToVideo.body(prompt, image_url: image_url, model: model, **options))
120
+ id = data.is_a?(Hash) ? data["id"] : nil
121
+ raise APIError, "freeimagetovideo.ai accepted the run but returned no id" if id.nil? || id.to_s.empty?
122
+
123
+ id.to_s
124
+ end
125
+
126
+ # Look up a run: {"id", "status" (pending|processing|success|failed|canceled), "videos", "errorMessage", "failureKind", "queued", "queueEta"}.
127
+ def status(task_id)
128
+ data = post("/api/ai/query", { "taskId" => task_id })
129
+ raise APIError, "unexpected status answer" unless data.is_a?(Hash)
130
+
131
+ data
132
+ end
133
+
134
+ # Credits left on the account.
135
+ def credits
136
+ data = post("/api/user/get-user-credits", nil)
137
+ data.is_a?(Hash) ? data["remainingCredits"].to_i : 0
138
+ end
139
+
140
+ # Submit, wait, and return the URL of the finished clip.
141
+ def generate_video(prompt, image_url: nil, model: nil, **options)
142
+ wait(submit(prompt, image_url: image_url, model: model, **options))
143
+ end
144
+
145
+ # Poll a submitted run until it finishes and return its video URL.
146
+ def wait(task_id)
147
+ deadline = Time.now + @timeout
148
+ loop do
149
+ t = status(task_id)
150
+ case t["status"]
151
+ when "success"
152
+ videos = Array(t["videos"])
153
+ raise GenerationError.new("freeimagetovideo: run #{task_id}: finished with no video", task_id: task_id, error_message: "finished with no video") if videos.empty?
154
+
155
+ return videos[0]
156
+ when "failed", "canceled"
157
+ em = t["errorMessage"].to_s
158
+ raise GenerationError.new("freeimagetovideo: generation failed: run #{task_id}: #{em.empty? ? t['status'] : em}",
159
+ task_id: task_id, error_message: em, failure_kind: t["failureKind"])
160
+ end
161
+ raise TimeoutError.new("freeimagetovideo: still generating; check run #{task_id} later", task_id: task_id) if Time.now + @poll_every > deadline
162
+
163
+ sleep @poll_every
164
+ end
165
+ end
166
+
167
+ private
168
+
169
+ def post(path, payload)
170
+ raise AuthError, "set FREEIMAGETOVIDEO_API_KEY (create a key at #{KEYS_URL})" if @key.empty?
171
+
172
+ status, raw = @transport.call("#{@base}#{path}", payload, @key)
173
+ FreeImageToVideo.parse_envelope(status, raw)
174
+ end
175
+
176
+ def http(url, payload, key)
177
+ uri = URI(url)
178
+ req = Net::HTTP::Post.new(uri)
179
+ req["Content-Type"] = "application/json"
180
+ req["Authorization"] = "Bearer #{key}"
181
+ req["User-Agent"] = "fitv-ruby/#{VERSION}"
182
+ req.body = JSON.generate(payload) unless payload.nil?
183
+ res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", read_timeout: 60) { |h| h.request(req) }
184
+ [res.code.to_i, res.body.to_s]
185
+ rescue SocketError, SystemCallError, Net::OpenTimeout, Net::ReadTimeout, OpenSSL::SSL::SSLError => e
186
+ raise APIError, "could not reach freeimagetovideo.ai: #{e.message}"
187
+ end
188
+ end
189
+
190
+ # Generate one clip with sound and return its URL.
191
+ def self.generate_video(prompt, image_url: nil, api_key: nil, **options)
192
+ Client.new(api_key).generate_video(prompt, image_url: image_url, **options)
193
+ end
194
+
195
+ # Start a run and return its task id.
196
+ def self.submit(prompt, image_url: nil, api_key: nil, **options)
197
+ Client.new(api_key).submit(prompt, image_url: image_url, **options)
198
+ end
199
+
200
+ # Look up a run.
201
+ def self.status(task_id, api_key: nil)
202
+ Client.new(api_key).status(task_id)
203
+ end
204
+
205
+ # Credits left on the account.
206
+ def self.credits(api_key: nil)
207
+ Client.new(api_key).credits
208
+ end
209
+ end
metadata ADDED
@@ -0,0 +1,57 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: freeimagetovideo
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Free Image to Video
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-11-10 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: 'Free image to video AI with no daily cap: start with no sign-up, keep
14
+ going on a free account. Every video comes with sound and no watermark. This is
15
+ the official Ruby SDK for Free Image to Video: turn a prompt or a photo into a video
16
+ clip with sound from Ruby, standard library only.'
17
+ email:
18
+ - support@freeimagetovideo.ai
19
+ executables: []
20
+ extensions: []
21
+ extra_rdoc_files: []
22
+ files:
23
+ - LICENSE
24
+ - README.md
25
+ - lib/freeimagetovideo.rb
26
+ - lib/freeimagetovideo/version.rb
27
+ homepage: https://freeimagetovideo.ai/?utm_source=rubygems&utm_medium=package
28
+ licenses:
29
+ - MIT
30
+ metadata:
31
+ homepage_uri: https://freeimagetovideo.ai/?utm_source=rubygems&utm_medium=package
32
+ source_code_uri: https://github.com/hanshs474/fitv-rb
33
+ documentation_uri: https://freeimagetovideo.ai/video/image-to-video-ai-with-sound
34
+ bug_tracker_uri: https://github.com/hanshs474/fitv-rb/issues
35
+ rubygems_mfa_required: 'false'
36
+ post_install_message:
37
+ rdoc_options: []
38
+ require_paths:
39
+ - lib
40
+ required_ruby_version: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: '2.6'
45
+ required_rubygems_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '0'
50
+ requirements: []
51
+ rubygems_version: 3.0.3.1
52
+ signing_key:
53
+ specification_version: 4
54
+ summary: 'Free image to video AI with no daily cap: start with no sign-up, keep going
55
+ on a free account. Every video comes with sound and no watermark. This is the official
56
+ Ruby SDK for Free Image to Video.'
57
+ test_files: []