From 17a0405e580b30d1d20bb8716b93afbbfc00845b Mon Sep 17 00:00:00 2001 From: jvJUCA Date: Thu, 17 Jul 2025 19:29:21 -0300 Subject: [PATCH 1/6] feat: new predict endpoint --- app/main.py | 6 +++ app/routes/session.py | 85 +++++++++++++++++++++++++++++++++++++++++++ package-lock.json | 6 +++ 3 files changed, 97 insertions(+) create mode 100644 package-lock.json diff --git a/app/main.py b/app/main.py index f46bb31e..1b0f6dba 100644 --- a/app/main.py +++ b/app/main.py @@ -59,3 +59,9 @@ def calib_validation(): if request.method == 'POST': return session_route.calib_results() return Response('Invalid request method for route', status=405, mimetype='application/json') + +@app.route('/api/session/batch_predict', methods=['POST']) +def batch_predict(): + if request.method == 'POST': + return session_route.batch_predict() + return Response('Invalid request method for route', status=405, mimetype='application/json') \ No newline at end of file diff --git a/app/routes/session.py b/app/routes/session.py index e6c69076..ab3fc01f 100644 --- a/app/routes/session.py +++ b/app/routes/session.py @@ -8,6 +8,8 @@ import csv from pathlib import Path import os +import pandas as pd +import traceback import re ALLOWED_EXTENSIONS = {'txt', 'webm'} @@ -181,8 +183,91 @@ def calib_results(): # data = gaze_tracker.train_to_validate_calib(calib_csv_file, predict_csv_file) data = gaze_tracker.predict(calib_csv_file, calib_csv_file, k) + try: + payload = { + "session_id": file_name, + "model": data, + "screen_height": screen_height, + "screen_width": screen_width, + "k": k + } + + RUXAILAB_WEBHOOK_URL = "https://us-central1-ruxailab-prod.cloudfunctions.net/receiveCalibration" + + resp = requests.post(RUXAILAB_WEBHOOK_URL, json=payload) + print("Enviado para RuxaiLab:", resp.status_code, resp.text) + except Exception as e: + print("Erro ao enviar para RuxaiLab:", e) + return Response(json.dumps(data), status=200, mimetype='application/json') +def batch_predict(): + try: + data = request.get_json() + + iris_data = data['iris_tracking_data'] + k = data.get('k', 3) + screen_height = data.get('screen_height') + screen_width = data.get('screen_width') + + base_path = Path().absolute() / 'app/services/calib_validation/csv/data' + calib_csv_path = base_path / 'vcczxvzxcv_fixed_train_data.csv' + predict_csv_path = base_path / 'temp_batch_predict.csv' + + print(f"Calib CSV Path: {calib_csv_path}") + print(f"Predict CSV Path: {predict_csv_path}") + print(f"Iris data sample (até 3): {iris_data[:3]}") + + # Debug: colunas do CSV de calibração + df_calib = pd.read_csv(calib_csv_path) + print("Colunas do CSV de calibração:", df_calib.columns.tolist()) + + # Cria CSV temporário com dados de íris para predição + with open(predict_csv_path, 'w', newline='') as csvfile: + writer = csv.DictWriter(csvfile, fieldnames=[ + 'left_iris_x', 'left_iris_y', 'right_iris_x', 'right_iris_y' + ]) + writer.writeheader() + for item in iris_data: + writer.writerow({ + 'left_iris_x': item['left_iris_x'], + 'left_iris_y': item['left_iris_y'], + 'right_iris_x': item['right_iris_x'], + 'right_iris_y': item['right_iris_y'] + }) + + # Chama a predição com os dois CSVs de calibração + predictions = gaze_tracker.predict( + calib_csv_path, + calib_csv_path, + k + ) + + # Verifica se o retorno é lista, dicionário ou outro + if isinstance(predictions, list): + # Se for lista, adiciona timestamp e metadados a cada item + for i in range(len(predictions)): + predictions[i]['timestamp'] = iris_data[i].get('timestamp') + if screen_height is not None: + predictions[i]['screen_height'] = screen_height + if screen_width is not None: + predictions[i]['screen_width'] = screen_width + elif isinstance(predictions, dict): + # Se for dicionário, anexa metadados gerais (exemplo) + if screen_height is not None: + predictions['screen_height'] = screen_height + if screen_width is not None: + predictions['screen_width'] = screen_width + # Timestamp pode não fazer sentido em dicionário com estrutura complexa + else: + print("Retorno da predição tem tipo inesperado:", type(predictions)) + + return Response(json.dumps(predictions), status=200, mimetype='application/json') + + except Exception as e: + print("Erro na batch_predict:", e) + traceback.print_exc() + return Response("Erro interno na predição", status=500) # def session_results(): # session_id = request.args.__getitem__('id') diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..91396f55 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "eye-tracker-api", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} From cdfae28b80cf62cc5f1c672d2fa3ee3578d37088 Mon Sep 17 00:00:00 2001 From: jvJUCA Date: Wed, 15 Oct 2025 10:52:01 -0300 Subject: [PATCH 2/6] fix: calib function --- app/requirements.txt | 20 ++++++++++++++++++++ app/routes/session.py | 39 +++++++++++++++++++++++---------------- requirements.txt | 2 ++ wsgi.py | 4 +--- 4 files changed, 46 insertions(+), 19 deletions(-) create mode 100644 app/requirements.txt diff --git a/app/requirements.txt b/app/requirements.txt new file mode 100644 index 00000000..ba4582f3 --- /dev/null +++ b/app/requirements.txt @@ -0,0 +1,20 @@ +blinker==1.9.0 +click==8.1.8 +Flask==3.1.0 +flask-cors==5.0.1 +itsdangerous==2.2.0 +Jinja2==3.1.6 +joblib==1.4.2 +MarkupSafe==3.0.2 +numpy==2.2.4 +pandas==2.2.3 +python-dateutil==2.9.0.post0 +pytz==2025.2 +scikit-learn==1.6.1 +scipy==1.15.2 +six==1.17.0 +threadpoolctl==3.6.0 +tzdata==2025.2 +Werkzeug==3.1.3 +gunicorn==23.0.0 +requests==2.31.0 \ No newline at end of file diff --git a/app/routes/session.py b/app/routes/session.py index ab3fc01f..3c4d9016 100644 --- a/app/routes/session.py +++ b/app/routes/session.py @@ -11,6 +11,7 @@ import pandas as pd import traceback import re +import requests ALLOWED_EXTENSIONS = {'txt', 'webm'} COLLECTION_NAME = u'session' @@ -141,6 +142,7 @@ def calib_results(): + from_ruxailab = json.loads(request.form['from_ruxailab']) file_name = json.loads(request.form['file_name']) fixed_points = json.loads(request.form['fixed_circle_iris_points']) calib_points = json.loads(request.form['calib_circle_iris_points']) @@ -180,27 +182,32 @@ def calib_results(): except IOError: print("I/O error") - # data = gaze_tracker.train_to_validate_calib(calib_csv_file, predict_csv_file) + # Run prediction data = gaze_tracker.predict(calib_csv_file, calib_csv_file, k) - try: - payload = { - "session_id": file_name, - "model": data, - "screen_height": screen_height, - "screen_width": screen_width, - "k": k - } - - RUXAILAB_WEBHOOK_URL = "https://us-central1-ruxailab-prod.cloudfunctions.net/receiveCalibration" - - resp = requests.post(RUXAILAB_WEBHOOK_URL, json=payload) - print("Enviado para RuxaiLab:", resp.status_code, resp.text) - except Exception as e: - print("Erro ao enviar para RuxaiLab:", e) + if from_ruxailab: + try: + payload = { + "session_id": file_name, + "model": data, + "screen_height": screen_height, + "screen_width": screen_width, + "k": k + } + + RUXAILAB_WEBHOOK_URL = "https://receivecalibration-ffptzpxikq-uc.a.run.app" + + print("file_name:", file_name) + + resp = requests.post(RUXAILAB_WEBHOOK_URL, json=payload) + print("Enviado para RuxaiLab:", resp.status_code, resp.text) + except Exception as e: + print("Erro ao enviar para RuxaiLab:", e) return Response(json.dumps(data), status=200, mimetype='application/json') + + def batch_predict(): try: data = request.get_json() diff --git a/requirements.txt b/requirements.txt index fe6f6368..ba4582f3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,3 +16,5 @@ six==1.17.0 threadpoolctl==3.6.0 tzdata==2025.2 Werkzeug==3.1.3 +gunicorn==23.0.0 +requests==2.31.0 \ No newline at end of file diff --git a/wsgi.py b/wsgi.py index 1b27f453..00e9d35f 100644 --- a/wsgi.py +++ b/wsgi.py @@ -1,7 +1,5 @@ from app.main import app import os - if __name__ == "__main__": - app.run(debug=True, host="0.0.0.0", port=int(os.environ.get("PORT", 5000))) - + app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 8080))) From 02894c74998de61e44150b7799c4081f3db4a0b4 Mon Sep 17 00:00:00 2001 From: jvJUCA Date: Wed, 15 Oct 2025 11:10:48 -0300 Subject: [PATCH 3/6] fix conflicts --- app/routes/session.py | 35 ----------------------------------- wsgi.py | 4 ---- 2 files changed, 39 deletions(-) diff --git a/app/routes/session.py b/app/routes/session.py index a4af3588..38eec9e9 100644 --- a/app/routes/session.py +++ b/app/routes/session.py @@ -155,7 +155,6 @@ def calib_results(): -<<<<<<< HEAD from_ruxailab = json.loads(request.form['from_ruxailab']) file_name = json.loads(request.form['file_name']) fixed_points = json.loads(request.form['fixed_circle_iris_points']) @@ -163,28 +162,6 @@ def calib_results(): screen_height = json.loads(request.form['screen_height']) screen_width = json.loads(request.form['screen_width']) k = json.loads(request.form['k']) -======= - """ - Generate calibration results. - - This function generates calibration results based on the provided form data. - It saves the calibration points to a CSV file. Then, it uses the gaze_tracker module to predict the calibration results. - - Returns: - Response: A JSON response containing the calibration results. - - Raises: - IOError: If there is an error while writing to the CSV files. - """ - # Get form data from request - file_name = json.loads(request.form["file_name"]) - fixed_points = json.loads(request.form["fixed_circle_iris_points"]) - calib_points = json.loads(request.form["calib_circle_iris_points"]) - screen_height = json.loads(request.form["screen_height"]) - screen_width = json.loads(request.form["screen_width"]) - k = json.loads(request.form["k"]) - model = json.loads(request.form["model"]) ->>>>>>> 42a70612727088340cf95589066fb593eb246472 # Generate csv dataset of calibration points os.makedirs( @@ -237,7 +214,6 @@ def calib_results(): except IOError: print("I/O error") -<<<<<<< HEAD # Run prediction data = gaze_tracker.predict(calib_csv_file, calib_csv_file, k) @@ -250,17 +226,6 @@ def calib_results(): "screen_width": screen_width, "k": k } -======= - # data = gaze_tracker.train_to_validate_calib(calib_csv_file, predict_csv_file) - try: - payload = { - "session_id": file_name, - "model": data, - "screen_height": screen_height, - "screen_width": screen_width, - "k": k - } ->>>>>>> 42a70612727088340cf95589066fb593eb246472 RUXAILAB_WEBHOOK_URL = "https://receivecalibration-ffptzpxikq-uc.a.run.app" diff --git a/wsgi.py b/wsgi.py index f4088cc8..f4846df2 100644 --- a/wsgi.py +++ b/wsgi.py @@ -14,8 +14,4 @@ from app.main import app if __name__ == "__main__": -<<<<<<< HEAD app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 8080))) -======= - app.run(debug=True, host="0.0.0.0", port=int(os.environ.get("PORT", 5000))) ->>>>>>> 42a70612727088340cf95589066fb593eb246472 From 9558d8f907fe176f37aec717cacf11acaa79b73f Mon Sep 17 00:00:00 2001 From: jvJUCA Date: Wed, 15 Oct 2025 11:26:12 -0300 Subject: [PATCH 4/6] fix conflicts --- requirements.txt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index 5ac4e6d8..69429c32 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,10 +15,6 @@ scipy==1.15.2 six==1.17.0 threadpoolctl==3.6.0 tzdata==2025.2 -<<<<<<< HEAD Werkzeug==3.1.3 gunicorn==23.0.0 requests==2.31.0 -======= -Werkzeug==3.1.3 ->>>>>>> 42a70612727088340cf95589066fb593eb246472 From 7bc8b71f1f9dc76ad51e469e96e3a7d1d2df448d Mon Sep 17 00:00:00 2001 From: jvJUCA Date: Mon, 10 Nov 2025 11:17:05 -0300 Subject: [PATCH 5/6] feat: dockerfiles and dockerignore --- .dockerignore | 38 ++++++++++++++++++++++++++++++++++---- Dockerfile | 23 +++++++++++++++++------ 2 files changed, 51 insertions(+), 10 deletions(-) diff --git a/.dockerignore b/.dockerignore index 3e4bdd9f..877bd67d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,7 +1,37 @@ -Dockerfile -README.md +# Arquivos e pastas que não precisam ir pro container + +# Git +.git +.gitignore + +# Python cache +__pycache__/ *.pyc *.pyo *.pyd -__pycache__ -.pytest_cache +*.pdb +.pytest_cache/ +*.pytest_cache + +# Virtualenvs +env/ +venv/ +.venv/ + +# Build / distribuições +build/ +dist/ +*.egg-info/ +*.egg + +# Logs e DB locais +*.log +*.sqlite3 +*.db + +# Node +node_modules/ + +# Outros +.DS_Store +*.swp diff --git a/Dockerfile b/Dockerfile index 52d78912..ef9c757c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,23 @@ FROM python:3.11-slim -ENV PYTHONUNBUFFERED True -ENV APP_HOME /app -ENV PORT 5000 +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + APP_HOME=/app \ + PORT=8080 WORKDIR $APP_HOME -COPY . ./ -RUN pip install --no-cache-dir -r requirements.txt +COPY requirements.txt . -CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 wsgi:app +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + && pip install --no-cache-dir -r requirements.txt \ + && apt-get purge -y --auto-remove build-essential \ + && rm -rf /var/lib/apt/lists/* + +COPY . . + +EXPOSE 8080 + +# Usando JSON array no CMD (mais seguro) +CMD ["gunicorn", "--bind", ":8080", "--workers", "1", "--threads", "8", "--timeout", "0", "wsgi:app"] From 24e2de92ab5c23101cfd59af7316a72f624180f6 Mon Sep 17 00:00:00 2001 From: jvJUCA Date: Mon, 10 Nov 2025 11:18:44 -0300 Subject: [PATCH 6/6] feat: new methods of predictions --- app/routes/session.py | 79 +++++++++++++++++---------------- app/services/gaze_tracker.py | 84 ++++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 36 deletions(-) diff --git a/app/routes/session.py b/app/routes/session.py index 38eec9e9..5384302d 100644 --- a/app/routes/session.py +++ b/app/routes/session.py @@ -10,11 +10,8 @@ import pandas as pd import traceback import re -<<<<<<< HEAD import requests -======= from flask import Flask, request, Response, send_file ->>>>>>> 42a70612727088340cf95589066fb593eb246472 # Local imports from app from app.services.storage import save_file_locally @@ -161,6 +158,8 @@ def calib_results(): calib_points = json.loads(request.form['calib_circle_iris_points']) screen_height = json.loads(request.form['screen_height']) screen_width = json.loads(request.form['screen_width']) + model_X = json.loads(request.form.get('model', '"Linear Regression"')) + model_Y = json.loads(request.form.get('model', '"Linear Regression"')) k = json.loads(request.form['k']) # Generate csv dataset of calibration points @@ -215,7 +214,7 @@ def calib_results(): print("I/O error") # Run prediction - data = gaze_tracker.predict(calib_csv_file, calib_csv_file, k) + data = gaze_tracker.predict(calib_csv_file, k, model_X, model_Y) if from_ruxailab: try: @@ -238,30 +237,28 @@ def calib_results(): return Response(json.dumps(data), status=200, mimetype='application/json') - - def batch_predict(): try: data = request.get_json() - iris_data = data['iris_tracking_data'] k = data.get('k', 3) screen_height = data.get('screen_height') screen_width = data.get('screen_width') - - base_path = Path().absolute() / 'app/services/calib_validation/csv/data' - calib_csv_path = base_path / 'vcczxvzxcv_fixed_train_data.csv' + model_X = data.get('model_X', 'Linear Regression') + model_Y = data.get('model_Y', 'Linear Regression') + calib_id = data.get('calib_id') + if not calib_id: + return Response("Missing 'calib_id' in request", status=400) + + base_path = Path().absolute() / 'app/services/calib_validation/csv/data' + calib_csv_path = base_path / f"{calib_id}_fixed_train_data.csv" predict_csv_path = base_path / 'temp_batch_predict.csv' print(f"Calib CSV Path: {calib_csv_path}") print(f"Predict CSV Path: {predict_csv_path}") print(f"Iris data sample (até 3): {iris_data[:3]}") - # Debug: colunas do CSV de calibração - df_calib = pd.read_csv(calib_csv_path) - print("Colunas do CSV de calibração:", df_calib.columns.tolist()) - - # Cria CSV temporário com dados de íris para predição + # Gera CSV temporário com os dados de íris with open(predict_csv_path, 'w', newline='') as csvfile: writer = csv.DictWriter(csvfile, fieldnames=[ 'left_iris_x', 'left_iris_y', 'right_iris_x', 'right_iris_y' @@ -275,33 +272,43 @@ def batch_predict(): 'right_iris_y': item['right_iris_y'] }) - # Chama a predição com os dois CSVs de calibração - predictions = gaze_tracker.predict( - calib_csv_path, + # Chama a função de predição corretamente + predictions_raw = gaze_tracker.predict_new_data( calib_csv_path, + predict_csv_path, + model_X, + model_Y, k ) - # Verifica se o retorno é lista, dicionário ou outro - if isinstance(predictions, list): - # Se for lista, adiciona timestamp e metadados a cada item - for i in range(len(predictions)): - predictions[i]['timestamp'] = iris_data[i].get('timestamp') - if screen_height is not None: - predictions[i]['screen_height'] = screen_height - if screen_width is not None: - predictions[i]['screen_width'] = screen_width - elif isinstance(predictions, dict): - # Se for dicionário, anexa metadados gerais (exemplo) - if screen_height is not None: - predictions['screen_height'] = screen_height - if screen_width is not None: - predictions['screen_width'] = screen_width - # Timestamp pode não fazer sentido em dicionário com estrutura complexa + # Constrói uma resposta mais visual e direta + result = [] + if isinstance(predictions_raw, dict): + # Percorre o dicionário retornado e transforma em lista plana + for true_x, inner_dict in predictions_raw.items(): + if true_x == "centroids": + continue + for true_y, info in inner_dict.items(): + pred_x_list = info.get("predicted_x", []) + pred_y_list = info.get("predicted_y", []) + precision = info.get("PrecisionSD") + accuracy = info.get("Accuracy") + + for i, (px, py) in enumerate(zip(pred_x_list, pred_y_list)): + timestamp = iris_data[i].get("timestamp") if i < len(iris_data) else None + result.append({ + "timestamp": timestamp, + "predicted_x": px, + "predicted_y": py, + "precision": precision, + "accuracy": accuracy, + "screen_width": screen_width, + "screen_height": screen_height + }) else: - print("Retorno da predição tem tipo inesperado:", type(predictions)) + print("Retorno inesperado da função predict:", type(predictions_raw)) - return Response(json.dumps(predictions), status=200, mimetype='application/json') + return Response(json.dumps(result), status=200, mimetype='application/json') except Exception as e: print("Erro na batch_predict:", e) diff --git a/app/services/gaze_tracker.py b/app/services/gaze_tracker.py index b2506c9f..d3377a65 100644 --- a/app/services/gaze_tracker.py +++ b/app/services/gaze_tracker.py @@ -235,6 +235,90 @@ def predict(data, k, model_X, model_Y): # Return the data return data +def predict_new_data_simple(calib_csv_path, predict_csv_path, model_X, model_Y, k=3): + """ + Versão simplificada de predict_new_data. + Treina modelos nos dados de calibração e prevê coordenadas nos novos dados. + Retorna o mesmo formato que a função `predict`. + """ + # -------------------- SCALERS -------------------- + sc_x = StandardScaler() + sc_y = StandardScaler() + + # -------------------- TREINO -------------------- + df_train = pd.read_csv(calib_csv_path).drop(["screen_height", "screen_width"], axis=1) + + X_train_x = df_train[["left_iris_x", "right_iris_x"]].values + y_train_x = df_train["point_x"].values + X_train_y = df_train[["left_iris_y", "right_iris_y"]].values + y_train_y = df_train["point_y"].values + + X_train_x_scaled = sc_x.fit_transform(X_train_x) + X_train_y_scaled = sc_y.fit_transform(X_train_y) + + # Modelos + model_fit_x = models[model_X].fit(X_train_x_scaled, y_train_x) + model_fit_y = models[model_Y].fit(X_train_y_scaled, y_train_y) + + # -------------------- NOVOS DADOS -------------------- + df_predict = pd.read_csv(predict_csv_path) + X_pred_x = sc_x.transform(df_predict[["left_iris_x", "right_iris_x"]].values) + X_pred_y = sc_y.transform(df_predict[["left_iris_y", "right_iris_y"]].values) + + y_pred_x = model_fit_x.predict(X_pred_x) + y_pred_y = model_fit_y.predict(X_pred_y) + + # Garantir valores não-negativos + y_pred_x = np.clip(y_pred_x, 0, None) + y_pred_y = np.clip(y_pred_y, 0, None) + + # -------------------- KMEANS -------------------- + data_pred = np.array([y_pred_x, y_pred_y]).T + kmeans_model = KMeans(n_clusters=k, n_init="auto", init="k-means++") + y_kmeans = kmeans_model.fit_predict(data_pred) + + # -------------------- FORMATA DADOS -------------------- + df_data = pd.DataFrame({ + "Predicted X": y_pred_x, + "Predicted Y": y_pred_y, + "True X": df_predict["point_x"] if "point_x" in df_predict else y_pred_x, + "True Y": df_predict["point_y"] if "point_y" in df_predict else y_pred_y + }) + + # Calcular métricas + precision_x = df_data.groupby(["True X", "True Y"]).apply(func_precision_x) + precision_y = df_data.groupby(["True X", "True Y"]).apply(func_presicion_y) + precision_xy = (precision_x + precision_y) / 2 + precision_xy /= np.mean(precision_xy) + + accuracy_x = df_data.groupby(["True X", "True Y"]).apply(func_accuracy_x) + accuracy_y = df_data.groupby(["True X", "True Y"]).apply(func_accuracy_y) + accuracy_xy = (accuracy_x + accuracy_y) / 2 + accuracy_xy /= np.mean(accuracy_xy) + + # Estrutura final + data = {} + for index, row in df_data.iterrows(): + outer_key = str(int(row["True X"])) + inner_key = str(int(row["True Y"])) + if outer_key not in data: + data[outer_key] = {} + data[outer_key][inner_key] = { + "predicted_x": df_data[ + (df_data["True X"] == row["True X"]) & + (df_data["True Y"] == row["True Y"]) + ]["Predicted X"].tolist(), + "predicted_y": df_data[ + (df_data["True X"] == row["True X"]) & + (df_data["True Y"] == row["True Y"]) + ]["Predicted Y"].tolist(), + "PrecisionSD": precision_xy[(row["True X"], row["True Y"])], + "Accuracy": accuracy_xy[(row["True X"], row["True Y"])], + } + + data["centroids"] = kmeans_model.cluster_centers_.tolist() + return data + def train_to_validate_calib(calib_csv_file, predict_csv_file): dataset_train_path = calib_csv_file