flux3_ai_prompt_tools 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/CHANGELOG.md +13 -0
- data/LICENSE +21 -0
- data/README.md +135 -0
- data/Rakefile +11 -0
- data/bin/flux3-prompt +23 -0
- data/examples/batch.jsonl +2 -0
- data/examples/kettle.json +16 -0
- data/flux3_ai_prompt_tools.gemspec +42 -0
- data/lib/flux3_ai_prompt_tools/assembler.rb +192 -0
- data/lib/flux3_ai_prompt_tools/cli.rb +349 -0
- data/lib/flux3_ai_prompt_tools/errors.rb +49 -0
- data/lib/flux3_ai_prompt_tools/fields.rb +37 -0
- data/lib/flux3_ai_prompt_tools/layouts.rb +8 -0
- data/lib/flux3_ai_prompt_tools/limits.rb +37 -0
- data/lib/flux3_ai_prompt_tools/manifest.rb +204 -0
- data/lib/flux3_ai_prompt_tools/numbers.rb +37 -0
- data/lib/flux3_ai_prompt_tools/presets.rb +53 -0
- data/lib/flux3_ai_prompt_tools/reader.rb +111 -0
- data/lib/flux3_ai_prompt_tools/schema.rb +108 -0
- data/lib/flux3_ai_prompt_tools/specification.rb +89 -0
- data/lib/flux3_ai_prompt_tools/validator.rb +368 -0
- data/lib/flux3_ai_prompt_tools/version.rb +5 -0
- data/lib/flux3_ai_prompt_tools.rb +51 -0
- data/test/assembler_test.rb +115 -0
- data/test/cli_test.rb +168 -0
- data/test/helper.rb +30 -0
- data/test/manifest_test.rb +112 -0
- data/test/packaging_test.rb +84 -0
- data/test/reader_test.rb +71 -0
- data/test/validator_test.rb +135 -0
- metadata +80 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 458cf2f3b6fa96e5d615158ec3ebd732850b9e0ef1637ea9f94948072167a1ee
|
|
4
|
+
data.tar.gz: 2d5279558f4f02efe4f75d0812d5c7bcda6e39c7724ea6fe63d03d07a0408d85
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 86572ec322850a4ebfe673cafaccdbeec4a4c3bb4eaa7bf9fe7f1b4d0689c2101f690f47dbebe3babfb024a0a31eec73169a7880be47515b47b69bc9173a1548
|
|
7
|
+
data.tar.gz: 438c501aea1204264bce8ff7348e0ff5718c9ca9993469cb0b85adfabc25e17d06e9ffc1230ada6d027b12e422bc2e24c3b0800484520ce53a38b2390ddfcc92
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
Initial release of the offline prompt assembler.
|
|
6
|
+
|
|
7
|
+
- Validate structured text-to-image prompt records.
|
|
8
|
+
- Assemble `natural`, `tagged`, and `labeled` prompt text.
|
|
9
|
+
- Write JSON and JSONL batch manifests.
|
|
10
|
+
- Provide the `flux3-prompt` executable and a Ruby library API.
|
|
11
|
+
- Ship local writing presets, a JSON Schema description, examples, and tests.
|
|
12
|
+
|
|
13
|
+
This version does not generate images and does not make network calls.
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Andy Wang
|
|
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,135 @@
|
|
|
1
|
+
# flux3_ai_prompt_tools
|
|
2
|
+
|
|
3
|
+
`flux3_ai_prompt_tools` is an offline Ruby library and command-line tool by Andy Wang. It turns structured prompt specifications into text-to-image prompt strings and batch manifests. The project homepage is [https://flux3-ai.com/](https://flux3-ai.com/).
|
|
4
|
+
|
|
5
|
+
The gem checks the fields you provide, rejects malformed records, and writes a deterministic prompt plus a separate negative prompt. It can read one JSON object, a JSON array, or JSONL, and it can write a JSON document or JSONL. Nothing in this process contacts a model host. The package does not generate images, videos, or audio, and it does not contain an API client.
|
|
6
|
+
|
|
7
|
+
This is an independent utility. It is not a Black Forest Labs product, it is not an official FLUX client, and it is not the `flux_3_org` gem.
|
|
8
|
+
|
|
9
|
+
## What it does
|
|
10
|
+
|
|
11
|
+
A record has a required `subject` and optional art-direction fields: `scene`, `medium`, `style`, `lighting`, `composition`, `camera`, `palette`, `mood`, `details`, and `negatives`. Optional manifest metadata is `aspect_ratio`, `seed`, `steps`, `guidance`, `id`, and `note`. A `preset` fills a few of the art-direction fields with a local writing template. Your own values replace the template. Set a field to `null` to clear a template value.
|
|
12
|
+
|
|
13
|
+
Three layouts are built in.
|
|
14
|
+
|
|
15
|
+
`natural` writes short sentences such as `Lighting: soft window light.` The assembler does not change your capitalization. If a sentence already ends with `.`, `!`, or `?`, it leaves that ending in place.
|
|
16
|
+
|
|
17
|
+
`tagged` joins the same fields with commas. A weighted detail is rendered as `(phrase:1.2)`. Commas that you typed inside a field are not escaped, so the labeled layout is the safer choice when a field itself contains commas.
|
|
18
|
+
|
|
19
|
+
`labeled` writes one `Label: value` line per filled field. Empty fields are skipped.
|
|
20
|
+
|
|
21
|
+
Weights are accepted only on details, from 0.1 through 2.0, with at most two decimal places. A weight of 1 is unmarked. The negative list is not pasted into the positive prompt. It is returned in `negative_prompt`. The `note` field is copied into the manifest and is never inserted into either prompt. `seed`, `steps`, `guidance`, and `aspect_ratio` are stored under `parameters` for your own bookkeeping. This gem does not send them to a renderer and does not claim that any model will honor them.
|
|
22
|
+
|
|
23
|
+
Batch mode assigns ids such as `item-0001` when you omit `id`. It rejects duplicate ids. It also rejects a later record that repeats the same prompt, negative prompt, layout, and parameters. Use a different seed, or change the wording, when you want more than one job from similar text. `--keep-going` still writes a manifest when some records are bad. Those records have `"ok": false`, a null prompt, and an `errors` array. Without that flag the command writes nothing and exits with an error. Record order is the input order.
|
|
24
|
+
|
|
25
|
+
Validation covers types, blank strings, control characters, Unicode line separators, UTF-8, unknown fields, preset names, and aspect ratios whose sides are integers from 1 through 64. Seeds must be integers in the safe JSON range, 0 through 9007199254740991. Steps must be integers from 1 through 150. Guidance must be a number from 0 through 30 with at most two decimal places. Assembled text must fit the character budget. The defaults are 2000 characters for the prompt and 1000 for the negative prompt. Counts are Unicode characters, not model tokens. Input larger than 5,000,000 bytes, or a batch larger than 5000 records, is refused. Unknown JSON keys are errors, so a typo is not silently dropped and is not inserted into the prompt.
|
|
26
|
+
|
|
27
|
+
The shipped presets are `product_still`, `portrait`, `environment`, and `graphic_poster`. They are short English writing templates stored in the gem. They are not model defaults and they are not account settings. `flux3-prompt schema` prints a JSON Schema description of one record. The validator in this gem is the authority if a document passes a looser third-party schema check. The schema is printed locally. The tool does not download it.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
The library needs Ruby 3.1 or newer. Runtime code uses the standard library only. There are no runtime gem dependencies.
|
|
32
|
+
|
|
33
|
+
From a local gem file:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
gem install --user-install flux3_ai_prompt_tools-0.1.0.gem
|
|
37
|
+
flux3-prompt version
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
After the owner publishes the package to RubyGems, the same gem can be installed with `gem install flux3_ai_prompt_tools`. Installing this repository does not publish the gem and does not create an account.
|
|
41
|
+
|
|
42
|
+
From a checkout of the source, without installing:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
ruby -Ilib bin/flux3-prompt version
|
|
46
|
+
rake test
|
|
47
|
+
gem build flux3_ai_prompt_tools.gemspec
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Library
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
require "flux3_ai_prompt_tools"
|
|
54
|
+
|
|
55
|
+
result = Flux3AiPromptTools.assemble(
|
|
56
|
+
{
|
|
57
|
+
"subject" => "a matte cream ceramic pour-over kettle",
|
|
58
|
+
"scene" => "on a walnut table",
|
|
59
|
+
"preset" => "product_still",
|
|
60
|
+
"negatives" => ["text", "watermark"],
|
|
61
|
+
"aspect_ratio" => "4:5",
|
|
62
|
+
"seed" => 1204
|
|
63
|
+
},
|
|
64
|
+
layout: "natural"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
result.prompt
|
|
68
|
+
result.negative_prompt
|
|
69
|
+
result.to_h
|
|
70
|
+
|
|
71
|
+
issues = Flux3AiPromptTools.validate({ "style" => "quiet" })
|
|
72
|
+
manifest = Flux3AiPromptTools.build_manifest(
|
|
73
|
+
[
|
|
74
|
+
{ "subject" => "a red bowl" },
|
|
75
|
+
{ "subject" => "a blue cup", "layout" => "tagged" }
|
|
76
|
+
]
|
|
77
|
+
)
|
|
78
|
+
File.write("prompts.jsonl", manifest.to_jsonl)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`assemble` raises `Flux3AiPromptTools::ValidationError` when the record is invalid or the assembled text exceeds its budget. `validate` returns an array of issues and does not raise. An empty array means the record is usable. `build_manifest` raises unless you pass `keep_going: true`. `load_file` reads a JSON or JSONL file into an array of hashes, which you can pass to `build_manifest`. Symbol keys are accepted from Ruby. A JSON null clears a preset field instead of failing the type check.
|
|
82
|
+
|
|
83
|
+
## Command line
|
|
84
|
+
|
|
85
|
+
Print one prompt:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
flux3-prompt assemble \
|
|
89
|
+
--preset product_still \
|
|
90
|
+
--subject "a matte cream ceramic pour-over kettle" \
|
|
91
|
+
--scene "on a walnut table" \
|
|
92
|
+
--negative text \
|
|
93
|
+
--negative watermark \
|
|
94
|
+
--aspect-ratio 4:5 \
|
|
95
|
+
--seed 1204
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Add `--manifest` to print the JSON record instead of the prompt text. Add `--layout tagged` or `--layout labeled` to change the wording. The plain command prints only the positive prompt, so a shell pipeline can capture it. Use `--manifest` when you also need the negative prompt, the parameters, and the character counts.
|
|
99
|
+
|
|
100
|
+
Check a file, including the examples in this repository:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
flux3-prompt validate examples/kettle.json
|
|
104
|
+
flux3-prompt batch examples/batch.jsonl --format jsonl -o prompts.jsonl
|
|
105
|
+
flux3-prompt schema
|
|
106
|
+
flux3-prompt presets
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`batch` accepts a JSON object, a JSON array, or JSONL. Use `-` to read stdin. `--input-format` may be `auto`, `json`, or `jsonl`. A file extension of `.json` or `.jsonl` selects the matching reader when the format is `auto`. A UTF-8 byte order mark at the start of the file is ignored. Blank lines in JSONL are ignored. A broken JSONL line stops the whole file, because a partial parse would drop the rest of the batch without telling you where the document became unreadable.
|
|
110
|
+
|
|
111
|
+
`--format` chooses the output. A path ending in `.jsonl` or `.json` selects that format when `--format` is omitted. Output written to a file replaces that file through a temporary file in the same directory. Stdout defaults to JSONL, one record per line, and each line includes the generator name and version. The JSON document form adds a top-level `ok` flag and the default layout around the same records.
|
|
112
|
+
|
|
113
|
+
Exit status is `0` for success, `1` when the input or a record is invalid, and `2` when the command line itself is invalid. In `--keep-going` mode a batch with any failed record is still written, and the exit status is `1`.
|
|
114
|
+
|
|
115
|
+
## Tests
|
|
116
|
+
|
|
117
|
+
The suite is Minitest. It does not need network access:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
rake test
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The packaged gem includes the library, the executable, the tests, the examples, this README, the changelog, and the MIT license.
|
|
124
|
+
|
|
125
|
+
## Limitations
|
|
126
|
+
|
|
127
|
+
The assembler is a formatter. It does not tokenize a prompt the way any particular model does, and a prompt that passes the character budget can still be too long, too vague, or rejected by a downstream service. Passing `aspect_ratio`, `seed`, `steps`, or `guidance` only stores those values in the manifest. It does not resize a canvas, set a random seed, or choose a sampler.
|
|
128
|
+
|
|
129
|
+
Tagged output does not escape commas or parentheses inside a field. Very large weights and more than two decimal places are rejected rather than rounded silently. The same prompt with the same parameters is treated as a duplicate job so an accidental double paste is visible. That check is exact string equality after assembly, not a similarity score.
|
|
130
|
+
|
|
131
|
+
The tool loads a whole batch into memory and writes the manifest in one pass. It is aimed at authoring batches of thousands of records, not at multi-gigabyte streams. Presets are English templates. The natural and labeled layouts do not translate text. There is no image upload, no preview, and no retry against a generator, because this package never opens a network connection and does not read credentials, tokens, or environment secrets. Publishing the gem, choosing a public source repository, and signing in to RubyGems are left to the owner.
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
MIT. Copyright (c) 2026 Andy Wang.
|
data/Rakefile
ADDED
data/bin/flux3-prompt
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "flux3_ai_prompt_tools"
|
|
5
|
+
require "flux3_ai_prompt_tools/cli"
|
|
6
|
+
|
|
7
|
+
begin
|
|
8
|
+
exit Flux3AiPromptTools::CLI.start(ARGV)
|
|
9
|
+
rescue Flux3AiPromptTools::UsageError => e
|
|
10
|
+
warn "error: #{e.message}"
|
|
11
|
+
exit 2
|
|
12
|
+
rescue Flux3AiPromptTools::ValidationError => e
|
|
13
|
+
e.issues.each do |issue|
|
|
14
|
+
warn "error: #{issue.path} #{issue.code}: #{issue.message}"
|
|
15
|
+
end
|
|
16
|
+
exit 1
|
|
17
|
+
rescue Flux3AiPromptTools::InputError => e
|
|
18
|
+
warn "error: #{e.message}"
|
|
19
|
+
exit 1
|
|
20
|
+
rescue Flux3AiPromptTools::Error => e
|
|
21
|
+
warn "error: #{e.message}"
|
|
22
|
+
exit 1
|
|
23
|
+
end
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
{"id":"kettle-01","preset":"product_still","subject":"a matte cream ceramic pour-over kettle","scene":"on a walnut table in a quiet kitchen","palette":"warm walnut and matte cream","negatives":["text","watermark"],"aspect_ratio":"4:5","seed":1204}
|
|
2
|
+
{"id":"cup-02","preset":"portrait","subject":"a person holding a small blue cup","lighting":"north window light","negatives":["distorted hands"],"aspect_ratio":"3:4","seed":1205,"layout":"tagged"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "kettle-01",
|
|
3
|
+
"preset": "product_still",
|
|
4
|
+
"subject": "a matte cream ceramic pour-over kettle",
|
|
5
|
+
"scene": "on a walnut table in a quiet kitchen",
|
|
6
|
+
"style": "editorial product photography",
|
|
7
|
+
"palette": "warm walnut, matte cream, and muted sage",
|
|
8
|
+
"details": [
|
|
9
|
+
"steam rising from the spout",
|
|
10
|
+
{"text": "visible wood grain", "weight": 1.2}
|
|
11
|
+
],
|
|
12
|
+
"negatives": ["text", "watermark", "extra handle"],
|
|
13
|
+
"aspect_ratio": "4:5",
|
|
14
|
+
"seed": 1204,
|
|
15
|
+
"note": "Local reminder only. This file is not sent anywhere."
|
|
16
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/flux3_ai_prompt_tools/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "flux3_ai_prompt_tools"
|
|
7
|
+
spec.version = Flux3AiPromptTools::VERSION
|
|
8
|
+
spec.authors = ["Andy Wang"]
|
|
9
|
+
spec.summary = "Validate structured inputs and assemble offline text-to-image prompt manifests."
|
|
10
|
+
spec.description = <<~TEXT
|
|
11
|
+
Offline Ruby library and command-line tool that validates structured prompt
|
|
12
|
+
specifications and assembles text-to-image prompts into JSON and JSONL
|
|
13
|
+
manifests. It does not generate images, call a model API, or send data
|
|
14
|
+
over the network. It is an independent utility by Andy Wang and is not a
|
|
15
|
+
Black Forest Labs product, an official FLUX client, or the flux_3_org gem.
|
|
16
|
+
TEXT
|
|
17
|
+
spec.homepage = "https://flux3-ai.com/"
|
|
18
|
+
spec.license = "MIT"
|
|
19
|
+
spec.required_ruby_version = ">= 3.1.0"
|
|
20
|
+
|
|
21
|
+
spec.files = Dir.chdir(__dir__) do
|
|
22
|
+
Dir[
|
|
23
|
+
"lib/**/*.rb",
|
|
24
|
+
"bin/flux3-prompt",
|
|
25
|
+
"test/**/*.rb",
|
|
26
|
+
"examples/**/*",
|
|
27
|
+
"README.md",
|
|
28
|
+
"LICENSE",
|
|
29
|
+
"CHANGELOG.md",
|
|
30
|
+
"Rakefile",
|
|
31
|
+
"flux3_ai_prompt_tools.gemspec"
|
|
32
|
+
].select { |path| File.file?(path) }
|
|
33
|
+
end
|
|
34
|
+
spec.bindir = "bin"
|
|
35
|
+
spec.executables = ["flux3-prompt"]
|
|
36
|
+
spec.require_paths = ["lib"]
|
|
37
|
+
|
|
38
|
+
spec.metadata = {
|
|
39
|
+
"homepage_uri" => "https://flux3-ai.com/",
|
|
40
|
+
"allowed_push_host" => "https://rubygems.org"
|
|
41
|
+
}
|
|
42
|
+
end
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
require_relative "errors"
|
|
6
|
+
require_relative "layouts"
|
|
7
|
+
require_relative "numbers"
|
|
8
|
+
require_relative "version"
|
|
9
|
+
|
|
10
|
+
module Flux3AiPromptTools
|
|
11
|
+
# One assembled record, successful or withheld after a budget or batch error.
|
|
12
|
+
class Result
|
|
13
|
+
attr_reader :id, :ok, :prompt, :negative_prompt, :layout, :parameters, :counts, :note, :errors
|
|
14
|
+
|
|
15
|
+
def initialize(id:, ok:, prompt:, negative_prompt:, layout:, parameters:, counts:, note:, errors:)
|
|
16
|
+
@id = id
|
|
17
|
+
@ok = ok
|
|
18
|
+
@prompt = prompt
|
|
19
|
+
@negative_prompt = negative_prompt
|
|
20
|
+
@layout = layout
|
|
21
|
+
@parameters = parameters.dup.freeze
|
|
22
|
+
@counts = counts.dup.freeze
|
|
23
|
+
@note = note
|
|
24
|
+
@errors = errors.dup.freeze
|
|
25
|
+
freeze
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def to_h
|
|
29
|
+
{
|
|
30
|
+
"generator" => "flux3_ai_prompt_tools/#{VERSION}",
|
|
31
|
+
"id" => id,
|
|
32
|
+
"ok" => ok,
|
|
33
|
+
"prompt" => prompt,
|
|
34
|
+
"negative_prompt" => negative_prompt,
|
|
35
|
+
"layout" => layout,
|
|
36
|
+
"parameters" => parameters,
|
|
37
|
+
"counts" => counts,
|
|
38
|
+
"note" => note,
|
|
39
|
+
"errors" => errors.map(&:to_h)
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Builds deterministic prompt text. It does not call a model.
|
|
45
|
+
class Assembler
|
|
46
|
+
def self.assemble(spec, layout:, limits:, raise_on_error: true)
|
|
47
|
+
unless spec.is_a?(Specification)
|
|
48
|
+
raise ArgumentError, "spec must be a Flux3AiPromptTools::Specification"
|
|
49
|
+
end
|
|
50
|
+
unless limits.is_a?(Limits)
|
|
51
|
+
raise ArgumentError, "limits must be a Flux3AiPromptTools::Limits"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
chosen = spec.layout || layout
|
|
55
|
+
unless chosen.is_a?(String) && Layouts::NAMES.include?(chosen)
|
|
56
|
+
raise ArgumentError, "layout must be one of: #{Layouts::NAMES.join(', ')}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
prompt = render(spec, chosen)
|
|
60
|
+
negative = spec.negatives.join(", ")
|
|
61
|
+
issues = []
|
|
62
|
+
if prompt.length > limits.max_prompt_chars
|
|
63
|
+
issues << Issue.new(
|
|
64
|
+
"/prompt",
|
|
65
|
+
"too_long",
|
|
66
|
+
"assembled prompt is #{prompt.length} characters; limit is #{limits.max_prompt_chars}"
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
if negative.length > limits.max_negative_chars
|
|
70
|
+
issues << Issue.new(
|
|
71
|
+
"/negative_prompt",
|
|
72
|
+
"too_long",
|
|
73
|
+
"assembled negative prompt is #{negative.length} characters; limit is #{limits.max_negative_chars}"
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
result = Result.new(
|
|
78
|
+
id: spec.id,
|
|
79
|
+
ok: issues.empty?,
|
|
80
|
+
prompt: issues.empty? ? prompt : nil,
|
|
81
|
+
negative_prompt: issues.empty? ? negative : nil,
|
|
82
|
+
layout: chosen,
|
|
83
|
+
parameters: {
|
|
84
|
+
"aspect_ratio" => spec.aspect_ratio,
|
|
85
|
+
"seed" => spec.seed,
|
|
86
|
+
"steps" => spec.steps,
|
|
87
|
+
"guidance" => spec.guidance
|
|
88
|
+
},
|
|
89
|
+
counts: {
|
|
90
|
+
"prompt_chars" => prompt.length,
|
|
91
|
+
"negative_chars" => negative.length,
|
|
92
|
+
"details" => spec.details.length,
|
|
93
|
+
"negatives" => spec.negatives.length
|
|
94
|
+
},
|
|
95
|
+
note: spec.note,
|
|
96
|
+
errors: issues
|
|
97
|
+
)
|
|
98
|
+
raise ValidationError, result.errors if raise_on_error && !result.ok
|
|
99
|
+
|
|
100
|
+
result
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def self.render(spec, layout)
|
|
104
|
+
case layout
|
|
105
|
+
when "natural" then render_natural(spec)
|
|
106
|
+
when "tagged" then render_tagged(spec)
|
|
107
|
+
when "labeled" then render_labeled(spec)
|
|
108
|
+
else
|
|
109
|
+
raise ArgumentError, "unknown layout #{layout.inspect}"
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
private_class_method :render
|
|
113
|
+
|
|
114
|
+
def self.render_natural(spec)
|
|
115
|
+
parts = []
|
|
116
|
+
opening = spec.scene ? "#{spec.subject}, #{spec.scene}" : spec.subject
|
|
117
|
+
parts << sentence(opening)
|
|
118
|
+
|
|
119
|
+
look = []
|
|
120
|
+
look << spec.medium if spec.medium
|
|
121
|
+
look << "#{spec.style} style" if spec.style
|
|
122
|
+
parts << sentence(look.join(", ")) if look.any?
|
|
123
|
+
|
|
124
|
+
[
|
|
125
|
+
["Lighting", spec.lighting],
|
|
126
|
+
["Composition", spec.composition],
|
|
127
|
+
["Camera", spec.camera],
|
|
128
|
+
["Palette", spec.palette],
|
|
129
|
+
["Mood", spec.mood]
|
|
130
|
+
].each do |label, value|
|
|
131
|
+
parts << sentence("#{label}: #{value}") if value
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
if spec.details.any?
|
|
135
|
+
rendered = spec.details.map { |detail| render_detail_words(detail) }
|
|
136
|
+
parts << sentence("Details: #{rendered.join('; ')}")
|
|
137
|
+
end
|
|
138
|
+
parts.join(" ")
|
|
139
|
+
end
|
|
140
|
+
private_class_method :render_natural
|
|
141
|
+
|
|
142
|
+
def self.render_tagged(spec)
|
|
143
|
+
tokens = []
|
|
144
|
+
tokens << spec.subject
|
|
145
|
+
tokens << spec.scene if spec.scene
|
|
146
|
+
tokens << spec.medium if spec.medium
|
|
147
|
+
tokens << spec.style if spec.style
|
|
148
|
+
tokens << spec.lighting if spec.lighting
|
|
149
|
+
tokens << spec.composition if spec.composition
|
|
150
|
+
tokens << spec.camera if spec.camera
|
|
151
|
+
tokens << spec.palette if spec.palette
|
|
152
|
+
tokens << spec.mood if spec.mood
|
|
153
|
+
spec.details.each do |detail|
|
|
154
|
+
tokens << (detail.neutral? ? detail.text : "(#{detail.text}:#{Numbers.format_hundredths(detail.hundredths)})")
|
|
155
|
+
end
|
|
156
|
+
tokens.join(", ")
|
|
157
|
+
end
|
|
158
|
+
private_class_method :render_tagged
|
|
159
|
+
|
|
160
|
+
def self.render_labeled(spec)
|
|
161
|
+
rows = [["Subject", spec.subject]]
|
|
162
|
+
rows << ["Scene", spec.scene]
|
|
163
|
+
rows << ["Medium", spec.medium]
|
|
164
|
+
rows << ["Style", spec.style]
|
|
165
|
+
rows << ["Lighting", spec.lighting]
|
|
166
|
+
rows << ["Composition", spec.composition]
|
|
167
|
+
rows << ["Camera", spec.camera]
|
|
168
|
+
rows << ["Palette", spec.palette]
|
|
169
|
+
rows << ["Mood", spec.mood]
|
|
170
|
+
if spec.details.any?
|
|
171
|
+
rows << ["Details", spec.details.map { |detail| render_detail_words(detail) }.join("; ")]
|
|
172
|
+
end
|
|
173
|
+
rows.select { |_label, value| value }.map { |label, value| "#{label}: #{value}" }.join("\n")
|
|
174
|
+
end
|
|
175
|
+
private_class_method :render_labeled
|
|
176
|
+
|
|
177
|
+
def self.render_detail_words(detail)
|
|
178
|
+
return detail.text if detail.neutral?
|
|
179
|
+
|
|
180
|
+
"#{detail.text} (emphasis #{Numbers.format_hundredths(detail.hundredths)})"
|
|
181
|
+
end
|
|
182
|
+
private_class_method :render_detail_words
|
|
183
|
+
|
|
184
|
+
def self.sentence(text)
|
|
185
|
+
stripped = text.strip
|
|
186
|
+
return stripped if stripped.match?(/[.!?]["']?\z/)
|
|
187
|
+
|
|
188
|
+
"#{stripped}."
|
|
189
|
+
end
|
|
190
|
+
private_class_method :sentence
|
|
191
|
+
end
|
|
192
|
+
end
|