From 9671a712a18e662172f16b588aee668f42ae6cf6 Mon Sep 17 00:00:00 2001 From: Tangent Date: Mon, 15 Jun 2026 02:03:58 -0600 Subject: [PATCH] initial --- .gitignore | 7 ++ cover.lua | 181 +++++++++++++++++++++++++++++++++++++++ cover_briefs/.gitkeep | 0 extracted_texts/.gitkeep | 0 4 files changed, 188 insertions(+) create mode 100644 .gitignore create mode 100755 cover.lua create mode 100644 cover_briefs/.gitkeep create mode 100644 extracted_texts/.gitkeep diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8f38cf2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +extracted_texts/** +!extracted_texts/.gitkeep +intermediates/** +!intermediates/.gitkeep +cover_briefs/** +!cover_briefs/.gitkeep diff --git a/cover.lua b/cover.lua new file mode 100755 index 0000000..0955b62 --- /dev/null +++ b/cover.lua @@ -0,0 +1,181 @@ +#!/usr/bin/env luajit + +package.path = (arg[0]:match("@?(.*/)") or arg[0]:match("@?(.*\\)")) .. "lib" .. package.config:sub(1, 1) .. "?.lua;" .. package.path +local utility = require "utility" +local json = utility.require("dkjson") + +local default_model = "gemma4:12b-mlx" +local maximum_bytes = 20000 + +local partial_prompt = [[You are extracting information for a later cover design process. + +This is only one section of a larger story. + +Do NOT attempt to design a cover. +Do NOT decide what should appear on the cover. +Do NOT write a summary. + +Extract only information that may be useful when creating a cover after all story sections have been analyzed. + +Return: + +GENRE: +- up to 5 items + +SETTING: +- locations +- environments +- time period + +CHARACTERS: +- names +- physical descriptions +- distinctive visual traits + +CREATURES: +- notable creatures + +OBJECTS: +- important recurring items + +VISUAL MOTIFS: +- recurring imagery +- symbols +- repeated visual elements + +THEMES: +- major themes + +MOOD: +- emotional tone + +COLORS: +- colors strongly associated with scenes or imagery + +MEMORABLE VISUAL SCENES: +- 3-10 visually striking moments + +CONFIDENCE: +- how central each item appears to be + +]] + +local cover_brief_prompt = [[Generate a cover brief from the following: + +]] + +local segmented_cover_brief_prompt = [[Generate a single cover brief. + +Prefer elements that: +- appear repeatedly across chunks +- have highest confidence +- best represent the entire story + +Avoid minor plot events. +Avoid spoilers. + +]] + +local read_all = function(file_name) + return utility.open(file_name, "r", function(file) + return file:read("*all") + end) +end + +local timings = {} +local timing_report = function() + local minimum, maximum, sum = math.huge, 0, 0 + for _, delta in ipairs(timings) do + if delta > maximum then maximum = delta end + if delta < minimum then minimum = delta end + sum = sum + delta + end + print("") + print("Prompts took a total of " .. sum .. " minutes.") + print("Average: " .. math.floor(sum / #timings) .. " minutes. Fastest: " .. minimum .. " minutes. Slowest: " .. maximum .. " minutes.") +end + +local strip_reasoning = function(text, reasoning_lines) + if not reasoning_lines then reasoning_lines = {} end + local tab = text:split("\n") + table.remove(tab, 1) -- remove "Thinking..." + + while true do + local done = tab[1] == "...done thinking." + local line = table.remove(tab, 1) + if done then + table.remove(tab, 1) -- remove newline after end of thinking + return table.concat(tab, "\n") + elseif #tab < 1 then + return text -- no reasoning output + else + reasoning_lines[#reasoning_lines + 1] = line -- export thinking lines + end + end +end + +local send_prompt = function(text, model) + if type(text) == "table" then text = table.concat(text, "\n") end + + local tmp_file_name = utility.tmp_file_name() + utility.open(tmp_file_name, "w", function(file) + file:write(text) + end) + + local start_time = os.time() + -- 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 delta = math.floor( (os.time() - start_time) / 60 * 10 ) / 10 + timings[#timings + 1] = delta + os.execute("rm " .. tmp_file_name) + if not output then error("ollama failed to generate output") end + output = output:sub(1, -2) -- strip extra newline from utility.capture_safe + + output = strip_reasoning(output) + + return output +end + +local tree +tree = function(path, fn) + utility.list(path or ".", function(path_name) + if path_name == ".git" then return end + if path_name == ".gitkeep" then return end + if utility.is_file(path_name) then + fn(path_name) + else + tree(path .. utility.path_separator .. path_name, fn) + end + end) +end + +tree("extracted_texts", function(file_name) + local path, name, extension = utility.split_path_components(file_name) + local text = read_all(file_name) + local result + + if #text > maximum_bytes then + local outputs = {} + -- TODO check if this would work just as well starting from 1 instead.. + for i = 0, #text/maximum_bytes do + print((i + 1) .. " of " .. math.floor(#text/maximum_bytes + 1)) + local piece = text:sub(i * maximum_bytes, (i + 1) * maximum_bytes - 1) + result = send_prompt(partial_prompt .. piece) + outputs[#outputs + 1] = result + + -- temporary + utility.open("intermediates/" .. (i + 1) .. ".txt", "w", function(file) + file:write(result) + file:write("\n") + end) + print(#result .. " characters added to full context.") + end + + text = table.concat(outputs, "\n\n") + end + + result = send_prompt(cover_brief_prompt .. text) + print(result) +end) + +timing_report() diff --git a/cover_briefs/.gitkeep b/cover_briefs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/extracted_texts/.gitkeep b/extracted_texts/.gitkeep new file mode 100644 index 0000000..e69de29