ホーム 記事一覧 ブック DH週間トピックス 検索 このサイトについて
English
Mirador 4用回転プラグインの開発とnpm公開

Mirador 4用回転プラグインの開発とnpm公開

はじめに IIIFビューアであるMiradorの最新版(Mirador 4)に対応した回転プラグイン「mirador-rotation」を開発し、npmで公開しました。本記事では、プラグインの開発から公開、そして実際に利用するための統合方法について解説します。 背景 Mirador 3からMirador 4へのメジャーアップデートに伴い、以下の変更がありました: React 16 → React 18 Material-UI v4 → MUI v7 その他多数の依存関係の更新 これにより、既存のMirador 3用プラグインはそのままでは動作しなくなりました。 mirador-rotation-pluginの開発 リポジトリ https://github.com/nakamura196/mirador-rotation-plugin 主な機能 画像の回転機能 Mirador 4のプラグインメニューへの統合 npmへの公開 開発したプラグインはnpmで公開しています: npm install mirador-rotation mirador-integrationの更新 公式のmirador-integrationリポジトリを参考に、Mirador 4対応の統合環境を構築しました。 主な変更点 項目 旧 新 Mirador 3.x 4.0.0 React 16.14.0 18.x ビルドツール Webpack Parcel UI Material-UI v4 MUI v7 package.json 最小限の依存関係で構成しています: { "dependencies": { "mirador": "^4.0.0", "mirador-rotation": "^4.0.0", "parcel": "^2.0.0", "react": "^18.0.0", "react-dom": "^18.0.0" } } 外部から利用するための工夫 問題 公式のmirador-integrationをそのままビルドすると、ESモジュール形式で出力されます。しかし、別のHTMLページから<script>タグで読み込んで使いたい場合、グローバル変数としてMiradorが定義されないという問題がありました。 script src="mirador.js">script> script> Mirador.viewer({...}); // Mirador is not defined script> 解決策 1. ライブラリ用エントリーポイントの作成 index.jsでグローバル変数として明示的にエクスポートします: ...

静的サイトでIIIF Content Search APIを実現する - Service Workerによるクライアントサイド検索

静的サイトでIIIF Content Search APIを実現する - Service Workerによるクライアントサイド検索

はじめに IIIF (International Image Interoperability Framework) は、デジタルアーカイブや美術館のコレクションで広く使われている画像配信の国際規格です。IIIF Content Search API を使うと、マニフェスト内のアノテーション(注釈やタグ)を検索できます。 しかし、IIIF Content Search API は通常、サーバーサイドでの実装が前提となっており、静的サイト(GitHub Pages、Vercel、Netlify など)では実現が難しいとされてきました。 本記事では、Service Worker を使ってクライアントサイドで IIIF Content Search API を実装する方法 を紹介します。この手法により、静的サイトでも Mirador などの IIIF ビューアで検索機能を利用できるようになります。 課題 従来のIIIF Search APIの仕組み [Mirador] → GET /search?q=keyword → [サーバー] → 検索処理 → JSON応答 IIIF Content Search API は、クエリパラメータ(?q=検索語)を受け取り、検索結果を JSON で返すエンドポイントを必要とします。これは動的なサーバー処理を前提としています。 静的サイトの制約 静的サイトでは: クエリパラメータに応じた動的なレスポンスを返せない サーバーサイドの検索処理を実行できない 静的 JSON ファイルしか配信できない 解決策:Service Worker によるリクエストインターセプト Service Worker は、ブラウザとネットワークの間に位置するプロキシとして機能します。これを活用して、検索リクエストをインターセプトし、クライアントサイドで検索処理を行います。 アーキテクチャ [Mirador] │ │ GET /iiif/site/search/index.json?q=keyword ↓ [Service Worker] ← インターセプト │ ├─ 静的な index.json を fetch(初回のみ) │ ├─ JavaScript で検索を実行 │ └─ IIIF Content Search API 形式で応答 ↓ [Mirador] ← 検索結果を表示 実装 1. 検索インデックスの生成(ビルド時) まず、アノテーションデータから検索インデックスを生成します。 ...

JavaScriptの演算子優先順位の罠 - Vercelビルドエラーの原因を探る

JavaScriptの演算子優先順位の罠 - Vercelビルドエラーの原因を探る

はじめに Next.jsアプリケーションをVercelにデプロイしようとしたところ、ローカルでは成功するのにVercelでは失敗するという問題に遭遇しました。エラーメッセージは曖昧で、原因の特定に苦労しました。 この記事では、問題の発見から解決までの過程を共有し、JavaScriptの演算子優先順位について学んだことをまとめます。 問題の症状 エラーメッセージ Error occurred prerendering page "/en/smells/22-03" [Error: An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details.] 特徴的な現象 ローカルビルドは成功 するが、Vercelでは失敗 毎回異なるページ でエラーが発生(22-03、24-03、25-04など) エラーの詳細が本番ビルドでは隠される 原因の発見 複数のファイルを調査する中で、以下のコードパターンを発見しました: const origin = process.env.NEXT_PUBLIC_SITE_URL || '' + process.env.NEXT_PUBLIC_BASE_PATH || ''; 一見問題なさそうに見えますが、ここに演算子優先順位の罠 が潜んでいました。 演算子優先順位とは JavaScriptでは、演算子には優先順位があります。数学で「掛け算は足し算より先に計算する」のと同じです。 優先順位の例 // 数学と同じ 2 + 3 * 4 // → 2 + 12 → 14(3 * 4 が先) // 文字列連結 (+) と論理和 (||) の場合 // + の優先順位: 13 // || の優先順位: 5 // → + が先に評価される! 問題のコードを分解 const origin = process.env.NEXT_PUBLIC_SITE_URL || '' + process.env.NEXT_PUBLIC_BASE_PATH || ''; このコードは以下のように解釈されます: ...

