Skip to content

Commit a9f07bb

Browse files
Merge branch 'openfoodfacts:main' into main
2 parents 06544bf + 390415b commit a9f07bb

File tree

9 files changed

+302
-430
lines changed

9 files changed

+302
-430
lines changed

.env

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,10 @@ ENABLE_ML_PREDICTIONS=False
6060

6161
# If you want to enable listening for Redis updates, set this to True
6262
ENABLE_REDIS_UPDATES=False
63+
64+
# Google specific configuration for Gemini AI
65+
# Gemini can use Vertex AI or Gemini API, we force it to use Vertex AI
66+
# as it allows us to set the location
67+
GOOGLE_GENAI_USE_VERTEXAI=true
68+
# Force location to be Paris
69+
GOOGLE_CLOUD_LOCATION=europe-west9

.github/workflows/container-deploy.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,9 @@ jobs:
145145
echo "POSTGRES_PASSWORD=${{ secrets.POSTGRES_PASSWORD }}" >> .env
146146
echo "ENVIRONMENT=${{ env.ENVIRONMENT }}" >> .env
147147
echo "GOOGLE_CLOUD_VISION_API_KEY=${{ secrets.GOOGLE_CLOUD_VISION_API_KEY }}" >> .env
148-
echo "GOOGLE_GEMINI_API_KEY=${{ secrets.GOOGLE_GEMINI_API_KEY }}" >> .env
148+
echo "GOOGLE_GENAI_USE_VERTEXAI=true" >> .env
149+
echo "GOOGLE_CLOUD_LOCATION=europe-west9" >> .env
150+
echo "GOOGLE_CREDENTIALS=${{ secrets.GOOGLE_CREDENTIALS }}" >> .env
149151
echo "TRITON_URI=${{ env.TRITON_URI }}" >> .env
150152
echo "ENABLE_ML_PREDICTIONS=True" >> .env
151153
# echo "ENABLE_IMPORT_OFF_DB_TASK=True" >> .env # disabled because we have the REDIS live updates

.github/workflows/merge-conflicts-autolabel.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ on:
77
concurrency:
88
group: ${{ github.workflow }}-${{ github.ref }}
99
cancel-in-progress: true
10-
10+
1111
jobs:
1212
triage:
1313
runs-on: ubuntu-latest

config/settings.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -293,8 +293,9 @@
293293
# Google Gemini API
294294
# ------------------------------------------------------------------------------
295295

296-
GOOGLE_GEMINI_API_KEY = os.getenv("GOOGLE_GEMINI_API_KEY")
297-
296+
# Google service account credentials. This is a base64-encoded version of the
297+
# JSON file
298+
GOOGLE_CREDENTIALS = os.getenv("GOOGLE_CREDENTIALS")
298299

299300
# Triton Inference Server (ML)
300301
# ------------------------------------------------------------------------------

docker-compose.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ x-api-common: &api-common
1616
- SENTRY_DSN
1717
- LOG_LEVEL
1818
- GOOGLE_CLOUD_VISION_API_KEY
19-
- GOOGLE_GEMINI_API_KEY
19+
- GOOGLE_GENAI_USE_VERTEXAI
20+
- GOOGLE_CLOUD_LOCATION
21+
- GOOGLE_CREDENTIALS
2022
- TRITON_URI
2123
- ENABLE_ML_PREDICTIONS
2224
- ENABLE_IMPORT_OFF_DB_TASK

open_prices/common/google.py

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
1-
import google.generativeai as genai
1+
import base64
2+
import json
3+
import logging
4+
from functools import lru_cache
5+
from pathlib import Path
6+
27
from django.conf import settings
8+
from google import genai
9+
from google.genai import types
10+
11+
logger = logging.getLogger(__name__)
312

