Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

First pass #3

Merged
merged 2 commits into from
Dec 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 11 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,25 +76,24 @@ Before you proceed with the installation of the subnet, note the following:

---

## Writing your own incentive mechanism

As described in [Quickstarter template](#quickstarter-template) section above, when you are ready to write your own incentive mechanism, update this template repository by editing the following files. The code in these files contains detailed documentation on how to update the template. Read the documentation in each of the files to understand how to update the template. There are multiple **TODO**s in each of the files identifying sections you should update. These files are:
- `template/protocol.py`: Contains the definition of the wire-protocol used by miners and validators.
- `neurons/miner.py`: Script that defines the miner's behavior, i.e., how the miner responds to requests from validators.
- `neurons/validator.py`: This script defines the validator's behavior, i.e., how the validator requests information from the miners and determines the scores.
- `template/forward.py`: Contains the definition of the validator's forward pass.
- `template/reward.py`: Contains the definition of how validators reward miner responses.
## What we changed to make our repo

In addition to the above files, you should also update the following files:
- Rename `/template` to `/ocr_subnet`
- `ocr_subnet/protocol.py`: Rename the synapse to `OCRSynapse` and provide the necessary attributes to communication between miner and validator
- `ocr_subnet/forward.py`: Included the synthetic data generation (invoice pdf), used `OCRSynapse`.
- `ocr_subnet/reward.py`: Added custom loss function to calculate the reward
- `neurons/miner.py`: Use `pytesseract` for OCR, and use `OCRSynapse` to communicate with validator

### Remaining changes to be done
In addition to the above files, we would also update the following files:
- `README.md`: This file contains the documentation for your project. Update this file to reflect your project's documentation.
- `CONTRIBUTING.md`: This file contains the instructions for contributing to your project. Update this file to reflect your project's contribution guidelines.
- `template/__init__.py`: This file contains the version of your project.
- `setup.py`: This file contains the metadata about your project. Update this file to reflect your project's metadata.
- `docs/`: This directory contains the documentation for your project. Update this directory to reflect your project's documentation.

__Note__
The `template` directory should also be renamed to your project name.
---



## License
This repository is licensed under the MIT License.
Expand Down
6 changes: 3 additions & 3 deletions neurons/miner.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
# Bittensor Miner Template:
import ocr_subnet

from ocr_subnet.utils.serialize import deserialize_image

# import base miner class which takes care of most of the boilerplate
from ocr_subnet.base.miner import BaseMinerNeuron

Expand Down Expand Up @@ -57,13 +59,11 @@ async def forward(

"""

image = synapse.image
image = deserialize_image(base64_string=synapse.base64_image)
# Use pytesseract to get the data
data = pytesseract.image_to_data(image, output_type=pytesseract.Output.DICT)

# Initialize the response list
response = []

# Loop over each item in the 'text' part of the data
for i in range(len(data['text'])):
if data['text'][i].strip() != '': # This filters out empty text results
Expand Down
2 changes: 0 additions & 2 deletions ocr_subnet/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.

# TODO(developer): Change this value when updating your code base.
# Define the version of the template module.
__version__ = "0.0.0"
version_split = __version__.split(".")
__spec_version__ = (
Expand Down
4 changes: 2 additions & 2 deletions ocr_subnet/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ class OCRSynapse(bt.Synapse):
- response: List[dict] containing data extracted from the image.
"""

# Required request input, filled by sending dendrite caller.
image: int
# Required request input, filled by sending dendrite caller. It is a base64 encoded string.
base64_image: str

# Optional request output, filled by recieving axon.
response: typing.Optional[typing.List[dict]] = None
Expand Down
19 changes: 19 additions & 0 deletions ocr_subnet/utils/serialize.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import base64
from io import BytesIO
from PIL import Image


def serialize_image(image, format="JPEG"):
"""Converts PIL image to base64 string.
"""

buffer = BytesIO()
image.save(buffer, format=format)
return buffer.getvalue()


def deserialize_image(base64_string):
"""Converts base64 string to PIL image.
"""

return Image.open(BytesIO(base64.b64decode(base64_string)))
23 changes: 14 additions & 9 deletions ocr_subnet/validator/forward.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from ocr_subnet.protocol import OCRSynapse
from ocr_subnet.validator.reward import get_rewards
from ocr_subnet.utils.uids import get_random_uids
from ocr_subnet.utils.serialize import serialize_image
from ocr_subnet.validator.generate import create_invoice
from ocr_subnet.validator.corrupt import corrupt_image

Expand Down Expand Up @@ -70,7 +71,10 @@ async def forward(self):
"""
The forward function is called by the validator every time step.

It is responsible for querying the network and scoring the responses.
It consists of 3 important steps:
- Generate a challenge for the miners (in this case it creates a synthetic invoice image)
- Query the miners with the challenge
- Score the responses from the miners

Args:
self (:obj:`bittensor.neuron.Neuron`): The neuron object which contains all the necessary state for the validator.
Expand All @@ -81,30 +85,31 @@ async def forward(self):
miner_uids = get_random_uids(self, k=self.config.neuron.sample_size)

# Create a random image and load it.
image_path = generate_image()
image_data, image_path = generate_image(corrupt=True)
image = load_image(image_path)

# Create synapse object to send to the miner and attach the image.
# TODO: it's probably not possible to send the image directly, so you'll need to encode it somehow.
synapse = OCRSynapse(image = image)
# convert PIL image into a json serializable format
synapse = OCRSynapse(base64_image = serialize_image(image))

# The dendrite client queries the network.
responses = self.dendrite.query(
# Send the query to selected miner axons in the network.
axons=[self.metagraph.axons[uid] for uid in miner_uids],
# Pass the synapse to the miner.
synapse=synapse,
# All responses have the deserialize function called on them before returning.
# You are encouraged to define your own deserialization function.
deserialize=True,
# Do not deserialize the response so that we have access to the raw response.
deserialize=False,
)

# Log the results for monitoring purposes.
bt.logging.info(f"Received responses: {responses}")

# TODO: We need ground truth labels to score the responses!
rewards = get_rewards(self, query=self.step, responses=responses)
rewards = get_rewards(self, image_data=image_data, responses=responses)

bt.logging.info(f"Scored responses: {rewards}")
# Update the scores based on the rewards. You may want to define your own update_scores function for custom behavior.
self.update_scores(rewards, miner_uids)

# TODO: return an event which can be logged by the validator.
# return event#
61 changes: 42 additions & 19 deletions ocr_subnet/validator/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,46 +5,68 @@
from reportlab.lib.pagesizes import letter
from reportlab.pdfgen import canvas



def apply_invoice_template(invoice_data, filename):
c = canvas.Canvas(filename, pagesize=letter)
w, h = c._pagesize
print(w,h)
c.setLineWidth(.3)
c.setFont('Helvetica', 12)
font = {'family': 'Helvetica', 'size': 12}
units = font.get('size')
c.setFont(font.get('family'), units)

data = []
def write_text(x, y, text):
c.drawString(x, y, text)
# scale x and y by the page size and estimate bounding box based on font size
# position = [x0, y0, x1, y1]
position = [
x/w,
1 - (y - 0.2*units)/h,
(x + (2 + len(text)) * 0.5*units)/w,
1 - (y + 1.2*units)/h
]

data.append({'position': position, 'text': text, 'font': font})

# Draw the invoice header
c.drawString(30, 750, invoice_data['company_name'])
c.drawString(30, 735, invoice_data['company_address'])
c.drawString(30, 720, invoice_data['company_city_zip'])
c.drawString(400, 750, "Invoice Date: " + invoice_data['invoice_date'])
c.drawString(400, 735, "Invoice #: " + invoice_data['invoice_number'])
write_text(30, 750, invoice_data['company_name'])
write_text(30, 735, invoice_data['company_address'])
write_text(30, 720, invoice_data['company_city_zip'])
write_text(400, 750, "Invoice Date: " + invoice_data['invoice_date'])
write_text(400, 735, "Invoice #: " + invoice_data['invoice_number'])

# Draw the bill to section
c.drawString(30, 690, "Bill To:")
c.drawString(120, 690, invoice_data['customer_name'])
write_text(30, 690, "Bill To:")
write_text(120, 690, invoice_data['customer_name'])

# Table headers
c.drawString(30, 650, "Description")
c.drawString(300, 650, "Qty")
c.drawString(460, 650, "Cost")
write_text(30, 650, "Description")
write_text(300, 650, "Qty")
write_text(460, 650, "Cost")
c.line(30, 645, 560, 645)

# List items
line_height = 625
total = 0
for item in invoice_data['items']:
c.drawString(30, line_height, item['desc'])
c.drawString(300, line_height, str(item['qty']))
c.drawString(460, line_height, "${:.2f}".format(item['cost']))
write_text(30, line_height, item['desc'])
write_text(300, line_height, str(item['qty']))
write_text(460, line_height, "${:.2f}".format(item['cost']))
total += item['qty'] * item['cost']
line_height -= 15

# Draw the total cost
c.drawString(400, line_height - 15, f"Total: ${total:,.2f}" )
write_text(400, line_height - 15, f"Total: ${total:,.2f}" )

# Terms and Conditions
c.drawString(30, line_height - 45, "Terms:")
c.drawString(120, line_height - 45, invoice_data['terms'])
write_text(30, line_height - 45, "Terms:")
write_text(120, line_height - 45, invoice_data['terms'])

c.save()
return data


def create_invoice(root_dir):

Expand Down Expand Up @@ -91,7 +113,8 @@ def random_items(n):
# make a random hash for the filename
filename = f"{fake.sha256()}.pdf"
path = os.path.join(root_dir, filename)

# Use the function and pass the data and the filename you want to save as
apply_invoice_template(invoice_info, path)
data = apply_invoice_template(invoice_info, path)

return path
return data, path
Loading