MP4ファイルを連結する。

2つあるいは3つのMP4ファイルを連結する必要が出てきました。たとえばビデオカメラで撮影した2本の動画だったり、ごにょごにょだったり、もにょもにょだったりしますが。


テキストファイルなどなら、コマンドプロンプトからcopy a.txt+b.txt c.txtでできますが、MP4ファイルの場合にはコンテナの中にビデオストリームとオーディオストリームが格納されていて、撮影情報とかメモとかいろんなタグが含まれていて、そのまま連結してもうまくいきません。

ビデオ編集には、PegasysのTMPGEnc MPEG Smart Renderer5(6にするメリットが今のところないので)を使っていますが、これで連結出力すると連結部分があまり良くない画質で再エンコードされてしまいます。いろいろとオプションなどを見てみたのですが、再エンコードせずに連結することはできないようです。
AviutlでMP4Pluginを使用すると、追加ファイルを読み込んだ上でMP4エクスポートすればいい、という記事も見つかりましたが、連結部分で止まってしまい、うまくいきませんでした。

ああ、そういえばffmpegをインストールしてたんだっけ、と、困ったときのffmpeg頼りで調べてみたら、できる方法がありました。
ffmpegのWikiよりConcatenate。 簡単に言えば二通りの方法があります。

リストファイルを作って作業する

連結したいMP4ファイルのリストを作成して、それをffmpegに処理させます。

以下のようなファイルを作ります。
file dir/input1.mp4
file dir/input2.mp4
file dir/input3.mp4
その上で、以下のコマンドで連結します。
ffmpeg -f concat -c copy -i listfile.lst outfile.mp4
もしもファイル名にスペースを含むようなら、''(シングルクォーテーション)でくくります。""(ダブルクォーテーション)ではエラーになります。

この方法では、中間ファイルを作らずに直接出力ファイルを作りますが、リストファイルを作成する手間がかかります。

中間ファイルを経由してmuxする

連結したいMP4ファイルからストリームを抜き出して、それを連結して新しいコンテナに収めます。

この方法では、エンコーダが異なったりFPSが異なったりフレームサイズが異なった場合には、取り出したストリームをエンコードし直すことで連結できる可能性があります。

ストリームがH264ビデオストリームの場合:
ffmpeg -i input1.mp4 -c copy -bsf:v h264_mp4toannexb -f mpegts input1.ts
ffmpeg -i input2.mp4 -c copy -bsf:v h264_mp4toannexb -f mpegts input2.ts
ffmpeg -i "concat:input1.ts|input2.ts" -c copy -bsf:a aac_adtstoasc output.mp4
上の2行のコマンドでそれぞれH264ストリームを抜き出して input1.ts、input2.ts という中間ファイルを作成します。

ストリームがHEVC(H265)の場合:
ffmpeg -i input1.mp4 -c copy -bsf:v hevc_mp4toannexb -f mpegts input1.ts
ffmpeg -i input2.mp4 -c copy -bsf:v hevc_mp4toannexb -f mpegts input2.ts
ffmpeg -i "concat:input1.ts|input2.ts" -c copy -bsf:a aac_adtstoasc output.mp4
コーデックが異なったりする場合には再エンコードが必要になるので、その分時間がかかりますが、詳細はこちらに。 ちなみにうちで使用しているffmpegはこちらからダウンロードしています。

Windows 11のための準備を整える。

Windows11のTPM。ではTPMについてちょっと触れましたが、Core i7-4990KではTPM2.0に対応していないので、TPM-SPIというようなTPMモジュールをインストールすれば…と思ったんですが、そもそも第4世代CPUはサポート対象外で、さらにノートPCで搭載している Core i7-6700HQ ではTPM2.0に対応してるのにやっぱりサポート対象外で。
今後サポートCPUが追加されるのかどうかはわかりませんが、現状ではWindows 11のインストーラ自体がCPUチェックで「インストールできません」と言ってきます。

前回の記事ではBIOS設定の Intel(R) Platform Trust Technology は有効化したのですが、PC正常性チェック(PC Health Check)プログラムまでは走らせていませんでした。そこで走らせてみたところ…。


と、セキュアブートをサポートしていないとだめです、と言われてしまいました。

そこでセキュアブートについても調べてみたのですが、これを有効化するためにはそもそもMBRパーティションではだめで、GPT(GUID Partition Table)に変換しないといけないようです。
うちのメインPCでは、mSATA SSDに換装したときに「起動ドライブは2TB未満だからMBRでいいか」と判断してMBRパーティションにしていました。

ということで、Windows 11にアップグレードするためには、起動ドライブをMBRからGPTに変換しないといけません。

変換方法を調べると、サードパーティのパーティションツールが色々出てきますが、どれもフリー版ではパーティションをMBRからGPTに変換する機能は利用できないようです。
これを見越したのかどうか、幸いにも Windows 10 Creators Update 1703以降では、MBR2GPT.exeというユーティリティプログラムが搭載されたようです。これを使うと、サードパーティツールを使わなくてもデータを失わずにGPTに変換できるようです。
まず、「ディスクの管理」で起動ドライブを確認します。

Cドライブはディスク4になります。ここで、ディスク4の上で右クリック→「プロパティ」で「ボリューム」タブを見ると、パーティションのスタイルが「レガシー」となっています。これをGPTに変更する必要があります。

別の方法として、コマンドプロンプトを管理者モードで開き、diskpartでも確認します。
C:\WINDOWS\system32# diskpart

Microsoft DiskPart バージョン 10.0.19041.964

Copyright (C) Microsoft Corporation.
コンピューター: UZI

DISKPART> list disk

  ディスク      状態           サイズ   空き   ダイナ GPT
  ###                                          ミック
  ------------  -------------  -------  -------  ---  ---
  ディスク 0    オンライン          7452 GB  1024 KB        *
  ディスク 1    オンライン           931 GB  1024 KB        *
  ディスク 2    オンライン          5589 GB      0 B        *
  ディスク 3    オンライン          5589 GB  1024 KB        *
  ディスク 4    オンライン           476 GB  1024 KB

DISKPART> exit

DiskPart を終了しています...

ディスク4のGPTの欄に "*" がないのがわかります。

次に変換を実行する前に、ディスクの検証を行います。
C:\WINDOWS\system32# mbr2gpt /validate /disk:4 /allowfullos
MBR2GPT: Attempting to validate disk 4
MBR2GPT: Retrieving layout of disk
MBR2GPT: Validating layout, disk sector size is: 512 bytes
MBR2GPT: Validation completed successfully

"/allowfullos"オプションを指定しないとエラーになりますが、正常終了したようです。

ということでいよいよ変換です。失敗するとディスクが飛ぶんじゃないかと心配もありますが、やってしまいます。
C:\WINDOWS\system32# mbr2gpt /convert /disk:4
ERROR: MBR2GPT can only be used from the Windows Preinstallation Environment. Use /allowFullOS to override.

C:\WINDOWS\system32# mbr2gpt /convert /disk:4 /allowfullos

MBR2GPT will now attempt to convert disk 4.
If conversion is successful the disk can only be booted in GPT mode.
These changes cannot be undone!

MBR2GPT: Attempting to convert disk 4
MBR2GPT: Retrieving layout of disk
MBR2GPT: Validating layout, disk sector size is: 512 bytes
MBR2GPT: Trying to shrink the OS partition
MBR2GPT: Creating the EFI system partition
MBR2GPT: Installing the new boot files
MBR2GPT: Performing the layout conversion
MBR2GPT: Migrating default boot entry
MBR2GPT: Adding recovery boot entry
MBR2GPT: Fixing drive letter mapping
MBR2GPT: Conversion completed successfully
Call WinReReapir to repair WinRE
MBR2GPT: Failed to update ReAgent.xml, please try to  manually disable and enable WinRE.
MBR2GPT: Before the new system can boot properly you need to switch the firmware to boot to UEFI mode!
C:\WINDOWS\system32#

一応20行目に "MBR2GPT: Conversion completed successfully" とあるので変換は成功したようですが、WinRE(Windows Recovery Environment: Windows)で ReAgent.xml を更新できないので手動で無効化/有効化してください、というメッセージが出ています。色々やってみたのですが、とりあえず回復の設定はあとからでも大丈夫そうです。

その下には「新しいシステムをブートする前にUEFIモードに切り替えないとだめよ」というメッセージもあります。うっかりここで再起動してしまうと大変なことになりそうです。

ということでここからはキャプチャはないのですが、再起動してUEFI画面(DELキー連打)を出して、CSM互換オプションを無効にしてPKを生成し、セキュアブートを有効化してもう一度再起動です。Windowsが起動する前に何度かUEFI画面で設定を確認し直す慎重さは必要かもしれません。

うれしいお知らせです。が表示されました。

WindowsにLua 5.1をインストールしてLuarocksする。

NeovimにはLuaJITが組み込まれています。これはLua 5.1がベースになったLuaJIT 2.1.0-beta3とのこと。ちなみにLua 5.1系自体は2012年2月17日にbug-fixリリースの5.1.5が最後です。その前年の12月に5.2.0がリリースされ、その後現在のバージョンは5.4.3になっています。

Pythonはちょろっといじってきたのですが、せっかくNeovimにLuaが組み込まれているのだし、VimScriptも難しいし、テキスト加工の自動化とかもLuaでできたら楽なのかな、と思ったのでLuaに手を出してみることにしました。

