Blog

自分の Blog admin site を書き直した

Kweb がどうも日本語入力がうまくいかなくなったので諦めて普通の Ktor ウェブアプリに戻した。 機能的にはほぼ同じだがシンプルになった。Kweb で少しシンプルに書けるかなーと思ったのだが、そうでもなかったし、、

相変わらず微妙に速度が遅いのだが、、この遅さはなぜか DigitalOcean を使っている結果、シンガポールにデータベースサーバーがあるので、、というよくわからない理由によるものなので、RDSにでも移すかなぁ。

magic research をクリアした

https://mcolotto.github.io/magic-research-demo/website/

Android 版をシコシコとやってた。いわゆる放置ゲー的なやつだけど、この手のやつでは一番おもしろかった。 トータルで 49日かかりました。

何が面白いかと言うとですね。。ゲームバランスが絶妙で、ボスが絶妙に倒せないのと、戦術を工夫しないと先に進めないようになっててめっちゃ楽しい。 転生するたびに見える景色が変わっていく感じがたまらないですな。

blog を更新しようとしたら、落ちた。

なんか知らんけど、 blog を更新しようとしたら落ちた。 kweb が kotlin-logging の 4 beta に依存してて、 class not found な状態に。悲しい。

しょうがないからアプリ自体も kotlin-loggin 4 beta に依存するようにして解決。

ついでに kweb 自体のバージョンも上げたのだが、普通の input box が、すげー微妙な挙動になってつらい。。日本語の入力時に入力途中の文字が入っちゃうというよくあるやつ。 kweb 飽きたし、そのうち全面的に書き直したいかもしれない。

SPA にするのが良いと思うが、どうしようかなぁ。

Marvel's Spider-Man: Miles Morales

https://www.playstation.com/ja-jp/games/marvels-spider-man-miles-morales/

PS5でクリア。 正当な拡張コンテンツという感じ。Spider man 2 も発売決定してるのでそれも楽しみですね。

Watchdogs 1/2 クリア

Watchdogs LEGION をクリアしてから youtube のレビューなどを見たところ、以下のような意見が散見された。

Watchdogs LEGION も結構面白かったのに 1,2 のほうが良かったという意見が多いようなので、そんならやってみるかということでヤッてみた。

やってみるとじっさい、カーチェイス多い。カーチェイス多すぎるぐらい。あと 1 はドローンないからドンパチメインになりがち。 ストーリーもまぁ 1 とか 2 は濃い。2の世界観はなんというか、学生の正義の味方気取りのテロリストって感じでノリについていけない感じはありつつ。。

全体としては 1 も 2 も面白かった。LEGION は方向性としては面白い気がするので、AI の進化を取り入れていければ今後この方向性はもっとおもしろくなるんじゃないかなーと期待している。LEGION の良くないのは、キャラクターを収監されちゃうところな気はするのと、キャラクターごとの個性が弱いってところだと思う。RimWorld とかのほうがよほど思い入れが持ちやすいね。

Watchdogs Legion クリア

寄り道しながら27時間ぐらいでクリア。

krangl が deprecate されていた

kotlin の dataframe 的なライブラリである krangl が deprecate されていた。

kotlinx.dataframe が後継なので、これを利用するようにするとよい。

ドキュメントが整備されていて、読みやすくなっている。dataframe 周りも kotlin で扱いやすくなって最高だ。 https://kotlin.github.io/dataframe/overview.html

kravis は kotlinx.dataframe ベースに変わっていた。

BioHazard RE:4 クリア

神ゲーだと思う。Atomic Heart でがっかりした直後だから余計にそう思うのかも。。 STANDARD 28.2時間 で B。Window gaming PC で Steam 版をクリア。弾がなくなることはほぼないかな。 ホラーというよりアクションって感じ。虫がいきなり出てきたところ以外で驚くことはないかな。 原作プレーしてない勢というか、スマホ版とかを昔ちょっとやったけど途中で操作性悪くて辞めた勢。

freemarker の js_string の歴史

