├── .github ├── profile │ ├── split-lang-banner.svg │ └── split-lang-logo.svg └── workflows │ └── publish.yml ├── .gitignore ├── LICENSE ├── README.md ├── docs ├── ja │ └── README.md └── zh │ └── README.md ├── event_log.txt ├── setup.py ├── split-lang-benchmark.ipynb ├── split-lang-demo.ipynb ├── split_lang ├── __init__.py ├── config.py ├── detect_lang │ ├── __init__.py │ └── detector.py ├── model.py └── split │ ├── __init__.py │ ├── splitter.py │ └── utils.py └── tests ├── __init__.py ├── data ├── __init__.py ├── correct_split.txt ├── correct_split_merge_punc.txt ├── novel_1.txt └── test_data.py ├── split_acc.py ├── test_config.py ├── test_split.py └── test_split_to_substrings.py /.github/profile/split-lang-banner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /.github/profile/split-lang-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Python Package 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" # 仅在推送标签时触发 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v2 15 | 16 | - name: Set up Python 17 | uses: actions/setup-python@v2 18 | with: 19 | python-version: "3.9" # 选择 Python 版本 20 | 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install setuptools wheel twine 25 | 26 | - name: Build package 27 | run: python setup.py sdist bdist_wheel 28 | 29 | - name: Publish package to PyPI 30 | env: 31 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 32 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 33 | run: twine upload dist/* 34 | 35 | - name: Clean up dist folder 36 | run: rm -rf dist 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | build/ 3 | dist/ 4 | split_lang.egg-info/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 DoodleBear 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /event_log.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DoodleBears/split-lang/428c92de255530c3b83e02999abb31241c282084/event_log.txt -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from os import path 2 | 3 | from setuptools import find_packages, setup 4 | 5 | 6 | def packagefile(*relpath): 7 | return path.join(path.dirname(__file__), *relpath) 8 | 9 | 10 | def read(*relpath): 11 | with open(packagefile(*relpath), encoding="utf-8") as f: 12 | return f.read() 13 | 14 | 15 | setup( 16 | name="split_lang", 17 | version="2.1.0", 18 | description="A package for splitting text by languages through concatenating over split substrings based on their language", 19 | long_description=read("README.md"), 20 | long_description_content_type="text/markdown", 21 | url="https://github.com/DoodleBears/langsplit", 22 | author="DoodleBear", 23 | author_email="yangmufeng233@gmail.com", 24 | license="MIT", 25 | packages=find_packages(), 26 | install_requires=[ 27 | "fast_langdetect", 28 | "pydantic", 29 | "budoux", 30 | ], 31 | classifiers=[ 32 | "Programming Language :: Python :: 3", 33 | "License :: OSI Approved :: MIT License", 34 | "Operating System :: OS Independent", 35 | ], 36 | python_requires=">=3.9", 37 | ) 38 | -------------------------------------------------------------------------------- /split-lang-benchmark.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## Import Language Detection Package" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": 80, 13 | "metadata": {}, 14 | "outputs": [], 15 | "source": [ 16 | "import langdetect\n", 17 | "import fast_langdetect\n", 18 | "from lingua import Language, LanguageDetectorBuilder" 19 | ] 20 | }, 21 | { 22 | "cell_type": "markdown", 23 | "metadata": {}, 24 | "source": [ 25 | "## Import Text Split Package" 26 | ] 27 | }, 28 | { 29 | "cell_type": "code", 30 | "execution_count": 81, 31 | "metadata": {}, 32 | "outputs": [], 33 | "source": [ 34 | "from wtpsplit import SaT, WtP\n", 35 | "sat = SaT(\"sat-1l-sm\")\n", 36 | "sat.half().to(\"cuda\")\n", 37 | "wtp = WtP(\"wtp-bert-mini\")\n", 38 | "import budoux" 39 | ] 40 | }, 41 | { 42 | "cell_type": "code", 43 | "execution_count": 82, 44 | "metadata": {}, 45 | "outputs": [], 46 | "source": [ 47 | "text = \"你喜欢看アニメ吗\"" 48 | ] 49 | }, 50 | { 51 | "cell_type": "code", 52 | "execution_count": 83, 53 | "metadata": {}, 54 | "outputs": [ 55 | { 56 | "name": "stderr", 57 | "output_type": "stream", 58 | "text": [ 59 | "100%|██████████| 1/1 [00:00<00:00, 124.43it/s]\n" 60 | ] 61 | }, 62 | { 63 | "data": { 64 | "text/plain": [ 65 | "['你', '喜欢看', 'アニメ', '吗']" 66 | ] 67 | }, 68 | "execution_count": 83, 69 | "metadata": {}, 70 | "output_type": "execute_result" 71 | } 72 | ], 73 | "source": [ 74 | "wtp.split(text_or_texts=text, threshold=4e-5, verbose=True)" 75 | ] 76 | }, 77 | { 78 | "cell_type": "code", 79 | "execution_count": 84, 80 | "metadata": {}, 81 | "outputs": [], 82 | "source": [ 83 | "texts_with_digit = [\n", 84 | " \"你喜欢看アニメ吗?\",\n", 85 | " \"衬衫的价格是9.15便士\",\n", 86 | " \"衬衫的价格是233亿元\",\n", 87 | " \"衬衫的价格是233亿元人民币\",\n", 88 | "]\n", 89 | "\n", 90 | "texts_zh_jp_ko_en = [\n", 91 | " \"我是 VGroupChatBot,一个旨在支持多人通信的助手,通过可视化消息来帮助团队成员更好地交流。我可以帮助团队成员更好地整理和共享信息,特别是在讨论、会议和Brainstorming等情况下。你好我的名字是西野くまですmy name is bob很高兴认识你どうぞよろしくお願いいたします「こんにちは」是什么意思。\",\n", 92 | " \"你好,我的名字是西野くまです。I am from Tokyo, 日本の首都。今天的天气非常好,sky is clear and sunny。おはようございます、皆さん!我们一起来学习吧。Learning languages can be fun and exciting。昨日はとても忙しかったので、今日は少しリラックスしたいです。Let's take a break and enjoy some coffee。中文、日本語、and English are three distinct languages, each with its own unique charm。希望我们能一起进步,一起成长。Let's keep studying and improving our language skills together. ありがとう!\",\n", 93 | " \"你好,今日はどこへ行きますか?\",\n", 94 | " \"你好今日はどこへ行きますか?\",\n", 95 | " \"我的名字是田中さんです。\",\n", 96 | " \"我喜欢吃寿司和拉面おいしいです。\",\n", 97 | " \"今天の天気はとてもいいですね。\",\n", 98 | " \"我在学习日本語少し難しいです。\",\n", 99 | " \"日语真是おもしろい啊\",\n", 100 | " \"你喜欢看アニメ吗?\",\n", 101 | " \"我想去日本旅行、特に京都に行きたいです。\",\n", 102 | " \"昨天見た映画はとても感動的でした。我朋友是日本人彼はとても優しいです。\",\n", 103 | " \"我们一起去カラオケ吧、楽しそうです。\",\n", 104 | " \"我的家在北京、でも、仕事で東京に住んでいます。\",\n", 105 | " \"我在学做日本料理、日本料理を作るのを習っています。\",\n", 106 | " \"你会说几种语言、何ヶ国語話せますか?\",\n", 107 | " \"我昨天看了一本书、その本はとても面白かったです。\",\n", 108 | " \"你最近好吗、最近どうですか?\",\n", 109 | " \"我在学做日本料理와 한국 요리、日本料理を作るのを習っています。\",\n", 110 | " \"你会说几种语言、何ヶ国語話せますか?몇 개 언어를 할 수 있어요?\",\n", 111 | " \"我昨天看了一本书、その本はとても面白かったです。어제 책을 읽었는데, 정말 재미있었어요。\",\n", 112 | " \"我们一起去逛街와 쇼핑、買い物に行きましょう。쇼핑하러 가요。\",\n", 113 | " \"你最近好吗、最近どうですか?요즘 어떻게 지내요?\",\n", 114 | "]\n", 115 | "\n", 116 | "texts_zh_jp = [\n", 117 | " \"你好今日はどこへ行きますか\",\n", 118 | " \"我的名字是田中さんです\",\n", 119 | " \"我喜欢吃寿司和拉面おいしいです\",\n", 120 | " \"今天の天気はとてもいいですね\",\n", 121 | " \"我在学习日本語少し難しいです\",\n", 122 | " \"日语真是おもしろい啊\",\n", 123 | " \"你喜欢看アニメ吗\",\n", 124 | " \"我想去日本旅行特に京都に行きたいです\",\n", 125 | " \"昨天見た映画はとても感動的でした\",\n", 126 | " \"我朋友是日本人彼はとても優しいです\",\n", 127 | " \"我们一起去カラオケ吧\",\n", 128 | " \"我的家在北京でも仕事で東京に住んでいます\",\n", 129 | " \"我的名字是西野くまです\",\n", 130 | " \"我的名字是西野くまですよろしくお願いいたします\",\n", 131 | " \"好吃美味しい上手い\",\n", 132 | " \"我给你送的手紙\",\n", 133 | " \"真是面白い\",\n", 134 | " \"春の花香り\",\n", 135 | " \"何ヶ国語話せますか\",\n", 136 | " \"今晚どこに行きます\"\n", 137 | "]\n", 138 | "\n", 139 | "texts_de_fr_en = [\n", 140 | " \"Ich liebe Paris, c'est une belle ville, and the food is amazing!\",\n", 141 | " \"Berlin ist wunderbar, je veux y retourner, and explore more.\",\n", 142 | " \"Bonjour, wie geht's dir today?\",\n", 143 | " \"Die Musik hier ist fantastisch, la musique est superbe, and I enjoy it a lot.\",\n", 144 | " \"Guten Morgen, je t'aime, have a great day!\",\n", 145 | " \"Das Wetter ist heute schön, il fait beau aujourd'hui, and it's perfect for a walk.\",\n", 146 | " \"Ich mag dieses Buch, ce livre est intéressant, and it has a great story.\",\n", 147 | " \"Vielen Dank, merci beaucoup, for your help.\",\n", 148 | " \"Wir reisen nach Deutschland, nous voyageons en Allemagne, and we are excited.\",\n", 149 | " \"Ich bin müde, je suis fatigué, and I need some rest.\",\n", 150 | " \"Ich liebe Paris c'est une belle ville and the food is amazing!\",\n", 151 | " \"Berlin ist wunderbar je veux y retourner and explore more.\",\n", 152 | " \"Bonjour wie geht's dir today?\",\n", 153 | " \"Die Musik hier ist fantastisch la musique est superbe and I enjoy it a lot.\",\n", 154 | " \"Guten Morgen je t'aime have a great day!\",\n", 155 | " \"Das Wetter ist heute schön il fait beau aujourd'hui and it's perfect for a walk.\",\n", 156 | " \"Ich mag dieses Buch ce livre est intéressant and it has a great story.\",\n", 157 | " \"Vielen Dank merci beaucoup for your help.\",\n", 158 | " \"Wir reisen nach Deutschland nous voyageons en Allemagne and we are excited.\",\n", 159 | " \"Ich bin müde je suis fatigué and I need some rest.\",\n", 160 | "]\n", 161 | "\n", 162 | "texts = texts_zh_jp_ko_en + texts_de_fr_en + texts_with_digit" 163 | ] 164 | }, 165 | { 166 | "cell_type": "markdown", 167 | "metadata": {}, 168 | "source": [ 169 | "## Simple Rule-Based judge" 170 | ] 171 | }, 172 | { 173 | "cell_type": "code", 174 | "execution_count": 85, 175 | "metadata": {}, 176 | "outputs": [], 177 | "source": [ 178 | "import re\n", 179 | "\n", 180 | "chinese_char_pattern = re.compile(r\"[\\u4e00-\\u9fff]\")\n", 181 | "hangul_pattern = re.compile(r\"[\\uac00-\\ud7af]\")\n", 182 | "hiragana_pattern = re.compile(r\"[\\u3040-\\u309f]\")\n", 183 | "katakana_pattern = re.compile(r\"[\\u30a0-\\u30ff]\")\n", 184 | "\n", 185 | "\n", 186 | "def contains_chinese_char(text: str):\n", 187 | " return bool(chinese_char_pattern.search(text))\n", 188 | "\n", 189 | "\n", 190 | "def _contains_hiragana(text: str):\n", 191 | " return bool(hiragana_pattern.search(text))\n", 192 | "\n", 193 | "\n", 194 | "def _contains_katakana(text: str):\n", 195 | " return bool(katakana_pattern.search(text))\n", 196 | "\n", 197 | "\n", 198 | "def contains_ja(text):\n", 199 | " if (\n", 200 | " _contains_hiragana(text)\n", 201 | " or _contains_katakana(text)\n", 202 | " ):\n", 203 | " return True\n", 204 | " return False" 205 | ] 206 | }, 207 | { 208 | "cell_type": "markdown", 209 | "metadata": {}, 210 | "source": [ 211 | "## Try different split logic" 212 | ] 213 | }, 214 | { 215 | "cell_type": "code", 216 | "execution_count": 86, 217 | "metadata": {}, 218 | "outputs": [ 219 | { 220 | "name": "stdout", 221 | "output_type": "stream", 222 | "text": [ 223 | "jp_budoux_parser----------\n", 224 | "['你好', '今日は', 'どこへ', '行きますか']\n", 225 | "['我的名字是田中さんです']\n", 226 | "['我喜欢吃寿司和拉面', 'おいしいです']\n", 227 | "['今天の', '天気は', 'とても', 'いいですね']\n", 228 | "['我在学习日本語少し', '難しいです']\n", 229 | "['日语真是おもしろい', '啊']\n", 230 | "['你喜欢看アニメ吗']\n", 231 | "['我想去日本旅行特に', '京都に', '行きたいです']\n", 232 | "['昨天見た', '映画は', 'とても', '感動的でした']\n", 233 | "['我朋友是日本人', '彼は', 'とても', '優しいです']\n", 234 | "['我们一起去カラオケ吧']\n", 235 | "['我的家在北京でも', '仕事で', '東京に', '住んでいます']\n", 236 | "['我的名字是西野くまです']\n", 237 | "['我的名字是西野くまですよろしく', 'お願い', 'いたします']\n", 238 | "['好吃美味しい', '上手い']\n", 239 | "['我给你送的手紙']\n", 240 | "['真是面白い']\n", 241 | "['春の', '花香り']\n", 242 | "['何ヶ国語話せますか']\n", 243 | "['今晚どこに', '行きます']\n", 244 | "----------jp_budoux_parser\n", 245 | "[['你好', '今日は', 'どこへ', '行きますか'], ['我的名字是田中さんです'], ['我喜欢吃寿司和拉面', 'おいしいです'], ['今天の', '天気は', 'とても', 'いいですね'], ['我在学习日本語少し', '難しいです'], ['日语真是おもしろい', '啊'], ['你喜欢看アニメ吗'], ['我想去日本旅行特に', '京都に', '行きたいです'], ['昨天見た', '映画は', 'とても', '感動的でした'], ['我朋友是日本人', '彼は', 'とても', '優しいです'], ['我们一起去カラオケ吧'], ['我的家在北京でも', '仕事で', '東京に', '住んでいます'], ['我的名字是西野くまです'], ['我的名字是西野くまですよろしく', 'お願い', 'いたします'], ['好吃美味しい', '上手い'], ['我给你送的手紙'], ['真是面白い'], ['春の', '花香り'], ['何ヶ国語話せますか'], ['今晚どこに', '行きます']]\n", 246 | "----------jp_budoux_parser+zh_budoux_parser\n", 247 | "['你', '好', '今日', 'は', 'どこへ', '行きますか']\n", 248 | "['我', '的', '名字', '是', '田', '中', 'さんです']\n", 249 | "['我', '喜欢', '吃', '寿司', '和', '拉面', 'おいしいです']\n", 250 | "['今天', 'の', '天', '気は', 'とても', 'いいですね']\n", 251 | "['我', '在', '学习', '日本', '語少', 'し', '難しいです']\n", 252 | "['日', '语真', '是', 'おも', 'しろい', '啊']\n", 253 | "['你', '喜欢', '看', 'アニメ', '吗']\n", 254 | "['我', '想', '去', '日本', '旅行', '特に', '京', '都', 'に', '行きたいです']\n", 255 | "['昨天', '見た', '映', '画', 'は', 'とても', '感動', '的', 'でし', 'た']\n", 256 | "['我', '朋友', '是', '日本', '人', '彼は', 'とても', '優しいです']\n", 257 | "['我们', '一起', '去', 'カラオケ', '吧']\n", 258 | "['我', '的', '家', '在', '北京', 'でも', '仕事で', '東京', 'に', '住ん', 'でいます']\n", 259 | "['我', '的', '名字', '是', '西野', 'くまです']\n", 260 | "['我', '的', '名字', '是', '西野', 'くまですよろしく', 'お願い', 'いたします']\n", 261 | "['好', '吃', '美味', 'しい', '上', '手い']\n", 262 | "['我', '给', '你', '送', '的', '手紙']\n", 263 | "['真', '是', '面白い']\n", 264 | "['春の', '花香り']\n", 265 | "['何', 'ヶ国', '語話', 'せますか']\n", 266 | "['今晚どこに', '行きます']\n", 267 | "[['你', '好', '今日', 'は', 'どこへ', '行きますか'], ['我', '的', '名字', '是', '田', '中', 'さんです'], ['我', '喜欢', '吃', '寿司', '和', '拉面', 'おいしいです'], ['今天', 'の', '天', '気は', 'とても', 'いいですね'], ['我', '在', '学习', '日本', '語少', 'し', '難しいです'], ['日', '语真', '是', 'おも', 'しろい', '啊'], ['你', '喜欢', '看', 'アニメ', '吗'], ['我', '想', '去', '日本', '旅行', '特に', '京', '都', 'に', '行きたいです'], ['昨天', '見た', '映', '画', 'は', 'とても', '感動', '的', 'でし', 'た'], ['我', '朋友', '是', '日本', '人', '彼は', 'とても', '優しいです'], ['我们', '一起', '去', 'カラオケ', '吧'], ['我', '的', '家', '在', '北京', 'でも', '仕事で', '東京', 'に', '住ん', 'でいます'], ['我', '的', '名字', '是', '西野', 'くまです'], ['我', '的', '名字', '是', '西野', 'くまですよろしく', 'お願い', 'いたします'], ['好', '吃', '美味', 'しい', '上', '手い'], ['我', '给', '你', '送', '的', '手紙'], ['真', '是', '面白い'], ['春の', '花香り'], ['何', 'ヶ国', '語話', 'せますか'], ['今晚どこに', '行きます']]\n", 268 | "----------jp_budoux_parser+zh_budoux_parser+combine single to left\n", 269 | "['你好', '今日', 'はどこへ行きますか']\n", 270 | "['我的', '名字是田中', 'さんです']\n", 271 | "['我喜欢吃', '寿司和', '拉面', 'おいしいです']\n", 272 | "['今天', 'の', '天', '気はとてもいいですね']\n", 273 | "['我在', '学习', '日本', '語少', 'し難しいです']\n", 274 | "['日语真是', 'おもしろい', '啊']\n", 275 | "['你喜欢看', 'アニメ', '吗']\n", 276 | "['我想去', '日本', '旅行', '特に', '京都', 'に行きたいです']\n", 277 | "['昨天', '見た', '映画', 'はとても', '感動的', 'でした']\n", 278 | "['我朋友是', '日本人', '彼はとても優しいです']\n", 279 | "['我们', '一起去', 'カラオケ', '吧']\n", 280 | "['我的家在', '北京', 'でも仕事で', '東京', 'に住んでいます']\n", 281 | "['我的', '名字是', '西野', 'くまです']\n", 282 | "['我的', '名字是', '西野', 'くまですよろしくお願いいたします']\n", 283 | "['好吃', '美味', 'しい', '上', '手い']\n", 284 | "['我给你送的', '手紙']\n", 285 | "['真是', '面白い']\n", 286 | "['春の花香り']\n", 287 | "['何ヶ国', '語話', 'せますか']\n", 288 | "['今晚どこに行きます']\n", 289 | "[['你好', '今日', 'はどこへ行きますか'], ['我的', '名字是田中', 'さんです'], ['我喜欢吃', '寿司和', '拉面', 'おいしいです'], ['今天', 'の', '天', '気はとてもいいですね'], ['我在', '学习', '日本', '語少', 'し難しいです'], ['日语真是', 'おもしろい', '啊'], ['你喜欢看', 'アニメ', '吗'], ['我想去', '日本', '旅行', '特に', '京都', 'に行きたいです'], ['昨天', '見た', '映画', 'はとても', '感動的', 'でした'], ['我朋友是', '日本人', '彼はとても優しいです'], ['我们', '一起去', 'カラオケ', '吧'], ['我的家在', '北京', 'でも仕事で', '東京', 'に住んでいます'], ['我的', '名字是', '西野', 'くまです'], ['我的', '名字是', '西野', 'くまですよろしくお願いいたします'], ['好吃', '美味', 'しい', '上', '手い'], ['我给你送的', '手紙'], ['真是', '面白い'], ['春の花香り'], ['何ヶ国', '語話', 'せますか'], ['今晚どこに行きます']]\n" 290 | ] 291 | } 292 | ], 293 | "source": [ 294 | "from typing import List\n", 295 | "\n", 296 | "\n", 297 | "zh_budoux_parser = budoux.load_default_simplified_chinese_parser()\n", 298 | "zh_tc_budoux_parser = budoux.load_default_traditional_chinese_parser()\n", 299 | "jp_budoux_parser = budoux.load_default_japanese_parser()\n", 300 | "\n", 301 | "# print(\"zh_budoux_parser----------\")\n", 302 | "# for text in texts_zh_jp:\n", 303 | "# print(zh_budoux_parser.parse(text))\n", 304 | " \n", 305 | "# print(\"zh_tc_budoux_parser----------\")\n", 306 | "# for text in texts_zh_jp:\n", 307 | "# print(zh_tc_budoux_parser.parse(text))\n", 308 | " \n", 309 | "print(\"jp_budoux_parser----------\")\n", 310 | "for text in texts_zh_jp:\n", 311 | " print(jp_budoux_parser.parse(text))\n", 312 | "\n", 313 | "\n", 314 | "print(\"----------jp_budoux_parser\")\n", 315 | "\n", 316 | "splitted_texts_jp = []\n", 317 | "for text in texts_zh_jp:\n", 318 | " jp_split_text = jp_budoux_parser.parse(text)\n", 319 | " splitted_texts_jp.append(jp_split_text)\n", 320 | "print(splitted_texts_jp)\n", 321 | "print(\"----------jp_budoux_parser+zh_budoux_parser\")\n", 322 | "\n", 323 | "splitted_texts_zh_jp = []\n", 324 | "for substrings in splitted_texts_jp:\n", 325 | " words = []\n", 326 | " for substring in substrings:\n", 327 | " words.extend(zh_budoux_parser.parse(substring))\n", 328 | " print(words)\n", 329 | " splitted_texts_zh_jp.append(words)\n", 330 | "print(splitted_texts_zh_jp)\n", 331 | "\n", 332 | "print(\"----------jp_budoux_parser+zh_budoux_parser+combine single to left\")\n", 333 | "pre_split_texts:List[List[str]] = []\n", 334 | "for words in splitted_texts_zh_jp:\n", 335 | " new_words = [words[0]]\n", 336 | " for sub_text in words[1:]:\n", 337 | " is_left_ja = contains_ja(new_words[-1])\n", 338 | " is_cur_ja = contains_ja(sub_text)\n", 339 | " is_both_same_lang = is_left_ja == is_cur_ja\n", 340 | " is_both_ja = is_left_ja == True and is_both_same_lang\n", 341 | " is_both_zh = is_left_ja == False and is_both_same_lang\n", 342 | " if is_both_ja: # both substring is katakana or hiragana, then concat\n", 343 | " new_words[-1] += sub_text\n", 344 | " elif is_both_zh and len(sub_text) == 1:\n", 345 | " # NOTE: both substring is full Chinese character, and current one is only one character\n", 346 | " # NOTE: 90% is because we first use ja_parser then zh_parser (from `budoux`)\n", 347 | " # NOTE: Since kanji in Japanese usually not appear by them self, Single character is CAUSED BY zh_parser \n", 348 | " # NOTE: So we let single character concat together, if both substring did not contain kana\n", 349 | " new_words[-1] += sub_text\n", 350 | " else:\n", 351 | " new_words.append(sub_text)\n", 352 | " \n", 353 | " if len(new_words) >= 2 and len(new_words[0]) == 1:\n", 354 | " new_words[1] = new_words[0] + new_words[1]\n", 355 | " new_words = new_words[1:]\n", 356 | " pre_split_texts.append(new_words) \n", 357 | " print(new_words) \n", 358 | "print(pre_split_texts)" 359 | ] 360 | }, 361 | { 362 | "cell_type": "code", 363 | "execution_count": 87, 364 | "metadata": {}, 365 | "outputs": [], 366 | "source": [ 367 | "from lingua import Language, LanguageDetectorBuilder\n", 368 | "all_detector = (\n", 369 | " LanguageDetectorBuilder.from_all_languages()\n", 370 | " .with_preloaded_language_models()\n", 371 | " .build()\n", 372 | ")" 373 | ] 374 | }, 375 | { 376 | "cell_type": "code", 377 | "execution_count": 88, 378 | "metadata": {}, 379 | "outputs": [ 380 | { 381 | "name": "stdout", 382 | "output_type": "stream", 383 | "text": [ 384 | "zh:你好|ja:今日|ja:はどこへ行きますか|\n", 385 | "zh:我的|zh:名字是田中|ja:さんです|\n", 386 | "zh:我喜欢吃|ja:寿司和|zh:拉面|ja:おいしいです|\n", 387 | "zh:今天|ja:の|zh:天|ja:気はとてもいいですね|\n", 388 | "zh:我在|zh:学习|ja:日本|zh:語少|ja:し難しいです|\n", 389 | "zh:日语真是|ja:おもしろい|zh:啊|\n", 390 | "zh:你喜欢看|ja:アニメ|zh:吗|\n", 391 | "zh:我想去|ja:日本|ja:旅行|ja:特に|ja:京都|ja:に行きたいです|\n", 392 | "zh:昨天|ja:見た|ja:映画|ja:はとても|zh:感動的|ja:でした|\n", 393 | "zh:我朋友是|zh:日本人|ja:彼はとても優しいです|\n", 394 | "zh:我们|zh:一起去|ja:カラオケ|zh:吧|\n", 395 | "zh:我的家在|ja:北京|ja:でも仕事で|ja:東京|ja:に住んでいます|\n", 396 | "zh:我的|zh:名字是|ja:西野|ja:くまです|\n", 397 | "zh:我的|zh:名字是|ja:西野|ja:くまですよろしくお願いいたします|\n", 398 | "zh:好吃|zh:美味|ja:しい|ja:上|ja:手い|\n", 399 | "zh:我给你送的|ja:手紙|\n", 400 | "zh:真是|ja:面白い|\n", 401 | "ja:春の花香り|\n", 402 | "zh:何ヶ国|ja:語話|ja:せますか|\n", 403 | "ja:今晚どこに行きます|\n" 404 | ] 405 | } 406 | ], 407 | "source": [ 408 | "from langdetect.lang_detect_exception import LangDetectException\n", 409 | "\n", 410 | "def lingua_lang_detect_all(text: str) -> str:\n", 411 | " language: Language | None = all_detector.detect_language_of(text=text)\n", 412 | " if language is None:\n", 413 | " return \"x\"\n", 414 | " return language.iso_code_639_1.name.lower()\n", 415 | "\n", 416 | "def fast_lang_detect(text: str) -> str:\n", 417 | " result = str(fast_langdetect.detect(text, low_memory=False)[\"lang\"])\n", 418 | " result = result.lower()\n", 419 | " return result\n", 420 | "\n", 421 | "def lang_detect(text: str) -> str:\n", 422 | " try:\n", 423 | " result = str(langdetect.detect(text))\n", 424 | " result = result.lower()\n", 425 | " return result\n", 426 | " except LangDetectException as e:\n", 427 | " return \"zh\"\n", 428 | " except Exception as e:\n", 429 | " pass\n", 430 | " return \"x\"\n", 431 | "\n", 432 | "\n", 433 | "for substrings in pre_split_texts:\n", 434 | " for substring in substrings:\n", 435 | " # lang = lingua_lang_detect_all(substring)\n", 436 | " lang = fast_lang_detect(substring)\n", 437 | " \n", 438 | " print(f\"{lang}:{substring}\",end='|')\n", 439 | " print()" 440 | ] 441 | }, 442 | { 443 | "cell_type": "markdown", 444 | "metadata": {}, 445 | "source": [ 446 | "## Difference between split by ` ` (space) and by `wtpsplit`" 447 | ] 448 | }, 449 | { 450 | "cell_type": "code", 451 | "execution_count": 116, 452 | "metadata": {}, 453 | "outputs": [ 454 | { 455 | "name": "stderr", 456 | "output_type": "stream", 457 | "text": [ 458 | "c:\\Users\\admin\\.conda\\envs\\melotts\\lib\\site-packages\\wtpsplit\\__init__.py:45: DeprecationWarning: You are using WtP, the old sentence segmentation model. It is highly encouraged to use SaT instead due to strongly improved performance and efficiency. See https://github.com/segment-any-text/wtpsplit for more info. To ignore this warning, set ignore_legacy_warning=True.\n", 459 | " warnings.warn(\n" 460 | ] 461 | }, 462 | { 463 | "name": "stdout", 464 | "output_type": "stream", 465 | "text": [ 466 | "['Ich ', 'bin müde ', 'je suis fatigué ', 'and ', 'I ', 'need some rest']\n", 467 | "\n", 468 | "0.8507270812988281\n", 469 | "['Ich', 'bin', 'müde', 'je', 'suis', 'fatigué', 'and', 'I', 'need', 'some', 'rest']\n", 470 | "Ich: ['de:1.0']\n", 471 | "\n", 472 | "bin: ['en:0.394521027803421', 'id:0.21292246878147125', 'tr:0.15927168726921082', 'ms:0.09677533060312271', 'eo:0.030221346765756607', 'jv:0.023466553539037704', 'sq:0.013604077510535717', 'sv:0.012493844144046307']\n", 473 | "\n", 474 | "müde: ['de:0.9626638293266296', 'tr:0.026752416044473648']\n", 475 | "\n", 476 | "je: ['sr:0.8350609540939331', 'fr:0.15938909351825714']\n", 477 | "\n", 478 | "suis: ['fr:0.9970543384552002']\n", 479 | "\n", 480 | "fatigué: ['fr:0.7745229601860046', 'es:0.14570455253124237', 'nl:0.03426196426153183', 'de:0.014598727226257324']\n", 481 | "\n", 482 | "and: ['en:0.9983668923377991']\n", 483 | "\n", 484 | "I: ['en:0.9979130625724792']\n", 485 | "\n", 486 | "need: ['en:0.9885499477386475']\n", 487 | "\n", 488 | "some: ['en:0.993599534034729']\n", 489 | "\n", 490 | "rest: ['en:0.46527764201164246', 'nl:0.05554379150271416', 'ja:0.039999693632125854', 'pl:0.033725958317518234', 'ar:0.028888104483485222', 'sv:0.028634123504161835', 'no:0.024481678381562233', 'id:0.022417806088924408', 'tr:0.02039233408868313', 'zh:0.014322302304208279']\n", 491 | "\n", 492 | "\n", 493 | "0.0\n" 494 | ] 495 | }, 496 | { 497 | "name": "stderr", 498 | "output_type": "stream", 499 | "text": [ 500 | "c:\\Users\\admin\\.conda\\envs\\melotts\\lib\\site-packages\\sklearn\\base.py:376: InconsistentVersionWarning: Trying to unpickle estimator LogisticRegression from version 1.2.2 when using version 1.5.0. This might lead to breaking code or invalid results. Use at your own risk. For more info please refer to:\n", 501 | "https://scikit-learn.org/stable/model_persistence.html#security-maintainability-limitations\n", 502 | " warnings.warn(\n" 503 | ] 504 | } 505 | ], 506 | "source": [ 507 | "from wtpsplit import WtP\n", 508 | "from time import time\n", 509 | "from datetime import datetime\n", 510 | "# text = \"Vielen Dank merci beaucoup for your help.\"\n", 511 | "text = \"Ich bin müde je suis fatigué and I need some rest\"\n", 512 | "# text = \"日语使用者应超过一亿三千万人\"\n", 513 | "# text = \"我是 VGroupChatBot,一个旨在支持多人通信的助手,通过可视化消息来帮助团队成员更好地交流。我可以帮助团队成员更好地整理和共享信息,特别是在讨论、会议和Brainstorming等情况下。你好我的名字是西野くまですmy name is bob很高兴认识你どうぞよろしくお願いいたします「こんにちは」是什么意思。我的名字是西野くまです。I am from Tokyo, 日本の首都。今天的天气非常好\"\n", 514 | "time1 = datetime.now().timestamp()\n", 515 | "wtp = WtP('wtp-bert-mini')\n", 516 | "substrings = wtp.split(text_or_texts=text, threshold=2e-3)\n", 517 | "print(substrings)\n", 518 | "for substring in substrings:\n", 519 | " # lang = lingua_lang_detect_all(substring)\n", 520 | " lang = lingua_lang_detect_all(substring)\n", 521 | " # print(f\"{lang}:{substring}\",end='|')\n", 522 | "print()\n", 523 | "time2 = datetime.now().timestamp()\n", 524 | "\n", 525 | "print(time2 - time1)\n", 526 | "\n", 527 | "from split_lang import LangSplitter\n", 528 | "lang_splitter = LangSplitter()\n", 529 | "substrings = lang_splitter._parse_without_zh_ja(text=text)\n", 530 | "substrings = text.split(' ')\n", 531 | "# substrings = lang_splitter._parse_zh_ja(text=text)\n", 532 | "# substrings = lang_splitter._parse_without_zh_ja(text=text)\n", 533 | "print(substrings)\n", 534 | "for substring in substrings:\n", 535 | " # lang = lingua_lang_detect_all(substring)\n", 536 | " lang = fast_langdetect.detect_multilingual(substring, low_memory=False, k=10, threshold=0.01)\n", 537 | " lang_list = [f\"{item['lang']}:{item['score']}\" for item in lang]\n", 538 | " print(f\"{substring}: {lang_list}\",end='\\n\\n')\n", 539 | "time3 = datetime.now().timestamp()\n", 540 | "print()\n", 541 | "print(time3 - time2)" 542 | ] 543 | }, 544 | { 545 | "cell_type": "code", 546 | "execution_count": 117, 547 | "metadata": {}, 548 | "outputs": [ 549 | { 550 | "name": "stdout", 551 | "output_type": "stream", 552 | "text": [ 553 | "1.66e-05\n", 554 | "0.00126\n", 555 | "0.013174603174603174\n" 556 | ] 557 | } 558 | ], 559 | "source": [ 560 | "from wordfreq import word_frequency\n", 561 | "ja_freq = word_frequency('bin ', 'en')\n", 562 | "zh_freq = word_frequency('bin ', 'de')\n", 563 | "print(ja_freq)\n", 564 | "print(zh_freq)\n", 565 | "print(ja_freq / zh_freq)" 566 | ] 567 | } 568 | ], 569 | "metadata": { 570 | "kernelspec": { 571 | "display_name": "melotts", 572 | "language": "python", 573 | "name": "python3" 574 | }, 575 | "language_info": { 576 | "codemirror_mode": { 577 | "name": "ipython", 578 | "version": 3 579 | }, 580 | "file_extension": ".py", 581 | "mimetype": "text/x-python", 582 | "name": "python", 583 | "nbconvert_exporter": "python", 584 | "pygments_lexer": "ipython3", 585 | "version": "3.10.14" 586 | } 587 | }, 588 | "nbformat": 4, 589 | "nbformat_minor": 2 590 | } 591 | -------------------------------------------------------------------------------- /split_lang/__init__.py: -------------------------------------------------------------------------------- 1 | from .split.splitter import LangSplitter 2 | from .model import SubString, SubStringSection, LangSectionType 3 | from .config import DEFAULT_LANG, DEFAULT_LANG_MAP 4 | -------------------------------------------------------------------------------- /split_lang/config.py: -------------------------------------------------------------------------------- 1 | DEFAULT_LANG_MAP = { 2 | "zh": "zh", 3 | "yue": "zh", # 粤语 4 | "wuu": "zh", # 吴语 5 | "zh-cn": "zh", 6 | "zh-tw": "x", 7 | "ko": "ko", 8 | "ja": "ja", 9 | "de": "de", 10 | "fr": "fr", 11 | "en": "en", 12 | "hr": "en", 13 | } 14 | ZH_JA_LANG_MAP = { 15 | "zh": "zh", 16 | "yue": "zh", # 粤语 17 | "wuu": "zh", # 吴语 18 | "zh-cn": "zh", 19 | "zh-tw": "x", 20 | "ja": "ja", 21 | } 22 | NO_ZH_JA_LANG_MAP = { 23 | "de": "de", 24 | "fr": "fr", 25 | "en": "en", 26 | "hr": "en", 27 | } 28 | 29 | DEFAULT_LANG = "x" 30 | -------------------------------------------------------------------------------- /split_lang/detect_lang/__init__.py: -------------------------------------------------------------------------------- 1 | from .detector import detect_lang_combined, possible_detection_list 2 | -------------------------------------------------------------------------------- /split_lang/detect_lang/detector.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import List 3 | 4 | import fast_langdetect 5 | 6 | from ..model import LangSectionType 7 | from ..split.utils import contains_ja_kana 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | def fast_lang_detect(text: str) -> str: 13 | result = str(fast_langdetect.detect(text, low_memory=False)["lang"]) 14 | result = result.lower() 15 | return result 16 | 17 | 18 | # For example '衬衫' cannot be detected by `langdetect`, and `fast_langdetect` will detect it as 'en' 19 | def detect_lang_combined(text: str, lang_section_type: LangSectionType) -> str: 20 | if lang_section_type is LangSectionType.ZH_JA: 21 | if contains_ja_kana(text): 22 | return "ja" 23 | return fast_lang_detect(text) 24 | return fast_lang_detect(text) 25 | 26 | 27 | def possible_detection_list(text: str) -> List[str]: 28 | text = text.replace("\n", "").strip() 29 | languages = [ 30 | item["lang"] 31 | for item in fast_langdetect.detect_multilingual( 32 | text, 33 | low_memory=False, 34 | k=5, 35 | threshold=0.01, 36 | ) 37 | ] 38 | return languages 39 | -------------------------------------------------------------------------------- /split_lang/model.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import List 3 | 4 | from pydantic import BaseModel 5 | 6 | 7 | class LangSectionType(Enum): 8 | ZH_JA = "zh_ja" 9 | KO = "ko" 10 | PUNCTUATION = "punctuation" 11 | NEWLINE = "newline" 12 | DIGIT = "digit" 13 | OTHERS = "others" 14 | ALL = "all" 15 | 16 | 17 | class SubString(BaseModel): 18 | lang: str 19 | """language of `text`""" 20 | text: str 21 | """text of substring""" 22 | index: int 23 | """index of `text` of original string""" 24 | length: int 25 | """length of `text`""" 26 | 27 | 28 | class SubStringSection(BaseModel): 29 | lang_section_type: LangSectionType 30 | """ 31 | Used for deal with different type of languages 32 | 1. Chinese and Japanese both have characters will be processed together 33 | """ 34 | text: str 35 | """original text of this section (combines all of the substrings)""" 36 | substrings: List[SubString] 37 | """substrings that splitted from `text`""" 38 | -------------------------------------------------------------------------------- /split_lang/split/__init__.py: -------------------------------------------------------------------------------- 1 | from .splitter import LangSplitter 2 | from .utils import PUNCTUATION, contains_hangul, contains_ja_kana, contains_zh_ja 3 | -------------------------------------------------------------------------------- /split_lang/split/splitter.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Dict, List 3 | 4 | import budoux 5 | 6 | from ..config import DEFAULT_LANG, DEFAULT_LANG_MAP, NO_ZH_JA_LANG_MAP, ZH_JA_LANG_MAP 7 | from ..detect_lang.detector import detect_lang_combined, possible_detection_list 8 | from ..model import LangSectionType, SubString, SubStringSection 9 | from .utils import PUNCTUATION, contains_hangul, contains_ja_kana, contains_zh_ja 10 | 11 | zh_budoux_parser = budoux.load_default_simplified_chinese_parser() 12 | jp_budoux_parser = budoux.load_default_japanese_parser() 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | class LangSplitter: 18 | def __init__( 19 | self, 20 | lang_map: Dict[str, str] = DEFAULT_LANG_MAP, 21 | default_lang: str = DEFAULT_LANG, 22 | punctuation: str = PUNCTUATION, 23 | not_merge_punctuation: str = "", 24 | merge_across_punctuation: bool = True, 25 | merge_across_digit: bool = True, 26 | merge_across_newline: bool = True, 27 | debug: bool = True, 28 | log_level: int = logging.INFO, 29 | ) -> None: 30 | """ 31 | 1. pre-split stage will use `punctuation` and `digit` to split text then leave substring that contains language character 32 | 2. rule-based and machine learning model will be used for extract words from substring 33 | 3. if language detection of words are same, concat the words into a new substring 34 | 35 | Args: 36 | lang_map (Dict, optional): `{"zh-tw": "zh"}` will map `zh-tw` to `zh`. Defaults to `DEFAULT_LANG_MAP`. 37 | default_lang (str, optional): when `lang_map` did not have the language key detected, fallback to default_lang. Defaults to `DEFAULT_LANG`. 38 | punctuation (str, optional): character that should be treat as punctuation. Defaults to `PUNCTUATION`. 39 | not_merge_punctuation (str, optional): usually been set to `.。??!!` if you do not want to concat all substrings together. Defaults to "". 40 | merge_across_punctuation (bool, optional): merge substring across punctuation. Defaults to True. 41 | merge_across_digit (bool, optional): merge substring across number (e.g. `233`, `9.15`) will be concat to near substring. Defaults to True. 42 | debug (bool, optional): print process for debug. Defaults to False. 43 | """ 44 | self.lang_map = lang_map 45 | self.default_lang = default_lang 46 | self.debug = debug 47 | self.punctuation = punctuation 48 | self.not_merge_punctuation = not_merge_punctuation 49 | self.merge_across_punctuation = merge_across_punctuation 50 | self.merge_across_digit = merge_across_digit 51 | self.merge_across_newline = merge_across_newline 52 | self.log_level = log_level 53 | logging.basicConfig( 54 | level=self.log_level, 55 | format="%(asctime)s %(levelname)s [%(name)s]: %(message)s", 56 | ) 57 | 58 | # MARK: split_by_lang 59 | def split_by_lang( 60 | self, 61 | text: str, 62 | ) -> List[SubString]: 63 | """ 64 | Args: 65 | text (str): text to split 66 | 67 | Returns: 68 | List[SubString]: which contains language, text, index, length, is_punctuation and is_digit data of substring text 69 | """ 70 | # FIXME: 调整 merge 顺序,最后合并 punctuation 71 | pre_split_section = self.pre_split(text=text) 72 | 73 | sections = self._split(pre_split_section=pre_split_section) 74 | 75 | if self.merge_across_digit: # 合并跨数字的 SubString 76 | sections = self._merge_substrings_across_digit_based_on_sections( 77 | sections=sections 78 | ) 79 | # sections = self._smart_merge_all(pre_split_section=pre_split_section) 80 | 81 | if self.merge_across_punctuation: # 合并跨标点符号的 SubString 82 | sections = self._merge_substrings_across_punctuation_based_on_sections( 83 | sections=sections 84 | ) 85 | # sections = self._smart_merge_all(pre_split_section=pre_split_section) 86 | 87 | if self.merge_across_newline: 88 | sections = self._merge_substrings_across_newline_based_on_sections( 89 | sections=sections 90 | ) 91 | # sections = self._smart_merge_all(pre_split_section=pre_split_section) 92 | 93 | for section in sections: 94 | if section.lang_section_type == LangSectionType.ZH_JA: 95 | section.substrings = self._special_merge_for_zh_ja(section.substrings) 96 | 97 | substrings: List[SubString] = [] 98 | for section in sections: 99 | substrings.extend(section.substrings) 100 | 101 | return substrings 102 | 103 | # MARK: pre_split 104 | def pre_split(self, text: str) -> List[SubStringSection]: 105 | # NOTE: since some language share some characters (e.g. Chinese and Japanese) 106 | # NOTE: while Korean has their own characters, 107 | # NOTE: For Cyrillic alphabet, Latin alphabet, a lot of languages share the alphabet 108 | text = text.strip() 109 | if self.debug: 110 | logger.debug("---------before pre_split:") 111 | logger.debug(text) 112 | sections: List[SubStringSection] = [] 113 | current_lang: LangSectionType = LangSectionType.OTHERS 114 | current_text = [] 115 | 116 | def add_substring(lang_section_type: LangSectionType): 117 | if current_text: 118 | concat_text = "".join(current_text) 119 | 120 | sections.append( 121 | SubStringSection( 122 | lang_section_type=lang_section_type, 123 | text=concat_text, 124 | substrings=[], 125 | ) 126 | ) 127 | current_text.clear() 128 | 129 | for index, char in enumerate(text): 130 | if contains_zh_ja(char): 131 | if current_lang != LangSectionType.ZH_JA: 132 | add_substring(current_lang) 133 | current_lang = LangSectionType.ZH_JA 134 | elif contains_hangul(char): 135 | if current_lang != LangSectionType.KO: 136 | add_substring(current_lang) 137 | current_lang = LangSectionType.KO 138 | elif char.isdigit(): 139 | if current_lang != LangSectionType.DIGIT: 140 | add_substring(current_lang) 141 | current_lang = LangSectionType.DIGIT 142 | elif char in self.punctuation: 143 | if char == "'" and index > 0 and text[index - 1].isspace() is False: 144 | # For English, "'" is a part of word 145 | pass 146 | # elif current_lang != LangSectionType.PUNCTUATION: 147 | else: 148 | # [, ] \n .都会单独作为一个 SubString 149 | add_substring(current_lang) 150 | current_lang = LangSectionType.PUNCTUATION 151 | elif char.isspace(): 152 | # concat space to current text 153 | if char in ["\n", "\r"]: 154 | add_substring(current_lang) 155 | current_lang = LangSectionType.NEWLINE 156 | else: 157 | pass 158 | else: 159 | if current_lang != LangSectionType.OTHERS: 160 | add_substring(current_lang) 161 | current_lang = LangSectionType.OTHERS 162 | 163 | current_text.append(char) 164 | 165 | add_substring(current_lang) 166 | if self.debug: 167 | logger.debug("---------after pre_split:") 168 | for section in sections: 169 | logger.debug(section) 170 | return sections 171 | 172 | # MARK: _split 173 | def _split( 174 | self, 175 | pre_split_section: List[SubStringSection], 176 | ) -> List[SubStringSection]: 177 | # TODO: 针对不同语言 set 进行不同处理 178 | section_index = 0 179 | for section in pre_split_section: 180 | section_len = len(section.text) 181 | if section.lang_section_type is LangSectionType.PUNCTUATION: 182 | # NOTE: 标点符号作为单独的 SubString 183 | section.substrings.append( 184 | SubString( 185 | text=section.text, 186 | lang="punctuation", 187 | index=section_index, 188 | length=section_len, 189 | ) 190 | ) 191 | elif section.lang_section_type is LangSectionType.DIGIT: 192 | # NOTE: 数字作为单独的 SubString 193 | section.substrings.append( 194 | SubString( 195 | text=section.text, 196 | lang="digit", 197 | index=section_index, 198 | length=section_len, 199 | ) 200 | ) 201 | elif section.lang_section_type is LangSectionType.NEWLINE: 202 | # NOTE: 换行作为单独的 SubString 203 | section.substrings.append( 204 | SubString( 205 | text=section.text, 206 | lang="newline", 207 | index=section_index, 208 | length=section_len, 209 | ) 210 | ) 211 | else: 212 | substrings_with_lang: List[SubString] = [] 213 | if section.lang_section_type is LangSectionType.ZH_JA: 214 | temp_substrings = self._parse_zh_ja(section.text) 215 | substrings_with_lang = self._init_substr_lang( 216 | texts=temp_substrings, 217 | lang_map=ZH_JA_LANG_MAP, 218 | lang_section_type=LangSectionType.ZH_JA, 219 | ) 220 | 221 | elif section.lang_section_type is LangSectionType.KO: 222 | substrings_with_lang = [ 223 | SubString( 224 | text=section.text, 225 | lang="ko", 226 | index=section_index, 227 | length=section_len, 228 | ) 229 | ] 230 | 231 | else: 232 | temp_substrings = self._parse_without_zh_ja(section.text) 233 | substrings_with_lang = self._init_substr_lang( 234 | texts=temp_substrings, 235 | lang_section_type=LangSectionType.OTHERS, 236 | ) 237 | # 更新 index 238 | for substr in substrings_with_lang: 239 | substr.index += section_index 240 | if self.debug: 241 | logger.debug("---------after _init_substr_lang:") 242 | for substr in substrings_with_lang: 243 | logger.debug(substr) 244 | 245 | section.substrings = substrings_with_lang 246 | 247 | section_index += section_len 248 | 249 | pre_split_section = self._smart_merge_all(pre_split_section) 250 | if self.debug: 251 | logger.debug("---------after split:") 252 | for section in pre_split_section: 253 | logger.debug(section) 254 | 255 | return pre_split_section 256 | 257 | # MARK: smart merge substring together 258 | 259 | def _smart_merge_all(self, pre_split_section: List[SubStringSection]): 260 | for section in pre_split_section: 261 | if ( 262 | section.lang_section_type is LangSectionType.PUNCTUATION 263 | or section.lang_section_type is LangSectionType.NEWLINE 264 | ): 265 | # print(section.text) 266 | continue 267 | smart_concat_result = self._smart_merge( 268 | substr_list=section.substrings, 269 | lang_section_type=section.lang_section_type, 270 | ) 271 | section.substrings.clear() 272 | section.substrings = smart_concat_result 273 | 274 | if self.debug: 275 | logger.debug("---------after smart_merge_all:") 276 | for section in pre_split_section: 277 | logger.debug(section) 278 | 279 | return pre_split_section 280 | 281 | # MARK: _parse_without_zh_ja 282 | def _parse_without_zh_ja(self, text: str): 283 | words: List[str] = [] 284 | exist_space = False 285 | chars = [] 286 | for char in text: 287 | if char.isspace() is False: 288 | if exist_space: 289 | words.append("".join(chars)) 290 | chars.clear() 291 | exist_space = False 292 | chars.append(char) 293 | else: 294 | exist_space = True 295 | chars.append(char) 296 | if len(chars) > 0: 297 | words.append("".join(chars)) 298 | 299 | return words 300 | 301 | # MARK: _parse_zh_ja 302 | def _parse_zh_ja(self, text) -> List[str]: 303 | splitted_texts_jp = [] 304 | splitted_texts_jp = jp_budoux_parser.parse(text) 305 | if self.debug: 306 | logger.debug("---------after jp_budoux_parser:") 307 | logger.debug(splitted_texts_jp) 308 | 309 | splitted_texts_zh_jp = [] 310 | for substring in splitted_texts_jp: 311 | splitted_texts_zh_jp.extend(zh_budoux_parser.parse(substring)) 312 | if self.debug: 313 | logger.debug("---------after zh_budoux_parser:") 314 | logger.debug(splitted_texts_zh_jp) 315 | 316 | new_substrings = [splitted_texts_zh_jp[0]] 317 | for substring in splitted_texts_zh_jp[1:]: 318 | is_left_ja_kana = contains_ja_kana(new_substrings[-1]) 319 | is_cur_ja_kana = contains_ja_kana(substring) 320 | is_both_same_lang = is_left_ja_kana == is_cur_ja_kana 321 | is_both_ja_kana = is_left_ja_kana is True and is_both_same_lang 322 | is_both_zh = is_left_ja_kana is False and is_both_same_lang 323 | if is_both_ja_kana: # both substring is katakana or hiragana, then concat 324 | new_substrings[-1] += substring 325 | elif is_both_zh and len(substring) == 1: 326 | # NOTE: both substring is full Chinese character, and current one is only one character 327 | # NOTE: 90% is because we first use ja_parser then zh_parser (from `budoux`) 328 | # NOTE: Since kanji in Japanese usually not appear by them self, Single character is CAUSED BY zh_parser 329 | # NOTE: So we let single character concat together, if both substring did not contain kana 330 | new_substrings[-1] += substring 331 | else: 332 | new_substrings.append(substring) 333 | 334 | if len(new_substrings) >= 2 and len(new_substrings[0]) == 1: 335 | new_substrings[1] = new_substrings[0] + new_substrings[1] 336 | new_substrings = new_substrings[1:] 337 | 338 | if self.debug: 339 | logger.debug("---------after parse_zh_ja:") 340 | logger.debug(new_substrings) 341 | 342 | return new_substrings 343 | 344 | # MARK: _smart_merge 345 | def _smart_merge( 346 | self, 347 | substr_list: List[SubString], 348 | lang_section_type: LangSectionType, 349 | ) -> List[SubString]: 350 | lang_text_list = substr_list 351 | lang_text_list = self._merge_substrings(lang_text_list) 352 | lang_text_list = self._merge_middle_substr_to_two_side(lang_text_list) 353 | lang_text_list = self._merge_substrings(lang_text_list) 354 | if self.debug: 355 | logger.debug("---------after _merge_middle_substr_to_two_side:") 356 | for substr in lang_text_list: 357 | logger.debug(substr) 358 | # NOTE: get_languages 需要根据不同语言进行不同处理 359 | lang_text_list = self._get_languages( 360 | lang_text_list=lang_text_list, 361 | lang_section_type=lang_section_type, 362 | lang_map=ZH_JA_LANG_MAP 363 | if lang_section_type is LangSectionType.ZH_JA 364 | else NO_ZH_JA_LANG_MAP, 365 | ) 366 | lang_text_list = self._merge_middle_substr_to_two_side(lang_text_list) 367 | lang_text_list = self._merge_substrings(lang_text_list) 368 | if self.debug: 369 | logger.debug("---------after _merge_middle_substr_to_two_side:") 370 | for substr in lang_text_list: 371 | logger.debug(substr) 372 | lang_text_list = self._fill_unknown_language(lang_text_list) 373 | lang_text_list = self._merge_side_substr_to_near(lang_text_list) 374 | lang_text_list = self._merge_substrings(lang_text_list) 375 | if self.debug: 376 | logger.debug("---------after _merge_side_substr_to_near:") 377 | for substr in lang_text_list: 378 | logger.debug(substr) 379 | lang_text_list = self._get_languages( 380 | lang_text_list=lang_text_list, 381 | lang_section_type=lang_section_type, 382 | lang_map=ZH_JA_LANG_MAP 383 | if lang_section_type is LangSectionType.ZH_JA 384 | else NO_ZH_JA_LANG_MAP, 385 | ) 386 | lang_text_list = self._merge_middle_substr_to_two_side(lang_text_list) 387 | lang_text_list = self._merge_substrings(lang_text_list) 388 | if self.debug: 389 | logger.debug("---------after _merge_middle_substr_to_two_side:") 390 | for substr in lang_text_list: 391 | logger.debug(substr) 392 | 393 | return lang_text_list 394 | 395 | def _is_merge_middle_to_two_side( 396 | self, left: SubString, middle: SubString, right: SubString 397 | ): 398 | is_middle_contains_no_kana = not contains_ja_kana(middle.text) 399 | 400 | middle_lang_is_possible_the_same_as_left = ( 401 | left.lang in possible_detection_list(middle.text) 402 | and is_middle_contains_no_kana 403 | ) 404 | middle_lang_is_x = middle.lang == "x" 405 | 406 | is_middle_short_and_two_side_long = ( 407 | middle.length <= 4 408 | and left.length + right.length >= 6 409 | and is_middle_contains_no_kana 410 | ) 411 | 412 | return ( 413 | middle_lang_is_possible_the_same_as_left 414 | or middle_lang_is_x 415 | or is_middle_short_and_two_side_long 416 | ) 417 | 418 | def _is_cur_short_and_near_long(self, cur: SubString, near: SubString): 419 | is_cur_contains_no_kana = not contains_ja_kana(cur.text) 420 | is_near_contains_no_kana = not contains_ja_kana(near.text) 421 | is_both_contains_no_kana = is_cur_contains_no_kana and is_near_contains_no_kana 422 | # NOTE: e.g. 日本人, 今晚, 国外移民 423 | is_near_str_is_long_zh = ( 424 | near.length > cur.length and near.length >= 6 and near.lang == "zh" 425 | ) 426 | is_cur_short_and_near_is_zh_and_long = ( 427 | is_near_str_is_long_zh and is_both_contains_no_kana 428 | ) 429 | cur_lang_is_possible_the_same_as_near = ( 430 | near.lang in possible_detection_list(cur.text) and is_both_contains_no_kana 431 | ) 432 | return ( 433 | is_cur_short_and_near_is_zh_and_long 434 | or cur_lang_is_possible_the_same_as_near 435 | ) 436 | 437 | # MARK: _merge_middle_substr_to_two_side 438 | def _merge_middle_substr_to_two_side(self, substrings: List[SubString]): 439 | substr_len = len(substrings) 440 | if substr_len <= 2: 441 | return substrings 442 | for index in range(substr_len - 2): 443 | left_block = substrings[index] 444 | middle_block = substrings[index + 1] 445 | right_block = substrings[index + 2] 446 | 447 | if ( 448 | left_block.lang == right_block.lang 449 | and left_block.lang != "x" 450 | and left_block.lang != "newline" 451 | ): 452 | # if different detectors results contains near block's language, then combine 453 | 454 | if self._is_merge_middle_to_two_side( 455 | left_block, middle_block, right_block 456 | ): 457 | substrings[index + 1].lang = left_block.lang 458 | 459 | return substrings 460 | 461 | # MARK: _merge_side_substr_to_near 462 | def _merge_side_substr_to_near(self, substrings: List[SubString]): 463 | # NOTE: Merge leftest substr 464 | is_lang_x = substrings[0].lang == "x" 465 | is_cur_short_and_near_long = False 466 | is_possible_same_lang_with_near = False 467 | if len(substrings) >= 2: 468 | # NOTE: if current substr is short and near substr is long, then merge 469 | is_cur_short_and_near_long = self._is_cur_short_and_near_long( 470 | substrings[0], substrings[1] 471 | ) 472 | 473 | # NOTE: if language of near substr is possible the same as current substr, then merge 474 | is_possible_same_lang_with_near = ( 475 | substrings[1].lang in possible_detection_list(substrings[0].text) 476 | and substrings[1].length <= 5 477 | ) 478 | 479 | is_need_merge_to_right = ( 480 | is_lang_x or is_cur_short_and_near_long or is_possible_same_lang_with_near 481 | ) 482 | 483 | if is_need_merge_to_right: 484 | substrings[0].lang = self._get_nearest_lang_with_direction( 485 | substrings, 0, search_left=False 486 | ) 487 | # NOTE: Merge rightest substr 488 | is_lang_x = substrings[-1].lang == "x" 489 | is_cur_short_and_near_long = False 490 | is_possible_same_lang_with_near = False 491 | if len(substrings) >= 2: 492 | is_cur_short_and_near_long = self._is_cur_short_and_near_long( 493 | substrings[-1], substrings[-2] 494 | ) 495 | is_possible_same_lang_with_near = ( 496 | substrings[-2].lang in possible_detection_list(substrings[-1].text) 497 | and substrings[-2].length <= 5 498 | ) 499 | 500 | is_need_merge_to_left = ( 501 | is_lang_x or is_cur_short_and_near_long or is_possible_same_lang_with_near 502 | ) 503 | 504 | if is_need_merge_to_left: 505 | substrings[-1].lang = self._get_nearest_lang_with_direction( 506 | substrings, len(substrings) - 1, search_left=True 507 | ) 508 | return substrings 509 | 510 | # MARK: _fill_unknown_language 511 | def _fill_unknown_language( 512 | self, 513 | substrings: List[SubString], 514 | ): 515 | for index, substr in enumerate(substrings): 516 | if substr.lang == "x": 517 | if index == 0: 518 | # For head substring, find right substring 519 | substrings[index].lang = self._get_nearest_lang_with_direction( 520 | substrings, index, search_left=False 521 | ) 522 | elif index == len(substrings) - 1: 523 | # For tail substring, find left substring 524 | substrings[index].lang = self._get_nearest_lang_with_direction( 525 | substrings, index, search_left=True 526 | ) 527 | else: 528 | # For body (middle) substring, find based on rule 529 | substrings[index].lang = self._get_nearest_lang_with_direction( 530 | substrings, index, self._get_merge_direction(substrings, index) 531 | ) 532 | return substrings 533 | 534 | # MARK: _find_nearest_lang_with_direction 535 | def _get_nearest_lang_with_direction( 536 | self, substrings: List[SubString], index: int, search_left: bool 537 | ) -> str: 538 | if search_left: 539 | for i in range(1, len(substrings)): 540 | left_i_index = index - i 541 | if ( 542 | left_i_index >= 0 543 | and substrings[left_i_index].lang != "x" 544 | and substrings[left_i_index].lang != "digit" 545 | ): 546 | return substrings[left_i_index].lang 547 | else: 548 | for i in range(1, len(substrings)): 549 | right_i_index = index + i 550 | if ( 551 | right_i_index < len(substrings) 552 | and substrings[right_i_index].lang != "x" 553 | and substrings[right_i_index].lang != "digit" 554 | ): 555 | return substrings[right_i_index].lang 556 | return substrings[index].lang 557 | 558 | # MARK: _get_merge_direction 559 | def _get_merge_direction(self, substrings: List[SubString], index: int) -> bool: 560 | is_left = False 561 | if index == 0: 562 | is_left = False 563 | return is_left 564 | elif index == len(substrings) - 1: 565 | is_left = True 566 | return is_left 567 | left_block = substrings[index - 1] 568 | right_block = substrings[index + 1] 569 | if len(left_block.text) >= len(right_block.text): 570 | is_left = True 571 | else: 572 | is_left = False 573 | return is_left 574 | 575 | # MARK: _merge_substrings 576 | def _merge_substrings( 577 | self, 578 | substrings: List[SubString], 579 | ): 580 | smart_concat_result: List[SubString] = [] 581 | lang = "" 582 | for block in substrings: 583 | cur_lang = block.lang 584 | if cur_lang != lang: 585 | smart_concat_result.append(block) 586 | else: 587 | smart_concat_result[-1].text += block.text 588 | smart_concat_result[-1].length += block.length 589 | lang = cur_lang 590 | return smart_concat_result 591 | 592 | # MARK: _special_merge_for_zh_ja 593 | def _special_merge_for_zh_ja( 594 | self, 595 | substrings: List[SubString], 596 | ) -> List[SubString]: 597 | new_substrings: List[SubString] = [] 598 | 599 | if len(substrings) == 1: 600 | return substrings 601 | # NOTE: 统计每个语言的字符串长度 602 | substring_text_len_by_lang = { 603 | "zh": 0, 604 | "ja": 0, 605 | "x": 0, 606 | "digit": 0, 607 | "punctuation": 0, 608 | "newline": 0, 609 | } 610 | index = 0 611 | while index < len(substrings): 612 | current_block = substrings[index] 613 | substring_text_len_by_lang[current_block.lang] += current_block.length 614 | if index == 0: 615 | if ( 616 | substrings[index + 1].lang in ["zh", "ja"] 617 | and substrings[index].lang in ["zh", "ja", "x"] 618 | and substrings[index].length * 10 < substrings[index + 1].length 619 | ): 620 | right_block = substrings[index + 1] 621 | new_substrings.append( 622 | SubString( 623 | is_digit=False, 624 | is_punctuation=False, 625 | lang=right_block.lang, 626 | text=current_block.text + right_block.text, 627 | length=current_block.length + right_block.length, 628 | index=current_block.index, 629 | ) 630 | ) 631 | index += 1 632 | else: 633 | new_substrings.append(substrings[index]) 634 | elif index == len(substrings) - 1: 635 | left_block = new_substrings[-1] 636 | if ( 637 | left_block.lang in ["zh", "ja"] 638 | and current_block.lang in ["zh", "ja", "x"] 639 | and current_block.length * 10 < left_block.length 640 | ): 641 | new_substrings[-1].text += current_block.text 642 | new_substrings[-1].length += current_block.length 643 | 644 | index += 1 645 | else: 646 | new_substrings.append(substrings[index]) 647 | else: 648 | if ( 649 | new_substrings[-1].lang == substrings[index + 1].lang 650 | and new_substrings[-1].lang in ["zh", "ja"] 651 | # and substrings[index].lang in ["zh", "ja", "x"] 652 | and substrings[index].lang != "en" 653 | and substrings[index].length * 10 654 | < new_substrings[-1].length + substrings[index + 1].length 655 | ): 656 | left_block = new_substrings[-1] 657 | right_block = substrings[index + 1] 658 | current_block = substrings[index] 659 | new_substrings[-1].text += current_block.text + right_block.text 660 | new_substrings[-1].length += ( 661 | current_block.length + right_block.length 662 | ) 663 | index += 1 664 | else: 665 | new_substrings.append(substrings[index]) 666 | index += 1 667 | 668 | # NOTE: 如果 substring_count 中 存在 x,则将 x 设置为最多的 lang 669 | if substring_text_len_by_lang["x"] > 0: 670 | max_lang = max( 671 | substring_text_len_by_lang, key=substring_text_len_by_lang.get 672 | ) 673 | for index, substr in enumerate(new_substrings): 674 | if substr.lang == "x": 675 | new_substrings[index].lang = max_lang 676 | # NOTE: 如果 ja 数量是 zh 数量的 10 倍以上,则该 zh 设置为 ja 677 | if substring_text_len_by_lang["ja"] >= substring_text_len_by_lang["zh"] * 10: 678 | for index, substr in enumerate(new_substrings): 679 | if substr.lang == "zh": 680 | new_substrings[index].lang = "ja" 681 | new_substrings = self._merge_substrings(substrings=new_substrings) 682 | return new_substrings 683 | 684 | # MARK: _merge_substrings_across_newline 685 | def _merge_substrings_across_newline( 686 | self, 687 | substrings: List[SubString], 688 | ) -> List[SubString]: 689 | new_substrings: List[SubString] = [] 690 | last_lang = "" 691 | 692 | for _, substring in enumerate(substrings): 693 | if new_substrings: 694 | if substring.lang == "newline": 695 | # If the last substring is also a newline, merge them 696 | new_substrings[-1].text += substring.text 697 | new_substrings[-1].length += substring.length 698 | else: 699 | if substring.lang == last_lang or last_lang == "": 700 | new_substrings[-1].text += substring.text 701 | new_substrings[-1].length += substring.length 702 | new_substrings[-1].lang = ( 703 | substring.lang 704 | if new_substrings[-1].lang == "newline" 705 | else new_substrings[-1].lang 706 | ) 707 | else: 708 | new_substrings.append(substring) 709 | else: 710 | new_substrings.append(substring) 711 | last_lang = substring.lang 712 | 713 | return new_substrings 714 | 715 | # MARK: _merge_substrings_across_newline_based_on_sections 716 | def _merge_substrings_across_newline_based_on_sections( 717 | self, 718 | sections: List[SubStringSection], 719 | ) -> List[SubStringSection]: 720 | new_sections: List[SubStringSection] = [sections[0]] 721 | # NOTE: 将 sections 中的 newline 合并到临近的非 punctuation 的 section 722 | for index, _ in enumerate(sections): 723 | if index == 0: 724 | continue 725 | if index >= len(sections): 726 | break 727 | 728 | current_section = sections[index] 729 | if new_sections[-1].lang_section_type == LangSectionType.NEWLINE: 730 | # NOTE: 如果前一个 section 是 newline,则合并 731 | new_sections[-1].lang_section_type = current_section.lang_section_type 732 | new_sections[-1].text += current_section.text 733 | new_sections[-1].substrings.extend(current_section.substrings) 734 | for index, substr in enumerate(new_sections[-1].substrings): 735 | if index == 0: 736 | continue 737 | else: 738 | substr.index = ( 739 | new_sections[-1].substrings[index - 1].index 740 | + new_sections[-1].substrings[index - 1].length 741 | ) 742 | 743 | elif current_section.lang_section_type == LangSectionType.NEWLINE: 744 | # NOTE: 如果前一个 section 不是 punctuation,则合并 745 | new_sections[-1].text += current_section.text 746 | new_sections[-1].substrings.extend(current_section.substrings) 747 | new_sections[-1].substrings[-1].index = ( 748 | new_sections[-1].substrings[-2].index 749 | + new_sections[-1].substrings[-2].length 750 | ) 751 | else: 752 | new_sections.append(current_section) 753 | # NOTE: 将相同类型的 section 合并 754 | new_sections_merged: List[SubStringSection] = [new_sections[0]] 755 | for index, _ in enumerate(new_sections): 756 | if index == 0: 757 | continue 758 | if ( 759 | new_sections_merged[-1].lang_section_type 760 | == new_sections[index].lang_section_type 761 | ): 762 | new_sections_merged[-1].text += new_sections[index].text 763 | new_sections_merged[-1].substrings.extend( 764 | new_sections[index].substrings 765 | ) 766 | else: 767 | new_sections_merged.append(new_sections[index]) 768 | # NOTE: 重新计算 index 769 | for section_index, section in enumerate(new_sections_merged): 770 | if section_index == 0: 771 | for substr_index, substr in enumerate(section.substrings): 772 | if substr_index == 0: 773 | continue 774 | else: 775 | substr.index = ( 776 | section.substrings[substr_index - 1].index 777 | + section.substrings[substr_index - 1].length 778 | ) 779 | else: 780 | for substr_index, substr in enumerate(section.substrings): 781 | if substr_index == 0: 782 | substr.index = ( 783 | new_sections_merged[section_index - 1].substrings[-1].index 784 | + new_sections_merged[section_index - 1] 785 | .substrings[-1] 786 | .length 787 | ) 788 | else: 789 | substr.index = ( 790 | section.substrings[substr_index - 1].index 791 | + section.substrings[substr_index - 1].length 792 | ) 793 | # NOTE: 合并 sections 中的 substrings 里面的 text 794 | for section in new_sections_merged: 795 | section.substrings = self._merge_substrings_across_newline( 796 | substrings=section.substrings 797 | ) 798 | if self.debug: 799 | logger.debug( 800 | "---------------------------------after_merge_newline_sections:" 801 | ) 802 | for section in new_sections_merged: 803 | logger.debug(section) 804 | return new_sections_merged 805 | 806 | # MARK: _merge_substring_across_digit 807 | def _merge_substrings_across_digit( 808 | self, 809 | substrings: List[SubString], 810 | ) -> List[SubString]: 811 | new_substrings: List[SubString] = [] 812 | last_lang = "" 813 | 814 | for _, substring in enumerate(substrings): 815 | if new_substrings: 816 | if substring.lang == "digit": 817 | new_substrings[-1].text += substring.text 818 | new_substrings[-1].length += substring.length 819 | else: 820 | if substring.lang == last_lang or last_lang == "": 821 | new_substrings[-1].text += substring.text 822 | new_substrings[-1].length += substring.length 823 | if substring.lang != "punctuation": 824 | new_substrings[-1].lang = substring.lang 825 | else: 826 | new_substrings.append(substring) 827 | else: 828 | new_substrings.append(substring) 829 | last_lang = substring.lang 830 | 831 | return new_substrings 832 | 833 | # MARK: _merge_substrings_across_digit_based_on_sections 834 | def _merge_substrings_across_digit_based_on_sections( 835 | self, 836 | sections: List[SubStringSection], 837 | ) -> List[SubStringSection]: 838 | new_sections: List[SubStringSection] = [sections[0]] 839 | # NOTE: 将 sections 中的 digit 合并到临近的非 digit 的 section 840 | for index, _ in enumerate(sections): 841 | if index == 0: 842 | continue 843 | if index >= len(sections): 844 | break 845 | 846 | current_section = sections[index] 847 | # print(f"当前 section:{current_section.lang_section_type}") 848 | # 如果前一个 section 和当前的 section 类型不同,则合并 849 | one_of_section_is_digit = ( 850 | new_sections[-1].lang_section_type == LangSectionType.DIGIT 851 | or current_section.lang_section_type == LangSectionType.DIGIT 852 | ) 853 | if one_of_section_is_digit: 854 | # print(f"测试:{new_sections[-1].text} | {current_section.text}") 855 | if new_sections[-1].lang_section_type == LangSectionType.DIGIT: 856 | if current_section.lang_section_type != LangSectionType.PUNCTUATION: 857 | new_sections[ 858 | -1 859 | ].lang_section_type = current_section.lang_section_type 860 | new_sections[-1].text += current_section.text 861 | new_sections[-1].substrings[-1].text += current_section.substrings[ 862 | 0 863 | ].text 864 | new_sections[-1].substrings[ 865 | -1 866 | ].length += current_section.substrings[0].length 867 | new_sections[-1].substrings[-1].lang = ( 868 | current_section.substrings[0].lang 869 | if current_section.substrings[0].lang != "punctuation" 870 | else new_sections[-1].substrings[-1].lang 871 | ) 872 | new_sections[-1].substrings.extend(current_section.substrings[1:]) 873 | for index, substr in enumerate(new_sections[-1].substrings): 874 | if index == 0: 875 | continue 876 | else: 877 | substr.index = ( 878 | new_sections[-1].substrings[index - 1].index 879 | + new_sections[-1].substrings[index - 1].length 880 | ) 881 | elif current_section.lang_section_type == LangSectionType.DIGIT: 882 | new_sections[-1].text += current_section.text 883 | new_sections[-1].substrings[-1].text += current_section.substrings[ 884 | 0 885 | ].text 886 | new_sections[-1].substrings[ 887 | -1 888 | ].length += current_section.substrings[0].length 889 | if new_sections[-1].substrings[-1].lang == "punctuation": 890 | new_sections[-1].substrings[-1].lang = "digit" 891 | 892 | if ( 893 | new_sections[-1].lang_section_type 894 | == LangSectionType.PUNCTUATION 895 | ): 896 | new_sections[-1].lang_section_type = LangSectionType.DIGIT 897 | 898 | else: 899 | new_sections.append(current_section) 900 | # NOTE: 将相同类型的 section 合并 901 | new_sections_merged: List[SubStringSection] = [new_sections[0]] 902 | for index, _ in enumerate(new_sections): 903 | if index == 0: 904 | continue 905 | if ( 906 | new_sections_merged[-1].lang_section_type 907 | == new_sections[index].lang_section_type 908 | ): 909 | new_sections_merged[-1].text += new_sections[index].text 910 | new_sections_merged[-1].substrings.extend( 911 | new_sections[index].substrings 912 | ) 913 | else: 914 | new_sections_merged.append(new_sections[index]) 915 | # NOTE: 重新计算 index 916 | for section_index, section in enumerate(new_sections_merged): 917 | if section_index == 0: 918 | for substr_index, substr in enumerate(section.substrings): 919 | if substr_index == 0: 920 | continue 921 | else: 922 | substr.index = ( 923 | section.substrings[substr_index - 1].index 924 | + section.substrings[substr_index - 1].length 925 | ) 926 | else: 927 | for substr_index, substr in enumerate(section.substrings): 928 | if substr_index == 0: 929 | substr.index = ( 930 | new_sections_merged[section_index - 1].substrings[-1].index 931 | + new_sections_merged[section_index - 1] 932 | .substrings[-1] 933 | .length 934 | ) 935 | else: 936 | substr.index = ( 937 | section.substrings[substr_index - 1].index 938 | + section.substrings[substr_index - 1].length 939 | ) 940 | # NOTE: 再次合并 sections 中的 substrings 里面的 text 941 | for section in new_sections_merged: 942 | section.substrings = self._merge_substrings_across_digit(section.substrings) 943 | if self.debug: 944 | logger.debug("---------------------------------after_merge_digit_sections:") 945 | for section in new_sections_merged: 946 | logger.debug(section) 947 | return new_sections_merged 948 | 949 | def _merge_substrings_across_punctuation( 950 | self, 951 | substrings: List[SubString], 952 | ) -> List[SubString]: 953 | new_substrings: List[SubString] = [] 954 | last_lang = "" # Changed from 'lang' to 'last_lang' for consistency 955 | 956 | for _, substring in enumerate(substrings): 957 | if new_substrings: 958 | if substring.lang == "punctuation": 959 | # If the last substring is also a punctuation, merge them 960 | new_substrings[-1].text += substring.text 961 | new_substrings[-1].length += substring.length 962 | else: 963 | if substring.lang == last_lang or last_lang == "": 964 | new_substrings[-1].text += substring.text 965 | new_substrings[-1].length += substring.length 966 | new_substrings[-1].lang = ( 967 | substring.lang 968 | if new_substrings[-1].lang == "punctuation" 969 | else new_substrings[-1].lang 970 | ) 971 | else: 972 | new_substrings.append(substring) 973 | else: 974 | new_substrings.append(substring) 975 | last_lang = substring.lang 976 | 977 | return new_substrings 978 | 979 | # MARK: _merge_substrings_across_punctuation based on sections 980 | def _merge_substrings_across_punctuation_based_on_sections( 981 | self, 982 | sections: List[SubStringSection], 983 | ) -> List[SubStringSection]: 984 | new_sections: List[SubStringSection] = [sections[0]] 985 | # NOTE: 将 sections 中的 punctuation 合并到临近的非 punctuation 的 section 986 | for index, _ in enumerate(sections): 987 | if index == 0: 988 | continue 989 | # 检查当前 section 和前一个 section 是否可以合并 990 | if index >= len(sections): 991 | break 992 | 993 | current_section = sections[index] 994 | # 如果前一个 section 和当前的 section 类型不同,且其中一个是 punctuation,则合并 995 | one_of_section_is_punctuation = ( 996 | new_sections[-1].lang_section_type == LangSectionType.PUNCTUATION 997 | or current_section.lang_section_type == LangSectionType.PUNCTUATION 998 | ) 999 | if one_of_section_is_punctuation: 1000 | # 如果当前 section 是 punctuation,且第一个元素不是 not_merge_punctuation,则合并 1001 | 1002 | if ( 1003 | new_sections[-1].lang_section_type == LangSectionType.PUNCTUATION 1004 | and new_sections[-1].substrings[0].text 1005 | not in self.not_merge_punctuation 1006 | ): 1007 | # 将前一个 punctuation section 和当前的 section 合并 1008 | new_sections[-1].text += current_section.text 1009 | new_sections[ 1010 | -1 1011 | ].lang_section_type = current_section.lang_section_type 1012 | new_sections[-1].substrings[-1].text += current_section.substrings[ 1013 | 0 1014 | ].text 1015 | new_sections[-1].substrings[ 1016 | -1 1017 | ].length += current_section.substrings[0].length 1018 | new_sections[-1].substrings[-1].lang = current_section.substrings[ 1019 | 0 1020 | ].lang 1021 | new_sections[-1].substrings.extend(current_section.substrings[1:]) 1022 | for index, substr in enumerate(new_sections[-1].substrings): 1023 | if index == 0: 1024 | # 第一个元素是前一个 punctuation section 的最后一个元素,不需要修改 1025 | continue 1026 | else: 1027 | # 其他元素是当前 section 的元素,需要修改 index 1028 | substr.index = ( 1029 | new_sections[-1].substrings[index - 1].index 1030 | + new_sections[-1].substrings[index - 1].length 1031 | ) 1032 | elif ( 1033 | current_section.lang_section_type == LangSectionType.PUNCTUATION 1034 | and current_section.substrings[0].text 1035 | not in self.not_merge_punctuation 1036 | ): 1037 | # 将当前的 punctuation section 和前一个 section 合并 1038 | new_sections[-1].text += current_section.text 1039 | new_sections[-1].substrings[-1].text += current_section.substrings[ 1040 | 0 1041 | ].text 1042 | new_sections[-1].substrings[ 1043 | -1 1044 | ].length += current_section.substrings[0].length 1045 | 1046 | new_sections[-1].substrings.extend(current_section.substrings[1:]) 1047 | for index, substr in enumerate(new_sections[-1].substrings): 1048 | if index == 0: 1049 | continue 1050 | else: 1051 | substr.index = ( 1052 | new_sections[-1].substrings[index - 1].index 1053 | + new_sections[-1].substrings[index - 1].length 1054 | ) 1055 | else: 1056 | new_sections.append(current_section) 1057 | else: 1058 | new_sections.append(current_section) 1059 | 1060 | # NOTE: 将相同类型的 section 合并 1061 | new_sections_merged: List[SubStringSection] = [new_sections[0]] 1062 | for index, _ in enumerate(new_sections): 1063 | if index == 0: 1064 | continue 1065 | if ( 1066 | new_sections_merged[-1].lang_section_type 1067 | == new_sections[index].lang_section_type 1068 | ): 1069 | new_sections_merged[-1].text += new_sections[index].text 1070 | new_sections_merged[-1].substrings.extend( 1071 | new_sections[index].substrings 1072 | ) 1073 | else: 1074 | new_sections_merged.append(new_sections[index]) 1075 | # NOTE: 重新计算 index 1076 | for section_index, section in enumerate(new_sections_merged): 1077 | if section_index == 0: 1078 | for substr_index, substr in enumerate(section.substrings): 1079 | if substr_index == 0: 1080 | continue 1081 | else: 1082 | substr.index = ( 1083 | section.substrings[substr_index - 1].index 1084 | + section.substrings[substr_index - 1].length 1085 | ) 1086 | else: 1087 | for substr_index, substr in enumerate(section.substrings): 1088 | if substr_index == 0: 1089 | substr.index = ( 1090 | new_sections_merged[section_index - 1].substrings[-1].index 1091 | + new_sections_merged[section_index - 1] 1092 | .substrings[-1] 1093 | .length 1094 | ) 1095 | else: 1096 | substr.index = ( 1097 | section.substrings[substr_index - 1].index 1098 | + section.substrings[substr_index - 1].length 1099 | ) 1100 | # NOTE: 再次合并 sections 中的 substrings 里面的 text 1101 | for section in new_sections_merged: 1102 | section.substrings = self._merge_substrings_across_punctuation( 1103 | section.substrings 1104 | ) 1105 | if self.debug: 1106 | logger.debug( 1107 | "---------------------------------after_merge_punctuation_sections:" 1108 | ) 1109 | for section in new_sections_merged: 1110 | logger.debug(section) 1111 | return new_sections_merged 1112 | 1113 | # MARK: _init_substr_lang 1114 | def _init_substr_lang( 1115 | self, 1116 | texts: List[str], 1117 | lang_section_type: LangSectionType, 1118 | lang_map: Dict[str, str] = None, 1119 | ) -> List[SubString]: 1120 | substrings: List[SubString] = [] 1121 | substring_index = 0 1122 | lang_map = self.lang_map if lang_map is None else lang_map 1123 | if self.debug: 1124 | logger.debug("---------lang_map:") 1125 | logger.debug(lang_map) 1126 | for text in texts: 1127 | length = len(text) 1128 | 1129 | cur_lang = detect_lang_combined(text, lang_section_type=lang_section_type) 1130 | cur_lang = lang_map.get(cur_lang, self.default_lang) 1131 | temp_substr = SubString( 1132 | lang=cur_lang, 1133 | text=text, 1134 | length=length, 1135 | index=substring_index, 1136 | ) 1137 | if self.debug: 1138 | logger.debug( 1139 | f"---------lang_map: {lang_map}, temp_substr: {temp_substr}" 1140 | ) 1141 | substrings.append(temp_substr) 1142 | 1143 | substring_index += length 1144 | return substrings 1145 | 1146 | # MARK: _get_languages 1147 | def _get_languages( 1148 | self, 1149 | lang_text_list: List[SubString], 1150 | lang_section_type: LangSectionType, 1151 | lang_map: Dict[str, str] = None, 1152 | ): 1153 | if lang_section_type in [ 1154 | LangSectionType.DIGIT, 1155 | LangSectionType.KO, 1156 | LangSectionType.PUNCTUATION, 1157 | ]: 1158 | return lang_text_list 1159 | 1160 | if lang_map is None: 1161 | lang_map = self.lang_map 1162 | 1163 | for _, substr in enumerate(lang_text_list): 1164 | cur_lang = detect_lang_combined( 1165 | text=substr.text, lang_section_type=lang_section_type 1166 | ) 1167 | cur_lang = lang_map.get(cur_lang, self.default_lang) 1168 | 1169 | return lang_text_list 1170 | -------------------------------------------------------------------------------- /split_lang/split/utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | PUNCTUATION = r"""〜~,.;:!?,。!?;:、·([{<(【《〈「『“‘)]}>)】》〉」』”’"-_——#$%&……¥'*+<=>?@[\]^_`{|}~""" 4 | 5 | chinese_char_pattern = re.compile(r"[\u4e00-\u9fff]") 6 | hangul_pattern = re.compile(r"[\uac00-\ud7af]") 7 | hiragana_katakana_pattern = re.compile(r"[\u3040-\u30ff々]") # 添加了对“々”符号的判断 8 | 9 | zh_ja_pattern = re.compile(r"[\u4e00-\u9fff\u3040-\u30ff々]") 10 | 11 | 12 | def contains_chinese_char(text: str) -> bool: 13 | """ 14 | check if the text contains Chinese character 15 | 16 | Args: 17 | text (str): the text to check 18 | 19 | Returns: 20 | """ 21 | return bool(chinese_char_pattern.search(text)) 22 | 23 | def contains_hangul(text: str) -> bool: 24 | """ 25 | check if the text contains hangul 26 | 27 | Args: 28 | text (str): the text to check 29 | 30 | Returns: 31 | """ 32 | return bool(hangul_pattern.search(text)) 33 | def contains_ja_kana(text: str) -> bool: 34 | """ 35 | check if the text contains Japanese or kana 36 | 37 | Args: 38 | text (str): the text to check 39 | 40 | Returns: 41 | """ 42 | return bool(hiragana_katakana_pattern.search(text)) 43 | 44 | def contains_zh_ja(text: str) -> bool: 45 | """ 46 | check if the text contains Chinese or Japanese 47 | 48 | Args: 49 | text (str): the text to check 50 | 51 | Returns: 52 | bool: if the text contains Chinese or Japanese, return True, otherwise return False 53 | """ 54 | return bool(zh_ja_pattern.search(text)) 55 | 56 | def contains_only_kana(text: str) -> bool: 57 | """ 58 | check if the text only contains hiragana or katakana 59 | 60 | Args: 61 | text (str): the text to check 62 | 63 | Returns: 64 | bool: if the text only contains hiragana or katakana, return True, otherwise return False 65 | """ 66 | is_only_kana = True 67 | for char in text: 68 | if not hiragana_katakana_pattern.match(char): 69 | is_only_kana = False 70 | break 71 | return is_only_kana 72 | 73 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | from .data.test_data import texts_zh_jp_ko_en, texts_de_fr_en 2 | -------------------------------------------------------------------------------- /tests/data/__init__.py: -------------------------------------------------------------------------------- 1 | from .test_data import * 2 | -------------------------------------------------------------------------------- /tests/data/correct_split.txt: -------------------------------------------------------------------------------- 1 | 我是 |VGroupChatBot|,|一个旨在支持多人通信的助手|,|通过可视化消息来帮助团队成员更好地交流|。|我可以帮助团队成员更好地整理和共享信息|,|特别是在讨论|、|会议和|Brainstorming|等情况下|。|你好我的名字是|西野くまです|my name is bob|很高兴认识你|どうぞよろしくお願いいたします|「|こんにちは|」|是什么意思|。 2 | 我的名字是|西野くまです|。|I am from Tokyo|, |日本の首都|。|今天的天气非常好 3 | 你好|,|今日はどこへ行きますか|? 4 | 你好|今日はどこへ行きますか|? 5 | 我的名字是|田中さんです|。 6 | 我喜欢吃寿司和拉面|おいしいです|。 7 | 今天|の天気はとてもいいですね|。 8 | 我在学习|日本語少し難しいです|。 9 | 日语真是|おもしろい|啊 10 | 你喜欢看|アニメ|吗|? 11 | 我想去日本旅行|、|特に京都に行きたいです|。 12 | 昨天|見た映画はとても感動的でした|。|我朋友是日本人|彼はとても優しいです|。 13 | 我们一起去|カラオケ|吧|、|楽しそうです|。 14 | 我的家在北京|、|でも|、|仕事で東京に住んでいます|。 15 | 我在学做日本料理|、|日本料理を作るのを習っています|。 16 | 你会说几种语言|、|何ヶ国語話せますか|? 17 | 我昨天看了一本书|、|その本はとても面白かったです|。 18 | 你最近好吗|、|最近どうですか|? 19 | 你最近好吗|最近どうですか|? 20 | 我在学做日本料理|와 한국 요리|、|日本料理を作るのを習っています|。 21 | 你会说几种语言|、|何ヶ国語話せますか|?|몇 개 언어를 할 수 있어요|? 22 | 我昨天看了一本书|、|その本はとても面白かったです|。|어제 책을 읽었는데|, |정말 재미있었어요|。 23 | 我们一起去逛街|와 쇼핑|、|買い物に行きましょう|。|쇼핑하러 가요|。 24 | 你最近好吗|、|最近どうですか|?|요즘 어떻게 지내요|? 25 | Bonjour|, |wie geht's dir |today|? 26 | Vielen Dank |merci beaucoup |for your help|. 27 | Ich bin müde |je suis fatigué |and I need some rest|. 28 | Ich mag dieses Buch |ce livre est intéressant |and it has a great story|. 29 | Ich mag dieses Buch|, |ce livre est intéressant|, |and it has a great story|. 30 | The shirt is |9|.|15 |dollars|. 31 | The shirt is |233 |dollars|. 32 | lang|-|split 33 | I have |10|, |€ 34 | 日本のメディアでは|「|匿名掲示板|」|であると紹介されることが多いが|、|2003|年|1|月|7|日から全書き込みについて|IP|アドレスの記録・保存を始めており|、|厳密には匿名掲示板ではなくなっていると|CNET Japan|は報じている 35 | 日本語|(|にほんご|、|にっぽんご|)|は|、|日本国内や|、|かつての日本領だった国|、|そして国外移民や移住者を含む日本人同士の間で使用されている言語|。|日本は法令によって公用語を規定していないが|、|法令その他の公用文は全て日本語で記述され|、|各種法令において日本語を用いることが規定され|、|学校教育においては|「|国語|」|の教科として学習を行うなど|、|事実上日本国内において唯一の公用語となっている|。 36 | 日语是日本通用语及事实上的官方语言|。|没有精确的日语使用人口的统计|,|如果计算日本人口以及居住在日本以外的日本人|、|日侨和日裔|,|日语使用者应超过一亿三千万人|。 37 | MyGO|?,|你也喜欢|まいご|吗|? 38 | まいご|我可太喜欢了 39 | 我的名字|is|小明|。 40 | 我的|名字は小田です|。 41 | 我喜欢|食べる|蔬菜和水果|。 42 | 食べる|蔬菜和水果我喜欢|。 43 | 寿司和蔬菜我都喜欢|。 44 | すし|和蔬菜我都喜欢|。 45 | 寝る|和蔬菜我都喜欢|。 46 | 料理私は大好きです|。 47 | 游戏|大好きです|。 48 | 游戏|面白いです|。 49 | 初次见面|,|私の名前は|Doodle|です|。 50 | 中国語と日本語のミックス|中文和日文的混合 51 | 真的很难分离|玉子焼き 52 | 真的很难分离|すしリンゴ|苹果 53 | 全是汉字很难分离|果物|苹果 54 | 寂しい|的人 55 | 我的姓名|は|爱音 56 | 我的|名前は|爱音 57 | 我的|名字は|爱音 58 | 我的|名前|是爱音 59 | -------------------------------------------------------------------------------- /tests/data/correct_split_merge_punc.txt: -------------------------------------------------------------------------------- 1 | 匿名掲示板 2 | 2024年の日本 3 | 你喜欢看|アニメ|吗? 4 | 我的名字是|田中さんです。 5 | 我的名字是田中|さんです。 6 | 日语真是|おもしろい|啊 7 | 衬衫的价格是9.15便士。 8 | 衬衫的价格是233亿元。 9 | 衬衫的价格是233亿元人民币。 10 | 我给你送的|手紙|你读了吗? 11 | 我给你送的|手紙,|你读了吗? 12 | 你最近好吗|最近どうですか? 13 | 你最近好吗最近|どうですか? 14 | 你最近好吗、|最近どうですか? 15 | 你最近好吗、最近|どうですか? 16 | 你好|今日はどこへ行きますか? 17 | 你好今日|はどこへ行きますか? 18 | 今天|の天気はとてもいいですね。 19 | 我在学习|日本語少し難しいです。 20 | 我在学习日本語|少し難しいです。 21 | 我喜欢吃寿司和拉面|おいしいです。 22 | 你会说几种语言、|何ヶ国語話せますか? 23 | 我们一起去|カラオケ|吧、|楽しそうです。 24 | 我想去日本旅行、|特に京都に行きたいです。 25 | 我想去|日本旅行、特に京都に行きたいです。 26 | 我的家在北京、|でも、仕事で東京に住んでいます。 27 | 我昨天看了一本书、|その本はとても面白かったです。 28 | 我在学做日本料理、|日本料理を作るのを習っています。 29 | 我在学做|日本料理、日本料理を作るのを習っています。 30 | 你最近好吗、|最近どうですか|요즘 어떻게 지내요? 31 | 你最近好吗、最近|どうですか|요즘 어떻게 지내요? 32 | 我们一起去逛街|와 쇼핑、|買い物に行きましょう。|쇼핑하러 가요。 33 | 我在学做日本料理|와 한국 요리、|日本料理を作るのを習っています。 34 | 我在学做|日本料理|와 한국 요리、|日本料理を作るのを習っています。 35 | 你会说几种语言、|何ヶ国語話せますか?|몇 개 언어를 할 수 있어요? 36 | 我昨天看了一本书、|その本はとても面白かったです。|어제 책을 읽었는데, 정말 재미있었어요。 37 | 昨天|見た映画はとても感動的でした。|我朋友是日本人|彼はとても優しいです。 38 | 我的名字是|西野くまです。|I am from Tokyo, |日本の首都。|今天的天气非常好 39 | 我是 |VGroupChatBot,|一个旨在支持多人通信的助手,通过可视化消息来帮助团队成员更好地交流。我可以帮助团队成员更好地整理和共享信息,特别是在讨论、会议和|Brainstorming|等情况下。你好我的名字是|西野くまです|my name is bob|很高兴认识你|どうぞよろしくお願いいたします「こんにちは」|是什么意思。 40 | Bonjour, |wie geht's dir |today? 41 | Vielen Dank |merci beaucoup |for your help. 42 | Ich bin müde |je suis fatigué |and I need some rest. 43 | Ich mag dieses Buch |ce livre est intéressant |and it has a great story. 44 | Ich mag dieses Buch, |ce livre est intéressant, |and it has a great story. 45 | The shirt is 9.15 dollars. 46 | The shirt is 233 dollars. 47 | lang-split 48 | I have 10, |€ 49 | 日本のメディアでは「匿名掲示板」であると紹介されることが多いが、2003年1月7日から全書き込みについて|IP|アドレスの記録・保存を始めており、厳密には匿名掲示板ではなくなっていると|CNET Japan|は報じている 50 | 日本語(にほんご、にっぽんご)は、日本国内や、かつての日本領だった国、そして国外移民や移住者を含む日本人同士の間で使用されている言語。日本は法令によって公用語を規定していないが、法令その他の公用文は全て日本語で記述され、各種法令において日本語を用いることが規定され、学校教育においては「国語」の教科として学習を行うなど、事実上日本国内において唯一の公用語となっている。 51 | 日语是日本通用语及事实上的官方语言。没有精确的日语使用人口的统计,如果计算日本人口以及居住在日本以外的日本人、日侨和日裔,日语使用者应超过一亿三千万人。 -------------------------------------------------------------------------------- /tests/data/novel_1.txt: -------------------------------------------------------------------------------- 1 | 入学早々隣のクラスの絵奈は男子からの人気者。 2 | 3 | 新入生代表の挨拶も絵奈だったから目立ってしまって余計にあの子誰?ってなった。 4 | 5 | 絵奈と私には近所に住む幼稚園の頃から仲の良い幼なじみの澤村 和樹(さわむら かずき)という同じ中学の同級生でもある男子生徒がいる。 6 | 7 | 絵奈も私もかずくんと呼んでいる。 8 | 9 | かずくんは小学1年生の頃からサッカーをやっていて、高学年になる頃には県の選抜に選ばれたりと、丸山台中の新入生では期待の星。 10 | 11 | 何でユースとかにいかないで、公立の中学校でサッカー部なのかはわからない。 12 | 13 | かずくんとは絵奈より私の方が話が合うことが多かった。 14 | 15 | 小学生の頃もよく3人で遊んだりもしたし、中学生になってからも3人で朝一緒に行こうなんて約束していた。 16 | 17 | 迎えた入学式後の初登校日。 18 | 19 | 「忘れ物とかない?気を付けて行くんだよ。急がなくても充分間に合うからね。」私にだけそう言ったママ。 20 | 21 | 絵奈には「いってらっしゃい」だけ。 22 | 23 | (二人とも一緒に行くのに、私だけ落ち着きないみたいに思われてる…?) 24 | 25 | 「ほら、早く行くよ。ママ、いってきます♪」 26 | 私の横で姉の絵奈が私に向かってそう言うと、ママには笑顔で挨拶をしていた。 27 | 28 | そそくさと出てしまう絵奈。 29 | 30 | 「ちょ、ちょっと(汗)充分間に合うからねってママ言ってるじゃん(汗)んじゃいってきま〜す!」 31 | 32 | そう言って、私も絵奈の後を急いで追う。 33 | 34 | 外に出るとかずくんがちょっと遠くから歩いてきているのが見えたので、私は大声で手を振って呼んだ。 35 | 36 | 「かずく〜ん!!おはよー!!」 37 | 38 | 私の大声に気づいたかずくんは走ってきた。 39 | 40 | 「よぉ!!絵奈と残念な方の妹!(笑)」 41 | 42 | (こいつ…面白がってやがるな…) 43 | 44 | 絵奈が新入生代表で目立ってしまって、私は自分のクラスで残念な方の妹だと、昨日、ふざけた男子に言われたのだ… 45 | 46 | そしてかずくんと私は同じクラスなのでそれを面白がって言ってきたのだろう… 47 | 48 | うるせぇーよって言ってやろうと思ったら、私より先に絵奈が口を開いた。 49 | 50 | 「かずくん、残念な方の妹って何?」 51 | 52 | 絵奈に聞かれるとちょっとバツが悪そうなかずくんが渋々答える。 53 | 54 | 「いや、その、実は俺達のクラスで、ふざけて紗羅が残念な方の妹って男子にからかわれてたんだよ(汗)」 55 | 56 | 「は?で、かずくんも面白がって、そんなこと言ってるわけ?くだらないことやめなよ。そういう時は普通、助ける男がカッコいいんでしょ。一緒になって紗羅が気にしてないからって最悪だよ。」 57 | 58 | 絵奈はどちらかと言うと温厚で優しいタイプなのに少し怒っているように言ったので、かずくんはしょんぼり、私も珍しく絵奈が怒っているので少しびっくりした… 59 | 60 | 3人でちょっと気まずくなって歩くのに耐えられない私が絵奈に言った。 61 | 62 | 「まぁまぁ、そんなツンケンしないでさ(汗)絵奈らしくないよ?かずくんも悪気があって言ったわけじゃないんだし、許してあげなよ(笑)」 63 | 64 | 「私らしいって何?紗羅は悔しくないの?残念な方の妹とか言われて。そもそも紗羅がそんなお気楽な感じだから駄目なんでしょ(怒)私は紗羅が悪口言われてたら自分のことのように面白くないよ(怒)」 65 | 66 | (怒りの矛先が私に向き始めたな…) 67 | 68 | 絵奈は温厚で優しく優等生タイプだけど、怒るとちょっと面倒くさいタイプ…何日も根に持つし、頑固なところもある。 69 | 70 | 私は対照的でカッとなっても、1日経てばそんなこと忘れてるし、あんまり根に持つこともない。頑固な所は同じかもしれないけど。 71 | 72 | 怒り出すと絵奈をなだめるのは結構大変…普段あんまり怒らないタイプだから、かもしれないけど… 73 | 74 | 「そのさ、絵奈は私と違っておとなしめな品のあるタイプなんだから、あんまりカッカしてるとイメージ崩れるよ?なんちゃって(笑)かずくんも少しびっくりしてるでしょ(汗)ほらほら、せっかく入学して登校初日なのに空気悪いのおしまいにして楽しく行こうよ♪(笑)」 75 | 76 | 一応、私がそう言うとカッカしてた絵奈も機嫌を少し直してくれて、かずくんもごめんって謝って一件落着し、その後は仲良く登校し中学校へ無事に到着。 77 | 78 | 「んじゃまた帰りね〜♪」そう言って、絵奈とは別々の教室へ。私はかずくんと同じクラスへ入る。 79 | 80 | 先生が来て、授業が始まる。 81 | 82 | 勉強は嫌いだ… 83 | 84 | (今日の給食なんだっけかなぁ〜) 85 | 86 | なんて授業に集中していないと見事に先生に当てられているのに気づかない。 87 | 88 | 隣の席のかずくんが小さな声で「おーい。紗羅。おーい」って言っているのに気づく。 89 | 90 | (ん?何?教科書忘れたのかこいつは本当にサッカーしかできない奴だからなぁ。しょうがないなぁ) 91 | 92 | 机をくっつけて教科書を見せてやろうとするとかずくんはオドオドする。 93 | 94 | 「ちげーよ(汗)紗羅(汗)」また小さな声でかずくんが言った後には怒った先生が私の目の前にいた… 95 | 96 | 「山本、お前何やってんだ?あくびしたり、外をボケーっと見てたり。同じ山本のお前の姉とは大違いだな。」 97 | 98 | 先生は不機嫌そうに言う。 99 | 100 | (すいませんね〜絵奈だったら、ちゃんと授業聞いてるんでしょうね〜私は残念な方の妹なので(笑))なんて怒られているはずなのに思っていると、朝、絵奈から怒られたからか、かずくんが私を庇った。 101 | 102 | 「その、先生。絵奈は絵奈。紗羅は紗羅ですよ。」 103 | 104 | かずくんの言葉に先生もちょっと悪いと思ったのか、ブツブツ言いながら戻っていき、私がボケーっとしていたことは流されて終わった。 105 | 106 | (おぉーかずくんが助けてくれた(笑)もしかして、絵奈に朝怒られたから?もしかして、かずくんも昔から絵奈のことが…いや、考えるのはやめよう。私達3人は幼馴染。変に関係がおかしくなるのも嫌だし。)なんてふと思った。 107 | 108 | そして、この先、私達3人の関係は壊れていくことにこの時は気づかなかった… -------------------------------------------------------------------------------- /tests/data/test_data.py: -------------------------------------------------------------------------------- 1 | texts_with_digit = [ 2 | "你喜欢看アニメ吗?", 3 | "衬衫的价格是9.15便士", 4 | "衬衫的价格是233亿元", 5 | "衬衫的价格是233亿元人民币", 6 | ] 7 | 8 | texts_zh_jp_ko_en = [ 9 | "我是 VGroupChatBot,一个旨在支持多人通信的助手,通过可视化消息来帮助团队成员更好地交流。我可以帮助团队成员更好地整理和共享信息,特别是在讨论、会议和Brainstorming等情况下。你好我的名字是西野くまですmy name is bob很高兴认识你どうぞよろしくお願いいたします「こんにちは」是什么意思。", 10 | "你好,我的名字是西野くまです。I am from Tokyo, 日本の首都。今天的天气非常好,sky is clear and sunny。おはようございます、皆さん!我们一起来学习吧。Learning languages can be fun and exciting。昨日はとても忙しかったので、今日は少しリラックスしたいです。Let's take a break and enjoy some coffee。中文、日本語、and English are three distinct languages, each with its own unique charm。希望我们能一起进步,一起成长。Let's keep studying and improving our language skills together. ありがとう!", 11 | "你好,今日はどこへ行きますか?", 12 | "你好今日はどこへ行きますか?", 13 | "我的名字是田中さんです。", 14 | "我喜欢吃寿司和拉面おいしいです。", 15 | "今天の天気はとてもいいですね。", 16 | "我在学习日本語少し難しいです。", 17 | "日语真是おもしろい啊", 18 | "你喜欢看アニメ吗?", 19 | "我想去日本旅行、特に京都に行きたいです。", 20 | "昨天見た映画はとても感動的でした。我朋友是日本人彼はとても優しいです。", 21 | "我们一起去カラオケ吧、楽しそうです。", 22 | "我的家在北京、でも、仕事で東京に住んでいます。", 23 | "我在学做日本料理、日本料理を作るのを習っています。", 24 | "你会说几种语言、何ヶ国語話せますか?", 25 | "我昨天看了一本书、その本はとても面白かったです。", 26 | "你最近好吗、最近どうですか?", 27 | "我在学做日本料理와 한국 요리、日本料理を作るのを習っています。", 28 | "你会说几种语言、何ヶ国語話せますか?몇 개 언어를 할 수 있어요?", 29 | "我昨天看了一本书、その本はとても面白かったです。어제 책을 읽었는데, 정말 재미있었어요。", 30 | "我们一起去逛街와 쇼핑、買い物に行きましょう。쇼핑하러 가요。", 31 | "你最近好吗、最近どうですか?요즘 어떻게 지내요?", 32 | ] 33 | 34 | texts_de_fr_en = [ 35 | "Ich liebe Paris, c'est une belle ville, and the food is amazing!", 36 | "Berlin ist wunderbar, je veux y retourner, and explore more.", 37 | "Bonjour, wie geht's dir today?", 38 | "Die Musik hier ist fantastisch, la musique est superbe, and I enjoy it a lot.", 39 | "Guten Morgen, je t'aime, have a great day!", 40 | "Das Wetter ist heute schön, il fait beau aujourd'hui, and it's perfect for a walk.", 41 | "Ich mag dieses Buch, ce livre est intéressant, and it has a great story.", 42 | "Vielen Dank, merci beaucoup, for your help.", 43 | "Wir reisen nach Deutschland, nous voyageons en Allemagne, and we are excited.", 44 | "Ich bin müde, je suis fatigué, and I need some rest.", 45 | "Ich liebe Paris c'est une belle ville and the food is amazing!", 46 | "Berlin ist wunderbar je veux y retourner and explore more.", 47 | "Bonjour wie geht's dir today?", 48 | "Die Musik hier ist fantastisch la musique est superbe and I enjoy it a lot.", 49 | "Guten Morgen je t'aime have a great day!", 50 | "Das Wetter ist heute schön il fait beau aujourd'hui and it's perfect for a walk.", 51 | "Ich mag dieses Buch ce livre est intéressant and it has a great story.", 52 | "Vielen Dank merci beaucoup for your help.", 53 | "Wir reisen nach Deutschland nous voyageons en Allemagne and we are excited.", 54 | "Ich bin müde je suis fatigué and I need some rest.", 55 | ] 56 | 57 | 58 | texts_with_newline = [ 59 | """newline, 60 | 123. 61 | abc 62 | 233""", 63 | ] 64 | -------------------------------------------------------------------------------- /tests/split_acc.py: -------------------------------------------------------------------------------- 1 | import time 2 | from typing import List 3 | 4 | from split_lang.config import DEFAULT_LANG 5 | from split_lang.model import LangSectionType 6 | from split_lang.split.splitter import LangSplitter, SubString 7 | from split_lang.split.utils import PUNCTUATION 8 | from tests.test_config import TEST_DATA_FOLDER 9 | 10 | 11 | def get_corrected_split_result( 12 | splitter: LangSplitter, text_file_path: str 13 | ) -> List[List[SubString]]: 14 | """ 15 | # 1. split by `|` 16 | # 2. convert to SubString, concat to list 17 | """ 18 | corrected_split_result: List[List[SubString]] = [] 19 | 20 | with open(text_file_path, "r", encoding="utf-8") as file: 21 | for line in file: 22 | substrings = line.strip().split("|") 23 | # print(substrings) 24 | 25 | substring_objects: List[SubString] = [] 26 | 27 | current_index = 0 28 | 29 | for substring in substrings: 30 | is_punctuation = substring.strip() in PUNCTUATION 31 | is_digit = substring.strip().isdigit() 32 | lang = DEFAULT_LANG 33 | if is_punctuation: 34 | lang = "punctuation" 35 | elif is_digit: 36 | lang = "digit" 37 | 38 | substring_objects.append( 39 | SubString( 40 | lang=lang, 41 | text=substring, 42 | index=current_index, 43 | length=len(substring), 44 | is_punctuation=is_punctuation, 45 | is_digit=is_digit, 46 | ) 47 | ) 48 | 49 | current_index += len(substring) 50 | substring_objects = splitter._get_languages( 51 | lang_text_list=substring_objects, 52 | lang_section_type=LangSectionType.ALL, 53 | ) 54 | corrected_split_result.append(substring_objects) 55 | 56 | return corrected_split_result 57 | 58 | 59 | def simple_test(splitter: LangSplitter, debug: bool = False): 60 | text_file_name = "correct_split.txt" 61 | correct_split = get_corrected_split_result( 62 | splitter=splitter, text_file_path=f"{TEST_DATA_FOLDER}/{text_file_name}" 63 | ) 64 | correct_total_substring_len = 0 65 | test_total_substring_len = 0 66 | correct_split_num = 0 67 | 68 | original_strings = [] 69 | test_split: List[List[SubString]] = [] 70 | original_strings.clear() 71 | test_split.clear() 72 | total_text_len = sum([len(item) for item in correct_split]) 73 | time_1 = time.time() 74 | # MARK: collect original_strings from .txt and test `split()` 75 | for str_index, correct_substrings in enumerate(correct_split): 76 | current_correct_num = 0 77 | correct_total_substring_len += len(correct_substrings) 78 | substrings_text = [] 79 | for correct_substring in correct_substrings: 80 | substrings_text.append(correct_substring.text) 81 | original_string = "".join(substrings_text) 82 | original_strings.append(original_string) 83 | # print(original_string) 84 | 85 | test_split_substrings = splitter.split_by_lang( 86 | text=original_string, 87 | ) 88 | test_split.append(test_split_substrings) 89 | test_total_substring_len += len(test_split_substrings) 90 | 91 | correct_substrings_text = [ 92 | f"{' '*(3 - len(item.lang))}{'pun' if item.lang == 'punctuation' else item.lang}|{item.text}" 93 | for item in correct_substrings 94 | ] 95 | test_split_substrings_text = [ 96 | f"{' '*(3 - len(item.lang))}{'pun' if item.lang == 'punctuation' else item.lang}|{item.text}" 97 | for item in test_split_substrings 98 | ] 99 | 100 | result_emoji_str = '' 101 | 102 | for test_substring in test_split_substrings: 103 | is_correct = False 104 | for correct_substring in correct_substrings: 105 | if ( 106 | test_substring.text == correct_substring.text 107 | and test_substring.index == correct_substring.index 108 | ): 109 | correct_split_num += 1 110 | current_correct_num += 1 111 | result_emoji_str += '✅' 112 | is_correct = True 113 | break 114 | if not is_correct: 115 | result_emoji_str += '❌' 116 | 117 | 118 | if debug: 119 | print(f"{str_index+1}{result_emoji_str}{'-'*(100 - len(str(str_index+1)))}") 120 | print(f"original_string: {original_string}") 121 | print(f"correct result : {correct_substrings_text}") 122 | print(f"test result : {test_split_substrings_text}") 123 | print( 124 | f"acc : {current_correct_num}/{len(correct_substrings_text)}" 125 | ) 126 | 127 | time_2 = time.time() 128 | precision = correct_split_num / correct_total_substring_len 129 | recall = correct_split_num / test_total_substring_len 130 | f1_score = 2 * precision * recall / (precision + recall) 131 | if debug: 132 | print(f"total substring num: {correct_total_substring_len}") 133 | print(f"test total substring num: {test_total_substring_len}") 134 | print(f"text acc num: {correct_split_num}") 135 | print(f"precision: {precision}") 136 | print(f"recall: {recall}") 137 | print(f"F1 Score: {f1_score}") 138 | print(f"time: {time_2-time_1}") 139 | print(f"process speed char/s: {total_text_len/(time_2-time_1)}") 140 | 141 | return precision 142 | 143 | 144 | def find_best_threshold(splitter: LangSplitter): 145 | best_score = 0 146 | best_threshold = 0 147 | for times in range(5): 148 | for i in range(1, 10): 149 | zeros = "0" * times 150 | threshold = float(f"0.{zeros}{str(i)}") 151 | score = simple_test(splitter=splitter, debug=False) 152 | if score > best_score: 153 | best_score = score 154 | best_threshold = threshold 155 | print(f"updated: best_f1_score: {best_score}") 156 | print(f"updated: best_threshold: {best_threshold}") 157 | print("------") 158 | 159 | print(f"best_score: {best_score}") 160 | print(f"best_threshold: {best_threshold}") 161 | 162 | 163 | def main(): 164 | splitter = LangSplitter( 165 | merge_across_punctuation=False, 166 | merge_across_digit=False, 167 | # log_level=logging.DEBUG, 168 | ) 169 | # find_best_threshold(splitter=splitter) 170 | simple_test(splitter=splitter, debug=True) 171 | 172 | 173 | if __name__ == "__main__": 174 | main() 175 | -------------------------------------------------------------------------------- /tests/test_config.py: -------------------------------------------------------------------------------- 1 | TEST_DATA_FOLDER = "tests/data" 2 | -------------------------------------------------------------------------------- /tests/test_split.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from split_lang import LangSplitter 4 | 5 | texts = [ 6 | "你喜欢看アニメ吗?", 7 | "衬衫的价格是9.15便士", 8 | """。3入学早々隣のクラスの絵奈は男子からの人気者。 9 | 10 | 新入生代表の挨拶も絵奈だったから目立ってしまって余計にあの子誰?ってなった。 11 | 12 | 絵奈と私には近所に住む幼稚園の頃から仲の良い幼なじみの澤村 和樹(さわむら かずき)という同じ中学の同級生でもある男子生徒がいる。 13 | 14 | 絵奈も私もかずくんと呼んでいる。 15 | 16 | かずくんは小学1年生の頃からサッカーをやっていて、高学年になる頃には県の選抜に選ばれたりと、丸山台中の新入生では期待の星。 17 | 18 | 何でユースとかにいかないで、公立の中学校でサッカー部なのかはわからない。 19 | 20 | かずくんとは絵奈より私の方が話が合うことが多かった。 21 | 22 | 小学生の頃もよく3人で遊んだりもしたし、中学生になってからも3人で朝一緒に行こうなんて約束していた。 23 | 24 | 迎えた入学式後の初登校日。 25 | 26 | 「忘れ物とかない?気を付けて行くんだよ。急がなくても充分間に合うからね。」私にだけそう言ったママ。 27 | 28 | 絵奈には「いってらっしゃい」だけ。 29 | 30 | (二人とも一緒に行くのに、私だけ落ち着きないみたいに思われてる…?) 31 | 32 | 「ほら、早く行くよ。ママ、いってきます♪」 33 | 私の横で姉の絵奈が私に向かってそう言うと、ママには笑顔で挨拶をしていた。 34 | 35 | そそくさと出てしまう絵奈。 36 | 37 | 「ちょ、ちょっと(汗)充分間に合うからねってママ言ってるじゃん(汗)んじゃいってきま〜す!」 38 | 39 | そう言って、私も絵奈の後を急いで追う。 40 | 41 | 外に出るとかずくんがちょっと遠くから歩いてきているのが見えたので、私は大声で手を振って呼んだ。 42 | 43 | 「かずく〜ん!!おはよー!!」 44 | 45 | 私の大声に気づいたかずくんは走ってきた。 46 | 47 | 「よぉ!!絵奈と残念な方の妹!(笑)」 48 | 49 | (こいつ…面白がってやがるな…) 50 | 51 | 絵奈が新入生代表で目立ってしまって、私は自分のクラスで残念な方の妹だと、昨日、ふざけた男子に言われたのだ… 52 | 53 | そしてかずくんと私は同じクラスなのでそれを面白がって言ってきたのだろう… 54 | 55 | うるせぇーよって言ってやろうと思ったら、私より先に絵奈が口を開いた。 56 | 57 | 「かずくん、残念な方の妹って何?」 58 | 59 | 絵奈に聞かれるとちょっとバツが悪そうなかずくんが渋々答える。 60 | 61 | 「いや、その、実は俺達のクラスで、ふざけて紗羅が残念な方の妹って男子にからかわれてたんだよ(汗)」 62 | 63 | 「は?で、かずくんも面白がって、そんなこと言ってるわけ?くだらないことやめなよ。そういう時は普通、助ける男がカッコいいんでしょ。一緒になって紗羅が気にしてないからって最悪だよ。」 64 | 65 | 絵奈はどちらかと言うと温厚で優しいタイプなのに少し怒っているように言ったので、かずくんはしょんぼり、私も珍しく絵奈が怒っているので少しびっくりした… 66 | 67 | 3人でちょっと気まずくなって歩くのに耐えられない私が絵奈に言った。 68 | 69 | 「まぁまぁ、そんなツンケンしないでさ(汗)絵奈らしくないよ?かずくんも悪気があって言ったわけじゃないんだし、許してあげなよ(笑)」 70 | 71 | 「私らしいって何?紗羅は悔しくないの?残念な方の妹とか言われて。そもそも紗羅がそんなお気楽な感じだから駄目なんでしょ(怒)私は紗羅が悪口言われてたら自分のことのように面白くないよ(怒)」 72 | 73 | (怒りの矛先が私に向き始めたな…) 74 | 75 | 絵奈は温厚で優しく優等生タイプだけど、怒るとちょっと面倒くさいタイプ…何日も根に持つし、頑固なところもある。 76 | 77 | 私は対照的でカッとなっても、1日経てばそんなこと忘れてるし、あんまり根に持つこともない。頑固な所は同じかもしれないけど。 78 | 79 | 怒り出すと絵奈をなだめるのは結構大変…普段あんまり怒らないタイプだから、かもしれないけど… 80 | 81 | 「そのさ、絵奈は私と違っておとなしめな品のあるタイプなんだから、あんまりカッカしてるとイメージ崩れるよ?なんちゃって(笑)かずくんも少しびっくりしてるでしょ(汗)ほらほら、せっかく入学して登校初日なのに空気悪いのおしまいにして楽しく行こうよ♪(笑)」 82 | 83 | 一応、私がそう言うとカッカしてた絵奈も機嫌を少し直してくれて、かずくんもごめんって謝って一件落着し、その後は仲良く登校し中学校へ無事に到着。 84 | 85 | 「んじゃまた帰りね〜♪」そう言って、絵奈とは別々の教室へ。私はかずくんと同じクラスへ入る。 86 | 87 | 先生が来て、授業が始まる。 88 | 89 | 勉強は嫌いだ… 90 | 91 | (今日の給食なんだっけかなぁ〜) 92 | 93 | なんて授業に集中していないと見事に先生に当てられているのに気づかない。 94 | 95 | 隣の席のかずくんが小さな声で「おーい。紗羅。おーい」って言っているのに気づく。 96 | 97 | (ん?何?教科書忘れたのかこいつは本当にサッカーしかできない奴だからなぁ。しょうがないなぁ) 98 | 99 | 机をくっつけて教科書を見せてやろうとするとかずくんはオドオドする。 100 | 101 | 「ちげーよ(汗)紗羅(汗)」また小さな声でかずくんが言った後には怒った先生が私の目の前にいた… 102 | 103 | 「山本、お前何やってんだ?あくびしたり、外をボケーっと見てたり。同じ山本のお前の姉とは大違いだな。」 104 | 105 | 先生は不機嫌そうに言う。 106 | 107 | (すいませんね〜絵奈だったら、ちゃんと授業聞いてるんでしょうね〜私は残念な方の妹なので(笑))なんて怒られているはずなのに思っていると、朝、絵奈から怒られたからか、かずくんが私を庇った。 108 | 109 | 「その、先生。絵奈は絵奈。紗羅は紗羅ですよ。」 110 | 111 | かずくんの言葉に先生もちょっと悪いと思ったのか、ブツブツ言いながら戻っていき、私がボケーっとしていたことは流されて終わった。 112 | 113 | (おぉーかずくんが助けてくれた(笑)もしかして、絵奈に朝怒られたから?もしかして、かずくんも昔から絵奈のことが…いや、考えるのはやめよう。私達3人は幼馴染。変に関係がおかしくなるのも嫌だし。)なんてふと思った。 114 | 115 | そして、この先、私達3人の関係は壊れていくことにこの時は気づかなかった…""", 116 | "入学早々隣のクラスの絵奈は男子からの人気者。新入生代表の挨拶も絵奈だったから目立ってしまって余計にあの子誰?ってなった。絵奈と私には近所に住む幼稚園の頃から仲の良い幼なじみの澤村 和樹(さわむら かずき)という同じ中学の同級生でもある男子生徒がいる。絵奈も私もかずくんと呼んでいる。かずくんは小学1年生の頃からサッカーをやっていて、高学年になる頃には県の選抜に選ばれたりと、丸山台中の新入生では期待の星。何でユースとかにいかないで、公立の中学校でサッカー部なのかはわからない。かずくんとは絵奈より私の方が話が合うことが多かった。小学生の頃もよく3人で遊んだりもしたし、中学生になってからも3人で朝一緒に行こうなんて約束していた。迎えた入学式後の初登校日。「忘れ物とかない?気を付けて行くんだよ。急がなくても充分間に合うからね。」私にだけそう言ったママ。絵奈には「いってらっしゃい」だけ。(二人とも一緒に行くのに、私だけ落ち着きないみたいに思われてる…?)「ほら、早く行くよ。ママ、いってきます♪」私の横で姉の絵奈が私に向かってそう言うと、ママには笑顔で挨拶をしていた。そそくさと出てしまう絵奈。「ちょ、ちょっと(汗)充分間に合うからねってママ言ってるじゃん(汗)んじゃいってきま〜す!」そう言って、私も絵奈の後を急いで追う。外に出るとかずくんがちょっと遠くから歩いてきているのが見えたので、私は大声で手を振って呼んだ。「かずく〜ん!!おはよー!!」私の大声に気づいたかずくんは走ってきた。「よぉ!!絵奈と残念な方の妹!(笑)」(こいつ…面白がってやがるな…)絵奈が新入生代表で目立ってしまって、私は自分のクラスで残念な方の妹だと、昨日、ふざけた男子に言われたのだ…そしてかずくんと私は同じクラスなのでそれを面白がって言ってきたのだろう…うるせぇーよって言ってやろうと思ったら、私より先に絵奈が口を開いた。「かずくん、残念な方の妹って何?」絵奈に聞かれるとちょっとバツが悪そうなかずくんが渋々答える。「いや、その、実は俺達のクラスで、ふざけて紗羅が残念な方の妹って男子にからかわれてたんだよ(汗)」「は?で、かずくんも面白がって、そんなこと言ってるわけ?くだらないことやめなよ。そういう時は普通、助ける男がカッコいいんでしょ。一緒になって紗羅が気にしてないからって最悪だよ。」絵奈はどちらかと言うと温厚で優しいタイプなのに少し怒っているように言ったので、かずくんはしょんぼり、私も珍しく絵奈が怒っているので少しびっくりした…3人でちょっと気まずくなって歩くのに耐えられない私が絵奈に言った。「まぁまぁ、そんなツンケンしないでさ(汗)絵奈らしくないよ?かずくんも悪気があって言ったわけじゃないんだし、許してあげなよ(笑)」「私らしいって何?紗羅は悔しくないの?残念な方の妹とか言われて。そもそも紗羅がそんなお気楽な感じだから駄目なんでしょ(怒)私は紗羅が悪口言われてたら自分のことのように面白くないよ(怒)」(怒りの矛先が私に向き始めたな…)絵奈は温厚で優しく優等生タイプだけど、怒るとちょっと面倒くさいタイプ…何日も根に持つし、頑固なところもある。私は対照的でカッとなっても、1日経てばそんなこと忘れてるし、あんまり根に持つこともない。頑固な所は同じかもしれないけど。怒り出すと絵奈をなだめるのは結構大変…普段あんまり怒らないタイプだから、かもしれないけど…「そのさ、絵奈は私と違っておとなしめな品のあるタイプなんだから、あんまりカッカしてるとイメージ崩れるよ?なんちゃって(笑)かずくんも少しびっくりしてるでしょ(汗)ほらほら、せっかく入学して登校初日なのに空気悪いのおしまいにして楽しく行こうよ♪(笑)」一応、私がそう言うとカッカしてた絵奈も機嫌を少し直してくれて、かずくんもごめんって謝って一件落着し、その後は仲良く登校し中学校へ無事に到着。「んじゃまた帰りね〜♪」そう言って、絵奈とは別々の教室へ。私はかずくんと同じクラスへ入る。先生が来て、授業が始まる。勉強は嫌いだ…(今日の給食なんだっけかなぁ〜)なんて授業に集中していないと見事に先生に当てられているのに気づかない。隣の席のかずくんが小さな声で「おーい。紗羅。おーい」って言っているのに気づく。(ん?何?教科書忘れたのかこいつは本当にサッカーしかできない奴だからなぁ。しょうがないなぁ)机をくっつけて教科書を見せてやろうとするとかずくんはオドオドする。「ちげーよ(汗)紗羅(汗)」また小さな声でかずくんが言った後には怒った先生が私の目の前にいた…「山本、お前何やってんだ?あくびしたり、外をボケーっと見てたり。同じ山本のお前の姉とは大違いだな。」先生は不機嫌そうに言う。(すいませんね〜絵奈だったら、ちゃんと授業聞いてるんでしょうね〜私は残念な方の妹なので(笑))なんて怒られているはずなのに思っていると、朝、絵奈から怒られたからか、かずくんが私を庇った。「その、先生。絵奈は絵奈。紗羅は紗羅ですよ。」かずくんの言葉に先生もちょっと悪いと思ったのか、ブツブツ言いながら戻っていき、私がボケーっとしていたことは流されて終わった。(おぉーかずくんが助けてくれた(笑)もしかして、絵奈に朝怒られたから?もしかして、かずくんも昔から絵奈のことが…いや、考えるのはやめよう。私達3人は幼馴染。変に関係がおかしくなるのも嫌だし。)なんてふと思った。そして、この先、私達3人の関係は壊れていくことにこの時は気づかなかった…", 117 | "衬衫的价格是9.15便士", 118 | "I have 10 €", 119 | "2.术语和定义 2.Terms and Definitions", 120 | "(2.2)术语和定义 (2.2)Terms and Definitions", 121 | "MyGO|?|,|你也喜欢|まいご|吗|?", 122 | ] 123 | 124 | lang_splitter = LangSplitter(log_level=logging.DEBUG) 125 | 126 | 127 | def test_split_step_by_step(): 128 | for text in texts: 129 | pre_split_sections = lang_splitter.pre_split( 130 | text=text, 131 | ) 132 | # for section in pre_split_sections: 133 | # print(section) 134 | 135 | split_sections = lang_splitter._split( 136 | pre_split_section=pre_split_sections, 137 | ) 138 | 139 | # for section in split_sections: 140 | # print(section) 141 | 142 | after_merge_punctuation_sections = ( 143 | lang_splitter._merge_substrings_across_punctuation_based_on_sections( 144 | sections=split_sections, 145 | ) 146 | ) 147 | # for section in after_merge_punctuation_sections: 148 | # print(section) 149 | after_merge_digit_sections = ( 150 | lang_splitter._merge_substrings_across_digit_based_on_sections( 151 | sections=after_merge_punctuation_sections, 152 | ) 153 | ) 154 | 155 | # for section in after_merge_digit_sections: 156 | # print(section) 157 | 158 | after_merge_newline_sections = ( 159 | lang_splitter._merge_substrings_across_newline_based_on_sections( 160 | sections=after_merge_digit_sections, 161 | ) 162 | ) 163 | # for section in after_merge_newline_sections: 164 | # print(section) 165 | 166 | 167 | def test_split(): 168 | print("===========test_split===========") 169 | lang_splitter.merge_across_digit = False 170 | lang_splitter.merge_across_punctuation = False 171 | # lang_splitter.not_merge_punctuation = ["。"] 172 | for text in texts: 173 | substrings = lang_splitter.split_by_lang(text=text) 174 | for index, item in enumerate(substrings): 175 | print(f"{index}|{item.lang}:{item.text}") 176 | print("----------------------") 177 | 178 | 179 | def main(): 180 | # test_split_step_by_step() 181 | test_split() 182 | pass 183 | 184 | 185 | if __name__ == "__main__": 186 | main() 187 | -------------------------------------------------------------------------------- /tests/test_split_to_substrings.py: -------------------------------------------------------------------------------- 1 | from split_lang import LangSplitter 2 | from tests.data.test_data import texts_with_newline, texts_zh_jp_ko_en 3 | 4 | lang_splitter = LangSplitter(special_merge_for_zh_ja=True) 5 | 6 | 7 | def test_split_to_substring(): 8 | for text in texts_zh_jp_ko_en: 9 | substr = lang_splitter.split_by_lang( 10 | text=text, 11 | ) 12 | for _, item in enumerate(substr): 13 | print(item) 14 | # print(f"{index}|{item.lang}:{item.text}") 15 | print("----------------------") 16 | 17 | # for text in texts_de_fr_en: 18 | # substr = lang_splitter.split_by_lang( 19 | # text=text, 20 | # ) 21 | # for _, item in enumerate(substr): 22 | # print(item) 23 | # # print(f"{index}|{item.lang}:{item.text}") 24 | # print("----------------------") 25 | 26 | # lang_splitter.merge_across_digit = False 27 | # for text in texts_with_digit: 28 | # substr = lang_splitter.split_by_lang( 29 | # text=text, 30 | # ) 31 | # for _, item in enumerate(substr): 32 | # print(item) 33 | # # print(f"{index}|{item.lang}:{item.text}") 34 | # print("----------------------") 35 | 36 | # lang_splitter.merge_across_digit = True 37 | # lang_splitter.merge_across_punctuation = True 38 | # for text in texts_with_digit: 39 | # substr = lang_splitter.split_by_lang( 40 | # text=text, 41 | # ) 42 | # for _, item in enumerate(substr): 43 | # print(item) 44 | # # print(f"{index}|{item.lang}:{item.text}") 45 | # print("----------------------") 46 | 47 | 48 | def test_split_to_substring_newline(): 49 | for text in texts_with_newline: 50 | substr = lang_splitter.split_by_lang( 51 | text=text, 52 | ) 53 | for index, item in enumerate(substr): 54 | print(item) 55 | # print(f"{index}|{item.lang}:{item.text}") 56 | print("----------------------") 57 | 58 | 59 | def main(): 60 | # test_split_to_substring() 61 | test_split_to_substring_newline() 62 | 63 | 64 | if __name__ == "__main__": 65 | main() 66 | --------------------------------------------------------------------------------