Spaces:
Sleeping
Sleeping
Commit
·
01ea955
0
Parent(s):
Initial commit with LFS setup
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .gitattributes +5 -0
- .gitignore +44 -0
- Dockerfile +36 -0
- bert/bert_models.json +12 -0
- bert/deberta-v2-large-japanese-char-wwm/.gitattributes +34 -0
- bert/deberta-v2-large-japanese-char-wwm/README.md +89 -0
- bert/deberta-v2-large-japanese-char-wwm/config.json +37 -0
- bert/deberta-v2-large-japanese-char-wwm/special_tokens_map.json +7 -0
- bert/deberta-v2-large-japanese-char-wwm/tokenizer_config.json +19 -0
- bert/deberta-v2-large-japanese-char-wwm/vocab.txt +0 -0
- config.py +307 -0
- configs/config.json +73 -0
- configs/config_jp_extra.json +80 -0
- configs/default_paths.yml +8 -0
- configs/paths.yml +8 -0
- default_config.yml +70 -0
- frontend/next-env.d.ts +5 -0
- frontend/next.config.js +11 -0
- frontend/package-lock.json +0 -0
- frontend/package.json +43 -0
- frontend/public/images/kariusagi.png +3 -0
- frontend/src/app/globals.css +3 -0
- frontend/src/app/layout.tsx +31 -0
- frontend/src/app/page.tsx +17 -0
- frontend/src/components/AccentEditor.tsx +108 -0
- frontend/src/components/AlertPopup.tsx +28 -0
- frontend/src/components/DictionaryDialog.tsx +541 -0
- frontend/src/components/EditorContainer.tsx +668 -0
- frontend/src/components/LineSetting.tsx +317 -0
- frontend/src/components/SimpleBackdrop.tsx +17 -0
- frontend/src/components/TermOfUse.tsx +155 -0
- frontend/src/components/TextEditor.tsx +25 -0
- frontend/src/contexts/PopupProvider.tsx +70 -0
- frontend/src/hooks/useWindowSize.ts +20 -0
- frontend/src/theme.ts +111 -0
- frontend/src/utils/api.ts +40 -0
- frontend/tailwind.config.ts +20 -0
- frontend/tsconfig.json +40 -0
- initialize.py +107 -0
- model_assets/kariusagi/config.json +120 -0
- model_assets/kariusagi/style_vectors.npy +0 -0
- requirements.txt +24 -0
- server_editor.py +450 -0
- static/404.html +1 -0
- static/404/index.html +1 -0
- static/_next/static/8q7amU-bnfgJwF-L0ePLq/_buildManifest.js +1 -0
- static/_next/static/8q7amU-bnfgJwF-L0ePLq/_ssgManifest.js +1 -0
- static/_next/static/chunks/117-b970401bd8e4e4e0.js +0 -0
- static/_next/static/chunks/313-0e35468afc829c33.js +0 -0
- 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
|
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,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";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,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";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"}}}]);
|