freemarker の js_string がどのような変遷をたどってきたのかをまとめてみた。

2.3

Date of release: 2004-June-15

https://freemarker.apache.org/docs/versions_2_3.html

New built-ins for Java and JavaScript string escaping: j_string and js_string

2.3.1

Date of release: 2005-01-04

https://freemarker.apache.org/docs/versions_2_3_1.html

The js_string built-in now escapes > as > (to avoid ).

2.3.20

Date of release: 2013-06-27

https://freemarker.apache.org/docs/versions_2_3_20.html

Bug fix [390] (and other improvements): ?js_string and ?json_string didn't escape the u2028-u2029 line terminators (problem for JavaScript) and the u007F-u009F control characters (maybe a problem in JSON, depending on implementation). Furthermore, the escaping of , <, and > become safer in that now they are escaped whenever it can't be guaranteed that they won't be part of <!, ]]> or </. Earlier they were only escaped when it was known that they are part of these patterns, thus it was possible to assemble these patterns from two adjacent interpolations. Additionally, from now on <? and --> also count as dangerous patterns, and will trigger < and > escaping.

まとめ

2.3.20 以後、2023年4月現在の最新版である 2.3.32 まで変更はなし。

Java 9 以後での InputStream を String にするやり方

commons-io で IOUtils.toString(inputStream, StandardCharsets.UTF_8) としているケースは、Java 9 以後なら new String(inputStream.readAllBytes(), StandardCharsets.UTF_8) と書けて便利。

https://www.baeldung.com/convert-input-stream-to-string

OpenAPI の enum を使いこなす

https://openapi-generator.tech/docs/templating/#all-generators-core

openapi-generator で enum を制御するにはこのへんを参照。

x-enum-varnames: 変数名 x-enum-descriptions: description

という2つを使って構成する。

varnames はともかくとして descriptions は OpenAPI の spec に入っておくべき要素だと思う。

openapi-generator の custom generator を gradle plugin から使う

https://github.com/OpenAPITools/openapi-generator/issues/6190

openapi-generator は ServiceLoader で generator を探すので、CLASSPATH に custom generator を突っ込むことができれば良いです。

CLASSPATH にカスタムジェネレータを突っ込むには buildSrc を使えば良いので、例えば以下のように階層構造を作れば良いですね。

.
├── build.gradle.kts
├── buildSrc
│   └── src
│       └── main
│           ├── java
│           │   └── my
│           │       └── own
│           │           └── Generator.java
│           └── resources
│               └── META-INF
│                   └── services
│                       └── org.openapitools.codegen.CodegenConfig
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle.kts

buildSrc/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig の中に my.own.Generator の名前を書けば読み取られます。

openapi-geneartor の java generator で NantokaAllOf.java が大量にできる件

AbstractJavaCodeGen あたりを継承して以下のようにメソッドを上書きすれば抑制できる。

    @Override
    public boolean getUseInlineModelResolver() {
        return false;
    }

Rust から NSLog を呼びたい

Mac OS においてデバッグするときには、NSLog を呼ばないととにかくやりづらい、というときがあります(InputMethodKit を使った開発をするときなどが特にそうです)。 そんなときには、以下の様にすると良いでしょう。

use cocoa::base::{id, nil};
use cocoa::foundation::NSString;


#[link(name = "Foundation", kind = "framework")]
extern {
    pub fn NSLog(fmt: id, ...);
}

fn main() {
    unsafe {
        NSLog(NSString::alloc(nil).init_str("Hello, %@"), NSString::alloc(nil).init_str("world"));
    }
}

Dependency は以下のように Cargo.toml に記述します。

[package]
name = "rust-nslog-sample"
version = "0.1.0"
edition = "2021"

[dependencies]
cocoa = "0.24.0"

さて、とはいえ毎回 NSString だなんだと書くのはめんどくさい、というのはあるかも。 せめて最初の引数はマクロ化したいですよね。

↓マクロ化するならこんな感じかな?

