├── .dockerignore ├── .gitignore ├── Dockerfile ├── README.md ├── app.py ├── cfg ├── __init__.py ├── cn_tn.py ├── dubb.py └── en_tn.py ├── requirements.txt ├── start.bat ├── static ├── bootstrap.bundle.min.js ├── bootstrap.min.css ├── jquery.min.js ├── pre.jpg ├── script.js └── style.css └── templates └── index.html /.dockerignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | data/ 3 | temp 4 | models 5 | ffmpeg 6 | runtime 7 | logs -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.srt 3 | *.7z 4 | *.zip 5 | .idea 6 | tmp 7 | temp 8 | logs 9 | ffmpeg 10 | models 11 | runtime 12 | venv 13 | 14 | __pycache__ 15 | *.spec 16 | *.bak 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM pytorch/torchserve:0.12.0-gpu as builder 2 | 3 | USER root 4 | 5 | RUN apt-get update && apt-get install -y ffmpeg 6 | 7 | 8 | # 设置工作目录 9 | WORKDIR /app 10 | 11 | # 将应用代码复制到工作目录 12 | COPY . . 13 | 14 | # 安装依赖 15 | RUN pip install --no-cache-dir -r requirements.txt 16 | 17 | # 设置环境变量 18 | ENV FLASK_APP app.py 19 | # 设置 Flask 监听地址,因为容器内不使用0.0.0.0 20 | ENV FLASK_RUN_HOST 0.0.0.0 21 | ENV FLASK_RUN_PORT 5066 22 | 23 | # 暴露端口 24 | EXPOSE 5066 25 | 26 | # 启动应用 27 | CMD ["python","app.py"] 28 | 29 | 30 | # 临时文件目录 -v temp:/app/temp 31 | # 模型目录 -v models:/app/models 32 | # 日志目录 -v logs:/app/logs -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 这是一个用于kokoro TTS项目的webui和api项目,支持中文、英文、日语、法语、意大利语、葡萄牙、西班牙、印地语共8种语言配音 3 | 4 | ## web界面 5 | 6 | ![](./static/pre.jpg) 7 | 8 | 启动后默认ui地址: http://127.0.0.1:5066 9 | 10 | - 支持对文字和SRT字幕进行配音 11 | - 支持在线试听和下载 12 | - 支持对齐字幕 13 | 14 | 15 | ## 兼容 OpenAI API 16 | 17 | api兼容 OpenAI TTS 18 | 19 | 启动后默认API地址:`http://127.0.0.1:5066/v1/audio/speech` 20 | 21 | 请求方法:`POST` 22 | 请求数据: 23 | `application/json` 24 | 25 | ``` 26 | { 27 | input:需要配音的文字, 28 | voice:配音角色, 29 | speed:语速默认1.0 30 | } 31 | ``` 32 | 33 | 成功返回mp3音频数据 34 | 35 | **OpenAI SDK 使用示例** 36 | 37 | ``` 38 | from openai import OpenAI 39 | client = OpenAI( 40 | api_key='123456', 41 | base_url='http://127.0.0.1:5066/v1' 42 | ) 43 | 44 | try: 45 | response = client.audio.speech.create( 46 | model='tts-1', 47 | input='你好啊,亲爱的朋友们', 48 | voice='zf_xiaobei', 49 | response_format='mp3', 50 | speed=1.0 51 | ) 52 | with open('./test_openai.mp3', 'wb') as f: 53 | f.write(response.content) 54 | print("MP3 file saved successfully to test_openai.mp3") 55 | except Exception as e: 56 | print(f"An error occurred: {e}") 57 | 58 | ``` 59 | 60 | ## 角色列表 61 | 62 | 英语配音角色: 63 | 64 | ``` 65 | 66 | af_alloy 67 | af_aoede 68 | af_bella 69 | af_jessica 70 | af_kore 71 | af_nicole 72 | af_nova 73 | af_river 74 | af_sarah 75 | af_sky 76 | am_adam 77 | am_echo 78 | am_eric 79 | am_fenrir 80 | am_liam 81 | am_michael 82 | am_onyx 83 | am_puck 84 | am_santa 85 | bf_alice 86 | bf_emma 87 | bf_isabella 88 | bf_lily 89 | bm_daniel 90 | bm_fable 91 | bm_george 92 | bm_lewis 93 | 94 | ``` 95 | 96 | 中文角色: 97 | 98 | ``` 99 | zf_xiaobei 100 | zf_xiaoni 101 | zf_xiaoxiao 102 | zf_xiaoyi 103 | zm_yunjian 104 | zm_yunxi 105 | zm_yunxia 106 | zm_yunyang 107 | ``` 108 | 109 | 日语角色: 110 | ``` 111 | jf_alpha 112 | jf_gongitsune 113 | jf_nezumi 114 | jf_tebukuro 115 | jm_kumo 116 | ``` 117 | 118 | 法语角色: `ff_siwis ` 119 | 120 | 意大利语角色: `if_sara,im_nicola` 121 | 122 | 印地语角色:`hf_alpha,hf_beta,hm_omega,hm_psi` 123 | 124 | 西班牙语角色:`ef_dora,em_alex,em_santa` 125 | 126 | 葡萄牙语角色:`pf_dora,pm_alex,pm_santa` 127 | 128 | 129 | ## 安装方法 130 | 131 | ### Windows 132 | 133 | win10/11 可直接下载整合包(https://github.com/jianchang512/kokoro-uiapi/releases),双击 `start.bat` 即可启动,若需GPU加速,请确保拥有NVIDIA显卡并安装CUDA12 134 | 135 | ### Linux/MacOS 136 | 137 | 首先确保系统已安装python3.8+,建议 3.10-3.11 138 | 139 | > Linux上使用 `apt install ffmpeg` 或 `yum install ffmpeg` 预先安装 ffmpeg 140 | > 141 | > MacOS使用 `brew install ffmpeg` 安装ffmpeg 142 | 143 | 1. 拉取源码 `git clone https://github.com/jianchang512/kokoro-uiapi` 144 | 2. 创建虚拟环境并激活 145 | ``` 146 | cd kokoro-uiapi 147 | python3 -m venv venv 148 | . venv/bin/activate 149 | 150 | ``` 151 | 3. 安装依赖 `pip3 install -r requirements.txt` 152 | 4. 启动 `python3 app.py` 153 | 154 | 155 | ## 代理VPN 156 | 157 | 源码部署方式需从 huggingface.co 上下载音色pt文件,需提前设置全局代理或系统代理,确保可访问 158 | 159 | 也可以提前下载好模型解压到 app.py 所在目录下。 160 | 161 | 模型下载地址 https://github.com/jianchang512/kokoro-uiapi/releases/download/0.1/moxing--jieya--dao--app.py--mulu.7z 162 | 163 | ## Credit 164 | 165 | - [Kokoro-82M](https://github.com/hexgrad/kokoro) -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, jsonify,send_file,render_template,send_from_directory 2 | from flask_cors import CORS 3 | from pathlib import Path 4 | import os,time,random,re,threading 5 | from waitress import serve 6 | import cfg 7 | 8 | 9 | app = Flask(__name__, template_folder="templates") 10 | 11 | @app.route("/static/") 12 | def send_data_file(filename): 13 | return send_from_directory(cfg.ROOT+f'/static', filename) 14 | 15 | @app.route("/temp/") 16 | def send_data_file2(filename): 17 | return send_from_directory(cfg.ROOT+f'/temp', filename) 18 | 19 | 20 | @app.route('/v1/audio/speech', methods=['POST']) 21 | def audio_speech(): 22 | """ 23 | 兼容 OpenAI /v1/audio/speech API 的接口 24 | """ 25 | if not request.is_json: 26 | return jsonify({"error": "请求必须是 JSON 格式"}), 400 27 | 28 | data = request.get_json() 29 | 30 | # 检查请求中是否包含必要的参数 31 | if 'input' not in data or 'voice' not in data: 32 | return jsonify({"error": "请求缺少必要的参数: input, voice"}), 400 33 | 34 | 35 | text = data.get('input') 36 | 37 | voice = data.get('voice','') 38 | 39 | 40 | speed = float(data.get('speed',1.0)) 41 | 42 | try: 43 | filename=cfg.dubb.process_synthesize_task( 44 | text, 45 | voice, 46 | speed, 47 | False, 48 | False) 49 | return send_file(filename, mimetype='audio/mpeg') 50 | except Exception as e: 51 | return jsonify({"error": {"message": f"{e}", "type": e.__class__.__name__, "param": f'speed={speed},voice={voice},input={text}', "code": 400}}), 500 52 | 53 | 54 | 55 | # 语音合成 56 | @app.route("/synthesize", methods=["POST"]) 57 | def synthesize(): 58 | data = request.get_json() 59 | text = data.get("text") 60 | voice = data.get("voice") 61 | speed = float(data.get("speed", 1.0)) 62 | 63 | keep_spacing = bool(data.get("keep_spacing", False)) 64 | auto_speed = bool(data.get("auto_speed", False)) 65 | if re.match( 66 | r"^1\s*[\r\n]+\s*\d{1,2}:\d{1,2}:\d{1,2}(\,\d{1,3})?\s*-->\s*\d{1,2}:\d{1,2}:\d{1,2}(\,\d{1,3})?", 67 | text.strip(), 68 | ): 69 | text = cfg.get_subtitle_from_srt(text, is_file=False) 70 | try: 71 | filename=cfg.dubb.process_synthesize_task( 72 | text, 73 | voice, 74 | speed, 75 | keep_spacing, 76 | auto_speed) 77 | return jsonify( 78 | { 79 | "status": "ok", 80 | "data": f"/temp/{Path(filename).name}", 81 | } 82 | ) 83 | except Exception as e: 84 | return jsonify({"error": f"{e}"}), 500 85 | 86 | 87 | # 首页 88 | @app.route("/") 89 | def index(): 90 | return render_template("index.html") 91 | 92 | 93 | def openwebbrowser(port): 94 | time.sleep(5) 95 | try: 96 | import webbrowser 97 | webbrowser.open(f"http://127.0.0.1:{port}") 98 | except: 99 | pass 100 | 101 | PORT=5066 102 | if __name__ == '__main__': 103 | try: 104 | threading.Thread(target=openwebbrowser,args=(PORT,)).start() 105 | serve(app, host='0.0.0.0', port=PORT,threads=8) 106 | except Exception as e: 107 | import traceback 108 | traceback.print_exc() -------------------------------------------------------------------------------- /cfg/__init__.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import datetime 3 | import io 4 | import time 5 | from datetime import timedelta 6 | import os 7 | from pathlib import Path 8 | ROOT=Path(os.getcwd()).as_posix() 9 | os.environ['HF_HOME'] = ROOT + "/models" 10 | os.environ['HF_HUB_DISABLE_SYMLINKS_WARNING'] = 'true' 11 | import re 12 | import sys 13 | import textwrap 14 | import json,math 15 | import random 16 | import shutil,logging,hashlib 17 | from filelock import FileLock, Timeout 18 | from . import dubb 19 | VOICE_LIST={ 20 | "en":[ 21 | "af_alloy", 22 | "af_aoede", 23 | "af_bella", 24 | "af_jessica", 25 | "af_kore", 26 | "af_nicole", 27 | "af_nova", 28 | "af_river", 29 | "af_sarah", 30 | "af_sky", 31 | "am_adam", 32 | "am_echo", 33 | "am_eric", 34 | "am_fenrir", 35 | "am_liam", 36 | "am_michael", 37 | "am_onyx", 38 | "am_puck", 39 | "am_santa", 40 | "bf_alice", 41 | "bf_emma", 42 | "bf_isabella", 43 | "bf_lily", 44 | "bm_daniel", 45 | "bm_fable", 46 | "bm_george", 47 | "bm_lewis" 48 | ], 49 | "zh":["zf_xiaobei","zf_xiaoni","zf_xiaoxiao","zf_xiaoyi","zm_yunjian","zm_yunxi","zm_yunxia","zm_yunyang"], 50 | "ja":["jf_alpha","jf_gongitsune","jf_nezumi","jf_tebukuro","jm_kumo"], 51 | "fr":["ff_siwis"], 52 | "it":["if_sara","im_nicola"], 53 | "hi":["hf_alpha","hf_beta","hm_omega","hm_psi"], 54 | "es":["ef_dora","em_alex","em_santa"], 55 | "pt":["pf_dora","pm_alex","pm_santa"] 56 | } 57 | 58 | 59 | 60 | 61 | 62 | TEMP_FOLDER = f'{ROOT}/temp' 63 | LOGS_FOLDER = f'{ROOT}/logs' 64 | 65 | 66 | 67 | 68 | 69 | if not os.path.exists(TEMP_FOLDER): 70 | os.makedirs(TEMP_FOLDER) 71 | if not os.path.exists(LOGS_FOLDER): 72 | os.makedirs(LOGS_FOLDER) 73 | 74 | if sys.platform == 'win32': 75 | os.environ['PATH'] = ROOT + f';{ROOT}/ffmpeg;' + os.environ['PATH'] 76 | 77 | 78 | # Set up logging 79 | logging.basicConfig(level=logging.INFO) 80 | logger = logging.getLogger(__name__) 81 | 82 | _file_handler = logging.FileHandler(f'{LOGS_FOLDER}/{datetime.datetime.now().strftime("%Y%m%d")}.log', encoding='utf-8') 83 | _file_handler.setLevel(logging.INFO) 84 | formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') 85 | _file_handler.setFormatter(formatter) 86 | logger.addHandler(_file_handler) 87 | 88 | 89 | 90 | # 将字符串做 md5 hash处理 91 | def get_md5(input_string: str): 92 | md5 = hashlib.md5() 93 | md5.update(input_string.encode('utf-8')) 94 | return md5.hexdigest() 95 | 96 | 97 | ''' 98 | 格式化毫秒或秒为符合srt格式的 2位小时:2位分:2位秒,3位毫秒 形式 99 | print(ms_to_time_string(ms=12030)) 100 | -> 00:00:12,030 101 | ''' 102 | def ms_to_time_string(*, ms=0, seconds=None): 103 | # 计算小时、分钟、秒和毫秒 104 | if seconds is None: 105 | td = timedelta(milliseconds=ms) 106 | else: 107 | td = timedelta(seconds=seconds) 108 | hours, remainder = divmod(td.seconds, 3600) 109 | minutes, seconds = divmod(remainder, 60) 110 | milliseconds = td.microseconds // 1000 111 | 112 | time_string = f"{hours}:{minutes}:{seconds},{milliseconds}" 113 | return format_time(time_string, ',') 114 | 115 | # 将不规范的 时:分:秒,|.毫秒格式为 aa:bb:cc,ddd形式 116 | # eg 001:01:2,4500 01:54,14 等做处理 117 | def format_time(s_time="", separate=','): 118 | if not s_time.strip(): 119 | return f'00:00:00{separate}000' 120 | hou, min, sec,ms = 0, 0, 0,0 121 | 122 | tmp = s_time.strip().split(':') 123 | if len(tmp) >= 3: 124 | hou,min,sec = tmp[-3].strip(),tmp[-2].strip(),tmp[-1].strip() 125 | elif len(tmp) == 2: 126 | min,sec = tmp[0].strip(),tmp[1].strip() 127 | elif len(tmp) == 1: 128 | sec = tmp[0].strip() 129 | 130 | if re.search(r',|\.', str(sec)): 131 | t = re.split(r',|\.', str(sec)) 132 | sec = t[0].strip() 133 | ms=t[1].strip() 134 | else: 135 | ms = 0 136 | hou = f'{int(hou):02}'[-2:] 137 | min = f'{int(min):02}'[-2:] 138 | sec = f'{int(sec):02}' 139 | ms = f'{int(ms):03}'[-3:] 140 | return f"{hou}:{min}:{sec}{separate}{ms}" 141 | 142 | # 将 datetime.timedelta 对象的秒和微妙转为毫秒整数值 143 | def toms(td): 144 | return (td.seconds * 1000) + int(td.microseconds / 1000) 145 | 146 | # 将 时:分:秒,毫秒 转为毫秒整数值 147 | def get_ms_from_hmsm(time_str): 148 | h,m,sec2ms=0,0,'00,000' 149 | tmp0= time_str.split(":") 150 | if len(tmp0)==3: 151 | h,m,sec2ms=tmp0[0],tmp0[1],tmp0[2] 152 | elif len(tmp0)==2: 153 | m,sec2ms=tmp0[0],tmp0[1] 154 | 155 | tmp=sec2ms.split(',') 156 | ms=tmp[1] if len(tmp)==2 else 0 157 | sec=tmp[0] 158 | 159 | return int(int(h) * 3600000 + int(m) * 60000 +int(sec)*1000 + int(ms)) 160 | 161 | 162 | def srt_str_to_listdict(srt_string): 163 | """解析 SRT 字幕字符串,更精确地处理数字行和时间行之间的关系""" 164 | srt_list = [] 165 | time_pattern = r'\s?(\d+):(\d+):(\d+)([,.]\d+)?\s*?-->\s*?(\d+):(\d+):(\d+)([,.]\d+)?\n?' 166 | lines = srt_string.splitlines() 167 | i = 0 168 | while i < len(lines): 169 | time_match = re.match(time_pattern, lines[i].strip()) 170 | if time_match: 171 | # 解析时间戳 172 | start_time_groups = time_match.groups()[0:4] 173 | end_time_groups = time_match.groups()[4:8] 174 | 175 | def parse_time(time_groups): 176 | h, m, s, ms = time_groups 177 | ms = ms.replace(',', '').replace('.','') if ms else "0" 178 | try: 179 | return int(h) * 3600000 + int(m) * 60000 + int(s)*1000 + int(ms) 180 | except (ValueError, TypeError): 181 | return None 182 | 183 | start_time = parse_time(start_time_groups) 184 | end_time = parse_time(end_time_groups) 185 | 186 | if start_time is None or end_time is None: 187 | i += 1 188 | continue 189 | 190 | i += 1 191 | text_lines = [] 192 | while i < len(lines): 193 | current_line = lines[i].strip() 194 | next_line = lines[i+1].strip() if i + 1 < len(lines) else "" # 获取下一行,如果没有则为空字符串 195 | 196 | if re.match(time_pattern, next_line): #判断下一行是否为时间行 197 | if re.fullmatch(r'\d+', current_line): #如果当前行为纯数字,则跳过 198 | i += 1 199 | break 200 | else: 201 | text_lines.append(current_line) 202 | i += 1 203 | break 204 | 205 | if current_line: 206 | text_lines.append(current_line) 207 | i += 1 208 | else: 209 | i += 1 210 | 211 | text = ('\n'.join(text_lines)).strip() 212 | text=re.sub(r'','',text.replace("\r",'').strip()) 213 | text=re.sub(r'\n{2,}','\n',text) 214 | it={ 215 | "line": len(srt_list)+1, #字幕索引,转换为整数 216 | "start_time": int(start_time), 217 | "end_time":int(end_time), #起始和结束时间 218 | "text": text, #字幕文本 219 | } 220 | it['startraw']=ms_to_time_string(ms=it['start_time']) 221 | it['endraw']=ms_to_time_string(ms=it['end_time']) 222 | it["time"]=f"{it['startraw']} --> {it['endraw']}" 223 | srt_list.append(it) 224 | 225 | 226 | else: 227 | i += 1 # 跳过非时间行 228 | 229 | 230 | return srt_list 231 | 232 | 233 | # 将字符串或者字幕文件内容,格式化为有效字幕数组对象 234 | # 格式化为有效的srt格式 235 | def format_srt(content): 236 | result=[] 237 | try: 238 | result=srt_str_to_listdict(content) 239 | except Exception: 240 | pass 241 | return result 242 | 243 | 244 | 245 | 246 | # 将srt文件或合法srt字符串转为字典对象 247 | def get_subtitle_from_srt(srtfile, *, is_file=True): 248 | def _readfile(file): 249 | content="" 250 | try: 251 | with open(file,'r',encoding='utf-8') as f: 252 | content=f.read().strip() 253 | except Exception as e: 254 | try: 255 | with open(file,'r', encoding='gbk') as f: 256 | content = f.read().strip() 257 | except Exception as e: 258 | logger.exception(e,exc_info=True) 259 | return content 260 | 261 | content='' 262 | if is_file: 263 | content=_readfile(srtfile) 264 | else: 265 | content = srtfile.strip() 266 | 267 | if len(content) < 1: 268 | raise Exception(f"srt is empty:{srtfile=},{content=}") 269 | 270 | result = format_srt(content) 271 | 272 | # txt 文件转为一条字幕 273 | if len(result) < 1: 274 | result = [ 275 | {"line": 1, "time": "00:00:00,000 --> 00:00:02,000", "text": "\n".join(content)} 276 | ] 277 | return result 278 | 279 | 280 | # 将字幕字典列表写入srt文件 281 | def save_srt(srt_list, srt_file): 282 | txt = get_srt_from_list(srt_list) 283 | with open(srt_file,"w", encoding="utf-8") as f: 284 | f.write(txt) 285 | return True 286 | 287 | def get_current_time_as_yymmddhhmmss(format='hms'): 288 | """将当前时间转换为 YYMMDDHHmmss 格式的字符串。""" 289 | now = datetime.datetime.now() 290 | return now.strftime("%y%m%d%H%M%S" if format!='hms' else "%H%M%S") 291 | 292 | # 从 字幕 对象中获取 srt 字幕串 293 | def get_srt_from_list(srt_list): 294 | txt = "" 295 | line = 0 296 | # it中可能含有完整时间戳 it['time'] 00:00:01,123 --> 00:00:12,345 297 | # 开始和结束时间戳 it['startraw']=00:00:01,123 it['endraw']=00:00:12,345 298 | # 开始和结束毫秒数值 it['start_time']=126 it['end_time']=678 299 | for it in srt_list: 300 | line += 1 301 | if "startraw" not in it: 302 | # 存在完整开始和结束时间戳字符串 时:分:秒,毫秒 --> 时:分:秒,毫秒 303 | if 'time' in it: 304 | startraw, endraw = it['time'].strip().split(" --> ") 305 | startraw = format_time(startraw.strip().replace('.', ','), ',') 306 | endraw = format_time(endraw.strip().replace('.', ','), ',') 307 | elif 'start_time' in it and 'end_time' in it: 308 | # 存在开始结束毫秒数值 309 | startraw = ms_to_time_string(ms=it['start_time']) 310 | endraw = ms_to_time_string(ms=it['end_time']) 311 | else: 312 | raise Exception( 313 | f'字幕中不存在 time/startraw/start_time 任何有效时间戳形式') 314 | else: 315 | # 存在单独开始和结束 时:分:秒,毫秒 字符串 316 | startraw = it['startraw'] 317 | endraw = it['endraw'] 318 | txt += f"{line}\n{startraw} --> {endraw}\n{it['text']}\n\n" 319 | return txt 320 | 321 | -------------------------------------------------------------------------------- /cfg/cn_tn.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding=utf-8 3 | 4 | ## 代码来自于开源项目 https://github.com/speechio/chinese_text_normalization 5 | ## 下为原文件附带信息 6 | 7 | # Authors: 8 | # 2019.5 Zhiyang Zhou (https://github.com/Joee1995/chn_text_norm.git) 9 | # 2019.9 - 2022 Jiayu DU 10 | # 11 | # requirements: 12 | # - python 3.X 13 | # notes: python 2.X WILL fail or produce misleading results 14 | 15 | import sys, os, argparse 16 | import string, re 17 | import csv 18 | 19 | # ================================================================================ # 20 | # basic constant 21 | # ================================================================================ # 22 | CHINESE_DIGIS = u'零一二三四五六七八九' 23 | BIG_CHINESE_DIGIS_SIMPLIFIED = u'零壹贰叁肆伍陆柒捌玖' 24 | BIG_CHINESE_DIGIS_TRADITIONAL = u'零壹貳參肆伍陸柒捌玖' 25 | SMALLER_BIG_CHINESE_UNITS_SIMPLIFIED = u'十百千万' 26 | SMALLER_BIG_CHINESE_UNITS_TRADITIONAL = u'拾佰仟萬' 27 | LARGER_CHINESE_NUMERING_UNITS_SIMPLIFIED = u'亿兆京垓秭穰沟涧正载' 28 | LARGER_CHINESE_NUMERING_UNITS_TRADITIONAL = u'億兆京垓秭穰溝澗正載' 29 | SMALLER_CHINESE_NUMERING_UNITS_SIMPLIFIED = u'十百千万' 30 | SMALLER_CHINESE_NUMERING_UNITS_TRADITIONAL = u'拾佰仟萬' 31 | 32 | ZERO_ALT = u'〇' 33 | ONE_ALT = u'幺' 34 | TWO_ALTS = [u'两', u'兩'] 35 | 36 | POSITIVE = [u'正', u'正'] 37 | NEGATIVE = [u'负', u'負'] 38 | POINT = [u'点', u'點'] 39 | # PLUS = [u'加', u'加'] 40 | # SIL = [u'杠', u'槓'] 41 | 42 | FILLER_CHARS = ['呃', '啊'] 43 | 44 | ER_WHITELIST = '(儿女|儿子|儿孙|女儿|儿媳|妻儿|' \ 45 | '胎儿|婴儿|新生儿|婴幼儿|幼儿|少儿|小儿|儿歌|儿童|儿科|托儿所|孤儿|' \ 46 | '儿戏|儿化|台儿庄|鹿儿岛|正儿八经|吊儿郎当|生儿育女|托儿带女|养儿防老|痴儿呆女|' \ 47 | '佳儿佳妇|儿怜兽扰|儿无常父|儿不嫌母丑|儿行千里母担忧|儿大不由爷|苏乞儿)' 48 | ER_WHITELIST_PATTERN = re.compile(ER_WHITELIST) 49 | 50 | # 中文数字系统类型 51 | NUMBERING_TYPES = ['low', 'mid', 'high'] 52 | 53 | CURRENCY_NAMES = '(人民币|美元|日元|英镑|欧元|马克|法郎|加拿大元|澳元|港币|先令|芬兰马克|爱尔兰镑|' \ 54 | '里拉|荷兰盾|埃斯库多|比塞塔|印尼盾|林吉特|新西兰元|比索|卢布|新加坡元|韩元|泰铢)' 55 | CURRENCY_UNITS = '((亿|千万|百万|万|千|百)|(亿|千万|百万|万|千|百|)元|(亿|千万|百万|万|千|百|)块|角|毛|分)' 56 | COM_QUANTIFIERS = '(匹|张|座|回|场|尾|条|个|首|阙|阵|网|炮|顶|丘|棵|只|支|袭|辆|挑|担|颗|壳|窠|曲|墙|群|腔|' \ 57 | '砣|座|客|贯|扎|捆|刀|令|打|手|罗|坡|山|岭|江|溪|钟|队|单|双|对|出|口|头|脚|板|跳|枝|件|贴|' \ 58 | '针|线|管|名|位|身|堂|课|本|页|家|户|层|丝|毫|厘|分|钱|两|斤|担|铢|石|钧|锱|忽|(千|毫|微)克|' \ 59 | '毫|厘|分|寸|尺|丈|里|寻|常|铺|程|(千|分|厘|毫|微)米|撮|勺|合|升|斗|石|盘|碗|碟|叠|桶|笼|盆|' \ 60 | '盒|杯|钟|斛|锅|簋|篮|盘|桶|罐|瓶|壶|卮|盏|箩|箱|煲|啖|袋|钵|年|月|日|季|刻|时|周|天|秒|分|旬|' \ 61 | '纪|岁|世|更|夜|春|夏|秋|冬|代|伏|辈|丸|泡|粒|颗|幢|堆|条|根|支|道|面|片|张|颗|块)' 62 | 63 | 64 | # Punctuation information are based on Zhon project (https://github.com/tsroten/zhon.git) 65 | CN_PUNCS_STOP = '!?。。' 66 | CN_PUNCS_NONSTOP = '"#$%&'()*+,-/:;<=>@[\]^_`{|}~⦅⦆「」、、〃《》「」『』【】〔〕〖〗〘〙〚〛〜〝〞〟〰〾〿–—‘’‛“”„‟…‧﹏·〈〉-' 67 | CN_PUNCS = CN_PUNCS_STOP + CN_PUNCS_NONSTOP 68 | 69 | PUNCS = CN_PUNCS + string.punctuation 70 | PUNCS_TRANSFORM = str.maketrans(PUNCS, ' ' * len(PUNCS), '') # replace puncs with space 71 | 72 | 73 | # https://zh.wikipedia.org/wiki/全行和半行 74 | QJ2BJ = { 75 | ' ': ' ', 76 | '!': '!', 77 | '"': '"', 78 | '#': '#', 79 | '$': '$', 80 | '%': '%', 81 | '&': '&', 82 | ''': "'", 83 | '(': '(', 84 | ')': ')', 85 | '*': '*', 86 | '+': '+', 87 | ',': ',', 88 | '-': '-', 89 | '.': '.', 90 | '/': '/', 91 | '0': '0', 92 | '1': '1', 93 | '2': '2', 94 | '3': '3', 95 | '4': '4', 96 | '5': '5', 97 | '6': '6', 98 | '7': '7', 99 | '8': '8', 100 | '9': '9', 101 | ':': ':', 102 | ';': ';', 103 | '<': '<', 104 | '=': '=', 105 | '>': '>', 106 | '?': '?', 107 | '@': '@', 108 | 'A': 'A', 109 | 'B': 'B', 110 | 'C': 'C', 111 | 'D': 'D', 112 | 'E': 'E', 113 | 'F': 'F', 114 | 'G': 'G', 115 | 'H': 'H', 116 | 'I': 'I', 117 | 'J': 'J', 118 | 'K': 'K', 119 | 'L': 'L', 120 | 'M': 'M', 121 | 'N': 'N', 122 | 'O': 'O', 123 | 'P': 'P', 124 | 'Q': 'Q', 125 | 'R': 'R', 126 | 'S': 'S', 127 | 'T': 'T', 128 | 'U': 'U', 129 | 'V': 'V', 130 | 'W': 'W', 131 | 'X': 'X', 132 | 'Y': 'Y', 133 | 'Z': 'Z', 134 | '[': '[', 135 | '\': '\\', 136 | ']': ']', 137 | '^': '^', 138 | '_': '_', 139 | '`': '`', 140 | 'a': 'a', 141 | 'b': 'b', 142 | 'c': 'c', 143 | 'd': 'd', 144 | 'e': 'e', 145 | 'f': 'f', 146 | 'g': 'g', 147 | 'h': 'h', 148 | 'i': 'i', 149 | 'j': 'j', 150 | 'k': 'k', 151 | 'l': 'l', 152 | 'm': 'm', 153 | 'n': 'n', 154 | 'o': 'o', 155 | 'p': 'p', 156 | 'q': 'q', 157 | 'r': 'r', 158 | 's': 's', 159 | 't': 't', 160 | 'u': 'u', 161 | 'v': 'v', 162 | 'w': 'w', 163 | 'x': 'x', 164 | 'y': 'y', 165 | 'z': 'z', 166 | '{': '{', 167 | '|': '|', 168 | '}': '}', 169 | '~': '~', 170 | } 171 | QJ2BJ_TRANSFORM = str.maketrans(''.join(QJ2BJ.keys()), ''.join(QJ2BJ.values()), '') 172 | 173 | 174 | # 2013 China National Standard: https://zh.wikipedia.org/wiki/通用规范汉字表, raw resources: 175 | # https://github.com/mozillazg/pinyin-data/blob/master/kMandarin_8105.txt with 8105 chinese chars in total 176 | CN_CHARS_COMMON = ( 177 | '一丁七万丈三上下不与丏丐丑专且丕世丘丙业丛东丝丞丢两严丧个丫中丰串临丸丹为主丽举' 178 | '乂乃久么义之乌乍乎乏乐乒乓乔乖乘乙乜九乞也习乡书乩买乱乳乸乾了予争事二亍于亏云互' 179 | '亓五井亘亚些亟亡亢交亥亦产亨亩享京亭亮亲亳亵亶亸亹人亿什仁仂仃仄仅仆仇仉今介仍从' 180 | '仑仓仔仕他仗付仙仝仞仟仡代令以仨仪仫们仰仲仳仵件价任份仿企伈伉伊伋伍伎伏伐休众优' 181 | '伙会伛伞伟传伢伣伤伥伦伧伪伫伭伯估伲伴伶伸伺似伽伾佁佃但位低住佐佑体何佖佗佘余佚' 182 | '佛作佝佞佟你佣佤佥佩佬佯佰佳佴佶佸佺佻佼佽佾使侁侂侃侄侈侉例侍侏侑侔侗侘供依侠侣' 183 | '侥侦侧侨侩侪侬侮侯侴侵侹便促俄俅俊俍俎俏俐俑俗俘俙俚俜保俞俟信俣俦俨俩俪俫俭修俯' 184 | '俱俳俵俶俸俺俾倌倍倏倒倓倔倕倘候倚倜倞借倡倥倦倧倨倩倪倬倭倮倴债倻值倾偁偃假偈偌' 185 | '偎偏偓偕做停偡健偬偭偰偲偶偷偻偾偿傀傃傅傈傉傍傒傕傣傥傧储傩催傲傺傻僇僎像僔僖僚' 186 | '僦僧僬僭僮僰僳僵僻儆儇儋儒儡儦儳儴儿兀允元兄充兆先光克免兑兔兕兖党兜兢入全八公六' 187 | '兮兰共关兴兵其具典兹养兼兽冀冁内冈冉册再冏冒冔冕冗写军农冠冢冤冥冬冮冯冰冱冲决况' 188 | '冶冷冻冼冽净凄准凇凉凋凌减凑凓凘凛凝几凡凤凫凭凯凰凳凶凸凹出击凼函凿刀刁刃分切刈' 189 | '刊刍刎刑划刖列刘则刚创初删判刨利别刬刭刮到刳制刷券刹刺刻刽刿剀剁剂剃剅削剋剌前剐' 190 | '剑剔剕剖剜剞剟剡剥剧剩剪副割剽剿劁劂劄劈劐劓力劝办功加务劢劣动助努劫劬劭励劲劳劼' 191 | '劾势勃勇勉勋勍勐勒勔勖勘勚募勠勤勰勺勾勿匀包匆匈匍匏匐匕化北匙匜匝匠匡匣匦匪匮匹' 192 | '区医匼匾匿十千卅升午卉半华协卑卒卓单卖南博卜卞卟占卡卢卣卤卦卧卫卬卮卯印危即却卵' 193 | '卷卸卺卿厂厄厅历厉压厌厍厕厖厘厚厝原厢厣厥厦厨厩厮去厾县叁参叆叇又叉及友双反发叔' 194 | '叕取受变叙叚叛叟叠口古句另叨叩只叫召叭叮可台叱史右叵叶号司叹叻叼叽吁吃各吆合吉吊' 195 | '同名后吏吐向吒吓吕吖吗君吝吞吟吠吡吣否吧吨吩含听吭吮启吱吲吴吵吸吹吻吼吽吾呀呃呆' 196 | '呇呈告呋呐呒呓呔呕呖呗员呙呛呜呢呣呤呦周呱呲味呵呶呷呸呻呼命咀咂咄咆咇咉咋和咍咎' 197 | '咏咐咒咔咕咖咙咚咛咝咡咣咤咥咦咧咨咩咪咫咬咯咱咳咴咸咺咻咽咿哀品哂哃哄哆哇哈哉哌' 198 | '响哎哏哐哑哒哓哔哕哗哙哚哝哞哟哢哥哦哧哨哩哪哭哮哱哲哳哺哼哽哿唁唆唇唉唏唐唑唔唛' 199 | '唝唠唢唣唤唧唪唬售唯唰唱唳唵唷唼唾唿啁啃啄商啉啊啐啕啖啜啡啤啥啦啧啪啫啬啭啮啰啴' 200 | '啵啶啷啸啻啼啾喀喁喂喃善喆喇喈喉喊喋喏喑喔喘喙喜喝喟喤喧喱喳喵喷喹喻喽喾嗄嗅嗉嗌' 201 | '嗍嗐嗑嗒嗓嗔嗖嗜嗝嗞嗟嗡嗣嗤嗥嗦嗨嗪嗫嗬嗯嗲嗳嗵嗷嗽嗾嘀嘁嘈嘉嘌嘎嘏嘘嘚嘛嘞嘟嘡' 202 | '嘣嘤嘧嘬嘭嘱嘲嘴嘶嘹嘻嘿噀噂噇噌噍噎噔噗噘噙噜噢噤器噩噪噫噬噱噶噻噼嚄嚅嚆嚎嚏嚓' 203 | '嚚嚣嚭嚯嚷嚼囊囔囚四回囟因囡团囤囫园困囱围囵囷囹固国图囿圃圄圆圈圉圊圌圐圙圜土圢' 204 | '圣在圩圪圫圬圭圮圯地圲圳圹场圻圾址坂均坉坊坋坌坍坎坏坐坑坒块坚坛坜坝坞坟坠坡坤坥' 205 | '坦坨坩坪坫坬坭坯坰坳坷坻坼坽垂垃垄垆垈型垌垍垎垏垒垓垕垙垚垛垞垟垠垡垢垣垤垦垧垩' 206 | '垫垭垮垯垱垲垴垵垸垺垾垿埂埃埆埇埋埌城埏埒埔埕埗埘埙埚埝域埠埤埪埫埭埯埴埵埸培基' 207 | '埼埽堂堃堆堇堉堋堌堍堎堐堑堕堙堞堠堡堤堧堨堪堰堲堵堼堽堾塄塅塆塌塍塑塔塘塝塞塥填' 208 | '塬塱塾墀墁境墅墈墉墐墒墓墕墘墙墚增墟墡墣墦墨墩墼壁壅壑壕壤士壬壮声壳壶壸壹处备复' 209 | '夏夐夔夕外夙多夜够夤夥大天太夫夬夭央夯失头夷夸夹夺夼奁奂奄奇奈奉奋奎奏契奓奔奕奖' 210 | '套奘奚奠奡奢奥奭女奴奶奸她好妁如妃妄妆妇妈妊妍妒妓妖妗妘妙妞妣妤妥妧妨妩妪妫妭妮' 211 | '妯妲妹妻妾姆姈姊始姐姑姒姓委姗姘姚姜姝姞姣姤姥姨姬姮姱姶姹姻姽姿娀威娃娄娅娆娇娈' 212 | '娉娌娑娓娘娜娟娠娣娥娩娱娲娴娵娶娼婀婆婉婊婌婍婕婘婚婞婠婢婤婧婪婫婳婴婵婶婷婺婻' 213 | '婼婿媂媄媆媒媓媖媚媛媞媪媭媱媲媳媵媸媾嫁嫂嫄嫉嫌嫒嫔嫕嫖嫘嫚嫜嫠嫡嫣嫦嫩嫪嫫嫭嫱' 214 | '嫽嬉嬖嬗嬛嬥嬬嬴嬷嬿孀孅子孑孓孔孕孖字存孙孚孛孜孝孟孢季孤孥学孩孪孬孰孱孳孵孺孽' 215 | '宁它宄宅宇守安宋完宏宓宕宗官宙定宛宜宝实宠审客宣室宥宦宧宪宫宬宰害宴宵家宸容宽宾' 216 | '宿寁寂寄寅密寇富寐寒寓寝寞察寡寤寥寨寮寰寸对寺寻导寿封射将尉尊小少尔尕尖尘尚尜尝' 217 | '尢尤尥尧尨尪尬就尴尸尹尺尻尼尽尾尿局屁层屃居屈屉届屋屎屏屐屑展屙属屠屡屣履屦屯山' 218 | '屹屺屼屾屿岁岂岈岊岌岍岐岑岔岖岗岘岙岚岛岜岞岠岢岣岨岩岫岬岭岱岳岵岷岸岽岿峁峂峃' 219 | '峄峋峒峗峘峙峛峡峣峤峥峦峧峨峪峭峰峱峻峿崀崁崂崃崄崆崇崌崎崒崔崖崚崛崞崟崡崤崦崧' 220 | '崩崭崮崴崶崽崾崿嵁嵅嵇嵊嵋嵌嵎嵖嵘嵚嵛嵝嵩嵫嵬嵯嵲嵴嶂嶅嶍嶒嶓嶙嶝嶟嶦嶲嶷巅巇巉' 221 | '巍川州巡巢工左巧巨巩巫差巯己已巳巴巷巽巾币市布帅帆师希帏帐帑帔帕帖帘帙帚帛帜帝帡' 222 | '带帧帨席帮帱帷常帻帼帽幂幄幅幌幔幕幖幛幞幡幢幪干平年并幸幺幻幼幽广庄庆庇床庋序庐' 223 | '庑库应底庖店庙庚府庞废庠庤庥度座庭庱庳庵庶康庸庹庼庾廆廉廊廋廑廒廓廖廙廛廨廪延廷' 224 | '建廿开弁异弃弄弆弇弈弊弋式弑弓引弗弘弛弟张弢弥弦弧弨弩弭弯弱弶弸弹强弼彀归当录彖' 225 | '彗彘彝彟形彤彦彧彩彪彬彭彰影彳彷役彻彼往征徂径待徇很徉徊律徐徒徕得徘徙徛徜御徨循' 226 | '徭微徵德徼徽心必忆忉忌忍忏忐忑忒忖志忘忙忝忞忠忡忤忧忪快忭忮忱忳念忸忺忻忽忾忿怀' 227 | '态怂怃怄怅怆怊怍怎怏怒怔怕怖怙怛怜思怠怡急怦性怨怩怪怫怯怵总怼怿恁恂恃恋恍恐恒恓' 228 | '恔恕恙恚恝恢恣恤恧恨恩恪恫恬恭息恰恳恶恸恹恺恻恼恽恿悃悄悆悈悉悌悍悒悔悖悚悛悝悟' 229 | '悠悢患悦您悫悬悭悯悰悱悲悴悸悻悼情惆惇惊惋惎惑惔惕惘惙惚惛惜惝惟惠惦惧惨惩惫惬惭' 230 | '惮惯惰想惴惶惹惺愀愁愃愆愈愉愍愎意愐愔愕愚感愠愣愤愦愧愫愭愿慆慈慊慌慎慑慕慝慢慥' 231 | '慧慨慬慭慰慵慷憋憎憔憕憙憧憨憩憬憭憷憺憾懂懈懊懋懑懒懔懦懵懿戆戈戊戋戌戍戎戏成我' 232 | '戒戕或戗战戚戛戟戡戢戣戤戥截戬戭戮戳戴户戽戾房所扁扂扃扅扆扇扈扉扊手才扎扑扒打扔' 233 | '托扛扞扣扦执扩扪扫扬扭扮扯扰扳扶批扺扼扽找承技抃抄抉把抑抒抓抔投抖抗折抚抛抟抠抡' 234 | '抢护报抨披抬抱抵抹抻押抽抿拂拃拄担拆拇拈拉拊拌拍拎拐拒拓拔拖拗拘拙招拜拟拢拣拤拥' 235 | '拦拧拨择括拭拮拯拱拳拴拶拷拼拽拾拿持挂指挈按挎挑挓挖挚挛挝挞挟挠挡挣挤挥挦挨挪挫' 236 | '振挲挹挺挽捂捃捅捆捉捋捌捍捎捏捐捕捞损捡换捣捧捩捭据捯捶捷捺捻捽掀掂掇授掉掊掌掎' 237 | '掏掐排掖掘掞掠探掣接控推掩措掬掭掮掰掳掴掷掸掺掼掾揄揆揉揍描提插揕揖揠握揣揩揪揭' 238 | '揳援揶揸揽揿搀搁搂搅搋搌搏搐搒搓搔搛搜搞搠搡搦搪搬搭搴携搽摁摄摅摆摇摈摊摏摒摔摘' 239 | '摛摞摧摩摭摴摸摹摽撂撄撅撇撑撒撕撖撙撞撤撩撬播撮撰撵撷撸撺撼擀擂擅操擎擐擒擘擞擢' 240 | '擤擦擿攀攉攒攘攥攫攮支收攸改攻攽放政故效敉敌敏救敔敕敖教敛敝敞敢散敦敩敫敬数敲整' 241 | '敷文斋斌斐斑斓斗料斛斜斝斟斠斡斤斥斧斩斫断斯新斶方於施旁旃旄旅旆旋旌旎族旐旒旖旗' 242 | '旞无既日旦旧旨早旬旭旮旯旰旱旴旵时旷旸旺旻旿昀昂昃昄昆昇昈昉昊昌明昏昒易昔昕昙昝' 243 | '星映昡昣昤春昧昨昪昫昭是昱昳昴昵昶昺昼昽显晁晃晅晊晋晌晏晐晒晓晔晕晖晗晙晚晞晟晡' 244 | '晢晤晦晨晪晫普景晰晱晴晶晷智晾暂暄暅暇暌暑暕暖暗暝暧暨暮暲暴暵暶暹暾暿曈曌曙曛曜' 245 | '曝曦曩曰曲曳更曷曹曼曾替最月有朋服朏朐朓朔朕朗望朝期朦木未末本札术朱朳朴朵朸机朽' 246 | '杀杂权杄杆杈杉杌李杏材村杓杕杖杙杜杞束杠条来杧杨杩杪杭杯杰杲杳杵杷杻杼松板极构枅' 247 | '枇枉枋枍析枕林枘枚果枝枞枢枣枥枧枨枪枫枭枯枰枲枳枵架枷枸枹柁柃柄柈柊柏某柑柒染柔' 248 | '柖柘柙柚柜柝柞柠柢查柩柬柯柰柱柳柴柷柽柿栀栅标栈栉栊栋栌栎栏栐树栒栓栖栗栝栟校栩' 249 | '株栲栳栴样核根栻格栽栾桀桁桂桃桄桅框案桉桊桌桎桐桑桓桔桕桠桡桢档桤桥桦桧桨桩桫桯' 250 | '桲桴桶桷桹梁梃梅梆梌梏梓梗梠梢梣梦梧梨梭梯械梳梴梵梼梽梾梿检棁棂棉棋棍棐棒棓棕棘' 251 | '棚棠棣棤棨棪棫棬森棰棱棵棹棺棻棼棽椀椁椅椆椋植椎椐椑椒椓椟椠椤椪椭椰椴椸椹椽椿楂' 252 | '楒楔楗楙楚楝楞楠楣楦楩楪楫楮楯楷楸楹楼概榃榄榅榆榇榈榉榍榑榔榕榖榛榜榧榨榫榭榰榱' 253 | '榴榷榻槁槃槊槌槎槐槔槚槛槜槟槠槭槱槲槽槿樊樗樘樟模樨横樯樱樵樽樾橄橇橐橑橘橙橛橞' 254 | '橡橥橦橱橹橼檀檄檎檐檑檗檞檠檩檫檬櫆欂欠次欢欣欤欧欲欸欹欺欻款歃歅歆歇歉歌歙止正' 255 | '此步武歧歪歹死歼殁殂殃殄殆殇殉殊残殍殒殓殖殚殛殡殣殪殳殴段殷殿毁毂毅毋毌母每毐毒' 256 | '毓比毕毖毗毙毛毡毪毫毯毳毵毹毽氅氆氇氍氏氐民氓气氕氖氘氙氚氛氟氡氢氤氦氧氨氩氪氮' 257 | '氯氰氲水永氾氿汀汁求汆汇汈汉汊汋汐汔汕汗汛汜汝汞江池污汤汧汨汩汪汫汭汰汲汴汶汹汽' 258 | '汾沁沂沃沄沅沆沇沈沉沌沏沐沓沔沘沙沚沛沟没沣沤沥沦沧沨沩沪沫沭沮沱河沸油沺治沼沽' 259 | '沾沿泂泃泄泅泇泉泊泌泐泓泔法泖泗泙泚泛泜泞泠泡波泣泥注泪泫泮泯泰泱泳泵泷泸泺泻泼' 260 | '泽泾洁洄洇洈洋洌洎洑洒洓洗洘洙洚洛洞洢洣津洧洨洪洫洭洮洱洲洳洴洵洸洹洺活洼洽派洿' 261 | '流浃浅浆浇浈浉浊测浍济浏浐浑浒浓浔浕浙浚浛浜浞浟浠浡浣浥浦浩浪浬浭浮浯浰浲浴海浸' 262 | '浼涂涄涅消涉涌涍涎涐涑涓涔涕涘涛涝涞涟涠涡涢涣涤润涧涨涩涪涫涮涯液涴涵涸涿淀淄淅' 263 | '淆淇淋淌淏淑淖淘淙淜淝淞淟淠淡淤淦淫淬淮淯深淳淴混淹添淼清渊渌渍渎渐渑渔渗渚渝渟' 264 | '渠渡渣渤渥温渫渭港渰渲渴游渺渼湃湄湉湍湎湑湓湔湖湘湛湜湝湟湣湫湮湲湴湾湿溁溃溅溆' 265 | '溇溉溍溏源溘溚溜溞溟溠溢溥溦溧溪溯溱溲溴溵溶溷溹溺溻溽滁滂滃滆滇滉滋滍滏滑滓滔滕' 266 | '滗滘滚滞滟滠满滢滤滥滦滧滨滩滪滫滴滹漂漆漈漉漋漏漓演漕漖漠漤漦漩漪漫漭漯漱漳漴漶' 267 | '漷漹漻漼漾潆潇潋潍潏潖潘潜潞潟潢潦潩潭潮潲潴潵潸潺潼潽潾澂澄澈澉澌澍澎澛澜澡澥澧' 268 | '澪澭澳澴澶澹澼澽激濂濉濋濑濒濞濠濡濩濮濯瀌瀍瀑瀔瀚瀛瀣瀱瀵瀹瀼灈灌灏灞火灭灯灰灵' 269 | '灶灸灼灾灿炀炅炆炉炊炌炎炒炔炕炖炘炙炜炝炟炣炫炬炭炮炯炱炳炷炸点炻炼炽烀烁烂烃烈' 270 | '烊烔烘烙烛烜烝烟烠烤烦烧烨烩烫烬热烯烶烷烹烺烻烽焆焉焊焌焐焓焕焖焗焘焙焚焜焞焦焯' 271 | '焰焱然煁煃煅煊煋煌煎煓煜煞煟煤煦照煨煮煲煳煴煸煺煽熄熇熊熏熔熘熙熛熜熟熠熥熨熬熵' 272 | '熹熻燃燊燋燎燏燔燕燚燠燥燧燮燹爆爇爔爚爝爟爨爪爬爰爱爵父爷爸爹爻爽爿牁牂片版牌牍' 273 | '牒牖牙牚牛牝牟牡牢牤牥牦牧物牮牯牲牵特牺牻牾牿犀犁犄犇犊犋犍犏犒犟犨犬犯犰犴状犷' 274 | '犸犹狁狂狃狄狈狉狍狎狐狒狗狙狝狞狠狡狨狩独狭狮狯狰狱狲狳狴狷狸狺狻狼猁猃猄猇猊猎' 275 | '猕猖猗猛猜猝猞猡猢猥猩猪猫猬献猯猰猱猴猷猹猺猾猿獍獐獒獗獠獬獭獯獴獾玃玄率玉王玎' 276 | '玑玒玓玕玖玘玙玚玛玞玟玠玡玢玤玥玦玩玫玭玮环现玱玲玳玶玷玹玺玻玼玿珀珂珅珇珈珉珊' 277 | '珋珌珍珏珐珑珒珕珖珙珛珝珞珠珢珣珥珦珧珩珪珫班珰珲珵珷珸珹珺珽琀球琄琅理琇琈琉琊' 278 | '琎琏琐琔琚琛琟琡琢琤琥琦琨琪琫琬琭琮琯琰琲琳琴琵琶琼瑀瑁瑂瑃瑄瑅瑆瑑瑓瑔瑕瑖瑗瑙' 279 | '瑚瑛瑜瑝瑞瑟瑢瑧瑨瑬瑭瑰瑱瑳瑶瑷瑾璀璁璃璆璇璈璋璎璐璒璘璜璞璟璠璥璧璨璩璪璬璮璱' 280 | '璲璺瓀瓒瓖瓘瓜瓞瓠瓢瓣瓤瓦瓮瓯瓴瓶瓷瓻瓿甄甍甏甑甓甗甘甚甜生甡甥甦用甩甪甫甬甭甯' 281 | '田由甲申电男甸町画甾畀畅畈畋界畎畏畔畖留畚畛畜畤略畦番畬畯畲畴畸畹畿疁疃疆疍疏疐' 282 | '疑疔疖疗疙疚疝疟疠疡疢疣疤疥疫疬疭疮疯疰疱疲疳疴疵疸疹疼疽疾痂痃痄病症痈痉痊痍痒' 283 | '痓痔痕痘痛痞痢痣痤痦痧痨痪痫痰痱痴痹痼痿瘀瘁瘃瘅瘆瘊瘌瘐瘕瘗瘘瘙瘛瘟瘠瘢瘤瘥瘦瘩' 284 | '瘪瘫瘭瘰瘳瘴瘵瘸瘼瘾瘿癀癃癌癍癔癖癗癜癞癣癫癯癸登白百癿皂的皆皇皈皋皎皑皓皕皖皙' 285 | '皛皞皤皦皭皮皱皲皴皿盂盅盆盈盉益盍盎盏盐监盒盔盖盗盘盛盟盥盦目盯盱盲直盷相盹盼盾' 286 | '省眄眇眈眉眊看眍眙眚真眠眢眦眨眩眬眭眯眵眶眷眸眺眼着睁睃睄睇睎睐睑睚睛睡睢督睥睦' 287 | '睨睫睬睹睽睾睿瞀瞄瞅瞋瞌瞍瞎瞑瞒瞟瞠瞢瞥瞧瞩瞪瞫瞬瞭瞰瞳瞵瞻瞽瞿矍矗矛矜矞矢矣知' 288 | '矧矩矫矬短矮矰石矶矸矻矼矾矿砀码砂砄砆砉砌砍砑砒研砖砗砘砚砜砝砟砠砣砥砧砫砬砭砮' 289 | '砰破砵砷砸砹砺砻砼砾础硁硅硇硊硌硍硎硐硒硔硕硖硗硙硚硝硪硫硬硭确硼硿碃碇碈碉碌碍' 290 | '碎碏碑碓碗碘碚碛碜碟碡碣碥碧碨碰碱碲碳碴碶碹碾磁磅磉磊磋磏磐磔磕磙磜磡磨磬磲磴磷' 291 | '磹磻礁礅礌礓礞礴礵示礼社祀祁祃祆祇祈祉祊祋祎祏祐祓祕祖祗祚祛祜祝神祟祠祢祥祧票祭' 292 | '祯祲祷祸祺祼祾禀禁禄禅禊禋福禒禔禘禚禛禤禧禳禹禺离禽禾秀私秃秆秉秋种科秒秕秘租秣' 293 | '秤秦秧秩秫秬秭积称秸移秽秾稀稂稃稆程稌稍税稑稔稗稙稚稞稠稣稳稷稹稻稼稽稿穄穆穑穗' 294 | '穙穜穟穰穴究穷穸穹空穿窀突窃窄窅窈窊窍窎窑窒窕窖窗窘窜窝窟窠窣窥窦窨窬窭窳窸窿立' 295 | '竑竖竘站竞竟章竣童竦竫竭端竹竺竽竿笃笄笆笈笊笋笏笑笔笕笙笛笞笠笤笥符笨笪笫第笮笯' 296 | '笱笳笸笺笼笾筀筅筇等筋筌筏筐筑筒答策筘筚筛筜筝筠筢筤筥筦筮筱筲筵筶筷筹筻筼签简箅' 297 | '箍箐箓箔箕箖算箜管箢箦箧箨箩箪箫箬箭箱箴箸篁篆篇篌篑篓篙篚篝篡篥篦篪篮篯篱篷篼篾' 298 | '簃簇簉簋簌簏簕簖簝簟簠簧簪簰簸簿籀籁籍籥米籴类籼籽粉粑粒粕粗粘粜粝粞粟粢粤粥粪粮' 299 | '粱粲粳粹粼粽精粿糁糅糇糈糊糌糍糒糕糖糗糙糜糟糠糨糯糵系紊素索紧紫累絜絮絷綦綮縠縢' 300 | '縻繁繄繇纂纛纠纡红纣纤纥约级纨纩纪纫纬纭纮纯纰纱纲纳纴纵纶纷纸纹纺纻纼纽纾线绀绁' 301 | '绂练组绅细织终绉绊绋绌绍绎经绐绑绒结绔绕绖绗绘给绚绛络绝绞统绠绡绢绣绤绥绦继绨绩' 302 | '绪绫续绮绯绰绱绲绳维绵绶绷绸绹绺绻综绽绾绿缀缁缂缃缄缅缆缇缈缉缊缌缎缐缑缒缓缔缕' 303 | '编缗缘缙缚缛缜缝缞缟缠缡缢缣缤缥缦缧缨缩缪缫缬缭缮缯缰缱缲缳缴缵缶缸缺罂罄罅罍罐' 304 | '网罔罕罗罘罚罟罡罢罨罩罪置罱署罴罶罹罽罾羁羊羌美羑羓羔羕羖羚羝羞羟羡群羧羯羰羱羲' 305 | '羸羹羼羽羿翀翁翂翃翅翈翊翌翎翔翕翘翙翚翛翟翠翡翥翦翩翮翯翰翱翳翷翻翼翾耀老考耄者' 306 | '耆耇耋而耍耏耐耑耒耔耕耖耗耘耙耜耠耢耤耥耦耧耨耩耪耰耱耳耵耶耷耸耻耽耿聂聃聆聊聋' 307 | '职聍聒联聘聚聩聪聱聿肃肄肆肇肉肋肌肓肖肘肚肛肝肟肠股肢肤肥肩肪肫肭肮肯肱育肴肷肸' 308 | '肺肼肽肾肿胀胁胂胃胄胆胈背胍胎胖胗胙胚胛胜胝胞胠胡胣胤胥胧胨胩胪胫胬胭胯胰胱胲胳' 309 | '胴胶胸胺胼能脂脆脉脊脍脎脏脐脑脒脓脔脖脘脚脞脟脩脬脯脱脲脶脸脾脿腆腈腊腋腌腐腑腒' 310 | '腓腔腕腘腙腚腠腥腧腨腩腭腮腯腰腱腴腹腺腻腼腽腾腿膀膂膈膊膏膑膘膙膛膜膝膦膨膳膺膻' 311 | '臀臂臃臆臊臌臑臜臣臧自臬臭至致臻臼臾舀舁舂舄舅舆舌舍舐舒舔舛舜舞舟舠舢舣舥航舫般' 312 | '舭舯舰舱舲舳舴舵舶舷舸船舻舾艄艅艇艉艋艎艏艘艚艟艨艮良艰色艳艴艺艽艾艿节芃芄芈芊' 313 | '芋芍芎芏芑芒芗芘芙芜芝芟芠芡芣芤芥芦芨芩芪芫芬芭芮芯芰花芳芴芷芸芹芼芽芾苁苄苇苈' 314 | '苉苊苋苌苍苎苏苑苒苓苔苕苗苘苛苜苞苟苠苡苣苤若苦苧苫苯英苴苷苹苻苾茀茁茂范茄茅茆' 315 | '茈茉茋茌茎茏茑茓茔茕茗茚茛茜茝茧茨茫茬茭茯茱茳茴茵茶茸茹茺茼茽荀荁荃荄荆荇草荏荐' 316 | '荑荒荓荔荖荙荚荛荜荞荟荠荡荣荤荥荦荧荨荩荪荫荬荭荮药荷荸荻荼荽莅莆莉莎莒莓莘莙莛' 317 | '莜莝莞莠莨莩莪莫莰莱莲莳莴莶获莸莹莺莼莽莿菀菁菂菅菇菉菊菌菍菏菔菖菘菜菝菟菠菡菥' 318 | '菩菪菰菱菲菹菼菽萁萃萄萆萋萌萍萎萏萑萘萚萜萝萣萤营萦萧萨萩萱萳萸萹萼落葆葎葑葖著' 319 | '葙葚葛葜葡董葩葫葬葭葰葱葳葴葵葶葸葺蒂蒄蒇蒈蒉蒋蒌蒎蒐蒗蒙蒜蒟蒡蒨蒯蒱蒲蒴蒸蒹蒺' 320 | '蒻蒽蒿蓁蓂蓄蓇蓉蓊蓍蓏蓐蓑蓓蓖蓝蓟蓠蓢蓣蓥蓦蓬蓰蓼蓿蔀蔃蔈蔊蔌蔑蔓蔗蔚蔟蔡蔫蔬蔷' 321 | '蔸蔹蔺蔻蔼蔽蕃蕈蕉蕊蕖蕗蕙蕞蕤蕨蕰蕲蕴蕹蕺蕻蕾薁薄薅薇薏薛薜薢薤薨薪薮薯薰薳薷薸' 322 | '薹薿藁藉藏藐藓藕藜藟藠藤藦藨藩藻藿蘅蘑蘖蘘蘧蘩蘸蘼虎虏虐虑虒虓虔虚虞虢虤虫虬虮虱' 323 | '虷虸虹虺虻虼虽虾虿蚀蚁蚂蚄蚆蚊蚋蚌蚍蚓蚕蚜蚝蚣蚤蚧蚨蚩蚪蚬蚯蚰蚱蚲蚴蚶蚺蛀蛃蛄蛆' 324 | '蛇蛉蛊蛋蛎蛏蛐蛑蛔蛘蛙蛛蛞蛟蛤蛩蛭蛮蛰蛱蛲蛳蛴蛸蛹蛾蜀蜂蜃蜇蜈蜉蜊蜍蜎蜐蜒蜓蜕蜗' 325 | '蜘蜚蜜蜞蜡蜢蜣蜥蜩蜮蜱蜴蜷蜻蜾蜿蝇蝈蝉蝌蝎蝓蝗蝘蝙蝠蝣蝤蝥蝮蝰蝲蝴蝶蝻蝼蝽蝾螂螃' 326 | '螅螈螋融螗螟螠螣螨螫螬螭螯螱螳螵螺螽蟀蟆蟊蟋蟏蟑蟒蟛蟠蟥蟪蟫蟮蟹蟾蠃蠊蠋蠓蠕蠖蠡' 327 | '蠢蠲蠹蠼血衃衄衅行衍衎衒衔街衙衠衡衢衣补表衩衫衬衮衰衲衷衽衾衿袁袂袄袅袆袈袋袍袒' 328 | '袖袗袜袢袤袪被袭袯袱袷袼裁裂装裆裈裉裎裒裔裕裘裙裛裟裢裣裤裥裨裰裱裳裴裸裹裼裾褂' 329 | '褊褐褒褓褕褙褚褛褟褡褥褪褫褯褰褴褶襁襄襕襚襜襞襟襦襫襻西要覃覆见观觃规觅视觇览觉' 330 | '觊觋觌觎觏觐觑角觖觚觜觞觟解觥触觫觭觯觱觳觿言訄訇訚訾詈詟詹誉誊誓謇警譬计订讣认' 331 | '讥讦讧讨让讪讫训议讯记讱讲讳讴讵讶讷许讹论讻讼讽设访诀证诂诃评诅识诇诈诉诊诋诌词' 332 | '诎诏诐译诒诓诔试诖诗诘诙诚诛诜话诞诟诠诡询诣诤该详诧诨诩诫诬语诮误诰诱诲诳说诵请' 333 | '诸诹诺读诼诽课诿谀谁谂调谄谅谆谇谈谊谋谌谍谎谏谐谑谒谓谔谕谖谗谙谚谛谜谝谞谟谠谡' 334 | '谢谣谤谥谦谧谨谩谪谫谬谭谮谯谰谱谲谳谴谵谶谷谼谿豁豆豇豉豌豕豚象豢豨豪豫豮豳豸豹' 335 | '豺貂貅貆貉貊貌貔貘贝贞负贡财责贤败账货质贩贪贫贬购贮贯贰贱贲贳贴贵贶贷贸费贺贻贼' 336 | '贽贾贿赀赁赂赃资赅赆赇赈赉赊赋赌赍赎赏赐赑赒赓赔赕赖赗赘赙赚赛赜赝赞赟赠赡赢赣赤' 337 | '赦赧赪赫赭走赳赴赵赶起趁趄超越趋趑趔趟趣趯趱足趴趵趸趺趼趾趿跂跃跄跆跋跌跎跏跐跑' 338 | '跖跗跚跛距跞跟跣跤跨跪跬路跱跳践跶跷跸跹跺跻跽踅踉踊踌踏踒踔踝踞踟踢踣踦踩踪踬踮' 339 | '踯踱踵踶踹踺踽蹀蹁蹂蹄蹅蹇蹈蹉蹊蹋蹐蹑蹒蹙蹚蹜蹢蹦蹩蹬蹭蹯蹰蹲蹴蹶蹼蹽蹾蹿躁躅躇' 340 | '躏躐躔躜躞身躬躯躲躺车轧轨轩轪轫转轭轮软轰轱轲轳轴轵轶轷轸轹轺轻轼载轾轿辀辁辂较' 341 | '辄辅辆辇辈辉辊辋辌辍辎辏辐辑辒输辔辕辖辗辘辙辚辛辜辞辟辣辨辩辫辰辱边辽达辿迁迂迄' 342 | '迅过迈迎运近迓返迕还这进远违连迟迢迤迥迦迨迩迪迫迭迮述迳迷迸迹迺追退送适逃逄逅逆' 343 | '选逊逋逍透逐逑递途逖逗通逛逝逞速造逡逢逦逭逮逯逴逵逶逸逻逼逾遁遂遄遆遇遍遏遐遑遒' 344 | '道遗遘遛遢遣遥遨遭遮遴遵遹遽避邀邂邃邈邋邑邓邕邗邘邙邛邝邠邡邢那邦邨邪邬邮邯邰邱' 345 | '邲邳邴邵邶邸邹邺邻邽邾邿郁郃郄郅郇郈郊郎郏郐郑郓郗郚郛郜郝郡郢郤郦郧部郪郫郭郯郴' 346 | '郸都郾郿鄀鄂鄃鄄鄅鄌鄑鄗鄘鄙鄚鄜鄞鄠鄢鄣鄫鄯鄱鄹酂酃酅酆酉酊酋酌配酎酏酐酒酗酚酝' 347 | '酞酡酢酣酤酥酦酩酪酬酮酯酰酱酲酴酵酶酷酸酹酺酽酾酿醅醇醉醋醌醍醐醑醒醚醛醢醨醪醭' 348 | '醮醯醴醵醺醾采釉释里重野量釐金釜鉴銎銮鋆鋈錾鍪鎏鏊鏖鐾鑫钆钇针钉钊钋钌钍钎钏钐钒' 349 | '钓钔钕钖钗钘钙钚钛钜钝钞钟钠钡钢钣钤钥钦钧钨钩钪钫钬钭钮钯钰钱钲钳钴钵钷钹钺钻钼' 350 | '钽钾钿铀铁铂铃铄铅铆铈铉铊铋铌铍铎铏铐铑铒铕铖铗铘铙铚铛铜铝铞铟铠铡铢铣铤铥铧铨' 351 | '铩铪铫铬铭铮铯铰铱铲铳铴铵银铷铸铹铺铻铼铽链铿销锁锂锃锄锅锆锇锈锉锊锋锌锍锎锏锐' 352 | '锑锒锓锔锕锖锗锘错锚锛锜锝锞锟锡锢锣锤锥锦锧锨锩锪锫锬锭键锯锰锱锲锳锴锵锶锷锸锹' 353 | '锺锻锼锽锾锿镀镁镂镃镄镅镆镇镈镉镊镋镌镍镎镏镐镑镒镓镔镕镖镗镘镚镛镜镝镞镠镡镢镣' 354 | '镤镥镦镧镨镩镪镫镬镭镮镯镰镱镲镳镴镵镶长门闩闪闫闭问闯闰闱闲闳间闵闶闷闸闹闺闻闼' 355 | '闽闾闿阀阁阂阃阄阅阆阇阈阉阊阋阌阍阎阏阐阑阒阔阕阖阗阘阙阚阜队阡阪阮阱防阳阴阵阶' 356 | '阻阼阽阿陀陂附际陆陇陈陉陋陌降陎限陑陔陕陛陞陟陡院除陧陨险陪陬陲陴陵陶陷隃隅隆隈' 357 | '隋隍随隐隔隗隘隙障隧隩隰隳隶隹隺隼隽难雀雁雄雅集雇雉雊雌雍雎雏雒雕雠雨雩雪雯雱雳' 358 | '零雷雹雾需霁霄霅霆震霈霉霍霎霏霓霖霜霞霨霪霭霰露霸霹霾青靓靖静靛非靠靡面靥革靬靰' 359 | '靳靴靶靸靺靼靽靿鞁鞅鞋鞍鞑鞒鞔鞘鞠鞡鞣鞧鞨鞫鞬鞭鞮鞯鞲鞳鞴韂韦韧韨韩韪韫韬韭音韵' 360 | '韶页顶顷顸项顺须顼顽顾顿颀颁颂颃预颅领颇颈颉颊颋颌颍颎颏颐频颓颔颖颗题颙颚颛颜额' 361 | '颞颟颠颡颢颤颥颦颧风飏飐飑飒飓飔飕飗飘飙飞食飧飨餍餐餮饔饕饥饧饨饩饪饫饬饭饮饯饰' 362 | '饱饲饳饴饵饶饷饸饹饺饻饼饽饿馁馃馄馅馆馇馈馉馊馋馌馍馏馐馑馒馓馔馕首馗馘香馝馞馥' 363 | '馧馨马驭驮驯驰驱驲驳驴驵驶驷驸驹驺驻驼驽驾驿骀骁骂骃骄骅骆骇骈骉骊骋验骍骎骏骐骑' 364 | '骒骓骕骖骗骘骙骚骛骜骝骞骟骠骡骢骣骤骥骦骧骨骰骱骶骷骸骺骼髀髁髂髃髅髋髌髎髑髓高' 365 | '髡髢髦髫髭髯髹髻髽鬃鬈鬏鬒鬓鬘鬟鬣鬯鬲鬶鬷鬻鬼魁魂魃魄魅魆魇魈魉魋魍魏魑魔鱼鱽鱾' 366 | '鱿鲀鲁鲂鲃鲅鲆鲇鲈鲉鲊鲋鲌鲍鲎鲏鲐鲑鲒鲔鲕鲖鲗鲘鲙鲚鲛鲜鲝鲞鲟鲠鲡鲢鲣鲤鲥鲦鲧鲨' 367 | '鲩鲪鲫鲬鲭鲮鲯鲰鲱鲲鲳鲴鲵鲷鲸鲹鲺鲻鲼鲽鲾鲿鳀鳁鳂鳃鳄鳅鳇鳈鳉鳊鳌鳍鳎鳏鳐鳑鳒鳓' 368 | '鳔鳕鳖鳗鳘鳙鳚鳛鳜鳝鳞鳟鳠鳡鳢鳣鳤鸟鸠鸡鸢鸣鸤鸥鸦鸧鸨鸩鸪鸫鸬鸭鸮鸯鸰鸱鸲鸳鸵鸶' 369 | '鸷鸸鸹鸺鸻鸼鸽鸾鸿鹀鹁鹂鹃鹄鹅鹆鹇鹈鹉鹊鹋鹌鹍鹎鹏鹐鹑鹒鹔鹕鹖鹗鹘鹙鹚鹛鹜鹝鹞鹟' 370 | '鹠鹡鹢鹣鹤鹦鹧鹨鹩鹪鹫鹬鹭鹮鹯鹰鹱鹲鹳鹴鹾鹿麀麂麇麈麋麑麒麓麖麝麟麦麸麹麻麽麾黄' 371 | '黇黉黍黎黏黑黔默黛黜黝黟黠黡黢黥黧黩黪黯黹黻黼黾鼋鼍鼎鼐鼒鼓鼗鼙鼠鼢鼩鼫鼬鼯鼱鼷' 372 | '鼹鼻鼽鼾齁齇齉齐齑齿龀龁龂龃龄龅龆龇龈龉龊龋龌龙龚龛龟龠龢鿍鿎鿏㑇㑊㕮㘎㙍㙘㙦㛃' 373 | '㛚㛹㟃㠇㠓㤘㥄㧐㧑㧟㫰㬊㬎㬚㭎㭕㮾㰀㳇㳘㳚㴔㵐㶲㸆㸌㺄㻬㽏㿠䁖䂮䃅䃎䅟䌹䎃䎖䏝䏡' 374 | '䏲䐃䓖䓛䓨䓫䓬䗖䗛䗪䗴䜣䝙䢺䢼䣘䥽䦃䲟䲠䲢䴓䴔䴕䴖䴗䴘䴙䶮𠅤𠙶𠳐𡎚𡐓𣗋𣲗𣲘𣸣𤧛𤩽' 375 | '𤫉𥔲𥕢𥖨𥻗𦈡𦒍𦙶𦝼𦭜𦰡𧿹𨐈𨙸𨚕𨟠𨭉𨱇𨱏𨱑𨱔𨺙𩽾𩾃𩾌𪟝𪣻𪤗𪨰𪨶𪩘𪾢𫄧𫄨𫄷𫄸𫇭𫌀𫍣𫍯' 376 | '𫍲𫍽𫐄𫐐𫐓𫑡𫓧𫓯𫓶𫓹𫔍𫔎𫔶𫖮𫖯𫖳𫗧𫗴𫘜𫘝𫘦𫘧𫘨𫘪𫘬𫚕𫚖𫚭𫛭𫞩𫟅𫟦𫟹𫟼𫠆𫠊𫠜𫢸𫫇𫭟' 377 | '𫭢𫭼𫮃𫰛𫵷𫶇𫷷𫸩𬀩𬀪𬂩𬃊𬇕𬇙𬇹𬉼𬊈𬊤𬌗𬍛𬍡𬍤𬒈𬒔𬒗𬕂𬘓𬘘𬘡𬘩𬘫𬘬𬘭𬘯𬙂𬙊𬙋𬜬𬜯𬞟' 378 | '𬟁𬟽𬣙𬣞𬣡𬣳𬤇𬤊𬤝𬨂𬨎𬩽𬪩𬬩𬬭𬬮𬬱𬬸𬬹𬬻𬬿𬭁𬭊𬭎𬭚𬭛𬭤𬭩𬭬𬭯𬭳𬭶𬭸𬭼𬮱𬮿𬯀𬯎𬱖𬱟' 379 | '𬳵𬳶𬳽𬳿𬴂𬴃𬴊𬶋𬶍𬶏𬶐𬶟𬶠𬶨𬶭𬶮𬷕𬸘𬸚𬸣𬸦𬸪𬹼𬺈𬺓' 380 | ) 381 | CN_CHARS_EXT = '吶诶屌囧飚屄' 382 | 383 | CN_CHARS = CN_CHARS_COMMON + CN_CHARS_EXT 384 | IN_CH_CHARS = { c : True for c in CN_CHARS } 385 | 386 | EN_CHARS = string.ascii_letters + string.digits 387 | IN_EN_CHARS = { c : True for c in EN_CHARS } 388 | 389 | VALID_CHARS = CN_CHARS + EN_CHARS + ' ' 390 | IN_VALID_CHARS = { c : True for c in VALID_CHARS } 391 | 392 | # ================================================================================ # 393 | # basic class 394 | # ================================================================================ # 395 | class ChineseChar(object): 396 | """ 397 | 中文字符 398 | 每个字符对应简体和繁体, 399 | e.g. 简体 = '负', 繁体 = '負' 400 | 转换时可转换为简体或繁体 401 | """ 402 | 403 | def __init__(self, simplified, traditional): 404 | self.simplified = simplified 405 | self.traditional = traditional 406 | #self.__repr__ = self.__str__ 407 | 408 | def __str__(self): 409 | return self.simplified or self.traditional or None 410 | 411 | def __repr__(self): 412 | return self.__str__() 413 | 414 | 415 | class ChineseNumberUnit(ChineseChar): 416 | """ 417 | 中文数字/数位字符 418 | 每个字符除繁简体外还有一个额外的大写字符 419 | e.g. '陆' 和 '陸' 420 | """ 421 | 422 | def __init__(self, power, simplified, traditional, big_s, big_t): 423 | super(ChineseNumberUnit, self).__init__(simplified, traditional) 424 | self.power = power 425 | self.big_s = big_s 426 | self.big_t = big_t 427 | 428 | def __str__(self): 429 | return '10^{}'.format(self.power) 430 | 431 | @classmethod 432 | def create(cls, index, value, numbering_type=NUMBERING_TYPES[1], small_unit=False): 433 | 434 | if small_unit: 435 | return ChineseNumberUnit(power=index + 1, 436 | simplified=value[0], traditional=value[1], big_s=value[1], big_t=value[1]) 437 | elif numbering_type == NUMBERING_TYPES[0]: 438 | return ChineseNumberUnit(power=index + 8, 439 | simplified=value[0], traditional=value[1], big_s=value[0], big_t=value[1]) 440 | elif numbering_type == NUMBERING_TYPES[1]: 441 | return ChineseNumberUnit(power=(index + 2) * 4, 442 | simplified=value[0], traditional=value[1], big_s=value[0], big_t=value[1]) 443 | elif numbering_type == NUMBERING_TYPES[2]: 444 | return ChineseNumberUnit(power=pow(2, index + 3), 445 | simplified=value[0], traditional=value[1], big_s=value[0], big_t=value[1]) 446 | else: 447 | raise ValueError( 448 | 'Counting type should be in {0} ({1} provided).'.format(NUMBERING_TYPES, numbering_type)) 449 | 450 | 451 | class ChineseNumberDigit(ChineseChar): 452 | """ 453 | 中文数字字符 454 | """ 455 | 456 | def __init__(self, value, simplified, traditional, big_s, big_t, alt_s=None, alt_t=None): 457 | super(ChineseNumberDigit, self).__init__(simplified, traditional) 458 | self.value = value 459 | self.big_s = big_s 460 | self.big_t = big_t 461 | self.alt_s = alt_s 462 | self.alt_t = alt_t 463 | 464 | def __str__(self): 465 | return str(self.value) 466 | 467 | @classmethod 468 | def create(cls, i, v): 469 | return ChineseNumberDigit(i, v[0], v[1], v[2], v[3]) 470 | 471 | 472 | class ChineseMath(ChineseChar): 473 | """ 474 | 中文数位字符 475 | """ 476 | 477 | def __init__(self, simplified, traditional, symbol, expression=None): 478 | super(ChineseMath, self).__init__(simplified, traditional) 479 | self.symbol = symbol 480 | self.expression = expression 481 | self.big_s = simplified 482 | self.big_t = traditional 483 | 484 | 485 | CC, CNU, CND, CM = ChineseChar, ChineseNumberUnit, ChineseNumberDigit, ChineseMath 486 | 487 | 488 | class NumberSystem(object): 489 | """ 490 | 中文数字系统 491 | """ 492 | pass 493 | 494 | 495 | class MathSymbol(object): 496 | """ 497 | 用于中文数字系统的数学符号 (繁/简体), e.g. 498 | positive = ['正', '正'] 499 | negative = ['负', '負'] 500 | point = ['点', '點'] 501 | """ 502 | 503 | def __init__(self, positive, negative, point): 504 | self.positive = positive 505 | self.negative = negative 506 | self.point = point 507 | 508 | def __iter__(self): 509 | for v in self.__dict__.values(): 510 | yield v 511 | 512 | 513 | # class OtherSymbol(object): 514 | # """ 515 | # 其他符号 516 | # """ 517 | # 518 | # def __init__(self, sil): 519 | # self.sil = sil 520 | # 521 | # def __iter__(self): 522 | # for v in self.__dict__.values(): 523 | # yield v 524 | 525 | 526 | # ================================================================================ # 527 | # basic utils 528 | # ================================================================================ # 529 | def create_system(numbering_type=NUMBERING_TYPES[1]): 530 | """ 531 | 根据数字系统类型返回创建相应的数字系统,默认为 mid 532 | NUMBERING_TYPES = ['low', 'mid', 'high']: 中文数字系统类型 533 | low: '兆' = '亿' * '十' = $10^{9}$, '京' = '兆' * '十', etc. 534 | mid: '兆' = '亿' * '万' = $10^{12}$, '京' = '兆' * '万', etc. 535 | high: '兆' = '亿' * '亿' = $10^{16}$, '京' = '兆' * '兆', etc. 536 | 返回对应的数字系统 537 | """ 538 | 539 | # chinese number units of '亿' and larger 540 | all_larger_units = zip( 541 | LARGER_CHINESE_NUMERING_UNITS_SIMPLIFIED, LARGER_CHINESE_NUMERING_UNITS_TRADITIONAL) 542 | larger_units = [CNU.create(i, v, numbering_type, False) 543 | for i, v in enumerate(all_larger_units)] 544 | # chinese number units of '十, 百, 千, 万' 545 | all_smaller_units = zip( 546 | SMALLER_CHINESE_NUMERING_UNITS_SIMPLIFIED, SMALLER_CHINESE_NUMERING_UNITS_TRADITIONAL) 547 | smaller_units = [CNU.create(i, v, small_unit=True) 548 | for i, v in enumerate(all_smaller_units)] 549 | # digis 550 | chinese_digis = zip(CHINESE_DIGIS, CHINESE_DIGIS, 551 | BIG_CHINESE_DIGIS_SIMPLIFIED, BIG_CHINESE_DIGIS_TRADITIONAL) 552 | digits = [CND.create(i, v) for i, v in enumerate(chinese_digis)] 553 | digits[0].alt_s, digits[0].alt_t = ZERO_ALT, ZERO_ALT 554 | digits[1].alt_s, digits[1].alt_t = ONE_ALT, ONE_ALT 555 | digits[2].alt_s, digits[2].alt_t = TWO_ALTS[0], TWO_ALTS[1] 556 | 557 | # symbols 558 | positive_cn = CM(POSITIVE[0], POSITIVE[1], '+', lambda x: x) 559 | negative_cn = CM(NEGATIVE[0], NEGATIVE[1], '-', lambda x: -x) 560 | point_cn = CM(POINT[0], POINT[1], '.', lambda x, 561 | y: float(str(x) + '.' + str(y))) 562 | # sil_cn = CM(SIL[0], SIL[1], '-', lambda x, y: float(str(x) + '-' + str(y))) 563 | system = NumberSystem() 564 | system.units = smaller_units + larger_units 565 | system.digits = digits 566 | system.math = MathSymbol(positive_cn, negative_cn, point_cn) 567 | # system.symbols = OtherSymbol(sil_cn) 568 | return system 569 | 570 | 571 | def chn2num(chinese_string, numbering_type=NUMBERING_TYPES[1]): 572 | 573 | def get_symbol(char, system): 574 | for u in system.units: 575 | if char in [u.traditional, u.simplified, u.big_s, u.big_t]: 576 | return u 577 | for d in system.digits: 578 | if char in [d.traditional, d.simplified, d.big_s, d.big_t, d.alt_s, d.alt_t]: 579 | return d 580 | for m in system.math: 581 | if char in [m.traditional, m.simplified]: 582 | return m 583 | 584 | def string2symbols(chinese_string, system): 585 | int_string, dec_string = chinese_string, '' 586 | for p in [system.math.point.simplified, system.math.point.traditional]: 587 | if p in chinese_string: 588 | int_string, dec_string = chinese_string.split(p) 589 | break 590 | return [get_symbol(c, system) for c in int_string], \ 591 | [get_symbol(c, system) for c in dec_string] 592 | 593 | def correct_symbols(integer_symbols, system): 594 | """ 595 | 一百八 to 一百八十 596 | 一亿一千三百万 to 一亿 一千万 三百万 597 | """ 598 | 599 | if integer_symbols and isinstance(integer_symbols[0], CNU): 600 | if integer_symbols[0].power == 1: 601 | integer_symbols = [system.digits[1]] + integer_symbols 602 | 603 | if len(integer_symbols) > 1: 604 | if isinstance(integer_symbols[-1], CND) and isinstance(integer_symbols[-2], CNU): 605 | integer_symbols.append( 606 | CNU(integer_symbols[-2].power - 1, None, None, None, None)) 607 | 608 | result = [] 609 | unit_count = 0 610 | for s in integer_symbols: 611 | if isinstance(s, CND): 612 | result.append(s) 613 | unit_count = 0 614 | elif isinstance(s, CNU): 615 | current_unit = CNU(s.power, None, None, None, None) 616 | unit_count += 1 617 | 618 | if unit_count == 1: 619 | result.append(current_unit) 620 | elif unit_count > 1: 621 | for i in range(len(result)): 622 | if isinstance(result[-i - 1], CNU) and result[-i - 1].power < current_unit.power: 623 | result[-i - 1] = CNU(result[-i - 1].power + 624 | current_unit.power, None, None, None, None) 625 | return result 626 | 627 | def compute_value(integer_symbols): 628 | """ 629 | Compute the value. 630 | When current unit is larger than previous unit, current unit * all previous units will be used as all previous units. 631 | e.g. '两千万' = 2000 * 10000 not 2000 + 10000 632 | """ 633 | value = [0] 634 | last_power = 0 635 | for s in integer_symbols: 636 | if isinstance(s, CND): 637 | value[-1] = s.value 638 | elif isinstance(s, CNU): 639 | value[-1] *= pow(10, s.power) 640 | if s.power > last_power: 641 | value[:-1] = list(map(lambda v: v * 642 | pow(10, s.power), value[:-1])) 643 | last_power = s.power 644 | value.append(0) 645 | return sum(value) 646 | 647 | system = create_system(numbering_type) 648 | int_part, dec_part = string2symbols(chinese_string, system) 649 | int_part = correct_symbols(int_part, system) 650 | int_str = str(compute_value(int_part)) 651 | dec_str = ''.join([str(d.value) for d in dec_part]) 652 | if dec_part: 653 | return '{0}.{1}'.format(int_str, dec_str) 654 | else: 655 | return int_str 656 | 657 | 658 | def num2chn(number_string, numbering_type=NUMBERING_TYPES[1], big=False, 659 | traditional=False, alt_zero=False, alt_one=False, alt_two=True, 660 | use_zeros=True, use_units=True): 661 | 662 | def get_value(value_string, use_zeros=True): 663 | 664 | striped_string = value_string.lstrip('0') 665 | 666 | # record nothing if all zeros 667 | if not striped_string: 668 | return [] 669 | 670 | # record one digits 671 | elif len(striped_string) == 1: 672 | if use_zeros and len(value_string) != len(striped_string): 673 | return [system.digits[0], system.digits[int(striped_string)]] 674 | else: 675 | return [system.digits[int(striped_string)]] 676 | 677 | # recursively record multiple digits 678 | else: 679 | result_unit = next(u for u in reversed( 680 | system.units) if u.power < len(striped_string)) 681 | result_string = value_string[:-result_unit.power] 682 | return get_value(result_string) + [result_unit] + get_value(striped_string[-result_unit.power:]) 683 | 684 | system = create_system(numbering_type) 685 | 686 | int_dec = number_string.split('.') 687 | if len(int_dec) == 1: 688 | int_string = int_dec[0] 689 | dec_string = "" 690 | elif len(int_dec) == 2: 691 | int_string = int_dec[0] 692 | dec_string = int_dec[1] 693 | else: 694 | raise ValueError( 695 | "invalid input num string with more than one dot: {}".format(number_string)) 696 | 697 | if use_units and len(int_string) > 1: 698 | result_symbols = get_value(int_string) 699 | else: 700 | result_symbols = [system.digits[int(c)] for c in int_string] 701 | dec_symbols = [system.digits[int(c)] for c in dec_string] 702 | if dec_string: 703 | result_symbols += [system.math.point] + dec_symbols 704 | 705 | if alt_two: 706 | liang = CND(2, system.digits[2].alt_s, system.digits[2].alt_t, 707 | system.digits[2].big_s, system.digits[2].big_t) 708 | for i, v in enumerate(result_symbols): 709 | if isinstance(v, CND) and v.value == 2: 710 | next_symbol = result_symbols[i + 711 | 1] if i < len(result_symbols) - 1 else None 712 | previous_symbol = result_symbols[i - 1] if i > 0 else None 713 | if isinstance(next_symbol, CNU) and isinstance(previous_symbol, (CNU, type(None))): 714 | if next_symbol.power != 1 and ((previous_symbol is None) or (previous_symbol.power != 1)): 715 | result_symbols[i] = liang 716 | 717 | # if big is True, '两' will not be used and `alt_two` has no impact on output 718 | if big: 719 | attr_name = 'big_' 720 | if traditional: 721 | attr_name += 't' 722 | else: 723 | attr_name += 's' 724 | else: 725 | if traditional: 726 | attr_name = 'traditional' 727 | else: 728 | attr_name = 'simplified' 729 | 730 | result = ''.join([getattr(s, attr_name) for s in result_symbols]) 731 | 732 | # if not use_zeros: 733 | # result = result.strip(getattr(system.digits[0], attr_name)) 734 | 735 | if alt_zero: 736 | result = result.replace( 737 | getattr(system.digits[0], attr_name), system.digits[0].alt_s) 738 | 739 | if alt_one: 740 | result = result.replace( 741 | getattr(system.digits[1], attr_name), system.digits[1].alt_s) 742 | 743 | for i, p in enumerate(POINT): 744 | if result.startswith(p): 745 | return CHINESE_DIGIS[0] + result 746 | 747 | # ^10, 11, .., 19 748 | if len(result) >= 2 and result[1] in [SMALLER_CHINESE_NUMERING_UNITS_SIMPLIFIED[0], 749 | SMALLER_CHINESE_NUMERING_UNITS_TRADITIONAL[0]] and \ 750 | result[0] in [CHINESE_DIGIS[1], BIG_CHINESE_DIGIS_SIMPLIFIED[1], BIG_CHINESE_DIGIS_TRADITIONAL[1]]: 751 | result = result[1:] 752 | 753 | return result 754 | 755 | 756 | # ================================================================================ # 757 | # different types of rewriters 758 | # ================================================================================ # 759 | class Cardinal: 760 | """ 761 | CARDINAL类 762 | """ 763 | 764 | def __init__(self, cardinal=None, chntext=None): 765 | self.cardinal = cardinal 766 | self.chntext = chntext 767 | 768 | def chntext2cardinal(self): 769 | return chn2num(self.chntext) 770 | 771 | def cardinal2chntext(self): 772 | return num2chn(self.cardinal) 773 | 774 | class Digit: 775 | """ 776 | DIGIT类 777 | """ 778 | 779 | def __init__(self, digit=None, chntext=None): 780 | self.digit = digit 781 | self.chntext = chntext 782 | 783 | # def chntext2digit(self): 784 | # return chn2num(self.chntext) 785 | 786 | def digit2chntext(self): 787 | return num2chn(self.digit, alt_two=False, use_units=False) 788 | 789 | 790 | class TelePhone: 791 | """ 792 | TELEPHONE类 793 | """ 794 | 795 | def __init__(self, telephone=None, raw_chntext=None, chntext=None): 796 | self.telephone = telephone 797 | self.raw_chntext = raw_chntext 798 | self.chntext = chntext 799 | 800 | # def chntext2telephone(self): 801 | # sil_parts = self.raw_chntext.split('') 802 | # self.telephone = '-'.join([ 803 | # str(chn2num(p)) for p in sil_parts 804 | # ]) 805 | # return self.telephone 806 | 807 | def telephone2chntext(self, fixed=False): 808 | 809 | if fixed: 810 | sil_parts = self.telephone.split('-') 811 | self.raw_chntext = ''.join([ 812 | num2chn(part, alt_two=False, use_units=False) for part in sil_parts 813 | ]) 814 | self.chntext = self.raw_chntext.replace('', '') 815 | else: 816 | sp_parts = self.telephone.strip('+').split() 817 | self.raw_chntext = ''.join([ 818 | num2chn(part, alt_two=False, use_units=False) for part in sp_parts 819 | ]) 820 | self.chntext = self.raw_chntext.replace('', '') 821 | return self.chntext 822 | 823 | 824 | class Fraction: 825 | """ 826 | FRACTION类 827 | """ 828 | 829 | def __init__(self, fraction=None, chntext=None): 830 | self.fraction = fraction 831 | self.chntext = chntext 832 | 833 | def chntext2fraction(self): 834 | denominator, numerator = self.chntext.split('分之') 835 | return chn2num(numerator) + '/' + chn2num(denominator) 836 | 837 | def fraction2chntext(self): 838 | numerator, denominator = self.fraction.split('/') 839 | return num2chn(denominator) + '分之' + num2chn(numerator) 840 | 841 | 842 | class Date: 843 | """ 844 | DATE类 845 | """ 846 | 847 | def __init__(self, date=None, chntext=None): 848 | self.date = date 849 | self.chntext = chntext 850 | 851 | # def chntext2date(self): 852 | # chntext = self.chntext 853 | # try: 854 | # year, other = chntext.strip().split('年', maxsplit=1) 855 | # year = Digit(chntext=year).digit2chntext() + '年' 856 | # except ValueError: 857 | # other = chntext 858 | # year = '' 859 | # if other: 860 | # try: 861 | # month, day = other.strip().split('月', maxsplit=1) 862 | # month = Cardinal(chntext=month).chntext2cardinal() + '月' 863 | # except ValueError: 864 | # day = chntext 865 | # month = '' 866 | # if day: 867 | # day = Cardinal(chntext=day[:-1]).chntext2cardinal() + day[-1] 868 | # else: 869 | # month = '' 870 | # day = '' 871 | # date = year + month + day 872 | # self.date = date 873 | # return self.date 874 | 875 | def date2chntext(self): 876 | date = self.date 877 | try: 878 | year, other = date.strip().split('年', 1) 879 | year = Digit(digit=year).digit2chntext() + '年' 880 | except ValueError: 881 | other = date 882 | year = '' 883 | if other: 884 | try: 885 | month, day = other.strip().split('月', 1) 886 | month = Cardinal(cardinal=month).cardinal2chntext() + '月' 887 | except ValueError: 888 | day = date 889 | month = '' 890 | if day: 891 | day = Cardinal(cardinal=day[:-1]).cardinal2chntext() + day[-1] 892 | else: 893 | month = '' 894 | day = '' 895 | chntext = year + month + day 896 | self.chntext = chntext 897 | return self.chntext 898 | 899 | 900 | class Money: 901 | """ 902 | MONEY类 903 | """ 904 | 905 | def __init__(self, money=None, chntext=None): 906 | self.money = money 907 | self.chntext = chntext 908 | 909 | # def chntext2money(self): 910 | # return self.money 911 | 912 | def money2chntext(self): 913 | money = self.money 914 | pattern = re.compile(r'(\d+(\.\d+)?)') 915 | matchers = pattern.findall(money) 916 | if matchers: 917 | for matcher in matchers: 918 | money = money.replace(matcher[0], Cardinal(cardinal=matcher[0]).cardinal2chntext()) 919 | self.chntext = money 920 | return self.chntext 921 | 922 | 923 | class Percentage: 924 | """ 925 | PERCENTAGE类 926 | """ 927 | 928 | def __init__(self, percentage=None, chntext=None): 929 | self.percentage = percentage 930 | self.chntext = chntext 931 | 932 | def chntext2percentage(self): 933 | return chn2num(self.chntext.strip().strip('百分之')) + '%' 934 | 935 | def percentage2chntext(self): 936 | return '百分之' + num2chn(self.percentage.strip().strip('%')) 937 | 938 | 939 | def normalize_nsw(raw_text): 940 | text = '^' + raw_text + '$' 941 | 942 | # 规范化日期 943 | pattern = re.compile(r"\D+((([089]\d|(19|20)\d{2})年)?(\d{1,2}月(\d{1,2}[日号])?)?)") 944 | matchers = pattern.findall(text) 945 | if matchers: 946 | #print('date') 947 | for matcher in matchers: 948 | text = text.replace(matcher[0], Date(date=matcher[0]).date2chntext(), 1) 949 | 950 | # 规范化金钱 951 | pattern = re.compile(r"\D+((\d+(\.\d+)?)[多余几]?" + CURRENCY_UNITS + r"(\d" + CURRENCY_UNITS + r"?)?)") 952 | matchers = pattern.findall(text) 953 | if matchers: 954 | #print('money') 955 | for matcher in matchers: 956 | text = text.replace(matcher[0], Money(money=matcher[0]).money2chntext(), 1) 957 | 958 | # 规范化固话/手机号码 959 | # 手机 960 | # http://www.jihaoba.com/news/show/13680 961 | # 移动:139、138、137、136、135、134、159、158、157、150、151、152、188、187、182、183、184、178、198 962 | # 联通:130、131、132、156、155、186、185、176 963 | # 电信:133、153、189、180、181、177 964 | pattern = re.compile(r"\D((\+?86 ?)?1([38]\d|5[0-35-9]|7[678]|9[89])\d{8})\D") 965 | matchers = pattern.findall(text) 966 | if matchers: 967 | #print('telephone') 968 | for matcher in matchers: 969 | text = text.replace(matcher[0], TelePhone(telephone=matcher[0]).telephone2chntext(), 1) 970 | # 固话 971 | pattern = re.compile(r"\D((0(10|2[1-3]|[3-9]\d{2})-?)?[1-9]\d{6,7})\D") 972 | matchers = pattern.findall(text) 973 | if matchers: 974 | # print('fixed telephone') 975 | for matcher in matchers: 976 | text = text.replace(matcher[0], TelePhone(telephone=matcher[0]).telephone2chntext(fixed=True), 1) 977 | 978 | # 规范化分数 979 | pattern = re.compile(r"(\d+/\d+)") 980 | matchers = pattern.findall(text) 981 | if matchers: 982 | #print('fraction') 983 | for matcher in matchers: 984 | text = text.replace(matcher, Fraction(fraction=matcher).fraction2chntext(), 1) 985 | 986 | # 规范化百分数 987 | text = text.replace('%', '%') 988 | pattern = re.compile(r"(\d+(\.\d+)?%)") 989 | matchers = pattern.findall(text) 990 | if matchers: 991 | #print('percentage') 992 | for matcher in matchers: 993 | text = text.replace(matcher[0], Percentage(percentage=matcher[0]).percentage2chntext(), 1) 994 | 995 | # 规范化纯数+量词 996 | pattern = re.compile(r"(\d+(\.\d+)?)[多余几]?" + COM_QUANTIFIERS) 997 | matchers = pattern.findall(text) 998 | if matchers: 999 | #print('cardinal+quantifier') 1000 | for matcher in matchers: 1001 | text = text.replace(matcher[0], Cardinal(cardinal=matcher[0]).cardinal2chntext(), 1) 1002 | 1003 | # 规范化数字编号 1004 | pattern = re.compile(r"(\d{4,32})") 1005 | matchers = pattern.findall(text) 1006 | if matchers: 1007 | #print('digit') 1008 | for matcher in matchers: 1009 | text = text.replace(matcher, Digit(digit=matcher).digit2chntext(), 1) 1010 | 1011 | # 规范化纯数 1012 | pattern = re.compile(r"(\d+(\.\d+)?)") 1013 | matchers = pattern.findall(text) 1014 | if matchers: 1015 | #print('cardinal') 1016 | for matcher in matchers: 1017 | text = text.replace(matcher[0], Cardinal(cardinal=matcher[0]).cardinal2chntext(), 1) 1018 | 1019 | 1020 | # restore P2P, O2O, B2C, B2B etc 1021 | pattern = re.compile(r"(([a-zA-Z]+)二([a-zA-Z]+))") 1022 | matchers = pattern.findall(text) 1023 | if matchers: 1024 | # print('particular') 1025 | for matcher in matchers: 1026 | text = text.replace(matcher[0], matcher[1]+'2'+matcher[2], 1) 1027 | 1028 | return text.lstrip('^').rstrip('$') 1029 | 1030 | 1031 | def remove_erhua(text): 1032 | """ 1033 | 去除儿化音词中的儿: 1034 | 他女儿在那边儿 -> 他女儿在那边 1035 | """ 1036 | 1037 | new_str='' 1038 | while re.search('儿',text): 1039 | a = re.search('儿',text).span() 1040 | remove_er_flag = 0 1041 | 1042 | if ER_WHITELIST_PATTERN.search(text): 1043 | b = ER_WHITELIST_PATTERN.search(text).span() 1044 | if b[0] <= a[0]: 1045 | remove_er_flag = 1 1046 | 1047 | if remove_er_flag == 0 : 1048 | new_str = new_str + text[0:a[0]] 1049 | text = text[a[1]:] 1050 | else: 1051 | new_str = new_str + text[0:b[1]] 1052 | text = text[b[1]:] 1053 | 1054 | text = new_str + text 1055 | return text 1056 | 1057 | 1058 | def remove_space(text): 1059 | tokens = text.split() 1060 | new = [] 1061 | for k,t in enumerate(tokens): 1062 | if k != 0: 1063 | if IN_EN_CHARS.get(tokens[k-1][-1]) and IN_EN_CHARS.get(t[0]): 1064 | new.append(' ') 1065 | new.append(t) 1066 | return ''.join(new) 1067 | 1068 | 1069 | class TextNorm: 1070 | def __init__(self, 1071 | to_banjiao:bool = False, 1072 | to_upper:bool = False, 1073 | to_lower:bool = False, 1074 | remove_fillers:bool = False, 1075 | remove_erhua:bool = False, 1076 | check_chars:bool = False, 1077 | remove_space:bool = False, 1078 | cc_mode:str = '', 1079 | ) : 1080 | self.to_banjiao = to_banjiao 1081 | self.to_upper = to_upper 1082 | self.to_lower = to_lower 1083 | self.remove_fillers = remove_fillers 1084 | self.remove_erhua = remove_erhua 1085 | self.check_chars = check_chars 1086 | self.remove_space = remove_space 1087 | 1088 | self.cc = None 1089 | if cc_mode: 1090 | from opencc import OpenCC # Open Chinese Convert: pip install opencc 1091 | self.cc = OpenCC(cc_mode) 1092 | 1093 | def __call__(self, text): 1094 | if self.cc: 1095 | text = self.cc.convert(text) 1096 | 1097 | if self.to_banjiao: 1098 | text = text.translate(QJ2BJ_TRANSFORM) 1099 | 1100 | if self.to_upper: 1101 | text = text.upper() 1102 | 1103 | if self.to_lower: 1104 | text = text.lower() 1105 | 1106 | if self.remove_fillers: 1107 | for c in FILLER_CHARS: 1108 | text = text.replace(c, '') 1109 | 1110 | if self.remove_erhua: 1111 | text = remove_erhua(text) 1112 | 1113 | text = normalize_nsw(text) 1114 | 1115 | text = text.translate(PUNCS_TRANSFORM) 1116 | 1117 | if self.check_chars: 1118 | for c in text: 1119 | if not IN_VALID_CHARS.get(c): 1120 | print(f'WARNING: illegal char {c} in: {text}', file=sys.stderr) 1121 | return '' 1122 | 1123 | if self.remove_space: 1124 | text = remove_space(text) 1125 | 1126 | return text 1127 | 1128 | 1129 | if __name__ == '__main__': 1130 | p = argparse.ArgumentParser() 1131 | 1132 | # normalizer options 1133 | p.add_argument('--to_banjiao', action='store_true', help='convert quanjiao chars to banjiao') 1134 | p.add_argument('--to_upper', action='store_true', help='convert to upper case') 1135 | p.add_argument('--to_lower', action='store_true', help='convert to lower case') 1136 | p.add_argument('--remove_fillers', action='store_true', help='remove filler chars such as "呃, 啊"') 1137 | p.add_argument('--remove_erhua', action='store_true', help='remove erhua chars such as "他女儿在那边儿 -> 他女儿在那边"') 1138 | p.add_argument('--check_chars', action='store_true' , help='skip sentences containing illegal chars') 1139 | p.add_argument('--remove_space', action='store_true' , help='remove whitespace') 1140 | p.add_argument('--cc_mode', choices=['', 't2s', 's2t'], default='', help='convert between traditional to simplified') 1141 | 1142 | # I/O options 1143 | p.add_argument('--log_interval', type=int, default=10000, help='log interval in number of processed lines') 1144 | p.add_argument('--has_key', action='store_true', help="will be deprecated, set --format ark instead") 1145 | p.add_argument('--format', type=str, choices=['txt', 'ark', 'tsv'], default='txt', help='input format') 1146 | p.add_argument('ifile', help='input filename, assume utf-8 encoding') 1147 | p.add_argument('ofile', help='output filename') 1148 | 1149 | args = p.parse_args() 1150 | 1151 | if args.has_key: 1152 | args.format = 'ark' 1153 | 1154 | normalizer = TextNorm( 1155 | to_banjiao = args.to_banjiao, 1156 | to_upper = args.to_upper, 1157 | to_lower = args.to_lower, 1158 | remove_fillers = args.remove_fillers, 1159 | remove_erhua = args.remove_erhua, 1160 | check_chars = args.check_chars, 1161 | remove_space = args.remove_space, 1162 | cc_mode = args.cc_mode, 1163 | ) 1164 | 1165 | ndone = 0 1166 | with open(args.ifile, 'r', encoding = 'utf8') as istream, open(args.ofile, 'w+', encoding = 'utf8') as ostream: 1167 | if args.format == 'tsv': 1168 | reader = csv.DictReader(istream, delimiter = '\t') 1169 | assert('TEXT' in reader.fieldnames) 1170 | print('\t'.join(reader.fieldnames), file=ostream) 1171 | 1172 | for item in reader: 1173 | text = item['TEXT'] 1174 | 1175 | if text: 1176 | text = normalizer(text) 1177 | 1178 | if text: 1179 | item['TEXT'] = text 1180 | print('\t'.join([ item[f] for f in reader.fieldnames ]), file = ostream) 1181 | 1182 | ndone += 1 1183 | if ndone % args.log_interval == 0: 1184 | print(f'text norm: {ndone} lines done.', file = sys.stderr, flush = True) 1185 | else: 1186 | for l in istream: 1187 | key, text = '', '' 1188 | if args.format == 'ark': # KALDI archive, line format: "key text" 1189 | cols = l.strip().split(maxsplit=1) 1190 | key, text = cols[0], cols[1] if len(cols) == 2 else '' 1191 | else: 1192 | text = l.strip() 1193 | 1194 | if text: 1195 | text = normalizer(text) 1196 | 1197 | if text: 1198 | if args.format == 'ark': 1199 | print(key + '\t' + text, file = ostream) 1200 | else: 1201 | print(text, file = ostream) 1202 | 1203 | ndone += 1 1204 | if ndone % args.log_interval == 0: 1205 | print(f'text norm: {ndone} lines done.', file = sys.stderr, flush = True) 1206 | print(f'text norm: {ndone} lines done in total.', file = sys.stderr, flush = True) 1207 | 1208 | -------------------------------------------------------------------------------- /cfg/dubb.py: -------------------------------------------------------------------------------- 1 | import json 2 | import cfg 3 | import os,sys,time,asyncio,re,copy,base64,requests,shutil 4 | from pydub import AudioSegment 5 | from pathlib import Path 6 | import concurrent.futures 7 | from kokoro import KPipeline 8 | import soundfile as sf 9 | from . import cn_tn,en_tn 10 | def merge_audio_segments(queue_tts,filename,keep_spacing=False,auto_speed=False): 11 | from pydub import AudioSegment 12 | merged_audio = AudioSegment.empty() 13 | err=0 14 | length = len(queue_tts) 15 | if length == 1: 16 | if queue_tts[0]['filename']!=filename: 17 | m=AudioSegment.from_file(queue_tts[0]['filename'], format=queue_tts[0]['filename'][-3:]) 18 | m.export(filename, format="mp3") 19 | #shutil.copy2(queue_tts[0]['filename'],filename) 20 | return filename 21 | 22 | # start is not 0 23 | if keep_spacing and queue_tts[0]['start_time'] > 0: 24 | merged_audio += AudioSegment.silent(duration=queue_tts[0]['start_time']) 25 | 26 | # 开始时间 27 | 28 | for i, it in enumerate(queue_tts): 29 | 30 | # 存在有效配音文件则加入,否则配音时长大于0则加入静音 31 | segment = None 32 | the_ext = it['filename'][-3:] 33 | 34 | # 存在配音文件 35 | if Path(it['filename']).is_file(): 36 | raw_time=it['end_time']-it['start_time'] 37 | try: 38 | segment = AudioSegment.from_file(it['filename'], format=the_ext) 39 | dubb_time=len(segment) 40 | if raw_time and auto_speed: 41 | if raw_timedubb_time: 44 | segment+=AudioSegment.silent(duration=raw_time-dubb_time) 45 | 46 | merged_audio+=segment 47 | except Exception as e: 48 | err+=1 49 | cfg.logger.exception(e, exc_info=True) 50 | merged_audio += AudioSegment.silent(duration=raw_time) 51 | else: 52 | # 不存在配音文件 53 | err+=1 54 | 55 | if i< length-1 and keep_spacing: 56 | silent_time=queue_tts[i+1]['start_time']-it['end_time'] 57 | if silent_time>0: 58 | merged_audio += AudioSegment.silent(duration=silent_time) 59 | if err>length/2: 60 | raise Exception("Too many errors, please try again later.") 61 | merged_audio.export(filename, format="mp3") 62 | return filename 63 | 64 | 65 | def process_synthesize_task(text, voice, speed=1.0, keep_spacing=False,auto_speed=False): 66 | try: 67 | return TTS(text, voice, speed, keep_spacing,auto_speed).run() 68 | except Exception as e: 69 | cfg.logger.exception(f'配音失败 {e}',exc_info=True) 70 | raise 71 | 72 | 73 | 74 | class TTS: 75 | 76 | def __init__(self, text,voice, speed, keep_spacing,auto_speed): 77 | self.uuid=cfg.get_md5(f'{str(text)}-{voice}-{speed}-{keep_spacing}-{auto_speed}') 78 | self.end_mp3name=f'{cfg.TEMP_FOLDER}/dubbing-end-{self.uuid}.mp3' 79 | 80 | self.keep_spacing=keep_spacing 81 | self.auto_speed=auto_speed 82 | normalizer=None 83 | if voice[0] == 'z': 84 | normalizer = cn_tn.TextNorm(to_banjiao = True) 85 | elif voice[0] in ['a','b']: 86 | normalizer = en_tn.EnglishNormalizer() 87 | 88 | 89 | # 如果不是srt字幕 90 | if isinstance(text,str): 91 | self.queue_tts=[{ 92 | "text":normalizer(text.strip()) if normalizer else text.strip(), 93 | "voice":voice, 94 | "speed":speed, 95 | "filename":self.end_mp3name+".wav", 96 | "break":0 97 | }] 98 | else: 99 | self.queue_tts=[] 100 | length=len(text) 101 | for i,it in enumerate(text): 102 | fname=cfg.TEMP_FOLDER+'/'+cfg.get_md5(f'{it["text"]}-{voice}-{speed}')+'.wav' 103 | self.queue_tts.append({ 104 | "text": normalizer(it['text']) if normalizer else it['text'], 105 | "voice": voice, 106 | "start_time": it['start_time'], 107 | "end_time": it['end_time'], 108 | "speed": speed, 109 | "filename": fname, 110 | "break":text[i+1]['start_time']-it['end_time'] if self.keep_spacing and i index 133 | print(gs) # gs => graphemes/text 134 | print(ps) # ps => phonemes 135 | sf.write(it['filename'], audio, 24000) # save each audio file 136 | 137 | return merge_audio_segments(self.queue_tts,self.end_mp3name,self.keep_spacing,self.auto_speed) 138 | 139 | 140 | -------------------------------------------------------------------------------- /cfg/en_tn.py: -------------------------------------------------------------------------------- 1 | # 本代码复制自 https://github.com/OpenDocCN/python-code-anls/blob/master/docs/hf-tfm/models----clvp----number_normalizer.py.md 2 | # 以下为原文件所附带版权声明 3 | # 4 | # coding=utf-8 5 | # Copyright 2023 The HuggingFace Inc. team. 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | 19 | """English Normalizer class for CLVP.""" 20 | 21 | 22 | import re 23 | 24 | class EnglishNormalizer: 25 | def __init__(self): 26 | # List of (regular expression, replacement) pairs for abbreviations: 27 | self._abbreviations = [ 28 | # Compile regular expressions for abbreviations and their replacements 29 | (re.compile("\\b%s\\." % x[0], re.IGNORECASE), x[1]) 30 | for x in [ 31 | ("mrs", "misess"), 32 | ("mr", "mister"), 33 | ("dr", "doctor"), 34 | ("st", "saint"), 35 | ("co", "company"), 36 | ("jr", "junior"), 37 | ("maj", "major"), 38 | ("gen", "general"), 39 | ("drs", "doctors"), 40 | ("rev", "reverend"), 41 | ("lt", "lieutenant"), 42 | ("hon", "honorable"), 43 | ("sgt", "sergeant"), 44 | ("capt", "captain"), 45 | ("esq", "esquire"), 46 | ("ltd", "limited"), 47 | ("col", "colonel"), 48 | ("ft", "fort"), 49 | ] 50 | ] 51 | 52 | # List of English words for numbers 53 | self.ones = ["", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"] 54 | self.teens = [ 55 | "ten", 56 | "eleven", 57 | "twelve", 58 | "thirteen", 59 | "fourteen", 60 | "fifteen", 61 | "sixteen", 62 | "seventeen", 63 | "eighteen", 64 | "nineteen", 65 | ] 66 | self.tens = ["", "", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety"] 67 | def number_to_words(self, num: int) -> str: 68 | """ 69 | Converts numbers(`int`) to words(`str`). 70 | 71 | Please note that it only supports up to - "'nine hundred ninety-nine quadrillion, nine hundred ninety-nine 72 | trillion, nine hundred ninety-nine billion, nine hundred ninety-nine million, nine hundred ninety-nine 73 | thousand, nine hundred ninety-nine'" or `number_to_words(999_999_999_999_999_999)`. 74 | """ 75 | # 如果输入的数字为0,返回字符串 "zero" 76 | if num == 0: 77 | return "zero" 78 | # 如果输入的数字小于0,返回负数的英文表示,递归调用自身处理绝对值 79 | elif num < 0: 80 | return "minus " + self.number_to_words(abs(num)) 81 | # 处理0到9之间的数字,直接返回对应的英文表示 82 | elif num < 10: 83 | return self.ones[num] 84 | # 处理10到19之间的数字,直接返回对应的英文表示 85 | elif num < 20: 86 | return self.teens[num - 10] 87 | # 处理20到99之间的数字,分解为十位和个位,递归调用自身处理个位 88 | elif num < 100: 89 | return self.tens[num // 10] + ("-" + self.number_to_words(num % 10) if num % 10 != 0 else "") 90 | # 处理100到999之间的数字,分解为百位和剩余部分,递归调用自身处理剩余部分 91 | elif num < 1000: 92 | return ( 93 | self.ones[num // 100] + " hundred" + (" " + self.number_to_words(num % 100) if num % 100 != 0 else "") 94 | ) 95 | # 处理1000到999999之间的数字,分解为千位和剩余部分,递归调用自身处理剩余部分 96 | elif num < 1_000_000: 97 | return ( 98 | self.number_to_words(num // 1000) 99 | + " thousand" 100 | + (", " + self.number_to_words(num % 1000) if num % 1000 != 0 else "") 101 | ) 102 | # 处理1000000到999999999之间的数字,分解为百万位和剩余部分,递归调用自身处理剩余部分 103 | elif num < 1_000_000_000: 104 | return ( 105 | self.number_to_words(num // 1_000_000) 106 | + " million" 107 | + (", " + self.number_to_words(num % 1_000_000) if num % 1_000_000 != 0 else "") 108 | ) 109 | # 处理1000000000到999999999999之间的数字,分解为十亿位和剩余部分,递归调用自身处理剩余部分 110 | elif num < 1_000_000_000_000: 111 | return ( 112 | self.number_to_words(num // 1_000_000_000) 113 | + " billion" 114 | + (", " + self.number_to_words(num % 1_000_000_000) if num % 1_000_000_000 != 0 else "") 115 | ) 116 | # 处理1000000000000到999999999999999之间的数字,分解为万亿位和剩余部分,递归调用自身处理剩余部分 117 | elif num < 1_000_000_000_000_000: 118 | return ( 119 | self.number_to_words(num // 1_000_000_000_000) 120 | + " trillion" 121 | + (", " + self.number_to_words(num % 1_000_000_000_000) if num % 1_000_000_000_000 != 0 else "") 122 | ) 123 | # 处理1000000000000000到999999999999999999之间的数字,分解为千万亿位和剩余部分,递归调用自身处理剩余部分 124 | elif num < 1_000_000_000_000_000_000: 125 | return ( 126 | self.number_to_words(num // 1_000_000_000_000_000) 127 | + " quadrillion" 128 | + ( 129 | ", " + self.number_to_words(num % 1_000_000_000_000_000) 130 | if num % 1_000_000_000_000_000 != 0 131 | else "" 132 | ) 133 | ) 134 | # 处理超出范围的数字,返回字符串 "number out of range" 135 | else: 136 | return "number out of range" 137 | 138 | def convert_to_ascii(self, text: str) -> str: 139 | """ 140 | Converts unicode to ascii 141 | """ 142 | # 将Unicode文本转换为ASCII编码,忽略非ASCII字符 143 | return text.encode("ascii", "ignore").decode("utf-8") 144 | def _expand_dollars(self, m: str) -> str: 145 | """ 146 | This method is used to expand numerical dollar values into spoken words. 147 | """ 148 | # 匹配到的数字字符串,即货币值 149 | match = m.group(1) 150 | # 将货币值按小数点分割为整数部分和小数部分 151 | parts = match.split(".") 152 | if len(parts) > 2: 153 | return match + " dollars" # 如果小数点超过一个,返回原始字符串加上 " dollars" 表示异常格式 154 | 155 | # 解析整数部分和小数部分 156 | dollars = int(parts[0]) if parts[0] else 0 157 | cents = int(parts[1]) if len(parts) > 1 and parts[1] else 0 158 | # 根据货币值的整数部分和小数部分,构造成对应的英文表达形式 159 | if dollars and cents: 160 | dollar_unit = "dollar" if dollars == 1 else "dollars" 161 | cent_unit = "cent" if cents == 1 else "cents" 162 | return "%s %s, %s %s" % (dollars, dollar_unit, cents, cent_unit) 163 | elif dollars: 164 | dollar_unit = "dollar" if dollars == 1 else "dollars" 165 | return "%s %s" % (dollars, dollar_unit) 166 | elif cents: 167 | cent_unit = "cent" if cents == 1 else "cents" 168 | return "%s %s" % (cents, cent_unit) 169 | else: 170 | return "zero dollars" 171 | 172 | def _remove_commas(self, m: str) -> str: 173 | """ 174 | This method is used to remove commas from sentences. 175 | """ 176 | # 去除输入字符串中的逗号 177 | return m.group(1).replace(",", "") 178 | 179 | def _expand_decimal_point(self, m: str) -> str: 180 | """ 181 | This method is used to expand '.' into spoken word ' point '. 182 | """ 183 | # 将输入字符串中的点号 '.' 替换为单词 " point " 184 | return m.group(1).replace(".", " point ") 185 | 186 | def _expand_ordinal(self, num: str) -> str: 187 | """ 188 | This method is used to expand ordinals such as '1st', '2nd' into spoken words. 189 | """ 190 | # 定义英文序数词的后缀映射表 191 | ordinal_suffixes = {1: "st", 2: "nd", 3: "rd"} 192 | 193 | # 提取序数词的数字部分并转换为整数 194 | num = int(num.group(0)[:-2]) 195 | # 根据序数的不同情况选择正确的后缀 196 | if 10 <= num % 100 and num % 100 <= 20: 197 | suffix = "th" 198 | else: 199 | suffix = ordinal_suffixes.get(num % 10, "th") 200 | # 将整数转换为对应的英文序数词形式并添加后缀 201 | return self.number_to_words(num) + suffix 202 | 203 | def _expand_number(self, m: str) -> str: 204 | """ 205 | This method acts as a preprocessing step for numbers between 1000 and 3000 (same as the original repository, 206 | link : 207 | https://github.com/neonbjb/tortoise-tts/blob/4003544b6ff4b68c09856e04d3eff9da26d023c2/tortoise/utils/tokenizer.py#L86) 208 | """ 209 | # 提取匹配到的数字字符串并转换为整数 210 | num = int(m.group(0)) 211 | 212 | # 如果数字在 1000 到 3000 之间,按特定规则进行英文数字的扩展 213 | if num > 1000 and num < 3000: 214 | if num == 2000: 215 | return "two thousand" 216 | elif num > 2000 and num < 2010: 217 | return "two thousand " + self.number_to_words(num % 100) 218 | elif num % 100 == 0: 219 | return self.number_to_words(num // 100) + " hundred" 220 | else: 221 | return self.number_to_words(num) 222 | else: 223 | return self.number_to_words(num) 224 | 225 | 226 | # 此方法用于规范化文本中的数字,如将数字转换为单词,移除逗号等操作。 227 | def normalize_numbers(self, text: str) -> str: 228 | # 使用正则表达式替换匹配的数字和逗号,调用 self._remove_commas 方法 229 | text = re.sub(re.compile(r"([0-9][0-9\,]+[0-9])"), self._remove_commas, text) 230 | # 替换匹配的英镑金额为其单词表示形式 231 | text = re.sub(re.compile(r"£([0-9\,]*[0-9]+)"), r"\1 pounds", text) 232 | # 替换匹配的美元金额为其完整的金额表达形式,调用 self._expand_dollars 方法 233 | text = re.sub(re.compile(r"\$([0-9\.\,]*[0-9]+)"), self._expand_dollars, text) 234 | # 替换匹配的小数形式为其完整的数值表达形式,调用 self._expand_decimal_point 方法 235 | text = re.sub(re.compile(r"([0-9]+\.[0-9]+)"), self._expand_decimal_point, text) 236 | # 替换匹配的序数词(如1st、2nd)为其完整的序数词形式,调用 self._expand_ordinal 方法 237 | text = re.sub(re.compile(r"[0-9]+(st|nd|rd|th)"), self._expand_ordinal, text) 238 | # 替换匹配的数字为其完整的数值表达形式,调用 self._expand_number 方法 239 | text = re.sub(re.compile(r"[0-9]+"), self._expand_number, text) 240 | # 返回规范化后的文本 241 | return text 242 | 243 | # 扩展缩写词 244 | def expand_abbreviations(self, text: str) -> str: 245 | # 遍历缩写词及其对应的替换规则,使用正则表达式进行替换 246 | for regex, replacement in self._abbreviations: 247 | text = re.sub(regex, replacement, text) 248 | # 返回扩展后的文本 249 | return text 250 | 251 | # 去除多余的空白字符 252 | def collapse_whitespace(self, text: str) -> str: 253 | # 使用正则表达式将多个连续的空白字符替换为一个空格 254 | return re.sub(re.compile(r"\s+"), " ", text) 255 | 256 | # 对象可调用方法,将文本转换为 ASCII 码,将数字转换为完整形式,并扩展缩写词 257 | def __call__(self, text): 258 | # 将文本转换为 ASCII 码表示形式 259 | text = self.convert_to_ascii(text) 260 | # 将文本转换为小写形式 261 | text = text.lower() 262 | # 规范化文本中的数字 263 | text = self.normalize_numbers(text) 264 | # 扩展文本中的缩写词 265 | text = self.expand_abbreviations(text) 266 | # 去除文本中的多余空白字符 267 | text = self.collapse_whitespace(text) 268 | # 移除文本中的双引号 269 | text = text.replace('"', "") 270 | 271 | # 返回处理后的文本 272 | return text -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | kokoro 2 | numpy==1.26.4 3 | pydantic 4 | pydantic_core 5 | requests 6 | soundfile 7 | torch 8 | torchaudio 9 | transformers 10 | waitress 11 | flask 12 | pydub 13 | flask_cors 14 | pypinyin 15 | ordered_set 16 | jieba 17 | cn2an -------------------------------------------------------------------------------- /start.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | @chcp 65001 3 | 4 | echo "启动中请稍等..." 5 | 6 | call %cd%/runtime/python ./app.py 7 | 8 | pause -------------------------------------------------------------------------------- /static/bootstrap.bundle.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v5.3.0 (https://getbootstrap.com/) 3 | * Copyright 2011-2023 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) 5 | */ 6 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e()}(this,(function(){"use strict";const t=new Map,e={set(e,i,n){t.has(e)||t.set(e,new Map);const s=t.get(e);s.has(i)||0===s.size?s.set(i,n):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(s.keys())[0]}.`)},get:(e,i)=>t.has(e)&&t.get(e).get(i)||null,remove(e,i){if(!t.has(e))return;const n=t.get(e);n.delete(i),0===n.size&&t.delete(e)}},i="transitionend",n=t=>(t&&window.CSS&&window.CSS.escape&&(t=t.replace(/#([^\s"#']+)/g,((t,e)=>`#${CSS.escape(e)}`))),t),s=t=>{t.dispatchEvent(new Event(i))},o=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),r=t=>o(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(n(t)):null,a=t=>{if(!o(t)||0===t.getClientRects().length)return!1;const e="visible"===getComputedStyle(t).getPropertyValue("visibility"),i=t.closest("details:not([open])");if(!i)return e;if(i!==t){const e=t.closest("summary");if(e&&e.parentNode!==i)return!1;if(null===e)return!1}return e},l=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),c=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?c(t.parentNode):null},h=()=>{},d=t=>{t.offsetHeight},u=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,f=[],p=()=>"rtl"===document.documentElement.dir,m=t=>{var e;e=()=>{const e=u();if(e){const i=t.NAME,n=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=n,t.jQueryInterface)}},"loading"===document.readyState?(f.length||document.addEventListener("DOMContentLoaded",(()=>{for(const t of f)t()})),f.push(e)):e()},g=(t,e=[],i=t)=>"function"==typeof t?t(...e):i,_=(t,e,n=!0)=>{if(!n)return void g(t);const o=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const n=Number.parseFloat(e),s=Number.parseFloat(i);return n||s?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(e)+5;let r=!1;const a=({target:n})=>{n===e&&(r=!0,e.removeEventListener(i,a),g(t))};e.addEventListener(i,a),setTimeout((()=>{r||s(e)}),o)},b=(t,e,i,n)=>{const s=t.length;let o=t.indexOf(e);return-1===o?!i&&n?t[s-1]:t[0]:(o+=i?1:-1,n&&(o=(o+s)%s),t[Math.max(0,Math.min(o,s-1))])},v=/[^.]*(?=\..*)\.|.*/,y=/\..*/,w=/::\d+$/,A={};let E=1;const T={mouseenter:"mouseover",mouseleave:"mouseout"},C=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function O(t,e){return e&&`${e}::${E++}`||t.uidEvent||E++}function x(t){const e=O(t);return t.uidEvent=e,A[e]=A[e]||{},A[e]}function k(t,e,i=null){return Object.values(t).find((t=>t.callable===e&&t.delegationSelector===i))}function L(t,e,i){const n="string"==typeof e,s=n?i:e||i;let o=N(t);return C.has(o)||(o=t),[n,s,o]}function S(t,e,i,n,s){if("string"!=typeof e||!t)return;let[o,r,a]=L(e,i,n);if(e in T){const t=t=>function(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};r=t(r)}const l=x(t),c=l[a]||(l[a]={}),h=k(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&s);const d=O(r,e.replace(v,"")),u=o?function(t,e,i){return function n(s){const o=t.querySelectorAll(e);for(let{target:r}=s;r&&r!==this;r=r.parentNode)for(const a of o)if(a===r)return M(s,{delegateTarget:r}),n.oneOff&&P.off(t,s.type,e,i),i.apply(r,[s])}}(t,i,r):function(t,e){return function i(n){return M(n,{delegateTarget:t}),i.oneOff&&P.off(t,n.type,e),e.apply(t,[n])}}(t,r);u.delegationSelector=o?i:null,u.callable=r,u.oneOff=s,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function D(t,e,i,n,s){const o=k(e[i],n,s);o&&(t.removeEventListener(i,o,Boolean(s)),delete e[i][o.uidEvent])}function I(t,e,i,n){const s=e[i]||{};for(const[o,r]of Object.entries(s))o.includes(n)&&D(t,e,i,r.callable,r.delegationSelector)}function N(t){return t=t.replace(y,""),T[t]||t}const P={on(t,e,i,n){S(t,e,i,n,!1)},one(t,e,i,n){S(t,e,i,n,!0)},off(t,e,i,n){if("string"!=typeof e||!t)return;const[s,o,r]=L(e,i,n),a=r!==e,l=x(t),c=l[r]||{},h=e.startsWith(".");if(void 0===o){if(h)for(const i of Object.keys(l))I(t,l,i,e.slice(1));for(const[i,n]of Object.entries(c)){const s=i.replace(w,"");a&&!e.includes(s)||D(t,l,r,n.callable,n.delegationSelector)}}else{if(!Object.keys(c).length)return;D(t,l,r,o,s?i:null)}},trigger(t,e,i){if("string"!=typeof e||!t)return null;const n=u();let s=null,o=!0,r=!0,a=!1;e!==N(e)&&n&&(s=n.Event(e,i),n(t).trigger(s),o=!s.isPropagationStopped(),r=!s.isImmediatePropagationStopped(),a=s.isDefaultPrevented());const l=M(new Event(e,{bubbles:o,cancelable:!0}),i);return a&&l.preventDefault(),r&&t.dispatchEvent(l),l.defaultPrevented&&s&&s.preventDefault(),l}};function M(t,e={}){for(const[i,n]of Object.entries(e))try{t[i]=n}catch(e){Object.defineProperty(t,i,{configurable:!0,get:()=>n})}return t}function j(t){if("true"===t)return!0;if("false"===t)return!1;if(t===Number(t).toString())return Number(t);if(""===t||"null"===t)return null;if("string"!=typeof t)return t;try{return JSON.parse(decodeURIComponent(t))}catch(e){return t}}function F(t){return t.replace(/[A-Z]/g,(t=>`-${t.toLowerCase()}`))}const H={setDataAttribute(t,e,i){t.setAttribute(`data-bs-${F(e)}`,i)},removeDataAttribute(t,e){t.removeAttribute(`data-bs-${F(e)}`)},getDataAttributes(t){if(!t)return{};const e={},i=Object.keys(t.dataset).filter((t=>t.startsWith("bs")&&!t.startsWith("bsConfig")));for(const n of i){let i=n.replace(/^bs/,"");i=i.charAt(0).toLowerCase()+i.slice(1,i.length),e[i]=j(t.dataset[n])}return e},getDataAttribute:(t,e)=>j(t.getAttribute(`data-bs-${F(e)}`))};class ${static get Default(){return{}}static get DefaultType(){return{}}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}_getConfig(t){return t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t}_mergeConfigObj(t,e){const i=o(e)?H.getDataAttribute(e,"config"):{};return{...this.constructor.Default,..."object"==typeof i?i:{},...o(e)?H.getDataAttributes(e):{},..."object"==typeof t?t:{}}}_typeCheckConfig(t,e=this.constructor.DefaultType){for(const[n,s]of Object.entries(e)){const e=t[n],r=o(e)?"element":null==(i=e)?`${i}`:Object.prototype.toString.call(i).match(/\s([a-z]+)/i)[1].toLowerCase();if(!new RegExp(s).test(r))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${n}" provided type "${r}" but expected type "${s}".`)}var i}}class W extends ${constructor(t,i){super(),(t=r(t))&&(this._element=t,this._config=this._getConfig(i),e.set(this._element,this.constructor.DATA_KEY,this))}dispose(){e.remove(this._element,this.constructor.DATA_KEY),P.off(this._element,this.constructor.EVENT_KEY);for(const t of Object.getOwnPropertyNames(this))this[t]=null}_queueCallback(t,e,i=!0){_(t,e,i)}_getConfig(t){return t=this._mergeConfigObj(t,this._element),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}static getInstance(t){return e.get(r(t),this.DATA_KEY)}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,"object"==typeof e?e:null)}static get VERSION(){return"5.3.0"}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}static eventName(t){return`${t}${this.EVENT_KEY}`}}const B=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i=`#${i.split("#")[1]}`),e=i&&"#"!==i?i.trim():null}return n(e)},z={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter((t=>t.matches(e))),parents(t,e){const i=[];let n=t.parentNode.closest(e);for(;n;)i.push(n),n=n.parentNode.closest(e);return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]},focusableChildren(t){const e=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map((t=>`${t}:not([tabindex^="-"])`)).join(",");return this.find(e,t).filter((t=>!l(t)&&a(t)))},getSelectorFromElement(t){const e=B(t);return e&&z.findOne(e)?e:null},getElementFromSelector(t){const e=B(t);return e?z.findOne(e):null},getMultipleElementsFromSelector(t){const e=B(t);return e?z.find(e):[]}},R=(t,e="hide")=>{const i=`click.dismiss${t.EVENT_KEY}`,n=t.NAME;P.on(document,i,`[data-bs-dismiss="${n}"]`,(function(i){if(["A","AREA"].includes(this.tagName)&&i.preventDefault(),l(this))return;const s=z.getElementFromSelector(this)||this.closest(`.${n}`);t.getOrCreateInstance(s)[e]()}))};class q extends W{static get NAME(){return"alert"}close(){if(P.trigger(this._element,"close.bs.alert").defaultPrevented)return;this._element.classList.remove("show");const t=this._element.classList.contains("fade");this._queueCallback((()=>this._destroyElement()),this._element,t)}_destroyElement(){this._element.remove(),P.trigger(this._element,"closed.bs.alert"),this.dispose()}static jQueryInterface(t){return this.each((function(){const e=q.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}R(q,"close"),m(q);const V='[data-bs-toggle="button"]';class K extends W{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each((function(){const e=K.getOrCreateInstance(this);"toggle"===t&&e[t]()}))}}P.on(document,"click.bs.button.data-api",V,(t=>{t.preventDefault();const e=t.target.closest(V);K.getOrCreateInstance(e).toggle()})),m(K);const Q={endCallback:null,leftCallback:null,rightCallback:null},X={endCallback:"(function|null)",leftCallback:"(function|null)",rightCallback:"(function|null)"};class Y extends ${constructor(t,e){super(),this._element=t,t&&Y.isSupported()&&(this._config=this._getConfig(e),this._deltaX=0,this._supportPointerEvents=Boolean(window.PointerEvent),this._initEvents())}static get Default(){return Q}static get DefaultType(){return X}static get NAME(){return"swipe"}dispose(){P.off(this._element,".bs.swipe")}_start(t){this._supportPointerEvents?this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX):this._deltaX=t.touches[0].clientX}_end(t){this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX-this._deltaX),this._handleSwipe(),g(this._config.endCallback)}_move(t){this._deltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this._deltaX}_handleSwipe(){const t=Math.abs(this._deltaX);if(t<=40)return;const e=t/this._deltaX;this._deltaX=0,e&&g(e>0?this._config.rightCallback:this._config.leftCallback)}_initEvents(){this._supportPointerEvents?(P.on(this._element,"pointerdown.bs.swipe",(t=>this._start(t))),P.on(this._element,"pointerup.bs.swipe",(t=>this._end(t))),this._element.classList.add("pointer-event")):(P.on(this._element,"touchstart.bs.swipe",(t=>this._start(t))),P.on(this._element,"touchmove.bs.swipe",(t=>this._move(t))),P.on(this._element,"touchend.bs.swipe",(t=>this._end(t))))}_eventIsPointerPenTouch(t){return this._supportPointerEvents&&("pen"===t.pointerType||"touch"===t.pointerType)}static isSupported(){return"ontouchstart"in document.documentElement||navigator.maxTouchPoints>0}}const U="next",G="prev",J="left",Z="right",tt="slid.bs.carousel",et="carousel",it="active",nt={ArrowLeft:Z,ArrowRight:J},st={interval:5e3,keyboard:!0,pause:"hover",ride:!1,touch:!0,wrap:!0},ot={interval:"(number|boolean)",keyboard:"boolean",pause:"(string|boolean)",ride:"(boolean|string)",touch:"boolean",wrap:"boolean"};class rt extends W{constructor(t,e){super(t,e),this._interval=null,this._activeElement=null,this._isSliding=!1,this.touchTimeout=null,this._swipeHelper=null,this._indicatorsElement=z.findOne(".carousel-indicators",this._element),this._addEventListeners(),this._config.ride===et&&this.cycle()}static get Default(){return st}static get DefaultType(){return ot}static get NAME(){return"carousel"}next(){this._slide(U)}nextWhenVisible(){!document.hidden&&a(this._element)&&this.next()}prev(){this._slide(G)}pause(){this._isSliding&&s(this._element),this._clearInterval()}cycle(){this._clearInterval(),this._updateInterval(),this._interval=setInterval((()=>this.nextWhenVisible()),this._config.interval)}_maybeEnableCycle(){this._config.ride&&(this._isSliding?P.one(this._element,tt,(()=>this.cycle())):this.cycle())}to(t){const e=this._getItems();if(t>e.length-1||t<0)return;if(this._isSliding)return void P.one(this._element,tt,(()=>this.to(t)));const i=this._getItemIndex(this._getActive());if(i===t)return;const n=t>i?U:G;this._slide(n,e[t])}dispose(){this._swipeHelper&&this._swipeHelper.dispose(),super.dispose()}_configAfterMerge(t){return t.defaultInterval=t.interval,t}_addEventListeners(){this._config.keyboard&&P.on(this._element,"keydown.bs.carousel",(t=>this._keydown(t))),"hover"===this._config.pause&&(P.on(this._element,"mouseenter.bs.carousel",(()=>this.pause())),P.on(this._element,"mouseleave.bs.carousel",(()=>this._maybeEnableCycle()))),this._config.touch&&Y.isSupported()&&this._addTouchEventListeners()}_addTouchEventListeners(){for(const t of z.find(".carousel-item img",this._element))P.on(t,"dragstart.bs.carousel",(t=>t.preventDefault()));const t={leftCallback:()=>this._slide(this._directionToOrder(J)),rightCallback:()=>this._slide(this._directionToOrder(Z)),endCallback:()=>{"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout((()=>this._maybeEnableCycle()),500+this._config.interval))}};this._swipeHelper=new Y(this._element,t)}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=nt[t.key];e&&(t.preventDefault(),this._slide(this._directionToOrder(e)))}_getItemIndex(t){return this._getItems().indexOf(t)}_setActiveIndicatorElement(t){if(!this._indicatorsElement)return;const e=z.findOne(".active",this._indicatorsElement);e.classList.remove(it),e.removeAttribute("aria-current");const i=z.findOne(`[data-bs-slide-to="${t}"]`,this._indicatorsElement);i&&(i.classList.add(it),i.setAttribute("aria-current","true"))}_updateInterval(){const t=this._activeElement||this._getActive();if(!t)return;const e=Number.parseInt(t.getAttribute("data-bs-interval"),10);this._config.interval=e||this._config.defaultInterval}_slide(t,e=null){if(this._isSliding)return;const i=this._getActive(),n=t===U,s=e||b(this._getItems(),i,n,this._config.wrap);if(s===i)return;const o=this._getItemIndex(s),r=e=>P.trigger(this._element,e,{relatedTarget:s,direction:this._orderToDirection(t),from:this._getItemIndex(i),to:o});if(r("slide.bs.carousel").defaultPrevented)return;if(!i||!s)return;const a=Boolean(this._interval);this.pause(),this._isSliding=!0,this._setActiveIndicatorElement(o),this._activeElement=s;const l=n?"carousel-item-start":"carousel-item-end",c=n?"carousel-item-next":"carousel-item-prev";s.classList.add(c),d(s),i.classList.add(l),s.classList.add(l),this._queueCallback((()=>{s.classList.remove(l,c),s.classList.add(it),i.classList.remove(it,c,l),this._isSliding=!1,r(tt)}),i,this._isAnimated()),a&&this.cycle()}_isAnimated(){return this._element.classList.contains("slide")}_getActive(){return z.findOne(".active.carousel-item",this._element)}_getItems(){return z.find(".carousel-item",this._element)}_clearInterval(){this._interval&&(clearInterval(this._interval),this._interval=null)}_directionToOrder(t){return p()?t===J?G:U:t===J?U:G}_orderToDirection(t){return p()?t===G?J:Z:t===G?Z:J}static jQueryInterface(t){return this.each((function(){const e=rt.getOrCreateInstance(this,t);if("number"!=typeof t){if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}else e.to(t)}))}}P.on(document,"click.bs.carousel.data-api","[data-bs-slide], [data-bs-slide-to]",(function(t){const e=z.getElementFromSelector(this);if(!e||!e.classList.contains(et))return;t.preventDefault();const i=rt.getOrCreateInstance(e),n=this.getAttribute("data-bs-slide-to");return n?(i.to(n),void i._maybeEnableCycle()):"next"===H.getDataAttribute(this,"slide")?(i.next(),void i._maybeEnableCycle()):(i.prev(),void i._maybeEnableCycle())})),P.on(window,"load.bs.carousel.data-api",(()=>{const t=z.find('[data-bs-ride="carousel"]');for(const e of t)rt.getOrCreateInstance(e)})),m(rt);const at="show",lt="collapse",ct="collapsing",ht='[data-bs-toggle="collapse"]',dt={parent:null,toggle:!0},ut={parent:"(null|element)",toggle:"boolean"};class ft extends W{constructor(t,e){super(t,e),this._isTransitioning=!1,this._triggerArray=[];const i=z.find(ht);for(const t of i){const e=z.getSelectorFromElement(t),i=z.find(e).filter((t=>t===this._element));null!==e&&i.length&&this._triggerArray.push(t)}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return dt}static get DefaultType(){return ut}static get NAME(){return"collapse"}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let t=[];if(this._config.parent&&(t=this._getFirstLevelChildren(".collapse.show, .collapse.collapsing").filter((t=>t!==this._element)).map((t=>ft.getOrCreateInstance(t,{toggle:!1})))),t.length&&t[0]._isTransitioning)return;if(P.trigger(this._element,"show.bs.collapse").defaultPrevented)return;for(const e of t)e.hide();const e=this._getDimension();this._element.classList.remove(lt),this._element.classList.add(ct),this._element.style[e]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const i=`scroll${e[0].toUpperCase()+e.slice(1)}`;this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(ct),this._element.classList.add(lt,at),this._element.style[e]="",P.trigger(this._element,"shown.bs.collapse")}),this._element,!0),this._element.style[e]=`${this._element[i]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if(P.trigger(this._element,"hide.bs.collapse").defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,d(this._element),this._element.classList.add(ct),this._element.classList.remove(lt,at);for(const t of this._triggerArray){const e=z.getElementFromSelector(t);e&&!this._isShown(e)&&this._addAriaAndCollapsedClass([t],!1)}this._isTransitioning=!0,this._element.style[t]="",this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(ct),this._element.classList.add(lt),P.trigger(this._element,"hidden.bs.collapse")}),this._element,!0)}_isShown(t=this._element){return t.classList.contains(at)}_configAfterMerge(t){return t.toggle=Boolean(t.toggle),t.parent=r(t.parent),t}_getDimension(){return this._element.classList.contains("collapse-horizontal")?"width":"height"}_initializeChildren(){if(!this._config.parent)return;const t=this._getFirstLevelChildren(ht);for(const e of t){const t=z.getElementFromSelector(e);t&&this._addAriaAndCollapsedClass([e],this._isShown(t))}}_getFirstLevelChildren(t){const e=z.find(":scope .collapse .collapse",this._config.parent);return z.find(t,this._config.parent).filter((t=>!e.includes(t)))}_addAriaAndCollapsedClass(t,e){if(t.length)for(const i of t)i.classList.toggle("collapsed",!e),i.setAttribute("aria-expanded",e)}static jQueryInterface(t){const e={};return"string"==typeof t&&/show|hide/.test(t)&&(e.toggle=!1),this.each((function(){const i=ft.getOrCreateInstance(this,e);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t]()}}))}}P.on(document,"click.bs.collapse.data-api",ht,(function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();for(const t of z.getMultipleElementsFromSelector(this))ft.getOrCreateInstance(t,{toggle:!1}).toggle()})),m(ft);var pt="top",mt="bottom",gt="right",_t="left",bt="auto",vt=[pt,mt,gt,_t],yt="start",wt="end",At="clippingParents",Et="viewport",Tt="popper",Ct="reference",Ot=vt.reduce((function(t,e){return t.concat([e+"-"+yt,e+"-"+wt])}),[]),xt=[].concat(vt,[bt]).reduce((function(t,e){return t.concat([e,e+"-"+yt,e+"-"+wt])}),[]),kt="beforeRead",Lt="read",St="afterRead",Dt="beforeMain",It="main",Nt="afterMain",Pt="beforeWrite",Mt="write",jt="afterWrite",Ft=[kt,Lt,St,Dt,It,Nt,Pt,Mt,jt];function Ht(t){return t?(t.nodeName||"").toLowerCase():null}function $t(t){if(null==t)return window;if("[object Window]"!==t.toString()){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function Wt(t){return t instanceof $t(t).Element||t instanceof Element}function Bt(t){return t instanceof $t(t).HTMLElement||t instanceof HTMLElement}function zt(t){return"undefined"!=typeof ShadowRoot&&(t instanceof $t(t).ShadowRoot||t instanceof ShadowRoot)}const Rt={name:"applyStyles",enabled:!0,phase:"write",fn:function(t){var e=t.state;Object.keys(e.elements).forEach((function(t){var i=e.styles[t]||{},n=e.attributes[t]||{},s=e.elements[t];Bt(s)&&Ht(s)&&(Object.assign(s.style,i),Object.keys(n).forEach((function(t){var e=n[t];!1===e?s.removeAttribute(t):s.setAttribute(t,!0===e?"":e)})))}))},effect:function(t){var e=t.state,i={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,i.popper),e.styles=i,e.elements.arrow&&Object.assign(e.elements.arrow.style,i.arrow),function(){Object.keys(e.elements).forEach((function(t){var n=e.elements[t],s=e.attributes[t]||{},o=Object.keys(e.styles.hasOwnProperty(t)?e.styles[t]:i[t]).reduce((function(t,e){return t[e]="",t}),{});Bt(n)&&Ht(n)&&(Object.assign(n.style,o),Object.keys(s).forEach((function(t){n.removeAttribute(t)})))}))}},requires:["computeStyles"]};function qt(t){return t.split("-")[0]}var Vt=Math.max,Kt=Math.min,Qt=Math.round;function Xt(){var t=navigator.userAgentData;return null!=t&&t.brands&&Array.isArray(t.brands)?t.brands.map((function(t){return t.brand+"/"+t.version})).join(" "):navigator.userAgent}function Yt(){return!/^((?!chrome|android).)*safari/i.test(Xt())}function Ut(t,e,i){void 0===e&&(e=!1),void 0===i&&(i=!1);var n=t.getBoundingClientRect(),s=1,o=1;e&&Bt(t)&&(s=t.offsetWidth>0&&Qt(n.width)/t.offsetWidth||1,o=t.offsetHeight>0&&Qt(n.height)/t.offsetHeight||1);var r=(Wt(t)?$t(t):window).visualViewport,a=!Yt()&&i,l=(n.left+(a&&r?r.offsetLeft:0))/s,c=(n.top+(a&&r?r.offsetTop:0))/o,h=n.width/s,d=n.height/o;return{width:h,height:d,top:c,right:l+h,bottom:c+d,left:l,x:l,y:c}}function Gt(t){var e=Ut(t),i=t.offsetWidth,n=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-n)<=1&&(n=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:n}}function Jt(t,e){var i=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(i&&zt(i)){var n=e;do{if(n&&t.isSameNode(n))return!0;n=n.parentNode||n.host}while(n)}return!1}function Zt(t){return $t(t).getComputedStyle(t)}function te(t){return["table","td","th"].indexOf(Ht(t))>=0}function ee(t){return((Wt(t)?t.ownerDocument:t.document)||window.document).documentElement}function ie(t){return"html"===Ht(t)?t:t.assignedSlot||t.parentNode||(zt(t)?t.host:null)||ee(t)}function ne(t){return Bt(t)&&"fixed"!==Zt(t).position?t.offsetParent:null}function se(t){for(var e=$t(t),i=ne(t);i&&te(i)&&"static"===Zt(i).position;)i=ne(i);return i&&("html"===Ht(i)||"body"===Ht(i)&&"static"===Zt(i).position)?e:i||function(t){var e=/firefox/i.test(Xt());if(/Trident/i.test(Xt())&&Bt(t)&&"fixed"===Zt(t).position)return null;var i=ie(t);for(zt(i)&&(i=i.host);Bt(i)&&["html","body"].indexOf(Ht(i))<0;){var n=Zt(i);if("none"!==n.transform||"none"!==n.perspective||"paint"===n.contain||-1!==["transform","perspective"].indexOf(n.willChange)||e&&"filter"===n.willChange||e&&n.filter&&"none"!==n.filter)return i;i=i.parentNode}return null}(t)||e}function oe(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}function re(t,e,i){return Vt(t,Kt(e,i))}function ae(t){return Object.assign({},{top:0,right:0,bottom:0,left:0},t)}function le(t,e){return e.reduce((function(e,i){return e[i]=t,e}),{})}const ce={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,i=t.state,n=t.name,s=t.options,o=i.elements.arrow,r=i.modifiersData.popperOffsets,a=qt(i.placement),l=oe(a),c=[_t,gt].indexOf(a)>=0?"height":"width";if(o&&r){var h=function(t,e){return ae("number"!=typeof(t="function"==typeof t?t(Object.assign({},e.rects,{placement:e.placement})):t)?t:le(t,vt))}(s.padding,i),d=Gt(o),u="y"===l?pt:_t,f="y"===l?mt:gt,p=i.rects.reference[c]+i.rects.reference[l]-r[l]-i.rects.popper[c],m=r[l]-i.rects.reference[l],g=se(o),_=g?"y"===l?g.clientHeight||0:g.clientWidth||0:0,b=p/2-m/2,v=h[u],y=_-d[c]-h[f],w=_/2-d[c]/2+b,A=re(v,w,y),E=l;i.modifiersData[n]=((e={})[E]=A,e.centerOffset=A-w,e)}},effect:function(t){var e=t.state,i=t.options.element,n=void 0===i?"[data-popper-arrow]":i;null!=n&&("string"!=typeof n||(n=e.elements.popper.querySelector(n)))&&Jt(e.elements.popper,n)&&(e.elements.arrow=n)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function he(t){return t.split("-")[1]}var de={top:"auto",right:"auto",bottom:"auto",left:"auto"};function ue(t){var e,i=t.popper,n=t.popperRect,s=t.placement,o=t.variation,r=t.offsets,a=t.position,l=t.gpuAcceleration,c=t.adaptive,h=t.roundOffsets,d=t.isFixed,u=r.x,f=void 0===u?0:u,p=r.y,m=void 0===p?0:p,g="function"==typeof h?h({x:f,y:m}):{x:f,y:m};f=g.x,m=g.y;var _=r.hasOwnProperty("x"),b=r.hasOwnProperty("y"),v=_t,y=pt,w=window;if(c){var A=se(i),E="clientHeight",T="clientWidth";A===$t(i)&&"static"!==Zt(A=ee(i)).position&&"absolute"===a&&(E="scrollHeight",T="scrollWidth"),(s===pt||(s===_t||s===gt)&&o===wt)&&(y=mt,m-=(d&&A===w&&w.visualViewport?w.visualViewport.height:A[E])-n.height,m*=l?1:-1),s!==_t&&(s!==pt&&s!==mt||o!==wt)||(v=gt,f-=(d&&A===w&&w.visualViewport?w.visualViewport.width:A[T])-n.width,f*=l?1:-1)}var C,O=Object.assign({position:a},c&&de),x=!0===h?function(t,e){var i=t.x,n=t.y,s=e.devicePixelRatio||1;return{x:Qt(i*s)/s||0,y:Qt(n*s)/s||0}}({x:f,y:m},$t(i)):{x:f,y:m};return f=x.x,m=x.y,l?Object.assign({},O,((C={})[y]=b?"0":"",C[v]=_?"0":"",C.transform=(w.devicePixelRatio||1)<=1?"translate("+f+"px, "+m+"px)":"translate3d("+f+"px, "+m+"px, 0)",C)):Object.assign({},O,((e={})[y]=b?m+"px":"",e[v]=_?f+"px":"",e.transform="",e))}const fe={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,i=t.options,n=i.gpuAcceleration,s=void 0===n||n,o=i.adaptive,r=void 0===o||o,a=i.roundOffsets,l=void 0===a||a,c={placement:qt(e.placement),variation:he(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:s,isFixed:"fixed"===e.options.strategy};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,ue(Object.assign({},c,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:r,roundOffsets:l})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,ue(Object.assign({},c,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:l})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})},data:{}};var pe={passive:!0};const me={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,i=t.instance,n=t.options,s=n.scroll,o=void 0===s||s,r=n.resize,a=void 0===r||r,l=$t(e.elements.popper),c=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&c.forEach((function(t){t.addEventListener("scroll",i.update,pe)})),a&&l.addEventListener("resize",i.update,pe),function(){o&&c.forEach((function(t){t.removeEventListener("scroll",i.update,pe)})),a&&l.removeEventListener("resize",i.update,pe)}},data:{}};var ge={left:"right",right:"left",bottom:"top",top:"bottom"};function _e(t){return t.replace(/left|right|bottom|top/g,(function(t){return ge[t]}))}var be={start:"end",end:"start"};function ve(t){return t.replace(/start|end/g,(function(t){return be[t]}))}function ye(t){var e=$t(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function we(t){return Ut(ee(t)).left+ye(t).scrollLeft}function Ae(t){var e=Zt(t),i=e.overflow,n=e.overflowX,s=e.overflowY;return/auto|scroll|overlay|hidden/.test(i+s+n)}function Ee(t){return["html","body","#document"].indexOf(Ht(t))>=0?t.ownerDocument.body:Bt(t)&&Ae(t)?t:Ee(ie(t))}function Te(t,e){var i;void 0===e&&(e=[]);var n=Ee(t),s=n===(null==(i=t.ownerDocument)?void 0:i.body),o=$t(n),r=s?[o].concat(o.visualViewport||[],Ae(n)?n:[]):n,a=e.concat(r);return s?a:a.concat(Te(ie(r)))}function Ce(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function Oe(t,e,i){return e===Et?Ce(function(t,e){var i=$t(t),n=ee(t),s=i.visualViewport,o=n.clientWidth,r=n.clientHeight,a=0,l=0;if(s){o=s.width,r=s.height;var c=Yt();(c||!c&&"fixed"===e)&&(a=s.offsetLeft,l=s.offsetTop)}return{width:o,height:r,x:a+we(t),y:l}}(t,i)):Wt(e)?function(t,e){var i=Ut(t,!1,"fixed"===e);return i.top=i.top+t.clientTop,i.left=i.left+t.clientLeft,i.bottom=i.top+t.clientHeight,i.right=i.left+t.clientWidth,i.width=t.clientWidth,i.height=t.clientHeight,i.x=i.left,i.y=i.top,i}(e,i):Ce(function(t){var e,i=ee(t),n=ye(t),s=null==(e=t.ownerDocument)?void 0:e.body,o=Vt(i.scrollWidth,i.clientWidth,s?s.scrollWidth:0,s?s.clientWidth:0),r=Vt(i.scrollHeight,i.clientHeight,s?s.scrollHeight:0,s?s.clientHeight:0),a=-n.scrollLeft+we(t),l=-n.scrollTop;return"rtl"===Zt(s||i).direction&&(a+=Vt(i.clientWidth,s?s.clientWidth:0)-o),{width:o,height:r,x:a,y:l}}(ee(t)))}function xe(t){var e,i=t.reference,n=t.element,s=t.placement,o=s?qt(s):null,r=s?he(s):null,a=i.x+i.width/2-n.width/2,l=i.y+i.height/2-n.height/2;switch(o){case pt:e={x:a,y:i.y-n.height};break;case mt:e={x:a,y:i.y+i.height};break;case gt:e={x:i.x+i.width,y:l};break;case _t:e={x:i.x-n.width,y:l};break;default:e={x:i.x,y:i.y}}var c=o?oe(o):null;if(null!=c){var h="y"===c?"height":"width";switch(r){case yt:e[c]=e[c]-(i[h]/2-n[h]/2);break;case wt:e[c]=e[c]+(i[h]/2-n[h]/2)}}return e}function ke(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=void 0===n?t.placement:n,o=i.strategy,r=void 0===o?t.strategy:o,a=i.boundary,l=void 0===a?At:a,c=i.rootBoundary,h=void 0===c?Et:c,d=i.elementContext,u=void 0===d?Tt:d,f=i.altBoundary,p=void 0!==f&&f,m=i.padding,g=void 0===m?0:m,_=ae("number"!=typeof g?g:le(g,vt)),b=u===Tt?Ct:Tt,v=t.rects.popper,y=t.elements[p?b:u],w=function(t,e,i,n){var s="clippingParents"===e?function(t){var e=Te(ie(t)),i=["absolute","fixed"].indexOf(Zt(t).position)>=0&&Bt(t)?se(t):t;return Wt(i)?e.filter((function(t){return Wt(t)&&Jt(t,i)&&"body"!==Ht(t)})):[]}(t):[].concat(e),o=[].concat(s,[i]),r=o[0],a=o.reduce((function(e,i){var s=Oe(t,i,n);return e.top=Vt(s.top,e.top),e.right=Kt(s.right,e.right),e.bottom=Kt(s.bottom,e.bottom),e.left=Vt(s.left,e.left),e}),Oe(t,r,n));return a.width=a.right-a.left,a.height=a.bottom-a.top,a.x=a.left,a.y=a.top,a}(Wt(y)?y:y.contextElement||ee(t.elements.popper),l,h,r),A=Ut(t.elements.reference),E=xe({reference:A,element:v,strategy:"absolute",placement:s}),T=Ce(Object.assign({},v,E)),C=u===Tt?T:A,O={top:w.top-C.top+_.top,bottom:C.bottom-w.bottom+_.bottom,left:w.left-C.left+_.left,right:C.right-w.right+_.right},x=t.modifiersData.offset;if(u===Tt&&x){var k=x[s];Object.keys(O).forEach((function(t){var e=[gt,mt].indexOf(t)>=0?1:-1,i=[pt,mt].indexOf(t)>=0?"y":"x";O[t]+=k[i]*e}))}return O}function Le(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=i.boundary,o=i.rootBoundary,r=i.padding,a=i.flipVariations,l=i.allowedAutoPlacements,c=void 0===l?xt:l,h=he(n),d=h?a?Ot:Ot.filter((function(t){return he(t)===h})):vt,u=d.filter((function(t){return c.indexOf(t)>=0}));0===u.length&&(u=d);var f=u.reduce((function(e,i){return e[i]=ke(t,{placement:i,boundary:s,rootBoundary:o,padding:r})[qt(i)],e}),{});return Object.keys(f).sort((function(t,e){return f[t]-f[e]}))}const Se={name:"flip",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name;if(!e.modifiersData[n]._skip){for(var s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0===r||r,l=i.fallbackPlacements,c=i.padding,h=i.boundary,d=i.rootBoundary,u=i.altBoundary,f=i.flipVariations,p=void 0===f||f,m=i.allowedAutoPlacements,g=e.options.placement,_=qt(g),b=l||(_!==g&&p?function(t){if(qt(t)===bt)return[];var e=_e(t);return[ve(t),e,ve(e)]}(g):[_e(g)]),v=[g].concat(b).reduce((function(t,i){return t.concat(qt(i)===bt?Le(e,{placement:i,boundary:h,rootBoundary:d,padding:c,flipVariations:p,allowedAutoPlacements:m}):i)}),[]),y=e.rects.reference,w=e.rects.popper,A=new Map,E=!0,T=v[0],C=0;C=0,S=L?"width":"height",D=ke(e,{placement:O,boundary:h,rootBoundary:d,altBoundary:u,padding:c}),I=L?k?gt:_t:k?mt:pt;y[S]>w[S]&&(I=_e(I));var N=_e(I),P=[];if(o&&P.push(D[x]<=0),a&&P.push(D[I]<=0,D[N]<=0),P.every((function(t){return t}))){T=O,E=!1;break}A.set(O,P)}if(E)for(var M=function(t){var e=v.find((function(e){var i=A.get(e);if(i)return i.slice(0,t).every((function(t){return t}))}));if(e)return T=e,"break"},j=p?3:1;j>0&&"break"!==M(j);j--);e.placement!==T&&(e.modifiersData[n]._skip=!0,e.placement=T,e.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function De(t,e,i){return void 0===i&&(i={x:0,y:0}),{top:t.top-e.height-i.y,right:t.right-e.width+i.x,bottom:t.bottom-e.height+i.y,left:t.left-e.width-i.x}}function Ie(t){return[pt,gt,mt,_t].some((function(e){return t[e]>=0}))}const Ne={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(t){var e=t.state,i=t.name,n=e.rects.reference,s=e.rects.popper,o=e.modifiersData.preventOverflow,r=ke(e,{elementContext:"reference"}),a=ke(e,{altBoundary:!0}),l=De(r,n),c=De(a,s,o),h=Ie(l),d=Ie(c);e.modifiersData[i]={referenceClippingOffsets:l,popperEscapeOffsets:c,isReferenceHidden:h,hasPopperEscaped:d},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":h,"data-popper-escaped":d})}},Pe={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.offset,o=void 0===s?[0,0]:s,r=xt.reduce((function(t,i){return t[i]=function(t,e,i){var n=qt(t),s=[_t,pt].indexOf(n)>=0?-1:1,o="function"==typeof i?i(Object.assign({},e,{placement:t})):i,r=o[0],a=o[1];return r=r||0,a=(a||0)*s,[_t,gt].indexOf(n)>=0?{x:a,y:r}:{x:r,y:a}}(i,e.rects,o),t}),{}),a=r[e.placement],l=a.x,c=a.y;null!=e.modifiersData.popperOffsets&&(e.modifiersData.popperOffsets.x+=l,e.modifiersData.popperOffsets.y+=c),e.modifiersData[n]=r}},Me={name:"popperOffsets",enabled:!0,phase:"read",fn:function(t){var e=t.state,i=t.name;e.modifiersData[i]=xe({reference:e.rects.reference,element:e.rects.popper,strategy:"absolute",placement:e.placement})},data:{}},je={name:"preventOverflow",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0!==r&&r,l=i.boundary,c=i.rootBoundary,h=i.altBoundary,d=i.padding,u=i.tether,f=void 0===u||u,p=i.tetherOffset,m=void 0===p?0:p,g=ke(e,{boundary:l,rootBoundary:c,padding:d,altBoundary:h}),_=qt(e.placement),b=he(e.placement),v=!b,y=oe(_),w="x"===y?"y":"x",A=e.modifiersData.popperOffsets,E=e.rects.reference,T=e.rects.popper,C="function"==typeof m?m(Object.assign({},e.rects,{placement:e.placement})):m,O="number"==typeof C?{mainAxis:C,altAxis:C}:Object.assign({mainAxis:0,altAxis:0},C),x=e.modifiersData.offset?e.modifiersData.offset[e.placement]:null,k={x:0,y:0};if(A){if(o){var L,S="y"===y?pt:_t,D="y"===y?mt:gt,I="y"===y?"height":"width",N=A[y],P=N+g[S],M=N-g[D],j=f?-T[I]/2:0,F=b===yt?E[I]:T[I],H=b===yt?-T[I]:-E[I],$=e.elements.arrow,W=f&&$?Gt($):{width:0,height:0},B=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},z=B[S],R=B[D],q=re(0,E[I],W[I]),V=v?E[I]/2-j-q-z-O.mainAxis:F-q-z-O.mainAxis,K=v?-E[I]/2+j+q+R+O.mainAxis:H+q+R+O.mainAxis,Q=e.elements.arrow&&se(e.elements.arrow),X=Q?"y"===y?Q.clientTop||0:Q.clientLeft||0:0,Y=null!=(L=null==x?void 0:x[y])?L:0,U=N+K-Y,G=re(f?Kt(P,N+V-Y-X):P,N,f?Vt(M,U):M);A[y]=G,k[y]=G-N}if(a){var J,Z="x"===y?pt:_t,tt="x"===y?mt:gt,et=A[w],it="y"===w?"height":"width",nt=et+g[Z],st=et-g[tt],ot=-1!==[pt,_t].indexOf(_),rt=null!=(J=null==x?void 0:x[w])?J:0,at=ot?nt:et-E[it]-T[it]-rt+O.altAxis,lt=ot?et+E[it]+T[it]-rt-O.altAxis:st,ct=f&&ot?function(t,e,i){var n=re(t,e,i);return n>i?i:n}(at,et,lt):re(f?at:nt,et,f?lt:st);A[w]=ct,k[w]=ct-et}e.modifiersData[n]=k}},requiresIfExists:["offset"]};function Fe(t,e,i){void 0===i&&(i=!1);var n,s,o=Bt(e),r=Bt(e)&&function(t){var e=t.getBoundingClientRect(),i=Qt(e.width)/t.offsetWidth||1,n=Qt(e.height)/t.offsetHeight||1;return 1!==i||1!==n}(e),a=ee(e),l=Ut(t,r,i),c={scrollLeft:0,scrollTop:0},h={x:0,y:0};return(o||!o&&!i)&&(("body"!==Ht(e)||Ae(a))&&(c=(n=e)!==$t(n)&&Bt(n)?{scrollLeft:(s=n).scrollLeft,scrollTop:s.scrollTop}:ye(n)),Bt(e)?((h=Ut(e,!0)).x+=e.clientLeft,h.y+=e.clientTop):a&&(h.x=we(a))),{x:l.left+c.scrollLeft-h.x,y:l.top+c.scrollTop-h.y,width:l.width,height:l.height}}function He(t){var e=new Map,i=new Set,n=[];function s(t){i.add(t.name),[].concat(t.requires||[],t.requiresIfExists||[]).forEach((function(t){if(!i.has(t)){var n=e.get(t);n&&s(n)}})),n.push(t)}return t.forEach((function(t){e.set(t.name,t)})),t.forEach((function(t){i.has(t.name)||s(t)})),n}var $e={placement:"bottom",modifiers:[],strategy:"absolute"};function We(){for(var t=arguments.length,e=new Array(t),i=0;iNumber.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return(this._inNavbar||"static"===this._config.display)&&(H.setDataAttribute(this._menu,"popper","static"),t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,...g(this._config.popperConfig,[t])}}_selectMenuItem({key:t,target:e}){const i=z.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter((t=>a(t)));i.length&&b(i,e,t===Xe,!i.includes(e)).focus()}static jQueryInterface(t){return this.each((function(){const e=ci.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}static clearMenus(t){if(2===t.button||"keyup"===t.type&&"Tab"!==t.key)return;const e=z.find(Ze);for(const i of e){const e=ci.getInstance(i);if(!e||!1===e._config.autoClose)continue;const n=t.composedPath(),s=n.includes(e._menu);if(n.includes(e._element)||"inside"===e._config.autoClose&&!s||"outside"===e._config.autoClose&&s)continue;if(e._menu.contains(t.target)&&("keyup"===t.type&&"Tab"===t.key||/input|select|option|textarea|form/i.test(t.target.tagName)))continue;const o={relatedTarget:e._element};"click"===t.type&&(o.clickEvent=t),e._completeHide(o)}}static dataApiKeydownHandler(t){const e=/input|textarea/i.test(t.target.tagName),i="Escape"===t.key,n=[Qe,Xe].includes(t.key);if(!n&&!i)return;if(e&&!i)return;t.preventDefault();const s=this.matches(Je)?this:z.prev(this,Je)[0]||z.next(this,Je)[0]||z.findOne(Je,t.delegateTarget.parentNode),o=ci.getOrCreateInstance(s);if(n)return t.stopPropagation(),o.show(),void o._selectMenuItem(t);o._isShown()&&(t.stopPropagation(),o.hide(),s.focus())}}P.on(document,Ue,Je,ci.dataApiKeydownHandler),P.on(document,Ue,ti,ci.dataApiKeydownHandler),P.on(document,Ye,ci.clearMenus),P.on(document,"keyup.bs.dropdown.data-api",ci.clearMenus),P.on(document,Ye,Je,(function(t){t.preventDefault(),ci.getOrCreateInstance(this).toggle()})),m(ci);const hi="show",di="mousedown.bs.backdrop",ui={className:"modal-backdrop",clickCallback:null,isAnimated:!1,isVisible:!0,rootElement:"body"},fi={className:"string",clickCallback:"(function|null)",isAnimated:"boolean",isVisible:"boolean",rootElement:"(element|string)"};class pi extends ${constructor(t){super(),this._config=this._getConfig(t),this._isAppended=!1,this._element=null}static get Default(){return ui}static get DefaultType(){return fi}static get NAME(){return"backdrop"}show(t){if(!this._config.isVisible)return void g(t);this._append();const e=this._getElement();this._config.isAnimated&&d(e),e.classList.add(hi),this._emulateAnimation((()=>{g(t)}))}hide(t){this._config.isVisible?(this._getElement().classList.remove(hi),this._emulateAnimation((()=>{this.dispose(),g(t)}))):g(t)}dispose(){this._isAppended&&(P.off(this._element,di),this._element.remove(),this._isAppended=!1)}_getElement(){if(!this._element){const t=document.createElement("div");t.className=this._config.className,this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_configAfterMerge(t){return t.rootElement=r(t.rootElement),t}_append(){if(this._isAppended)return;const t=this._getElement();this._config.rootElement.append(t),P.on(t,di,(()=>{g(this._config.clickCallback)})),this._isAppended=!0}_emulateAnimation(t){_(t,this._getElement(),this._config.isAnimated)}}const mi=".bs.focustrap",gi="backward",_i={autofocus:!0,trapElement:null},bi={autofocus:"boolean",trapElement:"element"};class vi extends ${constructor(t){super(),this._config=this._getConfig(t),this._isActive=!1,this._lastTabNavDirection=null}static get Default(){return _i}static get DefaultType(){return bi}static get NAME(){return"focustrap"}activate(){this._isActive||(this._config.autofocus&&this._config.trapElement.focus(),P.off(document,mi),P.on(document,"focusin.bs.focustrap",(t=>this._handleFocusin(t))),P.on(document,"keydown.tab.bs.focustrap",(t=>this._handleKeydown(t))),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,P.off(document,mi))}_handleFocusin(t){const{trapElement:e}=this._config;if(t.target===document||t.target===e||e.contains(t.target))return;const i=z.focusableChildren(e);0===i.length?e.focus():this._lastTabNavDirection===gi?i[i.length-1].focus():i[0].focus()}_handleKeydown(t){"Tab"===t.key&&(this._lastTabNavDirection=t.shiftKey?gi:"forward")}}const yi=".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",wi=".sticky-top",Ai="padding-right",Ei="margin-right";class Ti{constructor(){this._element=document.body}getWidth(){const t=document.documentElement.clientWidth;return Math.abs(window.innerWidth-t)}hide(){const t=this.getWidth();this._disableOverFlow(),this._setElementAttributes(this._element,Ai,(e=>e+t)),this._setElementAttributes(yi,Ai,(e=>e+t)),this._setElementAttributes(wi,Ei,(e=>e-t))}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,Ai),this._resetElementAttributes(yi,Ai),this._resetElementAttributes(wi,Ei)}isOverflowing(){return this.getWidth()>0}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,i){const n=this.getWidth();this._applyManipulationCallback(t,(t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+n)return;this._saveInitialAttribute(t,e);const s=window.getComputedStyle(t).getPropertyValue(e);t.style.setProperty(e,`${i(Number.parseFloat(s))}px`)}))}_saveInitialAttribute(t,e){const i=t.style.getPropertyValue(e);i&&H.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,(t=>{const i=H.getDataAttribute(t,e);null!==i?(H.removeDataAttribute(t,e),t.style.setProperty(e,i)):t.style.removeProperty(e)}))}_applyManipulationCallback(t,e){if(o(t))e(t);else for(const i of z.find(t,this._element))e(i)}}const Ci=".bs.modal",Oi="hidden.bs.modal",xi="show.bs.modal",ki="modal-open",Li="show",Si="modal-static",Di={backdrop:!0,focus:!0,keyboard:!0},Ii={backdrop:"(boolean|string)",focus:"boolean",keyboard:"boolean"};class Ni extends W{constructor(t,e){super(t,e),this._dialog=z.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._isTransitioning=!1,this._scrollBar=new Ti,this._addEventListeners()}static get Default(){return Di}static get DefaultType(){return Ii}static get NAME(){return"modal"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||P.trigger(this._element,xi,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isTransitioning=!0,this._scrollBar.hide(),document.body.classList.add(ki),this._adjustDialog(),this._backdrop.show((()=>this._showElement(t))))}hide(){this._isShown&&!this._isTransitioning&&(P.trigger(this._element,"hide.bs.modal").defaultPrevented||(this._isShown=!1,this._isTransitioning=!0,this._focustrap.deactivate(),this._element.classList.remove(Li),this._queueCallback((()=>this._hideModal()),this._element,this._isAnimated())))}dispose(){P.off(window,Ci),P.off(this._dialog,Ci),this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new pi({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new vi({trapElement:this._element})}_showElement(t){document.body.contains(this._element)||document.body.append(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0;const e=z.findOne(".modal-body",this._dialog);e&&(e.scrollTop=0),d(this._element),this._element.classList.add(Li),this._queueCallback((()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,P.trigger(this._element,"shown.bs.modal",{relatedTarget:t})}),this._dialog,this._isAnimated())}_addEventListeners(){P.on(this._element,"keydown.dismiss.bs.modal",(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():this._triggerBackdropTransition())})),P.on(window,"resize.bs.modal",(()=>{this._isShown&&!this._isTransitioning&&this._adjustDialog()})),P.on(this._element,"mousedown.dismiss.bs.modal",(t=>{P.one(this._element,"click.dismiss.bs.modal",(e=>{this._element===t.target&&this._element===e.target&&("static"!==this._config.backdrop?this._config.backdrop&&this.hide():this._triggerBackdropTransition())}))}))}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide((()=>{document.body.classList.remove(ki),this._resetAdjustments(),this._scrollBar.reset(),P.trigger(this._element,Oi)}))}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(P.trigger(this._element,"hidePrevented.bs.modal").defaultPrevented)return;const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._element.style.overflowY;"hidden"===e||this._element.classList.contains(Si)||(t||(this._element.style.overflowY="hidden"),this._element.classList.add(Si),this._queueCallback((()=>{this._element.classList.remove(Si),this._queueCallback((()=>{this._element.style.overflowY=e}),this._dialog)}),this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),i=e>0;if(i&&!t){const t=p()?"paddingLeft":"paddingRight";this._element.style[t]=`${e}px`}if(!i&&t){const t=p()?"paddingRight":"paddingLeft";this._element.style[t]=`${e}px`}}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each((function(){const i=Ni.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}}))}}P.on(document,"click.bs.modal.data-api",'[data-bs-toggle="modal"]',(function(t){const e=z.getElementFromSelector(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),P.one(e,xi,(t=>{t.defaultPrevented||P.one(e,Oi,(()=>{a(this)&&this.focus()}))}));const i=z.findOne(".modal.show");i&&Ni.getInstance(i).hide(),Ni.getOrCreateInstance(e).toggle(this)})),R(Ni),m(Ni);const Pi="show",Mi="showing",ji="hiding",Fi=".offcanvas.show",Hi="hidePrevented.bs.offcanvas",$i="hidden.bs.offcanvas",Wi={backdrop:!0,keyboard:!0,scroll:!1},Bi={backdrop:"(boolean|string)",keyboard:"boolean",scroll:"boolean"};class zi extends W{constructor(t,e){super(t,e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get Default(){return Wi}static get DefaultType(){return Bi}static get NAME(){return"offcanvas"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||P.trigger(this._element,"show.bs.offcanvas",{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._backdrop.show(),this._config.scroll||(new Ti).hide(),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add(Mi),this._queueCallback((()=>{this._config.scroll&&!this._config.backdrop||this._focustrap.activate(),this._element.classList.add(Pi),this._element.classList.remove(Mi),P.trigger(this._element,"shown.bs.offcanvas",{relatedTarget:t})}),this._element,!0))}hide(){this._isShown&&(P.trigger(this._element,"hide.bs.offcanvas").defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.add(ji),this._backdrop.hide(),this._queueCallback((()=>{this._element.classList.remove(Pi,ji),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._config.scroll||(new Ti).reset(),P.trigger(this._element,$i)}),this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_initializeBackDrop(){const t=Boolean(this._config.backdrop);return new pi({className:"offcanvas-backdrop",isVisible:t,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:t?()=>{"static"!==this._config.backdrop?this.hide():P.trigger(this._element,Hi)}:null})}_initializeFocusTrap(){return new vi({trapElement:this._element})}_addEventListeners(){P.on(this._element,"keydown.dismiss.bs.offcanvas",(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():P.trigger(this._element,Hi))}))}static jQueryInterface(t){return this.each((function(){const e=zi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}P.on(document,"click.bs.offcanvas.data-api",'[data-bs-toggle="offcanvas"]',(function(t){const e=z.getElementFromSelector(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),l(this))return;P.one(e,$i,(()=>{a(this)&&this.focus()}));const i=z.findOne(Fi);i&&i!==e&&zi.getInstance(i).hide(),zi.getOrCreateInstance(e).toggle(this)})),P.on(window,"load.bs.offcanvas.data-api",(()=>{for(const t of z.find(Fi))zi.getOrCreateInstance(t).show()})),P.on(window,"resize.bs.offcanvas",(()=>{for(const t of z.find("[aria-modal][class*=show][class*=offcanvas-]"))"fixed"!==getComputedStyle(t).position&&zi.getOrCreateInstance(t).hide()})),R(zi),m(zi);const Ri={"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],div:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},qi=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),Vi=/^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i,Ki=(t,e)=>{const i=t.nodeName.toLowerCase();return e.includes(i)?!qi.has(i)||Boolean(Vi.test(t.nodeValue)):e.filter((t=>t instanceof RegExp)).some((t=>t.test(i)))},Qi={allowList:Ri,content:{},extraClass:"",html:!1,sanitize:!0,sanitizeFn:null,template:"
"},Xi={allowList:"object",content:"object",extraClass:"(string|function)",html:"boolean",sanitize:"boolean",sanitizeFn:"(null|function)",template:"string"},Yi={entry:"(string|element|function|null)",selector:"(string|element)"};class Ui extends ${constructor(t){super(),this._config=this._getConfig(t)}static get Default(){return Qi}static get DefaultType(){return Xi}static get NAME(){return"TemplateFactory"}getContent(){return Object.values(this._config.content).map((t=>this._resolvePossibleFunction(t))).filter(Boolean)}hasContent(){return this.getContent().length>0}changeContent(t){return this._checkContent(t),this._config.content={...this._config.content,...t},this}toHtml(){const t=document.createElement("div");t.innerHTML=this._maybeSanitize(this._config.template);for(const[e,i]of Object.entries(this._config.content))this._setContent(t,i,e);const e=t.children[0],i=this._resolvePossibleFunction(this._config.extraClass);return i&&e.classList.add(...i.split(" ")),e}_typeCheckConfig(t){super._typeCheckConfig(t),this._checkContent(t.content)}_checkContent(t){for(const[e,i]of Object.entries(t))super._typeCheckConfig({selector:e,entry:i},Yi)}_setContent(t,e,i){const n=z.findOne(i,t);n&&((e=this._resolvePossibleFunction(e))?o(e)?this._putElementInTemplate(r(e),n):this._config.html?n.innerHTML=this._maybeSanitize(e):n.textContent=e:n.remove())}_maybeSanitize(t){return this._config.sanitize?function(t,e,i){if(!t.length)return t;if(i&&"function"==typeof i)return i(t);const n=(new window.DOMParser).parseFromString(t,"text/html"),s=[].concat(...n.body.querySelectorAll("*"));for(const t of s){const i=t.nodeName.toLowerCase();if(!Object.keys(e).includes(i)){t.remove();continue}const n=[].concat(...t.attributes),s=[].concat(e["*"]||[],e[i]||[]);for(const e of n)Ki(e,s)||t.removeAttribute(e.nodeName)}return n.body.innerHTML}(t,this._config.allowList,this._config.sanitizeFn):t}_resolvePossibleFunction(t){return g(t,[this])}_putElementInTemplate(t,e){if(this._config.html)return e.innerHTML="",void e.append(t);e.textContent=t.textContent}}const Gi=new Set(["sanitize","allowList","sanitizeFn"]),Ji="fade",Zi="show",tn=".modal",en="hide.bs.modal",nn="hover",sn="focus",on={AUTO:"auto",TOP:"top",RIGHT:p()?"left":"right",BOTTOM:"bottom",LEFT:p()?"right":"left"},rn={allowList:Ri,animation:!0,boundary:"clippingParents",container:!1,customClass:"",delay:0,fallbackPlacements:["top","right","bottom","left"],html:!1,offset:[0,6],placement:"top",popperConfig:null,sanitize:!0,sanitizeFn:null,selector:!1,template:'',title:"",trigger:"hover focus"},an={allowList:"object",animation:"boolean",boundary:"(string|element)",container:"(string|element|boolean)",customClass:"(string|function)",delay:"(number|object)",fallbackPlacements:"array",html:"boolean",offset:"(array|string|function)",placement:"(string|function)",popperConfig:"(null|object|function)",sanitize:"boolean",sanitizeFn:"(null|function)",selector:"(string|boolean)",template:"string",title:"(string|element|function)",trigger:"string"};class ln extends W{constructor(t,e){if(void 0===Ve)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");super(t,e),this._isEnabled=!0,this._timeout=0,this._isHovered=null,this._activeTrigger={},this._popper=null,this._templateFactory=null,this._newContent=null,this.tip=null,this._setListeners(),this._config.selector||this._fixTitle()}static get Default(){return rn}static get DefaultType(){return an}static get NAME(){return"tooltip"}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(){this._isEnabled&&(this._activeTrigger.click=!this._activeTrigger.click,this._isShown()?this._leave():this._enter())}dispose(){clearTimeout(this._timeout),P.off(this._element.closest(tn),en,this._hideModalHandler),this._element.getAttribute("data-bs-original-title")&&this._element.setAttribute("title",this._element.getAttribute("data-bs-original-title")),this._disposePopper(),super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this._isWithContent()||!this._isEnabled)return;const t=P.trigger(this._element,this.constructor.eventName("show")),e=(c(this._element)||this._element.ownerDocument.documentElement).contains(this._element);if(t.defaultPrevented||!e)return;this._disposePopper();const i=this._getTipElement();this._element.setAttribute("aria-describedby",i.getAttribute("id"));const{container:n}=this._config;if(this._element.ownerDocument.documentElement.contains(this.tip)||(n.append(i),P.trigger(this._element,this.constructor.eventName("inserted"))),this._popper=this._createPopper(i),i.classList.add(Zi),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))P.on(t,"mouseover",h);this._queueCallback((()=>{P.trigger(this._element,this.constructor.eventName("shown")),!1===this._isHovered&&this._leave(),this._isHovered=!1}),this.tip,this._isAnimated())}hide(){if(this._isShown()&&!P.trigger(this._element,this.constructor.eventName("hide")).defaultPrevented){if(this._getTipElement().classList.remove(Zi),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))P.off(t,"mouseover",h);this._activeTrigger.click=!1,this._activeTrigger.focus=!1,this._activeTrigger.hover=!1,this._isHovered=null,this._queueCallback((()=>{this._isWithActiveTrigger()||(this._isHovered||this._disposePopper(),this._element.removeAttribute("aria-describedby"),P.trigger(this._element,this.constructor.eventName("hidden")))}),this.tip,this._isAnimated())}}update(){this._popper&&this._popper.update()}_isWithContent(){return Boolean(this._getTitle())}_getTipElement(){return this.tip||(this.tip=this._createTipElement(this._newContent||this._getContentForTemplate())),this.tip}_createTipElement(t){const e=this._getTemplateFactory(t).toHtml();if(!e)return null;e.classList.remove(Ji,Zi),e.classList.add(`bs-${this.constructor.NAME}-auto`);const i=(t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t})(this.constructor.NAME).toString();return e.setAttribute("id",i),this._isAnimated()&&e.classList.add(Ji),e}setContent(t){this._newContent=t,this._isShown()&&(this._disposePopper(),this.show())}_getTemplateFactory(t){return this._templateFactory?this._templateFactory.changeContent(t):this._templateFactory=new Ui({...this._config,content:t,extraClass:this._resolvePossibleFunction(this._config.customClass)}),this._templateFactory}_getContentForTemplate(){return{".tooltip-inner":this._getTitle()}}_getTitle(){return this._resolvePossibleFunction(this._config.title)||this._element.getAttribute("data-bs-original-title")}_initializeOnDelegatedTarget(t){return this.constructor.getOrCreateInstance(t.delegateTarget,this._getDelegateConfig())}_isAnimated(){return this._config.animation||this.tip&&this.tip.classList.contains(Ji)}_isShown(){return this.tip&&this.tip.classList.contains(Zi)}_createPopper(t){const e=g(this._config.placement,[this,t,this._element]),i=on[e.toUpperCase()];return qe(this._element,t,this._getPopperConfig(i))}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_resolvePossibleFunction(t){return g(t,[this._element])}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"preSetPlacement",enabled:!0,phase:"beforeMain",fn:t=>{this._getTipElement().setAttribute("data-popper-placement",t.state.placement)}}]};return{...e,...g(this._config.popperConfig,[e])}}_setListeners(){const t=this._config.trigger.split(" ");for(const e of t)if("click"===e)P.on(this._element,this.constructor.eventName("click"),this._config.selector,(t=>{this._initializeOnDelegatedTarget(t).toggle()}));else if("manual"!==e){const t=e===nn?this.constructor.eventName("mouseenter"):this.constructor.eventName("focusin"),i=e===nn?this.constructor.eventName("mouseleave"):this.constructor.eventName("focusout");P.on(this._element,t,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusin"===t.type?sn:nn]=!0,e._enter()})),P.on(this._element,i,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusout"===t.type?sn:nn]=e._element.contains(t.relatedTarget),e._leave()}))}this._hideModalHandler=()=>{this._element&&this.hide()},P.on(this._element.closest(tn),en,this._hideModalHandler)}_fixTitle(){const t=this._element.getAttribute("title");t&&(this._element.getAttribute("aria-label")||this._element.textContent.trim()||this._element.setAttribute("aria-label",t),this._element.setAttribute("data-bs-original-title",t),this._element.removeAttribute("title"))}_enter(){this._isShown()||this._isHovered?this._isHovered=!0:(this._isHovered=!0,this._setTimeout((()=>{this._isHovered&&this.show()}),this._config.delay.show))}_leave(){this._isWithActiveTrigger()||(this._isHovered=!1,this._setTimeout((()=>{this._isHovered||this.hide()}),this._config.delay.hide))}_setTimeout(t,e){clearTimeout(this._timeout),this._timeout=setTimeout(t,e)}_isWithActiveTrigger(){return Object.values(this._activeTrigger).includes(!0)}_getConfig(t){const e=H.getDataAttributes(this._element);for(const t of Object.keys(e))Gi.has(t)&&delete e[t];return t={...e,..."object"==typeof t&&t?t:{}},t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t.container=!1===t.container?document.body:r(t.container),"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),t}_getDelegateConfig(){const t={};for(const[e,i]of Object.entries(this._config))this.constructor.Default[e]!==i&&(t[e]=i);return t.selector=!1,t.trigger="manual",t}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null),this.tip&&(this.tip.remove(),this.tip=null)}static jQueryInterface(t){return this.each((function(){const e=ln.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}m(ln);const cn={...ln.Default,content:"",offset:[0,8],placement:"right",template:'',trigger:"click"},hn={...ln.DefaultType,content:"(null|string|element|function)"};class dn extends ln{static get Default(){return cn}static get DefaultType(){return hn}static get NAME(){return"popover"}_isWithContent(){return this._getTitle()||this._getContent()}_getContentForTemplate(){return{".popover-header":this._getTitle(),".popover-body":this._getContent()}}_getContent(){return this._resolvePossibleFunction(this._config.content)}static jQueryInterface(t){return this.each((function(){const e=dn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}m(dn);const un="click.bs.scrollspy",fn="active",pn="[href]",mn={offset:null,rootMargin:"0px 0px -25%",smoothScroll:!1,target:null,threshold:[.1,.5,1]},gn={offset:"(number|null)",rootMargin:"string",smoothScroll:"boolean",target:"element",threshold:"array"};class _n extends W{constructor(t,e){super(t,e),this._targetLinks=new Map,this._observableSections=new Map,this._rootElement="visible"===getComputedStyle(this._element).overflowY?null:this._element,this._activeTarget=null,this._observer=null,this._previousScrollData={visibleEntryTop:0,parentScrollTop:0},this.refresh()}static get Default(){return mn}static get DefaultType(){return gn}static get NAME(){return"scrollspy"}refresh(){this._initializeTargetsAndObservables(),this._maybeEnableSmoothScroll(),this._observer?this._observer.disconnect():this._observer=this._getNewObserver();for(const t of this._observableSections.values())this._observer.observe(t)}dispose(){this._observer.disconnect(),super.dispose()}_configAfterMerge(t){return t.target=r(t.target)||document.body,t.rootMargin=t.offset?`${t.offset}px 0px -30%`:t.rootMargin,"string"==typeof t.threshold&&(t.threshold=t.threshold.split(",").map((t=>Number.parseFloat(t)))),t}_maybeEnableSmoothScroll(){this._config.smoothScroll&&(P.off(this._config.target,un),P.on(this._config.target,un,pn,(t=>{const e=this._observableSections.get(t.target.hash);if(e){t.preventDefault();const i=this._rootElement||window,n=e.offsetTop-this._element.offsetTop;if(i.scrollTo)return void i.scrollTo({top:n,behavior:"smooth"});i.scrollTop=n}})))}_getNewObserver(){const t={root:this._rootElement,threshold:this._config.threshold,rootMargin:this._config.rootMargin};return new IntersectionObserver((t=>this._observerCallback(t)),t)}_observerCallback(t){const e=t=>this._targetLinks.get(`#${t.target.id}`),i=t=>{this._previousScrollData.visibleEntryTop=t.target.offsetTop,this._process(e(t))},n=(this._rootElement||document.documentElement).scrollTop,s=n>=this._previousScrollData.parentScrollTop;this._previousScrollData.parentScrollTop=n;for(const o of t){if(!o.isIntersecting){this._activeTarget=null,this._clearActiveClass(e(o));continue}const t=o.target.offsetTop>=this._previousScrollData.visibleEntryTop;if(s&&t){if(i(o),!n)return}else s||t||i(o)}}_initializeTargetsAndObservables(){this._targetLinks=new Map,this._observableSections=new Map;const t=z.find(pn,this._config.target);for(const e of t){if(!e.hash||l(e))continue;const t=z.findOne(decodeURI(e.hash),this._element);a(t)&&(this._targetLinks.set(decodeURI(e.hash),e),this._observableSections.set(e.hash,t))}}_process(t){this._activeTarget!==t&&(this._clearActiveClass(this._config.target),this._activeTarget=t,t.classList.add(fn),this._activateParents(t),P.trigger(this._element,"activate.bs.scrollspy",{relatedTarget:t}))}_activateParents(t){if(t.classList.contains("dropdown-item"))z.findOne(".dropdown-toggle",t.closest(".dropdown")).classList.add(fn);else for(const e of z.parents(t,".nav, .list-group"))for(const t of z.prev(e,".nav-link, .nav-item > .nav-link, .list-group-item"))t.classList.add(fn)}_clearActiveClass(t){t.classList.remove(fn);const e=z.find("[href].active",t);for(const t of e)t.classList.remove(fn)}static jQueryInterface(t){return this.each((function(){const e=_n.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}P.on(window,"load.bs.scrollspy.data-api",(()=>{for(const t of z.find('[data-bs-spy="scroll"]'))_n.getOrCreateInstance(t)})),m(_n);const bn="ArrowLeft",vn="ArrowRight",yn="ArrowUp",wn="ArrowDown",An="active",En="fade",Tn="show",Cn='[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',On=`.nav-link:not(.dropdown-toggle), .list-group-item:not(.dropdown-toggle), [role="tab"]:not(.dropdown-toggle), ${Cn}`;class xn extends W{constructor(t){super(t),this._parent=this._element.closest('.list-group, .nav, [role="tablist"]'),this._parent&&(this._setInitialAttributes(this._parent,this._getChildren()),P.on(this._element,"keydown.bs.tab",(t=>this._keydown(t))))}static get NAME(){return"tab"}show(){const t=this._element;if(this._elemIsActive(t))return;const e=this._getActiveElem(),i=e?P.trigger(e,"hide.bs.tab",{relatedTarget:t}):null;P.trigger(t,"show.bs.tab",{relatedTarget:e}).defaultPrevented||i&&i.defaultPrevented||(this._deactivate(e,t),this._activate(t,e))}_activate(t,e){t&&(t.classList.add(An),this._activate(z.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.removeAttribute("tabindex"),t.setAttribute("aria-selected",!0),this._toggleDropDown(t,!0),P.trigger(t,"shown.bs.tab",{relatedTarget:e})):t.classList.add(Tn)}),t,t.classList.contains(En)))}_deactivate(t,e){t&&(t.classList.remove(An),t.blur(),this._deactivate(z.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.setAttribute("aria-selected",!1),t.setAttribute("tabindex","-1"),this._toggleDropDown(t,!1),P.trigger(t,"hidden.bs.tab",{relatedTarget:e})):t.classList.remove(Tn)}),t,t.classList.contains(En)))}_keydown(t){if(![bn,vn,yn,wn].includes(t.key))return;t.stopPropagation(),t.preventDefault();const e=[vn,wn].includes(t.key),i=b(this._getChildren().filter((t=>!l(t))),t.target,e,!0);i&&(i.focus({preventScroll:!0}),xn.getOrCreateInstance(i).show())}_getChildren(){return z.find(On,this._parent)}_getActiveElem(){return this._getChildren().find((t=>this._elemIsActive(t)))||null}_setInitialAttributes(t,e){this._setAttributeIfNotExists(t,"role","tablist");for(const t of e)this._setInitialAttributesOnChild(t)}_setInitialAttributesOnChild(t){t=this._getInnerElement(t);const e=this._elemIsActive(t),i=this._getOuterElement(t);t.setAttribute("aria-selected",e),i!==t&&this._setAttributeIfNotExists(i,"role","presentation"),e||t.setAttribute("tabindex","-1"),this._setAttributeIfNotExists(t,"role","tab"),this._setInitialAttributesOnTargetPanel(t)}_setInitialAttributesOnTargetPanel(t){const e=z.getElementFromSelector(t);e&&(this._setAttributeIfNotExists(e,"role","tabpanel"),t.id&&this._setAttributeIfNotExists(e,"aria-labelledby",`${t.id}`))}_toggleDropDown(t,e){const i=this._getOuterElement(t);if(!i.classList.contains("dropdown"))return;const n=(t,n)=>{const s=z.findOne(t,i);s&&s.classList.toggle(n,e)};n(".dropdown-toggle",An),n(".dropdown-menu",Tn),i.setAttribute("aria-expanded",e)}_setAttributeIfNotExists(t,e,i){t.hasAttribute(e)||t.setAttribute(e,i)}_elemIsActive(t){return t.classList.contains(An)}_getInnerElement(t){return t.matches(On)?t:z.findOne(On,t)}_getOuterElement(t){return t.closest(".nav-item, .list-group-item")||t}static jQueryInterface(t){return this.each((function(){const e=xn.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}P.on(document,"click.bs.tab",Cn,(function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),l(this)||xn.getOrCreateInstance(this).show()})),P.on(window,"load.bs.tab",(()=>{for(const t of z.find('.active[data-bs-toggle="tab"], .active[data-bs-toggle="pill"], .active[data-bs-toggle="list"]'))xn.getOrCreateInstance(t)})),m(xn);const kn="hide",Ln="show",Sn="showing",Dn={animation:"boolean",autohide:"boolean",delay:"number"},In={animation:!0,autohide:!0,delay:5e3};class Nn extends W{constructor(t,e){super(t,e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get Default(){return In}static get DefaultType(){return Dn}static get NAME(){return"toast"}show(){P.trigger(this._element,"show.bs.toast").defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove(kn),d(this._element),this._element.classList.add(Ln,Sn),this._queueCallback((()=>{this._element.classList.remove(Sn),P.trigger(this._element,"shown.bs.toast"),this._maybeScheduleHide()}),this._element,this._config.animation))}hide(){this.isShown()&&(P.trigger(this._element,"hide.bs.toast").defaultPrevented||(this._element.classList.add(Sn),this._queueCallback((()=>{this._element.classList.add(kn),this._element.classList.remove(Sn,Ln),P.trigger(this._element,"hidden.bs.toast")}),this._element,this._config.animation)))}dispose(){this._clearTimeout(),this.isShown()&&this._element.classList.remove(Ln),super.dispose()}isShown(){return this._element.classList.contains(Ln)}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout((()=>{this.hide()}),this._config.delay)))}_onInteraction(t,e){switch(t.type){case"mouseover":case"mouseout":this._hasMouseInteraction=e;break;case"focusin":case"focusout":this._hasKeyboardInteraction=e}if(e)return void this._clearTimeout();const i=t.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){P.on(this._element,"mouseover.bs.toast",(t=>this._onInteraction(t,!0))),P.on(this._element,"mouseout.bs.toast",(t=>this._onInteraction(t,!1))),P.on(this._element,"focusin.bs.toast",(t=>this._onInteraction(t,!0))),P.on(this._element,"focusout.bs.toast",(t=>this._onInteraction(t,!1)))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each((function(){const e=Nn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}return R(Nn),m(Nn),{Alert:q,Button:K,Carousel:rt,Collapse:ft,Dropdown:ci,Modal:Ni,Offcanvas:zi,Popover:dn,ScrollSpy:_n,Tab:xn,Toast:Nn,Tooltip:ln}})); 7 | //# sourceMappingURL=bootstrap.bundle.min.js.map -------------------------------------------------------------------------------- /static/pre.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jianchang512/kokoro-uiapi/9a2bc3902edb6454d7c39e6003bcdbd6d3efafa4/static/pre.jpg -------------------------------------------------------------------------------- /static/script.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | let voice_list = { 4 | "en":[ 5 | "af_alloy", 6 | "af_aoede", 7 | "af_bella", 8 | "af_jessica", 9 | "af_kore", 10 | "af_nicole", 11 | "af_nova", 12 | "af_river", 13 | "af_sarah", 14 | "af_sky", 15 | "am_adam", 16 | "am_echo", 17 | "am_eric", 18 | "am_fenrir", 19 | "am_liam", 20 | "am_michael", 21 | "am_onyx", 22 | "am_puck", 23 | "am_santa", 24 | "bf_alice", 25 | "bf_emma", 26 | "bf_isabella", 27 | "bf_lily", 28 | "bm_daniel", 29 | "bm_fable", 30 | "bm_george", 31 | "bm_lewis" 32 | ], 33 | "zh":["zf_xiaobei","zf_xiaoni","zf_xiaoxiao","zf_xiaoyi","zm_yunjian","zm_yunxi","zm_yunxia","zm_yunyang"], 34 | "ja":["jf_alpha","jf_gongitsune","jf_nezumi","jf_tebukuro","jm_kumo"], 35 | "fr":["ff_siwis"], 36 | "it":["if_sara","im_nicola"], 37 | "hi":["hf_alpha","hf_beta","hm_omega","hm_psi"], 38 | "es":["ef_dora","em_alex","em_santa"], 39 | "pt":["pf_dora","pm_alex","pm_santa"] 40 | }; 41 | 42 | const welcome = { 43 | "zh": "你好啊我的朋友", 44 | "en": "Hello my friend", 45 | "ja": "こんにちは、私の友達", 46 | "fr": "Bonjour mon ami", 47 | "es": "Hola mi amigo", 48 | "pt": "Olá meu amigo", 49 | "it": "Ciao amico mio", 50 | "hi": "नमस्ते मेरे दोस्त" 51 | }; 52 | 53 | 54 | $(document).ready(function () { 55 | [].forEach.call(document.querySelectorAll('[role="tooltip"]'), it => { 56 | new bootstrap.Tooltip(it); 57 | }); 58 | 59 | 60 | 61 | const synthesisArea = $('#synthesis-area'); 62 | const voiceSelect = $('#voice-select'); 63 | const speedInput = $('#speed-input'); 64 | const keepSpacingCheckbox = $('#keep-spacing-checkbox'); 65 | const synthesisText = $('#synthesis-text'); 66 | 67 | const startSynthesisButton = $('#start-synthesis-button'); 68 | const startListenButton = $('#start-listen-button'); 69 | 70 | const synthesisAudioContainer = $('#synthesis-audio-container'); 71 | 72 | const voiceLang = $('#lang-select'); 73 | 74 | const voiceImportSrtButton = $('#voice-import-srt-button'); 75 | const voiceImportTextButton = $('#voice-import-text-button'); 76 | 77 | 78 | 79 | 80 | 81 | 82 | voiceLang.on('change', function () { 83 | let html = []; 84 | voice_list[$(this).val()].forEach(n => { 85 | html.push(``); 86 | }); 87 | voiceSelect.html(html); 88 | }); 89 | let html = []; 90 | voice_list["zh"].forEach(n => { 91 | html.push(``); 92 | }); 93 | voiceSelect.html(html); 94 | 95 | 96 | 97 | 98 | 99 | 100 | voiceImportSrtButton.on('change', function () { 101 | const file = $(this)[0].files[0]; 102 | if (file) { 103 | const reader = new FileReader(); 104 | reader.onload = function (e) { 105 | synthesisText.val(e.target.result) 106 | }; 107 | reader.readAsText(file); 108 | } 109 | }); 110 | voiceImportTextButton.on('click', function () { 111 | voiceImportSrtButton.click(); 112 | }); 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | function getCurrentDateTimeString() { 126 | const now = new Date(); 127 | 128 | const year = now.getFullYear(); 129 | const month = String(now.getMonth() + 1).padStart(2, '0'); // 月份从 0 开始,所以 + 1 130 | const day = String(now.getDate()).padStart(2, '0'); 131 | const hours = String(now.getHours()).padStart(2, '0'); 132 | const minutes = String(now.getMinutes()).padStart(2, '0'); 133 | const seconds = String(now.getSeconds()).padStart(2, '0'); 134 | 135 | return `${year}-${month}-${day}-${hours}-${minutes}-${seconds}`; 136 | } 137 | 138 | // 开始语音合成 139 | startSynthesisButton.on('click', function () { 140 | const voice = voiceSelect.val(); 141 | const speed = speedInput.val(); 142 | 143 | const keepSpacing = keepSpacingCheckbox.is(':checked'); 144 | const text = synthesisText.val(); 145 | 146 | const auto_speed = $('#auto_speed').prop('checked'); 147 | 148 | 149 | if (!text) { 150 | alert('请先输入需要合成配音的文字或导入字幕') 151 | return; 152 | } 153 | 154 | 155 | startSynthesisButton.prop('disabled', true).text('合成中...'); 156 | synthesisAudioContainer.empty() // 清空之前的内容 157 | 158 | 159 | 160 | $.ajax({ 161 | url: '/synthesize', 162 | type: 'POST', 163 | timeout:86400000, 164 | 165 | contentType: 'application/json', 166 | 167 | data: JSON.stringify({ 168 | voice: voice, 169 | speed: speed, 170 | keep_spacing: keepSpacing, 171 | text: text, 172 | auto_speed: auto_speed 173 | }), 174 | 175 | success: function (response, status, xhr) { 176 | if (response.status=='ok' && response.data) { 177 | const audioUrl = response.data; 178 | const audio = $('').attr('src', audioUrl); 179 | 180 | const downloadButton = $('').on('click', function () { 181 | const downloadLink = $('').attr({ 182 | href: audioUrl, 183 | download: 'synthesized_audio-' + getCurrentDateTimeString() + '.mp3' 184 | }); 185 | $('body').append(downloadLink); 186 | downloadLink[0].click(); 187 | downloadLink.remove(); 188 | }); 189 | 190 | synthesisAudioContainer.append(audio) 191 | synthesisAudioContainer.append(downloadButton) 192 | startSynthesisButton.prop('disabled', false).text('开始合成配音'); 193 | return; 194 | } 195 | }, 196 | error: function (err) { 197 | alert(err.responseJSON ? err.responseJSON['error'] : "Failed"); 198 | startSynthesisButton.prop('disabled', false).text('开始合成配音'); 199 | } 200 | 201 | }); 202 | }); 203 | 204 | startListenButton.on('click', function () { 205 | const voice = voiceSelect.val(); 206 | 207 | if (!voice) { 208 | return alert('必须选择角色'); 209 | } 210 | 211 | const text = welcome[$('#lang-select').val()]; 212 | if (!text) { 213 | return alert('抱歉该角色暂不可试听') 214 | } 215 | $.ajax({ 216 | url: '/synthesize', 217 | type: 'POST', 218 | timeout:86400000, 219 | 220 | contentType: 'application/json', 221 | 222 | 223 | data: JSON.stringify({ 224 | voice: voice, 225 | text: text 226 | }), 227 | 228 | success: function (response, status, xhr) { 229 | if(response.status=='ok' && response.data){ 230 | const audioUrl = response.data; 231 | const audio = $('') 232 | audio.attr('src', audioUrl); 233 | $("body").append(audio) 234 | audio[0].play(); 235 | } 236 | }, 237 | error: function (err) { 238 | alert(err.responseJSON ? err.responseJSON['error'] : 'Failed'); 239 | } 240 | 241 | }); 242 | 243 | 244 | }); 245 | 246 | 247 | 248 | 249 | }); 250 | 251 | 252 | -------------------------------------------------------------------------------- /static/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Arial', sans-serif; 3 | background-color: #fff; 4 | color: #343a40; 5 | transition: background-color 0.3s ease; 6 | position: relative; 7 | } 8 | .fs-12{ 9 | font-size:12px !important; 10 | } 11 | 12 | .synthesis-area { 13 | padding: 20px; 14 | border: 1px solid #e0e0e0; 15 | border-radius: 8px; 16 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); 17 | display: flex; 18 | flex-direction: column; 19 | width: 100%; 20 | } 21 | 22 | 23 | .synthesis-area h5 { 24 | margin-bottom: 20px; 25 | font-weight: 500; 26 | border-bottom: 1px solid #dee2e6; 27 | padding-bottom: 10px; 28 | } 29 | 30 | .synthesis-area .synthesis-controls { 31 | display: flex; 32 | flex-wrap: wrap; 33 | gap: 15px; 34 | align-items: center; 35 | } 36 | .synthesis-area .synthesis-controls > div { 37 | display: flex; 38 | align-items: center; 39 | } 40 | .synthesis-area .synthesis-controls label { 41 | margin-right: 5px; 42 | font-weight: 500; 43 | white-space: nowrap; 44 | } 45 | .synthesis-area .synthesis-controls input[type="number"] { 46 | width: 80px; 47 | padding: 8px; 48 | border-radius: 4px; 49 | border: 1px solid #e0e0e0; 50 | outline: none; 51 | } 52 | .synthesis-area .synthesis-controls select { 53 | padding: 8px; 54 | border-radius: 4px; 55 | border: 1px solid #e0e0e0; 56 | outline: none; 57 | cursor: pointer; 58 | } 59 | .synthesis-area .synthesis-controls select:focus { 60 | border-color: #007bff; 61 | box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); 62 | } 63 | .synthesis-area textarea { 64 | width: 100%; 65 | height: 180px; 66 | border: 1px solid #e0e0e0; 67 | resize: none; 68 | padding: 10px; 69 | border-radius: 4px; 70 | font-size: 0.9rem; 71 | line-height: 1.6; 72 | color: #555; 73 | margin-bottom: 10px; 74 | } 75 | .synthesis-area .synthesis-audio { 76 | margin-top: 15px; 77 | display: flex; 78 | flex-direction: column; 79 | } 80 | .synthesis-area .synthesis-audio audio { 81 | width: 100%; 82 | margin-bottom: 10px; 83 | } 84 | .synthesis-area .synthesis-audio button { 85 | align-self: flex-start; 86 | } 87 | 88 | 89 | .synthesis-area .clear-text-btn { 90 | margin-left: 10px; 91 | } 92 | .btn:disabled{ 93 | border-width:0; 94 | } 95 | 96 | .cursor-pointer{ 97 | cursor: pointer; 98 | 99 | } 100 | .text-gray{ 101 | color:#999; 102 | } 103 | 104 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | kokoroTTS UI API 9 | 10 | 11 | 12 | 13 | 14 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | --------------------------------------------------------------------------------