Hi @dmnxprss,
First of all thanks a lot for sharing an extensive feedback on your experience with the new prodigy-pdf
recipes - this would definitely help us to make the solution more flexible.
Before getting on to your custom recipe, I just wanted to mention that the easiest solution to use pdf.layout.fetch
output in textcat.manual
would be to remove the image
key from the input stream.
I realize that it might be a bit confusing, in the end textcat
manual should only be looking for the text
as input key but since it uses the choice
UI (which is used for classifying images as well) it also checks for the image
field first. We are actually considering improving the configuration to make the choice of the input key more explicit.
With respect to your custom recipe, the actual problem is that the choice
UI is not being rendered at all. The labels you see come from the spans_manual
UI that comes from blocks
defined in the input stream (i.e. the output of the pdf.layout.fetch
recipe). The fact that these incoming blocks
are defined on the task level means that they take priority over the blocks
defined globally in your recipe. In other words, the blocks you define in the recipe:
blocks = [
{"view_id": "choice", "text": None},
{"view_id": "image"},
]
are being ignored because each task comes with blocks
defined on the task level and these are:
[
{'view_id': 'spans_manual'},
{'view_id': 'image', 'spans': [{...}]}
]
So in order to render blocks with span annotated image, text (from the bounding box) and the choice, you'd need to modify the blocks
on the task level or remove the blocks
that come with the input stream and define yours globally. You probably also want to get rid of spans_manual
UI altogether.
If you go with the first option (modify blocks on the task level) you could modify your option adding function like so:
def add_label_options_to_stream(stream, labels):
options = [{"id": label, "text": label} for label in labels]
for task in stream:
task["options"] = options
task["config"]["choice_style"] = "multiple"
# Filter out spans_manual blocks and add new view blocks
blocks = [block for block in task["config"]["blocks"]
if block["view_id"] != "spans_manual"]
blocks.extend([
{"view_id": "text"},
{"view_id": "choice", "text": None, "image": None}
])
task["config"]["blocks"] = blocks
yield task
You should now be rendering only image
, text
and choice
UI.
Please note that you'll need to adjust the CSS of your custom recipe to render all the elements in the right columns.
Here's how the recipe could be updated but you might want want to tweak the CSS to your preference:
# Selectors for each component
CSS_IMAGE = "div.prodigy-content:nth-child(2)"
CSS_TEXT = "div.prodigy-content:nth-child(3)"
CSS_CHOICE = "._Choice-root-0-1-196"
# Container setup
CSS_CONTAINER = """
.prodigy-container {
display: grid;
grid-template-columns: 1fr 50%;
height: 100vh;
overflow-y: auto;
overflow-x: hidden;
}
"""
# Layout CSS
CSS_PREVIEW = f"""
{CSS_CONTAINER}
/* Left column content - setting up the flex container */
{CSS_IMAGE} {{
grid-column: 1;
grid-row: 1;
border-right: 1px solid #ddd;
height: 100%;
}}
{CSS_TEXT} {{
grid-column: 1;
grid-row: 2;
border-right: 1px solid #ddd;
height: 100%;
top: 0;
}}
/* Right column for choices */
{CSS_CHOICE} {{
grid-column: 2;
grid-row: 1;
}}
"""
FONT_SIZE_TEXT = 14
@prodigy.recipe(
"textcat.contract.manual",
out_dataset=("Dataset to save annotations into", "positional", None, str),
in_dataset=("Dataset loader annotations from", "positional", None, str),
)
def custom_recipe(out_dataset: str, in_dataset: str):
# Log recipe details
log("RECIPE: Starting recipe textcat.contract.manual", locals())
# Connect to Prodigy database
db = connect()
# Define labels for text categorization
textcat_labels = ["ACOPERATION", "DEFINITION","DEFAULT","GOVLAW","INSURANCE",
"LEASETERM", "MAINTENANCE","MODS","MISC","PAYMENTS","REDELIVERY",
"SCHEDULES","TERMINATION","WARRANTY"]
# Helper functions for adding user provided labels to annotation tasks.
def add_label_options_to_stream(stream, labels):
options = [{"id": label, "text": label} for label in labels]
for task in stream:
task["options"] = options
task["config"]["choice_style"] = "multiple"
# Filter out spans_manual blocks and add new view blocks
blocks = [block for block in task["config"]["blocks"]
if block["view_id"] != "spans_manual"]
blocks.extend([
{"view_id": "text"},
{"view_id": "choice", "text": None, "image": None}
])
task["config"]["blocks"] = blocks
yield task
# Function to call when annotations are returned
def update(examples):
print(f"Received {len(examples)} annotations!")
stream =db.get_dataset_examples(in_dataset)
stream = add_label_options_to_stream(stream, textcat_labels)
css = CSS_PREVIEW
return {
"view_id": "blocks", # Annotation interface to use
"dataset": out_dataset, # Name of dataset to save annotations
"stream": stream, # Incoming stream of examples
"update": update, # Function to call when annotations are returned
"config": { # Additional config settings, mostly for app UI
"global_css": css,
"shade_bounding_boxes": True,
"custom_theme": {
"cardMaxWidth": "95%",
"smallText": FONT_SIZE_TEXT,
"tokenHeight": 25,
},
},
}