2026年8月23日日曜日

novelmat - LLMに文章を書かせず、「次に何を書きたいか」だけ覗いてテキストフィルタを作った

☆ この blog は チャッピー生成です☆


OCR文章の不要な改行を消すために、LLMへ文章を書き直させるのをやめた。
代わりにやったのは、数十トークン読ませて、logitsを数個盗むことだった——



OCRすると「そこは改行じゃないだろ」という場所で切れる

書籍をOCRすると、こんなテキストができることがある。

吾輩は猫であ
る。
名前はまだない。

元の紙面では単に行が折り返されていただけなのに、OCR結果には改行文字が入っている。

欲しいのは当然、

吾輩は猫である。
名前はまだない。

である。

一見すると簡単な問題に見える。

ところが、「日本語のどこが文章の区切りなのか」を機械的に判断しようとすると、意外と面倒だ。

形態素解析器を使う方法も考えられる。品詞を見たり、文末表現を判定したり、句読点や括弧を考慮したりすることもできるだろう。

しかし小説には、

  • 会話文

  • 句読点の省略

  • 三点リーダ

  • 括弧

  • 倒置

  • 文の途中でのページ送り

  • OCRによる文字欠落

などが普通に出てくる。

「句点なら文末」「助詞ならまだ続く」といった規則だけでは、すぐに例外が増えていく。

もちろん、きちんと日本語解析の仕組みを作れば解決できる問題ではある。

ただし、改行を1個消すために、日本語の文境界判定器を一から育てたいわけではない。

そこで考えた。

LLMはすでに大量の日本語を学習している。

だったら、その能力をもっと雑に奪えないだろうか。


最初は、「普通に」ChatGPTやQwenに直してもらっていた

実は、最初からこんな仕組みを作ろうと思っていたわけではない。

当初の運用はもっと単純だった。

OCRしたテキストファイルをQwenやChatGPTなどのLLMへ渡して、

OCRによって入った不要な改行を取り除いてください。文章そのものは変更しないでください。

と頼んでいた。

これで済むなら、それが一番楽である。

実際、短い文章ではかなりうまくいく。

しかし、ある程度まとまったテキストを処理させると、だんだん困ったことが起きた。

たとえば、

  • 頼んでいない誤字修正をする

  • 句読点や表記を勝手に整える

  • 文体を微妙に変える

  • 消してほしい改行をいくつか残す

  • 長い文章になると途中の作業が抜ける

といったことがある。

プロンプトで、

改行以外は絶対に変更しない

と強く指定しても、完全には安定しなかった。

LLMからすれば、文章全体を一度生成し直す以上、これはある意味当然でもある。

こちらが欲しいのは、

この改行を残す

か、

この改行を消す

かだけなのに、LLMには文章全体を書き直す権限を渡している。

かなり大げさな仕事の頼み方だった。

特に困るのは、結果が一見まともに見えることだ。

数千文字の文章のうち一か所だけ書き換えられていても、人間が目視で発見するのは難しい。

逆に、不要な改行が一つ残っていても同じである。

生成系LLMへ全文編集を任せる方法は便利ではあるものの、

「入力の文字列をほぼ完全に保存しながら、指定した改行だけを機械的に除去する」

という用途では、思ったより扱いづらかった。

そこで発想を逆にした。

そもそもLLMに文章を書き直させなければいいのではないか。

文章を生成させるから、余計な変更も作業漏れも起きる。

でもLLMが、

この改行は不自然だ

と判断する能力そのものは使いたい。

だったら完成した文章を返してもらうのではなく、

LLMがその場所で次に何を書こうとしているかだけを覗けばいい。

今回の novelmat は、そこから始まった。


文章を生成させるために使う「LLM」

LLMというと、普通は文章を生成させるために使う。

質問を投げれば答えを書いてくれる。要約を頼めば要約してくれる。OCRの誤りを直したいなら、

この文章を自然な日本語に修正してください

と渡せば、それらしい文章を返してくれる。

もちろん、それでもいい。

でも今回やりたかったのは、少し違う。

LLMには文章を書かせない。

代わりに、

「この続きを書くなら、次に何を書こうとしている?」

という情報だけを取り出すことにした。


LLMは「次に何が来るか」を知っている

LLMが

吾輩は猫であ

まで読んだとする。

次のトークンを生成するとき、内部ではざっくり、

「る」       ← かなりありそう
「り」
「る。」
「\n」       ← これはかなり変
「猫」
...

のように、語彙に存在する大量のトークンすべてへスコアを付けている。

普段のLLM利用では、その確率分布から1トークンを選び、

と実際に生成させる。

今回は生成させない。

その一歩手前で止めて、確率分布だけ読む。

これだけで、

吾輩は猫であ
↓
「\n」はどのくらい出したい?
「る」はどのくらい出したい?

を数値として比較できる。

つまり、LLMを文章生成器ではなく、日本語の違和感センサーとして使う。


形態素解析器を作り込む代わりに、LLMの出力を横取りする

何故こんな事を考えるのか?

日本語の文境界をちゃんと認識する仕組みを作ろうとすれば、それなりの実装になる。

形態素解析して、

助詞だから続きそう
終助詞だから切れそう
句点だから切れそう
でも括弧の中なので……

と考えることになる。

さらにOCR特有の壊れ方まで扱おうとすると、ルールは増えていく。

一方LLMは、そもそも、

吾輩は猫であ

を見せた時点で、

次はたぶん「る」だろう

という判断をしている。

ならば、その判断をそのまま盗めばいい。

日本語の構造をこちらで明示的に解析する必要はない。

必要だったのは、

  1. 問題の位置までLLMに読ませる

  2. logitsを取る

  3. 改行と次トークンのスコアを見る

だけだった。

専用モデルを学習したわけでもない。

形態素解析の辞書を調整したわけでもない。

大量の日本語ルールを書いたわけでもない。

それでも、それなりにうまくいく。

LLMの出力を奪うのは、手間のわりにかなり性能がいい。


改行あり vs 改行なしをLLMに競わせる

文章が

A\nB

となっているとする。

まず、改行しない場合を考える。

A → B

について、LLMがBをどの程度自然だと思っているかを見る。

一方で、改行を残す場合は、

A → \n → B

という系列を見る。

log probabilityを使えば、これらを比較できる。

たとえば、

吾輩は猫であ
る。

なら、「吾輩は猫であ」の次に「る」が来る確率は高い。

