Compare commits

...

3 Commits

12 changed files with 497 additions and 302 deletions

View File

@ -1,12 +1,12 @@
{ {
"codigo_tipo_PLC": "allenbradley", "codigo_tipo_PLC": "siemens",
"codigo_columna_maestra": "es-ES", "codigo_columna_maestra": "it-IT",
"codigo_idioma_seleccionado": "en-US", "codigo_idioma_seleccionado": "es-ES",
"codigo_idioma_secundario": "es-ES", "codigo_idioma_secundario": "en-GB",
"work_dir": "C:/Trabajo/VM/33 - 9.4022 - Shibuya - Usa/Reporte/Language", "work_dir": "C:/Trabajo/VM/38 - 93998 - Sipa - PortoRico/Reporte/Language",
"master_name": "1_hmi_master_translates_allenbradley.xlsx", "master_name": "1_hmi_master_translates_siemens.xlsx",
"translate_name": "2_master_export2translate_allenbradley_en-US.xlsx", "translate_name": "2_master_export2translate_siemens_es-ES.xlsx",
"auto_translate_name": "3_master_export2translate_translated_allenbradley_en-US.xlsx", "auto_translate_name": "3_master_export2translate_translated_siemens_es-ES.xlsx",
"nivel_afinidad_minimo": 0.5, "nivel_afinidad_minimo": 0.5,
"traducir_todo": false "traducir_todo": false
} }

View File

