文章を𝓕𝓪𝓷𝓬𝔂な帰国子女っぽくしてくれるクソアプリを作った


文章をかっこよくしたいことってありますよね?
かっこいい文章と言えば、アメリカ帰りの帰国子女やイケてる外資系コンサル社員がニューヨークの風をまとわせて書く、ルー大柴のような英語まじりの文です。
ついでに英文のスタイルも変えて𝓕𝓪𝓷𝓬𝔂な表記にできれば、平凡な文字列も鮮やかに生まれ変わるでしょう。

しかし、単語を翻訳してスタイルの変更を適用するのはなかなか手間です。そこで、一連の変換を自動化する 𝙁𝙪𝙘𝙠𝙞𝙣𝙜 𝙖𝙥𝙥 を作ってみました。

https://notra.herokuapp.com/

概要

おおまかにはこのアプリは以下の流れで動いています。

  1. 入力文を受け取る
  2. 形態素解析を適用
  3. 名詞を Google Cloud の Translation API で翻訳
  4. 英語部分のUnicodeをいろんなパターンに変換して表示

フロントエンド

フレームワーク

WebフレームワークはReactを使いました。その上で、UIフレームワークにAnt Designを使いました。Ant Designはアリババが提供するオープンソースのReact用UIフレームワークで、以下のように書くだけでコンポーネントのUIをいい感じにしてくれます。

import { Tabs, Typography } from "antd";

import { TabContent } from "../../types";

const { Paragraph } = Typography;

const { TabPane } = Tabs;

type StyleTabsProps = {
  options: TabContent[];
  onChange: (key: string) => void;
};

export const StyleTabs = (props: StyleTabsProps) => {
  const tabOptions = props.options.map((op) => (
    <TabPane tab={op.tabName} key={op.key}>
      <Paragraph className="Paragraph" copyable>
        {op.value}
      </Paragraph>
    </TabPane>
  ));
  return <Tabs onChange={props.onChange}>{tabOptions}</Tabs>;
};

Unicode変換

Fancy -> 𝓕𝓪𝓷𝓬𝔂のような変換を行うために、Fancifyというパッケージを使わせていただきました。

以下は実装の一部です。スタイルによっては小文字しかなかったり大文字しかなかったりするため、それに対応してアルファベットの大小を変えています。

const SentenceTransformer = (sentence: str): TabContent[] => {
  const sets = [
    { style: "circled", letter: "both" },
    { style: "negative circled", letter: "upper" },
    { style: "fullwidth", letter: "both" },
    { style: "math bold", letter: "both" },
    { style: "math bold fraktur", letter: "both" },
    { style: "math bold italic", letter: "both" },
    { style: "math bold script", letter: "both" },
    { style: "math double struck", letter: "lower" },
    { style: "math mono", letter: "both" },
    { style: "math sans", letter: "both" },
    { style: "math sans bold", letter: "both" },
    { style: "math sans italic", letter: "both" },
    { style: "math sans bold italic", letter: "both" },
    { style: "parenthesized", letter: "both" },
    { style: "regional indicator", letter: "upper" },
    { style: "squared", letter: "upper" },
    { style: "negative squared", letter: "upper" },
  ] as const;

  const transformedSentences: TabContent[] = sets.map((x, index) => {
    let tabName = "Tab";
    let casedSentence = sentence;
    if (x.letter === "lower") {
      tabName = tabName.toLowerCase();
      casedSentence = sentence.toLowerCase();
    } else if (x.letter === "upper") {
      tabName = tabName.toUpperCase();
      casedSentence = sentence.toUpperCase();
    }
    const key = index + 1;
    return {
      tabName: fancify({ input: tabName + key, set: x.style }),
      value: fancify({
        input: casedSentence,
        set: x.style,
      }),
      key: key,
    };
  });

  return transformedSentences;
};

バックエンド

バックエンドのAPIサーバーにはExpressを使いました。Expressにはexpress-generatorというスケルトン生成ツールがありますが、これが生成してくれるのはJavaScriptのファイル群です。今回はTypeScriptを使いたかったので似たようなものがないか探したところ、express-generator-typescriptというまさに求めていたものがあったので使わせていただきました。こちらは本家よりもさらにいろいろ生成してくれます(今回はクソアプリを作るだけなのでやや過剰感もありましたが……)。

形態素解析

形態素解析はkuromoji.jsのラッパーであるkuromojinを使わせていただきました。
TinySegmenterは分かち書きソフトウェアであり形態素解析による品詞推定はできないので、名詞を見つける必要がある今回の用途には適しませんでした)。

以下は形態素解析で名詞を取り出す部分のスニペットです。

tokenize(text).then((tokenized) => {
  const nouns = tokenized
    .filter((token) => token.pos === "名詞")
    .map((x) => x.surface_form);
}

翻訳

名詞だけ変換できればいいので和英辞書を使うことも考えたのですが、あまりいい辞書がネット上に見つからなかったことと、辞書にない語の扱いをローマ字変換するだけよりは機械に無理やり変換させたほうがおもしろそうだな、と思い機械翻訳を使いました。

最初は月に100万文字まで無料のWatson Language Translatorを使おうかと考えていたのですが、機械翻訳APIの選定時はまだバックエンドを用意せずフロントエンドだけで頑張ろうとしていた(最終的に形態素解析の辞書の重さがネックになりバックエンドを用意することにしました)ため、CORSに対応していないWatson Language Translatorは使えずGoogle CloudのTranslation APIを使うことにしました。こちらは月50万文字まで無料ですが、まあたぶん大丈夫でしょう。

インフラやリポジトリ構成

URLを見ればわかりますがデプロイ先にはherokuを使いました。どうせなら使ったことのないサービスを使ってみたかったのですが、サービスをDockerコンテナ化していて、コンテナのデプロイを無料でさっとできそうなのがherokuくらいだったので今回もお世話になりました。

プロジェクト管理はlernaを使ってモノレポにしてみました。以下のディレクトリ構造のclientディレクトリにReactのコード、serverにexpressのコード、sharedに共通の型ファイルなどが置いてあります。

.
├── Dockerfile
├── README.md
├── heroku.yml
├── lerna.json
├── package-lock.json
├── package.json
└── packages
    ├── client
    ├── server
    └── shared

おわりに

年末年始あまりにも暇だったので始めたのですが、結果的にいろんな技術やツールに触れて楽しかったです。ぜひ遊んでみてください。