Sketchfab APIでGLBファイルをダウンロード・表示するWebアプリを作る

Sketchfab APIでGLBファイルをダウンロード・表示するWebアプリを作る

Sketchfab APIを使って3DモデルをGLBファイルとしてダウンロードし、ブラウザ上でThree.jsで表示するWebアプリを作成しました。本記事では、セキュリティを考慮したアーキテクチャ設計から実装まで解説します。 やりたかったこと Sketchfab上の3DモデルをGLB形式でダウンロードしたい ダウンロードしたGLBをブラウザ上で3D表示したい APIトークンを安全に管理したい 技術スタック Next.js 16(App Router) React Three Fiber / @react-three/drei TypeScript 最初に試したこと:クライアントサイドのみで実装 最初はシンプルにHTML + JavaScriptだけで実装しようとしました。 // Sketchfab APIからダウンロードURLを取得 const response = await fetch( `https://api.sketchfab.com/v3/models/${modelUid}/download`, { headers: { 'Authorization': `Token ${apiToken}` } } ); const data = await response.json(); console.log(data.glb.url); // 署名付きS3 URL これ自体は動作しましたが、2つの問題がありました。 問題1: APIトークンの露出 クライアントサイドでAPIトークンを使用すると、ブラウザの開発者ツールから簡単に確認できてしまいます。APIトークンが漏洩すると、他人があなたのアカウントでAPIを利用できてしまうため、これは避けるべきです。 問題2: CORSエラー(場合による) ブラウザのセキュリティ制限により、異なるオリジンへのリクエストが制限されることがあります。今回のケースでは、Sketchfab APIへのリクエスト自体はCORSが許可されていましたが、環境によっては問題になる可能性があります。 解決策:サーバーサイドAPIを経由する設計 Next.jsのAPI Routesを使って、サーバーサイドでSketchfab APIと通信する設計にしました。 ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │ │ │ │ │ │ Client │────▶│ Next.js API │────▶│ Sketchfab API │ │ (Browser) │ │ (Server) │ │ │ │ │◀────│ │◀────│ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ │ ┌─────────────────┐ │ │ │ └──────────▶│ S3 (GLB) │ │ 署名付きURL │ └─────────────────┘ ポイント:署名付きURLの活用 Sketchfab APIは、GLBファイルの実体ではなく「署名付きS3 URL」を返します。 ...

ODD編集Tips:その1

ODD編集Tips:その1

要素の属性を特定のものだけに制限する TEIのデフォルトでは、要素は多くの属性クラス(att.global、att.datableなど)を継承しており、多数の属性が使用可能です。特定の属性のみを許可したい場合は、以下のように設定します。 例: persNameでxml:idとcorrespのみを許可 <elementSpec ident="persName" mode="change"> <classes mode="change"> <!-- 属性クラスを削除(モデルクラスは維持) --> <memberOf key="att.global" mode="delete"/> <memberOf key="att.cmc" mode="delete"/> <memberOf key="att.datable" mode="delete"/> <memberOf key="att.editLike" mode="delete"/> <memberOf key="att.personal" mode="delete"/> <memberOf key="att.typed" mode="delete"/> </classes> <attList> <attDef ident="xml:id" mode="add" usage="opt"> <desc>要素の一意な識別子</desc> <datatype> <dataRef name="ID"/> </datatype> </attDef> <attDef ident="corresp" mode="add" usage="opt"> <desc>関連する人物情報へのリンク</desc> <datatype> <dataRef key="teidata.pointer"/> </datatype> </attDef> </attList> </elementSpec> ポイント <classes mode="change">を使用: mode="replace"で空にすると、モデルクラスも削除され要素自体が使えなくなる 属性クラスを個別に削除 : <memberOf key="att.xxx" mode="delete"/>で不要な属性クラスを削除 必要な属性を追加 : <attDef ident="xxx" mode="add">で許可したい属性を定義 注意点 要素がどの属性クラスに属しているかは、TEI Guidelinesで確認できる att.globalを削除するとxml:id、xml:langなども使えなくなるため、必要に応じて個別に追加する 要素に属性を追加する 既存の属性クラスを維持したまま、新しい属性を追加する場合: <elementSpec ident="pb" mode="change"> <attList> <attDef ident="facs" mode="add" usage="opt"> <desc>原本画像へのリンク</desc> <datatype> <dataRef key="teidata.pointer"/> </datatype> </attDef> </attList> </elementSpec> この場合、既存の属性クラスはそのまま維持され、facs属性が追加されます。

Next Auth (Auth.js v5) の本番環境で AUTH_URL が必須な理由

Next Auth (Auth.js v5) の本番環境で AUTH_URL が必須な理由