@ -6,6 +6,7 @@ import langid
from openpyxl import load_workbook from openpyxl import load_workbook
from openpyxl.styles import PatternFill, Alignment, Font from openpyxl.styles import PatternFill, Alignment, Font
from collections import defaultdict from collections import defaultdict
from openai_api_key import openai_api_key
# Definir el logger a nivel de módulo # Definir el logger a nivel de módulo
logger = None logger = None
@ -35,73 +36,172 @@ def obtener_nombre_idioma(codigo_corto):
def exportar_para_traduccion(config: TranslationConfig): def exportar_para_traduccion(config: TranslationConfig):
"""
Exporta los textos del archivo maestro para su traducción, realizando validaciones
y cálculos de afinidad en lotes. Los textos idénticos reciben afinidad 1 automáticamente.
"""
master_path = config.get_master_path() master_path = config.get_master_path()
if not os.path.exists(master_path): if not os.path.exists(master_path):
print("El archivo maestro no existe.") print("El archivo maestro no existe.")
return return
configurar_detector_idiomas() configurar_detector_idiomas()
df_maestro = fc.read_dataframe_with_cleanup_retries(master_path) df_maestro = fc.read_dataframe_with_cleanup_retries(master_path)
# Preparar DataFrame de exportación
df_export = pd.DataFrame() df_export = pd.DataFrame()
primera_columna = df_maestro.columns[0] primera_columna = df_maestro.columns[0]
df_export[primera_columna] = df_maestro[primera_columna] df_export[primera_columna] = df_maestro[primera_columna]
df_export[config.codigo_idioma_seleccionado] = df_maestro[
config.codigo_idioma_seleccionado columna_propuesta = f"{config.codigo_idioma_seleccionado}_Propuesto"
] df_export[columna_propuesta] = df_maestro[config.codigo_idioma_seleccionado]
# Agregar columnas de validación si los idiomas son diferentes
if config.codigo_columna_maestra != config.codigo_idioma_seleccionado:
df_export["Validation_Error"] = ""
df_export["Affinity_Score"] = None
df_export["Idioma_Detectado"] = "" df_export["Idioma_Detectado"] = ""
# Agregar columna del idioma secundario
if config.codigo_idioma_secundario in df_maestro.columns:
df_export[config.codigo_idioma_secundario] = df_maestro[config.codigo_idioma_secundario]
ruta_export = config.get_translate_path() ruta_export = config.get_translate_path()
with pd.ExcelWriter(ruta_export, engine="openpyxl") as writer: with pd.ExcelWriter(ruta_export, engine="openpyxl") as writer:
df_export.to_excel(writer, index=False, sheet_name="Sheet1") df_export.to_excel(writer, index=False, sheet_name="Sheet1")
workbook = writer.book workbook = writer.book
worksheet = writer.sheets["Sheet1"] worksheet = writer.sheets["Sheet1"]
# Inmovilizar paneles en A2
worksheet.freeze_panes = "A2"
# Configurar estilos
wrap_alignment = Alignment(wrap_text=True, vertical="top") wrap_alignment = Alignment(wrap_text=True, vertical="top")
for col in ["A", "B"]: red_fill = PatternFill(start_color="FF0000", end_color="FF0000", fill_type="solid")
for cell in worksheet[col]: yellow_fill = PatternFill(start_color="FFFF00", end_color="FFFF00", fill_type="solid")
cell.alignment = wrap_alignment blue_fill = PatternFill(start_color="ADD8E6", end_color="ADD8E6", fill_type="solid")
worksheet.column_dimensions[col].width = 50
idioma_esperado = fc.idiomas_shortcodefromcode( # Ajustar anchos de columna
config.codigo_idioma_seleccionado for col in worksheet.columns:
) max_length = 0
fill = PatternFill(start_color="ADD8E6", end_color="ADD8E6", fill_type="solid") column = col[0].column_letter
bold_font = Font(bold=True)
total_rows = worksheet.max_row - 1 # Excluimos la fila de encabezado for cell in col:
progress_bar = fc.ProgressBar( try:
total_rows, prefix="Procesando filas:", suffix="Completado" if cell.value:
) text_length = len(str(cell.value))
if text_length > 50:
cell.alignment = wrap_alignment
text_length = min(50, max(len(word) for word in str(cell.value).split()))
max_length = max(max_length, text_length)
except:
pass
print("Iniciando procesamiento de filas...") adjusted_width = min(50, max_length + 2)
worksheet.column_dimensions[column].width = adjusted_width if adjusted_width > 8 else 8
# Primera fase: Procesar detección de idioma y recopilar textos para afinidad
texts_to_check = {} # Para textos que necesitan cálculo de afinidad
identical_texts = {} # Para textos idénticos (afinidad 1)
texto_a_filas = defaultdict(list) texto_a_filas = defaultdict(list)
inconsistencias = 0
afinidad_baja = 0
progress_bar = fc.ProgressBar(
worksheet.max_row - 1, prefix="Procesando textos:", suffix="Completado"
)
for row in range(2, worksheet.max_row + 1): for row in range(2, worksheet.max_row + 1):
texto = worksheet.cell(row=row, column=2).value texto_original = worksheet.cell(row=row, column=1).value
if texto: texto_propuesto = worksheet.cell(row=row, column=2).value
texto_limpio = fc.limpiar_texto(config.codigo_tipo_PLC, texto)
if texto_original and texto_propuesto:
# Detección de idioma
texto_limpio = fc.limpiar_texto(config.codigo_tipo_PLC, texto_propuesto)
if texto_propuesto == texto_limpio:
texto_a_filas[texto_propuesto].append(row)
# Solo considerar para duplicados si el texto limpio es igual al original idioma_detectado = detectar_idioma(texto_propuesto, config.codigo_tipo_PLC)
if texto == texto_limpio: idioma_esperado = fc.idiomas_shortcodefromcode(config.codigo_idioma_seleccionado)
texto_a_filas[texto].append(row)
if idioma_detectado != "unknown" and idioma_detectado != idioma_esperado:
# Detectar idioma y marcar si es incorrecto worksheet.cell(row=row, column=2).fill = blue_fill
idioma_detectado = detectar_idioma(texto, config.codigo_tipo_PLC)
if (
idioma_detectado != "unknown"
and idioma_detectado != idioma_esperado
):
worksheet.cell(row=row, column=2).fill = fill
nombre_idioma = obtener_nombre_idioma(idioma_detectado) nombre_idioma = obtener_nombre_idioma(idioma_detectado)
worksheet.cell(row=row, column=3).value = nombre_idioma worksheet.cell(
row=row,
column=df_export.columns.get_loc("Idioma_Detectado") + 1
).value = nombre_idioma
# Recopilar textos para afinidad si los idiomas son diferentes
if config.codigo_columna_maestra != config.codigo_idioma_seleccionado:
if pd.notnull(texto_propuesto) and texto_propuesto.strip() != "":
# Compactar los textos para comparación
texto_original_comp = fc.compactar_celda_traducida(config.codigo_tipo_PLC, str(texto_original))
texto_propuesto_comp = fc.compactar_celda_traducida(config.codigo_tipo_PLC, str(texto_propuesto))
# Si los textos son idénticos después de compactar, afinidad automática de 1
if texto_original_comp == texto_propuesto_comp:
identical_texts[texto_original] = row
else:
texts_to_check[texto_original] = texto_propuesto
progress_bar.increment() progress_bar.increment()
# Marcar celdas duplicadas en negrita progress_bar.finish()
# Configurar el modelo a usar
modelo_llm = fc.LLM_MODELS["OpenAI"] # o el que se prefiera
api_key = openai_api_key() # solo necesario para OpenAI y Grok
# Segunda fase: Procesar textos idénticos y calcular afinidades en lote
if config.codigo_columna_maestra != config.codigo_idioma_seleccionado:
# Asignar afinidad 1 a textos idénticos
logger.info(f"Asignando afinidad 1 a {len(identical_texts)} textos idénticos")
for _, row in identical_texts.items():
col_idx = df_export.columns.get_loc("Affinity_Score") + 1
worksheet.cell(row=row, column=col_idx).value = 1.0
# Calcular afinidades para textos diferentes
if texts_to_check:
logger.info(f"Calculando afinidad para {len(texts_to_check)} textos diferentes")
try:
# Calcular afinidades
affinities = fc.calcular_afinidad_batch(
texts_to_check,
config.codigo_tipo_PLC,
modelo_llm,
logger,
api_key
)
# Aplicar resultados de afinidad
progress_bar = fc.ProgressBar(
len(affinities), prefix="Aplicando afinidades:", suffix="Completado"
)
for texto_original, afinidad in affinities.items():
row = next(row for row in range(2, worksheet.max_row + 1)
if worksheet.cell(row=row, column=1).value == texto_original)
col_idx = df_export.columns.get_loc("Affinity_Score") + 1
worksheet.cell(row=row, column=col_idx).value = afinidad
if afinidad < 1:
worksheet.cell(row=row, column=2).fill = yellow_fill
afinidad_baja += 1
progress_bar.increment()
progress_bar.finish()
except Exception as e:
logger.error(f"Error en el cálculo de afinidad por lotes: {str(e)}")
print(f"Error en el cálculo de afinidad por lotes: {str(e)}")
# Marcar celdas duplicadas
bold_font = Font(bold=True)
celdas_duplicadas = 0 celdas_duplicadas = 0
for filas in texto_a_filas.values(): for filas in texto_a_filas.values():
if len(filas) > 1: if len(filas) > 1:
@ -110,17 +210,18 @@ def exportar_para_traduccion(config: TranslationConfig):
cell.font = bold_font cell.font = bold_font
celdas_duplicadas += len(filas) celdas_duplicadas += len(filas)
progress_bar.finish() # Imprimir resumen
print(f"\nArchivo exportado para traducción: {ruta_export}") print(f"\nArchivo exportado para traducción: {ruta_export}")
print("Las celdas con idioma incorrecto han sido marcadas en azul.") print("Las celdas con idioma incorrecto han sido marcadas en azul.")
print( print("Se ha añadido el nombre del idioma detectado cuando es diferente del esperado.")
"Se ha añadido el nombre del idioma detectado cuando es diferente del esperado." print(f"Se ha agregado la columna del idioma secundario ({config.codigo_idioma_secundario}) al final de la planilla.")
)
print( if config.codigo_columna_maestra != config.codigo_idioma_seleccionado:
f"Se han marcado {celdas_duplicadas} celdas en negrita por tener texto duplicado en la columna del idioma seleccionado." print(f"Se encontraron {len(identical_texts)} textos idénticos (afinidad 1)")
) print(f"Se encontraron {inconsistencias} celdas con errores de validación (marcadas en rojo)")
print(f"Se encontraron {afinidad_baja} celdas con afinidad menor a 1 (marcadas en amarillo)")
print(f"Se han marcado {celdas_duplicadas} celdas en negrita por tener texto duplicado.")
def run(config: TranslationConfig): def run(config: TranslationConfig):
global logger global logger

