From 39bfeb93558a3ba71cfc6fb1c22f4c0fd77b77e1 Mon Sep 17 00:00:00 2001 From: Miguel Date: Tue, 30 Jul 2024 14:58:19 +0200 Subject: [PATCH] Funcion de LLM mejorada --- .../master_export2translate.cpython-310.pyc | Bin 0 -> 1286 bytes llm_translate_text.py | 158 ++++++++++++------ master_export2translate_translated.xlsx | Bin 10354 -> 0 bytes 3 files changed, 109 insertions(+), 49 deletions(-) create mode 100644 __pycache__/master_export2translate.cpython-310.pyc delete mode 100644 master_export2translate_translated.xlsx diff --git a/__pycache__/master_export2translate.cpython-310.pyc b/__pycache__/master_export2translate.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..398c29adc18cc655df1b460805e5477424d68f05 GIT binary patch literal 1286 zcmZ`&&u`l{6ecOjlAXj|gCa<>4iK=@17pY|7XD1+w=7D# zJmlRo3}^7z^`!e2P|-nx&4%IypX%%=hXLEww%G(AF?1igios~K!VELgpbPI5yBUOR zu$?skoCF8U{^aT8Da)VdT1^fohf6XXEra3U=AF^f8-6`Xh#`_2PtBi!8)&}JBHeaF zrX{V^k&q>2;twP{5b@o2?Dph|6ep0NP?Pag<~0C+|M-XaCwsb}nomlqG?y7))I#c8 z4#lny>JxMIKTMgqC>B*&(i*Ml?p+Uj=p* znDAGf3v@~vU||+AvWNCi1GBY1M9u@LUc$};bhvfu!AWlqts>^L;2bdEv^PY*+;FkF z&h{)e>s*jizag`Y#%CdmUJ;BShi>Elihd&>GD&&|%dZ|5F_qKf{Dp{1%9WNPu0+fi zIgm4uR@b-Id{V1j@Su;Ot9>w5QyW)qXrjFaok6u@)WCrRA0)mZH8Wu)bTuyuM;$1s zB|IX$aC)sx2uLOx7PNfE=;*oyw{K}5EpM(xqB5(gq*ie@pH6d84Tgo7(n5{ESzlXT z|D3LiLMA)yJmeh*2h}WnO7% zf1>G0P8mF-DKAX-0oC-8q$M}uR21{Fg0r-)ixcC|L|&PY<+8HdkrwMaNnozD3S_MU zX^$OU`d#qX`K2u+O*iCR({}a$1nz#0t1V0D4wy|GVITKMr^ m@=Lp6|I and <#>: {text}", - max_tokens=150, # Ajusta esto según tus necesidades - n=1, - stop=None, - temperature=0.3 + logger.info( + f"Solicitando traducción de {source_lang} a {target_lang} para el texto: {text}" ) - return response.choices[0].text.strip() + response = client.chat.completions.create( + model="gpt-3.5-turbo", + messages=[ + {"role": "system", "content": f"You are a translator."}, + { + "role": "user", + "content": f"Translate the following text from {source_lang} to {target_lang} while preserving special fields like <> and <#>. This texts are for an HMI industrial machine: {text}", + }, + ], + max_tokens=150, + temperature=0.3, + ) + translated_text = response.choices[0].message.content.strip() + logger.info(f"Respuesta recibida: {translated_text}") + return translated_text + def translate_batch(texts, source_lang, target_lang): joined_text = "\n".join(texts) - response = openai.Completion.create( - engine="davinci", - prompt=f"Translate the following texts from {source_lang} to {target_lang} while preserving special fields like <> and <#>:\n\n{joined_text}", - max_tokens=1500, # Ajusta esto según tus necesidades - n=1, - stop=None, - temperature=0.3 + logger.info( + f"Solicitando traducción de {source_lang} a {target_lang} para el lote de textos:\n{joined_text}" ) - translations = response.choices[0].text.strip().split('\n') + response = client.chat.completions.create( + model="gpt-3.5-turbo", + messages=[ + {"role": "system", "content": f"You are a translator."}, + { + "role": "user", + "content": f"Translate the following texts from {source_lang} to {target_lang} while preserving special fields like <> and <#>:\n\n{joined_text}", + }, + ], + max_tokens=1500, + temperature=0.3, + ) + translations = response.choices[0].message.content.strip().split("\n") + logger.info(f"Respuestas recibidas:\n{translations}") return translations + def texto_requiere_traduccion(texto): - # Comprobar si hay una palabra con más de 3 letras y si no es solo campos especiales - palabras = re.findall(r'\b\w{4,}\b', texto) - campos_especiales = re.findall(r'<.*?>', texto) - return len(palabras) > 0 and len(campos_especiales) < len(re.findall(r'<#>', texto)) + palabras = re.findall(r"\b\w{4,}\b", texto) + campos_especiales = re.findall(r"<.*?>", texto) + requiere_traduccion = len(palabras) > 0 or len(campos_especiales) != len( + re.findall(r"<#>", texto) + ) + logger.debug( + f"Decisión de traducción para texto '{texto}': {'Sí' if requiere_traduccion else 'No'} (palabras > 3 letras: {len(palabras) > 0}, solo campos especiales: {len(campos_especiales) == len(re.findall(r'<#>', texto))})" + ) + return requiere_traduccion + def main(file_path, target_lang_code, traducir_todo, batch_size=10): - # Cargar el archivo master_export2translate.xlsx df = pd.read_excel(file_path) - - # Identificar la columna clave y la columna de destino source_col = "it-IT" target_col = f"{target_lang_code} Translated" - # Si la columna de destino ya existe, mantenerla o eliminarla según la opción de traducción if target_col in df.columns and not traducir_todo: df[target_col] = df[target_col] else: df[target_col] = None - # Preparar los textos para traducir + texts_to_translate = [] + indices_to_translate = [] + if traducir_todo: - texts_to_translate = [text for text in df[source_col].astype(str).tolist() if texto_requiere_traduccion(text)] + for index, text in df[source_col].astype(str).items(): + processed_text = transformar_texto(text) + if texto_requiere_traduccion(processed_text): + texts_to_translate.append(text) + indices_to_translate.append(index) else: - texts_to_translate = [text for text in df.loc[df[target_col].isnull(), source_col].astype(str).tolist() if texto_requiere_traduccion(text)] + for index, text in ( + df.loc[df[target_col].isnull(), source_col].astype(str).items() + ): + processed_text = transformar_texto(text) + if texto_requiere_traduccion(processed_text): + texts_to_translate.append(text) + indices_to_translate.append(index) + + num_texts = len(texts_to_translate) + logger.info(f"Número total de textos a traducir: {num_texts}") - num_rows = len(texts_to_translate) translations = [] - - for start_idx in range(0, num_rows, batch_size): - end_idx = min(start_idx + batch_size, num_rows) + for start_idx in range(0, num_texts, batch_size): + end_idx = min(start_idx + batch_size, num_texts) batch_texts = texts_to_translate[start_idx:end_idx] - batch_translations = translate_batch(batch_texts, 'Italian', target_lang_code) + batch_translations = translate_batch(batch_texts, "Italian", target_lang_code) translations.extend(batch_translations) - # Asignar las traducciones al DataFrame - if traducir_todo: - df[target_col] = [translate_batch([text], 'Italian', target_lang_code)[0] if texto_requiere_traduccion(text) else text for text in df[source_col].astype(str).tolist()] - else: - indices = df.loc[df[target_col].isnull()].index - for i, index in enumerate(indices): - if texto_requiere_traduccion(df.at[index, source_col]): - df.at[index, target_col] = translations[i] + logger.info(f"Número total de traducciones recibidas: {len(translations)}") - # Guardar el archivo actualizado - output_path = os.path.join(os.path.dirname(file_path), 'master_export2translate_translated.xlsx') + if len(translations) != len(indices_to_translate): + logger.warning( + f"Desajuste entre el número de traducciones ({len(translations)}) y el número de índices ({len(indices_to_translate)})" + ) + + for i, index in enumerate(indices_to_translate): + if i < len(translations): + df.at[index, target_col] = translations[i] + else: + logger.error(f"No hay traducción disponible para el índice {index}") + + output_path = os.path.join( + os.path.dirname(file_path), "master_export2translate_translated.xlsx" + ) df.to_excel(output_path, index=False) + logger.info(f"Archivo traducido guardado en: {output_path}") print(f"Archivo traducido guardado en: {output_path}") + if __name__ == "__main__": batch_size = 10 - translate_file = 'master_export2translate.xlsx' + translate_file = "master_export2translate.xlsx" mostrar_idiomas() seleccion_idioma = int(input("Introduce el número del idioma de destino: ")) @@ -105,5 +163,7 @@ if __name__ == "__main__": print("Selección inválida.") else: _, target_lang_code = IDIOMAS[seleccion_idioma] - traducir_todo = input("¿Desea traducir todas las celdas (sí/no)? ").strip().lower() == 'sí' - main(translate_file, target_lang_code, traducir_todo, batch_size) \ No newline at end of file + traducir_todo = ( + input("¿Desea traducir todas las celdas (s/n)? ").strip().lower() == "s" + ) + main(translate_file, target_lang_code, traducir_todo, batch_size) diff --git a/master_export2translate_translated.xlsx b/master_export2translate_translated.xlsx deleted file mode 100644 index 0883edfee96e4b849acca3295cff535c68be0abb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10354 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