一方、その途中に突然改行を挟む確率は低い。

だったら、

この改行はOCRによって紛れ込んだ可能性が高そうだ

と判断できる。

逆に、

吾輩は猫である。
名前はまだない。

なら句点後の改行はそれほど不自然ではない。

重要なのは、こちらが、

「ある。」は文末です

というルールを教えていないことだ。

LLMがすでに持っている日本語予測能力を利用しているだけである。


「日本語を理解させて答えを書かせた」わけではない

ありがちな実装なら、

あなたは日本語校正者です。
以下のOCR結果から不要な改行を削除してください。

とLLMに頼み、

吾輩は猫である。
名前はまだない。

という完成テキストを生成させる。

しかし、それをするとLLMにはかなり大きな自由が与えられる。

誤字を直すかもしれない。

句読点を変えるかもしれない。

表記を整えるかもしれない。

原文そのものを書き換えるかもしれない。

今回ほしいのはそんな高度な校正ではない。

この1文字の \n を消すか、残すか。

それだけだ。

そこでLLMには編集権限を与えない。

聞くのは、

この位置で \n をどのくらい出したかった?
その代わり次の文字をどのくらい出したかった?

という内部情報だけ。

最終的にファイルを書き換えるのは普通のPythonコードである。

AIに仕事を丸投げするというより、

日本語能力だけ借りる。



「生成させない」は速度面でも重要だった

この方法には、精度や制御のしやすさ以外にもう一つ大きな利点があった。

ほとんどdecodeしなくていい。

普通にChatGPTやQwenへ、

この文章から不要な改行を取り除いて、修正後の全文を出してください

と頼む場合、LLMは入力をprefillしたあと、修正済みの文章を最初から最後までdecodeして生成する。

たとえば数千トークンの文章を処理するなら、入力側の数千トークンを読むだけでなく、出力側でもまた数千トークンを1トークンずつ生成することになる。

LLM推論では、このdecode部分がかなり重い。

生成は基本的に、

1 token生成
↓
KV cacheを使って次の1 token生成
↓
また次の1 token生成
↓
...

と逐次的に進む。

文章が長ければ、そのぶん何千回もforwardする必要がある。

一方、今回欲しいのは修正済みの文章そのものではない。

ある位置まで、

吾輩は猫であ

とLLMに読ませて、

次は「る」なのか?
それとも改行なのか?

というlogitsを一度見るだけでよい。

つまり処理の大部分をprefillとしてまとめて流せる

prefillは複数トークンをまとめて行列演算できるので、decodeのように1トークンずつ逐次生成するより圧倒的にGPUを使いやすい。

今回の処理は感覚的には、

普通のLLM校正

長い入力をprefill
        ↓
長い修正版をdecode
decode
decode
decode
decode
...

ではなく、

novelmat

周辺テキストをprefill
        ↓
logitsを数個読む
        ↓
終わり

に近い。

LLMを使っているのに、ほとんど文章を生成しない。

そのため、普通にLLMへ全文を校正させる方法と比べるとかなり速い。

これは今回の用途と相性がよかった。

欲しいのは文章そのものではなく、

この場所では何を書こうとしていたか

という一瞬の判断だけだからである。


「これから校正する」と思わせてから確率を取る

先にLLMへ、

あなたの役割は日本語ライトノベルの校正担当です。
OCRで生成された日本語文を修正してください。

#で始まる行はページ情報なので残してください。
ページをまたいだ文は前ページ側へ統合してください。

といった指示やfew-shot例を見せておく。

その後、

Assistant:
吾輩は猫であ

までをprefillする。

しかし、やはり生成はさせない。

「これから校正結果を書くつもりになったLLM」が次に何を書きたがっているかだけを見る。

この使い方では、プロンプトは回答を生成させるためだけのものではなく、

次トークンの確率分布そのものを目的に合わせて変える手段

になる。指示のほか、参考にする画像をprefillするなどしても面白そうだ。


llm-jp-4への単純なモデル差し替えはうまくいかなかった

日本語を扱うなら、日本語特化モデルのほうがうまくいくのではないか。

そこで llm-jp-4-8b-thinking も試した。

ところが、Gemmaで使っていた判定基準をそのまま持っていくと、期待した結果にはならなかった。

たとえば、

今日はいい天気ですね。
明日も晴れるといいな。

のような通常の文間でも、Gemma用に決めた基準では改行を削除する側へ寄りやすかった。

最初は、

日本語特化モデルなのに意外だな

と思った。

しかし、考えてみれば判定ロジックそのものをGemmaのlogit分布を観察しながら作っている。

モデルが変われば、

P(\n)
P(next token)

の絶対値も、差の出方も、分布の形も変わって当然である。

つまりこれは、

llm-jp-4がこの用途に向いていない、という結果ではない。

むしろ、

モデルごとに判定基準をチューニングする必要がありそう

という話だと思う。

Gemma向けに作った閾値をllm-jpへそのまま持っていって、

Gemmaのほうが優秀だった

と結論するのは早い。

今後モデルを差し替えるなら、少量の教師データから閾値を自動的に合わせたり、logit差を正規化したりする仕組みがあってもよさそうだ。


26万語を毎回ソートして41秒かかっていた

最初は llama.cpp / llama-cpp-python の通常のlogprobs取得APIを使っていた。

ところが、やたら遅い。

調べてみると、必要なのは、

P(\n)
P(next_token)

という数個の値だけなのに、API側では巨大な語彙全体をソートしていた。

今回使ったモデルの語彙は約26万トークン。

それを何度もsortする。

3改行を見るだけで、

約41秒

かかっていた。

さすがにテキストフィルタとは呼びづらい。

そこで通常のcompletion APIを迂回し、

model.eval(tokens)
logits = model._scores

からraw logitsを直接取得するようにした。

あとはnumpyでlog-softmaxして、

p_newline = logprobs[newline_token_id]
p_next = logprobs[next_token_id]

だけ取り出せばいい。

必要なのは2要素程度なのだから、26万要素を順位順に並べる必要はない。

その結果、

3改行: 約41秒 → 約1秒

まで縮んだ。

全文を毎回読ませるのも無駄なので、現在は判定位置の前後約50トークンだけを見る。

最終的には、

数十トークン読ませて、logitsを数個盗む

という非常に小さな処理になった。


最後はsedみたいになった

こうしてできたのが novelmat である。