413
GOOGLE_CLOUD_VISION_OCR_API_URL = "https://vision.googleapis.com/v1/images:annotate"
514
GOOGLE_CLOUD_VISION_OCR_FEATURES = [
@@ -10,14 +19,46 @@
1019
"FACE_DETECTION",
1120
]
1221
GEMINI_MODEL_NAME = "gemini"
13-
GEMINI_MODEL_VERSION = "gemini-1.5-flash"
22+
GEMINI_MODEL_VERSION = "gemini-2.5-flash"
23+
24+
25+
def check_google_credentials() -> None:
26+
"""Create Google Application Default Credentials (ADC) from variable if
27+
doesn't exist.
28+
29+
See
30+
https://cloud.google.com/docs/authentication/set-up-adc-local-dev-environment#service-account
31+
for more information.
32+
"""
33+
credentials_path = Path(
34+
"~/.config/gcloud/application_default_credentials.json"
35+
).expanduser()
36+
if not credentials_path.is_file():
37+
if settings.GOOGLE_CREDENTIALS:
38+
logger.info(
39+
"No google credentials found at %s. Creating credentials from GOOGLE_CREDENTIALS.",
40+
credentials_path,
41+
)
42+
credentials_path.parent.mkdir(parents=True, exist_ok=True)
43+
credentials_base64 = settings.GOOGLE_CREDENTIALS
44+
credentials = json.loads(
45+
base64.b64decode(credentials_base64).decode("utf-8")
46+
)
47+
with open(credentials_path, "w") as f:
48+
json.dump(credentials, f, indent=4)
49+
else:
50+
logger.info(
51+
"No google credentials found in environment variable GOOGLE_CREDENTIALS",
52+
)
1453

1554

16-
genai.configure(api_key=settings.GOOGLE_GEMINI_API_KEY)
17-
gemini_model = genai.GenerativeModel(model_name=GEMINI_MODEL_VERSION)
55+
@lru_cache(maxsize=1)
56+
def get_genai_client() -> genai.Client:
57+
check_google_credentials()
58+
return genai.Client()
1859

1960

20-
def get_generation_config(response_schema):
21-
return genai.GenerationConfig(
61+
def get_generation_config(response_schema) -> types.GenerateContentConfig:
62+
return types.GenerateContentConfig(
2263
response_mime_type="application/json", response_schema=response_schema
2364
)

open_prices/proofs/ml.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ def extract_from_price_tag(image: Image.Image) -> Label:
193193
:param image: the input Pillow image
194194
:return: the extracted information as a dictionary
195195
"""
196-
196+
client = common_google.get_genai_client()
197197
# Gemini model max payload size is 20MB
198198
# To prevent the payload from being too large, we resize the images
199199
max_size = 1024
@@ -209,12 +209,13 @@ def extract_from_price_tag(image: Image.Image) -> Label:
209209
"I expect a single JSON in your reply, no more, no less. "
210210
"If you cannot decode an attribute, set it to an empty string."
211211
)
212-
response = common_google.gemini_model.generate_content(
213-
[
212+
response = client.models.generate_content(
213+
model=common_google.GEMINI_MODEL_VERSION,
214+
contents=[
214215
prompt,
215216
image,
216217
],
217-
generation_config=common_google.get_generation_config(Label),
218+
config=common_google.get_generation_config(Label),
218219
)
219220
return json.loads(response.text)
220221

@@ -249,9 +250,11 @@ def extract_from_price_tags(images: Image.Image) -> Labels:
249250
f"I expect a list of {len(image_list)} labels in your reply, no more, no less. "
250251
"If you cannot decode an attribute, set it to an empty string"
251252
)
252-
response = common_google.gemini_model.generate_content(
253-
[prompt] + image_list,
254-
generation_config=common_google.get_generation_config(Labels),
253+
client = common_google.get_genai_client()
254+
response = client.models.generate_content(
255+
model=common_google.GEMINI_MODEL_VERSION,
256+
contents=[prompt] + image_list,
257+
config=common_google.get_generation_config(Labels),
255258
)
256259
return json.loads(response.text)
257260

@@ -267,12 +270,14 @@ def extract_from_receipt(image: Image.Image) -> Receipt:
267270
image.thumbnail((max_size, max_size))
268271

269272
prompt = "Extract all relevent information, use empty strings for unknown values."
270-
response = common_google.gemini_model.generate_content(
271-
[
273+
client = common_google.get_genai_client()
274+
response = client.models.generate_content(
275+
model=common_google.GEMINI_MODEL_VERSION,
276+
contents=[
272277
prompt,
273278
image,
274279
],
275-
generation_config=common_google.get_generation_config(Receipt),
280+
config=common_google.get_generation_config(Receipt),
276281
)
277282
return json.loads(response.text)
278283

0 commit comments

Comments
 (0)