HN 日本語サマリー

← 一覧へ戻る
プログラミング

書いたものを読み上げるフォント

A font that reads what you wrote (rohanadwankar.github.io)

69 pointsby RohanAdwankar37 コメント

要約

semfontは、テキストの感情価、重要度、意外性、確実性といった要素を分析し、それに応じて自動的に色、太さ、ハイライト、傾斜などのタイポグラフィを調整するライブラリです。このライブラリは、特にAIシステムが生成する大量のテキストを読みやすくすることを目的としており、JavaScriptで実装され、Reactコンポーネントとしても利用可能です。より高度な分析のために、文脈を考慮した第二段階の処理も追加されています。

全文翻訳

書いた内容を読み上げるフォント

Hacker Newsでの議論

semfontは、タイポグラフィを自動設定する小さなライブラリです。以下に示すように、テキストを自動的にハイライト、色付け、太字、斜体表示し、読みやすくすることを目的としています。

theme editorial loud monochrome technical

仕組み

各単語は4つのスコアを持ちます。それぞれ辞書検索から始まり、周囲の単語に対するいくつかのルールによって調整されます。

感情価 (Valence) は、単語が良いか悪いかを示すスコアで、-1から1の範囲です。3単語前までの否定語は、符号を反転させて減衰させます。これは、「not great」が賞賛の真逆ではなく、軽い不満を表すためです。2単語前までの強調語は、スコアをスケールします。

let v = VALENCE[word] ?? 0;          // great -> 0.75
if (negatorWithin(3)) v = -v * 0.74; // not great -> -0.55
v *= gain;                           // really great -> 0.98

重要度 (Salience) は、単語がどれだけ注目に値するかを示すスコアで、0から1の範囲です。頻度リストは各単語に希少性を与え、英語で最も一般的な100語のいずれかであれば0、見たことがない単語であれば1となります。希少性だけでは不十分なため、このスコアは特定のテキストで単語がどれだけ繰り返されるかにも依存します。つまり、あまり一般的でない単語を繰り返し使うことは、そのテキストが何について書かれているかを示します。

const seen = Math.min(1, (repeats - 1) / 2);
const repetition = 0.45 + 0.55 * seen;
let s = SALIENCE[word] ?? 0;
s = Math.max(s, 0.55 * rarity * repetition);

// rarity('the') 0.00, rarity('kubelet') 0.93
// kubelet said once   -> 0.23
// kubelet said 3 times -> 0.51

意外性 (Surprise) は、文の転換点を示すスコアで、0から1の範囲です。「suddenly」や「ironically」のような単語はそれ自体で意外性を示します。そうでなければ、それは位置から来ます。対比語の後の6単語までは距離に応じて減衰しながら意外性が与えられ、また、その区間の他の単語よりも著しく希少な単語にも与えられます。

let s = SURPRISE[word] ?? 0;         // suddenly -> 0.85
if (afterContrast) {
  s = Math.max(s, 0.45 * 0.82 ** (distance - 1));
}
s += 0.3 * Math.max(0, rarity - passageMeanRarity - 0.25);

// 'The tests failed'            -> failed 0.16
// 'It compiled, but the tests failed' -> failed 0.44

確実性 (Certainty) は、書き手がどれだけ確信を持っているかを示すスコアで、-1(曖昧)から1(断定)の範囲です。「probably」や「definitely」のような単語はテーブルにあります。しかし、「The build probably failed」と書いた場合、単語「probably」について不確かなのではなく、それが失敗したかどうかについて不確かです。そのため、この曖昧さは単語自身のスコアを保持し、文中の他のすべての単語はそのスコアの55%を受け取ります。文全体が中央の1単語ではなく、少し傾くようになります。

c = CERTAINTY[word] ?? sentenceCertainty * 0.55;

// 'The build probably failed.'
// probably -0.40, every other word -0.22

その後、テーマが各スコアをタイポグラフィの軸にマッピングします。感情価は色、重要度は太さ、意外性はハイライト、確実性は傾斜に対応します。各軸にはしきい値があるため、ほとんどの単語は変更されずにそのまま表示されます。

アルゴリズムの改善

これらのルールは数単語しか考慮しないため、ウィンドウには盲点があります。「I would not go so far as to call the new editor great」と書くと、「not」が9単語後ろにあるため、「great」は緑色のままです。「We fixed the crash」と書くと、「fixed」とそれが修正したものを何も接続しないため、1つの緑色の単語と1つの赤色の単語が表示されます。

そのため、ウィンドウルールが実行された後に2回目のパスが実行され、節全体を読み取ります。否定語は節の最後まで到達し、距離に応じてフェードアウトするため、「great」は赤色になります。「fixed」、「recovered」、「avoided」のような動詞は、それに続くものを改善されたものとしてマークするため、「crash」は緑色になります。同じパスは、「less broken」と「fewer complaints」を改善として、「too simple」を不満として、「Great,」という単語が悪いニュースの前に単独で現れるのを皮肉として読み取ります。

