Initial Upload

This commit is contained in:
2025-09-11 22:23:36 +02:00
parent 2a1f8cbbdd
commit 4f1e1008d7
4 changed files with 855 additions and 2 deletions

153
README.md
View File

@@ -1,3 +1,152 @@
# CBZGenerator
# CBZ Generator
## Overview
CBZ Generator is a Python tool that creates realistic comic book archive (CBZ) files with proper ComicInfo.xml metadata for testing comic book management software. It generates complete comic series with authentic publisher/character relationships, realistic publication schedules, and proper file organization. Can easily generate 100,000+ comics in an hour on modern hardware.
## Features
- **Realistic Comic Metadata**: Generates proper ComicInfo.xml with series names, issue numbers, publication dates, publishers, writers, and characters
- **Authentic Publishing Schedule**: Respects monthly publication cycles for regular series and annual schedules for special issues
- **Multiple Format Support**: Main Series, Limited Series, One-Shots, TPBs, Annuals, Director's Cuts, and more
- **Continuation Support**: Can continue existing series by scanning for the highest issue number
- **Publisher Ecosystem**: Creates realistic relationships between publishers, writers, and character rosters
- **Organized File Structure**: Automatically organizes files by Publisher/Character/Format/Series hierarchy
- **Configurable Data**: JSON-based configuration for publishers, writers, characters, and series names
- **Year Cap Protection**: Built-in safeguards prevent generation beyond 2025
## Installation
### Requirements
- Python 3.7+
- Pillow library for image generation
### Install Dependencies
```bash
pip install pillow
```
### Clone Repository
```bash
git clone <repository-url>
cd cbz-generator
```
## Usage
### Basic Usage
```bash
# Generate 10 comic series
python generate_cbz.py 10
# Generate 50 series with custom output directory
python generate_cbz.py 50 --out /path/to/comics
# Use custom data file
python generate_cbz.py 25 --data my_comic_data.json
# Set random seed for reproducible results
python generate_cbz.py 15 --seed 12345
```
### Command Line Options
| Option | Description | Default |
|--------|-------------|---------|
| `count` | Number of comic series to generate | Required |
| `--out` | Output base directory | `output_cbz` |
| `--data` | Path to comic data JSON file | `./comicdata.json` |
| `--seed` | Random seed for reproducibility | None |
## Configuration
The `comicdata.json` file defines the comic universe:
### Publishers Structure
Each publisher includes:
- **name**: Publisher name (e.g., "Ironclad Comics")
- **writers**: Array of writer names associated with this publisher
- **characters**: Array of character names published by this publisher
### Works Array
Defines base series names that can be combined with various patterns like:
- `{work}` → "Odyssey"
- `The {work}` → "The Chronicles"
- `{work}: Genesis` → "Inferno: Genesis"
- `{work} & {work2}` → "Saga & Legends"
## File Organization
Generated CBZ files are organized in a hierarchical structure:
```
output_cbz/
├── Ironclad-Comics/
│ ├── Steel-Sentinel/
│ │ ├── Main-Series/
│ │ │ └── (1985)-Steel-Sentinel-Legacy/
│ │ │ ├── Steel Sentinel Legacy #001 [March, 1985].cbz
│ │ │ ├── Steel Sentinel Legacy #002 [April, 1985].cbz
│ │ │ └── ...
│ │ └── Annual/
│ │ └── (1987)-Steel-Sentinel-Annual/
│ │ └── Steel Sentinel Annual #001 [December, 1987].cbz
│ └── Crimson-Flame/
│ └── Limited-Series/
│ └── (1990)-Crimson-Flame-Rising/
│ └── ...
└── Nebula-Press/
└── ...
```
## CBZ File Contents
Each generated CBZ file contains:
- **ComicInfo.xml**: Complete metadata including title, series, issue number, publication date, publisher, writer, characters, format, and page count
- **Image Pages**: JPEG pages (P00001.jpg through P0000X.jpg) with simple text content
- **Proper Compression**: ZIP compression for realistic file sizes
## Series Types & Issue Counts
| Format | Typical Issue Count |
|--------|-------------------|
| Main Series | 1-500 issues |
| Limited Series | 1-15 issues |
| One-Shot | 1 issue |
| TPB | 1-10 volumes |
| Annual | 1-5 issues |
| Director's Cut | 1-5 issues |
## Publication Date Logic
- **Monthly Series**: Issues published monthly starting from a random date, ensuring the series doesn't extend beyond 2025
- **Annuals**: Published yearly with consistent month across all issues
- **Date Validation**: All publication dates are capped at December 2025
- **Continuation**: When continuing existing series, original publication schedule is preserved
## Error Handling
The tool includes robust error handling for:
- Invalid JSON configuration files
- Missing required fields in publisher data
- File system permission issues
- Image generation failures
- Date calculation edge cases
## Example Output
A typical generated CBZ might contain:
- **Filename**: `Steel Sentinel Legacy #042 [June, 1988].cbz`
- **Publisher**: Ironclad Comics
- **Writer**: Marcus Hale
- **Character**: Steel Sentinel
- **Format**: Main Series
- **Page Count**: 5-10 pages
- **Volume Year**: 1985 (year of issue #1)
## Use Cases
- **Stress Testing**: Test comic management software with large libraries
- **Performance Testing**: Evaluate application performance with realistic datasets
- **Development**: Create test data for comic book applications
- **Benchmarking**: Compare comic reader/organizer performance across different libraries
Generate thousands of realistic comic book CBZ files with proper metadata for stress testing comic management applications.

166
comicdata.json Normal file
View File

@@ -0,0 +1,166 @@
{
"publishers": [
{
"name": "Ironclad Comics",
"writers": [
"Marcus Hale", "Elena Duvall", "Grant Mercer", "Sophie Kane", "Darius Lowe",
"Natalie Brooks", "Victor Lang", "Harper Quinn", "Ishaan Patel", "Clara Morton",
"Felix Romero", "Yvette Zhang", "Jonas Clarke", "Maya Rios", "Sebastian Cross",
"Holly Sinclair", "Andrei Novak", "Riley Dawson", "Priya Deshmukh", "Tobias Green"
],
"characters": [
"Steel Sentinel", "Nightmare Jack", "Crimson Flame", "Echo Viper", "Solarion",
"Wraith Whisper", "Gale Arrow", "Obsidian Wolf", "Tempest Queen", "Iron Phantom",
"Blaze Monarch", "Cobalt Ghost", "Neon Talon", "Titan Fang", "Silver Oracle",
"Dreadmask", "Vortex Rider", "Ash Serpent", "Storm Herald", "Radiant Shade"
]
},
{
"name": "Nebula Press",
"writers": [
"Amira Khan", "Julian Frost", "Kendra Blake", "Owen Torres", "Lucille Hart",
"Damien Rowe", "Sasha Volkov", "Tara Mendoza", "Noah Stein", "Bianca Russo",
"Rafael Duarte", "Lydia Moon", "Caleb Foster", "Marina Choi", "Andre Silva",
"Gemma Clarke", "Diego Alvarez", "Hana Kobayashi", "Elliot Drake", "Phoebe Stone"
],
"characters": [
"Nova Shard", "Cosmos Fang", "Lunaris", "Black Nebula", "Eclipse Stalker",
"Gravity Warden", "Plasma Orchid", "Zero Comet", "Aurora Striker", "Starforge",
"Celestial Viper", "Oblivion Child", "Quasar Knight", "Dark Helix", "Meteor Monarch",
"Nebula Veil", "Asteroid Seraph", "Singularity Raven", "Starlight Phantom", "Ion Blaze"
]
},
{
"name": "Crimson Ink",
"writers": [
"Devon Pierce", "Anika Shah", "Malcolm Stone", "Camille Breton", "Ibrahim Qadir",
"Elise Grant", "Tristan Vale", "Joanna Myers", "Hector Dominguez", "Silvia Novak",
"Eric Cho", "Nadia Rivers", "Samuel Bishop", "Ivy Laurent", "Carlos Mendes",
"Rosalind Fox", "Hiro Tanaka", "Arjun Iyer", "Marta Kowalski", "Leo Barlow"
],
"characters": [
"Red Serpent", "Blood Rose", "Iron Widow", "Vermillion Stalker", "Sable Dagger",
"Onyx Howl", "Dark Oracle", "Venom Shade", "Crimson Warden", "Ash Thorn",
"Scarlet Hunter", "Feral Eclipse", "Obsidian Widow", "Grave Siren", "Ember Claw",
"Shade Raptor", "Night Ember", "Dagger Rain", "Cinder Hawk", "Phantom Ash"
]
},
{
"name": "Titan Forge",
"writers": [
"Isabella Moore", "Rajesh Kapoor", "Daniela Costa", "Simon Gallagher", "Freya Clarke",
"Victor Morales", "Lin Wei", "Sophia Nguyen", "Jacob Romero", "Yara Haddad",
"Anthony Blake", "Claudia Rossi", "Marcus Young", "Zahra Khalid", "Lorenzo Garcia",
"Tina Howard", "Mikhail Petrov", "Ella Simmons", "Andre Moreau", "Chloe Bennett"
],
"characters": [
"Colossus King", "Stonehammer", "Iron Leviathan", "Thunder Forge", "Goliath Seraph",
"Obsidian Titan", "Warbringer", "Ashen Behemoth", "Cyclone Crusher", "Pyre Colossus",
"Steel Mauler", "Shadow Gargoyle", "Molten Warden", "Skybreaker", "Bonecrusher",
"Titan Shade", "Iron Fistlord", "Storm Colossus", "Granite Stalker", "Vortex Colossus"
]
},
{
"name": "Silver Crown",
"writers": [
"Gabriella Reyes", "Tomas Varga", "Sienna Blake", "Ethan Walsh", "Leila Novak",
"Mateo Rossi", "Aisha Karim", "Noelle Chen", "Patrick Doyle", "Fatima Rahman",
"Oliver King", "Helena Duarte", "Jonah Price", "Daphne Winters", "Niklas Berger",
"Rami Youssef", "Monica Cruz", "David Stein", "Chiara Marino", "Callum Reid"
],
"characters": [
"Silver Hawk", "Moonlight Herald", "Crystal Viper", "Diamond Fang", "Gleam Phantom",
"Aurora Wolf", "Radiant Knight", "Mirror Queen", "Glass Seraph", "Ice Dagger",
"Frost Warden", "Shimmer Raven", "Opal Striker", "Moon Serpent", "Glacier Monarch",
"Diamond Shadow", "Iridescent Fang", "Snow Whisper", "Lustre Talon", "Shineblade"
]
},
{
"name": "Oblivion Works",
"writers": [
"Rosa Delgado", "Leon Wu", "Katerina Petrov", "Andres Vega", "Marisol Alvarez",
"Oskar Nilsson", "Emily Parker", "Hassan Farouk", "Kylie Summers", "Roberto Mendes",
"Sophia Laurent", "Zane Carter", "Mona Ali", "Ivan Kuznetsov", "Harriet Cook",
"Thiago Costa", "Ingrid Weber", "Jordan Price", "Farah Nasser", "Dominic Hayes"
],
"characters": [
"Oblivion Knight", "Chaos Reaver", "Dread Monarch", "Eternal Shade", "Abyss Serpent",
"Entropy Fang", "Hollow Wraith", "Black Maw", "Infinite Oracle", "Phantom Vortex",
"Nether Lord", "Ash Revenant", "Corruptor King", "Wastelord", "Forgotten Child",
"Ember Shade", "Decay Rider", "Grim Talon", "Unbound Harbinger", "Shattered Seraph"
]
},
{
"name": "Blue Horizon",
"writers": [
"Miguel Santos", "Clara Jensen", "Ahmed Said", "Louise Martin", "Nikhil Sharma",
"Olivia Scott", "Kurt Wagner", "Sophia Brown", "Benjamin Clarke", "Sofia Lopez",
"Isaac Turner", "Lucia Ferraro", "Noor Hassan", "Peter Novak", "Carmen Ortiz",
"Dominic Hughes", "Hana Lee", "George Wallace", "Amelie Laurent", "Rory Grant"
],
"characters": [
"Skyblade", "Aero Fang", "Storm Eagle", "Cloud Seraph", "Gust Phantom",
"Tempest Wing", "Azure Knight", "Wind Whisper", "Blue Talon", "Stratos Wolf",
"Cumulus Shade", "Horizon Rider", "Lightning Dove", "Storm Veil", "Cyclone Viper",
"Sky Oracle", "Thunder Hawk", "Boreal Fang", "Nimbus Striker", "Wind Serpent"
]
},
{
"name": "Emerald Realm",
"writers": [
"Jonas Eriksen", "Maria Paredes", "Keira O'Connor", "Liam Murphy", "Valeria Soto",
"Hugo Martins", "Sarah Johansson", "Amir Rahim", "Nicole Becker", "Diego Vargas",
"Claudia Silva", "Edward Harris", "Minji Park", "Sven Andersson", "Luciana Ribeiro",
"Adrian Holt", "Rania Mansour", "Tomasz Kowalski", "Laura Dupont", "Connor Mitchell"
],
"characters": [
"Emerald Guardian", "Green Wraith", "Forest Fang", "Venomous Ivy", "Serpent Root",
"Nature Phantom", "Verdant Talon", "Jungle Oracle", "Moss Seraph", "Leaf Blade",
"Bark Stalker", "Wildwood Hunter", "Vine Striker", "Timber Shade", "Swamp Dagger",
"Grove Sentinel", "Thorn Rider", "Canopy Hawk", "Willow Phantom", "Feral Root"
]
},
{
"name": "Phantom Quill",
"writers": [
"Evelyn Ross", "Arman Khatri", "Marta Lopez", "Oliver Hughes", "Naomi Fischer",
"Felipe Andrade", "Claudia Vega", "Erik Johansson", "Priya Raman", "Theo Brandt",
"Lucille Chang", "David Moretti", "Anna Kowalska", "Ravi Mehta", "Camila Borges",
"Sebastian Weber", "Helene Marchand", "Tariq Hassan", "Alicia Moreno", "Victor Rojas"
],
"characters": [
"Phantom Dagger", "Ink Wraith", "Quill Serpent", "Paper Shade", "Inkblot Oracle",
"Spectral Scribe", "Silent Raven", "Ghost Writer", "Obsidian Pen", "Black Scroll",
"Word Seraph", "Ink Storm", "Ashen Tome", "Script Phantom", "Scroll Whisper",
"Vellum Shade", "Runic Hunter", "Blotfang", "Manuscript Queen", "Glyph Warden"
]
},
{
"name": "Eclipse House",
"writers": [
"Tatiana Volkova", "Jonathan Cruz", "Aria West", "Mateo Torres", "Katrin Müller",
"Bruno Fernandes", "Sofia Adams", "Isak Lund", "Beatriz Silva", "Rohan Malhotra",
"Cecilia Anders", "Elias Becker", "Anastasia Ivanova", "Marcus Rossi", "Nina Bell",
"Jorge Mendez", "Amal Hussein", "Sophie Laurent", "Felix Braun", "Maya Patel"
],
"characters": [
"Shadow Eclipse", "Lunar Fang", "Midnight Oracle", "Dark Seraph", "Crescent Blade",
"Eclipse Wraith", "Solar Dagger", "Twilight Rider", "Moon Fang", "Night Serpent",
"Black Horizon", "Stellar Shade", "Dusk Phantom", "Umbra Queen", "Noctis Striker",
"Silent Eclipse", "Shadow Whisper", "Midnight Raven", "Sunless Talon", "Obscura Wolf"
]
}
],
"works": [
"Odyssey", "Inferno", "Chronicles", "Saga", "Mythos",
"Legends", "Codex", "Epic", "Fables", "Annals",
"Tales", "Scrolls", "Gospels", "Parables", "Revelations",
"Odyssey II", "Myths of Old", "The Forgotten Path", "War of Realms", "The Last Ember",
"Rise of Shadows", "Fall of Kings", "Storms Herald", "Ashes of Eternity", "Shattered Throne",
"Celestial War", "The Lost Age", "Dark Horizon", "Beyond the Void", "Silent Skies",
"The Eternal Flame", "The Broken Crown", "March of Iron", "The Frozen Gate", "Blood and Ash",
"Cinderfall", "Stormfire", "The Glass Throne", "Oblivion Tide", "The Verdant Curse",
"Moonfall", "Night Reign", "The Crimson Veil", "The Phantom Crown", "The Shattered Mask",
"The Hollow Tome", "The Eternal Eclipse", "The Radiant Path", "The Starlight Forge", "The Forgotten Realm"
]
}

166
comicdata2.json Normal file
View File

@@ -0,0 +1,166 @@
{
"publishers": [
{
"name": "Shadowspire Comics",
"writers": [
"Harper Lane", "Diego Mendes", "Aria Clarke", "Stefan Novak", "Lina Ortega",
"Maximilian Frost", "Aya Nakamura", "Cole Bryant", "Rhea Kapoor", "Jonas Müller",
"Claudia Navarro", "Victor Holm", "Leah Simmons", "Rajiv Menon", "Sophie Delgado",
"Nikolai Varga", "Paula Jensen", "Hassan Zayed", "Juliette Moreau", "Ethan Blake"
],
"characters": [
"Shadow Talon", "Night Seraph", "Whisper Fang", "Grim Oracle", "Midnight Claw",
"Phantom Raven", "Darkhowl", "Cinder Viper", "Ash Seraph", "Void Striker",
"Pale Dagger", "Moonfang", "Silent Talon", "Eclipse Hunter", "Obsidian Phantom",
"Gloom Rider", "Shattered Mask", "Dusk Serpent", "Twilight Fang", "Nether Raven"
]
},
{
"name": "Solaris Studio",
"writers": [
"Daniel Costa", "Freya Nilsson", "Aman Patel", "Gabrielle Fox", "Tomás Rivera",
"Lena Hoffman", "Marco Rinaldi", "Anjali Desai", "Erik Larson", "Sophia Jensen",
"Pedro Silva", "Chiara Bianchi", "Adam Novak", "Fatima Zahra", "Noah James",
"Lucia Vega", "Samuel Cross", "Elena Morales", "Jonah Wright", "Nadia Petrov"
],
"characters": [
"Sunflare", "Solar Warden", "Helios Fang", "Blaze Seraph", "Radiant Queen",
"Cinder Phoenix", "Corona Knight", "Aurora Flame", "Light Bringer", "Inferno Wolf",
"Ember Talon", "Solar Serpent", "Ashen Dawn", "Flare Monarch", "Bright Oracle",
"Sunblade", "Luminous Shade", "Golden Talon", "Pyre Rider", "Solaris Phantom"
]
},
{
"name": "Ironpeak Publishing",
"writers": [
"Marta Kowalczyk", "Owen Kelly", "Yasmin Darzi", "Igor Petrov", "Sofia Rojas",
"Hugo Bernard", "Priya Singh", "Julien Martin", "Camila Duarte", "Ali Hassan",
"Greta Larsson", "Enzo Costa", "Maya Robinson", "David Chan", "Helena Fischer",
"Adrian Brown", "Tarek Osman", "Isabella Romano", "Patrick O'Neil", "Leila Karim"
],
"characters": [
"Iron Sentinel", "Steel Fang", "Titan Hunter", "Obsidian Blade", "Forge Phantom",
"Hammer Seraph", "Granite Fang", "Crucible Knight", "Ash Titan", "Ironshade",
"Steel Oracle", "Stone Raven", "Molten Rider", "Bastion Fang", "Iron Wraith",
"Cinder Anvil", "Storm Forge", "Bronze Talon", "Metal Serpent", "Iron Sovereign"
]
},
{
"name": "Obsidian Crown",
"writers": [
"Clara Weiss", "Miguel Ortiz", "Anya Kuznetsova", "Jamal Green", "Valeria Pinto",
"Elias Sørensen", "Charlotte Payne", "Nikolaj Ivanov", "Hannah Cole", "Yusuf Hassan",
"Irene Romano", "Oskar Svensson", "Gabriel Mendes", "Aya Al-Masri", "Luciano Paredes",
"Ella Mitchell", "Thiago Rocha", "Mila Hart", "Jonas Weber", "Zara Ali"
],
"characters": [
"Crown Serpent", "Shadow Regent", "Onyx Warden", "Sable Knight", "Thorn Monarch",
"Ash Queen", "Phantom Regent", "Black Dagger", "Crown of Thorns", "Gloom Sovereign",
"Obsidian Fang", "Dark Crown", "Veil Monarch", "Crimson Regent", "Twilight Baron",
"Iron Crown", "Shattered King", "Phantom Court", "Ebon Seraph", "Cinder Throne"
]
},
{
"name": "Aurora Gate",
"writers": [
"Eva Torres", "Rami Khalil", "Greta Fischer", "Lucas Wang", "Mona Richter",
"Antonio Gomez", "Selin Yilmaz", "Hiroshi Tanaka", "Chiara Conti", "Markus Bauer",
"Dina Hassan", "George Carter", "Emilia Rossi", "Arjun Sethi", "Patricia Long",
"Noel Andersen", "Fatima Morales", "Leo Becker", "Aisha Khan", "Oscar Hughes"
],
"characters": [
"Aurora Fang", "Dawn Seraph", "Gleam Phantom", "Morning Blade", "Sunrise Warden",
"Horizon Rider", "Radiant Fang", "Shimmer Serpent", "Golden Herald", "Twilight Oracle",
"Prism Fang", "Daybreak Hunter", "Halo Knight", "Light Whisper", "Aurora Wolf",
"Glisten Queen", "Lustre Shade", "Skyshine", "Crystal Seraph", "Brilliance Rider"
]
},
{
"name": "Crucible House",
"writers": [
"Isla Bennett", "Ricardo Nunes", "Fatemeh Azimi", "Anton Volkov", "Sofia Allen",
"Leandro Pereira", "Helene Dubois", "Rafael Vega", "Nora Johansson", "Tobias Schmidt",
"Natalia Petrova", "Andre Clarke", "Olga Vasilev", "Marcus Bell", "Sahar Rahimi",
"Leonard Hughes", "Emma Karlsson", "David Moreira", "Rashid Farooq", "Laura Schmidt"
],
"characters": [
"Forge Seraph", "Anvil Fang", "Ash Knight", "Molten Oracle", "Iron Flame",
"Cinder Fang", "Hammer Shade", "Steel Herald", "Furnace Rider", "Ember Sovereign",
"Pyre Fang", "Brass Serpent", "Granite Warden", "Blazing Phantom", "Bronze Monarch",
"Ashen Forge", "Smelt Talon", "Shatter Seraph", "Obsidian Hammer", "Inferno Sentinel"
]
},
{
"name": "Tempest Reach",
"writers": [
"Amelie Laurent", "Hasan Malik", "Jonas Becker", "Camille Fontaine", "Pedro Costa",
"Sofia Eriksson", "Yousef Ibrahim", "Nina Alvarez", "Daniel Wong", "Olivia Meyer",
"Sebastian Rossi", "Isabel Novak", "Hassan El-Sayed", "Lucas Andersson", "Fiona Murphy",
"Viktor Pavlov", "Giulia Romano", "Jacob Lee", "Anita Sharma", "Mateo Alvarez"
],
"characters": [
"Tempest Fang", "Storm Oracle", "Cyclone Rider", "Thunder Warden", "Hurricane Queen",
"Lightning Seraph", "Gale Phantom", "Sky Striker", "Typhoon Fang", "Rain Sovereign",
"Cloud Serpent", "Vortex Fang", "Stormshade", "Bolt Rider", "Zephyr Knight",
"Gust Phantom", "Storm Talon", "Boreal Seraph", "Thunder Fang", "Squall Hunter"
]
},
{
"name": "Vermilion Arts",
"writers": [
"Nora Kim", "Santiago Ruiz", "Helena Papadopoulos", "Karim Saad", "Yara Silva",
"Andrei Popescu", "Lucia Hernandez", "Victor Johansson", "Mei Chen", "Mateus Carvalho",
"Rania Osman", "Theo Jensen", "Laila Karim", "Enrique Morales", "Zoe Fischer",
"Hugo Schmidt", "Alina Petrova", "Marcus Rivera", "Ella O'Connor", "Ibrahim Khaled"
],
"characters": [
"Vermilion Fang", "Crimson Oracle", "Scarlet Knight", "Blood Seraph", "Ruby Phantom",
"Ember Regent", "Red Talon", "Flame Sovereign", "Inferno Hunter", "Scarlet Shade",
"Rose Dagger", "Carmine Fang", "Obsidian Ruby", "Shattered Crimson", "Ember Queen",
"Bloodfang", "Sanguine Rider", "Vermilion Sovereign", "Cinder Rose", "Ruby Serpent"
]
},
{
"name": "Eternal Spire",
"writers": [
"Alejandro Torres", "Frederik Hansen", "Lea Müller", "Omar Rahman", "Camila Rocha",
"Noah Fischer", "Nina Petrova", "Rafael Oliveira", "Sarah Collins", "Mustafa Ali",
"Anna Eriksen", "Henrik Olsen", "Giovanni Rizzo", "Maya Grant", "Farid Hassan",
"Carolina Reyes", "Emil Larsen", "Tara Blake", "Lorenzo Bianchi", "Sofia Johansson"
],
"characters": [
"Eternal Fang", "Spire Seraph", "Tower Phantom", "Obelisk Fang", "Pillar Warden",
"Everlight Queen", "Infinite Oracle", "Beacon Knight", "Timeless Shade", "Aegis Hunter",
"Unending Serpent", "Endless Fang", "Crown Spire", "Everlast Regent", "Looming Sovereign",
"Immortal Talon", "Celestial Spire", "Dawn Tower", "Radiant Fang", "Infinite Dawn"
]
},
{
"name": "Mythos Guild",
"writers": [
"Giulia Conti", "Zaid Ahmed", "Martina Rossi", "Henrik Berg", "Layla Hassan",
"Andreas Müller", "Clara Rinaldi", "Hussein Omar", "Sofia Almeida", "Victor Karlsson",
"Cecilia Torres", "Jonas Lindholm", "Fatima Qureshi", "Diego Pereira", "Astrid Holm",
"Mustafa Khan", "Lara Vogel", "Tiago Fernandes", "Amina Farah", "Michael Steiner"
],
"characters": [
"Mythic Fang", "Legend Seraph", "Oracle of Fables", "Saga Warden", "Chronicle Phantom",
"Epic Fang", "Codex Knight", "Parable Serpent", "Revelation Hunter", "Scroll Sovereign",
"Tale Phantom", "Annals Fang", "Saga Regent", "Legend Shade", "Eternal Epic",
"Codex Oracle", "Fable Rider", "Chronicle Queen", "Saga Flame", "Mythos Phantom"
]
}
],
"works": [
"The Eternal Tower", "Rise of Dawn", "Fragments of Fire", "Winds of Time", "The Shattered Spire",
"Ashfall", "Crown of Ashes", "The Silver Path", "Voices of the Deep", "The Forgotten Flame",
"Broken Realms", "Nightfall Rising", "The Crimson Sky", "Echoes of Silence", "The Hidden Forge",
"Wardens Call", "The Last Oracle", "The Iron Banner", "Moonveil", "Thornbound",
"Stormward", "The Lost Citadel", "The Fractured Realm", "Blades of Twilight", "Sands of Eternity",
"The Obsidian Pact", "Cindersong", "The Ember Crown", "Chains of Dawn", "Specters Oath",
"The Hollow Forge", "Echo of the Ancients", "The Stolen Throne", "Blood of the Fallen", "The Frozen Spire",
"The Burning Tome", "Nightspire", "The Radiant Crown", "Wings of Ash", "The Drowned Throne",
"Fates Eclipse", "Shadowsong", "Crown of Winter", "The Silent March", "Ironveil",
"The Verdant Throne", "Celestial Pact", "The Whispering Gate", "Crimson Chains", "The Luminous Void"
]
}

372
generate_cbz.py Normal file
View File

@@ -0,0 +1,372 @@
#!/usr/bin/env python3
import argparse
import io
import json
import random
import zipfile
import re
from datetime import datetime
from pathlib import Path
try:
from PIL import Image, ImageDraw, ImageFont
except ImportError:
raise SystemExit("Please install Pillow first: pip install pillow")
MONTHS = [
"January","February","March","April","May","June",
"July","August","September","October","November","December"
]
MAX_YEAR = 2025 # <- hard cap
FORMAT_OPTIONS = [
"Main Series",
"Limited Series",
"One-Shot",
"TPB",
"Annual",
"Preview",
"Balck & White",
"Black & White",
"Director'Cut",
"Director's Cut",
"Graphic Novel"
]
def normalize_format(fmt: str) -> str:
f = fmt.strip().lower()
if f == "main series":
return "Main Series"
if f in {"limited series", "limited"}:
return "Limited Series"
if f in {"one-shot", "oneshot"}:
return "One-Shot"
if f in {"tpb", "trade", "trade paperback"}:
return "TPB"
if f == "annual":
return "Annual"
if f in {"director's cut", "director'cut", "directors cut"}:
return "Director's Cut"
return fmt
def load_data(json_path: Path):
with open(json_path, "r", encoding="utf-8") as f:
data = json.load(f)
pubs_raw = data.get("publishers") or []
if not pubs_raw or not isinstance(pubs_raw, list):
raise ValueError("`publishers` must be a list of objects.")
publishers, writers_by_pub, chars_by_pub = [], {}, {}
for p in pubs_raw:
if not isinstance(p, dict): continue
name = p.get("name")
if not name: continue
publishers.append(name)
writers_by_pub[name] = list(p.get("writers") or [])
chars_by_pub[name] = list(p.get("characters") or [])
if not publishers:
raise ValueError("No valid publishers with names found.")
works = data.get("works") or []
if not works:
works = [
"Odyssey","Legacy","Eclipse","Frontier","Spectrum","Monolith","Harbinger",
"Chronicle","Vanguard","Paradox","Catalyst","Requiem","Arcadia","Equinox",
"Ironclad","Apex","Arc","Vector","Nimbus","Cinder"
]
return publishers, writers_by_pub, chars_by_pub, works
def slugify(text: str):
keep = "-_.()[]#, "
return "".join(c for c in text if c.isalnum() or c in keep).strip()
def rand_series_title(works):
w1 = random.choice(works)
pattern = random.choice([
"{w1}","The {w1}","{w1} Chronicle","{w1}: Genesis","{w1} Rising","{w1} Reborn",
"{w1} & {w2}","{w1} of {w2}","{w1}: {w2}",
])
if "{w2}" in pattern:
choices = [w for w in works if w != w1] or works
w2 = random.choice(choices)
return pattern.format(w1=w1, w2=w2)
return pattern.format(w1=w1)
def choose_writer(publisher, writers_by_pub):
lst = writers_by_pub.get(publisher) or []
if lst: return random.choice(lst)
return random.choice([
"Alex Grant","Taylor Miller","Jordan Bishop","Morgan Reeves","Riley Carter",
"Sam Hayes","Casey Harper","Jamie Brooks","Avery Collins","Quinn Rowe"
])
def choose_character(publisher, chars_by_pub):
lst = chars_by_pub.get(publisher) or []
if lst: return random.choice(lst)
return random.choice([
"Sentinel","Nightglass","Starflare","Iron Warden","Moonstrike","Volt Runner","Red Quill"
])
def add_months(year: int, month: int, delta: int):
idx = (year * 12 + (month - 1)) + delta
new_year = idx // 12
new_month = (idx % 12) + 1
return new_year, new_month
def sub_months(year: int, month: int, delta: int):
idx = (year * 12 + (month - 1)) - delta
new_year = idx // 12
new_month = (idx % 12) + 1
return new_year, new_month
def rand_start_date_for_monthly(n_issues: int, year_min: int = 1960):
"""
Choose a random start (year,month) such that start + (n_issues-1) months <= Dec MAX_YEAR.
"""
latest_y, latest_m = sub_months(MAX_YEAR, 12, max(0, n_issues - 1))
# Build month-index range
min_idx = year_min * 12 # Jan
max_idx = latest_y * 12 + (latest_m - 1)
if max_idx < min_idx:
# If range is invalid, clamp to year_min Jan
return year_min, 1
pick = random.randint(min_idx, max_idx)
return pick // 12, (pick % 12) + 1
def rand_start_date_for_annuals(n_issues: int, year_min: int = 1960):
"""
Choose a start year so that start_year + (n_issues - 1) <= MAX_YEAR.
Month can be any (fixed across annuals).
"""
latest_start_year = MAX_YEAR - max(0, n_issues - 1)
if latest_start_year < year_min:
latest_start_year = year_min
y = random.randint(year_min, latest_start_year)
m = random.randint(1, 12)
return y, m
def zero_pad_page(n: int) -> str:
return f"P{n:05d}.jpg"
def make_jpeg_bytes(text: str, width=1200, height=1800):
img = Image.new("RGB", (width, height), color="white")
draw = ImageDraw.Draw(img)
try:
font = ImageFont.truetype("DejaVuSans.ttf", size=64)
except Exception:
font = ImageFont.load_default()
lines = text.split("\n")
sizes = []
for line in lines:
bbox = draw.textbbox((0,0), line, font=font)
w, h = bbox[2]-bbox[0], bbox[3]-bbox[1]
sizes.append((w,h))
total_h = sum(h for _,h in sizes) + (len(lines)-1)*20
y = (height - total_h) // 2
for (line,(w,h)) in zip(lines, sizes):
x = (width - w) // 2
draw.text((x,y), line, fill="black", font=font)
y += h + 20
buf = io.BytesIO()
img.save(buf, format="JPEG", quality=90)
return buf.getvalue()
def escape_xml(s: str) -> str:
return (s.replace("&","&amp;")
.replace("<","&lt;")
.replace(">","&gt;")
.replace('"',"&quot;")
.replace("'","&apos;"))
def build_comicinfo_xml(series, number, title, volume_year, year, month,
publisher, writer, characters, fmt, page_count):
chars_joined = ", ".join(characters if isinstance(characters, (list, tuple)) else [characters])
xml = f"""<?xml version="1.0" encoding="utf-8"?>
<ComicInfo>
<Title>{escape_xml(title)}</Title>
<Series>{escape_xml(series)}</Series>
<Number>{number}</Number>
<Volume>{volume_year}</Volume>
<Year>{year}</Year>
<Month>{month}</Month>
<Publisher>{escape_xml(publisher)}</Publisher>
<Writer>{escape_xml(writer)}</Writer>
<Characters>{escape_xml(chars_joined)}</Characters>
<Format>{escape_xml(fmt)}</Format>
<LanguageISO>en</LanguageISO>
<PageCount>{page_count}</PageCount>
<Summary>Generated for application stress testing.</Summary>
</ComicInfo>
"""
return xml
def make_filename(series, issue_no, month_name, year):
return f"{series} #{issue_no:03d} [{month_name}, {year}].cbz"
def issues_for_format(fmt_norm: str) -> int:
if fmt_norm == "Main Series": return random.randint(1, 500)
if fmt_norm == "Limited Series": return random.randint(1, 15)
if fmt_norm == "One-Shot": return 1
if fmt_norm == "TPB": return random.randint(1, 10)
if fmt_norm == "Director's Cut": return random.randint(1, 5)
if fmt_norm == "Annual": return random.randint(1, 5)
return 1
# ---------- continue existing volumes ----------
def series_target_dir(base_out: Path, publisher: str, character: str, fmt_display: str,
volume_year: int, series: str) -> Path:
series_folder_name = f"({volume_year}) {series}"
return base_out / slugify(publisher) / slugify(character) / slugify(fmt_display) / slugify(series_folder_name)
def scan_existing_issue_info(target_dir: Path, series: str):
"""
Returns (existing_max_issue_no, first_issue_year, first_issue_month)
for files like 'Series Name #NNN [Month, Year].cbz'
"""
if not target_dir.exists():
return 0, None, None
max_no = 0
first_year = None
first_month = None
pat = re.compile(rf"^{re.escape(series)} #(\d{{3}}) \[([A-Za-z]+), (\d{{4}})\]$")
for p in target_dir.glob("*.cbz"):
m = pat.match(p.stem)
if not m: continue
n = int(m.group(1))
mon_name = m.group(2)
yr = int(m.group(3))
if n > max_no: max_no = n
if n == 1 and mon_name in MONTHS:
first_month = MONTHS.index(mon_name) + 1
first_year = yr
return max_no, first_year, first_month
# ---------- core generation ----------
def generate_issue_cbz(base_out: Path, publisher: str, character: str, fmt_display: str,
series: str, issue_no: int, writer: str,
volume_year: int, issue_year: int, issue_month: int, page_count: int):
# Enforce cap at the final gate too (paranoia)
if issue_year > MAX_YEAR:
return None
month_name = MONTHS[issue_month - 1]
title = f"{series} #{issue_no}"
target_dir = series_target_dir(base_out, publisher, character, fmt_display, volume_year, series)
target_dir.mkdir(parents=True, exist_ok=True)
cbz_name = make_filename(series, issue_no, month_name, issue_year)
cbz_path = target_dir / cbz_name
comicinfo_xml = build_comicinfo_xml(
series=series, number=issue_no, title=title,
volume_year=volume_year, year=issue_year, month=issue_month,
publisher=publisher, writer=writer, characters=[character],
fmt=fmt_display, page_count=page_count
)
with zipfile.ZipFile(cbz_path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
for i in range(1, page_count + 1):
filename = zero_pad_page(i)
img_bytes = make_jpeg_bytes(cbz_name if i == 1 else f"Page {i}")
zf.writestr(filename, img_bytes)
zf.writestr("ComicInfo.xml", comicinfo_xml)
return cbz_path
def estimate_total_issues(series_count: int):
total = 0
for _ in range(series_count):
fmt = normalize_format(random.choice(FORMAT_OPTIONS))
total += issues_for_format(fmt)
return total # rough estimate; cap may reduce actual total when continuing existing
def generate_one_series(base_out: Path, publishers, writers_by_pub, chars_by_pub, works,
counter, total_issues):
publisher = random.choice(publishers)
character = choose_character(publisher, chars_by_pub)
fmt_display = random.choice(FORMAT_OPTIONS)
fmt_norm = normalize_format(fmt_display)
writer = choose_writer(publisher, writers_by_pub)
series = rand_series_title(works)
if random.random() < 0.35:
series = f"{character}: {series}"
n_issues = issues_for_format(fmt_norm)
# Choose start date with MAX_YEAR cap in mind (unless continuing an existing volume)
if fmt_norm == "Annual":
start_year, start_month = rand_start_date_for_annuals(n_issues)
else:
start_year, start_month = rand_start_date_for_monthly(n_issues)
volume_year = start_year # volume = year of #1
# If folder exists, continue numbering and keep original #1 date if found
target_dir = series_target_dir(base_out, publisher, character, fmt_display, volume_year, series)
existing_max, first_y, first_m = scan_existing_issue_info(target_dir, series)
if first_y and first_m:
start_year, start_month = first_y, first_m
volume_year = first_y
start_issue = max(1, existing_max + 1)
if start_issue > n_issues:
return # nothing left to create
def rand_pages(): return random.randint(5, 10)
if fmt_norm == "Annual":
# Issue i => year = start_year + (i-1), month fixed
for issue_no in range(start_issue, n_issues + 1):
y = start_year + (issue_no - 1)
if y > MAX_YEAR:
break
m = start_month
if generate_issue_cbz(base_out, publisher, character, fmt_display, series,
issue_no, writer, volume_year, y, m, rand_pages()):
counter[0] += 1
if counter[0] % 100 == 0:
print(f"Generated {counter[0]} issues out of {total_issues}")
else:
# Monthly progression
for issue_no in range(start_issue, n_issues + 1):
y, m = add_months(start_year, start_month, issue_no - 1)
if y > MAX_YEAR:
break
if generate_issue_cbz(base_out, publisher, character, fmt_display, series,
issue_no, writer, volume_year, y, m, rand_pages()):
counter[0] += 1
if counter[0] % 100 == 0:
print(f"Generated {counter[0]} issues out of {total_issues}")
def main():
parser = argparse.ArgumentParser(description="Generate CBZ files for stress testing.")
parser.add_argument("count", type=int, help="Number of series to generate")
parser.add_argument("--out", type=Path, default=Path("output_cbz"), help="Output base directory")
parser.add_argument("--data", type=Path, default=Path("./comicdata.json"),
help="Path to comic data JSON (default: ./comicdata.json)")
parser.add_argument("--seed", type=int, default=None, help="Random seed for reproducibility")
args = parser.parse_args()
if args.seed is not None:
random.seed(args.seed)
publishers, writers_by_pub, chars_by_pub, works = load_data(args.data)
args.out.mkdir(parents=True, exist_ok=True)
total_issues_est = estimate_total_issues(args.count) # rough
if args.seed is not None:
random.seed(args.seed)
counter = [0]
for _ in range(args.count):
generate_one_series(args.out, publishers, writers_by_pub, chars_by_pub, works,
counter, total_issues_est)
print(f"Done. Generated {counter[0]} issues total (estimated {total_issues_est}).")
if __name__ == "__main__":
main()