diff --git a/src-python/models/transliterate/transliterate_japanese.py b/src-python/models/transliterate/transliterate_japanese.py new file mode 100644 index 00000000..70a68189 --- /dev/null +++ b/src-python/models/transliterate/transliterate_japanese.py @@ -0,0 +1,110 @@ +from sudachipy import tokenizer +from sudachipy import dictionary +try: + from .transliterate_kana_to_hepburn import katakana_to_hepburn +except ImportError: + from transliterate_kana_to_hepburn import katakana_to_hepburn + +class Transliterator: + def __init__(self): + self.tokenizer_obj = dictionary.Dictionary().create() + self.mode = tokenizer.Tokenizer.SplitMode.A + + @staticmethod + def is_kanji(ch: str) -> bool: + return '\u4e00' <= ch <= '\u9fff' + + @staticmethod + def kata_to_hira(text: str) -> str: + return "".join( + chr(ord(c) - 0x60) if 'ァ' <= c <= 'ン' else c + for c in text + ) + + @staticmethod + def split_kanji_okurigana(surface: str, reading_kana: str): + """ + 1語の表層形(surface)と読み(reading_kana)を + [ {"orig":..., "kana":..., "hira":..., "hepburn":...}, ... ] に分割 + """ + result = [] + + # 表層を「漢字ブロック」と「非漢字ブロック」に分割 + buf = "" + prev_is_kanji = None + blocks = [] + for ch in surface: + now_is_kanji = Transliterator.is_kanji(ch) + if prev_is_kanji is None or now_is_kanji == prev_is_kanji: + buf += ch + else: + blocks.append((prev_is_kanji, buf)) + buf = ch + prev_is_kanji = now_is_kanji + if buf: + blocks.append((prev_is_kanji, buf)) + + # 読みを分配 + kana_left = reading_kana + for is_kan, part in blocks: + if is_kan: + # 仮ルール:残りの読みのうち、送り仮名分を除いた前半を充てる + # ex. "美しい"(うつくしい): 漢字=美, 残り送り仮名=しい + okuri_len = len(blocks[-1][1]) if not blocks[-1][0] else 0 + kana_for_kan = kana_left[:-okuri_len] if okuri_len else kana_left + result.append( + { + "orig": part, + "kana": kana_for_kan, + } + ) + kana_left = kana_left[len(kana_for_kan):] + else: + # 送り仮名部分 → そのまま残りを割り当てる + kana_for_okuri = kana_left + result.append( + { + "orig": part, + "kana": kana_for_okuri, + } + ) + kana_left = "" + + return result + + def analyze(self, text: str, use_macron: bool = True): + tokens = self.tokenizer_obj.tokenize(text, self.mode) + + results = [] + for t in tokens: + surface = t.surface() + parts = self.split_kanji_okurigana(surface, t.reading_form()) + for p in parts: + results.append({ + "orig": p["orig"], + "kana": p["kana"], + "hira": self.kata_to_hira(p["kana"]), + "hepburn": katakana_to_hepburn(p["kana"], use_macron=use_macron) + }) + return results + +# --- テスト --- +if __name__ == "__main__": + test_cases = [ + "美しい花を見る", + "東京に行く", + "漢字とカタカナの混在", + "パーティーに行く", + "コンピューターを使う", + "シェアハウスに住む", + "ヴァイオリンを弾く", + "ギュウニュウを飲む", + "ニューヨークに行く", + "ラーメンを食べる", + "チョコレートが好き", + "SessionIDを取得する", + ] + + transliterator = Transliterator() + for case in test_cases: + print(transliterator.analyze(case)) \ No newline at end of file diff --git a/src-python/models/transliterate/transliterate_kana_to_hepburn.py b/src-python/models/transliterate/transliterate_kana_to_hepburn.py new file mode 100644 index 00000000..e7ba04c2 --- /dev/null +++ b/src-python/models/transliterate/transliterate_kana_to_hepburn.py @@ -0,0 +1,215 @@ +# katakana_to_hepburn.py +# カタカナ -> ヘボン式ローマ字(パッケージ不要) + +def katakana_to_hepburn(kata: str, use_macron: bool = True) -> str: + """ + カタカナ文字列をヘボン式ローマ字に変換する。 + use_macron=True のとき ā ī ū ē ō で長音を表現(マクロン)。 + use_macron=False のときは単純に連続母音を残す(例: ou, oo)。 + """ + # 基本音の対応(主要なカタカナ) + base = { + 'ア':'a','イ':'i','ウ':'u','エ':'e','オ':'o', + 'カ':'ka','キ':'ki','ク':'ku','ケ':'ke','コ':'ko', + 'サ':'sa','シ':'shi','ス':'su','セ':'se','ソ':'so', + 'タ':'ta','チ':'chi','ツ':'tsu','テ':'te','ト':'to', + 'ナ':'na','ニ':'ni','ヌ':'nu','ネ':'ne','ノ':'no', + 'ハ':'ha','ヒ':'hi','フ':'fu','ヘ':'he','ホ':'ho', + 'マ':'ma','ミ':'mi','ム':'mu','メ':'me','モ':'mo', + 'ヤ':'ya','ユ':'yu','ヨ':'yo', + 'ラ':'ra','リ':'ri','ル':'ru','レ':'re','ロ':'ro', + 'ワ':'wa','ヲ':'wo','ン':'n', + 'ガ':'ga','ギ':'gi','グ':'gu','ゲ':'ge','ゴ':'go', + 'ザ':'za','ジ':'ji','ズ':'zu','ゼ':'ze','ゾ':'zo', + 'ダ':'da','ヂ':'ji','ヅ':'zu','デ':'de','ド':'do', + 'バ':'ba','ビ':'bi','ブ':'bu','ベ':'be','ボ':'bo', + 'パ':'pa','ピ':'pi','プ':'pu','ペ':'pe','ポ':'po', + # 小書き(単独で使われることは少ないがマップしておく) + 'ァ':'a','ィ':'i','ゥ':'u','ェ':'e','ォ':'o', + 'ャ':'ya','ュ':'yu','ョ':'yo','ッ':'xtsu','ー':'-', + 'ヴ':'vu','シェ':'she' # 特殊は下で組合せで処理 + } + + # 拡張:子音 + 小ャユョ の組合せ(主要なもの) + digraphs = { + ('キ','ャ'):'kya', ('キ','ュ'):'kyu', ('キ','ョ'):'kyo', + ('ギ','ャ'):'gya', ('ギ','ュ'):'gyu', ('ギ','ョ'):'gyo', + ('シ','ャ'):'sha', ('シ','ュ'):'shu', ('シ','ョ'):'sho', + ('ジ','ャ'):'ja', ('ジ','ュ'):'ju', ('ジ','ョ'):'jo', + ('チ','ャ'):'cha', ('チ','ュ'):'chu', ('チ','ョ'):'cho', + ('ニ','ャ'):'nya', ('ニ','ュ'):'nyu', ('ニ','ョ'):'nyo', + ('ヒ','ャ'):'hya', ('ヒ','ュ'):'hyu', ('ヒ','ョ'):'hyo', + ('ビ','ャ'):'bya', ('ビ','ュ'):'byu', ('ビ','ョ'):'byo', + ('ピ','ャ'):'pya', ('ピ','ュ'):'pyu', ('ピ','ョ'):'pyo', + ('ミ','ャ'):'mya', ('ミ','ュ'):'myu', ('ミ','ョ'):'myo', + ('リ','ャ'):'rya', ('リ','ュ'):'ryu', ('リ','ョ'):'ryo', + # 外来音対応(ファ/フィ/チェ 等) + ('フ','ャ'):'fya', ('フ','ュ'):'fyu', ('フ','ョ'):'fyo', + ('ト','ゥ'):'tu', ('ド','ゥ'):'du', + # F-sounds (ファ フィ フェ フォ) + ('フ','ァ'):'fa', ('フ','ィ'):'fi', ('フ','ェ'):'fe', ('フ','ォ'):'fo', + # シェ チェ ティ etc. + ('シ','ェ'):'she', ('チ','ェ'):'che', + ('テ','ィ'):'ti', ('ト','ゥ'):'tu', ('ド','ゥ'):'du', + ('ウ','ァ'):'wa', ('ウ','ィ'):'wi', ('ウ','ェ'):'we', ('ウ','ォ'):'wo', + # その他外来語によくある組合せ + ('ス','ィ'):'si', ('ズ','ィ'):'zi', ('ツ','ァ'):'tsa', ('ツ','ィ'):'tsi', ('ツ','ェ'):'tse', ('ツ','ォ'):'tso', + ('キ','ェ'):'kye', ('ギ','ェ'):'gye', + ('ヴ','ァ'):'va', ('ヴ','ィ'):'vi', ('ヴ','ェ'):'ve', ('ヴ','ォ'):'vo', ('ヴ','ュ'):'vyu' + } + + # 小文字一覧(ゃゅょぁぃぅぇぉ など) + small_kana = set(['ャ','ュ','ョ','ァ','ィ','ゥ','ェ','ォ','ヮ','ヵ','ヶ','ッ','ャ','ュ','ョ']) + + # マクロン変換マップ(連続母音 -> マクロン) + macron_map = { + 'aa':'ā','ii':'ī','uu':'ū','ee':'ē','oo':'ō', + # ou -> ō という扱いを多くのヘボン式はする(特に日本語由来の長音) + 'ou':'ō' + } + + # Helper: 次のローマ字の先頭子音を取り出す(促音処理用) + def initial_consonant(rom: str) -> str: + # romはローマ字(例 'shi','chi','ta') + # 子音は最初の母音直前までと考える(母音: a,i,u,e,o) + for i,ch in enumerate(rom): + if ch in 'aeiou': + return rom[:i] + return rom # 母音がないなら全部 + + # 変換メイン + res = [] + i = 0 + kata = kata.strip() + length = len(kata) + + while i < length: + ch = kata[i] + + # 促音(ッ):次の音の初めの子音を重ねる + if ch == 'ッ': + # lookahead + if i+1 < length: + # 先の1文字 or 合字を取り得る(小書きが続く可能性) + # まず合字優先で調べる + next_pair = None + if i+2 < length and (kata[i+1], kata[i+2]) in digraphs: + next_pair = digraphs[(kata[i+1], kata[i+2])] + elif kata[i+1] in base: + next_pair = base.get(kata[i+1]) + + if next_pair: + cons = initial_consonant(next_pair) + if cons == '': + # もし母音始まりなら促音は無視(稀) + pass + else: + # Hepburnでは "ch" の場合 "cch"(matcha)等の扱いになるように + # cons の先頭1文字を倍にするより、cons全体の先頭文字を重ねるのが一般的(例: 'shi' -> 'ssh' ? いい例は少ない) + # 実務上は先頭子音の最初の文字を重複する: + res.append(cons[0]) + # advance only the 促音 itself here; next loop handles next kana + i += 1 + continue + + # 長音符(ー):前の母音を伸ばす(マクロン処理は後でまとめて) + if ch == 'ー': + # append marker '-' to indicate prolong; we'll post-process + res.append('-') + i += 1 + continue + + # 合字(子 + 小ャュョ等) + if i+1 < length and (ch, kata[i+1]) in digraphs: + res.append(digraphs[(ch, kata[i+1])]) + i += 2 + continue + + # 小書きが前に独立して出てきた場合(通常は合字で処理されるが念のため) + if ch in small_kana and ch != 'ッ': + # 小書きを単独で英字に変換(例: 'ァ' -> 'a') + res.append(base.get(ch, '')) + i += 1 + continue + + # 普通のカタカナ + if ch in base: + res.append(base[ch]) + i += 1 + continue + + # 英数字や記号・ひらがななどはそのまま(変換対象外) + res.append(ch) + i += 1 + + # ここまでで res はローマ字パーツのリスト(長音は '-' でマーク) + raw = ''.join(res) + + # 撥音(ン)処理: n の前が b/p/m の場合 m にする + # ただし既に 'n' のまま次が母音や y の時は通常 n' を入れるべきだが簡易処理として n のまま保持。 + # 我々は 'n' の後に b/p/m が来たら 'm' に置換 + import re + raw = re.sub(r'n(?=[bmp])', 'm', raw) + + # 長音処理('-' マークを見て前の母音を伸ばす) + # raw 中の '-' を削って該当の母音を伸ばす + while '-' in raw: + idx = raw.find('-') + if idx == 0: + # 先頭に長音符が来るのはおかしいので削除 + raw = raw[:idx] + raw[idx+1:] + continue + # 前の文字が母音ならそれを重ねる + prev = raw[idx-1] + if prev in 'aiueo': + # 直前に既に vowel がある場合、後でマクロン処理に任せて母音を2つにする + raw = raw[:idx] + prev + raw[idx+1:] + else: + # 直前が子音なら何もして取り除く + raw = raw[:idx] + raw[idx+1:] + + # 小さな例外対応: 'ti' 等の表記は 'chi' と扱いたいが上述マップでカバー済み + # macron の適用(長音の正規化) + if use_macron: + # まず 'ou' を ō に(ただし語による例外はあるが、一般的ヘボンに合わせる) + # その前に 'oo' を 'ō' に(稀) + for pair, mac in macron_map.items(): + raw = raw.replace(pair, mac) + # else: leave as is (ou/oo/aa...) + + # 仕上げ:小文字統一(ヘボンは小文字) + raw = raw.lower() + + # 最後に、n の後に母音または y が来る場合は「んあ->n'a」的扱いが必要だが + # シンプル実装では n の後に母音や y が来るときは n' を入れる(明瞭化) + # ただし多くの実例では省略されることも多いのでコメントアウトしておく + # raw = re.sub(r"n(?=[aiueoy])", "n'", raw) + + return raw + + +# --- テスト例 --- +if __name__ == "__main__": + tests = [ + "カタカナ", + "コンピューター", + "キャッチ", + "マッチャ", + "シェア", + "ジェット", + "ヴァイオリン", + "ホテル", + "スーパー", + "ギュウニュウ", + "パーティー", + "トウキョウ", # 東京(トウキョウ -> tōkyō) + "オーケー", + "ファイル", + "ニューヨーク", + "ラーメン", + "パン", + "チョコレート", + ] + + for s in tests: + print(s, "->", katakana_to_hepburn(s, use_macron=True)) \ No newline at end of file