ということでちょっと調べてみると、Lua 5.1では文字列はbyte列として扱う、ということで、たとえばコマンドプロンプトでLuaインタプリタを起動し、string.len("あいうえお")とすると、
  • CP932環境では 10
  • CP65001環境では 5
です。一方Neovim(encoding=utf-8)で:lua print(string.len('あいうえお'))すると15が返ってきます。

ちょっとやってみただけなので間違ってたり環境依存だったり勘違いだったりするのかもしれませんが、まあそんな感じで一筋縄では行かなそうだし、本当に文字エンコーディングの問題は魂に刻み込まれた呪いのようです。

Windows 11が出るよ、と言われて、全部Unicodeになるの?と思ったらやっぱりならないみたいだし、この呪いはもはや「with 文字エンコーディング」として付き合っていかないとなのでしょうか。

閑話休題 それはさておき

調べてみると、Lua 5.1でも使えるutf8ライブラリがあるようです。starwindg/luautf8は、5.3のビルトインのものと互換性があるようで、これを使うのが一番良さそうです。ところがこれは LuaRocks を利用してインストールする形になっているので、Lua + LuaRocks が必要です。
 

Lua 5.1のインストール

ともかく、Luaをインストールしてしまいましょう。といっても、最新の5.4系ではなく、Neovimと同じ5.1系を選びます。具体的には最終版の5.1.5。

Lua DownloadページのToolsのところに、LuaBinariesというリンクがあるのでそこに飛びます。すると、[05/May/2014]のところに Lua 5.1.5 - Release 1 というリンクがあるのでそこをクリックします。
そこにある Tools Executables に実行形式が、Windows Libraries にDLLがあります。

使用しているNeovimは64bitビルドなので、ここでは lua-5.1.5_Win64_bin.zip を選びます。が!ここで注意なのですが、bin.zipのほうはVC+2005でコンパイルされています。そのためランタイムも必要になり、ちょっとめんどくさいです。

また、LuaRocks を動作させるためにはヘッダーファイルが必要となるため、上記 Windows Libraries から lua-5.1.5_Win64_dll16_lib.zip もダウンロードします。
bin.zipのほうはVC+2005でコンパイルされていますが、dll16_lib.zipはVS2019でコンパイルされています。

なので、余計なランタイムをなくすためにも、ここではソースもダウンロードして、Lua 5.1.5もVS2019でコンパイルし直します。ソースはtar ballになっていて、lua-5.1.5.tar.gz からダウンロードできます。

Lua 5.1のコンパイル

ダウンロードしたtar ballをほどいたら、VS2019のNative x64 コマンドプロンプトを開き、lua-5.1.5ディレクトリに移動します。INSTALLファイルを見るとプロジェクトを自分で作ってコンパイルしなさい、とありますが、ここでは簡単に etc\luavs.bat を起動します。必要に応じて src\luaconf.h を編集しなさいともありますが、特に希望はないのでそのままで。

コンパイルが無事に終われば、lua.exe、luac.exe、lua51.dll、lua51.libができています。ここではlua51.libを c:\Apps\Lua\lib に、その他を c:\Apps\Lua\bin にコピーします。
また、lua.h、lua.hpp、luaconf.h、lualib.h、luaxlib.hを c:\Apps\Lua\include にコピーします。lua.hppは etcディレクトリにあるのでご注意。
コピーしたら c:\Apps\Lua\bin にPATHを通して動作チェックです。

LuaRocksのインストール

次にLuaRocksをインストールします。といっても、Installation instructions for Windowsに従って実行形式ファイルをコピーするだけですが。
LuaRocksのダウンロードページから、Windows all-in-one executable (64-bit)をダウンロードします。このzipファイルを開くとluarocks.exeとluarocks-admin.exeがあるので、これを先程の  c:\Apps\Lua\bin コピーします。

コピーしたら、環境変数を設定します。
  • LUA_DIR : c:\Apps\Lua
  • LUA_BINDIR : c:\Apps\Lua\bin
  • LUA_INCDIR : c:\Apps\Lua\include
  • LUA_LIBDIR : c:\Apps\Lua\lib
あるいは、以下のようにしてconfigファイルを作成しておきます。
$ luarocks --lua-version 5.1 config --global variables.LUA_DIR c:\Apps\Lua
$ luarocks --lua-version 5.1 config --global variables.LUA_BINDIR c:\Apps\Lua\bin
$ luarocks --lua-version 5.1 config --global variables.LUA_INCDIR c:\Apps\Lua\include
$ luarocks --lua-version 5.1 config --global variables.LUA_LIBDIR c:\Apps\Lua\lib

します。Luaのデフォルトのバージョンは5.3になっているようで、5.1を利用する場合には --lua-version オプションが必要です。見落としているかもしれませんが、どうもデフォルトで5.1に固定するような設定はなさそうです。

luautf8のコンパイル

次はluautf8のコンパイルです。
これは、VS2019のx64 コマンドプロンプトを開いて、
$ luarocks --lua-version 5.1 --global install luautf8
とします。
$ luarocks --lua-version 5.1 --global install luautf8
Installing https://luarocks.org/luautf8-0.1.3-1.src.rock

luautf8 0.1.3-1 depends on lua >= 5.1 (5.1-1 provided by VM)
cl /nologo /MD /O2 -c -Folutf8lib.obj -Ic:\Apps\Lua\include lutf8lib.c
lutf8lib.c
link -dll -def:lua-utf8.def -out:lua-utf8.dll c:\Apps\Lua\lib/lua51.lib lutf8lib.obj
Microsoft (R) Incremental Linker Version 14.29.30133.0
Copyright (C) Microsoft Corporation.  All rights reserved.

   ライブラリ lua-utf8.lib とオブジェクト lua-utf8.exp を作成中
luautf8 0.1.3-1 is now installed in C:\Users\kats\AppData\Roaming/luarocks (license: MIT)
ちょっと深い場所に出力されていますが、ここでできた lua-utf8.dll をc:\Apps\Lua\bin にコピー、また Neovim から使うには c:\Apps\Neovim\bin にコピーすれば require で参照できます。

Lua 5.3の標準ライブラリと同様に使うために、lua-utf8 を utf8 として参照できるようにします。
> utf8 = require 'lua-utf8'
> print(utf8.reverse('あいうえお'))
おいうえあ
ただ、コマンドプロンプトとかCP932な環境なので今ひとつな感じ。Neovimだけならなんとかなるかしら。

Path.read_text()をリストに。

cp932からutf-8への文字コード変換。では、ファイルを明示的にオープンしなくてもテキストファイルをエンコード付きで読み込めることを書きました。

実はここで読み込めるテキストは、改行コード"\n"まで含めて1つの文字列になっています。ちょうど、改行付きのテキストファイルをrepr()したような感じです。

もしも1行ごとに取り出してなにかの処理をしたいとしたらこのままでは困りますから、リストにする必要があります。すごく簡単で当たり前の処理なのでわざわざ記事にするほどのものでもないのですが、str.split()を使います。
from pathlib import Path

textfile = Path("abc.txt")
content = textfile.read_text(encoding="utf-8")
c_list = str.split(content, "\n")
これでファイルの内容を読み出してリストにすることができました。

ちなみに、中間の読み出し結果が不要なら、
from pathlib import Path

textfile = Path("abc.txt")
content = str.split(textfile.read_text(encoding="utf-8"), "\n")
とすれば、最初からリストが得られます。ただし、改行コードがすべて置き換わっているので、必要に応じて行末に改行コードを付加しなくてはいけません。

cp932からutf-8への文字コード変換。

世の中、まだまだ文字コードの問題はなくなりません。特に日本語をはじめとするマルチバイト文字の文化圏では……。

ご多分に漏れずうちの環境でも同様で、Windows標準のFINDは率直に言って使いづらく、最近はripgrepthe Platinum Seacherなどを使っていますが。

それはともかく、プログラムからテキストファイルをいじりたいときに、文字コードが揃っていないととにかく大変です。個人的にはPCにあるすべてのテキストファイルはUTF-8であってほしいところですが、まだまだシフトJISがはびこっています。

なのでこれを一括でUTF-8に変換するにはどうすればいいかとちょっと前に検索したところ、Pythonのcodecs.StreamRecoder()を使ってストリームを流しながらcodecを通す、という方法があったのでこれを使っていました。

ところが最近pathlibをよく使うようになり、以前にos.walk()を使って取得していたディレクトリツリーのファイルリストをpathlib.Path.glob()に書き直しているときに、pathlib.Path.read_text()というのを見つけました。

Path.read_text()は以下のように使います。
from pathlib import Path

infile = Path("abc.txt")
content = infile.read_text(encoding="cp932")

outfile = Path("xyz.txt")
outfile.write_text(content, encoding="utf-8")
大きなサイズのファイルであれば読み込みサイズを加減する必要がありますが、小さいサイズならこれで十分です。明示的にファイルをopen()する必要すらありません。
あまり大きなファイルでは試していませんが、600KBのファイルを読み込んで処理するのは問題ないようです。

上記の例ではinfileoutfileを別にしていますが、Path.read_text()Path.write_text()は自動でファイルを開き、読み込み/書き込み処理をし、すみやかにファイルを閉じる、というところまでやってくれます。with open()...を使ったコンテキスト構文すら不要です。

また、ファイルは閉じられているので「読み込んですぐに文字コードを変えて書き戻す」ということもできます。
from pathlib import Path

textfile = Path("abc.txt")
content = textfile.read_text(encoding="cp932")
textfile.write_text(content, encoding="utf-8")
3行で文字コード変換できてしまいます。

