| @雑談

長田部海床路

生活

ランニングレース

熊本城マラソン

直前の食あたりと靴の取り間違いでコンディション最悪だった。加えて転倒もあり、マラニック状態で 5 時間オーバーでゴール。リタイアしなかったのは偉い。来年は雪辱を果たしたい。

ASO VOLCANO TRAIL

初のウルトラトレイル( 112km )。3 月、 4 月と自分にしてはしっかりトレーニングしてから臨んだ。25 時間以上かかって記録はいまひとつたが、完走できたことがうれしかった。

トレーニングで三瀬峠脊振山ピストン練を 3 回やって、糸島四座縦走が楽に感じられるくらい強くなった( 4 時間半でゴールできた)。

福岡マラソン

直前期にあまり走れていなかったので不安だったが、マラソン自己ベストを更新できた。もう少し練習して体重も落とせていたらもっと速く走れたかもしれない。

総括

マラソン2本とトレラン1本だった。カントリーレースも出たかったがうっかりしていてエントリーできなかった。

ボルケーノに備えてトレーニングしたおかげでランニングのレベルが一段階上がった気がする。 5'10"/km くらいのペースで走るのがそんなにしんどくなくなった。

ボルケーノに出るためにだいぶ時間とお金を使ったのであんまり出なくても良いかなぁと思っていたが、やはりたまにはレースに出ないと張り合いがない。来年はもうちょいレースに出たい。

登山・トレラン

脊振山系全山縦走トレラン

初めての夜通しのトレラン。 ASO VOLCANO TRAIL の予行演習になった。同僚にサポートしてもらってめっちゃうれしかった。

祖母・傾ファストパッキング

いわゆる祖母傾完全縦走。九合目小屋に泊まった。 20L の Rush 20 では宿泊装備が入りきらなかった、 Rush 30 も買うか…

祖母傾完全縦走

高尾山ハイキング

出張ついでに訪問、日本で一番登山者が多い山を訪れることができた。トレラン装備の人が多かった。ハイキングの人もトレラン装備で登ってた。結構年齢が上の人でもトレラン風の装備だった。来年あたり九州でもそういうスタイルが一般的になるかも。

下山後に食べ飲みした蕎麦とクラフトビールがうまかった。都会の山ならでは。

高尾神社

TAKO BEER

国見岳山頂祠再建ボランティア

熊本県最高峰の国見岳の山頂祠が倒壊したままなのは、熊本県民にとっては残念だった。再建をすると聞いて、少しでも役に立ちたいと歩荷のボランティアに行った。あまり役には立てなかったが、祠の再建に立ち会えたことは良かった。

国見岳山頂祠

八ヶ岳(硫黄岳)登山

出張ついでに訪問、九州の山ばかり登っていてはダメだと若い同僚にけしかけられて登りに行った。八ヶ岳、ガスガスで何も見えなかったが、久しぶりの森林限界突破と、有人の営業小屋のもてなしで心がほっとした。防風のなか、雨風を凌いでゆっくりできる場所を用意してもらえてるだけでありがたい。

八ヶ岳の森

インターネットトレラン

nagayama さん、 nmy さんとのインターネットトレランも思い出深い。インターネットとトレランという二つの趣味の交錯だし、なんなら仕事と趣味の交錯でもあり、とてと楽しい時間だった。

坊がつる野営( Happy Hikers Hokkein Gathering )

同僚と行ったが野営したのは一人(途中から別行動)。一昨年の方が寒かったが、 Hammock Bivvy Tyvek を使わなかったので寒くて眠れなかった。野営は定期的にやってないと勘が鈍って失敗する。

夜の三俣山

韓国岳

寒い日に登り、雪景色の山を堪能した。えびの高原は雰囲気抜群で、またキリエビに出たいなと思った。

高千穂峰

南阿蘇カルデラトレイルボランティア

