Commit
·
e01620b
1
Parent(s):
7d9c0d3
feat: :zap: App connected to PO
Browse files- app.py +54 -22
- back_end.py +93 -21
- utils.py +11 -2
app.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1 |
import gradio as gr
|
2 |
|
3 |
-
from back_end import next_annotation, submit_correction
|
4 |
from utils import diff_texts
|
5 |
|
6 |
|
@@ -11,30 +11,42 @@ with gr.Blocks() as demo:
|
|
11 |
### You can review corrections generated by our model and validate or correct the predictions.\n
|
12 |
Your feedback will be integrated to the OFF database!
|
13 |
|
14 |
-
|
15 |
-
|
16 |
-
|
|
|
17 |
|
18 |
with gr.Row():
|
19 |
with gr.Column():
|
20 |
insight_id = gr.Textbox(
|
21 |
label="Insight Id",
|
22 |
-
interactive=False,
|
23 |
visible=False,
|
24 |
)
|
25 |
|
26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
27 |
label="Original Text (Uneditable)",
|
28 |
info="This is the original text.",
|
29 |
interactive=False, # Make this text box uneditable
|
30 |
lines=3
|
31 |
)
|
32 |
|
33 |
-
|
34 |
label="Corrected Text (Editable)",
|
35 |
info="This is the AI-corrected text. You can modify it.",
|
36 |
interactive=True, # Make this text box editable
|
37 |
-
lines=3
|
38 |
)
|
39 |
|
40 |
# Diff Display using HighlightedText
|
@@ -49,31 +61,51 @@ with gr.Blocks() as demo:
|
|
49 |
|
50 |
# Validate button to move to next annotation
|
51 |
with gr.Row():
|
52 |
-
validate_button = gr.Button("Validate")
|
53 |
-
skip_button = gr.Button("Skip")
|
54 |
|
55 |
-
# Define action when validate button is clicked
|
56 |
validate_button.click(
|
57 |
-
submit_correction,
|
58 |
-
inputs=[insight_id,
|
59 |
-
outputs=[insight_id,
|
60 |
)
|
61 |
|
62 |
skip_button.click(
|
63 |
-
next_annotation,
|
64 |
-
inputs=[],
|
65 |
-
outputs=[insight_id,
|
66 |
)
|
67 |
|
68 |
-
|
69 |
-
corrected_text.change(
|
70 |
diff_texts, # Call diff function
|
71 |
-
inputs=[
|
72 |
-
outputs=diff_display
|
|
|
|
|
|
|
|
|
|
|
|
|
73 |
)
|
74 |
|
|
|
|
|
|
|
|
|
|
|
|
|
75 |
# Load the first set of texts when the demo starts
|
76 |
-
demo.load(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
77 |
|
78 |
|
79 |
if __name__ == "__main__":
|
|
|
1 |
import gradio as gr
|
2 |
|
3 |
+
from back_end import next_annotation, submit_correction, enable_buttons
|
4 |
from utils import diff_texts
|
5 |
|
6 |
|
|
|
11 |
### You can review corrections generated by our model and validate or correct the predictions.\n
|
12 |
Your feedback will be integrated to the OFF database!
|
13 |
|
14 |
+
To update a product in the Open Food Facts database, you need to indicate your Open Food Facts username and password.
|
15 |
+
If you're not registered yet, you can do it [here](https://world.openfoodfacts.org/cgi/user.pl)!
|
16 |
+
"""
|
17 |
+
)
|
18 |
|
19 |
with gr.Row():
|
20 |
with gr.Column():
|
21 |
insight_id = gr.Textbox(
|
22 |
label="Insight Id",
|
|
|
23 |
visible=False,
|
24 |
)
|
25 |
|
26 |
+
with gr.Row():
|
27 |
+
off_username = gr.Textbox(
|
28 |
+
label="OFF Username",
|
29 |
+
)
|
30 |
+
off_password = gr.Textbox(
|
31 |
+
label="OFF Password",
|
32 |
+
type="password",
|
33 |
+
)
|
34 |
+
|
35 |
+
# Saved to detect change from annotator
|
36 |
+
model_correction = gr.Text(visible=False)
|
37 |
+
|
38 |
+
original = gr.Textbox(
|
39 |
label="Original Text (Uneditable)",
|
40 |
info="This is the original text.",
|
41 |
interactive=False, # Make this text box uneditable
|
42 |
lines=3
|
43 |
)
|
44 |
|
45 |
+
annotator_correction = gr.Textbox(
|
46 |
label="Corrected Text (Editable)",
|
47 |
info="This is the AI-corrected text. You can modify it.",
|
48 |
interactive=True, # Make this text box editable
|
49 |
+
lines=3,
|
50 |
)
|
51 |
|
52 |
# Diff Display using HighlightedText
|
|
|
61 |
|
62 |
# Validate button to move to next annotation
|
63 |
with gr.Row():
|
64 |
+
validate_button = gr.Button("Validate", interactive=False)
|
65 |
+
skip_button = gr.Button("Skip", interactive=False)
|
66 |
|
|
|
67 |
validate_button.click(
|
68 |
+
submit_correction,
|
69 |
+
inputs=[insight_id, annotator_correction, model_correction, off_username, off_password],
|
70 |
+
outputs=[insight_id, original, model_correction, annotator_correction, image]
|
71 |
)
|
72 |
|
73 |
skip_button.click(
|
74 |
+
next_annotation,
|
75 |
+
inputs=[],
|
76 |
+
outputs=[insight_id, original, model_correction, annotator_correction, image]
|
77 |
)
|
78 |
|
79 |
+
annotator_correction.change(
|
|
|
80 |
diff_texts, # Call diff function
|
81 |
+
inputs=[original, annotator_correction],
|
82 |
+
outputs=diff_display
|
83 |
+
)
|
84 |
+
|
85 |
+
off_username.change(
|
86 |
+
enable_buttons,
|
87 |
+
inputs=[off_username, off_password],
|
88 |
+
outputs=[validate_button, skip_button]
|
89 |
)
|
90 |
|
91 |
+
off_password.change(
|
92 |
+
enable_buttons,
|
93 |
+
inputs=[off_username, off_password],
|
94 |
+
outputs=[validate_button, skip_button],
|
95 |
+
)
|
96 |
+
|
97 |
# Load the first set of texts when the demo starts
|
98 |
+
demo.load(
|
99 |
+
next_annotation,
|
100 |
+
inputs=[],
|
101 |
+
outputs=[
|
102 |
+
insight_id,
|
103 |
+
original,
|
104 |
+
model_correction,
|
105 |
+
annotator_correction,
|
106 |
+
image,
|
107 |
+
],
|
108 |
+
)
|
109 |
|
110 |
|
111 |
if __name__ == "__main__":
|
back_end.py
CHANGED
@@ -1,33 +1,81 @@
|
|
1 |
-
from typing import Dict, Tuple
|
2 |
import requests
|
|
|
|
|
|
|
|
|
|
|
3 |
|
4 |
from openfoodfacts.api import ProductResource, APIConfig
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
5 |
|
6 |
|
7 |
-
|
|
|
|
|
|
|
8 |
|
|
|
|
|
|
|
9 |
|
10 |
-
|
|
|
11 |
insight = import_random_insight()
|
|
|
12 |
return (
|
13 |
-
insight["id"],
|
14 |
-
insight["data"]["original"],
|
|
|
15 |
insight["data"]["correction"],
|
16 |
get_image_url(insight["barcode"])
|
17 |
)
|
18 |
|
19 |
|
20 |
-
def submit_correction(
|
21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
22 |
return next_annotation()
|
23 |
|
24 |
|
25 |
def import_random_insight(
|
26 |
insight_type: str = "ingredient_spellcheck",
|
27 |
-
count: int = 1,
|
28 |
predictor: str = "fine-tuned-mistral-7b",
|
29 |
) -> Dict:
|
30 |
-
url = f"{BASE_URL}/insights/random?count=
|
31 |
response = requests.get(url)
|
32 |
data = response.json()
|
33 |
insight = data["insights"][0]
|
@@ -36,29 +84,53 @@ def import_random_insight(
|
|
36 |
|
37 |
def submit_to_product_opener(
|
38 |
insight_id: str,
|
39 |
-
|
40 |
-
|
|
|
41 |
) -> None:
|
42 |
url = f"{BASE_URL}/insights/annotate"
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
"annotation": annotation,
|
47 |
-
"update": update,
|
48 |
}
|
49 |
-
|
50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
51 |
|
52 |
def get_image_url(
|
53 |
code: str,
|
54 |
user_agent: str = "Spellcheck-Annotate",
|
55 |
) -> str:
|
56 |
fields = ["image_ingredients_url"]
|
57 |
-
data = ProductResource(
|
58 |
-
api_config=APIConfig(user_agent=user_agent)
|
59 |
-
).get(
|
60 |
code=code,
|
61 |
fields=fields,
|
62 |
)
|
63 |
image_url = data.get(fields[0])
|
64 |
return image_url
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import Dict, Tuple, Optional, TypeAlias
|
2 |
import requests
|
3 |
+
import base64
|
4 |
+
import json
|
5 |
+
from dataclasses import dataclass
|
6 |
+
|
7 |
+
from utils import get_logger
|
8 |
|
9 |
from openfoodfacts.api import ProductResource, APIConfig
|
10 |
+
import gradio as gr
|
11 |
+
|
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 |
+
|
19 |
+
Annotation: TypeAlias = Tuple[str, str, str, str, str]
|
20 |
|
21 |
|
22 |
+
@dataclass
|
23 |
+
class Authentification:
|
24 |
+
username: str
|
25 |
+
password: str
|
26 |
|
27 |
+
def get_credentials(self) -> str:
|
28 |
+
credentials = f"{self.username}:{self.password}"
|
29 |
+
return base64.b64encode(credentials.encode()).decode()
|
30 |
|
31 |
+
|
32 |
+
def next_annotation() -> Annotation:
|
33 |
insight = import_random_insight()
|
34 |
+
logger.info("Imported insight: %s", insight)
|
35 |
return (
|
36 |
+
insight["id"],
|
37 |
+
insight["data"]["original"],
|
38 |
+
insight["data"]["correction"], # Saved for comparison with annotator changes
|
39 |
insight["data"]["correction"],
|
40 |
get_image_url(insight["barcode"])
|
41 |
)
|
42 |
|
43 |
|
44 |
+
def submit_correction(
|
45 |
+
insight_id: str,
|
46 |
+
annotator_correction: str,
|
47 |
+
model_correction: str,
|
48 |
+
username: str,
|
49 |
+
password: str,
|
50 |
+
update: bool = UPDATE
|
51 |
+
):
|
52 |
+
auth = Authentification(username, password)
|
53 |
+
correction = annotator_correction if annotator_correction != model_correction else None
|
54 |
+
try:
|
55 |
+
submit_to_product_opener(
|
56 |
+
insight_id=insight_id,
|
57 |
+
update=update,
|
58 |
+
annotator_correction=correction,
|
59 |
+
auth=auth,
|
60 |
+
)
|
61 |
+
except gr.Error as e:
|
62 |
+
gr.Warning(e.message) # We use gr.Warning instead of gr.Error to keep the flow
|
63 |
+
return (
|
64 |
+
gr.update(),
|
65 |
+
gr.update(),
|
66 |
+
gr.update(),
|
67 |
+
gr.update(),
|
68 |
+
gr.update(),
|
69 |
+
) # Stay unchanged
|
70 |
+
gr.Info("Product successfuly updated. Many thanks!")
|
71 |
return next_annotation()
|
72 |
|
73 |
|
74 |
def import_random_insight(
|
75 |
insight_type: str = "ingredient_spellcheck",
|
|
|
76 |
predictor: str = "fine-tuned-mistral-7b",
|
77 |
) -> Dict:
|
78 |
+
url = f"{BASE_URL}/insights/random?count=1&type={insight_type}&predictor={predictor}"
|
79 |
response = requests.get(url)
|
80 |
data = response.json()
|
81 |
insight = data["insights"][0]
|
|
|
84 |
|
85 |
def submit_to_product_opener(
|
86 |
insight_id: str,
|
87 |
+
update: bool,
|
88 |
+
auth: Authentification,
|
89 |
+
annotator_correction: Optional[str] = None,
|
90 |
) -> None:
|
91 |
url = f"{BASE_URL}/insights/annotate"
|
92 |
+
headers = {
|
93 |
+
"Authorization": f"Basic {auth.get_credentials()}",
|
94 |
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
|
|
|
95 |
}
|
96 |
+
if annotator_correction:
|
97 |
+
logger.info("Change from annotator. New insight sent to Product Opener. New correction: %s", annotator_correction)
|
98 |
+
payload = {
|
99 |
+
"insight_id": insight_id,
|
100 |
+
"annotation": 2,
|
101 |
+
"update": update,
|
102 |
+
"data": json.dumps({"annotation": annotator_correction}),
|
103 |
+
}
|
104 |
+
else:
|
105 |
+
logger.info("No change from annotator. Original insight sent to Product Opener.")
|
106 |
+
payload = {
|
107 |
+
"insight_id": insight_id,
|
108 |
+
"annotation": 1,
|
109 |
+
"update": update,
|
110 |
+
}
|
111 |
+
try:
|
112 |
+
response = requests.post(url, data=payload, headers=headers)
|
113 |
+
response.raise_for_status()
|
114 |
+
except requests.RequestException as e:
|
115 |
+
logger.error(e)
|
116 |
+
logger.error(response.content)
|
117 |
+
raise gr.Error("Failed to submit to Product Opener. Are your username and password correct?")
|
118 |
+
|
119 |
|
120 |
def get_image_url(
|
121 |
code: str,
|
122 |
user_agent: str = "Spellcheck-Annotate",
|
123 |
) -> str:
|
124 |
fields = ["image_ingredients_url"]
|
125 |
+
data = ProductResource(api_config=APIConfig(user_agent=user_agent)).get(
|
|
|
|
|
126 |
code=code,
|
127 |
fields=fields,
|
128 |
)
|
129 |
image_url = data.get(fields[0])
|
130 |
return image_url
|
131 |
+
|
132 |
+
|
133 |
+
def enable_buttons(username, password):
|
134 |
+
# Return the updated button states: interactive if both username and password are filled
|
135 |
+
state = bool(username) and bool(password)
|
136 |
+
return gr.update(interactive=state), gr.update(interactive=state)
|
utils.py
CHANGED
@@ -1,4 +1,5 @@
|
|
1 |
-
from difflib import Differ
|
|
|
2 |
|
3 |
|
4 |
def diff_texts(text1, text2):
|
@@ -6,4 +7,12 @@ def diff_texts(text1, text2):
|
|
6 |
return [
|
7 |
(token[2:], token[0] if token[0] != " " else None)
|
8 |
for token in d.compare(text1, text2)
|
9 |
-
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from difflib import Differ
|
2 |
+
import logging
|
3 |
|
4 |
|
5 |
def diff_texts(text1, text2):
|
|
|
7 |
return [
|
8 |
(token[2:], token[0] if token[0] != " " else None)
|
9 |
for token in d.compare(text1, text2)
|
10 |
+
]
|
11 |
+
|
12 |
+
|
13 |
+
def get_logger():
|
14 |
+
logging.basicConfig(
|
15 |
+
level=logging.INFO,
|
16 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
17 |
+
)
|
18 |
+
return logging.getLogger(__name__)
|