おかげでコードがけっこう短くなりました。

Manjaroで xhci-pci のファームウェアがないと言われる件。

Manjaro Linuxでpacman -Syuしてカーネルをアップグレードしたら、
==> Building image from preset: /etc/mkinitcpio.d/linux510.preset: 'default'
  -> -k /boot/vmlinuz-5.10-x86_64 -c /etc/mkinitcpio.conf -g /boot/initramfs-5.10-x86_64.img
==> Starting build: 5.10.56-1-MANJARO
  -> Running build hook: [base]
  -> Running build hook: [udev]
  -> Running build hook: [autodetect]
  -> Running build hook: [modconf]
  -> Running build hook: [block]
==> WARNING: Possibly missing firmware for module: xhci_pci
  -> Running build hook: [filesystems]
  -> Running build hook: [keyboard]
  -> Running build hook: [fsck]
==> Generating module dependencies
などという警告が出てきました。

このxhci_pciは、RenesasのuPD72020xというUSB3コントローラのようで、おそらくそのファームウェアがライセンスの関係で同梱できないためにインストールされない、ということのようです。ぐぐってみるとkernel-5.8以降で話題になっているようなので、そのタイミングで同梱されなくなったのかもしれません。kernel.orgのBugzillaでは、Bug 208911 - Renesas USB controller - FW has invalid version :8224というのが上がっています。

Renesasからはバイナリが提供されているためこれをインストールできればよいのですが、Manjaroではパッケージの提供がありません。このファームウェアなしでもUSBは動作しますが、パフォーマンスが出ないというような書込みも見られます。

一方、ManjaroのベースとなっているArchLinuxではAURにてパッケージが提供されています。ところがManjaroではAURからのインストールはできません。パッケージをダウンロードしてきてオフラインインストールしようとpacman -Uしても「パッケージが破損している」というメッセージが出てきてインストールできません。
mito /home/kats% sudo pacman -U upd72020x-fw.tar.gz 
パッケージをロード...
エラー: upd72020x-fw.tar.gz にパッケージのメタデータが見つかりません
エラー: 'upd72020x-fw.tar.gz': 無効または破損したパッケージ


同じpacmanでも、ArchLinuxとManjaroでは管理方法が違うためにエラーになります。 Manjaroでは、ユーザの自己責任でAURからパッケージをインストールするサポートツールのyayがあり、これを使えばインストールできるようです。

yayはrootやsudoで実行してはいけません。
mito /home/kats% sudo yay -Syua
 -> yay を root や sudo で実行しないでください。
-SyuaはAURのリポジトリのみを更新するためのオプションです。

mito /home/kats% yay -Syua
:: AUR からアップデートを検索...
 -> python2-gobject2: ローカルのパッケージ (2.28.7-7) は AUR (2.28.7-6) よりも新しいバージョンです
 -> 存在しない AUR パッケージ:  geneigothicm  gksu-polkit  gtk-xfce-engine  js52  js60  libnm-glib  libnm-gtk  libopenaptx  linux-latest  linux-latest-headers  linux-latest-r8168  linux59  linux59-headers  linux59-r8168  mhwd-catalyst  mhwd-nvidia-304xx  mhwd-nvidia-340xx  orage  python-sip-pyqt5  python2-gevent  python2-pyqt5  python2-sip-pyqt5  uwsgi-plugin-python2  xf86-input-keyboard  xf86-input-mouse
 -> メンテナが存在しない AUR パッケージ:  idnkit  python-lazr-smtptest  python2-trollius  zinnia
 -> 古いバージョンのフラグが立てられた AUR パッケージ:  moinmoin
:: 9 アップグレードするパッケージ。
9  aur/chrome-remote-desktop  77.0.3865.32-1 -> 92.0.4515.41-1
8  aur/idnkit                 2.3-3          -> 2.3-4
7  aur/makefontpkg            20160320-1     -> 20200526-1
6  aur/nkf                    2.1.5-1        -> 2.1.5-2
5  aur/python-lazr-smtptest   2.0.3-1        -> 2.0.4-1
4  aur/python2-futures        3.3.0-2        -> 3.3.0-3
3  aur/python2-greenlet       1.0.0-1        -> 1.1.0-1
2  aur/tortoisehg-hg          4.9.1.18769-1  -> 5.5.1.19453-1
1  aur/xdg-su                 1.2.3-1        -> 1.2.3-2
==> 除外するパッケージ: (例: "1 2 3", "1-3", "^4" またはリポジトリ名)
==> ^C
mito /home/kats% yay -Ss upd72020
aur/upd72020x-fw 20200826-3 (+38 4.39) 
    Renesas uPD720201 / uPD720202 USB 3.0 chipsets firmware
AURではManjaroのリポジトリよりも新しいものがあるためアップグレードのメッセージが出ますが、これらは無視します。

mito /home/kats% yay -S upd72020x-fw
:: 衝突を確認...
:: 内部衝突を確認...
[Aur:1]  upd72020x-fw-20200826-3

:: PKGBUILD のダウンロード (1/1): upd72020x-fw
  1 upd72020x-fw                             (ビルドファイルが存在)
==> 差異を表示しますか?
==> [N]なし [A]全て [Ab]中止 [I]インストール済み [No]未インストール または (1 2 3, 1-3, ^4)
==> 1
diff --git /home/kats/.cache/yay/upd72020x-fw/.gitignore /home/kats/.cache/yay/upd72020x-fw/.gitignore
new file mode 100644
index 0000000..62514e8
--- /dev/null
+++ /home/kats/.cache/yay/upd72020x-fw/.gitignore
@@ -0,0 +1,2 @@
+*
+.*
diff --git /home/kats/.cache/yay/upd72020x-fw/PKGBUILD /home/kats/.cache/yay/upd72020x-fw/PKGBUILD
new file mode 100644
index 0000000..bc1a063
--- /dev/null
+++ /home/kats/.cache/yay/upd72020x-fw/PKGBUILD
@@ -0,0 +1,28 @@
+# Maintainer: Jack Chen <redchenjs@live.com>
+
+pkgname=upd72020x-fw
+pkgver=20200826
+pkgrel=3
+pkgdesc="Renesas uPD720201 / uPD720202 USB 3.0 chipsets firmware"
+arch=('any')
+url="https://github.com/denisandroid/uPD72020x-Firmware"
+license=('custom')
+source=(
+  "https://raw.githubusercontent.com/denisandroid/uPD72020x-Firmware/master/UPDATE.mem"
+  "https://raw.githubusercontent.com/denisandroid/uPD72020x-Firmware/master/License.rtf"
+  "remove.hook"
+)
+sha512sums=(
+  '1ea117f9a1a772013fb7509c76d731865e6c05ae3c55a304ff42b31ec8a474e9bf16dd1b05b2e5b666ec5fd301aefed54bfeb6bfd7c3f23dc23faf082cf2a9f7'
+  'f5be9af49a6ec81f77275c6f4092e6675a707a95a33bf37eb9ba84a7226f3310eebffb7699f8b9b12110c9ca2af1a56f528a94f1e4891fd45f297affd8ebb577'
+  '47aa4c4c3a0014df79b7a7998edfbc7b436ae6e966432f3787d9f1655c986591c73165de6fad52ebb5cefd4f8101b9b094d117f0508cd1f8f0d2c7396bbd3f91'
+)
+
+package() {
+  install -Dm644 UPDATE.mem "$pkgdir/usr/lib/firmware/renesas_usb_fw.mem"
+  install -Dm644 License.rtf "$pkgdir/usr/share/licenses/$pkgname/LICENSE.rtf"
+
+  # firmware install & remove hooks
+  install -Dm644 /dev/null "$pkgdir/usr/lib/initcpio/hooks/$pkgname"
+  install -Dm644 remove.hook "$pkgdir/usr/share/libalpm/hooks/$pkgname.hook"
+}
diff --git /home/kats/.cache/yay/upd72020x-fw/remove.hook /home/kats/.cache/yay/upd72020x-fw/remove.hook
new file mode 100644
index 0000000..68e2b20
--- /dev/null
+++ /home/kats/.cache/yay/upd72020x-fw/remove.hook
@@ -0,0 +1,10 @@
+[Trigger]
+Type = Package
+Operation = Remove
+Target = upd72020x-fw
+
+[Action]
+Description = Updating linux initcpios...
+Depends = mkinitcpio
+When = PreTransaction
+Exec = /bin/sh -c "rm -f /usr/lib/firmware/renesas_usb_fw.mem &s;&s; mkinitcpio -P"

==> インストールを実行しますか? [Y/n] 
:: (1/1) SRCINFO を解析中: upd72020x-fw
==> パッケージを作成: upd72020x-fw 20200826-3 (2021年08月14日 10時39分13秒)
==> ソースを取得...
  -> ダウンロード UPDATE.mem...
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 13012  100 13012    0     0  48578      0 --:--:-- --:--:-- --:--:-- 48734
  -> ダウンロード License.rtf...
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 32697  100 32697    0     0   123k      0 --:--:-- --:--:-- --:--:--  123k
  -> remove.hook を見つけました
==> source で sha512sums ファイルを検証...
    UPDATE.mem ... 成功
    License.rtf ... 成功
    remove.hook ... 成功