二度ほど出場したことがある南阿蘇カルデラトレイルにボランティアとして参加した。ボランティアしんどい。 4 時半集合だったので 1 時半に起きて 2 時過ぎに福岡の家を出て向かった。自分が出た年は雪が降っていてソフトフラスクが凍り付いたが、今年はあたたかくて走りやすそうだった。自分もまたレースに出たくなった。

大矢岳へのトレイル

総括

脊振山系全山縦走と祖母傾完全縦走が思い出に残っている。どうしても限界までチャレンジするような登山の方が印象に残りやすい。年に 1 、 2 回、こういう登山をするといいのかも。

一方で、高尾や八ヶ岳、韓国岳へののんびりハイクも楽しかった。きつい登山とゆるハイクをバランス良くやりたい。ハードなトレランばかりだと疲れる。

今年は九州脊梁に泊まり登山に行けなかったのが残念。年に一回は脊梁で野営らしい野営をしたい。

買い物

Novablast 5

レースシューズに Magic Speed 2 を持っていたが、デイリートレーナーでは初のアシックス。めっちゃ走りやすくてランニングが楽しくなった。ベアフットシューズで足は鍛えられるがどうしても距離が踏めない。アルトラは良いが値段が高すぎる。アシックスがコスパよい。

Novablast 5

iPhone 17 Pro

iPhone 14 Pro からの買い換え。目玉が飛び出るほど高かったが iPhone 14 Pro の下取り価格が高くて助かった。カメラのデザインはどうかと思うが画質がメチャ良くなっててうれしい。 USB-C ケーブルになったのも地味に便利。

Garmin Epix Pro

検証機で Garmin Fenix 7 を使って Garmin に宗旨替えすることにした。 Garmin はヘルスケアや運動関連で踏み込んだアドバイスをしてくれる。 Apple Pay や Mac や iPhone のロック解除、 Siri は便利だったがランニングが趣味なら Garmin が一番いいと思う。

OTTO Cast Mini

Car Play がワイヤレス化されて最高便利。Amazonアソシエイトリンクを貼ったら結構買ってもらえててビックリ。みんな同じような悩みを抱えているっぽい。

HOUDINI Moon Walk Vest

これはガジェット系ではなく服。ほぼ毎日着ている。秋はTシャツの上に羽織り、冬もフリースの上に羽織ったりしてる。暑がりなのでダウンジャケットなどは暑すぎる。こういうのがちょうど良い。

HOUDINI Moon Walk Vest

水出し緑茶ティーバッグ

社長からもらって気に入って自分で買って飲むようになった。水出しでよく出ておいしい。一パックあたり 50 円くらい。毎日ペットボトルのお茶買うのは環境に良くないしお金ももったいない。

総括

年をとったせいか、同じものをスペアで買ったり、買い置きを増やすことが多くなった。その結果見つからなくなったり、食べ物なら腐らせてしまったりする。もっと賢くお金使えるようになりたい。

音楽・本・映画

国宝

Audible で聞いたがめっちゃよかった。映画見に行きたいが暇がなくて行けてない。

Yogee New Waves

ライブに行った。 Good Bye をすり切れるくらい聞いた。

Oasis

ライブのチケットは何度も申し込んだけど当たらなかった。 Live Forever と Supersonic と Acquiesce を何度も聞いた。

Wilderado

スターバックスの店内 BGM を聞いて知ったインディフォークロックバンド。 Surefire という曲がめっちゃいい。いつかアメリカでライブに行ってみたい。

総括

映画は1本も見てない。ドラマすら見てない。 YouTube ばかり見てしまった。来年は1本くらい映画見たい。

ブログ

写真でふりかえるシリーズを 12 ヶ月分書いた

書くネタがないが写真の整理がてら毎月ふりかえるのはよかった。今後も続けたい。

Ruby 3 で動くようにした

Ruby 2.7 で動かしていたのでとりあえず最低限の対応をして Ruby 3.1 にした。 Ruby 3.1 も EOL を迎えているのでなんとかしたい。

フォトギャラリー

