diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 2077b52..b0b00b4 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -9,101 +9,77 @@ on: jobs: - build_manylinux: - name: Build for manylinux2010 + build_manylinux2014: + name: Build for manylinux2014 runs-on: ubuntu-latest container: - image: docker://quay.io/pypa/manylinux2010_x86_64 + image: docker://quay.io/pypa/manylinux2014_x86_64 steps: - uses: actions/checkout@v1 with: submodules: recursive - - name: Install Git LFS - run: | - mkdir gitlfs && pushd gitlfs - curl -L https://github.com/git-lfs/git-lfs/releases/download/v2.13.2/git-lfs-linux-amd64-v2.13.2.tar.gz | tar -zxv - ./install.sh - popd - - name: Pull LFS files - run: cd Kiwi && git config --global --add safe.directory /__w/kiwipiepy/kiwipiepy/Kiwi && git lfs pull - name: Deploy continue-on-error: True env: TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} run: | - mv Kiwi/models/base/sj.* model/kiwipiepy_model/ - mv Kiwi/models/base/extract.mdl model/kiwipiepy_model/ - mv Kiwi/models/base/*.dict model/kiwipiepy_model/ - mv Kiwi/models/base/combiningRule.txt model/kiwipiepy_model/ - mv Kiwi/models/base/skipbigram.mdl model/kiwipiepy_model/ + mv Kiwi/models/cong/base/sj.* model/kiwipiepy_model/ + mv Kiwi/models/cong/base/*.mdl model/kiwipiepy_model/ + mv Kiwi/models/cong/base/*.dict model/kiwipiepy_model/ + mv Kiwi/models/cong/base/combiningRule.txt model/kiwipiepy_model/ - /opt/python/cp38-cp38/bin/pip install "cmake<4" + /opt/python/cp311-cp311/bin/pip install "cmake<4" rm /usr/local/bin/cmake || true - ln -s /opt/python/cp38-cp38/bin/cmake /usr/local/bin/cmake + ln -s /opt/python/cp311-cp311/bin/cmake /usr/local/bin/cmake yum install libffi-devel -y - /opt/python/cp38-cp38/bin/python -m pip install --upgrade pip "setuptools<71" - /opt/python/cp38-cp38/bin/python -m pip install "readme-renderer==41.0" "cryptography<38" "twine<4" wheel numpy==`/opt/python/cp38-cp38/bin/python .github/workflows/numpy_version.py` - /opt/python/cp38-cp38/bin/python setup.py sdist - /opt/python/cp38-cp38/bin/python -m twine upload dist/*.tar.gz - for cp in cp38-cp38 + /opt/python/cp311-cp311/bin/python -m pip install --upgrade pip setuptools + /opt/python/cp311-cp311/bin/python -m pip install twine wheel numpy==`/opt/python/cp311-cp311/bin/python .github/workflows/numpy_version.py` + for cp in cp39-cp39 cp310-cp310 cp311-cp311 cp312-cp312 cp313-cp313 cp314-cp314 cp314-cp314t do - /opt/python/${cp}/bin/python -m pip install wheel numpy==`/opt/python/${cp}/bin/python .github/workflows/numpy_version.py` + /opt/python/${cp}/bin/python -m pip install wheel setuptools numpy==`/opt/python/${cp}/bin/python .github/workflows/numpy_version.py` USE_MIMALLOC=1 /opt/python/${cp}/bin/python setup.py build bdist_wheel auditwheel repair dist/*-${cp}-linux_x86_64.whl done - /opt/python/cp38-cp38/bin/python -m twine upload wheelhouse/*.whl - - cd model - /opt/python/cp38-cp38/bin/python setup.py sdist - /opt/python/cp38-cp38/bin/python -m twine upload dist/*.tar.gz + /opt/python/cp311-cp311/bin/python -m twine upload wheelhouse/*.whl - build_manylinux2014: - name: Build for manylinux2014 - runs-on: ubuntu-latest - container: - image: docker://quay.io/pypa/manylinux2014_x86_64 + build_macos: + name: Build for macOS + runs-on: macos-15 + strategy: + max-parallel: 4 + matrix: + python-version: [3.9, "3.10", 3.11, 3.12, 3.13, 3.14, 3.14t] - steps: - - uses: actions/checkout@v1 + steps: + - uses: actions/checkout@v2 with: submodules: recursive + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + architecture: arm64 - name: Deploy continue-on-error: True env: TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} run: | - mv Kiwi/models/base/sj.* model/kiwipiepy_model/ - mv Kiwi/models/base/extract.mdl model/kiwipiepy_model/ - mv Kiwi/models/base/*.dict model/kiwipiepy_model/ - mv Kiwi/models/base/combiningRule.txt model/kiwipiepy_model/ - mv Kiwi/models/base/skipbigram.mdl model/kiwipiepy_model/ - - /opt/python/cp311-cp311/bin/pip install "cmake<4" - rm /usr/local/bin/cmake || true - ln -s /opt/python/cp311-cp311/bin/cmake /usr/local/bin/cmake + python -m pip install twine wheel numpy==`python .github/workflows/numpy_version.py` setuptools + MACOSX_DEPLOYMENT_TARGET=11.0 KIWI_CPU_ARCH=arm64 USE_MIMALLOC=1 python setup.py bdist_wheel + twine upload dist/* - yum install libffi-devel -y - /opt/python/cp311-cp311/bin/python -m pip install --upgrade pip setuptools - /opt/python/cp311-cp311/bin/python -m pip install twine wheel numpy==`/opt/python/cp311-cp311/bin/python .github/workflows/numpy_version.py` - for cp in cp39-cp39 cp310-cp310 cp311-cp311 cp312-cp312 cp313-cp313 - do - /opt/python/${cp}/bin/python -m pip install wheel setuptools numpy==`/opt/python/${cp}/bin/python .github/workflows/numpy_version.py` - USE_MIMALLOC=1 /opt/python/${cp}/bin/python setup.py build bdist_wheel - auditwheel repair dist/*-${cp}-linux_x86_64.whl - done - /opt/python/cp311-cp311/bin/python -m twine upload wheelhouse/*.whl - build_macos_13: - name: Build for macOS 13 - runs-on: macOS-13 + build_macos_intel: + name: Build for macOS Intel + runs-on: macos-15-intel strategy: max-parallel: 4 matrix: - python-version: [3.8, 3.9, "3.10", 3.11, 3.12, 3.13] + python-version: [3.9, "3.10", 3.11, 3.12, 3.13, 3.14, 3.14t] steps: - uses: actions/checkout@v2 @@ -113,6 +89,7 @@ jobs: uses: actions/setup-python@v1 with: python-version: ${{ matrix.python-version }} + architecture: x64 - name: Deploy continue-on-error: True env: @@ -121,17 +98,16 @@ jobs: run: | python -m pip install twine wheel numpy==`python .github/workflows/numpy_version.py` setuptools MACOSX_DEPLOYMENT_TARGET=10.14 KIWI_CPU_ARCH=x86_64 USE_MIMALLOC=1 python setup.py bdist_wheel - MACOSX_DEPLOYMENT_TARGET=10.14 KIWI_CPU_ARCH=arm64 USE_MIMALLOC=1 python setup.py bdist_wheel twine upload dist/* build_windows: name: Build for Windows - runs-on: windows-2019 + runs-on: windows-2022 strategy: max-parallel: 4 matrix: - python-version: [3.8, 3.9, "3.10", 3.11, 3.12, 3.13] - architecture: [x86, x64] + python-version: [3.9, "3.10", 3.11, 3.12, 3.13, 3.14, 3.14t] + architecture: [x64] steps: - uses: actions/checkout@v2 @@ -183,7 +159,7 @@ jobs: /opt/python/cp311-cp311/bin/pip install "cmake<4" rm /usr/local/bin/cmake || true ln -s /opt/python/cp311-cp311/bin/cmake /usr/local/bin/cmake - for cp in cp39-cp39 cp310-cp310 cp311-cp311 cp312-cp312 cp313-cp313 + for cp in cp39-cp39 cp310-cp310 cp311-cp311 cp312-cp312 cp313-cp313 cp314-cp314 cp314-cp314t do /opt/python/${cp}/bin/python -m pip install wheel setuptools numpy==`/opt/python/${cp}/bin/python .github/workflows/numpy_version.py` USE_MIMALLOC=1 /opt/python/${cp}/bin/python setup.py build bdist_wheel diff --git a/.github/workflows/deploy_test.yml b/.github/workflows/deploy_test.yml index 6f09ad9..b81ebd8 100644 --- a/.github/workflows/deploy_test.yml +++ b/.github/workflows/deploy_test.yml @@ -6,101 +6,77 @@ on: - 'v*.*.*d' jobs: - build_manylinux: - name: Build for manylinux2010 + build_manylinux2014: + name: Build for manylinux2014 runs-on: ubuntu-latest container: - image: docker://quay.io/pypa/manylinux2010_x86_64 + image: docker://quay.io/pypa/manylinux2014_x86_64 steps: - uses: actions/checkout@v1 with: submodules: recursive - - name: Install Git LFS - run: | - mkdir gitlfs && pushd gitlfs - curl -L https://github.com/git-lfs/git-lfs/releases/download/v2.13.2/git-lfs-linux-amd64-v2.13.2.tar.gz | tar -zxv - ./install.sh - popd - - name: Pull LFS files - run: cd Kiwi && git config --global --add safe.directory /__w/kiwipiepy/kiwipiepy/Kiwi && git lfs pull - name: Deploy continue-on-error: True env: TWINE_USERNAME: ${{ secrets.TEST_PYPI_USERNAME }} TWINE_PASSWORD: ${{ secrets.TEST_PYPI_PASSWORD }} run: | - mv Kiwi/models/base/sj.* model/kiwipiepy_model/ - mv Kiwi/models/base/extract.mdl model/kiwipiepy_model/ - mv Kiwi/models/base/*.dict model/kiwipiepy_model/ - mv Kiwi/models/base/combiningRule.txt model/kiwipiepy_model/ - mv Kiwi/models/base/skipbigram.mdl model/kiwipiepy_model/ + mv Kiwi/models/cong/base/sj.* model/kiwipiepy_model/ + mv Kiwi/models/cong/base/*.mdl model/kiwipiepy_model/ + mv Kiwi/models/cong/base/*.dict model/kiwipiepy_model/ + mv Kiwi/models/cong/base/combiningRule.txt model/kiwipiepy_model/ - /opt/python/cp38-cp38/bin/pip install "cmake<4" + /opt/python/cp311-cp311/bin/pip install "cmake<4" rm /usr/local/bin/cmake || true - ln -s /opt/python/cp38-cp38/bin/cmake /usr/local/bin/cmake + ln -s /opt/python/cp311-cp311/bin/cmake /usr/local/bin/cmake yum install libffi-devel -y - /opt/python/cp38-cp38/bin/python -m pip install --upgrade pip "setuptools<71" - /opt/python/cp38-cp38/bin/python -m pip install "readme-renderer==41.0" "cryptography<38" "twine<4" wheel numpy==`/opt/python/cp38-cp38/bin/python .github/workflows/numpy_version.py` - /opt/python/cp38-cp38/bin/python setup.py sdist - /opt/python/cp38-cp38/bin/python -m twine upload --repository testpypi dist/*.tar.gz - for cp in cp38-cp38 + /opt/python/cp311-cp311/bin/python -m pip install --upgrade pip setuptools + /opt/python/cp311-cp311/bin/python -m pip install twine wheel numpy==`/opt/python/cp311-cp311/bin/python .github/workflows/numpy_version.py` + for cp in cp39-cp39 cp310-cp310 cp311-cp311 cp312-cp312 cp313-cp313 cp314-cp314 cp314-cp314t do - /opt/python/${cp}/bin/python -m pip install wheel numpy==`/opt/python/${cp}/bin/python .github/workflows/numpy_version.py` + /opt/python/${cp}/bin/python -m pip install wheel setuptools numpy==`/opt/python/${cp}/bin/python .github/workflows/numpy_version.py` USE_MIMALLOC=1 /opt/python/${cp}/bin/python setup.py build bdist_wheel auditwheel repair dist/*-${cp}-linux_x86_64.whl done - /opt/python/cp38-cp38/bin/python -m twine upload --repository testpypi wheelhouse/*.whl - - cd model - /opt/python/cp38-cp38/bin/python setup.py sdist - /opt/python/cp38-cp38/bin/python -m twine upload --repository testpypi dist/*.tar.gz + /opt/python/cp311-cp311/bin/python -m twine upload --repository testpypi wheelhouse/*.whl - build_manylinux2014: - name: Build for manylinux2014 - runs-on: ubuntu-latest - container: - image: docker://quay.io/pypa/manylinux2014_x86_64 + build_macos: + name: Build for macOS + runs-on: macOS-15 + strategy: + max-parallel: 4 + matrix: + python-version: [3.9, "3.10", 3.11, 3.12, 3.13, 3.14, 3.14t] - steps: - - uses: actions/checkout@v1 + steps: + - uses: actions/checkout@v2 with: submodules: recursive + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + architecture: arm64 - name: Deploy continue-on-error: True env: TWINE_USERNAME: ${{ secrets.TEST_PYPI_USERNAME }} TWINE_PASSWORD: ${{ secrets.TEST_PYPI_PASSWORD }} run: | - mv Kiwi/models/base/sj.* model/kiwipiepy_model/ - mv Kiwi/models/base/extract.mdl model/kiwipiepy_model/ - mv Kiwi/models/base/*.dict model/kiwipiepy_model/ - mv Kiwi/models/base/combiningRule.txt model/kiwipiepy_model/ - mv Kiwi/models/base/skipbigram.mdl model/kiwipiepy_model/ - - /opt/python/cp311-cp311/bin/pip install "cmake<4" - rm /usr/local/bin/cmake || true - ln -s /opt/python/cp311-cp311/bin/cmake /usr/local/bin/cmake + python -m pip install twine wheel numpy==`python .github/workflows/numpy_version.py` setuptools + MACOSX_DEPLOYMENT_TARGET=11.0 KIWI_CPU_ARCH=arm64 USE_MIMALLOC=1 python setup.py bdist_wheel + twine upload --repository testpypi dist/* - yum install libffi-devel -y - /opt/python/cp311-cp311/bin/python -m pip install --upgrade pip setuptools - /opt/python/cp311-cp311/bin/python -m pip install twine wheel numpy==`/opt/python/cp311-cp311/bin/python .github/workflows/numpy_version.py` - for cp in cp39-cp39 cp310-cp310 cp311-cp311 cp312-cp312 cp313-cp313 - do - /opt/python/${cp}/bin/python -m pip install wheel setuptools numpy==`/opt/python/${cp}/bin/python .github/workflows/numpy_version.py` - USE_MIMALLOC=1 /opt/python/${cp}/bin/python setup.py build bdist_wheel - auditwheel repair dist/*-${cp}-linux_x86_64.whl - done - /opt/python/cp311-cp311/bin/python -m twine upload --repository testpypi wheelhouse/*.whl - build_macos_13: - name: Build for macOS 13 - runs-on: macOS-13 + build_macos_intel: + name: Build for macOS Intel + runs-on: macOS-15-intel strategy: max-parallel: 4 matrix: - python-version: [3.8, 3.9, "3.10", 3.11, 3.12, 3.13] + python-version: [3.9, "3.10", 3.11, 3.12, 3.13, 3.14, 3.14t] steps: - uses: actions/checkout@v2 @@ -110,6 +86,7 @@ jobs: uses: actions/setup-python@v1 with: python-version: ${{ matrix.python-version }} + architecture: x64 - name: Deploy continue-on-error: True env: @@ -118,17 +95,16 @@ jobs: run: | python -m pip install twine wheel numpy==`python .github/workflows/numpy_version.py` setuptools MACOSX_DEPLOYMENT_TARGET=10.14 KIWI_CPU_ARCH=x86_64 USE_MIMALLOC=1 python setup.py bdist_wheel - MACOSX_DEPLOYMENT_TARGET=10.14 KIWI_CPU_ARCH=arm64 USE_MIMALLOC=1 python setup.py bdist_wheel twine upload --repository testpypi dist/* build_windows: name: Build for Windows - runs-on: windows-2019 + runs-on: windows-2022 strategy: max-parallel: 4 matrix: - python-version: [3.8, 3.9, "3.10", 3.11, 3.12, 3.13] - architecture: [x86, x64] + python-version: [3.9, "3.10", 3.11, 3.12, 3.13, 3.14, 3.14t] + architecture: [x64] steps: - uses: actions/checkout@v2 @@ -180,7 +156,7 @@ jobs: /opt/python/cp311-cp311/bin/pip install "cmake<4" rm /usr/local/bin/cmake || true ln -s /opt/python/cp311-cp311/bin/cmake /usr/local/bin/cmake - for cp in cp39-cp39 cp310-cp310 cp311-cp311 cp312-cp312 cp313-cp313 + for cp in cp39-cp39 cp310-cp310 cp311-cp311 cp312-cp312 cp313-cp313 cp314-cp314 cp314-cp314t do /opt/python/${cp}/bin/python -m pip install --upgrade pip setuptools /opt/python/${cp}/bin/python -m pip install wheel numpy==`/opt/python/${cp}/bin/python .github/workflows/numpy_version.py` diff --git a/.github/workflows/numpy_version.py b/.github/workflows/numpy_version.py index 57f0210..00afe95 100644 --- a/.github/workflows/numpy_version.py +++ b/.github/workflows/numpy_version.py @@ -3,9 +3,10 @@ def get_old_numpy_version(use_v1=False): py_version = sys.version_info if not use_v1: + if py_version >= (3, 11): return '2.3.*' if py_version >= (3, 10): return '2.1.*' if py_version >= (3, 9): return '2.0.*' - if py_version >= (3, 13): return '2.1.*' + if py_version >= (3, 13): return '2.3.*' if py_version >= (3, 12): return '1.26.0' if py_version >= (3, 11): return '1.24.0' if py_version >= (3, 10): return '1.22.0' diff --git a/.github/workflows/pull_request_test.yml b/.github/workflows/pull_request_test.yml index 12fd819..7a6cd3c 100644 --- a/.github/workflows/pull_request_test.yml +++ b/.github/workflows/pull_request_test.yml @@ -12,7 +12,7 @@ jobs: strategy: max-parallel: 4 matrix: - cp: [cp310-cp310, cp311-cp311, cp312-cp312, cp313-cp313] + cp: [cp312-cp312, cp313-cp313, cp314-cp314, cp314-cp314t] steps: - uses: actions/checkout@v3 @@ -27,11 +27,10 @@ jobs: multipleRun: | - name: Build run: | - mv Kiwi/models/base/sj.* model/kiwipiepy_model/ - mv Kiwi/models/base/extract.mdl model/kiwipiepy_model/ - mv Kiwi/models/base/*.dict model/kiwipiepy_model/ - mv Kiwi/models/base/combiningRule.txt model/kiwipiepy_model/ - mv Kiwi/models/base/skipbigram.mdl model/kiwipiepy_model/ + mv Kiwi/models/cong/base/sj.* model/kiwipiepy_model/ + mv Kiwi/models/cong/base/*.mdl model/kiwipiepy_model/ + mv Kiwi/models/cong/base/*.dict model/kiwipiepy_model/ + mv Kiwi/models/cong/base/combiningRule.txt model/kiwipiepy_model/ /opt/python/${{ matrix.cp }}/bin/pip install "cmake<4" rm /usr/local/bin/cmake || true @@ -47,16 +46,20 @@ jobs: - name: Test kiwipiepy run: | /opt/python/${{ matrix.cp }}/bin/python -m pip install pytest - /opt/python/${{ matrix.cp }}/bin/python -m pytest -svv test/test_kiwipiepy.py + /opt/python/${{ matrix.cp }}/bin/python -m pytest -svv test/test_kiwipiepy.py test/test_nogil_safety.py - name: Test transformers_addon run: | - for v in {12..46} - do - echo "Test with transformers 4.$v ..." - if /opt/python/${{ matrix.cp }}/bin/python -m pip install -U "transformers<4.$(($v+1))"; then - /opt/python/${{ matrix.cp }}/bin/python -m pytest --verbose test/test_transformers_addon.py - fi - done + if [[ "${{ matrix.cp }}" != *t ]]; then + for v in {12..46} + do + echo "Test with transformers 4.$v ..." + if /opt/python/${{ matrix.cp }}/bin/python -m pip install -U "transformers<4.$(($v+1))"; then + /opt/python/${{ matrix.cp }}/bin/python -m pytest --verbose test/test_transformers_addon.py + fi + done + else + echo "Skipping transformers test for free-threaded Python" + fi - run: tar -zcvf build.tgz build - name: Archive binary uses: actions/upload-artifact@v4 @@ -64,13 +67,13 @@ jobs: name: Linux Binary ${{ matrix.cp }} path: build.tgz - build_macos_13: - name: Build for macOS 13 - runs-on: macOS-13 + build_macos: + name: Build for macOS + runs-on: macOS-15 strategy: max-parallel: 4 matrix: - python-version: ["3.10", 3.11, 3.12, 3.13] + python-version: [3.12, 3.13, 3.14, 3.14t] steps: - uses: actions/checkout@v2 @@ -78,16 +81,16 @@ jobs: submodules: recursive lfs: true - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} + architecture: arm64 - name: Build run: | - mv Kiwi/models/base/sj.* model/kiwipiepy_model/ - mv Kiwi/models/base/extract.mdl model/kiwipiepy_model/ - mv Kiwi/models/base/*.dict model/kiwipiepy_model/ - mv Kiwi/models/base/combiningRule.txt model/kiwipiepy_model/ - mv Kiwi/models/base/skipbigram.mdl model/kiwipiepy_model/ + mv Kiwi/models/cong/base/sj.* model/kiwipiepy_model/ + mv Kiwi/models/cong/base/*.mdl model/kiwipiepy_model/ + mv Kiwi/models/cong/base/*.dict model/kiwipiepy_model/ + mv Kiwi/models/cong/base/combiningRule.txt model/kiwipiepy_model/ python -m pip install numpy==`python .github/workflows/numpy_version.py` setuptools tqdm @@ -95,8 +98,7 @@ jobs: python setup.py build install cd .. - MACOSX_DEPLOYMENT_TARGET=10.14 KIWI_CPU_ARCH=arm64 USE_MIMALLOC=1 python setup.py build - MACOSX_DEPLOYMENT_TARGET=10.14 KIWI_CPU_ARCH=x86_64 USE_MIMALLOC=1 python setup.py build install + MACOSX_DEPLOYMENT_TARGET=11.0 KIWI_CPU_ARCH=arm64 USE_MIMALLOC=1 python setup.py build install python -m pip install numpy==`python .github/workflows/numpy_version.py v1` || true - name: Archive binary uses: actions/upload-artifact@v4 @@ -107,16 +109,60 @@ jobs: - name: Test kiwipiepy run: | python -m pip install pytest - python -m pytest -svv test/test_kiwipiepy.py + python -m pytest -svv test/test_kiwipiepy.py test/test_nogil_safety.py + + build_macos_intel: + name: Build for macOS Intel + runs-on: macOS-15-intel + strategy: + max-parallel: 4 + matrix: + python-version: [3.13, 3.14t] + + steps: + - uses: actions/checkout@v2 + with: + submodules: recursive + lfs: true + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + architecture: x64 + - name: Build + run: | + mv Kiwi/models/cong/base/sj.* model/kiwipiepy_model/ + mv Kiwi/models/cong/base/*.mdl model/kiwipiepy_model/ + mv Kiwi/models/cong/base/*.dict model/kiwipiepy_model/ + mv Kiwi/models/cong/base/combiningRule.txt model/kiwipiepy_model/ + + python -m pip install numpy==`python .github/workflows/numpy_version.py` setuptools tqdm + + cd model + python setup.py build install + cd .. + + MACOSX_DEPLOYMENT_TARGET=10.14 KIWI_CPU_ARCH=x86_64 USE_MIMALLOC=1 python setup.py build install + python -m pip install numpy==`python .github/workflows/numpy_version.py v1` || true + - name: Archive binary + uses: actions/upload-artifact@v4 + with: + name: macOS Intel Binary ${{ matrix.python-version }} + path: | + build/* + - name: Test kiwipiepy + run: | + python -m pip install pytest + python -m pytest -svv test/test_kiwipiepy.py test/test_nogil_safety.py build_windows: name: Build for Windows - runs-on: windows-2019 + runs-on: windows-2022 strategy: max-parallel: 5 matrix: - python-version: ["3.10", 3.11, 3.12, 3.13] - architecture: [x86, x64] + python-version: [3.12, 3.13, 3.14, 3.14t] + architecture: [x64] steps: - uses: actions/checkout@v2 @@ -124,7 +170,7 @@ jobs: submodules: recursive lfs: true - name: Set up Python ${{ matrix.python-version }} ${{ matrix.architecture }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} architecture: ${{ matrix.architecture }} @@ -133,12 +179,11 @@ jobs: python -m pip install --upgrade pip setuptools tqdm python -m pip install numpy==$(python .github/workflows/numpy_version.py) - mv Kiwi/models/base/sj.* model/kiwipiepy_model/ - mv Kiwi/models/base/extract.mdl model/kiwipiepy_model/ - mv Kiwi/models/base/*.dict model/kiwipiepy_model/ - mv Kiwi/models/base/combiningRule.txt model/kiwipiepy_model/ - mv Kiwi/models/base/skipbigram.mdl model/kiwipiepy_model/ - + mv Kiwi/models/cong/base/sj.* model/kiwipiepy_model/ + mv Kiwi/models/cong/base/*.mdl model/kiwipiepy_model/ + mv Kiwi/models/cong/base/*.dict model/kiwipiepy_model/ + mv Kiwi/models/cong/base/combiningRule.txt model/kiwipiepy_model/ + cd model python setup.py build install cd .. @@ -153,7 +198,7 @@ jobs: - name: Test kiwipiepy run: | python -m pip install pytest - python -m pytest -vv test/test_kiwipiepy.py + python -m pytest -vv test/test_kiwipiepy.py test/test_nogil_safety.py build_other_arch: name: Build for manylinux (other arch) @@ -161,7 +206,7 @@ jobs: strategy: max-parallel: 8 matrix: - cp: [cp310-cp310, cp311-cp311, cp312-cp312, cp313-cp313] + cp: [cp312-cp312, cp313-cp313, cp314-cp314, cp314-cp314t] arch: [aarch64] steps: @@ -181,11 +226,10 @@ jobs: multipleRun: | - name: Copy Model files run: | - mv Kiwi/models/base/sj.* model/kiwipiepy_model/ - mv Kiwi/models/base/extract.mdl model/kiwipiepy_model/ - mv Kiwi/models/base/*.dict model/kiwipiepy_model/ - mv Kiwi/models/base/combiningRule.txt model/kiwipiepy_model/ - mv Kiwi/models/base/skipbigram.mdl model/kiwipiepy_model/ + mv Kiwi/models/cong/base/sj.* model/kiwipiepy_model/ + mv Kiwi/models/cong/base/*.mdl model/kiwipiepy_model/ + mv Kiwi/models/cong/base/*.dict model/kiwipiepy_model/ + mv Kiwi/models/cong/base/combiningRule.txt model/kiwipiepy_model/ - name: Install dependencies run: | /opt/python/${{ matrix.cp }}/bin/pip install "cmake<4" @@ -207,7 +251,7 @@ jobs: - name: Test run: | /opt/python/${{ matrix.cp }}/bin/python -m pip install pytest - /opt/python/${{ matrix.cp }}/bin/python -m pytest -svv test/test_kiwipiepy.py + /opt/python/${{ matrix.cp }}/bin/python -m pytest -svv test/test_kiwipiepy.py test/test_nogil_safety.py - name: Archive binary uses: actions/upload-artifact@v4 diff --git a/CMakeLists.txt b/CMakeLists.txt index 39656bd..85c4503 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,6 +2,8 @@ cmake_minimum_required(VERSION 3.9) project(Kiwipiepy) +option(Py_GIL_DISABLED "Build kiwipiepy with GIL disabled" OFF) + set ( CMAKE_CXX_STANDARD 17 ) set ( CMAKE_VERBOSE_MAKEFILE true ) @@ -16,6 +18,11 @@ if(KIWI_USE_MIMALLOC) include_directories( Kiwi/third_party/mimalloc/include ) endif() +if(Py_GIL_DISABLED) + message(STATUS "Build kiwipiepy with GIL disabled") + set ( ADDITIONAL_FLAGS "${ADDITIONAL_FLAGS} -DPy_GIL_DISABLED=1" ) +endif() + if(MSVC) set ( CMAKE_C_FLAGS_DEBUG "-DDEBUG -DC_FLAGS -Zc:__cplusplus -Zi -Od ${ADDITIONAL_FLAGS}" ) set ( CMAKE_CXX_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG}" ) diff --git a/Kiwi b/Kiwi index 0e9b30f..189c76a 160000 --- a/Kiwi +++ b/Kiwi @@ -1 +1 @@ -Subproject commit 0e9b30f90e805d738cb8ee638b03214a2a138f99 +Subproject commit 189c76a110c8cb8978425940de8bd56492a52d7d diff --git a/kiwipiepy/__init__.py b/kiwipiepy/__init__.py index d543b13..dbb2d19 100644 --- a/kiwipiepy/__init__.py +++ b/kiwipiepy/__init__.py @@ -17,7 +17,7 @@ SimilarContext,) import kiwipiepy.sw_tokenizer as sw_tokenizer import kiwipiepy.utils as utils -from kiwipiepy.const import Match +from kiwipiepy.const import Match, Dialect from kiwipiepy.default_typo_transformer import ( basic_typos, continual_typos, diff --git a/kiwipiepy/_c_api.pyi b/kiwipiepy/_c_api.pyi index 5261eb4..48fa2da 100644 --- a/kiwipiepy/_c_api.pyi +++ b/kiwipiepy/_c_api.pyi @@ -157,3 +157,10 @@ form과 tag를 `형태/품사태그`꼴로 합쳐서 반환합니다.''' 해당 문자열이 어떤 언어 문자 집합에 속하는지를 나타냅니다. 문자 집합의 전체 목록에 대해서는 `Kiwi.list_all_scripts()`를 참조하세요.''' ... + + @property + def dialect(self) -> int: + '''.. versionadded:: 0.22.0 + +형태소의 방언 정보를 반환합니다.''' + ... \ No newline at end of file diff --git a/kiwipiepy/_version.py b/kiwipiepy/_version.py index e453371..81edede 100644 --- a/kiwipiepy/_version.py +++ b/kiwipiepy/_version.py @@ -1 +1 @@ -__version__ = '0.21.0' +__version__ = '0.22.0' diff --git a/kiwipiepy/_wrap.py b/kiwipiepy/_wrap.py index a7ae0f6..3cfddbf 100644 --- a/kiwipiepy/_wrap.py +++ b/kiwipiepy/_wrap.py @@ -10,7 +10,7 @@ from kiwipiepy._c_api import Token from kiwipiepy._version import __version__ from kiwipiepy.utils import Stopwords -from kiwipiepy.const import Match +from kiwipiepy.const import Match, Dialect from kiwipiepy.template import Template class Sentence(NamedTuple): @@ -49,6 +49,7 @@ class SimilarMorpheme(NamedTuple): '''의미적으로 유사한 형태소 정보를 담는 `namedtuple`입니다.''' form: str tag: POSTag + sense_id: int id: int score: float @@ -56,18 +57,23 @@ class SimilarMorpheme(NamedTuple): def form_tag(self) -> Tuple[str, POSTag]: return (self.form, self.tag) + @property + def form_tag_sense(self) -> Tuple[str, POSTag, int]: + return (self.form, self.tag, self.sense_id) + def __repr__(self): - return f'SimilarMorpheme(form={self.form!r}, tag={self.tag!r}, id={self.id!r}, score={self.score:.4g})' + return f'SimilarMorpheme(form={self.form!r}, tag={self.tag!r}, sense_id={self.sense_id!r}, id={self.id!r}, score={self.score:.4g})' SimilarMorpheme.form.__doc__ = '형태소의 형태' SimilarMorpheme.tag.__doc__ = '형태소의 품사 태그' +SimilarMorpheme.sense_id.__doc__ = '형태소의 의미 번호' SimilarMorpheme.id.__doc__ = '형태소의 고유 ID' SimilarMorpheme.score.__doc__ = '형태소의 유사도 점수' class SimilarContext(NamedTuple): '''의미적으로 유사한 문맥 정보를 담는 `namedtuple`입니다.''' forms: List[str] - analyses: List[List[Tuple[str, POSTag]]] + analyses: List[List[Tuple[str, POSTag, int]]] id: int score: float @@ -77,7 +83,7 @@ def repr_form(self) -> str: return self.forms[0] @property - def repr_analyses(self) -> List[Tuple[str, POSTag]]: + def repr_analyses(self) -> List[Tuple[str, POSTag, int]]: '''문맥들의 대표 형태의 형태소 분석 결과''' return self.analyses[0] @@ -181,6 +187,17 @@ def _convert_consonant(s): ret.append(c) return ''.join(ret) +def _convert_dialect(dialect): + if isinstance(dialect, str): + ds = dialect.upper().split(',') + dialect = 0 + for d in ds: + try: + dialect |= Dialect[d] + except KeyError: + raise ValueError(f"Unknown dialect name: {d}") + return dialect + class TypoTransformer(_TypoTransformer): '''.. versionadded:: 0.13.0 @@ -339,31 +356,32 @@ class MorphemeSet(_MorphemeSet): ---------- kiwi: Kiwi 형태소 집합을 정의할 Kiwi의 인스턴스입니다. -morphs: Iterable[Union[str, Tuple[str, POSTag]]] +morphs: Iterable[Union[str, Tuple[str, POSTag], Tuple[str, POSTag, int]]] 집합에 포함될 형태소의 목록입니다. 형태소는 단일 `str`이나 `tuple`로 표기될 수 있습니다. Notes ----- -형태소는 다음과 같이 크게 3가지 방법으로 표현될 수 있습니다. +형태소는 다음과 같이 크게 4가지 방법으로 표현될 수 있습니다. ```python morphset = MorphemeSet([ - '고마움' # 형태만을 사용해 표현. 형태가 '고마움'인 모든 형태소가 이 집합에 포함됨 - '고마움/NNG' # 형태와 품사 태그를 이용해 표현. 형태가 '고마움'인 일반 명사만 이 집합에 포함됨 - ('고마움', 'NNG') # tuple로 분리해서 표현하는 것도 가능 + '고마움', # 형태만을 사용해 표현. 형태가 '고마움'인 모든 형태소가 이 집합에 포함됨 + '고마움/NNG', # 형태와 품사 태그를 이용해 표현. 형태가 '고마움'인 일반 명사만 이 집합에 포함됨 + ('고마움', 'NNG'), # tuple로 분리해서 표현하는 것도 가능 + ('고마움', 'NNG', 1), # tuple의 세번째 원소로 의미 번호를 지정할 수도 있음. ]) ``` ''' def __init__(self, kiwi, - morphs:Iterable[Union[str, Tuple[str, POSTag]]] + morphs:Iterable[Union[str, Tuple[str, POSTag], Tuple[str, POSTag, int]]] ): if not isinstance(kiwi, Kiwi): raise ValueError("`kiwi` must be an instance of `Kiwi`.") super().__init__(kiwi) self.kiwi = kiwi self.set = set(map(self._normalize, morphs)) - self._updated = False + self._update(self.set) def __repr__(self): return f"MorphemeSet(kiwi, {repr(self.set)})" @@ -378,13 +396,57 @@ def _normalize(self, tagged_form): return form, tag elif isinstance(tagged_form, tuple): if len(tagged_form) == 2: return tagged_form + if len(tagged_form) == 3: return tagged_form - raise ValueError("Morpheme should has a `str` or `Tuple[str, str]` type.") - - def _update_self(self): - if self._updated: return - super()._update(self.set) - self._updated = True + raise ValueError("Morpheme should has a `str`, `Tuple[str, str]` or `Tuple[str, str, int]` type.") + +@dataclass +class KiwiConfig: + '''.. versionadded:: 0.22.0 + Kiwi의 형태소 분석과 관련된 설정값을 담는 데이터 클래스입니다. + ''' + + integrate_allomorph: bool = True + ''' +True일 경우 음운론적 이형태를 통합하여 출력합니다. /아/와 /어/나 /았/과 /었/ 같이 앞 모음의 양성/음성에 따라 형태가 바뀌는 어미들을 하나로 통합하여 출력합니다. + ''' + + cutoff_threshold: float = 8.0 + ''' +Beam 탐색 시 미리 제거할 후보의 점수 차를 설정합니다. 이 값이 클 수록 더 많은 후보를 탐색하게 되므로 분석 속도가 느려지지만 정확도가 올라갑니다. +반대로 이 값을 낮추면 더 적은 후보를 탐색하여 속도가 빨라지지만 정확도는 낮아집니다. 초기값은 5입니다. + ''' + + unk_form_score_scale: float = 5.0 + ''' + + ''' + + unk_form_score_bias: float = 5.0 + ''' + ''' + + space_penalty: float = 7.0 + ''' +형태소 중간에 삽입된 공백 문자가 있을 경우 언어모델 점수에 추가하는 페널티 점수입니다. 기본값은 7.0입니다. + ''' + + typo_cost_weight: float = 6.0 + ''' +오타 교정 시에 사용할 교정 가중치. 이 값이 클수록 교정을 보수적으로 수행합니다. 기본값은 6입니다. + ''' + + max_unk_form_size: int = 6 + ''' +분석 과정에서 허용할 미등재 형태의 최대 길이입니다. 기본값은 6입니다. + ''' + + space_tolerance: int = 0 + ''' +형태소 중간에 삽입된 공백문자를 몇 개까지 허용할지 설정합니다. 기본값은 0이며, 이 경우 형태소 중간에 공백문자가 삽입되는 걸 허용하지 않습니다. + +`Kiwi.space` 메소드 참고. + ''' class Kiwi(_Kiwi): '''Kiwi 클래스는 실제 형태소 분석을 수행하는 kiwipiepy 모듈의 핵심 클래스입니다. @@ -462,6 +524,7 @@ def __init__(self, model_type: Optional[str] = None, typos: Optional[Union[str, TypoTransformer]] = None, typo_cost_threshold: float = 2.5, + enabled_dialects: Optional[Union[Dialect, str]] = Dialect.STANDARD, ) -> None: if num_workers == 0: warnings.warn("behavior of `num_workers=0` is changed since v0.21.0. If you want to keep the previous behavior, please set `num_workers=-1`.", DeprecationWarning, 2) @@ -499,6 +562,8 @@ def __init__(self, else: raise ValueError("`typos` should be one of ('basic', 'continual', 'basic_with_continual', 'lengthening', 'basic_with_continual_and_lengthening', TypoTransformer), but {}".format(typos)) + enabled_dialects = _convert_dialect(enabled_dialects) + super().__init__( num_workers, model_path, @@ -509,20 +574,16 @@ def __init__(self, model_type, rtypos, typo_cost_threshold, + enabled_dialects, ) - self._ns_integrate_allomorph = integrate_allomorph - self._ns_cutoff_threshold = 8. - self._ns_unk_form_score_scale = 3. - self._ns_unk_form_score_bias = 5. - self._ns_space_penalty = 7. - self._ns_max_unk_form_size = 6 - self._ns_space_tolerance = 0 - self._ns_typo_cost_weight = 6. + self._global_config = KiwiConfig(integrate_allomorph=integrate_allomorph) + self._model_path = model_path self._load_default_dict = load_default_dict self._load_typo_dict = load_typo_dict self._typos = typos + self._enabled_dialects = enabled_dialects self._pretokenized_pats : List[Tuple['re.Pattern', str, Any]] = [] self._user_values : Dict[int, Any] = {} self._template_cache : Dict[str, Template] = {} @@ -536,7 +597,8 @@ def __repr__(self): f"load_typo_dict={self._load_typo_dict!r}, " f"model_type={self.model_type!r}, " f"typos={self._typos!r}, " - f"typo_cost_threshold={self.typo_cost_threshold!r}" + f"typo_cost_threshold={self.typo_cost_threshold!r}, " + f"enabled_dialects={Dialect(self._enabled_dialects)!r}" f")" ) @@ -601,6 +663,7 @@ def add_pre_analyzed_word(self, form:str, analyzed:Iterable[Union[Tuple[str, POSTag], Tuple[str, POSTag, int, int]]], score:float = 0., + dialect:Union[Dialect, str] = Dialect.STANDARD, ) -> bool: '''.. versionadded:: 0.11.0 @@ -647,7 +710,9 @@ def add_pre_analyzed_word(self, cursor = p if len(new_analyzed) == len(analyzed): analyzed = new_analyzed - return super().add_pre_analyzed_word(form, analyzed, score) + + dialect = _convert_dialect(dialect) + return super().add_pre_analyzed_word(form, analyzed, score, dialect) def add_re_word(self, pattern:Union[str, 're.Pattern'], @@ -1018,7 +1083,10 @@ def analyze(self, saisiot:Optional[bool] = None, blocklist:Optional[Union[MorphemeSet, Iterable[str]]] = None, open_ending:bool = False, + allowed_dialects:Union[Dialect, str] = Dialect.STANDARD, + dialect_cost:float = 3., pretokenized:Optional[Union[Callable[[str], PretokenizedTokenList], PretokenizedTokenList]] = None, + override_config:Optional[KiwiConfig] = None, ) -> List[Tuple[List[Token], float]]: '''형태소 분석을 실시합니다. @@ -1105,113 +1173,122 @@ def analyze(self, blocklist = MorphemeSet(self, blocklist.set) elif blocklist is not None: blocklist = MorphemeSet(self, blocklist) - - if blocklist: blocklist._update_self() + allowed_dialects = _convert_dialect(allowed_dialects) + if not isinstance(text, str) and pretokenized and not callable(pretokenized): raise ValueError("`pretokenized` must be a callable if `text` is an iterable of str.") pretokenized = partial(self._make_pretokenized_spans, pretokenized) if self._pretokenized_pats or pretokenized else None - return super().analyze(text, top_n, match_options, False, blocklist, open_ending, pretokenized) + if override_config is None: + override_config = self.global_config + + return super().analyze(text, top_n, match_options, False, blocklist, open_ending, allowed_dialects, dialect_cost, pretokenized, override_config) def morpheme(self, idx:int, ): return super().morpheme(idx) - - def _on_build(self): - self._integrate_allomorph = self._ns_integrate_allomorph - self._cutoff_threshold = self._ns_cutoff_threshold - self._unk_form_score_scale = self._ns_unk_form_score_scale - self._unk_form_score_bias = self._ns_unk_form_score_bias - self._space_penalty = self._ns_space_penalty - self._max_unk_form_size = self._ns_max_unk_form_size - self._space_tolerance = self._ns_space_tolerance - self._typo_cost_weight = self._ns_typo_cost_weight + + @property + def global_config(self): + '''.. versionadded:: 0.22.0 + ''' + return self._global_config @property def cutoff_threshold(self): '''.. versionadded:: 0.10.0 -Beam 탐색 시 미리 제거할 후보의 점수 차를 설정합니다. 이 값이 클 수록 더 많은 후보를 탐색하게 되므로 분석 속도가 느려지지만 정확도가 올라갑니다. -반대로 이 값을 낮추면 더 적은 후보를 탐색하여 속도가 빨라지지만 정확도는 낮아집니다. 초기값은 5입니다. +.. deprecated:: 0.22.0 + 이 속성은 0.22.0 버전에서 deprecated 되었습니다. 대신 `Kiwi.global_config.cutoff_threshold`를 사용해주세요. ''' - - return self._ns_cutoff_threshold + warnings.warn("`Kiwi.cutoff_threshold` is deprecated since 0.22.0. Please use `Kiwi.global_config.cutoff_threshold` instead.", DeprecationWarning, stacklevel=2) + return self.global_config.cutoff_threshold @cutoff_threshold.setter def cutoff_threshold(self, v:float): - self._cutoff_threshold = self._ns_cutoff_threshold = float(v) + warnings.warn("`Kiwi.cutoff_threshold` is deprecated since 0.22.0. Please use `Kiwi.global_config.cutoff_threshold` instead.", DeprecationWarning, stacklevel=2) + self.global_config.cutoff_threshold = v + @property def integrate_allomorph(self): '''.. versionadded:: 0.10.0 -True일 경우 음운론적 이형태를 통합하여 출력합니다. /아/와 /어/나 /았/과 /었/ 같이 앞 모음의 양성/음성에 따라 형태가 바뀌는 어미들을 하나로 통합하여 출력합니다. +.. deprecated:: 0.22.0 + 이 속성은 0.22.0 버전에서 deprecated 되었습니다. 대신 `Kiwi.global_config.integrate_allomorph`를 사용해주세요. ''' - return self._ns_integrate_allomorph + return self.global_config.integrate_allomorph @integrate_allomorph.setter def integrate_allomorph(self, v:bool): - self._integrate_allomorph = self._ns_integrate_allomorph = bool(v) + warnings.warn("`Kiwi.integrate_allomorph` is deprecated since 0.22.0. Please use `Kiwi.global_config.integrate_allomorph` instead.", DeprecationWarning, stacklevel=2) + self.global_config.integrate_allomorph = v @property def space_penalty(self): '''.. versionadded:: 0.11.1 -형태소 중간에 삽입된 공백 문자가 있을 경우 언어모델 점수에 추가하는 페널티 점수입니다. 기본값은 7.0입니다. +.. deprecated:: 0.22.0 + 이 속성은 0.22.0 버전에서 deprecated 되었습니다. 대신 `Kiwi.global_config.space_penalty`를 사용해주세요. ''' - return self._ns_space_penalty + return self.global_config.space_penalty @space_penalty.setter def space_penalty(self, v:float): - self._space_penalty = self._ns_space_penalty = float(v) + warnings.warn("`Kiwi.space_penalty` is deprecated since 0.22.0. Please use `Kiwi.global_config.space_penalty` instead.", DeprecationWarning, stacklevel=2) + self.global_config.space_penalty = v @property def space_tolerance(self): '''.. versionadded:: 0.11.1 -형태소 중간에 삽입된 공백문자를 몇 개까지 허용할지 설정합니다. 기본값은 0이며, 이 경우 형태소 중간에 공백문자가 삽입되는 걸 허용하지 않습니다. - -`Kiwi.space` 메소드 참고. +.. deprecated:: 0.22.0 + 이 속성은 0.22.0 버전에서 deprecated 되었습니다. 대신 `Kiwi.global_config.space_tolerance`를 사용해주세요. ''' - return self._ns_space_tolerance + return self.global_config.space_tolerance @space_tolerance.setter def space_tolerance(self, v:int): + warnings.warn("`Kiwi.space_tolerance` is deprecated since 0.22.0. Please use `Kiwi.global_config.space_tolerance` instead.", DeprecationWarning, stacklevel=2) if v < 0: raise ValueError("`space_tolerance` must be a zero or positive integer.") - self._space_tolerance = self._ns_space_tolerance = int(v) + self.global_config.space_tolerance = int(v) @property def max_unk_form_size(self): '''.. versionadded:: 0.11.1 -분석 과정에서 허용할 미등재 형태의 최대 길이입니다. 기본값은 6입니다. +.. deprecated:: 0.22.0 + 이 속성은 0.22.0 버전에서 deprecated 되었습니다. 대신 `Kiwi.global_config.max_unk_form_size`를 사용해주세요. ''' - return self._ns_max_unk_form_size + return self.global_config.max_unk_form_size @max_unk_form_size.setter def max_unk_form_size(self, v:int): + warnings.warn("`Kiwi.max_unk_form_size` is deprecated since 0.22.0. Please use `Kiwi.global_config.max_unk_form_size` instead.", DeprecationWarning, stacklevel=2) if v < 0: raise ValueError("`max_unk_form_size` must be a zero or positive integer.") - self._max_unk_form_size = self._ns_max_unk_form_size = int(v) + self.global_config.max_unk_form_size = int(v) @property def typo_cost_weight(self): '''.. versionadded:: 0.13.0 -오타 교정 시에 사용할 교정 가중치. 이 값이 클수록 교정을 보수적으로 수행합니다. 기본값은 6입니다. +.. deprecated:: 0.22.0 + 이 속성은 0.22.0 버전에서 deprecated 되었습니다. 대신 `Kiwi.global_config.typo_cost_weight`를 사용해주세요. ''' - return self._ns_typo_cost_weight + return self.global_config.typo_cost_weight @typo_cost_weight.setter def typo_cost_weight(self, v:float): + warnings.warn("`Kiwi.typo_cost_weight` is deprecated since 0.22.0. Please use `Kiwi.global_config.typo_cost_weight` instead.", DeprecationWarning, stacklevel=2) if v < 0: raise ValueError("`typo_cost_weight` must be a zero or positive float.") - self._typo_cost_weight = self._ns_typo_cost_weight = float(v) + self.global_config.typo_cost_weight = float(v) @property def num_workers(self): @@ -1256,7 +1333,10 @@ def _tokenize(self, echo:bool = False, blocklist:Optional[Union[Iterable[str], MorphemeSet]] = None, open_ending:bool = False, + allowed_dialects:Union[Dialect, str] = Dialect.STANDARD, + dialect_cost:float = 3., pretokenized:Optional[Union[Callable[[str], PretokenizedTokenList], PretokenizedTokenList]] = None, + override_config:Optional[KiwiConfig] = None, ): def _refine_result(results): if not split_sents: @@ -1284,25 +1364,28 @@ def _refine_result_with_echo(arg): elif saisiot is False: match_options = (match_options & ~Match.SPLIT_SAISIOT) | Match.MERGE_SAISIOT + allowed_dialects = _convert_dialect(allowed_dialects) + if isinstance(blocklist, MorphemeSet): if blocklist.kiwi != self: warnings.warn("This `MorphemeSet` isn't based on current Kiwi object.") blocklist = MorphemeSet(self, blocklist.set) elif blocklist is not None: blocklist = MorphemeSet(self, blocklist) - - if blocklist: blocklist._update_self() if not isinstance(text, str) and pretokenized and not callable(pretokenized): raise ValueError("`pretokenized` must be a callable if `text` is an iterable of str.") pretokenized = partial(self._make_pretokenized_spans, pretokenized) if self._pretokenized_pats or pretokenized else None + if override_config is None: + override_config = self.global_config + if isinstance(text, str): echo = False - return _refine_result(super().analyze(text, 1, match_options, False, blocklist, open_ending, pretokenized)) - - return map(_refine_result_with_echo if echo else _refine_result, super().analyze(text, 1, match_options, echo, blocklist, open_ending, pretokenized)) + return _refine_result(super().analyze(text, 1, match_options, False, blocklist, open_ending, allowed_dialects, dialect_cost, pretokenized, override_config)) + + return map(_refine_result_with_echo if echo else _refine_result, super().analyze(text, 1, match_options, echo, blocklist, open_ending, allowed_dialects, dialect_cost, pretokenized, override_config)) def tokenize(self, text:Union[str, Iterable[str]], @@ -1317,7 +1400,10 @@ def tokenize(self, echo:bool = False, blocklist:Optional[Union[Iterable[str], MorphemeSet]] = None, open_ending:bool = False, + allowed_dialects:Union[Dialect, str] = Dialect.STANDARD, + dialect_cost:float = 3., pretokenized:Optional[Union[Callable[[str], PretokenizedTokenList], PretokenizedTokenList]] = None, + override_config:Optional[KiwiConfig] = None, ) -> Union[List[Token], Iterable[List[Token]], List[List[Token]], Iterable[List[List[Token]]]]: '''.. versionadded:: 0.10.2 @@ -1522,8 +1608,11 @@ def tokenize(self, split_sents, stopwords, echo, blocklist=blocklist, open_ending=open_ending, - pretokenized=pretokenized - ) + allowed_dialects=allowed_dialects, + dialect_cost=dialect_cost, + pretokenized=pretokenized, + override_config=override_config, + ) def split_into_sents(self, text:Union[str, Iterable[str]], @@ -1535,6 +1624,9 @@ def split_into_sents(self, saisiot:Optional[bool] = None, stopwords:Optional[Stopwords] = None, blocklist:Optional[Union[Iterable[str], MorphemeSet]] = None, + allowed_dialects:Union[Dialect, str] = Dialect.STANDARD, + dialect_cost:float = 3., + override_config:Optional[KiwiConfig] = None, return_tokens:bool = False, return_sub_sents:bool = True, ) -> Union[List[Sentence], Iterable[List[Sentence]]]: @@ -1669,6 +1761,9 @@ def _make_result(arg): compatible_jamo=compatible_jamo, saisiot=saisiot, blocklist=blocklist, + allowed_dialects=allowed_dialects, + dialect_cost=dialect_cost, + override_config=override_config, split_sents=True), text)) return map(_make_result, self._tokenize(text, @@ -1679,6 +1774,9 @@ def _make_result(arg): compatible_jamo=compatible_jamo, saisiot=saisiot, blocklist=blocklist, + allowed_dialects=allowed_dialects, + dialect_cost=dialect_cost, + override_config=override_config, split_sents=True, echo=True)) @@ -1752,7 +1850,7 @@ def _repeat_false(): while 1: yield False - riter = super().analyze(_zip_consequences(iter(text_chunks)), 1, Match.ALL, False, None, False, None) + riter = super().analyze(_zip_consequences(iter(text_chunks)), 1, Match.ALL, False, None, False, 0, 0., None, self.global_config) if insert_new_lines is None: insert_new_lines = _repeat_false() @@ -1877,10 +1975,10 @@ def _space(arg): if isinstance(text, str): if reset_whitespace: text = _reset(text) - return _space((super().analyze(text, 1, Match.ALL | Match.Z_CODA, False, None, False, None), text)) + return _space((super().analyze(text, 1, Match.ALL | Match.Z_CODA, False, None, False, 0, 0., None, self.global_config), text)) else: if reset_whitespace: text = map(_reset, text) - return map(_space, super().analyze(text, 1, Match.ALL | Match.Z_CODA, True, None, False, None)) + return map(_space, super().analyze(text, 1, Match.ALL | Match.Z_CODA, True, None, False, 0, 0., None, self.global_config)) def join(self, morphs:Iterable[Tuple[str, str]], @@ -2098,7 +2196,7 @@ def _convert_input_to_token_list(self, inp, name): def most_similar_morphemes( self, - target:Union[str, Tuple[str, POSTag], Token, int], + target:Union[str, Tuple[str, POSTag], Tuple[str, POSTag, int], Token, int], top_n:int = 10, ) -> List[SimilarMorpheme]: '''..versionadded:: 0.21.0 @@ -2108,7 +2206,7 @@ def most_similar_morphemes( Parameters ---------- -target: Union[str, Tuple[str, POSTag], Token, int] +target: Union[str, Tuple[str, POSTag], Tuple[str, POSTag, int], Token, int] 입력 형태소. 단일 문자열 혹은 (형태, 품사태그)로 구성된 tuple, Token 객체, 혹은 Token 객체의 id를 입력할 수 있습니다. top_n: int 반환할 형태소의 개수입니다. 기본값은 10입니다. @@ -2344,8 +2442,8 @@ def predict_next_morpheme( def morpheme_similarity( self, - morpheme1:Union[str, Tuple[str, POSTag], Token, int], - morpheme2:Union[str, Tuple[str, POSTag], Token, int] + morpheme1:Union[str, Tuple[str, POSTag], Tuple[str, POSTag, int], Token, int], + morpheme2:Union[str, Tuple[str, POSTag], Tuple[str, POSTag, int], Token, int] ) -> float: '''..versionadded:: 0.21.0 @@ -2354,9 +2452,9 @@ def morpheme_similarity( Parameters ---------- -morpheme1: Union[str, Tuple[str, POSTag], Token, int] +morpheme1: Union[str, Tuple[str, POSTag], Tuple[str, POSTag, int], Token, int] 첫번째 입력 형태소. 단일 문자열 혹은 (형태, 품사태그)로 구성된 tuple, Token 객체, 혹은 Token 객체의 id를 입력할 수 있습니다. -morpheme2: Union[str, Tuple[str, POSTag], Token, int] +morpheme2: Union[str, Tuple[str, POSTag], Tuple[str, POSTag, int], Token, int] 두번째 입력 형태소. 타입은 morpheme1과 동일합니다. Returns @@ -2465,6 +2563,8 @@ def make_hsdataset( dropout:float = 0, dropout_on_history:float = 0, noun_augmenting_prob:float = 0, + emoji_augmenting_prob:float = 0, + sb_augmenting_prob:float = 0, token_filter:Callable[[str, str], bool] = None, window_filter:Callable[[str, str], bool] = None, split_ratio:float = 0, @@ -2485,6 +2585,8 @@ def make_hsdataset( dropout, dropout_on_history, noun_augmenting_prob, + emoji_augmenting_prob, + sb_augmenting_prob, generate_unlikelihoods, token_filter, window_filter, diff --git a/kiwipiepy/const.py b/kiwipiepy/const.py index 60fbd9b..66a6bb6 100644 --- a/kiwipiepy/const.py +++ b/kiwipiepy/const.py @@ -1,10 +1,9 @@ ''' const 모듈은 kiwipiepy에서 사용되는 주요 상수값들을 모아놓은 모듈입니다. ''' +from enum import IntFlag -from enum import IntEnum - -class Match(IntEnum): +class Match(IntFlag): """ .. versionadded:: 0.8.0 @@ -131,3 +130,57 @@ class Match(IntEnum): .. versionadded:: 0.11.0 """ + +class Dialect(IntFlag): + """ + .. versionadded:: 0.22.0 + + 방언 정보를 나타내는 열거형입니다. + """ + + STANDARD = 0 + """ 표준어 """ + 표준 = STANDARD + + GYEONGGI = 1 << 0 + """ 경기 방언 """ + 경기 = GYEONGGI + + CHUNGCHEONG = 1 << 1 + """ 충청 방언 """ + 충청 = CHUNGCHEONG + + GANGWON = 1 << 2 + """ 강원 방언 """ + 강원 = GANGWON + + GYEONGSANG = 1 << 3 + """ 경상 방언 """ + 경상 = GYEONGSANG + + JEOLLA = 1 << 4 + """ 전라 방언 """ + 전라 = JEOLLA + + JEJU = 1 << 5 + """ 제주 방언 """ + 제주 = JEJU + + HWANGHAE = 1 << 6 + """ 황해 방언 """ + 황해 = HWANGHAE + + HAMGYEONG = 1 << 7 + """ 함경 방언 """ + 함경 = HAMGYEONG + + PYEONGAN = 1 << 8 + """ 평안 방언 """ + 평안 = PYEONGAN + + ARCHAIC = 1 << 9 + """ 옛말 """ + 옛말 = ARCHAIC + + ALL = (GYEONGGI | CHUNGCHEONG | GANGWON | GYEONGSANG | JEOLLA | JEJU | HWANGHAE | HAMGYEONG | PYEONGAN | ARCHAIC) + """ 모든 방언 """ diff --git a/kiwipiepy/template.py b/kiwipiepy/template.py index 65acc51..da6b74c 100644 --- a/kiwipiepy/template.py +++ b/kiwipiepy/template.py @@ -38,7 +38,7 @@ def __init__(self, kiwi: 'Kiwi', format_str: str, ): - from kiwipiepy._wrap import _convert_consonant + from kiwipiepy._wrap import _convert_consonant, PretokenizedToken self._kiwi = kiwi self._format_str = format_str self._formatter = string.Formatter() @@ -55,7 +55,7 @@ def __init__(self, offset += len(literal) if field is not None: chunks.append('{}') - pretokenized_lists.append((offset, offset + 2, 'SSC')) + pretokenized_lists.append((offset, offset + 2, [PretokenizedToken('(', 'SSO', 0, 1), PretokenizedToken(')', 'SSC', 1, 2)])) offset += 2 if field.isdigit(): has_explicit_field_index = True @@ -66,17 +66,25 @@ def __init__(self, raise ValueError('cannot switch from automatic field numbering to manual field specification') field = str(implicit_field_index) implicit_field_index += 1 - self._parsed_format.append(([], field, format, conversion)) + + if self._parsed_format and self._parsed_format[-1][1] is None: + self._parsed_format[-1] = ([], field, format, conversion) + else: + self._parsed_format.append(([], field, format, conversion)) tokens = kiwi.tokenize(''.join(chunks), pretokenized=pretokenized_lists) placeholder_iter = iter(pretokenized_lists) next_placeholder = next(placeholder_iter, None) parsed_iter = iter(self._parsed_format) target_tokens = next(parsed_iter)[0] + opened = False for token in tokens: - if next_placeholder and token.span == next_placeholder[:2]: + if opened: target_tokens = next(parsed_iter)[0] next_placeholder = next(placeholder_iter, None) + opened = False + elif next_placeholder and token.start == next_placeholder[0] and token.end == next_placeholder[0] + 1: + opened = True else: target_tokens.append(token) diff --git a/model/kiwipiepy_model/_version.py b/model/kiwipiepy_model/_version.py index 4c3a627..23630f3 100644 --- a/model/kiwipiepy_model/_version.py +++ b/model/kiwipiepy_model/_version.py @@ -1 +1 @@ -__version__ = '0.21.0' \ No newline at end of file +__version__ = '0.22.0' \ No newline at end of file diff --git a/setup.py b/setup.py index e9c8b82..05e46a5 100644 --- a/setup.py +++ b/setup.py @@ -5,6 +5,7 @@ import shutil import subprocess import re +import sysconfig from distutils import log from setuptools.command.install import install @@ -28,6 +29,8 @@ def get_extra_cmake_options(): _cmake_extra_options.append("-DKIWI_CPU_ARCH=" + os.environ['KIWI_CPU_ARCH']) if os.environ.get('MACOSX_DEPLOYMENT_TARGET'): _cmake_extra_options.append("-DCMAKE_OSX_DEPLOYMENT_TARGET=" + os.environ['MACOSX_DEPLOYMENT_TARGET']) + if sysconfig.get_config_var('Py_GIL_DISABLED'): + _cmake_extra_options.append("-DPy_GIL_DISABLED=1") _clean_build_folder = False print(_cmake_extra_options) @@ -112,6 +115,10 @@ def run(self): def build_extension(self, ext): extdir = os.path.abspath(os.path.dirname(self.get_ext_fullpath(ext.name))) libs = self.get_libraries(ext) + if sysconfig.get_config_var('Py_GIL_DISABLED'): + for i, lib in enumerate(libs): + if re.fullmatch(r'python3[0-9]+', lib): + libs[i] = lib + 't' cmake_args = [ '-DINCLUDE_DIRS={}'.format(';'.join(self.include_dirs + [np.get_include()])), @@ -209,7 +216,7 @@ def build_extension(self, ext): keywords='Korean morphological analysis', install_requires=[ 'dataclasses; python_version < "3.7"', - 'kiwipiepy_model>=0.21,<0.22', + 'kiwipiepy_model>=0.22,<0.23', 'numpy<2; python_version < "3.9"', 'numpy; python_version >= "3.9"', 'tqdm', diff --git a/src/KiwiPy.cpp b/src/KiwiPy.cpp index 67e41d7..24e7b25 100644 --- a/src/KiwiPy.cpp +++ b/src/KiwiPy.cpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include #define USE_NUMPY #define MAIN_MODULE @@ -126,7 +128,7 @@ struct TypoTransformerObject : py::CObject py::UniqueObj getDefs() const { py::UniqueObj ret{ PyList_New(0) }; - vector, float>> defs{ tt.getTypos().begin(), tt.getTypos().end() }; + vector, float>> defs{ tt.getTypos().begin(), tt.getTypos().end() }; sort(defs.begin(), defs.end()); for (auto& p : defs) { @@ -154,8 +156,17 @@ struct TypoTransformerObject : py::CObject PreparedTypoTransformer& getPtt() { - if (!prepared) ptt = tt.prepare(); - prepared = true; + if (!prepared) + { +#ifdef Py_GIL_DISABLED + Py_BEGIN_CRITICAL_SECTION(this); +#endif + ptt = tt.prepare(); + prepared = true; +#ifdef Py_GIL_DISABLED + Py_END_CRITICAL_SECTION(); +#endif + } return ptt; } @@ -393,7 +404,7 @@ struct HSDatasetIterObject : py::CObject } else { - return py::buildPyTuple(inData, outData, lmLProbsData, outNgramNodeData, restLm, restLmCnt); + return py::buildPyTuple(inData, outData, lmLProbsData, outNgramNodeData, restLm, restLmCnt); } } }; @@ -856,6 +867,28 @@ struct ContextSpan ContextSpan(const uint32_t* _data = nullptr, size_t _size = 0) : data(_data), size(_size) {} }; +template +void setValueFromAttr(Ty& val, PyObject* obj, const char* attr) +{ + py::UniqueObj ret{ PyObject_GetAttrString(obj, attr) }; + if (!ret) throw py::ExcPropagation{}; + py::toCpp(ret.get(), val); +} + +KiwiConfig toKiwiConfig(PyObject* obj) +{ + KiwiConfig c; + setValueFromAttr(c.integrateAllomorph, obj, "integrate_allomorph"); + setValueFromAttr(c.cutOffThreshold, obj, "cutoff_threshold"); + setValueFromAttr(c.unkFormScoreScale, obj, "unk_form_score_scale"); + setValueFromAttr(c.unkFormScoreBias, obj, "unk_form_score_bias"); + setValueFromAttr(c.spacePenalty, obj, "space_penalty"); + setValueFromAttr(c.typoCostWeight, obj, "typo_cost_weight"); + setValueFromAttr(c.maxUnkFormSize, obj, "max_unk_form_size"); + setValueFromAttr(c.spaceTolerance, obj, "space_tolerance"); + return c; +} + struct KiwiObject : py::CObject { static constexpr const char* _name = "kiwipiepy._Kiwi"; @@ -863,12 +896,16 @@ struct KiwiObject : py::CObject static constexpr int _flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE; KiwiBuilder builder; - Kiwi kiwi; + mutable std::shared_ptr kiwi; TypoTransformerObject* typos = nullptr; float typoCostThreshold = 2.5f; Vector> contextForms; Vector, Vector>> contextAnalyses; +#ifdef Py_GIL_DISABLED + std::unique_ptr rwMutex; +#endif + using _InitArgs = std::tuple< size_t, std::optional, @@ -878,7 +915,8 @@ struct KiwiObject : py::CObject bool, std::string, PyObject*, - float + float, + Dialect >; KiwiObject() = default; @@ -891,7 +929,8 @@ struct KiwiObject : py::CObject bool loadMultiDict = true, const std::string& modelType = {}, PyObject* _typos = nullptr, - float _typoCostThreshold = 2.5f + float _typoCostThreshold = 2.5f, + Dialect enabledDialects = Dialect::standard ) { if (_typos == nullptr || _typos == Py_None) @@ -960,26 +999,24 @@ struct KiwiObject : py::CObject throw py::ValueError{ "invalid model type: " + modelType }; } - builder = KiwiBuilder{ spath, numThreads, (BuildOption)boptions, mtype }; + builder = KiwiBuilder{ spath, numThreads, (BuildOption)boptions, mtype, enabledDialects }; +#ifdef Py_GIL_DISABLED + rwMutex = std::make_unique(); +#endif } - void doPrepare() + std::shared_ptr doPrepare() const { - if (kiwi.ready()) return; - kiwi = builder.build(typos ? typos->tt : getDefaultTypoSet(DefaultTypoSet::withoutTypo), typoCostThreshold); - py::UniqueObj handler{ PyObject_GetAttrString((PyObject*)this, "_on_build") }; - if (handler) - { - py::UniqueObj res{ PyObject_CallFunctionObjArgs(handler.get(), nullptr)}; - if (!res) throw py::ExcPropagation{}; - } - else - { - PyErr_Clear(); - } + if (auto k = kiwi) return k; +#ifdef Py_GIL_DISABLED + std::unique_lock lock{ *rwMutex }; + if (kiwi) return kiwi; +#endif + kiwi = std::make_shared(builder.build(typos ? typos->tt : getDefaultTypoSet(DefaultTypoSet::withoutTypo), typoCostThreshold)); + return kiwi; } - void convertContextToReadableForm(const vector& context, vector& forms, pair, Vector>& analyses) const + void convertContextToReadableForm(Kiwi* kiwi, const vector& context, vector& forms, pair, Vector>& analyses) const { Vector spans; const uint32_t delimiter = -1; @@ -1011,7 +1048,7 @@ struct KiwiObject : py::CObject for (auto& span : spans) { - auto joiner = kiwi.newJoiner(false); + auto joiner = kiwi->newJoiner(false); for (size_t i = 0; i < span.size; ++i) { joiner.add(span.data[i]); @@ -1022,25 +1059,37 @@ struct KiwiObject : py::CObject } } - void prepareContextMap(const lm::CoNgramModelBase* cong) + void prepareContextMap(Kiwi* kiwi, const lm::CoNgramModelBase* cong) { if (!contextForms.empty()) return; +#ifdef Py_GIL_DISABLED + std::unique_lock lock{ *rwMutex }; + if (!contextForms.empty()) return; +#endif auto contextMap = cong->getContextWordMap(); for (size_t i = 0; i < contextMap.size(); ++i) { vector forms; pair, Vector> analyses; - convertContextToReadableForm(contextMap[i], forms, analyses); + convertContextToReadableForm(kiwi, contextMap[i], forms, analyses); contextForms.emplace_back(std::move(forms)); contextAnalyses.emplace_back(std::move(analyses)); } } std::pair addUserWord(const char* word, const char* tag = "NNP", float score = 0, std::optional origWord = {}); - bool addPreAnalyzedWord(const char* form, PyObject* oAnalyzed = nullptr, float score = 0); + bool addPreAnalyzedWord(const char* form, PyObject* oAnalyzed = nullptr, float score = 0, Dialect dialect = Dialect::standard); std::vector> addRule(const char* tag, PyObject* replacer, float score = 0); - py::UniqueObj analyze(PyObject* text, size_t topN = 1, Match matchOptions = Match::all, bool echo = false, PyObject* blockList = Py_None, bool openEnding = false, PyObject* pretokenized = Py_None); + py::UniqueObj analyze(PyObject* text, size_t topN = 1, + Match matchOptions = Match::all, + bool echo = false, + PyObject* blockList = Py_None, + bool openEnding = false, + Dialect allowedDialects = Dialect::standard, + float dialectCost = 3.f, + PyObject* pretokenized = Py_None, + PyObject* config = Py_None); py::UniqueObj extractAddWords(PyObject* sentences, size_t minCnt = 10, size_t maxWordLen = 10, float minScore = 0.25f, float posScore = -3, bool lmFilter = true); py::UniqueObj extractWords(PyObject* sentences, size_t minCnt, size_t maxWordLen = 10, float minScore = 0.25f, float posScore = -3, bool lmFilter = true) const; size_t loadUserDictionary(const char* path); @@ -1068,6 +1117,8 @@ struct KiwiObject : py::CObject float dropout = 0, float dropoutOnHistory = 0, float nounAugmentingProb = 0, + float emojiAugmentingProb = 0, + float sbAugmentingProb = 0, size_t generateUnlikelihoods = -1, PyObject* tokenFilter = nullptr, PyObject* windowFilter = nullptr, @@ -1081,95 +1132,49 @@ struct KiwiObject : py::CObject py::UniqueObj listAllScripts() const; - float getCutOffThreshold() const - { - return kiwi.getCutOffThreshold(); - } - - void setCutOffThreshold(float v) - { - kiwi.setCutOffThreshold(v); - } - - size_t getMaxUnkFormSize() const - { - return kiwi.getMaxUnkFormSize(); - } - - void setMaxUnkFormSize(size_t v) - { - kiwi.setMaxUnkFormSize(v); - } - - float getUnkScoreBias() const - { - return kiwi.getUnkScoreBias(); - } - - void setUnkScoreBias(float v) - { - kiwi.setUnkScoreBias(v); - } - - float getUnkScoreScale() const - { - return kiwi.getUnkScoreScale(); - } - - void setUnkScoreScale(float v) - { - kiwi.setUnkScoreScale(v); - } - - bool getIntegrateAllomorph() const - { - return kiwi.getIntegrateAllomorph(); - } - - void setIntegrateAllomorph(bool v) - { - kiwi.setIntegrateAllomorph(v); - } - - size_t getSpaceTolerance() const - { - return kiwi.getSpaceTolerance(); - } - - void setSpaceTolerance(size_t v) - { - kiwi.setSpaceTolerance(v); - } - - float getSpacePenalty() const - { - return kiwi.getSpacePenalty(); - } - - void setSpacePenalty(float v) - { - kiwi.setSpacePenalty(v); - } + py::UniqueObj getGlobalConfig() const + { + auto kiwiInst = doPrepare(); + auto config = kiwiInst->getGlobalConfig(); + static const char* keys[] = { + "integrate_allomorph", + "cutoff_threshold", + "unk_form_score_scale", + "unk_form_score_bias", + "space_penalty", + "typo_cost_weight", + "max_unk_form_size", + "space_tolerance", + }; - float getTypoCostWeight() const - { - return kiwi.getTypoCostWeight(); + return py::buildPyDict( + keys, + config.integrateAllomorph, + config.cutOffThreshold, + config.unkFormScoreScale, + config.unkFormScoreBias, + config.spacePenalty, + config.typoCostWeight, + config.maxUnkFormSize, + config.spaceTolerance + ); } - void setTypoCostWeight(float v) + void setGlobalConfig(PyObject* config) { - kiwi.setTypoCostWeight(v); + auto kiwiInst = doPrepare(); + kiwiInst->setGlobalConfig(toKiwiConfig(config)); } size_t getNumWorkers() const { - return kiwi.getNumThreads(); + auto kiwiInst = doPrepare(); + return kiwiInst->getNumThreads(); } const char* getModelType() { - doPrepare(); - return modelTypeToStr(kiwi.getLangModel()->getType()); + return modelTypeToStr(builder.getModelType()); } }; @@ -1198,14 +1203,7 @@ py::TypeWrapper _KiwiSetter{ gModule, [](PyTypeObject& obj) }; static PyGetSetDef getsets[] = { - { (char*)"_cutoff_threshold", PY_GETTER(&KiwiObject::getCutOffThreshold), PY_SETTER(&KiwiObject::setCutOffThreshold), "", nullptr }, - { (char*)"_integrate_allomorph", PY_GETTER(&KiwiObject::getIntegrateAllomorph), PY_SETTER(&KiwiObject::setIntegrateAllomorph), "", nullptr }, - { (char*)"_unk_score_bias", PY_GETTER(&KiwiObject::getUnkScoreBias), PY_SETTER(&KiwiObject::setUnkScoreBias), "", nullptr }, - { (char*)"_unk_score_scale", PY_GETTER(&KiwiObject::getUnkScoreScale), PY_SETTER(&KiwiObject::setUnkScoreScale), "", nullptr }, - { (char*)"_max_unk_form_size", PY_GETTER(&KiwiObject::getMaxUnkFormSize), PY_SETTER(&KiwiObject::setMaxUnkFormSize), "", nullptr }, - { (char*)"_space_tolerance", PY_GETTER(&KiwiObject::getSpaceTolerance), PY_SETTER(&KiwiObject::setSpaceTolerance), "", nullptr }, - { (char*)"_space_penalty", PY_GETTER(&KiwiObject::getSpacePenalty), PY_SETTER(&KiwiObject::setSpacePenalty), "", nullptr }, - { (char*)"_typo_cost_weight", PY_GETTER(&KiwiObject::getTypoCostWeight), PY_SETTER(&KiwiObject::setTypoCostWeight), "", nullptr }, + { (char*)"__global_config", PY_GETTER(&KiwiObject::getGlobalConfig), PY_SETTER(&KiwiObject::setGlobalConfig), "", nullptr }, { (char*)"_typo_cost_threshold", PY_GETTER(&KiwiObject::typoCostThreshold), PY_SETTER(&KiwiObject::typoCostThreshold), "", nullptr }, { (char*)"_num_workers", PY_GETTER(&KiwiObject::getNumWorkers), nullptr, "", nullptr }, { (char*)"_model_type", PY_GETTER(&KiwiObject::getModelType), nullptr, "", nullptr }, @@ -1220,6 +1218,7 @@ struct TokenObject : py::CObject static constexpr const char* _name = "kiwipiepy.Token"; static constexpr const char* _name_in_module = "Token"; + std::weak_ptr kiwiInst; u16string _form, _raw_form; const char* _tag = nullptr; size_t resultHash = 0; @@ -1232,6 +1231,7 @@ struct TokenObject : py::CObject py::UniqueObj _userValue; POSTag _rawTag = POSTag::unknown; ScriptType _script = ScriptType::unknown; + uint16_t _dialect = 0; bool _regularity = false; using _InitArgs = std::tuple; @@ -1387,6 +1387,7 @@ py::TypeWrapper _TokenSetter{ gModule, [](PyTypeObject& obj) { (char*)"user_value", PY_GETTER(&TokenObject::_userValue), nullptr, "", nullptr}, { (char*)"script", PY_GETTER(&TokenObject::script), nullptr, "", nullptr}, { (char*)"sense", PY_GETTER(&TokenObject::_sense), nullptr, "", nullptr}, + { (char*)"dialect", PY_GETTER(&TokenObject::_dialect), nullptr, "", nullptr}, { nullptr }, }; @@ -1439,9 +1440,8 @@ inline const char* getTagStr(const POSTag tag, const u16string& form) return tagToString(tag); } -py::UniqueObj resToPyList(vector&& res, const KiwiObject* kiwiObj, vector&& userValues = {}) +py::UniqueObj resToPyList(vector&& res, const KiwiObject* kiwiObj, const shared_ptr& kiwiInst, vector&& userValues = {}) { - auto& kiwi = kiwiObj->kiwi; // set the following objects semi-immortal. (they are neither freed nor managed) // it prevents crashes at Python3.12 static PyObject* userValuesAttr = py::buildPyValue("_user_values").release(); @@ -1455,7 +1455,7 @@ py::UniqueObj resToPyList(vector&& res, const KiwiObject* kiwiObj, py::UniqueObj rList{ PyList_New(p.first.size()) }; size_t jdx = 0; size_t u32offset = 0; - size_t resultHash = hashTokenInfo(p.first); + const size_t resultHash = hashTokenInfo(p.first); for (auto& q : p.first) { size_t u32chrs = 0; @@ -1465,6 +1465,7 @@ py::UniqueObj resToPyList(vector&& res, const KiwiObject* kiwiObj, } auto tItem = py::makeNewObject(); + tItem->kiwiInst = kiwiInst; tItem->_form = move(q.str); tItem->_regularity = !isIrregular(q.tag); tItem->_rawTag = q.tag; @@ -1479,10 +1480,12 @@ py::UniqueObj resToPyList(vector&& res, const KiwiObject* kiwiObj, tItem->_score = q.score; tItem->_typoCost = q.typoCost; tItem->_morph = q.morph; - tItem->_morphId = q.morph ? kiwi.morphToId(q.morph) : -1; - tItem->_baseMorph = q.morph ? (q.morph->origMorphemeId ? kiwi.idToMorph(q.morph->origMorphemeId) : q.morph) : nullptr; - tItem->_raw_form = q.typoCost ? kiwi.getTypoForm(q.typoFormId) : tItem->_form; + tItem->_morphId = q.morph ? kiwiInst->morphToId(q.morph) : -1; + tItem->_baseMorph = (q.morph && !!q.dialect) ? kiwiInst->idToMorph(q.morph->lmMorphemeId) : + (q.morph ? (q.morph->origMorphemeId ? kiwiInst->idToMorph(q.morph->origMorphemeId) : q.morph) : nullptr); + tItem->_raw_form = q.typoCost ? kiwiInst->getTypoForm(q.typoFormId) : tItem->_form; tItem->_pairedToken = q.pairedToken; + tItem->_dialect = (uint16_t)q.dialect; if (q.tag == POSTag::sl || q.tag == POSTag::sh || q.tag == POSTag::sw || q.tag == POSTag::w_emoji) { tItem->_script = q.script; @@ -1546,7 +1549,9 @@ struct MorphemeSetObject : py::CObject static constexpr int _flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE; py::UniqueCObj kiwi; - std::unordered_set morphSet; + std::vector> morphList; + mutable std::weak_ptr kiwiPtr; + mutable std::unordered_set morphSet; using _InitArgs = std::tuple>; @@ -1555,32 +1560,61 @@ struct MorphemeSetObject : py::CObject MorphemeSetObject(py::UniqueCObj&& _kiwi) { kiwi = std::move(_kiwi); - kiwi->doPrepare(); } void update(PyObject* morphs) { + morphList.clear(); morphSet.clear(); py::foreach(morphs, [&](PyObject* item) { - if (PyTuple_Check(item) && PyTuple_GET_SIZE(item) == 2) + if (PyTuple_Check(item) && (PyTuple_GET_SIZE(item) == 2 || PyTuple_GET_SIZE(item) == 3)) { auto form = py::toCpp(PyTuple_GET_ITEM(item, 0)); auto stag = py::toCpp(PyTuple_GET_ITEM(item, 1)); + uint8_t senseId = undefSenseId; + if (PyTuple_GET_SIZE(item) == 3) + { + senseId = (uint8_t)py::toCpp(PyTuple_GET_ITEM(item, 2)); + } POSTag tag = POSTag::unknown; if (!stag.empty()) { tag = parseTag(stag.c_str()); } - auto m = kiwi->kiwi.findMorphemes(utf8To16(form), tag); - morphSet.insert(m.begin(), m.end()); + morphList.emplace_back(form, tag, senseId); } else { throw py::ForeachFailed{}; } - }, "`morphs` must be an iterable of `str`."); + }, "`morphs` must be an iterable of `tuple`."); + } + + const std::unordered_set& getMorphemeSet() const + { + auto kiwiInst = kiwiPtr.lock(); + if (!kiwiInst) + { + morphSet.clear(); + kiwiPtr = kiwiInst = kiwi->doPrepare(); + } + if (morphSet.empty()) + { + for (auto& p : morphList) + { + auto form = utf8To16(std::get<0>(p)); + auto tag = std::get<1>(p); + auto senseId = std::get<2>(p); + auto morphs = kiwiInst->findMorphemes(form, tag, senseId); + for (auto m : morphs) + { + morphSet.insert(m); + } + } + } + return morphSet; } }; @@ -1637,6 +1671,7 @@ struct SwTokenizerObject : py::CObject static constexpr int _flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE; py::UniqueCObj kiwi; + std::shared_ptr kiwiInst; kiwi::SwTokenizer tokenizer; using _InitArgs = std::tuple, const char*>; @@ -1646,9 +1681,9 @@ struct SwTokenizerObject : py::CObject SwTokenizerObject(py::UniqueCObj&& _kiwi, const char* path) { kiwi = std::move(_kiwi); - kiwi->doPrepare(); + kiwiInst = kiwi->doPrepare(); std::ifstream ifs; - tokenizer = kiwi::SwTokenizer::load(kiwi->kiwi, openFile(ifs, path)); + tokenizer = kiwi::SwTokenizer::load(*kiwiInst, openFile(ifs, path)); } void save(const char* path) const @@ -1773,8 +1808,8 @@ struct SwTokenizerObject : py::CObject trainCfg.removeRepetitive = removeRepetitive; trainCfg.preventMixedDigitTokens = !!preventMixedDigitTokens; - kiwi->doPrepare(); - UnigramSwTrainer trainer{ kiwi->kiwi, cfg, trainCfg }; + auto kiwiInst = kiwi->doPrepare(); + UnigramSwTrainer trainer{ *kiwiInst, cfg, trainCfg }; py::UniqueObj methodNames[] { py::buildPyValue("begin_tokenization"), py::buildPyValue("proc_tokenization"), @@ -2119,11 +2154,15 @@ struct KiwiResIter : public py::ResultIter, Fut static constexpr const char* _name_in_module = "_ResIter"; py::UniqueCObj kiwi; + std::shared_ptr kiwiInst; py::UniqueCObj blocklist; py::UniqueObj pretokenizedCallable; +#ifdef Py_GIL_DISABLED + std::shared_lock lock; +#endif size_t topN = 1; - Match matchOptions = Match::all; - bool openEnding = false; + AnalyzeOption options; + KiwiConfig config; KiwiResIter() = default; KiwiResIter(KiwiResIter&&) = default; @@ -2139,7 +2178,7 @@ struct KiwiResIter : public py::ResultIter, Fut return py::handleExc([&]() { if (v.first.size() > topN) v.first.erase(v.first.begin() + topN, v.first.end()); - return resToPyList(move(v.first), kiwi.get(), move(v.second)); + return resToPyList(move(v.first), kiwi.get(), kiwiInst, move(v.second)); }); } @@ -2164,9 +2203,10 @@ struct KiwiResIter : public py::ResultIter, Fut updatePretokenizedSpanToU16(pretokenized.first, so); } return makeFutureCarrier( - kiwi->kiwi.asyncAnalyze(move(so.str), topN, - AnalyzeOption{ matchOptions, blocklist ? &blocklist->morphSet : nullptr, openEnding }, - move(pretokenized.first) + kiwiInst->asyncAnalyze(move(so.str), topN, + options, + move(pretokenized.first), + config ), move(pretokenized.second) ); @@ -2186,6 +2226,9 @@ struct SwTokenizerResIter : public py::ResultIter tokenizer; bool returnOffsets = false; +#ifdef Py_GIL_DISABLED + std::shared_lock lock; +#endif SwTokenizerResIter() = default; SwTokenizerResIter(SwTokenizerResIter&&) = default; @@ -2261,19 +2304,19 @@ struct SwTokenizerResTEIter : public py::ResultIter(v)), tokenizer->kiwi.get()), get<1>(v), get<2>(v)); - return py::buildPyTuple(resToPyList(move(get<0>(v)), tokenizer->kiwi.get()), get<1>(v)); + if (returnOffsets) return py::buildPyTuple(resToPyList(move(get<0>(v)), tokenizer->kiwi.get(), tokenizer->kiwiInst), get<1>(v), get<2>(v)); + return py::buildPyTuple(resToPyList(move(get<0>(v)), tokenizer->kiwi.get(), tokenizer->kiwiInst), get<1>(v)); } future feedNext(py::SharedObj&& next) { if (!PyUnicode_Check(next)) throw py::ValueError{ "`tokenize_encode` requires an instance of `str` or an iterable of `str`." }; - auto* pool = tokenizer->kiwi->kiwi.getThreadPool(); + auto* pool = tokenizer->kiwiInst->getThreadPool(); if (!pool) throw py::RuntimeError{ "async mode is unavailable in num_workers == 0" }; return pool->enqueue([&](size_t, const string& text) { vector> offsets; - auto res = tokenizer->kiwi->kiwi.analyze(text, 1, Match::allWithNormalizing | Match::zCoda); + auto res = tokenizer->kiwiInst->analyze(text, 1, Match::allWithNormalizing | Match::zCoda); auto tokenIds = tokenizer->tokenizer.encode(res[0].first.data(), res[0].first.size(), returnOffsets ? &offsets : nullptr); if (returnOffsets) chrOffsetsToTokenOffsets(res[0].first, offsets); return make_tuple(move(res), move(tokenIds), move(offsets)); @@ -2310,7 +2353,7 @@ py::UniqueObj SwTokenizerObject::encode(PyObject* text, bool returnOffsets) cons ret->inputIter = move(iter); ret->returnOffsets = !!returnOffsets; - for (size_t i = 0; i < kiwi->kiwi.getNumThreads() * 16; ++i) + for (size_t i = 0; i < kiwiInst->getNumThreads() * 16; ++i) { if (!ret->feed()) break; } @@ -2343,6 +2386,9 @@ py::UniqueObj SwTokenizerObject::encodeFromMorphs(PyObject* morphs, bool returnO tokens.emplace_back(form, pos, spaceness); } }, "`encodeFromMorphs` requires an iterable of `Tuple[str, str, bool]` parameters."); +#ifdef Py_GIL_DISABLED + std::shared_lock lock{ *kiwi->rwMutex }; +#endif vector> offsets; auto tokenIds = tokenizer.encode(tokens, returnOffsets ? &offsets : nullptr); if (returnOffsets) @@ -2360,16 +2406,16 @@ py::UniqueObj SwTokenizerObject::tokenizeAndEncode(PyObject* text, bool returnOf if (PyUnicode_Check(text)) { vector> offsets; - auto res = tokenizer.getKiwi()->analyze(py::toCpp(text), 1, Match::allWithNormalizing | Match::zCoda); + auto res = kiwiInst->analyze(py::toCpp(text), 1, Match::allWithNormalizing | Match::zCoda); auto tokenIds = tokenizer.encode(res[0].first.data(), res[0].first.size(), returnOffsets ? &offsets : nullptr); if (returnOffsets) { chrOffsetsToTokenOffsets(res[0].first, offsets); - return py::buildPyTuple(resToPyList(move(res), kiwi.get()), tokenIds, offsets); + return py::buildPyTuple(resToPyList(move(res), kiwi.get(), kiwiInst), tokenIds, offsets); } else { - return py::buildPyTuple(resToPyList(move(res), kiwi.get()), tokenIds); + return py::buildPyTuple(resToPyList(move(res), kiwi.get(), kiwiInst), tokenIds); } } @@ -2382,7 +2428,7 @@ py::UniqueObj SwTokenizerObject::tokenizeAndEncode(PyObject* text, bool returnOf ret->inputIter = move(iter); ret->returnOffsets = !!returnOffsets; - for (size_t i = 0; i < kiwi->kiwi.getNumThreads() * 16; ++i) + for (size_t i = 0; i < kiwiInst->getNumThreads() * 16; ++i) { if (!ret->feed()) break; } @@ -2396,6 +2442,9 @@ std::string SwTokenizerObject::decode(PyObject* ids, bool ignoreErrors) const std::pair KiwiObject::addUserWord(const char* word, const char* tag, float score, std::optional origWord) { +#ifdef Py_GIL_DISABLED + std::unique_lock lock{ *rwMutex }; +#endif auto pos = parseTag(tag); std::pair added = std::make_pair(0, false); if (origWord) @@ -2406,13 +2455,13 @@ std::pair KiwiObject::addUserWord(const char* word, const char* { added = builder.addWord(utf8To16(word), pos, score); } - if (added.second) kiwi = Kiwi{}; + if (added.second) kiwi.reset(); return added; } -bool KiwiObject::addPreAnalyzedWord(const char* form, PyObject* oAnalyzed, float score) +bool KiwiObject::addPreAnalyzedWord(const char* form, PyObject* oAnalyzed, float score, Dialect dialect) { - vector> analyzed; + vector> analyzed; vector> positions; py::foreach(oAnalyzed, [&](PyObject* item) { @@ -2424,21 +2473,32 @@ bool KiwiObject::addPreAnalyzedWord(const char* form, PyObject* oAnalyzed, float { throw py::ValueError{ "`analyzed` must be in format `{form}/{tag}`, but given : " + py::repr(item)}; } - analyzed.emplace_back(str.substr(0, p), parseTag(str.substr(p + 1))); + analyzed.emplace_back(str.substr(0, p), parseTag(str.substr(p + 1)), undefSenseId); } else if (PySequence_Check(item)) { if (Py_SIZE(item) == 2) { auto p = py::toCpp>(item); - analyzed.emplace_back(p.first, parseTag(p.second)); + analyzed.emplace_back(p.first, parseTag(p.second), undefSenseId); } - else + else if (Py_SIZE(item) == 3) + { + auto p = py::toCpp>(item); + analyzed.emplace_back(get<0>(p), parseTag(get<1>(p)), get<2>(p)); + } + else if (Py_SIZE(item) == 4) { auto t = py::toCpp>(item); - analyzed.emplace_back(get<0>(t), parseTag(get<1>(t))); + analyzed.emplace_back(get<0>(t), parseTag(get<1>(t)), undefSenseId); positions.emplace_back(get<2>(t), get<3>(t)); } + else + { + auto t = py::toCpp>(item); + analyzed.emplace_back(get<0>(t), parseTag(get<1>(t)), get<2>(t)); + positions.emplace_back(get<3>(t), get<4>(t)); + } } else { @@ -2449,9 +2509,11 @@ bool KiwiObject::addPreAnalyzedWord(const char* form, PyObject* oAnalyzed, float { throw py::ValueError{ "All items of `analyzed` must be in the type `Tuple[str, str]` or `Tuple[str, str, int, int]`."}; } - - auto added = builder.addPreAnalyzedWord(utf8To16(form), analyzed, positions, score); - if (added) kiwi = Kiwi{}; +#ifdef Py_GIL_DISABLED + std::unique_lock lock{ *rwMutex }; +#endif + auto added = builder.addPreAnalyzedWord(utf8To16(form), analyzed, positions, score, dialect); + if (added) kiwi.reset(); return added; } @@ -2459,6 +2521,9 @@ std::vector> KiwiObject::addRule(const char* { if (!PyCallable_Check(replacer)) throw py::ValueError{ "`replacer` must be an callable." }; +#ifdef Py_GIL_DISABLED + std::unique_lock lock{ *rwMutex }; +#endif auto pos = parseTag(tag); auto added = builder.addRule(pos, [&](const u16string& input) { @@ -2466,14 +2531,17 @@ std::vector> KiwiObject::addRule(const char* if (!ret) throw py::ExcPropagation{}; return py::toCpp(ret.get()); }, score); - if (!added.empty()) kiwi = Kiwi{}; + if (!added.empty()) kiwi.reset(); return added; } size_t KiwiObject::loadUserDictionary(const char* path) { - auto ret = builder.loadDictionary(path); - if (ret) kiwi = Kiwi{}; +#ifdef Py_GIL_DISABLED + std::unique_lock lock{ *rwMutex }; +#endif + size_t ret = builder.loadDictionary(path); + if (ret) kiwi.reset(); return ret; } @@ -2516,8 +2584,11 @@ py::UniqueObj KiwiObject::extractWords(PyObject* sentences, size_t minCnt, size_ py::UniqueObj KiwiObject::extractAddWords(PyObject* sentences, size_t minCnt, size_t maxWordLen, float minScore, float posScore, bool lmFilter) { +#ifdef Py_GIL_DISABLED + std::unique_lock lock{ *rwMutex }; +#endif auto res = builder.extractAddWords(obj2reader(sentences), minCnt, maxWordLen, minScore, posScore, lmFilter); - kiwi = Kiwi{}; + kiwi.reset(); py::UniqueObj retList{ PyList_New(res.size()) }; size_t idx = 0; @@ -2530,14 +2601,19 @@ py::UniqueObj KiwiObject::extractAddWords(PyObject* sentences, size_t minCnt, si return retList; } -py::UniqueObj KiwiObject::analyze(PyObject* text, size_t topN, Match matchOptions, bool echo, PyObject* blockList, bool openEnding, PyObject* pretokenized) +py::UniqueObj KiwiObject::analyze(PyObject* text, size_t topN, + Match matchOptions, bool echo, PyObject* blockList, bool openEnding, + Dialect allowedDialects, float dialectCost, + PyObject* pretokenized, PyObject* config) { - doPrepare(); + auto kiwiInst = doPrepare(); + KiwiConfig cConfig = toKiwiConfig(config); + if (PyUnicode_Check(text)) { const unordered_set* morphs = nullptr; pair, vector> pretokenizedSpans; - if (blockList != Py_None) morphs = &((MorphemeSetObject*)blockList)->morphSet; + if (blockList != Py_None) morphs = &((MorphemeSetObject*)blockList)->getMorphemeSet(); if (PyCallable_Check(pretokenized)) { py::UniqueObj ptResult{ PyObject_CallFunctionObjArgs(pretokenized, text, nullptr) }; @@ -2559,10 +2635,9 @@ py::UniqueObj KiwiObject::analyze(PyObject* text, size_t topN, Match matchOption so = py::toCpp>(text); updatePretokenizedSpanToU16(pretokenizedSpans.first, so); } - - auto res = kiwi.analyze(so.str, topN, AnalyzeOption{ matchOptions, morphs, openEnding }, pretokenizedSpans.first); + auto res = kiwiInst->analyze(so.str, topN, AnalyzeOption{ matchOptions, morphs, openEnding, allowedDialects, dialectCost}, pretokenizedSpans.first, cConfig); if (res.size() > topN) res.erase(res.begin() + topN, res.end()); - return resToPyList(move(res), this, move(pretokenizedSpans.second)); + return resToPyList(move(res), this, kiwiInst, move(pretokenizedSpans.second)); } else { @@ -2574,12 +2649,15 @@ py::UniqueObj KiwiObject::analyze(PyObject* text, size_t topN, Match matchOption Py_INCREF(this); ret->inputIter = move(iter); ret->topN = topN; - ret->matchOptions = matchOptions; - ret->openEnding = openEnding; + ret->options = AnalyzeOption{ matchOptions, nullptr, openEnding, allowedDialects, dialectCost }; + ret->config = cConfig; ret->echo = !!echo; + ret->kiwiInst = kiwiInst; + if (blockList != Py_None) { ret->blocklist = py::UniqueCObj{ (MorphemeSetObject*)blockList }; + ret->options.blocklist = &ret->blocklist->getMorphemeSet(); Py_INCREF(blockList); } @@ -2593,7 +2671,7 @@ py::UniqueObj KiwiObject::analyze(PyObject* text, size_t topN, Match matchOption throw py::ValueError{ "`analyze` of multiple inputs requires a callable `pretokenized` argument." }; } - for (size_t i = 0; i < kiwi.getNumThreads() * 16; ++i) + for (size_t i = 0; i < kiwiInst->getNumThreads() * 16; ++i) { if (!ret->feed()) break; } @@ -2603,11 +2681,13 @@ py::UniqueObj KiwiObject::analyze(PyObject* text, size_t topN, Match matchOption py::UniqueObj KiwiObject::getMorpheme(size_t id) { + auto kiwiInst = doPrepare(); + auto ret = py::makeNewObject(); - doPrepare(); - auto* morph = kiwi.idToMorph(id); + auto* morph = kiwiInst->idToMorph(id); if (!morph) throw py::ValueError{ "out of range" }; auto joinedForm = joinHangul(morph->getForm()); + ret->kiwiInst = kiwiInst; ret->_form = move(joinedForm); ret->_tag = getTagStr(morph->tag, ret->_form); ret->_baseMorph = ret->_morph = morph; @@ -2619,8 +2699,8 @@ py::UniqueObj KiwiObject::getMorpheme(size_t id) py::UniqueObj KiwiObject::join(PyObject* morphs, bool lmSearch, bool returnPositions) { - doPrepare(); - auto joiner = kiwi.newJoiner(!!lmSearch); + auto kiwiInst = doPrepare(); + auto joiner = kiwiInst->newJoiner(!!lmSearch); size_t prevHash = 0; size_t prevEnd = 0; py::foreach(morphs, [&](PyObject* item) @@ -2633,8 +2713,8 @@ py::UniqueObj KiwiObject::join(PyObject* morphs, bool lmSearch, bool returnPosit { space = token._pos <= prevEnd ? cmb::Space::no_space : cmb::Space::insert_space; } - - if (token._morph && token._morph->kform && !token._morph->kform->empty()) + + if (!token.kiwiInst.expired() && token._morph && token._morph->kform && !token._morph->kform->empty()) { joiner.add(token._morphId, space); } @@ -2699,12 +2779,13 @@ py::UniqueObj KiwiObject::join(PyObject* morphs, bool lmSearch, bool returnPosit } template -inline uint32_t convertToMorphId(const Kiwi& kiwi, PyObject* target, E&& errorMsg) +inline uint32_t convertToMorphId(const Kiwi* kiwi, PyObject* target, E&& errorMsg) { - if (PyUnicode_Check(target) || (PyTuple_Check(target) && PyTuple_GET_SIZE(target) == 2)) + if (PyUnicode_Check(target) || (PyTuple_Check(target) && (PyTuple_GET_SIZE(target) == 2 || PyTuple_GET_SIZE(target) == 3))) { u16string form; POSTag tag = POSTag::unknown; + uint8_t senseId = undefSenseId; if (PyUnicode_Check(target)) { form = py::toCpp(target); @@ -2713,9 +2794,13 @@ inline uint32_t convertToMorphId(const Kiwi& kiwi, PyObject* target, E&& errorMs { form = py::toCpp(PyTuple_GET_ITEM(target, 0)); tag = parseTag(py::toCpp(PyTuple_GET_ITEM(target, 1))); + if (PyTuple_GET_SIZE(target) > 2) + { + senseId = py::toCpp(PyTuple_GET_ITEM(target, 2)); + } } - auto cands = kiwi.findMorphemes(form, tag); + auto cands = kiwi->findMorphemes(form, tag, senseId); if (cands.empty()) { throw py::ValueError{ "No morpheme found for the given form: " + utf16To8(form) }; @@ -2728,6 +2813,9 @@ inline uint32_t convertToMorphId(const Kiwi& kiwi, PyObject* target, E&& errorMs errMsg += utf16To8(form); errMsg.push_back('/'); errMsg += tagToString(c->tag); + errMsg.push_back('_'); + errMsg.push_back('_'); + errMsg += to_string(c->senseId); errMsg.push_back(','); errMsg.push_back(' '); } @@ -2747,36 +2835,37 @@ inline uint32_t convertToMorphId(const Kiwi& kiwi, PyObject* target, E&& errorMs } } -inline Vector convertToIds(const Kiwi& kiwi, PyObject* iterable) +inline Vector convertToIds(const Kiwi* kiwi, PyObject* iterable) { Vector ids; py::foreach(iterable, [&](PyObject* item) { - ids.emplace_back(convertToMorphId(kiwi, item, "`prefix` must be an instance of `str`, `Tuple[str, str]` or `int`.")); - }, "`prefix` must be an iterable of `Tuple[str, str]` or `int`"); + ids.emplace_back(convertToMorphId(kiwi, item, "`prefix` must be an instance of `str`, `Tuple[str, str]`, `Tuple[str, str, int]` or `int`.")); + }, "`prefix` must be an iterable of `Tuple[str, str]`, `Tuple[str, str, int]` or `int`"); return ids; } py::UniqueObj KiwiObject::mostSimilarMorphemes(PyObject* retTy, PyObject* target, size_t topN) { - doPrepare(); - auto congLm = dynamic_cast(kiwi.getLangModel()); + auto kiwiInst = doPrepare(); + auto congLm = dynamic_cast(kiwiInst->getLangModel()); if (!congLm) { throw py::ValueError{ "`most_similar_morphemes` is supported only for CoNgramModel." }; } - const uint32_t targetId = convertToMorphId(kiwi, target, "`target` must be an instance of `str`, `Tuple[str, str]` or `int`."); + const uint32_t targetId = convertToMorphId(kiwiInst.get(), target, "`target` must be an instance of `str`, `Tuple[str, str]`, `Tuple[str, str, int]` or `int`."); Vector> output(topN); output.resize(congLm->mostSimilarWords(targetId, topN, output.data())); py::UniqueObj ret{ PyList_New(output.size()) }; for (size_t i = 0; i < output.size(); ++i) { - auto* morph = kiwi.idToMorph(output[i].first); + auto* morph = kiwiInst->idToMorph(output[i].first); PyList_SET_ITEM(ret.get(), i, PyObject_CallObject(retTy, py::buildPyTuple( joinHangul(morph->getForm()), tagToString(morph->tag), + morph->senseId, output[i].first, output[i].second ).get())); @@ -2786,20 +2875,20 @@ py::UniqueObj KiwiObject::mostSimilarMorphemes(PyObject* retTy, PyObject* target py::UniqueObj KiwiObject::mostSimilarContexts(PyObject* retTy, PyObject* target, PyObject* contextId, size_t topN) { - doPrepare(); - auto congLm = dynamic_cast(kiwi.getLangModel()); + auto kiwiInst = doPrepare(); + auto congLm = dynamic_cast(kiwiInst->getLangModel()); if (!congLm) { throw py::ValueError{ "`most_similar_contexts` is supported only for CoNgramModel." }; } + prepareContextMap(kiwiInst.get(), congLm); Vector targetIds; if (target != Py_None) { - targetIds = convertToIds(kiwi, target); + targetIds = convertToIds(kiwiInst.get(), target); } - prepareContextMap(congLm); - + const uint32_t targetContextId = target == Py_None ? PyLong_AsLong(contextId) : congLm->toContextId(targetIds.data(), targetIds.size()); @@ -2814,7 +2903,7 @@ py::UniqueObj KiwiObject::mostSimilarContexts(PyObject* retTy, PyObject* target, py::UniqueObj ret{ PyList_New(output.size()) }; for (size_t i = 0; i < output.size(); ++i) { - auto* morph = kiwi.idToMorph(output[i].first); + auto* morph = kiwiInst->idToMorph(output[i].first); auto& forms = contextForms[output[i].first]; auto& analysesData = contextAnalyses[output[i].first].first; auto& analysesPtr = contextAnalyses[output[i].first].second; @@ -2827,10 +2916,11 @@ py::UniqueObj KiwiObject::mostSimilarContexts(PyObject* retTy, PyObject* target, py::UniqueObj morphs{ PyList_New(end - start) }; for (size_t k = start; k < end; ++k) { - auto* morph = kiwi.idToMorph(analysesData[k]); + auto* morph = kiwiInst->idToMorph(analysesData[k]); PyList_SET_ITEM(morphs.get(), k - start, py::buildPyTuple( joinHangul(morph->getForm()), - tagToString(morph->tag) + tagToString(morph->tag), + morph->senseId ).release()); } PyList_SET_ITEM(analysisList.get(), j, morphs.release()); @@ -2847,18 +2937,18 @@ py::UniqueObj KiwiObject::mostSimilarContexts(PyObject* retTy, PyObject* target, py::UniqueObj KiwiObject::predictNextMorpheme(PyObject* retTy, PyObject* prefix, PyObject* bgPrefix, float bgWeight, size_t topN) { - doPrepare(); - auto congLm = dynamic_cast(kiwi.getLangModel()); + auto kiwiInst = doPrepare(); + auto congLm = dynamic_cast(kiwiInst->getLangModel()); if (!congLm) { throw py::ValueError{ "`predict_next_morpheme` is supported only for CoNgramModel." }; } - Vector prefixIds = convertToIds(kiwi, prefix); + Vector prefixIds = convertToIds(kiwiInst.get(), prefix); Vector bgPrefixIds; if (bgPrefix != Py_None) { - bgPrefixIds = convertToIds(kiwi, bgPrefix); + bgPrefixIds = convertToIds(kiwiInst.get(), bgPrefix); } const uint32_t prefixContextId = congLm->toContextId(prefixIds.data(), prefixIds.size()); @@ -2876,10 +2966,11 @@ py::UniqueObj KiwiObject::predictNextMorpheme(PyObject* retTy, PyObject* prefix, py::UniqueObj ret{ PyList_New(output.size()) }; for (size_t i = 0; i < output.size(); ++i) { - auto* morph = kiwi.idToMorph(output[i].first); + auto* morph = kiwiInst->idToMorph(output[i].first); PyList_SET_ITEM(ret.get(), i, PyObject_CallObject(retTy, py::buildPyTuple( joinHangul(morph->getForm()), tagToString(morph->tag), + morph->senseId, output[i].first, output[i].second ).get())); @@ -2889,30 +2980,30 @@ py::UniqueObj KiwiObject::predictNextMorpheme(PyObject* retTy, PyObject* prefix, float KiwiObject::morphemeSimilarity(PyObject* a, PyObject* b) { - doPrepare(); - auto congLm = dynamic_cast(kiwi.getLangModel()); + auto kiwiInst = doPrepare(); + auto congLm = dynamic_cast(kiwiInst->getLangModel()); if (!congLm) { throw py::ValueError{ "`morpheme_similarity` is supported only for CoNgramModel." }; } - const uint32_t aId = convertToMorphId(kiwi, a, "`morpheme1` must be an instance of `str`, `Tuple[str, str]` or `int`."); - const uint32_t bId = convertToMorphId(kiwi, b, "`morpheme2` must be an instance of `str`, `Tuple[str, str]` or `int`."); + const uint32_t aId = convertToMorphId(kiwiInst.get(), a, "`morpheme1` must be an instance of `str`, `Tuple[str, str]`, `Tuple[str, str, int]` or `int`."); + const uint32_t bId = convertToMorphId(kiwiInst.get(), b, "`morpheme2` must be an instance of `str`, `Tuple[str, str]`, `Tuple[str, str, int]` or `int`."); return congLm->wordSimilarity(aId, bId); } float KiwiObject::contextSimilarity(PyObject* a, PyObject* b) { - doPrepare(); - auto congLm = dynamic_cast(kiwi.getLangModel()); + auto kiwiInst = doPrepare(); + auto congLm = dynamic_cast(kiwiInst->getLangModel()); if (!congLm) { throw py::ValueError{ "`morpheme_similarity` is supported only for CoNgramModel." }; } - const Vector aId = convertToIds(kiwi, a); - const Vector bId = convertToIds(kiwi, b); + const Vector aId = convertToIds(kiwiInst.get(), a); + const Vector bId = convertToIds(kiwiInst.get(), b); const uint32_t aContextId = congLm->toContextId(aId.data(), aId.size()); const uint32_t bContextId = congLm->toContextId(bId.data(), bId.size()); @@ -2935,29 +3026,27 @@ void KiwiObject::convertHSData( morphemeDefPathStr = py::toCpp(morphemeDefPath); } - vector, pair>> transformMap; + vector, vector>>> transformMap; if (transform && transform != Py_None) { - py::UniqueObj iter{ PyObject_GetIter(transform) }; - if (!iter) throw py::ValueError{ "`transform` must be an iterable of `Tuple[Tuple[str, str], Tuple[str, str]]`." }; - py::foreach(iter.get(), [&](PyObject* item) + py::foreach(transform, [&](PyObject* item) { - if (PyTuple_Check(item) && PyTuple_Size(item) == 2) - { - auto a = py::toCpp>(PyTuple_GET_ITEM(item, 0)); - auto b = py::toCpp>(PyTuple_GET_ITEM(item, 1)); - POSTag aTag = parseTag(a.second.c_str()); - POSTag bTag = parseTag(b.second.c_str()); - transformMap.emplace_back( - make_pair(a.first, aTag), - make_pair(b.first, bTag) - ); - } - else + pair key; + vector> values; + py::foreach>(item, [&](const pair& token) { - throw py::ValueError{ "`transform` must be an iterable of `Tuple[Tuple[str, str], Tuple[str, str]]`." }; - } - }, "`transform` must be an iterable of `Tuple[Tuple[str, str], Tuple[str, str]]`."); + const POSTag tag = parseTag(token.second.c_str()); + if (key.first.empty()) + { + key = make_pair(token.first, tag); + } + else + { + values.emplace_back(token.first, tag); + } + }, "`transform` must be an iterable of `List[Tuple[str, str]]`."); + transformMap.emplace_back(key, move(values)); + }, "`transform` must be an iterable of `List[Tuple[str, str]]`."); } builder.convertHSData(py::toCpp>(inputPathes), @@ -2977,6 +3066,8 @@ py::UniqueObj KiwiObject::makeHSDataset(PyObject* inputPathes, float dropout, float dropoutOnHistory, float nounAugmentingProb, + float emojiAugmentingProb, + float sbAugmentingProb, size_t generateUnlikelihoods, PyObject* tokenFilter, PyObject* windowFilter, @@ -3013,29 +3104,27 @@ py::UniqueObj KiwiObject::makeHSDataset(PyObject* inputPathes, }; } - vector, pair>> transformMap; + vector, vector>>> transformMap; if (transform && transform != Py_None) { - py::UniqueObj iter{ PyObject_GetIter(transform) }; - if (!iter) throw py::ValueError{ "`transform` must be an iterable of `Tuple[Tuple[str, str], Tuple[str, str]]`." }; - py::foreach(iter.get(), [&](PyObject* item) + py::foreach(transform, [&](PyObject* item) { - if (PyTuple_Check(item) && PyTuple_Size(item) == 2) - { - auto a = py::toCpp>(PyTuple_GET_ITEM(item, 0)); - auto b = py::toCpp>(PyTuple_GET_ITEM(item, 1)); - POSTag aTag = parseTag(a.second.c_str()); - POSTag bTag = parseTag(b.second.c_str()); - transformMap.emplace_back( - make_pair(a.first, aTag), - make_pair(b.first, bTag) - ); - } - else + pair key; + vector> values; + py::foreach>(item, [&](const pair& token) { - throw py::ValueError{ "`transform` must be an iterable of `Tuple[Tuple[str, str], Tuple[str, str]]`." }; - } - }, "`transform` must be an iterable of `Tuple[Tuple[str, str], Tuple[str, str]]`."); + const POSTag tag = parseTag(token.second.c_str()); + if (key.first.empty()) + { + key = make_pair(token.first, tag); + } + else + { + values.emplace_back(token.first, tag); + } + }, "`transform` must be an iterable of `List[Tuple[str, str]]`."); + transformMap.emplace_back(key, move(values)); + }, "`transform` must be an iterable of `List[Tuple[str, str]]`."); } string morphemeDefPathStr; @@ -3050,10 +3139,14 @@ py::UniqueObj KiwiObject::makeHSDataset(PyObject* inputPathes, causalContextSize, windowSize, numWorkers, - dropout, - dropoutOnHistory, - nounAugmentingProb, - generateUnlikelihoods, + HSDatasetOption { + dropout, + dropoutOnHistory, + nounAugmentingProb, + emojiAugmentingProb, + sbAugmentingProb, + generateUnlikelihoods, + }, tf, wf, splitRatio, @@ -3102,6 +3195,10 @@ struct NgramExtractorObject : py::CObject using _InitArgs = std::tuple; NgramExtractor ne; + std::shared_ptr kiwiInst; +#ifdef Py_GIL_DISABLED + std::unique_ptr rwMutex; +#endif NgramExtractorObject() = default; @@ -3111,19 +3208,28 @@ struct NgramExtractorObject : py::CObject { throw py::ValueError{ "`kiwi` must be an instance of `Kiwi`." }; } - ((KiwiObject*)kiwi)->doPrepare(); - ne = NgramExtractor{ ((KiwiObject*)kiwi)->kiwi, gatherLmScore }; + kiwiInst = ((KiwiObject*)kiwi)->doPrepare(); + ne = NgramExtractor{ *kiwiInst.get(), gatherLmScore}; +#ifdef Py_GIL_DISABLED + rwMutex = std::make_unique(); +#endif } size_t add(PyObject* texts) { if (PyUnicode_Check(texts)) { +#ifdef Py_GIL_DISABLED + std::unique_lock lock{ *rwMutex }; +#endif return ne.addText(py::toCpp(texts)); } else { py::UniqueObj iter{ PyObject_GetIter(texts) }; +#ifdef Py_GIL_DISABLED + std::unique_lock lock{ *rwMutex }; +#endif auto ret = ne.addTexts([&]() { py::UniqueObj text{ PyIter_Next(iter.get()) }; @@ -3142,6 +3248,9 @@ struct NgramExtractorObject : py::CObject py::UniqueObj extract(PyObject* retTy, size_t maxCandidates, size_t minCnt, size_t maxLength, float minScore, size_t numWorkers) { +#ifdef Py_GIL_DISABLED + std::shared_lock lock{ *rwMutex }; +#endif auto ret = ne.extract(maxCandidates, minCnt, maxLength, minScore, numWorkers); py::UniqueObj retList{ PyList_New(0) }; for (auto& r : ret) diff --git a/src/PyUtils.h b/src/PyUtils.h index 68885b4..1f5af2c 100644 --- a/src/PyUtils.h +++ b/src/PyUtils.h @@ -856,7 +856,7 @@ namespace py bool _toCpp(PyObject* obj, std::pair<_Ty1, _Ty2>& out) { - if (Py_SIZE(obj) != 2) throw ConversionFail{ "input is not tuple with len=2" }; + if (Py_SIZE(obj) != 2) throw ConversionFail{ "input is not tuple with len=2: " + reprWithNestedError(obj) }; if (!toCpp<_Ty1>(UniqueObj{ PySequence_ITEM(obj, 0) }.get(), out.first)) return false; if (!toCpp<_Ty2>(UniqueObj{ PySequence_ITEM(obj, 1) }.get(), out.second)) return false; return true; @@ -922,6 +922,9 @@ namespace py bool _toCpp(PyObject* obj, std::unordered_map<_Ty1, _Ty2>& out) { +#ifdef Py_GIL_DISABLED + Py_BEGIN_CRITICAL_SECTION(obj); +#endif PyObject* key, * value; Py_ssize_t pos = 0; while (PyDict_Next(obj, &pos, &key, &value)) @@ -932,6 +935,9 @@ namespace py if (!toCpp<_Ty2>(value, v)) return false; out.emplace(std::move(k), std::move(v)); } +#ifdef Py_GIL_DISABLED + Py_END_CRITICAL_SECTION(); +#endif if (PyErr_Occurred()) return false; return true; } @@ -1550,7 +1556,7 @@ namespace py inline UniqueObj buildPyDict(const char** keys, _Rest&&... rest) { UniqueObj dict{ PyDict_New() }; - detail::setDictItem(dict, keys, std::forward<_Rest>(rest)...); + detail::setDictItem(dict.get(), keys, std::forward<_Rest>(rest)...); return dict; } @@ -1558,7 +1564,7 @@ namespace py inline UniqueObj buildPyDictSkipNull(const char** keys, _Rest&&... rest) { UniqueObj dict{ PyDict_New() }; - detail::setDictItemSkipNull(dict, keys, std::forward<_Rest>(rest)...); + detail::setDictItemSkipNull(dict.get(), keys, std::forward<_Rest>(rest)...); return dict; } @@ -1773,6 +1779,9 @@ namespace py { mod = PyModule_Create(&def); addToModule(); +#ifdef Py_GIL_DISABLED + PyUnstable_Module_SetGIL(mod, Py_MOD_GIL_NOT_USED); +#endif return mod; } diff --git a/test/test_kiwipiepy.py b/test/test_kiwipiepy.py index 161a30e..ddfef4d 100644 --- a/test/test_kiwipiepy.py +++ b/test/test_kiwipiepy.py @@ -93,6 +93,14 @@ def test_blocklist(): tokens = kiwi.tokenize("고마움을", blocklist=['고마움']) assert tokens[0].form == "고맙" + ms = MorphemeSet(kiwi, ['고마움']) + tokens = kiwi.tokenize("고마움을", blocklist=ms) + assert tokens[0].form == "고맙" + + kiwi.add_user_word('TEST1', 'NNP') + tokens = kiwi.tokenize("고마움을", blocklist=ms) + assert tokens[0].form == "고맙" + def test_pretokenized(): kiwi = Kiwi(load_multi_dict=False) text = "드디어패트와 매트가 2017년에 국내 개봉했다. 패트와매트는 2016년..." @@ -106,8 +114,8 @@ def test_pretokenized(): assert res[1].tag == "NNP" assert res[3].form == "2017년" assert res[3].tag == "NNP" - assert res[13].form == "2016년" - assert res[13].tag == "NNP" + assert res[-2].form == "2016년" + assert res[-2].tag == "NNP" res = kiwi.tokenize(text, pretokenized=[ (3, 9), @@ -116,8 +124,8 @@ def test_pretokenized(): ]) assert res[3].form == "2017년" assert res[3].tag == "NNG" - assert res[13].form == "2016년" - assert res[13].tag == "NNG" + assert res[-2].form == "2016년" + assert res[-2].tag == "NNG" res = kiwi.tokenize(text, pretokenized=[ (27, 29, PretokenizedToken('페트', 'NNB', 0, 2)), @@ -238,10 +246,10 @@ def test_user_value(): assert tokens[0].tag == 'NNG' assert tokens[0].user_value == 'babo' - tokens = kiwi.tokenize('이렇게 {이것}은 특별하다') - assert tokens[1].form == '{이것}' - assert tokens[1].tag == 'SPECIAL' - assert tokens[1].user_value == {'tag':'SPECIAL'} + tokens = kiwi.tokenize('{이것}은 특별하다') + assert tokens[0].form == '{이것}' + assert tokens[0].tag == 'SPECIAL' + assert tokens[0].user_value == {'tag':'SPECIAL'} assert sum(1 for t in tokens if t.user_value is not None) == 1 tokens = next(kiwi.tokenize(['{이것}은 특별하다'])) @@ -290,7 +298,7 @@ def test_words_with_space(): assert res5[0].form != '대학생 선교회' assert res6[0].form != '대학생 선교회' - kiwi.space_tolerance = 1 + kiwi.global_config.space_tolerance = 1 res1 = kiwi.tokenize('대학생 선교회') res2 = kiwi.tokenize('대학생선교회') res3 = kiwi.tokenize('대학생 \t 선교회') @@ -304,7 +312,7 @@ def test_words_with_space(): assert len(res5) == 1 assert len(res6) != 1 - kiwi.space_tolerance = 0 + kiwi.global_config.space_tolerance = 0 assert kiwi.add_user_word('농협 용인 육가공 공장', 'NNP') res1 = kiwi.tokenize('농협 용인 육가공 공장') res2 = kiwi.tokenize('농협용인 육가공 공장') @@ -319,7 +327,7 @@ def test_words_with_space(): assert res5[0].form == '농협 용인 육가공 공장' assert res6[0].form != '농협 용인 육가공 공장' - kiwi.space_tolerance = 1 + kiwi.global_config.space_tolerance = 1 res2 = kiwi.tokenize('농협용인육 가공 공장') res3 = kiwi.tokenize('농협용 인육 가공 공장') res4 = kiwi.tokenize('농협용 인육 가공공장') @@ -327,7 +335,7 @@ def test_words_with_space(): assert res3[0].form != '농협 용인 육가공 공장' assert res4[0].form != '농협 용인 육가공 공장' - kiwi.space_tolerance = 2 + kiwi.global_config.space_tolerance = 2 res3 = kiwi.tokenize('농협용 인육 가공 공장') res4 = kiwi.tokenize('농협용 인육 가공공장') assert res3[0].form == '농협 용인 육가공 공장' @@ -548,15 +556,6 @@ def test_bug_38(): kiwi = Kiwi(integrate_allomorph=False) print(kiwi.analyze(text)) -def test_property(): - kiwi = Kiwi() - print(kiwi.integrate_allomorph) - kiwi.integrate_allomorph = False - print(kiwi.integrate_allomorph) - print(kiwi.cutoff_threshold) - kiwi.cutoff_threshold = 1 - print(kiwi.cutoff_threshold) - def test_stopwords(): kiwi = Kiwi() tokens, _ = kiwi.analyze('불용어 처리 테스트 중입니다 ' @@ -620,22 +619,22 @@ def test_add_rule(): def test_add_pre_analyzed_word(): kiwi = Kiwi() - ores = kiwi.tokenize("팅겼어") + ores = kiwi.tokenize("뜅겼어") try: - kiwi.add_pre_analyzed_word("팅겼어", [("팅기", "VV"), "었/EP", "어/EF"]) + kiwi.add_pre_analyzed_word("뜅겼어", [("뜅기", "VV"), "었/EP", "어/EF"]) raise AssertionError("expected to raise `ValueError`") except ValueError: pass except: raise - kiwi.add_user_word("팅기", "VV", orig_word="튕기") - kiwi.add_pre_analyzed_word("팅겼어", [("팅기", "VV", 0, 2), ("었", "EP", 1, 2), ("어", "EF", 2, 3)]) + kiwi.add_user_word("뜅기", "VV", orig_word="튕기") + kiwi.add_pre_analyzed_word("뜅겼어", [("뜅기", "VV", 0, 2), ("었", "EP", 1, 2), ("어", "EF", 2, 3)]) - res = kiwi.tokenize("팅겼어...") + res = kiwi.tokenize("뜅겼어...") - assert res[0].form == "팅기" and res[0].tag == "VV" and res[0].start == 0 and res[0].end == 2 + assert res[0].form == "뜅기" and res[0].tag == "VV" and res[0].start == 0 and res[0].end == 2 assert res[1].form == "었" and res[1].tag == "EP" and res[1].start == 1 and res[1].end == 2 assert res[2].form == "어" and res[2].tag == "EF" and res[2].start == 2 and res[2].end == 3 assert res[3].form == "..." and res[3].tag == "SF" and res[3].start == 3 and res[3].end == 6 @@ -651,13 +650,13 @@ def test_add_pre_analyzed_word(): def test_space_tolerance(): kiwi = Kiwi() s = "띄 어 쓰 기 문 제 가 있 습 니 다" - kiwi.space_tolerance = 0 + kiwi.global_config.space_tolerance = 0 print(kiwi.tokenize(s)) - kiwi.space_tolerance = 1 + kiwi.global_config.space_tolerance = 1 print(kiwi.tokenize(s)) - kiwi.space_tolerance = 2 + kiwi.global_config.space_tolerance = 2 print(kiwi.tokenize(s)) - kiwi.space_tolerance = 3 + kiwi.global_config.space_tolerance = 3 print(kiwi.tokenize(s)) def test_space(): @@ -701,7 +700,7 @@ def test_space_issue_187(): def test_space_issue_189(): kiwi = Kiwi() - kiwi.add_user_word('팩', 'NNB') + kiwi.add_user_word('팩', 'NNB', score=1) assert kiwi.space('담아 1팩 무료') == '담아 1팩 무료' assert kiwi.space('골라 2팩 무료') == '골라 2팩 무료' @@ -729,6 +728,10 @@ def test_join(): assert kiwi.join(tokens) == "이렇게 형태소로 분해된 문장을 다시 합칠 수 있을까요?" + kiwi.add_user_word('TEST1', 'NNP') + + assert kiwi.join(tokens) == "이렇게 형태소로 분해된 문장을 다시 합칠 수 있을까요?" + assert (kiwi.join([("왜", "MAG"), ("저", "NP"), ("한테", "JKB"), ("묻", "VV"), ("어요", "EF")]) == "왜 저한테 물어요" ) @@ -800,10 +803,10 @@ def test_continual_typo(): assert tokens[1].form == '지각' assert tokens[2].form == '하' -def test_sbg(): - kiwi = Kiwi(model_type='knlm') +def test_long_dependency(): + kiwi = Kiwi(model_type='none') print(kiwi.tokenize('이 번호로 전화를 이따가 꼭 반드시 걸어.')) - kiwi = Kiwi(model_type='sbg') + kiwi = Kiwi(model_type='largest') print(kiwi.tokenize('이 번호로 전화를 이따가 꼭 반드시 걸어.')) def test_issue_92(): @@ -827,6 +830,15 @@ def test_unicode(): def test_template(): kiwi = Kiwi() + + tpl = kiwi.template("{}가 {}으로 돌아갔다.") + res = tpl.format("사람", "대구") + assert res == "사람이 대구로 돌아갔다." + + tpl = kiwi.template("{{}}도 {}이 좋다.") + res = tpl.format("키위") + assert res == "{}도 키위가 좋다." + tpl = kiwi.template("{}가 {}을 {}었다.") res = tpl.format(("나", "NP"), ("공부", "NNG"), ("하", "VV")) @@ -869,7 +881,7 @@ def test_issue_145(): assert tokens def test_issue_172(): - kiwi = Kiwi(model_type='sbg') + kiwi = Kiwi(model_type='largest') text = 'HOME > 커뮤니티 >\n묻고답하기\n작성일 : 17-07-18 07:56\n좋은채팅사이트《08455.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(08455)\n글쓴이 :\n조보노비41\n조회 : 3\nhttp://wanggame9.com\n[0]\n좋은채팅사이트《343916032.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(56007607)\n모바일 성인 바로가기\npc용 무료화상채팅\n좋은채팅사이트《06029.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(876587169)좋은채팅사이트《33375215.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(045657)좋은채팅사이트《08634139.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(25414)좋은채팅사이트《767505.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(5060548)좋은채팅사이트《596669.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(17296904)좋은채팅사이트《708393.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(369818073)좋은채팅사이트《90047.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(8563816)좋은채팅사이트《3684755.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(686192)좋은채팅사이트《7721832.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(977729803)좋은채팅사이트《71604315.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(958133020)좋은채팅사이트《696522.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(604699961)좋은채팅사이트《0408286.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(649148258)좋은채팅사이트《086640.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(432412)좋은채팅사이트《9528841.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(1459068)좋은채팅사이트《265319.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(74071856)좋은채팅사이트《786627.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(63459561)좋은채팅사이트《8446121.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(400026)좋은채팅사이트《107829393.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(969689)좋은채팅사이트《587468920.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(16661)좋은채팅사이트《442598.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(9991066)좋은채팅사이트《353709.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(683259098)좋은채팅사이트《5478202.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(69274033)좋은채팅사이트《196732132.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(7921704)좋은채팅사이트《7019651.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(14278)좋은채팅사이트《97002152.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(53087366)좋은채팅사이트《8661350.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(7772961)좋은채팅사이트《17541.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(98833856)좋은채팅사이트《84792.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(189818)좋은채팅사이트《15413.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(436678243)좋은채팅사이트《439910323.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(02535)좋은채팅사이트《78902.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(6582862)좋은채팅사이트《3963381.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(08870563)좋은채팅사이트《07277.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(501023)좋은채팅사이트《35163318.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(8189066)좋은채팅사이트《7121014.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(57896127)좋은채팅사이트《1826921.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(617181629)좋은채팅사이트《160740.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(14633024)좋은채팅사이트《96038267.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(761270)좋은채팅사이트《73064111.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(20228478)좋은채팅사이트《236003.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(74672)좋은채팅사이트《346639.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(3782376)좋은채팅사이트《87098261.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(76393)좋은채팅사이트《87415.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(7948603)좋은채팅사이트《8698058.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(914769083)좋은채팅사이트《071581955.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(1602981)좋은채팅사이트《728047143.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(45197089)좋은채팅사이트《8582160.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(23060)좋은채팅사이트《73144443.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(2849278)좋은채팅사이트《83533463.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(32451775)좋은채팅사이트《561504.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(776213918)좋은채팅사이트《9269222.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(748970655)좋은채팅사이트《33916.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(314050849)좋은채팅사이트《0000023.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(5703971)좋은채팅사이트《38287287.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(3145687)좋은채팅사이트《85251.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(9835876)좋은채팅사이트《16523.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(006783608)좋은채팅사이트《6486278.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(419384346)좋은채팅사이트《045072.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(814004)좋은채팅사이트《734679655.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(77153183)좋은채팅사이트《5900941.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(40584)좋은채팅사이트《64294.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(907755074)좋은채팅사이트《724052444.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(9887987)좋은채팅사이트《305214375.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(8222456)좋은채팅사이트《5026640.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(241432880)좋은채팅사이트《7838759.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(391313)좋은채팅사이트《31058.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(21679)좋은채팅사이트《625890.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(571682263)좋은채팅사이트《34946692.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(6424279)좋은채팅사이트《42107.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(08760)좋은채팅사이트《340075573.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(3917867)좋은채팅사이트《70139564.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(71221782)좋은채팅사이트《86771999.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(795532)좋은채팅사이트《2924157.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(9268915)좋은채팅사이트《655376855.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(59306414)좋은채팅사이트《78998.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(54308)좋은채팅사이트《10190.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(76479)좋은채팅사이트《203057712.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(14656072)좋은채팅사이트《12990.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(20498832)좋은채팅사이트《589955.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(213220)좋은채팅사이트《11026990.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(27199)좋은채팅사이트《26945193.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(244510)좋은채팅사이트《05255.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(343536584)좋은채팅사이트《521728.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(02924998)좋은채팅사이트《3932735.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(80003)좋은채팅사이트《410325671.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(3536059)좋은채팅사이트《25632.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(60667738)좋은채팅사이트《18218624.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(369376076)좋은채팅사이트《7976278.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(516814)좋은채팅사이트《635402271.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(675950)좋은채팅사이트《5761420.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(86129)좋은채팅사이트《258804679.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(010920)좋은채팅사이트《36339406.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(31826)좋은채팅사이트《315517429.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(29438)좋은채팅사이트《524000.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(923659204)좋은채팅사이트《280352781.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(01912060)좋은채팅사이트《14354858.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(96332591)좋은채팅사이트《83113.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(753470)좋은채팅사이트《5327109.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(66963934)좋은채팅사이트《469866371.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(554246961)좋은채팅사이트《791112.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(10303)좋은채팅사이트《24695299.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(34899)좋은채팅사이트《698614291.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(43441813)좋은채팅사이트《491271437.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(05027627)좋은채팅사이트《043578163.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(5646722)좋은채팅사이트《450135.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(14427873)좋은채팅사이트《6292543.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(749909)좋은채팅사이트《683563.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(5710678)좋은채팅사이트《61515.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(4167851)좋은채팅사이트《53094.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(781419)좋은채팅사이트《70812645.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(69544694)좋은채팅사이트《87181.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(0340508)좋은채팅사이트《85854552.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(00755069)좋은채팅사이트《58251631.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(91150)좋은채팅사이트《613836.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(0323810)좋은채팅사이트《18486.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(320112611)좋은채팅사이트《7903590.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(42628)좋은채팅사이트《57937.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(725476089)좋은채팅사이트《03619356.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(140186629)좋은채팅사이트《19152181.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(6351190)좋은채팅사이트《03858.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(9052367)좋은채팅사이트《19434.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(20757)좋은채팅사이트《061279.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(669707)좋은채팅사이트《1816579.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(23279710)좋은채팅사이트《4228660.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(1551218)좋은채팅사이트《923029.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(233155042)좋은채팅사이트《350162.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(039037923)좋은채팅사이트《29531.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(29850141)좋은채팅사이트《316945572.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(76252)좋은채팅사이트《33867869.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(67153155)좋은채팅사이트《340287625.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(6356147)좋은채팅사이트《3514156.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(347704524)좋은채팅사이트《0011326.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(2986649)좋은채팅사이트《408199631.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(508669)좋은채팅사이트《6749140.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(269055)좋은채팅사이트《398402.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(23146471)좋은채팅사이트《38667.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(551614885)좋은채팅사이트《441553.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(10553)좋은채팅사이트《93984423.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(676180408)좋은채팅사이트《13961.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(4965218)좋은채팅사이트《72319.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(827340805)좋은채팅사이트《211908.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(19718)좋은채팅사이트《784831.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(33551)좋은채팅사이트《93468.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(82255)좋은채팅사이트《03866460.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(36116)좋은채팅사이트《0484386.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(450049202)좋은채팅사이트《75356.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(159840019)좋은채팅사이트《137808.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(9230681)좋은채팅사이트《006024734.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(347425570)좋은채팅사이트《737061.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(33490225)좋은채팅사이트《908123.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(3702627)좋은채팅사이트《91694.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(6921736)좋은채팅사이트《293410.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(290593826)좋은채팅사이트《012780.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(28994235)좋은채팅사이트《11930.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(5963697)좋은채팅사이트《40773.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(56515801)좋은채팅사이트《76224.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(874768)좋은채팅사이트《3174815.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(44021186)좋은채팅사이트《27283328.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(088386936)좋은채팅사이트《201983421.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(767716568)좋은채팅사이트《04133148.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(88675)좋은채팅사이트《11464.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(790814)좋은채팅사이트《64173421.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(876120)좋은채팅사이트《12890.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(79791649)좋은채팅사이트《36663.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(3515082)좋은채팅사이트《972355.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(0316971)좋은채팅사이트《8828504.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(157778)좋은채팅사이트《212215705.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(26817441)좋은채팅사이트《7259216.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(46180)좋은채팅사이트《0122516.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(258041)좋은채팅사이트《42218815.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(29793)좋은채팅사이트《41791.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(28302)좋은채팅사이트《47107.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(401600)좋은채팅사이트《574097.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(371487)좋은채팅사이트《3291856.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(607873)좋은채팅사이트《83250.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(991079649)좋은채팅사이트《501997.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(602357)좋은채팅사이트《48385.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(5654729)좋은채팅사이트《31642.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(45733)좋은채팅사이트《63509074.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(171306848)좋은채팅사이트《68686.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(39921)좋은채팅사이트《532343.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(8870094)좋은채팅사이트《290100.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(993667)좋은채팅사이트《9988283.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(81225)좋은채팅사이트《7880889.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(999003)좋은채팅사이트《15752.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(49675)좋은채팅사이트《88258.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(23644)좋은채팅사이트《17829.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(26054)좋은채팅사이트《2109078.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(16897758)좋은채팅사이트《611713621.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(80245)좋은채팅사이트《728113375.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(89363)좋은채팅사이트《7390299.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(384825313)좋은채팅사이트《84720.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(401839)좋은채팅사이트《08444009.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(017591)좋은채팅사이트《34800.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(86719)좋은채팅사이트《80182242.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(38981)좋은채팅사이트《36336.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(975655)좋은채팅사이트《8223306.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(497449086)좋은채팅사이트《548183.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(324934)좋은채팅사이트《78766186.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(209896)좋은채팅사이트《3088198.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(827292877)좋은채팅사이트《797279.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(6655976)좋은채팅사이트《9125767.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(94684)좋은채팅사이트《63107032.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(476520)좋은채팅사이트《032095.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(65818342)좋은채팅사이트《333508.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(742225783)좋은채팅사이트《866920.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(25335031)좋은채팅사이트《5947784.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(872030)좋은채팅사이트《89232848.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(34647)좋은채팅사이트《64867.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(240875151)좋은채팅사이트《27438809.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(95872118)좋은채팅사이트《902724.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(040268)좋은채팅사이트《13972027.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(9420694)좋은채팅사이트《427118592.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(83238)좋은채팅사이트《797085979.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(30748)좋은채팅사이트《83880.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(05139986)좋은채팅사이트《411824.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(4715153)좋은채팅사이트《282420.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(85438)좋은채팅사이트《42862854.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(15930628)좋은채팅사이트《11454.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(13146)좋은채팅사이트《89489144.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(8704829)좋은채팅사이트《482956366.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트(05535626)좋은채팅사이트《32119225.wanggame9.com》좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사이트좋은채팅사\n이름\n패스워드\n비밀글' tokens = kiwi.tokenize(text) assert len(tokens) > 0 @@ -931,24 +943,24 @@ def test_issue_195(): if sys.maxsize <= 2**32: print("[skipped this test in 32bit OS.]", file=sys.stderr) return - kiwi = Kiwi(num_workers=0, model_type='sbg', typos='basic_with_continual_and_lengthening') + kiwi = Kiwi(num_workers=-1, model_type='largest', typos='basic_with_continual_and_lengthening') res = kiwi.tokenize('“타지크인은 …… 사마르칸트와 부하라를 우즈베키스탄으로 할당한 사실에 대해 매우 고통스러워했다. 타지크인에게 두 도시는 프랑스의 파리와 같은 의미를 지닌 도시였다.” - 131쪽\xa0\xa0중앙아시아는 서쪽의 카스피해에서 동쪽의 천산산맥까지, 그리고 남쪽의 아프가니스탄에서 북쪽의 러시아 타이가 지대까지 뻗어있다. 우즈베키스탄, 카자흐스탄, 키르기스스탄, 타지키스탄, 투르크메니스탄 등 5개의 구소련 공화국이 있는 이 지역의 총면적은 4,003,451㎢로 한국의 약 40배에 달한다. 중앙아시아는 아시아와 유럽을 연결하는 실크로드의 중심으로 수십 세기 동안 수많은 제국과 국가들이 흥망성쇠를 거듭했다. 또한, 다양한 유목민의 이동 통로였는데, 고대 스키타이족부터 돌궐족, 페르시아 왕조, 몽골족, 그리고 투르크족이 중앙아시아를 지배했다. 19세기 중반부터 20세기 말까지 중앙아시아는 러시아와 소비에트 제국령이었다. \xa0\xa0유목민은 역사를 기록으로 남기지 않는다. 이러한 이유로 중앙아시아의 역사는 수많은 논쟁과 국가 이데올로기의 경쟁 무대가 되었으며, 그 역사는 원주민의 언어가 아니라 페르시아어, 아랍어, 몽골어, 중국어, 러시아어 등으로 연구할 수밖에 없다. 최근 들어 한국 중앙아시아 학계에서도 이 지역 연구에 관한 중요한 업적들이 하나둘씩 소개되고 있다. 과거 영미 학자들의 번역서에서 이후 중국어와 러시아어를 바탕으로 이 지역을 심층적으로 연구하는 논문과 책들이 쏟아져 나왔다. 최근에는 페르시아어와 몽골 큽착어와 우즈베크어 등 투르크어를 원전으로 하는 연구들 또한 빠르게 진행되고 있다. \xa0\xa0정세진 교수의 『쉽게 읽는 중앙아시아 이야기』는 러시아어 원전을 바탕으로 쓴 중앙아시아 역사서이다. 정 교수는 이 책에서 중앙아시아 역사의 가장 논쟁적인 초점인 ‘우즈베크-타지크 역사 기원과 논쟁’을 다루고 있는데 이 내용은 다른 어떤 중앙아시아 관련 서적에서 나오지 않는 내용이다. 흔히 중앙아시아의 황금시대는 S. 프레더릭 스타의 『잃어버린 계몽의 시대』에서 잘 묘사한 9∼15세기 부하라와 사마르칸트이다. 당시 부하라는 세계 최고의 과학 문명을 자랑했으며 정복자 티무르는 사마르칸트에 기념비적인 건축물을 남겼다. 문제는 이것이 누구의 유산인가 하는 것이다. \xa0\xa01991년 중앙아시아 국가들은 갑작스러운 소연방의 해체 이후 준비되지 않은 상태에서 독립을 맞이하게 되었다. 소련공산당 핵심 당원이었던 중앙아시아 지도자들은 그들의 지위를 영원히 보장할 수 있는 절호의 기회임을 깨닫고, 공산주의를 버리고 민족주의를 내세우게 된다. 그들은 구소련이 인위적으로 그어준 소비에트 공화국을 민족의 경계 구역으로 확정하고, 나아가 공화국의 이름으로 새로운 민족을 창조해나간다. 1992년부터 중앙아시아 모든 국가는 과거 구소련 시절에 유명무실했던 ‘공화국 역사연구소’를 가장 중요한 국책 연구소로 승격시키고 엄청난 예산을 쏟아부어 신화 창조에 나서게 된다. 우즈베키스탄의 카리모프 대통령은 우즈베크인과 전혀 상관없는 투르크인 아미르 티무르를 우즈베키스탄의 건국 시조로 규정하고, 수도 타슈켄트의 도시공원에 놓인 엥겔스의 동상을 치우고 티무르의 동상을 올렸다. 카자흐스탄의 나자르바예프 대통령은 유목 민족 카자흐의 조상은 고대 스키타이인이며 이들은 흉노와 돌궐, 그리고 카자흐까지 이어졌다는 주장을 무려 자신의 이름으로 논문화하여 발표한다. \xa0\xa0투르크메니스탄의 나야조프 초대 대통령은 투르크멘 민족 창조까지는 시도하지 않았지만, 자신과 자신의 일가를 절대 우상화하여 다른 중앙아시아 국가와의 차별성을 강조했다. 이러한 역사 창조 과정에서 가장 소외된 국가는 타지키스탄이었다. 타지키스탄은 1992년 발생한 내전으로 국토의 3분의 1이 전쟁터화되고, 수많은 난민이 발생하면서 역사에 관심을 쏟을 여유가 없었다. 이 결과, 타지키스탄은 자신의 가장 중요한 유산인 중앙아시아의 황금시대를 우즈베키스탄에 그냥 넘겨주게 되었다. 우즈베키스탄은 자신의 역사가 실크로드의 역사라고 주장하지만, 이것은 전혀 사실에 맞지 않는다. 7세기부터 15세기까지 동서양을 연결하는 실크로드 역사의 주역은 타지크족인 소그드였으며, 이들은 페르시아의 문명을 받아들여 9세기에 세계 최고의 과학기술을 자랑했다.\xa0\xa0정세진 교수는 타지키스탄의 역사학자인 가푸로프와 마소프의 논문을 추적하여 타지키스탄이 실크로드, 부하라와 사마르칸트 황금시대의 주역이었음을 규명한다. 이에 대항하는 우즈베키스탄 역사학자는 타지크족이 산악 민족이라고 반박하지만, 이들의 주장은 별 설득력이 없다. 오늘날 사마르칸트 인구의 절반 이상은 타지크족이며 이들은 자신의 영혼은 부하라와 사마르칸트에 닿아 있다고 믿는다. 정세진 교수의 이 기념비적인 작품은 향후 후학들이 러시아어가 아닌 페르시아어, 큽착 몽골어 등으로 더 규명하여야 할 것이다. 이 책에서 가장 아쉬운 점은 결론 장이 없다는 점이다. 논쟁적인 주장들과 중앙아시아의 문명사적 특징을 결론에서 잘 정리했다면 독자들의 이해를 도왔을 것이다.\n') assert len(res) > 0 def test_cong_model(): - if sys.maxsize <= 2**32: + if sys.maxsize < 2**32: print("[skipped this test in 32bit OS.]", file=sys.stderr) return - kiwi = Kiwi(model_path='Kiwi/models/cong/base') + kiwi = Kiwi() assert kiwi.model_type in ('cong', 'cong-fp32') kiwi.tokenize('Cong 모델의 형태소 분석 테스트') - kiwi = Kiwi(model_path='Kiwi/models/cong/base', model_type='largest') + kiwi = Kiwi(model_type='largest') assert kiwi.model_type in ('cong-global', 'cong-global-fp32') kiwi.tokenize('Cong 모델의 형태소 분석 테스트') def test_cong_functions(): - kiwi = Kiwi(model_path='Kiwi/models/cong/base') + kiwi = Kiwi() sims = kiwi.most_similar_morphemes('언어', top_n=10) print(sims) assert len(sims) == 10 @@ -989,3 +1001,13 @@ def test_cong_functions(): sims = kiwi.predict_next_morpheme('오늘 점심은', bg_weight=0.5, top_n=10) print(sims) assert len(sims) == 10 + +def test_dialect(): + kiwi = Kiwi(enabled_dialects='jeju,archaic') + tokens = kiwi.tokenize("약주 ᄒᆞᆫ 잔 드셧수과?", allowed_dialects='jeju,archaic') + assert tokens[0].tagged_form == "약주/NNG" + assert tokens[1].tagged_form == "ᄒᆞᆫ/MM" + assert tokens[2].tagged_form == "잔/NNG" + assert tokens[3].tagged_form == "드시/VV" + assert tokens[4].tagged_form == "엇/EP" + assert tokens[5].tagged_form == "수과/EF" diff --git a/test/test_nogil_safety.py b/test/test_nogil_safety.py new file mode 100644 index 0000000..3cf31dc --- /dev/null +++ b/test/test_nogil_safety.py @@ -0,0 +1,53 @@ +from threading import Thread + +from kiwipiepy import Kiwi + +NUM_THREADS = 16 + +def test_multithread(): + print("Testing multi-threaded tokenization...") + kiwi = Kiwi() + def worker(results): + for _ in range(30000): + results.append(kiwi.tokenize("안녕하세요. 반갑습니다!")) + + all_results = [[] for _ in range(NUM_THREADS)] + threads = [] + for i in range(NUM_THREADS): + thread = Thread(target=worker, args=(all_results[i],)) + threads.append(thread) + + for thread in threads: + thread.start() + + for thread in threads: + thread.join() + + def _make_comparable(results): + return [' '.join(token.tagged_form for token in result) for result in results] + + ref = _make_comparable(all_results[0]) + for results in all_results: + assert _make_comparable(results) == ref + +def test_tokenize_with_adding(): + print("Testing tokenization with adding...") + kiwi = Kiwi() + def worker(results): + for _ in range(3000): + results.append(kiwi.tokenize("안녕하세요. 반갑습니다!")) + + all_results = [[] for _ in range(NUM_THREADS)] + threads = [] + for i in range(NUM_THREADS): + thread = Thread(target=worker, args=(all_results[i],)) + threads.append(thread) + + for thread in threads: + thread.start() + + for i in range(25): + kiwi.add_user_word(f"word{i:5}", "NNP") + + for thread in threads: + thread.join() diff --git a/test/test_transformers_addon.py b/test/test_transformers_addon.py index 9f4fd46..b7d7b6e 100644 --- a/test/test_transformers_addon.py +++ b/test/test_transformers_addon.py @@ -68,8 +68,8 @@ def test_pad(): def test_offset_mapping(): e = tokenizer("맞습니다요!", padding='max_length', max_length=8, return_offsets_mapping=True) - assert (e['input_ids'] == [2, 282, 64, 157, 85, 3, 0, 0]) - assert (e['offset_mapping'] == [(0, 0), (0, 1), (1, 4), (4, 5), (5, 6), (0, 0), (0, 0), (0, 0)]) + assert (e['input_ids'] == [2, 282, 27336, 85, 3, 0, 0, 0]) + assert (e['offset_mapping'] == [(0, 0), (0, 1), (1, 5), (5, 6), (0, 0), (0, 0), (0, 0), (0, 0)]) e = tokenizer("가자", "맞습니다요", return_offsets_mapping=True) assert (e['input_ids'] == [2, 75, 130, 3, 282, 64, 157, 3]) @@ -90,7 +90,7 @@ def test_decode(): def test_tokenize(): t = tokenizer.tokenize("맞습니다요!") - assert t == ["맞/V", "습니다/E", "요/J", "!"] + assert t == ["맞/V", "습니다요/E", "!"] def test_save_pretrained(): path = tempfile.gettempdir() + '/test_tokenizer'