テクノなまこ

科学の力

PXMLT5PEでTVTestを見れるようにする

Windows10でTVtestをTS抜き視聴環境に使っていたが、Windows11へのアップデートで一部のBonDriver以外は見れなくなってしまい、しばらくぶりに使うと「BonDriverの初期化に失敗しました」と表示されTVTestで全く視聴ができなくなってしまった。Geminiと相談し、最初からやり直してTVTestを視聴できる環境を作ったが、いくつか躓いたため過程を示す。

参考

TVTestを入手する

  • https://github.com/tsukumijima/DTV-Buildsの「ダウンロード」の「TVTest-0.10.0-251212 … TVTest-0.10.0-251212.zip」等の箇所からダウンロードする。
  • zipファイルを展開し、cert-install.jseをダブルクリックして証明書をインストールする。失敗する場合はcert-uninstall.jseを実行した後cert-install.jseを実行する。

BonDriverを入手する

安定性を高めるため、外部カードリーダーを用い非公式のBonDriverを使用する。

  • 記事https://blog.tsukumijima.net/article/ts-dtv-soft/の「px4_drv for WinUSB (2025/01/11) … px4_drv_winusb-250111.zip (ダウンロード)」の箇所から非公式BonDriverダウンロードする。
  • zipファイルを展開する
  • DriverフォルダのPX-MLT5PE.infを右クリック、「インストール」を選択しドライバをインストールする

古いドライバを削除する

  • 検索欄に「デバイスマネージャー」と入力してデバイスマネージャーを開く。
  • 「サウンド、ビデオ、およびゲーム コントローラー」にMLT5PEがあるので*「デバイスのアンインストール」を選択する。このとき、「このデバイスのドライバーを削除しようとしました」というチェックを入れてアンインストールを実行する。
  • デバイスマネージャー上部のメニューから「操作」>「ハードウェア変更のスキャン」をクリックする

TVTestを起動する

  • TVtest.exeをダブルクリックして起動する。ただしBonDriverにBonDriver_PX-MLT.dllを選択、MPEG-2デコーダを[TVTest DTV Video Decoder] に変更する、映像レンダラを[EVR] に変更する。

付録 EDCBを導入し、ブラウザで視聴できるようにする

  • https://github.com/tsukumijima/DTV-Buildsまたはhttps://blog.tsukumijima.net/article/ts-dtv-soft/#toc21でビルド済みEDCBをダウンロードする。zipファイルのダウンロードは失敗するが、ダウンロードされた一時ファイルの拡張子をエクスプローラーで.zipに書き換えると解凍、展開できる。
  • https://blog.tsukumijima.net/article/new-bs-channel-settings/#toc10 の手順に従いチャンネルスキャン等EDCB初期設定を行う。
  • EpgTimerの設定>録画動作>録画、EPG取得終了後のデフォルト動作を「何もしない」にする。こうしないと普段使いのPCで録画を行う場合録画終了と同時にPCがスリープし不便。
  • EpgTimerの設定>外部アプリケーション>TVTest連系>視聴に使用するBonDriverにBonDriverを追加。NetworkTVモードにチェック。これでブラウザと通信可能になる。
  • EpgCapBonの設定>ネットワーク設定>TCP送信先に0.0.0.1:0-29(SrvPipe)を追加
  • ブラウザでlocalhost:5510/EMWUI>リモート視聴にアクセスし、設定>画質>720p/h264/ffmpeg(一番上のもの)を選択する。これはCPUエンコードであり多くのPCで実行しやすい。

付録 Konomi TVを導入する

WebUIではKonomi TVより良いものはないと思う

https://github.com/tsukumijima/KonomiTVの指示に従って導入する

wsl上のUbuntuのdiskpartのexpandに失敗しvhdxの読み取り以外でのマウントができなくなったが、qemu-imgで復旧した

状況

windows11のwsl2で、4TBのSSDにdockerでUbuntuを動かし、seafile, immichをホストしている。容量不足で落ちてしまったので、diskpartでexpandを行ったがエラーが出た。diskpartを終了した後も、以下のエラーを出してUbuntuが起動しなくなった。

(base) PS C:\WINDOWS\system32> wsl -d Ubuntu

ディスク 'I:\WSL\Ubuntu\ext4.vhdx' を WSL2 にアタッチできませんでした: ファイルまたはディレクトリが壊れているため、読み 取ることができません。

エラー コード: Wsl/Service/CreateInstance/MountDisk/HCS/0x80070570

前提

