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 +7 -0
- data/LICENSE +21 -0
- data/README.md +67 -0
- data/lib/freeimagetovideo/version.rb +3 -0
- data/lib/freeimagetovideo.rb +209 -0
- metadata +57 -0
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,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: []
|