Compare commits

..

1 Commits

Author SHA1 Message Date
Yaros 2db907239f fix: update ocr & faces after asset edit 2026-06-24 13:18:42 +02:00
7 changed files with 111 additions and 123 deletions
@@ -446,6 +446,7 @@ class SyncStreamService {
await _syncStreamRepository.updateAssetsV1([asset], debugLabel: 'websocket-edit');
await _syncStreamRepository.replaceAssetEditsV1(asset.id, assetEdits, debugLabel: 'websocket-edit');
await _refreshAssetOcrAndFaces(asset.id);
_logger.info(
'Successfully processed AssetEditReadyV1 event for asset ${asset.id} with ${assetEdits.length} edits',
@@ -484,6 +485,7 @@ class SyncStreamService {
await _syncStreamRepository.updateAssetsV2([asset], debugLabel: 'websocket-edit');
await _syncStreamRepository.replaceAssetEditsV1(asset.id, assetEdits, debugLabel: 'websocket-edit');
await _refreshAssetOcrAndFaces(asset.id);
_logger.info(
'Successfully processed AssetEditReadyV2 event for asset ${asset.id} with ${assetEdits.length} edits',
@@ -493,6 +495,22 @@ class SyncStreamService {
}
}
Future<void> _refreshAssetOcrAndFaces(String assetId) async {
try {
final ocr = await _api.assetsApi.getAssetOcr(assetId);
await _syncStreamRepository.replaceAssetOcr(assetId, ocr ?? const []);
} catch (error, stackTrace) {
_logger.severe("Error refreshing OCR for asset $assetId", error, stackTrace);
}
try {
final faces = await _api.facesApi.getFaces(assetId);
await _syncStreamRepository.replaceAssetFaces(assetId, faces ?? const []);
} catch (error, stackTrace) {
_logger.severe("Error refreshing faces for asset $assetId", error, stackTrace);
}
}
Future<void> _handleRemoteDeleted(Iterable<String> remoteIds) async {
if (remoteIds.isEmpty) {
return Future.value();
@@ -896,6 +896,71 @@ class SyncStreamRepository extends DriftDatabaseRepository {
}
}
/// Replaces all OCR rows for [assetId] with [data] (e.g. after an asset edit re-runs OCR).
Future<void> replaceAssetOcr(String assetId, Iterable<AssetOcrResponseDto> data) async {
try {
await _db.batch((batch) {
batch.deleteWhere(_db.assetOcrEntity, (row) => row.assetId.equals(assetId));
for (final ocr in data) {
batch.insert(
_db.assetOcrEntity,
AssetOcrEntityCompanion(
id: Value(ocr.id),
assetId: Value(ocr.assetId),
recognizedText: Value(ocr.text),
x1: Value(ocr.x1),
y1: Value(ocr.y1),
x2: Value(ocr.x2),
y2: Value(ocr.y2),
x3: Value(ocr.x3),
y3: Value(ocr.y3),
x4: Value(ocr.x4),
y4: Value(ocr.y4),
boxScore: Value(ocr.boxScore),
textScore: Value(ocr.textScore),
isVisible: const Value(true),
),
);
}
});
} catch (error, stack) {
_logger.severe('Error: replaceAssetOcr', error, stack);
rethrow;
}
}
Future<void> replaceAssetFaces(String assetId, Iterable<AssetFaceResponseDto> data) async {
try {
await _db.batch((batch) {
batch.deleteWhere(_db.assetFaceEntity, (row) => row.assetId.equals(assetId));
for (final face in data) {
batch.insert(
_db.assetFaceEntity,
AssetFaceEntityCompanion(
id: Value(face.id),
assetId: Value(assetId),
personId: Value(face.person?.id),
imageWidth: Value(face.imageWidth),
imageHeight: Value(face.imageHeight),
boundingBoxX1: Value(face.boundingBoxX1),
boundingBoxY1: Value(face.boundingBoxY1),
boundingBoxX2: Value(face.boundingBoxX2),
boundingBoxY2: Value(face.boundingBoxY2),
sourceType: Value(face.sourceType.orElse(null)?.value ?? SourceType.machineLearning.value),
isVisible: const Value(true),
deletedAt: const Value(null),
),
);
}
});
} catch (error, stack) {
_logger.severe('Error: replaceAssetFaces', error, stack);
rethrow;
}
}
Future<void> pruneAssets() async {
try {
await _db.transaction(() async {
+26 -2
View File
@@ -7,6 +7,7 @@ import 'package:immich_mobile/infrastructure/repositories/network.repository.dar
import 'package:immich_mobile/models/server_info/server_version.model.dart';
import 'package:immich_mobile/providers/auth.provider.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/infrastructure/ocr.provider.dart';
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/utils/debounce.dart';
@@ -181,11 +182,34 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
}
void _handleSyncAssetEditReadyV1(dynamic data) {
unawaited(_ref.read(backgroundSyncProvider).syncWebsocketEditV1(data));
final assetId = _assetIdFromEditReady(data);
unawaited(
_ref.read(backgroundSyncProvider).syncWebsocketEditV1(data).whenComplete(() => _onAssetEditApplied(assetId)),
);
}
void _handleSyncAssetEditReadyV2(dynamic data) {
unawaited(_ref.read(backgroundSyncProvider).syncWebsocketEditV2(data));
final assetId = _assetIdFromEditReady(data);
unawaited(
_ref.read(backgroundSyncProvider).syncWebsocketEditV2(data).whenComplete(() => _onAssetEditApplied(assetId)),
);
}
String? _assetIdFromEditReady(dynamic data) {
if (data is Map && data['asset'] is Map) {
final id = (data['asset'] as Map)['id'];
return id is String ? id : null;
}
return null;
}
/// The edit handler refreshes OCR/faces in the drift DB from a background isolate,
/// so the main-isolate UI providers must be invalidated here to re-read the new data.
void _onAssetEditApplied(String? assetId) {
if (assetId == null) {
return;
}
_ref.invalidate(ocrAssetProvider(assetId));
}
void _processBatchedAssetUploadReadyV1() {
+2
View File
@@ -36,6 +36,7 @@ class ApiService {
late MemoriesApi memoriesApi;
late SessionsApi sessionsApi;
late TagsApi tagsApi;
late FacesApi facesApi;
ApiService() {
// The below line ensures that the api clients are initialized when the service is instantiated
@@ -77,6 +78,7 @@ class ApiService {
memoriesApi = MemoriesApi(_apiClient);
sessionsApi = SessionsApi(_apiClient);
tagsApi = TagsApi(_apiClient);
facesApi = FacesApi(_apiClient);
}
Future<String> resolveAndSetEndpoint(String serverUrl) async {
-90
View File
@@ -183,96 +183,6 @@
},
"uiHints": ["Filter"]
},
{
"name": "assetDateFilter",
"title": "Filter by date",
"description": "Filter assets by date taken",
"types": ["AssetV1"],
"schema": {
"type": "object",
"properties": {
"startDate": {
"type": "object",
"title": "Start date",
"description": "Earliest date of assets to include",
"properties": {
"day": {
"type": "number",
"title": "Day",
"description": "Day of the year to match",
"uiHint": {
"order": 1
}
},
"month": {
"type": "number",
"title": "Month",
"description": "Month of the year to match",
"uiHint": {
"order": 2
}
},
"year": {
"type": "number",
"title": "Year",
"description": "Year to match",
"uiHint": {
"order": 3
}
}
},
"uiHint": {
"order": 1
}
},
"endDate": {
"type": "object",
"title": "End date",
"description": "Latest date of assets to include",
"properties": {
"day": {
"type": "number",
"title": "Day",
"description": "Day of the year to match",
"uiHint": {
"order": 1
}
},
"month": {
"type": "number",
"title": "Month",
"description": "Month of the year to match",
"uiHint": {
"order": 2
}
},
"year": {
"type": "number",
"title": "Year",
"description": "Year to match",
"uiHint": {
"order": 3
}
}
},
"uiHint": {
"order": 2
}
},
"recurring": {
"type": "boolean",
"default": false,
"title": "Match recurring dates",
"description": "Allow any assets with matching months/days regardless of the year",
"uiHint": {
"order": 3
}
}
},
"required": ["recurring", "startDate", "endDate"]
},
"uiHints": ["Filter"]
},
{
"name": "assetTypeFilter",
"title": "Filter by asset type",
-1
View File
@@ -14,7 +14,6 @@ declare module 'main' {
export function assetFileFilter(): I32;
export function assetMissingTimeZoneFilter(): I32;
export function assetLocationFilter(): I32;
export function assetDateFilter(): I32;
export function assetTypeFilter(): I32;
// updates
-30
View File
@@ -95,36 +95,6 @@ export const assetLocationFilter = () => {
});
};
export const assetDateFilter = () => {
return wrapper<
WorkflowType.AssetV1,
{
startDate: { month: number; day: number; year: number };
endDate: { month: number; day: number; year: number };
recurring: boolean;
}
>(({ config, data }) => {
const assetDate = new Date(data.asset.localDateTime);
let startDate = new Date(config.startDate.year, config.startDate.month - 1, config.startDate.day);
let endDate = new Date(config.endDate.year, config.endDate.month - 1, config.endDate.day);
if (config.recurring) {
startDate.setFullYear(assetDate.getFullYear());
endDate.setFullYear(assetDate.getFullYear());
if (endDate < startDate) {
if (assetDate > endDate) {
endDate.setFullYear(endDate.getFullYear() + 1);
} else {
startDate.setFullYear(startDate.getFullYear() - 1);
}
}
}
return { workflow: { continue: assetDate >= startDate && assetDate <= endDate } };
});
};
export const assetTypeFilter = () => {
return wrapper<WorkflowType.AssetV1, { allowedTypes: AssetTypeEnum[] }>(({ config, data }) => {
return { workflow: { continue: config.allowedTypes.includes(data.asset.type) } };