Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .agents/skills/ai-service/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,5 @@ description: 画像の自動タグ付け、類似度計算、CCIP特徴量など
|---|---|
| タグ付け機能の実装 | `tagging-service.ts` を経由して `RustAiClient` を呼び出し |
| 類似度計算 | CCIP特徴量を使用したサービス層の実装 |

CCIP抽出をjob化する場合は`job-system`、検索画面へ統合する場合は`media-search`も参照する。
34 changes: 34 additions & 0 deletions .agents/skills/job-system/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
name: job-system
description: solid-imagerのbackground job、JobWorker、job dispatch、AI concurrency、親子batch進捗、stale recoveryを扱う。job type追加、非同期処理、Managerのbatch操作、job event変更時に使用する。
---

# Job System

## データフロー

1. jobは`IJobRepository.create`または`createIfUnique`で投入する。
2. `apps/server/src/infrastructure/jobs/job-worker.ts`がAI jobとその他jobを別poolでclaimする。
3. `apps/server/src/application/services/job-dispatch-service.ts`がjob typeごとのhandlerへ振り分ける。
4. handlerはpayloadをZod schemaでparseしてから処理する。
5. UIへ進捗を出す場合は`RealtimeEventBus.publishJob`だけを使用する。

## Job追加時の必須更新

- dispatchへ明示的な分岐を追加する。未知jobは警告後に完了扱いになるため、登録漏れを残さない。
- AI推論を行うjobは`JobWorker.aiJobTypes`へ追加し、`jobs.aiConcurrency`の対象にする。
- payload schema、成功・失敗、claim pool、stale recoveryのunit testを追加する。
- source単位で直列化が必要なjobはLanceDB syncと同様にclaim条件とactive keyを実装する。
- media lifecycleから投入する場合はupload、watcher、copy/move、delete、bulk操作を監査する。

## Batch Job

- 親jobは進捗記録でありworkerに実行させない。`status: in_progress`で作成する。
- 子jobへ`parentId`を設定し、子の完了時に親payloadの`processed`を原子的に更新する。
- 親更新では`updatedAt`も更新する。
- job event schemaは`packages/core/src/domain/sources/events.ts`を唯一の正とする。
- clientは既存のjob event hookを再利用し、独自pollingやEventSourceを追加しない。

## 検証

`claimPending`のinclude/exclude、AI concurrency、親job非claim、進捗完了、失敗、stale job回復を確認する。
4 changes: 4 additions & 0 deletions .agents/skills/job-system/agents/openai.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
interface:
display_name: "Job System"
short_description: "solid-imagerのbackground job設計と実装規約"
default_prompt: "既存のjob worker、dispatch、進捗イベント設計に従って変更してください。"
37 changes: 37 additions & 0 deletions .agents/skills/media-search/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
name: media-search
description: solid-imagerのメディア検索schema、simple/pro/vector mode、shared search store、session persistence、preset、検索API、無限スクロールを扱う。検索条件・mode・sort・類似検索・検索画面変更時に使用する。
---

# Media Search

## 既存設計

- schemaと型の正は`packages/core/src/domain/search/`とmedia schemaに置く。
- stateの正は`packages/ui/src/stores/search-store.ts`。server/Tauriは同じstoreをre-exportする。
- 検索状態はstoreと`use-current-search-persistence`の`sessionStorage`で保持する。URL queryへ別系統の状態を追加しない。
- simple/pro変換はcoreのsearch logicを経由する。
- user presetとcurrent session stateを区別する。一時的な類似元などを通常presetへ混ぜない。
- server/Tauri共通表示は`packages/ui`へ置き、app側はAPI clientとroute wiringだけを持つ。

## 変更手順

1. Zod `SearchState`とdefault stateを更新する。
2. mode遷移、condition生成、preset復元への影響を確認する。
3. session保存対象と復元処理を同時に更新する。
4. oRPC contract、router、application/repositoryの順で検索処理を追加する。
5. `useSearchPage`のquery keyへ結果を変える全stateを含める。
6. paginationを使わないmodeではnext pageを返さない。
7. 大量画像は既存のvirtualized gridとlazy loadingを維持する。