View File

@ -11,18 +11,19 @@ import html
from tqdm import tqdm from tqdm import tqdm
import PyLibrary.funciones_comunes as fc import PyLibrary.funciones_comunes as fc
import time import time
import PyLibrary.funciones_comunes as fc
from translation_config import TranslationConfig from translation_config import TranslationConfig
from openai import OpenAI from openpyxl.styles import PatternFill, Alignment
from tqdm import tqdm import sys
openai_client = OpenAI(api_key=openai_api_key())
GOOGLE_APPLICATION_CREDENTIALS = "translate-431108-020c17463fbb.json" GOOGLE_APPLICATION_CREDENTIALS = "translate-431108-020c17463fbb.json"
batch_size = 20 batch_size = 20
# Definir el logger a nivel de módulo # Definir el logger a nivel de módulo
logger = None logger = None
# Crear el cliente OpenAI
openai_client = OpenAI(api_key=openai_api_key())
def init_google_translate_client(): def init_google_translate_client():
if os.path.exists(GOOGLE_APPLICATION_CREDENTIALS): if os.path.exists(GOOGLE_APPLICATION_CREDENTIALS):
@ -105,109 +106,11 @@ def translate_batch_openai(texts_dict, source_lang, target_lang):
return dict(zip(texts_dict.keys(), translations)) return dict(zip(texts_dict.keys(), translations))
def affinity_batch_openai(codigo_tipo_PLC, texts_dict):
system_prompt = (
"Evaluate the semantic similarity between the following table of pairs of texts in json format on a scale from 0 to 1. "
"Return the similarity scores for every row in JSON format as a list of numbers, without any additional text or formatting."
)
original_list = [
fc.compactar_celda_traducida(codigo_tipo_PLC, key) for key in texts_dict.keys()
]
re_translated_list = list(texts_dict.values())
request_payload = json.dumps(
{"original": original_list, "compared": re_translated_list}
)
logger.info(f"Solicitando Afinidad para el lote de textos:\n{request_payload}")
response = openai_client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{
"role": "system",
"content": system_prompt,
},
{"role": "user", "content": request_payload},
],
max_tokens=1500,
temperature=0.3,
)
response_content = response.choices[0].message.content
# Limpiar y convertir el contenido de la respuesta
cleaned_response_content = response_content.strip().strip("'```json").strip("```")
# Intentar convertir el contenido a JSON
try:
response_payload = json.loads(cleaned_response_content)
except json.JSONDecodeError:
raise ValueError("La respuesta no se pudo decodificar como JSON.")
# Manejar diferentes formatos de respuesta
if isinstance(response_payload, dict) and "similarity_scores" in response_payload:
scores = response_payload["similarity_scores"]
elif isinstance(response_payload, list):
scores = response_payload
else:
raise ValueError("Formato de respuesta inesperado.")
logger.info(f"Respuestas recibidas:\n{scores}")
if len(scores) != len(original_list):
raise ValueError(
"La cantidad de afinidades recibidas no coincide con la cantidad de textos enviados."
)
return dict(zip(texts_dict.keys(), scores))
# Función que calcula la afinidad entre dos textos
def calcular_afinidad(tipo_PLC, texto1, texto2):
system_prompt = (
"Evaluate the semantic similarity between the following pair of texts on a scale from 0 to 1. "
"Return the similarity score as a single number."
)
original_text = fc.compactar_celda_traducida(tipo_PLC, texto1)
compared_text = texto2
request_payload = json.dumps({"original": original_text, "compared": compared_text})
logger.info(f"Solicitando afinidad para el par de textos:\n{request_payload}")
response = openai_client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{
"role": "system",
"content": system_prompt,
},
{"role": "user", "content": request_payload},
],
max_tokens=1500,
temperature=0.3,
)
response_content = response.choices[0].message.content
# Limpiar y convertir el contenido de la respuesta
cleaned_response_content = response_content.strip().strip("'```json").strip("```")
# Intentar convertir el contenido a JSON
try:
score = float(cleaned_response_content)
except ValueError:
raise ValueError(
f"La respuesta no se pudo decodificar como un número: {cleaned_response_content}"
)
return score
def main(config: TranslationConfig): def main(config: TranslationConfig):
global logger
df = fc.read_dataframe_with_cleanup_retries(config.get_translate_path()) df = fc.read_dataframe_with_cleanup_retries(config.get_translate_path())
source_col = config.codigo_columna_maestra source_col = config.codigo_columna_maestra
source_translated_col = config.codigo_idioma_seleccionado source_translated_col = f"{config.codigo_idioma_seleccionado}_Propuesto"
target_col = f"{config.codigo_idioma_seleccionado} Translated" target_col = f"{config.codigo_idioma_seleccionado} Translated"
check_translate_col = f"{config.codigo_idioma_seleccionado} CheckTranslate" check_translate_col = f"{config.codigo_idioma_seleccionado} CheckTranslate"
affinity_col = f"{config.codigo_idioma_seleccionado} Affinity" affinity_col = f"{config.codigo_idioma_seleccionado} Affinity"
@ -220,22 +123,39 @@ def main(config: TranslationConfig):
texts_to_translate = {} texts_to_translate = {}
# Inicializar ProgressBar para la fase de preparación # Inicializar ProgressBar para la fase de preparación
prep_progress = fc.ProgressBar(len(df), prefix='Preparando textos:', suffix='Completado') prep_progress = fc.ProgressBar(
len(df), prefix="Preparando textos:", suffix="Completado"
)
for index, row in df.iterrows(): for index, row in df.iterrows():
celda_clave = str(row[source_col]) celda_clave = str(row[source_col])
source_translated_text = str(row[source_translated_col]) if source_translated_col in df.columns else "" source_translated_text = (
celda_clave_compactada = fc.compactar_celda_traducida(config.codigo_tipo_PLC, celda_clave) str(row[source_translated_col])
if source_translated_col in df.columns
else ""
)
celda_clave_compactada = fc.compactar_celda_traducida(
config.codigo_tipo_PLC, celda_clave
)
if config.traducir_todo: if config.traducir_todo:
if fc.texto_requiere_traduccion(config.codigo_tipo_PLC, celda_clave_compactada, logger): if fc.texto_requiere_traduccion(
config.codigo_tipo_PLC, celda_clave_compactada, logger
):
df.at[index, source_translated_col] = "" df.at[index, source_translated_col] = ""
texts_to_translate[celda_clave] = celda_clave_compactada texts_to_translate[celda_clave] = celda_clave_compactada
else: else:
if pd.isna(row[source_translated_col]) or source_translated_text.strip() == "": if (
if fc.texto_requiere_traduccion(config.codigo_tipo_PLC, celda_clave_compactada, logger) or fc.texto_con_campos_especiales(config.codigo_tipo_PLC, celda_clave_compactada): pd.isna(row[source_translated_col])
or source_translated_text.strip() == ""
):
if fc.texto_requiere_traduccion(
config.codigo_tipo_PLC, celda_clave_compactada, logger
) or fc.texto_con_campos_especiales(
config.codigo_tipo_PLC, celda_clave_compactada
):
texts_to_translate[celda_clave] = celda_clave_compactada texts_to_translate[celda_clave] = celda_clave_compactada
prep_progress.update(index + 1) prep_progress.update(index + 1)
prep_progress.finish() prep_progress.finish()
@ -245,14 +165,16 @@ def main(config: TranslationConfig):
print(f"\nNúmero total de textos a traducir: {num_texts}") print(f"\nNúmero total de textos a traducir: {num_texts}")
# Inicializar ProgressBar para la fase de traducción # Inicializar ProgressBar para la fase de traducción
trans_progress = fc.ProgressBar(num_texts, prefix='Traduciendo:', suffix='Completado') trans_progress = fc.ProgressBar(
num_texts, prefix="Traduciendo:", suffix="Completado"
)
# Traducciones # Traducciones
translations = {} translations = {}
for start_idx in range(0, num_texts, batch_size): for start_idx in range(0, num_texts, batch_size):
end_idx = min(start_idx + batch_size, num_texts) end_idx = min(start_idx + batch_size, num_texts)
batch_texts = dict(list(texts_to_translate.items())[start_idx:end_idx]) batch_texts = dict(list(texts_to_translate.items())[start_idx:end_idx])
logger.info(f"Traduciendo: celdas desde {start_idx} a {end_idx}.") logger.info(f"Traduciendo: celdas desde {start_idx} a {end_idx}.")
retries = 4 retries = 4
for attempt in range(retries): for attempt in range(retries):
@ -260,26 +182,36 @@ def main(config: TranslationConfig):
batch_translations = translate_batch_openai( batch_translations = translate_batch_openai(
batch_texts, batch_texts,
fc.idiomas_idiomafromcode(config.codigo_columna_maestra), fc.idiomas_idiomafromcode(config.codigo_columna_maestra),
fc.idiomas_idiomafromcode(config.codigo_idioma_seleccionado) fc.idiomas_idiomafromcode(config.codigo_idioma_seleccionado),
) )
translations.update(batch_translations) translations.update(batch_translations)
break break
except Exception as e: except Exception as e:
if attempt < retries - 1: if attempt < retries - 1:
logger.warning(f"Error en el intento {attempt + 1} de traducción de celdas desde {start_idx} a {end_idx}: {e}. Reintentando...") logger.warning(
print(f"Error en el intento {attempt + 1} de traducción de celdas desde {start_idx} a {end_idx}: {e}. Reintentando...") f"Error en el intento {attempt + 1} de traducción de celdas desde {start_idx} a {end_idx}: {e}. Reintentando..."
)
print(
f"Error en el intento {attempt + 1} de traducción de celdas desde {start_idx} a {end_idx}: {e}. Reintentando..."
)
time.sleep(3) time.sleep(3)
else: else:
logger.error(f"Error en todos los intentos de traducción de celdas desde {start_idx} a {end_idx}: {e}") logger.error(
print(f"Error en todos los intentos de traducción de celdas desde {start_idx} a {end_idx}: {e}") f"Error en todos los intentos de traducción de celdas desde {start_idx} a {end_idx}: {e}"
)
print(
f"Error en todos los intentos de traducción de celdas desde {start_idx} a {end_idx}: {e}"
)
trans_progress.update(end_idx) trans_progress.update(end_idx)
trans_progress.finish() trans_progress.finish()
logger.info(f"Número total de traducciones recibidas: {len(translations)}") logger.info(f"Número total de traducciones recibidas: {len(translations)}")
# Inicializar ProgressBar para la fase de actualización del DataFrame # Inicializar ProgressBar para la fase de actualización del DataFrame
update_progress = fc.ProgressBar(len(df), prefix='Actualizando DataFrame:', suffix='Completado') update_progress = fc.ProgressBar(
len(df), prefix="Actualizando DataFrame:", suffix="Completado"
)
# Actualizar el DataFrame con las traducciones y hacemos la Traduccion inversa # Actualizar el DataFrame con las traducciones y hacemos la Traduccion inversa
for index, row in df.iterrows(): for index, row in df.iterrows():
@ -289,63 +221,106 @@ def main(config: TranslationConfig):
try: try:
google_translation = google_translate( google_translation = google_translate(
translations[celda_clave], translations[celda_clave],
fc.idiomas_shortcodefromcode(config.codigo_columna_maestra) fc.idiomas_shortcodefromcode(config.codigo_columna_maestra),
) )
df.at[index, check_translate_col] = google_translation df.at[index, check_translate_col] = google_translation
except Exception as e: except Exception as e:
logger.error(f"Error en la traducción de Google para el texto '{celda_clave}': {e}") logger.error(
f"Error en la traducción de Google para el texto '{celda_clave}': {e}"
)
df.at[index, check_translate_col] = "Error en la traducción" df.at[index, check_translate_col] = "Error en la traducción"
df.at[index, affinity_col] = 0.0 df.at[index, affinity_col] = 0.0
update_progress.increment() update_progress.increment()
update_progress.finish() update_progress.finish()
# Inicializar ProgressBar para la fase de cálculo de afinidad
affinity_progress = fc.ProgressBar(num_texts, prefix='Calculando afinidad:', suffix='Completado') # Configurar el modelo a usar
modelo_llm = fc.LLM_MODELS["OpenAI"] # o el que se prefiera
api_key = openai_api_key() # solo necesario para OpenAI y Grok
# Afinidades # Afinidades
affinities = {} # Los textos ya vienen del proceso de traducción
for start_idx in range(0, num_texts, batch_size): texts_to_check = {}
end_idx = min(start_idx + batch_size, num_texts) for key, translated_text in translations.items():
batch_texts = dict(list(texts_to_translate.items())[start_idx:end_idx]) if pd.notna(translated_text) and str(translated_text).strip() != "":
logger.info(f"Afinidad: celdas desde {start_idx} a {end_idx}.") texts_to_check[key] = translated_text
retries = 2 # Calcular afinidades usando LLM
for attempt in range(retries): affinities_dict = fc.calcular_afinidad_batch(
try: texts_to_check, config.codigo_tipo_PLC, modelo_llm, logger, api_key
batch_affinities = affinity_batch_openai(config.codigo_tipo_PLC, batch_texts) )
affinities.update(batch_affinities)
break
except Exception as e:
if attempt < retries - 1:
logger.warning(f"Error en el intento {attempt + 1} de Afinidad de celdas desde {start_idx} a {end_idx}: {e}. Reintentando...")
print(f"Error en el intento {attempt + 1} de Afinidad de celdas desde {start_idx} a {end_idx}: {e}. Reintentando...")
time.sleep(3)
else:
logger.error(f"Error en todos los intentos de Afinidad de celdas desde {start_idx} a {end_idx}: {e}")
print(f"Error en todos los intentos de Afinidad de celdas desde {start_idx} a {end_idx}: {e}")
for key, value in batch_texts.items():
try:
score = calcular_afinidad(config.codigo_tipo_PLC, key, value)
affinities[key] = score
except Exception as ind_e:
affinities[key] = "0"
logger.error(f"Error en el cálculo individual de Afinidad para el texto '{key}': {ind_e}")
print(f"Error en el cálculo individual de Afinidad para el texto '{key}': {ind_e}")
affinity_progress.increment()
affinity_progress.finish() # Asignar resultados al DataFrame
# Actualizar el DataFrame con las Afinidades
for index, row in df.iterrows(): for index, row in df.iterrows():
celda_clave = str(row[source_col]) key = str(row[source_col])
if celda_clave in affinities: if key in affinities_dict:
df.at[index, affinity_col] = affinities[celda_clave] df.at[index, affinity_col] = affinities_dict[key]
output_path = config.get_auto_translate_path() output_path = config.get_auto_translate_path()
fc.save_dataframe_with_retries(df, output_path=output_path)
with pd.ExcelWriter(output_path, engine="openpyxl") as writer:
df.to_excel(writer, index=False, sheet_name="Sheet1")
workbook = writer.book
worksheet = writer.sheets["Sheet1"]
# Inmovilizar paneles en A2
worksheet.freeze_panes = "A2"
# Configurar ancho de columnas basado en contenido
from openpyxl.utils import get_column_letter
for col in worksheet.columns:
max_length = 0
column = col[0].column_letter
for cell in col:
try:
if cell.value:
text_length = len(str(cell.value))
# Si el texto es más largo que 50, aplicamos wrap_text
if text_length > 50:
cell.alignment = Alignment(wrap_text=True, vertical="top")
text_length = min(
50, max(len(word) for word in str(cell.value).split())
)
max_length = max(max_length, text_length)
except:
pass
# Ajustar el ancho con un pequeño padding
adjusted_width = min(50, max_length + 2)
worksheet.column_dimensions[column].width = (
adjusted_width if adjusted_width > 8 else 8
)
# Colores para el formato condicional
light_blue = PatternFill(
start_color="ADD8E6", end_color="ADD8E6", fill_type="solid"
)
yellow = PatternFill(
start_color="FFFF00", end_color="FFFF00", fill_type="solid"
)
# Aplicar formatos
for row in worksheet.iter_rows(min_row=2):
translated_cell = row[df.columns.get_loc(target_col)]
if translated_cell.value:
affinity_cell = row[df.columns.get_loc(affinity_col)]
try:
affinity_value = float(
affinity_cell.value if affinity_cell.value else 0
)
if affinity_value == 1:
translated_cell.fill = light_blue
elif affinity_value < 1:
translated_cell.fill = yellow
except (ValueError, TypeError):
pass
logger.info(f"Archivo traducido guardado en: {output_path}") logger.info(f"Archivo traducido guardado en: {output_path}")
print(f"\nArchivo traducido guardado en: {output_path}") print(f"\nArchivo traducido guardado en: {output_path}")
def run(config: TranslationConfig): def run(config: TranslationConfig):
global logger global logger
logger = fc.configurar_logger(config.work_dir) logger = fc.configurar_logger(config.work_dir)
@ -353,6 +328,8 @@ def run(config: TranslationConfig):
print(f"\rIniciando: {script_name}\r") print(f"\rIniciando: {script_name}\r")
main(config) main(config)
if __name__ == "__main__": if __name__ == "__main__":
import menu_pasos_traduccion import menu_pasos_traduccion
menu_pasos_traduccion.main()
menu_pasos_traduccion.main()

