BudouXで日本語の改行位置を読みやすくする (Flutter)

# はじめに
* 文章を書く時に**改行位置**をどこで区切るかは難しい。画面右端に達したら折り返す仕組みは様々な言語でサポートされているが、中途半端な所で改行されると読みにくい
* Googleから読みやすい改行を実現する、BudouXと呼ばれるモデルが開発された 
* 本ページでは、BudouXの概略とFlutterで使用してみた結果を残す

# BudouX
## BudouXとは
* Google JPが開発した、読みやすい改行のためオープンソースの分かち書き器
    * [ニュースリリース](https://developers-jp.googleblog.com/2023/09/budoux-adobe.html)
    * [GitHub](https://github.com/google/budoux)
* 日本語、中国語、タイ語をJava, Python, JavaScriptでサポート
* 特徴
    * スタンドアロンで動作
    * **20KB程度に収まる軽量モデル**
    * 言語に依存しない（学習させることができる）
## モデルの特徴
BudouXは、ブースティング（弱学習器の繰り返しによる学習）を使ったAdaboostでモデル構築されており、日本語モデルのJsonの一部を下記に示す
```json
{
  "UW3": {
    "。": 6699,
    "、": 4784,
    "を": 5769,
    "っ": -1853
    ...
  },
  "UW1": {
    "お": 922,
    "ご": 890,
    "ふ": 1899,
    "甘": -1424
    ...
  },
  "BW1": {
    "のみ": 3058,
    "以上": 1223,
    "まま": 2830,
    "ちが": -2703
    ...
  },
  "BW3": {
    "うま": 4971,
    "もの": 3479,
    "あり": 1053,
    "おり": -2875,
    ...
  },
  "TW2": {
    "気に入": -4086,
    "ではな": -1086,
    "とがあ": -1130,
    "そもそ": -1441,
    "してい": -406
  },
  "TW1": {
    "という": 545,
    "ていく": 1686,
    "ような": 1298,
    "しかし": 2079,
    "持って": -1851
  }
  ...
}
```
* UW3, BW1, ... といった各カテゴリに対して、キーバリューが定義
    * UW (Unigram Wieght)：特定の位置にある文字の重み
    * BW (Bigram Weight)：隣接する2つの文字の組み合わせの重み
    * TW (Trigram Weight)：隣接する3つの文字の組み合わせの重み
* つまり、「何の文字が来たら改行するだろう」と、文字の種類ごとに重要度を割り当てているシンプルなモデル
    * 今回利用した[BudouXモデルの処理コード](https://github.com/KoheiKanagu/budoux-dart/blob/main/lib/src/budoux_dart.dart)を見ると、UX, BW, TWのスコアを算出し、スコアが0以上のときに改行している

# Flutterのサンプルサンプルコード
日本語で書かれた文字列を読み込み、表示するサンプルを以下に示す。
なお実装には、BudouXをFlutterに移植されていた[budoux_dart](https://pub.dev/packages/budoux_dart)を利用した。
```dart
import 'package:budoux_dart/budoux.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() {
  runApp(const MainApp());
}

class MainApp extends StatelessWidget {
  const MainApp({super.key});

  // BudouXモデルを非同期でロード
  Future<BudouX> loadBudouxModel() async {
    final modelJson = await rootBundle.loadString('assets/ja.json');
    return BudouX(modelJson);
  }

  // テキスト
  final simpleText = '今日は全国的に晴れです。洗濯物がすぐに乾きます。ですが、'
      '明日は1日中曇りになる見込みです。傘の準備をしておいた方が良いでしょう。'
      'さらに、明後日は雨です。';

  final complexText = 'Flutter（フラッター）は、Googleによって開発されたフリーかつ'
      'オープンソースのUIのSDKである。単一のコードベースから、Android、iOS、Linux、macOS、'
      'Windows、Google Fuchsia向けのクロスプラットフォームアプリケーションを'
      '開発するために利用される。'
      '2018年12月4日、ロンドンで開催されたFlutter Live 18にて、初の正式版となる'
      'Flutter 1.0のリリースが発表された。';

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
          body: FutureBuilder<BudouX>(
        future: loadBudouxModel(),
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(child: CircularProgressIndicator());
          } else if (snapshot.hasError) {
            return const Center(child: Text('Error loading model'));
            // コンテンツの表示
          } else {
            return buildContent(snapshot.data!);
          }
        },
      )),
    );
  }

  Widget buildContent(BudouX budouX) {
    final parsedSimpleText = parseText(budouX, simpleText);
    final parsedComplexText = parseText(budouX, complexText);

    return Column(
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      children: [
        // simpleTextの改行なし文章 & BudouX適用した文章
        Row(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const SizedBox(width: 20),
            buildContainer(Text(simpleText)),
            const SizedBox(width: 20),
            buildContainer(Wrap(children: parsedSimpleText)),
          ],
        ),
        // complexTextの改行なし文章 & BudouX適用した文章
        Row(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const SizedBox(width: 20),
            buildContainer(Text(complexText)),
            const SizedBox(width: 20),
            buildContainer(Wrap(children: parsedComplexText)),
          ],
        )
      ],
    );
  }

  Widget buildContainer(Widget child) {
    return Container(
      decoration: BoxDecoration(
        border: Border.all(),
      ),
      width: 200,
      child: child,
    );
  }

  // テキストを解析してウィジェットのリストを返すメソッド
  List<Widget> parseText(BudouX budouX, String text) {
    // BudouXでは、改行の単位でリストに格納されている
    return budouX.parse(text).map((rowText) => Text(rowText)).toList();
  }
}
```
```yaml
...

dependencies:
  flutter:
    sdk: flutter
  budoux_dart: ^1.0.2

...

flutter:
  uses-material-design: true
  assets:
    - assets/ja.json
```
# 実行結果
* 左側：原文、右側：BudouXで加工した文
* 単純な平仮名・漢字で構成された文章は、読みやすく改行されている
* 一方、**英語が混じった文章はモデルに組み込まれていない**ため、読みやすいとまでは言えない（多分、カタカナも？）
    * 例えば、"Google Fuchsia"や、"開発するために"、"Flutter Live 18"とかは改行したせいで読みにくい印象
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3751499/7e0e9f9c-cef9-75c9-e0ee-1ef543eaf54d.png)

# まとめ
* BudouXは、軽量なモデルで利用できる改行
* シンプルな文章では、その強みが発揮されていそう
* 一方で、英語やカタカナが混じる文章では、中途半端な位置で改行されていた
* 安易にUIに組み込むよりも、使いどころが大事そう

# 参考文献
* [BudouXをFlutterで動かしてみた](https://zenn.dev/kingu/articles/a90b3d22e45268)