Fablio commited on
Commit
01ea955
·
0 Parent(s):

Initial commit with LFS setup

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +5 -0
  2. .gitignore +44 -0
  3. Dockerfile +36 -0
  4. bert/bert_models.json +12 -0
  5. bert/deberta-v2-large-japanese-char-wwm/.gitattributes +34 -0
  6. bert/deberta-v2-large-japanese-char-wwm/README.md +89 -0
  7. bert/deberta-v2-large-japanese-char-wwm/config.json +37 -0
  8. bert/deberta-v2-large-japanese-char-wwm/special_tokens_map.json +7 -0
  9. bert/deberta-v2-large-japanese-char-wwm/tokenizer_config.json +19 -0
  10. bert/deberta-v2-large-japanese-char-wwm/vocab.txt +0 -0
  11. config.py +307 -0
  12. configs/config.json +73 -0
  13. configs/config_jp_extra.json +80 -0
  14. configs/default_paths.yml +8 -0
  15. configs/paths.yml +8 -0
  16. default_config.yml +70 -0
  17. frontend/next-env.d.ts +5 -0
  18. frontend/next.config.js +11 -0
  19. frontend/package-lock.json +0 -0
  20. frontend/package.json +43 -0
  21. frontend/public/images/kariusagi.png +3 -0
  22. frontend/src/app/globals.css +3 -0
  23. frontend/src/app/layout.tsx +31 -0
  24. frontend/src/app/page.tsx +17 -0
  25. frontend/src/components/AccentEditor.tsx +108 -0
  26. frontend/src/components/AlertPopup.tsx +28 -0
  27. frontend/src/components/DictionaryDialog.tsx +541 -0
  28. frontend/src/components/EditorContainer.tsx +668 -0
  29. frontend/src/components/LineSetting.tsx +317 -0
  30. frontend/src/components/SimpleBackdrop.tsx +17 -0
  31. frontend/src/components/TermOfUse.tsx +155 -0
  32. frontend/src/components/TextEditor.tsx +25 -0
  33. frontend/src/contexts/PopupProvider.tsx +70 -0
  34. frontend/src/hooks/useWindowSize.ts +20 -0
  35. frontend/src/theme.ts +111 -0
  36. frontend/src/utils/api.ts +40 -0
  37. frontend/tailwind.config.ts +20 -0
  38. frontend/tsconfig.json +40 -0
  39. initialize.py +107 -0
  40. model_assets/kariusagi/config.json +120 -0
  41. model_assets/kariusagi/style_vectors.npy +0 -0
  42. requirements.txt +24 -0
  43. server_editor.py +450 -0
  44. static/404.html +1 -0
  45. static/404/index.html +1 -0
  46. static/_next/static/8q7amU-bnfgJwF-L0ePLq/_buildManifest.js +1 -0
  47. static/_next/static/8q7amU-bnfgJwF-L0ePLq/_ssgManifest.js +1 -0
  48. static/_next/static/chunks/117-b970401bd8e4e4e0.js +0 -0
  49. static/_next/static/chunks/313-0e35468afc829c33.js +0 -0
  50. static/_next/static/chunks/589-1dfa6562a75dc337.js +23 -0
.gitattributes ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ *.bin filter=lfs diff=lfs merge=lfs -text
2
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
3
+ *.node filter=lfs diff=lfs merge=lfs -text
4
+ *.png filter=lfs diff=lfs merge=lfs -text
5
+ *.pickle filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .vscode/
2
+
3
+ __pycache__/
4
+ venv/
5
+ .ipynb_checkpoints/
6
+
7
+ /*.yml
8
+ !/default_config.yml
9
+ # /bert/*/*.bin
10
+ /bert/*/*.h5
11
+ /bert/*/*.model
12
+ /bert/*/*.safetensors
13
+ /bert/*/*.msgpack
14
+
15
+ /pretrained/*.safetensors
16
+ /pretrained/*.pth
17
+
18
+ /pretrained_jp_extra/*.safetensors
19
+ /pretrained_jp_extra/*.pth
20
+
21
+ /slm/*/*.bin
22
+
23
+ /scripts/test/
24
+ *.zip
25
+ *.csv
26
+ *.bak
27
+ /mos_results/
28
+
29
+ safetensors.ipynb
30
+ *.wav
31
+
32
+ # pyopenjtalk's dictionary
33
+ *.dic
34
+
35
+ # for development
36
+ .claude
37
+ CLAUDE.md
38
+ refs
39
+ .python-version
40
+
41
+ # Frontend dependencies
42
+ frontend/node_modules/
43
+ frontend/.next/
44
+ frontend/out/
Dockerfile ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Hugging face spaces (CPU) でエディタ (server_editor.py) のデプロイ用
2
+
3
+ # See https://huggingface.co/docs/hub/spaces-sdks-docker-first-demo
4
+
5
+ FROM python:3.10
6
+
7
+ RUN useradd -m -u 1000 user
8
+
9
+ USER user
10
+
11
+ ENV HOME=/home/user \
12
+ PATH=/home/user/.local/bin:$PATH
13
+
14
+ WORKDIR $HOME/app
15
+
16
+ RUN pip install --no-cache-dir --upgrade pip
17
+
18
+ COPY --chown=user . $HOME/app
19
+
20
+ RUN pip install --no-cache-dir -r $HOME/app/requirements.txt
21
+
22
+ # Download models from Hugging Face
23
+ RUN python -c "from huggingface_hub import hf_hub_download; \
24
+ import os; \
25
+ os.makedirs('bert/deberta-v2-large-japanese-char-wwm', exist_ok=True); \
26
+ os.makedirs('model_assets/kariusagi', exist_ok=True); \
27
+ files = ['pytorch_model.bin', 'config.json', 'tokenizer_config.json', 'special_tokens_map.json', 'vocab.txt']; \
28
+ [hf_hub_download('Fablio/samples-style-bert-vits2-models', f'deberta-v2-large-japanese-char-wwm/{f}', local_dir='bert', local_dir_use_symlinks=False) for f in files]; \
29
+ kariusagi_files = ['config.json', 'kariusagi_v0.0_e135_s9000.safetensors', 'style_vectors.npy']; \
30
+ [hf_hub_download('Fablio/samples-style-bert-vits2-models', f'kariusagi/{f}', local_dir='model_assets', local_dir_use_symlinks=False) for f in kariusagi_files]"
31
+
32
+ # Initialize other dependencies
33
+ RUN python initialize.py --skip_default_models
34
+
35
+ # 必要に応じて制限を変更してください
36
+ CMD ["python", "server_editor.py", "--line_length", "50", "--line_count", "3", "--skip_static_files", "--skip_default_models", "--port", "7860"]
bert/bert_models.json ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "deberta-v2-large-japanese-char-wwm": {
3
+ "repo_id": "ku-nlp/deberta-v2-large-japanese-char-wwm",
4
+ "files": [
5
+ "pytorch_model.bin",
6
+ "config.json",
7
+ "tokenizer_config.json",
8
+ "special_tokens_map.json",
9
+ "vocab.txt"
10
+ ]
11
+ }
12
+ }
bert/deberta-v2-large-japanese-char-wwm/.gitattributes ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tflite filter=lfs diff=lfs merge=lfs -text
29
+ *.tgz filter=lfs diff=lfs merge=lfs -text
30
+ *.wasm filter=lfs diff=lfs merge=lfs -text
31
+ *.xz filter=lfs diff=lfs merge=lfs -text
32
+ *.zip filter=lfs diff=lfs merge=lfs -text
33
+ *.zst filter=lfs diff=lfs merge=lfs -text
34
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
bert/deberta-v2-large-japanese-char-wwm/README.md ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ language: ja
3
+ license: cc-by-sa-4.0
4
+ library_name: transformers
5
+ tags:
6
+ - deberta
7
+ - deberta-v2
8
+ - fill-mask
9
+ - character
10
+ - wwm
11
+ datasets:
12
+ - wikipedia
13
+ - cc100
14
+ - oscar
15
+ metrics:
16
+ - accuracy
17
+ mask_token: "[MASK]"
18
+ widget:
19
+ - text: "京都大学で自然言語処理を[MASK][MASK]する。"
20
+ ---
21
+
22
+ # Model Card for Japanese character-level DeBERTa V2 large
23
+
24
+ ## Model description
25
+
26
+ This is a Japanese DeBERTa V2 large model pre-trained on Japanese Wikipedia, the Japanese portion of CC-100, and the Japanese portion of OSCAR.
27
+ This model is trained with character-level tokenization and whole word masking.
28
+
29
+ ## How to use
30
+
31
+ You can use this model for masked language modeling as follows:
32
+
33
+ ```python
34
+ from transformers import AutoTokenizer, AutoModelForMaskedLM
35
+ tokenizer = AutoTokenizer.from_pretrained('ku-nlp/deberta-v2-large-japanese-char-wwm')
36
+ model = AutoModelForMaskedLM.from_pretrained('ku-nlp/deberta-v2-large-japanese-char-wwm')
37
+
38
+ sentence = '京都大学で自然言語処理を[MASK][MASK]する。'
39
+ encoding = tokenizer(sentence, return_tensors='pt')
40
+ ...
41
+ ```
42
+
43
+ You can also fine-tune this model on downstream tasks.
44
+
45
+ ## Tokenization
46
+
47
+ There is no need to tokenize texts in advance, and you can give raw texts to the tokenizer.
48
+ The texts are tokenized into character-level tokens by [sentencepiece](https://github.com/google/sentencepiece).
49
+
50
+ ## Training data
51
+
52
+ We used the following corpora for pre-training:
53
+
54
+ - Japanese Wikipedia (as of 20221020, 3.2GB, 27M sentences, 1.3M documents)
55
+ - Japanese portion of CC-100 (85GB, 619M sentences, 66M documents)
56
+ - Japanese portion of OSCAR (54GB, 326M sentences, 25M documents)
57
+
58
+ Note that we filtered out documents annotated with "header", "footer", or "noisy" tags in OSCAR.
59
+ Also note that Japanese Wikipedia was duplicated 10 times to make the total size of the corpus comparable to that of CC-100 and OSCAR. As a result, the total size of the training data is 171GB.
60
+
61
+ ## Training procedure
62
+
63
+ We first segmented texts in the corpora into words using [Juman++ 2.0.0-rc3](https://github.com/ku-nlp/jumanpp/releases/tag/v2.0.0-rc3) for whole word masking.
64
+ Then, we built a sentencepiece model with 22,012 tokens including all characters that appear in the training corpus.
65
+
66
+ We tokenized raw corpora into character-level subwords using the sentencepiece model and trained the Japanese DeBERTa model using [transformers](https://github.com/huggingface/transformers) library.
67
+ The training took 26 days using 16 NVIDIA A100-SXM4-40GB GPUs.
68
+
69
+ The following hyperparameters were used during pre-training:
70
+
71
+ - learning_rate: 1e-4
72
+ - per_device_train_batch_size: 26
73
+ - distributed_type: multi-GPU
74
+ - num_devices: 16
75
+ - gradient_accumulation_steps: 8
76
+ - total_train_batch_size: 3,328
77
+ - max_seq_length: 512
78
+ - optimizer: Adam with betas=(0.9,0.999) and epsilon=1e-06
79
+ - lr_scheduler_type: linear schedule with warmup (lr = 0 at 300k steps)
80
+ - training_steps: 260,000
81
+ - warmup_steps: 10,000
82
+
83
+ The accuracy of the trained model on the masked language modeling task was 0.795.
84
+ The evaluation set consists of 5,000 randomly sampled documents from each of the training corpora.
85
+
86
+ ## Acknowledgments
87
+
88
+ This work was supported by Joint Usage/Research Center for Interdisciplinary Large-scale Information Infrastructures (JHPCN) through General Collaboration Project no. jh221004, "Developing a Platform for Constructing and Sharing of Large-Scale Japanese Language Models".
89
+ For training models, we used the mdx: a platform for the data-driven future.
bert/deberta-v2-large-japanese-char-wwm/config.json ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "DebertaV2ForMaskedLM"
4
+ ],
5
+ "attention_head_size": 64,
6
+ "attention_probs_dropout_prob": 0.1,
7
+ "conv_act": "gelu",
8
+ "conv_kernel_size": 3,
9
+ "hidden_act": "gelu",
10
+ "hidden_dropout_prob": 0.1,
11
+ "hidden_size": 1024,
12
+ "initializer_range": 0.02,
13
+ "intermediate_size": 4096,
14
+ "layer_norm_eps": 1e-07,
15
+ "max_position_embeddings": 512,
16
+ "max_relative_positions": -1,
17
+ "model_type": "deberta-v2",
18
+ "norm_rel_ebd": "layer_norm",
19
+ "num_attention_heads": 16,
20
+ "num_hidden_layers": 24,
21
+ "pad_token_id": 0,
22
+ "pooler_dropout": 0,
23
+ "pooler_hidden_act": "gelu",
24
+ "pooler_hidden_size": 1024,
25
+ "pos_att_type": [
26
+ "p2c",
27
+ "c2p"
28
+ ],
29
+ "position_biased_input": false,
30
+ "position_buckets": 256,
31
+ "relative_attention": true,
32
+ "share_att_key": true,
33
+ "torch_dtype": "float16",
34
+ "transformers_version": "4.25.1",
35
+ "type_vocab_size": 0,
36
+ "vocab_size": 22012
37
+ }
bert/deberta-v2-large-japanese-char-wwm/special_tokens_map.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "cls_token": "[CLS]",
3
+ "mask_token": "[MASK]",
4
+ "pad_token": "[PAD]",
5
+ "sep_token": "[SEP]",
6
+ "unk_token": "[UNK]"
7
+ }
bert/deberta-v2-large-japanese-char-wwm/tokenizer_config.json ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cls_token": "[CLS]",
3
+ "do_lower_case": false,
4
+ "do_subword_tokenize": true,
5
+ "do_word_tokenize": true,
6
+ "jumanpp_kwargs": null,
7
+ "mask_token": "[MASK]",
8
+ "mecab_kwargs": null,
9
+ "model_max_length": 1000000000000000019884624838656,
10
+ "never_split": null,
11
+ "pad_token": "[PAD]",
12
+ "sep_token": "[SEP]",
13
+ "special_tokens_map_file": null,
14
+ "subword_tokenizer_type": "character",
15
+ "sudachi_kwargs": null,
16
+ "tokenizer_class": "BertJapaneseTokenizer",
17
+ "unk_token": "[UNK]",
18
+ "word_tokenizer_type": "basic"
19
+ }
bert/deberta-v2-large-japanese-char-wwm/vocab.txt ADDED
The diff for this file is too large to render. See raw diff
 