View File

@ -18,6 +18,7 @@ def importar_traduccion_manual(config: TranslationConfig):
archivo_traduccion = config.get_translate_path() archivo_traduccion = config.get_translate_path()
master_col = config.codigo_idioma_seleccionado master_col = config.codigo_idioma_seleccionado
propuesto_col = f"{config.codigo_idioma_seleccionado}_Propuesto"
df_maestro = fc.read_dataframe_with_cleanup_retries(archivo_maestro) df_maestro = fc.read_dataframe_with_cleanup_retries(archivo_maestro)
df_traduccion = fc.read_dataframe_with_cleanup_retries(archivo_traduccion) df_traduccion = fc.read_dataframe_with_cleanup_retries(archivo_traduccion)
@ -28,7 +29,7 @@ def importar_traduccion_manual(config: TranslationConfig):
for index, fila in df_traduccion.iterrows(): for index, fila in df_traduccion.iterrows():
clave = fila[df_maestro.columns[0]] clave = fila[df_maestro.columns[0]]
if clave in df_maestro[df_maestro.columns[0]].values: if clave in df_maestro[df_maestro.columns[0]].values:
valor_traducido = fila[master_col] valor_traducido = fila[propuesto_col] # Use propuesto column
valor_original = df_maestro.loc[ valor_original = df_maestro.loc[
df_maestro[df_maestro.columns[0]] == clave, master_col df_maestro[df_maestro.columns[0]] == clave, master_col
].values[0] ].values[0]
@ -38,6 +39,7 @@ def importar_traduccion_manual(config: TranslationConfig):
and valor_traducido != "" and valor_traducido != ""
and str(valor_original) != str(valor_traducido) and str(valor_original) != str(valor_traducido)
): ):
okToSave, Error = fc.verificar_celda_traducida( okToSave, Error = fc.verificar_celda_traducida(
config.codigo_tipo_PLC, clave, valor_traducido config.codigo_tipo_PLC, clave, valor_traducido
) )
@ -62,7 +64,7 @@ def importar_traduccion_manual(config: TranslationConfig):
f"Fila {index}, Columna {master_col}: No actualizado porque: {Error}" f"Fila {index}, Columna {master_col}: No actualizado porque: {Error}"
) )
fila_excel = index + 2 fila_excel = index + 2
columna_excel = df_traduccion.columns.get_loc(master_col) + 1 columna_excel = df_traduccion.columns.get_loc(propuesto_col) + 1
celdas_con_errores[(fila_excel, columna_excel)] = Error celdas_con_errores[(fila_excel, columna_excel)] = Error
fc.save_dataframe_with_retries(df_maestro, output_path=archivo_maestro) fc.save_dataframe_with_retries(df_maestro, output_path=archivo_maestro)

