AIでもわかる、Nimで作るアイヌ語変換アプリ
最近マイブームとしてSuno AIにアイヌ語の曲を作ってもらうことですが、その際にローマ字で書けばなぜかほぼ完璧にアイヌ語を発音してくれるんですよね。ただやはり細かいところで発音がちょっと難しい、ということで、AIでもわかりやすいアイヌ語の表記に直せばいいのではないと思ったんです。
この文章では、AIはどこで読み間違えたのか、その間違いに対してどう対処すれば良いのか、技術選定の理由、Nimの現代的なパッケージ管理と開発体験のための環境設定、実際の開発と実装の流れなどについて解説していきます。
問題調査
現時点では二曲しか実験していないので、かなり限られたデータの中どう直せばよいのか、というのはまだ模索中ですが、少なくともいくつかの読み間違いを簡単な文字列置き換えで修正できることはわかっています。この辺については、ゆくゆく拡張、訂正していきたいと思っています。
まず、アイヌ語の表記は主にローマ字とカタカナの二種類がありますが、カタカナを試しておらず、ローマ字で作っています。なぜなら、カタカナの小文字を子音として読んでもらえるのかどうか(データがないので恐らくできないだろう)、日本語なまりが入る虞があったからです。ただ、実際どうなのかは、試してみないとわかりません。ひとまず、ローマ字について考えます。
読み間違い
限られた範囲での観察では、以下のような読み間違いが観察される。
- 促音(pp tt kk)は時々読んでくれるが読まないときもある
- okaramotte ◯
- opitta ◯
- cuppa → cupa
- sírpekettere ◯
- cikáppo → ciːkapo
- yakka ◯
- 珍しい子音連結(k.p)は日本語のように母音が挿入される
- kotekparparu → kotekeparparu
- 二重母音(ay)を英語読み(/eı/)にしてしまうときがある
- paykar → peykar
- 前母音(i e)以外の母音(a u o)の前の歯茎破擦音(c)が軟口蓋破裂音(k)になる
- sikcupu → sikupu
- cupki → kupki
- 語尾の歯茎硬口蓋摩擦音(-s)が歯茎摩擦音(s/_{a,u,e,o})になってしまう
- as;sikcup → /aɕ/(文を跨いでても次の単語が/ɕ/ならば逆行同化して正しい)
- sukus#kuaske → /sukus#kuaske/(間違いではないが標準的なアイヌ語ではなく若干摩擦音がきになる、再試行ではちゃんと/ɕ/になったりするので謎)
- as#wa#an → /as#wa#an/(Ditto.)
- niska → /niska/
- 歯茎弾き音(r)を読んでくれる時とそうでない時がある(英語と日本語の影響か)
- karimpani → kaiːpani
- okaramotte ◯
- asir pa → asippa
- 音位転換(メタセシス、metathesis)が発生するケースがある
- caketek → catekek(人間っぽくてかわいいですね)
- 人称接辞を時々合いの手(感嘆詞?)と勘違いされる
- e-tura → ee! tura(かわいい)
- 人称接辞を含む句で頭子音が脱落することがある
- a-kere → aere
- 不特定な単語やフレーズの幻覚(ハルシネーション)を観る
- karimpani → katuramani(turaの影響か?)
- エコの段にはアイヌ語っぽいけど意味不明なフレーズを連発する(解読や再解釈を無理やり頑張ったがやはり無理で字幕をつけるの諦めた)
- hで始まる単語が苦手(どういうこと???)
- hapur → katu
- hapur → hatoy
- hapur → sako
そこ読めるんだ
逆に、やはりびっくりしたのが、問題なくアイヌ語として読めていることで、以上の読み間違い以外は、ほとんど間違っておらず、英語読みにもならずにちゃんとアイヌ語として読めていることが不思議で、さらにいくつかの特殊表記も問題なく読めることに驚きました。また、歌だからかアクセントやイントネーションも気にならない程度になっている。更に、知識がまったくないはずなのに、いいところで単語を切ってくれる。
- e-mínakotom
- 人称接辞 e-
- アクセント í
- -m の発音 (それはそう)
- asir pa、sir
- si の発音(ちゃんと[ɕi]になっていて、[si]にならなかったが、再試行でesampesituriが[si]になっている)
- -r の発音(英語みたいに「アサー」にならずにちゃんと[ɾ]と読んでいるし、しかもなんかアイヌ語っぽい緩い=母音がコピーされる発音に聞こえる、ただ時々r自体が脱落することがあるのは英語か日本語の影響か)
- popke
- -p の発音(ちゃんと無声閉鎖音=内破音[p̚]になっている、英語の影響?)
- koraci#an
- ra の発音 ra 完璧
- ci の発音 ci 完璧
- ’a の発音 完璧
- toktoksetek
- -k の発音 完璧([k̚])
要件定義
要は以上の発音の間違いをできる避けるようなテキストを作成しなければならない。規則的な間違いは、特殊な表記を行うことで修正できるが、単発な言い間違いや幻覚は修正できないので、難しい単語を避けるなどで対応することしかないですね。また、Suno AIの性質上シードを指定できないため、全く同じ曲を再び作成することは叶わないので、個別な箇所の修正も叶わず、一発勝負を狙えるように歌詞を完璧にしなければならなりません。
置換方針
- 口蓋化音 s, c(sh, chに置換することで解消)
- s → sh / {V_,_i}
- c → ch / _{a,u,o}
- 促音 pp, tt, kk(別単語として認識させることで解消)
- pp → p␣p
- tt → t␣t
- kk → k␣k
- 二重母音 ay(aiと書くことで解消)
- ay → ai
- 珍しい子音連結 kp(別単語として認識させることで解消)
- kp → k␣p
- 人称接辞による誤読(ハイフン類を削除することで解消)
- {-,=} → ∅
- 歯茎弾き音 r → 諦める(母音を最後の入れることも可能だがアイヌ語で区別するので…)
- メタセシス → かわいいから許す
- 非規則的な誤読 → 諦める
UI
入力欄と出力欄とコピーボタン、以上。
技術選定
Nim
Nimは昔なんとなく聞いたことがあって、実際何回か試してみたところ、かなりエレガントで使いやすかたのでとても気に入りましたが、当時はまだ環境設定が難しく、正しく作動しておらず、JavaScriptにコンパイルすることでようやく無事を得ました。
それから、擬似コードを書く時には、Nimでよくない?となって、一時期マイブームになっていましたが、やはり普段の開発はウェブでJSとかPythonでアプリを済ませてしまうので、またライブラリも少なく、中々やりたいことが難しかったりするので、暫く触らずにいました。
なんで今回これをNimで開発しようとしたかというと、まず最近ゆる学徒ハウスのAI対談で言及されたので、正直どれくらい発展したのかちょっと気になったのと、単なる文字列置換でしかも自分向けのUIなので、最初はRustで書こうと思ったがコンパイルが時間かかるし、簡単なアプリなのでRustと戦いたくないので、気持ちよく効率よくWindowsで簡単にコンパイルしてそのまま使えるものが楽かなと思ったからです。
Nigui
NimでGUIアプリを作るのには、以下のスレやAwesomeリストによれば、以下のようなライブラリがあるそうです。
今回はその中でもNiguiを使ったのは、Google検索で最初に出てきたこと、実際試してみたら簡単だったことから、これを採用に至りました。
実装
Nimbleの環境まわりが結構整ってきているようなので、今回は単発のnimファイルでゴローバルに依存を入れるのではなく、ちゃんとnimbleのバイナリパッケージにして、testやwatchしながら開発したいので、色々整備していきます。
Nimのインストール
パソコンには一応Nimをインストールしていますが、結構古いのでアップデートしたいので、インストール方法を見ていきます。公式サイトに行ってチュートリアルを見ると、配布されたバイナリをそのままインストールすることもできますが、いづれも小書きでchoosenimを使うと簡単にバージョン切替ができるそうなので、それを試してみます。
choosenimの公式ページに行くと、WindowsではGithubのReleaseページで、ZIPからrunme.bat
を実行すればインストールされます。
以下のコマンドを実行してnimを最新のstable版をインストールします。
choosenim stable
完成したら、以下のコマンドを実行すると、バージョンが表示されます。
nim -v
Nim Compiler Version 2.0.2 [Windows: amd64]
Compiled at 2023-12-15
Copyright (c) 2006-2023 by Andreas Rumpf
active boot switches: -d:release
プロジェクト作成
まず、以下のコマンドを実行することで新しいフォルダーconvert
を作って、nimble init
を実行します。出てきたCLIに、Package typeにバイナリと、Tabで選択しEnterで確定、バージョンや説明・ライセンス・Nimバージョンなどを設定していきます。
mkdir convert
cd convert
nimble init
すると、フォルダー名と同じ名前のconvert.nimbleやconvert.nimが作られます。
├── convert.nimble
└── src
└── convert.nim
以下のコマンドを実行すると、NimファイルがCバックエンドでコンパイルされ、Hello World!
が表示されます。
nimble run
依存関係の整理
今回必要なものは多くなく、GUIやコピー機能以外は標準機能で足りるので、converter.nimble
ファイルを編集して、requires "nigui >= 0.2.8"
及びrequires "nimclipboard >= 0.1.2"
を追記すると、文章執筆時点の最新版のNiguiを依存します。JSやPython、Rustなどのようにadd
/install
のコマンドで追加することはまだできないようです。
# Package
version = "0.1.0"
author = "mkpoli"
description = "AI-pronounceable Ainu converter"
license = "MIT"
srcDir = "src"
bin = @["converter"]
# Dependencies
requires "nim >= 2.0.2"
requires "nigui >= 0.2.8"
requires "nimclipboard >= 0.1.2"
アーキテクチャ
変換用のcodeとGUI表示用のコードを別々のファイルにしたいので、convert.nim
とconverter.nim
を用意します。
まず変換用のcodeでそのまま返ってくるようなコードを用意します。
func convert(s: string): string = s
基本的なUIを入れます。入力用のテキストエリア、出力用の読み取り専用テキストエリア、削除ボタンとコピーボタンだけです。
import nigui
import nimclipboard/libclipboard
include convert
const labelWidth = 55
app.init()
app.defaultFontfamily = "Atkinson Hyperlegible"
app.defaultFontSize = 24
var window = newWindow("AI-Pronouceable Ainu Converter")
window.width = 640
window.height = 480
var mainContainer = newLayoutContainer(Layout_Vertical)
mainContainer.padding = 10
mainContainer.xAlign=XAlign_Center
mainContainer.heightMode = HeightMode_Fill
window.add(mainContainer)
var inputContainer = newLayoutContainer(Layout_Horizontal)
mainContainer.add(inputContainer)
var inputLabel = newLabel("Input:")
inputContainer.add(inputLabel)
inputLabel.minWidth = labelWidth
inputLabel.heightMode = HeightMode_Fill
var inputTextArea = newTextArea()
inputContainer.add(inputTextArea)
inputTextArea.heightMode = HeightMode_Fill
var clearButton = newButton("Clear")
clearButton.minWidth = 70
clearButton.heightMode = HeightMode_Fill
inputContainer.add(clearButton)
var resultContainer = newLayoutContainer(Layout_Horizontal)
mainContainer.add(resultContainer)
var resultLabel = newLabel("Result:")
resultContainer.add(resultLabel)
resultLabel.minWidth = labelWidth
resultLabel.heightMode = HeightMode_Fill
var resultTextArea = newTextArea()
resultContainer.add(resultTextArea)
resultTextArea.editable = false
resultTextArea.height = 300
inputTextArea.onTextChange = proc(event: TextChangeEvent) =
resultTextArea.text = convert(inputTextArea.text)
var cb = clipboard_new(nil)
var resultCopyButton = newButton("Copy")
resultCopyButton.minWidth = 150
resultCopyButton.onClick = proc(event: ClickEvent) =
discard cb.clipboard_set_text(cstring(resultTextArea.text))
mainContainer.add(resultCopyButton)
clearButton.onClick = proc(event: ClickEvent) =
inputTextArea.text = ""
resultTextArea.text = ""
inputTextArea.focus()
window.show()
inputTextArea.focus()
app.run()
以下のコマンドを実行するとウィンドウが表示されます。
nimble run
テスト環境整備
UIと基本的なアーキテクチャが決まったところで、変換コードのTDDを行うため、テスト環境を整備します。まず、tests/
フォルダーを作ります。そこでtests/config.nimx
でインポートのパスを戻すためのスイッチを入れ、test_convert.nim
でテストケースを書きます。
switch("path", "$projectDir/../src")
switch("d", "isTesting")
switch("hints", "off")
import std/unittest
include convert
suite "convert module":
test "s -> sh":
check convert("sukus") == "sukush"
check convert("siknu") == "shiknu"
check convert("aske") == "ashke"
check convert("niska") == "nishka"
test "c -> ch":
check convert("cupu") == "chupu"
check convert("cas") == "chash"
check convert("cep") == "chep"
test "pp, tt, kk -> p#p, t#t, k#k":
check convert("okaramotte") == "okaramot te"
check convert("aeppo") == "aep po"
check convert("yakka") == "yak ka"
test "ay -> ai":
check convert("paykar") == "paikar"
test "kp -> k#p":
check convert("kotekparparu") == "kotek parparu"
test "-,= -> ":
check convert("ku=anu") == "kuanu"
すると、以下のコードを実行すると全部失敗するはずです。そこで、実装していき、テストが通るように改修していけば、概ね期待通りの結果が得られるはずです。
nimble test
ウォッチ機能
ウォッチ機能を利用して快適に開発する予定ですが、nimble
などではwatch
を実装する予定がないとのことらしいので、他のツールを使うしかないようです。こちらのmonit
を使えば、ソースファイルを編集したらコンパイルやテストを自動的に実行してくれるのですが、あいにく中断することはまだできないので、生成されたバイナリーを手動で実行しないといけないっぽいです。使い方は簡単です。以下のコマンドを順に実行していけば、変更があった時に、buildやtestが自動的に行われるようになります。
nimble install monit
monit init
monit run
あるいは、monitを用いず、公式のVSCodeプラグインを使うことで、VSCodeのTask機能によって似たような結果を得られます。
変換機能の実装
import std/strutils
func convert(s: string): string =
var r = s
# s -> sh
r = r.replace("as", "ash")
r = r.replace("is", "ish")
r = r.replace("us", "ush")
r = r.replace("es", "esh")
r = r.replace("es", "osh")
r = r.replace("si", "shi")
# c -> ch
r = r.replace("c", "ch")
# pp, tt, kk -> p#p, t#t, k#k
r = r.replace("pp", "p p")
r = r.replace("tt", "t t")
r = r.replace("kk", "k k")
# ay -> ai
r = r.replace("ay", "ai")
# kp -> k#p
r = r.replace("kp", "k p")
# -,= -> ""
r = r.replace("-", "")
r = r.replace("=", "")
r
これでテストが通り、以下のような変換結果を得ることができたので、これを使って実際に曲を作り続けて、修正していきたいと思います。
公開
今回のミニプロジェクトのソースコードを、GithubにMITにて公開しました
まとめ
いかかでしたでしょうか。今の時代は、まさに人間がサービスを受けるのではなく、人間がプロデュースして、AIが気持ちよく物をこなせるように人間側が工夫し続けるという時代になっていますね。これからどうなっていくでしょうか。いづれにしても、今回はAIに歌ってもらうように、Nimで翻訳プログラムを作ってみました。予想以上時間がかかってしまったようですが、新しい言語を勉強したり、問題解決したりする練習にはなりました。
Discussion