diskpartを用いてvhdxファイルをディスクをとしてアタッチしようとすると、readonlyではアタッチできるが書き込み可能な状態ではアタッチできない。そのため、おそらくexpandが何らかの理由で途中で終了し、vhdxのメタデータと中身の整合性が取れず、windowsの安全機構によりWindowsのディスクapi経由では書き込みが不可能になっていると推定した。Debianなど他のOSをwslでインストールしても、wslがWindowsのapiでディスクを認識するため、vhdxをアタッチしても修復ツールを用いることができなかった。(鶏と卵状態) Geminiと問答を繰り返しいくつか解決法を探ったが最終的に以下の方法で解決した。

解決策

仮想マシンを用いてwin apiを迂回する。以下管理者権限power shell上の操作

QEMUでの操作

  • QEMU for Windows (Weilnetz)の最新版をダウンロードする。https://qemu.weilnetz.de/w64/にアクセスし、qemu-w64-setup-20260422.exeのようなファイルをダウンロードする。 インストーラーを実行し、qemuをインストールする。

  • qemuがインストールされたフォルダに移動し、VHDXファイルの修復を試みる。

cd "C:\Program Files\qemu"
.\qemu-img.exe check -r all "I:\WSL\Ubuntu\ext4.vhdx"
  • するとこのようなエラーが出た
(base) PS C:\WINDOWS\system32> wsl --shutdown
(base) PS C:\WINDOWS\system32> cd "C:\Program Files\qemu"
(base) PS C:\Program Files\qemu> .\qemu-img.exe check -r all "I:\WSL\Ubuntu\ext4.vhdx"
C:\Program Files\qemu\qemu-img.exe: VHDX BAT entry 101640 start offset 3785442197504 points after end of file (3784165031936). Image has probably been truncated.
C:\Program Files\qemu\qemu-img.exe: VHDX BAT entry 101641 start offset 3785443246080 points after end of file (3784165031936). Image has probably been truncated.
C:\Program Files\qemu\qemu-img.exe: VHDX BAT entry 101642 start offset 3785444294656 points after end of file (3784165031936). Image has probably been truncated.
C:\Program Files\qemu\qemu-img.exe: VHDX BAT entry 101804 start offset 3784883306496 points after end of file (3784165031936). Image has probably been truncated.
C:\Program Files\qemu\qemu-img.exe: VHDX BAT entry 101805 start offset 3784884355072 points after end of file (3784165031936). Image has probably been truncated.

5 errors were found on the image.
Data may be corrupted, or further writes to the image may corrupt it.
(base) PS C:\Program Files\qemu>

ファイルの末尾の長さがインデックスで指定されているより短く、エラーが出ている。3785444294656よりファイルが長ければいいので、fsutilを用いてファイル末尾に0を足す。

fsutil file seteof "I:\WSL\Ubuntu\ext4.vhdx" 3786000000000

再度QEMUでチェックする。

.\qemu-img.exe check -r all "I:\WSL\Ubuntu\ext4.vhdx"

以下のように成功する。

ファイル I:\WSL\Ubuntu\ext4.vhdx の eof が設定されました

書き込み可能な状態でマウントする

wsl --mount "I:\WSL\Ubuntu\ext4.vhdx" --vhd --bare

あらかじめインストールしておいたDebianに入る。

wsl -d Debian

ディスクを確認する。

lsblk

このように出てくる。修復したいのは4TBあるsddなので

NAME MAJ:MIN RM   SIZE RO TYPE MOUNTPOINTS
sda    8:0    0 388.6M  1 disk
sdb    8:16   0   186M  1 disk
sdc    8:32   0     8G  0 disk [SWAP]
sdd    8:48   0   3.9T  0 disk
sde    8:64   0     1T  0 disk /run/user

e2fsckとresize2fsでメタデータを修復する。

sudo e2fsck -fy /dev/sdd
sudo resize2fs /dev/sdd

ctrl+DかexitコマンドでDebianを抜け、アンマウントとUbuntuの起動を行う。

wsl --unmount "I:\WSL\Ubuntu\ext4.vhdx"
wsl --shutdown
wsl -d Ubuntu

瞬間日記のメールエクスポート形式のテキストをobsidian向けmarkdownに変換するpythonスクリプト例

仕様

  • ファイル名はISO形式のyyyy-mm-dd
  • 作成日時をファイル作成日時と更新日時に設定
  • すべてにタグ#diaryをつける
  • 前後の日へのリンクを張る
import re
import os
import ctypes
from datetime import datetime, timedelta
from pathlib import Path

