<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE feed [
      <!ENTITY lt "&#38;#60;">
      <!ENTITY gt "&#62;">
      <!ENTITY amp "&#38;#38;">
      <!ENTITY apos "&#39;">
      <!ENTITY quot "&#34;">
      <!ENTITY nbsp "&#160;">
      <!ENTITY copy "&#169;">
]>
<feed xmlns="http://www.w3.org/2005/Atom">
    <title type="text">ブログ</title>
    <subtitle type="text">UJP-Unwired Job Professional</subtitle>
    <updated>2026-06-12T01:21:47+09:00</updated>
    <id>http://www.ujp.jp/modules/d3blog/index.php</id>
    <link rel="alternate" type="text/xhtml" hreflang="ja" href="http://www.ujp.jp/" />
    <link rel="self" type="application/atom+xml" href="http://www.ujp.jp/modules/d3blog/index.php?page=atom" />
    <rights>Copyright (c) 1995-2020</rights>
    <generator uri="http://www.ujp.jp/">D3BLOG - XOOPS BLOG MODULE</generator>
    <entry>
        <title>最近のLLM所感</title>
        <link rel="alternate" type="text/xhtml" href="http://www.ujp.jp/modules/d3blog/details.php?bid=11211" />
        <id>http://www.ujp.jp/modules/d3blog/details.php?bid=11211</id>
        <published>2026-05-23T23:53:23+09:00</published>
        <updated>2026-05-26T10:25:16+09:00</updated>
        <category term="ハウツー" label="ハウツー" />
        <author>
            <name>ujpblog</name>
        </author>
        <summary type="html" xml:base="http://www.ujp.jp/" xml:lang="ja">　LLMをGoogle検索がわり以上に，おもちゃプログラミングでつかっているのだけど，現在の感想．Google検索のAIモード　無料でつかっていて，プログラム作成でも優秀．でも，なぜかURLが削...</summary>
       <content type="html" xml:lang="ja" xml:base="http://www.ujp.jp/">
<![CDATA[<div>　LLMをGoogle検索がわり以上に，おもちゃプログラミングでつかっているのだけど，現在の感想．<br /><br /><u><b><span style="font-size: large;">Google検索のAIモード</span></b></u><br /><br />　無料でつかっていて，プログラム作成でも優秀．でも，なぜかURLが削られる．サブドメインとかドメイン以降のパラメータとかが勝手に削られる．手動で入れていても，不意に勝手に削られる．<br />　勝手にURLを変えているからプログラム実行するとエラーがでるのだけど，エラーの原因が「通信障害」などと言い切ってしまう．URLを外出しのテキストファイルから読み込む仕様，にすればよいのだけど．<br />　例えるなら，お調子者の意言い訳下手の部下．<br /><br /><br /><u><b><span style="font-size: large;">Microsoft Copilot(Office 365についている有料版)</span></b></u><br /><br />　とりあえずブラウザでの文字入力にストレスが出る．ライブ入力との相性が悪いようだ．<br />　それとプログラム生成しているとこに，画面のリドローがおかしい．スクロールが上に引き戻されてしまう．　これは調べるとColilotの仕様で前の会話を参照するために上にスクロールするのだけど，戻るのが下手なようだ．<br />　これらの入力がおかしい件はさまざまな使い勝手の工夫で回避できるけど非常にストレス．<br /><br />　テキスト入力がおかしいという初歩で引っ掛かっているけど，プログラムはそこそこ優秀なんだけど，たまに忘れる．最初の頃に行っていた仕様を勝手に削除してしまうことがある．デグレードでもなくて，なくなってしまう．<br />　そして仕様欠落が始まると，やたらと人間を休ませようとしてくる．<br /><br /><center><img src="http://www.ujp.jp/modules/xelfinder/index.php?page=view&file=15121&MicrosoftCopilot20260522.jpg" align="center" alt="" /></center><br />　仕事で使っていると致命的だよね．<br /><br />　例えるなら，すっとぼけで仕事を誤魔化すタイプ．<br /><br /><u><b><span style="font-size: large;">Perplexity</span></b></u><br /><br />　ChatGPTのLLMがブームになった頃，学習している情報が古い問題があって，代わりによく使っていたのだけど，最近は全くダメ．調べのもについては日本の新聞社との訴訟の影響からか，情報が無いので役に立たない．そしてちょっと使うとすぐ「Proにアップグレードしました」となる．計算資源なんてそんなに使ってないような質問なのに？　って．　実質1日１質問くらいなのでプログラミングに使える気がしない．<br /><br /><u><b><span style="font-size: large;">Claude</span></b></u><br /><br />　Claude for Macをインストールして使ってる．GeminiやCopilotがダメな時に，懐刀として利用．<br />　難問をいくつか解決してくれたので優秀．信頼がおける．どんどん進捗するのだけど，その代わりに完成に近づくほど計算資源をたくさん使うようで活動限界がくる．<br />　優秀なんだけど，確実に定時退社する部下って感じかな．<br /><br /><u><b><span style="font-size: large;">ChatGPT</span></b></u><br />　すぐ限界が来ていたトラウマがあるので最近は使ってない．<br /><br /><br /><u><b><span style="font-size: large;">整理すると・・・</span></b></u><br />　普段はGoogle検索のAIモードを使ってる．バグが出て治せなくなっってAIモードがバカになってきたらCopilotに切り替えて修正する感じ．それらでもどうにもならなかったらClaudeに頼る感じかな．<br /><br />　サブスクリプションが毎月2,000円から3000円程度なんだけど，自分用のおもちゃ開発で業務利用じゃ無いし，ちょっと高い．月500円くらいでどうにかなればいいのになぁと思ったりしてる．これは欲張りだともわかってる．</div>]]>
       </content>
    </entry>
    <entry>
        <title>机の上に置く便利時計ダッシュボードを考える</title>
        <link rel="alternate" type="text/xhtml" href="http://www.ujp.jp/modules/d3blog/details.php?bid=11173" />
        <id>http://www.ujp.jp/modules/d3blog/details.php?bid=11173</id>
        <published>2026-04-30T01:22:10+09:00</published>
        <updated>2026-04-30T01:22:10+09:00</updated>
        <category term="ハウツー" label="ハウツー" />
        <author>
            <name>ujpblog</name>
        </author>
        <summary type="html" xml:base="http://www.ujp.jp/" xml:lang="ja">　パソコンを使っていると時間がわからなくなる．ウィンドウを最大化しているとメニューバーの時計は気づきにくい．　そこでNHK時計を入れた古いiPhoneを使っていたけど，なんとNHK時計...</summary>
       <content type="html" xml:lang="ja" xml:base="http://www.ujp.jp/">
