Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
0e668f0
refactor(accent): replace greedy aligner with Needleman-Wunsch DP
torrid-fish May 21, 2026
8931d3e
feat(accent): regex reading-override layer + URL/non-JP preprocessing
torrid-fish May 21, 2026
bab83e8
feat(accent): add /MarkAccent/stream/ NDJSON endpoint + dev helpers
torrid-fish May 21, 2026
330c6bd
feat(accent): swap Yahoo MA for local fugashi + UniDic (refs #50)
torrid-fish May 21, 2026
0fa9214
docs(accent): extend package README for new package shape (refs #50)
torrid-fish May 21, 2026
8cf7a37
chore(spike): import spike artefacts — docs + probe scripts (refs #50)
torrid-fish May 21, 2026
83a476a
fix(accent): merge acronyms + decimals to keep morae aligned (refs #50)
torrid-fish May 21, 2026
97952cd
fix(accent): english_compound k=0 free, stop DP stealing morae (refs …
torrid-fish May 21, 2026
76d01b6
chore: drop Yahoo-era config/, .env, and spike scripts/docs (refs #50)
torrid-fish May 21, 2026
99c6623
feat(accent): symbol readings, okurigana subwords, scripts (refs #50)
torrid-fish May 21, 2026
64f54be
refactor(accent): drop merge_readable_symbol_compounds (refs #50)
torrid-fish May 21, 2026
e9f4ffe
fix(accent): preserve unit-compound furigana from toggle wipe (refs #50)
torrid-fish May 21, 2026
d9432b3
feat(accent): user patch table for OJAD/UniDic misreads (refs #50)
torrid-fish May 21, 2026
429f275
feat(accent): per-mora accent override in USER_PATCHES (refs #50)
torrid-fish May 21, 2026
436194e
refactor(accent): explicit per-mora int tuples in USER_PATCHES (refs …
torrid-fish May 21, 2026
bff2d49
fix(accent): free-consume override-synthesized tokens (refs #50)
torrid-fish May 21, 2026
aba22b9
fix(accent): clear katakana per-mora furigana when toggle off (refs #50)
torrid-fish May 21, 2026
a2c4188
docs(accent): rewrite api/accent/README.md in English (refs #50)
torrid-fish May 21, 2026
36dfbda
fix(docker): download UniDic dict at build to fix MarkAccent 500
torrid-fish May 27, 2026
a6ab1e3
chore: update ignore file and remove unused test scripts
torrid-fish May 29, 2026
e746364
chore: remove unused test
torrid-fish May 29, 2026
7bf63ec
docs: update root readme
torrid-fish May 29, 2026
c8f7811
feat: add version-selectable dict cwj and csj script
torrid-fish May 29, 2026
5b88135
fix(docker): install curl in builder stage for download_unidic.sh
torrid-fish Jun 4, 2026
6260235
fix(accent): propagate chunk warning through /MarkAccent/ aggregation
torrid-fish Jun 4, 2026
a68052c
fix(accent): make tagger singleton thread-safe with lru_cache
torrid-fish Jun 4, 2026
5b07a93
docs(accent): drop stray comment from USER_PATCHES schema table
torrid-fish Jun 4, 2026
eb72d9d
perf(accent): defer tokeniser-count debug log formatting
torrid-fish Jun 4, 2026
c509f70
fix(accent): cancel in-flight chunk tasks on client disconnect
torrid-fish Jun 4, 2026
faa06b6
feat(accent): ship in-process OpenJTalk as the sole accent backend
torrid-fish Jun 22, 2026
9102f37
fix(accent): correct per-mora furigana and punctuation drift; bake OJ…
torrid-fish Jun 23, 2026
aa7111e
style(accent): ruff-format align.py
torrid-fish Jul 10, 2026
1c46122
fix(accent): dedup morae by accent phrase; drop katakana-block punct
torrid-fish Jul 16, 2026
4ef0e23
fix(accent): run OpenJTalk off the event loop
torrid-fish Jul 16, 2026
d2e7f9a
refactor(accent): describe in-process OpenJTalk in pipeline/align com…
torrid-fish Jul 16, 2026
dbbf78a
docs: correct README backend description to in-process OpenJTalk
torrid-fish Jul 16, 2026
b0138e3
test(accent): cover fullcontext parser and openjtalk engine
torrid-fish Jul 16, 2026
d37a93e
fix(docker): install build-essential+cmake for pyopenjtalk
torrid-fish Jul 16, 2026
4a4db5c
ci: add PR docker-build gate validating offline dict bake
torrid-fish Jul 16, 2026
035efd8
fix(accent): offload tokeniser off the event loop and harden offline …
torrid-fish Jul 19, 2026
158f35e
chore: tidy .gitignore grouping and trailing newline
torrid-fish Jul 19, 2026
6b3bc1f
ci: add PR workflow for lint, type-check, and tests
torrid-fish Jul 19, 2026
3ce9457
docs(accent): drop OJAD references from the docs
torrid-fish Jul 19, 2026
9c3baf7
refactor(accent): rename OJAD to OpenJTalk in comment-only modules
torrid-fish Jul 19, 2026
b46acc4
refactor(accent): drop OJAD naming and raise _K_MAX in the aligner
torrid-fish Jul 19, 2026
dfb897d
refactor(accent): make the accent pipeline fully offline and bounded
torrid-fish Jul 19, 2026
8c01eab
chore(docker): drop local tooling dirs from the build context
torrid-fish Jul 19, 2026
fd61632
build(docker): harden runtime (read-only, no-new-privileges, TZ)
torrid-fish Jul 19, 2026
e67e190
fix(accent): preserve rewritten source surfaces
torrid-fish Jul 24, 2026
ebe7a60
fix(accent): improve alignment correctness and performance
torrid-fish Jul 24, 2026
cc6f917
fix(accent): refine contextual reading overrides
torrid-fish Jul 24, 2026
35e1f1c
refactor(accent): extract bounded chunk scheduling
torrid-fish Jul 24, 2026
27f5948
fix(accent): normalize tokenizer rendering behavior
torrid-fish Jul 24, 2026
64567f1
fix(accent): harden the OpenJTalk pipeline
torrid-fish Jul 24, 2026
ea5b942
fix(api): enforce accent request resource limits
torrid-fish Jul 24, 2026
36dfd4a
docs(accent): document offline request contracts
torrid-fish Jul 24, 2026
c549047
ci: gate releases on smoke-tested images
torrid-fish Jul 24, 2026
8bc3781
fix(accent): preserve suffix lexical metadata
torrid-fish Jul 26, 2026
6887aad
fix(accent): address chunking review feedback
torrid-fish Aug 4, 2026
4f67add
fix(build): verify UniDic archive checksums
torrid-fish Aug 4, 2026
9827bd8
test(accent): type disconnect request doubles
torrid-fish Aug 4, 2026
80aff6c
fix(accent): address review cancellation edge cases
torrid-fish Aug 9, 2026
4480fae
fix(ci): satisfy full-project mypy check
torrid-fish Aug 9, 2026
6f0875d
style(tests): apply ruff formatting
torrid-fish Aug 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,18 @@ __pycache__/
.venv/
.env
.env.*
!.env.example
tests/
README.md
Procfile
render.yaml
vercel.json

# Local tooling, scratch, and editor state — never needed in the image
.claude/
.claude
tmp/
data/
output/
.idea/
.vscode/
.zed/
2 changes: 0 additions & 2 deletions .env.example

This file was deleted.

61 changes: 54 additions & 7 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,22 @@ on: # yamllint disable-line rule:truthy
workflow_dispatch:

permissions:
actions: read
contents: read
packages: write

concurrency:
group: cd
cancel-in-progress: true
group: cd-${{ github.ref }}
cancel-in-progress: ${{ !startsWith(github.ref, 'refs/tags/v') }}

jobs:
tests:
name: Validate source
uses: ./.github/workflows/tests.yml

build-and-push:
name: Build and push image to GHCR
needs: [tests]
runs-on: ubuntu-latest
env:
REGISTRY: ghcr.io
Expand All @@ -26,6 +32,23 @@ jobs:
# Must match deploy/compose.yml's api-tools image in jpcorrect-backend.
IMAGE_NAME: sessatakuma/api-tools
steps:
- name: Wait for earlier CD runs before publishing a release
if: startsWith(github.ref, 'refs/tags/v')
env:
GH_TOKEN: ${{ github.token }}
run: |
while true; do
blocking_run=$(gh api \
"repos/${GITHUB_REPOSITORY}/actions/workflows/cd.yml/runs?per_page=100" \
--jq ".workflow_runs[] | select(.run_number < ${GITHUB_RUN_NUMBER} and (.status == \"queued\" or .status == \"in_progress\")) | .id" \
| head -n 1)
if [[ -z "$blocking_run" ]]; then
break
fi
echo "Waiting for earlier CD run $blocking_run"
sleep 15
done

- name: Checkout
uses: actions/checkout@v6

Expand Down Expand Up @@ -54,16 +77,40 @@ jobs:
type=semver,pattern={{major}}.{{minor}}
type=raw,value=stable,enable=${{ startsWith(github.ref, 'refs/tags/v') }}

- name: Build and push
- name: Build image
uses: docker/build-push-action@v7
with:
context: .
file: ./Dockerfile # adjust if the api-tools Dockerfile path differs
file: ./Dockerfile
platforms: linux/amd64
push: true
provenance: true
sbom: true
push: false
load: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Smoke-test production lifecycle offline
run: |
image=$(printf '%s\n' '${{ steps.meta.outputs.tags }}' | head -n 1)
container=api-tools-release-smoke-${{ github.run_id }}
trap 'docker rm -f "$container" >/dev/null 2>&1 || true' EXIT
docker run -d --name "$container" --network none --read-only --tmpfs /tmp \
-e PYTHONDONTWRITEBYTECODE=1 \
"$image"
for attempt in $(seq 1 30); do
if docker exec "$container" python -c \
"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/openapi.json', timeout=2)"; then
break
fi
if [[ "$attempt" == 30 ]]; then
docker logs "$container"
exit 1
fi
sleep 1
done
docker exec "$container" python -c \
"import json, urllib.request; request=urllib.request.Request('http://127.0.0.1:8000/api/MarkAccent/', data=json.dumps({'text':'東京'}).encode(), headers={'Content-Type':'application/json'}); response=json.load(urllib.request.urlopen(request, timeout=30)); assert response['status'] == 200 and response['result']"

- name: Push smoke-tested image
run: docker push --all-tags "${REGISTRY}/${IMAGE_NAME}"
69 changes: 69 additions & 0 deletions .github/workflows/docker-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
---
name: Docker Build Check

# Build the image on PRs (no push) so a broken Dockerfile — most importantly
# the dict-bake layers that make the runtime self-contained/offline
# (open_jtalk_dic via pyopenjtalk, and the UniDic dict — ~700 MB compressed
# download, ~1.3 GB installed — from scripts/download_unidic.sh) — fails the
# PR instead of the release build.
# The smoke-test step then runs the built image with `--network none` and
# exercises BOTH baked dictionaries: the OpenJTalk open_jtalk_dic (via
# pyopenjtalk) AND the fugashi/UniDic dicdir (via the tokeniser). A broken
# or missing download of either dict fails the gate offline.
on: # yamllint disable-line rule:truthy
workflow_call:
pull_request:
branches: ["main", "dev"]
workflow_dispatch:

permissions:
contents: read

concurrency:
group: docker-build-${{ github.ref }}
cancel-in-progress: true

jobs:
build:
name: Build image (no push)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4

- name: Build image
uses: docker/build-push-action@v7
with:
context: .
file: ./Dockerfile
platforms: linux/amd64
push: false
load: true
tags: api-tools:pr-${{ github.event.pull_request.number || github.run_id }}
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Smoke-test the accent pipeline in the built image
run: |
image=api-tools:pr-${{ github.event.pull_request.number || github.run_id }}
container=api-tools-smoke-${{ github.run_id }}
trap 'docker rm -f "$container" >/dev/null 2>&1 || true' EXIT
docker run -d --name "$container" --network none --read-only --tmpfs /tmp \
-e PYTHONDONTWRITEBYTECODE=1 \
"$image"
for attempt in $(seq 1 30); do
if docker exec "$container" python -c \
"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/openapi.json', timeout=2)"; then
break
fi
if [[ "$attempt" == 30 ]]; then
docker logs "$container"
exit 1
fi
sleep 1
done
docker exec "$container" python -c \
"import json, urllib.request; request=urllib.request.Request('http://127.0.0.1:8000/api/MarkAccent/', data=json.dumps({'text':'東京'}).encode(), headers={'Content-Type':'application/json'}); response=json.load(urllib.request.urlopen(request, timeout=30)); assert response['status'] == 200 and response['result']"
51 changes: 51 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---
name: Tests

# Runs lint, type-check, and the unit test suite on every PR. Unit tests mock
# the UniDic boundary, while the mandatory Docker build check exercises both
# real dictionaries offline. This keeps the fast gate independent of the
# ~1.3 GB UniDic download without losing integration coverage.
on: # yamllint disable-line rule:truthy
workflow_call:
pull_request:
branches: ["main", "dev"]
workflow_dispatch:

permissions:
contents: read

concurrency:
group: tests-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
name: Lint, type-check, pytest
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Install uv
uses: astral-sh/setup-uv@v5
with:
version: "0.9.0"

- name: Install build toolchain for pyopenjtalk
# pyopenjtalk ships source-only and builds its bundled OpenJTalk/
# HTS-engine C++ from sdist during `uv sync` (same as the Dockerfile).
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends build-essential cmake

- name: Install dependencies
run: uv sync --frozen

- name: Ruff
run: uv run ruff check .

- name: Mypy
run: uv run mypy api

- name: Pytest
run: uv run pytest -q
13 changes: 11 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -215,5 +215,14 @@ __marimo__/
# Streamlit
.streamlit/secrets.toml

# Secrest files
secret*
# Secret files
secret*

# Local data / generated output
data/
output/

# AI assistant local config (not shared)
CLAUDE.md
AGENTS.md
.claude
46 changes: 41 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,51 @@ FROM python:3.11-slim AS builder

WORKDIR /app

# Install uv for fast dependency management (pinned for reproducible builds)
RUN pip install --no-cache-dir "uv==0.9.0"
# Install uv for fast dependency management (pinned for reproducible builds),
# curl for downloading the NINJAL UniDic archive (scripts/download_unidic.sh
# line ~74), and unzip for extracting it (Deflate64-compressed).
#
# build-essential (g++/make) and cmake are required to compile pyopenjtalk:
# it ships source-only on PyPI (no wheels for any arch) and builds its bundled
# OpenJTalk/HTS-engine C++ from sdist during `uv sync` below. Without them the
# build fails at cmake configure ("no CXX compiler"). This is a builder-stage
# cost only — the toolchain never lands in the slim runtime image.
RUN pip install --no-cache-dir "uv==0.9.0" \
&& apt-get update \
&& apt-get install -y --no-install-recommends \
curl unzip build-essential cmake \
&& rm -rf /var/lib/apt/lists/*

# Copy dependency files first for better layer caching
COPY pyproject.toml uv.lock ./

# Install dependencies using uv
RUN uv sync --frozen --no-dev --no-install-project

# Download the OpenJTalk dictionary (open_jtalk_dic_utf_8 ~23 MB) into the
# venv at build time. pyopenjtalk fetches it lazily on first frontend call;
# baking it here means the runtime (unprivileged appuser, read-only app dir,
# possibly offline) never has to download it. It lands inside the pyopenjtalk
# package under /app/.venv, which is copied wholesale into the final image.
# Own layer (gated by uv.lock) so app-code edits don't bust the download.
RUN /app/.venv/bin/python -c "import pyopenjtalk; pyopenjtalk.extract_fullcontext('テスト')"

# Download and install the UniDic dictionary into the venv so the image is
# self-contained — the `unidic` pip package ships the loader but not the
# dicdir, and fugashi.Tagger() fails at runtime without it. UniDic CWJ is
# a ~700 MB compressed download that expands to ~1.3 GB installed.
# Uses scripts/download_unidic.sh so local dev and Docker share the same
# download logic. Kept as its own layer (before COPY . .) so app-code
# edits don't bust the download cache; only re-runs when the script or
# uv.lock changes.
COPY scripts/download_unidic.sh scripts/
RUN VIRTUAL_ENV=/app/.venv bash scripts/download_unidic.sh

COPY . .

# Compile only our app code (skip .venv) and drop the .py sources
RUN python -m compileall -b -q main.py api config \
&& find main.py api config -name "*.py" -delete
RUN python -m compileall -b -q main.py api \
&& find main.py api -name "*.py" -delete

FROM python:3.11-slim

Expand All @@ -30,11 +61,16 @@ WORKDIR /app
COPY --from=builder /app/.venv /app/.venv
COPY --from=builder /app/main.pyc /app/main.pyc
COPY --from=builder /app/api /app/api
COPY --from=builder /app/config /app/config

# Set PATH to use venv binaries
ENV PATH="/app/.venv/bin:$PATH"
ENV PYTHONUNBUFFERED=1
# Don't attempt to write .pyc caches at runtime — app code is already compiled
# to .pyc in the builder, and this lets the container run with a read-only
# rootfs (see compose.deploy.yml) without Python tripping on cache writes.
ENV PYTHONDONTWRITEBYTECODE=1
# Local timezone so log timestamps match the deployment (tzdata ships in slim).
ENV TZ=Asia/Taipei

EXPOSE 8000

Expand Down
Loading
Loading