==> パッケージを作成: upd72020x-fw 20200826-3 (2021年08月14日 10時39分17秒)
==> ランタイムの依存関係を確認...
==> ビルドタイムの依存関係を確認...
==> ソースを取得...
  -> UPDATE.mem を見つけました
  -> License.rtf を見つけました
  -> remove.hook を見つけました
==> source で sha512sums ファイルを検証...
    UPDATE.mem ... 成功
    License.rtf ... 成功
    remove.hook ... 成功
==> 既存の $srcdir/ ディレクトリを削除...
==> ソースを展開...
==> ソースの準備ができました。
==> パッケージを作成: upd72020x-fw 20200826-3 (2021年08月14日 10時39分24秒)
==> ランタイムの依存関係を確認...
==> ビルドタイムの依存関係を確認...
==> 警告: 既存の $srcdir/ ツリーを使用
==> fakeroot 環境を開始します...
==> package() を開始...
==> インストールを整理...
  -> libtool ファイルを削除...
  -> 不要なファイルを削除...
  -> スタティックライブラリファイルを削除しています...
  -> バイナリとライブラリから不要なシンボルを削除...
  -> man と info ページを圧縮...
==> パッケージの問題をチェック...
==> パッケージを作成 "upd72020x-fw"...
  -> .PKGINFO ファイルを生成...
  -> .BUILDINFO ファイルを生成...
  -> .MTREE ファイルを生成...
  -> パッケージの圧縮...
==> fakeroot 環境を終了。
==> 作成完了: upd72020x-fw 20200826-3 (2021年08月14日 10時39分29秒)
==> 清掃...
パッケージをロード...
依存関係を解決しています...
衝突するパッケージがないか確認しています...

パッケージ (1) upd72020x-fw-20200826-3

合計インストール容量:  0.04 MiB