<![CDATA[<div>　パソコンを使っていると時間がわからなくなる．ウィンドウを最大化しているとメニューバーの時計は気づきにくい．<br />　そこでNHK時計を入れた古いiPhoneを使っていたけど，なんとNHK時計はサービス終了．iPhoneを横にしておくと表示されるスタンバイモードの時計とカレンダーも良いのだけど，そうなるともっとダッシュボード的にさまざまな情報を表示させたい気がしていた．<br /><br />　そんな頃に，「Google Nest Hub第２世代」の未使用品が秋葉原で売られているというニュースをみた．<br /><br /><center><img src="http://www.ujp.jp/modules/xelfinder/index.php?page=view&file=15039&GoogleNextHub.jpg" align="center" alt="" /></center><br />　動画は見ないけど時計として良いのでは？とおもったりしたけど，カスタマイズできる範囲が少ないので触手は動かず．<br />　そして「SwitchBotスマートデイリーステーション」というのが発売されてSNSで少し話題．<br /><br /><center><img src="http://www.ujp.jp/modules/xelfinder/index.php?page=view&file=15041&SwitchBot.jpg" align="center" alt="" /></center><br />　天気がわかるのは良いなぁとおもったけど，これは値段が高いので触手が動かず．<br /><br /><center><img src="http://www.ujp.jp/modules/xelfinder/index.php?page=view&file=15040&TimesGate.jpg" align="center" alt="" /></center><br />　デザイン的に気に入っているのは，このサイバーパンク的なデザインの，安物Apple Watchのディスプレイを再利用したような情報端末．これも主にはアクセサリー用途だからちょっと違うかな．<br /><br />　ふと，自宅にある使わなくなったタブレットやスマホ，これを再利用してたん機能Webサイトを自分でも作ろうかと思ったけど，車輪の再発明になりそうなので調べたら，もっとかっこいいものがたくさん出てきた．<br /><br />天気予報つき時計 by ritsuka<br /><a href="https://ritska.com/clock/" rel="external">https://ritska.com/clock/</a><br /><br /><center><img src="http://www.ujp.jp/modules/xelfinder/index.php?page=view&file=15038&ritsuka.jpg" align="center" alt="" /></center><br /><br />TIME.IS<br /><a href="https://time.is/ja/" rel="external">https://time.is/ja/</a><br /><br /><center><img src="http://www.ujp.jp/modules/xelfinder/index.php?page=view&file=15042&Time_is.jpg" align="center" alt="" /></center><br /><br />　まさに今セットアップしているSurface Go 2に表示させればサイネージ的に使える！と思って試してみたらこれ．<br /><br /><center><img src="http://www.ujp.jp/modules/xelfinder/index.php?page=view&file=15043&ritsukaSurfaceGo2.jpg" align="center" alt="" /></center><br /> ちょうどいい．これでkiskモードでブラウザを起動すれば最高だね．</div>]]>
       </content>
    </entry>
    <entry>
        <title>写真，360度全天球写真，動画ファイルを１つのフォルダに入れてアルバムページを作る</title>
        <link rel="alternate" type="text/xhtml" href="http://www.ujp.jp/modules/d3blog/details.php?bid=11146" />
        <id>http://www.ujp.jp/modules/d3blog/details.php?bid=11146</id>
        <published>2026-04-13T00:46:55+09:00</published>
        <updated>2026-04-13T00:56:42+09:00</updated>
        <category term="ハウツー" label="ハウツー" />
        <author>
            <name>ujpblog</name>
        </author>
        <summary type="html" xml:base="http://www.ujp.jp/" xml:lang="ja">　件名にある通り，１つのフォルダに写真，360度全天球写真，動画ファイルを入れて，そのフォルダを指定して実行すると，アルバムを作成してくれるツール．Microsoft Copilotを使って作り...</summary>
       <content type="html" xml:lang="ja" xml:base="http://www.ujp.jp/">
<![CDATA[<div>　件名にある通り，１つのフォルダに写真，360度全天球写真，動画ファイルを入れて，そのフォルダを指定して実行すると，アルバムを作成してくれるツール．Microsoft Copilotを使って作りました．<br /><br />　こんな感じで使います．<br /><div class="xoopsCode"><pre><code>$ python3 generate_viewer.py -f 20260409SunriseIzumo -o ¥ 
20260409SunriseIzumo.html -t サンライズ出雲乗車記録2026.04.09_150分おくれ
$

-f メディアの入ったフォルダ
-o HTMLファイル名
-t ページタイトル</code></pre></div>　-fで指定されたフォルダに，サムネイルファイルや生成されたHTMLファイルが入ります．<br />　-oで生成されたファイルをブラウザで読み込むとアルバムとして使えます．<br />　写真の説明は，ファイル名から取得する仕様なので，ファイル名に長く記載してください．ファイルの掲載順は，ファイルのタイムスタンプです．<br /><br />　iPhoneの写真をmacOSにAirDropするとHEIFファイルになりますが，そのままだとうまくサムネイルが作成できないので，Finderで「イメージ変換」してJPEGにしてください．画像サイズは「実際のサイズ」で良いかと思います．<br />　以下のプログラムは，macOS Montereyで動作確認していますが，<a href="http://www.ujp.jp/modules/d3blog/details.php?bid=11082" rel="external">RICOH THETAで撮影した360度写真と共に写真アルバムを作る</a>にあるPannellumを使っています．<br /><br />　作ったものはこんな感じ．<br /><br /><a href="http://www.ujp.jp/images/theta/20260409SunriseIzumo/20260409SunriseIzumo.html" rel="external">サンライズ出雲乗車記録2026.04.09_150分おくれ</a><br /><br /><div class="xoopsCode"><pre><code>$ cat generate_viewer.py🆑
#!/usr/bin/env python3
import argparse
from pathlib import Path
import subprocess
import json
from datetime import datetime, timezone, timedelta
import urllib.request
import urllib.parse
from collections import OrderedDict

# ==========================
# 共通
# ==========================
created_date = datetime.now().strftime(&quot;%Y年%m月%d日&quot;)
geocode_cache = {}
JST = timezone(timedelta(hours=9))

# ==========================
# 逆ジオコーディング
# ==========================
def reverse_geocode(lat, lon):
    key = f&quot;{lat},{lon}&quot;
    if key in geocode_cache:
        return geocode_cache[key]
    try:
        url = &quot;https://nominatim.openstreetmap.org/reverse?&quot; + urllib.parse.urlencode({
            &quot;lat&quot;: lat,
            &quot;lon&quot;: lon,
            &quot;format&quot;: &quot;json&quot;,
            &quot;zoom&quot;: 16,
            &quot;addressdetails&quot;: 1
        })
        req = urllib.request.Request(url, headers={&quot;User-Agent&quot;: &quot;Mozilla/5.0&quot;})
        with urllib.request.urlopen(req, timeout=5) as res:
            data = json.loads(res.read().decode(&quot;utf-8&quot;))
            addr = data.get(&quot;address&quot;, {})
            geocode_cache[key] = addr
            return addr
    except:
        geocode_cache[key] = {}
        return {}

def extract_city(addr):
    return (
        addr.get(&quot;city&quot;) or
        addr.get(&quot;town&quot;) or
        addr.get(&quot;village&quot;) or
        addr.get(&quot;municipality&quot;) or
        addr.get(&quot;ward&quot;) or
        addr.get(&quot;city_district&quot;) or
        addr.get(&quot;county&quot;) or
        addr.get(&quot;suburb&quot;) or
        &quot;&quot;
    )

def format_location_short(addr):
    if not addr:
        return &quot;ー&quot;
    state = addr.get(&quot;state&quot;, &quot;&quot;)
    city = extract_city(addr)
    if state and city:
        return f&quot;{state} {city}&quot;
    elif state:
        return state
    elif city:
        return city
    else:
        return &quot;ー&quot;

def format_location_full(addr):
    if not addr:
        return &quot;ー&quot;
    state = addr.get(&quot;state&quot;, &quot;&quot;)
    city = extract_city(addr)
    detail_parts = []
    for key in [&quot;suburb&quot;, &quot;neighbourhood&quot;, &quot;hamlet&quot;, &quot;quarter&quot;, &quot;road&quot;, &quot;house_number&quot;]:
        if addr.get(key):
            detail_parts.append(addr[key])
    detail = &quot; &quot;.join(detail_parts)
    if state and city:
        base = f&quot;{state} {city}&quot;.strip()
    elif state:
        base = state
    elif city:
        base = city
    else:
        base = &quot;&quot;
    if base and detail:
        return f&quot;{base} {detail}&quot;.strip()
    elif base:
        return base
    elif detail:
        return detail
    else:
        return &quot;ー&quot;
# ==========================
# 日時関連
# ==========================
def parse_dt(dt_str):
    try:
        return datetime.strptime(dt_str, &quot;%Y:%m:%d %H:%M:%S&quot;)
    except:
        return None

def format_datetime(dt_str):
    try:
        dt = datetime.strptime(dt_str, &quot;%Y:%m:%d %H:%M:%S&quot;)
        return dt.strftime(&quot;%Y年%m月%d日 %H:%M:%S&quot;)
    except:
        return &quot;ー&quot;

def parse_creation_time(s):
    if not s:
        return None
    try:
        return datetime.fromisoformat(s.replace(&quot;Z&quot;, &quot;+00:00&quot;))
    except:
        return None

def format_datetime_from_dt(dt):
    if not dt:
        return &quot;ー&quot;
    return dt.strftime(&quot;%Y年%m月%d日 %H:%M:%S&quot;)

def format_date_only_from_dt(dt):
    if not dt:
        return &quot;その他&quot;
    return dt.strftime(&quot;%Y年%m月%d日&quot;)

# ==========================
# EXIF / 動画メタデータ
# ==========================
def get_exif(path):
    result = subprocess.run(
        [&quot;exiftool&quot;, &quot;-json&quot;, &quot;-n&quot;, str(path)],
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        text=True
    )
    return json.loads(result.stdout)[0]

def get_video_metadata(path):
    cmd = [
        &quot;ffprobe&quot;, &quot;-v&quot;, &quot;quiet&quot;, &quot;-print_format&quot;, &quot;json&quot;,
        &quot;-show_format&quot;, &quot;-show_streams&quot;, str(path)
    ]
    result = subprocess.run(cmd, stdout=subprocess.PIPE, text=True)
    if not result.stdout.strip():
        return {}
    return json.loads(result.stdout)

def parse_video_location(loc):
    if not loc:
        return None, None
    try:
        loc = loc.strip(&quot;/&quot;)
        if loc[0] in &quot;+-&quot;:
            sign = loc[0]
            rest = loc[1:]
            parts = rest.split(&quot;+&quot;)
            if len(parts) &gt;= 2:
                lat = float(sign + parts[0])
                lon = float(&quot;+&quot; + parts[1])
                return lat, lon
    except:
        pass
    return None, None

def format_duration(sec):
    try:
        sec = int(float(sec))
        m = sec // 60
        s = sec % 60
        return f&quot;{m:02d}:{s:02d}&quot;
    except:
        return &quot;ー&quot;

# ==========================
# HTML テンプレート
# ==========================
INDEX_HEAD = &quot;&quot;&quot;&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;ja&quot;&gt;
&lt;head&gt;
&lt;meta charset=&quot;UTF-8&quot;&gt;
&lt;title&gt;{page_title}&lt;/title&gt;
&lt;style&gt;
body {{
  font-family: sans-serif;
  background: #fafafa;
  margin: 0;
}}
header {{
  text-align: center;
  padding: 20px;
}}
h1 {{
  margin: 10px 0 0 0;
  font-size: 26px;
}}
h3 {{
  margin: 5px 0 10px 0;
  font-size: 16px;
  color: #555;
}}
.topnav {{
  text-align: center;
  margin: 10px 0;
  font-size: 18px;
}}
.section-title {{
  max-width: 1200px;
  margin: 10px auto 0 auto;
  padding: 0 20px;
  font-size: 18px;
  font-weight: bold;
}}
.count-info {{
  text-align: center;
  font-size: 14px;
  color: #555;
  margin-bottom: 10px;
}}
.grid {{
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 20px;
  padding: 10px 20px 20px 20px;
  max-width: 1200px;
  margin: 0 auto 20px auto;
}}
.card {{
  background: white;
  border-radius: 8px;
  padding: 10px;
  box-shadow: 0 0 6px rgba(0,0,0,0.2);
  text-align: center;
}}
.thumb-img {{
  width: 100%;
  height: auto;
  border-radius: 6px;
}}
.filename {{
  text-align: left;
  font-size: 14px;
  margin-top: 5px;
}}
.datetime {{
  text-align: left;
  font-size: 12px;
  color: #666;
}}
.location {{
  text-align: left;
  font-size: 12px;
  color: #444;
  margin-top: 3px;
}}
.map-link {{
  text-align: left;
  font-size: 12px;
  margin-top: 3px;
}}
.map-link a {{
  text-decoration: none;
  color: #1a73e8;
}}
.badge {{
  display: inline-block;
  padding: 2px 6px;
  font-size: 10px;
  border-radius: 4px;
  background: #eee;
  margin-left: 4px;
}}
.badge-360 {{
  background: #4caf50;
  color: #fff;
}}
.badge-video {{
  background: #f44336;
  color: #fff;
}}
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

&lt;div class=&quot;topnav&quot;&gt;
  &lt;a href=&quot;../index.html&quot;&gt;↑ 1つ上に戻る&lt;/a&gt;
&lt;/div&gt;

&lt;header&gt;
  &lt;hr&gt;
  &lt;h1&gt;{page_title}&lt;/h1&gt;
  &lt;h3&gt;作成日：{created_date}&lt;/h3&gt;
  &lt;div class=&quot;count-info&quot;&gt;全 {total_count} 件&lt;/div&gt;
  &lt;hr&gt;
&lt;/header&gt;
&quot;&quot;&quot;
INDEX_FOOT = &quot;&quot;&quot;
&lt;/body&gt;
&lt;/html&gt;
&quot;&quot;&quot;

VIEWER_HEAD = &quot;&quot;&quot;&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;ja&quot;&gt;
&lt;head&gt;
&lt;meta charset=&quot;UTF-8&quot;&gt;
&lt;title&gt;{title}&lt;/title&gt;

&lt;link rel=&quot;stylesheet&quot; href=&quot;../pannellum.css&quot;&gt;
&lt;script src=&quot;../pannellum.js&quot;&gt;&lt;/script&gt;

&lt;style&gt;
body {{
  margin: 0;
  font-family: sans-serif;
  background: #fafafa;
}}
header {{
  text-align: center;
  padding: 20px;
}}
h1 {{
  margin: 10px 0 0 0;
  font-size: 26px;
}}
h3 {{
  margin: 5px 0 10px 0;
  font-size: 16px;
  color: #555;
}}
.pano {{
  width: 100%;
  height: 500px;
}}
.info-table {{
  margin: 20px auto;
  width: 90%;
  max-width: 700px;
  border-collapse: collapse;
}}
.info-table td {{
  border: 1px solid #ccc;
  padding: 8px;
}}
.info-table td:first-child {{
  width: 30%;
  background: #f0f0f0;
  font-weight: bold;
}}
.nav {{
  margin: 20px 0;
  font-size: 22px;
  text-align: center;
}}
.nav a {{
  margin: 0 25px;
  text-decoration: none;
}}
.map-btn {{
  display: inline-block;
  padding: 10px 20px;
  background: #4285F4;
  color: white;
  border-radius: 6px;
  text-decoration: none;
  font-size: 16px;
}}
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

&lt;header&gt;
  &lt;hr&gt;
  &lt;h1&gt;{title}&lt;/h1&gt;
  &lt;h3&gt;作成日：{created_date}&lt;/h3&gt;
  &lt;hr&gt;
&lt;/header&gt;

&lt;div class=&quot;nav&quot;&gt;
  {nav_top}
&lt;/div&gt;
&quot;&quot;&quot;

VIEWER_FOOT = &quot;&quot;&quot;
&lt;/body&gt;
&lt;/html&gt;
&quot;&quot;&quot;

# ==========================
# メイン処理
# ==========================
def main():
    parser = argparse.ArgumentParser(description=&quot;THETA 360° / 写真 / 動画 ビューア生成（HEIC変換 + 動画メタデータ + JST対応）&quot;)
    parser.add_argument(&quot;-f&quot;, &quot;--folder&quot;, required=True)
    parser.add_argument(&quot;-o&quot;, &quot;--output&quot;, required=True)
    parser.add_argument(&quot;-t&quot;, &quot;--title&quot;, required=True)
    args = parser.parse_args()

    folder = Path(args.folder).resolve()

    image_ext = {&quot;.jpg&quot;, &quot;.jpeg&quot;, &quot;.JPG&quot;, &quot;.JPEG&quot;, &quot;.png&quot;, &quot;.PNG&quot;, &quot;.heic&quot;, &quot;.HEIC&quot;}
    video_ext = {&quot;.mp4&quot;, &quot;.mov&quot;, &quot;.m4v&quot;, &quot;.webm&quot;, &quot;.MP4&quot;, &quot;.MOV&quot;, &quot;.M4V&quot;, &quot;.WEBM&quot;}

    files = []
    for p in sorted(folder.iterdir()):
        if p.suffix in image_ext or p.suffix in video_ext:
            files.append(p)

    items = []

    # ==========================
    # ファイルごとの処理
    # ==========================
    for f in files:
        ext = f.suffix
        lower_ext = f.suffix.lower()
        is_video = lower_ext in {e.lower() for e in video_ext}
        is_heic = lower_ext in {&quot;.heic&quot;}

        # HEIC → JPEG 変換（元ファイルは残す）
        if is_heic:
            jpg_path = f.with_suffix(&quot;.jpg&quot;)
            if not jpg_path.exists():
                subprocess.run(
                    [&quot;ffmpeg&quot;, &quot;-y&quot;, &quot;-i&quot;, str(f), str(jpg_path)],
                    stdout=subprocess.DEVNULL,
                    stderr=subprocess.DEVNULL
                )
            f = jpg_path
            lower_ext = &quot;.jpg&quot;
            is_video = False

        exif = {}
        dt = None
        dt_raw = None
        camera_model = &quot;ー&quot;
        lat = None
        lon = None
        projection = None

        video_meta = {}
        video_duration = &quot;ー&quot;
        video_codec = &quot;ー&quot;
        video_resolution = &quot;ー&quot;

        # ==========================
        # 写真（EXIF）
        # ==========================
        if not is_video:
            exif = get_exif(f)
            dt_raw = exif.get(&quot;DateTimeOriginal&quot;)
            dt = parse_dt(dt_raw) if dt_raw else None
            camera_model = exif.get(&quot;Model&quot;, &quot;ー&quot;)
            lat = exif.get(&quot;GPSLatitude&quot;)
            lon = exif.get(&quot;GPSLongitude&quot;)
            projection = exif.get(&quot;ProjectionType&quot;)

        # ==========================
        # 動画（ffprobe）
        # ==========================
        else:
            video_meta = get_video_metadata(f)
            fmt = video_meta.get(&quot;format&quot;, {})
            streams = video_meta.get(&quot;streams&quot;, [])

            tags = fmt.get(&quot;tags&quot;, {}) if isinstance(fmt, dict) else {}
            creation_time = tags.get(&quot;creation_time&quot;)
            dt = parse_creation_time(creation_time)

            # ★ UTC → JST → naive
            if dt and dt.tzinfo:
                dt = dt.astimezone(JST)
            if dt:
                dt = dt.replace(tzinfo=None)

            dt_raw = creation_time

            camera_model = tags.get(&quot;com.apple.quicktime.model&quot;) or tags.get(&quot;model&quot;) or &quot;ー&quot;

            loc = tags.get(&quot;location&quot;)
            if loc:
                lat, lon = parse_video_location(loc)

            dur = fmt.get(&quot;duration&quot;)
            if dur:
                video_duration = format_duration(dur)

            if streams:
                vstream = None
                for s in streams:
                    if s.get(&quot;codec_type&quot;) == &quot;video&quot;:
                        vstream = s
                        break
                if not vstream:
                    vstream = streams[0]

                codec_name = vstream.get(&quot;codec_name&quot;)
                if codec_name:
                    video_codec = codec_name

                w = vstream.get(&quot;width&quot;)
                h = vstream.get(&quot;height&quot;)
                if w and h:
                    video_resolution = f&quot;{w}×{h}&quot;

        items.append({
            &quot;path&quot;: f,
            &quot;is_video&quot;: is_video,
            &quot;exif&quot;: exif,
            &quot;dt_raw&quot;: dt_raw,
            &quot;dt&quot;: dt,
            &quot;camera_model&quot;: camera_model,
            &quot;lat&quot;: lat,
            &quot;lon&quot;: lon,
            &quot;projection&quot;: projection,
            &quot;video_duration&quot;: video_duration,
            &quot;video_codec&quot;: video_codec,
            &quot;video_resolution&quot;: video_resolution,
        })
    # ==========================
    # 日付順にソート
    # ==========================
    items.sort(key=lambda x: x[&quot;dt&quot;] if x[&quot;dt&quot;] else datetime.max)

    # ==========================
    # サムネイル生成フォルダ
    # ==========================
    thumb_dir = folder / &quot;thumb&quot;
    thumb_dir.mkdir(exist_ok=True)

    index_html = folder / args.output
    total_count = len(items)
    index_parts = [
        INDEX_HEAD.format(
            page_title=args.title,
            created_date=created_date,
            total_count=total_count
        )
    ]

    # ==========================
    # 日付グループ化
    # ==========================
    groups = OrderedDict()
    for idx, item in enumerate(items):
        dt = item[&quot;dt&quot;]
        if dt:
            date_key = dt.strftime(&quot;%Y-%m-%d&quot;)
            date_label = format_date_only_from_dt(dt)
        else:
            date_key = &quot;other&quot;
            date_label = &quot;その他&quot;

        if date_key not in groups:
            groups[date_key] = {&quot;label&quot;: date_label, &quot;items&quot;: []}

        groups[date_key][&quot;items&quot;].append((idx, item))

    total = len(items)

    # ==========================
    # 個別ページ生成
    # ==========================
    for i, item in enumerate(items):
        f = item[&quot;path&quot;]
        is_video = item[&quot;is_video&quot;]
        dt = item[&quot;dt&quot;]
        dt_raw = item[&quot;dt_raw&quot;]
        camera_model = item[&quot;camera_model&quot;]
        lat = item[&quot;lat&quot;]
        lon = item[&quot;lon&quot;]
        projection = item[&quot;projection&quot;]
        video_duration = item[&quot;video_duration&quot;]
        video_codec = item[&quot;video_codec&quot;]
        video_resolution = item[&quot;video_resolution&quot;]

        viewer_name = f&quot;viewer_{i+1:04d}.html&quot;
        viewer_path = folder / viewer_name

        # --------------------------
        # サムネイル生成
        # --------------------------
        thumb_name = f&quot;thumb_{i+1:04d}.jpg&quot;
        thumb_path = thumb_dir / thumb_name

        if not thumb_path.exists():
            if not is_video:
                subprocess.run(
                    [&quot;convert&quot;, str(f), &quot;-resize&quot;, &quot;300x300&quot;, str(thumb_path)]
                )
            else:
                subprocess.run(
                    [
                        &quot;ffmpeg&quot;, &quot;-y&quot;,
                        &quot;-i&quot;, str(f),
                        &quot;-ss&quot;, &quot;00:00:01&quot;,
                        &quot;-vframes&quot;, &quot;1&quot;,
                        &quot;-vf&quot;, &quot;scale=300:-1&quot;,
                        str(thumb_path)
                    ],
                    stdout=subprocess.DEVNULL,
                    stderr=subprocess.DEVNULL
                )

        # --------------------------
        # 日付表示
        # --------------------------
        if dt_raw and isinstance(dt_raw, str) and &quot;:&quot; in dt_raw and dt_raw.count(&quot;:&quot;) == 2 and &quot;T&quot; not in dt_raw:
            dt_html_full = format_datetime(dt_raw)
        else:
            dt_html_full = format_datetime_from_dt(dt)

        # --------------------------
        # 位置情報
        # --------------------------
        if lat and lon:
            addr = reverse_geocode(lat, lon)
            location_short = format_location_short(addr)
            location_full = format_location_full(addr)
            lat_str = str(lat)
            lon_str = str(lon)
        else:
            location_short = &quot;ー&quot;
            location_full = &quot;ー&quot;
            lat_str = &quot;&quot;
            lon_str = &quot;&quot;

        # --------------------------
        # ナビゲーション
        # --------------------------
        prev_link = f&quot;viewer_{i:04d}.html&quot; if i &gt; 0 else &quot;&quot;
        next_link = f&quot;viewer_{i+2:04d}.html&quot; if i &lt; total - 1 else &quot;&quot;

        nav_html = &quot;&quot;
        nav_html += f&#039;&lt;a href=&quot;{prev_link}&quot;&gt;←&lt;/a&gt;&#039; if prev_link else &#039;←&#039;
        nav_html += f&#039;&lt;a href=&quot;{args.output}&quot;&gt;TOPに戻る&lt;/a&gt;&#039;
        nav_html += f&#039;&lt;a href=&quot;{next_link}&quot;&gt;→&lt;/a&gt;&#039; if next_link else &#039;→&#039;

        prev_js = f&#039;window.location.href=&quot;{prev_link}&quot;;&#039; if prev_link else &#039;&#039;
        next_js = f&#039;window.location.href=&quot;{next_link}&quot;;&#039; if next_link else &#039;&#039;

        comment_name = f.stem

        # --------------------------
        # Google Map ボタン
        # --------------------------
        if lat_str and lon_str:
            map_button_html = f&quot;&quot;&quot;
&lt;div style=&quot;text-align:center; margin:20px;&quot;&gt;
  &lt;a class=&quot;map-btn&quot; href=&quot;https://www.google.com/maps?q={lat_str},{lon_str}&quot; target=&quot;_blank&quot;&gt;
    Google Map で開く
  &lt;/a&gt;
&lt;/div&gt;
&quot;&quot;&quot;
        else:
            map_button_html = &quot;&quot;

        # --------------------------
        # 表示方式（360° / 写真 / 動画）
        # --------------------------
        ext = f.suffix.lower()
        video_ext_lower = {&quot;.mp4&quot;, &quot;.mov&quot;, &quot;.m4v&quot;, &quot;.webm&quot;}

        if projection == &quot;equirectangular&quot; and not is_video:
            viewer_html = f&quot;&quot;&quot;
&lt;div id=&quot;panorama&quot; class=&quot;pano&quot;&gt;&lt;/div&gt;

&lt;script&gt;
pannellum.viewer(&#039;panorama&#039;, {{
  type: &#039;equirectangular&#039;,
  panorama: &#039;{f.name}&#039;,
  autoLoad: true,
  hfov: 120
}});
&lt;/script&gt;
&quot;&quot;&quot;
        elif ext in video_ext_lower:
            viewer_html = f&quot;&quot;&quot;