基本は、

novelmat input.txt

標準入力も使える。

cat input.txt | novelmat

ファイルを直接書き換えることもできる。

novelmat -i input.txt
novelmat -i.bak input.txt

判定結果だけJSONで確認することもできる。

novelmat --detect input.txt -o result.json

LLMアプリなのに、UIもチャット画面もない sed のような使い心地。



LLMは「文章生成API」だけではない

LLMを普通に使っていると、

prompt → generated text

というインターフェイスだけを見がちである。

しかし実際には、その途中に、

context
    ↓
巨大なtoken probability distribution
    ↓
sampling
    ↓
next token

がある。

我々が普段受け取っている文章は、その巨大な情報から最後に1個だけ選ばれた結果だ。

その手前には、

次に改行したい度
次に「る」と書きたい度
句点を書きたい度
EOSを出したい度

が全部入っている。

だったら、生成させずにそこを使ってもいい。

今回の改行フィルタは、その小さな例でしかない。

同じ考え方なら、たとえば、

  • OCR結果がページ末で途中切れしているか

  • 2つのテキスト断片が連続しているか

  • ここに句読点があるべきか

  • 改行すべき位置はどこか

  • 語尾が欠落していないか

  • 文書の境界らしいか

といった問題も、次トークン確率から拾える可能性がある。

こうした問題をまじめに解こうとすれば、形態素解析、構文解析、ルール、専用classifierなど、いろいろな方法がある。

それらが不要という話ではない。

精度や説明可能性が重要なら、専用手法のほうが適している場合も多いだろう。

ただ、

ちょっと日本語らしさを判定したい

という用途では、すでに手元にあるLLMへ文章を食わせ、次に何を出したがっているかを横取りするだけでも、意外なほど使える。

専用の日本語処理系を組み立てるより実装の手間が少なく、しかもモデルがすでに持っている大量の言語知識をそのまま利用できる。

日本語は英語などのように区切りが明確ではなく、形態素解析などしても正確な情報が得られるわけではない前提において、これはかなり便利な性質だと思う。


まとめ -「AIに直してもらう」より「AIが何を書こうとしたかを見る」

今回の novelmat は小さなツールである。

やっていることも最終的には、

この改行を残す / 消す

という二値判定にすぎない。

ただ、実装していてLLMとの付き合い方が途中から変わった。

最初は、

LLMならこの日本語を直せるのでは?

だった。

それが、

LLMはこの位置をどう感じているんだろう?

になり、

最終的には、

答えは書かなくていいから、その瞬間のlogitsだけ見せて

になった。

日本語の文章境界を見つける仕組みをこちらで頑張って作る代わりに、

LLMがすでに持っている「続きを予測する能力」を横から奪う。

少々乱暴だが、手間の割にはよく働く。

LLMは文章生成器として非常に便利だ。

でも、生成される文章はLLMが持っている情報のごく一部でもある。

その手前にある確率分布を覗いてみると、

昔ながらの小さなUNIXフィルタの中に、LLMをひっそり埋め込む

みたいな使い方もできる。

novelmat は、そんな実験になった。

2026年6月2日火曜日

Bambo Labs P1S のノズルつまり解消 (フィラメント供給口が詰まったホットエンドの復旧)

 ABS 素材を使用した後にフィラメントが完全に詰まってしまい、放置していた3Dプリンタを復旧しました。


FDMプリンタのしくみ

すごい雑な図面しか作らないけども、まず何故これからの作業でノズル詰まりが解消するのかを考察してみます。正常状態では押し出しギアで押し出されたフィラメントがホットエンドに送りこまれ、加熱されたノズル内で溶けて排出されます。


強制的な押し出しとコールドプル

この状態フィラメントを強制的に押し出すには以下の作業をします。

  • ノズル温度を 250~270度ぐらい(詰まっているノズルでフィラメントが溶ける温度)にする
  • その状態で、フィラメントを押し出しギアから押し込む
  • ノズルから古いフィラメントが抜ける
また、これでノズルからフィラメント上に引き抜くのがコールドプルです。
  • ノズル温度を 250~270℃ぐらいにする
  • フィラメントに押し出してノズルを満たす
  • ノズル温度を常温に戻して、フィラメントを個体化させる
  • ノズル温度を80℃ぐらいに上げる
  • フィラメントを上に引き抜く
    • 押し出しギアを逆回転させる方法
      • 利点:これで済めば楽
      • 欠点:引き抜いたフィラメントが押しだしギアとホットエンドの間に残る可能性がある
    • ホットエンドをと外した状態で手でフィラメントを引き抜く方法
      • 利点:わかりやすい
      • 欠点:目的温度までホットエンドを温める方法が必要(プリンタ操作)、ホットエンドから手動で引き抜きにはホットエンドを外す必要がある(プリンタから外さないといけない)
しかし今回はこのパターンでは解消しないタイプの詰まりでした。。。


ホットエンド入り口が詰まった状態のノズル → 加熱した金属棒でフィラメントを加熱

ホットエンドを加熱しても、ホットエンド入り口にあるフィラメントの塊が過熱されず、フィラメントを押し出しできない状態でした。
今回はこの状態だったので、加熱した六角レンチでホットエンドのフィラメント供給口を押してあげることで解決しました。


  • 六角レンチを温める。
    • 手許ではガスコンロの火を使った
    • 六角レンチが赤くはならないが、熱でめっきが変色するぐらいまで加熱した
    • 手でレンチを持っていても問題ない程度だった
  • ホットエンドのフィラメント供給口に六角レンチを押し当てる。
    • これによりフィラメントが溶ける。フィラメントを通じてノズルが熱くなるので注意
  • フィラメント供給口をふさいでいたフィラメントの塊をノズルに押し込めた
    • レンチなどにフィラメントを絡めて上に引き抜く「コールドプル」はできていない
この状態で、ノズル温度270℃で温めてからフィラメントを押し出したところ、フィラメントが問題なく吐出されるようになりました。ノズルからフィラメントは出るようになりましたが、この流れでフィラメント供給口から溶けたフィラメントを引き抜きコールドプル(先述)も行い、ノズル内の汚れを取り除きます。これで今回のメンテナンスは完了です。


詰まりのきっかけになった ABS 樹脂でもプリントに成功。大丈夫そう。


これでダメだった時になにをするか。。。

