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,
},
},
}