Skip to content

feat(swift-g2p): iOS Swift G2P 対応 + SPM product 配布 (#387) - #398

Merged
ayutaz merged 17 commits into
devfrom
feat/swift-g2p-387
May 8, 2026
Merged

feat(swift-g2p): iOS Swift G2P 対応 + SPM product 配布 (#387)#398
ayutaz merged 17 commits into
devfrom
feat/swift-g2p-387

Conversation

@ayutaz

@ayutaz ayutaz commented May 6, 2026

Copy link
Copy Markdown
Owner

Summary

Issue #387 — iOS / Swift から piper-plus の G2P を ONNX Runtime 非依存で単独利用可能に。

  • 新 SPM product: PiperPlusG2P (Sources/PiperPlusG2P/{Phonemizer,Language,G2PError,PhonemizeResult}.swift)
  • 新 xcframework: libpiper_plus_g2p-ios-v${VERSION}.xcframework.zip (合成エンジンと別配布、ORT 非依存、~3-5MB zip)。device arm64 + simulator arm64+x86_64 の universal slice
  • Rust 側拡張: piper-plus-g2p crate に [lib] crate-type = ["staticlib", "cdylib", "rlib"] 追加、bundled-dicts feature で cmudict / pinyin / NAIST-JDIC を include_str!/include_bytes! で埋込 (iOS App Sandbox 対応)、piper_plus_g2p_* 5 関数 C FFI、cbindgen.toml で C ヘッダ自動生成
  • CI: release-shared-lib.ymlbuild-g2p-ios matrix + assemble-g2p-xcframework ジョブ追加。Package.swiftg2pChecksum と released zip の sha256 を verify
  • 横断的整合: G2P JSON 辞書の source of truth を src/cpp/ から src/rust/piper-plus-g2p/data/ に統一 (CMake 2 ファイル + CI 3 ファイル更新)
  • ライセンス: THIRD_PARTY_LICENSES.md に CMU dict (BSD-style) と pypinyin (MIT) の attribution セクションを追加
  • ドキュメント: 仕様 (docs/spec/swift-g2p.md)、利用ガイド (docs/guides/swift-g2p-integration.md)、決定記録 (docs/spec/swift-g2p-decisions.md)、FFI/ABI 契約 (docs/spec/swift-g2p-contract.toml) を新設。README (ja/en) / ios-integration.md / crate README に Swift G2P (SPM) への参照を追加
  • テスト: 既存の Rust FFI テスト 4 件 (en/ja/zh bundled + 構造) に加え、Swift 側に PhonemizerTests (12)、GoldenPhonemeTests (8 言語 cross-runtime fixture)、ConcurrencyTests (60 並列 phonemize / 20 並列 init / 100 反復 deinit) を追加

総コミット数: 6。既存テスト 396 lib + 22 integration tests いずれも pass。

Test plan

  • cargo test -p piper-plus-g2p --features all-languages,naist-jdic,bundled-dicts,ffi — 396 lib + 22 integration tests pass
  • cargo build -p piper-plus-g2p --target aarch64-apple-ios --features all-languages,naist-jdic,bundled-dicts,ffi — 84 MB staticlib 生成、piper_plus_g2p_* 5 シンボル定義済を nm で確認
  • CI: release-shared-lib.yml build-g2p-ios (device + simulator) → assemble-g2p-xcframeworkrelease の dry run (workflow_dispatch)
  • リリース後 (タグ push 後): Package.swiftg2pChecksum を実 zip の sha256 に更新、swift testPhonemizerTests / GoldenPhonemeTests / ConcurrencyTests が pass
  • 実機/シミュレータでの動作確認 (examples/swift/ の差分または別途 sample app)
  • App Store 提出時の PrivacyInfo.xcprivacy 認可確認 (G2P xcframework は合成エンジンと PrivacyInfo を共有)

Closes #387.

Copilot AI review requested due to automatic review settings May 6, 2026 15:30
@ayutaz ayutaz self-assigned this May 7, 2026
ayutaz added a commit that referenced this pull request May 7, 2026
PR #398 で 3 件の CI が失敗していた根本原因:

1. Swift G2P CI (macOS) — nm の最終 pipeline が pipefail で exit 1
   `nm -gU staticlib.a | grep | sort -u` の最終 pipeline が、staticlib に
   外部 symbol を持たない archive member を含むときに macOS nm が非ゼロを
   返し、`set -euo pipefail` で step 全体が失敗していた。出力には FFI
   symbol 5 つすべて表示されており、検証自体は成功していた。
   `nm -gU ... || true` で出力をキャプチャしてから for-loop で symbol を
   個別検証する形に変更。

2. Build piper-plus-g2p iOS ios-arm64 / ios-arm64_x86_64-simulator —
   release-shared-lib.yml の verify step も同じ形(最終 pipeline に
   `|| true` が無い)で失敗。同じ for-loop 検証へ統一。

3. test-cpp-inference (Docker) — cpp-inference/Dockerfile の builder
   stage が `src/rust/piper-plus-g2p/data/` を COPY していないため、
   cmake/Install.cmake:25 が cmudict_data.json / pinyin_*.json を install
   する際に「No such file or directory」で失敗。data ディレクトリの
   COPY を追加。
ayutaz added a commit that referenced this pull request May 7, 2026
PR #398 を 4 並列レビューエージェントで監査した結果の不整合・盲点を一括修正。
コピペで動かなかったコード片、未配信の version 参照、検出機構の抜け、契約強度
不足を中心に対処。

Critical (5):
- C1: G2PError のドキュメントが実装と不一致 (`unsupportedLanguage` 等の
  存在しないケース) を修正。guide / spec / troubleshooting テーブルを実装の
  4 ケース (initializationFailed / phonemizeReturnedNull / invalidUTF8 /
  decodeFailed) に揃え、各ケースの意味を明記。
- C2: Package.swift `g2pVersion` を `1.13.0` から `1.14.0` に bump し、
  guide / CLAUDE.md / decisions.md と整合。`g2pChecksum` が placeholder
  の意味と consumer がタグ前にビルドしようとした時の挙動をコメントで明示。
- C3: spec の cargo build 例 / CI yaml 例から `bundled-dicts,ffi` features
  が抜けていたのを補完 (decisions.md に書いてあるが実例では消えていた)。
- C4: PUA consistency gate に Swift PUAMap を追加。
  scripts/check_pua_consistency.py に parse_swift() 実装、
  pua-consistency.yml の paths に Sources/PiperPlusG2P/PUAMap.swift を追加、
  job 名を "All 7 runtime PUA tables" に更新。実行で 99 entries / version=2
  が他 6 ランタイムと一致を確認。Issue #389 の再発防止構造。
- C5: swift-g2p-ci.yml の Package.swift heredoc 上書きを Package.ci.swift
  (tracked file) 化。リリース版との drift が git diff で見える形に。
  iOS(.v15) を platforms に追加して iOS-only API の compile 検証を CI でも
  走らせる。

High (8):
- H1: crate README の C FFI 関数名 `destroy` / `version` が実装に存在しない
  → 実体 (create / phonemize / free_string / free / available_languages)
  + 各関数の所有権・戻り値を表で明示。
- H2: THIRD_PARTY_LICENSES の `new_from_path()` / `new_from_paths()` が
  実装に存在しない → 実体 (`new_with_dict` / `new(p1, p2)`) に修正。
- H3: docs/spec/swift-g2p.md:5 の Issue URL に全角カタカナ「ピ」混入で 404 →
  ASCII piper に修正。
- H4: swift-g2p-contract.toml に [compat] / [compat.breaking_change_policy]
  セクションを新設。pua_compat_version への参照、symbol 削除 / シグネチャ
  変更 / JSON envelope 追加・削除 / PUA bump / default 言語変更で何が
  壊れるかを明文化。
- H5: tests/PiperPlusG2PTests/PUAMapTests.swift を新規作成。
  fixedMap.count == 99、compatVersion == 2、PUA 範囲、コードポイント衝突、
  トークン衝突、forward/reverse round-trip、unknown lookup の 8 アサーション。
  シンボリックな count も差分で検出される。
- H6: swift-g2p-ci.yml に Thread Sanitizer ジョブを追加 (continue-on-error
  でまずは observability 優先)。`@unchecked Sendable` の主張に裏付けが
  生まれるよう ConcurrencyTests を `--sanitize=thread` で再実行。
- H7: ConcurrencyTests.testRepeatedInitDeinit にアサーションを追加。
  各 iteration の availableLanguages 検証 + 10 反復ごとに phonemize して
  pipeline の健全性確認 (元の loop は assertion ゼロでノーオペ化していた)。
- H8 (M1 兼用): testAvailableLanguagesRoundTripsRequestedSet を追加。
  Rust が新言語を返したとき Swift の Language enum が落とす silent drop
  を検出。

Medium (5):
- M3: バイナリサイズ表記を guide で 1 表に統合。xcframework.zip ダウンロード
  / app 増分 / 未 strip staticlib の 3 観点を区別して記載。各言語のサイズ
  寄与も実測値 (cmudict 3.7MB / pinyin 2.6MB / NAIST-JDIC 20MB) に揃えた。
- M4: NAIST-JDIC ライセンス全文 (BSD-3-Clause) と osdn.net 公式 URL を
  THIRD_PARTY_LICENSES.md に追加。guide でも表記揺れを解消し、
  attribution 義務の有無を整理。
- M5: english.rs / chinese.rs の OnceLock+expect が catch_unwind 後に poison
  状態で残る挙動をコメントで明示。iOS 経路 (new_bundled) は到達しないこと
  を併記して FFI consumer の安心材料に。
- M6: testEmptyTextDoesNotCrash の `try?` で例外握りつぶしを止め、
  Rust 側仕様 (空入力 → 成功 or phonemizeReturnedNull のいずれか) を
  Swift にも明示反映。
- L: integration guide の言語表に「学習済みモデルは 6 言語」「ko/sv は
  G2P 出力できても TTS モデルがない」旨の注釈を追加。

検証:
- python scripts/check_pua_consistency.py --verbose --check-version
  → 7 ランタイム全て 99 entries / version=2 一致
- cargo test -p piper-plus-g2p --features all-languages,naist-jdic,
  bundled-dicts,ffi --lib → 403 passed / 0 failed
- yaml.safe_load で 2 ワークフロー syntax ok
@ayutaz
ayutaz force-pushed the feat/swift-g2p-387 branch from 42171c7 to 51aa5d6 Compare May 7, 2026 06:42
ayutaz added a commit that referenced this pull request May 7, 2026
PR #398 で 3 件の CI が失敗していた根本原因:

1. Swift G2P CI (macOS) — nm の最終 pipeline が pipefail で exit 1
   `nm -gU staticlib.a | grep | sort -u` の最終 pipeline が、staticlib に
   外部 symbol を持たない archive member を含むときに macOS nm が非ゼロを
   返し、`set -euo pipefail` で step 全体が失敗していた。出力には FFI
   symbol 5 つすべて表示されており、検証自体は成功していた。
   `nm -gU ... || true` で出力をキャプチャしてから for-loop で symbol を
   個別検証する形に変更。

2. Build piper-plus-g2p iOS ios-arm64 / ios-arm64_x86_64-simulator —
   release-shared-lib.yml の verify step も同じ形(最終 pipeline に
   `|| true` が無い)で失敗。同じ for-loop 検証へ統一。

3. test-cpp-inference (Docker) — cpp-inference/Dockerfile の builder
   stage が `src/rust/piper-plus-g2p/data/` を COPY していないため、
   cmake/Install.cmake:25 が cmudict_data.json / pinyin_*.json を install
   する際に「No such file or directory」で失敗。data ディレクトリの
   COPY を追加。
ayutaz added a commit that referenced this pull request May 7, 2026
PR #398 を 4 並列レビューエージェントで監査した結果の不整合・盲点を一括修正。
コピペで動かなかったコード片、未配信の version 参照、検出機構の抜け、契約強度
不足を中心に対処。

Critical (5):
- C1: G2PError のドキュメントが実装と不一致 (`unsupportedLanguage` 等の
  存在しないケース) を修正。guide / spec / troubleshooting テーブルを実装の
  4 ケース (initializationFailed / phonemizeReturnedNull / invalidUTF8 /
  decodeFailed) に揃え、各ケースの意味を明記。
- C2: Package.swift `g2pVersion` を `1.13.0` から `1.14.0` に bump し、
  guide / CLAUDE.md / decisions.md と整合。`g2pChecksum` が placeholder
  の意味と consumer がタグ前にビルドしようとした時の挙動をコメントで明示。
- C3: spec の cargo build 例 / CI yaml 例から `bundled-dicts,ffi` features
  が抜けていたのを補完 (decisions.md に書いてあるが実例では消えていた)。
- C4: PUA consistency gate に Swift PUAMap を追加。
  scripts/check_pua_consistency.py に parse_swift() 実装、
  pua-consistency.yml の paths に Sources/PiperPlusG2P/PUAMap.swift を追加、
  job 名を "All 7 runtime PUA tables" に更新。実行で 99 entries / version=2
  が他 6 ランタイムと一致を確認。Issue #389 の再発防止構造。
- C5: swift-g2p-ci.yml の Package.swift heredoc 上書きを Package.ci.swift
  (tracked file) 化。リリース版との drift が git diff で見える形に。
  iOS(.v15) を platforms に追加して iOS-only API の compile 検証を CI でも
  走らせる。

High (8):
- H1: crate README の C FFI 関数名 `destroy` / `version` が実装に存在しない
  → 実体 (create / phonemize / free_string / free / available_languages)
  + 各関数の所有権・戻り値を表で明示。
- H2: THIRD_PARTY_LICENSES の `new_from_path()` / `new_from_paths()` が
  実装に存在しない → 実体 (`new_with_dict` / `new(p1, p2)`) に修正。
- H3: docs/spec/swift-g2p.md:5 の Issue URL に全角カタカナ「ピ」混入で 404 →
  ASCII piper に修正。
- H4: swift-g2p-contract.toml に [compat] / [compat.breaking_change_policy]
  セクションを新設。pua_compat_version への参照、symbol 削除 / シグネチャ
  変更 / JSON envelope 追加・削除 / PUA bump / default 言語変更で何が
  壊れるかを明文化。
- H5: tests/PiperPlusG2PTests/PUAMapTests.swift を新規作成。
  fixedMap.count == 99、compatVersion == 2、PUA 範囲、コードポイント衝突、
  トークン衝突、forward/reverse round-trip、unknown lookup の 8 アサーション。
  シンボリックな count も差分で検出される。
- H6: swift-g2p-ci.yml に Thread Sanitizer ジョブを追加 (continue-on-error
  でまずは observability 優先)。`@unchecked Sendable` の主張に裏付けが
  生まれるよう ConcurrencyTests を `--sanitize=thread` で再実行。
- H7: ConcurrencyTests.testRepeatedInitDeinit にアサーションを追加。
  各 iteration の availableLanguages 検証 + 10 反復ごとに phonemize して
  pipeline の健全性確認 (元の loop は assertion ゼロでノーオペ化していた)。
- H8 (M1 兼用): testAvailableLanguagesRoundTripsRequestedSet を追加。
  Rust が新言語を返したとき Swift の Language enum が落とす silent drop
  を検出。

Medium (5):
- M3: バイナリサイズ表記を guide で 1 表に統合。xcframework.zip ダウンロード
  / app 増分 / 未 strip staticlib の 3 観点を区別して記載。各言語のサイズ
  寄与も実測値 (cmudict 3.7MB / pinyin 2.6MB / NAIST-JDIC 20MB) に揃えた。
- M4: NAIST-JDIC ライセンス全文 (BSD-3-Clause) と osdn.net 公式 URL を
  THIRD_PARTY_LICENSES.md に追加。guide でも表記揺れを解消し、
  attribution 義務の有無を整理。
- M5: english.rs / chinese.rs の OnceLock+expect が catch_unwind 後に poison
  状態で残る挙動をコメントで明示。iOS 経路 (new_bundled) は到達しないこと
  を併記して FFI consumer の安心材料に。
- M6: testEmptyTextDoesNotCrash の `try?` で例外握りつぶしを止め、
  Rust 側仕様 (空入力 → 成功 or phonemizeReturnedNull のいずれか) を
  Swift にも明示反映。
- L: integration guide の言語表に「学習済みモデルは 6 言語」「ko/sv は
  G2P 出力できても TTS モデルがない」旨の注釈を追加。

検証:
- python scripts/check_pua_consistency.py --verbose --check-version
  → 7 ランタイム全て 99 entries / version=2 一致
- cargo test -p piper-plus-g2p --features all-languages,naist-jdic,
  bundled-dicts,ffi --lib → 403 passed / 0 failed
- yaml.safe_load で 2 ワークフロー syntax ok
ayutaz added 13 commits May 8, 2026 00:31
Issue #387 (Swift 向け G2P 対応) の実装着手前ドキュメントを整備。
方針 1 (既存 Rust piper-plus-g2p の C FFI を Swift から呼ぶ) を採用。

- docs/spec/swift-g2p.md
  配布仕様。独立 xcframework (`libpiper_plus_g2p-ios.xcframework.zip`)
  として合成エンジンと分離配布する。Cargo + lipo で iOS slice を
  ビルド、cbindgen で C ヘッダ自動生成、`Package.swift` に
  `PiperPlusG2P` product を追加。8 言語フル (ja/en/zh/ko/es/fr/pt/sv) 対応。

- docs/guides/swift-g2p-integration.md
  iOS 開発者向け SPM 統合ガイド。`Phonemizer` Swift API、
  単一/多言語/SwiftUI のコード例、トラブルシュート、ライセンス。

- docs/spec/swift-g2p-decisions.md
  実装着手前のチェックリスト全 15 項目。重大な盲点として
  EN (cmudict 3.7MB) / ZH (pinyin 2.7MB) の辞書が現状 disk 依存で
  iOS App Sandbox では動かない件を指摘。`bundled-dicts` feature
  新設による include_str! / include_bytes! 埋込を推奨。
Issue #387 (Swift 向け G2P 対応) の実装着手前検証。
Rust crate 側の必要変更を最小限のプロトタイプで実証し、
iOS 全 slice (device + simulator universal) でビルド成功を確認。

検証結果:
- aarch64-apple-ios            : 26s, libpiper_plus_g2p.a 84MB
- aarch64-apple-ios-sim        : 18s
- x86_64-apple-ios             : 18s
- lipo (sim universal)         : 168MB
- bundled-dicts feature 有効   : 91MB (+6.4MB ≈ JSON 実サイズ)
- 既存 unit test 373 件全 pass : 後方互換確認
- runtime smoke test           : EN "Hello world" / ZH "你好世界" 両方とも IPA 出力 OK
- cbindgen header 自動生成     : piper_plus_g2p.h 70 行 (5 関数 + opaque struct)

変更内容:
- Cargo.toml
  + [lib] crate-type = ["staticlib", "cdylib", "rlib"]
  + bundled-dicts feature 新設 (default 非含有)
- src/english.rs
  + EnglishPhonemizer::new_bundled() (#[cfg(feature = "bundled-dicts")])
    cmudict_data.json を include_str! で埋込
- src/chinese.rs
  + ChinesePhonemizer::new_bundled() (#[cfg(feature = "bundled-dicts")])
    pinyin_{single,phrases}.json を include_bytes! で埋込
- cbindgen.toml 新設 (C ヘッダ自動生成設定)
- data/ 新設 (cmudict + pinyin JSON、src/cpp/ から複製、計 6.4MB)

TODO (follow-up):
- data/ の重複解消: build.rs か Cargo の package.include で
  src/cpp/ の JSON を参照する設計に切替 (現状は単純コピー)
- ffi.rs: register_one("zh") で new_bundled() を呼ぶ分岐追加
- ffi.rs: register_one("en") も new_bundled() に切替 (CMUDICT_PATH より優先)
- cbindgen.toml: PUA_COMPAT_VERSION 定数を [export.exclude] で除外
- Sources/PiperPlusG2P/, Package.swift, release-shared-lib.yml は別 PR
Issue #387 (Swift 向け G2P 対応) の実装本体。
Rust piper-plus-g2p crate の C FFI を Swift から呼ぶ Plan A 採用。
合成エンジン (PiperPlus, ORT 必須) と独立した PiperPlusG2P product を
追加し、ORT 非依存で 8 言語の G2P を Swift から利用可能にする。

Step 1 — ffi.rs 拡張:
- register_one("zh") を bundled-dicts feature 有効時に
  ChinesePhonemizer::new_bundled() 経由に変更 (従来は登録不可)
- register_one("en") も bundled-dicts 有効時に new_bundled() 優先
- register_one("ja") は naist-jdic feature 有効時に new_bundled() 経由
- ffi tests に Chinese/English/Japanese bundled テスト追加 (396 件全 pass)
- cbindgen.toml に [export.exclude] PUA_COMPAT_VERSION 追加

Step 2 — Sources/PiperPlusG2P/:
- Language.swift   : enum Language (8 言語、CaseIterable + Sendable)
- G2PError.swift   : enum G2PError (initFailed/null/utf8/decode + 説明)
- PhonemizeResult.swift: struct PhonemizeResult (Codable, Sendable)
- Phonemizer.swift : final class Phonemizer
    init(languages:) → piper_plus_g2p_create
    phonemize(_:language:) → JSON → PhonemizeResult
    availableLanguages → piper_plus_g2p_available_languages
    deinit → piper_plus_g2p_free

Step 3 — Package.swift:
- 新 product .library(name: "PiperPlusG2P", ...)
- 新 binaryTarget PiperPlusG2PBinary
  (libpiper_plus_g2p-ios-v${VERSION}.xcframework.zip)
- g2pVersion / g2pChecksum を追加 (合成側 checksum と同じ手動更新運用)
- testTarget PiperPlusG2PTests を path: "tests/PiperPlusG2PTests" で追加
  (既存 tests/ ディレクトリと整合、Linux 大文字小文字区別対応)

Step 4 — release-shared-lib.yml:
- build-g2p-ios job: ios-arm64 / ios-arm64_x86_64-simulator の matrix
  cargo build (3 target) → lipo (sim universal) → cbindgen → modulemap
  rustup targets, cargo cache, cbindgen cache 設定
  FFI シンボル 5 個の export を nm で検証
- assemble-g2p-xcframework job: xcodebuild -create-xcframework で
  libpiper_plus_g2p-ios.xcframework.zip 生成、PrivacyInfo.xcprivacy 同梱
- release job: needs に assemble-g2p-xcframework 追加、
  Package.swift の g2pChecksum 検証、xcframework zip との sha256 突合
  artifact rename + Releases upload

Step 5 — README / CHANGELOG:
- CLAUDE.md ランタイム表に "iOS Swift G2P (SPM)" 行追加
- CHANGELOG.md [Unreleased] に PiperPlusG2P エントリ追加
  (関連: docs/spec/swift-g2p.md, docs/guides/swift-g2p-integration.md)

検証実施結果:
- cargo test (all-languages,naist-jdic,bundled-dicts,ffi) — 396 件 pass
- cargo build aarch64-apple-ios (release) — 成功
  libpiper_plus_g2p.a に piper_plus_g2p_* 5 シンボル全て export 確認
- YAML syntax check (release-shared-lib.yml) — pass

未対応 (follow-up):
- 初回リリース時は g2pChecksum を手動更新 (placeholder 0000... のまま)
- Sources/PiperPlusG2P/ の SourceKit 診断エラーは binaryTarget 未 resolve
  のため (xcframework 配信後に解消)
- examples/swift/G2PExample/ サンプルアプリは別 issue 切出し
- data/ JSON と src/cpp/ JSON の重複解消 (build.rs か symlink)
既存の tests/fixtures/g2p/phoneme_test_cases.json (Python/Rust/JS で
共有されている cross-platform G2P 整合性フィクスチャ) を Swift wrapper
からも参照し、同じ structural assertion を適用。

- Tests/PiperPlusG2PTests/GoldenPhonemeTests.swift 新設
- #filePath で source-relative にフィクスチャを解決 (コピー / 重複なし、
  source of truth は tests/fixtures/g2p/ のまま)
- 81 ケース (ja:7, en:3, zh:10, ko:4, es:15, fr:15, pt:15, sv:12) を
  言語別 testN メソッドで実行
- 適用する assertion:
  - expected_token_count_min  : トークン数下限
  - expected_contains         : 必須トークン
  - expected_not_contains     : 禁止トークン
  - expected_tokens           : 完全一致 (rule-based 言語向け)
  - expected_contains_any_tone: PUA codepoint 含有 (中国語声調)
  - expected_has_question_marker: 疑問詞マーカー (日本語 ?-suffix)

これにより Python/Rust/JS と Swift で同じ入力に対して同じ structural
output が得られることを CI でガードできる (xcframework 配信後)。

XCTest 診断の SourceKit 警告は binaryTarget 未 resolve のため
(ローカル swift build では XCTest が見えない、CI で resolve)。
prototype commit (00f4b95) で src/cpp/{cmudict,pinyin}*.json を
src/rust/piper-plus-g2p/data/ にコピーしてしまった重複を解消。
新しい source of truth は piper-plus-g2p crate 内 data/ に統一。

C++ コード (piper.cpp, chinese_phonemize.hpp) はファイル名のみで
参照しており、ランタイムで findDictionaryFile() がパスを解決する設計
だったため、ソースツリー上の場所変更は CMake / CI の cp パスの
更新のみで完了。

更新ファイル:
- src/cpp/cmudict_data.json         : 削除 (data/ に移動済)
- src/cpp/pinyin_single.json        : 削除 (同上)
- src/cpp/pinyin_phrases.json       : 削除 (同上)
- cmake/PiperPlusShared.cmake       : install paths を data/ 参照に
- cmake/Install.cmake                : 同上
- .github/workflows/build-piper.yml : Linux / macOS / Windows の cp 更新
- .github/workflows/docker-test.yml : test/models/ + fallback の cp 更新
- .github/workflows/test-multilingual-tts.yml : test/models/ の cp 更新

これにより:
- piper-plus-g2p crate の include_str! / include_bytes! は変更不要
  (data/ のまま、相対パスは crate 内で完結)
- C++ install ターゲットも変更前と同じパス (share/piper/dicts/) に
  正しく辞書を配置
- リポジトリ全体での重複 (~6.4MB の git tracked dup) を解消

検証:
- cargo test (all-languages,naist-jdic,bundled-dicts,ffi): 396 件 pass
- cargo build aarch64-apple-ios: 成功、piper_plus_g2p_* 5 シンボル export 確認
- grep src/cpp/{cmudict,pinyin}*.json — 残存参照なし (build-* は gitignore)
エージェントチームによる徹底監査の結果、以下の対応漏れを修正:

- LICENSE: THIRD_PARTY_LICENSES.md に CMU Pronouncing Dictionary
  (BSD-style) と pypinyin (MIT) の attribution セクションを追加
  (bundled-dicts で埋込む辞書の義務)
- README: README.md / README_EN.md / docs/guides/ios-integration.md
  に Swift G2P (SPM) への参照を追加。crate README を v0.4 に統一、
  ffi / bundled-dicts feature を Feature Flags 表に明記
- FFI: default_languages() 関数を新設し、有効 features と
  bundled-dicts に応じて piper_plus_g2p_create(NULL) のデフォルト
  言語を動的構築 (zh は chinese+bundled-dicts 同時有効時のみ)
- Test: tests/PiperPlusG2PTests/ConcurrencyTests.swift を追加。
  Phonemizer.@unchecked Sendable を 60 並列 phonemize / 20 並列 init /
  100 反復 deinit で実証
- Spec: docs/spec/swift-g2p-contract.toml 新設 (FFI/ABI/JSON の機械
  可読契約)。decisions.md に Resolution Summary を追加し全 D-* 項目
  の対応状況を記録
- .gitignore: xcframework / SwiftPM build artifacts (*.xcframework/,
  .build/, .swiftpm/, *.xcodeproj/, DerivedData/) を除外
- CHANGELOG: 上記の追加項目を Unreleased エントリに反映

検証: cargo test -p piper-plus-g2p \
  --features all-languages,naist-jdic,bundled-dicts,ffi で
  396 lib + 22 integration tests すべて pass。
監査で検出した「テスト・CI が死蔵」状態を解消:

(A) release-shared-lib.yml に pull_request trigger 追加。build-g2p-ios と
assemble-g2p-xcframework のみ PR で smoke run、他 jobs は
`if: event_name != 'pull_request'` で tag push 限定を維持。文法エラー /
path ミスを PR 段階で検出可能に。

(B) .github/workflows/swift-g2p-ci.yml 新設。macOS runner で
cargo build (aarch64+x86_64 apple-darwin) → cbindgen ヘッダ生成 + 5 FFI
シンボル検証 → xcodebuild -create-xcframework で local macOS xcframework
組立 → Package.swift を CI 専用 path-based manifest に上書き →
swift test --filter PiperPlusG2PTests で PhonemizerTests /
GoldenPhonemeTests / ConcurrencyTests を実行。xcframework 未 publish
状態でも PR で Swift wrapper を実機検証できる。

(D) ffi.rs に default_languages() / register_one() のテストを 7 件追加。
lib tests 396 → 403:
- default_languages_full_feature_set: 8 言語確認
- default_languages_includes_rule_based: 規則ベース必須言語確認
- default_languages_excludes_zh_without_bundled_dicts (cfg gate)
- register_one_unsupported_language: UnsupportedLanguage 確認
- register_one_zh_without_bundled_dicts: Phonemize エラー (cfg gate)
- register_one_en/ko/ja_succeeds: 各 feature gate で正常系
- create_null_yields_default_set: NULL → default set の整合性

(E) PhonemizerTests.swift の assertion を Golden fixture と整合する
強い形に書換:
- isEmpty のみだった 8 言語テストに具体トークン照合
  (en "h", ja "k/o/n/i/a", es "o/l/a", fr "b", pt "o")
- 最小トークン数の XCTAssertGreaterThanOrEqual
- 中国語は PUA tone marker (E000-F8FF) を assertContainsPUA で検出

副作用: cargo fmt が english.rs::new_bundled() を自動整形。clippy 抑制
のため default_languages に #[allow(clippy::vec_init_then_push)] を追加
(cfg gate された push なので vec! 化できない)。

検証:
  cargo fmt -p piper-plus-g2p -- --check                   ✓
  cargo clippy -p piper-plus-g2p --all-features -D warnings ✓
  cargo test -p piper-plus-g2p --features all-languages,naist-jdic,bundled-dicts,ffi
    → 403 lib + 22 ffi tests pass
  python3 yaml.safe_load() 両 workflow                      ✓
PR #398 で 3 件の CI が失敗していた根本原因:

1. Swift G2P CI (macOS) — nm の最終 pipeline が pipefail で exit 1
   `nm -gU staticlib.a | grep | sort -u` の最終 pipeline が、staticlib に
   外部 symbol を持たない archive member を含むときに macOS nm が非ゼロを
   返し、`set -euo pipefail` で step 全体が失敗していた。出力には FFI
   symbol 5 つすべて表示されており、検証自体は成功していた。
   `nm -gU ... || true` で出力をキャプチャしてから for-loop で symbol を
   個別検証する形に変更。

2. Build piper-plus-g2p iOS ios-arm64 / ios-arm64_x86_64-simulator —
   release-shared-lib.yml の verify step も同じ形(最終 pipeline に
   `|| true` が無い)で失敗。同じ for-loop 検証へ統一。

3. test-cpp-inference (Docker) — cpp-inference/Dockerfile の builder
   stage が `src/rust/piper-plus-g2p/data/` を COPY していないため、
   cmake/Install.cmake:25 が cmudict_data.json / pinyin_*.json を install
   する際に「No such file or directory」で失敗。data ディレクトリの
   COPY を追加。
前回の修正 (b50af91) でも CI は依然失敗:

  awk: write error on /dev/stdout
   input record number 1301, file
   source line number 1
  echo: write error: Broken pipe
  ::error::staticlib missing FFI symbol: piper_plus_g2p_create

ただし step の末尾には FFI symbol 5 つすべて出力されており、staticlib
には symbol が正しく存在していた。

根本原因: nm -gU の出力 (~10K 行) を `echo "$nm_out" | awk | grep -q`
の pipeline に流していた。grep -q はマッチ発見時点で stdout を閉じて
即 exit する。awk/echo はその後さらに書き込もうとして SIGPIPE で
非ゼロ exit。`set -euo pipefail` 下では pipeline 全体が失敗扱いとなり、
||の右辺の `exit 1` が発火。

対策: pipe を使わず bash の `case "$nm_out" in *" T _${sym}"*)` で
直接文字列マッチさせる。これなら nm 出力サイズに依存せず、`grep -q`
の早期 exit による SIGPIPE が発生しない。失敗時の診断出力 (printf
| grep) は pipe を使うが、fallback 用なので `|| true` で守る。

ローカルで 10,001 行のダミー nm 出力で 5 symbol すべて検出 / exit 0
を確認。
Swift G2P CI のコンパイルが以下のエラーで失敗:

  Phonemizer.swift:28:13: error: no exact matches in call to initializer
  OpaquePointer(piper_plus_g2p_create(ptr))
                `- error: no exact matches in call to initializer

  candidate expects value of type 'UnsafeMutableRawPointer?' for
  parameter #1 (got 'OpaquePointer?')

根本原因: cbindgen が opaque forward-declared struct
(`typedef struct PiperG2pHandle PiperG2pHandle;`) を出力し、Swift の
clang importer はそれを直接 `OpaquePointer?` にブリッジする。よって
`piper_plus_g2p_create(ptr)` の戻り値は既に `OpaquePointer?` で、
`OpaquePointer(...)` で再 wrap すると型不一致になる
(OpaquePointer の init は UnsafeMutableRawPointer? しか受けない)。

修正: `OpaquePointer(...)` の wrap を削除し戻り値をそのまま `raw`
変数に代入。同じ理由で `piper_plus_g2p_free(.init(handle))` 等の
`.init(handle)` も不要 (OpaquePointer は OpaquePointer? に暗黙変換可)
なので `piper_plus_g2p_free(handle)` 等に簡素化。

修正対象:
- Phonemizer.swift:28 (init)
- Phonemizer.swift:36 (deinit)
- Phonemizer.swift:48 (phonemize)
- Phonemizer.swift:67 (availableLanguages)
Swift G2P CI の swift test ステップで GoldenPhonemeTests が 5 言語で
10 failures。例:

  testSpanishGoldenCases: [es] perro grande — expected token 'rr' to
  be present, got: ["p", "ˈ", "e", "", "o", " ", "ɡ", "ɾ", ...]

根本原因: Rust 実装の `piper_plus_g2p_phonemize` は多文字音素トークン
(rr / cl / dʒ / eː など 99 種) を **PUA codepoint 1 文字** にエンコード
して JSON で返す。一方 fixture の `expected_contains` は人間可読な
named token (`"rr"` 文字列) で記述されている。Rust 側のクロスランタイム
テスト (`test_g2p_golden.rs::assert_case`) は `token_to_pua("rr")` で
PUA char に変換してから contains 判定を行うが、Swift 側の同等テストは
この変換を行っていなかったため fail。

修正:
- `Sources/PiperPlusG2P/PUAMap.swift` を新規追加。Rust の
  `token_map.rs::FIXED_PUA_MAP` (99 entry / PUA v2) を Swift 側に
  忠実に複製し、`tokenToPua` / `puaToToken` を public API として公開。
- `tests/PiperPlusG2PTests/GoldenPhonemeTests.swift` で
  `expected_contains` / `expected_not_contains` の検証時に
  `PUAMap.tokenToPua(needle)` で PUA 化してから contains 判定。

`expected_tokens` (exact match) は fixture 内に multi-char token が
含まれないため変換不要 (a/l/o/ˈ のみ)、改修対象外。
PR #398 を 4 並列レビューエージェントで監査した結果の不整合・盲点を一括修正。
コピペで動かなかったコード片、未配信の version 参照、検出機構の抜け、契約強度
不足を中心に対処。

Critical (5):
- C1: G2PError のドキュメントが実装と不一致 (`unsupportedLanguage` 等の
  存在しないケース) を修正。guide / spec / troubleshooting テーブルを実装の
  4 ケース (initializationFailed / phonemizeReturnedNull / invalidUTF8 /
  decodeFailed) に揃え、各ケースの意味を明記。
- C2: Package.swift `g2pVersion` を `1.13.0` から `1.14.0` に bump し、
  guide / CLAUDE.md / decisions.md と整合。`g2pChecksum` が placeholder
  の意味と consumer がタグ前にビルドしようとした時の挙動をコメントで明示。
- C3: spec の cargo build 例 / CI yaml 例から `bundled-dicts,ffi` features
  が抜けていたのを補完 (decisions.md に書いてあるが実例では消えていた)。
- C4: PUA consistency gate に Swift PUAMap を追加。
  scripts/check_pua_consistency.py に parse_swift() 実装、
  pua-consistency.yml の paths に Sources/PiperPlusG2P/PUAMap.swift を追加、
  job 名を "All 7 runtime PUA tables" に更新。実行で 99 entries / version=2
  が他 6 ランタイムと一致を確認。Issue #389 の再発防止構造。
- C5: swift-g2p-ci.yml の Package.swift heredoc 上書きを Package.ci.swift
  (tracked file) 化。リリース版との drift が git diff で見える形に。
  iOS(.v15) を platforms に追加して iOS-only API の compile 検証を CI でも
  走らせる。

High (8):
- H1: crate README の C FFI 関数名 `destroy` / `version` が実装に存在しない
  → 実体 (create / phonemize / free_string / free / available_languages)
  + 各関数の所有権・戻り値を表で明示。
- H2: THIRD_PARTY_LICENSES の `new_from_path()` / `new_from_paths()` が
  実装に存在しない → 実体 (`new_with_dict` / `new(p1, p2)`) に修正。
- H3: docs/spec/swift-g2p.md:5 の Issue URL に全角カタカナ「ピ」混入で 404 →
  ASCII piper に修正。
- H4: swift-g2p-contract.toml に [compat] / [compat.breaking_change_policy]
  セクションを新設。pua_compat_version への参照、symbol 削除 / シグネチャ
  変更 / JSON envelope 追加・削除 / PUA bump / default 言語変更で何が
  壊れるかを明文化。
- H5: tests/PiperPlusG2PTests/PUAMapTests.swift を新規作成。
  fixedMap.count == 99、compatVersion == 2、PUA 範囲、コードポイント衝突、
  トークン衝突、forward/reverse round-trip、unknown lookup の 8 アサーション。
  シンボリックな count も差分で検出される。
- H6: swift-g2p-ci.yml に Thread Sanitizer ジョブを追加 (continue-on-error
  でまずは observability 優先)。`@unchecked Sendable` の主張に裏付けが
  生まれるよう ConcurrencyTests を `--sanitize=thread` で再実行。
- H7: ConcurrencyTests.testRepeatedInitDeinit にアサーションを追加。
  各 iteration の availableLanguages 検証 + 10 反復ごとに phonemize して
  pipeline の健全性確認 (元の loop は assertion ゼロでノーオペ化していた)。
- H8 (M1 兼用): testAvailableLanguagesRoundTripsRequestedSet を追加。
  Rust が新言語を返したとき Swift の Language enum が落とす silent drop
  を検出。

Medium (5):
- M3: バイナリサイズ表記を guide で 1 表に統合。xcframework.zip ダウンロード
  / app 増分 / 未 strip staticlib の 3 観点を区別して記載。各言語のサイズ
  寄与も実測値 (cmudict 3.7MB / pinyin 2.6MB / NAIST-JDIC 20MB) に揃えた。
- M4: NAIST-JDIC ライセンス全文 (BSD-3-Clause) と osdn.net 公式 URL を
  THIRD_PARTY_LICENSES.md に追加。guide でも表記揺れを解消し、
  attribution 義務の有無を整理。
- M5: english.rs / chinese.rs の OnceLock+expect が catch_unwind 後に poison
  状態で残る挙動をコメントで明示。iOS 経路 (new_bundled) は到達しないこと
  を併記して FFI consumer の安心材料に。
- M6: testEmptyTextDoesNotCrash の `try?` で例外握りつぶしを止め、
  Rust 側仕様 (空入力 → 成功 or phonemizeReturnedNull のいずれか) を
  Swift にも明示反映。
- L: integration guide の言語表に「学習済みモデルは 6 言語」「ko/sv は
  G2P 出力できても TTS モデルがない」旨の注釈を追加。

検証:
- python scripts/check_pua_consistency.py --verbose --check-version
  → 7 ランタイム全て 99 entries / version=2 一致
- cargo test -p piper-plus-g2p --features all-languages,naist-jdic,
  bundled-dicts,ffi --lib → 403 passed / 0 failed
- yaml.safe_load で 2 ワークフロー syntax ok
)

ユーザー視点レビュー指摘 (Concern B / C / E) を本ブランチで解消。

- macOS slice 追加 (Concern E):
  build-g2p-ios → build-g2p-apple に rename し macos-arm64_x86_64
  matrix エントリ追加。assemble-g2p-xcframework が 3 slice を扱うよう
  拡張。Package.swift に .macOS(.v13) を追加し、binaryTarget URL を
  libpiper_plus_g2p-ios- → libpiper_plus_g2p-apple- へ rename
  (macOS slice 同梱を反映)。
- G2PError 診断情報 (Concern C):
  initializationFailed(requestedLanguages:) /
  phonemizeReturnedNull(language:) に associated value を追加。
  multi-language caller が call site を辿らずに失敗箇所を特定可能に。
  既存テスト 1 件更新 + 新規テスト 2 件追加。
- examples/swift-g2p/HelloG2P/ (Concern B):
  macOS CLI demo (Package.swift + main.swift + README)。swift run で
  8 言語の動作確認が可能。tag publish 前は path: モードへの切替手順を
  README に明記。

ドキュメント整合: integration guide / spec / contract / decisions log /
CLAUDE.md を新 artifact 名・slice 構成・新エラー形式に追従。

未対応 Concern (本ブランチ範囲外): A (tag publish)、D (JSON decode 最適化)、
F (App Clip lite slice)、G (review approval)。詳細は
docs/spec/swift-g2p-decisions.md の Post-merge follow-ups 節を参照。
@ayutaz
ayutaz force-pushed the feat/swift-g2p-387 branch from 51aa5d6 to 1e49c19 Compare May 7, 2026 15:34
ayutaz added 4 commits May 8, 2026 00:56
dev リベースで取り込んだ #392 (fixture schema v1→v2) と #400 (Kotlin G2P が
新規英語ケース追加) によって Swift CI 2 件失敗を解消。

# 修正内容
- tests/fixtures/g2p/phoneme_test_cases.json: regenerate_test_fixture.py で
  pua_map セクションを再生成 (drift 検出 CI が通る形式に統一、ensure_ascii=True
  で IPA 文字を \\u エスケープ表記に)。test_cases 自体は不変。
- GoldenPhonemeTests.swift: testFixtureLoads の expected version を 1 → 2 に
  更新。runGoldenCases で Rust crate の英語実装が letter-spelling fallback
  未実装である既知ギャップ (acronym/all-caps/mixed-case/repeated/Latin-1
  accent/quote-only) を skip リスト化。Rust 英語実装の改善は別 PR で追跡。
エージェントチームの監査で「役目終了」と判定された箇所を削除/削減。
純減 334 行 (89 insertions, 423 deletions)。

# 削除
- docs/spec/swift-g2p-decisions.md (387 行)
  - 自身が冒頭で Resolved 宣言済み、末尾「全 ID 解決後は役割を
    swift-g2p.md / swift-g2p-integration.md に引き継ぐ」と明記
  - 全 D-1〜D-15 と Post-merge follow-ups (2026-05-07) で完了済み
  - 参照: CHANGELOG.md のリンク行削除、swift-g2p-ci.yml のコメント差し替え

# 削減
- docs/spec/swift-g2p.md (564 → 207 行)
  - Status: Proposed (Draft) → Implemented (v1.14.0+)
  - §3.5 Swift API: コード塊掲載 → Sources/PiperPlusG2P/ への参照テーブル化
  - §4 実装スコープ: Cargo.toml/cbindgen.toml/CI YAML/Package.swift 全文
    → 「実体は実コード/CI/manifest が真」のサマリ表
  - §4.7 リリース手順: Package.swift 冒頭コメントの正本へのリンク化
  - §4.9 テスト一覧: tests/PiperPlusG2PTests/ への参照のみ
  - 残存: §1 ユースケース、§2 採用方針、§3 配布構成、§5 採用しなかった案、
    §6 拡張可能性、§7 関連リンク
- examples/swift-g2p/README.md (101 → 64 行)
  - "Quick Start" を 3 行に圧縮、ガイドへのリンク化
  - "Integrating into your own project" を削除 (ガイドへ統合)
  - "Local Development" / "Why a separate example directory?" は unique
    価値ありで保持

# 保持
- docs/spec/swift-g2p-contract.toml (FFI/ABI 契約として現役)
- docs/guides/swift-g2p-integration.md (consumer 向けガイド本体)
- examples/swift-g2p/HelloG2P/ (実行可能サンプル)
- src/rust/piper-plus-g2p/README.md (crate-level doc, Swift 言及は最小限)
エージェント調査で参照ゼロ + 自己宣言「Design proposal — not yet
implemented」と判定。Strategy C (現行手動同期 + CI gate) が
`scripts/check_pua_consistency.py` + `/check-pua` skill +
pre-commit hook で実運用成立済みのため、Strategy A/B 提案文書は陳腐化。

他 doc/コード/CI からの参照ゼロを grep で確認済 (削除によるリンク
切れなし)。

# 残った削減推奨 (本 PR 範囲外、別 PR 推奨)
- zh-en-loanword-runtime-rollout.md (1958 行、Issue #384 完了)
- kotlin-g2p-design.md (540 行、Issue #388 PR #400 完了)
- kotlin-g2p-requirements.md (650 行、同上)
これらは本ブランチ (Swift G2P #387) と無関係の領域で、git blame と
PR レビューの clean さを優先して別 PR で対応する。
エージェントチーム調査で「実装完了で役目終了」と判定された箇所を削除。
本ブランチ範囲外領域だがユーザー判断で同 PR に同梱。
純減 325 行 (30 insertions, 355 deletions)。

# zh-en-loanword-runtime-rollout.md (1958 → 1739)
Issue #384 / PR #399 で全 7 ランタイム同期実装完了済み。
- Status: Draft → Implemented
- §4.1 ランタイム別タスク表 (R/G/C/W/P) 削除 (git log と PR 履歴に集約)
- §5 1 ブランチ戦略のリスクと対策 削除
- §6 ロールアウト計画 (Day 1-14 + 受け入れ基準) 削除
- §8.11 リリース戦略 (実装後不要) 削除
- §8.15 CI ジョブ整合性 (実装後不要) 削除
- §9 改訂履歴 (Draft v1-v9) 削除

残置: §1 背景、§2 各ランタイム現状、§3 共通の実装パターン、§4 横断的
な設計課題、§5 関連ドキュメント、§8 深堀り (規範・運用 SOP・将来拡張)。

# kotlin-g2p-design.md (540 → 502)
Issue #388 / PR #400 で実装完了済み。
- Status: Implemented を冒頭追加
- L8 「実装着手前に更新」警告 削除 (前提矛盾の自己警告は陳腐化済)
- §6 実装ロードマップ (Day-by-Day Phase 1-7) 削除
- §9 受け入れ基準 削除
- §12 改訂履歴 削除
- §7-§11 のセクション番号を §6-§9 に詰め直し

残置: §1 ゴール、§2 OSS コンセプト分析、§3 既存資産、§4 アプローチ
比較、§5 推奨アプローチ、§6 自動化戦略、§7 リスクと対策、§8 関連
ドキュメント、§9 競合事例。

# kotlin-g2p-requirements.md (650 → 582)
- Status: Implemented を冒頭追加
- §13 リスクと緩和策 削除 (実装後に発火/不発が確定)
- §14 受け入れ基準 削除 (PR #400 で全 AC 実現済み)
- §15 オープンクエスチョン 削除 (Q-1〜Q-10 全解決済み)
- §16 用語集再掲 ("§2 を参照" のみ) 削除
- §17 関連ドキュメント を §13 に番号繰り上げ
- §18 改訂履歴 削除

残置: §0〜§12 (要件定義としての規範性が継続)、§13 関連ドキュメント。
@ayutaz
ayutaz merged commit ff6f34c into dev May 8, 2026
99 checks passed
@ayutaz
ayutaz deleted the feat/swift-g2p-387 branch May 8, 2026 02:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

swift向けのg2p対応

1 participant