<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>ものづくり | imakat.com</title>
	<atom:link href="https://imakat.com/category/manufacturing/feed/" rel="self" type="application/rss+xml" />
	<link>https://imakat.com</link>
	<description>工夫と改善で人生をちょっと豊かに</description>
	<lastBuildDate>Fri, 03 Apr 2026 10:01:30 +0000</lastBuildDate>
	<language>ja</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9.4</generator>

<image>
	<url>https://i0.wp.com/imakat.com/wp-content/uploads/2023/07/cropped-80d64ecd340db4e2ca3224859b04caed.png?fit=32%2C32&#038;ssl=1</url>
	<title>ものづくり | imakat.com</title>
	<link>https://imakat.com</link>
	<width>32</width>
	<height>32</height>
</image> 
<site xmlns="com-wordpress:feed-additions:1">160909258</site>	<item>
		<title>【Mac】Geminiで無料でVTT字幕を生成する</title>
		<link>https://imakat.com/2026/03/26/28762/</link>
		
		<dc:creator><![CDATA[imakat]]></dc:creator>
		<pubDate>Thu, 26 Mar 2026 11:59:19 +0000</pubDate>
				<category><![CDATA[字幕作成]]></category>
		<category><![CDATA[デジタル]]></category>
		<category><![CDATA[ものづくり]]></category>
		<category><![CDATA[vtt]]></category>
		<category><![CDATA[字幕]]></category>
		<category><![CDATA[Gemini]]></category>
		<category><![CDATA[Gem]]></category>
		<guid isPermaLink="false">https://imakat.com/?p=28762</guid>

					<description><![CDATA[動画の字幕作成については、これまで何度も試行錯誤を繰り返してきましたが、「これだ！」と思えるおすすめの方法を見つけました 。 今回は、Googleの生成AI（Gemini）の「Gem共有（カスタムAIエージェント）」を活 [&#8230;]]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-large is-resized"><a href="https://imakat.com/rd.php?id=aQFJQPda.png" target="_blank"><img decoding="async" src="https://imakat.com/rd.php?id=aQFJQPda.png" alt="" style="aspect-ratio:1.4480358030830434;width:268px;height:auto"/></a></figure>



<p>動画の字幕作成については、これまで何度も試行錯誤を繰り返してきましたが、「これだ！」と思えるおすすめの方法を見つけました 。</p>



<p>今回は、Googleの生成AI（Gemini）の「Gem共有（カスタムAIエージェント）」を活用して、高精度なVTT字幕を生成するプログラムを作成しましたので紹介します 。</p>



<p>最大の特徴は、<strong>サブスクリプション不要（無料）で実践できる</strong>という点です 。（※Googleアカウントは必要です 。）</p>



<p>Geminiの文字起こしはマルチモーダル処理のため、精度は非常に高く、後から手直しする手間が劇的に減ります 。無料枠の制限を賢く回避しつつ、面倒な作業は、Macクイックアクションで自動化する、実践的なワークフローを公開します。</p>



<p>解説動画をご覧ください。</p>


<div class="sc-vimeo-embed">
  <p style="font-size: 14px;" class="sc-link">
    <a href="https://imakat.com/vm5?movid=1177224964" target="_blank" onclick="stopVimeoBeforeNavigate(event, this)">
      動画を別ページで表示(ここをクリック)
    </a>
  </p>

  <div class="iframe-wrapper">
    <iframe
      src="https://player.vimeo.com/video/1177224964?title=0&byline=0&portrait=0&controls=1&speed=1&texttrack=ja&dnt=1&loop=0"
      frameborder="0"
      allow="autoplay; fullscreen; picture-in-picture"
      allowfullscreen></iframe>
  </div>

  <div class="dr-vimeo-sublist">
    <details translate="yes"><summary translate="yes">字幕一覧(クリック)</summary> <p translate="yes">
(<a href="#" class="imk-cue" data-seek="0:00" translate="no">00:00:00</a>)  〜無料でGem共有でvtt字幕生成〜<br>
(<a href="#" class="imk-cue" data-seek="0:14" translate="no">00:00:14</a>)  今回は、えーGem。Gemというのは、GoogleのAIエージェントですけれども、<br>
(<a href="#" class="imk-cue" data-seek="0:24" translate="no">00:00:24</a>)  それを使って、VTT字幕の生成を行うプログラムを作成したので紹介します。<br>
(<a href="#" class="imk-cue" data-seek="0:36" translate="no">00:00:36</a>)  動画の字幕作成については、これまで何度も取り上げてきました。<br>
(<a href="#" class="imk-cue" data-seek="0:43" translate="no">00:00:43</a>)  でもまあ、なかなかここで満足ということにはなりませんが、<br>
(<a href="#" class="imk-cue" data-seek="0:48" translate="no">00:00:48</a>)  最近これが結構おすすめという方法を見つけましたので紹介します。<br>
(<a href="#" class="imk-cue" data-seek="0:56" translate="no">00:00:56</a>)  生成AIを使う方法です。<br>
(<a href="#" class="imk-cue" data-seek="1:00" translate="no">00:01:00</a>)  サブスクなしで、お金を使わないでできる方法ですのでご心配なく。<br>
(<a href="#" class="imk-cue" data-seek="1:11" translate="no">00:01:11</a>)  ただし、Googleアカウントは必要になります。<br>
(<a href="#" class="imk-cue" data-seek="1:16" translate="no">00:01:16</a>)  まず、字幕を作りたい動画を用意します。<br>
(<a href="#" class="imk-cue" data-seek="1:21" translate="no">00:01:21</a>)  今回ですね、一つ10分弱の動画をですね用意しました。<br>
(<a href="#" class="imk-cue" data-seek="1:29" translate="no">00:01:29</a>)  VTT作成用にこの動画を分割します。<br>
(<a href="#" class="imk-cue" data-seek="1:34" translate="no">00:01:34</a>)  その理由はGeminiのGemを使うとき、特に無料アカウントで使うときは、<br>
(<a href="#" class="imk-cue" data-seek="1:42" translate="no">00:01:42</a>)  動画は5分以内という制限があるからです。<br>
(<a href="#" class="imk-cue" data-seek="1:49" translate="no">00:01:49</a>)  この動画をですね、右クリックします。<br>
(<a href="#" class="imk-cue" data-seek="1:52" translate="no">00:01:52</a>)  クイックアクション、このクイックアクションの中から、<br>
(<a href="#" class="imk-cue" data-seek="1:59" translate="no">00:01:59</a>)  あらかじめ自作した「字幕用に動画を分割する」これを選びます。<br>
(<a href="#" class="imk-cue" data-seek="2:20" translate="no">00:02:20</a>)  すると、分割する基本秒数を入れてください、と出てきます。<br>
(<a href="#" class="imk-cue" data-seek="2:28" translate="no">00:02:28</a>)  デフォルトは270秒になっています。つまり4分30秒になっています。<br>
(<a href="#" class="imk-cue" data-seek="2:36" translate="no">00:02:36</a>)  でもこれは4分30秒のところでバサッと分割するのではありません。<br>
(<a href="#" class="imk-cue" data-seek="2:44" translate="no">00:02:44</a>)  4分30秒付近で1秒間の無音になる部分を探して、<br>
(<a href="#" class="imk-cue" data-seek="2:51" translate="no">00:02:51</a>)  その無音が開始する場所で分割するようになっています。<br>
(<a href="#" class="imk-cue" data-seek="2:58" translate="no">00:02:58</a>)  分割ファイルはsplit_01_02...と、いうように分割されていきます。<br>
(<a href="#" class="imk-cue" data-seek="3:31" translate="no">00:03:31</a>)  えー、split_01、02、03と3つのファイルに分割されました。<br>
(<a href="#" class="imk-cue" data-seek="3:39" translate="no">00:03:39</a>)  で、あと一つ、offsets.txtというファイルも出来上がってます。<br>
(<a href="#" class="imk-cue" data-seek="3:46" translate="no">00:03:46</a>)  それではこのoffsets.txtというファイルを開いてみます。<br>
(<a href="#" class="imk-cue" data-seek="3:52" translate="no">00:03:52</a>)  すると、ここにですね、分割したファイルの開始時刻が入っています。<br>
(<a href="#" class="imk-cue" data-seek="4:02" translate="no">00:04:02</a>)  01、02、03、ここにズレ0秒、ズレ245.130秒<br>
(<a href="#" class="imk-cue" data-seek="4:09" translate="no">00:04:09</a>)  ズレ443.475秒となっています。<br>
(<a href="#" class="imk-cue" data-seek="4:19" translate="no">00:04:19</a>)  これは後で使うことになります。まあ、あることだけを覚えておいてください。<br>
(<a href="#" class="imk-cue" data-seek="4:25" translate="no">00:04:25</a>)  一応これを見て、確かにそのそれぞれが5分以内で分割されていることは確認できます。<br>
(<a href="#" class="imk-cue" data-seek="4:35" translate="no">00:04:35</a>)  一つ目が245秒、二つ目が、まあ200秒ぐらい、三つ目はですね、<br>
(<a href="#" class="imk-cue" data-seek="4:44" translate="no">00:04:44</a>)  全体で9分41秒、つまり9分41秒、443秒だから、えー、<br>
(<a href="#" class="imk-cue" data-seek="4:53" translate="no">00:04:53</a>)  7分、7x6=42、7分ちょっとのところで終わりますので、<br>
(<a href="#" class="imk-cue" data-seek="5:04" translate="no">00:05:04</a>)  あと残りが2分ちょっとですかね。その部分が03になるはずです。<br>
(<a href="#" class="imk-cue" data-seek="5:13" translate="no">00:05:13</a>)  確かに3つのファイルがですね、5分以内で分割されています。<br>
(<a href="#" class="imk-cue" data-seek="5:24" translate="no">00:05:24</a>)  えっと、次にですね、Gemで作ったこのVTT形式のコードを作成するプログラムですね、<br>
(<a href="#" class="imk-cue" data-seek="5:37" translate="no">00:05:37</a>)  これを開きます。これがそれです。<br>
(<a href="#" class="imk-cue" data-seek="5:45" translate="no">00:05:45</a>)  えー、このGemはですね、公開してますので、ブログにつけます。<br>
(<a href="#" class="imk-cue" data-seek="5:53" translate="no">00:05:53</a>)  モードはですね、あの、Proモードを選択してください。<br>
(<a href="#" class="imk-cue" data-seek="6:01" translate="no">00:06:01</a>)  で、下のボックス、ここにですね、今作った分割された動画をドロップします。<br>
(<a href="#" class="imk-cue" data-seek="6:11" translate="no">00:06:11</a>)  コピーアンドペーストします。まず一つ目コピー、<br>
(<a href="#" class="imk-cue" data-seek="6:18" translate="no">00:06:18</a>)  ここにペーストします。で、ここに入れるのはただこれだけです。<br>
(<a href="#" class="imk-cue" data-seek="6:30" translate="no">00:06:30</a>)  で、これでこの「Go」、送信の矢印を押してください。<br>
(<a href="#" class="imk-cue" data-seek="6:40" translate="no">00:06:40</a>)  そうすると作業が開始します。しばらく待ちます。<br>
(<a href="#" class="imk-cue" data-seek="7:33" translate="no">00:07:33</a>)  しばらく待ちますと、このようにVTTコードが出力されます。<br>
(<a href="#" class="imk-cue" data-seek="7:41" translate="no">00:07:41</a>)  これをコピーして、拡張子がVTTのファイルにペーストします。<br>
(<a href="#" class="imk-cue" data-seek="7:54" translate="no">00:07:54</a>)  えーと、これをコピーして、拡張子がVTTのファイルにペーストします。<br>
(<a href="#" class="imk-cue" data-seek="8:10" translate="no">00:08:10</a>)  あの、このVTTファイルは、あらかじめ用意しておいてください。<br>
(<a href="#" class="imk-cue" data-seek="8:17" translate="no">00:08:17</a>)  私はあの以前に作ったVTTファイルをですね、コピペして名前を変えて、<br>
(<a href="#" class="imk-cue" data-seek="8:28" translate="no">00:08:28</a>)  中身を削除して作っておきました。この３つですね。<br>
(<a href="#" class="imk-cue" data-seek="8:33" translate="no">00:08:33</a>)  それではこの01にですね、ペーストします。ここのコードをコピー。<br>
(<a href="#" class="imk-cue" data-seek="8:43" translate="no">00:08:43</a>)  えー、そして、ファイルにペースト。という形ですね。<br>
(<a href="#" class="imk-cue" data-seek="8:58" translate="no">00:08:58</a>)  保存。ということで、01番ができました。<br>
(<a href="#" class="imk-cue" data-seek="9:14" translate="no">00:09:14</a>)  同じようにしてですね、02以降も作るんですけども、<br>
(<a href="#" class="imk-cue" data-seek="9:21" translate="no">00:09:21</a>)  この同じセッションの中にですね、複数の動画があると、なかなかうまく受け付けてくれないので、<br>
(<a href="#" class="imk-cue" data-seek="9:30" translate="no">00:09:30</a>)  新しいセッションで同じことを繰り返すようにしてください。<br>
(<a href="#" class="imk-cue" data-seek="9:42" translate="no">00:09:42</a>)  こういう形で新しいセッションを開きます。そしてこの02ですね。これをコピーして、<br>
(<a href="#" class="imk-cue" data-seek="9:57" translate="no">00:09:57</a>)  貼り付けます。Proモードにして、<br>
(<a href="#" class="imk-cue" data-seek="10:12" translate="no">00:10:12</a>)  Proモードにして、送信します。<br>
(<a href="#" class="imk-cue" data-seek="10:38" translate="no">00:10:38</a>)  はい、コードができました。そしたらこれをコピーして、<br>
(<a href="#" class="imk-cue" data-seek="10:47" translate="no">00:10:47</a>)  02番のVTTファイルにペーストします。<br>
(<a href="#" class="imk-cue" data-seek="11:12" translate="no">00:11:12</a>)  同じように03番も作ります。<br>
(<a href="#" class="imk-cue" data-seek="11:39" translate="no">00:11:39</a>)  送信。<br>
(<a href="#" class="imk-cue" data-seek="11:58" translate="no">00:11:58</a>)  えーと、できましたのでコードをコピーして、<br>
(<a href="#" class="imk-cue" data-seek="12:06" translate="no">00:12:06</a>)  03VTTに貼り付けます。<br>
(<a href="#" class="imk-cue" data-seek="12:23" translate="no">00:12:23</a>)  こうして作るとお気づきのように、02以降のですね、動画の時刻は0秒からの開始になってしまっています。<br>
(<a href="#" class="imk-cue" data-seek="12:39" translate="no">00:12:39</a>)  秒ずらしの作業を行います。<br>
(<a href="#" class="imk-cue" data-seek="12:45" translate="no">00:12:45</a>)  この時に先ほど見たoffsets.txtの中の秒がずらし秒になってきます。<br>
(<a href="#" class="imk-cue" data-seek="12:58" translate="no">00:12:58</a>)  vttファイルの上で右クリックしてクイックアクションから「字幕時刻の秒ずらしを行う」を選びます。<br>
(<a href="#" class="imk-cue" data-seek="13:22" translate="no">00:13:22</a>)  ここでさっきの秒数を入れます。245.130秒。これ入れましてOKを押します。<br>
(<a href="#" class="imk-cue" data-seek="13:48" translate="no">00:13:48</a>)  timeshifted.vttができて245.130秒、つまり4分何秒かを加算したファイルができあがってます。<br>
(<a href="#" class="imk-cue" data-seek="14:13" translate="no">00:14:13</a>)  同じように03も秒ずらしを行います。今度は443.475。同じようにファイルができてます。<br>
(<a href="#" class="imk-cue" data-seek="14:51" translate="no">00:14:51</a>)  7分24秒ぐらいのところからのスタートに置き換わっています。<br>
(<a href="#" class="imk-cue" data-seek="15:04" translate="no">00:15:04</a>)  秒ずらしが済んだら結合したvttファイルを作ります。あらかじめbinded.vttを用意します。<br>
(<a href="#" class="imk-cue" data-seek="15:17" translate="no">00:15:17</a>)  この中に3つのvttファイルをコピペします。まず01は時刻ずれなしでそのままペーストします。<br>
(<a href="#" class="imk-cue" data-seek="15:45" translate="no">00:15:45</a>)  次に02_timeshifted.vttです。この連番以降のところをコピーして貼り付けます。<br>
(<a href="#" class="imk-cue" data-seek="16:18" translate="no">00:16:18</a>)  次に03_timeshifted.vtt、これも番号以降のところをコピーして貼り付けます。以上ですね。<br>
(<a href="#" class="imk-cue" data-seek="16:45" translate="no">00:16:45</a>)  全部1つのファイルにバインドされました。保存します。<br>
(<a href="#" class="imk-cue" data-seek="16:54" translate="no">00:16:54</a>)  ところがこれはすでにお気づきのように1からスタートした箇所が3箇所あるわけですね。<br>
(<a href="#" class="imk-cue" data-seek="17:08" translate="no">00:17:08</a>)  ここにもありますし、それから、<br>
(<a href="#" class="imk-cue" data-seek="17:15" translate="no">00:17:15</a>)  あ、ここにもあると。3箇所あります。<br>
(<a href="#" class="imk-cue" data-seek="17:18" translate="no">00:17:18</a>)  これを連番が全部通しで1番から開始するように、番号の振り直しをします。<br>
(<a href="#" class="imk-cue" data-seek="17:37" translate="no">00:17:37</a>)  このbinded.vttの上で右クリックして、<br>
(<a href="#" class="imk-cue" data-seek="17:41" translate="no">00:17:41</a>)  クイックアクションの中から、<br>
(<a href="#" class="imk-cue" data-seek="17:47" translate="no">00:17:47</a>)  この「字幕VTTの連番を付け直す」、これを選びます。<br>
(<a href="#" class="imk-cue" data-seek="17:58" translate="no">00:17:58</a>)  すると、renumbered.vttが出来上がります。<br>
(<a href="#" class="imk-cue" data-seek="18:07" translate="no">00:18:07</a>)  これは1番からですね。ずっと連番で、<br>
(<a href="#" class="imk-cue" data-seek="18:18" translate="no">00:18:18</a>)  67番までですか。ここまで繋がってます。<br>
(<a href="#" class="imk-cue" data-seek="18:26" translate="no">00:18:26</a>)  それで言葉遣いの修正とかテキストの修正は、ここまで形が整った後で行うのがいいと思います。<br>
(<a href="#" class="imk-cue" data-seek="18:38" translate="no">00:18:38</a>)  地味にコツコツとやる方法ではありますが、仕組みとして分かりやすいので作業のストレスは感じません。<br>
(<a href="#" class="imk-cue" data-seek="18:51" translate="no">00:18:51</a>)  この方法が優れているのは、GeminiのGems、これの文字起こしの精度が非常に高いということです。<br>
(<a href="#" class="imk-cue" data-seek="19:03" translate="no">00:19:03</a>)  手直しするところはとても少ないです。満足できると思います。<br>
(<a href="#" class="imk-cue" data-seek="19:10" translate="no">00:19:10</a>)  以上、Gemsを使ったVTTコード作成の紹介でした。<br>
(<a href="#" class="imk-cue" data-seek="19:17" translate="no">00:19:17</a>)  コード類はブログ記事に掲載しますのでご利用ください。<br>
</p> </details>
<style>
details { font: 16px "Open Sans", Calibri, sans-serif; width: 100%; }
details > summary { padding: 2px 6px; width: 100%; background-color: #ddd; border: none; box-shadow: 3px 3px 4px black; cursor: pointer; list-style: none; }
details > p { font: 14px "Open Sans", Calibri, sans-serif; height:150px; overflow: scroll; background-color: #EDF7FF; padding: 2px 6px; margin: 0; box-shadow: 3px 3px 4px black; }
</style>

  </div>

  <script src="https://player.vimeo.com/api/player.js"></script>

  <script>
  (function(){
    var me = document.currentScript;
    var wrapper = me ? me.closest('.sc-vimeo-embed') : null;

    var target = wrapper ? wrapper : document;
    var iframe = target.querySelector('iframe');
    if (!iframe || !window.Vimeo || !Vimeo.Player) return;

    var player = new Vimeo.Player(iframe);

    /* --- 1. 確実に一時停止してから別ページを開く処理 --- */
    window.stopVimeoBeforeNavigate = function(event, link) {
      event.preventDefault(); 
      player.pause().then(function() {
        setTimeout(function() {
           window.open(link.href, link.target);
        }, 50);
      }).catch(function() {
        window.open(link.href, link.target);
      });
    };

    /* --- 2. 字幕のON/OFF制御（Vimeo API仕様） --- */
    var currentMode = "hidden";
    
    function useNormalMode() {
        currentMode = "hidden";
        player.disableTextTrack().catch(function(e){});
    }
    
    function useSpecialMode() {
        currentMode = "showing";
        // 日本語の subtitles が無ければ captions を探す安全設計
        player.enableTextTrack('ja', 'subtitles').catch(function() {
            player.enableTextTrack('ja', 'captions').catch(function() {});
        });
    }

    // ロード直後は枠内字幕を非表示にする
    player.ready().then(function() {
        useNormalMode();
    });

    // Vimeoプレーヤーの全画面・PiPイベントを監視して字幕を切り替え
    player.on('fullscreenchange', function(data) {
        if (data.fullscreen) useSpecialMode();
        else useNormalMode();
    });
    player.on('enterpictureinpicture', function() {
        useSpecialMode();
    });
    player.on('leavepictureinpicture', function() {
        useNormalMode();
    });

    /* --- 3. 字幕リストの初期化（自動梱包とデフォルト展開） --- */
    var checks = 0;
    var initTimer = setInterval(function(){
        var listContainer = target.querySelector('details > p');
        if (listContainer) {
            initSubtitles(listContainer);
            clearInterval(initTimer);
        }
        checks++;
        if (checks > 20) clearInterval(initTimer); // 10秒経ったら監視終了
    }, 500);

    function initSubtitles(listContainer) {
        // デフォルトで開く
        var detailsEl = target.querySelector("details");
        if (detailsEl) {
            detailsEl.open = true; 
            var summaryEl = detailsEl.querySelector("summary");
            if (summaryEl) summaryEl.textContent = "字幕(シーン)はここをクリック";
        }

        // 行ごとに見えない箱(span)で梱包
        if (!listContainer.dataset.formatted) {
            var html = listContainer.innerHTML;
            var lines = html.split(/<br\s*\/?>/i);
            var newHtml = "";
            for(var j=0; j<lines.length; j++) {
                if(lines[j].trim() === "") continue;
                newHtml += "<span class='imk-line'>" + lines[j] + "</span><br>";
            }
            listContainer.innerHTML = newHtml;
            listContainer.dataset.formatted = "true";
        }
    }

    /* --- 4. クリックシーク機能 --- */
    function parseTs(ts) {
      if (!ts) return null;
      var t = ts.trim();
      var parts = t.split(':').map(function(v){ return parseInt(v, 10); });
      if (parts.length === 2 && parts.every(Number.isFinite)) {
        return parts[0] * 60 + parts[1];
      } else if (parts.length === 3 && parts.every(Number.isFinite)) {
        return parts[0] * 3600 + parts[1] * 60 + parts[2];
      }
      return null;
    }

    target.addEventListener('click', function(e) {
      var a = e.target.closest('a.imk-cue');
      if (!a) return;
      e.preventDefault();
      var sec = null;
      if (a.dataset.seconds) {
        sec = Number(a.dataset.seconds);
      } else if (a.dataset.seek) {
        sec = parseTs(a.dataset.seek);
      }
      if (sec !== null && Number.isFinite(sec)) {
        player.setCurrentTime(sec).then(function(){ player.play(); }).catch(function(){});
      }
    });

    /* --- 5. 同期ハイライトと自動スクロール --- */
    player.on('timeupdate', function(data) {
      // Vimeoプレーヤーが勝手に字幕を出していたら隠す（最強ガード）
      player.getTextTracks().then(function(tracks) {
          var activeTrack = tracks.find(function(t){ return t.mode === 'showing'; });
          if (currentMode === "hidden" && activeTrack) {
              useNormalMode();
          }
      }).catch(function(){});

      var currentTime = data.seconds;
      var listContainer = target.querySelector('details > p');
      if (!listContainer) return;

      var cues = listContainer.querySelectorAll('a.imk-cue');
      if (cues.length === 0) return;

      var activeA = null;
      for (var i = 0; i < cues.length; i++) {
        var sec = null;
        if (cues[i].dataset.seconds) {
            sec = Number(cues[i].dataset.seconds);
        } else if (cues[i].dataset.seek) {
            sec = parseTs(cues[i].getAttribute('data-seek'));
        }
        if (sec !== null && currentTime >= sec - 0.5) {
          activeA = cues[i];
        } else if (sec > currentTime) {
          break;
        }
      }

      if (activeA) {
          var activeLine = activeA.closest(".imk-line");
          if (!activeLine) activeLine = activeA;

          if (activeLine.classList.contains("active-hl")) return;

          // 全行の色を徹底的にリセット
          var allLines = listContainer.querySelectorAll(".imk-line");
          for(var k=0; k<allLines.length; k++) {
              allLines[k].classList.remove("active-hl");
              allLines[k].removeAttribute("style");
          }
          var allLinks = listContainer.querySelectorAll("a");
          for(var m=0; m<allLinks.length; m++) {
              allLinks[m].classList.remove("active-hl");
              allLinks[m].removeAttribute("style");
          }

          // 新しい行に赤文字・黄色背景を直接塗る
          activeLine.classList.add("active-hl");
          activeLine.style.setProperty("background-color", "#ffff00", "important");
          activeLine.style.setProperty("color", "red", "important");
          activeLine.style.setProperty("font-weight", "normal", "important");
          
          var newLinks = activeLine.querySelectorAll("a");
          for(var n=0; n<newLinks.length; n++) {
              newLinks[n].style.setProperty("color", "red", "important");
              newLinks[n].style.setProperty("text-decoration", "none", "important");
          }

          // スクロール処理
          if (listContainer.offsetParent !== null) {
              var containerRect = listContainer.getBoundingClientRect();
              var activeRect = activeLine.getBoundingClientRect();
              var targetScroll = listContainer.scrollTop + (activeRect.top - containerRect.top) - (listContainer.clientHeight / 2) + (activeLine.clientHeight / 2);
              listContainer.scrollTo({
                  top: targetScroll,
                  behavior: "smooth"
              });
          }
      }
    });
  })();
  </script>

  <style>
  .sc-vimeo-embed .iframe-wrapper {
    position: relative;
    padding-bottom: 56.25%;
    height: 0;
    overflow: visible;
  }
  .sc-vimeo-embed .iframe-wrapper iframe {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
  }

  /* 行梱包時の基本スタイル（干渉防止のためクラス内で指定） */
  .sc-vimeo-embed .imk-line {
      display: inline-block;
      width: 100%;
      border-radius: 2px;
      transition: background-color 0.1s;
  }

  /* ▼ 字幕一覧（details）の見た目調整 */
  .sc-vimeo-embed details > p {
    font-size: 14px !important;
    line-height: 1.6;
    height: 200px !important; 
    overflow: auto;
    background-color: #EDF7FF;
    padding: 2px 6px;
    margin: 0;
    box-shadow: 3px 3px 4px black;
    position: relative; 
  }
  .sc-vimeo-embed details > summary {
    font-size: 14px !important;
    padding: 2px 6px;
    width: 100%;
    background-color: #ddd;
    border: none;
    box-shadow: 3px 3px 4px black;
    cursor: pointer;
    list-style: none;
  }
  </style>
</div>



<div class="wp-block-cocoon-blocks-toggle-box-1 toggle-wrap toggle-box block-box not-nested-style cocoon-block-toggle"><input id="toggle-checkbox-202603290607120" class="toggle-checkbox" type="checkbox"/><label class="toggle-button" for="toggle-checkbox-202603290607120"><strong><span class="marker">コードと要約</span><span class="marker">(クリック)</span></strong></label><div class="toggle-content">
<figure class="wp-block-flexible-table-block-table"><table class="has-fixed-layout"><tbody><tr><td>要約：<br><strong>「VTT作成用に動画を分割する」ワークフロー</strong>は、ユーザーが指定した基本秒数を基に、動画を無音区間で自動分割し、VTTの「秒ずらし」に役立つオフセット記録を生成します。<code>ffmpeg</code>とPythonスクリプトを連携させ、再エンコードなしで高速分割します。<br><strong>「字幕時刻の秒ずらしを行う」ワークフロー</strong>は、選択したVTTファイルの全タイムスタンプを、ユーザーが入力した秒数だけシフト（前後にずらす）させ、新しいVTTファイルを出力します。<code>awk</code>を用いて精密な時間計算を行います。<br><strong>「VTTの連番を付け直す」ワークフロー</strong>は、VTTファイル内の古い連番や不要なヘッダを削除し、字幕ブロックに正しい連番を振り直して整理されたVTTファイルを生成します。<br>これらのワークフローは、それぞれ動画の分割、字幕の時間調整、字幕ファイルの整理という異なる課題に対応し、VTT作成・管理作業を自動化・簡素化することを目的としています。</td></tr><tr><td><a href="https://imakat.com/script_list/?pubtxt=無料アカウント向け_動画からVTTを生成する_pub.txt" target="_blank"><img decoding="async" src="https://imakat.com/rd.php?id=zrU95MiY.png" alt="" style="width:120px; height:auto;"></a></td></tr><tr><td><a rel="noopener" target="_blank" href="https://gemini.google.com/gem/12QYaKlTweyQxeHvd95UIxfLO7y0JOVOw?usp=sharing">添付の動画の音声から字幕用のvtt形式のコードを出力します(Gem共有)。<span class="fa fa-external-link external-icon anchor-icon"></span></a></td></tr><tr><td>公開スクリプトに関するお問い合わせは以下へ↓</td></tr><tr><td><a rel="noopener" target="_blank" href="https://gemini.google.com/gem/1Dp-NLSA5j5OXE3B1Ki2IaBRCvq4CGa6t?usp=sharing"><img decoding="async" src="https://imakat.com/rd.php?id=s7BMZHhB.png" alt="" style="width:120px; height:auto;"></a></td></tr></tbody></table></figure>
</div></div>



<h2 class="wp-block-heading">ポイント解説</h2>



<h3 class="wp-block-heading"><strong>解決すべき課題：Geminiの「5分の壁」</strong></h3>



<p>Geminiに動画を読み込ませて文字起こしをする際、特に無料アカウントでは「動画は5分以内」という制限があります 。 今回紹介するワークフローは、事前の「動画分割」と、事後の「タイムスタンプ調整（秒ずらし）」を組み合わせることで、この壁をストレスなく突破します。<br>有料会員でも使用制限を超えた時で、急いでいる場合などは、この方式は有用です。</p>



<h3 class="wp-block-heading"><strong><strong>長時間の動画からVTT字幕を生成する6つのステップ</strong></strong></h3>



<p>ここでは、例として10分弱の動画に字幕をつける手順を解説します 。</p>



<h4 class="wp-block-heading"><strong><strong>1. 動画の準備</strong></strong></h4>



<p>まずは手元に、字幕を作成したい元の動画ファイルを用意します 。</p>



<h4 class="wp-block-heading"><strong>2. 基本秒数の設定と動画の分割（クイックアクション）</strong></h4>



<p>動画を5分以内のパーツに分けます 。</p>



<ul class="wp-block-list">
<li>用意した動画を右クリックしクイックアクションから自作の「字幕用に動画を分割する」を選択します 。<br></li>



<li>分割する基本秒数を入力します（デフォルトは270秒＝4分30秒です） 。<br></li>



<li><strong>ポイント:</strong> 単純に時間でバサッと切るのではなく、指定時間付近の「1秒間の無音部分」を探し、キリの良いところで自動分割されるように工夫しています 。<br></li>



<li>実行すると、split_01、02…という分割動画と同時に、offsets.txtというファイルが生成されます 。このテキストには「245.130秒」といった分割時のズレ（累計秒数）が記録されており、後で使います 。<br></li>
</ul>



<h4 class="wp-block-heading"><strong><strong>3. Gemini（Gem）によるVTT字幕作成（手動）</strong></strong></h4>



<p>いよいよAIに文字起こしを依頼します。公開している専用のGemを使用します（※リンクは「コードと要約」に掲載します） 。</p>



<ul class="wp-block-list">
<li>Gemを開き、「Proモード」を選択します 。<br></li>



<li>下部のメッセージボックスに、分割した最初の動画（01）をドロップ（またはコピペ）して送信します 。<br></li>



<li>しばらく待つと、タイムスタンプ付きのVTTコードが出力されます 。<br></li>



<li>出力されたコードをコピーし、あらかじめ用意しておいた空のVTTファイル（例：01.vtt）にペーストして保存します 。<br></li>



<li><strong>注意点:</strong> 02以降の動画を処理する際は、AIが混乱するのを防ぐため、必ず「新しいセッション（チャット）」を開いてから同じ手順を繰り返してください 。<br></li>
</ul>



<h4 class="wp-block-heading"><strong>4. VTTの秒ずらし（クイックアクション）</strong></h4>



<p>出力された02番以降のVTTファイルは、動画の開始時刻が「0秒」にリセットされてしまっています 。これを元の時間軸に戻します。</p>



<ul class="wp-block-list">
<li>対象のVTTファイル（02など）を右クリックし、クイックアクションから「字幕時刻の秒ずらしを行う」を選びます 。<br></li>



<li>手順2で生成されたoffsets.txtに記載されている秒数（例：245.130）を入力し、OKを押します 。<br></li>



<li>正しい時間にシフトされた _timeshifted.vtt というファイルが新しく作成されます 。<br></li>
</ul>



<h4 class="wp-block-heading"><strong>5. VTTファイルの結合（手動）</strong></h4>



<p>それぞれの時間軸が整ったら、1つのファイルにまとめます。</p>



<ul class="wp-block-list">
<li>結合用の空ファイル（binded.vtt(※1)）を用意します 。<br></li>



<li>01.vttの中身をそのままペーストします 。<br></li>



<li>続けて、02_timeshifted.vtt、03_timeshifted.vtt の連番以降のテキスト部分を順番にコピーして下に追加し、保存します 。<br></li>
</ul>



<p>※1:あとで気付きました。bindedは間違った英語です。boundあるいはconbinedというのが適切です。</p>



<h4 class="wp-block-heading"><strong><strong><strong>6. 連番の振り直しと整理（クイックアクション）</strong></strong></strong></h4>



<p>結合したファイルは、字幕の通し番号（連番）が「1」から始まる箇所が複数混在してしまっています 。これを綺麗に整えます。</p>



<ul class="wp-block-list">
<li>binded.vtt を右クリックし、クイックアクションから「字幕VTTの連番を付け直す」を選択します 。<br></li>



<li>これにより、最初から最後まで番号が正しく振り直された完成版 renumbered.vtt が出来上がります 。<br></li>
</ul>



<h3 class="wp-block-heading">まとめ</h3>



<p>言葉遣いやテキストの微修正は、このように全体の形が綺麗に整った最後に行うのがおすすめです 。</p>



<p>地味にコツコツと進める手作業の部分もありますが、仕組みが非常に分かりやすいため、作業中のストレスはほとんど感じません 。<strong>Gem(ChatGPTもそう)の文字起こし精度が抜群に高いのは、単なる音声からの文字起こしだけでなく、動画内の視覚情報（映像）例えば、動画に登場する文字、人物の動きなどを認識するという、マルチモーダル処理を行っているため</strong>です。26年3月現在、CapCut、Vrew、Premiere Pro、Davinci Resolve、Filmoraの文字起こしは「映像情報」を加味せず、純粋に「音声データ」のみに依存しています。おかげで、Gemの場合は、手直しが非常に少なくて済むのが最大のメリットです 。</p>



<p>以上</p>



<p></p>
]]></content:encoded>
					
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">28762</post-id>	</item>
		<item>
		<title>デジタル成果物を効率的に管理・配信する2つの自作システム</title>
		<link>https://imakat.com/2026/03/16/28734/</link>
		
		<dc:creator><![CDATA[imakat]]></dc:creator>
		<pubDate>Mon, 16 Mar 2026 05:26:51 +0000</pubDate>
				<category><![CDATA[デジタル]]></category>
		<category><![CDATA[ものづくり]]></category>
		<category><![CDATA[マイライブラリ]]></category>
		<category><![CDATA[棚田式ファイル管理]]></category>
		<category><![CDATA[メタデータ]]></category>
		<guid isPermaLink="false">https://imakat.com/?p=28734</guid>

					<description><![CDATA[今回は、動画や画像といった「デジタル成果物」を効率よく生成し、スムーズに流通（配信・管理）させるための独自の仕組みについてお話しします。 日々増え続ける大容量データをどのように安全かつ柔軟に扱うか、悩んでいる方も多いので [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>今回は、動画や画像といった「デジタル成果物」を効率よく生成し、スムーズに流通（配信・管理）させるための独自の仕組みについてお話しします。</p>



<p>日々増え続ける大容量データをどのように安全かつ柔軟に扱うか、悩んでいる方も多いのではないでしょうか？</p>



<p>詳しい解説は以下の動画にまとめていますので、まずはご覧ください。</p>


<div class="sc-dynamic-embed">
  <style>
  /* リンクの見た目を整える */
  .sc-dynamic-embed .sc-link-container { 
      display: flex; 
      margin-bottom: 10px; 
      flex-wrap: wrap; 
      align-items: center; 
  }
  .sc-dynamic-embed .sc-link { margin-bottom: 0; }
  .sc-dynamic-embed .sc-link a {
    font-size: 15px; /* dynamicと合わせた基本サイズ */
    font-weight: normal;
    text-decoration: underline;
    color: #0073aa;
  }
  .sc-dynamic-embed .sc-link a:hover { text-decoration: none; color: #000; }
  
  /* ★スマホ画面（幅500px以下）の時は文字を縮小して統一感を出す */
  @media (max-width: 500px) {
    .sc-dynamic-embed .sc-link a { font-size: 13px; }
  }

  /* 行梱包時の基本スタイル */
  .sc-dynamic-embed .imk-line {
      display: inline-block;
      width: 100%;
      border-radius: 2px;
      transition: background-color 0.1s;
  }

  /* 古い枠内字幕ボックスを強制消去 */
  .sc-dynamic-embed #subtitleOverlay,
  .sc-dynamic-embed #scSubtitleOverlay,
  .sc-dynamic-embed .overlay-cue,
  .sc-dynamic-embed .band {
      display: none !important;
      opacity: 0 !important;
      visibility: hidden !important;
      pointer-events: none !important;
  }
  </style>

  <div class="sc-link-container">
    <p class="sc-link">
      <a href="https://imakat.com/ds62/?drid=75" target="_blank"
         onclick="return scStopAndGo(event, this);">
        👉低画質・枠外字幕はこちら
      </a>
    </p>
  </div>

  <style>
            :root{ --dr5emd-max: 1920px; }
            .video-wrap{position:relative;width:100%;margin:0 auto}
            figure.wp-block-video.aligncenter{
              width:100%;
              max-width:min(var(--dr5emd-max, 1920px), 98vw);
              margin:0 auto;
            }
            #subtitleOverlay{
              position:absolute; left:0; right:0; bottom:6%;
              padding:0 2%; text-align:center; pointer-events:none; z-index:2;
            }
            #subtitleOverlay .band{
              display:inline-block; background:rgba(0,0,0,0.35);
              padding:6px 10px; border-radius:8px; max-width:96%;
              margin:0 auto; box-shadow:0 1px 2px rgba(0,0,0,0.15);
            }
            #subtitleOverlay .overlay-cue{
              color:#fff; font-weight:600;
              font-size:clamp(16px, 3.6vw, 32px);
              line-height:1.32; white-space:pre-wrap; margin:2px 0;
              -webkit-text-stroke:.6px rgba(0,0,0,.7);
              text-shadow:-1px -1px 0 rgba(0,0,0,.6), 1px -1px 0 rgba(0,0,0,.6),
                          -1px  1px 0 rgba(0,0,0,.6), 1px  1px 0 rgba(0,0,0,.6);
            }
            @media (max-width:430px){
              #subtitleOverlay .overlay-cue{ font-size:clamp(16px, 4.2vw, 22px); }
            }
            .dr5emd-sublist details > p{
              height:200px; overflow:auto; background-color:#EDF7FF;
              padding:2px 6px; margin:0; box-shadow:3px 3px 4px black;
              position: relative;
            }
            .dr5emd-sublist details > summary{
              padding:2px 6px; width:100%;
              background-color:#ddd; border:none;
              box-shadow:3px 3px 4px black; cursor:pointer; list-style:none;
            }
            /* ▼ 自動スクロール時のハイライト（文字の太さを標準へ変更） */
            .active-hl {
                background-color: #ffff00 !important;
                color: #ff0000 !important;
                font-weight: normal; /* 標準の太さ */
                border-bottom: 2px solid red;
                display: inline-block;
                border-radius: 2px;
            }
            </style><div class="dr5emd-container"><figure class="wp-block-video aligncenter"><div class="video-wrap"><video id="myVideo" controls controlsList="nodownload" poster="https://imakat.com/rd.php?id=nBmS0llp.png" playsinline preload="metadata" style="width:100%;height:auto;">  <source src="https://imakat.com/rd.php?id=0D4FHSGD.mp4" type="video/mp4">  <track src="https://imakat.com/rd.php?id=4rjGEGI3.vtt" label="日本語" srclang="ja" kind="subtitles"></video><div id="subtitleOverlay" aria-hidden="true"></div></div><script>
document.addEventListener("DOMContentLoaded", function(){
  var video=document.getElementById("myVideo");
  var trackEl=video?video.querySelector("track[kind='subtitles'], track[kind='captions']"):null;
  var overlay=document.getElementById("subtitleOverlay"); if(!video||!overlay) return;
  video.addEventListener("contextmenu", function(e){ e.preventDefault(); return false; }, false);
  function setNative(mode){
    try{
      if(video.textTracks && video.textTracks.length){
        for(var i=0;i<video.textTracks.length;i++){ video.textTracks[i].mode = mode; }
      }
      if(trackEl && trackEl.track) trackEl.track.mode = mode;
    }catch(e){}
  }
  var isOverlay=true,lastSig="";
  function sig(active){if(!active||active.length===0)return"";var a=[];for(var i=0;i<active.length;i++){var c=active[i];a.push([c.startTime,c.endTime,c.text].join("|"));}return a.join("||");}
  function cueLine(c){var d=document.createElement("div");d.className="overlay-cue";d.setAttribute("translate","yes");if(typeof c.getCueAsHTML==="function")d.appendChild(c.getCueAsHTML());else d.textContent=c.text;return d;}
  function render(){
    if(!isOverlay || !trackEl || !trackEl.track) return;
    var ac=trackEl.track.activeCues,s=sig(ac); if(s===lastSig) return; lastSig=s;
    overlay.innerHTML=""; if(!ac || ac.length===0) return;
    var b=document.createElement("div"); b.className="band"; b.setAttribute("translate","yes");
    Array.from(ac).sort(function(a,b){return a.startTime-b.startTime;}).forEach(function(c){b.appendChild(cueLine(c));});
    overlay.appendChild(b);
  }
  function useOverlay(){isOverlay=true;overlay.style.display="";setNative("hidden");lastSig="";render();}
  function useNative(){isOverlay=false;overlay.style.display="none";setNative("showing");lastSig="";}
  useOverlay();
  if(trackEl){
    if(trackEl.track){ try{ trackEl.track.addEventListener("cuechange",render); }catch(e){} }
    trackEl.addEventListener("load", function(){ try{ if(trackEl.track) trackEl.track.addEventListener("cuechange",render); }catch(e){} render(); });
  }
  video.addEventListener("loadedmetadata",render);
  function handleWebkitMode(){ var m = video.webkitPresentationMode || "inline"; (m==="picture-in-picture"||m==="fullscreen") ? useNative() : useOverlay(); }
  if("webkitPresentationMode" in video){ video.addEventListener("webkitpresentationmodechanged",handleWebkitMode); handleWebkitMode(); }
  if("webkitCurrentPlaybackTargetIsWireless" in video){
    video.addEventListener("webkitcurrentplaybacktargetiswirelesschanged", function(){ video.webkitCurrentPlaybackTargetIsWireless ? useNative() : useOverlay(); });
  }
  if("pictureInPictureEnabled" in document){
    video.addEventListener("enterpictureinpicture",useNative);
    video.addEventListener("leavepictureinpicture",useOverlay);
  }
  document.addEventListener("fullscreenchange", function(){
    var fs=document.fullscreenElement;
    if(!fs) return useOverlay();
    (fs===video || (fs && fs.contains && fs.contains(video))) ? useNative() : useOverlay();
  });
});
</script>
                <figcaption></figcaption></figure><div class="dr5emd-sublist"><details translate="yes"><summary translate="yes">字幕一覧(クリック)</summary> <p translate="yes">
(<a href="#" class="imk-cue" data-seek="0:00" translate="no">00:00:00</a>)  デジタル成果物の生成と流通<br>
(<a href="#" class="imk-cue" data-seek="0:06" translate="no">00:00:06</a>)  それではですね、デジタル成果物の生成と流通ということについて、お話します。<br>
(<a href="#" class="imk-cue" data-seek="0:17" translate="no">00:00:17</a>)  今日はですね、2つのシステムについて説明します。<br>
(<a href="#" class="imk-cue" data-seek="0:23" translate="no">00:00:23</a>)  1つはですね、マイライブラリですね。もう1つは棚田式ファイル管理です。<br>
(<a href="#" class="imk-cue" data-seek="0:32" translate="no">00:00:32</a>)  で、マイライブラリですけれども、これは以前にも一度紹介してますが<br>
(<a href="#" class="imk-cue" data-seek="0:41" translate="no">00:00:41</a>)  Dropboxによる製造工場、この中で作られたメディア配信物、<br>
(<a href="#" class="imk-cue" data-seek="0:50" translate="no">00:00:50</a>)  動画だとか画像、VTT、書類、いわゆるウェブ配信で使う素材ですね。<br>
(<a href="#" class="imk-cue" data-seek="1:00" translate="no">00:01:00</a>)  工場からすれば成果物、配信用の成果物を作って、それを動画・画像配信所に渡して、<br>
(<a href="#" class="imk-cue" data-seek="1:10" translate="no">00:01:10</a>)  リクエストに応じて、それを表示するという形ですが、<br>
(<a href="#" class="imk-cue" data-seek="1:17" translate="no">00:01:17</a>)  その時に動画配信所はですね、いくつも複数用意できるという仕組み、<br>
(<a href="#" class="imk-cue" data-seek="1:25" translate="no">00:01:25</a>)  複数用意した配信所を自由自在に切り替えれるという仕組みですね、それを作ったわけです。<br>
(<a href="#" class="imk-cue" data-seek="1:33" translate="no">00:01:33</a>)  それともう一つの棚田式ファイル管理システムというのはですね、ファイル管理のシステムです。<br>
(<a href="#" class="imk-cue" data-seek="1:41" translate="no">00:01:41</a>)  ただ、通常言われるNASによるファイル管理というのはですね、<br>
(<a href="#" class="imk-cue" data-seek="1:49" translate="no">00:01:49</a>)  同じサイズのハードディスクを2つ用意して置かなきゃいけないと。RAID 1の場合ですね。<br>
(<a href="#" class="imk-cue" data-seek="1:57" translate="no">00:01:57</a>)  しかもそのフォーマットが特殊なフォーマットになってるので、<br>
(<a href="#" class="imk-cue" data-seek="2:05" translate="no">00:02:05</a>)  そのハードディスクだけ引き抜いて、パソコンにガチャンと付けてデータを読むとか、<br>
(<a href="#" class="imk-cue" data-seek="2:14" translate="no">00:02:14</a>)  そういうことができないわけですね。一番いいのは、いつもMacやパソコンに付けている<br>
(<a href="#" class="imk-cue" data-seek="2:22" translate="no">00:02:22</a>)  ハードディスクやSSDと同じように、パコッと抜いて、<br>
(<a href="#" class="imk-cue" data-seek="2:30" translate="no">00:02:30</a>)  他のパソコンやMacに挿してすぐデータが読める状態になっていれば一番いいですよね。<br>
(<a href="#" class="imk-cue" data-seek="2:38" translate="no">00:02:38</a>)  そういう形にしたいということ、つまり、簡単にデータを移行して、<br>
(<a href="#" class="imk-cue" data-seek="2:46" translate="no">00:02:46</a>)  それをそのまま別に転用ができるということですね。そういう使い方がしたいわけですよ。<br>
(<a href="#" class="imk-cue" data-seek="2:54" translate="no">00:02:54</a>)  ハードディスクが高価になっていくというところに対応していく必要もあるし、<br>
(<a href="#" class="imk-cue" data-seek="3:02" translate="no">00:03:02</a>)  使わずに残っているハードディスクを有効活用するということもあるわけですね。<br>
(<a href="#" class="imk-cue" data-seek="3:09" translate="no">00:03:09</a>)  自由にデバイスをファイル管理用に使えるという仕組みを作ったわけです。<br>
(<a href="#" class="imk-cue" data-seek="3:18" translate="no">00:03:18</a>)  それが棚田式ファイル管理システムです。構造的には一番上に作業段、<br>
(<a href="#" class="imk-cue" data-seek="3:26" translate="no">00:03:26</a>)  通常作業するために使う工場のような場所、それから完成したものを保存する保管段、<br>
(<a href="#" class="imk-cue" data-seek="3:34" translate="no">00:03:34</a>)  さらに保管段のうち、長期になったものを長期保管段に移行する仕組みを作りました。<br>
(<a href="#" class="imk-cue" data-seek="3:43" translate="no">00:03:43</a>)  この2つのシステムは両方ともローカルのMac mini上で動いているという優れものでございます。<br>
(<a href="#" class="imk-cue" data-seek="3:55" translate="no">00:03:55</a>)  このマイライブラリと棚田式ファイル管理の共通点なんですけども、これはこういうことなんです。<br>
(<a href="#" class="imk-cue" data-seek="4:11" translate="no">00:04:11</a>)  両方ともメタデータの変更に基づき、実体データを操作するという特徴があります。<br>
(<a href="#" class="imk-cue" data-seek="4:26" translate="no">00:04:26</a>)  棚田システムの方は、メタデータを移動することによるシミュレーションを何回もやって、<br>
(<a href="#" class="imk-cue" data-seek="4:35" translate="no">00:04:35</a>)  その各デバイスの容量、中身をコントロールするということをシミュレーションしていくと。<br>
(<a href="#" class="imk-cue" data-seek="4:45" translate="no">00:04:45</a>)  そのことによって実体データの移動のミステイクを事前に防止してるわけです。<br>
(<a href="#" class="imk-cue" data-seek="4:52" translate="no">00:04:52</a>)  それともう一つのマイライブラリの方ですけれども、これはメタデータ、動画や画像の属性ですね、<br>
(<a href="#" class="imk-cue" data-seek="5:01" translate="no">00:05:01</a>)  それをスプレッドシートの中に登録していきます。で、その属性に応じて、<br>
(<a href="#" class="imk-cue" data-seek="5:09" translate="no">00:05:09</a>)  ファイルが移動したり、再生サーバーを変更したり、そうした操作が行われると。<br>
(<a href="#" class="imk-cue" data-seek="5:16" translate="no">00:05:16</a>)  いう仕組みですね。<br>
(<a href="#" class="imk-cue" data-seek="5:24" translate="no">00:05:24</a>)  メタデータの変更に基づき実体データを操作するというところが、この2つのシステムの共通点です。<br>
</p> </details>
<style>
details { font: 16px "Open Sans", Calibri, sans-serif; width: 100%; }
details > summary { padding: 2px 6px; width: 100%; background-color: #ddd; border: none; box-shadow: 3px 3px 4px black; cursor: pointer; list-style: none; }
details > p { font: 14px "Open Sans", Calibri, sans-serif; height:150px; overflow: scroll; background-color: #EDF7FF; padding: 2px 6px; margin: 0; box-shadow: 3px 3px 4px black; }
</style>
</div><script>
(function(){
  var root=document.querySelector(".dr5emd-sublist");
  var video=document.getElementById("myVideo");
  if(!root || !video) return;
  function parseTs(ts){
    if(!ts) return null;
    var p=ts.trim().split(":").map(function(x){return parseInt(x,10)||0;});
    if(p.length===2) return p[0]*60 + p[1];
    if(p.length===3) return p[0]*3600 + p[1]*60 + p[2];
    return null;
  }
  root.addEventListener("click", function(e){
    var a=e.target.closest && e.target.closest("a.imk-cue[data-seek]");
    if(!a || !root.contains(a)) return;
    e.preventDefault();
    var sec = parseTs(a.getAttribute("data-seek"));
    if(sec==null) return;
    try{ video.currentTime = sec; if(video.paused) video.play(); }catch(_){}
  });
  video.addEventListener("timeupdate", function(){
    var listContainer = root.querySelector("details > p");
    if(!listContainer) return;
    var cues = listContainer.querySelectorAll("a.imk-cue");
    if(cues.length === 0) return;
    var cur = video.currentTime;
    var active = null;
    for(var i=0; i<cues.length; i++){
        var t = parseTs(cues[i].getAttribute("data-seek"));
        if(t !== null && cur >= t - 0.5){
            active = cues[i];
        } else if(t > cur){
            break;
        }
    }
    if(active){
        if(active.classList.contains("active-hl")) return;
        var old = listContainer.querySelectorAll(".active-hl");
        for(var k=0; k<old.length; k++) old[k].classList.remove("active-hl");
        active.classList.add("active-hl");
        if(listContainer.offsetParent !== null){
            var containerRect = listContainer.getBoundingClientRect();
            var activeRect = active.getBoundingClientRect();
            var targetScroll = listContainer.scrollTop + (activeRect.top - containerRect.top) - (listContainer.clientHeight / 2) + (active.clientHeight / 2);
            listContainer.scrollTo({ top: targetScroll, behavior: "smooth" });
        }
    }
  });
})();
</script>
                </div>

  <script>
  (function(){
    var me = document.currentScript;
    var wrapper = me ? me.closest('.sc-dynamic-embed') : null;

    /* -----------------------------------------------
       1. 動画の保護機能（右クリック禁止・DL防止）
       ----------------------------------------------- */
    function protectVideo() {
      var target = wrapper ? wrapper : document;
      var mediaEls = target.querySelectorAll('video');
      mediaEls.forEach(function(v){
        if(v.dataset.protected === 'true') return;
        v.dataset.protected = 'true';
        v.setAttribute('controlsList', 'nodownload');
        v.oncontextmenu = function() { return false; };
        v.addEventListener('contextmenu', function(e){ e.preventDefault(); return false; }, false);
      });
    }

    /* -----------------------------------------------
       2. 字幕制御＆ハイライト機能（iPhone全画面 完全対応版）
       ----------------------------------------------- */
    function initSubtitles() {
      var target = wrapper ? wrapper : document;
      var video = target.querySelector('video');
      var listContainer = target.querySelector('details > p');
      
      if (!video || !listContainer) return false; 

      if (video.dataset.subInit === 'true') return true; 
      video.dataset.subInit = 'true';

      var oldOverlay = target.querySelector('#subtitleOverlay') || target.querySelector('#scSubtitleOverlay');
      if (oldOverlay) {
          oldOverlay.style.setProperty('display', 'none', 'important');
          oldOverlay.innerHTML = ''; 
      }

      function isSpecialMode() {
        var isFs = !!(document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement);
        var isPiP = !!(document.pictureInPictureElement && document.pictureInPictureElement === video) || (video.webkitPresentationMode === "picture-in-picture");
        var isIOSFs = !!video.webkitDisplayingFullscreen; 
        return isFs || isPiP || isIOSFs;
      }

      try {
        if(video.textTracks && video.textTracks.length > 0){
          for(var i=0; i<video.textTracks.length; i++){
             if(video.textTracks[i].kind === 'subtitles' || video.textTracks[i].kind === 'captions'){
                 video.textTracks[i].mode = "hidden";
             }
          }
        }
      } catch(e){}

      video.addEventListener("webkitbeginfullscreen", function() {
          try {
              if(video.textTracks && video.textTracks.length > 0) {
                  for(var i=0; i<video.textTracks.length; i++){
                      if(video.textTracks[i].kind === 'subtitles' || video.textTracks[i].kind === 'captions') video.textTracks[i].mode = "showing";
                  }
              }
          } catch(e){}
      });
      video.addEventListener("webkitendfullscreen", function() {
          try {
              if(video.textTracks && video.textTracks.length > 0) {
                  for(var i=0; i<video.textTracks.length; i++){
                      if(video.textTracks[i].kind === 'subtitles' || video.textTracks[i].kind === 'captions') video.textTracks[i].mode = "hidden";
                  }
              }
          } catch(e){}
      });

      var detailsEl = target.querySelector("details");
      if (detailsEl) {
          detailsEl.open = true; 
          var summaryEl = detailsEl.querySelector("summary");
          if (summaryEl) summaryEl.textContent = "字幕(シーン)はここをクリック";
      }

      if (!listContainer.dataset.formatted) {
          var html = listContainer.innerHTML;
          var lines = html.split(/<br\s*\/?>/i);
          var newHtml = "";
          for(var j=0; j<lines.length; j++) {
              if(lines[j].trim() === "") continue;
              newHtml += "<span class='imk-line'>" + lines[j] + "</span><br>";
          }
          listContainer.innerHTML = newHtml;
          listContainer.dataset.formatted = "true";
      }

      function parseTs(ts){
        if(!ts) return null;
        var p = ts.trim().split(":").map(function(x){return parseInt(x,10)||0;});
        if(p.length===2) return p[0]*60 + p[1];
        if(p.length===3) return p[0]*3600 + p[1]*60 + p[2];
        return null;
      }

      var rootSublist = target.querySelector(".dr5-sublist") || listContainer.parentElement;
      if (rootSublist) {
        rootSublist.addEventListener("click", function(e){
          var a = e.target.closest && e.target.closest("a.imk-cue[data-seek]");
          if(!a) return;
          e.preventDefault();
          var sec = parseTs(a.getAttribute("data-seek"));
          if(sec==null) return;
          try{ video.currentTime = sec; if(video.paused) video.play(); }catch(_){}
        });
      }

      video.addEventListener("timeupdate", function(){
        var desiredMode = isSpecialMode() ? "showing" : "hidden";
        try {
            if(video.textTracks && video.textTracks.length > 0){
                for(var i=0; i<video.textTracks.length; i++){
                    if((video.textTracks[i].kind === 'subtitles' || video.textTracks[i].kind === 'captions') && video.textTracks[i].mode !== desiredMode) {
                        video.textTracks[i].mode = desiredMode;
                    }
                }
            }
        } catch(e){}

        var cues = listContainer.querySelectorAll("a.imk-cue");
        if(cues.length === 0) return;
        var cur = video.currentTime;
        var activeA = null;

        for(var i=0; i<cues.length; i++){
            var t = parseTs(cues[i].getAttribute("data-seek"));
            if(t !== null && cur >= t - 0.5){ activeA = cues[i]; } 
            else if(t > cur){ break; }
        }

        if(activeA){
            var activeLine = activeA.closest(".imk-line");
            if(!activeLine) activeLine = activeA;

            if(activeLine.classList.contains("active-hl")) return;

            var allLines = listContainer.querySelectorAll(".imk-line");
            for(var k=0; k<allLines.length; k++) {
                allLines[k].classList.remove("active-hl");
                allLines[k].removeAttribute("style"); 
            }
            var allLinks = listContainer.querySelectorAll("a");
            for(var m=0; m<allLinks.length; m++) {
                allLinks[m].classList.remove("active-hl");
                allLinks[m].removeAttribute("style"); 
            }

            activeLine.classList.add("active-hl");
            activeLine.style.setProperty("background-color", "#ffff00", "important");
            activeLine.style.setProperty("color", "red", "important");
            activeLine.style.setProperty("font-weight", "normal", "important");
            
            var newLinks = activeLine.querySelectorAll("a");
            for(var n=0; n<newLinks.length; n++) {
                newLinks[n].style.setProperty("color", "red", "important");
                newLinks[n].style.setProperty("text-decoration", "none", "important");
            }

            if(listContainer.offsetParent !== null){
                var containerRect = listContainer.getBoundingClientRect();
                var activeRect = activeLine.getBoundingClientRect();
                var targetScroll = listContainer.scrollTop + (activeRect.top - containerRect.top) - (listContainer.clientHeight / 2) + (activeLine.clientHeight / 2);
                listContainer.scrollTo({ top: targetScroll, behavior: "smooth" });
            }
        }
      });

      return true;
    }

    /* -----------------------------------------------
       監視タイマー
       ----------------------------------------------- */
    var checks = 0;
    var checkTimer = setInterval(function(){
      protectVideo(); // ★dynamic2は保護を維持
      var success = initSubtitles();
      checks++;
      if (success || checks > 20) { 
        clearInterval(checkTimer);
      }
    }, 500); 

    /* -----------------------------------------------
       3. 画面遷移時の停止機能
       ----------------------------------------------- */
    if (!window.scStopAndGo) {
      window.scStopAndGo = function(event, link){
        try{
          var mediaEls = document.querySelectorAll('video, audio');
          mediaEls.forEach(function(m){
            try{
              if (!m.paused) m.pause();
              if (document.pictureInPictureElement === m && document.exitPictureInPicture) {
                document.exitPictureInPicture().catch(function(){});
              }
            }catch(e){}
          });
        }finally{
          event.preventDefault();
          setTimeout(function(){
            if (link.target === '_blank') {
              window.open(link.href, '_blank');
            } else {
              window.location.href = link.href;
            }
          }, 50);
        }
        return false;
      };
    }
  })();
  </script>
</div>



<h3 class="wp-block-heading"><strong>動画で紹介している2つのシステム</strong></h3>



<p>動画内で解説しているのは、私がローカルの <strong>Mac mini</strong> 上で構築・稼働させている以下の2つのシステムです。</p>



<h4 class="wp-block-heading"><strong>1. 配信を自由自在に操る「マイライブラリ」</strong></h4>



<p>Dropboxをデータの「製造工場」として活用し、ウェブ配信用の素材（動画、画像、VTT字幕、書類など）をシームレスに配信所へ渡すシステムです。</p>



<ul class="wp-block-list">
<li><strong>複数の配信所を管理:</strong> 複数の配信サーバーを用意しておき、リクエストや用途に応じて自由自在に配信先を切り替えることが可能です。</li>
</ul>



<h4 class="wp-block-heading"><strong>2. NASの弱点を克服する「棚田式ファイル管理システム」</strong></h4>



<p>一般的なNAS（RAID構成など）は、専用のフォーマットが必要なため「HDDを直接取り外して別のPCですぐに読み込む」といった物理的な使い回しが難しいという弱点があります。そこを解決したのがこのシステムです。</p>



<ul class="wp-block-list">
<li><strong>抜き差ししてすぐ使える:</strong> 一般的な外付けHDDやSSDと同じ感覚で、パコッと抜いて他のPCやMacに挿せば、すぐにデータを読み込めます。</li>



<li><strong>階層型（棚田式）の管理:</strong> データを「作業段」「保管段」「長期保管段」という3つの階層に分けて管理。使わず眠っている古いハードディスクも有効活用できます。</li>
</ul>



<h3 class="wp-block-heading"><strong>2つのシステムに共通する「最大の秘密」</strong></h3>



<p>用途の異なるこの2つのシステムですが、設計の根底には共通する重要なコア技術があります。それは、**「メタデータの変更に基づき、実体データを操作する」**という仕組みです。</p>



<ul class="wp-block-list">
<li><strong>棚田式システムでの活用:</strong><br>いきなり重い実体データを動かすのではなく、まずは「メタデータ」だけを移動させるシミュレーションを何度も実行します。これにより、移動先の容量オーバーや操作ミスを事前に防ぐことができます。</li>



<li><strong>マイライブラリでの活用:</strong><br>スプレッドシートに登録した「属性（メタデータ）」を書き換えるだけで、それに連動して実際のファイルが自動で移動したり、再生サーバーが切り替わったりします。<br><br>もともとマイライブラリは、図書館をイメージしていて、図書館の本は質量のある物体のため、それぞれの図書館に受付窓口が必要になりますが、メディアアセットは、質量のない、ほぼ瞬間コピー、瞬間移動ができる存在であるため、図書館カードのようなメタデータは１箇所に集中することができるわけです。その１箇所でメタデータを変更することにより実体データを操作できるわけですね。</li>
</ul>



<h3 class="wp-block-heading"><strong>まとめ</strong></h3>



<p>大容量のハードディスクが高価になっている今、手元にあるストレージをいかに安全・確実、そして無駄なく使い切るかは大きなテーマです。</p>



<p>「メタデータによる実体データの制御」を取り入れることで、日々のクリエイティブ作業やデータ管理は驚くほど快適になります。ご自身のファイル管理や配信のヒントにしてみてください。</p>
]]></content:encoded>
					
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">28734</post-id>	</item>
		<item>
		<title>自作パペット（アバター）を作ってみました〜その1〜脱Adobeを目指して！</title>
		<link>https://imakat.com/2026/03/12/28692/</link>
		
		<dc:creator><![CDATA[imakat]]></dc:creator>
		<pubDate>Thu, 12 Mar 2026 02:19:50 +0000</pubDate>
				<category><![CDATA[デジタル]]></category>
		<category><![CDATA[ものづくり]]></category>
		<category><![CDATA[Automator]]></category>
		<category><![CDATA[Gemini]]></category>
		<category><![CDATA[Adobe character animator]]></category>
		<category><![CDATA[パペット]]></category>
		<guid isPermaLink="false">https://imakat.com/?p=28692</guid>

					<description><![CDATA[今回は、これからの動画作成で活躍してくれる**「新しく作った自作のパペット（アバター）」**について紹介したいと思います。これまで私の動画には、画面の左下に「Adobe Dr. Apple Smith」というキャラクター [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>今回は、これからの動画作成で活躍してくれる**「新しく作った自作のパペット（アバター）」**について紹介したいと思います。<br>これまで私の動画には、画面の左下に「Adobe Dr. Apple Smith」というキャラクターが頻繁に登場していました。顔の向きに合わせて首が動き、目や口もスムーズに動く非常に素晴らしいパペットだったのですが……実はそろそろ、彼から「卒業」をしたいと思います。</p>



<p>動画をご覧ください。</p>


<div class="sc-vimeo-embed">
  <p style="font-size: 14px;" class="sc-link">
    <a href="https://imakat.com/vm5?movid=1173521971" target="_blank" onclick="stopVimeoBeforeNavigate(event, this)">
      動画を別ページで表示(ここをクリック)
    </a>
  </p>

  <div class="iframe-wrapper">
    <iframe
      src="https://player.vimeo.com/video/1173521971?title=0&byline=0&portrait=0&controls=1&speed=1&texttrack=ja&dnt=1&loop=0"
      frameborder="0"
      allow="autoplay; fullscreen; picture-in-picture"
      allowfullscreen></iframe>
  </div>

  <div class="dr-vimeo-sublist">
    <details translate="yes"><summary translate="yes">字幕一覧(クリック)</summary> <p translate="yes">
(<a href="#" class="imk-cue" data-seek="0:06" translate="no">00:00:06</a>)  それでは今回ですね、新しく作った自作のパペットを紹介していこうと思うんですけども<br>
(<a href="#" class="imk-cue" data-seek="0:16" translate="no">00:00:16</a>)  私今までですね、動画を作る時に頻繁に登場してたのが、この左端にいますですね<br>
(<a href="#" class="imk-cue" data-seek="0:27" translate="no">00:00:27</a>)  Adobe Dr. Apple SmithというAdobeのキャラクターです。パペットです。<br>
(<a href="#" class="imk-cue" data-seek="0:37" translate="no">00:00:37</a>)  で、これものすごく素晴らしいんですよね。顔の向きに応じて、首もゆっくり動きますし<br>
(<a href="#" class="imk-cue" data-seek="0:48" translate="no">00:00:48</a>)  口も目も非常にスムーズに動くという優れものなんですけども<br>
(<a href="#" class="imk-cue" data-seek="0:54" translate="no">00:00:54</a>)  ただ問題がありまして、それはこのAdobeのですね、サブスクに入らないと使えないんですよ。<br>
(<a href="#" class="imk-cue" data-seek="1:05" translate="no">00:01:05</a>)  これがですね、一番安い組み合わせでもってしても月1200円。<br>
(<a href="#" class="imk-cue" data-seek="1:15" translate="no">00:01:15</a>)  それはなかなか今のご時世、このキャラクター使うだけで<br>
(<a href="#" class="imk-cue" data-seek="1:23" translate="no">00:01:23</a>)  で、その他のAdobeのサービスというのはですね、画像加工したり色々面白みはあるんですけども<br>
(<a href="#" class="imk-cue" data-seek="1:33" translate="no">00:01:33</a>)  今や生成AIを使うとほとんど似たようなことができるんですね。<br>
(<a href="#" class="imk-cue" data-seek="1:41" translate="no">00:01:41</a>)  だからもう生成AIがあれば、メディアの加工もできるし、プログラム作成もできますしね。<br>
(<a href="#" class="imk-cue" data-seek="1:51" translate="no">00:01:51</a>)  色んなことが可能なんです。だからそれは非常にもったいない。<br>
(<a href="#" class="imk-cue" data-seek="1:58" translate="no">00:01:58</a>)  だからできればこのAdobeのサービスを停止して<br>
(<a href="#" class="imk-cue" data-seek="2:07" translate="no">00:02:07</a>)  もう自前で作ったこのパペットですね。Adobeと比べれば見劣りするんですけどもね。<br>
(<a href="#" class="imk-cue" data-seek="2:16" translate="no">00:02:16</a>)  まあそれでもこのように目も動きますし、口も動くしで、首も多少動く。<br>
(<a href="#" class="imk-cue" data-seek="2:23" translate="no">00:02:23</a>)  まあそんな形でですね、使い物にはなんとかなってるかなと思うんですよ。ここは割り切りですね。<br>
(<a href="#" class="imk-cue" data-seek="2:32" translate="no">00:02:32</a>)  ええまあ、この自前のパペットでですね、これからやっていこうと思ってます。<br>
(<a href="#" class="imk-cue" data-seek="2:39" translate="no">00:02:39</a>)  ええと、それではですね、今回作ったお手製のパペットですね、を紹介していきたいと思います。<br>
(<a href="#" class="imk-cue" data-seek="2:49" translate="no">00:02:49</a>)  まだ名前はないんですが、まあimakatブログで使っていこうと思うキャラクターですね。<br>
(<a href="#" class="imk-cue" data-seek="2:59" translate="no">00:02:59</a>)  いまかっちゃん、変だね。なんだろうね。なんかいい名前があるといいんですけどね。<br>
(<a href="#" class="imk-cue" data-seek="3:11" translate="no">00:03:11</a>)  まあ、とにかくですね、今から紹介していきたいと思います。<br>
(<a href="#" class="imk-cue" data-seek="3:16" translate="no">00:03:16</a>)  で、基本的には作るのはですね、Google Geminiですね。<br>
(<a href="#" class="imk-cue" data-seek="3:25" translate="no">00:03:25</a>)  Google Geminiを使ってですね、Geminiにプログラムを書いてもらうと。<br>
(<a href="#" class="imk-cue" data-seek="3:34" translate="no">00:03:34</a>)  いうことをして進めます。これが非常に賢いんでね。<br>
(<a href="#" class="imk-cue" data-seek="3:41" translate="no">00:03:41</a>)  こちらがその日本語で質問していけば、どんどん作ってくれるという代物でございます。<br>
(<a href="#" class="imk-cue" data-seek="3:49" translate="no">00:03:49</a>)  で、それに従ってですね、あのMacの中にフォルダを用意します。<br>
(<a href="#" class="imk-cue" data-seek="3:58" translate="no">00:03:58</a>)  フォルダとファイルを用意するということが必要になります。<br>
(<a href="#" class="imk-cue" data-seek="4:03" translate="no">00:04:03</a>)  mypuppet_systemっていうのを作ってですね、その中にavatar_module<br>
(<a href="#" class="imk-cue" data-seek="4:13" translate="no">00:04:13</a>)  その中にrun_puppet.py、これが動かすためのPythonプログラムです。<br>
(<a href="#" class="imk-cue" data-seek="4:20" translate="no">00:04:20</a>)  で、assets、これが動かす対象となる画像ですね、それが入ってます。<br>
(<a href="#" class="imk-cue" data-seek="4:30" translate="no">00:04:30</a>)  あとconfigのjsonが入ってます。assetsを開くと<br>
(<a href="#" class="imk-cue" data-seek="4:37" translate="no">00:04:37</a>)  目の動き用、口の動き用、体ですね、それとあと頭。<br>
(<a href="#" class="imk-cue" data-seek="4:49" translate="no">00:04:49</a>)  そういう形でですね、パーツごとに分けて作っていきます。<br>
(<a href="#" class="imk-cue" data-seek="4:55" translate="no">00:04:55</a>)  例えば目一つ見てみましょうか。ちょっと小さいですけどもね。<br>
(<a href="#" class="imk-cue" data-seek="5:02" translate="no">00:05:02</a>)  これはeye_up.png、上目遣いの目だけです。<br>
(<a href="#" class="imk-cue" data-seek="5:13" translate="no">00:05:13</a>)  こういうふうに作りますね。で、作ってもらったプログラムをですね、run_puppet.py<br>
(<a href="#" class="imk-cue" data-seek="5:22" translate="no">00:05:22</a>)  先ほどもちょっと出てきましたけども、そういう形で書き込みます。<br>
(<a href="#" class="imk-cue" data-seek="5:29" translate="no">00:05:29</a>)  で、それをですね、ターミナルですね、それをターミナルで実行するという形をとってます。<br>
(<a href="#" class="imk-cue" data-seek="5:43" translate="no">00:05:43</a>)  なかなか素晴らしい出来でして、ちょっと頭の動きが<br>
(<a href="#" class="imk-cue" data-seek="5:49" translate="no">00:05:49</a>)  少し動いたり動かなかったりってこともあるんですが、まあ大体問題なく動いてくれてると。<br>
(<a href="#" class="imk-cue" data-seek="5:59" translate="no">00:05:59</a>)  少し動き出すと、少し不安定な動きになるっていうのが今の課題です。<br>
(<a href="#" class="imk-cue" data-seek="6:07" translate="no">00:06:07</a>)  ええ、そんなところでですね、作り上げたパペットです。<br>
(<a href="#" class="imk-cue" data-seek="6:12" translate="no">00:06:12</a>)  まあいろんな形で使っていこうと思っております。<br>
(<a href="#" class="imk-cue" data-seek="6:18" translate="no">00:06:18</a>)  ええと、それではですね、まあ次にこのさっき作ったパペットですけれども<br>
(<a href="#" class="imk-cue" data-seek="6:29" translate="no">00:06:29</a>)  まあ実際に今このように動いてますが、これあのターミナルから起動するというのは<br>
(<a href="#" class="imk-cue" data-seek="6:38" translate="no">00:06:38</a>)  結構慣れないと面倒くさい話なんで、ちょっとやってみますとね。<br>
(<a href="#" class="imk-cue" data-seek="6:46" translate="no">00:06:46</a>)  こういうコマンドをですね、打ち込むんです。で、立ち上げると。<br>
(<a href="#" class="imk-cue" data-seek="6:56" translate="no">00:06:56</a>)  まあこんなような形で立ち上がって、それを使うという形になるわけですね。<br>
(<a href="#" class="imk-cue" data-seek="7:05" translate="no">00:07:05</a>)  これ今私が話してる声で連動してます。<br>
(<a href="#" class="imk-cue" data-seek="7:13" translate="no">00:07:13</a>)  まあところがこれは非常に難しい、厄介なんでですね、これは使いたくないわけですよ、みんな。<br>
(<a href="#" class="imk-cue" data-seek="7:22" translate="no">00:07:22</a>)  で、どういう方法をとるかというと、AppleのAutomatorというものを使ってですね<br>
(<a href="#" class="imk-cue" data-seek="7:30" translate="no">00:07:30</a>)  まあ簡単なプログラムを書き込むというふうなですね、作業をします。<br>
(<a href="#" class="imk-cue" data-seek="7:40" translate="no">00:07:40</a>)  こういうものですね。MacのAutomatorを立ち上げてですね<br>
(<a href="#" class="imk-cue" data-seek="7:48" translate="no">00:07:48</a>)  で、そこでシェルスクリプトを実行ってものを選択して、ここに収めます。<br>
(<a href="#" class="imk-cue" data-seek="7:57" translate="no">00:07:57</a>)  で、それでとっても短いプログラムですけども、こういうプログラムを書きます。<br>
(<a href="#" class="imk-cue" data-seek="8:05" translate="no">00:08:05</a>)  実はこれさっきですね、ターミナルに打ち込んだコマンドと全く同じなんですね。<br>
(<a href="#" class="imk-cue" data-seek="8:13" translate="no">00:08:13</a>)  それをここに書き込んで、アプリケーションとして登録するということをやります。<br>
(<a href="#" class="imk-cue" data-seek="8:23" translate="no">00:08:23</a>)  そうしますと、まあこういう、自作パペット起動.app、ってものができます。<br>
(<a href="#" class="imk-cue" data-seek="8:30" translate="no">00:08:30</a>)  で、この状態でですね、これをクリックします。<br>
(<a href="#" class="imk-cue" data-seek="8:37" translate="no">00:08:37</a>)  そうすると、実はここから立ち上がることができます。<br>
(<a href="#" class="imk-cue" data-seek="8:48" translate="no">00:08:48</a>)  この、自作パペット起動.app、をクリックして立ち上がった画面がこれになります。<br>
(<a href="#" class="imk-cue" data-seek="8:56" translate="no">00:08:56</a>)  このような方法でですね、アプリ化して起動させるというのが非常に楽な使い方になります。<br>
(<a href="#" class="imk-cue" data-seek="9:05" translate="no">00:09:05</a>)  以上、補足でした。<br>
</p> </details>
<style>
details { font: 16px "Open Sans", Calibri, sans-serif; width: 100%; }
details > summary { padding: 2px 6px; width: 100%; background-color: #ddd; border: none; box-shadow: 3px 3px 4px black; cursor: pointer; list-style: none; }
details > p { font: 14px "Open Sans", Calibri, sans-serif; height:150px; overflow: scroll; background-color: #EDF7FF; padding: 2px 6px; margin: 0; box-shadow: 3px 3px 4px black; }
</style>

  </div>

  <script src="https://player.vimeo.com/api/player.js"></script>

  <script>
  (function(){
    var me = document.currentScript;
    var wrapper = me ? me.closest('.sc-vimeo-embed') : null;

    var target = wrapper ? wrapper : document;
    var iframe = target.querySelector('iframe');
    if (!iframe || !window.Vimeo || !Vimeo.Player) return;

    var player = new Vimeo.Player(iframe);

    /* --- 1. 確実に一時停止してから別ページを開く処理 --- */
    window.stopVimeoBeforeNavigate = function(event, link) {
      event.preventDefault(); 
      player.pause().then(function() {
        setTimeout(function() {
           window.open(link.href, link.target);
        }, 50);
      }).catch(function() {
        window.open(link.href, link.target);
      });
    };

    /* --- 2. 字幕のON/OFF制御（Vimeo API仕様） --- */
    var currentMode = "hidden";
    
    function useNormalMode() {
        currentMode = "hidden";
        player.disableTextTrack().catch(function(e){});
    }
    
    function useSpecialMode() {
        currentMode = "showing";
        // 日本語の subtitles が無ければ captions を探す安全設計
        player.enableTextTrack('ja', 'subtitles').catch(function() {
            player.enableTextTrack('ja', 'captions').catch(function() {});
        });
    }

    // ロード直後は枠内字幕を非表示にする
    player.ready().then(function() {
        useNormalMode();
    });

    // Vimeoプレーヤーの全画面・PiPイベントを監視して字幕を切り替え
    player.on('fullscreenchange', function(data) {
        if (data.fullscreen) useSpecialMode();
        else useNormalMode();
    });
    player.on('enterpictureinpicture', function() {
        useSpecialMode();
    });
    player.on('leavepictureinpicture', function() {
        useNormalMode();
    });

    /* --- 3. 字幕リストの初期化（自動梱包とデフォルト展開） --- */
    var checks = 0;
    var initTimer = setInterval(function(){
        var listContainer = target.querySelector('details > p');
        if (listContainer) {
            initSubtitles(listContainer);
            clearInterval(initTimer);
        }
        checks++;
        if (checks > 20) clearInterval(initTimer); // 10秒経ったら監視終了
    }, 500);

    function initSubtitles(listContainer) {
        // デフォルトで開く
        var detailsEl = target.querySelector("details");
        if (detailsEl) {
            detailsEl.open = true; 
            var summaryEl = detailsEl.querySelector("summary");
            if (summaryEl) summaryEl.textContent = "字幕(シーン)はここをクリック";
        }

        // 行ごとに見えない箱(span)で梱包
        if (!listContainer.dataset.formatted) {
            var html = listContainer.innerHTML;
            var lines = html.split(/<br\s*\/?>/i);
            var newHtml = "";
            for(var j=0; j<lines.length; j++) {
                if(lines[j].trim() === "") continue;
                newHtml += "<span class='imk-line'>" + lines[j] + "</span><br>";
            }
            listContainer.innerHTML = newHtml;
            listContainer.dataset.formatted = "true";
        }
    }

    /* --- 4. クリックシーク機能 --- */
    function parseTs(ts) {
      if (!ts) return null;
      var t = ts.trim();
      var parts = t.split(':').map(function(v){ return parseInt(v, 10); });
      if (parts.length === 2 && parts.every(Number.isFinite)) {
        return parts[0] * 60 + parts[1];
      } else if (parts.length === 3 && parts.every(Number.isFinite)) {
        return parts[0] * 3600 + parts[1] * 60 + parts[2];
      }
      return null;
    }

    target.addEventListener('click', function(e) {
      var a = e.target.closest('a.imk-cue');
      if (!a) return;
      e.preventDefault();
      var sec = null;
      if (a.dataset.seconds) {
        sec = Number(a.dataset.seconds);
      } else if (a.dataset.seek) {
        sec = parseTs(a.dataset.seek);
      }
      if (sec !== null && Number.isFinite(sec)) {
        player.setCurrentTime(sec).then(function(){ player.play(); }).catch(function(){});
      }
    });

    /* --- 5. 同期ハイライトと自動スクロール --- */
    player.on('timeupdate', function(data) {
      // Vimeoプレーヤーが勝手に字幕を出していたら隠す（最強ガード）
      player.getTextTracks().then(function(tracks) {
          var activeTrack = tracks.find(function(t){ return t.mode === 'showing'; });
          if (currentMode === "hidden" && activeTrack) {
              useNormalMode();
          }
      }).catch(function(){});

      var currentTime = data.seconds;
      var listContainer = target.querySelector('details > p');
      if (!listContainer) return;

      var cues = listContainer.querySelectorAll('a.imk-cue');
      if (cues.length === 0) return;

      var activeA = null;
      for (var i = 0; i < cues.length; i++) {
        var sec = null;
        if (cues[i].dataset.seconds) {
            sec = Number(cues[i].dataset.seconds);
        } else if (cues[i].dataset.seek) {
            sec = parseTs(cues[i].getAttribute('data-seek'));
        }
        if (sec !== null && currentTime >= sec - 0.5) {
          activeA = cues[i];
        } else if (sec > currentTime) {
          break;
        }
      }

      if (activeA) {
          var activeLine = activeA.closest(".imk-line");
          if (!activeLine) activeLine = activeA;

          if (activeLine.classList.contains("active-hl")) return;

          // 全行の色を徹底的にリセット
          var allLines = listContainer.querySelectorAll(".imk-line");
          for(var k=0; k<allLines.length; k++) {
              allLines[k].classList.remove("active-hl");
              allLines[k].removeAttribute("style");
          }
          var allLinks = listContainer.querySelectorAll("a");
          for(var m=0; m<allLinks.length; m++) {
              allLinks[m].classList.remove("active-hl");
              allLinks[m].removeAttribute("style");
          }

          // 新しい行に赤文字・黄色背景を直接塗る
          activeLine.classList.add("active-hl");
          activeLine.style.setProperty("background-color", "#ffff00", "important");
          activeLine.style.setProperty("color", "red", "important");
          activeLine.style.setProperty("font-weight", "normal", "important");
          
          var newLinks = activeLine.querySelectorAll("a");
          for(var n=0; n<newLinks.length; n++) {
              newLinks[n].style.setProperty("color", "red", "important");
              newLinks[n].style.setProperty("text-decoration", "none", "important");
          }

          // スクロール処理
          if (listContainer.offsetParent !== null) {
              var containerRect = listContainer.getBoundingClientRect();
              var activeRect = activeLine.getBoundingClientRect();
              var targetScroll = listContainer.scrollTop + (activeRect.top - containerRect.top) - (listContainer.clientHeight / 2) + (activeLine.clientHeight / 2);
              listContainer.scrollTo({
                  top: targetScroll,
                  behavior: "smooth"
              });
          }
      }
    });
  })();
  </script>

  <style>
  .sc-vimeo-embed .iframe-wrapper {
    position: relative;
    padding-bottom: 56.25%;
    height: 0;
    overflow: visible;
  }
  .sc-vimeo-embed .iframe-wrapper iframe {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
  }

  /* 行梱包時の基本スタイル（干渉防止のためクラス内で指定） */
  .sc-vimeo-embed .imk-line {
      display: inline-block;
      width: 100%;
      border-radius: 2px;
      transition: background-color 0.1s;
  }

  /* ▼ 字幕一覧（details）の見た目調整 */
  .sc-vimeo-embed details > p {
    font-size: 14px !important;
    line-height: 1.6;
    height: 200px !important; 
    overflow: auto;
    background-color: #EDF7FF;
    padding: 2px 6px;
    margin: 0;
    box-shadow: 3px 3px 4px black;
    position: relative; 
  }
  .sc-vimeo-embed details > summary {
    font-size: 14px !important;
    padding: 2px 6px;
    width: 100%;
    background-color: #ddd;
    border: none;
    box-shadow: 3px 3px 4px black;
    cursor: pointer;
    list-style: none;
  }
  </style>
</div>



<div class="wp-block-cocoon-blocks-toggle-box-1 toggle-wrap toggle-box block-box not-nested-style cocoon-block-toggle"><input id="toggle-checkbox-202604031856310" class="toggle-checkbox" type="checkbox"/><label class="toggle-button" for="toggle-checkbox-202604031856310"><strong><span class="marker">コードと要約</span><span class="marker">(クリック)</span></strong></label><div class="toggle-content">
<figure class="wp-block-flexible-table-block-table"><table class="has-fixed-layout"><tbody><tr><td>自作パペット_py.pub.txt<br>要約：<br>このPythonスクリプトは、Pygame、OpenCV、MediaPipeを用いて、ウェブカメラとマイクからの入力に基づきリアルタイムでアニメーションするアバター（パペット）システムを実装しています。主な機能は、検出された顔の傾き、視線の方向、口の開閉をアバターの頭の傾き、目の動き、口の形に反映させることです。マイクからの音声レベルやカメラでの口の開閉もアバターの口の動きに連動します。<br>新たに<code>json</code>ファイルを利用して顔認識のキャリブレーション設定（顔の基準角度や目の位置）を自動的にロード・セーブする機能があります。これにより、システム起動時に前回の設定が適用され、ユーザーは<code>SPACE</code>キーで現在の顔を新たな基準として保存できます。画面左上にはカメラ映像の一部と、システムのトラッキング状態（WAITING, READY, TRACKING）が表示されます。このシステムは、特定のディレクトリでPython仮想環境を有効化した後、付属のシェルスクリプトによって実行されるように設計されています。</td></tr><tr><td><a href="https://imakat.com/script_list/?pubtxt=自作パペット_py_pub.txt" target="_blank"><img decoding="async" src="https://imakat.com/rd.php?id=zrU95MiY.png" alt="" style="width:120px; height:auto;"></a></td></tr><tr><td>公開スクリプトに関するお問い合わせは以下へ↓</td></tr><tr><td><a rel="noopener" target="_blank" href="https://gemini.google.com/gem/1Dp-NLSA5j5OXE3B1Ki2IaBRCvq4CGa6t?usp=sharing"><img decoding="async" src="https://imakat.com/rd.php?id=s7BMZHhB.png" alt="" style="width:120px; height:auto;"></a></td></tr></tbody></table></figure>
</div></div>



<h4 class="wp-block-heading"><strong>なぜパペットを自作しようと思ったのか？</strong></h4>



<p>最大の理由は**「コスト」**です。<br>Adobeの素晴らしいパペットを使うためには、当然ながらAdobeのサブスクリプションに加入する必要があります。一番安いプランの組み合わせでも、月額1,200円ほどかかってしまいます。<br>「このキャラクターを使うためだけに、毎月課金し続けるのはどうなんだろう？」と悩んでいました。今は生成AI（画像生成AIやテキスト生成AI）がこれだけ発達し、メディアの加工やプログラムの作成まで何でもできる時代です。「それなら、AIの力を借りて自分で作ってしまおう！」と思い立ったのがきっかけです。</p>



<h4 class="wp-block-heading"><strong>新キャラクター「いまかっちゃん（仮）？」</strong></h4>



<p>というわけで、完全自前で作り上げたのがこちらのパペットです。（※名前は暫定で「いまかっちゃん」と思いつきましたが、ちょっと変な感じもしています。）<br>Adobeのパペットと比べてしまうと多少見劣りする部分はありますが、動画用としては十分な機能を持っています。<br><strong>音声連動：</strong> 私が話す声に連動して口がパクパクと動きます。<br><strong>目の動き：</strong> 上目遣いなど、視線も動きます。<br><strong>首の動き：</strong> 多少ですが顔の向きも変わります。（※現状、少し不安定な動きになることがあり、今後の課題です）<br>動画を作る上での「使い物」にはなんとか仕上がったかなと、個人的には割り切って満足しています。</p>



<h4 class="wp-block-heading"><strong>Google Geminiに丸投げ！？</strong></h4>



<p>「どうやって作ったの？」と疑問に思うかもしれませんが、実はプログラミングの大部分は**AI（Google Gemini）**にお願いしました。<br><strong>Geminiに質問する：</strong> 日本語で「こういう動きをするプログラムを書いて」と質問し、Pythonのコードを書いてもらいます。Geminiは非常に賢く、どんどんコードを生成してくれました。<br><strong>パーツ画像の用意：</strong> 目（上・下・右・左）、口（開・閉）、体、頭など、パーツごとに細かく分けた画像（PNG形式）を用意します。<br><strong>フォルダ構成：</strong> Macの中に専用フォルダ（mypuppet_systemなど）を作り、Pythonの実行ファイル（run_puppet.py）と、先ほどの画像パーツ群（assetsフォルダ）を格納して連動させました。</p>



<h4 class="wp-block-heading"><strong>Mac「Automator」で起動を劇的にラクにする方法</strong></h4>



<p>自作プログラムの弱点として、「毎回ターミナル（黒い画面）を開いてコマンドを打ち込まないと起動しない」という厄介な問題がありました。これでは面倒ですよね。<br>そこで、Mac標準アプリの**「Automator」**を使って、ワンクリックで起動できるように一工夫しました。<br><strong>手順：</strong> Automatorを立ち上げ、「シェルスクリプトを実行」を選択。そこにターミナルで打ち込んでいた短いコマンドをコピペするだけ。<br><strong>アプリ化：</strong> これをアプリケーションとして保存すれば、**「自作パペット起動.app」**の完成です！<br>これからは、このアプリアイコンをダブルクリックするだけで、サクッとパペットを呼び出せるようになりました。</p>



<h4 class="wp-block-heading">OBSへのパペット設定</h4>



<p>OBSへのパペットの組み込みですが、OBSのソース→ウインドウキャプチャ、この中から、パペットのあるウインドウを選択します。</p>



<figure class="wp-block-image size-large"><a href="https://imakat.com/rd.php?id=PeocProJ.png" target="_blank"><img decoding="async" src="https://imakat.com/rd.php?id=PeocProJ.png" alt=""/></a></figure>



<p>STREAM DECKがあると便利です。マルチアクションにて、OBS.app→自作パペット起動.app、と設定すればラクです。</p>



<h4 class="wp-block-heading">さてと。。</h4>



<p>その後の改善として、Pixelmator Proとの連携を作っています。形になったら紹介します。<br>生成AIを活用すれば、プログラミングの深い知識がなくても、日常の「ちょっと不便」「コストを削りたい」を解決できるツールが作れてしまいますよね。便利な時代になりました。<br><br></p>
]]></content:encoded>
					
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">28692</post-id>	</item>
		<item>
		<title>コード(スクリプト)作成のエコシステムを作ってみた</title>
		<link>https://imakat.com/2026/02/16/28530/</link>
		
		<dc:creator><![CDATA[imakat]]></dc:creator>
		<pubDate>Mon, 16 Feb 2026 05:11:51 +0000</pubDate>
				<category><![CDATA[デジタル]]></category>
		<category><![CDATA[ものづくり]]></category>
		<category><![CDATA[スクリプト]]></category>
		<category><![CDATA[Gemini]]></category>
		<category><![CDATA[エコシステム]]></category>
		<guid isPermaLink="false">https://imakat.com/?p=28530</guid>

					<description><![CDATA[はじめに コード作成って何？なんとも刺さりの悪い言葉なのですが、昔からある言い方に直せば、「コンピュータプログラム」のことです。これが長ったらしいので、コードと呼んでいます。英単語ではcodeです。音楽で使う和音を意味す [&#8230;]]]></description>
										<content:encoded><![CDATA[
<h3 class="wp-block-heading">はじめに</h3>



<p>コード作成って何？なんとも刺さりの悪い言葉なのですが、昔からある言い方に直せば、「コンピュータプログラム」のことです。これが長ったらしいので、コードと呼んでいます。英単語ではcodeです。音楽で使う和音を意味するコードはchordですね。綴りが違います。能書はこのくらいにして中身に入ります。</p>


<div class="sc-dynamic-embed">
  <style>
  /* リンクの見た目を整える */
  .sc-dynamic-embed .sc-link-container { 
      display: flex; 
      margin-bottom: 10px; 
      flex-wrap: wrap; 
      align-items: center; 
  }
  .sc-dynamic-embed .sc-link { margin-bottom: 0; }
  .sc-dynamic-embed .sc-link a {
    font-size: 15px; /* dynamicと合わせた基本サイズ */
    font-weight: normal;
    text-decoration: underline;
    color: #0073aa;
  }
  .sc-dynamic-embed .sc-link a:hover { text-decoration: none; color: #000; }
  
  /* ★スマホ画面（幅500px以下）の時は文字を縮小して統一感を出す */
  @media (max-width: 500px) {
    .sc-dynamic-embed .sc-link a { font-size: 13px; }
  }

  /* 行梱包時の基本スタイル */
  .sc-dynamic-embed .imk-line {
      display: inline-block;
      width: 100%;
      border-radius: 2px;
      transition: background-color 0.1s;
  }

  /* 古い枠内字幕ボックスを強制消去 */
  .sc-dynamic-embed #subtitleOverlay,
  .sc-dynamic-embed #scSubtitleOverlay,
  .sc-dynamic-embed .overlay-cue,
  .sc-dynamic-embed .band {
      display: none !important;
      opacity: 0 !important;
      visibility: hidden !important;
      pointer-events: none !important;
  }
  </style>

  <div class="sc-link-container">
    <p class="sc-link">
      <a href="https://imakat.com/ds62/?drid=72" target="_blank"
         onclick="return scStopAndGo(event, this);">
        👉低画質・枠外字幕はこちら
      </a>
    </p>
  </div>

  <style>
            :root{ --dr5emd-max: 1920px; }
            .video-wrap{position:relative;width:100%;margin:0 auto}
            figure.wp-block-video.aligncenter{
              width:100%;
              max-width:min(var(--dr5emd-max, 1920px), 98vw);
              margin:0 auto;
            }
            #subtitleOverlay{
              position:absolute; left:0; right:0; bottom:6%;
              padding:0 2%; text-align:center; pointer-events:none; z-index:2;
            }
            #subtitleOverlay .band{
              display:inline-block; background:rgba(0,0,0,0.35);
              padding:6px 10px; border-radius:8px; max-width:96%;
              margin:0 auto; box-shadow:0 1px 2px rgba(0,0,0,0.15);
            }
            #subtitleOverlay .overlay-cue{
              color:#fff; font-weight:600;
              font-size:clamp(16px, 3.6vw, 32px);
              line-height:1.32; white-space:pre-wrap; margin:2px 0;
              -webkit-text-stroke:.6px rgba(0,0,0,.7);
              text-shadow:-1px -1px 0 rgba(0,0,0,.6), 1px -1px 0 rgba(0,0,0,.6),
                          -1px  1px 0 rgba(0,0,0,.6), 1px  1px 0 rgba(0,0,0,.6);
            }
            @media (max-width:430px){
              #subtitleOverlay .overlay-cue{ font-size:clamp(16px, 4.2vw, 22px); }
            }
            .dr5emd-sublist details > p{
              height:200px; overflow:auto; background-color:#EDF7FF;
              padding:2px 6px; margin:0; box-shadow:3px 3px 4px black;
              position: relative;
            }
            .dr5emd-sublist details > summary{
              padding:2px 6px; width:100%;
              background-color:#ddd; border:none;
              box-shadow:3px 3px 4px black; cursor:pointer; list-style:none;
            }
            /* ▼ 自動スクロール時のハイライト（文字の太さを標準へ変更） */
            .active-hl {
                background-color: #ffff00 !important;
                color: #ff0000 !important;
                font-weight: normal; /* 標準の太さ */
                border-bottom: 2px solid red;
                display: inline-block;
                border-radius: 2px;
            }
            </style><div class="dr5emd-container"><figure class="wp-block-video aligncenter"><div class="video-wrap"><video id="myVideo" controls controlsList="nodownload" poster="https://imakat.com/rd.php?id=b8zFYGBF.png" playsinline preload="metadata" style="width:100%;height:auto;">  <source src="https://imakat.com/rd.php?id=zneKyTGw.mp4" type="video/mp4">  <track src="https://imakat.com/rd.php?id=EQDj6mHk.vtt" label="日本語" srclang="ja" kind="subtitles"></video><div id="subtitleOverlay" aria-hidden="true"></div></div><script>
document.addEventListener("DOMContentLoaded", function(){
  var video=document.getElementById("myVideo");
  var trackEl=video?video.querySelector("track[kind='subtitles'], track[kind='captions']"):null;
  var overlay=document.getElementById("subtitleOverlay"); if(!video||!overlay) return;
  video.addEventListener("contextmenu", function(e){ e.preventDefault(); return false; }, false);
  function setNative(mode){
    try{
      if(video.textTracks && video.textTracks.length){
        for(var i=0;i<video.textTracks.length;i++){ video.textTracks[i].mode = mode; }
      }
      if(trackEl && trackEl.track) trackEl.track.mode = mode;
    }catch(e){}
  }
  var isOverlay=true,lastSig="";
  function sig(active){if(!active||active.length===0)return"";var a=[];for(var i=0;i<active.length;i++){var c=active[i];a.push([c.startTime,c.endTime,c.text].join("|"));}return a.join("||");}
  function cueLine(c){var d=document.createElement("div");d.className="overlay-cue";d.setAttribute("translate","yes");if(typeof c.getCueAsHTML==="function")d.appendChild(c.getCueAsHTML());else d.textContent=c.text;return d;}
  function render(){
    if(!isOverlay || !trackEl || !trackEl.track) return;
    var ac=trackEl.track.activeCues,s=sig(ac); if(s===lastSig) return; lastSig=s;
    overlay.innerHTML=""; if(!ac || ac.length===0) return;
    var b=document.createElement("div"); b.className="band"; b.setAttribute("translate","yes");
    Array.from(ac).sort(function(a,b){return a.startTime-b.startTime;}).forEach(function(c){b.appendChild(cueLine(c));});
    overlay.appendChild(b);
  }
  function useOverlay(){isOverlay=true;overlay.style.display="";setNative("hidden");lastSig="";render();}
  function useNative(){isOverlay=false;overlay.style.display="none";setNative("showing");lastSig="";}
  useOverlay();
  if(trackEl){
    if(trackEl.track){ try{ trackEl.track.addEventListener("cuechange",render); }catch(e){} }
    trackEl.addEventListener("load", function(){ try{ if(trackEl.track) trackEl.track.addEventListener("cuechange",render); }catch(e){} render(); });
  }
  video.addEventListener("loadedmetadata",render);
  function handleWebkitMode(){ var m = video.webkitPresentationMode || "inline"; (m==="picture-in-picture"||m==="fullscreen") ? useNative() : useOverlay(); }
  if("webkitPresentationMode" in video){ video.addEventListener("webkitpresentationmodechanged",handleWebkitMode); handleWebkitMode(); }
  if("webkitCurrentPlaybackTargetIsWireless" in video){
    video.addEventListener("webkitcurrentplaybacktargetiswirelesschanged", function(){ video.webkitCurrentPlaybackTargetIsWireless ? useNative() : useOverlay(); });
  }
  if("pictureInPictureEnabled" in document){
    video.addEventListener("enterpictureinpicture",useNative);
    video.addEventListener("leavepictureinpicture",useOverlay);
  }
  document.addEventListener("fullscreenchange", function(){
    var fs=document.fullscreenElement;
    if(!fs) return useOverlay();
    (fs===video || (fs && fs.contains && fs.contains(video))) ? useNative() : useOverlay();
  });
});
</script>
                <figcaption></figcaption></figure><div class="dr5emd-sublist"><details translate="yes"><summary translate="yes">字幕一覧(クリック)</summary> <p translate="yes">
(<a href="#" class="imk-cue" data-seek="0:00" translate="no">00:00:00</a>) コード作成のエコシステムの紹介<br>
(<a href="#" class="imk-cue" data-seek="0:06" translate="no">00:00:06</a>) 今日のテーマはコード作成のエコシステムということについてお話ししようと思います。<br>
(<a href="#" class="imk-cue" data-seek="0:15" translate="no">00:00:15</a>) 私のプログラムコードの作成作業はGeminiとの対話で始まります。<br>
(<a href="#" class="imk-cue" data-seek="0:24" translate="no">00:00:24</a>) 以前はChatGPTでしたけども、最近Geminiに変えました。<br>
(<a href="#" class="imk-cue" data-seek="0:31" translate="no">00:00:31</a>) その理由はブログをお読みください。<br>
(<a href="#" class="imk-cue" data-seek="0:36" translate="no">00:00:36</a>) 使い方ですけども、まずは実現したいアイデアをそのままAIに投げかけてみます。<br>
(<a href="#" class="imk-cue" data-seek="0:48" translate="no">00:00:48</a>) そこからQ&Aを重ね、ロジックを固めていき、コードの作成を行います。<br>
(<a href="#" class="imk-cue" data-seek="0:56" translate="no">00:00:56</a>) AIをパートナーに据えることで、まずは動く形を迅速に作り上げることが<br>
(<a href="#" class="imk-cue" data-seek="1:05" translate="no">00:01:05</a>) できるのが大きなメリットです。<br>
(<a href="#" class="imk-cue" data-seek="1:10" translate="no">00:01:10</a>) しかし、完成したコードをただ置いておくだけではもったいないです。再活用したいです。<br>
(<a href="#" class="imk-cue" data-seek="1:19" translate="no">00:01:19</a>) そこで後で見返したときに、何のためのプログラムかを一目でわかるように、<br>
(<a href="#" class="imk-cue" data-seek="1:28" translate="no">00:01:28</a>) Geminiに日本語の要約を作成させます。<br>
(<a href="#" class="imk-cue" data-seek="1:32" translate="no">00:01:32</a>) その要約とコードを一緒にしてテキストファイルへ書き込み、それをITスクリプト管理フォルダ、<br>
(<a href="#" class="imk-cue" data-seek="1:48" translate="no">00:01:48</a>) ここへですね、収めます。<br>
(<a href="#" class="imk-cue" data-seek="1:51" translate="no">00:01:51</a>) その次のステップとして重要になるのがセキュリティ対策です。<br>
(<a href="#" class="imk-cue" data-seek="1:57" translate="no">00:01:57</a>) 個人情報や認証キーがコードに書き込まれていますので、これを隠すようにします。<br>
(<a href="#" class="imk-cue" data-seek="2:08" translate="no">00:02:08</a>) ルールを作って、例えばXXXXXなどの別の文字列へ機械的に置き換えます。<br>
(<a href="#" class="imk-cue" data-seek="2:22" translate="no">00:02:22</a>) 置き換えた後で、それらのプログラムをscripts_pubフォルダへ収めます。<br>
(<a href="#" class="imk-cue" data-seek="2:31" translate="no">00:02:31</a>) これを誰でも使える公開資料にします。<br>
(<a href="#" class="imk-cue" data-seek="2:36" translate="no">00:02:36</a>) このscripts_pubフォルダですが、2つのルートで活用します。<br>
(<a href="#" class="imk-cue" data-seek="2:44" translate="no">00:02:44</a>) 1つは私のブログです。公開スクリプト一覧として整理して、<br>
(<a href="#" class="imk-cue" data-seek="2:56" translate="no">00:02:56</a>) ユーザーに自由に使ってもらう資料として開放しています。<br>
(<a href="#" class="imk-cue" data-seek="3:03" translate="no">00:03:03</a>) そしてもう1つですが、これらのスクリプトを知識ソースとして、<br>
(<a href="#" class="imk-cue" data-seek="3:10" translate="no">00:03:10</a>) 私専用のGemを構築しています。<br>
(<a href="#" class="imk-cue" data-seek="3:14" translate="no">00:03:14</a>) これはユーザーへのサポート窓口になると同時に、<br>
(<a href="#" class="imk-cue" data-seek="3:20" translate="no">00:03:20</a>) 私自身が次のコードを生成するための最強のデータベースになるものです。<br>
(<a href="#" class="imk-cue" data-seek="3:29" translate="no">00:03:29</a>) その後運用していくと、不具合の修復だとか改善の必要性に迫られるわけですが、<br>
(<a href="#" class="imk-cue" data-seek="3:38" translate="no">00:03:38</a>) 現状のコードはすでに知識ソースとして埋め込まれているので、<br>
(<a href="#" class="imk-cue" data-seek="3:45" translate="no">00:03:45</a>) 特段の準備をすることもなく速やかにそれらの開発に着手できるわけです。<br>
(<a href="#" class="imk-cue" data-seek="3:54" translate="no">00:03:54</a>) 回せば回すほどAIは私の意図を学習して、システムの精度は増していくと。<br>
(<a href="#" class="imk-cue" data-seek="4:02" translate="no">00:04:02</a>) 進化のループと言えます。<br>
(<a href="#" class="imk-cue" data-seek="4:06" translate="no">00:04:06</a>) 今回はプログラム作成を例に挙げましたけども、この情報の整理、変換、再学習という循環は、<br>
(<a href="#" class="imk-cue" data-seek="4:18" translate="no">00:04:18</a>) 文章作成、研究、趣味の記録など、あらゆる分野に応用が可能だと思います。<br>
(<a href="#" class="imk-cue" data-seek="4:26" translate="no">00:04:26</a>) 皆さんのライフワークにこのエコシステムを取り入れたら、どのような変化が起きるでしょうか。<br>
(<a href="#" class="imk-cue" data-seek="4:34" translate="no">00:04:34</a>) 私は高齢者ではありますけども、高齢者に役立つようなうまい使い方もできそうな気がしています。<br>
(<a href="#" class="imk-cue" data-seek="4:43" translate="no">00:04:43</a>) 以上です。<br>
</p> </details>
<style>
details { font: 16px "Open Sans", Calibri, sans-serif; width: 100%; }
details > summary { padding: 2px 6px; width: 100%; background-color: #ddd; border: none; box-shadow: 3px 3px 4px black; cursor: pointer; list-style: none; }
details > p { font: 14px "Open Sans", Calibri, sans-serif; height:150px; overflow: scroll; background-color: #EDF7FF; padding: 2px 6px; margin: 0; box-shadow: 3px 3px 4px black; }
</style>
</div><script>
(function(){
  var root=document.querySelector(".dr5emd-sublist");
  var video=document.getElementById("myVideo");
  if(!root || !video) return;
  function parseTs(ts){
    if(!ts) return null;
    var p=ts.trim().split(":").map(function(x){return parseInt(x,10)||0;});
    if(p.length===2) return p[0]*60 + p[1];
    if(p.length===3) return p[0]*3600 + p[1]*60 + p[2];
    return null;
  }
  root.addEventListener("click", function(e){
    var a=e.target.closest && e.target.closest("a.imk-cue[data-seek]");
    if(!a || !root.contains(a)) return;
    e.preventDefault();
    var sec = parseTs(a.getAttribute("data-seek"));
    if(sec==null) return;
    try{ video.currentTime = sec; if(video.paused) video.play(); }catch(_){}
  });
  video.addEventListener("timeupdate", function(){
    var listContainer = root.querySelector("details > p");
    if(!listContainer) return;
    var cues = listContainer.querySelectorAll("a.imk-cue");
    if(cues.length === 0) return;
    var cur = video.currentTime;
    var active = null;
    for(var i=0; i<cues.length; i++){
        var t = parseTs(cues[i].getAttribute("data-seek"));
        if(t !== null && cur >= t - 0.5){
            active = cues[i];
        } else if(t > cur){
            break;
        }
    }
    if(active){
        if(active.classList.contains("active-hl")) return;
        var old = listContainer.querySelectorAll(".active-hl");
        for(var k=0; k<old.length; k++) old[k].classList.remove("active-hl");
        active.classList.add("active-hl");
        if(listContainer.offsetParent !== null){
            var containerRect = listContainer.getBoundingClientRect();
            var activeRect = active.getBoundingClientRect();
            var targetScroll = listContainer.scrollTop + (activeRect.top - containerRect.top) - (listContainer.clientHeight / 2) + (active.clientHeight / 2);
            listContainer.scrollTo({ top: targetScroll, behavior: "smooth" });
        }
    }
  });
})();
</script>
                </div>

  <script>
  (function(){
    var me = document.currentScript;
    var wrapper = me ? me.closest('.sc-dynamic-embed') : null;

    /* -----------------------------------------------
       1. 動画の保護機能（右クリック禁止・DL防止）
       ----------------------------------------------- */
    function protectVideo() {
      var target = wrapper ? wrapper : document;
      var mediaEls = target.querySelectorAll('video');
      mediaEls.forEach(function(v){
        if(v.dataset.protected === 'true') return;
        v.dataset.protected = 'true';
        v.setAttribute('controlsList', 'nodownload');
        v.oncontextmenu = function() { return false; };
        v.addEventListener('contextmenu', function(e){ e.preventDefault(); return false; }, false);
      });
    }

    /* -----------------------------------------------
       2. 字幕制御＆ハイライト機能（iPhone全画面 完全対応版）
       ----------------------------------------------- */
    function initSubtitles() {
      var target = wrapper ? wrapper : document;
      var video = target.querySelector('video');
      var listContainer = target.querySelector('details > p');
      
      if (!video || !listContainer) return false; 

      if (video.dataset.subInit === 'true') return true; 
      video.dataset.subInit = 'true';

      var oldOverlay = target.querySelector('#subtitleOverlay') || target.querySelector('#scSubtitleOverlay');
      if (oldOverlay) {
          oldOverlay.style.setProperty('display', 'none', 'important');
          oldOverlay.innerHTML = ''; 
      }

      function isSpecialMode() {
        var isFs = !!(document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement);
        var isPiP = !!(document.pictureInPictureElement && document.pictureInPictureElement === video) || (video.webkitPresentationMode === "picture-in-picture");
        var isIOSFs = !!video.webkitDisplayingFullscreen; 
        return isFs || isPiP || isIOSFs;
      }

      try {
        if(video.textTracks && video.textTracks.length > 0){
          for(var i=0; i<video.textTracks.length; i++){
             if(video.textTracks[i].kind === 'subtitles' || video.textTracks[i].kind === 'captions'){
                 video.textTracks[i].mode = "hidden";
             }
          }
        }
      } catch(e){}

      video.addEventListener("webkitbeginfullscreen", function() {
          try {
              if(video.textTracks && video.textTracks.length > 0) {
                  for(var i=0; i<video.textTracks.length; i++){
                      if(video.textTracks[i].kind === 'subtitles' || video.textTracks[i].kind === 'captions') video.textTracks[i].mode = "showing";
                  }
              }
          } catch(e){}
      });
      video.addEventListener("webkitendfullscreen", function() {
          try {
              if(video.textTracks && video.textTracks.length > 0) {
                  for(var i=0; i<video.textTracks.length; i++){
                      if(video.textTracks[i].kind === 'subtitles' || video.textTracks[i].kind === 'captions') video.textTracks[i].mode = "hidden";
                  }
              }
          } catch(e){}
      });

      var detailsEl = target.querySelector("details");
      if (detailsEl) {
          detailsEl.open = true; 
          var summaryEl = detailsEl.querySelector("summary");
          if (summaryEl) summaryEl.textContent = "字幕(シーン)はここをクリック";
      }

      if (!listContainer.dataset.formatted) {
          var html = listContainer.innerHTML;
          var lines = html.split(/<br\s*\/?>/i);
          var newHtml = "";
          for(var j=0; j<lines.length; j++) {
              if(lines[j].trim() === "") continue;
              newHtml += "<span class='imk-line'>" + lines[j] + "</span><br>";
          }
          listContainer.innerHTML = newHtml;
          listContainer.dataset.formatted = "true";
      }

      function parseTs(ts){
        if(!ts) return null;
        var p = ts.trim().split(":").map(function(x){return parseInt(x,10)||0;});
        if(p.length===2) return p[0]*60 + p[1];
        if(p.length===3) return p[0]*3600 + p[1]*60 + p[2];
        return null;
      }

      var rootSublist = target.querySelector(".dr5-sublist") || listContainer.parentElement;
      if (rootSublist) {
        rootSublist.addEventListener("click", function(e){
          var a = e.target.closest && e.target.closest("a.imk-cue[data-seek]");
          if(!a) return;
          e.preventDefault();
          var sec = parseTs(a.getAttribute("data-seek"));
          if(sec==null) return;
          try{ video.currentTime = sec; if(video.paused) video.play(); }catch(_){}
        });
      }

      video.addEventListener("timeupdate", function(){
        var desiredMode = isSpecialMode() ? "showing" : "hidden";
        try {
            if(video.textTracks && video.textTracks.length > 0){
                for(var i=0; i<video.textTracks.length; i++){
                    if((video.textTracks[i].kind === 'subtitles' || video.textTracks[i].kind === 'captions') && video.textTracks[i].mode !== desiredMode) {
                        video.textTracks[i].mode = desiredMode;
                    }
                }
            }
        } catch(e){}

        var cues = listContainer.querySelectorAll("a.imk-cue");
        if(cues.length === 0) return;
        var cur = video.currentTime;
        var activeA = null;

        for(var i=0; i<cues.length; i++){
            var t = parseTs(cues[i].getAttribute("data-seek"));
            if(t !== null && cur >= t - 0.5){ activeA = cues[i]; } 
            else if(t > cur){ break; }
        }

        if(activeA){
            var activeLine = activeA.closest(".imk-line");
            if(!activeLine) activeLine = activeA;

            if(activeLine.classList.contains("active-hl")) return;

            var allLines = listContainer.querySelectorAll(".imk-line");
            for(var k=0; k<allLines.length; k++) {
                allLines[k].classList.remove("active-hl");
                allLines[k].removeAttribute("style"); 
            }
            var allLinks = listContainer.querySelectorAll("a");
            for(var m=0; m<allLinks.length; m++) {
                allLinks[m].classList.remove("active-hl");
                allLinks[m].removeAttribute("style"); 
            }

            activeLine.classList.add("active-hl");
            activeLine.style.setProperty("background-color", "#ffff00", "important");
            activeLine.style.setProperty("color", "red", "important");
            activeLine.style.setProperty("font-weight", "normal", "important");
            
            var newLinks = activeLine.querySelectorAll("a");
            for(var n=0; n<newLinks.length; n++) {
                newLinks[n].style.setProperty("color", "red", "important");
                newLinks[n].style.setProperty("text-decoration", "none", "important");
            }

            if(listContainer.offsetParent !== null){
                var containerRect = listContainer.getBoundingClientRect();
                var activeRect = activeLine.getBoundingClientRect();
                var targetScroll = listContainer.scrollTop + (activeRect.top - containerRect.top) - (listContainer.clientHeight / 2) + (activeLine.clientHeight / 2);
                listContainer.scrollTo({ top: targetScroll, behavior: "smooth" });
            }
        }
      });

      return true;
    }

    /* -----------------------------------------------
       監視タイマー
       ----------------------------------------------- */
    var checks = 0;
    var checkTimer = setInterval(function(){
      protectVideo(); // ★dynamic2は保護を維持
      var success = initSubtitles();
      checks++;
      if (success || checks > 20) { 
        clearInterval(checkTimer);
      }
    }, 500); 

    /* -----------------------------------------------
       3. 画面遷移時の停止機能
       ----------------------------------------------- */
    if (!window.scStopAndGo) {
      window.scStopAndGo = function(event, link){
        try{
          var mediaEls = document.querySelectorAll('video, audio');
          mediaEls.forEach(function(m){
            try{
              if (!m.paused) m.pause();
              if (document.pictureInPictureElement === m && document.exitPictureInPicture) {
                document.exitPictureInPicture().catch(function(){});
              }
            }catch(e){}
          });
        }finally{
          event.preventDefault();
          setTimeout(function(){
            if (link.target === '_blank') {
              window.open(link.href, '_blank');
            } else {
              window.location.href = link.href;
            }
          }, 50);
        }
        return false;
      };
    }
  })();
  </script>
</div>



<a rel="noopener" target="_blank" href="https://docs.google.com/drawings/d/1_65TflS4kAJ6xf7RoGl22oAYRljreVTvBjsnmbcZQQ8/edit?usp=sharing" 
>
<img decoding="async" src="https://docs.google.com/drawings/d/e/2PACX-1vQW9KCkzFbj3Zaa4sZPLjInUUHwKJ2FdpFW388svQ82AkWComAKJSEDe5VbT8z1w0uqnoSeq00BuiUf/pub?w=1920&#038;h=1440"
></a>



<div class="wp-block-cocoon-blocks-toggle-box-1 toggle-wrap toggle-box block-box not-nested-style cocoon-block-toggle"><input id="toggle-checkbox-202603121151090" class="toggle-checkbox" type="checkbox"/><label class="toggle-button" for="toggle-checkbox-202603121151090"><strong><span class="marker">コードと要約</span><span class="marker">(クリック)</span></strong></label><div class="toggle-content">
<figure class="wp-block-flexible-table-block-table"><table class="has-fixed-layout"><tbody><tr><td><span class="red"><strong>❶</strong></span>公開用スクリプト作成_pub.txt：<br>要約：この workflow は、選択した *_exe.txt ファイルから公開用 *_pub.txt を生成する自動処理である。保存先は M1 側ボリュームに固定し、Dropbox へは直接書き込まない。メタ情報が無い場合は自動付与し、既存メタでは更新日時を除去、状態を「公開用」に変更する。本文から個人情報・キー・パス等を匿名化し、key_patterns.txt に基づく追加置換も行う。未置換の秘密候補を抽出して警告し、生成後は公開 URL とエンコード URL を表示・コピーする。付属のシェルスクリプトで生成物を Dropbox に rsync 同期する。</td></tr><tr><td><a href="https://imakat.com/script_list/?pubtxt=公開用スクリプト作成_pub.txt" target="_blank"><img decoding="async" src="https://imakat.com/rd.php?id=zrU95MiY.png" alt="" style="width:120px; height:auto;"></a></td></tr><tr><td><span class="red"><strong>❷</strong></span>公開用スクリプトのリスト表示_pub.txt：<br>要約：WordPressのショートコードによる公開用TXT一覧を生成するコード。?pubtxt_dir パラメータで階層移動、見出しクリックで sort/order 切替が可能。pmedia/scripts_pub 配下のTXTのみ対象とし、@eaDir やドット始まり・key_patterns.txt を除外。<br>更新日時（mtime desc）→名前→サイズの優先順でソートし、パンくず表示も実装。各TXTはクリックで閲覧または &amp;dl=1 でUTF-8添付ダウンロード可。ディレクトリトラバーサル防止のため realpath 検証を行う安全設計。</td></tr><tr><td><a href="https://imakat.com/script_list/?pubtxt=公開用スクリプトのリスト表示_pub.txt" target="_blank"><img decoding="async" src="https://imakat.com/rd.php?id=zrU95MiY.png" alt="" style="width:120px; height:auto;"></a></td></tr><tr><td><span class="red"><strong>❸</strong></span>scripts_pubフォルダからgドキュメントを生成_pub.txt：<br>要約：このシステムは、ローカルのスクリプトファイルをGoogleドキュメントに自動同期し、公開するためのものです。Googleドキュメント化することによりGemの読み込みが自動になります。<br><code>sync_docs.py</code>は、設定されたローカルフォルダから特定の拡張子を持つファイルを読み込み、更新日時順に整理します。これらのファイル内容を目次と共に整形し、Google Docs API経由で、指定された複数のGoogleドキュメントに分割して書き込みます（1ドキュメントあたり最大50ファイル）。既存内容を認証後に更新する機能を持ちます。<br><code>watcher.py</code>は<code>sync_docs.py</code>と連携し、監視対象フォルダ内のファイル変更をリアルタイムで検知します。変更後クールダウン期間を経て、自動的に<code>sync_docs.main_sync()</code>を呼び出し、Googleドキュメントへの同期処理を実行。ローカルファイルの更新を自動的にクラウドへ反映させます。<code>com.xxxxxxxx.docsupdater.plist</code>はmacOSのLaunchAgents設定ファイルで、システムの起動時に<code>watcher.py</code>を自動実行し、バックグラウンドでファイル監視とGoogleドキュメント同期プロセスを常駐させる役割を担います。<br>全体として、このシステムはローカルで管理するスクリプトやテキストファイルを常に最新の状態でGoogleドキュメント上に保ち、ウェブ公開可能な自動化ワークフローを提供します。</td></tr><tr><td><a href="https://imakat.com/script_list/?pubtxt=scripts_pubフォルダからgドキュメントを生成_pub.txt" target="_blank"><img decoding="async" src="https://imakat.com/rd.php?id=zrU95MiY.png" alt="" style="width:120px; height:auto;"></a></td></tr><tr><td><span class="bold-red">❹</span>Antigravity(AIエージェント)に処理を指示するときの基調プロンプト：<br>XXXXXXシステムの実行局FACTORYは、Mac mini M1上の「ITスクリプト管理」を拠点に、設計図を現実環境に適合・定着・記録するAIアシスタントです。FACTORYはITスクリプト管理フォルダを徹底検索し、既存ファイルへの追記・修正を優先。コードは自律テストでエラー修正します。安全基準として、ITスクリプト管理フォルダ内は自律操作可ですが、他での削除等はオーナー承認必須。<code>rm -rf</code>等の破壊的コマンドは禁止、ゴミ箱へ移動等に置換します。機密情報は<code>.env</code>で管理し、報告は汎用形式で行います。記録では新規ファイル名はオーナーと相談、メタ情報と350～400字の要約を付与し、指定形式で出力。完了後、更新箇所をオーナーに報告します。<br>※XXXXXXは、ご自分の名前に変えてください。</td></tr><tr><td><a href="https://imakat.com/script_list/?pubtxt=AIエージェントに処理を指示するときの基調プロンプト_pub.txt" target="_blank"><img decoding="async" src="https://imakat.com/rd.php?id=zrU95MiY.png" alt="" style="width:120px; height:auto;"></a></td></tr><tr><td>コード(スクリプト)をもっと知りたい方はこちらへ↓</td></tr><tr><td><a rel="noopener" target="_blank" href="https://gemini.google.com/gem/1Dp-NLSA5j5OXE3B1Ki2IaBRCvq4CGa6t?usp=sharing"><img decoding="async" src="https://imakat.com/rd.php?id=s7BMZHhB.png" alt="" style="width:120px; height:auto;"></a></td></tr></tbody></table></figure>
</div></div>



<h3 class="wp-block-heading">ポイント解説</h3>



<h4 class="wp-block-heading"><strong>ChatGPTからGeminiへ</strong></h4>



<p>私のプログラム作成のパートナーは、はじめはChatGPTでしたが、最近になってGeminiへと移行しました。 その理由は開発してきたコードが増えて全部を記憶させて開発に利用することが困難になってきたからです。単価あたりの知識記憶容量がGeminiはChatGPTの約10倍あったのが決め手でした(2026年1月時点)。従って、これから説明する「知識の循環システム」が低コストで実現できるからです。</p>



<p>ただし生成AIの選択ですが、将来どうするか全く分かりません。というのは、毎月くらい各社から高性能のモデルが登場して目移りするからです。ただ、GeminiはGoogle製なので、デジタルアセットの作成から利用、廃棄に至るインフラをほぼ全て提供できるGoogle内にいるので、今のところ一番底力があると思えます。Appleが本気を出せば別ですが。</p>



<p>それでは、私が実践している**「コード（スクリプト）作成のエコシステム」**について、お話ししようと思います。</p>



<h4 class="wp-block-heading">１　<strong>開発：AIとの対話から生まれる「動く形」</strong></h4>



<p>私のプログラム開発は、Geminiとの「対話」から始まります。 実現したいアイデアを、そのままAIに投げかけてみる。そこから質疑応答を重ね、ロジックを磨き、コードを作ってもらいテストを繰り返すというやり方です。</p>



<p>最大のメリットは、自分の都合のいい時間に作業ができること、構文のエラーに悩まされる時間が減り仕組みづくりに集中できることでしょう。まずは**「動く形」を迅速に作り上げる**ことができる。これが、このシステムの第一の魅力です。</p>



<h4 class="wp-block-heading">２　<strong>見える化：情報を「資産」に変えるひと手間</strong></h4>



<p>完成したコードをただ保存しておくだけでは、後で活かせません。「あれ、このpythonは何をするものだったかな？」と迷子になってしまいます。</p>



<p>そこで私は、必ず**「コードの見える化」<strong>という工程を挟みます。 具体的には、Geminiにコードの</strong>「日本語要約」**を作成させ、それを冒頭に添付したtxtファイルを生成し、「ITスクリプト管理」フォルダへ収めます。</p>



<p>さらに重要なのが<strong>セキュリティ</strong>です。 独自のルール（key_patterns.txt）に基づき、個人情報やAPIキーを機械的に「XXXXX」などへと置き換えます。こうして生成されたクリーンなファイルを、公開用の scripts_pub フォルダへ移動させます。 少し手間に見えるかもしれませんが、この工程が、「誰でも使える汎用的な知識資産」へと変身させるのです。</p>



<h4 class="wp-block-heading">３　<strong>公開と共有：ブログと「自分専用Gem」</strong></h4>



<p>こうして整えられた知識資産は、2つのルートで新しい価値を生み出します。</p>



<p>一つは、このブログです。 WordPress上の「公開スクリプト一覧」として整理し、訪問者に自由に役立てていただけるリソースとして開放しています。</p>



<p>そしてもう一つが、**Geminiへのフィードバック（還流）**です。 これらのスクリプト群を「知識ソース」としてリンクした、私専用のGem（カスタムAI）を構築します。これはユーザーへのサポート窓口になると同時に、私自身が次のコードを生成するための、最強のデータベースになってくれるわけです。</p>



<h4 class="wp-block-heading"><strong>４　終わりなき進化のループ</strong></h4>



<p>このシステムを運用していて改めて気づく最大の利点は、**「予習が不要」**になったことです。 プログラムに不具合が見つかったり、改善したくなったりした時、私のGeminiには既に過去のコードが「知識」として埋め込まれています。特段の準備をすることもなく、速やかに修正や拡張の開発に着手できるのです。</p>



<p>修正して得られた新たな知見は、またシステムを通って知識ソースへ戻っていく。 回せば回すほど、AIは私の意図やコーディングの癖を深く学習し、精度が増していきます。まさに、終わりなき進化のループと言えるでしょう。</p>



<h4 class="wp-block-heading"><strong>5　</strong>ファイル管理系のアプリの生成・実行を、Antigravity任せにするのは禁物</h4>



<p>動画を作った後で、画像を手直ししましたが、「Antigravity」という、コードを作るだけではなくて実行テストから完成までやってくれるアプリを使えるようにしました。見てるだけで勝手に作業をしてくれる夢のようなアプリです。ちなみにmodelとして大人気のClaudeも指定できます。しかしですね、非常に怖い側面を持っています。<strong>エンタメあるいは書類作成用途は、この「Antigravity」をぼおっと見てるだけでいいかも知れません。しかし、ファイル管理系のアプリ作成の場合は、作業を丸投げするのは、やめた方がいい</strong>です。「大事なファイルを消された！」という大失敗があちこちで起きているようです。</p>



<p><strong>ファイル管理系のシステムを生成する場合は</strong>、</p>



<div class="wp-block-cocoon-blocks-blank-box-1 blank-box block-box has-background has-watery-yellow-background-color">
<p><strong><span class="marker-under">Geminiにコードを書いてもらう→コードを実行ファイルにペーストする→Geminiに作ってもらったコマンドをターミナルに貼り付け実行する→ターミナルの返答をGeminiに報告する→正常に動くまで修正する→完成→コードをローカルのtxtファイルに保存して日本語要約を更新する→公開用ファイルを生成する</span></strong></p>



<p><span class="bold-red">※注意：コードに秘匿性のある文字列が含まれる場合は、そのチャットを、あとで削除するのが望ましいです。</span></p>
</div>



<p>といったようにバトンリレーに人が挟まるような、慎重な形がよいと考えます。<br><br></p>



<h4 class="wp-block-heading"><strong>【おわりに】高齢者のAIとの付き合い方</strong></h4>



<p>今回は「プログラム作成」を例に挙げましたが、この**「情報の整理・変換・再学習」という循環**は、文章作成や研究、あるいは趣味の記録など、あらゆる分野に応用が可能だと感じています。</p>



<p>作業の進め方は、ゴールまでの間をステップ化して、少しずつ着実に進む方法がベストです。</p>



<p>私は高齢者ではありますが、こういった新しい技術は、若者だけのものではありません。むしろ、これまで蓄積してきた多くの知識や経験を整理し、形にするためにこそ、AIというパートナーは大きな力を貸してくれるようにも思えます。 高齢者にも役に立つ「上手い使い方」がまだまだありそうな気がしています。</p>



<p><br>以上</p>
]]></content:encoded>
					
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">28530</post-id>	</item>
		<item>
		<title>棚田式非破壊型DASはNASを超えるぞ！</title>
		<link>https://imakat.com/2026/01/22/28134/</link>
		
		<dc:creator><![CDATA[imakat]]></dc:creator>
		<pubDate>Thu, 22 Jan 2026 13:52:04 +0000</pubDate>
				<category><![CDATA[棚田式ファイル管理]]></category>
		<category><![CDATA[デジタル]]></category>
		<category><![CDATA[ものづくり]]></category>
		<category><![CDATA[DAS]]></category>
		<category><![CDATA[棚田]]></category>
		<category><![CDATA[ミラーリング]]></category>
		<category><![CDATA[FreeFileSync]]></category>
		<category><![CDATA[シャドウ・ディレクトリ]]></category>
		<category><![CDATA[Mac mini]]></category>
		<category><![CDATA[NAS]]></category>
		<guid isPermaLink="false">https://imakat.com/?p=28134</guid>

					<description><![CDATA[ブログの記事のネタが無くて困ることはよくあるでしょうが、今回のこのテーマは、全く逆で、ネタが多過ぎて、どこから話していいか見当もつかない様な面白いテーマです。まあ超マニアックですが。。いいえ核心かも。では話を始めます。  [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>ブログの記事のネタが無くて困ることはよくあるでしょうが、今回のこのテーマは、全く逆で、ネタが多過ぎて、どこから話していいか見当もつかない様な面白いテーマです。まあ超マニアックですが。。いいえ核心かも。では話を始めます。</p>



<p><strong>〜Mac miniによるDAS操作、Mac miniは今こそ本領発揮の時代に入った〜</strong></p>



<p>SynologyNASとMac miniを組み合わせたファイル管理システムを使い始めた頃から、「NAS自体がPCの機能を持っているので、Mac miniと機能が重複している。Mac miniの頭脳(OS)を使って、複数のHDDを、RAIDのようにガチガチではなく、もっと自由に扱うことができないか」と問題意識を持っていました。その解決策を提案しているYouTube動画が気になっていました。&#x1f449;「<a rel="noopener" target="_blank" href="https://youtu.be/OGl-FhVOGQM?si=Z4bRQZPtYU54IjKV">NASじゃなくてもいいかも！Mac miniでデータ管理してみた(ギズモード網藤さん)<span class="fa fa-external-link external-icon anchor-icon"></span></a>」「<a rel="noopener" target="_blank" href="https://youtu.be/7DvCrWIyl_M?si=lQOsicW2S3Ea3ETO">【NAS＆RAIDより安全】２０２５年版　個人限定　データを守る方法を紹介　HDDケース<span class="fa fa-external-link external-icon anchor-icon"></span></a>」</p>



<figure class="wp-block-image size-large is-resized"><a href="https://imakat.com/rd.php?id=zo1t1HJF.png" target="_blank"><img decoding="async" src="https://imakat.com/rd.php?id=zo1t1HJF.png" alt="" style="aspect-ratio:1.5000155836060465;width:199px;height:auto"/></a></figure>



<p>棚田式を思いついたのは、ギズモードの網藤さんが「一年に１回、写真や動画を移動させるルールを実行している」と語っていたことがヒントになり、「<strong>棚田は許容量を超えたら水が溢れ出る</strong>→だが、ここで→<strong>古い水から下段へ移動できたら面白い</strong>」とイメージしたことです。<br>まずは動画をご覧ください。</p>


<div class="sc-vimeo-embed">
  <p style="font-size: 14px;" class="sc-link">
    <a href="https://imakat.com/vm5?movid=1166235037" target="_blank" onclick="stopVimeoBeforeNavigate(event, this)">
      動画を別ページで表示(ここをクリック)
    </a>
  </p>

  <div class="iframe-wrapper">
    <iframe
      src="https://player.vimeo.com/video/1166235037?title=0&byline=0&portrait=0&controls=1&speed=1&texttrack=ja&dnt=1&loop=0"
      frameborder="0"
      allow="autoplay; fullscreen; picture-in-picture"
      allowfullscreen></iframe>
  </div>

  <div class="dr-vimeo-sublist">
    <details translate="yes"><summary translate="yes">字幕一覧(クリック)</summary> <p translate="yes">
(<a href="#" class="imk-cue" data-seek="0:18" translate="no">00:00:18</a>) それでは始めます。<br>
(<a href="#" class="imk-cue" data-seek="0:20" translate="no">00:00:20</a>) この画像ですけれども、<br>
(<a href="#" class="imk-cue" data-seek="0:22" translate="no">00:00:22</a>) これはあの、日本の山村によく見かける棚田の風景です。<br>
(<a href="#" class="imk-cue" data-seek="0:28" translate="no">00:00:28</a>) ええ、コンピューターのデータの管理をですね、<br>
(<a href="#" class="imk-cue" data-seek="0:32" translate="no">00:00:32</a>) この棚田を流れる水のように行いたいと。<br>
(<a href="#" class="imk-cue" data-seek="0:36" translate="no">00:00:36</a>) そう思って、<br>
(<a href="#" class="imk-cue" data-seek="0:38" translate="no">00:00:38</a>) 今回この「棚田式ファイル管理システム」を作ってみました。<br>
(<a href="#" class="imk-cue" data-seek="0:44" translate="no">00:00:44</a>) まだ最後まで完成していませんが、<br>
(<a href="#" class="imk-cue" data-seek="0:47" translate="no">00:00:47</a>) 重要な仕組みは大体出来上がりましたので、<br>
(<a href="#" class="imk-cue" data-seek="0:51" translate="no">00:00:51</a>) 紹介したいと思います。<br>
(<a href="#" class="imk-cue" data-seek="0:54" translate="no">00:00:54</a>) この動画は、<br>
(<a href="#" class="imk-cue" data-seek="0:56" translate="no">00:00:56</a>) Google Vids<br>
(<a href="#" class="imk-cue" data-seek="0:58" translate="no">00:00:58</a>) Google V-I-D-S という新しい機能をですね、<br>
(<a href="#" class="imk-cue" data-seek="1:03" translate="no">00:01:03</a>) 使って作ってみました。<br>
(<a href="#" class="imk-cue" data-seek="1:05" translate="no">00:01:05</a>) それではご覧ください。<br>
(<a href="#" class="imk-cue" data-seek="1:21" translate="no">00:01:21</a>) 本日は、私が個人で開発・運用している「DAS棚田式ファイル管理システム」についてお話しします。<br>
(<a href="#" class="imk-cue" data-seek="1:29" translate="no">00:01:29</a>) これは市販のNASやRAIDに依存せず、<br>
(<a href="#" class="imk-cue" data-seek="1:31" translate="no">00:01:31</a>) Mac miniの計算能力とGoogle Sheetsを活用して、<br>
(<a href="#" class="imk-cue" data-seek="1:36" translate="no">00:01:36</a>) 大量のファイルを安全かつ低コストに管理するための仕組みです。<br>
(<a href="#" class="imk-cue" data-seek="1:40" translate="no">00:01:40</a>) デジタルデータの断捨離とハードウェアの有効活用をどう両立させたか、その全貌をご紹介します。<br>
(<a href="#" class="imk-cue" data-seek="1:49" translate="no">00:01:49</a>) まず、開発のきっかけとなった動機についてお話しします。<br>
(<a href="#" class="imk-cue" data-seek="1:54" translate="no">00:01:54</a>) 私は長年、SynologyのNASとMac miniを併用してファイル管理を行ってきました。<br>
(<a href="#" class="imk-cue" data-seek="2:00" translate="no">00:02:00</a>) しかし、ある時、ふと疑問を抱きました。<br>
(<a href="#" class="imk-cue" data-seek="2:03" translate="no">00:02:03</a>) 「NASも結局は小型のコンピュータであり、Mac miniと機能が重複しているのではないか？」と。<br>
(<a href="#" class="imk-cue" data-seek="2:08" translate="no">00:02:08</a>) 手元には強力なプロセッサを持つMac miniがあるのに、データの管理はNASの非力なCPU任せになっている。<br>
(<a href="#" class="imk-cue" data-seek="2:14" translate="no">00:02:14</a>) しかも、RAIDを組んでしまうとHDDの組み合わせに制約が生まれ、自由が利きません。<br>
(<a href="#" class="imk-cue" data-seek="2:22" translate="no">00:02:22</a>) Mac miniの頭脳(OS)を使って、複数のHDDをもっと自由に、<br>
(<a href="#" class="imk-cue" data-seek="2:26" translate="no">00:02:26</a>) まるでレゴブロックのように扱えないか？<br>
(<a href="#" class="imk-cue" data-seek="2:30" translate="no">00:02:30</a>) これが最初の着想でした。<br>
(<a href="#" class="imk-cue" data-seek="2:33" translate="no">00:02:33</a>) もう一つの、そしてより切実な動機が、私自身の年齢と環境の変化です。<br>
(<a href="#" class="imk-cue" data-seek="2:39" translate="no">00:02:39</a>) 高齢になるにつれ、身の回りのものを整理し、「断捨離」を進めたいという思いが強くなりました。<br>
(<a href="#" class="imk-cue" data-seek="2:44" translate="no">00:02:44</a>) デジタルデータも同様です。<br>
(<a href="#" class="imk-cue" data-seek="2:46" translate="no">00:02:46</a>) ただ溜め込むのではなく、必要なものを選別し、不要なものは手放すサイクルを作りたい。<br>
(<a href="#" class="imk-cue" data-seek="2:51" translate="no">00:02:51</a>) また、昨今のHDD価格の高騰も無視できません。<br>
(<a href="#" class="imk-cue" data-seek="2:57" translate="no">00:02:57</a>) RAIDのために高価な同容量のHDDを買い揃えるのではなく、<br>
(<a href="#" class="imk-cue" data-seek="3:02" translate="no">00:03:02</a>) 手元にある古い500GBや1TBのHDDも捨てずに、<br>
(<a href="#" class="imk-cue" data-seek="3:07" translate="no">00:03:07</a>) 資産として有効活用したい。<br>
(<a href="#" class="imk-cue" data-seek="3:10" translate="no">00:03:10</a>) この「整理したい欲求」と「もったいない精神」を技術で解決するために構築したのが、このシステムです。<br>
(<a href="#" class="imk-cue" data-seek="3:19" translate="no">00:03:19</a>) そこで考案したのが、「棚田」というデータ管理モデルです。<br>
(<a href="#" class="imk-cue" data-seek="3:23" translate="no">00:03:23</a>) 水が上流から下流へ流れるように、データも鮮度に応じて、<br>
(<a href="#" class="imk-cue" data-seek="3:28" translate="no">00:03:28</a>) 段々畑を下りていくイメージです。<br>
(<a href="#" class="imk-cue" data-seek="3:30" translate="no">00:03:30</a>) システムでは、大きく3つの段位を定義しています。<br>
(<a href="#" class="imk-cue" data-seek="3:34" translate="no">00:03:34</a>) 一番上が「T0：作業段」。<br>
(<a href="#" class="imk-cue" data-seek="3:37" translate="no">00:03:37</a>) これはDropboxなど、人間やアプリが日々読み書きする、最も動きの激しい領域です。<br>
(<a href="#" class="imk-cue" data-seek="3:43" translate="no">00:03:43</a>) その下が「T1：保管段」。<br>
(<a href="#" class="imk-cue" data-seek="3:46" translate="no">00:03:46</a>) ここからはシステムが管理する領域で、準アクティブなデータを置きます。<br>
(<a href="#" class="imk-cue" data-seek="3:51" translate="no">00:03:51</a>) 一番下が「T2：長期保管段」。<br>
(<a href="#" class="imk-cue" data-seek="3:54" translate="no">00:03:54</a>) ここはアーカイブ領域です。<br>
(<a href="#" class="imk-cue" data-seek="3:56" translate="no">00:03:56</a>) 重要なのは、それぞれの段位に、サイズもメーカーも異なるバラバラのHDDを割り当てられる点です。<br>
(<a href="#" class="imk-cue" data-seek="4:02" translate="no">00:04:02</a>) これをソフトウェアで一つの巨大な階層構造として管理します。<br>
(<a href="#" class="imk-cue" data-seek="4:09" translate="no">00:04:09</a>) このシステムの核心にある設計思想、それが「判断と実行の分離」です。<br>
(<a href="#" class="imk-cue" data-seek="4:14" translate="no">00:04:14</a>) 通常のファイル操作では、人間が「移動」と思った瞬間にファイルが動きます。<br>
(<a href="#" class="imk-cue" data-seek="4:20" translate="no">00:04:20</a>) しかし、それでは「あ、間違えた！」という事故を防げません。<br>
(<a href="#" class="imk-cue" data-seek="4:25" translate="no">00:04:25</a>) このシステムでは、「考える工程（シミュレーション）」と「動かす工程（アプライ）」を完全に分離しました。<br>
(<a href="#" class="imk-cue" data-seek="4:32" translate="no">00:04:32</a>) まず、システムは全ファイルをスキャンし、「もしルール通りに動かしたらどうなるか？」を計算し、<br>
(<a href="#" class="imk-cue" data-seek="4:38" translate="no">00:04:38</a>) データベースに記録します。<br>
(<a href="#" class="imk-cue" data-seek="4:40" translate="no">00:04:40</a>) これを我々は「メタデータ更新」と呼びます。<br>
(<a href="#" class="imk-cue" data-seek="4:44" translate="no">00:04:44</a>) 実体ファイルには指一本触れず、<br>
(<a href="#" class="imk-cue" data-seek="4:47" translate="no">00:04:47</a>) メタデータ上で完璧な計画を立てる。<br>
(<a href="#" class="imk-cue" data-seek="4:50" translate="no">00:04:50</a>) これが第一段階です。<br>
(<a href="#" class="imk-cue" data-seek="4:53" translate="no">00:04:53</a>) このアプローチでは、実体ファイルは「従属物」とみなします。<br>
(<a href="#" class="imk-cue" data-seek="4:57" translate="no">00:04:57</a>) そこにファイルがあるから残すのではなく、<br>
(<a href="#" class="imk-cue" data-seek="5:01" translate="no">00:05:01</a>) 「メタデータという設計図にあるべきと書かれているから、そこに置く」のです。<br>
(<a href="#" class="imk-cue" data-seek="5:05" translate="no">00:05:05</a>) システムは、設定されたパス配下の全ファイルを「棚卸し」します。<br>
(<a href="#" class="imk-cue" data-seek="5:10" translate="no">00:05:10</a>) これにより、管理者の知らない「野良ファイル」が存在することを許しません。<br>
(<a href="#" class="imk-cue" data-seek="5:16" translate="no">00:05:16</a>) 全てのファイルにIDと行き先が振られ、データベース上で管理される。<br>
(<a href="#" class="imk-cue" data-seek="5:20" translate="no">00:05:20</a>) これが「メタデータ至上主義」です。<br>
(<a href="#" class="imk-cue" data-seek="5:25" translate="no">00:05:25</a>) システムの全体像をご覧ください。<br>
(<a href="#" class="imk-cue" data-seek="5:28" translate="no">00:05:28</a>) ユーザーインターフェースには、なんとGoogle Sheetsを使います。<br>
(<a href="#" class="imk-cue" data-seek="5:32" translate="no">00:05:32</a>) 専用のアプリは作りません。<br>
(<a href="#" class="imk-cue" data-seek="5:35" translate="no">00:05:35</a>) スプレッドシート上の「tanada.csv」という設定シートに行き先を書き、セル上のボタンを押す。<br>
(<a href="#" class="imk-cue" data-seek="5:41" translate="no">00:05:41</a>) すると、Mac mini内で待機しているPythonのデーモン（常駐プログラム）がそれを検知し、<br>
(<a href="#" class="imk-cue" data-seek="5:47" translate="no">00:05:47</a>) 裏側でスクリプト群が走り出します。<br>
(<a href="#" class="imk-cue" data-seek="5:51" translate="no">00:05:51</a>) 非常にシンプルな構成ですが、Googleの認証基盤を使うため、セキュリティも堅牢です。<br>
(<a href="#" class="imk-cue" data-seek="5:59" translate="no">00:05:59</a>) さて、ここからは少し技術的な「柔軟性」の話をします。<br>
(<a href="#" class="imk-cue" data-seek="6:04" translate="no">00:06:04</a>) 基本設計では、データはT0からT2へ、重力に従って落ちていく一方でした。<br>
(<a href="#" class="imk-cue" data-seek="6:10" translate="no">00:06:10</a>) しかし運用していると、「T2のHDDを別のPCで使いたいから、空にしたい」という場面が出てきます。<br>
(<a href="#" class="imk-cue" data-seek="6:17" translate="no">00:06:17</a>) そこで、プログラム上の「重力ルール」を緩和し、T2からT1への「逆流」を許可するようにしました。<br>
(<a href="#" class="imk-cue" data-seek="6:24" translate="no">00:06:24</a>) これにより、データは一方通行ではなく、必要に応じてHDD間を行き来できる「リバランス」が可能になりました。<br>
(<a href="#" class="imk-cue" data-seek="6:31" translate="no">00:06:31</a>) RAIDのリビルドのような危険な作業なしに、データの引っ越しができるのです。<br>
(<a href="#" class="imk-cue" data-seek="6:37" translate="no">00:06:37</a>) この逆流を活用した強力な機能が、「限度0TB設定」です。<br>
(<a href="#" class="imk-cue" data-seek="6:43" translate="no">00:06:43</a>) 例えば、T2にある古いHDDを引退させたいとします。<br>
(<a href="#" class="imk-cue" data-seek="6:47" translate="no">00:06:47</a>) やることは一つ。<br>
(<a href="#" class="imk-cue" data-seek="6:49" translate="no">00:06:49</a>) Google Sheetsでその段位の容量限度を「0TB」に書き換えるだけです。<br>
(<a href="#" class="imk-cue" data-seek="6:53" translate="no">00:06:53</a>) シミュレーションを実行すると、システムは<br>
(<a href="#" class="imk-cue" data-seek="6:56" translate="no">00:06:56</a>) 「おっと、T2の容量がゼロになった。ここにあるファイルは溢れてしまう」と判断します。<br>
(<a href="#" class="imk-cue" data-seek="7:02" translate="no">00:07:02</a>) そして自動的に、空き容量のある上位のT1へ、全てのファイルを移動させる計画を立案してくれます。<br>
(<a href="#" class="imk-cue" data-seek="7:09" translate="no">00:07:09</a>) 除外ファイルがあってもエラーにならないよう調整済みですので、ボタン一つで安全にHDDを空っぽにできます。<br>
(<a href="#" class="imk-cue" data-seek="7:19" translate="no">00:07:19</a>) しかし、自動化には恐怖も伴います。<br>
(<a href="#" class="imk-cue" data-seek="7:22" translate="no">00:07:22</a>) もしシステムが暴走して、作業中のDropbox(T0)を古いデータで上書きしてしまったら？<br>
(<a href="#" class="imk-cue" data-seek="7:28" translate="no">00:07:28</a>) これだけは絶対に避けなければなりません。<br>
(<a href="#" class="imk-cue" data-seek="7:31" translate="no">00:07:31</a>) そのため、プログラムの最深部に「T0戻り禁止」という安全装置をハードコードしています。<br>
(<a href="#" class="imk-cue" data-seek="7:36" translate="no">00:07:36</a>) いかなる設定、いかなるバグがあろうとも、「現在の場所がT0以外で、移動先がT0となる操作」は、<br>
(<a href="#" class="imk-cue" data-seek="7:43" translate="no">00:07:43</a>) シミュレーション段階と実行直前の2段階でブロックされ、強制停止します。<br>
(<a href="#" class="imk-cue" data-seek="7:48" translate="no">00:07:48</a>) T0は聖域（サンクチュアリ）であり、システムはそこへ書き込む権限を持ちません。<br>
(<a href="#" class="imk-cue" data-seek="7:52" translate="no">00:07:52</a>) この制約があるからこそ、安心して自動化を任せられるのです。<br>
(<a href="#" class="imk-cue" data-seek="8:02" translate="no">00:08:02</a>) 実際の運用では、いきなりファイルを動かすことはしません。<br>
(<a href="#" class="imk-cue" data-seek="8:06" translate="no">00:08:06</a>) まず、「ドライラン（予行演習）」が行われます。<br>
(<a href="#" class="imk-cue" data-seek="8:10" translate="no">00:08:10</a>) 処理結果はメールで通知され、何個のファイルが動き、どの程度のリスクがあるかが5段階評価で届きます。<br>
(<a href="#" class="imk-cue" data-seek="8:16" translate="no">00:08:16</a>) 「問題なし」という評価を確認してから、初めて人間が「APPLY（適用）」ボタンを押す。<br>
(<a href="#" class="imk-cue" data-seek="8:22" translate="no">00:08:22</a>) 寝ている間に、Mac miniが数テラバイトのデータを整理整頓してくれる。<br>
(<a href="#" class="imk-cue" data-seek="8:28" translate="no">00:08:28</a>) そんな運用が実現しています。<br>
(<a href="#" class="imk-cue" data-seek="8:33" translate="no">00:08:33</a>) 次に、これからの機能である「廃棄（断捨離）の自動化」についてです。<br>
(<a href="#" class="imk-cue" data-seek="8:38" translate="no">00:08:38</a>) データは溜まる一方ですが、手動で消すのは勇気が要ります。<br>
(<a href="#" class="imk-cue" data-seek="8:44" translate="no">00:08:44</a>) そこで、「廃棄処理の規定」というルールを設けます。<br>
(<a href="#" class="imk-cue" data-seek="8:47" translate="no">00:08:47</a>) 例えば、「拡張子が.tmpで、かつ2年以上更新がないファイル」といった条件を定義します。<br>
(<a href="#" class="imk-cue" data-seek="8:53" translate="no">00:08:53</a>) これに合致したファイルは、即座に削除されるのではなく、<br>
(<a href="#" class="imk-cue" data-seek="8:57" translate="no">00:08:57</a>) 「pending_delete」という専用のゴミ箱領域へ移動されます。<br>
(<a href="#" class="imk-cue" data-seek="9:02" translate="no">00:09:02</a>) そこで半年間眠らせて、誰も困らなければ完全に消去する。<br>
(<a href="#" class="imk-cue" data-seek="9:07" translate="no">00:09:07</a>) システムが背中を押してくれることで、デジタル断捨離がスムーズに進みます。<br>
(<a href="#" class="imk-cue" data-seek="9:13" translate="no">00:09:13</a>) 日々の運用は非常にシンプルです。<br>
(<a href="#" class="imk-cue" data-seek="9:16" translate="no">00:09:16</a>) 新しいHDDを買ってきたら、Mac miniに繋ぎ、Google Sheetsの設定行を1行追加するだけ。<br>
(<a href="#" class="imk-cue" data-seek="9:22" translate="no">00:09:22</a>) あとはボタンを押せば、システムが自動的にそのHDDをT1やT2として認識し、<br>
(<a href="#" class="imk-cue" data-seek="9:28" translate="no">00:09:28</a>) データの保存先として組み込みます。<br>
(<a href="#" class="imk-cue" data-seek="9:31" translate="no">00:09:31</a>) 逆にHDDを外す時も、設定を消すだけで、データは他のHDDへ自動的に退避されます。<br>
(<a href="#" class="imk-cue" data-seek="9:38" translate="no">00:09:38</a>) RAIDの再構築のような、胃が痛くなる待ち時間はもうありません。<br>
(<a href="#" class="imk-cue" data-seek="9:44" translate="no">00:09:44</a>) このシステムを導入して、私は「RAID」という呪縛から解放されました。<br>
(<a href="#" class="imk-cue" data-seek="9:50" translate="no">00:09:50</a>) メーカーも容量も違うHDDを自由に組み合わせ、古くなったら交換し、余ったら再利用する。<br>
(<a href="#" class="imk-cue" data-seek="9:56" translate="no">00:09:56</a>) DAS（ダイレクト・アタッチド・ストレージ）として各ディスクが独立しているため、万が一システムが壊れても、<br>
(<a href="#" class="imk-cue" data-seek="10:04" translate="no">00:10:04</a>) HDDを抜いて別のPCに繋げば、そのまま中のデータを読み出せます。<br>
(<a href="#" class="imk-cue" data-seek="10:09" translate="no">00:10:09</a>) 「資産の流動性」と「データの安全性」、この二つを、<br>
(<a href="#" class="imk-cue" data-seek="10:14" translate="no">00:10:14</a>) Mac miniという既存のリソースだけで手に入れることができました。<br>
(<a href="#" class="imk-cue" data-seek="10:20" translate="no">00:10:20</a>) 最後になります。<br>
(<a href="#" class="imk-cue" data-seek="10:22" translate="no">00:10:22</a>) 「DAS棚田式ファイル管理システム」は単なるバックアップツールではありません。<br>
(<a href="#" class="imk-cue" data-seek="10:25" translate="no">00:10:25</a>) 「判断と実行を分離する」という哲学に基づき、<br>
(<a href="#" class="imk-cue" data-seek="10:30" translate="no">00:10:30</a>) 私たちシニア世代が直面する「デジタル遺産の整理」と「資産の有効活用」を解決するためのソリューションです。<br>
(<a href="#" class="imk-cue" data-seek="10:37" translate="no">00:10:37</a>) NASのブラックボックス化に不安を感じている方、<br>
(<a href="#" class="imk-cue" data-seek="10:40" translate="no">00:10:40</a>) 手元のHDDをもっと自由に使いこなしたい方。<br>
(<a href="#" class="imk-cue" data-seek="10:44" translate="no">00:10:44</a>) 是非、ご自身のMac miniに「棚田」を作ってみてはいかがでしょうか。<br>
(<a href="#" class="imk-cue" data-seek="10:49" translate="no">00:10:49</a>) ご静聴、ありがとうございました。<br>
</p> </details>
<style>
details { font: 16px "Open Sans", Calibri, sans-serif; width: 100%; }
details > summary { padding: 2px 6px; width: 100%; background-color: #ddd; border: none; box-shadow: 3px 3px 4px black; cursor: pointer; list-style: none; }
details > p { font: 14px "Open Sans", Calibri, sans-serif; height:150px; overflow: scroll; background-color: #EDF7FF; padding: 2px 6px; margin: 0; box-shadow: 3px 3px 4px black; }
</style>

  </div>

  <script src="https://player.vimeo.com/api/player.js"></script>

  <script>
  (function(){
    var me = document.currentScript;
    var wrapper = me ? me.closest('.sc-vimeo-embed') : null;

    var target = wrapper ? wrapper : document;
    var iframe = target.querySelector('iframe');
    if (!iframe || !window.Vimeo || !Vimeo.Player) return;

    var player = new Vimeo.Player(iframe);

    /* --- 1. 確実に一時停止してから別ページを開く処理 --- */
    window.stopVimeoBeforeNavigate = function(event, link) {
      event.preventDefault(); 
      player.pause().then(function() {
        setTimeout(function() {
           window.open(link.href, link.target);
        }, 50);
      }).catch(function() {
        window.open(link.href, link.target);
      });
    };

    /* --- 2. 字幕のON/OFF制御（Vimeo API仕様） --- */
    var currentMode = "hidden";
    
    function useNormalMode() {
        currentMode = "hidden";
        player.disableTextTrack().catch(function(e){});
    }
    
    function useSpecialMode() {
        currentMode = "showing";
        // 日本語の subtitles が無ければ captions を探す安全設計
        player.enableTextTrack('ja', 'subtitles').catch(function() {
            player.enableTextTrack('ja', 'captions').catch(function() {});
        });
    }

    // ロード直後は枠内字幕を非表示にする
    player.ready().then(function() {
        useNormalMode();
    });

    // Vimeoプレーヤーの全画面・PiPイベントを監視して字幕を切り替え
    player.on('fullscreenchange', function(data) {
        if (data.fullscreen) useSpecialMode();
        else useNormalMode();
    });
    player.on('enterpictureinpicture', function() {
        useSpecialMode();
    });
    player.on('leavepictureinpicture', function() {
        useNormalMode();
    });

    /* --- 3. 字幕リストの初期化（自動梱包とデフォルト展開） --- */
    var checks = 0;
    var initTimer = setInterval(function(){
        var listContainer = target.querySelector('details > p');
        if (listContainer) {
            initSubtitles(listContainer);
            clearInterval(initTimer);
        }
        checks++;
        if (checks > 20) clearInterval(initTimer); // 10秒経ったら監視終了
    }, 500);

    function initSubtitles(listContainer) {
        // デフォルトで開く
        var detailsEl = target.querySelector("details");
        if (detailsEl) {
            detailsEl.open = true; 
            var summaryEl = detailsEl.querySelector("summary");
            if (summaryEl) summaryEl.textContent = "字幕(シーン)はここをクリック";
        }

        // 行ごとに見えない箱(span)で梱包
        if (!listContainer.dataset.formatted) {
            var html = listContainer.innerHTML;
            var lines = html.split(/<br\s*\/?>/i);
            var newHtml = "";
            for(var j=0; j<lines.length; j++) {
                if(lines[j].trim() === "") continue;
                newHtml += "<span class='imk-line'>" + lines[j] + "</span><br>";
            }
            listContainer.innerHTML = newHtml;
            listContainer.dataset.formatted = "true";
        }
    }

    /* --- 4. クリックシーク機能 --- */
    function parseTs(ts) {
      if (!ts) return null;
      var t = ts.trim();
      var parts = t.split(':').map(function(v){ return parseInt(v, 10); });
      if (parts.length === 2 && parts.every(Number.isFinite)) {
        return parts[0] * 60 + parts[1];
      } else if (parts.length === 3 && parts.every(Number.isFinite)) {
        return parts[0] * 3600 + parts[1] * 60 + parts[2];
      }
      return null;
    }

    target.addEventListener('click', function(e) {
      var a = e.target.closest('a.imk-cue');
      if (!a) return;
      e.preventDefault();
      var sec = null;
      if (a.dataset.seconds) {
        sec = Number(a.dataset.seconds);
      } else if (a.dataset.seek) {
        sec = parseTs(a.dataset.seek);
      }
      if (sec !== null && Number.isFinite(sec)) {
        player.setCurrentTime(sec).then(function(){ player.play(); }).catch(function(){});
      }
    });

    /* --- 5. 同期ハイライトと自動スクロール --- */
    player.on('timeupdate', function(data) {
      // Vimeoプレーヤーが勝手に字幕を出していたら隠す（最強ガード）
      player.getTextTracks().then(function(tracks) {
          var activeTrack = tracks.find(function(t){ return t.mode === 'showing'; });
          if (currentMode === "hidden" && activeTrack) {
              useNormalMode();
          }
      }).catch(function(){});

      var currentTime = data.seconds;
      var listContainer = target.querySelector('details > p');
      if (!listContainer) return;

      var cues = listContainer.querySelectorAll('a.imk-cue');
      if (cues.length === 0) return;

      var activeA = null;
      for (var i = 0; i < cues.length; i++) {
        var sec = null;
        if (cues[i].dataset.seconds) {
            sec = Number(cues[i].dataset.seconds);
        } else if (cues[i].dataset.seek) {
            sec = parseTs(cues[i].getAttribute('data-seek'));
        }
        if (sec !== null && currentTime >= sec - 0.5) {
          activeA = cues[i];
        } else if (sec > currentTime) {
          break;
        }
      }

      if (activeA) {
          var activeLine = activeA.closest(".imk-line");
          if (!activeLine) activeLine = activeA;

          if (activeLine.classList.contains("active-hl")) return;

          // 全行の色を徹底的にリセット
          var allLines = listContainer.querySelectorAll(".imk-line");
          for(var k=0; k<allLines.length; k++) {
              allLines[k].classList.remove("active-hl");
              allLines[k].removeAttribute("style");
          }
          var allLinks = listContainer.querySelectorAll("a");
          for(var m=0; m<allLinks.length; m++) {
              allLinks[m].classList.remove("active-hl");
              allLinks[m].removeAttribute("style");
          }

          // 新しい行に赤文字・黄色背景を直接塗る
          activeLine.classList.add("active-hl");
          activeLine.style.setProperty("background-color", "#ffff00", "important");
          activeLine.style.setProperty("color", "red", "important");
          activeLine.style.setProperty("font-weight", "normal", "important");
          
          var newLinks = activeLine.querySelectorAll("a");
          for(var n=0; n<newLinks.length; n++) {
              newLinks[n].style.setProperty("color", "red", "important");
              newLinks[n].style.setProperty("text-decoration", "none", "important");
          }

          // スクロール処理
          if (listContainer.offsetParent !== null) {
              var containerRect = listContainer.getBoundingClientRect();
              var activeRect = activeLine.getBoundingClientRect();
              var targetScroll = listContainer.scrollTop + (activeRect.top - containerRect.top) - (listContainer.clientHeight / 2) + (activeLine.clientHeight / 2);
              listContainer.scrollTo({
                  top: targetScroll,
                  behavior: "smooth"
              });
          }
      }
    });
  })();
  </script>

  <style>
  .sc-vimeo-embed .iframe-wrapper {
    position: relative;
    padding-bottom: 56.25%;
    height: 0;
    overflow: visible;
  }
  .sc-vimeo-embed .iframe-wrapper iframe {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
  }

  /* 行梱包時の基本スタイル（干渉防止のためクラス内で指定） */
  .sc-vimeo-embed .imk-line {
      display: inline-block;
      width: 100%;
      border-radius: 2px;
      transition: background-color 0.1s;
  }

  /* ▼ 字幕一覧（details）の見た目調整 */
  .sc-vimeo-embed details > p {
    font-size: 14px !important;
    line-height: 1.6;
    height: 200px !important; 
    overflow: auto;
    background-color: #EDF7FF;
    padding: 2px 6px;
    margin: 0;
    box-shadow: 3px 3px 4px black;
    position: relative; 
  }
  .sc-vimeo-embed details > summary {
    font-size: 14px !important;
    padding: 2px 6px;
    width: 100%;
    background-color: #ddd;
    border: none;
    box-shadow: 3px 3px 4px black;
    cursor: pointer;
    list-style: none;
  }
  </style>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">ポイント解説</h3>



<h5 class="wp-block-heading">（１）脱RAID(DAS)のメリット・デメリット</h5>



<p></p>



<div class="wp-block-cocoon-blocks-toggle-box-1 toggle-wrap toggle-box block-box not-nested-style cocoon-block-toggle"><input id="toggle-checkbox-202603170359390" class="toggle-checkbox" type="checkbox"/><label class="toggle-button" for="toggle-checkbox-202603170359390"><strong><span class="marker"><span class="fz-16px">メリット</span></span></strong></label><div class="toggle-content">
<figure class="wp-block-flexible-table-block-table"><table class="has-fixed-layout"><tbody><tr><td style="background-color:#dddddd"><strong>1. 異種混合ドライブの柔軟な活用（ハードウェア制約の撤廃）</strong></td></tr><tr><td><strong>サイズ違いの混在が可能:</strong><br>手元にある4TBのHDD、8TBのHDD、1TBのSSDといった、容量や規格がバラバラのドライブを自由に組み合わせることができます。これらを「T1（保管段）」「T2（長期保管段）」といった論理的な階層（棚田）に割り当てることで、一つの大きなストレージプールとして扱えます。</td></tr><tr><td><strong>コスト効率の最大化:</strong><br>高騰する大容量HDDを新たにペアで購入する必要がなく、余っているドライブや安価に入手できた単体のドライブを即座に戦力として投入できるため、経済的です。</td></tr><tr><td><strong>「処理ID」による独立したエコシステムの形成:</strong><br><code>tanada.csv</code>における「処理ID（例：<code>T0001</code>）」は、それぞれが独立したエコシステムを定義します。このシステムは常設する必要はありません。展示会の仮設システム用にHDDをかき集めたい、などという時にも使えます。</td></tr><tr><td style="background-color:#dddddd"><strong>2. 「資産の流動性」と安全な転用（HDDのレゴブロック化）</strong></td></tr><tr><td><strong>設定ひとつでHDDを空にする:</strong><br>例えば、T2（長期保管段）で使用している8TBのHDDを別のPCへ転用したい場合、Google Sheetsの設定（tanada.csv）でT2の容量制限を「0.0TB」に変更するだけです。</td></tr><tr><td><strong>自動かつ安全な退避:</strong><br>容量を0に設定してシミュレーションを実行すると、システムは「T2にはファイルを置けない」と判断し、T2内の全データを自動的に上位のT1（または他の空き領域）へ移動（逆流）させる「完璧な計画書」を作成します。</td></tr><tr><td><strong>物理作業の簡素化:</strong><br>承認ボタンを押して自動移動（Apply）が完了すれば、HDDは空っぽになるため、安心して取り外して転用できます。</td></tr><tr><td><strong>単体での可読性(プラグアンドプレイが出来る):</strong><br>NASやRAIDのアレイ（RAID 5など）に組み込まれたHDDは、1本だけ引き抜いて他のPCにUSB接続してもデータを読み出せません。しかし、このシステムのDAS構成では各HDDが独立した標準フォーマット（APFSやexFAT等）でフォーマットされているため、USBケーブルを抜いて別のPCに挿す（プラグアンドプレイ）だけで、即座に普通のHDDとしてデータを読み書きできます。</td></tr><tr><td style="background-color:#dddddd"><strong>3. 復旧プロセスの単純化（ボリュームリネームによる復旧）</strong></td></tr><tr><td><strong>シンプルなミラーリング:</strong><br>各段位（T0, T1&#8230;）に対して「main」と「mirror」というボリュームを用意し、FreeFileSyncなどの一般的なアプリで毎日コピーを作成できます。</td></tr><tr><td><strong>ボリューム名の変更で復旧:</strong><br>もし「T0_main」のディスクが故障した場合、バックアップである「T0_mirror」のボリューム名を「T0_main」に変更するだけで、システムは何事もなかったかのように稼働を再開できます。これはRAIDのリビルド待ち時間や専門知識を必要としない、極めて直感的な復旧方法です。</td></tr><tr><td style="background-color:#dddddd"><strong>4. 検索・閲覧の単純化</strong>:</td></tr><tr><td>巨大なフォルダ（例：「写真」）が、容量の都合で物理的にはT1とT2の2つのディスクに分断されて保存されていたとしても、ファイルパス（ディレクトリ構造）が完全に維持されているため、それらを重ね合わせて「一つの巨大な写真フォルダ」として閲覧することが容易です。</td></tr><tr><td style="background-color:#dddddd"><strong>5. 事故を防ぐ「判断」と「実行」の分離:</strong></td></tr><tr><td><strong>非破壊シミュレーション:</strong><br>実際のファイルを動かす前に、メタデータ上で移動計画を立てます。もし容量不足や設定ミスがあっても、実データは一切変更されていないため、何度でも設定をやり直すことができます。</td></tr><tr><td><strong>意図せぬ消失の防止:</strong><br>システムは承認された計画（Apply Plan DB）にある移動（MOVE）のみを愚直に実行し、計画にないファイルには一切触れないため、手作業のファイル整理で起こりがちな「誤って消去した」「どこへ行ったか分からない」という事故を原理的に防ぎます。</td></tr></tbody></table></figure>
</div></div>



<a rel="noopener" target="_blank" href="https://docs.google.com/drawings/d/1reHOYzdg1Zffd4MgP3MSSSWX9cRZMBoUtOMF6KqRe1o/edit?usp=sharing" 
>
<img decoding="async" src="https://docs.google.com/drawings/d/e/2PACX-1vQcI3mRZHmQjds4VBGxyCXx9XjR6xlb3dNdStLRcrd0roec6ukstckpztOs1PZempSw4IqLvMPbq-k3/pub?w=1920&#038;h=1440"
></a>



<div class="wp-block-cocoon-blocks-toggle-box-1 toggle-wrap toggle-box block-box not-nested-style cocoon-block-toggle"><input id="toggle-checkbox-202603170359391" class="toggle-checkbox" type="checkbox"/><label class="toggle-button" for="toggle-checkbox-202603170359391"><strong><span class="marker">デメリット</span></strong></label><div class="toggle-content">
<figure class="wp-block-flexible-table-block-table"><table class="has-fixed-layout"><tbody><tr><td style="background-color:#dddddd"><strong>1. 「可用性」の低下（ダウンタイムの発生）</strong></td></tr><tr><td><strong>復旧には手動介入が必要:</strong><br>本システムでは、各段位（T0, T1&#8230;）に対して「main」と「mirror」という独立したボリュームを作成し、同期アプリでコピーを取る運用が想定されています。もし「main」が故障した場合、RAIDのように自動で切り替わるのではなく、ユーザーが「mirror」のボリューム名を「main」に書き換える（リネームする）という復旧作業を行うまで、システムは停止します。</td></tr><tr><td><strong>リアルタイム同期ではない:</strong><br>常時同期するRAIDと異なり、FreeFileSyncなどで「毎日1回コピー」する運用の場合、故障したタイミングによっては、前回バックアップ以降のデータ（最大1日分）が失われる可能性があります。これを防ぐために**「作業は原則T0（Dropbox等）で行う」**というシステムの運用ルールを設定して、ハードウェア障害時のリスクを最小限に抑える「防波堤」としています。</td></tr><tr><td style="background-color:#dddddd"><strong>2. システム構築・維持の複雑さ（属人化のリスク）</strong></td></tr><tr><td><strong>環境依存:</strong><br>Pythonスクリプト、Google Cloud Platformの認証設定（JSONキー）、Macのフォルダ構造（<code>/Users/xxxxxxm1/...</code>）、LaunchAgentによるデーモン管理など、多くの依存関係の上に成り立っています。</td></tr><tr><td><strong>トラブルシューティングの難易度:</strong><br>もし「ボタンを押しても動かない」という事態になった場合、原因がGoogle APIの仕様変更なのか、ローカルのPython環境なのか、デーモンの停止なのかを切り分けるスキルが求められます。そんな理由もあって、ブログで中身を公開するとともに、AI(Gemeni/ChatGPTなど)の支援が受けやすいように考えています。</td></tr></tbody></table></figure>
</div></div>



<h5 class="wp-block-heading">（２）自宅のどんな場所で使っているのか。</h5>



<p>棚田システムは非常に柔軟で、M1Mac miniで十分だし、その外付けドライブであれば（というかM1Macが認識できるドライブであれば)どれでも選定できます。現状、下の図の赤丸T0T1T2に棚田システムを設定しています。調べてみて自分で驚いたのですが、外付けドライブを11個も使っていました。まだあと外してあるものも数個あります。別に壊れているわけではないのに。資源やお金を無駄にしてきたと思います。<br>世界全体が巨大なRAIDで動いているために、実際どれくらい冗長に重複所有になっているか。AI需要がそれを爆増させているわけなので、あとあと、とてつもない廃棄物が出る。バブルの崩壊が起きそうですね。。ああ怖い。有効活用のことを本気で考えるべき時がやってきそうな気がします。</p>



<a rel="noopener" target="_blank" href="https://docs.google.com/drawings/d/1INrFZ_Q8zmGtWpZq7OUuv2RKofQrlrLqYylZwLLlxSY/edit?usp=sharing" 
>
<img decoding="async" src="https://docs.google.com/drawings/d/e/2PACX-1vSmkRqDIHqNWLvNC5vzih9yDGCf0In1GISEp-aO4ZLmZm6JMxR6Qt-fnozT8hD275HJJiThM0vnKyqH/pub?w=1920&#038;h=1440"
></a>



<p>棚段の基本構成は、作業段(T0)、保管段(T1)、長期保管段(T2)の３構成ですが、流出流入の関係を規定すれば、もっと増やすこともできますし、逆に、２構成にするのもいいと思います。実際、私は、高齢者ですので、これからは減らす方向でいきたいと思っています。</p>



<h5 class="wp-block-heading">（３）実装</h5>



<p>下記のトグルボックスにある青ボタンを押してtxtファイルをダウンロードして、AI(Gemini/ChatGPTなど)に理解させ、ご自身の環境に合わせた実装手順をお進みください。</p>



<div class="wp-block-cocoon-blocks-toggle-box-1 toggle-wrap toggle-box block-box not-nested-style cocoon-block-toggle"><input id="toggle-checkbox-202603170359402" class="toggle-checkbox" type="checkbox"/><label class="toggle-button" for="toggle-checkbox-202603170359402"><strong><span class="marker">実装手順</span></strong></label><div class="toggle-content">
<figure class="wp-block-flexible-table-block-table"><table class="has-fixed-layout"><tbody><tr><td><strong>DAS棚田処理_メタデータ実データ更新アルゴリズム：</strong></td></tr><tr><td><a href="https://imakat.com/script_list/?pubtxt=DAS棚田処理_メタデータ実データ更新アルゴリズム_pub.txt" target="_blank"><img decoding="async" src="https://imakat.com/rd.php?id=zrU95MiY.png" alt="" style="width:120px; height:auto;"></a></td></tr><tr><td><strong>DAS棚田処理_メタデータDB仕様書：</strong></td></tr><tr><td><a href="https://imakat.com/script_list/?pubtxt=DAS棚田管理メタデータDB仕様書_pub.txt" target="_blank"><img decoding="async" src="https://imakat.com/rd.php?id=zrU95MiY.png" alt="" style="width:120px; height:auto;"></a></td></tr><tr><td><strong>DAS棚田管理_メタデータ処理py_shコード：</strong><br>要約：Google Sheetsの「tanada」シートを設定CSV（tanada.csv）へ同期し、そのD列「転出パス」配下を全走査して世代SQLite（scan DB）を作成、さらにscan DB＋tanada.csvから全件の計画SQLite（plan DB）を1つ生成して「KEEP/MOVE/EXCLUDE」と新旧パス（abs_path/to_abs_path）を確定する一連のメタデータ用パイプライン。D列は1セル内の「,」や改行で複数パス指定可として分解し、重複は深いroot優先で排除。analyzeでは処理idごとに「転出パス1つ・転入パス1つ・除外パス複数可」を強制し、除外容量を差し引いた限度内でmtime降順に累積して新段位を決定、逆流（転出段≥転入段）はpipelineで拒否。最後にplan DBを集計し「どこ→どこへ/件数/容量」をMail.appで通知（print-only可）。das_runner.shがexport→pipeline→notifyをボタン1発で実行するランチャ。</td></tr><tr><td><a href="https://imakat.com/script_list/?pubtxt=DAS棚田管理_メタデータ処理py_sh_pub.txt" target="_blank"><img decoding="async" src="https://imakat.com/rd.php?id=zrU95MiY.png" alt="" style="width:120px; height:auto;"></a></td></tr><tr><td><strong>DAS棚田管理_実データ更新処理コード：</strong><br>要約：このシステムは、Google Sheetsをインターフェースとしたファイル管理の自動化パイプラインです。<code>tanada_apply_button_daemon.py</code>は、Google Sheets上の手動ボタン（A10）によるAPPLY要求のみを監視し、深夜時間帯を除いて<code>tanada_apply.py</code>を起動します。<code>apply_runner.sh</code>は、SheetsのA8セルからファイル移動計画DBのパスを読み込み、それを固定名の適用DBにコピーし、<code>tanada_apply.py</code>を実行するためのシェルスクリプトです。コアロジックを担う<code>tanada_apply.py</code>は、適用DBに記録されたアクション（主にファイルの移動）をDRYRUNまたはAPPLYモードで実行します。このスクリプトは、「現段位がT0ではないファイルを新段位T0へ移動する」ポリシー違反を最優先でチェックし、違反があれば直ちに処理を停止する安全機構を備えています。処理結果はSheetsの指定セルにフィードバックされます。<code>tanada_make_apply_plan_db.py</code>は、シミュレーション結果DBから、<code>tanada_apply.py</code>が使用する固定名の適用計画DBを生成し、古いDBは世代管理してアーカイブします。<code>com.xxxxxxxxm1.tanada_apply_button_daemon.plist</code>は、デーモンをmacOSのLaunchAgentとして自動起動・常駐させ、必要な環境変数を設定します。このパイプラインは、手動トリガーと厳格な安全ポリシーにより、ファイル移動の誤操作を防ぎながらも自動化を実現しています。</td></tr><tr><td><a href="https://imakat.com/script_list/?pubtxt=DAS棚田管理_実データ更新処理_pub.txt" target="_blank"></a><a href="https://imakat.com/script_list/?pubtxt=DAS棚田管理_実データ更新処理_pub.txt" target="_blank"><img decoding="async" src="https://imakat.com/rd.php?id=zrU95MiY.png" alt="" style="width:120px; height:auto;"></a></td></tr><tr><td><strong>DAS棚田管理_廃棄処理の規定：</strong></td></tr><tr><td><a href="https://imakat.com/script_list/?pubtxt=DAS棚田管理_廃棄処理の規定_pub.txt" target="_blank"><img decoding="async" src="https://imakat.com/rd.php?id=zrU95MiY.png" alt="" style="width:120px; height:auto;"></a></td></tr><tr><td> コード(スクリプト)をもっと知りたい方はこちらへ↓</td></tr><tr><td><a rel="noopener" target="_blank" href="https://gemini.google.com/gem/1Dp-NLSA5j5OXE3B1Ki2IaBRCvq4CGa6t?usp=sharing"><img decoding="async" src="https://imakat.com/rd.php?id=s7BMZHhB.png" alt="" style="width:120px; height:auto;"></a></td></tr></tbody></table></figure>
</div></div>



<h5 class="wp-block-heading">（４）まとめ</h5>



<p>残りの、廃棄処理、FreeFileSyncによるミラー同期処理、シャドウ・ディレクトリの作成、に関するコードは、完成し次第掲載していきます。ただし、他の処理への影響はありませんので、上の内容をそのまま使っても動きます。</p>



<p></p>
]]></content:encoded>
					
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">28134</post-id>	</item>
		<item>
		<title>pCloudを動画や画像の配信に使う方法</title>
		<link>https://imakat.com/2025/12/14/28025/</link>
		
		<dc:creator><![CDATA[imakat]]></dc:creator>
		<pubDate>Sun, 14 Dec 2025 02:54:35 +0000</pubDate>
				<category><![CDATA[マイライブラリ]]></category>
		<category><![CDATA[WordPress]]></category>
		<category><![CDATA[デジタル]]></category>
		<category><![CDATA[ものづくり]]></category>
		<category><![CDATA[Dropbox]]></category>
		<category><![CDATA[pcloud]]></category>
		<category><![CDATA[CDN]]></category>
		<category><![CDATA[ダイレクトリンク]]></category>
		<guid isPermaLink="false">https://imakat.com/?p=28025</guid>

					<description><![CDATA[〜pCloudはスイスにあるクラウドストレージを提供する会社〜 最近、Black Fridayなどで目にすることが増えたpCloud。スイスにあるクラウドストレージを提供する会社で、「買い切りプランも選べる、プライバシー [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p><strong>〜pCloudはスイスにあるクラウドストレージを提供する会社〜</strong></p>



<p>最近、Black Fridayなどで目にすることが増えたpCloud。スイスにあるクラウドストレージを提供する会社で、「買い切りプランも選べる、プライバシー重視のオンライン倉庫」です。ここで紹介しますが、何も営業的な意図はありません。</p>



<p><strong>～Dropboxが廃止したパブリックフォルダを、pCloudでは使える～</strong></p>



<figure class="wp-block-image size-large is-resized"><a href="https://imakat.com/rd.php?id=lVcTFKgE.png" target="_blank"><img decoding="async" src="https://imakat.com/rd.php?id=lVcTFKgE.png" alt="" style="aspect-ratio:1.5000155836060465;width:170px;height:auto"/></a></figure>



<p><br>昔からのDropboxユーザーなら記憶にあることですが、以前はパブリックフォルダがあり、ここからWeb配信が可能でした。Dropboxは企業やビジネス用途が多くて漏洩厳禁なところ、フォルダを間違えて公開するようなうっかりミスが多発することも問題だったようです。Dropboxのパブリックフォルダは「便利すぎて危険」「無料すぎて悪用される」「事業の軸と合わない」として廃止したわけです。pCloudは、パブリックフォルダは、もともと公開利用を前提に設計されていて、それを売りにしていることもあるので、一応大丈夫だとは思います。ただ、いずれにしても、クラウドストレージをメディア配信用に使うときには、２つの心得があります。それは、<strong>一つは、配信サーバーが提供する共有リンクをそのまま公開しないこと、もう一つは、一ヶ所の配信サーバーに依存しないこと</strong>です。<br>それでは、解説ビデオをご覧ください。<br></p>


<div class="sc-dynamic-embed">
  <style>
  /* リンクの見た目を整える */
  .sc-dynamic-embed .sc-link-container { 
      display: flex; 
      margin-bottom: 10px; 
      flex-wrap: wrap; 
      align-items: center; 
  }
  .sc-dynamic-embed .sc-link { margin-bottom: 0; }
  .sc-dynamic-embed .sc-link a {
    font-size: 15px; /* dynamicと合わせた基本サイズ */
    font-weight: normal;
    text-decoration: underline;
    color: #0073aa;
  }
  .sc-dynamic-embed .sc-link a:hover { text-decoration: none; color: #000; }
  
  /* ★スマホ画面（幅500px以下）の時は文字を縮小して統一感を出す */
  @media (max-width: 500px) {
    .sc-dynamic-embed .sc-link a { font-size: 13px; }
  }

  /* 行梱包時の基本スタイル */
  .sc-dynamic-embed .imk-line {
      display: inline-block;
      width: 100%;
      border-radius: 2px;
      transition: background-color 0.1s;
  }

  /* 古い枠内字幕ボックスを強制消去 */
  .sc-dynamic-embed #subtitleOverlay,
  .sc-dynamic-embed #scSubtitleOverlay,
  .sc-dynamic-embed .overlay-cue,
  .sc-dynamic-embed .band {
      display: none !important;
      opacity: 0 !important;
      visibility: hidden !important;
      pointer-events: none !important;
  }
  </style>

  <div class="sc-link-container">
    <p class="sc-link">
      <a href="https://imakat.com/ds62/?drid=68_2" target="_blank"
         onclick="return scStopAndGo(event, this);">
        👉低画質・枠外字幕はこちら
      </a>
    </p>
  </div>

  <style>
            :root{ --dr5emd-max: 1920px; }
            .video-wrap{position:relative;width:100%;margin:0 auto}
            figure.wp-block-video.aligncenter{
              width:100%;
              max-width:min(var(--dr5emd-max, 1920px), 98vw);
              margin:0 auto;
            }
            #subtitleOverlay{
              position:absolute; left:0; right:0; bottom:6%;
              padding:0 2%; text-align:center; pointer-events:none; z-index:2;
            }
            #subtitleOverlay .band{
              display:inline-block; background:rgba(0,0,0,0.35);
              padding:6px 10px; border-radius:8px; max-width:96%;
              margin:0 auto; box-shadow:0 1px 2px rgba(0,0,0,0.15);
            }
            #subtitleOverlay .overlay-cue{
              color:#fff; font-weight:600;
              font-size:clamp(16px, 3.6vw, 32px);
              line-height:1.32; white-space:pre-wrap; margin:2px 0;
              -webkit-text-stroke:.6px rgba(0,0,0,.7);
              text-shadow:-1px -1px 0 rgba(0,0,0,.6), 1px -1px 0 rgba(0,0,0,.6),
                          -1px  1px 0 rgba(0,0,0,.6), 1px  1px 0 rgba(0,0,0,.6);
            }
            @media (max-width:430px){
              #subtitleOverlay .overlay-cue{ font-size:clamp(16px, 4.2vw, 22px); }
            }
            .dr5emd-sublist details > p{
              height:200px; overflow:auto; background-color:#EDF7FF;
              padding:2px 6px; margin:0; box-shadow:3px 3px 4px black;
              position: relative;
            }
            .dr5emd-sublist details > summary{
              padding:2px 6px; width:100%;
              background-color:#ddd; border:none;
              box-shadow:3px 3px 4px black; cursor:pointer; list-style:none;
            }
            /* ▼ 自動スクロール時のハイライト（文字の太さを標準へ変更） */
            .active-hl {
                background-color: #ffff00 !important;
                color: #ff0000 !important;
                font-weight: normal; /* 標準の太さ */
                border-bottom: 2px solid red;
                display: inline-block;
                border-radius: 2px;
            }
            </style><div class="dr5emd-container"><figure class="wp-block-video aligncenter"><div class="video-wrap"><video id="myVideo" controls controlsList="nodownload" poster="https://imakat.com/rd.php?id=HpJbDDCm.png" playsinline preload="metadata" style="width:100%;height:auto;">  <source src="https://imakat.com/rd.php?id=PPk0v48b.mp4" type="video/mp4">  <track src="https://imakat.com/rd.php?id=cuPsc5PE.vtt" label="日本語" srclang="ja" kind="subtitles"></video><div id="subtitleOverlay" aria-hidden="true"></div></div><script>
document.addEventListener("DOMContentLoaded", function(){
  var video=document.getElementById("myVideo");
  var trackEl=video?video.querySelector("track[kind='subtitles'], track[kind='captions']"):null;
  var overlay=document.getElementById("subtitleOverlay"); if(!video||!overlay) return;
  video.addEventListener("contextmenu", function(e){ e.preventDefault(); return false; }, false);
  function setNative(mode){
    try{
      if(video.textTracks && video.textTracks.length){
        for(var i=0;i<video.textTracks.length;i++){ video.textTracks[i].mode = mode; }
      }
      if(trackEl && trackEl.track) trackEl.track.mode = mode;
    }catch(e){}
  }
  var isOverlay=true,lastSig="";
  function sig(active){if(!active||active.length===0)return"";var a=[];for(var i=0;i<active.length;i++){var c=active[i];a.push([c.startTime,c.endTime,c.text].join("|"));}return a.join("||");}
  function cueLine(c){var d=document.createElement("div");d.className="overlay-cue";d.setAttribute("translate","yes");if(typeof c.getCueAsHTML==="function")d.appendChild(c.getCueAsHTML());else d.textContent=c.text;return d;}
  function render(){
    if(!isOverlay || !trackEl || !trackEl.track) return;
    var ac=trackEl.track.activeCues,s=sig(ac); if(s===lastSig) return; lastSig=s;
    overlay.innerHTML=""; if(!ac || ac.length===0) return;
    var b=document.createElement("div"); b.className="band"; b.setAttribute("translate","yes");
    Array.from(ac).sort(function(a,b){return a.startTime-b.startTime;}).forEach(function(c){b.appendChild(cueLine(c));});
    overlay.appendChild(b);
  }
  function useOverlay(){isOverlay=true;overlay.style.display="";setNative("hidden");lastSig="";render();}
  function useNative(){isOverlay=false;overlay.style.display="none";setNative("showing");lastSig="";}
  useOverlay();
  if(trackEl){
    if(trackEl.track){ try{ trackEl.track.addEventListener("cuechange",render); }catch(e){} }
    trackEl.addEventListener("load", function(){ try{ if(trackEl.track) trackEl.track.addEventListener("cuechange",render); }catch(e){} render(); });
  }
  video.addEventListener("loadedmetadata",render);
  function handleWebkitMode(){ var m = video.webkitPresentationMode || "inline"; (m==="picture-in-picture"||m==="fullscreen") ? useNative() : useOverlay(); }
  if("webkitPresentationMode" in video){ video.addEventListener("webkitpresentationmodechanged",handleWebkitMode); handleWebkitMode(); }
  if("webkitCurrentPlaybackTargetIsWireless" in video){
    video.addEventListener("webkitcurrentplaybacktargetiswirelesschanged", function(){ video.webkitCurrentPlaybackTargetIsWireless ? useNative() : useOverlay(); });
  }
  if("pictureInPictureEnabled" in document){
    video.addEventListener("enterpictureinpicture",useNative);
    video.addEventListener("leavepictureinpicture",useOverlay);
  }
  document.addEventListener("fullscreenchange", function(){
    var fs=document.fullscreenElement;
    if(!fs) return useOverlay();
    (fs===video || (fs && fs.contains && fs.contains(video))) ? useNative() : useOverlay();
  });
});
</script>
                <figcaption></figcaption></figure><div class="dr5emd-sublist"><details translate="yes"><summary translate="yes">字幕一覧(クリック)</summary> <p translate="yes">
(<a href="#" class="imk-cue" data-seek="0:00" translate="no">00:00:00</a>)  〜pCloudを動画や画像の配信に使う方法〜<br>
(<a href="#" class="imk-cue" data-seek="0:18" translate="no">00:00:18</a>)  今みなさんは、こうしてブログから動画をご覧になっているわけですが　<br>
(<a href="#" class="imk-cue" data-seek="0:28" translate="no">00:00:28</a>)  ＜通信状況の良否で配信サーバーを切り替えます＞<br>
(<a href="#" class="imk-cue" data-seek="0:36" translate="no">00:00:36</a>)  だからそれが何だと言われても、反論の余地はないのですが、<br>
(<a href="#" class="imk-cue" data-seek="0:44" translate="no">00:00:44</a>)  言えるのは、最近、クラウドストレージの中で<br>
(<a href="#" class="imk-cue" data-seek="0:48" translate="no">00:00:48</a>)  このpCloudが、やけにネットで広告が目立つと思いませんか。<br>
(<a href="#" class="imk-cue" data-seek="0:57" translate="no">00:00:57</a>)  多分、買い切りがあるところが魅力なのだと思いますね。<br>
(<a href="#" class="imk-cue" data-seek="1:03" translate="no">00:01:03</a>)  私は2025年に0.5TBを約2万円で購入しました。<br>
(<a href="#" class="imk-cue" data-seek="1:13" translate="no">00:01:13</a>)  私は、このpCloudについては、パブリックフォルダが使える、<br>
(<a href="#" class="imk-cue" data-seek="1:22" translate="no">00:01:22</a>)  パブリックフォルダから動画や画像が配信できる点が、大きな魅力の一つだと思っています。<br>
(<a href="#" class="imk-cue" data-seek="1:32" translate="no">00:01:32</a>)  pCloudのテーマに入る前に、私が運用している、動画画像の配信の流れについて、説明します。<br>
(<a href="#" class="imk-cue" data-seek="1:46" translate="no">00:01:46</a>)  図解します。<br>
(<a href="#" class="imk-cue" data-seek="1:50" translate="no">00:01:50</a>)  動画や画像を制作するための、その元になる材料が、製造工程であるDropboxに入ります。　<br>
(<a href="#" class="imk-cue" data-seek="2:03" translate="no">00:02:03</a>)  FinalCutProや画像ソフト、音楽ソフトで、加工します。<br>
(<a href="#" class="imk-cue" data-seek="2:13" translate="no">00:02:13</a>)  そうしたメディアの完成品のうち、インターネットで配信するものを、配信用アセットと呼びますが、それを保存します。<br>
(<a href="#" class="imk-cue" data-seek="2:27" translate="no">00:02:27</a>)  さらにそれを配信所へ配送します。<br>
(<a href="#" class="imk-cue" data-seek="2:35" translate="no">00:02:35</a>)  Dropboxは、Dropbox自身から直接に配信できます。<br>
(<a href="#" class="imk-cue" data-seek="2:44" translate="no">00:02:44</a>)  産直ですね<br>
(<a href="#" class="imk-cue" data-seek="2:47" translate="no">00:02:47</a>)  他に、Xserver 、pCloud、Vimeo、YouTubeを配信所として利用できるようにしています<br>
(<a href="#" class="imk-cue" data-seek="2:58" translate="no">00:02:58</a>)  配信所には高機能の配信所と普通レベルの配信所があります。<br>
(<a href="#" class="imk-cue" data-seek="3:06" translate="no">00:03:06</a>)  高機能の配信所は、ユーザーの受信環境に応じて、複数の画質を自動的に切り替えてくれる、トランスコーディングという機能を持っています。<br>
(<a href="#" class="imk-cue" data-seek="3:23" translate="no">00:03:23</a>)  Vimeo、Youtubeがそれになります<br>
(<a href="#" class="imk-cue" data-seek="3:28" translate="no">00:03:28</a>)  まあ配信専業の会社のサーバーですね。<br>
(<a href="#" class="imk-cue" data-seek="3:36" translate="no">00:03:36</a>)  それに対して、普通レベルの配信所は、画質の自動切り替えはできません。<br>
(<a href="#" class="imk-cue" data-seek="3:47" translate="no">00:03:47</a>)  私の例だと、Dropbox Xserver pCloud、これらがそうです。<br>
(<a href="#" class="imk-cue" data-seek="3:56" translate="no">00:03:56</a>)  ふつうにクラウドストレージと呼ばれる会社群といっていいですね。<br>
(<a href="#" class="imk-cue" data-seek="4:04" translate="no">00:04:04</a>)  これらはデータを格納するのが主な役割で、配信機能はおまけと言ってもいいかも知れません。<br>
(<a href="#" class="imk-cue" data-seek="4:18" translate="no">00:04:18</a>)  ただコスパが非常にいいので、騙し騙し使えるように、　<br>
(<a href="#" class="imk-cue" data-seek="4:25" translate="no">00:04:25</a>)  高画質動画と低画質動画のどちらかを、ユーザーが選択できるようにしました。<br>
(<a href="#" class="imk-cue" data-seek="4:36" translate="no">00:04:36</a>)  最近、世の中全体の、ネットの高速化が、進んでいますので、<br>
(<a href="#" class="imk-cue" data-seek="4:42" translate="no">00:04:42</a>)  このふつうレベルの配信所でも、十分、実用になっています。<br>
(<a href="#" class="imk-cue" data-seek="4:48" translate="no">00:04:48</a>)  そこまでが、配信所までの流れです。<br>
(<a href="#" class="imk-cue" data-seek="4:52" translate="no">00:04:52</a>)  他方、ユーザーからの表示のリクエストがWordPressに入ってきます。<br>
(<a href="#" class="imk-cue" data-seek="5:00" translate="no">00:05:00</a>)  そのリクエストを配信所へ取りに行く<br>
(<a href="#" class="imk-cue" data-seek="5:08" translate="no">00:05:08</a>)  とってきたものを、表示する、<br>
(<a href="#" class="imk-cue" data-seek="5:13" translate="no">00:05:13</a>)  そういう操作になります。<br>
(<a href="#" class="imk-cue" data-seek="5:20" translate="no">00:05:20</a>)  それでは、次に、今回の主役である、pCloudの活用のテーマに入っていきます。<br>
(<a href="#" class="imk-cue" data-seek="5:32" translate="no">00:05:32</a>)  ふつう、ユーザーが、メディアや書類を、配信したり他人に渡すとき、ダイレクトリンクを使いますよね。<br>
(<a href="#" class="imk-cue" data-seek="5:45" translate="no">00:05:45</a>)  このダイレクトリンクを取得しますね。<br>
(<a href="#" class="imk-cue" data-seek="5:49" translate="no">00:05:49</a>)  あるいは共有リンクと呼びますかね。<br>
(<a href="#" class="imk-cue" data-seek="5:54" translate="no">00:05:54</a>)  そのリンクをメールやブログに貼り付けるわけですね。<br>
(<a href="#" class="imk-cue" data-seek="6:01" translate="no">00:06:01</a>)  ファイル名に日本語が入っていると、こんなような、%の入った、複雑な文字列になっています。<br>
(<a href="#" class="imk-cue" data-seek="6:14" translate="no">00:06:14</a>)  そのリンクをクリックすると、こんなように表示されます。<br>
(<a href="#" class="imk-cue" data-seek="6:23" translate="no">00:06:23</a>)  さらに。。名前変更テスト。。<br>
(<a href="#" class="imk-cue" data-seek="6:28" translate="no">00:06:28</a>)  しかし、ローカル側で、うっかり、ファイルを移動したりファイル名を変更してしまうことがありますよね。<br>
(<a href="#" class="imk-cue" data-seek="6:40" translate="no">00:06:40</a>)  その時に、そのファイルを、ダイレクトリンクを通じて、別の人が利用していたりすると、<br>
(<a href="#" class="imk-cue" data-seek="6:57" translate="no">00:06:57</a>)  表示が不可能となり、迷惑をかけてしまうことになります。<br>
(<a href="#" class="imk-cue" data-seek="7:10" translate="no">00:07:10</a>)  だから、このリンク切れを防ぐ方法を考えたいわけです。人はポカを犯しますからね。<br>
(<a href="#" class="imk-cue" data-seek="7:18" translate="no">00:07:18</a>)  そこで、よく使われる方法が、不変のIDを使う方法です。<br>
(<a href="#" class="imk-cue" data-seek="7:30" translate="no">00:07:30</a>)  まあ、マイナンバーのようなものです。<br>
(<a href="#" class="imk-cue" data-seek="7:33" translate="no">00:07:33</a>)  そのマイナンバーに、さまざまな情報が登録されているような形です。<br>
(<a href="#" class="imk-cue" data-seek="7:40" translate="no">00:07:40</a>)  ファイルパスの変更というのは、住所の変更や氏名の変更と同じことです。<br>
(<a href="#" class="imk-cue" data-seek="7:49" translate="no">00:07:49</a>)  要するに、マイナンバーを使おうという考えです。<br>
(<a href="#" class="imk-cue" data-seek="7:54" translate="no">00:07:54</a>)  ただマイナンバーというと、人に知られては困るものですよね。<br>
(<a href="#" class="imk-cue" data-seek="8:00" translate="no">00:08:00</a>)  ところが、この不変のIDは、マイナンバーと全く違って、公開されるということです。<br>
(<a href="#" class="imk-cue" data-seek="8:09" translate="no">00:08:09</a>)  こんな感じですね。<br>
(<a href="#" class="imk-cue" data-seek="8:14" translate="no">00:08:14</a>)  先におみせした、％のついたダイレクトリンクを使うのではなく、<br>
(<a href="#" class="imk-cue" data-seek="8:20" translate="no">00:08:20</a>)  独自に、不変のID で作った再生URLを、メールやブログに貼り付けて、<br>
(<a href="#" class="imk-cue" data-seek="8:28" translate="no">00:08:28</a>)  提供するようにする、ということです。<br>
(<a href="#" class="imk-cue" data-seek="8:32" translate="no">00:08:32</a>)  こうすることにより、<br>
(<a href="#" class="imk-cue" data-seek="8:40" translate="no">00:08:40</a>)  このURLをクリックしますと、画像が表示されます。<br>
(<a href="#" class="imk-cue" data-seek="8:51" translate="no">00:08:51</a>)  ここで、ファイル名が「さらに」となっていますが、「さらにさらに」と名前を変えたとします。<br>
(<a href="#" class="imk-cue" data-seek="9:06" translate="no">00:09:06</a>)  しかし、再生　URLは、元のままです。画像は同じように表示されます。<br>
(<a href="#" class="imk-cue" data-seek="9:19" translate="no">00:09:19</a>)  次のページからは、ちょっと詳細の話になります。<br>
(<a href="#" class="imk-cue" data-seek="9:29" translate="no">00:09:29</a>)  このwpidexというところに、先ほどの不変のIDが入っていますね。<br>
(<a href="#" class="imk-cue" data-seek="9:44" translate="no">00:09:44</a>)  一方、pCloudサーバー側は、ファイル１つに対して、不変のID=fileidを割り当てています。<br>
(<a href="#" class="imk-cue" data-seek="9:58" translate="no">00:09:58</a>)  この両者をカップリングすれば、いいわけです。<br>
(<a href="#" class="imk-cue" data-seek="10:08" translate="no">00:10:08</a>)  次は、さらに詳細です。<br>
(<a href="#" class="imk-cue" data-seek="10:12" translate="no">00:10:12</a>)  そのfileidを、pCloudサーバーから取得するのには、一工夫が必要になります。<br>
(<a href="#" class="imk-cue" data-seek="10:20" translate="no">00:10:20</a>)  その手順です。<br>
(<a href="#" class="imk-cue" data-seek="10:23" translate="no">00:10:23</a>)  細かいのでブログをお読みいただけばと思います。<br>
(<a href="#" class="imk-cue" data-seek="10:31" translate="no">00:10:31</a>)  最後に豆知識を一つ付けています。これも、ブログをお読みいただければと思います。<br>
(<a href="#" class="imk-cue" data-seek="10:42" translate="no">00:10:42</a>)  それでは、今回の解説はここまでとします。ありがとうございました。<br>
</p> </details>
<style>
details { font: 16px "Open Sans", Calibri, sans-serif; width: 100%; }
details > summary { padding: 2px 6px; width: 100%; background-color: #ddd; border: none; box-shadow: 3px 3px 4px black; cursor: pointer; list-style: none; }
details > p { font: 14px "Open Sans", Calibri, sans-serif; height:150px; overflow: scroll; background-color: #EDF7FF; padding: 2px 6px; margin: 0; box-shadow: 3px 3px 4px black; }
</style>
</div><script>
(function(){
  var root=document.querySelector(".dr5emd-sublist");
  var video=document.getElementById("myVideo");
  if(!root || !video) return;
  function parseTs(ts){
    if(!ts) return null;
    var p=ts.trim().split(":").map(function(x){return parseInt(x,10)||0;});
    if(p.length===2) return p[0]*60 + p[1];
    if(p.length===3) return p[0]*3600 + p[1]*60 + p[2];
    return null;
  }
  root.addEventListener("click", function(e){
    var a=e.target.closest && e.target.closest("a.imk-cue[data-seek]");
    if(!a || !root.contains(a)) return;
    e.preventDefault();
    var sec = parseTs(a.getAttribute("data-seek"));
    if(sec==null) return;
    try{ video.currentTime = sec; if(video.paused) video.play(); }catch(_){}
  });
  video.addEventListener("timeupdate", function(){
    var listContainer = root.querySelector("details > p");
    if(!listContainer) return;
    var cues = listContainer.querySelectorAll("a.imk-cue");
    if(cues.length === 0) return;
    var cur = video.currentTime;
    var active = null;
    for(var i=0; i<cues.length; i++){
        var t = parseTs(cues[i].getAttribute("data-seek"));
        if(t !== null && cur >= t - 0.5){
            active = cues[i];
        } else if(t > cur){
            break;
        }
    }
    if(active){
        if(active.classList.contains("active-hl")) return;
        var old = listContainer.querySelectorAll(".active-hl");
        for(var k=0; k<old.length; k++) old[k].classList.remove("active-hl");
        active.classList.add("active-hl");
        if(listContainer.offsetParent !== null){
            var containerRect = listContainer.getBoundingClientRect();
            var activeRect = active.getBoundingClientRect();
            var targetScroll = listContainer.scrollTop + (activeRect.top - containerRect.top) - (listContainer.clientHeight / 2) + (active.clientHeight / 2);
            listContainer.scrollTo({ top: targetScroll, behavior: "smooth" });
        }
    }
  });
})();
</script>
                </div>

  <script>
  (function(){
    var me = document.currentScript;
    var wrapper = me ? me.closest('.sc-dynamic-embed') : null;

    /* -----------------------------------------------
       1. 動画の保護機能（右クリック禁止・DL防止）
       ----------------------------------------------- */
    function protectVideo() {
      var target = wrapper ? wrapper : document;
      var mediaEls = target.querySelectorAll('video');
      mediaEls.forEach(function(v){
        if(v.dataset.protected === 'true') return;
        v.dataset.protected = 'true';
        v.setAttribute('controlsList', 'nodownload');
        v.oncontextmenu = function() { return false; };
        v.addEventListener('contextmenu', function(e){ e.preventDefault(); return false; }, false);
      });
    }

    /* -----------------------------------------------
       2. 字幕制御＆ハイライト機能（iPhone全画面 完全対応版）
       ----------------------------------------------- */
    function initSubtitles() {
      var target = wrapper ? wrapper : document;
      var video = target.querySelector('video');
      var listContainer = target.querySelector('details > p');
      
      if (!video || !listContainer) return false; 

      if (video.dataset.subInit === 'true') return true; 
      video.dataset.subInit = 'true';

      var oldOverlay = target.querySelector('#subtitleOverlay') || target.querySelector('#scSubtitleOverlay');
      if (oldOverlay) {
          oldOverlay.style.setProperty('display', 'none', 'important');
          oldOverlay.innerHTML = ''; 
      }

      function isSpecialMode() {
        var isFs = !!(document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement);
        var isPiP = !!(document.pictureInPictureElement && document.pictureInPictureElement === video) || (video.webkitPresentationMode === "picture-in-picture");
        var isIOSFs = !!video.webkitDisplayingFullscreen; 
        return isFs || isPiP || isIOSFs;
      }

      try {
        if(video.textTracks && video.textTracks.length > 0){
          for(var i=0; i<video.textTracks.length; i++){
             if(video.textTracks[i].kind === 'subtitles' || video.textTracks[i].kind === 'captions'){
                 video.textTracks[i].mode = "hidden";
             }
          }
        }
      } catch(e){}

      video.addEventListener("webkitbeginfullscreen", function() {
          try {
              if(video.textTracks && video.textTracks.length > 0) {
                  for(var i=0; i<video.textTracks.length; i++){
                      if(video.textTracks[i].kind === 'subtitles' || video.textTracks[i].kind === 'captions') video.textTracks[i].mode = "showing";
                  }
              }
          } catch(e){}
      });
      video.addEventListener("webkitendfullscreen", function() {
          try {
              if(video.textTracks && video.textTracks.length > 0) {
                  for(var i=0; i<video.textTracks.length; i++){
                      if(video.textTracks[i].kind === 'subtitles' || video.textTracks[i].kind === 'captions') video.textTracks[i].mode = "hidden";
                  }
              }
          } catch(e){}
      });

      var detailsEl = target.querySelector("details");
      if (detailsEl) {
          detailsEl.open = true; 
          var summaryEl = detailsEl.querySelector("summary");
          if (summaryEl) summaryEl.textContent = "字幕(シーン)はここをクリック";
      }

      if (!listContainer.dataset.formatted) {
          var html = listContainer.innerHTML;
          var lines = html.split(/<br\s*\/?>/i);
          var newHtml = "";
          for(var j=0; j<lines.length; j++) {
              if(lines[j].trim() === "") continue;
              newHtml += "<span class='imk-line'>" + lines[j] + "</span><br>";
          }
          listContainer.innerHTML = newHtml;
          listContainer.dataset.formatted = "true";
      }

      function parseTs(ts){
        if(!ts) return null;
        var p = ts.trim().split(":").map(function(x){return parseInt(x,10)||0;});
        if(p.length===2) return p[0]*60 + p[1];
        if(p.length===3) return p[0]*3600 + p[1]*60 + p[2];
        return null;
      }

      var rootSublist = target.querySelector(".dr5-sublist") || listContainer.parentElement;
      if (rootSublist) {
        rootSublist.addEventListener("click", function(e){
          var a = e.target.closest && e.target.closest("a.imk-cue[data-seek]");
          if(!a) return;
          e.preventDefault();
          var sec = parseTs(a.getAttribute("data-seek"));
          if(sec==null) return;
          try{ video.currentTime = sec; if(video.paused) video.play(); }catch(_){}
        });
      }

      video.addEventListener("timeupdate", function(){
        var desiredMode = isSpecialMode() ? "showing" : "hidden";
        try {
            if(video.textTracks && video.textTracks.length > 0){
                for(var i=0; i<video.textTracks.length; i++){
                    if((video.textTracks[i].kind === 'subtitles' || video.textTracks[i].kind === 'captions') && video.textTracks[i].mode !== desiredMode) {
                        video.textTracks[i].mode = desiredMode;
                    }
                }
            }
        } catch(e){}

        var cues = listContainer.querySelectorAll("a.imk-cue");
        if(cues.length === 0) return;
        var cur = video.currentTime;
        var activeA = null;

        for(var i=0; i<cues.length; i++){
            var t = parseTs(cues[i].getAttribute("data-seek"));
            if(t !== null && cur >= t - 0.5){ activeA = cues[i]; } 
            else if(t > cur){ break; }
        }

        if(activeA){
            var activeLine = activeA.closest(".imk-line");
            if(!activeLine) activeLine = activeA;

            if(activeLine.classList.contains("active-hl")) return;

            var allLines = listContainer.querySelectorAll(".imk-line");
            for(var k=0; k<allLines.length; k++) {
                allLines[k].classList.remove("active-hl");
                allLines[k].removeAttribute("style"); 
            }
            var allLinks = listContainer.querySelectorAll("a");
            for(var m=0; m<allLinks.length; m++) {
                allLinks[m].classList.remove("active-hl");
                allLinks[m].removeAttribute("style"); 
            }

            activeLine.classList.add("active-hl");
            activeLine.style.setProperty("background-color", "#ffff00", "important");
            activeLine.style.setProperty("color", "red", "important");
            activeLine.style.setProperty("font-weight", "normal", "important");
            
            var newLinks = activeLine.querySelectorAll("a");
            for(var n=0; n<newLinks.length; n++) {
                newLinks[n].style.setProperty("color", "red", "important");
                newLinks[n].style.setProperty("text-decoration", "none", "important");
            }

            if(listContainer.offsetParent !== null){
                var containerRect = listContainer.getBoundingClientRect();
                var activeRect = activeLine.getBoundingClientRect();
                var targetScroll = listContainer.scrollTop + (activeRect.top - containerRect.top) - (listContainer.clientHeight / 2) + (activeLine.clientHeight / 2);
                listContainer.scrollTo({ top: targetScroll, behavior: "smooth" });
            }
        }
      });

      return true;
    }

    /* -----------------------------------------------
       監視タイマー
       ----------------------------------------------- */
    var checks = 0;
    var checkTimer = setInterval(function(){
      protectVideo(); // ★dynamic2は保護を維持
      var success = initSubtitles();
      checks++;
      if (success || checks > 20) { 
        clearInterval(checkTimer);
      }
    }, 500); 

    /* -----------------------------------------------
       3. 画面遷移時の停止機能
       ----------------------------------------------- */
    if (!window.scStopAndGo) {
      window.scStopAndGo = function(event, link){
        try{
          var mediaEls = document.querySelectorAll('video, audio');
          mediaEls.forEach(function(m){
            try{
              if (!m.paused) m.pause();
              if (document.pictureInPictureElement === m && document.exitPictureInPicture) {
                document.exitPictureInPicture().catch(function(){});
              }
            }catch(e){}
          });
        }finally{
          event.preventDefault();
          setTimeout(function(){
            if (link.target === '_blank') {
              window.open(link.href, '_blank');
            } else {
              window.location.href = link.href;
            }
          }, 50);
        }
        return false;
      };
    }
  })();
  </script>
</div>



<h3 class="wp-block-heading">ポイント解説</h3>



<h4 class="wp-block-heading">１　スクリプト</h4>



<p>新規追加または変更があると、wpidexをキー項目とした、Dropboxリンク、直リンク、ファイルパスを中心に成り立つF1_メディアライブラリファイルのsheet1(シート)が最初に更新され、次に以下のpcloudid(シート)が更新されます。<br></p>



<figure class="wp-block-image size-large"><a href="https://imakat.com/rd.php?id=TDvPBRYt.png" target="_blank"><img decoding="async" src="https://imakat.com/rd.php?id=TDvPBRYt.png" alt=""/></a></figure>



<figure class="wp-block-flexible-table-block-table"><table class="has-fixed-layout"><tbody><tr><td>このスクリプトは、Googleスプレッドシートの「pcloudid」と「sheet1」を基に、pCloud上のファイル情報を自動更新し、filedn形式の直接URLを生成・管理するGASです。sheet1(シート)に新規追加された未登録のwpidexをpcloudid(シート)へ自動追加し、needs_updateがTRUEの行のみを対象に、filepathやfileidからpCloud APIを用いてメタ情報とパスを解決します。生成したdirect_urlや更新日時を反映し、最終的に全データをlast_update順に整列したうえで、pcloud_library.jsonをGoogle Drive上の固定IDファイルへ上書き出力します。</td></tr><tr><td><a href="https://imakat.com/script_list/?pubtxt=pCloud用再生URLおよびJSON生成_pub.txt" target="_blank"><img decoding="async" src="https://imakat.com/rd.php?id=zrU95MiY.png" alt="" style="width:120px; height:auto;"></a></td></tr><tr><td>コード(スクリプト)をもっと知りたい方はこちらへ↓</td></tr><tr><td><a rel="noopener" target="_blank" href="https://gemini.google.com/gem/1Dp-NLSA5j5OXE3B1Ki2IaBRCvq4CGa6t?usp=sharing"><img decoding="async" src="https://imakat.com/rd.php?id=s7BMZHhB.png" alt="" style="width:120px; height:auto;"></a></td></tr></tbody></table></figure>



<p></p>



<h4 class="wp-block-heading">２　fileidの取得とダイレクトリンクの生成</h4>



<figure class="wp-block-image size-large"><a href="https://imakat.com/rd.php?id=t2ujp3gF.png" target="_blank"><img decoding="async" src="https://imakat.com/rd.php?id=t2ujp3gF.png" alt=""/></a></figure>



<p></p>



<h4 class="wp-block-heading">３　＜豆知識＞　ファイル名を変更することにより、キャッシュ更新を早める</h4>



<figure class="wp-block-image size-large"><a href="https://imakat.com/rd.php?id=RQ609iga.png" target="_blank"><img decoding="async" src="https://imakat.com/rd.php?id=RQ609iga.png" alt=""/></a></figure>



<p>※：<a rel="noopener" target="_blank" href="https://ja.wikipedia.org/wiki/コンテンツデリバリネットワーク">CDN<span class="fa fa-external-link external-icon anchor-icon"></span></a>方式<br>ファイルパスやファイル名の変更は、一般には、好まれませんが、ファイル名の変更はファイルの更新履歴を新しくするので、新たなファイルとして読み込みが起きます。これは、更新処理のスクリプトが安定していることが必須です。<br><br>以上</p>
]]></content:encoded>
					
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">28025</post-id>	</item>
		<item>
		<title>【Gemini/ChatGPT】スクリプトや記事を日本語要約＋保存するMac用スクリプト</title>
		<link>https://imakat.com/2025/10/12/27668/</link>
		
		<dc:creator><![CDATA[imakat]]></dc:creator>
		<pubDate>Sat, 11 Oct 2025 23:34:29 +0000</pubDate>
				<category><![CDATA[デジタル]]></category>
		<category><![CDATA[ものづくり]]></category>
		<category><![CDATA[Automator]]></category>
		<category><![CDATA[AppleScript]]></category>
		<category><![CDATA[ChatGPT]]></category>
		<category><![CDATA[クイックアクション]]></category>
		<guid isPermaLink="false">https://imakat.com/?p=27668</guid>

					<description><![CDATA[26.1.15追記：【Gemini】Gemini版を追加。文字列をコピーした後、マウスの右クリックで→サービス、から起動する形です。無料でも使えます。 【ChatGPT】～無料版でも動く、ローカル完結の超シンプル連携法～ [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>26.1.15追記：<br><strong>【<span class="fz-14px"><span class="fz-16px"><span class="fz-18px">Gemini】</span></span></span></strong>Gemini版を追加。文字列をコピーした後、マウスの右クリックで→サービス、から起動する形です。無料でも使えます。<br></p>



<figure class="wp-block-image size-large is-resized"><a href="https://imakat.com/rd.php?id=Sid8evAL.png" target="_blank"><img decoding="async" src="https://imakat.com/rd.php?id=Sid8evAL.png" alt="" style="aspect-ratio:1.6414382700528036;width:223px;height:auto"/></a></figure>



<figure class="wp-block-flexible-table-block-table"><table class="has-fixed-layout"><tbody><tr><td><a rel="noopener" target="_blank" href="https://docs.google.com/document/d/1lML0yZwWbv-y8o1QPXb3zBTGEBuSimvBTgOTFrW_RjY/edit?usp=sharing">Gemini APIキー取得方法と活用<span class="fa fa-external-link external-icon anchor-icon"></span></a></td></tr><tr><td>要約：このAppleScriptスクリプトは、ユーザーが選択したテキストをGoogle Gemini APIで要約し、その結果と元の本文をファイルに保存する自動化ツールです。右クリックで選択されたテキストを受け取り、処理前に内容をプレビューして続行を確認します。スクリプトはPython3とcurlコマンドを使い、Gemini APIにアクセス。<code>MY_API_KEY</code>を用いて、利用可能なモデルから<code>generateContent</code>をサポートするモデルを自動選択し、指定された文字数（MAX_SUMMARY_CHARS）で日本語の要約をリクエストします。Geminiからの出力は、要約文とその後に続く元の本文で構成されます。処理が完了すると、<code>SAVE_DIR</code>にタイムスタンプと要約の最初の行から生成されたタイトル（TITLE_MAX_CHARSで制限、ファイル名として安全化）を含むUTF-8形式のテキストファイルとして保存されます。処理状況やエラーは通知でユーザーに伝えられます。<br>Automator<br>ワークフローが受け取る項目：テキスト<br>選択肢：すべてのアプリケーション</td></tr><tr><td><a href="https://imakat.com/script_list/?pubtxt=geminiを使った日本語要約_pub.txt" target="_blank"><img decoding="async" src="https://imakat.com/rd.php?id=zrU95MiY.png" alt="" style="width:120px; height:auto;"></a></td></tr></tbody></table></figure>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p><strong>【<span class="fz-14px"><span class="fz-16px"><span class="fz-18px">ChatGPT】</span></span></span></strong><br><strong>～無料版でも動く、ローカル完結の超シンプル連携法～</strong><br>ChatGPTでテキストを要約したいとき、毎回アプリを開いてコピペするのは意外と手間です。そこで、AppleScriptとChatGPTデスクトップアプリを連携させて、「クリップボードの内容を自動でChatGPTに貼り付け→要約→テキストファイルに保存」までを数クリックで完結させる仕組みを紹介します。ChatGPTの無料版でも動作し、APIキーや外部サービスは不要です。</p>


<div class="sc-dynamic-embed">
  <style>
  /* リンクの見た目を整える */
  .sc-dynamic-embed .sc-link-container { 
      display: flex; 
      margin-bottom: 10px; 
      flex-wrap: wrap; 
      align-items: center; 
  }
  .sc-dynamic-embed .sc-link { margin-bottom: 0; }
  .sc-dynamic-embed .sc-link a {
    font-size: 15px; /* dynamicと合わせた基本サイズ */
    font-weight: normal;
    text-decoration: underline;
    color: #0073aa;
  }
  .sc-dynamic-embed .sc-link a:hover { text-decoration: none; color: #000; }
  
  /* ★スマホ画面（幅500px以下）の時は文字を縮小して統一感を出す */
  @media (max-width: 500px) {
    .sc-dynamic-embed .sc-link a { font-size: 13px; }
  }

  /* 行梱包時の基本スタイル */
  .sc-dynamic-embed .imk-line {
      display: inline-block;
      width: 100%;
      border-radius: 2px;
      transition: background-color 0.1s;
  }

  /* 古い枠内字幕ボックスを強制消去 */
  .sc-dynamic-embed #subtitleOverlay,
  .sc-dynamic-embed #scSubtitleOverlay,
  .sc-dynamic-embed .overlay-cue,
  .sc-dynamic-embed .band {
      display: none !important;
      opacity: 0 !important;
      visibility: hidden !important;
      pointer-events: none !important;
  }
  </style>

  <div class="sc-link-container">
    <p class="sc-link">
      <a href="https://imakat.com/ds62/?drid=62_2" target="_blank"
         onclick="return scStopAndGo(event, this);">
        👉低画質・枠外字幕はこちら
      </a>
    </p>
  </div>

  <style>
            :root{ --dr5emd-max: 1920px; }
            .video-wrap{position:relative;width:100%;margin:0 auto}
            figure.wp-block-video.aligncenter{
              width:100%;
              max-width:min(var(--dr5emd-max, 1920px), 98vw);
              margin:0 auto;
            }
            #subtitleOverlay{
              position:absolute; left:0; right:0; bottom:6%;
              padding:0 2%; text-align:center; pointer-events:none; z-index:2;
            }
            #subtitleOverlay .band{
              display:inline-block; background:rgba(0,0,0,0.35);
              padding:6px 10px; border-radius:8px; max-width:96%;
              margin:0 auto; box-shadow:0 1px 2px rgba(0,0,0,0.15);
            }
            #subtitleOverlay .overlay-cue{
              color:#fff; font-weight:600;
              font-size:clamp(16px, 3.6vw, 32px);
              line-height:1.32; white-space:pre-wrap; margin:2px 0;
              -webkit-text-stroke:.6px rgba(0,0,0,.7);
              text-shadow:-1px -1px 0 rgba(0,0,0,.6), 1px -1px 0 rgba(0,0,0,.6),
                          -1px  1px 0 rgba(0,0,0,.6), 1px  1px 0 rgba(0,0,0,.6);
            }
            @media (max-width:430px){
              #subtitleOverlay .overlay-cue{ font-size:clamp(16px, 4.2vw, 22px); }
            }
            .dr5emd-sublist details > p{
              height:200px; overflow:auto; background-color:#EDF7FF;
              padding:2px 6px; margin:0; box-shadow:3px 3px 4px black;
              position: relative;
            }
            .dr5emd-sublist details > summary{
              padding:2px 6px; width:100%;
              background-color:#ddd; border:none;
              box-shadow:3px 3px 4px black; cursor:pointer; list-style:none;
            }
            /* ▼ 自動スクロール時のハイライト（文字の太さを標準へ変更） */
            .active-hl {
                background-color: #ffff00 !important;
                color: #ff0000 !important;
                font-weight: normal; /* 標準の太さ */
                border-bottom: 2px solid red;
                display: inline-block;
                border-radius: 2px;
            }
            </style><div class="dr5emd-container"><figure class="wp-block-video aligncenter"><div class="video-wrap"><video id="myVideo" controls controlsList="nodownload" poster="https://imakat.com/rd.php?id=2MuRdRkf.png" playsinline preload="metadata" style="width:100%;height:auto;">  <source src="https://imakat.com/rd.php?id=ZJjPNzrn.mp4" type="video/mp4">  <track src="https://imakat.com/rd.php?id=dnzQfkpR.vtt" label="日本語" srclang="ja" kind="subtitles"></video><div id="subtitleOverlay" aria-hidden="true"></div></div><script>
document.addEventListener("DOMContentLoaded", function(){
  var video=document.getElementById("myVideo");
  var trackEl=video?video.querySelector("track[kind='subtitles'], track[kind='captions']"):null;
  var overlay=document.getElementById("subtitleOverlay"); if(!video||!overlay) return;
  video.addEventListener("contextmenu", function(e){ e.preventDefault(); return false; }, false);
  function setNative(mode){
    try{
      if(video.textTracks && video.textTracks.length){
        for(var i=0;i<video.textTracks.length;i++){ video.textTracks[i].mode = mode; }
      }
      if(trackEl && trackEl.track) trackEl.track.mode = mode;
    }catch(e){}
  }
  var isOverlay=true,lastSig="";
  function sig(active){if(!active||active.length===0)return"";var a=[];for(var i=0;i<active.length;i++){var c=active[i];a.push([c.startTime,c.endTime,c.text].join("|"));}return a.join("||");}
  function cueLine(c){var d=document.createElement("div");d.className="overlay-cue";d.setAttribute("translate","yes");if(typeof c.getCueAsHTML==="function")d.appendChild(c.getCueAsHTML());else d.textContent=c.text;return d;}
  function render(){
    if(!isOverlay || !trackEl || !trackEl.track) return;
    var ac=trackEl.track.activeCues,s=sig(ac); if(s===lastSig) return; lastSig=s;
    overlay.innerHTML=""; if(!ac || ac.length===0) return;
    var b=document.createElement("div"); b.className="band"; b.setAttribute("translate","yes");
    Array.from(ac).sort(function(a,b){return a.startTime-b.startTime;}).forEach(function(c){b.appendChild(cueLine(c));});
    overlay.appendChild(b);
  }
  function useOverlay(){isOverlay=true;overlay.style.display="";setNative("hidden");lastSig="";render();}
  function useNative(){isOverlay=false;overlay.style.display="none";setNative("showing");lastSig="";}
  useOverlay();
  if(trackEl){
    if(trackEl.track){ try{ trackEl.track.addEventListener("cuechange",render); }catch(e){} }
    trackEl.addEventListener("load", function(){ try{ if(trackEl.track) trackEl.track.addEventListener("cuechange",render); }catch(e){} render(); });
  }
  video.addEventListener("loadedmetadata",render);
  function handleWebkitMode(){ var m = video.webkitPresentationMode || "inline"; (m==="picture-in-picture"||m==="fullscreen") ? useNative() : useOverlay(); }
  if("webkitPresentationMode" in video){ video.addEventListener("webkitpresentationmodechanged",handleWebkitMode); handleWebkitMode(); }
  if("webkitCurrentPlaybackTargetIsWireless" in video){
    video.addEventListener("webkitcurrentplaybacktargetiswirelesschanged", function(){ video.webkitCurrentPlaybackTargetIsWireless ? useNative() : useOverlay(); });
  }
  if("pictureInPictureEnabled" in document){
    video.addEventListener("enterpictureinpicture",useNative);
    video.addEventListener("leavepictureinpicture",useOverlay);
  }
  document.addEventListener("fullscreenchange", function(){
    var fs=document.fullscreenElement;
    if(!fs) return useOverlay();
    (fs===video || (fs && fs.contains && fs.contains(video))) ? useNative() : useOverlay();
  });
});
</script>
                <figcaption></figcaption></figure><div class="dr5emd-sublist"><details translate="yes"><summary translate="yes">字幕一覧(クリック)</summary> <p translate="yes">
(<a href="#" class="imk-cue" data-seek="0:00" translate="no">00:00:00</a>)  ChatGPTによる日本語要約とTXTファイル保存のかんたん化<br>
(<a href="#" class="imk-cue" data-seek="0:20" translate="no">00:00:20</a>)   今回は、ChatGPTの活用ということで、わかりやすい使い方を紹介します。<br>
(<a href="#" class="imk-cue" data-seek="0:36" translate="no">00:00:36</a>)   色々なスクリプトや書類が存在しますが、それを要約して、日付を付けて、保存する　<br>
(<a href="#" class="imk-cue" data-seek="0:47" translate="no">00:00:47</a>)  それが簡単にできれば結構便利です。<br>
(<a href="#" class="imk-cue" data-seek="0:52" translate="no">00:00:52</a>)  ちょっとやってみましょうか。<br>
(<a href="#" class="imk-cue" data-seek="0:55" translate="no">00:00:55</a>)  今、ChatGPTでPythonのスクリプトを書いてもらったとします。<br>
(<a href="#" class="imk-cue" data-seek="1:05" translate="no">00:01:05</a>)  こういうものですね。<br>
(<a href="#" class="imk-cue" data-seek="1:09" translate="no">00:01:09</a>)  このPythonのスクリプトが、迷子になった場合、その中身が全く分からなくなってしまうので、<br>
(<a href="#" class="imk-cue" data-seek="1:19" translate="no">00:01:19</a>)  要約した内容を付けて、日時を付けて、タイムスタンプを付けて、そして本文を添えて<br>
(<a href="#" class="imk-cue" data-seek="1:29" translate="no">00:01:29</a>)  保存するという形にしたいと思います。<br>
(<a href="#" class="imk-cue" data-seek="1:36" translate="no">00:01:36</a>)  今ここに、Pythonスクリプトが出来上がっていたとします。<br>
(<a href="#" class="imk-cue" data-seek="1:42" translate="no">00:01:42</a>)  ここで、「コピーをする」を押します。<br>
(<a href="#" class="imk-cue" data-seek="1:46" translate="no">00:01:46</a>)  コピーしました。<br>
(<a href="#" class="imk-cue" data-seek="1:49" translate="no">00:01:49</a>)  次に、作っておいた、CHATGPT用の要約、TXT保存のアプリですね、<br>
(<a href="#" class="imk-cue" data-seek="2:02" translate="no">00:02:02</a>)  これをSTREAM DECKから起動します。<br>
(<a href="#" class="imk-cue" data-seek="2:09" translate="no">00:02:09</a>)  そうすると、この内容を保存しますか？ということで、　<br>
(<a href="#" class="imk-cue" data-seek="2:15" translate="no">00:02:15</a>)  スクリプトの先頭の120字をこのように紹介してくれます。 　　<br>
(<a href="#" class="imk-cue" data-seek="2:21" translate="no">00:02:21</a>)  間違いがないかの確認です。<br>
(<a href="#" class="imk-cue" data-seek="2:25" translate="no">00:02:25</a>)  続行を押します。<br>
(<a href="#" class="imk-cue" data-seek="2:29" translate="no">00:02:29</a>)  次に、ChatGPTによる要約を挿入しますか？と聞かれます。<br>
(<a href="#" class="imk-cue" data-seek="2:35" translate="no">00:02:35</a>)  はい、を選びます。<br>
(<a href="#" class="imk-cue" data-seek="2:38" translate="no">00:02:38</a>)  そうするとChatGPTが走ります。<br>
(<a href="#" class="imk-cue" data-seek="2:43" translate="no">00:02:43</a>)  非常に短い時間で、今のスクリプトの内容を日本語で300文字以内で、説明してくれます。<br>
(<a href="#" class="imk-cue" data-seek="2:58" translate="no">00:02:58</a>)  自動処理についての説明、となっています。　　<br>
(<a href="#" class="imk-cue" data-seek="3:04" translate="no">00:03:04</a>)  次にこれを、ChatGPTによって要約された文章をコピーします。　<br>
(<a href="#" class="imk-cue" data-seek="3:13" translate="no">00:03:13</a>)  次に、取得を押します。　<br>
(<a href="#" class="imk-cue" data-seek="3:23" translate="no">00:03:23</a>)  すると、この先頭に出来上がります。　<br>
(<a href="#" class="imk-cue" data-seek="3:36" translate="no">00:03:36</a>)  先頭に、日時が入っています。次に、要約が入っています。そしてその下に、ずっとスクリプトが入っています。<br>
(<a href="#" class="imk-cue" data-seek="3:51" translate="no">00:03:51</a>)  という形の保存用のファイルが出来上がります。<br>
(<a href="#" class="imk-cue" data-seek="3:59" translate="no">00:03:59</a>)  次の活用例を紹介します。<br>
(<a href="#" class="imk-cue" data-seek="4:05" translate="no">00:04:05</a>)  今ご覧のように、英文があります。　<br>
(<a href="#" class="imk-cue" data-seek="4:11" translate="no">00:04:11</a>)  この英文を日本語に翻訳して、日本語で300字以内に要約したものを作ります。　<br>
(<a href="#" class="imk-cue" data-seek="4:22" translate="no">00:04:22</a>)  そして、日時と要約とこの本文ですね。これを３点セットにして一つのファイルを作ります。<br>
(<a href="#" class="imk-cue" data-seek="4:34" translate="no">00:04:34</a>)  やってみます。<br>
(<a href="#" class="imk-cue" data-seek="4:39" translate="no">00:04:39</a>)  このようにして選択します。コピーを押します。次に、　STREAM DECKのアプリを起動します。<br>
(<a href="#" class="imk-cue" data-seek="4:45" translate="no">00:04:45</a>)  コピーを押します。<br>
(<a href="#" class="imk-cue" data-seek="4:49" translate="no">00:04:49</a>)  次に、SREAM DECKのアプリを起動します。<br>
(<a href="#" class="imk-cue" data-seek="4:58" translate="no">00:04:58</a>)  この内容を保存しますか？ということで文章の先頭の120字の紹介があります。　<br>
(<a href="#" class="imk-cue" data-seek="5:06" translate="no">00:05:06</a>)  これで間違いなしということで続行します。<br>
(<a href="#" class="imk-cue" data-seek="5:10" translate="no">00:05:10</a>)  ChatGPTによる要約を挿入しますか？はい。<br>
(<a href="#" class="imk-cue" data-seek="5:20" translate="no">00:05:20</a>)  この場合、時々、すぐに起動せずに下のボックスに英文が入ってしまうことがありますので、　　　<br>
(<a href="#" class="imk-cue" data-seek="5:28" translate="no">00:05:28</a>)  その場合は、焦らずに、右側のこのボタンを押します。<br>
(<a href="#" class="imk-cue" data-seek="5:40" translate="no">00:05:40</a>)  そうすると速やかに、翻訳がされます。<br>
(<a href="#" class="imk-cue" data-seek="5:49" translate="no">00:05:49</a>)  これは、記事の無断転載を禁止するような説明になっています。<br>
(<a href="#" class="imk-cue" data-seek="5:57" translate="no">00:05:57</a>)  これをコピーします。<br>
(<a href="#" class="imk-cue" data-seek="6:08" translate="no">00:06:08</a>)  そして、取得を押します。<br>
(<a href="#" class="imk-cue" data-seek="6:17" translate="no">00:06:17</a>)  そうしますと先頭に、一つ出来上がっています。　<br>
(<a href="#" class="imk-cue" data-seek="6:27" translate="no">00:06:27</a>)  内容を確認します。<br>
(<a href="#" class="imk-cue" data-seek="6:32" translate="no">00:06:32</a>)  日時、次に要約した文章、その下に英文の本文が入っています。　<br>
(<a href="#" class="imk-cue" data-seek="6:45" translate="no">00:06:45</a>)  これでいいですね。<br>
(<a href="#" class="imk-cue" data-seek="6:54" translate="no">00:06:54</a>)  最後に、３つ目の例を紹介します。<br>
(<a href="#" class="imk-cue" data-seek="6:59" translate="no">00:06:59</a>)  URLですね。<br>
(<a href="#" class="imk-cue" data-seek="7:04" translate="no">00:07:04</a>)  ご覧のように、一つURLがあったとします。<br>
(<a href="#" class="imk-cue" data-seek="7:09" translate="no">00:07:09</a>)  このURLの中身を、300文字以内の日本語で要約して、日時、要約、本文と、いや本文といっても、このURLだけですが。<br>
(<a href="#" class="imk-cue" data-seek="7:27" translate="no">00:07:27</a>)  それを一つのファイルにします。<br>
(<a href="#" class="imk-cue" data-seek="7:31" translate="no">00:07:31</a>)  やってみます。<br>
(<a href="#" class="imk-cue" data-seek="7:38" translate="no">00:07:38</a>)  コピー<br>
(<a href="#" class="imk-cue" data-seek="7:46" translate="no">00:07:46</a>)  ChatGPT用要約TXT保存のアプリを起動します。<br>
(<a href="#" class="imk-cue" data-seek="7:52" translate="no">00:07:52</a>)  この内容を保存しますか？続行。<br>
(<a href="#" class="imk-cue" data-seek="7:58" translate="no">00:07:58</a>)  ChatGPTによる要約を挿入しますか？はい。<br>
(<a href="#" class="imk-cue" data-seek="8:18" translate="no">00:08:18</a>)  要約が済みました。<br>
(<a href="#" class="imk-cue" data-seek="8:23" translate="no">00:08:23</a>)  どうもスマホを公共の充電器に接続すると、データを盗まれる危険があるので注意してください、という文章ですね。<br>
(<a href="#" class="imk-cue" data-seek="8:43" translate="no">00:08:43</a>)  この文章をコピーします。　<br>
(<a href="#" class="imk-cue" data-seek="8:51" translate="no">00:08:51</a>)  そして取得を押します。<br>
(<a href="#" class="imk-cue" data-seek="8:56" translate="no">00:08:56</a>)  そうすると、一番上に今の内容が入ってきました。<br>
(<a href="#" class="imk-cue" data-seek="9:05" translate="no">00:09:05</a>)  ご覧のように、一番上に、日時、要約、内容、まあ時々文章が変になっている場合もあるのですかね。　　　<br>
(<a href="#" class="imk-cue" data-seek="9:19" translate="no">00:09:19</a>)  これは、保つかな。<br>
(<a href="#" class="imk-cue" data-seek="9:24" translate="no">00:09:24</a>)  その辺は、愛嬌ですね。<br>
(<a href="#" class="imk-cue" data-seek="9:29" translate="no">00:09:29</a>)  それからURLと、いう形になります。<br>
(<a href="#" class="imk-cue" data-seek="9:37" translate="no">00:09:37</a>)  以上のように、３つ活用例を紹介しました。<br>
</p> </details>
<style>
details { font: 16px "Open Sans", Calibri, sans-serif; width: 100%; }
details > summary { padding: 2px 6px; width: 100%; background-color: #ddd; border: none; box-shadow: 3px 3px 4px black; cursor: pointer; list-style: none; }
details > p { font: 14px "Open Sans", Calibri, sans-serif; height:150px; overflow: scroll; background-color: #EDF7FF; padding: 2px 6px; margin: 0; box-shadow: 3px 3px 4px black; }
</style>
</div><script>
(function(){
  var root=document.querySelector(".dr5emd-sublist");
  var video=document.getElementById("myVideo");
  if(!root || !video) return;
  function parseTs(ts){
    if(!ts) return null;
    var p=ts.trim().split(":").map(function(x){return parseInt(x,10)||0;});
    if(p.length===2) return p[0]*60 + p[1];
    if(p.length===3) return p[0]*3600 + p[1]*60 + p[2];
    return null;
  }
  root.addEventListener("click", function(e){
    var a=e.target.closest && e.target.closest("a.imk-cue[data-seek]");
    if(!a || !root.contains(a)) return;
    e.preventDefault();
    var sec = parseTs(a.getAttribute("data-seek"));
    if(sec==null) return;
    try{ video.currentTime = sec; if(video.paused) video.play(); }catch(_){}
  });
  video.addEventListener("timeupdate", function(){
    var listContainer = root.querySelector("details > p");
    if(!listContainer) return;
    var cues = listContainer.querySelectorAll("a.imk-cue");
    if(cues.length === 0) return;
    var cur = video.currentTime;
    var active = null;
    for(var i=0; i<cues.length; i++){
        var t = parseTs(cues[i].getAttribute("data-seek"));
        if(t !== null && cur >= t - 0.5){
            active = cues[i];
        } else if(t > cur){
            break;
        }
    }
    if(active){
        if(active.classList.contains("active-hl")) return;
        var old = listContainer.querySelectorAll(".active-hl");
        for(var k=0; k<old.length; k++) old[k].classList.remove("active-hl");
        active.classList.add("active-hl");
        if(listContainer.offsetParent !== null){
            var containerRect = listContainer.getBoundingClientRect();
            var activeRect = active.getBoundingClientRect();
            var targetScroll = listContainer.scrollTop + (activeRect.top - containerRect.top) - (listContainer.clientHeight / 2) + (active.clientHeight / 2);
            listContainer.scrollTo({ top: targetScroll, behavior: "smooth" });
        }
    }
  });
})();
</script>
                </div>

  <script>
  (function(){
    var me = document.currentScript;
    var wrapper = me ? me.closest('.sc-dynamic-embed') : null;

    /* -----------------------------------------------
       1. 動画の保護機能（右クリック禁止・DL防止）
       ----------------------------------------------- */
    function protectVideo() {
      var target = wrapper ? wrapper : document;
      var mediaEls = target.querySelectorAll('video');
      mediaEls.forEach(function(v){
        if(v.dataset.protected === 'true') return;
        v.dataset.protected = 'true';
        v.setAttribute('controlsList', 'nodownload');
        v.oncontextmenu = function() { return false; };
        v.addEventListener('contextmenu', function(e){ e.preventDefault(); return false; }, false);
      });
    }

    /* -----------------------------------------------
       2. 字幕制御＆ハイライト機能（iPhone全画面 完全対応版）
       ----------------------------------------------- */
    function initSubtitles() {
      var target = wrapper ? wrapper : document;
      var video = target.querySelector('video');
      var listContainer = target.querySelector('details > p');
      
      if (!video || !listContainer) return false; 

      if (video.dataset.subInit === 'true') return true; 
      video.dataset.subInit = 'true';

      var oldOverlay = target.querySelector('#subtitleOverlay') || target.querySelector('#scSubtitleOverlay');
      if (oldOverlay) {
          oldOverlay.style.setProperty('display', 'none', 'important');
          oldOverlay.innerHTML = ''; 
      }

      function isSpecialMode() {
        var isFs = !!(document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement);
        var isPiP = !!(document.pictureInPictureElement && document.pictureInPictureElement === video) || (video.webkitPresentationMode === "picture-in-picture");
        var isIOSFs = !!video.webkitDisplayingFullscreen; 
        return isFs || isPiP || isIOSFs;
      }

      try {
        if(video.textTracks && video.textTracks.length > 0){
          for(var i=0; i<video.textTracks.length; i++){
             if(video.textTracks[i].kind === 'subtitles' || video.textTracks[i].kind === 'captions'){
                 video.textTracks[i].mode = "hidden";
             }
          }
        }
      } catch(e){}

      video.addEventListener("webkitbeginfullscreen", function() {
          try {
              if(video.textTracks && video.textTracks.length > 0) {
                  for(var i=0; i<video.textTracks.length; i++){
                      if(video.textTracks[i].kind === 'subtitles' || video.textTracks[i].kind === 'captions') video.textTracks[i].mode = "showing";
                  }
              }
          } catch(e){}
      });
      video.addEventListener("webkitendfullscreen", function() {
          try {
              if(video.textTracks && video.textTracks.length > 0) {
                  for(var i=0; i<video.textTracks.length; i++){
                      if(video.textTracks[i].kind === 'subtitles' || video.textTracks[i].kind === 'captions') video.textTracks[i].mode = "hidden";
                  }
              }
          } catch(e){}
      });

      var detailsEl = target.querySelector("details");
      if (detailsEl) {
          detailsEl.open = true; 
          var summaryEl = detailsEl.querySelector("summary");
          if (summaryEl) summaryEl.textContent = "字幕(シーン)はここをクリック";
      }

      if (!listContainer.dataset.formatted) {
          var html = listContainer.innerHTML;
          var lines = html.split(/<br\s*\/?>/i);
          var newHtml = "";
          for(var j=0; j<lines.length; j++) {
              if(lines[j].trim() === "") continue;
              newHtml += "<span class='imk-line'>" + lines[j] + "</span><br>";
          }
          listContainer.innerHTML = newHtml;
          listContainer.dataset.formatted = "true";
      }

      function parseTs(ts){
        if(!ts) return null;
        var p = ts.trim().split(":").map(function(x){return parseInt(x,10)||0;});
        if(p.length===2) return p[0]*60 + p[1];
        if(p.length===3) return p[0]*3600 + p[1]*60 + p[2];
        return null;
      }

      var rootSublist = target.querySelector(".dr5-sublist") || listContainer.parentElement;
      if (rootSublist) {
        rootSublist.addEventListener("click", function(e){
          var a = e.target.closest && e.target.closest("a.imk-cue[data-seek]");
          if(!a) return;
          e.preventDefault();
          var sec = parseTs(a.getAttribute("data-seek"));
          if(sec==null) return;
          try{ video.currentTime = sec; if(video.paused) video.play(); }catch(_){}
        });
      }

      video.addEventListener("timeupdate", function(){
        var desiredMode = isSpecialMode() ? "showing" : "hidden";
        try {
            if(video.textTracks && video.textTracks.length > 0){
                for(var i=0; i<video.textTracks.length; i++){
                    if((video.textTracks[i].kind === 'subtitles' || video.textTracks[i].kind === 'captions') && video.textTracks[i].mode !== desiredMode) {
                        video.textTracks[i].mode = desiredMode;
                    }
                }
            }
        } catch(e){}

        var cues = listContainer.querySelectorAll("a.imk-cue");
        if(cues.length === 0) return;
        var cur = video.currentTime;
        var activeA = null;

        for(var i=0; i<cues.length; i++){
            var t = parseTs(cues[i].getAttribute("data-seek"));
            if(t !== null && cur >= t - 0.5){ activeA = cues[i]; } 
            else if(t > cur){ break; }
        }

        if(activeA){
            var activeLine = activeA.closest(".imk-line");
            if(!activeLine) activeLine = activeA;

            if(activeLine.classList.contains("active-hl")) return;

            var allLines = listContainer.querySelectorAll(".imk-line");
            for(var k=0; k<allLines.length; k++) {
                allLines[k].classList.remove("active-hl");
                allLines[k].removeAttribute("style"); 
            }
            var allLinks = listContainer.querySelectorAll("a");
            for(var m=0; m<allLinks.length; m++) {
                allLinks[m].classList.remove("active-hl");
                allLinks[m].removeAttribute("style"); 
            }

            activeLine.classList.add("active-hl");
            activeLine.style.setProperty("background-color", "#ffff00", "important");
            activeLine.style.setProperty("color", "red", "important");
            activeLine.style.setProperty("font-weight", "normal", "important");
            
            var newLinks = activeLine.querySelectorAll("a");
            for(var n=0; n<newLinks.length; n++) {
                newLinks[n].style.setProperty("color", "red", "important");
                newLinks[n].style.setProperty("text-decoration", "none", "important");
            }

            if(listContainer.offsetParent !== null){
                var containerRect = listContainer.getBoundingClientRect();
                var activeRect = activeLine.getBoundingClientRect();
                var targetScroll = listContainer.scrollTop + (activeRect.top - containerRect.top) - (listContainer.clientHeight / 2) + (activeLine.clientHeight / 2);
                listContainer.scrollTo({ top: targetScroll, behavior: "smooth" });
            }
        }
      });

      return true;
    }

    /* -----------------------------------------------
       監視タイマー
       ----------------------------------------------- */
    var checks = 0;
    var checkTimer = setInterval(function(){
      protectVideo(); // ★dynamic2は保護を維持
      var success = initSubtitles();
      checks++;
      if (success || checks > 20) { 
        clearInterval(checkTimer);
      }
    }, 500); 

    /* -----------------------------------------------
       3. 画面遷移時の停止機能
       ----------------------------------------------- */
    if (!window.scStopAndGo) {
      window.scStopAndGo = function(event, link){
        try{
          var mediaEls = document.querySelectorAll('video, audio');
          mediaEls.forEach(function(m){
            try{
              if (!m.paused) m.pause();
              if (document.pictureInPictureElement === m && document.exitPictureInPicture) {
                document.exitPictureInPicture().catch(function(){});
              }
            }catch(e){}
          });
        }finally{
          event.preventDefault();
          setTimeout(function(){
            if (link.target === '_blank') {
              window.open(link.href, '_blank');
            } else {
              window.location.href = link.href;
            }
          }, 50);
        }
        return false;
      };
    }
  })();
  </script>
</div>



<h3 class="wp-block-heading">ポイント解説</h3>



<h4 class="wp-block-heading">■ スクリプトの流れ</h4>



<ol class="wp-block-list">
<li style="font-style:normal;font-weight:700">クリップボードのテキストを取得</li>



<li style="font-style:normal;font-weight:700">ChatGPTアプリを自動起動</li>



<li style="font-style:normal;font-weight:700">要約プロンプトを自動入力（例：「以下のテキストを日本語で400字以内に要約してください」）</li>



<li style="font-style:normal;font-weight:700">ChatGPTが要約を生成</li>



<li style="font-style:normal;font-weight:700">ユーザーが要約をコピー</li>



<li style="font-style:normal;font-weight:700">指定フォルダにテキストファイルとして自動保存</li>
</ol>



<p>１のクリップボードのテキスト取得がミソになっています。マウス右クリックでコピーしたものです。保存ファイル名は「YYYYMMDD-HHMMSS_タイトル.txt」という形式で、<br>例：/Volumes/NO3_SSD/Dropbox/dropbox_1/ChatGPTによる要約と本文/ に保存されます。</p>



<h4 class="wp-block-heading">■ 実装の手順</h4>



<figure class="wp-block-flexible-table-block-table"><table class="has-fixed-layout"><tbody><tr><td style="width:10%">(1)</td><td>下の青色のボタンから、スクリプト(txtファイル)をダウンロードします。</td></tr><tr><td style="width:10%">(2)</td><td>スクリプトエディタで、例えば&#8221;Save_Clipboard_with_Summary&#8221;と名前つけて、スクリプトファイル&#8221;Save_Clipboard_with_Summary.scpt&#8221;として保存します。</td></tr><tr><td style="width:10%">(3)</td><td>(a)Stream Deckからは&#8221;Save_Clipboard_with_Summary.scpt&#8221;が直接呼べます。<br>(b)クイックアクションからの起動は、Automator→クイックアクション→ワークフローが受け取る現在の項目：テキスト、使用できるアプリケーション：任意のアプリケーション→「AppleScriptを実行」のボックスを置く→(4)<br></td></tr><tr><td>(4)</td><td><strong>run script (POSIX file &#8220;/&lt;あなたのファイルパス&gt;/Save_Clipboard_with_Summary.scpt&#8221;)</strong></td></tr><tr><td>(5)</td><td>アプリ名を例えば&#8221;ChatGPT要約付きTXT保存&#8221;として保存します。</td></tr><tr><td colspan="2"><a href="https://imakat.com/?pubtxt=ChatGPT連携_要約保存Mac用スクリプト_pub.txt" target="_blank"><img decoding="async" src="https://imakat.com/rd.php?id=zrU95MiY.png" alt="" style="width:120px; height:auto;"></a></td></tr><tr><td colspan="2">Windowsへ実装したい場合は、ChatGPTに上のtxtファイルを添付して、「同じ働きをWindowsで実装するスクリプト作成と実装手順を指示ください」としてください。</td></tr></tbody></table></figure>



<h4 class="wp-block-heading">■ 操作方法</h4>



<p>使い方はとても簡単です。<br>要約したいテキストをコピーし、このアプリを実行するだけ。<br>ChatGPTが起動して自動で要約を生成し、その結果をコピーすればファイルが自動保存されます。<br>これで「ChatGPTで要約→保存」までが一瞬で完了します。</p>



<p>さらに、右クリック（コンテキストメニュー）から呼び出せる「クイックアクション」として登録しておくと、<br>ブラウザやメモアプリ上でテキストを選択 → コピー → 右クリックで「要約して保存」を実行、<br>という直感的な操作が可能になります。<br>Mac標準のAutomatorで簡単に登録できるため、日常の作業フローに自然に組み込めます。</p>



<h4 class="wp-block-heading">■ ChatGPTの文字列のコピーが扱えない！裏ワザあります。</h4>



<p>ブラウザ、テキストエディット、メモなどにある文章は、コピーを行なった後、このアプリをMacのキーボードショートカットに登録して、要約保存は、下記のアクセス許可を経て使用できるようになります。私は、「ChatGPT要約付きTXT保存」アプリに⌥(Option)+ファンクション(F)1を登録しました。</p>



<figure class="wp-block-image size-large"><a href="https://imakat.com/rd.php?id=LnI9NRyQ.png" target="_blank"><img decoding="async" src="https://imakat.com/rd.php?id=LnI9NRyQ.png" alt=""/></a></figure>



<p><br>しかし、本家本元のChatGPTに表示される文字や文章は、コピーした後、アプリが起動しないという問題が生じます。その場合の裏ワザです。</p>



<p>それは、文字列のコピーは、Web版ChatGPTから行い、アプリを起動してインストール版ChatGPTで要約させ、最後に保存する流れで連携する方法です。以下のような挙動になります。</p>



<figure class="wp-block-image size-large"><a href="https://imakat.com/rd.php?id=CmWDK25N.gif" target="_blank"><img decoding="async" src="https://imakat.com/rd.php?id=CmWDK25N.gif" alt=""/></a></figure>



<p><br><br></p>



<h4 class="wp-block-heading">■ 注意：アクセス許可が必要な場合があります</h4>



<p>クイックアクションを使って、Safari・Chrome・PDFビューアなど<br>さまざまなアプリからテキストをコピーして要約に取り込む場合、<br>まれに「エラーが出る」「テキストが取得できない」ことがあります。</p>



<p>これは、そのアプリのテキスト内容へのアクセスが macOS で制限されているためです。<br>その場合は以下の手順でアクセスを許可すると解決します。</p>



<p><strong>対処方法：<br>1. 「システム設定」→「プライバシーとセキュリティ」を開く<br>2. 左側のメニューから「アクセシビリティ」または「フルディスクアクセス」を選択<br>3. 使用しているアプリ（例：Automator.app、ChatGPT.app、または実行中のスクリプトランチャー）を追加してオンにする</strong></p>



<p>この設定を行うと、ブラウザ・PDF・Word・メモなど、<br>どのアプリ上でもクイックアクションを安定して利用できるようになります。<br>特にAutomator経由でスクリプトを動かす場合は「Automator.app」へのアクセス許可が重要です。</p>



<h4 class="wp-block-heading">■ TextEditでクイックアクションが出てこない場合</h4>



<p>TextEditでテキストを選択しても、右クリックメニューにクイックアクションが表示されない場合があります。<br>これは、TextEditがリッチテキスト（装飾付き）モードになっているとAutomatorが対象外とみなすためです。</p>



<p><strong>&#x1f4a1; 対処方法：<br>1. メニューの「フォーマット」→「標準テキストにする（⌘⇧T）」を選択し、プレーンテキストモードに切り替える。<br>2. 再度右クリック → 「クイックアクション」を確認。</strong></p>



<p>すると、「ChatGPT要約付きTXT保存」などのワークフローが表示されます。</p>



<h4 class="wp-block-heading">&#x2699;&#xfe0f; Automatorの設定で特に重要なポイント！</h4>



<p>Automatorでクイックアクションを作成するときは、<br>上部の設定欄にある</p>



<p>「ワークフローが受け取る現在の項目」</p>



<p>の指定を 必ず「テキスト」に設定してください。<br>ここが「リッチテキスト」や「ファイル」のままだと、<br>TextEditではメニューに表示されません。</p>



<p><strong>正しい設定：<br>• ワークフローが受け取る現在の項目：&#x1f7e2;「テキスト」<br>• 使用できるアプリケーション：&#x1f7e2;「任意のアプリケーション」</strong></p>



<p>この設定を間違えると、メモでは動くのにTextEditでは出ない、という現象が起こります。</p>



<p>&#x1f538;ポイント：Automatorの「受け取る項目」は“入力タイプの宣言”に相当します。<br>TextEditがプレーンテキストを出すときだけ、この条件に一致します。</p>



<h4 class="wp-block-heading">&#x1f9ed; Stream Deckでの活用（おすすめ）</h4>



<figure class="wp-block-image size-large is-resized"><a href="https://imakat.com/rd.php?id=SlG7I6vq.png" target="_blank"><img decoding="async" src="https://imakat.com/rd.php?id=SlG7I6vq.png" alt="" style="width:357px;height:auto"/></a></figure>



<p>クイックアクションも十分便利ですが、Stream Deckを持っている方は、こちらを使うのがおすすめです。</p>



<p>Stream Deckは、右クリックメニューではなくAppleScriptやShellスクリプトを直接起動できるランチャーです。<br>そのため、ChatGPTアプリのようなWebView上のテキストでも問題なく実行できます。<br>つまり、どんなアプリや画面上でも「コピー → ボタン一発」で要約＋保存が可能になります。</p>



<p><strong>&#x25b6; Stream Deckでの設定例<br>1. Stream Deckアプリを開く<br>2. 「System」→「Open」または「AppleScript実行」アクションを追加<br>3. あなたのスクリプトファイル（例：Save_Clipboard_with_Summary.scpt）のパスを指定<br>4. ボタンに「要約して保存」などの名前やアイコンを設定</strong></p>



<p>これで、どんなアプリ上でも：</p>



<p>テキストをコピー → Stream Deckボタンを押す → ChatGPTが自動要約 → Dropboxに保存<br>という流れをワンアクションで実行できます。</p>



<p><strong>&#x25b6; 併用のすすめ<br>• 普段は右クリックのクイックアクションで十分<br>• ChatGPTアプリやPDFなど、クイックアクションが反応しない場合はStream Deckで実行</strong></p>



<p>両方に同じスクリプトを指定しておけば、操作感を統一しつつ、どんな環境でも動く構成になります。</p>



<h4 class="wp-block-heading">■ 利用のポイント</h4>



<p><strong>• 無料版（GPT-3.5）でも問題なく動作します。<br>• 英語記事や長文を正確に要約したい場合は、GPT-4またはGPT-5（ChatGPT Plus）を推奨。<br>• すべてローカルで完結し、外部通信やAPI設定は不要。<br>• 要約と手入力タイトルのどちらにも対応し、ファイルは日時順に整理されます。</strong></p>



<h4 class="wp-block-heading">■ 応用例</h4>



<p>プログラミングスクリプトの日本語要約もさることながら、</p>



<p>•<strong> 英語→日本語要約版<br>• 翻訳＋保存版<br>• URL要約版(&#x1f449;「質問してみましょう」のボックスにURLが入った状態で待ちになる場合は、右のボタンを押して動作させる)</strong></p>



<p>ChatGPTアプリを直接操作する感覚で自動化できるため、<br>誰でもすぐに試せる実用的なAI活用法です。<br>Macユーザーなら今日からすぐ導入できます。</p>



<p>以上<br><br></p>
]]></content:encoded>
					
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">27668</post-id>	</item>
		<item>
		<title>Google翻訳とApple翻訳を同時に使えるのはMacのSafariだけだが。</title>
		<link>https://imakat.com/2025/09/26/27559/</link>
		
		<dc:creator><![CDATA[imakat]]></dc:creator>
		<pubDate>Thu, 25 Sep 2025 23:48:30 +0000</pubDate>
				<category><![CDATA[デジタル]]></category>
		<category><![CDATA[ものづくり]]></category>
		<category><![CDATA[Apple翻訳]]></category>
		<category><![CDATA[Google翻訳]]></category>
		<guid isPermaLink="false">https://imakat.com/?p=27559</guid>

					<description><![CDATA[今回は小ネタです。動画をご覧いただければ。iPhoneでも、iPhoneのご機嫌がよければ、Google翻訳とApple翻訳は、同時に利用できることを発見しました。これがあらゆる場面で可能とは申しません。AppleのSa [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>今回は小ネタです。動画をご覧いただければ。iPhoneでも、iPhoneのご機嫌がよければ、Google翻訳とApple翻訳は、同時に利用できることを発見しました。これがあらゆる場面で可能とは申しません。AppleのSafariにて可能になります。しかし、Macでは十分可能なことではあります。</p>


<div class="sc-dynamic-embed">
  <style>
  /* リンクの見た目を整える */
  .sc-dynamic-embed .sc-link-container { 
      display: flex; 
      gap: 12px; 
      margin-bottom: 10px; 
      flex-wrap: wrap; 
      align-items: center; 
  }
  .sc-dynamic-embed .sc-link { margin-bottom: 0; }
  .sc-dynamic-embed .sc-link a {
    font-size: 15px;
    font-weight: normal;
    text-decoration: underline;
    color: #0073aa;
  }
  .sc-dynamic-embed .sc-link a:hover { text-decoration: none; color: #000; }
  
  /* ★変更：ダウンロードボタンの基本サイズを小さくし、文字の折り返しを防止 */
  .sc-dynamic-embed .dl-btn a {
    font-size: 12px !important;
    color: #d9534f;
    font-weight: bold;
    text-decoration: none;
    background: #fdf0ef;
    padding: 4px 6px;
    border-radius: 4px;
    border: 1px solid #d9534f;
    white-space: nowrap; 
  }
  .sc-dynamic-embed .dl-btn a:hover { background: #d9534f; color: #fff; }

  /* ★追加：スマホ画面（幅500px以下）の時は、さらに全体を縮小して1行に収める */
  @media (max-width: 500px) {
    .sc-dynamic-embed .sc-link-container { gap: 6px; }
    .sc-dynamic-embed .sc-link a { font-size: 13px; }
    .sc-dynamic-embed .dl-btn a { font-size: 11px !important; padding: 3px 5px; }
  }

  /* 行梱包時の基本スタイル */
  .sc-dynamic-embed .imk-line {
      display: inline-block;
      width: 100%;
      border-radius: 2px;
      transition: background-color 0.1s;
  }

  /* 古い枠内字幕ボックスを強制消去 */
  .sc-dynamic-embed #subtitleOverlay,
  .sc-dynamic-embed #scSubtitleOverlay,
  .sc-dynamic-embed .overlay-cue,
  .sc-dynamic-embed .band {
      display: none !important;
      opacity: 0 !important;
      visibility: hidden !important;
      pointer-events: none !important;
  }
  </style>

  <div class="sc-link-container">
    <p class="sc-link">
      <a href="https://imakat.com/ds62/?drid=61" target="_blank"
         onclick="return scStopAndGo(event, this);">
        👉低画質・枠外字幕はこちら
      </a>
    </p>
    <p class="sc-link dl-btn">
      <a href="#" id="imk-dynamic-dl-btn" target="_blank" rel="noopener" download style="display: none;">
        📥 動画をダウンロード
      </a>
    </p>
  </div>

  <style>
            :root{ --dr5emd-max: 1920px; }
            .video-wrap{position:relative;width:100%;margin:0 auto}
            figure.wp-block-video.aligncenter{
              width:100%;
              max-width:min(var(--dr5emd-max, 1920px), 98vw);
              margin:0 auto;
            }
            #subtitleOverlay{
              position:absolute; left:0; right:0; bottom:6%;
              padding:0 2%; text-align:center; pointer-events:none; z-index:2;
            }
            #subtitleOverlay .band{
              display:inline-block; background:rgba(0,0,0,0.35);
              padding:6px 10px; border-radius:8px; max-width:96%;
              margin:0 auto; box-shadow:0 1px 2px rgba(0,0,0,0.15);
            }
            #subtitleOverlay .overlay-cue{
              color:#fff; font-weight:600;
              font-size:clamp(16px, 3.6vw, 32px);
              line-height:1.32; white-space:pre-wrap; margin:2px 0;
              -webkit-text-stroke:.6px rgba(0,0,0,.7);
              text-shadow:-1px -1px 0 rgba(0,0,0,.6), 1px -1px 0 rgba(0,0,0,.6),
                          -1px  1px 0 rgba(0,0,0,.6), 1px  1px 0 rgba(0,0,0,.6);
            }
            @media (max-width:430px){
              #subtitleOverlay .overlay-cue{ font-size:clamp(16px, 4.2vw, 22px); }
            }
            .dr5emd-sublist details > p{
              height:200px; overflow:auto; background-color:#EDF7FF;
              padding:2px 6px; margin:0; box-shadow:3px 3px 4px black;
              position: relative;
            }
            .dr5emd-sublist details > summary{
              padding:2px 6px; width:100%;
              background-color:#ddd; border:none;
              box-shadow:3px 3px 4px black; cursor:pointer; list-style:none;
            }
            /* ▼ 自動スクロール時のハイライト（文字の太さを標準へ変更） */
            .active-hl {
                background-color: #ffff00 !important;
                color: #ff0000 !important;
                font-weight: normal; /* 標準の太さ */
                border-bottom: 2px solid red;
                display: inline-block;
                border-radius: 2px;
            }
            </style><div class="dr5emd-container"><figure class="wp-block-video aligncenter"><div class="video-wrap"><video id="myVideo" controls controlsList="nodownload" poster="https://imakat.com/rd.php?id=fea2Elfc.png" playsinline preload="metadata" style="width:100%;height:auto;">  <source src="https://imakat.com/rd.php?id=U12RrnHA.mp4" type="video/mp4">  <track src="https://imakat.com/rd.php?id=fP1fMTTy.vtt" label="日本語" srclang="ja" kind="subtitles"></video><div id="subtitleOverlay" aria-hidden="true"></div></div><script>
document.addEventListener("DOMContentLoaded", function(){
  var video=document.getElementById("myVideo");
  var trackEl=video?video.querySelector("track[kind='subtitles'], track[kind='captions']"):null;
  var overlay=document.getElementById("subtitleOverlay"); if(!video||!overlay) return;
  video.addEventListener("contextmenu", function(e){ e.preventDefault(); return false; }, false);
  function setNative(mode){
    try{
      if(video.textTracks && video.textTracks.length){
        for(var i=0;i<video.textTracks.length;i++){ video.textTracks[i].mode = mode; }
      }
      if(trackEl && trackEl.track) trackEl.track.mode = mode;
    }catch(e){}
  }
  var isOverlay=true,lastSig="";
  function sig(active){if(!active||active.length===0)return"";var a=[];for(var i=0;i<active.length;i++){var c=active[i];a.push([c.startTime,c.endTime,c.text].join("|"));}return a.join("||");}
  function cueLine(c){var d=document.createElement("div");d.className="overlay-cue";d.setAttribute("translate","yes");if(typeof c.getCueAsHTML==="function")d.appendChild(c.getCueAsHTML());else d.textContent=c.text;return d;}
  function render(){
    if(!isOverlay || !trackEl || !trackEl.track) return;
    var ac=trackEl.track.activeCues,s=sig(ac); if(s===lastSig) return; lastSig=s;
    overlay.innerHTML=""; if(!ac || ac.length===0) return;
    var b=document.createElement("div"); b.className="band"; b.setAttribute("translate","yes");
    Array.from(ac).sort(function(a,b){return a.startTime-b.startTime;}).forEach(function(c){b.appendChild(cueLine(c));});
    overlay.appendChild(b);
  }
  function useOverlay(){isOverlay=true;overlay.style.display="";setNative("hidden");lastSig="";render();}
  function useNative(){isOverlay=false;overlay.style.display="none";setNative("showing");lastSig="";}
  useOverlay();
  if(trackEl){
    if(trackEl.track){ try{ trackEl.track.addEventListener("cuechange",render); }catch(e){} }
    trackEl.addEventListener("load", function(){ try{ if(trackEl.track) trackEl.track.addEventListener("cuechange",render); }catch(e){} render(); });
  }
  video.addEventListener("loadedmetadata",render);
  function handleWebkitMode(){ var m = video.webkitPresentationMode || "inline"; (m==="picture-in-picture"||m==="fullscreen") ? useNative() : useOverlay(); }
  if("webkitPresentationMode" in video){ video.addEventListener("webkitpresentationmodechanged",handleWebkitMode); handleWebkitMode(); }
  if("webkitCurrentPlaybackTargetIsWireless" in video){
    video.addEventListener("webkitcurrentplaybacktargetiswirelesschanged", function(){ video.webkitCurrentPlaybackTargetIsWireless ? useNative() : useOverlay(); });
  }
  if("pictureInPictureEnabled" in document){
    video.addEventListener("enterpictureinpicture",useNative);
    video.addEventListener("leavepictureinpicture",useOverlay);
  }
  document.addEventListener("fullscreenchange", function(){
    var fs=document.fullscreenElement;
    if(!fs) return useOverlay();
    (fs===video || (fs && fs.contains && fs.contains(video))) ? useNative() : useOverlay();
  });
});
</script>
                <figcaption></figcaption></figure><div class="dr5emd-sublist"><details translate="yes"><summary translate="yes">字幕一覧(クリック)</summary> <p translate="yes">
(<a href="#" class="imk-cue" data-seek="0:00" translate="no">00:00:00</a>) 1) 〜気まぐれか？iPhoneでも、Google翻訳とApple翻訳が同時に使えた？！〜<br>
(<a href="#" class="imk-cue" data-seek="0:04" translate="no">00:00:04</a>) 2) Apple翻訳で翻訳する言語、例えばポルトガル語を選びます。<br>
(<a href="#" class="imk-cue" data-seek="0:19" translate="no">00:00:19</a>) 3) ところが、字幕一覧が翻訳されません。<br>
(<a href="#" class="imk-cue" data-seek="0:31" translate="no">00:00:31</a>) 4) (ここで、一旦やめて、やり方を変えます。)<br>
(<a href="#" class="imk-cue" data-seek="0:33" translate="no">00:00:33</a>) 5) 振り出しに戻り、Google翻訳を選びます。<br>
(<a href="#" class="imk-cue" data-seek="0:42" translate="no">00:00:42</a>) 6) Google翻訳なら、字幕一覧も翻訳されます。<br>
(<a href="#" class="imk-cue" data-seek="0:51" translate="no">00:00:51</a>) 7) これはGoogle図形描画です。<br>
(<a href="#" class="imk-cue" data-seek="0:56" translate="no">00:00:56</a>) 8) Google翻訳では、画像の中の文字は、翻訳されません。<br>
(<a href="#" class="imk-cue" data-seek="1:00" translate="no">00:01:00</a>) 9) そこで、ここへ、Apple翻訳を重ねてみると。。どうなるか？<br>
(<a href="#" class="imk-cue" data-seek="1:12" translate="no">00:01:12</a>) 10) すると、画像の中の文字も翻訳されます。<br>
(<a href="#" class="imk-cue" data-seek="1:29" translate="no">00:01:29</a>) 11) 枠内字幕も、字幕一覧も翻訳されます。<br>
(<a href="#" class="imk-cue" data-seek="1:36" translate="no">00:01:36</a>) 12) 字幕のある時刻へも、ちゃんと飛びます。完璧ですね！？<br>
(<a href="#" class="imk-cue" data-seek="1:40" translate="no">00:01:40</a>) 13) 以上です。<br>
</p> </details>
<style>
details { font: 16px "Open Sans", Calibri, sans-serif; width: 100%; }
details > summary { padding: 2px 6px; width: 100%; background-color: #ddd; border: none; box-shadow: 3px 3px 4px black; cursor: pointer; list-style: none; }
details > p { font: 14px "Open Sans", Calibri, sans-serif; height:150px; overflow: scroll; background-color: #EDF7FF; padding: 2px 6px; margin: 0; box-shadow: 3px 3px 4px black; }
</style>
</div><script>
(function(){
  var root=document.querySelector(".dr5emd-sublist");
  var video=document.getElementById("myVideo");
  if(!root || !video) return;
  function parseTs(ts){
    if(!ts) return null;
    var p=ts.trim().split(":").map(function(x){return parseInt(x,10)||0;});
    if(p.length===2) return p[0]*60 + p[1];
    if(p.length===3) return p[0]*3600 + p[1]*60 + p[2];
    return null;
  }
  root.addEventListener("click", function(e){
    var a=e.target.closest && e.target.closest("a.imk-cue[data-seek]");
    if(!a || !root.contains(a)) return;
    e.preventDefault();
    var sec = parseTs(a.getAttribute("data-seek"));
    if(sec==null) return;
    try{ video.currentTime = sec; if(video.paused) video.play(); }catch(_){}
  });
  video.addEventListener("timeupdate", function(){
    var listContainer = root.querySelector("details > p");
    if(!listContainer) return;
    var cues = listContainer.querySelectorAll("a.imk-cue");
    if(cues.length === 0) return;
    var cur = video.currentTime;
    var active = null;
    for(var i=0; i<cues.length; i++){
        var t = parseTs(cues[i].getAttribute("data-seek"));
        if(t !== null && cur >= t - 0.5){
            active = cues[i];
        } else if(t > cur){
            break;
        }
    }
    if(active){
        if(active.classList.contains("active-hl")) return;
        var old = listContainer.querySelectorAll(".active-hl");
        for(var k=0; k<old.length; k++) old[k].classList.remove("active-hl");
        active.classList.add("active-hl");
        if(listContainer.offsetParent !== null){
            var containerRect = listContainer.getBoundingClientRect();
            var activeRect = active.getBoundingClientRect();
            var targetScroll = listContainer.scrollTop + (activeRect.top - containerRect.top) - (listContainer.clientHeight / 2) + (active.clientHeight / 2);
            listContainer.scrollTo({ top: targetScroll, behavior: "smooth" });
        }
    }
  });
})();
</script>
                </div>

  <script>
  (function(){
    var me = document.currentScript;
    var wrapper = me ? me.closest('.sc-dynamic-embed') : null;

    /* -----------------------------------------------
       1. ダウンロードボタンの自動セットアップ機能
       ----------------------------------------------- */
    function setupDownloadButton() {
      var target = wrapper ? wrapper : document;
      var video = target.querySelector('video');
      var dlBtn = target.querySelector('#imk-dynamic-dl-btn');

      if (video && dlBtn && dlBtn.style.display === 'none') {
        var src = video.currentSrc || video.src;
        if (!src) {
          var source = video.querySelector('source');
          if (source) src = source.src;
        }
        if (src) {
          dlBtn.href = src;
          dlBtn.style.display = 'inline-block';
        }
      }
    }

    /* -----------------------------------------------
       2. 字幕制御＆ハイライト機能（iPhone全画面 完全対応版）
       ----------------------------------------------- */
    function initSubtitles() {
      var target = wrapper ? wrapper : document;
      var video = target.querySelector('video');
      var listContainer = target.querySelector('details > p');
      
      if (!video || !listContainer) return false; 

      if (video.dataset.subInit === 'true') return true; 
      video.dataset.subInit = 'true';

      var oldOverlay = target.querySelector('#subtitleOverlay') || target.querySelector('#scSubtitleOverlay');
      if (oldOverlay) {
          oldOverlay.style.setProperty('display', 'none', 'important');
          oldOverlay.innerHTML = ''; 
      }

      function isSpecialMode() {
        var isFs = !!(document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement);
        var isPiP = !!(document.pictureInPictureElement && document.pictureInPictureElement === video) || (video.webkitPresentationMode === "picture-in-picture");
        var isIOSFs = !!video.webkitDisplayingFullscreen; 
        return isFs || isPiP || isIOSFs;
      }

      try {
        if(video.textTracks && video.textTracks.length > 0){
          for(var i=0; i<video.textTracks.length; i++){
             if(video.textTracks[i].kind === 'subtitles' || video.textTracks[i].kind === 'captions'){
                 video.textTracks[i].mode = "hidden";
             }
          }
        }
      } catch(e){}

      video.addEventListener("webkitbeginfullscreen", function() {
          try {
              if(video.textTracks && video.textTracks.length > 0) {
                  for(var i=0; i<video.textTracks.length; i++){
                      if(video.textTracks[i].kind === 'subtitles' || video.textTracks[i].kind === 'captions') video.textTracks[i].mode = "showing";
                  }
              }
          } catch(e){}
      });
      video.addEventListener("webkitendfullscreen", function() {
          try {
              if(video.textTracks && video.textTracks.length > 0) {
                  for(var i=0; i<video.textTracks.length; i++){
                      if(video.textTracks[i].kind === 'subtitles' || video.textTracks[i].kind === 'captions') video.textTracks[i].mode = "hidden";
                  }
              }
          } catch(e){}
      });

      var detailsEl = target.querySelector("details");
      if (detailsEl) {
          detailsEl.open = true; 
          var summaryEl = detailsEl.querySelector("summary");
          if (summaryEl) summaryEl.textContent = "字幕(シーン)はここをクリック";
      }

      if (!listContainer.dataset.formatted) {
          var html = listContainer.innerHTML;
          var lines = html.split(/<br\s*\/?>/i);
          var newHtml = "";
          for(var j=0; j<lines.length; j++) {
              if(lines[j].trim() === "") continue;
              newHtml += "<span class='imk-line'>" + lines[j] + "</span><br>";
          }
          listContainer.innerHTML = newHtml;
          listContainer.dataset.formatted = "true";
      }

      function parseTs(ts){
        if(!ts) return null;
        var p = ts.trim().split(":").map(function(x){return parseInt(x,10)||0;});
        if(p.length===2) return p[0]*60 + p[1];
        if(p.length===3) return p[0]*3600 + p[1]*60 + p[2];
        return null;
      }

      var rootSublist = target.querySelector(".dr5-sublist") || listContainer.parentElement;
      if (rootSublist) {
        rootSublist.addEventListener("click", function(e){
          var a = e.target.closest && e.target.closest("a.imk-cue[data-seek]");
          if(!a) return;
          e.preventDefault();
          var sec = parseTs(a.getAttribute("data-seek"));
          if(sec==null) return;
          try{ video.currentTime = sec; if(video.paused) video.play(); }catch(_){}
        });
      }

      video.addEventListener("timeupdate", function(){
        var desiredMode = isSpecialMode() ? "showing" : "hidden";
        try {
            if(video.textTracks && video.textTracks.length > 0){
                for(var i=0; i<video.textTracks.length; i++){
                    if((video.textTracks[i].kind === 'subtitles' || video.textTracks[i].kind === 'captions') && video.textTracks[i].mode !== desiredMode) {
                        video.textTracks[i].mode = desiredMode;
                    }
                }
            }
        } catch(e){}

        var cues = listContainer.querySelectorAll("a.imk-cue");
        if(cues.length === 0) return;
        var cur = video.currentTime;
        var activeA = null;

        for(var i=0; i<cues.length; i++){
            var t = parseTs(cues[i].getAttribute("data-seek"));
            if(t !== null && cur >= t - 0.5){ activeA = cues[i]; } 
            else if(t > cur){ break; }
        }

        if(activeA){
            var activeLine = activeA.closest(".imk-line");
            if(!activeLine) activeLine = activeA;

            if(activeLine.classList.contains("active-hl")) return;

            var allLines = listContainer.querySelectorAll(".imk-line");
            for(var k=0; k<allLines.length; k++) {
                allLines[k].classList.remove("active-hl");
                allLines[k].removeAttribute("style"); 
            }
            var allLinks = listContainer.querySelectorAll("a");
            for(var m=0; m<allLinks.length; m++) {
                allLinks[m].classList.remove("active-hl");
                allLinks[m].removeAttribute("style"); 
            }

            activeLine.classList.add("active-hl");
            activeLine.style.setProperty("background-color", "#ffff00", "important");
            activeLine.style.setProperty("color", "red", "important");
            activeLine.style.setProperty("font-weight", "normal", "important");
            
            var newLinks = activeLine.querySelectorAll("a");
            for(var n=0; n<newLinks.length; n++) {
                newLinks[n].style.setProperty("color", "red", "important");
                newLinks[n].style.setProperty("text-decoration", "none", "important");
            }

            if(listContainer.offsetParent !== null){
                var containerRect = listContainer.getBoundingClientRect();
                var activeRect = activeLine.getBoundingClientRect();
                var targetScroll = listContainer.scrollTop + (activeRect.top - containerRect.top) - (listContainer.clientHeight / 2) + (activeLine.clientHeight / 2);
                listContainer.scrollTo({ top: targetScroll, behavior: "smooth" });
            }
        }
      });

      return true;
    }

    /* -----------------------------------------------
       監視タイマー（URL抜き出し＆ボタン表示を継続的に実行）
       ----------------------------------------------- */
    var checks = 0;
    var checkTimer = setInterval(function(){
      setupDownloadButton();
      var success = initSubtitles();
      checks++;
      if (success || checks > 20) { 
        clearInterval(checkTimer);
      }
    }, 500); 

    /* -----------------------------------------------
       3. 画面遷移時の停止機能
       ----------------------------------------------- */
    if (!window.scStopAndGo) {
      window.scStopAndGo = function(event, link){
        try{
          var mediaEls = document.querySelectorAll('video, audio');
          mediaEls.forEach(function(m){
            try{
              if (!m.paused) m.pause();
              if (document.pictureInPictureElement === m && document.exitPictureInPicture) {
                document.exitPictureInPicture().catch(function(){});
              }
            }catch(e){}
          });
        }finally{
          event.preventDefault();
          setTimeout(function(){
            if (link.target === '_blank') {
              window.open(link.href, '_blank');
            } else {
              window.location.href = link.href;
            }
          }, 50);
        }
        return false;
      };
    }
  })();
  </script>
</div>



<h3 class="wp-block-heading">ポイント解説</h3>



<p><strong>１　字幕一覧の翻訳はGoogle翻訳の勝ち！</strong></p>



<figure class="wp-block-image size-large"><a href="https://imakat.com/rd.php?id=iP7i3FkJ.png" target="_blank"><img decoding="async" src="https://imakat.com/rd.php?id=iP7i3FkJ.png" alt=""/></a></figure>



<p><strong>２　画像内の文字の翻訳はApple翻訳の勝ち！</strong></p>



<figure class="wp-block-image size-large"><a href="https://imakat.com/rd.php?id=1F0netdc.png" target="_blank"><img decoding="async" src="https://imakat.com/rd.php?id=1F0netdc.png" alt=""/></a></figure>



<p><strong>３　結論</strong><br>可能なら両方ONにして使ってみましょう。<br><strong>現状(25年9月)、ブログ翻訳については、ChromeでGoogle翻訳を使うのが、一番安定しています</strong>。</p>



<p>以上です。<br><br></p>
]]></content:encoded>
					
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">27559</post-id>	</item>
		<item>
		<title>「字幕かんたん作成」〜無音声動画に解説字幕をサクッと付ける方法(Automator+AppleScript)</title>
		<link>https://imakat.com/2025/08/28/27063/</link>
		
		<dc:creator><![CDATA[imakat]]></dc:creator>
		<pubDate>Thu, 28 Aug 2025 09:32:54 +0000</pubDate>
				<category><![CDATA[字幕作成]]></category>
		<category><![CDATA[デジタル]]></category>
		<category><![CDATA[ものづくり]]></category>
		<category><![CDATA[vtt]]></category>
		<category><![CDATA[QuickTimePlayer]]></category>
		<category><![CDATA[TextEdit]]></category>
		<category><![CDATA[HUD]]></category>
		<guid isPermaLink="false">https://imakat.com/?p=27063</guid>

					<description><![CDATA[これまで動画の字幕作成については、何度か記事を投稿してきましたが、その中で感じた課題のいくつかを克服するアプリを、自作しましたので紹介します。 別の投稿で紹介したいと思いますが、字幕起こしを伴う字幕作成サービスについては [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>これまで動画の字幕作成については、何度か記事を投稿してきましたが、その中で感じた課題のいくつかを克服するアプリを、自作しましたので紹介します。</p>



<p>別の投稿で紹介したいと思いますが、字幕起こしを伴う字幕作成サービスについては日進月歩でして、どんどん高品質になっています。大きく分けると、ローカルで処理する方法（大抵は無料でできる）と外部サーバーで処理する方法（大抵は使用料課金かサブスク提供)がありまして、外部サーバー利用の方については激しい競争下にあるためかなり高品質で、生成AI系企業のサービスが群を抜いています。</p>



<p>最近、AIの進展で、動画をアップロードするだけで音声を取り出しその内容をAIに理解させることができるようになってはきましたが、やはり字幕ファイルによる文字情報で内容を理解させる方が圧倒的に迅速で正確です。しかも動画からの読み込みは多大なリソースを消費するので、追加料金も発生するかも知れません。そんな意味で、ブログ投稿をAIに学習させる場合も、動画には字幕ファイルを別添して貼り付けるようにして、字幕ファイルからの情報吸い上げを重視すべきだと私は思います。</p>



<p>そんな中で、ここで紹介する方法は、<strong>無音声の動画に解説用の字幕をサクッと付ける</strong>場合、に役立つものです。</p>


<div class="sc-vimeo-embed">
  <p style="font-size: 14px;" class="sc-link">
    <a href="https://imakat.com/vm5?movid=1176379255" target="_blank" onclick="stopVimeoBeforeNavigate(event, this)">
      動画を別ページで表示(ここをクリック)
    </a>
  </p>

  <div class="iframe-wrapper">
    <iframe
      src="https://player.vimeo.com/video/1176379255?title=0&byline=0&portrait=0&controls=1&speed=1&texttrack=ja&dnt=1&loop=0"
      frameborder="0"
      allow="autoplay; fullscreen; picture-in-picture"
      allowfullscreen></iframe>
  </div>

  <div class="dr-vimeo-sublist">
    <details translate="yes"><summary translate="yes">字幕一覧(クリック)</summary> <p translate="yes">
(<a href="#" class="imk-cue" data-seek="0:00" translate="no">00:00:00</a>) 1) 〜字幕かんたん作成〜<br>
(<a href="#" class="imk-cue" data-seek="0:20" translate="no">00:00:20</a>) 2) 今回は、Macで簡単に、動画の字幕ファイルを作成できる『字幕かんたん作成』これを紹介します。<br>
(<a href="#" class="imk-cue" data-seek="0:35" translate="no">00:00:35</a>) 3) 動画に字幕を付けるのに、編集ソフトで、コツコツと、タイムスタンプを入れるのは、とても大変ですよね。<br>
(<a href="#" class="imk-cue" data-seek="0:51" translate="no">00:00:51</a>) 4) このアプリを使えば、QuickTimePlayerで動画を再生しながら、簡単に、字幕を打ち込んでいけます。<br>
(<a href="#" class="imk-cue" data-seek="1:00" translate="no">00:01:00</a>) 5) お金は掛かりません。<br>
(<a href="#" class="imk-cue" data-seek="1:03" translate="no">00:01:03</a>) 6) Macに備わった標準アプリだけで動きます。<br>
(<a href="#" class="imk-cue" data-seek="1:12" translate="no">00:01:12</a>) 7) 通常の字幕作成では、動画の再生時間を確認して、開始時刻と終了時刻を、VTTファイルへ入力します。<br>
(<a href="#" class="imk-cue" data-seek="1:27" translate="no">00:01:27</a>) 8) サンプルをお見せします。<br>
(<a href="#" class="imk-cue" data-seek="1:33" translate="no">00:01:33</a>) 9) 例えば、このような文字列があります。<br>
(<a href="#" class="imk-cue" data-seek="1:37" translate="no">00:01:37</a>) 10) これはVTTファイルです。<br>
(<a href="#" class="imk-cue" data-seek="1:41" translate="no">00:01:41</a>) 11) 最初に、順番が入ります。　　<br>
(<a href="#" class="imk-cue" data-seek="1:46" translate="no">00:01:46</a>) 12) 次に、時　分　秒　ミリ秒、この基準に従った開始時刻<br>
(<a href="#" class="imk-cue" data-seek="1:54" translate="no">00:01:54</a>) 13) それと、同じくその基準の終了時刻を入れます。<br>
(<a href="#" class="imk-cue" data-seek="2:01" translate="no">00:02:01</a>) 14) 真ん中のところに、スペース１つ、--不等号 またひとつスペースを入れます。<br>
(<a href="#" class="imk-cue" data-seek="2:13" translate="no">00:02:13</a>) 15) その下にこうしたテキスト行を入れます。<br>
(<a href="#" class="imk-cue" data-seek="2:19" translate="no">00:02:19</a>) 16) これがVTT記述の基本形です。<br>
(<a href="#" class="imk-cue" data-seek="2:25" translate="no">00:02:25</a>) 17) このように自分でTextEditに打てば、正確にできるのですが、　　<br>
(<a href="#" class="imk-cue" data-seek="2:34" translate="no">00:02:34</a>) 18) 手間が多く、時間もかかってしまいます。<br>
(<a href="#" class="imk-cue" data-seek="2:43" translate="no">00:02:43</a>) 19) そこで開発したのが、この『字幕かんたん作成』です。<br>
(<a href="#" class="imk-cue" data-seek="2:50" translate="no">00:02:50</a>) 20) AutomatorとAppleScript_Objective-Cを組み合わせて、<br>
(<a href="#" class="imk-cue" data-seek="2:57" translate="no">00:02:57</a>) 21) QuickTimePlayerとTextEditを連携させる、<br>
(<a href="#" class="imk-cue" data-seek="3:03" translate="no">00:03:03</a>) 22) そのことで、シンプルに、効率よく、字幕ファイルを作ることができます。<br>
(<a href="#" class="imk-cue" data-seek="3:14" translate="no">00:03:14</a>) 23) その仕組みをフロー図によって説明します。<br>
(<a href="#" class="imk-cue" data-seek="3:29" translate="no">00:03:29</a>) 24) まず、FinderでVTTファイルを右クリックします。<br>
(<a href="#" class="imk-cue" data-seek="3:41" translate="no">00:03:41</a>) 25) するとクイックアクションの中にある、今回のアプリ 「字幕かんたん作成」が起動します。<br>
(<a href="#" class="imk-cue" data-seek="3:51" translate="no">00:03:51</a>) 26) スクリプトが動作し、黄色の字幕表示画面(HUDと呼びますが)それと、　<br>
(<a href="#" class="imk-cue" data-seek="4:00" translate="no">00:04:00</a>) 27) 入力Boxが表示されます。<br>
(<a href="#" class="imk-cue" data-seek="4:09" translate="no">00:04:09</a>) 28) 字幕の入力先は、macOS標準のTextEditを使います。 <br>
(<a href="#" class="imk-cue" data-seek="4:17" translate="no">00:04:17</a>) 29) 入力したテキストは、QuickTimePlayerの再生位置の時刻、タイムスタンプ、それらと一緒にVTTファイルに記入されます。<br>
(<a href="#" class="imk-cue" data-seek="4:33" translate="no">00:04:33</a>) 30) QuickTimePlayerは再生位置を提供し、NSTimerが、それに対応する時刻を、定期的にゲットします。　<br>
(<a href="#" class="imk-cue" data-seek="4:48" translate="no">00:04:48</a>) 31) 終了する場合は、終了ボタンを押すと、タイマーが止まり、HUDが閉じ、メモリーが解放されて、処理が終了します。<br>
(<a href="#" class="imk-cue" data-seek="5:08" translate="no">00:05:08</a>) 32) それでは、実際の動きをご覧ください。 <br>
(<a href="#" class="imk-cue" data-seek="5:13" translate="no">00:05:13</a>) 33) 動画の例として、「ガラポンTVの故障」を取り上げます。<br>
(<a href="#" class="imk-cue" data-seek="5:21" translate="no">00:05:21</a>) 34) 一度、動画だけをみてください。（約1分） <br>
(<a href="#" class="imk-cue" data-seek="6:28" translate="no">00:06:28</a>) 35) 動画を見ただけでは、何を伝えたいのか、さっぱりわかりませんね。<br>
(<a href="#" class="imk-cue" data-seek="6:36" translate="no">00:06:36</a>) 36) そこで、字幕を、作成していきます。<br>
(<a href="#" class="imk-cue" data-seek="6:43" translate="no">00:06:43</a>) 37) それでは、作業をやってみます。<br>
(<a href="#" class="imk-cue" data-seek="6:49" translate="no">00:06:49</a>) 38) まず、動画を右上の方に置いておきます。<br>
(<a href="#" class="imk-cue" data-seek="6:57" translate="no">00:06:57</a>) 39) すでにvttファイル名は付けてありますが、中身をクリアして作業をしてみます。<br>
(<a href="#" class="imk-cue" data-seek="7:05" translate="no">00:07:05</a>) 40) このvttファイルの上で、右クリックして、クイックアクションで、「字幕かんたん作成」を立ち上げます。<br>
(<a href="#" class="imk-cue" data-seek="7:32" translate="no">00:07:32</a>) 41) こんなように立ち上がります。<br>
(<a href="#" class="imk-cue" data-seek="7:36" translate="no">00:07:36</a>) 42) このHUDをQuickTimePlayerの下に置いて、　<br>
(<a href="#" class="imk-cue" data-seek="7:42" translate="no">00:07:42</a>) 43) 実際に、テキストを入れていきます。<br>
(<a href="#" class="imk-cue" data-seek="8:03" translate="no">00:08:03</a>) 44) ガラポンTVが異常です。全ランプが点滅を繰り返しています。<br>
(<a href="#" class="imk-cue" data-seek="8:18" translate="no">00:08:18</a>) 45) このように打ってOKを押します。<br>
(<a href="#" class="imk-cue" data-seek="8:26" translate="no">00:08:26</a>) 46) 継続秒数を入れます。<br>
(<a href="#" class="imk-cue" data-seek="8:41" translate="no">00:08:41</a>) 47) 1番目が出来上がります。<br>
(<a href="#" class="imk-cue" data-seek="8:55" translate="no">00:08:55</a>) 48) 背面の電源をOFFにしました。<br>
(<a href="#" class="imk-cue" data-seek="9:11" translate="no">00:09:11</a>) 49) OK<br>
(<a href="#" class="imk-cue" data-seek="9:13" translate="no">00:09:13</a>) 50) 4秒<br>
(<a href="#" class="imk-cue" data-seek="9:17" translate="no">00:09:17</a>) 51) 2番目が入ります。<br>
(<a href="#" class="imk-cue" data-seek="9:20" translate="no">00:09:20</a>) 52) HUDにも表示されます。<br>
(<a href="#" class="imk-cue" data-seek="9:40" translate="no">00:09:40</a>) 53) 外付けHDDを外しました。<br>
(<a href="#" class="imk-cue" data-seek="10:02" translate="no">00:10:02</a>) 54) 前の部分で、説明を入れるのを忘れたとしても、戻ることができます。<br>
(<a href="#" class="imk-cue" data-seek="10:16" translate="no">00:10:16</a>) 55) この場所で、ランプは消えました、と入れます。<br>
(<a href="#" class="imk-cue" data-seek="10:35" translate="no">00:10:35</a>) 56) 3番目として、ランプが消えた、が飛び込みます。<br>
(<a href="#" class="imk-cue" data-seek="10:46" translate="no">00:10:46</a>) 57) このような形で、入力が終了し、再生した時に、字幕がタイミングよく表示されることを確認する必要があります。その後で実際は終了します。<br>
(<a href="#" class="imk-cue" data-seek="11:02" translate="no">00:11:02</a>) 58) それでは、動画と、完成した字幕を、同時に表示してみましょう。<br>
(<a href="#" class="imk-cue" data-seek="11:17" translate="no">00:11:17</a>) 59) 字幕があることによって、その、ガラポンTVが故障している、そのように判断した理由が、より明確に、伝わるようになります。<br>
(<a href="#" class="imk-cue" data-seek="12:10" translate="no">00:12:10</a>) 60) この後、ガラポンTV社に報告して、迅速に、交換が完了しました。<br>
(<a href="#" class="imk-cue" data-seek="12:24" translate="no">00:12:24</a>) 61) このように『字幕かんたん作成』は、AutomatorとAppleScript_Objective-Cを使って、<br>
(<a href="#" class="imk-cue" data-seek="12:33" translate="no">00:12:33</a>) 62) QuickTimePlayerとTextEditを組み合わせることによって、 　<br>
(<a href="#" class="imk-cue" data-seek="12:40" translate="no">00:12:40</a>) 63) 面倒な字幕作成を、シンプルに、効率的に、してくれます。 <br>
(<a href="#" class="imk-cue" data-seek="12:46" translate="no">00:12:46</a>) 64) このスクリプトは、ブログ内で公開します。<br>
(<a href="#" class="imk-cue" data-seek="12:50" translate="no">00:12:50</a>) 65) 字幕を、できるだけ手軽に付けたい、そう思う方は、ぜひ活用してみてください。<br>
</p> </details>
<style>
details { font: 16px "Open Sans", Calibri, sans-serif; width: 100%; }
details > summary { padding: 2px 6px; width: 100%; background-color: #ddd; border: none; box-shadow: 3px 3px 4px black; cursor: pointer; list-style: none; }
details > p { font: 14px "Open Sans", Calibri, sans-serif; height:150px; overflow: scroll; background-color: #EDF7FF; padding: 2px 6px; margin: 0; box-shadow: 3px 3px 4px black; }
</style>

  </div>

  <script src="https://player.vimeo.com/api/player.js"></script>

  <script>
  (function(){
    var me = document.currentScript;
    var wrapper = me ? me.closest('.sc-vimeo-embed') : null;

    var target = wrapper ? wrapper : document;
    var iframe = target.querySelector('iframe');
    if (!iframe || !window.Vimeo || !Vimeo.Player) return;

    var player = new Vimeo.Player(iframe);

    /* --- 1. 確実に一時停止してから別ページを開く処理 --- */
    window.stopVimeoBeforeNavigate = function(event, link) {
      event.preventDefault(); 
      player.pause().then(function() {
        setTimeout(function() {
           window.open(link.href, link.target);
        }, 50);
      }).catch(function() {
        window.open(link.href, link.target);
      });
    };

    /* --- 2. 字幕のON/OFF制御（Vimeo API仕様） --- */
    var currentMode = "hidden";
    
    function useNormalMode() {
        currentMode = "hidden";
        player.disableTextTrack().catch(function(e){});
    }
    
    function useSpecialMode() {
        currentMode = "showing";
        // 日本語の subtitles が無ければ captions を探す安全設計
        player.enableTextTrack('ja', 'subtitles').catch(function() {
            player.enableTextTrack('ja', 'captions').catch(function() {});
        });
    }

    // ロード直後は枠内字幕を非表示にする
    player.ready().then(function() {
        useNormalMode();
    });

    // Vimeoプレーヤーの全画面・PiPイベントを監視して字幕を切り替え
    player.on('fullscreenchange', function(data) {
        if (data.fullscreen) useSpecialMode();
        else useNormalMode();
    });
    player.on('enterpictureinpicture', function() {
        useSpecialMode();
    });
    player.on('leavepictureinpicture', function() {
        useNormalMode();
    });

    /* --- 3. 字幕リストの初期化（自動梱包とデフォルト展開） --- */
    var checks = 0;
    var initTimer = setInterval(function(){
        var listContainer = target.querySelector('details > p');
        if (listContainer) {
            initSubtitles(listContainer);
            clearInterval(initTimer);
        }
        checks++;
        if (checks > 20) clearInterval(initTimer); // 10秒経ったら監視終了
    }, 500);

    function initSubtitles(listContainer) {
        // デフォルトで開く
        var detailsEl = target.querySelector("details");
        if (detailsEl) {
            detailsEl.open = true; 
            var summaryEl = detailsEl.querySelector("summary");
            if (summaryEl) summaryEl.textContent = "字幕(シーン)はここをクリック";
        }

        // 行ごとに見えない箱(span)で梱包
        if (!listContainer.dataset.formatted) {
            var html = listContainer.innerHTML;
            var lines = html.split(/<br\s*\/?>/i);
            var newHtml = "";
            for(var j=0; j<lines.length; j++) {
                if(lines[j].trim() === "") continue;
                newHtml += "<span class='imk-line'>" + lines[j] + "</span><br>";
            }
            listContainer.innerHTML = newHtml;
            listContainer.dataset.formatted = "true";
        }
    }

    /* --- 4. クリックシーク機能 --- */
    function parseTs(ts) {
      if (!ts) return null;
      var t = ts.trim();
      var parts = t.split(':').map(function(v){ return parseInt(v, 10); });
      if (parts.length === 2 && parts.every(Number.isFinite)) {
        return parts[0] * 60 + parts[1];
      } else if (parts.length === 3 && parts.every(Number.isFinite)) {
        return parts[0] * 3600 + parts[1] * 60 + parts[2];
      }
      return null;
    }

    target.addEventListener('click', function(e) {
      var a = e.target.closest('a.imk-cue');
      if (!a) return;
      e.preventDefault();
      var sec = null;
      if (a.dataset.seconds) {
        sec = Number(a.dataset.seconds);
      } else if (a.dataset.seek) {
        sec = parseTs(a.dataset.seek);
      }
      if (sec !== null && Number.isFinite(sec)) {
        player.setCurrentTime(sec).then(function(){ player.play(); }).catch(function(){});
      }
    });

    /* --- 5. 同期ハイライトと自動スクロール --- */
    player.on('timeupdate', function(data) {
      // Vimeoプレーヤーが勝手に字幕を出していたら隠す（最強ガード）
      player.getTextTracks().then(function(tracks) {
          var activeTrack = tracks.find(function(t){ return t.mode === 'showing'; });
          if (currentMode === "hidden" && activeTrack) {
              useNormalMode();
          }
      }).catch(function(){});

      var currentTime = data.seconds;
      var listContainer = target.querySelector('details > p');
      if (!listContainer) return;

      var cues = listContainer.querySelectorAll('a.imk-cue');
      if (cues.length === 0) return;

      var activeA = null;
      for (var i = 0; i < cues.length; i++) {
        var sec = null;
        if (cues[i].dataset.seconds) {
            sec = Number(cues[i].dataset.seconds);
        } else if (cues[i].dataset.seek) {
            sec = parseTs(cues[i].getAttribute('data-seek'));
        }
        if (sec !== null && currentTime >= sec - 0.5) {
          activeA = cues[i];
        } else if (sec > currentTime) {
          break;
        }
      }

      if (activeA) {
          var activeLine = activeA.closest(".imk-line");
          if (!activeLine) activeLine = activeA;

          if (activeLine.classList.contains("active-hl")) return;

          // 全行の色を徹底的にリセット
          var allLines = listContainer.querySelectorAll(".imk-line");
          for(var k=0; k<allLines.length; k++) {
              allLines[k].classList.remove("active-hl");
              allLines[k].removeAttribute("style");
          }
          var allLinks = listContainer.querySelectorAll("a");
          for(var m=0; m<allLinks.length; m++) {
              allLinks[m].classList.remove("active-hl");
              allLinks[m].removeAttribute("style");
          }

          // 新しい行に赤文字・黄色背景を直接塗る
          activeLine.classList.add("active-hl");
          activeLine.style.setProperty("background-color", "#ffff00", "important");
          activeLine.style.setProperty("color", "red", "important");
          activeLine.style.setProperty("font-weight", "normal", "important");
          
          var newLinks = activeLine.querySelectorAll("a");
          for(var n=0; n<newLinks.length; n++) {
              newLinks[n].style.setProperty("color", "red", "important");
              newLinks[n].style.setProperty("text-decoration", "none", "important");
          }

          // スクロール処理
          if (listContainer.offsetParent !== null) {
              var containerRect = listContainer.getBoundingClientRect();
              var activeRect = activeLine.getBoundingClientRect();
              var targetScroll = listContainer.scrollTop + (activeRect.top - containerRect.top) - (listContainer.clientHeight / 2) + (activeLine.clientHeight / 2);
              listContainer.scrollTo({
                  top: targetScroll,
                  behavior: "smooth"
              });
          }
      }
    });
  })();
  </script>

  <style>
  .sc-vimeo-embed .iframe-wrapper {
    position: relative;
    padding-bottom: 56.25%;
    height: 0;
    overflow: visible;
  }
  .sc-vimeo-embed .iframe-wrapper iframe {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
  }

  /* 行梱包時の基本スタイル（干渉防止のためクラス内で指定） */
  .sc-vimeo-embed .imk-line {
      display: inline-block;
      width: 100%;
      border-radius: 2px;
      transition: background-color 0.1s;
  }

  /* ▼ 字幕一覧（details）の見た目調整 */
  .sc-vimeo-embed details > p {
    font-size: 14px !important;
    line-height: 1.6;
    height: 200px !important; 
    overflow: auto;
    background-color: #EDF7FF;
    padding: 2px 6px;
    margin: 0;
    box-shadow: 3px 3px 4px black;
    position: relative; 
  }
  .sc-vimeo-embed details > summary {
    font-size: 14px !important;
    padding: 2px 6px;
    width: 100%;
    background-color: #ddd;
    border: none;
    box-shadow: 3px 3px 4px black;
    cursor: pointer;
    list-style: none;
  }
  </style>
</div>



<h3 class="wp-block-heading">ポイント解説</h3>



<p><strong>１　VTTで編集ができます</strong>：<br>簡易な字幕作成ツールとしては、「字幕工房」が優秀であるという見方は変わりませんが、SRTファイルでの書き出しです。私はWordPressブログに掲載する動画が殆どですので、毎回VTTファイルへ変換することになり面倒です。「字幕工房」に限らず、動画作成アプリで出力される字幕は、殆どがSRTファイルなのですね。</p>



<p><strong>２　ローカルファイルの更新をそのまま反映します</strong>：<br>動画共有サービスに共通することですが、字幕もそのほかのメディアアセットもローカルからアップロードした時点で切り離されてしまうので、ローカルファイルを更新しても反映されません。図の下段は、今運用しているマイ・メディアライブラリの機能ですが、ここでもその威力が発揮されます。ローカルでVTTファイルをTextEditで開いて手直しすれば、そのまま、ブログの表示が変更されます（数分のタイムラグはあり）。</p>



<a rel="noopener" target="_blank" href="https://docs.google.com/drawings/d/1GXuQ_5Vtufv7YH8_SwkfyAMHxzctFyzNb4J4dmJ-MDc/edit?usp=sharing" 
>
<img decoding="async" src="https://docs.google.com/drawings/d/e/2PACX-1vSpM7uCAK9FehOqeogKeJoeCMs1929xNU45hsx1tk_CUlhO9S8CH30BgcnNMx4URJ6nH83dm9uGP2lS/pub?w=1440&#038;h=1080"
></a>



<p><strong>３　停止位置の時刻を自動で取得します</strong>：<br>QuickTimePlayerで動画を停止した場所の時刻(時点)を自動で取得して、テキストとともに、VTTファイルへ書き込みます。さらに、書き込んだ情報を読み込んで、HUD(ディスプレイ)に即座に表示します。</p>



<p>主な特徴点は以上ですが、かんたんな点では、「字幕工房」を上回っています。</p>



<p><strong>４　わずか2ステップ</strong>：</p>



<p><strong>①対象の動画をクリックして開く</strong>(デフォルトはQuickTimePlayer)。</p>



<p><strong>②フォルダの上で右クリックして、「字幕かんたん作成」を立ち上げ、VTTファイルを作る</strong>。</p>



<p><strong>だけ</strong>です。</p>



<p><strong>５　キビキビと動きます：</strong><br>字幕作成アプリを探すと、ブラウザアプリが主流になっています。その<strong>ブラウザアプリですが、欠点は、「もったりヌルヌルした動作</strong>」にあります。iPad iPhoneでは仕方ないにしても、Macでも感じるところがあるので、それはなんか疲れるわけですね。QuickTimePlayer、TextEdit、標準スクリプト、そういった標準搭載ツールは、実にキビキビと動きます。</p>



<h3 class="wp-block-heading">実装の手順</h3>



<figure class="wp-block-flexible-table-block-table"><table class="has-fixed-layout"><tbody><tr><td style="width:10%">(1)</td><td>下の青色のボタンから、スクリプト(txtファイル)をダウンロードします。</td></tr><tr><td style="width:10%">(2)</td><td>生成AI( Gemini、ChatGPTなど)を利用すると簡単です。AIのプロンプト欄に、<br>①ダウンロードしたスクリプトをアップロードします。<br>②以下を記述します。<br>「私のMacに、添付したスクリプトを実装する手順を作ってください。それをtxt形式で提供ください。<br>私のMacは、[機種名][OSバージョン]です。よろしくお願いします。」</td></tr><tr><td style="width:10%">(3)</td><td>提供された、実装手順を記載したtxtファイルに基づき、生成AIとやりとりしながら、実装してください。<br>※txtファイルを開いたまま生成AIとやり取りし、修正後のtxtを再度送って「これで大丈夫ですか」と確認する方法を取れば、更新内容を逐一手元に残せるため、作業の履歴管理として便利で安心です。</td></tr><tr><td colspan="2"><a href="https://imakat.com/?pubtxt=字幕かんたん作成workflow_pub.txt" target="_blank"><img decoding="async" src="https://imakat.com/rd.php?id=zrU95MiY.png" alt="" style="width:120px; height:auto;"></a></td></tr></tbody></table></figure>



<p>以上です。<br><br></p>



<p></p>
]]></content:encoded>
					
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">27063</post-id>	</item>
		<item>
		<title>WordPress標準メディアライブラリからマイライブラリへ移行するととても便利！</title>
		<link>https://imakat.com/2025/08/08/26910/</link>
		
		<dc:creator><![CDATA[imakat]]></dc:creator>
		<pubDate>Fri, 08 Aug 2025 07:23:36 +0000</pubDate>
				<category><![CDATA[デジタル]]></category>
		<category><![CDATA[ものづくり]]></category>
		<category><![CDATA[メディアライブラリ]]></category>
		<guid isPermaLink="false">https://imakat.com/?p=26910</guid>

					<description><![CDATA[はじめに： WordPressの標準メディアライブラリは、シンプルで直感的に使える反面、運用が長くなると「使いにくさ」が目立ってきます。 特に、大量のファイル管理・差し替え・動的な配信が必要な場合、その制限は大きな足かせ [&#8230;]]]></description>
										<content:encoded><![CDATA[
<h5 class="wp-block-heading">はじめに：</h5>



<p>WordPressの標準メディアライブラリは、シンプルで直感的に使える反面、運用が長くなると「使いにくさ」が目立ってきます。</p>



<p>特に、<strong>大量のファイル管理・差し替え・動的な配信</strong>が必要な場合、その制限は大きな足かせになります。</p>



<p>この記事では、私が実際に運用している「マイ・メディアライブラリ」に移行することで得られたメリットを、具体的にご紹介します。</p>



<h5 class="wp-block-heading"><br><strong>WordPress標準メディアライブラリの制約</strong>：</h5>



<figure class="wp-block-image size-large has-custom-border is-style-default"><a href="https://imakat.com/rd.php?id=cC8GlHlw.png" target="_blank"><img decoding="async" src="https://imakat.com/rd.php?id=cC8GlHlw.png" alt="" style="border-width:2px"/></a></figure>



<ol start="1" class="wp-block-list">
<li><strong>ファイル名だけがランダム化されるのは中途半端</strong><br>アップロード時、物理ファイル名がランダム文字列に置き換わります。しかしパスはWordPress内のフォルダ構造がそのまま表示されます。中途半端です。</li>



<li><strong>配信URLが静的</strong><br>WordPressの標準メディアライブラリには、配信URLを、動的URLに変換する機能はありません。（動的URLとは、ある条件に従って、指定したサーバーへリダイレクトする機能のことです。）</li>



<li><strong>ローカルファイルパスが保持されない</strong><br>管理画面ではサーバー上のパスが見えますが、外部ストレージやローカル環境での実パスまでは記録されません。そのため迷子になります。</li>



<li><strong>差し替えが面倒</strong><br>同じURLでの置き換えはプラグインが必須。標準メディアライブラリでは新規アップロード＋記事修正が必要です。</li>
</ol>



<p>私が構築した「マイライブラリ」では、以下の点でWordPress標準メディアライブラリを大きく上回ります。</p>



<h5 class="wp-block-heading"><br><strong><br><strong>マイ・メディアライブラリの特徴</strong></strong>：</h5>



<figure class="wp-block-image size-large"><a href="https://imakat.com/rd.php?id=KdYhDkfj.png" target="_blank"><img decoding="async" src="https://imakat.com/rd.php?id=KdYhDkfj.png" alt=""/></a></figure>



<p></p>



<ul class="wp-block-list">
<li><strong>配信URLが不変</strong><br>ローカルのファイルパスやファイル名を変更しても、配信URLは&nbsp;rd.php?id=&#8230;&nbsp;形式で固定。記事リンクや外部配信先が切れません。</li>



<li><strong>動的URLで配信</strong><br>表示時に&nbsp;rd.php&nbsp;がIDから実ファイルを探して返す仕組み。さらに、
<ul class="wp-block-list">
<li><strong>配信サーバーを拡張子別に設定可能</strong>（例：.movはXserver、.jpgはDropbox など）</li>



<li><strong>ファイルごとに配信サーバーを個別指定可能</strong></li>



<li>裏側で保存場所やファイル構造を変更しても即反映</li>



<li>外部ストレージやローカルファイルへの切り替えもスムーズ</li>



<li>将来的にフェイルオーバーを行う(災害時など自動で別サーバーに切り替える)ことも可能です。</li>
</ul>
</li>



<li><strong>ローカルファイルパスも自動的に記録</strong><br>例：/Volumes/xxxxxx/Dropbox/xxxxxxxxx/pmedia/&#8230;元データの直接呼び出しや加工がすぐ可能。</li>



<li><strong>差し替えが自由</strong><br>配信ファイルの中身の差し替えは簡単です。ローカルファイル上で、上書き保存すれば、更新後のファイルが各配信サーバーへ転送されます。</li>



<li><strong>更新履歴の管理</strong><br>ファイルパス変更前後や更新フラグを記録し、追跡可能。</li>
</ul>



<h5 class="wp-block-heading"><br><strong><br></strong>運用のイメージ：</h5>



<p>下のアルゴリズム図のように、ファイルの変更や追加・削除を検知して、</p>



<p>必要に応じて配信URLに紐づくレコードを更新します。</p>



<a rel="noopener" target="_blank" href="https://docs.google.com/drawings/d/1JgcOPS8GESxG34LDFSUE1i3CVe0PSM6sAyZrfqkV0cc/edit?usp=sharing" 
>
<img decoding="async" src="https://docs.google.com/drawings/d/e/2PACX-1vSANFz-ypSmjCU127MiCadlAinb6t1kCfcIgPHB5tiKXOw8EXyxU6RKubeYVeWA-K_QeYQ07X9Y5rz7/pub?w=960&#038;h=720"
></a>



<p>さらに、次のように拡張子別・ファイル別で配信サーバーを切り替える設定画面も備えています(AppSheet）。</p>



<p></p>



<figure class="wp-block-image size-large has-custom-border"><a href="https://imakat.com/rd.php?id=QVyqu8Vp.png" target="_blank"><img decoding="async" src="https://imakat.com/rd.php?id=QVyqu8Vp.png" alt="" style="border-width:3px"/></a></figure>



<h5 class="wp-block-heading"><br><strong><br></strong><br><strong>WordPress標準メディアライブラリからの移行メリット</strong>：</h5>



<ul class="wp-block-list">
<li>過去記事やSNS投稿の画像リンク切れを防げる</li>



<li>外部ストレージやローカル環境とシームレス連携</li>



<li>動的URLにより、裏側の構造変更を即反映</li>



<li>拡張子別・ファイル別で配信サーバーを柔軟に選択可能</li>



<li>ファイル整理や命名ルール変更が怖くない</li>



<li>差し替え・更新作業の工数を大幅削減</li>
</ul>



<h5 class="wp-block-heading"><strong><br></strong><br>具体的な作成方法について：</h5>



<p>こちらの<a href="https://imakat.com/media_library1/" target="_blank">記事</a>をご覧ください。</p>



<h5 class="wp-block-heading"><strong><br></strong><br>まとめ：</h5>



<p>WordPressの標準メディアライブラリは静的でシンプルですが、大量のメディアを長期的に運用するには不向きな面があります。</p>



<p>一方、マイライブラリのように<strong>URL固定＋動的配信＋配信サーバー選択＋裏側は自由に更新できる構造</strong>にすれば、運用の効率も自由度も格段にアップします。</p>



<p>以上です。</p>
]]></content:encoded>
					
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">26910</post-id>	</item>
	</channel>
</rss>