use cocoa::base::{id, nil};
use cocoa::foundation::NSString;


#[link(name = "Foundation", kind = "framework")]
extern {
    pub fn NSLog(fmt: id, ...);
}

macro_rules! NSLog {
    ( $fmt:expr ) => {
        unsafe {
            NSLog(NSString::alloc(nil).init_str($fmt))
        }
    };
    ( $fmt:expr, $( $x:expr ),* ) => {
        unsafe {
            NSLog(NSString::alloc(nil).init_str($fmt), $($x, )*)
        }
    };
}

fn main() {
    unsafe {
        NSLog!("Hello, %@ %d", NSString::alloc(nil).init_str("world"), 5963);
    }
}

オープンなかな漢字変換エンジン 0.2 リリースのお知らせ

オープンなかな漢字変換エンジンを開発しているということを先日お知らせしました。 https://blog.64p.org/entry/2023/01/16/032912

そのあと、順調に開発が推移しまして、いい感じにいろいろな機能が盛り込まれました。日本のOSSでは29日に定期リリースするという監修があります(最近聞きませんが。。)なので、今日 v0.2.0 を出しました。出したと言っても tag を打っただけですが。

https://github.com/akaza-im/akaza/releases/tag/v0.2.0

など、利用するにあたって基本的な機能を一通り実装した感じになります(ほかにもいろいろあるのだけど、おおすぎて書ききれない)。

特に大きな変化として、外部辞書を利用できるようになったことがあります。SKK 形式の外部辞書が使えるようになったので、何らかの新しい語彙が必要になった時は、SKK形式の辞書を利用させてもらえばよいのです。圧倒的な安心感ですね〜。

多分、割とインストールして試せるぐらいにはなってきたかなーと思います。ので、試してみていただけるとありがたいです。

本バージョンを出すまでに omasanori さん、tkng さん、pklionさんにご協力いただきました。ありがとうございました!

Rust で整数を漢数字に変換したいよー

こんな感じで実装できる。真面目にやればもうちょいちゃんと書けるけどこんな感じで。。

use std::cmp::min;

const NUMS: [&str; 10] = ["", "一", "二", "三", "四", "五", "六", "七", "八", "九"];
const SUBS: [&str; 4] = ["", "十", "百", "千"];
const PARTS: [&str; 18] = [
    "",
    "万",
    "億",
    "兆",
    "京",
    "垓",
    "𥝱",
    "穣",
    "溝",
    "澗",
    "正",
    "載",
    "極",
    "恒河沙",
    "阿僧祇",
    "那由他",
    "不可思議",
    "無量大数",
];

fn int2kanji(i: i64) -> String {
    let s = i.to_string();
    let chars = s.bytes();
    let p = chars.into_iter().rev().enumerate().collect::<Vec<_>>();
    let mut buf: Vec<&'static str> = Vec::new();
    for (i, b) in p.clone() {
        let c = (b - 48) as usize; // 48 is '0'
        if i % 4 == 0
            && i > 0
            && (i..min(i + 4, s.len()))
                .map(|i| {
                    let (_, c) = p.get(i).unwrap();
                    *c
                })
                .any(|n| n != 48)
        {
            buf.push(PARTS[i / 4]);
        }
        if c != 0 {
            // その桁が 0 のときは区切りを追加しない
            buf.push(SUBS[i % 4]);
        }
        if !(i % 4 != 0 && c == 1) {
            // 十百千を表示したときで、一のときは追加しない。
            buf.push(NUMS[c]); // 48 is '0'
        }
    }
    buf.reverse();
    buf.join("")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_int2kanji() {
        assert_eq!(int2kanji(1), "一");
        assert_eq!(int2kanji(9), "九");
        assert_eq!(int2kanji(10), "十");
        assert_eq!(int2kanji(11), "十一");
        assert_eq!(int2kanji(21), "二十一");
        assert_eq!(int2kanji(99), "九十九");
        assert_eq!(int2kanji(100), "百");
        assert_eq!(int2kanji(999), "九百九十九");
        assert_eq!(int2kanji(1000), "千");
        assert_eq!(int2kanji(9999), "九千九百九十九");
        assert_eq!(int2kanji(10000), "一万");
        assert_eq!(int2kanji(10020), "一万二十");
        assert_eq!(int2kanji(1_000_020), "百万二十");
        assert_eq!(int2kanji(100_000_020), "一億二十");
        assert_eq!(int2kanji(1_0000_4423), "一億四千四百二十三");
        assert_eq!(int2kanji(1_8000_4423), "一億八千万四千四百二十三");
    }
}

