From 526edb63b0dcfc603e9eee182a915eae085f0e59 Mon Sep 17 00:00:00 2001 From: Twahaaa Date: Thu, 15 Jan 2026 14:28:51 +0530 Subject: [PATCH 1/3] feat(encoder): Implement audio embedding generation This commit introduces the functionality to generate audio embeddings using a pre-trained wav2vec2 model. The key changes include: - A new service to handle the embedding extraction. - Integration of this service into the main FastAPI application. - Updates to the test driver to use a real audio file for testing. - A typo fix in the audio preprocessing service. - Addition of and to to avoid committing large files. --- .gitignore | 4 ++ ml_v2/encoder/main.py | 9 ++-- ml_v2/encoder/services/generate_embeddings.py | 42 +++++++++++++++++++ ml_v2/encoder/services/preprocess_audio.py | 5 ++- ml_v2/orchastrater/test_driver.py | 7 +--- 5 files changed, 56 insertions(+), 11 deletions(-) create mode 100644 ml_v2/encoder/services/generate_embeddings.py diff --git a/.gitignore b/.gitignore index 4689f14..9d98c5e 100644 --- a/.gitignore +++ b/.gitignore @@ -169,3 +169,7 @@ ml/tests/sample_audio/ pytest_cache/ .coverage htmlcov/ + +audios/ + +models/ \ No newline at end of file diff --git a/ml_v2/encoder/main.py b/ml_v2/encoder/main.py index c0a4873..c77b6e2 100644 --- a/ml_v2/encoder/main.py +++ b/ml_v2/encoder/main.py @@ -3,6 +3,7 @@ import logging import librosa from services.preprocess_audio import preprocess_audio +from services.generate_embeddings import extract_embedding logging.basicConfig(level=logging.INFO) logger = logging.getLogger("encoder-service") @@ -20,15 +21,15 @@ async def vectorize_audio(file: UploadFile = File(...)): audio = preprocess_audio(file) - logger.info("Generated embedding successfully") + logger.info("Preprocessed audio successfully") + embedding = extract_embedding(audio) - #just dummy data for now, will add the encoder tomorrow - vector = [1,2,3,4,5] + logger.info("Generated embedding successfully") return { "fileName": file.filename, - "embedding": vector + "embedding": embedding.tolist() } diff --git a/ml_v2/encoder/services/generate_embeddings.py b/ml_v2/encoder/services/generate_embeddings.py new file mode 100644 index 0000000..9a86c09 --- /dev/null +++ b/ml_v2/encoder/services/generate_embeddings.py @@ -0,0 +1,42 @@ +import torch +import librosa +from functools import lru_cache +from transformers import Wav2Vec2FeatureExtractor, Wav2Vec2Model +from pathlib import Path +import numpy + +SAMPLE_RATE = 16000 + +@lru_cache() +def load_embedding_model(model_name="facebook/wav2vec2-large-xlsr-53"): + base_path = Path(__file__).resolve().parent.parent /"models"/"wav2vec2" + + feature_extractor = Wav2Vec2FeatureExtractor.from_pretrained( + model_name, + cache_dir = str(base_path) + ) + + model = Wav2Vec2Model.from_pretrained( + model_name, + cache_dir = str(base_path) + ) + + model.eval() + + return model, feature_extractor + +def extract_embedding(audio): + model, feature_extractor = load_embedding_model() + + inputs = feature_extractor( + audio, + sampling_rate = SAMPLE_RATE, + return_tensors = "pt", + padding = True + ) + + with torch.no_grad(): + outputs = model(**inputs) + embedding = outputs.last_hidden_state.mean(dim=1) + + return embedding.squeeze().cpu().numpy() \ No newline at end of file diff --git a/ml_v2/encoder/services/preprocess_audio.py b/ml_v2/encoder/services/preprocess_audio.py index dfdce53..56204f0 100644 --- a/ml_v2/encoder/services/preprocess_audio.py +++ b/ml_v2/encoder/services/preprocess_audio.py @@ -5,11 +5,12 @@ import os import shutil + def preprocess_audio(file: UploadFile): SAMPLE_RATE = 16000 filename = file.filename or "audio.wav" - file_ext = os.path.splittext(filename)[1] - + file_ext = os.path.splitext(filename)[1] + try: with tempfile.NamedTemporaryFile(suffix=file_ext) as tmp: diff --git a/ml_v2/orchastrater/test_driver.py b/ml_v2/orchastrater/test_driver.py index 63ac631..e1e5dd6 100644 --- a/ml_v2/orchastrater/test_driver.py +++ b/ml_v2/orchastrater/test_driver.py @@ -1,17 +1,14 @@ # orchestrator/test_driver.py import sys +import os # Fix python path to find 'services' sys.path.append(".") from services.remote_encoder import get_audio_embedding -# Make a dummy file to test -with open("test_audio.txt", "w") as f: - f.write("This is fake audio data") - try: print("Attempting to talk to Encoder...") - vector = get_audio_embedding("test_audio.txt") + vector = get_audio_embedding("../audios/English.m4a") print("\nSUCCESS! Received Vector:") print(vector) except Exception as e: From b71e81a84394d53c72874ead838672487e54557c Mon Sep 17 00:00:00 2001 From: Twahaaa Date: Thu, 15 Jan 2026 21:58:27 +0530 Subject: [PATCH 2/3] feat(ml_v2): add whisper transcription function --- ml_v2/orchastrater/services/whisper_utils.py | 50 ++++++++++++++++++++ ml_v2/orchastrater/test_driver.py | 7 ++- 2 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 ml_v2/orchastrater/services/whisper_utils.py diff --git a/ml_v2/orchastrater/services/whisper_utils.py b/ml_v2/orchastrater/services/whisper_utils.py new file mode 100644 index 0000000..f6e15a5 --- /dev/null +++ b/ml_v2/orchastrater/services/whisper_utils.py @@ -0,0 +1,50 @@ +from faster_whisper import WhisperModel +from utils.logger import get_logger +import os + +logger = get_logger("whisper-app") + +MODEL_SIZE = os.getenv("WHISPER_MODEL_SIZE","small") +DEVICE = "cpu" +COMPUTE_TYPE = "int8" + +class WhisperService: + _instance = None + + @classmethod + def get_instance(cls): + + if cls._instance is None: + logger.info(f"Loading whisper model '{MODEL_SIZE}' on {DEVICE}") + cls._instance = WhisperModel(MODEL_SIZE,device=DEVICE,compute_type=COMPUTE_TYPE) + logger.info(f"Loaded whisper model successfully") + return cls._instance + +def transcribe_audio(file_path: str): + model = WhisperService.get_instance() + + try: + + segments, info = model.transcribe( + file_path, + beam_size = 5, + vad_filter = True, + vad_parameters = dict(min_silence_duration_ms = 500) + ) + + full_text = " ".join([segment.text for segment in segments]).strip() + + result = { + "text" : full_text, + "language" : info.language, + "probability" : info.language_probability + } + + logger.info(f"Transcribed audio successfully: {info.language} ({info.language_probability:.2f})") + + return result + + except Exception as e: + logger.error(f"Failed to transcribe audio: {e}") + raise + diff --git a/ml_v2/orchastrater/test_driver.py b/ml_v2/orchastrater/test_driver.py index e1e5dd6..3cce5f5 100644 --- a/ml_v2/orchastrater/test_driver.py +++ b/ml_v2/orchastrater/test_driver.py @@ -5,11 +5,14 @@ sys.path.append(".") from services.remote_encoder import get_audio_embedding +from services.whisper_utils import transcribe_audio try: print("Attempting to talk to Encoder...") - vector = get_audio_embedding("../audios/English.m4a") + # vector = get_audio_embedding("../audios/English.m4a") + result = transcribe_audio("../audios/English.m4a") print("\nSUCCESS! Received Vector:") - print(vector) + # print(vector) + print(result) except Exception as e: print(f"\nFAILED: {e}") \ No newline at end of file From 42f7e0108e476298c157cda83de8700de642191b Mon Sep 17 00:00:00 2001 From: Twahaaa Date: Thu, 15 Jan 2026 23:40:30 +0530 Subject: [PATCH 3/3] feat(ml_v2): add database and environment utilities This commit introduces several enhancements to the ml_v2 orchestrator service: - Adds `python-dotenv` and `psycopg2-binary` to manage environment variables and database connections. - Creates `utils/db.py` and `utils/env.py` to handle database connections and environment variable loading. - Updates the `remote_encoder.py` service to use the `ENCODER_URL` from the environment. - Adds `data_structure.txt` to document the API contract between the frontend and the ml service. --- ml_v2/data_structure.txt | 45 ++++++++++++++++ ml_v2/orchastrater/pyproject.toml | 1 + ml_v2/orchastrater/services/remote_encoder.py | 5 +- ml_v2/orchastrater/utils/db.py | 48 +++++++++++++++++ ml_v2/orchastrater/utils/env.py | 17 ++++++ ml_v2/orchastrater/uv.lock | 54 +++++++++++++++++++ 6 files changed, 167 insertions(+), 3 deletions(-) create mode 100644 ml_v2/data_structure.txt create mode 100644 ml_v2/orchastrater/utils/db.py create mode 100644 ml_v2/orchastrater/utils/env.py diff --git a/ml_v2/data_structure.txt b/ml_v2/data_structure.txt new file mode 100644 index 0000000..f87d5e5 --- /dev/null +++ b/ml_v2/data_structure.txt @@ -0,0 +1,45 @@ +## Data Structures for Frontend <-> ML Backend Communication + +### Frontend to ML Backend (`/process` endpoint) + +When the frontend sends an audio file for processing, it makes a POST request to the `/process` endpoint of the `ml` backend. + +**Request Body:** +```json +{ + "file_url": "https://your-azure-blob-storage-url/audio.webm", + "lat": 12.9716, + "lng": 77.5946 +} +``` + +- `file_url` (string): The URL of the audio file stored in Azure Blob Storage. +- `lat` (number): The latitude of the user's location. +- `lng` (number): The longitude of the user's location. + +--- + +### ML Backend to Frontend + +After processing the audio, the `ml` backend returns a JSON object with the analysis results. + +**Response Body:** +```json +{ + "language": "en", + "confidence": 0.98, + "transcript": "A sample transcription of the audio.", + "cluster_id": 12, + "embedding": [0.123, 0.456, ...], + "lat": 12.9716, + "lng": 77.5946 +} +``` + +- `language` (string): The detected language code (e.g., "en", "hi"). +- `confidence` (float): A score from 0.0 to 1.0 indicating the model's confidence in the language detection. +- `transcript` (string): The text transcribed from the audio. +- `cluster_id` (integer | null): The ID of the cluster the audio is assigned to if it's an unknown dialect. It is `null` for known languages. +- `embedding` (array of floats): The vector representation of the audio. +- `lat` (number): The latitude passed in the original request. +- `lng` (number): The longitude passed in the original request. diff --git a/ml_v2/orchastrater/pyproject.toml b/ml_v2/orchastrater/pyproject.toml index ac19a07..7c32188 100644 --- a/ml_v2/orchastrater/pyproject.toml +++ b/ml_v2/orchastrater/pyproject.toml @@ -8,6 +8,7 @@ dependencies = [ "fastapi>=0.128.0", "faster-whisper>=1.2.1", "numpy>=2.4.1", + "psycopg2-binary>=2.9.11", "pydantic>=2.12.5", "requests>=2.32.5", "uvicorn>=0.40.0", diff --git a/ml_v2/orchastrater/services/remote_encoder.py b/ml_v2/orchastrater/services/remote_encoder.py index 1eda665..5d07862 100644 --- a/ml_v2/orchastrater/services/remote_encoder.py +++ b/ml_v2/orchastrater/services/remote_encoder.py @@ -2,11 +2,10 @@ import requests import os from utils.logger import get_logger +from utils.env import ENCODER_URL logger = get_logger(__name__) - -ENCODER_URL = "http://localhost:8001" - + def get_audio_embedding(file_path: str): """ Sends an audio file to the Encoder service and returns the vector. diff --git a/ml_v2/orchastrater/utils/db.py b/ml_v2/orchastrater/utils/db.py new file mode 100644 index 0000000..0893251 --- /dev/null +++ b/ml_v2/orchastrater/utils/db.py @@ -0,0 +1,48 @@ +import psycopg2 +from psycopg2.extras import RealDictCursor +from utils.logger import get_logger +from utils.env import DATABASE_URL + +logger = get_logger("db") + +def get_db_connection(): + + try: + conn = psycopg2.connect(DATABASE_URL) + return conn + except Exception as e: + logger.error("Failed to connect to the db") + raise + +def excecute_query(query: str, params: tuple = None, fetch_one = False, fetch_all = False): + + conn = None + + try: + conn = get_db_connection() + + with conn.cursor(cursor_factory=RealDictCursor) as cur: + cur.execute(query, params) + + if fetch_one: + result = cur.fetch_one() + + elif fetch_all: + result = cur.fetch_all() + + else: + result = None + + conn.commit() + + return result + + except Exception as e: + if conn: + conn.rollback() + logger.error(f"DB Query Failed: {e} | Query: {query}") + raise + + finally: + if conn: + conn.close() diff --git a/ml_v2/orchastrater/utils/env.py b/ml_v2/orchastrater/utils/env.py new file mode 100644 index 0000000..ec3fac0 --- /dev/null +++ b/ml_v2/orchastrater/utils/env.py @@ -0,0 +1,17 @@ +import os +import sys +from dotenv import load_dotenv + +# Load .env from the current directory +load_dotenv() + +def get_env_variable(key: str, default: str = None, required: bool = False) -> str: + value = os.getenv(key, default) + if required and not value: + print(f"CRITICAL ERROR: Missing environment variable '{key}'") + sys.exit(1) + return value + +# Config Variables +DATABASE_URL = get_env_variable("DATABASE_URL", required=True) +ENCODER_URL = get_env_variable("ENCODER_URL", "http://localhost:8001") \ No newline at end of file diff --git a/ml_v2/orchastrater/uv.lock b/ml_v2/orchastrater/uv.lock index 39fd031..08e1beb 100644 --- a/ml_v2/orchastrater/uv.lock +++ b/ml_v2/orchastrater/uv.lock @@ -528,6 +528,7 @@ dependencies = [ { name = "fastapi" }, { name = "faster-whisper" }, { name = "numpy" }, + { name = "psycopg2-binary" }, { name = "pydantic" }, { name = "requests" }, { name = "uvicorn" }, @@ -538,6 +539,7 @@ requires-dist = [ { name = "fastapi", specifier = ">=0.128.0" }, { name = "faster-whisper", specifier = ">=1.2.1" }, { name = "numpy", specifier = ">=2.4.1" }, + { name = "psycopg2-binary", specifier = ">=2.9.11" }, { name = "pydantic", specifier = ">=2.12.5" }, { name = "requests", specifier = ">=2.32.5" }, { name = "uvicorn", specifier = ">=0.40.0" }, @@ -567,6 +569,58 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/75/b1/1dc83c2c661b4c62d56cc081706ee33a4fc2835bd90f965baa2663ef7676/protobuf-6.33.4-py3-none-any.whl", hash = "sha256:1fe3730068fcf2e595816a6c34fe66eeedd37d51d0400b72fabc848811fdc1bc", size = 170532, upload-time = "2026-01-12T18:33:39.199Z" }, ] +[[package]] +name = "psycopg2-binary" +version = "2.9.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/6c/8767aaa597ba424643dc87348c6f1754dd9f48e80fdc1b9f7ca5c3a7c213/psycopg2-binary-2.9.11.tar.gz", hash = "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c", size = 379620, upload-time = "2025-10-10T11:14:48.041Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/ae/8d8266f6dd183ab4d48b95b9674034e1b482a3f8619b33a0d86438694577/psycopg2_binary-2.9.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0e8480afd62362d0a6a27dd09e4ca2def6fa50ed3a4e7c09165266106b2ffa10", size = 3756452, upload-time = "2025-10-10T11:11:11.583Z" }, + { url = "https://files.pythonhosted.org/packages/4b/34/aa03d327739c1be70e09d01182619aca8ebab5970cd0cfa50dd8b9cec2ac/psycopg2_binary-2.9.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:763c93ef1df3da6d1a90f86ea7f3f806dc06b21c198fa87c3c25504abec9404a", size = 3863957, upload-time = "2025-10-10T11:11:16.932Z" }, + { url = "https://files.pythonhosted.org/packages/48/89/3fdb5902bdab8868bbedc1c6e6023a4e08112ceac5db97fc2012060e0c9a/psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e164359396576a3cc701ba8af4751ae68a07235d7a380c631184a611220d9a4", size = 4410955, upload-time = "2025-10-10T11:11:21.21Z" }, + { url = "https://files.pythonhosted.org/packages/ce/24/e18339c407a13c72b336e0d9013fbbbde77b6fd13e853979019a1269519c/psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:d57c9c387660b8893093459738b6abddbb30a7eab058b77b0d0d1c7d521ddfd7", size = 4468007, upload-time = "2025-10-10T11:11:24.831Z" }, + { url = "https://files.pythonhosted.org/packages/91/7e/b8441e831a0f16c159b5381698f9f7f7ed54b77d57bc9c5f99144cc78232/psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2c226ef95eb2250974bf6fa7a842082b31f68385c4f3268370e3f3870e7859ee", size = 4165012, upload-time = "2025-10-10T11:11:29.51Z" }, + { url = "https://files.pythonhosted.org/packages/0d/61/4aa89eeb6d751f05178a13da95516c036e27468c5d4d2509bb1e15341c81/psycopg2_binary-2.9.11-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a311f1edc9967723d3511ea7d2708e2c3592e3405677bf53d5c7246753591fbb", size = 3981881, upload-time = "2025-10-30T02:55:07.332Z" }, + { url = "https://files.pythonhosted.org/packages/76/a1/2f5841cae4c635a9459fe7aca8ed771336e9383b6429e05c01267b0774cf/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ebb415404821b6d1c47353ebe9c8645967a5235e6d88f914147e7fd411419e6f", size = 3650985, upload-time = "2025-10-10T11:11:34.975Z" }, + { url = "https://files.pythonhosted.org/packages/84/74/4defcac9d002bca5709951b975173c8c2fa968e1a95dc713f61b3a8d3b6a/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f07c9c4a5093258a03b28fab9b4f151aa376989e7f35f855088234e656ee6a94", size = 3296039, upload-time = "2025-10-10T11:11:40.432Z" }, + { url = "https://files.pythonhosted.org/packages/6d/c2/782a3c64403d8ce35b5c50e1b684412cf94f171dc18111be8c976abd2de1/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:00ce1830d971f43b667abe4a56e42c1e2d594b32da4802e44a73bacacb25535f", size = 3043477, upload-time = "2025-10-30T02:55:11.182Z" }, + { url = "https://files.pythonhosted.org/packages/c8/31/36a1d8e702aa35c38fc117c2b8be3f182613faa25d794b8aeaab948d4c03/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cffe9d7697ae7456649617e8bb8d7a45afb71cd13f7ab22af3e5c61f04840908", size = 3345842, upload-time = "2025-10-10T11:11:45.366Z" }, + { url = "https://files.pythonhosted.org/packages/6e/b4/a5375cda5b54cb95ee9b836930fea30ae5a8f14aa97da7821722323d979b/psycopg2_binary-2.9.11-cp311-cp311-win_amd64.whl", hash = "sha256:304fd7b7f97eef30e91b8f7e720b3db75fee010b520e434ea35ed1ff22501d03", size = 2713894, upload-time = "2025-10-10T11:11:48.775Z" }, + { url = "https://files.pythonhosted.org/packages/d8/91/f870a02f51be4a65987b45a7de4c2e1897dd0d01051e2b559a38fa634e3e/psycopg2_binary-2.9.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:be9b840ac0525a283a96b556616f5b4820e0526addb8dcf6525a0fa162730be4", size = 3756603, upload-time = "2025-10-10T11:11:52.213Z" }, + { url = "https://files.pythonhosted.org/packages/27/fa/cae40e06849b6c9a95eb5c04d419942f00d9eaac8d81626107461e268821/psycopg2_binary-2.9.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f090b7ddd13ca842ebfe301cd587a76a4cf0913b1e429eb92c1be5dbeb1a19bc", size = 3864509, upload-time = "2025-10-10T11:11:56.452Z" }, + { url = "https://files.pythonhosted.org/packages/2d/75/364847b879eb630b3ac8293798e380e441a957c53657995053c5ec39a316/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ab8905b5dcb05bf3fb22e0cf90e10f469563486ffb6a96569e51f897c750a76a", size = 4411159, upload-time = "2025-10-10T11:12:00.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a0/567f7ea38b6e1c62aafd58375665a547c00c608a471620c0edc364733e13/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf940cd7e7fec19181fdbc29d76911741153d51cab52e5c21165f3262125685e", size = 4468234, upload-time = "2025-10-10T11:12:04.892Z" }, + { url = "https://files.pythonhosted.org/packages/30/da/4e42788fb811bbbfd7b7f045570c062f49e350e1d1f3df056c3fb5763353/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db", size = 4166236, upload-time = "2025-10-10T11:12:11.674Z" }, + { url = "https://files.pythonhosted.org/packages/3c/94/c1777c355bc560992af848d98216148be5f1be001af06e06fc49cbded578/psycopg2_binary-2.9.11-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a1cf393f1cdaf6a9b57c0a719a1068ba1069f022a59b8b1fe44b006745b59757", size = 3983083, upload-time = "2025-10-30T02:55:15.73Z" }, + { url = "https://files.pythonhosted.org/packages/bd/42/c9a21edf0e3daa7825ed04a4a8588686c6c14904344344a039556d78aa58/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3", size = 3652281, upload-time = "2025-10-10T11:12:17.713Z" }, + { url = "https://files.pythonhosted.org/packages/12/22/dedfbcfa97917982301496b6b5e5e6c5531d1f35dd2b488b08d1ebc52482/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:31b32c457a6025e74d233957cc9736742ac5a6cb196c6b68499f6bb51390bd6a", size = 3298010, upload-time = "2025-10-10T11:12:22.671Z" }, + { url = "https://files.pythonhosted.org/packages/66/ea/d3390e6696276078bd01b2ece417deac954dfdd552d2edc3d03204416c0c/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:edcb3aeb11cb4bf13a2af3c53a15b3d612edeb6409047ea0b5d6a21a9d744b34", size = 3044641, upload-time = "2025-10-30T02:55:19.929Z" }, + { url = "https://files.pythonhosted.org/packages/12/9a/0402ded6cbd321da0c0ba7d34dc12b29b14f5764c2fc10750daa38e825fc/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d", size = 3347940, upload-time = "2025-10-10T11:12:26.529Z" }, + { url = "https://files.pythonhosted.org/packages/b1/d2/99b55e85832ccde77b211738ff3925a5d73ad183c0b37bcbbe5a8ff04978/psycopg2_binary-2.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:b33fabeb1fde21180479b2d4667e994de7bbf0eec22832ba5d9b5e4cf65b6c6d", size = 2714147, upload-time = "2025-10-10T11:12:29.535Z" }, + { url = "https://files.pythonhosted.org/packages/ff/a8/a2709681b3ac11b0b1786def10006b8995125ba268c9a54bea6f5ae8bd3e/psycopg2_binary-2.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c", size = 3756572, upload-time = "2025-10-10T11:12:32.873Z" }, + { url = "https://files.pythonhosted.org/packages/62/e1/c2b38d256d0dafd32713e9f31982a5b028f4a3651f446be70785f484f472/psycopg2_binary-2.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee", size = 3864529, upload-time = "2025-10-10T11:12:36.791Z" }, + { url = "https://files.pythonhosted.org/packages/11/32/b2ffe8f3853c181e88f0a157c5fb4e383102238d73c52ac6d93a5c8bffe6/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0", size = 4411242, upload-time = "2025-10-10T11:12:42.388Z" }, + { url = "https://files.pythonhosted.org/packages/10/04/6ca7477e6160ae258dc96f67c371157776564679aefd247b66f4661501a2/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766", size = 4468258, upload-time = "2025-10-10T11:12:48.654Z" }, + { url = "https://files.pythonhosted.org/packages/3c/7e/6a1a38f86412df101435809f225d57c1a021307dd0689f7a5e7fe83588b1/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3", size = 4166295, upload-time = "2025-10-10T11:12:52.525Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7d/c07374c501b45f3579a9eb761cbf2604ddef3d96ad48679112c2c5aa9c25/psycopg2_binary-2.9.11-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84011ba3109e06ac412f95399b704d3d6950e386b7994475b231cf61eec2fc1f", size = 3983133, upload-time = "2025-10-30T02:55:24.329Z" }, + { url = "https://files.pythonhosted.org/packages/82/56/993b7104cb8345ad7d4516538ccf8f0d0ac640b1ebd8c754a7b024e76878/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4", size = 3652383, upload-time = "2025-10-10T11:12:56.387Z" }, + { url = "https://files.pythonhosted.org/packages/2d/ac/eaeb6029362fd8d454a27374d84c6866c82c33bfc24587b4face5a8e43ef/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c", size = 3298168, upload-time = "2025-10-10T11:13:00.403Z" }, + { url = "https://files.pythonhosted.org/packages/2b/39/50c3facc66bded9ada5cbc0de867499a703dc6bca6be03070b4e3b65da6c/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d526864e0f67f74937a8fce859bd56c979f5e2ec57ca7c627f5f1071ef7fee60", size = 3044712, upload-time = "2025-10-30T02:55:27.975Z" }, + { url = "https://files.pythonhosted.org/packages/9c/8e/b7de019a1f562f72ada81081a12823d3c1590bedc48d7d2559410a2763fe/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1", size = 3347549, upload-time = "2025-10-10T11:13:03.971Z" }, + { url = "https://files.pythonhosted.org/packages/80/2d/1bb683f64737bbb1f86c82b7359db1eb2be4e2c0c13b947f80efefa7d3e5/psycopg2_binary-2.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa", size = 2714215, upload-time = "2025-10-10T11:13:07.14Z" }, + { url = "https://files.pythonhosted.org/packages/64/12/93ef0098590cf51d9732b4f139533732565704f45bdc1ffa741b7c95fb54/psycopg2_binary-2.9.11-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1", size = 3756567, upload-time = "2025-10-10T11:13:11.885Z" }, + { url = "https://files.pythonhosted.org/packages/7c/a9/9d55c614a891288f15ca4b5209b09f0f01e3124056924e17b81b9fa054cc/psycopg2_binary-2.9.11-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e0deeb03da539fa3577fcb0b3f2554a97f7e5477c246098dbb18091a4a01c16f", size = 3864755, upload-time = "2025-10-10T11:13:17.727Z" }, + { url = "https://files.pythonhosted.org/packages/13/1e/98874ce72fd29cbde93209977b196a2edae03f8490d1bd8158e7f1daf3a0/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5", size = 4411646, upload-time = "2025-10-10T11:13:24.432Z" }, + { url = "https://files.pythonhosted.org/packages/5a/bd/a335ce6645334fb8d758cc358810defca14a1d19ffbc8a10bd38a2328565/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8", size = 4468701, upload-time = "2025-10-10T11:13:29.266Z" }, + { url = "https://files.pythonhosted.org/packages/44/d6/c8b4f53f34e295e45709b7568bf9b9407a612ea30387d35eb9fa84f269b4/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c", size = 4166293, upload-time = "2025-10-10T11:13:33.336Z" }, + { url = "https://files.pythonhosted.org/packages/4b/e0/f8cc36eadd1b716ab36bb290618a3292e009867e5c97ce4aba908cb99644/psycopg2_binary-2.9.11-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e35b7abae2b0adab776add56111df1735ccc71406e56203515e228a8dc07089f", size = 3983184, upload-time = "2025-10-30T02:55:32.483Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/2a8fe18a4e61cfb3417da67b6318e12691772c0696d79434184a511906dc/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747", size = 3652650, upload-time = "2025-10-10T11:13:38.181Z" }, + { url = "https://files.pythonhosted.org/packages/76/36/03801461b31b29fe58d228c24388f999fe814dfc302856e0d17f97d7c54d/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f", size = 3298663, upload-time = "2025-10-10T11:13:44.878Z" }, + { url = "https://files.pythonhosted.org/packages/97/77/21b0ea2e1a73aa5fa9222b2a6b8ba325c43c3a8d54272839c991f2345656/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:32770a4d666fbdafab017086655bcddab791d7cb260a16679cc5a7338b64343b", size = 3044737, upload-time = "2025-10-30T02:55:35.69Z" }, + { url = "https://files.pythonhosted.org/packages/67/69/f36abe5f118c1dca6d3726ceae164b9356985805480731ac6712a63f24f0/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d", size = 3347643, upload-time = "2025-10-10T11:13:53.499Z" }, + { url = "https://files.pythonhosted.org/packages/e1/36/9c0c326fe3a4227953dfb29f5d0c8ae3b8eb8c1cd2967aa569f50cb3c61f/psycopg2_binary-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", size = 2803913, upload-time = "2025-10-10T11:13:57.058Z" }, +] + [[package]] name = "pydantic" version = "2.12.5"