私家版 Flickr のようなやつを作った。撮った写真をどこかに上げたいたいという欲求があったが、 Instagram はなんかちょっと違う。 Flickr のような場所が良いが、いまの Flickr は値段が高すぎるのでフォトギャラリー機能を実装してしまった。ちょうど良いアウトプットの場が作れてうれしい。

AI を使って機能の追加

要約生成、自動タグ付け機能を作った。 Dify を使ってポータルシット問いかけ君( AI チャットボット)も使った。開発のサポートだけじゃなく、機能の一部として AI を使う時代になってきてる。

総括

職業エンジニアじゃなくなってブログの維持管理がさすがに厳しくなってきた。いつまでできるかわからないがボケ防止と思って頑張る。

仕事

プロダクトマネジメント

プロダクトマネージャーの役割に葛藤する一年だった。結局、経営やビジネスサイドに言われたことを実行するだけで、プロダクトマネージャーはプロジェクトマネジメントしかやれてなくてつまらない、という不満を部下から聞かされることが多かった。やりがい云々の前に、確かにビジネスとプロダクトが分断されていてなかなか大きな成果を出せなかった。この構造をなんとかしなければならないが、しかしこのような分断が起きるのは自分を含むプロダクトマネージャーが身銭を切りきれていないからではないかと思い至った。コンフォートゾーンを出てリスクを取りにいかないと信頼されないし、大きな権限は渡してもらえない。もっとプロダクトマネージャーが事業を引っ張っていくような体制をつくっていきたい。

とはいえ、必ずしもプロダクトマネージャー自身が革新的なアイディアを思いつく必要はなくて、プロダクトディスカバリーのプロセスをめっちゃ丁寧にやることが大事だと思う。なぜこのプロダクトを自分たちが作る必要があるのか、このプロダクトで世の中にどんな変化を引き起こすのかをつまびらかにする。この部分だけで記事が書けるので別に書こうと思う。

開発部長業

エンジニアに対してもプロダクトマネージャーと同じで、もっとリスクをとってほしいと思うことがしばしばあった。9月にバズってた以下の記事が良かった。

心理的安全性についても結構議論した。職場ではこれまでも心理的安全性を高めることが大事だと言われてきたが、なぜ心理的安全性を高める必要があるのかについての議論が不足していたように思う。働きやすさとかそういうのは二の次で、個々人にフィードバックが適切に行われ、フィードバックによって学びを得て生産性を高めていくことが開発組織における心理的安全性を高めなければならない根拠になると思う。心理的安全性がなければフィードバックをする方は怖くてフィードバックできないし、フィードバックを受ける方も個人攻撃ではないという確信がなければそのフィードバックを素直に受け止めることができない。

これらが機能するために、ダニエル・キムの成功循環モデルはめっちゃ重要だと思う。関係性の質、思考の質、行動の質、結果の質にはループ構造になっているというもの。めっちゃスタープレイヤーが揃っているチームじゃなくても、関係性の質が良ければ結果を出すということは身をもって知った。自分が何となくチームビルディングできていたのでなんとかなるだろうと思っていたけど、これは仕組み化しないとダメだとわかった。チームビルディングをチーム任せにせず、少々御節介でも組織として積極的に関与していくことが大事だと思う。

総括

自分は起業できるほど人間力高くないが、それでも世の中に影響を及ぼすことをやって人生終えられる仕事がいまのような仕事(中間管理職兼プロダクトマネージャー)なのかなと思ってる。来年はどうすればもっと自分の強みが生きるのか考えて行動していきたい。なんのかのと書いているが、とにかく結果を残したい。

| @ブログ

カテゴリー検索

ふと思い立ってカテゴリーで検索できるようにしたら便利だろうと改修してみた。 Tantiny には facet_query というものがあって、カテゴリー名などは検索インデックスの型を facet にしておくと、カテゴリーだけを検索対象にできる。実装してみたところ便利。カテゴリー内の記事一覧ページを表示する機能はあるが、本文やコメントなども読み込むため遅い。タイトルだけ一覧でがっと欲しいときにカテゴリー検索は便利。