最近またLinux用の日本語IMEを作っている

本件は mozc の ut がどうこうとかは関係なくて、ふと linux desktop を使おうと昨年末に思いまして、昨年末からちまちまやってます

https://github.com/tokuhirom/akaza

かな漢字変換って作るの難しいのかなぁ、と思ったので作ってみている。これはまさに Just for Fun でやっている。 わりと普通に自分で常用してる分には困らないかな、というところまできている。

以下は、思ってることの垂れ流しという感じで、まとまってないですが。

「日本語入力を支える技術」という本が 2018年に出ていて、この本の内容を読めば、だいたいエンジン部分は実装できる。Amazon のレビューではこの本よんでも実装できないって書いてあるけど、変換エンジン自体は実装できます。 UI が辛い。けど。

エンジンは、ビタビアルゴリズムで最小コスト法を実装する、とかであればプログラミングできる人なら割とまじで誰でも実装できる。

ただ、どうしてもかな漢字変換エンジンは大量の細かいオブジェクトを取り扱う必要が出てくるので、python とかで実装すると結構遅い。 2年前に一回 pure python で実装したが、おそすぎたのでコア部分を C++ で書き直したという経緯がある。 Python で書いてると、色々なアルゴリズムためす上でも、Python が遅いからだめなのか、アルゴリズムがそもそも遅くて無理なのかがよくわからなくなりがちなんだよな。

UI を Python で書いて、ロジックを C++ で実装したのだがどうもインストールまわりが煩雑になるし、Rust 触ってみたかったので rust で全面的に書き直した。 Pure Rust にしたことによって、SEGV したりして謎に死ぬことがなくなったので快適になったし、高速に動いている。所有権とかがまぁめんどくさいとかはありつつ、わりと慣れれば大丈夫になる。

基本的な方針としては、mozc に近い感じの構成になっていて、最小コスト法でビタビアルゴリズムで統計的かな漢字変換である。 個人で開発しているから Google と違って大量のウェブコーパスを利用できるわけではないから、Wikipedia と青空文庫を形態素解析してそれを学習元として利用している。 mozc は Google の大量の資源を利用できるから変換精度がいいんでしょ、と思う人が多いと思うんだけど、それはまったくもってそのとおり。ただ、書き言葉という点においては、wikipedia ぐらいの規模の言語資源があれば、それを使えば割とそこそこの変換精度は出る。逆に話し言葉はめちゃくちゃ弱いので、話し言葉とかくだけた書き方のコーパスがもっとほしい。 利用できる日本語のコーパスとしては BCCWJ がほぼ唯一に近いもので、BCCWJ を使えばもっと変換精度は向上するんだけど、、個人で利用しようとすると 25万円だかかかるので二の足を踏んでいる。

識別モデルを使う というのも試してみた。今どきだと機械学習的なアプローチがナウいかなぁと思い。。だが、どうしても個人のマシンで動かしていると時間がかかってしょうがない。金をかけられない個人の趣味開発においては、統計的かな漢字変換のほうが向いていると思う。 開発を継続的にやるには無料の環境で使えるのが大事かなと思っている。モデルデータを github actions とかで生成できるのが理想的で、github actions で生成するならチープなスペックのインスタンスで 回せるぐらいのものが良い。github actions は 6 時間しか回せない。

