RDTvlokip commited on
Commit
ea3f62e
·
verified ·
1 Parent(s): 0ee325f

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +243 -0
app.py ADDED
@@ -0,0 +1,243 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ AG-BPE Standalone Usage Script & Web Visualizer
4
+ ================================================
5
+
6
+ This script demonstrates how to load and use a pre-trained AG-BPE tokenizer
7
+ and provides a real-time web interface using Gradio to visualize its behavior.
8
+
9
+ It defines a self-contained AGBPETokenizer class and then launches a web app
10
+ that allows users to type text and see the tokenization and corresponding IDs
11
+ update live.
12
+
13
+ This entire script is designed to be run as a single file in a Hugging Face Space.
14
+ """
15
+ import json
16
+ import regex as re
17
+ from pathlib import Path
18
+ from typing import List, Dict, Tuple
19
+ import unicodedata
20
+ import gradio as gr
21
+
22
+ # --- TextCleaner Class ---
23
+ # This class is included to ensure that the input text is pre-processed
24
+ # in exactly the same way as during the tokenizer's training.
25
+ class TextCleaner:
26
+ """A text cleaner for AI datasets, designed to remove invisible, abnormal, and disruptive characters."""
27
+ UNWANTED_CHARS = {
28
+ '\ufffd', '\u200b', '\u200c', '\u200d', '\u2060', '\u2061', '\u2063',
29
+ '\u00a0', '\u202f', '\u2007', '\u2028', '\u2029', '\ufeff', '\ue000',
30
+ '\uf8ff', '\ue001', '\xad', '\u180e', '\u200e', '\uFE0F',
31
+ }
32
+
33
+ @classmethod
34
+ def clean_text(cls, text: str) -> str:
35
+ """Cleans a given string by normalizing it, removing unwanted characters, and collapsing whitespace."""
36
+ text = unicodedata.normalize("NFKC", text)
37
+ text = text.replace('’', "'").replace('‘', "'")
38
+ text = text.replace('“', '"').replace('”', '"')
39
+ for char in cls.UNWANTED_CHARS:
40
+ text = text.replace(char, '')
41
+ text = ''.join(c for c in text if ord(c) >= 32 or c in '\n\r\t')
42
+ text = re.sub(r'\s+', ' ', text)
43
+ return text.strip()
44
+
45
+ # --- Standalone Tokenizer Class ---
46
+ class AGBPETokenizer:
47
+ """
48
+ A self-contained tokenizer that loads and uses a pre-trained AG-BPE model
49
+ from a JSON file containing the vocabulary and merge rules.
50
+ """
51
+ def __init__(self, vocab: Dict[str, int], merges: Dict[str, int], special_tokens: Dict[str, int]):
52
+ """Initializes the tokenizer from loaded vocabulary and merge data."""
53
+ self.vocab = vocab
54
+ self.merges = {tuple(k.split()): v for k, v in merges.items()}
55
+ self.special_tokens_map = special_tokens
56
+ self.id_to_token: Dict[int, str] = {i: s for s, i in self.vocab.items()}
57
+ self.pat = re.compile(r"""'s|'t|'re|'ve|'m|'ll|'d| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+""")
58
+ self.unk_token_id = self.vocab.get('<unk>')
59
+ if self.unk_token_id is None:
60
+ raise ValueError("The '<unk>' token is missing from the vocabulary.")
61
+ self.text_cleaner = TextCleaner()
62
+
63
+ @classmethod
64
+ def from_file(cls, filepath: str) -> 'AGBPETokenizer':
65
+ """Class method to conveniently load a tokenizer from a JSON file path."""
66
+ path = Path(filepath)
67
+ if not path.exists():
68
+ raise FileNotFoundError(f"Tokenizer file not found: '{filepath}'")
69
+ with open(path, 'r', encoding='utf-8') as f:
70
+ data = json.load(f)
71
+ required_keys = ['vocab', 'merges', 'special_tokens']
72
+ if not all(key in data for key in required_keys):
73
+ raise ValueError("The JSON file is malformed. Missing one of: vocab, merges, special_tokens.")
74
+ return cls(data['vocab'], data['merges'], data['special_tokens'])
75
+
76
+ def _apply_bpe(self, word_chars: List[str]) -> List[str]:
77
+ """Applies the BPE merge rules to a list of characters, with a crucial validation step."""
78
+ if not self.merges:
79
+ return word_chars
80
+ while len(word_chars) > 1:
81
+ pairs = list(zip(word_chars[:-1], word_chars[1:]))
82
+ local_merges = self.merges.copy()
83
+ best_pair = None
84
+ while True:
85
+ if not local_merges:
86
+ best_pair = None
87
+ break
88
+ valid_pairs_in_word = (p for p in pairs if p in local_merges)
89
+ current_best_pair = min(valid_pairs_in_word, key=local_merges.get, default=None)
90
+ if current_best_pair is None:
91
+ best_pair = None
92
+ break
93
+ merged_token = current_best_pair[0] + current_best_pair[1]
94
+ if merged_token in self.vocab:
95
+ best_pair = current_best_pair
96
+ break
97
+ else:
98
+ del local_merges[current_best_pair]
99
+ if best_pair is None:
100
+ break
101
+ new_word_chars = []
102
+ i = 0
103
+ while i < len(word_chars):
104
+ if i < len(word_chars) - 1 and (word_chars[i], word_chars[i+1]) == best_pair:
105
+ new_word_chars.append(word_chars[i] + word_chars[i+1])
106
+ i += 2
107
+ else:
108
+ new_word_chars.append(word_chars[i])
109
+ i += 1
110
+ word_chars = new_word_chars
111
+ return word_chars
112
+
113
+ def encode(self, text: str, add_special_tokens: bool = True) -> List[int]:
114
+ """Encodes a string of text into a list of token IDs."""
115
+ cleaned_text = self.text_cleaner.clean_text(text)
116
+ token_ids = []
117
+ if add_special_tokens and (bos_id := self.special_tokens_map.get('<bos>')) is not None:
118
+ token_ids.append(bos_id)
119
+ for chunk in self.pat.findall(cleaned_text):
120
+ tokens = self._apply_bpe(list(chunk))
121
+ token_ids.extend(self.vocab.get(token, self.unk_token_id) for token in tokens)
122
+ if add_special_tokens and (eos_id := self.special_tokens_map.get('<eos>')) is not None:
123
+ token_ids.append(eos_id)
124
+ return token_ids
125
+
126
+ def decode(self, token_ids: List[int]) -> str:
127
+ """Decodes a list of token IDs back into a string of text."""
128
+ special_ids_to_skip = set(self.special_tokens_map.values())
129
+ tokens = [self.id_to_token.get(token_id, '') for token_id in token_ids if token_id not in special_ids_to_skip]
130
+ return "".join(tokens)
131
+
132
+
133
+ # --- Gradio Web Application ---
134
+
135
+ # 1. Définir le chemin vers le fichier du tokenizer.
136
+ # Assurez-vous que ce fichier est présent dans votre Space Hugging Face.
137
+ TOKENIZER_FILE = "ag_bpe_tokenizer.json"
138
+ TOKENIZER_LOADED = False
139
+ ERROR_MESSAGE = ""
140
+ tokenizer = None
141
+
142
+ # 2. Essayer de charger le tokenizer au démarrage de l'application.
143
+ try:
144
+ # Création d'un fichier factice si celui-ci n'existe pas (pour test local facile)
145
+ if not Path(TOKENIZER_FILE).exists():
146
+ print(f"⚠️ Attention : Le fichier '{TOKENIZER_FILE}' est introuvable.")
147
+ print("Création d'un fichier tokenizer factice pour le test local.")
148
+ dummy_data = {
149
+ "vocab": {"<unk>": 0, "<bos>": 1, "<eos>": 2, "Hel": 3, "lo": 4, "W": 5, "orld": 6, "HelloWorld": 7, " ": 8},
150
+ "merges": {"H e l": 1, "l o": 2, "W o r l d": 3, "Hello World": 4},
151
+ "special_tokens": {"<unk>": 0, "<bos>": 1, "<eos>": 2}
152
+ }
153
+ with open(TOKENIZER_FILE, 'w', encoding='utf-8') as f:
154
+ json.dump(dummy_data, f, indent=2)
155
+ print("Fichier factice créé. L'application utilisera ce fichier.")
156
+
157
+ print(f"🧠 Chargement du tokenizer depuis '{TOKENIZER_FILE}'...")
158
+ tokenizer = AGBPETokenizer.from_file(TOKENIZER_FILE)
159
+ TOKENIZER_LOADED = True
160
+ print(f"✅ Tokenizer chargé avec succès. Taille du vocabulaire : {len(tokenizer.vocab)}")
161
+
162
+ except (FileNotFoundError, ValueError, KeyError) as e:
163
+ ERROR_MESSAGE = str(e)
164
+ print(f"❌ ERREUR lors du chargement du tokenizer : {ERROR_MESSAGE}")
165
+
166
+
167
+ # 3. Définir la fonction principale qui sera appelée par Gradio.
168
+ def visualize_tokenization(text: str) -> List[Tuple[str, str]]:
169
+ """
170
+ Prend un texte en entrée, le tokenize et renvoie une liste de tuples
171
+ (token, id) pour l'affichage avec gr.HighlightedText.
172
+ """
173
+ if not TOKENIZER_LOADED or not tokenizer:
174
+ return [("ERREUR LORS DU CHARGEMENT DU TOKENIZER", ERROR_MESSAGE)]
175
+
176
+ if not text:
177
+ return [("Veuillez entrer du texte...", "")]
178
+
179
+ # Encoder le texte pour obtenir les IDs des tokens.
180
+ # add_special_tokens=False pour ne pas afficher <bos> et <eos> dans la démo.
181
+ encoded_ids = tokenizer.encode(text, add_special_tokens=False)
182
+
183
+ # Préparer la sortie pour le composant HighlightedText.
184
+ # Le format est une liste de tuples (token_string, label).
185
+ highlighted_output = []
186
+ for token_id in encoded_ids:
187
+ # Récupérer la chaîne de caractères du token à partir de son ID.
188
+ token_string = tokenizer.id_to_token.get(token_id, f"<unk:{token_id}>")
189
+ # Le label sera l'ID du token.
190
+ highlighted_output.append((token_string, str(token_id)))
191
+
192
+ return highlighted_output
193
+
194
+ # 4. Construire l'interface Gradio.
195
+ with gr.Blocks(theme=gr.themes.Soft(primary_hue="sky"), css="footer {display: none !important}") as demo:
196
+ gr.Markdown(
197
+ """
198
+ # 👁️ Visualiseur de Tokenizer en Temps Réel
199
+ Entrez du texte dans le champ ci-dessous pour observer la segmentation (tokenization) en direct.
200
+ Chaque segment de texte coloré est un "token", et son ID numérique est affiché juste en dessous.
201
+ """
202
+ )
203
+
204
+ with gr.Column():
205
+ input_textbox = gr.Textbox(
206
+ label="Entrez votre texte ici",
207
+ placeholder="Écrivez quelque chose...",
208
+ lines=7,
209
+ show_label=False,
210
+ )
211
+
212
+ output_highlight = gr.HighlightedText(
213
+ label="Tokens et IDs",
214
+ show_label=False,
215
+ interactive=True, # Permet de sélectionner le texte
216
+ combine_consecutive=True,
217
+ show_legend=True,
218
+ color_map={"ID": "lightblue"} # Simple color map
219
+ )
220
+
221
+ # Lier l'événement 'input' (chaque frappe) du champ de texte à notre fonction.
222
+ # 'live=True' est une autre façon de le faire, mais .input() est plus explicite.
223
+ input_textbox.input(
224
+ fn=visualize_tokenization,
225
+ inputs=[input_textbox],
226
+ outputs=[output_highlight]
227
+ )
228
+
229
+ # Ajouter un exemple pour guider l'utilisateur.
230
+ gr.Examples(
231
+ examples=[
232
+ "L'intelligence artificielle est fascinante.",
233
+ "Test avec des espaces multiples et des ’apostrophes’ typographiques.",
234
+ "Le code `if (x==10)` et les emojis 👍🚀 sont gérés.",
235
+ "Hello world! This is a test of the AG-BPE tokenizer.",
236
+ "안녕하세요"
237
+ ],
238
+ inputs=input_textbox
239
+ )
240
+
241
+ # 5. Lancer l'application (le point d'entrée pour Hugging Face Spaces).
242
+ if __name__ == "__main__":
243
+ demo.launch()