TurkishCodeMan commited on
Commit
989436d
·
verified ·
1 Parent(s): 36190f2

Upload tokenizer.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. tokenizer.py +201 -0
tokenizer.py ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ from typing import List, Optional, Tuple, Union
4
+ import numpy as np
5
+ import torch
6
+
7
+
8
+ # -----------------------------------------------------------------------------
9
+ # 1. Text Tokenizer (Qwen2.5 / Breeze-TTS Standardı)
10
+ # -----------------------------------------------------------------------------
11
+
12
+ class TextTokenizer:
13
+ """
14
+ Qwen2.5 tabanlı gelişmiş BPE Metin Tokenizer'ı (Breeze-TTS standardı).
15
+ Özel kontrol tokenları ile Standart TTS, Voice Design ve Voice Clone destekler:
16
+ - <|instruct|> : Ses tasarım talimatı (Voice Design)
17
+ - <|text|> : Seslendirilecek metin
18
+ - <|ref_audio|>: Klonlanacak referans ses (Voice Clone)
19
+ - <|audio|> : Ses tokenlarının başladığı yer
20
+ - <|audio_end|>: Ses tokenlarının bittiği yer
21
+ """
22
+ def __init__(self, model_id: str = "Qwen/Qwen2.5-0.5B"):
23
+ from transformers import AutoTokenizer
24
+ self.model_id = model_id
25
+ try:
26
+ self.tokenizer = AutoTokenizer.from_pretrained(model_id, local_files_only=True)
27
+ except Exception:
28
+ self.tokenizer = AutoTokenizer.from_pretrained(model_id)
29
+
30
+ # Özel kontrol tokenlarını ekle
31
+ self.special_tokens = [
32
+ "<|instruct|>",
33
+ "<|text|>",
34
+ "<|ref_audio|>",
35
+ "<|audio|>",
36
+ "<|audio_end|>",
37
+ ]
38
+ self.tokenizer.add_special_tokens({"additional_special_tokens": self.special_tokens})
39
+
40
+ # Token ID erişimleri
41
+ self.pad_id = self.tokenizer.pad_token_id if self.tokenizer.pad_token_id is not None else 0
42
+ self.bos_id = self.tokenizer.bos_token_id if self.tokenizer.bos_token_id is not None else 1
43
+ self.eos_id = self.tokenizer.eos_token_id if self.tokenizer.eos_token_id is not None else 2
44
+ self.instruct_id = self.tokenizer.convert_tokens_to_ids("<|instruct|>")
45
+ self.text_id = self.tokenizer.convert_tokens_to_ids("<|text|>")
46
+ self.ref_audio_id = self.tokenizer.convert_tokens_to_ids("<|ref_audio|>")
47
+ self.audio_start_id = self.tokenizer.convert_tokens_to_ids("<|audio|>")
48
+ self.audio_end_id = self.tokenizer.convert_tokens_to_ids("<|audio_end|>")
49
+ self.vocab_size = len(self.tokenizer)
50
+
51
+ def encode(self, text: str, add_bos: bool = False, add_eos: bool = False) -> List[int]:
52
+ """
53
+ Metni token ID'lerine dönüştürür.
54
+ """
55
+ ids = self.tokenizer.encode(text, add_special_tokens=False)
56
+ if add_bos and self.bos_id is not None:
57
+ ids = [self.bos_id] + ids
58
+ if add_eos and self.eos_id is not None:
59
+ ids = ids + [self.eos_id]
60
+ return ids
61
+
62
+ def decode(self, ids: List[int], skip_special_tokens: bool = False) -> str:
63
+ """
64
+ Token ID'lerini tekrar metne çevirir.
65
+ """
66
+ return self.tokenizer.decode(ids, skip_special_tokens=skip_special_tokens)
67
+
68
+
69
+ # -----------------------------------------------------------------------------
70
+ # 2. Audio Codec Tokenizer (Kyutai Mimi)
71
+ # -----------------------------------------------------------------------------
72
+
73
+ class AudioCodecTokenizer:
74
+ """
75
+ Kyutai Mimi Audio Codec Entegrasyonu:
76
+ 24 kHz dalga boyunu saniyede 12.5 kare ve 8 codebook ile ayrık sayılara çevirir.
77
+ Ağırlıklar dondurulmuştur (frozen), sadece encode/decode için kullanılır.
78
+ """
79
+ def __init__(self, model_id: str = "kyutai/mimi", device: str = "cpu"):
80
+ self.model_id = model_id
81
+ self.device = device
82
+ self.sample_rate = 24000
83
+ self.frame_rate = 12.5
84
+ self.num_codebooks = 8
85
+ self.codebook_size = 2048
86
+
87
+ from transformers import MimiModel, AutoFeatureExtractor
88
+ print(f"Kyutai Mimi codec yükleniyor ({model_id})...")
89
+ try:
90
+ self.codec = MimiModel.from_pretrained(model_id, local_files_only=True).to(device)
91
+ self.feature_extractor = AutoFeatureExtractor.from_pretrained(model_id, local_files_only=True)
92
+ except Exception:
93
+ self.codec = MimiModel.from_pretrained(model_id).to(device)
94
+ self.feature_extractor = AutoFeatureExtractor.from_pretrained(model_id)
95
+ self.codec.eval()
96
+ print("Kyutai Mimi başarıyla yüklendi!")
97
+
98
+ @torch.inference_mode()
99
+ def encode(self, wav: Union[np.ndarray, torch.Tensor]) -> torch.Tensor:
100
+ """
101
+ Giriş: (1, audio_len) 24 kHz ses
102
+ Çıkış: (8, T_audio) ayrık token matrisi
103
+ """
104
+ if isinstance(wav, torch.Tensor):
105
+ wav_np = wav.squeeze().cpu().numpy()
106
+ else:
107
+ wav_np = wav.squeeze()
108
+
109
+ inputs = self.feature_extractor(
110
+ raw_audio=wav_np,
111
+ sampling_rate=self.sample_rate,
112
+ return_tensors="pt"
113
+ ).to(self.device)
114
+
115
+ encoder_outputs = self.codec.encode(inputs["input_values"], inputs.get("padding_mask"))
116
+ # audio_codes shape: (1, num_codebooks, T) -> hedef codebook sayısına (8) dilimle
117
+ codes = encoder_outputs.audio_codes.squeeze(0)
118
+ return codes[:self.num_codebooks, :]
119
+
120
+ @torch.inference_mode()
121
+ def decode(self, audio_codes: torch.Tensor) -> torch.Tensor:
122
+ """
123
+ Giriş: (8, T_audio) veya (1, 8, T_audio) ayrık token matrisi
124
+ Çıkış: (1, audio_len) 24 kHz dalga boyu
125
+ """
126
+ if audio_codes.dim() == 2:
127
+ audio_codes = audio_codes.unsqueeze(0) # (1, 8, T)
128
+
129
+ # Mimi decode: (1, 8, T) -> (1, 1, audio_len) veya (1, audio_len)
130
+ audio_values = self.codec.decode(audio_codes.to(self.device))[0]
131
+ if audio_values.dim() == 3:
132
+ audio_values = audio_values.squeeze(1) # (1, audio_len)
133
+ elif audio_values.dim() == 1:
134
+ audio_values = audio_values.unsqueeze(0)
135
+ return audio_values
136
+
137
+
138
+ # -----------------------------------------------------------------------------
139
+ # 3. TTS Processor (Prompt Hazırlayıcı)
140
+ # -----------------------------------------------------------------------------
141
+
142
+ class TTSProcessor:
143
+ """
144
+ Metin, Voice Design talimatı ve Ses Klonlama girdilerini
145
+ modele beslenecek formatta hazırlayan yönetici sınıf.
146
+ """
147
+ def __init__(self, text_tokenizer: TextTokenizer, audio_tokenizer: AudioCodecTokenizer):
148
+ self.text_tokenizer = text_tokenizer
149
+ self.audio_tokenizer = audio_tokenizer
150
+
151
+ def format_text_prompt(self, text: str, instruction: Optional[str] = None) -> str:
152
+ """
153
+ Senaryolara göre uygun prompt string'i üretir:
154
+ 1. Standart: <|text|> {text} <|audio|>
155
+ 2. Voice Design: <|instruct|> {instruction} <|text|> {text} <|audio|>
156
+ """
157
+ if instruction is not None and instruction.strip():
158
+ return f"<|instruct|> {instruction.strip()} <|text|> {text.strip()} <|audio|>"
159
+ return f"<|text|> {text.strip()} <|audio|>"
160
+
161
+ def prepare_inference_inputs(
162
+ self,
163
+ text: str,
164
+ instruction: Optional[str] = None,
165
+ ref_audio_path: Optional[str] = None,
166
+ max_ref_sec: Optional[float] = None,
167
+ device: str = "cpu"
168
+ ) -> Tuple[torch.Tensor, Optional[torch.Tensor]]:
169
+ """
170
+ İnference için gerekli tensörleri (text_ids, ref_audio_codes) üretir.
171
+ """
172
+ prompt_str = self.format_text_prompt(text, instruction=instruction)
173
+ text_ids = torch.tensor([self.text_tokenizer.encode(prompt_str)], device=device, dtype=torch.long)
174
+
175
+ ref_codes = None
176
+ if ref_audio_path is not None and os.path.exists(ref_audio_path):
177
+ try:
178
+ import soundfile as sf
179
+ wav, sr = sf.read(ref_audio_path)
180
+ if wav.ndim > 1:
181
+ wav = wav.mean(axis=1) # Mono'ya dönüştür
182
+
183
+ target_sr = self.audio_tokenizer.sample_rate # 24000
184
+ if sr != target_sr:
185
+ import torchaudio.functional as AF
186
+ wav_t = torch.from_numpy(wav).float().unsqueeze(0)
187
+ wav = AF.resample(wav_t, orig_freq=sr, new_freq=target_sr).squeeze(0).numpy()
188
+
189
+ # Kullanıcı sınır belirtmişse kırp, belirtmemişse sesin TAMAMINI al
190
+ if max_ref_sec is not None and max_ref_sec > 0:
191
+ max_ref_samples = int(target_sr * max_ref_sec)
192
+ if len(wav) > max_ref_samples:
193
+ wav = wav[:max_ref_samples]
194
+
195
+ ref_codes = self.audio_tokenizer.encode(wav).unsqueeze(0).to(device) # (1, 8, T_ref)
196
+ ref_sec = ref_codes.shape[-1] / self.audio_tokenizer.frame_rate
197
+ print(f"[Inference] 🎙️ Referans ses işlendi ({ref_audio_path}): {ref_sec:.2f} sn ({ref_codes.shape[-1]} kare - Tamamı alındı)", flush=True)
198
+ except Exception as e:
199
+ print(f"[Inference] Referans ses okunamadı ({ref_audio_path}): {e}", flush=True)
200
+
201
+ return text_ids, ref_codes