diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index ab05e29..37ae6b1 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -42,4 +42,4 @@ Verify that the following are valid * ... ## Other Information - \ No newline at end of file + diff --git a/.gitignore b/.gitignore index b3b1fe9..7515298 100644 --- a/.gitignore +++ b/.gitignore @@ -447,4 +447,4 @@ apps/whisper_fine_tuning/data/ data/ -predictions_dir/ \ No newline at end of file +predictions_dir/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c1ea265..72ff87d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ repos: rev: 24.10.0 hooks: - id: black - language_version: python3.11 # require Python 3.11 or newer + language_version: python3.12 # require Python 3.12 or newer - repo: https://github.com/charliermarsh/ruff-pre-commit rev: v0.1.0 @@ -41,4 +41,4 @@ repos: - --disable-error-code=used-before-def - --disable-error-code=attr-defined files: \.py$ - language_version: python3.11 + language_version: python3.12 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0c82519..6bbfac5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -47,7 +47,7 @@ chances of your issue being dealt with quickly: * **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be causing the problem (line of code or commit) -You can file new issues by providing the above information at the corresponding repository's issues link: +You can file new issues by providing the above information at the corresponding repository's issues link: replace`[organization-name]` and `[repository-name]` in `https://github.com/[organization-name]/[repository-name]/issues/new` . diff --git a/LICENSE.md b/LICENSE.md index 7965606..9e841e7 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -18,4 +18,4 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE \ No newline at end of file + SOFTWARE diff --git a/README.md b/README.md index ddc1e72..e57e006 100644 --- a/README.md +++ b/README.md @@ -294,4 +294,3 @@ Feel free to open issues for missing docs or suggested improvements. --- Maintained by the community with ❤️. Contributions welcome. - diff --git a/apps/text_generation/src/__init__.py b/apps/text_generation/src/__init__.py index 8c2d26c..ce86e5a 100644 --- a/apps/text_generation/src/__init__.py +++ b/apps/text_generation/src/__init__.py @@ -2,14 +2,15 @@ __author__ = "AI Apps GBB Team" __version__ = "0.1.0" -import os import logging +import os import sys from logging.handlers import RotatingFileHandler try: # pragma: no cover - optional dependency for local tooling from dotenv import load_dotenv # type: ignore[import-not-found] except ImportError: # pragma: no cover - provide fallback + def load_dotenv(*args, **kwargs): # type: ignore[override] return False @@ -19,26 +20,29 @@ def setup_logging() -> logging.Logger: logger.setLevel(logging.DEBUG) console_handler = logging.StreamHandler(sys.stdout) - console_format = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + console_format = logging.Formatter( + "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + ) console_handler.setFormatter(console_format) file_handler = RotatingFileHandler( - "app.log", maxBytes=10*1024*1024, backupCount=5 + "app.log", maxBytes=10 * 1024 * 1024, backupCount=5 ) file_handler.setLevel(logging.DEBUG) file_format = logging.Formatter( - '%(asctime)s - %(name)s - %(levelname)s - %(message)s' + "%(asctime)s - %(name)s - %(levelname)s - %(message)s" ) file_handler.setFormatter(file_format) logger.addHandler(console_handler) logger.addHandler(file_handler) - + return logger + logger = setup_logging() logger.info(f"{__app__} - {__author__} - Version: {__version__} initialized") -env_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), '.env') +env_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), ".env") if not load_dotenv(dotenv_path=env_path): # pragma: no cover - optional env file - logger.debug("dotenv skipped or not found at %s", env_path) \ No newline at end of file + logger.debug("dotenv skipped or not found at %s", env_path) diff --git a/apps/text_generation/src/blob.py b/apps/text_generation/src/blob.py index b37e5df..c0804d0 100644 --- a/apps/text_generation/src/blob.py +++ b/apps/text_generation/src/blob.py @@ -1,115 +1,119 @@ -from abc import ABC, abstractmethod -from copy import deepcopy -from typing import Optional, Tuple, Any, BinaryIO import os -import tempfile -import shutil import pathlib - -from fastapi import UploadFile +import shutil +import tempfile +from abc import ABC, abstractmethod +from copy import deepcopy +from typing import Any, Optional, Tuple from azure.identity import DefaultAzureCredential # type: ignore[import-not-found] from azure.storage.blob import BlobServiceClient # type: ignore[import-not-found] +from fastapi import UploadFile from config.settings import BlobConfig - LANGUAGES = ["matis", "mayuruna", "katukina"] TYPES = ["paper", "book", "dictionary"] class BlobUploaderPrototype(ABC): - """Prototype interface for blob uploaders.""" + """Prototype interface for blob uploaders.""" - @abstractmethod - def clone(self) -> Any: - """Create a copy of this uploader instance.""" + @abstractmethod + def clone(self) -> Any: + """Create a copy of this uploader instance.""" - @abstractmethod - def upload_pdf(self, file_path: str) -> str: - """Upload a PDF and return the blob path. + @abstractmethod + def upload_pdf(self, file_path: str) -> str: + """Upload a PDF and return the blob path. - Args: - file_path: Local path to the PDF file. + Args: + file_path: Local path to the PDF file. - Returns: - The path of the uploaded blob inside the container. - """ + Returns: + The path of the uploaded blob inside the container. + """ class BlobStorageUploader(BlobUploaderPrototype): - """Uploader that sends PDF files to Azure Blob Storage organized by language and type. - - The class implements the Prototype pattern via `clone()` so different uploader - instances can be created from a base configuration. For easier testing, a - container client may be injected directly. - - Args: - config: `BlobConfig` containing connection details. - container_client: Optional pre-created container client for testing or - advanced scenarios. If None, the client is created from the - connection string in `config`. - """ - - def __init__(self, config: BlobConfig, container_client: Optional[Any] = None): - self.config = config - if container_client is not None: - # Allow dependency injection for testing - self.container_client = container_client - self.client = None - else: - if BlobServiceClient is None or DefaultAzureCredential is None: - raise RuntimeError("Azure Blob dependencies are required when no container client is provided") - credential = DefaultAzureCredential() - self.client = BlobServiceClient(account_url=self.config.account_url, credential=credential) - self.container_client = self.client.get_container_client(self.config.container_name) - - def clone(self) -> "BlobStorageUploader": - """Return a deep copy of this uploader (including config). - - Note: cloned uploader will keep the same container client reference. - """ - - return deepcopy(self) - - def infer_language_and_type(self, filename: str) -> Tuple[str, str]: - """Infer language and document type from a filename. - - Simple heuristic: find the first known language and type contained in the - lowercased filename. Falls back to 'unknown' and 'other'. - - Args: - filename: Name of the file to inspect. - - Returns: - A tuple (language, doc_type). - """ - - lower = filename.lower() - language = next((lang for lang in LANGUAGES if lang in lower), "unknown") - doc_type = next((typ for typ in TYPES if typ in lower), "other") - return language, doc_type - - def upload_pdf(self, file_path: str) -> str: - """Upload a PDF file to the container under language/type folders. - - The blob path will be: // - - Args: - file_path: Local path to the PDF file. - - Returns: - The blob path used for the uploaded file. - """ - - filename = os.path.basename(file_path) - language, doc_type = self.infer_language_and_type(filename) - blob_path = f"{language}/{doc_type}/{filename}" - # Open the file in binary mode and upload. The container client is - # expected to implement upload_blob(blob_path, data, overwrite=True). - with open(file_path, "rb") as data: - self.container_client.upload_blob(blob_path, data, overwrite=True) - return blob_path + """Uploader that sends PDF files to Azure Blob Storage organized by language and type. + + The class implements the Prototype pattern via `clone()` so different uploader + instances can be created from a base configuration. For easier testing, a + container client may be injected directly. + + Args: + config: `BlobConfig` containing connection details. + container_client: Optional pre-created container client for testing or + advanced scenarios. If None, the client is created from the + connection string in `config`. + """ + + def __init__(self, config: BlobConfig, container_client: Optional[Any] = None): + self.config = config + if container_client is not None: + # Allow dependency injection for testing + self.container_client = container_client + self.client = None + else: + if BlobServiceClient is None or DefaultAzureCredential is None: + raise RuntimeError( + "Azure Blob dependencies are required when no container client is provided" + ) + credential = DefaultAzureCredential() + self.client = BlobServiceClient( + account_url=self.config.account_url, credential=credential + ) + self.container_client = self.client.get_container_client( + self.config.container_name + ) + + def clone(self) -> "BlobStorageUploader": + """Return a deep copy of this uploader (including config). + + Note: cloned uploader will keep the same container client reference. + """ + + return deepcopy(self) + + def infer_language_and_type(self, filename: str) -> Tuple[str, str]: + """Infer language and document type from a filename. + + Simple heuristic: find the first known language and type contained in the + lowercased filename. Falls back to 'unknown' and 'other'. + + Args: + filename: Name of the file to inspect. + + Returns: + A tuple (language, doc_type). + """ + + lower = filename.lower() + language = next((lang for lang in LANGUAGES if lang in lower), "unknown") + doc_type = next((typ for typ in TYPES if typ in lower), "other") + return language, doc_type + + def upload_pdf(self, file_path: str) -> str: + """Upload a PDF file to the container under language/type folders. + + The blob path will be: // + + Args: + file_path: Local path to the PDF file. + + Returns: + The blob path used for the uploaded file. + """ + + filename = os.path.basename(file_path) + language, doc_type = self.infer_language_and_type(filename) + blob_path = f"{language}/{doc_type}/{filename}" + # Open the file in binary mode and upload. The container client is + # expected to implement upload_blob(blob_path, data, overwrite=True). + with open(file_path, "rb") as data: + self.container_client.upload_blob(blob_path, data, overwrite=True) + return blob_path def make_blob_uploader() -> BlobStorageUploader: diff --git a/apps/text_generation/src/config/settings.py b/apps/text_generation/src/config/settings.py index 13ff9e1..b7401eb 100644 --- a/apps/text_generation/src/config/settings.py +++ b/apps/text_generation/src/config/settings.py @@ -12,7 +12,7 @@ >>> os.environ.pop("AZURE_SEARCH_INDEX", None) >>> os.environ.pop("PDF_INDEX_NAME", None) >>> from config.settings import AzureSearchSettings ->>> AzureSearchSettings().index_name +>>> AzureSearchSettings().index_name 'pdf-index-v2' >>> os.environ['AZURE_SEARCH_INDEX'] = 'my-index' >>> AzureSearchSettings().index_name @@ -21,10 +21,11 @@ """ from __future__ import annotations + import os -from pydantic_settings import BaseSettings from pydantic import Field +from pydantic_settings import BaseSettings class AzureSearchSettings(BaseSettings): @@ -58,11 +59,17 @@ class AzureSearchSettings(BaseSettings): >>> os.environ.pop('AZURE_SEARCH_INDEX', None) """ - endpoint: str = Field(default_factory=lambda: os.getenv("AZURE_AI_SEARCH_ENDPOINT", "")) + endpoint: str = Field( + default_factory=lambda: os.getenv("AZURE_AI_SEARCH_ENDPOINT", "") + ) index_name: str = Field( - default_factory=lambda: os.getenv("AZURE_SEARCH_INDEX", os.getenv("PDF_INDEX_NAME", "pdf-index-v2")) + default_factory=lambda: os.getenv( + "AZURE_SEARCH_INDEX", os.getenv("PDF_INDEX_NAME", "pdf-index-v2") + ) + ) + azure_search_key: str = Field( + default_factory=lambda: os.getenv("AZURE_AI_SEARCH_KEY", "") ) - azure_search_key: str = Field(default_factory=lambda: os.getenv("AZURE_AI_SEARCH_KEY", "")) embedding_vectorizer_endpoint: str = Field( default_factory=lambda: os.getenv("AZURE_OPENAI_ENDPOINT", "") ) @@ -103,7 +110,9 @@ class AzureFoundrySettings(BaseSettings): """ openai_endpoint: str = Field( - default_factory=lambda: os.getenv("AZURE_OPENAI_ENDPOINT", os.getenv("AZURE_FOUNDRY_URL", "")) + default_factory=lambda: os.getenv( + "AZURE_OPENAI_ENDPOINT", os.getenv("AZURE_FOUNDRY_URL", "") + ) ) foundry_endpoint: str = Field( default_factory=lambda: os.getenv("AZURE_FOUNDRY_URL", "") @@ -145,19 +154,31 @@ class BlobConfig(BaseSettings): >>> os.environ.pop('AZURE_BLOB_CONTAINER_SOURCE', None) """ - account_url: str = Field(default_factory=lambda: os.getenv("AZURE_STORAGE_ACCOUNT_URL", "")) - container_name: str = Field(default_factory=lambda: os.getenv("AZURE_BLOB_CONTAINER_SOURCE", "pdf-source")) - artifacts_container: str = Field(default_factory=lambda: os.getenv("AZURE_BLOB_CONTAINER_ARTIFACTS", "artifacts")) + account_url: str = Field( + default_factory=lambda: os.getenv("AZURE_STORAGE_ACCOUNT_URL", "") + ) + container_name: str = Field( + default_factory=lambda: os.getenv("AZURE_BLOB_CONTAINER_SOURCE", "pdf-source") + ) + artifacts_container: str = Field( + default_factory=lambda: os.getenv("AZURE_BLOB_CONTAINER_ARTIFACTS", "artifacts") + ) class CosmosSettings(BaseSettings): """Configuration for Azure Cosmos DB persistence.""" endpoint: str = Field(default_factory=lambda: os.getenv("COSMOS_DB_ENDPOINT", "")) - database_name: str = Field(default_factory=lambda: os.getenv("COSMOS_DATABASE_ORIGINAL", "")) - container_name: str = Field(default_factory=lambda: os.getenv("COSMOS_CONTAINER", "")) + database_name: str = Field( + default_factory=lambda: os.getenv("COSMOS_DATABASE_ORIGINAL", "") + ) + container_name: str = Field( + default_factory=lambda: os.getenv("COSMOS_CONTAINER", "") + ) key: str = Field(default_factory=lambda: os.getenv("COSMOS_DB_KEY", "")) - partition_key: str = Field(default_factory=lambda: os.getenv("COSMOS_PARTITION_KEY", "")) + partition_key: str = Field( + default_factory=lambda: os.getenv("COSMOS_PARTITION_KEY", "") + ) class AppSettings(BaseSettings): @@ -191,8 +212,14 @@ class AppSettings(BaseSettings): foundry: AzureFoundrySettings = Field(default_factory=AzureFoundrySettings) storage: BlobConfig = Field(default_factory=BlobConfig) cosmos: CosmosSettings = Field(default_factory=CosmosSettings) - embedding_dimensions: int = Field(default_factory=lambda: int(os.getenv("AZURE_OPENAI_EMBEDDING_DIMENSIONS", 1536))) - azure_foundry_key: str = Field(default_factory=lambda: os.getenv("AZURE_FOUNDRY_KEY", "")) + embedding_dimensions: int = Field( + default_factory=lambda: int( + os.getenv("AZURE_OPENAI_EMBEDDING_DIMENSIONS", 1536) + ) + ) + azure_foundry_key: str = Field( + default_factory=lambda: os.getenv("AZURE_FOUNDRY_KEY", "") + ) class Config: env_prefix = "" # explicit field env names @@ -213,4 +240,4 @@ def load_settings() -> AppSettings: >>> isinstance(load_settings(), AppSettings) True """ - return AppSettings() \ No newline at end of file + return AppSettings() diff --git a/apps/text_generation/src/configurations.py b/apps/text_generation/src/configurations.py index 504aeb5..b6c650c 100644 --- a/apps/text_generation/src/configurations.py +++ b/apps/text_generation/src/configurations.py @@ -2,8 +2,8 @@ import os -from pydantic_settings import BaseSettings from pydantic import Field +from pydantic_settings import BaseSettings class FoundryAuthConfig(BaseSettings): @@ -12,27 +12,47 @@ class FoundryAuthConfig(BaseSettings): class ChatConfig(BaseSettings): endpoint: str = Field(default_factory=lambda: os.getenv("AZURE_FOUNDRY_URL", "")) - deployment: str = Field(default_factory=lambda: os.getenv("AZURE_OPENAI_CHAT_DEPLOYMENT", "")) + deployment: str = Field( + default_factory=lambda: os.getenv("AZURE_OPENAI_CHAT_DEPLOYMENT", "") + ) auth: FoundryAuthConfig = Field(default_factory=FoundryAuthConfig) class EmbeddingConfig(BaseSettings): - endpoint: str = Field(default_factory=lambda: os.getenv("AZURE_OPENAI_EMBEDDING_ENDPOINT", "")) - model: str = Field(default_factory=lambda: os.getenv("AZURE_OPENAI_EMBEDDING_MODEL", "")) + endpoint: str = Field( + default_factory=lambda: os.getenv("AZURE_OPENAI_EMBEDDING_ENDPOINT", "") + ) + model: str = Field( + default_factory=lambda: os.getenv("AZURE_OPENAI_EMBEDDING_MODEL", "") + ) dimensions: int = Field(default_factory=lambda: int(os.getenv("EMBED_DIM", "1024"))) auth: FoundryAuthConfig = Field(default_factory=FoundryAuthConfig) class EmbeddingVectorizerConfig(BaseSettings): - endpoint: str = Field(default_factory=lambda: os.getenv("AZURE_OPENAI_EMBEDDING_VECTORIZER_ENDPOINT", "")) - model: str = Field(default_factory=lambda: os.getenv("AZURE_OPENAI_EMBEDDING_VECTORIZER_MODEL", "")) - deployment: str = Field(default_factory=lambda: os.getenv("AZURE_OPENAI_EMBEDDING_VECTORIZER_DEPLOYMENT", "")) + endpoint: str = Field( + default_factory=lambda: os.getenv( + "AZURE_OPENAI_EMBEDDING_VECTORIZER_ENDPOINT", "" + ) + ) + model: str = Field( + default_factory=lambda: os.getenv("AZURE_OPENAI_EMBEDDING_VECTORIZER_MODEL", "") + ) + deployment: str = Field( + default_factory=lambda: os.getenv( + "AZURE_OPENAI_EMBEDDING_VECTORIZER_DEPLOYMENT", "" + ) + ) class SearchConfig(BaseSettings): - endpoint: str = Field(default_factory=lambda: os.getenv("AZURE_SEARCH_ENDPOINT", "")) + endpoint: str = Field( + default_factory=lambda: os.getenv("AZURE_SEARCH_ENDPOINT", "") + ) key: str = Field(default_factory=lambda: os.getenv("AZURE_SEARCH_API_KEY", "")) - index_name: str = Field(default_factory=lambda: os.getenv("PDF_INDEX_NAME", "pdf-index-v2")) + index_name: str = Field( + default_factory=lambda: os.getenv("PDF_INDEX_NAME", "pdf-index-v2") + ) class PDFIngestionConfig(BaseSettings): @@ -42,7 +62,9 @@ class PDFIngestionConfig(BaseSettings): class AppConfigBundle(BaseSettings): search: SearchConfig = Field(default_factory=SearchConfig) embedding: EmbeddingConfig = Field(default_factory=EmbeddingConfig) - embedding_vectorizer: EmbeddingVectorizerConfig = Field(default_factory=EmbeddingVectorizerConfig) + embedding_vectorizer: EmbeddingVectorizerConfig = Field( + default_factory=EmbeddingVectorizerConfig + ) chat: ChatConfig = Field(default_factory=ChatConfig) pdf: PDFIngestionConfig = Field(default_factory=PDFIngestionConfig) auth: FoundryAuthConfig = Field(default_factory=FoundryAuthConfig) @@ -57,40 +79,52 @@ def __init__(self, bundle: AppConfigBundle): self._b = bundle @property - def search_service_endpoint(self) -> str: return self._b.search.endpoint + def search_service_endpoint(self) -> str: + return self._b.search.endpoint @property - def search_service_key(self) -> str: return self._b.search.key + def search_service_key(self) -> str: + return self._b.search.key @property - def pdf_index_name(self) -> str: return self._b.search.index_name + def pdf_index_name(self) -> str: + return self._b.search.index_name @property - def pdf_folder(self) -> str: return self._b.pdf.folder + def pdf_folder(self) -> str: + return self._b.pdf.folder @property - def azure_foundry_key(self) -> str: return self._b.auth.api_key + def azure_foundry_key(self) -> str: + return self._b.auth.api_key @property - def azure_foundry_url(self) -> str: return self._b.chat.endpoint + def azure_foundry_url(self) -> str: + return self._b.chat.endpoint @property - def chat_deployment(self) -> str: return self._b.chat.deployment + def chat_deployment(self) -> str: + return self._b.chat.deployment @property - def embedding_endpoint(self) -> str: return self._b.embedding.endpoint + def embedding_endpoint(self) -> str: + return self._b.embedding.endpoint @property - def embedding_model(self) -> str: return self._b.embedding.model + def embedding_model(self) -> str: + return self._b.embedding.model @property - def embedding_dimensions(self) -> int: return self._b.embedding.dimensions + def embedding_dimensions(self) -> int: + return self._b.embedding.dimensions @property - def embedding_vectorizer_endpoint(self) -> str: return self._b.embedding_vectorizer.endpoint + def embedding_vectorizer_endpoint(self) -> str: + return self._b.embedding_vectorizer.endpoint @property - def embedding_vectorizer_model(self) -> str: return self._b.embedding_vectorizer.model + def embedding_vectorizer_model(self) -> str: + return self._b.embedding_vectorizer.model def load_app_config(compat: bool = True): diff --git a/apps/text_generation/src/dialogues.json b/apps/text_generation/src/dialogues.json index 3f95743..f588d0a 100644 --- a/apps/text_generation/src/dialogues.json +++ b/apps/text_generation/src/dialogues.json @@ -1,487 +1,487 @@ -[ - { - "id": "1.1", - "ator": "Atendente", - "fala": "Olá! Estou aqui para te ajudar com seu atendimento bancário. Como posso ajudar?" - }, - { - "id": "1.2", - "ator": "Cliente", - "fala": "Quero acessar minha conta e verificar meu saldo." - }, - { - "id": "1.3", - "ator": "Atendente", - "fala": "Entendido! Por favor, vá até o terminal de autoatendimento para iniciar o processo." - }, - { - "id": "2.1", - "ator": "IA", - "fala": "Por favor, coloque seu dedo no leitor biométrico para identificarmos sua conta." - }, - { - "id": "2.2", - "ator": "Cliente", - "fala": "Tudo bem, já fiz isso." - }, - { - "id": "2.3", - "ator": "IA", - "fala": "Identificação concluída. Sua conta está ativa e funcionando normalmente." - }, - { - "id": "2.4", - "ator": "IA", - "fala": "Deseja consultar seu saldo ou acessar outro serviço?" - }, - { - "id": "2.5", - "ator": "Cliente", - "fala": "Quero ver meu saldo e saber se há algum problema com minha conta." - }, - { - "id": "3.1", - "ator": "IA", - "fala": "Seu saldo atual é de R$ 2.000,00. Não há problemas com sua conta." - }, - { - "id": "3.2", - "ator": "Cliente", - "fala": "Ótimo! Posso ver os depósitos dos últimos três meses?" - }, - { - "id": "3.3", - "ator": "IA", - "fala": "Sim, os depósitos estão disponíveis na tela. Deseja imprimir o extrato?" - }, - { - "id": "3.4", - "ator": "Cliente", - "fala": "Sim, quero imprimir o extrato." - }, - { - "id": "3.5", - "ator": "IA", - "fala": "Extrato impresso. Precisa de mais alguma coisa?" - }, - { - "id": "4.1", - "ator": "Cliente", - "fala": "Meu cartão está com problema. Como faço para resolver isso?" - }, - { - "id": "4.2", - "ator": "IA", - "fala": "Seu cartão foi bloqueado. Deseja solicitar um novo cartão?" - }, - { - "id": "4.3", - "ator": "Cliente", - "fala": "Sim, quero pedir um novo cartão." - }, - { - "id": "4.4", - "ator": "IA", - "fala": "Novo cartão solicitado com sucesso. O comprovante será enviado por SMS." - }, - { - "id": "4.5", - "ator": "Cliente", - "fala": "Obrigado! Isso resolve meu problema." - }, - { - "id": "5.1", - "ator": "IA", - "fala": "Seu cadastro está incompleto. Precisamos atualizá-lo." - }, - { - "id": "5.2", - "ator": "Cliente", - "fala": "Ok, quero atualizar meus dados agora." - }, - { - "id": "5.3", - "ator": "IA", - "fala": "Por favor, insira os documentos necessários no leitor do terminal." - }, - { - "id": "5.4", - "ator": "Cliente", - "fala": "Já enviei os documentos. O que mais preciso fazer?" - }, - { - "id": "5.5", - "ator": "IA", - "fala": "Cadastro atualizado com sucesso. Precisa de mais alguma coisa?" - }, - { - "id": "6.1", - "ator": "Cliente", - "fala": "Minha conta está bloqueada. Como faço para desbloqueá-la?" - }, - { - "id": "6.2", - "ator": "IA", - "fala": "Sua conta está bloqueada por falta de atualização cadastral. Deseja realizar a atualização agora?" - }, - { - "id": "6.3", - "ator": "Cliente", - "fala": "Sim, quero desbloquear minha conta." - }, - { - "id": "6.4", - "ator": "IA", - "fala": "Atualização concluída. Sua conta foi desbloqueada e está ativa." - }, - { - "id": "6.5", - "ator": "Cliente", - "fala": "Obrigado! Agora posso acessar minha conta normalmente." - }, - { - "id": "7.1", - "ator": "Cliente", - "fala": "Preciso trocar minha senha. Não lembro mais a atual." - }, - { - "id": "7.2", - "ator": "Gerente", - "fala": "Olá! Vamos iniciar o processo para troca de senha agora." - }, - { - "id": "7.3", - "ator": "Cliente", - "fala": "Por favor, me ajude com as instruções para gerar uma nova senha." - }, - { - "id": "7.4", - "ator": "Gerente", - "fala": "As instruções para a troca de senha estão na tela. Siga os passos indicados." - }, - { - "id": "7.5", - "ator": "Cliente", - "fala": "Já consegui trocar a senha. Obrigado pela ajuda!" - }, - { - "id": "8.1", - "ator": "IA", - "fala": "Você já acessou o aplicativo Caixa Tem hoje? Ele pode facilitar seu acesso aos serviços bancários." - }, - { - "id": "8.2", - "ator": "Cliente", - "fala": "Não, ainda não. Preciso de ajuda para instalar o aplicativo." - }, - { - "id": "8.3", - "ator": "IA", - "fala": "Vamos começar a instalação do aplicativo Caixa Tem. Está pronto para iniciar?" - }, - { - "id": "8.4", - "ator": "Cliente", - "fala": "Sim, por favor. Quero instalar o aplicativo agora." - }, - { - "id": "8.5", - "ator": "IA", - "fala": "Aplicativo instalado com sucesso. Você pode acessar seu saldo e benefícios diretamente pelo celular." - }, - { - "id": "9.1", - "ator": "Cliente", - "fala": "Meu benefício do Bolsa Família está bloqueado. Como faço para desbloqueá-lo?" - }, - { - "id": "9.2", - "ator": "IA", - "fala": "Seu benefício está bloqueado por falta de atualização cadastral. Deseja realizar a atualização?" - }, - { - "id": "9.3", - "ator": "Cliente", - "fala": "Sim, quero atualizar meu cadastro para desbloquear o benefício." - }, - { - "id": "9.4", - "ator": "IA", - "fala": "Atualização concluída. Seu benefício já está disponível para saque." - }, - { - "id": "9.5", - "ator": "Cliente", - "fala": "Obrigado! Isso resolve meu problema." - }, - { - "id": "10.1", - "ator": "IA", - "fala": "O Auxílio Gás será pago na próxima semana. Deseja verificar o valor ou a data de pagamento?" - }, - { - "id": "10.2", - "ator": "Cliente", - "fala": "Sim, quero confirmar o valor e a data de pagamento." - }, - { - "id": "10.3", - "ator": "IA", - "fala": "O valor do Auxílio Gás será de R$ 120,00 e estará disponível para saque a partir do dia 15 deste mês." - }, - { - "id": "10.4", - "ator": "Cliente", - "fala": "Obrigado por confirmar! Isso me ajuda bastante." - }, - { - "id": "10.5", - "ator": "IA", - "fala": "Por nada! Caso precise de mais informações, estou à disposição." - }, - { - "id": "11.1", - "ator": "Cliente", - "fala": "Olá, estou com dúvidas sobre como abrir uma conta poupança. Pode me ajudar?" - }, - { - "id": "11.2", - "ator": "Atendente", - "fala": "Claro! Você deseja abrir uma conta poupança agora?" - }, - { - "id": "11.3", - "ator": "Cliente", - "fala": "Sim, quero saber como funciona o processo." - }, - { - "id": "11.4", - "ator": "Atendente", - "fala": "Por favor, insira seus documentos no leitor do terminal para iniciar a abertura da conta poupança." - }, - { - "id": "11.5", - "ator": "Cliente", - "fala": "Ok, estou enviando os documentos agora." - }, - { - "id": "11.6", - "ator": "IA", - "fala": "Os documentos foram recebidos. Sua conta poupança será aberta em instantes." - }, - { - "id": "11.7", - "ator": "Cliente", - "fala": "Obrigado! Preciso de mais alguma coisa para finalizar?" - }, - { - "id": "11.8", - "ator": "IA", - "fala": "Não, está tudo certo. Sua conta poupança foi aberta com sucesso!" - }, - { - "id": "12.1", - "ator": "Cliente", - "fala": "Minha senha precisa ser trocada, mas estou com dúvidas. Pode me ajudar?" - }, - { - "id": "12.2", - "ator": "Gerente", - "fala": "Claro! Vamos iniciar o processo para troca de senha." - }, - { - "id": "12.3", - "ator": "Cliente", - "fala": "Obrigado! Quero entender como funciona a troca de senha." - }, - { - "id": "12.4", - "ator": "Gerente", - "fala": "Você receberá as instruções diretamente na tela. Basta seguir os passos para concluir a troca." - }, - { - "id": "12.5", - "ator": "Cliente", - "fala": "Ok, já finalizei a troca. Muito obrigado!" - }, - { - "id": "12.6", - "ator": "Gerente", - "fala": "Por nada! Se precisar de mais ajuda, estarei por aqui." - }, - { - "id": "13.1", - "ator": "Cliente", - "fala": "Perdi meu cartão e preciso resolver isso rápido. O que posso fazer?" - }, - { - "id": "13.2", - "ator": "IA", - "fala": "Entendido. Você deseja solicitar um novo cartão agora?" - }, - { - "id": "13.3", - "ator": "Cliente", - "fala": "Sim, por favor. Quero pedir um novo cartão." - }, - { - "id": "13.4", - "ator": "IA", - "fala": "Novo cartão solicitado com sucesso. O comprovante será enviado por SMS." - }, - { - "id": "13.5", - "ator": "Cliente", - "fala": "Muito obrigado! Isso resolve meu problema." - }, - { - "id": "14.1", - "ator": "Cliente", - "fala": "Quero saber se meus dados estão atualizados corretamente." - }, - { - "id": "14.2", - "ator": "IA", - "fala": "Verifiquei que seus dados estão atualizados. Precisa de mais alguma coisa?" - }, - { - "id": "14.3", - "ator": "Cliente", - "fala": "Ótimo! Posso consultar meus benefícios também?" - }, - { - "id": "14.4", - "ator": "IA", - "fala": "Sim, seus benefícios estão disponíveis e já foram depositados." - }, - { - "id": "14.5", - "ator": "Cliente", - "fala": "Obrigado pela confirmação!" - }, - { - "id": "15.1", - "ator": "Cliente", - "fala": "Minha conta está ativa, mas quero garantir que não há problemas. Pode verificar?" - }, - { - "id": "15.2", - "ator": "IA", - "fala": "Verifiquei que sua conta está ativa e funcionando normalmente. Tudo certo!" - }, - { - "id": "15.3", - "ator": "Cliente", - "fala": "Ótimo! Posso ver meu saldo também?" - }, - { - "id": "15.4", - "ator": "IA", - "fala": "Seu saldo atual é de R$ 3.200,00. Precisa de mais alguma coisa?" - }, - { - "id": "15.5", - "ator": "Cliente", - "fala": "Não, isso já resolve. Obrigado!" - }, - { - "id": "16.1", - "ator": "Cliente", - "fala": "Quero atualizar meu endereço. Como faço isso?" - }, - { - "id": "16.2", - "ator": "IA", - "fala": "Por favor, insira os documentos necessários no leitor do terminal para atualizar seu endereço." - }, - { - "id": "16.3", - "ator": "Cliente", - "fala": "Ok, estou enviando os documentos agora." - }, - { - "id": "16.4", - "ator": "IA", - "fala": "Atualização concluída. Seu endereço foi alterado com sucesso." - }, - { - "id": "16.5", - "ator": "Cliente", - "fala": "Obrigado! Isso resolve meu problema." - }, - { - "id": "17.1", - "ator": "Cliente", - "fala": "Quero saber o valor do meu benefício do Bolsa Família. Pode verificar?" - }, - { - "id": "17.2", - "ator": "IA", - "fala": "Seu benefício do Bolsa Família já foi depositado e o valor é de R$ 600,00." - }, - { - "id": "17.3", - "ator": "Cliente", - "fala": "Ótimo! Posso sacar o benefício agora?" - }, - { - "id": "17.4", - "ator": "IA", - "fala": "Sim, o benefício está liberado para saque. Precisa de mais alguma coisa?" - }, - { - "id": "17.5", - "ator": "Cliente", - "fala": "Não, obrigado pela ajuda!" - }, - { - "id": "18.1", - "ator": "Cliente", - "fala": "Quero imprimir o extrato da minha conta. É possível?" - }, - { - "id": "18.2", - "ator": "IA", - "fala": "Sim, o extrato está disponível na tela. Deseja imprimir agora?" - }, - { - "id": "18.3", - "ator": "Cliente", - "fala": "Sim, por favor. Quero o extrato impresso." - }, - { - "id": "18.4", - "ator": "IA", - "fala": "Extrato impresso com sucesso. Precisa de mais alguma coisa?" - }, - { - "id": "18.5", - "ator": "Cliente", - "fala": "Não, isso já resolve. Obrigado!" - }, - { - "id": "19.1", - "ator": "Cliente", - "fala": "Minha conta foi bloqueada por segurança. Como faço para desbloqueá-la?" - }, - { - "id": "19.2", - "ator": "IA", - "fala": "Sua conta foi bloqueada por falta de atualização cadastral. Deseja realizar a atualização agora?" - }, - { - "id": "19.3", - "ator": "Cliente", - "fala": "Sim, quero desbloquear minha conta." - }, - { - "id": "19.4", - "ator": "IA", - "fala": "Atualização concluída. Sua conta foi desbloqueada e está ativa." - }, - { - "id": "19.5", - "ator": "Cliente", - "fala": "Obrigado! Agora posso acessar minha conta normalmente." +[ + { + "id": "1.1", + "ator": "Atendente", + "fala": "Olá! Estou aqui para te ajudar com seu atendimento bancário. Como posso ajudar?" + }, + { + "id": "1.2", + "ator": "Cliente", + "fala": "Quero acessar minha conta e verificar meu saldo." + }, + { + "id": "1.3", + "ator": "Atendente", + "fala": "Entendido! Por favor, vá até o terminal de autoatendimento para iniciar o processo." + }, + { + "id": "2.1", + "ator": "IA", + "fala": "Por favor, coloque seu dedo no leitor biométrico para identificarmos sua conta." + }, + { + "id": "2.2", + "ator": "Cliente", + "fala": "Tudo bem, já fiz isso." + }, + { + "id": "2.3", + "ator": "IA", + "fala": "Identificação concluída. Sua conta está ativa e funcionando normalmente." + }, + { + "id": "2.4", + "ator": "IA", + "fala": "Deseja consultar seu saldo ou acessar outro serviço?" + }, + { + "id": "2.5", + "ator": "Cliente", + "fala": "Quero ver meu saldo e saber se há algum problema com minha conta." + }, + { + "id": "3.1", + "ator": "IA", + "fala": "Seu saldo atual é de R$ 2.000,00. Não há problemas com sua conta." + }, + { + "id": "3.2", + "ator": "Cliente", + "fala": "Ótimo! Posso ver os depósitos dos últimos três meses?" + }, + { + "id": "3.3", + "ator": "IA", + "fala": "Sim, os depósitos estão disponíveis na tela. Deseja imprimir o extrato?" + }, + { + "id": "3.4", + "ator": "Cliente", + "fala": "Sim, quero imprimir o extrato." + }, + { + "id": "3.5", + "ator": "IA", + "fala": "Extrato impresso. Precisa de mais alguma coisa?" + }, + { + "id": "4.1", + "ator": "Cliente", + "fala": "Meu cartão está com problema. Como faço para resolver isso?" + }, + { + "id": "4.2", + "ator": "IA", + "fala": "Seu cartão foi bloqueado. Deseja solicitar um novo cartão?" + }, + { + "id": "4.3", + "ator": "Cliente", + "fala": "Sim, quero pedir um novo cartão." + }, + { + "id": "4.4", + "ator": "IA", + "fala": "Novo cartão solicitado com sucesso. O comprovante será enviado por SMS." + }, + { + "id": "4.5", + "ator": "Cliente", + "fala": "Obrigado! Isso resolve meu problema." + }, + { + "id": "5.1", + "ator": "IA", + "fala": "Seu cadastro está incompleto. Precisamos atualizá-lo." + }, + { + "id": "5.2", + "ator": "Cliente", + "fala": "Ok, quero atualizar meus dados agora." + }, + { + "id": "5.3", + "ator": "IA", + "fala": "Por favor, insira os documentos necessários no leitor do terminal." + }, + { + "id": "5.4", + "ator": "Cliente", + "fala": "Já enviei os documentos. O que mais preciso fazer?" + }, + { + "id": "5.5", + "ator": "IA", + "fala": "Cadastro atualizado com sucesso. Precisa de mais alguma coisa?" + }, + { + "id": "6.1", + "ator": "Cliente", + "fala": "Minha conta está bloqueada. Como faço para desbloqueá-la?" + }, + { + "id": "6.2", + "ator": "IA", + "fala": "Sua conta está bloqueada por falta de atualização cadastral. Deseja realizar a atualização agora?" + }, + { + "id": "6.3", + "ator": "Cliente", + "fala": "Sim, quero desbloquear minha conta." + }, + { + "id": "6.4", + "ator": "IA", + "fala": "Atualização concluída. Sua conta foi desbloqueada e está ativa." + }, + { + "id": "6.5", + "ator": "Cliente", + "fala": "Obrigado! Agora posso acessar minha conta normalmente." + }, + { + "id": "7.1", + "ator": "Cliente", + "fala": "Preciso trocar minha senha. Não lembro mais a atual." + }, + { + "id": "7.2", + "ator": "Gerente", + "fala": "Olá! Vamos iniciar o processo para troca de senha agora." + }, + { + "id": "7.3", + "ator": "Cliente", + "fala": "Por favor, me ajude com as instruções para gerar uma nova senha." + }, + { + "id": "7.4", + "ator": "Gerente", + "fala": "As instruções para a troca de senha estão na tela. Siga os passos indicados." + }, + { + "id": "7.5", + "ator": "Cliente", + "fala": "Já consegui trocar a senha. Obrigado pela ajuda!" + }, + { + "id": "8.1", + "ator": "IA", + "fala": "Você já acessou o aplicativo Caixa Tem hoje? Ele pode facilitar seu acesso aos serviços bancários." + }, + { + "id": "8.2", + "ator": "Cliente", + "fala": "Não, ainda não. Preciso de ajuda para instalar o aplicativo." + }, + { + "id": "8.3", + "ator": "IA", + "fala": "Vamos começar a instalação do aplicativo Caixa Tem. Está pronto para iniciar?" + }, + { + "id": "8.4", + "ator": "Cliente", + "fala": "Sim, por favor. Quero instalar o aplicativo agora." + }, + { + "id": "8.5", + "ator": "IA", + "fala": "Aplicativo instalado com sucesso. Você pode acessar seu saldo e benefícios diretamente pelo celular." + }, + { + "id": "9.1", + "ator": "Cliente", + "fala": "Meu benefício do Bolsa Família está bloqueado. Como faço para desbloqueá-lo?" + }, + { + "id": "9.2", + "ator": "IA", + "fala": "Seu benefício está bloqueado por falta de atualização cadastral. Deseja realizar a atualização?" + }, + { + "id": "9.3", + "ator": "Cliente", + "fala": "Sim, quero atualizar meu cadastro para desbloquear o benefício." + }, + { + "id": "9.4", + "ator": "IA", + "fala": "Atualização concluída. Seu benefício já está disponível para saque." + }, + { + "id": "9.5", + "ator": "Cliente", + "fala": "Obrigado! Isso resolve meu problema." + }, + { + "id": "10.1", + "ator": "IA", + "fala": "O Auxílio Gás será pago na próxima semana. Deseja verificar o valor ou a data de pagamento?" + }, + { + "id": "10.2", + "ator": "Cliente", + "fala": "Sim, quero confirmar o valor e a data de pagamento." + }, + { + "id": "10.3", + "ator": "IA", + "fala": "O valor do Auxílio Gás será de R$ 120,00 e estará disponível para saque a partir do dia 15 deste mês." + }, + { + "id": "10.4", + "ator": "Cliente", + "fala": "Obrigado por confirmar! Isso me ajuda bastante." + }, + { + "id": "10.5", + "ator": "IA", + "fala": "Por nada! Caso precise de mais informações, estou à disposição." + }, + { + "id": "11.1", + "ator": "Cliente", + "fala": "Olá, estou com dúvidas sobre como abrir uma conta poupança. Pode me ajudar?" + }, + { + "id": "11.2", + "ator": "Atendente", + "fala": "Claro! Você deseja abrir uma conta poupança agora?" + }, + { + "id": "11.3", + "ator": "Cliente", + "fala": "Sim, quero saber como funciona o processo." + }, + { + "id": "11.4", + "ator": "Atendente", + "fala": "Por favor, insira seus documentos no leitor do terminal para iniciar a abertura da conta poupança." + }, + { + "id": "11.5", + "ator": "Cliente", + "fala": "Ok, estou enviando os documentos agora." + }, + { + "id": "11.6", + "ator": "IA", + "fala": "Os documentos foram recebidos. Sua conta poupança será aberta em instantes." + }, + { + "id": "11.7", + "ator": "Cliente", + "fala": "Obrigado! Preciso de mais alguma coisa para finalizar?" + }, + { + "id": "11.8", + "ator": "IA", + "fala": "Não, está tudo certo. Sua conta poupança foi aberta com sucesso!" + }, + { + "id": "12.1", + "ator": "Cliente", + "fala": "Minha senha precisa ser trocada, mas estou com dúvidas. Pode me ajudar?" + }, + { + "id": "12.2", + "ator": "Gerente", + "fala": "Claro! Vamos iniciar o processo para troca de senha." + }, + { + "id": "12.3", + "ator": "Cliente", + "fala": "Obrigado! Quero entender como funciona a troca de senha." + }, + { + "id": "12.4", + "ator": "Gerente", + "fala": "Você receberá as instruções diretamente na tela. Basta seguir os passos para concluir a troca." + }, + { + "id": "12.5", + "ator": "Cliente", + "fala": "Ok, já finalizei a troca. Muito obrigado!" + }, + { + "id": "12.6", + "ator": "Gerente", + "fala": "Por nada! Se precisar de mais ajuda, estarei por aqui." + }, + { + "id": "13.1", + "ator": "Cliente", + "fala": "Perdi meu cartão e preciso resolver isso rápido. O que posso fazer?" + }, + { + "id": "13.2", + "ator": "IA", + "fala": "Entendido. Você deseja solicitar um novo cartão agora?" + }, + { + "id": "13.3", + "ator": "Cliente", + "fala": "Sim, por favor. Quero pedir um novo cartão." + }, + { + "id": "13.4", + "ator": "IA", + "fala": "Novo cartão solicitado com sucesso. O comprovante será enviado por SMS." + }, + { + "id": "13.5", + "ator": "Cliente", + "fala": "Muito obrigado! Isso resolve meu problema." + }, + { + "id": "14.1", + "ator": "Cliente", + "fala": "Quero saber se meus dados estão atualizados corretamente." + }, + { + "id": "14.2", + "ator": "IA", + "fala": "Verifiquei que seus dados estão atualizados. Precisa de mais alguma coisa?" + }, + { + "id": "14.3", + "ator": "Cliente", + "fala": "Ótimo! Posso consultar meus benefícios também?" + }, + { + "id": "14.4", + "ator": "IA", + "fala": "Sim, seus benefícios estão disponíveis e já foram depositados." + }, + { + "id": "14.5", + "ator": "Cliente", + "fala": "Obrigado pela confirmação!" + }, + { + "id": "15.1", + "ator": "Cliente", + "fala": "Minha conta está ativa, mas quero garantir que não há problemas. Pode verificar?" + }, + { + "id": "15.2", + "ator": "IA", + "fala": "Verifiquei que sua conta está ativa e funcionando normalmente. Tudo certo!" + }, + { + "id": "15.3", + "ator": "Cliente", + "fala": "Ótimo! Posso ver meu saldo também?" + }, + { + "id": "15.4", + "ator": "IA", + "fala": "Seu saldo atual é de R$ 3.200,00. Precisa de mais alguma coisa?" + }, + { + "id": "15.5", + "ator": "Cliente", + "fala": "Não, isso já resolve. Obrigado!" + }, + { + "id": "16.1", + "ator": "Cliente", + "fala": "Quero atualizar meu endereço. Como faço isso?" + }, + { + "id": "16.2", + "ator": "IA", + "fala": "Por favor, insira os documentos necessários no leitor do terminal para atualizar seu endereço." + }, + { + "id": "16.3", + "ator": "Cliente", + "fala": "Ok, estou enviando os documentos agora." + }, + { + "id": "16.4", + "ator": "IA", + "fala": "Atualização concluída. Seu endereço foi alterado com sucesso." + }, + { + "id": "16.5", + "ator": "Cliente", + "fala": "Obrigado! Isso resolve meu problema." + }, + { + "id": "17.1", + "ator": "Cliente", + "fala": "Quero saber o valor do meu benefício do Bolsa Família. Pode verificar?" + }, + { + "id": "17.2", + "ator": "IA", + "fala": "Seu benefício do Bolsa Família já foi depositado e o valor é de R$ 600,00." + }, + { + "id": "17.3", + "ator": "Cliente", + "fala": "Ótimo! Posso sacar o benefício agora?" + }, + { + "id": "17.4", + "ator": "IA", + "fala": "Sim, o benefício está liberado para saque. Precisa de mais alguma coisa?" + }, + { + "id": "17.5", + "ator": "Cliente", + "fala": "Não, obrigado pela ajuda!" + }, + { + "id": "18.1", + "ator": "Cliente", + "fala": "Quero imprimir o extrato da minha conta. É possível?" + }, + { + "id": "18.2", + "ator": "IA", + "fala": "Sim, o extrato está disponível na tela. Deseja imprimir agora?" + }, + { + "id": "18.3", + "ator": "Cliente", + "fala": "Sim, por favor. Quero o extrato impresso." + }, + { + "id": "18.4", + "ator": "IA", + "fala": "Extrato impresso com sucesso. Precisa de mais alguma coisa?" + }, + { + "id": "18.5", + "ator": "Cliente", + "fala": "Não, isso já resolve. Obrigado!" + }, + { + "id": "19.1", + "ator": "Cliente", + "fala": "Minha conta foi bloqueada por segurança. Como faço para desbloqueá-la?" + }, + { + "id": "19.2", + "ator": "IA", + "fala": "Sua conta foi bloqueada por falta de atualização cadastral. Deseja realizar a atualização agora?" + }, + { + "id": "19.3", + "ator": "Cliente", + "fala": "Sim, quero desbloquear minha conta." + }, + { + "id": "19.4", + "ator": "IA", + "fala": "Atualização concluída. Sua conta foi desbloqueada e está ativa." + }, + { + "id": "19.5", + "ator": "Cliente", + "fala": "Obrigado! Agora posso acessar minha conta normalmente." } -] \ No newline at end of file +] diff --git a/apps/text_generation/src/domain/cosmos_store.py b/apps/text_generation/src/domain/cosmos_store.py index c01219f..995e2e5 100644 --- a/apps/text_generation/src/domain/cosmos_store.py +++ b/apps/text_generation/src/domain/cosmos_store.py @@ -5,8 +5,8 @@ from dataclasses import dataclass, field from typing import Any, Optional, Type -from src.config.settings import CosmosSettings from src import logger +from src.config.settings import CosmosSettings class CosmosPersistenceError(RuntimeError): @@ -30,16 +30,28 @@ async def _get_container(self): async with self._lock: if self._container is not None: return self._container - if not self.settings.endpoint or not self.settings.database_name or not self.settings.container_name: + if ( + not self.settings.endpoint + or not self.settings.database_name + or not self.settings.container_name + ): raise CosmosPersistenceError("Cosmos DB configuration is incomplete.") CosmosClient, cosmos_error_cls = self._import_cosmos_sdk() credential = self.credential or self._resolve_default_credential() - self._client = CosmosClient(url=self.settings.endpoint, credential=credential) + self._client = CosmosClient( + url=self.settings.endpoint, credential=credential + ) self._cosmos_error_cls = cosmos_error_cls - database_client = self._client.get_database_client(self.settings.database_name) - self._container = database_client.get_container_client(self.settings.container_name) + database_client = self._client.get_database_client( + self.settings.database_name + ) + self._container = database_client.get_container_client( + self.settings.container_name + ) logger.debug( - "Initialized Cosmos DB container '%s/%s'", self.settings.database_name, self.settings.container_name + "Initialized Cosmos DB container '%s/%s'", + self.settings.database_name, + self.settings.container_name, ) return self._container @@ -66,7 +78,8 @@ def _resolve_default_credential(self) -> Any: # prompts are disabled so long-running ingest jobs do not stall waiting # for user input. credential = DefaultAzureCredential( - exclude_interactive_browser_credential=True, exclude_cached_token_credential=True + exclude_interactive_browser_credential=True, + exclude_cached_token_credential=True, ) logger.debug( "Initialized DefaultAzureCredential for Cosmos access (CLI context preferred)" @@ -76,13 +89,17 @@ def _resolve_default_credential(self) -> Any: async def save_document(self, document: dict[str, Any]) -> str: container: Any = await self._get_container() payload = dict(document) - payload.setdefault("id", document.get("id") or document.get("document_id") or uuid.uuid4().hex) + payload.setdefault( + "id", document.get("id") or document.get("document_id") or uuid.uuid4().hex + ) try: await container.upsert_item(payload) except Exception as exc: # pragma: no cover - network dependent if self._cosmos_error_cls and isinstance(exc, self._cosmos_error_cls): logger.error("Failed to persist ingest snapshot in Cosmos DB: %s", exc) - raise CosmosPersistenceError("Failed to persist ingest snapshot") from exc + raise CosmosPersistenceError( + "Failed to persist ingest snapshot" + ) from exc raise return payload["id"] @@ -92,22 +109,28 @@ async def load_document(self, document_id: str) -> Optional[dict[str, Any]]: partition_key_value = document_id if self.settings.partition_key: # If partition key path is provided (e.g., "/document_path"), best effort query. - if self.settings.partition_key.strip('/').lower() == 'id': + if self.settings.partition_key.strip("/").lower() == "id": partition_key_value = document_id else: partition_key_value = None if partition_key_value is not None: try: - item = await container.read_item(item=document_id, partition_key=partition_key_value) + item = await container.read_item( + item=document_id, partition_key=partition_key_value + ) return item except Exception: # Fallback to query below pass query = "SELECT * FROM c WHERE c.id = @id" - results = container.read_all_items() if not hasattr(container, "query_items") else container.query_items( - query=query, - parameters=[{"name": "@id", "value": document_id}], - enable_cross_partition_query=True, + results = ( + container.read_all_items() + if not hasattr(container, "query_items") + else container.query_items( + query=query, + parameters=[{"name": "@id", "value": document_id}], + enable_cross_partition_query=True, + ) ) if hasattr(results, "__aiter__"): async for item in results: # type: ignore[attr-defined] @@ -117,9 +140,17 @@ async def load_document(self, document_id: str) -> Optional[dict[str, Any]]: return item except Exception as exc: # pragma: no cover - network dependent if self._cosmos_error_cls and isinstance(exc, self._cosmos_error_cls): - logger.warning("Failed to load ingest snapshot %s from Cosmos DB: %s", document_id, exc) + logger.warning( + "Failed to load ingest snapshot %s from Cosmos DB: %s", + document_id, + exc, + ) else: - logger.debug("Non-Cosmos error while loading ingest snapshot %s: %s", document_id, exc) + logger.debug( + "Non-Cosmos error while loading ingest snapshot %s: %s", + document_id, + exc, + ) return None async def close(self) -> None: @@ -134,16 +165,24 @@ async def close(self) -> None: self._cosmos_error_cls = None -def make_cosmos_store(settings: Optional[CosmosSettings] = None) -> Optional[CosmosDocumentStore]: +def make_cosmos_store( + settings: Optional[CosmosSettings] = None, +) -> Optional[CosmosDocumentStore]: """Factory that builds a CosmosDocumentStore when configuration is present.""" cosmos_settings = settings or CosmosSettings() - if not cosmos_settings.endpoint or not cosmos_settings.database_name or not cosmos_settings.container_name: + if ( + not cosmos_settings.endpoint + or not cosmos_settings.database_name + or not cosmos_settings.container_name + ): logger.warning("Cosmos DB settings incomplete; skipping store creation") return None try: # pragma: no cover - optional dependency __import__("azure.cosmos.aio") except ImportError: - logger.warning("azure-cosmos package not available; skipping Cosmos store creation") + logger.warning( + "azure-cosmos package not available; skipping Cosmos store creation" + ) return None return CosmosDocumentStore(settings=cosmos_settings) diff --git a/apps/text_generation/src/domain/models.py b/apps/text_generation/src/domain/models.py index c3d319f..86db43e 100644 --- a/apps/text_generation/src/domain/models.py +++ b/apps/text_generation/src/domain/models.py @@ -1,4 +1,5 @@ from __future__ import annotations + from typing import Any from pydantic import BaseModel, Field @@ -29,7 +30,9 @@ class Document(BaseModel): file_name: str = Field(..., description="Original file name") content: str = Field(..., description="Full extracted text content") page_count: int | None = Field(None, description="Total page count, if available") - metadata: dict[str, Any] = Field(default_factory=dict, description="Arbitrary metadata map") + metadata: dict[str, Any] = Field( + default_factory=dict, description="Arbitrary metadata map" + ) class Chunk(BaseModel): @@ -49,7 +52,9 @@ class Chunk(BaseModel): page_start: int = Field(..., ge=1, description="Starting page number (1-based)") page_end: int = Field(..., ge=1, description="Ending page number (inclusive)") text: str = Field(..., description="Chunk textual content") - explanation: str | None = Field(None, description="Optional explanation or annotation") + explanation: str | None = Field( + None, description="Optional explanation or annotation" + ) @property def page_span(self) -> tuple[int, int]: # convenience helper @@ -62,14 +67,30 @@ class AggregatedChunk(BaseModel): chunk_id: str = Field(..., description="Stable identifier (uuid4 hex string)") chunk_topic: list[str] = Field(..., description="Instructional taxonomy labels") - chunk_files: list[str] = Field(..., description="Source files kept in the aggregation") - chunk_summary: str = Field(..., description="Declarative summary of the instruction") - chunk_content: str = Field(..., description="Instructional narrative for the language") - language: str = Field(..., description="Language identifier to keep rules separated") + chunk_files: list[str] = Field( + ..., description="Source files kept in the aggregation" + ) + chunk_summary: str = Field( + ..., description="Declarative summary of the instruction" + ) + chunk_content: str = Field( + ..., description="Instructional narrative for the language" + ) + language: str = Field( + ..., description="Language identifier to keep rules separated" + ) def ensure_topic_invariants(self) -> None: """Normalize topic labels to uppercase tokens from the allowed taxonomy.""" - allowed = {"SINTAXE", "SEMANTICA", "ORTOGRAFIA", "MORFOLOGIA", "MORFOSINTAXE", "LEXICO", "LEXICOGRAFICA"} + allowed = { + "SINTAXE", + "SEMANTICA", + "ORTOGRAFIA", + "MORFOLOGIA", + "MORFOSINTAXE", + "LEXICO", + "LEXICOGRAFICA", + } normalized: list[str] = [] for label in self.chunk_topic: token = label.strip().upper() @@ -92,7 +113,9 @@ def to_cosmos_document(self) -> dict[str, Any]: "language": self.language, } - def to_search_document(self, embedding: list[float], index_name: str) -> dict[str, Any]: + def to_search_document( + self, embedding: list[float], index_name: str + ) -> dict[str, Any]: """Serialize to Azure AI Search document format.""" self.ensure_topic_invariants() return { @@ -121,7 +144,9 @@ class IndexRecord(BaseModel): chunk_id: str = Field(..., description="Associated chunk id") vector: list[float] | None = Field(None, description="Embedding vector") text: str = Field(..., description="Text content stored with the vector") - metadata: dict[str, Any] = Field(default_factory=dict, description="Additional metadata") + metadata: dict[str, Any] = Field( + default_factory=dict, description="Additional metadata" + ) class DialogueLine(BaseModel): @@ -153,9 +178,15 @@ class EvaluationNote(BaseModel): id: str = Field(..., description="Evaluation identifier") status: str = Field(..., description="Evaluation status or category") comment: str = Field(..., description="Reviewer comment") - adjustments: list[str] = Field(default_factory=list, description="Suggested adjustments") - consistencia_fonte: int | None = Field(None, description="Consistency score (optional)") - confianca_fonetica: int | None = Field(None, description="Phonetic confidence score (optional)") + adjustments: list[str] = Field( + default_factory=list, description="Suggested adjustments" + ) + consistencia_fonte: int | None = Field( + None, description="Consistency score (optional)" + ) + confianca_fonetica: int | None = Field( + None, description="Phonetic confidence score (optional)" + ) class EvaluationReport(BaseModel): @@ -171,7 +202,9 @@ class EvaluationReport(BaseModel): notes: list[EvaluationNote] = Field(..., description="List of evaluation notes") recommended_action: str = Field(..., description="Recommended next action") global_score: int = Field(..., description="Aggregate numeric score") - risks: list[str] = Field(default_factory=list, description="List of identified risks") + risks: list[str] = Field( + default_factory=list, description="List of identified risks" + ) @property def average_scores(self) -> dict[str, float]: # helper for quick analytics @@ -180,7 +213,10 @@ def average_scores(self) -> dict[str, float]: # helper for quick analytics Returns: A mapping of metric name to average value (only metrics with >=1 numeric value). """ - metrics: dict[str, list[int]] = {"consistencia_fonte": [], "confianca_fonetica": []} + metrics: dict[str, list[int]] = { + "consistencia_fonte": [], + "confianca_fonetica": [], + } for note in self.notes: if note.consistencia_fonte is not None: metrics["consistencia_fonte"].append(note.consistencia_fonte) @@ -191,4 +227,3 @@ def average_scores(self) -> dict[str, float]: # helper for quick analytics for name, values in metrics.items() if values } - diff --git a/apps/text_generation/src/domain/prompts.py b/apps/text_generation/src/domain/prompts.py index 4441b50..aa9e256 100644 --- a/apps/text_generation/src/domain/prompts.py +++ b/apps/text_generation/src/domain/prompts.py @@ -7,12 +7,13 @@ import sys from pathlib import Path + from jinja2 import Template base = Path(__file__).parent / "data" / "translation" ROOT = Path(__file__).resolve().parents[4] -SRC = ROOT / 'src/backend' -base = ROOT / 'notebooks/data/translation' +SRC = ROOT / "src/backend" +base = ROOT / "notebooks/data/translation" sys.path.append(str(SRC)) @@ -110,7 +111,7 @@ def render_prompt(template_str, **kwargs): "{{ idioma }}": " "fontes_{{ idioma }}": "", "sem_fonte": , - "observacao": "(opcional) breve nota se houver lacunas ou adaptações" + "observacao": "(opcional) breve nota se houver lacunas ou adaptações" } ] @@ -204,11 +205,11 @@ def render_prompt(template_str, **kwargs): " - Criar `aggregated_topic` curto coerente.\n\n" "FASE 5 – SAÍDA ESTRUTURADA\n" "Retorne JSON P U R O com: {\n" - " \"pages\": [ {page_number, page_summary, page_full_content, keep} ... ],\n" - " \"initial_chunks\": [ {\n" + ' "pages": [ {page_number, page_summary, page_full_content, keep} ... ],\n' + ' "initial_chunks": [ {\n' " chunk_id, start_page, end_page, chunk_summary, language_concept, topic, discarded, needs_embedding\n" " } ],\n" - " \"similarity_groups\": [ {\n" + ' "similarity_groups": [ {\n' " group_id, members, aggregated_summary, aggregated_topic, language_concept\n" " } ]\n" "}\n" diff --git a/apps/text_generation/src/domain/utils.py b/apps/text_generation/src/domain/utils.py index 489a5ea..d8c9df3 100644 --- a/apps/text_generation/src/domain/utils.py +++ b/apps/text_generation/src/domain/utils.py @@ -14,12 +14,12 @@ def _timed(stage: str, collector: dict[str, float]): def get_reference_pdfs(data_dir: Path) -> list[str]: - return [p.name for p in data_dir.glob('*.pdf')] + return [p.name for p in data_dir.glob("*.pdf")] def derive_languages_from_filenames(pdf_files: list[str]) -> list[str]: langs = set() for fn in pdf_files: - lang = fn.split('_')[0] + lang = fn.split("_")[0] langs.add(lang.capitalize()) return sorted(langs) diff --git a/apps/text_generation/src/ingest_data/chunking.py b/apps/text_generation/src/ingest_data/chunking.py index a77ace6..eb119d1 100644 --- a/apps/text_generation/src/ingest_data/chunking.py +++ b/apps/text_generation/src/ingest_data/chunking.py @@ -10,6 +10,9 @@ from typing import Iterable, List import semantic_kernel as sk +from azure.core.exceptions import HttpResponseError +from azure.identity import DefaultAzureCredential +from pydantic import Field, ValidationError from semantic_kernel.agents import AgentThread, ChatCompletionAgent from semantic_kernel.connectors.ai import FunctionChoiceBehavior from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion @@ -17,11 +20,6 @@ from semantic_kernel.functions.kernel_arguments import KernelArguments from semantic_kernel.kernel_pydantic import KernelBaseModel -from pydantic import Field, ValidationError - -from azure.core.exceptions import HttpResponseError -from azure.identity import DefaultAzureCredential - from src.config.settings import AppSettings from src.domain.models import AggregatedChunk as DomainAggregatedChunk from src.ingest_data.document_reader import PageSlice @@ -40,25 +38,25 @@ "1. Leia páginas sequenciais de um PDF.\n" "2. Leve em consideração chunks existentes (mesmo chunkId e resumo).\n" "3. Reaproveite chunks quando houver continuidade lógica explícita; caso contrário crie um novo chunk.\n" - "4. Cada chunk deve ser didático, explicar \"como\" realizar a instrução na língua, e trazer apenas conteúdo sequencial coerente.\n" + '4. Cada chunk deve ser didático, explicar "como" realizar a instrução na língua, e trazer apenas conteúdo sequencial coerente.\n' "5. Use apenas as categorias: SINTAXE, SEMANTICA, ORTOGRAFIA, MORFOLOGIA, MORFOSINTAXE, LEXICO, LEXICOGRÁFICO.\n" "6. Quando o conteúdo apresentar versões bilíngues, seu sumário deve apresentar os pares de palavras nas duas línguas. Caso a segunda língua não seja português, traduza-a para o português. Neste caso, a classificação é 'LEXICO'.\n" "7. Sempre gere JSON puro conforme o formato informado.\n" "Formato:\n" "{\n" - " \"chunks\": [\n" + ' "chunks": [\n' " {\n" - " \"chunkId\": \"uuid4\",\n" - " \"chunkTopic\": [\"SINTAXE\"],\n" - " \"chunkFiles\": [\"arquivo.pdf\"],\n" - " \"chunkSummary\": \"resumo instrucional\",\n" - " \"chunkContent\": \"texto instrutivo\"\n" + ' "chunkId": "uuid4",\n' + ' "chunkTopic": ["SINTAXE"],\n' + ' "chunkFiles": ["arquivo.pdf"],\n' + ' "chunkSummary": "resumo instrucional",\n' + ' "chunkContent": "texto instrutivo"\n' " }\n" " ]\n" "}\n" "Regras adicionais:\n" "- Não crie resumo vazio.\n" - "- Se a página não tiver instrução clara, resuma declarativamente mas mantenha a orientação de \"como\" fazer.\n" + '- Se a página não tiver instrução clara, resuma declarativamente mas mantenha a orientação de "como" fazer.\n' "- Nunca inclua comentários fora do JSON.\n" ) @@ -117,7 +115,9 @@ def __init__(self, settings: AppSettings) -> None: def _provide_ad_token() -> str: try: return credential.get_token(_COGNITIVE_SCOPE).token - except Exception as exc: # pragma: no cover - credential failures depend on env + except ( + Exception + ) as exc: # pragma: no cover - credential failures depend on env logger.error("Failed to acquire Azure AD token: %s", exc) raise @@ -131,7 +131,9 @@ def _provide_ad_token() -> str: ad_token_provider=_provide_ad_token, ) ) - settings_prompt = kernel.get_prompt_execution_settings_from_service_id(service_id="chunking") + settings_prompt = kernel.get_prompt_execution_settings_from_service_id( + service_id="chunking" + ) settings_prompt.function_choice_behavior = FunctionChoiceBehavior.Auto() settings_prompt.response_format = AggregatedChunk # type: ignore[attr-defined] self._agent = ChatCompletionAgent( @@ -142,7 +144,9 @@ def _provide_ad_token() -> str: arguments=KernelArguments(settings=settings_prompt), ) - async def propose_chunks(self, request: ChunkingRequest) -> List[DomainAggregatedChunk]: + async def propose_chunks( + self, request: ChunkingRequest + ) -> List[DomainAggregatedChunk]: existing_payload = [ { "chunkId": c.chunk_id, @@ -181,7 +185,12 @@ async def propose_chunks(self, request: ChunkingRequest) -> List[DomainAggregate if message and message.content: gathered += message.content last_thread = item.thread - except (ValueError, RuntimeError, AgentInvokeException, HttpResponseError) as exc: # pragma: no cover + except ( + ValueError, + RuntimeError, + AgentInvokeException, + HttpResponseError, + ) as exc: # pragma: no cover logger.error("Chunking agent failure: %s", exc) finally: if last_thread is not None: @@ -200,11 +209,17 @@ async def propose_chunks(self, request: ChunkingRequest) -> List[DomainAggregate return [] if payload != normalized: - logger.warning("Agent response contained extra text; extracted JSON payload") + logger.warning( + "Agent response contained extra text; extracted JSON payload" + ) try: structured = AggregatedChunk.model_validate_json(payload) - except (json.JSONDecodeError, ValidationError, ValueError) as exc: # pragma: no cover + except ( + json.JSONDecodeError, + ValidationError, + ValueError, + ) as exc: # pragma: no cover logger.error("Invalid JSON from agent payload: %s", exc) return [] @@ -244,7 +259,9 @@ async def synthesize( if not page_list: return [] - language_history = [c for c in existing if not language or c.language == language] + language_history = [ + c for c in existing if not language or c.language == language + ] produced: List[DomainAggregatedChunk] = [] for start in range(0, len(page_list), MAX_PAGES_PER_PROMPT): batch = page_list[start : start + MAX_PAGES_PER_PROMPT] @@ -266,13 +283,17 @@ async def synthesize( chunk.chunk_topic = ["SEMANTICA"] chunk.ensure_topic_invariants() validated.append(chunk) - consolidated = self._enforce_continuity(validated, language_history, existing) + consolidated = self._enforce_continuity( + validated, language_history, existing + ) for chunk in consolidated: chunk.language = language produced.extend(consolidated) return produced - def _select_context(self, history: List[DomainAggregatedChunk]) -> List[DomainAggregatedChunk]: + def _select_context( + self, history: List[DomainAggregatedChunk] + ) -> List[DomainAggregatedChunk]: if not history: return [] topics_seen: set[tuple[str, ...]] = set() @@ -306,14 +327,27 @@ def _enforce_continuity( consolidated.append(chunk) return consolidated - def _is_continuation(self, candidate: DomainAggregatedChunk, previous: DomainAggregatedChunk) -> bool: - overlapping = bool(set(candidate.chunk_topic).intersection(previous.chunk_topic)) - has_instruction = candidate.chunk_content.lower().startswith("continu") or "passo" in candidate.chunk_content.lower() + def _is_continuation( + self, candidate: DomainAggregatedChunk, previous: DomainAggregatedChunk + ) -> bool: + overlapping = bool( + set(candidate.chunk_topic).intersection(previous.chunk_topic) + ) + has_instruction = ( + candidate.chunk_content.lower().startswith("continu") + or "passo" in candidate.chunk_content.lower() + ) return overlapping and has_instruction - def _merge(self, base: DomainAggregatedChunk, extension: DomainAggregatedChunk) -> DomainAggregatedChunk: - base.chunk_content = base.chunk_content.rstrip() + "\n" + extension.chunk_content.lstrip() - base.chunk_summary = base.chunk_summary.rstrip(".") + "; " + extension.chunk_summary.lstrip() + def _merge( + self, base: DomainAggregatedChunk, extension: DomainAggregatedChunk + ) -> DomainAggregatedChunk: + base.chunk_content = ( + base.chunk_content.rstrip() + "\n" + extension.chunk_content.lstrip() + ) + base.chunk_summary = ( + base.chunk_summary.rstrip(".") + "; " + extension.chunk_summary.lstrip() + ) base.chunk_files = list({*base.chunk_files, *extension.chunk_files}) base.chunk_topic = list({*base.chunk_topic, *extension.chunk_topic}) return base diff --git a/apps/text_generation/src/ingest_data/embed.py b/apps/text_generation/src/ingest_data/embed.py index 356d24b..3a73f5e 100644 --- a/apps/text_generation/src/ingest_data/embed.py +++ b/apps/text_generation/src/ingest_data/embed.py @@ -28,17 +28,14 @@ import logging from typing import Optional -from dotenv import find_dotenv, load_dotenv - -from semantic_kernel.functions.kernel_function_decorator import kernel_function - -from azure.core.credentials import AzureKeyCredential from azure.ai.inference.aio import EmbeddingsClient +from azure.core.credentials import AzureKeyCredential from azure.core.exceptions import HttpResponseError +from dotenv import find_dotenv, load_dotenv +from semantic_kernel.functions.kernel_function_decorator import kernel_function from src.config.settings import AppSettings - load_dotenv(find_dotenv()) logger = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) @@ -100,7 +97,9 @@ def __init__(self, config: AppSettings): logger.warning("Falha inicializando EmbeddingsClient: %s", exc) self._client = None - @kernel_function(name="embed", description="Generate an embedding vector for the given text.") + @kernel_function( + name="embed", description="Generate an embedding vector for the given text." + ) async def embed(self, text: str, dimensions: Optional[int] = None) -> list[float]: """ Generate an embedding vector for the given text. @@ -139,7 +138,7 @@ async def embed(self, text: str, dimensions: Optional[int] = None) -> list[float model=self._config.foundry.embedding_model, input=[text], ) # type: ignore[attr-defined] - embeddings_vector: list[float] = [float(v) for v in resp.data[0].embedding] # type: ignore[index] + embeddings_vector: list[float] = [float(v) for v in resp.data[0].embedding] # type: ignore[index] dims: int = dimensions or self._config.embedding_dimensions if len(embeddings_vector) < dims: embeddings_vector.extend([0.0] * (dims - len(embeddings_vector))) diff --git a/apps/text_generation/src/ingest_data/indexer.py b/apps/text_generation/src/ingest_data/indexer.py index 377bbac..1572040 100644 --- a/apps/text_generation/src/ingest_data/indexer.py +++ b/apps/text_generation/src/ingest_data/indexer.py @@ -7,7 +7,7 @@ from src.config.settings import AppSettings from src.domain.models import AggregatedChunk -from src.ingest_data.chunking import InstructionalChunkSynthesizer, ChunkingModelClient +from src.ingest_data.chunking import ChunkingModelClient, InstructionalChunkSynthesizer from src.ingest_data.document_reader import DocumentReader from src.ingest_data.repositories import CosmosChunkRepository from src.ingest_data.vector_indexer import SearchVectorIndexer @@ -62,6 +62,8 @@ async def index(self, paths: Iterable[str]) -> List[AggregatedChunk]: return produced -def run_two_stage_indexer(settings: AppSettings, pdf_paths: Iterable[str], *, language: str) -> List[AggregatedChunk]: +def run_two_stage_indexer( + settings: AppSettings, pdf_paths: Iterable[str], *, language: str +) -> List[AggregatedChunk]: indexer = TwoStageIndexer(settings, language=language) return asyncio.run(indexer.index(pdf_paths)) diff --git a/apps/text_generation/src/ingest_data/repositories.py b/apps/text_generation/src/ingest_data/repositories.py index d62ae86..c703538 100644 --- a/apps/text_generation/src/ingest_data/repositories.py +++ b/apps/text_generation/src/ingest_data/repositories.py @@ -7,9 +7,9 @@ from dataclasses import dataclass, field from typing import Any, Dict, List, Optional -from src.domain.models import AggregatedChunk -from src.domain.cosmos_store import CosmosDocumentStore, CosmosPersistenceError, make_cosmos_store from src.config.settings import AppSettings, CosmosSettings +from src.domain.cosmos_store import CosmosDocumentStore, CosmosPersistenceError, make_cosmos_store +from src.domain.models import AggregatedChunk logger = logging.getLogger(__name__) @@ -78,7 +78,9 @@ async def list_chunks(self) -> List[AggregatedChunk]: try: query = "SELECT * FROM c WHERE c.type = 'aggregated_chunk'" try: - iterator = container.query_items(query=query, enable_cross_partition_query=True) + iterator = container.query_items( + query=query, enable_cross_partition_query=True + ) async for item in iterator: # type: ignore[attr-defined] self._append_chunk(results, item) except TypeError: # older SDKs bubble this via aiohttp @@ -100,20 +102,24 @@ async def list_chunks(self) -> List[AggregatedChunk]: def _append_chunk(self, results: List[AggregatedChunk], item: Any) -> None: try: - results.append(AggregatedChunk( - chunk_id=item.get("chunkId") or item.get("id"), - chunk_topic=item.get("chunkTopic", []), - chunk_files=item.get("chunkFiles", []), - chunk_summary=item.get("chunkSummary", ""), - chunk_content=item.get("chunkContent", ""), - language=item.get("language", ""), - )) + results.append( + AggregatedChunk( + chunk_id=item.get("chunkId") or item.get("id"), + chunk_topic=item.get("chunkTopic", []), + chunk_files=item.get("chunkFiles", []), + chunk_summary=item.get("chunkSummary", ""), + chunk_content=item.get("chunkContent", ""), + language=item.get("language", ""), + ) + ) except Exception as exc: logger.debug("Discarding malformed Cosmos item: %s", exc) async def save_chunk(self, chunk: AggregatedChunk) -> None: if not self._store: - logger.debug("Cosmos store disabled; skipping chunk save for %s", chunk.chunk_id) + logger.debug( + "Cosmos store disabled; skipping chunk save for %s", chunk.chunk_id + ) return payload = chunk.to_cosmos_document() try: @@ -201,7 +207,9 @@ async def append_iteration( ) await self._persist(document) - async def upsert_dialogue_result(self, dialogue_id: str, result: Dict[str, Any]) -> None: + async def upsert_dialogue_result( + self, dialogue_id: str, result: Dict[str, Any] + ) -> None: if not self._store: return document = self._build_document( diff --git a/apps/text_generation/src/ingest_data/retrieve.py b/apps/text_generation/src/ingest_data/retrieve.py index f5e66c6..cf7422e 100644 --- a/apps/text_generation/src/ingest_data/retrieve.py +++ b/apps/text_generation/src/ingest_data/retrieve.py @@ -38,9 +38,9 @@ True """ -import os import glob import logging +import os from typing import Any from azure.core.credentials import AzureKeyCredential @@ -48,11 +48,8 @@ from azure.search.documents import SearchClient from azure.search.documents.indexes import SearchIndexClient from azure.search.documents.indexes.models import SearchIndex - -from semantic_kernel.functions.kernel_function_decorator import kernel_function - from pypdf import PdfReader - +from semantic_kernel.functions.kernel_function_decorator import kernel_function logger = logging.getLogger(__name__) @@ -61,10 +58,11 @@ class AzureAISearchTool: """ Uses Azure AI Search to perform search queries on indexed data. """ + def __init__(self, endpoint: str, index_name: str, key: str): """ Initialize the AzureAISearchTool. - + Args: endpoint (str, optional): Azure AI Search service endpoint. Defaults to os.environ["AZURE_AI_SEARCH_SERVICE"]. index_name (str, optional): Name of the search index. Defaults to os.environ["AZURE_AI_INDEX"]. @@ -87,10 +85,13 @@ def __init__(self, endpoint: str, index_name: str, key: str): self.search_client = SearchClient( endpoint=self.endpoint, index_name=self.index_name, - credential=AzureKeyCredential(self.key) + credential=AzureKeyCredential(self.key), ) - @kernel_function(name="search_index", description="Executa uma busca no índice Azure AI Search e retorna documentos relevantes") + @kernel_function( + name="search_index", + description="Executa uma busca no índice Azure AI Search e retorna documentos relevantes", + ) def search(self, query: str) -> list[dict[str, Any]]: """ Execute a search query against the Azure AI Search index. @@ -106,12 +107,14 @@ def search(self, query: str) -> list[dict[str, Any]]: output = [] for result in results: try: - output.append({ - "chunk_id": result.get("chunk_id", None), - "chunk_topic": result.get("chunk_topic", None), - "chunk_summary": result.get("chunk_summary", None), - "chunk_content": result.get("chunk_content", None) - }) + output.append( + { + "chunk_id": result.get("chunk_id", None), + "chunk_topic": result.get("chunk_topic", None), + "chunk_summary": result.get("chunk_summary", None), + "chunk_content": result.get("chunk_content", None), + } + ) except Exception: # pragma: no cover continue return output @@ -122,16 +125,18 @@ def search(self, query: str) -> list[dict[str, Any]]: def create_index(self, index_schema: SearchIndex) -> dict: """ Create a new index in the Azure AI Search service using the provided index schema. - + Args: index_schema (SearchIndex): A SearchIndex object defining the schema of the new index. - + Returns: dict: A dictionary containing the result of the index creation operation. On success, returns the created index name; on failure, returns an error message. """ try: - index_client = SearchIndexClient(endpoint=self.endpoint, credential=AzureKeyCredential(self.key)) + index_client = SearchIndexClient( + endpoint=self.endpoint, credential=AzureKeyCredential(self.key) + ) created_index = index_client.create_index(index_schema) return {"status": "Index created", "index": created_index.name} except Exception as e: @@ -143,10 +148,13 @@ class CosmosSearchTool: """ Performs search operations on an Azure Cosmos DB container. """ - def __init__(self, endpoint: str, key: str, database_name: str, container_name: str): + + def __init__( + self, endpoint: str, key: str, database_name: str, container_name: str + ): """ Initialize the CosmosSearchTool. - + Args: endpoint (str, optional): Azure Cosmos DB endpoint. Defaults to os.environ["COSMOS_ENDPOINT"]. key (str, optional): Azure Cosmos DB key. Defaults to os.environ["COSMOS_KEY"]. @@ -157,17 +165,19 @@ def __init__(self, endpoint: str, key: str, database_name: str, container_name: self.key = key or os.environ.get("COSMOS_KEY", "") self.database_name = database_name or os.environ.get("COSMOS_DATABASE", "") self.container_name = container_name or os.environ.get("COSMOS_CONTAINER", "") - if not (self.endpoint and self.key and self.database_name and self.container_name): + if not ( + self.endpoint and self.key and self.database_name and self.container_name + ): logger.error("Azure Cosmos DB configuration missing.") raise ValueError("Azure Cosmos DB configuration required.") async def search(self, query: str) -> list[dict[str, Any]]: """ Execute a search query against the Cosmos DB container using SQL query language. - + Args: query (str): A filter condition to be used in the WHERE clause. - + Returns: List[Dict[str, Any]]: A list of documents that match the query. """ @@ -178,7 +188,12 @@ async def search(self, query: str) -> list[dict[str, Any]]: container = database.get_container_client(self.container_name) # Build a SQL query with the provided condition. sql_query = f"SELECT * FROM c WHERE {query}" - items = [item async for item in container.query_items(query=sql_query, enable_cross_partition_query=True)] + items = [ + item + async for item in container.query_items( + query=sql_query, enable_cross_partition_query=True + ) + ] return items except Exception as e: logger.error("Error performing Cosmos DB search: %s", str(e)) @@ -193,9 +208,10 @@ class PDFLoader: the module-level docstring demonstrate basic behaviors such as listing PDF files and handling missing files when loading content. """ + @kernel_function( name="list_pdf_files", - description="Lists all PDF files in the specified directory." + description="Lists all PDF files in the specified directory.", ) def list_pdfs(self, folder: str) -> list[str]: """ @@ -216,7 +232,7 @@ def list_pdfs(self, folder: str) -> list[str]: @kernel_function( name="load_pdf_files", - description="Loads a PDF file and extracts text from a range of pages with optional offset." + description="Loads a PDF file and extracts text from a range of pages with optional offset.", ) def load_pdfs(self, file_path: str, page_amount: int = 10, offset: int = 0) -> str: """ diff --git a/apps/text_generation/src/ingest_data/run_agent_indexation.py b/apps/text_generation/src/ingest_data/run_agent_indexation.py index 8173611..807bc52 100644 --- a/apps/text_generation/src/ingest_data/run_agent_indexation.py +++ b/apps/text_generation/src/ingest_data/run_agent_indexation.py @@ -1,4 +1,5 @@ """Manual driver for the two-stage indexing pipeline.""" + from __future__ import annotations import argparse @@ -6,7 +7,7 @@ from pathlib import Path from typing import Iterable -from dotenv import load_dotenv, find_dotenv +from dotenv import find_dotenv, load_dotenv from src.config.settings import AppSettings, load_settings # type: ignore from src.ingest_data.indexer import run_two_stage_indexer @@ -30,13 +31,13 @@ def _collect_pdf_paths(limit: int | None, language: str) -> Iterable[str]: def _validate_env(settings: AppSettings) -> list[str]: missing: list[str] = [] if not settings.azure_foundry_key: - missing.append('AZURE_FOUNDRY_KEY') + missing.append("AZURE_FOUNDRY_KEY") if not settings.foundry.foundry_endpoint and not settings.foundry.openai_endpoint: - missing.append('AZURE_FOUNDRY_URL/AZURE_OPENAI_ENDPOINT') + missing.append("AZURE_FOUNDRY_URL/AZURE_OPENAI_ENDPOINT") if not settings.search.endpoint: - missing.append('AZURE_SEARCH_ENDPOINT') + missing.append("AZURE_SEARCH_ENDPOINT") if not settings.foundry.chat_model: - missing.append('AZURE_OPENAI_CHAT_MODEL') + missing.append("AZURE_OPENAI_CHAT_MODEL") return missing @@ -49,7 +50,7 @@ async def run(language: str, limit: int | None) -> None: paths = list(_collect_pdf_paths(limit=limit, language=language)) if not paths: - print('[WARN] No PDFs discovered for indexing.') + print("[WARN] No PDFs discovered for indexing.") return # Ensure the Azure AI Search index exists before processing to avoid runtime 404s. @@ -66,9 +67,15 @@ async def run(language: str, limit: int | None) -> None: def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser(description="Run the two-stage indexer over sample PDFs.") - parser.add_argument("--limit", type=int, default=None, help="Limit number of files (debug).") - parser.add_argument("--language", default="matis", help="Filter PDFs by language token in filename.") + parser = argparse.ArgumentParser( + description="Run the two-stage indexer over sample PDFs." + ) + parser.add_argument( + "--limit", type=int, default=None, help="Limit number of files (debug)." + ) + parser.add_argument( + "--language", default="matis", help="Filter PDFs by language token in filename." + ) return parser.parse_args() diff --git a/apps/text_generation/src/ingest_data/vector_indexer.py b/apps/text_generation/src/ingest_data/vector_indexer.py index 7f57b46..5eefb45 100644 --- a/apps/text_generation/src/ingest_data/vector_indexer.py +++ b/apps/text_generation/src/ingest_data/vector_indexer.py @@ -48,7 +48,9 @@ def __init__(self, settings: AppSettings) -> None: raise RuntimeError("Azure Search endpoint not configured") self._settings = settings credential = _search_credential(settings) - self._index_client = SearchIndexClient(endpoint=settings.search.endpoint, credential=credential) + self._index_client = SearchIndexClient( + endpoint=settings.search.endpoint, credential=credential + ) self._search_client: Optional[SearchClient] = None self._credential = credential @@ -120,7 +122,12 @@ def create_or_update_index(self) -> None: ] vector_search = VectorSearch( algorithms=[HnswAlgorithmConfiguration(name="underlyingHnsw")], - profiles=[VectorSearchProfile(name="underlyingHnswProfile", algorithm_configuration_name="underlyingHnsw")], + profiles=[ + VectorSearchProfile( + name="underlyingHnswProfile", + algorithm_configuration_name="underlyingHnsw", + ) + ], vectorizers=[ AzureOpenAIVectorizer( vectorizer_name="myVectorizer", @@ -176,7 +183,11 @@ def _build_vectorizer_parameters(self) -> AzureOpenAIVectorizerParameters: class SearchVectorIndexer: """Coordinates embedding generation and search upload.""" - def __init__(self, settings: AppSettings, embedding_service: Optional[EmbeddingService] = None) -> None: + def __init__( + self, + settings: AppSettings, + embedding_service: Optional[EmbeddingService] = None, + ) -> None: self._settings = settings self._embedding = embedding_service or EmbeddingService(settings) self._search_service = SearchIndexService(settings) @@ -186,5 +197,7 @@ async def index_chunks(self, chunks: Iterable[AggregatedChunk]) -> List[bool]: payloads: list[dict[str, Any]] = [] for chunk in chunks: vector = await self._embedding.embed(chunk.chunk_content) - payloads.append(chunk.to_search_document(vector, self._search_service.index_name)) + payloads.append( + chunk.to_search_document(vector, self._search_service.index_name) + ) return self._search_service.upload_documents(payloads) diff --git a/apps/text_generation/src/main.py b/apps/text_generation/src/main.py index da78d06..dc15dab 100644 --- a/apps/text_generation/src/main.py +++ b/apps/text_generation/src/main.py @@ -11,16 +11,16 @@ import os import time -import psutil -from fastapi import FastAPI, Request, UploadFile, File +import psutil +from fastapi import FastAPI, File, Request, UploadFile from fastapi.responses import JSONResponse from fastapi.security import OAuth2PasswordBearer from starlette.middleware.cors import CORSMiddleware -from src import __app__, __author__, __version__, logger -from .blob import save_upload_to_temp, make_blob_uploader +from src import __app__, __version__, logger +from .blob import make_blob_uploader, save_upload_to_temp tags_metadata: list[dict] = [ { @@ -59,17 +59,14 @@ """ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") -app = FastAPI( - title=__app__, - version=__version__ -) +app = FastAPI(title=__app__, version=__version__) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], - allow_headers=["*"] + allow_headers=["*"], ) @@ -94,10 +91,10 @@ async def health_check(request: Request): "metadata": { "timestamp": time.time(), "memory_mb": round(memory_usage, 2), - "version": __version__ - } + "version": __version__, + }, }, - status_code=200 + status_code=200, ) @@ -139,17 +136,15 @@ async def incoming_call_handler(request: Request): result = await room_user_observer.handle_incoming_call(event_dict) if result and "validationResponse" in result: return JSONResponse( - content={ - "validationResponse": result["validationResponse"] - }, - status_code=200 + content={"validationResponse": result["validationResponse"]}, + status_code=200, ) return JSONResponse( content={ "title": "Incoming call handler", - "message": "Incoming call event processed successfully" + "message": "Incoming call event processed successfully", }, - status_code=201 + status_code=201, ) @@ -170,7 +165,7 @@ async def callbacks(request: Request): return JSONResponse( content={ "title": "Incoming call handler", - "message": "Incoming call event processed successfully" + "message": "Incoming call event processed successfully", }, - status_code=200 + status_code=200, ) diff --git a/apps/text_generation/src/pipelines/dialogues/runner.py b/apps/text_generation/src/pipelines/dialogues/runner.py index 0432008..40a82cf 100644 --- a/apps/text_generation/src/pipelines/dialogues/runner.py +++ b/apps/text_generation/src/pipelines/dialogues/runner.py @@ -25,8 +25,12 @@ ROOT = Path(__file__).resolve().parents[4] SRC = ROOT / "text_generation" / "src" -LOG_LEVEL = getattr(logging, os.getenv("DIALOGUE_LOG_LEVEL", "INFO").upper(), logging.INFO) -logging.basicConfig(level=LOG_LEVEL, format="%(asctime)s %(levelname)s %(name)s %(message)s") +LOG_LEVEL = getattr( + logging, os.getenv("DIALOGUE_LOG_LEVEL", "INFO").upper(), logging.INFO +) +logging.basicConfig( + level=LOG_LEVEL, format="%(asctime)s %(levelname)s %(name)s %(message)s" +) logger = logging.getLogger("dialogue_generation") @@ -51,14 +55,18 @@ def build_kernel() -> sk.Kernel: if search_endpoint and search_index and search_key: plugins.append(AzureAISearchTool(search_endpoint, search_index, search_key)) else: - logger.warning("Azure AI Search environment variables missing; Search plugin not registered.") + logger.warning( + "Azure AI Search environment variables missing; Search plugin not registered." + ) for plugin in plugins: kernel.add_plugin(plugin, plugin.__class__.__name__) return kernel def create_agent(kernel: sk.Kernel, prompt: str) -> ChatCompletionAgent: - settings = kernel.get_prompt_execution_settings_from_service_id(service_id="default") + settings = kernel.get_prompt_execution_settings_from_service_id( + service_id="default" + ) settings.function_choice_behavior = FunctionChoiceBehavior.Auto() return ChatCompletionAgent( kernel=kernel, @@ -114,7 +122,9 @@ async def generate_dialogues(kernel: sk.Kernel, idioma: str) -> List[Dict[str, A } history = ChatHistory() history.add_message( - ChatMessageContent(role=AuthorRole.USER, content=json.dumps(payload, ensure_ascii=False)) + ChatMessageContent( + role=AuthorRole.USER, content=json.dumps(payload, ensure_ascii=False) + ) ) response_text = "" diff --git a/apps/text_generation/src/pipelines/evaluate/runner.py b/apps/text_generation/src/pipelines/evaluate/runner.py index 5d29161..0c26b53 100644 --- a/apps/text_generation/src/pipelines/evaluate/runner.py +++ b/apps/text_generation/src/pipelines/evaluate/runner.py @@ -4,6 +4,7 @@ """ from __future__ import annotations + from dataclasses import dataclass from typing import List diff --git a/apps/text_generation/src/pipelines/translate/ft_dataset_creator.py b/apps/text_generation/src/pipelines/translate/ft_dataset_creator.py index 1dc5139..d250716 100644 --- a/apps/text_generation/src/pipelines/translate/ft_dataset_creator.py +++ b/apps/text_generation/src/pipelines/translate/ft_dataset_creator.py @@ -15,143 +15,149 @@ @dataclass(frozen=True) class LanguageSpec: - canonical: str - display_name: str - target_key: str + canonical: str + display_name: str + target_key: str LANGUAGE_SPECS: Dict[str, LanguageSpec] = { - "matis": LanguageSpec(canonical="matis", display_name="Matis", target_key="matis"), - "matses": LanguageSpec(canonical="matses", display_name="Matses", target_key="matses"), + "matis": LanguageSpec(canonical="matis", display_name="Matis", target_key="matis"), + "matses": LanguageSpec( + canonical="matses", display_name="Matses", target_key="matses" + ), } ALIAS_TO_LANGUAGE: Dict[str, str] = { - "matis": "matis", - "mtq": "matis", - "matses": "matses", - "mtr": "matses", + "matis": "matis", + "mtq": "matis", + "matses": "matses", + "mtr": "matses", } def _resolve_language_spec(language: str) -> LanguageSpec: - token = (language or "").strip().lower() - canonical = ALIAS_TO_LANGUAGE.get(token) - if not canonical: - raise ValueError("Supported languages are 'matis' (mtq) and 'matses' (mtr).") - return LANGUAGE_SPECS[canonical] + token = (language or "").strip().lower() + canonical = ALIAS_TO_LANGUAGE.get(token) + if not canonical: + raise ValueError("Supported languages are 'matis' (mtq) and 'matses' (mtr).") + return LANGUAGE_SPECS[canonical] -def _render_system_prompt(base: Template, spec: LanguageSpec, knowledge_block: str) -> str: - rendered = base.safe_substitute(language=spec.display_name) - rendered = rendered.replace("{{lingua}}", spec.display_name) - appendix = knowledge_block.strip() - if appendix: - return f"{rendered.rstrip()}\n\n{appendix}\n" - return rendered +def _render_system_prompt( + base: Template, spec: LanguageSpec, knowledge_block: str +) -> str: + rendered = base.safe_substitute(language=spec.display_name) + rendered = rendered.replace("{{lingua}}", spec.display_name) + appendix = knowledge_block.strip() + if appendix: + return f"{rendered.rstrip()}\n\n{appendix}\n" + return rendered def _parse_pairs(payload: str, target_key: str) -> List[Tuple[str, str]]: - try: - data = json.loads(payload) - except json.JSONDecodeError as exc: # pragma: no cover - defensive guard - raise ValueError("Prompt payload is not valid JSON.") from exc - - pairs: List[Tuple[str, str]] = [] - for item in data if isinstance(data, list) else []: - if not isinstance(item, dict): - continue - portuguese = str(item.get("portugues", "")).strip() - target = str(item.get(target_key, "")).strip() - if portuguese and target: - pairs.append((portuguese, target)) - return pairs - - -def _build_examples(system_prompt: str, pairs: Sequence[Tuple[str, str]]) -> Iterable[Dict[str, Any]]: - system_content = _normalize_text(system_prompt) - for portuguese, indigenous in pairs: - portuguese_clean = _normalize_text(portuguese) - indigenous_clean = _normalize_text(indigenous) - yield { - "messages": [ - {"role": "system", "content": system_content}, - {"role": "user", "content": portuguese_clean}, - {"role": "assistant", "content": indigenous_clean}, - ] - } - yield { - "messages": [ - {"role": "system", "content": system_content}, - {"role": "user", "content": indigenous_clean}, - {"role": "assistant", "content": portuguese_clean}, - ] - } + try: + data = json.loads(payload) + except json.JSONDecodeError as exc: # pragma: no cover - defensive guard + raise ValueError("Prompt payload is not valid JSON.") from exc + + pairs: List[Tuple[str, str]] = [] + for item in data if isinstance(data, list) else []: + if not isinstance(item, dict): + continue + portuguese = str(item.get("portugues", "")).strip() + target = str(item.get(target_key, "")).strip() + if portuguese and target: + pairs.append((portuguese, target)) + return pairs + + +def _build_examples( + system_prompt: str, pairs: Sequence[Tuple[str, str]] +) -> Iterable[Dict[str, Any]]: + system_content = _normalize_text(system_prompt) + for portuguese, indigenous in pairs: + portuguese_clean = _normalize_text(portuguese) + indigenous_clean = _normalize_text(indigenous) + yield { + "messages": [ + {"role": "system", "content": system_content}, + {"role": "user", "content": portuguese_clean}, + {"role": "assistant", "content": indigenous_clean}, + ] + } + yield { + "messages": [ + {"role": "system", "content": system_content}, + {"role": "user", "content": indigenous_clean}, + {"role": "assistant", "content": portuguese_clean}, + ] + } def _normalize_text(value: str) -> str: - text = (value or "").replace("\r", "\n") - text = re.sub(r"\s+", " ", text) - return text.strip() + text = (value or "").replace("\r", "\n") + text = re.sub(r"\s+", " ", text) + return text.strip() def _write_jsonl(path: Path, rows: Iterable[Dict[str, Any]]) -> int: - path.parent.mkdir(parents=True, exist_ok=True) - count = 0 - with path.open("w", encoding="utf-8") as handle: - for row in rows: - handle.write(json.dumps(row, ensure_ascii=False)) - handle.write("\n") - count += 1 - return count + path.parent.mkdir(parents=True, exist_ok=True) + count = 0 + with path.open("w", encoding="utf-8") as handle: + for row in rows: + handle.write(json.dumps(row, ensure_ascii=False)) + handle.write("\n") + count += 1 + return count def create_fine_tuning_dataset(language: str, output_path: Path | None = None) -> Path: - spec = _resolve_language_spec(language) - base_prompt, prompt_matses, prompt_matis = _load_translation_prompts() - knowledge_raw = prompt_matis if spec.canonical == "matis" else prompt_matses - system_prompt = _render_system_prompt(base_prompt, spec, knowledge_raw) - pairs = _parse_pairs(knowledge_raw, spec.target_key) - if not pairs: - raise ValueError(f"No translation pairs available for {spec.display_name}.") + spec = _resolve_language_spec(language) + base_prompt, prompt_matses, prompt_matis = _load_translation_prompts() + knowledge_raw = prompt_matis if spec.canonical == "matis" else prompt_matses + system_prompt = _render_system_prompt(base_prompt, spec, knowledge_raw) + pairs = _parse_pairs(knowledge_raw, spec.target_key) + if not pairs: + raise ValueError(f"No translation pairs available for {spec.display_name}.") - rows = list(_build_examples(system_prompt, pairs)) - if not rows: - raise ValueError("No dataset rows were generated.") + rows = list(_build_examples(system_prompt, pairs)) + if not rows: + raise ValueError("No dataset rows were generated.") - destination = output_path or TRANSLATIONS_OUTPUT_DIR / f"ft_{spec.canonical}.jsonl" - destination = destination if isinstance(destination, Path) else Path(destination) - _write_jsonl(destination, rows) - return destination + destination = output_path or TRANSLATIONS_OUTPUT_DIR / f"ft_{spec.canonical}.jsonl" + destination = destination if isinstance(destination, Path) else Path(destination) + _write_jsonl(destination, rows) + return destination def _build_argument_parser() -> argparse.ArgumentParser: - parser = argparse.ArgumentParser( - description="Create a chat fine-tuning dataset for Matis or Matses translations.", - ) - parser.add_argument( - "language", - help="Target language identifier (matis/mtq or matses/mtr).", - ) - parser.add_argument( - "-o", - "--output", - type=Path, - help="Path to the output .jsonl file (defaults to the pipeline artifacts directory).", - ) - return parser + parser = argparse.ArgumentParser( + description="Create a chat fine-tuning dataset for Matis or Matses translations.", + ) + parser.add_argument( + "language", + help="Target language identifier (matis/mtq or matses/mtr).", + ) + parser.add_argument( + "-o", + "--output", + type=Path, + help="Path to the output .jsonl file (defaults to the pipeline artifacts directory).", + ) + return parser def main(argv: Sequence[str] | None = None) -> Path: - parser = _build_argument_parser() - args = parser.parse_args(argv) - try: - output = create_fine_tuning_dataset(args.language, args.output) - except ValueError as exc: - parser.error(str(exc)) - return output + parser = _build_argument_parser() + args = parser.parse_args(argv) + try: + output = create_fine_tuning_dataset(args.language, args.output) + except ValueError as exc: + parser.error(str(exc)) + return output if __name__ == "__main__": # pragma: no cover - CLI entrypoint - path = main() - print(f"Dataset written to {path}") + path = main() + print(f"Dataset written to {path}") diff --git a/apps/text_generation/src/pipelines/translate/runner.py b/apps/text_generation/src/pipelines/translate/runner.py index 77690bf..bbad1e8 100644 --- a/apps/text_generation/src/pipelines/translate/runner.py +++ b/apps/text_generation/src/pipelines/translate/runner.py @@ -28,20 +28,22 @@ FineTuningTaskType = None # type: ignore[assignment] create_finetuning_job = None # type: ignore[assignment] -if importlib_util.find_spec("azure.ai.ml") is not None: # pragma: no cover - optional dependency - from azure.ai.ml import MLClient as _MLClient # type: ignore[import] - from azure.ai.ml.constants import AssetTypes as _AssetTypes # type: ignore[import] - from azure.ai.ml.entities import Data as _Data # type: ignore[import] - from azure.ai.ml.finetuning import ( # type: ignore[import] - FineTuningTaskType as _FineTuningTaskType, - create_finetuning_job as _create_finetuning_job, - ) - - MLClient = _MLClient - AssetTypes = _AssetTypes - Data = _Data - FineTuningTaskType = _FineTuningTaskType - create_finetuning_job = _create_finetuning_job +if ( + importlib_util.find_spec("azure.ai.ml") is not None +): # pragma: no cover - optional dependency + from azure.ai.ml import MLClient as _MLClient # type: ignore[import] + from azure.ai.ml.constants import AssetTypes as _AssetTypes # type: ignore[import] + from azure.ai.ml.entities import Data as _Data # type: ignore[import] + from azure.ai.ml.finetuning import ( + FineTuningTaskType as _FineTuningTaskType, # type: ignore[import] + ) + from azure.ai.ml.finetuning import create_finetuning_job as _create_finetuning_job + + MLClient = _MLClient + AssetTypes = _AssetTypes + Data = _Data + FineTuningTaskType = _FineTuningTaskType + create_finetuning_job = _create_finetuning_job LOGGER = logger @@ -57,455 +59,549 @@ @dataclass(frozen=True) class FineTuningExample: - source_text: str - target_text: str - source_language: str - target_language: str - target_alias: str - metadata: Dict[str, Any] + source_text: str + target_text: str + source_language: str + target_language: str + target_alias: str + metadata: Dict[str, Any] @dataclass(frozen=True) class FineTuningDataset: - train_path: Path - validation_path: Optional[Path] - train_count: int - validation_count: int + train_path: Path + validation_path: Optional[Path] + train_count: int + validation_count: int @dataclass(frozen=True) class FineTuningJobSummary: - job_name: str - display_name: str - status: str - output_model_name: str - training_examples: int - validation_examples: int - submitted_at: str - language: str + job_name: str + display_name: str + status: str + output_model_name: str + training_examples: int + validation_examples: int + submitted_at: str + language: str @dataclass(frozen=True) class FineTuningConfig: - subscription_id: str - resource_group: str - workspace_name: str - registry_name: Optional[str] - base_model_id: Optional[str] - base_model_name: Optional[str] - base_model_version: Optional[str] - base_model_label: str - experiment_name: str - output_model_prefix: str - job_prefix: str - train_dataset_name: str - validation_dataset_name: str - dataset_version: str - validation_split: float - hyperparameters: Dict[str, str] - - @classmethod - def from_env(cls) -> "FineTuningConfig": - subscription_id = os.getenv("AZURE_ML_SUBSCRIPTION_ID") or os.getenv("AZURE_SUBSCRIPTION_ID") - resource_group = os.getenv("AZURE_ML_RESOURCE_GROUP") or os.getenv("AZURE_RESOURCE_GROUP") - workspace_name = ( - os.getenv("AZURE_ML_WORKSPACE_NAME") - or os.getenv("AZURE_AI_PROJECT_NAME") - or os.getenv("AZURE_PROJECT_NAME") - ) - if not subscription_id or not resource_group or not workspace_name: - raise RuntimeError( - "Azure ML configuration missing. Set AZURE_ML_SUBSCRIPTION_ID, AZURE_ML_RESOURCE_GROUP, and AZURE_ML_WORKSPACE_NAME." - ) - - registry_name = os.getenv("AZURE_ML_REGISTRY_NAME") - base_model_id = os.getenv("AZURE_ML_BASE_MODEL_ID") - base_model_name = os.getenv("AZURE_ML_BASE_MODEL_NAME") - base_model_version = os.getenv("AZURE_ML_BASE_MODEL_VERSION") - base_model_label = os.getenv("AZURE_ML_BASE_MODEL_LABEL", "latest") - - experiment_name = os.getenv("AZURE_ML_EXPERIMENT_NAME", "translation-finetuning") - output_model_prefix = os.getenv("AZURE_ML_OUTPUT_MODEL_PREFIX", "translation-ft") - job_prefix = os.getenv("AZURE_ML_JOB_PREFIX", "translation-ft") - train_dataset_name = os.getenv("AZURE_ML_TRAIN_DATASET_NAME", "translation-ft-train") - validation_dataset_name = os.getenv("AZURE_ML_VALIDATION_DATASET_NAME", f"{train_dataset_name}-validation") - dataset_version = datetime.utcnow().strftime("%Y%m%d%H%M%S") - - validation_split_env = os.getenv("AZURE_ML_VALIDATION_SPLIT") - try: - validation_split = float(validation_split_env) if validation_split_env else 0.1 - except ValueError: - validation_split = 0.1 - validation_split = max(0.0, min(validation_split, 0.5)) - - hyperparameters = { - "per_device_train_batch_size": os.getenv("AZURE_ML_FT_BATCH_SIZE", "1"), - "learning_rate": os.getenv("AZURE_ML_FT_LEARNING_RATE", "0.00002"), - "num_train_epochs": os.getenv("AZURE_ML_FT_EPOCHS", "1"), - } - - return cls( - subscription_id=subscription_id, - resource_group=resource_group, - workspace_name=workspace_name, - registry_name=registry_name, - base_model_id=base_model_id, - base_model_name=base_model_name, - base_model_version=base_model_version, - base_model_label=base_model_label, - experiment_name=experiment_name, - output_model_prefix=output_model_prefix, - job_prefix=job_prefix, - train_dataset_name=train_dataset_name, - validation_dataset_name=validation_dataset_name, - dataset_version=dataset_version, - validation_split=validation_split, - hyperparameters=hyperparameters, - ) + subscription_id: str + resource_group: str + workspace_name: str + registry_name: Optional[str] + base_model_id: Optional[str] + base_model_name: Optional[str] + base_model_version: Optional[str] + base_model_label: str + experiment_name: str + output_model_prefix: str + job_prefix: str + train_dataset_name: str + validation_dataset_name: str + dataset_version: str + validation_split: float + hyperparameters: Dict[str, str] + + @classmethod + def from_env(cls) -> "FineTuningConfig": + subscription_id = os.getenv("AZURE_ML_SUBSCRIPTION_ID") or os.getenv( + "AZURE_SUBSCRIPTION_ID" + ) + resource_group = os.getenv("AZURE_ML_RESOURCE_GROUP") or os.getenv( + "AZURE_RESOURCE_GROUP" + ) + workspace_name = ( + os.getenv("AZURE_ML_WORKSPACE_NAME") + or os.getenv("AZURE_AI_PROJECT_NAME") + or os.getenv("AZURE_PROJECT_NAME") + ) + if not subscription_id or not resource_group or not workspace_name: + raise RuntimeError( + "Azure ML configuration missing. Set AZURE_ML_SUBSCRIPTION_ID, AZURE_ML_RESOURCE_GROUP, and AZURE_ML_WORKSPACE_NAME." + ) + + registry_name = os.getenv("AZURE_ML_REGISTRY_NAME") + base_model_id = os.getenv("AZURE_ML_BASE_MODEL_ID") + base_model_name = os.getenv("AZURE_ML_BASE_MODEL_NAME") + base_model_version = os.getenv("AZURE_ML_BASE_MODEL_VERSION") + base_model_label = os.getenv("AZURE_ML_BASE_MODEL_LABEL", "latest") + + experiment_name = os.getenv( + "AZURE_ML_EXPERIMENT_NAME", "translation-finetuning" + ) + output_model_prefix = os.getenv( + "AZURE_ML_OUTPUT_MODEL_PREFIX", "translation-ft" + ) + job_prefix = os.getenv("AZURE_ML_JOB_PREFIX", "translation-ft") + train_dataset_name = os.getenv( + "AZURE_ML_TRAIN_DATASET_NAME", "translation-ft-train" + ) + validation_dataset_name = os.getenv( + "AZURE_ML_VALIDATION_DATASET_NAME", f"{train_dataset_name}-validation" + ) + dataset_version = datetime.utcnow().strftime("%Y%m%d%H%M%S") + + validation_split_env = os.getenv("AZURE_ML_VALIDATION_SPLIT") + try: + validation_split = ( + float(validation_split_env) if validation_split_env else 0.1 + ) + except ValueError: + validation_split = 0.1 + validation_split = max(0.0, min(validation_split, 0.5)) + + hyperparameters = { + "per_device_train_batch_size": os.getenv("AZURE_ML_FT_BATCH_SIZE", "1"), + "learning_rate": os.getenv("AZURE_ML_FT_LEARNING_RATE", "0.00002"), + "num_train_epochs": os.getenv("AZURE_ML_FT_EPOCHS", "1"), + } + + return cls( + subscription_id=subscription_id, + resource_group=resource_group, + workspace_name=workspace_name, + registry_name=registry_name, + base_model_id=base_model_id, + base_model_name=base_model_name, + base_model_version=base_model_version, + base_model_label=base_model_label, + experiment_name=experiment_name, + output_model_prefix=output_model_prefix, + job_prefix=job_prefix, + train_dataset_name=train_dataset_name, + validation_dataset_name=validation_dataset_name, + dataset_version=dataset_version, + validation_split=validation_split, + hyperparameters=hyperparameters, + ) def _load_translation_prompts() -> Tuple[Template, str, str]: - global PROMPT_CACHE - if PROMPT_CACHE: - return PROMPT_CACHE + global PROMPT_CACHE + if PROMPT_CACHE: + return PROMPT_CACHE - translation_src = Path(__file__).resolve().parents[4] / "translation" / "src" / "prompts.py" - if not translation_src.exists(): - raise FileNotFoundError( - f"Translation prompts module not found at {translation_src}. Ensure translation app is available." - ) + translation_src = ( + Path(__file__).resolve().parents[4] / "translation" / "src" / "prompts.py" + ) + if not translation_src.exists(): + raise FileNotFoundError( + f"Translation prompts module not found at {translation_src}. Ensure translation app is available." + ) - spec = importlib_util.spec_from_file_location("translation_prompts", translation_src) - if not spec or not spec.loader: - raise RuntimeError("Failed to load translation prompts module specification.") - module = importlib_util.module_from_spec(spec) - spec.loader.exec_module(module) + spec = importlib_util.spec_from_file_location( + "translation_prompts", translation_src + ) + if not spec or not spec.loader: + raise RuntimeError("Failed to load translation prompts module specification.") + module = importlib_util.module_from_spec(spec) + spec.loader.exec_module(module) - base_prompt = getattr(module, "BASE_PROMPT", None) - prompt_matses = getattr(module, "PROMPT_MATSES", "") - prompt_matis = getattr(module, "PROMPT_MATIS", "") + base_prompt = getattr(module, "BASE_PROMPT", None) + prompt_matses = getattr(module, "PROMPT_MATSES", "") + prompt_matis = getattr(module, "PROMPT_MATIS", "") - if not isinstance(base_prompt, Template): - base_prompt = Template(str(base_prompt)) + if not isinstance(base_prompt, Template): + base_prompt = Template(str(base_prompt)) - PROMPT_CACHE = (base_prompt, str(prompt_matses), str(prompt_matis)) - return PROMPT_CACHE + PROMPT_CACHE = (base_prompt, str(prompt_matses), str(prompt_matis)) + return PROMPT_CACHE def _resolve_language_tokens(target: str) -> Tuple[str, str]: - token = (target or "").strip().lower() - code_map = { - "matis": "mtq", - "mtq": "mtq", - "matses": "mtr", - "mtr": "mtr", - "portugues": "pt-br", - "pt-br": "pt-br", - } - code = code_map.get(token, token or "mtq") - alias_map = { - "mtq": "matis", - "mtr": "matses", - "pt-br": "portugues", - } - alias = alias_map.get(code, token or code) - return code, alias + token = (target or "").strip().lower() + code_map = { + "matis": "mtq", + "mtq": "mtq", + "matses": "mtr", + "mtr": "mtr", + "portugues": "pt-br", + "pt-br": "pt-br", + } + code = code_map.get(token, token or "mtq") + alias_map = { + "mtq": "matis", + "mtr": "matses", + "pt-br": "portugues", + } + alias = alias_map.get(code, token or code) + return code, alias def _system_prompt_for_language(language_alias: str) -> str: - base_prompt, prompt_matses, prompt_matis = _load_translation_prompts() - rendered = base_prompt.substitute(language=language_alias) - appendix = prompt_matses if language_alias.lower() == "matses" else prompt_matis - return f"{rendered}{appendix}" + base_prompt, prompt_matses, prompt_matis = _load_translation_prompts() + rendered = base_prompt.substitute(language=language_alias) + appendix = prompt_matses if language_alias.lower() == "matses" else prompt_matis + return f"{rendered}{appendix}" def _user_prompt(source_language: str, target_language: str, text: str) -> str: - payload = (text or "").strip() - return f"Translate the following text from {source_language} into {target_language}.\n\n{payload}" + payload = (text or "").strip() + return f"Translate the following text from {source_language} into {target_language}.\n\n{payload}" def _load_transliteration_results(language: str) -> List[Dict[str, Any]]: - bundle_path = TRANSLITERATIONS_DIR / "transliterations_all.json" - if not bundle_path.exists(): - raise FileNotFoundError(f"Transliteration bundle not found: {bundle_path}") - with open(bundle_path, "r", encoding="utf-8") as handle: - payload = json.load(handle) - language_token = (language or "").lower() - return [item for item in payload if str(item.get("idioma", "")).lower() == language_token] - - -def _collect_examples(results: Sequence[Dict[str, Any]], language: str) -> List[FineTuningExample]: - examples: List[FineTuningExample] = [] - target_code, target_alias = _resolve_language_tokens(language) - for result in results: - if str(result.get("status", "")).lower() != "aceito": - continue - dialogue = result.get("dialogo") if isinstance(result, dict) else None - transliteration = result.get("transliteracao") if isinstance(result, dict) else None - items = transliteration.get("itens") if isinstance(transliteration, dict) else None - if not isinstance(items, list): - continue - for item in items: - if not isinstance(item, dict): - continue - source_text = item.get("portugues") or item.get("fala") - if not source_text and isinstance(dialogue, dict): - source_text = dialogue.get("fala") - target_text = ( - item.get(target_alias) - or item.get(target_code) - or item.get(language) - or item.get(target_alias.capitalize()) - ) - if not source_text or not target_text: - continue - examples.append( - FineTuningExample( - source_text=str(source_text).strip(), - target_text=str(target_text).strip(), - source_language="pt-br", - target_language=target_code, - target_alias=target_alias, - metadata={"dialogue_id": (dialogue or {}).get("id") if isinstance(dialogue, dict) else None}, - ) - ) - return examples + bundle_path = TRANSLITERATIONS_DIR / "transliterations_all.json" + if not bundle_path.exists(): + raise FileNotFoundError(f"Transliteration bundle not found: {bundle_path}") + with open(bundle_path, "r", encoding="utf-8") as handle: + payload = json.load(handle) + language_token = (language or "").lower() + return [ + item + for item in payload + if str(item.get("idioma", "")).lower() == language_token + ] + + +def _collect_examples( + results: Sequence[Dict[str, Any]], language: str +) -> List[FineTuningExample]: + examples: List[FineTuningExample] = [] + target_code, target_alias = _resolve_language_tokens(language) + for result in results: + if str(result.get("status", "")).lower() != "aceito": + continue + dialogue = result.get("dialogo") if isinstance(result, dict) else None + transliteration = ( + result.get("transliteracao") if isinstance(result, dict) else None + ) + items = ( + transliteration.get("itens") if isinstance(transliteration, dict) else None + ) + if not isinstance(items, list): + continue + for item in items: + if not isinstance(item, dict): + continue + source_text = item.get("portugues") or item.get("fala") + if not source_text and isinstance(dialogue, dict): + source_text = dialogue.get("fala") + target_text = ( + item.get(target_alias) + or item.get(target_code) + or item.get(language) + or item.get(target_alias.capitalize()) + ) + if not source_text or not target_text: + continue + examples.append( + FineTuningExample( + source_text=str(source_text).strip(), + target_text=str(target_text).strip(), + source_language="pt-br", + target_language=target_code, + target_alias=target_alias, + metadata={ + "dialogue_id": ( + (dialogue or {}).get("id") + if isinstance(dialogue, dict) + else None + ) + }, + ) + ) + return examples def _example_to_messages(example: FineTuningExample) -> Dict[str, Any]: - return { - "messages": [ - {"role": "system", "content": _system_prompt_for_language(example.target_alias)}, - { - "role": "user", - "content": _user_prompt(example.source_language, example.target_language, example.source_text), - }, - {"role": "assistant", "content": example.target_text}, - ] - } + return { + "messages": [ + { + "role": "system", + "content": _system_prompt_for_language(example.target_alias), + }, + { + "role": "user", + "content": _user_prompt( + example.source_language, + example.target_language, + example.source_text, + ), + }, + {"role": "assistant", "content": example.target_text}, + ] + } def _write_jsonl(path: Path, rows: Iterable[Dict[str, Any]]) -> int: - count = 0 - with open(path, "w", encoding="utf-8") as handle: - for row in rows: - handle.write(json.dumps(row, ensure_ascii=False)) - handle.write("\n") - count += 1 - return count - - -def _prepare_datasets(language: str, results: Sequence[Dict[str, Any]], config: FineTuningConfig) -> FineTuningDataset: - examples = _collect_examples(results, language) - if not examples: - raise ValueError("No approved transliterations available to build fine-tuning dataset.") - - rng = random.Random(42) - rng.shuffle(examples) - - train_cutoff = len(examples) - if 0.0 < config.validation_split < 0.5 and len(examples) > 1: - proposed = int(len(examples) * (1.0 - config.validation_split)) - train_cutoff = max(1, min(len(examples) - 1, proposed)) - - train_examples = examples[:train_cutoff] - validation_examples = examples[train_cutoff:] if train_cutoff < len(examples) else [] - - language_token = re.sub(r"[^0-9a-zA-Z]+", "_", language).strip("_") or "idioma" - timestamp = config.dataset_version - train_path = TRANSLATIONS_OUTPUT_DIR / f"train_{language_token}_{timestamp}.jsonl" - validation_path = TRANSLATIONS_OUTPUT_DIR / f"validation_{language_token}_{timestamp}.jsonl" - - train_count = _write_jsonl(train_path, (_example_to_messages(example) for example in train_examples)) - validation_count = 0 - if validation_examples: - validation_count = _write_jsonl(validation_path, (_example_to_messages(example) for example in validation_examples)) - else: - validation_path = None - - return FineTuningDataset( - train_path=train_path, - validation_path=validation_path, - train_count=train_count, - validation_count=validation_count, - ) + count = 0 + with open(path, "w", encoding="utf-8") as handle: + for row in rows: + handle.write(json.dumps(row, ensure_ascii=False)) + handle.write("\n") + count += 1 + return count + + +def _prepare_datasets( + language: str, results: Sequence[Dict[str, Any]], config: FineTuningConfig +) -> FineTuningDataset: + examples = _collect_examples(results, language) + if not examples: + raise ValueError( + "No approved transliterations available to build fine-tuning dataset." + ) + + rng = random.Random(42) + rng.shuffle(examples) + + train_cutoff = len(examples) + if 0.0 < config.validation_split < 0.5 and len(examples) > 1: + proposed = int(len(examples) * (1.0 - config.validation_split)) + train_cutoff = max(1, min(len(examples) - 1, proposed)) + + train_examples = examples[:train_cutoff] + validation_examples = ( + examples[train_cutoff:] if train_cutoff < len(examples) else [] + ) + + language_token = re.sub(r"[^0-9a-zA-Z]+", "_", language).strip("_") or "idioma" + timestamp = config.dataset_version + train_path = TRANSLATIONS_OUTPUT_DIR / f"train_{language_token}_{timestamp}.jsonl" + validation_path = ( + TRANSLATIONS_OUTPUT_DIR / f"validation_{language_token}_{timestamp}.jsonl" + ) + + train_count = _write_jsonl( + train_path, (_example_to_messages(example) for example in train_examples) + ) + validation_count = 0 + if validation_examples: + validation_count = _write_jsonl( + validation_path, + (_example_to_messages(example) for example in validation_examples), + ) + else: + validation_path = None + + return FineTuningDataset( + train_path=train_path, + validation_path=validation_path, + train_count=train_count, + validation_count=validation_count, + ) def _sanitize_identifier(raw: str, max_length: int) -> str: - token = re.sub(r"[^0-9a-zA-Z-]+", "-", raw).strip("-") - token = re.sub(r"-+", "-", token) - return token[:max_length] if len(token) > max_length else token or "ft-job" - - -def _register_dataset(client: Any, path: Path, name: str, version: str, description: str) -> str: - if AssetTypes is None or Data is None: - raise RuntimeError("azure-ai-ml is required to register fine-tuning datasets. Install the package first.") - asset = Data( - path=str(path), - type=AssetTypes.URI_FILE, - description=description, - name=name, - version=version, - ) - created = client.data.create_or_update(asset) - return getattr(created, "id", None) or str(created) - - -def _resolve_base_model_id(config: FineTuningConfig, credential: DefaultAzureCredential) -> str: - if config.base_model_id: - return config.base_model_id - if MLClient is None: - raise RuntimeError("azure-ai-ml not available to resolve base model. Install the package and retry.") - if not config.base_model_name or not config.registry_name: - raise RuntimeError( - "Provide AZURE_ML_BASE_MODEL_ID or AZURE_ML_BASE_MODEL_NAME together with AZURE_ML_REGISTRY_NAME." - ) - registry_client = MLClient(credential, registry_name=config.registry_name) - try: - if config.base_model_version: - model = registry_client.models.get(config.base_model_name, version=config.base_model_version) - else: - model = registry_client.models.get(config.base_model_name, label=config.base_model_label) - except Exception as exc: # pragma: no cover - network dependent - raise RuntimeError( - f"Failed to locate base model '{config.base_model_name}' in registry '{config.registry_name}'." - ) from exc - model_id = getattr(model, "id", None) - if not model_id: - raise RuntimeError("Base model resolved from registry is missing an id.") - return model_id - - -def _build_job_identifiers(config: FineTuningConfig, language: str) -> Tuple[str, str, str]: - suffix = f"{language}-{config.dataset_version}-{uuid.uuid4().hex[:6]}".lower() - job_name = _sanitize_identifier(f"{config.job_prefix}-{suffix}", 60) - display_name = f"{config.job_prefix}-{language}-{config.dataset_version}"[:250] - output_prefix = _sanitize_identifier(f"{config.output_model_prefix}-{language}-{config.dataset_version}", 120) - return job_name, display_name, output_prefix - - -def _submit_job(language: str, config: FineTuningConfig, dataset: FineTuningDataset) -> FineTuningJobSummary: - if MLClient is None or create_finetuning_job is None or FineTuningTaskType is None: - raise RuntimeError( - "azure-ai-ml not available. Install 'azure-ai-ml>=1.19.0' to submit fine-tuning jobs." - ) - - credential = DefaultAzureCredential() - try: - workspace_client = MLClient( - credential=credential, - subscription_id=config.subscription_id, - resource_group_name=config.resource_group, - workspace_name=config.workspace_name, - ) - - model_id = _resolve_base_model_id(config, credential) - - train_asset_id = _register_dataset( - workspace_client, - dataset.train_path, - config.train_dataset_name, - config.dataset_version, - f"Training dataset generated from transliterations ({language}).", - ) - - validation_asset_id: Optional[str] = None - if dataset.validation_path and dataset.validation_count > 0 and config.validation_split > 0: - validation_asset_id = _register_dataset( - workspace_client, - dataset.validation_path, - config.validation_dataset_name, - config.dataset_version, - f"Validation dataset generated from transliterations ({language}).", - ) - - job_name, display_name, output_prefix = _build_job_identifiers(config, language) - - job_kwargs: Dict[str, Any] = { - "task": FineTuningTaskType.CHAT_COMPLETION, - "training_data": train_asset_id, - "model": model_id, - "display_name": display_name, - "name": job_name, - "experiment_name": config.experiment_name, - "output_model_name_prefix": output_prefix, - "hyperparameters": config.hyperparameters, - "tags": {"language": language, "source": "transliteracoes"}, - "properties": {"dataset_version": config.dataset_version}, - } - if validation_asset_id: - job_kwargs["validation_data"] = validation_asset_id - - job = create_finetuning_job(**job_kwargs) - created_job = workspace_client.jobs.create_or_update(job) - job_details = workspace_client.jobs.get(created_job.name) - - status = getattr(job_details, "status", getattr(created_job, "status", "unknown")) or "unknown" - outputs = getattr(created_job, "outputs", {}) if hasattr(created_job, "outputs") else {} - registered = outputs.get("registered_model") if isinstance(outputs, dict) else None - model_name = getattr(registered, "name", "") if registered else "" - if not model_name and hasattr(job_details, "outputs"): - registered_details = job_details.outputs.get("registered_model") if isinstance(job_details.outputs, dict) else None - if registered_details: - model_name = getattr(registered_details, "name", "") or getattr(registered_details, "asset_name", "") - - summary = FineTuningJobSummary( - job_name=created_job.name, - display_name=getattr(created_job, "display_name", display_name) or display_name, - status=status, - output_model_name=model_name or output_prefix, - training_examples=dataset.train_count, - validation_examples=dataset.validation_count, - submitted_at=datetime.utcnow().isoformat(timespec="seconds"), - language=language, - ) - LOGGER.info( - "Fine-tuning job submitted language=%s job=%s status=%s base_model=%s output_model=%s", - language, - summary.job_name, - summary.status, - model_id, - summary.output_model_name, - ) - return summary - finally: - try: - credential.close() - except Exception: # pragma: no cover - best effort cleanup - LOGGER.debug("Failed to close Azure credential", exc_info=True) + token = re.sub(r"[^0-9a-zA-Z-]+", "-", raw).strip("-") + token = re.sub(r"-+", "-", token) + return token[:max_length] if len(token) > max_length else token or "ft-job" + + +def _register_dataset( + client: Any, path: Path, name: str, version: str, description: str +) -> str: + if AssetTypes is None or Data is None: + raise RuntimeError( + "azure-ai-ml is required to register fine-tuning datasets. Install the package first." + ) + asset = Data( + path=str(path), + type=AssetTypes.URI_FILE, + description=description, + name=name, + version=version, + ) + created = client.data.create_or_update(asset) + return getattr(created, "id", None) or str(created) + + +def _resolve_base_model_id( + config: FineTuningConfig, credential: DefaultAzureCredential +) -> str: + if config.base_model_id: + return config.base_model_id + if MLClient is None: + raise RuntimeError( + "azure-ai-ml not available to resolve base model. Install the package and retry." + ) + if not config.base_model_name or not config.registry_name: + raise RuntimeError( + "Provide AZURE_ML_BASE_MODEL_ID or AZURE_ML_BASE_MODEL_NAME together with AZURE_ML_REGISTRY_NAME." + ) + registry_client = MLClient(credential, registry_name=config.registry_name) + try: + if config.base_model_version: + model = registry_client.models.get( + config.base_model_name, version=config.base_model_version + ) + else: + model = registry_client.models.get( + config.base_model_name, label=config.base_model_label + ) + except Exception as exc: # pragma: no cover - network dependent + raise RuntimeError( + f"Failed to locate base model '{config.base_model_name}' in registry '{config.registry_name}'." + ) from exc + model_id = getattr(model, "id", None) + if not model_id: + raise RuntimeError("Base model resolved from registry is missing an id.") + return model_id + + +def _build_job_identifiers( + config: FineTuningConfig, language: str +) -> Tuple[str, str, str]: + suffix = f"{language}-{config.dataset_version}-{uuid.uuid4().hex[:6]}".lower() + job_name = _sanitize_identifier(f"{config.job_prefix}-{suffix}", 60) + display_name = f"{config.job_prefix}-{language}-{config.dataset_version}"[:250] + output_prefix = _sanitize_identifier( + f"{config.output_model_prefix}-{language}-{config.dataset_version}", 120 + ) + return job_name, display_name, output_prefix + + +def _submit_job( + language: str, config: FineTuningConfig, dataset: FineTuningDataset +) -> FineTuningJobSummary: + if MLClient is None or create_finetuning_job is None or FineTuningTaskType is None: + raise RuntimeError( + "azure-ai-ml not available. Install 'azure-ai-ml>=1.19.0' to submit fine-tuning jobs." + ) + + credential = DefaultAzureCredential() + try: + workspace_client = MLClient( + credential=credential, + subscription_id=config.subscription_id, + resource_group_name=config.resource_group, + workspace_name=config.workspace_name, + ) + + model_id = _resolve_base_model_id(config, credential) + + train_asset_id = _register_dataset( + workspace_client, + dataset.train_path, + config.train_dataset_name, + config.dataset_version, + f"Training dataset generated from transliterations ({language}).", + ) + + validation_asset_id: Optional[str] = None + if ( + dataset.validation_path + and dataset.validation_count > 0 + and config.validation_split > 0 + ): + validation_asset_id = _register_dataset( + workspace_client, + dataset.validation_path, + config.validation_dataset_name, + config.dataset_version, + f"Validation dataset generated from transliterations ({language}).", + ) + + job_name, display_name, output_prefix = _build_job_identifiers(config, language) + + job_kwargs: Dict[str, Any] = { + "task": FineTuningTaskType.CHAT_COMPLETION, + "training_data": train_asset_id, + "model": model_id, + "display_name": display_name, + "name": job_name, + "experiment_name": config.experiment_name, + "output_model_name_prefix": output_prefix, + "hyperparameters": config.hyperparameters, + "tags": {"language": language, "source": "transliteracoes"}, + "properties": {"dataset_version": config.dataset_version}, + } + if validation_asset_id: + job_kwargs["validation_data"] = validation_asset_id + + job = create_finetuning_job(**job_kwargs) + created_job = workspace_client.jobs.create_or_update(job) + job_details = workspace_client.jobs.get(created_job.name) + + status = ( + getattr(job_details, "status", getattr(created_job, "status", "unknown")) + or "unknown" + ) + outputs = ( + getattr(created_job, "outputs", {}) + if hasattr(created_job, "outputs") + else {} + ) + registered = ( + outputs.get("registered_model") if isinstance(outputs, dict) else None + ) + model_name = getattr(registered, "name", "") if registered else "" + if not model_name and hasattr(job_details, "outputs"): + registered_details = ( + job_details.outputs.get("registered_model") + if isinstance(job_details.outputs, dict) + else None + ) + if registered_details: + model_name = getattr(registered_details, "name", "") or getattr( + registered_details, "asset_name", "" + ) + + summary = FineTuningJobSummary( + job_name=created_job.name, + display_name=getattr(created_job, "display_name", display_name) + or display_name, + status=status, + output_model_name=model_name or output_prefix, + training_examples=dataset.train_count, + validation_examples=dataset.validation_count, + submitted_at=datetime.utcnow().isoformat(timespec="seconds"), + language=language, + ) + LOGGER.info( + "Fine-tuning job submitted language=%s job=%s status=%s base_model=%s output_model=%s", + language, + summary.job_name, + summary.status, + model_id, + summary.output_model_name, + ) + return summary + finally: + try: + credential.close() + except Exception: # pragma: no cover - best effort cleanup + LOGGER.debug("Failed to close Azure credential", exc_info=True) def _persist_summary(summary: FineTuningJobSummary) -> Path: - TRANSLATIONS_OUTPUT_DIR.mkdir(exist_ok=True, parents=True) - path = TRANSLATIONS_OUTPUT_DIR / f"finetune_{summary.language}_{summary.job_name}.json" - with open(path, "w", encoding="utf-8") as handle: - json.dump(asdict(summary), handle, ensure_ascii=False, indent=2) - latest_path = TRANSLATIONS_OUTPUT_DIR / "latest.json" - with open(latest_path, "w", encoding="utf-8") as handle: - json.dump(asdict(summary), handle, ensure_ascii=False, indent=2) - return path + TRANSLATIONS_OUTPUT_DIR.mkdir(exist_ok=True, parents=True) + path = ( + TRANSLATIONS_OUTPUT_DIR / f"finetune_{summary.language}_{summary.job_name}.json" + ) + with open(path, "w", encoding="utf-8") as handle: + json.dump(asdict(summary), handle, ensure_ascii=False, indent=2) + latest_path = TRANSLATIONS_OUTPUT_DIR / "latest.json" + with open(latest_path, "w", encoding="utf-8") as handle: + json.dump(asdict(summary), handle, ensure_ascii=False, indent=2) + return path def run(language: str) -> Optional[FineTuningJobSummary]: - config = FineTuningConfig.from_env() - results = _load_transliteration_results(language) - dataset = _prepare_datasets(language, results, config) - summary = _submit_job(language, config, dataset) - persisted = _persist_summary(summary) - LOGGER.info("Fine-tuning metadata persisted at %s", persisted) - return summary + config = FineTuningConfig.from_env() + results = _load_transliteration_results(language) + dataset = _prepare_datasets(language, results, config) + summary = _submit_job(language, config, dataset) + persisted = _persist_summary(summary) + LOGGER.info("Fine-tuning metadata persisted at %s", persisted) + return summary def main(language: str = "matis") -> None: # pragma: no cover - summary = run(language) - if summary: - LOGGER.info( - "Fine-tuning queued: job=%s model=%s status=%s", - summary.job_name, - summary.output_model_name, - summary.status, - ) + summary = run(language) + if summary: + LOGGER.info( + "Fine-tuning queued: job=%s model=%s status=%s", + summary.job_name, + summary.output_model_name, + summary.status, + ) if __name__ == "__main__": # pragma: no cover - main() + main() diff --git a/apps/text_generation/src/pipelines/transliteracoes/runner.py b/apps/text_generation/src/pipelines/transliteracoes/runner.py index f6dfd51..263281a 100644 --- a/apps/text_generation/src/pipelines/transliteracoes/runner.py +++ b/apps/text_generation/src/pipelines/transliteracoes/runner.py @@ -7,10 +7,10 @@ from __future__ import annotations -import os -import re import asyncio import json +import os +import re import time from abc import ABC, abstractmethod from dataclasses import dataclass, field @@ -64,6 +64,7 @@ } DEFAULT_LANGUAGE_DICTIONARY = "" + class AgentTimeoutError(RuntimeError): """Raised when an agent run exceeds the maximum allowed duration.""" @@ -126,7 +127,9 @@ async def _resolve_references( except Exception as exc: # pragma: no cover - network failure path LOGGER.debug("Busca falhou query=%s err=%s", query, exc) return await _resolve_references(tool, queries, limit, current, keys, index + 1) - return await _merge_references(tool, queries, limit, current, keys, results, index, 0) + return await _merge_references( + tool, queries, limit, current, keys, results, index, 0 + ) async def _merge_references( @@ -141,7 +144,9 @@ async def _merge_references( ) -> List[Dict[str, Any]]: """Aplica deduplicação incremental das referências recuperadas, assegurando que apenas fontes relevantes alimentem o workflow.""" if result_index >= len(results) or len(current) >= limit: - return await _resolve_references(tool, queries, limit, current, keys, query_index + 1) + return await _resolve_references( + tool, queries, limit, current, keys, query_index + 1 + ) doc = results[result_index] or {} key = (doc.get("file_name"), doc.get("page_number")) if key not in keys: @@ -183,7 +188,9 @@ def __init__( self._agent_owned = agent_id is None self._poll_interval = max(poll_interval, 0.5) self._run_timeout = max(run_timeout, 60.0) - self._run_alert_threshold = min(max(run_alert_threshold, 0.0), self._run_timeout) + self._run_alert_threshold = min( + max(run_alert_threshold, 0.0), self._run_timeout + ) async def generate( self, @@ -353,14 +360,20 @@ async def _run_agent( messages = await asyncio.to_thread( lambda: list(self._client.agents.messages.list(thread_id=thread.id)) ) - assistant_text = self._extract_assistant_text(messages) or latest_assistant_text + assistant_text = ( + self._extract_assistant_text(messages) or latest_assistant_text + ) if not assistant_text: raise RuntimeError("Resposta do agente vazia") try: return _extract_json_block(assistant_text) except ValueError as exc: - LOGGER.error("[%s] saída não estruturada: %s", self._name, assistant_text[:200]) - raise RuntimeError("Resposta do agente fora do formato esperado") from exc + LOGGER.error( + "[%s] saída não estruturada: %s", self._name, assistant_text[:200] + ) + raise RuntimeError( + "Resposta do agente fora do formato esperado" + ) from exc finally: delete_thread = getattr(self._client.agents.threads, "delete", None) if callable(delete_thread): @@ -504,6 +517,7 @@ async def run(self, context: "TranslationWorkflow") -> Optional["WorkflowState"] @dataclass class TranslationWorkflow: """Coordena traduções, revisões e referências, controlando o ciclo que culmina na transliteração aceita ou rejeitada.""" + idioma: str dialogue: Dict[str, Any] translator: FoundryChatAgent @@ -593,7 +607,9 @@ async def _transition(self, state: Optional[WorkflowState]) -> None: """Avança para o próximo estado, garantindo que cada decisão seja aplicada na ordem correta para não distorcer o resultado.""" if state is None: return - LOGGER.debug("dialogo=%s estado=%s", _sanitize_dialogue_id(self.dialogue), state.name) + LOGGER.debug( + "dialogo=%s estado=%s", _sanitize_dialogue_id(self.dialogue), state.name + ) next_state = await state.run(self) await self._transition(next_state) @@ -640,6 +656,7 @@ def dialogue_id(self) -> str: class TranslatingState(WorkflowState): """Estado que solicita nova transliteração, determinando insumos iniciais que afetam todas as iterações subsequentes.""" + name = "traduzindo" async def run(self, context: TranslationWorkflow) -> Optional[WorkflowState]: @@ -674,6 +691,7 @@ async def run(self, context: TranslationWorkflow) -> Optional[WorkflowState]: class TranslatedState(WorkflowState): """Estado que avalia o texto traduzido, decidindo se deve ser aceito ou iterado novamente.""" + name = "traduzido" async def run(self, context: TranslationWorkflow) -> Optional[WorkflowState]: @@ -701,6 +719,7 @@ async def run(self, context: TranslationWorkflow) -> Optional[WorkflowState]: class RejectedState(WorkflowState): """Estado responsável por decidir retentativas, controlando limites para evitar resultados incompletos.""" + name = "rejeitado" async def run(self, context: TranslationWorkflow) -> Optional[WorkflowState]: @@ -724,6 +743,7 @@ async def run(self, context: TranslationWorkflow) -> Optional[WorkflowState]: class AcceptedState(WorkflowState): """Estado terminal que consolida o resultado aprovado e o torna disponível para uso downstream.""" + name = "aceito" async def run(self, context: TranslationWorkflow) -> Optional[WorkflowState]: @@ -797,7 +817,9 @@ async def _build_client( or settings.foundry.openai_endpoint ) if not endpoint: - raise RuntimeError("Azure Foundry Project endpoint não configurado (defina PROJECT_ENDPOINT)") + raise RuntimeError( + "Azure Foundry Project endpoint não configurado (defina PROJECT_ENDPOINT)" + ) credential = DefaultAzureCredential() project_client = AIProjectClient(endpoint=endpoint, credential=credential) @@ -844,7 +866,9 @@ async def _build_client( return project_client, translator_agent, reviewer_agent, credential -async def run_pipeline(idioma: str, settings: Optional[AppSettings] = None) -> List[Dict[str, Any]]: +async def run_pipeline( + idioma: str, settings: Optional[AppSettings] = None +) -> List[Dict[str, Any]]: """Executa o pipeline completo para um idioma, produzindo transliterações registradas que alimentam o produto final.""" cfg = settings or load_settings() dialogues = _load_dialogues() @@ -852,7 +876,9 @@ async def run_pipeline(idioma: str, settings: Optional[AppSettings] = None) -> L LOGGER.warning("Nenhum diálogo encontrado em %s", DIALOGUES_PATH) return [] - project_client, translator_agent, reviewer_agent, azure_credential = await _build_client(cfg, idioma) + project_client, translator_agent, reviewer_agent, azure_credential = ( + await _build_client(cfg, idioma) + ) search_tool = AzureAISearchTool( cfg.search.endpoint, @@ -875,7 +901,9 @@ async def run_pipeline(idioma: str, settings: Optional[AppSettings] = None) -> L try: result = await workflow.run() except Exception as exc: # pragma: no cover - defensive logging - LOGGER.exception("Falha no diálogo %s: %s", _sanitize_dialogue_id(dialogue), exc) + LOGGER.exception( + "Falha no diálogo %s: %s", _sanitize_dialogue_id(dialogue), exc + ) result = { "dialogo": dialogue, "idioma": idioma, @@ -907,7 +935,9 @@ def _persist_outputs(results: List[Dict[str, Any]]) -> None: for result in results: dialogue = result.get("dialogo", {}) dlg_id = _sanitize_dialogue_id(dialogue) - with open(OUTPUT_DIR / f"transliteration_{dlg_id}.json", "w", encoding="utf-8") as handle: + with open( + OUTPUT_DIR / f"transliteration_{dlg_id}.json", "w", encoding="utf-8" + ) as handle: json.dump(result, handle, ensure_ascii=False, indent=2) LOGGER.info("Resultados persistidos em %s", OUTPUT_DIR) diff --git a/apps/text_generation/src/prompts.py b/apps/text_generation/src/prompts.py index cf33ae2..1648061 100644 --- a/apps/text_generation/src/prompts.py +++ b/apps/text_generation/src/prompts.py @@ -1,20 +1,21 @@ import sys from pathlib import Path -from jinja2 import Template -from semantic_kernel.kernel_pydantic import KernelBaseModel +from jinja2 import Template base = Path(__file__).parent / "data" / "translation" ROOT = Path(__file__).resolve().parents[4] -SRC = ROOT / 'src/backend' -base = ROOT / 'notebooks/data/translation' +SRC = ROOT / "src/backend" +base = ROOT / "notebooks/data/translation" sys.path.append(str(SRC)) + def render_prompt(template_str, **kwargs): template = Template(template_str) return template.render(**kwargs) + BASE_PROMPT_TEMPLATE = """ ────────────────────────────────────────────────────────────────────────────── Contexto base (índice vetorial Azure AI Search) @@ -118,683 +119,683 @@ def render_prompt(template_str, **kwargs): {{ language_dictionary }} **EXEMPLOS DE TRADUÇÃO** -[ - { - "numero": 1, - "portugues": "Digite ou diga seu CPF", - "matis": "Min CPF dadawata apadermen ikkin CPF numero Txuita", - "observacoes": [ - "A palavra 'dadawata' indica 'digitar' ou 'informar' e é derivada da ação de escrever.", - "O termo 'apadermen' traduz 'dizer' e é usado como forma verbal.", - "O número do CPF é tratado como um empréstimo direto do português, mantendo sua pronúncia original." - ] - }, - { - "numero": 2, - "portugues": "Não sei meu CPF", - "matis": "Nukun CPF tanawamen ëmbi.", - "observacoes": [ - "A palavra 'tanawamen' é formada pela raiz 'tanawa' (saber) com o sufixo '-men' para negação.", - "O termo 'nukun' é um pronome que significa 'meu' e aparece no início da frase." - ] - }, - { - "numero": 3, - "portugues": "Coloque seu dedo no leitor como na imagem", - "matis": "Min mëkën sukanta leitor no imagem bedatnu.", - "observacoes": [ - "O termo 'mëkën' refere-se a 'dedo' e é usado como substantivo singular.", - "O verbo 'sukanta' indica a ação de colocar ou posicionar algo.", - "A palavra 'imagem' foi mantida como empréstimo do português." - ] - }, - { - "numero": 4, - "portugues": "Seu saldo atual é de* ....", - "matis": "Min saldo nëbi tet in ikedak", - "observacoes": [ - "O termo 'saldo' é um empréstimo direto do português.", - "A expressão 'nëbi tet in' indica 'atual', enquanto 'ikedak' sugere um estado ou condição." - ] - }, - { - "numero": 5, - "portugues": "Por favor, aguarde mais um pouco*...", - "matis": "Tximota sotanbotsëkta", - "observacoes": [ - "A palavra 'tximota' é lexicalizada e usada para indicar polidez.", - "O verbo 'sotanbotsëkta' inclui o sufixo '-këta', que sugere um pedido ou espera prolongada." - ] - }, - { - "numero": 6, - "portugues": "Sua conta está bloqueada", - "matis": "Min conta bloqueapak", - "observacoes": [ - "O verbo 'bloqueapak' é derivado da raiz 'bloquea' (bloquear) com o sufixo '-pak', que indica estado ou condição." - ] - }, - { - "numero": 7, - "portugues": "Sua senha precisa ser trocada", - "matis": "Min senha wëtsiwatepak", - "observacoes": [ - "O termo 'wëtsi' significa 'trocar' ou 'alterar'.", - "O sufixo '-pak' marca uma necessidade ou obrigação no contexto." - ] - }, - { - "numero": 8, - "portugues": "Seus dados precisam ser atualizados", - "matis": "Min dados bëdawakin naknentepat", - "observacoes": [ - "A palavra 'bëdawakin' indica 'atualizar' e é formada pela raiz 'bëda' com o sufixo '-kin'.", - "O sufixo '-pat' sugere obrigação ou necessidade no contexto da frase." - ] - }, - { - "numero": 9, - "portugues": "Deseja desbloquear seu cartão?", - "matis": "Min cartão bloqueanu kek mibi?", - "observacoes": [ - "O verbo 'bloqueanu' é formado pela raiz 'bloquea' com o sufixo '-nu', indicando reversão do estado de bloqueio.", - "O termo 'kek mibi' adiciona intenção ou pergunta ao contexto." - ] - }, - { - "numero": 10, - "portugues": "Deseja falar com o gerente?", - "matis": "Gerente bët onkenu kek mibi?", - "observacoes": [ - "O termo 'bët' refere-se ao gerente como sujeito explícito.", - "O verbo 'onkenu' sugere a ação de falar ou comunicar." - ] - }, - { - "numero": 11, - "portugues": "Atendimento encerrado. A CAIXA agradece", - "matis": "Atendeakiti wesadax. CAIXA no.", - "observacoes": [ - "A expressão 'wesadax' indica conclusão ou término.", - "O nome 'CAIXA' é mantido como empréstimo direto do português." - ] - }, - { - "numero": 12, - "portugues": "Quero ver meu saldo", - "matis": "Nunkun saldo isnukek ëbi.", - "observacoes": [ - "O termo 'saldo' é traduzido diretamente como empréstimo.", - "O verbo 'isnukek' indica a ação de visualizar ou consultar algo." - ] - }, - { - "numero": 13, - "portugues": "Meu cartão está com problema", - "matis": "Nukun cartão bëdapimenpa", - "observacoes": [ - "O termo 'bëdapimenpa' sugere que algo não está funcionando corretamente.", - "O pronome 'nukun' indica posse, traduzido como 'meu'." - ] - }, - { - "numero": 14, - "portugues": "Quero trocar minha senha", - "matis": "Nukun senha wëtsiwanu kek ëbi", - "observacoes": [ - "O verbo 'wëtsiwanu' descreve a ação de alterar ou trocar.", - "O termo 'senha' é um empréstimo direto do português." - ] - }, - { - "numero": 15, - "portugues": "Perdi meu cartão", - "matis": "Nunkun cartão amawabok ëmbi", - "observacoes": [ - "A palavra 'amawabok' é formada pela raiz 'ama' (perder) e o sufixo '-wabok', indicando passado.", - "O pronome 'nunkun' indica posse ou pertencimento." - ] - }, - { - "numero": 16, - "portugues": "Sim", - "matis": "Ain", - "observacoes": [ - "A palavra 'Ain' é usada diretamente como afirmação.", - "Não há necessidade de modificadores na tradução." - ] - }, - { - "numero": 17, - "portugues": "Não", - "matis": "Bama", - "observacoes": [ - "O termo 'Bama' expressa negação direta ou ausência.", - "Pode ser usado como resposta curta ou em contexto mais amplo." - ] - }, - { - "numero": 18, - "portugues": "Olá, estou aqui para te ajudar", - "matis": "Eh, në abi ëbi mibi dabëtwakit.", - "observacoes": [ - "A expressão 'dabëtwakit' sugere ação ou ajuda em contexto de interação.", - "O pronome 'abi' refere-se à localização ou presença." - ] - }, - { - "numero": 19, - "portugues": "Você quer desbloquear o cartão?", - "matis": "Cartão desbloqueanukek mibi?", - "observacoes": [ - "O verbo 'desbloqueanukek' descreve ação de liberar ou desbloquear.", - "A estrutura mantém a pergunta como no português." - ] - }, - { - "numero": 20, - "portugues": "Você quer trocar a senha?", - "matis": "Senha wëtsiwanukek mibi?", - "observacoes": [ - "O verbo 'wëtsiwanukek' sugere ação de troca ou alteração.", - "A palavra 'senha' foi mantida como empréstimo do português." - ] - }, - { - "numero": 21, - "portugues": "Você quer atualizar seus dados?", - "matis": "Min dados bëdawanukek ëbi?", - "observacoes": [ - "O termo 'bëdawanukek' combina a raiz 'bëdawa' (fazer bom) com o sufixo '-nukek' (intenção ou pergunta).", - "O pronome 'min' indica posse, traduzido como 'seus'.", - "O uso de 'dados' é um empréstimo do português e refere-se a informações pessoais." - ] - }, - { - "numero": 22, - "portugues": "Por favor, coloque o cartão no totem", - "matis": "Cartão txokonta totennën.", - "observacoes": [ - "O verbo 'txokonta' descreve a ação de colocar algo em um lugar específico.", - "O termo 'totennën' é um empréstimo do português, adaptado à fonética Matis.", - "A estrutura mantém a ordem O-V (Objeto-Verbo)." - ] - }, - { - "numero": 23, - "portugues": "Aguarde um momento, estou resolvendo", - "matis": "Sotanbota, nakneneke ëmbi ikek", - "observacoes": [ - "O termo 'sotanbota' é usado para indicar espera e inclui o sufixo '-bota' (duração).", - "O verbo 'nakneneke' sugere o ato de resolver ou trabalhar em algo.", - "A expressão 'ëmbi ikek' indica 'eu estou', com o sujeito implícito." - ] - }, - { - "numero": 24, - "portugues": "Resolvi o problema. Está tudo certo agora", - "matis": "Isama ikakit nanenak enbi. Nëbi bëda.", - "observacoes": [ - "A palavra 'isama' indica solução ou resolução.", - "O verbo 'ikakit' sugere conclusão de uma ação.", - "A expressão 'nëbi bëda' traduz a ideia de que tudo está bem ou correto." - ] - }, - { - "numero": 25, - "portugues": "O próximo depósito será feito no dia [dia][mês], daqui a [dias] dias", - "matis": "Nëbi depositatekit in nëkit nëtën ikek, mês ikek kek", - "observacoes": [ - "A palavra 'depositatekit' é formada pela raiz 'deposita' com o sufixo '-tekit', que indica ação futura.", - "A estrutura temporal utiliza 'nëkit' (dia) e 'mês', que são empréstimos do português.", - "O sufixo '-kek' no final da frase sinaliza pergunta ou intenção." - ] - }, - { - "numero": 26, - "portugues": "Quer ver se está tudo certo com a sua conta?", - "matis": "Min conta bëdara ikek isnukek?", - "observacoes": [ - "O verbo 'isnukek' indica a ação de verificar ou visualizar.", - "A expressão 'bëdara ikek' sugere que algo está em ordem ou correto.", - "A palavra 'conta' é um empréstimo do português, adaptado ao contexto bancário." - ] - }, - { - "numero": 27, - "portugues": "Só um momento, estou vendo se tem algum problema na sua conta", - "matis": "Tximota, min conta bëdada ikek isbono.", - "observacoes": [ - "O termo 'tximota' indica espera ou paciência.", - "A expressão 'bëdada ikek' sugere a possibilidade de um problema na conta.", - "O verbo 'isbono' indica a ação de verificar ou investigar." - ] - }, - { - "numero": 28, - "portugues": "Nenhum problema com os seus dados", - "matis": "Min dados isamama bëdabi?", - "observacoes": [ - "O termo 'isamama' indica ausência de problema ou erro.", - "O sufixo '-bi' é usado para marcar uma pergunta ou confirmação.", - "A palavra 'dados' é um empréstimo do português, utilizado para informações pessoais." - ] - }, - { - "numero": 29, - "portugues": "Nenhum problema com o seu cartão", - "matis": "Min cartão bëdabi, isamama?", - "observacoes": [ - "A expressão 'bëdabi' indica ausência de erro ou problema.", - "O termo 'isamama' reforça a ideia de que está tudo bem.", - "O uso de 'cartão' é um empréstimo direto do português." - ] - }, - { - "numero": 30, - "portugues": "Nenhum problema com a sua senha", - "matis": "Min senha bëdabi, isamama?", - "observacoes": [ - "O verbo 'bëdabi' sugere que algo está funcionando corretamente.", - "A palavra 'senha' é mantida como empréstimo do português.", - "O termo 'isamama' é usado para confirmar que não há problemas." - ] - }, - { - "numero": 31, - "portugues": "Estamos ligando para um gerente", - "matis": "Gerente ligabono?", - "observacoes": [ - "O verbo 'ligabono' indica a ação de fazer uma ligação ou chamada.", - "O termo 'gerente' é usado como empréstimo do português, adaptado ao contexto bancário." - ] - }, - { - "numero": 32, - "portugues": "Você trouxe um documento com foto?", - "matis": "Minbi documento foto txokit bëak?", - "observacoes": [ - "O verbo 'txokit' descreve a ação de trazer ou levar algo.", - "A palavra 'documento' e 'foto' são empréstimos diretos do português.", - "O sufixo '-këak' marca uma pergunta ou dúvida." - ] - }, - { - "numero": 33, - "portugues": "Seu cadastro está incompleto, precisamos atualizá-lo.", - "matis": "Min cadastro weskin naknenamapa, bëdawakin naknennuk.", - "observacoes": [ - "O verbo 'weskin' indica estado de incompletude ou falta.", - "A expressão 'bëdawakin' sugere a ação de atualizar algo.", - "O sufixo '-nuk' marca intenção ou necessidade no contexto." - ] - }, - { - "numero": 34, - "portugues": "Você já fez o cadastro no aplicativo Caixa Tem?", - "matis": "Aplicativo Caixa tem nënda minbi cadastro nakabo?", - "observacoes": [ - "O termo 'nënda' traduz 'já' e indica ação concluída.", - "A palavra 'cadastro' é um empréstimo adaptado do português.", - "O verbo 'nakabo' sugere ação completada ou feita anteriormente." - ] - }, - { - "numero": 35, - "portugues": "Você mudou de endereço recentemente?", - "matis": "Nëbi mini endereço wëtsino mannëatbok?", - "observacoes": [ - "O verbo 'wëtsino' descreve a ação de mudar ou alterar.", - "A palavra 'endereço' é usada como empréstimo direto do português.", - "O sufixo '-bok' indica passado recente ou ação já realizada." - ] - }, - { - "numero": 36, - "portugues": "Seu cartão foi cancelado por segurança.", - "matis": "Min cartão bëdek canceladak.", - "observacoes": [ - "O verbo 'canceladak' descreve a ação de cancelar ou invalidar algo.", - "O termo 'bëdek' indica causa ou motivo, como segurança neste caso." - ] - }, - { - "numero": 37, - "portugues": "Você quer pedir um novo cartão?", - "matis": "Mibi cartão paxa pediwanukek?", - "observacoes": [ - "O termo 'paxa' indica 'novo' ou 'recente'.", - "O verbo 'pediwanukek' descreve a ação de solicitar ou pedir algo." - ] - }, - { - "numero": 38, - "portugues": "Sua conta está ativa e funcionando normalmente.", - "matis": "Min conta ativak, bedek funcionaek?", - "observacoes": [ - "O termo 'ativak' indica estado ativo ou disponível.", - "O verbo 'funcionaek' sugere que algo está operando corretamente." - ] - }, - { - "numero": 39, - "portugues": "Você quer abrir uma conta poupança?", - "matis": "Mibi conta poupança abrir nukek?", - "observacoes": [ - "O verbo 'abrir' é usado diretamente como empréstimo do português.", - "O termo 'poupança' foi adaptado ao contexto financeiro." - ] - }, - { - "numero": 40, - "portugues": "Seu benefício do Bolsa Família já foi depositado.", - "matis": "Min Bolsa Famílian benefício depositak.", - "observacoes": [ - "O verbo 'depositak' descreve a ação de realizar um depósito.", - "Os termos 'Bolsa Família' e 'benefício' são empréstimos do português." - ] - }, - { - "numero": 41, - "portugues": "O Auxílio Gás será pago na próxima semana.", - "matis": "Auxilio Gás sën semana wëtsin pagaedak.", - "observacoes": [ - "O termo 'Auxilio Gás' é mantido como empréstimo do português.", - "A expressão 'wëtsin' indica 'próximo' ou 'seguinte' e é adaptada ao contexto de tempo.", - "O verbo 'pagaedak' descreve a ação de pagamento futuro." - ] - }, - { - "numero": 42, - "portugues": "Você quer saber o valor do seu benefício?", - "matis": "Mibi min benefício awestenda ikek isnukek?", - "observacoes": [ - "O verbo 'awestenda' significa 'saber' ou 'conhecer' e está associado à obtenção de informações.", - "A palavra 'benefício' é um empréstimo do português, adaptado ao contexto financeiro.", - "O sufixo '-kek' é usado para marcar intenção ou pergunta." - ] - }, - { - "numero": 43, - "portugues": "Seu benefício está bloqueado por falta de atualização cadastral.", - "matis": "Min beneficio bloqueapak minbi atualizakin naknemapa ikak.", - "observacoes": [ - "O verbo 'bloqueapak' descreve o estado de bloqueio, indicando que o benefício não está disponível.", - "A expressão 'minbi atualizakin' indica a necessidade de atualização cadastral.", - "O sufixo '-ikak' reforça a ideia de estado ou condição." - ] - }, - { - "numero": 44, - "portugues": "Você já acessou o Caixa Tem hoje?", - "matis": "Minbi nëbi isak Caixa tem nën?", - "observacoes": [ - "O termo 'nëbi' traduz 'hoje' e está relacionado ao contexto temporal.", - "O verbo 'isak' significa 'acessar' ou 'entrar' e é utilizado em contextos digitais.", - "A palavra 'Caixa Tem' é mantida como empréstimo direto do português, sendo uma marca registrada." - ] - }, - { - "numero": 45, - "portugues": "Quer ajuda para instalar o aplicativo no seu celular?", - "matis": "Dabët wa ëbi kek mibi aplicativo instalano min celulan?", - "observacoes": [ - "O verbo 'instalano' descreve a ação de instalar ou configurar algo.", - "O termo 'celulan' é um empréstimo do português, adaptado à fonética Matis.", - "A expressão 'dabët wa ëbi kek mibi' adiciona intenção de ajuda ou oferta." - ] - }, - { - "numero": 46, - "portugues": "Você pode consultar seu saldo pelo aplicativo.", - "matis": "Aplicativon min saldo consultar ta minbibi.", - "observacoes": [ - "O verbo 'consultar' é mantido como empréstimo direto do português.", - "O termo 'saldo' é usado como um conceito financeiro, também emprestado do português.", - "A palavra 'aplicativo' foi adaptada ao uso tecnológico em Matis." - ] - }, - { - "numero": 47, - "portugues": "O aplicativo está fora do ar no momento, tente mais tarde.", - "matis": "Aplicativo nebi fora do ar dapa, txitxin tan uata.", - "observacoes": [ - "A expressão 'fora do ar' é mantida como empréstimo do português e traduzida diretamente.", - "O termo 'txitxin tan uata' descreve a ideia de 'tentar mais tarde' ou 'aguardar'.", - "A palavra 'aplicativo' foi lexicalizada e adaptada ao contexto digital." - ] - }, - { - "numero": 48, - "portugues": "Quer imprimir o extrato da sua conta?", - "matis": "Min contan extrato imprimir nukek?", - "observacoes": [ - "O verbo 'imprimir' é mantido como empréstimo direto do português.", - "O termo 'extrato' é usado como conceito financeiro e adaptado ao contexto bancário.", - "O sufixo '-kek' marca a intenção ou pergunta." - ] - }, - { - "numero": 49, - "portugues": "Você precisa de um comprovante de recebimento?", - "matis": "Mibi comprovante minbi bedakit betnukek?", - "observacoes": [ - "O termo 'comprovante' é mantido como empréstimo do português.", - "O verbo 'bedakit' descreve a ação de mostrar ou confirmar algo.", - "O sufixo '-nukek' marca intenção ou necessidade no contexto." - ] - }, - { - "numero": 50, - "portugues": "O comprovante será enviado por SMS.", - "matis": "SMSsin bin comprovante koanedak.", - "observacoes": [ - "O termo 'SMSsin bin' traduz 'por SMS' e é adaptado ao contexto tecnológico.", - "O verbo 'koanedak' descreve a ação de enviar ou transmitir algo.", - "A palavra 'comprovante' foi mantida como empréstimo direto do português." - ] - }, - { - "numero": 51, - "portugues": "Você quer ver os depósitos dos últimos três meses?", - "matis": "Mibi nëbi minbi mëkën tet uxë depositabokit isnukek?", - "observacoes": [ - "O termo 'mëkën tet uxë' traduz 'últimos três meses' e reflete um período temporal.", - "O verbo 'depositabokit' descreve depósitos realizados no passado recente.", - "O sufixo '-kek' marca intenção ou pergunta." - ] - }, - { - "numero": 52, - "portugues": "Vou te encaminhar para o atendimento presencial.", - "matis": "Mibi buannu abinokimoxon mibi atentenu.", - "observacoes": [ - "O verbo 'abinokimoxon' descreve a ação de encaminhar ou direcionar alguém.", - "O termo 'atentenu' refere-se ao atendimento, mantido como empréstimo do português.", - "A expressão 'buannu' indica 'vou' ou 'irei fazer algo'." - ] - }, - { - "numero": 53, - "portugues": "Aguarde, o gerente virá te atender.", - "matis": "Sotanta, gerente txoek mibi atendek.", - "observacoes": [ - "O termo 'sotanta' sugere paciência ou espera.", - "O verbo 'txoek' indica a ação de vir ou aproximar-se.", - "A palavra 'gerente' é mantida como empréstimo direto do português." - ] - }, - { - "numero": 54, - "portugues": "Você precisa ir até o CRAS para atualizar seus dados.", - "matis": "CRAS no kuantanta min dados bëdawamek.", - "observacoes": [ - "O termo 'kuantanta' traduz 'ir até' e sugere deslocamento.", - "A expressão 'bëdawamek' indica a ação de atualizar ou corrigir algo.", - "A palavra 'CRAS' é mantida como empréstimo direto do português, sendo um nome próprio." - ] - }, - { - "numero": 55, - "portugues": "Esse atendimento precisa ser feito com agendamento.", - "matis": "Nëkit atendimento pukinkin agendakin nëndoxonbin nakaedak.", - "observacoes": [ - "A expressão 'pukinkin agendakin' descreve a necessidade de agendamento.", - "O verbo 'nakaedak' indica que algo precisa ser realizado ou concluído.", - "A palavra 'atendimento' foi mantida como empréstimo direto do português." - ] - }, - { - "numero": 56, - "portugues": "Posso te ajudar com mais alguma coisa?", - "matis": "Abi ëbi kek mëkën dabët wanukek?", - "observacoes": [ - "O termo 'abi ëbi' indica uma oferta de ajuda, traduzido como 'eu posso te ajudar'.", - "O sufixo '-kek' marca a intenção ou a pergunta.", - "O verbo 'wanukek' sugere disponibilidade ou oferta de realizar uma ação." - ] - }, - { - "numero": 57, - "portugues": "Tudo bem com você?", - "matis": "Nëbi bëdabi ëbi?", - "observacoes": [ - "A expressão 'bëdabi' indica que algo está bem ou correto.", - "O pronome 'ëbi' refere-se diretamente ao interlocutor ('você').", - "A estrutura da frase segue o padrão interrogativo do Matis, com a pergunta no final." - ] - }, - { - "numero": 58, - "portugues": "Seu benefício será cancelado se não for atualizado.", - "matis": "Min benefício bëdek cancelanuk minbi bëdawamek?", - "observacoes": [ - "O verbo 'cancelanuk' descreve a ação de cancelar no futuro, com o sufixo '-nuk'.", - "O termo 'bëdawamek' indica a necessidade de atualização.", - "A palavra 'benefício' é mantida como empréstimo direto do português." - ] - }, - { - "numero": 59, - "portugues": "Você já recebeu o Auxílio Emergencial?", - "matis": "Minbi nëbi Auxílio Emergencial bedatekit?", - "observacoes": [ - "O verbo 'bedatekit' descreve a ação de receber algo.", - "Os termos 'Auxílio Emergencial' são mantidos como empréstimos diretos do português.", - "A estrutura da pergunta segue o padrão do Matis, com a partícula interrogativa implícita." - ] - }, - { - "numero": 60, - "portugues": "Por favor, digite novamente sua senha.", - "matis": "Tximota, min senha wëtsiwata dadawanuk.", - "observacoes": [ - "O termo 'tximota' indica uma solicitação ou pedido educado.", - "O verbo 'wëtsiwata' descreve a ação de alterar ou confirmar algo.", - "O verbo 'dadawanuk' inclui o sufixo '-nuk', que marca repetição ou intenção." - ] - }, - { - "numero": 61, - "portugues": "Seu cartão não está funcionando.", - "matis": "Nukun cartão bëdabi nëbi iksamak.", - "observacoes": [ - "A expressão 'bëdabi nëbi iksamak' descreve o estado de algo que não está funcionando corretamente.", - "O termo 'iksamak' sugere um problema ou falha.", - "A palavra 'cartão' é mantida como empréstimo direto do português." - ] - }, - { - "numero": 62, - "portugues": "Você quer saber o saldo disponível?", - "matis": "Mibi min saldo nëbi isnukek?", - "observacoes": [ - "O verbo 'isnukek' indica a ação de consultar ou verificar algo.", - "A palavra 'saldo' é usada como empréstimo do português e refere-se a dinheiro disponível.", - "O sufixo '-kek' marca a intenção ou pergunta no contexto." - ] - }, - { - "numero": 63, - "portugues": "O sistema está fora do ar no momento.", - "matis": "Sistema nëbi fora do ar dapa.", - "observacoes": [ - "A palavra 'sistema' é mantida como empréstimo direto do português.", - "A expressão 'fora do ar' é uma tradução direta e adaptada ao contexto técnico.", - "O termo 'dapa' indica o estado atual ou presente." - ] - }, - { - "numero": 64, - "portugues": "Você já cadastrou sua biometria?", - "matis": "Minbi biometria nëbi cadastro nakabo?", - "observacoes": [ - "O verbo 'nakabo' descreve a ação de cadastrar ou registrar algo no passado.", - "A palavra 'biometria' é mantida como empréstimo direto do português.", - "O termo 'nëbi' indica temporalidade, traduzido como 'já'." - ] - }, - { - "numero": 65, - "portugues": "Quer saber se há depósitos recentes na sua conta?", - "matis": "Mibi nëbi min contan mëkën depositak isnukek?", - "observacoes": [ - "O verbo 'depositak' indica a ação de depósitos realizados.", - "A expressão 'mëkën' traduz 'recentes' e está relacionada ao tempo.", - "O sufixo '-kek' marca intenção ou pergunta no contexto." - ] - }, - { - "numero": 66, - "portugues": "O valor do benefício será atualizado.", - "matis": "Min benefício bëdawakin nëbi atualizak.", - "observacoes": [ - "O verbo 'atualizak' descreve a ação de atualizar algo.", - "O termo 'bëdawakin' indica uma melhoria ou correção.", - "A palavra 'benefício' é usada como empréstimo do português." - ] - }, - { - "numero": 67, - "portugues": "Seu cadastro foi concluído com sucesso.", - "matis": "Min cadastro nëbi nakabo bëdaik.", - "observacoes": [ - "O verbo 'nakabo' indica uma ação concluída no passado.", - "A expressão 'bëdaik' traduz 'com sucesso', indicando um resultado positivo.", - "O termo 'cadastro' é mantido como empréstimo direto do português." - ] - }, - { - "numero": 68, - "portugues": "Quer saber como acessar o aplicativo?", - "matis": "Mibi nëbi aplikativon isak isnukek?", - "observacoes": [ - "O verbo 'isak' indica a ação de acessar ou entrar.", - "A palavra 'aplicativo' é mantida como empréstimo do português.", - "O sufixo '-kek' marca intenção ou pergunta no contexto." - ] - }, - { - "numero": 69, - "portugues": "Sua conta foi desbloqueada com sucesso.", - "matis": "Min conta bëdawanu nëbi bëdaik.", - "observacoes": [ - "O verbo 'bëdawanu' descreve a ação de desbloqueio.", - "A expressão 'bëdaik' traduz 'com sucesso' e indica um resultado positivo.", - "A palavra 'conta' é mantida como empréstimo direto do português." - ] - }, - { - "numero": 70, - "portugues": "Você quer saber o próximo passo?", - "matis": "Mibi nëbi wëtsin bëdara isnukek?", - "observacoes": [ - "A palavra 'wëtsin' traduz 'próximo' ou 'seguinte'.", - "O termo 'bëdara' indica 'passo' ou 'etapa'.", - "O sufixo '-kek' marca a intenção de perguntar ou oferecer algo." - ] - } -] +[ + { + "numero": 1, + "portugues": "Digite ou diga seu CPF", + "matis": "Min CPF dadawata apadermen ikkin CPF numero Txuita", + "observacoes": [ + "A palavra 'dadawata' indica 'digitar' ou 'informar' e é derivada da ação de escrever.", + "O termo 'apadermen' traduz 'dizer' e é usado como forma verbal.", + "O número do CPF é tratado como um empréstimo direto do português, mantendo sua pronúncia original." + ] + }, + { + "numero": 2, + "portugues": "Não sei meu CPF", + "matis": "Nukun CPF tanawamen ëmbi.", + "observacoes": [ + "A palavra 'tanawamen' é formada pela raiz 'tanawa' (saber) com o sufixo '-men' para negação.", + "O termo 'nukun' é um pronome que significa 'meu' e aparece no início da frase." + ] + }, + { + "numero": 3, + "portugues": "Coloque seu dedo no leitor como na imagem", + "matis": "Min mëkën sukanta leitor no imagem bedatnu.", + "observacoes": [ + "O termo 'mëkën' refere-se a 'dedo' e é usado como substantivo singular.", + "O verbo 'sukanta' indica a ação de colocar ou posicionar algo.", + "A palavra 'imagem' foi mantida como empréstimo do português." + ] + }, + { + "numero": 4, + "portugues": "Seu saldo atual é de* ....", + "matis": "Min saldo nëbi tet in ikedak", + "observacoes": [ + "O termo 'saldo' é um empréstimo direto do português.", + "A expressão 'nëbi tet in' indica 'atual', enquanto 'ikedak' sugere um estado ou condição." + ] + }, + { + "numero": 5, + "portugues": "Por favor, aguarde mais um pouco*...", + "matis": "Tximota sotanbotsëkta", + "observacoes": [ + "A palavra 'tximota' é lexicalizada e usada para indicar polidez.", + "O verbo 'sotanbotsëkta' inclui o sufixo '-këta', que sugere um pedido ou espera prolongada." + ] + }, + { + "numero": 6, + "portugues": "Sua conta está bloqueada", + "matis": "Min conta bloqueapak", + "observacoes": [ + "O verbo 'bloqueapak' é derivado da raiz 'bloquea' (bloquear) com o sufixo '-pak', que indica estado ou condição." + ] + }, + { + "numero": 7, + "portugues": "Sua senha precisa ser trocada", + "matis": "Min senha wëtsiwatepak", + "observacoes": [ + "O termo 'wëtsi' significa 'trocar' ou 'alterar'.", + "O sufixo '-pak' marca uma necessidade ou obrigação no contexto." + ] + }, + { + "numero": 8, + "portugues": "Seus dados precisam ser atualizados", + "matis": "Min dados bëdawakin naknentepat", + "observacoes": [ + "A palavra 'bëdawakin' indica 'atualizar' e é formada pela raiz 'bëda' com o sufixo '-kin'.", + "O sufixo '-pat' sugere obrigação ou necessidade no contexto da frase." + ] + }, + { + "numero": 9, + "portugues": "Deseja desbloquear seu cartão?", + "matis": "Min cartão bloqueanu kek mibi?", + "observacoes": [ + "O verbo 'bloqueanu' é formado pela raiz 'bloquea' com o sufixo '-nu', indicando reversão do estado de bloqueio.", + "O termo 'kek mibi' adiciona intenção ou pergunta ao contexto." + ] + }, + { + "numero": 10, + "portugues": "Deseja falar com o gerente?", + "matis": "Gerente bët onkenu kek mibi?", + "observacoes": [ + "O termo 'bët' refere-se ao gerente como sujeito explícito.", + "O verbo 'onkenu' sugere a ação de falar ou comunicar." + ] + }, + { + "numero": 11, + "portugues": "Atendimento encerrado. A CAIXA agradece", + "matis": "Atendeakiti wesadax. CAIXA no.", + "observacoes": [ + "A expressão 'wesadax' indica conclusão ou término.", + "O nome 'CAIXA' é mantido como empréstimo direto do português." + ] + }, + { + "numero": 12, + "portugues": "Quero ver meu saldo", + "matis": "Nunkun saldo isnukek ëbi.", + "observacoes": [ + "O termo 'saldo' é traduzido diretamente como empréstimo.", + "O verbo 'isnukek' indica a ação de visualizar ou consultar algo." + ] + }, + { + "numero": 13, + "portugues": "Meu cartão está com problema", + "matis": "Nukun cartão bëdapimenpa", + "observacoes": [ + "O termo 'bëdapimenpa' sugere que algo não está funcionando corretamente.", + "O pronome 'nukun' indica posse, traduzido como 'meu'." + ] + }, + { + "numero": 14, + "portugues": "Quero trocar minha senha", + "matis": "Nukun senha wëtsiwanu kek ëbi", + "observacoes": [ + "O verbo 'wëtsiwanu' descreve a ação de alterar ou trocar.", + "O termo 'senha' é um empréstimo direto do português." + ] + }, + { + "numero": 15, + "portugues": "Perdi meu cartão", + "matis": "Nunkun cartão amawabok ëmbi", + "observacoes": [ + "A palavra 'amawabok' é formada pela raiz 'ama' (perder) e o sufixo '-wabok', indicando passado.", + "O pronome 'nunkun' indica posse ou pertencimento." + ] + }, + { + "numero": 16, + "portugues": "Sim", + "matis": "Ain", + "observacoes": [ + "A palavra 'Ain' é usada diretamente como afirmação.", + "Não há necessidade de modificadores na tradução." + ] + }, + { + "numero": 17, + "portugues": "Não", + "matis": "Bama", + "observacoes": [ + "O termo 'Bama' expressa negação direta ou ausência.", + "Pode ser usado como resposta curta ou em contexto mais amplo." + ] + }, + { + "numero": 18, + "portugues": "Olá, estou aqui para te ajudar", + "matis": "Eh, në abi ëbi mibi dabëtwakit.", + "observacoes": [ + "A expressão 'dabëtwakit' sugere ação ou ajuda em contexto de interação.", + "O pronome 'abi' refere-se à localização ou presença." + ] + }, + { + "numero": 19, + "portugues": "Você quer desbloquear o cartão?", + "matis": "Cartão desbloqueanukek mibi?", + "observacoes": [ + "O verbo 'desbloqueanukek' descreve ação de liberar ou desbloquear.", + "A estrutura mantém a pergunta como no português." + ] + }, + { + "numero": 20, + "portugues": "Você quer trocar a senha?", + "matis": "Senha wëtsiwanukek mibi?", + "observacoes": [ + "O verbo 'wëtsiwanukek' sugere ação de troca ou alteração.", + "A palavra 'senha' foi mantida como empréstimo do português." + ] + }, + { + "numero": 21, + "portugues": "Você quer atualizar seus dados?", + "matis": "Min dados bëdawanukek ëbi?", + "observacoes": [ + "O termo 'bëdawanukek' combina a raiz 'bëdawa' (fazer bom) com o sufixo '-nukek' (intenção ou pergunta).", + "O pronome 'min' indica posse, traduzido como 'seus'.", + "O uso de 'dados' é um empréstimo do português e refere-se a informações pessoais." + ] + }, + { + "numero": 22, + "portugues": "Por favor, coloque o cartão no totem", + "matis": "Cartão txokonta totennën.", + "observacoes": [ + "O verbo 'txokonta' descreve a ação de colocar algo em um lugar específico.", + "O termo 'totennën' é um empréstimo do português, adaptado à fonética Matis.", + "A estrutura mantém a ordem O-V (Objeto-Verbo)." + ] + }, + { + "numero": 23, + "portugues": "Aguarde um momento, estou resolvendo", + "matis": "Sotanbota, nakneneke ëmbi ikek", + "observacoes": [ + "O termo 'sotanbota' é usado para indicar espera e inclui o sufixo '-bota' (duração).", + "O verbo 'nakneneke' sugere o ato de resolver ou trabalhar em algo.", + "A expressão 'ëmbi ikek' indica 'eu estou', com o sujeito implícito." + ] + }, + { + "numero": 24, + "portugues": "Resolvi o problema. Está tudo certo agora", + "matis": "Isama ikakit nanenak enbi. Nëbi bëda.", + "observacoes": [ + "A palavra 'isama' indica solução ou resolução.", + "O verbo 'ikakit' sugere conclusão de uma ação.", + "A expressão 'nëbi bëda' traduz a ideia de que tudo está bem ou correto." + ] + }, + { + "numero": 25, + "portugues": "O próximo depósito será feito no dia [dia][mês], daqui a [dias] dias", + "matis": "Nëbi depositatekit in nëkit nëtën ikek, mês ikek kek", + "observacoes": [ + "A palavra 'depositatekit' é formada pela raiz 'deposita' com o sufixo '-tekit', que indica ação futura.", + "A estrutura temporal utiliza 'nëkit' (dia) e 'mês', que são empréstimos do português.", + "O sufixo '-kek' no final da frase sinaliza pergunta ou intenção." + ] + }, + { + "numero": 26, + "portugues": "Quer ver se está tudo certo com a sua conta?", + "matis": "Min conta bëdara ikek isnukek?", + "observacoes": [ + "O verbo 'isnukek' indica a ação de verificar ou visualizar.", + "A expressão 'bëdara ikek' sugere que algo está em ordem ou correto.", + "A palavra 'conta' é um empréstimo do português, adaptado ao contexto bancário." + ] + }, + { + "numero": 27, + "portugues": "Só um momento, estou vendo se tem algum problema na sua conta", + "matis": "Tximota, min conta bëdada ikek isbono.", + "observacoes": [ + "O termo 'tximota' indica espera ou paciência.", + "A expressão 'bëdada ikek' sugere a possibilidade de um problema na conta.", + "O verbo 'isbono' indica a ação de verificar ou investigar." + ] + }, + { + "numero": 28, + "portugues": "Nenhum problema com os seus dados", + "matis": "Min dados isamama bëdabi?", + "observacoes": [ + "O termo 'isamama' indica ausência de problema ou erro.", + "O sufixo '-bi' é usado para marcar uma pergunta ou confirmação.", + "A palavra 'dados' é um empréstimo do português, utilizado para informações pessoais." + ] + }, + { + "numero": 29, + "portugues": "Nenhum problema com o seu cartão", + "matis": "Min cartão bëdabi, isamama?", + "observacoes": [ + "A expressão 'bëdabi' indica ausência de erro ou problema.", + "O termo 'isamama' reforça a ideia de que está tudo bem.", + "O uso de 'cartão' é um empréstimo direto do português." + ] + }, + { + "numero": 30, + "portugues": "Nenhum problema com a sua senha", + "matis": "Min senha bëdabi, isamama?", + "observacoes": [ + "O verbo 'bëdabi' sugere que algo está funcionando corretamente.", + "A palavra 'senha' é mantida como empréstimo do português.", + "O termo 'isamama' é usado para confirmar que não há problemas." + ] + }, + { + "numero": 31, + "portugues": "Estamos ligando para um gerente", + "matis": "Gerente ligabono?", + "observacoes": [ + "O verbo 'ligabono' indica a ação de fazer uma ligação ou chamada.", + "O termo 'gerente' é usado como empréstimo do português, adaptado ao contexto bancário." + ] + }, + { + "numero": 32, + "portugues": "Você trouxe um documento com foto?", + "matis": "Minbi documento foto txokit bëak?", + "observacoes": [ + "O verbo 'txokit' descreve a ação de trazer ou levar algo.", + "A palavra 'documento' e 'foto' são empréstimos diretos do português.", + "O sufixo '-këak' marca uma pergunta ou dúvida." + ] + }, + { + "numero": 33, + "portugues": "Seu cadastro está incompleto, precisamos atualizá-lo.", + "matis": "Min cadastro weskin naknenamapa, bëdawakin naknennuk.", + "observacoes": [ + "O verbo 'weskin' indica estado de incompletude ou falta.", + "A expressão 'bëdawakin' sugere a ação de atualizar algo.", + "O sufixo '-nuk' marca intenção ou necessidade no contexto." + ] + }, + { + "numero": 34, + "portugues": "Você já fez o cadastro no aplicativo Caixa Tem?", + "matis": "Aplicativo Caixa tem nënda minbi cadastro nakabo?", + "observacoes": [ + "O termo 'nënda' traduz 'já' e indica ação concluída.", + "A palavra 'cadastro' é um empréstimo adaptado do português.", + "O verbo 'nakabo' sugere ação completada ou feita anteriormente." + ] + }, + { + "numero": 35, + "portugues": "Você mudou de endereço recentemente?", + "matis": "Nëbi mini endereço wëtsino mannëatbok?", + "observacoes": [ + "O verbo 'wëtsino' descreve a ação de mudar ou alterar.", + "A palavra 'endereço' é usada como empréstimo direto do português.", + "O sufixo '-bok' indica passado recente ou ação já realizada." + ] + }, + { + "numero": 36, + "portugues": "Seu cartão foi cancelado por segurança.", + "matis": "Min cartão bëdek canceladak.", + "observacoes": [ + "O verbo 'canceladak' descreve a ação de cancelar ou invalidar algo.", + "O termo 'bëdek' indica causa ou motivo, como segurança neste caso." + ] + }, + { + "numero": 37, + "portugues": "Você quer pedir um novo cartão?", + "matis": "Mibi cartão paxa pediwanukek?", + "observacoes": [ + "O termo 'paxa' indica 'novo' ou 'recente'.", + "O verbo 'pediwanukek' descreve a ação de solicitar ou pedir algo." + ] + }, + { + "numero": 38, + "portugues": "Sua conta está ativa e funcionando normalmente.", + "matis": "Min conta ativak, bedek funcionaek?", + "observacoes": [ + "O termo 'ativak' indica estado ativo ou disponível.", + "O verbo 'funcionaek' sugere que algo está operando corretamente." + ] + }, + { + "numero": 39, + "portugues": "Você quer abrir uma conta poupança?", + "matis": "Mibi conta poupança abrir nukek?", + "observacoes": [ + "O verbo 'abrir' é usado diretamente como empréstimo do português.", + "O termo 'poupança' foi adaptado ao contexto financeiro." + ] + }, + { + "numero": 40, + "portugues": "Seu benefício do Bolsa Família já foi depositado.", + "matis": "Min Bolsa Famílian benefício depositak.", + "observacoes": [ + "O verbo 'depositak' descreve a ação de realizar um depósito.", + "Os termos 'Bolsa Família' e 'benefício' são empréstimos do português." + ] + }, + { + "numero": 41, + "portugues": "O Auxílio Gás será pago na próxima semana.", + "matis": "Auxilio Gás sën semana wëtsin pagaedak.", + "observacoes": [ + "O termo 'Auxilio Gás' é mantido como empréstimo do português.", + "A expressão 'wëtsin' indica 'próximo' ou 'seguinte' e é adaptada ao contexto de tempo.", + "O verbo 'pagaedak' descreve a ação de pagamento futuro." + ] + }, + { + "numero": 42, + "portugues": "Você quer saber o valor do seu benefício?", + "matis": "Mibi min benefício awestenda ikek isnukek?", + "observacoes": [ + "O verbo 'awestenda' significa 'saber' ou 'conhecer' e está associado à obtenção de informações.", + "A palavra 'benefício' é um empréstimo do português, adaptado ao contexto financeiro.", + "O sufixo '-kek' é usado para marcar intenção ou pergunta." + ] + }, + { + "numero": 43, + "portugues": "Seu benefício está bloqueado por falta de atualização cadastral.", + "matis": "Min beneficio bloqueapak minbi atualizakin naknemapa ikak.", + "observacoes": [ + "O verbo 'bloqueapak' descreve o estado de bloqueio, indicando que o benefício não está disponível.", + "A expressão 'minbi atualizakin' indica a necessidade de atualização cadastral.", + "O sufixo '-ikak' reforça a ideia de estado ou condição." + ] + }, + { + "numero": 44, + "portugues": "Você já acessou o Caixa Tem hoje?", + "matis": "Minbi nëbi isak Caixa tem nën?", + "observacoes": [ + "O termo 'nëbi' traduz 'hoje' e está relacionado ao contexto temporal.", + "O verbo 'isak' significa 'acessar' ou 'entrar' e é utilizado em contextos digitais.", + "A palavra 'Caixa Tem' é mantida como empréstimo direto do português, sendo uma marca registrada." + ] + }, + { + "numero": 45, + "portugues": "Quer ajuda para instalar o aplicativo no seu celular?", + "matis": "Dabët wa ëbi kek mibi aplicativo instalano min celulan?", + "observacoes": [ + "O verbo 'instalano' descreve a ação de instalar ou configurar algo.", + "O termo 'celulan' é um empréstimo do português, adaptado à fonética Matis.", + "A expressão 'dabët wa ëbi kek mibi' adiciona intenção de ajuda ou oferta." + ] + }, + { + "numero": 46, + "portugues": "Você pode consultar seu saldo pelo aplicativo.", + "matis": "Aplicativon min saldo consultar ta minbibi.", + "observacoes": [ + "O verbo 'consultar' é mantido como empréstimo direto do português.", + "O termo 'saldo' é usado como um conceito financeiro, também emprestado do português.", + "A palavra 'aplicativo' foi adaptada ao uso tecnológico em Matis." + ] + }, + { + "numero": 47, + "portugues": "O aplicativo está fora do ar no momento, tente mais tarde.", + "matis": "Aplicativo nebi fora do ar dapa, txitxin tan uata.", + "observacoes": [ + "A expressão 'fora do ar' é mantida como empréstimo do português e traduzida diretamente.", + "O termo 'txitxin tan uata' descreve a ideia de 'tentar mais tarde' ou 'aguardar'.", + "A palavra 'aplicativo' foi lexicalizada e adaptada ao contexto digital." + ] + }, + { + "numero": 48, + "portugues": "Quer imprimir o extrato da sua conta?", + "matis": "Min contan extrato imprimir nukek?", + "observacoes": [ + "O verbo 'imprimir' é mantido como empréstimo direto do português.", + "O termo 'extrato' é usado como conceito financeiro e adaptado ao contexto bancário.", + "O sufixo '-kek' marca a intenção ou pergunta." + ] + }, + { + "numero": 49, + "portugues": "Você precisa de um comprovante de recebimento?", + "matis": "Mibi comprovante minbi bedakit betnukek?", + "observacoes": [ + "O termo 'comprovante' é mantido como empréstimo do português.", + "O verbo 'bedakit' descreve a ação de mostrar ou confirmar algo.", + "O sufixo '-nukek' marca intenção ou necessidade no contexto." + ] + }, + { + "numero": 50, + "portugues": "O comprovante será enviado por SMS.", + "matis": "SMSsin bin comprovante koanedak.", + "observacoes": [ + "O termo 'SMSsin bin' traduz 'por SMS' e é adaptado ao contexto tecnológico.", + "O verbo 'koanedak' descreve a ação de enviar ou transmitir algo.", + "A palavra 'comprovante' foi mantida como empréstimo direto do português." + ] + }, + { + "numero": 51, + "portugues": "Você quer ver os depósitos dos últimos três meses?", + "matis": "Mibi nëbi minbi mëkën tet uxë depositabokit isnukek?", + "observacoes": [ + "O termo 'mëkën tet uxë' traduz 'últimos três meses' e reflete um período temporal.", + "O verbo 'depositabokit' descreve depósitos realizados no passado recente.", + "O sufixo '-kek' marca intenção ou pergunta." + ] + }, + { + "numero": 52, + "portugues": "Vou te encaminhar para o atendimento presencial.", + "matis": "Mibi buannu abinokimoxon mibi atentenu.", + "observacoes": [ + "O verbo 'abinokimoxon' descreve a ação de encaminhar ou direcionar alguém.", + "O termo 'atentenu' refere-se ao atendimento, mantido como empréstimo do português.", + "A expressão 'buannu' indica 'vou' ou 'irei fazer algo'." + ] + }, + { + "numero": 53, + "portugues": "Aguarde, o gerente virá te atender.", + "matis": "Sotanta, gerente txoek mibi atendek.", + "observacoes": [ + "O termo 'sotanta' sugere paciência ou espera.", + "O verbo 'txoek' indica a ação de vir ou aproximar-se.", + "A palavra 'gerente' é mantida como empréstimo direto do português." + ] + }, + { + "numero": 54, + "portugues": "Você precisa ir até o CRAS para atualizar seus dados.", + "matis": "CRAS no kuantanta min dados bëdawamek.", + "observacoes": [ + "O termo 'kuantanta' traduz 'ir até' e sugere deslocamento.", + "A expressão 'bëdawamek' indica a ação de atualizar ou corrigir algo.", + "A palavra 'CRAS' é mantida como empréstimo direto do português, sendo um nome próprio." + ] + }, + { + "numero": 55, + "portugues": "Esse atendimento precisa ser feito com agendamento.", + "matis": "Nëkit atendimento pukinkin agendakin nëndoxonbin nakaedak.", + "observacoes": [ + "A expressão 'pukinkin agendakin' descreve a necessidade de agendamento.", + "O verbo 'nakaedak' indica que algo precisa ser realizado ou concluído.", + "A palavra 'atendimento' foi mantida como empréstimo direto do português." + ] + }, + { + "numero": 56, + "portugues": "Posso te ajudar com mais alguma coisa?", + "matis": "Abi ëbi kek mëkën dabët wanukek?", + "observacoes": [ + "O termo 'abi ëbi' indica uma oferta de ajuda, traduzido como 'eu posso te ajudar'.", + "O sufixo '-kek' marca a intenção ou a pergunta.", + "O verbo 'wanukek' sugere disponibilidade ou oferta de realizar uma ação." + ] + }, + { + "numero": 57, + "portugues": "Tudo bem com você?", + "matis": "Nëbi bëdabi ëbi?", + "observacoes": [ + "A expressão 'bëdabi' indica que algo está bem ou correto.", + "O pronome 'ëbi' refere-se diretamente ao interlocutor ('você').", + "A estrutura da frase segue o padrão interrogativo do Matis, com a pergunta no final." + ] + }, + { + "numero": 58, + "portugues": "Seu benefício será cancelado se não for atualizado.", + "matis": "Min benefício bëdek cancelanuk minbi bëdawamek?", + "observacoes": [ + "O verbo 'cancelanuk' descreve a ação de cancelar no futuro, com o sufixo '-nuk'.", + "O termo 'bëdawamek' indica a necessidade de atualização.", + "A palavra 'benefício' é mantida como empréstimo direto do português." + ] + }, + { + "numero": 59, + "portugues": "Você já recebeu o Auxílio Emergencial?", + "matis": "Minbi nëbi Auxílio Emergencial bedatekit?", + "observacoes": [ + "O verbo 'bedatekit' descreve a ação de receber algo.", + "Os termos 'Auxílio Emergencial' são mantidos como empréstimos diretos do português.", + "A estrutura da pergunta segue o padrão do Matis, com a partícula interrogativa implícita." + ] + }, + { + "numero": 60, + "portugues": "Por favor, digite novamente sua senha.", + "matis": "Tximota, min senha wëtsiwata dadawanuk.", + "observacoes": [ + "O termo 'tximota' indica uma solicitação ou pedido educado.", + "O verbo 'wëtsiwata' descreve a ação de alterar ou confirmar algo.", + "O verbo 'dadawanuk' inclui o sufixo '-nuk', que marca repetição ou intenção." + ] + }, + { + "numero": 61, + "portugues": "Seu cartão não está funcionando.", + "matis": "Nukun cartão bëdabi nëbi iksamak.", + "observacoes": [ + "A expressão 'bëdabi nëbi iksamak' descreve o estado de algo que não está funcionando corretamente.", + "O termo 'iksamak' sugere um problema ou falha.", + "A palavra 'cartão' é mantida como empréstimo direto do português." + ] + }, + { + "numero": 62, + "portugues": "Você quer saber o saldo disponível?", + "matis": "Mibi min saldo nëbi isnukek?", + "observacoes": [ + "O verbo 'isnukek' indica a ação de consultar ou verificar algo.", + "A palavra 'saldo' é usada como empréstimo do português e refere-se a dinheiro disponível.", + "O sufixo '-kek' marca a intenção ou pergunta no contexto." + ] + }, + { + "numero": 63, + "portugues": "O sistema está fora do ar no momento.", + "matis": "Sistema nëbi fora do ar dapa.", + "observacoes": [ + "A palavra 'sistema' é mantida como empréstimo direto do português.", + "A expressão 'fora do ar' é uma tradução direta e adaptada ao contexto técnico.", + "O termo 'dapa' indica o estado atual ou presente." + ] + }, + { + "numero": 64, + "portugues": "Você já cadastrou sua biometria?", + "matis": "Minbi biometria nëbi cadastro nakabo?", + "observacoes": [ + "O verbo 'nakabo' descreve a ação de cadastrar ou registrar algo no passado.", + "A palavra 'biometria' é mantida como empréstimo direto do português.", + "O termo 'nëbi' indica temporalidade, traduzido como 'já'." + ] + }, + { + "numero": 65, + "portugues": "Quer saber se há depósitos recentes na sua conta?", + "matis": "Mibi nëbi min contan mëkën depositak isnukek?", + "observacoes": [ + "O verbo 'depositak' indica a ação de depósitos realizados.", + "A expressão 'mëkën' traduz 'recentes' e está relacionada ao tempo.", + "O sufixo '-kek' marca intenção ou pergunta no contexto." + ] + }, + { + "numero": 66, + "portugues": "O valor do benefício será atualizado.", + "matis": "Min benefício bëdawakin nëbi atualizak.", + "observacoes": [ + "O verbo 'atualizak' descreve a ação de atualizar algo.", + "O termo 'bëdawakin' indica uma melhoria ou correção.", + "A palavra 'benefício' é usada como empréstimo do português." + ] + }, + { + "numero": 67, + "portugues": "Seu cadastro foi concluído com sucesso.", + "matis": "Min cadastro nëbi nakabo bëdaik.", + "observacoes": [ + "O verbo 'nakabo' indica uma ação concluída no passado.", + "A expressão 'bëdaik' traduz 'com sucesso', indicando um resultado positivo.", + "O termo 'cadastro' é mantido como empréstimo direto do português." + ] + }, + { + "numero": 68, + "portugues": "Quer saber como acessar o aplicativo?", + "matis": "Mibi nëbi aplikativon isak isnukek?", + "observacoes": [ + "O verbo 'isak' indica a ação de acessar ou entrar.", + "A palavra 'aplicativo' é mantida como empréstimo do português.", + "O sufixo '-kek' marca intenção ou pergunta no contexto." + ] + }, + { + "numero": 69, + "portugues": "Sua conta foi desbloqueada com sucesso.", + "matis": "Min conta bëdawanu nëbi bëdaik.", + "observacoes": [ + "O verbo 'bëdawanu' descreve a ação de desbloqueio.", + "A expressão 'bëdaik' traduz 'com sucesso' e indica um resultado positivo.", + "A palavra 'conta' é mantida como empréstimo direto do português." + ] + }, + { + "numero": 70, + "portugues": "Você quer saber o próximo passo?", + "matis": "Mibi nëbi wëtsin bëdara isnukek?", + "observacoes": [ + "A palavra 'wëtsin' traduz 'próximo' ou 'seguinte'.", + "O termo 'bëdara' indica 'passo' ou 'etapa'.", + "O sufixo '-kek' marca a intenção de perguntar ou oferecer algo." + ] + } +] """ REVIEWER_AGENT_PROMPT_TEMPLATE = """ @@ -843,172 +844,172 @@ def render_prompt(template_str, **kwargs): • reexecutar: Falhas estruturais (ex.: literalidade excessiva, fontes inexistentes, morfologia sem respaldo, ampla ausência de cobertura semântica). **EXEMPLOS DE TRADUÇÃO** -{ - "exemplos": [ - { - "numero": 1, - "comando": "Digite ou Diga seu CPF", - "traducao_matis": "Min CPF dadawata ou Min CPF txuita", - "observacoes": [ - "A frase utiliza a conjunção 'ou' do português.", - "Não há agente explícito na frase.", - "A ordem dos constituintes é Objeto (O) Verbo (V)." - ] - }, - { - "numero": 2, - "comando": "Não sei meu CPF", - "traducao_matis": "Nukun CPF tanawemen ënbi", - "observacoes": [ - "A junção do verbalizado '-wa' com a marca de não passado '-e' resulta em '-we'.", - "O pronome do agente aparece no final da oração, mas poderia ocorrer no início." - ] - }, - { - "numero": 3, - "comando": "Coloque seu dedo no leitor como a imagem", - "traducao_matis": "Min mëkën sukwanta", - "observacoes": [ - "O uso do centrípeto '-wan' sugere um movimento de retorno.", - "A parte 'como na imagem' não foi traduzida, mas poderia ser 'tsusin padkid' ou 'tsusin paden'." - ] - }, - { - "numero": 4, - "comando": "Seu saldo atual é de", - "traducao_matis": "Min dinheiro ted ikek ista", - "observacoes": [ - "Esta é uma oração complexa com dois núcleos verbais.", - "Uma tradução possível seria 'min saldo ista'." - ] - }, - { - "numero": 5, - "comando": "Por favor, aguarde mais um pouco", - "traducao_matis": "Tximota sotanta", - "observacoes": [ - "Marcas de polidez como 'por favor' não são registradas em Matis.", - "A forma 'tximota' é lexicalizada e não se conhece uma raiz produtiva 'tximo'." - ] - }, - { - "numero": 6, - "comando": "Sua conta está bloqueada", - "traducao_matis": "Min conta bloqueak bëda pimen", - "observacoes": [ - "Há duas orações: 'bloquearam tua conta' e '(isso) não é bom'.", - "A predicação nominal 'não é bom' não exige verbo." - ] - }, - { - "numero": 7, - "comando": "Sua senha precisa ser trocada", - "traducao_matis": "Min senha wëtsiwata", - "observacoes": [ - "A ideia de 'necessidade' é expressa com o uso do imperativo." - ] - }, - { - "numero": 8, - "comando": "Seus dados precisam ser atualizados", - "traducao_matis": "Min anë dadawata", - "observacoes": [ - "A tradução pode não ser correspondente ao solicitado.", - "Alternativas como 'min cadastro abibi nakata' podem ser consideradas." - ] - }, - { - "numero": 9, - "comando": "Deseja desbloquear seu cartão?", - "traducao_matis": "Min cartão naknennu?", - "observacoes": [ - "O verbo 'nakne' parece ser uma junção de 'fazer' e 'soltar'." - ] - }, - { - "numero": 10, - "comando": "Deseja falar com o gerente?", - "traducao_matis": "Gerente bëd onkenu kek", - "observacoes": [ - "O agente não está explícito na frase." - ] - }, - { - "numero": 11, - "comando": "Atendimento encerrado. A CAIXA agradece", - "traducao_matis": "Ain wesadax. Atedi", - "observacoes": [ - "'Atedi' é uma expressão idiomática para encerrar uma conversa." - ] - }, - { - "numero": 12, - "comando": "Quero ver meu saldo", - "traducao_matis": "Nukun dinheiro mistedta ike isnu", - "observacoes": [ - "A oração 'quanto é o meu dinheiro' funciona como objeto do verbo 'ver'." - ] - }, - { - "numero": 13, - "comando": "Meu cartão está com problema", - "traducao_matis": "Nukun cartão iksamak", - "observacoes": [ - "A tradução livre 'não presta' é mais comum entre os Matis." - ] - }, - { - "numero": 14, - "comando": "Quero atualizar meus dados", - "traducao_matis": "Nukun anë dadawanu kek ëbi", - "observacoes": [ - "O uso de 'anë' (nome) para 'dados' pode não ser adequado." - ] - }, - { - "numero": 15, - "comando": "Perdi meu cartão", - "traducao_matis": "Nukun cartão amabox", - "observacoes": [ - "A marca de passado '-bo' indica passado recente." - ] - }, - { - "numero": 16, - "comando": "Sim", - "traducao_matis": "Ain", - "observacoes": [] - }, - { - "numero": 17, - "comando": "Não", - "traducao_matis": "Bama", - "observacoes": [ - "'Bama' expressa uma negação existencial, como 'não há'." - ] - }, - { - "numero": 18, - "comando": "Olá, estou aqui para te ajudar", - "traducao_matis": "Ei, në abi ëbi mibi dabëruakid", - "observacoes": [ - "'Dabëdawakid' é uma nominalização que significa 'ajudante'." - ] - }, - { - "numero": 19, - "comando": "Quer desbloquear o cartão?", - "traducao_matis": "Min cartão bëdawanu kek mibi?", - "observacoes": [ - "'Bëdawa' pode ser traduzido como 'fazer ficar bom'." - ] - }, - { - "numero": 20, - "comando": "Quer trocar a senha?", - "traducao_matis": "Senha wëtsiwanu kek?", - "observacoes": [] - } - ] +{ + "exemplos": [ + { + "numero": 1, + "comando": "Digite ou Diga seu CPF", + "traducao_matis": "Min CPF dadawata ou Min CPF txuita", + "observacoes": [ + "A frase utiliza a conjunção 'ou' do português.", + "Não há agente explícito na frase.", + "A ordem dos constituintes é Objeto (O) Verbo (V)." + ] + }, + { + "numero": 2, + "comando": "Não sei meu CPF", + "traducao_matis": "Nukun CPF tanawemen ënbi", + "observacoes": [ + "A junção do verbalizado '-wa' com a marca de não passado '-e' resulta em '-we'.", + "O pronome do agente aparece no final da oração, mas poderia ocorrer no início." + ] + }, + { + "numero": 3, + "comando": "Coloque seu dedo no leitor como a imagem", + "traducao_matis": "Min mëkën sukwanta", + "observacoes": [ + "O uso do centrípeto '-wan' sugere um movimento de retorno.", + "A parte 'como na imagem' não foi traduzida, mas poderia ser 'tsusin padkid' ou 'tsusin paden'." + ] + }, + { + "numero": 4, + "comando": "Seu saldo atual é de", + "traducao_matis": "Min dinheiro ted ikek ista", + "observacoes": [ + "Esta é uma oração complexa com dois núcleos verbais.", + "Uma tradução possível seria 'min saldo ista'." + ] + }, + { + "numero": 5, + "comando": "Por favor, aguarde mais um pouco", + "traducao_matis": "Tximota sotanta", + "observacoes": [ + "Marcas de polidez como 'por favor' não são registradas em Matis.", + "A forma 'tximota' é lexicalizada e não se conhece uma raiz produtiva 'tximo'." + ] + }, + { + "numero": 6, + "comando": "Sua conta está bloqueada", + "traducao_matis": "Min conta bloqueak bëda pimen", + "observacoes": [ + "Há duas orações: 'bloquearam tua conta' e '(isso) não é bom'.", + "A predicação nominal 'não é bom' não exige verbo." + ] + }, + { + "numero": 7, + "comando": "Sua senha precisa ser trocada", + "traducao_matis": "Min senha wëtsiwata", + "observacoes": [ + "A ideia de 'necessidade' é expressa com o uso do imperativo." + ] + }, + { + "numero": 8, + "comando": "Seus dados precisam ser atualizados", + "traducao_matis": "Min anë dadawata", + "observacoes": [ + "A tradução pode não ser correspondente ao solicitado.", + "Alternativas como 'min cadastro abibi nakata' podem ser consideradas." + ] + }, + { + "numero": 9, + "comando": "Deseja desbloquear seu cartão?", + "traducao_matis": "Min cartão naknennu?", + "observacoes": [ + "O verbo 'nakne' parece ser uma junção de 'fazer' e 'soltar'." + ] + }, + { + "numero": 10, + "comando": "Deseja falar com o gerente?", + "traducao_matis": "Gerente bëd onkenu kek", + "observacoes": [ + "O agente não está explícito na frase." + ] + }, + { + "numero": 11, + "comando": "Atendimento encerrado. A CAIXA agradece", + "traducao_matis": "Ain wesadax. Atedi", + "observacoes": [ + "'Atedi' é uma expressão idiomática para encerrar uma conversa." + ] + }, + { + "numero": 12, + "comando": "Quero ver meu saldo", + "traducao_matis": "Nukun dinheiro mistedta ike isnu", + "observacoes": [ + "A oração 'quanto é o meu dinheiro' funciona como objeto do verbo 'ver'." + ] + }, + { + "numero": 13, + "comando": "Meu cartão está com problema", + "traducao_matis": "Nukun cartão iksamak", + "observacoes": [ + "A tradução livre 'não presta' é mais comum entre os Matis." + ] + }, + { + "numero": 14, + "comando": "Quero atualizar meus dados", + "traducao_matis": "Nukun anë dadawanu kek ëbi", + "observacoes": [ + "O uso de 'anë' (nome) para 'dados' pode não ser adequado." + ] + }, + { + "numero": 15, + "comando": "Perdi meu cartão", + "traducao_matis": "Nukun cartão amabox", + "observacoes": [ + "A marca de passado '-bo' indica passado recente." + ] + }, + { + "numero": 16, + "comando": "Sim", + "traducao_matis": "Ain", + "observacoes": [] + }, + { + "numero": 17, + "comando": "Não", + "traducao_matis": "Bama", + "observacoes": [ + "'Bama' expressa uma negação existencial, como 'não há'." + ] + }, + { + "numero": 18, + "comando": "Olá, estou aqui para te ajudar", + "traducao_matis": "Ei, në abi ëbi mibi dabëruakid", + "observacoes": [ + "'Dabëdawakid' é uma nominalização que significa 'ajudante'." + ] + }, + { + "numero": 19, + "comando": "Quer desbloquear o cartão?", + "traducao_matis": "Min cartão bëdawanu kek mibi?", + "observacoes": [ + "'Bëdawa' pode ser traduzido como 'fazer ficar bom'." + ] + }, + { + "numero": 20, + "comando": "Quer trocar a senha?", + "traducao_matis": "Senha wëtsiwanu kek?", + "observacoes": [] + } + ] } """ diff --git a/apps/text_generation/src/tests/test_blob.py b/apps/text_generation/src/tests/test_blob.py index 45dc723..015ed3f 100644 --- a/apps/text_generation/src/tests/test_blob.py +++ b/apps/text_generation/src/tests/test_blob.py @@ -1,8 +1,6 @@ import os import sys import tempfile -from unittest.mock import patch -import pytest # Ensure pytest can import the package module from the source folder SRC_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) @@ -11,6 +9,7 @@ from blob import BlobConfig, BlobStorageUploader + class DummyContainerClient: """Simple in-memory container client for tests. @@ -27,6 +26,7 @@ def upload_blob(self, blob_path, data, overwrite): self.uploaded[blob_path] = data.read() return True + class DummyBlobServiceClient: def __init__(self, *args, **kwargs): self.dummy_container = DummyContainerClient() @@ -34,6 +34,7 @@ def __init__(self, *args, **kwargs): def get_container_client(self, container_name): return self.dummy_container + def test_blob_storage_uploader_upload_pdf(): """Integration-style unit test using a dummy container client. @@ -58,9 +59,13 @@ def test_blob_storage_uploader_upload_pdf(): finally: os.remove(tmp_path) + def test_infer_language_and_type(): config = BlobConfig(connection_string="dummy", container_name="test-container") uploader = BlobStorageUploader(config, container_client=DummyContainerClient()) assert uploader.infer_language_and_type("katukina_book.pdf") == ("katukina", "book") - assert uploader.infer_language_and_type("mayuruna_dictionary.pdf") == ("mayuruna", "dictionary") + assert uploader.infer_language_and_type("mayuruna_dictionary.pdf") == ( + "mayuruna", + "dictionary", + ) assert uploader.infer_language_and_type("unknownfile.pdf") == ("unknown", "other") diff --git a/apps/text_generation/src/tests/test_chunker_pipeline.py b/apps/text_generation/src/tests/test_chunker_pipeline.py index a2e9080..7530f3b 100644 --- a/apps/text_generation/src/tests/test_chunker_pipeline.py +++ b/apps/text_generation/src/tests/test_chunker_pipeline.py @@ -95,7 +95,9 @@ async def test_synthesizer_merges_continuation() -> None: @pytest.mark.asyncio -async def test_two_stage_indexer_orchestrates(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: +async def test_two_stage_indexer_orchestrates( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: chunk = AggregatedChunk( chunk_id="x", chunk_topic=["MORFOLOGIA"], diff --git a/apps/text_generation/src/utils.py b/apps/text_generation/src/utils.py index 8d3a982..e18605c 100644 --- a/apps/text_generation/src/utils.py +++ b/apps/text_generation/src/utils.py @@ -1,5 +1,5 @@ -import uuid import time +import uuid from contextlib import contextmanager from pathlib import Path @@ -26,7 +26,7 @@ def get_reference_pdfs(data_dir: Path) -> list[str]: Returns: list[str]: Lista de nomes de arquivos PDF. """ - return [p.name for p in data_dir.glob('*.pdf')] + return [p.name for p in data_dir.glob("*.pdf")] def derive_languages_from_filenames(pdf_files: list[str]) -> list[str]: @@ -40,6 +40,6 @@ def derive_languages_from_filenames(pdf_files: list[str]) -> list[str]: """ langs = set() for fn in pdf_files: - lang = fn.split('_')[0] + lang = fn.split("_")[0] langs.add(lang.capitalize()) return sorted(langs) diff --git a/apps/translation/src/__init__.py b/apps/translation/src/__init__.py index b9497e7..50f0b14 100644 --- a/apps/translation/src/__init__.py +++ b/apps/translation/src/__init__.py @@ -2,14 +2,15 @@ __author__ = "AI Apps GBB Team" __version__ = "0.2.0" -import os import logging +import os import sys from logging.handlers import RotatingFileHandler try: # pragma: no cover - optional dependency for local tooling from dotenv import load_dotenv # type: ignore[import-not-found] except ImportError: # pragma: no cover - provide fallback + def load_dotenv(*args, **kwargs): # type: ignore[override] return False @@ -19,26 +20,29 @@ def setup_logging() -> logging.Logger: logger.setLevel(logging.DEBUG) console_handler = logging.StreamHandler(sys.stdout) - console_format = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + console_format = logging.Formatter( + "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + ) console_handler.setFormatter(console_format) file_handler = RotatingFileHandler( - "app.log", maxBytes=10*1024*1024, backupCount=5 + "app.log", maxBytes=10 * 1024 * 1024, backupCount=5 ) file_handler.setLevel(logging.DEBUG) file_format = logging.Formatter( - '%(asctime)s - %(name)s - %(levelname)s - %(message)s' + "%(asctime)s - %(name)s - %(levelname)s - %(message)s" ) file_handler.setFormatter(file_format) logger.addHandler(console_handler) logger.addHandler(file_handler) - + return logger + logger = setup_logging() logger.info(f"{__app__} - {__author__} - Version: {__version__} initialized") -env_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), '.env') +env_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), ".env") if not load_dotenv(dotenv_path=env_path): # pragma: no cover - optional env file - logger.debug("dotenv skipped or not found at %s", env_path) \ No newline at end of file + logger.debug("dotenv skipped or not found at %s", env_path) diff --git a/apps/translation/src/main.py b/apps/translation/src/main.py index 87cb7e8..cb25cbb 100644 --- a/apps/translation/src/main.py +++ b/apps/translation/src/main.py @@ -14,11 +14,8 @@ from starlette.middleware.cors import CORSMiddleware from src import __app__, __version__, logger -from .synthetization import ( - AzureSpeechSynthesizer, - SynthesisConfig, - SynthesisParameters, -) + +from .synthetization import AzureSpeechSynthesizer, SynthesisConfig, SynthesisParameters from .transcription import MLFlowTranscriptionService, load_transcription_locators from .translation import ( AzureInferenceTranslationAgent, @@ -64,7 +61,7 @@ class Config: "example": { "text": "Hello, how are you?", "source_language": "en", - "target_language": "mtr" # ISO or custom code for indigenous language + "target_language": "mtr", # ISO or custom code for indigenous language } } @@ -115,6 +112,7 @@ def _resolve_audio_media(audio_format: str) -> tuple[str, str]: return "ogg", "audio/ogg" return "bin", "application/octet-stream" + app.add_middleware( CORSMiddleware, allow_origins=["*"], @@ -133,7 +131,9 @@ def get_translation_agent() -> AzureInferenceTranslationAgent: @lru_cache def get_transcription_service() -> MLFlowTranscriptionService: locators, default_locator = load_transcription_locators() - return MLFlowTranscriptionService(locators=locators, default_locator=default_locator) + return MLFlowTranscriptionService( + locators=locators, default_locator=default_locator + ) @lru_cache @@ -206,9 +206,15 @@ async def translate_text(payload: TranslationPayload) -> TranslationResponse: # Validate that the provided languages are among the allowed literals allowed_languages = {"pt-br", "mtr", "mtq"} if payload.source_language not in allowed_languages: - raise HTTPException(status_code=400, detail=f"Unsupported source_language: {payload.source_language}") + raise HTTPException( + status_code=400, + detail=f"Unsupported source_language: {payload.source_language}", + ) if payload.target_language not in allowed_languages: - raise HTTPException(status_code=400, detail=f"Unsupported target_language: {payload.target_language}") + raise HTTPException( + status_code=400, + detail=f"Unsupported target_language: {payload.target_language}", + ) result = agent.translate( TranslationInput( @@ -278,9 +284,19 @@ async def transcribe_audio( # Validate content type and size (example limits) content_type = (audio_file.content_type or "").lower() - allowed_types = {"audio/wav", "audio/x-wav", "audio/mpeg", "audio/ogg", "audio/webm", "audio/opus"} + allowed_types = { + "audio/wav", + "audio/x-wav", + "audio/mpeg", + "audio/ogg", + "audio/webm", + "audio/opus", + } if content_type not in allowed_types: - raise HTTPException(status_code=415, detail=f"Unsupported audio content type: {content_type}. Supported: {sorted(list(allowed_types))}") + raise HTTPException( + status_code=415, + detail=f"Unsupported audio content type: {content_type}. Supported: {sorted(list(allowed_types))}", + ) audio_bytes = await audio_file.read() if not audio_bytes: @@ -300,8 +316,12 @@ async def transcribe_audio( raise HTTPException(status_code=400, detail=str(exc)) from exc except Exception as exc: # pragma: no cover - propagate model errors logger.exception("Transcription failed: %s", exc) - raise HTTPException(status_code=500, detail="Transcription model error: " + str(exc)) - return TranscriptionResponse(text=result.text, model=result.model, latency_ms=result.latency_ms) + raise HTTPException( + status_code=500, detail="Transcription model error: " + str(exc) + ) + return TranscriptionResponse( + text=result.text, model=result.model, latency_ms=result.latency_ms + ) @app.post("/synthetize", response_model=SynthetizeResponse, tags=["Synthetization"]) @@ -353,14 +373,20 @@ async def synthetize_text( candidate = payload.language.strip().lower() normalised_language = language_aliases.get(candidate) if not normalised_language: - raise HTTPException(status_code=400, detail=f"Unsupported language: {payload.language}") + raise HTTPException( + status_code=400, detail=f"Unsupported language: {payload.language}" + ) if normalised_language and normalised_language not in {"mtr", "mtq", "pt-br"}: - raise HTTPException(status_code=400, detail=f"Unsupported language: {payload.language}") + raise HTTPException( + status_code=400, detail=f"Unsupported language: {payload.language}" + ) text_value = payload.text.strip() if payload.text else "" if not text_value: - raise HTTPException(status_code=400, detail="Synthetize text must be provided and non-empty.") + raise HTTPException( + status_code=400, detail="Synthetize text must be provided and non-empty." + ) parameters = SynthesisParameters( text=text_value or None, @@ -413,4 +439,3 @@ async def shutdown_dependencies() -> None: await service.close() except Exception as exc: # pragma: no cover - defensive path logger.warning("Failed to close transcription service cleanly: %s", exc) - diff --git a/apps/translation/src/prompts.py b/apps/translation/src/prompts.py index 85f1fdb..8f98b6e 100644 --- a/apps/translation/src/prompts.py +++ b/apps/translation/src/prompts.py @@ -1,6 +1,7 @@ from string import Template -BASE_PROMPT = Template(""" +BASE_PROMPT = Template( + """ You are a highly skilled and accurate translator. In your prompts, you are going to receive text along with the source and target language codes, **which can be only Portuguese (pt-br), Matses (mtr) and Matis (mtq)**. Your task is to translate the given text from the source language to the target language while preserving the original meaning, tone, and context. @@ -21,334 +22,335 @@ EQUIVALÊNCIAS CONHECIDAS ENTRE PORTUGUÊS E {{lingua}}: -""") +""" +) PROMPT_MATSES = """ -[ - { - "portugues": "Você trouxe um documento com foto?", - "matses": "Ada min documento fotodiadquid beo?" - }, - { - "portugues": "Seu cadastro está incompleto, precisamos atualizá-lo", - "matses": "Min cadastro abitedi nambo iquec, nacnen nu na?" - }, - { - "portugues": "Você já fez o cadastro no aplicativo Caixa Tem?", - "matses": "Ada aplicativo Caixa Tem caidën cadastra uau?" - }, - { - "portugues": "Você mudou de endereço recentemente?", - "matses": "Ada min shubu Manniacco nëbi?" - }, - { - "portugues": "Seu cartão foi cancelado por segurança.", - "matses": "Min cartão ampenushe queshun cancela uaumbi." - }, - { - "portugues": "Você quer pedir um novo cartão?", - "matses": "Cartão chucada chidte bune?" - }, - { - "portugues": "Sua conta está ativa e funcionando normalmente.", - "matses": "Min conta bëdambobi iquendac." - }, - { - "portugues": "Você quer abrir uma conta poupança?", - "matses": "Ada conta poupanca bêchicte bune?" - }, - { - "portugues": "Seu benefício do Bolsa Família já foi depositado.", - "matses": "Min benefício bolsa família caic mitsana paëdac" - }, - { - "portugues": "O Auxílio Gás será pago na próxima semana.", - "matses": "Min auxilio Gás Semana utsin paëdendac" - }, - { - "portugues": "Você quer saber o valor do seu benefício?", - "matses": "Ada min beneficio tedtsi iqueque iste bune?" - }, - { - "portugues": "Seu benefício está bloqueado por falta de atualização cadastral.", - "matses": "Mimbi chuca uaquin nacnenambo icac min piucquid bloque auac?" - }, - { - "portugues": "Você já acessou o Caixa Tem hoje?", - "matses": "Ada nëbi min conta Caixa Tem caído isso?" - }, - { - "portugues": "Quer ajuda para instalar o aplicativo no seu celular?", - "matses": "Min célulan ada aplicativo nate bune?" - }, - { - "portugues": "Você pode consultar seu saldo pelo aplicativo.", - "matses": "Aid aplicativon min piucquid istequid nendac" - }, - { - "portugues": "O aplicativo está fora do ar no momento, tente mais tarde.", - "matses": "Aplicativo icsambo nëbi iquec,iuepambocshon apiden istac?" - }, - { - "portugues": "Quer imprimir o extrato da sua conta?", - "matses": "Min contan extrato caid ada iste bune?" - }, - { - "portugues": "Você precisa de um comprovante de recebimento?", - "matses": "Ada mimbi piucquid chicaid comprovante bedte bune?" - }, - { - "portugues": "O comprovante será enviado por SMS.", - "matses": "Mim comprovante caidic min celulan nidmendac" - }, - { - "portugues": "Você quer ver os depósitos dos últimos três meses?", - "matses": "Ada min contan 3 ted uëshë nidmequin deposita uaid iste bune?" - }, - { - "portugues": "Vou te encaminhar para o atendimento presencial.", - "matses": "Matses tsadacno mibi nidmenu ashun atende uate." - }, - { - "portugues": "Aguarde, o gerente virá te atender.", - "matses": "Cain, gerente mibëd onquec choeque." - }, - { - "portugues": "Você precisa ir até o CRAS para atualizar seus dados.", - "matses": "Cras iacnoshon min cuëmëdaido abitedi nacnentan, atualiza uaquin." - }, - { - "portugues": "Esse atendimento precisa ser feito com agendamento.", - "matses": "Nëid atende uate agedamento nashun nate nendac." - } -] +[ + { + "portugues": "Você trouxe um documento com foto?", + "matses": "Ada min documento fotodiadquid beo?" + }, + { + "portugues": "Seu cadastro está incompleto, precisamos atualizá-lo", + "matses": "Min cadastro abitedi nambo iquec, nacnen nu na?" + }, + { + "portugues": "Você já fez o cadastro no aplicativo Caixa Tem?", + "matses": "Ada aplicativo Caixa Tem caidën cadastra uau?" + }, + { + "portugues": "Você mudou de endereço recentemente?", + "matses": "Ada min shubu Manniacco nëbi?" + }, + { + "portugues": "Seu cartão foi cancelado por segurança.", + "matses": "Min cartão ampenushe queshun cancela uaumbi." + }, + { + "portugues": "Você quer pedir um novo cartão?", + "matses": "Cartão chucada chidte bune?" + }, + { + "portugues": "Sua conta está ativa e funcionando normalmente.", + "matses": "Min conta bëdambobi iquendac." + }, + { + "portugues": "Você quer abrir uma conta poupança?", + "matses": "Ada conta poupanca bêchicte bune?" + }, + { + "portugues": "Seu benefício do Bolsa Família já foi depositado.", + "matses": "Min benefício bolsa família caic mitsana paëdac" + }, + { + "portugues": "O Auxílio Gás será pago na próxima semana.", + "matses": "Min auxilio Gás Semana utsin paëdendac" + }, + { + "portugues": "Você quer saber o valor do seu benefício?", + "matses": "Ada min beneficio tedtsi iqueque iste bune?" + }, + { + "portugues": "Seu benefício está bloqueado por falta de atualização cadastral.", + "matses": "Mimbi chuca uaquin nacnenambo icac min piucquid bloque auac?" + }, + { + "portugues": "Você já acessou o Caixa Tem hoje?", + "matses": "Ada nëbi min conta Caixa Tem caído isso?" + }, + { + "portugues": "Quer ajuda para instalar o aplicativo no seu celular?", + "matses": "Min célulan ada aplicativo nate bune?" + }, + { + "portugues": "Você pode consultar seu saldo pelo aplicativo.", + "matses": "Aid aplicativon min piucquid istequid nendac" + }, + { + "portugues": "O aplicativo está fora do ar no momento, tente mais tarde.", + "matses": "Aplicativo icsambo nëbi iquec,iuepambocshon apiden istac?" + }, + { + "portugues": "Quer imprimir o extrato da sua conta?", + "matses": "Min contan extrato caid ada iste bune?" + }, + { + "portugues": "Você precisa de um comprovante de recebimento?", + "matses": "Ada mimbi piucquid chicaid comprovante bedte bune?" + }, + { + "portugues": "O comprovante será enviado por SMS.", + "matses": "Mim comprovante caidic min celulan nidmendac" + }, + { + "portugues": "Você quer ver os depósitos dos últimos três meses?", + "matses": "Ada min contan 3 ted uëshë nidmequin deposita uaid iste bune?" + }, + { + "portugues": "Vou te encaminhar para o atendimento presencial.", + "matses": "Matses tsadacno mibi nidmenu ashun atende uate." + }, + { + "portugues": "Aguarde, o gerente virá te atender.", + "matses": "Cain, gerente mibëd onquec choeque." + }, + { + "portugues": "Você precisa ir até o CRAS para atualizar seus dados.", + "matses": "Cras iacnoshon min cuëmëdaido abitedi nacnentan, atualiza uaquin." + }, + { + "portugues": "Esse atendimento precisa ser feito com agendamento.", + "matses": "Nëid atende uate agedamento nashun nate nendac." + } +] """ PROMPT_MATIS = """ -[ - { - "portugues": "Digite ou diga seu CPF", - "matis": "Min CPF dadawata apadermen ikkin CPF numero Txuita" - }, - { - "portugues": "Não sei meu CPF", - "matis": "Nukun CPF tanawamen ëmbi" - }, - { - "portugues": "Coloque seu dedo no leitor como na imagem", - "matis": "Min mëkën sukanta leitor no imagem bedatnu" - }, - { - "portugues": "Seu saldo atual é de* ....", - "matis": "Min saldo nëbi tet in ikedak" - }, - { - "portugues": "Por favor, aguarde mais um pouco*...", - "matis": "Tximota sotanbotsëkta" - }, - { - "portugues": "Sua conta está bloqueada", - "matis": "Min conta bloqueapak" - }, - { - "portugues": "Sua senha precisa ser trocada", - "matis": "Min senha wëtsiwatepak" - }, - { - "portugues": "Seus dados precisam ser atualizados", - "matis": "Min dados bëdawakin naknentepat" - }, - { - "portugues": "Deseja desbloquear seu cartão?", - "matis": "Min cartão bloqueanu kek mibi?" - }, - { - "portugues": "Deseja falar com o gerente?", - "matis": "Gerente bët onkenu kek mibi?" - }, - { - "portugues": "Atendimento encerrado. A CAIXA agradece", - "matis": "Atendeakiti wesadax. CAIXA no." - }, - { - "portugues": "Quero ver meu saldo", - "matis": "Nunkun saldo isnukek ëbi." - }, - { - "portugues": "Meu cartão está com problema", - "matis": "Nukun cartão bëdapimenpa" - }, - { - "portugues": "Quero trocar minha senha", - "matis": "Nukun senha wëtsiwanu ëbi" - }, - { - "portugues": "Quero atualizar meus dados", - "matis": "Nukun dados bëdawanukek ëbi" - }, - { - "portugues": "Perdi meu cartão", - "matis": "Nunkun cartão amawabok ëmbi" - }, - { - "portugues": "Sim", - "matis": "Ain" - }, - { - "portugues": "Não", - "matis": "Bama" - }, - { - "portugues": "Olá, estou aqui para te ajudar", - "matis": "Eh, në abi ëbi mibi dabëtwakit." - }, - { - "portugues": "Você quer desbloquear o cartão?", - "matis": "Cartão desbloqueanukek mibi?" - }, - { - "portugues": "Você quer trocar a senha?", - "matis": "Senha wëtsiwanukek mibi?" - }, - { - "portugues": "Você quer atualizar seus dados?", - "matis": "Dados bëdawamek nukek mibi?" - }, - { - "portugues": "Por favor, coloque o cartão no totem", - "matis": "Cartão txokonta totennën." - }, - { - "portugues": "Aguarde um momento, estou resolvendo", - "matis": "Sotanbota, nakneneke ëmbi ikek" - }, - { - "portugues": "Resolvi o problema. Está tudo certo agora", - "matis": "Isama ikakit nanenak enbi. Nëbi bëda." - }, - { - "portugues": "O próximo depósito será feito no dia [dia][mês], daqui a [dias] dias", - "matis": "Nëbi depositatekit in nëkit nëtën ikek, mês ikek kek" - }, - { - "portugues": "Quer ver se está tudo certo com a sua conta?", - "matis": "Min conta bëdara ikek isnukek?" - }, - { - "portugues": "Só um momento, estou vendo se tem algum problema na sua conta", - "matis": "Tximota, min conta bëdada ikek isbono." - }, - { - "portugues": "Nenhum problema com os seus dados", - "matis": "Min dados isamama bëdabi?" - }, - { - "portugues": "Nenhum problema com o seu cartão", - "matis": "Min cartão bëdabi, isamama?" - }, - { - "portugues": "Nenhum problema com a sua senha", - "matis": "Min senha bëdabi, isamama?" - }, - { - "portugues": "Estamos ligando para um gerente", - "matis": "Gerente ligabono?" - }, - { - "portugues": "Você trouxe um documento com foto?", - "matis": "Minbi documento foto txokit bëak?" - }, - { - "portugues": "Seu cadastro está incompleto, precisamos atualizá-lo.", - "matis": "Min cadastro weskin naknenamapa, bëdawakin naknennuk." - }, - { - "portugues": "Você já fez o cadastro no aplicativo Caixa Tem?", - "matis": "Aplicativo Caixa tem nënda minbi cadastro nakabo?" - }, - { - "portugues": "Você mudou de endereço recentemente?", - "matis": "Nëbi mini endereço wëtsino mannëatbok?" - }, - { - "portugues": "Seu cartão foi cancelado por segurança.", - "matis": "Min cartão bëdek canceladak." - }, - { - "portugues": "Você quer pedir um novo cartão?", - "matis": "Mibi cartão paxa pediwanukek?" - }, - { - "portugues": "Sua conta está ativa e funcionando normalmente.", - "matis": "Min conta ativak, bedek funcionaek?" - }, - { - "portugues": "Você quer abrir uma conta poupança?", - "matis": "Mibi conta poupança abrir nukek?" - }, - { - "portugues": "Seu benefício do Bolsa Família já foi depositado.", - "matis": "Min Bolsa Famílian benefício depositak." - }, - { - "portugues": "O Auxílio Gás será pago na próxima semana.", - "matis": "Auxilio Gás sën semana wëtsin pagaedak." - }, - { - "portugues": "Você quer saber o valor do seu benefício?", - "matis": "Mibi min benefício awestenda ikek isnukek?" - }, - { - "portugues": "Seu benefício está bloqueado por falta de atualização cadastral.", - "matis": "Min beneficio bloqueapak minbi atualizakin naknemapa ikak." - }, - { - "portugues": "Você já acessou o Caixa Tem hoje?", - "matis": "Minbi nëbi isak Caixa tem nën?" - }, - { - "portugues": "Quer ajuda para instalar o aplicativo no seu celular?", - "matis": "Dabët wa ëbi kek mibi aplicativo instalano min celulan?" - }, - { - "portugues": "Você pode consultar seu saldo pelo aplicativo.", - "matis": "Aplicativon min saldo consultar ta minbibi." - }, - { - "portugues": "O aplicativo está fora do ar no momento, tente mais tarde.", - "matis": "Aplicativo nebi fora do ar dapa, txitxin tan uata." - }, - { - "portugues": "Quer imprimir o extrato da sua conta?", - "matis": "Min contan extrato imprimir nukek?" - }, - { - "portugues": "Você precisa de um comprovante de recebimento?", - "matis": "Mibi comprovante minbi bedakit betnukek?" - }, - { - "portugues": "O comprovante será enviado por SMS.", - "matis": "SMSsin bin comprovante koanedak." - }, - { - "portugues": "Você quer ver os depósitos dos últimos três meses?", - "matis": "Mibi nëbi minbi mëkën tet uxë depositabokit isnukek?" - }, - { - "portugues": "Vou te encaminhar para o atendimento presencial.", - "matis": "Mibi buannu abinokimoxon mibi atentenu." - }, - { - "portugues": "Aguarde, o gerente virá te atender.", - "matis": "Sotanta, gerente txoek mibi atendek." - }, - { - "portugues": "Você precisa ir até o CRAS para atualizar seus dados.", - "matis": "CRAS no kuantanta min dados bëdawamek." - }, - { - "portugues": "Esse atendimento precisa ser feito com agendamento.", - "matis": "Nëkit atendimento pukinkin agendakin nëndoxonbin nakaedak." - } +[ + { + "portugues": "Digite ou diga seu CPF", + "matis": "Min CPF dadawata apadermen ikkin CPF numero Txuita" + }, + { + "portugues": "Não sei meu CPF", + "matis": "Nukun CPF tanawamen ëmbi" + }, + { + "portugues": "Coloque seu dedo no leitor como na imagem", + "matis": "Min mëkën sukanta leitor no imagem bedatnu" + }, + { + "portugues": "Seu saldo atual é de* ....", + "matis": "Min saldo nëbi tet in ikedak" + }, + { + "portugues": "Por favor, aguarde mais um pouco*...", + "matis": "Tximota sotanbotsëkta" + }, + { + "portugues": "Sua conta está bloqueada", + "matis": "Min conta bloqueapak" + }, + { + "portugues": "Sua senha precisa ser trocada", + "matis": "Min senha wëtsiwatepak" + }, + { + "portugues": "Seus dados precisam ser atualizados", + "matis": "Min dados bëdawakin naknentepat" + }, + { + "portugues": "Deseja desbloquear seu cartão?", + "matis": "Min cartão bloqueanu kek mibi?" + }, + { + "portugues": "Deseja falar com o gerente?", + "matis": "Gerente bët onkenu kek mibi?" + }, + { + "portugues": "Atendimento encerrado. A CAIXA agradece", + "matis": "Atendeakiti wesadax. CAIXA no." + }, + { + "portugues": "Quero ver meu saldo", + "matis": "Nunkun saldo isnukek ëbi." + }, + { + "portugues": "Meu cartão está com problema", + "matis": "Nukun cartão bëdapimenpa" + }, + { + "portugues": "Quero trocar minha senha", + "matis": "Nukun senha wëtsiwanu ëbi" + }, + { + "portugues": "Quero atualizar meus dados", + "matis": "Nukun dados bëdawanukek ëbi" + }, + { + "portugues": "Perdi meu cartão", + "matis": "Nunkun cartão amawabok ëmbi" + }, + { + "portugues": "Sim", + "matis": "Ain" + }, + { + "portugues": "Não", + "matis": "Bama" + }, + { + "portugues": "Olá, estou aqui para te ajudar", + "matis": "Eh, në abi ëbi mibi dabëtwakit." + }, + { + "portugues": "Você quer desbloquear o cartão?", + "matis": "Cartão desbloqueanukek mibi?" + }, + { + "portugues": "Você quer trocar a senha?", + "matis": "Senha wëtsiwanukek mibi?" + }, + { + "portugues": "Você quer atualizar seus dados?", + "matis": "Dados bëdawamek nukek mibi?" + }, + { + "portugues": "Por favor, coloque o cartão no totem", + "matis": "Cartão txokonta totennën." + }, + { + "portugues": "Aguarde um momento, estou resolvendo", + "matis": "Sotanbota, nakneneke ëmbi ikek" + }, + { + "portugues": "Resolvi o problema. Está tudo certo agora", + "matis": "Isama ikakit nanenak enbi. Nëbi bëda." + }, + { + "portugues": "O próximo depósito será feito no dia [dia][mês], daqui a [dias] dias", + "matis": "Nëbi depositatekit in nëkit nëtën ikek, mês ikek kek" + }, + { + "portugues": "Quer ver se está tudo certo com a sua conta?", + "matis": "Min conta bëdara ikek isnukek?" + }, + { + "portugues": "Só um momento, estou vendo se tem algum problema na sua conta", + "matis": "Tximota, min conta bëdada ikek isbono." + }, + { + "portugues": "Nenhum problema com os seus dados", + "matis": "Min dados isamama bëdabi?" + }, + { + "portugues": "Nenhum problema com o seu cartão", + "matis": "Min cartão bëdabi, isamama?" + }, + { + "portugues": "Nenhum problema com a sua senha", + "matis": "Min senha bëdabi, isamama?" + }, + { + "portugues": "Estamos ligando para um gerente", + "matis": "Gerente ligabono?" + }, + { + "portugues": "Você trouxe um documento com foto?", + "matis": "Minbi documento foto txokit bëak?" + }, + { + "portugues": "Seu cadastro está incompleto, precisamos atualizá-lo.", + "matis": "Min cadastro weskin naknenamapa, bëdawakin naknennuk." + }, + { + "portugues": "Você já fez o cadastro no aplicativo Caixa Tem?", + "matis": "Aplicativo Caixa tem nënda minbi cadastro nakabo?" + }, + { + "portugues": "Você mudou de endereço recentemente?", + "matis": "Nëbi mini endereço wëtsino mannëatbok?" + }, + { + "portugues": "Seu cartão foi cancelado por segurança.", + "matis": "Min cartão bëdek canceladak." + }, + { + "portugues": "Você quer pedir um novo cartão?", + "matis": "Mibi cartão paxa pediwanukek?" + }, + { + "portugues": "Sua conta está ativa e funcionando normalmente.", + "matis": "Min conta ativak, bedek funcionaek?" + }, + { + "portugues": "Você quer abrir uma conta poupança?", + "matis": "Mibi conta poupança abrir nukek?" + }, + { + "portugues": "Seu benefício do Bolsa Família já foi depositado.", + "matis": "Min Bolsa Famílian benefício depositak." + }, + { + "portugues": "O Auxílio Gás será pago na próxima semana.", + "matis": "Auxilio Gás sën semana wëtsin pagaedak." + }, + { + "portugues": "Você quer saber o valor do seu benefício?", + "matis": "Mibi min benefício awestenda ikek isnukek?" + }, + { + "portugues": "Seu benefício está bloqueado por falta de atualização cadastral.", + "matis": "Min beneficio bloqueapak minbi atualizakin naknemapa ikak." + }, + { + "portugues": "Você já acessou o Caixa Tem hoje?", + "matis": "Minbi nëbi isak Caixa tem nën?" + }, + { + "portugues": "Quer ajuda para instalar o aplicativo no seu celular?", + "matis": "Dabët wa ëbi kek mibi aplicativo instalano min celulan?" + }, + { + "portugues": "Você pode consultar seu saldo pelo aplicativo.", + "matis": "Aplicativon min saldo consultar ta minbibi." + }, + { + "portugues": "O aplicativo está fora do ar no momento, tente mais tarde.", + "matis": "Aplicativo nebi fora do ar dapa, txitxin tan uata." + }, + { + "portugues": "Quer imprimir o extrato da sua conta?", + "matis": "Min contan extrato imprimir nukek?" + }, + { + "portugues": "Você precisa de um comprovante de recebimento?", + "matis": "Mibi comprovante minbi bedakit betnukek?" + }, + { + "portugues": "O comprovante será enviado por SMS.", + "matis": "SMSsin bin comprovante koanedak." + }, + { + "portugues": "Você quer ver os depósitos dos últimos três meses?", + "matis": "Mibi nëbi minbi mëkën tet uxë depositabokit isnukek?" + }, + { + "portugues": "Vou te encaminhar para o atendimento presencial.", + "matis": "Mibi buannu abinokimoxon mibi atentenu." + }, + { + "portugues": "Aguarde, o gerente virá te atender.", + "matis": "Sotanta, gerente txoek mibi atendek." + }, + { + "portugues": "Você precisa ir até o CRAS para atualizar seus dados.", + "matis": "CRAS no kuantanta min dados bëdawamek." + }, + { + "portugues": "Esse atendimento precisa ser feito com agendamento.", + "matis": "Nëkit atendimento pukinkin agendakin nëndoxonbin nakaedak." + } ] -""" \ No newline at end of file +""" diff --git a/apps/translation/src/synthetization.py b/apps/translation/src/synthetization.py index 0b44788..ef185f0 100644 --- a/apps/translation/src/synthetization.py +++ b/apps/translation/src/synthetization.py @@ -3,240 +3,239 @@ from __future__ import annotations import base64 +import importlib.resources as resources import os from dataclasses import dataclass from functools import lru_cache from typing import Optional -import importlib.resources as resources - import azure.cognitiveservices.speech as speechsdk from azure.identity import DefaultAzureCredential from src import logger - _FORMAT_MAP: dict[str, speechsdk.SpeechSynthesisOutputFormat] = { - "audio-24khz-48kbitrate-mono-mp3": speechsdk.SpeechSynthesisOutputFormat.Audio24Khz48KBitRateMonoMp3, - "riff-16khz-16bit-mono-pcm": speechsdk.SpeechSynthesisOutputFormat.Riff16Khz16BitMonoPcm, - "audio-16khz-32kbitrate-mono-mp3": speechsdk.SpeechSynthesisOutputFormat.Audio16Khz32KBitRateMonoMp3, - "riff-24khz-16bit-mono-pcm": speechsdk.SpeechSynthesisOutputFormat.Riff24Khz16BitMonoPcm, - "riff-48khz-16bit-mono-pcm": speechsdk.SpeechSynthesisOutputFormat.Riff48Khz16BitMonoPcm, + "audio-24khz-48kbitrate-mono-mp3": speechsdk.SpeechSynthesisOutputFormat.Audio24Khz48KBitRateMonoMp3, + "riff-16khz-16bit-mono-pcm": speechsdk.SpeechSynthesisOutputFormat.Riff16Khz16BitMonoPcm, + "audio-16khz-32kbitrate-mono-mp3": speechsdk.SpeechSynthesisOutputFormat.Audio16Khz32KBitRateMonoMp3, + "riff-24khz-16bit-mono-pcm": speechsdk.SpeechSynthesisOutputFormat.Riff24Khz16BitMonoPcm, + "riff-48khz-16bit-mono-pcm": speechsdk.SpeechSynthesisOutputFormat.Riff48Khz16BitMonoPcm, } _LANGUAGE_LEXICON_RESOURCES: dict[str, tuple[str, str]] = { - "mtr": ("src.resources.lexicon", "matses.pls"), - "matses": ("src.resources.lexicon", "matses.pls"), - "mtq": ("src.resources.lexicon", "matis.pls"), - "matis": ("src.resources.lexicon", "matis.pls"), + "mtr": ("src.resources.lexicon", "matses.pls"), + "matses": ("src.resources.lexicon", "matses.pls"), + "mtq": ("src.resources.lexicon", "matis.pls"), + "matis": ("src.resources.lexicon", "matis.pls"), } @lru_cache(maxsize=None) def _load_lexicon_resource(package: str, filename: str) -> Optional[str]: - try: - resource = resources.files(package).joinpath(filename) - except (FileNotFoundError, ModuleNotFoundError): - return None - try: - with resources.as_file(resource) as handle: - return str(handle) - except FileNotFoundError: - return None + try: + resource = resources.files(package).joinpath(filename) + except (FileNotFoundError, ModuleNotFoundError): + return None + try: + with resources.as_file(resource) as handle: + return str(handle) + except FileNotFoundError: + return None def _apply_language_lexicon( - speech_config: speechsdk.SpeechConfig, - language: Optional[str], + speech_config: speechsdk.SpeechConfig, + language: Optional[str], ) -> None: - if not language: - return - spec = _LANGUAGE_LEXICON_RESOURCES.get(language) - if not spec: - return - package, filename = spec - lexicon_path = _load_lexicon_resource(package, filename) - if not lexicon_path: - logger.warning("Lexicon resource missing for language %s", language) - return - property_id = getattr( - speechsdk.PropertyId, - "SpeechServiceConnection_PronunciationLexiconFiles", - None, - ) - if property_id is None: - logger.warning( - "Azure Speech SDK does not expose lexicon property; skipping lexicon registration for %s", - language, - ) - return - existing = speech_config.get_property(property_id) - paths = [path for path in (existing.split(";") if existing else []) if path] - if lexicon_path in paths: - logger.debug("Lexicon %s already registered", lexicon_path) - return - paths.append(lexicon_path) - speech_config.set_property(property_id, ";".join(paths)) - logger.debug("Registered lexicon %s for language %s", lexicon_path, language) + if not language: + return + spec = _LANGUAGE_LEXICON_RESOURCES.get(language) + if not spec: + return + package, filename = spec + lexicon_path = _load_lexicon_resource(package, filename) + if not lexicon_path: + logger.warning("Lexicon resource missing for language %s", language) + return + property_id = getattr( + speechsdk.PropertyId, + "SpeechServiceConnection_PronunciationLexiconFiles", + None, + ) + if property_id is None: + logger.warning( + "Azure Speech SDK does not expose lexicon property; skipping lexicon registration for %s", + language, + ) + return + existing = speech_config.get_property(property_id) + paths = [path for path in (existing.split(";") if existing else []) if path] + if lexicon_path in paths: + logger.debug("Lexicon %s already registered", lexicon_path) + return + paths.append(lexicon_path) + speech_config.set_property(property_id, ";".join(paths)) + logger.debug("Registered lexicon %s for language %s", lexicon_path, language) @dataclass(frozen=True) class SynthesisConfig: - """Configuration for Azure Speech synthetisation.""" - - region: str - voice: str - audio_format: str - key: Optional[str] - - @classmethod - def from_env(cls) -> "SynthesisConfig": - region = os.getenv("AZURE_SPEECH_REGION") - voice = os.getenv("AZURE_SPEECH_VOICE", "es-MX-DaliaNeural") - audio_format = os.getenv( - "AZURE_SPEECH_AUDIO_FORMAT", "riff-24khz-16bit-mono-pcm" - ) - key = os.getenv("AZURE_SPEECH_KEY") - if not region: - raise ValueError("Azure Speech region not configured.") - return cls(region=region, voice=voice, audio_format=audio_format, key=key) + """Configuration for Azure Speech synthetisation.""" + + region: str + voice: str + audio_format: str + key: Optional[str] + + @classmethod + def from_env(cls) -> "SynthesisConfig": + region = os.getenv("AZURE_SPEECH_REGION") + voice = os.getenv("AZURE_SPEECH_VOICE", "es-MX-DaliaNeural") + audio_format = os.getenv( + "AZURE_SPEECH_AUDIO_FORMAT", "riff-24khz-16bit-mono-pcm" + ) + key = os.getenv("AZURE_SPEECH_KEY") + if not region: + raise ValueError("Azure Speech region not configured.") + return cls(region=region, voice=voice, audio_format=audio_format, key=key) @dataclass(frozen=True) class SynthesisParameters: - """Input settings for the synthetisation process.""" + """Input settings for the synthetisation process.""" - text: Optional[str] = None - voice: Optional[str] = None - style: Optional[str] = None - role: Optional[str] = None - language: Optional[str] = None + text: Optional[str] = None + voice: Optional[str] = None + style: Optional[str] = None + role: Optional[str] = None + language: Optional[str] = None @dataclass(frozen=True) class SynthesisResult: - """Synthetisation output returned to the API layer.""" + """Synthetisation output returned to the API layer.""" - audio_bytes: bytes - audio_base64: str - voice: str - audio_format: str - ssml: str + audio_bytes: bytes + audio_base64: str + voice: str + audio_format: str + ssml: str class AzureSpeechSynthesizer: - """Produces audio output from text using SSML and Azure Speech.""" - - def __init__(self, config: SynthesisConfig) -> None: - self._config = config - self._credential: Optional[DefaultAzureCredential] - if config.key: - self._credential = None - else: - self._credential = DefaultAzureCredential() - - @property - def default_voice(self) -> str: - return self._config.voice - - @property - def audio_format(self) -> str: - return self._config.audio_format - - def synthesize(self, parameters: SynthesisParameters) -> SynthesisResult: - language = (parameters.language or "").strip().lower() or None - text = parameters.text.strip() if parameters.text else "" - if not text: - raise ValueError("No text provided for synthetisation.") - - voice = parameters.voice or self._config.voice - if self._config.key: - speech_config = speechsdk.SpeechConfig( - subscription=self._config.key, - region=self._config.region, - ) - else: - if not self._credential: - raise RuntimeError("Azure credential unavailable for speech synthesis.") - token = self._credential.get_token("https://cognitiveservices.azure.com/.default") - speech_config = speechsdk.SpeechConfig( - auth_token=token.token, - region=self._config.region, - ) - speech_config.speech_synthesis_voice_name = voice - speech_config.set_speech_synthesis_output_format( - _FORMAT_MAP.get( - self._config.audio_format, - speechsdk.SpeechSynthesisOutputFormat.Riff24Khz16BitMonoPcm, - ) - ) - _apply_language_lexicon(speech_config, language) - - ssml = self._build_ssml(text, voice, parameters) - if language: - logger.debug( - "Synthesising speech with voice %s using lexicon for %s", - voice, - language, - ) - else: - logger.debug("Synthesising speech with voice %s", voice) - - synthesizer = speechsdk.SpeechSynthesizer( - speech_config=speech_config, - ) - result = synthesizer.speak_ssml_async(ssml).get() - reason = getattr(result, "reason", None) - if reason != speechsdk.ResultReason.SynthesizingAudioCompleted: - if reason == speechsdk.ResultReason.Canceled: - cancellation = result.cancellation_details # type: ignore[attr-defined] - error_code = getattr(cancellation, "error_code", None) - error_details = getattr(cancellation, "error_details", "") - logger.error( - "Speech synthesis canceled error_code=%s details=%s", - error_code, - error_details, - ) - else: - logger.error("Speech synthesis failed with reason: %s", reason) - raise RuntimeError("Azure Speech synthesis failed.") - - audio_data_raw = getattr(result, "audio_data", None) - if not audio_data_raw: - logger.error("Speech synthesis completed but returned no audio data") - raise RuntimeError("Azure Speech synthesis returned empty audio data.") - - audio_data = bytes(audio_data_raw) - encoded_audio = base64.b64encode(audio_data).decode("ascii") - return SynthesisResult( - audio_bytes=audio_data, - audio_base64=encoded_audio, - voice=voice, - audio_format=self._config.audio_format, - ssml=ssml, - ) - - def _build_ssml(self, text: str, voice: str, parameters: SynthesisParameters) -> str: - """Compose an SSML payload with optional speaking style and role.""" - - style_attr = ( - f' style="{parameters.style}"' if parameters.style else "" - ) - role_attr = f' role="{parameters.role}"' if parameters.role else "" - ssml = ( - "" - "" - f"{text}" - "" - ) - return ssml + """Produces audio output from text using SSML and Azure Speech.""" + + def __init__(self, config: SynthesisConfig) -> None: + self._config = config + self._credential: Optional[DefaultAzureCredential] + if config.key: + self._credential = None + else: + self._credential = DefaultAzureCredential() + + @property + def default_voice(self) -> str: + return self._config.voice + + @property + def audio_format(self) -> str: + return self._config.audio_format + + def synthesize(self, parameters: SynthesisParameters) -> SynthesisResult: + language = (parameters.language or "").strip().lower() or None + text = parameters.text.strip() if parameters.text else "" + if not text: + raise ValueError("No text provided for synthetisation.") + + voice = parameters.voice or self._config.voice + if self._config.key: + speech_config = speechsdk.SpeechConfig( + subscription=self._config.key, + region=self._config.region, + ) + else: + if not self._credential: + raise RuntimeError("Azure credential unavailable for speech synthesis.") + token = self._credential.get_token( + "https://cognitiveservices.azure.com/.default" + ) + speech_config = speechsdk.SpeechConfig( + auth_token=token.token, + region=self._config.region, + ) + speech_config.speech_synthesis_voice_name = voice + speech_config.set_speech_synthesis_output_format( + _FORMAT_MAP.get( + self._config.audio_format, + speechsdk.SpeechSynthesisOutputFormat.Riff24Khz16BitMonoPcm, + ) + ) + _apply_language_lexicon(speech_config, language) + + ssml = self._build_ssml(text, voice, parameters) + if language: + logger.debug( + "Synthesising speech with voice %s using lexicon for %s", + voice, + language, + ) + else: + logger.debug("Synthesising speech with voice %s", voice) + + synthesizer = speechsdk.SpeechSynthesizer( + speech_config=speech_config, + ) + result = synthesizer.speak_ssml_async(ssml).get() + reason = getattr(result, "reason", None) + if reason != speechsdk.ResultReason.SynthesizingAudioCompleted: + if reason == speechsdk.ResultReason.Canceled: + cancellation = result.cancellation_details # type: ignore[attr-defined] + error_code = getattr(cancellation, "error_code", None) + error_details = getattr(cancellation, "error_details", "") + logger.error( + "Speech synthesis canceled error_code=%s details=%s", + error_code, + error_details, + ) + else: + logger.error("Speech synthesis failed with reason: %s", reason) + raise RuntimeError("Azure Speech synthesis failed.") + + audio_data_raw = getattr(result, "audio_data", None) + if not audio_data_raw: + logger.error("Speech synthesis completed but returned no audio data") + raise RuntimeError("Azure Speech synthesis returned empty audio data.") + + audio_data = bytes(audio_data_raw) + encoded_audio = base64.b64encode(audio_data).decode("ascii") + return SynthesisResult( + audio_bytes=audio_data, + audio_base64=encoded_audio, + voice=voice, + audio_format=self._config.audio_format, + ssml=ssml, + ) + + def _build_ssml( + self, text: str, voice: str, parameters: SynthesisParameters + ) -> str: + """Compose an SSML payload with optional speaking style and role.""" + + style_attr = f' style="{parameters.style}"' if parameters.style else "" + role_attr = f' role="{parameters.role}"' if parameters.role else "" + ssml = ( + '' + '' + f'{text}' + "" + ) + return ssml __all__ = [ - "AzureSpeechSynthesizer", - "SynthesisConfig", - "SynthesisParameters", - "SynthesisResult", + "AzureSpeechSynthesizer", + "SynthesisConfig", + "SynthesisParameters", + "SynthesisResult", ] - diff --git a/apps/translation/src/transcription.py b/apps/translation/src/transcription.py index 7ffda92..c2cb3ea 100644 --- a/apps/translation/src/transcription.py +++ b/apps/translation/src/transcription.py @@ -18,7 +18,6 @@ from src import logger - _DEFAULT_SCOPE = "https://ml.azure.com/.default" _DEFAULT_TIMEOUT_SECONDS = 60.0 _DEFAULT_MAX_RETRIES = 3 @@ -62,23 +61,37 @@ def from_env(cls, prefix: str | None = None) -> "AMLModelLocator": "API_KEY", ) - scope = cls._read_env( - *[f"{candidate}_TRANSCRIPTION_SCOPE" for candidate in prefixes], - "AZUREML_TRANSCRIPTION_SCOPE", - ) or _DEFAULT_SCOPE + scope = ( + cls._read_env( + *[f"{candidate}_TRANSCRIPTION_SCOPE" for candidate in prefixes], + "AZUREML_TRANSCRIPTION_SCOPE", + ) + or _DEFAULT_SCOPE + ) use_managed_identity_env = cls._read_env( - *[f"{candidate}_TRANSCRIPTION_USE_MANAGED_IDENTITY" for candidate in prefixes], + *[ + f"{candidate}_TRANSCRIPTION_USE_MANAGED_IDENTITY" + for candidate in prefixes + ], "AZUREML_TRANSCRIPTION_USE_MANAGED_IDENTITY", ) if use_managed_identity_env is None: use_default_credential = api_key is None else: - use_default_credential = use_managed_identity_env.strip().lower() in {"1", "true", "yes", "on"} + use_default_credential = use_managed_identity_env.strip().lower() in { + "1", + "true", + "yes", + "on", + } timeout_seconds = cls._read_float( cls._read_env( - *[f"{candidate}_TRANSCRIPTION_TIMEOUT_SECONDS" for candidate in prefixes], + *[ + f"{candidate}_TRANSCRIPTION_TIMEOUT_SECONDS" + for candidate in prefixes + ], "AZUREML_TRANSCRIPTION_TIMEOUT_SECONDS", ), _DEFAULT_TIMEOUT_SECONDS, @@ -92,7 +105,10 @@ def from_env(cls, prefix: str | None = None) -> "AMLModelLocator": ) backoff_factor = cls._read_float( cls._read_env( - *[f"{candidate}_TRANSCRIPTION_BACKOFF_FACTOR" for candidate in prefixes], + *[ + f"{candidate}_TRANSCRIPTION_BACKOFF_FACTOR" + for candidate in prefixes + ], "AZUREML_TRANSCRIPTION_BACKOFF_FACTOR", ), _DEFAULT_BACKOFF_FACTOR, @@ -103,11 +119,15 @@ def from_env(cls, prefix: str | None = None) -> "AMLModelLocator": "AZUREML_TRANSCRIPTION_COLUMNS", ) if columns_env: - columns = tuple(col.strip() for col in columns_env.split(",") if col.strip()) + columns = tuple( + col.strip() for col in columns_env.split(",") if col.strip() + ) else: columns = ("audio_base64",) if not columns: - raise ValueError("Transcription columns configuration must include at least one column name.") + raise ValueError( + "Transcription columns configuration must include at least one column name." + ) params_env = cls._read_env( *[f"{candidate}_TRANSCRIPTION_PARAMS_JSON" for candidate in prefixes], @@ -120,7 +140,9 @@ def from_env(cls, prefix: str | None = None) -> "AMLModelLocator": if isinstance(loaded, dict): params = loaded else: - logger.warning("Transcription params JSON must decode to an object; using empty params.") + logger.warning( + "Transcription params JSON must decode to an object; using empty params." + ) except json.JSONDecodeError as exc: logger.warning("Invalid JSON for transcription params: %s", exc) @@ -162,7 +184,9 @@ def _read_float(raw_value: str | None, default: float) -> float: try: return float(raw_value) except ValueError: - logger.warning("Invalid float value '%s'. Falling back to %s", raw_value, default) + logger.warning( + "Invalid float value '%s'. Falling back to %s", raw_value, default + ) return default @staticmethod @@ -172,7 +196,9 @@ def _read_int(raw_value: str | None, default: int) -> int: try: return int(raw_value) except ValueError: - logger.warning("Invalid integer value '%s'. Falling back to %s", raw_value, default) + logger.warning( + "Invalid integer value '%s'. Falling back to %s", raw_value, default + ) return default @staticmethod @@ -208,9 +234,15 @@ class TranscriptionResult: class MLFlowTranscriptionService: """Routes transcription requests to Azure ML managed online endpoints.""" - def __init__(self, locators: dict[tuple[str, str], AMLModelLocator], default_locator: AMLModelLocator | None = None) -> None: + def __init__( + self, + locators: dict[tuple[str, str], AMLModelLocator], + default_locator: AMLModelLocator | None = None, + ) -> None: if not locators and default_locator is None: - raise ValueError("At least one Azure ML transcription model must be configured.") + raise ValueError( + "At least one Azure ML transcription model must be configured." + ) self._locators = { (self._normalise_language(src), self._normalise_language(dst)): locator @@ -224,9 +256,9 @@ def __init__(self, locators: dict[tuple[str, str], AMLModelLocator], default_loc self._access_tokens: dict[str, AccessToken] = {} self._token_margin_seconds = 60 - use_managed_identity = any(locator.use_default_credential for locator in self._locators.values()) or ( - default_locator.use_default_credential if default_locator else False - ) + use_managed_identity = any( + locator.use_default_credential for locator in self._locators.values() + ) or (default_locator.use_default_credential if default_locator else False) if use_managed_identity: try: @@ -240,7 +272,9 @@ def __init__(self, locators: dict[tuple[str, str], AMLModelLocator], default_loc "Unable to configure Azure ML transcription client. Provide API keys or configure a managed identity." ) from exc - endpoints = ", ".join({locator.scoring_uri for locator in self._locators.values()}) + endpoints = ", ".join( + {locator.scoring_uri for locator in self._locators.values()} + ) logger.debug("Azure ML transcription configured with endpoints: %s", endpoints) async def ensure_ready(self) -> None: @@ -285,7 +319,9 @@ async def transcribe( output_language, ) start_time = time.perf_counter() - body, content_type_header = await self._post_with_retries(session, locator, payload, headers) + body, content_type_header = await self._post_with_retries( + session, locator, payload, headers + ) latency_ms = (time.perf_counter() - start_time) * 1000.0 text = self._parse_response_body(body, content_type_header) logger.debug( @@ -295,7 +331,12 @@ async def transcribe( len(text), ) model_name = locator.model_name or f"{input_language}->{output_language}" - return TranscriptionResult(text=text, endpoint=locator.scoring_uri, model=model_name, latency_ms=latency_ms) + return TranscriptionResult( + text=text, + endpoint=locator.scoring_uri, + model=model_name, + latency_ms=latency_ms, + ) async def _ensure_session(self) -> ClientSession: async with self._session_lock: @@ -311,7 +352,9 @@ def _resolve_timeout(self) -> float: return self._default_locator.timeout_seconds return _DEFAULT_TIMEOUT_SECONDS - def _build_payload(self, audio_bytes: bytes, locator: AMLModelLocator) -> dict[str, Any]: + def _build_payload( + self, audio_bytes: bytes, locator: AMLModelLocator + ) -> dict[str, Any]: encoded_audio = base64.b64encode(audio_bytes).decode("utf-8") data_row = [encoded_audio] + [None] * (len(locator.payload_columns) - 1) return { @@ -348,15 +391,23 @@ async def _ensure_token(self, locator: AMLModelLocator) -> AccessToken: if token and token.expires_on - self._token_margin_seconds > time.time(): return token if self._credential is None: - raise RuntimeError("Managed identity was not configured for the Azure ML transcription client.") + raise RuntimeError( + "Managed identity was not configured for the Azure ML transcription client." + ) try: token = await self._credential.get_token(scope) except CredentialUnavailableError as exc: logger.exception("Managed identity credential unavailable: %s", exc) - raise RuntimeError("Managed identity is unavailable for Azure ML transcription.") from exc + raise RuntimeError( + "Managed identity is unavailable for Azure ML transcription." + ) from exc except Exception as exc: # pragma: no cover - azure identity failure - logger.exception("Failed to obtain access token for Azure ML transcription: %s", exc) - raise RuntimeError("Unable to acquire access token for Azure ML transcription endpoint.") from exc + logger.exception( + "Failed to obtain access token for Azure ML transcription: %s", exc + ) + raise RuntimeError( + "Unable to acquire access token for Azure ML transcription endpoint." + ) from exc self._access_tokens[scope] = token return token @@ -370,11 +421,16 @@ async def _post_with_retries( attempt = 0 while True: try: - async with session.post(locator.scoring_uri, json=payload, headers=headers) as response: + async with session.post( + locator.scoring_uri, json=payload, headers=headers + ) as response: body = await response.read() if 200 <= response.status < 300: return body, response.headers.get("Content-Type") - if response.status in _RETRY_STATUSES and attempt < locator.max_retries: + if ( + response.status in _RETRY_STATUSES + and attempt < locator.max_retries + ): wait_time = self._compute_backoff(locator, attempt) logger.warning( "Azure ML transcription transient error %s for %s. Retrying in %.2fs", @@ -392,7 +448,9 @@ async def _post_with_retries( response.status, detail, ) - raise RuntimeError(f"Azure ML endpoint error {response.status}: {detail}") + raise RuntimeError( + f"Azure ML endpoint error {response.status}: {detail}" + ) except (ClientResponseError, ClientError, asyncio.TimeoutError) as exc: if attempt < locator.max_retries: wait_time = self._compute_backoff(locator, attempt) @@ -405,16 +463,24 @@ async def _post_with_retries( await asyncio.sleep(wait_time) attempt += 1 continue - logger.exception("Failed to call Azure ML transcription endpoint %s: %s", locator.scoring_uri, exc) - raise RuntimeError("Could not reach the Azure ML transcription endpoint.") from exc + logger.exception( + "Failed to call Azure ML transcription endpoint %s: %s", + locator.scoring_uri, + exc, + ) + raise RuntimeError( + "Could not reach the Azure ML transcription endpoint." + ) from exc def _compute_backoff(self, locator: AMLModelLocator, attempt: int) -> float: base = max(locator.backoff_factor, 0) - return base * (2 ** attempt) + return base * (2**attempt) def _parse_response_body(self, body: bytes, content_type: str | None) -> str: if not body: - raise RuntimeError("Azure ML transcription endpoint returned an empty response.") + raise RuntimeError( + "Azure ML transcription endpoint returned an empty response." + ) payload: Any | None = None if content_type and "json" in content_type.lower(): try: @@ -433,10 +499,14 @@ def _parse_response_body(self, body: bytes, content_type: str | None) -> str: fallback = self._find_first_string(payload) if fallback: return self._ensure_plain_text(fallback) - raise RuntimeError(f"Unable to extract transcription text from response: {payload}") + raise RuntimeError( + f"Unable to extract transcription text from response: {payload}" + ) text = body.decode("utf-8", errors="ignore").strip() if not text: - raise RuntimeError("Azure ML transcription endpoint returned an empty response.") + raise RuntimeError( + "Azure ML transcription endpoint returned an empty response." + ) return self._ensure_plain_text(text) def _extract_text(self, payload: Any) -> str: @@ -495,13 +565,20 @@ def _ensure_plain_text(self, text: str) -> str: return self._ensure_plain_text(fallback) return stripped - def _select_locator(self, input_language: str, output_language: str) -> AMLModelLocator: - key = (self._normalise_language(input_language), self._normalise_language(output_language)) + def _select_locator( + self, input_language: str, output_language: str + ) -> AMLModelLocator: + key = ( + self._normalise_language(input_language), + self._normalise_language(output_language), + ) if key in self._locators: return self._locators[key] if self._default_locator is not None: return self._default_locator - available = ", ".join(f"{src}->{dst}" for src, dst in sorted(self._locators)) or "none" + available = ( + ", ".join(f"{src}->{dst}" for src, dst in sorted(self._locators)) or "none" + ) raise ValueError( f"No transcription model configured for {input_language}->{output_language}. Available pairs: {available}." ) @@ -525,7 +602,9 @@ def _normalise_language(language: str) -> str: ) -def load_transcription_locators() -> tuple[dict[tuple[str, str], AMLModelLocator], AMLModelLocator | None]: +def load_transcription_locators() -> ( + tuple[dict[tuple[str, str], AMLModelLocator], AMLModelLocator | None] +): """Load transcription model locators from the environment. Returns a mapping keyed by (input_language, output_language) and an optional default locator. @@ -538,7 +617,9 @@ def load_transcription_locators() -> tuple[dict[tuple[str, str], AMLModelLocator try: locator = AMLModelLocator.from_env(prefix=prefix) except ValueError as exc: - missing_pairs.append(f"{input_language}->{output_language} ({prefix}) - {exc}") + missing_pairs.append( + f"{input_language}->{output_language} ({prefix}) - {exc}" + ) continue registry[(input_language, output_language)] = locator diff --git a/apps/translation/src/translation.py b/apps/translation/src/translation.py index 036ca15..cf16b79 100644 --- a/apps/translation/src/translation.py +++ b/apps/translation/src/translation.py @@ -4,189 +4,194 @@ import os import time -from typing import Literal -from string import Template from dataclasses import dataclass -from pydantic import BaseModel +from string import Template +from typing import Literal from azure.ai.inference import ChatCompletionsClient from azure.ai.inference.models import SystemMessage, UserMessage from azure.core.credentials import AzureKeyCredential from azure.core.exceptions import HttpResponseError from azure.identity import DefaultAzureCredential +from pydantic import BaseModel from src import logger -from src.prompts import BASE_PROMPT, PROMPT_MATSES, PROMPT_MATIS +from src.prompts import BASE_PROMPT, PROMPT_MATIS, PROMPT_MATSES def _to_int(value: str | None, default: int) -> int: - try: - return int(value) if value is not None else default - except ValueError: - return default + try: + return int(value) if value is not None else default + except ValueError: + return default def _to_float(value: str | None, default: float) -> float: - try: - return float(value) if value is not None else default - except ValueError: - return default + try: + return float(value) if value is not None else default + except ValueError: + return default class TranslationInput(BaseModel): - """Value object representing the translation request.""" + """Value object representing the translation request.""" - text: str - source_language: Literal['pt-br', 'mtr', 'mtq'] - target_language: Literal['pt-br', 'mtr', 'mtq'] + text: str + source_language: Literal["pt-br", "mtr", "mtq"] + target_language: Literal["pt-br", "mtr", "mtq"] @dataclass(frozen=True) class TranslationOutput: - """Result produced by the translation client.""" + """Result produced by the translation client.""" - translated_text: str - latency_ms: float - model: str + translated_text: str + latency_ms: float + model: str @dataclass(frozen=True) class AzureInferenceTranslationConfig: - """Settings required to call Azure AI Inference chat completions.""" - - endpoint: str - system_prompt: Template - max_tokens: int - temperature: float - top_p: float - presence_penalty: float - frequency_penalty: float - deployment: str | None = None - - @classmethod - def from_env(cls) -> "AzureInferenceTranslationConfig": - endpoint = os.getenv("AZURE_INFERENCE_ENDPOINT") or os.getenv( - "AZURE_INFERENCE_SDK_ENDPOINT" - ) - deployment = os.getenv("AZURE_INFERENCE_TRANSLATION_DEPLOYMENT") or os.getenv( - "AZURE_INFERENCE_SDK_DEPLOYMENT", - None, - ) - max_tokens = _to_int(os.getenv("AZURE_INFERENCE_TRANSLATION_MAX_TOKENS"), 1024) - temperature = _to_float(os.getenv("AZURE_INFERENCE_TRANSLATION_TEMPERATURE"), 0.3) - top_p = _to_float(os.getenv("AZURE_INFERENCE_TRANSLATION_TOP_P"), 0.95) - presence_penalty = _to_float( - os.getenv("AZURE_INFERENCE_TRANSLATION_PRESENCE_PENALTY"), - 0.0, - ) - frequency_penalty = _to_float( - os.getenv("AZURE_INFERENCE_TRANSLATION_FREQUENCY_PENALTY"), - 0.0, - ) - if not endpoint: - raise ValueError("Azure AI Inference endpoint not configured.") - return cls( - endpoint=endpoint, - deployment=deployment or 'not defined', - system_prompt=BASE_PROMPT, - max_tokens=max_tokens, - temperature=temperature, - top_p=top_p, - presence_penalty=presence_penalty, - frequency_penalty=frequency_penalty, - ) + """Settings required to call Azure AI Inference chat completions.""" + + endpoint: str + system_prompt: Template + max_tokens: int + temperature: float + top_p: float + presence_penalty: float + frequency_penalty: float + deployment: str | None = None + + @classmethod + def from_env(cls) -> "AzureInferenceTranslationConfig": + endpoint = os.getenv("AZURE_INFERENCE_ENDPOINT") or os.getenv( + "AZURE_INFERENCE_SDK_ENDPOINT" + ) + deployment = os.getenv("AZURE_INFERENCE_TRANSLATION_DEPLOYMENT") or os.getenv( + "AZURE_INFERENCE_SDK_DEPLOYMENT", + None, + ) + max_tokens = _to_int(os.getenv("AZURE_INFERENCE_TRANSLATION_MAX_TOKENS"), 1024) + temperature = _to_float( + os.getenv("AZURE_INFERENCE_TRANSLATION_TEMPERATURE"), 0.3 + ) + top_p = _to_float(os.getenv("AZURE_INFERENCE_TRANSLATION_TOP_P"), 0.95) + presence_penalty = _to_float( + os.getenv("AZURE_INFERENCE_TRANSLATION_PRESENCE_PENALTY"), + 0.0, + ) + frequency_penalty = _to_float( + os.getenv("AZURE_INFERENCE_TRANSLATION_FREQUENCY_PENALTY"), + 0.0, + ) + if not endpoint: + raise ValueError("Azure AI Inference endpoint not configured.") + return cls( + endpoint=endpoint, + deployment=deployment or "not defined", + system_prompt=BASE_PROMPT, + max_tokens=max_tokens, + temperature=temperature, + top_p=top_p, + presence_penalty=presence_penalty, + frequency_penalty=frequency_penalty, + ) class AzureInferenceTranslationAgent: - """Wrapper around Azure AI Inference chat completions to translate text.""" - - def __init__( - self, - config: AzureInferenceTranslationConfig, - credential: DefaultAzureCredential | AzureKeyCredential | None = None, - ) -> None: - self._config = config - self._credential = credential or self._resolve_credential() - self._client = ChatCompletionsClient( - endpoint=self._config.endpoint, - credential=self._credential, - ) - - def translate(self, payload: TranslationInput) -> TranslationOutput: - if not payload.text or not payload.text.strip(): - raise ValueError("Translation text cannot be empty.") - - system_prompt = self._config.system_prompt.substitute(language=payload.target_language) - if payload.target_language.lower() == "matses": - system_prompt += PROMPT_MATSES - else: - system_prompt += PROMPT_MATIS - - user_prompt = ( - f"Translate the following text from {payload.source_language} " - f"into {payload.target_language}.\n\n{payload.text.strip()}" - ) - messages = [ - SystemMessage(content=system_prompt), - UserMessage(content=user_prompt), - ] - start = time.perf_counter() - try: - response = self._client.complete( - messages=messages, - max_tokens=self._config.max_tokens, - temperature=self._config.temperature, - top_p=self._config.top_p, - presence_penalty=self._config.presence_penalty, - frequency_penalty=self._config.frequency_penalty, - ) - except HttpResponseError as exc: - logger.exception("Azure AI Inference translation request failed") - raise RuntimeError("Translation request to Azure AI Inference failed.") from exc - - latency_ms = (time.perf_counter() - start) * 1000 - translated_text = self._extract_text(response) - if not translated_text: - raise RuntimeError("Azure AI Inference did not return translated text.") - logger.debug("Translation completed in %.2f ms", latency_ms) - return TranslationOutput( - translated_text=translated_text, - latency_ms=latency_ms, - model=self._config.deployment, # type: ignore - ) - - def _resolve_credential(self) -> DefaultAzureCredential | AzureKeyCredential: - api_key = os.getenv("AZURE_INFERENCE_SDK_KEY") - if api_key: - logger.debug("Using AzureKeyCredential for Azure AI Inference client") - return AzureKeyCredential(api_key) - logger.debug("Using DefaultAzureCredential for Azure AI Inference client") - return DefaultAzureCredential() - - @staticmethod - def _extract_text(response) -> str: - choices = getattr(response, "choices", None) - if not choices: - return "" - for choice in choices: - message = getattr(choice, "message", None) - if not message: - continue - content = getattr(message, "content", None) - if not content: - continue - return content - return "" - - @property - def model_name(self) -> str: - return self._config.deployment or "unknown" + """Wrapper around Azure AI Inference chat completions to translate text.""" + + def __init__( + self, + config: AzureInferenceTranslationConfig, + credential: DefaultAzureCredential | AzureKeyCredential | None = None, + ) -> None: + self._config = config + self._credential = credential or self._resolve_credential() + self._client = ChatCompletionsClient( + endpoint=self._config.endpoint, + credential=self._credential, + ) + + def translate(self, payload: TranslationInput) -> TranslationOutput: + if not payload.text or not payload.text.strip(): + raise ValueError("Translation text cannot be empty.") + + system_prompt = self._config.system_prompt.substitute( + language=payload.target_language + ) + if payload.target_language.lower() == "matses": + system_prompt += PROMPT_MATSES + else: + system_prompt += PROMPT_MATIS + + user_prompt = ( + f"Translate the following text from {payload.source_language} " + f"into {payload.target_language}.\n\n{payload.text.strip()}" + ) + messages = [ + SystemMessage(content=system_prompt), + UserMessage(content=user_prompt), + ] + start = time.perf_counter() + try: + response = self._client.complete( + messages=messages, + max_tokens=self._config.max_tokens, + temperature=self._config.temperature, + top_p=self._config.top_p, + presence_penalty=self._config.presence_penalty, + frequency_penalty=self._config.frequency_penalty, + ) + except HttpResponseError as exc: + logger.exception("Azure AI Inference translation request failed") + raise RuntimeError( + "Translation request to Azure AI Inference failed." + ) from exc + + latency_ms = (time.perf_counter() - start) * 1000 + translated_text = self._extract_text(response) + if not translated_text: + raise RuntimeError("Azure AI Inference did not return translated text.") + logger.debug("Translation completed in %.2f ms", latency_ms) + return TranslationOutput( + translated_text=translated_text, + latency_ms=latency_ms, + model=self._config.deployment, # type: ignore + ) + + def _resolve_credential(self) -> DefaultAzureCredential | AzureKeyCredential: + api_key = os.getenv("AZURE_INFERENCE_SDK_KEY") + if api_key: + logger.debug("Using AzureKeyCredential for Azure AI Inference client") + return AzureKeyCredential(api_key) + logger.debug("Using DefaultAzureCredential for Azure AI Inference client") + return DefaultAzureCredential() + + @staticmethod + def _extract_text(response) -> str: + choices = getattr(response, "choices", None) + if not choices: + return "" + for choice in choices: + message = getattr(choice, "message", None) + if not message: + continue + content = getattr(message, "content", None) + if not content: + continue + return content + return "" + + @property + def model_name(self) -> str: + return self._config.deployment or "unknown" __all__ = [ - "AzureInferenceTranslationAgent", - "AzureInferenceTranslationConfig", - "TranslationInput", - "TranslationOutput", + "AzureInferenceTranslationAgent", + "AzureInferenceTranslationConfig", + "TranslationInput", + "TranslationOutput", ] - diff --git a/apps/whisper_fine_tuning/README.md b/apps/whisper_fine_tuning/README.md index fcfd62c..84ef9d3 100644 --- a/apps/whisper_fine_tuning/README.md +++ b/apps/whisper_fine_tuning/README.md @@ -83,9 +83,15 @@ Full option lists live in each script’s `--help` output. - Checkpoints are written to `output_model_dir/` and can be registered with MLflow or uploaded to Azure. ## Pre-Commit Hooks +Install dependencies once, then run the hook suite locally before committing: ``` pip install pre-commit pre-commit install + +# Format/lint only the files staged for commit +pre-commit run + +# (Optional) check the entire tree pre-commit run --all-files ``` Hooks enforce formatting and linting before changes land in version control. diff --git a/apps/whisper_fine_tuning/deployment/endpoint/inference.py b/apps/whisper_fine_tuning/deployment/endpoint/inference.py index d502fcd..2dc7358 100644 --- a/apps/whisper_fine_tuning/deployment/endpoint/inference.py +++ b/apps/whisper_fine_tuning/deployment/endpoint/inference.py @@ -1,9 +1,10 @@ -import urllib.request +import base64 import json -from dotenv import load_dotenv import os -import base64, json, os, pathlib, requests +from pathlib import Path +from urllib.request import Request, urlopen +from dotenv import load_dotenv # Request data goes here # The example below assumes JSON formatting which may be updated @@ -12,7 +13,7 @@ # https://docs.microsoft.com/azure/machine-learning/how-to-deploy-advanced-entry-script load_dotenv() # Load environment variables from a .env file -audio_path = pathlib.Path( +audio_path = Path( "/Users/karinaassiniandreatta/Documents/06 microsoft_codes/azuresamples/whisper-fine-tuning/src/core/create_data/matis/train_data/audio2.ogg" ) audio_b64 = base64.b64encode(audio_path.read_bytes()).decode("utf-8") @@ -43,10 +44,10 @@ "Authorization": ("Bearer " + api_key), } -req = urllib.request.Request(url, body, headers) +req = Request(url, body, headers) try: - response = urllib.request.urlopen(req) + response = urlopen(req) raw_text = response.read().decode("utf-8") payload = json.loads(raw_text) diff --git a/apps/whisper_fine_tuning/deployment/endpoint/terraform/score.py b/apps/whisper_fine_tuning/deployment/endpoint/terraform/score.py index 8b64add..0cefc6b 100644 --- a/apps/whisper_fine_tuning/deployment/endpoint/terraform/score.py +++ b/apps/whisper_fine_tuning/deployment/endpoint/terraform/score.py @@ -57,8 +57,8 @@ def run(raw_data): continue audio_bytes = base64.b64decode(audio_b64) # Convert bytes to numpy array (float32 PCM, 16kHz expected for Whisper) - import numpy as np import io + import soundfile as sf audio_array, _ = sf.read(io.BytesIO(audio_bytes), dtype="float32") diff --git a/apps/whisper_fine_tuning/deployment/register/README.md b/apps/whisper_fine_tuning/deployment/register/README.md index 3dd7aad..44aff36 100644 --- a/apps/whisper_fine_tuning/deployment/register/README.md +++ b/apps/whisper_fine_tuning/deployment/register/README.md @@ -138,7 +138,6 @@ If you want, I can: az extension update -n ml -example +example az ml online-deployment get-logs -g KA-SAND-RG -w ml-sandbox-core -e whisper-edp-v1 -n whisper-fine-tuned-3 -l 100 - diff --git a/apps/whisper_fine_tuning/deployment/register/register_model.py b/apps/whisper_fine_tuning/deployment/register/register_model.py index 764915a..e4813d4 100644 --- a/apps/whisper_fine_tuning/deployment/register/register_model.py +++ b/apps/whisper_fine_tuning/deployment/register/register_model.py @@ -1,7 +1,7 @@ -import os -import sys import argparse import logging +import os +import sys from datetime import datetime import mlflow @@ -78,7 +78,7 @@ def register_model(run_id: str, model_name: str, artifact_path: str = "model") - # Register the model model_version = mlflow.register_model(model_uri=model_uri, name=model_name) - logger.info(f"Model registered successfully!") + logger.info("Model registered successfully!") logger.info(f"Model name: {model_name}") logger.info(f"Model version: {model_version.version}") logger.info(f"Model URI: {model_uri}") diff --git a/apps/whisper_fine_tuning/src/core/data_prep/README.md b/apps/whisper_fine_tuning/src/core/data_prep/README.md index 31ac02d..8b64e6b 100644 --- a/apps/whisper_fine_tuning/src/core/data_prep/README.md +++ b/apps/whisper_fine_tuning/src/core/data_prep/README.md @@ -215,4 +215,4 @@ The current dataset contains audio samples in what appears to be a indigenous or **Audio loading issues:** - Confirm audio files exist at specified paths -- Check audio file format compatibility \ No newline at end of file +- Check audio file format compatibility diff --git a/apps/whisper_fine_tuning/src/core/train/main_train.py b/apps/whisper_fine_tuning/src/core/train/main_train.py index f5f0317..1b85f46 100644 --- a/apps/whisper_fine_tuning/src/core/train/main_train.py +++ b/apps/whisper_fine_tuning/src/core/train/main_train.py @@ -6,17 +6,13 @@ import logging from datetime import datetime +# Fix the path to point to the repository root where 'src' directory is located +from pathlib import Path + import mlflow import torch from datasets import load_from_disk -from transformers import ( - WhisperForConditionalGeneration, - WhisperProcessor, - WhisperTokenizer, -) - -# Fix the path to point to the repository root where 'src' directory is located -from pathlib import Path +from transformers import WhisperForConditionalGeneration, WhisperProcessor, WhisperTokenizer APP_ROOT = ( Path(__file__).resolve().parents[3] @@ -26,9 +22,10 @@ print(sys.path) from config import ModelLoraConfig, TrainingArgsConfig -from src.core.data_prep.load_data import DataCollatorSpeechSeq2SeqWithPadding from train import Trainer +from src.core.data_prep.load_data import DataCollatorSpeechSeq2SeqWithPadding + # Configure logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) diff --git a/apps/whisper_fine_tuning/src/core/train/main_train_nemo.py b/apps/whisper_fine_tuning/src/core/train/main_train_nemo.py index 058376c..5733f7b 100644 --- a/apps/whisper_fine_tuning/src/core/train/main_train_nemo.py +++ b/apps/whisper_fine_tuning/src/core/train/main_train_nemo.py @@ -6,7 +6,6 @@ import argparse import logging - import sys from datetime import datetime from pathlib import Path diff --git a/apps/whisper_fine_tuning/src/core/train/train.py b/apps/whisper_fine_tuning/src/core/train/train.py index 2da3ad7..ec9e47a 100644 --- a/apps/whisper_fine_tuning/src/core/train/train.py +++ b/apps/whisper_fine_tuning/src/core/train/train.py @@ -13,11 +13,7 @@ import transformers from datasets import Dataset from peft import LoraConfig, PeftModel, get_peft_model, prepare_model_for_kbit_training -from transformers import ( - DataCollatorForSeq2Seq, - Seq2SeqTrainer, - Seq2SeqTrainingArguments, -) +from transformers import DataCollatorForSeq2Seq, Seq2SeqTrainer, Seq2SeqTrainingArguments from config import ModelLoraConfig, TrainingArgsConfig diff --git a/apps/whisper_fine_tuning/src/core/train/train_nemo.py b/apps/whisper_fine_tuning/src/core/train/train_nemo.py index 63a298d..f064ea9 100644 --- a/apps/whisper_fine_tuning/src/core/train/train_nemo.py +++ b/apps/whisper_fine_tuning/src/core/train/train_nemo.py @@ -16,9 +16,7 @@ logger = logging.getLogger(__name__) try: - from nemo.collections.common.parts.mixins.adapter_mixins import ( - LoraConfig as NemoLoraConfig, - ) + from nemo.collections.common.parts.mixins.adapter_mixins import LoraConfig as NemoLoraConfig except ImportError: NemoLoraConfig = None diff --git a/apps/whisper_fine_tuning/src/utils/utils.py b/apps/whisper_fine_tuning/src/utils/utils.py index 7f49880..d7795f8 100644 --- a/apps/whisper_fine_tuning/src/utils/utils.py +++ b/apps/whisper_fine_tuning/src/utils/utils.py @@ -3,7 +3,7 @@ def display_table(dataset_or_sample): - # A helper fuction to display a Transformer dataset or single sample contains multi-line string nicely + # A helper function to display a Transformer dataset or single sample contains multi-line string nicely pd.set_option("display.max_colwidth", None) pd.set_option("display.width", None) pd.set_option("display.max_rows", None) diff --git a/infra/bicep/main.json b/infra/bicep/main.json index 3b78c96..31412f5 100644 --- a/infra/bicep/main.json +++ b/infra/bicep/main.json @@ -890,4 +890,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/infra/bicep/modules/vnet.bicep b/infra/bicep/modules/vnet.bicep index 30c452c..1254368 100644 --- a/infra/bicep/modules/vnet.bicep +++ b/infra/bicep/modules/vnet.bicep @@ -28,7 +28,7 @@ resource managementNSG 'Microsoft.Network/networkSecurityGroups@2020-11-01' = { protocol: 'Tcp' sourcePortRange: '*' destinationPortRange: '22' - sourceAddressPrefix: '*' + sourceAddressPrefix: '*' destinationAddressPrefix: '*' access: 'Allow' priority: 1000