これでダメなら.... 押し出し機の掃除かな。ペットボトルを溶かして遊んでいたときに、押し出し機とノズルの間にPETカスが残ってしまい、エクストルーダを分解したことはあります。今回はそこまでする必要はありませんでした。



おわりに

0.4mm、 0.2mm のノズルが同じフィラメントにより、この問題で詰まっていました。
プリンタメーカー純正フィラメントをパラメータ設定して造形するのは楽しいのだけど、しかし、同じフィラメントを使ったらまた詰まるんじゃないかと思うと、面倒だなぁという気持ちに……。とはいえ、今回、手許で具体的な対処法を見いだせたので、今後同じことがあってもなんとかなりそうです。





2026年5月19日火曜日

古い Proxmove VE 環境からVMをリカバリする

 古くなったSATA SSDが時々リブートで見えなくなったり、ついには稼働中にI/Oエラーを起こしたりするようになったので交換することにしました。


新しいSSDにProxmox VEをクリーンインストールした後、ダメモトで旧ドライブをUSBマスストレージコントローラ経由で接続したところ、認識成功。見えているうちにデータを復元することにします。

仮想マシンの定義ファイルは /etc/pve/ 以下にあるのだが

Proxmox VE の仮想マシン定義ファイルは /etc/pve/nodes/{hostname}/qemu-server/ 以下にテキストの定義ファイルとして置かれています。これをバックアップするため以下のコマンドを実行しました。

cd /mnt/oldroot
tar cvzf ~/old.etc.pve.tar.gz ./etc/pve
cd ~
tar xvzf old.etc.pve.tar.gz

そうするとあら不思議... 空のディレクトリが。/etc/pve は普通のディレクトリではなく、pmxcfs による分散設定ファイルシステムになっています。実体は /var/lib/pve-cluster/config.db に保存されていました。

ドライブが読めなくなる前にDBファイルを保存します。

cd /mnt/oldroot
tar cvzf ~/old.var.lib.pve-cluster.tar.gz ./var/lib/pve-cluster
cd ~
tar xvzf old.var.lib.pve-cluster.tar.gz

sqlite3で覗いてみます。

% sqlite3 config.db
SQLite version 3.51.0 2025-06-12 13:14:41
Enter ".help" for usage hints.
sqlite> select * from tree;
0|0|46624809|0|1777877202|8|__version__|

6|0|7|0|1682337809|8|datacenter.cfg|keyboard: en-us

8|0|8|0|1682337874|4|virtual-guest|
9|0|9|0|1682337875|4|priv|
11|0|11|0|1682337875|4|nodes|
12|11|12|0|1682337875|4|pve|
13|12|13|0|1682337875|4|lxc|
14|12|14|0|1682337875|4|qemu-server|
15|12|15|0|1682337875|4|openvz|
16|12|16|0|1682337875|4|priv|
17|9|17|0|1682337875|4|lock|
24|0|25|0|1682337875|8|pve-www.key|-----BEGIN RSA PRIVATE KEY-----
...

なるほど、6つ目のフィールドがファイル名で、7つ目のフィールドにファイルコンテンツがあるだけの模様。ただ手作業でファイルをひとつひとつリカバリするのも面倒です。


opencode + Local LLM で DB 構造を解析させリカバリプログラムを書かせる

もう、やることが決まってしまった作業は退屈なだけです。

これぐらいのタスクなら軽量の Local LLM でも十分こなせます。外部のAPIサービスに投げない理由は、DBのなかにシークレットなどの情報が混ざっているかもしれないので、手許で推論します。

Mac に先の sqlite3 データベースをコピー、また適当に python venv を用意して、opencode を起動。プロンプトしました。推論にはローカル LM Studio で動く Qwen 3.6 35B-A3B を使用しました。

you will find config.db file which is in sqlite3 format. inspect its "tree" table and build a python script that recover files in the database. use venv/bin/python