概要 Next Auth (Auth.js v5) を使用したアプリケーションを Docker コンテナや本番環境にデプロイする際、AUTH_URL 環境変数を設定しないと GitHub OAuth 認証で以下のエラーが発生します: Be careful! The redirect_uri is not associated with this application. 開発環境 vs 本番環境 開発環境(npm run dev) 開発環境では Next.js が自動的にホスト情報を検出するため、AUTH_URL の設定は不要 です。 # これだけで動作する npm run dev 本番環境(Docker / npm run build && npm start) 本番環境では Next Auth がホスト情報を自動検出できないため、AUTH_URL の設定が必須 です。 # .env.local または環境変数に追加 AUTH_URL=http://localhost:3000 # または https://your-domain.com AUTH_TRUST_HOST=true Docker での設定例 docker run の場合 docker run -d --name myapp -p 3000:3000 \ --env-file .env.local \ -e AUTH_TRUST_HOST=true \ myapp:latest docker-compose.yml の場合 services: app: image: myapp:latest ports: - "3000:3000" environment: - AUTH_URL=https://your-domain.com - AUTH_TRUST_HOST=true env_file: - .env.local 必要な環境変数一覧 変数名 開発環境 本番環境 説明 AUTH_SECRET 必須 必須 セッション暗号化キー AUTH_GITHUB_ID 必須 必須 GitHub OAuth Client ID AUTH_GITHUB_SECRET 必須 必須 GitHub OAuth Client Secret AUTH_URL 不要 必須 アプリケーションのベースURL AUTH_TRUST_HOST 不要 必須 ホストを信頼する設定 .env.local の例 # NextAuth.js Configuration AUTH_SECRET=your-secret-key-here # GitHub OAuth App Credentials AUTH_GITHUB_ID=your-client-id AUTH_GITHUB_SECRET=your-client-secret # Production only (Docker / deployed environments) AUTH_URL=http://localhost:3000 なぜこの問題が起きるのか 開発環境 : Next.js の開発サーバーがリクエストヘッダーから正確なホスト情報を取得できる 本番環境 : Docker コンテナ内では 0.0.0.0:3000 として起動するため、外部からアクセスする際の実際のURLがわからない OAuth の仕組み : GitHub は登録された redirect_uri と完全一致するURLにのみリダイレクトを許可する トラブルシューティング エラー: “redirect_uri is not associated with this application” 原因 : AUTH_URL が未設定、または GitHub OAuth App の Callback URL と一致しない ...

YOLOv11xモデルをHugging Faceに公開する

YOLOv11xモデルをHugging Faceに公開する

日本古典籍くずし字データセットで学習したYOLOv11xモデルをHugging Faceに公開し、Gradio Spacesでデモを作成する手順を紹介します。 概要 モデル : YOLOv11x(くずし字検出用) データセット : 日本古典籍くずし字データセット 公開先 : Hugging Face Models + Spaces 1. Hugging Face Modelsにモデルを登録 1.1 huggingface_hubのインストール pip install huggingface_hub 1.2 ログイン huggingface-cli login または Python から: from huggingface_hub import login login() トークンは https://huggingface.co/settings/tokens から取得できます(Write権限が必要)。 1.3 モデルのアップロード from huggingface_hub import HfApi, create_repo api = HfApi() repo_id = "your-username/yolov11x-codh-char" # リポジトリ作成 create_repo(repo_id, repo_type="model", exist_ok=True) # モデルファイルをアップロード api.upload_file( path_or_fileobj="best.pt", path_in_repo="best.pt", repo_id=repo_id, repo_type="model" ) 1.4 Model Card (README.md) の作成 モデルの使い方やライセンス情報を記載したREADME.mdを作成してアップロードします。 2. Hugging Face Spacesでデモを公開 2.1 Spacesの設定 (README.md) --- title: YOLOv11x Character emoji: 👁 colorFrom: pink colorTo: green sdk: gradio sdk_version: 5.49.1 app_file: app.py pinned: false --- ポイント : ...

IIIF画像をWeb Tile Map Serviceで配信する

IIIF画像をWeb Tile Map Serviceで配信する

IIIF Georeference Extension JSONからXYZタイルを生成し、TileServer GLで配信、MapLibre GL JSで表示するまでの手順をまとめます。 OSM上に東京大学鳥瞰図をオーバーレイ表示 概要 IIIF Georeference JSON │ ▼ ┌───────────────────────┐ │ iiif-georef-tiles │ │ (XYZタイル生成) │ └───────────────────────┘ │ ▼ ┌───────────────────────┐ │ mb-util │ │ (mbtiles変換) │ └───────────────────────┘ │ ▼ ┌───────────────────────┐ │ TileServer GL │ │ (タイル配信) │ └───────────────────────┘ │ ▼ ┌───────────────────────┐ │ MapLibre GL JS │ │ (地図表示) │ └───────────────────────┘ 必要環境 Docker / Docker Compose Python 3.x GDAL (gdal_translate, gdalwarp, gdal2tiles.py) Pillow (pip3 install pillow) mb-util GDALのインストール # macOS (Homebrew) brew install gdal # Ubuntu/Debian sudo apt install gdal-bin python3-gdal mb-utilのインストール pip3 install mbutil 1. プロジェクト構成 wtms/ ├── docker-compose.yml ├── data/ # mbtilesファイル ├── styles/ # カスタムスタイル(オプション) ├── frontend/ # MapLibreビューア └── docs/ 2. Docker Compose設定 docker-compose.yml: ...

Nuxt 3 プロジェクトのパッケージ更新まとめ

Nuxt 3 プロジェクトのパッケージ更新まとめ