同様にタグでも検索できるようにしてみた。こちらは term_query を使っている。本文の検索は検索キーワードを形態素解析してトークナイズされた文字列で検索しているが、タグ名での検索は exact match をするようにしている。なので過不足あればヒットしない。完全一致で検索したいときに便利。

ただ、これまで記事にタグを設定してこなかったのでタグ未設定の記事が多い。ということで ChatGPT に記事の内容を読ませてタグを自動生成してもらうようにした。ちょっとへんちくりんなタグを設定することもあるが、文脈を読んでタグを設定してくれる。本文中にキーワードはないけど記事の内容に合致するタグを選んできたりもする。賢い。

ちなみに今回 ChatGPT のモデルを gpt-4o から gpt-5-mini に変更した。 GPT 5 系に移行するためには Chat API から Responses API への移行が必要だった。最初それがわからず難儀したが、 Responses API の方がパラメーターがシンプルで使いやすい。例えば JSON のフォーマットを指定したいとき、 Chat API だと JSON Schema で定義を書いて渡す必要があったが、 Response API であればプロンプト中に「こういうフォーマットでくれ」と書けばそのフォーマットで返してくれる。すごい。

require 'openai'

class EntryTagGenerator
  MODEL = 'gpt-5-mini'
  PROMPT = <<~EOF
添付のブログ記事に関して、文章の内容を反映したタグを付与してください。ただし、以下の条件を遵守してください。

1. ブログ全体のタグの一覧は「 %s 」です。ふさわしいものがあればそこから利用してください。なければ新しいタグを考えて設定してください。
2. タグの数は最大で 5 個までとします。
3. すでにタグが付与されている場合は、現在のタグを加味して 5 個までタグを付与してください。現在のタグが内容にふさわしくなければ削除しても構いません。
4. タグは極力一語で構成されるようにしてください。固有名詞の場合はその限りではありません。
5. 略語は避けてください。固有名詞の場合はその限りではありません。
6. 「減量ダイエット」のような意味が重複する二つの単語で構成されるタグは付与しないでください。固有名詞の場合はその限りではありません。
7. タグは半角スペースを含んでも問題ありません。"GoogleAnalytics" ではなく "Google Analytics" でよいです。
8. レスポンスは JSON フォーマットで、以下のような形式にしてください。