:: インストールを行いますか? [Y/n] 
(1/1) キーリングのキーを確認                                                                [######################################################] 100%
(1/1) パッケージの整合性をチェック                                                          [######################################################] 100%
(1/1) パッケージファイルのロード                                                            [######################################################] 100%
(1/1) ファイルの衝突をチェック                                                              [######################################################] 100%
(1/1) 空き容量を確認                                                                        [######################################################] 100%
:: パッケージの変更を処理しています...
(1/1) インストール upd72020x-fw                                                             [######################################################] 100%
:: トランザクション後のフックを実行...
(1/2) Arming ConditionNeedsUpdate...
(2/2) Updating linux initcpios...
==> Building image from preset: /etc/mkinitcpio.d/linux510.preset: 'default'
  -> -k /boot/vmlinuz-5.10-x86_64 -c /etc/mkinitcpio.conf -g /boot/initramfs-5.10-x86_64.img
==> Starting build: 5.10.56-1-MANJARO
  -> Running build hook: [base]
  -> Running build hook: [udev]
  -> Running build hook: [autodetect]
  -> Running build hook: [modconf]
  -> Running build hook: [block]
  -> Running build hook: [keyboard]
  -> Running build hook: [keymap]
  -> Running build hook: [resume]
  -> Running build hook: [filesystems]
  -> Running build hook: [fsck]
==> Generating module dependencies
==> Creating gzip-compressed initcpio image: /boot/initramfs-5.10-x86_64.img
==> Image generation successful
ちょっと余計な差分表示もありますが、こんな感じでxchi_pciの警告は消えました。

mito /home/kats% lsmod | grep xhci_pci
xhci_pci               20480  0
xhci_pci_renesas       20480  1 xhci_pci
ファームウェアもちゃんとロードされているようです。

QNAPがサムネイルキャッシュを自動的に作成する件。

うちでは NAS に QNAP TS-669L を使っています。

古いモデルなので最近の大容量 HDD に換装するためには HDD トレイを買い直す必要があったりして余計な出費になりましたが、今の所 10TB x6 の構成で元気に動いています。一応 CPU は Intel Atom D2701 デュアルコア(論理4コア)だし、メモリは 2GB 追加してトータル 3GB はあるし、Gigabitイーサなので、LANを10Gbitにしない限りは余裕は不足感はありません。

で、SSH ログインしていろいろいじっていたら、フォルダに所有権が管理者になっている .@__thumb というディレクトリがあって、サムネイルキャッシュが格納されていました。結構地味に容量食ってます。

これはケシカランということで、サムネイルの自動生成を止める方法と、すでに作られてしまったキャッシュを.@__thumbディレクトリごと削除する方法をまとめます。


まず、QNAP の管理ウェブインタフェースを開きます。そして File Station を開きます。


File Station 右上のメニューから、「設定」を開きます。

すると「マルチメディア再生とサムネイル再生をサポートします」という項目があるので、チェックを外します。
もしも使っていないようなら、「コントロールパネル」→「マルチメディア管理」→「メディアライブラリー」で、メディアライブラリーを停止しておきます。
以上でサムネイルは自動生成されなくなるはず、です。たぶん。

次に.@__thumbをまるごと削除する方法ですが、検索すると、findxargsを使って find ./* -name ".@__thumb" -type d | xargs rm -rfという方法が引っかかってきます。

ところがこれだとうまく削除できません。たぶん、Busyboxだからじゃないかと思いますが、ともあれこれじゃだめです。

QTS 4.3.4 にはデフォルトで Python 2.7.5 がありますので、これを使います。
import os
import shutil

for root, dirs, files in os.walk(".", topdown=False):
    print("Root: %s" % root)
    for dir in dirs:
        if dir == ".@__thumb":
            delpath = os.path.join(root, dir)
            print "Removing %s" % delpath
            shutil.rmtree(delpath)
os.walk()してカレントディレクトリ以下のすべてのディレクトリを検索して、名前が ".@__thumb" だったらツリーごと削除、というだけのスクリプトです。

あとはadminでSSHログインして然るべきディレクトリで実行するだけです。

C/MigemoでAND検索するには。

検索結果を絞り込むために、「AかつBを含む」という条件を設定したいことがあります。

ところが、Pythonのreモジュールには、論理和(AまたはB)はあっても論理積(AかつB)はありません。というか、正規表現というもの自体に論理積がないのかもしれません。if文の条件にはできるのに。

PDFのお料理レシピを検索する。で、recipe tamago cabbageができませんと書いたのですが、これを実現するには篩がけを2回すればいいことになります。
つまり、"tamago"で検索した結果に対してさらに"cabbage"で検索を行って絞り込むわけです。

前回のコードでは、一致した結果をrecipe_files.append()で追加していきましたが、2回め以降の絞り込みでは、一致しない要素をrecipe_files.remove()で取り除いてやればいけそうです。

よくよく考えてみると、これはC/Migemoに限らずre.search()でAND検索をやるときにも使えそうです。汎用性を考えるなら、モジュール化してやれば便利そうなので、ちょっと考えてみます。(次回に続く)

PDFのお料理レシピを検索する。

日本語検索を「かな漢字変換」を通さずにやる。その2で、ファイル名で検索かけて一致するものをピックアップする、というのをやったので、これは使えるかも、とPDFにウェブクリップしていたお料理レシピを検索するようにしてみました。

  • 複数キーワード対応(AND検索)とマッチ数を表示するようにしました。2021/8/1 ソース改定
  • 新規追加されたファイルのみキャッシュに登録するオプションを追加しました。2021/8/2 ソース改定

ク****ドや味*素などいろんなサイトでレシピが公開されていますが、「今日は豚こまがあるから…」とグーグル先生に「豚こま レシピ」とか毎回検索してもいいんですが、気に入ったレシピはChromeからPDF形式でファイルに落として(印刷して)おいて、ちょくちょく参照しています。当然それが溜まってくるとタイトル(ファイル名)から見つけにくくなってくるので、これをなんとかしようと。

ライブラリのように数千もあるわけではないので、ディレクトリ指定でos.walk()してファイル名をパターンサーチしていけばいいか、ということで、引っかかったファイル名をSumatraPDFに食わせてそのままPDF表示、という形にしました。

ついでに、「せっかくC/Migemoを使えるんだからローマ字でも検索できるようにしちゃえ」ということで組み合わせ、さらに「コンテンツ(レシピ内容)からも検索できるようにしたほうがいいよね」ということでpdfminerも組み合わせることにしました。

ところがPDFファイルが少なければ問題ないのですが、多くなってくると毎回pdfminerでテキスト抽出してパターンサーチというのはものすごく時間がかかります。そこで、一回pdfminerでextract_text()したらその結果をキャッシュしておけばいい、ということで、SQLite3を使ってキャッシュを行うようにしました。

さらに、コンテンツを検索したいときには"-c"スイッチを指定して、デフォルトではコンテンツを検索しないようにしました。未キャッシュのファイルが多いときには"-c"を指定するとものすごく時間がかかりますが、一度キャッシュすれば楽ちんです。ほんとはタイムスタンプまで含めて登録しておいて、ファイル日付が違ってたらキャッシュし直しとかしてもいいんですが…。

手前味噌ながらこのユーティリティのいいところは、recipe tamagoとすると、「たまご」「タマゴ」「玉子」「卵」「Egg」などの表記の揺れをカバーしてくれることです(辞書による)。ただし、C/Migemoの仕様の関係でrecipe tamago cabbageなどのように複数のキーワードを指定しても無駄なところがあります。複数キーワードも受け付けるようにしました。複数指定された場合には、AND検索(AとBを同時に含むもの)に絞り込みできます。たとえばrecipe バジル トマト とかrecipe 鶏肉 pi-manとか。

ちょっと長いですが、recipi.pyです。
  • 実行にはC/Migemoとpdfminer、SumatraPDFがインストールされていることが必要です。
  • 配置ディレクトリなどは適宜修正してください。
  • recipe -h でオプション一覧が出ます。

#!python
#!python
# -*- coding: utf-8 -*-
# vim:fenc=utf-8 ff=unix ft=python ts=4 sw=4 sts=4 si et fdm fdl=99:
# vim:cinw=if,elif,else,for,while,try,except,finally,def,class:
#
# copyright (c) K.Fujii 2021
# created : Jul 26, 2021
# last modified: Aug 2, 2021
# Changelog:
# Jul 26, 2021: migemoを利用してローマ字でも検索するように変更
#             : pdfminerでPDFをテキスト化し、コンテンツについても検索できるよう変更
# Jul 27, 2021: SQLite3でテキスト化したコンテンツをキャッシュできるように変更
#             : 複数キーワード対応
# Jul 28, 2021: キャッシュをドロップして再生成するオプションを追加
# Aug 01, 2021: 一致した件数を表示
# Aug 02, 2021: 新規ファイルのみキャッシュ追加を行うオプションを追加
"""
recipi.py:
レシピのウェブクリップPDFから入力されたキーワードに一致するものを検索し、SumatraPDFで表示する
キーワード検索にはC/Migemoを利用し、かな漢字変換をしなくても検索できる
対象ディレクトリは recipe_dir に指定する
TODO: SumatraPDFで同じファイルを2重に表示しないようにする?
TODO: 候補数が多いときには選べるようにする?
"""

import argparse
import io
import logging
import os
import subprocess
import re
import sqlite3
import sys

import cmigemo
from pdfminer.high_level import extract_text
from pdfminer.pdfparser import PDFSyntaxError

migemo_dict = "c:/Apps/bin/dict/base-dict"
recipe_dirs = "p:/料理レシピ"
cache_db = "p:/料理レシピ/recipe.db"
PDF_viewer = [
    "SumatraPDF",
    "-reuse-instance",
]


def compile_pattern(S):
    """return compiled pattern"""
    logger.debug(S)
    migemo = cmigemo.Migemo(migemo_dict)
    ret = migemo.query(S)
    logger.debug("regex=%s", ret)
    return re.compile(ret, re.IGNORECASE)


def list_files(directory):
    """list all the files under specified directory"""
    ret = []
    logger.debug("recipe_dirs: %s", directory)
    for root, dirs, files in os.walk(directory):
        t = [os.path.join(root, f) for f in files]
        ret.extend(t)
    logger.debug(ret)
    return ret


def extract_content(f):
    try:
        ret = repr(extract_text(f))
    except PDFSyntaxError:
        ret = None
    return ret


def search_in_dirs(patterns, files, contents):
    """search pattern in files"""
    recipe_files = []
    if contents:
        conn = sqlite3.connect(cache_db)
        # テーブルがなければ作成する
        conn.execute("CREATE TABLE IF NOT EXISTS recipes (fname TEXT, contents TEXT)")
        conn.row_factory = sqlite3.Row
        cur = conn.cursor()
    pat = compile_pattern(patterns[0])
    for f in files:
        if os.path.splitext(f)[1] == ".pdf":
            if pat.search(f):
                # ファイル名のみの検索
                logger.debug("matched %s", f)
                recipe_files.append(f)
            elif contents:
                # PDFコンテンツも検索
                cur.execute(
                    "SELECT * FROM recipes WHERE fname LIKE ?", ("%" + f + "%",)
                )
                data = cur.fetchone()
                if data:
                    if pat.search(data["contents"]):
                        recipe_files.append(f)
                else:
                    # キャッシュされていない場合にはキャッシュに登録
                    c = repr(extract_content(f))
                    if c:
                        logger.debug("INSERT: %s", f)
                        logger.debug("CONTENTS: %s", c)
                        cur.execute(
                            "INSERT into recipes VALUES (?, ?)",
                            (f, c),
                        )
                        conn.commit()
                        if pat.search(c):
                            recipe_files.append(f)
    # patternsの要素が1のときにエラーになる?
    for pattern in patterns[1:]:
        pat = compile_pattern(pattern)
        for f in recipe_files[:]:
            logger.debug("file: %s", f)
            if not pat.search(f):
                if contents:
                    # PDFコンテンツも検索
                    cur.execute(
                        "SELECT * FROM recipes WHERE fname LIKE ?", ("%" + f + "%",)
                    )
                    data = cur.fetchone()
                    if data:
                        if not pat.search(data["contents"]):
                            logger.debug("unmatched %s", f)
                            recipe_files.remove(f)
                else:
                    recipe_files.remove(f)
    if contents:
        conn.close()

    if len(recipe_files):
        view_cmd = PDF_viewer + recipe_files
        print("matched {0} files with {1}".format(len(recipe_files), patterns))
        subprocess.Popen(view_cmd)
    else:
        print("No file matched :", patterns)


def generate_cache():
    """新規追加されたファイルをキャッシュに追加する"""
    files = list_files(recipe_dirs)
    conn = sqlite3.connect(cache_db)
    # テーブルがなければ作成する
    conn.execute("CREATE TABLE IF NOT EXISTS recipes (fname TEXT, contents TEXT)")
    conn.row_factory = sqlite3.Row
    cur = conn.cursor()
    for f in files:
        # キャッシュされていない場合にはキャッシュに登録
        cur.execute("SELECT * FROM recipes WHERE fname LIKE ?", ("%" + f + "%",))
        data = cur.fetchone()
        if not data:
            c = repr(extract_content(f))
            logger.debug("INSERT: %s", f)
            logger.debug("CONTENTS: %s", c)
            cur.execute(
                "INSERT into recipes VALUES (?, ?)",
                (f, c),
            )
    conn.commit()
    conn.close()


def regenerate_cache():
    """テーブルをドロップしてキャッシュを生成し直す"""
    files = list_files(recipe_dirs)
    conn = sqlite3.connect(cache_db)
    conn.execute("DROP TABLE recipes")
    conn.execute("CREATE TABLE IF NOT EXISTS recipes (fname TEXT, contents TEXT)")
    cur = conn.cursor()
    for f in files:
        if os.path.splitext(f)[1] == ".pdf":
            c = repr(extract_content(f))
            cur.execute("INSERT into recipes VALUES (?, ?)", (f, c))
    conn.commit()
    conn.close()


if __name__ == "__main__":
    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
    logger = logging.getLogger(__name__)
    handler = logging.StreamHandler()
    formatter = logging.Formatter("%(asctime)s %(name)-12s %(levelname)-8s %(message)s")
    handler.setFormatter(formatter)
    logger.addHandler(handler)
    logger.setLevel(logging.INFO)
    logger.propagate = False

    parser = argparse.ArgumentParser(
        description="ウェブクリップから料理レシピを検索し、PDFビューワで表示する",
        #    formatter_class=argparse.ArgumentDefaultsHelpFormatter,
    )
    parser.add_argument(
        "pattern",
        metavar="pattern(s)",
        type=str,
        nargs="*",
        help="pattern(s) for search. accept REGEX, C/Migemo",
    )
    parser.add_argument(
        "-c",
        "--contents",
        metavar="contents",
        action="store_const",
        const=1,
        default=0,
        help="Search keyword(s) in PDF contents as well as file names",
    )
    parser.add_argument(
        "-g",
        "--generate",
        metavar="generate",
        action="store_const",
        const=1,
        default=0,
        help="Generate cache with PDF contents for new file(s)",
    )
    parser.add_argument(
        "-r",
        "--recache",
        metavar="recache",
        action="store_const",
        const=1,
        default=0,
        help="Clear and Re-generate cache with PDF contents",
    )
    parser.add_argument(
        "-d",
        "--debug",
        metavar="debug",
        action="store_const",
        const=1,
        default=0,
        help="Print Debug information",
    )
    args = parser.parse_args()

    if args.debug == 1:
        logger.setLevel(logging.DEBUG)

    if args.generate:
        generate_cache()
        sys.exit()
    elif args.recache:
        regenerate_cache()
        sys.exit()
    elif not args.pattern:
        parser.print_help()
        sys.exit()

    patterns = args.pattern
    search_in_dirs(patterns, list_files(recipe_dirs), args.contents)

Windows11のTPM。

今年後半に発表されるというWindows11ですが、実行要件としてTPM(Trusted Platform Module)2.0が必須となるようです。

これはCPUに搭載された機能のようですが、別途ハードウェアモジュールとしても提供されるようです。うちではAsrockのマザーボードを使用しており、メインは Core i9-9900K を搭載した Asrock Z390 Extreme4 ですが、これはBIOSで対応しているようです。一方、サブとして使っているのは Core i7-4990K の Z97 Extreme4 で、こちらはTPM2.0に対応していません。

Windows11がサポートしているIntelのCPUはこちら、AMDのCPUはこちらにリストがあります。

これだと4990Kのサブ機は非対応ということになるのですが、AsrockからはハードウェアTPMモジュールが提供されているようです。チップ製造元はNuvotonやInfineonとなっています。こちらのものはマザーボード上のTPMヘッダーに装着して使用するようですが、Z97 Extreme4にもTPMヘッダーは実装されています。また、TPM-SPIというモジュールもあるようで、こちらは非常に小型です。SPIインタフェースというのが気になりますが、対応マザーボードリストも掲示されていません。さらには暗号化の輸出規制の関係か、販売される国が限定されているようです。

TPM-sモジュールについては、過去にAmazonで1500円程度で販売されていたこともあるようですから、市場に流れてくれば買いやすい値段になるかもしれません。現状では2~3万円しているようですから、様子見ですね。

さて、TPM2.0は9900Kではサポートしているということで、BIOSの設定を見ると、右の方に項目がありました。"Security"タブの一番下の項目が "Intel(R) Platform Trust Technology"で、デフォルトでは"Disabled"されています。

この状態では TPM.msc の画面は以下のようになっています。


これを"Enabled"にして再起動してみると、以下のように変わります。
使い方はわからないし、とりあえずユーザがいじるものではない、みたいな記述もあるのでとりあえずここまでにしておきます。

pyls_msがなくなった。

deopleteのソースをJediからLSPに変更する。で、Microsoft Python Language ServerをNeovimから使えるようにしてみたんだけれど、:call dein#update()してみたら、エラーが出てしまいました。:messageしてみると以下のような出力が。

[dein] Error occurred while executing hook: nvim-lspconfig
[dein] Vim(lua):E5108: Error executing lua [string ":lua"]:3: attempt to index field 'pyls_ms' (a nil value)
Lua が pyls_ms を読み込もうとしたんだけれど値が nil でした、ってことです。

え、もしや、と思って検索すると、https://github.com/neovim/nvim-lspconfig/issues/1073で、Python3で使っていると Error 134を出してクラッシュする、というようなことが書いてあります。

それに対して、pyls_msはpylance/pyrightに引き継がれていてMicrosoftはメンテナンスを放棄しているので、pyrightかpylspに切り替えることを推奨します、とのこと。
pyls_ms is borderline abandoned by microsoft (in favor of pylance/pyright), I would recommend switching to pyright or pylsp (`pyls is also abandoned) I'll probably remove pyls_ms shortly. It's not a bug in the built-in client (which this repo is not, this repo is just the basic settings we send to the language servers), it's likely a bug in pyls_ms that will never be fixed
ということで、pyls_ms.luaはすでに削除されてしまっていたのでした。実際、たしかに時々LSPによる補完が効かないことがあって、なんでかなぁと思ったり、NeovimはおろかChromeとか起動しているすべてのアプリを巻き込んでクラッシュしたりということもあったので、潮時なのかもしれません。ということで候補なのですが…。

pylspことPython LSP ServerはJediをエンジンとして利用しているので、せっかくJediを離れてpyls_msにしたのにまた戻ることになります。補完速度的にも若干遅い気がしますし。

pyrightはpyls_msと同じMicrosoftから提供されていますが、使用するためにはNode.jsを別途インストールする必要があるようです。

仕方ないので、Node.jsの現行推奨版である14.17.3-x64 LTSをインストールします。ただ、Chocolateyを含むビルドツールも必要に応じてインストールする、というオプションはチェックをしないでおきます。PythonもVisual Studio 2019もインストールしてあるし。

インストールが終了したら、新規にコマンドプロンプトを開いて動作を確認します。
C:\Users\kats$ node --version
v14.17.3

C:\Users\kats$ npm --version
6.14.13
動くようなのでpyrightをインストールします。
C:\Users\kats$ npm install -g pyright
C:\Users\kats\AppData\Roaming\npm\pyright -> C:\Users\kats\AppData\Roaming\npm\node_modules\pyright\index.js
C:\Users\kats\AppData\Roaming\npm\pyright-langserver -> C:\Users\kats\AppData\Roaming\npm\node_modules\pyright\langserver.index.js
+ pyright@1.1.158
added 1 package from 1 contributor in 1.015s
pyrightを起動してみると、
C:\Users\kats$ pyright
No configuration file found.
No pyproject.toml file found.
stubPath C:\Users\kats\typings is not a valid directory.
Assuming Python platform Windows
Searching for source files
Auto-excluding C:\Users\kats\.cache\dein\.cache\init.vim\.dein\test\test-files\python\with_virtualenv\env
Auto-excluding C:\Users\kats\.cache\dein\repos\github.com\dense-analysis\ale\test\test-files\python\with_virtualenv\env
Skipping recursive symlink "C:\Users\kats\AppData\Local\Application Data" -> "C:\Users\kats\AppData\Local"
Skipping broken link "C:\Users\kats\AppData\Local\ElevatedDiagnostics"
Auto-excluding C:\Users\kats\Projects\JupyterLab
Auto-excluding C:\Users\kats\work
Found 11346 source files
Emptying type cache to avoid heap overflow. Heap size used: 1537MB
Emptying type cache to avoid heap overflow. Heap size used: 1823MB
Emptying type cache to avoid heap overflow. Heap size used: 2108MB
Emptying type cache to avoid heap overflow. Heap size used: 2381MB
Emptying type cache to avoid heap overflow. Heap size used: 1538MB
Emptying type cache to avoid heap overflow. Heap size used: 1734MB
なにやらカレントディレクトリ以下のすべての*.pyファイルを検索してキャッシュしているようです。

それはさておき。

Neovim側では、これまで使用していたpyls_msの設定を削除して、pyrightの設定をdein.tomlに追加します。
nvim_lsp.pyright.setup{
}
ここに記入する設定は、CONFIG.md#pyrightに説明されています。が、基本的にはパスさえ通っていれば特に設定の必要はなさそうです。

dein.tomlを保存してからNeovimを起動し直すと、こころなしかpyls_msのときよりも起動が早いようです。遅延発火(lazy)はしていないので、これはNode.jsをインストールしてまで導入したかいがあったかもしれません。

日本語検索を「かな漢字変換」を通さずにやる。その3

日本語検索を「かな漢字変換」を通さずにやる。その2を受けて…。

検索文字列に "class" を指定したのに "分類" などが引っかかってくる(かつ "クラス" などが含まれない)と、「一体何でキミはここにいるのかね?」な状況になって困惑してしまいます。そこで、"ripgrep" や "The Platinum Searcher" の出力のように、マッチした文字列をエスケープシーケンスで強調するようにしてみました。

こんな感じです。
BRIGHT_RED = "\033[91m"
BRIGHT_GREEN = "\033[92m"
BRIGHT_YELLOW = "\033[93m"
BRIGHT_BLUE = "\033[94m"
BRIGHT_MAGENTA = "\033[95m"
BRIGHT_CYAN = "\033[96m"
BRIGHT_WHITE = "\033[97m"
DEFAULT = "\033[39m"

def search_in_lsr(pat, dirs):
    """search pat in ls-R files"""
    for target in dirs:
        logger.debug("finding in %s", target)
        try:
            with open(target + "/ls-R", "r", encoding="utf-8") as f:
                lines = f.readlines()

                for line in lines:
                    if res := pat.search(line):
                        line = pat.sub(BRIGHT_CYAN + res.group() + DEFAULT, line)
                        print(target + line, end="")
        except FileNotFoundError as e:
            print(e)
            print(f'run "mp4lsr.py {target}" to generate ls-R files.')
            sys.exit(1)
ファイル名のリストは、mktexlsrの作成する "ls-R" ファイルのように、「このディレクトリ以下のファイルリスト」という形で、いくつかのディレクトリに散らばっています。

例にしてあげるとこんな感じ。
M: -+- 自然科学 -+- 物理学
    |            +- 化学
    |            +- ・・・
    |            +- ls-R
    |
    +- 文化芸術 -+- 音楽 -+- クラシック
                 |        +- 雅楽
                 |        +- 民謡
                 |        +- ・・・
                 |        +- ls-R
                 +- 美術
                 +- ・・・
言ってみれば、ジャンルごとにそのディレクトリツリーの "ls-R" ファイルを作成するわけです。

引数のpatはCMigemoが出力してきた正規表現をre.compileした正規表現オブジェクト、dirsは "ls-R" が置いてあるディレクトリのリストです。
pat.search(line)Nullでなければ、マッチした文字列をすべてエスケープシーケンスで囲むように置換する、という処理をしています。

ところがこれをコマンドプロンプトでやると、指定したエスケープシーケンスが文字のママ出力されてしまい、一層困惑することになります。

そこでPython Windows10 において Console 出力に色を付けるを参考にコマンドプロンプトをVT互換のエスケープシーケンスを処理するように設定することで、一致した文字列を強調できるようになりました。Microsoftによる日本語の解説はコンソールの仮想ターミナル シーケンスにあります。

自画自賛なんですがめちゃくちゃ便利。

日本語検索を「かな漢字変換」を通さずにやる。その2

それではPythonからC/Migemoを使ってみます。

CMigemo.exeの出力をsubprocessで取り込んで使おうかと思ったのですが、出力形式がVimスタイルやEmacsスタイルはあっても、今ひとつPythonのreに丁度いいような形ではなくてうまく引っかかってこなかったので、なにかないかとぐぐってみるとcmigemo 0.1.6というパッケージがありました。ctypesを使ってmigemo.dllをロードするようです。

Githubのプロジェクトページはこちら。moozさんという方が作られたようです。

ということで早速これを
pip install python-cmigemoして、組み込んでみました。
import cmigemo

migemo_dict = "c:/Apps/bin/dict/base-dict"

def compile_pattern(S):
    """return compiled pattern"""
    logger.debug(S)
    migemo = cmigemo.Migemo(migemo_dict)
    ret = migemo.query(S)
    logger.debug("regex=%s", ret)
    return re.compile(ret, re.IGNORECASE)
なのですが、どうにもmigemo.dllをロードしてくれません。 ソースを追いかけてみると、
    def _load_libmigemo(self, lib_name="migemo"):
        import platform

        if platform.system() == u"Windows":
            libmigemo = windll.migemo
のところでエラーになっています。ということで、この部分を
    def _load_libmigemo(self, lib_name="migemo"):
        import platform

        if platform.system() == u"Windows":
            libmigemo = windll.LoadLibrary(find_library("migemo"))
として、find_library()に探させてみたら、migemo.dllをPATHの通ったところに置いておけば見つけてくれるようです。

これで、C/Migemoが出力してきた正規表現を検索パターンにしてre.compile()してやればいろいろとできそうです。

でもって結果ですが…。

もちろん辞書にもよると思いますが、思った以上に強力です。
"class" で検索をかけると、"Class" や "クラス" はもちろんのこと、"クラシック" とか "分類" などまでも引っかかってきます。検索時にre.IGNORECASEを指定してやると、"CLASS" も "class" も "ClaSS" も引っかかってくるし、日本語もちゃんと受け取って、その場合にはちゃんと日本語での検索結果が上がってきます。 これは思った以上に快適です。

Pythonのshutil.copyが異常に遅い件。

前から気付いていて、Pythonも3.9になったので改善しているかしら?と試してみましたがやっぱり未だに異常に遅いようです。
なぜかこれはWindowsプラットフォームだけのようですが。

1GBのファイルをSSDの2台のドライブの間でコピーする速度を計測してみました。
1 Subprocess copy to dir 0.3177815
2 shutil.copyfile 5.799549600000001
3 shutil.copy to dir 6.2538441
4 shutil.copy to file 5.642608300000001
5 shutil.copyfileobj (16MB) 9.0621711
6 shutil.copyfileobj (128MB) 20.3517843
7 shutil.copyfileobj (-1) 21.7088837
8 copyFile to file (default) 9.210321199999996
9 copyFile to file (16MB) 8.914450500000001
すべて1GBのファイルを別のSSDドライブに10回コピーしたのに要した時間です。圧倒的にsubprocessが早いです。 1のsubprocessは、
for i in range(10):
    subprocess.run('cmd /c copy "{0}" {1} > nul'.format(fname, dest_dir), check=False)
    
というコードです。
2はshutil.copyfile()、3はコピー先にファイルを、4はコピー先にディレクトリを指定したshutil.copy()です。
5、6、7はそれぞれ、buffer_sizeに16MB、128MB、-1を指定したshutil.copyfileobj()です。-1の場合にはファイルを一度に全部読み込むだけのバッファを確保する、という説明がありましたので、計測してみました。
8と9は、Faster Python File CopyにあるcopyFile()を使用してみました。処理手的にはshutil.copyfileobj()を使っていますので、5と同程度になります。
Linuxなどの場合にはここまでの差は出ないようで、ほとんど1と同じ時間だということですが、shutil.copy()は"cmd /c copy"の約20倍の時間がかかっています。本来ならば外部コマンドを呼び出すオーバーヘッドなどを考えると、subprocessのほうが遅いはずなのですが、ことWindows上での大きいファイルのコピーに関してはsubprocessに軍配が上がるようです。

追記(2022年6月26日):
前から気づいてたんですが、ここに書いてなかったので追記しておきます。
Changed in version 3.8: Platform-specific fast-copy syscalls may be used internally in order to copy the file more efficiently. See Platform-dependent efficient copy operations section.

https://docs.python.org/3/library/shutil.htmlに記載されてますが、Python 3.8からコピーが劇的に速くなっています。

日本語検索を「かな漢字変換」を通さずにやる。

ローカルなライブラリの検索で、日本語ファイル名が数千になってきているのです。
そうなるとあいうえお順で(というかエクスプローラーの一覧から)目視で探すのは限界だし、ディレクトリツリーを順番に見ていくのも大変なので、mktexlsrのようにファイル名のデータベースを作っておいてそのデータベースから探すのが早そう、ということでPythonでls-R作成スクリプトと検索スクリプトを作りました。

検索文字列はreモジュールで正規表現検索するので、一部だけでも適当に検索してくれます。ものすごく便利で自画自賛なんですが、検索したいファイル名(の一部)を入力するときにいちいち「かな漢字変換」をオン・オフするのは面倒だな、と思って、そういえばローマ字入力で日本語文書を検索できるヤツがあったな、と思い出し……。

NamazuじゃなくてKakasiじゃなくてMeCabじゃなくてなんだっけ……とグーグル先生にお尋ねして。

名前が思い出せなかったので、そういえばVimを2chブラウザにするヤツを作っていた人がいて、その人がなんかやってたような、なんだっけ、としばらく考えて検索して、"Chalice"をようやく思い出して。そこから "Migemo" にやっとたどり着きました。
Migemo自体はRubyで記述されているようで、かつすでにメンテされていないようです。もったいない。でも自分はRubyはわからないので、KaoriYaのKoronさんがCに移植されたC/Migemoを見てみようか、と。

Cで実装されてDLLにされているなら、Pythonから呼び出すのもできそうだし、Neovimにも組み込むことができそうです。まあ、後者は追々というとこで、とりあえずPythonから呼び出してローマ字で日本語検索!をやれれば楽ができるかなということで、ぼちぼちいじってみようと思います。

C/MigemoはGitHubで公開されていましたので、これをクローンして作業開始です。

README_j.txtには以下のようにVisualC++でのビルド方法が書かれています。
  (Windows + VisualC++)
  次のコマンドでRelease/内にmigemo.dllとcmigemo.exeが作成されます。
    > nmake msvc
  必要な外部プログラム、ネットワーク接続が揃っていれば
    > nmake msvc-dict
  で辞書ファイルをビルドできます。migemo.dswをVC++6.0で開き、ビルドする方法も
  あります。以上が終了すれば次のコマンドでテストプログラムが動作します。
    > .\build\cmigemo -d dict/migemo-dict
現在の環境はVisualC++を含んだVisualStudio 2019なので、そのままビルドと思いきややっぱりエラーが出ました。

幸い\compile\vs2003.slnファイルがあったので、それをVS2019で開くとそのままソリューションを変換してくれて一応プロジェクトは開けました。

そのままF6キーでビルドすると、"afxres.h"がインクルードできない、というエラーが出てビルドできません。
ぐぐってみると、"winres.h"に書き換えるといいよ、ということだったので仰せに従い無事ビルド終了。ついでにプロジェクトにx64ビルドも追加してこちらもビルド。一応どちらもDebugビルドにしておきました。


お次は辞書ファイルの生成ですが、Makefileを追いかけるとnkfではなくiconvやqkcを使っていました。qkcは今どうしてるんだろう…。

それはさておき。

現状ではnkfを使っているので、nmakeと同じことをコマンドプロンプトからそのままやりました。
dictディレクトリで、
$ for %i in (base-dict,SKK-JISYO.L,*.dat) do (
More? nkf -w %i > utf-8.d\%i
More? )
$ for %i in (base-dict,SKK-JISYO.L,*.dat) do (
More? nkf -e %i > euc-jp.d\%i
More? )
で辞書の文字コード変換は終了。


早速テストしてみます。cmigemoディレクトリに移動して、
$ compile\vs2003\x64\Debug\CMigemo.exe
migemo_open("./dict/migemo-dict")=000002AB8059C640
clock()=0.128000
QUERY: aho
PATTERN: (アホ|アホ|信天翁|阿[房呆]|あほ|aho|aho)
QUERY: clean
PATTERN: (clean|ク(レン(ザー|ジング)|リ(ンナップ|ー(ン|ナー|ニング)))|c(ェア[ンノネヌニナ]|ェア[ンノネヌニナ]|ぇあ[んのねぬにな]|lean))
QUERY: nihongo
PATTERN: (ニホンゴ|ニホンゴ|日本(語|極道史|合成ゴム)|にほんご|nihongo|nihongo)
QUERY: 日本語
PATTERN: 日本語
QUERY:
QUERY:
おお、返り値がそのまま正規表現として利用できるようなパターンになっています。なにげに信天翁(アホウドリ)が混じっているのが点数高いです。そして、かな漢字変換で日本語を食わせるとそのまま返ってきます。

これは結構使えるのでは……。 辞書の指定とかエンコードとか出力形式とかはどうなんだろうと見てみると、
$ compile\vs2003\x64\Debug\CMigemo.exe -h
cmigemo - C/Migemo Library 1.3 Driver

USAGE: compile\vs2003\x64\Debug\CMigemo.exe [OPTIONS]

OPTIONS:
  -d --dict <dict>      Use a file <dict> for dictionary.
  -s --subdict <dict>   Sub dictionary files. (MAX 8 times)
  -q --quiet            Show no message except results.
  -v --vim              Use vim style regexp.
  -e --emacs            Use emacs style regexp.
  -n --nonewline        Don't use newline match.
  -w --word <word>      Expand a <word> and soon exit.
  -h --help             Show this message.
また、辞書を指定するとそのエンコードで結果が返ってくるようです。
$ compile\vs2003\x64\Debug\CMigemo.exe -w aho
(アホ|アホ|信天翁|阿[房呆]|あほ|aho|aho)

$ compile\vs2003\x64\Debug\CMigemo.exe -d dict\utf-8.d\base-dict -w aho
(繧「繝斈菫。螟ゥ鄙-髦ソ[謌ソ蜻・|縺ゅ⊇|・・ス茨ス楯aho)

$ compile\vs2003\x64\Debug\CMigemo.exe -d dict\utf-8.d\base-dict -w aho | nkf -s
(アホ|信天翁|阿[房呆]|あほ|aho|aho)

$ compile\vs2003\x64\Debug\CMigemo.exe -d dict\euc-jp.d\base-dict -w aho
(・「・ロ|ソョナキイァ|ー、[ヒシハ|、「、ロ|」皀陬・aho)

$ compile\vs2003\x64\Debug\CMigemo.exe -d dict\euc-jp.d\base-dict -w aho | nkf -s
(アホ|信天翁|阿[房呆]|あほ|aho|aho)
受け取り側に都合のよいエンコードの辞書を使えば良さそうです。

pipじゃなくてpacmanを使う。

ArchLinux系ではパッケージマネージャのpacmanを使いますが、Pythonの外部モジュールをインストールするときにはpipを使うのかpacmanを使うのか、どっちがいいのか、という質問がいろいろなフォーラムでたびたび出てきます。 

回答はいつも明確で、システムワイドで使うならpacmanを使え、pipを使うなら'--user' オプションを必ず指定しろ、venvなどの仮想環境で使うならpipを使え、です。

ところがそういう方針でやっていても、ついうっかりpip使っちゃったりすることもあります。  ということで、あるモジュールをインストールしたのがpipなのかpacmanなのかを表示するための1ライナーです。

$ for i in `pip list | tail -n +3 | awk '{print $1}'`; do pacman -Qs $i ; done
local/python-appdirs 1.4.4-3
    A small Python module for determining appropriate platform-specific dirs, e.g. a "user data dir".
local/python-black 21.6b0-3
    Uncompromising Python code formatter
local/python-cachecontrol 0.12.6-3
    httplib2 caching for requests

出力で先頭に'local'が付いてるのは、pacmanでローカルにインストールされてますよ、ということです。 めでたしめでたし。

Neovim+vimtex+TeXLive 2021+SumatraPDFでTeX環境を作ってみる。

NeovimでのPythonの環境をLSPに変更したので、vimtexが使っている補完ソースもLSPにしてみます。

TeX用Language Serverのセットアップ

LSはtexlab/Relaseからバイナリをダウンロードします。
実行形式ファイルが1つだけなので、インストールはこのtexlab.exec:/Apps/languageServerにコピーするだけです。

次にdein.tomlのnvim-lspconfigのところにLSに関する設定を追加します。
[[plugins]]
# A collection of common configurations for Neovim's built-in language server client.
repo = 'neovim/nvim-lspconfig'
hook_add = '''
set runtimepath+=~/.cache/dein/repos/github.com/neovim/nvim-lspconfig/lua
lua << EOF
local nvim_lsp = require('lspconfig')

nvim_lsp.pyls_ms.setup{
    cmd = { "dotnet", "exec", "c:/Apps/languageServer/Microsoft.Python.LanguageServer.dll" },
	init_options = {
	  analysisUpdates = true,
	  asyncStartup = true,
	}
}
nvim_lsp.texlab.setup{
    cmd = { "c:/Apps/languageServer/texlab.exe" },
}
EOF
'''
一応これだけでLSPを利用した補完はできるようになります。 ただし、ALEを使っている場合にはALEとLSPで二重にチェックされたりするらしいので、問題がある場合にはその部分で変更が必要です。ALEの設定部分で let g:ale_disable_lsp = 1と設定するといいらしいです。

vimtexとSumatraPDFの連携

vimtexでコンパイルまでできるんだから、逆順検索もちゃんとできるようにします。

まず、逆順検索(inverse search)はRPCを使ってNeovimとSumatraPDFがやりとりします。具体的には、SumatraPDF側でクリックされた行情報をRPCでNeovimに送ります。Neovim側はRPCで受け取ったコマンドを実行して、PDFでのクリックされた行のソースにジャンプします。

ということでNeovimを外部から、コントロールするためのneovim-remoteが必要になります。
$ pip install neovim-remote
するとnvrというコマンドが利用できるようになります。

nvrでは予めサーバの場所を指定しておく必要があります。NeovimのRPCの場合には、\\.\pipe\nvim-1234-0というようにパイプが作成されるので、それを利用する必要がありますが、パイプ名は毎回異なります。
一方、SumatraPDFでは逆順検索コマンドで利用するためにサーバの場所を知りたいのですが、そのためにはNeovimと通信できないとわかりません。毎回手動で設定するのも大変ですし、現実的ではありません。

そこで、起動時にNeovimが作成するRPCサーバとは別に、SumatraPDF専用のRPCサーバを起動してそこと通信させればうまくいきます。

Neovim側で
if has('win32')
	call serverstart('\\.\pipe\nvim-vimtex-1234')
endif

let g:vimtex_view_general_options
			\ = '-reuse-instance'
			\ . ' -forward-search @tex @line @pdf'
			\ . ' -inverse-search "nvr --servername ' . '\\.\nvim-vimtex-1234'
			\ . ' --remote-send \"\%lG\""'
などとvimtexの設定時にRPCサーバを起動し、このパイプ名をSumatraPDFの逆順検索コマンドに指定してやればよいのです。
nvr --servername \\.\pipe\nvim-vimtex-1234 --remote-send "%lG"
これでPDFで行をクリックするとNeovim側で対応する行にジャンプします。 ・・・でも順方向がちゃんと動かない・・・。

deopleteのソースをJediからLSPに変更する。

ついでにvimtexまわりの変更も行います。

JediからLanguage Serverへ

約2年前にNeovim+vimtex+TeXLive2018でTeX環境を作ってみる。というエントリを書いたのですが、ここへきて Neovim の 0.5.0 がぼちぼち(予定では6月15日)にリリースされるということで、Nightly ビルドを持ってきて使っています。

もともとは Python プログラミングの補完などのためにdeoplete-jediを使っていましたが、jediよりも速いという話もあってLSPを利用した入力サポートへの変更を行いました。

Python用のLanguage Server(LS)はいくつかありますが、VS Codeと併用するならばVS Codeで使用されている Pylance のベースとなっているMicrosoft Python Language Serverが、一番違和感なく使えるのではないかということでこれを候補にしました。
他にもpalantir/python-language-serverhttps://github.com/python-lsp/python-lsp-serverにありますが、こちらはエンジン?にJediを利用するということで、deoplete-jediで使っているのと変わらないためにパスしています。また、NeovimをVS Codeのような感じにするというCoC(Conquer of Completion)というフレームワークがあり、そのプラグインで使うという方法も調べてみましたが、こちらはNode.jsで動くということで、Node.jsを使う予定はないのでこれもパスしました。

結果、構成としては以下のようになります。
  • Neovim + deoplete を利用する
  • deoplete からLSを利用するため、deoplete-lsp プラグインを利用する
  • LSPとしてはMicrosoft Python Language Server(pyls_ms)を使用する
  • pyls_msの設定を簡単に行うため、nvim-lspconfigを使用する

pyls_msをビルドする

Microsoft Python Language ServerはC#で記述されているので、.NETのビルド環境が必要です。うちではVisual Studio 2019がインストールされているので、コマンドプロンプトから以下のコマンドを叩けばビルドできます。
$ git clone https://github.com/Microsoft/python-language-server.git
$ cd python-language-server/src/LanguageServer/Impl
$ dotnet build -c Release
ビルドが正常に終了したらpython-language-server/output/binディレクトリにある全ファイルをc:/Apps/languageServerディレクトリにコピーします。

deoplete-lspを導入する

dein.tomlを編集し、deoplete-jediを削除したあとで以下の記述を追加します。
[[plugins]]
# LSP Completion source for deoplete
repo = 'deoplete-plugins/deoplete-lsp'
hook_add = '''
let g:deoplete#lsp#handler_enabled=1
let g:deoplete#lsp#use_icons_for_candidates=0

'''

nvim-lspconfigを導入する

続けて以下の記述を同じくdein.tomlに追加します。
[[plugins]]
# A collection of common configurations for Neovim's built-in language server client.
repo = 'neovim/nvim-lspconfig'
hook_add = '''
set runtimepath+=~/.cache/dein/repos/github.com/neovim/nvim-lspconfig/lua
lua << EOF
local nvim_lsp = require('lspconfig')

nvim_lsp.pyls_ms.setup{
    cmd = { "dotnet", "exec", "c:/Apps/languageServer/Microsoft.Python.LanguageServer.dll" },
	init_options = {
	  analysisUpdates = true,
	  asyncStartup = true,
	}
}
EOF
'''
一応このとき、dein_lazy.tomlでのdeopleteの設定は以下のようになります。
[[plugins]]
# Dark powered asynchronous completion framework for Neovim / Vim8
# require: python3
repo = 'Shougo/deoplete.nvim'
depends = 'context_filetype.vim'
on_i = 1
hook_post_source = '''
  call deoplete#enable()
  source $XDG_CONFIG_HOME/nvim/rc/deoplete.rc.vim
'''

起動する

設定が終わったらインストールとキャッシュの作り直しをしておきます。
:UpdateRemotePlugins
:call dein#update()
:call dein#recache_runtimepath()
Neovimを再起動してPythonファイルを編集し、補完候補の右側に[LSP]と出てくればちゃんと動いています。

Vimの補完プラグインをインストール。その4

Vimの補完プラグインをインストール。その3 で、 ddc-tabnine が使えそうです、などと書いたのですが、早速やってみました。 まず、tabnineのバイナリを用意しないといけません。がどうにもTabNineのサイトがわかりにくいので、 tabnine-nvim にあるダ...