概要 Nuxt 3.2.3 から 3.20.2 へのメジャーアップデートを含む、依存パッケージの大規模更新を実施しました。 主要なパッケージ更新 パッケージ 更新前 更新後 nuxt 3.2.3 3.20.2 @nuxt/content 2.5.2 3.11.0 @nuxtjs/i18n 8.0.0-beta.10 10.2.1 vuetify 3.1.8 3.7.6 sass 1.58.3 1.83.4 @mdi/js 7.1.96 7.4.47 新規追加パッケージ better-sqlite3: ^12.5.0 - @nuxt/content v3 の依存 vue-i18n: ^11.0.0 - i18n モジュールの依存 対応が必要だった変更点 1. @nuxt/content v3 への移行 content.config.ts の新規作成が必要になりました。 // content.config.ts import { defineContentConfig, defineCollection } from '@nuxt/content' export default defineContentConfig({ collections: { content: defineCollection({ type: 'page', source: '**/*.md' }) } }) 2. @nuxtjs/i18n v10 への移行 nuxt.config.ts の変更点 // Before i18n: { locales: [ { code: "ja", iso: "ja_JP", file: "ja.js" }, { code: "en", iso: "en-US", file: "en.js" }, ], langDir: "locales/", vueI18n: { fallbackLocale: lang, }, } // After i18n: { locales: [ { code: "ja", language: "ja-JP", file: "ja.js" }, // iso → language { code: "en", language: "en-US", file: "en.js" }, ], bundle: { optimizeTranslationDirective: false, runtimeOnly: true, }, langDir: "locales", // 末尾スラッシュ削除 vueI18n: "./i18n.config.ts", // 外部ファイル化 } i18n.config.ts の新規作成 export default defineI18nConfig(() => ({ legacy: false, fallbackLocale: "ja", })); localesフォルダの移動 locales/ → i18n/locales/ ...

IIIF Georeference to XYZ Tiles

IIIF Georeference to XYZ Tiles

IIIF Georeference Extension JSONからXYZタイルを生成し、MapLibre GL JSで表示するツール。 リポジトリ : https://github.com/nakamura196/iiif-georef-tiles GitHub Pages : https://nakamura196.github.io/iiif-georef-tiles/ 必要環境 Python 3.x GDAL (gdal_translate, gdalwarp, gdal2tiles.py) GDALのインストール # macOS (Homebrew) brew install gdal # Ubuntu/Debian sudo apt install gdal-bin python3-gdal 使用方法 python3 scripts/iiif_georef_to_tiles.py <IIIF_GEOREF_JSON_URL> 例 python3 scripts/iiif_georef_to_tiles.py https://nakamura196.github.io/iiif_geo/canvas.json オプション オプション デフォルト 説明 --scale 0.25 画像の縮小率 --zoom 14-18 タイルのズームレベル範囲 --output-dir docs 出力ディレクトリ --name tiles タイルフォルダ名 --work-dir work 作業用ディレクトリ --keep-work - 作業用ファイルを削除しない 処理の流れ IIIF Georeference JSON │ ▼ ┌───────────────────────┐ │ 1. JSONを取得 │ │ (URLからfetch) │ └───────────────────────┘ │ ▼ ┌───────────────────────┐ │ 2. 画像をダウンロード │ │ (IIIF Image API) │ └───────────────────────┘ │ ▼ ┌───────────────────────┐ │ 3. GCPを埋め込み │ │ (gdal_translate) │ └───────────────────────┘ │ ▼ ┌───────────────────────┐ │ 4. 座標変換 │ │ (gdalwarp) │ └───────────────────────┘ │ ▼ ┌───────────────────────┐ │ 5. タイル生成 │ │ (gdal2tiles.py) │ └───────────────────────┘ │ ▼ ┌───────────────────────┐ │ 6. HTMLビューア生成 │ │ (MapLibre GL JS) │ └───────────────────────┘ 変換結果 元画像 地理参照後 出力ファイル docs/ ├── index.html # MapLibre GL JSビューア ├── source.json # 元のIIIF Georeference JSON └── tiles/ # XYZタイル ├── 14/ ├── 15/ ├── 16/ ├── 17/ └── 18/ ローカルで確認 cd docs && python3 -m http.server 8000 # http://localhost:8000/ を開く IIIF Georeference Extension IIIF Georeference Extensionは、IIIF画像に地理参照情報を付与するための拡張仕様です。 ...

Azure OpenAI Whisper + Speech Services で動画に英語字幕・音声を自動生成する

Azure OpenAI Whisper + Speech Services で動画に英語字幕・音声を自動生成する

日本語の動画に英語字幕と英語音声を自動で付与する方法をまとめました。Azure OpenAI ServiceのWhisperとSpeech Servicesを使用します。 概要 今回の目的は、日本語音声の動画を以下のように多言語対応させることです: 日本語版 : 元の動画(日本語音声、字幕なし) 英語版 : 英語音声 + 英語字幕 使用サービス サービス 用途 Azure OpenAI Service (Whisper) 日本語音声 → 英語テキストへの翻訳 Azure Speech Services (TTS) 英語テキスト → 英語音声の合成 FFmpeg 音声抽出・動画結合 手順 1. 環境準備 必要なツール # FFmpegのインストール(macOS) brew install ffmpeg # Pythonライブラリ pip install python-dotenv requests Azure設定(.env) AZURE_OPENAI_ENDPOINT=https://xxxxx.openai.azure.com AZURE_OPENAI_API_KEY=your-api-key AZURE_OPENAI_DEPLOYMENT_NAME=whisper AZURE_OPENAI_API_VERSION=2024-06-01 2. 動画から音声を抽出 Azure Whisper APIには25MBのファイルサイズ制限があるため、音声を圧縮して抽出します。 ffmpeg -i input.mp4 -vn -acodec libmp3lame -b:a 64k -ar 16000 audio.mp3 3. Whisperで英語字幕を生成 Azure OpenAI ServiceのWhisper APIを使用して、日本語音声を英語に翻訳しながら文字起こしします。 ...

