tsukemono commited on
Commit
aeced03
·
verified ·
1 Parent(s): 90e68b6

Upload model_causal files and README

Browse files
README.md ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ language:
3
+ - ja
4
+ library_name: transformers
5
+ pipeline_tag: text-to-speech
6
+ tags:
7
+ - tts
8
+ - audio
9
+ - japanese
10
+ ---
11
+
12
+ # neuTTS-JP-150m
13
+
14
+ 日本語専用TTSモデルです
15
+ トークナイザーを大幅に修正している関係で日本語以外はしゃべません
16
+ 150Mパラメータでのvoice cloneingを目指しています
17
+ ストリーミング再生を意識しており、無音部分としてpaddingトークンを導入しています
18
+ 推論時はpaddingトークンを無視するか、そこで一区切りさせるなど、工夫を行ってください
19
+
20
+ ### インストール
21
+
22
+ ```bash
23
+ pip install torch torchaudio transformers neucodec pyopenjtalk
24
+ ```
25
+
26
+ ## 推論
27
+
28
+ ```python
29
+ from pathlib import Path
30
+
31
+ import torch
32
+ import torchaudio
33
+ from torchaudio import transforms as T
34
+ from transformers import AutoModelForCausalLM, AutoTokenizer
35
+ from neucodec import NeuCodec
36
+
37
+ # model読み込み
38
+ tokenizer = AutoTokenizer.from_pretrained("tsukemono/neuTTS-JP-150m", trust_remote_code=True)
39
+ model = AutoModelForCausalLM.from_pretrained("tsukemono/neuTTS-JP-150m")
40
+ model.eval()
41
+ codec = codec.eval()
42
+
43
+ # 参照音声のエンコード
44
+ waveform, sr = torchaudio.load("参照音源.mp3")
45
+ if waveform.shape[0] > 1:
46
+ waveform = waveform.mean(dim=0, keepdim=True)
47
+ if sr != 16_000:
48
+ waveform = T.Resample(sr, 16_000)(waveform)
49
+ waveform = waveform.unsqueeze(0) # (B, 1, T_16k)
50
+ with torch.inference_mode():
51
+ ref_codes = codec.encode_code(waveform).flatten().tolist()
52
+
53
+ # テキストをトークナイズしてプロンプト作成
54
+ text_ids = tokenizer(
55
+ "ここに作成したいテキストを書いてください",
56
+ add_special_tokens=False,
57
+ return_attention_mask=False,
58
+ return_token_type_ids=False,
59
+ )["input_ids"]
60
+
61
+ eos_id = int(tokenizer.eos_token_id)
62
+ input_ids = ref_codes + [eos_id] + text_ids + [eos_id]
63
+ input_ids = torch.tensor([input_ids], dtype=torch.long)
64
+
65
+ # 生成
66
+ with torch.inference_mode():
67
+ generated = model.generate(
68
+ input_ids=input_ids,
69
+ repetition_penalty=1.1,
70
+ max_new_tokens=1500,
71
+ )
72
+
73
+ # 生成トークンから音声トークンだけ抽出
74
+ gen_ids = generated[0, input_ids.shape[1] :]
75
+ gen_ids = gen_ids[gen_ids < 65536]
76
+
77
+ # vocoderで音声作成
78
+ with torch.inference_mode():
79
+ audio_data = codec.decode_code(gen_ids.unsqueeze(0).unsqueeze(0)).cpu()
80
+ torchaudio.save("output.mp3", audio_data[0], 24_000, format="mp3")
81
+ ```
82
+
83
+
84
+ ## サンプル音声
85
+
86
+ <audio controls src="inference_sample/sample_output_1.mp3"></audio>
87
+ <audio controls src="inference_sample/sample_output_2.mp3"></audio>
88
+ <audio controls src="inference_sample/sample_output_3.mp3"></audio>
89
+ <audio controls src="inference_sample/sample_output_4.mp3"></audio>
90
+ <audio controls src="inference_sample/sample_output_5.mp3"></audio>
config.json ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "LlamaForCausalLM"
4
+ ],
5
+ "attention_bias": false,
6
+ "attention_dropout": 0.0,
7
+ "bos_token_id": 1,
8
+ "dtype": "float32",
9
+ "eos_token_id": 2,
10
+ "head_dim": 64,
11
+ "hidden_act": "silu",
12
+ "hidden_size": 512,
13
+ "initializer_range": 0.02,
14
+ "intermediate_size": 2048,
15
+ "max_position_embeddings": 4096,
16
+ "mlp_bias": false,
17
+ "model_type": "llama",
18
+ "num_attention_heads": 8,
19
+ "num_hidden_layers": 12,
20
+ "num_key_value_heads": 8,
21
+ "pretraining_tp": 1,
22
+ "pruned_heads": {},
23
+ "rms_norm_eps": 1e-05,
24
+ "rope_parameters": {
25
+ "rope_theta": 10000,
26
+ "rope_type": "default"
27
+ },
28
+ "tf_legacy_loss": false,
29
+ "tie_word_embeddings": false,
30
+ "torchscript": false,
31
+ "transformers_version": "5.0.0rc1",
32
+ "use_bfloat16": false,
33
+ "use_cache": true,
34
+ "vocab_size": 99111
35
+ }
generation_config.json ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {
2
+ "_from_model_config": true,
3
+ "bos_token_id": 1,
4
+ "eos_token_id": 2,
5
+ "transformers_version": "5.0.0rc1"
6
+ }
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:387762ec141730adb7b96702a4123c30f45f961feb8b566f42efabc05b3fc691
3
+ size 607348768
tokenization_llm_jp_tts.py ADDED
@@ -0,0 +1,352 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+
3
+ import pyopenjtalk
4
+ from transformers import PreTrainedTokenizerFast
5
+
6
+
7
+ def katakana_to_hiragana(text: str) -> str:
8
+ """
9
+ カタカナをひらがなに変換
10
+
11
+ Args:
12
+ text: カタカナテキスト
13
+
14
+ Returns:
15
+ ひらがなテキスト
16
+ """
17
+ result = []
18
+ for char in text:
19
+ # カタカナ(ァ-ヶ)をひらがな(ぁ-ゖ)に変換
20
+ if "ァ" <= char <= "ヶ":
21
+ # カタカナの開始コードポイント: 0x30A1
22
+ # ひらがなの開始コードポイント: 0x3041
23
+ # 差分: 0x60
24
+ result.append(chr(ord(char) - 0x60))
25
+ else:
26
+ result.append(char)
27
+ return "".join(result)
28
+
29
+
30
+ def add_ruby_single(text: str, file_path: str = None) -> str:
31
+ """
32
+ 単一のテキストにルビを振る(pyopenjtalk-plus使用)
33
+
34
+ Args:
35
+ text: 入力テキスト
36
+ file_path: ファイルパス(エラー時のログ用)
37
+
38
+ Returns:
39
+ ルビが振られたテキスト(形式: 漢字[よみ])
40
+
41
+ Raises:
42
+ RuntimeError: テキストが長すぎる場合(512バイト超過)
43
+ """
44
+ # 事前にバイト数をチェック(pyopenjtalkの制限: 512バイト)
45
+ text_bytes = len(text.encode("utf-8"))
46
+ if text_bytes > 512:
47
+ # エラー情報を出力
48
+ if file_path:
49
+ print("\nERROR: pyopenjtalk入力長制限エラー(事前チェック)")
50
+ print(f" ファイル: {file_path}")
51
+ print(f" テキスト長: {len(text)} 文字, {text_bytes} bytes (max: 512 bytes)")
52
+ print(f" テキスト内容: {text[:100]}...")
53
+ raise RuntimeError(f"Input too long: {text_bytes} bytes (max 512 bytes)")
54
+
55
+ try:
56
+ # pyopenjtalk-plusで形態素解析
57
+ features = pyopenjtalk.run_frontend(text)
58
+
59
+ # 空の結果が返された場合もエラー扱い(処理失敗の可能性)
60
+ if not features:
61
+ if text.strip(): # 空白だけのテキストは除く
62
+ print("\nWARNING: pyopenjtalkが空の結果を返しました")
63
+ if file_path:
64
+ print(f" ファイル: {file_path}")
65
+ print(f" テキスト: {text[:100]}...")
66
+
67
+ result = []
68
+ for feature in features:
69
+ surface = feature["string"] # 表層形(元のテキスト)
70
+ reading = feature["read"] # 読み(カタカナ)
71
+
72
+ # カタカナをひらがなに変換
73
+ reading_hira = katakana_to_hiragana(reading)
74
+
75
+ # 表層形と読みが異なる場合のみルビを振る
76
+ # ひらがな・カタカナはそのまま(ルビ不要)
77
+ if surface != reading_hira and not all(
78
+ c in "ぁ-ん" or c in "ァ-ヶー" for c in surface
79
+ ):
80
+ result.append(f"{surface}[{reading_hira}]")
81
+ else:
82
+ result.append(surface)
83
+
84
+ return "".join(result)
85
+ except Exception as e:
86
+ error_msg = str(e)
87
+ # 既知のエラーメッセージをチェック
88
+ if "Input too long" in error_msg or "max 512" in error_msg:
89
+ if file_path:
90
+ print("\nERROR: pyopenjtalk入力長制限エラー(実行時)")
91
+ print(f" ファイル: {file_path}")
92
+ print(f" テキスト長: {len(text)} 文字, {text_bytes} bytes")
93
+ print(f" テキスト内容: {text[:100]}...")
94
+ raise RuntimeError(f"Input too long: {text_bytes} bytes") from e
95
+ else:
96
+ # その他のエラー
97
+ raise
98
+
99
+
100
+ def add_ruby(text: str, file_path: str = None) -> str:
101
+ """
102
+ テキストにルビを振る(pyopenjtalk-plus使用、長文対応)
103
+
104
+ Args:
105
+ text: 入力テキスト
106
+ file_path: ファイルパス(エラー時のログ用)
107
+
108
+ Returns:
109
+ ルビが振られたテキスト(形式: 漢字[よみ])
110
+ """
111
+ # 1. まず全体を処理してみる
112
+ try:
113
+ return add_ruby_single(text, file_path)
114
+ except (RuntimeError, Exception) as e:
115
+ # RuntimeErrorまたは"Input too long"を含むエラーの場合のみ分割処理
116
+ if (
117
+ not isinstance(e, RuntimeError)
118
+ and "Input too long" not in str(e)
119
+ and "max 512" not in str(e)
120
+ ):
121
+ # 長さ以外のエラーは再スロー
122
+ raise
123
+
124
+ # 2. 「。」と「?」と「!」で分割して処理
125
+ sentences = re.split(r"(。|?|!)", text)
126
+ result_parts = []
127
+
128
+ for sentence in sentences:
129
+ if not sentence:
130
+ continue
131
+
132
+ try:
133
+ result_parts.append(add_ruby_single(sentence, file_path))
134
+ except (RuntimeError, Exception) as e:
135
+ if (
136
+ not isinstance(e, RuntimeError)
137
+ and "Input too long" not in str(e)
138
+ and "max 512" not in str(e)
139
+ ):
140
+ raise
141
+
142
+ # 3. 「、」でさらに分割
143
+ sub_sentences = re.split(r"(、)", sentence)
144
+
145
+ for sub_sentence in sub_sentences:
146
+ if not sub_sentence:
147
+ continue
148
+
149
+ try:
150
+ result_parts.append(add_ruby_single(sub_sentence, file_path))
151
+ except (RuntimeError, Exception) as e:
152
+ if (
153
+ not isinstance(e, RuntimeError)
154
+ and "Input too long" not in str(e)
155
+ and "max 512" not in str(e)
156
+ ):
157
+ raise
158
+
159
+ # 4. 空白でさらに分割
160
+ words = re.split(r"(\s+)", sub_sentence)
161
+
162
+ for word in words:
163
+ if not word:
164
+ continue
165
+
166
+ try:
167
+ result_parts.append(add_ruby_single(word, file_path))
168
+ except (RuntimeError, Exception) as e:
169
+ if (
170
+ not isinstance(e, RuntimeError)
171
+ and "Input too long" not in str(e)
172
+ and "max 512" not in str(e)
173
+ ):
174
+ raise
175
+
176
+ # 5. 強制的に文字数で分割(バイト数ベース)
177
+ print("\nWARNING: 強制分割を実行します(句読点・空白なし)")
178
+ if file_path:
179
+ print(f" ファイル: {file_path}")
180
+ print(
181
+ f" テキスト長: {len(word)} 文字, {len(word.encode('utf-8'))} bytes"
182
+ )
183
+
184
+ # 最大バイト数(安全のため少し余裕を持たせる)
185
+ max_bytes = 400
186
+
187
+ current_chunk = ""
188
+ current_bytes = 0
189
+
190
+ for char in word:
191
+ char_bytes = len(char.encode("utf-8"))
192
+
193
+ # 次の文字を追加すると制限を超える場合
194
+ if current_bytes + char_bytes > max_bytes:
195
+ # 現在のチャンクを処理
196
+ if current_chunk:
197
+ try:
198
+ result_parts.append(
199
+ add_ruby_single(
200
+ current_chunk, file_path
201
+ )
202
+ )
203
+ except Exception as chunk_e:
204
+ # それでもエラーの場合は元のテキストをそのまま使用
205
+ print(
206
+ f" WARNING: チャンク処理も失敗、元のテキストを使用: {chunk_e}"
207
+ )
208
+ result_parts.append(current_chunk)
209
+
210
+ # 新しいチャンクを開始
211
+ current_chunk = char
212
+ current_bytes = char_bytes
213
+ else:
214
+ current_chunk += char
215
+ current_bytes += char_bytes
216
+
217
+ # 最後のチャンクを処理
218
+ if current_chunk:
219
+ try:
220
+ result_parts.append(
221
+ add_ruby_single(current_chunk, file_path)
222
+ )
223
+ except Exception as chunk_e:
224
+ print(
225
+ f" WARNING: 最後のチャンク処理も失敗、元のテキストを使用: {chunk_e}"
226
+ )
227
+ result_parts.append(current_chunk)
228
+
229
+ print(f" 強制分割完了: {len(word)} 文字を処理")
230
+
231
+ return "".join(result_parts)
232
+
233
+
234
+ class LlmJpTtsTokenizer(PreTrainedTokenizerFast):
235
+ def _apply_ruby_to_text(self, text, *, is_split_into_words: bool):
236
+ if text is None or is_split_into_words:
237
+ return text
238
+
239
+ if isinstance(text, str):
240
+ return add_ruby(text)
241
+
242
+ if isinstance(text, (list, tuple)):
243
+ if not text:
244
+ return text
245
+
246
+ if all(isinstance(item, str) for item in text):
247
+ return [add_ruby(item) for item in text]
248
+
249
+ if all(isinstance(item, (list, tuple)) and len(item) == 2 for item in text):
250
+ processed = []
251
+ for first, second in text:
252
+ first_text = add_ruby(first) if isinstance(first, str) else first
253
+ second_text = (
254
+ add_ruby(second) if isinstance(second, str) else second
255
+ )
256
+ processed.append((first_text, second_text))
257
+ return processed
258
+
259
+ return text
260
+
261
+ def __call__(
262
+ self,
263
+ text=None,
264
+ text_pair=None,
265
+ text_target=None,
266
+ text_pair_target=None,
267
+ add_special_tokens=True,
268
+ padding=False,
269
+ truncation=None,
270
+ max_length=None,
271
+ stride=0,
272
+ is_split_into_words=False,
273
+ pad_to_multiple_of=None,
274
+ padding_side=None,
275
+ return_tensors=None,
276
+ return_token_type_ids=None,
277
+ return_attention_mask=None,
278
+ return_overflowing_tokens=False,
279
+ return_special_tokens_mask=False,
280
+ return_offsets_mapping=False,
281
+ return_length=False,
282
+ verbose=True,
283
+ tokenizer_kwargs=None,
284
+ **kwargs,
285
+ ):
286
+ text = self._apply_ruby_to_text(text, is_split_into_words=is_split_into_words)
287
+ text_pair = self._apply_ruby_to_text(
288
+ text_pair, is_split_into_words=is_split_into_words
289
+ )
290
+ text_target = self._apply_ruby_to_text(
291
+ text_target, is_split_into_words=is_split_into_words
292
+ )
293
+ text_pair_target = self._apply_ruby_to_text(
294
+ text_pair_target, is_split_into_words=is_split_into_words
295
+ )
296
+
297
+ return super().__call__(
298
+ text=text,
299
+ text_pair=text_pair,
300
+ text_target=text_target,
301
+ text_pair_target=text_pair_target,
302
+ add_special_tokens=add_special_tokens,
303
+ padding=padding,
304
+ truncation=truncation,
305
+ max_length=max_length,
306
+ stride=stride,
307
+ is_split_into_words=is_split_into_words,
308
+ pad_to_multiple_of=pad_to_multiple_of,
309
+ padding_side=padding_side,
310
+ return_tensors=return_tensors,
311
+ return_token_type_ids=return_token_type_ids,
312
+ return_attention_mask=return_attention_mask,
313
+ return_overflowing_tokens=return_overflowing_tokens,
314
+ return_special_tokens_mask=return_special_tokens_mask,
315
+ return_offsets_mapping=return_offsets_mapping,
316
+ return_length=return_length,
317
+ verbose=verbose,
318
+ tokenizer_kwargs=tokenizer_kwargs,
319
+ **kwargs,
320
+ )
321
+
322
+ def encode(
323
+ self,
324
+ text,
325
+ text_pair=None,
326
+ add_special_tokens=True,
327
+ padding=False,
328
+ truncation=None,
329
+ max_length=None,
330
+ stride=0,
331
+ padding_side=None,
332
+ return_tensors=None,
333
+ **kwargs,
334
+ ):
335
+ is_split_into_words = bool(kwargs.get("is_split_into_words", False))
336
+ text = self._apply_ruby_to_text(text, is_split_into_words=is_split_into_words)
337
+ text_pair = self._apply_ruby_to_text(
338
+ text_pair, is_split_into_words=is_split_into_words
339
+ )
340
+
341
+ return super().encode(
342
+ text,
343
+ text_pair=text_pair,
344
+ add_special_tokens=add_special_tokens,
345
+ padding=padding,
346
+ truncation=truncation,
347
+ max_length=max_length,
348
+ stride=stride,
349
+ padding_side=padding_side,
350
+ return_tensors=return_tensors,
351
+ **kwargs,
352
+ )
tokenizer.json ADDED
The diff for this file is too large to render. See raw diff
 
tokenizer_config.json ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "additional_special_tokens": null,
3
+ "backend": "tokenizers",
4
+ "bos_token": "<s>",
5
+ "clean_up_tokenization_spaces": true,
6
+ "cls_token": "<CLS|LLM-jp>",
7
+ "eos_token": "</s>",
8
+ "is_local": true,
9
+ "mask_token": "<MASK|LLM-jp>",
10
+ "model_max_length": 1000000000000000019884624838656,
11
+ "model_specific_special_tokens": {},
12
+ "pad_token": "<PAD|LLM-jp>",
13
+ "sep_token": "<SEP|LLM-jp>",
14
+ "tokenizer_class": "LlmJpTtsTokenizer",
15
+ "unk_token": "<unk>",
16
+ "use_fast": true,
17
+ "auto_map": {
18
+ "AutoTokenizer": [
19
+ "tokenization_llm_jp_tts.LlmJpTtsTokenizer",
20
+ "tokenization_llm_jp_tts.LlmJpTtsTokenizer"
21
+ ]
22
+ }
23
+ }