|
11 | 11 |
|
12 | 12 | # Local application imports |
13 | 13 | from model_ai.models import LlamaModel |
| 14 | +from .choices import order_labels |
14 | 15 |
|
15 | 16 |
|
16 | 17 | MODEL_NAME_GEMINI = 'GEMINI' |
@@ -836,3 +837,302 @@ def extract_keywords(text): |
836 | 837 | clean_keywords = ", ".join(keywords) |
837 | 838 |
|
838 | 839 | return {"title": label, "keywords": clean_keywords} |
| 840 | + |
| 841 | + |
| 842 | +def create_special_content_object(item, stream_data_body, counts): |
| 843 | + """Create objects for special content types (image, table, list, compound)""" |
| 844 | + obj = {} |
| 845 | + |
| 846 | + if item.get('type') == 'image': |
| 847 | + obj = {} |
| 848 | + counts['numfig'] += 1 |
| 849 | + obj['type'] = 'image' |
| 850 | + obj['value'] = { |
| 851 | + 'figid' : f"f{counts['numfig']}", |
| 852 | + 'label' : '<fig>', |
| 853 | + 'image' : item.get('image') |
| 854 | + } |
| 855 | + |
| 856 | + #Obitiene el elemento aterior |
| 857 | + try: |
| 858 | + prev_element = stream_data_body[-1] |
| 859 | + label_title = extract_label_and_title(prev_element['value']['paragraph']) |
| 860 | + obj['value']['figlabel'] = label_title['label'] |
| 861 | + obj['value']['title'] = label_title['title'] |
| 862 | + stream_data_body.pop(-1) |
| 863 | + except: |
| 864 | + pass |
| 865 | + |
| 866 | + elif item.get('type') == 'table': |
| 867 | + obj = {} |
| 868 | + counts['numtab'] += 1 |
| 869 | + obj['type'] = 'table' |
| 870 | + obj['value'] = { |
| 871 | + 'tabid' : f"t{counts['numtab']}", |
| 872 | + 'label' : '<table>', |
| 873 | + 'content' : item.get('table') |
| 874 | + } |
| 875 | + |
| 876 | + #Obitiene el elemento aterior |
| 877 | + try: |
| 878 | + prev_element = stream_data_body[-1] |
| 879 | + label_title = extract_label_and_title(prev_element['value']['paragraph']) |
| 880 | + obj['value']['tablabel'] = label_title['label'] |
| 881 | + obj['value']['title'] = label_title['title'] |
| 882 | + stream_data_body.pop(-1) |
| 883 | + except: |
| 884 | + #No hay elemento anterior |
| 885 | + pass |
| 886 | + |
| 887 | + elif item.get('type') == 'list': |
| 888 | + obj = {} |
| 889 | + obj['type'] = 'paragraph' |
| 890 | + obj['value'] = { |
| 891 | + 'label' : '<list>', |
| 892 | + 'paragraph' : item.get('list') |
| 893 | + } |
| 894 | + |
| 895 | + elif item.get('type') == 'compound': |
| 896 | + obj = {} |
| 897 | + counts['numeq'] += 1 |
| 898 | + obj['type'] = 'compound_paragraph' |
| 899 | + obj['value'] = { |
| 900 | + 'eid' : f"e{counts['numeq']}", |
| 901 | + #'label' : '<formula>', |
| 902 | + 'content': item.get('text') |
| 903 | + } |
| 904 | + text_count = sum( |
| 905 | + 1 for c in obj['value']['content'] |
| 906 | + if c['type'] == 'text' |
| 907 | + ) |
| 908 | + |
| 909 | + if text_count > 1: |
| 910 | + obj['value']['label'] = '<inline-formula>' |
| 911 | + return obj, counts |
| 912 | + |
| 913 | + if text_count == 0: |
| 914 | + obj['value']['label'] = '<disp-formula>' |
| 915 | + return obj, counts |
| 916 | + |
| 917 | + text_value = next( |
| 918 | + item['value'] |
| 919 | + for item in obj['value']['content'] |
| 920 | + if item['type'] == 'text' |
| 921 | + ) |
| 922 | + text = is_number_parenthesis(text_value) |
| 923 | + if text: |
| 924 | + obj['value']['label'] = '<disp-formula>' |
| 925 | + next( |
| 926 | + item |
| 927 | + for item in obj['value']['content'] |
| 928 | + if item['type'] == 'text' |
| 929 | + )['value'] = text |
| 930 | + else: |
| 931 | + obj['value']['label'] = '<inline-formula>' |
| 932 | + |
| 933 | + return obj, counts |
| 934 | + |
| 935 | + |
| 936 | +def extract_subsection(text): |
| 937 | + # Quitar punto final si existe |
| 938 | + text = text.strip() |
| 939 | + |
| 940 | + # Ver si contiene una etiqueta con dos puntos |
| 941 | + match = re.match(r'(?i)\s*(.+?)\s*:\s*(.+)', text) |
| 942 | + |
| 943 | + if match: |
| 944 | + label = match.group(1).strip() |
| 945 | + content = match.group(2).strip() |
| 946 | + else: |
| 947 | + label = None |
| 948 | + content = text |
| 949 | + |
| 950 | + return {"title": label, "content": content} |
| 951 | + |
| 952 | + |
| 953 | +def search_special_id(data_body, label): |
| 954 | + for d in data_body: |
| 955 | + if d['type'] in ['image', 'table']: |
| 956 | + data = d['value'] |
| 957 | + clean_label = re.sub(r'^[\s\.,;:–—-]+', '', label).capitalize() |
| 958 | + |
| 959 | + if d['type'] == 'image': |
| 960 | + if clean_label == data['figlabel']: |
| 961 | + return data.get('figid') |
| 962 | + if data['figid'][0] == clean_label.lower()[0] and data['figid'][1] in clean_label.lower(): |
| 963 | + return data.get('figid') |
| 964 | + |
| 965 | + if d['type'] == 'table': |
| 966 | + if clean_label == data['tablabel']: |
| 967 | + return data.get('tabid') |
| 968 | + if data['tabid'][0] == clean_label.lower()[0] and data['tabid'][1] in clean_label.lower(): |
| 969 | + return data.get('tabid') |
| 970 | + |
| 971 | + for d in data_body: |
| 972 | + if d['type'] in ['compound_paragraph']: |
| 973 | + data = d['value'] |
| 974 | + clean_label = re.sub(r'^[\s\.,;:–—-]+', '', label).lower() |
| 975 | + |
| 976 | + if d['type'] == 'compound_paragraph': |
| 977 | + if data['eid'][0] in clean_label[0] and data['eid'][1] in clean_label: |
| 978 | + return data.get('eid') |
| 979 | + |
| 980 | + return None |
| 981 | + |
| 982 | + |
| 983 | +def is_number_parenthesis(text): |
| 984 | + pattern = re.compile(r'^\s*\(\s*(\d+)\s*\)\s*$') |
| 985 | + match = pattern.fullmatch(text) |
| 986 | + if match: |
| 987 | + return f"({match.group(1)})" |
| 988 | + return None |
| 989 | + |
| 990 | + |
| 991 | +def remove_unpaired_tags(text): |
| 992 | + # Match opening/closing tags, capturing only the tag name (before any space or >) |
| 993 | + pattern = re.compile(r'<(/?)([a-zA-Z0-9]+)(?:\s[^>]*)?>') |
| 994 | + |
| 995 | + result = [] |
| 996 | + stack = [] # Stores (tag_name, position_in_result) |
| 997 | + |
| 998 | + i = 0 |
| 999 | + for match in pattern.finditer(text): |
| 1000 | + is_closing, tag_name = match.groups() |
| 1001 | + is_closing = bool(is_closing) |
| 1002 | + |
| 1003 | + # Text between tags |
| 1004 | + if match.start() > i: |
| 1005 | + result.append(text[i:match.start()]) |
| 1006 | + |
| 1007 | + tag_text = text[match.start():match.end()] |
| 1008 | + |
| 1009 | + if not is_closing: |
| 1010 | + # Opening tag |
| 1011 | + stack.append((tag_name, len(result))) |
| 1012 | + result.append(tag_text) |
| 1013 | + else: |
| 1014 | + # Closing tag |
| 1015 | + if stack and stack[-1][0] == tag_name: |
| 1016 | + stack.pop() |
| 1017 | + result.append(tag_text) |
| 1018 | + else: |
| 1019 | + # Orphan closing tag - skip |
| 1020 | + pass |
| 1021 | + |
| 1022 | + i = match.end() |
| 1023 | + |
| 1024 | + # Append remaining text |
| 1025 | + if i < len(text): |
| 1026 | + result.append(text[i:]) |
| 1027 | + |
| 1028 | + # Remove unclosed opening tags |
| 1029 | + for tag_name, pos in sorted(stack, reverse=True, key=lambda x: x[1]): |
| 1030 | + result.pop(pos) |
| 1031 | + |
| 1032 | + return ''.join(result) |
| 1033 | + |
| 1034 | + |
| 1035 | +def append_fragment(node_dest, val): |
| 1036 | + if not val: |
| 1037 | + parent = node_dest.getparent() |
| 1038 | + if parent: |
| 1039 | + parent.remove(node_dest) |
| 1040 | + return |
| 1041 | + |
| 1042 | + # 1) Limpiezas mínimas |
| 1043 | + # - eliminar <br> / <br/> |
| 1044 | + # - quitar saltos de línea |
| 1045 | + clean = re.sub(r"(?i)<br\s*/?>", "", val) |
| 1046 | + clean = clean.replace("\n", "") |
| 1047 | + |
| 1048 | + # normaliza entidades problemáticas |
| 1049 | + clean = clean.replace(" ", " ") |
| 1050 | + clean = re.sub(r'&(?!\w+;|#\d+;)', '&', clean) |
| 1051 | + |
| 1052 | + clean = remove_unpaired_tags(clean) |
| 1053 | + |
| 1054 | + if clean == "": |
| 1055 | + parent = node_dest.getparent() |
| 1056 | + if parent: |
| 1057 | + parent.remove(node_dest) |
| 1058 | + return |
| 1059 | + |
| 1060 | + # 2) Si no hay etiquetas, es texto plano |
| 1061 | + if "<" not in clean: |
| 1062 | + node_dest.text = (node_dest.text or "") + clean |
| 1063 | + return |
| 1064 | + |
| 1065 | + # 3) Envolver para que sea XML bien formado aunque empiece con texto |
| 1066 | + wrapper = etree.XML(f"<_wrap_>{clean}</_wrap_>") |
| 1067 | + |
| 1068 | + # 4) Pasar el texto inicial (antes del primer tag) |
| 1069 | + if wrapper.text: |
| 1070 | + node_dest.text = (node_dest.text or "") + wrapper.text |
| 1071 | + |
| 1072 | + # 5) Mover cada hijo al destino (sus .tail se conservan) |
| 1073 | + for child in list(wrapper): |
| 1074 | + node_dest.append(child) |
| 1075 | + |
| 1076 | + |
| 1077 | +def extract_label_and_title(text): |
| 1078 | + """ |
| 1079 | + Extrae el Label (Figura/Figure/Tabla/Table/Tabela + número) y el Title (resto del texto limpio). |
| 1080 | + Ignora mayúsculas y minúsculas y limpia puntuación/espacios entre el número y el título. |
| 1081 | + """ |
| 1082 | + # Acepta Figura/Figure y Tabla/Table/Tabela |
| 1083 | + pattern = r'\b(Imagen|Imágen|Image|Imagem|Figura|Figure|Tabla|Table|Tabela)\s+(\d+)\b' |
| 1084 | + match = re.search(pattern, text, re.IGNORECASE) |
| 1085 | + |
| 1086 | + if match: |
| 1087 | + word = match.group(1).capitalize() # Normaliza capitalización |
| 1088 | + number = match.group(2) |
| 1089 | + label = f"{word} {number}" |
| 1090 | + |
| 1091 | + # Texto después del número |
| 1092 | + rest = text[match.end():] |
| 1093 | + |
| 1094 | + # Quita puntuación/espacios iniciales (.,;: guiones, etc.) |
| 1095 | + rest_clean = re.sub(r'^[\s\.,;:–—-]+', '', rest) |
| 1096 | + |
| 1097 | + return {"label": label, "title": rest_clean.strip()} |
| 1098 | + else: |
| 1099 | + return {"label": None, "title": text.strip()} |
| 1100 | + |
| 1101 | + |
| 1102 | +def proccess_special_content(text, data_body): |
| 1103 | + # normaliza espacios no separables por si acaso |
| 1104 | + text = re.sub(r'[\u00A0\u2007\u202F]', ' ', text) |
| 1105 | + |
| 1106 | + pattern = r""" |
| 1107 | + (?<!\w) # inicio no al medio de una palabra |
| 1108 | + (?: |
| 1109 | + Imagen|Imágen|Image|Imagem| |
| 1110 | + Figura|Figure| |
| 1111 | + Tabla|Table|Tabela| |
| 1112 | + Ecuaci[oó]n|Equa(?:ç[aã]o|cao)|Equation| |
| 1113 | + F[oó]rmula|Formula| |
| 1114 | + Eq\.|Ec\.|Form\.|F[óo]rm\. |
| 1115 | + )\s* |
| 1116 | + (?:\(\s*\d+\s*\)|\d+) # 1 o (1) |
| 1117 | + (?!\w) # que no siga una letra/número |
| 1118 | + """ |
| 1119 | + |
| 1120 | + res = [] |
| 1121 | + dict_type = {'f': 'fig', 't': 'table', 'e': 'disp-formula'} |
| 1122 | + |
| 1123 | + try: |
| 1124 | + for match in re.finditer(pattern, text, re.IGNORECASE | re.UNICODE | re.VERBOSE): |
| 1125 | + label = match.group(0) |
| 1126 | + |
| 1127 | + id = search_special_id(data_body, label) |
| 1128 | + |
| 1129 | + res.append({ |
| 1130 | + "label": label, |
| 1131 | + "id": id, |
| 1132 | + "reftype": dict_type.get(id[0].lower(), 'other') |
| 1133 | + }) |
| 1134 | + except Exception as exc: |
| 1135 | + print(f'ERROR proccess_special_content: {exc}') |
| 1136 | + pass |
| 1137 | + |
| 1138 | + return res |
0 commit comments