image2mcq 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 image2mcq
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,194 @@
1
+ Metadata-Version: 2.4
2
+ Name: image2mcq
3
+ Version: 1.0.0
4
+ Summary: Convert images (screenshots, scanned pages, diagrams) into MCQ questions using AI
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/manjur-ai/image2mcq
7
+ Project-URL: Issues, https://github.com/manjur-ai/image2mcq/issues
8
+ Keywords: mcq,quiz,ai,education,image,ocr,vision,llm,openrouter
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Intended Audience :: Education
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Education
18
+ Classifier: Topic :: Scientific/Engineering :: Image Recognition
19
+ Requires-Python: >=3.9
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: anthropic>=0.25
23
+ Requires-Dist: openai>=1.30
24
+ Requires-Dist: Pillow>=10.0
25
+ Requires-Dist: pytesseract>=0.3
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=7; extra == "dev"
28
+ Requires-Dist: pytest-cov; extra == "dev"
29
+ Requires-Dist: build; extra == "dev"
30
+ Requires-Dist: twine; extra == "dev"
31
+ Dynamic: license-file
32
+
33
+ # image2mcq
34
+
35
+ Convert images — screenshots, scanned pages, diagrams, charts, and photographs — into high-quality MCQ questions using AI.
36
+
37
+ Built on top of **html2mcq**'s image pipeline, extracted as a standalone library focused purely on image-to-MCQ generation.
38
+
39
+ ---
40
+
41
+ ## Features
42
+
43
+ - **Two processing methods:**
44
+ - `twostep` (default) — OCR image text, then generate MCQs from extracted text
45
+ - `images2mcq` — send images directly to a vision LLM for MCQ generation
46
+ - **Multiple AI providers:** OpenRouter, Anthropic, OpenAI, Ollama
47
+ - **Auto model failover:** if one model fails (e.g. quota exhausted), automatically tries the next
48
+ - **Local OCR fallback:** Tesseract OCR when vision APIs are unavailable
49
+ - **CLI & Python API** — use from terminal or integrate into your code
50
+
51
+ ---
52
+
53
+ ## Quick Start
54
+
55
+ ### CLI
56
+
57
+ ```bash
58
+ # Single image file
59
+ image2mcq --image-path diagram.png -n 5
60
+
61
+ # Multiple image URLs
62
+ image2mcq --image-url https://example.com/chart1.png --image-url https://example.com/chart2.png
63
+
64
+ # Scan a folder of images
65
+ image2mcq --image-folder ./lecture-slides/ --method images2mcq
66
+
67
+ # Output as JSON
68
+ image2mcq --image-path notes.png -o questions.json --format json
69
+
70
+ # Use n=999 to generate as many as the content supports
71
+ image2mcq --image-path textbook-page.png
72
+ ```
73
+
74
+ ### Python API
75
+
76
+ ```python
77
+ from image2mcq import ImageMCQGenerator
78
+
79
+ gen = ImageMCQGenerator(
80
+ api_key="sk-or-v1-...",
81
+ provider="openrouter",
82
+ mcq_model="google/gemini-2.5-flash-lite",
83
+ )
84
+
85
+ # From local files
86
+ mcq = gen.from_image_paths("screenshot.png", n=5)
87
+ print(mcq.to_pretty_str())
88
+
89
+ # From URLs
90
+ mcq = gen.from_image_urls("https://example.com/diagram.png", n=3)
91
+ print(mcq.to_json())
92
+
93
+ # From multiple images
94
+ mcq = gen.from_image_paths(["page1.png", "page2.png", "page3.png"])
95
+ ```
96
+
97
+ ### Two-Step (OCR → MCQ)
98
+
99
+ ```python
100
+ gen = ImageMCQGenerator(
101
+ api_key="sk-or-v1-...",
102
+ method="twostep", # default
103
+ )
104
+ mcq = gen.from_image_paths("scanned-page.png", n=10)
105
+ ```
106
+
107
+ ### Images2MCQ (Vision Direct)
108
+
109
+ ```python
110
+ gen = ImageMCQGenerator(
111
+ api_key="sk-or-v1-...",
112
+ method="images2mcq",
113
+ mcq_model="openai/gpt-4o", # vision model
114
+ )
115
+ mcq = gen.from_image_paths("architecture-diagram.png", n=5)
116
+ ```
117
+
118
+ ### Custom Instructions
119
+
120
+ ```python
121
+ mcq = gen.from_image_paths(
122
+ "graph.png",
123
+ n=5,
124
+ difficulty_mix="50% easy, 50% hard",
125
+ focus_topics=["data structures", "time complexity"],
126
+ custom_instructions="Make answers very close and confusing",
127
+ )
128
+ ```
129
+
130
+ ### Auto Model Selection
131
+
132
+ ```python
133
+ gen = ImageMCQGenerator(
134
+ api_key="sk-or-v1-...",
135
+ mcq_model="auto",
136
+ mcq_model_list=[
137
+ "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free",
138
+ "google/gemma-4-31b-it:free",
139
+ ],
140
+ )
141
+ ```
142
+
143
+ ### Environment Variables
144
+
145
+ | Variable | Purpose |
146
+ |---|---|
147
+ | `OPENROUTER_API_KEY` | Default API key for OpenRouter |
148
+ | `ANTHROPIC_API_KEY` | API key for Anthropic |
149
+ | `OPENAI_API_KEY` | API key for OpenAI |
150
+ | `IMAGE2MCQ_MCQ_MODELS` | Comma-separated MCQ model priority list for `model="auto"` |
151
+ | `IMAGE2MCQ_OCR_MODELS` | Comma-separated OCR model priority list for `ocr_model="auto"` |
152
+
153
+ ---
154
+
155
+ ## Output Format
156
+
157
+ ```python
158
+ # Pretty-print
159
+ print(mcq.to_pretty_str())
160
+
161
+ # JSON
162
+ print(mcq.to_json())
163
+ # {
164
+ # "total_exam_time": 20,
165
+ # "questions": [
166
+ # {
167
+ # "question_html": "What is the time complexity of binary search?",
168
+ # "options": ["O(n)", "O(log n)", "O(n^2)", "O(1)"],
169
+ # "answers": [1],
170
+ # "multi": false,
171
+ # "marks": 1.0,
172
+ # "negative_marks": 0.25,
173
+ # "difficulty": "easy",
174
+ # "explaination": "Binary search halves the search space each iteration."
175
+ # }
176
+ # ]
177
+ # }
178
+ ```
179
+
180
+ ---
181
+
182
+ ## Installation
183
+
184
+ ```bash
185
+ pip install image2mcq
186
+ ```
187
+
188
+ For OCR support, also install [Tesseract](https://github.com/tesseract-ocr/tesseract).
189
+
190
+ ---
191
+
192
+ ## License
193
+
194
+ MIT
@@ -0,0 +1,162 @@
1
+ # image2mcq
2
+
3
+ Convert images — screenshots, scanned pages, diagrams, charts, and photographs — into high-quality MCQ questions using AI.
4
+
5
+ Built on top of **html2mcq**'s image pipeline, extracted as a standalone library focused purely on image-to-MCQ generation.
6
+
7
+ ---
8
+
9
+ ## Features
10
+
11
+ - **Two processing methods:**
12
+ - `twostep` (default) — OCR image text, then generate MCQs from extracted text
13
+ - `images2mcq` — send images directly to a vision LLM for MCQ generation
14
+ - **Multiple AI providers:** OpenRouter, Anthropic, OpenAI, Ollama
15
+ - **Auto model failover:** if one model fails (e.g. quota exhausted), automatically tries the next
16
+ - **Local OCR fallback:** Tesseract OCR when vision APIs are unavailable
17
+ - **CLI & Python API** — use from terminal or integrate into your code
18
+
19
+ ---
20
+
21
+ ## Quick Start
22
+
23
+ ### CLI
24
+
25
+ ```bash
26
+ # Single image file
27
+ image2mcq --image-path diagram.png -n 5
28
+
29
+ # Multiple image URLs
30
+ image2mcq --image-url https://example.com/chart1.png --image-url https://example.com/chart2.png
31
+
32
+ # Scan a folder of images
33
+ image2mcq --image-folder ./lecture-slides/ --method images2mcq
34
+
35
+ # Output as JSON
36
+ image2mcq --image-path notes.png -o questions.json --format json
37
+
38
+ # Use n=999 to generate as many as the content supports
39
+ image2mcq --image-path textbook-page.png
40
+ ```
41
+
42
+ ### Python API
43
+
44
+ ```python
45
+ from image2mcq import ImageMCQGenerator
46
+
47
+ gen = ImageMCQGenerator(
48
+ api_key="sk-or-v1-...",
49
+ provider="openrouter",
50
+ mcq_model="google/gemini-2.5-flash-lite",
51
+ )
52
+
53
+ # From local files
54
+ mcq = gen.from_image_paths("screenshot.png", n=5)
55
+ print(mcq.to_pretty_str())
56
+
57
+ # From URLs
58
+ mcq = gen.from_image_urls("https://example.com/diagram.png", n=3)
59
+ print(mcq.to_json())
60
+
61
+ # From multiple images
62
+ mcq = gen.from_image_paths(["page1.png", "page2.png", "page3.png"])
63
+ ```
64
+
65
+ ### Two-Step (OCR → MCQ)
66
+
67
+ ```python
68
+ gen = ImageMCQGenerator(
69
+ api_key="sk-or-v1-...",
70
+ method="twostep", # default
71
+ )
72
+ mcq = gen.from_image_paths("scanned-page.png", n=10)
73
+ ```
74
+
75
+ ### Images2MCQ (Vision Direct)
76
+
77
+ ```python
78
+ gen = ImageMCQGenerator(
79
+ api_key="sk-or-v1-...",
80
+ method="images2mcq",
81
+ mcq_model="openai/gpt-4o", # vision model
82
+ )
83
+ mcq = gen.from_image_paths("architecture-diagram.png", n=5)
84
+ ```
85
+
86
+ ### Custom Instructions
87
+
88
+ ```python
89
+ mcq = gen.from_image_paths(
90
+ "graph.png",
91
+ n=5,
92
+ difficulty_mix="50% easy, 50% hard",
93
+ focus_topics=["data structures", "time complexity"],
94
+ custom_instructions="Make answers very close and confusing",
95
+ )
96
+ ```
97
+
98
+ ### Auto Model Selection
99
+
100
+ ```python
101
+ gen = ImageMCQGenerator(
102
+ api_key="sk-or-v1-...",
103
+ mcq_model="auto",
104
+ mcq_model_list=[
105
+ "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free",
106
+ "google/gemma-4-31b-it:free",
107
+ ],
108
+ )
109
+ ```
110
+
111
+ ### Environment Variables
112
+
113
+ | Variable | Purpose |
114
+ |---|---|
115
+ | `OPENROUTER_API_KEY` | Default API key for OpenRouter |
116
+ | `ANTHROPIC_API_KEY` | API key for Anthropic |
117
+ | `OPENAI_API_KEY` | API key for OpenAI |
118
+ | `IMAGE2MCQ_MCQ_MODELS` | Comma-separated MCQ model priority list for `model="auto"` |
119
+ | `IMAGE2MCQ_OCR_MODELS` | Comma-separated OCR model priority list for `ocr_model="auto"` |
120
+
121
+ ---
122
+
123
+ ## Output Format
124
+
125
+ ```python
126
+ # Pretty-print
127
+ print(mcq.to_pretty_str())
128
+
129
+ # JSON
130
+ print(mcq.to_json())
131
+ # {
132
+ # "total_exam_time": 20,
133
+ # "questions": [
134
+ # {
135
+ # "question_html": "What is the time complexity of binary search?",
136
+ # "options": ["O(n)", "O(log n)", "O(n^2)", "O(1)"],
137
+ # "answers": [1],
138
+ # "multi": false,
139
+ # "marks": 1.0,
140
+ # "negative_marks": 0.25,
141
+ # "difficulty": "easy",
142
+ # "explaination": "Binary search halves the search space each iteration."
143
+ # }
144
+ # ]
145
+ # }
146
+ ```
147
+
148
+ ---
149
+
150
+ ## Installation
151
+
152
+ ```bash
153
+ pip install image2mcq
154
+ ```
155
+
156
+ For OCR support, also install [Tesseract](https://github.com/tesseract-ocr/tesseract).
157
+
158
+ ---
159
+
160
+ ## License
161
+
162
+ MIT
@@ -0,0 +1,13 @@
1
+ from .generator import ImageMCQGenerator
2
+ from .image_ocr import ImageOCRExtractor
3
+ from .models import MCQQuestion, MCQSet, ContentBlock
4
+
5
+ __version__ = "1.0.0"
6
+ __author__ = "image2mcq"
7
+ __all__ = [
8
+ "ImageMCQGenerator",
9
+ "ImageOCRExtractor",
10
+ "MCQQuestion",
11
+ "MCQSet",
12
+ "ContentBlock",
13
+ ]
@@ -0,0 +1,199 @@
1
+ import argparse
2
+ import json
3
+ import os
4
+ import sys
5
+ from pathlib import Path
6
+
7
+
8
+ _IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".tif", ".webp"}
9
+
10
+
11
+ def _glob_files(folder: str, extensions: set) -> list:
12
+ folder = Path(folder)
13
+ if not folder.is_dir():
14
+ print(f"Error: folder not found: {folder}", file=sys.stderr)
15
+ sys.exit(1)
16
+ files = []
17
+ for ext in extensions:
18
+ files.extend(folder.glob(f"*{ext}"))
19
+ files.extend(folder.glob(f"*{ext.upper()}"))
20
+ seen = set()
21
+ unique = []
22
+ for f in sorted(files, key=lambda p: p.name.lower()):
23
+ if f.suffix.lower() in extensions and f.name not in seen:
24
+ seen.add(f.name)
25
+ unique.append(str(f))
26
+ return unique
27
+
28
+
29
+ def _get_api_key(args):
30
+ key = args.api_key or ""
31
+ if key:
32
+ return key
33
+ env_vars = {
34
+ "openrouter": "OPENROUTER_API_KEY",
35
+ "anthropic": "ANTHROPIC_API_KEY",
36
+ "openai": "OPENAI_API_KEY",
37
+ "ollama": "",
38
+ }
39
+ env_key = env_vars.get(args.provider, "")
40
+ if env_key:
41
+ return os.environ.get(env_key, "")
42
+ return ""
43
+
44
+
45
+ def main():
46
+ parser = argparse.ArgumentParser(
47
+ prog="image2mcq",
48
+ description="Convert images (screenshots, scanned pages, diagrams) into MCQ questions using AI.",
49
+ )
50
+
51
+ parser.add_argument("--version", action="store_true", help="Show version and exit")
52
+
53
+ input_group = parser.add_argument_group("Input sources (at least one required)")
54
+ input_group.add_argument("--image-url", metavar="URL", action="append", default=[],
55
+ help="Image URL (repeatable: --image-url url1 --image-url url2)")
56
+ input_group.add_argument("--image-path", metavar="FILE", action="append", default=[],
57
+ help="Local image file path (repeatable)")
58
+ input_group.add_argument("--image-folder", metavar="DIR", default="",
59
+ help="Scan folder for images (.png, .jpg, .jpeg, .gif, .bmp, .tiff, .webp)")
60
+
61
+ gen_group = parser.add_argument_group("Generation options")
62
+ gen_group.add_argument("-n", "--n", type=int, default=999,
63
+ help="Number of questions (default: 999 = as many as content supports)")
64
+ gen_group.add_argument("--difficulty", default=None,
65
+ help='E.g. "30%% easy, 40%% medium, 30%% hard"')
66
+ gen_group.add_argument("--topics", nargs="*", help="Focus topics")
67
+ gen_group.add_argument("--instructions", "-i", default="",
68
+ help='Custom instructions e.g. "Make answers very close and confusing"')
69
+ gen_group.add_argument("--batch-size", type=int, default=10,
70
+ help="Questions per API call (default: 10)")
71
+
72
+ ai_group = parser.add_argument_group("AI provider")
73
+ ai_group.add_argument("--provider", default="openrouter",
74
+ choices=["anthropic", "openai", "openrouter", "ollama"],
75
+ help="AI provider (default: openrouter). Use 'ollama' for local LLM.")
76
+ ai_group.add_argument("--mcq-model", default="",
77
+ help="MCQ generation model (or 'auto' to try --mcq-models)")
78
+ ai_group.add_argument("--mcq-models", default="",
79
+ help="Comma-separated priority model list for --mcq-model auto. "
80
+ "Runtime-reloadable via IMAGE2MCQ_MCQ_MODELS env var.")
81
+ ai_group.add_argument("--api-key", default="",
82
+ help="API key. Falls back to OPENROUTER_API_KEY / ANTHROPIC_API_KEY / OPENAI_API_KEY env var.")
83
+ ai_group.add_argument("--ollama-base-url", default="http://localhost:11434/v1",
84
+ help="Ollama API base URL (default: http://localhost:11434/v1). "
85
+ "Only used when --provider ollama.")
86
+
87
+ ocr_group = parser.add_argument_group("OCR & image processing")
88
+ ocr_group.add_argument("--ocr-model", default="pytesseract",
89
+ help="OCR backend: 'pytesseract', 'auto', or any OpenRouter model ID "
90
+ "(e.g. 'openai/gpt-4o'). (default: pytesseract)")
91
+ ocr_group.add_argument("--ocr-models", default="",
92
+ help="Comma-separated priority model list for --ocr-model auto. "
93
+ "E.g. 'gpt-4o,gemma-27b,gemma-12b,pytesseract'")
94
+ ocr_group.add_argument("--method", default="twostep", choices=["twostep", "images2mcq"],
95
+ help="Image processing: 'twostep' (OCR->MCQ) or 'images2mcq' (vision direct). "
96
+ "(default: twostep)")
97
+ ocr_group.add_argument("--save-ocr-path", default="",
98
+ help="File path to save OCR text when method=twostep")
99
+ ocr_group.add_argument("--prompt-log-path", default="",
100
+ help="Dump prompts to file, or 'stdout' / '-' for terminal")
101
+
102
+ out_group = parser.add_argument_group("Output")
103
+ out_group.add_argument("--output", "-o", default="",
104
+ help="Output file (.json or .txt). Default: stdout")
105
+ out_group.add_argument("--format", choices=["json", "pretty"], default="pretty",
106
+ help="Output format (default: pretty)")
107
+
108
+ args = parser.parse_args()
109
+
110
+ if args.version:
111
+ try:
112
+ from image2mcq import __version__
113
+ except ImportError:
114
+ __version__ = "unknown"
115
+ print(f"image2mcq v{__version__}")
116
+ sys.exit(0)
117
+
118
+ if args.image_folder:
119
+ args.image_path.extend(_glob_files(args.image_folder, _IMAGE_EXTENSIONS))
120
+
121
+ has_input = bool(args.image_url or args.image_path)
122
+ if not has_input:
123
+ parser.print_help()
124
+ print("\nError: at least one input source is required "
125
+ "(--image-url, --image-path, --image-folder)", file=sys.stderr)
126
+ sys.exit(1)
127
+
128
+ try:
129
+ from image2mcq import ImageMCQGenerator
130
+ except ImportError as e:
131
+ print(f"Error: {e}", file=sys.stderr)
132
+ sys.exit(1)
133
+
134
+ api_key = _get_api_key(args)
135
+ ocr_models = None
136
+ if args.ocr_models:
137
+ ocr_models = [m.strip() for m in args.ocr_models.split(",") if m.strip()]
138
+ mcq_model_list = None
139
+ if args.mcq_models:
140
+ mcq_model_list = [m.strip() for m in args.mcq_models.split(",") if m.strip()]
141
+
142
+ try:
143
+ gen = ImageMCQGenerator(
144
+ api_key=api_key or None,
145
+ provider=args.provider,
146
+ mcq_model=args.mcq_model,
147
+ mcq_model_list=mcq_model_list,
148
+ batch_size=args.batch_size,
149
+ ocr_model=args.ocr_model,
150
+ ocr_models=ocr_models,
151
+ method=args.method,
152
+ save_ocr_path=args.save_ocr_path or None,
153
+ prompt_log_path=args.prompt_log_path or None,
154
+ ollama_base_url=args.ollama_base_url,
155
+ )
156
+ except ValueError as e:
157
+ print(f"Configuration error: {e}", file=sys.stderr)
158
+ sys.exit(1)
159
+
160
+ n = args.n
161
+ difficulty = args.difficulty
162
+ topics = args.topics
163
+ instructions = args.instructions or None
164
+
165
+ mcq_set = None
166
+
167
+ try:
168
+ if args.image_url and args.image_path:
169
+ print("Error: specify either --image-url or --image-path, not both.", file=sys.stderr)
170
+ sys.exit(1)
171
+ if args.image_url:
172
+ mcq_set = gen.from_image_urls(args.image_url, n=n, difficulty_mix=difficulty,
173
+ focus_topics=topics, custom_instructions=instructions)
174
+ if args.image_path:
175
+ mcq_set = gen.from_image_paths(args.image_path, n=n, difficulty_mix=difficulty,
176
+ focus_topics=topics, custom_instructions=instructions)
177
+ except Exception as e:
178
+ print(f"Generation failed: {e}", file=sys.stderr)
179
+ sys.exit(1)
180
+
181
+ if mcq_set is None or not mcq_set.questions:
182
+ print("No questions were generated.", file=sys.stderr)
183
+ sys.exit(1)
184
+
185
+ if args.format == "json":
186
+ output = mcq_set.to_json()
187
+ else:
188
+ output = mcq_set.to_pretty_str()
189
+
190
+ if args.output:
191
+ with open(args.output, "w", encoding="utf-8") as f:
192
+ f.write(output)
193
+ print(f"Saved {mcq_set.total_questions} questions to {args.output}", file=sys.stderr)
194
+ else:
195
+ print(output)
196
+
197
+
198
+ if __name__ == "__main__":
199
+ main()