&lt;div style=&quot;text-align:center; margin:20px;&quot;&gt;
  &lt;video controls style=&quot;max-width:100%; border-radius:8px;&quot;&gt;
    &lt;source src=&quot;{f.name}&quot; type=&quot;video/mp4&quot;&gt;
    お使いのブラウザは動画再生に対応していません。
  &lt;/video&gt;
&lt;/div&gt;
&quot;&quot;&quot;
        else:
            viewer_html = f&quot;&quot;&quot;
&lt;div style=&quot;text-align:center; margin:20px;&quot;&gt;
  &lt;img src=&quot;{f.name}&quot; style=&quot;max-width:100%; height:auto; border-radius:8px;&quot;&gt;
&lt;/div&gt;
&quot;&quot;&quot;

        # --------------------------
        # 個別ページ HTML 書き込み
        # --------------------------
        with open(viewer_path, &quot;w&quot;, encoding=&quot;utf-8&quot;) as vf:
            vf.write(VIEWER_HEAD.format(
                title=args.title,
                created_date=created_date,
                nav_top=nav_html
            ))

            vf.write(viewer_html)

            vf.write(f&quot;&quot;&quot;
&lt;script&gt;
document.addEventListener(&#039;keydown&#039;, function(e) {{
    if (e.key === &quot;ArrowLeft&quot;) {{
        {prev_js}
    }}
    if (e.key === &quot;ArrowRight&quot;) {{
        {next_js}
    }}
}});
&lt;/script&gt;