Eclipse EDCを使ったデータスペース入門 - ローカル環境でデータ交換フローを体験する

Eclipse EDCを使ったデータスペース入門 - ローカル環境でデータ交換フローを体験する

はじめに 近年、企業間でのデータ共有・流通の重要性が高まっています。しかし、単純にAPIを公開するだけでは、「誰が」「どんな条件で」「どのデータに」アクセスできるかを制御することが困難です。 データスペース(Dataspace) は、この課題を解決するための概念です。データの所有者が主権を持ちながら、信頼できる相手とデータを安全に共有できる仕組みを提供します。 本記事では、データスペースの実装基盤である Eclipse EDC(Eclipse Dataspace Components) を使って、ローカル環境でデータ交換フローを体験します。 目次 データスペースとは? Eclipse EDCの概要 環境構築 データ交換フローの実行 GUIダッシュボードの作成 まとめ データスペースとは? 従来のデータ共有の課題 従来のAPI連携では、以下のような課題がありました: アクセス制御が困難 : APIキーを渡すと、どんなデータでも取得できてしまう 利用条件の管理 : 「このデータは社内利用のみ」といった条件を技術的に強制できない 監査・追跡 : 誰がいつデータを取得したか追跡しづらい データスペースの解決策 データスペースは以下の仕組みで解決します: ┌─────────────────────────────────────────────────────────────┐ │ 従来のAPI連携 │ │ │ │ Consumer ─────APIキー────→ Provider API │ │ ←────データ──── │ │ │ │ 課題: 誰でもデータ取得可能、条件管理なし │ └─────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────┐ │ データスペース │ │ │ │ 1. Consumer: 「〇〇のデータが欲しい」(カタログ確認) │ │ 2. Provider: 「この条件を満たせば提供する」(ポリシー提示) │ │ 3. 両者: 契約交渉・合意 │ │ 4. Consumer: 契約に基づきデータ取得 │ │ │ │ メリット: 条件付きアクセス、監査可能、主権維持 │ └─────────────────────────────────────────────────────────────┘ 主要な用語 用語 説明 Connector データスペースに参加するためのソフトウェア。ProviderとConsumerそれぞれが持つ Provider データを提供する側 Consumer データを取得する側 Catalog Providerが公開しているデータの一覧 Policy データアクセスの条件(誰が、いつ、どのように使えるか) Contract ProviderとConsumer間で結ばれる契約 EDR Endpoint Data Reference。データ取得用の一時的なアクセス情報 Eclipse EDCの概要 Eclipse EDC は、Eclipse Foundationが開発するオープンソースのデータスペース実装基盤です。 ...

@elastic/react-search-ui を React 19 + Next.js 15.5 で使う方法

@elastic/react-search-ui を React 19 + Next.js 15.5 で使う方法

はじめに React 19 と Next.js 15 を使用しているプロジェクトで @elastic/react-search-ui を使おうとすると、以下のような依存関係エラーに遭遇することがあります。 npm error ERESOLVE could not resolve npm error peer react@">= 16.8.0 < 19" from @elastic/react-search-ui@1.23.1 この記事では、この問題の原因と解決方法を詳しく解説します。 問題の原因 @elastic/react-search-ui@1.23.1 の peer dependency が react@">= 16.8.0 < 19" となっており、React 19 をサポートしていませんでした。 解決策 1. パッケージのアップグレード 2025年5月に PR #1162 がマージされ、React 19 がサポートされました。バージョン 1.24.2 以降を使用します。 // package.json { "dependencies": { - "@elastic/react-search-ui": "^1.23.1", - "@elastic/react-search-ui-views": "^1.23.1", - "@elastic/search-ui": "^1.23.1", + "@elastic/react-search-ui": "^1.24.2", + "@elastic/react-search-ui-views": "^1.24.2", + "@elastic/search-ui": "^1.24.2", - "next": "15.3.8", + "next": "15.5.9", } } 2. 型定義の変更への対応 1.24.2 では WithSearch コンポーネントの型定義が変更されました。SearchContextState の filters が Filter[] | undefined になっています。 ...

GLBファイルのDraco圧縮 - 87%のサイズ削減と精度への影響

GLBファイルのDraco圧縮 - 87%のサイズ削減と精度への影響

