Initial Upload
This commit is contained in:
153
README.md
153
README.md
@@ -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
166
comicdata.json
Normal 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", "Storm’s 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
166
comicdata2.json
Normal 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",
|
||||
"Warden’s 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", "Specter’s 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",
|
||||
"Fate’s 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
372
generate_cbz.py
Normal 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("&","&")
|
||||
.replace("<","<")
|
||||
.replace(">",">")
|
||||
.replace('"',""")
|
||||
.replace("'","'"))
|
||||
|
||||
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()
|
||||
Reference in New Issue
Block a user