## Vector類似検索

- `vector`はsimple/proと独立した第三modeとして扱う。
- 個別media画面はstoreへanchor IDを設定して`/search`へ通常遷移する。
- URL queryは使用しない。
- source filterとtopKを明示し、通常sortは適用しない。
- CCIPはキャラクター類似であり一般的な重複画像検索ではないことをUIに表示する。

## 検証

mode遷移、session復元、preset非汚染、query key、server/Tauri parity、空結果、エラー、無限scroll停止を確認する。
4 changes: 4 additions & 0 deletions .agents/skills/media-search/agents/openai.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
interface:
display_name: "Media Search"
short_description: "solid-imagerの検索状態・API・UI設計規約"
default_prompt: "既存の検索store、session persistence、API、共有UI設計に従って変更してください。"
2 changes: 2 additions & 0 deletions .agents/skills/solid-imager/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ bun run test
- UI 変更: `ui-components`, `tanstack-db`, 必要に応じて `modern-web-guidance`
- リアルタイムイベント/SSE 変更: `realtime-events`, `orpc-api`, `schema-driven-dev`
- AI/ML 連携: `ai-service`
- Background job・batch進捗: `job-system`
- メディア検索・検索状態・類似検索: `media-search`
- CLI 変更: `cli`
- ブラウザ拡張: `browser-extension`
- ログ実装・整理: `logging-rules`
Expand Down
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@
| `issue-driven` | GitHub Issue駆動開発ワークフロー(証跡・進捗管理) | issueをベースに開発作業を始めるとき |
| `tanstack-db` | TanStack DBクライアントデータレイヤー(永続化、useLiveQuery、includes) | クライアント側データレイヤー変更時 |
| `realtime-events` | 型付きリアルタイムイベント、oRPC Event Iterator、pub/sub、再接続 | SSE・イベント配信・購読・イベントschema変更時 |
| `job-system` | background job、worker、dispatch、AI concurrency、batch親子進捗 | job type追加・非同期処理・batch操作変更時 |
| `media-search` | 検索schema、shared store、session persistence、preset、類似検索 | 検索条件・mode・検索画面変更時 |

<!--VITE PLUS START-->