&lt;table class=&quot;info-table&quot;&gt;
  &lt;tr&gt;&lt;td&gt;撮影日&lt;/td&gt;&lt;td&gt;{dt_html_full}&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;td&gt;撮影場所&lt;/td&gt;&lt;td&gt;{location_full}&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;td&gt;カメラ&lt;/td&gt;&lt;td&gt;{camera_model}&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;td&gt;コメント&lt;/td&gt;&lt;td&gt;{comment_name}&lt;/td&gt;&lt;/tr&gt;
&quot;&quot;&quot;)

            if is_video:
                vf.write(f&quot;&quot;&quot;
  &lt;tr&gt;&lt;td&gt;動画の長さ&lt;/td&gt;&lt;td&gt;{video_duration}&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;td&gt;コーデック&lt;/td&gt;&lt;td&gt;{video_codec}&lt;/td&gt;&lt;/tr&gt;
  &lt;tr&gt;&lt;td&gt;解像度&lt;/td&gt;&lt;td&gt;{video_resolution}&lt;/td&gt;&lt;/tr&gt;
&quot;&quot;&quot;)

            vf.write(&quot;&lt;/table&gt;\n&quot;)
            vf.write(map_button_html)

            vf.write(f&quot;&quot;&quot;
&lt;div class=&quot;nav&quot;&gt;
  {nav_html}
&lt;/div&gt;
&quot;&quot;&quot;)

            vf.write(VIEWER_FOOT)

        # --------------------------
        # 一覧ページ用データ保存
        # --------------------------
        item[&quot;viewer_name&quot;] = viewer_name
        item[&quot;thumb_name&quot;] = thumb_name
        item[&quot;dt_html_full&quot;] = dt_html_full
        item[&quot;location_short&quot;] = location_short
        item[&quot;lat_str&quot;] = lat_str
        item[&quot;lon_str&quot;] = lon_str

        if projection == &quot;equirectangular&quot; and not is_video:
            item[&quot;badge&quot;] = &quot;360&quot;
        elif is_video:
            item[&quot;badge&quot;] = &quot;video&quot;
        else:
            item[&quot;badge&quot;] = &quot;photo&quot;

    # ==========================
    # 一覧ページ生成
    # ==========================
    for date_key, group in groups.items():
        label = group[&quot;label&quot;]
        index_parts.append(f&#039;&lt;div class=&quot;section-title&quot;&gt;{label}&lt;/div&gt;\n&#039;)
        index_parts.append(&#039;&lt;div class=&quot;grid&quot;&gt;\n&#039;)

        for idx, _ in group[&quot;items&quot;]:
            img_item = items[idx]
            viewer_name = img_item[&quot;viewer_name&quot;]
            thumb_name = img_item[&quot;thumb_name&quot;]
            dt_html_full = img_item[&quot;dt_html_full&quot;]
            location_short = img_item[&quot;location_short&quot;]
            lat_str = img_item[&quot;lat_str&quot;]
            lon_str = img_item[&quot;lon_str&quot;]
            short_comment = img_item[&quot;path&quot;].stem

            if len(short_comment) &gt; 15:
                short_comment = short_comment[:15] + &quot;…&quot;

            if lat_str and lon_str:
                map_link_html = f&#039;&lt;div class=&quot;map-link&quot;&gt;&lt;a href=&quot;https://www.google.com/maps?q={lat_str},{lon_str}&quot; target=&quot;_blank&quot;&gt;Google Map&lt;/a&gt;&lt;/div&gt;&#039;
            else:
                map_link_html = &quot;&quot;

            if img_item[&quot;badge&quot;] == &quot;360&quot;:
                badge_html = &#039;&lt;span class=&quot;badge badge-360&quot;&gt;360°&lt;/span&gt;&#039;
            elif img_item[&quot;badge&quot;] == &quot;video&quot;:
                badge_html = &#039;&lt;span class=&quot;badge badge-video&quot;&gt;動画&lt;/span&gt;&#039;
            else:
                badge_html = &#039;&lt;span class=&quot;badge&quot;&gt;写真&lt;/span&gt;&#039;

            index_parts.append(f&quot;&quot;&quot;
&lt;div class=&quot;card&quot;&gt;
  &lt;a href=&quot;{viewer_name}&quot;&gt;
    &lt;img class=&quot;thumb-img&quot; src=&quot;thumb/{thumb_name}&quot; alt=&quot;{img_item[&#039;path&#039;].name}&quot;&gt;
  &lt;/a&gt;
  &lt;div class=&quot;datetime&quot;&gt;{dt_html_full}&lt;/div&gt;
  &lt;div class=&quot;location&quot;&gt;{location_short} {badge_html}&lt;/div&gt;
  {map_link_html}
  &lt;div class=&quot;filename&quot;&gt;{short_comment}&lt;/div&gt;
&lt;/div&gt;
&quot;&quot;&quot;)

        index_parts.append(&quot;&lt;/div&gt;\n&quot;)

    index_parts.append(INDEX_FOOT)

    # ==========================
    # 一覧ページ書き込み
    # ==========================
    with open(index_html, &quot;w&quot;, encoding=&quot;utf-8&quot;) as f:
        f.write(&quot;&quot;.join(index_parts))

    print(&quot;サムネイル・一覧ページ:&quot;, index_html)
    print(&quot;個別ビューア / プレイヤー:&quot;, total_count, &quot;件生成しました。&quot;)


if __name__ == &quot;__main__&quot;:
    main()</code></pre></div></div>]]>
       </content>
    </entry>
    <entry>
        <title>RICOH THETAで撮影した360度写真と共に写真アルバムを作る</title>
        <link rel="alternate" type="text/xhtml" href="http://www.ujp.jp/modules/d3blog/details.php?bid=11082" />
        <id>http://www.ujp.jp/modules/d3blog/details.php?bid=11082</id>
        <published>2026-02-19T15:57:17+09:00</published>
        <updated>2026-02-20T12:07:09+09:00</updated>
        <category term="ハウツー" label="ハウツー" />
        <author>
            <name>ujpblog</name>
        </author>
        <summary type="html" xml:base="http://www.ujp.jp/" xml:lang="ja">　9年ほど前に買ったリコー THETA S撮影中に三脚が倒れてレンズに傷が入り修理して分譲して手に入れたTHETA Vですが，撮影した写真データは，theta360[.]comにアップロードしてブログパーツと...</summary>
       <content type="html" xml:lang="ja" xml:base="http://www.ujp.jp/">
<![CDATA[<div>　9年ほど前に買った<a href="http://www.ujp.jp/modules/d3blog/details.php?bid=4901" rel="external">リコー THETA S</a><a href="http://www.ujp.jp/modules/d3blog/details.php?bid=5610" rel="external">撮影中に三脚が倒れてレンズに傷が入り修理</a>して<a href="http://www.ujp.jp/modules/d3blog/details.php?bid=5649" rel="external">分譲して手に入れたTHETA V</a>ですが，撮影した写真データは，theta360[.]comにアップロードしてブログパーツとして貼り付けていた．<br />　しかしいつの間にかサービス終了．<br /><br />　仕方ないので，静的HTMLを生成するpannellumを使って他のサイトに依存しないで表示させるHTMLを生成するプログラムを作ってみた．<br /><br />Pannellum<br /><a href="https://github.com/mpetroff/pannellum/releases" rel="external">https://github.com/mpetroff/pannellum/releases</a><br /><br />　今回は，このサイトからpannellum-2.5.6.zipをダウンロードし，その中からpannellum.jsとpannellum.cssファイルを取り出し，htmlと同じフォルダに配置し，以下のHTMLファイルも同じ場所に保存して，360度写真ファイル名部分を変更すれば良い．<br /><br />　使ったソースコードはこれ．<div class="xoopsCode"><pre><code>&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;ja&quot;&gt;
&lt;head&gt;
  &lt;meta charset=&quot;UTF-8&quot;&gt;
  &lt;title&gt;THETA 360 Viewer&lt;/title&gt;

  &lt;!-- pannellum --&gt;
  &lt;link rel=&quot;stylesheet&quot; href=&quot;pannellum.css&quot;&gt;

  &lt;style&gt;
    body {
      margin: 0;
      font-family: sans-serif;
      background: #fafafa;
    }

    /* --- ヘッダ部分 --- */
    header {
      text-align: center;
      padding: 20px 10px;
    }

    /* 3ドット風の区切り線 */
    .dot-hr {
      border: none;
      text-align: center;
      margin: 10px auto;
    }
    .dot-hr:before {
      content: &quot;···&quot;;
      font-size: 20px;
      color: #999;
    }

    /* 戻るボタン */
    .back-button {
      display: inline-block;
      margin-top: 10px;
      padding: 8px 16px;
      background: #eee;
      border-radius: 6px;
      text-decoration: none;
      color: #333;
      font-size: 14px;
      border: 1px solid #ccc;
    }
    .back-button:hover {
      background: #ddd;
    }

    /* pannellum 表示領域 */
    #panorama {
      width: 100%;
      height: 500px;
    }
  &lt;/style&gt;
&lt;/head&gt;

&lt;body&gt;

&lt;header&gt;
  &lt;hr class=&quot;dot-hr&quot;&gt;
  &lt;h1&gt;マルチアルバム&lt;/h1&gt;
  &lt;hr class=&quot;dot-hr&quot;&gt;

  &lt;!-- 戻るボタン --&gt;
  &lt;a href=&quot;java script:history.back();&quot; class=&quot;back-button&quot;&gt;戻る&lt;/a&gt;
&lt;/header&gt;

&lt;div id=&quot;panorama&quot;&gt;&lt;/div&gt;

&lt;script src=&quot;pannellum.js&quot;&gt;&lt;/script&gt;
&lt;script&gt;
  pannellum.viewer(&#039;panorama&#039;, {
    type: &#039;equirectangular&#039;,
    panorama: &#039;サンライズ出雲シングルのパノラマ写真.jpg&#039;,
    autoLoad: true
  });
&lt;/script&gt;

&lt;/body&gt;
&lt;/html&gt;</code></pre></div><br />　できたコマンドラインツールで自動生成して作成したアルバムがこれ．<br /><br /><a href="http://www.ujp.jp/images/theta/20260206SunriseIzumo/20260206SunriseIzumo.html" rel="external">サンライズ出雲乗車記録2026.02.06</a><br /><br />　今回，自分用ツールなのだけど，これでデジカメの写真，スマホの写真，360度写真，動画を全部一緒にしてサイトで表示できるアルバムを作るPython3スクリプトを作ってみた．久しぶりにこういうのを作ったのでちょっと楽しかった．</div>]]>
       </content>
    </entry>
    <entry>
        <title>Brave Browserでページ全体のスクリーンショット</title>
        <link rel="alternate" type="text/xhtml" href="http://www.ujp.jp/modules/d3blog/details.php?bid=11075" />
        <id>http://www.ujp.jp/modules/d3blog/details.php?bid=11075</id>
        <published>2026-02-13T19:10:11+09:00</published>
        <updated>2026-02-13T19:43:42+09:00</updated>
        <category term="ハウツー" label="ハウツー" />
        <author>
            <name>ujpblog</name>
        </author>
        <summary type="html" xml:base="http://www.ujp.jp/" xml:lang="ja">　見ているSUUMOのページを丸ごと保存したいと考えた．　ブラウザでページをPDFにして印刷すれば良いのだけど，スタイルシートの影響か，リクルート社が意図的にそうしているかわから...</summary>
       <content type="html" xml:lang="ja" xml:base="http://www.ujp.jp/">
