Compare commits
No commits in common. "e96f23ddc84a98985ebfe329b7baeb877c319fe8" and "a24d0ea96fb5a1243da7df41983613442ab88537" have entirely different histories.
e96f23ddc8
...
a24d0ea96f
107
README.md
107
README.md
|
@ -1,76 +1,67 @@
|
||||||
# AnkiAI - Automated Anki Deck Creator
|
# AnkiAI
|
||||||
|
|
||||||
AnkiAI is a tool that leverages OCR (Optical Character Recognition) and GPT-3's powerful natural language processing capabilities to automatically generate Anki decks from images containing text.
|
AnkiAI is a robust system that converts images containing text into structured Anki cards using Optical Character Recognition (OCR) and OpenAI's GPT-4 language model. Users can quickly generate decks of flashcards from their images for effective study.
|
||||||
|
|
||||||
### Overview
|
## Features
|
||||||
|
- Converts image content to textual content using OCR.
|
||||||
|
- Uses OpenAI's GPT-4 model to structure the content into Anki decks and cards.
|
||||||
|
- Outputs the structured content as an Anki package.
|
||||||
|
|
||||||
- AnkiAI is designed to streamline the process of creating Anki decks from images.
|
## Dependencies
|
||||||
- The core idea is to use OCR to extract text from images and then use GPT-3 to transform this text into a structured Anki deck format.
|
- genanki: Used for creating Anki decks and cards.
|
||||||
- Users can make a POST request to a Flask server endpoint with their images to receive the Anki deck (.apkg file).
|
- Pillow: Image processing library.
|
||||||
|
- openai: API library for OpenAI's GPT-4 model.
|
||||||
|
- flask: Web server to host the service.
|
||||||
|
|
||||||
### Directory Structure
|
## Setup and Installation
|
||||||
|
|
||||||
- `.vscode/`: Contains configuration for VSCode debugger for Flask applications.
|
|
||||||
- `ankiai.py`: The main script that drives the creation of Anki decks from images.
|
|
||||||
- `constants.py`: Contains constant variables used across the project.
|
|
||||||
- `deck_creation.py`: Contains logic for communicating with OpenAI's API and deck creation using genanki.
|
|
||||||
- `image_processing.py`: Processes images, converting them for OCR and then performing OCR to extract text.
|
|
||||||
- `logging_config.py`: Logging configuration for the entire project.
|
|
||||||
- `server.py`: Flask server that provides an API endpoint to upload images and get back an Anki deck.
|
|
||||||
|
|
||||||
### Requirements
|
|
||||||
|
|
||||||
To run AnkiAI, you'll need to have the following dependencies installed:
|
|
||||||
|
|
||||||
```
|
|
||||||
genanki==0.8.0
|
|
||||||
Pillow
|
|
||||||
openai
|
|
||||||
flask
|
|
||||||
```
|
|
||||||
|
|
||||||
You can install these via `pip` using the `requirements.txt` file:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
### How to Run
|
|
||||||
|
|
||||||
1. **Environment Variables**: Make sure to set the `OPENAI_API_KEY` environment variable to your OpenAI API key.
|
|
||||||
|
|
||||||
2. **Run the Flask server**:
|
|
||||||
|
|
||||||
|
1. Clone this repository:
|
||||||
```bash
|
```bash
|
||||||
python server.py
|
git clone https://git.rudefox.io/bj/anki-json2ankicards.git
|
||||||
|
cd json2ankicards
|
||||||
```
|
```
|
||||||
|
|
||||||
This will start the Flask server. You can then make a POST request to `http://localhost:5000/deck-from-images` with your images to get an Anki deck.
|
2. Set up a virtual environment and activate it:
|
||||||
|
|
||||||
3. **Run Directly**:
|
|
||||||
|
|
||||||
If you prefer not to use the Flask server, you can also run `ankiai.py` directly:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python ankiai.py <directory_path_containing_images>
|
python3 -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
```
|
```
|
||||||
|
|
||||||
### How to Debug (VSCode Users)
|
3. Install the required packages:
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
- Open the project in VSCode.
|
4. Set up the OpenAI API key:
|
||||||
- Set up your breakpoints.
|
```bash
|
||||||
- Use the VSCode debugger and select "Python: Flask" to start debugging the Flask server.
|
export OPENAI_API_KEY=your_openai_api_key
|
||||||
|
```
|
||||||
|
|
||||||
### Important Notes
|
5. Run the server:
|
||||||
|
```bash
|
||||||
|
python server.py
|
||||||
|
```
|
||||||
|
|
||||||
- **API Key**: For the project to work, it is essential to have the `OPENAI_API_KEY` environment variable set.
|
## Usage
|
||||||
- **Image Types**: Currently, the image processing module supports PNG, JPG, and JPEG formats.
|
|
||||||
- **Output**: The output `.apkg` file (Anki package file) will be named `out.apkg`.
|
|
||||||
|
|
||||||
### Acknowledgements
|
1. Start the server as mentioned above.
|
||||||
|
|
||||||
This project heavily relies on the `openai` library for processing and the `genanki` library for deck generation.
|
2. Use a tool like [Postman](https://www.postman.com/) or `curl` to send images to `http://localhost:5000/deck-from-images` as a multi-part POST request.
|
||||||
|
|
||||||
### Contributions
|
3. The server will respond with a downloadable Anki package. Import this into your Anki app and start studying!
|
||||||
|
|
||||||
Contributions are always welcome. Please create a new issue or a pull request for any bug fixes or feature requests.
|
## Modules
|
||||||
|
|
||||||
|
1. **ankiai.py**: The main module that orchestrates the flow.
|
||||||
|
2. **images2text.py**: Converts image content into text using OCR.
|
||||||
|
3. **json2deck.py**: Converts structured JSON data into an Anki package.
|
||||||
|
4. **prompt4cards.py**: Uses OpenAI to structure the content into Anki decks and cards.
|
||||||
|
5. **server.py**: Flask server to host the service.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions are welcome! Please submit a pull request or open an issue to discuss changes or fixes.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[MIT License](LICENSE)
|
||||||
|
|
17
ankiai.py
17
ankiai.py
|
@ -2,18 +2,18 @@ import sys
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from logging_config import setup_logging
|
from logging_config import setup_logging
|
||||||
from image_processing import process_images
|
from images2text import main as ocr_images
|
||||||
from deck_creation import prompt_for_card_content, response_to_json, to_package
|
from prompt4cards import prompt_for_card_content, response_to_json
|
||||||
|
from json2deck import to_package
|
||||||
APKG_FILE = "out.apkg"
|
|
||||||
|
|
||||||
setup_logging()
|
setup_logging()
|
||||||
|
|
||||||
def images_to_package(directory_path):
|
def images_to_package(directory_path, outfile):
|
||||||
ocr_text = process_images(directory_path)
|
ocr_text = ocr_images(directory_path)
|
||||||
response_text = prompt_for_card_content(ocr_text)
|
response_text = prompt_for_card_content(ocr_text)
|
||||||
deck_json = response_to_json(response_text)
|
deck_json = response_to_json(response_text)
|
||||||
return to_package(deck_json)
|
to_package(deck_json).write_to_file(outfile)
|
||||||
|
logging.info(f"Deck created at: {outfile}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
@ -21,5 +21,4 @@ if __name__ == "__main__":
|
||||||
print("Usage: python ankiai.py <directory_path_containing_images>")
|
print("Usage: python ankiai.py <directory_path_containing_images>")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
images_to_package(sys.argv[1]).write_to_file(APKG_FILE)
|
images_to_package(sys.argv[1])
|
||||||
logging.info(f"Deck created at: {APKG_FILE}")
|
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
# File and Directory Constants
|
# File and Directory Constants
|
||||||
IMAGE_KEY="image"
|
|
||||||
APKG_FILE="out.apkg"
|
|
||||||
CONVERTED_DIR = "converted"
|
CONVERTED_DIR = "converted"
|
||||||
TEXT_OCR_FILE = "final.txt"
|
FINAL_OUTPUT = "final.txt"
|
||||||
IMAGE_EXTENSIONS = ['.png', '.jpg', '.jpeg']
|
IMAGE_EXTENSIONS = ['.png', '.jpg', '.jpeg']
|
||||||
DECK_JSON_FILE = "output_deck.json"
|
OUTPUT_FILENAME = "output_deck.json"
|
||||||
|
|
||||||
# API Constants
|
# API Constants
|
||||||
API_KEY_ENV = "OPENAI_API_KEY"
|
API_KEY_ENV = "OPENAI_API_KEY"
|
||||||
|
|
20
image_processing.py → images2text.py
Normal file → Executable file
20
image_processing.py → images2text.py
Normal file → Executable file
|
@ -5,21 +5,13 @@ import logging
|
||||||
from logging_config import setup_logging
|
from logging_config import setup_logging
|
||||||
from subprocess import run, CalledProcessError
|
from subprocess import run, CalledProcessError
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
from constants import CONVERTED_DIR, TEXT_OCR_FILE, IMAGE_EXTENSIONS
|
from utilities import is_image_file, ensure_directory_exists
|
||||||
|
from constants import CONVERTED_DIR, FINAL_OUTPUT
|
||||||
|
|
||||||
|
|
||||||
setup_logging()
|
setup_logging()
|
||||||
|
|
||||||
|
|
||||||
def is_image_file(path):
|
|
||||||
return any(path.lower().endswith(ext) for ext in IMAGE_EXTENSIONS)
|
|
||||||
|
|
||||||
|
|
||||||
def ensure_directory_exists(directory):
|
|
||||||
if not os.path.exists(directory):
|
|
||||||
os.mkdir(directory)
|
|
||||||
|
|
||||||
|
|
||||||
def convert_image(image_path):
|
def convert_image(image_path):
|
||||||
logging.info(f"Converting {image_path}...")
|
logging.info(f"Converting {image_path}...")
|
||||||
converted_path = os.path.join(CONVERTED_DIR, os.path.basename(image_path))
|
converted_path = os.path.join(CONVERTED_DIR, os.path.basename(image_path))
|
||||||
|
@ -70,7 +62,7 @@ def process_image(image_path):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def process_images(directory_path):
|
def main(directory_path):
|
||||||
final_text = []
|
final_text = []
|
||||||
|
|
||||||
ensure_directory_exists(CONVERTED_DIR)
|
ensure_directory_exists(CONVERTED_DIR)
|
||||||
|
@ -88,10 +80,10 @@ def process_images(directory_path):
|
||||||
|
|
||||||
# Filter out any None values and write the text to final.txt
|
# Filter out any None values and write the text to final.txt
|
||||||
final_text = [text for text in final_text if text is not None]
|
final_text = [text for text in final_text if text is not None]
|
||||||
with open(TEXT_OCR_FILE, 'w') as f:
|
with open(FINAL_OUTPUT, 'w') as f:
|
||||||
f.write("\n".join(final_text))
|
f.write("\n".join(final_text))
|
||||||
|
|
||||||
logging.info(f"All images processed! Final output saved to {TEXT_OCR_FILE}")
|
logging.info(f"All images processed! Final output saved to {FINAL_OUTPUT}")
|
||||||
return final_text # Add this line
|
return final_text # Add this line
|
||||||
|
|
||||||
|
|
||||||
|
@ -99,4 +91,4 @@ if __name__ == "__main__":
|
||||||
if len(sys.argv) != 2:
|
if len(sys.argv) != 2:
|
||||||
print("Usage: python images2text.py <directory_path>")
|
print("Usage: python images2text.py <directory_path>")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
process_images(sys.argv[1])
|
main(sys.argv[1])
|
61
json2deck.py
Normal file
61
json2deck.py
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
import json
|
||||||
|
import genanki
|
||||||
|
import sys
|
||||||
|
import logging
|
||||||
|
from logging_config import setup_logging
|
||||||
|
|
||||||
|
|
||||||
|
setup_logging()
|
||||||
|
|
||||||
|
|
||||||
|
# Create a new model for our cards. This is necessary for genanki.
|
||||||
|
MY_MODEL = genanki.Model(
|
||||||
|
1607372319,
|
||||||
|
"Simple Model",
|
||||||
|
fields=[
|
||||||
|
{"name": "Title"},
|
||||||
|
{"name": "Question"},
|
||||||
|
{"name": "Answer"},
|
||||||
|
],
|
||||||
|
templates=[
|
||||||
|
{
|
||||||
|
"name": "{{Title}}",
|
||||||
|
"qfmt": "{{Question}}",
|
||||||
|
"afmt": "{{FrontSide}}<hr id='answer'>{{Answer}}",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
def json_file_to_package(json_path):
|
||||||
|
with open(json_path, 'r', encoding='utf-8') as f:
|
||||||
|
json_data = json.load(f)
|
||||||
|
package = to_package(json_data)
|
||||||
|
|
||||||
|
return package
|
||||||
|
|
||||||
|
def to_package(deck_json):
|
||||||
|
deck_title = deck_json["DeckTitle"]
|
||||||
|
deck = genanki.Deck(1607372319, deck_title)
|
||||||
|
|
||||||
|
for card_json in deck_json["Cards"]:
|
||||||
|
title = card_json["Title"]
|
||||||
|
question = card_json["Question"]
|
||||||
|
answer = card_json["Answer"]
|
||||||
|
|
||||||
|
note = genanki.Note(
|
||||||
|
model=MY_MODEL,
|
||||||
|
fields=[title, question, answer]
|
||||||
|
)
|
||||||
|
|
||||||
|
deck.add_note(note)
|
||||||
|
|
||||||
|
return genanki.Package(deck)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) != 3:
|
||||||
|
print("Usage: python convert.py <input_json> <output_apkg>")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
input_json = sys.argv[1]
|
||||||
|
output_apkg = sys.argv[2]
|
||||||
|
json_file_to_package(input_json).write_to_file(output_apkg)
|
||||||
|
logging.info(f"Deck created at: {output_apkg}")
|
|
@ -1,12 +1,8 @@
|
||||||
import openai
|
import openai
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
import json
|
import json
|
||||||
import genanki
|
from constants import API_KEY_ENV, CHAT_MODEL, OUTPUT_FILENAME
|
||||||
from logging_config import setup_logging
|
|
||||||
from constants import API_KEY_ENV, CHAT_MODEL
|
|
||||||
|
|
||||||
|
|
||||||
setup_logging()
|
|
||||||
|
|
||||||
|
|
||||||
API_KEY = os.environ.get(API_KEY_ENV)
|
API_KEY = os.environ.get(API_KEY_ENV)
|
||||||
|
@ -87,44 +83,21 @@ def response_to_json(response_text):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# Create a new model for our cards. This is necessary for genanki.
|
if __name__ == "__main__":
|
||||||
MY_MODEL = genanki.Model(
|
if len(sys.argv) != 2:
|
||||||
1607372319,
|
print("Usage: python prompt4cards.py <text_file_path>")
|
||||||
"Simple Model",
|
sys.exit(1)
|
||||||
fields=[
|
|
||||||
{"name": "Title"},
|
text_file_path = sys.argv[1]
|
||||||
{"name": "Question"},
|
|
||||||
{"name": "Answer"},
|
|
||||||
],
|
|
||||||
templates=[
|
|
||||||
{
|
|
||||||
"name": "{{Title}}",
|
|
||||||
"qfmt": "{{Question}}",
|
|
||||||
"afmt": "{{FrontSide}}<hr id='answer'>{{Answer}}",
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|
||||||
def json_file_to_package(json_path):
|
# Read the text content
|
||||||
with open(json_path, 'r', encoding='utf-8') as f:
|
with open(text_file_path, 'r') as file:
|
||||||
json_data = json.load(f)
|
text_content = file.read()
|
||||||
package = to_package(json_data)
|
|
||||||
|
|
||||||
return package
|
response_text = prompt_for_card_content(text_content)
|
||||||
|
deck_json = response_to_json(response_text)
|
||||||
|
|
||||||
def to_package(deck_json):
|
with open(OUTPUT_FILENAME, 'w') as json_file:
|
||||||
deck_title = deck_json["DeckTitle"]
|
json.dump(deck_json, json_file)
|
||||||
deck = genanki.Deck(1607372319, deck_title)
|
|
||||||
|
|
||||||
for card_json in deck_json["Cards"]:
|
print(f"Saved generated deck to {OUTPUT_FILENAME}")
|
||||||
title = card_json["Title"]
|
|
||||||
question = card_json["Question"]
|
|
||||||
answer = card_json["Answer"]
|
|
||||||
|
|
||||||
note = genanki.Note(
|
|
||||||
model=MY_MODEL,
|
|
||||||
fields=[title, question, answer]
|
|
||||||
)
|
|
||||||
|
|
||||||
deck.add_note(note)
|
|
||||||
|
|
||||||
return genanki.Package(deck)
|
|
10
server.py
10
server.py
|
@ -3,16 +3,17 @@ import tempfile
|
||||||
import shutil
|
import shutil
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from logging_config import setup_logging
|
||||||
from flask import Flask, request, send_from_directory, jsonify
|
from flask import Flask, request, send_from_directory, jsonify
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
from ankiai import images_to_package
|
from ankiai import images_to_package
|
||||||
from constants import IMAGE_KEY, APKG_FILE, NO_IMAGE_PART_ERROR, NO_SELECTED_FILE_ERROR, INVALID_FILENAME_ERROR
|
from constants import IMAGE_KEY, OUTPUT_FILE, NO_IMAGE_PART_ERROR, NO_SELECTED_FILE_ERROR, INVALID_FILENAME_ERROR
|
||||||
|
|
||||||
|
|
||||||
from logging_config import setup_logging
|
|
||||||
setup_logging()
|
setup_logging()
|
||||||
|
|
||||||
|
|
||||||
|
from logging_config import setup_logging
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
def save_uploaded_images(images, directory):
|
def save_uploaded_images(images, directory):
|
||||||
|
@ -40,9 +41,8 @@ def deck_from_images():
|
||||||
save_uploaded_images(images, temp_dir)
|
save_uploaded_images(images, temp_dir)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
images_to_package(temp_dir).write_to_file(APKG_FILE)
|
images_to_package(temp_dir, OUTPUT_FILE)
|
||||||
logging.info(f"Anki package written to {APKG_FILE}")
|
return send_from_directory('.', OUTPUT_FILE, as_attachment=True)
|
||||||
return send_from_directory('.', APKG_FILE, as_attachment=True)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error("Exception occurred: "+str(e), exc_info=True)
|
logging.error("Exception occurred: "+str(e), exc_info=True)
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
9
utilities.py
Normal file
9
utilities.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import os
|
||||||
|
from constants import IMAGE_EXTENSIONS
|
||||||
|
|
||||||
|
def is_image_file(path):
|
||||||
|
return any(path.lower().endswith(ext) for ext in IMAGE_EXTENSIONS)
|
||||||
|
|
||||||
|
def ensure_directory_exists(directory):
|
||||||
|
if not os.path.exists(directory):
|
||||||
|
os.mkdir(directory)
|
Loading…
Reference in New Issue
Block a user