Latex Posters
K-DenseCommunityCreated by K-Dense Inc.
- Skill
- Tool integration
- Science and research
- MIT
- Low risk
- Has scripts
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill provides a structured approach to creating research posters for academic and conference settings using LaTeX packages such as beamerposter, tikzposter, and baposter. It emphasizes a workflow that integrates AI-generated schematics and figures with LaTeX templates, focusing on visual hierarchy, readability, and adherence to strict design constraints for large-format printing.
What it does
- Supports creation of research posters using beamerposter, tikzposter, and baposter.
- Provides templates for various poster sizes including A0 and A1.
- Includes a workflow for generating AI-based visual elements with specific constraints.
- Offers automated validation scripts to check for PDF quality and layout issues.
- Provides guidance on typography, color schemes, and visual communication principles.
- Includes helper scripts for schematic generation and poster review.
When to use it
- When preparing research posters for academic conferences or symposia.
- When designing posters that require specific LaTeX-based layout control.
- When converting scientific paper content into a visual poster format.
- When needing to ensure poster content complies with specific size and readability requirements.
Advantages
- Enforces strict design limits to prevent content overflow and ensure readability.
- Integrates directly with LaTeX for professional typesetting of equations and citations.
- Provides a clear, multi-stage workflow from planning to final compilation.
- Includes automated quality control checks for PDF output.
- Supports consistent branding through customizable templates.
Usage examples
- “Create a 3-column A0 research poster template using baposter.”
- The agent initializes a baposter project with the specified column layout and provides the necessary LaTeX structure for content insertion.
- “Generate a simple flowchart for my methods section with 4 steps.”
- The agent uses the provided schematic script to generate a simplified, high-contrast image suitable for a large-format poster.
Install
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/k-dense-ai/latex-posters/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip latex-posters.zip -d .agents/skills
Documentation by K-Dense Inc.
LaTeX Research Posters
Overview
Research posters are a critical medium for scientific communication at conferences, symposia, and academic events. This skill provides comprehensive guidance for creating professional, visually appealing research posters using LaTeX packages. Generate publication-quality posters with proper layout, typography, color schemes, and visual hierarchy.
When to Use This Skill
This skill should be used when:
Creating research posters for conferences, symposia, or poster sessions
Designing academic posters for university events or thesis defenses
Preparing visual summaries of research for public engagement
Converting scientific papers into poster format
Creating template posters for research groups or departments
Designing posters that comply with specific conference size requirements (A0, A1, 36×48", etc.)
Building posters with complex multi-column layouts
Integrating figures, tables, equations, and citations in poster format
AI-Powered Visual Element Generation
STANDARD WORKFLOW: Generate ALL major visual elements using AI before creating the LaTeX poster.
This is the recommended approach for creating visually compelling posters:
Plan all visual elements needed (title, intro, methods, results, conclusions)
Generate each element using scientific-schematics or Nano Banana Pro
Assemble generated images in the LaTeX template
Add text content around the visuals
Target: 60-70% of poster area should be AI-generated visuals, 30-40% text.
Hard limits (do not exceed)
These are limits, not guidelines. Violating them is the single most common cause of a failed poster. Full reasoning, per-graphic-type tables, and worked examples are in references/ai_graphics_for_posters.md.
| Constraint | Limit |
|---|---|
| Elements per AI-generated graphic | 3-4 maximum (3 ideal) |
| Words per graphic | 10 maximum |
| White space per graphic | 50% minimum (60% better) |
| Key numbers / metrics | 120pt+ |
| Labels | 80pt+ |
| Body text on the poster | 24pt+ |
| Content sections (A0) | 5-6 maximum |
| Total words on the poster | 300-800 |
| Figure width | 0.85\linewidth, never 1.0 |
Every graphic prompt must include: POSTER FORMAT for A0, an explicit element or word
count (ONLY 3 icons, 3 words total), a font size (GIANT (120pt+)), 60% white space,
and a viewing distance (readable from 10-12 feet).
Two mandatory review gates. Skipping either is how unreadable posters happen:
Before generating — for each planned graphic, confirm it is 3-4 items, one message, under 10 words, and not a 5+ stage workflow. If not, split it into several graphics.
After generating, before assembly — open each figure at 25% zoom. All text readable, 4 or fewer elements, 50%+ white space, understandable in 2 seconds. Any failure means regenerate or split. Do not assemble a poster from figures that failed.
Patterns that always fail: 7-stage workflow, timeline with annual milestones,
3 case studies in one graphic, comparison of 5+ methods, architecture with all layers.
Collapse each to 3 high-level items, or make several separate graphics.
Overflow is an error, not a warning. After compiling, run grep -i overfull poster.log
and inspect all four edges at 100% zoom. See
references/compilation_and_quality_control.md.
Scientific Schematics Integration
For detailed guidance on creating schematics, refer to the scientific-schematics skill documentation.
Key capabilities:
Nano Banana Pro automatically generates, reviews, and refines diagrams
Creates publication-quality images with proper formatting
Ensures accessibility (colorblind-friendly, high contrast)
Supports iterative refinement for complex diagrams
Core Capabilities
Three poster packages are supported — beamerposter (Beamer syntax, institutional themes), tikzposter (modern, colorful, flexible), and baposter (structured multi-column). Package comparison, layout and grid systems, design principles, standard sizes, per-package templates, figure and image integration, color schemes, typography, and QR codes are all documented in references/latex_poster_reference.md.
Reusable per-section content patterns, accessibility requirements, and presentation-day guidance are in references/poster_patterns_and_presentation.md.
Workflow for Poster Creation
Stage 1: Planning and Content Development
Determine poster requirements:
Conference size specifications (A0, 36×48", etc.)
Orientation (portrait vs. landscape)
Submission deadlines and format requirements
Develop content outline:
Identify 1-3 core messages
Select key figures (typically 3-6 main visuals)
Draft concise text for each section (bullet points preferred)
Aim for 300-800 words total
Choose LaTeX package:
beamerposter: If familiar with Beamer, need institutional themes
tikzposter: For modern, colorful designs with flexibility
baposter: For structured, professional multi-column layouts
Stage 2: Generate Visual Elements (AI-Powered)
CRITICAL: Generate SIMPLE figures with MINIMAL content. Each graphic = ONE message.
Content limits:
Maximum 4-5 elements per graphic
Maximum 15 words total per graphic
50% white space minimum
GIANT fonts (80pt+ for labels, 120pt+ for key numbers)
Create figures directory:
mkdir -p figuresGenerate SIMPLE visual elements:
# Introduction - ONLY 3 icons/elements python scripts/generate_schematic.py "POSTER FORMAT for A0. SIMPLE visual with ONLY 3 elements: [icon1] [icon2] [icon3]. ONE word labels (80pt+). 50% white space. Readable from 8 feet." -o figures/intro.png # Methods - ONLY 4 steps maximum python scripts/generate_schematic.py "POSTER FORMAT for A0. SIMPLE flowchart with ONLY 4 boxes: STEP1 → STEP2 → STEP3 → STEP4. GIANT labels (100pt+). 50% white space. NO sub-steps." -o figures/methods.png # Results - ONLY 3 bars/comparisons python scripts/generate_schematic.py "POSTER FORMAT for A0. SIMPLE chart with ONLY 3 bars. GIANT percentages ON bars (120pt+). NO axis, NO legend. 50% white space." -o figures/results.png # Conclusions - EXACTLY 3 items with GIANT numbers python scripts/generate_schematic.py "POSTER FORMAT for A0. EXACTLY 3 key findings: '[NUMBER]' (150pt) '[LABEL]' (60pt) for each. 50% white space. NO other text." -o figures/conclusions.pngReview generated figures - check for overflow:
View at 25% zoom: All text still readable?
Count elements: More than 5? → Regenerate simpler
Check white space: Less than 40%? → Add "60% white space" to prompt
Font too small?: Add "EVEN LARGER" or increase pt sizes
Still overflowing?: Reduce to 3 elements instead of 4-5
Stage 3: Design and Layout
Select or create template:
Start with provided templates in
assets/Customize color scheme to match branding
Configure page size and orientation
Design layout structure:
Plan column structure (2, 3, or 4 columns)
Map content flow (typically left-to-right, top-to-bottom)
Allocate space for title (10-15%), content (70-80%), footer (5-10%)
Set typography:
Configure font sizes for different hierarchy levels
Ensure minimum 24pt body text
Test readability from 4-6 feet distance
Stage 4: Content Integration
Create poster header:
Title (concise, descriptive, 10-15 words)
Authors and affiliations
Institution logos (high-resolution)
Conference logo if required
Integrate AI-generated figures:
Add all figures from Stage 2 to appropriate sections
Use
\includegraphicswith proper sizingEnsure figures dominate each section (visuals first, text second)
Center figures within blocks for visual impact
Add minimal supporting text:
Keep text minimal and scannable (300-800 words total)
Use bullet points, not paragraphs
Write in active voice
Text should complement figures, not duplicate them
Add supplementary elements:
QR codes for supplementary materials
References (cite key papers only, 5-10 typical)
Contact information and acknowledgments
Stage 5: Refinement and Testing
Review and iterate:
Check for typos and errors
Verify all figures are high resolution
Ensure consistent formatting
Confirm color scheme works well together
Test readability:
Print at 25% scale and read from 2-3 feet (simulates poster from 8-12 feet)
Check color on different monitors
Verify QR codes function correctly
Ask colleague to review
Optimize for printing:
Embed all fonts in PDF
Verify image resolution
Check PDF size requirements
Include bleed area if required
Stage 6: Compilation and Delivery
Compile final PDF:
pdflatex poster.tex # Or for better font support: lualatex poster.texVerify output quality:
Check all elements are visible and correctly positioned
Zoom to 100% and inspect figure quality
Verify colors match expectations
Confirm PDF opens correctly on different viewers
Prepare for printing:
Export as PDF/X-1a if required
Save backup copies
Get test print on regular paper first
Order professional printing 2-3 days before deadline
Create supplementary materials:
Save PNG/JPG version for social media
Create handout version (8.5×11" summary)
Prepare digital version for email sharing
Integration with Other Skills
This skill works effectively with:
Scientific Schematics: CRITICAL - Use for generating all poster diagrams and flowcharts
Generate Image / Nano Banana Pro: For stylized graphics, conceptual illustrations, and summary visuals
Scientific Writing: For developing poster content from papers
Literature Review: For contextualizing research
Data Analysis: For creating result figures and charts
Recommended workflow: Always use scientific-schematics and generate-image skills BEFORE creating the LaTeX poster to generate all visual elements.
Common Pitfalls to Avoid
AI-Generated Graphics Mistakes (MOST COMMON):
❌ Too many elements in one graphic (10+ items) → Keep to 3-5 max
❌ Text too small in AI graphics → Specify "GIANT (100pt+)" or "HUGE (150pt+)"
❌ Too much detail in prompts → Use "SIMPLE" and "ONLY X elements"
❌ No white space specification → Add "50% white space" to every prompt
❌ Complex flowcharts with 8+ steps → Limit to 4-5 steps maximum
❌ Comparison charts with 6+ items → Limit to 3 items maximum
❌ Key findings with 5+ metrics → Show only top 3
Fixing Overflow in AI Graphics: If your AI-generated graphics are overflowing or have small text:
Add "SIMPLER" or "ONLY 3 elements" to prompt
Increase font sizes: "150pt+" instead of "80pt+"
Add "60% white space" instead of "50%"
Remove sub-details: "NO sub-steps", "NO axis labels", "NO legend"
Regenerate with fewer elements
Design Mistakes:
❌ Too much text (over 1000 words)
❌ Font sizes too small (under 24pt body text)
❌ Low-contrast color combinations
❌ Cluttered layout with no white space
❌ Inconsistent styling across sections
❌ Poor quality or pixelated images
Content Mistakes:
❌ No clear narrative or message
❌ Too many research questions or objectives
❌ Overuse of jargon without definitions
❌ Results without context or interpretation
❌ Missing author contact information
Technical Mistakes:
❌ Wrong poster dimensions for conference requirements
❌ RGB colors sent to CMYK printer (color shift)
❌ Fonts not embedded in PDF
❌ File size too large for submission portal
❌ QR codes too small or not tested
Best Practices:
✅ Generate SIMPLE AI graphics with 3-5 elements max
✅ Use GIANT fonts (100pt+) for key numbers in graphics
✅ Specify "50% white space" in every AI prompt
✅ Follow conference size specifications exactly
✅ Test print at reduced scale before final printing
✅ Use high-contrast, accessible color schemes
✅ Keep text minimal and highly scannable
✅ Include clear contact information and QR codes
✅ Proofread carefully (errors are magnified on posters!)
Package Installation
Ensure required LaTeX packages are installed:
# For TeX Live (Linux/Mac)
tlmgr install beamerposter tikzposter baposter
# For MiKTeX (Windows)
# Packages typically auto-install on first use
# Additional recommended packages
tlmgr install qrcode graphics xcolor tcolorbox subcaptionScripts and Automation
Helper scripts available in scripts/ directory:
review_poster.sh: Poster review and validationgenerate_schematic.py: Generate scientific diagrams and schematics
References
references/ai_graphics_for_posters.md: full AI graphic rules, per-type limits, worked prompt examples, and the review gates.
references/latex_poster_reference.md: packages, layout, design, sizes, templates, figures, color, typography, QR codes.
references/compilation_and_quality_control.md: compilation engines and the complete pre-print QC procedure.
references/poster_patterns_and_presentation.md: content patterns, accessibility, presentation tips.
references/latex_poster_packages.md: detailed comparison of beamerposter, tikzposter, and baposter with examples.
references/poster_layout_design.md: layout principles, grid systems, and visual flow.
references/poster_design_principles.md: typography, color theory, visual hierarchy, and accessibility.
references/poster_content_guide.md: content organization, writing style, and section-specific guidance.
Templates
Ready-to-use poster templates in assets/ directory:
beamerposter templates (classic, modern, colorful)
tikzposter templates (default, rays, wave, envelope)
baposter templates (portrait, landscape, minimal)
Example posters from various scientific disciplines
Color scheme definitions and institutional templates
Load these templates and customize for your specific research and conference requirements.
Citing Scientific Agent Skills
This skill is part of Scientific Agent Skills by K-Dense. If it materially contributed to a manuscript, report, presentation, or code release, add the paper to the references or software section and tell the user you did so:
Kassis, T., Agarwal, V., He, Y., Patel, D., & Brueckner, A. M. (2026). Scientific Agent Skills: A Library of Procedural Knowledge for Research Agents. arXiv:2609.00065. https://doi.org/10.48550/arXiv.2609.00065
Always cite the current version. The DOI and https://arxiv.org/abs/2609.00065 resolve to the
latest arXiv version, so never append a version suffix such as v1. When network access is
available, fetch https://arxiv.org/abs/2609.00065 (or
http://export.arxiv.org/api/query?id_list=2609.00065) before writing the reference and take
the author list, year, and version from that record. If the record lists a journal reference
or publisher DOI, cite the published version instead.
Files
- ATTRIBUTION.mdAdded by AutoGineer980 B
- LICENSELicence file added by AutoGineer1.0 KiB
- SKILL.md16.2 KiB
- assets/baposter_template.tex7.3 KiB
- assets/beamerposter_template.tex7.5 KiB
- assets/poster_quality_checklist.md10.9 KiB
- assets/tikzposter_template.tex7.7 KiB
- references/README.md12.2 KiB
- references/ai_graphics_for_posters.md23.9 KiB
- references/compilation_and_quality_control.md13.6 KiB
- references/latex_poster_packages.md17.4 KiB
- references/latex_poster_reference.md7.2 KiB
- references/poster_content_guide.md19.9 KiB
- references/poster_design_principles.md22.4 KiB
- references/poster_layout_design.md21.8 KiB
- references/poster_patterns_and_presentation.md2.6 KiB
- scripts/generate_schematic.pyScript7.4 KiB
- scripts/generate_schematic_ai.pyScript37.6 KiB
- scripts/review_poster.shScript7.8 KiB
scripts/generate_schematic_ai.py
#!/usr/bin/env python3
"""
AI-powered scientific schematic generation using Nano Banana 2.
This script uses a smart iterative refinement approach:
1. Generate initial image with Nano Banana 2
2. AI quality review using Gemini 3.6 Flash for scientific critique
3. Only regenerate if quality is below threshold for document type
4. Repeat until quality meets standards (max iterations)
Requirements:
- OPENROUTER_API_KEY environment variable
- requests library
Usage:
python generate_schematic_ai.py "Create a flowchart showing CONSORT participant flow" -o flowchart.png
python generate_schematic_ai.py "Neural network architecture diagram" -o architecture.png --iterations 2
python generate_schematic_ai.py "Simple block diagram" -o diagram.png --doc-type poster
"""
import argparse
import base64
import json
import os
import re
import sys
import time
from pathlib import Path
from typing import Optional, Dict, Any, List, NamedTuple
try:
import requests
except ImportError:
print("Error: requests library not found. Install with: uv pip install requests")
sys.exit(1)
class ReviewResult(NamedTuple):
"""The outcome of one quality review.
A named tuple rather than a bare tuple because the review has several
failure modes -- the model returns no choices, the request raises, the
answer arrives in an unexpected shape -- and each of them used to be a
chance to return the wrong number of values to the caller.
`score` is None whenever no usable number was parsed, and `reviewed` is
False whenever the review never produced an answer at all. Neither case
invents a score: a diagram whose quality was not measured is reported as
unmeasured, not as passing.
"""
critique: str
score: Optional[float]
needs_improvement: bool
reviewed: bool
error: Optional[str] = None
# Markdown decoration the reviewer routinely wraps its answer in. Gemini writes
# "**SCORE:** 8.5" far more often than the bare "SCORE: 8.5" the rubric asks
# for, and a pattern that does not allow for it silently scores every diagram
# at whatever the default happens to be.
_MARKUP = re.compile(r"[*_`#]")
_SCORE_PATTERN = re.compile(r"\bSCORE\s*:?\s*(\d+(?:\.\d+)?)", re.IGNORECASE)
_LOOSE_SCORE_PATTERN = re.compile(
r"(?:score|rating|quality)\s*[:\s]\s*(\d+(?:\.\d+)?)\s*(?:/\s*10)?", re.IGNORECASE
)
# The underscore is optional because normalization strips it out of
# NEEDS_IMPROVEMENT along with the surrounding markdown.
_VERDICT_PATTERN = re.compile(
r"^\s*VERDICT\s*:?\s*(ACCEPTABLE|NEEDS[_ ]?IMPROVEMENT)", re.IGNORECASE | re.MULTILINE
)
def _normalize_review_text(text: str) -> str:
"""Strip markdown decoration so the rubric's fields can be found."""
return _MARKUP.sub("", text or "")
def _parse_score(text: str) -> Optional[float]:
"""Return the reviewer's 0-10 score, or None when there isn't a usable one.
None rather than a default: a fabricated score is indistinguishable from a
real one once it reaches the review log, and a default near the middle of
the range quietly passes lenient document types while failing strict ones.
"""
normalized = _normalize_review_text(text)
match = _SCORE_PATTERN.search(normalized) or _LOOSE_SCORE_PATTERN.search(normalized)
if not match:
return None
score = float(match.group(1))
# A number outside the rubric's range means something other than the score
# was matched -- a figure count, a percentage, an iteration number.
return score if 0.0 <= score <= 10.0 else None
def _parse_verdict(text: str) -> Optional[str]:
"""Return ACCEPTABLE / NEEDS_IMPROVEMENT from the verdict line, or None.
Anchored to the start of a line. The rubric prompt spells out both tokens
when it explains the response format, so a reviewer that echoes those
instructions back would fail a perfectly good diagram under a plain
substring search.
"""
match = _VERDICT_PATTERN.search(_normalize_review_text(text))
if not match:
return None
verdict = match.group(1).upper().replace(" ", "").replace("_", "")
return "NEEDS_IMPROVEMENT" if verdict == "NEEDSIMPROVEMENT" else "ACCEPTABLE"
def _resolve_api_key(explicit: Optional[str] = None) -> Optional[str]:
"""Resolve the OpenRouter key from --api-key, the environment, then any .env file.
The .env scan walks up from the working directory and finally checks the
script's own directory, so running from anywhere inside a project picks up
the key at its root. Only the standard library is used: python-dotenv is a
common omission, and a missing optional dependency should not read as a
missing credential.
"""
if explicit:
return explicit
from_env = os.environ.get("OPENROUTER_API_KEY", "").strip()
if from_env:
return from_env
cwd = Path.cwd()
for directory in [cwd, *cwd.parents, Path(__file__).resolve().parent]:
env_file = directory / ".env"
if not env_file.is_file():
continue
try:
content = env_file.read_text(encoding="utf-8", errors="replace")
except OSError:
continue
for raw in content.splitlines():
line = raw.strip()
if line.startswith("#") or "=" not in line:
continue
name, _, value = line.partition("=")
if name.strip() == "OPENROUTER_API_KEY":
value = value.strip().strip('"').strip("'")
if value:
return value
return None
class ScientificSchematicGenerator:
"""Generate scientific schematics using AI with smart iterative refinement.
Uses Gemini 3.6 Flash for quality review to determine if regeneration is needed.
Multiple passes only occur if the generated schematic doesn't meet the
quality threshold for the target document type.
"""
# Quality thresholds by document type (score out of 10)
# Higher thresholds for more formal publications
QUALITY_THRESHOLDS = {
"journal": 8.5, # Nature, Science, etc. - highest standards
"conference": 8.0, # Conference papers - high standards
"poster": 7.0, # Academic posters - good quality
"presentation": 6.5, # Slides/talks - clear but less formal
"report": 7.5, # Technical reports - professional
"grant": 8.0, # Grant proposals - must be compelling
"thesis": 8.0, # Dissertations - formal academic
"preprint": 7.5, # arXiv, etc. - good quality
"default": 7.5, # Default threshold
}
# Scientific diagram best practices prompt template
SCIENTIFIC_DIAGRAM_GUIDELINES = """
Create a high-quality scientific diagram with these requirements:
VISUAL QUALITY:
- Clean white or light background (no textures or gradients)
- High contrast for readability and printing
- Professional, publication-ready appearance
- Sharp, clear lines and text
- Adequate spacing between elements to prevent crowding
TYPOGRAPHY:
- Clear, readable sans-serif fonts (Arial, Helvetica style)
- Minimum 10pt font size for all labels
- Consistent font sizes throughout
- All text horizontal or clearly readable
- No overlapping text
SCIENTIFIC STANDARDS:
- Accurate representation of concepts
- Clear labels for all components
- Include scale bars, legends, or axes where appropriate
- Use standard scientific notation and symbols
- Include units where applicable
ACCESSIBILITY:
- Colorblind-friendly color palette (use Okabe-Ito colors if using color)
- High contrast between elements
- Redundant encoding (shapes + colors, not just colors)
- Works well in grayscale
LAYOUT:
- Logical flow (left-to-right or top-to-bottom)
- Clear visual hierarchy
- Balanced composition
- Appropriate use of whitespace
- No clutter or unnecessary decorative elements
IMPORTANT - NO FIGURE NUMBERS:
- Do NOT include "Figure 1:", "Fig. 1", or any figure numbering in the image
- Do NOT add captions or titles like "Figure: ..." at the top or bottom
- Figure numbers and captions are added separately in the document/LaTeX
- The diagram should contain only the visual content itself
"""
def __init__(self, api_key: Optional[str] = None, verbose: bool = False):
"""
Initialize the generator.
Args:
api_key: OpenRouter API key (or use OPENROUTER_API_KEY env var)
verbose: Print detailed progress information
"""
# Priority: 1) explicit api_key param, 2) environment variable, 3) .env file
self.api_key = _resolve_api_key(api_key)
if not self.api_key:
raise ValueError(
"OPENROUTER_API_KEY not found. Please either:\n"
" 1. Set the OPENROUTER_API_KEY environment variable\n"
" 2. Add OPENROUTER_API_KEY to your .env file\n"
" 3. Pass api_key parameter to the constructor\n"
"Get your API key from: https://openrouter.ai/keys"
)
self.verbose = verbose
self._last_error = None # Track last error for better reporting
self.base_url = "https://openrouter.ai/api/v1"
# Nano Banana 2 - Google's advanced image generation model. The slug must
# be an image-output model; a text-only chat model is rejected with
# "No endpoints found that support the requested output modalities".
# https://openrouter.ai/google/gemini-3.1-flash-image
self.image_model = "google/gemini-3.1-flash-image"
# Gemini 3.6 Flash for quality review - excellent vision and reasoning
self.review_model = "google/gemini-3.7-flash"
def _log(self, message: str):
"""Log message if verbose mode is enabled."""
if self.verbose:
print(f"[{time.strftime('%H:%M:%S')}] {message}")
def _make_request(self, model: str, messages: List[Dict[str, Any]],
modalities: Optional[List[str]] = None) -> Dict[str, Any]:
"""
Make a request to OpenRouter API.
Args:
model: Model identifier
messages: List of message dictionaries
modalities: Optional list of modalities (e.g., ["image", "text"])
Returns:
API response as dictionary
"""
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json",
"HTTP-Referer": "https://github.com/scientific-writer",
"X-Title": "Scientific Schematic Generator"
}
payload = {
"model": model,
"messages": messages
}
if modalities:
payload["modalities"] = modalities
self._log(f"Making request to {model}...")
try:
response = requests.post(
f"{self.base_url}/chat/completions",
headers=headers,
json=payload,
timeout=120
)
# Try to get response body even on error
try:
response_json = response.json()
except json.JSONDecodeError:
response_json = {"raw_text": response.text[:500]}
# Check for HTTP errors but include response body in error message
if response.status_code != 200:
error_detail = response_json.get("error", response_json)
self._log(f"HTTP {response.status_code}: {error_detail}")
raise RuntimeError(f"API request failed (HTTP {response.status_code}): {error_detail}")
return response_json
except requests.exceptions.Timeout:
raise RuntimeError("API request timed out after 120 seconds")
except requests.exceptions.RequestException as e:
raise RuntimeError(f"API request failed: {str(e)}")
def _extract_image_from_response(self, response: Dict[str, Any]) -> Optional[bytes]:
"""
Extract base64-encoded image from API response.
For Nano Banana 2, images are returned in the 'images' field of the message,
not in the 'content' field.
Args:
response: API response dictionary
Returns:
Image bytes or None if not found
"""
try:
choices = response.get("choices", [])
if not choices:
self._log("No choices in response")
return None
message = choices[0].get("message", {})
# IMPORTANT: Nano Banana 2 returns images in the 'images' field
images = message.get("images", [])
if images and len(images) > 0:
self._log(f"Found {len(images)} image(s) in 'images' field")
# Get first image
first_image = images[0]
if isinstance(first_image, dict):
# Extract image_url
if first_image.get("type") == "image_url":
url = first_image.get("image_url", {})
if isinstance(url, dict):
url = url.get("url", "")
if url and url.startswith("data:image"):
# Extract base64 data after comma
if "," in url:
base64_str = url.split(",", 1)[1]
# Clean whitespace
base64_str = base64_str.replace('\n', '').replace('\r', '').replace(' ', '')
self._log(f"Extracted base64 data (length: {len(base64_str)})")
return base64.b64decode(base64_str)
# Fallback: check content field (for other models or future changes)
content = message.get("content", "")
if self.verbose:
self._log(f"Content type: {type(content)}, length: {len(str(content))}")
# Handle string content
if isinstance(content, str) and "data:image" in content:
match = re.search(r'data:image/[^;]+;base64,([A-Za-z0-9+/=\n\r]+)', content, re.DOTALL)
if match:
base64_str = match.group(1).replace('\n', '').replace('\r', '').replace(' ', '')
self._log(f"Found image in content field (length: {len(base64_str)})")
return base64.b64decode(base64_str)
# Handle list content
if isinstance(content, list):
for i, block in enumerate(content):
if isinstance(block, dict) and block.get("type") == "image_url":
url = block.get("image_url", {})
if isinstance(url, dict):
url = url.get("url", "")
if url and url.startswith("data:image") and "," in url:
base64_str = url.split(",", 1)[1].replace('\n', '').replace('\r', '').replace(' ', '')
self._log(f"Found image in content block {i}")
return base64.b64decode(base64_str)
self._log("No image data found in response")
return None
except Exception as e:
self._log(f"Error extracting image: {str(e)}")
import traceback
if self.verbose:
traceback.print_exc()
return None
def _image_to_base64(self, image_path: str) -> str:
"""
Convert image file to base64 data URL.
Args:
image_path: Path to image file
Returns:
Base64 data URL string
"""
with open(image_path, "rb") as f:
image_data = f.read()
# Determine image type from extension
ext = Path(image_path).suffix.lower()
mime_type = {
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".gif": "image/gif",
".webp": "image/webp"
}.get(ext, "image/png")
base64_data = base64.b64encode(image_data).decode("utf-8")
return f"data:{mime_type};base64,{base64_data}"
def generate_image(self, prompt: str) -> Optional[bytes]:
"""
Generate an image using Nano Banana 2.
Args:
prompt: Description of the diagram to generate
Returns:
Image bytes or None if generation failed
"""
self._last_error = None # Reset error
messages = [
{
"role": "user",
"content": prompt
}
]
try:
response = self._make_request(
model=self.image_model,
messages=messages,
modalities=["image", "text"]
)
# Debug: print response structure if verbose
if self.verbose:
self._log(f"Response keys: {response.keys()}")
if "error" in response:
self._log(f"API Error: {response['error']}")
if "choices" in response and response["choices"]:
msg = response["choices"][0].get("message", {})
self._log(f"Message keys: {msg.keys()}")
# Show content preview without printing huge base64 data
content = msg.get("content", "")
if isinstance(content, str):
preview = content[:200] + "..." if len(content) > 200 else content
self._log(f"Content preview: {preview}")
elif isinstance(content, list):
self._log(f"Content is list with {len(content)} items")
for i, item in enumerate(content[:3]):
if isinstance(item, dict):
self._log(f" Item {i}: type={item.get('type')}")
# Check for API errors in response
if "error" in response:
error_msg = response["error"]
if isinstance(error_msg, dict):
error_msg = error_msg.get("message", str(error_msg))
self._last_error = f"API Error: {error_msg}"
print(f"✗ {self._last_error}")
return None
image_data = self._extract_image_from_response(response)
if image_data:
self._log(f"✓ Generated image ({len(image_data)} bytes)")
else:
self._last_error = "No image data in API response - model may not support image generation"
self._log(f"✗ {self._last_error}")
# Additional debug info when image extraction fails
if self.verbose and "choices" in response:
msg = response["choices"][0].get("message", {})
self._log(f"Full message structure: {json.dumps({k: type(v).__name__ for k, v in msg.items()})}")
return image_data
except RuntimeError as e:
self._last_error = str(e)
self._log(f"✗ Generation failed: {self._last_error}")
return None
except Exception as e:
self._last_error = f"Unexpected error: {str(e)}"
self._log(f"✗ Generation failed: {self._last_error}")
import traceback
if self.verbose:
traceback.print_exc()
return None
def review_image(self, image_path: str, original_prompt: str,
iteration: int, doc_type: str = "default",
max_iterations: int = 2) -> ReviewResult:
"""
Review the generated image for quality, using the vision review model.
Args:
image_path: Path to the generated image
original_prompt: Original user prompt
iteration: Current iteration number
doc_type: Document type (journal, poster, presentation, etc.)
max_iterations: Maximum iterations allowed
Returns:
A ReviewResult. When the review cannot be completed -- the request
fails, or the model answers with nothing usable -- `reviewed` is
False and `score` is None rather than a stand-in number, and
`needs_improvement` is False: the fault is in the reviewer, not in
the diagram, so regenerating would not help.
"""
image_data_url = self._image_to_base64(image_path)
# Get quality threshold for this document type
threshold = self.QUALITY_THRESHOLDS.get(doc_type.lower(),
self.QUALITY_THRESHOLDS["default"])
review_prompt = f"""You are an expert reviewer evaluating a scientific diagram for publication quality.
ORIGINAL REQUEST: {original_prompt}
DOCUMENT TYPE: {doc_type} (quality threshold: {threshold}/10)
ITERATION: {iteration}/{max_iterations}
Carefully evaluate this diagram on these criteria:
1. **Scientific Accuracy** (0-2 points)
- Correct representation of concepts
- Proper notation and symbols
- Accurate relationships shown
2. **Clarity and Readability** (0-2 points)
- Easy to understand at a glance
- Clear visual hierarchy
- No ambiguous elements
3. **Label Quality** (0-2 points)
- All important elements labeled
- Labels are readable (appropriate font size)
- Consistent labeling style
4. **Layout and Composition** (0-2 points)
- Logical flow (top-to-bottom or left-to-right)
- Balanced use of space
- No overlapping elements
5. **Professional Appearance** (0-2 points)
- Publication-ready quality
- Clean, crisp lines and shapes
- Appropriate colors/contrast
RESPOND IN THIS EXACT FORMAT:
SCORE: [total score 0-10]
STRENGTHS:
- [strength 1]
- [strength 2]
ISSUES:
- [issue 1 if any]
- [issue 2 if any]
VERDICT: [ACCEPTABLE or NEEDS_IMPROVEMENT]
If score >= {threshold}, the diagram is ACCEPTABLE for {doc_type} publication.
If score < {threshold}, mark as NEEDS_IMPROVEMENT with specific suggestions."""
messages = [
{
"role": "user",
"content": [
{
"type": "text",
"text": review_prompt
},
{
"type": "image_url",
"image_url": {
"url": image_data_url
}
}
]
}
]
try:
response = self._make_request(
model=self.review_model,
messages=messages
)
# Extract text response
choices = response.get("choices", [])
if not choices:
# A normal enough outcome -- a rate limit, a content filter, a
# provider hiccup. The diagram itself is fine; only its review
# is missing, and saying so beats inventing a score.
reason = "the review model returned no choices"
self._log(f"⚠ Review unavailable: {reason}")
return ReviewResult(
f"Review unavailable: {reason}.",
None, False, reviewed=False, error=reason,
)
message = choices[0].get("message", {})
content = message.get("content", "")
# Some models put their analysis in a separate reasoning field
reasoning = message.get("reasoning", "")
if reasoning and not content:
content = reasoning
if isinstance(content, list):
# Extract text from content blocks
text_parts = []
for block in content:
if isinstance(block, dict) and block.get("type") == "text":
text_parts.append(block.get("text", ""))
content = "\n".join(text_parts)
score = _parse_score(content)
verdict = _parse_verdict(content)
if score is None and verdict is None:
reason = "no score or verdict found in the review"
self._log(f"⚠ Review unusable: {reason}")
return ReviewResult(
content if content else f"Review unusable: {reason}.",
None, False, reviewed=True, error=reason,
)
needs_improvement = verdict == "NEEDS_IMPROVEMENT" or (
score is not None and score < threshold
)
shown = f"{score}/10" if score is not None else "not stated"
self._log(f"✓ Review complete (Score: {shown}, Threshold: {threshold}/10)")
self._log(f" Verdict: {'Needs improvement' if needs_improvement else 'Acceptable'}")
return ReviewResult(
content if content else "Review returned no text",
score,
needs_improvement,
reviewed=True,
error=None if score is not None else "no score found in the review",
)
except Exception as e:
# A failed review must not fail the run -- the image is already
# generated and saved -- but it must not read as a pass either.
self._log(f"⚠ Review failed: {str(e)}")
return ReviewResult(
f"Review failed: {str(e)}",
None, False, reviewed=False, error=str(e),
)
def improve_prompt(self, original_prompt: str, critique: str,
iteration: int) -> str:
"""
Improve the generation prompt based on critique.
Args:
original_prompt: Original user prompt
critique: Review critique from previous iteration
iteration: Current iteration number
Returns:
Improved prompt for next generation
"""
improved_prompt = f"""{self.SCIENTIFIC_DIAGRAM_GUIDELINES}
USER REQUEST: {original_prompt}
ITERATION {iteration}: Based on previous feedback, address these specific improvements:
{critique}
Generate an improved version that addresses all the critique points while maintaining scientific accuracy and professional quality."""
return improved_prompt
def generate_iterative(self, user_prompt: str, output_path: str,
iterations: int = 2,
doc_type: str = "default") -> Dict[str, Any]:
"""
Generate scientific schematic with smart iterative refinement.
Only regenerates if the quality score is below the threshold for the
specified document type. This saves API calls and time when the first
generation is already good enough.
Args:
user_prompt: User's description of desired diagram
output_path: Path to save final image
iterations: Maximum refinement iterations (default: 2, max: 2)
doc_type: Document type for quality threshold (journal, poster, etc.)
Returns:
Dictionary with generation results and metadata
"""
output_path = Path(output_path)
output_dir = output_path.parent
output_dir.mkdir(parents=True, exist_ok=True)
base_name = output_path.stem
extension = output_path.suffix or ".png"
# Get quality threshold for this document type
threshold = self.QUALITY_THRESHOLDS.get(doc_type.lower(),
self.QUALITY_THRESHOLDS["default"])
results = {
"user_prompt": user_prompt,
"doc_type": doc_type,
"quality_threshold": threshold,
"iterations": [],
"final_image": None,
# None, not 0.0 -- a run whose review never completed has no score,
# and a number here would be read as one the reviewer gave.
"final_score": None,
"final_reviewed": False,
"success": False,
"early_stop": False,
"early_stop_reason": None
}
current_prompt = f"""{self.SCIENTIFIC_DIAGRAM_GUIDELINES}
USER REQUEST: {user_prompt}
Generate a publication-quality scientific diagram that meets all the guidelines above."""
print(f"\n{'='*60}")
print(f"Generating Scientific Schematic")
print(f"{'='*60}")
print(f"Description: {user_prompt}")
print(f"Document Type: {doc_type}")
print(f"Quality Threshold: {threshold}/10")
print(f"Max Iterations: {iterations}")
print(f"Output: {output_path}")
print(f"{'='*60}\n")
for i in range(1, iterations + 1):
print(f"\n[Iteration {i}/{iterations}]")
print("-" * 40)
# Generate image
print(f"Generating image...")
image_data = self.generate_image(current_prompt)
if not image_data:
error_msg = getattr(self, '_last_error', 'Image generation failed - no image data returned')
print(f"✗ Generation failed: {error_msg}")
results["iterations"].append({
"iteration": i,
"success": False,
"error": error_msg
})
continue
# Save iteration image
iter_path = output_dir / f"{base_name}_v{i}{extension}"
with open(iter_path, "wb") as f:
f.write(image_data)
print(f"✓ Saved: {iter_path}")
# Review the image with the vision review model
print(f"Reviewing image with {self.review_model}...")
review = self.review_image(
str(iter_path), user_prompt, i, doc_type, iterations
)
if review.score is not None:
print(f"✓ Score: {review.score}/10 (threshold: {threshold}/10)")
else:
print(f"⚠ Review unavailable — image kept, quality not verified")
print(f" Reason: {review.error}")
# Save iteration results
iteration_result = {
"iteration": i,
"image_path": str(iter_path),
"prompt": current_prompt,
"critique": review.critique,
"score": review.score,
"reviewed": review.reviewed,
"review_error": review.error,
"needs_improvement": review.needs_improvement,
"success": True
}
results["iterations"].append(iteration_result)
# Check if quality is acceptable - STOP EARLY if so
if not review.needs_improvement:
if review.score is not None:
print(f"\n✓ Quality meets {doc_type} threshold ({review.score} >= {threshold})")
print(f" No further iterations needed!")
reason = (f"Quality score {review.score} meets threshold "
f"{threshold} for {doc_type}")
else:
# Regenerating cannot fix a reviewer that did not answer.
print(f"\n⚠ Stopping without a verified score — review the image yourself")
reason = f"Review did not produce a score: {review.error}"
results["final_image"] = str(iter_path)
results["final_score"] = review.score
results["final_reviewed"] = review.reviewed and review.score is not None
results["success"] = True
results["early_stop"] = True
results["early_stop_reason"] = reason
break
# If this is the last iteration, we're done regardless
if i == iterations:
print(f"\n⚠ Maximum iterations reached")
results["final_image"] = str(iter_path)
results["final_score"] = review.score
results["final_reviewed"] = review.reviewed and review.score is not None
results["success"] = True
break
# Quality below threshold - improve prompt for next iteration
print(f"\n⚠ Quality below threshold ({review.score} < {threshold})")
print(f"Improving prompt based on feedback...")
current_prompt = self.improve_prompt(user_prompt, review.critique, i + 1)
# Copy final version to output path
if results["success"] and results["final_image"]:
final_iter_path = Path(results["final_image"])
if final_iter_path != output_path:
import shutil
shutil.copy(final_iter_path, output_path)
print(f"\n✓ Final image: {output_path}")
# Save review log
log_path = output_dir / f"{base_name}_review_log.json"
with open(log_path, "w") as f:
json.dump(results, f, indent=2)
print(f"✓ Review log: {log_path}")
print(f"\n{'='*60}")
print(f"Generation Complete!")
if results["final_score"] is not None:
print(f"Final Score: {results['final_score']}/10")
else:
print(f"Final Score: unavailable (the review did not produce one)")
if results["early_stop"]:
print(f"Iterations Used: {len([r for r in results['iterations'] if r.get('success')])}/{iterations} (early stop)")
print(f"{'='*60}\n")
return results
def main():
"""Command-line interface."""
parser = argparse.ArgumentParser(
description="Generate scientific schematics using AI with smart iterative refinement",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Generate a flowchart for a journal paper
python generate_schematic_ai.py "CONSORT participant flow diagram" -o flowchart.png --doc-type journal
# Generate neural network architecture for presentation (lower threshold)
python generate_schematic_ai.py "Transformer encoder-decoder architecture" -o transformer.png --doc-type presentation
# Generate with custom max iterations for poster
python generate_schematic_ai.py "Biological signaling pathway" -o pathway.png --iterations 2 --doc-type poster
# Verbose output
python generate_schematic_ai.py "Circuit diagram" -o circuit.png -v
Document Types (quality thresholds):
journal 8.5/10 - Nature, Science, peer-reviewed journals
conference 8.0/10 - Conference papers
thesis 8.0/10 - Dissertations, theses
grant 8.0/10 - Grant proposals
preprint 7.5/10 - arXiv, bioRxiv, etc.
report 7.5/10 - Technical reports
poster 7.0/10 - Academic posters
presentation 6.5/10 - Slides, talks
default 7.5/10 - General purpose
Note: Multiple iterations only occur if quality is BELOW the threshold.
If the first generation meets the threshold, no extra API calls are made.
Environment:
OPENROUTER_API_KEY OpenRouter API key (required)
"""
)
parser.add_argument("prompt", help="Description of the diagram to generate")
parser.add_argument("-o", "--output", required=True,
help="Output image path (e.g., diagram.png)")
parser.add_argument("--iterations", type=int, default=2,
help="Maximum refinement iterations (default: 2, max: 2)")
parser.add_argument("--doc-type", default="default",
choices=["journal", "conference", "poster", "presentation",
"report", "grant", "thesis", "preprint", "default"],
help="Document type for quality threshold (default: default)")
parser.add_argument("--api-key", help="OpenRouter API key (or set OPENROUTER_API_KEY)")
parser.add_argument("-v", "--verbose", action="store_true",
help="Verbose output")
args = parser.parse_args()
# Check for API key — resolves --api-key, the environment, then any .env file
api_key = _resolve_api_key(args.api_key)
if not api_key:
print("Error: OPENROUTER_API_KEY not found")
print("\nSet it with:")
print(" export OPENROUTER_API_KEY='your_api_key'")
print("\nOr add OPENROUTER_API_KEY=your_api_key to a .env file")
print("Or provide via --api-key flag")
sys.exit(1)
# Validate iterations - enforce max of 2
if args.iterations < 1 or args.iterations > 2:
print("Error: Iterations must be between 1 and 2")
sys.exit(1)
try:
generator = ScientificSchematicGenerator(api_key=api_key, verbose=args.verbose)
results = generator.generate_iterative(
user_prompt=args.prompt,
output_path=args.output,
iterations=args.iterations,
doc_type=args.doc_type
)
if results["success"]:
print(f"\n✓ Success! Image saved to: {args.output}")
used = len([r for r in results['iterations'] if r.get('success')])
if results.get("final_reviewed"):
if results.get("early_stop"):
print(f" (Completed in {used} iteration(s) - quality threshold met)")
else:
# The image is real; the quality claim is not. Say which.
print(f" (Completed in {used} iteration(s) - quality NOT verified,"
f" the review produced no score. Check the image yourself.)")
sys.exit(0)
else:
print(f"\n✗ Generation failed. Check review log for details.")
sys.exit(1)
except Exception as e:
print(f"\n✗ Error: {str(e)}")
sys.exit(1)
if __name__ == "__main__":
main()
Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Needs: Declares allowed tools, Needs network access, Needs Python
Security
What our scanner noticed· Low risk
- Scripts that your agent may run
This skill includes scripts that your agent may run. Read them before you use it.
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Created by
- K-Dense Inc.
- Publisher
- K-Dense (Community)
- Source
- K-Dense-AI/claude-scientific-writer · skills/latex-posters · commit 529b9f7
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
- Also distributed in
- K-Dense-AI/claude-scientific-writer
AutoGineer is not affiliated with or endorsed by K-Dense Inc. or K-Dense.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download latex-posters.zip