<![CDATA[<div>　見ているSUUMOのページを丸ごと保存したいと考えた．<br /><br />　ブラウザでページをPDFにして印刷すれば良いのだけど，スタイルシートの影響か，リクルート社が意図的にそうしているかわからないけど，レイアウトが崩れてぐちゃぐちゃになる．<br />　調べるとBrave Browserのデベロッパーツールでスクリーンキャプチャー機能があるというので試してみた．<br /><br /><b><span style="font-size: x-large;">デベロッパーツール（検証）を使う</span></b><br /><br />・保存したいページで F12 キー（Macは Cmd+Option+I）を押して検証メニューを開く。<br />・Ctrl+Shift+P（Macは Cmd+Shift+P）を押してコマンドメニューを開く。<br />・screenshot と入力し、「Capture full site screenshot」を選択すると、ページ全体がPNGで保存されます。 <br /><br />　簡単だけど使用頻度から考えると手順を覚えるのが無理．．．でも何も入れないのが良いのかも．<br /><br /><b><span style="font-size: x-large;">拡張機能GoFullPageを使う</span></b><br /><br />　Chromeの拡張機能でGoFullPageをインストール．<br /><br /><center><img src="http://www.ujp.jp/modules/xelfinder/index.php?page=view&file=14843&GoFullPage1.jpg" align="center" alt="" /></center><br /><br />　保存の際にA4ページサイズとかにするとページ替えの時に空白ができる．<br /><br /><center><img src="http://www.ujp.jp/modules/xelfinder/index.php?page=view&file=14842&GoFullPage2.jpg" align="center" alt="" /></center><br />　１枚のイメージにするには，image formatをfull imageにすればよかった．<br /><br />　これで定期的にキャプチャーして，<a href="http://www.ujp.jp/modules/d3blog/details.php?bid=5487" rel="external">画像でdiff</a>にすれば，画像の変化した場所がわかるようになる．<br /><br />　早速画像でdiffを取ってみた．<br /><center><img src="http://www.ujp.jp/modules/xelfinder/index.php?page=view&file=14844&GoFullPage3.jpg" align="center" alt="" /></center><br />　今回はサイトで「並び順」を変更しただけの変化を可視化してみたのだけど，新着分のある場所だけが赤色で示されている．また，サイト下部にある広告・他の地域の物件などの動的に変更される部分も赤色になってる．検証は成功かな．</div>]]>
       </content>
    </entry>
</feed>