基本的に今は単語 Bi-gram でコスト計算している。tri-gram に拡張するとプログラムが複雑になりすぎるし、有効なケースも限定的なのでまぁ。あまり bi-gram で困ってないし、という。mozc のようにクラスNグラムにしたほうがいいのかなぁ、と思いつつ、単語Nグラムで困ってないのでまぁいいかなぁといったところ。単語Nグラムだと品詞のこととか考えなくていいのがとにかく良い。 日本語の品詞難しいよ〜。個人的には、かな漢字変換の辞書登録で品詞を入れさせられるの、だるいなと前々から思ってたし。

https://komachi.hatenablog.com/entry/20081127/p2 http://www.fenix.ne.jp/~G-HAL/soft/nosettle/anthy.html#patch13

↑このへんとかを見ても思うこと。個人的には SKK の学習の具合を気に入っている。システムで提供するモデルはもちろん大事なのだが、個人の語彙などはたかが知れているので、システムで提供するモデルは「ある程度学習が進むまで耐えられる」ぐらいの精度があればよく、細かいところは個人の学習データで補ってくれや。っていうふうにするしかないのかな、と思っている。 だって個人だと Google みたいな規模のコーパス使えないんだもの。

ユーザーの学習は、unigram と bigram をテキストファイルで ~/.local/share/ とかに落とすようになっていて、自分が変換した結果の確率が学習データにあればそっちが優先されるようになっている。ので、誤変換で確定しちゃっても、正しい変換で何回か確定すればそれが優先されるようになる。このへんの学習ロジックはめちゃくちゃシンプル。基本的にユーザーの語彙は wikipedia 全体の語彙よりも少ないから、分母が小さくなるので、ユーザーの学習結果の確率計算のほうがキツく効くっていう仕組みにしている。

ちなみに、この手のかな漢字変換エンジンって、どういうふうに考えて実装したか、みたいな日記を書いてる人が多くてそういうの読むのって面白いなぁと思う。僕はそのへんの経緯を github issues とかに書くのがいいのかなぁと思ったので github issues とか discussions とか wiki に書いている。バザール的に開発できるような感じの構成にしているつもり、ではある。 お気軽になにかアイデアとかあれば書いていただければ嬉しいし、PR 送っていただければなお嬉しい。

https://github.com/tokuhirom/akaza/discussions

今月中ぐらいに一旦スナップショットリリースを出来たらいいなぁと思っている。

lindera-cli を試す

Rust で書かれた形態素解析機の lindera には CLI もある。

cargo install lindera-cli --features ipadic

のようにして入れる。辞書がオプショナルなので、features オプションを指定しないで入れると何もできない。ipadic か unidic を指定すること。

実行は以下のようになる。

echo "やはり原因はヤッポ" | lindera tokenize -t ipadic
やはり  副詞,一般,*,*,*,*,やはり,ヤハリ,ヤハリ
原因    名詞,一般,*,*,*,*,原因,ゲンイン,ゲンイン
は      助詞,係助詞,*,*,*,*,は,ハ,ワ
ヤッポ  UNK
EOS

cargo test で fork させたい

https://crates.io/crates/rusty-fork

cargo test はプロセスを共有する。これではテストをしづらいケースがある。例えば C のライブラリのラッパーなどの場合、初期化関数を何回も呼んではいけないケースがある。

rusty-fork crate を利用すると、以下のように書くだけで別プロセスでテストを実行させることが可能である。

use rusty_fork::rusty_fork_test;

rusty_fork_test! {
    #[test]
    fn my_test() {
        assert_eq!(2, 1 + 1);
    }

    // more tests...
}

rust の split_once が便利だった。

最近、趣味で僕が書いてるプログラムでは split を 2 こまでしたいというケースが多くて、以下のように書いていた。

fn main() {
    let s = "foo bar";
    let p :Vec<&str> = s.splitn(2, ' ').collect();
    println!("{},{}", p[0], p[1]);
}

これは、実は split_once というメソッドを使えば良い。

fn main() {
    let s = "foo bar";
    let (a, b) = s.split_once(' ').unwrap();
    println!("{},{}", a, b);
}

便利すぎる。