Commit
·
0c7edbd
1
Parent(s):
d008f82
feat: :art: Add guidelines + Product page
Browse files- app.py +41 -25
- back_end.py +33 -15
app.py
CHANGED
@@ -4,18 +4,23 @@ from back_end import next_annotation, submit_correction, enable_buttons
|
|
4 |
from utils import diff_texts, LANGS
|
5 |
|
6 |
|
7 |
-
|
8 |
with gr.Blocks() as demo:
|
9 |
gr.Markdown(
|
10 |
"""# Ingredients Spellcheck 🍊
|
11 |
-
### Review the Spellcheck corrections
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
|
|
|
|
|
|
|
|
|
|
16 |
"""
|
17 |
)
|
18 |
-
|
19 |
with gr.Row():
|
20 |
with gr.Column():
|
21 |
insight_id = gr.Textbox(
|
@@ -28,15 +33,14 @@ with gr.Blocks() as demo:
|
|
28 |
label="OFF Username",
|
29 |
)
|
30 |
off_password = gr.Textbox(
|
31 |
-
|
32 |
-
|
33 |
)
|
34 |
lang = gr.Dropdown(
|
35 |
choices=LANGS.keys(),
|
36 |
value="All",
|
37 |
-
label="Language",
|
38 |
info="Optional: Filter a language.",
|
39 |
-
|
40 |
)
|
41 |
|
42 |
# Saved to detect change from annotator
|
@@ -46,16 +50,16 @@ with gr.Blocks() as demo:
|
|
46 |
label="Original Text (Uneditable)",
|
47 |
info="This is the original text.",
|
48 |
interactive=False, # Make this text box uneditable
|
49 |
-
lines=3
|
50 |
)
|
51 |
-
|
52 |
annotator_correction = gr.Textbox(
|
53 |
label="Corrected Text (Editable)",
|
54 |
info="This is the AI-corrected text. You can modify it.",
|
55 |
interactive=True, # Make this text box editable
|
56 |
lines=3,
|
57 |
)
|
58 |
-
|
59 |
# Diff Display using HighlightedText
|
60 |
diff_display = gr.HighlightedText(
|
61 |
label="Difference Between Original and Corrected Text",
|
@@ -64,25 +68,36 @@ with gr.Blocks() as demo:
|
|
64 |
color_map={"+": "green", "-": "red"},
|
65 |
)
|
66 |
|
67 |
-
|
68 |
-
|
|
|
|
|
69 |
# Validate button to move to next annotation
|
70 |
with gr.Row():
|
71 |
validate_button = gr.Button("Validate", interactive=False)
|
72 |
skip_button = gr.Button("Skip", interactive=False)
|
73 |
-
|
|
|
|
|
74 |
validate_button.click(
|
75 |
submit_correction,
|
76 |
-
inputs=[
|
77 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
78 |
)
|
79 |
|
80 |
skip_button.click(
|
81 |
next_annotation,
|
82 |
inputs=[lang],
|
83 |
-
outputs=[insight_id, original, model_correction, annotator_correction, image],
|
84 |
)
|
85 |
-
|
86 |
annotator_correction.change(
|
87 |
diff_texts, # Call diff function
|
88 |
inputs=[original, annotator_correction],
|
@@ -90,11 +105,11 @@ with gr.Blocks() as demo:
|
|
90 |
)
|
91 |
|
92 |
off_username.change(
|
93 |
-
enable_buttons,
|
94 |
-
inputs=[off_username, off_password],
|
95 |
outputs=[validate_button, skip_button],
|
96 |
)
|
97 |
-
|
98 |
off_password.change(
|
99 |
enable_buttons,
|
100 |
inputs=[off_username, off_password],
|
@@ -104,7 +119,7 @@ with gr.Blocks() as demo:
|
|
104 |
lang.change(
|
105 |
next_annotation,
|
106 |
inputs=[lang],
|
107 |
-
outputs=[insight_id, original, model_correction, annotator_correction, image],
|
108 |
)
|
109 |
|
110 |
# Load the first set of texts when the demo starts
|
@@ -117,6 +132,7 @@ with gr.Blocks() as demo:
|
|
117 |
model_correction,
|
118 |
annotator_correction,
|
119 |
image,
|
|
|
120 |
],
|
121 |
)
|
122 |
|
|
|
4 |
from utils import diff_texts, LANGS
|
5 |
|
6 |
|
7 |
+
## COMPONENTS
|
8 |
with gr.Blocks() as demo:
|
9 |
gr.Markdown(
|
10 |
"""# Ingredients Spellcheck 🍊
|
11 |
+
### Review the Spellcheck corrections. Your precious feedback will be integrated to the Open Food Facts database!
|
12 |
+
|
13 |
+
### Instructions:
|
14 |
+
* *You are provided the original list of ingredients text as stored in the Open Food Facts (OFF) database, the Spellcheck prediction, and optionally a picture of the product.*
|
15 |
+
* *Your task, if you accept 💣, is to review the Spellcheck prediction by either validating or correcting it.*
|
16 |
+
* *The picture is only here to help you during the annotation as a reference. It can happen that the language of the text and the picture are different. **Keep calm and focus on the text.***
|
17 |
+
* *It can happen that the Producer has made a mistake on the product packaging. Since we parse the list of ingredients to extract its information, it would be preferable if you fix the typo.*
|
18 |
+
|
19 |
+
### Important:
|
20 |
+
* Authenticate yourself using your Open Food Facts username and password to add modifications to a product. If you're not registered yet, you can do so [here](https://world.openfoodfacts.org/cgi/user.pl)!
|
21 |
"""
|
22 |
)
|
23 |
+
|
24 |
with gr.Row():
|
25 |
with gr.Column():
|
26 |
insight_id = gr.Textbox(
|
|
|
33 |
label="OFF Username",
|
34 |
)
|
35 |
off_password = gr.Textbox(
|
36 |
+
label="OFF Password",
|
37 |
+
type="password",
|
38 |
)
|
39 |
lang = gr.Dropdown(
|
40 |
choices=LANGS.keys(),
|
41 |
value="All",
|
42 |
+
label="Language",
|
43 |
info="Optional: Filter a language.",
|
|
|
44 |
)
|
45 |
|
46 |
# Saved to detect change from annotator
|
|
|
50 |
label="Original Text (Uneditable)",
|
51 |
info="This is the original text.",
|
52 |
interactive=False, # Make this text box uneditable
|
53 |
+
lines=3,
|
54 |
)
|
55 |
+
|
56 |
annotator_correction = gr.Textbox(
|
57 |
label="Corrected Text (Editable)",
|
58 |
info="This is the AI-corrected text. You can modify it.",
|
59 |
interactive=True, # Make this text box editable
|
60 |
lines=3,
|
61 |
)
|
62 |
+
|
63 |
# Diff Display using HighlightedText
|
64 |
diff_display = gr.HighlightedText(
|
65 |
label="Difference Between Original and Corrected Text",
|
|
|
68 |
color_map={"+": "green", "-": "red"},
|
69 |
)
|
70 |
|
71 |
+
with gr.Column():
|
72 |
+
url = gr.Textbox(label="Product page", show_copy_button=True)
|
73 |
+
image = gr.Image()
|
74 |
+
|
75 |
# Validate button to move to next annotation
|
76 |
with gr.Row():
|
77 |
validate_button = gr.Button("Validate", interactive=False)
|
78 |
skip_button = gr.Button("Skip", interactive=False)
|
79 |
+
|
80 |
+
|
81 |
+
## COMPONENT INTERACTIONS
|
82 |
validate_button.click(
|
83 |
submit_correction,
|
84 |
+
inputs=[
|
85 |
+
insight_id,
|
86 |
+
annotator_correction,
|
87 |
+
model_correction,
|
88 |
+
off_username,
|
89 |
+
off_password,
|
90 |
+
lang,
|
91 |
+
],
|
92 |
+
outputs=[insight_id, original, model_correction, annotator_correction, image, url],
|
93 |
)
|
94 |
|
95 |
skip_button.click(
|
96 |
next_annotation,
|
97 |
inputs=[lang],
|
98 |
+
outputs=[insight_id, original, model_correction, annotator_correction, image, url],
|
99 |
)
|
100 |
+
|
101 |
annotator_correction.change(
|
102 |
diff_texts, # Call diff function
|
103 |
inputs=[original, annotator_correction],
|
|
|
105 |
)
|
106 |
|
107 |
off_username.change(
|
108 |
+
enable_buttons,
|
109 |
+
inputs=[off_username, off_password],
|
110 |
outputs=[validate_button, skip_button],
|
111 |
)
|
112 |
+
|
113 |
off_password.change(
|
114 |
enable_buttons,
|
115 |
inputs=[off_username, off_password],
|
|
|
119 |
lang.change(
|
120 |
next_annotation,
|
121 |
inputs=[lang],
|
122 |
+
outputs=[insight_id, original, model_correction, annotator_correction, image, url],
|
123 |
)
|
124 |
|
125 |
# Load the first set of texts when the demo starts
|
|
|
132 |
model_correction,
|
133 |
annotator_correction,
|
134 |
image,
|
135 |
+
url,
|
136 |
],
|
137 |
)
|
138 |
|
back_end.py
CHANGED
@@ -12,7 +12,7 @@ import gradio as gr
|
|
12 |
|
13 |
logger = get_logger()
|
14 |
|
15 |
-
BASE_URL = "https://robotoff.openfoodfacts.org/api/v1"
|
16 |
# BASE_URL = "http://localhost:5500/api/v1" # Dev
|
17 |
UPDATE = 1
|
18 |
|
@@ -38,9 +38,10 @@ def next_annotation(lang: Optional[str]) -> Annotation:
|
|
38 |
return (
|
39 |
insight["id"],
|
40 |
insight["data"]["original"],
|
41 |
-
insight["data"]["correction"],
|
42 |
insight["data"]["correction"],
|
43 |
-
get_image_url(insight["barcode"])
|
|
|
44 |
)
|
45 |
|
46 |
|
@@ -50,10 +51,12 @@ def submit_correction(
|
|
50 |
model_correction: str,
|
51 |
username: str,
|
52 |
password: str,
|
53 |
-
update: bool = UPDATE
|
54 |
):
|
55 |
auth = Authentification(username, password)
|
56 |
-
correction =
|
|
|
|
|
57 |
try:
|
58 |
submit_to_product_opener(
|
59 |
insight_id=insight_id,
|
@@ -62,14 +65,15 @@ def submit_correction(
|
|
62 |
auth=auth,
|
63 |
)
|
64 |
except gr.Error as e:
|
65 |
-
gr.Warning(e.message)
|
66 |
return (
|
67 |
gr.update(),
|
68 |
gr.update(),
|
69 |
gr.update(),
|
70 |
gr.update(),
|
71 |
gr.update(),
|
72 |
-
|
|
|
73 |
gr.Info("Product successfuly updated. Many thanks!")
|
74 |
return next_annotation()
|
75 |
|
@@ -77,9 +81,11 @@ def submit_correction(
|
|
77 |
def import_random_insight(
|
78 |
insight_type: str = "ingredient_spellcheck",
|
79 |
predictor: str = "fine-tuned-mistral-7b",
|
80 |
-
lang_key: Optional[str] = None
|
81 |
) -> Dict:
|
82 |
-
url =
|
|
|
|
|
83 |
if lang_key:
|
84 |
url += f"&value_tag={lang_key}"
|
85 |
try:
|
@@ -93,7 +99,7 @@ def import_random_insight(
|
|
93 |
return import_random_insight(insight_type, predictor)
|
94 |
except requests.RequestException as e:
|
95 |
gr.Error(f"Import product from Product Opener failed: {e}")
|
96 |
-
|
97 |
|
98 |
def submit_to_product_opener(
|
99 |
insight_id: str,
|
@@ -104,10 +110,13 @@ def submit_to_product_opener(
|
|
104 |
url = f"{BASE_URL}/insights/annotate"
|
105 |
headers = {
|
106 |
"Authorization": f"Basic {auth.get_credentials()}",
|
107 |
-
|
108 |
}
|
109 |
if annotator_correction:
|
110 |
-
logger.info(
|
|
|
|
|
|
|
111 |
payload = {
|
112 |
"insight_id": insight_id,
|
113 |
"annotation": 2,
|
@@ -115,7 +124,9 @@ def submit_to_product_opener(
|
|
115 |
"data": json.dumps({"annotation": annotator_correction}),
|
116 |
}
|
117 |
else:
|
118 |
-
logger.info(
|
|
|
|
|
119 |
payload = {
|
120 |
"insight_id": insight_id,
|
121 |
"annotation": 1,
|
@@ -127,8 +138,10 @@ def submit_to_product_opener(
|
|
127 |
except requests.RequestException as e:
|
128 |
logger.error(e)
|
129 |
logger.error(response.content)
|
130 |
-
raise gr.Error(
|
131 |
-
|
|
|
|
|
132 |
|
133 |
def get_image_url(
|
134 |
code: str,
|
@@ -147,3 +160,8 @@ def enable_buttons(username, password):
|
|
147 |
# Return the updated button states: interactive if both username and password are filled
|
148 |
state = bool(username) and bool(password)
|
149 |
return gr.update(interactive=state), gr.update(interactive=state)
|
|
|
|
|
|
|
|
|
|
|
|
12 |
|
13 |
logger = get_logger()
|
14 |
|
15 |
+
BASE_URL = "https://robotoff.openfoodfacts.org/api/v1" # Prod
|
16 |
# BASE_URL = "http://localhost:5500/api/v1" # Dev
|
17 |
UPDATE = 1
|
18 |
|
|
|
38 |
return (
|
39 |
insight["id"],
|
40 |
insight["data"]["original"],
|
41 |
+
insight["data"]["correction"], # Saved for comparison with annotator changes
|
42 |
insight["data"]["correction"],
|
43 |
+
get_image_url(insight["barcode"]),
|
44 |
+
get_product_url(insight["barcode"]),
|
45 |
)
|
46 |
|
47 |
|
|
|
51 |
model_correction: str,
|
52 |
username: str,
|
53 |
password: str,
|
54 |
+
update: bool = UPDATE,
|
55 |
):
|
56 |
auth = Authentification(username, password)
|
57 |
+
correction = (
|
58 |
+
annotator_correction if annotator_correction != model_correction else None
|
59 |
+
)
|
60 |
try:
|
61 |
submit_to_product_opener(
|
62 |
insight_id=insight_id,
|
|
|
65 |
auth=auth,
|
66 |
)
|
67 |
except gr.Error as e:
|
68 |
+
gr.Warning(e.message) # We use gr.Warning instead of gr.Error to keep the flow
|
69 |
return (
|
70 |
gr.update(),
|
71 |
gr.update(),
|
72 |
gr.update(),
|
73 |
gr.update(),
|
74 |
gr.update(),
|
75 |
+
gr.update(),
|
76 |
+
) # Stay unchanged
|
77 |
gr.Info("Product successfuly updated. Many thanks!")
|
78 |
return next_annotation()
|
79 |
|
|
|
81 |
def import_random_insight(
|
82 |
insight_type: str = "ingredient_spellcheck",
|
83 |
predictor: str = "fine-tuned-mistral-7b",
|
84 |
+
lang_key: Optional[str] = None,
|
85 |
) -> Dict:
|
86 |
+
url = (
|
87 |
+
f"{BASE_URL}/insights/random?count=1&type={insight_type}&predictor={predictor}"
|
88 |
+
)
|
89 |
if lang_key:
|
90 |
url += f"&value_tag={lang_key}"
|
91 |
try:
|
|
|
99 |
return import_random_insight(insight_type, predictor)
|
100 |
except requests.RequestException as e:
|
101 |
gr.Error(f"Import product from Product Opener failed: {e}")
|
102 |
+
|
103 |
|
104 |
def submit_to_product_opener(
|
105 |
insight_id: str,
|
|
|
110 |
url = f"{BASE_URL}/insights/annotate"
|
111 |
headers = {
|
112 |
"Authorization": f"Basic {auth.get_credentials()}",
|
113 |
+
"Content-Type": "application/x-www-form-urlencoded",
|
114 |
}
|
115 |
if annotator_correction:
|
116 |
+
logger.info(
|
117 |
+
"Change from annotator. New insight sent to Product Opener. New correction: %s",
|
118 |
+
annotator_correction,
|
119 |
+
)
|
120 |
payload = {
|
121 |
"insight_id": insight_id,
|
122 |
"annotation": 2,
|
|
|
124 |
"data": json.dumps({"annotation": annotator_correction}),
|
125 |
}
|
126 |
else:
|
127 |
+
logger.info(
|
128 |
+
"No change from annotator. Original insight sent to Product Opener."
|
129 |
+
)
|
130 |
payload = {
|
131 |
"insight_id": insight_id,
|
132 |
"annotation": 1,
|
|
|
138 |
except requests.RequestException as e:
|
139 |
logger.error(e)
|
140 |
logger.error(response.content)
|
141 |
+
raise gr.Error(
|
142 |
+
"Failed to submit to Product Opener. Are your username and password correct?"
|
143 |
+
)
|
144 |
+
|
145 |
|
146 |
def get_image_url(
|
147 |
code: str,
|
|
|
160 |
# Return the updated button states: interactive if both username and password are filled
|
161 |
state = bool(username) and bool(password)
|
162 |
return gr.update(interactive=state), gr.update(interactive=state)
|
163 |
+
|
164 |
+
|
165 |
+
def get_product_url(barcode: int) -> str:
|
166 |
+
return f"https://world.openfoodfacts.org/product/{barcode}"
|
167 |
+
|