def parse_diary_entries(text):
    """日記テキストを日付ごとに分割してパース"""
    # 日付パターン
    date_pattern = r'-\s+(\w+),\s+(\w+)\s+(\d+),\s+(\d+)\s+(\d+):(\d+)'
    
    # 月名を数字に変換
    months = {
        'January': 1, 'February': 2, 'March': 3, 'April': 4,
        'May': 5, 'June': 6, 'July': 7, 'August': 8,
        'September': 9, 'October': 10, 'November': 11, 'December': 12
    }
    
    entries = []
    
    # すべての日付位置を見つける
    matches = list(re.finditer(date_pattern, text))
    
    for i, match in enumerate(matches):
        day_of_week, month, day, year, hour, minute = match.groups()
        
        month_num = months.get(month)
        if not month_num:
            continue
        
        # 日付オブジェクトを作成
        date = datetime(int(year), month_num, int(day), int(hour), int(minute))
        
        # 本文の開始位置(日付行の終わり)
        content_start = match.end()
        
        # 本文の終了位置(次の日付の開始、または文末)
        if i + 1 < len(matches):
            content_end = matches[i + 1].start()
        else:
            content_end = len(text)
        
        # 本文を抽出
        content = text[content_start:content_end].strip()
        
        entries.append({
            'date': date,
            'day_of_week': day_of_week,
            'time': f"{hour}:{minute}",
            'content': content
        })
    
    return entries

def convert_to_markdown(entry_data):
    """エントリデータをMarkdown形式に変換"""
    date_iso = entry_data['date'].strftime('%Y-%m-%d')
    
    # 前日と翌日の日付を計算
    prev_date = (entry_data['date'] - timedelta(days=1)).strftime('%Y-%m-%d')
    next_date = (entry_data['date'] + timedelta(days=1)).strftime('%Y-%m-%d')
    
    # Markdownファイルの内容を生成
    markdown = ""
    
    # 前後の日付へのリンク
    markdown += f"[[{prev_date}]] | [[{next_date}]]\n\n"
    
    # 本文の改行を保持(Markdownでは行末に2つのスペースで改行)
    content_lines = entry_data['content'].split('\n')
    for line in content_lines:
        if line.strip():
            markdown += line + "  \n"
        else:
            markdown += "\n"
    
    # タグを追加
    markdown += "\n#diary\n"
    
    return date_iso, markdown

def set_file_times_windows(filepath, timestamp):
    """Windowsでファイルの作成日時、更新日時、アクセス日時を設定"""
    # Windows API の定義
    kernel32 = ctypes.windll.kernel32
    
    # 定数
    GENERIC_WRITE = 0x40000000
    FILE_SHARE_READ = 0x00000001
    FILE_SHARE_WRITE = 0x00000002
    OPEN_EXISTING = 3
    FILE_ATTRIBUTE_NORMAL = 0x80
    
    # ファイルハンドルを開く
    handle = kernel32.CreateFileW(
        str(filepath),
        GENERIC_WRITE,
        FILE_SHARE_READ | FILE_SHARE_WRITE,
        None,
        OPEN_EXISTING,
        FILE_ATTRIBUTE_NORMAL,
        None
    )
    
    if handle == -1:
        print(f"ファイルを開けませんでした: {filepath}")
        return
    
    try:
        # datetimeをWindowsのFILETIME形式に変換
        # FILETIMEは1601年1月1日からの100ナノ秒単位
        unix_epoch = datetime(1970, 1, 1)
        windows_epoch = datetime(1601, 1, 1)
        
        # Unixエポックからの秒数を取得
        delta = timestamp - windows_epoch
        # 100ナノ秒単位に変換
        filetime = int(delta.total_seconds() * 10000000)
        
        # FILETIME構造体を作成
        class FILETIME(ctypes.Structure):
            _fields_ = [
                ("dwLowDateTime", ctypes.c_uint32),
                ("dwHighDateTime", ctypes.c_uint32)
            ]
        
        ft = FILETIME()
        ft.dwLowDateTime = filetime & 0xFFFFFFFF
        ft.dwHighDateTime = filetime >> 32
        
        # ファイル時刻を設定(作成日時、アクセス日時、更新日時)
        kernel32.SetFileTime(
            handle,
            ctypes.byref(ft),  # 作成日時
            ctypes.byref(ft),  # アクセス日時
            ctypes.byref(ft)   # 更新日時
        )
    finally:
        kernel32.CloseHandle(handle)

def process_diary(input_text, output_dir='diary'):
    """日記テキストを処理してファイルに出力"""
    # 出力ディレクトリを作成
    Path(output_dir).mkdir(exist_ok=True)
    
    # エントリをパース
    entries = parse_diary_entries(input_text)
    
    # 日付順にソート
    entries.sort(key=lambda x: x['date'])
    
    for entry_data in entries:
        # Markdownに変換
        filename, markdown_content = convert_to_markdown(entry_data)
        
        # ファイルに書き出し
        output_path = Path(output_dir) / f"{filename}.md"
        with open(output_path, 'w', encoding='utf-8') as f:
            f.write(markdown_content)
        
        # Windowsでファイルのタイムスタンプを設定
        set_file_times_windows(output_path, entry_data['date'])
        
        print(f"作成: {output_path}")