3DモデルをWebで配信する際、ファイルサイズは重要な課題です。本記事では、Draco圧縮 を使ってGLBファイルを87%削減した事例と、圧縮時の注意点(特にUV座標)について解説します。 https://3dtiles-viewer.vercel.app/glb-viewer.html 使用データ モデル : Rotunde Brunnen(噴水のある円形建築物) 出典 : Sketchfab 形式 : GLB (glTF 2.0 Binary) Draco圧縮とは DracoはGoogleが開発したオープンソースの3Dメッシュ圧縮ライブラリです。glTF 2.0ではKHR_draco_mesh_compression拡張として標準サポートされています。 圧縮の仕組み 量子化(Quantization) : 頂点座標やUV座標を指定ビット数に丸める 予測符号化 : 隣接頂点との差分を予測して符号化 エントロピー符号化 : 予測誤差を効率的に圧縮 圧縮コマンド # gltf-transformを使用 npx gltf-transform draco input.glb output-draco.glb # オプション付き(高品質設定) npx gltf-transform draco input.glb output-draco.glb \ --quantize-position 14 \ --quantize-normal 10 \ --quantize-texcoord 12 圧縮結果の比較 ファイルサイズ ファイル サイズ 削減率 rotunde-brunnen.glb(元) 94.7 MB - rotunde-brunnen-draco.glb 12.5 MB 87%削減 メッシュ構造 項目 元ファイル Draco圧縮後 メッシュ数 38 2(統合) 三角形数 約175万 約167万 テクスチャ 1024x1024 PNG 同一 バウンディングボックス ほぼ同一 ほぼ同一 精度 視覚的な精度低下はありません 。 ...

300万点超の点群データをブラウザで快適に表示する - Potree LODビューアの構築

300万点超の点群データをブラウザで快適に表示する - Potree LODビューアの構築

大規模な点群データ(LiDAR/LAZ)をWebブラウザで表示しようとすると、メモリ不足でクラッシュしてしまうことがあります。本記事では、Potree のLOD(Level of Detail)技術を使って、数百万点の点群をストレスなく表示する方法を紹介します。 https://3dtiles-viewer.vercel.app/potree-lod-viewer.html 使用データ データ名 : Utah State Capitol(ユタ州議事堂) 出典 : OpenTopography ダウンロードURL : https://object.cloud.sdsc.edu/v1/AUTH_opentopography/www/education/MatlabTopo/Utah_state_capitol.laz ファイルサイズ : 15MB(LAZ圧縮) 点数 : 3,481,512点 位置 : Salt Lake City, Utah, USA 課題 このデータをそのままThree.jsなどで読み込もうとすると、ブラウザがフリーズする可能性があります。 解決策:Potree Potreeは、大規模点群データのためのWebGLベースのビューアです。**LOD(Level of Detail)**により、カメラに近い部分は詳細に、遠い部分は粗く表示することで、数十億点のデータでもスムーズに動作します。 仕組み 点群をオクトリー構造 で空間分割 各ノードに異なる詳細度のデータを格納 視点に応じて必要なノードのみ動的に読み込み 手順 1. LAZファイルのダウンロード curl -L -o utah_capitol.laz \ "https://object.cloud.sdsc.edu/v1/AUTH_opentopography/www/education/MatlabTopo/Utah_state_capitol.laz" 2. LAZからPotree形式への変換 PotreeConverterをDockerで実行します。 # Dockerイメージの取得 docker pull synth3d/potreeconverter # 変換実行 docker run --rm \ -v $(pwd):/data \ -v $(pwd)/output:/output \ synth3d/potreeconverter \ PotreeConverter /data/utah_capitol.laz -o /output/utah_capitol 出力結果 : ...

mirador-annotations を Mirador 4.x へ移行した記録

mirador-annotations を Mirador 4.x へ移行した記録

背景 mirador-annotations は、IIIF ビューア Mirador にアノテーション機能を追加するプラグインです。 従来のプロジェクトは以下の構成でした: ビルドツール : nwb (Create React App ベース) UI ライブラリ : Material-UI v4 Mirador : 3.x React : 17.x しかし、以下の問題が発生していました: nwb のメンテナンス停止 - nwb は長期間更新されておらず、依存関係の競合が頻発 npm install の失敗 - 古い依存関係により、新しい環境でのセットアップが困難に セキュリティ脆弱性 - 古いパッケージに多数の脆弱性警告 これらの問題を解決するため、以下への移行を決定しました: ビルドツール : Vite UI ライブラリ : MUI v7 Mirador : 4.x React : 18.x 移行作業の概要 1. ビルドツールの移行 (nwb → Vite) nwb の設定ファイルを削除し、vite.config.js を新規作成しました。 主なポイント: // vite.config.js export default defineConfig(({ mode }) => { const env = loadEnv(mode, process.cwd(), ''); return { // draft-js が global を参照するため define: { global: 'globalThis', }, // 重複パッケージの解決 resolve: { dedupe: [ '@emotion/react', '@emotion/styled', 'react', 'react-dom', ], }, }; }); 2. Material-UI の移行 (v4 → v7) @material-ui/* を @mui/* に変更 makeStyles を sx prop に置き換え Grid コンポーネントの API 変更に対応 (item と xs props が size に統合) // 変更前 (MUI v4) Grid item xs={12}> // 変更後 (MUI v7) Grid size={12}> 3. Mirador 4.x への対応 Mirador 4.x では、アクションやセレクターのインポート方法が変更されました: ...

mirador-rotation-plugin 機能拡張

mirador-rotation-plugin 機能拡張

