module ComicCore
Constants
- AGENT_MODEL
- DEFAULT_PANEL_COUNT
- MAX_PANEL_COUNT
- MIN_PANEL_COUNT
- PANEL_LINES
- PANEL_MARKER
- PANEL_WIDTH
- STYLE_TITLE
- USAGE
Public Instance Methods
Source
# File lib/comic_core.rb, line 83 def build_comic_prompt(style:, panel_count:, panels:, nick: nil, size: nil) preset = preset_size(size) style_text = style.to_s.strip style_line = style_text.empty? ? "Style: classic comic strip with clear panels." : "Style: #{style_text}." panel_lines = panels.each_with_index.map { |panel, index| "Panel #{index + 1}: #{panel}" } if panels.length < panel_count panel_lines << "Panels #{panels.length + 1}-#{panel_count}: continue the story coherently." end parts = [ "Create a #{panel_count}-panel ASCII comic strip for IRC chat.", "", style_line, "", "Story panels:", *panel_lines, "", "Requirements:", *DrawCore.irc_ascii_requirements(max_width: preset.panel_width, max_lines: preset.panel_lines), "- Exactly #{panel_count} panels.", "- Separate each panel with a line containing only: ---PANEL n--- (n = 1..#{panel_count}).", "- Each panel must be at most #{preset.panel_lines} lines and #{preset.panel_width} characters wide.", "- Requested by IRC nick: #{nick}", "", "Output ONLY the panel markers and coloured ASCII art. No markdown fences, no explanation." ] AiGuard.guard_prompt(parts.join("\n")) end
Source
# File lib/comic_core.rb, line 186 def dispatch(text:, nick:, ai_allowed:, runner: nil, model: AGENT_MODEL) return { success: false, reply: AiAccess.denial_message } unless ai_allowed parsed = parse_command(text) return { success: false, reply: parsed[:error] } if parsed[:error] run_comic( style: parsed[:style], panel_count: parsed[:panel_count], panels: parsed[:panels], nick: nick, runner: runner, model: model, size: parsed[:size] ) end
Source
# File lib/comic_core.rb, line 134 def layout_strip(panel_art_lines:, style: nil, model:, duration_s:, size: nil) preset = preset_size(size) title = style.to_s.strip title = "comic strip" if title.empty? title = "#{title} ยท #{preset.label}" unless preset.id == :normal lines = AsciiDisplaySize.clear_prefix(preset) + [IrcFormat.report_header(tag: "COMIC", title: title, style: STYLE_TITLE)] panel_art_lines.each_with_index do |art_lines, index| lines << IrcFormat.color(14) + "Panel #{index + 1}" + IrcFormat::RESET if art_lines.empty? lines << "(empty panel)" else lines.concat(art_lines) end lines << "-" * preset.panel_width unless index == panel_art_lines.length - 1 end lines << IrcFormat.report_footer("#{panel_art_lines.length} panels", "cursor", model, "#{duration_s}s") lines.join("\n") end
Source
# File lib/comic_core.rb, line 39 def parse_command(text) stripped = text.to_s.strip return { error: USAGE } if stripped.empty? body, size = AsciiDisplaySize.peel_token(stripped) return { error: USAGE } if body.empty? segments = body.split("|").map(&:strip).reject(&:empty?) return { error: USAGE } if segments.empty? first = segments.shift style = nil panel_count = DEFAULT_PANEL_COUNT first_panel = nil if (match = first.match(/\A(?:(?<style>.+?)\s+)?(?<count>\d+)\s+(?<panel>.+)\z/m)) style = match[:style]&.strip panel_count = match[:count].to_i first_panel = match[:panel].strip else first_panel = first.strip end panels = [first_panel, *segments].compact.reject(&:empty?) return { error: USAGE } if panels.empty? panel_count = panel_count.clamp(MIN_PANEL_COUNT, MAX_PANEL_COUNT) if style && AiGuard.system_request?(style) return { error: AiGuard.rejection_message } end panels.each do |panel| return { error: AiGuard.rejection_message } if AiGuard.system_request?(panel) end { style: style, panel_count: panel_count, panels: panels, size: size } end
Source
# File lib/comic_core.rb, line 113 def parse_panels(text, panel_count:, size: nil) preset = preset_size(size) chunks = text.to_s.split(PANEL_MARKER) panels = {} current = nil chunks.each do |chunk| if chunk.match?(/\A\d+\z/) current = chunk.to_i panels[current] = [] elsif current panels[current] ||= [] panels[current] << chunk end end (1..panel_count).map do |index| body = panels[index]&.join || "" DrawCore.extract_art_lines(body, max_width: preset.panel_width, max_lines: preset.panel_lines) end end
Source
# File lib/comic_core.rb, line 30 def preset_size(size) case size when AsciiDisplaySize::Preset size else AsciiDisplaySize.resolve(size) || AsciiDisplaySize::DEFAULT end end
Source
# File lib/comic_core.rb, line 155 def run_comic(style:, panel_count:, panels:, nick: nil, runner: nil, model: AGENT_MODEL, size: nil) preset = preset_size(size) prompt = build_comic_prompt(style: style, panel_count: panel_count, panels: panels, nick: nick, size: preset) result = AiAgent.run_agent(prompt, model: model, runner: runner || AiAgent.method(:default_runner)) unless result.success? message = result.text.to_s.strip return { success: false, reply: message.empty? ? "Comic generation failed." : message } end filtered = AiGuard.filter_output(result.text) if filtered == AiGuard.rejection_message return { success: false, reply: filtered } end panel_art = parse_panels(filtered, panel_count: panel_count, size: preset) if panel_art.all?(&:empty?) return { success: false, reply: "Could not parse comic panels from the AI response." } end { success: true, reply: layout_strip( panel_art_lines: panel_art, style: style, model: result.model || model, duration_s: result.duration_s, size: preset ) } end