jeremyarancio commited on
Commit
e01620b
·
1 Parent(s): 7d9c0d3

feat: :zap: App connected to PO

Browse files
Files changed (3) hide show
  1. app.py +54 -22
  2. back_end.py +93 -21
  3. 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
- *Note: We are working on connecting this tool to OFF Product Opener. 👷*
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
- original_text = gr.Textbox(
 
 
 
 
 
 
 
 
 
 
 
 
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
- corrected_text = gr.Textbox(
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, # Function to handle submission
58
- inputs=[insight_id, original_text, corrected_text], # Original and edited texts as inputs
59
- outputs=[insight_id, original_text, corrected_text, image] # Load next pair of texts
60
  )
61
 
62
  skip_button.click(
63
- next_annotation, # Function to handle submission
64
- inputs=[], # Original and edited texts as inputs
65
- outputs=[insight_id, original_text, corrected_text, image] # Load next pair of texts
66
  )
67
 
68
- # Update diff display dynamically when corrected text is modified
69
- corrected_text.change(
70
  diff_texts, # Call diff function
71
- inputs=[original_text, corrected_text], # Compare original and corrected texts
72
- outputs=diff_display # Update diff display
 
 
 
 
 
 
73
  )
74
 
 
 
 
 
 
 
75
  # Load the first set of texts when the demo starts
76
- demo.load(next_annotation, inputs=[], outputs=[insight_id, original_text, corrected_text, image])
 
 
 
 
 
 
 
 
 
 
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
- BASE_URL = "https://robotoff.openfoodfacts.org/api/v1/"
 
 
 
8
 
 
 
 
9
 
10
- def next_annotation() -> Tuple[str, str, str, str]:
 
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(insight_id: str, original: str, correction: str) -> Tuple[str, str, str]:
21
- # Not implemented yet
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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={count}&type={insight_type}&predictor={predictor}"
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
- skipped: bool,
40
- update: int = 0,
 
41
  ) -> None:
42
  url = f"{BASE_URL}/insights/annotate"
43
- annotation = -1 if skipped else 1
44
- data = {
45
- "insight_id": insight_id,
46
- "annotation": annotation,
47
- "update": update,
48
  }
49
- requests.post(url, data=data)
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__)