\```json
{
  "tags": ["tag1", "tag2"...]
}
\```

# ブログ記事 #

## タイトル ##

%s

## 本文

%s

## すでに付与済みのタグ

%s
EOF

  attr_reader :response

  def initialize(title, body, tags)
    @title = title
    @body = body
    @tags = tags
    @client = OpenAI::Client.new(access_token: ENV['OPENAI_API_KEY'])
  end

  def generate
    @response ||= begin
                    request = @client.responses.create(
                      parameters: {
                        model: MODEL,
                        input: input,
                        reasoning: { effort: 'minimal' }
                      }
                    )
                    JSON.parse(request.dig("output", 1, "content", 0, "text").strip)
                  rescue => e
                    puts "タグ生成エラー: #{e.message}"
                    nil
                  end
  end

  def input
    PROMPT % [Tag.joins(:entries).pluck(:name), @title, @body, @tags]
  end
end

| @ブログ

OpenAI の API を利用してブログの本文の要約を自動生成する機能をつけてみた。ブログの編集画面に要約欄を追加し、チェックボックスにチェックが入っていれば OpenAI の API にリクエストを投げて記事本文の要約を生成して保存するようにした。

要約自動生成君

これも ChatGPT に設計を依頼してレビュー&手直ししながらやった。めっちゃ便利。要約生成君のコードはこんな感じ。

require 'openai'

class EntrySummarizer
  MODEL = 'gpt-4o-mini'

  def initialize(content)
    @content = content
    @client = OpenAI::Client.new(access_token: ENV['OPENAI_API_KEY'])
  end

  def summarize
    response = @client.chat(
      parameters: {
        model: MODEL,
        messages: [
          { role: "system", content: "以下のブログ記事を著者になりきって、日本語で簡潔に要約してください。文章の長さは200文字以内で、受動態表現と「ですます」調を避けて下さい。" },
          { role: "user", content: @content }
        ],
        temperature: 0.3,
        max_tokens: 150
      }
    )
    response.dig("choices", 0, "message", "content").strip
  rescue => e
    puts "要約生成エラー: #{e.message}"
    nil
  end
end

| @ブログ

OGP の og:image を動的に生成する機能をブログに実装していた( 1 年半も前)。

記事本文中に画像がある記事であれば og:image は本文中に含まれる最初の画像を og:image として設定するようにしている。画像がない文章だけの記事の場合はこれまでサイトのロゴを og:image として表示していた。それだと金太郎飴っぽくなってしまうので、はてなブログとか Qiita とかがやってるみたいに、タイトルとサイトロゴを使って動的に og:image を生成して表示することにした。

こだわりポイントとしては、日本語のタイトルの折り返し位置をいい感じにするために形態素解析して、ちょうどいい折り返し位置を決定するような処理を実装した。この辺のコードは結構頑張ってる。

def nm
  @nm ||= Natto::MeCab.new(
    userdic: File.expand_path('lib/tokenizer/userdic.dic'),
    node_format: "%M\t%H\n",
    unk_format: "%M\t%H\n"
  )
end

def prepare_text(text:)
  splitted_text = nm.enum_parse(text).map(&:feature)
  row_length = 0
  result = []
  do_loop = true
  while do_loop do
    splitted_text.each.with_index(1) do |item, i|
      result[row_length] ||= ''
      if (result[row_length].length + item.length) > INDENTION_COUNT
        row_length += 1
        result[row_length] = ''
      end
      result[row_length] += item
      do_loop = false if splitted_text.length == i
    end
    do_loop = false if ROW_LIMIT - 1 > row_length
  end
  result.each {|item| item.gsub!(/EOS\n\z/, '') }
  if result[-1].length == 1
    result[-2] += result[-1]
    result.pop
  end
  result.map(&:strip).join("\n").gsub(/"/, '\"').chomp
end

結果はこんな感じになる。

実際に動的に生成されたこの記事の og:image

| @技術/プログラミング

バッチ処理を動かしている Docker コンテナ含めてすべての環境を Ruby 3 で動かせるようになった。以下の点に難儀した。

  • MeCab が Google Drive からダウンロードできなくなっているので代替を探した
  • Tantiny が依存する rutie という Rust と Ruby をブリッジする gem が新しめの Rust に対応しておらず、 Rust のバージョンを 1.77 に固定する必要があった
  • ActiveRecord が v6 に上がったことにより、 DATABASE_URL を環境変数で渡すことで DB 接続設定を上書きできなくなってしまった
    • 設定ファイルの方を優先して読み込むようだった

ついでにキャッシュも効くように修正した。 sinatra-cache がおかしかったのは Haml の挙動が変わって - form_tag としていたところを = form_tag とする必要があるのと同様に、 - cache_fragment= cache_fragment にする必要があった。再びキャッシュが効くようになって高速になったが、一部 HTML タグが混ざって表示されることがある。 sinatra-cache.gem が依存する sinatra-outputbuffe.gem の方に問題がありそう。この gem は 16 年以上更新されていない。どこかでキャッシュ依存はやめないといけないかもしれない。

| @技術/プログラミング

先週末と今日ガチャガチャやって、ようやく Ruby 3 にアップグレードすることができた。 Ruby 2.7.3 → Ruby 3.1.6 。ただ Ruby 3.1 は今年の 5 月に EOL を迎えるみたいなのでこちらもさっさと新しいバージョンの Ruby に上げないといけない。

やったことは一つ前の記事に加えて以下。

kaminari-sinatra の SinatraHelpers が Ruby 3 & ActionView v6 対応していなかったのでちょこちょこと修正した。

次に padrino-helpers が Ruby 3 と Haml v6 に対応していないのを対応させた。具体的には form_tag の中身のタグが過剰に escape されてしまうので、あんまり良くないかもだが capture_html したやつを html_safe した。 form_tag の内側に来るものはユーザー投稿コンテンツではないはずなのでエスケープはサイト管理者側でできるはず。

ドキュメントでは

= form_tag

!= form_tag

にしろとは言われているが、 form_tag の中身で concat_contet してる片方( capture_html(&block) の結果)が html_safe? => false になるので、 View テンプレート側で何かやっても意味がない( Buffer が汚染されると View で html_safe しても汚染された部分の文字列はエスケープ済みになっている)。

kaminari-sinatra も padrino-helpers も本家にパッチを送ると良いのだろうが、職業プログラマーではなくなったのでなかなか腰が重い。 kaminari-sinatra はテストが通らないし、 padrino-helpers は git clone で submodule の clone に失敗するのでテストが実行すらできないかもしれない。

心の余裕ができたらやってみる。

ちなみに Ruby 3 化するにあたり sinatra-cache を完全に捨てたので負荷が上がるかも。オリジナルの gem は 15 年くらいコミットされてなくて fork して使い続けてきたけど Sinatra や Haml の変更に追従できる気がしないのでいったん捨ててみる。

| @技術/プログラミング

このブログは Ruby 2.7 でずっと動かしていた。コミットログをたどると 2020 年の 1 月から Ruby 2.7 のようだ。 Ruby 2.7 は 2023 に EOL を迎えている。

さすがにまずいと思ったので Ruby 3 にしようと一日頑張ってみたが、なかなかうまくいかない。Ruby 3 の キーワード引数の仕様変更はかなり対応がきつい。どこで ArgumentError が起こっているのかが極めて追いかけづらい。他のメソッドに委譲している場合などは特に。

ガチャガチャやってトップページと個別記事ページまでは Ruby 3 化できたので Ruby 3 でデプロイしてみたが、動かない画面があることに気がついたので Ruby 2.7 に戻した。 Kaminari がちゃんと動かない(具体的には kaminari-sinatra と actionview v6 系の互換性がない)のが原因でページネーションするページがちゃんと動かなそうだったので Ruby 3 化は諦めた。 kaminari-sinatra の ActionViewTemplateProxy#initialize を actionview v6 対応させないと無理っぽい。

kaminari のような有名な gem の派生 gem ならきちんとメンテされてるかなーと思っていたが、 kaminari-sinatra の最終コミットは 4 年前だった。

Ruby で View まで作る人たちはほとんどいなくなってるのだろう。

なお動かないところのデバッグは ChatGPT と対話しながらやった。めっちゃ便利。一人だと気がつかないような部分のコードを見てみろと ChatGPT が言ってくれて、そこにデバッグコードを入れてみるとビンゴだったりする。便利な世の中になった。

忘れないようにやったこと・気づいたことをメモっておく。

  • sinatra は v4 にあげないといけないのでパスの正規表現から ^$ は消さないといけない
  • better_errors の REPL がちゃんと動かないので Backtrace を見たいときはログを開くか better_errors を使うのをやめる
  • fork していた sinatra-cache は sinatra 4 では動かないので外した(キャッシュできない部分をどうするかは要検討)
  • capistrano-puma も Ruby 3 対応させないといけない(期待される systemd のフォーマットが変わっているので単にデプロイするだけではだめで一部手作業が必要)
  • tilt は v2.1.0 に固定( Tilt::ErubisTemplate クラスが消えるため、 padrino-helper がエラーを出す)
  • concurrent-ruby は 1.3.5 未満に固定
  • compass は 1.0.3 に固定
  • SESSION_SECRET は 64 文字以上にする
  • haml の過剰な escape を抑制
  • kaminari-sinatra の ActionViewTemplateProxy#initialize を actionview v6.1.7.8ActionView::Base#initialize に対応させる