すべての変更は単語に記録されるため、単語がなぜその色になったのかを尋ねることができます。

analyze('We fixed the crash.').tokens[6];
// { text: 'crash', valence: 0.44,
//   notes: ['resolved by "fixed"'] }

これは最初のパスと同程度のコストで、予算内に収まるため、切り替えるスイッチはありません。これらは、それにつながった10の文であり、それらの2つも含まれています。左が最初のバージョン、右が現在のバージョンです。

before

now

Great, another outage. Just what I needed today.

Great, another outage. Just what I needed today.

We fixed the crash and closed the security hole before anyone noticed.

We fixed the crash and closed the security hole before anyone noticed.

The cluster recovered from the crash in under a minute.

The cluster recovered from the crash in under a minute.

We avoided a catastrophic outage by catching the bug in staging.

We avoided a catastrophic outage by catching the bug in staging.

I would not go so far as to call the new editor great.

I would not go so far as to call the new editor great.

Less broken than last week, and far fewer complaints.

Less broken than last week, and far fewer complaints.

The memory leak is gone.

The memory leak is gone.

The reviewer called it "terrible", which is wrong.

The reviewer called it "terrible", which is wrong.

The API is too simple and the docs are too clever.

The API is too simple and the docs are too clever.

使用方法

import { SemanticText } from 'semfont';

<SemanticText as="p">
  The migration ran clean on staging. In production it deleted the index,
  and the rollback failed too.
</SemanticText>

テーマを選択するか、必要なチャンネルのみを選択できます。

<SemanticText text={incident} theme="monochrome" />
<SemanticText text={incident} channels={['valence']} />

独自の語彙を教え込むことができます。

<SemanticText
  lexicon={{ valence: { flaky: -0.7, oncall: -0.4 }, salience: { rollback: 0.8 } }}
  text={incident}
/>

私がこれを始めたケースについて説明します。AIシステムは大量のテキストをストリームしますが、それをすべて読むのは困難です。そのため、この意図は、ほとんどのストリーミングコンポーネントに簡単に組み込んで、テキストを読みやすくするためのライブラリです。

import { useChat } from '@ai-sdk/react';
import { SemanticText } from 'semfont';

function Chat() {
  const { messages } = useChat();
  return messages.map((m) => {
    const text = m.parts.filter((p) => p.type === 'text').map((p) => p.text).join('');
    return m.role === 'assistant'
      ? <SemanticText key={m.id} as="p" text={text} />
      : <p key={m.id}>{text}</p>;
  });
}

Reactをスキップしてスコアを取得することもできます。「analyze」はエンジン単体で、単語ごとの4つの数値、CSSなしで、これらのインポートはReactなしでも動作します。

import { analyze } from 'semfont/analyze';
import { styleFor, themes } from 'semfont/theme';

const { tokens } = analyze('The rollback failed too.');
tokens[4];   // { text: 'failed', valence: -0.7, salience: 0.23, surprise: 0.06, certainty: 0, ... }
styleFor(tokens[4], themes.editorial).style;   // { color: 'color-mix(in oklab, currentColor, oklch(0.58 0.19 25) 53%)' }

最後の形式は、このページが機能する方法です。ここにはバンドラーがないため、1つのインポートマップがブラウザにsemfont/analyzeとsemfont/themeの場所を示し、npmのバージョンにピン留めされています。上記のデモボックスは、Reactコンポーネントと同じ3行です。分析、スタイル設定、レンダリングを行います。

<script type="importmap">
{
  "imports": {
    "semfont/analyze": "https://cdn.jsdelivr.net/npm/semfont@0.2.0/src/analyze.js",
    "semfont/theme":   "https://cdn.jsdelivr.net/npm/semfont@0.2.0/src/theme.js"
  }
}
</script>
<script type="module">
  import { analyze } from 'semfont/analyze';
  import { styleFor, themes } from 'semfont/theme';
</script>

次のステップ

実装から推測できるかもしれませんが、完璧な分類を達成しつつ、ストリーミングを遅延させないほど高速にすることは本質的に不可能です。しかし、テキストを読みやすくするという目的のためには完璧である必要はなく、いくつかの単純なヒューリスティクスが十分な効果を発揮する可能性があります。とはいえ、ヒューリスティクスで試すのが興味深い皮肉のようなケースや、埋め込み節を持つ否定のような、解析可能なケースも存在するかもしれません。さらに、ストリーミングコーディングエージェントのために、語彙に含める価値のある技術用語もあります。

コードとデモは github.com/RohanAdwankar/semfont にあります。