presents estimated time to run before running

This commit is contained in:
2026-08-22 22:33:17 -06:00
parent 74a02504f4
commit a931967dbd
3 changed files with 66 additions and 38 deletions
+28 -18
View File
@@ -5,31 +5,41 @@ A simple tool for using local LLMs to generate cover briefs for ebooks.
- A Mac mini M4 base model (original version) or better M-series computer - A Mac mini M4 base model (original version) or better M-series computer
- Ollama and `gemma4:12b-mlx` - Ollama and `gemma4:12b-mlx`
- LuaJIT - LuaJIT
- calibre
## Usage ## Usage
1. Use a tool like [audiobook-creator](https://github.com/prakharsr/audiobook-creator) to extract the text of an audiobook. 1. Create `config.json` if necessary (see format below).
2. Place it in `extracted_texts/TITLE by AUTHOR.txt`. (The format of this doesn't matter except that the file name sans extension will be used in the output.) 2. Run `make_cover_briefs.lua` once to create necessary directories.
3. Run `make_cover_briefs.lua`. 3. Put ebooks in `raw_ebooks`.
4. Cover briefs will be placed in `cover_briefs`. 4. Run `make_cover_briefs.lua`
5. A prompt ready to paste directly into Gemini will be in `for_gemini`. An extra border is prompted for to make cropping easy to remove the Gemini watermark. (There are other small changes to increase likelihood of Gemini following the prompt correctly and making a good output.)
Cover briefs will be placed in `cover_briefs`. A prompt ready to paste directly
into Gemini will be in `for_gemini`. An extra border is prompted for to make
cropping easy to remove the Gemini watermark. (There are other small changes to
increase likelihood of Gemini following the prompt correctly and making a good
output.)
### Gemini Issues ### Gemini Issues
None of this works unless you **disable Gemini's personalization/memory**, because it pollutes context None of this works unless you **disable Gemini's personalization/memory**,
extremely badly and will randomly add elements and rejections from different conversations everywhere. because it pollutes context extremely badly and will randomly add elements and
rejections from different conversations everywhere.
If Gemini rejects the prompt, adding <code id="d2651e">Remove any elements that may go against guidelines before generating the image.</code> If Gemini rejects the prompt, adding
<code id="d2651e">Remove any elements that may go against guidelines before generating the image.</code>
<button onclick="navigator.clipboard.writeText(d2651e.textContent)">⧉</button> <button onclick="navigator.clipboard.writeText(d2651e.textContent)">⧉</button>
to the opening paragraph can fix that issue. to the opening paragraph can fix that issue.
- This suddenly got a lot less effective. I recommend instead telling it to rewrite the prompt and then - This suddenly got a lot less effective. I recommend instead telling it to
in a separate conversation, use that instead. I also recommend one retry before modifying it. rewrite the prompt and then in a separate conversation, use that instead. I
also recommend one retry before modifying it.
Gemini is really bad about adding hardcover seams. Despite being instructed not to, it shows up sometimes. Gemini is really bad about adding hardcover seams. Despite being instructed not
More strong prompting breaks other requested features, so the best way to deal with it is to request its to, it shows up sometimes. More strong prompting breaks other requested
removal after the initial image is generated. features, so the best way to deal with it is to request its removal after the
initial image is generated.
The local model sometimes completely forgets critical details of characters, The local model sometimes completely forgets critical details of characters, so
so it's important you have some familiarity with the work before running the generator it's important you have some familiarity with the work before running the
to check accuracy. generator to check accuracy.
## Tasks ## Tasks
- [ ] Add timestamps to when a prompt is sent. - [ ] Add timestamps to when a prompt is sent.
@@ -37,9 +47,9 @@ to check accuracy.
- [ ] option to delete input files or intermediate files at end of each operation step - [ ] option to delete input files or intermediate files at end of each operation step
- [x] instead of placeholder directories, `mkdir -p` should be used to only make them when necessary - [x] instead of placeholder directories, `mkdir -p` should be used to only make them when necessary
- [x] make script resumable instead of having to restart per book - [x] make script resumable instead of having to restart per book
- [ ] dry run to estimate total time before running full script, print starting ETA - [x] dry run to estimate total time before running full script, print starting ETA
- [x] Build a list of files to work on and current states before doing anything by looping over source directories - [x] Build a list of files to work on and current states before doing anything by looping over source directories
- [x] Combine everything into one script to manage things - [x] Combine everything into one script to manage things
- [ ] Allow specifiying a filter on what to process - [ ] Allow specifying a filter on what to process
- [ ] Allow changing the maximum bytes per prompt dynamically (run smaller while I'm doing other things, run full while I'm asleep) - [ ] Allow changing the maximum bytes per prompt dynamically (run smaller while I'm doing other things, run full while I'm asleep)
- [ ] specify config format - [ ] specify config format
+19 -17
View File
@@ -1,34 +1,36 @@
local timing = { local timing = {}
entries = {} local entries = {}
}
local function to_minutes(t) -- floored to tenths function timing.seconds_to_minutes(seconds) -- floored to tenths
return math.floor( t / 60 * 10 ) / 10 return math.floor( seconds / 60 * 10 ) / 10
end end
function timing:start() function timing.start()
-- if not self then self = {} end entries[#entries + 1] = { start_time = os.time() }
self.entries[#self.entries + 1] = { start_time = os.time() }
end end
function timing:stop() function timing.stop()
local entry = self.entries[#self.entries] local entry = entries[#entries]
entry.stop_time = os.time() entry.stop_time = os.time()
entry.delta = entry.stop_time - entry.start_time
return to_minutes(entry.stop_time - entry.start_time) -- return delta return timing.seconds_to_minutes(entry.delta)
end end
function timing:print_report() function timing.print_report()
local minimum, maximum, sum = math.huge, -math.huge, 0 local minimum, maximum, sum = math.huge, -math.huge, 0
for _, data in pairs(self.entries) do for _, data in pairs(entries) do
local delta = data.stop_time - data.start_time local delta = data.delta
if delta > maximum then maximum = delta end if delta > maximum then maximum = delta end
if delta < minimum then minimum = delta end if delta < minimum then minimum = delta end
sum = sum + delta sum = sum + delta
end end
print("Total: " .. to_minutes(sum) .. " minutes.")
print("Averge: " .. to_minutes(sum / #self.entries) .. " minutes. Fastest: " print("Total: " .. timing.seconds_to_minutes(sum) .. " minutes.")
.. to_minutes(minimum) .. " minutes. Slowest: " .. to_minutes(maximum) .. " minutes.") print("Averge: " .. timing.seconds_to_minutes(sum / #entries)
.. " minutes. Fastest: " .. timing.seconds_to_minutes(minimum)
.. " minutes. Slowest: " .. timing.seconds_to_minutes(maximum)
.. " minutes.")
end end
return timing return timing
+19 -3
View File
@@ -50,11 +50,11 @@ local send_prompt = function(text, model)
file:write(text) file:write(text)
end) end)
timing:start() timing.start()
-- word wrap breaks the raw output badly, so I need to implement my own for terminal output somehow -- word wrap breaks the raw output badly, so I need to implement my own for terminal output somehow
local output = utility.capture_safe("cat " .. tmp_file_name:enquote() .. " | ollama run " .. (model or default_model) .. " --nowordwrap") local output = utility.capture_safe("cat " .. tmp_file_name:enquote() .. " | ollama run " .. (model or default_model) .. " --nowordwrap")
os.execute("ollama stop " .. (model or default_model)) -- NOTE this makes things slower, but more stable os.execute("ollama stop " .. (model or default_model)) -- NOTE this makes things slower, but more stable
local delta = timing:stop() local delta = timing.stop()
print("Took " .. delta .. " minutes.") print("Took " .. delta .. " minutes.")
os.execute("rm " .. tmp_file_name) os.execute("rm " .. tmp_file_name)
@@ -267,14 +267,30 @@ local generate_cover_briefs = function(books)
end end
end end
local estimate_time_required = function(books)
local sum, count = 0, 0
for name, data in pairs(books) do
if data.cover_brief_file then return end
sum = sum + 60 * (2 + data.extracted_file_size/(config.llm_covers.maximum_bytes/7.5))
count = count + 1
end
print(count .. " cover briefs to generate. Estimated time: "
.. timing.seconds_to_minutes(sum) .. " minutes.")
end
make_directories() make_directories()
local books = get_books() local books = get_books()
convert_ebooks(books) convert_ebooks(books)
estimate_time_required(books)
print("")
generate_cover_briefs(books) generate_cover_briefs(books)
print("") print("")
timing:print_report() timing.print_report()
-- ebook_file, extracted_file, extracted_file_size, -- ebook_file, extracted_file, extracted_file_size,
-- intermediate_file, condensed_intermediate_file, cover_brief_file -- intermediate_file, condensed_intermediate_file, cover_brief_file