kavel 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.
Files changed (6) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +80 -0
  4. data/lib/kavel/version.rb +3 -0
  5. data/lib/kavel.rb +127 -0
  6. metadata +52 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 679fb519c2cc395d4219b8e350479387aed76177523c661eb6f9a4b37c8bc5dd
4
+ data.tar.gz: 4b77c92e7c6e7cc66ffa0432cec5a71b7a9cc1e22c416e04328031bb8cfa1284
5
+ SHA512:
6
+ metadata.gz: f112956669e912aa67ce9a0ddd20be03e042f2f82e9330f0dba678c8d7c956600eb7f2239b583ba7fa4e297937d2276c56f3f1135d48cb3a6be704a00fddef4d
7
+ data.tar.gz: 599a92a96c155b8bbcd976b76d05e23ceee3b33c0ed99db13d57bd30529302d0350f3fa6d3bb64061e1cea8495f5abed0981d6be8e53fe794064e6d9f78ef74b
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kavel (https://www.kavel.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,80 @@
1
+ # kavel
2
+
3
+ Generate **and edit** images from Ruby with no API key, no account and no card.
4
+
5
+ ```bash
6
+ gem install kavel
7
+ ```
8
+
9
+ ```ruby
10
+ require "kavel"
11
+
12
+ img = Kavel.generate(
13
+ "matte black ceramic mug on pale oak, soft window light from the left, shallow depth of field",
14
+ aspect_ratio: "16:9"
15
+ )
16
+ img.url # => "https://cdn.kavel.ai/uploads/kie/image/....webp"
17
+ img.watermarked # => true on the free tier
18
+
19
+ edited = Kavel.edit(img.url, "place the mug on a white marble surface")
20
+ ```
21
+
22
+ Every other image gem wants a key from OpenAI, fal or Replicate before it runs once. This one talks to
23
+ the free tier of [Kavel](https://www.kavel.ai/?utm_source=rubygems&utm_medium=package) — a client id the
24
+ gem invents rather than an account you register — so a Rake task, a Sidekiq job or a one-off script works
25
+ on a machine with nothing configured.
26
+
27
+ ## Why it is shaped this way
28
+
29
+ - **Standard library only.** `net/http` and `json`; nothing to conflict with your Gemfile.
30
+ - **An edit lane.** `Kavel.edit` takes a photo you already have and one sentence describing the change.
31
+ - **Exceptions you can rescue by intent.** `Kavel::QuotaError` means wait or sign in,
32
+ `Kavel::RejectedError` means reword, `Kavel::SignInError` means the request is off the free shelf.
33
+ - **It keeps polling through a dropped connection.** A free run waits in a queue and only reaches the
34
+ model at the end of it; giving up on one failed poll would throw away a job about to run. A call holds
35
+ its thread for a minute or more, so run it from a background job, not a request cycle.
36
+
37
+ ## Limits, measured against the running service
38
+
39
+ - The free grant is **15 credits**. An image costs **5**, an edit costs **15**. The package mints a new
40
+ client id per call, so a loop is not capped at three.
41
+ - **30 credits per IP per day** sits on top — about six images from one machine — and surfaces as
42
+ `Kavel::QuotaError`.
43
+ - Free output is **1K and watermarked**; `img.watermarked` tells you instead of leaving you to guess.
44
+ - Free runs queue **25–80 seconds** before the model is called, which is why the default deadline is
45
+ six minutes.
46
+ - **Video does not run anonymously** — the cheapest clip costs more than the grant. The
47
+ [AI video generator](https://www.kavel.ai/video?utm_source=rubygems&utm_medium=package) has a browser
48
+ lane for that.
49
+
50
+ Signing in removes the watermark and opens the full shelf:
51
+ [Nano Banana 2](https://www.kavel.ai/image/nano-banana-2?utm_source=rubygems&utm_medium=package) for up to
52
+ 14 reference photos, [GPT Image 2](https://www.kavel.ai/image/gpt-image-2?utm_source=rubygems&utm_medium=package)
53
+ for 4K output, [Qwen Image 3](https://www.kavel.ai/image/qwen-image-3?utm_source=rubygems&utm_medium=package)
54
+ when the picture has to contain correctly spelled text, and
55
+ [Seedream 5.0 Pro](https://www.kavel.ai/image/seedream-5-pro?utm_source=rubygems&utm_medium=package) for
56
+ photoreal people. [Pricing](https://www.kavel.ai/pricing?utm_source=rubygems&utm_medium=package) has every tier.
57
+
58
+ ## Prompts that work
59
+
60
+ Name the light, the material and the composition — that moves the result more than adjectives do.
61
+ For edits, say what changes and the model keeps the rest; it is the same
62
+ [Nano Banana 2 Lite](https://www.kavel.ai/image/nano-banana-2-lite?utm_source=rubygems&utm_medium=package)
63
+ lane behind the [AI hairstyle changer](https://www.kavel.ai/image/ai-hairstyle-changer?utm_source=rubygems&utm_medium=package)
64
+ and the [AI outfit generator](https://www.kavel.ai/image/ai-outfit-generator?utm_source=rubygems&utm_medium=package).
65
+ The [free AI image generator](https://www.kavel.ai/image?utm_source=rubygems&utm_medium=package) runs the
66
+ same engines in a browser, and the [showcase](https://www.kavel.ai/showcases?utm_source=rubygems&utm_medium=package)
67
+ has finished output.
68
+
69
+ ## Tests
70
+
71
+ ```bash
72
+ ruby test/test_kavel.rb
73
+ ```
74
+
75
+ Offline: argument validation, client id uniqueness, the quota wall (an HTTP 200 recognised only by a
76
+ field), the sign-in refusal, and polling through a dropped connection.
77
+
78
+ More languages and recipes: [kavel.readthedocs.io](https://kavel.readthedocs.io/).
79
+
80
+ MIT
@@ -0,0 +1,3 @@
1
+ module Kavel
2
+ VERSION = "0.1.0"
3
+ end
data/lib/kavel.rb ADDED
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+ require "securerandom"
6
+ require "uri"
7
+ require_relative "kavel/version"
8
+
9
+ # Generate and edit images with no API key and no account.
10
+ #
11
+ # Calls the free tier of https://www.kavel.ai, which meters its allowance
12
+ # against a client id this library invents rather than an account you register.
13
+ # Measured against the running service: 15 credits per id, 5 per image, 15 per
14
+ # edit, 30 per IP per day. Free output is 1K and watermarked.
15
+ module Kavel
16
+ BASE_URL = "https://www.kavel.ai"
17
+ ANON_GRANT = 15
18
+ COST_TEXT_TO_IMAGE = 5
19
+ COST_EDIT = 15
20
+ IP_DAILY_CEILING = 30
21
+ MODEL_GENERATE = "kavel-image-v1"
22
+ MODEL_EDIT = "nano-banana-2-lite"
23
+
24
+ # One finished picture. +url+ is a permanent CDN link; +watermarked+ says
25
+ # whether a mark was actually drawn.
26
+ Image = Struct.new(:url, :watermarked)
27
+
28
+ # Base error. Rescue the subclass that matches what you want to do next.
29
+ class Error < StandardError; end
30
+ # The free allowance is spent for this id or this machine today. Wait, or sign in.
31
+ class QuotaError < Error; end
32
+ # The content filter or model refused the prompt. Reword it.
33
+ class RejectedError < Error; end
34
+ # The request needs a signed-in account (video, or a model off the free shelf).
35
+ class SignInError < Error; end
36
+ # The deadline passed while the job was queued or running.
37
+ class TimeoutError < Error; end
38
+
39
+ class << self
40
+ # Turn a prompt into a new image. Name the light, the material and the composition.
41
+ def generate(prompt, aspect_ratio: "1:1", **opts)
42
+ raise ArgumentError, "prompt is required" if prompt.to_s.strip.empty?
43
+
44
+ run({ provider: "kie", mediaType: "image", model: MODEL_GENERATE, scene: "text-to-image",
45
+ prompt: prompt, options: { aspect_ratio: aspect_ratio } }, **opts)
46
+ end
47
+
48
+ # Rewrite an existing image. +source_url+ must be a public http(s) url.
49
+ def edit(source_url, instruction, **opts)
50
+ raise ArgumentError, "source_url must be a public http(s) url" unless source_url.to_s =~ %r{\Ahttps?://}
51
+ raise ArgumentError, "instruction is required" if instruction.to_s.strip.empty?
52
+
53
+ run({ provider: "kie", mediaType: "image", model: MODEL_EDIT, scene: "image-to-image",
54
+ prompt: instruction, options: { image_input: [source_url] } }, **opts)
55
+ end
56
+
57
+ # A fresh client id. A reused id walls partway through a loop.
58
+ def anon_id
59
+ "rb-#{SecureRandom.hex(8)}"
60
+ end
61
+
62
+ # Interpret the submit response. The quota wall answers HTTP 200 with code 0,
63
+ # so it can only be recognised by a field.
64
+ def parse_submit(env)
65
+ if env["code"] != 0
66
+ msg = env["message"].to_s
67
+ raise SignInError, msg if msg.downcase.include?("sign in")
68
+
69
+ raise Error, msg
70
+ end
71
+ data = env["data"] || {}
72
+ if data["wall"] == true
73
+ raise QuotaError, "this machine has used its #{IP_DAILY_CEILING} credits for today" if data["reason"] == "anon_ip_daily"
74
+ raise SignInError, "video" if data["reason"] == "anon_unmetered_video"
75
+
76
+ raise QuotaError, "free allowance spent"
77
+ end
78
+ id = data["id"].to_s
79
+ raise Error, "the service returned no task id" if id.empty?
80
+
81
+ id
82
+ end
83
+
84
+ private
85
+
86
+ def run(payload, base_url: BASE_URL, poll_every: 5, timeout: 360, transport: nil)
87
+ http = transport || method(:request)
88
+ deadline = Time.now + timeout
89
+ id = anon_id
90
+ task = parse_submit(http.call(:post, "#{base_url}/api/ai/generate", payload, id))
91
+
92
+ # Free runs queue 25-80s and only reach the model on the poll that crosses
93
+ # the end of it, so polling is what starts the work.
94
+ query = "#{base_url}/api/ai/anon-query?taskId=#{URI.encode_www_form_component(task)}&provider=kie&mediaType=image"
95
+ while Time.now < deadline
96
+ sleep poll_every
97
+ begin
98
+ env = http.call(:get, query, nil, id)
99
+ rescue StandardError => e
100
+ raise if e.is_a?(Error)
101
+
102
+ next # a dropped poll is not a failed generation
103
+ end
104
+ next unless env["code"] == 0
105
+
106
+ data = env["data"] || {}
107
+ images = data["images"] || []
108
+ return Image.new(images[0], (data["watermarked"] || [])[0] == true) unless images.empty?
109
+ raise RejectedError, "prompt refused" if %w[failed error].include?(data["status"])
110
+ end
111
+ raise TimeoutError, "deadline passed"
112
+ end
113
+
114
+ def request(verb, url, body, anon_id)
115
+ uri = URI(url)
116
+ req = verb == :post ? Net::HTTP::Post.new(uri) : Net::HTTP::Get.new(uri)
117
+ req["x-anon-id"] = anon_id
118
+ req["user-agent"] = "kavel-rb/#{VERSION}"
119
+ if body
120
+ req["content-type"] = "application/json"
121
+ req.body = JSON.generate(body)
122
+ end
123
+ res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", read_timeout: 60) { |h| h.request(req) }
124
+ JSON.parse(res.body)
125
+ end
126
+ end
127
+ end
metadata ADDED
@@ -0,0 +1,52 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: kavel
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Kavel
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-09-13 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: 'A zero-dependency client for the free tier of kavel.ai: text-to-image
14
+ and image editing without an API key, an account or a card.'
15
+ email:
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - LICENSE
21
+ - README.md
22
+ - lib/kavel.rb
23
+ - lib/kavel/version.rb
24
+ homepage: https://www.kavel.ai
25
+ licenses:
26
+ - MIT
27
+ metadata:
28
+ homepage_uri: https://www.kavel.ai
29
+ source_code_uri: https://github.com/hanshs474/kavel-rb
30
+ documentation_uri: https://kavel.readthedocs.io/
31
+ bug_tracker_uri: https://github.com/hanshs474/kavel-rb/issues
32
+ rubygems_mfa_required: 'false'
33
+ post_install_message:
34
+ rdoc_options: []
35
+ require_paths:
36
+ - lib
37
+ required_ruby_version: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '2.6'
42
+ required_rubygems_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ requirements: []
48
+ rubygems_version: 3.0.3.1
49
+ signing_key:
50
+ specification_version: 4
51
+ summary: Generate and edit images from Ruby with no API key and no account
52
+ test_files: []