View File

@ -4,6 +4,7 @@ import os
from translation_config import TranslationConfig from translation_config import TranslationConfig
from openpyxl import load_workbook from openpyxl import load_workbook
from openpyxl.styles import Font from openpyxl.styles import Font
from openpyxl.styles import PatternFill, Alignment, Font
# Definir el logger a nivel de módulo # Definir el logger a nivel de módulo
logger = None logger = None
@ -16,6 +17,7 @@ def importar_traduccion(config: TranslationConfig):
return return
master_col = config.codigo_idioma_seleccionado master_col = config.codigo_idioma_seleccionado
master_propuesto_col = f"{master_col}_Propuesto"
translated_col = f"{config.codigo_idioma_seleccionado} Translated" translated_col = f"{config.codigo_idioma_seleccionado} Translated"
affinity_col = f"{config.codigo_idioma_seleccionado} Affinity" affinity_col = f"{config.codigo_idioma_seleccionado} Affinity"
@ -24,47 +26,106 @@ def importar_traduccion(config: TranslationConfig):
config.get_auto_translate_path() config.get_auto_translate_path()
) )
# Caso especial: columna maestra es igual al idioma seleccionado
is_same_column = config.codigo_columna_maestra == config.codigo_idioma_seleccionado
# Si es el mismo, asegurarse que existe la columna propuesta
if is_same_column and master_propuesto_col not in df_maestro.columns:
df_maestro[master_propuesto_col] = df_maestro[master_col]
celdas_modificadas = {} celdas_modificadas = {}
if config.codigo_columna_maestra not in df_maestro.columns:
print(f"Error: Columnas requeridas no encontradas en el archivo maestro")
return
if (
config.codigo_columna_maestra not in df_traduccion.columns
or translated_col not in df_traduccion.columns
or affinity_col not in df_traduccion.columns
):
print(f"Error: Columnas requeridas no encontradas en el archivo de traducción")
return
for index, fila in df_traduccion.iterrows(): for index, fila in df_traduccion.iterrows():
clave = fila[df_maestro.columns[0]] clave = fila[config.codigo_columna_maestra]
if clave in df_maestro[df_maestro.columns[0]].values: mascara = df_maestro[config.codigo_columna_maestra] == clave
if mascara.any():
if ( if (
fila[affinity_col] >= config.nivel_afinidad_minimo pd.notna(fila[affinity_col])
and pd.notnull(fila[translated_col]) and fila[affinity_col] >= config.nivel_afinidad_minimo
and fila[translated_col] != "" and pd.notna(fila[translated_col])
): ):
valor_traducido = fila[translated_col] valor_traducido = fila[translated_col]
valor_original = df_maestro.loc[ indice_maestro = df_maestro.index[mascara].tolist()[0]
df_maestro[df_maestro.columns[0]] == clave, master_col
].values[0] # Determinar la columna a actualizar según el caso
columna_destino = master_propuesto_col if is_same_column else master_col
valor_original = df_maestro.loc[indice_maestro, columna_destino]
if str(valor_original) != str(valor_traducido): if str(valor_original) != str(valor_traducido):
df_maestro.loc[ df_maestro.loc[indice_maestro, columna_destino] = valor_traducido
df_maestro[df_maestro.columns[0]] == clave, master_col
] = valor_traducido
logger.info( logger.info(
f'Fila {index}, Columna {translated_col}: "{valor_original}" actualizado a "{valor_traducido}"' f'Fila {index}, Columna {columna_destino}: "{valor_original}" actualizado a "{valor_traducido}"'
) )
fila_excel = ( fila_excel = indice_maestro + 2
df_maestro.index[ columna_excel = df_maestro.columns.get_loc(columna_destino) + 1
df_maestro[df_maestro.columns[0]] == clave
].tolist()[0]
+ 2
)
columna_excel = df_maestro.columns.get_loc(master_col) + 1
celdas_modificadas[(fila_excel, columna_excel)] = valor_traducido celdas_modificadas[(fila_excel, columna_excel)] = valor_traducido
else :
logger.error( # Guardar con formato Excel
f'Clave {clave} no encontrada en master.' with pd.ExcelWriter(archivo_maestro, engine="openpyxl") as writer:
df_maestro.to_excel(writer, index=False, sheet_name="Sheet1")
workbook = writer.book
worksheet = writer.sheets["Sheet1"]
# Inmovilizar paneles en A2
worksheet.freeze_panes = "A2"
# Configurar ancho de columnas basado en contenido
from openpyxl.utils import get_column_letter
from openpyxl.styles import Alignment, Font, PatternFill
for col in worksheet.columns:
max_length = 0
column = col[0].column_letter
for cell in col:
try:
if cell.value:
text_length = len(str(cell.value))
if text_length > 50:
cell.alignment = Alignment(wrap_text=True, vertical="top")
text_length = min(
50, max(len(word) for word in str(cell.value).split())
)
max_length = max(max_length, text_length)
except:
pass
adjusted_width = min(50, max_length + 2)
worksheet.column_dimensions[column].width = (
adjusted_width if adjusted_width > 8 else 8
) )
fc.save_dataframe_with_retries(df_maestro, output_path=archivo_maestro) # Aplicar negrita y color a las celdas modificadas
light_blue = PatternFill(
start_color="ADD8E6", end_color="ADD8E6", fill_type="solid"
)
for (fila, columna), valor in celdas_modificadas.items():
celda = worksheet.cell(row=fila, column=columna)
celda.font = Font(bold=True)
celda.value = valor
celda.fill = light_blue
aplicar_negrita_celdas_modificadas(archivo_maestro, celdas_modificadas) if celdas_modificadas:
print(
print(f"Traducciones importadas y archivo maestro actualizado: {archivo_maestro}.") f"Se han actualizado y marcado {len(celdas_modificadas)} celdas en el archivo maestro."
)
else:
print("No se realizaron modificaciones en el archivo maestro.")
def aplicar_negrita_celdas_modificadas(archivo, celdas_modificadas): def aplicar_negrita_celdas_modificadas(archivo, celdas_modificadas):