Expand Down
156 changes: 156 additions & 0 deletions apps/server/public/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,32 @@
}
}
},
"/media/searchSimilar": {
"post": {
"operationId": "media.searchSimilar",
"summary": "searchSimilar",
"tags": [
"Media"
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"anyOf": [
{},
{
"not": {}
}
]
}
}
}
}
}
}
},
Comment thread
hmjn023 marked this conversation as resolved.
"/media/get": {
"post": {
"operationId": "media.get",
Expand Down Expand Up @@ -2070,6 +2096,32 @@
}
}
},
"/ai/ccipDistances": {
"post": {
"operationId": "ai.ccipDistances",
"summary": "ccipDistances",
"tags": [
"AI"
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"anyOf": [
{},
{
"not": {}
}
]
}
}
}
}
}
}
},
"/ai/scanBatchTaggingTargets": {
"post": {
"operationId": "ai.scanBatchTaggingTargets",
Expand Down Expand Up @@ -2148,6 +2200,110 @@
}
}
},
"/ai/ccipVectorStatus": {
"post": {
"operationId": "ai.ccipVectorStatus",
"summary": "ccipVectorStatus",
"tags": [
"AI"
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"anyOf": [
{},
{
"not": {}
}
]
}
}
}
}
}
}
},
"/ai/startCcipExtraction": {
"post": {
"operationId": "ai.startCcipExtraction",
"summary": "startCcipExtraction",
"tags": [
"AI"
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"anyOf": [
{},
{
"not": {}
}
]
}
}
}
}
}
}
},
"/ai/scanBatchCcipTargets": {
"post": {
"operationId": "ai.scanBatchCcipTargets",
"summary": "scanBatchCcipTargets",
"tags": [
"AI"
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"anyOf": [
{},
{
"not": {}
}
]
}
}
}
}
}
}
},
"/ai/startBatchCcipExtraction": {
"post": {
"operationId": "ai.startBatchCcipExtraction",
"summary": "startBatchCcipExtraction",
"tags": [
"AI"
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"anyOf": [
{},
{
"not": {}
}
]
}
}
}
}
}
}
},
"/ai/detectAndCropCharacters": {
"post": {
"operationId": "ai.detectAndCropCharacters",
Expand Down
27 changes: 27 additions & 0 deletions apps/server/src/application/services/ccip-vector-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { CcipVectorService } from "@solid-imager/application/services/ccip-vector-service";
import { services } from "~/application/registry";
import { taggingService } from "~/application/services/tagging-service";
import { LanceDbCcipVectorStore } from "~/infrastructure/ai/lancedb-ccip-vector-store";

let service: CcipVectorService | null = null;

export function getCcipVectorService(): CcipVectorService {
if (!service) {
const config = services.getConfigService().getConfig();
service = new CcipVectorService({
mediaRepository: services.getMediaRepository(),
sourceRepository: services.getSourceRepository(),
taggingService,
vectorStore: new LanceDbCcipVectorStore(config.lancedb.ccipVectorDir),
});
Comment thread
hmjn023 marked this conversation as resolved.
}
return service;
}

export const ccipVectorService = new Proxy({} as CcipVectorService, {
get(_target, property) {
const instance = getCcipVectorService();
const value = instance[property as keyof CcipVectorService];
return typeof value === "function" ? value.bind(instance) : value;
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import fs from "node:fs/promises";
import path from "node:path";
import { Glob } from "bun";
import { services } from "~/application/registry";
import { ccipVectorService } from "~/application/services/ccip-vector-service";
import { MediaProcessingService } from "~/application/services/media-processing-service";
import { RealtimeEventBus } from "~/infrastructure/events/realtime-event-bus";
import { deleteThumbnail } from "~/infrastructure/jobs/thumbnails";
Expand Down Expand Up @@ -60,6 +61,14 @@ async function processDeletions(
filesToDelete.map(async (fileToDelete) => {
try {
await MediaRepository.delete(fileToDelete.id);
try {
await ccipVectorService.delete(fileToDelete.id);
} catch (error) {
logger.warn(
{ err: error, mediaId: fileToDelete.id },
"Failed to delete CCIP vector during directory sync",
);
}
Comment thread
hmjn023 marked this conversation as resolved.
await deleteThumbnail(mediaSourceId, fileToDelete.id);
RealtimeEventBus.publishSource(mediaSourceId, "media-deleted", {
filePath: fileToDelete.relativePath,
Expand Down
5 changes: 5 additions & 0 deletions apps/server/src/application/services/job-dispatch-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ export async function processJob(job: DbJob) {
await processDownloadJob(job);
} else if (job.type === "auto_tagging") {
await processAutoTaggingJob(job);
} else if (job.type === "extract_ccip_vector") {
const { processCcipExtractionJob } = await import(
"~/infrastructure/jobs/ccip-jobs"
);
await processCcipExtractionJob(job);
Comment thread
hmjn023 marked this conversation as resolved.
} else if (job.type === "bulk_tagging_dispatch") {
await processBulkTaggingDispatchJob(job);
} else if (job.type === "sync_lancedb" || job.type === "sync_lancedb_full") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ export const syncLanceDB = service.syncLanceDB;
export const syncLanceDBPages = service.syncLanceDBPages;
export const syncLanceDBDelta = service.syncLanceDBDelta;
export const readFromLanceDB = service.readFromLanceDB;
export const readMediaIds = service.readMediaIds;
export const cleanupLanceDBDir = service.cleanupLanceDBDir;
Loading