config.py ADDED
@@ -0,0 +1,307 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ @Desc: 全局配置文件读取
3
+ """
4
+
5
+ import shutil
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ import torch
10
+ import yaml
11
+
12
+ from style_bert_vits2.logging import logger
13
+
14
+
15
+ class PathConfig:
16
+ def __init__(self, dataset_root: str, assets_root: str):
17
+ self.dataset_root = Path(dataset_root)
18
+ self.assets_root = Path(assets_root)
19
+
20
+
21
+ # If not cuda available, set possible devices to cpu
22
+ cuda_available = torch.cuda.is_available()
23
+
24
+
25
+ class Resample_config:
26
+ """重采样配置"""
27
+
28
+ def __init__(self, in_dir: str, out_dir: str, sampling_rate: int = 44100):
29
+ self.sampling_rate = sampling_rate # 目标采样率
30
+ self.in_dir = Path(in_dir) # 待处理音频目录路径
31
+ self.out_dir = Path(out_dir) # 重采样输出路径
32
+
33
+ @classmethod
34
+ def from_dict(cls, dataset_path: Path, data: dict[str, Any]):
35
+ """从字典中生成实例"""
36
+
37
+ # 不检查路径是否有效,此逻辑在resample.py中处理
38
+ data["in_dir"] = dataset_path / data["in_dir"]
39
+ data["out_dir"] = dataset_path / data["out_dir"]
40
+
41
+ return cls(**data)
42
+
43
+
44
+ class Preprocess_text_config:
45
+ """数据预处理配置"""
46
+
47
+ def __init__(
48
+ self,
49
+ transcription_path: str,
50
+ cleaned_path: str,
51
+ train_path: str,
52
+ val_path: str,
53
+ config_path: str,
54
+ val_per_lang: int = 5,
55
+ max_val_total: int = 10000,
56
+ clean: bool = True,
57
+ ):
58
+ self.transcription_path = Path(transcription_path)
59
+ self.train_path = Path(train_path)
60
+ if cleaned_path == "" or cleaned_path is None:
61
+ self.cleaned_path = self.transcription_path.with_name(
62
+ self.transcription_path.name + ".cleaned"
63
+ )
64
+ else:
65
+ self.cleaned_path = Path(cleaned_path)
66
+ self.val_path = Path(val_path)
67
+ self.config_path = Path(config_path)
68
+ self.val_per_lang = val_per_lang
69
+ self.max_val_total = max_val_total
70
+ self.clean = clean
71
+
72
+ @classmethod
73
+ def from_dict(cls, dataset_path: Path, data: dict[str, Any]):
74
+ """从字典中生成实例"""
75
+
76
+ data["transcription_path"] = dataset_path / data["transcription_path"]
77
+ if data["cleaned_path"] == "" or data["cleaned_path"] is None:
78
+ data["cleaned_path"] = ""
79
+ else:
80
+ data["cleaned_path"] = dataset_path / data["cleaned_path"]
81
+ data["train_path"] = dataset_path / data["train_path"]
82
+ data["val_path"] = dataset_path / data["val_path"]
83
+ data["config_path"] = dataset_path / data["config_path"]
84
+
85
+ return cls(**data)
86
+
87
+
88
+ class Bert_gen_config:
89
+ """bert_gen 配置"""
90
+
91
+ def __init__(
92
+ self,
93
+ config_path: str,
94
+ num_processes: int = 1,
95
+ device: str = "cuda",
96
+ use_multi_device: bool = False,
97
+ ):
98
+ self.config_path = Path(config_path)
99
+ self.num_processes = num_processes
100
+ if not cuda_available:
101
+ device = "cpu"
102
+ self.device = device
103
+ self.use_multi_device = use_multi_device
104
+
105
+ @classmethod
106
+ def from_dict(cls, dataset_path: Path, data: dict[str, Any]):
107
+ data["config_path"] = dataset_path / data["config_path"]
108
+
109
+ return cls(**data)
110
+
111
+
112
+ class Style_gen_config:
113
+ """style_gen 配置"""
114
+
115
+ def __init__(
116
+ self,
117
+ config_path: str,
118
+ num_processes: int = 4,
119
+ device: str = "cuda",
120
+ ):
121
+ self.config_path = Path(config_path)
122
+ self.num_processes = num_processes
123
+ if not cuda_available:
124
+ device = "cpu"
125
+ self.device = device
126
+
127
+ @classmethod
128
+ def from_dict(cls, dataset_path: Path, data: dict[str, Any]):
129
+ data["config_path"] = dataset_path / data["config_path"]
130
+
131
+ return cls(**data)
132
+
133
+
134
+ class Train_ms_config:
135
+ """训练配置"""
136
+
137
+ def __init__(
138
+ self,
139
+ config_path: str,
140
+ env: dict[str, Any],
141
+ # base: Dict[str, any],
142
+ model_dir: str,
143
+ num_workers: int,
144
+ spec_cache: bool,
145
+ keep_ckpts: int,
146
+ ):
147
+ self.env = env # 需要加载的环境变量
148
+ # self.base = base # 底模配置
149
+ self.model_dir = Path(
150
+ model_dir
151
+ ) # 训练模型存储目录,该路径为相对于dataset_path的路径,而非项目根目录
152
+ self.config_path = Path(config_path) # 配置文件路径
153
+ self.num_workers = num_workers # worker数量
154
+ self.spec_cache = spec_cache # 是否启用spec缓存
155
+ self.keep_ckpts = keep_ckpts # ckpt数量
156
+
157
+ @classmethod
158
+ def from_dict(cls, dataset_path: Path, data: dict[str, Any]):
159
+ # data["model"] = os.path.join(dataset_path, data["model"])
160
+ data["config_path"] = dataset_path / data["config_path"]
161
+
162
+ return cls(**data)
163
+
164
+
165
+ class Webui_config:
166
+ """webui 配置 (for webui.py, not supported now)"""
167
+
168
+ def __init__(
169
+ self,
170
+ device: str,
171
+ model: str,
172
+ config_path: str,
173
+ language_identification_library: str,
174
+ port: int = 7860,
175
+ share: bool = False,
176
+ debug: bool = False,
177
+ ):
178
+ if not cuda_available:
179
+ device = "cpu"
180
+ self.device = device
181
+ self.model = Path(model)
182
+ self.config_path = Path(config_path)
183
+ self.port: int = port
184
+ self.share: bool = share
185
+ self.debug: bool = debug
186
+ self.language_identification_library: str = language_identification_library
187
+
188
+ @classmethod
189
+ def from_dict(cls, dataset_path: Path, data: dict[str, Any]):
190
+ data["config_path"] = dataset_path / data["config_path"]
191
+ data["model"] = dataset_path / data["model"]
192
+ return cls(**data)
193
+
194
+
195
+ class Server_config:
196
+ def __init__(
197
+ self,
198
+ port: int = 5000,
199
+ device: str = "cuda",
200
+ limit: int = 100,
201
+ language: str = "JP",
202
+ origins: list[str] = ["*"],
203
+ ):
204
+ self.port: int = port
205
+ if not cuda_available:
206
+ device = "cpu"
207
+ self.device: str = device
208
+ self.language: str = language
209
+ self.limit: int = limit
210
+ self.origins: list[str] = origins
211
+
212
+ @classmethod
213
+ def from_dict(cls, data: dict[str, Any]):
214
+ return cls(**data)
215
+
216
+
217
+ class Translate_config:
218
+ """翻译api配置"""
219
+
220
+ def __init__(self, app_key: str, secret_key: str):
221
+ self.app_key = app_key
222
+ self.secret_key = secret_key
223
+
224
+ @classmethod
225
+ def from_dict(cls, data: dict[str, Any]):
226
+ return cls(**data)
227
+
228
+
229
+ class Config:
230
+ def __init__(self, config_path: str, path_config: PathConfig):
231
+ if not Path(config_path).exists():
232
+ shutil.copy(src="default_config.yml", dst=config_path)
233
+ logger.info(
234
+ f"A configuration file {config_path} has been generated based on the default configuration file default_config.yml."
235
+ )
236
+ logger.info(
237
+ "Please do not modify default_config.yml. Instead, modify config.yml."
238
+ )
239
+ # sys.exit(0)
240
+ with open(config_path, encoding="utf-8") as file:
241
+ yaml_config: dict[str, Any] = yaml.safe_load(file.read())
242
+ model_name: str = yaml_config["model_name"]
243
+ self.model_name: str = model_name
244
+ if "dataset_path" in yaml_config:
245
+ dataset_path = Path(yaml_config["dataset_path"])
246
+ else:
247
+ dataset_path = path_config.dataset_root / model_name
248
+ self.dataset_path = dataset_path
249
+ self.dataset_root = path_config.dataset_root
250
+ self.assets_root = path_config.assets_root
251
+ self.out_dir = self.assets_root / model_name
252
+ self.resample_config: Resample_config = Resample_config.from_dict(
253
+ dataset_path, yaml_config["resample"]
254
+ )
255
+ self.preprocess_text_config: Preprocess_text_config = (
256
+ Preprocess_text_config.from_dict(
257
+ dataset_path, yaml_config["preprocess_text"]
258
+ )
259
+ )
260
+ self.bert_gen_config: Bert_gen_config = Bert_gen_config.from_dict(
261
+ dataset_path, yaml_config["bert_gen"]
262
+ )
263
+ self.style_gen_config: Style_gen_config = Style_gen_config.from_dict(
264
+ dataset_path, yaml_config["style_gen"]
265
+ )
266
+ self.train_ms_config: Train_ms_config = Train_ms_config.from_dict(
267
+ dataset_path, yaml_config["train_ms"]
268
+ )
269
+ self.webui_config: Webui_config = Webui_config.from_dict(
270
+ dataset_path, yaml_config["webui"]
271
+ )
272
+ self.server_config: Server_config = Server_config.from_dict(
273
+ yaml_config["server"]
274
+ )
275
+ # self.translate_config: Translate_config = Translate_config.from_dict(
276
+ # yaml_config["translate"]
277
+ # )
278
+
279
+
280
+ # Load and initialize the configuration
281
+
282
+
283
+ def get_path_config() -> PathConfig:
284
+ path_config_path = Path("configs/paths.yml")
285
+ if not path_config_path.exists():
286
+ shutil.copy(src="configs/default_paths.yml", dst=path_config_path)
287
+ logger.info(
288
+ f"A configuration file {path_config_path} has been generated based on the default configuration file default_paths.yml."
289
+ )
290
+ logger.info(
291
+ "Please do not modify configs/default_paths.yml. Instead, modify configs/paths.yml."
292
+ )
293
+ with open(path_config_path, encoding="utf-8") as file:
294
+ path_config_dict: dict[str, str] = yaml.safe_load(file.read())
295
+ return PathConfig(**path_config_dict)
296
+
297
+
298
+ def get_config() -> Config:
299
+ path_config = get_path_config()
300
+ try:
301
+ config = Config("config.yml", path_config)
302
+ except (TypeError, KeyError):
303
+ logger.warning("Old config.yml found. Replace it with default_config.yml.")
304
+ shutil.copy(src="default_config.yml", dst="config.yml")
305
+ config = Config("config.yml", path_config)
306
+
307
+ return config
configs/config.json ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_name": "Dummy",
3
+ "train": {
4
+ "log_interval": 200,
5
+ "eval_interval": 1000,
6
+ "seed": 42,
7
+ "epochs": 1000,
8
+ "learning_rate": 0.0002,
9
+ "betas": [0.8, 0.99],
10
+ "eps": 1e-9,
11
+ "batch_size": 2,
12
+ "bf16_run": false,
13
+ "lr_decay": 0.99995,
14
+ "segment_size": 16384,
15
+ "init_lr_ratio": 1,
16
+ "warmup_epochs": 0,
17
+ "c_mel": 45,
18
+ "c_kl": 1.0,
19
+ "skip_optimizer": false,
20
+ "freeze_ZH_bert": false,
21
+ "freeze_JP_bert": false,
22
+ "freeze_EN_bert": false,
23
+ "freeze_style": false,
24
+ "freeze_encoder": false
25
+ },
26
+ "data": {
27
+ "use_jp_extra": false,
28
+ "training_files": "Data/Dummy/train.list",
29
+ "validation_files": "Data/Dummy/val.list",
30
+ "max_wav_value": 32768.0,
31
+ "sampling_rate": 44100,
32
+ "filter_length": 2048,
33
+ "hop_length": 512,
34
+ "win_length": 2048,
35
+ "n_mel_channels": 128,
36
+ "mel_fmin": 0.0,
37
+ "mel_fmax": null,
38
+ "add_blank": true,
39
+ "n_speakers": 1,
40
+ "cleaned_text": true,
41
+ "num_styles": 1,
42
+ "style2id": {
43
+ "Neutral": 0
44
+ }
45
+ },
46
+ "model": {
47
+ "use_spk_conditioned_encoder": true,
48
+ "use_noise_scaled_mas": true,
49
+ "use_mel_posterior_encoder": false,
50
+ "use_duration_discriminator": true,
51
+ "inter_channels": 192,
52
+ "hidden_channels": 192,
53
+ "filter_channels": 768,
54
+ "n_heads": 2,
55
+ "n_layers": 6,
56
+ "kernel_size": 3,
57
+ "p_dropout": 0.1,
58
+ "resblock": "1",
59
+ "resblock_kernel_sizes": [3, 7, 11],
60
+ "resblock_dilation_sizes": [
61
+ [1, 3, 5],
62
+ [1, 3, 5],
63
+ [1, 3, 5]
64
+ ],
65
+ "upsample_rates": [8, 8, 2, 2, 2],
66
+ "upsample_initial_channel": 512,
67
+ "upsample_kernel_sizes": [16, 16, 8, 2, 2],
68
+ "n_layers_q": 3,
69
+ "use_spectral_norm": false,
70
+ "gin_channels": 256
71
+ },
72
+ "version": "2.5.0"
73
+ }
configs/config_jp_extra.json ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_name": "Dummy",
3
+ "train": {
4
+ "log_interval": 200,
5
+ "eval_interval": 1000,
6
+ "seed": 42,
7
+ "epochs": 1000,
8
+ "learning_rate": 0.0001,
9
+ "betas": [0.8, 0.99],
10
+ "eps": 1e-9,
11
+ "batch_size": 2,
12
+ "bf16_run": false,
13
+ "fp16_run": false,
14
+ "lr_decay": 0.99996,
15
+ "segment_size": 16384,
16
+ "init_lr_ratio": 1,
17
+ "warmup_epochs": 0,
18
+ "c_mel": 45,
19
+ "c_kl": 1.0,
20
+ "c_commit": 100,
21
+ "skip_optimizer": false,
22
+ "freeze_ZH_bert": false,
23
+ "freeze_JP_bert": false,
24
+ "freeze_EN_bert": false,
25
+ "freeze_emo": false,
26
+ "freeze_style": false,
27
+ "freeze_decoder": false
28
+ },
29
+ "data": {
30
+ "use_jp_extra": true,
31
+ "training_files": "Data/Dummy/train.list",
32
+ "validation_files": "Data/Dummy/val.list",
33
+ "max_wav_value": 32768.0,
34
+ "sampling_rate": 44100,
35
+ "filter_length": 2048,
36
+ "hop_length": 512,
37
+ "win_length": 2048,
38
+ "n_mel_channels": 128,
39
+ "mel_fmin": 0.0,
40
+ "mel_fmax": null,
41
+ "add_blank": true,
42
+ "n_speakers": 512,
43
+ "cleaned_text": true
44
+ },
45
+ "model": {
46
+ "use_spk_conditioned_encoder": true,
47
+ "use_noise_scaled_mas": true,
48
+ "use_mel_posterior_encoder": false,
49
+ "use_duration_discriminator": false,
50
+ "use_wavlm_discriminator": true,
51
+ "inter_channels": 192,
52
+ "hidden_channels": 192,
53
+ "filter_channels": 768,
54
+ "n_heads": 2,
55
+ "n_layers": 6,
56
+ "kernel_size": 3,
57
+ "p_dropout": 0.1,
58
+ "resblock": "1",
59
+ "resblock_kernel_sizes": [3, 7, 11],
60
+ "resblock_dilation_sizes": [
61
+ [1, 3, 5],
62
+ [1, 3, 5],
63
+ [1, 3, 5]
64
+ ],
65
+ "upsample_rates": [8, 8, 2, 2, 2],
66
+ "upsample_initial_channel": 512,
67
+ "upsample_kernel_sizes": [16, 16, 8, 2, 2],
68
+ "n_layers_q": 3,
69
+ "use_spectral_norm": false,
70
+ "gin_channels": 512,
71
+ "slm": {
72
+ "model": "./slm/wavlm-base-plus",
73
+ "sr": 16000,
74
+ "hidden": 768,
75
+ "nlayers": 13,
76
+ "initial_channel": 64
77
+ }
78
+ },
79
+ "version": "2.5.0-JP-Extra"
80
+ }
configs/default_paths.yml ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ # Root directory of the training dataset.
2
+ # The training dataset of {model_name} should be placed in {dataset_root}/{model_name}.
3
+ dataset_root: Data
4
+
5
+ # Root directory of the model assets (for inference).
6
+ # In training, the model assets will be saved to {assets_root}/{model_name},
7
+ # and in inference, we load all the models from {assets_root}.
8
+ assets_root: model_assets
configs/paths.yml ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ # Root directory of the training dataset.
2
+ # The training dataset of {model_name} should be placed in {dataset_root}/{model_name}.
3
+ dataset_root: Data
4
+
5
+ # Root directory of the model assets (for inference).
6
+ # In training, the model assets will be saved to {assets_root}/{model_name},
7
+ # and in inference, we load all the models from {assets_root}.
8
+ assets_root: model_assets
default_config.yml ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ model_name: "model_name"
2
+
3
+ # If you want to use a specific dataset path, uncomment the following line.
4
+ # Otherwise, the dataset path is `{dataset_root}/{model_name}`.
5
+
6
+ # dataset_path: "your/dataset/path"
7
+
8
+ resample:
9
+ sampling_rate: 44100
10
+ in_dir: "raw"
11
+ out_dir: "wavs"
12
+
13
+ preprocess_text:
14
+ transcription_path: "esd.list"
15
+ cleaned_path: ""
16
+ train_path: "train.list"
17
+ val_path: "val.list"
18
+ config_path: "config.json"
19
+ val_per_lang: 0
20
+ max_val_total: 12
21
+ clean: true
22
+
23
+ bert_gen:
24
+ config_path: "config.json"
25
+ num_processes: 1
26
+ device: "cuda"
27
+ use_multi_device: false
28
+
29
+ style_gen:
30
+ config_path: "config.json"
31
+ num_processes: 4
32
+ device: "cuda"
33
+
34
+ train_ms:
35
+ env:
36
+ MASTER_ADDR: "localhost"
37
+ MASTER_PORT: 10086
38
+ WORLD_SIZE: 1
39
+ LOCAL_RANK: 0
40
+ RANK: 0
41
+ model_dir: "models" # The directory to save the model (for training), relative to `{dataset_root}/{model_name}`.
42
+ config_path: "config.json"
43
+ num_workers: 16
44
+ spec_cache: True
45
+ keep_ckpts: 1 # Set this to 0 to keep all checkpoints
46
+
47
+ webui: # For `webui.py`, which is not supported yet in Style-Bert-VITS2.
48
+ # 推理设备
49
+ device: "cuda"
50
+ # 模型路径
51
+ model: "models/G_8000.pth"
52
+ # 配置文件路径
53
+ config_path: "config.json"
54
+ # 端口号
55
+ port: 7860
56
+ # 是否公开部署,对外网开放
57
+ share: false
58
+ # 是否开启debug模式
59
+ debug: false
60
+ # 语种识别库,可选langid, fastlid
61
+ language_identification_library: "langid"
62
+
63
+ # server_fastapi's config
64
+ server:
65
+ port: 5000
66
+ device: "cuda"
67
+ language: "JP"
68
+ limit: 100
69
+ origins:
70
+ - "*"
frontend/next-env.d.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
3
+
4
+ // NOTE: This file should not be edited
5
+ // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
frontend/next.config.js ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('next').NextConfig} */
2
+ const nextConfig = {
3
+ output: 'export',
4
+ trailingSlash: true,
5
+ distDir: 'out',
6
+ images: {
7
+ unoptimized: true,
8
+ },
9
+ };
10
+
11
+ module.exports = nextConfig;
frontend/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
frontend/package.json ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "sbv2-editor",
3
+ "version": "0.4.3",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build && npm run copy-static",
8
+ "copy-static": "rm -rf ../static && cp -r out ../static",
9
+ "start": "next start",
10
+ "lint": "next lint",
11
+ "lint:fix": "next lint --fix",
12
+ "format": "prettier --write --ignore-path .gitignore './**/*.{js,jsx,ts,tsx,json}'",
13
+ "node-version": "node --version"
14
+ },
15
+ "dependencies": {
16
+ "@emotion/react": "^11.11.3",
17
+ "@emotion/styled": "^11.11.0",
18
+ "@mui/icons-material": "^5.15.10",
19
+ "@mui/material": "^5.15.10",
20
+ "@mui/material-nextjs": "^5.15.9",
21
+ "file-saver": "^2.0.5",
22
+ "next": "^14.2.3",
23
+ "react": "^18",
24
+ "react-dom": "^18"
25
+ },
26
+ "devDependencies": {
27
+ "@types/file-saver": "^2.0.7",
28
+ "@types/node": "^20",
29
+ "@types/react": "^18",
30
+ "@types/react-dom": "^18",
31
+ "@typescript-eslint/eslint-plugin": "^6.20.0",
32
+ "autoprefixer": "^10.0.1",
33
+ "eslint": "^8",
34
+ "eslint-config-next": "14.1.0",
35
+ "eslint-config-prettier": "^9.1.0",
36
+ "eslint-plugin-import": "^2.29.1",
37
+ "eslint-plugin-simple-import-sort": "^12.0.0",
38
+ "postcss": "^8",
39
+ "prettier": "^3.2.5",
40
+ "tailwindcss": "^3.3.0",
41
+ "typescript": "^5"
42
+ }
43
+ }
frontend/public/images/kariusagi.png ADDED

Git LFS Details

  • SHA256: 573a3960e5edc5f2de0c5ace778d311917214127e4ea197b16a83085e83c2c39
  • Pointer size: 131 Bytes
  • Size of remote file: 372 kB
frontend/src/app/globals.css ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
frontend/src/app/layout.tsx ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import './globals.css';
2
+
3
+ import { ThemeProvider } from '@mui/material/styles';
4
+ import { AppRouterCacheProvider } from '@mui/material-nextjs/v13-appRouter';
5
+ import type { Metadata } from 'next';
6
+ import { Inter } from 'next/font/google';
7
+
8
+ import theme from '../theme';
9
+
10
+ const inter = Inter({ subsets: ['latin'] });
11
+
12
+ export const metadata: Metadata = {
13
+ title: 'サンプルズの音声生成ツール(Style-Bert-VITS2 エディター)',
14
+ description: 'Style-Bert-VITS2の音声合成エディターです。',
15
+ };
16
+
17
+ export default function RootLayout({
18
+ children,
19
+ }: Readonly<{
20
+ children: React.ReactNode;
21
+ }>) {
22
+ return (
23
+ <html lang='en'>
24
+ <body className={inter.className}>
25
+ <AppRouterCacheProvider>
26
+ <ThemeProvider theme={theme}>{children}</ThemeProvider>
27
+ </AppRouterCacheProvider>
28
+ </body>
29
+ </html>
30
+ );
31
+ }
frontend/src/app/page.tsx ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+ import { Container } from '@mui/material';
3
+
4
+ import AlertPopup from '@/components/AlertPopup';
5
+ import EditorContainer from '@/components/EditorContainer';
6
+ import PopupProvider from '@/contexts/PopupProvider';
7
+
8
+ export default function Home() {
9
+ return (
10
+ <Container maxWidth='xl'>
11
+ <PopupProvider>
12
+ <AlertPopup />
13
+ <EditorContainer />
14
+ </PopupProvider>
15
+ </Container>
16
+ );
17
+ }
frontend/src/components/AccentEditor.tsx ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import {
4
+ Paper,
5
+ Stack,
6
+ ToggleButton,
7
+ ToggleButtonGroup,
8
+ Typography,
9
+ } from '@mui/material';
10
+
11
+ export interface MoraTone {
12
+ mora: string; // katakana
13
+ tone: 0 | 1; // 0: low, 1: high
14
+ }
15
+
16
+ interface MoraToneToggleProps {
17
+ moraTone: MoraTone;
18
+ onChange: (tone: 0 | 1) => void;
19
+ visible?: boolean;
20
+ disabled?: boolean;
21
+ }
22
+
23
+ function MoraToneToggle({
24
+ moraTone: { mora, tone },
25
+ onChange,
26
+ visible = true,
27
+ disabled = false,
28
+ }: MoraToneToggleProps) {
29
+ const handleChange = (_: React.MouseEvent<HTMLElement>, newTone: 0 | 1) => {
30
+ if (newTone !== null) {
31
+ onChange(newTone);
32
+ }
33
+ };
34
+
35
+ return (
36
+ <Stack
37
+ direction='column'
38
+ spacing={1}
39
+ sx={{ textAlign: 'center', visibility: visible ? 'visible' : 'hidden' }}
40
+ >
41
+ <Typography>{mora}</Typography>
42
+ <ToggleButtonGroup
43
+ exclusive
44
+ color='primary'
45
+ orientation='vertical'
46
+ value={tone}
47
+ onChange={handleChange}
48
+ disabled={disabled}
49
+ >
50
+ <ToggleButton value={1}>高</ToggleButton>
51
+ <ToggleButton value={0}>低</ToggleButton>
52
+ </ToggleButtonGroup>
53
+ </Stack>
54
+ );
55
+ }
56
+
57
+ interface AccentEditorProps {
58
+ moraToneList: MoraTone[];
59
+ setMoraToneList?: (moraToneList: MoraTone[]) => void;
60
+ onChange?: (tone: 0 | 1, index: number) => void;
61
+ disabled?: boolean;
62
+ }
63
+
64
+ export default function AccentEditor({
65
+ moraToneList,
66
+ setMoraToneList,
67
+ onChange,
68
+ disabled = false,
69
+ }: AccentEditorProps) {
70
+ // ダミーのデータを定義
71
+ const dummyMoraToneList: MoraTone[] = [{ mora: 'ア', tone: 0 }];
72
+
73
+ // 実際にレンダリングするリストを決定
74
+ const displayList =
75
+ moraToneList && moraToneList.length > 0 ? moraToneList : dummyMoraToneList;
76
+
77
+ const handleChange = (tone: 0 | 1, index: number) => {
78
+ if (onChange) {
79
+ onChange(tone, index);
80
+ return;
81
+ }
82
+ if (!setMoraToneList) {
83
+ return;
84
+ }
85
+ const newKataToneList = [...moraToneList];
86
+ newKataToneList[index] = { ...newKataToneList[index], tone };
87
+ setMoraToneList(newKataToneList);
88
+ };
89
+ return (
90
+ <Paper sx={{ p: 1, mt: 2 }}>
91
+ <Stack
92
+ spacing={1}
93
+ direction='row'
94
+ sx={{ maxWidth: '100%', overflow: 'auto' }}
95
+ >
96
+ {displayList.map((moraTone, index) => (
97
+ <MoraToneToggle
98
+ key={index}
99
+ moraTone={moraTone}
100
+ onChange={(tone) => handleChange(tone, index)}
101
+ visible={moraToneList && moraToneList.length > 0}
102
+ disabled={disabled}
103
+ />
104
+ ))}
105
+ </Stack>
106
+ </Paper>
107
+ );
108
+ }
frontend/src/components/AlertPopup.tsx ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Alert, Snackbar } from '@mui/material';
2
+ import React from 'react';
3
+
4
+ import { usePopup } from '@/contexts/PopupProvider';
5
+
6
+ const AlertPopup = () => {
7
+ const { isOpen, closePopup, message, severity } = usePopup();
8
+
9
+ return (
10
+ <Snackbar
11
+ open={isOpen}
12
+ // autoHideDuration={6000}
13
+ onClose={closePopup}
14
+ anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
15
+ >
16
+ <Alert
17
+ onClose={closePopup}
18
+ severity={severity}
19
+ sx={{ width: '100%' }}
20
+ elevation={6}
21
+ >
22
+ {message}
23
+ </Alert>
24
+ </Snackbar>
25
+ );
26
+ };
27
+
28
+ export default AlertPopup;
frontend/src/components/DictionaryDialog.tsx ADDED
@@ -0,0 +1,541 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // TODO: Refactor, 長い
2
+ import AddIcon from '@mui/icons-material/Add';
3
+ import DeleteIcon from '@mui/icons-material/Delete';
4
+ import RefreshIcon from '@mui/icons-material/Refresh';
5
+ import {
6
+ Box,
7
+ Button,
8
+ Dialog,
9
+ DialogContent,
10
+ DialogTitle,
11
+ Divider,
12
+ FormControl,
13
+ FormControlLabel,
14
+ FormLabel,
15
+ List,
16
+ ListItem,
17
+ ListItemButton,
18
+ ListItemIcon,
19
+ ListItemText,
20
+ Radio,
21
+ RadioGroup,
22
+ Slider,
23
+ Stack,
24
+ TextField,
25
+ Typography,
26
+ } from '@mui/material';
27
+ import Grid from '@mui/material/Unstable_Grid2/Grid2';
28
+ import { useEffect, useState } from 'react';
29
+
30
+ import { usePopup } from '@/contexts/PopupProvider';
31
+ import { fetchApi } from '@/utils/api';
32
+
33
+ import type { MoraTone } from './AccentEditor';
34
+ import AccentEditor from './AccentEditor';
35
+
36
+ interface UserDictWord {
37
+ surface: string;
38
+ pronunciation: string;
39
+ // 1-indexedだがアクセント核がない場合は0。json辞書のキーと合わせるためにsnake_caseにしている
40
+ accent_type: number;
41
+ priority: number;
42
+ }
43
+
44
+ interface UserDict {
45
+ [uuid: string]: UserDictWord;
46
+ }
47
+
48
+ interface UserDictElement {
49
+ uuid: string;
50
+ word: UserDictWord;
51
+ }
52
+
53
+ // UserDictElementと等価だがUI上での使いやすい形式
54
+ // moraToneListは最後に助詞「ガ」を含むが、pronunciationは助詞「ガ」を含まない
55
+ interface WordState {
56
+ uuid: string;
57
+ surface: string;
58
+ pronunciation: string;
59
+ moraToneList: MoraTone[];
60
+ accentIndex: number; // 0-indexedで高→低となる最後の高の添字
61
+ priority: number;
62
+ }
63
+
64
+ const defaultWordState: WordState = {
65
+ uuid: '',
66
+ surface: '',
67
+ pronunciation: '',
68
+ moraToneList: [{ mora: 'ガ', tone: 0 }],
69
+ accentIndex: 0,
70
+ priority: 5,
71
+ };
72
+
73
+ // カタカナのみで構成された文字列をモーラに分割する
74
+ // 参考:https://github.com/VOICEVOX/voicevox_engine/blob/f181411ec69812296989d9cc583826c22eec87ae/voicevox_engine/model.py#L270
75
+ function extractMorae(pronunciation: string): string[] {
76
+ const ruleOthers =
77
+ '[イ][ェ]|[ヴ][ャュョ]|[トド][ゥ]|[テデ][ィャュョ]|[デ][ェ]|[クグ][ヮ]';
78
+ const ruleLineI = '[キシチニヒミリギジビピ][ェャュョ]';
79
+ const ruleLineU = '[ツフヴ][ァ]|[ウスツフヴズ][ィ]|[ウツフヴ][ェォ]';
80
+ const ruleOneMora = '[ァ-ヴー]';
81
+
82
+ const pattern = new RegExp(
83
+ `${ruleOthers}|${ruleLineI}|${ruleLineU}|${ruleOneMora}`,
84
+ 'g',
85
+ );
86
+
87
+ return pronunciation.match(pattern) || [];
88
+ }
89
+
90
+ function wordStateToUserDictElement(state: WordState): UserDictElement {
91
+ return {
92
+ uuid: state.uuid,
93
+ word: {
94
+ surface: state.surface,
95
+ pronunciation: state.pronunciation,
96
+ accent_type:
97
+ state.accentIndex === state.moraToneList.length - 1
98
+ ? 0
99
+ : state.accentIndex + 1,
100
+ priority: state.priority,
101
+ },
102
+ };
103
+ }
104
+
105
+ function userDictElementToWordState(elem: UserDictElement): WordState {
106
+ const moraToneList = wordToMoraToneList(elem.word);
107
+ return {
108
+ uuid: elem.uuid,
109
+ surface: elem.word.surface,
110
+ pronunciation: elem.word.pronunciation,
111
+ moraToneList,
112
+ accentIndex:
113
+ elem.word.accent_type === 0
114
+ ? moraToneList.length - 1 // 助詞「ガ」を除く
115
+ : elem.word.accent_type - 1,
116
+ priority: elem.word.priority,
117
+ };
118
+ }
119
+
120
+ function wordToMoraToneList(word: UserDictWord): MoraTone[] {
121
+ // 最後に助詞「ガ」が追加されたものを使用する
122
+ const moraToneList: MoraTone[] = [];
123
+ const morae = extractMorae(word.pronunciation);
124
+ const accentIndex =
125
+ word.accent_type === 0 ? morae.length : word.accent_type - 1;
126
+ for (let i = 0; i < morae.length; i++) {
127
+ const mora = morae[i];
128
+ const tone =
129
+ i === 0 && word.accent_type === 1 ? 1 : i > 0 && i <= accentIndex ? 1 : 0;
130
+ moraToneList.push({ mora, tone });
131
+ }
132
+ moraToneList.push({ mora: 'ガ', tone: word.accent_type === 0 ? 1 : 0 });
133
+ return moraToneList;
134
+ }
135
+
136
+ export interface DictionaryDialogProps {
137
+ open: boolean;
138
+ onClose: () => void;
139
+ }
140
+
141
+ const marks = [
142
+ {
143
+ value: 0,
144
+ label: '最低',
145
+ },
146
+ {
147
+ value: 3,
148
+ label: '低',
149
+ },
150
+ {
151
+ value: 5,
152
+ label: '標準',
153
+ },
154
+ {
155
+ value: 7,
156
+ label: '高',
157
+ },
158
+ {
159
+ value: 10,
160
+ label: '最高',
161
+ },
162
+ ];
163
+
164
+ export default function DictionaryDialog({
165
+ open,
166
+ onClose,
167
+ }: DictionaryDialogProps) {
168
+ const [isNew, setIsNew] = useState(true);
169
+
170
+ // 現在の辞書の情報
171
+ const [dict, setDict] = useState<UserDict>({});
172
+
173
+ // 右側に表示する単語の情報
174
+ const [wordState, setWordState] = useState<WordState>(defaultWordState);
175
+ const { surface, pronunciation, moraToneList, accentIndex, priority } =
176
+ wordState;
177
+
178
+ const [pronunciationError, setPronunciationError] = useState(false);
179
+
180
+ const [fetched, setFetched] = useState(false);
181
+
182
+ const { openPopup } = usePopup();
183
+
184
+ useEffect(() => {
185
+ const fetchDict = async () => {
186
+ const res = await fetchApi<UserDict>('/user_dict');
187
+ setDict(res);
188
+ };
189
+ fetchDict();
190
+ }, []);
191
+
192
+ const handleNewItemClick = () => {
193
+ setIsNew(true);
194
+ setWordState(defaultWordState);
195
+ setFetched(false);
196
+ };
197
+
198
+ // 単語を正規化、読みに入力された文字からg2pを叩いてモーラを取得
199
+ // 音声合成や学習には正規化された文字列が使われるため、辞書でもそれを登録する必要がある
200
+ const fetchNormMora = async () => {
201
+ setPronunciationError(false);
202
+
203
+ // 単語を正規化。`/normalize`を叩く
204
+ const fetchedSurface = await fetchApi<string>('/normalize', {
205
+ method: 'POST',
206
+ body: JSON.stringify({ text: surface }),
207
+ }).catch((e) => {
208
+ console.error(e);
209
+ openPopup('正規化に失敗しました', 'error');
210
+ return surface;
211
+ });
212
+
213
+ const fetchedMoraTone = await fetchApi<MoraTone[]>('/g2p', {
214
+ method: 'POST',
215
+ body: JSON.stringify({ text: pronunciation + 'が' }),
216
+ }).catch((e) => {
217
+ console.error(e);
218
+ setPronunciationError(true);
219
+ return [];
220
+ });
221
+ // アクセント情報は使わず(アクセント核を1文字目に設定)、読み情報だけを更新する
222
+ const newMoraTone: MoraTone[] = fetchedMoraTone.map((moraTone, index) => {
223
+ return { ...moraTone, tone: index === 0 ? 1 : 0 };
224
+ });
225
+ // Remove last 'が' and join mora
226
+ const newPronunciation = newMoraTone
227
+ .slice(0, -1)
228
+ .map((moraTone) => moraTone.mora)
229
+ .join('');
230
+ setWordState({
231
+ ...wordState,
232
+ surface: fetchedSurface,
233
+ moraToneList: newMoraTone,
234
+ accentIndex: 0,
235
+ pronunciation: newPronunciation,
236
+ });
237
+ setFetched(true);
238
+ };
239
+
240
+ const handleAccentIndexChange = (newAccentIndex: number) => {
241
+ const newMoraToneList: MoraTone[] = moraToneList.map((moraTone, index) => ({
242
+ ...moraTone,
243
+ tone:
244
+ newAccentIndex === 0
245
+ ? index === 0
246
+ ? 1
247
+ : 0
248
+ : index === 0
249
+ ? 0
250
+ : index <= newAccentIndex
251
+ ? 1
252
+ : 0,
253
+ }));
254
+ setWordState({
255
+ ...wordState,
256
+ accentIndex: newAccentIndex,
257
+ moraToneList: newMoraToneList,
258
+ });
259
+ };
260
+
261
+ const handleRegister = async () => {
262
+ if (!surface || !pronunciation) {
263
+ openPopup('単語と読みを入力してください', 'error');
264
+ return;
265
+ }
266
+ const elem = wordStateToUserDictElement(wordState);
267
+ const res = await fetchApi<{ uuid: string }>('/user_dict_word', {
268
+ method: 'POST',
269
+ body: JSON.stringify(elem.word),
270
+ }).catch((e) => {
271
+ openPopup(`登録に失敗しました: ${e}`, 'error');
272
+ });
273
+ if (!res) return;
274
+ openPopup('登録しました', 'success', 3000);
275
+ setDict({
276
+ ...dict,
277
+ [res.uuid]: elem.word,
278
+ });
279
+ setWordState({ ...wordState, uuid: res.uuid });
280
+ setIsNew(false);
281
+ };
282
+
283
+ const handleUpdate = async () => {
284
+ if (!surface || !pronunciation) {
285
+ openPopup('単語と読みを入力してください', 'error');
286
+ return;
287
+ }
288
+ const elem = wordStateToUserDictElement(wordState);
289
+ const res = await fetchApi<{ uuid: string }>(
290
+ `/user_dict_word/${elem.uuid}`,
291
+ {
292
+ method: 'PUT',
293
+ body: JSON.stringify(elem.word),
294
+ },
295
+ ).catch((e) => {
296
+ openPopup(`更新に失敗しました: ${e}`, 'error');
297
+ });
298
+ if (!res) return;
299
+ openPopup('更新しました', 'success', 3000);
300
+ setDict({
301
+ ...dict,
302
+ [elem.uuid]: elem.word,
303
+ });
304
+ };
305
+
306
+ const handleDelete = async () => {
307
+ const elem = wordStateToUserDictElement(wordState);
308
+ const res = await fetchApi<{ uuid: number }>(
309
+ `/user_dict_word/${elem.uuid}`,
310
+ {
311
+ method: 'DELETE',
312
+ },
313
+ ).catch((e) => {
314
+ openPopup(`削除に失敗しました: ${e}`, 'error');
315
+ });
316
+ if (!res) return;
317
+ openPopup('削除しました', 'success', 3000);
318
+ const newDict = { ...dict };
319
+ delete newDict[elem.uuid];
320
+ setDict(newDict);
321
+ setWordState(defaultWordState);
322
+ setIsNew(true);
323
+ };
324
+
325
+ const handleClose = () => {
326
+ onClose();
327
+ setWordState(defaultWordState);
328
+ setFetched(false);
329
+ setPronunciationError(false);
330
+ setIsNew(true);
331
+ };
332
+
333
+ return (
334
+ <Dialog onClose={onClose} open={open} fullWidth maxWidth='md'>
335
+ <DialogTitle>ユーザー辞書</DialogTitle>
336
+ <Box display='flex' justifyContent='space-between' height={600}>
337
+ <Box
338
+ minWidth={200}
339
+ pb={2}
340
+ px={2}
341
+ border={1}
342
+ borderColor='divider'
343
+ borderRadius={1}
344
+ >
345
+ <List>
346
+ <ListItem disablePadding>
347
+ <ListItemButton
348
+ onClick={handleNewItemClick}
349
+ selected={isNew}
350
+ sx={{ justifyContent: 'center' }}
351
+ >
352
+ <ListItemIcon>
353
+ <AddIcon />
354
+ </ListItemIcon>
355
+ <ListItemText primary='新規登録' />
356
+ </ListItemButton>
357
+ </ListItem>
358
+ </List>
359
+ <Divider />
360
+ <List sx={{ overflow: 'auto', height: '90%' }}>
361
+ {Object.keys(dict).map((key) => (
362
+ <ListItem key={key} disablePadding>
363
+ <ListItemButton
364
+ onClick={() => {
365
+ console.log(wordToMoraToneList(dict[key]));
366
+ setWordState(
367
+ userDictElementToWordState({
368
+ uuid: key,
369
+ word: dict[key],
370
+ }),
371
+ );
372
+ console.log(
373
+ userDictElementToWordState({
374
+ uuid: key,
375
+ word: dict[key],
376
+ }),
377
+ );
378
+ setIsNew(false);
379
+ }}
380
+ selected={key === wordState.uuid}
381
+ >
382
+ <ListItemText primary={dict[key].surface} />
383
+ </ListItemButton>
384
+ </ListItem>
385
+ ))}
386
+ </List>
387
+ {/* <Fab
388
+ color='primary'
389
+ // size='small'
390
+ // variant='extended'
391
+ sx={{ position: 'absolute', top: 0, right: 0 }}
392
+ onClick={handleNewItemClick}
393
+ >
394
+ <AddIcon />
395
+ </Fab> */}
396
+ </Box>
397
+ <Box sx={{ flexGrow: 1 }}>
398
+ <DialogContent>
399
+ <TextField
400
+ autoFocus
401
+ required
402
+ label='単語(名詞)(自動的に正規化される)'
403
+ fullWidth
404
+ value={surface}
405
+ onChange={(e) =>
406
+ setWordState({ ...wordState, surface: e.target.value })
407
+ }
408
+ sx={{ mb: 2 }}
409
+ />
410
+ <Grid
411
+ container
412
+ spacing={2}
413
+ justifyContent='center'
414
+ alignItems='center'
415
+ >
416
+ <Grid xs={9}>
417
+ <TextField
418
+ required
419
+ label='読み(平仮名かカタカナ)'
420
+ error={pronunciationError}
421
+ helperText={
422
+ pronunciationError
423
+ ? '平仮名かカタカナのみで入力してください'
424
+ : ''
425
+ }
426
+ fullWidth
427
+ value={pronunciation}
428
+ onChange={(e) => {
429
+ setWordState({
430
+ ...wordState,
431
+ pronunciation: e.target.value,
432
+ });
433
+ setFetched(false);
434
+ }}
435
+ sx={{ mb: 2 }}
436
+ onKeyDown={(e) => {
437
+ if (e.key === 'Enter') {
438
+ fetchNormMora();
439
+ }
440
+ }}
441
+ />
442
+ </Grid>
443
+ <Grid xs>
444
+ <Button
445
+ color='primary'
446
+ variant='outlined'
447
+ onClick={fetchNormMora}
448
+ startIcon={<RefreshIcon />}
449
+ sx={{ mb: 2 }}
450
+ fullWidth
451
+ >
452
+ 情報取得
453
+ </Button>
454
+ </Grid>
455
+ </Grid>
456
+ <Stack>
457
+ <FormControl>
458
+ <FormLabel>
459
+ アクセント位置(最後に助詞「が」が追加されています)
460
+ </FormLabel>
461
+ <RadioGroup
462
+ row
463
+ value={accentIndex}
464
+ onChange={(e) =>
465
+ handleAccentIndexChange(Number(e.target.value))
466
+ }
467
+ sx={{ flexWrap: 'nowrap', overflow: 'auto' }}
468
+ >
469
+ {moraToneList.map((moraTone, index) => (
470
+ <FormControlLabel
471
+ key={index}
472
+ value={index}
473
+ control={<Radio />}
474
+ label={moraTone.mora}
475
+ labelPlacement='bottom'
476
+ sx={{ mx: 0 }}
477
+ />
478
+ ))}
479
+ </RadioGroup>
480
+ </FormControl>
481
+ <AccentEditor moraToneList={moraToneList} disabled />
482
+ <Typography sx={{ mt: 1 }}>優先度</Typography>
483
+ <Box sx={{ textAlign: 'center' }}>
484
+ <Slider
485
+ value={priority}
486
+ onChange={(_, newValue) => {
487
+ setWordState({
488
+ ...wordState,
489
+ priority: newValue as number,
490
+ });
491
+ }}
492
+ marks={marks}
493
+ step={1}
494
+ min={0}
495
+ max={10}
496
+ sx={{
497
+ mt: 2,
498
+ width: '80%',
499
+ }}
500
+ />
501
+ </Box>
502
+ </Stack>
503
+ </DialogContent>
504
+ <Stack direction='row' spacing={2} justifyContent='space-around'>
505
+ {isNew && (
506
+ <Button
507
+ type='submit'
508
+ variant='outlined'
509
+ onClick={handleRegister}
510
+ disabled={!fetched}
511
+ >
512
+ 登録
513
+ </Button>
514
+ )}
515
+ {!isNew && (
516
+ <>
517
+ <Button
518
+ type='submit'
519
+ variant='outlined'
520
+ onClick={handleUpdate}
521
+ startIcon={<RefreshIcon />}
522
+ >
523
+ 更新
524
+ </Button>
525
+ <Button
526
+ type='submit'
527
+ variant='outlined'
528
+ onClick={handleDelete}
529
+ startIcon={<DeleteIcon />}
530
+ >
531
+ 削除
532
+ </Button>
533
+ </>
534
+ )}
535
+ <Button onClick={handleClose}>閉じる</Button>
536
+ </Stack>
537
+ </Box>
538
+ </Box>
539
+ </Dialog>
540
+ );
541
+ }
frontend/src/components/EditorContainer.tsx ADDED
@@ -0,0 +1,668 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // TODO: コンポーネントを分割してリファクタリングする
2
+ import AddCircleRoundedIcon from '@mui/icons-material/AddCircleRounded';
3
+ import ChevronRightIcon from '@mui/icons-material/ChevronRight';
4
+ import DeleteIcon from '@mui/icons-material/Delete';
5
+ import MenuIcon from '@mui/icons-material/Menu';
6
+ import {
7
+ AppBar,
8
+ Box,
9
+ Button,
10
+ CircularProgress,
11
+ Divider,
12
+ IconButton,
13
+ Menu,
14
+ MenuItem,
15
+ Paper,
16
+ TextField,
17
+ Toolbar,
18
+ Typography,
19
+ } from '@mui/material';
20
+ import Grid from '@mui/material/Unstable_Grid2'; // Grid version 2
21
+ import { saveAs } from 'file-saver';
22
+ import { useEffect, useRef, useState } from 'react';
23
+
24
+ import AccentEditor from '@/components/AccentEditor';
25
+ import { usePopup } from '@/contexts/PopupProvider';
26
+ import useWindowSize from '@/hooks/useWindowSize';
27
+ import { fetchApi } from '@/utils/api';
28
+
29
+ import type { MoraTone } from './AccentEditor';
30
+ import DictionaryDialog from './DictionaryDialog';
31
+ import LineSetting from './LineSetting';
32
+ import SimpleBackdrop from './SimpleBackdrop';
33
+ import TermOfUseDialog from './TermOfUse';
34
+
35
+ export interface ModelInfo {
36
+ name: string;
37
+ files: string[];
38
+ styles: string[];
39
+ speakers: string[];
40
+ }
41
+
42
+ export interface LineState {
43
+ text: string;
44
+ model: string;
45
+ modelFile: string;
46
+ speaker: string;
47
+ style: string;
48
+ moraToneList: MoraTone[];
49
+ accentModified: boolean;
50
+ styleWeight: number;
51
+ speed: number;
52
+ sdpRatio: number;
53
+ noise: number;
54
+ noisew: number;
55
+ pitchScale: number;
56
+ intonationScale: number;
57
+ silenceAfter: number;
58
+ }
59
+
60
+ export const defaultLineState: LineState = {
61
+ text: '',
62
+ model: '',
63
+ modelFile: '',
64
+ style: '', // fetch前はNeutralがないので空文字列にする
65
+ speaker: '',
66
+ moraToneList: [],
67
+ accentModified: false,
68
+ styleWeight: 1,
69
+ speed: 1,
70
+ sdpRatio: 0.2,
71
+ noise: 0.6,
72
+ noisew: 0.8,
73
+ pitchScale: 1,
74
+ intonationScale: 1,
75
+ silenceAfter: 0.5,
76
+ };
77
+
78
+ // Validation
79
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
80
+ function isMoraTone(data: any): data is MoraTone {
81
+ return typeof data.mora === 'string' && (data.tone === 0 || data.tone === 1);
82
+ }
83
+
84
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
85
+ function isLineState(data: any): data is LineState {
86
+ return (
87
+ typeof data.text === 'string' &&
88
+ typeof data.model === 'string' &&
89
+ typeof data.modelFile === 'string' &&
90
+ typeof data.style === 'string' &&
91
+ Array.isArray(data.moraToneList) &&
92
+ data.moraToneList.every(isMoraTone) &&
93
+ typeof data.accentModified === 'boolean' &&
94
+ typeof data.styleWeight === 'number' &&
95
+ typeof data.speed === 'number' &&
96
+ typeof data.sdpRatio === 'number' &&
97
+ typeof data.noise === 'number' &&
98
+ typeof data.noisew === 'number' &&
99
+ typeof data.silenceAfter === 'number' &&
100
+ typeof data.pitchScale === 'number' &&
101
+ typeof data.intonationScale === 'number'
102
+ );
103
+ }
104
+
105
+ export default function EditorContainer() {
106
+ const [modelList, setModelList] = useState<ModelInfo[]>([]);
107
+
108
+ const [lines, setLines] = useState<LineState[]>([defaultLineState]);
109
+ const [currentLineIndex, setCurrentLineIndex] = useState(0);
110
+
111
+ const { openPopup } = usePopup();
112
+
113
+ const [audioUrl, setAudioUrl] = useState('');
114
+ const [openBackdrop, setOpenBackdrop] = useState(false);
115
+ const [loading, setLoading] = useState(false);
116
+ const [dictOpen, setDictOpen] = useState(false);
117
+ const [termOfUseOpen, setTermOfUseOpen] = useState(false);
118
+
119
+ const [version, setVersion] = useState('');
120
+
121
+ const { height } = useWindowSize();
122
+
123
+ const [composing, setComposition] = useState(false);
124
+ const startComposition = () => setComposition(true);
125
+ const endComposition = () => setComposition(false);
126
+ const refs = useRef<HTMLTextAreaElement[]>([]);
127
+
128
+ useEffect(() => {
129
+ // 初期のモデル情報取得
130
+ setOpenBackdrop(true);
131
+
132
+ let timeoutId: NodeJS.Timeout;
133
+ let retryCount = 0;
134
+ const maxRetries = 10;
135
+ const retryInterval = 1000; // 1秒
136
+
137
+ const fetchModelInfo = () => {
138
+ fetchApi<ModelInfo[]>('/models_info')
139
+ .then((data) => {
140
+ setModelList(data);
141
+ setLines([
142
+ {
143
+ ...defaultLineState,
144
+ model: data[0].name || '',
145
+ modelFile: data[0].files[0] || '',
146
+ style: data[0].styles[0] || '',
147
+ speaker: data[0].speakers?.[0] || '',
148
+ },
149
+ ]);
150
+ setOpenBackdrop(false);
151
+ })
152
+ .catch((e) => {
153
+ if (retryCount < maxRetries) {
154
+ console.log(e);
155
+ retryCount++;
156
+ console.log(
157
+ `モデル情報の取得に失敗しました。リトライします。${retryCount}回目...`,
158
+ );
159
+ timeoutId = setTimeout(fetchModelInfo, retryInterval);
160
+ } else {
161
+ console.log(
162
+ `モデル情報の取得に失敗しました。リトライ回数: ${retryCount}`,
163
+ );
164
+ console.log(e);
165
+ openPopup(`モデ���情報の取得に失敗しました。\n${e}`, 'error');
166
+ setOpenBackdrop(false);
167
+ }
168
+ });
169
+ };
170
+
171
+ fetchModelInfo();
172
+
173
+ return () => {
174
+ if (timeoutId) {
175
+ clearTimeout(timeoutId);
176
+ }
177
+ };
178
+ }, [openPopup]);
179
+
180
+ useEffect(() => {
181
+ // 初回のバージョン取得
182
+ let retryCount = 0;
183
+ const maxRetries = 10;
184
+ const retryInterval = 1000; // 1秒
185
+ let timeoutId: NodeJS.Timeout;
186
+
187
+ const fetchVersion = () => {
188
+ fetchApi<string>('/version')
189
+ .then((data) => {
190
+ setVersion(data);
191
+ })
192
+ .catch((e) => {
193
+ if (retryCount < maxRetries) {
194
+ console.log(e);
195
+ retryCount++;
196
+ console.log(
197
+ `バージョン情報の取得に失敗しました。リトライします。${retryCount}回目...`,
198
+ );
199
+ timeoutId = setTimeout(fetchVersion, retryInterval);
200
+ } else {
201
+ console.log(
202
+ `バージョン情報の取得に失敗しました。リトライ回数: ${retryCount}`,
203
+ );
204
+ console.log(e);
205
+ }
206
+ });
207
+ };
208
+
209
+ fetchVersion();
210
+
211
+ return () => {
212
+ if (timeoutId) {
213
+ clearTimeout(timeoutId);
214
+ }
215
+ };
216
+ }, []);
217
+
218
+ useEffect(() => {
219
+ console.log('currentLineIndex:', currentLineIndex);
220
+ // 現在の行にフォーカスを設定
221
+ if (refs.current[currentLineIndex]) {
222
+ refs.current[currentLineIndex].focus();
223
+ }
224
+ }, [currentLineIndex]); // currentLineIndex が変更された時に実行
225
+
226
+ const handleRefresh = () => {
227
+ // TODO: 初期読み込み時とだいたい同じ処理なので共通化する
228
+ handleMenuClose();
229
+ setOpenBackdrop(true);
230
+ fetchApi<ModelInfo[]>('/models_info')
231
+ .then((data) => {
232
+ setModelList(data);
233
+ setOpenBackdrop(false);
234
+ })
235
+ .catch((e) => {
236
+ console.error(e);
237
+ openPopup(`モデル情報の取得に失敗しました。\n${e}`, 'error');
238
+ setOpenBackdrop(false);
239
+ });
240
+ };
241
+
242
+ const addLineAt = (index: number) => {
243
+ const newLine = {
244
+ ...lines[index],
245
+ text: '',
246
+ moraToneList: [],
247
+ accentModified: false,
248
+ };
249
+
250
+ const newLines = [...lines];
251
+ newLines.splice(index + 1, 0, newLine);
252
+
253
+ setLines(newLines);
254
+ setCurrentLineIndex(index + 1);
255
+ };
256
+
257
+ const setLineState = (newState: Partial<LineState>) => {
258
+ const newLines = lines.map((line, index) => {
259
+ if (index === currentLineIndex) {
260
+ return {
261
+ ...line,
262
+ ...newState,
263
+ };
264
+ }
265
+ return line;
266
+ });
267
+ setLines(newLines);
268
+ };
269
+
270
+ const fetchMoraTonePromise = async (): Promise<MoraTone[]> => {
271
+ return fetchApi<MoraTone[]>('/g2p', {
272
+ method: 'POST',
273
+ body: JSON.stringify({ text: lines[currentLineIndex].text }),
274
+ });
275
+ };
276
+
277
+ const handleTextChange = (newText: string) => {
278
+ setLineState({ text: newText, moraToneList: [], accentModified: false });
279
+ };
280
+
281
+ const handleSynthesis = async () => {
282
+ setLoading(true);
283
+ const newMoraToneList = lines[currentLineIndex].accentModified
284
+ ? lines[currentLineIndex].moraToneList
285
+ : await fetchMoraTonePromise();
286
+ setLineState({ moraToneList: newMoraToneList });
287
+ const newLine = {
288
+ ...lines[currentLineIndex],
289
+ moraToneList: newMoraToneList,
290
+ };
291
+ await fetchApi<Blob>(
292
+ '/synthesis',
293
+ {
294
+ method: 'POST',
295
+ body: JSON.stringify(newLine),
296
+ },
297
+ 'blob',
298
+ )
299
+ .then((data) => {
300
+ const newAudioUrl = URL.createObjectURL(data);
301
+ setAudioUrl(newAudioUrl);
302
+ })
303
+ .catch((e) => {
304
+ console.error(e);
305
+ openPopup(`音声合成に失敗しました。${e}`, 'error');
306
+ })
307
+ .finally(() => {
308
+ setLoading(false);
309
+ });
310
+ };
311
+
312
+ const handleMultiSynthesis = async () => {
313
+ setLoading(true);
314
+ // すべてのテキストに対して未取得な読み・アクセント情報を取得
315
+ const newMoraToneList = await Promise.all(
316
+ lines.map(async (line) => {
317
+ if (line.accentModified) {
318
+ return line.moraToneList;
319
+ }
320
+ return await fetchApi<MoraTone[]>('/g2p', {
321
+ method: 'POST',
322
+ body: JSON.stringify({ text: line.text }),
323
+ });
324
+ }),
325
+ );
326
+ const newLines = lines.map((line, index) => ({
327
+ ...line,
328
+ moraToneList: newMoraToneList[index],
329
+ }));
330
+ setLines(newLines);
331
+
332
+ await fetchApi<Blob>(
333
+ '/multi_synthesis',
334
+ {
335
+ method: 'POST',
336
+ body: JSON.stringify({
337
+ lines: newLines,
338
+ }),
339
+ },
340
+ 'blob',
341
+ )
342
+ .then((data) => {
343
+ const newAudioUrl = URL.createObjectURL(data);
344
+ setAudioUrl(newAudioUrl);
345
+ })
346
+ .catch((e) => {
347
+ console.error(e);
348
+ openPopup(`音声合成に失敗しました。${e}`, 'error');
349
+ })
350
+ .finally(() => {
351
+ setLoading(false);
352
+ });
353
+ };
354
+
355
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
356
+ if (e.key === 'Enter' && !composing) {
357
+ handleSynthesis();
358
+ } else if (e.key === 'ArrowDown') {
359
+ if (currentLineIndex < lines.length - 1) {
360
+ setCurrentLineIndex(currentLineIndex + 1);
361
+ } else {
362
+ addLineAt(currentLineIndex);
363
+ }
364
+ } else if (e.key === 'ArrowUp') {
365
+ if (currentLineIndex > 0) {
366
+ setCurrentLineIndex(currentLineIndex - 1);
367
+ }
368
+ }
369
+ };
370
+
371
+ const handleDelete = (lineIndex: number) => {
372
+ setLines([...lines.slice(0, lineIndex), ...lines.slice(lineIndex + 1)]);
373
+ if (lineIndex <= currentLineIndex && currentLineIndex > 0) {
374
+ setCurrentLineIndex(currentLineIndex - 1);
375
+ }
376
+ };
377
+
378
+ const handlePaste = (e: React.ClipboardEvent<HTMLDivElement>) => {
379
+ const text = e.clipboardData.getData('text');
380
+ if (text) {
381
+ const newTexts = text.split(/[\r\n]+/);
382
+ // 複数行のテキストがペーストされた場合は、改行で分けて新規行を挿入
383
+ // 単一行の場合はそのままペースト(選択されているテキストを置き換えて挿入する)
384
+ if (newTexts.length > 1) {
385
+ e.preventDefault();
386
+ // 改行で分けて、currentLineIndexの設定のまま新規行を間に挿入
387
+ const beforeLines = lines.slice(0, currentLineIndex);
388
+
389
+ const newLines = newTexts.map((newText, index) => {
390
+ if (index === 0) {
391
+ return {
392
+ ...lines[currentLineIndex],
393
+ text: lines[currentLineIndex].text + newText,
394
+ };
395
+ }
396
+ return { ...lines[currentLineIndex], text: newText };
397
+ });
398
+ const afterLines = lines.slice(currentLineIndex + 1);
399
+
400
+ const updatedLines = [...beforeLines, ...newLines, ...afterLines];
401
+ setLines(updatedLines);
402
+ setCurrentLineIndex(currentLineIndex + newTexts.length - 1);
403
+ }
404
+ }
405
+ };
406
+
407
+ const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
408
+
409
+ const handleMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
410
+ setAnchorEl(event.currentTarget);
411
+ };
412
+
413
+ const handleMenuClose = () => {
414
+ setAnchorEl(null);
415
+ };
416
+
417
+ const handleSave = () => {
418
+ const json = JSON.stringify(lines);
419
+ const blob = new Blob([json], { type: 'application/json' });
420
+ saveAs(blob, 'project.json');
421
+ handleMenuClose();
422
+ };
423
+
424
+ const handleLoad = (event: React.ChangeEvent<HTMLInputElement>) => {
425
+ handleMenuClose();
426
+ // FIXME: ファイル選択ダイアログが閉じるまでメニューが閉じない
427
+
428
+ const file = event.target.files?.[0];
429
+ if (!file) {
430
+ return;
431
+ }
432
+
433
+ const reader = new FileReader();
434
+
435
+ reader.onload = (e: ProgressEvent<FileReader>) => {
436
+ setCurrentLineIndex(0);
437
+ const content = e.target?.result;
438
+ if (typeof content === 'string') {
439
+ try {
440
+ const data: LineState[] = JSON.parse(content);
441
+ if (!Array.isArray(data) || !data.every(isLineState)) {
442
+ console.error('データがLineState[]型と一致しません。');
443
+ openPopup('データが有効な形式ではありません。', 'error');
444
+ return;
445
+ }
446
+ setLines(data);
447
+ } catch (e) {
448
+ console.error(e);
449
+ openPopup(`プロジェクトの読み込みに失敗しました。${e}`, 'error');
450
+ }
451
+ } else {
452
+ console.error('typeof content', typeof content);
453
+ openPopup('ファイルの読み込みに失敗しました。', 'error');
454
+ }
455
+ };
456
+
457
+ reader.readAsText(file);
458
+ };
459
+
460
+ return (
461
+ <>
462
+ <AppBar position='static'>
463
+ <Toolbar>
464
+ <Button
465
+ onClick={handleMenuOpen}
466
+ color='inherit'
467
+ startIcon={<MenuIcon />}
468
+ >
469
+ メニュー
470
+ </Button>
471
+
472
+ <Typography variant='h6' sx={{ flexGrow: 3, textAlign: 'center' }}>
473
+ サンプルズの音声生成ツール(Style-Bert-VITS2 エディター)
474
+ </Typography>
475
+ <Typography variant='subtitle1'>
476
+ SBV2 ver: {version}, editor ver: fablio 0.1.0
477
+ </Typography>
478
+ <Menu
479
+ anchorEl={anchorEl}
480
+ open={Boolean(anchorEl)}
481
+ onClose={handleMenuClose}
482
+ >
483
+ <MenuItem onClick={handleRefresh}>モデル情報をリロード</MenuItem>
484
+ <Divider />
485
+ <MenuItem onClick={handleSave}>プロジェクトの保存</MenuItem>
486
+ <MenuItem component='label'>
487
+ プロジェクトの読み込み
488
+ <input type='file' onChange={handleLoad} hidden accept='.json' />
489
+ </MenuItem>
490
+ <Divider />
491
+ <MenuItem
492
+ onClick={() => {
493
+ setDictOpen(true);
494
+ handleMenuClose();
495
+ }}
496
+ >
497
+ ユーザー辞書の編集
498
+ </MenuItem>
499
+ <Divider />
500
+ <MenuItem
501
+ onClick={() => {
502
+ setTermOfUseOpen(true);
503
+ handleMenuClose();
504
+ }}
505
+ >
506
+ 利用規約
507
+ </MenuItem>
508
+ </Menu>
509
+ </Toolbar>
510
+ </AppBar>
511
+ <Box display='flex' justifyContent='space-between' gap={2} mt={2}>
512
+ <Box flexGrow={1} width='100%' overflow='auto'>
513
+ <Paper
514
+ sx={{ p: 2, height: height / 2, overflow: 'auto' }}
515
+ elevation={2}
516
+ >
517
+ {lines.map((line, index) => (
518
+ <Grid
519
+ container
520
+ key={index}
521
+ spacing={1}
522
+ mt={2}
523
+ alignItems='center'
524
+ justifyContent='space-between'
525
+ sx={{
526
+ // ホバーしたときに削除ボタンを表示
527
+ '& .delete-button': {
528
+ display: 'none',
529
+ },
530
+ '&:hover .delete-button': {
531
+ display: 'block',
532
+ },
533
+ '& .add-line-button': {
534
+ display: 'none',
535
+ },
536
+ '&:hover .add-line-button': {
537
+ display: 'block',
538
+ },
539
+ }}
540
+ >
541
+ <Grid xs='auto'>
542
+ <ChevronRightIcon
543
+ fontSize='small'
544
+ sx={{
545
+ display: currentLineIndex === index ? 'block' : 'none',
546
+ }}
547
+ />
548
+ </Grid>
549
+ <Grid xs>
550
+ <TextField
551
+ label={`テキスト${index + 1}`}
552
+ fullWidth
553
+ value={line.text}
554
+ onFocus={() => setCurrentLineIndex(index)}
555
+ onChange={(e) => handleTextChange(e.target.value)}
556
+ onKeyDown={handleKeyDown}
557
+ onCompositionStart={startComposition}
558
+ onCompositionEnd={endComposition}
559
+ focused={currentLineIndex === index}
560
+ onPaste={handlePaste}
561
+ // inputRef={(input) => {
562
+ // if (input && currentLineIndex === index) {
563
+ // input.focus();
564
+ // }
565
+ // }}
566
+ inputRef={(el) => (refs.current[index] = el)} // 各行の ref を保存
567
+ />
568
+ </Grid>
569
+ <Grid xs='auto'>
570
+ <IconButton
571
+ className='delete-button'
572
+ disabled={lines.length === 1}
573
+ onClick={() => handleDelete(index)}
574
+ title='この行を削除する'
575
+ >
576
+ <DeleteIcon />
577
+ </IconButton>
578
+ </Grid>
579
+ <Grid xs='auto'>
580
+ <IconButton
581
+ className='add-line-button'
582
+ onClick={() => addLineAt(index)}
583
+ title='行を追加する'
584
+ >
585
+ <AddCircleRoundedIcon />
586
+ </IconButton>
587
+ </Grid>
588
+ </Grid>
589
+ ))}
590
+ </Paper>
591
+ <AccentEditor
592
+ moraToneList={lines[currentLineIndex].moraToneList}
593
+ setMoraToneList={(moraToneList) =>
594
+ setLineState({ moraToneList, accentModified: true })
595
+ }
596
+ />
597
+ <Box
598
+ mt={2}
599
+ sx={{
600
+ position: 'relative',
601
+ display: 'flex',
602
+ justifyContent: 'center',
603
+ alignItems: 'center',
604
+ }}
605
+ >
606
+ <Button
607
+ variant='contained'
608
+ color='primary'
609
+ disabled={loading}
610
+ onClick={handleSynthesis}
611
+ >
612
+ 音声合成
613
+ </Button>
614
+ {loading && (
615
+ <CircularProgress
616
+ size={24}
617
+ sx={{
618
+ position: 'absolute',
619
+ }}
620
+ />
621
+ )}
622
+ </Box>
623
+ <Box
624
+ mt={2}
625
+ sx={{
626
+ position: 'relative',
627
+ display: 'flex',
628
+ justifyContent: 'center',
629
+ alignItems: 'center',
630
+ }}
631
+ >
632
+ <Button
633
+ variant='outlined'
634
+ color='primary'
635
+ disabled={loading}
636
+ onClick={handleMultiSynthesis}
637
+ >
638
+ 全てのテキストを音声合成
639
+ </Button>
640
+ {loading && (
641
+ <CircularProgress
642
+ size={24}
643
+ sx={{
644
+ position: 'absolute',
645
+ }}
646
+ />
647
+ )}
648
+ </Box>
649
+ {audioUrl && <audio src={audioUrl} controls autoPlay />}
650
+ </Box>
651
+ <Box width='30%' maxWidth={350} minWidth={200}>
652
+ <LineSetting
653
+ modelList={modelList}
654
+ lines={lines}
655
+ currentIndex={currentLineIndex}
656
+ setLines={setLines}
657
+ />
658
+ </Box>
659
+ </Box>
660
+ <SimpleBackdrop open={openBackdrop} />
661
+ <DictionaryDialog open={dictOpen} onClose={() => setDictOpen(false)} />
662
+ <TermOfUseDialog
663
+ open={termOfUseOpen}
664
+ onClose={() => setTermOfUseOpen(false)}
665
+ />
666
+ </>
667
+ );
668
+ }
frontend/src/components/LineSetting.tsx ADDED
@@ -0,0 +1,317 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import RefreshIcon from '@mui/icons-material/Refresh';
2
+ import {
3
+ Box,
4
+ FormControl,
5
+ IconButton,
6
+ Input,
7
+ InputLabel,
8
+ MenuItem,
9
+ Paper,
10
+ Select,
11
+ Slider,
12
+ Tooltip,
13
+ Typography,
14
+ } from '@mui/material';
15
+ import Grid from '@mui/material/Unstable_Grid2/Grid2';
16
+ import { useState } from 'react';
17
+
18
+ import type { LineState, ModelInfo } from './EditorContainer';
19
+ import { defaultLineState } from './EditorContainer';
20
+
21
+ interface InputSliderProps {
22
+ value: number;
23
+ setValue: (value: number) => void;
24
+ step: number;
25
+ min: number;
26
+ max: number;
27
+ label: string;
28
+ }
29
+
30
+ function InputSlider({
31
+ value,
32
+ setValue,
33
+ step,
34
+ min,
35
+ max,
36
+ label,
37
+ }: InputSliderProps) {
38
+ const handleSliderChange = (event: Event, newValue: number | number[]) => {
39
+ setValue(newValue as number);
40
+ };
41
+
42
+ const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
43
+ setValue(event.target.value === '' ? 0 : Number(event.target.value));
44
+ };
45
+
46
+ const handleBlur = () => {
47
+ if (value < min) {
48
+ setValue(min);
49
+ } else if (value > max) {
50
+ setValue(max);
51
+ }
52
+ };
53
+
54
+ return (
55
+ <Box>
56
+ <Grid
57
+ container
58
+ spacing={2}
59
+ alignItems='center'
60
+ justifyContent='space-between'
61
+ >
62
+ <Grid xs={9}>
63
+ <Typography id='input-slider' gutterBottom>
64
+ {label}
65
+ </Typography>
66
+ <Slider
67
+ value={typeof value === 'number' ? value : min}
68
+ onChange={handleSliderChange}
69
+ aria-labelledby='input-slider'
70
+ step={step}
71
+ min={min}
72
+ max={max}
73
+ />
74
+ </Grid>
75
+ <Grid xs={3}>
76
+ <Input
77
+ value={value}
78
+ onChange={handleInputChange}
79
+ onBlur={handleBlur}
80
+ inputProps={{
81
+ step,
82
+ min,
83
+ max,
84
+ type: 'number',
85
+ }}
86
+ />
87
+ </Grid>
88
+ </Grid>
89
+ </Box>
90
+ );
91
+ }
92
+
93
+ interface LineSettingProps {
94
+ modelList: ModelInfo[];
95
+ lines: LineState[];
96
+ setLines: (lines: LineState[]) => void;
97
+ currentIndex: number;
98
+ }
99
+
100
+ export default function LineSetting({
101
+ modelList,
102
+ lines,
103
+ setLines,
104
+ currentIndex,
105
+ }: LineSettingProps) {
106
+ const [styleWeightUB, setStyleWeightUB] = useState(10);
107
+
108
+ const setLineState = (newState: Partial<LineState>) => {
109
+ const newLines = lines.map((line, index) => {
110
+ if (index === currentIndex) {
111
+ return {
112
+ ...line,
113
+ ...newState,
114
+ };
115
+ }
116
+ return line;
117
+ });
118
+ setLines(newLines);
119
+ };
120
+
121
+ const handleModelChange = (model: string) => {
122
+ const selected = modelList?.find((m) => m.name === model);
123
+ setLineState({
124
+ model,
125
+ modelFile: selected?.files[0] || '',
126
+ style: selected?.styles[0] || '',
127
+ speaker: selected?.speakers?.[0] || '',
128
+ });
129
+ };
130
+
131
+ const handleDefault = () => {
132
+ setLineState({
133
+ ...defaultLineState,
134
+ model: lines[currentIndex].model,
135
+ modelFile: lines[currentIndex].modelFile,
136
+ text: lines[currentIndex].text,
137
+ speaker: lines[currentIndex].speaker,
138
+ //もし選択肢にNeutralがあればそれを選択、あるはずだが一応
139
+ style: modelList
140
+ .find((model) => model.name === lines[currentIndex].model)
141
+ ?.styles.includes('Neutral')
142
+ ? 'Neutral'
143
+ : modelList.find((model) => model.name === lines[currentIndex].model)
144
+ ?.styles[0] || '',
145
+ });
146
+ };
147
+
148
+ return (
149
+ <Paper sx={{ p: 2 }}>
150
+ <Box
151
+ sx={{
152
+ display: 'flex',
153
+ justifyContent: 'space-between',
154
+ mb: 1,
155
+ alignItems: 'center',
156
+ }}
157
+ >
158
+ <Typography>テキスト{currentIndex + 1}の設定</Typography>
159
+ <Tooltip title='デフォルト設定に戻す' placement='left'>
160
+ <IconButton onClick={handleDefault}>
161
+ <RefreshIcon />
162
+ </IconButton>
163
+ </Tooltip>
164
+ </Box>
165
+ {/* Model Image Display */}
166
+ <Box sx={{ mb: 2, display: 'flex', justifyContent: 'center' }}>
167
+ <img
168
+ src={`/images/${lines[currentIndex].model}.png`}
169
+ alt={lines[currentIndex].model}
170
+ style={{
171
+ width: '250px',
172
+ height: '250px',
173
+ objectFit: 'cover',
174
+ borderRadius: '12px',
175
+ border: '1px solid rgba(0, 0, 0, 0.12)',
176
+ }}
177
+ onError={(e) => {
178
+ // Hide image if it doesn't exist
179
+ (e.target as HTMLImageElement).style.display = 'none';
180
+ }}
181
+ />
182
+ </Box>
183
+ <FormControl fullWidth variant='standard' sx={{ mb: 1, minWidth: 120 }}>
184
+ <InputLabel>モデル</InputLabel>
185
+ <Select
186
+ value={lines[currentIndex].model}
187
+ onChange={(e) => handleModelChange(e.target.value as string)}
188
+ >
189
+ {modelList.map((modelInfo, index) => (
190
+ <MenuItem key={index} value={modelInfo.name}>
191
+ {modelInfo.name}
192
+ </MenuItem>
193
+ ))}
194
+ </Select>
195
+ </FormControl>
196
+ <FormControl fullWidth variant='standard' sx={{ mb: 1, minWidth: 120 }}>
197
+ <InputLabel>モデルファイル</InputLabel>
198
+ <Select
199
+ value={lines[currentIndex].modelFile}
200
+ onChange={(e) =>
201
+ setLineState({ modelFile: e.target.value as string })
202
+ }
203
+ >
204
+ {modelList
205
+ .find((speaker) => speaker.name === lines[currentIndex].model)
206
+ ?.files.map((file, index) => (
207
+ <MenuItem key={index} value={file}>
208
+ {file}
209
+ </MenuItem>
210
+ ))}
211
+ </Select>
212
+ </FormControl>
213
+ <FormControl fullWidth variant='standard' sx={{ mb: 1, minWidth: 120 }}>
214
+ <InputLabel>話者</InputLabel>
215
+ <Select
216
+ value={lines[currentIndex].speaker}
217
+ onChange={(e) => setLineState({ speaker: e.target.value as string })}
218
+ >
219
+ {modelList
220
+ .find((model) => model.name === lines[currentIndex].model)
221
+ ?.speakers?.map((model, index) => (
222
+ <MenuItem key={index} value={model}>
223
+ {model}
224
+ </MenuItem>
225
+ ))}
226
+ </Select>
227
+ </FormControl>
228
+ <FormControl fullWidth variant='standard' sx={{ mb: 2, minWidth: 120 }}>
229
+ <InputLabel>スタイル</InputLabel>
230
+ <Select
231
+ value={lines[currentIndex].style}
232
+ onChange={(e) => setLineState({ style: e.target.value as string })}
233
+ >
234
+ {modelList
235
+ .find((speaker) => speaker.name === lines[currentIndex].model)
236
+ ?.styles.map((style, index) => (
237
+ <MenuItem key={index} value={style}>
238
+ {style}
239
+ </MenuItem>
240
+ ))}
241
+ </Select>
242
+ </FormControl>
243
+ <InputSlider
244
+ label='スタイルの強さ上限設定'
245
+ value={styleWeightUB}
246
+ setValue={(value) => setStyleWeightUB(value)}
247
+ step={0.1}
248
+ min={1}
249
+ max={20}
250
+ />
251
+ <InputSlider
252
+ label='スタイルの強さ(崩壊したら下げて)'
253
+ value={lines[currentIndex].styleWeight}
254
+ setValue={(value) => setLineState({ styleWeight: value })}
255
+ step={0.1}
256
+ min={0}
257
+ max={styleWeightUB}
258
+ />
259
+ <InputSlider
260
+ label='話速'
261
+ value={lines[currentIndex].speed}
262
+ setValue={(value) => setLineState({ speed: value })}
263
+ step={0.05}
264
+ min={0.5}
265
+ max={2}
266
+ />
267
+ <InputSlider
268
+ label='テンポの緩急'
269
+ value={lines[currentIndex].sdpRatio}
270
+ setValue={(value) => setLineState({ sdpRatio: value })}
271
+ step={0.05}
272
+ min={0}
273
+ max={1}
274
+ />
275
+ <InputSlider
276
+ label='Noise'
277
+ value={lines[currentIndex].noise}
278
+ setValue={(value) => setLineState({ noise: value })}
279
+ step={0.05}
280
+ min={0}
281
+ max={1}
282
+ />
283
+ <InputSlider
284
+ label='NoiseW'
285
+ value={lines[currentIndex].noisew}
286
+ setValue={(value) => setLineState({ noisew: value })}
287
+ step={0.05}
288
+ min={0}
289
+ max={1}
290
+ />
291
+ <InputSlider
292
+ label='音高(1以外では音質劣化)'
293
+ value={lines[currentIndex].pitchScale}
294
+ setValue={(value) => setLineState({ pitchScale: value })}
295
+ step={0.05}
296
+ min={0.7}
297
+ max={1.3}
298
+ />
299
+ <InputSlider
300
+ label='抑揚(1以外では音質劣化)'
301
+ value={lines[currentIndex].intonationScale}
302
+ setValue={(value) => setLineState({ intonationScale: value })}
303
+ step={0.05}
304
+ min={0.7}
305
+ max={1.3}
306
+ />
307
+ <InputSlider
308
+ label='次のテキストとの間の無音'
309
+ value={lines[currentIndex].silenceAfter}
310
+ setValue={(value) => setLineState({ silenceAfter: value })}
311
+ step={0.05}
312
+ min={0}
313
+ max={1.5}
314
+ />
315
+ </Paper>
316
+ );
317
+ }
frontend/src/components/SimpleBackdrop.tsx ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Backdrop from '@mui/material/Backdrop';
2
+ import CircularProgress from '@mui/material/CircularProgress';
3
+
4
+ interface SimpleBackdropProps {
5
+ open: boolean;
6
+ }
7
+
8
+ export default function SimpleBackdrop({ open }: SimpleBackdropProps) {
9
+ return (
10
+ <Backdrop
11
+ sx={{ color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1 }}
12
+ open={open}
13
+ >
14
+ <CircularProgress color='inherit' />
15
+ </Backdrop>
16
+ );
17
+ }
frontend/src/components/TermOfUse.tsx ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ Box,
3
+ Button,
4
+ Dialog,
5
+ DialogActions,
6
+ DialogContent,
7
+ DialogTitle,
8
+ Divider,
9
+ Link,
10
+ Typography,
11
+ } from '@mui/material';
12
+
13
+ export interface TermOfUseDialogProps {
14
+ open: boolean;
15
+ onClose: () => void;
16
+ }
17
+
18
+ export default function TermOfUseDialog({
19
+ open,
20
+ onClose,
21
+ }: TermOfUseDialogProps) {
22
+ return (
23
+ <Dialog open={open} fullWidth maxWidth='md'>
24
+ <DialogTitle>お願いとモデルの利用規約</DialogTitle>
25
+ <DialogContent>
26
+ <Typography>
27
+ Style-Bert-VITS2を利用する際は、
28
+ <Link
29
+ href='https://github.com/litagin02/Style-Bert-VITS2/blob/master/docs/TERMS_OF_USE.md'
30
+ target='_blank'
31
+ rel='noopener noreferrer'
32
+ >
33
+ Style-Bert-VITS2の開発陣からのお願いとデフォルトモデルの利用規約
34
+ </Link>
35
+ を読んで、使用するモデルの利用規約を遵守してください。
36
+ 初期からあるデフォルトモデルの利用規約の抜粋は以下の通りです(完全な利用規約は上記リンクにあります)。
37
+ </Typography>
38
+ <Box sx={{ fontWeight: 'bold' }}>JVNV</Box>
39
+ <Typography>
40
+ <Link
41
+ href='https://huggingface.co/litagin/style_bert_vits2_jvnv'
42
+ target='_blank'
43
+ rel='noopener noreferrer'
44
+ >
45
+ 「jvnv-」から始まるモデル
46
+ </Link>
47
+ は、
48
+ <Link
49
+ href='https://sites.google.com/site/shinnosuketakamichi/research-topics/jvnv_corpus'
50
+ target='_blank'
51
+ rel='noopener noreferrer'
52
+ >
53
+ JVNVコーパス
54
+ </Link>
55
+ の音声で学習されました。このコーパスのライセンスは
56
+ <Link
57
+ href='https://creativecommons.org/licenses/by-sa/4.0/deed.ja'
58
+ target='_blank'
59
+ rel='noopener noreferrer'
60
+ >
61
+ CC BY-SA 4.0
62
+ </Link>
63
+ ですので、jvnv-で始まるモデルの利用規約はこれを継承します。
64
+ </Typography>
65
+ <Box sx={{ fontWeight: 'bold' }}>小春音アミ・あみたろ</Box>
66
+ <Typography>
67
+ 「koharune-ami / amitaro」モデルは、
68
+ <Link
69
+ href='https://amitaro.net/'
70
+ target='_blank'
71
+ rel='noopener noreferrer'
72
+ >
73
+ あみたろの声素材工房
74
+ </Link>
75
+ のコーパス音声・ライブ配信音声から許可を得て学習されました(Ver
76
+ 2.5.0で追加されたモデルです、アップデートした方は`Initialize.bat`ファイルをダブルクリックするとモデルのダウンロードができます)。利用の際には、
77
+ <Link
78
+ href='https://amitaro.net/voice/voice_rule/'
79
+ target='_blank'
80
+ rel='noopener noreferrer'
81
+ >
82
+ あみたろの声素材工房の規約
83
+ </Link>
84
+
85
+ <Link
86
+ href='https://amitaro.net/voice/livevoice/#index_id6'
87
+ target='_blank'
88
+ rel='noopener noreferrer'
89
+ >
90
+ あみたろのライブ配信音声・利用規約
91
+ </Link>
92
+ を遵守してください。
93
+ </Typography>
94
+ <Typography>
95
+ 特に、年齢制限がかかりそうなセリフやセンシティブな用途には使用できません。
96
+ </Typography>
97
+ <Typography>
98
+ 生成音声を公開する際は(媒体は問わない)、必ず分かりやすい場所に
99
+ 「あみたろの声素材工房 (https://amitaro.net/)」
100
+ の声を元にした音声モデルを使用していることが分かるようなクレジット表記を記載してください:
101
+ 「Style-BertVITS2モデル: 小春音アミ、あみたろの声素材工房
102
+ (https://amitaro.net/)」 「Style-BertVITS2モデル:
103
+ あみたろ、あみたろの声素材工房 (https://amitaro.net/)」
104
+ </Typography>
105
+ <Box sx={{ fontWeight: 'bold' }}>サンプルズ・仮うさぎ (kariusagi)</Box>
106
+ <Typography>
107
+ 「kariusagi」モデルは、
108
+ <Link
109
+ href='https://fablio.jp/characters/samples'
110
+ target='_blank'
111
+ rel='noopener noreferrer'
112
+ >
113
+ サンプルズの仮うさぎ
114
+ </Link>
115
+ の音声を使用して学習されました。利用の際には、
116
+ <Link
117
+ href='https://fablio.jp/characters/samples/rules'
118
+ target='_blank'
119
+ rel='noopener noreferrer'
120
+ >
121
+ サンプルズの利用規約
122
+ </Link>
123
+ を遵守してください。
124
+ </Typography>
125
+ <Typography>
126
+ 主な利用規約:
127
+ </Typography>
128
+ <Typography component="div">
129
+ <ul style={{ paddingLeft: '20px', margin: '8px 0' }}>
130
+ <li>サンプル目的(デモ、プロトタイプ、背景の一員、にぎやかし等)での法人・商用利用OK</li>
131
+ <li>個人や小規模企業(年間売上1000万円未満)による二次創作、配信、同人誌・グッズ制作も自由にOK</li>
132
+ <li>政治・宗教的な表現や他者を害する可能性のある用途は禁止</li>
133
+ <li>違法行為を推進する内容での使用は禁止</li>
134
+ <li>クレジット表記は不要(「© サンプルズ」の表記があると喜ばれます)</li>
135
+ </ul>
136
+ </Typography>
137
+ <Typography>
138
+ 完全なモデルの利用規約は
139
+ <Link
140
+ href='https://github.com/litagin02/Style-Bert-VITS2/blob/master/docs/TERMS_OF_USE.md'
141
+ target='_blank'
142
+ rel='noopener noreferrer'
143
+ >
144
+ Style-Bert-VITS2の利用規約
145
+ </Link>
146
+ をお読みください。
147
+ </Typography>
148
+ </DialogContent>
149
+ <Divider />
150
+ <DialogActions>
151
+ <Button onClick={onClose}>同意する</Button>
152
+ </DialogActions>
153
+ </Dialog>
154
+ );
155
+ }
frontend/src/components/TextEditor.tsx ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Box, TextField } from '@mui/material';
2
+
3
+ interface TextEditorProps {
4
+ text: string;
5
+ setText: (text: string) => void;
6
+ onKeyDown?: (e: React.KeyboardEvent<HTMLDivElement>) => void;
7
+ }
8
+
9
+ export default function TextEditor({
10
+ text,
11
+ setText,
12
+ onKeyDown,
13
+ }: TextEditorProps) {
14
+ return (
15
+ <Box mt={2}>
16
+ <TextField
17
+ label='Input text'
18
+ fullWidth
19
+ value={text}
20
+ onChange={(e) => setText(e.target.value)}
21
+ onKeyDown={onKeyDown}
22
+ />
23
+ </Box>
24
+ );
25
+ }
frontend/src/contexts/PopupProvider.tsx ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { AlertColor } from '@mui/material';
2
+ import React, { createContext, useCallback, useState } from 'react';
3
+
4
+ interface PopupContextType {
5
+ isOpen: boolean;
6
+ message: string;
7
+ severity: AlertColor;
8
+ duration: number | null;
9
+ openPopup: (message: string, severity: AlertColor, duration?: number) => void;
10
+ closePopup: () => void;
11
+ }
12
+
13
+ const PopupContext = createContext<PopupContextType>({
14
+ isOpen: false,
15
+ message: '',
16
+ severity: 'error',
17
+ duration: null,
18
+ openPopup: () => {},
19
+ closePopup: () => {},
20
+ });
21
+
22
+ interface DialogProviderProps {
23
+ children: React.ReactNode;
24
+ }
25
+
26
+ export default function PopupProvider({ children }: DialogProviderProps) {
27
+ const [isOpen, setIsOpen] = useState(false);
28
+ const [message, setMessage] = useState('');
29
+ const [severity, setSeverity] = useState<AlertColor>('error');
30
+ const [duration, setDuration] = useState<number | null>(null);
31
+
32
+ const openPopup = useCallback(
33
+ (message: string, severity?: AlertColor, duration?: number) => {
34
+ setIsOpen(true);
35
+ setMessage(message);
36
+ if (severity) {
37
+ setSeverity(severity);
38
+ } else {
39
+ setSeverity('error');
40
+ }
41
+ if (duration) {
42
+ setDuration(duration);
43
+ }
44
+ },
45
+ [],
46
+ );
47
+
48
+ const closePopup = useCallback(() => {
49
+ setIsOpen(false);
50
+ setMessage('');
51
+ setSeverity('error');
52
+ }, []);
53
+
54
+ return (
55
+ <PopupContext.Provider
56
+ value={{
57
+ isOpen,
58
+ message,
59
+ openPopup,
60
+ closePopup,
61
+ severity,
62
+ duration,
63
+ }}
64
+ >
65
+ {children}
66
+ </PopupContext.Provider>
67
+ );
68
+ }
69
+
70
+ export const usePopup = () => React.useContext(PopupContext);
frontend/src/hooks/useWindowSize.ts ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useLayoutEffect, useState } from 'react';
2
+
3
+ const useWindowSize = (): { width: number; height: number } => {
4
+ const [width, setWidth] = useState(0);
5
+ const [height, setHeight] = useState(0);
6
+ useLayoutEffect(() => {
7
+ const updateSize = (): void => {
8
+ setWidth(window.innerWidth);
9
+ setHeight(window.innerHeight);
10
+ };
11
+
12
+ window.addEventListener('resize', updateSize);
13
+ updateSize();
14
+
15
+ return () => window.removeEventListener('resize', updateSize);
16
+ }, []);
17
+ return { width, height };
18
+ };
19
+
20
+ export default useWindowSize;
frontend/src/theme.ts ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+ import { createTheme } from '@mui/material/styles';
3
+ import { Noto_Sans_JP } from 'next/font/google';
4
+
5
+ const notoSansJP = Noto_Sans_JP({
6
+ weight: ['300', '400', '500', '600', '700'],
7
+ subsets: ['latin'],
8
+ display: 'swap',
9
+ });
10
+
11
+ const theme = createTheme({
12
+ palette: {
13
+ mode: 'light',
14
+ primary: {
15
+ main: '#10b981', // Modern emerald green
16
+ light: '#34d399',
17
+ dark: '#047857',
18
+ contrastText: '#ffffff',
19
+ },
20
+ secondary: {
21
+ main: '#f59e0b', // Modern amber
22
+ light: '#fbbf24',
23
+ dark: '#d97706',
24
+ contrastText: '#ffffff',
25
+ },
26
+ background: {
27
+ default: '#f8fafc',
28
+ paper: '#ffffff',
29
+ },
30
+ text: {
31
+ primary: '#1e293b',
32
+ secondary: '#64748b',
33
+ },
34
+ },
35
+ typography: {
36
+ fontFamily: notoSansJP.style.fontFamily,
37
+ h6: {
38
+ fontWeight: 600,
39
+ fontSize: '1.125rem',
40
+ },
41
+ subtitle1: {
42
+ fontSize: '0.875rem',
43
+ fontWeight: 500,
44
+ },
45
+ button: {
46
+ textTransform: 'none',
47
+ fontWeight: 500,
48
+ },
49
+ },
50
+ shape: {
51
+ borderRadius: 12,
52
+ },
53
+ components: {
54
+ MuiAppBar: {
55
+ styleOverrides: {
56
+ root: {
57
+ backgroundColor: '#ffffff',
58
+ color: '#1e293b',
59
+ boxShadow: '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)',
60
+ borderBottom: '1px solid #e2e8f0',
61
+ },
62
+ },
63
+ },
64
+ MuiPaper: {
65
+ styleOverrides: {
66
+ root: {
67
+ boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)',
68
+ borderRadius: 12,
69
+ },
70
+ },
71
+ },
72
+ MuiButton: {
73
+ styleOverrides: {
74
+ root: {
75
+ borderRadius: 8,
76
+ paddingTop: 8,
77
+ paddingBottom: 8,
78
+ paddingLeft: 16,
79
+ paddingRight: 16,
80
+ },
81
+ contained: {
82
+ boxShadow: '0 2px 4px -1px rgb(0 0 0 / 0.2), 0 1px 2px -1px rgb(0 0 0 / 0.12)',
83
+ '&:hover': {
84
+ boxShadow: '0 4px 8px -2px rgb(0 0 0 / 0.25), 0 2px 4px -2px rgb(0 0 0 / 0.15)',
85
+ },
86
+ },
87
+ },
88
+ },
89
+ MuiTextField: {
90
+ styleOverrides: {
91
+ root: {
92
+ '& .MuiOutlinedInput-root': {
93
+ borderRadius: 8,
94
+ '&:hover fieldset': {
95
+ borderColor: '#10b981',
96
+ },
97
+ },
98
+ },
99
+ },
100
+ },
101
+ MuiIconButton: {
102
+ styleOverrides: {
103
+ root: {
104
+ borderRadius: 8,
105
+ },
106
+ },
107
+ },
108
+ },
109
+ });
110
+
111
+ export default theme;
frontend/src/utils/api.ts ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export async function fetchApi<T>(
2
+ endpoint: string,
3
+ options: RequestInit = {},
4
+ responseType: 'json' | 'blob' | 'text' = 'json',
5
+ ): Promise<T> {
6
+ console.log('fetchApi', endpoint, responseType, options);
7
+ const baseUrl =
8
+ process.env.NODE_ENV === 'development'
9
+ ? process.env.NEXT_PUBLIC_API_URL // 開発環境では.envから読み込む
10
+ : ''; // 本番環境では相対URLを使用
11
+ const fullUrl = `${baseUrl}/api${endpoint}`;
12
+
13
+ const res = await fetch(fullUrl, {
14
+ headers: { 'Content-Type': 'application/json' },
15
+ ...options,
16
+ });
17
+ if (!res.ok) {
18
+ const error = await res.json();
19
+ console.error('Error code: ', res.status, 'Error message: ', error.detail);
20
+ throw new Error(error.detail);
21
+ }
22
+
23
+ let result: T;
24
+ switch (responseType) {
25
+ case 'json':
26
+ result = await res.json();
27
+ break;
28
+ case 'blob':
29
+ result = await res.blob() as unknown as T;
30
+ break;
31
+ case 'text':
32
+ result = await res.text() as unknown as T;
33
+ break;
34
+ default:
35
+ result = res.status === 204 ? ({} as T) : (await res.json());
36
+ }
37
+
38
+ console.log('fetchApi result', result);
39
+ return result;
40
+ }
frontend/tailwind.config.ts ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Config } from 'tailwindcss';
2
+
3
+ const config: Config = {
4
+ content: [
5
+ './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
6
+ './src/components/**/*.{js,ts,jsx,tsx,mdx}',
7
+ './src/app/**/*.{js,ts,jsx,tsx,mdx}',
8
+ ],
9
+ theme: {
10
+ extend: {
11
+ backgroundImage: {
12
+ 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
13
+ 'gradient-conic':
14
+ 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
15
+ },
16
+ },
17
+ },
18
+ plugins: [],
19
+ };
20
+ export default config;
frontend/tsconfig.json ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "lib": [
4
+ "dom",
5
+ "dom.iterable",
6
+ "esnext"
7
+ ],
8
+ "allowJs": true,
9
+ "skipLibCheck": true,
10
+ "strict": true,
11
+ "noEmit": true,
12
+ "esModuleInterop": true,
13
+ "module": "esnext",
14
+ "moduleResolution": "bundler",
15
+ "resolveJsonModule": true,
16
+ "isolatedModules": true,
17
+ "jsx": "preserve",
18
+ "incremental": true,
19
+ "plugins": [
20
+ {
21
+ "name": "next"
22
+ }
23
+ ],
24
+ "paths": {
25
+ "@/*": [
26
+ "./src/*"
27
+ ]
28
+ }
29
+ },
30
+ "include": [
31
+ "next-env.d.ts",
32
+ "**/*.ts",
33
+ "**/*.tsx",
34
+ ".next/types/**/*.ts",
35
+ "out/types/**/*.ts"
36
+ ],
37
+ "exclude": [
38
+ "node_modules"
39
+ ]
40
+ }
initialize.py ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import json
3
+ import shutil
4
+ from pathlib import Path
5
+
6
+ import yaml
7
+ from huggingface_hub import hf_hub_download
8
+
9
+ from style_bert_vits2.logging import logger
10
+
11
+
12
+ def download_bert_models():
13
+ with open("bert/bert_models.json", encoding="utf-8") as fp:
14
+ models = json.load(fp)
15
+ for k, v in models.items():
16
+ local_path = Path("bert").joinpath(k)
17
+ for file in v["files"]:
18
+ if not Path(local_path).joinpath(file).exists():
19
+ logger.info(f"Downloading {k} {file}")
20
+ hf_hub_download(v["repo_id"], file, local_dir=local_path)
21
+
22
+
23
+ def download_slm_model():
24
+ local_path = Path("slm/wavlm-base-plus/")
25
+ file = "pytorch_model.bin"
26
+ if not Path(local_path).joinpath(file).exists():
27
+ logger.info(f"Downloading wavlm-base-plus {file}")
28
+ hf_hub_download("microsoft/wavlm-base-plus", file, local_dir=local_path)
29
+
30
+
31
+ def download_pretrained_models():
32
+ files = ["G_0.safetensors", "D_0.safetensors", "DUR_0.safetensors"]
33
+ local_path = Path("pretrained")
34
+ for file in files:
35
+ if not Path(local_path).joinpath(file).exists():
36
+ logger.info(f"Downloading pretrained {file}")
37
+ hf_hub_download(
38
+ "litagin/Style-Bert-VITS2-1.0-base", file, local_dir=local_path
39
+ )
40
+
41
+
42
+ def download_jp_extra_pretrained_models():
43
+ files = ["G_0.safetensors", "D_0.safetensors", "WD_0.safetensors"]
44
+ local_path = Path("pretrained_jp_extra")
45
+ for file in files:
46
+ if not Path(local_path).joinpath(file).exists():
47
+ logger.info(f"Downloading JP-Extra pretrained {file}")
48
+ hf_hub_download(
49
+ "litagin/Style-Bert-VITS2-2.0-base-JP-Extra", file, local_dir=local_path
50
+ )
51
+
52
+
53
+ def download_default_models():
54
+ # Only use kariusagi model - no default models to download
55
+ # The kariusagi model is already included in model_assets/kariusagi/
56
+ logger.info("Using kariusagi model only - no default models to download")
57
+
58
+
59
+ def main():
60
+ parser = argparse.ArgumentParser()
61
+ parser.add_argument("--skip_default_models", action="store_true")
62
+ parser.add_argument("--only_infer", action="store_true")
63
+ parser.add_argument(
64
+ "--dataset_root",
65
+ type=str,
66
+ help="Dataset root path (default: Data)",
67
+ default=None,
68
+ )
69
+ parser.add_argument(
70
+ "--assets_root",
71
+ type=str,
72
+ help="Assets root path (default: model_assets)",
73
+ default=None,
74
+ )
75
+ args = parser.parse_args()
76
+
77
+ download_bert_models()
78
+
79
+ if not args.skip_default_models:
80
+ download_default_models()
81
+ if not args.only_infer:
82
+ download_slm_model()
83
+ download_pretrained_models()
84
+ download_jp_extra_pretrained_models()
85
+
86
+ # If configs/paths.yml not exists, create it
87
+ default_paths_yml = Path("configs/default_paths.yml")
88
+ paths_yml = Path("configs/paths.yml")
89
+ if not paths_yml.exists():
90
+ shutil.copy(default_paths_yml, paths_yml)
91
+
92
+ if args.dataset_root is None and args.assets_root is None:
93
+ return
94
+
95
+ # Change default paths if necessary
96
+ with open(paths_yml, encoding="utf-8") as f:
97
+ yml_data = yaml.safe_load(f)
98
+ if args.assets_root is not None:
99
+ yml_data["assets_root"] = args.assets_root
100
+ if args.dataset_root is not None:
101
+ yml_data["dataset_root"] = args.dataset_root
102
+ with open(paths_yml, "w", encoding="utf-8") as f:
103
+ yaml.dump(yml_data, f, allow_unicode=True)
104
+
105
+
106
+ if __name__ == "__main__":
107
+ main()
model_assets/kariusagi/config.json ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_name": "kariusagi_v0.0",
3
+ "train": {
4
+ "log_interval": 200,
5
+ "eval_interval": 3000,
6
+ "seed": 42,
7
+ "epochs": 200,
8
+ "learning_rate": 0.0001,
9
+ "betas": [
10
+ 0.8,
11
+ 0.99
12
+ ],
13
+ "eps": 1e-09,
14
+ "batch_size": 3,
15
+ "bf16_run": false,
16
+ "fp16_run": false,
17
+ "lr_decay": 0.99996,
18
+ "segment_size": 16384,
19
+ "init_lr_ratio": 1,
20
+ "warmup_epochs": 0,
21
+ "c_mel": 45,
22
+ "c_kl": 1.0,
23
+ "c_commit": 100,
24
+ "skip_optimizer": false,
25
+ "freeze_ZH_bert": false,
26
+ "freeze_JP_bert": false,
27
+ "freeze_EN_bert": false,
28
+ "freeze_emo": false,
29
+ "freeze_style": false,
30
+ "freeze_decoder": false
31
+ },
32
+ "data": {
33
+ "use_jp_extra": true,
34
+ "training_files": "Data/kariusagi_v0.0/train.list",
35
+ "validation_files": "Data/kariusagi_v0.0/val.list",
36
+ "max_wav_value": 32768.0,
37
+ "sampling_rate": 44100,
38
+ "filter_length": 2048,
39
+ "hop_length": 512,
40
+ "win_length": 2048,
41
+ "n_mel_channels": 128,
42
+ "mel_fmin": 0.0,
43
+ "mel_fmax": null,
44
+ "add_blank": true,
45
+ "n_speakers": 1,
46
+ "cleaned_text": true,
47
+ "spk2id": {
48
+ "kariusagi": 0
49
+ },
50
+ "num_styles": 3,
51
+ "style2id": {
52
+ "Neutral": 0,
53
+ "dark": 1,
54
+ "happy": 2
55
+ }
56
+ },
57
+ "model": {
58
+ "use_spk_conditioned_encoder": true,
59
+ "use_noise_scaled_mas": true,
60
+ "use_mel_posterior_encoder": false,
61
+ "use_duration_discriminator": false,
62
+ "use_wavlm_discriminator": true,
63
+ "inter_channels": 192,
64
+ "hidden_channels": 192,
65
+ "filter_channels": 768,
66
+ "n_heads": 2,
67
+ "n_layers": 6,
68
+ "kernel_size": 3,
69
+ "p_dropout": 0.1,
70
+ "resblock": "1",
71
+ "resblock_kernel_sizes": [
72
+ 3,
73
+ 7,
74
+ 11
75
+ ],
76
+ "resblock_dilation_sizes": [
77
+ [
78
+ 1,
79
+ 3,
80
+ 5
81
+ ],
82
+ [
83
+ 1,
84
+ 3,
85
+ 5
86
+ ],
87
+ [
88
+ 1,
89
+ 3,
90
+ 5
91
+ ]
92
+ ],
93
+ "upsample_rates": [
94
+ 8,
95
+ 8,
96
+ 2,
97
+ 2,
98
+ 2
99
+ ],
100
+ "upsample_initial_channel": 512,
101
+ "upsample_kernel_sizes": [
102
+ 16,
103
+ 16,
104
+ 8,
105
+ 2,
106
+ 2
107
+ ],
108
+ "n_layers_q": 3,
109
+ "use_spectral_norm": false,
110
+ "gin_channels": 512,
111
+ "slm": {
112
+ "model": "./slm/wavlm-base-plus",
113
+ "sr": 16000,
114
+ "hidden": 768,
115
+ "nlayers": 13,
116
+ "initial_channel": 64
117
+ }
118
+ },
119
+ "version": "2.6.1-JP-Extra"
120
+ }
model_assets/kariusagi/style_vectors.npy ADDED
Binary file (3.2 kB). View file
 
requirements.txt ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # cmudict
2
+ # cn2an
3
+ # faster-whisper==0.10.1
4
+ fastapi[standard]
5
+ # g2p_en
6
+ # GPUtil
7
+ # gradio
8
+ # jieba
9
+ # librosa==0.9.2
10
+ loguru
11
+ num2words
12
+ # protobuf==4.25
13
+ # psutil
14
+ # punctuators
15
+ pyannote.audio>=3.1.0
16
+ # pyloudnorm
17
+ pyopenjtalk-dict
18
+ # pypinyin
19
+ pyworld-prebuilt
20
+ # stable_ts
21
+ # tensorboard
22
+ torch
23
+ transformers
24
+ # umap-learn
server_editor.py ADDED
@@ -0,0 +1,450 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Style-Bert-VITS2-Editor用のサーバー。
3
+ 次のリポジトリ
4
+ https://github.com/litagin02/Style-Bert-VITS2-Editor
5
+ をビルドしてできあがったファイルをWebフォルダに入れて実行する。
6
+
7
+ TODO: リファクタリングやドキュメンテーションやAPI整理、辞書周りの改善などが必要。
8
+ """
9
+
10
+ import argparse
11
+ import io
12
+ import shutil
13
+ import sys
14
+ import webbrowser
15
+ import zipfile
16
+ from datetime import datetime
17
+ from io import BytesIO
18
+ from pathlib import Path
19
+ from typing import Optional
20
+
21
+ import numpy as np
22
+ import requests
23
+ import torch
24
+ import uvicorn
25
+ from fastapi import APIRouter, FastAPI, HTTPException, status
26
+ from fastapi.middleware.cors import CORSMiddleware
27
+ from fastapi.responses import JSONResponse, Response
28
+ from fastapi.staticfiles import StaticFiles
29
+ from pydantic import BaseModel
30
+ from scipy.io import wavfile
31
+
32
+ from config import get_path_config
33
+ from initialize import download_default_models
34
+ from style_bert_vits2.constants import (
35
+ DEFAULT_ASSIST_TEXT_WEIGHT,
36
+ DEFAULT_NOISE,
37
+ DEFAULT_NOISEW,
38
+ DEFAULT_SDP_RATIO,
39
+ DEFAULT_STYLE,
40
+ DEFAULT_STYLE_WEIGHT,
41
+ VERSION,
42
+ Languages,
43
+ )
44
+ from style_bert_vits2.logging import logger
45
+ from style_bert_vits2.nlp import bert_models
46
+ from style_bert_vits2.nlp.japanese import pyopenjtalk_worker as pyopenjtalk
47
+ from style_bert_vits2.nlp.japanese.g2p_utils import g2kata_tone, kata_tone2phone_tone
48
+ from style_bert_vits2.nlp.japanese.normalizer import normalize_text
49
+ from style_bert_vits2.nlp.japanese.user_dict import (
50
+ apply_word,
51
+ delete_word,
52
+ read_dict,
53
+ rewrite_word,
54
+ update_dict,
55
+ )
56
+ from style_bert_vits2.tts_model import TTSModelHolder, TTSModelInfo
57
+
58
+
59
+ # ---フロントエンド部分に関する処理---
60
+
61
+ # エディターのビルドファイルを配置するディレクトリ
62
+ STATIC_DIR = Path("static")
63
+ # エディターの最新のビルドファイルのダウンロード日時を記録するファイル
64
+ LAST_DOWNLOAD_FILE = STATIC_DIR / "last_download.txt"
65
+
66
+
67
+ def download_static_files(user, repo, asset_name):
68
+ """Style-Bert-VITS2エディターの最新のビルドzipをダウンロードして展開する。"""
69
+
70
+ logger.info("Checking for new release...")
71
+ latest_release = get_latest_release(user, repo)
72
+ if latest_release is None:
73
+ logger.warning(
74
+ "Failed to fetch the latest release. Proceeding without static files."
75
+ )
76
+ return
77
+
78
+ if not new_release_available(latest_release):
79
+ logger.info("No new release available. Proceeding with existing static files.")
80
+ return
81
+
82
+ logger.info("New release available. Downloading static files...")
83
+ asset_url = get_asset_url(latest_release, asset_name)
84
+ if asset_url:
85
+ if STATIC_DIR.exists():
86
+ shutil.rmtree(STATIC_DIR)
87
+ STATIC_DIR.mkdir(parents=True, exist_ok=True)
88
+ download_and_extract(asset_url, STATIC_DIR)
89
+ save_last_download(latest_release)
90
+ else:
91
+ logger.warning("Asset not found. Proceeding without static files.")
92
+
93
+
94
+ def get_latest_release(user, repo):
95
+ url = f"https://api.github.com/repos/{user}/{repo}/releases/latest"
96
+ try:
97
+ response = requests.get(url)
98
+ response.raise_for_status()
99
+ return response.json()
100
+ except requests.RequestException:
101
+ return None
102
+
103
+
104
+ def get_asset_url(release, asset_name):
105
+ for asset in release["assets"]:
106
+ if asset["name"] == asset_name:
107
+ return asset["browser_download_url"]
108
+ return None
109
+
110
+
111
+ def download_and_extract(url, extract_to: Path):
112
+ response = requests.get(url)
113
+ response.raise_for_status()
114
+ with zipfile.ZipFile(io.BytesIO(response.content)) as zip_ref:
115
+ zip_ref.extractall(extract_to)
116
+
117
+ # 展開先が1つのディレクトリだけの場合、その中身を直下に移動する
118
+ extracted_dirs = list(extract_to.iterdir())
119
+ if len(extracted_dirs) == 1 and extracted_dirs[0].is_dir():
120
+ for file in extracted_dirs[0].iterdir():
121
+ file.rename(extract_to / file.name)
122
+ extracted_dirs[0].rmdir()
123
+
124
+ # index.htmlが存在するかチェック
125
+ if not (extract_to / "index.html").exists():
126
+ logger.warning("index.html not found in the extracted files.")
127
+
128
+
129
+ def new_release_available(latest_release):
130
+ if LAST_DOWNLOAD_FILE.exists():
131
+ with open(LAST_DOWNLOAD_FILE) as file:
132
+ last_download_str = file.read().strip()
133
+ # 'Z'を除去して日時オブジェクトに変換
134
+ last_download_str = last_download_str.replace("Z", "+00:00")
135
+ last_download = datetime.fromisoformat(last_download_str)
136
+ return (
137
+ datetime.fromisoformat(
138
+ latest_release["published_at"].replace("Z", "+00:00")
139
+ )
140
+ > last_download
141
+ )
142
+ return True
143
+
144
+
145
+ def save_last_download(latest_release):
146
+ with open(LAST_DOWNLOAD_FILE, "w") as file:
147
+ file.write(latest_release["published_at"])
148
+
149
+
150
+ # ---フロントエンド部分に関する処理ここまで---
151
+ # 以降はAPIの設定
152
+
153
+ # pyopenjtalk_worker を起動
154
+ ## pyopenjtalk_worker は TCP ソケットサーバーのため、ここで起動する
155
+ pyopenjtalk.initialize_worker()
156
+
157
+ # pyopenjtalk の辞書を更新
158
+ update_dict()
159
+
160
+ # 事前に BERT モデル/トークナイザーをロードしておく
161
+ ## ここでロードしなくても必要になった際に自動ロードされるが、時間がかかるため事前にロードしておいた方が体験が良い
162
+ ## server_editor.py は日本語にしか対応していないため、日本語の BERT モデル/トークナイザーのみロードする
163
+ bert_models.load_model(Languages.JP)
164
+ bert_models.load_tokenizer(Languages.JP)
165
+
166
+
167
+ class AudioResponse(Response):
168
+ media_type = "audio/wav"
169
+
170
+
171
+ origins = [
172
+ "http://localhost:3000",
173
+ "http://localhost:8000",
174
+ "http://127.0.0.1:3000",
175
+ "http://127.0.0.1:8000",
176
+ ]
177
+
178
+ path_config = get_path_config()
179
+ parser = argparse.ArgumentParser()
180
+ parser.add_argument("--model_dir", type=str, default=path_config.assets_root)
181
+ parser.add_argument("--device", type=str, default="cuda")
182
+ parser.add_argument("--port", type=int, default=8000)
183
+ parser.add_argument("--inbrowser", action="store_true")
184
+ parser.add_argument("--line_length", type=int, default=None)
185
+ parser.add_argument("--line_count", type=int, default=None)
186
+ parser.add_argument("--skip_default_models", action="store_true")
187
+ parser.add_argument("--skip_static_files", action="store_true")
188
+ args = parser.parse_args()
189
+ device = args.device
190
+ if device == "cuda" and not torch.cuda.is_available():
191
+ device = "cpu"
192
+ model_dir = Path(args.model_dir)
193
+ port = int(args.port)
194
+ if not args.skip_default_models:
195
+ download_default_models()
196
+ skip_static_files = bool(args.skip_static_files)
197
+
198
+ model_holder = TTSModelHolder(model_dir, device)
199
+ if len(model_holder.model_names) == 0:
200
+ logger.error(f"Models not found in {model_dir}.")
201
+ sys.exit(1)
202
+
203
+
204
+ app = FastAPI()
205
+
206
+
207
+ app.add_middleware(
208
+ CORSMiddleware,
209
+ allow_origins=origins,
210
+ allow_credentials=True,
211
+ allow_methods=["*"],
212
+ allow_headers=["*"],
213
+ )
214
+
215
+ router = APIRouter()
216
+
217
+
218
+ @router.get("/version")
219
+ def version() -> str:
220
+ return VERSION
221
+
222
+
223
+ class MoraTone(BaseModel):
224
+ mora: str
225
+ tone: int
226
+
227
+
228
+ class TextRequest(BaseModel):
229
+ text: str
230
+
231
+
232
+ @router.post("/g2p")
233
+ async def read_item(item: TextRequest):
234
+ try:
235
+ # 最初に正規化しないと整合性がとれない
236
+ text = normalize_text(item.text)
237
+ kata_tone_list = g2kata_tone(text)
238
+ except Exception as e:
239
+ raise HTTPException(
240
+ status_code=400,
241
+ detail=f"Failed to convert {item.text} to katakana and tone, {e}",
242
+ )
243
+ return [MoraTone(mora=kata, tone=tone) for kata, tone in kata_tone_list]
244
+
245
+
246
+ @router.post("/normalize")
247
+ async def normalize(item: TextRequest):
248
+ return normalize_text(item.text)
249
+
250
+
251
+ @router.get("/models_info", response_model=list[TTSModelInfo])
252
+ def models_info():
253
+ return model_holder.models_info
254
+
255
+
256
+ class SynthesisRequest(BaseModel):
257
+ model: str
258
+ modelFile: str
259
+ text: str
260
+ moraToneList: list[MoraTone]
261
+ style: str = DEFAULT_STYLE
262
+ styleWeight: float = DEFAULT_STYLE_WEIGHT
263
+ assistText: str = ""
264
+ assistTextWeight: float = DEFAULT_ASSIST_TEXT_WEIGHT
265
+ speed: float = 1.0
266
+ noise: float = DEFAULT_NOISE
267
+ noisew: float = DEFAULT_NOISEW
268
+ sdpRatio: float = DEFAULT_SDP_RATIO
269
+ language: Languages = Languages.JP
270
+ silenceAfter: float = 0.5
271
+ pitchScale: float = 1.0
272
+ intonationScale: float = 1.0
273
+ speaker: Optional[str] = None
274
+
275
+
276
+ @router.post("/synthesis", response_class=AudioResponse)
277
+ def synthesis(request: SynthesisRequest):
278
+ if args.line_length is not None and len(request.text) > args.line_length:
279
+ raise HTTPException(
280
+ status_code=400,
281
+ detail=f"1行の文字数は{args.line_length}文字以下にしてください。",
282
+ )
283
+ try:
284
+ model = model_holder.get_model(
285
+ model_name=request.model, model_path_str=request.modelFile
286
+ )
287
+ except Exception as e:
288
+ logger.error(e)
289
+ raise HTTPException(
290
+ status_code=500,
291
+ detail=f"Failed to load model {request.model} from {request.modelFile}, {e}",
292
+ )
293
+ text = request.text
294
+ kata_tone_list = [
295
+ (mora_tone.mora, mora_tone.tone) for mora_tone in request.moraToneList
296
+ ]
297
+ phone_tone = kata_tone2phone_tone(kata_tone_list)
298
+ tone = [t for _, t in phone_tone]
299
+ try:
300
+ sid = 0 if request.speaker is None else model.spk2id[request.speaker]
301
+ except KeyError:
302
+ raise HTTPException(
303
+ status_code=400,
304
+ detail=f"Speaker {request.speaker} not found in {model.spk2id}",
305
+ )
306
+ sr, audio = model.infer(
307
+ text=text,
308
+ language=request.language,
309
+ sdp_ratio=request.sdpRatio,
310
+ noise=request.noise,
311
+ noise_w=request.noisew,
312
+ length=1 / request.speed,
313
+ given_tone=tone,
314
+ style=request.style,
315
+ style_weight=request.styleWeight,
316
+ assist_text=request.assistText,
317
+ assist_text_weight=request.assistTextWeight,
318
+ use_assist_text=bool(request.assistText),
319
+ line_split=False,
320
+ pitch_scale=request.pitchScale,
321
+ intonation_scale=request.intonationScale,
322
+ speaker_id=sid,
323
+ )
324
+
325
+ with BytesIO() as wavContent:
326
+ wavfile.write(wavContent, sr, audio)
327
+ return Response(content=wavContent.getvalue(), media_type="audio/wav")
328
+
329
+
330
+ class MultiSynthesisRequest(BaseModel):
331
+ lines: list[SynthesisRequest]
332
+
333
+
334
+ @router.post("/multi_synthesis", response_class=AudioResponse)
335
+ def multi_synthesis(request: MultiSynthesisRequest):
336
+ lines = request.lines
337
+ if args.line_count is not None and len(lines) > args.line_count:
338
+ raise HTTPException(
339
+ status_code=400,
340
+ detail=f"行数は{args.line_count}行以下にしてください。",
341
+ )
342
+ audios = []
343
+ sr = None
344
+ for i, req in enumerate(lines):
345
+ if args.line_length is not None and len(req.text) > args.line_length:
346
+ raise HTTPException(
347
+ status_code=400,
348
+ detail=f"1行の文字数は{args.line_length}文字以下にしてください。",
349
+ )
350
+ try:
351
+ model = model_holder.get_model(
352
+ model_name=req.model, model_path_str=req.modelFile
353
+ )
354
+ except Exception as e:
355
+ logger.error(e)
356
+ raise HTTPException(
357
+ status_code=500,
358
+ detail=f"Failed to load model {req.model} from {req.modelFile}, {e}",
359
+ )
360
+ text = req.text
361
+ kata_tone_list = [
362
+ (mora_tone.mora, mora_tone.tone) for mora_tone in req.moraToneList
363
+ ]
364
+ phone_tone = kata_tone2phone_tone(kata_tone_list)
365
+ tone = [t for _, t in phone_tone]
366
+ sr, audio = model.infer(
367
+ text=text,
368
+ language=req.language,
369
+ sdp_ratio=req.sdpRatio,
370
+ noise=req.noise,
371
+ noise_w=req.noisew,
372
+ length=1 / req.speed,
373
+ given_tone=tone,
374
+ style=req.style,
375
+ style_weight=req.styleWeight,
376
+ assist_text=req.assistText,
377
+ assist_text_weight=req.assistTextWeight,
378
+ use_assist_text=bool(req.assistText),
379
+ line_split=False,
380
+ pitch_scale=req.pitchScale,
381
+ intonation_scale=req.intonationScale,
382
+ )
383
+ audios.append(audio)
384
+ if i < len(lines) - 1:
385
+ silence = int(sr * req.silenceAfter)
386
+ audios.append(np.zeros(silence, dtype=np.int16))
387
+ audio = np.concatenate(audios)
388
+
389
+ with BytesIO() as wavContent:
390
+ wavfile.write(wavContent, sr, audio)
391
+ return Response(content=wavContent.getvalue(), media_type="audio/wav")
392
+
393
+
394
+ class UserDictWordRequest(BaseModel):
395
+ surface: str
396
+ pronunciation: str
397
+ accent_type: int # アクセント核位置(存在しない場合は0、1文字目は1)
398
+ priority: int = 5
399
+
400
+
401
+ @router.get("/user_dict")
402
+ def get_user_dict():
403
+ return read_dict()
404
+
405
+
406
+ @router.post("/user_dict_word")
407
+ def add_user_dict_word(request: UserDictWordRequest):
408
+ uuid = apply_word(
409
+ surface=request.surface,
410
+ pronunciation=request.pronunciation,
411
+ accent_type=request.accent_type,
412
+ priority=request.priority,
413
+ )
414
+ update_dict()
415
+
416
+ return JSONResponse(
417
+ status_code=status.HTTP_201_CREATED,
418
+ content={"uuid": uuid},
419
+ )
420
+
421
+
422
+ @router.put("/user_dict_word/{uuid}")
423
+ def update_user_dict_word(uuid: str, request: UserDictWordRequest):
424
+ rewrite_word(
425
+ word_uuid=uuid,
426
+ surface=request.surface,
427
+ pronunciation=request.pronunciation,
428
+ accent_type=request.accent_type,
429
+ priority=request.priority,
430
+ )
431
+ update_dict()
432
+ return JSONResponse(status_code=status.HTTP_200_OK, content={"uuid": uuid})
433
+
434
+
435
+ @router.delete("/user_dict_word/{uuid}")
436
+ def delete_user_dict_word(uuid: str):
437
+ delete_word(uuid)
438
+ update_dict()
439
+ return JSONResponse(status_code=status.HTTP_200_OK, content={"uuid": uuid})
440
+
441
+
442
+ app.include_router(router, prefix="/api")
443
+
444
+ if __name__ == "__main__":
445
+ if not skip_static_files:
446
+ download_static_files("litagin02", "Style-Bert-VITS2-Editor", "out.zip")
447
+ app.mount("/", StaticFiles(directory=STATIC_DIR, html=True), name="static")
448
+ if args.inbrowser:
449
+ webbrowser.open(f"http://localhost:{port}")
450
+ uvicorn.run(app, host="0.0.0.0", port=port)
static/404.html ADDED
@@ -0,0 +1 @@
 
 
1
+ <!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="preload" href="/_next/static/media/e4af272ccee01ff0-s.p.woff2" as="font" crossorigin="" type="font/woff2"/><link rel="preload" href="/_next/static/media/f2dba9fbcf2f771b-s.p.woff2" as="font" crossorigin="" type="font/woff2"/><link rel="stylesheet" href="/_next/static/css/b77273ca8272af3e.css" data-precedence="next"/><link rel="stylesheet" href="/_next/static/css/07cba05a441081ea.css" data-precedence="next"/><link rel="preload" as="script" fetchPriority="low" href="/_next/static/chunks/webpack-3dfb36f26716f3ca.js"/><script src="/_next/static/chunks/fd9d1056-ec0959f0939cb609.js" async=""></script><script src="/_next/static/chunks/117-b970401bd8e4e4e0.js" async=""></script><script src="/_next/static/chunks/main-app-fcb16a029ffad6b5.js" async=""></script><script src="/_next/static/chunks/617-7f5b64eca86bc01a.js" async=""></script><script src="/_next/static/chunks/589-1dfa6562a75dc337.js" async=""></script><script src="/_next/static/chunks/app/layout-31a9e0a0d0521a65.js" async=""></script><meta name="robots" content="noindex"/><title>404: This page could not be found.</title><title>サンプルズの音声生成ツール(Style-Bert-VITS2 エディター)</title><meta name="description" content="Style-Bert-VITS2の音声合成エディターです。"/><meta name="next-size-adjust"/><script src="/_next/static/chunks/polyfills-42372ed130431b0a.js" noModule=""></script></head><body class="__className_e8ce0c"><div style="font-family:system-ui,&quot;Segoe UI&quot;,Roboto,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot;;height:100vh;text-align:center;display:flex;flex-direction:column;align-items:center;justify-content:center"><div><style>body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}</style><h1 class="next-error-h1" style="display:inline-block;margin:0 20px 0 0;padding:0 23px 0 0;font-size:24px;font-weight:500;vertical-align:top;line-height:49px">404</h1><div style="display:inline-block"><h2 style="font-size:14px;font-weight:400;line-height:49px;margin:0">This page could not be found.</h2></div></div></div><script src="/_next/static/chunks/webpack-3dfb36f26716f3ca.js" async=""></script><script>(self.__next_f=self.__next_f||[]).push([0]);self.__next_f.push([2,null])</script><script>self.__next_f.push([1,"1:HL[\"/_next/static/media/e4af272ccee01ff0-s.p.woff2\",\"font\",{\"crossOrigin\":\"\",\"type\":\"font/woff2\"}]\n2:HL[\"/_next/static/media/f2dba9fbcf2f771b-s.p.woff2\",\"font\",{\"crossOrigin\":\"\",\"type\":\"font/woff2\"}]\n3:HL[\"/_next/static/css/b77273ca8272af3e.css\",\"style\"]\n4:HL[\"/_next/static/css/07cba05a441081ea.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"5:I[2846,[],\"\"]\n7:I[4707,[],\"\"]\n8:I[6423,[],\"\"]\n9:I[291,[\"617\",\"static/chunks/617-7f5b64eca86bc01a.js\",\"589\",\"static/chunks/589-1dfa6562a75dc337.js\",\"185\",\"static/chunks/app/layout-31a9e0a0d0521a65.js\"],\"default\"]\na:I[60,[\"617\",\"static/chunks/617-7f5b64eca86bc01a.js\",\"589\",\"static/chunks/589-1dfa6562a75dc337.js\",\"185\",\"static/chunks/app/layout-31a9e0a0d0521a65.js\"],\"ThemeProvider\"]\nb:I[9500,[\"617\",\"static/chunks/617-7f5b64eca86bc01a.js\",\"589\",\"static/chunks/589-1dfa6562a75dc337.js\",\"185\",\"static/chunks/app/layout-31a9e0a0d0521a65.js\"],\"default\"]\n11:I[1060,[],\"\"]\nc:{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"}\nd:{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"}\ne:{\"display\":\"inline-block\"}\nf:{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0}\n12:[]\n"])</script><script>self.__next_f.push([1,"0:[\"$\",\"$L5\",null,{\"buildId\":\"8q7amU-bnfgJwF-L0ePLq\",\"assetPrefix\":\"\",\"urlParts\":[\"\",\"_not-found\",\"\"],\"initialTree\":[\"\",{\"children\":[\"/_not-found\",{\"children\":[\"__PAGE__\",{}]}]},\"$undefined\",\"$undefined\",true],\"initialSeedData\":[\"\",{\"children\":[\"/_not-found\",{\"children\":[\"__PAGE__\",{},[[\"$L6\",[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":\"404\"}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]],null],null],null]},[null,[\"$\",\"$L7\",null,{\"parallelRouterKey\":\"children\",\"segmentPath\":[\"children\",\"/_not-found\",\"children\"],\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L8\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$undefined\",\"notFoundStyles\":\"$undefined\"}]],null]},[[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/b77273ca8272af3e.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\"}],[\"$\",\"link\",\"1\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/07cba05a441081ea.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\"}]],[\"$\",\"html\",null,{\"lang\":\"en\",\"children\":[\"$\",\"body\",null,{\"className\":\"__className_e8ce0c\",\"children\":[\"$\",\"$L9\",null,{\"children\":[\"$\",\"$La\",null,{\"theme\":\"$b\",\"children\":[\"$\",\"$L7\",null,{\"parallelRouterKey\":\"children\",\"segmentPath\":[\"children\"],\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L8\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":\"$c\",\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":\"$d\",\"children\":\"404\"}],[\"$\",\"div\",null,{\"style\":\"$e\",\"children\":[\"$\",\"h2\",null,{\"style\":\"$f\",\"children\":\"This page could not be found.\"}]}]]}]}]],\"notFoundStyles\":[]}]}]}]}]}]],null],null],\"couldBeIntercepted\":false,\"initialHead\":[[\"$\",\"meta\",null,{\"name\":\"robots\",\"content\":\"noindex\"}],\"$L10\"],\"globalErrorComponent\":\"$11\",\"missingSlots\":\"$W12\"}]\n"])</script><script>self.__next_f.push([1,"10:[[\"$\",\"meta\",\"0\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}],[\"$\",\"meta\",\"1\",{\"charSet\":\"utf-8\"}],[\"$\",\"title\",\"2\",{\"children\":\"サンプルズの音声生成ツール(Style-Bert-VITS2 エディター)\"}],[\"$\",\"meta\",\"3\",{\"name\":\"description\",\"content\":\"Style-Bert-VITS2の音声合成エディターです。\"}],[\"$\",\"meta\",\"4\",{\"name\":\"next-size-adjust\"}]]\n6:null\n"])</script></body></html>
static/404/index.html ADDED
@@ -0,0 +1 @@
 
 
1
+ <!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="preload" href="/_next/static/media/e4af272ccee01ff0-s.p.woff2" as="font" crossorigin="" type="font/woff2"/><link rel="preload" href="/_next/static/media/f2dba9fbcf2f771b-s.p.woff2" as="font" crossorigin="" type="font/woff2"/><link rel="stylesheet" href="/_next/static/css/b77273ca8272af3e.css" data-precedence="next"/><link rel="stylesheet" href="/_next/static/css/07cba05a441081ea.css" data-precedence="next"/><link rel="preload" as="script" fetchPriority="low" href="/_next/static/chunks/webpack-3dfb36f26716f3ca.js"/><script src="/_next/static/chunks/fd9d1056-ec0959f0939cb609.js" async=""></script><script src="/_next/static/chunks/117-b970401bd8e4e4e0.js" async=""></script><script src="/_next/static/chunks/main-app-fcb16a029ffad6b5.js" async=""></script><script src="/_next/static/chunks/617-7f5b64eca86bc01a.js" async=""></script><script src="/_next/static/chunks/589-1dfa6562a75dc337.js" async=""></script><script src="/_next/static/chunks/app/layout-31a9e0a0d0521a65.js" async=""></script><meta name="robots" content="noindex"/><title>404: This page could not be found.</title><title>サンプルズの音声生成ツール(Style-Bert-VITS2 エディター)</title><meta name="description" content="Style-Bert-VITS2の音声合成エディターです。"/><meta name="next-size-adjust"/><script src="/_next/static/chunks/polyfills-42372ed130431b0a.js" noModule=""></script></head><body class="__className_e8ce0c"><div style="font-family:system-ui,&quot;Segoe UI&quot;,Roboto,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot;;height:100vh;text-align:center;display:flex;flex-direction:column;align-items:center;justify-content:center"><div><style>body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}</style><h1 class="next-error-h1" style="display:inline-block;margin:0 20px 0 0;padding:0 23px 0 0;font-size:24px;font-weight:500;vertical-align:top;line-height:49px">404</h1><div style="display:inline-block"><h2 style="font-size:14px;font-weight:400;line-height:49px;margin:0">This page could not be found.</h2></div></div></div><script src="/_next/static/chunks/webpack-3dfb36f26716f3ca.js" async=""></script><script>(self.__next_f=self.__next_f||[]).push([0]);self.__next_f.push([2,null])</script><script>self.__next_f.push([1,"1:HL[\"/_next/static/media/e4af272ccee01ff0-s.p.woff2\",\"font\",{\"crossOrigin\":\"\",\"type\":\"font/woff2\"}]\n2:HL[\"/_next/static/media/f2dba9fbcf2f771b-s.p.woff2\",\"font\",{\"crossOrigin\":\"\",\"type\":\"font/woff2\"}]\n3:HL[\"/_next/static/css/b77273ca8272af3e.css\",\"style\"]\n4:HL[\"/_next/static/css/07cba05a441081ea.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"5:I[2846,[],\"\"]\n7:I[4707,[],\"\"]\n8:I[6423,[],\"\"]\n9:I[291,[\"617\",\"static/chunks/617-7f5b64eca86bc01a.js\",\"589\",\"static/chunks/589-1dfa6562a75dc337.js\",\"185\",\"static/chunks/app/layout-31a9e0a0d0521a65.js\"],\"default\"]\na:I[60,[\"617\",\"static/chunks/617-7f5b64eca86bc01a.js\",\"589\",\"static/chunks/589-1dfa6562a75dc337.js\",\"185\",\"static/chunks/app/layout-31a9e0a0d0521a65.js\"],\"ThemeProvider\"]\nb:I[9500,[\"617\",\"static/chunks/617-7f5b64eca86bc01a.js\",\"589\",\"static/chunks/589-1dfa6562a75dc337.js\",\"185\",\"static/chunks/app/layout-31a9e0a0d0521a65.js\"],\"default\"]\n11:I[1060,[],\"\"]\nc:{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"}\nd:{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"}\ne:{\"display\":\"inline-block\"}\nf:{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0}\n12:[]\n"])</script><script>self.__next_f.push([1,"0:[\"$\",\"$L5\",null,{\"buildId\":\"8q7amU-bnfgJwF-L0ePLq\",\"assetPrefix\":\"\",\"urlParts\":[\"\",\"_not-found\",\"\"],\"initialTree\":[\"\",{\"children\":[\"/_not-found\",{\"children\":[\"__PAGE__\",{}]}]},\"$undefined\",\"$undefined\",true],\"initialSeedData\":[\"\",{\"children\":[\"/_not-found\",{\"children\":[\"__PAGE__\",{},[[\"$L6\",[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":\"404\"}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]],null],null],null]},[null,[\"$\",\"$L7\",null,{\"parallelRouterKey\":\"children\",\"segmentPath\":[\"children\",\"/_not-found\",\"children\"],\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L8\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$undefined\",\"notFoundStyles\":\"$undefined\"}]],null]},[[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/b77273ca8272af3e.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\"}],[\"$\",\"link\",\"1\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/07cba05a441081ea.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\"}]],[\"$\",\"html\",null,{\"lang\":\"en\",\"children\":[\"$\",\"body\",null,{\"className\":\"__className_e8ce0c\",\"children\":[\"$\",\"$L9\",null,{\"children\":[\"$\",\"$La\",null,{\"theme\":\"$b\",\"children\":[\"$\",\"$L7\",null,{\"parallelRouterKey\":\"children\",\"segmentPath\":[\"children\"],\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L8\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":\"$c\",\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":\"$d\",\"children\":\"404\"}],[\"$\",\"div\",null,{\"style\":\"$e\",\"children\":[\"$\",\"h2\",null,{\"style\":\"$f\",\"children\":\"This page could not be found.\"}]}]]}]}]],\"notFoundStyles\":[]}]}]}]}]}]],null],null],\"couldBeIntercepted\":false,\"initialHead\":[[\"$\",\"meta\",null,{\"name\":\"robots\",\"content\":\"noindex\"}],\"$L10\"],\"globalErrorComponent\":\"$11\",\"missingSlots\":\"$W12\"}]\n"])</script><script>self.__next_f.push([1,"10:[[\"$\",\"meta\",\"0\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}],[\"$\",\"meta\",\"1\",{\"charSet\":\"utf-8\"}],[\"$\",\"title\",\"2\",{\"children\":\"サンプルズの音声生成ツール(Style-Bert-VITS2 エディター)\"}],[\"$\",\"meta\",\"3\",{\"name\":\"description\",\"content\":\"Style-Bert-VITS2の音声合成エディターです。\"}],[\"$\",\"meta\",\"4\",{\"name\":\"next-size-adjust\"}]]\n6:null\n"])</script></body></html>
static/_next/static/8q7amU-bnfgJwF-L0ePLq/_buildManifest.js ADDED
@@ -0,0 +1 @@
 
 
1
+ self.__BUILD_MANIFEST={__rewrites:{afterFiles:[],beforeFiles:[],fallback:[]},"/_error":["static/chunks/pages/_error-7ba65e1336b92748.js"],sortedPages:["/_app","/_error"]},self.__BUILD_MANIFEST_CB&&self.__BUILD_MANIFEST_CB();
static/_next/static/8q7amU-bnfgJwF-L0ePLq/_ssgManifest.js ADDED
@@ -0,0 +1 @@
 
 
1
+ self.__SSG_MANIFEST=new Set([]);self.__SSG_MANIFEST_CB&&self.__SSG_MANIFEST_CB()
static/_next/static/chunks/117-b970401bd8e4e4e0.js ADDED
The diff for this file is too large to render. See raw diff
 
static/_next/static/chunks/313-0e35468afc829c33.js ADDED
The diff for this file is too large to render. See raw diff
 
static/_next/static/chunks/589-1dfa6562a75dc337.js ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ (self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[589],{291:function(e,t,r){"use strict";r.r(t),r.d(t,{default:function(){return s}});var n=r(1119),o=r(2265),a=r(8242),l=r(5246),i=r(5475),c=r(7437);function s(e){let{options:t,CacheProvider:r=l.C,children:s}=e,[u]=o.useState(()=>{var e;let r=(0,a.Z)((0,n.Z)({},t,{key:null!=(e=null==t?void 0:t.key)?e:"mui"}));r.compat=!0;let o=r.insert,l=[];return r.insert=function(){for(var e=arguments.length,n=Array(e),a=0;a<e;a++)n[a]=arguments[a];null!=t&&t.enableCssLayer&&(n[1].styles="@layer mui {".concat(n[1].styles,"}"));let[i,c]=n;return void 0===r.inserted[c.name]&&l.push({name:c.name,isGlobal:!i}),o(...n)},{cache:r,flush:()=>{let e=l;return l=[],e}}});return(0,i.useServerInsertedHTML)(()=>{let e=u.flush();if(0===e.length)return null;let r="",n=u.cache.key,a=[];return e.forEach(e=>{let{name:t,isGlobal:o}=e,l=u.cache.inserted[t];"string"==typeof l&&(o?a.push({name:t,style:l}):(r+=l,n+=" ".concat(t)))}),(0,c.jsxs)(o.Fragment,{children:[a.map(e=>{let{name:r,style:n}=e;return(0,c.jsx)("style",{nonce:null==t?void 0:t.nonce,"data-emotion":"".concat(u.cache.key,"-global ").concat(r),dangerouslySetInnerHTML:{__html:n}},r)}),r&&(0,c.jsx)("style",{nonce:null==t?void 0:t.nonce,"data-emotion":n,dangerouslySetInnerHTML:{__html:r}})]})}),(0,c.jsx)(r,{value:u.cache,children:s})}},60:function(e,t,r){"use strict";r.r(t),r.d(t,{Experimental_CssVarsProvider:function(){return eT},StyledEngineProvider:function(){return R.Z},THEME_ID:function(){return o.Z},ThemeProvider:function(){return H},adaptV4Theme:function(){return d},alpha:function(){return m.Fq},createMuiTheme:function(){return h.A},createStyles:function(){return S},createTheme:function(){return h.Z},css:function(){return g.iv},darken:function(){return m._j},decomposeColor:function(){return m.tB},duration:function(){return x.x9},easing:function(){return x.Ui},emphasize:function(){return m._4},experimentalStyled:function(){return B.ZP},experimental_extendTheme:function(){return ex},experimental_sx:function(){return ej},getContrastRatio:function(){return m.mi},getInitColorSchemeScript:function(){return eI},getLuminance:function(){return m.H3},getOverlayAlpha:function(){return eh.Z},hexToRgb:function(){return m.oo},hslToRgb:function(){return m.ve},keyframes:function(){return g.F4},lighten:function(){return m.$n},makeStyles:function(){return N},private_createMixins:function(){return eF.Z},private_createTypography:function(){return ew.Z},private_excludeVariablesFromRoot:function(){return eZ},recomposeColor:function(){return m.wy},responsiveFontSizes:function(){return k},rgbToHex:function(){return m.vq},shouldSkipGeneratingVar:function(){return eg},styled:function(){return B.ZP},unstable_createMuiStrictModeTheme:function(){return p},unstable_getUnit:function(){return b},unstable_toUnitless:function(){return C},useColorScheme:function(){return eB},useTheme:function(){return w.Z},useThemeProps:function(){return T},withStyles:function(){return U},withTheme:function(){return W}});var n=r(399),o=r(2166),a=r(1119),l=r(4610),i=r(3045),c=r(1324);let s=["defaultProps","mixins","overrides","palette","props","styleOverrides"],u=["type","mode"];function d(e){let{defaultProps:t={},mixins:r={},overrides:n={},palette:o={},props:d={},styleOverrides:m={}}=e,g=(0,l.Z)(e,s),h=(0,a.Z)({},g,{components:{}});Object.keys(t).forEach(e=>{let r=h.components[e]||{};r.defaultProps=t[e],h.components[e]=r}),Object.keys(d).forEach(e=>{let t=h.components[e]||{};t.defaultProps=d[e],h.components[e]=t}),Object.keys(m).forEach(e=>{let t=h.components[e]||{};t.styleOverrides=m[e],h.components[e]=t}),Object.keys(n).forEach(e=>{let t=h.components[e]||{};t.styleOverrides=n[e],h.components[e]=t}),h.spacing=(0,i.Z)(e.spacing);let f=(0,c.Z)(e.breakpoints||{}),p=h.spacing;h.mixins=(0,a.Z)({gutters:function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return(0,a.Z)({paddingLeft:p(2),paddingRight:p(2)},e,{[f.up("sm")]:(0,a.Z)({paddingLeft:p(3),paddingRight:p(3)},e[f.up("sm")])})}},r);let{type:y,mode:S}=o,v=(0,l.Z)(o,u),b=S||y||"light";return h.palette=(0,a.Z)({text:{hint:"dark"===b?"rgba(255, 255, 255, 0.5)":"rgba(0, 0, 0, 0.38)"},mode:b,type:b},v),h}var m=r(8064),g=r(3146),h=r(931),f=r(7354);function p(e){for(var t=arguments.length,r=Array(t>1?t-1:0),n=1;n<t;n++)r[n-1]=arguments[n];return(0,h.Z)((0,f.Z)({unstable_strictMode:!0},e),...r)}let y=!1;function S(e){return y||(console.warn("MUI: createStyles from @mui/material/styles is deprecated.\nPlease use @mui/styles/createStyles"),y=!0),e}function v(e){return String(parseFloat(e)).length===String(e).length}function b(e){return String(e).match(/[\d.\-+]*\s*(.*)/)[1]||""}function C(e){return parseFloat(e)}function k(e){var t;let r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},{breakpoints:o=["sm","md","lg"],disableAlign:l=!1,factor:i=2,variants:c=["h1","h2","h3","h4","h5","h6","subtitle1","subtitle2","body1","body2","caption","button","overline"]}=r,s=(0,a.Z)({},e);s.typography=(0,a.Z)({},s.typography);let u=s.typography,d=(t=u.htmlFontSize,(e,r)=>{let n=b(e);if(n===r)return e;let o=C(e);"px"!==n&&("em"===n?o=C(e)*C(t):"rem"===n&&(o=C(e)*C(t)));let a=o;if("px"!==r){if("em"===r)a=o/C(t);else{if("rem"!==r)return e;a=o/C(t)}}return parseFloat(a.toFixed(5))+r}),m=o.map(e=>s.breakpoints.values[e]);return c.forEach(e=>{let t=u[e];if(!t)return;let r=parseFloat(d(t.fontSize,"rem"));if(r<=1)return;let{lineHeight:o}=t;if(!v(o)&&!l)throw Error((0,n.Z)(6));v(o)||(o=parseFloat(d(o,"rem"))/parseFloat(r));let c=null;l||(c=e=>(function(e){let{size:t,grid:r}=e,n=t-t%r,o=n+r;return t-n<o-t?n:o})({size:e,grid:function(e){let{lineHeight:t,pixels:r,htmlFontSize:n}=e;return r/(t*n)}({pixels:4,lineHeight:o,htmlFontSize:u.htmlFontSize})})),u[e]=(0,a.Z)({},t,function(e){let{cssProperty:t,min:r,max:n,unit:o="rem",breakpoints:a=[600,900,1200],transform:l=null}=e,i={[t]:"".concat(r).concat(o)},c=(n-r)/a[a.length-1];return a.forEach(e=>{let n=r+c*e;null!==l&&(n=l(n)),i["@media (min-width:".concat(e,"px)")]={[t]:"".concat(Math.round(1e4*n)/1e4).concat(o)}}),i}({cssProperty:"fontSize",min:1+(r-1)/i,max:r,unit:"rem",breakpoints:m,transform:c}))}),s}var x=r(3220),w=r(1691),Z=r(6269),A=r(5201);function T(e){let{props:t,name:r}=e;return(0,Z.Z)({props:t,name:r,defaultTheme:A.Z,themeId:o.Z})}var B=r(6210),_=r(2265);let I=_.createContext(null);function F(){return _.useContext(I)}var j="function"==typeof Symbol&&Symbol.for?Symbol.for("mui.nested"):"__THEME_NESTED__",E=r(7437),$=function(e){let{children:t,theme:r}=e,n=F(),o=_.useMemo(()=>{let e=null===n?r:"function"==typeof r?r(n):(0,a.Z)({},n,r);return null!=e&&(e[j]=null!==n),e},[r,n]);return(0,E.jsx)(I.Provider,{value:o,children:t})},P=r(5246),q=r(2570),L=r(7126),M=r(7804);let D={};function O(e,t,r){let n=arguments.length>3&&void 0!==arguments[3]&&arguments[3];return _.useMemo(()=>{let o=e&&t[e]||t;if("function"==typeof r){let l=r(o),i=e?(0,a.Z)({},t,{[e]:l}):l;return n?()=>i:i}return e?(0,a.Z)({},t,{[e]:r}):(0,a.Z)({},t,r)},[e,t,r,n])}var V=function(e){let{children:t,theme:r,themeId:n}=e,o=(0,q.Z)(D),a=F()||D,l=O(n,o,r),i=O(n,a,r,!0),c="rtl"===l.direction;return(0,E.jsx)($,{theme:i,children:(0,E.jsx)(P.T.Provider,{value:l,children:(0,E.jsx)(L.Z,{value:c,children:(0,E.jsx)(M.Z,{value:null==l?void 0:l.components,children:t})})})})};let z=["theme"];function H(e){let{theme:t}=e,r=(0,l.Z)(e,z),n=t[o.Z],i=n||t;return"function"!=typeof t&&(n&&!n.vars?i=(0,a.Z)({},n,{vars:null}):t&&!t.vars&&(i=(0,a.Z)({},t,{vars:null}))),(0,E.jsx)(V,(0,a.Z)({},r,{themeId:n?o.Z:void 0,theme:i}))}var R=r(1696);function N(){throw Error((0,n.Z)(14))}function U(){throw Error((0,n.Z)(15))}function W(){throw Error((0,n.Z)(16))}var K=r(8598);let G="mode",J="color-scheme",Q="data-color-scheme";function X(e){if("undefined"!=typeof window&&"system"===e)return window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"}function Y(e,t){return"light"===e.mode||"system"===e.mode&&"light"===e.systemMode?t("light"):"dark"===e.mode||"system"===e.mode&&"dark"===e.systemMode?t("dark"):void 0}function ee(e,t){let r;if("undefined"!=typeof window){try{(r=localStorage.getItem(e)||void 0)||localStorage.setItem(e,t)}catch(e){}return r||t}}let et=["colorSchemes","components","generateCssVars","cssVarPrefix"];var er=r(8720);function en(e){return(en="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function eo(e){var t=function(e,t){if("object"!=en(e)||!e)return e;var r=e[Symbol.toPrimitive];if(void 0!==r){var n=r.call(e,t||"default");if("object"!=en(n))return n;throw TypeError("@@toPrimitive must return a primitive value.")}return("string"===t?String:Number)(e)}(e,"string");return"symbol"==en(t)?t:t+""}let ea=(e,t,r,n=[])=>{let o=e;t.forEach((e,a)=>{a===t.length-1?Array.isArray(o)?o[Number(e)]=r:o&&"object"==typeof o&&(o[e]=r):o&&"object"==typeof o&&(o[e]||(o[e]=n.includes(e)?[]:{}),o=o[e])})},el=(e,t,r)=>{!function e(n,o=[],a=[]){Object.entries(n).forEach(([n,l])=>{r&&(!r||r([...o,n]))||null==l||("object"==typeof l&&Object.keys(l).length>0?e(l,[...o,n],Array.isArray(l)?[...a,n]:a):t([...o,n],l,a))})}(e)},ei=(e,t)=>"number"==typeof t?["lineHeight","fontWeight","opacity","zIndex"].some(t=>e.includes(t))||e[e.length-1].toLowerCase().indexOf("opacity")>=0?t:`${t}px`:t;function ec(e,t){let{prefix:r,shouldSkipGeneratingVar:n}=t||{},o={},a={},l={};return el(e,(e,t,i)=>{if(("string"==typeof t||"number"==typeof t)&&(!n||!n(e,t))){let n=`--${r?`${r}-`:""}${e.join("-")}`;Object.assign(o,{[n]:ei(e,t)}),ea(a,e,`var(${n})`,i),ea(l,e,`var(${n}, ${t})`,i)}},e=>"vars"===e[0]),{css:o,vars:a,varsWithDefaults:l}}let es=["colorSchemes","components","defaultColorScheme"];var eu=function(e,t){let{colorSchemes:r={},defaultColorScheme:n="light"}=e,{vars:o,css:i,varsWithDefaults:c}=ec((0,l.Z)(e,es),t),s=c,u={},{[n]:d}=r;if(Object.entries((0,l.Z)(r,[n].map(eo))||{}).forEach(([e,r])=>{let{vars:n,css:o,varsWithDefaults:a}=ec(r,t);s=(0,f.Z)(s,a),u[e]={css:o,vars:n}}),d){let{css:e,vars:r,varsWithDefaults:o}=ec(d,t);s=(0,f.Z)(s,o),u[n]={css:e,vars:r}}return{vars:s,generateCssVars:e=>{var r,n;if(!e){let r=(0,a.Z)({},i);return{css:r,vars:o,selector:(null==t||null==(n=t.getSelector)?void 0:n.call(t,e,r))||":root"}}let l=(0,a.Z)({},u[e].css);return{css:l,vars:u[e].vars,selector:(null==t||null==(r=t.getSelector)?void 0:r.call(t,e,l))||":root"}}}},ed=r(6445),em=r(4031);function eg(e){var t;return!!e[0].match(/(cssVarPrefix|typography|mixins|breakpoints|direction|transitions)/)||!!e[0].match(/sxConfig$/)||"palette"===e[0]&&!!(null!=(t=e[1])&&t.match(/(mode|contrastThreshold|tonalOffset)/))}var eh=r(6821);let ef=["colorSchemes","cssVarPrefix","shouldSkipGeneratingVar"],ep=["palette"],ey=[...Array(25)].map((e,t)=>{if(0===t)return;let r=(0,eh.Z)(t);return"linear-gradient(rgba(255 255 255 / ".concat(r,"), rgba(255 255 255 / ").concat(r,"))")});function eS(e,t,r){!e[t]&&r&&(e[t]=r)}function ev(e){return e&&e.startsWith("hsl")?(0,em.ve)(e):e}function eb(e,t){"".concat(t,"Channel") in e||(e["".concat(t,"Channel")]=(0,em.LR)(ev(e[t]),"MUI: Can't create `palette.".concat(t,"Channel` because `palette.").concat(t,"` is not one of these formats: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla(), color().")+"\n"+"To suppress this warning, you need to explicitly provide the `palette.".concat(t,'Channel` as a string (in rgb format, for example "12 12 12") or undefined if you want to remove the channel token.')))}let eC=e=>{try{return e()}catch(e){}},ek=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"mui";return function(e=""){return(t,...r)=>`var(--${e?`${e}-`:""}${t}${function t(...r){if(!r.length)return"";let n=r[0];return"string"!=typeof n||n.match(/(#|\(|\)|(-?(\d*\.)?\d+)(px|em|%|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc))|^(-?(\d*\.)?\d+)$|(\d+ \d+ \d+)/)?`, ${n}`:`, var(--${e?`${e}-`:""}${n}${t(...r.slice(1))})`}(...r)})`}(e)};function ex(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};for(var t,r,n,o,i,c,s=arguments.length,u=Array(s>1?s-1:0),d=1;d<s;d++)u[d-1]=arguments[d];let{colorSchemes:m={},cssVarPrefix:g="mui",shouldSkipGeneratingVar:p=eg}=e,y=(0,l.Z)(e,ef),S=ek(g),v=(0,h.Z)((0,a.Z)({},y,m.light&&{palette:null==(t=m.light)?void 0:t.palette})),{palette:b}=v,C=(0,l.Z)(v,ep),{palette:k}=(0,h.Z)({palette:(0,a.Z)({mode:"dark"},null==(r=m.dark)?void 0:r.palette)}),x=(0,a.Z)({},C,{cssVarPrefix:g,getCssVar:S,colorSchemes:(0,a.Z)({},m,{light:(0,a.Z)({},m.light,{palette:b,opacity:(0,a.Z)({inputPlaceholder:.42,inputUnderline:.42,switchTrackDisabled:.12,switchTrack:.38},null==(n=m.light)?void 0:n.opacity),overlays:(null==(o=m.light)?void 0:o.overlays)||[]}),dark:(0,a.Z)({},m.dark,{palette:k,opacity:(0,a.Z)({inputPlaceholder:.5,inputUnderline:.7,switchTrackDisabled:.2,switchTrack:.3},null==(i=m.dark)?void 0:i.opacity),overlays:(null==(c=m.dark)?void 0:c.overlays)||ey})})});Object.keys(x.colorSchemes).forEach(e=>{let t=x.colorSchemes[e].palette,r=e=>{let r=e.split("-"),n=r[1],o=r[2];return S(e,t[n][o])};if("light"===e?(eS(t.common,"background","#fff"),eS(t.common,"onBackground","#000")):(eS(t.common,"background","#000"),eS(t.common,"onBackground","#fff")),function(e,t){t.forEach(t=>{e[t]||(e[t]={})})}(t,["Alert","AppBar","Avatar","Button","Chip","FilledInput","LinearProgress","Skeleton","Slider","SnackbarContent","SpeedDialAction","StepConnector","StepContent","Switch","TableCell","Tooltip"]),"light"===e){eS(t.Alert,"errorColor",(0,em.q8)(t.error.light,.6)),eS(t.Alert,"infoColor",(0,em.q8)(t.info.light,.6)),eS(t.Alert,"successColor",(0,em.q8)(t.success.light,.6)),eS(t.Alert,"warningColor",(0,em.q8)(t.warning.light,.6)),eS(t.Alert,"errorFilledBg",r("palette-error-main")),eS(t.Alert,"infoFilledBg",r("palette-info-main")),eS(t.Alert,"successFilledBg",r("palette-success-main")),eS(t.Alert,"warningFilledBg",r("palette-warning-main")),eS(t.Alert,"errorFilledColor",eC(()=>b.getContrastText(t.error.main))),eS(t.Alert,"infoFilledColor",eC(()=>b.getContrastText(t.info.main))),eS(t.Alert,"successFilledColor",eC(()=>b.getContrastText(t.success.main))),eS(t.Alert,"warningFilledColor",eC(()=>b.getContrastText(t.warning.main))),eS(t.Alert,"errorStandardBg",(0,em.ux)(t.error.light,.9)),eS(t.Alert,"infoStandardBg",(0,em.ux)(t.info.light,.9)),eS(t.Alert,"successStandardBg",(0,em.ux)(t.success.light,.9)),eS(t.Alert,"warningStandardBg",(0,em.ux)(t.warning.light,.9)),eS(t.Alert,"errorIconColor",r("palette-error-main")),eS(t.Alert,"infoIconColor",r("palette-info-main")),eS(t.Alert,"successIconColor",r("palette-success-main")),eS(t.Alert,"warningIconColor",r("palette-warning-main")),eS(t.AppBar,"defaultBg",r("palette-grey-100")),eS(t.Avatar,"defaultBg",r("palette-grey-400")),eS(t.Button,"inheritContainedBg",r("palette-grey-300")),eS(t.Button,"inheritContainedHoverBg",r("palette-grey-A100")),eS(t.Chip,"defaultBorder",r("palette-grey-400")),eS(t.Chip,"defaultAvatarColor",r("palette-grey-700")),eS(t.Chip,"defaultIconColor",r("palette-grey-700")),eS(t.FilledInput,"bg","rgba(0, 0, 0, 0.06)"),eS(t.FilledInput,"hoverBg","rgba(0, 0, 0, 0.09)"),eS(t.FilledInput,"disabledBg","rgba(0, 0, 0, 0.12)"),eS(t.LinearProgress,"primaryBg",(0,em.ux)(t.primary.main,.62)),eS(t.LinearProgress,"secondaryBg",(0,em.ux)(t.secondary.main,.62)),eS(t.LinearProgress,"errorBg",(0,em.ux)(t.error.main,.62)),eS(t.LinearProgress,"infoBg",(0,em.ux)(t.info.main,.62)),eS(t.LinearProgress,"successBg",(0,em.ux)(t.success.main,.62)),eS(t.LinearProgress,"warningBg",(0,em.ux)(t.warning.main,.62)),eS(t.Skeleton,"bg","rgba(".concat(r("palette-text-primaryChannel")," / 0.11)")),eS(t.Slider,"primaryTrack",(0,em.ux)(t.primary.main,.62)),eS(t.Slider,"secondaryTrack",(0,em.ux)(t.secondary.main,.62)),eS(t.Slider,"errorTrack",(0,em.ux)(t.error.main,.62)),eS(t.Slider,"infoTrack",(0,em.ux)(t.info.main,.62)),eS(t.Slider,"successTrack",(0,em.ux)(t.success.main,.62)),eS(t.Slider,"warningTrack",(0,em.ux)(t.warning.main,.62));let e=(0,em.fk)(t.background.default,.8);eS(t.SnackbarContent,"bg",e),eS(t.SnackbarContent,"color",eC(()=>b.getContrastText(e))),eS(t.SpeedDialAction,"fabHoverBg",(0,em.fk)(t.background.paper,.15)),eS(t.StepConnector,"border",r("palette-grey-400")),eS(t.StepContent,"border",r("palette-grey-400")),eS(t.Switch,"defaultColor",r("palette-common-white")),eS(t.Switch,"defaultDisabledColor",r("palette-grey-100")),eS(t.Switch,"primaryDisabledColor",(0,em.ux)(t.primary.main,.62)),eS(t.Switch,"secondaryDisabledColor",(0,em.ux)(t.secondary.main,.62)),eS(t.Switch,"errorDisabledColor",(0,em.ux)(t.error.main,.62)),eS(t.Switch,"infoDisabledColor",(0,em.ux)(t.info.main,.62)),eS(t.Switch,"successDisabledColor",(0,em.ux)(t.success.main,.62)),eS(t.Switch,"warningDisabledColor",(0,em.ux)(t.warning.main,.62)),eS(t.TableCell,"border",(0,em.ux)((0,em.zp)(t.divider,1),.88)),eS(t.Tooltip,"bg",(0,em.zp)(t.grey[700],.92))}else{eS(t.Alert,"errorColor",(0,em.ux)(t.error.light,.6)),eS(t.Alert,"infoColor",(0,em.ux)(t.info.light,.6)),eS(t.Alert,"successColor",(0,em.ux)(t.success.light,.6)),eS(t.Alert,"warningColor",(0,em.ux)(t.warning.light,.6)),eS(t.Alert,"errorFilledBg",r("palette-error-dark")),eS(t.Alert,"infoFilledBg",r("palette-info-dark")),eS(t.Alert,"successFilledBg",r("palette-success-dark")),eS(t.Alert,"warningFilledBg",r("palette-warning-dark")),eS(t.Alert,"errorFilledColor",eC(()=>k.getContrastText(t.error.dark))),eS(t.Alert,"infoFilledColor",eC(()=>k.getContrastText(t.info.dark))),eS(t.Alert,"successFilledColor",eC(()=>k.getContrastText(t.success.dark))),eS(t.Alert,"warningFilledColor",eC(()=>k.getContrastText(t.warning.dark))),eS(t.Alert,"errorStandardBg",(0,em.q8)(t.error.light,.9)),eS(t.Alert,"infoStandardBg",(0,em.q8)(t.info.light,.9)),eS(t.Alert,"successStandardBg",(0,em.q8)(t.success.light,.9)),eS(t.Alert,"warningStandardBg",(0,em.q8)(t.warning.light,.9)),eS(t.Alert,"errorIconColor",r("palette-error-main")),eS(t.Alert,"infoIconColor",r("palette-info-main")),eS(t.Alert,"successIconColor",r("palette-success-main")),eS(t.Alert,"warningIconColor",r("palette-warning-main")),eS(t.AppBar,"defaultBg",r("palette-grey-900")),eS(t.AppBar,"darkBg",r("palette-background-paper")),eS(t.AppBar,"darkColor",r("palette-text-primary")),eS(t.Avatar,"defaultBg",r("palette-grey-600")),eS(t.Button,"inheritContainedBg",r("palette-grey-800")),eS(t.Button,"inheritContainedHoverBg",r("palette-grey-700")),eS(t.Chip,"defaultBorder",r("palette-grey-700")),eS(t.Chip,"defaultAvatarColor",r("palette-grey-300")),eS(t.Chip,"defaultIconColor",r("palette-grey-300")),eS(t.FilledInput,"bg","rgba(255, 255, 255, 0.09)"),eS(t.FilledInput,"hoverBg","rgba(255, 255, 255, 0.13)"),eS(t.FilledInput,"disabledBg","rgba(255, 255, 255, 0.12)"),eS(t.LinearProgress,"primaryBg",(0,em.q8)(t.primary.main,.5)),eS(t.LinearProgress,"secondaryBg",(0,em.q8)(t.secondary.main,.5)),eS(t.LinearProgress,"errorBg",(0,em.q8)(t.error.main,.5)),eS(t.LinearProgress,"infoBg",(0,em.q8)(t.info.main,.5)),eS(t.LinearProgress,"successBg",(0,em.q8)(t.success.main,.5)),eS(t.LinearProgress,"warningBg",(0,em.q8)(t.warning.main,.5)),eS(t.Skeleton,"bg","rgba(".concat(r("palette-text-primaryChannel")," / 0.13)")),eS(t.Slider,"primaryTrack",(0,em.q8)(t.primary.main,.5)),eS(t.Slider,"secondaryTrack",(0,em.q8)(t.secondary.main,.5)),eS(t.Slider,"errorTrack",(0,em.q8)(t.error.main,.5)),eS(t.Slider,"infoTrack",(0,em.q8)(t.info.main,.5)),eS(t.Slider,"successTrack",(0,em.q8)(t.success.main,.5)),eS(t.Slider,"warningTrack",(0,em.q8)(t.warning.main,.5));let e=(0,em.fk)(t.background.default,.98);eS(t.SnackbarContent,"bg",e),eS(t.SnackbarContent,"color",eC(()=>k.getContrastText(e))),eS(t.SpeedDialAction,"fabHoverBg",(0,em.fk)(t.background.paper,.15)),eS(t.StepConnector,"border",r("palette-grey-600")),eS(t.StepContent,"border",r("palette-grey-600")),eS(t.Switch,"defaultColor",r("palette-grey-300")),eS(t.Switch,"defaultDisabledColor",r("palette-grey-600")),eS(t.Switch,"primaryDisabledColor",(0,em.q8)(t.primary.main,.55)),eS(t.Switch,"secondaryDisabledColor",(0,em.q8)(t.secondary.main,.55)),eS(t.Switch,"errorDisabledColor",(0,em.q8)(t.error.main,.55)),eS(t.Switch,"infoDisabledColor",(0,em.q8)(t.info.main,.55)),eS(t.Switch,"successDisabledColor",(0,em.q8)(t.success.main,.55)),eS(t.Switch,"warningDisabledColor",(0,em.q8)(t.warning.main,.55)),eS(t.TableCell,"border",(0,em.q8)((0,em.zp)(t.divider,1),.68)),eS(t.Tooltip,"bg",(0,em.zp)(t.grey[700],.92))}eb(t.background,"default"),eb(t.background,"paper"),eb(t.common,"background"),eb(t.common,"onBackground"),eb(t,"divider"),Object.keys(t).forEach(e=>{let r=t[e];r&&"object"==typeof r&&(r.main&&eS(t[e],"mainChannel",(0,em.LR)(ev(r.main))),r.light&&eS(t[e],"lightChannel",(0,em.LR)(ev(r.light))),r.dark&&eS(t[e],"darkChannel",(0,em.LR)(ev(r.dark))),r.contrastText&&eS(t[e],"contrastTextChannel",(0,em.LR)(ev(r.contrastText))),"text"===e&&(eb(t[e],"primary"),eb(t[e],"secondary")),"action"===e&&(r.active&&eb(t[e],"active"),r.selected&&eb(t[e],"selected")))})});let{vars:w,generateCssVars:Z}=eu(x=u.reduce((e,t)=>(0,f.Z)(e,t),x),{prefix:g,shouldSkipGeneratingVar:p});return x.vars=w,x.generateCssVars=Z,x.shouldSkipGeneratingVar=p,x.unstable_sxConfig=(0,a.Z)({},ed.Z,null==y?void 0:y.unstable_sxConfig),x.unstable_sx=function(e){return(0,er.Z)({sx:e,theme:this})},x}var ew=r(4792),eZ=e=>[...[...Array(24)].map((t,r)=>"--".concat(e?"".concat(e,"-"):"","overlays-").concat(r+1)),"--".concat(e?"".concat(e,"-"):"","palette-AppBar-darkBg"),"--".concat(e?"".concat(e,"-"):"","palette-AppBar-darkColor")];let eA=ex(),{CssVarsProvider:eT,useColorScheme:eB,getInitColorSchemeScript:e_}=function(e){let{themeId:t,theme:r={},attribute:o=Q,modeStorageKey:i=G,colorSchemeStorageKey:c=J,defaultMode:s="light",defaultColorScheme:u,disableTransitionOnChange:d=!1,resolveTheme:m,excludeVariablesFromRoot:g}=e;r.colorSchemes&&("string"!=typeof u||r.colorSchemes[u])&&("object"!=typeof u||r.colorSchemes[null==u?void 0:u.light])&&("object"!=typeof u||r.colorSchemes[null==u?void 0:u.dark])||console.error(`MUI: \`${u}\` does not exist in \`theme.colorSchemes\`.`);let h=_.createContext(void 0),p="string"==typeof u?u:u.light,y="string"==typeof u?u:u.dark;return{CssVarsProvider:function(e){let{children:n,theme:p=r,modeStorageKey:y=i,colorSchemeStorageKey:S=c,attribute:v=o,defaultMode:b=s,defaultColorScheme:C=u,disableTransitionOnChange:k=d,storageWindow:x="undefined"==typeof window?void 0:window,documentNode:w="undefined"==typeof document?void 0:document,colorSchemeNode:Z="undefined"==typeof document?void 0:document.documentElement,colorSchemeSelector:A=":root",disableNestedContext:T=!1,disableStyleSheetGeneration:B=!1}=e,I=_.useRef(!1),j=F(),$=_.useContext(h),P=!!$&&!T,q=p[t],L=q||p,{colorSchemes:M={},components:D={},generateCssVars:O=()=>({vars:{},css:{}}),cssVarPrefix:z}=L,H=(0,l.Z)(L,et),R=Object.keys(M),N="string"==typeof C?C:C.light,U="string"==typeof C?C:C.dark,{mode:W,setMode:Q,systemMode:er,lightColorScheme:en,darkColorScheme:eo,colorScheme:ea,setColorScheme:el}=function(e){let{defaultMode:t="light",defaultLightColorScheme:r,defaultDarkColorScheme:n,supportedColorSchemes:o=[],modeStorageKey:l=G,colorSchemeStorageKey:i=J,storageWindow:c="undefined"==typeof window?void 0:window}=e,s=o.join(","),[u,d]=_.useState(()=>{let e=ee(l,t),o=ee("".concat(i,"-light"),r),a=ee("".concat(i,"-dark"),n);return{mode:e,systemMode:X(e),lightColorScheme:o,darkColorScheme:a}}),m=Y(u,e=>"light"===e?u.lightColorScheme:"dark"===e?u.darkColorScheme:void 0),g=_.useCallback(e=>{d(r=>{if(e===r.mode)return r;let n=null!=e?e:t;try{localStorage.setItem(l,n)}catch(e){}return(0,a.Z)({},r,{mode:n,systemMode:X(n)})})},[l,t]),h=_.useCallback(e=>{e?"string"==typeof e?e&&!s.includes(e)?console.error("`".concat(e,"` does not exist in `theme.colorSchemes`.")):d(t=>{let r=(0,a.Z)({},t);return Y(t,t=>{try{localStorage.setItem("".concat(i,"-").concat(t),e)}catch(e){}"light"===t&&(r.lightColorScheme=e),"dark"===t&&(r.darkColorScheme=e)}),r}):d(t=>{let o=(0,a.Z)({},t),l=null===e.light?r:e.light,c=null===e.dark?n:e.dark;if(l){if(s.includes(l)){o.lightColorScheme=l;try{localStorage.setItem("".concat(i,"-light"),l)}catch(e){}}else console.error("`".concat(l,"` does not exist in `theme.colorSchemes`."))}if(c){if(s.includes(c)){o.darkColorScheme=c;try{localStorage.setItem("".concat(i,"-dark"),c)}catch(e){}}else console.error("`".concat(c,"` does not exist in `theme.colorSchemes`."))}return o}):d(e=>{try{localStorage.setItem("".concat(i,"-light"),r),localStorage.setItem("".concat(i,"-dark"),n)}catch(e){}return(0,a.Z)({},e,{lightColorScheme:r,darkColorScheme:n})})},[s,i,r,n]),f=_.useCallback(e=>{"system"===u.mode&&d(t=>{let r=null!=e&&e.matches?"dark":"light";return t.systemMode===r?t:(0,a.Z)({},t,{systemMode:r})})},[u.mode]),p=_.useRef(f);return p.current=f,_.useEffect(()=>{let e=function(){for(var e=arguments.length,t=Array(e),r=0;r<e;r++)t[r]=arguments[r];return p.current(...t)},t=window.matchMedia("(prefers-color-scheme: dark)");return t.addListener(e),e(t),()=>{t.removeListener(e)}},[]),_.useEffect(()=>{if(c){let e=e=>{let r=e.newValue;"string"==typeof e.key&&e.key.startsWith(i)&&(!r||s.match(r))&&(e.key.endsWith("light")&&h({light:r}),e.key.endsWith("dark")&&h({dark:r})),e.key===l&&(!r||["light","dark","system"].includes(r))&&g(r||t)};return c.addEventListener("storage",e),()=>{c.removeEventListener("storage",e)}}},[h,g,l,i,s,t,c]),(0,a.Z)({},u,{colorScheme:m,setMode:g,setColorScheme:h})}({supportedColorSchemes:R,defaultLightColorScheme:N,defaultDarkColorScheme:U,modeStorageKey:y,colorSchemeStorageKey:S,defaultMode:b,storageWindow:x}),ei=W,ec=ea;P&&(ei=$.mode,ec=$.colorScheme);let es=ei||("system"===b?s:b),eu=ec||("dark"===es?U:N),{css:ed,vars:em}=O(),eg=(0,a.Z)({},H,{components:D,colorSchemes:M,cssVarPrefix:z,vars:em,getColorSchemeSelector:e=>`[${v}="${e}"] &`}),eh={},ef={};Object.entries(M).forEach(([e,t])=>{let{css:r,vars:n}=O(e);if(eg.vars=(0,f.Z)(eg.vars,n),e===eu&&(Object.keys(t).forEach(e=>{t[e]&&"object"==typeof t[e]?eg[e]=(0,a.Z)({},eg[e],t[e]):eg[e]=t[e]}),eg.palette&&(eg.palette.colorScheme=e)),e===("string"==typeof C?C:"dark"===b?C.dark:C.light)){if(g){let t={};g(z).forEach(e=>{t[e]=r[e],delete r[e]}),eh[`[${v}="${e}"]`]=t}eh[`${A}, [${v}="${e}"]`]=r}else ef[`${":root"===A?"":A}[${v}="${e}"]`]=r}),eg.vars=(0,f.Z)(eg.vars,em),_.useEffect(()=>{ec&&Z&&Z.setAttribute(v,ec)},[ec,v,Z]),_.useEffect(()=>{let e;if(k&&I.current&&w){let t=w.createElement("style");t.appendChild(w.createTextNode("*{-webkit-transition:none!important;-moz-transition:none!important;-o-transition:none!important;-ms-transition:none!important;transition:none!important}")),w.head.appendChild(t),window.getComputedStyle(w.body),e=setTimeout(()=>{w.head.removeChild(t)},1)}return()=>{clearTimeout(e)}},[ec,k,w]),_.useEffect(()=>(I.current=!0,()=>{I.current=!1}),[]);let ep=_.useMemo(()=>({allColorSchemes:R,colorScheme:ec,darkColorScheme:eo,lightColorScheme:en,mode:ei,setColorScheme:el,setMode:Q,systemMode:er}),[R,ec,eo,en,ei,el,Q,er]),ey=!0;(B||P&&(null==j?void 0:j.cssVarPrefix)===z)&&(ey=!1);let eS=(0,E.jsxs)(_.Fragment,{children:[ey&&(0,E.jsxs)(_.Fragment,{children:[(0,E.jsx)(K.Z,{styles:{[A]:ed}}),(0,E.jsx)(K.Z,{styles:eh}),(0,E.jsx)(K.Z,{styles:ef})]}),(0,E.jsx)(V,{themeId:q?t:void 0,theme:m?m(eg):eg,children:n})]});return P?eS:(0,E.jsx)(h.Provider,{value:ep,children:eS})},useColorScheme:()=>{let e=_.useContext(h);if(!e)throw Error((0,n.Z)(19));return e},getInitColorSchemeScript:e=>(function(e){let{defaultMode:t="light",defaultLightColorScheme:r="light",defaultDarkColorScheme:n="dark",modeStorageKey:o=G,colorSchemeStorageKey:a=J,attribute:l=Q,colorSchemeNode:i="document.documentElement",nonce:c}=e||{};return(0,E.jsx)("script",{suppressHydrationWarning:!0,nonce:"undefined"==typeof window?c:"",dangerouslySetInnerHTML:{__html:`(function() {
2
+ try {
3
+ var mode = localStorage.getItem('${o}') || '${t}';
4
+ var colorScheme = '';
5
+ if (mode === 'system') {
6
+ // handle system mode
7
+ var mql = window.matchMedia('(prefers-color-scheme: dark)');
8
+ if (mql.matches) {
9
+ colorScheme = localStorage.getItem('${a}-dark') || '${n}';
10
+ } else {
11
+ colorScheme = localStorage.getItem('${a}-light') || '${r}';
12
+ }
13
+ }
14
+ if (mode === 'light') {
15
+ colorScheme = localStorage.getItem('${a}-light') || '${r}';
16
+ }
17
+ if (mode === 'dark') {
18
+ colorScheme = localStorage.getItem('${a}-dark') || '${n}';
19
+ }
20
+ if (colorScheme) {
21
+ ${i}.setAttribute('${l}', colorScheme);
22
+ }
23
+ } catch(e){}})();`}},"mui-color-scheme-init")})((0,a.Z)({attribute:o,colorSchemeStorageKey:c,defaultMode:s,defaultLightColorScheme:p,defaultDarkColorScheme:y,modeStorageKey:i},e))}}({themeId:o.Z,theme:eA,attribute:"data-mui-color-scheme",colorSchemeStorageKey:"mui-color-scheme",modeStorageKey:"mui-mode",defaultColorScheme:{light:"light",dark:"dark"},resolveTheme:e=>{let t=(0,a.Z)({},e,{typography:(0,ew.Z)(e.palette,e.typography)});return t.unstable_sx=function(e){return(0,er.Z)({sx:e,theme:this})},t},excludeVariablesFromRoot:eZ}),eI=e_;var eF=r(3363);function ej(){throw Error((0,n.Z)(20))}},9974:function(e){e.exports={style:{fontFamily:"'__Inter_e8ce0c', '__Inter_Fallback_e8ce0c'",fontStyle:"normal"},className:"__className_e8ce0c"}},9334:function(e){e.exports={style:{fontFamily:"'__Noto_Sans_JP_3b94a0', '__Noto_Sans_JP_Fallback_3b94a0'",fontStyle:"normal"},className:"__className_3b94a0"}}}]);