呼吸していたら、動く recover.py が出てきました。(大したものでもないですが gist においておきます

このスクリプトを実行したら確かに DB 内のファイルが recovered/ サブディレクトリに階層構造つきで書き出されました。

% find recovered 
recovered
recovered/mapping
recovered/mapping/directory.cfg
recovered/authkey.pub
recovered/pve-www.key
recovered/sdn
recovered/nodes
recovered/nodes/pve
recovered/nodes/pve/pve-ssl.pem
recovered/nodes/pve/ssh_known_hosts
recovered/nodes/pve/openvz
recovered/nodes/pve/pve-ssl.key
recovered/nodes/pve/lxc
recovered/nodes/pve/qemu-server
recovered/nodes/pve/qemu-server/237.conf
recovered/nodes/pve/qemu-server/104.conf
recovered/nodes/pve/qemu-server/105.conf
recovered/nodes/pve/qemu-server/200.conf
recovered/nodes/pve/qemu-server/102.conf
recovered/nodes/pve/qemu-server/231.conf
recovered/nodes/pve/qemu-server/103.conf
recovered/nodes/pve/qemu-server/100.conf
recovered/nodes/pve/qemu-server/249.conf
recovered/nodes/pve/qemu-server/232.conf
recovered/nodes/pve/qemu-server/245.conf
recovered/nodes/pve/qemu-server/101.conf
recovered/nodes/pve/qemu-server/235.conf
...


VM定義ファイルを新環境に展開

経験上、これで qemu-server/XXX.conf ファイルを書き込んであげればVMが出てくるのを知っています。

% cd recovered/nodes/pve/qemu-server/
% scp * "root@{PVE}:/etc/pve/nodes/pve/qemu-server/"
root@{PVE}'s password: 100.conf 100% 537 113.0KB/s 00:00 101.conf 100% 421 110.6KB/s 00:00 102.conf 100% 575 129.4KB/s 00:00 103.conf 100% 417 84.1KB/s 00:00 104.conf 100% 462 102.1KB/s 00:00 105.conf 100% 462 99.0KB/s 00:00 106.conf 100% 485 103.6KB/s 00:00 200.conf 100% 253 54.6KB/s 00:00 ... 

qemu-server/XXX.conf が復元できれば、Proxmox VE は VM を再認識します。

Proxmox VE のコンソールを確認したところ、仮想マシンがホスト上に表示されるようになりました。


NVMeドライブ上のLVMプールを新環境に追加

仮想マシンのディスク本体は起動ドライブとは別の NVMe ドライブにあるので無事です。ただし、VM定義だけではディスク実体にアクセスできないため、NVMe上のLVM Volume Groupを新しい PVE 環境へ再登録します。

root@pve:~# pvesm add lvm NVMe1_ZP4000GM30023_SERIAL123 --vgname NVMe1_ZP4000GM30023_SERIAL123 \ --content images


ネットワーク設定も単純だったこともあり、過去のドライブ上のISOメディアやUSBデバイスの接続など、イレギュラーな設定が入っているものをのぞけば、これでVMが起動できるところまで復元できました。


古いドライブからVMを救出する場合

古いドライブの LVM に VM がある場合は、VMイメージもリカバリしたくなるかもしれません。Proxmox VE は qm コマンドで VM をオンラインでマイグレーションさせられるので、以下の手順でできそうです。
  • 古いドライブを pvesm add lvm コマンドにて新しい名前で登録
    (デフォルトの LVM 名がコンフリクトする)
  • sed などで VM定義のなかのディスクイメージのプール名を編集
  • qm move_disk で新しいドライブへマイグレーションさせる

今回はこのニーズがないため、この記事では扱いません。


まとめ

Proxmox VE の仮想マシンデータのリカバリを、面倒くさい部分は生成AIで自動化して済ませました。仮想ディスク本体が別ストレージに残っている場合、VM定義ファイルを DB から抜き出してしまえば優勝です。

手間のかかる部分をスクリプト生成・実行で済ませたので何も困ることはありませんでしたが、復元作業よりも、このエントリを書くことのほうに時間がかかりました。

次の投稿ネタまでに、作業記録の記事化をどう自動化するかを考えたほうがいいのかもしれません。

2026年5月1日金曜日

Proxmox VEホストのZFSデータセットにTimeMachineバックアップ

MacBook Proを新しく購入したので、Proxmox VE上のZFSストレージをバックアップ用途として見直し、TimeMachineの保存先として利用することにしました。

■ バックアップの圧縮と暗号化のポリシー: ZFSの機能を利用する

万が一どころか億が一、ハードディスクの持ち去り対策としてデータの暗号化を検討します。まぁ個人の Mac で、プール全体を暗号化していないので片手落ちなのですが...

データの圧縮や暗号化をどのレイヤーで行うか考え、最終的にどちらも ZFS のレイヤで行うことにしました。

結論としては、TimeMachine自体は暗号化することはあっても積極的には圧縮しないようで、ZFSのレイヤで圧縮をかけるとしても、TimeMachineの暗号化を有効にすると、ZFS側での圧縮はほぼ効かなくなります。圧縮したければTimeMachineの暗号化機能は利用しないほうがよい、ということになります。


■ Proxmox VE側の設定

まずは zpool、データセットの準備を行います。

array5 zpool

今回は既存の zpool プールを使用しますので、 zpool の作成方法などについては割愛します。 10TB HDD x5, RAIDZ2 で構成したプールです。

hasegaw@pve:~$ zpool list
NAME     SIZE  ALLOC   FREE  CKPOINT  EXPANDSZ   FRAG    CAP  DEDUP    HEALTH  ALTROOT
array5  45.5T  11.2T  34.3T        -         -     0%    24%  1.00x    ONLINE  -

array5/backup
array5/backup/timemachine

ここにはarray5/backup, array5/backup/timemachine データセットを作成します。まぁ、データセットも作成済みなのですが、

hasegaw@pve:~$ zfs list array5/backup
NAME            USED  AVAIL  REFER  MOUNTPOINT
array5/backup 1.31T 20.2T 211G /export/backup
array5/backup/timemachine 959G 20.2T 185K /export/backup/timemachine

新たに作成するのであれば以下のようなコマンドラインになりますね。

sudo zfs create array5/backup
sudo zfs create -o compression=lz4 array5/backup/timemachine

compression=lz4 を指定していますが、とても積極的な圧縮とまでいかなくても、ゼロが続くような領域についてはランレングス圧縮がかかれば良いな、というぐらいの期待値です。ここで gzip などを指定すれば多少圧縮率もあがるかもしれませんが、CPUサイクルとのバランス感は微妙だと思っています。

また、このホストではデータセットのマウント先を /export/backup にしています。(-o mountpoint=/export/backup)


array5/backup/Arseille

続いて TimeMachine 用の array5/backup/Arseille を作成します。 TimeMachine はマウントされたファイルシステムをまるごとバックアップ先として使用する仕様なので、TimeMachineの単位でSMB共有を作成することになります。

折角なので、ZFSのレイヤでも、ZFSデータセットとしても分けておきます。スナップショット単位の操作(zfs send/recv や destroy)を考えると、分けない理由があまり思いつきません。

  • MacBook Pro の内蔵SSDが 4TB なので、 TimeMachine バックアップと、ストレージが溢れた状態でもある程度の世代が残るように、 5TB でクオータを設定します。
  • 暗号化用の鍵をプロンプトにて指定します。鍵は別途管理します。

root@pve:~# zfs create -o quota=5T -o encryption=aes-256-gcm -o keyformat=passphrase \
-o keylocation=prompt array5/backup/timemachine/Arseille Enter new passphrase: Re-enter new passphrase:

TimeMachineで使用するユーザで、データセットのディレクトリに書き込めるようにしておきます。

# chown hasegaw:hasegaw /export/backup/timemachine/Arseille
hasegaw% touch /export/backup/timemachine/Arseille/hoge
hasegaw% unlink /export/backup/timemachine/Arseille/hoge

Samba のインストール

# apt upgrade
# apt install samba

smbpasswd

まだ Samba 用のパスワードを設定していなければ smbpasswd で設定しておきます。

今回はパスワード登録済みですが、新たに登録する場合は以下のイメージになります。

# smbpasswd -a hasegaw
※ -a は新規ユーザ登録。事前の /etc/passwd にそのユーザを作っておくこと

この SMB サーバに対しては私自身が普段使いのユーザアカウントで SMB マウントするため、今回はメインのユーザアカウントで TimeMachine 接続を想定します。

/etc/samba/smb.conf

もらってきた設定を貼り付けて少し弄っただけですが、以下の要領になります。

  • fruit:time machine=yes
  • fruit:time machine max size = 5000G: 設定は可能なのですが ZFSの quota で最大容量は伝わっているかなと思うので、いったん様子見で外しています。どのように振る舞うのかが判っていませんが、 ZFS 側の制限がハードクオータ、こちらの設定が TimeMachine に対する容量のヒントとして振る舞うように見えるので、 ZFS データセットよりもすこし小さい値に設定してほうがよいかもしれません。
  • ea support = yes となっていますが実際には ZFS レベルで EA を有効化していません。現状、手許では問題なく動いているように見えていますが、将来的に見直すかもしれません。
[timemachine_Arseille]
path = /export/backup/timemachine/Arseille
browseable = yes
read only = no
guest ok = no
writable = yes
valid users = hasegaw
fruit:time machine = yes
; fruit:time machine max size = 5000G
ea support = yes

Samba プロセスを再起動しておきます。

root# systemctl restart smbd


■ Mac 側の設定

MacBook Proからのマウント確認

Finderから Cmd-K 、接続先に smb://hostname_or_ip_address/ を入力し接続します。

ユーザ名、パスワードを聞かれたら PVE ホスト側のユーザ名および先に smbpasswd で指定したパスワードを入力します。

TimeMachine バックアップ先となる timemachine_Arseille 共有が見えたら、それをマウントします。中にフォルダを作成できることを確認し、書き込みが正しくできることを確認できたらテストで作成したフォルダを削除し、共有を改めて空の状態にしておきます。

Finder の左側ペインから、該当する共有のマウントを解除します。

TimeMachine 設定

バックアップ対象の MacBook Pro 側で、以下の要領で TimeMachine 先を設定します。

root# tmutil setdestination smb://user:passwd@hostname/timemachine_Arseille
root# tmutil startbackup

設定で TimeMachine を検索し、バックアップが開始されたことを確認します。



バックアップ終了後に ZFS データセットの割り当て量が増えていることが確認できます。

root@pve:~# zfs list array5/backup/timemachine/Arsielle
NAME                                 USED  AVAIL  REFER  MOUNTPOINT
array5/backup/timemachine/Arsielle  9.17G  4.99T  9.17G  /export/backup/timemachine/Arsielle


■ 再起動後の暗号化解除

Proxmox VEで動作するZFSのプールのうち、該当する timemachine 領域をマウントするには鍵を提供する必要があります。このためホストの再起動後はプールが見えなくなり、バックアップや TimeMachine の内容確認などの操作ができなくなります。

 zfs load-key コマンドにてプロンプトから鍵を入力しアンロックします。

root# zfs load-key array5/backup/timemachine/Arseille
root# systemctl restart smbd

プールが見えない状態では TimeMachine が sparse bundle を見つけられないため、バックアップは(安全に)失敗します。次回以降のバックアップで再マウントいs sparse bundle が見つかれば、バックアップは再開します。


■ ToDo: Mac持ち出し時の他ネットワークでのバックアップ防止

Mac の TimeMachine 機能はスケジュール通りにバックアップを繰り返すため、自宅から Mac を持ち出している間も、 tmutil setdestination で指定した hostname に対して user:passwd でログインしようとする挙動がつづきます。

セキュリティ的には気持ちが悪い挙動なので、ネットワーク条件に応じてTimeMachineを有効/無効に切り替える仕組みを今後検討したいと思います。

クラスCアドレスなので仮にインターネットに繋がっていても経路がなくSMB共有は繋がらないはずですが、接続先のWiFiが(以下自主規制)

かといって、この状況で IPsec などを使うのもどうかと思うし :S 自宅LANに接続していなければ自動バックアップ無効までやりたいと思いますが... これは後日としたいと思います。

2026年4月29日水曜日

Proxmox VEを7.xから9.xへアップデート

 インストール後に割と放置していた Proxmox をアップデートしました。


作業ログをとらずにまとめてやってしまったので具体的なコマンドラインや出力は記載しませんが、おおよその手順としては、下記の手順になります。

  • Proxmove VE 7.x を最新バージョンまでアップデート
    • apt update
    • apt dist-upgrade
  • PVE 7 -> 8 のアップデート検証、アップデート
    • pve7to8 コマンドを実行しアップデート事前検証
      • すでに zfs destroy したプールが見つからない警告が出たので、ここで削除しておく
      • DKMS でインストール済みだった iomemory-vsl について警告が出ていたので、 DKMS を外しておく
      • 推奨に従い VM を停止
    • sed -i 's/bullseye/bookworm/g' /etc/apt/sources.list
    • apt update
    • apt dist-upgrade
    • 新しい kernel もあるため一度再起動、 VMが起動することまで確認しておく
  • PVE 8->9 のアップデート検証
    • pve8to9 コマンドを実行しアップデート事前検証
      • ブートディスク容量不足の警告が出たため ISO イメージを整理
      • 推奨に従い VM を停止
    • sed -i 's/bookworm/trixie/g' /etc/apt/sources.list
    • apt update
    • apt dist-upgrade
    • 再起動、 VMが起動することまで確認

その他考慮したポイントなど
  •  PVE 9.x へのアップデートにあたり多少 grub.cfg が変化しているようで。iommu 関連で GRUB の設定を書き換えていましたが、メンテナ版を受け入れ、個別の修正は改めて適用。
  • smb.conf は現行バージョンを採用。
  • AMD 向けマイクロコードのパッケージなどが表示された案内通りにはインストールできていないが、とりあえずそのままで。
  • GRUB の際インストールを促されたが、案内通りにはうまく進まなかったので、とりあえずそのままで。 boot してるから、まあいいでしょう
  • 当初はクリーンインストールになるのかなと思いながら、手順どおり /etc 以下や /var/lib/ の下などをバックアップしてまわろうと思い作業してましたが、 apt dist-upgrade の手順が出てきた段階で「まあなんとかなるだろう」と思い始めて、そのまま上書きしてしまいました。 Proxmox VE のブートドライブ (SATA 128GB), メインの仮想マシンプール(NVMe 4TB), ストレージアレイ(10TB x5) が別れていたので、失敗してもなんとかなるだろうと思ったのもあります。痛い目に遭いたくなければ、バックアップぐらいは取っておきましょう 

参考にしたURL

  • https://pve.proxmox.com/wiki/Upgrade_from_7_to_8
  • https://pve.proxmox.com/wiki/Upgrade_from_8_to_9


2026年2月14日土曜日

3Dプリンタを併用してSHURE SRH1540用ロングケーブルを製作

SHURE SRH1540

2000年ごろ、大学生のときにATH-A700ヘッドホンを購入してから20年近く使っていたのですが、最終的に樹脂部品が経年劣化で分解してしまい、処分しました。それ以降は解放型の SENHEISER GAME ONE を自宅で使っていたものの、モニタリング用途でどうしても密閉型のヘッドホンが欲しくなり、 SRH1540 を購入。


新品で購入しなくてもよいかなと思い、最終的に3万円台で中古を購入しました。それまでに使っていたヘッドホン類と比べると(新品販売時の価格帯も倍以上と違い)かなり解像度が高く感じ、聞こえ方が別物に感じます。

常用してわかってきた問題点


問題なく聴けているときの利用感は非常に好みです。しかし、常用していると、純正の「左右両だしケーブル」にかなり不便を感じました。

  • ケーブルが 1.5m 程度?で短め
    • 装着状態では身動きをとりづらい
    • ケーブル長から、利用場所の制限を感じる
  • 左右両だし仕様
    • ギターを抱えたり離したりしづらい
    • どちらが右・左か、わからなくなる
  • 3.5ピンコネクタ相性
    • 一般的な3.5ピンコネクタよりも細めなのか、接触不良になりやすい模様。検索すると悩んでいる人がけっこう出てくる

ケーブルの作製を決断、しかしコネクタが手に入らない


先述のトラブル内容でイライラしてしまうので、別のモニタリングヘッドホン購入を検討。しかし定価6万円台のヘッドホンを基準にしてしまうと、次の選択肢もなかなか決まりません。
「最終的に、これ以上ヘッドホン買うよりは SRH1540をなんとか使える状態にしたほうがよいだろう」と思い直し。ケーブルを作製することにしました。

作製にあたり困ったのはコネクタ部分。接点部分は一般的な MMCX ですが、イヤホンのリケーブルなどで使用される MMCX コネクタより細身の独自仕様になっていて、これにあった部品がありません。秋葉原で相談すると「そんなもん売ってねー」と冷たくあしらわれる状況。


既存 MMCX コネクタ + 3Dプリントの自作ハウジング製作にチャレンジ


最終的に NOBUNAGA Labs MMCXプラグ自作キット と、3Dプリントの自作ハウジングで製作にチャレンジすることにしました。







こちらの MMCX コネクタはネジ山が彫ってあるため、ハウジングの内径次第で、よい感じに閉めることができます(オヤイデで取り扱いのあるMMCXコネクタも購入しましたが、こちらにはネジ山がありません)。 Bambu Labs のPLAフィラメントで輪状に出力したハウジングに対して、そのまま締めることができました。

ただ、非常に小さいモノなため、このハウジングは3Dプリントするには 0.4mm ノズルでは難しい印象です。普段はあまり使わない 0.2mm ノズルを利用しました。 0.2mm ノズルで壁ひとつ分の壁厚しかとれないと思います(2壁はチャレンジしていませんが)。変に力をかけると層がはがれて壊れてしまうかもしれませんが。壊れたら、コネクタのはんだ付けからやり直しです(コネクタ部分の再利用はむつかしいと思いますので、修理のタイミングで新しい部品が必要な気がします)。このサイズ感のはんだ付けは得意ではないので、断線・修理が怖いです。

余談ですが、この製作の前に Kaika の P1S 利用可能なノズルを注文していたのですが、他のユーザからのトラブル報告などを経て最終的に入手しませんでした。 0.1mm ノズルが手元にあったら、絶対にプロジェクトでは 使ってみたかったです。

そして、ついに SRH1540 に装着可能な MMCX のハウジングを製作できました。多少不格好ですが、自分で使うには問題ないでしょう。



チャンネルの見分けがつくように片方をマゼンダにしました(CMYKセットとしてもっていたものを利用)


3mの片出しケーブルを作製


SRH1540に対応できるMMCXハウジングを作れてしまえば、もう後は単純なはんだ付け作業です。

今回は 3.5ピン側も NOBUNAGA Labs のものを使いました。正直私には200円未満のプラグとの利用感の違いはわかりませんが……。ピンジャックとSHURE純正プラグの相性問題とも、これでサヨナラです。

ケーブルは、オヤイデの、イヤホン用2芯2ペアのものを使用。作製したのは半年以上前ですが、これがすごく高かった覚えがあります。普通なら3mとかで使うようなものでもない。。。

右チャンネル分は短く、左チャンネル分はヘッドレストを経てコネクタに接続できるように作ったことで、事実上片出し運用ができるようになりました。

作製したケーブルはデスクトップ利用もできますし、ソファでギターを弾いているときにオーディオインターフェイスで音をモニタリングするのにも、不自由なく使えています。


3Dプリンタがあるからこそできた


このケーブルは 2025/7 に作製し、手元の利用ではハウジングが破損することもなく半年以上問題なく利用できています。

この製作のキモは、先述のとおり、特別なMMCXコネクタ用ハウジングの製作でした。もし3Dプリンタが手元になければ、最初から諦めていたと思います。私は手先が器用な方ではないですが、それでも「3Dプリンタで、解決できる課題が広がっている」と感じたプロジェクトでした。

Bambu P1Sアップグレード記録: AMS+Tetras乾燥機, AMSドロア, AMSロングケーブル

 Bambu Labs の P1S を購入してから一年以上がたちました。収益化している人ほどガンガン使ってはいないのですが、けっこう楽しく、便利に利用しています。今回は AMS 関連のアップグレード内容をまとめてみます。




AMS 2段 + EIBOS Series X: Teras 乾燥機

手元ではAMSを2台、P1Sのとなりにおいて運用してきました。合計8スプールを利用可能状態にしておけることで、便利に使っています。

さらに、この半年で、クラウドファンディングで buck していた EIBOS Series X: Tetras が到着。AMSに乾燥機能を追加するシステムです。これを注文したときは AMS2 はまだ出ていませんでしたので、これはAMS無印2台持ちの私にとっては現実的なアップグレードでした。

フィラメントドライヤーとしての Tetras の感想などはあまり書くつもりありませんが、「使用予定をフィラメントをセットして、ボタンを押してドライヤーにかけておくだけ」はとても便利です。PLAは上段メイン、ABSやPETGなどを使うときや、PLAでも乾燥にかけたいときなどは下段のAMS+Tetrasからフィラメント供給しています。

AMSが販売終了になっていることなどを考えると、これから Tetras を入手する人は限られるでしょうし、そもそも入手できる期間も相当限られそうですが、購入してよかったなと思います。


AMSドロア

Tetras 導入前、ダイソーのメタルラック用ポール等で製作したドロアを使用していました。3Dプリンタ購入直後で、モデリングも能力的にもぎりぎり、インサートなどを使うことも考えずに作ったものですが、いくらか問題があったものの便利に運用していました。




第一世代で発生していた問題は以下の部分です。

  • ダイソーのつっぱりミニポール用の棚を棚板として利用していたが、これ自体の強度が足りなかった。
  • 上記にPLAのはりを付けていたが、ありあわせのネジで組み立てた都合で、意図した強度が出ていなかった。
  • P1S - AMS - AMS のケーブルが短かく、ドロアをフル活用できなかった。 -> 後述ケーブルにて解決

この第一世代は、 Tetras 装着後にケーブル長の問題などからメンテ時に負荷がかかり、棚板が崩れてしまったので、第二世代として作り直すことにしました。


第二世代は、 Tetras 装着状態、もしくは未装着状態の AMS を搭載できるように作ってあります。AMSのサイズ感は 256x256 (mm) のベッドでは出力できないため、複数パーツにて構成して、インサートとM3ネジで組み立て、引き出し用のレールに載せています。加重はほとんど左右のレールに固定されたブロックにかかるようになっていて、思ったよりも安定しています。あえて言えば手でひっかける部分を付けておけばよかったか。
本業の始業前などにやっつけでモデリングしたので(言い訳です)あまりかっこよかったりはしませんが、目的達成には十分でした。直したいところがないわけではありませんが、「これでいいや」と使い続けています。
ダイソーポールとアウターレールをとめる部分については過去のプリント品をそのまま再利用しました。


ロングAMSケーブル

これまでの内容のように、AMSを複数台装着していると、AMS本体付属のケーブル(通信&電源)の長さが足りなくなります。ケーブルの長さが足りないことで、以下のような問題が発生します。

  • AMSをプリンタのすぐ隣に配置する必要があり、数センチ離すこともむつかしい。このため P1S 背面へのアクセスがしづらい。
  • 上段AMSと下段AMSのケーブルが短いために、片方のAMSをしっかり前に引き出すことがむつかしい。特に Tetras 装着後は運用上の課題が発生。

このため、付属ケーブルの約2倍の長さで代替のAMS接続ケーブルを作製しました。



AmazonやAliexpressなどで探せば既製品が見つかるかもしれないので、あれば既製品を使ってもいいと思います。私の場合は、このタイプのコネクタの圧着経験などがなかったので、自分で加工してみたかったという理由もあり、自分で製作しました。

ケーブル配線

純正ケーブルは調べたところ以下のシンプルなものでした。この調査結果に基づいてケーブルを製作します。
1-1
2-2
3-3
4-4
5-5
6-6

加工手順

加工は以下の手順で行います。
  • フラットケーブルを必要長さ+αで切断する。
  • 皮むき位置を揃えるため、油性ペンでフラットケーブルで皮むき位置をマークする。
  • 1芯目(赤)~6芯目まで、以下を繰り返す。
    • 皮むき位置で皮をむく。
    • 露出した銅より線が広がってしまう前に、はんだで軽く固めてしまう。
    • MicroFit用ピンソケットに、いちばん奥まで線をつけた状態で被膜もぎりぎり圧着できるところまで導線を切り詰める。
    • ターミナルに線を仮置きした状態で、はんだごての熱で、ケーブル側についたはんだとターミナルを軽くはんだ付け。(はんだを載せすぎると、のちの圧着作業やハウジングへの取り付けができなくなるので、軽めに)
    • 銅線の先側から被膜側まで、圧着工具でかしめていく
    • ケーブル部分からターミナルの先までまっすぐになるようにする。曲がっているとスムーズにターミナルを装着ができない
  • ハウジングを装着
  • テスターにて抵抗値を確認
私はそれ程この手の作業になれておらず、この手順で2本のケーブルを製作するのにおよそ3時間ほどかかりました。
テスターの抵抗値を見る限り問題がなさそうだったので、P1S-AMSの間をこのケーブルで接続してみると、P1S側からAMSのシリアルが正しく取得できていることが確認できました。これを確認の上、2本目に着手、AMS-AMSのデイジーチェーンを自作ケーブルに切り替え。問題なく動作しています。

部材調達メモ

フラットケーブルは、30芯ぐらいのものなら手元にありましたが、ここから6芯で50cm以上もとるには向かなかったので、今回の目的のために購入しました。秋葉原のマルツや千石などに見に行ったところ、思ったものがなく、最終的にオヤイデで5m購入しました。欲しいものが決まっていれば、依頼するだけで店員さんが用意してくれるのはありがたいです(無いものを聞いてしまうとすごい塩対応をくらう印象もありますがww)。
6芯利用なのに8芯を買ってしまったのですが、結果的になんとなく1芯目を赤色にできました。使っていない2芯は、その気になれば裂くだけです。

ケーブルを製作する場合、ターミナルはケーブル1本製作あたり12個、2本では24個必要になります。はんだ付けや圧着の失敗があれば、より多くの部品を消費することになるでしょう。ターミナル購入時は24本+予備を数えて購入は手間です。今回はまとめて100個で買いました。(歳をとって、こういった所をお金で解決できるようになったのは幸せだなあ)

当初は100個単位が店頭になく、通販しようとしたのですが、荷物に不着などの問題がおき、後日に店頭で100個単位で購入しました。

100個パックとはいえ、24個使用すれば 1/4 はすでに使っています。今回は失敗ナシで、無駄が出なかったため、残り76個あるはずですが、3Dプリンタ+電子工作などで何か作るときに利用したいと思っています。

購入から1年以上経過したが、現役スペックで、動かしても拡張しても楽しい P1S

直近では H2C や、 P1S 後継の P2S も販売されており、P1Sは「過去のモデル」となっていますが、自分にとっては P1S はまだまだオモチャ作りで活躍しています。また今回はAMS関連のアップグレードですが、H2Cなどを買ってしまうと、この内容はそのままH2Cに持っていけるでしょう。

購入から一年以上たち販売終了したモデルとはいえ、今回まとめたようなアップグレードに取り組むのも楽しいです。組み立てPCではもはや感じられなくなってしまった楽しさがあるようにーー 8ビット機や16ビット機も楽しく育てていたような人たちはこんな気持ちでパソコンを触っていたのかな、などと思ってみたり。