概要 mirador-rotation-pluginに以下の機能を追加しました: 90度単位の回転ボタン URLパラメータによるマニフェスト・回転角度の指定 UIの改善(リセットボタンのアイコン変更) ヘルプ機能(使い方を説明するダイアログ) 新機能の詳細 1. 90度単位の回転ボタン 従来は1度単位のスライダーのみでしたが、90度単位で素早く回転できるボタンを追加しました。 実装内容 src/plugins/MiradorRotation.js に以下の変更を加えました: import RotateLeftIcon from '@mui/icons-material/RotateLeft'; import RotateRightIcon from '@mui/icons-material/RotateRight'; // 90度回転のハンドラー const handleRotate90 = (direction) => { const newRotation = rotation + (direction * 90); updateViewport(windowId, { rotation: newRotation }); }; UIには2つのボタンを追加: 左回転ボタン : 反時計回りに90度回転 右回転ボタン : 時計回りに90度回転 翻訳対応 src/translations.js に英語・日本語の翻訳を追加: { en: { rotateLeft: 'Rotate 90° left', rotateRight: 'Rotate 90° right', }, ja: { rotateLeft: '左に90度回転', rotateRight: '右に90度回転', }, } 2. URLパラメータ対応 デモページでURLパラメータからマニフェストと回転角度を指定できるようになりました。 対応パラメータ パラメータ 説明 区切り文字 manifest IIIFマニフェストURL ;(セミコロン) rotation 初期回転角度(度) ;(セミコロン) デモページURL https://nakamura196.github.io/mirador-rotation-plugin/ 使用例 # 単一マニフェスト https://nakamura196.github.io/mirador-rotation-plugin/?manifest=https://example.com/manifest.json&rotation=180 # 複数マニフェスト(同じ回転角度) https://nakamura196.github.io/mirador-rotation-plugin/?manifest=url1;url2&rotation=90 # 複数マニフェスト(異なる回転角度) https://nakamura196.github.io/mirador-rotation-plugin/?manifest=url1;url2&rotation=90;180 実装内容 demo/src/index.js の主な変更: ...

Next.js + next-auth で GakuNin RDM と OAuth2 連携する

Next.js + next-auth で GakuNin RDM と OAuth2 連携する

はじめに 研究データ管理基盤「GakuNin RDM」と Next.js アプリケーションを OAuth2 で連携する方法を解説します。GakuNin RDM は OSF(Open Science Framework)互換の API を提供しているため、OSF の OAuth2 フローを参考に実装できます。 本記事では、next-auth を使用した実装方法と、アクセストークンの自動リフレッシュ というハマりポイントについて詳しく説明します。 GakuNin RDM とは GakuNin RDM(Research Data Management)は、国立情報学研究所(NII)が提供する研究データ管理サービスです。 URL : https://rdm.nii.ac.jp/ API : OSF 互換 REST API(https://api.rdm.nii.ac.jp/v2/) 認証 : OAuth2(https://accounts.rdm.nii.ac.jp/) 研究者が研究データを安全に保存・共有・公開できるプラットフォームで、学認(GakuNin)認証との連携により、日本の大学・研究機関のユーザーが利用できます。 事前準備 1. OAuth アプリケーションの登録 GakuNin RDM の設定画面から OAuth アプリケーションを登録します。 https://rdm.nii.ac.jp/settings/applications/ にアクセス 「Developer application を登録する」をクリック 以下を設定: Application name : アプリ名 Application homepage URL : http://localhost:3000(開発時) Application description : 説明 Authorization callback URL : http://localhost:3000/api/auth/callback/gakunin 登録後、Client ID と Client Secret が発行されます。 ...

【AWS Amplify】さくらのドメインでカスタムドメイン設定時にハマったポイント

【AWS Amplify】さくらのドメインでカスタムドメイン設定時にハマったポイント

はじめに AWS Amplifyでホスティングしているアプリに、さくらインターネットで管理しているドメインのサブドメインを設定しようとしたところ、「ドメインの所有権を検証中…」のまま進まない問題に遭遇しました。 原因はさくらのDNS特有の仕様 でした。同じ問題でハマっている方の参考になれば幸いです。 環境 AWS Amplify Hosting さくらインターネット ドメインコントロールパネル ネームサーバー: ns1.dns.ne.jp / ns2.dns.ne.jp 症状 Amplifyのカスタムドメイン設定画面で指示されたCNAMEレコードを設定したが、いつまで経っても「ドメインの所有権を検証中…」から進まない。 Amplifyからの指示内容 Amplifyからは以下のようなDNSレコード設定を求められます: 1. SSL証明書検証用 ホスト名 タイプ 値 _abc123.your-subdomain.example.com. CNAME _def456.xyz.acm-validations.aws. 2. サブドメイン転送用 ホスト名 タイプ 値 your-subdomain CNAME xxxxx.cloudfront.net 原因 digコマンドで確認したところ、CNAMEの値にドメイン名が二重に追加されていました。 $ dig your-subdomain.example.com CNAME # 期待する結果 your-subdomain.example.com. IN CNAME xxxxx.cloudfront.net. # 実際の結果(誤り) your-subdomain.example.com. IN CNAME xxxxx.cloudfront.net.example.com. さくらのDNSでは、CNAMEの値に末尾ドット(.)がない場合、自動的にゾーン名(ドメイン名)が補完される仕様 になっています。 解決方法 さくらのドメインコントロールパネルでCNAMEレコードを設定する際、値(データ)の末尾に必ずドット(.)を付ける 必要があります。 正しい設定例 エントリ名 タイプ データ _abc123.your-subdomain CNAME _def456.xyz.acm-validations.aws. ← 末尾に. your-subdomain CNAME xxxxx.cloudfront.net. ← 末尾に. ⚠️ 注意 : Amplifyの指示画面では2つ目のレコード(cloudfront.net)に末尾ドットが表示されていない場合がありますが、さくらのDNSでは両方とも末尾ドットが必要 です。 ...

