ホーム 記事一覧 ブック DH週間トピックス 検索 このサイトについて
English

DIGITAL ARCHIVE TECH BLOG

デジタルアーカイブシステムの技術ブログ

デジタルアーカイブ・デジタルヒューマニティーズ・ソフトウェア開発に関する記事を発信しています

最新の記事

Hugo + Tailwind CSS v4による独自テーマの作成とHugo Themesギャラリーへの登録

Hugo + Tailwind CSS v4による独自テーマの作成とHugo Themesギャラリーへの登録

PaperModテーマを使っていたHugoブログを、Tailwind CSS v4ベースの独自テーマに移行し、Hugo公式テーマギャラリーに登録しました。その過程で遭遇した問題と対処を記録します。 Hugo + Tailwind CSS v4の統合 Hugo 0.157以降ではcss.TailwindCSSパイプラインがサポートされています。Tailwind v4は@import "tailwindcss"ベースの構文に変わっており、tailwind.config.jsは不要です。 /* assets/css/main.css */ @import "tailwindcss"; @theme { --color-primary: #2563eb; --font-sans: "Inter", sans-serif; } @variant dark (&:where(.dark, .dark *)); テンプレート側ではcss.TailwindCSSを呼び出します。 {{ with resources.Get "css/main.css" | css.TailwindCSS }} <link rel="stylesheet" href="{{ .RelPermalink }}"> {{ end }} ビルド時に注意が必要な点として、@tailwindcss/cliへのパスが通っている必要があります。Cloudflare Pagesなどのデプロイ環境では以下のようにPATHを設定します。 # hugo.yaml (build settings for Cloudflare Pages) build: command: "PATH=$PWD/node_modules/.bin:$PATH hugo --minify && npx pagefind --site public" PaperModからの移行方法 テーマの切り替えはhugo.yamlのtheme:を変更するだけで済むため、独自テーマを開発しながらいつでもPaperModに戻せます。 # hugo.yaml theme: hugo-theme-flavor # PaperModに戻す場合は "PaperMod" に変更 移行作業は主に、layouts/ディレクトリのオーバーライドファイルをテーマ内に統合する作業でした。PaperModの構造に依存していた箇所(パーシャルの呼び出しやCSS変数名)を独自テーマの構造に合わせて書き換えています。 スタッキングコンテキストの問題 position: stickyを指定したヘッダー内にposition: fixedのモバイルメニューを配置したところ、z-indexを大きな値に設定してもメニューが他の要素の背面に表示される現象が発生しました。 ...

AI画像生成でiOSアプリアイコンを作るためのプロンプトテンプレート

はじめに iOSアプリのアイコンをAI画像生成ツール(Gemini、ChatGPT/DALL-E、Midjourney等)で作る際、Apple Human Interface Guidelines(HIG)に準拠した画像を一発で生成するのは意外と難しいです。 よくある失敗: 角丸が事前に適用されている(iOSが自動で角丸マスクをかけるので不要) 四隅に白い隙間がある テキストが入っている 細かすぎて小サイズで潰れる 本記事では、これらの問題を回避するプロンプトテンプレートを紹介します。 テンプレート A flat, square 1024x1024 app icon for an iOS app called "{{APP_NAME}}" that {{APP_DESCRIPTION}}. Design requirements: - Fill the entire canvas edge-to-edge with a uniform {{BACKGROUND_COLOR}} background. No white areas, no transparency, no empty corners. - Do NOT include frames, borders, or decorative edges of any kind. - Center illustration: {{ILLUSTRATION_DESCRIPTION}}, rendered in {{INK_COLOR}} line art style. - Include one {{ACCENT_COLOR}} {{ACCENT_ELEMENT}} placed on the illustration to {{ACCENT_PURPOSE}}. - {{OPTIONAL_ELEMENT}} - Style: elegant, minimal color palette ({{COLOR_PALETTE}}), flat design, no gradients on illustration, no shadows, no 3D effects. - Detailed enough to be interesting but clear at small sizes (29px). Medium-weight lines. - Do NOT add any text, letters, or characters. - Do NOT pre-round corners — perfectly square image. iOS applies rounded corners automatically. - Output: square PNG, 1024x1024 pixels. ```text ## 変数一覧 | 変数 | 説明 | 例 | |------|------|-----| | `APP_NAME` | アプリ名 | IIIF AR | | `APP_DESCRIPTION` | 機能を1文で | places historical maps in AR | | `BACKGROUND_COLOR` | 背景色 | warm cream (#F0E4CC) | | `ILLUSTRATION_DESCRIPTION` | 中央イラストの内容 | a Japanese landscape with mountains | | `INK_COLOR` | イラストの線画色 | dark navy (#1A237E) | | `ACCENT_COLOR` | アクセント色 | blue (#1565C0) | | `ACCENT_ELEMENT` | アクセント要素 | AR pin marker | | `ACCENT_PURPOSE` | アクセントの意図 | suggest spatial placement | | `OPTIONAL_ELEMENT` | 追加要素(不要なら行ごと削除) | a subtle perspective grid | | `COLOR_PALETTE` | 色パレットの説明 | cream + navy + blue accent | ## 使用例 ### 歴史的絵図ARアプリ ```text A flat, square 1024x1024 app icon for an iOS app called "IIIF AR" that places high-resolution cultural heritage images in augmented reality at real-world scale. Design requirements: - Fill the entire canvas edge-to-edge with a uniform warm cream/parchment (#F0E4CC) background. No white areas, no transparency, no empty corners. - Do NOT include frames, borders, or decorative edges of any kind. - Center illustration: a stylized composition showing overlapping cultural heritage imagery — such as a fragment of a traditional Japanese landscape painting with mountains, water, and architecture — rendered in dark navy (#1A237E) line art style. - Include one blue (#1565C0) location/AR pin marker placed on the illustration to suggest spatial placement. - Optionally include a subtle perspective grid (2-3 thin lines) at the bottom to hint at AR floor placement. - Style: elegant, minimal color palette (cream + navy ink + one blue accent), flat design, no gradients on illustration, no shadows, no 3D effects. - Detailed enough to be interesting but clear at small sizes (29px). Medium-weight lines. - Do NOT add any text, letters, or characters. - Do NOT pre-round corners — perfectly square image. iOS applies rounded corners automatically. - Output: square PNG, 1024x1024 pixels. ```text ### くずし字OCRアプリ ```text A flat, square 1024x1024 app icon for an iOS app called "KotenOCR" that recognizes classical Japanese cursive script (kuzushiji) using on-device AI. Design requirements: - Fill the entire canvas edge-to-edge with a uniform warm ivory (#FAF3E8) background. No white areas, no transparency, no empty corners. - Do NOT include frames, borders, or decorative edges of any kind. - Center illustration: a stylized fragment of a classical Japanese manuscript page with elegant brushstroke characters, rendered in dark sumi ink (#2C1810) line art style. - Include one teal (#00897B) scanning/recognition indicator (such as a viewfinder bracket or highlight box) placed over one character to suggest OCR recognition. - Style: elegant, minimal color palette (ivory + sumi ink + one teal accent), flat design, no gradients on illustration, no shadows, no 3D effects. - Detailed enough to be interesting but clear at small sizes (29px). Medium-weight lines. - Do NOT add any text, letters, or characters. - Do NOT pre-round corners — perfectly square image. iOS applies rounded corners automatically. - Output: square PNG, 1024x1024 pixels. ```text ## Apple HIG準拠チェックリスト 生成後に以下を確認してください。 - [ ] 正方形(1024×1024px)で、角丸なし - [ ] 四隅まで背景色で塗りつぶされている(透明や白の隙間なし) - [ ] テキストが含まれていない - [ ] 写真ではなくイラスト/グラフィック - [ ] 29pxに縮小しても何のアプリかわかる - [ ] Apple製品のレプリカが含まれていない - [ ] カスタムのハイライトやドロップシャドウがない ## 適用方法 ```bash # 生成した画像を配置 cp ~/Downloads/generated_icon.png \ {PROJECT}/Assets.xcassets/AppIcon.appiconset/AppIcon.png # ビルドして確認 xcodegen generate xcodebuild build -project {PROJECT}.xcodeproj -scheme {SCHEME} ... ```text ## 参考 - [Apple Human Interface Guidelines - App Icons](https://developer.apple.com/design/human-interface-guidelines/app-icons)

App Store審査リジェクト後の修正・再提出をApp Store Connect APIで実行する

App Store審査リジェクト後の修正・再提出をApp Store Connect APIで実行する

App Store Connect の審査でリジェクトされた後、修正からの再提出までの全工程を API で実行しました。ブラウザでの操作は一切行っていません。 リジェクトの内容 JPS Explorer(ジャパンサーチ文化資源探索アプリ)の初回提出で、2つの問題を指摘されました。 チップ(Tip Jar)画面でエラーが表示される — In-App Purchase の商品が App Store Connect に未登録だったため カメラ検索の「撮影」ボタンでクラッシュ — iOS の Info.plist に NSCameraUsageDescription が未設定だったため 修正内容 カメラクラッシュの修正 Info.plist にカメラと写真ライブラリの権限記述を追加しました。 <key>NSCameraUsageDescription</key> <string>文化資源に似た画像を検索するためにカメラを使用します</string> <key>NSPhotoLibraryUsageDescription</key> <string>文化資源に似た画像を検索するために写真ライブラリを使用します</string> Flutter の image_picker パッケージを使ってカメラにアクセスする場合、この記述がないと実機でクラッシュします。シミュレータではカメラが使えないため、この問題には気づきにくいようです。 合わせて、PlatformException の camera_access_denied と photo_access_denied のハンドリングも追加しました。 チップ画面の修正 In-App Purchase の商品が未登録の場合、StoreKit がエラーを返します。エラーをそのまま表示するのではなく、「準備中です」というメッセージに変更しました。 API による再提出の手順 リジェクト後の状態では、App Store Connect のブラウザに「編集」ボタンと「App Reviewに再提出」ボタンが表示されます。これらの操作はすべて API で実行できます。 Step 1: ビルド番号を上げてアップロード リジェクトされたビルドと同じビルド番号では再アップロードできないため、pubspec.yaml のビルド番号を上げます。 # 変更前 version: 1.0.0+1 # 変更後 version: 1.0.0+2 ビルドしてアップロードします。 ...

Firebase不要:Apple標準ツールだけでiOSアプリを運用する

はじめに iOSアプリの運用を始めると、すぐに以下のような課題に直面します。 クラッシュしているのか、どこで落ちているのか知りたい ダウンロード数やセッション数を把握したい 古いバージョンのユーザーにアップデートを促したい レビューを書いてもらいたい 多くの記事が Firebase Crashlytics + Firebase Analytics の導入を勧めますが、これはGoogleのサーバーにユーザーのデバイス情報・使用状況・クラッシュログなどを送信することを意味します。App Storeへの提出時にも「データ収集・追跡」の申告が必要になります。 個人開発や学術目的のアプリでは、そこまでのデータ収集が本当に必要でしょうか? 本記事では、ユーザーデータを外部に送信せず、Apple標準ツールとOSSだけで運用する構成を、実際のApp Store公開アプリ KotenOCR での経験をもとに紹介します。 構成の全体像 目的 Firebase構成 本記事の構成 外部送信 クラッシュ計測 Crashlytics MetricKit なし アナリティクス Firebase Analytics App Store Connect Analytics API なし アップデート促進 Remote Config Siren(iTunes Lookup API) なし※ レビュー促進 In-App Messaging SKStoreReviewRequest なし クラッシュログ閲覧 Crashlytics Console Xcode Organizer なし ※ Sirenは iTunes Lookup API(Apple公式)にバージョン確認のリクエストを送りますが、ユーザーの個人情報は含まれません。 1. MetricKit — クラッシュ計測 MetricKit はiOS 13+で利用可能なApple標準フレームワークです。クラッシュレポート、ハング(応答なし)、起動時間などのパフォーマンスデータをアプリ内で受け取れます。 実装 import MetricKit class MetricKitManager: NSObject, MXMetricManagerSubscriber { static let shared = MetricKitManager() func start() { MXMetricManager.shared.add(self) } // iOS 15+: クラッシュ・ハングの即時通知 func didReceive(_ payloads: [MXDiagnosticPayload]) { for payload in payloads { if let crashDiagnostics = payload.crashDiagnostics { for crash in crashDiagnostics { // クラッシュのスタックトレースを処理 let description = crash.callStackTree.jsonRepresentation() // ログに記録、またはローカルに保存 } } } } // iOS 14+: 24時間ごとのパフォーマンスメトリクス func didReceive(_ payloads: [MXMetricPayload]) { for payload in payloads { // 起動時間、メモリ使用量、ディスクI/Oなど } } } ```text アプリの起動時に `MetricKitManager.shared.start()` を呼ぶだけです。 ### MetricKitの特徴 - **外部送信なし** — データはアプリ内で完結。Appleのサーバーにも送らない - **即時通知** — iOS 15以降、クラッシュ直後に `didReceive(_: [MXDiagnosticPayload])` が呼ばれる - **バッテリー・パフォーマンス** — クラッシュだけでなく、起動時間やメモリ使用量も計測できる ### Xcode Organizer との違い Xcode Organizer(Xcode → Window → Organizer → Crashes)も追加実装なしでクラッシュログを表示しますが、以下の制限があります。 - ユーザーが「診断データ共有」をオンにしている必要がある(オプトイン) - ある程度のユーザー数がないとデータが集まらない - 蓄積に数日〜数週間かかる ユーザー数が少ない段階では MetricKit の方が実用的です。 ## 2. App Store Connect Analytics API — アナリティクス App Store Connect のWebコンソールでもアナリティクスは見られますが、APIで取得すると自動化や詳細な分析が可能になります。 ### 準備:JWT認証 App Store Connect APIは JWT(JSON Web Token)で認証します。 ```python import jwt import time import requests KEY_ID = "YOUR_KEY_ID" ISSUER_ID = "YOUR_ISSUER_ID" with open("AuthKey.p8", "r") as f: private_key = f.read() payload = { "iss": ISSUER_ID, "iat": int(time.time()), "exp": int(time.time()) + 1200, "aud": "appstoreconnect-v1" } token = jwt.encode(payload, private_key, algorithm="ES256", headers={"kid": KEY_ID}) headers = {"Authorization": f"Bearer {token}"} ```text ### レポートリクエストの作成 Analytics APIを使うには、まずレポートリクエストを作成(または既存のものを取得)します。 ```python APP_ID = "YOUR_APP_ID" # レポートリクエストの確認 r = requests.get( f"https://api.appstoreconnect.apple.com/v1/apps/{APP_ID}/analyticsReportRequests", headers=headers ) report_requests = r.json()["data"] # なければ作成(ONGOINGで継続的にレポート生成) if not report_requests: r = requests.post( f"https://api.appstoreconnect.apple.com/v1/apps/{APP_ID}/analyticsReportRequests", headers=headers, json={ "data": { "type": "analyticsReportRequests", "attributes": {"accessType": "ONGOING"}, "relationships": { "app": {"data": {"type": "apps", "id": APP_ID}} } } } ) ```text ### レポートの取得 ```python import gzip import csv import io from collections import defaultdict REPORT_REQUEST_ID = report_requests[0]["id"] def fetch_report(report_name): """指定したレポートの全インスタンスを取得して結合""" # レポート一覧を取得 r = requests.get( f"https://api.appstoreconnect.apple.com/v1/analyticsReportRequests/" f"{REPORT_REQUEST_ID}/reports", headers=headers, params={"limit": 200} ) for report in r.json()["data"]: if report["attributes"]["name"] == report_name: # インスタンス(日付ごとのデータ)を取得 r2 = requests.get( f"https://api.appstoreconnect.apple.com/v1/analyticsReports/" f"{report['id']}/instances", headers=headers, params={"limit": 10} ) all_data = "" for inst in r2.json().get("data", []): # セグメント(実際のデータファイル)を取得 r3 = requests.get( f"https://api.appstoreconnect.apple.com/v1/" f"analyticsReportInstances/{inst['id']}/segments", headers=headers ) for seg in r3.json().get("data", []): url = seg["attributes"].get("url") if url: r4 = requests.get(url) text = gzip.decompress(r4.content).decode("utf-8") lines = text.strip().split("\n") if not all_data: all_data = text.strip() else: all_data += "\n" + "\n".join(lines[1:]) return all_data return None ```text ### 利用可能なレポートの例 | レポート名 | カテゴリ | 内容 | |-----------|---------|------| | App Downloads Standard | COMMERCE | ダウンロード数(新規/再DL、デバイス、地域別) | | App Sessions Standard | APP_USAGE | セッション数、平均時間 | | App Store Discovery and Engagement Standard | APP_STORE_ENGAGEMENT | インプレッション、ページ閲覧、タップ | | App Store Installation and Deletion Standard | APP_USAGE | インストール・削除数 | | App Crashes | APP_USAGE | クラッシュ数 | ### 実際の出力例 KotenOCRの公開直後のデータです。 ```text === ダウンロード数 === 2026-03-20: 新規 95 / 再DL 2 / 計 97 2026-03-21: 新規 348 / 再DL 14 / 計 362 2026-03-22: 新規 104 / 再DL 188 / 計 292 2026-03-23: 新規 23 / 再DL 231 / 計 254 合計: 新規 570 / 再DL 435 / 計 1,005 === セッション数 === 2026-03-20: 159 セッション / 平均 81秒 2026-03-21: 345 セッション / 平均 85秒 2026-03-22: 126 セッション / 平均 84秒 2026-03-23: 50 セッション / 平均 115秒 === App Store エンゲージメント === 2026-03-19: Impression: 79 / Page view: 14 / Tap: 19 2026-03-20: Impression: 172 / Page view: 315 / Tap: 336 2026-03-21: Impression: 596 / Page view: 576 / Tap: 600 2026-03-22: Impression: 193 / Page view: 106 / Tap: 101 === インストール / 削除 === 2026-03-20: インストール 63 / 削除 0 2026-03-21: インストール 249 / 削除 0 2026-03-22: インストール 66 / 削除 0 2026-03-23: インストール 56 / 削除 0 ```text データの遅延は約1〜2日です。Web UIではほぼ前日分まで閲覧できます。 ## 3. Siren — アップデート促進 [Siren](https://github.com/ArtSabintsev/Siren) は、App Storeの最新バージョンとアプリのバージョンを比較し、更新を促すダイアログを表示するOSSライブラリです。 ### 導入 Swift Package Manager で追加します。 ```text https://github.com/ArtSabintsev/Siren (6.1.0+) ```text ### 実装 ```swift import Siren // SwiftUIの場合、onAppear内で呼ぶこと(init()では動かない) private func configureSiren() { let siren = Siren.shared siren.rulesManager = RulesManager( majorUpdateRules: .critical, // メジャー: スキップ不可 minorUpdateRules: .annoying, // マイナー: スキップ不可(後で通知) patchUpdateRules: .default // パッチ: スキップ可能 ) siren.wail() } ```text ### 注意点 - **SwiftUIでは `onAppear` で呼ぶ** — `App.init()` の段階ではUIWindowが存在しないため、Sirenが正しく動作しません - **テスト方法** — Sirenは iTunes Lookup API で App Store上のバージョンと比較するため、テスト時は Info.plist の `CFBundleShortVersionString` を意図的に古いバージョンに設定する必要があります - **リリース直後の遅延** — デフォルトでリリースから1日経過しないとダイアログが表示されません(`showAlertAfterCurrentVersionHasBeenReleasedForDays` で変更可能) ### ダイアログの種類 | ルール | 動作 | |--------|------| | `.critical` | 「Update」ボタンのみ。スキップ不可 | | `.annoying` | 「Update」「Next time」の2ボタン。スキップ不可だが後回し可 | | `.default` | 「Update」「Next time」「Skip」の3ボタン | | `.relaxed` | `.default` と同じだが、表示頻度が低い | ## 4. SKStoreReviewRequest — レビュー促進 Apple標準のレビュー促進APIです。追加のフレームワークは不要です。 ```swift import StoreKit // OCR成功後など、ユーザーが価値を感じたタイミングで呼ぶ func requestReviewIfAppropriate() { let count = UserDefaults.standard.integer(forKey: "ocrSuccessCount") // 3回目のOCR成功後にレビューを依頼 if count == 3 { if let scene = UIApplication.shared.connectedScenes .first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene { SKStoreReviewController.requestReview(in: scene) } } } ```text ### ポイント - Appleがダイアログの表示を制御するため、呼び出しても必ず表示されるとは限らない - **年3回まで**の制限がある - ユーザーが「成功体験」を感じた直後に呼ぶのが効果的 ## 5. 所感と限界 ### この構成でカバーできること - 基本的なダウンロード数・セッション数の把握 - クラッシュの検知と原因調査 - ユーザーへのアップデート促進 - レビュー促進 実際に KotenOCR をこの構成で運用していますが、個人開発アプリとしては十分な情報が得られています。 ### この構成の限界 - **画面単位・機能単位の詳細な計測ができない** — 「どの画面が最も使われているか」「翻訳機能の利用率は?」といった分析にはアプリ内のイベント計測が必要 - **リアルタイム性がない** — Analytics APIのデータには1〜2日の遅延がある - **MetricKitのクラッシュ情報は構造化されていない** — スタックトレースのJSON解析は自前で実装する必要がある - **A/Bテストや段階的ロールアウトは不可能** — これらが必要な場合はFirebase Remote Configなどが必要 ### Firebaseが必要になるケース - ユーザー行動の詳細な分析が必要な場合 - リアルタイムのクラッシュ通知が必要な場合 - Feature Flagsやリモート設定が必要な場合 - プッシュ通知を実装する場合 ## まとめ | ツール | 用途 | 実装コスト | |--------|------|-----------| | MetricKit | クラッシュ・パフォーマンス計測 | 小 | | App Store Connect Analytics API | DL数・セッション・エンゲージメント | 中(Python) | | Siren | アップデート促進ダイアログ | 小 | | SKStoreReviewRequest | レビュー促進 | 最小 | | Xcode Organizer | クラッシュログ閲覧 | なし | Firebase不要でも、個人開発アプリの運用に必要な情報はほぼ揃います。プライバシーポリシーの記載義務も最小限で済み、ユーザーに対して「データを収集していません」と言い切れるのは大きなメリットです。 まずはこの構成で始めて、必要に応じてFirebaseを追加する、という段階的なアプローチがおすすめです。 ## 関連記事 - [KotenOCR:くずし字をオフラインで認識するiOSアプリの開発と公開](/posts/kotenocr-ios-app/) - [App Store Connect APIだけでiOSアプリを審査提出する手順](/posts/appstore-connect-api-guide/) - [iOSアプリのメモリ最適化とクラッシュ修正の実践記録](/posts/ios-memory-crash-fixes/)

TEI XMLのスタンドオフ注釈をインライン化する際の落とし穴とDOM操作による解決

デジタル延喜式は、延長5年(927年)に完成した律令の施行細則集『延喜式』を TEI (Text Encoding Initiative) XML で符号化し、Web上で閲覧・検索できるようにするプロジェクトです。国立歴史民俗博物館を中心に、校訂文・現代語訳・英訳を TEI でマークアップし、Nuxt.js(Vue.js)ベースのビューアで公開しています。 この開発の中で、TEI XML のスタンドオフ(standoff)注釈をインライン注釈に変換する処理において、XML の文書構造が崩壊するバグに遭遇しました。本記事では、その原因と DOM 操作ベースの解決策を記録します。 スタンドオフ注釈とは TEI XML では、テキスト中の校異(variant readings)を記録する方法として、スタンドオフ方式がよく使われます。デジタル延喜式では、複数の写本間のテキストの異同を <app> 要素で記録しており、テキスト中に <anchor> 要素で範囲を示し、対応する <app> 要素を別の場所に置く構造になっています。 <p> 前テキスト <anchor xml:id="app001"/> 校異対象のテキスト <anchor xml:id="app001e"/> 後テキスト </p> <!-- 別の場所に校異情報 --> <app from="#app001" to="#app001e"> <lem>校異対象のテキスト</lem> <rdg wit="#写本A">異なるテキスト</rdg> </app> この方式は、XML のネスト制約を回避できる利点があります。校異の範囲が要素境界をまたぐ場合(overlapping hierarchy)でも、anchor はどこにでも置けるためです。 インライン化の理由 XML ツリーと UI コンポーネントツリーの対応 デジタル延喜式のビューアは Vue.js で構築しています。Vue.js や React のようなコンポーネントベースのフレームワークでは、UI はツリー構造で記述されます。TEI XML もツリー構造なので、XML の各要素を UI コンポーネントに 1:1 でマッピングする再帰レンダリングが自然なアプローチになります。 <!-- TEI.vue: XML要素を再帰的にコンポーネントにマッピング --> <template> <component v-for="child in element.children" :is="getComponent(child.tagName)" :element="child" /> </template> この設計では、<app> 要素がテキスト中にインラインで存在すれば、ツリーの走査だけでレンダリングできます。 ...

YouTube StudioでVTT字幕ファイルをアップロードする手順

YouTube StudioでVTT字幕ファイルをアップロードする手順

YouTube StudioでVTT字幕ファイルをアップロードする際、UIがわかりにくく手順に迷ったので整理しました。 前提 VTTファイル(タイムコード付き)が手元にある YouTube Studioの動画編集画面にアクセスできる Step 1: 動画の言語を設定する 左メニューから「言語」を開くと、初回は言語が未設定の状態です。 「言語を設定」のプルダウンから「日本語」を選択し、「確認」をクリックします。 確認後、言語設定画面が切り替わり、「字幕を編集」「手動でアップロード」ボタンと「翻訳」セクションが表示されます。 Step 2: 元言語(日本語)の字幕をアップロードする 上部の「手動でアップロード」をクリックすると、ファイル形式の選択ダイアログが表示されます。 「タイムコードあり」を選択して「続行」をクリックし、ja.vtt ファイルを選択します。 アップロードだけでは公開されない ここが注意点です。VTTファイルをアップロードしただけでは字幕は公開されません。 アップロード後、以下の追加操作が必要です。 「字幕を編集」をクリックしてエディタを開く 内容を確認する 右上の「公開」ボタンをクリックする 「公開」を押すまでは下書き状態のままで、視聴者には表示されません。タイムコード付きの完成データをアップロードしているので、そのまま公開されても良さそうですが、YouTube Studioでは手動での確認・公開が必須のようです。 Step 3: 翻訳言語(英語)の字幕をアップロードする 翻訳セクションの右上にある「言語を追加」をクリックすると、言語一覧が表示されます。 「英語」を選択すると、英語の字幕追加画面が開きます。「ファイルをアップロード」を選択し、「タイムコードあり」を選んで en.vtt をアップロードします。 アップロード後、エディタ画面が表示されます。 右上の「公開」をクリックして完了です。翻訳言語の場合も、アップロード後に公開操作が必要です。 元言語と翻訳言語の操作の違い 操作 元言語(日本語) 翻訳言語(英語) アップロード場所 上部「手動でアップロード」 翻訳テーブルの該当言語行 公開操作 「字幕を編集」→ 確認 →「公開」 エディタ画面で「公開」 UIの導線が元言語と翻訳言語で異なるため、混乱しやすいです。 URLパラメータによる字幕の制御 YouTubeのURLに cc_load_policy=1 を付けると、字幕をデフォルトでONにできます。 https://www.youtube.com/watch?v=VIDEO_ID&cc_load_policy=1 cc_lang_pref による言語指定 cc_lang_pref=en のように表示する字幕言語を指定するパラメータも存在します。YouTube IFrame Player APIに記載されています。 https://www.youtube.com/watch?v=VIDEO_ID&cc_load_policy=1&cc_lang_pref=en ただし、このパラメータは通常のwatch URLでは安定して動作しません。実際に試したところ、同じURLでもあるときは英語のみ、別のときは日本語のみが表示されるなど、再現性がありませんでした。 Google Issue Trackerにもバグとして報告されています。 ...

ジャパンサーチAPIを活用した文化資源探索アプリの開発とApp Store公開

ジャパンサーチAPIを活用した文化資源探索アプリの開発とApp Store公開

ジャパンサーチ( https://jpsearch.go.jp )のWeb APIを使い、日本の文化資源を探索するiOS/Androidアプリ「JPS Explorer」を開発しました。API調査からアプリ実装、App Storeリリースの自動化までの過程を記録します。 ジャパンサーチのAPI ジャパンサーチは国立国会図書館が運営する、3,200万件以上のデジタル文化資源のメタデータを横断検索できるサービスです。簡易Web APIが公開されており、以下のような検索が可能です。 パラメータ 機能 keyword キーワード検索 text2image テキストでモチーフを指定して画像検索 image 既存アイテムIDで類似画像検索 g-coordinates 緯度・経度・半径で場所検索 r-tempo 年代範囲で時代検索 API調査で気づいた点 座標フィールドのキー名 位置情報検索(g-coordinates)のレスポンスで座標データは common.coordinates に格納されています。経度のキーは lon で、lng や longitude ではありません。 "coordinates": { "lat": 35.669, "lon": 139.764 } ギャラリーAPIの多言語フィールド ギャラリー検索(/api/curation/search)のレスポンスでは、title と summary が文字列ではなくオブジェクトになっています。 "title": {"ja": "耳鳥斎", "en": "Jichosai"}, "image": {"url": "https://...", "thumbnailUrl": "https://..."} 単純に .toString() すると {ja: 耳鳥斎, en: Jichosai} のような文字列がUIに表示されてしまうため、言語キーでアクセスする必要があります。 ギャラリー詳細のアイテム構造 ギャラリー詳細(/api/curation/{id})のアイテムは contents ではなく parts 配列にネストされています。type: "jps-curation-list-item" を再帰的に探索してIDを収集する必要がありました。一部のギャラリーでは subPages にもアイテムが含まれているようです。 画像アップロードによる類似検索 公式APIガイドには記載されていませんが、Web UIのネットワーク通信を調査したところ、画像アップロードによる類似検索が3段階のAPIで実現されていることがわかりました。 POST /dl/api/imagefeatures/ で画像のBase64データから64次元の特徴量ベクトルを取得 POST /api/item/create-image-feature で特徴量から一時的な検索IDを生成 GET /api/item/search/jps-cross?image={ID} で通常の類似検索を実行 Step 2では X-Requested-With: XmlHttpRequest ヘッダーが必要で、レスポンスはプレーンテキストでIDが返ります。 ...

iOS OCRアプリのメモリ起因クラッシュの調査と対策

KotenOCRは古典籍のOCR処理にONNX Runtimeを使用しているiOSアプリです。6つのONNXモデル(合計約230MB)を搭載しています。ダウンロード数が300に達した時点で、クラッシュ率が6.7%(20件)に上昇していることが確認されました。Xcode Organizerにクラッシュログが表示されなかったため、原因の特定には別のアプローチが必要でした。 調査体制 4つの調査方針を並行して進めました。 メモリ・モデルサイズの調査 画像処理パイプラインの調査 ONNX Runtimeのスレッドセーフティの調査 カメラ・UIライフサイクルの調査 原因の特定 調査の結果、以下の4つの問題が確認されました。影響度の大きい順に記載します。 1. 起動時の全モデル一括ロード 6つのONNXモデルをアプリ起動時にすべてメモリに展開していました。ディスク上では約230MBですが、ONNX Runtimeがモデルをメモリに展開すると1.5〜2.5倍程度に膨張するため、RAM上では推定350〜550MB程度を占有していたと考えられます。 iOSにはjetsam(カーネルレベルのメモリ監視機構)があり、アプリ単位のメモリ上限を超えるとプロセスが強制終了されます。デバイスごとのRAMとアプリあたりのメモリ上限の目安は以下のとおりです。 デバイス RAM リスク iPhone 8 2GB CRITICAL — iOS 16対応機種で最もメモリが少ない iPhone X / XR / XS 3GB HIGH iPhone 11 4GB MEDIUM iPhone 12以降 4〜6GB LOW iPhone 8やiPhone Xでは、アプリあたりのメモリ上限が200〜300MB程度とされており、全モデル一括ロードだけで上限を超過する状況でした。 2. DEIMDetectorの画像前処理 DEIMDetectorが入力画像をリサイズせずにフルサイズのまま正方形パディング用のCGContextを生成していました。12MPの写真(4032×3024)の場合、4032×4032ピクセルのCGContextが作成され、これだけで約63MBのメモリを消費します。 3. 無制限の並列認識タスク withThrowingTaskGroupで最大100件の認識タスクを同時に実行可能な状態でした。各タスクがモデル推論時にメモリを確保するため、並列数に比例してメモリ使用量が増加します。 4. メモリ警告のハンドリング不在 UIApplication.didReceiveMemoryWarningNotificationを購読しておらず、iOSからのメモリ警告を受け取っても何も対処していませんでした。jetsam発動前の最後の解放機会を逃していたことになります。 実施した修正 Lazy Model Loading 起動時にすべてのモデルをロードする方式から、選択中のモードに必要なモデルのみをロードする方式に変更しました。初期メモリ使用量が約350MBから約150MBに減少しました。 func loadModelsForMode(_ mode: OCRMode) { // 選択中のモードに必要なモデルのみロード let requiredModels = mode.requiredModelKeys for key in requiredModels { if sessionCache[key] == nil { sessionCache[key] = try? createSession(for: key) } } } 画像前処理の改善 DEIMDetectorの前処理で、パディング前にリサイズを行うように変更しました。メモリ使用量が63MBから約2.4MBに削減されました。 ...

KotenOCR: 近代OCRの検出重複を解消する(NMS追加とクラスフィルタリング)

はじめに KotenOCRは、国立国会図書館(NDL)が公開したOCRモデルをiOS上で動作させ、くずし字や近代活字をオフラインで認識するアプリです。 近代OCRモード(NDLモード)では、NDLのDEIMv2-Sモデルでレイアウト検出を行い、PARSeQで文字認識を行います。しかし、iOS実装の検出結果が本家のndlocr-liteと比べて明らかに多く、テキストが重複して認識されるという問題がありました。 本記事では、原因の調査から修正までの過程を記録します。 症状:検出数が多すぎる テスト画像として、NDLデジタルコレクションから取得した「校異源氏物語」の序文ページを使いました。 本家ndlocr-liteでは17件の行検出(line_main、line_captionなど)が得られるのに対し、iOS実装では28件の検出が返り、OCR結果にも重複テキストや文字化けが混入していました。 検出数が多い原因は大きく3つありました。 原因1:NMS(Non-Maximum Suppression)が未実装だった DEIMv2モデルのONNX出力は、内部でNMSを完了した結果を返すと想定していました。しかし本家ndlocr-liteのコードを読むと、モデル推論後に追加のNMS(IoU閾値=0.2)を適用していることがわかりました。 つまり、モデル出力にはまだ重複する矩形が含まれており、後処理でそれを除去する必要があったのです。 修正:NMSの追加 DEIMDetector.swiftにNMS処理を追加しました。 // DEIMDetector.swift — postprocess() の末尾 // Sort by score descending let sorted = detections.sorted { $0.score > $1.score } // Apply NMS to remove overlapping detections let nmsResult = applyNMS(sorted, iouThreshold: iouThreshold) return Array(nmsResult.prefix(maxDetections)) NMS本体の実装は以下のとおりです。スコア降順にソートされた検出結果を走査し、既に採用した矩形とのIoUが閾値を超える場合は除外します。 // DEIMDetector.swift /// Apply Non-Maximum Suppression to remove overlapping detections. private func applyNMS(_ detections: [Detection], iouThreshold: Float) -> [Detection] { var kept: [Detection] = [] for det in detections { var shouldKeep = true for existing in kept { if computeIoU(det.box, existing.box) > iouThreshold { shouldKeep = false break } } if shouldKeep { kept.append(det) } } return kept } /// Compute Intersection over Union between two boxes [x1, y1, x2, y2]. private func computeIoU(_ a: [Int], _ b: [Int]) -> Float { let x1 = max(a[0], b[0]) let y1 = max(a[1], b[1]) let x2 = min(a[2], b[2]) let y2 = min(a[3], b[3]) let interW = max(0, x2 - x1) let interH = max(0, y2 - y1) let interArea = Float(interW * interH) let areaA = Float((a[2] - a[0]) * (a[3] - a[1])) let areaB = Float((b[2] - b[0]) * (b[3] - b[1])) let unionArea = areaA + areaB - interArea guard unionArea > 0 else { return 0 } return interArea / unionArea } 原因2:全クラスをOCRに送っていた DEIMv2モデルは17クラスのレイアウト要素を検出します。 ...

Universal Viewer 4.xの「Unknown content type」エラーとローカルホスティングによる対処

Universal Viewer 4.xの「Unknown content type」エラーとローカルホスティングによる対処

発生した問題 Nuxt 3プロジェクトでIIIFマニフェストの画像を表示するために、Universal Viewer(UV)をiframeで埋め込んでいました。従来は外部の https://universalviewer.io/uv.html を参照していましたが、ある時点からビューアが表示されなくなりました。 ブラウザのコンソールには以下のエラーが出力されます。 SES Removing unpermitted intrinsics UV.js:2 Unknown content type 原因の調査 universalviewer.ioのリダイレクト まず確認したところ、universalviewer.io は universalviewer.dev にリダイレクトされるようになっていました。 <meta http-equiv="refresh" content="0; url=https://universalviewer.dev/uv.html"> リダイレクト先の universalviewer.dev/uv.html でも同じ「Unknown content type」エラーが発生します。公式サイト自体で問題が再現する状態でした。 埋め込み用HTMLの初期化方式の違い UV 4.xには2つのHTMLファイルが同梱されています。 uv.html:iframe埋め込み用。IIIFURLAdapter(true)(embeddedモード)で初期化 index.html:デモページ。IIIFURLAdapter()(通常モード)で初期化し、iiifManifestIdを明示的に渡す uv.htmlの埋め込みモードでは、IIIF Presentation API 2.0のマニフェストを読み込んだ際にコンテンツタイプの判定に失敗し、「Unknown content type」が発生するようです。 一方、デモページ(index.html)と同じ初期化方式を使うと正常に動作します。Netlifyにデプロイされた uv-v4.netlify.app で確認できました。 https://uv-v4.netlify.app/#?manifest=https://kokusho.nijl.ac.jp/biblio/200017711/manifest&cv=80 URLパラメータの形式 もうひとつの違いは、URLパラメータの渡し方です。 uv.html(埋め込み用):?manifest=...#?cv=...(クエリパラメータ+ハッシュ) index.html(デモ用):#?manifest=...&cv=...(ハッシュパラメータのみ) 動作する方式はハッシュパラメータのみで完結する形式でした。 対処方法 1. UV 4.2.1をローカルに配置 npmパッケージからUV 4.2.1の必要ファイルを public/uv/ に配置しました。 npm pack universalviewer@4.2.1 tar xzf universalviewer-4.2.1.tgz 最終的に必要なファイルは以下の4点です。 public/uv/ ├── umd/ # UV本体 + チャンクJS(約190ファイル) ├── uv.css # スタイルシート ├── uv.html # 埋め込みページ(カスタム版) └── uv-iiif-config.json # IIIF設定 cjs/、esm/、デモ用のindex.htmlやコレクションJSONは不要です。 ...

YouTube Studioで英語字幕(VTTファイル)をアップロードする手順

YouTube Studioで英語字幕(VTTファイル)をアップロードする手順

YouTube Studioで日本語字幕のアップロードが完了している状態から、英語字幕(en.vtt)を追加する手順をまとめます。 関連記事:Claude Codeを使って動画に多言語字幕を自動生成し、IIIF v3マニフェストで公開する 前提 YouTube Studioにログイン済み 対象動画の日本語字幕(ja.vtt)はアップロード済み 英語字幕ファイル(en.vtt)が手元にある 手順 1. 字幕の管理画面を開く YouTube Studio → 対象の動画を選択 → 左メニューの「字幕」をクリックします。 日本語(動画の言語)が「公開済み」になっていることを確認します。 2. 「言語を追加」をクリック 「言語を追加」ボタンをクリックし、言語一覧から「英語」を選択します。 「英語(アメリカ合衆国)」「英語(イギリス)」など複数の候補が表示されますが、通常は「英語」を選択すれば問題ありません。 3. 英語の行の「追加」をクリック 字幕一覧に「英語」の行が追加されます。この行にある「追加」ボタンをクリックします。 4. 「ファイルをアップロード」を選択 字幕を追加する方法の選択画面が表示されます。「ファイルをアップロード」を選択します。 5. 「タイムコードあり」を選択 VTTファイルにはタイムコード(タイムスタンプ)が含まれているため、「タイムコードあり」を選択して「続行」をクリックします。 6. en.vttファイルを選択してアップロード ファイル選択ダイアログが表示されるので、en.vtt を選択します。 アップロードが完了すると、字幕のプレビューが表示されます。内容を確認し、右上の「公開」をクリックします。 7. 完了確認 YouTube動画で、字幕メニュー(CCボタンまたは歯車アイコン → 字幕)から「日本語」と「英語」を切り替えられることを確認します。 補足 VTTファイルの中身はテキストエディタで確認できます。タイムスタンプと字幕テキストが交互に記述されています 字幕を修正したい場合は、YouTube Studio上でテキストを直接編集するか、修正したVTTファイルを再アップロードします 複数の動画がある場合は、各動画に同じ手順で英語字幕を追加します

Apple Sales Reports APIのデータ反映時刻とYouTube APIのクォータリセットを実測した

外部APIを使って日次のデータ取得を自動化する場合、「いつデータが利用可能になるか」「いつクォータがリセットされるか」を把握しておくとスケジューリングに役立ちます。この記事では、Apple App Store Connect Sales Reports APIとYouTube Data API v3について、実際に観測したタイミングを記録します。 Apple App Store Connect Sales Reports API 公式ドキュメントの記載 Appleの公式ドキュメントでは、日次の売上レポートは翌日の太平洋時間 午前8時まで(by 8:00 AM Pacific Time)に利用可能になるとされています。 参照: Sales and Trends Reports Availability - Apple Developer 実測結果 2026年3月22日に、前日(2026年3月21日)分のデータがいつ取得可能になるかを1時間ごとに確認しました。 16:00 JST (0:00 PT) → 取得不可 17:00 JST (1:00 PT) → 取得不可 18:00 JST (2:00 PT) → 取得不可 19:00 JST (3:00 PT) → 取得不可 20:00 JST (4:00 PT) → 取得不可 21:09 JST (5:09 PT) → 取得可能 今回の計測では、太平洋時間の午前5時頃(日本時間 21時頃)にデータが利用可能になっていました。公式に記載されている午前8時よりも約3時間早いタイミングです。 ...

Claude Codeを使って動画に多言語字幕を自動生成し、IIIF v3マニフェストで公開する

Claude Codeを使って動画に多言語字幕を自動生成し、IIIF v3マニフェストで公開する

動画コンテンツに字幕をつける作業は手間がかかります。本記事では、Claude Code(CLI版Claude)を使い、動画のフレーム分析から多言語字幕(VTT)の生成、IIIF v3マニフェストの作成までを効率的に行う方法を紹介します。 実際のプロジェクトについてはこちらの記事をご覧ください。 全体の流れ 1. 動画ファイル(mp4)を用意する 2. ffmpegでシーンチェンジを検出 3. シーンチェンジポイントのフレームを抽出 4. Claude Codeでフレーム画像を読み取り、内容を把握 5. シーンチェンジのタイムスタンプに基づいてVTTファイルを作成 6. 英語字幕も同様に作成 7. IIIF v3マニフェストを作成 8. HTMLプレーヤーで動画・字幕・読み上げを同期 前提条件 Claude Code(CLI版) ffmpeg / ffprobe 字幕をつけたい動画ファイル(mp4) # macOSの場合 brew install ffmpeg Step 1: シーンチェンジの検出 動画の画面が切り替わるタイミングを自動検出します。これが字幕のタイムスタンプの基準になります。 ffmpeg -i "video.mp4" \ -vf "select='gt(scene,0.15)',showinfo" \ -vsync vfr -f null - 2>&1 \ | grep "pts_time" \ | sed 's/.*pts_time:\([0-9.]*\).*/\1/' 出力例: 3.033333 8.066667 20.066667 25.066667 32.100000 ... なぜシーンチェンジ検出が重要か 最初は3秒間隔でフレームを抽出していましたが、実際の画面切り替えとずれが生じました。シーンチェンジ検出を使うことで、実際に画面が変わるタイミングに基づいた正確な字幕タイミングが得られます。 Step 2: シーンチェンジポイントのフレーム抽出 mkdir -p scenes ffmpeg -i "video.mp4" \ -vf "select='gt(scene,0.15)'" \ -vsync vfr -q:v 2 \ scenes/scene_%03d.jpg Step 3: Claude Codeでフレーム画像の内容を読み取る Claude Codeのマルチモーダル機能で、抽出したフレーム画像の内容を読み取ります。 ...

デジタル源氏物語 動画字幕プロジェクト:IIIF v3マニフェストによる多言語字幕の公開

デジタル源氏物語 動画字幕プロジェクト:IIIF v3マニフェストによる多言語字幕の公開

デジタル源氏物語の機能紹介動画に、日本語・英語の二言語字幕を付与し、IIIF v3マニフェストとして公開するプロジェクトを作成しました。 Demo: https://nakamura196.github.io/genji-movie/ GitHub: https://github.com/nakamura196/genji-movie 対象動画 以下の3本の機能紹介動画に字幕を付与しています。 動画 時間 内容 画像とテキストを一緒にみる 2:42 TEI & IIIFを活用したParallel Text Viewerの使い方 AI画像検索(改訂版) 4:19 くずし字OCRと類似度計算による写本画像の横断検索 パタパタ顔比較 1:38 vdiff.jsによる源氏百人一首の挿絵比較ツール 多言語字幕 各動画に日本語・英語のWebVTT字幕ファイルを用意しています。字幕は1文単位で分割されており、読みやすさを重視しています。 VTTファイルはそのままYouTubeの字幕としてもアップロードできます。 IIIF v3マニフェスト 各動画にIIIF Presentation API 3.0準拠のマニフェストファイルを作成しています。動画はpaintingのAnnotation、字幕はsupplementingのAnnotationとして記述しています。 { "annotations": [{ "type": "AnnotationPage", "items": [ { "type": "Annotation", "motivation": "supplementing", "label": { "ja": ["日本語"] }, "body": { "id": "https://example.com/ja.vtt", "type": "Text", "format": "text/vtt", "language": "ja" } }, { "type": "Annotation", "motivation": "supplementing", "label": { "en": ["English"] }, "body": { "id": "https://example.com/en.vtt", "type": "Text", "format": "text/vtt", "language": "en" } } ] }] } 複数のIIIF対応ビューアで表示可能 IIIF v3マニフェストを採用しているため、以下のIIIF対応ビューアで直接表示できます。 ビューア 動画再生 字幕表示 言語切替 備考 Player(本プロジェクト) ○ ○ ○ Web Speech APIによる読み上げ機能付き RAMP ○ ○ ○ AV資料に最も強い Theseus ○ ○ ○ IIIF対応の汎用ビューア Clover ○ ○ △ Samvera/Northwestern開発 Universal Viewer ○ △ - v4でAV対応改善 各ビューアへのリンクは、デモページの各動画カードに用意されています。 ...

DH(デジタル人文学)ツール情報の自動収集・記事生成システムの構築

DH(デジタル人文学)ツール情報の自動収集・記事生成システムの構築

DH分野のツール情報を追いかける デジタル人文学(DH)の分野では、OCR、IIIF、テキスト翻刻といった領域で新しいツールが継続的に開発されています。NDL(国立国会図書館)の ndl-lab や CODH(人文学オープンデータ共同利用センター)などの機関がGitHub上でツールを公開しており、研究者個人による開発も活発です。 こうした情報を体系的に収集し、カレントアウェアネスのように定期的にまとめる仕組みが欲しいと考え、自動収集・記事生成のシステムを構築しました。 収集対象 情報源は3種類です。 X (Twitter) では、DH分野で活発にツールを開発・公開している研究者や機関のアカウントを対象にしています。 RSSフィードでは、カレントアウェアネス・ポータル(国立国会図書館)の https://current.ndl.go.jp/rss.xml を取得しています。 GitHub では、DH関連のツールを公開している組織・個人の公開リポジトリの更新情報を GitHub API 経由で取得しています。 方法の検討 X投稿の取得 X の投稿取得にはいくつかの方法を検討しました。 方法 費用 結果 X API (Basic) $100/月 確実だが高コストのため不採用 Web検索(site:x.com/xxx) 無料 インデックスされたごく一部しか取れず不採用 RSSHub(セルフホスト) 無料 Xの内部API経由で全ツイート取得可能。Docker運用が必要で、GitHub Actions内での一時起動も検討したが、Playwright案のほうがシンプルなため不採用 Playwright(ログインなし) 無料 一部アカウントで0件。ログインウォールに阻まれ不採用 Playwright(Cookie認証) 無料 全アカウントで取得成功。毎日の取得であれば取りこぼしも少ない。採用 最終的に、Playwright で Cookie 認証を使う方法を採用しました。DevTools から auth_token と ct0 を手動で取得し、.x_cookies.json に保存する形です。GitHub Actions 上では Secret TWITTER_COOKIE から環境変数として渡しています。 Playwright による自動ログインも試みましたが、X のボット検出で失敗したため、手動取得に落ち着きました。Cookie は数ヶ月で期限切れになるため、定期的な更新が必要です。 AI要約・記事生成 方法 費用 結果 Claude Code 内で手動実行 プラン内 自動化できないためテスト用のみ Claude API (Anthropic直接) 従量課金 動作するが独自のAPI Key管理が必要で不採用 OpenRouter 従量課金 複数モデル選択可能、1回$0.05程度で採用 RSSフィード カレントアウェアネス・ポータルでは /feed エンドポイントがアイテム0件で、/rss.xml に30件のアイテムがあることを確認し、後者を使用しています。CODH のサイトはメンテナンス中でRSS取得ができなかったため、X 投稿で代替しています。 ...

DH週間トピックス — 2026年3月第4週

DH週間トピックス — 2026年3月第4週

デジタル人文学(DH)関連の新規ツール開発・公開情報を週次でまとめています。 NDL OCR-Lite のモバイル対応・段組み認識対応 国立国会図書館が開発した古典籍OCRエンジン「NDL OCR-Lite」のWebアプリケーション版がモバイル対応および段組み認識機能に対応しました。iPhone、iPad、Android端末でカメラや写真ライブラリから画像を選択してOCR処理が可能になったとのことです。また、段組みレイアウトの認識にも対応し、複雑な文書構造の処理精度が向上したようです。 NDL OCR-Lite Web版 @blue0620の投稿およびGitHub更新情報より 軽量AIモデルQwen 3.5 9Bの数学問題解答性能 @blue0620による投稿で、軽量AIモデル「Qwen 3.5 9B」をローカルサーバで動作させたところ、東京大学の数学問題を解答できたとの報告がありました。軽量モデルでありながら高い性能を示しており、今後の自治体等での活用可能性が示唆されています。 @blue0620の投稿より 筑摩書房版芥川龍之介全集専用OCRスクリプト NDL OCR-Liteを活用した、筑摩書房版芥川龍之介全集の特殊なレイアウト(本文2段・脚注1段)に対応した専用OCRスクリプトが開発されました。本文と脚注を分離して作品ごとに整理する機能を持つとのことです。 @tolle_et_legeの投稿より 本記事は X投稿・GitHub更新・カレントアウェアネス・ポータルから自動収集した情報を基に生成しています。

Swift ConcurrencyでOCR認識処理を並列化し最大6.7倍高速化する

Swift ConcurrencyでOCR認識処理を並列化し最大6.7倍高速化する

OCRパイプラインの構造 iOSでONNX Runtimeを使ったOCRパイプラインは、大まかに以下の流れで動作します。 画像全体に対する文字領域検出(Detection) 検出された各領域に対する文字認識(Recognition) 読み順の推定と結合 この中で、検出は画像全体に対して1回だけ実行されます。一方、認識は検出された領域の数だけ繰り返されます。領域数が多い画像では、認識処理が全体の処理時間の大部分を占めることになります。 逐次処理の問題 認識処理を単純なforループで逐次実行すると、処理時間は領域数にほぼ比例します。 for det in detections { let rect = CGRect(x: max(0, det.box[0]), y: max(0, det.box[1]), width: max(1, det.box[2] - det.box[0]), height: max(1, det.box[3] - det.box[1])) if let cropped = image.cropping(to: rect) { _ = try recognizer.recognize(image: cropped) } } たとえば98領域が検出された画像の場合、1領域あたり約80msとすると合計で約8秒かかります。 withThrowingTaskGroupによる並列化 Swift ConcurrencyのwithThrowingTaskGroupを使うと、各領域の認識処理を並列に実行できます。 let results = try await withThrowingTaskGroup(of: (Int, String).self) { group in for i in 0..<detections.count { let box = detections[i].box let cropRect = CGRect( x: max(0, box[0]), y: max(0, box[1]), width: max(1, box[2] - box[0]), height: max(1, box[3] - box[1]) ) guard let cropped = image.cropping(to: cropRect) else { continue } group.addTask { let text = try recognizer.recognize(image: cropped) return (i, text) } } var texts: [(Int, String)] = [] for try await result in group { texts.append(result) } return texts } for (i, text) in results { recognized[i].text = text } 各タスクはインデックスとともに結果を返すため、完了順序に関係なく元の順序を保持できます。 ベンチマーク結果 iOSシミュレータ上で、逐次処理と並列処理の実行時間を比較しました。 古典籍(源氏物語、6642x4990px、21領域) 出典: 東京大学デジタルアーカイブ 源氏物語 方式 処理時間 逐次 4.55s 並列 3.24s 高速化率 1.4x 近代活字(NDLデジタルコレクション、6890x4706px、98領域) 出典: 国立国会図書館デジタルコレクション 方式 処理時間 逐次 7.59s 並列 1.13s 高速化率 6.7x 高速化率が異なる理由 21領域の場合と98領域の場合で高速化率に差が出ています。 ...

ジャパンサーチの類似画像検索APIの内部構造

ジャパンサーチの類似画像検索APIの内部構造

ジャパンサーチ(https://jpsearch.go.jp)の「画像AI検索」機能は、テキストによるモチーフ検索と、画像をアップロードしての類似画像検索を提供しています。公式の簡易Web APIガイドにはテキスト検索(text2image)と既存アイテムIDによる類似検索(imageパラメータ)の説明がありますが、画像アップロードによる検索については記載がありません。 Web UIのネットワーク通信を調査したところ、画像アップロード検索は3段階のAPIで実現されているようです。 APIの3段階フロー Step 1: 画像から特徴量ベクトルを抽出 エンドポイント: POST https://jpsearch.go.jp/dl/api/imagefeatures/ 画像をBase64エンコードして送信すると、64次元の特徴量ベクトルが返ります。 リクエスト: { "img_b64": "data:image/jpeg;base64,/9j/4AAQ..." } レスポンス: { "body": [-0.0879, -0.0091, -0.0712, ...(64次元)] } Step 2: 特徴量ベクトルから一時的な検索IDを生成 エンドポイント: POST https://jpsearch.go.jp/api/item/create-image-feature Step 1で取得した特徴量ベクトル(配列)をそのままPOSTすると、一時的なIDが文字列で返ります。 リクエスト: [-0.0879, -0.0091, -0.0712, ...] レスポンス(プレーンテキスト): jpsxt-Wnl3p6dJJ8b このリクエストには X-Requested-With: XmlHttpRequest ヘッダーが必要です。 Step 3: IDで類似画像検索を実行 エンドポイント: GET https://jpsearch.go.jp/api/item/search/jps-cross?image={ID}&size=20 Step 2で取得したIDを、通常の類似画像検索と同じ image パラメータに渡します。このIDはJPSの既存アイテムIDと同じ形式で扱われるため、通常の検索APIがそのまま使えます。 レスポンスは通常のアイテム検索と同じJSON形式です。 検証結果 アプリのアイコン画像(虫眼鏡のイラスト)で検索したところ、1,932件がヒットし、民博の「肩掛袋」「上衣」「腰帯」などテキスタイル系の資料が上位に返りました。色味や形状の特徴で類似度が計算されていると考えられます。 ヘッダーの要件 調査の過程で、いくつかのヘッダーが必要であることがわかりました。 ヘッダー Step 1 Step 2 Step 3 Content-Type: application/json 必要 必要 不要 Origin: https://jpsearch.go.jp 必要 必要 不要 X-Requested-With: XmlHttpRequest 不要 必要 不要 Step 1とStep 2はPOST、Step 3はGETです。 ...

KotenOCR:くずし字をオフラインで認識するiOSアプリの開発と公開

KotenOCR:くずし字をオフラインで認識するiOSアプリの開発と公開

はじめに 古典籍に書かれたくずし字(変体仮名・草書体の漢字)を読むのは、専門家でも容易ではありません。近年はAI-OCRによって機械的な認識が可能になってきましたが、調査した限り、スマートフォンでオフライン利用できるツールは見当たりませんでした。 KotenOCRは、国立国会図書館(NDL)が公開した軽量くずし字OCRモデル「NDL古典籍OCR-Lite」をiOS上で動作させ、写真を撮るだけでくずし字を認識できるアプリです。 App Store(無料): https://apps.apple.com/jp/app/kotenocr/id6760045646 背景:既存ツールの状況 NDLが「NDL古典籍OCR-Lite」を公開したことで、くずし字OCRの敷居は下がりました。既存ツールを見渡すと以下のような状況でした。 ツール 形態 インターネット接続 NDL古典籍OCR-Lite デスクトップ / Web / CLI 不要(デスクトップ版) miwo(CODH) モバイルアプリ 必要 古文書カメラ(TOPPAN) モバイルアプリ 必要 モバイルアプリは存在するものの、いずれもクラウド通信が必要です。一方、NDL古典籍OCR-LiteはPC環境でしか動作しません。 そこで、NDL古典籍OCR-Liteのモデルをスマートフォンに載せて、オフラインで動くiOSアプリを作ることにしました。 KotenOCRの特徴 完全オフライン — すべての処理がデバイス上で完結。通信不要 iPhone / iPad対応 — iOS 16.0以上 無料 — App Storeから無料でダウンロード可能 スキャン履歴 — 認識結果を保存・管理 TXT / PDFエクスポート — 認識テキストをファイルとして出力 範囲トリミング — 認識する領域を指定可能 使い方 古典籍の写真を撮影する(またはライブラリから選択) AIがくずし字を自動認識 認識されたテキストをコピー・エクスポート OCRパイプライン 写真からテキストが認識されるまでの処理フローは以下の通りです。 写真 → トリミング → テキスト領域検出 → 文字認識 → 読み順決定 → 表示 テキスト領域検出: RTMDetモデルにより、画像内の文字領域を検出 文字認識: PARSeqモデルにより、検出領域内の文字を認識(7,141文字、NDLmojiの文字集合に対応) 読み順: 日本語の縦書き・右から左への読み順を考慮して並べ替え ...

Yahoo News記事のローカル保存手段の比較(SingleFile・Playwright・ArchiveBox・WARC・yt-dlp)

Yahoo News記事のローカル保存手段の比較(SingleFile・Playwright・ArchiveBox・WARC・yt-dlp)

Yahoo Newsの記事は一定期間で削除されることがあります。個人的な記録としてローカルに保存しておきたい場合、いくつかの手段があります。 ここでは、以下の5つの方法を同一の記事に対して実行し、結果を比較しました。 SingleFile CLI — 単一HTMLファイルとして保存 Playwright PDF — ページをPDF化 ArchiveBox — 複数形式を一括保存(WARC含む) WARC — 標準的なウェブアーカイブ形式 yt-dlp — 記事内の動画をダウンロード 比較結果 手段 形式 フォルダサイズ 広告 動画 SingleFile CLI 単一HTML 1.3MB 含まれる × Playwright PDF PDF 2.5MB 含まれにくい × ArchiveBox 複数形式一括 43MB 含まれる △ yt-dlp MP4 27MB - ○ ArchiveBoxの43MBにはSingleFile・PDF・WARC・本文抽出などが全て含まれています。全手段を併用した場合、1記事あたり約74MBのストレージを消費します。 SingleFile CLI SingleFile は、Webページを画像・CSS込みの単一HTMLファイルとして保存するツールです。 Chrome拡張版が有名ですが、CLI版もあります。 インストールと実行 npm install -g single-file-cli single-file 'https://news.yahoo.co.jp/articles/xxxxx' output.html 不要な要素の除去 --removed-elements-selector オプションで特定の要素を除去することもできます。 single-file 'https://news.yahoo.co.jp/articles/xxxxx' output.html \ --removed-elements-selector='header, footer, nav, aside, [id^="yads_"]' ただし、CSSセレクタの指定によっては記事の構成要素(配信元情報、更新日時など)まで意図せず削除してしまう可能性があるため、除去する場合は保存結果を確認する必要があります。 ...