# メイン処理
if __name__ == "__main__":
    # diary.txtを読み込み
    try:
        with open('diary.txt', 'r', encoding='utf-8') as f:
            diary_text = f.read()
        
        print("diary.txtを読み込みました")
        print("変換を開始します...\n")
        
        # 変換実行
        process_diary(diary_text)
        
        print("\n変換が完了しました!")
        
    except FileNotFoundError:
        print("エラー: diary.txtが見つかりません")
        print("スクリプトと同じディレクトリにdiary.txtを配置してください")
    except Exception as e:
        print(f"エラーが発生しました: {e}")

google keepのエクスポート形式のhtmlをobsidian形式のmarkdownに変換するpythonスクリプト例

使い方

  • python html2md.py example.htmlのようにコマンドを打つとexample.meとなって変換される ディレクトリ内のすべてのhtmlファイルを変換したい場合は
  • for %i in (*.html) do python html_to_obsidian_md.py "%i"(コマンドプロンプト)
  • Get-ChildItem *.html | ForEach-Object { python html_to_obsidian_md.py $_.Name }(Powershell)とコマンドを打つと良い
  • 変換先のmdファイルが既にある場合、-fオプションをつけてpython html2md.py example.html -fとすることで上書きできる

仕様

  • タイトルは削除 (e.g.<div class="title">title</div> ->)
  • imgタグはmarkdown画像埋め込みに変換 (e.g.<li><img alt="" src="image.png" /></li> -> ![image.png](image.png))
  • タグはハッシュタグに変換 (e.g.<span class="label-name">namako</span> -> #namako)
  • 作成日時をファイルの作成日時と更新日時に設定
  • ピン止め、アーカイブされたノートはそれぞれpinned, archivedのタグを付加 (e.g.<span class="pinned" title="Note pinned"> -> #pinned, <span class="archived" title="Note archived"> -> #archived)

スクリプト例

#!/usr/bin/env python3
"""
Google Keep HTML エクスポートを Obsidian Markdown に変換するスクリプト

使い方:
    python html_to_obsidian_md.py input.html [output.md]
    
    output.mdを指定しない場合は、入力ファイル名から自動生成されます
"""
#!/usr/bin/env python3
"""
Google Keep HTML エクスポートを Obsidian Markdown に変換するスクリプト

使い方:
    python html_to_obsidian_md.py input.html [output.md]
    
    output.mdを指定しない場合は、入力ファイル名から自動生成されます
"""

import sys
import re
from pathlib import Path
from html.parser import HTMLParser
from html import unescape
import argparse


class KeepNoteParser(HTMLParser):
    """Google Keep ノートのHTMLをパースするクラス"""
    
    def __init__(self):
        super().__init__()
        self.in_heading = False
        self.in_title = False
        self.in_content = False
        self.in_chips = False
        self.in_chip = False
        self.in_attachments = False
        self.in_meta_icons = False
        
        self.heading = ""
        self.title = ""
        self.content = ""
        self.labels = []
        self.weblinks = []
        self.attachments = []
        self.is_archived = False
        self.is_pinned = False
        self.datetime = ""
        
        self.current_chip_text = ""
        self.current_chip_is_weblink = False
        self.current_chip_is_label = False
        self.current_chip_url = ""
    
    def handle_starttag(self, tag, attrs):
        attrs_dict = dict(attrs)
        
        if tag == 'div':
            if attrs_dict.get('class') == 'heading':
                self.in_heading = True
            elif attrs_dict.get('class') == 'title':
                self.in_title = True
            elif attrs_dict.get('class') == 'content':
                self.in_content = True
            elif attrs_dict.get('class') == 'chips':
                self.in_chips = True
            elif attrs_dict.get('class') == 'attachments':
                self.in_attachments = True
            elif attrs_dict.get('class') == 'meta-icons':
                self.in_meta_icons = True
        
        elif tag == 'span':
            class_attr = attrs_dict.get('class', '')
            title_attr = attrs_dict.get('title', '')
            
            # アーカイブとピン留めの検出
            if 'archived' in class_attr and 'Note archived' in title_attr:
                self.is_archived = True
            elif 'pinned' in class_attr and 'Note pinned' in title_attr:
                self.is_pinned = True
            
            # チップの処理
            if self.in_chips and 'chip' in class_attr:
                self.in_chip = True
                self.current_chip_text = ""
                self.current_chip_url = ""
                
                if 'WEBLINK' in class_attr:
                    self.current_chip_is_weblink = True
                    self.current_chip_is_label = False
                elif 'label' in class_attr:
                    self.current_chip_is_label = True
                    self.current_chip_is_weblink = False
        
        elif tag == 'a' and self.in_chip:
            self.current_chip_url = attrs_dict.get('href', '')
        
        elif tag == 'br' and self.in_content:
            self.content += '\n'
        
        elif tag == 'img' and self.in_attachments:
            src = attrs_dict.get('src', '')
            if src:
                self.attachments.append(src)
    
    def handle_endtag(self, tag):
        if tag == 'div':
            if self.in_meta_icons:
                self.in_meta_icons = False
            elif self.in_title:
                self.in_title = False
            elif self.in_content:
                self.in_content = False
            elif self.in_chips:
                self.in_chips = False
            elif self.in_attachments:
                self.in_attachments = False
            elif self.in_heading:
                self.in_heading = False
        
        elif tag == 'span' and self.in_chip:
            if self.current_chip_is_weblink and self.current_chip_url:
                self.weblinks.append(self.current_chip_url)
            elif self.current_chip_is_label and self.current_chip_text:
                self.labels.append(self.current_chip_text.strip())
            
            self.in_chip = False
            self.current_chip_is_weblink = False
            self.current_chip_is_label = False
    
    def handle_data(self, data):
        if self.in_heading and not self.in_title and not self.in_meta_icons:
            # 日付時刻を抽出(&#8239;はnarrow no-break space = U+202F)
            # 例: Oct 28, 2025, 11:16:42 PM
            date_pattern = '(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)' + r'\s+\d+,\s+\d{4},\s+\d+:\d+:\d+[\s\u202F]+(?:AM|PM)'
            match = re.search(date_pattern, data)
            if match:
                self.datetime = match.group()
            self.heading += data
        elif self.in_title:
            self.title += data
        elif self.in_content:
            self.content += data
        elif self.in_chip:
            self.current_chip_text += data


def set_file_times(filepath: Path, timestamp: float) -> bool:
    """
    ファイルの作成日時と更新日時を設定
    Windows、Linux/Mac の両方に対応
    
    Args:
        filepath: 対象ファイルのパス
        timestamp: Unix timestamp
    
    Returns:
        成功したらTrue、失敗したらFalse
    """
    import platform
    import os
    
    try:
        # Unix系(Linux, Mac)の場合
        if platform.system() != 'Windows':
            # 更新日時とアクセス日時を設定
            os.utime(filepath, (timestamp, timestamp))
            return True
        
        # Windows の場合
        import ctypes
        from ctypes import wintypes
        
        # Windows API 定数
        GENERIC_WRITE = 0x40000000
        OPEN_EXISTING = 3
        FILE_SHARE_READ = 0x00000001
        FILE_SHARE_WRITE = 0x00000002
        
        # datetime から FILETIME に変換
        # FILETIME は 1601年1月1日からの100ナノ秒単位
        EPOCH_AS_FILETIME = 116444736000000000  # 1970年1月1日
        timestamp_100ns = int((timestamp * 10000000) + EPOCH_AS_FILETIME)
        
        # FILETIME 構造体を作成
        class FILETIME(ctypes.Structure):
            _fields_ = [
                ("dwLowDateTime", wintypes.DWORD),
                ("dwHighDateTime", wintypes.DWORD)
            ]
        
        ft = FILETIME()
        ft.dwLowDateTime = timestamp_100ns & 0xFFFFFFFF
        ft.dwHighDateTime = timestamp_100ns >> 32
        
        # Windows API 関数の定義
        CreateFileW = ctypes.windll.kernel32.CreateFileW
        CreateFileW.argtypes = [
            wintypes.LPCWSTR,  # lpFileName
            wintypes.DWORD,    # dwDesiredAccess
            wintypes.DWORD,    # dwShareMode
            wintypes.LPVOID,   # lpSecurityAttributes
            wintypes.DWORD,    # dwCreationDisposition
            wintypes.DWORD,    # dwFlagsAndAttributes
            wintypes.HANDLE    # hTemplateFile
        ]
        CreateFileW.restype = wintypes.HANDLE
        
        SetFileTime = ctypes.windll.kernel32.SetFileTime
        SetFileTime.argtypes = [
            wintypes.HANDLE,            # hFile
            ctypes.POINTER(FILETIME),   # lpCreationTime
            ctypes.POINTER(FILETIME),   # lpLastAccessTime
            ctypes.POINTER(FILETIME)    # lpLastWriteTime
        ]
        SetFileTime.restype = wintypes.BOOL
        
        CloseHandle = ctypes.windll.kernel32.CloseHandle
        CloseHandle.argtypes = [wintypes.HANDLE]
        CloseHandle.restype = wintypes.BOOL
        
        # ファイルを開く
        handle = CreateFileW(
            str(filepath),
            GENERIC_WRITE,
            FILE_SHARE_READ | FILE_SHARE_WRITE,
            None,
            OPEN_EXISTING,
            0,
            None
        )
        
        if handle == -1:  # INVALID_HANDLE_VALUE
            return False
        
        try:
            # 作成日時、アクセス日時、更新日時をすべて設定
            result = SetFileTime(handle, ctypes.byref(ft), ctypes.byref(ft), ctypes.byref(ft))
            return bool(result)
        finally:
            CloseHandle(handle)
            
    except Exception as e:
        print(f"  警告: ファイル日時の設定に失敗しました: {e}", file=sys.stderr)
        return False


def html_to_markdown(html_path: Path) -> str:
    """HTMLファイルをMarkdown形式に変換"""
    
    # HTMLファイルを読み込み
    with open(html_path, 'r', encoding='utf-8') as f:
        html_content = f.read()
    
    # HTMLをパース
    parser = KeepNoteParser()
    parser.feed(html_content)
    
    # Markdownを構築
    markdown_lines = []
    
    # タグ(ハッシュタグ形式)
    tags = []
    
    # アーカイブ/ピン留めタグ
    if parser.is_archived:
        tags.append("#archived")
    if parser.is_pinned:
        tags.append("#pinned")
    
    # ラベルタグ
    for label in parser.labels:
        # スペースを含む場合はアンダースコアに置換
        tag_name = label.replace(' ', '_')
        tags.append(f"#{tag_name}")
    
    if tags:
        markdown_lines.append(" ".join(tags) + "\n\n")
    
    # 本文
    if parser.content:
        # 連続する改行を整理
        content = parser.content.strip()
        # 3つ以上連続する改行を2つに
        content = re.sub(r'\n{3,}', '\n\n', content)
        markdown_lines.append(content + "\n")
    
    # Webリンク
    if parser.weblinks:
        markdown_lines.append("\n## リンク\n\n")
        for link in parser.weblinks:
            markdown_lines.append(f"- {link}\n")
    
    # 添付画像(Markdown形式で表示)
    if parser.attachments:
        markdown_lines.append("\n## 添付ファイル\n\n")
        # 重複を除去
        unique_attachments = list(dict.fromkeys(parser.attachments))
        for attachment in unique_attachments:
            # Markdown画像記法で表示
            markdown_lines.append(f"![{attachment}]({attachment})\n\n")
    
    return "".join(markdown_lines), parser.datetime


def main():
    """メイン処理"""
    
    # コマンドライン引数のパース
    parser = argparse.ArgumentParser(
        description='Google Keep HTMLエクスポートをObsidian Markdownに変換'
    )
    parser.add_argument('input', help='入力HTMLファイルのパス')
    parser.add_argument('output', nargs='?', help='出力Markdownファイルのパス(省略可)')
    parser.add_argument('-f', '--force', action='store_true', 
                       help='出力ファイルが既に存在する場合でも上書き')
    
    args = parser.parse_args()
    
    # 入力ファイルのパス
    input_path = Path(args.input)
    
    if not input_path.exists():
        print(f"エラー: ファイルが見つかりません: {input_path}", file=sys.stderr)
        sys.exit(1)
    
    # 出力ファイルのパス
    if args.output:
        output_path = Path(args.output)
    else:
        # 入力ファイル名から出力ファイル名を生成
        output_path = input_path.with_suffix('.md')
    
    # 既存ファイルの確認
    if output_path.exists() and not args.force:
        print(f"エラー: 出力ファイルが既に存在します: {output_path}", file=sys.stderr)
        print("上書きする場合は -f オプションを使用してください", file=sys.stderr)
        sys.exit(1)
    
    # 変換実行
    try:
        markdown_content, datetime_str = html_to_markdown(input_path)
        
        # Markdownファイルを書き込み
        with open(output_path, 'w', encoding='utf-8') as f:
            f.write(markdown_content)
        
        # ファイルの作成日時と更新日時を設定
        if datetime_str:
            try:
                # 日付文字列をパース (例: Oct 28, 2025, 11:16:42 PM)
                from datetime import datetime
                import locale
                
                # 英語ロケールを試みる(月名が英語のため)
                try:
                    locale.setlocale(locale.LC_TIME, 'en_US.UTF-8')
                except:
                    try:
                        locale.setlocale(locale.LC_TIME, 'C')
                    except:
                        pass
                
                # datetimeオブジェクトにパース
                dt = datetime.strptime(datetime_str, '%b %d, %Y, %I:%M:%S %p')
                
                # Unix timestampに変換
                timestamp = dt.timestamp()
                
                # ファイルの作成日時と更新日時を設定
                success = set_file_times(output_path, timestamp)
                
                print(f"変換完了: {output_path}")
                print(f"  作成日時: {datetime_str}")
                print(f"  出力サイズ: {len(markdown_content)} 文字")
                if success:
                    print(f"  ファイル日時: 設定成功")
                else:
                    print(f"  ファイル日時: 設定失敗(警告)")
                
            except Exception as e:
                print(f"変換完了: {output_path}")
                print(f"  出力サイズ: {len(markdown_content)} 文字")
                print(f"  警告: ファイル日時の設定に失敗しました: {e}", file=sys.stderr)
        else:
            print(f"変換完了: {output_path}")
            print(f"  出力サイズ: {len(markdown_content)} 文字")
        
    except Exception as e:
        print(f"エラー: 変換中に問題が発生しました: {e}", file=sys.stderr)
        sys.exit(1)


if __name__ == '__main__':
    main()

余談

obsidianのコミュニティープラグイン、custom attachment locationが良さそう。画像貼り付けると自動でノート名と同じディレクトリに格納してノートを移動すると画像フォルダも追随する。vault全体の画像のパスも整理してくれる 参考:https://note.com/rock72/n/ne69fd99b193d

Macで、OSの言語設定とは別にOfficeソフト(Word, Excel, Powerpoint)の言語設定を変更する

背景

Macの言語設定を英語にしているが、フォントの選択画面のプレビューなど日本語で表示した方が嬉しい場面が多い。Windows版のOfficeソフトはFile>Optionからソフトの表示言語を変更できるが、Mac版のOfficeソフトはOSの設定言語を表示言語に反映し、ソフトには表示言語を変更するUIがない。そこで調べるとMacでもコマンドラインから表示言語を変更できることを知ったため日本語記事として共有する。

手法

  • Terminalで以下のコマンドを実行する
$ defaults write com.microsoft.Powerpoint AppleLanguages '(ja)'
$ defaults write com.microsoft.Word AppleLanguages '(ja)'
$ defaults write com.microsoft.Excel AppleLanguages '(ja)'
  • Activity Monitorでプロセスを終了して再起動すると言語が反映されている(ウィンドウを閉じて再起動するだけでは反映されない)

参考: - コマンド例https://www.reddit.com/r/Colombia/comments/x1sw0j/cambiar_idioma_de_excel_o_powerpoint_o_word_en_un/?tl=ja - コマンド例https://www.lets-talk-about.tech/2018/09/change-software-language-in-microsoft.html - 言語リストhttps://www.ibabbleon.com/iOS-Language-Codes-ISO-639.html

3Dプリンター入門 東京を造る

この記事は[outré Advent Calendar 2025](https://adventar.org/calendars/11982)9日目の記事です。
遅れて大変申し訳ございません。

概要:
3Dプリンター(Bambu Lab P2S)を手に入れたので東京の立体地図を作る。

3Dプリンター入門
へだるさんやendenさんから3Dプリンターの物欲を刺激されたので、創作としては絵や漫画を描いていて必ずしも物理世界での工作力があるわけではない私が、3Dプリンターを買うことに相成った。まず3Dプリンター界について大雑把に述べる。3Dプリンターとは、主に植物性樹脂であるポリ乳酸を熱した金属製ノズルから連続的に押し出して、ノズルが動くことにより、下から積層して物を作っていくという機械で、ポリ乳酸以外にもPET樹脂、ABS樹脂が用いられることもある。この業界は、特許が切れた関係で2010年代から様々な企業が参入したらしい。キーボード界のCherryMXに似ている。Bambu A1 miniという機種が今のところ信頼できる情報源によりコストパフォーマンスが最強とされている。値段とできることは、大抵y=√xみたいな関数になるが、a1 miniはその最初の急な立ち上がりに相当する。上位機種のA1は造形範囲が広くなる。その上のP1Sはエンクロージャー(覆い)がついてABS樹脂など急冷すると反る樹脂を刷ることができる。P2SはP1Sの改良機種で、P1Sと違ってPLA印刷中にドア開けっぱなしにしなくていい、ノズル交換が容易といった違いがある。
Bambu labというのは中国深圳の会社で、ドローンのDJIから2020年に技術者が独立して作った会社らしい。急成長を遂げており、DJIは人材獲得競争でBambu labを仮想敵として競合の3DP企業に出資したと年末騒動になっていた[1]。DJIからスピンアウト起業した企業には他にANKERがあり、中国珠江デルタ地域の製造業の強さに打ちのめされる。情勢が落ち着いたらまた華強北電子城に行きたい。

[1]
日本語訳:https://x.com/yutr0n/status/1989939453226623286?s=46&t=rWvY73qZa5Ie23yU0UA6WA
weixin(削除済み):https://mp.weixin.qq.com/s/qA2zCjLEJ6jCkoxsEP7COg

今回はこのMap2Model[3]というウェブサイトを用いて皇居周辺の地図を印刷する。地図から3DP用の立体モデルを出力するサイトでショート動画[2]のリコメンドで知った。おそらくOpenStreetMapからデータをとってきており標高差は反映されず香港や神戸などを出力すると不自然になる。サイトにアクセスし領域を選択してSTLファイルをダウンロードする。

[2]ショート動画https://www.tiktok.com/@edutechexpert.ua/video/7556536137267809592?_r=1&_t=ZS-912cFtT791k
[3]Map2Model https://map2model.com/

模型であり微細な表現が求められるので、P2Sのノズルを標準の焼き入れ0.4 mm径から別売のステンレス0.2 mm径に交換する。
フィラメントはELEGOO白、ELEGOO灰色、OVERTUREオリーブグリーンを用いる。いずれも2000円前後ですがセールで1500円程度で購入。水路があるので青色が欲しかったがきりがないので進める。いろいろ吟味した結果、灰色を道路に割り当てるより水路に割り当てた方が見栄えがしそうだったので水路に。オリーブグリーンは緑地に使う。

印刷を開始。お濠が美しい。

しばらくするとお濠が白のフィラメントで覆われた、フィラメント切り替えミス?と思ったが首都高がお濠の上を通っているためだと気づいた。都市の解剖学的構造を印刷を眺めながら楽しむ、贅沢だ。P2Sは上がガラス板なのでAMS2を動かすと上からじっくり眺めたり写真が撮れる。

緑地が出てきた、なんて細かいんだ。

建物が生えてきた。最近知った話として、東京駅の正面は皇居に対して微妙に正面を向いていないらしい。

完成。本体にもともと入っているモデルのカメさんを怪獣役に出演させた。こちらのモデルはプラスチックのばね構造で足が反可動になっておりすごい。

「カメ、東京にあらわる」

速習Git + GitHub

特に認証周りでネットの情報が妙に古かったり情報が分散しているため躓いた。ここにショートカットをメモする。 非エンジニア向けに基本的かつ簡潔に書く。

前提

  • OS: windows11
  • gitコマンドインストール済み
  • シェルはコマンドプロンプトを使用

Git入門

ユーザー登録

名前とメールを登録する - git config --global user.name "Namako Uni" - git config --global user.email "unamako@gmail.com"

ローカルリポジトリの作成

Gitとは、ファイルの編集履歴を保存するツール。プログラミングのソースコードの管理で主に用いられる。

  • gitコマンドをインストールする
  • コマンドプロンプトを起動する
  • 作業するフォルダを作る(mkdir neko等)
  • 作業するフォルダに移動する(cd neko等)
  • 作業フォルダにgitを入れる(git init)
  • 作業フォルダに何かファイルを作る(echo nekodesu > readme.md等)
  • フォルダ内にある全てのファイルをaddする(git add -A)
  • フォルダ内にあるaddされたファイルを何かコメントをつけてcommitする(git commit -m "first commit"等)

メモ:基礎的なコマンド

windowsのコマンドプロンプトの機能としてあるコマンド(シェルコマンド)

  • ディレクトリ(フォルダ)を作る: mkdir フォルダ名
  • ディレクトリに移動するcd フォルダ名
  • ディレクトリから出る(一つ上のディレクトリに戻る)cd ..
  • ディレクトリにあるものをリストとして表示dir

    gitコマンド

  • 今どのブランチにいるか何がaddされているか見る: git status

GitHub入門

参考: - https://www.tohoho-web.com/ex/github.html - https://zenn.dev/nasubita/articles/b4d9cfaf0b6a78 GitHubはローカル環境でGitで管理したファイルをクラウドで共有するサービスである。GitLabとしてセルフホストも可能。GitHubを用いることでインターネットを介して複数人で開発作業ができたり、インターネットに製作したコードを公開できる。

準備

  • github.comに行く
  • アカウントを作る
  • リポジトリを作る

    トークンの発行

  • github.comにログインする
  • メニューバー(アイコン画像をクリックすると現れる)から、Setting>Developer settings>Personal access tokens>Tokens (classic)>Generate new tokenと進みトークンを作成画面を表示する
  • repo, admin:repo_hock, delete_repo にチェックを入れる
  • Generate tokenをクリックしトークンを発行する
  • ghp_sakgjhgJkhgadawliHLiuaslhiuGhjksegみたいな文字列のトークンをコピーしてメモする

    ローカルリポジトリをリモートリポジトリに対応させる

  • ローカルリポジトリをリモートリポジトリに対応させる git remote add origin https://namako:ghp_sakgjhgJkhgadawliHLiuaslhiuGhjkseg@github.com/namako/warabimochi.git
  • リモートリポジトリにpushするgit push --set-upstream origin master以降は単にgit pushでよい

その他Gitのコマンド

  • git add -Aとgit commitを一度に行う: git commit -a(ただし新規のファイルについてはaddされない)
  • ブランチ名aからbに変更: git branch a b最近はmasterではなくmainをメインのブランチ名として使うことが多いのでgit branch master mainと打つと良い
  • リモートリポジトリの確認: git remote -v