Dydra JSON-LDシリアライゼーションの挙動と回避策

Dydra JSON-LDシリアライゼーションの挙動と回避策

概要 Dydraは優れたクラウドベースのRDFトリプルストアですが、JSON-LDシリアライゼーションにおいて、一部のケースで期待と異なる出力が得られることがあります。このブログでは、その挙動と、我々が実装した回避策について解説します。 確認された挙動 期待される出力 JSON-LD仕様では、URI参照は以下のようにオブジェクト形式で出力されることが一般的です: { "@id": "https://example.com/item/1", "@type": ["prov:Entity"], "prov:wasAttributedTo": { "@id": "https://sepolia.etherscan.io/address/0x1234..." }, "prov:wasGeneratedBy": { "@id": "https://sepolia.etherscan.io/tx/0xabcd..." } } Dydraで確認された出力 DydraのJSON-LDエンドポイントでは、一部のURI参照が単なる文字列として出力されるケースが確認されました: { "@id": "https://example.com/item/1", "@type": ["prov:Entity"], "prov:wasAttributedTo": "https://sepolia.etherscan.io/address/0x1234...", "prov:wasGeneratedBy": "https://sepolia.etherscan.io/tx/0xabcd..." } 注意 : この挙動は全てのプロパティで発生するわけではなく、@contextの定義やプロパティの種類によって異なる場合があります。 挙動の違いによる影響 形式 JSON-LDパーサーの解釈 { "@id": "..." } URI参照(他ノードへのリンク) "..." リテラル文字列 この違いにより、以下の影響が生じる可能性があります: グラフ構造のトラバーサルに影響 一部のSPARQLクエリ結果に影響 JSON-LDフレーミング処理に影響 型付きリテラルについて 同様に、xsd:dateTime などの型付きリテラルでも型情報が省略されるケースがあります。 期待される出力 : { "prov:startedAtTime": { "@value": "2025-01-15T10:30:00Z", "@type": "xsd:dateTime" } } 確認された出力 : { "prov:startedAtTime": "2025-01-15T10:30:00Z" } 回避策 アプローチ:TTL形式で取得してJSON-LDを構築 DydraはTurtle (TTL) 形式では正確にシリアライズするため、以下の戦略を採用しました: [クライアント] │ │ Accept: text/turtle v [Dydra SPARQL Endpoint] │ │ TTL形式で返却 v [n3パーサー] │ │ Quadsに変換 v [JSON-LD構築ロジック] │ │ 正しいJSON-LD v [アプリケーション] 実装 import { Parser } from "n3"; /** * TTLをパースしてJSON-LDに変換 * DydraのJSON-LDシリアライゼーションの挙動を回避 */ function turtleToJsonLd(turtle: string): RDFGraph { const parser = new Parser(); const quads = parser.parse(turtle); // Subject別にトリプルをグループ化 const subjects = new Map<string, Map<string, unknown[]>>(); for (const quad of quads) { const subjectId = quad.subject.value; if (!subjects.has(subjectId)) { subjects.set(subjectId, new Map()); } const predicates = subjects.get(subjectId)!; const predicateId = quad.predicate.value; if (!predicates.has(predicateId)) { predicates.set(predicateId, []); } // オブジェクトの値を型情報付きで構築 let objectValue: unknown; if (quad.object.termType === "NamedNode") { // URI参照: { "@id": "..." } ← ここがポイント objectValue = { "@id": quad.object.value }; } else if (quad.object.termType === "Literal") { const literal = quad.object; if (literal.language) { // 言語タグ付きリテラル objectValue = { "@value": literal.value, "@language": literal.language }; } else if (literal.datatype && literal.datatype.value !== "http://www.w3.org/2001/XMLSchema#string") { // 型付きリテラル(xsd:string以外) objectValue = { "@value": literal.value, "@type": literal.datatype.value }; } else { // プレーンリテラル objectValue = literal.value; } } else if (quad.object.termType === "BlankNode") { objectValue = { "@id": `_:${quad.object.value}` }; } else { objectValue = quad.object.value; } predicates.get(predicateId)!.push(objectValue); } // JSON-LD @graphを構築 const graph: Array<Record<string, unknown>> = []; for (const [subjectId, predicates] of subjects) { const node: Record<string, unknown> = { "@id": subjectId }; for (const [predicateId, objects] of predicates) { if (predicateId === "http://www.w3.org/1999/02/22-rdf-syntax-ns#type") { // @typeは特別扱い node["@type"] = objects.map((o) => { if (typeof o === "object" && o !== null && "@id" in o) { return (o as { "@id": string })["@id"]; } return o; }); } else { // 単一値の場合は配列から取り出す node[predicateId] = objects.length === 1 ? objects[0] : objects; } } graph.push(node); } return { "@context": JSONLD_CONTEXT, "@graph": graph, }; } 使用例 // TTL形式で取得して変換 const response = await fetch(`${DYDRA_ENDPOINT}/sparql`, { method: "POST", headers: { "Accept": "text/turtle", // TTLで取得 }, body: query, }); const turtle = await response.text(); const jsonld = turtleToJsonLd(turtle); // JSON-LDに変換 依存ライブラリ この回避策には n3 ライブラリが必要です: ...