View File

@ -16,91 +16,139 @@ def update_from_master(config: TranslationConfig, archivo_to_update):
print("El archivo maestro no existe.") print("El archivo maestro no existe.")
return return
logger.info(" .... ") logger.info(f"Iniciando actualización en {archivo_to_update} desde el archivo maestro. Para {config.codigo_idioma_seleccionado}")
logger.info(
f"Iniciando actualización en {archivo_to_update} desde el archivo maestro. Para {config.codigo_idioma_seleccionado}"
)
df_maestro = fc.read_dataframe_with_cleanup_retries(archivo_maestro) df_maestro = fc.read_dataframe_with_cleanup_retries(archivo_maestro)
df_to_update = fc.read_dataframe_with_cleanup_retries(archivo_to_update) df_to_update = fc.read_dataframe_with_cleanup_retries(archivo_to_update)
# Create copy for changes tracking
df_changes = df_to_update.copy()
df_changes["Original_Value"] = ""
col_clave = config.codigo_columna_maestra col_clave = config.codigo_columna_maestra
# Si la columna maestra es igual al idioma seleccionado, usamos la columna propuesta
is_same_column = config.codigo_columna_maestra == config.codigo_idioma_seleccionado
master_col = f"{config.codigo_idioma_seleccionado}_Propuesto" if is_same_column else config.codigo_idioma_seleccionado
if master_col not in df_maestro.columns:
print(f"Error: Columna {master_col} no encontrada en el archivo maestro")
return
celdas_modificadas = []
celdas_con_errores = {} progress_bar = fc.ProgressBar(len(df_to_update), prefix="Actualizando filas:", suffix="Completado")
celdas_vacias = {}
progress_bar = fc.ProgressBar(
len(df_to_update), prefix="Actualizando filas:", suffix="Completado"
)
for index, fila in df_to_update.iterrows(): for index, fila in df_to_update.iterrows():
valor_original = fila[col_clave] valor_original = fila[col_clave]
clave = fc.compactar_celda_clave(config.codigo_tipo_PLC, valor_original) clave = fc.compactar_celda_clave(config.codigo_tipo_PLC, valor_original)
if not pd.isna(clave) and clave in df_maestro[col_clave].values: if not pd.isna(clave) and clave in df_maestro[col_clave].values:
# logger.info(f"Fila {index} : Clave: {clave}") indice_maestro = df_maestro.index[df_maestro[col_clave] == clave].tolist()[0]
valor_traducido_compacto = df_maestro.loc[indice_maestro, master_col]
indice_maestro = df_maestro.index[df_maestro[col_clave] == clave].tolist()[
0
]
valor_traducido_compacto = df_maestro.loc[
indice_maestro, config.codigo_idioma_seleccionado
]
if pd.isna(valor_traducido_compacto): if pd.isna(valor_traducido_compacto):
celdas_vacias[indice_maestro] = "Celda vacía en el archivo maestro" continue
logger.info(f"Fila {index}: Celda vacía en el archivo maestro")
else: valor_traducido = fc.decompactar_celda_traducida(
valor_traducido = fc.decompactar_celda_traducida( config.codigo_tipo_PLC,
config.codigo_tipo_PLC, celda_original=valor_original,
celda_original=valor_original, celda_traducida=valor_traducido_compacto
celda_traducida=valor_traducido_compacto, )
if not pd.isna(valor_traducido) and fila[config.codigo_idioma_seleccionado] != valor_traducido:
okToSave, Error = fc.verificar_celda_traducida(
config.codigo_tipo_PLC, clave, valor_traducido_compacto
) )
if ( if okToSave:
not pd.isna(valor_traducido) # Store original value in changes DataFrame
and fila[config.codigo_idioma_seleccionado] != valor_traducido df_changes.at[index, "Original_Value"] = fila[config.codigo_idioma_seleccionado]
):
okToSave, Error = fc.verificar_celda_traducida( # Update both DataFrames
config.codigo_tipo_PLC, clave, valor_traducido_compacto df_to_update.at[index, config.codigo_idioma_seleccionado] = valor_traducido
) df_changes.at[index, config.codigo_idioma_seleccionado] = valor_traducido
if okToSave:
logger.info(f"Actualizado: Fila {index} : Clave: {clave}") celdas_modificadas.append(index)
df_to_update.at[index, config.codigo_idioma_seleccionado] = ( logger.info(f"Actualizado: Fila {index} : Clave: {clave}")
valor_traducido
)
else:
df_to_update.at[index, config.codigo_idioma_seleccionado] = (
valor_original
)
logger.error(f"No actualizado porque: {Error}")
celdas_con_errores[indice_maestro] = Error
progress_bar.increment() progress_bar.increment()
progress_bar.finish() progress_bar.finish()
# Save updated file with formatting
nombre, extension = os.path.splitext(archivo_to_update) nombre, extension = os.path.splitext(archivo_to_update)
nuevo_nombre = f"{nombre}_import{extension}" nuevo_nombre = f"{nombre}_import{extension}"
fc.save_dataframe_with_retries(df_to_update, output_path=nuevo_nombre)
with pd.ExcelWriter(nuevo_nombre, engine='openpyxl') as writer:
df_to_update.to_excel(writer, sheet_name="User Texts", index=False)
workbook = writer.book
worksheet = writer.sheets["User Texts"]
# Format columns
from openpyxl.utils import get_column_letter
from openpyxl.styles import Alignment, PatternFill
for col in worksheet.columns:
max_length = 0
column = col[0].column_letter
for cell in col:
try:
if cell.value:
text_length = len(str(cell.value))
if text_length > 50:
cell.alignment = Alignment(wrap_text=True, vertical='top')
text_length = min(50, max(len(word) for word in str(cell.value).split()))
max_length = max(max_length, text_length)
except:
pass
adjusted_width = min(50, max_length + 2)
worksheet.column_dimensions[column].width = adjusted_width if adjusted_width > 8 else 8
marcar_celdas_con_errores( # Save changes file with highlighting
archivo_maestro, changes_nombre = f"{nombre}_changes{extension}"
celdas_con_errores, if len(celdas_modificadas) > 0:
celdas_vacias, with pd.ExcelWriter(changes_nombre, engine="openpyxl") as writer:
config.codigo_idioma_seleccionado, df_changes.to_excel(writer, index=False)
)
print( workbook = writer.book
f"Se han actualizado las filas en {archivo_to_update} desde el archivo maestro." worksheet = writer.sheets["User Texts"]
)
print( light_blue = PatternFill(start_color="ADD8E6", end_color="ADD8E6", fill_type="solid")
f"Se han marcado {len(celdas_con_errores)} celdas con errores en el archivo maestro."
) for row_idx in celdas_modificadas:
print(f"Se han marcado {len(celdas_vacias)} celdas vacías en el archivo maestro.") for col in range(1, len(df_changes.columns) + 1):
cell = worksheet.cell(row=row_idx + 2, column=col)
cell.fill = light_blue
# Format columns in changes file too
for col in worksheet.columns:
max_length = 0
column = col[0].column_letter
for cell in col:
try:
if cell.value:
text_length = len(str(cell.value))
if text_length > 50:
cell.alignment = Alignment(wrap_text=True, vertical='top')
text_length = min(50, max(len(word) for word in str(cell.value).split()))
max_length = max(max_length, text_length)
except:
pass
adjusted_width = min(50, max_length + 2)
worksheet.column_dimensions[column].width = adjusted_width if adjusted_width > 8 else 8
print(f"Se han actualizado las filas en {archivo_to_update} desde el archivo maestro.")
print(f"Archivo de cambios guardado en: {changes_nombre}")
print(f"Se han marcado {len(celdas_modificadas)} filas modificadas.")
logger.info(" .... ") logger.info(" .... ")
def marcar_celdas_con_errores(
def marcar_celdas_con_errores(archivo_maestro, celdas_con_errores, celdas_vacias, target_lang_code): archivo_maestro, celdas_con_errores, celdas_vacias, target_lang_code
):
workbook = load_workbook(archivo_maestro) workbook = load_workbook(archivo_maestro)
sheet = workbook.active sheet = workbook.active
@ -112,8 +160,12 @@ def marcar_celdas_con_errores(archivo_maestro, celdas_con_errores, celdas_vacias
print(f"No se encontró la columna para el idioma {target_lang_code}") print(f"No se encontró la columna para el idioma {target_lang_code}")
return return
error_fill = PatternFill(start_color="FF0000", end_color="FF0000", fill_type="solid") error_fill = PatternFill(
empty_fill = PatternFill(start_color="FFFF00", end_color="FFFF00", fill_type="solid") start_color="FF0000", end_color="FF0000", fill_type="solid"
)
empty_fill = PatternFill(
start_color="FFFF00", end_color="FFFF00", fill_type="solid"
)
white_font = Font(color="FFFFFF") # Fuente blanca para celdas con fondo rojo white_font = Font(color="FFFFFF") # Fuente blanca para celdas con fondo rojo
for indice_maestro, mensaje_error in celdas_con_errores.items(): for indice_maestro, mensaje_error in celdas_con_errores.items():
@ -132,7 +184,9 @@ def marcar_celdas_con_errores(archivo_maestro, celdas_con_errores, celdas_vacias
cell.comment = comment cell.comment = comment
workbook.save(archivo_maestro) workbook.save(archivo_maestro)
print(f"Se han marcado las celdas con errores y vacías en el archivo maestro {archivo_maestro}") print(
f"Se han marcado las celdas con errores y vacías en el archivo maestro {archivo_maestro}"
)
def run(config: TranslationConfig, archivo_to_update): def run(config: TranslationConfig, archivo_to_update):